diff --git a/demo/civ-lite/game.js b/demo/civ-lite/game.js index 725f3a8..132b191 100644 --- a/demo/civ-lite/game.js +++ b/demo/civ-lite/game.js @@ -9,6 +9,7 @@ import { CIV_COLORS, RESOURCE_TYPES, TERRAIN_DEFENSE, + TERRAIN_YIELDS as TERRAIN_YIELD, claimTerritory, computeVisibility, createCivState, @@ -25,6 +26,16 @@ import { getUnitProfile, } from './combat-model.js'; import { buildSpriteAtlas } from './sprites.js'; +import { + activeTradeYield, + advanceTradeRoutes, + collectEmpireResources, + createTradeRoute, + formatYield, + generateResourceMap, + resourceAt, + summarizeResourceList, +} from './resource-model.js'; import { applyEventChoice, buildRandomEvent, @@ -87,6 +98,10 @@ const dom = { goldRate: document.getElementById('goldRate'), currentTech: document.getElementById('currentTech'), techList: document.getElementById('techList'), + strategic: document.getElementById('strategicList'), + luxury: document.getElementById('luxuryList'), + tradeStatus: document.getElementById('tradeStatus'), + tradeBtn: document.getElementById('btnTrade'), unitDet: document.getElementById('contextPanel') || document.getElementById('unitDetails'), eventPanel: document.getElementById('eventPanel'), logBox: document.getElementById('eventLog') || document.getElementById('logContent'), @@ -230,6 +245,9 @@ const S = { units: initialCiv.units, cities: initialCiv.cities, resources: initialCiv.resources, + resourceTiles: [], + empireResources: { player: null, bot: null }, + tradeRoutes: [], camps: [], pendingEvent: null, eventHistory: new Set(), @@ -259,6 +277,13 @@ const S = { tileVariants: [], }; +S.resourceTiles = generateResourceMap({ + map: S.map, + cities: S.cities, + width: MAP_W, + height: MAP_H, +}); + function createUnit(id, owner, x, y, type = 'warrior') { const stats = UNIT_STATS[type] || UNIT_STATS.warrior; return { id, owner, x, y, hp: 100, atk: stats.atk, def: stats.def, mov: stats.mov, movLeft: stats.mov, type }; @@ -351,9 +376,78 @@ function refreshVision() { initializeNeutralCamps(); refreshVision(); +refreshEmpireResourceState(); +updateResourcePanel(); dom.status.textContent = 'Ready'; renderUx(); +// ─── Resource & trade helpers ────────────────────── +function refreshEmpireResourceState() { + S.empireResources.player = collectEmpireResources({ + owner: 'player', + cities: S.cities, + map: S.map, + resources: S.resourceTiles, + terrainYield: TERRAIN_YIELD, + }); + S.empireResources.bot = collectEmpireResources({ + owner: 'bot', + cities: S.cities, + map: S.map, + resources: S.resourceTiles, + terrainYield: TERRAIN_YIELD, + }); +} + +function updateResourcePanel() { + const playerState = S.empireResources.player; + if (!playerState) return; + + dom.strategic.textContent = summarizeResourceList(playerState.categories.strategic); + dom.luxury.textContent = summarizeResourceList(playerState.categories.luxury); + + const activeRoutes = S.tradeRoutes.filter(route => route.turnsLeft > 0); + if (activeRoutes.length) { + const route = activeRoutes[0]; + dom.tradeStatus.textContent = `${route.exportResource} for ${route.importResource}: +${formatYield(route.playerYield)} for ${route.turnsLeft} turns.`; + dom.tradeBtn.disabled = true; + return; + } + + const route = createTradeRoute({ + playerState: S.empireResources.player, + botState: S.empireResources.bot, + turn: S.turn, + }); + dom.tradeBtn.disabled = route === null; + dom.tradeStatus.textContent = route + ? `Available: export ${route.exportResource} for ${route.importResource}.` + : 'Need distinct controlled resources for trade.'; +} + +function openTradeRoute() { + refreshEmpireResourceState(); + if (S.tradeRoutes.some(route => route.turnsLeft > 0)) { + updateResourcePanel(); + return; + } + + const route = createTradeRoute({ + playerState: S.empireResources.player, + botState: S.empireResources.bot, + turn: S.turn, + }); + if (!route) { + log('No resource trade is available yet.', 'trade'); + updateResourcePanel(); + return; + } + + S.tradeRoutes.push(route); + log(`Trade route opened: ${route.exportResource} for ${route.importResource}.`, 'trade'); + updateResourcePanel(); +} + // ─── Pathfinding (A*) ─────────────────────────────── function heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); } @@ -520,8 +614,21 @@ function updateParticles() { // ─── City production ──────────────────────────────── function gatherResources(owner) { + S.empireResources[owner] = collectEmpireResources({ + owner, + cities: S.cities, + map: S.map, + resources: S.resourceTiles, + terrainYield: TERRAIN_YIELD, + }); + const cities = S.cities.filter(c => c.owner === owner); let totalFood = 0, totalProd = 0, totalSci = 0, totalGold = 0; + const tradeYield = activeTradeYield(S.tradeRoutes, owner); + totalFood += tradeYield.food; + totalProd += tradeYield.prod; + totalSci += tradeYield.sci; + let firstCity = true; for (const city of cities) { const cityYield = summarizeCityYield(S, city); totalFood += cityYield.food; @@ -532,6 +639,12 @@ function gatherResources(owner) { city.food += cityYield.food; city.prod += cityYield.prod; + if (firstCity) { + city.food += tradeYield.food; + city.prod += tradeYield.prod; + firstCity = false; + } + // City growth if (city.food >= 8 * city.pop) { city.pop++; @@ -578,6 +691,7 @@ function gatherResources(owner) { S.resources[owner].prod += totalProd; S.resources[owner].science += totalSci; S.resources[owner].gold += totalGold; + updateResourcePanel(); return { food: totalFood, prod: totalProd, science: totalSci, gold: totalGold }; } @@ -791,6 +905,12 @@ function startPlayerTurn() { function botTurn() { const botRes = gatherResources('bot'); completeResearch('bot', botRes.science); + const routesBefore = S.tradeRoutes.length; + S.tradeRoutes = advanceTradeRoutes(S.tradeRoutes); + if (routesBefore > 0 && S.tradeRoutes.length === 0) { + log('Trade route completed.', 'trade'); + } + updateResourcePanel(); const bots = S.units.filter(u => u.owner === 'bot'); for (const bot of bots) { @@ -1109,6 +1229,39 @@ function drawLayerTerrain() { } } +// Layer 0.5: Strategic and luxury resources +function drawLayerResources() { + for (const resource of S.resourceTiles) { + if (S.fog[resource.y][resource.x] === 0) continue; + const px = resource.x * TILE + TILE / 2; + const py = resource.y * TILE + TILE / 2; + const radius = resource.category === 'luxury' ? 7 : 6; + + ctx.fillStyle = 'rgba(0,0,0,0.45)'; + ctx.beginPath(); + ctx.arc(px + 1, py + 1, radius + 2, 0, Math.PI * 2); + ctx.fill(); + + ctx.fillStyle = resource.marker; + ctx.beginPath(); + if (resource.category === 'luxury') { + ctx.moveTo(px, py - radius); + ctx.lineTo(px + radius, py); + ctx.lineTo(px, py + radius); + ctx.lineTo(px - radius, py); + ctx.closePath(); + } else { + ctx.arc(px, py, radius, 0, Math.PI * 2); + } + ctx.fill(); + + ctx.fillStyle = '#07111f'; + ctx.font = `bold ${9 / S.zoom}px system-ui`; + ctx.textAlign = 'center'; + ctx.fillText(resource.name.charAt(0), px, py + 3 / S.zoom); + } +} + // Layer 1: Grid overlay function drawLayerGrid() { const vw = viewW(), vh = viewH(); @@ -1396,6 +1549,14 @@ function drawMinimap() { } } + for (const resource of S.resourceTiles) { + if (S.fog[resource.y][resource.x] === 0) continue; + miniCtx.fillStyle = resource.marker; + miniCtx.beginPath(); + miniCtx.arc(resource.x * tw + tw / 2, resource.y * th + th / 2, Math.max(tw, th) * 0.35, 0, Math.PI * 2); + miniCtx.fill(); + } + // Units & cities on minimap for (const camp of S.camps) { if (camp.cleared || S.fog[camp.y][camp.x] === 0) continue; @@ -1490,6 +1651,10 @@ function showTooltip(x, y, tileX, tileY) { const terrain = tile.terrain; const yields = getTileYield(tile); const def = Math.round(TERRAIN_DEFENSE[terrain] * 100); + const resource = resourceAt(S.resourceTiles, tileX, tileY); + const resourceLine = resource + ? `
${resource.name} (${resource.category}) +${formatYield(resource.yield)}
` + : ''; const camp = findActiveCampAt(tileX, tileY); const campText = camp ? `
${camp.name}: clear for +4 prod, +2 science
` @@ -1513,6 +1678,7 @@ function showTooltip(x, y, tileX, tileY) { 🔬${yields.science} 💰${yields.gold} + ${resourceLine} ${campText} ${combatHtml}`; tooltip.style.display = 'block'; @@ -1767,6 +1933,7 @@ document.getElementById('btnCenter').addEventListener('click', () => { centerOn(u.x, u.y); } }); +dom.tradeBtn.addEventListener('click', openTradeRoute); document.getElementById('btnHelp').addEventListener('click', () => setTutorial()); document.getElementById('btnStartGame').addEventListener('click', beginGame); document.getElementById('btnMenuTutorial').addEventListener('click', () => setTutorial()); @@ -1799,6 +1966,7 @@ function gameLoop(now) { ctx.scale(S.zoom, S.zoom); ctx.translate(-S.camX, -S.camY); drawLayerTerrain(); + drawLayerResources(); drawLayerGrid(); drawLayerReachable(); drawLayerPath(); diff --git a/demo/civ-lite/index.html b/demo/civ-lite/index.html index 6e1f7d1..f0b5c7f 100644 --- a/demo/civ-lite/index.html +++ b/demo/civ-lite/index.html @@ -33,6 +33,20 @@

Empire

💰 Gold 0+0
+
+

Resources & Trade

+
+ Strategic + None controlled +
+
+ Luxury + None controlled +
+ +

Find resources near cities to trade.

+
+

Context

diff --git a/demo/civ-lite/resource-model.js b/demo/civ-lite/resource-model.js new file mode 100644 index 0000000..a57e01c --- /dev/null +++ b/demo/civ-lite/resource-model.js @@ -0,0 +1,283 @@ +export const RESOURCE_DEFS = { + iron: { + key: 'iron', + name: 'Iron', + category: 'strategic', + terrains: ['hill', 'desert'], + yield: { food: 0, prod: 2, sci: 0 }, + marker: '#b7c0c7', + }, + horses: { + key: 'horses', + name: 'Horses', + category: 'strategic', + terrains: ['plains', 'forest'], + yield: { food: 1, prod: 1, sci: 0 }, + marker: '#c58b4a', + }, + gems: { + key: 'gems', + name: 'Gems', + category: 'luxury', + terrains: ['hill', 'desert'], + yield: { food: 0, prod: 0, sci: 2 }, + marker: '#c084fc', + }, + spices: { + key: 'spices', + name: 'Spices', + category: 'luxury', + terrains: ['plains', 'forest', 'desert'], + yield: { food: 1, prod: 0, sci: 1 }, + marker: '#f59e0b', + }, +}; + +const DEFAULT_TARGET_COUNT = 16; + +export function generateResourceMap({ map, cities, width, height, targetCount = DEFAULT_TARGET_COUNT }) { + const resources = []; + const occupied = new Set(cities.map(city => keyFor(city.x, city.y))); + const addResource = createResourceAdder({ map, width, height, occupied, resources }); + placeCapitalRingResources({ map, cities, width, height, occupied, addResource }); + scatterResources({ map, width, height, occupied, resources, addResource, targetCount }); + return resources; +} + +export function resourceAt(resources, x, y) { + return resources.find(resource => resource.x === x && resource.y === y) ?? null; +} + +export function collectEmpireResources({ owner, cities, map, resources, terrainYield, radius = 1 }) { + const controlled = []; + const seen = new Set(); + const cityYields = cities + .filter(city => city.owner === owner) + .map(city => collectCityResources({ city, map, resources, terrainYield, radius, controlled, seen })); + const totals = cityYields.reduce((acc, entry) => addYield(acc, entry.yields), emptyYield()); + + return { + owner, + yields: totals, + cityYields, + controlled, + stock: stockByResource(controlled), + categories: { + strategic: controlled.filter(resource => resource.category === 'strategic'), + luxury: controlled.filter(resource => resource.category === 'luxury'), + }, + }; +} + +function createResourceAdder({ map, width, height, occupied, resources }) { + return (defKey, x, y, source = 'scatter') => { + const def = RESOURCE_DEFS[defKey]; + if (!def || !canPlaceResource({ map, width, height, occupied, def, x, y })) return false; + resources.push(makeResource(def, x, y, resources.length + 1, source)); + occupied.add(keyFor(x, y)); + return true; + }; +} + +function makeResource(def, x, y, index, source) { + return { + id: `${def.key}-${index}`, + key: def.key, + name: def.name, + category: def.category, + x, + y, + yield: { ...def.yield }, + marker: def.marker, + source, + }; +} + +function placeCapitalRingResources({ map, cities, width, height, occupied, addResource }) { + for (const city of cities) { + placeNearCity({ + map, + width, + height, + city, + defKey: city.owner === 'player' ? 'iron' : 'horses', + occupied, + addResource, + }); + placeNearCity({ + map, + width, + height, + city, + defKey: city.owner === 'player' ? 'gems' : 'spices', + occupied, + addResource, + }); + } +} + +function scatterResources({ map, width, height, occupied, resources, addResource, targetCount }) { + const resourceKeys = Object.keys(RESOURCE_DEFS); + for (const candidate of resourceCandidates({ map, width, height, occupied })) { + if (resources.length >= targetCount) break; + addFirstCompatibleResource({ candidate, resourceKeys, addResource }); + } +} + +function resourceCandidates({ map, width, height, occupied }) { + const candidates = []; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + if (!isResourceCandidate(map, occupied, x, y)) continue; + candidates.push({ x, y, score: hashResource(x, y, width, height) }); + } + } + return candidates.sort((a, b) => a.score - b.score); +} + +function addFirstCompatibleResource({ candidate, resourceKeys, addResource }) { + const start = candidate.score % resourceKeys.length; + for (let i = 0; i < resourceKeys.length; i++) { + if (addResource(resourceKeys[(start + i) % resourceKeys.length], candidate.x, candidate.y)) return; + } +} + +function isResourceCandidate(map, occupied, x, y) { + return !occupied.has(keyFor(x, y)) && map[y][x] !== 'water'; +} + +function collectCityResources({ city, map, resources, terrainYield, radius, controlled, seen }) { + const yields = emptyYield(); + forEachCityTile(city, radius, ({ x, y }) => { + if (!map[y]?.[x]) return; + addYield(yields, terrainYield[map[y][x]] ?? emptyYield()); + const resource = resourceAt(resources, x, y); + if (resource) trackControlledResource({ resource, yields, controlled, seen }); + }); + return { city, yields }; +} + +function forEachCityTile(city, radius, visit) { + for (let dy = -radius; dy <= radius; dy++) { + for (let dx = -radius; dx <= radius; dx++) { + visit({ x: city.x + dx, y: city.y + dy }); + } + } +} + +function trackControlledResource({ resource, yields, controlled, seen }) { + addYield(yields, resource.yield); + if (seen.has(resource.id)) return; + controlled.push(resource); + seen.add(resource.id); +} + +function stockByResource(resources) { + const stock = {}; + for (const resource of resources) { + stock[resource.key] = (stock[resource.key] ?? 0) + 1; + } + return stock; +} + +export function createTradeRoute({ playerState, botState, turn }) { + const exportResource = firstTradable(playerState, botState); + const importResource = firstTradable(botState, playerState); + if (!exportResource || !importResource) return null; + + const exportBonus = exportResource.category === 'luxury' ? { food: 1, prod: 0, sci: 2 } : { food: 0, prod: 2, sci: 0 }; + const importBonus = importResource.category === 'luxury' ? { food: 1, prod: 0, sci: 2 } : { food: 0, prod: 2, sci: 0 }; + const playerYield = addYield(addYield(emptyYield(), exportBonus), importBonus); + + return { + id: `trade-${turn}-${exportResource.key}-${importResource.key}`, + from: 'player', + to: 'bot', + exportKey: exportResource.key, + importKey: importResource.key, + exportResource: exportResource.name, + importResource: importResource.name, + turnsLeft: 3, + playerYield, + botYield: { food: 1, prod: 1, sci: 1 }, + }; +} + +export function activeTradeYield(routes, owner) { + const totals = emptyYield(); + const field = owner === 'player' ? 'playerYield' : 'botYield'; + for (const route of routes) { + if (route.turnsLeft > 0) addYield(totals, route[field] ?? emptyYield()); + } + return totals; +} + +export function advanceTradeRoutes(routes) { + return routes + .map(route => ({ ...route, turnsLeft: route.turnsLeft - 1 })) + .filter(route => route.turnsLeft > 0); +} + +export function summarizeResourceList(resources) { + if (resources.length === 0) return 'None controlled'; + return resources.map(resource => `${resource.name} +${formatYield(resource.yield)}`).join(', '); +} + +export function formatYield(yieldValue) { + const parts = []; + if (yieldValue.food) parts.push(`${yieldValue.food}F`); + if (yieldValue.prod) parts.push(`${yieldValue.prod}P`); + if (yieldValue.sci) parts.push(`${yieldValue.sci}S`); + return parts.length ? parts.join(' ') : '0'; +} + +function placeNearCity({ map, width, height, city, defKey, occupied, addResource }) { + const offsets = [ + [1, 0], [-1, 0], [0, 1], [0, -1], + [1, 1], [-1, -1], [2, 0], [0, 2], + [-2, 0], [0, -2], [2, 1], [-1, 2], + ]; + const ranked = offsets + .map(([dx, dy], idx) => ({ x: city.x + dx, y: city.y + dy, idx })) + .filter(({ x, y }) => x >= 0 && x < width && y >= 0 && y < height && !occupied.has(keyFor(x, y))) + .sort((a, b) => { + const terrainA = map[a.y][a.x]; + const terrainB = map[b.y][b.x]; + const def = RESOURCE_DEFS[defKey]; + const scoreA = def.terrains.includes(terrainA) ? a.idx : a.idx + 100; + const scoreB = def.terrains.includes(terrainB) ? b.idx : b.idx + 100; + return scoreA - scoreB; + }); + for (const tile of ranked) { + if (addResource(defKey, tile.x, tile.y, 'capital-ring')) return; + } +} + +function canPlaceResource({ map, width, height, occupied, def, x, y }) { + if (x < 0 || x >= width || y < 0 || y >= height) return false; + if (occupied.has(keyFor(x, y))) return false; + return def.terrains.includes(map[y][x]); +} + +function firstTradable(primaryState, otherState) { + return primaryState.controlled.find(resource => (otherState.stock?.[resource.key] ?? 0) === 0) ?? null; +} + +function addYield(target, source) { + target.food += source.food ?? 0; + target.prod += source.prod ?? 0; + target.sci += source.sci ?? 0; + return target; +} + +function emptyYield() { + return { food: 0, prod: 0, sci: 0 }; +} + +function keyFor(x, y) { + return `${x},${y}`; +} + +function hashResource(x, y, width, height) { + return ((x + 11) * 73856093 ^ (y + 17) * 19349663 ^ width * 83492791 ^ height * 2654435761) >>> 0; +} diff --git a/demo/civ-lite/resource-model.test.mjs b/demo/civ-lite/resource-model.test.mjs new file mode 100644 index 0000000..b809128 --- /dev/null +++ b/demo/civ-lite/resource-model.test.mjs @@ -0,0 +1,110 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + activeTradeYield, + advanceTradeRoutes, + collectEmpireResources, + createTradeRoute, + generateResourceMap, + resourceAt, +} from './resource-model.js'; + +const terrainYield = { + plains: { food: 2, prod: 1, sci: 0 }, + forest: { food: 1, prod: 2, sci: 0 }, + hill: { food: 0, prod: 2, sci: 1 }, + water: { food: 3, prod: 0, sci: 0 }, + desert: { food: 0, prod: 1, sci: 1 }, +}; + +function makeMap(width = 8, height = 6) { + return Array.from({ length: height }, (_, y) => + Array.from({ length: width }, (_, x) => { + if (x === 0 || y === 0) return 'water'; + if ((x + y) % 5 === 0) return 'hill'; + if ((x + y) % 4 === 0) return 'forest'; + if ((x + y) % 3 === 0) return 'desert'; + return 'plains'; + }), + ); +} + +test('generateResourceMap places deterministic strategic and luxury resources on valid non-city tiles', () => { + const map = makeMap(); + const cities = [ + { owner: 'player', x: 2, y: 2, name: 'Athens' }, + { owner: 'bot', x: 6, y: 4, name: 'Babylon' }, + ]; + + const resourcesA = generateResourceMap({ map, cities, width: 8, height: 6 }); + const resourcesB = generateResourceMap({ map, cities, width: 8, height: 6 }); + + assert.deepEqual(resourcesA, resourcesB); + assert.ok(resourcesA.some(resource => resource.category === 'strategic')); + assert.ok(resourcesA.some(resource => resource.category === 'luxury')); + + const cityKeys = new Set(cities.map(city => `${city.x},${city.y}`)); + for (const resource of resourcesA) { + assert.notEqual(map[resource.y][resource.x], 'water'); + assert.equal(cityKeys.has(`${resource.x},${resource.y}`), false); + assert.ok(resource.yield.food >= 0); + assert.ok(resource.yield.prod >= 0); + assert.ok(resource.yield.sci >= 0); + } +}); + +test('collectEmpireResources adds controlled resource yields and stock counts', () => { + const map = makeMap(); + const cities = [{ owner: 'player', x: 2, y: 2, name: 'Athens' }]; + const resources = [ + { id: 'iron-1', key: 'iron', name: 'Iron', category: 'strategic', x: 3, y: 2, yield: { food: 0, prod: 2, sci: 0 } }, + { id: 'gems-1', key: 'gems', name: 'Gems', category: 'luxury', x: 1, y: 2, yield: { food: 0, prod: 0, sci: 2 } }, + { id: 'spices-1', key: 'spices', name: 'Spices', category: 'luxury', x: 6, y: 5, yield: { food: 1, prod: 0, sci: 1 } }, + ]; + + const collected = collectEmpireResources({ owner: 'player', cities, map, resources, terrainYield }); + + assert.equal(resourceAt(resources, 3, 2).name, 'Iron'); + assert.equal(collected.controlled.length, 2); + assert.equal(collected.stock.iron, 1); + assert.equal(collected.stock.gems, 1); + assert.equal(collected.stock.spices ?? 0, 0); + assert.ok(collected.yields.prod >= 2); + assert.ok(collected.yields.sci >= 2); + assert.equal(collected.categories.strategic.length, 1); + assert.equal(collected.categories.luxury.length, 1); +}); + +test('createTradeRoute exchanges distinct controlled resources for recurring yield', () => { + const playerState = { + controlled: [{ key: 'gems', name: 'Gems', category: 'luxury' }], + stock: { gems: 1 }, + }; + const botState = { + controlled: [{ key: 'iron', name: 'Iron', category: 'strategic' }], + stock: { iron: 1 }, + }; + + const route = createTradeRoute({ playerState, botState, turn: 4 }); + + assert.equal(route.from, 'player'); + assert.equal(route.to, 'bot'); + assert.equal(route.exportResource, 'Gems'); + assert.equal(route.importResource, 'Iron'); + assert.equal(route.turnsLeft, 3); + assert.deepEqual(activeTradeYield([route], 'player'), { food: 1, prod: 2, sci: 2 }); +}); + +test('advanceTradeRoutes expires completed trade routes', () => { + const routes = [ + { id: 'a', turnsLeft: 2, playerYield: { food: 1, prod: 1, sci: 0 }, botYield: { food: 0, prod: 1, sci: 1 } }, + { id: 'b', turnsLeft: 1, playerYield: { food: 0, prod: 0, sci: 2 }, botYield: { food: 1, prod: 0, sci: 0 } }, + ]; + + const next = advanceTradeRoutes(routes); + + assert.deepEqual(next, [ + { id: 'a', turnsLeft: 1, playerYield: { food: 1, prod: 1, sci: 0 }, botYield: { food: 0, prod: 1, sci: 1 } }, + ]); +}); diff --git a/demo/civ-lite/styles.css b/demo/civ-lite/styles.css index f20b6f8..a64ec13 100644 --- a/demo/civ-lite/styles.css +++ b/demo/civ-lite/styles.css @@ -501,6 +501,46 @@ button:active { #eventLog .log-good { color: var(--accent2); } #logContent .log-event, #eventLog .log-event { color: var(--gold); } +#logContent .log-trade, +#eventLog .log-trade { color: var(--gold); } + +/* ───── Resources & Trade ────────────────────────── */ +.trade-group { + display: grid; + gap: 3px; + padding: 3px 0 6px; +} + +.trade-label { + color: var(--dim); + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.8px; +} + +.trade-list { + color: var(--text); + font-size: 0.74rem; + line-height: 1.35; +} + +#btnTrade { + margin-top: 4px; + width: 100%; +} + +#btnTrade:disabled { + cursor: default; + opacity: 0.55; + box-shadow: none; +} + +.trade-status { + margin-top: 6px; + color: var(--dim); + font-size: 0.72rem; + line-height: 1.35; +} /* ───── UX overlays ──────────────────────────────── */ .overlay { @@ -635,6 +675,12 @@ button:active { gap: 2px; } +#tooltip .tt-resource { + margin-top: 4px; + color: var(--gold); + font-size: 0.7rem; +} + #tooltip .tt-camp { color: var(--gold); font-size: 0.7rem;