feat(civ): add per-leader starting techs, union with trait techs, and setup background
Allow each opponent to declare their own 2-3 starting technologies in opponents.json, independent of their civ trait. During game creation, personal and trait-based starting techs are unioned (deduped) so pioneering leaders with overlapping lists don't double-grant. - Add optional setup screen background image (civilization-setup-bg) - Update leader detail panel description to list personal starting techs - Deduplicate trait + personal techs via Set in CivilizationLogic - Add validation for startingTechs arrays, tech existence, 2-3 total count, and variety across leaders in verifyCivilization - Add integration tests for personal tech granting and overlap deduplication
This commit is contained in:
parent
80d4a79593
commit
def5c9904a
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue