Compare commits

..

No commits in common. "3c7184564aa308830bd38926b6abb7f080d3ac46" and "09a62f12cce07c02976441b9873d24999c5276ab" have entirely different histories.

24 changed files with 310 additions and 989 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Binary file not shown.

View File

@ -38,7 +38,7 @@
"arm-structures": {
"key": "ta-arm-structures", "path": "assets/images/ta/arm-structures.png", "kind": "structure",
"frameWidth": 192, "frameHeight": 192, "cols": 6,
"rows": 6
"rows": 4
},
"core-units": {
"key": "ta-core-units", "path": "assets/images/ta/core-units.png", "kind": "unit",
@ -48,7 +48,7 @@
"core-structures": {
"key": "ta-core-structures", "path": "assets/images/ta/core-structures.png", "kind": "structure",
"frameWidth": 192, "frameHeight": 192, "cols": 6,
"rows": 6
"rows": 4
},
"terrain-grasslands": {
"key": "ta-terrain-grasslands", "path": "assets/images/ta/terrain-grasslands.png", "kind": "terrain",
@ -67,7 +67,7 @@
},
"icons": {
"key": "ta-icons", "path": null, "kind": "icon",
"frameWidth": 64, "frameHeight": 64, "cols": 10,
"frameWidth": 44, "frameHeight": 44, "cols": 10,
"rows": 4
}
},

View File

@ -598,7 +598,7 @@
"id": "m05",
"name": "Tropic Vice",
"theme": "tropics",
"seed": 67890,
"seed": 71644,
"playerArmy": "arm",
"playerCommander": "vance",
"enemies": [
@ -628,7 +628,7 @@
{
"speaker": "ethel",
"mood": "idle",
"text": "Cy-Bro starts ahead and pushes early. Rockets crack their base open — armour holds the line getting there."
"text": "Cy-Bro starts ahead and pushes early. Rocket tanks out-range everything CORE fields."
}
],
"victoryLine": {
@ -646,102 +646,102 @@
"h": 96,
"theme": "tropics",
"rows": [
"...............rr.rr.............rr....rrrr~~~....~~~rrrr....rr.............rr.rr...............",
"r.............rr..rr...................rrr~~~~~..~~~~~rrr...................rr..rr.............r",
"..........rrrrrr...rrr...................r~~~~~..~~~~~r...................rrr...rrrrrr..........",
".........rrrrr.......rr..................~~~~~~~~~~~~~~..................rr.......rrrrr.........",
".......rr^^^^^............................~~~~~~~~~~~~............................^^^^^rr.......",
"...r.^^^^^^^^^^..................................................................^^^^^^^^^^.r...",
"...r^^^^^^^^^^^^................................................................^^^^^^^^^^^^r...",
"..r^^^^^^^^^^^^^^^^...rr.........^r..........................r^.........rr...^^^^^^^^^^^^^^^^r..",
"..^^^^^^^^^^^^^^^^^^^^^rrrr...r^^^^..........................^^^^r...rrrr^^^^^^^^^^^^^^^^^^^^^..",
".^^^^^^^^^^^^^^^^^^^^^^r.rr...^^^^^..........................^^^^^...rr.r^^^^^^^^^^^^^^^^^^^^^^.",
"^^^^^^^^^^^^^^^^^^^^^^^^..r...^^^^^^^.r..................r.^^^^^^^...r..^^^^^^^^^^^^^^^^^^^^^^^^",
"^^^^^^^^^^^^^^^^^^^^^^^^^^^....^^^^^^^^^................^^^^^^^^^....^^^^^^^^^^^^^^^^^^^^^^^^^^^",
"...^^^^..^.........^^^^^^^^.....^^^^^^^^^..............^^^^^^^^^.....^^^^^^^^.........^..^^^^...",
"...rrr.............^^^^^^^^.....^^^^^^^^^..............^^^^^^^^^.....^^^^^^^^.............rrr...",
"...........^^^.....^^^^^^^^....^^^^^^^^^................^^^^^^^^^....^^^^^^^^.....^^^...........",
"..........^^^^^....^^^^^^^.....^^^^^^^....................^^^^^^^.....^^^^^^^....^^^^^..........",
"..........^^^^^.....^^^^rr.....^^^^^^...rr............rr...^^^^^^.....rr^^^^.....^^^^^..........",
"...........^^^^....r^^^rrr.....^^^^^^..rrrr..........rrrr..^^^^^^.....rrr^^^r....^^^^...........",
"...........^^^^r...rr........r^^^^^^^^^rrrrr........rrrrr^^^^^^^^^r........rr...r^^^^...........",
"...........^^^^.r............^^^^^^^^^^^^^r..........r^^^^^^^^^^^^^............r.^^^^...........",
"...........^^^^^rr..........^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..........rr^^^^^...........",
"............^^..rr..........^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..........rr..^^............",
"....rrr.....................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.....................rrr....",
"....rrrr......................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^......................rrrr....",
"......r...........................^^^^^^^^^^^^^^^^^^^^^^^^^^^^...........................r......",
"...................................^^^^^^^^^^^^^^^^^^^^^^^^^^...................................",
"rrr.................................^^^^^^^^^^^^^^^^^^^^^^^^.................................rrr",
"..r..................................^......rrr..rrr......^..................................r..",
"........rrr..........................................................................rrr........",
"........rrr..........................................................................rrr........",
"........rrr......rr...........rr................................rr...........rr......rrr........",
"r.........r.....rr............rr...rr......................rr...rr............rr.....r.........r",
"................rr...........~..rrrrr......................rrrrr..~...........rr................",
"..................rrrr.......~~~.r.~~~~~~~............~~~~~~~.r.~~~.......rrrr..................",
"..................rrrr.......~~~...~~~~~~~~r........r~~~~~~~~...~~~.......rrrr..................",
"................rrrrrr.......~~......~~~~~~~~......~~~~~~~~......~~.......rrrrrr................",
"................rrrrr.................r..~~~~~....~~~~~..r.................rrrrr................",
".................rrr......................~~~~....~~~~......................rrr.................",
"..................rr....~.................~~~~~rr~~~~~.................~....rr..................",
"............rr....rr...r~.................~~~~~..~~~~~.................~r...rr....rr............",
"~r..........~~....rr...r~.................~~~~~..~~~~~.................~r...rr....~~..........r~",
"~~............~~~~rr...~~........^r.......~~~~~~~~~~~~.......r^........~~...rr~~~~............~~",
"~~..............~~rr..r~~.......^^^.......~~~~~~~~~~~~.......^^^.......~~r..rr~~..............~~",
"~~~..............~~.MMr~~.......^^^^......~~~~~~~~~~~~......^^^^.......~~rMM.~~..............~~~",
"~~~r.............~~.MM.~~......^^^^^....rr~~~~~~~~~~~~rr....^^^^^......~~.MM.~~.............r~~~",
"~~~...............~....~~......^^^^^....rr~~~~~~~~~~~~rr....^^^^^......~~....~...............~~~",
"~~~..MM...........~....~~......^^^^^....rr.~~~~..~~~~.rr....^^^^^......~~....~...........MM..~~~",
"~~r..MM...........r............^^^^^.......~~~....~~~.......^^^^^............r...........MM..r~~",
"~~..............................^^^^........rr....rr........^^^^..............................~~",
"~~...................rr.........^^^^........................^^^^.........rr...................~~",
"~.............MM....rrr.........^^^^........................^^^^.........rrr....MM.............~",
"..............MM....rrr.........^^^..........................^^^.........rrr....MM..............",
"................................^^^..........................^^^................................",
"................................^^^..........................^^^................................",
".................................^^r........................r^^.................................",
"...................................rr......................rr...................................",
"..........MM.......................rr......................rr.......................MM..........",
"..........MM........................................................................MM..........",
".....rr..................................................................................rr.....",
".....rr..................................................................................rr.....",
".....rr..................................................................................rr.....",
".....rr..................................................................................rr.....",
"....^rr.................~~~..........................................~~~.................rr^....",
"..^^^^^................~~~~~~~~....rr......................rr....~~~~~~~~................^^^^^..",
".^^^^^^^r.............~~~~~~~~~~...~~~~~~r.....rr.....r~~~~~~...~~~~~~~~~~.............r^^^^^^^.",
"^^^^^^^^^.............~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.............^^^^^^^^^",
"^^^^^^^^^.............~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.............^^^^^^^^^",
"^^^^^^^^^............~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~............^^^^^^^^^",
"^^^^^^^^^.............~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.............^^^^^^^^^",
"^^^^^^^^^....rr........~~~~~~~~~...~~~~~~~~~~~~~~~~~~~~~~~~~~...~~~~~~~~~........rr....^^^^^^^^^",
"..^^^^^^r....r.........r~~~~~~r....rr~~~~~~~~~~~~~~~~~~~~~~rr....r~~~~~~r.........r....r^^^^^^..",
"...^^^^......r...............rr....r....~~~~~~~~~~~~~~~~....r....rr...............r......^^^^...",
"....^^r......rr..............rr....rr.....~~~~~~~~~~~~.....rr....rr..............rr......r^^....",
".....rr.......rr.............rr.....rr....~~~~~~~~~~~~....rr.....rr.............rr.......rr.....",
".....rr......................rr......r.....~~~~..~~~~.....r......rr......................rr.....",
".....rr......................rr.^^^........~~~~rr~~~~........^^^.rr......................rr.....",
".....rr......................rr^^^^^.......~~~~rr~~~~.......^^^^^rr......................rr.....",
".....rr......................r^^^^^^.......~~~~..~~~~.......^^^^^^r......................rr.....",
"..............................^^^^^^.......~~~~..~~~~.......^^^^^^..............................",
"..............................^^^^^^.......~~~....~~~.......^^^^^^..............................",
".....................^r.......^^^^^^r.......~~....~~.......r^^^^^^.......r^.....................",
".....................^r........^^^^^r.......~~....~~.......r^^^^^........r^.....................",
"...............................^^^^rr......................rr^^^^...............................",
"................................^^^..........................^^^................................",
"................................^^^..........................^^^................................",
"...............................rr.......rrr..........rrr.......rr...............................",
"r..............................rr.......rrr..........rrr.......rr..............................r",
"r..............................rr.........r..........r.........rr..............................r",
".....rr........................r..........rr........rr..........r........................rr.....",
".....rr......................rr...........rrr......rrr...........rr......................rr.....",
"...........rr................rr............rrrr..rrrr............rr................rr...........",
"...........rr................rr............rrrr..rrrr............rr................rr...........",
"...........rr.....rr.........rr...........rrrr....rrrr...........rr.........rr.....rr...........",
"..................rr........................................................rr..................",
"..................rrr......................................................rrr..................",
"..................r.rr..........r..............................r..........rr.r.................."
"~~r....^^^^^^^^^^^^...........rr~~~...r...^^^^^^^^^^^^...r...~~~rr...........^^^^^^^^^^^^....r~~",
"~~.....^^^^^^^^^^^^...........r.~~~........^^^....^^^........~~~.r...........^^^^^^^^^^^^.....~~",
"~~....rr^^^^^^^^^^^............r~~~~.r....................r.~~~~r............^^^^^^^^^^^rr....~~",
"~~~......^^^...................~~~~~~~rr................rr~~~~~~~...................^^^......~~~",
"~~~~.....rr....................~~~~~~~~~................~~~~~~~~~....................rr.....~~~~",
"~~~~......rr.....................~~~~~~rr..............rr~~~~~~.....................rr......~~~~",
"~~~~......rr..............rr.....~~.....rr............rr.....~~.....rr..............rr......~~~~",
"~~~..................rrrrrrr.....~......rr............rr......~.....rrrrrrr..................~~~",
"~~~.................rrrrrrrr.....~r..........................r~.....rrrrrrrr.................~~~",
"~~~~................rrrr.............rr..................rr.............rrrr................~~~~",
"~~~~~...............rrrr.............rr..................rr.............rrrr...............~~~~~",
"~~~~~~................rrr...........rr....................rr...........rrr................~~~~~~",
"~~~~~~r...............rr................................................rr...............r~~~~~~",
"~~~~~.rr................................................................................rr.~~~~~",
"~~~...rr................................................................................rr...~~~",
"~~rr........................................................................................rr~~",
"~~rr....................rr..............rr............rr..............rr....................rr~~",
".................rr....rrr..............rr............rr..............rrr....rr.................",
".................rr..~~.....r....r...........rrrrrr...........r....r.....~~..rr.................",
".................r..~~~~...~~~~~~~~~.......^^^^..^^^^.......~~~~~~~~~...~~~~..r.................",
"..................r.~~~~..~~~~~~~~~~~......^^^^..^^^^......~~~~~~~~~~~..~~~~.r..................",
"..................r~~~~~~~~~~~~~~~~~~.....^^^^^^^^^^^^.....~~~~~~~~~~~~~~~~~~r..................",
".................r.~~~~~~~~~~~~~~~~~~.....^^^^^^^^^^^^.....~~~~~~~~~~~~~~~~~~.r.................",
"................rrr~~~~~~~~~~~~~~~~~~......^^^^..^^^^......~~~~~~~~~~~~~~~~~~rrr................",
"..............rrrr~~~~~~~.~~~~~~~~~~~...rr.^^^^..^^^^.rr...~~~~~~~~~~~.~~~~~~~rrrr..............",
"..............rrr.~~~~~~...~~~~~~~~~~...rrr^^^^..^^^^rrr...~~~~~~~~~~...~~~~~~.rrr..............",
"..............r............................^^^^..^^^^............................r..............",
"..............r...........................^^^^^^^^^^^^...........................r..............",
"............rrrrrr.......................^^^^^^^^^^^^^^.......................rrrrrr............",
"........rr.rrrrrrr......................^^^^^^^^^^^^^^^^......................rrrrrrr.rr........",
"........rrrrr..rrr....................^^^^^^^^^^^^^^^^^^^^....................rrr..rrrrr........",
"...........rr...rr...................^^^^^^^^^^^^^^^^^^^^^^...................rr...rr...........",
"...........rr....r...........^......^^^^^^^^^^^^^^^^^^^^^^^^......^...........r....rr...........",
"...........................r^^^...r^^^^^^^^^^^^^^^^^^^^^^^^^^r...^^^r...........................",
"r...........................^^^....^^^^^^^^^^^^^^^^^^^^^^^^^^....^^^...........................r",
"...................................r^^^^^^^^^^^^^^^^^^^^^^^^r...................................",
"......................................^^^^^^^^^^^^^^^^^^^^......................................",
".................................rrr.....^^^^^^..^^^^^^.....rrr.................................",
".........MM.....................rrrrr.....^^^^....^^^^.....rrrrr.....................MM.........",
".........MM.....^^...............rrrrr....................rrrrr...............^^.....MM.........",
"................^^...............rrrrr....................rrrrr...............^^................",
"...............^^^...............rrrrr....................rrrrr...............^^^...............",
"................^^^...............r..........................r...............^^^................",
"~~...............^^..........................................................^^...............~~",
"~~~.MM...........^^..........................................................^^...........MM.~~~",
"~~~.MM.......MM...^...rr................................................rr...^...MM.......MM.~~~",
"~~...........MM.......rr................................................rr.......MM...........~~",
"~.....................rr................................................rr.....................~",
"r..............................................................................................r",
"....MM............rr........................................................rr............MM....",
"....MM............rr........................................................rr............MM....",
"..................rr~~~~................................................~~~~rr..................",
"..................r.~~~~~~~..r..............^^....^^..............r..~~~~~~~.r..................",
"...................~~~~~~~~rr..............^^^....^^^..............rr~~~~~~~~...................",
"...................~~~~~~~~~...............^^^^..^^^^...............~~~~~~~~~...................",
"rr.................~~~~~~~~~..............^^^^^..^^^^^..............~~~~~~~~~.................rr",
"rrr................~~~~~~~~~..............^^^^^..^^^^^..............~~~~~~~~~................rrr",
"..r................~~~~~~~~~................^^....^^................~~~~~~~~~................r..",
"...................~~~~~~~~..........................................~~~~~~~~...................",
"..................r~~~~~~~~.r......................................r.~~~~~~~~r..................",
"...................r~~~~~~.rrr.rr......rrrr..........rrrr......rr.rrr.~~~~~~r...................",
"...................r~~~~rrrrrrrrr.....rrrr............rrrr.....rrrrrrrrr~~~~r...................",
"....................rr..rrrrrr.rrr....rrrr............rrrr....rrr.rrrrrr..rr....................",
".............r..........rrrrr....r....rr................rr....r....rrrrr..........r.............",
"...........rrrrr.........rr..........rr..................rr..........rr.........rrrrr...........",
"..........~~~.rr.........r...........rr.....~~....~~.....rr...........r.........rr.~~~..........",
"........r~~~~~rr.........rr..........rr....~~~~~~~~~~....rr..........rr.........rr~~~~~r........",
"........r~~~~~rrr.......rrr..........rrr...~~~~~~~~~~...rrr..........rrr.......rrr~~~~~r........",
".........~~~~~..rr......rrr...........rr...r~~~rr~~~r...rr...........rrr......rr..~~~~~.........",
".........~~~~~..rr......rrr..........................................rrr......rr..~~~~~.........",
"........~~~~~~..rr............................................................rr..~~~~~~........",
"........~~~~~~..rr............................................................rr..~~~~~~........",
"........~~~~~~..rr............................................................rr..~~~~~~........",
"........~~~~~~~.......................rrrr............rrrr.......................~~~~~~~........",
".......~~~~~~~~......r..........r^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^r..........r......~~~~~~~~.......",
".......~~~~~~~~.....rrrr........^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^........rrrr.....~~~~~~~~.......",
".......~~~~~~~~....rr.rr.......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.......rr.rr....~~~~~~~~.......",
".......~~~~~~~~.....~~.r.......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.......r.~~.....~~~~~~~~.......",
".......~~~~~~~~.....~~..r......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^......r..~~.....~~~~~~~~.......",
".......~~~~~~~~.....~~..rrr....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^....rrr..~~.....~~~~~~~~.......",
"......r~~~~~~~~.....~~~..rrr....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^....rrr..~~~.....~~~~~~~~r......",
"......rr~~~~~~~.....~~~..rrr.....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.....rrr..~~~.....~~~~~~~rr......",
"......rr~~~~~~~.....~~~~............^^^^^^^^^^^^^^^^^^^^^^^^............~~~~.....~~~~~~~rr......",
"......rr.~~~~~r.....~~~~...............rrrr..........rrrr...............~~~~.....r~~~~~.rr......",
"^.........~~~~r...r~~~~~~..............rrrrrrrr..rrrrrrrr..............~~~~~~r...r~~~~.........^",
"^^............rr..r~~~~~~~................rrrrrrrrrrrr................~~~~~~~r..rr............^^",
"^^^...........rr..r~~~~~~~................rrr..rr..rrr................~~~~~~~r..rr...........^^^",
"^^^.....rr........r~~~~~~~...............rrrr......rrrr...............~~~~~~~r........rr.....^^^",
"^^^^....rr.........~~~~~~~~...............rr........rr...............~~~~~~~~.........rr....^^^^",
"^^^^...............~~~~~~~~..........................................~~~~~~~~...............^^^^",
"^^^^...............~~~~~~~~..........................................~~~~~~~~...............^^^^",
"^^^^...............~~~~~~~~..........................................~~~~~~~~...............^^^^",
"^^^................~~~~~~~~..........................................~~~~~~~~................^^^",
"^^^................~~~~~~~~...........r....r........r....r...........~~~~~~~~................^^^",
"^^^.................~~~~~~...........rrr..rrr......rrr..rrr...........~~~~~~.................^^^",
"^^.....r......rr.r..~~~~~~............r....r........r....r............~~~~~~..r.rr......r.....^^"
],
"starts": [
{
@ -787,7 +787,7 @@
"id": "m06",
"name": "Annihilation",
"theme": "snowfields",
"seed": 80808,
"seed": 63541,
"playerArmy": "arm",
"playerCommander": "vance",
"enemies": [
@ -817,7 +817,7 @@
{
"speaker": "ethel",
"mood": "idle",
"text": "KLAXON at full strength with a base already standing, dug in behind its own defences. Nothing clever left — out-build it and end it."
"text": "KLAXON at full strength with a base already standing. Nothing clever left — out-build it and end it."
}
],
"victoryLine": {
@ -835,102 +835,102 @@
"h": 96,
"theme": "snowfields",
"rows": [
"~~~r....r...rrrrrr..........r.~~~~~~...............^^^^^^^^^^^^^^..................r....r..^^^^^",
"~~~r..........r....MM.........~~~~~~...............^^^^^^^^^^^^^...........................^^^^^",
"~~~...............rMM.........~~~~~r................^^^^^^^^^^^............................^^^^^",
"~~~...............rr..............r...................^^^^^^............................rr.^^^^^",
"rr................rrrr....................................r..........rr.................^^^^^^^^",
"...................rrrr.r............................................rr................^^^^^^^^^",
"...................rrrrrrrr............................................................^^^^^^^^^",
"....................r...rr..................r...........................................^^^^^^rr",
"r.....MM...................................r^^................rr........................^^^^^^..",
"......MM.............^^^..................^^^^^............rrrrrrrrr...................^^^^^^^^.",
"..MM................^^^^^^.............rr^^^^^^^...........rrrrrrrrr.................r^^^^^^^^^^",
"..MM...............^^^^^^^...rr........r^^^^^^^^^............r....^^r................^^^^^^^^^^^",
"...................^^^^^^^..rrr..rr....r^^^^^^^^^.................^^^................^^^^^^^^^^^",
"..................r^^^^^^^rrrrrr.rr....r^^^^^^^^^..................^........rr.......^^^^^^^^^^^",
"..................r^^^^^^...rrrr..........^^^^^^rr.....rrrrrrr..............rrrrr.....^^^^^^^^^.",
"..................^^^^^^....rrrrr................rrrrr~~~~~rrr..............rrrrr........r...r..",
".................^^^^^^r...rrr.rr..~.............rrr~~~~~~~~~.............................rrr...",
"~..............^^^^^.^.....rr....~~~~.rrr........rrr~~~~~~~~~..............................r....",
"~~.........r^^^^^^..............~~~~..rrr.............~~~~~~....................................",
"~~MM.......^^^^^...............~~~~........................rrrrrr...............................",
"~~MM.......^^^^^..............~~~~~........................rrrrrr...............................",
"~~~.......r^^^^^.............~~~~~~.........................rrrr................................",
"~~........^^^^^^^.............~~~~~.............................................................",
"..........^^^^^^^^..............~~~.............................................rrr.....r^......",
".........r^^^^^^^^................r.............................................rrr....^^^^.....",
".........r^^^^^^^^rrrr.....r....................................................rrrr...^^^^^....",
"r..........^^^^^^^rrrr....rrr......................................................rr..^^^^^....",
"r.............rrr....rrrrr.rr......................................................rrrrr^^^.....",
".rrrr........rrrr.......^^..r.......................................................rr..........",
"..rrrr.......rr........^^^^^^.........................r...r...............................rrr...",
"^^.rrr............rr..r^^^^^^r.......................^^^^^^^............................^^^^^^^^",
"^^.rrrr...........rrrr.^^^^^.......................^^^^^^^^^^^^.r.......................^^^^^^^^",
"^^rrrrr........rrrr...^^^^^.....^^^^^.............^^^^^^^^^^^^^^^rr....................^^^^^^^^^",
"^^rrr..........rrr^...^^^^.....r^^^^^.............^^^^^^^^^^^^^^^^^....................^^^^^^^^^",
"^^.rr..........rrr....^^^r.....r^^^^...............^^^^^^^^^^^^^........................^^^^^^^^",
"^..rr..rr...............r.......^^..................^^^^^^^^^..............rrr..................",
"....r.rrr...............r...^^^^.....................r^^^^................rrrr~.................",
".....rrrr..............rrr..^^^^.....................rr...................rrrr~.................",
"r......rr...............r...^^^^.....................rr.........................................",
".r...........................^^^^^^...r...........rrr........................................rr.",
".rr..........................^^^^^^^rrrr..........rrr.......................................rrr.",
"..r...................~~~....^^^^^^^^^^r.r..........r.......................................rrr.",
"~~~....rrr..........~~~~~~...^^^^^^^^^^^^^^^^^^.............................................rrrr",
"~~~~...rrrr........~~~~~~~....^^^^^^^^^^^^^^^^^^..........rr..................................~~",
"~~~~...rrrr........~~~~~~~....^^^^^^^^^^^^^^^^^^^.........rr......~~~........................~~~",
"~~~~...rr..........~~~~~~~....^^^^^^^^^^^^^^^^^^^..........r......~~~........................~~~",
"~~~~...rr..........~~~~~~~....^^^^^^^^^^^^^^^^^^.r........rr......~~..........................~~",
"~~~~...rr..........~~~~~~~....^^^^^^^^^^^^^^^^^..rrrrr....rr..r...~~.....r....................~~",
"~~....................r.....~~...r..rr....rrrrr..^^^^^^^^^^^^^^^^^....~~~~~~~..........rr...~~~~",
"~~..........................~~......rr........r.^^^^^^^^^^^^^^^^^^....~~~~~~~..........rr...~~~~",
"~~~........................~~~......r..........^^^^^^^^^^^^^^^^^^^....~~~~~~~..........rr...~~~~",
"~~~........................~~~......rr.........^^^^^^^^^^^^^^^^^^^....~~~~~~~........rrrr...~~~~",
"~~..................................rr..........^^^^^^^^^^^^^^^^^^....~~~~~~~........rrrr...~~~~",
"rrrr.............................................^^^^^^^^^^^^^^^^^^...~~~~~~..........rrr....~~~",
".rrr.......................................r..........r.r^^^^^^^^^^....~~~...................r..",
".rrr.......................................rrr..........rrrr^^^^^^^..........................rr.",
".rr........................................rrr...........r...^^^^^^...........................r.",
".........................................rr.....................^^^^...r...............rr......r",
".................~rrrr...................rr.....................^^^^..rrr..............rrrr.....",
".................~rrrr................^^^^r.....................^^^^...r...............rrr.r....",
"..................rrr..............^^^^^^^^^..................^^.......r...............rr..rr..^",
"^^^^^^^^........................^^^^^^^^^^^^^...............^^^^r.....r^^^....rrr..........rr.^^",
"^^^^^^^^^....................^^^^^^^^^^^^^^^^^.............^^^^^r.....^^^^...^rrr..........rrr^^",
"^^^^^^^^^....................rr^^^^^^^^^^^^^^^.............^^^^^.....^^^^^...rrrr........rrrrr^^",
"^^^^^^^^.......................r.^^^^^^^^^^^^.......................^^^^^.rrrr...........rrrr.^^",
"^^^^^^^^............................^^^^^^^.......................r^^^^^^r..rr............rrr.^^",
"...rrr...............................r...r.........................^^^^^^........rr.......rrrr..",
"..........rr.......................................................r..^^.......rrrr........rrrr.",
".....^^^rrrrr......................................................rr.rrrrr....rrr.............r",
"....^^^^^..rr......................................................rrr....rrrr^^^^^^^..........r",
"....^^^^^...rrrr....................................................r.....rrrr^^^^^^^^r.........",
".....^^^^....rrr.............................................r................^^^^^^^^r.........",
"......^r.....rrr.............................................~~~..............^^^^^^^^..........",
".............................................................~~~~~.............^^^^^^^........~~",
"................................rrrr.........................~~~~~~.............^^^^^r.......~~~",
"...............................rrrrrr........................~~~~~..............^^^^^.......MM~~",
"...............................rrrrrr........................~~~~...............^^^^^.......MM~~",
"....................................~~~~~~.............rrr..~~~~..............^^^^^^r.........~~",
"....r..............................~~~~~~~~~rrr........rrr.~~~~....rr.....^.^^^^^..............~",
"...rrr.............................~~~~~~~~~rrr.............~..rr.rrr...r^^^^^^.................",
"..r...r........rrrrr..............rrr~~~~~rrrrr................rrrrr....^^^^^^..................",
".^^^^^^^^^.....rrrrr..............rrrrrrr.....rr^^^^^^..........rrrr...^^^^^^r..................",
"^^^^^^^^^^^.......rr........^..................^^^^^^^^^r....rr.rrrrrr^^^^^^^r..................",
"^^^^^^^^^^^................^^^.................^^^^^^^^^r....rr..rrr..^^^^^^^...................",
"^^^^^^^^^^^................r^^....r............^^^^^^^^^r........rr...^^^^^^^...............MM..",
"^^^^^^^^^^r.................rrrrrrrrr...........^^^^^^^rr.............^^^^^^................MM..",
".^^^^^^^^...................rrrrrrrrr............^^^^^..................^^^.............MM......",
"..^^^^^^........................rr................^^r...................................MM.....r",
"rr^^^^^^...........................................r..................rr...r....................",
"^^^^^^^^^............................................................rrrrrrrr...................",
"^^^^^^^^^................rr............................................r.rrrr...................",
"^^^^^^^^.................rr..........r....................................rrrr................rr",
"^^^^^.rr............................^^^^^^...................r..............rr...............~~~",
"^^^^^............................^^^^^^^^^^^................r~~~~~.........MMr...............~~~",
"^^^^^...........................^^^^^^^^^^^^^...............~~~~~~.........MM....r..........r~~~",
"^^^^^..r....r..................^^^^^^^^^^^^^^...............~~~~~~.r..........rrrrrr...r....r~~~"
"................^^^...^^^^^.............^^^^^^.......r...rr....r.........r....r...........rrr...",
"..............MM.^^..r^^^^^..............^^^^^............................................rrr...",
"..............MM......^^^^^^...............rr..............................................rr...",
"......................^^^^^^..............r...............................................rrr...",
"....................................................................rrr...................rrr...",
"........................rr..........................................rrrr.....................r..",
"........................rr.........................................^..rr.......rr............rr.",
"................................................................r^^^^^..........rr...........rr.",
".......................~~~.......................................^^^^^^.......................rr",
"......................~~~~~.....................................^^^^^^^^........................",
"......................~~~~~......................................^^^^^^^........................",
"......................~~~~.....rr..............................rrrr...........................rr",
".......................~~......rr...........................rrrrr.......................rr....rr",
"...............................r...rr.................rr....rrrr.........................r....rr",
"................................rrrrr................rrr.....rrr................................",
"...............MM................rrrr..............rrrr.......rrr...............................",
"...............MM...............rrr...............rrrrr...rr..rr................................",
"......r..........................rr..r............rrrr....rrrrrr................rrr............r",
"r....MM.............................rrr.........................................rrr.............",
".....MM..............................rr...........................~~~~~~~~~~~~~~~~~.............",
".......................^^^^..rr......rr...r~~~..................~~~~~~~~~~~~~~~~~~~~............",
".........MM...........^^^^^.rrr......rr...~~~~~................~~~~~~~~~~~~~~~~~~~~~~~..........",
".........MM.........^^^^^^^rrrr..........~~~~~~~...............~~~~~~~~~~~~~~~~~~~~~~~~..rr....r",
"^^................r^^^^^rr...rr..........~~~~~~~...............~~~~~~~~~~~~~~~~~~~~~~~~.rrrrrrrr",
"^^^...............^^^^^..................~~~~~~~.......rr......~~~~~~~~~~~~~~~~~~~~~~~~~.rrrr...",
"^^^................^^^^...................~~~~~........rr.......~~~~~~~~~~~~~~~~~~~~~~~~~~......",
"r..................^^^^.........................................~~~~~~~~~~~~~~~~~~~~~~~~~~~~....",
"r..................rr^............................................~~~~~~~~~~~~~~~~~~~~~~~~~~~~..",
".......................................................................~~~~~~~~~~~~~~~~~~~~~~~~.",
"...rr..................rr.................................................~~~~~~~~~~~~~~~~~~~~~~",
"r..rr..................rrr.......................................................~~~~~~~~~~~~~~~",
"r..rr..................rrrr........................................................~~~~~~~~~~~~~",
"...rr....................rr..........rr....................r........................~~~~~~~~~~~~",
"..........................rr.........rr...................rrr.......................~~~~~~~~~~~~",
"..........................rr......................rr.......r........................r~~~~~~~~~~~",
".........................rrrr...rr................rr.................................r~~~~~~~~~~",
".........................rrrr...rr..r.............rr.................................rrr~~~~~~..",
".........................r.r.....rrrrrr.............................................rrrrrrrrr...",
".................................rr..rr................^^...........................rr.rr.......",
"................................rr...................^^^^^..rr..................................",
".....rrr............~~..........rr..............^^^^^^^^^^..rr..................................",
".....rrr.......rr...~~..........rr........^^^^^^^^^^^^^^^^..rr..................................",
".......rr......rr..~~~..........rr.......^^^^^^^^^^^^^^^^^..rr..................................",
"........rr.........~~~..................^^^^^^^^^^^^^^^^^^..rrrr..............^.................",
"........rr.........~~~~.................^^^^^^^^^^^^^^^^^^^.rrrr.............^^^................",
"...................~~~~................^^^^^^^^^^^^^^^^^^^..rrr..............^^^rrr......rrrrrrr",
"...................~~~..................^^^^^^^^^^^^^^^^^^..rr...............^^^rrr......rrrrrrr",
"....................~~..................^^^^^^^^^^^^^^^^......................^................r",
"r................^......................^^^^^^^^^^^^^^^^..................~~....................",
"rrrrrrr......rrr^^^...............rr..^^^^^^^^^^^^^^^^^^..................~~~...................",
"rrrrrrr......rrr^^^..............rrr..^^^^^^^^^^^^^^^^^^^................~~~~...................",
"................^^^.............rrrr.^^^^^^^^^^^^^^^^^^^.................~~~~.........rr........",
".................^..............rrrr..^^^^^^^^^^^^^^^^^^..................~~~.........rr........",
"..................................rr..^^^^^^^^^^^^^^^^^.......rr..........~~~..rr......rr.......",
"..................................rr..^^^^^^^^^^^^^^^^........rr..........~~...rr.......rrr.....",
"..................................rr..^^^^^^^^^^..............rr..........~~............rrr.....",
"..................................rr..^^^^^...................rr................................",
".......rr.rr...........................^^................rr..rr.................................",
"...rrrrrrrrr.............................................rrrrrr.....r.r.........................",
"..~~~~~~rrr.................................rr.............r..rr...rrrr.........................",
"~~~~~~~~~~r.................................rr................rr...rrrr.........................",
"~~~~~~~~~~~r........................r.......rr......................rr..........................",
"~~~~~~~~~~~~.......................rrr...................rr.........rr..........................",
"~~~~~~~~~~~~........................r....................rr..........rr....................rr...",
"~~~~~~~~~~~~~........................................................rrrr..................rr..r",
"~~~~~~~~~~~~~~~.......................................................rrr..................rr..r",
"~~~~~~~~~~~~~~~~~~~~~~.................................................rr..................rr...",
".~~~~~~~~~~~~~~~~~~~~~~~~.......................................................................",
"..~~~~~~~~~~~~~~~~~~~~~~~~~~~~............................................^rr..................r",
"....~~~~~~~~~~~~~~~~~~~~~~~~~~~~.........................................^^^^..................r",
"......~~~~~~~~~~~~~~~~~~~~~~~~~~.......rr........~~~~~...................^^^^................^^^",
"...rrrr.~~~~~~~~~~~~~~~~~~~~~~~~~......rr.......~~~~~~~..................^^^^^...............^^^",
"rrrrrrrr.~~~~~~~~~~~~~~~~~~~~~~~~...............~~~~~~~..........rr...rr^^^^^r................^^",
"r....rr..~~~~~~~~~~~~~~~~~~~~~~~~...............~~~~~~~..........rrrr^^^^^^^.........MM.........",
"..........~~~~~~~~~~~~~~~~~~~~~~~................~~~~~...rr......rrr.^^^^^...........MM.........",
"............~~~~~~~~~~~~~~~~~~~~..................~~~r...rr......rr..^^^^.......................",
".............~~~~~~~~~~~~~~~~~...........................rr..............................MM.....",
".............rrr.........................................rrr.............................MM....r",
"r............rrr................rrrrrr....rrrr............r..rr..........................r......",
"................................rr..rr...rrrrr...............rrr...............MM...............",
"...............................rrr.......rrrr..............rrrr................MM...............",
"................................rrr.....rrr................rrrrr................................",
"rr....r.........................rrrr....rr.................rr...r...............................",
"rr....rr.......................rrrrr...........................rr......~~.......................",
"rr...........................rrrr..............................rr.....~~~~......................",
"........................^^^^^^^......................................~~~~~......................",
"........................^^^^^^^^.....................................~~~~~......................",
"rr.......................^^^^^^.......................................~~~.......................",
".rr...........rr..........^^^^^r................................................................",
".rr............rr.......rr..^.........................................rr........................",
"..r.....................rrrr..........................................rr........................",
"...rrr...................rrr....................................................................",
"...rrr...............................................r..............^^^^^^......................",
"...rr..............................................rr...............^^^^^^......MM..............",
"...rrr............................................^^^^^..............^^^^^r..^^.MM..............",
"...rrr...........r....r.........r....rr...r.......^^^^^^.............^^^^^...^^^................"
],
"starts": [
{

View File

@ -402,9 +402,9 @@
],
"armorMul": {
"infantry": 0.3,
"light": 0.35,
"medium": 0.3,
"heavy": 0.3,
"light": 0.6,
"medium": 1.0,
"heavy": 1.4,
"structure": 1.5,
"fortification": 1.6,
"air": 2.6
@ -418,78 +418,6 @@
"sound": "sfx-ta-rocket-1",
"impactSound": "sfx-ta-rocket-2"
},
{
"id": "megatankgun",
"name": "Twin 150mm Cannon",
"kind": "ballistic",
"damage": 145,
"reload": 2.0,
"burst": 1,
"range": 380,
"speed": 700,
"spread": 0.015,
"leadTarget": true,
"aoe": 44,
"aoeFalloff": 0.15,
"targets": [
"ground"
],
"barrels": 2,
"barrelSpacing": 9,
"armorMul": {
"infantry": 1.3,
"light": 1.15,
"medium": 1.0,
"heavy": 0.8,
"structure": 1.2,
"fortification": 1.1,
"air": 1.0
},
"fx": {
"style": "shell",
"color": "#ffcf6a",
"width": 4,
"muzzle": 22
},
"sound": "sfx-battle-tank"
},
{
"id": "megatankrockets",
"name": "Side Rocket Pods",
"kind": "guided",
"damage": 55,
"reload": 1.32,
"burst": 1,
"range": 400,
"minRange": 90,
"speed": 330,
"turnRate": 2.1,
"aoe": 42,
"aoeFalloff": 0.15,
"targets": [
"ground",
"air"
],
"barrels": 2,
"barrelSpacing": 20,
"armorMul": {
"infantry": 0.3,
"light": 0.6,
"medium": 1.0,
"heavy": 1.3,
"structure": 1.4,
"fortification": 1.5,
"air": 2.4
},
"fx": {
"style": "rocket",
"color": "#ff5c3c",
"width": 2,
"trail": true
},
"sound": "sfx-ta-rocket-1",
"impactSound": "sfx-ta-rocket-2"
},
{
"id": "shoulderrocket",
"name": "Shoulder Rocket",
@ -950,38 +878,6 @@
"spritePx": 56,
"moveSound": "sfx-engine-heavy"
},
{
"id": "megatank",
"name": "Megatank",
"role": "combat",
"size": "medium",
"radius": 33,
"hp": 3200,
"speed": 65,
"turnRate": 1.6,
"moveClass": "tread",
"armorClass": "heavy",
"sight": 320,
"cost": {
"energy": 3200,
"mass": 1000
},
"buildTime": 80,
"builtBy": [
"advancedvehicleplant"
],
"weapons": [
"megatankgun",
"megatankrockets"
],
"sheetSlot": "unitSheet",
"frame": 15,
"turretFrame": 16,
"procShape": "tank",
"icon": 32,
"spritePx": 66,
"moveSound": "sfx-engine-heavy"
},
{
"id": "constructor",
"name": "Construction Vehicle",
@ -1024,39 +920,6 @@
"spritePx": 50,
"moveSound": "sfx-engine-heavy"
},
{
"id": "advancedconstructor",
"name": "Advanced Construction Vehicle",
"role": "builder",
"size": "medium",
"radius": 26,
"hp": 950,
"speed": 80,
"turnRate": 2.2,
"moveClass": "tread",
"armorClass": "light",
"sight": 360,
"cost": {
"energy": 900,
"mass": 220
},
"buildTime": 22,
"builtBy": [
"advancedvehicleplant"
],
"buildPower": 135,
"buildRange": 200,
"builds": [
"advancedmassgen",
"nuclearplant"
],
"sheetSlot": "unitSheet",
"frame": 17,
"procShape": "constructor",
"icon": 33,
"spritePx": 54,
"moveSound": "sfx-engine-heavy"
},
{
"id": "fighter",
"name": "Fighter",
@ -1215,38 +1078,6 @@
"icon": 10,
"idleAnimation": true
},
{
"id": "nuclearplant",
"name": "Nuclear Power Plant",
"footprint": {
"w": 2,
"h": 2
},
"hp": 4200,
"armorClass": "structure",
"sight": 160,
"cost": {
"energy": 2200,
"mass": 260
},
"buildTime": 42,
"produce": {
"energy": 80
},
"storage": {
"energy": 900
},
"deathExplosion": {
"radius": 220,
"damage": 650
},
"upgradesFrom": "energygen",
"sheetSlot": "structureSheet",
"frame": 22,
"buildFrame": 23,
"procShape": "energyGen",
"icon": 35
},
{
"id": "massgen",
"name": "Mass Generator",
@ -1279,39 +1110,6 @@
"icon": 11,
"idleAnimation": true
},
{
"id": "advancedmassgen",
"name": "Advanced Metal Generator",
"footprint": {
"w": 2,
"h": 2
},
"hp": 3200,
"armorClass": "structure",
"sight": 160,
"cost": {
"energy": 1400,
"mass": 150
},
"buildTime": 30,
"produce": {
"mass": 8.0
},
"upkeep": {
"energy": 60
},
"storage": {
"mass": 400
},
"terrainMultiplier": "massMultiplier",
"upgradesFrom": "massgen",
"sheetSlot": "structureSheet",
"frame": 24,
"buildFrame": 25,
"topperFrame": 26,
"procShape": "massGen",
"icon": 34
},
{
"id": "barracks",
"name": "Barracks",
@ -1454,8 +1252,9 @@
"buildTime": 55,
"buildPower": 140,
"builds": [
"megatank",
"advancedconstructor"
"jeep",
"tank",
"rockettank"
],
"spawnOffset": {
"x": 0,
@ -1652,7 +1451,7 @@
"defaults": {
"size": "medium",
"symmetry": "mirror-x",
"theme": "tropics",
"theme": "grasslands",
"aiSkill": 3,
"playerArmy": "arm"
},

View File

@ -302,19 +302,16 @@ function chooseBuilding(ctx, mine) {
}
/**
* Static defence is a skill-3-and-up behaviour, like the Airfield.
* Static defence is a skill-4-and-up behaviour, like the Airfield is skill 3 and up.
*
* These structures hit hard for their cost a Laser Tower puts out 130 dps behind 1700 hp for
* 60% of a Tank's price and the ground roster has no dedicated siege answer to them yet, so
* two equally-matched AIs both digging in grind rather than resolve. Reserving them for skill 3
* and up puts that pressure where a player goes looking for it once they're past the beginner
* tier, and leaves skill 1-2 playing the open game they were tuned for: `urgentDefence` and
* `steadyDefence` are still called for every skill, but their own `ctx.skill < DEFENCE_MIN_SKILL`
* guard keeps returning null for skill 1-2 exactly as before, so lowering this constant alone
* touches only skill 3 and up. Drop it further (to 1) once there is a unit whose job is cracking
* fortifications.
* two equally-matched AIs both digging in grind rather than resolve. Reserving them for the
* stronger opponents puts that pressure where a player goes looking for it and leaves the low
* end of the ladder playing the open game it was tuned for. Drop this to 3 (or 1) once there is
* a unit whose job is cracking fortifications.
*/
const DEFENCE_MIN_SKILL = 3;
const DEFENCE_MIN_SKILL = 4;
/** Has anything hostile been inside the base perimeter recently? */
function underThreat(ctx) {
@ -328,18 +325,9 @@ function enemyAirSeen(ctx) {
/**
* The one structure the base needs RIGHT NOW, ahead of any economic consideration. Kept
* deliberately tiny one tower, one launcher against aircraft. Everything beyond the first of
* each is a steady-state decision (see steadyDefence), because an AI that answers pressure by
* building walls stops building an army and loses slowly instead of fast.
*
* The first Laser Tower is PROACTIVE, not a raid reaction: it goes up right after the opening
* economy (2 energygen/1 massgen/1 barracks/3 energygen/2 massgen/1 vehicleplant), the moment
* this function is first reachable, regardless of `underThreat`. A purely reactive first tower
* the previous behaviour never exists until the base has already been hit once, which is no
* help at all against a single decisive rush that goes straight for the Commander: the raid
* that would have triggered it IS the loss. Everything past this first one stays reactive
* (steadyDefence), which is what keeps two skill-3+ AIs from both digging in and turning a
* mirror match into a stalemate see the soak notes on that.
* deliberately tiny one tower against a raid, one launcher against aircraft. Everything
* beyond the first of each is a steady-state decision (see steadyDefence), because an AI that
* answers pressure by building walls stops building an army and loses slowly instead of fast.
*/
function urgentDefence(ctx, mine) {
const { rules } = ctx;
@ -350,7 +338,7 @@ function urgentDefence(ctx, mine) {
// Anti-air first: a Laser Tower cannot shoot at aircraft at all, so a base being bombed gets
// nothing whatsoever from one.
if (aa && enemyAirSeen(ctx) && n('missilelauncher') < 1) return aa;
if (tower && n('lasertower') + n('missilelauncher') < 1) return tower;
if (tower && underThreat(ctx) && n('lasertower') + n('missilelauncher') < 1) return tower;
return null;
}
@ -544,11 +532,9 @@ function applyCounters(ctx, weights) {
const seen = infantryish + armour;
if (seen) {
const armourShare = armour / seen;
// The Rocket Tank's rocketpod is weak against vehicle armour (weak vs light/medium/heavy,
// strong vs structure/fortification/air) — Tanks are now the answer to an armour-heavy
// enemy, not Rocket Tanks. Rocket Tanks still lean into a base full of structures.
weights.tank *= 1 + (1 - armourShare) * 0.8 + armourShare * 0.4;
weights.rockettank *= 1 + (structures > 2 ? 0.5 : 0);
// Tanks shred infantry; rockets and snipers answer armour and buildings.
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;
}

View File

@ -671,9 +671,7 @@ const PROC_PAINTERS = {
structure(scene, key, spec, rules) {
const defs = rules.buildings.filter((b) => b.sheetSlot === 'structureSheet');
let count = 0;
for (const d of defs) {
count = Math.max(count, d.frame + 1, (d.buildFrame ?? -1) + 1, (d.topperFrame ?? -1) + 1);
}
for (const d of defs) count = Math.max(count, d.frame + 1, (d.buildFrame ?? -1) + 1);
const sh = mkCanvasSheet(scene, key, spec.frameWidth, spec.frameHeight, spec.cols, count);
for (const d of defs) {
const draw = STRUCT_SHAPES[d.procShape];
@ -686,22 +684,6 @@ const PROC_PAINTERS = {
ctx.globalAlpha = 1;
});
}
// A generic spinner glyph — real toppers (the Advanced Metal Generator's centrifuge) get
// real art; this is just enough for the frame to register and not render blank.
if (d.topperFrame != null) {
sh.at(d.topperFrame, (ctx, S) => {
const c = S / 2, r = S * 0.3;
ctx.strokeStyle = '#cfe3ff'; ctx.lineWidth = S * 0.035;
ctx.beginPath(); ctx.arc(c, c, r, 0, Math.PI * 2); ctx.stroke();
for (let i = 0; i < 4; i++) {
const ang = (Math.PI / 2) * i;
ctx.beginPath();
ctx.moveTo(c, c);
ctx.lineTo(c + Math.cos(ang) * r, c + Math.sin(ang) * r);
ctx.stroke();
}
});
}
}
return sh.finish();
},

View File

@ -273,9 +273,6 @@ export default class TAHud {
this._clearGrid();
const s = this.scene;
const army = this.state.armies[this.playerArmy];
// Real art or the procedural fallback (TAArt.js always supplies one or the other), so this
// is only null for a def that predates the `icon` field — see the fallback layout below.
const iconKey = this.view.sheetKeys?.icons;
options.forEach((def, i) => {
const col = i % GRID_COLS, row = Math.floor(i / GRID_COLS);
// The container is placed at the button's CENTRE, not its top-left — see below.
@ -289,23 +286,13 @@ export default class TAHud {
// child here is centred on (0,0) so the visual and the hit area coincide.
const box = s.add.rectangle(0, 0, BTN_W, BTN_H, affordable ? 0x22304a : 0x1a2130, 1)
.setStrokeStyle(2, affordable ? EDGE : 0x2a3244);
const children = [box];
const hasIcon = iconKey != null && def.icon != null;
// The icon carries the button's identity at a glance once one exists; name and cost drop
// underneath it rather than owning the whole button the way they did before icons.
if (hasIcon) {
const icon = s.add.image(0, -18, iconKey, def.icon).setDisplaySize(36, 36);
if (!affordable) icon.setAlpha(0.5);
children.push(icon);
}
const label = s.add.text(0, hasIcon ? 2 : -BTN_H / 2 + 12, shortName(def.name), {
fontFamily: FONT, fontSize: hasIcon ? '14px' : '20px', color: affordable ? TEXT : '#5d6b80',
const label = s.add.text(0, -BTN_H / 2 + 12, shortName(def.name), {
fontFamily: FONT, fontSize: '20px', color: affordable ? TEXT : '#5d6b80',
}).setOrigin(0.5, 0);
const cost = s.add.text(0, hasIcon ? 18 : -BTN_H / 2 + 44, `${def.cost.mass}m ${def.cost.energy}e`, {
fontFamily: FONT, fontSize: hasIcon ? '12px' : '15px', color: affordable ? DIM : '#4e5a6d',
const cost = s.add.text(0, -BTN_H / 2 + 44, `${def.cost.mass}m ${def.cost.energy}e`, {
fontFamily: FONT, fontSize: '15px', color: affordable ? DIM : '#4e5a6d',
}).setOrigin(0.5, 0);
children.push(label, cost);
c.add(children);
c.add([box, label, cost]);
c.setSize(BTN_W, BTN_H);
c.setInteractive({ useHandCursor: true });
c.on('pointerover', () => box.setFillStyle(affordable ? 0x2e3f5f : 0x1a2130, 1));

View File

@ -1,140 +0,0 @@
// Total Annihilation — the full-screen studio intro that plays once, ahead of the main menu,
// every time the scene starts fresh (see TotalAnnihilationGame.create — this scene is never
// resumed in place, only entered, so "fresh" is every time).
//
// The clip carries its own audio track, so it plays unmuted — autoplay-with-sound is safe here
// specifically because reaching this scene already required a user gesture (clicking the
// game's icon in the arcade), which is what the browser's autoplay gate actually checks for.
// The game's own soundtrack is paused for the duration so the two don't fight over the same
// speakers, then resumed once the clip ends or is skipped.
import * as Phaser from 'phaser';
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
import { FONT } from './TAHud.js';
const INTRO_KEY = 'ta-mov-intro';
const INTRO_PATH = 'assets/videos/ta/ta_intro.mp4';
// Source resolution, used until the real texture decodes (a Video's own width/height report a
// square placeholder before its first frame).
const INTRO_SRC_W = 864;
const INTRO_SRC_H = 480;
// How long "Press any key to skip" stays up after the first press before it quietly stands
// down (rather than skipping on its own) and waits for the next one.
const SKIP_HOLD_MS = 5000;
/**
* Play the intro clip full-screen, then call `onDone`. The first key press or click surfaces a
* skip prompt; a second one inside SKIP_HOLD_MS jumps straight to `onDone`. A load failure or
* playback error also falls straight through to `onDone` a missing intro is never allowed to
* strand the player in front of a black screen.
*/
export function playIntroVideo(scene, onDone) {
let done = false;
let video = null;
let skipArmed = false;
let skipTimer = null;
let skipText = null;
scene.music?.pause();
let musicResumed = false;
const resumeMusic = () => {
if (musicResumed) return;
musicResumed = true;
scene.music?.resume();
};
const root = scene.add.container(0, 0).setDepth(1000);
root.add(scene.add.rectangle(0, 0, GAME_WIDTH, GAME_HEIGHT, 0x000000, 1).setOrigin(0, 0));
const clearPrompt = () => {
skipTimer?.remove(false);
skipTimer = null;
skipText?.destroy();
skipText = null;
skipArmed = false;
};
const cleanup = () => {
scene.input.keyboard?.off('keydown', onInput);
scene.input.off('pointerdown', onInput);
clearPrompt();
video?.destroy();
root.destroy();
};
const finish = () => {
if (done) return;
done = true;
cleanup();
resumeMusic();
onDone();
};
// Belt-and-braces: if the scene tears down mid-clip (e.g. a hot reload), drop our own
// listeners and the video element without firing onDone a second time behind whatever else
// shutdown is already doing.
scene.events.once('shutdown', () => { if (!done) { done = true; cleanup(); } });
const armSkipPrompt = () => {
skipArmed = true;
skipText = scene.add.text(GAME_WIDTH / 2, GAME_HEIGHT - 96, 'Press any key to skip', {
fontFamily: FONT, fontSize: '28px', color: '#e8f4ff',
}).setOrigin(0.5).setAlpha(0);
root.add(skipText);
scene.tweens.add({ targets: skipText, alpha: 1, duration: 200 });
skipTimer = scene.time.delayedCall(SKIP_HOLD_MS, clearPrompt);
};
function onInput() {
if (done) return;
if (skipArmed) { finish(); return; }
armSkipPrompt();
}
scene.input.keyboard?.on('keydown', onInput);
scene.input.on('pointerdown', onInput);
const startVideo = () => {
video = scene.add.video(GAME_WIDTH / 2, GAME_HEIGHT / 2, INTRO_KEY);
video.setLoop(false);
video.setMute(false);
video.setVolume(1);
// Cover, not contain: this is meant to fill the screen edge-to-edge, and the clip's own
// aspect (864x480) is within a hair of the game's (1920x1080) anyway, so the crop is
// imperceptible.
const fit = () => {
const real = !!video.videoTexture;
const w = real ? video.width : INTRO_SRC_W;
const h = real ? video.height : INTRO_SRC_H;
video.setScale(Math.max(GAME_WIDTH / w, GAME_HEIGHT / h));
};
fit();
video.on('created', fit);
video.on('playing', fit);
video.once('complete', finish);
video.once('error', finish);
// A browser that refuses to autoplay sound fires 'locked' and keeps retrying muted from
// preUpdate.
video.once('locked', () => { if (video?.scene) video.setMute(true); });
video.play(false);
root.add(video);
};
// Deliberately NOT in the asset manifest / preloaded ahead of time — this clip is loaded
// on demand, right here, the moment the game itself starts.
if (scene.cache.video?.exists(INTRO_KEY)) {
startVideo();
return;
}
scene.load.video(INTRO_KEY, INTRO_PATH, false);
scene.load.once(`filecomplete-video-${INTRO_KEY}`, () => { if (!done) startVideo(); });
scene.load.once(Phaser.Loader.Events.FILE_LOAD_ERROR, (file) => {
if (file?.key === INTRO_KEY) finish();
});
if (!scene.load.isLoading()) scene.load.start();
}

View File

@ -370,58 +370,15 @@ function buildCommand(state, rules, army, units, order, queue) {
if (!(bdef.builds ?? []).includes(order.defId)) {
return { ok: false, error: `${bdef.name} cannot build ${def.name}` };
}
const legal = canPlaceAt(state, rules, order.tx, order.ty, def, army);
const legal = canPlaceAt(state, rules, order.tx, order.ty, def);
if (!legal.ok) return legal;
// An upgrade building (Nuclear Power Plant over an Energy Generator, Advanced Metal Generator
// over a Mass Generator) may land exactly on the friendly building it names in `upgradesFrom`
// instead of needing clear ground — canPlaceAt already let this through, so re-find the same
// target here and consume it.
const upgradeTarget = findUpgradeTarget(state, army, order.tx, order.ty, def);
if (upgradeTarget) reclaimUpgradeTarget(state, rules, army, upgradeTarget);
const site = placeBuilding(state, rules, army, order.defId, order.tx, order.ty);
if (!site) return { ok: false, error: 'placement failed' };
pushOrder(builder, { type: 'build', targetId: site.id }, queue);
return { ok: true, siteId: site.id };
}
/**
* A friendly, finished building of `def.upgradesFrom`'s type sitting in EXACTLY this footprint
* the only shape of "occupied" an upgrade building is allowed to land on. Deliberately not a
* looser "overlaps anywhere" match: a 2x2 upgrade has to replace a 2x2 original one-for-one, not
* clip a neighbour.
*/
function findUpgradeTarget(state, army, tx, ty, def) {
if (!def.upgradesFrom) return null;
for (const e of state.entities) {
if (e.dead || e.site || !e.isBuilding || e.army !== army) continue;
if (e.defId !== def.upgradesFrom) continue;
if (e.tx === tx && e.ty === ty && e.fw === def.footprint.w && e.fh === def.footprint.h) return e;
}
return null;
}
/**
* Consume a building being upgraded in place: half its own build cost comes back (a genuine
* refund, not a discount on the new building's price), and it disappears quietly no death
* explosion, no kill/loss stat, no 'unitDestroyed' blast FX. This is recycling your own
* structure to make room for its replacement, not losing it to the enemy.
*/
function reclaimUpgradeTarget(state, rules, army, target) {
const tdef = rules.buildingById[target.defId];
const a = state.armies[army];
a.energy = Math.min(a.energyCap, a.energy + (tdef.cost?.energy ?? 0) * 0.5);
a.mass = Math.min(a.massCap, a.mass + (tdef.cost?.mass ?? 0) * 0.5);
target.dead = true;
stampFootprint(state.nav, rules, target.tx, target.ty, target.fw, target.fh, false);
recomputeEconomyCaps(state, rules);
for (const o of state.entities) {
if (o.buildTargetId === target.id) o.buildTargetId = 0;
if (o.targetId === target.id) o.targetId = 0;
}
}
/**
* Remove one order from a unit's queue by index the missing counterpart to `stop`, which
* only ever clears the whole queue. Cancelling the active order (index 0) gets the same state
@ -461,20 +418,12 @@ export function cancelOrder(state, rules, army, unitId, index) {
return { ok: true };
}
/**
* Can this footprint go here? Checks bounds, terrain buildability and occupancy.
*
* `army` is optional and only needed to recognise an upgrade placement (see `upgradesFrom`)
* every caller that doesn't pass one just gets the plain "is this ground clear" answer, which
* is what siting logic that has no notion of ownership (the AI's placement search, verify
* fixtures) actually wants anyway.
*/
export function canPlaceAt(state, rules, tx, ty, def, army = null) {
/** Can this footprint go here? Checks bounds, terrain buildability and occupancy. */
export function canPlaceAt(state, rules, tx, ty, def) {
const fw = def.footprint.w, fh = def.footprint.h;
if (tx < 0 || ty < 0 || tx + fw > state.w || ty + fh > state.h) {
return { ok: false, error: 'off the map' };
}
if (army != null && findUpgradeTarget(state, army, tx, ty, def)) return { ok: true };
for (let y = ty; y < ty + fh; y++) {
for (let x = tx; x < tx + fw; x++) {
const i = y * state.w + x;
@ -1695,67 +1644,59 @@ function acquireTarget(state, rules, e, def, hash, near) {
function fireWeapon(state, rules, e, w, target, facing, aimPt) {
const aim = aimPt ?? aimPointOn(rules, e, target);
const muzzle = (w.fx?.muzzle ?? e.radius);
const barrels = w.barrels ?? 1;
// Perpendicular to the firing line, so twin-barrel weapons (the Megatank's cannon and its
// side-mounted rockets) spawn each shot from its own barrel tip instead of the hull centre.
const perpX = -Math.sin(facing), perpY = Math.cos(facing);
const ox = e.x + Math.cos(facing) * muzzle;
const oy = e.y + Math.sin(facing) * muzzle;
for (let bi = 0; bi < barrels; bi++) {
const lateral = barrels > 1 ? (bi - (barrels - 1) / 2) * (w.barrelSpacing / (barrels - 1)) : 0;
const ox = e.x + Math.cos(facing) * muzzle + perpX * lateral;
const oy = e.y + Math.sin(facing) * muzzle + perpY * lateral;
state.events.push({
t: 'weaponFired', id: e.id, army: e.army, weapon: w.id,
x: ox, y: oy, heading: facing, style: w.fx.style, sound: w.sound,
});
if (w.kind === 'hitscan' || w.kind === 'beam') {
let hit = true;
if (w.spread > 0) {
// Spread becomes a miss chance that grows with range — cheap, and it means massed
// infantry still trade meaningfully at the edge of their envelope.
const d = Math.hypot(aim.x - e.x, aim.y - e.y);
const missChance = Math.min(0.6, w.spread * (d / w.range) * 3);
hit = rngNext(state) >= missChance;
}
const ex = hit ? aim.x : aim.x + rngRange(state, -24, 24);
const ey = hit ? aim.y : aim.y + rngRange(state, -24, 24);
state.events.push({
t: 'weaponFired', id: e.id, army: e.army, weapon: w.id,
x: ox, y: oy, heading: facing, style: w.fx.style, sound: w.sound,
t: 'shot', style: w.fx.style, x1: ox, y1: oy, x2: ex, y2: ey,
color: w.fx.color, width: w.fx.width, lifeMs: w.fx.lifeMs ?? 80, army: e.army,
});
if (w.kind === 'hitscan' || w.kind === 'beam') {
let hit = true;
if (w.spread > 0) {
// Spread becomes a miss chance that grows with range — cheap, and it means massed
// infantry still trade meaningfully at the edge of their envelope.
const d = Math.hypot(aim.x - e.x, aim.y - e.y);
const missChance = Math.min(0.6, w.spread * (d / w.range) * 3);
hit = rngNext(state) >= missChance;
}
const ex = hit ? aim.x : aim.x + rngRange(state, -24, 24);
const ey = hit ? aim.y : aim.y + rngRange(state, -24, 24);
state.events.push({
t: 'shot', style: w.fx.style, x1: ox, y1: oy, x2: ex, y2: ey,
color: w.fx.color, width: w.fx.width, lifeMs: w.fx.lifeMs ?? 80, army: e.army,
});
if (hit) {
applyDamage(state, rules, target, w.damage * (w.armorMul?.[rules.defById[target.defId].armorClass] ?? 1), e);
if (w.aoe) applyAoe(state, rules, aim.x, aim.y, w, e);
state.events.push({ t: 'impact', x: ex, y: ey, radius: w.aoe ?? 8, style: w.fx.style, sound: w.impactSound });
}
continue;
if (hit) {
applyDamage(state, rules, target, w.damage * (w.armorMul?.[rules.defById[target.defId].armorClass] ?? 1), e);
if (w.aoe) applyAoe(state, rules, aim.x, aim.y, w, e);
state.events.push({ t: 'impact', x: ex, y: ey, radius: w.aoe ?? 8, style: w.fx.style, sound: w.impactSound });
}
// Travelling ordnance: leading matters, and a strafing target can genuinely be missed.
let aimX = aim.x, aimY = aim.y;
if (w.leadTarget) {
const d = Math.hypot(target.x - ox, target.y - oy);
const flight = d / w.speed;
aimX += (target.vx ?? 0) * flight;
aimY += (target.vy ?? 0) * flight;
}
let ang = Math.atan2(aimY - oy, aimX - ox);
if (w.spread > 0) ang += rngRange(state, -w.spread, w.spread);
if (state.projectiles.length >= rules.constants.projectileCap) break;
state.projectiles.push({
id: state.nextProjectileId++,
weapon: w.id, army: e.army, ownerId: e.id,
x: ox, y: oy, px: ox, py: oy,
vx: Math.cos(ang) * w.speed, vy: Math.sin(ang) * w.speed,
targetId: w.kind === 'guided' ? target.id : 0,
aimX, aimY,
ttl: Math.ceil(((w.range * 1.4) / w.speed) * rules.constants.tickHz),
trail: [],
});
return;
}
// Travelling ordnance: leading matters, and a strafing target can genuinely be missed.
let aimX = aim.x, aimY = aim.y;
if (w.leadTarget) {
const d = Math.hypot(target.x - ox, target.y - oy);
const flight = d / w.speed;
aimX += (target.vx ?? 0) * flight;
aimY += (target.vy ?? 0) * flight;
}
let ang = Math.atan2(aimY - oy, aimX - ox);
if (w.spread > 0) ang += rngRange(state, -w.spread, w.spread);
if (state.projectiles.length >= rules.constants.projectileCap) return;
state.projectiles.push({
id: state.nextProjectileId++,
weapon: w.id, army: e.army, ownerId: e.id,
x: ox, y: oy, px: ox, py: oy,
vx: Math.cos(ang) * w.speed, vy: Math.sin(ang) * w.speed,
targetId: w.kind === 'guided' ? target.id : 0,
aimX, aimY,
ttl: Math.ceil(((w.range * 1.4) / w.speed) * rules.constants.tickHz),
trail: [],
});
}
function stepProjectiles(state, rules) {

View File

@ -195,12 +195,6 @@ export function compileRules(json) {
// The D-Gun is the reason this flag exists — left on auto-fire, a defending Commander
// deletes one attacker every reload for free, which makes assaulting any base suicide.
w.manual = w.manual === true;
// Multi-barrel weapons (the Megatank's twin cannon and side rockets) fire every barrel
// simultaneously on one reload, each barrel's shot spawned `barrelSpacing` px apart,
// perpendicular to the firing line, split evenly either side of the hull centreline.
w.barrels = w.barrels ?? 1;
if (!(Number.isInteger(w.barrels) && w.barrels >= 1)) fail(`weapon "${w.id}" barrels must be a positive integer`);
if (w.barrels > 1 && !(w.barrelSpacing > 0)) fail(`weapon "${w.id}" has barrels > 1 but no positive barrelSpacing`);
}
const weaponById = indexById(weapons);
@ -303,9 +297,6 @@ export function compileRules(json) {
fail(`building "${b.id}" needs a footprint of at least 1x1 tiles`);
}
if (!Number.isInteger(b.frame) || b.frame < 0) fail(`building "${b.id}" needs a non-negative integer frame`);
if (b.topperFrame != null && (!Number.isInteger(b.topperFrame) || b.topperFrame < 0)) {
fail(`building "${b.id}" topperFrame must be a non-negative integer`);
}
if (b.terrainMultiplier && !terrain.some((t) => b.terrainMultiplier in t)) {
fail(`building "${b.id}" terrainMultiplier "${b.terrainMultiplier}" is on no terrain type`);
}
@ -358,15 +349,6 @@ export function compileRules(json) {
}
}
}
// `upgradesFrom` names the building a def may be placed directly on top of, reclaiming half
// its build cost (see TALogic.buildCommand/findUpgradeTarget) — must be a real building, and
// one with a real cost to actually reclaim from.
for (const b of buildings) {
if (b.upgradesFrom == null) continue;
const from = buildingById[b.upgradesFrom];
if (!from) fail(`building "${b.id}" upgradesFrom references unknown building "${b.upgradesFrom}"`);
else if (!from.cost) fail(`building "${b.id}" upgradesFrom "${b.upgradesFrom}" has no cost to reclaim`);
}
// ---- victory modes -----------------------------------------------------
// The engine understands two rules: "commander" (an army dies with its Commander) and

View File

@ -140,12 +140,11 @@ export function skirmishSetup(scene, { onBack, onStart }) {
const skills = [1, 2, 3, 4, 5].map((n) => ({ label: `SKILL ${n}`, value: n }));
const victories = (rules.victoryModes ?? []).map((v) => ({ label: v.label, value: v.id, desc: v.desc }));
const victoryStart = Math.max(0, victories.findIndex((v) => v.value === rules.constants.victoryDefault));
const themeStart = Math.max(0, themes.findIndex((t) => t.value === sk.defaults?.theme));
const cfg = {
seed: (Math.random() * 0xffffffff) >>> 0,
size: sizes[0].value,
theme: themes[themeStart]?.value ?? 'grasslands',
theme: themes[0]?.value ?? 'grasslands',
symmetry: syms[0].value,
victory: victories[victoryStart]?.value ?? rules.constants.victoryDefault,
armies: [
@ -156,7 +155,7 @@ export function skirmishSetup(scene, { onBack, onStart }) {
const left = 300, right = GAME_WIDTH - 700;
cycler(scene, objs, left, 300, 'MAP SIZE', sizes, 0, (v) => { cfg.size = v; });
cycler(scene, objs, left, 400, 'THEME', themes, themeStart, (v) => { cfg.theme = v; });
cycler(scene, objs, left, 400, 'THEME', themes, 0, (v) => { cfg.theme = v; });
cycler(scene, objs, left, 500, 'SYMMETRY', syms, 0, (v) => { cfg.symmetry = v; });
cycler(scene, objs, left, 600, 'OPPONENT SKILL', skills, 2, (v) => { cfg.armies[1].skill = v; });

View File

@ -384,17 +384,6 @@ export default class TAWorldView {
this._addWorld(turret);
}
// A building's decorative topper (e.g. the Advanced Metal Generator's centrifuge): a second
// sprite stacked on the roof that spins continuously and fast, independent of the slow
// idleAnimation breathing/turn every other resource building gets — this is meant to read as
// active machinery, not ambient scenery. Hidden while under construction (see render()).
let topper = null;
if (def.isBuilding && def.topperFrame != null) {
topper = this.scene.add.image(e.x, e.y, key, def.topperFrame);
topper.setDisplaySize(def.footprint.w * this.ts, def.footprint.h * this.ts);
this._addWorld(topper);
}
// Anything that leaves the ground gets a flattened black copy of its own frame beneath it.
let shadow = null;
if (def.flight) {
@ -405,7 +394,7 @@ export default class TAWorldView {
// `alt` is the current altitude as a 0..1 fraction of def.flight.height, eased toward its
// target every frame in render(). Newly built units start on the ground.
s = {
img, turret, final, shadow, topper, defId: e.defId,
img, turret, final, shadow, defId: e.defId,
alt: 0, baseScale: img.scaleX,
batchDone: 0, batchPrev: 0, // production-dial accounting, see _drawProductionPies
};
@ -420,7 +409,6 @@ export default class TAWorldView {
s.turret?.destroy();
s.final?.destroy();
s.shadow?.destroy();
s.topper?.destroy();
this.sprites.delete(id);
}
@ -461,7 +449,6 @@ export default class TAWorldView {
if (s.turret) s.turret.setVisible(shown);
if (s.final) s.final.setVisible(shown);
if (s.shadow) s.shadow.setVisible(shown);
if (s.topper) s.topper.setVisible(shown);
if (!shown) continue;
const x = e.px + (e.x - e.px) * alpha;
@ -533,21 +520,6 @@ export default class TAWorldView {
s.img.setRotation((s.idleRot.turns + turnT) * (Math.PI / 2));
}
// The topper (e.g. the Advanced Metal Generator's centrifuge) spins fast and continuously
// off real time rather than sim ticks, so it stays smooth regardless of simSpeed and never
// needs its own per-entity timer state. Hidden while the site is still a wireframe skeleton
// — a spinning centrifuge on unfinished girders would read as broken, not "under construction".
if (s.topper) {
s.topper.setPosition(x, y);
s.topper.setDepth(band + (y / state.worldH) * 10 + 0.06);
if (e.site) {
s.topper.setAlpha(0);
} else {
const spinMs = 1800; // one full rotation every 1.8s — fast enough to read as active
s.topper.setAlpha(1).setRotation(((this.scene.time.now % spinMs) / spinMs) * Math.PI * 2);
}
}
if (s.turret) {
const tr = lerpAngle(e.pturretRot, e.turretRot, alpha);
s.turret.setPosition(x, y - lift).setRotation(tr);

View File

@ -23,7 +23,6 @@ import TAWorldView, { DEPTHS } from './TAWorldView.js';
import TAFx from './TAFx.js';
import TAHud from './TAHud.js';
import * as Screens from './TAScreens.js';
import { playIntroVideo } from './TAIntroVideo.js';
const SAVE_KEY = 'totalannihilation-save';
const SETTINGS_KEY = 'totalannihilation-settings';
@ -94,7 +93,7 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
.catch(() => { /* portraits are optional */ });
this.events.once('shutdown', () => this._teardown());
playIntroVideo(this, () => this.showMenu());
this.showMenu();
}
// -------------------------------------------------------------------------
@ -884,7 +883,7 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
this.placement.tx = tx; this.placement.ty = ty;
this.view.placement = {
def, tx, ty,
legal: Logic.canPlaceAt(this.match, this.rules, tx, ty, def, this.playerArmy),
legal: Logic.canPlaceAt(this.match, this.rules, tx, ty, def),
builderX: builder?.x, builderY: builder?.y,
buildRange: this.rules.defById[builder?.defId ?? '']?.buildRange ?? 0,
};

View File

@ -30,20 +30,20 @@ capacity the verify script holds every `frame` index to, and it is measured off
| Sheet name (JSON key) | Texture key | Cell | Cols | Contents |
|---|---|---|---|---|
| `arm-units` | `ta-arm-units` | 128×128 | 8 | ARM mobile units |
| `core-units` | `ta-core-units` | 128×128 | 8 | CORE mobile units |
| `arm-units` | `ta-arm-units` | 64×64 | 8 | ARM mobile units |
| `core-units` | `ta-core-units` | 64×64 | 8 | CORE mobile units |
| `arm-structures` | `ta-arm-structures` | 192×192 | 6×4 | ARM buildings |
| `core-structures` | `ta-core-structures` | 192×192 | 6×4 | CORE buildings |
| `terrain-grasslands` | `ta-terrain-grasslands` | 64×64 | 9 | Grassland tiles |
| `terrain-snowfields` | `ta-terrain-snowfields` | 64×64 | 9 | Snow tiles |
| `terrain-tropics` | `ta-terrain-tropics` | 64×64 | 9 | Jungle tiles |
| `icons` | `ta-icons` | 64×64 | 10 | Command/build glyphs — shared, NOT per-army |
| `icons` | `ta-icons` | 44×44 | 10 | Command/build glyphs |
Frames are numbered **left to right, then top to bottom**, starting at 0.
---
## Unit sheets (128×128 cells, 8 per row)
## Unit sheets (64×64 cells, 8 per row)
Both army sheets use the identical frame layout — that is what lets one unit definition serve
every army. `spritePx` is the on-screen size the frame is scaled to; draw to fill the cell and
@ -66,9 +66,6 @@ let the scale do the work.
| 12 | Fighter | plan-view airframe, swept wings, no tracks or wheels | 46 |
| 13 | Bomber | plan-view airframe, long straight wing, wing-mounted engines | 60 |
| 14 | Hover Constructor | skirted hull over a plenum, nanolathe crane, no tracks | 52 |
| 15 | Megatank | hull + tracks, wider than the Tank | 66 |
| 16 | Megatank turret | twin cannon barrels + side rocket pods | 66 |
| 17 | Advanced Construction Vehicle | tracked hull, twin nanolathe cranes, no gun | 54 |
### Units that leave the ground
@ -118,25 +115,17 @@ The Hover Constructor uses the same machinery at a much lower height — it shou
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 the cell centre (64, 64 for the current 128×128 unit cells), with the barrel
extending to the **right**.
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, rocket trooper, construction vehicle, advanced
construction vehicle, and all three Airfield units) simply has no turret frame; its whole
sprite rotates to face its target. The two Construction Vehicles and the Hover Constructor are
unarmed, so they never turn to aim regardless — they just rotate to face wherever they're
moving. Aircraft have fixed forward guns, so they aim by pointing the whole airframe, which is
why they have no turret either.
- A unit with no turret (infantry, sniper, rocket trooper, construction vehicle, and all three
Airfield units) simply has no turret frame; its whole sprite rotates to face its target. The
Construction Vehicle and Hover Constructor are unarmed, so they never turn to aim regardless —
they just rotate to face wherever they're moving. Aircraft have fixed forward guns, so they
aim by pointing the whole airframe, which is why they have no turret either.
Frames 1863 are free. Add a unit by appending a definition to `units[]` in
Frames 1563 are free. Add a unit by appending a definition to `units[]` in
`data/totalannihilation-rules.json` with its `frame` (and optional `turretFrame`) — no code.
A weapon may declare `barrels` (default 1) and, when `barrels > 1`, a `barrelSpacing` in world
px: every barrel fires at once on the same reload, each shot spawned that many px apart,
perpendicular to the firing line and centred on the hull. This is how the Megatank's twin
cannon and side rocket pods line up with the two barrels/pods painted into its turret frame —
measure the offset between them in the source PNG and scale by `spritePx / frameWidth`.
---
## Structure sheets (128×128 cells, 6 per row)
@ -175,11 +164,6 @@ build completes; the finished frame crossfades in underneath starting at 20% pro
| 19 | Advanced Radar — under construction | 2×2 | |
| 20 | Radar Jammer | 2×2 | squat emitter, no dish, broken rings — **placeholder** |
| 21 | Radar Jammer — under construction | 2×2 | |
| 22 | Nuclear Power Plant | 2×2 | cooling towers + reactor dome |
| 23 | Nuclear Power Plant — under construction | 2×2 | |
| 24 | Advanced Metal Generator | 2×2 | circular receptacle in the centre — the topper (frame 26) sits into it |
| 25 | Advanced Metal Generator — under construction | 2×2 | |
| 26 | Advanced Metal Generator topper — spinning centrifuge | — | **not a building**, see below |
The three sensor structures share a visual language deliberately: a low pad with corner
footings and **no armoured plinth**, so a glance separates "sensor" from "gun". They never
@ -195,36 +179,7 @@ Defensive structures never rotate, and their mount traverses freely in the simul
shoot in every direction regardless of how the art is drawn. Draw them facing **up** and do not
imply a firing arc.
**Frame 26 is a `topperFrame`, not a second building.** A building def may name one
(`"topperFrame": 26` on `advancedmassgen`) to get a second sprite stacked on top of the finished
structure, at the same display size as the building itself, that spins **continuously and
fast** — 360° every 1.8s, real time, independent of `idleAnimation`'s slow whole-building
breathing/turn (`TAWorldView`'s `render()`, the block right after the `idleAnimation` one).
Unlike a unit's `turretFrame`, a topper never aims at anything; it just spins, and it fades in
only once the building completes (hidden throughout the wireframe/build phase — a spinning
centrifuge on unfinished girders would read as broken machinery, not construction). Draw a
topper's frame centred exactly like a turret's, with matching transparent padding to whatever
extent it should read as smaller than the building itself — the renderer scales the whole cell
uniformly, so the padding you draw is the only thing controlling how big it looks once placed.
`sh.at(frame, ...)` isn't required for the sheet to hold it — see `mkCanvasSheet.finish()` — but
`TAArt.js`'s `structure` painter does draw a generic spinning-glyph fallback for any topper.
**`upgradesFrom` lets a building be placed directly on top of a friendly building it names,
consuming it for a 50% refund of ITS OWN build cost** (not a discount on the new building's
price — a genuine resource credit) instead of requiring clear ground. Both `nuclearplant`
(`upgradesFrom: "energygen"`) and `advancedmassgen` (`upgradesFrom: "massgen"`) use it. The
match has to be exact — same tile position AND same footprint size as the building being
replaced, checked by `TALogic.findUpgradeTarget` — so this is "swap this generator for its
advanced version," not "clear whatever's nearby." The old building disappears quietly: no death
explosion, no kill/loss stat, no 'unitDestroyed' blast FX, because reclaiming your own structure
to upgrade it isn't the same event as losing it. `TALogic.canPlaceAt` takes an optional 5th
`army` argument to recognise this — omit it (as the AI's placement search and most of the verify
fixtures do) and you get the plain "is this ground clear" answer with no upgrade awareness at
all, which is the deliberately conservative default for any caller that doesn't actually know
whose building is sitting there.
**No frames remain free in the current 6×6 sheet** (grown from 4 rows to 6 for this session,
1152×1152) — growing it another row is the next move for anything new.
Frames 2223 are free in the current 6×4 sheet; growing it another row adds six more.
---
@ -264,71 +219,24 @@ The theme then appears in the skirmish setup screen automatically.
---
## Icons (64×64 cells, 10 per row)
## Icons (44×44 cells, 10 per row)
Build-menu and command glyphs. Frame numbers come from each def's `icon` in the rules JSON
and from `commandIcons`, so this table is a reflection of that file, not a second source of
truth.
**Shared across both armies — this is the one sheet that does NOT get the "paint every army's
sheet in its own finished colours" treatment** from the top of this doc. There's a single
`icons` entry in the artwork JSON, not an `arm-icons`/`core-icons` pair, and the same frame
renders in either player's build menu. Paint it in a neutral palette (dark plate + light
linework is what the procedural fallback already does) rather than ARM blue or CORE red/orange.
**Wired into the build menu** (`TAHud._drawGrid`, since 2026-08-16): every button in the
build/production grid — buildings AND units — now draws this sheet's `def.icon` frame above the
shortened name and cost, at a **36×36 display size**. The command bar (Move/Attack/Patrol/…)
still renders text-only for now; its glyphs (frames 2029) only exist for the procedural
fallback painter today.
**The 64×64 source is intentionally bigger than that 36×36 display size.** Painting at exactly
the display size (the old 44×44 spec was already close to that trap) means every icon gets
upscaled a little and softens; painting bigger and letting Phaser scale down is what keeps it
crisp. Draw at the full 64×64 canvas — don't pre-shrink your art to "look right" at a smaller
preview size.
truth. Purely decorative today — the build menu labels itself from unit names — so this sheet
is the lowest priority of the eight.
| Frames | Contents |
|---|---|
| 09 | Units: commander, infantry, sniper, jeep, tank, rocket tank, rocket trooper, construction vehicle, fighter, bomber |
| 1016 | Buildings: energy gen, mass gen, barracks, vehicle plant, laser tower, missile launcher, advanced vehicle plant |
| 1719 | Overflow: hover constructor (unit), airfield, radar tower |
| 2029 | Commands: move, attack, attack-move, stop, hold, patrol, guard, assist, repair, rally |
| 3031 | Advanced radar, radar jammer |
| 32 | Megatank (unit) |
| 33 | Advanced Construction Vehicle (unit) |
| 3435 | Advanced Metal Generator, Nuclear Power Plant |
| 2029 | Commands: move, attack, attack-move, stop, hold, patrol, guard, assist, repair, rally |
Row 0 filled up before the Airfield units were added, which is why the hover constructor sits
next to the buildings at 17 rather than with the other units. Nothing reads a row as a
category — only the `icon` numbers matter. Frames 3639 are free.
### Painting priority: buildings the Commander and Construction Vehicle put up
This is the set that actually appears in the build-menu grid during a normal game — the union
of everything `commander`, `constructor` and `hoverconstructor` can build — in a sensible
painting order (the 8 the Commander itself can queue from turn one, then the 3 that need a
Construction Vehicle or Hover Constructor first):
| Frame | Building | Also needs a Constructor? | Footprint |
|---|---|---|---|
| 10 | Energy Generator | no — Commander builds it too | 2×2 |
| 11 | Mass Generator | no | 2×2 |
| 12 | Barracks | no | 3×3 |
| 13 | Vehicle Plant | no | 3×3 |
| 14 | Laser Tower | no | 1×1 |
| 15 | Missile Launcher | no | 2×2 |
| 18 | Airfield | no | 3×3 |
| 19 | Radar Tower | no | 2×2 |
| 16 | Advanced Vehicle Plant | **yes** | 4×4 |
| 30 | Advanced Radar | **yes** | 2×2 |
| 31 | Radar Jammer | **yes** | 2×2 |
Draw each as a simplified, high-contrast read of its full structure-sheet art (see Structure
sheets below), not a literal crop of it — a 128192px building shrunk straight down to 64px
loses its silhouette to surface detail. Keep the same silhouette and colour language (Laser
Tower's plinth-and-emitter, the Radar family's dish/mast/broken-rings distinction, etc.) so an
icon and its full-size building are still recognisably the same thing at a glance.
category — only the `icon` numbers matter.
---

View File

@ -72,14 +72,11 @@ const MISSIONS = [
defeat: 'KLAXON out-produced us and it showed.',
},
{
// Re-seeded three times as combat balance moved under it, each time measured over an
// 8-run sweep: 55930 -> 40411 when armed aircraft stopped being able to hover, then 40411
// (which fell to 4/8) -> 71644 when the AI learned to build static defences, then 71644
// (which fell to 6/8, and lost both of verify §13's own fixed sample runs — seed and
// seed+101 — even though the full 8-run rate was technically still at the bar) -> 67890
// (8/8) when rocketpod's armorMul was rebalanced weak-vs-vehicle/unchanged-vs-structure.
// Cy-Bro is skill 4 here, which is exactly the level at which defences switch on.
id: 'm05', name: 'Tropic Vice', seed: 67890, size: 'medium', theme: 'tropics', symmetry: 'mirror-x',
// Re-seeded twice as combat balance moved under it, both times measured over an 8-run
// sweep: 55930 -> 40411 when armed aircraft stopped being able to hover, then 40411 (which
// fell to 4/8) -> 71644 when the AI learned to build static defences. Cy-Bro is skill 4
// here, which is exactly the level at which defences switch on.
id: 'm05', name: 'Tropic Vice', seed: 71644, size: 'medium', theme: 'tropics', symmetry: 'mirror-x',
enemy: { commander: 'cybro', skill: 4, aggression: 0.7 },
startResources: [{ mass: 2600, energy: 4200 }, { mass: 1500, energy: 2600 }],
prebuilt: [
@ -87,17 +84,12 @@ const MISSIONS = [
{ id: 'barracks', dx: 4, dy: 3 },
{ enemy: true, id: 'energygen', dx: 3, dy: 2 },
],
briefing: 'Cy-Bro starts ahead and pushes early. Rockets crack their base open — armour holds the line getting there.',
briefing: 'Cy-Bro starts ahead and pushes early. Rocket tanks out-range everything CORE fields.',
victory: 'Cy-Bro is scrap on the sand.',
defeat: 'The push came faster than the plant did.',
},
{
// Re-seeded once, 8-run sweep: 63541 (fell to 0/8) -> 80808 (8/8) when static defence
// became a skill-3+ behaviour with a PROACTIVE first tower (TAAI.urgentDefence no longer
// waits for a raid to build the first one). KLAXON here is skill 5, well past that gate,
// so it now stands up a tower before the player-bot's rush can land — 90123 and 56789 also
// scored 8/8 in the same sweep if this needs revisiting again.
id: 'm06', name: 'Annihilation', seed: 80808, size: 'medium', theme: 'snowfields', symmetry: 'rotational',
id: 'm06', name: 'Annihilation', seed: 63541, size: 'medium', theme: 'snowfields', symmetry: 'rotational',
enemy: { commander: 'klaxon', skill: 5, aggression: 0.85 },
startResources: [{ mass: 3200, energy: 5200 }, { mass: 1800, energy: 3000 }],
prebuilt: [
@ -105,7 +97,7 @@ const MISSIONS = [
{ id: 'barracks', dx: 4, dy: 3 }, { id: 'vehicleplant', dx: -4, dy: 3 },
{ enemy: true, id: 'energygen', dx: 3, dy: -3 }, { enemy: true, id: 'barracks', dx: 4, dy: 3 },
],
briefing: 'KLAXON at full strength with a base already standing, dug in behind its own defences. Nothing clever left — out-build it and end it.',
briefing: 'KLAXON at full strength with a base already standing. Nothing clever left — out-build it and end it.',
victory: 'Total annihilation. There is nothing left to fight.',
defeat: 'KLAXON was simply better prepared.',
},

View File

@ -139,8 +139,6 @@ section('2. Artwork manifest');
const cap = (sheet.cols ?? 8) * (sheet.rows ?? 8);
check(`${d.id} frame ${d.frame} fits ${sheet.key}`, d.frame < cap);
if (d.turretFrame != null) check(`${d.id} turret frame fits`, d.turretFrame < cap);
if (d.buildFrame != null) check(`${d.id} build frame fits`, d.buildFrame < cap);
if (d.topperFrame != null) check(`${d.id} topper frame fits`, d.topperFrame < cap);
}
}
}
@ -218,9 +216,6 @@ section('2b. Procedural art actually paints');
if (d.buildFrame != null) {
check(`${d.id} build frame was painted`, tex?.frames.includes(d.buildFrame));
}
if (d.topperFrame != null) {
check(`${d.id} topper frame was painted`, tex?.frames.includes(d.topperFrame));
}
}
}
for (const [name, sheet] of Object.entries(artJson.sheets ?? {})) {
@ -699,81 +694,6 @@ section('3. Economy fixtures');
check('some terrain carries that multiplier', !!multTerrain);
}
// ---------------------------------------------------------------------------
section('3b. Building upgrades in place');
// ---------------------------------------------------------------------------
{
// The Advanced Metal Generator / Nuclear Power Plant can be placed directly on top of a
// friendly Mass/Energy Generator instead of needing clear ground, reclaiming half the older
// building's cost. This exercises the whole path end to end: canPlaceAt(..., army) recognising
// the spot, buildCommand consuming the old building and crediting the refund, and a real site
// for the new building landing exactly where the old one stood.
const map = generateMap(rules, { seed: 40, size: 'small', symmetry: 'mirror-x' });
const cmdStart = map.starts.find((s) => s.army === 0) ?? map.starts[0];
const tx = cmdStart.x + 3, ty = cmdStart.y;
const st = L.createMatch(rules, {
seed: 40, map: { ...map, buildings: [{ army: 0, type: 'massgen', tx, ty }] },
armies: [{ armyId: 'arm' }, { armyId: 'core' }],
});
const a = st.armies[0];
// Headroom below the cap, or the refund's Math.min(cap, ...) clamp would silently eat it and
// this fixture would "pass" while testing nothing.
a.massCap = 5000; a.energyCap = 5000; a.mass = 2500; a.energy = 2500;
const oldGen = st.entities.find((e) => e.defId === 'massgen' && !e.dead);
check('fixture mass generator placed', !!oldGen);
const advDef = rules.buildingById.advancedmassgen;
check('advanced metal generator declares upgradesFrom massgen', advDef.upgradesFrom === 'massgen');
const builder = L.spawnUnit(st, rules, 0, 'advancedconstructor', oldGen.x + 300, oldGen.y);
const massBefore = a.mass;
const expectedRefund = rules.buildingById.massgen.cost.mass * 0.5;
const r = L.issueOrder(st, rules, {
army: 0, unitIds: [builder.id],
order: { type: 'build', defId: 'advancedmassgen', tx, ty },
});
check('advanced metal generator order accepted directly on the mass generator', r.ok, r.error);
check('the old mass generator is gone', !!oldGen.dead);
check('half the mass generator\'s build cost was refunded',
Math.abs((a.mass - massBefore) - expectedRefund) < 1e-6,
`+${(a.mass - massBefore).toFixed(1)} vs expected +${expectedRefund}`);
const site = st.entities.find((e) => e.id === r.siteId);
check('a new advanced metal generator site stands in its exact footprint',
!!site && site.defId === 'advancedmassgen' && site.tx === tx && site.ty === ty && site.site === true);
// Fresh ground still works — upgrading in place is an alternative, not a replacement.
let fx = tx + 6, fy = ty;
while (!L.canPlaceAt(st, rules, fx, fy, advDef, 0).ok && fx < st.w - 4) fx++;
const r2 = L.issueOrder(st, rules, {
army: 0, unitIds: [builder.id],
order: { type: 'build', defId: 'advancedmassgen', tx: fx, ty: fy },
});
check('advanced metal generator also builds on fresh ground', r2.ok, r2.error);
// Ownership: an enemy's finished mass generator must not be upgradeable.
const enemyGen = L.placeBuilding(st, rules, 1, 'massgen', tx + 20, ty);
enemyGen.site = false; enemyGen.progress = 1; enemyGen.hp = rules.buildingById.massgen.hp;
check('cannot upgrade an enemy\'s mass generator', !L.canPlaceAt(st, rules, tx + 20, ty, advDef, 0).ok);
// Type mismatch: an Advanced Metal Generator's upgradesFrom is massgen, not energygen.
const wrongType = L.placeBuilding(st, rules, 0, 'energygen', tx + 30, ty);
wrongType.site = false; wrongType.progress = 1; wrongType.hp = rules.buildingById.energygen.hp;
check('cannot upgrade a different building type', !L.canPlaceAt(st, rules, tx + 30, ty, advDef, 0).ok);
// The reverse pairing: a Nuclear Power Plant upgrades that same Energy Generator.
const nukeDef = rules.buildingById.nuclearplant;
check('nuclear power plant declares upgradesFrom energygen', nukeDef.upgradesFrom === 'energygen');
check('nuclear power plant can place directly on that energy generator',
L.canPlaceAt(st, rules, tx + 30, ty, nukeDef, 0).ok);
// Without an army argument, canPlaceAt must fall back to the plain occupancy check — callers
// that don't know ownership (the AI's placement search, most of this very test file) should
// never silently start recognising upgrade spots.
check('canPlaceAt without an army never grants an upgrade placement',
!L.canPlaceAt(st, rules, tx + 30, ty, nukeDef).ok);
}
// ---------------------------------------------------------------------------
section('4. Pathfinding');
// ---------------------------------------------------------------------------
@ -1158,13 +1078,11 @@ section('6. Combat');
const rvs = equal('rockettank', 'sniper');
check('rocket tanks beat equal-cost snipers', rvs.fracA > rvs.fracB,
`${rvs.a} (${(rvs.fracA * 100) | 0}%) v ${rvs.b} (${(rvs.fracB * 100) | 0}%)`);
// Rockets are the answer to buildings and air, not to armour — a straight fight against a
// Tank is one the Rocket Tank should lose, which is what makes the roster a triangle
// (Tank counters Rocket Tank, Rocket Tank counters Sniper/structures) rather than a ladder
// where one unit is simply the answer.
// Rockets out-range and out-damage armour; that is what makes the roster a triangle
// rather than a ladder where one unit is simply the answer.
const rvt = equal('rockettank', 'tank');
check('tanks beat equal-cost rocket tanks', rvt.fracB > rvt.fracA,
`${rvt.b} tanks (${(rvt.fracB * 100) | 0}%) v ${rvt.a} rocket tanks (${(rvt.fracA * 100) | 0}%)`);
check('rocket tanks beat equal-cost tanks', rvt.fracA > rvt.fracB,
`${rvt.a} (${(rvt.fracA * 100) | 0}%) v ${rvt.b} (${(rvt.fracB * 100) | 0}%)`);
// Engagements must take long enough for reinforcement and composition to matter. When
// fights resolved in 2-4 seconds the whole AI skill ladder collapsed to a coin flip.
@ -1180,11 +1098,7 @@ section('6. Combat');
check('rifle is strong vs infantry, weak vs medium',
armorMul(rifle, 'infantry') > armorMul(rifle, 'medium') * 3);
const rocket = combatRules.weaponById.rocketpod;
check('rockets favour fortifications over vehicle armour',
armorMul(rocket, 'fortification') > armorMul(rocket, 'heavy') * 3);
check('rockets are weak against vehicle armour',
armorMul(rocket, 'light') < 1 && armorMul(rocket, 'medium') < 1 && armorMul(rocket, 'heavy') < 1,
`light ${armorMul(rocket, 'light')}, medium ${armorMul(rocket, 'medium')}, heavy ${armorMul(rocket, 'heavy')}`);
check('rockets favour armour over infantry', armorMul(rocket, 'heavy') > armorMul(rocket, 'infantry') * 3);
// Commander death explosion.
const dth = combatRules.unitById.commander.deathExplosion;