Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 119 additions & 40 deletions demo/civ-lite/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@
getUnitProfile,
} from './combat-model.js';
import { buildSpriteAtlas } from './sprites.js';
import {
createProductionQueue,
enqueueProduction,
evaluateVictory,

Check warning on line 44 in demo/civ-lite/game.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'evaluateVictory'.

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Human-vs-bots&issues=AZ8acQNcaauKZWBXwti2&open=AZ8acQNcaauKZWBXwti2&pullRequest=68
movementPlanForPath,

Check warning on line 45 in demo/civ-lite/game.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this unused import of 'movementPlanForPath'.

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Human-vs-bots&issues=AZ8acQNcaauKZWBXwti3&open=AZ8acQNcaauKZWBXwti3&pullRequest=68
progressProductionQueue,
} from './production-model.js';
import {
buildBuilding,
calculateCityEconomy,
Expand Down Expand Up @@ -90,11 +97,16 @@
const PAN_SPEED = 500; // WASD px/sec (world units)
const SIGHT = 2;
const UNIT_STATS = {
warrior: { atk: 30, def: 15, mov: 2 },
archer: { atk: 24, def: 12, mov: 2 },
swordsman: { atk: 38, def: 18, mov: 2 },
horseman: { atk: 34, def: 14, mov: 3 },
warrior: { hp: 100, atk: 30, def: 15, mov: 2 },
archer: { hp: 90, atk: 24, def: 12, mov: 2 },
swordsman: { hp: 110, atk: 38, def: 18, mov: 2 },
horseman: { hp: 95, atk: 34, def: 14, mov: 3 },
scout: { hp: 80, atk: 18, def: 10, mov: 3 },
};
const PRODUCTION_OPTIONS = [
{ id: 'warrior', label: 'Warrior', cost: 12, unitType: 'warrior' },
{ id: 'scout', label: 'Scout', cost: 8, unitType: 'scout' },
];
const GAME_SEED = 'civ-lite-barbarian-events-21';

// ─── DOM refs ───────────────────────────────────────
Expand Down Expand Up @@ -141,6 +153,7 @@
masterVolume: document.getElementById('masterVolume'),
musicVolume: document.getElementById('musicVolume'),
sfxVolume: document.getElementById('sfxVolume'),
prodPanel: document.getElementById('productionPanel'),
menu: document.getElementById('mainMenu'),
gameOver: document.getElementById('gameOver'),
tutorial: document.getElementById('tutorialPanel'),
Expand Down Expand Up @@ -295,7 +308,10 @@
diplomacy: buildDiplomacy(DEFAULT_CIVS),
map: initialCiv.map,
units: initialCiv.units,
cities: initialCiv.cities.map((city) => createCity(city)),
cities: initialCiv.cities.map((city) => ({
...createCity(city),
productionQueue: createProductionQueue(PRODUCTION_OPTIONS),
})),
resources: initialCiv.resources,
resourceTiles: [],
empireResources: { player: null, bot: null },
Expand Down Expand Up @@ -331,6 +347,10 @@
tileVariants: [],
};

for (const city of S.cities) {
if (city.owner === 'player') enqueueProduction(city.productionQueue, 'warrior');
}

function resetVisibilityState() {
S.fog = [];
S.tileVariants = [];
Expand Down Expand Up @@ -739,6 +759,37 @@
}

// ─── City production ────────────────────────────────
function spawnUnitFromProduction(owner, city, item) {
const stats = UNIT_STATS[item.unitType] || UNIT_STATS.warrior;
const id = S.nextId++;
let spawnX = city.x, spawnY = city.y;
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,-1]]) {
const nx = city.x + dx, ny = city.y + dy;
if (nx >= 0 && nx < MAP_W && ny >= 0 && ny < MAP_H &&
!isWaterTerrain(S.map[ny][nx].terrain) &&
!S.units.find(u => u.x === nx && u.y === ny)) {
spawnX = nx; spawnY = ny; break;
}
}
S.units.push({
id,
owner,
x: spawnX,
y: spawnY,
hp: stats.hp,
atk: stats.atk,
def: stats.def,
mov: stats.mov,
movLeft: stats.mov,
type: item.unitType,
xp: 0,
level: 1,
});
if (owner === 'player') playAudioEvent(AUDIO_EVENTS.build);
log(`${city.name} completed ${item.label}!`, 'build');
if (owner === 'player') revealAround(spawnX, spawnY);
}

function gatherResources(owner) {
S.empireResources[owner] = collectEmpireResources({
owner,
Expand Down Expand Up @@ -779,41 +830,14 @@
log(`${city.name} grows to pop ${city.pop}!`, 'build');
}

// Bot auto-recruitment keeps the prototype opponent active; player
// production is banked for city buildings until #16 adds a queue.
if (owner === 'bot' && city.prod >= 12) {
city.prod -= 12;
const id = S.nextId++;
let spawnX = city.x, spawnY = city.y;
// Find empty adjacent tile
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,-1]]) {
const nx = city.x + dx, ny = city.y + dy;
if (nx >= 0 && nx < MAP_W && ny >= 0 && ny < MAP_H &&
!isWaterTerrain(S.map[ny][nx].terrain) &&
!S.units.find(u => u.x === nx && u.y === ny)) {
spawnX = nx; spawnY = ny; break;
}
}
const type = chooseUnitToTrain(owner);
const profile = getUnitProfile({ type });
S.units.push({
id,
owner,
x: spawnX,
y: spawnY,
hp: 100,
atk: profile.strength,
def: profile.defense,
mov: 2,
movLeft: 2,
type,
xp: 0,
level: 1,
});
if (owner === 'player') playAudioEvent(AUDIO_EVENTS.build);
log(`${city.name} trained a ${type}!`, 'build');
if (owner === 'player') revealAround(spawnX, spawnY);
if (owner === 'bot' && city.productionQueue.items.length === 0) {
enqueueProduction(city.productionQueue, 'warrior');
}

const result = progressProductionQueue(city.productionQueue, city.prod);
city.productionQueue = result.queue;
city.prod = city.productionQueue.progress;
result.completed.forEach(item => spawnUnitFromProduction(owner, city, item));
}
S.resources[owner].food += totalFood;
S.resources[owner].prod += totalProd;
Expand Down Expand Up @@ -1119,6 +1143,8 @@
updateEmpireHud();
completeResearch('player', res.science);
updateHud();
updateCityPanel();
updateProductionPanel();
log(`Income: +${res.food} food, +${res.prod} prod, +${res.science} science, +${res.gold} gold`, 'build');

setTimeout(botTurn, 400);
Expand All @@ -1135,6 +1161,8 @@
claimTerritory(S.map, S.cities);
updateHud();
updateTechPanel();
updateCityPanel();
updateProductionPanel();
log(`─── Turn ${S.turn} ───`);
maybeQueueRandomEvent();
renderEventPanel();
Expand Down Expand Up @@ -1193,7 +1221,10 @@
addCaptureFeedback(cap);
claimTerritory(S.map, S.cities);
updateHud();
updateCityPanel();
updateProductionPanel();
log(`Bot captured ${cap.name}!`, 'combat');
checkWin();
}
continue;
}
Expand All @@ -1210,7 +1241,7 @@
const blocker = S.units.find(u => u.x === path[i].x && u.y === path[i].y && u !== bot);
if (blocker && isHostileTo(bot, blocker)) {
combat(bot, blocker);
movLeft = 0;

Check warning on line 1244 in demo/civ-lite/game.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "movLeft".

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Human-vs-bots&issues=AZ8acQNcaauKZWBXwti4&open=AZ8acQNcaauKZWBXwti4&pullRequest=68
}
break;
}
Expand All @@ -1219,15 +1250,17 @@
movLeft -= cost;
steps++;
}
bot.movLeft = movLeft;
// Capture city
const cap = S.cities.find(c => c.owner === 'player' && c.x === bot.x && c.y === bot.y);
if (cap) {
cap.owner = 'bot';
addCaptureFeedback(cap);
claimTerritory(S.map, S.cities);
updateHud();
updateCityPanel();
updateProductionPanel();
log(`Bot captured ${cap.name}!`, 'combat');
checkWin();
}
}
}
Expand Down Expand Up @@ -1927,6 +1960,46 @@
}
}

// ─── Production queue panel ─────────────────────────
function updateProductionPanel() {
if (!dom.prodPanel) return;
const city = S.cities.find(c => c.owner === 'player');
if (!city) {
dom.prodPanel.innerHTML = '<p class="placeholder">No city remains</p>';
return;
}

const current = city.productionQueue.items[0];
const pct = current ? Math.min(100, Math.round((city.productionQueue.progress / current.cost) * 100)) : 0;
const queueText = city.productionQueue.items.length > 1
? city.productionQueue.items.slice(1).map(item => item.label).join(' → ')
: 'No queued follow-up';

dom.prodPanel.innerHTML = `
<div class="city-row">
<span class="city-name">${city.name}</span>
<span class="queue-meta">Pop ${city.pop}</span>
</div>
<div class="queue-row">
<span class="queue-name">${current ? current.label : 'Idle'}</span>
<span class="queue-meta">${current ? `${city.productionQueue.progress}/${current.cost}` : '0/0'}</span>
</div>
<div class="queue-track" aria-label="Production progress">
<div class="queue-fill" style="--pct: ${pct}%"></div>
</div>
<div class="queue-list">Next: ${queueText}</div>
<div class="queue-list">Win: capture Babylon or remove all bot pieces.</div>`;
}

function queueCityProduction(itemId) {
if (S.phase === 'gameover') return;
const city = S.cities.find(c => c.owner === 'player');
if (!city) return;
const item = enqueueProduction(city.productionQueue, itemId).items.at(-1);
log(`${city.name} queued ${item.label}`, 'build');
updateProductionPanel();
}

// ─── City management panel ──────────────────────────
function updateCityPanel() {
const city = S.cities.find(c => c.owner === 'player');
Expand Down Expand Up @@ -2208,6 +2281,7 @@
claimTerritory(S.map, S.cities);
updateHud();
updateCityPanel();
updateProductionPanel();
playAudioEvent(AUDIO_EVENTS.build);
log(`Captured ${cap.name}!`, 'build');
checkWin();
Expand Down Expand Up @@ -2280,6 +2354,8 @@
centerOn(u.x, u.y);
}
});
document.getElementById('btnQueueWarrior')?.addEventListener('click', () => queueCityProduction('warrior'));
document.getElementById('btnQueueScout')?.addEventListener('click', () => queueCityProduction('scout'));
dom.btnStartSetup?.addEventListener('click', () => applyGameSetup());
renderSetupRoster();
dom.cityDet.addEventListener('click', e => {
Expand All @@ -2295,6 +2371,7 @@
log(`Cannot build ${CITY_BUILDINGS[btn.dataset.building]?.name || 'building'}: ${result.reason}.`, 'combat');
}
updateCityPanel();
updateProductionPanel();
});
dom.tradeBtn.addEventListener('click', openTradeRoute);
document.getElementById('btnHelp').addEventListener('click', () => setTutorial());
Expand Down Expand Up @@ -2392,5 +2469,7 @@
updateEmpireHud();
renderEventPanel();
updateTechPanel();
updateCityPanel();
updateProductionPanel();
requestAnimationFrame(gameLoop);
})();
9 changes: 9 additions & 0 deletions demo/civ-lite/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ <h3>City Management</h3>
</div>
</section>

<section id="cityProduction">
<h3>City Production</h3>
<div id="productionPanel"></div>
<div class="production-actions">
<button id="btnQueueWarrior">Warrior</button>
<button id="btnQueueScout">Scout</button>
</div>
</section>

<section id="unitInfo">
<h3>Context</h3>
<div id="contextPanel">
Expand Down
Loading
Loading