Creature Collection
Gemma 4 26b a4b qat · composer

Reviews & generation details
Reviews
Human review · Jul 17, 2026 · rubric 1
Historical path-based review. Association with these exact bytes is unverified.
- Visual design
- 2 / 5
- UX / feel
- 2 / 5
- Works
- 3 / 5
- Prompt spirit
- 3 / 5
- Delight
- 1 / 5
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 35,912 bytes · SHA-256 7d67f21b06e3
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Creature Collection</title>
<style>
/* ── css: undefined ── */
:root {
--bg-color: #1a1a1a;
--panel-bg: rgba(255, 255, 255, 0.1);
--panel-border: rgba(255, 255, 255, 0.2);
--text-color: #e0e0e0;
--accent-color: #00d4ff;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
user-select: none;
}
#game-container {
position: relative;
width: 800px;
height: 480px;
border: 4px solid #333;
overflow: hidden;
box-shadow: 0 0 50px rgba(0,0,0,0.5);
}
#game-canvas {
width: 100%;
height: 100%;
display: block;
background: #000;
}
/* HUD */
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 60px;
pointer-events: none;
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 10px;
box-sizing: border-box;
z-index: 10;
}
#party-list {
display: flex;
gap: 10px;
}
.party-unit {
background: var(--panel-bg);
backdrop-filter: blur(5px);
padding: 5px 10px;
border: 1px solid var(--panel-border);
border-radius: 5px;
min-width: 120px;
}
.party-unit span {
display: block;
font-size: 12px;
font-weight: bold;
margin-bottom: 4px;
text-transform: uppercase;
}
.hp-container {
width: 100%;
height: 8px;
background: #333;
border-radius: 4px;
overflow: hidden;
}
.hp-bar-fill {
height: 100%;
transition: width 0.3s ease;
}
#controls-info {
background: var(--panel-bg);
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
}
/* Overlays */
.overlay-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.hidden {
display: none !important;
}
.overlay-screen.visible {
display: flex !important;
}
/* Title Screen */
#title-screen {
background: radial-gradient(circle, #222, #000);
}
.title-glow {
text-align: center;
font-size: 56px;
color: #fff;
text-shadow: 0 0 20px var(--accent-color);
margin-bottom: 40px;
}
.title-buttons {
display: flex;
flex-direction: column;
gap: 20px;
}
.title-buttons button {
padding: 12px 40px;
font-size: 20px;
background: transparent;
color: #fff;
border: 2px solid #555;
cursor: pointer;
transition: all 0.3s;
}
.title-buttons button:hover {
border-color: var(--accent-color);
box-shadow: 0 0 15px var(--accent-color);
background: rgba(0, 212, 255, 0.1);
}
/* Battle UI */
#battle-overlay {
background: #000;
}
.battle-scene {
display: flex;
width: 100%;
height: 60%;
position: relative;
}
.enemy-area, .player-area {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
padding: 20px;
}
.enemy-area {
align-items: flex-end;
}
.player-area {
align-items: flex-start;
}
.hp-container {
width: 120px;
height: 12px;
}
.hp-bar {
height: 100%;
width: 100%;
background: #0f0;
transition: width 0.5s;
}
#battle-menu {
width: 100%;
height: 40%;
background: rgba(20, 20, 20, 0.9);
border-top: 3px solid var(--accent-color);
display: flex;
}
#battle-commands {
width: 50%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
padding: 10px;
}
#battle-commands button {
background: #333;
color: white;
border: none;
border-bottom: 3px solid #555;
cursor: pointer;
font-weight: bold;
}
#battle-commands button:active {
border-bottom: none;
margin-top: 3px;
}
#battle-log {
width: 50%;
background: rgba(0,0,0,0.5);
padding: 10px;
font-size: 13px;
overflow-y: auto;
color: #aaa;
}
/* Dialog */
#dialog {
background: rgba(0,0,0,0.7);
pointer-events: none;
}
#dialog-box {
width: 80%;
height: 100px;
background: rgba(40, 40, 60, 0.9);
border: 3px solid var(--accent-color);
padding: 20px;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
}
#dialog-text {
font-size: 20px;
}
#dialog-close {
background: var(--accent-color);
border: none;
padding: 5px 15px;
cursor: pointer;
}
/* Menu Overlay */
#overlay {
background: rgba(0,0,0,0.8);
}
#menu-content {
background: #222;
padding: 20px;
border-radius: 10px;
width: 300px;
text-align: center;
}
#menu-items {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
#menu-items button, #menu-close {
padding: 8px;
background: #444;
color: white;
border: none;
cursor: pointer;
}
`,name:
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="hud">
<div id="party-list"></div>
<div id="controls-info">WASD/Arrows to move | M for Menu</div>
</div>
<div id="battle-overlay" class="overlay-screen hidden">
<div class="battle-scene">
<div class="enemy-area">
<div id="enemy-info">
<div id="enemy-name">Enemy</div>
<div class="hp-container"><div id="enemy-hp-bar" class="hp-bar"></div></div>
</div>
<div id="enemy-sprite"></div>
</div>
<div class="player-area">
<div id="player-info">
<div id="player-name">Player</div>
<div class="hp-container"><div id="player-hp-bar" class="hp-bar"></div></div>
</div>
<div id="player-sprite"></div>
</div>
</div>
<div id="battle-menu">
<div id="battle-commands">
<button id="cmd-attack">ATTACK</button>
<button id="cmd-skill">SKILL</button>
<button id="cmd-switch">SWITCH</button>
<button id="cmd-item">ITEM</button>
</div>
<div id="battle-log"></div>
</div>
</div>
<div id="overlay" class="overlay-screen hidden">
<div id="menu-content">
<h2 id="menu-title">Menu</h2>
<div id="menu-items"></div>
<button id="menu-close">Close</button>
</div>
</div>
<div id="dialog" class="overlay-screen hidden">
<div id="dialog-box">
<span id="dialog-text"></span>
<button id="dialog-close">Next</button>
</div>
</div>
<div id="title-screen" class="overlay-screen">
<h1 class="title-glow">ESSENTIA:<br>PRISMATIC BONDS</h1>
<div class="title-buttons">
<button id="btn-new-game">NEW BINDING</button>
<button id="btn-continue">CONTINUE</button>
</div>
</div>
</div>
<div id="app"></div>
<script>
'use strict';
// ── const: TYPES ──
/**
* ESSENTIA: PRISMATIC BONDS
* A single-file creature collection and adventure game.
*/
const TYPES = {
CINDER: { name: 'Cinder', color: '#FF4500', beats: ['UMBRA'], weakTo: ['AERO'] },
UMBRA: { name: 'Umbra', color: '#4B0082', beats: ['BLOOM'], weakTo: ['CINDER', 'HYDRO'] },
BLOOM: { name: 'Bloom', color: '#32CD32', beats: ['CINDER'], weakTo: ['AERO', 'UMBRA'] },
AERO: { name: 'Aero', color: '#87CEEB', beats: ['CINDER', 'BLOOM'], weakTo: ['HYDRO'] },
HYDRO: { name: 'Hydro', color: '#1E90FF', beats: ['AERO', 'UMBRA'], weakTo: ['CINDER'] },
PRIME: { name: 'Prime', color: '#FFD700', beats: ['CINDER', 'UMBRA', 'BLOOM', 'AERO', 'HYDRO', 'NULL'], weakTo: ['PRIME'] },
NULL: { name: 'Null', color: '#2F4F4F', beats: [], resists: ['CINDER', 'UMBRA', 'BLOOM', 'AERO', 'HYDRO', 'NULL'], weakTo: ['PRIME'] }
};
// ── const: TYPE_ID ──
// Refined logic based on exact prompt rules:
// A > B > C > A
// D > A & C
// E > D & B
// F beats all slightly
// G resists everything but is weak to F
// Mapping:
// A: Cinder, B: Umbra, C: Bloom, D: Aero, E: Hydro, F: Prime, G: Null
const TYPE_ID = {
CINDER: 0,
UMBRA: 1,
BLOOM: 2,
AERO: 3,
HYDRO: 4,
PRIME: 5,
NULL: 6
};
// ── const: TYPE_NAMES ──
const TYPE_NAMES = ['Cinder', 'Umbra', 'Bloom', 'Aero', 'Hydro', 'Prime', 'Null'];
// ── const: TYPE_COLORS ──
const TYPE_COLORS = ['#FF4500', '#4B0082', '#32CD32', '#87CEEB', '#1E90FF', '#FFD700', '#2F4F4F'];
// ── const: ABILITY_TYPES ──
const ABILITY_TYPES = {
PHYSICAL: 'Physical',
SPECIAL: 'Special',
STATUS: 'Status'
};
// ── const: STATUS_EFFECTS ──
const STATUS_EFFECTS = {
BURN: 'Burn', // DoT + half ATK
FROST: 'Frost', // Halve SPD
SHOCK: 'Shock', // Skip turn chance
REGEN: 'Regen', // Heal per turn
BARRIER: 'Barrier'// Deflect one hit
};
// ── const: ITEM_TYPES ──
const ITEM_TYPES = {
RESTORE: 'Restoration',
CATALYST: 'Catalyst',
TOOL: 'Field Tool',
COLLECTION: 'Collection Item'
};
// ── const: ITEMS ──
const ITEMS = {
RECOVERY_PULSE: { name: 'Recovery Pulse', type: ITEM_TYPES.RESTORE, effect: 'heal' },
ESSENCE_CORE: { name: 'Essence Core', type: ITEM_TYPES.CATALYST, effect: 'evolve' },
PRISM_LENS: { name: 'Prism Lens', type: ITEM_TYPES.TOOL, effect: 'field' },
STARDUST: { name: 'Stardust', type: ITEM_TYPES.COLLECTION, effect: 'capture' }
};
// ── const: GAME_CONSTANTS ──
const GAME_CONSTANTS = {
MAX_PARTY_SIZE: 6,
LEVEL_CAP: 50,
TILE_SIZE: 48,
VIEWPORT_W: 800,
VIEWPORT_H: 480,
SAVE_KEY: 'essentia_v1_save'
};
// ── class: Creature ──
// Refined Creature Class
class Creature {
constructor(id, level = 1) {
const data = CREATURE_DATA[id];
if (!data) throw new Error(`Creature data for ${id} not found.`);
this.id = id;
this.level = level;
this.name = data.name;
this.type = data.type; // 0-6 index
this.hpMax = this.calculateStat(data.baseHp, 'hp');
this.hp = this.hpMax;
this.atk = this.calculateStat(data.baseAtk, 'atk');
this.def = this.calculateStat(data.baseDef, 'def');
this.spd = this.calculateStat(data.baseSpd, 'spd');
this.abilities = [...data.abilities];
this.exp = 0;
this.status = null;
this.stats = { hp: data.baseHp, atk: data.baseAtk, def: data.baseDef, spd: data.baseSpd };
this.evolutionId = data.evolutionId || null;
}
calculateStat(base, statName) {
// Level 1-50 scaling
const growth = 2 + (this.level * 0.5);
return Math.floor(base + growth);
}
gainExp(amount) {
this.exp += amount;
const nextLevelExp = this.level * 100;
if (this.exp >= nextLevelExp && this.level < GAME_CONSTANTS.LEVEL_CAP) {
this.levelUp();
return true;
}
return false;
}
levelUp() {
this.level++;
this.stats.hp += 5;
this.stats.atk += 2;
this.stats.def += 2;
this.stats.spd += 2;
this.updateDerivedStats();
}
updateDerivedStats() {
this.hpMax = this.calculateStat(this.stats.hp, 'hp');
this.atk = this.calculateStat(this.stats.atk, 'atk');
this.def = this.calculateStat(this.stats.def, 'def');
this.spd = this.calculateStat(this.stats.spd, 'spd');
}
takeDamage(amount) {
const realDamage = Math.max(1, Math.floor(amount));
this.hp = Math.max(0, this.hp - realDamage);
return realDamage;
}
heal(amount) {
this.hp = Math.min(this.hpMax, this.hp + amount);
}
}
// ── const: CREATURE_DATA ──
const CREATURE_DATA = {
// CINDER LINE
'EMBERLIT': { name: 'Emberlit', type: 0, baseHp: 45, baseAtk: 12, baseDef: 8, baseSpd: 10, abilities: [{ name: 'Flare Kick', power: 40, type: 0, accuracy: 100, effect: 'burn' }, { name: 'Singe', power: 25, type: 0, accuracy: 100, effect: 'none' }], evolutionId: 'FLAMEWISP' },
'FLAMEWISP': { name: 'Flamewisp', type: 0, baseHp: 60, baseAtk: 20, baseDef: 12, baseSpd: 18, abilities: [{ name: 'Scorching Burst', power: 65, type: 0, accuracy: 95, effect: 'burn' }, { name: 'Flame Tail', power: 40, type: 0, accuracy: 100, effect: 'none' }], evolutionId: 'PYROPHANTOM' },
'PYROPHANTOM': { name: 'Pyrophantom', type: 0, baseHp: 85, baseAtk: 35, baseDef: 22, baseSpd: 35, abilities: [{ name: 'Hellfire', power: 110, type: 0, accuracy: 85, effect: 'burn' }, { name: 'Ash Cloud', power: 50, type: 0, accuracy: 90, effect: 'none' }], evolutionId: null },
// UMBRA LINE
'VOIDCLAW': { name: 'Voidclaw', type: 1, baseHp: 50, baseAtk: 15, baseDef: 10, baseSpd: 12, abilities: [{ name: 'Night Slash', power: 45, type: 1, accuracy: 100, effect: 'none' }, { name: 'Shadow Bite', power: 30, type: 1, accuracy: 100, effect: 'none' }], evolutionId: 'SHADOWSTALKER' },
'SHADOWSTALKER': { name: 'Shadowstalker', type: 1, baseHp: 70, baseAtk: 30, baseDef: 18, baseSpd: 30, abilities: [{ name: 'Shadow Dance', power: 60, type: 1, accuracy: 100, effect: 'speed_boost' }, { name: 'Dark Pulse', power: 55, type: 1, accuracy: 95, effect: 'none' }], evolutionId: null },
// BLOOM LINE
'FLORALIS': { name: 'Floralis', type: 2, baseHp: 65, baseAtk: 10, baseDef: 15, baseSpd: 8, abilities: [{ name: 'Leaf Blade', power: 40, type: 2, accuracy: 100, effect: 'none' }, { name: 'Seed Shot', power: 30, type: 2, accuracy: 100, effect: 'none' }], evolutionId: 'BLOOMWING' },
'BLOOMWING': { name: 'Bloomwing', type: 2, baseHp: 80, baseAtk: 15, baseDef: 20, baseSpd: 15, abilities: [{ name: 'Petal Storm', power: 55, type: 2, accuracy: 90, effect: 'none' }, { name: 'Nectar Sips', power: 40, type: 2, accuracy: 100, effect: 'heal' }], evolutionId: null },
// AERO LINE
'AEROBE': { name: 'Aerobe', type: 3, baseHp: 40, baseAtk: 18, baseDef: 6, baseSpd: 22, abilities: [{ name: 'Wind Whip', power: 40, type: 3, accuracy: 100, effect: 'none' }, { name: 'Gale Force', power: 50, type: 3, accuracy: 85, effect: 'none' }], evolutionId: 'SKYRAVEN' },
'SKYRAVEN': { name: 'Skyraven', type: 3, baseHp: 55, baseAtk: 30, baseDef: 10, baseSpd: 40, abilities: [{ name: 'Cyclone', power: 70, type: 3, accuracy: 85, effect: 'none' }, { name: 'Feather Dart', power: 35, type: 3, accuracy: 100, effect: 'none' }], evolutionId: null },
// HYDRO LINE
'TIDALIS': { name: 'Tidalis', type: 4, baseHp: 75, baseAtk: 10, baseDef: 18, baseSpd: 5, abilities: [{ name: 'Water Jet', power: 45, type: 4, accuracy: 100, effect: 'none' }, { name: 'Bubble Pop', power: 30, type: 4, accuracy: 100, effect: 'none' }], evolutionId: 'OCEANUS' },
'OCEANUS': { name: 'Oceanus', type: 4, baseHp: 100, baseAtk: 25, baseDef: 30, baseSpd: 5, abilities: [{ name: 'Tidal Wave', power: 80, type: 4, accuracy: 90, effect: 'none' }, { name: 'Deep Surge', power: 60, type: 4, accuracy: 100, effect: 'none' }], evolutionId: null },
// NULL/PRIME LINE (Wild/Special)
'VOIDGLIMMER': { name: 'Voidglimmer', type: 6, baseHp: 40, baseAtk: 40, baseDef: 10, baseSpd: 40, abilities: [{ name: 'Void Pulse', power: 50, type: 6, accuracy: 100, effect: 'none' }], evolutionId: 'NULLCORE' },
'NULLCORE': { name: 'Nullcore', type: 6, baseHp: 120, baseAtk: 60, baseDef: 40, baseSpd: 20, abilities: [{ name: 'Singularity', power: 150, type: 6, accuracy: 70, effect: 'none' }], evolutionId: null },
'PRIMUS': { name: 'Primus', type: 5, baseHp: 100, baseAtk: 50, baseDef: 50, baseSpd: 50, abilities: [{ name: 'Prismatic Ray', power: 100, type: 5, accuracy: 100, effect: 'none' }], evolutionId: null }
};
// ── class: GameState ──
class GameState {
constructor() {
this.party = [];
this.inventory = [];
this.currentZone = 0;
this.exploredZones = [];
this.badges = [];
this.lastSaveTime = null;
this.isGameOver = false;
this.isPaused = false;
}
addCreature(creature) {
if (this.party.length < GAME_CONSTANTS.MAX_PARTY_SIZE) {
this.party.push(creature);
return true;
}
return false;
}
addInventory(item) {
this.inventory.push(item);
return true;
}
save() {
const data = JSON.stringify({
party: this.party,
inventory: this.inventory,
currentZone: this.currentZone,
exploredZones: this.exploredZones,
badges: this.badges,
lastSaveTime: Date.now()
});
localStorage.setItem(GAME_CONSTANTS.SAVE_KEY, data);
}
load() {
const data = localStorage.getItem(GAME_CONSTANTS.SAVE_KEY);
if (data) {
const parsed = JSON.parse(data);
// Re-instantiate creatures
this.party = parsed.party.map(c => {
const creature = new Creature(c.id, c.level);
Object.assign(creature, c);
return creature;
});
this.inventory = parsed.inventory;
this.currentZone = parsed.currentZone;
this.exploredZones = parsed.exploredZones;
this.badges = parsed.badges;
this.lastSaveTime = parsed.lastSaveTime;
return true;
}
return false;
}
}
// ── class: WorldManager ──
class WorldManager {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.map = [];
this.playerPos = { x: 2, y: 2 };
this.zones = [
{ name: "Whispering Glade", color: "#2d5a27", encounterRate: 0.1 },
{ name: "Searing Wastes", color: "#a0522d", encounterRate: 0.2 },
{ name: "Azure Reef", color: "#0077be", encounterRate: 0.15 },
{ name: "Shadowed Canyon", color: "#3c1c5c", encounterRate: 0.3 },
{ name: "Cloud Peak", color: "#87ceeb", encounterRate: 0.1 },
{ name: "Void Rift", color: "#000000", encounterRate: 0.4 }
];
this.tileSize = 48;
this.initMap();
}
initMap() {
this.map = [];
for (let y = 0; y < 15; y++) {
const row = [];
for (let x = 0; x < 20; x++) {
row.push(this.generateTile(x, y));
}
this.map.push(row);
}
}
generateTile(x, y) {
const rand = Math.random();
if (rand < 0.05) return 'WALL';
if (rand < 0.07) return 'SHRINE';
if (rand < 0.09) return 'BOSS';
return 'EMPTY';
}
update() {}
draw(ctx) {
const zone = this.zones[gameState.currentZone];
ctx.fillStyle = zone.color;
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (let y = 0; y < 15; y++) {
for (let x = 0; x < 20; x++) {
const tile = this.map[y][x];
const drawX = (x - this.playerPos.x) * this.tileSize + (this.canvas.width/2);
const drawY = (y - this.playerPos.y) * this.tileSize + (this.canvas.height/2);
if (tile === 'WALL') {
ctx.fillStyle = '#333';
ctx.fillRect(drawX, drawY, this.tileSize, this.tileSize);
} else if (tile === 'SHRINE') {
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(drawX + 24, drawY + 24, 10, 0, Math.PI * 2);
ctx.fill();
} else if (tile === 'BOSS') {
ctx.fillStyle = 'red';
ctx.fillRect(drawX, drawY, this.tileSize, this.tileSize);
}
}
}
// Player
ctx.fillStyle = 'white';
ctx.fillRect(
this.playerPos.x * this.tileSize + (this.canvas.width/2) - 24 + 8,
this.playerPos.y * this.tileSize + (this.canvas.height/2) - 24 + 8,
this.tileSize - 16,
this.tileSize - 16
);
}
movePlayer(dx, dy) {
const newX = this.playerPos.x + dx;
const newY = this.playerPos.y + dy;
if (newX >= 0 && newX < 20 && newY >= 0 && newY < 15) {
const tile = this.map[newY][newX];
if (tile !== 'WALL') {
this.playerPos.x = newX;
this.playerPos.y = newY;
this.checkEvent(tile);
}
}
}
checkEvent(tile) {
if (tile === 'SHRINE') {
uiManager.showDialogue("A Shrine! You feel refreshed.");
gameState.party.forEach(c => c.heal(20));
uiManager.updateHUD(gameState);
} else if (tile === 'BOSS') {
this.triggerBattle('BOSS');
} else {
const zone = this.zones[gameState.currentZone];
if (Math.random() < zone.encounterRate) {
this.triggerBattle('WILD');
}
}
}
triggerBattle(type) {
const playerCreature = gameState.party[0];
let enemyId = 'EMBERLIT';
if (type === 'BOSS') enemyId = 'PYROPHANTOM';
else if (Math.random() > 0.5) enemyId = 'VOIDCLAW';
else enemyId = 'FLORALIS';
const enemy = new Creature(enemyId, playerCreature.level);
battleManager.start(playerCreature, enemy);
document.getElementById('battle-overlay').classList.remove('hidden');
document.getElementById('battle-overlay').classList.add('visible');
}
}
// ── class: BattleManager ──
class BattleManager {
constructor() {
this.active = false;
this.turn = 0;
this.log = [];
this.playerCreature = null;
this.enemyCreature = null;
}
start(playerCreature, enemyCreature) {
this.active = true;
this.playerCreature = playerCreature;
this.enemyCreature = enemyCreature;
this.turn = 0;
this.log = [`A wild ${enemyCreature.name} appeared!`];
this.updateUI();
this.renderBattleLog();
document.getElementById('battle-overlay').classList.remove('hidden');
document.getElementById('battle-overlay').classList.add('visible');
}
updateUI() {
document.getElementById('enemy-name').textContent = this.enemyCreature.name;
document.getElementById('enemy-hp-bar').style.width = `${(this.enemyCreature.hp / this.enemyCreature.hpMax) * 100}%`;
document.getElementById('player-name').textContent = this.playerCreature.name;
document.getElementById('player-hp-bar').style.width = `${(this.playerCreature.hp / this.playerCreature.hpMax) * 100}%`;
}
renderBattleLog() {
const logDiv = document.getElementById('battle-log');
logDiv.innerHTML = this.log.map(msg => `<div>${msg}</div>`).join('');
logDiv.scrollTop = logDiv.scrollHeight;
}
processTurn(action) {
if (!this.active) return;
this.turn++;
// Player turn
if (action.type === 'attack') {
const ability = this.playerCreature.abilities[action.abilityIdx % this.playerCreature.abilities.length];
this.resolveMove(this.playerCreature, this.enemyCreature, ability);
} else if (action.type === 'item') {
this.useItem();
}
if (this.enemyCreature.hp <= 0) {
this.endBattle('victory');
return;
}
// Enemy turn (delay slightly for effect)
setTimeout(() => {
if (!this.active) return;
const enemyAbility = this.enemyCreature.abilities[0];
this.resolveMove(this.enemyCreature, this.playerCreature, enemyAbility);
if (this.playerCreature.hp <= 0) {
this.endBattle('defeat');
}
this.updateUI();
this.renderBattleLog();
}, 600);
}
resolveMove(attacker, defender, ability) {
const isCrit = Math.random() < 0.1;
const typeMultiplier = this.getTypeMultiplier(attacker.type, defender.type);
let damage = (0.4 * attacker.level * attacker.atk / defender.def * ability.power) + (Math.random() * 5);
damage *= (isCrit ? 1.5 : 1.0);
damage *= typeMultiplier;
const finalDmg = Math.floor(damage);
defender.takeDamage(finalDmg);
let msg = `${attacker.name} used ${ability.name}!`;
if (isCrit) msg += " (CRITICAL!)";
if (typeMultiplier > 1) msg += " (SUPER EFFECTIVE!)";
else if (typeMultiplier < 1) msg += " (NOT VERY EFFECTIVE...)";
this.log.push(`${msg} ${finalDmg} dmg.`);
if (defender.hp <= 0) this.log.push(`${defender.name} fainted!`);
playSound(isCrit ? 440 : 220, 'square', 0.1);
}
getTypeMultiplier(atkType, defType) {
if (atkType === 5) return 1.5;
if (defType === 5) return 0.5;
if (atkType === 6) return 0.5;
if (defType === 6) return 1.5;
if (atkType === 0 && defType === 1) return 2.0;
if (atkType === 1 && defType === 2) return 2.0;
if (atkType === 2 && defType === 0) return 2.0;
if (atkType === 3 && (defType === 0 || defType === 2)) return 2.0;
if (atkType === 4 && (defType === 3 || defType === 1)) return 2.0;
if (defType === 0 && atkType === 1) return 0.5;
if (defType === 1 && atkType === 2) return 0.5;
if (defType === 2 && atkType === 0) return 0.5;
if (defType === 0 && atkType === 3) return 0.5;
if (defType === 2 && atkType === 3) return 0.5;
if (defType === 3 && atkType === 4) return 0.5;
if (defType === 1 && atkType === 4) return 0.5;
return 1.0;
}
useItem() {
const creature = this.playerCreature;
creature.heal(30);
this.log.push(`${creature.name} used a Recovery Pulse! (+30 HP)`);
this.updateUI();
}
endBattle(result) {
this.active = false;
if (result === 'victory') {
this.playerCreature.gainExp(60);
uiManager.updateHUD(gameState);
uiManager.showDialogue(`${this.playerCreature.name} gained EXP!`);
} else {
uiManager.showDialogue("You were defeated...");
gameState.isPaused = true;
}
setTimeout(() => {
document.getElementById('battle-overlay').classList.add('hidden');
document.getElementById('battle-overlay').classList.remove('visible');
if (result === 'defeat') gameState.isPaused = false;
}, 2000);
}
draw(ctx, canvas) {
if (!this.active) return;
// Clear canvas for battle (done by gameLoop if we manage it well)
// Draw Background (simulated)
ctx.fillStyle = '#333';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw Sprites on canvas
// Enemy: Top-right
SpriteRenderer.drawCreature(ctx, this.enemyCreature, canvas.width * 0.7, canvas.height * 0.25, 60, true);
// Player: Bottom-left
SpriteRenderer.drawCreature(ctx, this.playerCreature, canvas.width * 0.3, canvas.height * 0.6, 60, false);
}
}
// ── class: UIManager ──
class UIManager {
constructor() {
this.hud = document.getElementById('hud');
this.overlay = document.getElementById('overlay');
this.dialog = document.getElementById('dialog');
}
showDialogue(text, color = '#fff') {
const box = document.getElementById('dialog-box');
box.textContent = text;
box.style.color = color;
this.dialog.style.display = 'flex';
}
hideDialogue() {
this.dialog.style.display = 'none';
}
updateHUD(gameState) {
const partyHtml = gameState.party.map(c => `
<div class="party-unit">
<span>${c.name} (Lv.${c.level})</span>
<div class="hp-bar-container">
<div class="hp-bar-fill" style="width: ${(c.hp/c.hpMax)*100}%; background: ${TYPE_COLORS[c.type]}"></div>
</div>
</div>
`).join('');
document.getElementById('party-list').innerHTML = partyHtml;
}
showMenu(menuId) {
const menu = document.getElementById(menuId);
menu.classList.add('visible');
}
hideMenu(menuId) {
const menu = document.getElementById(menuId);
menu.classList.remove('visible');
}
}
// ── const: gameState ──
const gameState = new GameState();
// ── let: worldManager ──
let worldManager;
// ── let: battleManager ──
let battleManager;
// ── let: uiManager ──
let uiManager;
// ── let: audioCtx ──
let audioCtx;
// ── let: masterVolume ──
let masterVolume = 0.5;
// ── function: initAudio ──
function initAudio() {
if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// ── function: playSound ──
function playSound(freq, type, duration, volume = 0.1) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(volume * masterVolume, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
// ── function: playNote ──
function playNote(freq, type, duration) {
playSound(freq, type, duration);
}
// ── const: CREATURE_LEVELS ──
const CREATURE_LEVELS = [
{ name: 'EMBERLIT', start: 1 },
{ name: 'FLAMEWISP', start: 15 },
{ name: 'PYROPHANTOM', start: 30 }
];
// ── const: SpriteRenderer ──
// Sprite Renderer - Simple geometric procedural sprites
const SpriteRenderer = {
drawCreature(ctx, creature, x, y, size, isEnemy = false) {
const typeColor = TYPE_COLORS[creature.type];
ctx.save();
ctx.translate(x, y);
if (isEnemy) ctx.scale(-1, 1);
// Body
ctx.fillStyle = typeColor;
ctx.shadowBlur = 15;
ctx.shadowColor = typeColor;
ctx.beginPath();
if (creature.type === 0) { // Cinder: Spiky
ctx.moveTo(size/2, 0);
ctx.lineTo(size, size);
ctx.lineTo(0, size);
ctx.closePath();
} else if (creature.type === 1) { // Umbra: Swirly
ctx.arc(size/2, size/2, size/2, 0, Math.PI * 2);
} else if (creature.type === 2) { // Bloom: Petals
for (let i = 0; i < 5; i++) {
ctx.rotate(Math.PI * 2 / 5);
ctx.ellipse(size/2, 0, size/3, size/5, 0, 0, Math.PI * 2);
}
} else { // Default: Square/Circle
ctx.arc(size/2, size/2, size/2, 0, Math.PI * 2);
}
ctx.fill();
// Eyes
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(size/3, size/3, size/6, 0, Math.PI * 2);
ctx.arc(2*size/3, size/3, size/6, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'black';
ctx.beginPath();
ctx.arc(size/3, size/3, size/12, 0, Math.PI * 2);
ctx.arc(2*size/3, size/3, size/12, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
};
// ── function: initializeCreatureAbilities ──
function initializeCreatureAbilities() {
// This is a helper to add more variety if I can't redefine the object
// But I'll just include them in the next large CREATURE_DATA definition.
}
// ── main ──
// Finalize the main block to include the loop and the setup
// I'll move all the initialization logic here.
function init() {
const canvas = document.getElementById('game-canvas');
canvas.width = GAME_CONSTANTS.VIEWPORT_W;
canvas.height = GAME_CONSTANTS.VIEWPORT_H;
uiManager = new UIManager();
worldManager = new WorldManager(canvas);
battleManager = new BattleManager();
// Start Game
document.getElementById('btn-new-game').addEventListener('click', startNewGame);
document.getElementById('btn-continue').addEventListener('click', continueGame);
document.getElementById('dialog-close').addEventListener('click', () => uiManager.hideDialogue());
document.getElementById('menu-close').addEventListener('click', () => uiManager.hideMenu('overlay'));
// Control Listeners
window.addEventListener('keydown', handleKeydown);
// Battle Controls
document.getElementById('cmd-attack').addEventListener('click', () => {
battleManager.processTurn({ type: 'attack', abilityIdx: 0 });
});
document.getElementById('cmd-skill').addEventListener('click', () => {
battleManager.processTurn({ type: 'attack', abilityIdx: 1 });
});
document.getElementById('cmd-item').addEventListener('click', () => {
battleManager.processTurn({ type: 'item' });
});
// Main Loop
requestAnimationFrame(gameLoop);
// Show Title
document.getElementById('title-screen').classList.remove('hidden');
}
function startNewGame() {
initAudio();
document.getElementById('title-screen').classList.add('hidden');
document.getElementById('overlay').classList.remove('hidden');
// Initial Party
const emberlit = new Creature('EMBERLIT', 5);
const voidclaw = new Creature('VOIDCLAW', 5);
gameState.addCreature(emberlit);
gameState.addCreature(voidclaw);
worldManager.initMap();
uiManager.updateHUD(gameState);
uiManager.showDialogue("Welcome to Essentia. Your journey begins in the Whispering Glade.");
document.getElementById('overlay').classList.add('hidden');
}
function continueGame() {
if (gameState.load()) {
initAudio();
document.getElementById('title-screen').classList.add('hidden');
document.getElementById('overlay').classList.remove('hidden');
uiManager.updateHUD(gameState);
document.getElementById('overlay').classList.add('hidden');
} else {
uiManager.showDialogue("No save file found.");
}
}
function handleKeydown(e) {
if (gameState.isPaused) return;
if (e.key === 'w' || e.key === 'ArrowUp') worldManager.movePlayer(0, -1);
if (e.key === 's' || e.key === 'ArrowDown') worldManager.movePlayer(0, 1);
if (e.key === 'a' || e.key === 'ArrowLeft') worldManager.movePlayer(-1, 0);
if (e.key === 'd' || e.key === 'ArrowRight') worldManager.movePlayer(1, 0);
if (e.key.toLowerCase() === 'm') {
gameState.isPaused = !gameState.isPaused;
if (gameState.isPaused) uiManager.showMenu('overlay');
else uiManager.hideMenu('overlay');
}
}
function gameLoop(time) {
const canvas = document.getElementById('game-canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (gameState.isPaused) {
// Show a dark overlay for pause
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0,0, canvas.width, canvas.height);
} else if (battleManager.active) {
battleManager.draw(ctx, canvas);
} else {
worldManager.update();
worldManager.draw(ctx);
}
requestAnimationFrame(gameLoop);
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"google/gemma-4-26b-a4b-qat","provider":"lmstudio","persona":"composer","sessionId":"99899f02-3f4a-47fc-ae49-69d2dcb9d07a","tokensIn":556710,"tokensOut":28060,"tokensTotal":584770,"turns":29,"toolCalls":28,"failedToolCalls":4,"timestamp":"2026-07-14T01:00:05.322Z"} -->