From fbcc3a91d920c3a35abcc246b47412a572d62626 Mon Sep 17 00:00:00 2001 From: John Kenyon Date: Sat, 29 Nov 2025 17:33:30 -0800 Subject: [PATCH] Work in progress --- index.html | 64 ++++++++ src/Entity.js | 76 ++++++++++ src/Game.js | 409 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/Item.js | 18 +++ src/Map.js | 138 +++++++++++++++++ src/UI.js | 189 +++++++++++++++++++++++ src/main.js | 6 + style.css | 127 ++++++++++++++++ 8 files changed, 1027 insertions(+) create mode 100644 index.html create mode 100644 src/Entity.js create mode 100644 src/Game.js create mode 100644 src/Item.js create mode 100644 src/Map.js create mode 100644 src/UI.js create mode 100644 src/main.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..0bdd0df --- /dev/null +++ b/index.html @@ -0,0 +1,64 @@ + + + + + + + Castle of the Winds Clone + + + + +
+
+
Castle of the Winds Clone
+
+ + + +
+
+
+
+
+ +
+ +
+
+
Welcome to the Castle of the Winds Clone!
+
+
+
+

Ready

+
+
+ + + + \ No newline at end of file diff --git a/src/Entity.js b/src/Entity.js new file mode 100644 index 0000000..53ab1f1 --- /dev/null +++ b/src/Entity.js @@ -0,0 +1,76 @@ +export class Entity { + constructor(x, y, name, symbol, color) { + this.x = x; + this.y = y; + this.name = name; + this.symbol = symbol; + this.color = color; + } +} + +export class Player extends Entity { + constructor(x, y) { + super(x, y, 'Player', '@', '#ffffff'); + this.hp = 10; + this.maxHp = 10; + this.inventory = []; + this.equipment = { + weapon: null, + armor: null, + shield: null + }; + this.stats = { + str: 10, + dex: 10 + }; + this.level = 1; + this.xp = 0; + this.maxXp = 10; + this.gold = 0; + } + + gainXp(amount) { + this.xp += amount; + if (this.xp >= this.maxXp) { + this.level++; + this.xp -= this.maxXp; + this.maxXp = Math.floor(this.maxXp * 1.5); + this.maxHp += 5; + this.hp = this.maxHp; + this.stats.str += 2; + return true; // Leveled up + } + return false; + } + + getDamage() { + let dmg = Math.floor(this.stats.str / 5); // Base damage + if (dmg < 1) dmg = 1; + if (this.equipment.weapon) { + dmg += this.equipment.weapon.stats.damage || 0; + } + return dmg; + } + + getDefense() { + let def = 0; + if (this.equipment.shield) { + def += this.equipment.shield.stats.defense || 0; + } + // Could add armor here later + return def; + } +} + +export class Monster extends Entity { + constructor(x, y, name, symbol, color, level) { + super(x, y, name, symbol, color); + this.level = level; + this.hp = level * 5; + this.maxHp = this.hp; + } + + getDefense() { + return Math.floor(this.level / 2); // Slight natural armor for higher level monsters + } +} diff --git a/src/Game.js b/src/Game.js new file mode 100644 index 0000000..8839641 --- /dev/null +++ b/src/Game.js @@ -0,0 +1,409 @@ +import { Map } from './Map.js'; +import { UI } from './UI.js'; +import { Player } from './Entity.js'; + +import { Monster } from './Entity.js'; +import { Item } from './Item.js'; + +export class Game { + constructor() { + this.canvas = document.getElementById('game-canvas'); + this.ctx = this.canvas.getContext('2d'); + this.ui = new UI(this.ctx, this); // Pass game instance to UI for callbacks + this.map = new Map(50, 50); + this.player = new Player(10, 10); + this.monsters = []; + this.items = []; + this.gameOver = false; + this.monsters = []; + this.items = []; + this.gameOver = false; + this.depth = 0; // Start in Town + this.maxDepth = 3; + + this.tileSize = 32; + + // Bind input + window.addEventListener('keydown', (e) => this.handleInput(e)); + } + + start() { + console.log("Game Started"); + this.generateLevel(); + this.ui.log("Welcome to the Town of Oakhaven!"); + } + + generateLevel() { + this.monsters = []; + this.items = []; + + if (this.depth === 0) { + this.map.generateTown(); + // Spawn player in center + this.player.x = Math.floor(this.map.width / 2); + this.player.y = Math.floor(this.map.height / 2); + } else { + this.map.generate(); + // Spawn Player in first room + if (this.map.rooms.length > 0) { + const startRoom = this.map.rooms[0]; + this.player.x = Math.floor(startRoom.x + startRoom.w / 2); + this.player.y = Math.floor(startRoom.y + startRoom.h / 2); + } + } + + this.spawnMonsters(); + this.spawnItems(); + this.spawnStairs(); + + this.render(); + this.ui.updateStats(this.player, this.depth); + this.ui.updateInventory(this.player); + } + + nextLevel() { + if (this.depth >= this.maxDepth) { + this.ui.log("You have reached the bottom! You Win!"); + setTimeout(() => alert("You Win!"), 100); + return; + } + + this.depth++; + this.ui.log(`You descend to level ${this.depth}...`); + this.generateLevel(); + } + + spawnStairs() { + if (this.depth === 0) { + // Stairs in a random building or just random spot + if (this.map.rooms.length > 0) { + const room = this.map.rooms[0]; + const sx = Math.floor(Math.random() * room.w) + room.x; + const sy = Math.floor(Math.random() * room.h) + room.y; + this.map.tiles[sy][sx] = '>'; + } + return; + } + + // Spawn stairs in the last room + if (this.map.rooms.length > 0) { + const room = this.map.rooms[this.map.rooms.length - 1]; + const sx = Math.floor(Math.random() * room.w) + room.x; + const sy = Math.floor(Math.random() * room.h) + room.y; + this.map.tiles[sy][sx] = '>'; + } + } + + spawnMonsters() { + if (this.depth === 0) { + // Spawn Peasants in Town + for (let i = 0; i < 10; i++) { + const mx = Math.floor(Math.random() * this.map.width); + const my = Math.floor(Math.random() * this.map.height); + if (this.map.isWalkable(mx, my)) { + const peasant = new Monster(mx, my, "Peasant", "p", "#00ffff", 1); + this.monsters.push(peasant); + } + } + return; + } + + // Skip first room + for (let i = 1; i < this.map.rooms.length; i++) { + const room = this.map.rooms[i]; + // Spawn 1-3 monsters per room + const count = Math.floor(Math.random() * 3) + 1; + for (let j = 0; j < count; j++) { + const mx = Math.floor(Math.random() * room.w) + room.x; + const my = Math.floor(Math.random() * room.h) + room.y; + + const type = Math.random(); + let monster; + + // Difficulty scaling + if (this.depth === 1) { + if (type < 0.7) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1); + else monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); + } else if (this.depth === 2) { + if (type < 0.4) monster = new Monster(mx, my, "Rat", "r", "#a0a0a0", 1); + else if (type < 0.8) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); + else monster = new Monster(mx, my, "Orc", "o", "#008000", 3); + } else { + if (type < 0.3) monster = new Monster(mx, my, "Kobold", "k", "#00ff00", 2); + else if (type < 0.7) monster = new Monster(mx, my, "Orc", "o", "#008000", 3); + else monster = new Monster(mx, my, "Ogre", "O", "#ff0000", 4); + } + + this.monsters.push(monster); + } + } + + // Spawn Boss on final level + if (this.depth === this.maxDepth) { + const room = this.map.rooms[this.map.rooms.length - 1]; // Last room + const mx = Math.floor(Math.random() * room.w) + room.x; + const my = Math.floor(Math.random() * room.h) + room.y; + // Boss: Level 10, High HP + const boss = new Monster(mx, my, "The Dungeon Lord", "D", "#ff00ff", 10); + boss.hp = 100; + boss.maxHp = 100; + this.monsters.push(boss); + } + } + + spawnItems() { + if (this.depth === 0) return; // No items in town + + // Spawn items in random rooms + for (let i = 0; i < this.map.rooms.length; i++) { + const room = this.map.rooms[i]; + if (Math.random() < 0.5) { + const ix = Math.floor(Math.random() * room.w) + room.x; + const iy = Math.floor(Math.random() * room.h) + room.y; + + // Random item + const type = Math.random(); + let item; + + // Better loot deeper + let tier = this.depth; + if (Math.random() < 0.2) tier++; // Chance for higher tier + + if (type < 0.3) item = new Item("Potion", "potion", 1, { heal: 5 }); + else if (type < 0.5) { + if (tier <= 1) item = new Item("Dagger", "weapon", 1, { damage: 4 }); + else if (tier === 2) item = new Item("Short Sword", "weapon", 2, { damage: 6 }); + else item = new Item("Long Sword", "weapon", 3, { damage: 8 }); + } else if (type < 0.7) { + if (tier <= 1) item = new Item("Buckler", "shield", 1, { defense: 1 }); + else if (tier === 2) item = new Item("Wooden Shield", "shield", 2, { defense: 2 }); + else item = new Item("Kite Shield", "shield", 3, { defense: 3 }); + } else { + if (tier >= 3) item = new Item("Great Axe", "weapon", 4, { damage: 12 }); + else item = new Item("Short Sword", "weapon", 2, { damage: 6 }); + } + + item.x = ix; + item.y = iy; + this.items.push(item); + } + } + } + + handleInput(e) { + if (this.gameOver) return; + + let dx = 0; + let dy = 0; + + switch (e.key) { + case 'ArrowUp': dy = -1; break; + case 'ArrowDown': dy = 1; break; + case 'ArrowLeft': dx = -1; break; + case 'ArrowRight': dx = 1; break; + case 'NumPad8': dy = -1; break; + case 'NumPad2': dy = 1; break; + case 'NumPad4': dx = -1; break; + case 'NumPad6': dx = 1; break; + case 'NumPad7': dx = -1; dy = -1; break; + case 'NumPad9': dx = 1; dy = -1; break; + case 'NumPad1': dx = -1; dy = 1; break; + case 'NumPad3': dx = 1; dy = 1; break; + case '.': case 'NumPad5': break; // Wait + case 'Enter': + case '>': + console.log(`Trying to descend. Player at ${this.player.x},${this.player.y}. Tile: '${this.map.tiles[this.player.y][this.player.x]}'`); + if (this.map.tiles[this.player.y][this.player.x] === '>') { + this.nextLevel(); + } else { + this.ui.log("There are no stairs here."); + } + break; + case 'g': this.pickupItem(); break; + } + + if (dx !== 0 || dy !== 0 || e.key === '.' || e.key === 'NumPad5') { + this.movePlayer(dx, dy); + this.render(); + } + } + + movePlayer(dx, dy) { + const newX = this.player.x + dx; + const newY = this.player.y + dy; + + // Check for monster + const targetMonster = this.monsters.find(m => m.x === newX && m.y === newY); + if (targetMonster) { + this.attack(this.player, targetMonster); + } else if (this.map.isWalkable(newX, newY)) { + this.player.x = newX; + this.player.y = newY; + + // Check for item + const item = this.items.find(i => i.x === newX && i.y === newY); + if (item) { + this.ui.log(`You see a ${item.name}. (Press 'g' to get)`); + } + } else { + if (dx !== 0 || dy !== 0) this.ui.log("Blocked!"); + } + + // Monsters turn + this.updateMonsters(); + } + + pickupItem() { + const itemIndex = this.items.findIndex(i => i.x === this.player.x && i.y === this.player.y); + if (itemIndex !== -1) { + const item = this.items[itemIndex]; + this.items.splice(itemIndex, 1); + this.player.inventory.push(item); + this.ui.log(`You picked up ${item.name}.`); + this.ui.updateInventory(this.player); + this.render(); + } else { + this.ui.log("There is nothing here to pick up."); + } + } + + useItem(index) { + const item = this.player.inventory[index]; + if (!item) return; + + if (item.type === 'weapon') { + this.player.equipment.weapon = item; + this.ui.log(`You equipped ${item.name}.`); + } else if (item.type === 'shield') { + this.player.equipment.shield = item; + this.ui.log(`You equipped ${item.name}.`); + } else if (item.type === 'potion') { + if (item.stats.heal) { + this.player.hp = Math.min(this.player.hp + item.stats.heal, this.player.maxHp); + this.ui.log(`You drank ${item.name} and recovered ${item.stats.heal} HP.`); + // Remove potion + this.player.inventory.splice(index, 1); + } + } + + this.ui.updateInventory(this.player); + this.ui.updateStats(this.player, this.depth); + this.render(); // Re-render to show equipped status if we add that + } + + attack(attacker, defender) { + // Peasants are invincible + if (defender.name === "Peasant") { + const quotes = [ + "This is our town.", + "The Dungeon Lord is oppressing us! Please help!", + "You can get healing at the temple.", + "There are things to buy in the shops.", + "It's unpleasant for peasants at present.", + "Hello, hero!" + ]; + const quote = quotes[Math.floor(Math.random() * quotes.length)]; + this.ui.showPopup(quote, 1000); + return; + } + + let damage = 0; + if (attacker === this.player) { + damage = this.player.getDamage() + Math.floor(Math.random() * 2); + } else { + damage = 1 + Math.floor(Math.random() * 2); // Monster damage + } + + // Apply defense + const defense = defender.getDefense ? defender.getDefense() : 0; + damage = Math.max(1, damage - defense); // Always take at least 1 damage + + defender.hp -= damage; + this.ui.log(`${attacker.name} hits ${defender.name} for ${damage} damage.`); + + if (defender.hp <= 0) { + this.ui.log(`${defender.name} dies!`); + if (defender === this.player) { + this.ui.log("GAME OVER"); + this.gameOver = true; + setTimeout(() => alert("Game Over! Refresh to restart."), 100); + } else { + // Remove monster + this.monsters = this.monsters.filter(m => m !== defender); + + // Gold Drop + const gold = defender.level * (Math.floor(Math.random() * 5) + 1); + this.player.gold += gold; + this.ui.log(`You kill the ${defender.name}! It drops ${gold} gold.`); + + // Boss Drop + if (defender.name === "The Dungeon Lord") { + const map = new Item("Ancient Map", "map", 1, {}); + map.x = defender.x; + map.y = defender.y; + this.items.push(map); + this.ui.log("The Dungeon Lord drops an Ancient Map!"); + } + + // Award XP + if (this.player.gainXp(defender.level * 5)) { + this.ui.log(`You reached level ${this.player.level}!`); + } + } + } + } + + updateMonsters() { + for (const monster of this.monsters) { + if (monster.name === "Peasant") { + // Random walk (Slow: 10% chance to move) + if (Math.random() < 0.1) { + const dx = Math.floor(Math.random() * 3) - 1; + const dy = Math.floor(Math.random() * 3) - 1; + const newX = monster.x + dx; + const newY = monster.y + dy; + if (this.map.isWalkable(newX, newY) && !this.monsters.some(m => m.x === newX && m.y === newY) && (newX !== this.player.x || newY !== this.player.y)) { + monster.x = newX; + monster.y = newY; + } + } + continue; + } + + // Simple chase logic + const dx = this.player.x - monster.x; + const dy = this.player.y - monster.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 8) { // Aggro range + let moveX = 0; + let moveY = 0; + + if (Math.abs(dx) > Math.abs(dy)) { + moveX = dx > 0 ? 1 : -1; + } else { + moveY = dy > 0 ? 1 : -1; + } + + const newX = monster.x + moveX; + const newY = monster.y + moveY; + + if (newX === this.player.x && newY === this.player.y) { + this.attack(monster, this.player); + } else if (this.map.isWalkable(newX, newY)) { + // Check if occupied by another monster + if (!this.monsters.some(m => m.x === newX && m.y === newY)) { + monster.x = newX; + monster.y = newY; + } + } + } + } + } + + render() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ui.renderMap(this.map, this.player, this.monsters, this.items, this.tileSize); + this.ui.updateStats(this.player, this.depth); + } +} diff --git a/src/Item.js b/src/Item.js new file mode 100644 index 0000000..442a2cf --- /dev/null +++ b/src/Item.js @@ -0,0 +1,18 @@ +export class Item { + constructor(name, type, level, stats) { + this.name = name; + this.type = type; // 'weapon', 'armor', 'potion' + this.level = level; + this.stats = stats || {}; // { damage: 5 } or { defense: 2 } + this.x = 0; + this.y = 0; + this.symbol = '?'; + this.color = '#ffff00'; + + if (type === 'weapon') this.symbol = ')'; + if (type === 'armor') this.symbol = '['; + if (type === 'shield') this.symbol = ']'; + if (type === 'potion') this.symbol = '!'; + if (type === 'map') { this.symbol = '?'; this.color = '#ffd700'; } + } +} diff --git a/src/Map.js b/src/Map.js new file mode 100644 index 0000000..f0d2bc2 --- /dev/null +++ b/src/Map.js @@ -0,0 +1,138 @@ +export class Map { + constructor(width, height) { + this.width = width; + this.height = height; + this.tiles = []; + this.rooms = []; + } + + generate() { + this.rooms = []; + // Initialize with walls + for (let y = 0; y < this.height; y++) { + this.tiles[y] = []; + for (let x = 0; x < this.width; x++) { + this.tiles[y][x] = '#'; + } + } + + const MAX_ROOMS = 10; + const MIN_SIZE = 6; + const MAX_SIZE = 12; + + for (let i = 0; i < MAX_ROOMS; i++) { + const w = Math.floor(Math.random() * (MAX_SIZE - MIN_SIZE + 1)) + MIN_SIZE; + const h = Math.floor(Math.random() * (MAX_SIZE - MIN_SIZE + 1)) + MIN_SIZE; + const x = Math.floor(Math.random() * (this.width - w - 1)) + 1; + const y = Math.floor(Math.random() * (this.height - h - 1)) + 1; + + const newRoom = { x, y, w, h }; + + // Check overlap + let failed = false; + for (const other of this.rooms) { + if (newRoom.x <= other.x + other.w && newRoom.x + newRoom.w >= other.x && + newRoom.y <= other.y + other.h && newRoom.y + newRoom.h >= other.y) { + failed = true; + break; + } + } + + if (!failed) { + this.createRoom(newRoom); + + if (this.rooms.length > 0) { + const prev = this.rooms[this.rooms.length - 1]; + const newCenter = { x: Math.floor(newRoom.x + newRoom.w / 2), y: Math.floor(newRoom.y + newRoom.h / 2) }; + const prevCenter = { x: Math.floor(prev.x + prev.w / 2), y: Math.floor(prev.y + prev.h / 2) }; + + if (Math.random() < 0.5) { + this.createHKorridor(prevCenter.x, newCenter.x, prevCenter.y); + this.createVKorridor(prevCenter.y, newCenter.y, newCenter.x); + } else { + this.createVKorridor(prevCenter.y, newCenter.y, prevCenter.x); + this.createHKorridor(prevCenter.x, newCenter.x, newCenter.y); + } + } + + this.rooms.push(newRoom); + } + } + } + + generateTown() { + this.rooms = []; + // Fill with grass/floor + for (let y = 0; y < this.height; y++) { + this.tiles[y] = []; + for (let x = 0; x < this.width; x++) { + this.tiles[y][x] = '.'; + } + } + + // Create buildings + const numBuildings = 5; + for (let i = 0; i < numBuildings; i++) { + const w = Math.floor(Math.random() * 6) + 5; + const h = Math.floor(Math.random() * 6) + 5; + const x = Math.floor(Math.random() * (this.width - w - 2)) + 1; + const y = Math.floor(Math.random() * (this.height - h - 2)) + 1; + + // Check overlap (simple check against other buildings) + // For now, just place them. If they overlap, they merge. + + // Draw Walls + for (let by = y; by < y + h; by++) { + for (let bx = x; bx < x + w; bx++) { + if (by === y || by === y + h - 1 || bx === x || bx === x + w - 1) { + this.tiles[by][bx] = '#'; + } else { + this.tiles[by][bx] = '.'; + } + } + } + + // Add Door (randomly on one side) + if (Math.random() < 0.5) { + // Horizontal wall door + const doorX = Math.floor(Math.random() * (w - 2)) + x + 1; + const doorY = Math.random() < 0.5 ? y : y + h - 1; + this.tiles[doorY][doorX] = '.'; + } else { + // Vertical wall door + const doorY = Math.floor(Math.random() * (h - 2)) + y + 1; + const doorX = Math.random() < 0.5 ? x : x + w - 1; + this.tiles[doorY][doorX] = '.'; + } + + // Add to rooms list so we can spawn things inside if we want + this.rooms.push({ x, y, w, h }); + } + } + + + createRoom(room) { + for (let y = room.y; y < room.y + room.h; y++) { + for (let x = room.x; x < room.x + room.w; x++) { + this.tiles[y][x] = '.'; + } + } + } + + createHKorridor(x1, x2, y) { + for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x++) { + this.tiles[y][x] = '.'; + } + } + + createVKorridor(y1, y2, x) { + for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y++) { + this.tiles[y][x] = '.'; + } + } + + isWalkable(x, y) { + if (x < 0 || x >= this.width || y < 0 || y >= this.height) return false; + return this.tiles[y][x] === '.' || this.tiles[y][x] === '>'; + } +} diff --git a/src/UI.js b/src/UI.js new file mode 100644 index 0000000..078c60b --- /dev/null +++ b/src/UI.js @@ -0,0 +1,189 @@ +export class UI { + constructor(ctx, game) { + this.ctx = ctx; + this.game = game; + this.messageLog = document.getElementById('message-log'); + this.inventoryList = document.getElementById('inventory-list'); + this.popup = null; + this.popupTimeout = null; + } + + log(message) { + if (!this.messageLog) return; + const div = document.createElement('div'); + div.textContent = message; + this.messageLog.appendChild(div); + this.messageLog.scrollTop = this.messageLog.scrollHeight; + } + + renderMap(map, player, monsters, items, tileSize) { + // Fill background + this.ctx.fillStyle = '#000000'; + this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height); + + this.ctx.font = `${tileSize}px monospace`; + this.ctx.textBaseline = 'top'; + + // Calculate viewport (center on player) + const viewWidth = Math.ceil(this.ctx.canvas.width / tileSize); + const viewHeight = Math.ceil(this.ctx.canvas.height / tileSize); + + const startX = player.x - Math.floor(viewWidth / 2); + const startY = player.y - Math.floor(viewHeight / 2); + + for (let y = 0; y < viewHeight; y++) { + for (let x = 0; x < viewWidth; x++) { + const mapX = startX + x; + const mapY = startY + y; + + if (mapX >= 0 && mapX < map.width && mapY >= 0 && mapY < map.height) { + const tile = map.tiles[mapY][mapX]; + this.drawTile(x * tileSize, y * tileSize, tile, tileSize); + } + } + } + + // Draw Items + if (items) { + for (const item of items) { + const screenX = (item.x - startX) * tileSize; + const screenY = (item.y - startY) * tileSize; + if (screenX >= -tileSize && screenX < this.ctx.canvas.width && + screenY >= -tileSize && screenY < this.ctx.canvas.height) { + this.drawEntity(screenX, screenY, item, tileSize); + } + } + } + + // Draw Monsters + for (const monster of monsters) { + const screenX = (monster.x - startX) * tileSize; + const screenY = (monster.y - startY) * tileSize; + // Only draw if visible + if (screenX >= -tileSize && screenX < this.ctx.canvas.width && + screenY >= -tileSize && screenY < this.ctx.canvas.height) { + this.drawEntity(screenX, screenY, monster, tileSize); + } + } + + // Draw Player + const playerScreenX = (player.x - startX) * tileSize; + const playerScreenY = (player.y - startY) * tileSize; + + this.drawEntity(playerScreenX, playerScreenY, player, tileSize); + + this.drawPopup(); + } + + updateStats(player, depth) { + document.getElementById('stat-hp').textContent = player.hp; + document.getElementById('stat-max-hp').textContent = player.maxHp; + document.getElementById('stat-str').textContent = player.stats.str; + document.getElementById('stat-level').textContent = player.level; + document.getElementById('stat-xp').textContent = player.xp; + document.getElementById('stat-max-xp').textContent = player.maxXp; + document.getElementById('stat-ac').textContent = player.getDefense(); + document.getElementById('stat-floor').textContent = depth; + document.getElementById('stat-gold').textContent = player.gold; + } + + updateInventory(player) { + this.inventoryList.innerHTML = ''; + player.inventory.forEach((item, index) => { + const li = document.createElement('li'); + li.textContent = item.name; + if (player.equipment.weapon === item || player.equipment.armor === item || player.equipment.shield === item) { + li.textContent += " (E)"; + } + li.style.cursor = 'pointer'; + li.onclick = () => { + this.game.useItem(index); + }; + this.inventoryList.appendChild(li); + }); + } + + drawTile(screenX, screenY, tile, size) { + if (tile === '#') { + this.ctx.fillStyle = '#808080'; // Gray wall + this.ctx.fillRect(screenX, screenY, size, size); + // Add a bevel effect for walls + this.ctx.strokeStyle = '#ffffff'; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + this.ctx.moveTo(screenX, screenY + size); + this.ctx.lineTo(screenX, screenY); + this.ctx.lineTo(screenX + size, screenY); + this.ctx.stroke(); + + this.ctx.strokeStyle = '#404040'; + this.ctx.beginPath(); + this.ctx.moveTo(screenX + size, screenY); + this.ctx.lineTo(screenX + size, screenY + size); + this.ctx.lineTo(screenX, screenY + size); + this.ctx.stroke(); + + } else if (tile === '>') { + // Stairs + this.ctx.fillStyle = '#202020'; + this.ctx.fillRect(screenX, screenY, size, size); + this.ctx.fillStyle = '#ffffff'; + this.ctx.fillText('>', screenX + size / 4, screenY); + } else { + // Floor + this.ctx.fillStyle = '#202020'; + this.ctx.fillRect(screenX, screenY, size, size); + this.ctx.fillStyle = '#404040'; + this.ctx.fillText('.', screenX + size / 4, screenY); + } + } + + showPopup(text, duration) { + this.popup = text; + if (this.popupTimeout) clearTimeout(this.popupTimeout); + this.game.render(); + + this.popupTimeout = setTimeout(() => { + this.popup = null; + this.game.render(); + }, duration); + } + + drawPopup() { + if (!this.popup) return; + + const ctx = this.ctx; + ctx.font = '16px "Courier New", monospace'; + const textMetrics = ctx.measureText(this.popup); + const textWidth = textMetrics.width; + + const width = textWidth + 40; // Add padding + const height = 50; + const x = (this.ctx.canvas.width - width) / 2; + const y = (this.ctx.canvas.height - height) / 2 - 50; // Slightly above center + + // Background + ctx.fillStyle = '#ffffff'; + ctx.fillRect(x, y, width, height); + + // Border + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 2; + ctx.strokeRect(x, y, width, height); + + // Text + ctx.fillStyle = '#000000'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(this.popup, x + width / 2, y + height / 2); + + // Reset text align + ctx.textAlign = 'start'; + ctx.textBaseline = 'alphabetic'; + } + + drawEntity(screenX, screenY, entity, size) { + this.ctx.fillStyle = entity.color; + this.ctx.fillText(entity.symbol, screenX + size / 4, screenY); + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..b3fa6fd --- /dev/null +++ b/src/main.js @@ -0,0 +1,6 @@ +import { Game } from './Game.js'; + +window.addEventListener('DOMContentLoaded', () => { + const game = new Game(); + game.start(); +}); diff --git a/style.css b/style.css new file mode 100644 index 0000000..44cdf08 --- /dev/null +++ b/style.css @@ -0,0 +1,127 @@ +:root { + --win-bg: #c0c0c0; + --win-text: #000000; + --win-gray-light: #dfdfdf; + --win-gray-dark: #808080; + --win-blue: #000080; + --win-white: #ffffff; +} + +body { + background-color: #008080; /* Classic teal desktop */ + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Fallback to modern sans */ + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + overflow: hidden; +} + +/* Windows 3.1 / 95 Style Window */ +.window { + background-color: var(--win-bg); + border: 2px solid var(--win-white); + border-right-color: var(--win-gray-dark); + border-bottom-color: var(--win-gray-dark); + box-shadow: 2px 2px 0 #000; + padding: 2px; + display: flex; + flex-direction: column; + width: 900px; + height: 700px; +} + +.title-bar { + background: var(--win-blue); + padding: 3px 2px; + display: flex; + justify-content: space-between; + align-items: center; + color: var(--win-white); + font-weight: bold; + font-family: sans-serif; + letter-spacing: 0.5px; +} + +.title-bar-text { + margin-left: 4px; +} + +.window-body { + flex: 1; + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +/* Inset Border (Sunken look) */ +.inset-border { + border: 2px solid var(--win-gray-dark); + border-right-color: var(--win-white); + border-bottom-color: var(--win-white); + background: var(--win-white); /* Canvas background */ +} + +#main-layout { + display: flex; + gap: 10px; + flex: 1; + min-height: 0; /* Fix flex overflow */ +} + +#viewport-container { + flex: 1; + background: #000; /* Game background */ + overflow: hidden; + position: relative; + display: flex; + justify-content: center; + align-items: center; +} + +#game-canvas { + image-rendering: pixelated; +} + +#sidebar { + width: 200px; + display: flex; + flex-direction: column; + gap: 10px; +} + +/* Group Box */ +.group-box { + border: 2px solid var(--win-gray-light); + border-top-color: var(--win-white); + border-left-color: var(--win-white); + border-right-color: var(--win-gray-dark); + border-bottom-color: var(--win-gray-dark); + padding: 10px; + position: relative; + margin-top: 10px; +} + +.group-box-label { + position: absolute; + top: -10px; + left: 10px; + background: var(--win-bg); + padding: 0 5px; +} + +#message-log { + height: 100px; + padding: 5px; + overflow-y: auto; + font-family: 'Courier New', Courier, monospace; + font-size: 14px; +} + +.status-bar { + border-top: 1px solid var(--win-gray-dark); + padding: 2px 5px; + font-size: 12px; +}