Progress
parent
fbcc3a91d9
commit
e082b12cc7
|
|
@ -48,6 +48,12 @@
|
|||
<!-- Items will go here -->
|
||||
</ul>
|
||||
</div>
|
||||
<div id="spells-panel" class="group-box">
|
||||
<div class="group-box-label">Spells</div>
|
||||
<ul id="spells-list">
|
||||
<!-- Spells will go here -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="message-log" class="inset-border">
|
||||
|
|
|
|||
|
|
@ -21,8 +21,12 @@ export class Player extends Entity {
|
|||
};
|
||||
this.stats = {
|
||||
str: 10,
|
||||
dex: 10
|
||||
dex: 10,
|
||||
int: 10
|
||||
};
|
||||
this.mana = 10;
|
||||
this.maxMana = 10;
|
||||
this.spells = ['Magic Arrow', 'Fireball'];
|
||||
this.level = 1;
|
||||
this.xp = 0;
|
||||
this.maxXp = 10;
|
||||
|
|
@ -37,7 +41,10 @@ export class Player extends Entity {
|
|||
this.maxXp = Math.floor(this.maxXp * 1.5);
|
||||
this.maxHp += 5;
|
||||
this.hp = this.maxHp;
|
||||
this.stats.str += 2;
|
||||
this.maxMana += 5;
|
||||
this.mana = this.maxMana;
|
||||
this.stats.str += 1;
|
||||
this.stats.int += 1;
|
||||
return true; // Leveled up
|
||||
}
|
||||
return false;
|
||||
|
|
@ -74,3 +81,11 @@ export class Monster extends Entity {
|
|||
return Math.floor(this.level / 2); // Slight natural armor for higher level monsters
|
||||
}
|
||||
}
|
||||
|
||||
export class Shopkeeper extends Entity {
|
||||
constructor(x, y, name, shopName, inventory) {
|
||||
super(x, y, name, '$', '#ffd700');
|
||||
this.shopName = shopName;
|
||||
this.inventory = inventory; // Array of { item: Item, price: number }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
389
src/Game.js
389
src/Game.js
|
|
@ -2,7 +2,7 @@ import { Map } from './Map.js';
|
|||
import { UI } from './UI.js';
|
||||
import { Player } from './Entity.js';
|
||||
|
||||
import { Monster } from './Entity.js';
|
||||
import { Monster, Shopkeeper } from './Entity.js';
|
||||
import { Item } from './Item.js';
|
||||
|
||||
export class Game {
|
||||
|
|
@ -23,32 +23,52 @@ export class Game {
|
|||
|
||||
this.tileSize = 32;
|
||||
|
||||
this.gameState = 'PLAY'; // 'PLAY', 'SHOP', 'TARGETING'
|
||||
this.currentShopkeeper = null;
|
||||
this.shopSelection = 0;
|
||||
this.targetingSpell = null;
|
||||
this.projectiles = [];
|
||||
|
||||
// Bind input
|
||||
window.addEventListener('keydown', (e) => this.handleInput(e));
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log("Game Started");
|
||||
this.generateLevel();
|
||||
this.generateLevel('down');
|
||||
this.ui.log("Welcome to the Town of Oakhaven!");
|
||||
}
|
||||
|
||||
generateLevel() {
|
||||
generateLevel(direction = 'down') {
|
||||
this.monsters = [];
|
||||
this.items = [];
|
||||
|
||||
if (this.depth === 0) {
|
||||
this.map.generateTown();
|
||||
// Spawn player in center
|
||||
// Spawn player in center (default) or at stairs if coming up
|
||||
if (direction === 'up') {
|
||||
// Coming up from dungeon, spawn at dungeon entrance
|
||||
this.player.x = 43;
|
||||
this.player.y = 8;
|
||||
} else {
|
||||
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
|
||||
// Spawn Player
|
||||
if (this.map.rooms.length > 0) {
|
||||
if (direction === 'down') {
|
||||
// Start at first room (Stairs Up location)
|
||||
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);
|
||||
} else {
|
||||
// Start at last room (Stairs Down location)
|
||||
const endRoom = this.map.rooms[this.map.rooms.length - 1];
|
||||
this.player.x = Math.floor(endRoom.x + endRoom.w / 2);
|
||||
this.player.y = Math.floor(endRoom.y + endRoom.h / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,6 +79,7 @@ export class Game {
|
|||
this.render();
|
||||
this.ui.updateStats(this.player, this.depth);
|
||||
this.ui.updateInventory(this.player);
|
||||
this.ui.updateSpells(this.player);
|
||||
}
|
||||
|
||||
nextLevel() {
|
||||
|
|
@ -70,27 +91,49 @@ export class Game {
|
|||
|
||||
this.depth++;
|
||||
this.ui.log(`You descend to level ${this.depth}...`);
|
||||
this.generateLevel();
|
||||
this.generateLevel('down');
|
||||
}
|
||||
|
||||
prevLevel() {
|
||||
if (this.depth === 0) {
|
||||
this.ui.log("You are already in town.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.depth--;
|
||||
if (this.depth === 0) {
|
||||
this.ui.log("You return to the Town of Oakhaven.");
|
||||
} else {
|
||||
this.ui.log(`You ascend to level ${this.depth}...`);
|
||||
}
|
||||
this.generateLevel('up');
|
||||
}
|
||||
|
||||
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;
|
||||
// Town: Stairs Down in the Dungeon Entrance building
|
||||
const sx = 43; // Center X
|
||||
const sy = 8; // Center Y
|
||||
this.map.tiles[sy][sx] = '>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawn stairs in the last room
|
||||
// Dungeon Levels
|
||||
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] = '>';
|
||||
// Stairs Up (<) in the first room (where you start when going down)
|
||||
const startRoom = this.map.rooms[0];
|
||||
const ux = Math.floor(startRoom.x + startRoom.w / 2);
|
||||
const uy = Math.floor(startRoom.y + startRoom.h / 2);
|
||||
// Ensure we don't overwrite player if they are standing there (visual only, logic handles it)
|
||||
this.map.tiles[uy][ux] = '<';
|
||||
|
||||
// Stairs Down (>) in the last room (unless max depth)
|
||||
if (this.depth < this.maxDepth) {
|
||||
const endRoom = this.map.rooms[this.map.rooms.length - 1];
|
||||
const dx = Math.floor(Math.random() * endRoom.w) + endRoom.x;
|
||||
const dy = Math.floor(Math.random() * endRoom.h) + endRoom.y;
|
||||
this.map.tiles[dy][dx] = '>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +148,25 @@ export class Game {
|
|||
this.monsters.push(peasant);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn Shopkeepers
|
||||
// Weapon Shop (East)
|
||||
const weaponShopInventory = [
|
||||
{ item: new Item("Dagger", "weapon", 1, { damage: 4 }), price: 50 },
|
||||
{ item: new Item("Short Sword", "weapon", 2, { damage: 6 }), price: 100 },
|
||||
{ item: new Item("Buckler", "shield", 1, { defense: 1 }), price: 40 },
|
||||
{ item: new Item("Wooden Shield", "shield", 2, { defense: 2 }), price: 80 }
|
||||
];
|
||||
const weaponSmith = new Shopkeeper(41, 18, "Smith", "Weapon Shop", weaponShopInventory);
|
||||
this.monsters.push(weaponSmith);
|
||||
|
||||
// General Store (West)
|
||||
const generalStoreInventory = [
|
||||
{ item: new Item("Potion", "potion", 1, { heal: 5 }), price: 20 }
|
||||
];
|
||||
const merchant = new Shopkeeper(9, 18, "Merchant", "General Store", generalStoreInventory);
|
||||
this.monsters.push(merchant);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -193,33 +255,61 @@ export class Game {
|
|||
handleInput(e) {
|
||||
if (this.gameOver) return;
|
||||
|
||||
if (this.gameState === 'SHOP') {
|
||||
this.handleShopInput(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.gameState === 'TARGETING') {
|
||||
this.handleTargetingInput(e);
|
||||
return;
|
||||
}
|
||||
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
let handled = false;
|
||||
|
||||
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 'ArrowUp': dy = -1; handled = true; break;
|
||||
case 'ArrowDown': dy = 1; handled = true; break;
|
||||
case 'ArrowLeft': dx = -1; handled = true; break;
|
||||
case 'ArrowRight': dx = 1; handled = true; break;
|
||||
case 'NumPad8': dy = -1; handled = true; break;
|
||||
case 'NumPad2': dy = 1; handled = true; break;
|
||||
case 'NumPad4': dx = -1; handled = true; break;
|
||||
case 'NumPad6': dx = 1; handled = true; break;
|
||||
case 'NumPad7': dx = -1; dy = -1; handled = true; break;
|
||||
case 'NumPad9': dx = 1; dy = -1; handled = true; break;
|
||||
case 'NumPad1': dx = -1; dy = 1; handled = true; break;
|
||||
case 'NumPad3': dx = 1; dy = 1; handled = true; break;
|
||||
case '.': case 'NumPad5': handled = true; 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 if (this.map.tiles[this.player.y][this.player.x] === '<') {
|
||||
this.prevLevel();
|
||||
} else {
|
||||
this.ui.log("There are no stairs here.");
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
case 'g': this.pickupItem(); break;
|
||||
case '<':
|
||||
case ',':
|
||||
if (this.map.tiles[this.player.y][this.player.x] === '<') {
|
||||
this.prevLevel();
|
||||
} else {
|
||||
this.ui.log("There are no stairs up here.");
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
case 'g': this.pickupItem(); handled = true; break;
|
||||
case '1': this.startTargeting('Magic Arrow'); handled = true; break;
|
||||
case '2': this.startTargeting('Fireball'); handled = true; break;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (dx !== 0 || dy !== 0 || e.key === '.' || e.key === 'NumPad5') {
|
||||
|
|
@ -228,6 +318,64 @@ export class Game {
|
|||
}
|
||||
}
|
||||
|
||||
handleShopInput(e) {
|
||||
e.preventDefault();
|
||||
const inventory = this.currentShopkeeper.inventory;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
case 'NumPad8':
|
||||
this.shopSelection--;
|
||||
if (this.shopSelection < 0) this.shopSelection = inventory.length - 1;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 'NumPad2':
|
||||
this.shopSelection++;
|
||||
if (this.shopSelection >= inventory.length) this.shopSelection = 0;
|
||||
break;
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
this.buyItem(inventory[this.shopSelection]);
|
||||
break;
|
||||
case 'Escape':
|
||||
this.closeShop();
|
||||
break;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
openShop(shopkeeper) {
|
||||
this.gameState = 'SHOP';
|
||||
this.currentShopkeeper = shopkeeper;
|
||||
this.shopSelection = 0;
|
||||
this.ui.log(`You talk to ${shopkeeper.name}.`);
|
||||
this.render();
|
||||
}
|
||||
|
||||
closeShop() {
|
||||
this.gameState = 'PLAY';
|
||||
this.currentShopkeeper = null;
|
||||
this.render();
|
||||
}
|
||||
|
||||
buyItem(entry) {
|
||||
if (this.player.gold >= entry.price) {
|
||||
this.player.gold -= entry.price;
|
||||
// Clone item to avoid reference issues if buying multiple
|
||||
const newItem = new Item(entry.item.name, entry.item.type, entry.item.level, entry.item.stats);
|
||||
// Copy visual props
|
||||
newItem.symbol = entry.item.symbol;
|
||||
newItem.color = entry.item.color;
|
||||
|
||||
this.player.inventory.push(newItem);
|
||||
this.ui.log(`You bought ${entry.item.name} for ${entry.price} gold.`);
|
||||
this.ui.updateInventory(this.player);
|
||||
this.ui.updateStats(this.player, this.depth);
|
||||
} else {
|
||||
this.ui.log("You don't have enough gold!");
|
||||
}
|
||||
}
|
||||
|
||||
movePlayer(dx, dy) {
|
||||
const newX = this.player.x + dx;
|
||||
const newY = this.player.y + dy;
|
||||
|
|
@ -235,7 +383,11 @@ export class Game {
|
|||
// Check for monster
|
||||
const targetMonster = this.monsters.find(m => m.x === newX && m.y === newY);
|
||||
if (targetMonster) {
|
||||
if (targetMonster instanceof Shopkeeper) {
|
||||
this.openShop(targetMonster);
|
||||
} else {
|
||||
this.attack(this.player, targetMonster);
|
||||
}
|
||||
} else if (this.map.isWalkable(newX, newY)) {
|
||||
this.player.x = newX;
|
||||
this.player.y = newY;
|
||||
|
|
@ -246,13 +398,174 @@ export class Game {
|
|||
this.ui.log(`You see a ${item.name}. (Press 'g' to get)`);
|
||||
}
|
||||
} else {
|
||||
console.log(`Blocked at ${newX},${newY}. Tile: '${this.map.tiles[newY][newX]}'`);
|
||||
if (dx !== 0 || dy !== 0) this.ui.log("Blocked!");
|
||||
}
|
||||
|
||||
// Temple Healing (Town only)
|
||||
if (this.depth === 0 && this.player.x === 25 && this.player.y === 12) {
|
||||
if (this.player.hp < this.player.maxHp) {
|
||||
this.player.hp = this.player.maxHp;
|
||||
this.ui.log("You enter the temple and feel refreshed. HP fully restored!");
|
||||
this.ui.updateStats(this.player, this.depth);
|
||||
} else {
|
||||
this.ui.log("You enter the temple. It is peaceful here.");
|
||||
}
|
||||
}
|
||||
|
||||
// Monsters turn
|
||||
this.updateMonsters();
|
||||
}
|
||||
|
||||
handleTargetingInput(e) {
|
||||
e.preventDefault();
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp': case 'NumPad8': dy = -1; break;
|
||||
case 'ArrowDown': case 'NumPad2': dy = 1; break;
|
||||
case 'ArrowLeft': case 'NumPad4': dx = -1; break;
|
||||
case 'ArrowRight': 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 'Escape':
|
||||
this.gameState = 'PLAY';
|
||||
this.ui.log("Canceled.");
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
this.castSpell(this.targetingSpell, dx, dy);
|
||||
this.gameState = 'PLAY';
|
||||
this.targetingSpell = null;
|
||||
}
|
||||
}
|
||||
|
||||
startTargeting(spellName) {
|
||||
this.targetingSpell = spellName;
|
||||
this.gameState = 'TARGETING';
|
||||
this.ui.log(`Select direction for ${spellName}...`);
|
||||
}
|
||||
|
||||
castSpell(spellName, dx, dy) {
|
||||
let manaCost = 0;
|
||||
let damage = 0;
|
||||
let symbol = '*';
|
||||
let color = '#ffffff';
|
||||
|
||||
if (spellName === 'Magic Arrow') {
|
||||
manaCost = 2;
|
||||
damage = Math.floor(this.player.stats.int / 2) + Math.floor(Math.random() * 4) + 1;
|
||||
symbol = '*';
|
||||
color = '#aaaaff';
|
||||
} else if (spellName === 'Fireball') {
|
||||
manaCost = 5;
|
||||
damage = this.player.stats.int + Math.floor(Math.random() * 6) + 1;
|
||||
symbol = 'o';
|
||||
color = '#ff4400';
|
||||
}
|
||||
|
||||
if (this.player.mana < manaCost) {
|
||||
this.ui.log("Not enough mana!");
|
||||
return;
|
||||
}
|
||||
|
||||
this.player.mana -= manaCost;
|
||||
this.ui.updateStats(this.player, this.depth);
|
||||
|
||||
// Create Projectile
|
||||
const projectile = {
|
||||
x: this.player.x,
|
||||
y: this.player.y,
|
||||
dx: dx,
|
||||
dy: dy,
|
||||
symbol: symbol,
|
||||
color: color,
|
||||
damage: damage,
|
||||
name: spellName
|
||||
};
|
||||
|
||||
this.projectiles.push(projectile);
|
||||
this.animateProjectile(projectile);
|
||||
}
|
||||
|
||||
animateProjectile(projectile) {
|
||||
const interval = setInterval(() => {
|
||||
const newX = projectile.x + projectile.dx;
|
||||
const newY = projectile.y + projectile.dy;
|
||||
|
||||
// Check collision
|
||||
if (!this.map.isWalkable(newX, newY)) {
|
||||
clearInterval(interval);
|
||||
this.removeProjectile(projectile);
|
||||
this.ui.log(`${projectile.name} hits the wall.`);
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const monster = this.monsters.find(m => m.x === newX && m.y === newY);
|
||||
if (monster) {
|
||||
clearInterval(interval);
|
||||
this.removeProjectile(projectile);
|
||||
|
||||
// Shopkeepers are immune
|
||||
if (monster instanceof Shopkeeper) {
|
||||
this.ui.log(`${projectile.name} hits ${monster.name} but does nothing.`);
|
||||
} else {
|
||||
// Deal damage
|
||||
monster.hp -= projectile.damage;
|
||||
this.ui.log(`${projectile.name} hits ${monster.name} for ${projectile.damage} damage.`);
|
||||
if (monster.hp <= 0) {
|
||||
this.killMonster(monster);
|
||||
}
|
||||
}
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
projectile.x = newX;
|
||||
projectile.y = newY;
|
||||
this.render();
|
||||
|
||||
// Range limit (optional, but good for safety)
|
||||
if (Math.abs(projectile.x - this.player.x) > 20 || Math.abs(projectile.y - this.player.y) > 20) {
|
||||
clearInterval(interval);
|
||||
this.removeProjectile(projectile);
|
||||
this.render();
|
||||
}
|
||||
|
||||
}, 50); // Speed of projectile
|
||||
}
|
||||
|
||||
removeProjectile(projectile) {
|
||||
const index = this.projectiles.indexOf(projectile);
|
||||
if (index > -1) {
|
||||
this.projectiles.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
killMonster(monster) {
|
||||
this.ui.log(`${monster.name} dies!`);
|
||||
this.monsters = this.monsters.filter(m => m !== monster);
|
||||
const gold = monster.level * (Math.floor(Math.random() * 5) + 1);
|
||||
this.player.gold += gold;
|
||||
this.ui.log(`You kill the ${monster.name}! It drops ${gold} gold.`);
|
||||
if (monster.name === "The Dungeon Lord") {
|
||||
const map = new Item("Ancient Map", "map", 1, {});
|
||||
map.x = monster.x;
|
||||
map.y = monster.y;
|
||||
this.items.push(map);
|
||||
this.ui.log("The Dungeon Lord drops an Ancient Map!");
|
||||
}
|
||||
if (this.player.gainXp(monster.level * 5)) {
|
||||
this.ui.log(`You reached level ${this.player.level}!`);
|
||||
}
|
||||
}
|
||||
|
||||
pickupItem() {
|
||||
const itemIndex = this.items.findIndex(i => i.x === this.player.x && i.y === this.player.y);
|
||||
if (itemIndex !== -1) {
|
||||
|
|
@ -292,6 +605,9 @@ export class Game {
|
|||
}
|
||||
|
||||
attack(attacker, defender) {
|
||||
// Shopkeepers are invincible/pacifist
|
||||
if (defender instanceof Shopkeeper) return;
|
||||
|
||||
// Peasants are invincible
|
||||
if (defender.name === "Peasant") {
|
||||
const quotes = [
|
||||
|
|
@ -355,6 +671,8 @@ export class Game {
|
|||
|
||||
updateMonsters() {
|
||||
for (const monster of this.monsters) {
|
||||
if (monster instanceof Shopkeeper) continue; // Shopkeepers don't move
|
||||
|
||||
if (monster.name === "Peasant") {
|
||||
// Random walk (Slow: 10% chance to move)
|
||||
if (Math.random() < 0.1) {
|
||||
|
|
@ -403,7 +721,12 @@ export class Game {
|
|||
|
||||
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.renderMap(this.map, this.player, this.monsters, this.items, this.projectiles, this.tileSize);
|
||||
|
||||
if (this.gameState === 'SHOP') {
|
||||
this.ui.drawShop(this.currentShopkeeper, this.player, this.shopSelection);
|
||||
}
|
||||
|
||||
this.ui.updateStats(this.player, this.depth);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
58
src/Map.js
58
src/Map.js
|
|
@ -70,18 +70,9 @@ export class Map {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Helper to draw a building
|
||||
const drawBuilding = (x, y, w, h, doorSide) => {
|
||||
// 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) {
|
||||
|
|
@ -92,22 +83,37 @@ export class Map {
|
|||
}
|
||||
}
|
||||
|
||||
// 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] = '.';
|
||||
// Door
|
||||
if (doorSide === 'bottom') {
|
||||
this.tiles[y + h - 1][Math.floor(x + w / 2)] = '.';
|
||||
} else if (doorSide === 'top') {
|
||||
this.tiles[y][Math.floor(x + w / 2)] = '.';
|
||||
} else if (doorSide === 'left') {
|
||||
this.tiles[Math.floor(y + h / 2)][x] = '.';
|
||||
} else if (doorSide === 'right') {
|
||||
this.tiles[Math.floor(y + h / 2)][x + w - 1] = '.';
|
||||
}
|
||||
|
||||
// Add to rooms list so we can spawn things inside if we want
|
||||
// Add to rooms for reference
|
||||
this.rooms.push({ x, y, w, h });
|
||||
}
|
||||
};
|
||||
|
||||
// 1. Temple (North Center)
|
||||
drawBuilding(20, 5, 10, 8, 'bottom');
|
||||
|
||||
// 2. General Store (West)
|
||||
drawBuilding(5, 15, 8, 6, 'right');
|
||||
|
||||
// 3. Weapon Shop (East)
|
||||
drawBuilding(37, 15, 8, 6, 'left');
|
||||
|
||||
// 4. Dungeon Entrance (North East)
|
||||
drawBuilding(40, 5, 6, 6, 'bottom');
|
||||
|
||||
// 5. Houses (South)
|
||||
drawBuilding(10, 30, 6, 5, 'top');
|
||||
drawBuilding(22, 30, 6, 5, 'top');
|
||||
drawBuilding(34, 30, 6, 5, 'top');
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -133,6 +139,6 @@ export class Map {
|
|||
|
||||
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] === '>';
|
||||
return this.tiles[y][x] === '.' || this.tiles[y][x] === '>' || this.tiles[y][x] === '<';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
98
src/UI.js
98
src/UI.js
|
|
@ -3,7 +3,9 @@ export class UI {
|
|||
this.ctx = ctx;
|
||||
this.game = game;
|
||||
this.messageLog = document.getElementById('message-log');
|
||||
this.messageLog = document.getElementById('message-log');
|
||||
this.inventoryList = document.getElementById('inventory-list');
|
||||
this.spellsList = document.getElementById('spells-list');
|
||||
this.popup = null;
|
||||
this.popupTimeout = null;
|
||||
}
|
||||
|
|
@ -16,7 +18,7 @@ export class UI {
|
|||
this.messageLog.scrollTop = this.messageLog.scrollHeight;
|
||||
}
|
||||
|
||||
renderMap(map, player, monsters, items, tileSize) {
|
||||
renderMap(map, player, monsters, items, projectiles, tileSize) {
|
||||
// Fill background
|
||||
this.ctx.fillStyle = '#000000';
|
||||
this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
||||
|
|
@ -66,6 +68,18 @@ export class UI {
|
|||
}
|
||||
}
|
||||
|
||||
// Draw Projectiles
|
||||
if (projectiles) {
|
||||
for (const proj of projectiles) {
|
||||
const screenX = (proj.x - startX) * tileSize;
|
||||
const screenY = (proj.y - startY) * tileSize;
|
||||
if (screenX >= -tileSize && screenX < this.ctx.canvas.width &&
|
||||
screenY >= -tileSize && screenY < this.ctx.canvas.height) {
|
||||
this.drawEntity(screenX, screenY, proj, tileSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw Player
|
||||
const playerScreenX = (player.x - startX) * tileSize;
|
||||
const playerScreenY = (player.y - startY) * tileSize;
|
||||
|
|
@ -85,6 +99,9 @@ export class UI {
|
|||
document.getElementById('stat-ac').textContent = player.getDefense();
|
||||
document.getElementById('stat-floor').textContent = depth;
|
||||
document.getElementById('stat-gold').textContent = player.gold;
|
||||
document.getElementById('stat-gold').textContent = player.gold;
|
||||
document.getElementById('stat-mana').textContent = `${player.mana}/${player.maxMana}`;
|
||||
document.getElementById('stat-int').textContent = player.stats.int;
|
||||
}
|
||||
|
||||
updateInventory(player) {
|
||||
|
|
@ -103,6 +120,16 @@ export class UI {
|
|||
});
|
||||
}
|
||||
|
||||
updateSpells(player) {
|
||||
if (!this.spellsList) return;
|
||||
this.spellsList.innerHTML = '';
|
||||
player.spells.forEach((spell, index) => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `${index + 1}. ${spell}`;
|
||||
this.spellsList.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
drawTile(screenX, screenY, tile, size) {
|
||||
if (tile === '#') {
|
||||
this.ctx.fillStyle = '#808080'; // Gray wall
|
||||
|
|
@ -124,11 +151,17 @@ export class UI {
|
|||
this.ctx.stroke();
|
||||
|
||||
} else if (tile === '>') {
|
||||
// Stairs
|
||||
// Stairs Down
|
||||
this.ctx.fillStyle = '#202020';
|
||||
this.ctx.fillRect(screenX, screenY, size, size);
|
||||
this.ctx.fillStyle = '#ffffff';
|
||||
this.ctx.fillText('>', screenX + size / 4, screenY);
|
||||
} else if (tile === '<') {
|
||||
// Stairs Up
|
||||
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';
|
||||
|
|
@ -186,4 +219,65 @@ export class UI {
|
|||
this.ctx.fillStyle = entity.color;
|
||||
this.ctx.fillText(entity.symbol, screenX + size / 4, screenY);
|
||||
}
|
||||
|
||||
drawShop(shopkeeper, player, selectedIndex) {
|
||||
const ctx = this.ctx;
|
||||
const width = 400;
|
||||
const height = 300;
|
||||
const x = (ctx.canvas.width - width) / 2;
|
||||
const y = (ctx.canvas.height - height) / 2;
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillRect(x, y, width, height);
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
|
||||
// Title
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '20px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(shopkeeper.shopName, x + width / 2, y + 30);
|
||||
|
||||
// Items
|
||||
ctx.font = '16px monospace';
|
||||
ctx.textAlign = 'left';
|
||||
let itemY = y + 70;
|
||||
|
||||
shopkeeper.inventory.forEach((entry, index) => {
|
||||
const item = entry.item;
|
||||
const price = entry.price;
|
||||
const isSelected = index === selectedIndex;
|
||||
|
||||
if (isSelected) {
|
||||
ctx.fillStyle = '#333333';
|
||||
ctx.fillRect(x + 20, itemY - 5, width - 40, 25);
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.fillText('>', x + 25, itemY + 10);
|
||||
} else {
|
||||
ctx.fillStyle = '#aaaaaa';
|
||||
}
|
||||
|
||||
ctx.fillText(`${item.name}`, x + 50, itemY + 10);
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(`${price}g`, x + width - 50, itemY + 10);
|
||||
ctx.textAlign = 'left';
|
||||
|
||||
itemY += 30;
|
||||
});
|
||||
|
||||
// Player Gold
|
||||
ctx.fillStyle = '#ffd700';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(`Gold: ${player.gold}`, x + width / 2, y + height - 30);
|
||||
|
||||
// Instructions
|
||||
ctx.fillStyle = '#888888';
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText('Up/Down: Select | Enter: Buy | Esc: Exit', x + width / 2, y + height - 10);
|
||||
|
||||
// Reset
|
||||
ctx.textAlign = 'start';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@ import { Game } from './Game.js';
|
|||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const game = new Game();
|
||||
window.game = game;
|
||||
game.start();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue