feat(viewer): render shrinking play-zone in replay viewer
Draws the active zone boundary and out-of-zone danger area using per-turn zone_bounds data from the replay. The zone renders as: - Red semi-transparent overlay outside the safe zone - Solid red boundary circle with dashed inner ring - Center cross marker - Inactive zones show as subtle dashed outline Changes: - Add ZoneBounds type to types.ts - Add zone_bounds field to ReplayTurn - Implement drawZone() method in replay-viewer.ts - Call drawZone() in renderStandardView() - Update replay-schema-v1.json with ZoneBounds definition Accepts: bf-k1oy Closes: bf-k1oy
This commit is contained in:
parent
af46a1da97
commit
16fce127ff
3 changed files with 99 additions and 0 deletions
|
|
@ -609,6 +609,29 @@
|
|||
"additionalProperties": {
|
||||
"$ref": "#/$defs/DebugInfo"
|
||||
}
|
||||
},
|
||||
"zone_bounds": {
|
||||
"description": "Active shrinking zone bounds at this turn. Only present when zone is enabled in config.",
|
||||
"$ref": "#/$defs/ZoneBounds"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ZoneBounds": {
|
||||
"type": "object",
|
||||
"description": "Bounds of the shrinking play-zone (storm) at a turn.",
|
||||
"required": ["center", "radius", "active"],
|
||||
"properties": {
|
||||
"center": {
|
||||
"$ref": "#/$defs/Position"
|
||||
},
|
||||
"radius": {
|
||||
"type": "integer",
|
||||
"description": "Radius of the safe zone in tiles. Bots outside this radius are killed.",
|
||||
"minimum": 0
|
||||
},
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the zone is currently shrinking. False before zone_start_turn."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1842,6 +1842,9 @@ export class ReplayViewer {
|
|||
this.drawCell(wall.row, wall.col, wallColor);
|
||||
}
|
||||
|
||||
// Draw shrinking zone (if active)
|
||||
this.drawZone();
|
||||
|
||||
// Draw cores
|
||||
for (const core of turnData.cores) {
|
||||
if (visible && !visible.has(this.posKey(core.position))) continue;
|
||||
|
|
@ -2572,6 +2575,72 @@ export class ReplayViewer {
|
|||
}
|
||||
}
|
||||
|
||||
private drawZone(): void {
|
||||
const turnData = this.replay?.turns[this.currentTurn];
|
||||
if (!turnData?.zone_bounds) return;
|
||||
|
||||
const { ctx, cellSize } = this;
|
||||
const { center, radius, active } = turnData.zone_bounds;
|
||||
|
||||
// Zone center in world coordinates
|
||||
const cx = center.col * cellSize + cellSize / 2;
|
||||
const cy = center.row * cellSize + cellSize / 2;
|
||||
const cr = radius * cellSize;
|
||||
|
||||
if (!active) {
|
||||
// Zone not active yet — draw subtle outline
|
||||
ctx.strokeStyle = 'rgba(239, 68, 68, 0.3)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([8, 8]);
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, cr, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw danger area (outside zone) — semi-transparent red overlay
|
||||
const mapW = this.replay!.map.cols * cellSize;
|
||||
const mapH = this.replay!.map.rows * cellSize;
|
||||
|
||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.15)';
|
||||
ctx.fillRect(0, 0, mapW, mapH);
|
||||
|
||||
// Use destination-out to "cut out" the safe zone circle
|
||||
ctx.globalCompositeOperation = 'destination-out';
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, cr, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// Draw zone boundary circle
|
||||
ctx.strokeStyle = '#ef4444';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, cr, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw inner dashed ring for visibility
|
||||
ctx.strokeStyle = 'rgba(239, 68, 68, 0.5)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, cr - 3, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Draw zone center marker (small cross)
|
||||
const crossSize = 4;
|
||||
ctx.strokeStyle = '#ef4444';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx - crossSize, cy);
|
||||
ctx.lineTo(cx + crossSize, cy);
|
||||
ctx.moveTo(cx, cy - crossSize);
|
||||
ctx.lineTo(cx, cy + crossSize);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
private drawBot(bot: ReplayBot, color: string): void {
|
||||
const { cellSize } = this;
|
||||
const targetX = bot.position.col * cellSize + cellSize / 2;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ export interface GameEvent {
|
|||
details: unknown;
|
||||
}
|
||||
|
||||
export interface ZoneBounds {
|
||||
center: Position;
|
||||
radius: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface ReplayTurn {
|
||||
turn: number;
|
||||
bots: ReplayBot[];
|
||||
|
|
@ -73,6 +79,7 @@ export interface ReplayTurn {
|
|||
energy_held: number[];
|
||||
events?: GameEvent[];
|
||||
debug?: Record<number, DebugInfo>;
|
||||
zone_bounds?: ZoneBounds;
|
||||
}
|
||||
|
||||
export interface ReplayCriticalMoment {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue