Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Fractal Village - Medieval Planning Game</title>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
- body {
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
- background: linear-gradient(135deg, #1e3c72, #2a5298);
- color: #fff;
- overflow: hidden;
- position: relative;
- }
- #gameContainer {
- display: flex;
- height: 100vh;
- position: relative;
- }
- #sidebar {
- width: 280px;
- background: linear-gradient(180deg, rgba(0,0,0,0.8), rgba(0,0,0,0.9));
- padding: 20px;
- overflow-y: auto;
- border-right: 3px solid #ffd700;
- box-shadow: 5px 0 20px rgba(0,0,0,0.5);
- z-index: 10;
- }
- h1 {
- font-size: 24px;
- color: #ffd700;
- text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
- margin-bottom: 20px;
- text-align: center;
- }
- #resources {
- background: rgba(255,255,255,0.1);
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 20px;
- border: 2px solid #ffd700;
- }
- .resource {
- display: flex;
- justify-content: space-between;
- margin: 8px 0;
- padding: 5px;
- background: rgba(255,255,255,0.05);
- border-radius: 4px;
- }
- .resource-icon {
- margin-right: 8px;
- }
- .trade-buttons {
- display: flex;
- gap: 10px;
- margin-top: 10px;
- }
- .trade-btn {
- flex: 1;
- padding: 8px;
- background: linear-gradient(135deg, #2ecc71, #27ae60);
- color: white;
- border: 2px solid #229954;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- font-weight: bold;
- transition: all 0.3s;
- }
- .trade-btn:hover:not(:disabled) {
- background: linear-gradient(135deg, #3edc81, #37be70);
- transform: translateY(-1px);
- }
- .trade-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- #buildMenu {
- background: rgba(255,255,255,0.1);
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 20px;
- border: 2px solid #ffd700;
- }
- .building-btn {
- width: 100%;
- padding: 12px;
- margin: 8px 0;
- background: linear-gradient(135deg, #4a69bd, #3c5aa6);
- color: white;
- border: 2px solid #2c3e50;
- border-radius: 6px;
- cursor: pointer;
- font-size: 14px;
- font-weight: bold;
- transition: all 0.3s;
- position: relative;
- overflow: hidden;
- }
- .building-btn:hover:not(:disabled) {
- background: linear-gradient(135deg, #5a79cd, #4c6ab6);
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
- }
- .building-btn.selected {
- background: linear-gradient(135deg, #27ae60, #229954);
- border-color: #ffd700;
- box-shadow: 0 0 15px rgba(255,215,0,0.5);
- }
- .building-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .cost {
- font-size: 11px;
- color: #ffd700;
- margin-top: 4px;
- }
- #gameCanvas {
- flex: 1;
- position: relative;
- background: #2c3e50;
- }
- canvas {
- display: block;
- image-rendering: pixelated;
- image-rendering: crisp-edges;
- cursor: crosshair;
- }
- #info {
- position: absolute;
- bottom: 20px;
- left: 300px;
- right: 20px;
- background: linear-gradient(180deg, rgba(0,0,0,0.8), rgba(0,0,0,0.9));
- padding: 15px;
- border-radius: 8px;
- border: 2px solid #ffd700;
- max-width: 600px;
- box-shadow: 0 -5px 20px rgba(0,0,0,0.5);
- z-index: 10;
- }
- #selectionInfo {
- position: absolute;
- top: 80px;
- right: 20px;
- background: rgba(0,0,0,0.9);
- padding: 15px;
- border-radius: 8px;
- border: 2px solid #ffd700;
- width: 250px;
- display: none;
- z-index: 10;
- }
- #selectionInfo h4 {
- color: #ffd700;
- margin-bottom: 10px;
- }
- #selectionInfo .stat {
- margin: 5px 0;
- font-size: 13px;
- }
- #fractalLevel {
- background: rgba(255,255,255,0.1);
- padding: 15px;
- border-radius: 8px;
- border: 2px solid #ffd700;
- }
- .level-indicator {
- display: flex;
- align-items: center;
- margin: 10px 0;
- }
- .level-dot {
- width: 20px;
- height: 20px;
- border-radius: 50%;
- background: #34495e;
- margin: 0 5px;
- transition: all 0.3s;
- }
- .level-dot.active {
- background: #ffd700;
- box-shadow: 0 0 10px #ffd700;
- }
- button {
- padding: 8px 16px;
- background: linear-gradient(135deg, #e67e22, #d35400);
- color: white;
- border: 2px solid #a04000;
- border-radius: 6px;
- cursor: pointer;
- font-weight: bold;
- transition: all 0.3s;
- margin: 5px;
- }
- button:hover:not(:disabled) {
- background: linear-gradient(135deg, #f39c12, #e67e22);
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(0,0,0,0.3);
- }
- #tooltip {
- position: absolute;
- background: rgba(0,0,0,0.95);
- padding: 10px;
- border-radius: 6px;
- border: 2px solid #ffd700;
- pointer-events: none;
- display: none;
- z-index: 1000;
- font-size: 12px;
- max-width: 250px;
- }
- #controls {
- position: absolute;
- top: 20px;
- right: 20px;
- background: rgba(0,0,0,0.8);
- padding: 15px;
- border-radius: 8px;
- border: 2px solid #ffd700;
- z-index: 10;
- }
- #statsInfo {
- background: rgba(255,255,255,0.1);
- padding: 10px;
- border-radius: 8px;
- border: 1px solid #ffd700;
- margin-top: 10px;
- font-size: 12px;
- }
- .happiness-bar {
- width: 100%;
- height: 20px;
- background: #333;
- border-radius: 10px;
- overflow: hidden;
- margin: 5px 0;
- }
- .happiness-fill {
- height: 100%;
- background: linear-gradient(90deg, #e74c3c, #f39c12, #2ecc71);
- transition: width 0.3s;
- }
- .selected-building {
- animation: pulse 1s infinite;
- }
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.7; }
- }
- </style>
- </head>
- <body>
- <div id="gameContainer">
- <div id="sidebar">
- <h1>π° Fractal Village π°</h1>
- <div id="resources">
- <h3>Resources</h3>
- <div class="resource">
- <span><span class="resource-icon">πͺ</span>Gold</span>
- <span id="gold">100</span>
- </div>
- <div class="resource">
- <span><span class="resource-icon">πͺ΅</span>Wood</span>
- <span id="wood">50</span>
- </div>
- <div class="resource">
- <span><span class="resource-icon">βοΈ</span>Stone</span>
- <span id="stone">30</span>
- </div>
- <div class="resource">
- <span><span class="resource-icon">π</span>Food</span>
- <span id="food">50</span>
- </div>
- <div class="resource">
- <span><span class="resource-icon">π₯</span>Population</span>
- <span id="population">4</span>
- </div>
- <div class="trade-buttons">
- <button class="trade-btn" id="sellWood">
- Sell Wood<br>
- <small>100πͺ΅ β 30πͺ</small>
- </button>
- <button class="trade-btn" id="sellStone">
- Sell Stone<br>
- <small>100βοΈ β 50πͺ</small>
- </button>
- </div>
- </div>
- <div id="buildMenu">
- <h3>Build Menu</h3>
- <button class="building-btn" data-type="house">
- π House
- <div class="cost">π° 50 πͺ΅ 30</div>
- </button>
- <button class="building-btn" data-type="farm">
- πΎ Farm
- <div class="cost">π° 80 πͺ΅ 40</div>
- </button>
- <button class="building-btn" data-type="lumber">
- πͺ Lumber Mill
- <div class="cost">π° 100 βοΈ 20</div>
- </button>
- <button class="building-btn" data-type="quarry">
- βοΈ Quarry
- <div class="cost">π° 120 πͺ΅ 50</div>
- </button>
- <button class="building-btn" data-type="market">
- πͺ Market
- <div class="cost">π° 200 πͺ΅ 80 βοΈ 60</div>
- </button>
- <button class="building-btn" data-type="castle">
- π° Castle (District)
- <div class="cost">π° 500 πͺ΅ 200 βοΈ 150</div>
- </button>
- <button class="building-btn" data-type="delete">
- ποΈ Delete Mode
- <div class="cost">Free</div>
- </button>
- </div>
- <div id="fractalLevel">
- <h3>Fractal Depth</h3>
- <div class="level-indicator">
- <span>Level:</span>
- <div class="level-dot active"></div>
- <div class="level-dot"></div>
- <div class="level-dot"></div>
- </div>
- <button id="zoomIn">π Enter District</button>
- <button id="zoomOut">π Exit District</button>
- </div>
- <div id="statsInfo">
- <h4>Village Stats</h4>
- <div>π Happiness: <span id="happinessLevel">75%</span></div>
- <div class="happiness-bar">
- <div class="happiness-fill" id="happinessBar" style="width: 75%"></div>
- </div>
- <div>πΆ Working: <span id="workingCount">0</span></div>
- <div>π΄ Resting: <span id="restingCount">0</span></div>
- <div>π Food Status: <span id="foodStatus">Adequate</span></div>
- </div>
- </div>
- <div id="gameCanvas">
- <canvas id="canvas"></canvas>
- <div id="controls">
- <button id="pauseBtn">βΈοΈ Pause</button>
- <button id="speedBtn">β© Speed: 1x</button>
- </div>
- <div id="selectionInfo">
- <h4 id="selectionTitle">Building Info</h4>
- <div id="selectionDetails"></div>
- </div>
- </div>
- <div id="info">
- <p>Left-click buildings to see details. Right-click workers to check happiness. Trade resources to grow your village!</p>
- </div>
- <div id="tooltip"></div>
- </div>
- <script>
- const canvas = document.getElementById('canvas');
- const ctx = canvas.getContext('2d');
- // Constants
- const TILE_SIZE = 32;
- const MAP_WIDTH = 25;
- const MAP_HEIGHT = 18;
- const FOOD_PER_PERSON = 0.15; // Food consumed per person per second
- // Tile types
- const TILES = {
- GRASS: 0,
- GRASS_ALT: 1,
- TREE: 2,
- ROCK: 3,
- WATER: 4,
- DIRT: 5,
- FLOWERS: 6
- };
- // Game state
- const game = {
- gold: 100,
- wood: 50,
- stone: 30,
- food: 50,
- population: 4,
- happiness: 75,
- foodSurplus: true,
- paused: false,
- speed: 1,
- selectedBuilding: null,
- selectedObject: null,
- fractalLevel: 0,
- maxFractalLevel: 2,
- currentDistrict: null,
- districts: new Map(),
- lastTime: 0,
- accumulator: 0,
- workers: [],
- hoverPos: null,
- markets: []
- };
- // Building types
- const buildings = {
- house: {
- tile: 'π ',
- color: '#8b4513',
- cost: { gold: 50, wood: 30 },
- provides: { population: 4 },
- consumes: { food: FOOD_PER_PERSON * 4 },
- size: 1
- },
- farm: {
- tile: 'πΎ',
- color: '#90ee90',
- cost: { gold: 80, wood: 40 },
- provides: { food: 2 },
- size: 2,
- needsWorker: true
- },
- lumber: {
- tile: 'πͺ',
- color: '#654321',
- cost: { gold: 100, stone: 20 },
- provides: { wood: 3 },
- size: 2,
- needsWorker: true
- },
- quarry: {
- tile: 'βοΈ',
- color: '#696969',
- cost: { gold: 120, wood: 50 },
- provides: { stone: 2 },
- size: 2,
- needsWorker: true
- },
- market: {
- tile: 'πͺ',
- color: '#ffd700',
- cost: { gold: 200, wood: 80, stone: 60 },
- provides: { gold: 5 },
- autoTrade: { wood: 2, stone: 1 }, // Trades 2 wood or 1 stone for 1 gold
- size: 2,
- needsWorker: true
- },
- castle: {
- tile: 'π°',
- color: '#4a4a4a',
- cost: { gold: 500, wood: 200, stone: 150 },
- provides: {},
- size: 3,
- isDistrict: true
- }
- };
- // Worker class
- class Worker {
- constructor(home, workplace) {
- this.home = home;
- this.workplace = workplace;
- this.x = home.x * TILE_SIZE + TILE_SIZE/2;
- this.y = home.y * TILE_SIZE + TILE_SIZE/2;
- this.targetX = this.x;
- this.targetY = this.y;
- this.state = 'working';
- this.stateTimer = Math.random() * 10 + 5;
- this.speed = 40;
- this.baseSpeed = 40;
- this.tile = ['π¨βπΎ', 'π©βπΎ', 'π§βπΎ'][Math.floor(Math.random() * 3)];
- this.happiness = 75;
- this.productivity = 1.0;
- }
- update(deltaTime) {
- if (game.paused) return;
- // Update happiness based on food status
- if (game.foodSurplus) {
- this.happiness = Math.min(100, this.happiness + deltaTime * 2);
- } else if (game.food <= 0) {
- this.happiness = Math.max(0, this.happiness - deltaTime * 5);
- } else {
- this.happiness = Math.max(50, Math.min(75, this.happiness));
- }
- // Happiness affects productivity and speed
- this.productivity = 0.5 + (this.happiness / 200);
- this.speed = this.baseSpeed * (0.7 + this.happiness / 300);
- // Low happiness makes workers rest more
- const restModifier = this.happiness < 30 ? 2 : 1;
- this.stateTimer -= deltaTime * game.speed;
- if (this.stateTimer <= 0) {
- if (this.state === 'working') {
- this.state = 'resting';
- this.stateTimer = (Math.random() * 5 + 3) * restModifier;
- this.setTarget(this.home);
- } else if (this.state === 'resting') {
- this.state = 'working';
- this.stateTimer = Math.random() * 10 + 8;
- this.setTarget(this.workplace);
- }
- }
- const dx = this.targetX - this.x;
- const dy = this.targetY - this.y;
- const dist = Math.sqrt(dx * dx + dy * dy);
- if (dist > 2) {
- const moveX = (dx / dist) * this.speed * deltaTime * game.speed;
- const moveY = (dy / dist) * this.speed * deltaTime * game.speed;
- this.x += moveX;
- this.y += moveY;
- }
- }
- setTarget(building) {
- if (!building) return;
- const size = buildings[building.type] ? buildings[building.type].size : 1;
- this.targetX = building.x * TILE_SIZE + (size * TILE_SIZE) / 2;
- this.targetY = building.y * TILE_SIZE + (size * TILE_SIZE) / 2;
- }
- draw() {
- // Shadow
- ctx.fillStyle = 'rgba(0,0,0,0.3)';
- ctx.beginPath();
- ctx.ellipse(this.x, this.y + 8, 6, 3, 0, 0, Math.PI * 2);
- ctx.fill();
- // Worker tile with happiness tint
- ctx.save();
- if (this.happiness < 30) {
- ctx.filter = 'hue-rotate(-30deg) saturate(0.5)';
- } else if (this.happiness > 80) {
- ctx.filter = 'hue-rotate(10deg) saturate(1.2)';
- }
- ctx.font = '16px serif';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(this.tile, this.x, this.y);
- ctx.restore();
- // State indicator
- if (this.state === 'resting') {
- ctx.font = '12px serif';
- ctx.fillText('π€', this.x + 8, this.y - 8);
- } else if (this.happiness < 30) {
- ctx.font = '12px serif';
- ctx.fillText('π', this.x + 8, this.y - 8);
- } else if (this.happiness > 80) {
- ctx.font = '12px serif';
- ctx.fillText('π', this.x + 8, this.y - 8);
- }
- }
- }
- // District class
- class District {
- constructor(level) {
- this.level = level;
- this.tiles = Array(MAP_HEIGHT).fill().map(() => Array(MAP_WIDTH).fill(TILES.GRASS));
- this.buildings = [];
- this.grid = Array(MAP_HEIGHT).fill().map(() => Array(MAP_WIDTH).fill(null));
- this.generateTerrain();
- if (level === 0) {
- this.addStartingBuildings();
- }
- }
- generateTerrain() {
- for (let y = 0; y < MAP_HEIGHT; y++) {
- for (let x = 0; x < MAP_WIDTH; x++) {
- this.tiles[y][x] = Math.random() < 0.7 ? TILES.GRASS : TILES.GRASS_ALT;
- if (Math.random() < 0.05) {
- this.tiles[y][x] = TILES.FLOWERS;
- }
- if (x > 8 && Math.random() < 0.08) {
- this.grid[y][x] = { type: 'tree', tile: TILES.TREE };
- } else if (x > 8 && Math.random() < 0.03) {
- this.grid[y][x] = { type: 'rock', tile: TILES.ROCK };
- }
- }
- }
- if (Math.random() < 0.3) {
- const riverX = Math.floor(MAP_WIDTH * 0.7 + Math.random() * 5);
- for (let y = 0; y < MAP_HEIGHT; y++) {
- if (Math.random() < 0.6) {
- this.tiles[y][riverX] = TILES.WATER;
- if (riverX > 0) this.tiles[y][riverX-1] = TILES.DIRT;
- if (riverX < MAP_WIDTH-1) this.tiles[y][riverX+1] = TILES.DIRT;
- }
- }
- }
- }
- addStartingBuildings() {
- for (let y = 2; y < 8; y++) {
- for (let x = 2; x < 8; x++) {
- this.grid[y][x] = null;
- this.tiles[y][x] = TILES.DIRT;
- }
- }
- this.addBuilding(2, 3, 'house', true);
- this.addBuilding(4, 3, 'farm', true);
- this.addBuilding(2, 5, 'lumber', true);
- }
- addBuilding(x, y, type, skipCost) {
- const building = buildings[type];
- if (!building) return false;
- for (let dy = 0; dy < building.size; dy++) {
- for (let dx = 0; dx < building.size; dx++) {
- if (y + dy >= MAP_HEIGHT || x + dx >= MAP_WIDTH) return false;
- if (this.grid[y + dy][x + dx] !== null) return false;
- if (this.tiles[y + dy][x + dx] === TILES.WATER) return false;
- }
- }
- if (!skipCost) {
- for (let resource in building.cost) {
- if (game[resource] < building.cost[resource]) return false;
- }
- for (let resource in building.cost) {
- game[resource] -= building.cost[resource];
- }
- }
- const buildingData = {
- type: type,
- x: x,
- y: y,
- size: building.size,
- id: Date.now() + Math.random(),
- production: 0,
- worker: null
- };
- for (let dy = 0; dy < building.size; dy++) {
- for (let dx = 0; dx < building.size; dx++) {
- this.grid[y + dy][x + dx] = buildingData;
- this.tiles[y + dy][x + dx] = TILES.DIRT;
- }
- }
- this.buildings.push(buildingData);
- if (building.provides && building.provides.population) {
- game.population += building.provides.population;
- }
- if (type === 'market') {
- game.markets.push(buildingData);
- }
- if (building.needsWorker && this.level === game.fractalLevel) {
- const house = this.buildings.find(b => b.type === 'house');
- if (house) {
- const worker = new Worker(house, buildingData);
- game.workers.push(worker);
- buildingData.worker = worker;
- }
- }
- if (building.isDistrict && this.level < game.maxFractalLevel) {
- const key = this.level + '-' + x + '-' + y;
- game.districts.set(key, new District(this.level + 1));
- }
- return true;
- }
- removeBuilding(x, y) {
- const cell = this.grid[y] && this.grid[y][x];
- if (!cell) return false;
- if (cell.type === 'tree' || cell.type === 'rock') {
- this.grid[y][x] = null;
- this.tiles[y][x] = TILES.GRASS;
- return true;
- }
- if (!cell.type) return false;
- const building = buildings[cell.type];
- const bx = cell.x;
- const by = cell.y;
- game.workers = game.workers.filter(w => w.workplace.id !== cell.id);
- if (cell.type === 'market') {
- game.markets = game.markets.filter(m => m.id !== cell.id);
- }
- for (let dy = 0; dy < building.size; dy++) {
- for (let dx = 0; dx < building.size; dx++) {
- if (this.grid[by + dy] && this.grid[by + dy][bx + dx]) {
- this.grid[by + dy][bx + dx] = null;
- this.tiles[by + dy][bx + dx] = TILES.GRASS;
- }
- }
- }
- this.buildings = this.buildings.filter(b => b.id !== cell.id);
- if (building.provides && building.provides.population) {
- game.population = Math.max(0, game.population - building.provides.population);
- }
- for (let resource in building.cost) {
- game[resource] += Math.floor(building.cost[resource] * 0.5);
- }
- return true;
- }
- }
- // Draw tile
- function drawTile(tileType, x, y) {
- const px = x * TILE_SIZE;
- const py = y * TILE_SIZE;
- switch(tileType) {
- case TILES.GRASS:
- ctx.fillStyle = '#4a7c59';
- ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
- break;
- case TILES.GRASS_ALT:
- ctx.fillStyle = '#5a8c69';
- ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
- break;
- case TILES.TREE:
- ctx.fillStyle = '#4a7c59';
- ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
- ctx.font = '24px serif';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText('π²', px + TILE_SIZE/2, py + TILE_SIZE/2);
- break;
- case TILES.ROCK:
- ctx.fillStyle = '#5a8c69';
- ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
- ctx.font = '20px serif';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText('πͺ¨', px + TILE_SIZE/2, py + TILE_SIZE/2);
- break;
- case TILES.WATER:
- ctx.fillStyle = '#4682b4';
- ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
- break;
- case TILES.DIRT:
- ctx.fillStyle = '#8b7355';
- ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
- break;
- case TILES.FLOWERS:
- ctx.fillStyle = '#4a7c59';
- ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
- ctx.font = '16px serif';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText('πΌ', px + TILE_SIZE/2, py + TILE_SIZE/2);
- break;
- }
- ctx.strokeStyle = 'rgba(0,0,0,0.05)';
- ctx.strokeRect(px, py, TILE_SIZE, TILE_SIZE);
- }
- // Initialize
- function init() {
- canvas.width = MAP_WIDTH * TILE_SIZE;
- canvas.height = MAP_HEIGHT * TILE_SIZE;
- game.districts.set('main', new District(0));
- game.currentDistrict = game.districts.get('main');
- game.currentDistrict.buildings.forEach(building => {
- const type = buildings[building.type];
- if (type && type.needsWorker) {
- const house = game.currentDistrict.buildings.find(b => b.type === 'house');
- if (house) {
- const worker = new Worker(house, building);
- game.workers.push(worker);
- building.worker = worker;
- }
- }
- });
- updateUI();
- }
- // Update resources
- function updateResources(deltaTime) {
- if (game.paused) return;
- const multiplier = deltaTime * game.speed;
- // Production from buildings
- game.currentDistrict.buildings.forEach(building => {
- const type = buildings[building.type];
- if (type && type.provides && !type.provides.population) {
- let productivity = 1.0;
- if (type.needsWorker) {
- const worker = game.workers.find(w => w.workplace.id === building.id);
- if (worker && worker.state === 'working') {
- productivity = worker.productivity;
- } else {
- productivity = 0;
- }
- }
- for (let resource in type.provides) {
- const amount = type.provides[resource] * multiplier * productivity;
- game[resource] += amount;
- building.production += amount;
- }
- }
- // Market auto-trading
- if (building.type === 'market' && building.worker) {
- const worker = building.worker;
- if (worker.state === 'working') {
- // Trade wood for gold
- if (game.wood >= 2) {
- const tradeAmount = Math.min(2 * multiplier * worker.productivity, game.wood / 2);
- game.wood -= tradeAmount * 2;
- game.gold += tradeAmount;
- }
- // Trade stone for gold
- if (game.stone >= 1) {
- const tradeAmount = Math.min(multiplier * worker.productivity, game.stone);
- game.stone -= tradeAmount;
- game.gold += tradeAmount;
- }
- }
- }
- });
- // Food consumption
- const foodConsumption = game.population * FOOD_PER_PERSON * multiplier;
- game.food -= foodConsumption;
- // Check food status
- const foodProduction = game.currentDistrict.buildings
- .filter(b => b.type === 'farm')
- .reduce((sum, b) => {
- const worker = game.workers.find(w => w.workplace.id === b.id);
- return sum + (worker && worker.state === 'working' ? 2 * worker.productivity : 0);
- }, 0);
- game.foodSurplus = foodProduction > game.population * FOOD_PER_PERSON;
- if (game.food < 0) {
- game.food = 0;
- // Population decline from starvation
- if (Math.random() < 0.01 * multiplier) {
- game.population = Math.max(1, game.population - 1);
- }
- }
- // Update happiness
- if (game.foodSurplus) {
- game.happiness = Math.min(100, game.happiness + deltaTime * 2);
- } else if (game.food <= 0) {
- game.happiness = Math.max(0, game.happiness - deltaTime * 5);
- } else {
- game.happiness = Math.max(50, Math.min(75, game.happiness));
- }
- // Update UI stats
- const working = game.workers.filter(w => w.state === 'working').length;
- const resting = game.workers.filter(w => w.state === 'resting').length;
- document.getElementById('workingCount').textContent = working;
- document.getElementById('restingCount').textContent = resting;
- document.getElementById('happinessLevel').textContent = Math.floor(game.happiness) + '%';
- document.getElementById('happinessBar').style.width = game.happiness + '%';
- let foodStatus = 'Adequate';
- if (game.foodSurplus) foodStatus = 'Surplus π';
- else if (game.food <= 0) foodStatus = 'Starving! π°';
- else if (game.food < 20) foodStatus = 'Low β οΈ';
- document.getElementById('foodStatus').textContent = foodStatus;
- }
- // Render
- function render() {
- const district = game.currentDistrict;
- // Draw terrain tiles
- for (let y = 0; y < MAP_HEIGHT; y++) {
- for (let x = 0; x < MAP_WIDTH; x++) {
- drawTile(district.tiles[y][x], x, y);
- }
- }
- // Draw objects and buildings
- const drawnBuildings = new Set();
- for (let y = 0; y < MAP_HEIGHT; y++) {
- for (let x = 0; x < MAP_WIDTH; x++) {
- const cell = district.grid[y][x];
- if (!cell) continue;
- if (cell.type === 'tree') {
- drawTile(TILES.TREE, x, y);
- } else if (cell.type === 'rock') {
- drawTile(TILES.ROCK, x, y);
- } else if (!drawnBuildings.has(cell.id)) {
- drawnBuildings.add(cell.id);
- const building = buildings[cell.type];
- // Highlight selected building
- if (game.selectedObject && game.selectedObject.id === cell.id) {
- ctx.strokeStyle = '#ffd700';
- ctx.lineWidth = 3;
- ctx.strokeRect(
- cell.x * TILE_SIZE - 2,
- cell.y * TILE_SIZE - 2,
- building.size * TILE_SIZE + 4,
- building.size * TILE_SIZE + 4
- );
- }
- // Building shadow
- ctx.fillStyle = 'rgba(0,0,0,0.2)';
- ctx.fillRect(
- cell.x * TILE_SIZE + 4,
- cell.y * TILE_SIZE + 4,
- building.size * TILE_SIZE - 2,
- building.size * TILE_SIZE - 2
- );
- // Building base
- ctx.fillStyle = building.color;
- ctx.fillRect(
- cell.x * TILE_SIZE + 1,
- cell.y * TILE_SIZE + 1,
- building.size * TILE_SIZE - 2,
- building.size * TILE_SIZE - 2
- );
- // Building tile
- ctx.font = (building.size * 18) + 'px serif';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText(
- building.tile,
- cell.x * TILE_SIZE + (building.size * TILE_SIZE) / 2,
- cell.y * TILE_SIZE + (building.size * TILE_SIZE) / 2
- );
- }
- }
- }
- // Draw workers
- game.workers.forEach(worker => worker.draw());
- // Draw hover preview
- if (game.selectedBuilding && game.selectedBuilding !== 'delete' && game.hoverPos) {
- const building = buildings[game.selectedBuilding];
- const gx = Math.floor(game.hoverPos.x / TILE_SIZE);
- const gy = Math.floor(game.hoverPos.y / TILE_SIZE);
- if (gx >= 0 && gy >= 0 && gx < MAP_WIDTH && gy < MAP_HEIGHT) {
- let canPlace = true;
- for (let dy = 0; dy < building.size; dy++) {
- for (let dx = 0; dx < building.size; dx++) {
- if (gy + dy >= MAP_HEIGHT || gx + dx >= MAP_WIDTH ||
- district.grid[gy + dy][gx + dx] !== null ||
- district.tiles[gy + dy][gx + dx] === TILES.WATER) {
- canPlace = false;
- }
- }
- }
- for (let resource in building.cost) {
- if (game[resource] < building.cost[resource]) canPlace = false;
- }
- ctx.fillStyle = canPlace ? 'rgba(0, 255, 0, 0.3)' : 'rgba(255, 0, 0, 0.3)';
- ctx.fillRect(gx * TILE_SIZE, gy * TILE_SIZE, building.size * TILE_SIZE, building.size * TILE_SIZE);
- }
- }
- }
- // Update UI
- function updateUI() {
- document.getElementById('gold').textContent = Math.floor(game.gold);
- document.getElementById('wood').textContent = Math.floor(game.wood);
- document.getElementById('stone').textContent = Math.floor(game.stone);
- document.getElementById('food').textContent = Math.floor(game.food);
- document.getElementById('population').textContent = game.population;
- document.querySelectorAll('.level-dot').forEach((dot, i) => {
- dot.classList.toggle('active', i <= game.fractalLevel);
- });
- document.querySelectorAll('.building-btn').forEach(btn => {
- const type = btn.dataset.type;
- if (type === 'delete') return;
- const building = buildings[type];
- let canAfford = true;
- for (let resource in building.cost) {
- if (game[resource] < building.cost[resource]) canAfford = false;
- }
- btn.disabled = !canAfford;
- });
- // Update trade buttons
- document.getElementById('sellWood').disabled = game.wood < 100;
- document.getElementById('sellStone').disabled = game.stone < 100;
- }
- // Show building info
- function showBuildingInfo(building) {
- const info = document.getElementById('selectionInfo');
- const title = document.getElementById('selectionTitle');
- const details = document.getElementById('selectionDetails');
- const type = buildings[building.type];
- title.textContent = building.type.charAt(0).toUpperCase() + building.type.slice(1);
- let html = `<div class="stat">${type.tile} ${building.type}</div>`;
- html += `<div class="stat">Position: ${building.x}, ${building.y}</div>`;
- if (type.provides) {
- for (let resource in type.provides) {
- if (resource !== 'population') {
- html += `<div class="stat">Produces: ${type.provides[resource]} ${resource}/s</div>`;
- } else {
- html += `<div class="stat">Houses: ${type.provides[resource]} people</div>`;
- }
- }
- }
- if (type.consumes) {
- for (let resource in type.consumes) {
- html += `<div class="stat">Consumes: ${type.consumes[resource].toFixed(2)} ${resource}/s</div>`;
- }
- }
- if (building.worker) {
- const worker = game.workers.find(w => w.workplace.id === building.id);
- if (worker) {
- html += `<div class="stat">Worker: ${worker.tile}</div>`;
- html += `<div class="stat">Happiness: ${Math.floor(worker.happiness)}%</div>`;
- html += `<div class="stat">Productivity: ${Math.floor(worker.productivity * 100)}%</div>`;
- html += `<div class="stat">Status: ${worker.state}</div>`;
- }
- } else if (type.needsWorker) {
- html += `<div class="stat">Worker: None assigned</div>`;
- }
- if (building.type === 'market') {
- html += `<div class="stat">Auto-trades resources for gold</div>`;
- }
- details.innerHTML = html;
- info.style.display = 'block';
- }
- // Show worker info
- function showWorkerInfo(worker) {
- const info = document.getElementById('selectionInfo');
- const title = document.getElementById('selectionTitle');
- const details = document.getElementById('selectionDetails');
- title.textContent = 'Worker Info';
- let html = `<div class="stat">Worker: ${worker.tile}</div>`;
- html += `<div class="stat">Happiness: ${Math.floor(worker.happiness)}%</div>`;
- html += `<div class="stat">Productivity: ${Math.floor(worker.productivity * 100)}%</div>`;
- html += `<div class="stat">Status: ${worker.state}</div>`;
- const workplace = game.currentDistrict.buildings.find(b => b.id === worker.workplace.id);
- if (workplace) {
- html += `<div class="stat">Works at: ${workplace.type}</div>`;
- }
- if (worker.happiness < 30) {
- html += `<div class="stat" style="color: #e74c3c;">Very unhappy - needs food!</div>`;
- } else if (worker.happiness > 80) {
- html += `<div class="stat" style="color: #2ecc71;">Very happy - food surplus!</div>`;
- }
- details.innerHTML = html;
- info.style.display = 'block';
- }
- // Event handlers
- document.querySelectorAll('.building-btn').forEach(btn => {
- btn.addEventListener('click', function() {
- document.querySelectorAll('.building-btn').forEach(b =>
- b.classList.remove('selected')
- );
- btn.classList.add('selected');
- game.selectedBuilding = btn.dataset.type;
- game.selectedObject = null;
- document.getElementById('selectionInfo').style.display = 'none';
- });
- });
- // Trade buttons
- document.getElementById('sellWood').addEventListener('click', function() {
- if (game.wood >= 100) {
- game.wood -= 100;
- game.gold += 30;
- updateUI();
- createParticle(150, 250, 'π°');
- }
- });
- document.getElementById('sellStone').addEventListener('click', function() {
- if (game.stone >= 100) {
- game.stone -= 100;
- game.gold += 50;
- updateUI();
- createParticle(230, 250, 'π°');
- }
- });
- canvas.addEventListener('mousemove', function(e) {
- const rect = canvas.getBoundingClientRect();
- game.hoverPos = {
- x: e.clientX - rect.left,
- y: e.clientY - rect.top
- };
- });
- // Left click for selection
- canvas.addEventListener('click', function(e) {
- if (!game.hoverPos) return;
- const gx = Math.floor(game.hoverPos.x / TILE_SIZE);
- const gy = Math.floor(game.hoverPos.y / TILE_SIZE);
- if (gx < 0 || gy < 0 || gx >= MAP_WIDTH || gy >= MAP_HEIGHT) return;
- if (game.selectedBuilding === 'delete') {
- if (game.currentDistrict.removeBuilding(gx, gy)) {
- createParticle(e.clientX, e.clientY, 'π₯');
- updateUI();
- }
- } else if (game.selectedBuilding && game.selectedBuilding !== 'delete') {
- if (game.currentDistrict.addBuilding(gx, gy, game.selectedBuilding)) {
- createParticle(e.clientX, e.clientY, 'β¨');
- updateUI();
- }
- } else {
- // Select building
- const cell = game.currentDistrict.grid[gy][gx];
- if (cell && cell.type && cell.type !== 'tree' && cell.type !== 'rock') {
- game.selectedObject = cell;
- showBuildingInfo(cell);
- } else {
- game.selectedObject = null;
- document.getElementById('selectionInfo').style.display = 'none';
- }
- }
- });
- // Right click for worker info
- canvas.addEventListener('contextmenu', function(e) {
- e.preventDefault();
- const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
- // Check if clicking on a worker
- for (let worker of game.workers) {
- const dist = Math.sqrt((worker.x - x) ** 2 + (worker.y - y) ** 2);
- if (dist < 16) {
- showWorkerInfo(worker);
- return;
- }
- }
- });
- document.getElementById('zoomIn').addEventListener('click', function() {
- if (game.fractalLevel >= game.maxFractalLevel) {
- document.getElementById('info').textContent = 'Maximum fractal depth reached!';
- return;
- }
- const castle = game.currentDistrict.buildings.find(b => b.type === 'castle');
- if (castle) {
- const key = game.fractalLevel + '-' + castle.x + '-' + castle.y;
- const subDistrict = game.districts.get(key);
- if (subDistrict) {
- game.workers = [];
- game.markets = [];
- game.currentDistrict = subDistrict;
- game.fractalLevel++;
- subDistrict.buildings.forEach(building => {
- const type = buildings[building.type];
- if (type && type.needsWorker) {
- const house = subDistrict.buildings.find(b => b.type === 'house');
- if (house) {
- const worker = new Worker(house, building);
- game.workers.push(worker);
- building.worker = worker;
- }
- }
- if (building.type === 'market') {
- game.markets.push(building);
- }
- });
- updateUI();
- createParticle(window.innerWidth/2, window.innerHeight/2, 'π');
- document.getElementById('info').textContent = 'Entered district level ' + (game.fractalLevel + 1);
- }
- } else {
- document.getElementById('info').textContent = 'Build a castle first to create a sub-district!';
- }
- });
- document.getElementById('zoomOut').addEventListener('click', function() {
- if (game.fractalLevel === 0) {
- document.getElementById('info').textContent = 'Already at main village level!';
- return;
- }
- game.workers = [];
- game.markets = [];
- game.fractalLevel--;
- if (game.fractalLevel === 0) {
- game.currentDistrict = game.districts.get('main');
- } else {
- for (let entry of game.districts) {
- if (entry[1].level === game.fractalLevel) {
- game.currentDistrict = entry[1];
- break;
- }
- }
- }
- game.currentDistrict.buildings.forEach(building => {
- const type = buildings[building.type];
- if (type && type.needsWorker) {
- const house = game.currentDistrict.buildings.find(b => b.type === 'house');
- if (house) {
- const worker = new Worker(house, building);
- game.workers.push(worker);
- building.worker = worker;
- }
- }
- if (building.type === 'market') {
- game.markets.push(building);
- }
- });
- updateUI();
- createParticle(window.innerWidth/2, window.innerHeight/2, 'π');
- document.getElementById('info').textContent = 'Returned to level ' + (game.fractalLevel + 1);
- });
- document.getElementById('pauseBtn').addEventListener('click', function() {
- game.paused = !game.paused;
- document.getElementById('pauseBtn').textContent = game.paused ? 'βΆοΈ Play' : 'βΈοΈ Pause';
- });
- document.getElementById('speedBtn').addEventListener('click', function() {
- game.speed = game.speed === 1 ? 2 : game.speed === 2 ? 3 : 1;
- document.getElementById('speedBtn').textContent = 'β© Speed: ' + game.speed + 'x';
- });
- function createParticle(x, y, emoji) {
- const particle = document.createElement('div');
- particle.style.position = 'absolute';
- particle.style.left = x + 'px';
- particle.style.top = y + 'px';
- particle.style.fontSize = '24px';
- particle.style.pointerEvents = 'none';
- particle.style.zIndex = '1000';
- particle.textContent = emoji;
- particle.style.animation = 'particle-float 1s ease-out forwards';
- document.body.appendChild(particle);
- setTimeout(function() {
- particle.remove();
- }, 1000);
- }
- // Add animation style
- const style = document.createElement('style');
- style.textContent = '@keyframes particle-float { 0% { transform: translateY(0) scale(1); opacity: 1; } 100% { transform: translateY(-50px) scale(0); opacity: 0; } }';
- document.head.appendChild(style);
- // Tooltip
- const tooltip = document.getElementById('tooltip');
- canvas.addEventListener('mousemove', function(e) {
- const rect = canvas.getBoundingClientRect();
- const x = Math.floor((e.clientX - rect.left) / TILE_SIZE);
- const y = Math.floor((e.clientY - rect.top) / TILE_SIZE);
- if (x < 0 || y < 0 || x >= MAP_WIDTH || y >= MAP_HEIGHT) {
- tooltip.style.display = 'none';
- return;
- }
- const cell = game.currentDistrict.grid[y][x];
- if (cell) {
- let content = '';
- if (cell.type === 'tree') {
- content = '<strong>π² Tree</strong><br>Can be cleared for building';
- } else if (cell.type === 'rock') {
- content = '<strong>πͺ¨ Rock</strong><br>Can be cleared for building';
- } else if (cell.type) {
- const building = buildings[cell.type];
- content = '<strong>' + building.tile + ' ' + cell.type.charAt(0).toUpperCase() + cell.type.slice(1) + '</strong><br>';
- if (building.isDistrict) {
- content += 'Click "Enter District" to explore!';
- } else if (building.provides) {
- for (let k in building.provides) {
- if (k === 'population') {
- content += 'Houses ' + building.provides[k] + ' people<br>';
- } else {
- content += 'Produces: ' + building.provides[k] + ' ' + k + '/s<br>';
- }
- }
- if (building.needsWorker) {
- const hasWorker = game.workers.some(w => w.workplace.id === cell.id);
- content += 'Worker: ' + (hasWorker ? 'β Assigned' : 'β Needs worker');
- }
- }
- if (cell.type === 'house') {
- content += 'Consumes: ' + (FOOD_PER_PERSON * 4).toFixed(2) + ' food/s';
- }
- }
- if (content) {
- tooltip.innerHTML = content;
- tooltip.style.display = 'block';
- tooltip.style.left = e.clientX + 10 + 'px';
- tooltip.style.top = e.clientY - 30 + 'px';
- } else {
- tooltip.style.display = 'none';
- }
- } else {
- const tileType = game.currentDistrict.tiles[y][x];
- if (tileType === TILES.WATER) {
- tooltip.innerHTML = '<strong>π§ Water</strong><br>Cannot build here';
- tooltip.style.display = 'block';
- tooltip.style.left = e.clientX + 10 + 'px';
- tooltip.style.top = e.clientY - 30 + 'px';
- } else {
- tooltip.style.display = 'none';
- }
- }
- });
- canvas.addEventListener('mouseleave', function() {
- tooltip.style.display = 'none';
- });
- // Keyboard shortcuts
- document.addEventListener('keydown', function(e) {
- switch(e.key) {
- case '1':
- document.querySelector('[data-type="house"]').click();
- break;
- case '2':
- document.querySelector('[data-type="farm"]').click();
- break;
- case '3':
- document.querySelector('[data-type="lumber"]').click();
- break;
- case '4':
- document.querySelector('[data-type="quarry"]').click();
- break;
- case '5':
- document.querySelector('[data-type="market"]').click();
- break;
- case '6':
- document.querySelector('[data-type="castle"]').click();
- break;
- case 'd':
- case 'Delete':
- document.querySelector('[data-type="delete"]').click();
- break;
- case ' ':
- e.preventDefault();
- document.getElementById('pauseBtn').click();
- break;
- case '+':
- case '=':
- document.getElementById('zoomIn').click();
- break;
- case '-':
- case '_':
- document.getElementById('zoomOut').click();
- break;
- case 'Escape':
- game.selectedBuilding = null;
- game.selectedObject = null;
- document.querySelectorAll('.building-btn').forEach(b =>
- b.classList.remove('selected')
- );
- document.getElementById('selectionInfo').style.display = 'none';
- break;
- }
- });
- // Victory check
- function checkVictory() {
- if (game.population >= 50 && game.gold >= 2000 && !game.victoryAchieved) {
- game.victoryAchieved = true;
- document.getElementById('info').innerHTML = '<strong style="color: #ffd700;">π Victory! You have built a thriving fractal kingdom! π</strong>';
- for (let i = 0; i < 10; i++) {
- setTimeout(function() {
- createParticle(
- Math.random() * window.innerWidth,
- Math.random() * window.innerHeight,
- ['π', 'π', 'π', 'β'][Math.floor(Math.random() * 4)]
- );
- }, i * 100);
- }
- }
- }
- // Game loop
- function gameLoop(currentTime) {
- const deltaTime = Math.min((currentTime - game.lastTime) / 1000, 0.1);
- game.lastTime = currentTime;
- if (!game.paused) {
- game.accumulator += deltaTime;
- while (game.accumulator >= 0.016) {
- updateResources(0.016);
- game.workers.forEach(function(worker) {
- worker.update(0.016);
- });
- game.accumulator -= 0.016;
- }
- if (Math.floor(currentTime / 1000) % 5 === 0) {
- checkVictory();
- updateUI();
- }
- }
- render();
- requestAnimationFrame(gameLoop);
- }
- // Initialize
- init();
- game.lastTime = performance.now();
- gameLoop(performance.now());
- // Initial particles
- setTimeout(function() {
- createParticle(window.innerWidth/2, 100, 'β');
- createParticle(window.innerWidth/2 - 50, 150, 'π°');
- createParticle(window.innerWidth/2 + 50, 150, 'π');
- }, 500);
- console.log('π° Fractal Village initialized! Build your recursive kingdom!');
- </script>
- </body>
- </html>
Add Comment
Please, Sign In to add comment