Compare commits
3 Commits
09a62f12cc
...
3c7184564a
| Author | SHA1 | Date |
|---|---|---|
|
|
3c7184564a | |
|
|
c690d80e68 | |
|
|
045327d38a |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 318 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 288 KiB |
|
|
@ -38,7 +38,7 @@
|
||||||
"arm-structures": {
|
"arm-structures": {
|
||||||
"key": "ta-arm-structures", "path": "assets/images/ta/arm-structures.png", "kind": "structure",
|
"key": "ta-arm-structures", "path": "assets/images/ta/arm-structures.png", "kind": "structure",
|
||||||
"frameWidth": 192, "frameHeight": 192, "cols": 6,
|
"frameWidth": 192, "frameHeight": 192, "cols": 6,
|
||||||
"rows": 4
|
"rows": 6
|
||||||
},
|
},
|
||||||
"core-units": {
|
"core-units": {
|
||||||
"key": "ta-core-units", "path": "assets/images/ta/core-units.png", "kind": "unit",
|
"key": "ta-core-units", "path": "assets/images/ta/core-units.png", "kind": "unit",
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
"core-structures": {
|
"core-structures": {
|
||||||
"key": "ta-core-structures", "path": "assets/images/ta/core-structures.png", "kind": "structure",
|
"key": "ta-core-structures", "path": "assets/images/ta/core-structures.png", "kind": "structure",
|
||||||
"frameWidth": 192, "frameHeight": 192, "cols": 6,
|
"frameWidth": 192, "frameHeight": 192, "cols": 6,
|
||||||
"rows": 4
|
"rows": 6
|
||||||
},
|
},
|
||||||
"terrain-grasslands": {
|
"terrain-grasslands": {
|
||||||
"key": "ta-terrain-grasslands", "path": "assets/images/ta/terrain-grasslands.png", "kind": "terrain",
|
"key": "ta-terrain-grasslands", "path": "assets/images/ta/terrain-grasslands.png", "kind": "terrain",
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
},
|
},
|
||||||
"icons": {
|
"icons": {
|
||||||
"key": "ta-icons", "path": null, "kind": "icon",
|
"key": "ta-icons", "path": null, "kind": "icon",
|
||||||
"frameWidth": 44, "frameHeight": 44, "cols": 10,
|
"frameWidth": 64, "frameHeight": 64, "cols": 10,
|
||||||
"rows": 4
|
"rows": 4
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -598,7 +598,7 @@
|
||||||
"id": "m05",
|
"id": "m05",
|
||||||
"name": "Tropic Vice",
|
"name": "Tropic Vice",
|
||||||
"theme": "tropics",
|
"theme": "tropics",
|
||||||
"seed": 71644,
|
"seed": 67890,
|
||||||
"playerArmy": "arm",
|
"playerArmy": "arm",
|
||||||
"playerCommander": "vance",
|
"playerCommander": "vance",
|
||||||
"enemies": [
|
"enemies": [
|
||||||
|
|
@ -628,7 +628,7 @@
|
||||||
{
|
{
|
||||||
"speaker": "ethel",
|
"speaker": "ethel",
|
||||||
"mood": "idle",
|
"mood": "idle",
|
||||||
"text": "Cy-Bro starts ahead and pushes early. Rocket tanks out-range everything CORE fields."
|
"text": "Cy-Bro starts ahead and pushes early. Rockets crack their base open — armour holds the line getting there."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"victoryLine": {
|
"victoryLine": {
|
||||||
|
|
@ -646,102 +646,102 @@
|
||||||
"h": 96,
|
"h": 96,
|
||||||
"theme": "tropics",
|
"theme": "tropics",
|
||||||
"rows": [
|
"rows": [
|
||||||
"~~r....^^^^^^^^^^^^...........rr~~~...r...^^^^^^^^^^^^...r...~~~rr...........^^^^^^^^^^^^....r~~",
|
"...............rr.rr.............rr....rrrr~~~....~~~rrrr....rr.............rr.rr...............",
|
||||||
"~~.....^^^^^^^^^^^^...........r.~~~........^^^....^^^........~~~.r...........^^^^^^^^^^^^.....~~",
|
"r.............rr..rr...................rrr~~~~~..~~~~~rrr...................rr..rr.............r",
|
||||||
"~~....rr^^^^^^^^^^^............r~~~~.r....................r.~~~~r............^^^^^^^^^^^rr....~~",
|
"..........rrrrrr...rrr...................r~~~~~..~~~~~r...................rrr...rrrrrr..........",
|
||||||
"~~~......^^^...................~~~~~~~rr................rr~~~~~~~...................^^^......~~~",
|
".........rrrrr.......rr..................~~~~~~~~~~~~~~..................rr.......rrrrr.........",
|
||||||
"~~~~.....rr....................~~~~~~~~~................~~~~~~~~~....................rr.....~~~~",
|
".......rr^^^^^............................~~~~~~~~~~~~............................^^^^^rr.......",
|
||||||
"~~~~......rr.....................~~~~~~rr..............rr~~~~~~.....................rr......~~~~",
|
"...r.^^^^^^^^^^..................................................................^^^^^^^^^^.r...",
|
||||||
"~~~~......rr..............rr.....~~.....rr............rr.....~~.....rr..............rr......~~~~",
|
"...r^^^^^^^^^^^^................................................................^^^^^^^^^^^^r...",
|
||||||
"~~~..................rrrrrrr.....~......rr............rr......~.....rrrrrrr..................~~~",
|
"..r^^^^^^^^^^^^^^^^...rr.........^r..........................r^.........rr...^^^^^^^^^^^^^^^^r..",
|
||||||
"~~~.................rrrrrrrr.....~r..........................r~.....rrrrrrrr.................~~~",
|
"..^^^^^^^^^^^^^^^^^^^^^rrrr...r^^^^..........................^^^^r...rrrr^^^^^^^^^^^^^^^^^^^^^..",
|
||||||
"~~~~................rrrr.............rr..................rr.............rrrr................~~~~",
|
".^^^^^^^^^^^^^^^^^^^^^^r.rr...^^^^^..........................^^^^^...rr.r^^^^^^^^^^^^^^^^^^^^^^.",
|
||||||
"~~~~~...............rrrr.............rr..................rr.............rrrr...............~~~~~",
|
"^^^^^^^^^^^^^^^^^^^^^^^^..r...^^^^^^^.r..................r.^^^^^^^...r..^^^^^^^^^^^^^^^^^^^^^^^^",
|
||||||
"~~~~~~................rrr...........rr....................rr...........rrr................~~~~~~",
|
"^^^^^^^^^^^^^^^^^^^^^^^^^^^....^^^^^^^^^................^^^^^^^^^....^^^^^^^^^^^^^^^^^^^^^^^^^^^",
|
||||||
"~~~~~~r...............rr................................................rr...............r~~~~~~",
|
"...^^^^..^.........^^^^^^^^.....^^^^^^^^^..............^^^^^^^^^.....^^^^^^^^.........^..^^^^...",
|
||||||
"~~~~~.rr................................................................................rr.~~~~~",
|
"...rrr.............^^^^^^^^.....^^^^^^^^^..............^^^^^^^^^.....^^^^^^^^.............rrr...",
|
||||||
"~~~...rr................................................................................rr...~~~",
|
"...........^^^.....^^^^^^^^....^^^^^^^^^................^^^^^^^^^....^^^^^^^^.....^^^...........",
|
||||||
"~~rr........................................................................................rr~~",
|
"..........^^^^^....^^^^^^^.....^^^^^^^....................^^^^^^^.....^^^^^^^....^^^^^..........",
|
||||||
"~~rr....................rr..............rr............rr..............rr....................rr~~",
|
"..........^^^^^.....^^^^rr.....^^^^^^...rr............rr...^^^^^^.....rr^^^^.....^^^^^..........",
|
||||||
".................rr....rrr..............rr............rr..............rrr....rr.................",
|
"...........^^^^....r^^^rrr.....^^^^^^..rrrr..........rrrr..^^^^^^.....rrr^^^r....^^^^...........",
|
||||||
".................rr..~~.....r....r...........rrrrrr...........r....r.....~~..rr.................",
|
"...........^^^^r...rr........r^^^^^^^^^rrrrr........rrrrr^^^^^^^^^r........rr...r^^^^...........",
|
||||||
".................r..~~~~...~~~~~~~~~.......^^^^..^^^^.......~~~~~~~~~...~~~~..r.................",
|
"...........^^^^.r............^^^^^^^^^^^^^r..........r^^^^^^^^^^^^^............r.^^^^...........",
|
||||||
"..................r.~~~~..~~~~~~~~~~~......^^^^..^^^^......~~~~~~~~~~~..~~~~.r..................",
|
"...........^^^^^rr..........^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..........rr^^^^^...........",
|
||||||
"..................r~~~~~~~~~~~~~~~~~~.....^^^^^^^^^^^^.....~~~~~~~~~~~~~~~~~~r..................",
|
"............^^..rr..........^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^..........rr..^^............",
|
||||||
".................r.~~~~~~~~~~~~~~~~~~.....^^^^^^^^^^^^.....~~~~~~~~~~~~~~~~~~.r.................",
|
"....rrr.....................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.....................rrr....",
|
||||||
"................rrr~~~~~~~~~~~~~~~~~~......^^^^..^^^^......~~~~~~~~~~~~~~~~~~rrr................",
|
"....rrrr......................^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^......................rrrr....",
|
||||||
"..............rrrr~~~~~~~.~~~~~~~~~~~...rr.^^^^..^^^^.rr...~~~~~~~~~~~.~~~~~~~rrrr..............",
|
"......r...........................^^^^^^^^^^^^^^^^^^^^^^^^^^^^...........................r......",
|
||||||
"..............rrr.~~~~~~...~~~~~~~~~~...rrr^^^^..^^^^rrr...~~~~~~~~~~...~~~~~~.rrr..............",
|
"...................................^^^^^^^^^^^^^^^^^^^^^^^^^^...................................",
|
||||||
"..............r............................^^^^..^^^^............................r..............",
|
"rrr.................................^^^^^^^^^^^^^^^^^^^^^^^^.................................rrr",
|
||||||
"..............r...........................^^^^^^^^^^^^...........................r..............",
|
"..r..................................^......rrr..rrr......^..................................r..",
|
||||||
"............rrrrrr.......................^^^^^^^^^^^^^^.......................rrrrrr............",
|
"........rrr..........................................................................rrr........",
|
||||||
"........rr.rrrrrrr......................^^^^^^^^^^^^^^^^......................rrrrrrr.rr........",
|
"........rrr..........................................................................rrr........",
|
||||||
"........rrrrr..rrr....................^^^^^^^^^^^^^^^^^^^^....................rrr..rrrrr........",
|
"........rrr......rr...........rr................................rr...........rr......rrr........",
|
||||||
"...........rr...rr...................^^^^^^^^^^^^^^^^^^^^^^...................rr...rr...........",
|
"r.........r.....rr............rr...rr......................rr...rr............rr.....r.........r",
|
||||||
"...........rr....r...........^......^^^^^^^^^^^^^^^^^^^^^^^^......^...........r....rr...........",
|
"................rr...........~..rrrrr......................rrrrr..~...........rr................",
|
||||||
"...........................r^^^...r^^^^^^^^^^^^^^^^^^^^^^^^^^r...^^^r...........................",
|
"..................rrrr.......~~~.r.~~~~~~~............~~~~~~~.r.~~~.......rrrr..................",
|
||||||
"r...........................^^^....^^^^^^^^^^^^^^^^^^^^^^^^^^....^^^...........................r",
|
"..................rrrr.......~~~...~~~~~~~~r........r~~~~~~~~...~~~.......rrrr..................",
|
||||||
"...................................r^^^^^^^^^^^^^^^^^^^^^^^^r...................................",
|
"................rrrrrr.......~~......~~~~~~~~......~~~~~~~~......~~.......rrrrrr................",
|
||||||
"......................................^^^^^^^^^^^^^^^^^^^^......................................",
|
"................rrrrr.................r..~~~~~....~~~~~..r.................rrrrr................",
|
||||||
".................................rrr.....^^^^^^..^^^^^^.....rrr.................................",
|
".................rrr......................~~~~....~~~~......................rrr.................",
|
||||||
".........MM.....................rrrrr.....^^^^....^^^^.....rrrrr.....................MM.........",
|
"..................rr....~.................~~~~~rr~~~~~.................~....rr..................",
|
||||||
".........MM.....^^...............rrrrr....................rrrrr...............^^.....MM.........",
|
"............rr....rr...r~.................~~~~~..~~~~~.................~r...rr....rr............",
|
||||||
"................^^...............rrrrr....................rrrrr...............^^................",
|
"~r..........~~....rr...r~.................~~~~~..~~~~~.................~r...rr....~~..........r~",
|
||||||
"...............^^^...............rrrrr....................rrrrr...............^^^...............",
|
"~~............~~~~rr...~~........^r.......~~~~~~~~~~~~.......r^........~~...rr~~~~............~~",
|
||||||
"................^^^...............r..........................r...............^^^................",
|
"~~..............~~rr..r~~.......^^^.......~~~~~~~~~~~~.......^^^.......~~r..rr~~..............~~",
|
||||||
"~~...............^^..........................................................^^...............~~",
|
"~~~..............~~.MMr~~.......^^^^......~~~~~~~~~~~~......^^^^.......~~rMM.~~..............~~~",
|
||||||
"~~~.MM...........^^..........................................................^^...........MM.~~~",
|
"~~~r.............~~.MM.~~......^^^^^....rr~~~~~~~~~~~~rr....^^^^^......~~.MM.~~.............r~~~",
|
||||||
"~~~.MM.......MM...^...rr................................................rr...^...MM.......MM.~~~",
|
"~~~...............~....~~......^^^^^....rr~~~~~~~~~~~~rr....^^^^^......~~....~...............~~~",
|
||||||
"~~...........MM.......rr................................................rr.......MM...........~~",
|
"~~~..MM...........~....~~......^^^^^....rr.~~~~..~~~~.rr....^^^^^......~~....~...........MM..~~~",
|
||||||
"~.....................rr................................................rr.....................~",
|
"~~r..MM...........r............^^^^^.......~~~....~~~.......^^^^^............r...........MM..r~~",
|
||||||
"r..............................................................................................r",
|
"~~..............................^^^^........rr....rr........^^^^..............................~~",
|
||||||
"....MM............rr........................................................rr............MM....",
|
"~~...................rr.........^^^^........................^^^^.........rr...................~~",
|
||||||
"....MM............rr........................................................rr............MM....",
|
"~.............MM....rrr.........^^^^........................^^^^.........rrr....MM.............~",
|
||||||
"..................rr~~~~................................................~~~~rr..................",
|
"..............MM....rrr.........^^^..........................^^^.........rrr....MM..............",
|
||||||
"..................r.~~~~~~~..r..............^^....^^..............r..~~~~~~~.r..................",
|
"................................^^^..........................^^^................................",
|
||||||
"...................~~~~~~~~rr..............^^^....^^^..............rr~~~~~~~~...................",
|
"................................^^^..........................^^^................................",
|
||||||
"...................~~~~~~~~~...............^^^^..^^^^...............~~~~~~~~~...................",
|
".................................^^r........................r^^.................................",
|
||||||
"rr.................~~~~~~~~~..............^^^^^..^^^^^..............~~~~~~~~~.................rr",
|
"...................................rr......................rr...................................",
|
||||||
"rrr................~~~~~~~~~..............^^^^^..^^^^^..............~~~~~~~~~................rrr",
|
"..........MM.......................rr......................rr.......................MM..........",
|
||||||
"..r................~~~~~~~~~................^^....^^................~~~~~~~~~................r..",
|
"..........MM........................................................................MM..........",
|
||||||
"...................~~~~~~~~..........................................~~~~~~~~...................",
|
".....rr..................................................................................rr.....",
|
||||||
"..................r~~~~~~~~.r......................................r.~~~~~~~~r..................",
|
".....rr..................................................................................rr.....",
|
||||||
"...................r~~~~~~.rrr.rr......rrrr..........rrrr......rr.rrr.~~~~~~r...................",
|
".....rr..................................................................................rr.....",
|
||||||
"...................r~~~~rrrrrrrrr.....rrrr............rrrr.....rrrrrrrrr~~~~r...................",
|
".....rr..................................................................................rr.....",
|
||||||
"....................rr..rrrrrr.rrr....rrrr............rrrr....rrr.rrrrrr..rr....................",
|
"....^rr.................~~~..........................................~~~.................rr^....",
|
||||||
".............r..........rrrrr....r....rr................rr....r....rrrrr..........r.............",
|
"..^^^^^................~~~~~~~~....rr......................rr....~~~~~~~~................^^^^^..",
|
||||||
"...........rrrrr.........rr..........rr..................rr..........rr.........rrrrr...........",
|
".^^^^^^^r.............~~~~~~~~~~...~~~~~~r.....rr.....r~~~~~~...~~~~~~~~~~.............r^^^^^^^.",
|
||||||
"..........~~~.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..~~~~~~........",
|
"..^^^^^^r....r.........r~~~~~~r....rr~~~~~~~~~~~~~~~~~~~~~~rr....r~~~~~~r.........r....r^^^^^^..",
|
||||||
"........~~~~~~..rr............................................................rr..~~~~~~........",
|
"...^^^^......r...............rr....r....~~~~~~~~~~~~~~~~....r....rr...............r......^^^^...",
|
||||||
"........~~~~~~..rr............................................................rr..~~~~~~........",
|
"....^^r......rr..............rr....rr.....~~~~~~~~~~~~.....rr....rr..............rr......r^^....",
|
||||||
"........~~~~~~~.......................rrrr............rrrr.......................~~~~~~~........",
|
".....rr.......rr.............rr.....rr....~~~~~~~~~~~~....rr.....rr.............rr.......rr.....",
|
||||||
".......~~~~~~~~......r..........r^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^r..........r......~~~~~~~~.......",
|
".....rr......................rr......r.....~~~~..~~~~.....r......rr......................rr.....",
|
||||||
".......~~~~~~~~.....rrrr........^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^........rrrr.....~~~~~~~~.......",
|
".....rr......................rr.^^^........~~~~rr~~~~........^^^.rr......................rr.....",
|
||||||
".......~~~~~~~~....rr.rr.......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.......rr.rr....~~~~~~~~.......",
|
".....rr......................rr^^^^^.......~~~~rr~~~~.......^^^^^rr......................rr.....",
|
||||||
".......~~~~~~~~.....~~.r.......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.......r.~~.....~~~~~~~~.......",
|
".....rr......................r^^^^^^.......~~~~..~~~~.......^^^^^^r......................rr.....",
|
||||||
".......~~~~~~~~.....~~..r......^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^......r..~~.....~~~~~~~~.......",
|
"..............................^^^^^^.......~~~~..~~~~.......^^^^^^..............................",
|
||||||
".......~~~~~~~~.....~~..rrr....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^....rrr..~~.....~~~~~~~~.......",
|
"..............................^^^^^^.......~~~....~~~.......^^^^^^..............................",
|
||||||
"......r~~~~~~~~.....~~~..rrr....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^....rrr..~~~.....~~~~~~~~r......",
|
".....................^r.......^^^^^^r.......~~....~~.......r^^^^^^.......r^.....................",
|
||||||
"......rr~~~~~~~.....~~~..rrr.....^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^.....rrr..~~~.....~~~~~~~rr......",
|
".....................^r........^^^^^r.......~~....~~.......r^^^^^........r^.....................",
|
||||||
"......rr~~~~~~~.....~~~~............^^^^^^^^^^^^^^^^^^^^^^^^............~~~~.....~~~~~~~rr......",
|
"...............................^^^^rr......................rr^^^^...............................",
|
||||||
"......rr.~~~~~r.....~~~~...............rrrr..........rrrr...............~~~~.....r~~~~~.rr......",
|
"................................^^^..........................^^^................................",
|
||||||
"^.........~~~~r...r~~~~~~..............rrrrrrrr..rrrrrrrr..............~~~~~~r...r~~~~.........^",
|
"................................^^^..........................^^^................................",
|
||||||
"^^............rr..r~~~~~~~................rrrrrrrrrrrr................~~~~~~~r..rr............^^",
|
"...............................rr.......rrr..........rrr.......rr...............................",
|
||||||
"^^^...........rr..r~~~~~~~................rrr..rr..rrr................~~~~~~~r..rr...........^^^",
|
"r..............................rr.......rrr..........rrr.......rr..............................r",
|
||||||
"^^^.....rr........r~~~~~~~...............rrrr......rrrr...............~~~~~~~r........rr.....^^^",
|
"r..............................rr.........r..........r.........rr..............................r",
|
||||||
"^^^^....rr.........~~~~~~~~...............rr........rr...............~~~~~~~~.........rr....^^^^",
|
".....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...........",
|
||||||
"^^^................~~~~~~~~...........r....r........r....r...........~~~~~~~~................^^^",
|
"..................rr........................................................rr..................",
|
||||||
"^^^.................~~~~~~...........rrr..rrr......rrr..rrr...........~~~~~~.................^^^",
|
"..................rrr......................................................rrr..................",
|
||||||
"^^.....r......rr.r..~~~~~~............r....r........r....r............~~~~~~..r.rr......r.....^^"
|
"..................r.rr..........r..............................r..........rr.r.................."
|
||||||
],
|
],
|
||||||
"starts": [
|
"starts": [
|
||||||
{
|
{
|
||||||
|
|
@ -787,7 +787,7 @@
|
||||||
"id": "m06",
|
"id": "m06",
|
||||||
"name": "Annihilation",
|
"name": "Annihilation",
|
||||||
"theme": "snowfields",
|
"theme": "snowfields",
|
||||||
"seed": 63541,
|
"seed": 80808,
|
||||||
"playerArmy": "arm",
|
"playerArmy": "arm",
|
||||||
"playerCommander": "vance",
|
"playerCommander": "vance",
|
||||||
"enemies": [
|
"enemies": [
|
||||||
|
|
@ -817,7 +817,7 @@
|
||||||
{
|
{
|
||||||
"speaker": "ethel",
|
"speaker": "ethel",
|
||||||
"mood": "idle",
|
"mood": "idle",
|
||||||
"text": "KLAXON at full strength with a base already standing. Nothing clever left — out-build it and end it."
|
"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."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"victoryLine": {
|
"victoryLine": {
|
||||||
|
|
@ -835,102 +835,102 @@
|
||||||
"h": 96,
|
"h": 96,
|
||||||
"theme": "snowfields",
|
"theme": "snowfields",
|
||||||
"rows": [
|
"rows": [
|
||||||
"................^^^...^^^^^.............^^^^^^.......r...rr....r.........r....r...........rrr...",
|
"~~~r....r...rrrrrr..........r.~~~~~~...............^^^^^^^^^^^^^^..................r....r..^^^^^",
|
||||||
"..............MM.^^..r^^^^^..............^^^^^............................................rrr...",
|
"~~~r..........r....MM.........~~~~~~...............^^^^^^^^^^^^^...........................^^^^^",
|
||||||
"..............MM......^^^^^^...............rr..............................................rr...",
|
"~~~...............rMM.........~~~~~r................^^^^^^^^^^^............................^^^^^",
|
||||||
"......................^^^^^^..............r...............................................rrr...",
|
"~~~...............rr..............r...................^^^^^^............................rr.^^^^^",
|
||||||
"....................................................................rrr...................rrr...",
|
"rr................rrrr....................................r..........rr.................^^^^^^^^",
|
||||||
"........................rr..........................................rrrr.....................r..",
|
"...................rrrr.r............................................rr................^^^^^^^^^",
|
||||||
"........................rr.........................................^..rr.......rr............rr.",
|
"...................rrrrrrrr............................................................^^^^^^^^^",
|
||||||
"................................................................r^^^^^..........rr...........rr.",
|
"....................r...rr..................r...........................................^^^^^^rr",
|
||||||
".......................~~~.......................................^^^^^^.......................rr",
|
"r.....MM...................................r^^................rr........................^^^^^^..",
|
||||||
"......................~~~~~.....................................^^^^^^^^........................",
|
"......MM.............^^^..................^^^^^............rrrrrrrrr...................^^^^^^^^.",
|
||||||
"......................~~~~~......................................^^^^^^^........................",
|
"..MM................^^^^^^.............rr^^^^^^^...........rrrrrrrrr.................r^^^^^^^^^^",
|
||||||
"......................~~~~.....rr..............................rrrr...........................rr",
|
"..MM...............^^^^^^^...rr........r^^^^^^^^^............r....^^r................^^^^^^^^^^^",
|
||||||
".......................~~......rr...........................rrrrr.......................rr....rr",
|
"...................^^^^^^^..rrr..rr....r^^^^^^^^^.................^^^................^^^^^^^^^^^",
|
||||||
"...............................r...rr.................rr....rrrr.........................r....rr",
|
"..................r^^^^^^^rrrrrr.rr....r^^^^^^^^^..................^........rr.......^^^^^^^^^^^",
|
||||||
"................................rrrrr................rrr.....rrr................................",
|
"..................r^^^^^^...rrrr..........^^^^^^rr.....rrrrrrr..............rrrrr.....^^^^^^^^^.",
|
||||||
"...............MM................rrrr..............rrrr.......rrr...............................",
|
"..................^^^^^^....rrrrr................rrrrr~~~~~rrr..............rrrrr........r...r..",
|
||||||
"...............MM...............rrr...............rrrrr...rr..rr................................",
|
".................^^^^^^r...rrr.rr..~.............rrr~~~~~~~~~.............................rrr...",
|
||||||
"......r..........................rr..r............rrrr....rrrrrr................rrr............r",
|
"~..............^^^^^.^.....rr....~~~~.rrr........rrr~~~~~~~~~..............................r....",
|
||||||
"r....MM.............................rrr.........................................rrr.............",
|
"~~.........r^^^^^^..............~~~~..rrr.............~~~~~~....................................",
|
||||||
".....MM..............................rr...........................~~~~~~~~~~~~~~~~~.............",
|
"~~MM.......^^^^^...............~~~~........................rrrrrr...............................",
|
||||||
".......................^^^^..rr......rr...r~~~..................~~~~~~~~~~~~~~~~~~~~............",
|
"~~MM.......^^^^^..............~~~~~........................rrrrrr...............................",
|
||||||
".........MM...........^^^^^.rrr......rr...~~~~~................~~~~~~~~~~~~~~~~~~~~~~~..........",
|
"~~~.......r^^^^^.............~~~~~~.........................rrrr................................",
|
||||||
".........MM.........^^^^^^^rrrr..........~~~~~~~...............~~~~~~~~~~~~~~~~~~~~~~~~..rr....r",
|
"~~........^^^^^^^.............~~~~~.............................................................",
|
||||||
"^^................r^^^^^rr...rr..........~~~~~~~...............~~~~~~~~~~~~~~~~~~~~~~~~.rrrrrrrr",
|
"..........^^^^^^^^..............~~~.............................................rrr.....r^......",
|
||||||
"^^^...............^^^^^..................~~~~~~~.......rr......~~~~~~~~~~~~~~~~~~~~~~~~~.rrrr...",
|
".........r^^^^^^^^................r.............................................rrr....^^^^.....",
|
||||||
"^^^................^^^^...................~~~~~........rr.......~~~~~~~~~~~~~~~~~~~~~~~~~~......",
|
".........r^^^^^^^^rrrr.....r....................................................rrrr...^^^^^....",
|
||||||
"r..................^^^^.........................................~~~~~~~~~~~~~~~~~~~~~~~~~~~~....",
|
"r..........^^^^^^^rrrr....rrr......................................................rr..^^^^^....",
|
||||||
"r..................rr^............................................~~~~~~~~~~~~~~~~~~~~~~~~~~~~..",
|
"r.............rrr....rrrrr.rr......................................................rrrrr^^^.....",
|
||||||
".......................................................................~~~~~~~~~~~~~~~~~~~~~~~~.",
|
".rrrr........rrrr.......^^..r.......................................................rr..........",
|
||||||
"...rr..................rr.................................................~~~~~~~~~~~~~~~~~~~~~~",
|
"..rrrr.......rr........^^^^^^.........................r...r...............................rrr...",
|
||||||
"r..rr..................rrr.......................................................~~~~~~~~~~~~~~~",
|
"^^.rrr............rr..r^^^^^^r.......................^^^^^^^............................^^^^^^^^",
|
||||||
"r..rr..................rrrr........................................................~~~~~~~~~~~~~",
|
"^^.rrrr...........rrrr.^^^^^.......................^^^^^^^^^^^^.r.......................^^^^^^^^",
|
||||||
"...rr....................rr..........rr....................r........................~~~~~~~~~~~~",
|
"^^rrrrr........rrrr...^^^^^.....^^^^^.............^^^^^^^^^^^^^^^rr....................^^^^^^^^^",
|
||||||
"..........................rr.........rr...................rrr.......................~~~~~~~~~~~~",
|
"^^rrr..........rrr^...^^^^.....r^^^^^.............^^^^^^^^^^^^^^^^^....................^^^^^^^^^",
|
||||||
"..........................rr......................rr.......r........................r~~~~~~~~~~~",
|
"^^.rr..........rrr....^^^r.....r^^^^...............^^^^^^^^^^^^^........................^^^^^^^^",
|
||||||
".........................rrrr...rr................rr.................................r~~~~~~~~~~",
|
"^..rr..rr...............r.......^^..................^^^^^^^^^..............rrr..................",
|
||||||
".........................rrrr...rr..r.............rr.................................rrr~~~~~~..",
|
"....r.rrr...............r...^^^^.....................r^^^^................rrrr~.................",
|
||||||
".........................r.r.....rrrrrr.............................................rrrrrrrrr...",
|
".....rrrr..............rrr..^^^^.....................rr...................rrrr~.................",
|
||||||
".................................rr..rr................^^...........................rr.rr.......",
|
"r......rr...............r...^^^^.....................rr.........................................",
|
||||||
"................................rr...................^^^^^..rr..................................",
|
".r...........................^^^^^^...r...........rrr........................................rr.",
|
||||||
".....rrr............~~..........rr..............^^^^^^^^^^..rr..................................",
|
".rr..........................^^^^^^^rrrr..........rrr.......................................rrr.",
|
||||||
".....rrr.......rr...~~..........rr........^^^^^^^^^^^^^^^^..rr..................................",
|
"..r...................~~~....^^^^^^^^^^r.r..........r.......................................rrr.",
|
||||||
".......rr......rr..~~~..........rr.......^^^^^^^^^^^^^^^^^..rr..................................",
|
"~~~....rrr..........~~~~~~...^^^^^^^^^^^^^^^^^^.............................................rrrr",
|
||||||
"........rr.........~~~..................^^^^^^^^^^^^^^^^^^..rrrr..............^.................",
|
"~~~~...rrrr........~~~~~~~....^^^^^^^^^^^^^^^^^^..........rr..................................~~",
|
||||||
"........rr.........~~~~.................^^^^^^^^^^^^^^^^^^^.rrrr.............^^^................",
|
"~~~~...rrrr........~~~~~~~....^^^^^^^^^^^^^^^^^^^.........rr......~~~........................~~~",
|
||||||
"...................~~~~................^^^^^^^^^^^^^^^^^^^..rrr..............^^^rrr......rrrrrrr",
|
"~~~~...rr..........~~~~~~~....^^^^^^^^^^^^^^^^^^^..........r......~~~........................~~~",
|
||||||
"...................~~~..................^^^^^^^^^^^^^^^^^^..rr...............^^^rrr......rrrrrrr",
|
"~~~~...rr..........~~~~~~~....^^^^^^^^^^^^^^^^^^.r........rr......~~..........................~~",
|
||||||
"....................~~..................^^^^^^^^^^^^^^^^......................^................r",
|
"~~~~...rr..........~~~~~~~....^^^^^^^^^^^^^^^^^..rrrrr....rr..r...~~.....r....................~~",
|
||||||
"r................^......................^^^^^^^^^^^^^^^^..................~~....................",
|
"~~....................r.....~~...r..rr....rrrrr..^^^^^^^^^^^^^^^^^....~~~~~~~..........rr...~~~~",
|
||||||
"rrrrrrr......rrr^^^...............rr..^^^^^^^^^^^^^^^^^^..................~~~...................",
|
"~~..........................~~......rr........r.^^^^^^^^^^^^^^^^^^....~~~~~~~..........rr...~~~~",
|
||||||
"rrrrrrr......rrr^^^..............rrr..^^^^^^^^^^^^^^^^^^^................~~~~...................",
|
"~~~........................~~~......r..........^^^^^^^^^^^^^^^^^^^....~~~~~~~..........rr...~~~~",
|
||||||
"................^^^.............rrrr.^^^^^^^^^^^^^^^^^^^.................~~~~.........rr........",
|
"~~~........................~~~......rr.........^^^^^^^^^^^^^^^^^^^....~~~~~~~........rrrr...~~~~",
|
||||||
".................^..............rrrr..^^^^^^^^^^^^^^^^^^..................~~~.........rr........",
|
"~~..................................rr..........^^^^^^^^^^^^^^^^^^....~~~~~~~........rrrr...~~~~",
|
||||||
"..................................rr..^^^^^^^^^^^^^^^^^.......rr..........~~~..rr......rr.......",
|
"rrrr.............................................^^^^^^^^^^^^^^^^^^...~~~~~~..........rrr....~~~",
|
||||||
"..................................rr..^^^^^^^^^^^^^^^^........rr..........~~...rr.......rrr.....",
|
".rrr.......................................r..........r.r^^^^^^^^^^....~~~...................r..",
|
||||||
"..................................rr..^^^^^^^^^^..............rr..........~~............rrr.....",
|
".rrr.......................................rrr..........rrrr^^^^^^^..........................rr.",
|
||||||
"..................................rr..^^^^^...................rr................................",
|
".rr........................................rrr...........r...^^^^^^...........................r.",
|
||||||
".......rr.rr...........................^^................rr..rr.................................",
|
".........................................rr.....................^^^^...r...............rr......r",
|
||||||
"...rrrrrrrrr.............................................rrrrrr.....r.r.........................",
|
".................~rrrr...................rr.....................^^^^..rrr..............rrrr.....",
|
||||||
"..~~~~~~rrr.................................rr.............r..rr...rrrr.........................",
|
".................~rrrr................^^^^r.....................^^^^...r...............rrr.r....",
|
||||||
"~~~~~~~~~~r.................................rr................rr...rrrr.........................",
|
"..................rrr..............^^^^^^^^^..................^^.......r...............rr..rr..^",
|
||||||
"~~~~~~~~~~~r........................r.......rr......................rr..........................",
|
"^^^^^^^^........................^^^^^^^^^^^^^...............^^^^r.....r^^^....rrr..........rr.^^",
|
||||||
"~~~~~~~~~~~~.......................rrr...................rr.........rr..........................",
|
"^^^^^^^^^....................^^^^^^^^^^^^^^^^^.............^^^^^r.....^^^^...^rrr..........rrr^^",
|
||||||
"~~~~~~~~~~~~........................r....................rr..........rr....................rr...",
|
"^^^^^^^^^....................rr^^^^^^^^^^^^^^^.............^^^^^.....^^^^^...rrrr........rrrrr^^",
|
||||||
"~~~~~~~~~~~~~........................................................rrrr..................rr..r",
|
"^^^^^^^^.......................r.^^^^^^^^^^^^.......................^^^^^.rrrr...........rrrr.^^",
|
||||||
"~~~~~~~~~~~~~~~.......................................................rrr..................rr..r",
|
"^^^^^^^^............................^^^^^^^.......................r^^^^^^r..rr............rrr.^^",
|
||||||
"~~~~~~~~~~~~~~~~~~~~~~.................................................rr..................rr...",
|
"...rrr...............................r...r.........................^^^^^^........rr.......rrrr..",
|
||||||
".~~~~~~~~~~~~~~~~~~~~~~~~.......................................................................",
|
"..........rr.......................................................r..^^.......rrrr........rrrr.",
|
||||||
"..~~~~~~~~~~~~~~~~~~~~~~~~~~~~............................................^rr..................r",
|
".....^^^rrrrr......................................................rr.rrrrr....rrr.............r",
|
||||||
"....~~~~~~~~~~~~~~~~~~~~~~~~~~~~.........................................^^^^..................r",
|
"....^^^^^..rr......................................................rrr....rrrr^^^^^^^..........r",
|
||||||
"......~~~~~~~~~~~~~~~~~~~~~~~~~~.......rr........~~~~~...................^^^^................^^^",
|
"....^^^^^...rrrr....................................................r.....rrrr^^^^^^^^r.........",
|
||||||
"...rrrr.~~~~~~~~~~~~~~~~~~~~~~~~~......rr.......~~~~~~~..................^^^^^...............^^^",
|
".....^^^^....rrr.............................................r................^^^^^^^^r.........",
|
||||||
"rrrrrrrr.~~~~~~~~~~~~~~~~~~~~~~~~...............~~~~~~~..........rr...rr^^^^^r................^^",
|
"......^r.....rrr.............................................~~~..............^^^^^^^^..........",
|
||||||
"r....rr..~~~~~~~~~~~~~~~~~~~~~~~~...............~~~~~~~..........rrrr^^^^^^^.........MM.........",
|
".............................................................~~~~~.............^^^^^^^........~~",
|
||||||
"..........~~~~~~~~~~~~~~~~~~~~~~~................~~~~~...rr......rrr.^^^^^...........MM.........",
|
"................................rrrr.........................~~~~~~.............^^^^^r.......~~~",
|
||||||
"............~~~~~~~~~~~~~~~~~~~~..................~~~r...rr......rr..^^^^.......................",
|
"...............................rrrrrr........................~~~~~..............^^^^^.......MM~~",
|
||||||
".............~~~~~~~~~~~~~~~~~...........................rr..............................MM.....",
|
"...............................rrrrrr........................~~~~...............^^^^^.......MM~~",
|
||||||
".............rrr.........................................rrr.............................MM....r",
|
"....................................~~~~~~.............rrr..~~~~..............^^^^^^r.........~~",
|
||||||
"r............rrr................rrrrrr....rrrr............r..rr..........................r......",
|
"....r..............................~~~~~~~~~rrr........rrr.~~~~....rr.....^.^^^^^..............~",
|
||||||
"................................rr..rr...rrrrr...............rrr...............MM...............",
|
"...rrr.............................~~~~~~~~~rrr.............~..rr.rrr...r^^^^^^.................",
|
||||||
"...............................rrr.......rrrr..............rrrr................MM...............",
|
"..r...r........rrrrr..............rrr~~~~~rrrrr................rrrrr....^^^^^^..................",
|
||||||
"................................rrr.....rrr................rrrrr................................",
|
".^^^^^^^^^.....rrrrr..............rrrrrrr.....rr^^^^^^..........rrrr...^^^^^^r..................",
|
||||||
"rr....r.........................rrrr....rr.................rr...r...............................",
|
"^^^^^^^^^^^.......rr........^..................^^^^^^^^^r....rr.rrrrrr^^^^^^^r..................",
|
||||||
"rr....rr.......................rrrrr...........................rr......~~.......................",
|
"^^^^^^^^^^^................^^^.................^^^^^^^^^r....rr..rrr..^^^^^^^...................",
|
||||||
"rr...........................rrrr..............................rr.....~~~~......................",
|
"^^^^^^^^^^^................r^^....r............^^^^^^^^^r........rr...^^^^^^^...............MM..",
|
||||||
"........................^^^^^^^......................................~~~~~......................",
|
"^^^^^^^^^^r.................rrrrrrrrr...........^^^^^^^rr.............^^^^^^................MM..",
|
||||||
"........................^^^^^^^^.....................................~~~~~......................",
|
".^^^^^^^^...................rrrrrrrrr............^^^^^..................^^^.............MM......",
|
||||||
"rr.......................^^^^^^.......................................~~~.......................",
|
"..^^^^^^........................rr................^^r...................................MM.....r",
|
||||||
".rr...........rr..........^^^^^r................................................................",
|
"rr^^^^^^...........................................r..................rr...r....................",
|
||||||
".rr............rr.......rr..^.........................................rr........................",
|
"^^^^^^^^^............................................................rrrrrrrr...................",
|
||||||
"..r.....................rrrr..........................................rr........................",
|
"^^^^^^^^^................rr............................................r.rrrr...................",
|
||||||
"...rrr...................rrr....................................................................",
|
"^^^^^^^^.................rr..........r....................................rrrr................rr",
|
||||||
"...rrr...............................................r..............^^^^^^......................",
|
"^^^^^.rr............................^^^^^^...................r..............rr...............~~~",
|
||||||
"...rr..............................................rr...............^^^^^^......MM..............",
|
"^^^^^............................^^^^^^^^^^^................r~~~~~.........MMr...............~~~",
|
||||||
"...rrr............................................^^^^^..............^^^^^r..^^.MM..............",
|
"^^^^^...........................^^^^^^^^^^^^^...............~~~~~~.........MM....r..........r~~~",
|
||||||
"...rrr...........r....r.........r....rr...r.......^^^^^^.............^^^^^...^^^................"
|
"^^^^^..r....r..................^^^^^^^^^^^^^^...............~~~~~~.r..........rrrrrr...r....r~~~"
|
||||||
],
|
],
|
||||||
"starts": [
|
"starts": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -402,9 +402,9 @@
|
||||||
],
|
],
|
||||||
"armorMul": {
|
"armorMul": {
|
||||||
"infantry": 0.3,
|
"infantry": 0.3,
|
||||||
"light": 0.6,
|
"light": 0.35,
|
||||||
"medium": 1.0,
|
"medium": 0.3,
|
||||||
"heavy": 1.4,
|
"heavy": 0.3,
|
||||||
"structure": 1.5,
|
"structure": 1.5,
|
||||||
"fortification": 1.6,
|
"fortification": 1.6,
|
||||||
"air": 2.6
|
"air": 2.6
|
||||||
|
|
@ -418,6 +418,78 @@
|
||||||
"sound": "sfx-ta-rocket-1",
|
"sound": "sfx-ta-rocket-1",
|
||||||
"impactSound": "sfx-ta-rocket-2"
|
"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",
|
"id": "shoulderrocket",
|
||||||
"name": "Shoulder Rocket",
|
"name": "Shoulder Rocket",
|
||||||
|
|
@ -878,6 +950,38 @@
|
||||||
"spritePx": 56,
|
"spritePx": 56,
|
||||||
"moveSound": "sfx-engine-heavy"
|
"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",
|
"id": "constructor",
|
||||||
"name": "Construction Vehicle",
|
"name": "Construction Vehicle",
|
||||||
|
|
@ -920,6 +1024,39 @@
|
||||||
"spritePx": 50,
|
"spritePx": 50,
|
||||||
"moveSound": "sfx-engine-heavy"
|
"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",
|
"id": "fighter",
|
||||||
"name": "Fighter",
|
"name": "Fighter",
|
||||||
|
|
@ -1078,6 +1215,38 @@
|
||||||
"icon": 10,
|
"icon": 10,
|
||||||
"idleAnimation": true
|
"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",
|
"id": "massgen",
|
||||||
"name": "Mass Generator",
|
"name": "Mass Generator",
|
||||||
|
|
@ -1110,6 +1279,39 @@
|
||||||
"icon": 11,
|
"icon": 11,
|
||||||
"idleAnimation": true
|
"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",
|
"id": "barracks",
|
||||||
"name": "Barracks",
|
"name": "Barracks",
|
||||||
|
|
@ -1252,9 +1454,8 @@
|
||||||
"buildTime": 55,
|
"buildTime": 55,
|
||||||
"buildPower": 140,
|
"buildPower": 140,
|
||||||
"builds": [
|
"builds": [
|
||||||
"jeep",
|
"megatank",
|
||||||
"tank",
|
"advancedconstructor"
|
||||||
"rockettank"
|
|
||||||
],
|
],
|
||||||
"spawnOffset": {
|
"spawnOffset": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
|
|
@ -1451,7 +1652,7 @@
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"size": "medium",
|
"size": "medium",
|
||||||
"symmetry": "mirror-x",
|
"symmetry": "mirror-x",
|
||||||
"theme": "grasslands",
|
"theme": "tropics",
|
||||||
"aiSkill": 3,
|
"aiSkill": 3,
|
||||||
"playerArmy": "arm"
|
"playerArmy": "arm"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -302,16 +302,19 @@ function chooseBuilding(ctx, mine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static defence is a skill-4-and-up behaviour, like the Airfield is skill 3 and up.
|
* Static defence is a skill-3-and-up behaviour, like the Airfield.
|
||||||
*
|
*
|
||||||
* These structures hit hard for their cost — a Laser Tower puts out 130 dps behind 1700 hp for
|
* 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
|
* 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 the
|
* two equally-matched AIs both digging in grind rather than resolve. Reserving them for skill 3
|
||||||
* stronger opponents puts that pressure where a player goes looking for it and leaves the low
|
* and up puts that pressure where a player goes looking for it once they're past the beginner
|
||||||
* end of the ladder playing the open game it was tuned for. Drop this to 3 (or 1) once there is
|
* tier, and leaves skill 1-2 playing the open game they were tuned for: `urgentDefence` and
|
||||||
* a unit whose job is cracking fortifications.
|
* `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.
|
||||||
*/
|
*/
|
||||||
const DEFENCE_MIN_SKILL = 4;
|
const DEFENCE_MIN_SKILL = 3;
|
||||||
|
|
||||||
/** Has anything hostile been inside the base perimeter recently? */
|
/** Has anything hostile been inside the base perimeter recently? */
|
||||||
function underThreat(ctx) {
|
function underThreat(ctx) {
|
||||||
|
|
@ -325,9 +328,18 @@ function enemyAirSeen(ctx) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The one structure the base needs RIGHT NOW, ahead of any economic consideration. Kept
|
* The one structure the base needs RIGHT NOW, ahead of any economic consideration. Kept
|
||||||
* deliberately tiny — one tower against a raid, one launcher against aircraft. Everything
|
* deliberately tiny — one tower, one launcher against aircraft. Everything beyond the first of
|
||||||
* beyond the first of each is a steady-state decision (see steadyDefence), because an AI that
|
* each is a steady-state decision (see steadyDefence), because an AI that answers pressure by
|
||||||
* answers pressure by building walls stops building an army and loses slowly instead of fast.
|
* 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.
|
||||||
*/
|
*/
|
||||||
function urgentDefence(ctx, mine) {
|
function urgentDefence(ctx, mine) {
|
||||||
const { rules } = ctx;
|
const { rules } = ctx;
|
||||||
|
|
@ -338,7 +350,7 @@ function urgentDefence(ctx, mine) {
|
||||||
// Anti-air first: a Laser Tower cannot shoot at aircraft at all, so a base being bombed gets
|
// Anti-air first: a Laser Tower cannot shoot at aircraft at all, so a base being bombed gets
|
||||||
// nothing whatsoever from one.
|
// nothing whatsoever from one.
|
||||||
if (aa && enemyAirSeen(ctx) && n('missilelauncher') < 1) return aa;
|
if (aa && enemyAirSeen(ctx) && n('missilelauncher') < 1) return aa;
|
||||||
if (tower && underThreat(ctx) && n('lasertower') + n('missilelauncher') < 1) return tower;
|
if (tower && n('lasertower') + n('missilelauncher') < 1) return tower;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -532,9 +544,11 @@ function applyCounters(ctx, weights) {
|
||||||
const seen = infantryish + armour;
|
const seen = infantryish + armour;
|
||||||
if (seen) {
|
if (seen) {
|
||||||
const armourShare = armour / seen;
|
const armourShare = armour / seen;
|
||||||
// Tanks shred infantry; rockets and snipers answer armour and buildings.
|
// The Rocket Tank's rocketpod is weak against vehicle armour (weak vs light/medium/heavy,
|
||||||
weights.tank *= 1 + (1 - armourShare) * 0.8;
|
// strong vs structure/fortification/air) — Tanks are now the answer to an armour-heavy
|
||||||
weights.rockettank *= 1 + armourShare * 1.0 + (structures > 2 ? 0.3 : 0);
|
// 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);
|
||||||
weights.sniper *= 1 + (1 - armourShare) * 0.5;
|
weights.sniper *= 1 + (1 - armourShare) * 0.5;
|
||||||
weights.rockettrooper *= 1 + armourShare * 0.6;
|
weights.rockettrooper *= 1 + armourShare * 0.6;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -671,7 +671,9 @@ const PROC_PAINTERS = {
|
||||||
structure(scene, key, spec, rules) {
|
structure(scene, key, spec, rules) {
|
||||||
const defs = rules.buildings.filter((b) => b.sheetSlot === 'structureSheet');
|
const defs = rules.buildings.filter((b) => b.sheetSlot === 'structureSheet');
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const d of defs) count = Math.max(count, d.frame + 1, (d.buildFrame ?? -1) + 1);
|
for (const d of defs) {
|
||||||
|
count = Math.max(count, d.frame + 1, (d.buildFrame ?? -1) + 1, (d.topperFrame ?? -1) + 1);
|
||||||
|
}
|
||||||
const sh = mkCanvasSheet(scene, key, spec.frameWidth, spec.frameHeight, spec.cols, count);
|
const sh = mkCanvasSheet(scene, key, spec.frameWidth, spec.frameHeight, spec.cols, count);
|
||||||
for (const d of defs) {
|
for (const d of defs) {
|
||||||
const draw = STRUCT_SHAPES[d.procShape];
|
const draw = STRUCT_SHAPES[d.procShape];
|
||||||
|
|
@ -684,6 +686,22 @@ const PROC_PAINTERS = {
|
||||||
ctx.globalAlpha = 1;
|
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();
|
return sh.finish();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,9 @@ export default class TAHud {
|
||||||
this._clearGrid();
|
this._clearGrid();
|
||||||
const s = this.scene;
|
const s = this.scene;
|
||||||
const army = this.state.armies[this.playerArmy];
|
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) => {
|
options.forEach((def, i) => {
|
||||||
const col = i % GRID_COLS, row = Math.floor(i / GRID_COLS);
|
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.
|
// The container is placed at the button's CENTRE, not its top-left — see below.
|
||||||
|
|
@ -286,13 +289,23 @@ export default class TAHud {
|
||||||
// child here is centred on (0,0) so the visual and the hit area coincide.
|
// 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)
|
const box = s.add.rectangle(0, 0, BTN_W, BTN_H, affordable ? 0x22304a : 0x1a2130, 1)
|
||||||
.setStrokeStyle(2, affordable ? EDGE : 0x2a3244);
|
.setStrokeStyle(2, affordable ? EDGE : 0x2a3244);
|
||||||
const label = s.add.text(0, -BTN_H / 2 + 12, shortName(def.name), {
|
const children = [box];
|
||||||
fontFamily: FONT, fontSize: '20px', color: affordable ? TEXT : '#5d6b80',
|
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',
|
||||||
}).setOrigin(0.5, 0);
|
}).setOrigin(0.5, 0);
|
||||||
const cost = s.add.text(0, -BTN_H / 2 + 44, `${def.cost.mass}m ${def.cost.energy}e`, {
|
const cost = s.add.text(0, hasIcon ? 18 : -BTN_H / 2 + 44, `${def.cost.mass}m ${def.cost.energy}e`, {
|
||||||
fontFamily: FONT, fontSize: '15px', color: affordable ? DIM : '#4e5a6d',
|
fontFamily: FONT, fontSize: hasIcon ? '12px' : '15px', color: affordable ? DIM : '#4e5a6d',
|
||||||
}).setOrigin(0.5, 0);
|
}).setOrigin(0.5, 0);
|
||||||
c.add([box, label, cost]);
|
children.push(label, cost);
|
||||||
|
c.add(children);
|
||||||
c.setSize(BTN_W, BTN_H);
|
c.setSize(BTN_W, BTN_H);
|
||||||
c.setInteractive({ useHandCursor: true });
|
c.setInteractive({ useHandCursor: true });
|
||||||
c.on('pointerover', () => box.setFillStyle(affordable ? 0x2e3f5f : 0x1a2130, 1));
|
c.on('pointerover', () => box.setFillStyle(affordable ? 0x2e3f5f : 0x1a2130, 1));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
@ -370,15 +370,58 @@ function buildCommand(state, rules, army, units, order, queue) {
|
||||||
if (!(bdef.builds ?? []).includes(order.defId)) {
|
if (!(bdef.builds ?? []).includes(order.defId)) {
|
||||||
return { ok: false, error: `${bdef.name} cannot build ${def.name}` };
|
return { ok: false, error: `${bdef.name} cannot build ${def.name}` };
|
||||||
}
|
}
|
||||||
const legal = canPlaceAt(state, rules, order.tx, order.ty, def);
|
const legal = canPlaceAt(state, rules, order.tx, order.ty, def, army);
|
||||||
if (!legal.ok) return legal;
|
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);
|
const site = placeBuilding(state, rules, army, order.defId, order.tx, order.ty);
|
||||||
if (!site) return { ok: false, error: 'placement failed' };
|
if (!site) return { ok: false, error: 'placement failed' };
|
||||||
pushOrder(builder, { type: 'build', targetId: site.id }, queue);
|
pushOrder(builder, { type: 'build', targetId: site.id }, queue);
|
||||||
return { ok: true, siteId: site.id };
|
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
|
* 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
|
* only ever clears the whole queue. Cancelling the active order (index 0) gets the same state
|
||||||
|
|
@ -418,12 +461,20 @@ export function cancelOrder(state, rules, army, unitId, index) {
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Can this footprint go here? Checks bounds, terrain buildability and occupancy. */
|
/**
|
||||||
export function canPlaceAt(state, rules, tx, ty, def) {
|
* 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) {
|
||||||
const fw = def.footprint.w, fh = def.footprint.h;
|
const fw = def.footprint.w, fh = def.footprint.h;
|
||||||
if (tx < 0 || ty < 0 || tx + fw > state.w || ty + fh > state.h) {
|
if (tx < 0 || ty < 0 || tx + fw > state.w || ty + fh > state.h) {
|
||||||
return { ok: false, error: 'off the map' };
|
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 y = ty; y < ty + fh; y++) {
|
||||||
for (let x = tx; x < tx + fw; x++) {
|
for (let x = tx; x < tx + fw; x++) {
|
||||||
const i = y * state.w + x;
|
const i = y * state.w + x;
|
||||||
|
|
@ -1644,59 +1695,67 @@ function acquireTarget(state, rules, e, def, hash, near) {
|
||||||
function fireWeapon(state, rules, e, w, target, facing, aimPt) {
|
function fireWeapon(state, rules, e, w, target, facing, aimPt) {
|
||||||
const aim = aimPt ?? aimPointOn(rules, e, target);
|
const aim = aimPt ?? aimPointOn(rules, e, target);
|
||||||
const muzzle = (w.fx?.muzzle ?? e.radius);
|
const muzzle = (w.fx?.muzzle ?? e.radius);
|
||||||
const ox = e.x + Math.cos(facing) * muzzle;
|
const barrels = w.barrels ?? 1;
|
||||||
const oy = e.y + Math.sin(facing) * muzzle;
|
// 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);
|
||||||
|
|
||||||
state.events.push({
|
for (let bi = 0; bi < barrels; bi++) {
|
||||||
t: 'weaponFired', id: e.id, army: e.army, weapon: w.id,
|
const lateral = barrels > 1 ? (bi - (barrels - 1) / 2) * (w.barrelSpacing / (barrels - 1)) : 0;
|
||||||
x: ox, y: oy, heading: facing, style: w.fx.style, sound: w.sound,
|
const ox = e.x + Math.cos(facing) * muzzle + perpX * lateral;
|
||||||
});
|
const oy = e.y + Math.sin(facing) * muzzle + perpY * lateral;
|
||||||
|
|
||||||
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({
|
state.events.push({
|
||||||
t: 'shot', style: w.fx.style, x1: ox, y1: oy, x2: ex, y2: ey,
|
t: 'weaponFired', id: e.id, army: e.army, weapon: w.id,
|
||||||
color: w.fx.color, width: w.fx.width, lifeMs: w.fx.lifeMs ?? 80, army: e.army,
|
x: ox, y: oy, heading: facing, style: w.fx.style, sound: w.sound,
|
||||||
});
|
});
|
||||||
if (hit) {
|
|
||||||
applyDamage(state, rules, target, w.damage * (w.armorMul?.[rules.defById[target.defId].armorClass] ?? 1), e);
|
if (w.kind === 'hitscan' || w.kind === 'beam') {
|
||||||
if (w.aoe) applyAoe(state, rules, aim.x, aim.y, w, e);
|
let hit = true;
|
||||||
state.events.push({ t: 'impact', x: ex, y: ey, radius: w.aoe ?? 8, style: w.fx.style, sound: w.impactSound });
|
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;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Travelling ordnance: leading matters, and a strafing target can genuinely be missed.
|
// Travelling ordnance: leading matters, and a strafing target can genuinely be missed.
|
||||||
let aimX = aim.x, aimY = aim.y;
|
let aimX = aim.x, aimY = aim.y;
|
||||||
if (w.leadTarget) {
|
if (w.leadTarget) {
|
||||||
const d = Math.hypot(target.x - ox, target.y - oy);
|
const d = Math.hypot(target.x - ox, target.y - oy);
|
||||||
const flight = d / w.speed;
|
const flight = d / w.speed;
|
||||||
aimX += (target.vx ?? 0) * flight;
|
aimX += (target.vx ?? 0) * flight;
|
||||||
aimY += (target.vy ?? 0) * flight;
|
aimY += (target.vy ?? 0) * flight;
|
||||||
}
|
}
|
||||||
let ang = Math.atan2(aimY - oy, aimX - ox);
|
let ang = Math.atan2(aimY - oy, aimX - ox);
|
||||||
if (w.spread > 0) ang += rngRange(state, -w.spread, w.spread);
|
if (w.spread > 0) ang += rngRange(state, -w.spread, w.spread);
|
||||||
|
|
||||||
if (state.projectiles.length >= rules.constants.projectileCap) return;
|
if (state.projectiles.length >= rules.constants.projectileCap) break;
|
||||||
state.projectiles.push({
|
state.projectiles.push({
|
||||||
id: state.nextProjectileId++,
|
id: state.nextProjectileId++,
|
||||||
weapon: w.id, army: e.army, ownerId: e.id,
|
weapon: w.id, army: e.army, ownerId: e.id,
|
||||||
x: ox, y: oy, px: ox, py: oy,
|
x: ox, y: oy, px: ox, py: oy,
|
||||||
vx: Math.cos(ang) * w.speed, vy: Math.sin(ang) * w.speed,
|
vx: Math.cos(ang) * w.speed, vy: Math.sin(ang) * w.speed,
|
||||||
targetId: w.kind === 'guided' ? target.id : 0,
|
targetId: w.kind === 'guided' ? target.id : 0,
|
||||||
aimX, aimY,
|
aimX, aimY,
|
||||||
ttl: Math.ceil(((w.range * 1.4) / w.speed) * rules.constants.tickHz),
|
ttl: Math.ceil(((w.range * 1.4) / w.speed) * rules.constants.tickHz),
|
||||||
trail: [],
|
trail: [],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stepProjectiles(state, rules) {
|
function stepProjectiles(state, rules) {
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,12 @@ export function compileRules(json) {
|
||||||
// The D-Gun is the reason this flag exists — left on auto-fire, a defending Commander
|
// 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.
|
// deletes one attacker every reload for free, which makes assaulting any base suicide.
|
||||||
w.manual = w.manual === true;
|
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);
|
const weaponById = indexById(weapons);
|
||||||
|
|
||||||
|
|
@ -297,6 +303,9 @@ export function compileRules(json) {
|
||||||
fail(`building "${b.id}" needs a footprint of at least 1x1 tiles`);
|
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 (!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)) {
|
if (b.terrainMultiplier && !terrain.some((t) => b.terrainMultiplier in t)) {
|
||||||
fail(`building "${b.id}" terrainMultiplier "${b.terrainMultiplier}" is on no terrain type`);
|
fail(`building "${b.id}" terrainMultiplier "${b.terrainMultiplier}" is on no terrain type`);
|
||||||
}
|
}
|
||||||
|
|
@ -349,6 +358,15 @@ 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 -----------------------------------------------------
|
// ---- victory modes -----------------------------------------------------
|
||||||
// The engine understands two rules: "commander" (an army dies with its Commander) and
|
// The engine understands two rules: "commander" (an army dies with its Commander) and
|
||||||
|
|
|
||||||
|
|
@ -140,11 +140,12 @@ export function skirmishSetup(scene, { onBack, onStart }) {
|
||||||
const skills = [1, 2, 3, 4, 5].map((n) => ({ label: `SKILL ${n}`, value: n }));
|
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 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 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 = {
|
const cfg = {
|
||||||
seed: (Math.random() * 0xffffffff) >>> 0,
|
seed: (Math.random() * 0xffffffff) >>> 0,
|
||||||
size: sizes[0].value,
|
size: sizes[0].value,
|
||||||
theme: themes[0]?.value ?? 'grasslands',
|
theme: themes[themeStart]?.value ?? 'grasslands',
|
||||||
symmetry: syms[0].value,
|
symmetry: syms[0].value,
|
||||||
victory: victories[victoryStart]?.value ?? rules.constants.victoryDefault,
|
victory: victories[victoryStart]?.value ?? rules.constants.victoryDefault,
|
||||||
armies: [
|
armies: [
|
||||||
|
|
@ -155,7 +156,7 @@ export function skirmishSetup(scene, { onBack, onStart }) {
|
||||||
|
|
||||||
const left = 300, right = GAME_WIDTH - 700;
|
const left = 300, right = GAME_WIDTH - 700;
|
||||||
cycler(scene, objs, left, 300, 'MAP SIZE', sizes, 0, (v) => { cfg.size = v; });
|
cycler(scene, objs, left, 300, 'MAP SIZE', sizes, 0, (v) => { cfg.size = v; });
|
||||||
cycler(scene, objs, left, 400, 'THEME', themes, 0, (v) => { cfg.theme = v; });
|
cycler(scene, objs, left, 400, 'THEME', themes, themeStart, (v) => { cfg.theme = v; });
|
||||||
cycler(scene, objs, left, 500, 'SYMMETRY', syms, 0, (v) => { cfg.symmetry = 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; });
|
cycler(scene, objs, left, 600, 'OPPONENT SKILL', skills, 2, (v) => { cfg.armies[1].skill = v; });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,17 @@ export default class TAWorldView {
|
||||||
this._addWorld(turret);
|
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.
|
// Anything that leaves the ground gets a flattened black copy of its own frame beneath it.
|
||||||
let shadow = null;
|
let shadow = null;
|
||||||
if (def.flight) {
|
if (def.flight) {
|
||||||
|
|
@ -394,7 +405,7 @@ export default class TAWorldView {
|
||||||
// `alt` is the current altitude as a 0..1 fraction of def.flight.height, eased toward its
|
// `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.
|
// target every frame in render(). Newly built units start on the ground.
|
||||||
s = {
|
s = {
|
||||||
img, turret, final, shadow, defId: e.defId,
|
img, turret, final, shadow, topper, defId: e.defId,
|
||||||
alt: 0, baseScale: img.scaleX,
|
alt: 0, baseScale: img.scaleX,
|
||||||
batchDone: 0, batchPrev: 0, // production-dial accounting, see _drawProductionPies
|
batchDone: 0, batchPrev: 0, // production-dial accounting, see _drawProductionPies
|
||||||
};
|
};
|
||||||
|
|
@ -409,6 +420,7 @@ export default class TAWorldView {
|
||||||
s.turret?.destroy();
|
s.turret?.destroy();
|
||||||
s.final?.destroy();
|
s.final?.destroy();
|
||||||
s.shadow?.destroy();
|
s.shadow?.destroy();
|
||||||
|
s.topper?.destroy();
|
||||||
this.sprites.delete(id);
|
this.sprites.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,6 +461,7 @@ export default class TAWorldView {
|
||||||
if (s.turret) s.turret.setVisible(shown);
|
if (s.turret) s.turret.setVisible(shown);
|
||||||
if (s.final) s.final.setVisible(shown);
|
if (s.final) s.final.setVisible(shown);
|
||||||
if (s.shadow) s.shadow.setVisible(shown);
|
if (s.shadow) s.shadow.setVisible(shown);
|
||||||
|
if (s.topper) s.topper.setVisible(shown);
|
||||||
if (!shown) continue;
|
if (!shown) continue;
|
||||||
|
|
||||||
const x = e.px + (e.x - e.px) * alpha;
|
const x = e.px + (e.x - e.px) * alpha;
|
||||||
|
|
@ -520,6 +533,21 @@ export default class TAWorldView {
|
||||||
s.img.setRotation((s.idleRot.turns + turnT) * (Math.PI / 2));
|
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) {
|
if (s.turret) {
|
||||||
const tr = lerpAngle(e.pturretRot, e.turretRot, alpha);
|
const tr = lerpAngle(e.pturretRot, e.turretRot, alpha);
|
||||||
s.turret.setPosition(x, y - lift).setRotation(tr);
|
s.turret.setPosition(x, y - lift).setRotation(tr);
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import TAWorldView, { DEPTHS } from './TAWorldView.js';
|
||||||
import TAFx from './TAFx.js';
|
import TAFx from './TAFx.js';
|
||||||
import TAHud from './TAHud.js';
|
import TAHud from './TAHud.js';
|
||||||
import * as Screens from './TAScreens.js';
|
import * as Screens from './TAScreens.js';
|
||||||
|
import { playIntroVideo } from './TAIntroVideo.js';
|
||||||
|
|
||||||
const SAVE_KEY = 'totalannihilation-save';
|
const SAVE_KEY = 'totalannihilation-save';
|
||||||
const SETTINGS_KEY = 'totalannihilation-settings';
|
const SETTINGS_KEY = 'totalannihilation-settings';
|
||||||
|
|
@ -93,7 +94,7 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
|
||||||
.catch(() => { /* portraits are optional */ });
|
.catch(() => { /* portraits are optional */ });
|
||||||
|
|
||||||
this.events.once('shutdown', () => this._teardown());
|
this.events.once('shutdown', () => this._teardown());
|
||||||
this.showMenu();
|
playIntroVideo(this, () => this.showMenu());
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
@ -883,7 +884,7 @@ export default class TotalAnnihilationGame extends Phaser.Scene {
|
||||||
this.placement.tx = tx; this.placement.ty = ty;
|
this.placement.tx = tx; this.placement.ty = ty;
|
||||||
this.view.placement = {
|
this.view.placement = {
|
||||||
def, tx, ty,
|
def, tx, ty,
|
||||||
legal: Logic.canPlaceAt(this.match, this.rules, tx, ty, def),
|
legal: Logic.canPlaceAt(this.match, this.rules, tx, ty, def, this.playerArmy),
|
||||||
builderX: builder?.x, builderY: builder?.y,
|
builderX: builder?.x, builderY: builder?.y,
|
||||||
buildRange: this.rules.defById[builder?.defId ?? '']?.buildRange ?? 0,
|
buildRange: this.rules.defById[builder?.defId ?? '']?.buildRange ?? 0,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| Sheet name (JSON key) | Texture key | Cell | Cols | Contents |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `arm-units` | `ta-arm-units` | 64×64 | 8 | ARM mobile units |
|
| `arm-units` | `ta-arm-units` | 128×128 | 8 | ARM mobile units |
|
||||||
| `core-units` | `ta-core-units` | 64×64 | 8 | CORE mobile units |
|
| `core-units` | `ta-core-units` | 128×128 | 8 | CORE mobile units |
|
||||||
| `arm-structures` | `ta-arm-structures` | 192×192 | 6×4 | ARM buildings |
|
| `arm-structures` | `ta-arm-structures` | 192×192 | 6×4 | ARM buildings |
|
||||||
| `core-structures` | `ta-core-structures` | 192×192 | 6×4 | CORE buildings |
|
| `core-structures` | `ta-core-structures` | 192×192 | 6×4 | CORE buildings |
|
||||||
| `terrain-grasslands` | `ta-terrain-grasslands` | 64×64 | 9 | Grassland tiles |
|
| `terrain-grasslands` | `ta-terrain-grasslands` | 64×64 | 9 | Grassland tiles |
|
||||||
| `terrain-snowfields` | `ta-terrain-snowfields` | 64×64 | 9 | Snow tiles |
|
| `terrain-snowfields` | `ta-terrain-snowfields` | 64×64 | 9 | Snow tiles |
|
||||||
| `terrain-tropics` | `ta-terrain-tropics` | 64×64 | 9 | Jungle tiles |
|
| `terrain-tropics` | `ta-terrain-tropics` | 64×64 | 9 | Jungle tiles |
|
||||||
| `icons` | `ta-icons` | 44×44 | 10 | Command/build glyphs |
|
| `icons` | `ta-icons` | 64×64 | 10 | Command/build glyphs — shared, NOT per-army |
|
||||||
|
|
||||||
Frames are numbered **left to right, then top to bottom**, starting at 0.
|
Frames are numbered **left to right, then top to bottom**, starting at 0.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Unit sheets (64×64 cells, 8 per row)
|
## Unit sheets (128×128 cells, 8 per row)
|
||||||
|
|
||||||
Both army sheets use the identical frame layout — that is what lets one unit definition serve
|
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
|
every army. `spritePx` is the on-screen size the frame is scaled to; draw to fill the cell and
|
||||||
|
|
@ -66,6 +66,9 @@ let the scale do the work.
|
||||||
| 12 | Fighter | plan-view airframe, swept wings, no tracks or wheels | 46 |
|
| 12 | Fighter | plan-view airframe, swept wings, no tracks or wheels | 46 |
|
||||||
| 13 | Bomber | plan-view airframe, long straight wing, wing-mounted engines | 60 |
|
| 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 |
|
| 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
|
### Units that leave the ground
|
||||||
|
|
||||||
|
|
@ -115,17 +118,25 @@ The Hover Constructor uses the same machinery at a much lower height — it shou
|
||||||
independently, so the unit aims while it drives. Rules:
|
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
|
- The turret's **pivot is the centre of the cell** — draw the turret so its rotation point
|
||||||
sits at (32, 32), with the barrel extending to the **right**.
|
sits at the cell centre (64, 64 for the current 128×128 unit cells), with the barrel
|
||||||
|
extending to the **right**.
|
||||||
- Leave the rest of the turret cell transparent. The hull shows through it.
|
- Leave the rest of the turret cell transparent. The hull shows through it.
|
||||||
- A unit with no turret (infantry, sniper, rocket trooper, construction vehicle, and all three
|
- A unit with no turret (infantry, sniper, rocket trooper, construction vehicle, advanced
|
||||||
Airfield units) simply has no turret frame; its whole sprite rotates to face its target. The
|
construction vehicle, and all three Airfield units) simply has no turret frame; its whole
|
||||||
Construction Vehicle and Hover Constructor are unarmed, so they never turn to aim regardless —
|
sprite rotates to face its target. The two Construction Vehicles and the Hover Constructor are
|
||||||
they just rotate to face wherever they're moving. Aircraft have fixed forward guns, so they
|
unarmed, so they never turn to aim regardless — they just rotate to face wherever they're
|
||||||
aim by pointing the whole airframe, which is why they have no turret either.
|
moving. Aircraft have fixed forward guns, so they aim by pointing the whole airframe, which is
|
||||||
|
why they have no turret either.
|
||||||
|
|
||||||
Frames 15–63 are free. Add a unit by appending a definition to `units[]` in
|
Frames 18–63 are free. Add a unit by appending a definition to `units[]` in
|
||||||
`data/totalannihilation-rules.json` with its `frame` (and optional `turretFrame`) — no code.
|
`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)
|
## Structure sheets (128×128 cells, 6 per row)
|
||||||
|
|
@ -164,6 +175,11 @@ build completes; the finished frame crossfades in underneath starting at 20% pro
|
||||||
| 19 | Advanced Radar — under construction | 2×2 | |
|
| 19 | Advanced Radar — under construction | 2×2 | |
|
||||||
| 20 | Radar Jammer | 2×2 | squat emitter, no dish, broken rings — **placeholder** |
|
| 20 | Radar Jammer | 2×2 | squat emitter, no dish, broken rings — **placeholder** |
|
||||||
| 21 | Radar Jammer — under construction | 2×2 | |
|
| 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
|
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
|
footings and **no armoured plinth**, so a glance separates "sensor" from "gun". They never
|
||||||
|
|
@ -179,7 +195,36 @@ 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
|
shoot in every direction regardless of how the art is drawn. Draw them facing **up** and do not
|
||||||
imply a firing arc.
|
imply a firing arc.
|
||||||
|
|
||||||
Frames 22–23 are free in the current 6×4 sheet; growing it another row adds six more.
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -219,24 +264,71 @@ The theme then appears in the skirmish setup screen automatically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Icons (44×44 cells, 10 per row)
|
## Icons (64×64 cells, 10 per row)
|
||||||
|
|
||||||
Build-menu and command glyphs. Frame numbers come from each def's `icon` in the rules JSON
|
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
|
and from `commandIcons`, so this table is a reflection of that file, not a second source of
|
||||||
truth. Purely decorative today — the build menu labels itself from unit names — so this sheet
|
truth.
|
||||||
is the lowest priority of the eight.
|
|
||||||
|
**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 20–29) 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.
|
||||||
|
|
||||||
| Frames | Contents |
|
| Frames | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 0–9 | Units: commander, infantry, sniper, jeep, tank, rocket tank, rocket trooper, construction vehicle, fighter, bomber |
|
| 0–9 | Units: commander, infantry, sniper, jeep, tank, rocket tank, rocket trooper, construction vehicle, fighter, bomber |
|
||||||
| 10–16 | Buildings: energy gen, mass gen, barracks, vehicle plant, laser tower, missile launcher, advanced vehicle plant |
|
| 10–16 | Buildings: energy gen, mass gen, barracks, vehicle plant, laser tower, missile launcher, advanced vehicle plant |
|
||||||
| 17–19 | Overflow: hover constructor (unit), airfield, radar tower |
|
| 17–19 | Overflow: hover constructor (unit), airfield, radar tower |
|
||||||
| 30–31 | Advanced radar, radar jammer |
|
|
||||||
| 20–29 | Commands: move, attack, attack-move, stop, hold, patrol, guard, assist, repair, rally |
|
| 20–29 | Commands: move, attack, attack-move, stop, hold, patrol, guard, assist, repair, rally |
|
||||||
|
| 30–31 | Advanced radar, radar jammer |
|
||||||
|
| 32 | Megatank (unit) |
|
||||||
|
| 33 | Advanced Construction Vehicle (unit) |
|
||||||
|
| 34–35 | Advanced Metal Generator, Nuclear Power Plant |
|
||||||
|
|
||||||
Row 0 filled up before the Airfield units were added, which is why the hover constructor sits
|
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
|
next to the buildings at 17 rather than with the other units. Nothing reads a row as a
|
||||||
category — only the `icon` numbers matter.
|
category — only the `icon` numbers matter. Frames 36–39 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 128–192px 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,14 @@ const MISSIONS = [
|
||||||
defeat: 'KLAXON out-produced us and it showed.',
|
defeat: 'KLAXON out-produced us and it showed.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Re-seeded twice as combat balance moved under it, both times measured over an 8-run
|
// Re-seeded three times as combat balance moved under it, each time measured over an
|
||||||
// sweep: 55930 -> 40411 when armed aircraft stopped being able to hover, then 40411 (which
|
// 8-run sweep: 55930 -> 40411 when armed aircraft stopped being able to hover, then 40411
|
||||||
// fell to 4/8) -> 71644 when the AI learned to build static defences. Cy-Bro is skill 4
|
// (which fell to 4/8) -> 71644 when the AI learned to build static defences, then 71644
|
||||||
// here, which is exactly the level at which defences switch on.
|
// (which fell to 6/8, and lost both of verify §13's own fixed sample runs — seed and
|
||||||
id: 'm05', name: 'Tropic Vice', seed: 71644, size: 'medium', theme: 'tropics', symmetry: 'mirror-x',
|
// 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',
|
||||||
enemy: { commander: 'cybro', skill: 4, aggression: 0.7 },
|
enemy: { commander: 'cybro', skill: 4, aggression: 0.7 },
|
||||||
startResources: [{ mass: 2600, energy: 4200 }, { mass: 1500, energy: 2600 }],
|
startResources: [{ mass: 2600, energy: 4200 }, { mass: 1500, energy: 2600 }],
|
||||||
prebuilt: [
|
prebuilt: [
|
||||||
|
|
@ -84,12 +87,17 @@ const MISSIONS = [
|
||||||
{ id: 'barracks', dx: 4, dy: 3 },
|
{ id: 'barracks', dx: 4, dy: 3 },
|
||||||
{ enemy: true, id: 'energygen', dx: 3, dy: 2 },
|
{ enemy: true, id: 'energygen', dx: 3, dy: 2 },
|
||||||
],
|
],
|
||||||
briefing: 'Cy-Bro starts ahead and pushes early. Rocket tanks out-range everything CORE fields.',
|
briefing: 'Cy-Bro starts ahead and pushes early. Rockets crack their base open — armour holds the line getting there.',
|
||||||
victory: 'Cy-Bro is scrap on the sand.',
|
victory: 'Cy-Bro is scrap on the sand.',
|
||||||
defeat: 'The push came faster than the plant did.',
|
defeat: 'The push came faster than the plant did.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'm06', name: 'Annihilation', seed: 63541, size: 'medium', theme: 'snowfields', symmetry: 'rotational',
|
// 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',
|
||||||
enemy: { commander: 'klaxon', skill: 5, aggression: 0.85 },
|
enemy: { commander: 'klaxon', skill: 5, aggression: 0.85 },
|
||||||
startResources: [{ mass: 3200, energy: 5200 }, { mass: 1800, energy: 3000 }],
|
startResources: [{ mass: 3200, energy: 5200 }, { mass: 1800, energy: 3000 }],
|
||||||
prebuilt: [
|
prebuilt: [
|
||||||
|
|
@ -97,7 +105,7 @@ const MISSIONS = [
|
||||||
{ id: 'barracks', dx: 4, dy: 3 }, { id: 'vehicleplant', dx: -4, dy: 3 },
|
{ 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 },
|
{ 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. Nothing clever left — out-build it and end it.',
|
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.',
|
||||||
victory: 'Total annihilation. There is nothing left to fight.',
|
victory: 'Total annihilation. There is nothing left to fight.',
|
||||||
defeat: 'KLAXON was simply better prepared.',
|
defeat: 'KLAXON was simply better prepared.',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,8 @@ section('2. Artwork manifest');
|
||||||
const cap = (sheet.cols ?? 8) * (sheet.rows ?? 8);
|
const cap = (sheet.cols ?? 8) * (sheet.rows ?? 8);
|
||||||
check(`${d.id} frame ${d.frame} fits ${sheet.key}`, d.frame < cap);
|
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.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,6 +218,9 @@ section('2b. Procedural art actually paints');
|
||||||
if (d.buildFrame != null) {
|
if (d.buildFrame != null) {
|
||||||
check(`${d.id} build frame was painted`, tex?.frames.includes(d.buildFrame));
|
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 ?? {})) {
|
for (const [name, sheet] of Object.entries(artJson.sheets ?? {})) {
|
||||||
|
|
@ -694,6 +699,81 @@ section('3. Economy fixtures');
|
||||||
check('some terrain carries that multiplier', !!multTerrain);
|
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');
|
section('4. Pathfinding');
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -1078,11 +1158,13 @@ section('6. Combat');
|
||||||
const rvs = equal('rockettank', 'sniper');
|
const rvs = equal('rockettank', 'sniper');
|
||||||
check('rocket tanks beat equal-cost snipers', rvs.fracA > rvs.fracB,
|
check('rocket tanks beat equal-cost snipers', rvs.fracA > rvs.fracB,
|
||||||
`${rvs.a} (${(rvs.fracA * 100) | 0}%) v ${rvs.b} (${(rvs.fracB * 100) | 0}%)`);
|
`${rvs.a} (${(rvs.fracA * 100) | 0}%) v ${rvs.b} (${(rvs.fracB * 100) | 0}%)`);
|
||||||
// Rockets out-range and out-damage armour; that is what makes the roster a triangle
|
// Rockets are the answer to buildings and air, not to armour — a straight fight against a
|
||||||
// rather than a ladder where one unit is simply the answer.
|
// 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.
|
||||||
const rvt = equal('rockettank', 'tank');
|
const rvt = equal('rockettank', 'tank');
|
||||||
check('rocket tanks beat equal-cost tanks', rvt.fracA > rvt.fracB,
|
check('tanks beat equal-cost rocket tanks', rvt.fracB > rvt.fracA,
|
||||||
`${rvt.a} (${(rvt.fracA * 100) | 0}%) v ${rvt.b} (${(rvt.fracB * 100) | 0}%)`);
|
`${rvt.b} tanks (${(rvt.fracB * 100) | 0}%) v ${rvt.a} rocket tanks (${(rvt.fracA * 100) | 0}%)`);
|
||||||
|
|
||||||
// Engagements must take long enough for reinforcement and composition to matter. When
|
// 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.
|
// fights resolved in 2-4 seconds the whole AI skill ladder collapsed to a coin flip.
|
||||||
|
|
@ -1098,7 +1180,11 @@ section('6. Combat');
|
||||||
check('rifle is strong vs infantry, weak vs medium',
|
check('rifle is strong vs infantry, weak vs medium',
|
||||||
armorMul(rifle, 'infantry') > armorMul(rifle, 'medium') * 3);
|
armorMul(rifle, 'infantry') > armorMul(rifle, 'medium') * 3);
|
||||||
const rocket = combatRules.weaponById.rocketpod;
|
const rocket = combatRules.weaponById.rocketpod;
|
||||||
check('rockets favour armour over infantry', armorMul(rocket, 'heavy') > armorMul(rocket, 'infantry') * 3);
|
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')}`);
|
||||||
|
|
||||||
// Commander death explosion.
|
// Commander death explosion.
|
||||||
const dth = combatRules.unitById.commander.deathExplosion;
|
const dth = combatRules.unitById.commander.deathExplosion;
|
||||||
|
|
|
||||||