Fix replay error alert, plan attack visualization feature

Replace native alert() calls in replay.ts URL/file load error handlers
with inline error display in the no-replay div. Add combat attack
direction visualization to §16.9 of the plan: engine emits combat_death
events with killer bot list; viewer draws directed arrows on kills.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-30 14:25:35 -04:00
parent a312a9bf3d
commit 250140d0f7
2 changed files with 8 additions and 2 deletions

View file

@ -4994,6 +4994,7 @@ smoothly between grid positions instead of teleporting.
| Bot idle | Subtle 2% scale pulse, 2s cycle. Stops on movement. | Continuous |
| Bot movement | 1-tile motion trail fading behind the bot, indicates direction of travel. At high speed, trails create visible flow patterns. | 150ms fade |
| Combat threat | Thin dashed line between bots within attack range (red). Shows who threatens whom. | 1 turn |
| Attack event | Directed arrows from each attacker to the dying bot's tile on the turn a combat kill lands. The Go engine's `executeCombat()` needs to emit a `combat_death` event (type constant already exists: `EventCombatDeath`) alongside each `bot_died`, listing `killers: [{bot_id, owner, position}]` — the enemies within attack radius that triggered the outnumbering condition. The web viewer reads `combat_death` events and draws a solid line (attacker player color, arrowhead) from each killer's tile center to the defender's tile center, fading over 300ms. Old replays lacking `combat_death` events keep the existing proximity-inference lines. Distinct from the ambient threat dashes — these fire only on actual kills and encode exact participants rather than spatial proximity. | 300ms fade |
| Bot death | Burst of 68 particles scattering outward from death position, fading to transparent. | 400ms |
| Energy collection | 4-line starburst radiating from the energy node + small "+1" text floating upward. | 200ms |
| Core capture | Radial shockwave ring expanding from the core. Core color transitions from loser to capturer. | 500ms |

View file

@ -1349,6 +1349,11 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void {
prevCriticalBtn.addEventListener('click', navigateToPrevCriticalMoment);
nextCriticalBtn.addEventListener('click', navigateToNextCriticalMoment);
function showLoadError(msg: string): void {
noReplayDiv.style.display = '';
noReplayDiv.innerHTML = `<span style="color:#f87171">${escapeHtml(String(msg))}</span>`;
}
fileInput.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
@ -1357,7 +1362,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void {
const replay = JSON.parse(text) as Replay;
loadReplay(replay);
} catch (err) {
alert('Failed to load replay: ' + err);
showLoadError('Failed to load replay: ' + err);
}
});
@ -1370,7 +1375,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void {
const replay = await response.json() as Replay;
loadReplay(replay);
} catch (err) {
alert('Failed to load replay from URL: ' + err);
showLoadError('Failed to load replay from URL: ' + err);
}
});