diff --git a/src/games/civilization/CivilizationMapView.js b/src/games/civilization/CivilizationMapView.js index f972211..0507709 100644 --- a/src/games/civilization/CivilizationMapView.js +++ b/src/games/civilization/CivilizationMapView.js @@ -344,7 +344,7 @@ export class CivilizationMapView { if (bits & (IMP.ROAD | IMP.RAILROAD)) { g.clear(); const rail = !!(bits & IMP.RAILROAD); - let drewAny = false; + const segments = []; for (let dy = -1; dy <= 1; dy += 1) { for (let dx = -1; dx <= 1; dx += 1) { if (dx === 0 && dy === 0) continue; @@ -355,21 +355,24 @@ export class CivilizationMapView { if (!(nBits & (IMP.ROAD | IMP.RAILROAD))) continue; const ex = (this.isoX(nc, nr) - this.isoX(c, r)) / 2; const ey = (this.isoY(nc, nr) - this.isoY(c, r)) / 2; - g.lineStyle(rail ? 4 : 3, rail ? 0x4a4038 : 0x8a6f4d, 0.9); - g.beginPath(); - g.moveTo(0, cy); - g.lineTo(ex, cy + ey); - g.strokePath(); - drewAny = true; + segments.push([ex, cy + ey]); } } - if (!drewAny && !hasCity) { - g.lineStyle(3, 0x8a6f4d, 0.9); - g.beginPath(); - g.moveTo(-14, cy); - g.lineTo(14, cy); - g.strokePath(); - } + if (!segments.length && !hasCity) segments.push([-14, cy], [14, cy]); + // A dark outline pass under the road/rail colour itself makes the line + // read clearly against any terrain, instead of blending in on lighter + // ground like desert/plains. + const strokeAll = (width, color, alpha) => { + g.lineStyle(width, color, alpha); + for (const [ex, ey] of segments) { + g.beginPath(); + g.moveTo(0, cy); + g.lineTo(ex, ey); + g.strokePath(); + } + }; + strokeAll(rail ? 7 : 6, 0x000000, 0.4); + strokeAll(rail ? 4 : 3, rail ? 0x4a4038 : 0x8a6f4d, 1); this.drawStampAt(g, x, y); } diff --git a/src/games/starcontrol/StarControlAI.js b/src/games/starcontrol/StarControlAI.js index d11d403..0e92b38 100644 --- a/src/games/starcontrol/StarControlAI.js +++ b/src/games/starcontrol/StarControlAI.js @@ -340,6 +340,11 @@ export function updateAI(ai, state, dtMs) { && !(enemy.fx.cloaked && dist > 900)) { input.fire = true; } + // Kohr-Ah blades are one-shot-per-press: the ship must let go of the + // button before it can launch another, so tap it instead of holding it. + if (ship.def.primary.type === 'guidedBlades' && ship.fx.bladeFireHeld) { + input.fire = false; + } // Charge weapons (Melnorme pump-up): keep pumping while far or unaimed, // release once the shot is charged and the nose is on target. if (ship.fx.pump) { diff --git a/src/games/starcontrol/StarControlLogic.js b/src/games/starcontrol/StarControlLogic.js index b746400..b2782dd 100644 --- a/src/games/starcontrol/StarControlLogic.js +++ b/src/games/starcontrol/StarControlLogic.js @@ -22,7 +22,7 @@ export const FACING_STEP = (Math.PI * 2) / FACINGS; export const TUNE = { MAX_FRAMES_PER_STEP: 6, // spiral-of-death cap after tab suspend PLANET_RADIUS: 220, // world units (UQM planet ~29 display px) - PLANET_GRAVITY: 1.6e6, // accel = G / r^2 (world units / frame^2) + PLANET_GRAVITY: 1.1e6, // accel = G / r^2 (world units / frame^2) PLANET_GRAVITY_MIN_R: 420, // clamp radius so slingshots stay sane PLANET_CREW_DAMAGE: 3, // crew lost on planet impact PLANET_DAMAGE_COOLDOWN: 24, // frames between crew-damaging planet hits diff --git a/src/games/starcontrol/StarControlShips.js b/src/games/starcontrol/StarControlShips.js index da6e088..6e827b8 100644 --- a/src/games/starcontrol/StarControlShips.js +++ b/src/games/starcontrol/StarControlShips.js @@ -318,23 +318,56 @@ export const PRIMARIES = { }, }, - // Kohr-Ah spinning blades: launched forward, then steered toward wherever - // the Marauder points while the button is held. + // Kohr-Ah spinning blades: one shot per button press (the button must be + // released before another blade can launch), launched dead ahead and left + // flying straight while the button stays held. Releasing lets a blade + // coast to a stop and then drift as a slow homing mine toward any enemy + // that strays close, otherwise it just sits there as a trap. Blades never + // expire on their own — only dying to a ship, asteroid, planet or another + // blade — but only maxCount (default 8) may be live at once; launching + // past that cap pops the oldest. guidedBlades: { fire(state, ship, def) { + ship.fx.bladeFireHeld = true; const m = muzzle(ship, (def.muzzleDist ?? 9) * 4, 0); - spawnProjectile(state, ship, { + const blade = spawnProjectile(state, ship, { type: 'blade', x: m.x, y: m.y, angle: m.angle, speed: def.speed, - damage: def.damage, hp: def.hp ?? 10, life: def.lifeFrames, + damage: def.damage, hp: def.hp ?? 10, life: Infinity, radius: def.radius ?? 30, + data: { trackRange: (def.trackRange ?? 224) * 4, wait: 0 }, behavior(st, p) { const owner = st.ships[p.owner]; - if (!owner.alive || !owner.input.fire) return; - steerProjectile(p, facingToRad(quantizeFacing(owner.facing)), FACING_STEP / 2, def.speed); + if (owner.alive && owner.input.fire) return; + if (Math.abs(p.vx) > 1 || Math.abs(p.vy) > 1) { + p.vx *= 0.5; p.vy *= 0.5; + return; + } + p.vx = 0; p.vy = 0; + if (p.data.wait > 0) { p.data.wait -= 1; return; } + p.data.wait = 4; + const enemy = enemyOf(st, owner); + if (!enemy.alive || tdist(p.x, p.y, enemy.x, enemy.y) > p.data.trackRange) return; + const a = bearingTo(p, enemy); + p.vx = Math.cos(a) * 8; p.vy = Math.sin(a) * 8; }, }); + const blades = (ship.fx.blades ?? []).filter((b) => b.alive); + blades.push(blade); + const max = def.maxCount ?? 8; + while (blades.length > max) { + const oldest = blades.shift(); + oldest.alive = false; + emit(state, { t: 'shotHit', x: oldest.x, y: oldest.y, kind: 'blade' }); + } + ship.fx.blades = blades; + }, + canFire(state, ship) { + return !ship.fx.bladeFireHeld; + }, + tick(state, ship) { + if (!ship.input.fire) ship.fx.bladeFireHeld = false; }, },