Work in progress

main
John Kenyon 2025-11-29 17:33:30 -08:00
commit fbcc3a91d9
8 changed files with 1027 additions and 0 deletions

64
index.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Castle of the Winds Clone</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game-container" class="window">
<div class="title-bar">
<div class="title-bar-text">Castle of the Winds Clone</div>
<div class="title-bar-controls">
<button aria-label="Minimize"></button>
<button aria-label="Maximize"></button>
<button aria-label="Close"></button>
</div>
</div>
<div class="window-body">
<div id="main-layout">
<div id="viewport-container" class="inset-border">
<canvas id="game-canvas" width="640" height="480"></canvas>
</div>
<div id="sidebar">
<div id="stats-panel" class="group-box">
<div class="group-box-label">Stats</div>
<div id="stats-content">
<div>Floor: <span id="stat-floor">1</span></div>
<div>Gold: <span id="stat-gold">0</span></div>
<div>Level: <span id="stat-level">1</span></div>
<div>XP: <span id="stat-xp">0</span>/<span id="stat-max-xp">10</span></div>
<br>
<div>STR: <span id="stat-str">10</span></div>
<div>AC: <span id="stat-ac">0</span></div>
<div>INT: <span id="stat-int">10</span></div>
<div>DEX: <span id="stat-dex">10</span></div>
<div>CON: <span id="stat-con">10</span></div>
<br>
<div>HP: <span id="stat-hp">10</span>/<span id="stat-max-hp">10</span></div>
<div>Mana: <span id="stat-mana">10</span>/<span id="stat-max-mana">10</span></div>
</div>
</div>
<div id="inventory-panel" class="group-box">
<div class="group-box-label">Inventory</div>
<ul id="inventory-list">
<!-- Items will go here -->
</ul>
</div>
</div>
</div>
<div id="message-log" class="inset-border">
<div>Welcome to the Castle of the Winds Clone!</div>
</div>
</div>
<div class="status-bar">
<p class="status-bar-field">Ready</p>
</div>
</div>
<script type="module" src="src/main.js"></script>
</body>
</html>

76
src/Entity.js Normal file
View File

@ -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
}
}

409
src/Game.js Normal file
View File

@ -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);
}
}

18
src/Item.js Normal file
View File

@ -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'; }
}
}

138
src/Map.js Normal file
View File

@ -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] === '>';
}
}

189
src/UI.js Normal file
View File

@ -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);
}
}

6
src/main.js Normal file
View File

@ -0,0 +1,6 @@
import { Game } from './Game.js';
window.addEventListener('DOMContentLoaded', () => {
const game = new Game();
game.start();
});

127
style.css Normal file
View File

@ -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;
}