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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ src/
AudioManager.js / sounds.js
public/ # browser runtime assets only
tests/
game.spec.js / site.spec.js # 261 Playwright browser tests
game.spec.js / site.spec.js # 260+ Playwright browser tests
```

See [docs/reference-data.md](docs/reference-data.md) for sprite coordinates, wonder IDs, city screen layout, and map file format.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ownership attestation locally in your browser.
- Original city, research, Civilopedia, throne-room, advisor, score, and map UI
- Save/load, scenarios, diplomacy, research, combat, AI, Wonders, spaceship,
government, trade, and late-game mechanics
- 261 Playwright browser regression tests
- 260+ Playwright browser regression tests
- Production build deployable beneath the GitHub Pages `/civ2/` project path

This is a playable public beta, not a claim that every undocumented MGE edge
Expand Down Expand Up @@ -56,7 +56,11 @@ npm run preview:pages

Choose **Game → Report Bug...** inside a running game. The app creates one ZIP
containing a screenshot, a restorable game-state snapshot, the current camera
and open-screen state, and browser/viewport diagnostics.
and open-screen state, browser/viewport diagnostics, and a local traceback of
the last ten canvas clicks. Each traceback entry records the screen before and
after the click, pointer and map-tile coordinates where applicable, the active
unit/city context, and resolved menu action where available. It does not record
typed city names or transmit anything automatically.

- **Download ZIP** saves the report locally.
- **Share / Email** uses the browser or operating system share sheet when file
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ <h1>Build a civilization<br>to stand the test of time.</h1>
</section>

<section class="stats" aria-label="Project status">
<div><strong>261</strong><span>browser tests</span></div>
<div><strong>260+</strong><span>browser tests</span></div>
<div><strong>59</strong><span>original movies supported</span></div>
<div><strong>7</strong><span>world maps</span></div>
<div><strong>BSD-3</strong><span>project code</span></div>
Expand Down
29 changes: 23 additions & 6 deletions src/audio/AudioManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export class AudioManager {
this._musicTrackIdx = 0;
/** @type {boolean} */
this._musicReady = false;
this._musicRetryTimer = null;
this._musicFailureCount = 0;
}

_loadSettings() {
Expand Down Expand Up @@ -267,7 +269,11 @@ export class AudioManager {
* @param {number} [fadeOut=1000] fade-out duration in ms
*/
stopMusic(fadeOut = 1000) {
if (!this._musicSource || !this._musicGain || !this._ctx) return;
if (this._musicAudio) this._stopMusic();
if (!this._musicSource || !this._musicGain || !this._ctx) {
this._currentMusic = null;
return;
}

const gain = this._musicGain;
const source = this._musicSource;
Expand All @@ -287,7 +293,7 @@ export class AudioManager {
}

/** @returns {boolean} */
get musicPlaying() { return this._currentMusic !== null; }
get musicPlaying() { return this._currentMusic !== null || this._musicAudio !== null; }
/** @returns {string|null} */
get currentTrackName() { return this._currentMusic; }

Expand Down Expand Up @@ -336,6 +342,7 @@ export class AudioManager {

audio.addEventListener('canplaythrough', () => {
if (this._musicAudio !== audio) return;
this._musicFailureCount = 0;
audio.play().catch(err => {
console.warn('AudioManager: failed to play track:', err.message);
});
Expand All @@ -351,11 +358,17 @@ export class AudioManager {
});

audio.addEventListener('error', (e) => {
if (this._musicAudio !== audio) return;
console.warn(`AudioManager: error playing ${trackName}.mp3`, e);
this._musicTrackIdx++;
const tracks = MUSIC_TRACKS[this._currentEra];
if (tracks) {
setTimeout(() => this._playMusicTrack(tracks[this._musicTrackIdx % tracks.length]), 1000);
this._musicFailureCount++;
if (tracks && this._musicFailureCount < tracks.length) {
clearTimeout(this._musicRetryTimer);
this._musicRetryTimer = setTimeout(() => {
this._musicRetryTimer = null;
if (this._musicAudio === audio) this._playMusicTrack(tracks[this._musicTrackIdx % tracks.length]);
}, 1000);
}
});

Expand All @@ -364,10 +377,14 @@ export class AudioManager {
}

_stopMusic() {
clearTimeout(this._musicRetryTimer);
this._musicRetryTimer = null;
if (this._musicAudio) {
this._musicAudio.pause();
this._musicAudio.src = '';
const audio = this._musicAudio;
this._musicAudio = null;
audio.pause();
audio.removeAttribute('src');
audio.load();
}
}

Expand Down
25 changes: 24 additions & 1 deletion src/engine/Civ2SaveLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,35 @@ export class Civ2SaveLoader {

// Diplomatic relations (7 bytes × 8 civs, packed in 4-byte groups)
const relations = new Map();
const contacts = new Set();
for (let other = 0; other < 8; other++) {
const t0 = b[base + 32 + 4 * other];
const t1 = b[base + 32 + 4 * other + 1];
if (bit(t0, 0)) contacts.add(other);
const isWar = bit(t1, 5);
relations.set(other, isWar ? 'war' : 'peace');
}

const savedUnitTypeCount = isMGE ? 62 : 54;
const late = 216 + 3 * savedUnitTypeCount + 592;
const hasSpaceship = bit(b[base + late + 30], 0);
const spaceship = {
structural: u16(b, base + late + 38),
propulsion: u16(b, base + late + 40),
fuel: u16(b, base + late + 42),
habitation: u16(b, base + late + 44),
lifeSupport: u16(b, base + late + 46),
solar: u16(b, base + late + 48),
unassignedComponents: 0,
unassignedModules: 0,
launched: hasSpaceship,
launchYear: hasSpaceship ? u16(b, base + late + 34) : null,
arrivalYear: hasSpaceship ? u16(b, base + late + 32) : null,
};

savCivs.push({
savId, tribeId, gold, beakers, currentResearch,
sciRate, taxRate, govtId, advances, relations,
sciRate, taxRate, govtId, advances, relations, contacts, spaceship,
alive: civsInPlay[savId],
});
}
Expand Down Expand Up @@ -402,6 +421,10 @@ export class Civ2SaveLoader {
civ.relations.set(civMapping.get(otherSavId), rel);
}
}
civ.contacts = new Set(
[...sd.contacts].filter(otherSavId => civMapping.has(otherSavId)).map(otherSavId => civMapping.get(otherSavId))
);
civ.spaceship = { ...civ.spaceship, ...sd.spaceship };
civArray[ourId] = civ;
}
gs.civs = civArray.filter(Boolean);
Expand Down
18 changes: 18 additions & 0 deletions src/engine/Civilization.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ export class Civilization {
/** Set of civIds with which this civ has established an embassy. */
this.embassies = new Set();

/** Set of civIds this civilization has actually encountered. */
this.contacts = new Set();

/** Civilization II spaceship construction and flight state. */
this.spaceship = {
structural: 0,
propulsion: 0,
fuel: 0,
habitation: 0,
lifeSupport: 0,
solar: 0,
unassignedComponents: 0,
unassignedModules: 0,
launched: false,
launchYear: null,
arrivalYear: null,
};

/** Custom leader name set by the player (null = use default from civs.js). */
this.leaderNameOverride = null;

Expand Down
Loading
Loading