<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fantasy City Ruler - Basic</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Fantasy City Ruler</h1>
<div id="resource-bar">
<span>Gold: <span id="gold">100</span></span> |
<span>Mana: <span id="mana">50</span></span> |
<span>Pop: <span id="population">0</span> / <span id="population-cap">5</span></span>
</div>
<div id="build-menu">
<h2>Build</h2>
<button data-building="House" data-cost-gold="50" data-cost-mana="0">House (+5 Pop Cap) - 50G</button>
<button data-building="Mine" data-cost-gold="75" data-cost-mana="10">Mine (+2 Gold/sec) - 75G, 10M</button>
<button data-building="Mana Well" data-cost-gold="30" data-cost-mana="60">Mana Well (+1 Mana/sec) - 30G, 60M</button>
<!-- Add more buildings later -->
</div>
<div id="city-grid-container">
<h2>Your City</h2>
<div id="city-grid">
<!-- Grid cells will be generated by JS -->
</div>
</div>
<div id="message-log">
<h2>Log</h2>
<ul id="messages">
<li>Welcome, Ruler! Build your city.</li>
</ul>
</div>
<script src="script.js"></script>
</body>
</html>
body {
font-family: sans-serif;
display: flex;
flex-direction: column;
align-items: center;
background-color: #f4f1e9;
color: #3a3a3a;
}
#resource-bar {
background-color: #d4cba8;
padding: 10px 20px;
border-radius: 5px;
margin-bottom: 15px;
border: 1px solid #a89e80;
font-weight: bold;
}
#resource-bar span span { /* Target the value spans */
color: #0056b3; /* Example color for values */
}
#resource-bar span #mana {
color: #8a2be2; /* BlueViolet for mana */
}
#build-menu, #city-grid-container, #message-log {
width: 80%;
max-width: 700px;
background-color: #e8e0c9;
border: 1px solid #c8bda1;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
h1, h2 {
text-align: center;
color: #5a4d35;
}
#build-menu button {
display: block;
margin: 5px auto;
padding: 8px 12px;
background-color: #a4885a;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
#build-menu button:hover {
background-color: #8a6d40;
}
#build-menu button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
#city-grid {
display: grid;
grid-template-columns: repeat(10, 1fr); /* 10x10 grid */
grid-template-rows: repeat(10, 1fr);
width: 400px; /* Fixed size grid */
height: 400px;
margin: 15px auto;
border: 2px solid #a89e80;
gap: 2px; /* Small gap between cells */
background-color: #c8bda1; /* Grid background */
}
.grid-cell {
background-color: #f0ead6; /* Empty cell color */
border: 1px solid #d8ccb0;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.5em; /* For building emojis/letters */
transition: background-color 0.2s;
}
.grid-cell:hover {
background-color: #e0d8c5;
}
/* Building styles */
.grid-cell.House { background-color: #b8a98f; content: '🏠'; }
.grid-cell.Mine { background-color: #8d8d8d; content: '⛏️'; }
.grid-cell.Mana.Well { background-color: #a890d8; content: '💧'; } /* Might need pseudo element for emoji */
/* Add emojis using ::after pseudo-elements for better control */
.grid-cell.House::after { content: '🏠'; }
.grid-cell.Mine::after { content: '⛏️'; }
.grid-cell.ManaWell::after { content: '💧'; } /* Use CSS safe class name */
#message-log ul {
list-style: none;
padding: 0;
height: 100px;
overflow-y: scroll;
border: 1px solid #ccc;
background-color: #fff;
padding: 5px;
}
#message-log li {
margin-bottom: 5px;
font-size: 0.9em;
border-bottom: 1px dashed #eee;
padding-bottom: 3px;
}
#message-log li.event {
color: purple;
font-weight: bold;
}
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Elements ---
const goldDisplay = document.getElementById('gold');
const manaDisplay = document.getElementById('mana');
const populationDisplay = document.getElementById('population');
const populationCapDisplay = document.getElementById('population-cap');
const buildMenu = document.getElementById('build-menu');
const cityGrid = document.getElementById('city-grid');
const messageList = document.getElementById('messages');
// --- Game State ---
let resources = {
gold: 100,
mana: 50,
population: 0,
populationCap: 5
};
let income = {
gold: 1, // Base income
mana: 0
};
const gridRows = 10;
const gridCols = 10;
let gridData = Array(gridRows).fill(null).map(() => Array(gridCols).fill(null)); // null = empty
let selectedBuilding = null;
// --- Event State ---
let currentEvent = null; // { name, description, effect, duration }
let eventTimer = null;
const EVENT_CHANCE_PER_TICK = 0.01; // 1% chance per second
// --- Building Definitions ---
// Simple definitions - expand later
const buildingDefs = {
'House': { cost: { gold: 50, mana: 0 }, provides: { populationCap: 5 }, requiresPop: 0, symbol: '🏠', cssClass: 'House' },
'Mine': { cost: { gold: 75, mana: 10 }, provides: { income: { gold: 2 } }, requiresPop: 1, symbol: '⛏️', cssClass: 'Mine' },
'Mana Well': { cost: { gold: 30, mana: 60 }, provides: { income: { mana: 1 } }, requiresPop: 1, symbol: '💧', cssClass: 'ManaWell' } // CSS safe name
};
// --- Initialization ---
function initGame() {
renderResources();
renderGrid();
setupEventListeners();
logMessage("Game Initialized. Start building!");
// Start the main game loop (updates every second)
setInterval(gameTick, 1000);
}
// --- Rendering Functions ---
function renderResources() {
goldDisplay.textContent = Math.floor(resources.gold);
manaDisplay.textContent = Math.floor(resources.mana);
populationDisplay.textContent = resources.population;
populationCapDisplay.textContent = resources.populationCap;
// Update button disabled state based on resources
buildMenu.querySelectorAll('button').forEach(button => {
const buildingType = button.dataset.building;
if (!buildingType) return;
const def = buildingDefs[buildingType];
button.disabled = resources.gold < def.cost.gold || resources.mana < def.cost.mana;
});
}
function renderGrid() {
cityGrid.innerHTML = ''; // Clear grid
for (let r = 0; r < gridRows; r++) {
for (let c = 0; c < gridCols; c++) {
const cell = document.createElement('div');
cell.classList.add('grid-cell');
cell.dataset.r = r;
cell.dataset.c = c;
const buildingType = gridData[r][c];
if (buildingType) {
const def = buildingDefs[buildingType];
cell.classList.add(def.cssClass); // Add class for styling/emoji
// cell.textContent = def.symbol; // Add symbol directly (alternative to CSS pseudo-elements)
cell.title = buildingType; // Tooltip
} else {
cell.addEventListener('click', handleGridCellClick);
}
cityGrid.appendChild(cell);
}
}
}
function logMessage(message, type = 'info') {
const li = document.createElement('li');
li.textContent = message;
if(type === 'event') {
li.classList.add('event');
}
messageList.prepend(li); // Add to top
// Keep log from getting too long (optional)
while (messageList.children.length > 20) {
messageList.removeChild(messageList.lastChild);
}
}
// --- Event Listeners ---
function setupEventListeners() {
// Build Menu Buttons
buildMenu.querySelectorAll('button').forEach(button => {
button.addEventListener('click', () => {
selectedBuilding = button.dataset.building;
// Optional: Add visual feedback for selection
logMessage(`Selected: ${selectedBuilding}. Click on an empty grid cell to build.`);
});
});
}
function handleGridCellClick(event) {
if (!selectedBuilding) return; // Nothing selected to build
const cell = event.target;
const r = parseInt(cell.dataset.r);
const c = parseInt(cell.dataset.c);
if (gridData[r][c] !== null) {
logMessage("Cell is already occupied!");
return;
}
const def = buildingDefs[selectedBuilding];
// Check cost
if (resources.gold < def.cost.gold || resources.mana < def.cost.mana) {
logMessage("Not enough resources!");
selectedBuilding = null; // Deselect
return;
}
// Check population requirement
if (resources.population + def.requiresPop > resources.populationCap) {
logMessage("Not enough population capacity for workers!");
// Don't deselect - they might build a house first
return;
}
// --- Place Building ---
// Deduct cost
resources.gold -= def.cost.gold;
resources.mana -= def.cost.mana;
// Add building to grid
gridData[r][c] = selectedBuilding;
// Apply building benefits
resources.population += def.requiresPop; // Assign population
if (def.provides.populationCap) {
resources.populationCap += def.provides.populationCap;
}
if (def.provides.income) {
income.gold += def.provides.income.gold || 0;
income.mana += def.provides.income.mana || 0;
}
logMessage(`Built ${selectedBuilding} at (${r},${c}).`);
selectedBuilding = null; // Deselect after building
renderGrid(); // Update grid visuals immediately
renderResources(); // Update resource display and buttons
}
// --- Game Loop ---
function gameTick() {
let currentIncome = { income }; // Start with base income
// Apply event effects
if (currentEvent) {
currentIncome = applyEventEffect(currentIncome);
currentEvent.duration--;
if (currentEvent.duration <= 0) {
endCurrentEvent();
}
} else {
// Try to trigger a new event
if (Math.random() < EVENT_CHANCE_PER_TICK) {
triggerRandomEvent();
}
}
// Add resources based on calculated income
resources.gold += currentIncome.gold;
resources.mana += currentIncome.mana;
// --- Update UI ---
renderResources(); // Update resource display every tick
// NOTE: Grid rendering is only done after building, not every tick, for performance.
}
// --- Magical Events ---
function triggerRandomEvent() {
const events = [
{ name: "Mana Surge", description: "Ley lines flare! Mana income doubled!", effect: (inc) => ({ inc, mana: inc.mana * 2 }), duration: 15 }, // 15 seconds
{ name: "Gold Rush", description: "Prospectors strike gold! Gold income doubled!", effect: (inc) => ({ inc, gold: inc.gold * 2 }), duration: 20 },
{ name: "Minor Curse", description: "A minor curse saps motivation. Gold income halved.", effect: (inc) => ({ inc, gold: inc.gold * 0.5 }), duration: 30 },
{ name: "Magical Dampening", description: "The ambient magic fades. Mana income halved.", effect: (inc) => ({ inc, mana: inc.mana * 0.5 }), duration: 25 },
];
currentEvent = events[Math.floor(Math.random() * events.length)];
logMessage(`EVENT: ${currentEvent.description} (Lasts ${currentEvent.duration}s)`, 'event');
}
function applyEventEffect(baseIncome) {
if (!currentEvent || !currentEvent.effect) return baseIncome;
return currentEvent.effect(baseIncome);
}
function endCurrentEvent() {
if (!currentEvent) return;
logMessage(`EVENT ENDED: ${currentEvent.name}`, 'event');
currentEvent = null;
}
// --- Start Game ---
initGame();
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.