diff --git a/data/opponents.json b/data/opponents.json index 1477f35..8762e0a 100644 --- a/data/opponents.json +++ b/data/opponents.json @@ -5,6 +5,10 @@ "spriteIndex": 0, "name": "Cy-Bro", "trait": "scientific", + "startingTechs": [ + "alphabet", + "pottery" + ], "bio": "AI Bro sent from the future to show the superiority of the future.", "voice": "Legion", "convo": [ @@ -41,6 +45,11 @@ "spriteIndex": 1, "name": "Brad", "trait": "wealthy", + "startingTechs": [ + "horsebackriding", + "bronzeworking", + "thewheel" + ], "bio": "Came for the honey, stayed for the salmon!", "voice": "Jewel Weaver - Bear", "convo": [ @@ -77,6 +86,7 @@ "spriteIndex": 2, "name": "Victor", "trait": "pioneering", + "startingTechs": [], "bio": "Patient and calculating. Moves are made using ancient technology.", "voice": "Starfield - Vasco", "convo": [ @@ -113,6 +123,10 @@ "spriteIndex": 3, "name": "Croc", "trait": "commercial", + "startingTechs": [ + "bronzeworking", + "masonry" + ], "bio": "Wassssuppp everybody! I'm here for the party!!", "voice": "Batman - Killer Croc", "convo": [ @@ -149,6 +163,11 @@ "spriteIndex": 4, "name": "Mario", "trait": "philosophical", + "startingTechs": [ + "alphabet", + "warriorcode", + "thewheel" + ], "bio": "Can you find your way out of my maze of puzzles, games and fun!", "speech": { "intro": [ @@ -172,6 +191,10 @@ "spriteIndex": 5, "name": "Ethel", "trait": "wealthy", + "startingTechs": [ + "horsebackriding", + "masonry" + ], "bio": "Thank you for visiting with me.", "voice": "Betty White", "convo": [ @@ -208,6 +231,10 @@ "spriteIndex": 6, "name": "Jeff", "trait": "philosophical", + "startingTechs": [ + "horsebackriding", + "pottery" + ], "bio": "I play slow, I win fast!", "voice": "SlowAltered", "convo": [ @@ -244,6 +271,11 @@ "spriteIndex": 7, "name": "Gerome", "trait": "zealous", + "startingTechs": [ + "horsebackriding", + "warriorcode", + "masonry" + ], "bio": "I'm here for the thrill of extreme victory!", "voice": "Mass Effect - Commander Shepard", "convo": [ @@ -280,6 +312,11 @@ "spriteIndex": 8, "name": "Fireball", "trait": "industrious", + "startingTechs": [ + "alphabet", + "thewheel", + "bronzeworking" + ], "bio": "Come on let's play! I promise not to use my x-ray eyes.", "voice": "Zero - Borderlands 3", "convo": [ @@ -316,6 +353,11 @@ "spriteIndex": 9, "name": "Bernie", "trait": "mercantile", + "startingTechs": [ + "pottery", + "ceremonialburial", + "bronzeworking" + ], "bio": "Having fun and playing is the name of the game for me!", "speech": { "intro": [ @@ -339,6 +381,10 @@ "spriteIndex": 10, "name": "Jerry", "trait": "commercial", + "startingTechs": [ + "pottery", + "alphabet" + ], "bio": "Well now yall know I want to play some them games!", "voice": "Life is Strange - Jed Lucan", "convo": [ @@ -375,6 +421,7 @@ "spriteIndex": 11, "name": "Natasha", "trait": "pioneering", + "startingTechs": [], "bio": "Perhaps if I win, you will spill the secrets you've been keeping!", "voice": "Street Fighter 6 - Marisa", "convo": [ @@ -411,6 +458,10 @@ "spriteIndex": 12, "name": "Zanthor", "trait": "philosophical", + "startingTechs": [ + "thewheel", + "alphabet" + ], "bio": "Hocus Pocus Alacazam!!!!", "convo": [ "terry", @@ -446,6 +497,11 @@ "spriteIndex": 14, "name": "Kona", "trait": "industrious", + "startingTechs": [ + "pottery", + "masonry", + "ceremonialburial" + ], "bio": "Woof! Woof!", "speech": { "intro": [ @@ -469,6 +525,10 @@ "spriteIndex": 15, "name": "Michael", "trait": "commercial", + "startingTechs": [ + "warriorcode", + "masonry" + ], "bio": "Hey mon! Yous be knowin' I been wantin' ta play on da Fertig Games for a while now mon!", "voice": "Djed Spence", "convo": [ @@ -504,6 +564,10 @@ "spriteIndex": 13, "name": "Juliet", "trait": "wealthy", + "startingTechs": [ + "thewheel", + "alphabet" + ], "bio": "Let's take a break on a warm summer day to spend time playing games!", "voice": "Street Fighter 6 - Crimson Viper", "speech": { @@ -535,6 +599,7 @@ "spriteIndex": 16, "name": "Balam", "trait": "pioneering", + "startingTechs": [], "bio": "Mystical Powers and a knack for games.", "speech": { "intro": [ @@ -565,6 +630,11 @@ "spriteIndex": 17, "name": "Steve", "trait": "scientific", + "startingTechs": [ + "pottery", + "masonry", + "bronzeworking" + ], "bio": "I traveled WAY to far for this stupid Earth Internet.", "speech": { "intro": [ @@ -595,6 +665,10 @@ "spriteIndex": 18, "name": "Beth", "trait": "industrious", + "startingTechs": [ + "alphabet", + "pottery" + ], "bio": "I ain't seen you 'round these parts before.", "speech": { "intro": [ @@ -625,6 +699,10 @@ "spriteIndex": 19, "name": "Blackwind", "trait": "mercantile", + "startingTechs": [ + "masonry", + "alphabet" + ], "bio": "Aaaaaaargh! Me be playin' these games matey!", "voice": "pirate", "speech": { @@ -656,6 +734,10 @@ "spriteIndex": 20, "name": "Nadia", "trait": "scientific", + "startingTechs": [ + "bronzeworking", + "horsebackriding" + ], "bio": "She's here from the future to learn our games and study our culture.", "voice": "Invincible VS - Atom Eve", "speech": { @@ -687,6 +769,11 @@ "spriteIndex": 21, "name": "The Smasher", "trait": "zealous", + "startingTechs": [ + "warriorcode", + "pottery", + "ceremonialburial" + ], "bio": "Oooooooh yeah brother!!! You wanna play with The Smasher?!", "speech": { "intro": [ @@ -717,6 +804,7 @@ "spriteIndex": 22, "name": "DV-8-2303", "trait": "pioneering", + "startingTechs": [], "bio": "Cybernetic Systems online. Activate human defeat protocol... EOF", "speech": { "intro": [ @@ -747,6 +835,10 @@ "spriteIndex": 23, "name": "Nicole", "trait": "scientific", + "startingTechs": [ + "pottery", + "ceremonialburial" + ], "bio": "I've disabled the firewall protections on this website. Now to hack the core!", "voice": "Life is Strange - Rachael Amber", "speech": { @@ -778,6 +870,10 @@ "spriteIndex": 24, "name": "Kage", "trait": "philosophical", + "startingTechs": [ + "masonry", + "pottery" + ], "bio": "I have traveled far to play these games. Honor me and let us play!", "speech": { "intro": [ @@ -808,6 +904,10 @@ "spriteIndex": 25, "name": "Terry", "trait": "industrious", + "startingTechs": [ + "ceremonialburial", + "masonry" + ], "bio": "I haven't had this much fun since the Mezasoic era!", "speech": { "intro": [ @@ -838,6 +938,10 @@ "spriteIndex": 26, "name": "Schooner", "trait": "commercial", + "startingTechs": [ + "ceremonialburial", + "horsebackriding" + ], "bio": "Ahoy you land lovers!", "voice": "Defiant Calder", "convo": [ @@ -874,6 +978,11 @@ "spriteIndex": 27, "name": "Maurice", "trait": "industrious", + "startingTechs": [ + "masonry", + "pottery", + "thewheel" + ], "bio": "Bucky Beaver and I are there to TRAP all the fun and games!", "speech": { "intro": [ @@ -904,6 +1013,10 @@ "spriteIndex": 28, "name": "Aiko", "trait": "scientific", + "startingTechs": [ + "pottery", + "thewheel" + ], "bio": "From a different galaxy, but has the same great Earth based lifeforms taste for games!", "speech": { "intro": [ @@ -934,6 +1047,10 @@ "spriteIndex": 29, "name": "Klaxon", "trait": "zealous", + "startingTechs": [ + "horsebackriding", + "thewheel" + ], "bio": "MISSION OBJECTIVE: Victory.... all other priorities recinded.", "speech": { "intro": [ diff --git a/src/data/assetManifest.js b/src/data/assetManifest.js index 74e4daf..fe82bbd 100644 --- a/src/data/assetManifest.js +++ b/src/data/assetManifest.js @@ -157,6 +157,7 @@ export const MANIFEST = { // (classic today; asian etc. drop in later with no code change). civilization: [ { type: 'json', key: 'civilization-rules', path: 'data/civilization-rules.json' }, + image('civilization-setup-bg', 'assets/images/civilization/background.png'), (scene) => sheetsFrom(scene, 'civilization-artwork', ['terrainSheet', 'resourceSheet', 'improvementSheet', 'unitSheet', 'iconSheet', 'citySheets']), ], diff --git a/src/games/civilization/CivilizationGame.js b/src/games/civilization/CivilizationGame.js index da2c30c..19d7b67 100644 --- a/src/games/civilization/CivilizationGame.js +++ b/src/games/civilization/CivilizationGame.js @@ -100,6 +100,14 @@ export default class CivilizationGame extends Phaser.Scene { const root = this.setupRoot; const cx = GAME_WIDTH / 2; + // Background art is optional (assets/images/civilization/background.png, + // registered in src/data/assetManifest.js) — the scene's base dark fill + // from create() already covers the fallback if it isn't loaded, so this + // is simply skipped when missing rather than needing its own else-branch. + if (this.textures.exists('civilization-setup-bg')) { + root.add(this.add.image(cx, GAME_HEIGHT / 2, 'civilization-setup-bg')); + } + root.add(this.add.text(cx, 52, 'CIVILIZATION', { fontFamily: 'Righteous', fontSize: '54px', color: COLORS.accentHex, }).setOrigin(0.5)); @@ -245,7 +253,11 @@ export default class CivilizationGame extends Phaser.Scene { const { nameText, traitText, descText } = this.leaderDetailPanel; nameText.setText(op?.name ?? ''); traitText.setText(trait ? trait.name : 'Balanced (no bonuses)'); - descText.setText(trait ? trait.description : ''); + const techNames = (op?.startingTechs ?? []).map((id) => this.rules.techs[id]?.name ?? id); + const lines = []; + if (trait?.description) lines.push(trait.description); + if (techNames.length) lines.push(`Starts knowing ${techNames.join(', ')}`); + descText.setText(lines.join(' • ')); } hasSave() { @@ -263,7 +275,7 @@ export default class CivilizationGame extends Phaser.Scene { [rivals[i], rivals[j]] = [rivals[j], rivals[i]]; } const leaders = [me, ...rivals.slice(0, opponents)] - .map((op) => ({ id: op.id, name: op.name, trait: op.trait })); + .map((op) => ({ id: op.id, name: op.name, trait: op.trait, startingTechs: op.startingTechs })); const seed = (Date.now() % 1000000) + 1; try { this.state = Logic.createGame(this.rules, { diff --git a/src/games/civilization/CivilizationLogic.js b/src/games/civilization/CivilizationLogic.js index 2f770b5..9e994c7 100644 --- a/src/games/civilization/CivilizationLogic.js +++ b/src/games/civilization/CivilizationLogic.js @@ -116,7 +116,13 @@ export function createGame(rules, opts) { nameOrder, score: 0, }; - for (const techId of trait?.startingTechs ?? []) grantTech(rules, state, civ, techId); + // Every leader personally lists 2-3 starting techs (data/opponents.json + // startingTechs) independent of their trait — pioneering leaders already + // get 2 from their trait above and list none of their own, everyone else + // gets their personal pick. Union (not concat) guards against double- + // granting if a personal list ever overlaps a trait-granted tech. + const startingTechs = new Set([...(trait?.startingTechs ?? []), ...(leaders[i].startingTechs ?? [])]); + for (const techId of startingTechs) grantTech(rules, state, civ, techId); state.civs.push(civ); state.explored.push(new Array(n).fill(0)); } diff --git a/tools/verifyCivilization.js b/tools/verifyCivilization.js index ecc3d3c..f877c1e 100644 --- a/tools/verifyCivilization.js +++ b/tools/verifyCivilization.js @@ -197,7 +197,20 @@ if (RULES) { for (const op of opponentsJson.opponents ?? []) { check(`opponent ${op.id} has trait`, typeof op.trait === 'string' && op.trait.length > 0); check(`opponent ${op.id} trait resolves`, !!RULES.civTraits[op.trait], op.trait); + check(`opponent ${op.id} has startingTechs array`, Array.isArray(op.startingTechs)); + for (const techId of op.startingTechs ?? []) { + check(`opponent ${op.id} startingTechs ${techId} known tech`, !!RULES.techs[techId]); + } + const traitTechs = RULES.civTraits[op.trait]?.startingTechs ?? []; + const total = new Set([...traitTechs, ...(op.startingTechs ?? [])]).size; + check(`opponent ${op.id} has 2-3 total starting techs`, total === 2 || total === 3, `${total}`); } + + // Some variety across leaders — not every leader with the same trait + // should end up with an identical personal tech list. + const nonPioneering = opponentsJson.opponents.filter((op) => op.trait !== 'pioneering'); + const distinctLists = new Set(nonPioneering.map((op) => [...op.startingTechs].sort().join(','))); + check('starting tech lists show variety across leaders', distinctLists.size > 5, `${distinctLists.size}`); } // --------------------------------------------------------------------------- @@ -205,17 +218,25 @@ section('1c. trait application'); if (RULES) { const leaders = [ - { id: 'a', name: 'A', trait: 'scientific' }, + { id: 'a', name: 'A', trait: 'scientific', startingTechs: ['pottery', 'masonry'] }, { id: 'b', name: 'B', trait: 'wealthy' }, { id: 'c', name: 'C', trait: 'pioneering' }, { id: 'd', name: 'D', trait: undefined }, + // trait's alphabet/bronzeworking overlaps this leader's own list — the + // union must dedupe, not error or double-grant. + { id: 'e', name: 'E', trait: 'pioneering', startingTechs: ['alphabet', 'thewheel'] }, ]; const st = Logic.createGame(RULES, { sizeId: 'small', seed: 5, difficultyId: 'prince', leaders }); + check('personal startingTechs granted', st.civs[0].known.pottery === true && st.civs[0].known.masonry === true); check('wealthy gets +100 starting gold', st.civs[1].gold === 150, `${st.civs[1].gold}`); check('pioneering knows alphabet', st.civs[2].known.alphabet === true); check('pioneering knows bronzeworking', st.civs[2].known.bronzeworking === true); check('no-trait civ unaffected', st.civs[3].gold === 50 && Object.keys(st.civs[3].known).length === 0, `${st.civs[3].gold}`); + check('overlapping trait + personal techs dedupe to 3 known', + Object.keys(st.civs[4].known).length === 3 + && st.civs[4].known.alphabet && st.civs[4].known.bronzeworking && st.civs[4].known.thewheel, + `${Object.keys(st.civs[4].known).join(',')}`); // scienceMult/goldMult/shieldMult actually flow into cityYields: two // synthetic cities sharing the exact same tile/buildings/size, differing