feat(advance-wars): add unit progression system with mission-based unlocks
Introduce a unit unlock system that progressively grants access to new unit types as the player advances through the campaign. Each mission specifies which units each army can build, starting with basic units and unlocking MD Tanks, Rockets, Anti-Air, Missiles, Lander, Cruiser, and eventually the full armory. - Add unit sprite sheet (advancewars-units.png) and artwork config - Implement unlock validation in production and build options - Add tutorial dialogue from Ethel explaining new mechanics and units - Balance AI difficulty by reducing skill levels and aggression - Adjust capture weights for more balanced AI behavior - Add introductory city capture mechanic in first mission - Update verification tool to support unlockedUnits
This commit is contained in:
parent
4a8b78c538
commit
bf71bfc3c7
|
|
@ -14,7 +14,7 @@
|
|||
},
|
||||
"unitSheet": {
|
||||
"key": "advancewars-units",
|
||||
"path": null,
|
||||
"path": "assets/images/advancewars/advancewars-units.png",
|
||||
"frameWidth": 48,
|
||||
"frameHeight": 64
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
},
|
||||
"aiProfile": {
|
||||
"skill": 1,
|
||||
"aggression": 0.4,
|
||||
"aggression": 0.25,
|
||||
"captureWeight": 0.0
|
||||
},
|
||||
"briefing": [
|
||||
|
|
@ -41,7 +41,12 @@
|
|||
{
|
||||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "Deary, just remember: tanks are strong, artillery shoots from afar, and infantry are brave little things. Destroy every enemy unit and the day is ours."
|
||||
"text": "Oh, and dear — see that empty city in the middle of the field? Walk infantry onto it and choose Capture to claim it. It won't win the day, but every property we hold pays the bills."
|
||||
},
|
||||
{
|
||||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "Now remember: tanks are strong, artillery shoots from afar, and infantry are brave little things. Destroy every enemy unit and the day is ours."
|
||||
}
|
||||
],
|
||||
"victoryLine": {
|
||||
|
|
@ -62,7 +67,7 @@
|
|||
"w.q..rr..c.w",
|
||||
"....mrr.....",
|
||||
"..rrrrrrrr..",
|
||||
"....w..m....",
|
||||
"....wc.m....",
|
||||
".rrrrrrrrrr.",
|
||||
".c.......q..",
|
||||
"....mm......",
|
||||
|
|
@ -163,8 +168,12 @@
|
|||
"aiProfile": {
|
||||
"skill": 1,
|
||||
"aggression": 0.3,
|
||||
"captureWeight": 0.6
|
||||
"captureWeight": 0.5
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "ethel",
|
||||
|
|
@ -290,8 +299,8 @@
|
|||
"type": "rout"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 2,
|
||||
"aggression": 0.5,
|
||||
"skill": 1,
|
||||
"aggression": 0.35,
|
||||
"captureWeight": 0.0
|
||||
},
|
||||
"briefing": [
|
||||
|
|
@ -453,10 +462,14 @@
|
|||
"type": "hq"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 2,
|
||||
"aggression": 0.4,
|
||||
"captureWeight": 0.3
|
||||
"skill": 1,
|
||||
"aggression": 0.35,
|
||||
"captureWeight": 0.25
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "jeff",
|
||||
|
|
@ -477,6 +490,11 @@
|
|||
"speaker": "jeff",
|
||||
"mood": "idle",
|
||||
"text": "Come on over whenever you're ready. I'll be here. Sipping lemonade. Aiming."
|
||||
},
|
||||
{
|
||||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "One more thing, dear \u2014 headquarters is releasing the MD Tank to your motor pool. It costs more to build, but it hits harder and shrugs off return fire. Perfect for cracking a dug-in position like Jeff's."
|
||||
}
|
||||
],
|
||||
"victoryLine": {
|
||||
|
|
@ -656,10 +674,14 @@
|
|||
"type": "rout"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 3,
|
||||
"aggression": 0.5,
|
||||
"captureWeight": 0.4
|
||||
"skill": 2,
|
||||
"aggression": 0.4,
|
||||
"captureWeight": 0.35
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank", "rockets", "antiair"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "brad",
|
||||
|
|
@ -675,6 +697,11 @@
|
|||
"speaker": "natasha",
|
||||
"mood": "idle",
|
||||
"text": "Then we finish this before the snow falls. All units \u2014 forward!"
|
||||
},
|
||||
{
|
||||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "Before you head out: two new units are up for grabs. Rockets fire clear across the field without ever being fired back at, and the Anti-Air truck keeps our tanks safe from anything that flies. Build wisely."
|
||||
}
|
||||
],
|
||||
"victoryLine": {
|
||||
|
|
@ -804,10 +831,14 @@
|
|||
"type": "rout"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 3,
|
||||
"aggression": 0.45,
|
||||
"skill": 2,
|
||||
"aggression": 0.4,
|
||||
"captureWeight": 0.3
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank", "rockets", "antiair", "missiles"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank", "rockets", "antiair"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "ethel",
|
||||
|
|
@ -823,6 +854,11 @@
|
|||
"speaker": "maurice",
|
||||
"mood": "idle",
|
||||
"text": "Can't see a thing out there... Bucky says use recon units. Bucky's smart. Big vision range on those."
|
||||
},
|
||||
{
|
||||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "And dear, we've cleared the Missile Launcher for production. It hits just as far as rockets, but its crew keeps a much longer eye on things \u2014 perfect for punching through all this fog."
|
||||
}
|
||||
],
|
||||
"victoryLine": {
|
||||
|
|
@ -942,8 +978,12 @@
|
|||
"aiProfile": {
|
||||
"skill": 2,
|
||||
"aggression": 0.4,
|
||||
"captureWeight": 0.4
|
||||
"captureWeight": 0.35
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank", "rockets", "antiair", "missiles", "lander", "cruiser"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank", "rockets", "antiair", "missiles"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "schooner",
|
||||
|
|
@ -964,6 +1004,11 @@
|
|||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "Landers carry two land units across the sea and unload on those pale shoal tiles. Guard them \u2014 they can't fight back!"
|
||||
},
|
||||
{
|
||||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "Oh \u2014 and both the Lander and the Cruiser are yours to build now, not just borrow. Keep that channel escorted and the beachhead is ours."
|
||||
}
|
||||
],
|
||||
"victoryLine": {
|
||||
|
|
@ -1145,8 +1190,8 @@
|
|||
"type": "rout"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 3,
|
||||
"aggression": 0.55,
|
||||
"skill": 2,
|
||||
"aggression": 0.45,
|
||||
"captureWeight": 0.0
|
||||
},
|
||||
"briefing": [
|
||||
|
|
@ -1285,6 +1330,10 @@
|
|||
"aggression": 0.5,
|
||||
"captureWeight": 0.4
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "mdtank", "apc", "artillery", "rockets", "antiair", "missiles", "fighter", "bomber", "bcopter", "tcopter", "battleship", "cruiser", "lander", "sub"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank", "rockets", "antiair", "missiles", "lander", "cruiser"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "schooner",
|
||||
|
|
@ -1300,6 +1349,11 @@
|
|||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "You can win on land or at sea, dear \u2014 but don't ignore either. A battleship parked off your coast ruins everyone's morning."
|
||||
},
|
||||
{
|
||||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"text": "Last thing, dear \u2014 the whole armory's open to you now. Battleships, submarines, even our air wing are standing by. Whatever the enemy throws at us, we can answer in kind."
|
||||
}
|
||||
],
|
||||
"victoryLine": {
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ export default class AdvanceWarsGame extends Phaser.Scene {
|
|||
enemyCos: [meta.cfg.enemyCo],
|
||||
fog: meta.cfg.fog,
|
||||
production: true,
|
||||
unlockedUnits: undefined,
|
||||
startFunds: [3000, 3000],
|
||||
objective: { type: 'rout' },
|
||||
dayLimit: 60,
|
||||
|
|
@ -157,6 +158,7 @@ export default class AdvanceWarsGame extends Phaser.Scene {
|
|||
cos: [mission.playerCo, ...mission.enemyCos],
|
||||
fog: mission.fog,
|
||||
production: mission.production,
|
||||
unlockedUnits: mission.unlockedUnits,
|
||||
startFunds: mission.startFunds,
|
||||
objective: mission.objective,
|
||||
dayLimit: mission.dayLimit,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export function createGame(rules, mapDef, opts = {}) {
|
|||
turn: 0,
|
||||
fog: !!opts.fog,
|
||||
production: opts.production !== false,
|
||||
unlockedUnits: opts.unlockedUnits,
|
||||
rngState: (opts.seed ?? 1) >>> 0,
|
||||
w, h,
|
||||
terrain,
|
||||
|
|
@ -685,6 +686,8 @@ export function applyAction(state, rules, action) {
|
|||
if (!t.builds || state.owner[tileKey(state, x, y)] !== army) return fail('not your factory');
|
||||
const domain = spec.domain === 'air' ? 'air' : spec.domain === 'sea' ? 'sea' : 'land';
|
||||
if (t.builds !== domain) return fail('wrong factory type');
|
||||
const allowed = state.unlockedUnits?.[army];
|
||||
if (allowed && !allowed.includes(unitType)) return fail('unit not unlocked');
|
||||
if (unitAt(state, x, y)) return fail('tile occupied');
|
||||
const fx = coEffects(rules, state, army);
|
||||
const cost = Math.round(spec.cost * (fx.costMult ?? 1));
|
||||
|
|
@ -922,8 +925,10 @@ export function buildOptions(rules, state, x, y) {
|
|||
const army = state.turn;
|
||||
if (!t.builds || state.owner[tileKey(state, x, y)] !== army || unitAt(state, x, y)) return [];
|
||||
const fx = coEffects(rules, state, army);
|
||||
const allowed = state.unlockedUnits?.[army];
|
||||
return rules.units
|
||||
.filter((u) => (u.domain === 'air' ? 'air' : u.domain === 'sea' ? 'sea' : 'land') === t.builds)
|
||||
.filter((u) => !allowed || allowed.includes(u.id))
|
||||
.map((u) => ({
|
||||
type: u.id,
|
||||
cost: Math.round(u.cost * (fx.costMult ?? 1)),
|
||||
|
|
|
|||
|
|
@ -691,7 +691,7 @@ console.log('Campaign data');
|
|||
try {
|
||||
state = Logic.createGame(rules, m.map, {
|
||||
cos: [m.playerCo, ...m.enemyCos],
|
||||
fog: m.fog, production: m.production,
|
||||
fog: m.fog, production: m.production, unlockedUnits: m.unlockedUnits,
|
||||
startFunds: m.startFunds, objective: m.objective, dayLimit: m.dayLimit, seed: 42,
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
@ -796,7 +796,7 @@ console.log('AI');
|
|||
for (let i = 0; i < tries; i++) {
|
||||
const state = Logic.createGame(rules, m.map, {
|
||||
cos: [m.playerCo, ...m.enemyCos],
|
||||
fog: m.fog, production: m.production, startFunds: m.startFunds,
|
||||
fog: m.fog, production: m.production, unlockedUnits: m.unlockedUnits, startFunds: m.startFunds,
|
||||
objective: m.objective, dayLimit: m.dayLimit, seed: 500 + i,
|
||||
});
|
||||
let guard = (m.dayLimit ?? 60) * (1 + m.enemyCos.length) + 8;
|
||||
|
|
|
|||
Loading…
Reference in New Issue