Add faction crossfire: rivals shoot each other in war-zones
- Bullets and seekers now carry their shooter's factionId so a faction's fire drops rival-faction craft but never its own wing (classic pack bolts/seekers are unaffected) - NPC kills via crossfire are silent: no kill count, XP, defeat comm, or screen shake — the war is the world's, not the player's ledger - Survivors of a neutral-to-player side peel off once their rivals are spent, clearing the sky for the director's cadence - Bump faction cooldown to 40–60 s and update data/encounters tests
This commit is contained in:
parent
89fac9ef78
commit
1a581af211
|
|
@ -1679,11 +1679,11 @@
|
||||||
],
|
],
|
||||||
"_tiers": "THE LEVEL-6 JUMP (design): 1–2 → 2–3 → 5–7 (a ~2× step in the pack) → 7–9 → 9–12 (the cap — past level 13 the count holds and the roster's heavier types are the pressure). roster = the band's weighted pirate MIX (js/combat/Encounters.js → compose draws it into `pirateTypes`; a band without one leaves the scene's random pick standing — tiers 0–5 predate the rosters): raiders stay the anchor (the filler), spitter/bomber land at the jump, stinger/gunner at 9+, hunter (missiles) and barrage (rings) at 13+ — the heavy hitters, never the early pack.",
|
"_tiers": "THE LEVEL-6 JUMP (design): 1–2 → 2–3 → 5–7 (a ~2× step in the pack) → 7–9 → 9–12 (the cap — past level 13 the count holds and the roster's heavier types are the pressure). roster = the band's weighted pirate MIX (js/combat/Encounters.js → compose draws it into `pirateTypes`; a band without one leaves the scene's random pick standing — tiers 0–5 predate the rosters): raiders stay the anchor (the filler), spitter/bomber land at the jump, stinger/gunner at 9+, hunter (missiles) and barrage (rings) at 13+ — the heavy hitters, never the early pack.",
|
||||||
"faction": {
|
"faction": {
|
||||||
"_comment": "FACTION SPACE (the war-zones, js/combat/Encounters.js's compose context + GameScene._spawnFactionEncounter): in a faction-controlled system the ambient encounter is the LOCAL faction and one OPPOSING faction (js/combat/Encounters.js picks one of the other roster factions seeded; BOTH groups are sized off the `bands`' pirate counts, each drawn from its OWN combat.factions roster — no pirates/neutrals). DISPOSITION is per-faction, off the player's standing WITH each (js/reputation/Reputation.js → factionStanding, data/reputation.json): standing below `hostileBelow` → that faction targets the player and the player may return fire (the guns/skills' pools include it); at/above `hostileBelow` → it fights only the other faction, never the player (its bolts/seekers/rams can't touch the ship), and once its rival is spent it peels off (CombatSystem._peelOff) so the encounter ends on its own — the player observes. Factions ALWAYS fight each other while they're both up (CombatSystem.pickEnemyTarget). TIMERS: faction space rolls more often than neutral wild space (rollIntervalMin..Max, the director's roll schedule) with a shorter quiet after each encounter (cooldownMin..Max). The ships are the factions' OWN roster crafts (no new art).",
|
"_comment": "FACTION SPACE (the war-zones, js/combat/Encounters.js's compose context + GameScene._spawnFactionEncounter): in a faction-controlled system the ambient encounter is the LOCAL faction and one OPPOSING faction (js/combat/Encounters.js picks one of the other roster factions seeded; BOTH groups are sized off the `bands`' pirate counts, each drawn from its OWN combat.factions roster — no pirates/neutrals). DISPOSITION is per-faction, off the player's standing WITH each (js/reputation/Reputation.js → factionStanding, data/reputation.json): standing below `hostileBelow` → that faction targets the player and the player may return fire (the guns/skills' pools include it); at/above `hostileBelow` → it fights only the other faction, never the player (its bolts/seekers/rams can't touch the ship), and once its rival is spent it peels off (CombatSystem._peelOff) so the encounter ends on its own — the player observes. Factions ALWAYS fight each other while they're both up (CombatSystem.pickEnemyTarget). TIMERS: faction space rolls more often than neutral wild space (rollIntervalMin..Max, the director's roll schedule) with a longer quiet after each encounter (cooldownMin..Max, 40–60 s). The ships are the factions' OWN roster crafts (no new art).",
|
||||||
"rollIntervalMin": 20,
|
"rollIntervalMin": 20,
|
||||||
"rollIntervalMax": 40,
|
"rollIntervalMax": 40,
|
||||||
"cooldownMin": 20,
|
"cooldownMin": 40,
|
||||||
"cooldownMax": 40,
|
"cooldownMax": 60,
|
||||||
"hostileBelow": -5
|
"hostileBelow": -5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1802,5 +1802,105 @@ const E = (k, d) => config.get(`combat.enemy.${k}`, d);
|
||||||
|
|
||||||
// ------------------------------------------------------------------ done
|
// ------------------------------------------------------------------ done
|
||||||
|
|
||||||
|
// THE FACTION CROSSFIRE (the war-zones — both fleets shoot and kill each
|
||||||
|
// other, and the player's disposition is the only thing that decides who
|
||||||
|
// may fight the PLAYER): the crossfire collide passes (a faction's bolts /
|
||||||
|
// seekers drop a hostile-faction craft — never their own wing, never the
|
||||||
|
// classic pack), the SILENT npc kill (no kill count — the bounty is the
|
||||||
|
// player's, the war isn't), and the peel-off (the last survivor of a
|
||||||
|
// neutral-to-the-player side flies away once its rivals are spent, and the
|
||||||
|
// cull clears the sky for the director's cadence).
|
||||||
|
{
|
||||||
|
const VW = 1280, VH = 720;
|
||||||
|
const scene = makeScene({ withSheet: true });
|
||||||
|
scene.cameras = { main: { worldView: { x: 0, y: 0, width: VW, height: VH } } };
|
||||||
|
const ship = scene.physics.add.existing(new PhaserStub.Physics.Arcade.Sprite(scene, 640, 360, '__ship', 0));
|
||||||
|
ship.radius = 12;
|
||||||
|
ship.stop = () => {};
|
||||||
|
const combat = new CombatSystem(scene, { ship, onKill: () => {} });
|
||||||
|
const tag = (e, fid, hostileToPlayer) => {
|
||||||
|
e.factionId = fid;
|
||||||
|
e.hostileToPlayer = hostileToPlayer;
|
||||||
|
e.respawn = false;
|
||||||
|
e.nextFireAt = 999999999; // hold their own lances — the test drives the volleys
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
const zen = tag(combat.spawnEnemy(500, 360, 0), 'zenith', false);
|
||||||
|
const apx = tag(combat.spawnEnemy(780, 360, 1), 'apex', false);
|
||||||
|
const zen2 = tag(combat.spawnEnemy(500, 300, 2), 'zenith', false);
|
||||||
|
const apx2 = tag(combat.spawnEnemy(780, 420, 3), 'apex', false);
|
||||||
|
|
||||||
|
// TARGETING: each ship's target is the RIVAL faction — the player is not
|
||||||
|
// in the fight (both sides stand neutral to it).
|
||||||
|
check('crossfire: a faction ship targets the rival faction',
|
||||||
|
combat.pickEnemyTarget(zen, ship) === apx && combat.pickEnemyTarget(apx, ship) === zen);
|
||||||
|
check('crossfire: neutral-to-the-player — the player pool is empty (no auto-fire)',
|
||||||
|
combat._playerPool().length === 0 && combat.playerTargetable(zen) === false &&
|
||||||
|
combat.playerTargetable(apx) === false);
|
||||||
|
|
||||||
|
// CROSSFIRE SEEKERS: a faction's missile drops a hostile-faction craft
|
||||||
|
// (the same one-kill read; the seeker's own faction is the friendly-fire
|
||||||
|
// rule — and its neutral disposition keeps it off the player's hull).
|
||||||
|
// A minimal seeker stub, held still on the victim's line — the pass
|
||||||
|
// reads exactly this shape.
|
||||||
|
const seeker = {
|
||||||
|
x: 780 - 5, y: 360, radius: 12, dead: false, owner: 'enemy',
|
||||||
|
factionId: 'zenith', hostileToPlayer: false, targetRef: apx,
|
||||||
|
damage: 999, heading: 0, body: { velocity: { x: 0, y: 0 } },
|
||||||
|
update: () => {}, // held on the line — the hit pass settles it
|
||||||
|
consume: () => { seeker.dead = true; return true; },
|
||||||
|
};
|
||||||
|
combat.missiles.push(seeker);
|
||||||
|
combat.update(1000, 16);
|
||||||
|
check('crossfire: a faction seeker drops the rival (and is spent)',
|
||||||
|
seeker.dead === true && apx.dead === true);
|
||||||
|
check('crossfire: the npc kill banks no kill count (not a player kill)', combat.kills === 0);
|
||||||
|
check('crossfire: the neutral seeker fizzles past the ship (no player threat)', ship.dead !== true);
|
||||||
|
|
||||||
|
// CROSSFIRE BOLTS: zenith's lance drops the other apex hull.
|
||||||
|
combat.spawnBullet(780 - 18, 420, 0, { bulletSpeed: 500, damage: 999, life: 2, color: '#fff', size: 16, factionId: 'zenith' }, 'enemy');
|
||||||
|
combat.update(1016, 16);
|
||||||
|
check('crossfire: the rival hull takes the bolt and goes down', apx2.dead === true);
|
||||||
|
|
||||||
|
// FRIENDLY FIRE: the same volley passes through the shooter's own wing.
|
||||||
|
combat.spawnBullet(500 - 18, 300, 0, { bulletSpeed: 500, damage: 999, life: 2, color: '#fff', size: 16, factionId: 'zenith' }, 'enemy');
|
||||||
|
combat.update(1032, 16);
|
||||||
|
check('crossfire: a faction volley never touches its own wing', zen2.dead === false);
|
||||||
|
|
||||||
|
// CLASSIC PACK: a bolt with no faction stamp never shoots the pack
|
||||||
|
// (pirates don't fire on pirates — the seam holds).
|
||||||
|
combat.spawnBullet(500 - 18, 300, 0, { bulletSpeed: 500, damage: 999, life: 2, color: '#fff', size: 16 }, 'enemy');
|
||||||
|
combat.update(1048, 16);
|
||||||
|
check('crossfire: a classic bolt passes through the pack', zen2.dead === false);
|
||||||
|
|
||||||
|
// PEEL-OFF: the apex side is all spent — the neutral zenith survivors
|
||||||
|
// disengage (they steer AWAY from the ship, then the cull collects them
|
||||||
|
// off-screen — the sky clears for the director's cadence).
|
||||||
|
combat.update(1064, 16);
|
||||||
|
check("peel: the survivors' disengage arms (peeling)", zen.peeling === true && zen2.peeling === true);
|
||||||
|
check('peel: the velocity runs away from the player',
|
||||||
|
zen.body && zen.body.velocity.x < 0 && zen2.body && zen2.body.velocity.x < 0);
|
||||||
|
let frames = 0;
|
||||||
|
while ((combat.enemies.includes(zen) || combat.enemies.includes(zen2)) && frames < 240) {
|
||||||
|
if (zen.body) { zen.x += zen.body.velocity.x * 0.016; zen.y += zen.body.velocity.y * 0.016; }
|
||||||
|
if (zen2.body) { zen2.x += zen2.body.velocity.x * 0.016; zen2.y += zen2.body.velocity.y * 0.016; }
|
||||||
|
combat.update(1064 + (frames + 1) * 16, 16);
|
||||||
|
frames++;
|
||||||
|
}
|
||||||
|
check('peel: off-screen, the survivors are culled (the sky clears)',
|
||||||
|
frames < 240 && !combat.enemies.includes(zen) && !combat.enemies.includes(zen2) &&
|
||||||
|
zen._peelGone === true && zen2._peelGone === true);
|
||||||
|
|
||||||
|
// HOSTILE DISPOSITION: a faction ship the player HAS heat on keeps the
|
||||||
|
// classic fight — with no rival up, the player is its target (and the
|
||||||
|
// player's guns may engage it in return).
|
||||||
|
const hot = tag(combat.spawnEnemy(500, 360, 3), 'apex', true);
|
||||||
|
check('disposition: hostile-to-the-player — the player is the fallback target',
|
||||||
|
combat.pickEnemyTarget(hot, ship) === ship);
|
||||||
|
check('disposition: the player guns may engage a hostile faction (pool + lock)',
|
||||||
|
combat._playerPool().includes(hot) && combat.nearestEnemy(640, 360) === hot);
|
||||||
|
combat.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
console.log(failures === 0 ? '\ncombat rules: all checks passed' : `\n${failures} check(s) FAILED`);
|
console.log(failures === 0 ? '\ncombat rules: all checks passed' : `\n${failures} check(s) FAILED`);
|
||||||
process.exit(failures === 0 ? 0 : 1);
|
process.exit(failures === 0 ? 0 : 1);
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,8 @@ const fixed = {
|
||||||
);
|
);
|
||||||
check('data: the post-encounter quiet is the ~30–60 s band', e.cooldownMin === 30 && e.cooldownMax === 60);
|
check('data: the post-encounter quiet is the ~30–60 s band', e.cooldownMin === 30 && e.cooldownMax === 60);
|
||||||
check(
|
check(
|
||||||
'data: the FACTION schedule ships (tighter: 20–40 s rolls, 20–40 s quiet, the hostile floor)',
|
'data: the FACTION schedule ships (tighter rolls: 20–40 s, a 40–60 s quiet, the hostile floor)',
|
||||||
!!e.faction && e.faction.rollIntervalMin === 20 && e.faction.rollIntervalMax === 40 && e.faction.cooldownMin === 20 && e.faction.cooldownMax === 40 && e.faction.hostileBelow === -5,
|
!!e.faction && e.faction.rollIntervalMin === 20 && e.faction.rollIntervalMax === 40 && e.faction.cooldownMin === 40 && e.faction.cooldownMax === 60 && e.faction.hostileBelow === -5,
|
||||||
JSON.stringify(e.faction),
|
JSON.stringify(e.faction),
|
||||||
);
|
);
|
||||||
check(
|
check(
|
||||||
|
|
|
||||||
|
|
@ -1316,8 +1316,14 @@ export class CombatSystem {
|
||||||
// The WAR-ZONE disposition (e.hostileToPlayer, see the spawner) rides
|
// The WAR-ZONE disposition (e.hostileToPlayer, see the spawner) rides
|
||||||
// the volley — the bolts/seekers it spawns carry it, so a
|
// the volley — the bolts/seekers it spawns carry it, so a
|
||||||
// neutral-to-the-player faction's fire fizzles past the ship (the
|
// neutral-to-the-player faction's fire fizzles past the ship (the
|
||||||
// collide passes + the missile's home rule).
|
// collide passes + the missile's home rule) — and the shooter's
|
||||||
return firePattern(this, e, player, { ...(e.wpn ?? {}), hostileToPlayer: e.hostileToPlayer !== false });
|
// FACTION rides them too (the crossfire passes: a faction's fire
|
||||||
|
// hits the rival faction's craft, never its own wing).
|
||||||
|
return firePattern(this, e, player, {
|
||||||
|
...(e.wpn ?? {}),
|
||||||
|
hostileToPlayer: e.hostileToPlayer !== false,
|
||||||
|
factionId: typeof e.factionId === 'string' ? e.factionId : null,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1360,6 +1366,10 @@ export class CombatSystem {
|
||||||
// neutral-to-the-player faction fizzles past the ship (collide's
|
// neutral-to-the-player faction fizzles past the ship (collide's
|
||||||
// lances×player pass).
|
// lances×player pass).
|
||||||
b.hostileToPlayer = typeof wpn.hostileToPlayer === 'boolean' ? wpn.hostileToPlayer : true;
|
b.hostileToPlayer = typeof wpn.hostileToPlayer === 'boolean' ? wpn.hostileToPlayer : true;
|
||||||
|
// The shooter's FACTION (the crossfire passes' friendly-fire rule —
|
||||||
|
// a faction's bolt drops the rival faction's hulls, never its own
|
||||||
|
// wing; a classic bolt has none and never shoots itself).
|
||||||
|
b.factionId = typeof wpn.factionId === 'string' ? wpn.factionId : null;
|
||||||
b.setDepth(20);
|
b.setDepth(20);
|
||||||
this.bullets.push(b);
|
this.bullets.push(b);
|
||||||
return b;
|
return b;
|
||||||
|
|
@ -1461,6 +1471,36 @@ export class CombatSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Their lances × the RIVAL FACTION's craft (the war-zones' crossfire
|
||||||
|
// — a faction's bolt drops a hostile-faction hull, exactly the way a
|
||||||
|
// pirate's can drop a neutral escort in the mixed fight). The bolt
|
||||||
|
// carries its shooter's faction (fireEnemy's stamp — spawnBullet): a
|
||||||
|
// faction's fire never touches its own wing (no friendly fire), and a
|
||||||
|
// classic pack (no faction stamp) never shoots itself. The kill is
|
||||||
|
// SILENT (killEnemy's npc flag — no XP, no defeat line, no shake: the
|
||||||
|
// bounty is the player's, the war isn't), and the downed side's
|
||||||
|
// survivors peel off (the update loop's gate).
|
||||||
|
for (let i = this.bullets.length - 1; i >= 0; i--) {
|
||||||
|
const b = this.bullets[i];
|
||||||
|
if (b.dead || b.owner !== 'enemy') continue;
|
||||||
|
if (!b.factionId) continue; // a classic bolt: the pack never shoots itself
|
||||||
|
for (const e of this.enemies) {
|
||||||
|
if (e.dead) continue;
|
||||||
|
if (!e.factionId || e.factionId === b.factionId) continue; // own wing stands
|
||||||
|
if (!this.craftVisible(e)) continue;
|
||||||
|
if (this.hitsSwept(b, e, e.hitRadius, delta)) {
|
||||||
|
const preShields = e.shields;
|
||||||
|
const killed = e.damage(b.damage);
|
||||||
|
const kind = preShields > 0 ? 'shield' : 'hull';
|
||||||
|
this.fx.enemyHit(e, b.x, b.y, b.angle0, kind, b.damage);
|
||||||
|
this.fx.damage(b.x, b.y, b.damage, kind);
|
||||||
|
combatSfxEnemyHit(this.scene, preShields, b.damage);
|
||||||
|
if (killed) this.killEnemy(e, this._now(), true);
|
||||||
|
if (b.consume()) this.bullets.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Their lances × the WRAITH'S DECOYS (data/skills.json → 'blink'): the
|
// Their lances × the WRAITH'S DECOYS (data/skills.json → 'blink'): the
|
||||||
// decoy is what drew the fire — a bolt that reaches it fizzles on the
|
// decoy is what drew the fire — a bolt that reaches it fizzles on the
|
||||||
// shell (the hologram has no hull to break, only the pop).
|
// shell (the hologram has no hull to break, only the pop).
|
||||||
|
|
@ -1521,6 +1561,9 @@ export class CombatSystem {
|
||||||
// Their heat-seekers × your escorts (mixed fight: a pirate seeker
|
// Their heat-seekers × your escorts (mixed fight: a pirate seeker
|
||||||
// can drop a neutral ship).
|
// can drop a neutral ship).
|
||||||
this.missilesHitAllies(delta);
|
this.missilesHitAllies(delta);
|
||||||
|
// FACTION SEEKERS × the rival faction's craft (the war-zones'
|
||||||
|
// crossfire — a faction's seeker drops a hostile-faction hull).
|
||||||
|
this.missilesHitRivals(delta);
|
||||||
// Your escorts' heat-seekers × the pack (the escorts' missile
|
// Your escorts' heat-seekers × the pack (the escorts' missile
|
||||||
// patterns — one kill path, the XP still lands).
|
// patterns — one kill path, the XP still lands).
|
||||||
this.missilesHitEnemies(delta);
|
this.missilesHitEnemies(delta);
|
||||||
|
|
@ -1569,6 +1612,43 @@ export class CombatSystem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FACTION SEEKERS × the RIVAL FACTION's craft (the war-zones' crossfire
|
||||||
|
* — a faction's seeker drops a hostile-faction hull, exactly the way a
|
||||||
|
* pirate's can drop a neutral escort in the mixed fight; the seeker
|
||||||
|
* carries its shooter's faction — launchMissiles' stamp — so a
|
||||||
|
* faction's seekers never take their own wing, and a classic seeker
|
||||||
|
* (no faction) never shoots the pack). The kill is SILENT (killEnemy's
|
||||||
|
* npc flag — no XP, no defeat line: the bounty is the player's, the
|
||||||
|
* war isn't).
|
||||||
|
*/
|
||||||
|
missilesHitRivals(delta) {
|
||||||
|
const ms = this.missiles;
|
||||||
|
if (!ms || ms.length === 0 || this.enemies.length === 0) return;
|
||||||
|
for (let i = ms.length - 1; i >= 0; i--) {
|
||||||
|
const m = ms[i];
|
||||||
|
if (m.dead) { ms.splice(i, 1); continue; }
|
||||||
|
if (m.owner !== 'enemy') continue;
|
||||||
|
if (!m.factionId) continue; // a classic seeker: the pack never shoots itself
|
||||||
|
for (const e of this.enemies) {
|
||||||
|
if (e.dead) continue;
|
||||||
|
if (!e.factionId || e.factionId === m.factionId) continue; // own wing stands
|
||||||
|
if (!this.craftVisible(e)) continue;
|
||||||
|
if (this.hitsSwept(m, e, e.hitRadius, delta)) {
|
||||||
|
const preShields = e.shields;
|
||||||
|
const killed = e.damage(m.damage);
|
||||||
|
const kind = preShields > 0 ? 'shield' : 'hull';
|
||||||
|
this.fx.enemyHit(e, m.x, m.y, m.heading, kind, m.damage);
|
||||||
|
this.fx.damage(m.x, m.y, m.damage, kind);
|
||||||
|
combatSfxEnemyHit(this.scene, preShields, m.damage);
|
||||||
|
if (killed) this.killEnemy(e, this._now(), true);
|
||||||
|
if (m.consume()) ms.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Escort seekers × the pack — the same hit read as the player's. */
|
/** Escort seekers × the pack — the same hit read as the player's. */
|
||||||
missilesHitEnemies(delta) {
|
missilesHitEnemies(delta) {
|
||||||
const ms = this.missiles;
|
const ms = this.missiles;
|
||||||
|
|
@ -1777,7 +1857,7 @@ export class CombatSystem {
|
||||||
* / the type's override). Idempotent via _killFx — damage() flips
|
* / the type's override). Idempotent via _killFx — damage() flips
|
||||||
* e.dead first, so the dead flag can't be the "already counted" guard.
|
* e.dead first, so the dead flag can't be the "already counted" guard.
|
||||||
*/
|
*/
|
||||||
killEnemy(e, time) {
|
killEnemy(e, time, npc = false) {
|
||||||
if (e._killFx) return;
|
if (e._killFx) return;
|
||||||
e._killFx = true;
|
e._killFx = true;
|
||||||
// Settle any pending white hit-flash NOW — the craft dies in its
|
// Settle any pending white hit-flash NOW — the craft dies in its
|
||||||
|
|
@ -1786,23 +1866,28 @@ export class CombatSystem {
|
||||||
// AND settle any strung read (the kill supersedes the stun's dim and
|
// AND settle any strung read (the kill supersedes the stun's dim and
|
||||||
// discharge — no dimmed corpse, no orphaned bolts).
|
// discharge — no dimmed corpse, no orphaned bolts).
|
||||||
this._stunRelease(e, false);
|
this._stunRelease(e, false);
|
||||||
this.kills += 1;
|
// THE NPC KILL (the war-zones' crossfire — a faction dropped the
|
||||||
|
// craft, not the player): the read stands (the wreck, the beat),
|
||||||
|
// but the kill's BOUNTY doesn't — no XP, no kill count, no defeat
|
||||||
|
// line, no screen kick (the war is the world's, the player's ledger
|
||||||
|
// is the player's).
|
||||||
|
if (!npc) this.kills += 1;
|
||||||
// COMBAT XP — the kill's reward (the hostile's own type's value —
|
// COMBAT XP — the kill's reward (the hostile's own type's value —
|
||||||
// combat.enemy.xp / the type's override; the curve is
|
// combat.enemy.xp / the type's override; the curve is
|
||||||
// data/progression.json). The +N floater lands on the wreckage, and a
|
// data/progression.json). The +N floater lands on the wreckage, and a
|
||||||
// crossed level's beat rides the manager's 'levelup' event (the armor
|
// crossed level's beat rides the manager's 'levelup' event (the armor
|
||||||
// bump, the burst, the feed's line) — the kill path stays one grant.
|
// bump, the burst, the feed's line) — the kill path stays one grant.
|
||||||
if (this.xp) {
|
if (!npc && this.xp) {
|
||||||
const gained = this.xp.grant(Number(e.xp) || 0, 'kill');
|
const gained = this.xp.grant(Number(e.xp) || 0, 'kill');
|
||||||
if (gained.gained > 0) this.fx.xp(e.x, e.y, gained.gained);
|
if (gained.gained > 0) this.fx.xp(e.x, e.y, gained.gained);
|
||||||
}
|
}
|
||||||
this.fx.explode(e.x, e.y, '#ff5d6f');
|
this.fx.explode(e.x, e.y, '#ff5d6f');
|
||||||
combatShake(this.scene, 'kill'); // the world kick (the bar stays still)
|
if (!npc) combatShake(this.scene, 'kill'); // the world kick (the bar stays still)
|
||||||
combatSfx(this.scene, 'kill');
|
combatSfx(this.scene, 'kill');
|
||||||
// THE DEFEAT COMM (data/combat.json → comms.defeat): a downed pirate
|
// THE DEFEAT COMM (data/combat.json → comms.defeat): a downed pirate
|
||||||
// rolls its defeat line (20%) — the comm's beat rides the same kill
|
// rolls its defeat line (20%) — the comm's beat rides the same kill
|
||||||
// (once per death — killEnemy is idempotent via _killFx).
|
// (once per death — killEnemy is idempotent via _killFx).
|
||||||
this._commDefeatRoll(e);
|
if (!npc) this._commDefeatRoll(e);
|
||||||
e.setVisible(false);
|
e.setVisible(false);
|
||||||
if (e.body) {
|
if (e.body) {
|
||||||
e.body.velocity.x = 0;
|
e.body.velocity.x = 0;
|
||||||
|
|
|
||||||
|
|
@ -538,6 +538,10 @@ const HANDLERS = {
|
||||||
// neutral-to-the-player faction's wing seekers never re-home
|
// neutral-to-the-player faction's wing seekers never re-home
|
||||||
// onto the ship when their victim falls (Weapons.js's rule).
|
// onto the ship when their victim falls (Weapons.js's rule).
|
||||||
m.hostileToPlayer = e.hostileToPlayer === true;
|
m.hostileToPlayer = e.hostileToPlayer === true;
|
||||||
|
// The shooter's FACTION (the crossfire pass's friendly-fire
|
||||||
|
// rule — a faction's wing seekers drop the rival faction's
|
||||||
|
// craft, never its own wing).
|
||||||
|
m.factionId = typeof e.factionId === 'string' ? e.factionId : null;
|
||||||
ctx.missiles.push(m);
|
ctx.missiles.push(m);
|
||||||
} catch { /* stub scene — the volley's beat still plays */ }
|
} catch { /* stub scene — the volley's beat still plays */ }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,10 @@ export function launchMissiles(ctx, ship, player, wpn) {
|
||||||
// onto the weapon): the seeker's fizzle rule + the no-rehome rule
|
// onto the weapon): the seeker's fizzle rule + the no-rehome rule
|
||||||
// above read it off the missile.
|
// above read it off the missile.
|
||||||
m.hostileToPlayer = typeof wpn.hostileToPlayer === 'boolean' ? wpn.hostileToPlayer : true;
|
m.hostileToPlayer = typeof wpn.hostileToPlayer === 'boolean' ? wpn.hostileToPlayer : true;
|
||||||
|
// The shooter's FACTION (the crossfire pass's friendly-fire rule —
|
||||||
|
// a faction's seeker drops the rival faction's craft, never its own
|
||||||
|
// wing; a classic seeker has none and the pass stands it off).
|
||||||
|
m.factionId = typeof wpn.factionId === 'string' ? wpn.factionId : null;
|
||||||
ctx.missiles.push(m);
|
ctx.missiles.push(m);
|
||||||
}
|
}
|
||||||
combatSfx(scene, 'missile_fire'); // the rocket voice (distinct from the laser)
|
combatSfx(scene, 'missile_fire'); // the rocket voice (distinct from the laser)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue