Compare commits
No commits in common. "a0b3b5f063054efa88dfcbc03bdf5e069021c2c4" and "4a8b78c53864e9daa2e3a24f048a8abe6c2d89d8" have entirely different histories.
a0b3b5f063
...
4a8b78c538
Binary file not shown.
|
Before Width: | Height: | Size: 76 KiB |
Binary file not shown.
|
|
@ -14,7 +14,7 @@
|
|||
},
|
||||
"unitSheet": {
|
||||
"key": "advancewars-units",
|
||||
"path": "assets/images/advancewars/advancewars-units.png",
|
||||
"path": null,
|
||||
"frameWidth": 48,
|
||||
"frameHeight": 64
|
||||
},
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
},
|
||||
"aiProfile": {
|
||||
"skill": 1,
|
||||
"aggression": 0.25,
|
||||
"aggression": 0.4,
|
||||
"captureWeight": 0.0
|
||||
},
|
||||
"briefing": [
|
||||
|
|
@ -41,12 +41,7 @@
|
|||
{
|
||||
"speaker": "ethel",
|
||||
"mood": "idle",
|
||||
"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."
|
||||
"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."
|
||||
}
|
||||
],
|
||||
"victoryLine": {
|
||||
|
|
@ -67,7 +62,7 @@
|
|||
"w.q..rr..c.w",
|
||||
"....mrr.....",
|
||||
"..rrrrrrrr..",
|
||||
"....wc.m....",
|
||||
"....w..m....",
|
||||
".rrrrrrrrrr.",
|
||||
".c.......q..",
|
||||
"....mm......",
|
||||
|
|
@ -168,12 +163,8 @@
|
|||
"aiProfile": {
|
||||
"skill": 1,
|
||||
"aggression": 0.3,
|
||||
"captureWeight": 0.5
|
||||
"captureWeight": 0.6
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "ethel",
|
||||
|
|
@ -299,8 +290,8 @@
|
|||
"type": "rout"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 1,
|
||||
"aggression": 0.35,
|
||||
"skill": 2,
|
||||
"aggression": 0.5,
|
||||
"captureWeight": 0.0
|
||||
},
|
||||
"briefing": [
|
||||
|
|
@ -462,14 +453,10 @@
|
|||
"type": "hq"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 1,
|
||||
"aggression": 0.35,
|
||||
"captureWeight": 0.25
|
||||
"skill": 2,
|
||||
"aggression": 0.4,
|
||||
"captureWeight": 0.3
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "jeff",
|
||||
|
|
@ -490,11 +477,6 @@
|
|||
"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": {
|
||||
|
|
@ -674,14 +656,10 @@
|
|||
"type": "rout"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 2,
|
||||
"aggression": 0.4,
|
||||
"captureWeight": 0.35
|
||||
"skill": 3,
|
||||
"aggression": 0.5,
|
||||
"captureWeight": 0.4
|
||||
},
|
||||
"unlockedUnits": [
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank", "rockets", "antiair"],
|
||||
["infantry", "mech", "recon", "tank", "apc", "artillery", "mdtank"]
|
||||
],
|
||||
"briefing": [
|
||||
{
|
||||
"speaker": "brad",
|
||||
|
|
@ -697,11 +675,6 @@
|
|||
"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": {
|
||||
|
|
@ -831,14 +804,10 @@
|
|||
"type": "rout"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 2,
|
||||
"aggression": 0.4,
|
||||
"skill": 3,
|
||||
"aggression": 0.45,
|
||||
"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",
|
||||
|
|
@ -854,11 +823,6 @@
|
|||
"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": {
|
||||
|
|
@ -978,12 +942,8 @@
|
|||
"aiProfile": {
|
||||
"skill": 2,
|
||||
"aggression": 0.4,
|
||||
"captureWeight": 0.35
|
||||
"captureWeight": 0.4
|
||||
},
|
||||
"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",
|
||||
|
|
@ -1004,11 +964,6 @@
|
|||
"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": {
|
||||
|
|
@ -1190,8 +1145,8 @@
|
|||
"type": "rout"
|
||||
},
|
||||
"aiProfile": {
|
||||
"skill": 2,
|
||||
"aggression": 0.45,
|
||||
"skill": 3,
|
||||
"aggression": 0.55,
|
||||
"captureWeight": 0.0
|
||||
},
|
||||
"briefing": [
|
||||
|
|
@ -1330,10 +1285,6 @@
|
|||
"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",
|
||||
|
|
@ -1349,11 +1300,6 @@
|
|||
"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": {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export function playBattleAnim(scene, rules, texKey, battles, onDone) {
|
|||
};
|
||||
dim.on('pointerdown', cleanup);
|
||||
|
||||
let delay = 700;
|
||||
let delay = 350;
|
||||
for (const b of battles) {
|
||||
const fromLeft = b.attackerId === first.attackerId;
|
||||
const shooter = fromLeft ? left : right;
|
||||
|
|
@ -99,20 +99,20 @@ export function playBattleAnim(scene, rules, texKey, battles, onDone) {
|
|||
if (finished) return;
|
||||
scene.tweens.add({
|
||||
targets: shooter.img, x: shooter.img.x + (fromLeft ? 40 : -40),
|
||||
duration: 150, yoyo: true,
|
||||
duration: 90, yoyo: true,
|
||||
});
|
||||
const flash = scene.add.rectangle(victim.img.x, victim.img.y - 60, W / 2 - 20, H - 60, 0xffffff, 0.55).setDepth(53);
|
||||
objs.push(flash);
|
||||
scene.tweens.add({ targets: flash, alpha: 0, duration: 450, onComplete: () => flash.destroy() });
|
||||
scene.tweens.add({ targets: flash, alpha: 0, duration: 240, onComplete: () => flash.destroy() });
|
||||
scene.cameras.main.shake(120, 0.004);
|
||||
if (fromLeft) { rightHp = b.defender.hp; setBar(right, rightHp); }
|
||||
else { leftHp = b.defender.hp; setBar(left, leftHp); }
|
||||
if (b.killed) {
|
||||
scene.tweens.add({ targets: victim.img, alpha: 0, y: victim.img.y + 16, duration: 550 });
|
||||
scene.tweens.add({ targets: victim.img, alpha: 0, y: victim.img.y + 16, duration: 320 });
|
||||
}
|
||||
}));
|
||||
delay += 1200;
|
||||
delay += 620;
|
||||
}
|
||||
timers.push(scene.time.delayedCall(delay + 700, cleanup));
|
||||
timers.push(scene.time.delayedCall(delay + 350, cleanup));
|
||||
return { skip: cleanup };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
// Advance Wars capture cut-in: a small window showing the capturing unit
|
||||
// hopping atop the property, then the building squishing down to reflect
|
||||
// the capture points remaining (or, if this action finished the capture,
|
||||
// popping back up to full height in the new owner's color). Driven purely
|
||||
// by the engine's 'capturing' (+ optional paired 'captured') events.
|
||||
|
||||
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
|
||||
import * as Logic from './AdvanceWarsLogic.js';
|
||||
import { armyColorInt } from './AdvanceWarsMapView.js';
|
||||
import { FONT } from './AdvanceWarsUI.js';
|
||||
|
||||
const W = 380;
|
||||
const H = 500;
|
||||
const BUILDING_SCALE = 2.6;
|
||||
const UNIT_SCALE = 2.2;
|
||||
const HOP = 30;
|
||||
const NUMBER_OFFSET_X = 95;
|
||||
|
||||
// capturing: engine 'capturing' event ({unitId, x, y, before, left}).
|
||||
// captured: the paired 'captured' event if this action finished the
|
||||
// capture ({unitId, x, y, army, prevOwner}), else null/undefined.
|
||||
export function playCaptureAnim(scene, rules, view, capturing, captured, onDone) {
|
||||
const unit = Logic.unitById(view.state, capturing.unitId);
|
||||
const k = Logic.tileKey(view.state, capturing.x, capturing.y);
|
||||
const t = rules.terrains[view.state.terrain[k]];
|
||||
if (!unit || !t) { onDone?.(); return null; }
|
||||
|
||||
const goal = rules.constants.captureGoal;
|
||||
const beforeFrac = capturing.before / goal;
|
||||
const afterFrac = capturing.left / goal;
|
||||
const priorOwner = captured ? captured.prevOwner : view.state.owner[k];
|
||||
|
||||
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2 - 40;
|
||||
const objs = [];
|
||||
let finished = false;
|
||||
const timers = [];
|
||||
|
||||
const dim = scene.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.35)
|
||||
.setDepth(50).setInteractive();
|
||||
const panel = scene.add.rectangle(cx, cy, W, H, 0x10141f, 0.97)
|
||||
.setStrokeStyle(3, 0x3a4260).setDepth(50);
|
||||
objs.push(dim, panel);
|
||||
|
||||
const groundCenterY = cy + H / 2 - 50;
|
||||
const ground = scene.add.rectangle(cx, groundCenterY, W - 60, 80, 0x27324a, 1).setDepth(50);
|
||||
objs.push(ground);
|
||||
const groundY = groundCenterY - 40; // top edge of the ground strip
|
||||
|
||||
const label = scene.add.text(cx, cy - H / 2 + 30, (t.name ?? 'Property').toUpperCase(),
|
||||
{ fontFamily: FONT, fontSize: '26px', color: '#ffffff' }).setOrigin(0.5, 0.5).setDepth(51);
|
||||
objs.push(label);
|
||||
|
||||
const building = scene.add.image(cx, groundY, view.tex.buildings, t.frame)
|
||||
.setOrigin(0.5, 1).setDepth(51)
|
||||
.setTint(armyColorInt(rules, priorOwner));
|
||||
building.scaleX = BUILDING_SCALE;
|
||||
building.scaleY = BUILDING_SCALE * beforeFrac;
|
||||
objs.push(building);
|
||||
|
||||
const spec = rules.unitById[unit.type];
|
||||
const unitImg = scene.add.image(cx, groundY - building.displayHeight, view.tex.units, spec.frame)
|
||||
.setOrigin(0.5, 1).setScale(UNIT_SCALE).setDepth(52)
|
||||
.setFlipX(unit.army !== 0)
|
||||
.setTint(armyColorInt(rules, unit.army));
|
||||
objs.push(unitImg);
|
||||
|
||||
// large capture-points counter, floating to the unit's left, always
|
||||
// vertically centered on it — tracked in every phase's onUpdate below.
|
||||
const pointsText = scene.add.text(cx - NUMBER_OFFSET_X, unitImg.y - unitImg.displayHeight / 2,
|
||||
String(capturing.before),
|
||||
{ fontFamily: FONT, fontSize: '64px', color: '#ffe14d', stroke: '#000000', strokeThickness: 6 })
|
||||
.setOrigin(0.5, 0.5).setDepth(52);
|
||||
objs.push(pointsText);
|
||||
const trackUnit = () => { pointsText.x = unitImg.x - NUMBER_OFFSET_X; pointsText.y = unitImg.y - unitImg.displayHeight / 2; };
|
||||
|
||||
const flip = scene.time.addEvent({
|
||||
delay: 450, loop: true, callback: () => {
|
||||
unitImg.setFrame(unitImg.frame.name === spec.frame ? spec.frame + 1 : spec.frame);
|
||||
},
|
||||
});
|
||||
timers.push(flip);
|
||||
|
||||
const cleanup = () => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
for (const timer of timers) timer.remove();
|
||||
for (const o of objs) o.destroy();
|
||||
onDone?.();
|
||||
};
|
||||
dim.on('pointerdown', cleanup);
|
||||
|
||||
function restY() { return groundY - building.displayHeight; }
|
||||
// live points value from the building's current squish fraction, so the
|
||||
// counter is always exactly in sync with what's on screen.
|
||||
function currentPoints() { return Math.round((building.scaleY / BUILDING_SCALE) * goal); }
|
||||
|
||||
function phase3raise() {
|
||||
if (finished) return;
|
||||
building.setTint(armyColorInt(rules, captured.army));
|
||||
scene.tweens.add({
|
||||
targets: building, scaleY: BUILDING_SCALE, duration: 500, ease: 'Sine.easeOut',
|
||||
onUpdate: () => { unitImg.y = restY(); trackUnit(); pointsText.setText(String(currentPoints())); },
|
||||
onComplete: () => timers.push(scene.time.delayedCall(300, cleanup)),
|
||||
});
|
||||
}
|
||||
|
||||
function phase2squish() {
|
||||
if (finished) return;
|
||||
// .75s: squish the building down to the new remaining-points fraction,
|
||||
// keeping the unit glued to the shrinking top edge every frame, and
|
||||
// counting the points-remaining number down in lockstep.
|
||||
scene.tweens.add({
|
||||
targets: building, scaleY: BUILDING_SCALE * afterFrac, duration: 750, ease: 'Sine.easeInOut',
|
||||
onUpdate: () => { unitImg.y = restY(); trackUnit(); pointsText.setText(String(currentPoints())); },
|
||||
onComplete: () => {
|
||||
if (captured) phase3raise();
|
||||
else timers.push(scene.time.delayedCall(400, cleanup));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 1.5s: two hops in place atop the (pre-squish) building height.
|
||||
scene.tweens.add({
|
||||
targets: unitImg, y: restY() - HOP, duration: 375, yoyo: true, repeat: 1, ease: 'Sine.easeOut',
|
||||
onUpdate: trackUnit,
|
||||
onComplete: phase2squish,
|
||||
});
|
||||
|
||||
return { skip: cleanup };
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ import { AdvanceWarsMapView, ensureSheets, armyColorInt } from './AdvanceWarsMap
|
|||
import { AdvanceWarsHUD, ActionMenu, DamagePreview, ProductionMenu, TileInfo, mkButton, mkText } from './AdvanceWarsUI.js';
|
||||
import * as Screens from './AdvanceWarsScreens.js';
|
||||
import { playBattleAnim } from './AdvanceWarsBattleAnim.js';
|
||||
import { playCaptureAnim } from './AdvanceWarsCaptureAnim.js';
|
||||
|
||||
const SAVE_KEY = 'advancewars-save';
|
||||
const ANIM_KEY = 'advancewars-battle-anims';
|
||||
|
|
@ -139,7 +138,6 @@ 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,
|
||||
|
|
@ -159,7 +157,6 @@ 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,
|
||||
|
|
@ -533,15 +530,6 @@ export default class AdvanceWarsGame extends Phaser.Scene {
|
|||
}
|
||||
this.crt.pulse(0.6, 260);
|
||||
}
|
||||
const capturing = events.find((e) => e.type === 'capturing');
|
||||
if (capturing) {
|
||||
const captured = events.find((e) => e.type === 'captured');
|
||||
const visible = !run.state.fog ||
|
||||
Logic.computeVision(this.rules, run.state, 0).has(Logic.tileKey(run.state, capturing.x, capturing.y));
|
||||
if (this.battleAnims && visible) {
|
||||
await new Promise((resolve) => playCaptureAnim(this, this.rules, run.view, capturing, captured, resolve));
|
||||
}
|
||||
}
|
||||
for (const e of events) {
|
||||
if (e.type === 'destroyed' || e.type === 'crashed') {
|
||||
run.view.boom(e.x, e.y);
|
||||
|
|
@ -556,21 +544,10 @@ export default class AdvanceWarsGame extends Phaser.Scene {
|
|||
if (e.type === 'meteor' || e.type === 'tsunami') this.crt.pulse(1.0, 500);
|
||||
if (e.type === 'dayStart' && e.army === 0) this.saveGame();
|
||||
}
|
||||
// Reveal only the unit(s) this entry actually touched. `state` is
|
||||
// already the whole turn's final result computed up front, so a
|
||||
// blanket sync would snap every not-yet-animated unit straight to
|
||||
// its end-of-turn position/hp — then animateMove would visibly jerk
|
||||
// it back to its start tile to replay the path. Wide army-level
|
||||
// effects (CO powers, meteor, etc.) already get their own dedicated
|
||||
// full-screen reveal, so those still do a full sync.
|
||||
const wideReveal = events.some((e) =>
|
||||
['powerFired', 'healedAll', 'refreshed', 'tsunami', 'meteor', 'armyEliminated'].includes(e.type));
|
||||
const touchedIds = wideReveal ? null : new Set(events.flatMap((e) =>
|
||||
[e.unitId, e.attackerId, e.defenderId, e.transportId, e.intoId, e.byId].filter((id) => id != null)));
|
||||
run.view.syncUnits(touchedIds);
|
||||
run.view.syncUnits();
|
||||
run.view.refreshFog(0);
|
||||
run.hud.refresh(run.state, 0);
|
||||
if (enemy && (moved || battles.length || capturing)) await this.wait(500);
|
||||
if (enemy && (moved || battles.length)) await this.wait(140);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ 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,
|
||||
|
|
@ -686,8 +685,6 @@ 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));
|
||||
|
|
@ -739,10 +736,9 @@ export function applyAction(state, rules, action) {
|
|||
if (state.owner[k] >= 0 && !hostile(state, state.owner[k], army)) return fail('allied property');
|
||||
const fx = coEffects(rules, state, army);
|
||||
const points = Math.floor(hpDisplay(unit) * (fx.captureMult ?? 1));
|
||||
const before = state.captureHp[k];
|
||||
state.captureHp[k] = Math.max(0, state.captureHp[k] - points);
|
||||
unit.capturing = state.captureHp[k] > 0;
|
||||
events.push({ type: 'capturing', unitId: unit.id, x: unit.x, y: unit.y, before, left: state.captureHp[k] });
|
||||
events.push({ type: 'capturing', unitId: unit.id, x: unit.x, y: unit.y, left: state.captureHp[k] });
|
||||
if (state.captureHp[k] === 0) {
|
||||
const prevOwner = state.owner[k];
|
||||
state.owner[k] = army;
|
||||
|
|
@ -926,10 +922,8 @@ 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)),
|
||||
|
|
|
|||
|
|
@ -15,18 +15,6 @@ import * as Logic from './AdvanceWarsLogic.js';
|
|||
const CELL = 48;
|
||||
const TALL = 64; // building/unit cells: 48×48 footprint + 16px headroom
|
||||
|
||||
// Units and buildings share one Y-sorted depth band (actors + row), so a
|
||||
// unit two rows down still draws over a building one row up, etc. This bias
|
||||
// only breaks a tie — same row, same depth — in the unit's favor, so a unit
|
||||
// standing on the same tile as a building always renders above it, without
|
||||
// disturbing the row-vs-row ordering (it's well under the 1-per-row step).
|
||||
const UNIT_DEPTH_BIAS = 0.5;
|
||||
|
||||
// building strength number: anchored just outside the building's left edge,
|
||||
// sinking back in by this many pixels so it reads as sitting on/against the
|
||||
// building rather than floating disconnected from it.
|
||||
const PROP_NUMBER_OVERLAP = 5;
|
||||
|
||||
// Painted drop-in sheets and the layout their procedural stand-ins mirror.
|
||||
export const SHEETS = {
|
||||
terrain: { key: 'advancewars-terrain', cellH: CELL, cols: 8, frames: 8 },
|
||||
|
|
@ -212,14 +200,9 @@ export class AdvanceWarsMapView {
|
|||
this.ox = areaX + Math.floor((areaW - this.tile * state.w) / 2);
|
||||
this.oy = areaY + Math.floor((areaH - this.tile * state.h) / 2);
|
||||
|
||||
// props (buildings) and units share one Y-sorted "actors" band — depth is
|
||||
// actors + row, so anything lower on screen (higher row) always draws in
|
||||
// front of anything above it, regardless of whether it's a unit or a
|
||||
// building. moveOv/atkOv sit below the actor band so the tinted-tile
|
||||
// overlay reads as a ground decal instead of washing over sprites.
|
||||
this.depths = { terrain: 1, moveOv: 2, atkOv: 3, actors: 4, fog: 20, path: 21, cursor: 22 };
|
||||
this.depths = { terrain: 1, props: 2, moveOv: 3, atkOv: 4, units: 5, fog: 12, path: 13, cursor: 14 };
|
||||
this.unitViews = new Map(); // unitId -> { c, sprite, hp, badge }
|
||||
this.propViews = new Map(); // tileKey -> { img, strength }
|
||||
this.propViews = new Map(); // tileKey -> image
|
||||
this.animStep = 0;
|
||||
|
||||
this.buildTerrain();
|
||||
|
|
@ -250,7 +233,7 @@ export class AdvanceWarsMapView {
|
|||
this.moveG?.destroy(); this.atkG?.destroy(); this.fogG?.destroy();
|
||||
this.pathG?.destroy(); this.cursor?.destroy();
|
||||
for (const v of this.unitViews.values()) v.c.destroy();
|
||||
for (const v of this.propViews.values()) { v.img.destroy(); v.strength.destroy(); }
|
||||
for (const img of this.propViews.values()) img.destroy();
|
||||
}
|
||||
|
||||
// pixel center of a tile
|
||||
|
|
@ -367,89 +350,44 @@ export class AdvanceWarsMapView {
|
|||
const img = this.scene.add.image(this.px(x), this.oy + (y + 1) * this.tile, this.tex.buildings, t.frame)
|
||||
.setOrigin(0.5, 1)
|
||||
.setScale(this.spriteScaleFor())
|
||||
.setDepth(this.depths.actors + y);
|
||||
// strength readout, sitting just left of the building itself (right-
|
||||
// anchored so it grows leftward with digit count, always overlapping
|
||||
// the building's left edge by the same few pixels regardless of
|
||||
// whether it's showing "5" or "20") — mirrors units' HP digit on the
|
||||
// opposite side so the two never collide on a garrisoned property.
|
||||
const strength = this.scene.add.text(
|
||||
this.px(x) - this.tile / 2 + PROP_NUMBER_OVERLAP, this.oy + (y + 1) * this.tile - 2, '', {
|
||||
fontFamily: 'm6x11, "Julius Sans One"', fontSize: `${Math.max(13, this.tile * 0.34)}px`,
|
||||
color: '#ffffff', stroke: '#000000', strokeThickness: 3,
|
||||
}).setOrigin(1, 1).setDepth(this.depths.actors + y + 0.25);
|
||||
this.propViews.set(k, { img, strength });
|
||||
.setDepth(this.depths.props);
|
||||
this.propViews.set(k, img);
|
||||
}
|
||||
this.refreshProps();
|
||||
}
|
||||
|
||||
refreshProps() {
|
||||
const goal = this.rules.constants.captureGoal;
|
||||
for (const [k, v] of this.propViews) {
|
||||
const owner = this.state.owner[k];
|
||||
v.img.setTint(armyColorInt(this.rules, owner));
|
||||
const hp = this.state.captureHp[k];
|
||||
// a building under partial capture stays visibly squished at rest —
|
||||
// height is remaining-capture-points / captureGoal, animated live by
|
||||
// playCaptureAnim while a capture action is actively resolving.
|
||||
v.img.scaleY = this.spriteScaleFor() * (hp / goal);
|
||||
// strength number only shows below full — tinted toward the owner's
|
||||
// color (or light gray if neutral) but blended well toward white so
|
||||
// it stays legible over the black stroke outline.
|
||||
if (hp < goal) {
|
||||
v.strength.setText(String(hp)).setColor(toHexColor(lighten(armyColorInt(this.rules, owner), 0.6))).setVisible(true);
|
||||
} else {
|
||||
v.strength.setVisible(false);
|
||||
}
|
||||
for (const [k, img] of this.propViews) {
|
||||
img.setTint(armyColorInt(this.rules, this.state.owner[k]));
|
||||
}
|
||||
}
|
||||
|
||||
// ── units ────────────────────────────────────────────────────────────────
|
||||
|
||||
// onlyIds, when given, restricts this pass to just those unit ids — any
|
||||
// other unit (known or not-yet-created) is left exactly as last shown.
|
||||
// Used by replayEvents to reveal a turn's units one action at a time
|
||||
// instead of snapping everyone straight to the turn's final result. Pass
|
||||
// nothing (or null) for a full sync — creates/updates/prunes everyone.
|
||||
syncUnits(onlyIds = null) {
|
||||
syncUnits() {
|
||||
const { state, rules, scene } = this;
|
||||
const present = new Set();
|
||||
const seen = new Set();
|
||||
for (const u of state.units) {
|
||||
present.add(u.id);
|
||||
const known = this.unitViews.has(u.id);
|
||||
if (!known && onlyIds && !onlyIds.has(u.id)) continue; // not revealed yet
|
||||
seen.add(u.id);
|
||||
let v = this.unitViews.get(u.id);
|
||||
const justCreated = !v;
|
||||
if (justCreated) {
|
||||
const c = scene.add.container(0, 0);
|
||||
if (!v) {
|
||||
const c = scene.add.container(0, 0).setDepth(this.depths.units);
|
||||
const sprite = scene.add.image(0, 0, this.tex.units, rules.unitById[u.type].frame)
|
||||
.setOrigin(0.5, 1).setScale(this.spriteScaleFor());
|
||||
const hp = scene.add.text(this.tile * 0.30, -2, '', {
|
||||
fontFamily: 'm6x11, "Julius Sans One"', fontSize: `${Math.max(13, this.tile * 0.34)}px`,
|
||||
color: '#ffffff', stroke: '#000000', strokeThickness: 3,
|
||||
}).setOrigin(0.5, 1);
|
||||
// sits directly above where the building's strength number renders
|
||||
// on this same tile (see refreshProps) — aligned to that number's
|
||||
// fixed right-edge anchor and a few pixels of padding above its top.
|
||||
const numberFontH = Math.max(13, this.tile * 0.34);
|
||||
const badgeH = this.tile * 0.4;
|
||||
const badge = scene.add.image(
|
||||
-this.tile / 2 + PROP_NUMBER_OVERLAP, -2 - numberFontH - 3 - badgeH / 2, this.tex.ui, UI_FRAMES.capture)
|
||||
const badge = scene.add.image(-this.tile * 0.32, -this.tile * 0.16, this.tex.ui, UI_FRAMES.capture)
|
||||
.setScale(this.spriteScaleFor() * 0.4).setVisible(false);
|
||||
c.add([sprite, hp, badge]);
|
||||
v = { c, sprite, hp, badge };
|
||||
this.unitViews.set(u.id, v);
|
||||
}
|
||||
if (justCreated || !onlyIds || onlyIds.has(u.id)) this.placeUnit(u, v);
|
||||
this.placeUnit(u, v);
|
||||
}
|
||||
// Stale-view cleanup only runs on a full sync — a scoped pass can't
|
||||
// tell a genuinely-dead unit from one whose own (not yet replayed)
|
||||
// death is later in this same turn's log; the 'destroyed'/'crashed'
|
||||
// event handlers already remove views at the right moment for that.
|
||||
if (!onlyIds) {
|
||||
for (const [id, v] of [...this.unitViews]) {
|
||||
if (!present.has(id)) { v.c.destroy(); this.unitViews.delete(id); }
|
||||
}
|
||||
for (const [id, v] of [...this.unitViews]) {
|
||||
if (!seen.has(id)) { v.c.destroy(); this.unitViews.delete(id); }
|
||||
}
|
||||
this.refreshProps();
|
||||
}
|
||||
|
|
@ -457,7 +395,7 @@ export class AdvanceWarsMapView {
|
|||
placeUnit(u, v = this.unitViews.get(u.id)) {
|
||||
if (!v) return;
|
||||
const { rules } = this;
|
||||
v.c.setPosition(this.px(u.x), this.oy + (u.y + 1) * this.tile).setDepth(this.depths.actors + u.y + UNIT_DEPTH_BIAS);
|
||||
v.c.setPosition(this.px(u.x), this.oy + (u.y + 1) * this.tile);
|
||||
v.sprite.setFrame(rules.unitById[u.type].frame + this.animStep);
|
||||
v.sprite.setFlipX(u.army !== 0);
|
||||
const base = armyColorInt(rules, u.army);
|
||||
|
|
@ -553,22 +491,21 @@ export class AdvanceWarsMapView {
|
|||
boom(x, y, onDone) {
|
||||
const img = this.scene.add.image(this.px(x), this.py(y), this.tex.ui, UI_FRAMES.explosion1)
|
||||
.setScale(this.spriteScaleFor()).setDepth(this.depths.cursor);
|
||||
this.scene.time.delayedCall(200, () => img.setFrame(UI_FRAMES.explosion2));
|
||||
this.scene.time.delayedCall(450, () => { img.destroy(); onDone?.(); });
|
||||
this.scene.time.delayedCall(110, () => img.setFrame(UI_FRAMES.explosion2));
|
||||
this.scene.time.delayedCall(260, () => { img.destroy(); onDone?.(); });
|
||||
}
|
||||
|
||||
// animate one unit view along a path (state already updated)
|
||||
animateMove(unitId, path, onDone) {
|
||||
const v = this.unitViews.get(unitId);
|
||||
if (!v || !path?.length) { onDone?.(); return; }
|
||||
const points = path.map((s) => ({ x: this.px(s.x), y: this.oy + (s.y + 1) * this.tile, row: s.y }));
|
||||
const points = path.map((s) => ({ x: this.px(s.x), y: this.oy + (s.y + 1) * this.tile }));
|
||||
let i = 0;
|
||||
const step = () => {
|
||||
if (i >= points.length) { onDone?.(); return; }
|
||||
const p = points[i++];
|
||||
v.c.setDepth(this.depths.actors + p.row + UNIT_DEPTH_BIAS);
|
||||
this.scene.tweens.add({
|
||||
targets: v.c, x: p.x, y: p.y, duration: 180, onComplete: step,
|
||||
targets: v.c, x: p.x, y: p.y, duration: 70, onComplete: step,
|
||||
});
|
||||
};
|
||||
step();
|
||||
|
|
@ -581,14 +518,3 @@ function darken(color, f) {
|
|||
const b = Math.floor((color & 0xff) * f);
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
// blend a color toward white by f (0 = unchanged, 1 = pure white) — used to
|
||||
// keep team-colored text pale enough to stay legible over its stroke.
|
||||
function lighten(color, f) {
|
||||
const r = Math.round(((color >> 16) & 0xff) + (255 - ((color >> 16) & 0xff)) * f);
|
||||
const g = Math.round(((color >> 8) & 0xff) + (255 - ((color >> 8) & 0xff)) * f);
|
||||
const b = Math.round((color & 0xff) + (255 - (color & 0xff)) * f);
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
function toHexColor(n) { return `#${n.toString(16).padStart(6, '0')}`; }
|
||||
|
|
|
|||
|
|
@ -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, unlockedUnits: m.unlockedUnits,
|
||||
fog: m.fog, production: m.production,
|
||||
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, unlockedUnits: m.unlockedUnits, startFunds: m.startFunds,
|
||||
fog: m.fog, production: m.production, 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