RPG
Qwen 3.5 122b a10b · typescript

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 90,453 bytes · SHA-256 f8cbb14489eb
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quest of the Shattered Realm - Action Adventure RPG</title>
<script src="https://cdn.jsdelivr.net/npm/p5@1.9.0/lib/p5.min.js"></script>
<style>
/* ── css: layout ── */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
#app {
display: flex;
justify-content: center;
align-items: center;
}
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
border: 4px solid #3d3d5c;
border-radius: 8px;
box-shadow: 0 0 40px rgba(0, 0, 0, 0.5), 0 0 100px rgba(92, 107, 192, 0.2);
}
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./player":"src/player.ts","./world":"src/world.ts","./combat":"src/combat.ts","./inventory":"src/inventory.ts","./save":"src/save.ts","./audio":"src/audio.ts","./particles":"src/particles.ts","./ui":"src/ui.ts"},"src/particles.ts":{"./state":"src/state.ts"},"src/world.ts":{"./state":"src/state.ts","./particles":"src/particles.ts","./audio":"src/audio.ts"},"src/player.ts":{"./state":"src/state.ts","./world":"src/world.ts","./audio":"src/audio.ts","./particles":"src/particles.ts"},"src/combat.ts":{"./state":"src/state.ts","./player":"src/player.ts","./audio":"src/audio.ts","./particles":"src/particles.ts"},"src/inventory.ts":{"./state":"src/state.ts"},"src/save.ts":{"./state":"src/state.ts"},"src/ui.ts":{"./state":"src/state.ts","./player":"src/player.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";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setup = setup;
exports.draw = draw;
exports.keyPressed = keyPressed;
const state_1 = require("./state");
const player_1 = require("./player");
const world_1 = require("./world");
const combat_1 = require("./combat");
const inventory_1 = require("./inventory");
const save_1 = require("./save");
const audio_1 = require("./audio");
const particles_1 = require("./particles");
const ui_1 = require("./ui");
let p;
let gameState;
let cameraX = 0;
let cameraY = 0;
let areaNameTimer = 0;
let tutorialVisible = true;
let tutorialTimer = 600;
function setup() {
p = this;
p.createCanvas(state_1.CANVAS_WIDTH, state_1.CANVAS_HEIGHT);
p.rectMode(p.CORNER);
p.ellipseMode(p.CENTER);
p.textAlign(p.LEFT, p.TOP);
(0, audio_1.initAudio)();
// Try to load saved game or start new
const saved = (0, save_1.loadGame)();
if (saved) {
gameState = createFromSave(saved);
}
else {
gameState = createNewGame();
}
}
function createNewGame() {
const player = (0, player_1.createPlayer)(12 * 32 + 16, 10 * 32 + 16);
const inventory = (0, inventory_1.createInventory)();
const world = (0, world_1.generateArea)("village");
const ui = {
showInventory: false,
showShop: false,
showSaveMenu: false,
showGameOver: false,
showVictory: false,
message: "",
messageTimer: 0,
currentScreen: "game",
selectedSlot: 0
};
return { player, inventory, world, ui, saveData: null };
}
function createFromSave(saved) {
const player = (0, player_1.createPlayer)(saved.player.x, saved.player.y);
Object.assign(player, saved.player);
const inventory = (0, inventory_1.createInventory)();
for (const [key, qty] of Object.entries(saved.inventory.items || {})) {
inventory.items.set(key, qty);
}
inventory.accessories = [...(saved.inventory.accessories || [])];
inventory.keyItems = [...(saved.inventory.keyItems || [])];
const world = (0, world_1.generateArea)(saved.world.currentArea || "village");
// Restore collected pickups
for (const pickupId of saved.world.collectedPickups || []) {
const pickup = world.pickups.find(p => p.id === pickupId);
if (pickup)
pickup.collected = true;
}
// Restore opened treasures
for (const treasureId of saved.world.openedTreasures || []) {
const treasure = world.treasures.find(t => t.id === treasureId);
if (treasure) {
treasure.collected = true;
treasure.chestOpen = true;
}
}
// Restore activated save points
for (const spId of saved.world.activatedSavePoints || []) {
const sp = world.savePoints.find(s => s.id === spId);
if (sp) {
sp.saved = true;
sp.active = true;
}
}
const ui = {
showInventory: false,
showShop: false,
showSaveMenu: false,
showGameOver: false,
showVictory: false,
message: "",
messageTimer: 0,
currentScreen: "game",
selectedSlot: 0
};
return { player, inventory, world, ui, saveData: saved };
}
function draw() {
p.background(20, 20, 30);
if (gameState.ui.showGameOver) {
(0, ui_1.drawGameOver)(p);
return;
}
if (gameState.ui.showVictory) {
(0, ui_1.drawVictory)(p, gameState.player);
return;
}
if (gameState.ui.currentScreen === "pause") {
drawPaused();
return;
}
// Update camera to follow player
updateCamera();
// Draw world
(0, world_1.drawWorld)(p, gameState.world, cameraX, cameraY);
// Draw pickups
drawPickups();
// Draw treasures
drawTreasures();
// Draw enemies
for (const enemy of gameState.world.enemies) {
if (enemy.health > 0) {
(0, combat_1.drawEnemy)(p, enemy, cameraX, cameraY);
}
}
// Draw player
(0, player_1.drawPlayer)(p, gameState.player, cameraX, cameraY);
// Draw projectiles
drawProjectiles();
// Update particles
(0, particles_1.updateParticles)();
(0, particles_1.drawParticles)(p);
// Draw HUD
drawHUD();
// Draw messages
drawLocalMessage(gameState.ui.message || "", gameState.ui.messageTimer, 300);
// Area name display
if (areaNameTimer > 0) {
(0, ui_1.drawAreaName)(p, gameState.world.currentArea, areaNameTimer);
areaNameTimer--;
}
// Tutorial
if (tutorialTimer > 0) {
tutorialTimer--;
if (tutorialTimer <= 0) {
tutorialVisible = false;
}
(0, ui_1.drawTutorial)(p, tutorialVisible);
}
}
function updateCamera() {
cameraX = gameState.player.x - state_1.CANVAS_WIDTH / 2;
cameraY = gameState.player.y - state_1.CANVAS_HEIGHT / 2;
// Clamp camera to world bounds
const worldWidth = gameState.world.tiles[0].length * 32;
const worldHeight = gameState.world.tiles.length * 32;
cameraX = Math.max(0, Math.min(cameraX, worldWidth - state_1.CANVAS_WIDTH));
cameraY = Math.max(0, Math.min(cameraY, worldHeight - state_1.CANVAS_HEIGHT));
}
function drawPickups() {
for (const pickup of gameState.world.pickups) {
if (pickup.collected)
continue;
const screenX = pickup.x * 32 - cameraX;
const screenY = pickup.y * 32 - cameraY;
// Bobbing animation
const bob = Math.sin(p.frameCount * 0.1) * 3;
switch (pickup.type) {
case "money":
p.fill(255, 215, 0);
p.circle(screenX + 16, screenY + 16 + bob, 14);
p.fill(255, 235, 59);
p.circle(screenX + 16, screenY + 16 + bob, 10);
p.fill(255, 215, 0);
p.textSize(10);
p.text("$", screenX + 13, screenY + 20 + bob);
break;
case "heart_container":
p.fill(233, 30, 99);
drawMiniHeart(screenX + 16, screenY + 16 + bob, 16);
break;
case "ranged_weapon":
p.fill(156, 39, 176);
p.rect(screenX + 8, screenY + 8 + bob, 16, 16, 4);
p.fill(255, 255, 255);
p.textSize(12);
p.text("🏹", screenX + 10, screenY + 20 + bob);
break;
case "item":
p.fill(100, 200, 100);
p.circle(screenX + 16, screenY + 16 + bob, 14);
p.fill(255, 255, 255);
p.textSize(10);
p.text("?", screenX + 13, screenY + 20 + bob);
break;
case "weapon_upgrade":
p.fill(255, 152, 0);
p.rect(screenX + 8, screenY + 8 + bob, 16, 16, 4);
p.fill(255, 255, 0);
p.textSize(12);
p.text("↑", screenX + 12, screenY + 20 + bob);
break;
}
}
}
function drawMiniHeart(p, x, y, size) {
p.push();
p.translate(x, y);
p.scale(size / 20);
p.fill(233, 30, 99);
p.noStroke();
p.beginShape();
p.vertex(0, -6);
p.bezierVertex(-4, -12, -12, -6, 0, 10);
p.bezierVertex(12, -6, 4, -12, 0, -6);
p.endShape();
p.pop();
}
function drawTreasures() {
for (const treasure of gameState.world.treasures) {
if (treasure.collected)
continue;
const screenX = treasure.x * 32 - cameraX;
const screenY = treasure.y * 32 - cameraY;
p.fill(treasure.chestOpen ? 139 : 184, 69, 19);
p.rect(screenX + 4, screenY + 10, 24, 16, 2);
p.fill(139, 69, 19);
p.rect(screenX + 6, screenY + 10, 20, 4);
}
}
function drawProjectiles() {
// Projectiles are handled in combat module
}
function drawHUD() {
// Health bar
(0, ui_1.drawHealthBar)(p, gameState.player, 15, 15);
// Money
(0, ui_1.drawMoney)(p, gameState.player.money, 15, 50);
// Weapon indicator
(0, ui_1.drawWeaponIndicator)(p, gameState.player.weaponLevel, gameState.player.hasRangedWeapon, gameState.player.ammo, 15, 80);
}
function handlePlayerMovement(dt) {
(0, player_1.handleInput)(gameState.player, p);
(0, player_1.updatePlayer)(gameState.player, dt);
// Check door collision
const doorResult = (0, world_1.handleDoorCollision)(gameState.world, gameState.player.x, gameState.player.y);
if (doorResult) {
changeArea(doorResult.destination, doorResult.x, doorResult.y);
}
// Check pickup collision
checkPickupCollision();
// Check treasure collision
checkTreasureCollision();
// Update enemies and check player damage
const attackResult = (0, combat_1.updateEnemies)(gameState.world, gameState.player.x, gameState.player.y, dt);
// Check if player took damage from enemies
for (const enemy of gameState.world.enemies) {
if (enemy.health > 0) {
const dx = gameState.player.x - enemy.x;
const dy = gameState.player.y - enemy.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 20 && enemy.attackCooldown > 40) { // Enemy just attacked
(0, player_1.takeDamage)(gameState.player, enemy.damage);
if (gameState.player.health <= 0) {
gameOver();
}
}
}
}
}
function keyPressed() {
if (gameState.ui.showGameOver || gameState.ui.showVictory) {
if (p.keyCode === 82) { // R key
restartGame();
}
return false;
}
if (gameState.ui.currentScreen === "pause") {
handlePauseInput();
return false;
}
if (p.keyCode === 27) { // ESC
togglePause();
return false;
}
if (p.keyCode === 73) { // I key
toggleInventory();
return false;
}
if (p.keyCode === 80) { // P key
toggleSound();
return false;
}
if (gameState.ui.showInventory) {
handleInventoryInput();
return false;
}
if (gameState.ui.showShop) {
handleShopInput();
return false;
}
if (gameState.ui.showSaveMenu) {
handleSaveInput();
return false;
}
// Game controls
if (p.keyCode === 32) { // Space - Attack
if (!gameState.player.isAttacking && gameState.player.attackCooldown <= 0) {
if (gameState.player.hasRangedWeapon && gameState.player.ammo > 0 && p.keyIsDown(p.SHIFT)) {
const proj = (0, player_1.performRangedAttack)(gameState.player);
if (proj) {
(0, combat_1.addProjectile)(proj.x, proj.y, proj.vx, proj.vy, 3);
}
}
else {
(0, player_1.performAttack)(gameState.player);
const hitEnemies = (0, combat_1.handlePlayerAttack)(gameState.world, gameState.player.x, gameState.player.y, gameState.player.facing, gameState.player.weaponLevel) || [];
// Check for deaths
for (const enemy of hitEnemies) {
const result = (0, combat_1.checkEnemyDeath)(enemy);
if (result.dead) {
collectEnemyDrop(result.dropMoney, result.dropItem, enemy.x, enemy.y);
// Remove dead enemy
const idx = gameState.world.enemies.indexOf(enemy);
if (idx > -1) {
gameState.world.enemies.splice(idx, 1);
}
}
}
}
}
return false;
}
if (p.keyCode === 69) { // E key - Interact
handleInteraction();
return false;
}
return false;
}
function handleInteraction() {
const player = gameState.player;
const world = gameState.world;
// Check for save point
for (const sp of world.savePoints) {
const dx = player.x - sp.x * 32;
const dy = player.y - sp.y * 32;
if (Math.sqrt(dx * dx + dy * dy) < 24) {
if (!sp.saved) {
(0, save_1.activateSavePoint)(world, sp.id);
(0, save_1.saveGame)(player, gameState.inventory, world);
(0, audio_1.playSound)("save");
showMessage("Game Saved!");
(0, particles_1.createExplosion)(sp.x * 32 + 16, sp.y * 32 + 16, "magic", 10);
}
else {
showMessage("Save Point (already saved)");
}
return;
}
}
// Check for shop
for (const shop of world.shops) {
const dx = player.x - shop.x * 32;
const dy = player.y - shop.y * 32;
if (Math.sqrt(dx * dx + dy * dy) < 24) {
openShop();
return;
}
}
}
function checkPickupCollision() {
const player = gameState.player;
for (const pickup of gameState.world.pickups) {
if (pickup.collected)
continue;
const dx = player.x - pickup.x * 32;
const dy = player.y - pickup.y * 32;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 24) {
collectPickup(pickup);
}
}
}
function collectPickup(pickup) {
pickup.collected = true;
(0, audio_1.playSound)("pickup");
switch (pickup.type) {
case "money":
const moneyMult = getAccessoryEffects().moneyBonus;
const amount = Math.floor((pickup.value || 10) * (1 + moneyMult));
gameState.player.money += amount;
(0, particles_1.createExplosion)(pickup.x * 32 + 16, pickup.y * 32 + 16, "coin", 8);
showMessage(`+${amount} Gold!`);
break;
case "heart_container":
(0, player_1.addHeartContainer)(gameState.player);
(0, particles_1.createExplosion)(pickup.x * 32 + 16, pickup.y * 32 + 16, "heart", 15);
showMessage("Heart Container! Max HP +2");
break;
case "ranged_weapon":
gameState.player.hasRangedWeapon = true;
gameState.player.ammo = 10;
(0, particles_1.createExplosion)(pickup.x * 32 + 16, pickup.y * 32 + 16, "magic", 15);
showMessage("Discovered Bow & Arrows!");
// Check if mid-game reached
if (!gameState.world.midGameReached) {
gameState.world.midGameReached = true;
showMessage("Mid-game reached! New areas unlocked!");
}
break;
case "item":
if (pickup.itemName) {
if ((0, inventory_1.isInventoryFull)(gameState.inventory)) {
showMessage("Inventory full!");
return;
}
if ((0, inventory_1.addItem)(gameState.inventory, pickup.itemName)) {
(0, particles_1.createExplosion)(pickup.x * 32 + 16, pickup.y * 32 + 16, "spark", 8);
showMessage(`Found: ${state_2.ITEMS[pickup.itemName]?.name || pickup.itemName}`);
}
}
break;
case "weapon_upgrade":
if (gameState.player.weaponLevel < 2) {
(0, player_1.upgradeWeapon)(gameState.player);
(0, particles_1.createExplosion)(pickup.x * 32 + 16, pickup.y * 32 + 16, "magic", 15);
showMessage(`Weapon Upgraded to Level ${gameState.player.weaponLevel}!`);
}
break;
}
}
function checkTreasureCollision() {
const player = gameState.player;
for (const treasure of gameState.world.treasures) {
if (treasure.collected)
continue;
const dx = player.x - treasure.x * 32;
const dy = player.y - treasure.y * 32;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 24) {
openTreasure(treasure);
}
}
}
function openTreasure(treasure) {
treasure.collected = true;
treasure.chestOpen = true;
(0, audio_1.playSound)("pickup");
if (treasure.item === "heart_container") {
(0, player_1.addHeartContainer)(gameState.player);
showMessage("Heart Container! Max HP +2");
}
else if (treasure.item) {
if ((0, inventory_1.isInventoryFull)(gameState.inventory)) {
showMessage("Inventory full!");
return;
}
if ((0, inventory_1.addItem)(gameState.inventory, treasure.item)) {
showMessage(`Found: ${state_2.ITEMS[treasure.item]?.name || treasure.item}`);
}
}
(0, particles_1.createExplosion)(treasure.x * 32 + 16, treasure.y * 32 + 16, "coin", 12);
}
function collectEnemyDrop(money, item, x, y) {
if (money > 0) {
gameState.player.money += money;
(0, particles_1.createExplosion)(x, y, "coin", 6);
}
if (item) {
if (!(0, inventory_1.isInventoryFull)(gameState.inventory)) {
if ((0, inventory_1.addItem)(gameState.inventory, item)) {
(0, particles_1.createExplosion)(x, y, "spark", 8);
}
}
}
}
function changeArea(area, x, y) {
(0, audio_1.playSound)("door_open");
gameState.world = (0, world_1.generateArea)(area);
gameState.player.x = x;
gameState.player.y = y;
areaNameTimer = 120;
// Show area name
const areaNames = {
"village": "Peaceful Village",
"forest": "Dark Forest",
"caves": "Mysterious Caves",
"temple": "Ancient Temple",
"boss_room": "Boss Arena"
};
if (area === "boss_room") {
// Check victory condition after boss defeat
}
}
function getAccessoryEffects() {
let defense = 0;
let speed = 0;
let moneyBonus = 0;
for (const acc of gameState.inventory.accessories) {
switch (acc) {
case "silver_ring":
defense += 1;
break;
case "speed_boots":
speed += 0.1;
break;
case "lucky_charm":
moneyBonus += 0.1;
break;
case "iron_shield":
defense += 2;
break;
}
}
return { defense, speed, moneyBonus };
}
function showMessage(msg) {
gameState.ui.message = msg;
gameState.ui.messageTimer = 120;
}
function drawLocalMessage(msg, timer, maxWidth) {
if (timer <= 0 || !msg)
return;
const alpha = Math.min(255, timer * 5);
p.push();
p.fill(0, 0, 0, alpha);
p.rectMode(p.CENTER);
p.rect(p.width / 2, p.height - 60, maxWidth + 20, 30, 8);
p.fill(255, 255, 255, alpha);
p.textAlign(p.CENTER);
p.textSize(14);
p.text(msg, p.width / 2, p.height - 55);
p.rectMode(p.CORNER);
p.pop();
}
// UI State management
function togglePause() {
if (gameState.ui.currentScreen === "pause") {
gameState.ui.currentScreen = "game";
}
else {
gameState.ui.currentScreen = "pause";
}
}
function drawPaused() {
(0, ui_1.drawPauseMenu)(p, state_1.CANVAS_WIDTH / 2 - 120, state_1.CANVAS_HEIGHT / 2 - 80, 240, 160, 0);
}
function handlePauseInput() {
if (p.keyCode === 27) { // ESC
togglePause();
}
}
function toggleInventory() {
gameState.ui.showInventory = !gameState.ui.showInventory;
gameState.ui.showShop = false;
gameState.ui.showSaveMenu = false;
}
function handleInventoryInput() {
if (p.keyCode === p.UP || p.keyCode === 87) {
gameState.ui.selectedSlot = Math.max(0, gameState.ui.selectedSlot - 1);
}
if (p.keyCode === p.DOWN || p.keyCode === 83) {
const totalSlots = (0, inventory_1.getTotalItemCount)(gameState.inventory);
gameState.ui.selectedSlot = Math.min(totalSlots - 1, gameState.ui.selectedSlot + 1);
}
if (p.keyCode === 13) { // Enter - use item
// Simplified: just consume first potion found
if ((0, inventory_1.hasItem)(gameState.inventory, "potion")) {
const result = (0, inventory_1.useItem)(gameState.inventory, "potion");
if (result.used && result.effect === "heal_4") {
(0, player_1.heal)(gameState.player, 4);
showMessage("Used Health Potion (+4 HP)");
}
}
}
}
function openShop() {
gameState.ui.showShop = true;
gameState.ui.showInventory = false;
gameState.ui.selectedSlot = 0;
}
function handleShopInput() {
const shop = gameState.world.shops[0];
if (!shop)
return;
if (p.keyCode === p.UP || p.keyCode === 87) {
gameState.ui.selectedSlot = Math.max(0, gameState.ui.selectedSlot - 1);
}
if (p.keyCode === p.DOWN || p.keyCode === 83) {
gameState.ui.selectedSlot = Math.min(shop.items.length - 1, gameState.ui.selectedSlot + 1);
}
if (p.keyCode === 13) { // Enter - buy
const item = shop.items[gameState.ui.selectedSlot];
if (item && gameState.player.money >= item.price) {
gameState.player.money -= item.price;
(0, audio_1.playSound)("shop_buy");
if (item.itemType === "potion") {
(0, inventory_1.addItem)(gameState.inventory, "potion");
showMessage("Bought Health Potion");
}
else if (state_2.ITEMS[item.itemType]) {
if ((0, inventory_1.isInventoryFull)(gameState.inventory)) {
showMessage("Inventory full!");
return;
}
(0, inventory_1.addItem)(gameState.inventory, item.itemType);
showMessage(`Bought ${item.name}`);
}
}
else {
showMessage("Not enough gold!");
}
}
}
function handleSaveInput() {
if (p.keyCode === 13) { // Enter - save
(0, save_1.saveGame)(gameState.player, gameState.inventory, gameState.world);
(0, audio_1.playSound)("save");
showMessage("Game Saved!");
gameState.ui.showSaveMenu = false;
}
}
function toggleSound() {
const currentlyEnabled = true; // Would track actual state
(0, audio_1.setEnabled)(!currentlyEnabled);
showMessage(currentlyEnabled ? "Sound OFF" : "Sound ON");
}
function restartGame() {
localStorage.removeItem("rpg_save_data");
gameState = createNewGame();
cameraX = 0;
cameraY = 0;
areaNameTimer = 120;
tutorialTimer = 600;
tutorialVisible = true;
}
function gameOver() {
(0, audio_1.playSound)("gameover");
gameState.ui.showGameOver = true;
gameState.ui.currentScreen = "gameover";
}
function checkVictory() {
// Check if boss is defeated and all heart containers collected
const bossAlive = gameState.world.enemies.some(e => e.type === "boss" && e.health > 0);
const allHearts = gameState.player.heartContainers >= 5;
if (!bossAlive && allHearts && gameState.world.currentArea === "boss_room") {
(0, audio_1.playSound)("victory");
gameState.ui.showVictory = true;
gameState.ui.currentScreen = "victory";
gameState.world.gameCompleted = true;
}
}
// Import ITEMS for reference
const state_2 = require("./state");
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// Global game state and constants
Object.defineProperty(exports, "__esModule", { value: true });
exports.TOTAL_AREAS = exports.areaProgress = exports.gameTime = exports.ENEMY_TYPES = exports.ITEMS = exports.WEAPON_UPGRADES = exports.CANVAS_HEIGHT = exports.CANVAS_WIDTH = exports.TILE_SIZE = void 0;
exports.TILE_SIZE = 32;
exports.CANVAS_WIDTH = 800;
exports.CANVAS_HEIGHT = 600;
exports.WEAPON_UPGRADES = [
{
name: "Sharpened Blade",
description: "A sharper edge for increased damage",
damageBonus: 2,
attackSpeedBonus: 0.2,
rangeBonus: 4,
specialAbility: "Faster attacks",
price: 150,
requiresItem: "Whetstone"
},
{
name: "Flame Edge",
description: "Enchanted with fire, burns enemies",
damageBonus: 4,
attackSpeedBonus: 0.4,
rangeBonus: 8,
specialAbility: "Fire damage over time",
price: 300,
requiresItem: "Fire Essence"
}
];
exports.ITEMS = {
"potion": { name: "Health Potion", description: "Restores 4 HP", type: "consumable" },
"whetstone": { name: "Whetstone", description: "Required for first weapon upgrade", type: "key_item" },
"fire_essence": { name: "Fire Essence", description: "Required for second weapon upgrade", type: "key_item" },
"silver_ring": { name: "Silver Ring", description: "Accessory: +1 defense", type: "accessory" },
"speed_boots": { name: "Speed Boots", description: "Accessory: +10% movement speed", type: "accessory" },
"lucky_charm": { name: "Lucky Charm", description: "Accessory: +10% money gain", type: "accessory" },
"iron_shield": { name: "Iron Shield", description: "Accessory: Blocks ranged attacks", type: "accessory" },
"ancient_scroll": { name: "Ancient Scroll", description: "Key Item: Opens the shrine", type: "key_item" }
};
exports.ENEMY_TYPES = {
"slime": { health: 4, damage: 1, speed: 0.5, money: 10, color: [76, 175, 80] },
"bat": { health: 3, damage: 1, speed: 1.5, money: 8, color: [121, 85, 72] },
"skeleton": { health: 6, damage: 2, speed: 0.8, money: 20, color: [236, 240, 241] },
"guard": { health: 10, damage: 3, speed: 0.6, money: 35, color: [92, 107, 192] },
"boss": { health: 30, damage: 5, speed: 0.4, money: 100, color: [183, 28, 28] }
};
// Game progress tracking
exports.gameTime = 0;
exports.areaProgress = 0;
exports.TOTAL_AREAS = 5;
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
// Audio system using Web Audio API for synthesized sounds
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.setEnabled = setEnabled;
exports.playSound = playSound;
let audioContext = null;
let enabled = true;
function initAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
function setEnabled(value) {
enabled = value;
}
function playTone(frequency, duration, type, volume, attack = 0.01, decay = 0.1) {
if (!enabled || !audioContext)
return;
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + attack);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration - decay);
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + duration);
}
function playNoise(duration, volume) {
if (!enabled || !audioContext)
return;
const bufferSize = audioContext.sampleRate * duration;
const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = audioContext.createBufferSource();
noise.buffer = buffer;
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(volume, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
noise.connect(gainNode);
gainNode.connect(audioContext.destination);
noise.start();
}
function playSound(type) {
switch (type) {
case "step":
playTone(80, 0.05, "triangle", 0.1);
break;
case "attack":
playTone(300, 0.1, "sawtooth", 0.2, 0.01, 0.08);
playTone(200, 0.1, "square", 0.15, 0.01, 0.08);
break;
case "hit":
playNoise(0.15, 0.3);
playTone(100, 0.1, "sawtooth", 0.25);
break;
case "enemy_hit":
playTone(150, 0.1, "square", 0.3);
playTone(100, 0.15, "sawtooth", 0.25);
break;
case "pickup":
playTone(600, 0.1, "sine", 0.3);
setTimeout(() => playTone(800, 0.15, "sine", 0.3), 50);
break;
case "coin":
playTone(1200, 0.1, "sine", 0.2);
setTimeout(() => playTone(1600, 0.15, "sine", 0.25), 40);
setTimeout(() => playTone(2000, 0.2, "sine", 0.3), 80);
break;
case "heart":
playTone(400, 0.15, "sine", 0.3);
setTimeout(() => playTone(600, 0.2, "sine", 0.35), 100);
break;
case "upgrade":
playTone(300, 0.1, "sine", 0.2);
setTimeout(() => playTone(400, 0.1, "sine", 0.2), 80);
setTimeout(() => playTone(500, 0.1, "sine", 0.2), 160);
setTimeout(() => playTone(700, 0.3, "sine", 0.3), 240);
break;
case "shop_buy":
playTone(800, 0.1, "sine", 0.25);
setTimeout(() => playTone(1000, 0.15, "sine", 0.3), 60);
break;
case "save":
playTone(400, 0.1, "sine", 0.2);
setTimeout(() => playTone(500, 0.1, "sine", 0.2), 100);
setTimeout(() => playTone(600, 0.1, "sine", 0.2), 200);
setTimeout(() => playTone(800, 0.3, "sine", 0.3), 300);
break;
case "door_open":
playNoise(0.2, 0.15);
playTone(100, 0.2, "sine", 0.2);
break;
case "damage":
playTone(150, 0.2, "sawtooth", 0.35);
playTone(100, 0.25, "square", 0.3);
break;
case "ranged_attack":
playTone(600, 0.15, "sine", 0.25);
playTone(800, 0.1, "sine", 0.2);
break;
case "projectile_hit":
playNoise(0.1, 0.25);
playTone(400, 0.08, "sawtooth", 0.2);
break;
case "victory":
for (let i = 0; i < 5; i++) {
setTimeout(() => playTone(400 + i * 100, 0.2, "sine", 0.3), i * 100);
}
setTimeout(() => playTone(800, 0.4, "sine", 0.4), 500);
break;
case "gameover":
playTone(300, 0.3, "sawtooth", 0.3);
setTimeout(() => playTone(250, 0.3, "sawtooth", 0.3), 200);
setTimeout(() => playTone(200, 0.4, "sawtooth", 0.3), 400);
break;
}
}
};
// ── module: src/particles.ts ──
__mods["src/particles.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createParticle = createParticle;
exports.createExplosion = createExplosion;
exports.updateParticles = updateParticles;
exports.drawParticles = drawParticles;
exports.clearParticles = clearParticles;
exports.getParticleCount = getParticleCount;
const particles = [];
function createParticle(x, y, type, vx = (Math.random() - 0.5) * 4, vy = (Math.random() - 0.5) * 4, life = 30, size = 4) {
const colors = {
"spark": [255, 235, 59],
"dust": [189, 189, 189],
"blood": [211, 47, 47],
"magic": [156, 39, 176],
"coin": [255, 215, 0],
"heart": [233, 30, 99]
};
particles.push({
x,
y,
vx,
vy,
life,
maxLife: life,
size,
color: colors[type],
type
});
}
function createExplosion(x, y, type, count) {
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 / count) * i;
const speed = 2 + Math.random() * 2;
createParticle(x, y, type, Math.cos(angle) * speed, Math.sin(angle) * speed, 20 + Math.random() * 15, 3 + Math.random() * 3);
}
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life--;
// Apply gravity to some types
if (p.type === "dust" || p.type === "coin") {
p.vy += 0.1;
}
// Friction
p.vx *= 0.95;
p.vy *= 0.95;
if (p.life <= 0) {
particles.splice(i, 1);
}
}
}
function drawParticles(p) {
for (const particle of particles) {
const alpha = particle.life / particle.maxLife;
p.push();
p.noStroke();
p.fill(p.color[0], p.color[1], p.color[2], alpha * 255);
if (particle.type === "heart") {
drawHeart(p, particle.x, particle.y, particle.size * alpha);
}
else {
p.circle(p.x, p.y, particle.size * alpha);
}
p.pop();
}
}
function drawHeart(p, x, y, size) {
p.push();
p.translate(x, y);
p.scale(size / 10);
p.beginShape();
p.vertex(0, -3);
p.bezierVertex(-2, -6, -6, -3, 0, 5);
p.bezierVertex(6, -3, 2, -6, 0, -3);
p.endShape();
p.pop();
}
function clearParticles() {
particles.length = 0;
}
function getParticleCount() {
return particles.length;
}
};
// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TILE = void 0;
exports.generateArea = generateArea;
exports.getTileAt = getTileAt;
exports.checkCollision = checkCollision;
exports.drawWorld = drawWorld;
exports.handleDoorCollision = handleDoorCollision;
const state_1 = require("./state");
// Tile types
exports.TILE = {
EMPTY: 0,
WALL: 1,
FLOOR: 2,
WATER: 3,
GRASS: 4,
SAVE_POINT: 5,
SHOP: 6,
DOOR_NORTH: 7,
DOOR_SOUTH: 8,
DOOR_WEST: 9,
DOOR_EAST: 10,
CHEST: 11,
BOSS_DOOR: 12
};
const AREAS = {
"village": {
name: "Peaceful Village",
width: 25,
height: 20,
tileMap: [],
enemies: [
{ type: "slime", x: 5, y: 5, health: 4, maxHealth: 4, damage: 1, speed: 0.5, patrolStartX: 5, patrolStartY: 5, patrolRadius: 3, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 10 },
{ type: "slime", x: 18, y: 8, health: 4, maxHealth: 4, damage: 1, speed: 0.5, patrolStartX: 18, patrolStartY: 8, patrolRadius: 4, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 10 }
],
pickups: [
{ type: "money", x: 10, y: 10, value: 25, collected: false },
{ type: "item", x: 3, y: 15, itemName: "potion", collected: false }
],
shops: [
{ x: 20, y: 5, items: [
{ name: "Health Potion", description: "Restores 4 HP", price: 20, itemType: "potion" },
{ name: "Silver Ring", description: "+1 defense", price: 100, itemType: "silver_ring" },
{ name: "Speed Boots", description: "+10% movement", price: 150, itemType: "speed_boots" }
], open: true }
],
savePoints: [{ x: 12, y: 3, saved: false, active: true }],
doors: [
{ x: 12, y: 18, width: 2, height: 1, destination: "forest", destinationX: 12, destinationY: 2, locked: false }
],
treasures: [
{ x: 2, y: 5, item: "whetstone", value: 50, collected: false, chestOpen: false }
]
},
"forest": {
name: "Dark Forest",
width: 30,
height: 25,
tileMap: [],
enemies: [
{ type: "bat", x: 8, y: 8, health: 3, maxHealth: 3, damage: 1, speed: 1.5, patrolStartX: 8, patrolStartY: 8, patrolRadius: 5, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 8 },
{ type: "bat", x: 20, y: 12, health: 3, maxHealth: 3, damage: 1, speed: 1.5, patrolStartX: 20, patrolStartY: 12, patrolRadius: 4, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 8 },
{ type: "skeleton", x: 15, y: 18, health: 6, maxHealth: 6, damage: 2, speed: 0.8, patrolStartX: 15, patrolStartY: 18, patrolRadius: 6, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 20 }
],
pickups: [
{ type: "money", x: 5, y: 5, value: 30, collected: false },
{ type: "money", x: 25, y: 20, value: 50, collected: false },
{ type: "item", x: 10, y: 22, itemName: "lucky_charm", collected: false }
],
shops: [],
savePoints: [{ x: 15, y: 5, saved: false, active: true }],
doors: [
{ x: 15, y: 23, width: 2, height: 1, destination: "caves", destinationX: 15, destinationY: 2, locked: false },
{ x: 2, y: 12, width: 1, height: 2, destination: "village", destinationX: 23, destinationY: 12, locked: false }
],
treasures: [
{ x: 28, y: 8, item: "fire_essence", value: 100, collected: false, chestOpen: false },
{ x: 5, y: 18, item: "iron_shield", value: 75, collected: false, chestOpen: false }
]
},
"caves": {
name: "Mysterious Caves",
width: 25,
height: 20,
tileMap: [],
enemies: [
{ type: "skeleton", x: 6, y: 6, health: 6, maxHealth: 6, damage: 2, speed: 0.8, patrolStartX: 6, patrolStartY: 6, patrolRadius: 4, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 20 },
{ type: "skeleton", x: 18, y: 10, health: 6, maxHealth: 6, damage: 2, speed: 0.8, patrolStartX: 18, patrolStartY: 10, patrolRadius: 5, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 20 },
{ type: "guard", x: 12, y: 15, health: 10, maxHealth: 10, damage: 3, speed: 0.6, patrolStartX: 12, patrolStartY: 15, patrolRadius: 7, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 35 }
],
pickups: [
{ type: "money", x: 8, y: 12, value: 40, collected: false },
{ type: "money", x: 16, y: 6, value: 60, collected: false },
{ type: "item", x: 20, y: 16, itemName: "ancient_scroll", collected: false }
],
shops: [
{ x: 4, y: 4, items: [
{ name: "Health Potion", description: "Restores 4 HP", price: 20, itemType: "potion" },
{ name: "Lucky Charm", description: "+10% money gain", price: 200, itemType: "lucky_charm" },
{ name: "Iron Shield", description: "Blocks ranged attacks", price: 180, itemType: "iron_shield" }
], open: true }
],
savePoints: [{ x: 12, y: 3, saved: false, active: true }],
doors: [
{ x: 12, y: 18, width: 2, height: 1, destination: "temple", destinationX: 12, destinationY: 2, requiredItem: "ancient_scroll", locked: true }
],
treasures: [
{ x: 2, y: 10, item: "silver_ring", value: 100, collected: false, chestOpen: false }
]
},
"temple": {
name: "Ancient Temple",
width: 30,
height: 25,
tileMap: [],
enemies: [
{ type: "guard", x: 8, y: 8, health: 10, maxHealth: 10, damage: 3, speed: 0.6, patrolStartX: 8, patrolStartY: 8, patrolRadius: 5, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 35 },
{ type: "guard", x: 20, y: 10, health: 10, maxHealth: 10, damage: 3, speed: 0.6, patrolStartX: 20, patrolStartY: 10, patrolRadius: 6, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 35 },
{ type: "guard", x: 15, y: 18, health: 10, maxHealth: 10, damage: 3, speed: 0.6, patrolStartX: 15, patrolStartY: 18, patrolRadius: 4, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 35 }
],
pickups: [
{ type: "money", x: 10, y: 10, value: 75, collected: false },
{ type: "money", x: 20, y: 18, value: 100, collected: false },
{ type: "ranged_weapon", x: 15, y: 12, value: 0, collected: false }
],
shops: [],
savePoints: [{ x: 15, y: 4, saved: false, active: true }],
doors: [
{ x: 15, y: 23, width: 2, height: 1, destination: "boss_room", destinationX: 15, destinationY: 3, locked: false }
],
treasures: [
{ x: 25, y: 8, item: "speed_boots", value: 150, collected: false, chestOpen: false },
{ x: 5, y: 18, item: "heart_container", value: 200, collected: false, chestOpen: false }
]
},
"boss_room": {
name: "Boss Arena",
width: 20,
height: 15,
tileMap: [],
enemies: [
{ type: "boss", x: 10, y: 8, health: 30, maxHealth: 30, damage: 5, speed: 0.4, patrolStartX: 10, patrolStartY: 8, patrolRadius: 8, direction: 0, attackCooldown: 0, isAggro: false, dropMoney: 100, dropItem: "heart_container" }
],
pickups: [
{ type: "money", x: 5, y: 5, value: 100, collected: false },
{ type: "money", x: 15, y: 10, value: 100, collected: false },
{ type: "heart_container", x: 10, y: 11, value: 500, collected: false }
],
shops: [],
savePoints: [],
doors: [
{ x: 9, y: 13, width: 2, height: 1, destination: "temple", destinationX: 9, destinationY: 12, locked: false }
],
treasures: []
}
};
function generateArea(areaName) {
const config = AREAS[areaName];
if (!config)
throw new Error(`Unknown area: ${areaName}`);
// Generate tile map with borders and some obstacles
const tiles = [];
for (let y = 0; y < config.height; y++) {
const row = [];
for (let x = 0; x < config.width; x++) {
if (x === 0 || x === config.width - 1 || y === 0 || y === config.height - 1) {
row.push(exports.TILE.WALL);
}
else {
row.push(Math.random() < 0.1 ? exports.TILE.WALL : exports.TILE.FLOOR);
}
}
tiles.push(row);
}
// Clear paths for doors
for (const door of config.doors) {
const dh = door.height || 1;
const dw = door.width || 1;
const dx0 = door.x || 0;
const dy0 = door.y || 0;
for (let dy = 0; dy < dh; dy++) {
for (let dx = 0; dx < dw; dx++) {
const tx = Math.floor(dx0 + dx);
const ty = Math.floor(dy0 + dy);
if (tx > 0 && tx < config.width - 1 && ty > 0 && ty < config.height - 1) {
tiles[ty][tx] = exports.TILE.FLOOR;
}
}
}
}
const world = {
tiles,
enemies: config.enemies.map((e, i) => ({ ...e, id: `${areaName}_enemy_${i}`, type: e.type })),
pickups: config.pickups.map((p, i) => ({ ...p, id: `${areaName}_pickup_${i}`, collected: false, type: p.type })),
shops: config.shops.map((s, i) => ({ ...s, id: `${areaName}_shop_${i}`, x: s.x, y: s.y, items: s.items || [], open: s.open !== false })),
savePoints: config.savePoints.map((s, i) => ({ ...s, id: `${areaName}_save_${i}`, saved: false, active: true, x: s.x, y: s.y })),
doors: config.doors.map((d, i) => ({ ...d, id: `${areaName}_door_${i}`, x: d.x, y: d.y, width: d.width, height: d.height, destination: d.destination, destinationX: d.destinationX, destinationY: d.destinationY, locked: d.locked !== false })),
treasures: config.treasures.map((t, i) => ({ ...t, id: `${areaName}_treasure_${i}`, collected: false, chestOpen: false, x: t.x, y: t.y, item: t.item, value: t.value })),
currentArea: areaName,
areasUnlocked: [areaName],
midGameReached: false,
gameCompleted: false
};
return world;
}
function getTileAt(world, x, y) {
const tx = Math.floor(x / state_1.TILE_SIZE);
const ty = Math.floor(y / state_1.TILE_SIZE);
if (ty < 0 || ty >= world.tiles.length || tx < 0 || tx >= world.tiles[0].length) {
return exports.TILE.WALL;
}
return world.tiles[ty][tx];
}
function checkCollision(world, x, y, radius = 8) {
const points = [
{ x: x - radius, y: y - radius },
{ x: x + radius, y: y - radius },
{ x: x - radius, y: y + radius },
{ x: x + radius, y: y + radius }
];
for (const p of points) {
const tile = getTileAt(world, p.x, p.y);
if (tile === exports.TILE.WALL || tile === exports.TILE.WATER) {
return true;
}
}
return false;
}
function drawWorld(p, world, cameraX, cameraY) {
const startX = Math.floor(cameraX / state_1.TILE_SIZE);
const startY = Math.floor(cameraY / state_1.TILE_SIZE);
const endX = startX + Math.ceil(p.width / state_1.TILE_SIZE) + 1;
const endY = startY + Math.ceil(p.height / state_1.TILE_SIZE) + 1;
for (let y = Math.max(0, startY); y < Math.min(world.tiles.length, endY); y++) {
for (let x = Math.max(0, startX); x < Math.min(world.tiles[0].length, endX); x++) {
const tile = world.tiles[y][x];
const screenX = x * state_1.TILE_SIZE - cameraX;
const screenY = y * state_1.TILE_SIZE - cameraY;
switch (tile) {
case exports.TILE.EMPTY:
p.fill(30, 30, 40);
p.rect(screenX, screenY, state_1.TILE_SIZE, state_1.TILE_SIZE);
break;
case exports.TILE.WALL:
p.fill(80, 80, 90);
p.rect(screenX, screenY, state_1.TILE_SIZE, state_1.TILE_SIZE);
p.fill(60, 60, 70);
p.rect(screenX + 2, screenY + 2, state_1.TILE_SIZE - 4, state_1.TILE_SIZE - 4);
break;
case exports.TILE.FLOOR:
p.fill(50, 50, 60);
p.rect(screenX, screenY, state_1.TILE_SIZE, state_1.TILE_SIZE);
if ((x + y) % 3 === 0) {
p.fill(60, 60, 70);
p.rect(screenX + 8, screenY + 8, 4, 4);
}
break;
case exports.TILE.WATER:
p.fill(52, 152, 219);
p.rect(screenX, screenY, state_1.TILE_SIZE, state_1.TILE_SIZE);
break;
case exports.TILE.GRASS:
p.fill(76, 175, 80);
p.rect(screenX, screenY, state_1.TILE_SIZE, state_1.TILE_SIZE);
break;
}
}
}
// Draw save points
for (const sp of world.savePoints) {
const screenX = sp.x * state_1.TILE_SIZE - cameraX;
const screenY = sp.y * state_1.TILE_SIZE - cameraY;
p.fill(156, 39, 176);
p.circle(screenX + state_1.TILE_SIZE / 2, screenY + state_1.TILE_SIZE / 2, 20);
p.fill(255, 255, 255);
p.textSize(12);
p.text("S", screenX + state_1.TILE_SIZE / 2 - 4, screenY + state_1.TILE_SIZE / 2 + 4);
}
// Draw shops
for (const shop of world.shops) {
const screenX = shop.x * state_1.TILE_SIZE - cameraX;
const screenY = shop.y * state_1.TILE_SIZE - cameraY;
p.fill(255, 193, 7);
p.rect(screenX + 4, screenY + 4, state_1.TILE_SIZE - 8, state_1.TILE_SIZE - 8);
p.fill(0, 0, 0);
p.textSize(12);
p.text("$", screenX + state_1.TILE_SIZE / 2 - 4, screenY + state_1.TILE_SIZE / 2 + 4);
}
// Draw doors
for (const door of world.doors) {
const screenX = door.x * state_1.TILE_SIZE - cameraX;
const screenY = door.y * state_1.TILE_SIZE - cameraY;
p.fill(door.locked ? 128 : 64, 0, 64);
p.rect(screenX, screenY, door.width * state_1.TILE_SIZE, door.height * state_1.TILE_SIZE);
if (door.locked) {
p.fill(255, 215, 0);
p.textSize(10);
p.text("L", screenX + 4, screenY + 16);
}
}
// Draw treasures
for (const treasure of world.treasures) {
if (treasure.collected)
continue;
const screenX = treasure.x * state_1.TILE_SIZE - cameraX;
const screenY = treasure.y * state_1.TILE_SIZE - cameraY;
p.fill(treasure.chestOpen ? 139 : 184, 69, 19);
p.rect(screenX + 4, screenY + 8, 24, 16);
p.fill(139, 69, 19);
p.rect(screenX + 6, screenY + 8, 20, 4);
}
}
function handleDoorCollision(world, playerX, playerY) {
for (const door of world.doors) {
if (playerX >= door.x * state_1.TILE_SIZE &&
playerX <= (door.x + door.width) * state_1.TILE_SIZE &&
playerY >= door.y * state_1.TILE_SIZE &&
playerY <= (door.y + door.height) * state_1.TILE_SIZE) {
if (door.locked && door.requiredItem) {
continue;
}
return {
destination: door.destination,
x: door.destinationX * state_1.TILE_SIZE + state_1.TILE_SIZE / 2,
y: door.destinationY * state_1.TILE_SIZE + state_1.TILE_SIZE / 2
};
}
}
return undefined;
}
};
// ── module: src/player.ts ──
__mods["src/player.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createPlayer = createPlayer;
exports.updatePlayer = updatePlayer;
exports.handleInput = handleInput;
exports.performAttack = performAttack;
exports.performRangedAttack = performRangedAttack;
exports.takeDamage = takeDamage;
exports.heal = heal;
exports.addHeartContainer = addHeartContainer;
exports.upgradeWeapon = upgradeWeapon;
exports.getAttackRange = getAttackRange;
exports.getAttackDamage = getAttackDamage;
exports.getAttackCooldown = getAttackCooldown;
exports.drawPlayer = drawPlayer;
exports.getWeaponStats = getWeaponStats;
const world_1 = require("./world");
const audio_1 = require("./audio");
const particles_1 = require("./particles");
let stepTimer = 0;
let lastStepSound = 0;
function createPlayer(x, y) {
return {
x,
y,
vx: 0,
vy: 0,
speed: 3,
health: 6, // Start with 3 heart containers (2 HP each)
maxHealth: 6,
heartContainers: 3,
weaponLevel: 0,
hasRangedWeapon: false,
ammo: 0,
money: 0,
direction: 0,
isAttacking: false,
attackCooldown: 0,
invincibilityTimer: 0,
animationFrame: 0,
animTimer: 0,
facing: Math.PI / 2 // Facing down
};
}
function updatePlayer(player, dt) {
// Update attack cooldown
if (player.attackCooldown > 0) {
player.attackCooldown -= dt;
}
// Update invincibility timer
if (player.invincibilityTimer > 0) {
player.invincibilityTimer -= dt;
}
// Animation
player.animTimer += dt;
if (player.animTimer > 10) {
player.animTimer = 0;
player.animationFrame = (player.animationFrame + 1) % 4;
}
}
function handleInput(player, p) {
const prevX = player.x;
const prevY = player.y;
player.vx = 0;
player.vy = 0;
if (p.keyIsDown(p.LEFT) || p.keyIsDown(65)) { // A key
player.vx = -player.speed;
player.facing = Math.PI;
player.direction = 2;
}
if (p.keyIsDown(p.RIGHT) || p.keyIsDown(68)) { // D key
player.vx = player.speed;
player.facing = 0;
player.direction = 3;
}
if (p.keyIsDown(p.UP) || p.keyIsDown(87)) { // W key
player.vy = -player.speed;
player.facing = -Math.PI / 2;
player.direction = 1;
}
if (p.keyIsDown(p.DOWN) || p.keyIsDown(83)) { // S key
player.vy = player.speed;
player.facing = Math.PI / 2;
player.direction = 0;
}
// Normalize diagonal movement
if (player.vx !== 0 && player.vy !== 0) {
player.vx *= 0.707;
player.vy *= 0.707;
}
// Apply movement with collision detection
const newX = player.x + player.vx;
const newY = player.y + player.vy;
// Check X collision
if (!(0, world_1.checkCollision)({ tiles: [] }, newX, player.y, 10)) {
player.x = newX;
}
// Check Y collision
if (!(0, world_1.checkCollision)({ tiles: [] }, player.x, newY, 10)) {
player.y = newY;
}
// Step sound when moving
const moved = Math.abs(player.x - prevX) > 0.5 || Math.abs(player.y - prevY) > 0.5;
if (moved) {
stepTimer += Math.abs(player.vx) + Math.abs(player.vy);
if (stepTimer > 15) {
stepTimer = 0;
const now = Date.now();
if (now - lastStepSound > 200) {
(0, audio_1.playSound)("step");
lastStepSound = now;
}
}
}
}
function performAttack(player) {
if (player.attackCooldown <= 0 && !player.isAttacking) {
player.isAttacking = true;
player.attackCooldown = getAttackCooldown(player.weaponLevel);
(0, audio_1.playSound)("attack");
return true;
}
return false;
}
function performRangedAttack(player) {
if (player.attackCooldown <= 0 && player.hasRangedWeapon && player.ammo > 0) {
player.ammo--;
player.attackCooldown = 20;
(0, audio_1.playSound)("ranged_attack");
return {
x: player.x,
y: player.y,
vx: Math.cos(player.facing) * 6,
vy: Math.sin(player.facing) * 6
};
}
return null;
}
function takeDamage(player, damage) {
if (player.invincibilityTimer > 0) {
return false;
}
player.health -= damage;
player.invincibilityTimer = 60; // 1 second at 60 FPS
(0, audio_1.playSound)("damage");
(0, particles_1.createExplosion)(player.x, player.y, "blood", 8);
return true;
}
function heal(player, amount) {
const oldHealth = player.health;
player.health = Math.min(player.health + amount, player.maxHealth);
const healed = player.health - oldHealth;
if (healed > 0) {
(0, audio_1.playSound)("heart");
(0, particles_1.createExplosion)(player.x, player.y, "heart", 5);
}
return healed;
}
function addHeartContainer(player) {
if (player.heartContainers < 5) {
player.heartContainers++;
player.maxHealth += 2;
player.health = player.maxHealth;
(0, audio_1.playSound)("upgrade");
(0, particles_1.createExplosion)(player.x, player.y, "heart", 10);
}
}
function upgradeWeapon(player) {
if (player.weaponLevel < 2) {
player.weaponLevel++;
(0, audio_1.playSound)("upgrade");
(0, particles_1.createExplosion)(player.x, player.y, "magic", 15);
return true;
}
return false;
}
function getAttackRange(weaponLevel) {
const baseRange = 24;
const upgrades = [0, 4, 8];
return baseRange + upgrades[weaponLevel];
}
function getAttackDamage(weaponLevel) {
const baseDamage = 2;
const upgrades = [0, 2, 4];
return baseDamage + upgrades[weaponLevel];
}
function getAttackCooldown(weaponLevel) {
const baseCooldown = 25;
const upgrades = [0, -5, -10];
return Math.max(10, baseCooldown + upgrades[weaponLevel]);
}
function drawPlayer(p, player, cameraX, cameraY) {
const screenX = player.x - cameraX;
const screenY = player.y - cameraY;
p.push();
p.translate(screenX, screenY);
// Blink when invincible
if (player.invincibilityTimer > 0 && Math.floor(player.invincibilityTimer / 5) % 2 === 0) {
p.pop();
return;
}
// Draw shadow
p.fill(0, 0, 0, 100);
p.ellipse(0, 12, 20, 10);
// Draw body
p.fill(52, 152, 219);
p.rect(-10, -8, 20, 16, 4);
// Draw head
p.fill(255, 213, 174);
p.circle(0, -12, 14);
// Draw eyes based on direction
p.fill(0, 0, 0);
if (player.direction === 0) { // Down
p.circle(-4, -12, 3);
p.circle(4, -12, 3);
}
else if (player.direction === 1) { // Up
p.circle(-4, -14, 3);
p.circle(4, -14, 3);
}
else if (player.direction === 2) { // Left
p.circle(-6, -13, 3);
}
else { // Right
p.circle(6, -13, 3);
}
// Draw weapon
if (player.isAttacking) {
p.push();
p.rotate(player.facing);
// Weapon appearance changes with level
if (player.weaponLevel === 0) {
// Basic sword
p.fill(189, 189, 189);
p.rect(8, -2, 16, 4);
p.fill(101, 160, 64);
p.rect(8, -3, 4, 6);
}
else if (player.weaponLevel === 1) {
// Sharpened blade with glow
p.fill(255, 255, 255);
p.rect(8, -2, 20, 4);
p.fill(255, 235, 59);
p.rect(8, -3, 4, 6);
// Glow effect
p.fill(255, 235, 59, 100);
p.ellipse(18, 0, 12, 8);
}
else {
// Flame edge
p.fill(255, 255, 255);
p.rect(8, -2, 24, 4);
p.fill(255, 87, 34);
p.rect(8, -3, 4, 6);
// Fire effect
p.fill(255, 152, 0, 150);
p.ellipse(20, 0, 16, 10);
p.fill(255, 235, 59, 100);
p.ellipse(24, 0, 12, 8);
}
p.pop();
}
else {
// Idle weapon position
p.push();
p.rotate(player.facing + 0.3);
if (player.weaponLevel === 0) {
p.fill(189, 189, 189);
p.rect(8, -2, 12, 3);
}
else if (player.weaponLevel === 1) {
p.fill(255, 255, 255);
p.rect(8, -2, 14, 3);
}
else {
p.fill(255, 255, 255);
p.rect(8, -2, 16, 3);
p.fill(255, 87, 34, 100);
p.ellipse(16, 0, 8, 5);
}
p.pop();
}
// Ranged weapon indicator
if (player.hasRangedWeapon) {
p.fill(156, 39, 176);
p.rect(-12, 8, 8, 4);
p.fill(255, 255, 255);
p.textSize(8);
p.text(player.ammo, -8, 12);
}
p.pop();
}
function getWeaponStats(weaponLevel) {
return {
damage: getAttackDamage(weaponLevel),
range: getAttackRange(weaponLevel),
speed: 1 / (getAttackCooldown(weaponLevel) / 60)
};
}
};
// ── module: src/combat.ts ──
__mods["src/combat.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addProjectile = addProjectile;
exports.updateProjectiles = updateProjectiles;
exports.clearProjectiles = clearProjectiles;
exports.updateEnemies = updateEnemies;
exports.handlePlayerAttack = handlePlayerAttack;
exports.checkEnemyDeath = checkEnemyDeath;
exports.drawEnemy = drawEnemy;
exports.getProjectileCount = getProjectileCount;
const state_1 = require("./state");
const player_1 = require("./player");
const state_2 = require("./state");
const audio_1 = require("./audio");
const particles_1 = require("./particles");
const projectiles = [];
function addProjectile(x, y, vx, vy, damage) {
projectiles.push({
x,
y,
vx,
vy,
damage,
life: 60
});
}
function updateProjectiles(world, onHit) {
for (let i = projectiles.length - 1; i >= 0; i--) {
const proj = projectiles[i];
proj.x += proj.vx;
proj.y += proj.vy;
proj.life--;
// Check collision with enemies
let hit = false;
for (const enemy of world.enemies) {
const dx = proj.x - enemy.x;
const dy = proj.y - enemy.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 15) {
if (onHit(proj.x, proj.y, enemy)) {
(0, particles_1.createExplosion)(proj.x, proj.y, "spark", 5);
(0, audio_1.playSound)("projectile_hit");
hit = true;
}
break;
}
}
if (hit || proj.life <= 0) {
projectiles.splice(i, 1);
}
}
}
function clearProjectiles() {
projectiles.length = 0;
}
function updateEnemies(world, playerX, playerY, dt) {
for (const enemy of world.enemies) {
const stats = state_2.ENEMY_TYPES[enemy.type];
// Update attack cooldown
if (enemy.attackCooldown > 0) {
enemy.attackCooldown -= dt;
}
// Calculate distance to player
const dx = playerX - enemy.x;
const dy = playerY - enemy.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Aggro range
const aggroRange = 80;
if (dist < aggroRange && !enemy.isAggro) {
enemy.isAggro = true;
}
if (enemy.isAggro) {
// Move towards player
const angle = Math.atan2(dy, dx);
const moveSpeed = enemy.speed;
const newX = enemy.x + Math.cos(angle) * moveSpeed;
const newY = enemy.y + Math.sin(angle) * moveSpeed;
// Simple collision check
if (!isWallCollision(world, newX, enemy.y, 10)) {
enemy.x = newX;
}
if (!isWallCollision(world, enemy.x, newY, 10)) {
enemy.y = newY;
}
// Attack player if close enough
const newDist = Math.sqrt((playerX - enemy.x) ** 2 + (playerY - enemy.y) ** 2);
if (newDist < 20 && enemy.attackCooldown <= 0) {
enemy.attackCooldown = 60;
return { x: enemy.x, y: enemy.y, damage: enemy.damage };
}
}
else {
// Patrol behavior
const patrolDx = enemy.patrolStartX - enemy.x;
const patrolDy = enemy.patrolStartY - enemy.y;
const patrolDist = Math.sqrt(patrolDx * patrolDx + patrolDy * patrolDy);
if (patrolDist > enemy.patrolRadius * state_1.TILE_SIZE * 0.5) {
const angle = Math.atan2(patrolDy, patrolDx);
enemy.x += Math.cos(angle) * enemy.speed * 0.5;
enemy.y += Math.sin(angle) * enemy.speed * 0.5;
}
}
}
return null;
}
function isWallCollision(world, x, y, radius = 8) {
const points = [
{ x: x - radius, y: y - radius },
{ x: x + radius, y: y - radius },
{ x: x - radius, y: y + radius },
{ x: x + radius, y: y + radius }
];
for (const p of points) {
const tile = getTileAt(world, p.x, p.y);
if (tile === 1 || tile === 3) { // WALL or WATER
return true;
}
}
return false;
}
function getTileAt(world, x, y) {
const tx = Math.floor(x / state_1.TILE_SIZE);
const ty = Math.floor(y / state_1.TILE_SIZE);
if (ty < 0 || ty >= world.tiles.length || tx < 0 || tx >= world.tiles[0].length) {
return 1; // WALL
}
return world.tiles[ty][tx];
}
function handlePlayerAttack(world, playerX, playerY, facing, weaponLevel) {
const damage = (0, player_1.getAttackDamage)(weaponLevel);
const range = (0, player_1.getAttackRange)(weaponLevel);
const hitEnemies = [];
// Create attack arc
const arcWidth = Math.PI / 2;
const startAngle = facing - arcWidth / 2;
for (const enemy of world.enemies) {
const dx = enemy.x - playerX;
const dy = enemy.y - playerY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist <= range + 10) {
const angleToEnemy = Math.atan2(dy, dx);
let angleDiff = angleToEnemy - facing;
// Normalize angle
while (angleDiff > Math.PI)
angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI)
angleDiff += Math.PI * 2;
if (Math.abs(angleDiff) < arcWidth / 2 + 0.3) {
enemy.health -= damage;
hitEnemies.push(enemy);
// Knockback
enemy.x += Math.cos(facing) * 10;
enemy.y += Math.sin(facing) * 10;
(0, particles_1.createExplosion)(enemy.x, enemy.y, "spark", 4);
(0, audio_1.playSound)("enemy_hit");
}
}
}
return hitEnemies;
}
function checkEnemyDeath(enemy) {
if (enemy.health <= 0) {
(0, particles_1.createExplosion)(enemy.x, enemy.y, "spark", 10);
(0, audio_1.playSound)("pickup");
return {
dead: true,
dropMoney: enemy.dropMoney,
dropItem: enemy.dropItem
};
}
return { dead: false, dropMoney: 0 };
}
function drawEnemy(p, enemy, cameraX, cameraY) {
const screenX = enemy.x - cameraX;
const screenY = enemy.y - cameraY;
const stats = state_2.ENEMY_TYPES[enemy.type];
p.push();
p.translate(screenX, screenY);
// Draw shadow
p.fill(0, 0, 0, 100);
p.ellipse(0, 12, 20, 10);
// Draw enemy based on type
p.fill(stats.color[0], stats.color[1], stats.color[2]);
switch (enemy.type) {
case "slime":
// Slime body
p.beginShape();
p.vertex(-12, 8);
p.bezierVertex(-14, -4, 14, -4, 12, 8);
p.bezierVertex(10, 12, -10, 12, -12, 8);
p.endShape();
// Eyes
p.fill(0, 0, 0);
p.circle(-5, 2, 4);
p.circle(5, 2, 4);
break;
case "bat":
// Bat wings
p.fill(stats.color[0] + 20, stats.color[1], stats.color[2]);
p.beginShape();
p.vertex(-8, 0);
p.bezierVertex(-20, -10, -25, 5, -10, 4);
p.vertex(8, 0);
p.bezierVertex(25, 5, 20, -10, 8, 0);
p.endShape();
// Body
p.fill(stats.color[0], stats.color[1], stats.color[2]);
p.circle(0, 2, 12);
// Eyes
p.fill(255, 0, 0);
p.circle(-4, 0, 3);
p.circle(4, 0, 3);
break;
case "skeleton":
// Skeleton body
p.fill(236, 240, 241);
p.circle(0, -4, 14); // Head
p.rect(-6, 6, 12, 12); // Body
// Ribs
p.stroke(100, 100, 100);
p.strokeWeight(2);
p.line(-4, 8, 4, 8);
p.line(-4, 10, 4, 10);
p.noStroke();
// Eye sockets
p.fill(0, 0, 0);
p.circle(-4, -6, 4);
p.circle(4, -6, 4);
break;
case "guard":
// Armor
p.fill(92, 107, 192);
p.rect(-10, -6, 20, 20, 4);
// Helmet
p.fill(79, 107, 192);
p.circle(0, -10, 14);
// Visor
p.fill(0, 0, 0);
p.rect(-6, -12, 12, 4);
// Red eyes
p.fill(255, 0, 0);
p.circle(-3, -11, 2);
p.circle(3, -11, 2);
break;
case "boss":
// Boss is bigger
p.scale(1.5);
p.fill(183, 28, 28);
p.rect(-10, -8, 20, 24, 6);
// Horns
p.fill(100, 0, 0);
p.triangle(-12, -12, -8, -20, -6, -10);
p.triangle(12, -12, 8, -20, 6, -10);
// Angry eyes
p.fill(255, 255, 0);
p.circle(-5, -2, 5);
p.circle(5, -2, 5);
// Mouth
p.fill(0, 0, 0);
p.rect(-6, 8, 12, 4);
break;
}
// Health bar
const healthPercent = enemy.health / enemy.maxHealth;
p.fill(50, 50, 50);
p.rect(-12, -20, 24, 4);
p.fill(76, 175, 80);
p.rect(-12, -20, 24 * healthPercent, 4);
p.pop();
}
function getProjectileCount() {
return projectiles.length;
}
};
// ── module: src/inventory.ts ──
__mods["src/inventory.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInventory = createInventory;
exports.addItem = addItem;
exports.removeItem = removeItem;
exports.hasItem = hasItem;
exports.getItemCount = getItemCount;
exports.useItem = useItem;
exports.equipAccessory = equipAccessory;
exports.unequipAccessory = unequipAccessory;
exports.getAccessoryEffects = getAccessoryEffects;
exports.getTotalItemCount = getTotalItemCount;
exports.isInventoryFull = isInventoryFull;
exports.drawInventory = drawInventory;
exports.drawShop = drawShop;
const state_1 = require("./state");
function createInventory() {
return {
items: new Map(),
accessories: [],
keyItems: [],
maxSlots: 12
};
}
function addItem(inventory, itemKey, quantity = 1) {
const itemInfo = state_1.ITEMS[itemKey];
if (!itemInfo)
return false;
if (itemInfo.type === "accessory") {
// Accessories can only be equipped once
if (!inventory.accessories.includes(itemKey)) {
inventory.accessories.push(itemKey);
return true;
}
return false;
}
if (itemInfo.type === "key_item") {
if (!inventory.keyItems.includes(itemKey)) {
inventory.keyItems.push(itemKey);
return true;
}
return false;
}
// Consumables stack
const current = inventory.items.get(itemKey) || 0;
inventory.items.set(itemKey, current + quantity);
return true;
}
function removeItem(inventory, itemKey, quantity = 1) {
const current = inventory.items.get(itemKey) || 0;
if (current >= quantity) {
const newAmount = current - quantity;
if (newAmount <= 0) {
inventory.items.delete(itemKey);
}
else {
inventory.items.set(itemKey, newAmount);
}
return true;
}
return false;
}
function hasItem(inventory, itemKey, quantity = 1) {
const itemInfo = state_1.ITEMS[itemKey];
if (!itemInfo)
return false;
if (itemInfo.type === "accessory") {
return inventory.accessories.includes(itemKey);
}
if (itemInfo.type === "key_item") {
return inventory.keyItems.includes(itemKey);
}
return (inventory.items.get(itemKey) || 0) >= quantity;
}
function getItemCount(inventory, itemKey) {
return inventory.items.get(itemKey) || 0;
}
function useItem(inventory, itemKey) {
const itemInfo = state_1.ITEMS[itemKey];
if (!itemInfo)
return { used: false };
if (itemInfo.type !== "consumable") {
return { used: false, effect: "Cannot use this item" };
}
if (!removeItem(inventory, itemKey, 1)) {
return { used: false, effect: "Item not found" };
}
let effect = "";
switch (itemKey) {
case "potion":
effect = "heal_4";
break;
}
return { used: true, effect };
}
function equipAccessory(inventory, itemKey) {
const itemInfo = state_1.ITEMS[itemKey];
if (!itemInfo || itemInfo.type !== "accessory")
return false;
if (!inventory.accessories.includes(itemKey)) {
inventory.accessories.push(itemKey);
return true;
}
return false;
}
function unequipAccessory(inventory, itemKey) {
const index = inventory.accessories.indexOf(itemKey);
if (index > -1) {
inventory.accessories.splice(index, 1);
return true;
}
return false;
}
function getAccessoryEffects(inventory) {
let defense = 0;
let speed = 0;
let moneyBonus = 0;
for (const accessory of inventory.accessories) {
switch (accessory) {
case "silver_ring":
defense += 1;
break;
case "speed_boots":
speed += 0.1;
break;
case "lucky_charm":
moneyBonus += 0.1;
break;
case "iron_shield":
defense += 2;
break;
}
}
return { defense, speed, moneyBonus };
}
function getTotalItemCount(inventory) {
let count = 0;
for (const [_, qty] of inventory.items) {
count += qty;
}
count += inventory.accessories.length;
count += inventory.keyItems.length;
return count;
}
function isInventoryFull(inventory) {
return getTotalItemCount(inventory) >= inventory.maxSlots;
}
function drawInventory(p, inventory, x, y, width, height, selectedSlot) {
// Background
p.fill(30, 30, 40, 240);
p.rect(x, y, width, height, 8);
p.stroke(100, 100, 120);
p.strokeWeight(2);
p.noFill();
p.rect(x, y, width, height, 8);
p.noStroke();
// Title
p.fill(255, 255, 255);
p.textSize(16);
p.text("INVENTORY", x + width / 2, y + 20);
// Slot size
const slotSize = 40;
const slotsPerRow = Math.floor((width - 40) / (slotSize + 10));
let slotIndex = 0;
// Draw consumable items
p.textSize(12);
for (const [itemKey, qty] of inventory.items) {
const col = slotIndex % slotsPerRow;
const row = Math.floor(slotIndex / slotsPerRow);
const slotX = x + 20 + col * (slotSize + 10);
const slotY = y + 35 + row * (slotSize + 10);
// Slot background
p.fill(50, 50, 60);
if (slotIndex === selectedSlot) {
p.fill(70, 70, 90);
p.stroke(255, 215, 0);
p.strokeWeight(2);
}
else {
p.noStroke();
}
p.rect(slotX, slotY, slotSize, slotSize, 4);
p.noStroke();
// Item icon (simple representation)
p.fill(100, 200, 100);
p.circle(slotX + slotSize / 2, slotY + slotSize / 2 - 2, 12);
// Quantity
p.fill(255, 255, 255);
p.textSize(10);
p.text(qty.toString(), slotX + slotSize - 8, slotY + slotSize - 4);
slotIndex++;
}
// Draw accessories
p.textSize(12);
p.fill(200, 200, 255);
p.text("Accessories:", x + 20, y + 35 + Math.ceil(inventory.items.size / slotsPerRow) * (slotSize + 10) + 10);
let accIndex = 0;
for (const acc of inventory.accessories) {
const col = accIndex % slotsPerRow;
const row = Math.floor(accIndex / slotsPerRow);
const slotX = x + 20 + col * (slotSize + 10);
const slotY = y + 35 + Math.ceil(inventory.items.size / slotsPerRow) * (slotSize + 10) + 25 + row * (slotSize + 10);
p.fill(50, 50, 80);
p.rect(slotX, slotY, slotSize, slotSize, 4);
// Accessory icon
p.fill(150, 150, 255);
p.rect(slotX + 10, slotY + 10, 20, 20, 2);
accIndex++;
}
// Draw key items
p.textSize(12);
p.fill(255, 200, 100);
const keyY = y + 35 + Math.ceil(inventory.items.size / slotsPerRow) * (slotSize + 10) + 25 + Math.ceil(inventory.accessories.length / slotsPerRow) * (slotSize + 10) + 10;
p.text("Key Items:", x + 20, keyY);
let keyIndex = 0;
for (const key of inventory.keyItems) {
const col = keyIndex % slotsPerRow;
const row = Math.floor(keyIndex / slotsPerRow);
const slotX = x + 20 + col * (slotSize + 10);
const slotY = keyY + 15 + row * (slotSize + 10);
p.fill(80, 60, 40);
p.rect(slotX, slotY, slotSize, slotSize, 4);
// Key icon
p.fill(255, 215, 0);
p.circle(slotX + slotSize / 2, slotY + slotSize / 2, 10);
keyIndex++;
}
// Instructions
p.fill(150, 150, 150);
p.textSize(10);
p.text("I - Close | Arrow keys - Select", x + 20, y + height - 10);
}
function drawShop(p, shopItems, x, y, width, height, playerMoney, selectedSlot) {
// Background
p.fill(30, 30, 40, 240);
p.rect(x, y, width, height, 8);
p.stroke(200, 180, 50);
p.strokeWeight(2);
p.noFill();
p.rect(x, y, width, height, 8);
p.noStroke();
// Title
p.fill(255, 215, 0);
p.textSize(16);
p.text("SHOP", x + width / 2, y + 20);
// Money display
p.fill(100, 255, 100);
p.textSize(14);
p.text(`Gold: ${playerMoney}`, x + 20, y + 40);
// Shop items
const slotSize = 35;
const startY = y + 55;
for (let i = 0; i < shopItems.length && i < 6; i++) {
const item = shopItems[i];
const itemY = startY + i * (slotSize + 8);
// Slot background
p.fill(50, 50, 60);
if (i === selectedSlot) {
p.fill(70, 70, 90);
p.stroke(255, 215, 0);
p.strokeWeight(2);
}
else {
p.noStroke();
}
p.rect(x + 20, itemY, width - 40, slotSize, 4);
p.noStroke();
// Item name
p.fill(255, 255, 255);
p.textSize(12);
p.text(item.name, x + 30, itemY + 14);
// Price
p.fill(255, 215, 0);
p.text(`${item.price} G`, x + width - 50, itemY + 14);
// Can afford indicator
if (playerMoney >= item.price) {
p.fill(100, 255, 100);
p.text("✓", x + width - 80, itemY + 14);
}
else {
p.fill(255, 100, 100);
p.text("✗", x + width - 80, itemY + 14);
}
}
// Instructions
p.fill(150, 150, 150);
p.textSize(10);
p.text("I - Close | Enter - Buy | Arrows - Select", x + 20, y + height - 10);
}
};
// ── module: src/save.ts ──
__mods["src/save.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveGame = saveGame;
exports.loadGame = loadGame;
exports.hasSave = hasSave;
exports.deleteSave = deleteSave;
exports.activateSavePoint = activateSavePoint;
exports.isSavePointActive = isSavePointActive;
exports.getSaveTimestamp = getSaveTimestamp;
exports.formatSaveTime = formatSaveTime;
const SAVE_KEY = "rpg_save_data";
function saveGame(player, inventory, world) {
const saveData = {
player: {
x: player.x,
y: player.y,
health: player.health,
maxHealth: player.maxHealth,
heartContainers: player.heartContainers,
weaponLevel: player.weaponLevel,
hasRangedWeapon: player.hasRangedWeapon,
ammo: player.ammo,
money: player.money
},
inventory: {
items: Object.fromEntries(inventory.items),
accessories: [...inventory.accessories],
keyItems: [...inventory.keyItems]
},
world: {
currentArea: world.currentArea,
areasUnlocked: [...world.areasUnlocked],
midGameReached: world.midGameReached,
collectedPickups: world.pickups.filter(p => p.collected).map(p => p.id),
openedTreasures: world.treasures.filter(t => t.collected).map(t => t.id),
activatedSavePoints: world.savePoints.filter(sp => sp.saved).map(sp => sp.id)
},
timestamp: Date.now()
};
try {
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
return saveData;
}
catch (e) {
console.error("Failed to save game:", e);
return saveData;
}
}
function loadGame() {
try {
const saved = localStorage.getItem(SAVE_KEY);
if (!saved)
return null;
const data = JSON.parse(saved);
return data;
}
catch (e) {
console.error("Failed to load game:", e);
return null;
}
}
function hasSave() {
try {
return localStorage.getItem(SAVE_KEY) !== null;
}
catch {
return false;
}
}
function deleteSave() {
try {
localStorage.removeItem(SAVE_KEY);
}
catch (e) {
console.error("Failed to delete save:", e);
}
}
function activateSavePoint(world, savePointId) {
for (const sp of world.savePoints) {
if (sp.id === savePointId) {
sp.saved = true;
sp.active = true;
break;
}
}
}
function isSavePointActive(world, savePointId) {
for (const sp of world.savePoints) {
if (sp.id === savePointId) {
return sp.active && sp.saved;
}
}
return false;
}
function getSaveTimestamp() {
try {
const saved = localStorage.getItem(SAVE_KEY);
if (!saved)
return null;
const data = JSON.parse(saved);
return data.timestamp;
}
catch {
return null;
}
}
function formatSaveTime(timestamp) {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${month}/${day} ${hours}:${minutes}`;
}
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.drawHealthBar = drawHealthBar;
exports.drawMoney = drawMoney;
exports.drawWeaponIndicator = drawWeaponIndicator;
exports.drawMessage = drawMessage;
exports.drawPauseMenu = drawPauseMenu;
exports.drawGameOver = drawGameOver;
exports.drawVictory = drawVictory;
exports.drawTutorial = drawTutorial;
exports.drawAreaName = drawAreaName;
exports.drawInteractionPrompt = drawInteractionPrompt;
function drawHealthBar(p, player, x, y) {
const heartSize = 24;
const spacing = 4;
p.push();
p.translate(x, y);
// Draw heart containers
for (let i = 0; i < 5; i++) {
const heartX = i * (heartSize + spacing);
const isFullContainer = i < Math.floor(player.heartContainers);
const isInHalf = player.heartContainers % 1 !== 0 && i === Math.floor(player.heartContainers);
p.push();
p.translate(heartX, 0);
if (i < player.heartContainers) {
// Full or partial heart
const healthInThisContainer = player.health - i * 2;
if (healthInThisContainer >= 2) {
// Full heart
drawFullHeart(p, 0, 0, heartSize);
}
else if (healthInThisContainer > 0) {
// Half heart
drawHalfHeart(p, 0, 0, heartSize);
}
else {
// Empty (shouldn't happen but just in case)
drawEmptyHeart(p, 0, 0, heartSize);
}
}
else {
// Empty heart container outline
drawEmptyHeart(p, 0, 0, heartSize);
}
p.pop();
}
p.pop();
}
function drawFullHeart(p, x, y, size) {
p.push();
p.translate(x, y);
p.scale(size / 20);
p.fill(233, 30, 99);
p.noStroke();
p.beginShape();
p.vertex(0, -6);
p.bezierVertex(-4, -12, -12, -6, 0, 10);
p.bezierVertex(12, -6, 4, -12, 0, -6);
p.endShape();
p.pop();
}
function drawHalfHeart(p, x, y, size) {
p.push();
p.translate(x, y);
p.scale(size / 20);
// Left half (filled)
p.fill(233, 30, 99);
p.noStroke();
p.beginShape();
p.vertex(0, -6);
p.bezierVertex(-4, -12, -12, -6, 0, 10);
p.vertex(0, 10);
p.vertex(0, -6);
p.endShape();
// Right half (empty outline)
p.stroke(150, 50, 70);
p.strokeWeight(1);
p.noFill();
p.beginShape();
p.vertex(0, -6);
p.bezierVertex(4, -12, 12, -6, 0, 10);
p.endShape();
p.pop();
}
function drawEmptyHeart(p, x, y, size) {
p.push();
p.translate(x, y);
p.scale(size / 20);
p.noFill();
p.stroke(150, 50, 70);
p.strokeWeight(1.5);
p.beginShape();
p.vertex(0, -6);
p.bezierVertex(-4, -12, -12, -6, 0, 10);
p.bezierVertex(12, -6, 4, -12, 0, -6);
p.endShape();
p.pop();
}
function drawMoney(p, money, x, y) {
p.push();
p.translate(x, y);
p.fill(255, 215, 0);
p.textSize(14);
p.text(`G ${money}`, 0, 0);
// Gold coin icon
p.fill(255, 215, 0);
p.circle(-28, 0, 16);
p.fill(255, 235, 59);
p.circle(-28, 0, 12);
p.fill(255, 215, 0);
p.textSize(10);
p.text("$", -31, 4);
p.pop();
}
function drawWeaponIndicator(p, weaponLevel, hasRanged, ammo, x, y) {
p.push();
p.translate(x, y);
// Weapon level indicator
p.fill(150, 150, 150);
p.rect(0, 0, 60, 20, 4);
p.fill(100, 200, 100);
for (let i = 0; i < weaponLevel; i++) {
p.rect(4 + i * 18, 3, 14, 14, 2);
p.fill(255, 255, 0);
p.textSize(10);
p.text("↑", 8 + i * 18, 14);
p.fill(100, 200, 100);
}
// Ranged weapon indicator
if (hasRanged) {
p.fill(156, 39, 176);
p.rect(70, 0, 50, 20, 4);
p.fill(255, 255, 255);
p.textSize(12);
p.text(`🏹 ${ammo}`, 75, 14);
}
p.pop();
}
function drawMessage(_p, _message, _timer, _maxWidth) {
if (_timer <= 0 || !_message)
return;
const alpha = Math.min(255, _timer * 5);
_p.push();
_p.fill(0, 0, 0, alpha);
_p.rectMode(_p.CENTER);
_p.rect(_p.width / 2, _p.height - 60, _maxWidth + 20, 30, 8);
_p.fill(255, 255, 255, alpha);
_p.textAlign(_p.CENTER);
_p.textSize(14);
_p.text(_message, _p.width / 2, _p.height - 55);
_p.rectMode(_p.CORNER);
_p.pop();
}
function drawPauseMenu(p, x, y, width, height, selectedOption) {
// Background overlay
p.fill(0, 0, 0, 180);
p.rect(0, 0, p.width, p.height);
// Menu box
p.fill(40, 40, 50, 240);
p.rect(x, y, width, height, 10);
p.stroke(100, 100, 120);
p.strokeWeight(2);
p.noFill();
p.rect(x, y, width, height, 10);
p.noStroke();
// Title
p.fill(255, 255, 255);
p.textAlign(p.CENTER);
p.textSize(20);
p.text("PAUSED", x + width / 2, y + 35);
// Options
const options = ["Resume", "Save Game", "Options", "Quit"];
p.textAlign(p.LEFT);
p.textSize(16);
for (let i = 0; i < options.length; i++) {
const optionY = y + 60 + i * 35;
if (i === selectedOption) {
p.fill(100, 200, 100);
p.text("> " + options[i], x + 30, optionY);
}
else {
p.fill(200, 200, 200);
p.text(options[i], x + 40, optionY);
}
}
p.textAlign(p.LEFT);
}
function drawGameOver(p) {
// Dark overlay
p.fill(0, 0, 0, 200);
p.rect(0, 0, p.width, p.height);
// Game Over text
p.fill(200, 50, 50);
p.textAlign(p.CENTER);
p.textSize(48);
p.text("GAME OVER", p.width / 2, p.height / 2 - 30);
p.fill(200, 200, 200);
p.textSize(18);
p.text("Press R to restart", p.width / 2, p.height / 2 + 20);
p.textAlign(p.LEFT);
}
function drawVictory(p, player) {
// Golden overlay
p.fill(50, 40, 20, 200);
p.rect(0, 0, p.width, p.height);
// Victory text
p.fill(255, 215, 0);
p.textAlign(p.CENTER);
p.textSize(48);
p.text("VICTORY!", p.width / 2, p.height / 2 - 60);
p.fill(255, 255, 255);
p.textSize(20);
p.text("You have conquered the realm!", p.width / 2, p.height / 2 - 10);
p.textSize(16);
p.text(`Final Gold: ${player.money}`, p.width / 2, p.height / 2 + 30);
p.text("Press R to play again", p.width / 2, p.height / 2 + 60);
p.textAlign(p.LEFT);
}
function drawTutorial(p, visible) {
if (!visible)
return;
p.push();
p.fill(0, 0, 0, 150);
p.rect(10, p.height - 120, 200, 110, 8);
p.fill(255, 255, 255);
p.textSize(12);
p.text("WASD/Arrows - Move", 20, p.height - 100);
p.text("SPACE - Attack", 20, p.height - 85);
p.text("I - Inventory", 20, p.height - 70);
p.text("E - Interact", 20, p.height - 55);
p.text("ESC - Pause", 20, p.height - 40);
p.text("P - Toggle Sound", 20, p.height - 25);
p.pop();
}
function drawAreaName(p, areaName, timer) {
if (timer <= 0)
return;
const alpha = Math.min(255, timer * 8);
p.push();
p.fill(0, 0, 0, alpha);
p.rectMode(p.CENTER);
p.rect(p.width / 2, 50, 200, 40, 8);
p.fill(255, 255, 255, alpha);
p.textAlign(p.CENTER);
p.textSize(16);
p.text(areaName, p.width / 2, 55);
p.rectMode(p.CORNER);
p.pop();
}
function drawInteractionPrompt(p, prompt, x, y) {
p.push();
p.fill(0, 0, 0, 180);
p.rect(x, y, 80, 25, 4);
p.fill(255, 255, 255);
p.textAlign(p.CENTER);
p.textSize(12);
p.text(prompt, x + 40, y + 17);
p.textAlign(p.LEFT);
p.pop();
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.5-122b-a10b","provider":"openrouter","persona":"typescript","sessionId":"9149beba-e5dc-4911-a17d-2f6782772ff4","tokensIn":2709224,"tokensOut":44643,"tokensTotal":2753867,"cost":0.79725568,"turns":54,"toolCalls":53,"failedToolCalls":2,"timestamp":"2026-08-05T05:37:33.135Z"} -->