refactor starcontrol weapons and improve road visibility

- Rewrite Kohr-Ah guidedBlades: one-shot-per-press with blades that fly
  straight while held, then slow and become homing mines near enemies;
  blades persist indefinitely and cap at 8 live (oldest recycled)
- Add bladeFireHeld tracking and canFire/tick hooks for blade cooldown
- Update AI to tap fire for Kohr-Ah blades instead of holding
- Reduce PLANET_GRAVITY from 1.6e6 to 1.1e6 for better slingshot physics
- Add dark outline pass under road/rail lines in Civilization map for
  better visibility against light terrain
This commit is contained in:
Brian Fertig 2026-07-16 14:44:52 -06:00
parent 237a024f80
commit 2a08df9cd8
4 changed files with 62 additions and 21 deletions

View File

@ -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);
}

View File

@ -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) {

View File

@ -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

View File

@ -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;
},
},