Aethelgard
Gemma 4 31b · composer

Reviews & generation details
Reviews
Human review · Jul 16, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
- Visual design
- 2 / 5
- UX / feel
- 3 / 5
- Works
- 2 / 5
- Prompt spirit
- 3 / 5
- Delight
- 2 / 5
good sound effects; broken game loop (enemy keeps attacking, not waiting for turn); poor/boring design with basic fonts
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 48,389 bytes · SHA-256 0c13c72f02f8
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aethelgard: Resonant Harmony</title>
<script src="https://cdn.tailwindcss.com/"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Press+Start+2P&display=swap">
<style>
/* ── css: styles ── */
:root {
--bg-deep: #0f172a;
--glass: rgba(30, 41, 59, 0.7);
--glass-border: rgba(255, 255, 255, 0.1);
--pyro: #ef4444;
--hydro: #3b82f6;
--flora: #22c55e;
--zephyr: #a855f7;
--earth: #f59e0b;
--volt: #eab308;
--cosmos: #94a3b8;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-deep);
color: white;
overflow: hidden;
user-select: none;
}
.retro-font {
font-family: 'Press Start 2P', cursive;
}
.glass-panel {
background: var(--glass);
backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
.neon-border-pyro { border-color: var(--pyro); box-shadow: 0 0 10px var(--pyro); }
.neon-border-hydro { border-color: var(--hydro); box-shadow: 0 0 10px var(--hydro); }
.neon-border-flora { border-color: var(--flora); box-shadow: 0 0 10px var(--flora); }
.neon-border-zephyr { border-color: var(--zephyr); box-shadow: 0 0 10px var(--zephyr); }
.neon-border-earth { border-color: var(--earth); box-shadow: 0 0 10px var(--earth); }
.neon-border-volt { border-color: var(--volt); box-shadow: 0 0 10px var(--volt); }
.neon-border-cosmos { border-color: var(--cosmos); box-shadow: 0 0 10px var(--cosmos); }
.hp-bar-fill {
transition: width 0.5s ease-out, background-color 0.5s ease;
}
@keyframes bounce-idle {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.animate-bounce-idle {
animation: bounce-idle 2s ease-in-out infinite;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.animate-shake {
animation: shake 0.2s ease-in-out 0s 2;
}
.screen-transition {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.node-active {
filter: drop-shadow(0 0 8px white);
cursor: pointer;
}
.node-locked {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<div id="app" class="relative w-full h-screen overflow-hidden flex items-center justify-center p-4">
<!-- Main Menu -->
<div id="screen-menu" class="screen-transition absolute inset-0 z-20 flex flex-col items-center justify-center bg-slate-950">
<div class="text-center mb-12">
<h1 class="retro-font text-4xl md:text-6xl text-white mb-4 tracking-tighter drop-shadow-[0_0_15px_rgba(255,255,255,0.5)]">AETHELGARD</h1>
<p class="text-slate-400 font-light tracking-widest uppercase">Resonant Harmony</p>
</div>
<div class="flex flex-col gap-4 w-64">
<button id="btn-start" class="glass-panel py-3 px-6 rounded-lg hover:bg-white/10 transition-all text-center font-semibold uppercase tracking-wider">Begin Journey</button>
<button id="btn-load" class="glass-panel py-3 px-6 rounded-lg hover:bg-white/10 transition-all text-center font-semibold uppercase tracking-wider">Continue</button>
<button id="btn-reset" class="glass-panel py-3 px-6 rounded-lg hover:bg-white/10 transition-all text-center font-semibold uppercase tracking-wider text-red-400">Wipe Memory</button>
</div>
<div class="absolute bottom-8 text-slate-600 text-xs uppercase tracking-widest">Capture the Resonance</div>
</div>
<!-- Overworld Map -->
<div id="screen-map" class="screen-transition hidden absolute inset-0 z-10 flex flex-col p-6">
<div class="flex justify-between items-center mb-8">
<div class="glass-panel px-4 py-2 rounded-full flex items-center gap-4">
<span class="text-xs uppercase text-slate-400">Shards:</span>
<span id="map-shards" class="font-bold text-yellow-400">0</span>
</div>
<h2 id="zone-title" class="retro-font text-lg text-white">The Verdant Canopy</h2>
<button id="btn-open-dex" class="glass-panel px-4 py-2 rounded-full text-xs uppercase font-semibold hover:bg-white/10">Dex</button>
</div>
<div id="map-canvas-container" class="flex-1 relative overflow-hidden rounded-3xl border border-white/10 bg-slate-900/50">
<svg id="map-svg" class="w-full h-full"></svg>
</div>
<div class="flex justify-center gap-4 mt-8">
<button id="btn-open-shop" class="glass-panel px-8 py-3 rounded-xl font-semibold uppercase text-sm hover:bg-white/10">Camp Shop</button>
<button id="btn-open-party" class="glass-panel px-8 py-3 rounded-xl font-semibold uppercase text-sm hover:bg-white/10">Resonant Team</button>
</div>
</div>
<!-- Battle Screen -->
<div id="screen-battle" class="screen-transition hidden absolute inset-0 z-30 flex flex-col bg-slate-950 p-6">
<div class="flex-1 flex flex-col justify-between relative">
<!-- Enemy Side -->
<div class="flex justify-end items-start gap-6">
<div id="enemy-stats" class="glass-panel p-4 rounded-2xl w-64">
<div class="flex justify-between items-center mb-2">
<span id="enemy-name" class="font-bold truncate">Enemy</span>
<span id="enemy-lv" class="text-xs text-slate-400">Lv 5</span>
</div>
<div class="w-full h-3 bg-slate-800 rounded-full overflow-hidden">
<div id="enemy-hp-bar" class="hp-bar-fill h-full w-full bg-green-500"></div>
</div>
<div class="text-right text-[10px] mt-1 text-slate-500 uppercase" id="enemy-hp-text">100 / 100</div>
</div>
<div id="enemy-sprite-container" class="w-48 h-48 animate-bounce-idle flex items-center justify-center">
<!-- SVG goes here -->
</div>
</div>
<!-- Player Side -->
<div class="flex justify-start items-end gap-6">
<div id="player-sprite-container" class="w-48 h-48 animate-bounce-idle flex items-center justify-center">
<!-- SVG goes here -->
</div>
<div id="player-stats" class="glass-panel p-4 rounded-2xl w-64">
<div class="flex justify-between items-center mb-2">
<span id="player-name" class="font-bold truncate">Ally</span>
<span id="player-lv" class="text-xs text-slate-400">Lv 5</span>
</div>
<div class="w-full h-3 bg-slate-800 rounded-full overflow-hidden">
<div id="player-hp-bar" class="hp-bar-fill h-full w-full bg-green-500"></div>
</div>
<div class="text-right text-[10px] mt-1 text-slate-500 uppercase" id="player-hp-text">100 / 100</div>
</div>
</div>
</div>
<!-- Battle Console -->
<div id="battle-console" class="glass-panel h-48 rounded-t-3xl p-6 flex flex-col gap-4 relative">
<div id="battle-text" class="text-lg font-medium text-center py-2">A wild Resonant appeared!</div>
<div id="battle-actions" class="grid grid-cols-2 gap-4 flex-1">
<!-- Action buttons injected here -->
</div>
<!-- Battle Overlay / Transition -->
<div id="battle-overlay" class="absolute inset-0 bg-black/60 hidden flex items-center justify-center rounded-t-3xl">
<div id="battle-message" class="retro-font text-xl text-center px-8">...</div>
</div>
</div>
</div>
<!-- Modal Overlay -->
<div id="modal-overlay" class="hidden absolute inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-6">
<div id="modal-content" class="glass-panel max-w-2xl w-full max-h-[80vh] overflow-y-auto rounded-3xl p-8 relative">
<button id="btn-close-modal" class="absolute top-4 right-4 text-slate-400 hover:text-white text-2xl">×</button>
<div id="modal-body"></div>
</div>
</div>
</div>
<script>
'use strict';
// ── class: Synth ──
class Synth {
constructor() {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
async play(type) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
const now = this.ctx.currentTime;
switch (type) {
case 'hover':
osc.type = 'sine';
osc.frequency.setValueAtTime(440, now);
osc.frequency.exponentialRampToValueAtTime(880, now + 0.1);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start();
osc.stop(now + 0.1);
break;
case 'select':
osc.type = 'square';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(600, now + 0.1);
gain.gain.setValueAtTime(0.1, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
osc.start();
osc.stop(now + 0.1);
break;
case 'hit':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, now);
osc.frequency.linearRampToValueAtTime(50, now + 0.2);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0, now + 0.2);
osc.start();
osc.stop(now + 0.2);
break;
case 'critical':
osc.type = 'square';
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(1200, now + 0.1);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.2);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0, now + 0.3);
osc.start();
osc.stop(now + 0.3);
break;
case 'levelUp':
const notes = [440, 554, 659, 880];
notes.forEach((f, i) => {
const o = this.ctx.createOscillator();
const g = this.ctx.createGain();
o.type = 'triangle';
o.frequency.setValueAtTime(f, now + i*0.1);
o.connect(g);
g.connect(this.ctx.destination);
g.gain.setValueAtTime(0.1, now + i*0.1);
g.gain.exponentialRampToValueAtTime(0.01, now + i*0.1 + 0.2);
o.start(now + i*0.1);
o.stop(now + i*0.1 + 0.2);
});
break;
case 'capture':
osc.type = 'sine';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(1000, now + 0.5);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
osc.start();
osc.stop(now + 0.5);
break;
}
}
}
// ── const: ELEMENT_DATA ──
const ELEMENT_DATA = {
Pyro: { color: '#ef4444', strong: 'Flora', weak: 'Hydro', borderColor: 'neon-border-pyro' },
Hydro: { color: '#3b82f6', strong: 'Pyro', weak: 'Flora', borderColor: 'neon-border-hydro' },
Flora: { color: '#22c55e', strong: 'Hydro', weak: 'Pyro', borderColor: 'neon-border-flora' },
Zephyr: { color: '#a855f7', strong: 'Earth', weak: 'Volt', borderColor: 'neon-border-zephyr' },
Earth: { color: '#f59e0b', strong: 'Volt', weak: 'Zephyr', borderColor: 'neon-border-earth' },
Volt: { color: '#eab308', strong: 'Zephyr', weak: 'Earth', borderColor: 'neon-border-volt' },
Cosmos: { color: '#94a3b8', strong: null, weak: null, borderColor: 'neon-border-cosmos' }
};
// ── const: RESONANTS_DB ──
const RESONANTS_DB = [
// PYRO LINE
{
id: 'emberkit',
name: 'Emberkit',
type: 'Pyro',
baseStats: { hp: 45, att: 55, def: 40, spd: 60 },
evoLevel: 8,
evoTo: 'pyrowolf',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><defs><radialGradient id="grad-pyro"><stop offset="0%" stop-color="#ff7e5f"/><stop offset="100%" stop-color="#feb47b"/></radialGradient></defs><path d="M50 20 L70 50 L50 80 L30 50 Z" fill="url(#grad-pyro)" /><path d="M40 40 Q50 30 60 40" stroke="white" fill="none" stroke-width="2"/><circle cx="40" cy="45" r="3" fill="white"/><circle cx="60" cy="45" r="3" fill="white"/></svg>`,
moves: [
{ name: 'Singe', power: 40, acc: 95, type: 'Pyro', lv: 1 },
{ name: 'Flame Dash', power: 60, acc: 90, type: 'Pyro', lv: 5 },
{ name: 'Heat Wave', power: 80, acc: 85, type: 'Pyro', lv: 12 },
{ name: 'Inferno Burst', power: 110, acc: 80, type: 'Pyro', lv: 20 }
]
},
{
id: 'pyrowolf',
name: 'Pyrowolf',
type: 'Pyro',
baseStats: { hp: 65, att: 75, def: 60, spd: 80 },
evoLevel: 15,
evoTo: 'ignisaur',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M30 80 L50 40 L70 80 Z" fill="#ef4444"/><path d="M40 30 L50 10 L60 30" fill="#ef4444"/><circle cx="45" cy="40" r="3" fill="white"/><circle cx="55" cy="40" r="3" fill="white"/></svg>`,
moves: [
{ name: 'Singe', power: 40, acc: 95, type: 'Pyro', lv: 1 },
{ name: 'Flame Dash', power: 60, acc: 90, type: 'Pyro', lv: 5 },
{ name: 'Heat Wave', power: 80, acc: 85, type: 'Pyro', lv: 12 },
{ name: 'Inferno Burst', power: 110, acc: 80, type: 'Pyro', lv: 20 }
]
},
{
id: 'ignisaur',
name: 'Ignisaur',
type: 'Pyro',
baseStats: { hp: 90, att: 100, def: 85, spd: 70 },
evoLevel: null,
evoTo: null,
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M20 80 Q50 20 80 80 Z" fill="#b91c1c"/><path d="M40 20 Q50 0 60 20" fill="#ef4444"/><circle cx="40" cy="40" r="4" fill="yellow"/><circle cx="60" cy="40" r="4" fill="yellow"/></svg>`,
moves: [
{ name: 'Singe', power: 40, acc: 95, type: 'Pyro', lv: 1 },
{ name: 'Flame Dash', power: 60, acc: 90, type: 'Pyro', lv: 5 },
{ name: 'Heat Wave', power: 80, acc: 85, type: 'Pyro', lv: 12 },
{ name: 'Inferno Burst', power: 110, acc: 80, type: 'Pyro', lv: 20 }
]
},
// HYDRO LINE
{
id: 'droplet',
name: 'Droplet',
type: 'Hydro',
baseStats: { hp: 50, att: 40, def: 50, spd: 65 },
evoLevel: 8,
evoTo: 'geoxus',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M50 20 Q80 50 50 80 Q20 50 50 20" fill="#3b82f6"/><circle cx="45" cy="40" r="2" fill="white"/><circle cx="55" cy="40" r="2" fill="white"/></svg>`,
moves: [
{ name: 'Saturate', power: 40, acc: 95, type: 'Hydro', lv: 1 },
{ name: 'Aqua Jet', power: 60, acc: 90, type: 'Hydro', lv: 5 },
{ name: 'Hydro Pump', power: 80, acc: 85, type: 'Hydro', lv: 12 },
{ name: 'Oceanic Tide', power: 110, acc: 80, type: 'Hydro', lv: 20 }
]
},
{
id: 'geoxus',
name: 'Geoxus',
type: 'Hydro',
baseStats: { hp: 70, att: 60, def: 70, spd: 70 },
evoLevel: 15,
evoTo: 'neptunian',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M30 60 Q50 20 70 60 L70 80 L30 80 Z" fill="#2563eb"/><circle cx="45" cy="45" r="3" fill="white"/><circle cx="55" cy="45" r="3" fill="white"/></svg>`,
moves: [
{ name: 'Saturate', power: 40, acc: 95, type: 'Hydro', lv: 1 },
{ name: 'Aqua Jet', power: 60, acc: 90, type: 'Hydro', lv: 5 },
{ name: 'Hydro Pump', power: 80, acc: 85, type: 'Hydro', lv: 12 },
{ name: 'Oceanic Tide', power: 110, acc: 80, type: 'Hydro', lv: 20 }
]
},
{
id: 'neptunian',
name: 'Neptunian',
type: 'Hydro',
baseStats: { hp: 95, att: 80, def: 90, spd: 60 },
evoLevel: null,
evoTo: null,
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M20 70 Q50 10 80 70 L80 90 L20 90 Z" fill="#1d4ed8"/><path d="M30 70 L20 60 M70 70 L80 60" stroke="#1d4ed8" stroke-width="4"/><circle cx="40" cy="40" r="4" fill="white"/><circle cx="60" cy="40" r="4" fill="white"/></svg>`,
moves: [
{ name: 'Saturate', power: 40, acc: 95, type: 'Hydro', lv: 1 },
{ name: 'Aqua Jet', power: 60, acc: 90, type: 'Hydro', lv: 5 },
{ name: 'Hydro Pump', power: 80, acc: 85, type: 'Hydro', lv: 12 },
{ name: 'Oceanic Tide', power: 110, acc: 80, type: 'Hydro', lv: 20 }
]
},
// FLORA LINE
{
id: 'seedling',
name: 'Seedling',
type: 'Flora',
baseStats: { hp: 60, att: 45, def: 55, spd: 50 },
evoLevel: 8,
evoTo: 'thornback',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><circle cx="50" cy="60" r="20" fill="#22c55e"/><path d="M50 40 L50 20 Q60 20 60 30" stroke="#16a34a" stroke-width="4" fill="none"/><circle cx="45" cy="55" r="2" fill="white"/><circle cx="55" cy="55" r="2" fill="white"/></svg>`,
moves: [
{ name: 'Leaf Slap', power: 40, acc: 95, type: 'Flora', lv: 1 },
{ name: 'Vine Whip', power: 60, acc: 90, type: 'Flora', lv: 5 },
{ name: 'Solar Beam', power: 80, acc: 85, type: 'Flora', lv: 12 },
{ name: 'Gaea Crash', power: 110, acc: 80, type: 'Flora', lv: 20 }
]
},
{
id: 'thornback',
name: 'Thornback',
type: 'Flora',
baseStats: { hp: 80, att: 65, def: 75, spd: 60 },
evoLevel: 15,
evoTo: 'bloomgoliath',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M30 80 L70 80 L60 40 L40 40 Z" fill="#16a34a"/><path d="M30 80 L20 70 M70 80 L80 70" stroke="#16a34a" stroke-width="4"/><circle cx="45" cy="50" r="3" fill="white"/><circle cx="55" cy="50" r="3" fill="white"/></svg>`,
moves: [
{ name: 'Leaf Slap', power: 40, acc: 95, type: 'Flora', lv: 1 },
{ name: 'Vine Whip', power: 60, acc: 90, type: 'Flora', lv: 5 },
{ name: 'Solar Beam', power: 80, acc: 85, type: 'Flora', lv: 12 },
{ name: 'Gaea Crash', power: 110, acc: 80, type: 'Flora', lv: 20 }
]
},
{
id: 'bloomgoliath',
name: 'Bloomgoliath',
type: 'Flora',
baseStats: { hp: 110, att: 85, def: 100, spd: 40 },
evoLevel: null,
evoTo: null,
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M20 80 Q50 30 80 80 Z" fill="#14532d"/><circle cx="50" cy="40" r="25" fill="#22c55e"/><circle cx="40" cy="35" r="4" fill="white"/><circle cx="60" cy="35" r="4" fill="white"/></svg>`,
moves: [
{ name: 'Leaf Slap', power: 40, acc: 95, type: 'Flora', lv: 1 },
{ name: 'Vine Whip', power: 60, acc: 90, type: 'Flora', lv: 5 },
{ name: 'Solar Beam', power: 80, acc: 85, type: 'Flora', lv: 12 },
{ name: 'Gaea Crash', power: 110, acc: 80, type: 'Flora', lv: 20 }
]
},
// VOLT LINE
{
id: 'sparky',
name: 'Sparky',
type: 'Volt',
baseStats: { hp: 40, att: 60, def: 40, spd: 80 },
evoLevel: 8,
evoTo: 'boltsaber',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M50 30 L60 50 L50 70 L40 50 Z" fill="#eab308"/><path d="M50 30 L40 20 M50 30 L60 20" stroke="#eab308" stroke-width="3"/><circle cx="45" cy="45" r="2" fill="black"/><circle cx="55" cy="45" r="2" fill="black"/></svg>`,
moves: [
{ name: 'Static Zap', power: 40, acc: 95, type: 'Volt', lv: 1 },
{ name: 'Volt Tackle', power: 60, acc: 90, type: 'Volt', lv: 5 },
{ name: 'Thunder Bolt', power: 80, acc: 85, type: 'Volt', lv: 12 },
{ name: 'Plasma Storm', power: 110, acc: 80, type: 'Volt', lv: 20 }
]
},
{
id: 'boltsaber',
name: 'Boltsaber',
type: 'Volt',
baseStats: { hp: 60, att: 80, def: 60, spd: 100 },
evoLevel: 15,
evoTo: 'stormlord',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M30 70 L50 30 L70 70 Z" fill="#ca8a04"/><path d="M50 30 L50 10" stroke="#eab308" stroke-width="5"/><circle cx="44" cy="50" r="3" fill="black"/><circle cx="56" cy="50" r="3" fill="black"/></svg>`,
moves: [
{ name: 'Static Zap', power: 40, acc: 95, type: 'Volt', lv: 1 },
{ name: 'Volt Tackle', power: 60, acc: 90, type: 'Volt', lv: 5 },
{ name: 'Thunder Bolt', power: 80, acc: 85, type: 'Volt', lv: 12 },
{ name: 'Plasma Storm', power: 110, acc: 80, type: 'Volt', lv: 20 }
]
},
{
id: 'stormlord',
name: 'Stormlord',
type: 'Volt',
baseStats: { hp: 85, att: 110, def: 70, spd: 110 },
evoLevel: null,
evoTo: null,
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M20 80 L50 20 L80 80 Z" fill="#a16207"/><path d="M30 30 L20 10 M70 30 L80 10" stroke="#eab308" stroke-width="4"/><circle cx="40" cy="50" r="4" fill="black"/><circle cx="60" cy="50" r="4" fill="black"/></svg>`,
moves: [
{ name: 'Static Zap', power: 40, acc: 95, type: 'Volt', lv: 1 },
{ name: 'Volt Tackle', power: 60, acc: 90, type: 'Volt', lv: 5 },
{ name: 'Thunder Bolt', power: 80, acc: 85, type: 'Volt', lv: 12 },
{ name: 'Plasma Storm', power: 110, acc: 80, type: 'Volt', lv: 20 }
]
},
// ZEPHYR LINE
{
id: 'gusty',
name: 'Gusty',
type: 'Zephyr',
baseStats: { hp: 45, att: 50, def: 40, spd: 85 },
evoLevel: 8,
evoTo: 'slipstream',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M50 20 Q80 50 50 80 Q20 50 50 20" fill="#a855f7" opacity="0.7"/><path d="M40 40 Q50 30 60 40" stroke="white" fill="none"/><circle cx="40" cy="45" r="2" fill="white"/><circle cx="60" cy="45" r="2" fill="white"/></svg>`,
moves: [
{ name: 'Air Puff', power: 40, acc: 95, type: 'Zephyr', lv: 1 },
{ name: 'Wind Cut', power: 60, acc: 90, type: 'Zephyr', lv: 5 },
{ name: 'Cyclone', power: 80, acc: 85, type: 'Zephyr', lv: 12 },
{ name: 'Sky Rupture', power: 110, acc: 80, type: 'Zephyr', lv: 20 }
]
},
{
id: 'slipstream',
name: 'Slipstream',
type: 'Zephyr',
baseStats: { hp: 65, att: 70, def: 60, spd: 105 },
evoLevel: 15,
evoTo: 'zephyrus',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M20 50 L50 20 L80 50 L50 80 Z" fill="#8b5cf6"/><path d="M30 40 L20 40 M70 40 L80 40" stroke="white" stroke-width="2"/><circle cx="45" cy="45" r="3" fill="white"/><circle cx="55" cy="45" r="3" fill="white"/></svg>`,
moves: [
{ name: 'Air Puff', power: 40, acc: 95, type: 'Zephyr', lv: 1 },
{ name: 'Wind Cut', power: 60, acc: 90, type: 'Zephyr', lv: 5 },
{ name: 'Cyclone', power: 80, acc: 85, type: 'Zephyr', lv: 12 },
{ name: 'Sky Rupture', power: 110, acc: 80, type: 'Zephyr', lv: 20 }
]
},
{
id: 'zephyrus',
name: 'Zephyrus',
type: 'Zephyr',
baseStats: { hp: 85, att: 90, def: 80, spd: 120 },
evoLevel: null,
evoTo: null,
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M10 50 L50 10 L90 50 L50 90 Z" fill="#6d28d9"/><path d="M20 50 L10 60 M80 50 L90 60" stroke="white" stroke-width="3"/><circle cx="40" cy="40" r="4" fill="white"/><circle cx="60" cy="40" r="4" fill="white"/></svg>`,
moves: [
{ name: 'Air Puff', power: 40, acc: 95, type: 'Zephyr', lv: 1 },
{ name: 'Wind Cut', power: 60, acc: 90, type: 'Zephyr', lv: 5 },
{ name: 'Cyclone', power: 80, acc: 85, type: 'Zephyr', lv: 12 },
{ name: 'Sky Rupture', power: 110, acc: 80, type: 'Zephyr', lv: 20 }
]
},
// EARTH LINE
{
id: 'pebble',
name: 'Pebble',
type: 'Earth',
baseStats: { hp: 65, att: 50, def: 70, spd: 30 },
evoLevel: 8,
evoTo: 'obsidian',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M30 80 L70 80 L80 50 L20 50 Z" fill="#f59e0b"/><circle cx="40" cy="60" r="2" fill="black"/><circle cx="60" cy="60" r="2" fill="black"/></svg>`,
moves: [
{ name: 'Rock Throw', power: 40, acc: 95, type: 'Earth', lv: 1 },
{ name: 'Dirt Slam', power: 60, acc: 90, type: 'Earth', lv: 5 },
{ name: 'Tremor', power: 80, acc: 85, type: 'Earth', lv: 12 },
{ name: 'Mountain Split', power: 110, acc: 80, type: 'Earth', lv: 20 }
]
},
{
id: 'obsidian',
name: 'Obsidian',
type: 'Earth',
baseStats: { hp: 85, att: 70, def: 90, spd: 40 },
evoLevel: 15,
evoTo: 'terrasunder',
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M20 80 L80 80 L70 40 L30 40 Z" fill="#b45309"/><path d="M30 40 L40 30 L50 40 L60 30 L70 40" fill="none" stroke="#f59e0b" stroke-width="3"/><circle cx="40" cy="55" r="3" fill="black"/><circle cx="60" cy="55" r="3" fill="black"/></svg>`,
moves: [
{ name: 'Rock Throw', power: 40, acc: 95, type: 'Earth', lv: 1 },
{ name: 'Dirt Slam', power: 60, acc: 90, type: 'Earth', lv: 5 },
{ name: 'Tremor', power: 80, acc: 85, type: 'Earth', lv: 12 },
{ name: 'Mountain Split', power: 110, acc: 80, type: 'Earth', lv: 20 }
]
},
{
id: 'terrasunder',
name: 'Terrasunder',
type: 'Earth',
baseStats: { hp: 120, att: 90, def: 110, spd: 20 },
evoLevel: null,
evoTo: null,
svg: (lv) => `<svg viewBox="0 0 100 100" class="w-full h-full"><path d="M10 90 L90 90 L80 30 L20 30 Z" fill="#78350f"/><path d="M20 30 L30 10 L50 30 L70 10 L80 30" fill="none" stroke="#f59e0b" stroke-width="5"/><circle cx="35" cy="50" r="5" fill="black"/><circle cx="65" cy="50" r="5" fill="black"/></svg>`,
moves: [
{ name: 'Rock Throw', power: 40, acc: 95, type: 'Earth', lv: 1 },
{ name: 'Dirt Slam', power: 60, acc: 90, type: 'Earth', lv: 5 },
{ name: 'Tremor', power: 80, acc: 85, type: 'Earth', lv: 12 },
{ name: 'Mountain Split', power: 110, acc: 80, type: 'Earth', lv: 20 }
]
}
];
// ── class: GameState ──
class GameState {
constructor() {
this.player = {
shards: 500,
unlockedZone: 0,
completedNodes: [],
inventory: {
prisms: 5,
hyperPrisms: 2,
elixirs: 3,
cleansers: 3
},
party: []
};
this.zones = [
{
name: 'The Verdant Canopy',
nodes: [
{ id: 0, type: 'encounter', lv: [1, 3], resIds: ['seedling', 'gusty'], x: 100, y: 400, connected: [1] },
{ id: 1, type: 'cache', reward: { shards: 100 }, x: 250, y: 300, connected: [0, 2] },
{ id: 2, type: 'encounter', lv: [3, 5], resIds: ['seedling', 'gusty'], x: 400, y: 450, connected: [1, 3] },
{ id: 3, type: 'boss', bossId: 'guardian_1', x: 550, y: 250, connected: [2], unlockNext: true }
]
},
{
name: 'The Sunken Grotto',
nodes: [
{ id: 4, type: 'encounter', lv: [5, 8], resIds: ['droplet', 'pebble'], x: 100, y: 200, connected: [5] },
{ id: 5, type: 'spring', x: 300, y: 300, connected: [4, 6] },
{ id: 6, type: 'encounter', lv: [8, 11], resIds: ['droplet', 'pebble'], x: 500, y: 150, connected: [5, 7] },
{ id: 7, type: 'boss', bossId: 'guardian_2', x: 700, y: 300, connected: [6], unlockNext: true }
]
},
{
name: 'The Obsidian Crags',
nodes: [
{ id: 8, type: 'encounter', lv: [11, 14], resIds: ['emberkit', 'sparky'], x: 100, y: 400, connected: [9] },
{ id: 9, type: 'cache', reward: { shards: 250 }, x: 300, y: 200, connected: [8, 10] },
{ id: 10, type: 'encounter', lv: [14, 17], resIds: ['emberkit', 'sparky'], x: 500, y: 400, connected: [9, 11] },
{ id: 11, type: 'boss', bossId: 'guardian_3', x: 700, y: 200, connected: [10], unlockNext: true }
]
},
{
name: 'The Apex Pillar',
nodes: [
{ id: 12, type: 'encounter', lv: [15, 20], resIds: ['ignisaur', 'neptunian', 'bloomgoliath', 'stormlord', 'zephyrus', 'terrasunder'], x: 200, y: 400, connected: [13] },
{ id: 13, type: 'boss', bossId: 'final_boss', x: 500, y: 200, connected: [12], unlockNext: false }
]
}
];
}
save() {
localStorage.setItem('aethelgard_save', JSON.stringify(this.player));
}
load() {
const saved = localStorage.getItem('aethelgard_save');
if (saved) {
this.player = JSON.parse(saved);
}
}
}
// ── class: BattleEngine ──
class BattleEngine {
constructor(playerSync, synth) {
this.playerSync = playerSync;
this.synth = synth;
this.state = 'init'; // init, player-turn, enemy-turn, transition, win, loss
this.playerRes = null;
this.enemyRes = null;
this.logEl = document.getElementById('battle-text');
this.actionEl = document.getElementById('battle-actions');
this.overlayEl = document.getElementById('battle-overlay');
this.overlayMsgEl = document.getElementById('battle-message');
}
startBattle(enemyConfig) {
// Setup Resonants
const enemyBase = RESONANTS_DB.find(r => r.id === enemyConfig.resId);
this.enemyRes = this.createResonant(enemyBase, enemyConfig.lv);
this.playerRes = this.playerSync.party[0]; // Simplified: always first
this.updateUI();
this.setBattleState('player-turn');
this.log('A wild ' + this.enemyRes.name + ' appeared!');
this.renderActions();
}
createResonant(base, lv) {
const stats = {};
for (let s in base.baseStats) {
stats[s] = Math.floor((base.baseStats[s] * (lv * 2 + 5)) / 10);
}
return {
...base,
level: lv,
hp: stats.hp,
maxHp: stats.hp,
stats: stats,
currentHp: stats.hp,
xp: 0,
nextLv: lv * 100
};
}
setBattleState(state) {
this.state = state;
if (state === 'player-turn') {
this.renderActions();
} else {
this.actionEl.innerHTML = '';
}
}
log(msg) {
this.logEl.innerText = msg;
}
async showMessage(msg, duration = 1500) {
this.overlayEl.classList.remove('hidden');
this.overlayMsgEl.innerText = msg;
await new Promise(r => setTimeout(r, duration));
this.overlayEl.classList.add('hidden');
}
updateUI() {
// Enemy UI
document.getElementById('enemy-name').innerText = this.enemyRes.name;
document.getElementById('enemy-lv').innerText = `Lv ${this.enemyRes.level}`;
const enemyHpPerc = (this.enemyRes.currentHp / this.enemyRes.maxHp) * 100;
document.getElementById('enemy-hp-bar').style.width = `${enemyHpPerc}%`;
document.getElementById('enemy-hp-bar').className = 'hp-bar-fill h-full ' + this.getHpColor(enemyHpPerc);
document.getElementById('enemy-hp-text').innerText = `${Math.ceil(this.enemyRes.currentHp)} / ${this.enemyRes.maxHp}`;
document.getElementById('enemy-sprite-container').innerHTML = this.enemyRes.svg(this.enemyRes.level);
// Player UI
document.getElementById('player-name').innerText = this.playerRes.name;
document.getElementById('player-lv').innerText = `Lv ${this.playerRes.level}`;
const playerHpPerc = (this.playerRes.currentHp / this.playerRes.maxHp) * 100;
document.getElementById('player-hp-bar').style.width = `${playerHpPerc}%`;
document.getElementById('player-hp-bar').className = 'hp-bar-fill h-full ' + this.getHpColor(playerHpPerc);
document.getElementById('player-hp-text').innerText = `${Math.ceil(this.playerRes.currentHp)} / ${this.playerRes.maxHp}`;
document.getElementById('player-sprite-container').innerHTML = this.playerRes.svg(this.playerRes.level);
}
getHpColor(perc) {
if (perc > 50) return 'bg-green-500';
if (perc > 25) return 'bg-yellow-500';
return 'bg-red-500';
}
renderActions() {
this.actionEl.innerHTML = '';
// Attack
const attackBtn = this.createActionBtn('Attack', () => this.handleAttackSelection());
this.actionEl.appendChild(attackBtn);
// Prism
const prismBtn = this.createActionBtn('Prism', () => this.handleCapture());
this.actionEl.appendChild(prismBtn);
// Items
const itemBtn = this.createActionBtn('Items', () => this.handleItems());
this.actionEl.appendChild(itemBtn);
// Run
const runBtn = this.createActionBtn('Run', () => this.handleRun());
this.actionEl.appendChild(runBtn);
}
createActionBtn(text, callback) {
const b = document.createElement('button');
b.className = 'glass-panel py-2 px-4 rounded-xl hover:bg-white/10 transition-all font-semibold uppercase text-xs';
b.innerText = text;
b.onclick = () => {
this.synth.play('select');
callback();
};
return b;
}
async handleAttackSelection() {
// For simplicity in this demo, we'll show a sub-menu of moves
const moves = this.playerRes.moves.filter(m => m.lv <= this.playerRes.level);
this.actionEl.innerHTML = '';
moves.forEach(move => {
const b = this.createActionBtn(move.name, () => this.executeMove(this.playerRes, this.enemyRes, move));
this.actionEl.appendChild(b);
});
const back = this.createActionBtn('Back', () => this.renderActions());
this.actionEl.appendChild(back);
}
async executeMove(attacker, target, move) {
this.setBattleState('transition');
// Math
const typeMod = this.getTypeModifier(move.type, target.type);
const crit = Math.random() < 0.1;
const dmg = this.calcDamage(attacker, target, move) * typeMod * (crit ? 1.5 : 1);
this.synth.play(crit ? 'critical' : 'hit');
// Animation effect
const targetSprite = attacker === this.playerRes ?
document.getElementById('enemy-sprite-container') :
document.getElementById('player-sprite-container');
targetSprite.classList.add('animate-shake');
setTimeout(() => targetSprite.classList.remove('animate-shake'), 200);
target.currentHp -= dmg;
if (target.currentHp < 0) target.currentHp = 0;
this.updateUI();
let msg = `${attacker.name} used ${move.name}!`;
if (typeMod > 1) msg += " It's super effective!";
if (typeMod < 1 && typeMod > 0) msg += " It's not very effective...";
if (crit) msg += " CRITICAL HIT!";
this.log(msg);
await new Promise(r => setTimeout(r, 1500));
if (target.currentHp <= 0) {
this.handleVictory();
} else {
this.setBattleState('enemy-turn');
setTimeout(() => this.enemyTurn(), 1000);
}
}
calcDamage(attacker, target, move) {
const atk = attacker.stats.att;
const def = target.stats.def;
const power = move.power;
const lv = attacker.level;
const base = (((2 * lv / 5 + 2) * power * (atk / def)) / 50 + 2);
return base * (Math.random() * 0.15 + 0.85);
}
getTypeModifier(moveType, resType) {
const data = ELEMENT_DATA[moveType];
if (!data) return 1;
if (data.strong === resType) return 2;
if (data.weak === resType) return 0.5;
return 1;
}
async enemyTurn() {
const moves = this.enemyRes.moves.filter(m => m.lv <= this.enemyRes.level);
const move = moves[Math.floor(Math.random() * moves.length)];
await this.executeMove(this.enemyRes, this.playerRes, move);
if (this.playerRes.currentHp <= 0) {
this.handleLoss();
} else if (this.state !== 'win' && this.state !== 'loss') {
this.setBattleState('player-turn');
this.renderActions();
}
}
async handleCapture() {
if (this.playerSync.inventory.prisms <= 0) {
this.log("No Prisms left!");
return;
}
this.setBattleState('transition');
this.playerSync.inventory.prisms--;
this.log("Throwing Aether Prism...");
await new Promise(r => setTimeout(r, 1000));
const rate = (1 - (this.enemyRes.currentHp / this.enemyRes.maxHp)) * 0.8 + 0.2;
if (Math.random() < rate) {
this.synth.play('capture');
await this.showMessage("Successfully Captured " + this.enemyRes.name + "!");
this.playerSync.party.push({...this.enemyRes});
this.handleVictory();
} else {
this.log("The Resonant broke free!");
await new Promise(r => setTimeout(r, 1500));
this.setBattleState('player-turn');
this.renderActions();
}
}
async handleItems() {
this.actionEl.innerHTML = '';
const inv = this.playerSync.inventory;
if (inv.elixirs > 0) {
const b = this.createActionBtn(`Elixir (${inv.elixirs})`, () => {
this.playerRes.currentHp = Math.min(this.playerRes.maxHp, this.playerRes.currentHp + this.playerRes.maxHp * 0.5);
inv.elixirs--;
this.log("Used Elixir! Restored HP.");
this.updateUI();
setTimeout(() => this.setBattleState('enemy-turn'), 1500);
this.synth.play('select');
});
this.actionEl.appendChild(b);
}
const back = this.createActionBtn('Back', () => this.renderActions());
this.actionEl.appendChild(back);
}
handleRun() {
this.log("Escaped successfully!");
setTimeout(() => {
document.getElementById('screen-battle').classList.add('hidden');
document.getElementById('screen-map').classList.remove('hidden');
}, 1000);
}
handleVictory() {
this.state = 'win';
this.synth.play('levelUp');
const xpGain = this.enemyRes.level * 50;
this.playerRes.xp += xpGain;
this.log(`Victory! Gained ${xpGain} XP and 50 Shards.`);
this.playerSync.shards += 50;
// Level up check
if (this.playerRes.xp >= this.playerRes.nextLv) {
this.playerRes.level++;
this.playerRes.xp -= this.playerRes.nextLv;
this.playerRes.nextLv *= 1.2;
this.log(`${this.playerRes.name} leveled up to ${this.playerRes.level}!`);
}
setTimeout(() => {
document.getElementById('screen-battle').classList.add('hidden');
document.getElementById('screen-map').classList.remove('hidden');
}, 3000);
}
handleLoss() {
this.state = 'loss';
this.log("Your Resonant fainted...");
setTimeout(() => {
document.getElementById('screen-battle').classList.add('hidden');
document.getElementById('screen-map').classList.remove('hidden');
}, 3000);
}
}
// ── main ──
const synth = new Synth();
const state = new GameState();
const battle = new BattleEngine(state.player, synth);
// UI Elements
const screens = {
menu: document.getElementById('screen-menu'),
map: document.getElementById('screen-map'),
battle: document.getElementById('screen-battle'),
modal: document.getElementById('modal-overlay')
};
function showScreen(screenKey) {
Object.values(screens).forEach(s => s.classList.add('hidden'));
screens[screenKey].classList.remove('hidden');
}
function initPlayer() {
if (state.player.party.length === 0) {
// Starter choice
const starter = RESONANTS_DB[0]; // Emberkit
state.player.party.push({
...starter,
level: 5,
hp: 70,
maxHp: 70,
currentHp: 70,
stats: { hp: 70, att: 60, def: 50, spd: 60 },
xp: 0,
nextLv: 500
});
}
}
function renderMap() {
const zone = state.zones[state.player.unlockedZone];
document.getElementById('zone-title').innerText = zone.name;
document.getElementById('map-shards').innerText = state.player.shards;
const svg = document.getElementById('map-svg');
svg.innerHTML = '';
// Draw connections first
zone.nodes.forEach(node => {
node.connected.forEach(connId => {
const connNode = zone.nodes.find(n => n.id === connId);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', node.x);
line.setAttribute('y1', node.y);
line.setAttribute('x2', connNode.x);
line.setAttribute('y2', connNode.y);
line.setAttribute('stroke', 'rgba(255,255,255,0.2)');
line.setAttribute('stroke-width', '3');
svg.appendChild(line);
});
});
// Draw nodes
zone.nodes.forEach(node => {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', node.x);
circle.setAttribute('cy', node.y);
circle.setAttribute('r', '15');
circle.setAttribute('fill', node.type === 'boss' ? '#ef4444' : '#3b82f6');
const isUnlocked = state.player.completedNodes.includes(0) || node.id === 0 ||
zone.nodes.filter(n => state.player.completedNodes.includes(n.id) && n.connected.includes(node.id)).length > 0;
if (isUnlocked) {
circle.classList.add('node-active');
circle.onclick = () => handleNodeClick(node);
} else {
circle.classList.add('node-locked');
}
svg.appendChild(circle);
});
}
function handleNodeClick(node) {
synth.play('select');
if (node.type === 'encounter') {
const resId = node.resIds[Math.floor(Math.random() * node.resIds.length)];
const lv = Math.floor(Math.random() * (node.lv[1] - node.lv[0] + 1)) + node.lv[0];
showScreen('battle');
battle.startBattle({ resId, lv });
} else if (node.type === 'cache') {
state.player.shards += node.reward.shards;
state.player.completedNodes.push(node.id);
openModal(`<b> Cache Found!</b><br>Found ${node.reward.shards} Aether Shards!`);
renderMap();
} else if (node.type === 'spring') {
state.player.party.forEach(r => r.currentHp = r.maxHp);
openModal(`<b> Healing Spring</b><br>Your team has been fully restored!`);
renderMap();
} else if (node.type === 'boss') {
const boss = { resId: 'ignisaur', lv: 10 + state.player.unlockedZone * 5 };
showScreen('battle');
battle.startBattle(boss);
// Logic to unlock next zone would go here in victory handler
}
}
function openModal(html) {
document.getElementById('modal-body').innerHTML = `<div class="text-center">${html}</div>`;
screens.modal.classList.remove('hidden');
}
document.getElementById('btn-close-modal').onclick = () => screens.modal.classList.add('hidden');
document.getElementById('btn-start').onclick = () => {
synth.play('select');
initPlayer();
showScreen('map');
renderMap();
};
document.getElementById('btn-load').onclick = () => {
synth.play('select');
state.load();
initPlayer();
showScreen('map');
renderMap();
};
document.getElementById('btn-reset').onclick = () => {
synth.play('select');
localStorage.removeItem('aethelgard_save');
location.reload();
};
document.getElementById('btn-open-party').onclick = () => {
synth.play('select');
let html = '<h3 class="retro-font text-xl mb-4">Resonant Team</h3><div class="grid gap-4">';
state.player.party.forEach((r, i) => {
html += `
<div class="glass-panel p-4 rounded-xl flex items-center justify-between">
<div>
<div class="font-bold">${r.name} (Lv ${r.level})</div>
<div class="text-xs text-slate-400">${r.type} | HP: ${r.currentHp}/${r.maxHp}</div>
</div>
<div class="w-16 h-16">${r.svg(r.level)}</div>
</div>`;
});
html += '</div>';
openModal(html);
};
document.getElementById('btn-open-shop').onclick = () => {
synth.play('select');
let html = '<h3 class="retro-font text-xl mb-4">Camp Shop</h3><div class="grid gap-4">';
const items = [
{ name: 'Harmonizer Prism', price: 100, key: 'prisms', amount: 5 },
{ name: 'Hyper Prism', price: 300, key: 'hyperPrisms', amount: 2 },
{ name: 'Core Restorer', price: 150, key: 'elixirs', amount: 3 }
];
items.forEach(item => {
html += `
<div class="glass-panel p-4 rounded-xl flex items-center justify-between">
<div>
<div class="font-bold">${item.name}</div>
<div class="text-xs text-yellow-400">${item.price} Shards</div>
</div>
<button onclick="buyItem('${item.key}', ${item.price})" class="bg-white/20 px-4 py-2 rounded-lg text-xs uppercase font-bold hover:bg-white/30">Buy</button>
</div>`;
});
html += '</div>';
openModal(html);
};
window.buyItem = (key, price) => {
if (state.player.shards >= price) {
state.player.shards -= price;
state.player.inventory[key]++;
synth.play('select');
alert('Item purchased!');
renderMap();
} else {
alert('Not enough shards!');
}
};
document.getElementById('btn-open-dex').onclick = () => {
synth.play('select');
let html = '<h3 class="retro-font text-xl mb-4">Resonants Dex</h3><div class="grid grid-cols-2 gap-4">';
RESONANTS_DB.forEach(r => {
html += `
<div class="glass-panel p-4 rounded-xl flex flex-col items-center text-center">
<div class="w-20 h-20 mb-2">${r.svg(1)}</div>
<div class="font-bold text-sm">${r.name}</div>
<div class="text-[10px] text-slate-400 uppercase">${r.type}</div>
</div>`;
});
html += '</div>';
openModal(html);
};
</script>
</body>
</html>
<!-- agent-meta {"model":"gemma-4-31b","provider":"cerebras","persona":"composer","sessionId":"3d3a11f9-8dfa-4bbe-9c48-a9274d7dbadc","tokensIn":28874,"tokensOut":17124,"tokensTotal":45998,"turns":2,"toolCalls":11,"failedToolCalls":0,"timestamp":"2026-07-14T16:20:24.344Z"} -->