Work in progress
commit
fbcc3a91d9
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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] === '>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { Game } from './Game.js';
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const game = new Game();
|
||||||
|
game.start();
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue