-- Wireframe 3D models — vertex + edge data traced from the public 1980 arcade -- AVG ROM (vertex table at $388e, draw-command table at $2472, both -- documented by the 6502 disassembly preservation project). -- -- Axis translation: the arcade uses +X forward, +Y right, +Z up. We use -- +X right, +Y up, +Z forward. The arcade math box effectively halves X/Z -- at draw time (visualizer's X and Z, which are arcade's right-axis and -- forward-axis), so those are stored 2× and we halve here. Arcade's Z (up) -- axis is stored at native scale. -- -- All models pre-scaled ×0.025 to fit our world; bottoms shifted to Y=0 -- except the shell, which is centred so it can fly at arbitrary Y. local Models = {} local function buildModel(verts, lines, scale) scale = scale or 1 local edges = {} for _, pair in ipairs(lines) do local a, b = verts[pair[1]], verts[pair[2]] edges[#edges + 1] = { a[1] * scale, a[2] * scale, a[3] * scale, b[1] * scale, b[2] * scale, b[3] * scale, } end return edges end -- Slow (enemy) tank — arcade ROM shape 2 — 24v, 38e. -- Stepped hull, moderate height, forward-extending turret / antenna. local tankVerts = { { 6.40, 0.00, -9.20}, { -6.40, 0.00, -9.20}, { -6.40, 0.00, 12.10}, { 6.40, 0.00, 12.10}, { 7.10, 2.80, -12.80}, { -7.10, 2.80, -12.80}, { -7.10, 2.80, 15.60}, { 7.10, 2.80, 15.60}, { 4.30, 5.00, -8.50}, { -4.30, 5.00, -8.50}, { -4.30, 5.00, 8.50}, { 4.30, 5.00, 8.50}, { 2.10, 9.20, -6.40}, { -2.10, 9.20, -6.40}, { 0.50, 7.80, -1.60}, { -0.50, 7.80, -1.60}, { -0.50, 6.80, 1.60}, { 0.50, 6.80, 1.60}, { -0.50, 7.80, 14.00}, { -0.50, 6.80, 14.00}, { 0.50, 7.80, 14.00}, { 0.50, 6.80, 14.00}, { 0.00, 9.20, -6.40}, { 0.00, 10.00, -6.40}, } local tankLines = { {24,23},{13,14},{15,21},{21,19},{19,16},{16,15},{15,18},{18,17}, {17,20},{20,22},{22,18},{16,17},{20,19},{21,22},{4,1},{1,5}, {5,8},{8,7},{7,3},{3,4},{4,8},{8,12},{12,11},{11,7}, {7,6},{6,10},{10,11},{11,14},{14,10},{10,9},{9,12},{12,13}, {13,9},{9,5},{5,6},{6,2},{2,3},{2,1}, } Models.tank = buildModel(tankVerts, tankLines) -- Super tank — arcade ROM shape 33 — 25v, 34e. -- Similar stepped profile but slightly taller and with rear antenna mount. local supertankVerts = { { -4.60, 0.00, 18.20}, { -6.90, 0.00, -5.70}, { 6.90, 0.00, -5.70}, { 4.60, 0.00, 18.20}, { -5.70, 5.70, -5.70}, { 5.70, 5.70, -5.70}, { 0.00, 1.10, 13.70}, { -3.40, 5.10, -3.40}, { -3.40, 5.70, -5.70}, { 3.40, 5.70, -5.70}, { 3.40, 5.10, -3.40}, { -2.30, 9.10, -3.40}, { -2.30, 9.10, -5.70}, { 2.30, 9.10, -5.70}, { 2.30, 9.10, -3.40}, { -1.10, 6.90, 16.00}, { -1.10, 6.90, 1.10}, { 1.10, 6.90, 1.10}, { 1.10, 6.90, 16.00}, { -1.10, 8.00, 16.00}, { -1.10, 8.00, -1.10}, { 1.10, 8.00, -1.10}, { 1.10, 8.00, 16.00}, { 0.00, 9.10, -5.70}, { 0.00, 14.90, -5.70}, } local supertankLines = { {1,2},{2,5},{5,1},{1,4},{4,3},{3,6},{6,4},{3,2}, {5,6},{10,11},{11,7},{7,15},{15,14},{14,10},{10,9},{9,8}, {8,7},{7,12},{12,13},{13,9},{13,14},{15,12},{20,23},{23,22}, {22,21},{21,17},{17,16},{16,19},{19,18},{18,17},{16,20},{23,19}, {18,22},{24,25}, } Models.supertank = buildModel(supertankVerts, supertankLines) -- Guided missile — arcade ROM shape 22 (type $16) — 26v, 43e. local missileVerts = { { -1.80, 4.20, -4.80}, { -0.90, 5.40, -4.80}, { 0.90, 5.40, -4.80}, { 1.80, 4.20, -4.80}, { 0.90, 3.00, -4.80}, { -0.90, 3.00, -4.80}, { -3.60, 4.20, -1.20}, { -2.40, 6.60, -1.20}, { 2.40, 6.60, -1.20}, { 3.60, 4.20, -1.20}, { 2.40, 1.80, -1.20}, { -2.40, 1.80, -1.20}, { 0.00, 4.20, 14.40}, { 0.00, 4.20, 17.40}, { 1.80, 0.00, -1.80}, { -1.80, 0.00, -1.80}, { -1.80, 0.00, 1.80}, { 1.80, 0.00, 1.80}, { 0.60, 1.90, -0.60}, { -0.60, 1.90, -0.60}, { -0.60, 2.10, 0.60}, { 0.60, 2.10, 0.60}, { 0.00, 6.60, -1.20}, { -0.90, 5.40, 6.60}, { 0.90, 5.40, 6.60}, { 0.00, 7.80, 0.60}, } local missileLines = { {14,13},{13,7},{7,1},{1,2},{2,8},{8,9},{9,10},{10,11}, {11,12},{12,7},{7,8},{8,13},{13,9},{9,3},{3,4},{4,10}, {10,13},{13,11},{11,5},{5,6},{6,12},{12,13},{25,24},{24,23}, {23,25},{25,26},{26,24},{26,23},{2,3},{4,5},{6,1},{19,20}, {20,21},{21,22},{22,19},{19,15},{15,16},{16,17},{17,18},{18,15}, {16,20},{21,17},{18,22}, } Models.missile = buildModel(missileVerts, missileLines) -- Saucer / UFO — arcade ROM shape 32 (type $20) — 17v, 32e. local saucerVerts = { { 0.00, 0.00, -3.00}, { -2.00, 0.00, -2.00}, { -3.00, 0.00, 0.00}, { -2.00, 0.00, 2.00}, { 0.00, 0.00, 3.00}, { 2.00, 0.00, 2.00}, { 3.00, 0.00, 0.00}, { 2.00, 0.00, -2.00}, { 0.00, 3.00, -12.00}, { -8.50, 3.00, -8.50}, { -12.00, 3.00, 0.00}, { -8.50, 3.00, 8.50}, { 0.00, 3.00, 12.00}, { 8.50, 3.00, 8.50}, { 12.00, 3.00, 0.00}, { 8.50, 3.00, -8.50}, { 0.00, 8.00, 0.00}, } local saucerLines = { {17,9},{9,10},{10,17},{17,11},{11,12},{12,17},{17,13},{13,14}, {14,17},{17,15},{15,16},{16,17},{1,8},{8,16},{16,9},{9,1}, {1,2},{2,10},{10,11},{11,3},{3,4},{4,12},{12,13},{13,5}, {5,6},{6,14},{14,15},{15,7},{7,8},{7,6},{5,4},{3,2}, } Models.ufo = buildModel(saucerVerts, saucerLines) -- Low/wide pyramid obstacle — arcade ROM shape 12 — 5v, 8e. local pyramidVerts = { { 10.00, 0.00, -10.00}, { -10.00, 0.00, -10.00}, { -10.00, 0.00, 10.00}, { 10.00, 0.00, 10.00}, { 0.00, 18.00, 0.00}, } local pyramidLines = { {1,5},{5,2},{2,1},{1,4},{4,5},{5,3},{3,4},{3,2}, } Models.pyramid = buildModel(pyramidVerts, pyramidLines) -- Tall / narrower pyramid obstacle — arcade ROM shape 0 — 5v, 8e. local pyramidTallVerts = { { 6.40, 0.00, -6.40}, { -6.40, 0.00, -6.40}, { -6.40, 0.00, 6.40}, { 6.40, 0.00, 6.40}, { 0.00, 16.00, 0.00}, } local pyramidTallLines = { {1,5},{5,2},{2,1},{1,4},{4,5},{5,3},{3,4},{3,2}, } Models.pyramidTall = buildModel(pyramidTallVerts, pyramidTallLines) -- Tall block / cube — arcade ROM shape 1 — 8v, 12e. -- Taller than wide — matches arcade's "tall box" obstacle. local cubeVerts = { { 6.40, 0.00, -6.40}, { -6.40, 0.00, -6.40}, { -6.40, 0.00, 6.40}, { 6.40, 0.00, 6.40}, { 6.40, 16.00, -6.40}, { -6.40, 16.00, -6.40}, { -6.40, 16.00, 6.40}, { 6.40, 16.00, 6.40}, } local cubeLines = { {1,2},{2,3},{3,4},{4,1},{1,5},{5,6},{6,7},{7,8}, {8,5},{6,2},{3,7},{8,4}, } Models.cube = buildModel(cubeVerts, cubeLines) -- Low block / slab — arcade ROM shape 15 — 8v, 12e. local halfPrismVerts = { { 8.00, 0.00, -8.00}, { -8.00, 0.00, -8.00}, { -8.00, 0.00, 8.00}, { 8.00, 0.00, 8.00}, { 8.00, 7.00, -8.00}, { -8.00, 7.00, -8.00}, { -8.00, 7.00, 8.00}, { 8.00, 7.00, 8.00}, } local halfPrismLines = { {1,2},{2,3},{3,4},{4,1},{1,5},{5,6},{6,7},{7,8}, {8,5},{6,2},{3,7},{8,4}, } Models.halfPrism = buildModel(halfPrismVerts, halfPrismLines) -- Tank shell — the arcade draws in-flight shells as a single bright point, -- so there's no ROM model to trace. We use a small square-base pyramid with -- the apex pointing forward (+Z) so the player can actually see their shot. local shellVerts = { { 2.0, 1.5, 0.0}, { 2.0, -1.5, 0.0}, { -2.0, -1.5, 0.0}, { -2.0, 1.5, 0.0}, { 0.0, 0.0, 8.0}, } local shellLines = { {1,2},{2,3},{3,4},{4,1}, {1,5},{2,5},{3,5},{4,5}, } Models.shell = buildModel(shellVerts, shellLines) -- Debris — three shrapnel shapes used by the tank-destruction explosion. local debris1Verts = { { 0, -3.9, 0}, { 0, 0, 3}, { 0, 3, -0.75}, { 0, 1.5, -2.1}, { 3, 0, 0}, {-3, 0, -0.3}, } local debris1Lines = { {1,2},{2,3},{3,4},{4,1}, {1,5},{2,5},{3,5},{4,5}, {1,6},{2,6},{3,6},{4,6}, } Models.debris1 = buildModel(debris1Verts, debris1Lines) local debris2Verts = { { 0, -3, 9}, { 2.1, -3, 7.5}, { 1.5, -3, 0}, { 0.6, -3, -0.75}, {-1.2, -3, 6}, { 0, 0, 1.5}, { 0, 4.5, 0}, } local debris2Lines = { {1,2},{2,3},{3,4},{4,5},{5,1}, {1,6},{6,7},{7,4},{7,3}, } Models.debris2 = buildModel(debris2Verts, debris2Lines) local debris3Verts = { { 0, 3, 12}, { 0, 3, -3}, { 1.5, 0, 11.1}, { 1.5, 0, 0}, { 0, 0, -3}, {-1.5, 0, 0}, {-1.5, 0, 10.5}, { 0, -3, 9}, { 0, -3, 0}, { 0, -6, 0}, { 1.5, -6, 0}, { 0, -6, -3}, {-1.5, -6, 0}, } local debris3Lines = { {1,2}, {3,4},{4,5},{5,6},{6,7}, {8,9}, {1,8},{1,3},{1,7},{3,8},{7,8}, {9,10}, {10,11},{11,12},{12,13},{13,10}, {11,4},{13,6},{12,5}, {4,9},{6,9}, {2,5},{2,4},{2,6}, } Models.debris3 = buildModel(debris3Verts, debris3Lines) Models.debrisPool = { Models.debris1, Models.debris2, Models.debris3 } return Models