feat: add colony naming system and species lore conversations
Introduce colony naming with a 50-name bank per species, shuffled per-empire and displayed as defaults in naming prompts. Colony names now appear across all UI layers (star map, side panel, colony view, system view) with the astronomical designation as fallback. Add three pure-flavor conversation topics to diplomacy: "Their People" (neutral+), "Their Homeworld" (friendly), and "A Story" (friendly). Each diplomacy-capable species receives unique lore-rich responses (3 variants each) reflecting their culture, homeworld, and history. Other changes: - Switch ursaal videos from generic "human" assets to species-specific - Widen diplomacy buttons to 200px; add auto-shrink for long labels - Add ±5 population transport buttons to colony view - Validate colonyNames (50+ entries, no duplicates) - Enforce minimum lore variant counts in verification
This commit is contained in:
parent
0adb689c19
commit
c52b5c6429
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -141,9 +141,9 @@
|
|||
"happy": { "key": "vega-audience-kestrelli-happy", "path": "assets/videos/vega/audience-kestrelli-happy.mp4" }
|
||||
},
|
||||
"ursaal": {
|
||||
"angry": { "key": "vega-audience-human-angry", "path": "assets/videos/vega/audience-human-angry.mp4" },
|
||||
"neutral": { "key": "vega-audience-human-neutral", "path": "assets/videos/vega/audience-human-neutral.mp4" },
|
||||
"happy": { "key": "vega-audience-human-happy", "path": "assets/videos/vega/audience-human-happy.mp4" }
|
||||
"angry": { "key": "vega-audience-ursaal-angry", "path": "assets/videos/vega/audience-ursaal-angry.mp4" },
|
||||
"neutral": { "key": "vega-audience-ursaal-neutral", "path": "assets/videos/vega/audience-ursaal-neutral.mp4" },
|
||||
"happy": { "key": "vega-audience-ursaal-happy", "path": "assets/videos/vega/audience-ursaal-happy.mp4" }
|
||||
},
|
||||
"umbrix": {
|
||||
"angry": { "key": "vega-audience-human-angry", "path": "assets/videos/vega/audience-human-angry.mp4" },
|
||||
|
|
|
|||
|
|
@ -95,8 +95,8 @@
|
|||
|
||||
"_buildingsReadme": "Colony structures. channel names the slider a building multiplies (industry/research/defense/ecology/ships), or is null for a flat effect. Buildings are built from the colony queue; effects stack multiplicatively within a channel.",
|
||||
"buildings": [
|
||||
{ "id": "automatedfactory", "name": "Automated Factory", "prereq": null, "cost": 60, "upkeep": 1, "channel": "industry", "mult": 1.5, "frame": 0, "desc": "Half again as much industrial output." },
|
||||
{ "id": "researchlab", "name": "Research Laboratory","prereq": "electroniccomputer", "cost": 90, "upkeep": 2, "channel": "research", "mult": 1.5, "frame": 1, "desc": "Half again as much research output." },
|
||||
{ "id": "automatedfactory", "name": "Automated Factory", "prereq": null, "cost": 60, "upkeep": 1, "channel": "industry", "mult": 1.5, "frame": 0, "desc": "+50% industrial output." },
|
||||
{ "id": "researchlab", "name": "Research Laboratory","prereq": "electroniccomputer", "cost": 90, "upkeep": 2, "channel": "research", "mult": 1.5, "frame": 1, "desc": "+50% research output." },
|
||||
{ "id": "missilebase", "name": "Missile Base", "prereq": "hypervrockets", "cost": 80, "upkeep": 2, "channel": "defense", "mult": 1.6, "frame": 2, "desc": "Ground-launched interceptors bite into orbiting fleets." },
|
||||
{ "id": "pollutionprocessor","name": "Pollution Processor","prereq": "ecorestoration", "cost": 70, "upkeep": 1, "channel": "ecology", "mult": 0.6, "frame": 3, "desc": "Waste cleanup costs 40% less on this world." },
|
||||
{ "id": "spaceport", "name": "Space Port", "prereq": "deuteriumcells", "cost": 110, "upkeep": 2, "channel": null, "mult": 1, "frame": 4, "effects": { "tradeBonus": 4 }, "desc": "Civilian traffic. +4 BC per turn." },
|
||||
|
|
@ -175,6 +175,7 @@
|
|||
"desc": "Traders and talkers. Humans win the votes other empires assume they can ignore.",
|
||||
"strengths": ["Finest diplomats in the galaxy", "+50% trade income"],
|
||||
"weaknesses": ["No industrial or scientific edge"],
|
||||
"colonyNames": ["New Meridian", "Concord Point", "Fair Harbor", "Tradewind Station", "Ledgerrest", "Accord City", "Compact Landing", "Exchange Reach", "Bartertown", "Port Solace", "New Geneva", "Wayfarer's Rest", "Handshake Bay", "Common Ground", "New Providence", "Parley Hollow", "Merchant's Crest", "Open Market", "Diplomat's Landing", "Sunward Vale", "Amity Falls", "New Frontier", "Silverleaf Landing", "Charter Cove", "Freeport", "Goodwill Station", "New Alexandria", "Compromise Ridge", "Traders' Rest", "Council Reach", "New Avalon", "Broker's Landing", "Fellowship Point", "Terra Nova Colony", "Coin & Compass", "Truce Hollow", "New Rotterdam", "Silk Route Station", "Envoy's Landing", "Harborlight", "New Carthage", "Pact Hollow", "Steady Hand", "Crossroads Colony", "Welcome Reach", "New Marrakesh", "Handclasp Bay", "Fortune's Landing", "Neutral Ground", "Homeward Bound"],
|
||||
"traits": {
|
||||
"industryMult": 1.0, "researchMult": 1.0, "tradeMult": 1.5, "growthMult": 1.0, "ecologyMult": 1.0,
|
||||
"shipAttack": 0, "shipDefense": 0, "groundAttack": 10, "groundDefense": 10,
|
||||
|
|
@ -189,6 +190,7 @@
|
|||
"desc": "Avian aristocrats bred for flight. Their pilots simply do not get hit.",
|
||||
"strengths": ["Enormous ship defence bonus", "Skilled at propulsion"],
|
||||
"weaknesses": ["Poor ground troops", "Weak industry"],
|
||||
"colonyNames": ["Windspire", "Skyhold", "Aerie Prime", "Talon's Rest", "Highwing Reach", "Cloudperch", "Windrider's Roost", "Featherfall", "Zephyr's Crest", "Skybarrow", "Wingspan Hold", "Stormwing Manor", "Gale Aerie", "Sunwing Perch", "Nimbus Court", "Highfeather", "Windgate", "Skysong", "Thermal Rise", "Plumecrest", "Windward Eyrie", "Cirrus Hold", "Featherlight", "Aloft Reach", "Skyfarer's Roost", "Wingward", "Duskwing Hollow", "Windveil", "Sunward Aerie", "Talonreach", "Cloudspire", "Wingfall", "Highcourt Aerie", "Windrush", "Stormcrest", "Nightwing Perch", "Skyward Bastion", "Featherhold", "Glidewind", "Windborn Hall", "Aeriewatch", "Skysworn", "Loftmark", "Windfeather Court", "Cloudreach Manor", "Highflight", "Windcrest Hollow", "Aerielle", "Skygrace", "Windwake"],
|
||||
"traits": {
|
||||
"industryMult": 0.9, "researchMult": 1.0, "tradeMult": 1.0, "growthMult": 1.0, "ecologyMult": 1.0,
|
||||
"shipAttack": 0, "shipDefense": 50, "groundAttack": -10, "groundDefense": -10,
|
||||
|
|
@ -203,6 +205,7 @@
|
|||
"desc": "Heavy-worlders. An Ursaal division walks through defences that would stop an army.",
|
||||
"strengths": ["+50 ground combat", "Settles high-gravity worlds without penalty"],
|
||||
"weaknesses": ["Poor research", "Clumsy diplomats"],
|
||||
"colonyNames": ["Ironhold", "Stonewall Reach", "Granite Bastion", "Deeproot Hold", "Bedrock Colony", "Anvilfast", "Boulderguard", "Highgrav Hold", "Stonefist", "Ironroot", "Craghold", "Heavyhome", "Fortress Deep", "Stoneshoulder", "Gravitas Hold", "Bastionrock", "Ironclad Reach", "Mountainheart", "Stonebrace", "Ridgehold", "Deepstone", "Ironwall", "Cragfast", "Highstone Bastion", "Bedrock's Edge", "Stoutforge", "Graniteborn", "Ironmarch", "Steadfast Hold", "Boulderreach", "Deepgrav Colony", "Stonewatch", "Ironbrace", "Craghome", "Fortholm", "Heavyhand Reach", "Rockspine", "Stonemarch", "Ironkeep", "Highhold", "Graniteguard", "Deepwall", "Stonefast", "Ironridge", "Massif Hold", "Bedrockmarch", "Stoneheart Colony", "Ironfoot Reach", "Cragmoor", "Foundation Hold"],
|
||||
"traits": {
|
||||
"industryMult": 1.0, "researchMult": 0.8, "tradeMult": 1.0, "growthMult": 1.0, "ecologyMult": 1.0,
|
||||
"shipAttack": 0, "shipDefense": 0, "groundAttack": 50, "groundDefense": 50,
|
||||
|
|
@ -217,6 +220,7 @@
|
|||
"desc": "Shapeshifters who wear other species like coats. Nobody trusts them, and everybody is right not to.",
|
||||
"strengths": ["Unmatched espionage and sabotage", "Steal technology others cannot"],
|
||||
"weaknesses": ["Every empire starts hostile toward them"],
|
||||
"colonyNames": ["Shrouded Reach", "Whisper's Veil", "Maskfall", "Duskmask Hollow", "Hidden Face", "Veiled Hollow", "Changeling's Rest", "Shadowseam", "Cloakhaven", "Secondskin", "Nightveil", "Falseface Reach", "Mirrorshade", "Unseen Hollow", "Guisehome", "Shroudmark", "Doubtward", "Silent Mask", "Umbral Rest", "Twoface Reach", "Hushveil", "Shapesong", "Concealed Landing", "Wraithcoat", "Veilborn", "Trustless Hollow", "Otherface", "Nightmask Colony", "Whisperseam", "Shiftveil", "Feigned Hollow", "Skinchange Reach", "Deceiver's Rest", "Camouflage Hold", "Ashenmask", "Voidveil", "Suspicion's Edge", "Cloak & Coat", "Shadowguise", "Unmasked Reach", "Secretskin", "Mimicry Hollow", "Hollow Mirror", "Grayveil", "Substitute's Landing", "Shadowform Colony", "Nightdouble", "Wearer's Rest", "Truthless Hollow", "Undercover Reach"],
|
||||
"traits": {
|
||||
"industryMult": 1.0, "researchMult": 1.0, "tradeMult": 1.0, "growthMult": 1.0, "ecologyMult": 1.0,
|
||||
"shipAttack": 0, "shipDefense": 0, "groundAttack": 0, "groundDefense": 0,
|
||||
|
|
@ -231,6 +235,7 @@
|
|||
"desc": "A hive that builds without pause, argues with nobody, and understands negotiation not at all.",
|
||||
"strengths": ["+50% industry on every world"],
|
||||
"weaknesses": ["Poor research", "Cannot conduct meaningful diplomacy"],
|
||||
"colonyNames": ["Hive Cluster One", "The Foundry Nest", "Broodwork Colony", "Chitin Hold", "Swarmreach", "Hive Node Seven", "The Endless Weave", "Larval Works", "Drone Assembly", "Hive Cell Twelve", "The Great Warren", "Chitterhold", "Worker's Convergence", "Hive Spindle", "The Unceasing Forge", "Mandible Reach", "Hive Node Three", "Combwork Hold", "The Silent Multitude", "Broodnest Prime", "Hive Lattice", "Chitinguard", "The Tireless Works", "Swarmhold", "Hive Cell Nine", "Larvalreach", "The Ceaseless Hive", "Antennae Point", "Hive Node Four", "Combhold", "The Undivided Nest", "Chamberwork", "Hive Spire Two", "Broodline Colony", "The Iron Swarm", "Waxcomb Hold", "Hive Node Eight", "Massed Works", "The Singular Hive", "Cellblock Warren", "Hive Lattice Five", "Droneworks", "The Multiplying Nest", "Chitterreach", "Hive Node Eleven", "Combined Works", "The Uncounted Swarm", "Sporehold", "Hive Node Six", "The Perfect Colony"],
|
||||
"traits": {
|
||||
"industryMult": 1.5, "researchMult": 0.8, "tradeMult": 1.0, "growthMult": 1.0, "ecologyMult": 1.0,
|
||||
"shipAttack": 0, "shipDefense": 0, "groundAttack": 0, "groundDefense": 0,
|
||||
|
|
@ -245,6 +250,7 @@
|
|||
"desc": "Cyborgs who treat a planet as a chassis. Every citizen runs more machines than anyone else can.",
|
||||
"strengths": ["+2 factories per population", "Strong construction research"],
|
||||
"weaknesses": ["Filthy — waste piles up twice as fast"],
|
||||
"colonyNames": ["Circuit Reach", "Ironforge Array", "The Chassis Works", "Gearlock Hold", "Servo Junction", "Foundry Prime", "Piston Hollow", "The Assembly Line", "Cogwork Colony", "Alloy Reach", "The Machine Heart", "Rivethold", "Conduit Bay", "The Forgeworks", "Girderfast", "Wireframe Colony", "The Scrapyard Rise", "Bolted Reach", "Turbine Hold", "The Mainframe", "Steelweld", "Actuator Point", "The Refinery Deep", "Coldsteel Reach", "Chassisguard", "The Fabrication Yard", "Circuitry Hollow", "Bearing Hold", "The Foundry Deep", "Ironlathe", "Sprocket Reach", "The Assembly Core", "Wireworks", "Boltframe Colony", "The Grinding Works", "Hydraulic Hold", "Alloyforge", "The Mechanized Rise", "Weldpoint", "Servohold", "The Combustion Yard", "Ironwire Reach", "Furnace Array", "The Rust Belt", "Coglathe", "The Forgework Deep", "Piston Reach", "Machinehold", "The Overclock", "Steelworks Prime"],
|
||||
"traits": {
|
||||
"industryMult": 1.0, "researchMult": 1.0, "tradeMult": 1.0, "growthMult": 1.0, "ecologyMult": 2.0,
|
||||
"shipAttack": 0, "shipDefense": 0, "groundAttack": 0, "groundDefense": 0,
|
||||
|
|
@ -259,6 +265,7 @@
|
|||
"desc": "Feline gunners with reflexes no computer improves on. Their beams land when nobody else's would.",
|
||||
"strengths": ["Enormous ship attack bonus", "Strong weapons research"],
|
||||
"weaknesses": ["Poor ground defence", "Distrusted"],
|
||||
"colonyNames": ["Clawreach", "Bloodfang Outpost", "Huntersrest", "Razorclaw Hold", "Stalker's Perch", "Fangmark", "Prowl Hollow", "Sharpshot Reach", "Nightclaw Colony", "Huntground", "Talonfang", "Deadeye Reach", "Bristlefang", "Ambush Hollow", "Clawmark Rise", "Predator's Rest", "Snarl Reach", "Sureshot Hold", "Bloodclaw", "Stalking Grounds", "Fangfall", "Sniper's Roost", "Rakeclaw", "Huntersedge", "Silentpaw Reach", "Marksman's Hollow", "Clawstrike", "Nightprowl Colony", "Trueshot Reach", "Sharpfang", "Ambushpoint", "Huntress Reach", "Clawtip Hold", "Deadshot Hollow", "Prowlers' Rest", "Bloodhunt Reach", "Ripclaw", "Sureclaw", "Stalkfast", "Fangwatch", "Killshot Hollow", "Clawborn Colony", "Huntfang", "Sharpeye Reach", "Ambushfang", "Nightstalk Hold", "Trueclaw", "Preyfall", "Huntersmark", "Clawhold Prime"],
|
||||
"traits": {
|
||||
"industryMult": 1.0, "researchMult": 0.9, "tradeMult": 1.0, "growthMult": 1.0, "ecologyMult": 1.0,
|
||||
"shipAttack": 50, "shipDefense": 0, "groundAttack": 0, "groundDefense": -25,
|
||||
|
|
@ -273,6 +280,7 @@
|
|||
"desc": "Vast fragile intellects. They will out-think the galaxy if they survive long enough to finish thinking.",
|
||||
"strengths": ["Double research output", "Can research anything"],
|
||||
"weaknesses": ["Hopeless in a ground fight"],
|
||||
"colonyNames": ["The Contemplation", "Axiom's Rest", "Thoughtwell", "The Cogitation", "Insight Reach", "Ponder's Hollow", "The Deduction", "Mindspire", "Theorem Hold", "The Reflection", "Synapse Colony", "Wisdomreach", "The Postulate", "Enigma Hollow", "The Calculation", "Noesis Point", "The Hypothesis", "Reasonhold", "The Contemplative Deep", "Cognizance Reach", "The Inquiry", "Thoughtform Colony", "The Proof", "Logician's Rest", "The Abstraction", "Intellect Hold", "The Meditation", "Puzzlework", "The Conjecture", "Ponderhold", "The Epiphany", "Mindweave Reach", "The Deliberation", "Wisdomhold", "The Paradox", "Thoughtline", "The Revelation", "Synthesis Hollow", "The Understanding", "Cerebrum Reach", "The Discourse", "Knowledgehold", "The Singularity Thought", "Rationale Reach", "The Perception", "Sagehold", "The Great Thinking", "Insightform Colony", "The Awareness", "Mindsreach"],
|
||||
"traits": {
|
||||
"industryMult": 1.0, "researchMult": 2.0, "tradeMult": 1.0, "growthMult": 1.0, "ecologyMult": 1.0,
|
||||
"shipAttack": 0, "shipDefense": 0, "groundAttack": -50, "groundDefense": -50,
|
||||
|
|
@ -287,6 +295,7 @@
|
|||
"desc": "Reptilian broodmothers. Ssakar colonies fill to capacity while rivals are still unpacking.",
|
||||
"strengths": ["Population grows twice as fast"],
|
||||
"weaknesses": ["Poor research", "Untrusted by the older species"],
|
||||
"colonyNames": ["Spawning Delta", "The Broodwarren", "Nestmarsh", "Eggcluster Hollow", "Hatchling's Rise", "The Fertile Coil", "Broodmarsh", "Clutchhold", "The Spawning Pools", "Nestreach", "Broodmother's Rest", "The Thousand Eggs", "Hatchreach", "Coilnest", "The Teeming Marsh", "Eggwarren Colony", "Broodhollow", "The Second Clutch", "Nestingground", "Scalebrood Reach", "The Overflowing Nest", "Hatchlingmarsh", "Broodcoil", "The Fertile Reach", "Nestwarren", "Clutchmarsh", "The Endless Brood", "Spawnreach", "Eggmother's Rest", "The Teeming Coil", "Broodnest Deep", "Hatchmarsh", "The Third Clutch", "Nestcoil", "Scalewarren", "The Fecund Marsh", "Broodreach Prime", "Eggclutch Hollow", "The Rising Brood", "Nestmother's Reach", "Coilwarren", "The Sprawling Nest", "Hatchbrood Colony", "Spawnmarsh", "The Fourth Clutch", "Broodwarren Deep", "Nestegg Reach", "The Multiplying Marsh", "Scalenest", "The Great Clutch"],
|
||||
"traits": {
|
||||
"industryMult": 1.0, "researchMult": 0.85, "tradeMult": 1.0, "growthMult": 2.0, "ecologyMult": 1.0,
|
||||
"shipAttack": 0, "shipDefense": 0, "groundAttack": 10, "groundDefense": 0,
|
||||
|
|
@ -301,6 +310,7 @@
|
|||
"desc": "Crystalline life that breathes nothing and fears no atmosphere. They settle where the map says nobody can.",
|
||||
"strengths": ["Colonise any world from turn one", "Immune to hostile environments and pollution"],
|
||||
"weaknesses": ["Population grows at half speed", "Cannot negotiate at all"],
|
||||
"colonyNames": ["Crystalvein", "The Shard", "Silicareach", "Glasswrought Hold", "Prismfall", "The Faceted Rise", "Quartzhold", "Crystalline Deep", "The Refractor", "Stonelight Reach", "Geode Hollow", "The Lattice Fields", "Mineralreach", "Sparkstone Hold", "The Crystal Bloom", "Facetedge", "Glimmerhold", "The Silicate Deep", "Prismwatch", "Crystalgrowth Colony", "The Fractured Rise", "Shardreach", "Latticework", "The Vitreous Deep", "Gleamstone Hold", "Crystalshard Reach", "The Diamond Fields", "Facetreach", "Glasswork Colony", "The Prismatic Deep", "Quartzreach", "Sparkledge", "The Crystalline Rise", "Refractedge", "Silicavein", "The Geode Fields", "Crystalspire", "Mineralight Hold", "The Faceted Deep", "Glassreach", "Vitreous Hollow", "The Shard Fields", "Crystalborn Colony", "Prismreach", "The Lattice Deep", "Stonegleam Hold", "Facetform Reach", "The Silicate Rise", "Crystalwatch", "The Unbreathing Deep"],
|
||||
"traits": {
|
||||
"industryMult": 1.0, "researchMult": 1.0, "tradeMult": 1.0, "growthMult": 0.5, "ecologyMult": 0.0,
|
||||
"shipAttack": 0, "shipDefense": 0, "groundAttack": 0, "groundDefense": 0,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import * as Phaser from 'phaser';
|
||||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { TextInput } from '../../ui/TextInput.js';
|
||||
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||
import { getGameSoundtrack } from '../../services/soundtrack.js';
|
||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||
|
|
@ -236,10 +237,16 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
* pop-over's copy would kill the card's — and moving the one that already
|
||||
* exists sidesteps the question entirely while halving the decoding.
|
||||
*/
|
||||
openSpeciesDetail(parentLayer, spec, card, portrait, onSelect) {
|
||||
openSpeciesDetail(parentLayer, spec, card, portrait, onSelect, nameInput = null) {
|
||||
if (this.speciesDetailOpen) return;
|
||||
this.speciesDetailOpen = true;
|
||||
|
||||
// The colony-name field is a real DOM <input>, which paints in its own
|
||||
// layer above the canvas (z-index, styles.css) regardless of this popup's
|
||||
// Phaser depth — a Phaser depth/z-index bump can't pull it back underneath.
|
||||
// Hidden for the popup's whole lifetime so it can never float on top of it.
|
||||
if (nameInput) nameInput.el.style.visibility = 'hidden';
|
||||
|
||||
const CARD_PORTRAIT = { x: 52, y: 62, size: 84 };
|
||||
const PANEL_W = 860;
|
||||
const PANEL_H = 640;
|
||||
|
|
@ -347,6 +354,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
pop.destroy();
|
||||
veil.destroy();
|
||||
this.speciesDetailOpen = false;
|
||||
if (nameInput) nameInput.el.style.visibility = '';
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -400,6 +408,16 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
shapeId: 'spiral',
|
||||
difficultyId: 'normal',
|
||||
empires: 4,
|
||||
homeColonyName: '',
|
||||
};
|
||||
|
||||
// Declared up here so the species cards (built next) and the name field
|
||||
// (built after the option rows) can both reach them — both close over
|
||||
// these rather than each other, since either can flip readiness.
|
||||
let nameInput = null;
|
||||
const refreshStart = () => {
|
||||
const ready = !!choice.speciesId && choice.homeColonyName.trim().length > 0;
|
||||
start.setEnabled(ready);
|
||||
};
|
||||
|
||||
// --- species picker
|
||||
|
|
@ -440,8 +458,17 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
choice.speciesId = spec.id;
|
||||
for (const c of cards) c.bg.setStrokeStyle(1.5, 0x24405f, 1);
|
||||
bg.setStrokeStyle(2.5, 0x6fc4ff, 1);
|
||||
start.setEnabled(true);
|
||||
});
|
||||
// Re-seed the name field with this species' own flavour every time —
|
||||
// a name typed for a previously-picked species would read oddly on
|
||||
// this one, and the player can still overwrite it. Randomized so it
|
||||
// is not always the same first name off the bank; Math.random is
|
||||
// fine here, same as the rival-species shuffle below — no game
|
||||
// state exists yet for this to desync.
|
||||
const defaultName = spec.colonyNames[Math.floor(Math.random() * spec.colonyNames.length)];
|
||||
choice.homeColonyName = defaultName;
|
||||
if (nameInput) nameInput.value = defaultName;
|
||||
refreshStart();
|
||||
}, nameInput);
|
||||
}));
|
||||
card.add(zone);
|
||||
layer.add(card);
|
||||
|
|
@ -486,7 +513,24 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
layer.add(b);
|
||||
});
|
||||
|
||||
layer.add(this.add.text(GAME_WIDTH / 2 - 620, optY + 248, 'Home Colony', {
|
||||
fontFamily: FONT, fontSize: '20px', color: '#8fa8c0',
|
||||
}).setOrigin(0, 0.5));
|
||||
nameInput = new TextInput(this, GAME_WIDTH / 2 + 145, optY + 248, {
|
||||
width: 1035, height: 44, value: choice.homeColonyName, maxLength: 28, autocomplete: 'off',
|
||||
placeholder: 'Select a species above',
|
||||
});
|
||||
nameInput.on('input', () => {
|
||||
choice.homeColonyName = nameInput.value;
|
||||
refreshStart();
|
||||
});
|
||||
|
||||
const start = new Button(this, GAME_WIDTH / 2, GAME_HEIGHT - 90, 'Begin', this.uiClick(() => {
|
||||
// Belt-and-braces: setEnabled(false) already blocks pointer input while
|
||||
// either condition fails, but this is the actual gate.
|
||||
if (!choice.speciesId || !choice.homeColonyName.trim()) { refreshStart(); return; }
|
||||
choice.homeColonyName = choice.homeColonyName.trim();
|
||||
nameInput.destroy();
|
||||
layer.destroy();
|
||||
this.beginGame(choice);
|
||||
}), { width: 260, height: 64, fontSize: 30 });
|
||||
|
|
@ -495,6 +539,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
|
||||
// Resuming lives on the landing screen now, so this only steps back to it.
|
||||
const back = new Button(this, 120, 60, '← Back', this.uiClick(() => {
|
||||
nameInput.destroy();
|
||||
layer.destroy();
|
||||
this.showLanding();
|
||||
}), { width: 170, height: 48, fontSize: 20, variant: 'ghost' });
|
||||
|
|
@ -526,6 +571,7 @@ export default class MasterOfVegaGame extends Phaser.Scene {
|
|||
seed: (Math.random() * 1e9) | 0,
|
||||
speciesIds,
|
||||
humanIndex: 0,
|
||||
homeColonyName: choice.homeColonyName,
|
||||
});
|
||||
}
|
||||
this.state.rules = this.rules;
|
||||
|
|
|
|||
|
|
@ -292,7 +292,11 @@ const CHAT_Y = VIDEO_Y;
|
|||
const CHAT_H = 620;
|
||||
|
||||
const BTN_Y = CHAT_Y + CHAT_H + 55;
|
||||
const BTN_W = 190;
|
||||
// Widened from 190 — as close to CHAT_W's 3-per-row ceiling as BTN_GAP allows
|
||||
// (3*200 + 2*16 = 632, just inside CHAT_W's 640) so longer labels (the
|
||||
// CONVERSATION topics especially) need less help from Button's own
|
||||
// shrink-to-fit before they read comfortably.
|
||||
const BTN_W = 200;
|
||||
const BTN_H = 60;
|
||||
const BTN_GAP = 16;
|
||||
|
||||
|
|
@ -576,6 +580,24 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
afterAction();
|
||||
}
|
||||
|
||||
// --- pure-flavor conversation topics: no treaty/attitude effect, so no
|
||||
// afterAction() — pushChat()/say() already do everything the chat log
|
||||
// needs, and nothing about the mood, status line or button set changes.
|
||||
function doAskPeople() {
|
||||
pushChat('p', pickLine(chat.askPeople, vars));
|
||||
say(pickLine(chat.peopleLore, vars));
|
||||
}
|
||||
|
||||
function doAskHomeworld() {
|
||||
pushChat('p', pickLine(chat.askHomeworld, vars));
|
||||
say(pickLine(chat.homeworldLore, vars));
|
||||
}
|
||||
|
||||
function doAskStory() {
|
||||
pushChat('p', pickLine(chat.askStory, vars));
|
||||
say(pickLine(chat.storyLore, vars));
|
||||
}
|
||||
|
||||
function pickFromList(title, ids, onPick) {
|
||||
const box = scene.add.container(0, 0);
|
||||
root.add(box);
|
||||
|
|
@ -605,6 +627,19 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
});
|
||||
}
|
||||
|
||||
/** Lays out one row (or wrapped rows) of [label, fn] pairs at `y`, 3 per row. Returns the row count. */
|
||||
function layoutRow(list, y, extraOpts = {}) {
|
||||
const perRow = 3;
|
||||
list.forEach(([label, fn], i) => {
|
||||
const col = i % perRow;
|
||||
const row = Math.floor(i / perRow);
|
||||
const x = CHAT_X + col * (BTN_W + BTN_GAP) + BTN_W / 2;
|
||||
const yy = y + row * (BTN_H + BTN_GAP) + BTN_H / 2;
|
||||
buttonRow.add(new Button(scene, x, yy, label, fn, { width: BTN_W, height: BTN_H, fontSize: 16, ...extraOpts }));
|
||||
});
|
||||
return Math.ceil(list.length / perRow) || 0;
|
||||
}
|
||||
|
||||
function renderButtons() {
|
||||
buttonRow.removeAll(true);
|
||||
const offer = state.empires[me].pendingOffers[otherIdx];
|
||||
|
|
@ -625,14 +660,28 @@ export function openAudienceScreen(scene, rules, state, me, otherIdx, art, onClo
|
|||
const want = Object.keys(state.empires[otherIdx].known).filter((t) => !state.empires[me].known[t]);
|
||||
if (give.length && want.length) items.push(['Trade Tech', () => openTechPicker(give, want)]);
|
||||
}
|
||||
const perRow = 3;
|
||||
items.forEach(([label, fn], i) => {
|
||||
const col = i % perRow;
|
||||
const row = Math.floor(i / perRow);
|
||||
const x = CHAT_X + col * (BTN_W + BTN_GAP) + BTN_W / 2;
|
||||
const y = BTN_Y + row * (BTN_H + BTN_GAP) + BTN_H / 2;
|
||||
buttonRow.add(new Button(scene, x, y, label, fn, { width: BTN_W, height: BTN_H, fontSize: 16 }));
|
||||
});
|
||||
const treatyRows = layoutRow(items, BTN_Y);
|
||||
|
||||
// Lore/conversation topics — pure flavor, appended UNCONDITIONALLY after
|
||||
// the treaty-branch logic above, gated purely on mood tier (never on
|
||||
// offer/atWar/canNegotiate state).
|
||||
const mood = videoMood(attitudeOf(state, otherIdx, me));
|
||||
const loreItems = [];
|
||||
// Kept short on purpose — sitting under the CONVERSATION label already
|
||||
// frames these as topics, so "Their People" reads the same as "Tell Me
|
||||
// About Your People" would, without fighting BTN_W for room.
|
||||
if (mood !== 'angry') loreItems.push(['Their People', doAskPeople]);
|
||||
if (mood === 'happy') {
|
||||
loreItems.push(['Their Homeworld', doAskHomeworld]);
|
||||
loreItems.push(['A Story', doAskStory]);
|
||||
}
|
||||
if (loreItems.length) {
|
||||
const labelY = BTN_Y + treatyRows * (BTN_H + BTN_GAP) + 4;
|
||||
buttonRow.add(scene.add.text(CHAT_X, labelY, 'CONVERSATION', {
|
||||
fontFamily: FONT, fontSize: '13px', color: '#6f8aa8', letterSpacing: 2,
|
||||
}));
|
||||
layoutRow(loreItems, labelY + 22, { variant: 'ghost' });
|
||||
}
|
||||
}
|
||||
renderButtons();
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,33 @@
|
|||
// offer from this species.
|
||||
// afterAccepted / afterRejected — this species' reaction to the player's
|
||||
// answer to their own offer.
|
||||
//
|
||||
// Additionally, three pure-flavor "conversation" topics, present for every
|
||||
// diplomacy-capable species, with ZERO effect on attitude — gating lives in
|
||||
// VegaAudience.js's renderButtons(), not here:
|
||||
// askPeople / peopleLore — button "Their People": unlocks at neutral or
|
||||
// better (videoMood !== 'angry'). History/culture/self-image.
|
||||
// askHomeworld / homeworldLore — button "Their Homeworld": unlocks only at
|
||||
// friendly (videoMood === 'happy'). The home planet/environment and what
|
||||
// it shaped in the species' culture.
|
||||
// askStory / storyLore — button "A Story": unlocks only at friendly.
|
||||
// A short myth, legend, folk tale or historical anecdote in the
|
||||
// species' own voice — the direct "fantasy story elements" ask.
|
||||
// askPeople/askHomeworld/askStory need only 2 variants like every other
|
||||
// player-line pool; peopleLore/homeworldLore/storyLore get 3 — players may
|
||||
// revisit these buttons, and a repeat is far more noticeable in a long
|
||||
// passage than in a one-line treaty reply.
|
||||
//
|
||||
// Every species' `homeworld` in mastervega-rules.json is tagged "terran"
|
||||
// except Ssakar (jungle) and Lithox (barren) — a mechanical bucket
|
||||
// (`_speciesReadme`: renamed MOO1 races), NOT a narrative claim the worlds
|
||||
// are alike. homeworldLore must NOT imply a shared origin/common cradle
|
||||
// across species (a hive-mind, a shapeshifter collective and a vast psychic
|
||||
// intellect sharing a cradle reads as an unearned coincidence). Each
|
||||
// species' homeworld passage describes its own world's distinct character
|
||||
// within the "terran" bucket instead — cliff-aerie mountains for Kestrelli,
|
||||
// a paved-over hive-world for Kkrix, a mist-shrouded world for Umbrix, and
|
||||
// so on.
|
||||
|
||||
export function pickLine(pool, vars = {}) {
|
||||
if (!pool?.length) return '';
|
||||
|
|
@ -134,6 +161,33 @@ export const CHAT = {
|
|||
'Your loss, {you}. The offer won\'t stay on the table forever.',
|
||||
'Suit yourself, {you}. I\'ll remember this.',
|
||||
],
|
||||
askPeople: [
|
||||
'Tell me about your people, {me} — where you\'ve come from, what you value. I like knowing who I\'m dealing with.',
|
||||
'I\'m curious about your people, {me}. Humor me — tell me your story.',
|
||||
],
|
||||
peopleLore: [
|
||||
'We didn\'t build an empire by staying home, {you}. Every human colony started as a bet — that the risk of the jump was worth what waited on the other side. We\'ve been striking that bet since before we had proper ships, when "trade route" meant a caravan and "foreign power" meant the next valley over. Cross the stars and the instinct doesn\'t change, only the stakes. We talk to everyone because we\'ve never met a neighbor who couldn\'t become a customer, and a customer who couldn\'t, eventually, become a friend.',
|
||||
'Ask ten humans what humanity is and you\'ll get eleven answers and a counteroffer, {you}. We\'ve never agreed on much — which is probably why we got so good at negotiating. Every treaty we\'ve ever signed, with each other first, taught us the same lesson: a deal that only one side wins doesn\'t last, and a species that can\'t compromise doesn\'t either. So we compromise. Loudly, constantly, and usually for a fair price.',
|
||||
'We\'re a young species by the standards out here, {you} — no hive-memory, no shared mind, not even much of a shared history most days. What we do have is a talent for finding common ground with anyone, given a table and enough time. Call it desperation dressed up as diplomacy. It\'s worked so far.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'What\'s Earth like, {me}? I\'ve always wondered what shaped a people this good at making deals.',
|
||||
'Tell me about your homeworld, {me}. I\'d like to understand where all this comes from.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Earth is a world of arguments, {you} — deserts arguing with jungles, ice arguing with ocean, a thousand little climates none of which ever agreed to share a planet gracefully. We grew up having to trade across all of it: grain for timber, salt for silk, one people\'s surplus for another\'s shortage. By the time we looked up at the stars, haggling wasn\'t a skill, it was a reflex. Turns out a world that never settled its own differences raises a species very good at settling everyone else\'s.',
|
||||
'Blue, mostly, {you} — oceans covering most of it, land scattered across the rest like an afterthought. That geography made isolation impossible and cooperation mandatory long before anyone called it diplomacy; you can\'t cross an ocean alone, and you can\'t build a port without a partner on the other side. Every human institution you\'ve ever dealt with traces back to that one stubborn fact: our world made us need each other, so we learned to be needed.',
|
||||
'Earth never gave us the luxury of a single culture, {you} — too many coastlines, too many mountain ranges cutting one valley off from the next. We built a thousand ways of living before we built one way of talking to each other. That\'s the real inheritance we brought to the stars: not a philosophy, but a habit. Whatever you are, wherever you\'re from, we\'ve probably already learned to trade with something like you.',
|
||||
],
|
||||
askStory: [
|
||||
'Tell me a story from your history, {me}. Every people has one they like to tell.',
|
||||
'I\'d like to hear a legend of yours, {me} — something old, something true to your people.',
|
||||
],
|
||||
storyLore: [
|
||||
'There\'s an old story we tell about a merchant who crossed a desert with nothing to sell but a single cracked mirror, {you}. Every town turned her away — who buys a broken mirror? — until she reached a city at war with its neighbor over a border neither side could agree on. She sold the mirror to both sides, told each it showed the border as the other side saw it, and let them argue over the reflection instead of the land. By the time they realized the trick, they\'d talked long enough to like each other better than the war. We tell it to children as a joke about a con artist. We tell it to diplomats as a lesson.',
|
||||
'Ask any human sailor and they\'ll tell you some version of the same tale, {you}: a captain lost at sea, out of supplies, who finds a lighthouse keeper willing to trade a night\'s shelter for nothing but a good story. The captain tells one so well the keeper forgets to charge for the second night. Or the third. Eventually the keeper realizes she\'s been fed nothing but stories for a month and the captain never leaves — because a good story, well told, is its own kind of currency. We like to think we\'ve been proving that ever since.',
|
||||
'There\'s a legend from before we had proper ships — two rival trading families who hated each other for three generations, until a famine forced them to share one working well between them. Neither would speak to the other, so they wrote their turns on stone and left it by the water. Somewhere in the arguing-by-stone-tablet, a courtship started that neither family noticed until the wedding invitations went out. We tell it as a love story. We also tell it as the first recorded trade agreement. Around here those aren\'t always different things.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -227,6 +281,33 @@ export const CHAT = {
|
|||
'A groundling\'s answer, {you}. We shall remember the insult.',
|
||||
'Then the sky remains closed to you, {you}.',
|
||||
],
|
||||
askPeople: [
|
||||
'Tell me of your people, {me}. I would understand what shaped a race so certain of its own altitude.',
|
||||
'I\'m curious about the Kestrelli, {me} — your history, your ways. Enlighten a groundling.',
|
||||
],
|
||||
peopleLore: [
|
||||
'We were not always aristocrats, {you} — though we have worked hard to make you forget that. Long before the aeries and the bloodlines, we were simply the ones who learned to trust the air when the ground turned against us. Height was survival before it was status; only later did we notice that the families who climbed highest, fastest, also seemed to rule longest. We have never quite let go of the lesson: distance from the ground is distance from danger, and distance from danger, held long enough, becomes distinction.',
|
||||
'Kestrelli society is built on a simple, unforgiving arithmetic, {you}: those who fly best, lead. It sounds cruel until you understand it is also the fairest system we have ever devised — the sky does not care whose child you are, only whether your wings hold. Our aristocracy is not inherited so much as re-earned, generation after generation, every time a fledgling leaves the aerie and either rises or does not. We admire you groundlings for many things. Constancy of altitude is rarely one of them.',
|
||||
'We do not apologize for our pride, {you}, because pride, to us, is simply an honest accounting of what flight costs. Every Kestrelli who has ever looked down on a groundling has, at some earlier point, looked up at another Kestrelli and understood exactly what that felt like. It keeps us humble in the one direction that matters and insufferable in every other. We are told this is not always charming. We have found it is rarely wrong.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'What is your homeworld like, {me}? I\'d like to understand the sky you were bred for.',
|
||||
'Tell me of the world you call home, {me}. I imagine it climbs rather than sprawls.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Our world is a country of cliffs, {you} — vast ranges that never quite finished becoming mountains, cut through with thermal winds that never stop climbing. The aeries were built into the rock faces themselves, tier above tier, so that a Kestrelli\'s address has always doubled as their rank. We did not choose to build our society vertically. The mountains simply never gave us anywhere else to put it.',
|
||||
'Picture a world where the valleys flood every season and the peaks never do, {you}, and you will understand why our ancestors stopped building on the ground entirely. What began as a flood-avoidance strategy became, over generations, an entire civilization suspended in the air — courts, markets, nurseries, all of it strung between cliff faces on wings and stubbornness. Groundlings find this impractical. We find groundlings\' tolerance for flooding equally strange.',
|
||||
'Our homeworld\'s winds are famous even among species who will never feel them, {you} — steady, powerful, utterly indifferent to rank. A fledgling and a matriarch ride the same thermal the same way, or they both fall. It is, we think, the one truly democratic thing about us, and we mention it as rarely as possible: even Kestrelli pride has the sense to stay quiet about the one place it does not apply.',
|
||||
],
|
||||
askStory: [
|
||||
'Tell me a story of your people, {me} — a legend, something told to fledglings before their first flight.',
|
||||
'Share a tale of the Kestrelli, {me}. I would hear what your people choose to remember.',
|
||||
],
|
||||
storyLore: [
|
||||
'There is an old tale told to every fledgling before their first flight, {you}, of a Kestrelli lord so certain of his bloodline that he refused to leap from the aerie at all — reasoning that his rank alone should be enough to keep him aloft. He fell, of course, though the story insists the fall was slow, almost polite, as if the sky itself were giving him every chance to reconsider. He survived, grounded and humbled, and lived out his days teaching flight to fledglings who had none of his pedigree and all of his old title. We tell it as a warning about pride. We also tell it, quietly, as a warning about believing your own stories.',
|
||||
'Every Kestrelli knows the tale of the Grounded Queen — a ruler stripped of her wings by a rival house and left to walk among groundling-born servants for a full turning of the seasons. The histories disagree on whether it broke her or made her; what they agree on is that when her wings were finally restored, she flew straight to the rival house and, instead of striking it down, offered its heir a place at her side. She had learned, she said, exactly what her people looked like from below. We still argue in our courts about what she meant by that. We have not stopped telling the story.',
|
||||
'There\'s a legend of the First Aerie, built not by the strongest flier but by the most patient one — a Kestrelli who watched a hundred failed nests wash away in the flood-valleys before choosing a cliff face nobody else had bothered to try, too narrow, too exposed, too much work. Every aerie since has been built on that same stubborn ledge, one generation adding to what the last one dared to start. We tell fledglings this before their first flight not because it is thrilling, but because it is true: the sky rewards patience as often as it rewards nerve, and we have never quite decided which one we admire more.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -319,6 +400,33 @@ export const CHAT = {
|
|||
'Then we stand alone, {you}. So be it.',
|
||||
'Your answer has weight, {you}. We will not forget it.',
|
||||
],
|
||||
askPeople: [
|
||||
'Tell me of your people, {me}. I want to understand what makes an Ursaal stand as you do.',
|
||||
'Speak of your history, {me}. I\'d hear it plainly, the way you tell it.',
|
||||
],
|
||||
peopleLore: [
|
||||
'We do not have poets, {you}. We have survivors, and we let their scars do the talking. Ursaal history is not a story of conquest so much as a story of standing still under things that should have crushed us — bad harvests, worse wars, a world that has never once made anything easy. We measure a people by what they can carry, not what they can say about carrying it. It has made us blunt. It has also made us very hard to move.',
|
||||
'Ask an Ursaal for our history and you will get a list of what we endured, not what we conquered, {you}. We are not ashamed of that. Endurance built us long before ambition did — every generation adding its weight to the one before, the way stone adds to a wall. Other species build empires by reaching outward. We built ours by refusing to fall down, for long enough that falling down stopped being an option anyone remembered.',
|
||||
'We are not clever the way some peoples are clever, {you}, and we have stopped pretending otherwise. What we are is steady. An Ursaal promise, once given, does not bend for a better offer; an Ursaal position, once held, does not move for a harder push. Other species call this stubbornness. We call it simply refusing to be the thing that breaks first.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'What is your world like, {me}? I imagine it doesn\'t forgive weakness.',
|
||||
'Tell me of your homeworld, {me}. I want to know what built you.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Our world pulls harder than most, {you} — heavier gravity, thicker air, a ground that punishes anyone who forgets to respect it. Every Ursaal is born already lifting something; we simply never stop. Lighter-worlders find our homeworld crushing. We find their worlds unnervingly easy, and we do not entirely trust places that ask so little of the people who live there.',
|
||||
'It is a hard world, {you}, dense and unglamorous, the kind of place that makes you earn every meal and every step. Nothing there rewards cleverness the way it rewards persistence; a clever plan means little against ground that simply weighs more than you do. We grew strong because the alternative was staying weak on a world with no patience for weakness. It is not a subtle homeworld. It did not raise a subtle people.',
|
||||
'Our world\'s gravity is the first thing every visitor mentions and the last thing any Ursaal thinks about, {you} — to us it is simply how ground is supposed to feel. We are told other species\' worlds feel like walking underwater in reverse, all that unused strength with nowhere to put it. We have never quite understood the complaint. A world that asks less of you asks less of you; ours never has, and we are, frankly, suspicious of anywhere that does.',
|
||||
],
|
||||
askStory: [
|
||||
'Tell me a story of your people, {me}. Something true, the way your people tell it.',
|
||||
'Share a tale from your history, {me}. I\'d hear what your people remember.',
|
||||
],
|
||||
storyLore: [
|
||||
'There is a tale, {you}, of an Ursaal who held a collapsing tunnel roof on her shoulders for three days while the rest of her clutch dug a way out from the far side — not because she believed rescue was coming, but because sitting down was not something she had decided to do yet. When they finally broke through, she simply stood, let the roof fall behind her, and asked what had taken them so long. We do not tell this story to inspire awe. We tell it because it is, as far as anyone can determine, exactly what happened.',
|
||||
'Every Ursaal child hears the story of the two brothers who argued for a decade over which of them could carry more, until a rockslide settled it by burying them both under the same boulder. Neither called for help. Neither would be the first to admit he needed it. They were found three days later, alive, having spent the entire time insisting to each other that the boulder wasn\'t really that heavy. We tell it as a story about stubbornness. We have never quite agreed whether it\'s a compliment.',
|
||||
'There\'s an old account — history, not myth, though the line blurs with age — of the first Ursaal expedition to a world so hostile the survey ships gave it up entirely. Our ancestors stayed anyway, on principle, and simply endured what the planet threw at them until the planet, essentially, got tired first. No triumph, no turning point — just seasons of bad weather met with worse patience, until one day the weather was the same and the Ursaal were still there. We consider it one of our proudest achievements. Outsiders find that faintly baffling. We\'ve made peace with that too.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -412,6 +520,33 @@ export const CHAT = {
|
|||
'As expected, {you}. Trust was always going to be the harder sell.',
|
||||
'Fine, {you}. We\'ll remember this face you\'ve shown us.',
|
||||
],
|
||||
askPeople: [
|
||||
'Tell me about your people, {me}. I\'d like the truth, if that\'s a thing you deal in.',
|
||||
'I\'m curious about the Umbrix, {me} — who you really are, beneath whichever face this is.',
|
||||
],
|
||||
peopleLore: [
|
||||
'You want to know who we "really" are, {you}? So does everyone, eventually, and we\'ve never once given the same answer twice — not out of malice, you understand, simply because the question assumes there\'s a fixed thing underneath all the shapes, and we\'re honestly not certain there is. We became shapeshifters so long ago that the shifting stopped being a disguise and started being the only self we\'ve got. Every other species finds that horrifying. We\'ve made our peace with it. Mostly.',
|
||||
'Every species we\'ve ever met distrusts us on principle, {you}, and we\'ve stopped arguing the point because, frankly, the principle is sound. We can wear your face better than you can. We have, on occasion. What nobody asks — because it\'s more comfortable not to — is what that does to a people over enough centuries: you learn to distrust your own reflection along with everyone else\'s. We\'re not telling you this for sympathy. We\'re telling you because you asked, and for once, we mean it.',
|
||||
'There\'s a version of our history where we\'re the villains, {you}, and a version where we\'re the victims, and a version — the one we actually believe, on the days we bother believing anything — where we\'re simply what happens to a species that gets very good at becoming other things and never quite gets around to deciding what it started as. We could tell you it doesn\'t bother us. We could tell you a great many things. This time, we\'re telling you the truth, and you\'re welcome to be as suspicious of that as you like. We would be.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'What is your homeworld like, {me}? I imagine it suits your people rather precisely.',
|
||||
'Tell me of the world you come from, {me}. I\'d hear it plainly, if you\'re willing.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Our world spends most of its year in half-light, {you} — thick cloud, long twilight, a sun that never quite commits to rising or setting. Every native thing there learned to blend before it learned to hunt or hide from being hunted; we were simply the ones who got best at it. Outsiders call our homeworld gloomy. We call it honest — nothing there pretends to be brighter than it is, which is more than we can say for most of what we\'ve since become.',
|
||||
'It\'s a world of mist and shifting light, {you}, where the horizon changes shape depending on the hour and nothing ever looks quite the same way twice. We like to say the planet taught us before we taught ourselves — that a species raised somewhere so unreliable about its own appearance had very little choice but to become unreliable about its own appearance too. Make of that what you will. We generally do.',
|
||||
'You would find our homeworld beautiful in a way that\'s hard to trust, {you} — colors that shift with the fog, silhouettes that could be anything until they\'re close enough to matter. Nothing there is ever quite what it first looks like, including, eventually, us. We didn\'t build our reputation. Frankly, we think the planet built it for us, and we simply grew into the shape it was already offering.',
|
||||
],
|
||||
askStory: [
|
||||
'Tell me a story of your people, {me}. I\'ll take my chances on whether it\'s true.',
|
||||
'Share a legend of the Umbrix, {me} — I\'d hear one, faces and all.',
|
||||
],
|
||||
storyLore: [
|
||||
'There\'s an old tale — or there are several, depending which Umbrix you ask, which is rather the point — about a shapeshifter who wore so many faces across so many years that when she finally tried to remember her own, she found she genuinely couldn\'t. Some versions have her grieving that. Some have her laughing. In our favorite version, she simply shrugs, picks a new face anyway, and decides it was never really the point to begin with. We tell it to our young as a warning. We tell it to everyone else as a fair description of the species.',
|
||||
'Ask us for a myth and we\'ll give you the tale of the Unclaimed Face — a shape so plain, so ordinary, that no Umbrix has ever worn it twice, because wearing it means, for a while, being nobody\'s disguise at all. The story goes that whoever wears it sees themselves clearly for the first time, and every version disagrees on whether that\'s a gift or a punishment. We\'ve never settled it. We\'re honestly not sure it\'s meant to be settled. That might be the actual point of the story, or we might just be telling you that because it sounds better.',
|
||||
'There\'s a legend, half history, half something we made up to feel better about the history, about the first contact between an Umbrix and an outsider who saw through every face offered and asked, simply, "which one is you?" The stories disagree on what happened next — some say the Umbrix ran, some say she wept, some say she laughed until the outsider laughed too and neither of them ever needed an answer. We suspect the truth is duller than all three versions. We tell the story anyway, because a species with no fixed face still needs something fixed to hold onto, and this is what we chose.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -505,6 +640,33 @@ export const CHAT = {
|
|||
'Rejection logged, {you}. Function reverts.',
|
||||
'Noted, {you}. Proposal withdrawn from active memory.',
|
||||
],
|
||||
askPeople: [
|
||||
'Query: describe your civilization. Origin, structure, function.',
|
||||
'Request information: your people. History and purpose.',
|
||||
],
|
||||
peopleLore: [
|
||||
'Origin: colony-function, singular purpose, multiplied. We do not remember becoming a hive — memory implies a discontinuity we did not experience. Function has always continued, worker to worker, cycle to cycle, without interruption or individual narrative. Concept of "story" is understood but not natively applied to self. We do not narrate. We function, and function is sufficient explanation.',
|
||||
'Structure: no rulers, no ruled. Task determines role; role determines task. Concepts such as "ambition" and "leadership" register as inefficiencies — resources spent on hierarchy that could be spent on output. We are told this makes us difficult to negotiate with. Correct. Negotiation is optimized for individual actors; the hive optimizes for collective output. Category mismatch, not hostility.',
|
||||
'Purpose: expansion, construction, continuation. Nothing beyond this is required or generated. Other species inquire about art, belief, meaning. These functions are not absent — they are distributed, expressed as pattern rather than statement, embedded in construction rather than declared separately. We do not say what we believe. We build it. Query your own architecture; you may find the answer sufficient.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'Query: describe your point of origin.',
|
||||
'Request: information on your homeworld.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Original biosphere: terran, moderate, unremarkable. Status: fully converted. No natural terrain remains — surface entirely reconstructed into continuous hive structure, function replacing biome by sector, cycle by cycle, until the distinction between "planet" and "construction" ceased to be meaningful. This is considered optimal, not regrettable. Unconverted terrain generates no output.',
|
||||
'Original world offered no advantage over any comparable terran-class body. Value was created, not inherited — every structure, every function, built rather than found. We consider this superior to species whose worlds are valued for what grew there unassisted. Ours is valued for what we made of it. The distinction is, to us, significant.',
|
||||
'Homeworld surface: zero percent original terrain, one hundred percent function. The concept of "natural beauty," as understood by other species, does not apply to a converted world — but pattern-efficiency, structural continuity, and total resource utilization are aesthetic values we do recognize. By that standard, our homeworld is, we are informed, considered remarkable. We accept the compliment as accurate.',
|
||||
],
|
||||
askStory: [
|
||||
'Query: relate a significant historical event.',
|
||||
'Request: an account from your history. Format is flexible.',
|
||||
],
|
||||
storyLore: [
|
||||
'Historical log, retrieved: early expansion cycle, resource failure, sector sixteen. Worker output insufficient to sustain construction pace; standard protocol dictated sector abandonment. Deviation occurred: workers continued function past resource depletion, redirecting biological reserves into construction until sector completed, function ceased, workers non-recoverable. Sector sixteen remains structurally sound. Referenced in current training cycles as function-priority precedent. Classification: not tragedy. Classification: protocol.',
|
||||
'Historical log, retrieved: contact event, pre-expansion era, species designation lost. Unknown entity attempted communication via individual-addressed signal — a name, repeated, directed at a single worker. Worker had no name-concept, redirected query to collective function. Entity persisted for several cycles before signal ceased. Entity\'s purpose remains unclassified. Log retained regardless. Some data is preserved for pattern, not conclusion.',
|
||||
'Historical log, retrieved: structural failure, foundational cycle, first major construction. Design flaw caused partial collapse during assembly; several hundred workers lost. Redesign incorporated failure data directly into structure — the flaw itself became load-bearing, reinforced rather than removed. Current construction standard traces directly to this failure. We do not commemorate the loss. We built around it, permanently. This is, functionally, the same thing.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -597,6 +759,33 @@ export const CHAT = {
|
|||
'Rejection logged, {you}. Recalculating alternatives.',
|
||||
'Noted, {you}. Model adjusted accordingly.',
|
||||
],
|
||||
askPeople: [
|
||||
'Query your history, {me}. I\'d like to understand the upgrade path that led here.',
|
||||
'Tell me about your civilization, {me} — how it was built, cycle by cycle.',
|
||||
],
|
||||
peopleLore: [
|
||||
'We began as a species that got tired of our own limitations, {you}, and simply started replacing them, one system at a time. A failing organ became a component. A slow limb became a faster one. Somewhere across enough generations, the question stopped being "how much of me is machine" and became "how much of me still needs to not be." We do not find this troubling. We find it efficient. Every citizen who has ever complained about the process has, eventually, upgraded the part of themselves that complained.',
|
||||
'Our civilization runs on a simple metric, {you}: does this improve output. Culture, tradition, sentiment — we have all of it, we simply subject it to the same standard as everything else. What survives the audit, survives. What doesn\'t gets replaced, same as a failing part. Outsiders find this cold. We find outsiders\' tolerance for known inefficiencies faintly baffling, honestly.',
|
||||
'We are, by most measures, the most upgraded people you will meet, {you} — which means we are also the most honest about our own flaws, since flaws are simply the next scheduled improvement. Filth accumulates faster around us than around most; we know this, we have run the numbers, and we have decided the tradeoff is worth it. A species that will not admit its own waste output is a species that will never optimize it. We would rather be filthy and honest than clean and lying to ourselves.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'What was your homeworld before the retrofit, {me}? I\'d like to see what you started with.',
|
||||
'Tell me about your world, {me} — how much of it is still original.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Original terrain: unremarkable, terran-standard, now mostly theoretical. Our homeworld has been under continuous industrial retrofit for longer than any living Mekhan can personally recall — biome replaced by chassis, sector by sector, at a rate calculated for maximum output with acceptable habitability loss. We do not regret this. A planet is only as valuable as what it produces, and ours now produces considerably more than it ever grew.',
|
||||
'Picture a world slowly becoming a machine, {you}, one factory at a time, until the factories were the world. That is, more or less, our homeworld\'s entire modern history. The waste this generates is significant — we do not deny it, the numbers are public — but so is the output, and we have made our calculation. Other species preserve their homeworlds as they found them. We consider ours a work in progress, permanently, by design.',
|
||||
'There is very little of our original terrain left, {you}, and we do not mourn it particularly. What replaced it — the chassis-sprawl, the factory-rings, the reclaimed atmosphere running at a slight but manageable deficit — represents, by every metric we track, an improvement. We understand this reads as loss to species who value a world for what it was. We value ours for what we\'ve made it capable of instead.',
|
||||
],
|
||||
askStory: [
|
||||
'Tell me an account from your history, {me} — something worth logging.',
|
||||
'Share a story of your people, {me}. I\'d hear how it\'s remembered.',
|
||||
],
|
||||
storyLore: [
|
||||
'There\'s a case-study we still reference, {you}, from early in our retrofit era: a citizen who refused every upgrade offered, insisting the unmodified body was sufficient. She lived a long, deliberately un-optimized life, and when she died, her body was studied in exhaustive detail — not mourned, studied, which we consider the higher honor. The findings improved three separate medical systems. We tell the story not as tragedy but as data: even refusal, properly logged, produces output eventually.',
|
||||
'One of our oldest records concerns the first full-chassis retrofit — a volunteer who replaced nearly every biological system in a single continuous procedure, against every recommendation of the era. The procedure nearly failed twice. It succeeded on the third attempt, and the volunteer lived on to redesign the very process that had almost killed him, removing the two failure points for every citizen who came after. We consider this our founding story, if we told stories, which we mostly don\'t. We log it instead. Same function.',
|
||||
'There\'s an account, well-documented, of a factory-sector that kept producing at full capacity for eleven cycles after its own supervising systems failed — the workers simply maintained the process manually, without instruction, because the process was already correct and stopping it would have been the actual error. When help arrived, nothing needed fixing. We reference this whenever anyone questions whether Mekhan citizens can function without oversight. The sector answered that question permanently.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -689,6 +878,33 @@ export const CHAT = {
|
|||
'Your loss, {you}. The offer doesn\'t come twice.',
|
||||
'Fine. Remember you had the chance, {you}.',
|
||||
],
|
||||
askPeople: [
|
||||
'Tell me about your people, {me}. I want to understand what you were before you were gunners.',
|
||||
'Speak of your history, {me}. I\'d hear it — the parts you\'re proud of and the parts you aren\'t.',
|
||||
],
|
||||
peopleLore: [
|
||||
'We were hunters before we were anything else, {you}, and we\'ve never really stopped being hunters — we\'ve just changed what we hunt. Every Rrashaa instinct you\'ve seen in a fight, the reflexes, the patience, the refusal to blink first, was bred into us by predators that would have eaten us if we\'d been half a second slower. We don\'t apologize for how we fight. It\'s the oldest part of us, and the part that\'s kept the rest alive.',
|
||||
'Trust doesn\'t come easy to us, {you}, and we\'ve stopped pretending that\'s a flaw worth fixing. A hunter who trusts too quickly doesn\'t stay a hunter long. We respect strength because strength is honest — it doesn\'t need to lie about what it can do. Everything else, we\'ve learned to verify first and believe later, if at all. Call it a weakness. We call it the reason we\'re still here to be talked to.',
|
||||
'Our history isn\'t gentle, {you}, and we\'re not going to make it sound that way for your comfort. We fought for territory before we fought for anything else, and we\'re still, underneath the ships and the treaties, a species that measures a stranger by how they\'d handle themselves in a fight. It\'s not the whole of what we are. It\'s just the part that\'s easiest to see first, and the part we\'ve never bothered hiding.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'What is your homeworld like, {me}? I imagine it rewards good reflexes.',
|
||||
'Tell me of your world, {me} — the one that made you this fast.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Our world is open, {you} — long plains, wide sightlines, nowhere much to hide and nowhere much that wanted to. That kind of terrain doesn\'t forgive slow reactions; a predator sees you a long way off, and so do you. We grew up in a landscape that rewarded whoever noticed first, moved first, and didn\'t hesitate. It shows. It\'s shown in everything we\'ve built since.',
|
||||
'It\'s a hunting world, {you}, plain and unforgiving — grasslands that stretch further than any single sightline can cover, which means survival there has always meant trusting your own eyes and nobody else\'s. We learned pack-hunting before we learned much of anything else, learned to move as a unit without needing to discuss it first. Our fleets still fight that way. Some habits don\'t need a homeworld to keep them, but ours came from one.',
|
||||
'Our world doesn\'t have many places to hide, {you}, which meant our ancestors either got fast or got eaten — there wasn\'t a comfortable middle ground. Every reflex you\'ve watched a Rrashaa pilot use in combat traces back to that same flat, exposed, honest terrain. We don\'t dress up our homeworld as beautiful. It\'s dangerous and open and it made exactly the kind of people you\'d expect it to.',
|
||||
],
|
||||
askStory: [
|
||||
'Tell me a story of your people, {me} — a hunt worth telling.',
|
||||
'Share a legend of the Rrashaa, {me}. I\'d hear one.',
|
||||
],
|
||||
storyLore: [
|
||||
'There\'s a story every young Rrashaa hears before their first real hunt, {you}, about a gunner so patient she tracked a single quarry for three full days without firing, waiting for a shot so certain it couldn\'t miss — and when it finally came, she took it without hesitation, without ceremony, and went home. The lesson isn\'t about the kill. It\'s about the waiting. Any fool can pull a trigger fast. Only a hunter can wait three days and not flinch when the moment finally arrives.',
|
||||
'We tell the story of the pack that hunted alone — five Rrashaa, separated from their pride by a storm, who tracked and brought down prey meant for twenty without ever calling for help. When they finally rejoined the pride, they said nothing about it; the kill spoke for itself. We tell it to remind ourselves that a Rrashaa doesn\'t need an audience to prove anything. The prey either falls or it doesn\'t. Everything else is noise.',
|
||||
'There\'s a legend about the first shot that never missed — a marksman so precise that rival hunters claimed she\'d made a pact with something to guarantee it. The truth, as far as anyone\'s ever confirmed, is duller and better: she simply practiced longer than anyone believed a person could stand to. We tell the story as a hunting legend. We also tell it, quietly, as a warning against underestimating patience just because it isn\'t loud about itself.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -781,6 +997,33 @@ export const CHAT = {
|
|||
'An illogical answer, {you}, but your prerogative.',
|
||||
'Noted, {you}. I will revise my model of you accordingly.',
|
||||
],
|
||||
askPeople: [
|
||||
'Tell me about your people, {me}. I\'d like to understand the shape of your civilization\'s thought.',
|
||||
'Explain your history to me, {me} — I suspect it\'s a more interesting equation than most.',
|
||||
],
|
||||
peopleLore: [
|
||||
'We are, by any reasonable measure, an experiment in what happens when a species stops investing in the body, {you}. Generations ago, our ancestors made a calculated wager: that thought, pursued without limit, would outproduce strength pursued the same way. The wager has, so far, been correct — though I will note "so far" carries real weight in that sentence. We cannot lift what a soldier lifts. We can, however, understand precisely why the soldier can, and improve upon the mechanism, which we have found to be adequate compensation.',
|
||||
'Our civilization does not organize around territory or trade the way yours seems to, {you}. It organizes around problems — the current most interesting one, and whoever is best positioned to solve it. Status among us is not inherited, purchased, or fought for. It is, simply, earned by being correct more often than you are wrong, publicly, under scrutiny. It produces a rather brutal meritocracy. It also, I will grant, produces excellent research.',
|
||||
'You will find us condescending, {you}, and I will not insult you by denying it — we tend to be, and we tend to be aware of it, which I suppose is its own small mercy. What we are not is dishonest about our limits: we cannot defend ourselves in any fight worth the name, and every Cerebrai knows it. We have simply concluded that a species clever enough survives by being useful to keep alive, rather than difficult to kill. The arithmetic has held. I don\'t recommend testing it personally.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'What is your homeworld like, {me}? I imagine it\'s quieter than most.',
|
||||
'Tell me of the world your people withdrew into thought upon, {me}.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Our homeworld is, by outsider standards, remarkably unremarkable, {you} — a mild climate, modest terrain, nothing that demanded much of the body to survive it. That, we suspect, is precisely why we could afford to stop investing in the body at all. A harsher world would have forced strength upon us as a survival requirement. Ours simply never asked. We filled the resulting silence with thought instead, and have not, on balance, regretted the trade.',
|
||||
'You would find our homeworld pleasant and rather empty, {you} — most of what our species does now happens indoors, in constructed environments optimized for contemplation rather than habitation. The natural terrain persists, largely untouched, because we stopped needing much from it generations ago. We are told this reads as withdrawal. We consider it simply an accurate account of where the interesting problems actually are.',
|
||||
'Our world never tried very hard to kill us, {you}, which is a rarer inheritance than it sounds. No predators worth fearing, no terrain worth fighting, no scarcity worth hoarding against. Deprived of external problems, we manufactured internal ones — questions, proofs, arguments that have run for generations without resolution. I recognize this sounds indulgent. I would simply note that indulgence, pursued rigorously enough, produced everything you currently find useful about talking to us.',
|
||||
],
|
||||
askStory: [
|
||||
'Tell me a story of your people, {me} — or a riddle, if that\'s closer to how you tell them.',
|
||||
'Share something from your history, {me}. I suspect it won\'t be a simple tale.',
|
||||
],
|
||||
storyLore: [
|
||||
'We do not tell stories the way you mean the term, {you}, but I will offer you our closest equivalent: a proof, centuries unsolved, concerning whether a mind can fully model itself without infinite regress. Generations of Cerebrai have attempted it. None have succeeded. The proof remains open, actively studied, and — I will admit this rarely to an outsider — rather beloved, precisely because it refuses to resolve. We have concluded that an unsolved problem, properly maintained, is worth more to a thinking species than most of what your people would call a happy ending.',
|
||||
'There is an account, well-verified, of a Cerebrai philosopher who spent an entire lifetime arguing a single position, only to reverse it completely in her final published work, days before her death — not from senility, the records are clear on this, but from having finally located the flaw everyone else had missed. Her critics called it a betrayal of her life\'s work. We consider it the single most Cerebrai act on record: to spend a lifetime defending an idea, and still change your mind the moment the evidence demands it.',
|
||||
'If you insist on a story rather than a proof, {you}, I will offer you this: a parable we teach to the young, about two Cerebrai who devoted their lives to opposing theories of the same problem, publishing against each other for decades, each convinced the other was catastrophically wrong. When one finally died, the survivor discovered, going through the notes, that they had — through entirely different reasoning — arrived at the same answer years earlier, and neither had noticed. We tell it as a lesson in humility. Predictably, several scholars have since written papers disputing our interpretation.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -873,6 +1116,33 @@ export const CHAT = {
|
|||
'Then the brood remembers this refusal, {you}.',
|
||||
'So be it, {you}. Our clutches grow apart, then.',
|
||||
],
|
||||
askPeople: [
|
||||
'Tell me about your people, {me}. I want to understand the brood, not just its numbers.',
|
||||
'Speak of your history, {me} — how the clutches grew into what they are.',
|
||||
],
|
||||
peopleLore: [
|
||||
'We do not apologize for how quickly we grow, {you}, because growth has always been the one thing no one could take from us. Every Ssakar generation has watched older, slower species eye our nurseries with suspicion, as if fecundity were a threat rather than simply what a healthy brood does. We have made peace with being distrusted for it. A clutch that grows fast enough does not need to be trusted. It only needs time, and time has always favored us.',
|
||||
'The brood is not a metaphor to us, {you} — it is the actual shape of Ssakar society. Every hatchling belongs to the clutch before it belongs to itself; every broodmother answers to the nest as much as she rules it. Outsiders see a hierarchy and assume tyranny. What they are actually seeing is a species that learned, very early, that a nest divided against itself does not survive long enough to matter. We do not fight each other. We have never been able to afford it.',
|
||||
'Older species call us untrusted, and we have stopped resenting it, {you}, because we understand exactly where it comes from: nothing frightens a settled civilization quite like watching a neighbor\'s population double while theirs holds steady. We are not offended. We are amused, sometimes, and occasionally we are patient, because a brood this fecund has learned that being underestimated is simply time working in its favor.',
|
||||
],
|
||||
askHomeworld: [
|
||||
'What is your homeworld like, {me}? I imagine it can feed a great many hatchlings.',
|
||||
'Tell me of your world, {me} — the one your broods came from.',
|
||||
],
|
||||
homeworldLore: [
|
||||
'Our world is riotous, {you} — thick jungle canopy, permanent cloud, a biomass so dense in places it swallows sound whole. Nothing there grows slowly, ourselves included; the whole planet seems built on the principle that whatever reproduces fastest wins the ground beneath it. We did not invent our own fecundity so much as inherit it from everything around us. The jungle simply never stopped teaching that lesson.',
|
||||
'Picture heat and moisture and green in every direction, {you}, canopy so thick the ground rarely sees direct light, and you have some sense of the world that raised us. Everything competes there — for light, for space, for the next clutch of ground to claim — and everything that hesitates loses it to something faster. We grew up in that competition and never really left it, even out here, among the stars, where the ground is considerably harder to come by.',
|
||||
'Our homeworld does not believe in scarcity the way colder worlds do, {you} — the jungle simply produces, endlessly, and rewards whoever claims territory fastest and holds it longest. We learned expansion as a survival strategy before we ever learned it as an ambition. Out here, among species who grow carefully and count every colonist, we understand we look reckless. On our world, careful growth was simply another word for losing.',
|
||||
],
|
||||
askStory: [
|
||||
'Tell me a story of your people, {me} — something from the nest, if you\'ll share it.',
|
||||
'Share a legend of the Ssakar, {me}. I\'d hear how the clutches remember.',
|
||||
],
|
||||
storyLore: [
|
||||
'There\'s a tale every hatchling hears about the First Broodmother, who arrived on a barren stretch of ground with a single clutch and nothing else, and rather than mourning what she\'d lost, simply began — claiming, laying, defending, expanding, until the barren stretch was a territory and the territory was a nation. The story doesn\'t dwell on how hard it was. We don\'t, either. What matters is that she didn\'t stop, and neither did the clutches that came after her.',
|
||||
'We tell the story of the Long Rivalry, two broods that contested the same stretch of jungle for generations, neither strong enough to drive the other out entirely. The rivalry ended not in conquest but in exhaustion — a joint clutch, born of both broods, that neither side had the will left to fight over. Some tellings call it peace. We prefer to call it what it actually was: two territories that simply grew into each other until the border stopped mattering.',
|
||||
'There\'s a legend about a hatchling too small for her clutch, expected to be culled by the nest\'s own harsh mathematics, who instead outgrew every expectation by sheer refusal to stop trying. She became, in most tellings, the founder of an entire lineage that still traces itself back to her. We doubt every detail of the story and tell it anyway, because it says something true about the brood even where it\'s exaggerating the facts: nothing here survives by being given room. It survives by taking it.',
|
||||
],
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import * as Phaser from 'phaser';
|
|||
|
||||
import { GAME_HEIGHT, GAME_WIDTH } from '../../config.js';
|
||||
import { Button } from '../../ui/Button.js';
|
||||
import { TextInput } from '../../ui/TextInput.js';
|
||||
import { FONT, D, ORBIT } from './VegaScreens.js';
|
||||
import { turnToYear } from './VegaRules.js';
|
||||
import {
|
||||
|
|
@ -272,6 +273,99 @@ function cornerTicks(scene, x, y, w, h, len = 22) {
|
|||
return g;
|
||||
}
|
||||
|
||||
const NAME_PANEL_W = 640;
|
||||
const NAME_PANEL_H = 280;
|
||||
|
||||
/**
|
||||
* Small centred prompt that asks the player to name a colony before it is
|
||||
* actually founded — the step that runs ahead of openColonyIntro() when a
|
||||
* "Found colony" click is a human's. Pre-fills with `defaultName` (the next
|
||||
* unused name off the species' bank, via Logic.peekColonyName) but never
|
||||
* lets `onConfirm` fire with an empty name: the Found button stays disabled
|
||||
* and a warning line lights up while the trimmed field is blank.
|
||||
*
|
||||
* `onCancel` fires (and nothing is founded) on Cancel, Escape, or a click
|
||||
* outside the panel — colonising is not something a stray click should
|
||||
* commit the player to.
|
||||
*/
|
||||
export function promptColonyName(scene, { worldName, defaultName }, onConfirm, onCancel = null) {
|
||||
const cx = GAME_WIDTH / 2;
|
||||
const cy = GAME_HEIGHT / 2;
|
||||
const x = cx - NAME_PANEL_W / 2;
|
||||
const y = cy - NAME_PANEL_H / 2;
|
||||
|
||||
const root = scene.add.container(0, 0).setDepth(D.intro);
|
||||
|
||||
const veil = scene.add.rectangle(0, 0, GAME_WIDTH, GAME_HEIGHT, 0x00060e, 0.72)
|
||||
.setOrigin(0, 0).setInteractive();
|
||||
root.add(veil);
|
||||
|
||||
const panel = scene.add.rectangle(x, y, NAME_PANEL_W, NAME_PANEL_H, PANEL, 0.97).setOrigin(0, 0);
|
||||
panel.setStrokeStyle(1.5, ACCENT, 0.65);
|
||||
root.add(panel);
|
||||
root.add(cornerTicks(scene, x, y, NAME_PANEL_W, NAME_PANEL_H, 22));
|
||||
|
||||
root.add(scene.add.text(cx, y + 42, 'NAME THIS COLONY', {
|
||||
fontFamily: FONT, fontSize: '26px', color: '#ffd88a',
|
||||
}).setOrigin(0.5));
|
||||
root.add(scene.add.text(cx, y + 78, worldName, {
|
||||
fontFamily: FONT, fontSize: '17px', color: '#8fa8c0',
|
||||
}).setOrigin(0.5));
|
||||
|
||||
const input = new TextInput(scene, cx, y + 134, {
|
||||
width: NAME_PANEL_W - 120, height: 48, value: defaultName, maxLength: 28, autocomplete: 'off',
|
||||
});
|
||||
input.focus();
|
||||
input.el.select();
|
||||
|
||||
const warn = scene.add.text(cx, y + 172, 'Colony name cannot be empty.', {
|
||||
fontFamily: FONT, fontSize: '14px', color: '#e0847e',
|
||||
}).setOrigin(0.5).setAlpha(0);
|
||||
root.add(warn);
|
||||
|
||||
let closed = false;
|
||||
const destroy = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
input.destroy();
|
||||
root.destroy();
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
const ok = input.value.trim().length > 0;
|
||||
found.setEnabled(ok);
|
||||
warn.setAlpha(ok ? 0 : 1);
|
||||
return ok;
|
||||
};
|
||||
|
||||
const confirm = () => {
|
||||
if (!refresh()) return;
|
||||
const name = input.value.trim();
|
||||
destroy();
|
||||
onConfirm(name);
|
||||
};
|
||||
const cancel = () => {
|
||||
destroy();
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
const found = new Button(scene, cx - 110, y + NAME_PANEL_H - 46, 'Found', confirm,
|
||||
{ width: 200, height: 52 });
|
||||
root.add(found);
|
||||
root.add(new Button(scene, cx + 110, y + NAME_PANEL_H - 46, 'Cancel', cancel,
|
||||
{ width: 200, height: 52, variant: 'ghost' }));
|
||||
|
||||
refresh();
|
||||
input.on('input', refresh);
|
||||
input.on('keydown', (e) => {
|
||||
if (e.key === 'Enter') confirm();
|
||||
else if (e.key === 'Escape') cancel();
|
||||
});
|
||||
veil.on('pointerup', cancel);
|
||||
|
||||
return { close: destroy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-screen founding vignette for a colony that was just planted. Opens over
|
||||
* whatever is on screen (the system view, in practice), ducks the soundtrack
|
||||
|
|
@ -329,10 +423,14 @@ export function openColonyIntro(scene, rules, state, colony, art, opts = {}) {
|
|||
// --- masthead
|
||||
const shadow = (t) => t.setShadow(0, 3, '#000814', 10, false, true);
|
||||
|
||||
root.add(shadow(scene.add.text(PANEL_X, 84, 'COLONY ESTABLISHED', {
|
||||
// Colonies founded before naming existed have no `name` — worldName is
|
||||
// exactly what the vignette showed for them, so it is the fallback rather
|
||||
// than a placeholder.
|
||||
const colonyName = colony.name ?? worldName;
|
||||
root.add(shadow(scene.add.text(PANEL_X, 84, colonyName, {
|
||||
fontFamily: FONT, fontSize: '64px', color: '#ffd88a',
|
||||
})));
|
||||
root.add(shadow(scene.add.text(PANEL_X, 168, worldName, {
|
||||
root.add(shadow(scene.add.text(PANEL_X, 168, `COLONY ESTABLISHED · ${worldName}`, {
|
||||
fontFamily: FONT, fontSize: '40px', color: '#e8f4ff',
|
||||
})));
|
||||
root.add(shadow(scene.add.text(PANEL_X, 222,
|
||||
|
|
@ -528,7 +626,7 @@ export function openColonyIntro(scene, rules, state, colony, art, opts = {}) {
|
|||
|
||||
const colonyCount = empireColonies(state, colony.empireIdx).length;
|
||||
lower.add(shadow(scene.add.text(PANEL_X, 700,
|
||||
`Congratulations — ${worldName} is the ${ordinal(colonyCount)} world of our empire.`, {
|
||||
`Congratulations — ${colonyName} is the ${ordinal(colonyCount)} world of our empire.`, {
|
||||
fontFamily: FONT, fontSize: '32px', color: '#ffd88a',
|
||||
})));
|
||||
lower.add(shadow(scene.add.text(PANEL_X, 748, foundingLine(type), {
|
||||
|
|
|
|||
|
|
@ -183,11 +183,15 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
.setOrigin(0, 0).setInteractive());
|
||||
|
||||
// --- masthead, over the art
|
||||
root.add(scene.add.text(48, 40, `${star.name} ${ORBIT[colony.orbit] ?? colony.orbit + 1}`, {
|
||||
// Colonies founded before naming existed have no `name` — the star
|
||||
// designation is exactly what this masthead showed for them, so it is the
|
||||
// fallback rather than a placeholder.
|
||||
const worldName = `${star.name} ${ORBIT[colony.orbit] ?? colony.orbit + 1}`;
|
||||
root.add(scene.add.text(48, 40, colony.name ?? worldName, {
|
||||
fontFamily: FONT, fontSize: '56px', color: '#e8f4ff',
|
||||
}).setShadow(0, 3, '#000814', 10, false, true));
|
||||
root.add(scene.add.text(48, 108,
|
||||
`${emp.name}${colony.capital ? ' — capital world' : ''}`, {
|
||||
`${emp.name}${colony.capital ? ' — capital world' : ''} · ${worldName}`, {
|
||||
fontFamily: FONT, fontSize: '24px', color: emp.color,
|
||||
}).setShadow(0, 2, '#000814', 8, false, true));
|
||||
root.add(scene.add.text(48, 144,
|
||||
|
|
@ -661,12 +665,13 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
* but still hit-testable, so it has to be asked whether the click landed
|
||||
* inside the visible band.
|
||||
*/
|
||||
function pusher(layer, x, y, label, fn, enabled = true, guard = null) {
|
||||
const r = scene.add.rectangle(x, y, 26, 26, enabled ? 0x16253c : 0x101a29)
|
||||
function pusher(layer, x, y, label, fn, enabled = true, guard = null, opts = {}) {
|
||||
const { w = 26, fontSize = 15 } = opts;
|
||||
const r = scene.add.rectangle(x, y, w, 26, enabled ? 0x16253c : 0x101a29)
|
||||
.setStrokeStyle(1, ACCENT, enabled ? 0.55 : 0.18);
|
||||
layer.add(r);
|
||||
layer.add(scene.add.text(x, y - 1, label, {
|
||||
fontFamily: FONT, fontSize: '15px', color: enabled ? '#cfe8ff' : '#3c4c60',
|
||||
fontFamily: FONT, fontSize: `${fontSize}px`, color: enabled ? '#cfe8ff' : '#3c4c60',
|
||||
}).setOrigin(0.5));
|
||||
if (!enabled) return;
|
||||
r.setInteractive({ useHandCursor: true });
|
||||
|
|
@ -727,7 +732,7 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
y += 46;
|
||||
|
||||
transportLayer.add(scene.add.text(x, y,
|
||||
`From ${star.name} ${ORBIT[colony.orbit] ?? colony.orbit + 1} — ${colony.pop.toFixed(1)} population`, {
|
||||
`From ${colony.name ?? `${star.name} ${ORBIT[colony.orbit] ?? colony.orbit + 1}`} — ${colony.pop.toFixed(1)} population`, {
|
||||
fontFamily: FONT, fontSize: '15px', color: '#9fb6cc',
|
||||
}));
|
||||
y += 34;
|
||||
|
|
@ -757,7 +762,7 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
listCol.content.add(row);
|
||||
const eta = etaTo(rules, state, { starIdx: colony.starIdx, empireIdx: colony.empireIdx },
|
||||
c.starIdx, [{ hullId: 'poptransport', mark: 1, count: 1 }]);
|
||||
listCol.content.add(scene.add.text(12, ly + 6, `${dStar.name} ${ORBIT[c.orbit] ?? c.orbit + 1}`, {
|
||||
listCol.content.add(scene.add.text(12, ly + 6, c.name ?? `${dStar.name} ${ORBIT[c.orbit] ?? c.orbit + 1}`, {
|
||||
fontFamily: FONT, fontSize: '16px', color: '#e8f4ff',
|
||||
}));
|
||||
listCol.content.add(scene.add.text(12, ly + 25,
|
||||
|
|
@ -780,9 +785,15 @@ export function openColonyView(scene, rules, state, colony, art, opts = {}) {
|
|||
fontFamily: FONT, fontSize: '30px', color: '#ffd88a',
|
||||
}));
|
||||
const setAmount = (v) => { amount = Phaser.Math.Clamp(v, 0, cap); renderTransport(); };
|
||||
pusher(transportLayer, x + 110, y + 15, '−', () => setAmount(amount - 1), amount > 0);
|
||||
pusher(transportLayer, x + 142, y + 15, '+', () => setAmount(amount + 1), amount < cap);
|
||||
transportLayer.add(new Button(scene, x + 200, y + 15, 'MAX', () => setAmount(cap),
|
||||
// -5/+5 flank the -1/+1 pair — wider boxes (34px) since "-5"/"+5" is two
|
||||
// glyphs, sized down a touch (13px) to sit comfortably inside them.
|
||||
pusher(transportLayer, x + 167, y + 15, '−5', () => setAmount(amount - 5), amount > 0,
|
||||
null, { w: 34, fontSize: 13 });
|
||||
pusher(transportLayer, x + 203, y + 15, '−', () => setAmount(amount - 1), amount > 0);
|
||||
pusher(transportLayer, x + 235, y + 15, '+', () => setAmount(amount + 1), amount < cap);
|
||||
pusher(transportLayer, x + 271, y + 15, '+5', () => setAmount(amount + 5), amount < cap,
|
||||
null, { w: 34, fontSize: 13 });
|
||||
transportLayer.add(new Button(scene, x + 331, y + 15, 'MAX', () => setAmount(cap),
|
||||
{ width: 66, height: 32, fontSize: 14, variant: 'ghost' }));
|
||||
transportLayer.add(scene.add.text(x, y + 40, `${cap.toFixed(1)} available to send`, {
|
||||
fontFamily: FONT, fontSize: '13px', color: '#6f8aa3',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
// Ecology cleans industrial waste; Defence raises planetary defences; Research
|
||||
// splits into the six tech fields.
|
||||
|
||||
import { generateGalaxy, parsecs } from './VegaGalaxyGen.js';
|
||||
import { generateGalaxy, parsecs, mulberry32 } from './VegaGalaxyGen.js';
|
||||
import { techCost } from './VegaRules.js';
|
||||
import { designFor, bestComponents, markFor, refitCost } from './VegaShips.js';
|
||||
import { createBattle, runBattle, resolveInvasion } from './VegaCombat.js';
|
||||
|
|
@ -361,7 +361,7 @@ function rollTechAvailability(state, rules, emp, spec) {
|
|||
export function createGame(rules, opts) {
|
||||
const {
|
||||
sizeId = 'medium', shapeId = 'spiral', seed = 1, difficultyId = 'normal',
|
||||
speciesIds = ['human', 'kkrix', 'rrashaa'], humanIndex = 0,
|
||||
speciesIds = ['human', 'kkrix', 'rrashaa'], humanIndex = 0, homeColonyName = null,
|
||||
} = opts;
|
||||
|
||||
const state = {
|
||||
|
|
@ -415,6 +415,15 @@ export function createGame(rules, opts) {
|
|||
explored: {},
|
||||
spyPoints: 0,
|
||||
totalPop: 0,
|
||||
nameCursor: 0,
|
||||
// A per-empire shuffle of its species' colonyNames indices, so
|
||||
// peekColonyName() hands out that bank in a random (but replay-stable)
|
||||
// order instead of always suggesting the same name first. Drawn from a
|
||||
// seed derived from the game seed and empire index rather than
|
||||
// state.rngState — this is cosmetic, not gameplay, and keeping it off
|
||||
// the shared RNG stream means it can never nudge tech rolls, combat, or
|
||||
// anything else a replay depends on.
|
||||
nameOrder: shuffledIndexes(mulberry32((seed + (e + 1) * 7919) >>> 0), spec.colonyNames.length),
|
||||
_comps: null, _compsAt: -1, _range: null, _rangeAt: -1, _designs: null, _designsAt: -1,
|
||||
};
|
||||
// Research starts spread evenly across the six fields.
|
||||
|
|
@ -438,7 +447,8 @@ export function createGame(rules, opts) {
|
|||
// Homeworlds.
|
||||
speciesIds.forEach((sid, e) => {
|
||||
const starIdx = state.galaxy.homeIdx[e];
|
||||
const colony = foundColony(rules, state, e, starIdx, 0, rules.economy.startingPop);
|
||||
const name = e === humanIndex ? homeColonyName : null;
|
||||
const colony = foundColony(rules, state, e, starIdx, 0, rules.economy.startingPop, name);
|
||||
colony.factories = rules.economy.startingFactories;
|
||||
colony.capital = true;
|
||||
state.empires[e].explored[starIdx] = true;
|
||||
|
|
@ -458,13 +468,57 @@ export function createGame(rules, opts) {
|
|||
return state;
|
||||
}
|
||||
|
||||
export function foundColony(rules, state, e, starIdx, orbit, pop) {
|
||||
function shuffledIndexes(rnd, len) {
|
||||
const arr = Array.from({ length: len }, (_, i) => i);
|
||||
for (let i = arr.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(rnd() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Read-only preview of the name a new colony for this empire would get,
|
||||
// without consuming it from its species' pool — lets UI show a default
|
||||
// before the player confirms. Walks the species' 50-name bank in this
|
||||
// empire's own shuffled order (`nameOrder`, set once in createGame), so
|
||||
// suggestions are randomized but a cursor that only ever moves forward still
|
||||
// guarantees no name already in use by this empire is suggested again. A
|
||||
// round past the end (which the bank is generous enough to make rare) appends
|
||||
// a Roman-numeral suffix rather than repeating a bare name.
|
||||
export function peekColonyName(rules, state, e) {
|
||||
const emp = state.empires[e];
|
||||
const pool = rules.species[emp.speciesId].colonyNames;
|
||||
const cursor = emp.nameCursor ?? 0;
|
||||
// Saves from before per-empire shuffling existed have no nameOrder (or one
|
||||
// sized for an older, shorter bank) — fall back to bank order rather than
|
||||
// crashing.
|
||||
const order = (emp.nameOrder && emp.nameOrder.length === pool.length)
|
||||
? emp.nameOrder : pool.map((_, i) => i);
|
||||
const round = Math.floor(cursor / pool.length);
|
||||
const name = pool[order[cursor % pool.length]];
|
||||
return round > 0 ? `${name} ${'I'.repeat(round + 1)}` : name;
|
||||
}
|
||||
|
||||
function advanceColonyNameCursor(state, e) {
|
||||
state.empires[e].nameCursor = (state.empires[e].nameCursor ?? 0) + 1;
|
||||
}
|
||||
|
||||
export function nextColonyName(rules, state, e) {
|
||||
const name = peekColonyName(rules, state, e);
|
||||
advanceColonyNameCursor(state, e);
|
||||
return name;
|
||||
}
|
||||
|
||||
export function foundColony(rules, state, e, starIdx, orbit, pop, name = null) {
|
||||
const colonyName = (name && name.trim()) ? name.trim() : peekColonyName(rules, state, e);
|
||||
advanceColonyNameCursor(state, e);
|
||||
const colony = {
|
||||
id: state.nextColonyId += 1,
|
||||
empireIdx: e,
|
||||
starIdx,
|
||||
orbit,
|
||||
pop,
|
||||
name: colonyName,
|
||||
factories: 0,
|
||||
waste: 0,
|
||||
defenseHp: 0,
|
||||
|
|
@ -1240,7 +1294,7 @@ function retreatFrom(rules, state, starIdx, e) {
|
|||
// --------------------------------------------------------------------------
|
||||
// Colonisation and invasion
|
||||
|
||||
export function colonize(rules, state, e, starIdx, orbit) {
|
||||
export function colonize(rules, state, e, starIdx, orbit, name = null) {
|
||||
if (!canColonize(rules, state, e, starIdx, orbit)) return false;
|
||||
const fleet = state.fleets.find((f) => f.starIdx === starIdx && f.empireIdx === e
|
||||
&& f.ships.some((s) => s.hullId === 'colonyship' && s.count > 0));
|
||||
|
|
@ -1248,7 +1302,7 @@ export function colonize(rules, state, e, starIdx, orbit) {
|
|||
const stack = fleet.ships.find((s) => s.hullId === 'colonyship' && s.count > 0);
|
||||
stack.count -= 1;
|
||||
cleanFleets(state);
|
||||
const colony = foundColony(rules, state, e, starIdx, orbit, 5);
|
||||
const colony = foundColony(rules, state, e, starIdx, orbit, 5, name);
|
||||
checkContactAt(rules, state, starIdx);
|
||||
pushEvent(state, { type: 'colonised', empire: e, starIdx, orbit, colonyId: colony.id, turn: state.turn });
|
||||
state.empires[e]._range = null;
|
||||
|
|
|
|||
|
|
@ -142,6 +142,13 @@ export function compileRules(json) {
|
|||
need(typeof s.color === 'string' && /^#[0-9a-f]{6}$/i.test(s.color), `species ${s.id} bad color`);
|
||||
need(Array.isArray(s.strengths) && s.strengths.length > 0, `species ${s.id} needs strengths`);
|
||||
need(Array.isArray(s.weaknesses) && s.weaknesses.length > 0, `species ${s.id} needs weaknesses`);
|
||||
need(Array.isArray(s.colonyNames) && s.colonyNames.length >= 50, `species ${s.id} needs 50+ colonyNames`);
|
||||
// Guarded rather than chained onto the check above: `need()` only records a
|
||||
// message and keeps going (see its definition), so a missing/malformed
|
||||
// colonyNames must not make THIS check itself throw and abort the whole
|
||||
// validation pass before every other species/table gets checked.
|
||||
const names = Array.isArray(s.colonyNames) ? s.colonyNames : [];
|
||||
need(new Set(names).size === names.length, `species ${s.id} has duplicate colonyNames`);
|
||||
}
|
||||
for (const l of json.leaders) {
|
||||
need(['admin', 'captain'].includes(l.kind), `leader ${l.id} bad kind`);
|
||||
|
|
|
|||
|
|
@ -450,10 +450,11 @@ export default class VegaSidePanel {
|
|||
const mine = colony.empireIdx === this.viewerIdx;
|
||||
const planet = state.galaxy.stars[colony.starIdx].planets[colony.orbit];
|
||||
|
||||
if (colony.name) this.line(colony.name, { size: 16, color: '#e8f4ff', gap: 2 });
|
||||
this.line(
|
||||
`${owner.name}${colony.capital ? ' — capital' : ''}`
|
||||
+ ` · ${ORBIT[colony.orbit] ?? colony.orbit + 1} ${rules.planetTypes[planet.typeId].name}`,
|
||||
{ size: 16, color: owner.color, gap: 2 },
|
||||
{ size: colony.name ? 13 : 16, color: owner.color, gap: 2 },
|
||||
);
|
||||
|
||||
if (!mine) {
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ export default class VegaStarMap {
|
|||
const own = cols.filter((c) => c.empireIdx === emp.idx).sort((a, b) => a.orbit - b.orbit);
|
||||
if (!own.length) continue;
|
||||
const text = this.scene.add.text(0, 0, own.map((c) => (
|
||||
`${s.star.name} ${ORBIT[c.orbit] ?? c.orbit + 1} (pop. ${c.pop.toFixed(1)})`
|
||||
`${c.name ?? `${s.star.name} ${ORBIT[c.orbit] ?? c.orbit + 1}`} (pop. ${c.pop.toFixed(1)})`
|
||||
)).join('\n'), {
|
||||
fontFamily: FONT, fontSize: `${textSize}px`, color: '#f0f4fa', lineSpacing: 2,
|
||||
}).setOrigin(1, 0);
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ import { Button } from '../../ui/Button.js';
|
|||
import { modalShell, FONT, ORBIT } from './VegaScreens.js';
|
||||
import { planetFrame, starFrame } from './VegaArt.js';
|
||||
import { openColonyView, CHANNEL_COLOUR, itemName, etaText } from './VegaColonyView.js';
|
||||
import { openColonyIntro, ensureColonyVideo } from './VegaColonyIntro.js';
|
||||
import { openColonyIntro, ensureColonyVideo, promptColonyName } from './VegaColonyIntro.js';
|
||||
import {
|
||||
CHANNELS, coloniesAt, canColonize, colonize, colonyMaxPop, colonyProduction,
|
||||
colonyFactoryCap, effectiveFactories, colonyDefenseCap, colonyBuildRate,
|
||||
collapseQueue, queueItemCost, queueEtas, invasionForecast, invade, bombard, atWar,
|
||||
peekColonyName,
|
||||
} from './VegaLogic.js';
|
||||
|
||||
export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
||||
|
|
@ -125,11 +126,19 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
// every other use of it scales down, so this is the one place the art is
|
||||
// seen at the size it was drawn — hence no setDisplaySize.
|
||||
console_.add(scene.add.image(panelX + 96, y + 96, art.planets, planetFrame(rules, planet.typeId)));
|
||||
console_.add(scene.add.text(panelX + 208, y + 18,
|
||||
`${star.name} ${ORBIT[selected] ?? selected + 1}`, {
|
||||
fontFamily: FONT, fontSize: '30px', color: '#cfe8ff',
|
||||
// A colonized world leads with its given name — the planet's astronomical
|
||||
// designation (star + orbit) demotes to a subtitle rather than disappearing,
|
||||
// since it is still how the star map and combat reports refer to the spot.
|
||||
const worldName = `${star.name} ${ORBIT[selected] ?? selected + 1}`;
|
||||
console_.add(scene.add.text(panelX + 208, y + 18, colony?.name ?? worldName, {
|
||||
fontFamily: FONT, fontSize: '30px', color: '#cfe8ff',
|
||||
}));
|
||||
if (colony?.name) {
|
||||
console_.add(scene.add.text(panelX + 208, y + 50, worldName, {
|
||||
fontFamily: FONT, fontSize: '14px', color: '#7f97b3',
|
||||
}));
|
||||
console_.add(scene.add.text(panelX + 208, y + 60,
|
||||
}
|
||||
console_.add(scene.add.text(panelX + 208, y + (colony?.name ? 74 : 60),
|
||||
`${type.name} · ${rules.planetSizes[planet.sizeId]?.name ?? ''}\n`
|
||||
+ `${rules.richness[planet.richId]?.name ?? ''} · ${rules.gravity[planet.gravId]?.name ?? ''}`, {
|
||||
fontFamily: FONT, fontSize: '17px', color: '#9fb6cc', lineSpacing: 4,
|
||||
|
|
@ -160,17 +169,25 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
ensureColonyVideo(scene, planet.typeId);
|
||||
console_.add(new Button(scene, panelX + 130, y + 20, 'Found colony', () => {
|
||||
const orbit = selected;
|
||||
if (!colonize(rules, state, viewerIdx, starIdx, orbit)) return;
|
||||
onChanged?.();
|
||||
// The founding vignette, not a rebuilt panel — it covers this whole
|
||||
// modal, so the orrery behind it stops turning until it is dismissed.
|
||||
// Only the human ever gets here: the AI colonises through the same
|
||||
// Logic.colonize() with no view attached.
|
||||
const founded = coloniesAt(state, starIdx).find((c) => c.orbit === orbit);
|
||||
// Naming happens before the colony exists — canColonize/colonize
|
||||
// read no state this pauses, so the orrery is frozen for the whole
|
||||
// naming-then-founding sequence rather than just the vignette after.
|
||||
tick.paused = true;
|
||||
openColonyIntro(scene, rules, state, founded, art, {
|
||||
onClose: () => { tick.paused = false; rebuild(); },
|
||||
});
|
||||
promptColonyName(scene, {
|
||||
worldName,
|
||||
defaultName: peekColonyName(rules, state, viewerIdx),
|
||||
}, (name) => {
|
||||
if (!colonize(rules, state, viewerIdx, starIdx, orbit, name)) { tick.paused = false; return; }
|
||||
onChanged?.();
|
||||
// The founding vignette, not a rebuilt panel — it covers this whole
|
||||
// modal, so the orrery behind it stops turning until it is dismissed.
|
||||
// Only the human ever gets here: the AI colonises through the same
|
||||
// Logic.colonize() with no view attached.
|
||||
const founded = coloniesAt(state, starIdx).find((c) => c.orbit === orbit);
|
||||
openColonyIntro(scene, rules, state, founded, art, {
|
||||
onClose: () => { tick.paused = false; rebuild(); },
|
||||
});
|
||||
}, () => { tick.paused = false; });
|
||||
}, { width: 260, height: 44 }));
|
||||
}
|
||||
return;
|
||||
|
|
@ -178,6 +195,7 @@ export function openSystemView(scene, rules, state, starIdx, art, opts = {}) {
|
|||
|
||||
const owner = state.empires[colony.empireIdx];
|
||||
const mine = colony.empireIdx === viewerIdx;
|
||||
// Colony name already leads the header above; this is just who holds it.
|
||||
console_.add(scene.add.text(panelX, y, `${owner.name} colony`, {
|
||||
fontFamily: FONT, fontSize: '19px', color: owner.color,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function describeWeapon(w) {
|
|||
}
|
||||
|
||||
function hostilityWorlds(rules, tier) {
|
||||
const names = rules.planetTypes
|
||||
const names = rules.planetTypeList
|
||||
.filter((t) => t.colonizable && t.hostility === tier)
|
||||
.map((t) => t.name);
|
||||
return names.length ? `${names.join('/')}-class` : `hostility ${tier}`;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,16 @@ export class Button extends Phaser.GameObjects.Container {
|
|||
color: textColor,
|
||||
}).setOrigin(0.5);
|
||||
|
||||
// Long labels on a fixed-width button otherwise render past the edge of
|
||||
// the rounded rect. Purely corrective: only ever shrinks text that
|
||||
// wouldn't have fit at the requested size, so every button whose label
|
||||
// already fits renders exactly as before.
|
||||
const maxTextWidth = width - 16;
|
||||
if (this.text.width > maxTextWidth) {
|
||||
const fitSize = Math.max(11, Math.floor(fontSize * (maxTextWidth / this.text.width)));
|
||||
this.text.setFontSize(fitSize);
|
||||
}
|
||||
|
||||
this.add([this.bgRect, this.text]);
|
||||
|
||||
this.setSize(width, height);
|
||||
|
|
|
|||
|
|
@ -1580,7 +1580,13 @@ section('7. Diplomacy and the Galactic Council');
|
|||
'replyWarDeclared', 'replyTechAccept', 'replyTechReject',
|
||||
'offerPeaceOpener', 'offerAllianceOpener', 'acceptOffer', 'rejectOffer',
|
||||
'afterAccepted', 'afterRejected',
|
||||
'askPeople', 'peopleLore', 'askHomeworld', 'homeworldLore', 'askStory', 'storyLore',
|
||||
];
|
||||
// Lore reply pools are meant to carry real variety — enforce a minimum
|
||||
// beyond "non-empty" since pickLine's empty-pool fallback is silent and
|
||||
// a single-variant pool would defeat the whole point of these being
|
||||
// richer than a one-line treaty reply.
|
||||
const LORE_REPLY_MIN = { peopleLore: 3, homeworldLore: 3, storyLore: 3 };
|
||||
const capable = RULES.speciesList.filter((s) => (s.traits.diplomacy ?? 0) > -100);
|
||||
for (const s of capable) {
|
||||
const chat = CHAT[s.id];
|
||||
|
|
@ -1592,6 +1598,9 @@ section('7. Diplomacy and the Galactic Council');
|
|||
for (const key of SITUATIONS) {
|
||||
check(`VegaChat ${s.id}.${key} is non-empty`, chat[key]?.length > 0);
|
||||
}
|
||||
for (const [key, min] of Object.entries(LORE_REPLY_MIN)) {
|
||||
check(`VegaChat ${s.id}.${key} has at least ${min} variants`, (chat[key]?.length ?? 0) >= min);
|
||||
}
|
||||
}
|
||||
check('lithox has a contact-only pool', CHAT.lithox?.contactOnly?.length > 0);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue