Scale faction enemy damage and armor by player level
- Add combat.factionScaling config: linear ramp from 1.2x damage / 2.0x armor at level 10 to the shipped 2.0x / 3.5x at level 35, clamped at both ends - Implement isFactionType and factionScaleForLevel in EnemyTypes.js; extend enemyTypeConfig to accept a level and apply the ramp to weapon.damage, hull, and shields for faction roster fighters only (pirates, neutrals, transport stay flat) - CombatSystem.spawnEnemy now passes the player's XP level so faction enemies scale with progression - Gate GameScene escort rendezvous logic on originSystemId match before checking proximity, preventing convoy link in the wrong system - Add dev tests covering ramp math at levels 0/10/20/35/99, per-fighter consistency, flat ramDamage/xp, and non-faction exclusion
This commit is contained in:
parent
2eb2d46ba4
commit
e7b76ceccf
|
|
@ -243,7 +243,7 @@
|
|||
"color": "#ff2d6f"
|
||||
}
|
||||
},
|
||||
"$factions_note": "FRAMES 30–37 = THE APEX, frames 40–47 = THE MINDWEAVE — the two FACTION rosters (the ?combat=apex / ?combat=mindweave duel arena — combat.factions below names them). BALANCE RULE (the user's numbers, vs the PIRATE raider — 60 armor, 8 bolt damage): BOTH FACTIONS = 2× DAMAGE (16 = 8 × 2, every bolt/seeker/shell) and 3.5× ARMOR (175 hull + 35 shields = 210 = 60 × 3.5). Cooldowns 0.6–3.0 s (pattern character, not power), ramDamage 13 (10 × 1.3). xp = (hull+shields)/5 = 42 (the roster's rule of thumb). DIFFERENTIATION comes from the WEAPON PATTERN (weapon.pattern — the shared dispatch, js/combat/Weapons.js) + the ABILITY LAYER (abilities[].id — js/combat/EnemyAbilities.js: cloaked_ambush / flank_strafe for the Apex's predatory kit, blink_reposition / illusion_decoy / telekinetic_wave for the Mindweave's psychic kit). Each fighter is its own craft; the faction's SHARED skills are the kit's common entries (the Apex all carry the cloak and most the flank; the Mindweave all carry the blink or the phantoms and several the tides) at per-ship numbers.",
|
||||
"$factions_note": "FRAMES 30–37 = THE APEX, frames 40–47 = THE MINDWEAVE — the two FACTION rosters (the ?combat=apex / ?combat=mindweave duel arena — combat.factions below names them). BALANCE RULE (the user's numbers, vs the PIRATE raider — 60 armor, 8 bolt damage): BOTH FACTIONS = 2× DAMAGE (16 = 8 × 2, every bolt/seeker/shell) and 3.5× ARMOR (175 hull + 35 shields = 210 = 60 × 3.5). Cooldowns 0.6–3.0 s (pattern character, not power), ramDamage 13 (10 × 1.3). xp = (hull+shields)/5 = 42 (the roster's rule of thumb). DIFFERENTIATION comes from the WEAPON PATTERN (weapon.pattern — the shared dispatch, js/combat/Weapons.js) + the ABILITY LAYER (abilities[].id — js/combat/EnemyAbilities.js: cloaked_ambush / flank_strafe for the Apex's predatory kit, blink_reposition / illusion_decoy / telekinetic_wave for the Mindweave's psychic kit). Each fighter is its own craft; the faction's SHARED skills are the kit's common entries (the Apex all carry the cloak and most the flank; the Mindweave all carry the blink or the phantoms and several the tides) at per-ship numbers. LEVEL RAMP: the 2×/3.5× above are the LEVEL-35 numbers — combat.factionScaling scales the damage (1.2× at level 10) and armor (2.0× at level 10) DOWN with the player's level below 35 (ramDamage and xp stay flat).",
|
||||
"apex_raptor": {
|
||||
"$comment": "APEX 1 (frame 30) — RAPTOR, the forward gun: a 5-bolt SWEEP out the nose (heavy forward firepower) on a medium beat; the flank's cut gets it behind the player every 10 s or so so the sweep lands on the back.",
|
||||
"name": "Raptor",
|
||||
|
|
@ -834,7 +834,7 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"_zenith_note": "FRAMES 50–57 = THE ZENITH — the third FACTION roster (the ?combat=zenith duel arena). TWO ARCHETYPES, split 4/4: FRAMES 50–53 = THE COMBO SEEKERS — a fast low-damage WEAPON (the gun) paired with the flank_barrage ABILITY (js/combat/EnemyAbilities.js): a salvo of HEAT-SEEKERS out of BOTH WINGS at 90° off the heading, on a SLOW capped turn-rate (missileTurn 0.7–1.3, far below the nose seekers' 2.2) so they curve wide and a committed turn beats them. FRAMES 54–57 = THE RANGED ELEMENTALS — the shock_dart ABILITY: fast darts that detonate near the target and bloom an ELECTRIFIED ZONE (startRadius→maxRadius over growMs, finite lifeMs, fadeMs), ticking damage (tickDamage/tickMs) on anything inside — a growing, dodgeable field, not a one-shot. BALANCE (the same rule as the $factions_note above, vs the PIRATE raider — 60 armor, 8 bolt damage): 2× DAMAGE (16 every bolt/seeker/shell; the shock zones tick 3–5 per beat — a sustained, avoidable pressure, never a spike) and 3.5× ARMOR (175 hull + 35 shields = 210). xp = (hull+shields)/5 = 42. Differentiation comes from PACE AND SHAPE: the combo ships vary the gun (rapid / burst / spray / sweep) and the volley's size and seek-rate; the elemental ships vary the dart count, the zone's size, its bloom time and its tick rate.",
|
||||
"_zenith_note": "FRAMES 50–57 = THE ZENITH — the third FACTION roster (the ?combat=zenith duel arena). TWO ARCHETYPES, split 4/4: FRAMES 50–53 = THE COMBO SEEKERS — a fast low-damage WEAPON (the gun) paired with the flank_barrage ABILITY (js/combat/EnemyAbilities.js): a salvo of HEAT-SEEKERS out of BOTH WINGS at 90° off the heading, on a SLOW capped turn-rate (missileTurn 0.7–1.3, far below the nose seekers' 2.2) so they curve wide and a committed turn beats them. FRAMES 54–57 = THE RANGED ELEMENTALS — the shock_dart ABILITY: fast darts that detonate near the target and bloom an ELECTRIFIED ZONE (startRadius→maxRadius over growMs, finite lifeMs, fadeMs), ticking damage (tickDamage/tickMs) on anything inside — a growing, dodgeable field, not a one-shot. BALANCE (the same rule as the $factions_note above, vs the PIRATE raider — 60 armor, 8 bolt damage): 2× DAMAGE (16 every bolt/seeker/shell; the shock zones tick 3–5 per beat — a sustained, avoidable pressure, never a spike) and 3.5× ARMOR (175 hull + 35 shields = 210). xp = (hull+shields)/5 = 42. Differentiation comes from PACE AND SHAPE: the combo ships vary the gun (rapid / burst / spray / sweep) and the volley's size and seek-rate; the elemental ships vary the dart count, the zone's size, its bloom time and its tick rate. LEVEL RAMP: same as the Apex/Mindweave note — combat.factionScaling scales the damage (1.2× at level 10) and armor (2.0× at level 10) with the player's level up to these level-35 numbers; ramDamage and xp stay flat.",
|
||||
"zenith_wingblade": {
|
||||
"$comment": "ZENITH 1 (frame 50) — WINGBLADE, the gun-wing: a RAPID gun (0.55 s) for constant low-damage pressure, and on its own clock a SIX-SEEKER wing volley (three out of each wing at 90°, a slight fan) that curves wide toward the player — the archetype's base kit at balanced tempo.",
|
||||
"name": "Wingblade",
|
||||
|
|
@ -1155,6 +1155,15 @@
|
|||
"durationMs": 1000,
|
||||
"range": 720
|
||||
},
|
||||
"factionScaling": {
|
||||
"_comment": "THE FACTION RAMP (js/combat/EnemyTypes.js → factionScaleForLevel / enemyTypeConfig(typeId, level), applied by CombatSystem.spawnEnemy at the player's combat level): the FACTION rosters' (combat.factions — Apex / Mindweave / Zenith, 24 fighters) per-shot damage and armor scale with the player's level between levelStart and levelEnd (clamped at both ends), measured against the PIRATE raider baseline (the classic 2× DAMAGE / 3.5× ARMOR balance rule — 60 armor, 8 bolt damage). The data/*.json static values ARE the levelEnd numbers (16 damage, 175+35 armor); at lower levels they scale DOWN proportionally (× damageMult(level)/damageMultEnd, × armorMult(level)/armorMultEnd, rounded). Level 10: damage ×0.6 (16→10), armor ×0.5714 (210→120). Level 35 and above: the shipped 2×/3.5×. EVERYTHING ELSE is untouched — cooldowns, the weapon pattern, the ability kit, ramDamage (13 = 10×1.3) and xp (42) stay flat across the whole ramp. Non-faction types (the pirate classics, the neutrals, the transport) never scale.",
|
||||
"levelStart": 10,
|
||||
"levelEnd": 35,
|
||||
"damageMultStart": 1.2,
|
||||
"damageMultEnd": 2.0,
|
||||
"armorMultStart": 2.0,
|
||||
"armorMultEnd": 3.5
|
||||
},
|
||||
"factions": {
|
||||
"_comment": "The FACTION DUEL ARENA (the ?combat=apex / ?combat=mindweave boots — js/scenes/GameScene.js → _combatBootMode, js/combat/DuelPicker.js the in-game picker, CombatSystem.startDuel/spawnDuel/spawnDuelPack the fight's seam). Each faction names its fighters (enemyTypes ids, display order = the picker's rows 1–8); 'style' is the picker's one-line read. Key '0' (or the row) sends the WHOLE pack. These rosters are NOT wired into any quest/galaxy content — the arena is their only home for now.",
|
||||
"apex": {
|
||||
|
|
|
|||
|
|
@ -176,6 +176,7 @@ const { EnemyShip } = await import(pathToFileURL(join(__dirname, '../js/combat/E
|
|||
const {
|
||||
enemyTypeConfig, enemyTypeIds, enemySheetList,
|
||||
neutralTypeConfig, neutralTypeIds, neutralSheetList,
|
||||
factionScaleForLevel, isFactionType,
|
||||
} = await import(pathToFileURL(join(__dirname, '../js/combat/EnemyTypes.js')).href);
|
||||
const { normalizeWaves, resolveRoster, WaveRunner } = await import(
|
||||
pathToFileURL(join(__dirname, '../js/combat/Waves.js')).href
|
||||
|
|
@ -837,6 +838,51 @@ const E = (k, d) => config.get(`combat.enemy.${k}`, d);
|
|||
const unknown = enemyTypeConfig('nope');
|
||||
check('types: unknown id → the base craft (warned, not thrown)', unknown === base);
|
||||
|
||||
// ---- the faction level ramp (combat.factionScaling: 1.2×/2.0× at level 10
|
||||
// → 2.0×/3.5× at level 35, vs the pirate raider; the data's numbers ARE the
|
||||
// level-35 ones, so level 10 = damage ×0.6, armor ×4/7)
|
||||
const factionIds24 = enemyTypeIds().slice(8, 32);
|
||||
check('ramp: the 24 faction fighters are the ramp apply gate (pirates/neutrals never scale)',
|
||||
factionIds24.length === 24 &&
|
||||
factionIds24.every((id) => isFactionType(id)) &&
|
||||
!isFactionType('raider') && !isFactionType('warden') && !isFactionType('gunner') &&
|
||||
!isFactionType(null) && !isFactionType(''));
|
||||
check('ramp: scale = mult(level)/mult(35) — 1.0 at and past 35, 0.6/4/7 at and below 10',
|
||||
approx(factionScaleForLevel(35).damage, 1) && approx(factionScaleForLevel(35).armor, 1) &&
|
||||
approx(factionScaleForLevel(99).damage, 1) && approx(factionScaleForLevel(99).armor, 1) &&
|
||||
approx(factionScaleForLevel(10).damage, 0.6) && approx(factionScaleForLevel(10).armor, 4 / 7) &&
|
||||
approx(factionScaleForLevel(0).damage, 0.6) && approx(factionScaleForLevel(0).armor, 4 / 7));
|
||||
const raptor10 = enemyTypeConfig('apex_raptor', 10);
|
||||
const raptor35 = enemyTypeConfig('apex_raptor', 35);
|
||||
check('ramp: level 10 → damage 16×0.6 = 10, armor 210×4/7 = 120 (hull 100 + shields 20)',
|
||||
raptor10.weapon.damage === 10 && raptor10.hull === 100 && raptor10.shields === 20);
|
||||
check('ramp: level 35 → the shipped stats (damage 16, hull 175 + shields 35)',
|
||||
raptor35.weapon.damage === 16 && raptor35.hull === 175 && raptor35.shields === 35);
|
||||
const raptor20 = enemyTypeConfig('apex_raptor', 20);
|
||||
check('ramp: level 20 (mid-way) → damage 16×0.76 = 12, armor 210×(2.6/3.5) = 130 + 26',
|
||||
raptor20.weapon.damage === 12 && raptor20.hull === 130 && raptor20.shields === 26);
|
||||
check('ramp: clamped past both ends (level 0 = level 10, level 99 = level 35)',
|
||||
JSON.stringify(enemyTypeConfig('apex_raptor', 0)) === JSON.stringify(raptor10) &&
|
||||
JSON.stringify(enemyTypeConfig('apex_raptor', 99)) === JSON.stringify(raptor35));
|
||||
check('ramp: EVERY faction fighter lands on the same ramp (120 armor @10, 210 @35, damage ×0.6 @10)',
|
||||
factionIds24.every((id) => {
|
||||
const lo = enemyTypeConfig(id, 10);
|
||||
const hi = enemyTypeConfig(id, 35);
|
||||
return lo.hull + lo.shields === 120 && hi.hull + hi.shields === 210 &&
|
||||
lo.weapon.damage === Math.max(1, Math.round(hi.weapon.damage * 0.6));
|
||||
}));
|
||||
check('ramp: ramDamage (13) and xp (42) stay flat across the whole ramp',
|
||||
[10, 20, 35].every((lv) => {
|
||||
const t = enemyTypeConfig('apex_raptor', lv);
|
||||
return t.ramDamage === 13 && t.xp === 42;
|
||||
}));
|
||||
check('ramp: non-faction types are untouched even with a level passed (raider/warden/gunner)',
|
||||
['raider', 'warden', 'gunner'].every((id) =>
|
||||
JSON.stringify(enemyTypeConfig(id, 10)) === JSON.stringify(enemyTypeConfig(id))));
|
||||
check('ramp: no level passed = the shipped stats (the arena/tests without an XP bank)',
|
||||
JSON.stringify(enemyTypeConfig('apex_raptor')) === JSON.stringify(enemyTypeConfig('apex_raptor', null)) &&
|
||||
JSON.stringify(enemyTypeConfig('apex_raptor')) === JSON.stringify(raptor35));
|
||||
|
||||
const sheets = enemySheetList();
|
||||
check('sheets: base first, one entry per source file (bomber shares the sheet)',
|
||||
sheets.length === 1 && sheets[0].src === base.texture && sheets[0].frameWidth === 128 && sheets[0].frameHeight === 128);
|
||||
|
|
|
|||
|
|
@ -388,11 +388,16 @@ export class CombatSystem {
|
|||
|
||||
/**
|
||||
* One hostile (a dev/probe spawn, or a wave). Anchors its orbit to its
|
||||
* bearing.
|
||||
* bearing. FACTION roster fighters (isFactionType) get their combat
|
||||
* damage/armor at the player's current combat level (the
|
||||
* combat.factionScaling ramp — the 2×/3.5× pirate ratio scales down to
|
||||
* 1.2×/2.0× at level 10; js/combat/EnemyTypes.js →
|
||||
* enemyTypeConfig(type, level)).
|
||||
* @param {string|null} [type] the hostile type (a combat.enemyTypes id) — null = the base
|
||||
*/
|
||||
spawnEnemy(x, y, index, type = null) {
|
||||
const cfg = enemyTypeConfig(type);
|
||||
const level = this.xp ? this.xp.level : null;
|
||||
const cfg = enemyTypeConfig(type, level);
|
||||
const e = new EnemyShip(this.scene, x, y, index, cfg);
|
||||
// The comm rolls' config (data/combat.json → enemyTypes.<type>.comms —
|
||||
// the pirate's lines; null for a type without one, then every roll is
|
||||
|
|
|
|||
|
|
@ -77,16 +77,72 @@ export function factionIds() {
|
|||
return Object.keys(f).filter((k) => !k.startsWith('_') && isPlain(f[k]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is a type id one of the FACTION rosters' fighters (listed in any
|
||||
* combat.factions.<id>.types)? The ramp's (factionScaleForLevel) apply
|
||||
* gate — the pirate classics, neutrals, and the transport never scale.
|
||||
*
|
||||
* @param {string} typeId a combat.enemyTypes id
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isFactionType(typeId) {
|
||||
if (typeof typeId !== 'string' || typeId === '') return false;
|
||||
for (const fid of factionIds()) {
|
||||
const f = config.section('combat.factions', {})[fid];
|
||||
if (Array.isArray(f.types) && f.types.some((t) => isPlain(t) && t.id === typeId)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The FACTION RAMP (data/combat.json → combat.factionScaling): the
|
||||
* per-shot-damage and armor multipliers for a player LEVEL, measured
|
||||
* against the pirate raider baseline (the classic 2× damage / 3.5× armor
|
||||
* balance rule). Linear between levelStart (10 — 1.2×/2.0×) and levelEnd
|
||||
* (35 — 2.0×/3.5×), clamped at both ends. Returned NORMALIZED to the
|
||||
* shipped static values (÷ the levelEnd multiplier), so a caller simply
|
||||
* multiplies the data's numbers: at level ≥ levelEnd both are 1.0.
|
||||
* A missing/malformed section degrades to no scaling (the shipped stats).
|
||||
*
|
||||
* @param {number} level the player's combat level
|
||||
* @returns {{damage: number, armor: number}} the scale ratios (≤ 1)
|
||||
*/
|
||||
export function factionScaleForLevel(level) {
|
||||
const s = config.section('combat.factionScaling', null);
|
||||
if (!isPlain(s)) return { damage: 1, armor: 1 };
|
||||
const l0 = Number(s.levelStart) || 0;
|
||||
const l1 = Number(s.levelEnd) || 1;
|
||||
const d0 = Number(s.damageMultStart) || 0;
|
||||
const d1 = Number(s.damageMultEnd) || 0;
|
||||
const a0 = Number(s.armorMultStart) || 0;
|
||||
const a1 = Number(s.armorMultEnd) || 0;
|
||||
if (!(d1 > 0) || !(a1 > 0) || !(l1 > l0)) return { damage: 1, armor: 1 };
|
||||
const lv = Number(level);
|
||||
const t = !Number.isFinite(lv) ? 1 : Math.min(1, Math.max(0, (lv - l0) / (l1 - l0)));
|
||||
return {
|
||||
damage: (d0 + (d1 - d0) * t) / d1,
|
||||
armor: (a0 + (a1 - a0) * t) / a1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The FULL config for a hostile type: the base (combat.enemy) deep-merged
|
||||
* with the type's overrides (combat.enemyTypes[id]).
|
||||
*
|
||||
* `level` (optional): the player's combat level — for FACTION roster
|
||||
* fighters (isFactionType) it applies the combat.factionScaling ramp:
|
||||
* weapon.damage × factionScaleForLevel(level).damage, hull/shields × .armor
|
||||
* (rounded; damage floors at 1, shields at 0). The pirates, neutrals,
|
||||
* transport, and an omitted level all return the shipped stats untouched;
|
||||
* ramDamage and xp never scale.
|
||||
*
|
||||
* @param {string|null} [typeId] null/'' = the base craft; a named id =
|
||||
* base + that type's overrides; an unknown id = the base (warned once
|
||||
* per call — a roster typo, not a crash).
|
||||
* @param {number|null} [level] the player's combat level (the faction ramp)
|
||||
* @returns {object} the config an EnemyShip builds from
|
||||
*/
|
||||
export function enemyTypeConfig(typeId = null) {
|
||||
export function enemyTypeConfig(typeId = null, level = null) {
|
||||
const base = config.section('combat.enemy', {});
|
||||
if (typeId == null || typeId === '') return base;
|
||||
const t = config.section('combat.enemyTypes', {});
|
||||
|
|
@ -97,7 +153,18 @@ export function enemyTypeConfig(typeId = null) {
|
|||
}
|
||||
return base;
|
||||
}
|
||||
return deepMerge(base, over);
|
||||
const c = deepMerge(base, over);
|
||||
if (!isFactionType(typeId) || level == null) return c;
|
||||
const s = factionScaleForLevel(level);
|
||||
if (s.damage === 1 && s.armor === 1) return c;
|
||||
const weapon = { ...(c.weapon ?? {}) };
|
||||
weapon.damage = Math.max(1, Math.round((Number(weapon.damage) || 0) * s.damage));
|
||||
return {
|
||||
...c,
|
||||
weapon,
|
||||
hull: Math.max(1, Math.round((Number(c.hull) || 0) * s.armor)),
|
||||
shields: Math.max(0, Math.round((Number(c.shields) || 0) * s.armor)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6480,25 +6480,30 @@ export class GameScene extends Phaser.Scene {
|
|||
// land on the fade's close (_sealEscortDelivery).
|
||||
if (esc.phase === 'delivering') { this._runEscortDelivery(id, route); return; }
|
||||
|
||||
// RENDEZVOUS LINK — the ship reaches the convoy's standing point; the convoy
|
||||
// forms on the wing and the lane goes hot.
|
||||
if (esc.phase === 'rendezvous' && esc.originSystemId === this.systemRecord.id) {
|
||||
const wp = Array.isArray(route.waypoints) && route.waypoints.length > 0 ? route.waypoints[0] : null;
|
||||
const radius = Math.max(1, Number(config.get('sidequests.escort.visitRadius', 460)) || 460);
|
||||
if (wp && Number.isFinite(wp[0]) && Number.isFinite(wp[1]) && Math.hypot(this.ship.x - wp[0], this.ship.y - wp[1]) <= radius) {
|
||||
esc.phase = 'transit';
|
||||
this.sideQuestState.setRoute(id, route); // persist the phase
|
||||
this._ensureEscortConvoy(id, route); // form on the wing
|
||||
const t = config.get('sidequests.ui.escortLinkToast', {});
|
||||
this.playSfx('construct');
|
||||
this.comms?.trigger('escortLink'); // the TRANSPORT CAPTAIN's greeting (data/comms.json — neutral-transport-link)
|
||||
this.consoleToast(
|
||||
String(t.text ?? 'CONVOY LINKED — {name} HOLDS YOUR WING THROUGH TO {dest}')
|
||||
.replace('{name}', esc.name || 'the convoy').replace('{dest}', esc.destSystemName || 'the destination'),
|
||||
{ glyph: t.glyph ?? '➳', glyphColor: t.color ? toCss(t.color) : undefined },
|
||||
);
|
||||
// RENDEZVOUS — before the link the convoy stands at its RENDEZVOUS point
|
||||
// only (the faction escort's rendezvous is in ANOTHER system than the
|
||||
// board's), so nothing convoy-related happens here: no spawn on the wing,
|
||||
// no lane, no delivery witness. The link lands when the ship reaches the
|
||||
// standing point; the convoy forms on the wing and the lane goes hot.
|
||||
if (esc.phase === 'rendezvous') {
|
||||
if (esc.originSystemId === this.systemRecord.id) {
|
||||
const wp = Array.isArray(route.waypoints) && route.waypoints.length > 0 ? route.waypoints[0] : null;
|
||||
const radius = Math.max(1, Number(config.get('sidequests.escort.visitRadius', 460)) || 460);
|
||||
if (wp && Number.isFinite(wp[0]) && Number.isFinite(wp[1]) && Math.hypot(this.ship.x - wp[0], this.ship.y - wp[1]) <= radius) {
|
||||
esc.phase = 'transit';
|
||||
this.sideQuestState.setRoute(id, route); // persist the phase
|
||||
this._ensureEscortConvoy(id, route); // form on the wing
|
||||
const t = config.get('sidequests.ui.escortLinkToast', {});
|
||||
this.playSfx('construct');
|
||||
this.comms?.trigger('escortLink'); // the TRANSPORT CAPTAIN's greeting (data/comms.json — neutral-transport-link)
|
||||
this.consoleToast(
|
||||
String(t.text ?? 'CONVOY LINKED — {name} HOLDS YOUR WING THROUGH TO {dest}')
|
||||
.replace('{name}', esc.name || 'the convoy').replace('{dest}', esc.destSystemName || 'the destination'),
|
||||
{ glyph: t.glyph ?? '➳', glyphColor: t.color ? toCss(t.color) : undefined },
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
return; // not linked yet — the convoy waits at the rendezvous
|
||||
}
|
||||
|
||||
// TRANSIT — the convoy follows (re-forms after a jump), the lane fires its
|
||||
|
|
|
|||
Loading…
Reference in New Issue