feat(total-annihilation): add rocket trooper, constructor, and advanced vehicle plant
Introduce three new entities: - Rocket Trooper: anti-armor infantry with shoulder-mounted guided rocket - Constructor: unarmed tracked builder to replace lost commander build capacity - Advanced Vehicle Plant: 4×4 factory with doubled production bays Update AI composition weights and counter logic for the new units. Fix byDef map to use Object.create(null) so the "constructor" key doesn't shadow Object.prototype.constructor and silently poison gap calculations. Add procedural art shapes for all three, register proc shapes in TARules, update sprite frame assignments, and add core-structures assets.
This commit is contained in:
parent
c835096610
commit
39059a3551
Binary file not shown.
|
After Width: | Height: | Size: 1006 KiB |
Binary file not shown.
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue