Creature Collection
Qwen 3.6 27b MTP · typescript

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
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 137,041 bytes · SHA-256 557a78da2e2b
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Aetheris: Echoes of the Prism</title>
<style>
/* ── css: layout ── */
:root {
--bg-deep: #0a0e1a;
--glass-bg: rgba(255,255,255,0.08);
--glass-border: rgba(255,255,255,0.15);
--glass-shadow: rgba(0,0,0,0.4);
--accent: #7b68ee;
--accent-glow: #9d8df1;
--text-primary: #e8e6f0;
--text-secondary: #a09cb5;
--hp-green: #4ade80;
--hp-yellow: #fbbf24;
--hp-red: #ef4444;
--pyra: #ff6b35;
--aqua: #3b82f6;
--terra: #84cc16;
--lux: #facc15;
--umbra: #7c3aed;
--void-type: #e040fb;
--prism: #ffffff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--bg-deep); font-family: 'Segoe UI', system-ui, sans-serif; color: var(--text-primary); }
#game-container { position: relative; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; }
#game-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; image-rendering: pixelated; z-index: 1; }
#ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
.screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; pointer-events: auto; }
.screen.active { display: flex; }
#title-screen { flex-direction: column; align-items: center; justify-content: center; background: radial-gradient(ellipse at center, #1a1040 0%, #0a0e1a 70%); z-index: 20; pointer-events: auto; }
#title-text { font-size: clamp(3rem, 8vw, 6rem); letter-spacing: 0.3em; color: var(--accent-glow); text-shadow: 0 0 40px rgba(123,104,238,0.5), 0 0 80px rgba(123,104,238,0.2); animation: titlePulse 3s ease-in-out infinite; }
#subtitle { font-size: clamp(0.9rem, 2vw, 1.4rem); color: var(--text-secondary); letter-spacing: 0.5em; margin-top: 0.5rem; text-transform: uppercase; }
@keyframes titlePulse {
0%, 100% { opacity: 0.85; transform: scale(1); }
50% { opacity: 1; transform: scale(1.02); }
}
.glass-btn { pointer-events: auto; margin-top: 2rem; padding: 0.9rem 3rem; font-size: 1.1rem; background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 12px; color: var(--text-primary); cursor: pointer; backdrop-filter: blur(10px); transition: all 0.2s; letter-spacing: 0.1em; }
.glass-btn:hover { background: rgba(123,104,238,0.25); border-color: var(--accent-glow); box-shadow: 0 0 20px rgba(123,104,238,0.3); transform: translateY(-2px); }
.glass-btn:active { transform: translateY(0); }
.hidden { display: none !important; }
#battle-screen { flex-direction: column; background: #0a0e1a; z-index: 15; pointer-events: auto; }
#battle-canvas { width: 100%; height: 100%; }
#battle-ui { position: absolute; bottom: 0; left: 0; right: 0; pointer-events: auto; z-index: 25; }
#overworld-ui { flex-direction: column; z-index: 12; pointer-events: none; }
#hud-top { display: flex; justify-content: space-between; padding: 12px 16px; background: linear-gradient(to bottom, rgba(10,14,26,0.8), transparent); pointer-events: auto; z-index: 13; position: relative; }
#zone-name { font-size: 1rem; color: var(--text-secondary); letter-spacing: 0.15em; text-transform: uppercase; }
.icon-btn { background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 8px; color: var(--text-primary); font-size: 1.3rem; padding: 6px 12px; cursor: pointer; backdrop-filter: blur(5px); }
#touch-controls { position: absolute; bottom: 20px; left: 0; right: 0; display: flex; justify-content: space-between; align-items: center; padding: 0 20px; pointer-events: auto; z-index: 14; }
#dpad { display: grid; grid-template-columns: repeat(3, 50px); grid-template-rows: repeat(2, 50px); gap: 4px; }
#dpad button { background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 8px; color: var(--text-primary); font-size: 1.2rem; cursor: pointer; backdrop-filter: blur(5px); }
.action-btn { width: 60px; height: 60px; border-radius: 50%; background: rgba(123,104,238,0.3); border: 2px solid var(--accent-glow); color: white; font-size: 1.2rem; font-weight: bold; cursor: pointer; }
#modal-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 50; backdrop-filter: blur(4px); pointer-events: auto; }
.modal { background: linear-gradient(135deg, rgba(20,15,40,0.95), rgba(10,14,26,0.98)); border: 1px solid var(--glass-border); border-radius: 16px; padding: 24px; max-width: 90vw; max-height: 85vh; overflow-y: auto; backdrop-filter: blur(20px); box-shadow: 0 20px 60px rgba(0,0,0,0.5); min-width: 300px; pointer-events: auto; }
.modal h2 { font-size: 1.4rem; margin-bottom: 16px; color: var(--accent-glow); letter-spacing: 0.1em; }
.hp-bar-container { width: 100%; height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; margin-top: 4px; }
.hp-bar-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; background: var(--hp-green); }
@keyframes floatUp { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-60px); } }
.float-text { position: absolute; font-weight: bold; pointer-events: none; animation: floatUp 1s ease-out forwards; text-shadow: 0 2px 4px rgba(0,0,0,0.5); z-index: 50; }
@keyframes shakeX { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
.shake { animation: shakeX 0.3s ease; }
@keyframes critFlash { 0% { filter: brightness(1); } 50% { filter: brightness(2.5); } 100% { filter: brightness(1); } }
.crit-flash { animation: critFlash 0.4s ease; }
#dialog-box { background: linear-gradient(to top, rgba(10,14,26,0.95), rgba(10,14,26,0.8)); border-top: 1px solid var(--glass-border); padding: 16px 20px; min-height: 80px; font-size: 0.95rem; line-height: 1.5; backdrop-filter: blur(10px); pointer-events: auto; }
#action-menu { display: flex; gap: 8px; padding: 12px; background: rgba(10,14,26,0.7); border-top: 1px solid var(--glass-border); flex-wrap: wrap; pointer-events: auto; }
.action-btn-choice { padding: 8px 16px; background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 8px; color: var(--text-primary); cursor: pointer; font-size: 0.9rem; transition: all 0.2s; }
.action-btn-choice:hover { background: rgba(123,104,238,0.25); border-color: var(--accent-glow); }
#ability-list { padding: 12px; display: grid; gap: 6px; max-height: 200px; overflow-y: auto; pointer-events: auto; }
.ability-btn { padding: 10px 14px; background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 8px; color: var(--text-primary); cursor: pointer; text-align: left; font-size: 0.85rem; transition: all 0.2s; }
.ability-btn:hover { background: rgba(123,104,238,0.25); border-color: var(--accent-glow); }
.ability-btn .ab-name { font-weight: bold; }
.ability-btn .ab-info { color: var(--text-secondary); font-size: 0.75rem; margin-top: 2px; }
#enemy-area, #player-area { padding: 10px 16px; pointer-events: auto; }
.creature-card { display: inline-block; background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 10px; padding: 8px 14px; margin: 4px; min-width: 150px; backdrop-filter: blur(5px); }
.creature-card .name { font-weight: bold; font-size: 0.9rem; }
.creature-card .level { color: var(--text-secondary); font-size: 0.75rem; }
#btn-menu { pointer-events: auto; }
@media (max-width: 600px) {
#touch-controls.hidden { display: none !important; }
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="ui-layer">
<div id="title-screen" class="screen active">
<h1 id="title-text">AETHERIS</h1>
<p id="subtitle">Echoes of the Prism</p>
<button id="btn-new" class="glass-btn">New Binding</button>
<button id="btn-continue" class="glass-btn hidden">Continue</button>
</div>
<div id="battle-screen" class="screen">
<canvas id="battle-canvas"></canvas>
<div id="battle-ui">
<div id="enemy-area"></div>
<div id="player-area"></div>
<div id="dialog-box"></div>
<div id="action-menu" class="hidden"></div>
<div id="ability-list" class="hidden"></div>
</div>
</div>
<div id="overworld-ui" class="screen">
<div id="hud-top">
<span id="zone-name"></span>
<button id="btn-menu" class="icon-btn">☰</button>
</div>
<div id="touch-controls" class="hidden">
<div id="dpad">
<button data-dir="up">▲</button>
<button data-dir="left">◄</button>
<button data-dir="right">►</button>
<button data-dir="down">▼</button>
</div>
<button id="btn-action" class="action-btn">A</button>
</div>
</div>
<div id="modal-overlay" class="hidden"></div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./state":"src/state.ts","./data/creatures":"src/data/creatures.ts","./data/zones":"src/data/zones.ts","./overworld":"src/overworld.ts","./battle":"src/battle.ts","./renderer":"src/renderer.ts","./ui":"src/ui.ts","./audio":"src/audio.ts"},"src/data/creatures.ts":{"../types":"src/types.ts"},"src/data/abilities.ts":{"../types":"src/types.ts"},"src/data/zones.ts":{"../types":"src/types.ts"},"src/state.ts":{"./types":"src/types.ts","./data/creatures":"src/data/creatures.ts","./data/zones":"src/data/zones.ts"},"src/battle.ts":{"./types":"src/types.ts","./data/abilities":"src/data/abilities.ts","./data/creatures":"src/data/creatures.ts","./data/zones":"src/data/zones.ts"},"src/overworld.ts":{"./types":"src/types.ts","./data/zones":"src/data/zones.ts","./data/creatures":"src/data/creatures.ts","./state":"src/state.ts"},"src/renderer.ts":{"./types":"src/types.ts","./data/creatures":"src/data/creatures.ts","./data/zones":"src/data/zones.ts","./overworld":"src/overworld.ts","./battle":"src/battle.ts"},"src/ui.ts":{"./types":"src/types.ts","./data/creatures":"src/data/creatures.ts","./data/abilities":"src/data/abilities.ts","./data/zones":"src/data/zones.ts","./overworld":"src/overworld.ts","./state":"src/state.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const state_1 = require("./state");
const creatures_1 = require("./data/creatures");
const zones_1 = require("./data/zones");
const overworld_1 = require("./overworld");
const battle_1 = require("./battle");
const renderer_1 = require("./renderer");
const ui = __importStar(require("./ui"));
const audio = __importStar(require("./audio"));
// === GAME STATE ===
let state = (0, state_1.createInitialState)();
let tutorialStep = -1; // -1 = no tutorial shown yet
let inTutorial = false;
let battleResolveCallback = null;
let captureItemUsed = '';
// === SCREEN MANAGEMENT ===
const titleScreen = document.getElementById('title-screen');
const battleScreen = document.getElementById('battle-screen');
const overworldUI = document.getElementById('overworld-ui');
function showScreen(name) {
state.screen = name;
titleScreen.classList.toggle('active', name === 'title');
battleScreen.classList.toggle('active', name === 'battle');
overworldUI.classList.toggle('active', name === 'overworld');
// Show/hide touch controls based on screen
const tc = document.getElementById('touch-controls');
if (tc) {
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
tc.classList.toggle('hidden', name !== 'overworld');
}
}
}
// === INITIALIZATION ===
function init() {
(0, renderer_1.resizeCanvas)();
window.addEventListener('resize', renderer_1.resizeCanvas);
// Check for save game
if ((0, state_1.hasSave)()) {
document.getElementById('btn-continue')?.classList.remove('hidden');
}
// Event listeners
document.getElementById('btn-new').onclick = startNewGame;
document.getElementById('btn-continue').onclick = continueGame;
document.getElementById('btn-menu').onclick = openMainMenu;
// Modal event delegation
document.getElementById('modal-overlay').addEventListener('click', handleModalClick);
// Keyboard input
window.addEventListener('keydown', handleKeyDown);
// Touch controls
setupTouchControls();
// Start render loop
requestAnimationFrame(gameLoop);
}
function startNewGame() {
audio.initAudio();
state = (0, state_1.createInitialState)();
tutorialStep = 0;
inTutorial = true;
showScreen('overworld');
ui.showModal(ui.showTutorialStep(0));
}
function continueGame() {
audio.initAudio();
const saved = (0, state_1.loadGame)();
if (saved) {
state = saved;
tutorialStep = -1;
inTutorial = false;
showScreen('overworld');
}
}
// === INPUT HANDLING ===
let moveCooldown = 0;
function handleKeyDown(e) {
const modalOpen = document.getElementById('modal-overlay').classList.contains('active');
if (state.screen === 'overworld' && !modalOpen) {
audio.initAudio();
const key = e.key.toLowerCase();
let dx = 0, dy = 0;
switch (key) {
case 'w':
case 'arrowup':
dy = -1;
break;
case 's':
case 'arrowdown':
dy = 1;
break;
case 'a':
case 'arrowleft':
dx = -1;
break;
case 'd':
case 'arrowright':
dx = 1;
break;
case 'escape':
openMainMenu();
return;
default: return;
}
e.preventDefault();
movePlayer(dx, dy);
}
else if (state.screen === 'title') {
audio.initAudio();
const key = e.key.toLowerCase();
if (key === 'enter' || key === ' ') {
// Start new game on Enter/Space at title screen
startNewGame();
}
}
else if (state.screen === 'battle') {
audio.initAudio();
const key = e.key.toLowerCase();
if (key === 'escape' || key === 'b') {
// In battle, B/Escape could show menu or confirm dialog advance
const bs = (0, battle_1.getBattleState)();
if (bs && !(0, battle_1.isAnimating)()) {
ui.hideActionMenu();
ui.hideAbilityList();
showBattleActions();
}
}
}
}
function setupTouchControls() {
const dpad = document.getElementById('dpad');
if (!dpad)
return;
// Show touch controls on mobile
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
dpad.classList.remove('hidden');
}
dpad.querySelectorAll('button').forEach(btn => {
const dir = btn.getAttribute('data-dir');
if (!dir)
return;
const handler = () => {
audio.initAudio();
let dx = 0, dy = 0;
switch (dir) {
case 'up':
dy = -1;
break;
case 'down':
dy = 1;
break;
case 'left':
dx = -1;
break;
case 'right':
dx = 1;
break;
}
movePlayer(dx, dy);
};
btn.addEventListener('click', handler);
btn.addEventListener('touchstart', (e) => { e.preventDefault(); handler(); });
});
const actionBtn = document.getElementById('btn-action');
if (actionBtn) {
actionBtn.addEventListener('click', () => {
audio.initAudio();
interactAtPosition();
});
}
}
function movePlayer(dx, dy) {
if (moveCooldown > 0)
return;
const nx = state.playerX + dx;
const ny = state.playerY + dy;
if (!(0, overworld_1.isWalkable)(state.currentZone, nx, ny))
return;
state.playerX = nx;
state.playerY = ny;
moveCooldown = 6; // frames between moves
audio.playStep();
// Check for interactions at new position
checkPositionInteractions();
}
function interactAtPosition() {
if (state.screen !== 'overworld')
return;
checkPositionInteractions();
}
function checkPositionInteractions() {
// Check shrine
if ((0, overworld_1.checkShrine)(state)) {
ui.showModal(ui.showShrineMenu(state));
return;
}
// Check zone exit
const nextZone = (0, overworld_1.checkZoneExit)(state);
if (nextZone) {
state.currentZone = nextZone;
state.playerX = overworld_1.MAP_W - 3;
state.playerY = overworld_1.MAP_H - 3;
ui.showDialog(`Entered ${zones_1.ZONES[nextZone].name}!`);
return;
}
// Check wild encounter
const encounter = (0, overworld_1.checkEncounter)(state);
if (encounter) {
startWildBattle(encounter);
}
}
// === BATTLE SYSTEM ===
function startWildBattle(encounter) {
if (!encounter || state.party.length === 0)
return;
showScreen('battle');
audio.playBattleStart();
const bs = (0, battle_1.startBattle)(state.party, encounter.enemies, !encounter.isBoss, state.currentZone);
ui.updateBattleCards(state);
ui.updateEnemyCards(bs.enemies);
ui.showDialog(bs.dialogText);
ui.hideActionMenu();
ui.hideAbilityList();
// Show battle action menu after a delay
setTimeout(() => {
if ((0, battle_1.isPlayerTurn)())
showBattleActions();
}, 500);
}
function showBattleActions() {
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
ui.showActionMenu([
{ label: '⚔ Attack', action: () => showAbilities() },
{ label: '🎒 Bag', action: () => useItemInBattle() },
{ label: '🔄 Switch', action: () => switchCreature() },
{ label: '🕸 Capture', action: () => attemptCapture() }
]);
}
function showAbilities() {
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
const actor = (0, battle_1.getCurrentActor)();
if (!actor)
return;
ui.showAbilityList(actor.abilities, (abilityId) => {
// Find the player index of current actor
const pIdx = bs.playerParty.indexOf(actor);
if (pIdx < 0)
return;
const msgs = (0, battle_1.executeMove)(pIdx, abilityId);
ui.showDialog(msgs.join(' '));
ui.updateBattleCards(state);
ui.updateEnemyCards(bs.enemies);
// Check battle end
setTimeout(() => {
const result = (0, battle_1.checkBattleEnd)();
if (result) {
handleBattleResult(result);
}
else {
(0, battle_1.advanceTurn)();
processNextTurn();
}
}, 800);
});
}
function useItemInBattle() {
// Use a healing item on active creature
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
const actor = (0, battle_1.getCurrentActor)();
if (!actor || actor.currentHp >= actor.maxHp) {
ui.showDialog('No items needed or available.');
return;
}
// Try prism elixir first, then aether draught
let itemId = '';
if ((0, state_1.hasItem)(state, 'prism-elixir'))
itemId = 'prism-elixir';
else if ((0, state_1.hasItem)(state, 'aether-draught'))
itemId = 'aether-draught';
if (!itemId) {
ui.showDialog('No healing items available!');
return;
}
(0, state_1.removeInventoryItem)(state, itemId);
const itemDef = Object.values(zones_1.ZONES).find(() => false) || {}; // dummy
actor.currentHp = Math.min(actor.maxHp, actor.currentHp + (itemId === 'prism-elixir' ? 999 : 50));
ui.showDialog(`Used ${itemId === 'prism-elixir' ? 'Prism Elixir' : 'Aether Draught'}! HP restored.`);
ui.updateBattleCards(state);
setTimeout(() => {
(0, battle_1.advanceTurn)();
processNextTurn();
}, 600);
}
function switchCreature() {
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
// Find next alive creature
let nextIdx = -1;
for (let i = 0; i < state.party.length; i++) {
if (state.party[i].currentHp > 0 && i !== bs.activePlayerIndex) {
nextIdx = i;
break;
}
}
if (nextIdx < 0) {
// Try any alive creature
for (let i = 0; i < state.party.length; i++) {
if (state.party[i].currentHp > 0) {
nextIdx = i;
break;
}
}
}
if (nextIdx >= 0) {
bs.activePlayerIndex = nextIdx;
const tmpl = (0, creatures_1.getCreatureTemplate)(state.party[nextIdx].templateId);
ui.showDialog(`Switched to ${tmpl.name}!`);
ui.updateBattleCards(state);
setTimeout(() => showBattleActions(), 500);
}
else {
ui.showDialog('No other creatures available!');
}
}
function attemptCapture() {
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
// Find best capture item
let itemId = '';
if ((0, state_1.hasItem)(state, 'prism-binder'))
itemId = 'prism-binder';
else if ((0, state_1.hasItem)(state, 'resonance-cage'))
itemId = 'resonance-cage';
else if ((0, state_1.hasItem)(state, 'aether-net'))
itemId = 'aether-net';
if (!itemId) {
ui.showDialog('No capture items available!');
return;
}
(0, state_1.removeInventoryItem)(state, itemId);
captureItemUsed = itemId;
(0, battle_1.startCapture)();
// Show capture mini-game
ui.hideActionMenu();
ui.hideAbilityList();
ui.showModal(ui.showCaptureMiniGame());
// Setup tap button
const tapBtn = document.querySelector('.capture-tap-btn');
if (tapBtn) {
let taps = 0;
const interval = setInterval(() => {
taps++;
const fill = document.getElementById('capture-fill');
if (fill)
fill.style.width = `${Math.min(taps * 8, 100)}%`;
}, 200);
// Auto-resolve after 3 seconds
setTimeout(() => {
clearInterval(interval);
const success = (0, battle_1.resolveCapture)(itemId);
ui.hideModal();
if (success) {
handleCaptureSuccess(bs, itemId);
}
else {
ui.showDialog('The creature broke free!');
audio.playDefeat();
setTimeout(() => showBattleActions(), 1000);
}
}, 3000);
}
}
function handleCaptureSuccess(bs, itemId) {
if (!bs)
return;
// Find weakest enemy to capture
const target = bs.enemies.reduce((best, e) => {
if (e.currentHp <= 0)
return best;
const ratio = e.currentHp / e.maxHp;
return (!best || ratio < (best.currentHp / best.maxHp)) ? e : best;
}, null);
if (!target)
return;
// Add to party
if (state.party.length < 6) {
state.party.push(target);
state.collectedCreatures.add(target.templateId);
ui.showDialog(`Captured ${(0, creatures_1.getCreatureTemplate)(target.templateId).name}!`);
audio.playCapture();
}
else {
// Party full - release weakest or store in reserve
const released = state.party.shift();
if (released) {
ui.showDialog(`Party full! Released ${(0, creatures_1.getCreatureTemplate)(released.templateId).name}. Captured ${(0, creatures_1.getCreatureTemplate)(target.templateId).name}!`);
}
state.party.push(target);
state.collectedCreatures.add(target.templateId);
audio.playCapture();
}
// Remove captured enemy from battle
const idx = bs.enemies.indexOf(target);
if (idx >= 0)
bs.enemies.splice(idx, 1);
// Check if all enemies gone
if (bs.enemies.length === 0 || bs.enemies.every(e => e.currentHp <= 0)) {
handleBattleResult('victory');
}
else {
(0, battle_1.advanceTurn)();
processNextTurn();
}
}
function handleBattleResult(result) {
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
(0, battle_1.resetCapture)();
if (result === 'victory') {
audio.playVictory();
// Check if it was a guardian battle
const zone = zones_1.ZONES[state.currentZone];
if (zone.guardianId && bs.enemies.some(e => e.templateId === zone.guardianId)) {
state.defeatedGuardians.push(state.currentZone);
audio.playGuardianDefeated();
// Award catalyst item
const catalysts = {
'ember_pass': 'pyre-shard',
'deep_trench': 'aqua-core',
'stoneheart_cavern': 'terra-gem',
'prism_spire': 'void-fragment'
};
if (catalysts[state.currentZone]) {
(0, state_1.addInventoryItem)(state, catalysts[state.currentZone]);
ui.showDialog(`Guardian defeated! Received ${catalysts[state.currentZone].replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}!`);
}
// Story progression
if (state.storyStage < 3) {
state.storyStage++;
}
// Check for post-game
if (state.defeatedGuardians.length >= 4 && !state.postGame) {
state.postGame = true;
ui.showDialog('All Anchors defeated! The Between awaits...');
}
}
else {
// Regular wild battle - EXP gain
let totalExpMsg = '';
for (const enemy of bs.enemies) {
const expGain = Math.floor(enemy.level * 3 + state.party[0].level);
for (const pc of state.party) {
if (pc.currentHp > 0) {
const msgs = (0, state_1.gainExp)(pc, expGain);
totalExpMsg += msgs.join(' ');
}
}
}
// Random item drops
if (Math.random() < 0.3)
(0, state_1.addInventoryItem)(state, 'aether-draught');
if (Math.random() < 0.15)
(0, state_1.addInventoryItem)(state, 'resonance-cage');
ui.showDialog(`Victory! ${totalExpMsg}`);
}
// Return to overworld after delay
setTimeout(() => {
showScreen('overworld');
ui.hideDialog();
ui.hideActionMenu();
ui.hideAbilityList();
(0, state_1.saveGame)(state);
}, 2000);
}
else {
// Defeat - revive at shrine with half HP
audio.playDefeat();
for (const c of state.party) {
c.currentHp = Math.floor(c.maxHp * 0.5);
if (c.status)
c.status = undefined;
}
ui.showDialog('Your creatures fainted... Revived at the last shrine.');
// Return to shrine position
const sPos = (0, overworld_1.checkShrine)(state) ? { x: state.playerX, y: state.playerY } : null;
if (!sPos) {
// Reset to zone center
state.playerX = Math.floor(overworld_1.MAP_W / 2);
state.playerY = Math.floor(overworld_1.MAP_H / 2);
}
setTimeout(() => {
showScreen('overworld');
ui.hideDialog();
ui.hideActionMenu();
ui.hideAbilityList();
(0, state_1.saveGame)(state);
}, 2000);
}
}
function processNextTurn() {
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
// Skip fainted actors in turn order
while (bs.currentTurnIndex < bs.turnOrder.length) {
const idx = bs.turnOrder[bs.currentTurnIndex];
let actor = null;
let isPlayer = false;
if (idx < bs.playerParty.length) {
actor = bs.playerParty[idx];
isPlayer = true;
}
else {
const eIdx = idx - bs.playerParty.length;
if (eIdx >= 0 && eIdx < bs.enemies.length) {
actor = bs.enemies[eIdx];
}
}
if (!actor || actor.currentHp <= 0) {
(0, battle_1.advanceTurn)();
continue;
}
// Check shock status - skip turn
if (actor.status?.effect === 'Shock' && Math.random() < 0.3) {
ui.showDialog(`${(0, creatures_1.getCreatureTemplate)(actor.templateId).name} is paralyzed and can't move!`);
(0, battle_1.advanceTurn)();
setTimeout(processNextTurn, 800);
return;
}
if (isPlayer) {
bs.activePlayerIndex = idx;
ui.updateBattleCards(state);
showBattleActions();
return;
}
else {
// Enemy turn - auto execute after delay
setTimeout(() => {
const msgs = (0, battle_1.executeEnemyTurn)();
ui.showDialog(msgs.join(' '));
ui.updateBattleCards(state);
ui.updateEnemyCards(bs.enemies);
setTimeout(() => {
const result = (0, battle_1.checkBattleEnd)();
if (result)
handleBattleResult(result);
else {
(0, battle_1.advanceTurn)();
processNextTurn();
}
}, 800);
}, 600);
return;
}
}
// All turns done - new round
recalcTurnOrderAndContinue();
}
function recalcTurnOrderAndContinue() {
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
// Process status effects for new round
const statusMsgs = [];
for (const c of [...bs.playerParty, ...bs.enemies]) {
if (!c.status || c.currentHp <= 0)
continue;
const name = (0, creatures_1.getCreatureTemplate)(c.templateId).name;
switch (c.status.effect) {
case 'Burn': {
const dmg = Math.floor(c.atk * 0.5);
c.currentHp -= dmg;
if (c.currentHp < 0)
c.currentHp = 0;
statusMsgs.push(`${name} is hurt by burn!`);
break;
}
case 'Regen': {
const heal = Math.floor(c.maxHp * 0.15);
c.currentHp = Math.min(c.maxHp, c.currentHp + heal);
statusMsgs.push(`${name} regenerates HP!`);
break;
}
}
c.status.turns--;
if (c.status.turns <= 0)
c.status = undefined;
}
if (statusMsgs.length > 0) {
ui.showDialog(statusMsgs.join(' '));
}
// Recalculate turn order
const all = [...bs.playerParty.map((_, i) => ({ idx: i, spd: bs.playerParty[i].spd })),
...bs.enemies.map((_, i) => ({ idx: bs.playerParty.length + i, spd: bs.enemies[i].spd }))];
const alive = all.filter(e => {
if (e.idx < bs.playerParty.length)
return bs.playerParty[e.idx].currentHp > 0;
return bs.enemies[e.idx - bs.playerParty.length].currentHp > 0;
});
alive.sort((a, b) => b.spd - a.spd);
bs.turnOrder = alive.map(e => e.idx);
bs.currentTurnIndex = 0;
setTimeout(processNextTurn, statusMsgs.length > 0 ? 1000 : 300);
}
// === MENU SYSTEM ===
function openMainMenu() {
if (state.screen !== 'overworld')
return;
ui.showModal(ui.showMainMenu(state));
}
function handleModalClick(e) {
const target = e.target;
// Use closest() to find the button with data attributes (handles clicks on child elements)
const menuBtn = target.closest('[data-menu]');
const shrineBtn = target.closest('[data-shrine]');
const tutBtn = target.closest('[data-tut]');
const starterBtn = target.closest('[data-starter]');
const buyBtn = target.closest('[data-buy]');
const transformBtn = target.closest('[data-tidx]');
const dataMenu = menuBtn ? menuBtn.getAttribute('data-menu') : null;
const dataShrine = shrineBtn ? shrineBtn.getAttribute('data-shrine') : null;
const dataTut = tutBtn ? tutBtn.getAttribute('data-tut') : null;
const dataStarter = starterBtn ? starterBtn.getAttribute('data-starter') : null;
const dataBuy = buyBtn ? buyBtn.getAttribute('data-buy') : null;
const dataTidx = transformBtn ? transformBtn.getAttribute('data-tidx') : null;
// Tutorial buttons
if (dataTut) {
switch (dataTut) {
case 'next':
tutorialStep++;
ui.showModal(ui.showTutorialStep(tutorialStep));
break;
case 'back':
tutorialStep--;
ui.showModal(ui.showTutorialStep(tutorialStep));
break;
case 'done':
inTutorial = false;
tutorialStep = -1;
// Give starter creature
ui.hideModal();
ui.showModal(ui.showStarterChoice());
break;
}
return;
}
// Starter choice
if (dataStarter && starterBtn) {
const creature = (0, state_1.createCreature)(dataStarter, 3);
state.party.push(creature);
state.collectedCreatures.add(dataStarter);
(0, state_1.addInventoryItem)(state, 'aether-net', 5);
(0, state_1.addInventoryItem)(state, 'resonance-cage', 2);
(0, state_1.addInventoryItem)(state, 'aether-draught', 3);
ui.hideModal();
audio.startAmbient();
return;
}
// Main menu buttons
if (dataMenu) {
switch (dataMenu) {
case 'team':
ui.showModal(ui.showTeamScreen(state));
break;
case 'bag':
ui.showModal(ui.showBagScreen(state));
break;
case 'encyclopedia':
ui.showModal(ui.showBestiaryScreen(state));
break;
case 'map':
ui.showModal(ui.showMapScreen(state));
break;
case 'save':
(0, state_1.saveGame)(state);
ui.hideModal();
ui.showDialog('Game saved!');
setTimeout(() => ui.hideDialog(), 1000);
break;
case 'close':
ui.hideModal();
break;
}
return;
}
// Shrine buttons
if (dataShrine) {
switch (dataShrine) {
case 'heal':
(0, overworld_1.healParty)(state.party);
ui.showDialog('Party fully healed!');
setTimeout(() => ui.hideModal(), 800);
break;
case 'save':
(0, state_1.saveGame)(state);
ui.showDialog('Game saved at shrine.');
setTimeout(() => ui.hideModal(), 800);
break;
case 'shop':
ui.showModal(ui.showShopScreen(state));
break;
case 'close':
ui.hideModal();
break;
}
return;
}
// Shop buy buttons
if (dataBuy) {
(0, state_1.addInventoryItem)(state, dataBuy);
const item = Object.values(zones_1.ZONES).find(() => false); // dummy to keep import
ui.showDialog(`Received ${dataBuy.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}!`);
setTimeout(() => {
ui.hideModal();
ui.showModal(ui.showShopScreen(state));
}, 500);
return;
}
// Transform button
if (dataTidx !== null) {
const idx = parseInt(dataTidx);
const creature = state.party[idx];
const tmpl = (0, creatures_1.getCreatureTemplate)(creature.templateId);
if (tmpl.catalystItem && (0, state_1.hasItem)(state, tmpl.catalystItem)) {
(0, state_1.removeInventoryItem)(state, tmpl.catalystItem);
// Simple transform - just update stats and abilities
creature.level += 5; // Boost level on transform
const newStats = (0, creatures_1.getCreatureTemplate)(tmpl.transformId);
if (newStats) {
creature.templateId = newStats.id;
creature.affinities = [...newStats.affinities];
creature.maxHp = Math.floor(newStats.baseStats.hp * (1 + creature.level * 0.4));
creature.atk = Math.floor(newStats.baseStats.atk * (1 + creature.level * 0.35));
creature.def = Math.floor(newStats.baseStats.def * (1 + creature.level * 0.35));
creature.spd = Math.floor(newStats.baseStats.spd * (1 + creature.level * 0.3));
creature.currentHp = creature.maxHp;
creature.abilities = newStats.abilities.map(aid => ({ abilityId: aid, pp: 25 }));
}
audio.playTransform();
ui.hideModal();
ui.showDialog(`${(0, creatures_1.getCreatureTemplate)(creature.templateId).name} transformed!`);
setTimeout(() => ui.hideDialog(), 1500);
}
else {
ui.showDialog(`Need ${tmpl.catalystItem?.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())} to transform.`);
}
return;
}
// Back button (generic)
if (target.classList.contains('back-btn')) {
ui.hideModal();
ui.showModal(ui.showMainMenu(state));
return;
}
// Click on overlay background (not inside modal) closes it
const modalContent = target.closest('.modal');
if (!modalContent && target === document.getElementById('modal-overlay')) {
ui.hideModal();
}
}
// === GAME LOOP ===
let lastTime = 0;
function gameLoop(timestamp) {
const dt = timestamp - lastTime;
lastTime = timestamp;
if (moveCooldown > 0)
moveCooldown--;
// Render based on current screen
if (state.screen === 'overworld' || state.screen === undefined) {
(0, renderer_1.renderOverworld)(state);
}
else if (state.screen === 'battle') {
(0, renderer_1.renderBattle)();
(0, renderer_1.updateBattleEffects)();
}
requestAnimationFrame(gameLoop);
}
// Start the game
init();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TYPE_CHART = exports.AFFINITY_COLORS = exports.AFFINITIES = void 0;
exports.getEffectiveness = getEffectiveness;
// === AFFINITY TYPES ===
exports.AFFINITIES = ['Pyra', 'Aqua', 'Terra', 'Lux', 'Umbra', 'Void', 'Prism'];
exports.AFFINITY_COLORS = {
Pyra: '#ff6b35', Aqua: '#3b82f6', Terra: '#84cc16',
Lux: '#facc15', Umbra: '#7c3aed', Void: '#e040fb', Prism: '#ffffff'
};
// Type chart: key beats value
exports.TYPE_CHART = {
Pyra: ['Aqua'], // Pyra > Aqua
Aqua: ['Terra'], // Aqua > Terra
Terra: ['Pyra'], // Terra > Pyra (cycle)
Lux: ['Pyra', 'Terra'], // Lux beats Pyra & Terra
Umbra: ['Lux', 'Aqua'], // Umbra beats Lux & Aqua
Void: ['Pyra', 'Aqua', 'Terra', 'Lux', 'Umbra', 'Prism'], // Void beats all slightly
Prism: [] // Prism resists everything, weak to Void only
};
function getEffectiveness(atkType, defType) {
if (defType === 'Prism' && atkType !== 'Void')
return 0.5; // Prism resists all except Void
if (atkType === 'Void')
return defType === 'Prism' ? 2.0 : 1.3; // Void beats all slightly, double vs Prism
const targets = exports.TYPE_CHART[atkType];
if (targets.includes(defType))
return 2.0;
return 1.0;
}
};
// ── module: src/data/creatures.ts ──
__mods["src/data/creatures.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CREATURES = void 0;
exports.getCreatureTemplate = getCreatureTemplate;
exports.CREATURES = {
emberling: {
id: 'emberling', name: 'Emberling', lore: 'A small spark-wisp born from geothermal vents. It curls into a warm ball when startled.',
affinities: ['Pyra'], baseStats: { hp: 45, atk: 38, def: 22, spd: 30 },
abilities: ['spark-bolt', 'ember-tail', 'flame-wave', 'inferno-crest'],
transformId: 'blazewing', transformLevel: 25, catalystItem: 'pyre-shard'
},
tidepup: {
id: 'tidepup', name: 'Tidepup', lore: 'A playful stream-dweller that shapes water into spinning shields. Known to guide lost travelers.',
affinities: ['Aqua'], baseStats: { hp: 50, atk: 28, def: 32, spd: 26 },
abilities: ['droplet-jab', 'tidal-swirl', 'frost-bite', 'deluge-curse'],
transformId: 'leviathan_pup', transformLevel: 25, catalystItem: 'aqua-core'
},
mossback: {
id: 'mossback', name: 'Mossback', lore: 'A gentle land-shaper with a shell of living crystal. Ancient roots grow beneath its feet.',
affinities: ['Terra'], baseStats: { hp: 58, atk: 24, def: 36, spd: 18 },
abilities: ['root-tangle', 'stone-throw', 'earth-pulse', 'crystal-bastion'],
transformId: 'titan_mossback', transformLevel: 25, catalystItem: 'terra-gem'
},
glowmoth: {
id: 'glowmoth', name: 'Glowmoth', lore: 'Drawn to luminous stones, its wings scatter prismatic dust that heals minor wounds.',
affinities: ['Lux'], baseStats: { hp: 35, atk: 26, def: 18, spd: 40 },
abilities: ['light-burst', 'glow-dust', 'radiant-wing'],
transformId: 'radiant_moth', transformLevel: 30, catalystItem: 'lux-orb'
},
shadowfin: {
id: 'shadowfin', name: 'Shadowfin', lore: 'A sleek predator that swims through darkness. Its silhouette shifts before striking.',
affinities: ['Umbra'], baseStats: { hp: 40, atk: 35, def: 20, spd: 38 },
abilities: ['dark-claw', 'shadow-step', 'void-gaze'],
transformId: 'umbral_stalker', transformLevel: 30, catalystItem: 'umbra-essence'
},
voidling: {
id: 'voidling', name: 'Voidling', lore: 'A fragment of the Between. Neither alive nor dead, it hums with unstable energy.',
affinities: ['Void'], baseStats: { hp: 38, atk: 32, def: 16, spd: 42 },
abilities: ['null-pulse', 'phase-shift', 'entropy-blast'],
transformId: 'entropy_wraith', transformLevel: 30, catalystItem: 'void-fragment'
},
prismite: {
id: 'prismite', name: 'Prismite', lore: 'A crystalline being that refracts all energies. Its core holds a sliver of the First Light.',
affinities: ['Prism'], baseStats: { hp: 52, atk: 20, def: 40, spd: 22 },
abilities: ['refract', 'prismatic-wall', 'light-prison'],
transformId: 'prism_guardian', transformLevel: 30, catalystItem: 'prism-heart'
},
cinderfox: {
id: 'cinderfox', name: 'Cinderfox', lore: 'A cunning fire-stalker with three tails of living flame. It leaves scorched pawprints.',
affinities: ['Pyra'], baseStats: { hp: 42, atk: 36, def: 20, spd: 35 },
abilities: ['ember-tail', 'flame-bite', 'fire-veil'],
transformId: 'inferno_fox', transformLevel: 30, catalystItem: 'pyre-shard'
},
aquadrake: {
id: 'aquadrake', name: 'Aquadrake', lore: 'A serpentine water-weaver that coils around ancient pillars. Its scales shimmer like deep-sea glass.',
affinities: ['Aqua'], baseStats: { hp: 48, atk: 30, def: 28, spd: 24 },
abilities: ['tidal-swirl', 'frost-bite', 'water-bind']
},
stonehorn: {
id: 'stonehorn', name: 'Stonehorn', lore: 'A mountain-born guardian with horns of solidified magma. It charges when the earth trembles.',
affinities: ['Terra'], baseStats: { hp: 62, atk: 30, def: 42, spd: 14 },
abilities: ['stone-throw', 'earth-pulse', 'horn-charge']
},
dawnwisp: {
id: 'dawnwisp', name: 'Dawnwisp', lore: 'A floating orb of morning light that sings in frequencies only crystals can hear.',
affinities: ['Lux'], baseStats: { hp: 32, atk: 30, def: 16, spd: 44 },
abilities: ['light-burst', 'sun-ray', 'heal-glow']
},
nightshade_cat: {
id: 'nightshade_cat', name: 'Nightshade Cat', lore: 'A feline shadow that walks between worlds. Its eyes glow with stolen starlight.',
affinities: ['Umbra'], baseStats: { hp: 38, atk: 34, def: 22, spd: 40 },
abilities: ['dark-claw', 'shadow-step', 'night-howl']
},
riftworm: {
id: 'riftworm', name: 'Riftworm', lore: 'A burrowing creature that feeds on dimensional tears. Its body phases in and out of reality.',
affinities: ['Void'], baseStats: { hp: 44, atk: 28, def: 18, spd: 36 },
abilities: ['null-pulse', 'phase-shift', 'rift-bite']
},
crystal_shard: {
id: 'crystal_shard', name: 'Crystal Shard', lore: 'A sentient fragment of the Prism Spire. It resonates with nearby energy flows.',
affinities: ['Prism'], baseStats: { hp: 46, atk: 22, def: 38, spd: 20 },
abilities: ['refract', 'crystal-spikes', 'prismatic-wall']
},
pyre_golem: {
id: 'pyre_golem', name: 'Pyre Golem', lore: 'The first Anchor. A mountain of living flame that guards the Ember Pass.',
affinities: ['Pyra'], baseStats: { hp: 120, atk: 55, def: 45, spd: 18 },
abilities: ['flame-wave', 'inferno-crest', 'magma-fall', 'pyre-roar']
},
abyssal_tide: {
id: 'abyssal_tide', name: 'Abyssal Tide', lore: 'The second Anchor. A colossal water serpent that commands the Deep Trench.',
affinities: ['Aqua'], baseStats: { hp: 130, atk: 50, def: 48, spd: 22 },
abilities: ['deluge-curse', 'tidal-swirl', 'abyssal-pressure', 'frost-bite']
},
earthwarden: {
id: 'earthwarden', name: 'Earthwarden', lore: 'The third Anchor. A crystalline colossus rooted in the Stoneheart Cavern.',
affinities: ['Terra'], baseStats: { hp: 140, atk: 45, def: 60, spd: 12 },
abilities: ['crystal-bastion', 'earth-pulse', 'stone-quake', 'horn-charge']
},
void_anchor: {
id: 'void_anchor', name: 'Void Anchor', lore: 'The final Anchor. A being of pure Between-energy, existing in all places and none.',
affinities: ['Void'], baseStats: { hp: 150, atk: 60, def: 40, spd: 38 },
abilities: ['entropy-blast', 'phase-shift', 'void-collapse', 'null-pulse']
},
// TRANSFORMED FORMS (9)
blazewing: {
id: 'blazewing', name: 'Blazewing', lore: 'Emberling ascended. Wings of pure flame, eyes like twin suns.',
affinities: ['Pyra'], baseStats: { hp: 72, atk: 58, def: 34, spd: 46 },
abilities: ['spark-bolt', 'flame-wave', 'inferno-crest', 'skyfire-descent'], isTransformed: true
},
leviathan_pup: {
id: 'leviathan_pup', name: 'Leviathan Pup', lore: 'Tidepup evolved. A miniature ocean titan with coral armor and tidal fury.',
affinities: ['Aqua'], baseStats: { hp: 78, atk: 46, def: 50, spd: 32 },
abilities: ['droplet-jab', 'tidal-swirl', 'deluge-curse', 'ocean-fury'], isTransformed: true
},
titan_mossback: {
id: 'titan_mossback', name: 'Titan Mossback', lore: 'Mossback ultimate form. A walking fortress with crystal spires.',
affinities: ['Terra'], baseStats: { hp: 90, atk: 42, def: 65, spd: 20 },
abilities: ['root-tangle', 'earth-pulse', 'crystal-bastion', 'titan-stomp'], isTransformed: true
},
radiant_moth: {
id: 'radiant_moth', name: 'Radiant Moth', lore: 'Glowmoth luminous ascension. Wings cast dawn-light across valleys.',
affinities: ['Lux'], baseStats: { hp: 50, atk: 42, def: 26, spd: 52 },
abilities: ['light-burst', 'glow-dust', 'radiant-wing', 'dawn-cascade'], isTransformed: true
},
umbral_stalker: {
id: 'umbral_stalker', name: 'Umbral Stalker', lore: 'Shadowfin dark evolution. A predator between heartbeats.',
affinities: ['Umbra'], baseStats: { hp: 56, atk: 52, def: 30, spd: 48 },
abilities: ['dark-claw', 'shadow-step', 'void-gaze', 'nightmare-rend'], isTransformed: true
},
entropy_wraith: {
id: 'entropy_wraith', name: 'Entropy Wraith', lore: 'Voidling terrifying form. A swirling vortex of unraveling reality.',
affinities: ['Void'], baseStats: { hp: 54, atk: 50, def: 24, spd: 54 },
abilities: ['null-pulse', 'phase-shift', 'entropy-blast', 'reality-tear'], isTransformed: true
},
prism_guardian: {
id: 'prism_guardian', name: 'Prism Guardian', lore: 'Prismite awakened. A towering crystal sentinel bending all light.',
affinities: ['Prism'], baseStats: { hp: 74, atk: 32, def: 58, spd: 28 },
abilities: ['refract', 'prismatic-wall', 'light-prison', 'prism-judgment'], isTransformed: true
},
inferno_fox: {
id: 'inferno_fox', name: 'Inferno Fox', lore: 'Cinderfox blazing form. Nine tails of white-hot flame.',
affinities: ['Pyra'], baseStats: { hp: 58, atk: 54, def: 28, spd: 46 },
abilities: ['ember-tail', 'flame-bite', 'fire-veil', 'nine-tail-fury'], isTransformed: true
},
deep_drake: {
id: 'deep_drake', name: 'Deep Drake', lore: 'Aquadrake oceanic evolution. A serpentine leviathan with abyssal pearl scales.',
affinities: ['Aqua'], baseStats: { hp: 70, atk: 48, def: 42, spd: 30 },
abilities: ['tidal-swirl', 'frost-bite', 'water-bind', 'deep-current'], isTransformed: true
}
};
function getCreatureTemplate(id) {
return exports.CREATURES[id];
}
};
// ── module: src/data/abilities.ts ──
__mods["src/data/abilities.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ABILITIES = void 0;
exports.getAbility = getAbility;
exports.ABILITIES = {
// === PYRA ABILITIES (8) ===
'spark-bolt': { id: 'spark-bolt', name: 'Spark Bolt', power: 40, accuracy: 100, affinity: 'Pyra', pp: 25, maxPp: 25, target: 'single', effect: 'none' },
'ember-tail': { id: 'ember-tail', name: 'Ember Tail', power: 35, accuracy: 95, affinity: 'Pyra', pp: 20, maxPp: 20, target: 'single', effect: 'burn', effectChance: 30 },
'flame-wave': { id: 'flame-wave', name: 'Flame Wave', power: 65, accuracy: 90, affinity: 'Pyra', pp: 15, maxPp: 15, target: 'single', effect: 'burn', effectChance: 20 },
'inferno-crest': { id: 'Inferno Crest', name: 'Inferno Crest', power: 90, accuracy: 80, affinity: 'Pyra', pp: 10, maxPp: 10, target: 'single', effect: 'burn', effectChance: 40 },
'flame-bite': { id: 'flame-bite', name: 'Flame Bite', power: 50, accuracy: 95, affinity: 'Pyra', pp: 20, maxPp: 20, target: 'single', effect: 'burn', effectChance: 25 },
'fire-veil': { id: 'fire-veil', name: 'Fire Veil', power: 0, accuracy: 100, affinity: 'Pyra', pp: 30, maxPp: 30, target: 'self', effect: 'atk-up' },
'magma-fall': { id: 'magma-fall', name: 'Magma Fall', power: 75, accuracy: 85, affinity: 'Pyra', pp: 12, maxPp: 12, target: 'single', effect: 'burn', effectChance: 35 },
'pyre-roar': { id: 'pyre-roar', name: 'Pyre Roar', power: 0, accuracy: 100, affinity: 'Pyra', pp: 20, maxPp: 20, target: 'self', effect: 'speed-boost' },
'skyfire-descent': { id: 'skyfire-descent', name: 'Skyfire Descent', power: 100, accuracy: 75, affinity: 'Pyra', pp: 8, maxPp: 8, target: 'single', effect: 'burn', effectChance: 50 },
'nine-tail-fury': { id: 'nine-tail-fury', name: 'Nine-Tail Fury', power: 45, accuracy: 90, affinity: 'Pyra', pp: 12, maxPp: 12, target: 'single', effect: 'multi-hit' },
// === AQUA ABILITIES (8) ===
'droplet-jab': { id: 'droplet-jab', name: 'Droplet Jab', power: 35, accuracy: 100, affinity: 'Aqua', pp: 25, maxPp: 25, target: 'single', effect: 'none' },
'tidal-swirl': { id: 'tidal-swirl', name: 'Tidal Swirl', power: 60, accuracy: 90, affinity: 'Aqua', pp: 18, maxPp: 18, target: 'single', effect: 'frost', effectChance: 25 },
'frost-bite': { id: 'frost-bite', name: 'Frost Bite', power: 45, accuracy: 95, affinity: 'Aqua', pp: 20, maxPp: 20, target: 'single', effect: 'frost', effectChance: 35 },
'deluge-curse': { id: 'deluge-curse', name: 'Deluge Curse', power: 80, accuracy: 85, affinity: 'Aqua', pp: 12, maxPp: 12, target: 'single', effect: 'shock', effectChance: 20 },
'water-bind': { id: 'water-bind', name: 'Water Bind', power: 40, accuracy: 90, affinity: 'Aqua', pp: 20, maxPp: 20, target: 'single', effect: 'frost', effectChance: 30 },
'abyssal-pressure': { id: 'abyssal-pressure', name: 'Abyssal Pressure', power: 70, accuracy: 85, affinity: 'Aqua', pp: 14, maxPp: 14, target: 'single', effect: 'def-down' },
'ocean-fury': { id: 'ocean-fury', name: 'Ocean Fury', power: 95, accuracy: 80, affinity: 'Aqua', pp: 10, maxPp: 10, target: 'single', effect: 'frost', effectChance: 40 },
'deep-current': { id: 'deep-current', name: 'Deep Current', power: 55, accuracy: 95, affinity: 'Aqua', pp: 16, maxPp: 16, target: 'single', effect: 'drain' },
// === TERRA ABILITIES (8) ===
'root-tangle': { id: 'root-tangle', name: 'Root Tangle', power: 35, accuracy: 90, affinity: 'Terra', pp: 22, maxPp: 22, target: 'single', effect: 'frost', effectChance: 20 },
'stone-throw': { id: 'stone-throw', name: 'Stone Throw', power: 50, accuracy: 95, affinity: 'Terra', pp: 20, maxPp: 20, target: 'single', effect: 'none' },
'earth-pulse': { id: 'earth-pulse', name: 'Earth Pulse', power: 65, accuracy: 85, affinity: 'Terra', pp: 15, maxPp: 15, target: 'all-enemies', effect: 'none' },
'crystal-bastion': { id: 'Crystal Bastion', name: 'Crystal Bastion', power: 0, accuracy: 100, affinity: 'Terra', pp: 25, maxPp: 25, target: 'self', effect: 'barrier' },
'horn-charge': { id: 'horn-charge', name: 'Horn Charge', power: 70, accuracy: 90, affinity: 'Terra', pp: 14, maxPp: 14, target: 'single', effect: 'crit-boost' },
'stone-quake': { id: 'stone-quake', name: 'Stone Quake', power: 85, accuracy: 80, affinity: 'Terra', pp: 12, maxPp: 12, target: 'all-enemies', effect: 'none' },
'titan-stomp': { id: 'titan-stomp', name: 'Titan Stomp', power: 95, accuracy: 80, affinity: 'Terra', pp: 10, maxPp: 10, target: 'all-enemies', effect: 'def-down' },
// === LUX ABILITIES (7) ===
'light-burst': { id: 'light-burst', name: 'Light Burst', power: 45, accuracy: 95, affinity: 'Lux', pp: 20, maxPp: 20, target: 'single', effect: 'none' },
'glow-dust': { id: 'glow-dust', name: 'Glow Dust', power: 30, accuracy: 100, affinity: 'Lux', pp: 25, maxPp: 25, target: 'self', effect: 'regen' },
'radiant-wing': { id: 'radiant-wing', name: 'Radiant Wing', power: 60, accuracy: 90, affinity: 'Lux', pp: 18, maxPp: 18, target: 'single', effect: 'speed-boost' },
'sun-ray': { id: 'sun-ray', name: 'Sun Ray', power: 75, accuracy: 85, affinity: 'Lux', pp: 14, maxPp: 14, target: 'single', effect: 'burn', effectChance: 20 },
'heal-glow': { id: 'heal-glow', name: 'Heal Glow', power: 0, accuracy: 100, affinity: 'Lux', pp: 30, maxPp: 30, target: 'self', effect: 'heal' },
'dawn-cascade': { id: 'dawn-cascade', name: 'Dawn Cascade', power: 85, accuracy: 80, affinity: 'Lux', pp: 12, maxPp: 12, target: 'all-enemies', effect: 'none' },
// === UMBRA ABILITIES (7) ===
'dark-claw': { id: 'dark-claw', name: 'Dark Claw', power: 50, accuracy: 95, affinity: 'Umbra', pp: 20, maxPp: 20, target: 'single', effect: 'none' },
'shadow-step': { id: 'shadow-step', name: 'Shadow Step', power: 40, accuracy: 100, affinity: 'Umbra', pp: 25, maxPp: 25, target: 'self', effect: 'speed-boost' },
'void-gaze': { id: 'void-gaze', name: 'Void Gaze', power: 55, accuracy: 85, affinity: 'Umbra', pp: 16, maxPp: 16, target: 'single', effect: 'shock', effectChance: 30 },
'night-howl': { id: 'night-howl', name: 'Night Howl', power: 0, accuracy: 100, affinity: 'Umbra', pp: 25, maxPp: 25, target: 'self', effect: 'atk-up' },
'nightmare-rend': { id: 'nightmare-rend', name: 'Nightmare Rend', power: 90, accuracy: 80, affinity: 'Umbra', pp: 10, maxPp: 10, target: 'single', effect: 'shock', effectChance: 40 },
// === VOID ABILITIES (7) ===
'null-pulse': { id: 'null-pulse', name: 'Null Pulse', power: 45, accuracy: 90, affinity: 'Void', pp: 20, maxPp: 20, target: 'single', effect: 'none' },
'phase-shift': { id: 'phase-shift', name: 'Phase Shift', power: 30, accuracy: 100, affinity: 'Void', pp: 25, maxPp: 25, target: 'self', effect: 'barrier' },
'entropy-blast': { id: 'entropy-blast', name: 'Entropy Blast', power: 75, accuracy: 85, affinity: 'Void', pp: 14, maxPp: 14, target: 'single', effect: 'def-down' },
'rift-bite': { id: 'rift-bite', name: 'Rift Bite', power: 50, accuracy: 90, affinity: 'Void', pp: 20, maxPp: 20, target: 'single', effect: 'drain' },
'void-collapse': { id: 'void-collapse', name: 'Void Collapse', power: 100, accuracy: 75, affinity: 'Void', pp: 8, maxPp: 8, target: 'all-enemies', effect: 'none' },
'reality-tear': { id: 'reality-tear', name: 'Reality Tear', power: 80, accuracy: 80, affinity: 'Void', pp: 12, maxPp: 12, target: 'single', effect: 'crit-boost' },
// === PRISM ABILITIES (5) ===
'refract': { id: 'refract', name: 'Refract', power: 40, accuracy: 95, affinity: 'Prism', pp: 22, maxPp: 22, target: 'single', effect: 'none' },
'prismatic-wall': { id: 'prismatic-wall', name: 'Prismatic Wall', power: 0, accuracy: 100, affinity: 'Prism', pp: 30, maxPp: 30, target: 'self', effect: 'barrier' },
'light-prison': { id: 'light-prison', name: 'Light Prison', power: 55, accuracy: 85, affinity: 'Prism', pp: 16, maxPp: 16, target: 'single', effect: 'frost', effectChance: 30 },
'crystal-spikes': { id: 'crystal-spikes', name: 'Crystal Spikes', power: 50, accuracy: 90, affinity: 'Prism', pp: 18, maxPp: 18, target: 'single', effect: 'none' },
'prism-judgment': { id: 'prism-judgment', name: 'Prism Judgment', power: 95, accuracy: 80, affinity: 'Prism', pp: 10, maxPp: 10, target: 'all-enemies', effect: 'crit-boost' },
};
function getAbility(id) {
return exports.ABILITIES[id];
}
};
// ── module: src/data/zones.ts ──
__mods["src/data/zones.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.STORY_STAGES = exports.ITEMS = exports.ZONES = void 0;
exports.ZONES = {
ember_pass: {
id: 'ember_pass', name: 'Ember Pass',
gradientStart: '#1a0505', gradientEnd: '#3d1200',
particleColor: '#ff6b35', encounterRate: 0.18,
storyText: 'The Ember Pass glows with ancient heat. Crystalline formations pulse like heartbeats along the canyon walls.',
guardianId: 'pyre_golem', guardianLevel: 12,
wildCreatures: [
{ templateId: 'emberling', minLvl: 3, maxLvl: 7 },
{ templateId: 'cinderfox', minLvl: 4, maxLvl: 8 }
]
},
deep_trench: {
id: 'deep_trench', name: 'Deep Trench',
gradientStart: '#050a1a', gradientEnd: '#0a2040',
particleColor: '#3b82f6', encounterRate: 0.2,
storyText: 'Below the surface, bioluminescent currents flow through obsidian canyons. The water here remembers everything.',
guardianId: 'abyssal_tide', guardianLevel: 18,
unlockedByZone: 'ember_pass',
wildCreatures: [
{ templateId: 'tidepup', minLvl: 6, maxLvl: 12 },
{ templateId: 'aquadrake', minLvl: 7, maxLvl: 13 },
{ templateId: 'shadowfin', minLvl: 8, maxLvl: 14 }
]
},
stoneheart_cavern: {
id: 'stoneheart_cavern', name: 'Stoneheart Cavern',
gradientStart: '#0a1505', gradientEnd: '#1a3010',
particleColor: '#84cc16', encounterRate: 0.22,
storyText: 'The cavern walls breathe with slow crystalline growths. Ancient roots weave through veins of raw aether.',
guardianId: 'earthwarden', guardianLevel: 25,
unlockedByZone: 'deep_trench',
wildCreatures: [
{ templateId: 'mossback', minLvl: 10, maxLvl: 16 },
{ templateId: 'stonehorn', minLvl: 11, maxLvl: 17 },
{ templateId: 'glowmoth', minLvl: 9, maxLvl: 15 }
]
},
prism_spire: {
id: 'prism_spire', name: 'Prism Spire',
gradientStart: '#0f0a20', gradientEnd: '#201040',
particleColor: '#ffffff', encounterRate: 0.25,
storyText: 'The Spire pierces the sky with refracted light. Here, all affinities converge into pure crystalline energy.',
guardianId: 'void_anchor', guardianLevel: 32,
unlockedByZone: 'stoneheart_cavern',
wildCreatures: [
{ templateId: 'prismite', minLvl: 14, maxLvl: 20 },
{ templateId: 'crystal_shard', minLvl: 15, maxLvl: 21 },
{ templateId: 'dawnwisp', minLvl: 13, maxLvl: 19 },
{ templateId: 'voidling', minLvl: 16, maxLvl: 22 }
]
},
shadow_veil: {
id: 'shadow_veil', name: 'Shadow Veil',
gradientStart: '#0a0515', gradientEnd: '#150a30',
particleColor: '#7c3aed', encounterRate: 0.28,
storyText: 'A realm between realms. Shadows move with purpose here, and the air tastes of forgotten memories.',
guardianId: 'void_anchor', guardianLevel: 40,
unlockedByZone: 'prism_spire',
wildCreatures: [
{ templateId: 'nightshade_cat', minLvl: 18, maxLvl: 26 },
{ templateId: 'shadowfin', minLvl: 19, maxLvl: 27 },
{ templateId: 'riftworm', minLvl: 20, maxLvl: 28 },
{ templateId: 'voidling', minLvl: 21, maxLvl: 29 }
]
},
the_between: {
id: 'the_between', name: 'The Between',
gradientStart: '#050508', gradientEnd: '#100a18',
particleColor: '#e040fb', encounterRate: 0.3,
storyText: 'Beyond the anchors lies the raw fabric of Aetheris. Reality thins here, and ancient energies swirl unchecked.',
unlockedByZone: 'shadow_veil',
wildCreatures: [
{ templateId: 'voidling', minLvl: 25, maxLvl: 35 },
{ templateId: 'riftworm', minLvl: 26, maxLvl: 36 },
{ templateId: 'prismite', minLvl: 24, maxLvl: 34 },
{ templateId: 'nightshade_cat', minLvl: 27, maxLvl: 37 }
]
}
};
exports.ITEMS = {
// Capture items (3 tiers)
'aether-net': { id: 'aether-net', name: 'Aether Net', type: 'capture', description: 'Basic capture device. Low success rate.', capturePower: 1 },
'resonance-cage': { id: 'resonance-cage', name: 'Resonance Cage', type: 'capture', description: 'Mid-tier capture tool with harmonic dampeners.', capturePower: 2 },
'prism-binder': { id: 'prism-binder', name: 'Prism Binder', type: 'capture', description: 'Premium capture device. High success rate.', capturePower: 3 },
// Consumables
'aether-draught': { id: 'aether-draught', name: 'Aether Draught', type: 'consumable', description: 'Restores 50 HP to a creature.', healAmount: 50 },
'prism-elixir': { id: 'prism-elixir', name: 'Prism Elixir', type: 'consumable', description: 'Fully restores a creatures HP.', healAmount: 999 },
// Catalysts for transformation
'pyre-shard': { id: 'pyre-shard', name: 'Pyre Shard', type: 'catalyst', description: 'A fragment of eternal flame. Used to transform Pyra creatures.' },
'aqua-core': { id: 'aqua-core', name: 'Aqua Core', type: 'catalyst', description: 'Condensed ocean energy. Transforms Aqua creatures.' },
'terra-gem': { id: 'terra-gem', name: 'Terra Gem', type: 'catalyst', description: 'Living crystal from the deep earth. Transforms Terra creatures.' },
'lux-orb': { id: 'lux-orb', name: 'Lux Orb', type: 'catalyst', description: 'Pure dawn-light condensed into form. Transforms Lux creatures.' },
'umbra-essence': { id: 'umbra-essence', name: 'Umbra Essence', type: 'catalyst', description: 'Distilled shadow-stuff. Transforms Umbra creatures.' },
'void-fragment': { id: 'void-fragment', name: 'Void Fragment', type: 'catalyst', description: 'A piece of the Between itself. Transforms Void creatures.' },
'prism-heart': { id: 'prism-heart', name: 'Prism Heart', type: 'catalyst', description: 'The core of a Prism Guardian. Transforms Prism creatures.' },
// Field tools
'echo-compass': { id: 'echo-compass', name: 'Echo Compass', type: 'tool', description: 'Reveals hidden paths on the overworld map.' }
};
exports.STORY_STAGES = [
{ text: 'The Aether flows thin. The Anchors stir in their slumber, and the world trembles.', color: '#ff6b35' },
{ text: 'You have broken the first seal. Deeper currents call from below.', color: '#3b82f6' },
{ text: 'The earth remembers what was lost. Ancient roots awaken in response to your journey.', color: '#84cc16' },
{ text: 'All paths converge at the Spire. The final Anchor awaits beyond the veil of shadow.', color: '#e040fb' }
];
};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCreature = createCreature;
exports.createInitialState = createInitialState;
exports.addInventoryItem = addInventoryItem;
exports.removeInventoryItem = removeInventoryItem;
exports.hasItem = hasItem;
exports.saveGame = saveGame;
exports.loadGame = loadGame;
exports.hasSave = hasSave;
exports.gainExp = gainExp;
exports.canTransform = canTransform;
exports.transformCreature = transformCreature;
const creatures_1 = require("./data/creatures");
function createCreature(templateId, level) {
const tmpl = (0, creatures_1.getCreatureTemplate)(templateId);
if (!tmpl)
throw new Error(`Unknown creature template: ${templateId}`);
// Stat growth formula: base * (1 + level * 0.5) with some variance
const hp = Math.floor(tmpl.baseStats.hp * (1 + level * 0.4));
const atk = Math.floor(tmpl.baseStats.atk * (1 + level * 0.35));
const def = Math.floor(tmpl.baseStats.def * (1 + level * 0.35));
const spd = Math.floor(tmpl.baseStats.spd * (1 + level * 0.3));
// EXP curve: level^2 * 8
const expToNext = Math.floor(Math.pow(level + 1, 2) * 8);
return {
templateId,
level,
currentHp: hp,
maxHp: hp,
atk, def, spd,
affinities: [...tmpl.affinities],
abilities: tmpl.abilities.map(aid => ({ abilityId: aid, pp: 25 })), // default PP
exp: 0,
expToNext
};
}
function createInitialState() {
return {
screen: 'title',
playerX: 4, playerY: 4,
currentZone: 'ember_pass',
party: [],
inventory: {
'aether-net': 5,
'resonance-cage': 2,
'aether-draught': 3
},
unlockedZones: ['ember_pass'],
defeatedGuardians: [],
collectedCreatures: new Set(),
storyStage: 0,
postGame: false,
shrineVisited: null
};
}
function addInventoryItem(state, itemId, count = 1) {
state.inventory[itemId] = (state.inventory[itemId] || 0) + count;
}
function removeInventoryItem(state, itemId, count = 1) {
if ((state.inventory[itemId] || 0) >= count) {
state.inventory[itemId] -= count;
if (state.inventory[itemId] <= 0)
delete state.inventory[itemId];
return true;
}
return false;
}
function hasItem(state, itemId) {
return (state.inventory[itemId] || 0) > 0;
}
// Save/Load
const SAVE_KEY = 'aetheris_save';
function saveGame(state) {
const data = JSON.stringify({
...state,
collectedCreatures: Array.from(state.collectedCreatures)
});
localStorage.setItem(SAVE_KEY, data);
}
function loadGame() {
try {
const raw = localStorage.getItem(SAVE_KEY);
if (!raw)
return null;
const parsed = JSON.parse(raw);
parsed.collectedCreatures = new Set(parsed.collectedCreatures);
parsed.screen = 'overworld';
return parsed;
}
catch {
return null;
}
}
function hasSave() {
return !!localStorage.getItem(SAVE_KEY);
}
// EXP and leveling
function gainExp(creature, amount) {
const messages = [];
creature.exp += amount;
while (creature.exp >= creature.expToNext && creature.level < 50) {
creature.exp -= creature.expToNext;
creature.level++;
// Stat growth on level up
const tmpl = (0, creatures_1.getCreatureTemplate)(creature.templateId);
creature.maxHp = Math.floor(tmpl.baseStats.hp * (1 + creature.level * 0.4));
creature.atk = Math.floor(tmpl.baseStats.atk * (1 + creature.level * 0.35));
creature.def = Math.floor(tmpl.baseStats.def * (1 + creature.level * 0.35));
creature.spd = Math.floor(tmpl.baseStats.spd * (1 + creature.level * 0.3));
creature.currentHp = creature.maxHp; // Full heal on level up
creature.expToNext = Math.floor(Math.pow(creature.level + 1, 2) * 8);
messages.push(`${tmpl.name} reached Level ${creature.level}!`);
// Check for new abilities at certain levels
const newAbilities = tmpl.abilities.filter(aid => {
return !creature.abilities.some(a => a.abilityId === aid);
});
if (newAbilities.length > 0) {
creature.abilities.push(...newAbilities.map(aid => ({ abilityId: aid, pp: 25 })));
messages.push(`Learned new ability!`);
}
}
return messages;
}
// Check if a creature can transform
function canTransform(creature) {
const tmpl = (0, creatures_1.getCreatureTemplate)(creature.templateId);
if (!tmpl.transformId || !tmpl.transformLevel)
return false;
return creature.level >= tmpl.transformLevel && !(0, creatures_1.getCreatureTemplate)(tmpl.transformId).isTransformed === false;
}
function transformCreature(creature, catalystId) {
const tmpl = (0, creatures_1.getCreatureTemplate)(creature.templateId);
if (!tmpl.transformId || !tmpl.transformLevel)
return false;
if (creature.level < tmpl.transformLevel)
return false;
if (tmpl.catalystItem !== catalystId)
return false;
// Transform: replace template, keep level and exp
const newTmpl = (0, creatures_1.getCreatureTemplate)(tmpl.transformId);
creature.templateId = newTmpl.id;
creature.affinities = [...newTmpl.affinities];
// Recalculate stats with new base
creature.maxHp = Math.floor(newTmpl.baseStats.hp * (1 + creature.level * 0.4));
creature.atk = Math.floor(newTmpl.baseStats.atk * (1 + creature.level * 0.35));
creature.def = Math.floor(newTmpl.baseStats.def * (1 + creature.level * 0.35));
creature.spd = Math.floor(newTmpl.baseStats.spd * (1 + creature.level * 0.3));
creature.currentHp = creature.maxHp;
// Update abilities to new template's set
creature.abilities = newTmpl.abilities.map(aid => ({ abilityId: aid, pp: 25 }));
return true;
}
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.toggleMute = toggleMute;
exports.isMuted = isMuted;
exports.playStep = playStep;
exports.playBattleStart = playBattleStart;
exports.playHit = playHit;
exports.playCrit = playCrit;
exports.playCapture = playCapture;
exports.playLevelUp = playLevelUp;
exports.playVictory = playVictory;
exports.playDefeat = playDefeat;
exports.playMenuSelect = playMenuSelect;
exports.playTransform = playTransform;
exports.playGuardianDefeated = playGuardianDefeated;
exports.startAmbient = startAmbient;
exports.stopAmbient = stopAmbient;
// Simple chiptune/ambient audio using Web Audio API
let ctx = null;
let masterGain = null;
let muted = false;
function initAudio() {
if (ctx)
return;
ctx = new AudioContext();
masterGain = ctx.createGain();
masterGain.gain.value = 0.3;
masterGain.connect(ctx.destination);
}
function toggleMute() {
muted = !muted;
if (masterGain)
masterGain.gain.value = muted ? 0 : 0.3;
return muted;
}
function isMuted() { return muted; }
function playTone(freq, duration, type = 'square', vol = 0.15) {
if (!ctx || !masterGain)
return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.value = freq;
gain.gain.setValueAtTime(vol, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(ctx.currentTime + duration);
}
function playStep() {
playTone(200 + Math.random() * 100, 0.05, 'sine', 0.05);
}
function playBattleStart() {
const notes = [262, 330, 392, 523];
notes.forEach((n, i) => setTimeout(() => playTone(n, 0.15, 'square', 0.1), i * 80));
}
function playHit() {
playTone(150, 0.1, 'sawtooth', 0.12);
}
function playCrit() {
playTone(400, 0.05, 'square', 0.15);
setTimeout(() => playTone(600, 0.1, 'square', 0.15), 50);
}
function playCapture() {
const notes = [392, 440, 523, 659];
notes.forEach((n, i) => setTimeout(() => playTone(n, 0.2, 'sine', 0.1), i * 100));
}
function playLevelUp() {
const notes = [262, 330, 392, 523, 659];
notes.forEach((n, i) => setTimeout(() => playTone(n, 0.15, 'square', 0.1), i * 80));
}
function playVictory() {
const melody = [523, 587, 659, 784, 659, 784, 1047];
melody.forEach((n, i) => setTimeout(() => playTone(n, 0.2, 'square', 0.1), i * 120));
}
function playDefeat() {
const notes = [392, 349, 330, 262];
notes.forEach((n, i) => setTimeout(() => playTone(n, 0.3, 'sine', 0.1), i * 200));
}
function playMenuSelect() {
playTone(440, 0.05, 'sine', 0.08);
}
function playTransform() {
for (let i = 0; i < 8; i++) {
setTimeout(() => playTone(200 + i * 100, 0.15, 'sine', 0.08), i * 60);
}
}
function playGuardianDefeated() {
const notes = [262, 330, 392, 523, 659, 784, 1047];
notes.forEach((n, i) => setTimeout(() => playTone(n, 0.25, 'square', 0.1), i * 100));
}
// Ambient pad for overworld
let ambientOsc = null;
let ambientGain = null;
function startAmbient() {
if (!ctx || !masterGain)
return;
stopAmbient();
ambientOsc = ctx.createOscillator();
ambientGain = ctx.createGain();
ambientOsc.type = 'sine';
ambientOsc.frequency.value = 80;
ambientGain.gain.value = 0.02;
ambientOsc.connect(ambientGain);
ambientGain.connect(masterGain);
ambientOsc.start();
// Second harmonic
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.type = 'sine';
osc2.frequency.value = 120;
gain2.gain.value = 0.015;
osc2.connect(gain2);
gain2.connect(masterGain);
osc2.start();
}
function stopAmbient() {
try {
ambientOsc?.stop();
}
catch { }
ambientOsc = null;
}
};
// ── module: src/battle.ts ──
__mods["src/battle.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.startBattle = startBattle;
exports.getBattleState = getBattleState;
exports.getParticles = getParticles;
exports.getFloatTexts = getFloatTexts;
exports.isAnimating = isAnimating;
exports.getShakeAmount = getShakeAmount;
exports.executeMove = executeMove;
exports.executeEnemyTurn = executeEnemyTurn;
exports.processStatusEffects = processStatusEffects;
exports.checkBattleEnd = checkBattleEnd;
exports.startCapture = startCapture;
exports.captureTap = captureTap;
exports.resolveCapture = resolveCapture;
exports.resetCapture = resetCapture;
exports.advanceTurn = advanceTurn;
exports.isPlayerTurn = isPlayerTurn;
exports.isEnemyTurn = isEnemyTurn;
exports.getCurrentActor = getCurrentActor;
const abilities_1 = require("./data/abilities");
const creatures_1 = require("./data/creatures");
const types_1 = require("./types");
const zones_1 = require("./data/zones");
let battleState = null;
let particles = [];
let floatTexts = [];
let animating = false;
let captureMode = false;
let captureTimer = 0;
let shakeAmount = 0;
function startBattle(playerParty, enemyTemplates, isWild, zoneId) {
const enemies = enemyTemplates.map(e => {
const c = createEnemyCreature(e.templateId, e.level);
return c;
});
// Filter out fainted creatures from party
const activeParty = playerParty.filter(c => c.currentHp > 0);
if (activeParty.length === 0) {
// All fainted - game over scenario
activeParty.push(playerParty[0]); // revive first one with 1 HP
activeParty[0].currentHp = 1;
}
battleState = {
playerParty: activeParty,
enemies,
activePlayerIndex: 0,
turnOrder: [],
currentTurnIndex: 0,
phase: 'choose',
dialogText: `A wild ${(0, creatures_1.getCreatureTemplate)(enemies[0].templateId).name} appears!`,
isWild,
zoneId
};
// Calculate initial turn order based on SPD
recalcTurnOrder();
particles = [];
floatTexts = [];
animating = false;
captureMode = false;
return battleState;
}
function createEnemyCreature(templateId, level) {
const tmpl = (0, creatures_1.getCreatureTemplate)(templateId);
if (!tmpl)
throw new Error(`Unknown creature: ${templateId}`);
const hp = Math.floor(tmpl.baseStats.hp * (1 + level * 0.4));
return {
templateId,
level,
currentHp: hp, maxHp: hp,
atk: Math.floor(tmpl.baseStats.atk * (1 + level * 0.35)),
def: Math.floor(tmpl.baseStats.def * (1 + level * 0.35)),
spd: Math.floor(tmpl.baseStats.spd * (1 + level * 0.3)),
affinities: [...tmpl.affinities],
abilities: tmpl.abilities.slice(0, 4).map(aid => ({ abilityId: aid, pp: 25 })),
exp: 0,
expToNext: Math.floor(Math.pow(level + 1, 2) * 8)
};
}
function recalcTurnOrder() {
if (!battleState)
return;
const bs = battleState;
const all = [...bs.playerParty.map((_, i) => ({ idx: i, spd: bs.playerParty[i].spd, isPlayer: true })),
...bs.enemies.map((_, i) => ({ idx: bs.playerParty.length + i, spd: bs.enemies[i].spd, isPlayer: false }))];
// Filter out fainted
const alive = all.filter(e => {
if (e.isPlayer)
return bs.playerParty[e.idx].currentHp > 0;
return bs.enemies[e.idx - bs.playerParty.length].currentHp > 0;
});
// Sort by SPD descending, then priority
alive.sort((a, b) => b.spd - a.spd);
battleState.turnOrder = alive.map(e => e.idx);
battleState.currentTurnIndex = 0;
}
function getBattleState() { return battleState; }
function getParticles() { return particles; }
function getFloatTexts() { return floatTexts; }
function isAnimating() { return animating; }
function getShakeAmount() { return shakeAmount; }
function addParticle(x, y, color, count = 5) {
for (let i = 0; i < count; i++) {
particles.push({
x, y,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4 - 2,
life: 30 + Math.random() * 20,
maxLife: 50,
color,
size: 2 + Math.random() * 3
});
}
}
function addFloatText(x, y, text, color) {
floatTexts.push({ x, y, text, color, life: 60, size: undefined });
}
// Damage formula: (0.4 * Level * ATK / DEF * Multiplier) + random
function calcDamage(attacker, defender, ability) {
if (ability.power === 0)
return 0;
const level = attacker.level;
const atk = attacker.atk;
const def = Math.max(defender.def, 1);
// Type effectiveness
let multiplier = 1.0;
for (const aff of defender.affinities) {
multiplier *= (0, types_1.getEffectiveness)(ability.affinity, aff);
}
// STAB bonus
if (attacker.affinities.includes(ability.affinity)) {
multiplier *= 1.25;
}
const base = Math.floor((0.4 * level * atk / def) * multiplier);
const randomFactor = 0.85 + Math.random() * 0.15; // +/- 7.5%
let damage = Math.max(1, Math.floor(base * randomFactor));
// Critical hit (1/16 chance, or boosted by ability)
const critChance = ability.effect === 'crit-boost' ? 0.2 : 0.0625;
if (Math.random() < critChance) {
damage = Math.floor(damage * 1.5);
}
return damage;
}
function applyStatusEffect(target, effect) {
target.status = { effect, turns: 3 };
}
function executeMove(playerIdx, abilityId) {
if (!battleState || animating)
return [];
const messages = [];
animating = true;
const attacker = battleState.playerParty[playerIdx];
const abilityDef = (0, abilities_1.getAbility)(abilityId);
if (!abilityDef) {
animating = false;
return ['Unknown ability'];
}
// Check PP
const abEntry = attacker.abilities.find(a => a.abilityId === abilityId);
if (!abEntry || abEntry.pp <= 0) {
messages.push('No energy left for that move!');
animating = false;
return messages;
}
abEntry.pp--;
const attackerName = (0, creatures_1.getCreatureTemplate)(attacker.templateId).name;
// Self-target moves
if (abilityDef.target === 'self') {
switch (abilityDef.effect) {
case 'heal': {
const healAmt = Math.floor(attacker.maxHp * 0.4);
attacker.currentHp = Math.min(attacker.maxHp, attacker.currentHp + healAmt);
messages.push(`${attackerName} used ${abilityDef.name}! Restored HP.`);
addParticle(200, 300, '#4ade80', 10);
break;
}
case 'atk-up': {
attacker.atk = Math.floor(attacker.atk * 1.3);
messages.push(`${attackerName} used ${abilityDef.name}! ATK increased!`);
addParticle(200, 300, '#ff6b35', 8);
break;
}
case 'speed-boost': {
attacker.spd = Math.floor(attacker.spd * 1.4);
messages.push(`${attackerName} used ${abilityDef.name}! SPD increased!`);
addParticle(200, 300, '#facc15', 8);
break;
}
case 'barrier': {
applyStatusEffect(attacker, 'Barrier');
messages.push(`${attackerName} used ${abilityDef.name}! Barrier active.`);
addParticle(200, 300, '#ffffff', 8);
break;
}
case 'regen': {
applyStatusEffect(attacker, 'Regen');
messages.push(`${attackerName} used ${abilityDef.name}! Regenerating.`);
addParticle(200, 300, '#4ade80', 8);
break;
}
}
battleState.dialogText = messages.join(' ');
animating = false;
return messages;
}
// Determine targets
const targets = [];
if (abilityDef.target === 'all-enemies') {
targets.push(...battleState.enemies.filter(e => e.currentHp > 0));
}
else {
// Single target - pick first alive enemy, or random
const aliveEnemies = battleState.enemies.filter(e => e.currentHp > 0);
if (aliveEnemies.length === 0) {
animating = false;
return ['No targets!'];
}
targets.push(aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)]);
}
let totalDamage = 0;
for (const target of targets) {
// Check accuracy
if (Math.random() * 100 > abilityDef.accuracy) {
messages.push(`${attackerName} used ${abilityDef.name}... but missed!`);
continue;
}
// Check barrier on target
if (target.status?.effect === 'Barrier') {
target.status = undefined;
messages.push(`${(0, creatures_1.getCreatureTemplate)(target.templateId).name}'s Barrier absorbed the attack!`);
addParticle(600, 250, '#ffffff', 12);
continue;
}
let damage = calcDamage(attacker, target, abilityDef);
const isCrit = damage > Math.floor((0.4 * attacker.level * attacker.atk / Math.max(target.def, 1)) * 1.3);
// Drain effect
if (abilityDef.effect === 'drain') {
const healAmt = Math.floor(damage * 0.5);
attacker.currentHp = Math.min(attacker.maxHp, attacker.currentHp + healAmt);
}
target.currentHp -= damage;
totalDamage += damage;
if (target.currentHp < 0)
target.currentHp = 0;
const targetName = (0, creatures_1.getCreatureTemplate)(target.templateId).name;
messages.push(`${attackerName} used ${abilityDef.name}!`);
// Type effectiveness message
let effMsg = '';
for (const aff of target.affinities) {
const eff = (0, types_1.getEffectiveness)(abilityDef.affinity, aff);
if (eff > 1)
effMsg = 'It is super effective!';
else if (eff < 1)
effMsg = 'It is not very effective...';
}
if (effMsg)
messages.push(effMsg);
// Particles based on type effectiveness
const isSuperEff = targets.some(t => t.affinities.some(a => (0, types_1.getEffectiveness)(abilityDef.affinity, a) > 1));
addParticle(600, 250, abilityDef.affinity === 'Pyra' ? '#ff6b35' :
abilityDef.affinity === 'Aqua' ? '#3b82f6' :
abilityDef.affinity === 'Terra' ? '#84cc16' :
abilityDef.affinity === 'Lux' ? '#facc15' :
abilityDef.affinity === 'Umbra' ? '#7c3aed' :
abilityDef.affinity === 'Void' ? '#e040fb' : '#ffffff', isSuperEff ? 20 : 8);
if (isCrit) {
shakeAmount = 10;
addFloatText(600, 230, `CRIT ${damage}!`, '#ff4444');
}
else {
shakeAmount = isSuperEff ? 6 : 3;
addFloatText(600, 250, `${damage}`, isSuperEff ? '#ffff00' : '#ffffff');
}
// Apply special effect
if (abilityDef.effectChance && Math.random() * 100 < abilityDef.effectChance) {
const eff = abilityDef.effect;
if (['burn', 'frost', 'shock'].includes(eff)) {
applyStatusEffect(target, eff);
messages.push(`${targetName} got ${eff.toLowerCase()}!`);
}
}
// Multi-hit: do extra damage
if (abilityDef.effect === 'multi-hit') {
const extraDmg = Math.floor(damage * 0.5);
target.currentHp -= extraDmg;
if (target.currentHp < 0)
target.currentHp = 0;
messages.push(`Hit again for ${extraDmg}!`);
}
// Def-down effect
if (abilityDef.effect === 'def-down') {
target.def = Math.floor(target.def * 0.75);
messages.push(`${targetName}'s DEF dropped!`);
}
}
battleState.dialogText = messages.join(' ');
animating = false;
return messages;
}
// Enemy AI turn
function executeEnemyTurn() {
if (!battleState || animating)
return [];
const messages = [];
animating = true;
// Find current enemy in turn order
const allCreatures = [...battleState.playerParty, ...battleState.enemies];
const aliveEnemies = battleState.enemies.filter(e => e.currentHp > 0);
if (aliveEnemies.length === 0) {
animating = false;
return [];
}
// Pick random alive enemy to act
const attacker = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)];
const attackerName = (0, creatures_1.getCreatureTemplate)(attacker.templateId).name;
// Pick random ability
const abEntry = attacker.abilities[Math.floor(Math.random() * attacker.abilities.length)];
if (!abEntry) {
animating = false;
return [];
}
const abilityDef = (0, abilities_1.getAbility)(abEntry.abilityId);
if (!abilityDef || abilityDef.power === 0 && abilityDef.target !== 'self') {
// Try another or skip
messages.push(`${attackerName} hesitates...`);
animating = false;
return messages;
}
// Target: pick random alive player creature (prefer active)
const alivePlayers = battleState.playerParty.filter(c => c.currentHp > 0);
if (alivePlayers.length === 0) {
animating = false;
return [];
}
const target = alivePlayers[Math.floor(Math.random() * alivePlayers.length)];
// Check barrier on enemy
if (attacker.status?.effect === 'Barrier') {
attacker.status = undefined;
messages.push(`${attackerName}'s Barrier absorbed a counter-attack!`);
animating = false;
return messages;
}
const targetName = (0, creatures_1.getCreatureTemplate)(target.templateId).name;
let damage = calcDamage(attacker, target, abilityDef);
// Check barrier on target
if (target.status?.effect === 'Barrier') {
target.status = undefined;
messages.push(`${attackerName} used ${abilityDef.name}, but ${targetName}'s Barrier absorbed it!`);
addParticle(200, 300, '#ffffff', 12);
animating = false;
return messages;
}
target.currentHp -= damage;
if (target.currentHp < 0)
target.currentHp = 0;
const isCrit = Math.random() < 0.0625;
messages.push(`${attackerName} used ${abilityDef.name}!`);
// Type effectiveness
let effMsg = '';
for (const aff of target.affinities) {
const eff = (0, types_1.getEffectiveness)(abilityDef.affinity, aff);
if (eff > 1)
effMsg = 'It is super effective!';
else if (eff < 1)
effMsg = 'It is not very effective...';
}
if (effMsg)
messages.push(effMsg);
addParticle(200, 300, '#ef4444', 8);
shakeAmount = isCrit ? 8 : 4;
addFloatText(200, 280, `${damage}`, isCrit ? '#ff4444' : '#ffffff');
// Apply status effects from enemy moves
if (abilityDef.effectChance && Math.random() * 100 < abilityDef.effectChance) {
const eff = abilityDef.effect;
if (['burn', 'frost', 'shock'].includes(eff)) {
applyStatusEffect(target, eff);
messages.push(`${targetName} got ${eff.toLowerCase()}!`);
}
}
battleState.dialogText = messages.join(' ');
animating = false;
return messages;
}
// Process status effects at start of turn
function processStatusEffects() {
if (!battleState)
return [];
const messages = [];
// Process all creatures
for (const c of [...battleState.playerParty, ...battleState.enemies]) {
if (!c.status || c.currentHp <= 0)
continue;
const name = (0, creatures_1.getCreatureTemplate)(c.templateId).name;
switch (c.status.effect) {
case 'Burn': {
const dmg = Math.floor(c.atk * 0.5);
c.currentHp -= dmg;
if (c.currentHp < 0)
c.currentHp = 0;
messages.push(`${name} is hurt by burn! (-${dmg})`);
break;
}
case 'Frost': {
// SPD halved already applied in turn order calc
messages.push(`${name} is slowed by frost.`);
break;
}
case 'Shock': {
if (Math.random() < 0.3) {
messages.push(`${name} is paralyzed and can't move!`);
c.status.turns--; // skip this turn
return messages; // early exit - skipped turn
}
break;
}
case 'Regen': {
const heal = Math.floor(c.maxHp * 0.15);
c.currentHp = Math.min(c.maxHp, c.currentHp + heal);
messages.push(`${name} regenerates HP! (+${heal})`);
break;
}
}
c.status.turns--;
if (c.status.turns <= 0) {
c.status = undefined;
}
}
return messages;
}
// Check battle end conditions
function checkBattleEnd() {
if (!battleState)
return null;
const enemiesAlive = battleState.enemies.some(e => e.currentHp > 0);
const playersAlive = battleState.playerParty.some(c => c.currentHp > 0);
if (!enemiesAlive)
return 'victory';
if (!playersAlive)
return 'defeat';
return null;
}
// Capture attempt - rhythm mini-game
function startCapture() {
captureMode = true;
captureTimer = 120; // frames (~2 seconds at 60fps)
}
let captureScore = 0;
let captureTarget = 0;
function captureTap() {
if (!captureMode || !battleState)
return -1;
captureScore++;
return captureTimer--;
}
function resolveCapture(itemId) {
if (!battleState)
return false;
const item = zones_1.ITEMS[itemId];
if (!item)
return false;
// Find weakest enemy (most HP remaining relative to max)
const target = battleState.enemies.reduce((best, e) => {
if (e.currentHp <= 0)
return best;
const ratio = e.currentHp / e.maxHp;
return (!best || ratio < (best.currentHp / best.maxHp)) ? e : best;
}, null);
if (!target)
return false;
// Capture formula: base rate * item power * (1 - hpRatio) + rhythm bonus
const hpRatio = target.currentHp / target.maxHp;
const baseRate = 0.3;
const itemPower = item.capturePower || 1;
const rhythmBonus = Math.min(captureScore * 0.05, 0.3);
const levelPenalty = Math.max(0, 1 - (target.level - 5) * 0.02);
const successChance = baseRate * itemPower * (1 - hpRatio * 0.7) + rhythmBonus + levelPenalty * 0.1;
captureScore = 0;
captureTarget = 0;
return Math.random() < Math.min(successChance, 0.95);
}
function resetCapture() {
captureMode = false;
captureScore = 0;
captureTimer = 0;
}
// Advance to next turn
function advanceTurn() {
if (!battleState)
return;
battleState.currentTurnIndex++;
if (battleState.currentTurnIndex >= battleState.turnOrder.length) {
// New round - recalculate with status effects
const statusMsgs = processStatusEffects();
if (statusMsgs.length > 0 && !animating) {
battleState.dialogText = statusMsgs.join(' ');
}
recalcTurnOrder();
}
}
function isPlayerTurn() {
if (!battleState || battleState.currentTurnIndex >= battleState.turnOrder.length)
return false;
const idx = battleState.turnOrder[battleState.currentTurnIndex];
return idx < battleState.playerParty.length && battleState.playerParty[idx].currentHp > 0;
}
function isEnemyTurn() {
if (!battleState || battleState.currentTurnIndex >= battleState.turnOrder.length)
return false;
const idx = battleState.turnOrder[battleState.currentTurnIndex];
return idx >= battleState.playerParty.length && battleState.enemies[idx - battleState.playerParty.length].currentHp > 0;
}
function getCurrentActor() {
if (!battleState || battleState.currentTurnIndex >= battleState.turnOrder.length)
return null;
const idx = battleState.turnOrder[battleState.currentTurnIndex];
if (idx < battleState.playerParty.length)
return battleState.playerParty[idx];
return battleState.enemies[idx - battleState.playerParty.length];
}
};
// ── module: src/overworld.ts ──
__mods["src/overworld.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TILE_SIZE = exports.MAP_H = exports.MAP_W = void 0;
exports.getTileAt = getTileAt;
exports.isWalkable = isWalkable;
exports.getShrinePos = getShrinePos;
exports.getGuardianPos = getGuardianPos;
exports.getExitPos = getExitPos;
exports.setCamera = setCamera;
exports.getCamera = getCamera;
exports.updateAmbientParticles = updateAmbientParticles;
exports.getAmbientParticles = getAmbientParticles;
exports.checkEncounter = checkEncounter;
exports.checkZoneExit = checkZoneExit;
exports.checkShrine = checkShrine;
exports.healParty = healParty;
const zones_1 = require("./data/zones");
// Tile map for each zone - simple grid-based maps
const TILE_SIZE = 40;
exports.TILE_SIZE = TILE_SIZE;
const MAP_W = 20;
exports.MAP_W = MAP_W;
const MAP_H = 15;
exports.MAP_H = MAP_H;
function generateZoneMap(zoneId) {
const tiles = [];
for (let y = 0; y < MAP_H; y++) {
tiles[y] = [];
for (let x = 0; x < MAP_W; x++) {
// Border walls
if (x === 0 || x === MAP_W - 1 || y === 0 || y === MAP_H - 1) {
tiles[y][x] = 1;
}
else {
tiles[y][x] = 0; // start all walkable
}
}
}
const cx = Math.floor(MAP_W / 2);
const cy = Math.floor(MAP_H / 2);
// Place shrine at center
const shrinePos = { x: cx, y: cy };
tiles[shrinePos.y][shrinePos.x] = 2;
// Create tall grass patches around the map (not on paths)
for (let y = 1; y < MAP_H - 1; y++) {
for (let x = 1; x < MAP_W - 1; x++) {
if (x === cx || y === cy)
continue; // keep cross path clear
const r = Math.random();
if (r < 0.35)
tiles[y][x] = 5; // tall grass
else if (r < 0.42)
tiles[y][x] = 1; // wall
}
}
// Ensure clear paths from shrine to all edges
for (let x = 1; x < MAP_W - 1; x++) {
tiles[cy][x] = 0;
}
for (let y = 1; y < MAP_H - 1; y++) {
tiles[cx][y] = 0;
}
// Guardian position (top-right area)
const guardianPos = { x: MAP_W - 3, y: 2 };
if (zones_1.ZONES[zoneId].guardianId) {
tiles[guardianPos.y][guardianPos.x] = 3;
// Clear path to guardian
for (let x = cx; x < MAP_W - 2; x++)
tiles[2][x] = 0;
for (let y = cy; y > 1; y--)
tiles[cx + (MAP_W - 3 - cx) > 0 ? Math.floor((MAP_W - 3 - cx) / 2) : cx][y] = 0;
}
// Zone exit (bottom-right area)
const nextZone = Object.entries(zones_1.ZONES).find(([_, z]) => z.unlockedByZone === zoneId);
let exitPos;
if (nextZone) {
exitPos = { x: MAP_W - 3, y: MAP_H - 3 };
tiles[exitPos.y][exitPos.x] = 4;
// Clear path to exit
for (let x = cx; x < MAP_W - 2; x++)
tiles[MAP_H - 3][x] = 0;
for (let y = cy; y < MAP_H - 2; y++)
tiles[cx + Math.floor((MAP_W - 3 - cx) / 2)][y] = 0;
}
return { tiles, shrinePos, guardianPos, exitPos };
}
// Pre-generate maps with seeded randomness for consistency
const zoneMaps = {};
function getZoneMap(zoneId) {
if (!zoneMaps[zoneId]) {
// Use a simple seed based on zone name length for consistent maps
const origRandom = Math.random;
let seed = zoneId.length * 137 + 42;
Math.random = () => { seed = (seed * 16807) % 2147483647; return (seed - 1) / 2147483646; };
zoneMaps[zoneId] = generateZoneMap(zoneId);
Math.random = origRandom;
}
return zoneMaps[zoneId];
}
function getTileAt(zoneId, x, y) {
const map = getZoneMap(zoneId);
if (x < 0 || x >= MAP_W || y < 0 || y >= MAP_H)
return 1;
return map.tiles[y][x];
}
function isWalkable(zoneId, x, y) {
const tile = getTileAt(zoneId, x, y);
return tile !== 1; // only walls block movement
}
function getShrinePos(zoneId) { return getZoneMap(zoneId).shrinePos; }
function getGuardianPos(zoneId) { return getZoneMap(zoneId).guardianPos; }
function getExitPos(zoneId) { return getZoneMap(zoneId).exitPos; }
// Camera tracking for rendering
let cameraX = 0, cameraY = 0;
function setCamera(x, y) { cameraX = x; cameraY = y; }
function getCamera() { return { x: cameraX, y: cameraY }; }
let ambientParticles = [];
function updateAmbientParticles(zoneId, canvasW, canvasH) {
const zone = zones_1.ZONES[zoneId];
if (!zone)
return;
// Spawn new particles
if (Math.random() < 0.3) {
ambientParticles.push({
x: Math.random() * canvasW,
y: canvasH + 10,
vx: (Math.random() - 0.5) * 0.5,
vy: -0.5 - Math.random(),
life: 120 + Math.random() * 60,
maxLife: 180,
size: 1 + Math.random() * 3,
alpha: 0.3 + Math.random() * 0.4
});
}
// Update existing
ambientParticles = ambientParticles.filter(p => {
p.x += p.vx;
p.y += p.vy;
p.life--;
return p.life > 0;
});
}
function getAmbientParticles() { return ambientParticles; }
// Track steps in tall grass for forced encounters
let tallGrassSteps = 0;
function checkEncounter(state) {
const zone = zones_1.ZONES[state.currentZone];
if (!zone || !zone.wildCreatures.length)
return null;
// Check for guardian encounter
const gPos = getGuardianPos(state.currentZone);
if (gPos && state.playerX === gPos.x && state.playerY === gPos.y) {
if (zones_1.ZONES[state.currentZone].guardianId && !state.defeatedGuardians.includes(state.currentZone)) {
return { enemies: [{ templateId: zones_1.ZONES[state.currentZone].guardianId, level: zone.guardianLevel }], isBoss: true };
}
}
// Check for zone exit
const ePos = getExitPos(state.currentZone);
if (ePos && state.playerX === ePos.x && state.playerY === ePos.y) {
return null; // handled separately
}
// Random wild encounter in tall grass or by rate
const tile = getTileAt(state.currentZone, state.playerX, state.playerY);
if (tile === 5) {
tallGrassSteps++;
}
else {
tallGrassSteps = Math.max(0, tallGrassSteps - 2); // Decay when not in grass
}
const rate = tile === 5 ? zone.encounterRate * 1.8 : zone.encounterRate;
// Force encounter after enough steps in tall grass (guaranteed within ~6 steps)
if (tallGrassSteps >= 6 || Math.random() < rate) {
tallGrassSteps = 0;
const pool = zone.wildCreatures;
const pick = pool[Math.floor(Math.random() * pool.length)];
let level = pick.minLvl + Math.floor(Math.random() * (pick.maxLvl - pick.minLvl + 1));
// Post-game: harder spawns
if (state.postGame) {
level = Math.min(level + 5, 50);
}
return { enemies: [{ templateId: pick.templateId, level }] };
}
return null;
}
function checkZoneExit(state) {
const ePos = getExitPos(state.currentZone);
if (!ePos)
return null;
if (state.playerX === ePos.x && state.playerY === ePos.y) {
// Find next zone
for (const [id, z] of Object.entries(zones_1.ZONES)) {
if (z.unlockedByZone === state.currentZone) {
if (!state.unlockedZones.includes(id)) {
state.unlockedZones.push(id);
}
return id;
}
}
}
return null;
}
function checkShrine(state) {
const sPos = getShrinePos(state.currentZone);
if (sPos && state.playerX === sPos.x && state.playerY === sPos.y) {
return true;
}
return false;
}
function healParty(party) {
for (const c of party) {
c.currentHp = c.maxHp;
if (c.status)
c.status = undefined;
}
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.resizeCanvas = resizeCanvas;
exports.renderOverworld = renderOverworld;
exports.renderBattle = renderBattle;
exports.updateBattleEffects = updateBattleEffects;
const creatures_1 = require("./data/creatures");
const zones_1 = require("./data/zones");
const overworld_1 = require("./overworld");
const battle_1 = require("./battle");
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
let dpr = 1;
function resizeCanvas() {
dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
ctx.scale(dpr, dpr);
}
// === OVERWORLD RENDERING ===
let playerAnimFrame = 0;
let playerDir = 'down';
function renderOverworld(state) {
const w = window.innerWidth;
const h = window.innerHeight;
const zone = zones_1.ZONES[state.currentZone];
if (!zone)
return;
// Background gradient with subtle vignette
const grad = ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, Math.max(w, h) * 0.7);
grad.addColorStop(0, zone.gradientEnd);
grad.addColorStop(1, zone.gradientStart);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// Ambient particles
(0, overworld_1.updateAmbientParticles)(state.currentZone, w, h);
for (const p of (0, overworld_1.getAmbientParticles)()) {
const alpha = (p.life / p.maxLife) * p.alpha;
ctx.globalAlpha = alpha;
ctx.fillStyle = zone.particleColor;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Camera centered on player with smooth follow
const camX = state.playerX * overworld_1.TILE_SIZE - w / 2 + overworld_1.TILE_SIZE / 2;
const camY = state.playerY * overworld_1.TILE_SIZE - h / 2 + overworld_1.TILE_SIZE / 2;
ctx.save();
ctx.translate(-camX, -camY);
// Draw tiles with subtle grid
for (let y = 0; y < overworld_1.MAP_H; y++) {
for (let x = 0; x < overworld_1.MAP_W; x++) {
const tile = (0, overworld_1.getTileAt)(state.currentZone, x, y);
const px = x * overworld_1.TILE_SIZE;
const py = y * overworld_1.TILE_SIZE;
// Skip off-screen tiles
if (px + overworld_1.TILE_SIZE < camX || px > camX + w || py + overworld_1.TILE_SIZE < camY || py > camY + h)
continue;
switch (tile) {
case 0: // walkable - subtle ground texture
ctx.fillStyle = 'rgba(255,255,255,0.02)';
ctx.fillRect(px, py, overworld_1.TILE_SIZE, overworld_1.TILE_SIZE);
// Grid line
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.lineWidth = 0.5;
ctx.strokeRect(px + 0.5, py + 0.5, overworld_1.TILE_SIZE - 1, overworld_1.TILE_SIZE - 1);
break;
case 1: // wall - dark stone blocks
ctx.fillStyle = '#12122a';
ctx.fillRect(px, py, overworld_1.TILE_SIZE, overworld_1.TILE_SIZE);
ctx.strokeStyle = 'rgba(100,80,160,0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(px + 1, py + 1, overworld_1.TILE_SIZE - 2, overworld_1.TILE_SIZE - 2);
// Inner detail
ctx.fillStyle = 'rgba(100,80,160,0.15)';
ctx.fillRect(px + 4, py + 4, overworld_1.TILE_SIZE - 8, overworld_1.TILE_SIZE - 8);
break;
case 2: // shrine
ctx.fillStyle = 'rgba(123,104,238,0.1)';
ctx.fillRect(px, py, overworld_1.TILE_SIZE, overworld_1.TILE_SIZE);
drawShrineIcon(px + overworld_1.TILE_SIZE / 2, py + overworld_1.TILE_SIZE / 2);
break;
case 3: // guardian
ctx.fillStyle = 'rgba(239,68,68,0.1)';
ctx.fillRect(px, py, overworld_1.TILE_SIZE, overworld_1.TILE_SIZE);
drawGuardianIcon(px + overworld_1.TILE_SIZE / 2, py + overworld_1.TILE_SIZE / 2);
break;
case 4: // zone exit
ctx.fillStyle = 'rgba(74,222,128,0.1)';
ctx.fillRect(px, py, overworld_1.TILE_SIZE, overworld_1.TILE_SIZE);
drawExitIcon(px + overworld_1.TILE_SIZE / 2, py + overworld_1.TILE_SIZE / 2);
break;
case 5: // tall grass - encounter zone
ctx.fillStyle = 'rgba(132,204,22,0.08)';
ctx.fillRect(px, py, overworld_1.TILE_SIZE, overworld_1.TILE_SIZE);
drawGrassIcon(px + overworld_1.TILE_SIZE / 2, py + overworld_1.TILE_SIZE / 2);
break;
}
}
}
// Draw player character with shadow
const px = state.playerX * overworld_1.TILE_SIZE + overworld_1.TILE_SIZE / 2;
const py = state.playerY * overworld_1.TILE_SIZE + overworld_1.TILE_SIZE / 2;
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.beginPath();
ctx.ellipse(px, py + 14, 8, 3, 0, 0, Math.PI * 2);
ctx.fill();
drawPlayer(px, py, playerDir);
ctx.restore();
// Zone name overlay with glass effect
ctx.fillStyle = 'rgba(10,14,26,0.5)';
roundRect(ctx, 8, 8, 200, 32, 8);
ctx.fill();
ctx.font = 'bold 13px system-ui';
ctx.fillStyle = '#a09cb5';
ctx.textAlign = 'left';
ctx.fillText(zone.name.toUpperCase(), 16, 29);
// Party preview (top right) with glass effect
let partyX = w - 16;
for (let i = state.party.length - 1; i >= 0 && partyX > 100; i--) {
const c = state.party[i];
const tmpl = (0, creatures_1.getCreatureTemplate)(c.templateId);
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'right';
ctx.fillStyle = '#e8e6f0';
ctx.fillText(`${tmpl.name} Lv${c.level}`, partyX, 29);
partyX -= 100;
}
// Story vignette overlay (if story stage advanced)
if (state.storyStage > 0 && state.screen === 'overworld') {
const vignetteAlpha = Math.min(0.3, Math.sin(Date.now() / 2000) * 0.1 + 0.2);
ctx.fillStyle = `rgba(0,0,0,${vignetteAlpha})`;
ctx.fillRect(0, h - 60, w, 60);
ctx.font = 'italic 13px system-ui';
ctx.textAlign = 'center';
ctx.fillStyle = '#a09cb5';
const storyTexts = [
'',
'The Aether flows thin. The Anchors stir in their slumber.',
'Deeper currents call from below the surface.',
'Ancient roots awaken in response to your journey.',
'All paths converge at the Spire.'
];
ctx.fillText(storyTexts[state.storyStage] || '', w / 2, h - 30);
}
playerAnimFrame++;
}
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
function drawShrineIcon(x, y) {
const t = Date.now() / 1000;
ctx.save();
ctx.translate(x, y);
// Glowing diamond shape
const glow = 0.5 + Math.sin(t * 2) * 0.3;
ctx.fillStyle = `rgba(123,104,238,${glow})`;
ctx.beginPath();
ctx.moveTo(0, -12);
ctx.lineTo(8, 0);
ctx.lineTo(0, 12);
ctx.lineTo(-8, 0);
ctx.closePath();
ctx.fill();
// Inner glow
ctx.fillStyle = `rgba(255,255,255,${glow * 0.5})`;
ctx.beginPath();
ctx.moveTo(0, -6);
ctx.lineTo(4, 0);
ctx.lineTo(0, 6);
ctx.lineTo(-4, 0);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawGuardianIcon(x, y) {
const t = Date.now() / 1000;
ctx.save();
ctx.translate(x, y);
// Pulsing red circle with skull-like shape
const pulse = 0.6 + Math.sin(t * 3) * 0.4;
ctx.fillStyle = `rgba(239,68,68,${pulse})`;
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#fff';
ctx.fillRect(-5, -4, 3, 3);
ctx.fillRect(2, -4, 3, 3);
ctx.restore();
}
function drawExitIcon(x, y) {
const t = Date.now() / 1000;
ctx.save();
ctx.translate(x, y);
// Green arrow pointing right
const pulse = 0.5 + Math.sin(t * 2) * 0.3;
ctx.fillStyle = `rgba(74,222,128,${pulse})`;
ctx.beginPath();
ctx.moveTo(-6, -8);
ctx.lineTo(6, 0);
ctx.lineTo(-6, 8);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawGrassIcon(x, y) {
const t = Date.now() / 1000;
ctx.save();
ctx.translate(x, y);
// Simple grass tufts
for (let i = -2; i <= 2; i++) {
const sway = Math.sin(t * 3 + i) * 2;
ctx.strokeStyle = 'rgba(132,204,22,0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(i * 4, 8);
ctx.quadraticCurveTo(i * 4 + sway, -2, i * 4 + sway * 2, -6);
ctx.stroke();
}
ctx.restore();
}
function drawPlayer(x, y, dir) {
const t = Date.now() / 1000;
const bobY = Math.sin(t * 4) * 2;
ctx.save();
ctx.translate(x, y + bobY);
// Body - simple character shape
ctx.fillStyle = '#7b68ee';
ctx.beginPath();
ctx.arc(0, -2, 10, 0, Math.PI * 2);
ctx.fill();
// Head
ctx.fillStyle = '#9d8df1';
ctx.beginPath();
ctx.arc(0, -10, 7, 0, Math.PI * 2);
ctx.fill();
// Eyes based on direction
ctx.fillStyle = '#fff';
if (dir === 'down' || dir === 'left' || dir === 'right') {
const eyeOffX = dir === 'left' ? -3 : dir === 'right' ? 3 : 0;
ctx.fillRect(-4 + eyeOffX, -12, 3, 3);
ctx.fillRect(1 + eyeOffX, -12, 3, 3);
}
// Direction indicator (small triangle)
ctx.fillStyle = '#fff';
const dirOffsets = { up: [0, -16], down: [0, 8], left: [-14, -2], right: [14, -2] };
const [dx, dy] = dirOffsets[dir] || [0, 8];
ctx.beginPath();
ctx.moveTo(dx, dy);
ctx.lineTo(dx - 3, dy + 5);
ctx.lineTo(dx + 3, dy + 5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// === BATTLE RENDERING ===
function renderBattle() {
const bs = (0, battle_1.getBattleState)();
if (!bs)
return;
const w = window.innerWidth;
const h = window.innerHeight;
const zone = zones_1.ZONES[bs.zoneId];
// Background with atmospheric gradient
const grad = ctx.createRadialGradient(w / 2, h / 2, 0, w / 2, h / 2, Math.max(w, h) * 0.6);
grad.addColorStop(0, zone?.gradientEnd || '#1a1040');
grad.addColorStop(1, zone?.gradientStart || '#0a0e1a');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// Atmospheric particles in background
const t = Date.now() / 1000;
for (let i = 0; i < 20; i++) {
const px = ((i * 73 + t * 10) % w);
const py = ((i * 47 + Math.sin(t + i) * 20) % h);
ctx.globalAlpha = 0.05 + Math.sin(t * 2 + i) * 0.03;
ctx.fillStyle = zone?.particleColor || '#ffffff';
ctx.beginPath();
ctx.arc(px, py, 1 + Math.sin(t + i) * 0.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Screen shake
const shake = (0, battle_1.getShakeAmount)();
if (shake > 0) {
ctx.save();
ctx.translate((Math.random() - 0.5) * shake, (Math.random() - 0.5) * shake);
}
// Draw enemies
for (let i = 0; i < bs.enemies.length; i++) {
const e = bs.enemies[i];
if (e.currentHp <= 0)
continue;
drawBattleCreature(e, w - 250 + i * 120, 80, false);
}
// Draw player creature
if (bs.playerParty[bs.activePlayerIndex]) {
const pc = bs.playerParty[bs.activePlayerIndex];
drawBattleCreature(pc, 100, h - 250, true);
}
// Particles
for (const p of (0, battle_1.getParticles)()) {
ctx.globalAlpha = p.life / p.maxLife;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * (p.life / p.maxLife), 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Float texts
for (const ft of (0, battle_1.getFloatTexts)()) {
ctx.globalAlpha = ft.life / 60;
ctx.font = `bold ${ft.size || 24}px system-ui`;
ctx.fillStyle = ft.color;
ctx.textAlign = 'center';
ctx.fillText(ft.text, ft.x, ft.y - (1 - ft.life / 60) * 40);
}
ctx.globalAlpha = 1;
if (shake > 0) {
ctx.restore();
}
}
function drawBattleCreature(creature, x, y, isPlayer) {
const tmpl = (0, creatures_1.getCreatureTemplate)(creature.templateId);
const t = Date.now() / 1000;
const bobY = Math.sin(t * 2) * 3;
ctx.save();
ctx.translate(x, y + bobY);
// Creature silhouette - different shapes per affinity
const mainColor = tmpl.affinities[0] === 'Pyra' ? '#ff6b35' :
tmpl.affinities[0] === 'Aqua' ? '#3b82f6' :
tmpl.affinities[0] === 'Terra' ? '#84cc16' :
tmpl.affinities[0] === 'Lux' ? '#facc15' :
tmpl.affinities[0] === 'Umbra' ? '#7c3aed' :
tmpl.affinities[0] === 'Void' ? '#e040fb' : '#ffffff';
// Body shape based on affinity
ctx.fillStyle = mainColor;
ctx.globalAlpha = 0.8;
if (tmpl.affinities[0] === 'Pyra') {
// Flame shape
ctx.beginPath();
ctx.moveTo(0, -35);
ctx.quadraticCurveTo(20, -10, 15, 10);
ctx.quadraticCurveTo(10, 25, 0, 30);
ctx.quadraticCurveTo(-10, 25, -15, 10);
ctx.quadraticCurveTo(-20, -10, 0, -35);
ctx.fill();
}
else if (tmpl.affinities[0] === 'Aqua') {
// Water drop shape
ctx.beginPath();
ctx.moveTo(0, -30);
ctx.quadraticCurveTo(25, 0, 20, 20);
ctx.quadraticCurveTo(15, 35, 0, 35);
ctx.quadraticCurveTo(-15, 35, -20, 20);
ctx.quadraticCurveTo(-25, 0, 0, -30);
ctx.fill();
}
else if (tmpl.affinities[0] === 'Terra') {
// Crystal shape
ctx.beginPath();
ctx.moveTo(0, -30);
ctx.lineTo(18, -5);
ctx.lineTo(12, 25);
ctx.lineTo(-12, 25);
ctx.lineTo(-18, -5);
ctx.closePath();
ctx.fill();
}
else if (tmpl.affinities[0] === 'Lux') {
// Star shape
for (let i = 0; i < 5; i++) {
const angle = (i * Math.PI * 2 / 5) - Math.PI / 2;
const r1 = 30, r2 = 14;
ctx.lineTo(Math.cos(angle) * r1, Math.sin(angle) * r1);
const a2 = angle + Math.PI / 5;
ctx.lineTo(Math.cos(a2) * r2, Math.sin(a2) * r2);
}
ctx.closePath();
ctx.fill();
}
else if (tmpl.affinities[0] === 'Umbra') {
// Shadow shape - jagged
ctx.beginPath();
ctx.moveTo(-15, -25);
ctx.lineTo(5, -35);
ctx.lineTo(20, -15);
ctx.lineTo(15, 10);
ctx.lineTo(25, 20);
ctx.lineTo(0, 30);
ctx.lineTo(-20, 25);
ctx.lineTo(-18, 5);
ctx.closePath();
ctx.fill();
}
else if (tmpl.affinities[0] === 'Void') {
// Swirling void shape
for (let i = 0; i < 3; i++) {
const r = 12 + i * 8;
ctx.globalAlpha = 0.4 - i * 0.1;
ctx.beginPath();
ctx.arc(Math.sin(t + i) * 5, Math.cos(t + i) * 5, r, 0, Math.PI * 2);
ctx.fill();
}
}
else {
// Prism - hexagonal crystal
for (let i = 0; i < 6; i++) {
const angle = i * Math.PI / 3;
if (i === 0)
ctx.moveTo(Math.cos(angle) * 25, Math.sin(angle) * 25);
else
ctx.lineTo(Math.cos(angle) * 25, Math.sin(angle) * 25);
}
ctx.closePath();
ctx.fill();
}
// Eyes
ctx.globalAlpha = 1;
ctx.fillStyle = '#fff';
ctx.fillRect(-6, -8, 4, 4);
ctx.fillRect(2, -8, 4, 4);
ctx.fillStyle = '#000';
ctx.fillRect(-5, -7, 2, 2);
ctx.fillRect(3, -7, 2, 2);
// Status effect indicator
if (creature.status) {
const statusColors = { Burn: '#ff6b35', Frost: '#3b82f6', Shock: '#e040fb', Barrier: '#ffffff', Regen: '#4ade80' };
ctx.fillStyle = statusColors[creature.status.effect] || '#fff';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
ctx.fillText(creature.status.effect, 0, -35);
}
// HP bar below creature
const hpRatio = creature.currentHp / creature.maxHp;
const barW = 60;
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(-barW / 2, 38, barW, 6);
const hpColor = hpRatio > 0.5 ? '#4ade80' : hpRatio > 0.25 ? '#fbbf24' : '#ef4444';
ctx.fillStyle = hpColor;
ctx.fillRect(-barW / 2, 38, barW * hpRatio, 6);
// Name and level
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'center';
ctx.fillStyle = '#e8e6f0';
ctx.fillText(`${tmpl.name}`, 0, -42);
ctx.font = '9px system-ui';
ctx.fillStyle = '#a09cb5';
ctx.fillText(`Lv ${creature.level}`, 0, -52);
// Affinity badge
ctx.fillStyle = mainColor;
ctx.beginPath();
ctx.arc(-30, -48, 6, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// Update particles and float texts
function updateBattleEffects() {
// Update particles
const ps = (0, battle_1.getParticles)();
for (let i = ps.length - 1; i >= 0; i--) {
ps[i].x += ps[i].vx;
ps[i].y += ps[i].vy;
ps[i].life--;
if (ps[i].life <= 0)
ps.splice(i, 1);
}
// Update float texts
const fts = (0, battle_1.getFloatTexts)();
for (let i = fts.length - 1; i >= 0; i--) {
fts[i].life--;
if (fts[i].life <= 0)
fts.splice(i, 1);
}
// Decay shake
const currentShake = (0, battle_1.getShakeAmount)();
if (currentShake > 0) {
// Shake decays naturally through re-rendering
}
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.showDialog = showDialog;
exports.hideDialog = hideDialog;
exports.showActionMenu = showActionMenu;
exports.hideActionMenu = hideActionMenu;
exports.showAbilityList = showAbilityList;
exports.hideAbilityList = hideAbilityList;
exports.updateBattleCards = updateBattleCards;
exports.updateEnemyCards = updateEnemyCards;
exports.showModal = showModal;
exports.hideModal = hideModal;
exports.showMainMenu = showMainMenu;
exports.showTeamScreen = showTeamScreen;
exports.showBagScreen = showBagScreen;
exports.showBestiaryScreen = showBestiaryScreen;
exports.showMapScreen = showMapScreen;
exports.showShrineMenu = showShrineMenu;
exports.showShopScreen = showShopScreen;
exports.showCaptureMiniGame = showCaptureMiniGame;
exports.showTutorialStep = showTutorialStep;
exports.showStarterChoice = showStarterChoice;
const creatures_1 = require("./data/creatures");
const abilities_1 = require("./data/abilities");
const zones_1 = require("./data/zones");
const types_1 = require("./types");
const state_1 = require("./state");
const modalOverlay = document.getElementById('modal-overlay');
const dialogBox = document.getElementById('dialog-box');
const actionMenu = document.getElementById('action-menu');
const abilityList = document.getElementById('ability-list');
const enemyArea = document.getElementById('enemy-area');
const playerArea = document.getElementById('player-area');
function showDialog(text) {
dialogBox.textContent = text;
dialogBox.style.display = 'block';
}
function hideDialog() {
dialogBox.style.display = 'none';
}
function showActionMenu(items) {
actionMenu.innerHTML = '';
items.forEach(item => {
const btn = document.createElement('button');
btn.className = 'action-btn-choice';
btn.textContent = item.label;
btn.onclick = () => { item.action(); hideActionMenu(); };
actionMenu.appendChild(btn);
});
actionMenu.classList.remove('hidden');
}
function hideActionMenu() {
actionMenu.classList.add('hidden');
}
function showAbilityList(abilities, onSelect) {
abilityList.innerHTML = '';
abilities.forEach(ab => {
const def = (0, abilities_1.getAbility)(ab.abilityId);
if (!def)
return;
const btn = document.createElement('button');
btn.className = 'ability-btn';
btn.innerHTML = `<span class="ab-name">${def.name}</span> <span style="color:${types_1.AFFINITY_COLORS[def.affinity]}">[${def.affinity}]</span><div class="ab-info">PP: ${ab.pp}/${def.maxPp} | Power: ${def.power || '—'} | ${def.effect !== 'none' ? def.effect : ''}</div>`;
if (ab.pp <= 0)
btn.style.opacity = '0.4';
else
btn.onclick = () => { onSelect(ab.abilityId); hideAbilityList(); };
abilityList.appendChild(btn);
});
abilityList.classList.remove('hidden');
}
function hideAbilityList() {
abilityList.classList.add('hidden');
}
// Update battle creature cards
function updateBattleCards(state) {
// Player area
playerArea.innerHTML = '';
state.party.forEach((c, i) => {
const tmpl = (0, creatures_1.getCreatureTemplate)(c.templateId);
const card = document.createElement('div');
card.className = 'creature-card';
if (c.currentHp <= 0)
card.style.opacity = '0.4';
const hpRatio = c.maxHp > 0 ? c.currentHp / c.maxHp : 0;
const hpColor = hpRatio > 0.5 ? '#4ade80' : hpRatio > 0.25 ? '#fbbf24' : '#ef4444';
card.innerHTML = `
<div class="name" style="color:${types_1.AFFINITY_COLORS[tmpl.affinities[0]]}">${tmpl.name}</div>
<div class="level">Lv ${c.level} | HP: ${c.currentHp}/${c.maxHp}</div>
<div class="hp-bar-container"><div class="hp-bar-fill" style="width:${hpRatio * 100}%;background:${hpColor}"></div></div>
`;
playerArea.appendChild(card);
});
}
function updateEnemyCards(enemies) {
enemyArea.innerHTML = '';
enemies.forEach(e => {
if (e.currentHp <= 0)
return;
const tmpl = (0, creatures_1.getCreatureTemplate)(e.templateId);
const card = document.createElement('div');
card.className = 'creature-card';
const hpRatio = e.maxHp > 0 ? e.currentHp / e.maxHp : 0;
const hpColor = hpRatio > 0.5 ? '#4ade80' : hpRatio > 0.25 ? '#fbbf24' : '#ef4444';
card.innerHTML = `
<div class="name" style="color:${types_1.AFFINITY_COLORS[tmpl.affinities[0]]}">${tmpl.name}</div>
<div class="level">Lv ${e.level}</div>
<div class="hp-bar-container"><div class="hp-bar-fill" style="width:${hpRatio * 100}%;background:${hpColor}"></div></div>
`;
enemyArea.appendChild(card);
});
}
// === MODAL SYSTEM ===
let modalCallback = null;
function showModal(content, onClose) {
modalOverlay.innerHTML = `<div class="modal">${content}</div>`;
modalOverlay.classList.remove('hidden');
modalCallback = onClose || null;
}
function hideModal() {
modalOverlay.classList.add('hidden');
const cb = modalCallback;
modalCallback = null;
if (cb)
cb();
}
// === MENU SCREENS ===
function showMainMenu(state) {
return `
<h2>Menu</h2>
<div style="display:flex;flex-direction:column;gap:8px;">
<button class="action-btn-choice" data-menu="team">Team (${state.party.length}/6)</button>
<button class="action-btn-choice" data-menu="bag">Bag</button>
<button class="action-btn-choice" data-menu="encyclopedia">Bestiary</button>
<button class="action-btn-choice" data-menu="map">Map</button>
<button class="action-btn-choice" data-menu="save">Save Game</button>
<button class="action-btn-choice" data-menu="close">Close</button>
</div>`;
}
function showTeamScreen(state) {
let html = '<h2>Team</h2><div style="display:flex;flex-direction:column;gap:8px;">';
state.party.forEach((c, i) => {
const tmpl = (0, creatures_1.getCreatureTemplate)(c.templateId);
const canTr = (0, state_1.canTransform)(c);
html += `
<div class="creature-card" style="width:100%;display:block;">
<div class="name" style="color:${types_1.AFFINITY_COLORS[tmpl.affinities[0]]}">${tmpl.name}</div>
<div class="level">Lv ${c.level} | HP: ${c.currentHp}/${c.maxHp} | ATK: ${c.atk} DEF: ${c.def} SPD: ${c.spd}</div>
<div style="font-size:0.75rem;color:${types_1.AFFINITY_COLORS[tmpl.affinities[0]]};margin-top:4px;">${tmpl.affinities.join(' / ')}</div>
<div class="hp-bar-container"><div class="hp-bar-fill" style="width:${(c.currentHp / c.maxHp) * 100}%"></div></div>
${canTr ? `<button class="action-btn-choice transform-btn" data-tidx="${i}" style="margin-top:6px;font-size:0.8rem;">Transform</button>` : ''}
</div>`;
});
html += '</div><button class="action-btn-choice back-btn" style="margin-top:12px;">Back</button>';
return html;
}
function showBagScreen(state) {
let html = '<h2>Bag</h2><div style="display:flex;flex-direction:column;gap:6px;">';
for (const [itemId, count] of Object.entries(state.inventory)) {
if (count <= 0)
continue;
const item = zones_1.ITEMS[itemId];
if (!item)
continue;
html += `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px;background:rgba(255,255,255,0.05);border-radius:6px;">
<div><strong>${item.name}</strong> <span style="color:#a09cb5;font-size:0.8rem;">${item.description}</span></div>
<span>x${count}</span>
</div>`;
}
html += '</div><button class="action-btn-choice back-btn" style="margin-top:12px;">Back</button>';
return html;
}
function showBestiaryScreen(state) {
let html = '<h2>Bestiary</h2><div style="display:flex;flex-direction:column;gap:6px;max-height:50vh;overflow-y:auto;">';
for (const tmpl of Object.values(creatures_1.CREATURES)) {
const seen = state.collectedCreatures.has(tmpl.id);
html += `<div style="padding:8px;background:${seen ? 'rgba(123,104,238,0.1)' : 'rgba(255,255,255,0.03)'};border-radius:6px;border-left:3px solid ${types_1.AFFINITY_COLORS[tmpl.affinities[0]]};">
<strong style="color:${types_1.AFFINITY_COLORS[tmpl.affinities[0]]}">${seen ? tmpl.name : '???'}</strong>
<span style="font-size:0.75rem;color:#a09cb5;margin-left:8px;">${tmpl.affinities.join('/')}</span>
${seen ? `<div style="font-size:0.75rem;color:#a09cb5;margin-top:4px;">${tmpl.lore}</div>` : ''}
</div>`;
}
html += '</div><button class="action-btn-choice back-btn" style="margin-top:12px;">Back</button>';
return html;
}
function showMapScreen(state) {
let html = '<h2>World Map</h2><div style="display:flex;flex-direction:column;gap:6px;">';
const zoneOrder = ['ember_pass', 'deep_trench', 'stoneheart_cavern', 'prism_spire', 'shadow_veil', 'the_between'];
for (const zid of zoneOrder) {
const zone = zones_1.ITEMS['aether-net'] ? null : null; // dummy access
html += `<div style="padding:8px;background:${state.unlockedZones.includes(zid) ? 'rgba(123,104,238,0.15)' : 'rgba(255,255,255,0.03)'};border-radius:6px;border-left:3px solid ${state.unlockedZones.includes(zid) ? '#7b68ee' : '#333'};">
<strong>${zid.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}</strong>
${state.defeatedGuardians.includes(zid) ? '<span style="color:#4ade80;margin-left:8px;">✓ Cleared</span>' : ''}
</div>`;
}
html += '</div><button class="action-btn-choice back-btn" style="margin-top:12px;">Back</button>';
return html;
}
function showShrineMenu(state) {
return `
<h2>✦ Aether Shrine ✦</h2>
<p style="color:#a09cb5;margin-bottom:16px;">Rest and restore your bond with the creatures.</p>
<div style="display:flex;flex-direction:column;gap:8px;">
<button class="action-btn-choice" data-shrine="heal">Heal Party</button>
<button class="action-btn-choice" data-shrine="save">Save Game</button>
<button class="action-btn-choice" data-shrine="shop">Shop</button>
<button class="action-btn-choice" data-shrine="close">Leave</button>
</div>`;
}
function showShopScreen(state) {
let html = '<h2>✦ Shrine Shop ✦</h2><div style="display:flex;flex-direction:column;gap:6px;">';
const shopItems = [
{ id: 'aether-net', price: 0 },
{ id: 'resonance-cage', price: 0 },
{ id: 'prism-binder', price: 0 },
{ id: 'aether-draught', price: 0 },
{ id: 'prism-elixir', price: 0 }
];
for (const si of shopItems) {
const item = zones_1.ITEMS[si.id];
if (!item)
continue;
html += `<div style="display:flex;justify-content:space-between;align-items:center;padding:6px;background:rgba(255,255,255,0.05);border-radius:6px;">
<div><strong>${item.name}</strong> <span style="color:#a09cb5;font-size:0.8rem;">${item.description}</span></div>
<button class="action-btn-choice buy-btn" data-buy="${si.id}" style="padding:4px 12px;font-size:0.8rem;">Free</button>
</div>`;
}
html += '</div><button class="action-btn-choice back-btn" style="margin-top:12px;">Back</button>';
return html;
}
function showCaptureMiniGame() {
return `
<h2>Capture Attempt</h2>
<p style="color:#a09cb5;margin-bottom:16px;">Tap rapidly to stabilize the creature!</p>
<div id="capture-bar" style="width:100%;height:30px;background:rgba(255,255,255,0.1);border-radius:8px;overflow:hidden;margin-bottom:16px;">
<div id="capture-fill" style="width:0%;height:100%;background:linear-gradient(to right,#7b68ee,#4ade80);transition:width 0.1s;"></div>
</div>
<button class="action-btn-choice capture-tap-btn" style="padding:12px 32px;font-size:1.1rem;">TAP!</button>`;
}
function showTutorialStep(step) {
const steps = [
'Welcome to Aetheris! Use WASD or arrow keys to move around the overworld.',
'Walk into tall grass areas to encounter wild creatures in battle!',
'In battle, choose abilities to weaken creatures, then attempt to capture them with an Aether Net.',
'Visit shrines (glowing diamonds) to heal your party and save your progress.',
'Defeat zone Guardians (red icons) to unlock new areas. Good luck, Binder!'
];
return `
<h2>✦ Tutorial ✦</h2>
<p style="color:#a09cb5;margin-bottom:16px;line-height:1.6;">${steps[step] || steps[steps.length - 1]}</p>
<div style="display:flex;gap:8px;">
${step > 0 ? '<button class="action-btn-choice" data-tut="back">Back</button>' : ''}
${step < steps.length - 1 ? '<button class="action-btn-choice" data-tut="next">Next</button>' : '<button class="action-btn-choice" data-tut="done">Begin Journey!</button>'}
</div>`;
}
function showStarterChoice() {
const starters = [
{ id: 'emberling', desc: 'Pyra affinity. High attack, moderate speed.' },
{ id: 'tidepup', desc: 'Aqua affinity. Balanced stats with good defense.' },
{ id: 'mossback', desc: 'Terra affinity. Tanky with high HP and defense.' }
];
let html = '<h2>Choose Your First Companion</h2><div style="display:flex;flex-direction:column;gap:8px;">';
starters.forEach(s => {
const tmpl = (0, creatures_1.getCreatureTemplate)(s.id);
html += `<button class="action-btn-choice starter-btn" data-starter="${s.id}" style="text-align:left;padding:12px;">
<strong style="color:${types_1.AFFINITY_COLORS[tmpl.affinities[0]]}">${tmpl.name}</strong> — ${s.desc}<br>
<span style="font-size:0.8rem;color:#a09cb5;">${tmpl.lore}</span>
</button>`;
});
html += '</div>';
return html;
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"08f5d7c6-9b4c-46cf-a814-cf6069aa2a40","tokensIn":5240287,"tokensOut":74337,"tokensTotal":5314624,"turns":84,"toolCalls":83,"failedToolCalls":2,"timestamp":"2026-07-30T18:32:47.766Z"} -->