diff --git a/assets/images/ta/core-structures.png b/assets/images/ta/core-structures.png new file mode 100644 index 0000000..baf2bff Binary files /dev/null and b/assets/images/ta/core-structures.png differ diff --git a/assets/images/ta/core-structures.psd b/assets/images/ta/core-structures.psd new file mode 100644 index 0000000..9750a4f Binary files /dev/null and b/assets/images/ta/core-structures.psd differ diff --git a/data/totalannihilation-rules.json b/data/totalannihilation-rules.json index 0111b8b..6465b3e 100644 --- a/data/totalannihilation-rules.json +++ b/data/totalannihilation-rules.json @@ -366,6 +366,38 @@ "sound": "sfx-ta-rocket-1", "impactSound": "sfx-ta-rocket-2" }, + { + "id": "shoulderrocket", + "name": "Shoulder Rocket", + "kind": "guided", + "damage": 32, + "reload": 2.2, + "burst": 1, + "range": 320, + "minRange": 60, + "speed": 260, + "turnRate": 2.0, + "aoe": 24, + "aoeFalloff": 0.15, + "targets": [ + "ground" + ], + "armorMul": { + "infantry": 0.35, + "light": 0.8, + "medium": 1.1, + "heavy": 1.3, + "structure": 1.0 + }, + "fx": { + "style": "rocket", + "color": "#ff9a5c", + "width": 1.5, + "trail": true + }, + "sound": "sfx-ta-rocket-1", + "impactSound": "sfx-ta-rocket-2" + }, { "id": "commanderlaser", "name": "Commander Laser", @@ -596,6 +628,36 @@ "spritePx": 30, "moveSound": "sfx-march" }, + { + "id": "rockettrooper", + "name": "Rocket Trooper", + "role": "combat", + "size": "small", + "radius": 16, + "hp": 240, + "speed": 50, + "turnRate": 5.5, + "moveClass": "foot", + "armorClass": "infantry", + "sight": 280, + "cost": { + "energy": 220, + "mass": 45 + }, + "buildTime": 5, + "builtBy": [ + "barracks" + ], + "weapons": [ + "shoulderrocket" + ], + "sheetSlot": "unitSheet", + "frame": 10, + "procShape": "rocketTrooper", + "icon": 6, + "spritePx": 30, + "moveSound": "sfx-march" + }, { "id": "jeep", "name": "Jeep", @@ -688,6 +750,44 @@ "icon": 5, "spritePx": 56, "moveSound": "sfx-engine-heavy" + }, + { + "id": "constructor", + "name": "Construction Vehicle", + "role": "builder", + "size": "medium", + "radius": 24, + "hp": 700, + "speed": 85, + "turnRate": 2.4, + "moveClass": "tread", + "armorClass": "light", + "sight": 340, + "cost": { + "energy": 500, + "mass": 140 + }, + "buildTime": 14, + "builtBy": [ + "vehicleplant" + ], + "buildPower": 90, + "buildRange": 180, + "builds": [ + "energygen", + "massgen", + "barracks", + "vehicleplant", + "lasertower", + "missilelauncher", + "advancedvehicleplant" + ], + "sheetSlot": "unitSheet", + "frame": 11, + "procShape": "constructor", + "icon": 7, + "spritePx": 50, + "moveSound": "sfx-engine-heavy" } ], "buildings": [ @@ -773,7 +873,8 @@ "buildPower": 100, "builds": [ "infantry", - "sniper" + "sniper", + "rockettrooper" ], "spawnOffset": { "x": 0, @@ -804,7 +905,8 @@ "builds": [ "jeep", "tank", - "rockettank" + "rockettank", + "constructor" ], "spawnOffset": { "x": 0, @@ -877,6 +979,37 @@ "buildFrame": 11, "procShape": "missileLauncher", "icon": 15 + }, + { + "id": "advancedvehicleplant", + "name": "Advanced Vehicle Plant", + "footprint": { + "w": 4, + "h": 4 + }, + "hp": 6600, + "armorClass": "structure", + "sight": 240, + "cost": { + "energy": 2400, + "mass": 900 + }, + "buildTime": 55, + "buildPower": 140, + "builds": [ + "jeep", + "tank", + "rockettank" + ], + "spawnOffset": { + "x": 0, + "y": 2.6 + }, + "sheetSlot": "structureSheet", + "frame": 12, + "buildFrame": 13, + "procShape": "advancedVehiclePlant", + "icon": 16 } ], "commandIcons": { diff --git a/src/games/totalannihilation/TAAI.js b/src/games/totalannihilation/TAAI.js index 6598599..7f21125 100644 --- a/src/games/totalannihilation/TAAI.js +++ b/src/games/totalannihilation/TAAI.js @@ -16,6 +16,10 @@ import { worldToTileX, worldToTileY } from './TANav.js'; /** Desired force mix per skill. Below skill 3 the AI just buys whatever it can afford. */ const COMPOSITION = { infantry: 0.34, sniper: 0.10, jeep: 0.06, tank: 0.34, rockettank: 0.16, + // The new anti-armour infantry and the unarmed construction vehicle: a modest slice of the + // mix for the trooper, and just enough of the vehicle to occasionally replace a lost + // Commander's construction capacity — not a real combat unit, so it stays a low priority. + rockettrooper: 0.14, constructor: 0.04, }; function memFor(state, armyIdx) { @@ -120,7 +124,15 @@ function observe(ctx) { function gatherOwn(ctx) { const { state, armyIdx, rules } = ctx; - const out = { commanders: [], builders: [], factories: [], buildings: [], units: [], sites: [], byDef: {} }; + // Object.create(null): a unit id of "constructor" (or any other Object.prototype member) as + // a plain-object key would otherwise shadow the inherited property instead of adding one, + // so `byDef['constructor'] ?? 0` reads back the Object constructor FUNCTION — a truthy, + // non-nullish value the ?? guard doesn't catch — and every arithmetic use of it silently + // becomes NaN from tick one, which poisons every gap comparison built.reduce touches. + const out = { + commanders: [], builders: [], factories: [], buildings: [], units: [], sites: [], + byDef: Object.create(null), + }; for (const e of state.entities) { if (e.dead || e.army !== armyIdx) continue; const def = rules.defById[e.defId]; @@ -364,6 +376,7 @@ function applyCounters(ctx, weights) { weights.tank *= 1 + (1 - armourShare) * 0.8; weights.rockettank *= 1 + armourShare * 1.0 + (structures > 2 ? 0.3 : 0); weights.sniper *= 1 + (1 - armourShare) * 0.5; + weights.rockettrooper *= 1 + armourShare * 0.6; } // --------------------------------------------------------------------------- diff --git a/src/games/totalannihilation/TAArt.js b/src/games/totalannihilation/TAArt.js index af1ca02..a800760 100644 --- a/src/games/totalannihilation/TAArt.js +++ b/src/games/totalannihilation/TAArt.js @@ -146,6 +146,22 @@ const UNIT_SHAPES = { ctx.restore(); }, + rocketTrooper(ctx, S) { + const c = S / 2; + ctx.save(); ctx.translate(c, c); + roundRect(ctx, -8, -5, 16, 10, 5); fillStroke(ctx, BODY); + ctx.beginPath(); ctx.arc(1, 0, 4.5, 0, Math.PI * 2); fillStroke(ctx, BODY_DARK); + // shoulder-mounted launch tube, angled up off the body line so it reads distinct from + // the rifle silhouette even at a glance. + ctx.strokeStyle = OUTLINE; ctx.lineWidth = 4.5; + ctx.beginPath(); ctx.moveTo(2, -3); ctx.lineTo(17, -8); ctx.stroke(); + ctx.strokeStyle = ACCENT; ctx.lineWidth = 2; + ctx.beginPath(); ctx.moveTo(2, -3); ctx.lineTo(17, -8); ctx.stroke(); + ctx.fillStyle = HOT; + ctx.beginPath(); ctx.arc(17, -8, 2.2, 0, Math.PI * 2); ctx.fill(); + ctx.restore(); + }, + jeep(ctx, S) { const c = S / 2; ctx.save(); ctx.translate(c, c); @@ -199,6 +215,20 @@ const UNIT_SHAPES = { } ctx.restore(); }, + + // Unarmed — no turret frame. The hull is the same tread-vehicle base as the combat tanks, + // topped with a folded nanolathe crane instead of a gun so it reads as a builder at a glance. + constructor(ctx, S) { + tankHull(ctx, S, 32, 26); + const c = S / 2; + ctx.save(); ctx.translate(c, c); + roundRect(ctx, -6, -8, 14, 16, 3); fillStroke(ctx, BODY_DARK); + ctx.strokeStyle = ACCENT; ctx.lineWidth = 3; + ctx.beginPath(); ctx.moveTo(2, 0); ctx.lineTo(15, 0); ctx.stroke(); + ctx.fillStyle = HOT; + ctx.beginPath(); ctx.arc(16, 0, 2.5, 0, Math.PI * 2); ctx.fill(); + ctx.restore(); + }, }; function tankHull(ctx, S, len, wid) { @@ -378,6 +408,42 @@ const STRUCT_SHAPES = { ctx.moveTo(p + w * 0.24, p + h - 8); ctx.lineTo(p + w * 0.76, p + h - 8); ctx.stroke(); }, + + // The 4x4 footprint's extra width goes to a SECOND bay door — twice the production, which + // is the whole pitch of the "advanced" tier — plus a trim line the basic plant doesn't have. + advancedVehiclePlant(ctx, S) { + const p = S * 0.06, w = S - p * 2, h = S - p * 2; + roundRect(ctx, p, p, w, h, S * 0.04); fillStroke(ctx, BODY, OUTLINE, 3); + // twin gantry rails across the roof + ctx.fillStyle = BODY_DARK; + ctx.fillRect(p + 4, p + h * 0.10, w - 8, h * 0.05); + ctx.fillRect(p + 4, p + h * 0.18, w - 8, h * 0.05); + ctx.fillStyle = '#3b3f47'; + for (let i = 0; i < 7; i++) ctx.fillRect(p + 8 + i * ((w - 16) / 7), p + h * 0.10, 4, h * 0.13); + // two roll-up bay doors side by side, each slatted like the basic plant's single door + const dw = w * 0.30, dy = p + h * 0.36, dh = h * 0.46; + const doorX = [p + w * 0.16, p + w * 0.54]; + for (const dx of doorX) { + ctx.fillStyle = '#2f3238'; + ctx.fillRect(dx, dy, dw, dh); + ctx.strokeStyle = '#4a4e56'; ctx.lineWidth = 2; + for (let i = 1; i < 6; i++) { + const y = dy + (dh * i) / 6; + ctx.beginPath(); ctx.moveTo(dx + 2, y); ctx.lineTo(dx + dw - 2, y); ctx.stroke(); + } + } + // accent trim along the roofline marks the upgraded facility + ctx.strokeStyle = ACCENT; ctx.lineWidth = 3; + ctx.beginPath(); ctx.moveTo(p + 6, p + 6); ctx.lineTo(p + w - 6, p + 6); ctx.stroke(); + // floor chevron out of each mouth, same language as the basic plant + ctx.strokeStyle = HOT; ctx.lineWidth = 4; + for (const dx of doorX) { + const mx = dx + dw / 2; + ctx.beginPath(); + ctx.moveTo(mx - 12, p + h - 18); ctx.lineTo(mx, p + h - 8); ctx.lineTo(mx + 12, p + h - 18); + ctx.stroke(); + } + }, }; // --------------------------------------------------------------------------- diff --git a/src/games/totalannihilation/TARules.js b/src/games/totalannihilation/TARules.js index cf779ae..10f6de8 100644 --- a/src/games/totalannihilation/TARules.js +++ b/src/games/totalannihilation/TARules.js @@ -20,8 +20,9 @@ const SIGHT_RANGE_MARGIN_TILES = 2; // Procedural painter shapes. A def may only name a shape TAArt knows how to draw; // this set is duplicated there and the verify script asserts the two agree. export const PROC_SHAPES = new Set([ - 'commander', 'infantry', 'sniper', 'jeep', 'tank', 'rockettank', + 'commander', 'infantry', 'sniper', 'rocketTrooper', 'jeep', 'tank', 'rockettank', 'constructor', 'energyGen', 'massGen', 'barracks', 'vehiclePlant', 'laserTower', 'missileLauncher', + 'advancedVehiclePlant', ]); function fail(msg) { diff --git a/src/games/totalannihilation/sprites.md b/src/games/totalannihilation/sprites.md index 367c958..d22f8fb 100644 --- a/src/games/totalannihilation/sprites.md +++ b/src/games/totalannihilation/sprites.md @@ -58,6 +58,8 @@ let the scale do the work. | 7 | Tank turret | turret + barrel | 54 | | 8 | Rocket Tank | hull + tracks | 56 | | 9 | Rocket Tank turret | rocket pod | 56 | +| 10 | Rocket Trooper | whole figure, shoulder-mounted launch tube | 30 | +| 11 | Construction Vehicle | tracked hull, folded nanolathe crane, no gun | 50 | **Turret frames** are drawn as a separate image stacked on the hull and rotated independently, so the unit aims while it drives. Rules: @@ -65,10 +67,11 @@ independently, so the unit aims while it drives. Rules: - The turret's **pivot is the centre of the cell** — draw the turret so its rotation point sits at (32, 32), with the barrel extending to the **right**. - Leave the rest of the turret cell transparent. The hull shows through it. -- A unit with no turret (infantry, sniper) simply has no turret frame; its whole sprite - rotates to face its target. +- A unit with no turret (infantry, sniper, rocket trooper, construction vehicle) simply has + no turret frame; its whole sprite rotates to face its target. The Construction Vehicle is + unarmed, so it never turns to aim regardless — it just rotates to face wherever it's moving. -Frames 10–63 are free. Add a unit by appending a definition to `units[]` in +Frames 12–63 are free. Add a unit by appending a definition to `units[]` in `data/totalannihilation-rules.json` with its `frame` (and optional `turretFrame`) — no code. --- @@ -99,12 +102,14 @@ build completes; the finished frame crossfades in underneath starting at 20% pro | 9 | Laser Tower — under construction | 1×1 | | | 10 | Missile Launcher | 2×2 | bunker + launch rack; draw it pointing **up** | | 11 | Missile Launcher — under construction | 2×2 | | +| 12 | Advanced Vehicle Plant | 4×4 | wider bay, **two** doors side by side | +| 13 | Advanced Vehicle Plant — under construction | 4×4 | | Defensive structures never rotate, and their mount traverses freely in the simulation — they shoot in every direction regardless of how the art is drawn. Draw them facing **up** and do not imply a firing arc. -Frames 12–35 are free. +Frames 14–35 are free. ---