Skip to content

Commit 886b67f

Browse files
committed
Add phase transition banner, enemy focus shortcut, and visual polish
- Phase change banner: brief centered overlay ('YOUR TURN', 'COMBAT', 'ORDNANCE') with fade in/out animation on canvas - 'E' key focuses camera on nearest detected enemy ship - Added E key to help overlay controls reference
1 parent 14027b0 commit 886b67f

File tree

3 files changed

+88
-1
lines changed

3 files changed

+88
-1
lines changed

src/client/main.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { GameState, S2C, AstrogationOrder, OrdnanceLaunch, CombatAttack, ShipMovement } from '../shared/types';
2-
import { pixelToHex, hexEqual, hexVecLength } from '../shared/hex';
2+
import { pixelToHex, hexToPixel, hexEqual, hexVecLength } from '../shared/hex';
33
import { canAttack, getCombatStrength, computeOdds, computeRangeMod, computeVelocityMod } from '../shared/combat';
44
import { getSolarSystemMap, SCENARIOS, findBaseHex } from '../shared/map-data';
55
import { SHIP_STATS, ORDNANCE_MASS } from '../shared/constants';
@@ -130,6 +130,10 @@ class GameClient {
130130
} else if (e.key === '0' && this.state === 'playing_astrogation') {
131131
// 0 to clear burn
132132
this.clearSelectedBurn();
133+
} else if (e.key.toLowerCase() === 'e' && this.gameState &&
134+
(this.state === 'playing_astrogation' || this.state === 'playing_ordnance' || this.state === 'playing_combat' || this.state === 'playing_opponentTurn')) {
135+
// Focus camera on nearest enemy
136+
this.focusNearestEnemy();
133137
} else if (e.key.toLowerCase() === 'w' || e.key === 'ArrowUp') {
134138
this.renderer.camera.pan(0, 40);
135139
} else if (e.key.toLowerCase() === 's' || e.key === 'ArrowDown') {
@@ -580,12 +584,15 @@ class GameClient {
580584

581585
if (this.gameState.phase === 'combat' && isMyTurn) {
582586
this.setState('playing_combat');
587+
this.renderer.showPhaseBanner('COMBAT');
583588
playPhaseChange();
584589
} else if (this.gameState.phase === 'ordnance' && isMyTurn) {
585590
this.setState('playing_ordnance');
591+
this.renderer.showPhaseBanner('ORDNANCE');
586592
playPhaseChange();
587593
} else if (this.gameState.phase === 'astrogation' && isMyTurn) {
588594
this.setState('playing_astrogation');
595+
this.renderer.showPhaseBanner('YOUR TURN');
589596
playPhaseChange();
590597
} else {
591598
this.setState('playing_opponentTurn');
@@ -966,6 +973,31 @@ class GameClient {
966973
this.updateHUD();
967974
}
968975

976+
private focusNearestEnemy() {
977+
if (!this.gameState) return;
978+
const enemies = this.gameState.ships.filter(s =>
979+
s.owner !== this.playerId && !s.destroyed && s.detected,
980+
);
981+
if (enemies.length === 0) {
982+
this.ui.showToast('No detected enemies', 'info');
983+
return;
984+
}
985+
// Find the one nearest to current camera center
986+
let nearest = enemies[0];
987+
let bestDist = Infinity;
988+
for (const e of enemies) {
989+
const p = hexToPixel(e.position, HEX_SIZE);
990+
const dx = p.x - this.renderer.camera.x;
991+
const dy = p.y - this.renderer.camera.y;
992+
const dist = Math.sqrt(dx * dx + dy * dy);
993+
if (dist < bestDist) {
994+
bestDist = dist;
995+
nearest = e;
996+
}
997+
}
998+
this.renderer.centerOnHex(nearest.position);
999+
}
1000+
9691001
// --- Burn shortcuts ---
9701002

9711003
private setBurnDirection(dir: number) {

src/client/renderer.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export class Renderer {
182182
private combatEffects: CombatEffect[] = [];
183183
private hexFlashes: HexFlash[] = [];
184184
private movementEvents: { events: MovementEvent[]; showUntil: number } | null = null;
185+
private phaseBanner: { text: string; showUntil: number } | null = null;
185186
private lastTime = 0;
186187

187188
constructor(canvas: HTMLCanvasElement) {
@@ -349,6 +350,13 @@ export class Renderer {
349350
});
350351
}
351352

353+
showPhaseBanner(text: string) {
354+
this.phaseBanner = {
355+
text,
356+
showUntil: performance.now() + 1500,
357+
};
358+
}
359+
352360
isAnimating(): boolean {
353361
return this.animState !== null;
354362
}
@@ -467,6 +475,15 @@ export class Renderer {
467475
}
468476
}
469477

478+
// Phase banner (screen-space, center)
479+
if (this.phaseBanner) {
480+
if (now > this.phaseBanner.showUntil) {
481+
this.phaseBanner = null;
482+
} else {
483+
this.renderPhaseBanner(ctx, this.phaseBanner.text, now, this.phaseBanner.showUntil, w, h);
484+
}
485+
}
486+
470487
// Minimap (screen-space, bottom-right)
471488
if (this.map && this.gameState) {
472489
this.renderMinimap(ctx, w, h);
@@ -1605,6 +1622,43 @@ export class Renderer {
16051622
ctx.restore();
16061623
}
16071624

1625+
private renderPhaseBanner(ctx: CanvasRenderingContext2D, text: string, now: number, showUntil: number, screenW: number, screenH: number) {
1626+
const elapsed = 1500 - (showUntil - now);
1627+
// Fade in over 200ms, stay for 1000ms, fade out over 300ms
1628+
let alpha: number;
1629+
if (elapsed < 200) {
1630+
alpha = elapsed / 200;
1631+
} else if (elapsed < 1200) {
1632+
alpha = 1;
1633+
} else {
1634+
alpha = 1 - (elapsed - 1200) / 300;
1635+
}
1636+
alpha = Math.max(0, Math.min(1, alpha));
1637+
1638+
ctx.save();
1639+
ctx.globalAlpha = alpha * 0.9;
1640+
ctx.font = 'bold 22px monospace';
1641+
ctx.textAlign = 'center';
1642+
ctx.textBaseline = 'middle';
1643+
1644+
// Background bar
1645+
const metrics = ctx.measureText(text);
1646+
const barW = metrics.width + 60;
1647+
const barH = 40;
1648+
const barX = screenW / 2 - barW / 2;
1649+
const barY = screenH * 0.35 - barH / 2;
1650+
ctx.fillStyle = 'rgba(10, 10, 40, 0.7)';
1651+
ctx.beginPath();
1652+
ctx.roundRect(barX, barY, barW, barH, 6);
1653+
ctx.fill();
1654+
1655+
// Text
1656+
ctx.fillStyle = '#4fc3f7';
1657+
ctx.fillText(text, screenW / 2, screenH * 0.35);
1658+
1659+
ctx.restore();
1660+
}
1661+
16081662
private renderMinimap(ctx: CanvasRenderingContext2D, screenW: number, screenH: number) {
16091663
if (!this.map || !this.gameState) return;
16101664

static/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ <h3>Combat</h3>
146146
</div>
147147
<div class="help-section">
148148
<h3>Other</h3>
149+
<div class="help-row"><span class="help-key">E</span> Focus nearest enemy</div>
149150
<div class="help-row"><span class="help-key">M</span> Toggle sound</div>
150151
<div class="help-row"><span class="help-key">?</span> Toggle this help</div>
151152
</div>

0 commit comments

Comments
 (0)