feat(master-vega): add Galactic Council Session ceremony UI
Introduce a full-screen, click-advanced ceremony (VegaCouncilSession.js) that replays runCouncil()'s result delegate-by-delegate instead of hiding the tally behind the turn-report. Three fixed stages — Roster, Voting, and Verdict — with a persistent 2:3 portrait anchor video, green-screen ticker, live scoreboard with animated bars, and sting-then-loop audio. Key changes: - VegaLogic.js: expose per-voter `voters` breakdown and `pendingSession` flag so the UI can replay the session before the victory check. - MasterOfVegaGame.js: consume `pendingSession` in `runToHumanTurn()` before `state.over`, ensuring a decisive council still gets its ceremony. - VegaTurnReport.js: remove `council` from NOTABLE_TYPES (ceremony handles it); keep `councilRefused` as a distinct war-declaration row. - assetManifest.js / Sounds.js: register video + sting/loop audio cues. - VegaScreens.js: add `D.council` depth value. - verifyMasterOfVega.js: assert voter breakdown consistency and pending session lifecycle.
This commit is contained in:
parent
b145beb28d
commit
590bca1721
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
|
@ -298,6 +298,16 @@
|
||||||
"rows": 2,
|
"rows": 2,
|
||||||
"_layout": "One frame per building.frame, 0-15, in rules order."
|
"_layout": "One frame per building.frame, 0-15, in rules order."
|
||||||
},
|
},
|
||||||
|
"techfields": {
|
||||||
|
"key": "vega-techfields",
|
||||||
|
"path": "assets/images/vega/techfields.png",
|
||||||
|
"kind": "icon",
|
||||||
|
"frameWidth": 48,
|
||||||
|
"frameHeight": 48,
|
||||||
|
"cols": 3,
|
||||||
|
"rows": 2,
|
||||||
|
"_layout": "One frame per tech FIELD (techFields[].iconFrame), 0-5, in rules order: Computers, Construction, Force Fields, Planetology, Propulsion, Weapons. Exact fit, no spares."
|
||||||
|
},
|
||||||
"techicons": {
|
"techicons": {
|
||||||
"key": "vega-techicons",
|
"key": "vega-techicons",
|
||||||
"path": null,
|
"path": null,
|
||||||
|
|
@ -306,7 +316,7 @@
|
||||||
"frameHeight": 48,
|
"frameHeight": 48,
|
||||||
"cols": 10,
|
"cols": 10,
|
||||||
"rows": 10,
|
"rows": 10,
|
||||||
"_layout": "Frames 0-5 are the six tech FIELDS (techFields[].iconFrame); frames 6-65 are the original 60 individual techs, 66-77 are the first MOO1-branching alternates, 78-96 are a second branching pass (techs[].iconFrame). 97-99 spare."
|
"_layout": "One frame per individual tech (techs[].iconFrame), 0-90 — the 60 original techs plus two MOO1-branching passes added later, in rules order. Split out of the combined tech sheet so field icons and tech icons can be painted/sized independently. 91-99 spare."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,102 +13,102 @@
|
||||||
|
|
||||||
"_techsReadme": "prereqs is always 0 or 1 entries, pointing at the previous tech in the same field. tier must equal the topological rank. effects is a free-form bag read by VegaShips (weapon/armor/shield/engine/fuelRange/targeting) and VegaLogic (everything else).",
|
"_techsReadme": "prereqs is always 0 or 1 entries, pointing at the previous tech in the same field. tier must equal the topological rank. effects is a free-form bag read by VegaShips (weapon/armor/shield/engine/fuelRange/targeting) and VegaLogic (everything else).",
|
||||||
"techs": [
|
"techs": [
|
||||||
{ "id": "electroniccomputer", "name": "Electronic Computer", "field": "computers", "tier": 0, "prereqs": [], "cost": 60, "iconFrame": 6, "desc": "Mark I battle computer. +1 to hit.", "effects": { "targeting": 1 } },
|
{ "id": "electroniccomputer", "name": "Electronic Computer", "field": "computers", "tier": 0, "prereqs": [], "cost": 60, "iconFrame": 0, "desc": "Mark I battle computer. +1 to hit.", "effects": { "targeting": 1 } },
|
||||||
{ "id": "battlescanner", "name": "Battle Scanner", "field": "computers", "tier": 1, "prereqs": ["electroniccomputer"], "cost": 150, "iconFrame": 7, "desc": "Reveals enemy fleet composition and adds combat initiative.", "effects": { "initiative": 2, "scanRange": 1 } },
|
{ "id": "battlescanner", "name": "Battle Scanner", "field": "computers", "tier": 1, "prereqs": ["electroniccomputer"], "cost": 150, "iconFrame": 1, "desc": "Reveals enemy fleet composition and adds combat initiative.", "effects": { "initiative": 2, "scanRange": 1 } },
|
||||||
{ "id": "securecommarray", "name": "Secure Comm Array", "field": "computers", "tier": 1, "prereqs": ["electroniccomputer"], "cost": 150, "iconFrame": 78, "desc": "Encrypted fleet channels. Enemy spies have a harder time reading your signals.", "effects": { "counterEspionage": 14, "scanRange": 1 } },
|
{ "id": "securecommarray", "name": "Secure Comm Array", "field": "computers", "tier": 1, "prereqs": ["electroniccomputer"], "cost": 150, "iconFrame": 72, "desc": "Encrypted fleet channels. Enemy spies have a harder time reading your signals.", "effects": { "counterEspionage": 14, "scanRange": 1 } },
|
||||||
{ "id": "optroniccomputer", "name": "Optronic Computer", "field": "computers", "tier": 2, "prereqs": ["battlescanner"], "cost": 400, "iconFrame": 8, "desc": "Mark II battle computer. +2 to hit.", "effects": { "targeting": 2 } },
|
{ "id": "optroniccomputer", "name": "Optronic Computer", "field": "computers", "tier": 2, "prereqs": ["battlescanner"], "cost": 400, "iconFrame": 2, "desc": "Mark II battle computer. +2 to hit.", "effects": { "targeting": 2 } },
|
||||||
{ "id": "neuralscanner", "name": "Neural Scanner", "field": "computers", "tier": 3, "prereqs": ["optroniccomputer"], "cost": 900, "iconFrame": 9, "desc": "Deep-scan interrogation. Espionage missions far more likely.", "effects": { "espionage": 15 } },
|
{ "id": "neuralscanner", "name": "Neural Scanner", "field": "computers", "tier": 3, "prereqs": ["optroniccomputer"], "cost": 900, "iconFrame": 3, "desc": "Deep-scan interrogation. Espionage missions far more likely.", "effects": { "espionage": 15 } },
|
||||||
{ "id": "tachyoncomputer", "name": "Tachyon Computer", "field": "computers", "tier": 3, "prereqs": ["optroniccomputer"], "cost": 900, "iconFrame": 66, "desc": "Overclocked targeting matrix, no spycraft. +4 to hit.", "effects": { "targeting": 4 } },
|
{ "id": "tachyoncomputer", "name": "Tachyon Computer", "field": "computers", "tier": 3, "prereqs": ["optroniccomputer"], "cost": 900, "iconFrame": 60, "desc": "Overclocked targeting matrix, no spycraft. +4 to hit.", "effects": { "targeting": 4 } },
|
||||||
{ "id": "positroniccomputer", "name": "Positronic Computer", "field": "computers", "tier": 4, "prereqs": ["neuralscanner"], "cost": 1800, "iconFrame": 10, "desc": "Mark III battle computer. +3 to hit.", "effects": { "targeting": 3 }, "gnnHeadline": true },
|
{ "id": "positroniccomputer", "name": "Positronic Computer", "field": "computers", "tier": 4, "prereqs": ["neuralscanner"], "cost": 1800, "iconFrame": 4, "desc": "Mark III battle computer. +3 to hit.", "effects": { "targeting": 3 }, "gnnHeadline": true },
|
||||||
{ "id": "cybersecuritylink", "name": "Cyber Security Link", "field": "computers", "tier": 5, "prereqs": ["positroniccomputer"], "cost": 3200, "iconFrame": 11, "desc": "Hardened colony networks. Enemy spies rarely get through.", "effects": { "counterEspionage": 25 } },
|
{ "id": "cybersecuritylink", "name": "Cyber Security Link", "field": "computers", "tier": 5, "prereqs": ["positroniccomputer"], "cost": 3200, "iconFrame": 5, "desc": "Hardened colony networks. Enemy spies rarely get through.", "effects": { "counterEspionage": 25 } },
|
||||||
{ "id": "adaptiveresearchalgorithms", "name": "Adaptive Research Algorithms", "field": "computers", "tier": 5, "prereqs": ["positroniccomputer"], "cost": 3200, "iconFrame": 79, "desc": "Machine-learning heuristics comb through lab data around the clock. Research output up 10%, though the network offers no extra hardening against spies.", "effects": { "researchMult": 1.1 } },
|
{ "id": "adaptiveresearchalgorithms", "name": "Adaptive Research Algorithms", "field": "computers", "tier": 5, "prereqs": ["positroniccomputer"], "cost": 3200, "iconFrame": 73, "desc": "Machine-learning heuristics comb through lab data around the clock. Research output up 10%, though the network offers no extra hardening against spies.", "effects": { "researchMult": 1.1 } },
|
||||||
{ "id": "emissionsguidance", "name": "Emissions Guidance", "field": "computers", "tier": 6, "prereqs": ["cybersecuritylink"], "cost": 5400, "iconFrame": 12, "desc": "Mark IV battle computer. +4 to hit.", "effects": { "targeting": 4 } },
|
{ "id": "emissionsguidance", "name": "Emissions Guidance", "field": "computers", "tier": 6, "prereqs": ["cybersecuritylink"], "cost": 5400, "iconFrame": 6, "desc": "Mark IV battle computer. +4 to hit.", "effects": { "targeting": 4 } },
|
||||||
{ "id": "oracleinterface", "name": "Oracle Interface", "field": "computers", "tier": 7, "prereqs": ["emissionsguidance"], "cost": 8600, "iconFrame": 13, "desc": "Mark V battle computer, and research output climbs sharply.", "effects": { "targeting": 5, "researchMult": 1.15 } },
|
{ "id": "oracleinterface", "name": "Oracle Interface", "field": "computers", "tier": 7, "prereqs": ["emissionsguidance"], "cost": 8600, "iconFrame": 7, "desc": "Mark V battle computer, and research output climbs sharply.", "effects": { "targeting": 5, "researchMult": 1.15 } },
|
||||||
{ "id": "omniscanarray", "name": "Omniscan Array", "field": "computers", "tier": 7, "prereqs": ["emissionsguidance"], "cost": 8600, "iconFrame": 67, "desc": "Full-spectrum battle sensors. +5 to hit and always strikes first.", "effects": { "targeting": 5, "initiative": 5 } },
|
{ "id": "omniscanarray", "name": "Omniscan Array", "field": "computers", "tier": 7, "prereqs": ["emissionsguidance"], "cost": 8600, "iconFrame": 61, "desc": "Full-spectrum battle sensors. +5 to hit and always strikes first.", "effects": { "targeting": 5, "initiative": 5 } },
|
||||||
{ "id": "cybertroniccomputer","name": "Cybertronic Computer", "field": "computers", "tier": 8, "prereqs": ["oracleinterface"], "cost": 13000, "iconFrame": 14, "desc": "Mark VI battle computer. +6 to hit.", "effects": { "targeting": 6 }, "gnnHeadline": true },
|
{ "id": "cybertroniccomputer","name": "Cybertronic Computer", "field": "computers", "tier": 8, "prereqs": ["oracleinterface"], "cost": 13000, "iconFrame": 8, "desc": "Mark VI battle computer. +6 to hit.", "effects": { "targeting": 6 }, "gnnHeadline": true },
|
||||||
{ "id": "deepfieldsensorarray","name": "Deep Field Sensor Array", "field": "computers", "tier": 8, "prereqs": ["oracleinterface"], "cost": 13000, "iconFrame": 80, "desc": "Long-baseline sensor net. Fleets see further and react faster, at the cost of raw fire-control precision.", "effects": { "scanRange": 2, "initiative": 2 } },
|
{ "id": "deepfieldsensorarray","name": "Deep Field Sensor Array", "field": "computers", "tier": 8, "prereqs": ["oracleinterface"], "cost": 13000, "iconFrame": 74, "desc": "Long-baseline sensor net. Fleets see further and react faster, at the cost of raw fire-control precision.", "effects": { "scanRange": 2, "initiative": 2 } },
|
||||||
{ "id": "galacticcybernet", "name": "Galactic Cybernet", "field": "computers", "tier": 9, "prereqs": ["cybertroniccomputer"], "cost": 20000, "iconFrame": 15, "desc": "Mark VII battle computer and an empire-wide research grid.", "effects": { "targeting": 7, "researchMult": 1.3 }, "gnnHeadline": true },
|
{ "id": "galacticcybernet", "name": "Galactic Cybernet", "field": "computers", "tier": 9, "prereqs": ["cybertroniccomputer"], "cost": 20000, "iconFrame": 9, "desc": "Mark VII battle computer and an empire-wide research grid.", "effects": { "targeting": 7, "researchMult": 1.3 }, "gnnHeadline": true },
|
||||||
|
|
||||||
{ "id": "reinforcedhull", "name": "Reinforced Hull", "field": "construction", "tier": 0, "prereqs": [], "cost": 60, "iconFrame": 16, "desc": "Cross-braced spaceframes. Hulls absorb 25% more damage.", "effects": { "armor": { "name": "Reinforced", "hpMult": 1.25 } } },
|
{ "id": "reinforcedhull", "name": "Reinforced Hull", "field": "construction", "tier": 0, "prereqs": [], "cost": 60, "iconFrame": 10, "desc": "Cross-braced spaceframes. Hulls absorb 25% more damage.", "effects": { "armor": { "name": "Reinforced", "hpMult": 1.25 } } },
|
||||||
{ "id": "duralloyarmor", "name": "Duralloy Armour", "field": "construction", "tier": 1, "prereqs": ["reinforcedhull"], "cost": 160, "iconFrame": 17, "desc": "Duralloy plating. Hulls absorb 50% more damage.", "effects": { "armor": { "name": "Duralloy", "hpMult": 1.5 } } },
|
{ "id": "duralloyarmor", "name": "Duralloy Armour", "field": "construction", "tier": 1, "prereqs": ["reinforcedhull"], "cost": 160, "iconFrame": 11, "desc": "Duralloy plating. Hulls absorb 50% more damage.", "effects": { "armor": { "name": "Duralloy", "hpMult": 1.5 } } },
|
||||||
{ "id": "modularhullframes", "name": "Modular Hull Frames", "field": "construction", "tier": 1, "prereqs": ["reinforcedhull"], "cost": 160, "iconFrame": 81, "desc": "Standardised hull sections. Ships refit for less, though the plating itself is no thicker than reinforced hull.", "effects": { "refitCostMult": 0.85 } },
|
{ "id": "modularhullframes", "name": "Modular Hull Frames", "field": "construction", "tier": 1, "prereqs": ["reinforcedhull"], "cost": 160, "iconFrame": 75, "desc": "Standardised hull sections. Ships refit for less, though the plating itself is no thicker than reinforced hull.", "effects": { "refitCostMult": 0.85 } },
|
||||||
{ "id": "improvedindustrial", "name": "Improved Industrial Tech","field": "construction","tier": 2,"prereqs": ["duralloyarmor"], "cost": 420, "iconFrame": 18, "desc": "Factories cost 20% less to build and to run.", "effects": { "factoryCostMult": 0.8 } },
|
{ "id": "improvedindustrial", "name": "Improved Industrial Tech","field": "construction","tier": 2,"prereqs": ["duralloyarmor"], "cost": 420, "iconFrame": 12, "desc": "Factories cost 20% less to build and to run.", "effects": { "factoryCostMult": 0.8 } },
|
||||||
{ "id": "nanitehullsealant", "name": "Nanite Hull Sealant", "field": "construction", "tier": 2, "prereqs": ["duralloyarmor"], "cost": 420, "iconFrame": 68, "desc": "Self-sealing microfractures. Ships repair 2% of their hull between combat rounds.", "effects": { "repairPerRound": 0.02 } },
|
{ "id": "nanitehullsealant", "name": "Nanite Hull Sealant", "field": "construction", "tier": 2, "prereqs": ["duralloyarmor"], "cost": 420, "iconFrame": 62, "desc": "Self-sealing microfractures. Ships repair 2% of their hull between combat rounds.", "effects": { "repairPerRound": 0.02 } },
|
||||||
{ "id": "zortriumarmor", "name": "Zortrium Armour", "field": "construction", "tier": 3, "prereqs": ["improvedindustrial"], "cost": 950, "iconFrame": 19, "desc": "Zortrium plating. Hulls absorb 80% more damage.", "effects": { "armor": { "name": "Zortrium", "hpMult": 1.8 } } },
|
{ "id": "zortriumarmor", "name": "Zortrium Armour", "field": "construction", "tier": 3, "prereqs": ["improvedindustrial"], "cost": 950, "iconFrame": 13, "desc": "Zortrium plating. Hulls absorb 80% more damage.", "effects": { "armor": { "name": "Zortrium", "hpMult": 1.8 } } },
|
||||||
{ "id": "autorepairunit", "name": "Automated Repair Unit", "field": "construction", "tier": 4, "prereqs": ["zortriumarmor"], "cost": 1900, "iconFrame": 20, "desc": "Ships repair 3% of their hull between combat rounds.", "effects": { "repairPerRound": 0.03 } },
|
{ "id": "autorepairunit", "name": "Automated Repair Unit", "field": "construction", "tier": 4, "prereqs": ["zortriumarmor"], "cost": 1900, "iconFrame": 14, "desc": "Ships repair 3% of their hull between combat rounds.", "effects": { "repairPerRound": 0.03 } },
|
||||||
{ "id": "roboticassemblylines","name": "Robotic Assembly Lines", "field": "construction", "tier": 4, "prereqs": ["zortriumarmor"], "cost": 1900, "iconFrame": 82, "desc": "Fully robotic production lines undercut even Improved Industrial Tech. Factories cost 25% less to build and run, though the yards spend no effort on hull repair drones.", "effects": { "factoryCostMult": 0.75 }, "gnnHeadline": true },
|
{ "id": "roboticassemblylines","name": "Robotic Assembly Lines", "field": "construction", "tier": 4, "prereqs": ["zortriumarmor"], "cost": 1900, "iconFrame": 76, "desc": "Fully robotic production lines undercut even Improved Industrial Tech. Factories cost 25% less to build and run, though the yards spend no effort on hull repair drones.", "effects": { "factoryCostMult": 0.75 }, "gnnHeadline": true },
|
||||||
{ "id": "andriumarmor", "name": "Andrium Armour", "field": "construction", "tier": 5, "prereqs": ["autorepairunit"], "cost": 3400, "iconFrame": 21, "desc": "Andrium plating. Hulls absorb 110% more damage.", "effects": { "armor": { "name": "Andrium", "hpMult": 2.1 } } },
|
{ "id": "andriumarmor", "name": "Andrium Armour", "field": "construction", "tier": 5, "prereqs": ["autorepairunit"], "cost": 3400, "iconFrame": 15, "desc": "Andrium plating. Hulls absorb 110% more damage.", "effects": { "armor": { "name": "Andrium", "hpMult": 2.1 } } },
|
||||||
{ "id": "damagecontrol", "name": "Advanced Damage Control","field": "construction","tier": 6, "prereqs": ["andriumarmor"], "cost": 5600, "iconFrame": 22, "desc": "Damaged ships rejoin the line faster and cost less to refit.", "effects": { "repairPerRound": 0.06, "refitCostMult": 0.7 } },
|
{ "id": "damagecontrol", "name": "Advanced Damage Control","field": "construction","tier": 6, "prereqs": ["andriumarmor"], "cost": 5600, "iconFrame": 16, "desc": "Damaged ships rejoin the line faster and cost less to refit.", "effects": { "repairPerRound": 0.06, "refitCostMult": 0.7 } },
|
||||||
{ "id": "redundantlifesupport","name": "Redundant Life Support","field": "construction", "tier": 6, "prereqs": ["andriumarmor"], "cost": 5600, "iconFrame": 69, "desc": "Backup systems patch hulls faster in the heat of battle. Ships repair 7% each round.", "effects": { "repairPerRound": 0.07 } },
|
{ "id": "redundantlifesupport","name": "Redundant Life Support","field": "construction", "tier": 6, "prereqs": ["andriumarmor"], "cost": 5600, "iconFrame": 63, "desc": "Backup systems patch hulls faster in the heat of battle. Ships repair 7% each round.", "effects": { "repairPerRound": 0.07 } },
|
||||||
{ "id": "tritaniumarmor", "name": "Tritanium Armour", "field": "construction", "tier": 7, "prereqs": ["damagecontrol"], "cost": 9000, "iconFrame": 23, "desc": "Tritanium plating. Hulls absorb 150% more damage.", "effects": { "armor": { "name": "Tritanium", "hpMult": 2.5 } } },
|
{ "id": "tritaniumarmor", "name": "Tritanium Armour", "field": "construction", "tier": 7, "prereqs": ["damagecontrol"], "cost": 9000, "iconFrame": 17, "desc": "Tritanium plating. Hulls absorb 150% more damage.", "effects": { "armor": { "name": "Tritanium", "hpMult": 2.5 } } },
|
||||||
{ "id": "adamantiumarmor", "name": "Adamantium Armour", "field": "construction", "tier": 8, "prereqs": ["tritaniumarmor"], "cost": 14000, "iconFrame": 24, "desc": "Adamantium plating. Hulls absorb 200% more damage.", "effects": { "armor": { "name": "Adamantium", "hpMult": 3.0 } }, "gnnHeadline": true },
|
{ "id": "adamantiumarmor", "name": "Adamantium Armour", "field": "construction", "tier": 8, "prereqs": ["tritaniumarmor"], "cost": 14000, "iconFrame": 18, "desc": "Adamantium plating. Hulls absorb 200% more damage.", "effects": { "armor": { "name": "Adamantium", "hpMult": 3.0 } }, "gnnHeadline": true },
|
||||||
{ "id": "autonomousrepairswarms","name": "Autonomous Repair Swarms", "field": "construction", "tier": 8, "prereqs": ["tritaniumarmor"], "cost": 14000, "iconFrame": 83, "desc": "Self-directing nanite swarms patch hull breaches faster than any yard crew. Ships repair 9% of their hull between combat rounds, though plating stays at Tritanium grade.", "effects": { "repairPerRound": 0.09 } },
|
{ "id": "autonomousrepairswarms","name": "Autonomous Repair Swarms", "field": "construction", "tier": 8, "prereqs": ["tritaniumarmor"], "cost": 14000, "iconFrame": 77, "desc": "Self-directing nanite swarms patch hull breaches faster than any yard crew. Ships repair 9% of their hull between combat rounds, though plating stays at Tritanium grade.", "effects": { "repairPerRound": 0.09 } },
|
||||||
{ "id": "neutroniumarmor", "name": "Neutronium Armour", "field": "construction", "tier": 9, "prereqs": ["adamantiumarmor"], "cost": 21000, "iconFrame": 25, "desc": "Degenerate-matter plating. Hulls absorb 260% more damage.", "effects": { "armor": { "name": "Neutronium", "hpMult": 3.6 } }, "gnnHeadline": true },
|
{ "id": "neutroniumarmor", "name": "Neutronium Armour", "field": "construction", "tier": 9, "prereqs": ["adamantiumarmor"], "cost": 21000, "iconFrame": 19, "desc": "Degenerate-matter plating. Hulls absorb 260% more damage.", "effects": { "armor": { "name": "Neutronium", "hpMult": 3.6 } }, "gnnHeadline": true },
|
||||||
|
|
||||||
{ "id": "deflectori", "name": "Class I Deflector Shield","field": "forcefields","tier": 0, "prereqs": [], "cost": 80, "iconFrame": 26, "desc": "Absorbs 1 point from every incoming hit.", "effects": { "shield": 1 } },
|
{ "id": "deflectori", "name": "Class I Deflector Shield","field": "forcefields","tier": 0, "prereqs": [], "cost": 80, "iconFrame": 20, "desc": "Absorbs 1 point from every incoming hit.", "effects": { "shield": 1 } },
|
||||||
{ "id": "personalshield", "name": "Personal Shield", "field": "forcefields", "tier": 1, "prereqs": ["deflectori"], "cost": 200, "iconFrame": 27, "desc": "Shielded infantry. +10 to ground combat on attack and defence.","effects": { "groundAttack": 10, "groundDefense": 10 } },
|
{ "id": "personalshield", "name": "Personal Shield", "field": "forcefields", "tier": 1, "prereqs": ["deflectori"], "cost": 200, "iconFrame": 21, "desc": "Shielded infantry. +10 to ground combat on attack and defence.","effects": { "groundAttack": 10, "groundDefense": 10 } },
|
||||||
{ "id": "pointdefensegrid", "name": "Point-Defense Grid", "field": "forcefields", "tier": 1, "prereqs": ["deflectori"], "cost": 200, "iconFrame": 70, "desc": "Automated point-defense turrets. Fleets react a beat faster in combat.", "effects": { "initiative": 1 } },
|
{ "id": "pointdefensegrid", "name": "Point-Defense Grid", "field": "forcefields", "tier": 1, "prereqs": ["deflectori"], "cost": 200, "iconFrame": 64, "desc": "Automated point-defense turrets. Fleets react a beat faster in combat.", "effects": { "initiative": 1 } },
|
||||||
{ "id": "deflectoriii", "name": "Class III Deflector", "field": "forcefields", "tier": 2, "prereqs": ["personalshield"], "cost": 500, "iconFrame": 28, "desc": "Absorbs 2 points from every incoming hit.", "effects": { "shield": 2 } },
|
{ "id": "deflectoriii", "name": "Class III Deflector", "field": "forcefields", "tier": 2, "prereqs": ["personalshield"], "cost": 500, "iconFrame": 22, "desc": "Absorbs 2 points from every incoming hit.", "effects": { "shield": 2 } },
|
||||||
{ "id": "modulardeflectorcoils","name": "Modular Deflector Coils","field": "forcefields", "tier": 2, "prereqs": ["personalshield"], "cost": 500, "iconFrame": 84, "desc": "Standardised shield generators drop into any hull. Fleets refit for 15% less, though the coils themselves absorb no extra damage.", "effects": { "refitCostMult": 0.85 } },
|
{ "id": "modulardeflectorcoils","name": "Modular Deflector Coils","field": "forcefields", "tier": 2, "prereqs": ["personalshield"], "cost": 500, "iconFrame": 78, "desc": "Standardised shield generators drop into any hull. Fleets refit for 15% less, though the coils themselves absorb no extra damage.", "effects": { "refitCostMult": 0.85 } },
|
||||||
{ "id": "planetaryshield5", "name": "Planetary Shield V", "field": "forcefields", "tier": 3, "prereqs": ["deflectoriii"], "cost": 1100, "iconFrame": 29, "desc": "Colonies shrug off 5 points from every orbital bombardment.", "effects": { "planetaryShield": 5 } },
|
{ "id": "planetaryshield5", "name": "Planetary Shield V", "field": "forcefields", "tier": 3, "prereqs": ["deflectoriii"], "cost": 1100, "iconFrame": 23, "desc": "Colonies shrug off 5 points from every orbital bombardment.", "effects": { "planetaryShield": 5 } },
|
||||||
{ "id": "deflectorv", "name": "Class V Deflector", "field": "forcefields", "tier": 4, "prereqs": ["planetaryshield5"], "cost": 2100, "iconFrame": 30, "desc": "Absorbs 3 points from every incoming hit.", "effects": { "shield": 3 } },
|
{ "id": "deflectorv", "name": "Class V Deflector", "field": "forcefields", "tier": 4, "prereqs": ["planetaryshield5"], "cost": 2100, "iconFrame": 24, "desc": "Absorbs 3 points from every incoming hit.", "effects": { "shield": 3 } },
|
||||||
{ "id": "stealthfield", "name": "Stealth Field", "field": "forcefields", "tier": 5, "prereqs": ["deflectorv"], "cost": 3600, "iconFrame": 31, "desc": "Cloaked hulls. Fleets move unseen and strike first.", "effects": { "cloaked": true, "initiative": 3 }, "gnnHeadline": true },
|
{ "id": "stealthfield", "name": "Stealth Field", "field": "forcefields", "tier": 5, "prereqs": ["deflectorv"], "cost": 3600, "iconFrame": 25, "desc": "Cloaked hulls. Fleets move unseen and strike first.", "effects": { "cloaked": true, "initiative": 3 }, "gnnHeadline": true },
|
||||||
{ "id": "colonialdeflectorgrid","name": "Colonial Deflector Grid","field": "forcefields", "tier": 5, "prereqs": ["deflectorv"], "cost": 3600, "iconFrame": 85, "desc": "Extends deflector coverage down to the colony surface. Absorbs 8 points from every orbital bombardment, though fleets stay visible on enemy scopes.", "effects": { "planetaryShield": 8 } },
|
{ "id": "colonialdeflectorgrid","name": "Colonial Deflector Grid","field": "forcefields", "tier": 5, "prereqs": ["deflectorv"], "cost": 3600, "iconFrame": 79, "desc": "Extends deflector coverage down to the colony surface. Absorbs 8 points from every orbital bombardment, though fleets stay visible on enemy scopes.", "effects": { "planetaryShield": 8 } },
|
||||||
{ "id": "deflectorvii", "name": "Class VII Deflector", "field": "forcefields", "tier": 6, "prereqs": ["stealthfield"], "cost": 6000, "iconFrame": 32, "desc": "Absorbs 5 points from every incoming hit.", "effects": { "shield": 5 } },
|
{ "id": "deflectorvii", "name": "Class VII Deflector", "field": "forcefields", "tier": 6, "prereqs": ["stealthfield"], "cost": 6000, "iconFrame": 26, "desc": "Absorbs 5 points from every incoming hit.", "effects": { "shield": 5 } },
|
||||||
{ "id": "planetaryshield15", "name": "Planetary Shield XV", "field": "forcefields", "tier": 7, "prereqs": ["deflectorvii"], "cost": 9500, "iconFrame": 33, "desc": "Colonies shrug off 15 points from every orbital bombardment.", "effects": { "planetaryShield": 15 } },
|
{ "id": "planetaryshield15", "name": "Planetary Shield XV", "field": "forcefields", "tier": 7, "prereqs": ["deflectorvii"], "cost": 9500, "iconFrame": 27, "desc": "Colonies shrug off 15 points from every orbital bombardment.", "effects": { "planetaryShield": 15 } },
|
||||||
{ "id": "deepshieldharmonics","name": "Deep Shield Harmonics", "field": "forcefields", "tier": 7, "prereqs": ["deflectorvii"], "cost": 9500, "iconFrame": 71, "desc": "Ship-side shield regenerators. Absorbs 6 points from every incoming hit.", "effects": { "shield": 6 } },
|
{ "id": "deepshieldharmonics","name": "Deep Shield Harmonics", "field": "forcefields", "tier": 7, "prereqs": ["deflectorvii"], "cost": 9500, "iconFrame": 65, "desc": "Ship-side shield regenerators. Absorbs 6 points from every incoming hit.", "effects": { "shield": 6 } },
|
||||||
{ "id": "deflectorx", "name": "Class X Deflector", "field": "forcefields", "tier": 8, "prereqs": ["planetaryshield15"], "cost": 15000, "iconFrame": 34, "desc": "Absorbs 7 points from every incoming hit.", "effects": { "shield": 7 }, "gnnHeadline": true },
|
{ "id": "deflectorx", "name": "Class X Deflector", "field": "forcefields", "tier": 8, "prereqs": ["planetaryshield15"], "cost": 15000, "iconFrame": 28, "desc": "Absorbs 7 points from every incoming hit.", "effects": { "shield": 7 }, "gnnHeadline": true },
|
||||||
{ "id": "harmonicdeflectorarray","name": "Harmonic Deflector Array","field": "forcefields","tier": 8, "prereqs": ["planetaryshield15"], "cost": 15000, "iconFrame": 86, "desc": "Shield harmonics tuned to scramble infiltration attempts. +18 counter-espionage empire-wide, though the array trades depth of coverage for breadth.", "effects": { "counterEspionage": 18 } },
|
{ "id": "harmonicdeflectorarray","name": "Harmonic Deflector Array","field": "forcefields","tier": 8, "prereqs": ["planetaryshield15"], "cost": 15000, "iconFrame": 80, "desc": "Shield harmonics tuned to scramble infiltration attempts. +18 counter-espionage empire-wide, though the array trades depth of coverage for breadth.", "effects": { "counterEspionage": 18 } },
|
||||||
{ "id": "blackholegenerator", "name": "Black Hole Generator", "field": "forcefields", "tier": 9, "prereqs": ["deflectorx"], "cost": 22000, "iconFrame": 35, "desc": "Folds enemy hulls into a singularity. Devastating in battle.", "effects": { "shield": 9, "singularity": true }, "gnnHeadline": true },
|
{ "id": "blackholegenerator", "name": "Black Hole Generator", "field": "forcefields", "tier": 9, "prereqs": ["deflectorx"], "cost": 22000, "iconFrame": 29, "desc": "Folds enemy hulls into a singularity. Devastating in battle.", "effects": { "shield": 9, "singularity": true }, "gnnHeadline": true },
|
||||||
|
|
||||||
{ "id": "ecorestoration", "name": "Ecological Restoration","field": "planetology", "tier": 0, "prereqs": [], "cost": 70, "iconFrame": 36, "desc": "Industrial waste costs 25% less to clean up.", "effects": { "wasteMult": 0.75 } },
|
{ "id": "ecorestoration", "name": "Ecological Restoration","field": "planetology", "tier": 0, "prereqs": [], "cost": 70, "iconFrame": 30, "desc": "Industrial waste costs 25% less to clean up.", "effects": { "wasteMult": 0.75 } },
|
||||||
{ "id": "controlledbarren", "name": "Controlled Barren Environment","field": "planetology","tier": 1,"prereqs": ["ecorestoration"], "cost": 180, "iconFrame": 37, "desc": "Sealed habitats let you colonise barren worlds.", "effects": { "colonizeHostility": 1 } },
|
{ "id": "controlledbarren", "name": "Controlled Barren Environment","field": "planetology","tier": 1,"prereqs": ["ecorestoration"], "cost": 180, "iconFrame": 31, "desc": "Sealed habitats let you colonise barren worlds.", "effects": { "colonizeHostility": 1 } },
|
||||||
{ "id": "preemptivewastefiltration","name": "Preemptive Waste Filtration","field": "planetology","tier": 1,"prereqs": ["ecorestoration"], "cost": 180, "iconFrame": 87, "desc": "Filters industrial runoff before it ever reaches the surface. Waste cleanup costs another 15% less, though it does nothing to open up barren worlds.", "effects": { "wasteMult": 0.65 } },
|
{ "id": "preemptivewastefiltration","name": "Preemptive Waste Filtration","field": "planetology","tier": 1,"prereqs": ["ecorestoration"], "cost": 180, "iconFrame": 81, "desc": "Filters industrial runoff before it ever reaches the surface. Waste cleanup costs another 15% less, though it does nothing to open up barren worlds.", "effects": { "wasteMult": 0.65 } },
|
||||||
{ "id": "enhancedeco", "name": "Enhanced Eco Restoration","field": "planetology","tier": 2, "prereqs": ["controlledbarren"], "cost": 460, "iconFrame": 38, "desc": "Industrial waste costs 50% less to clean up.", "effects": { "wasteMult": 0.5 } },
|
{ "id": "enhancedeco", "name": "Enhanced Eco Restoration","field": "planetology","tier": 2, "prereqs": ["controlledbarren"], "cost": 460, "iconFrame": 32, "desc": "Industrial waste costs 50% less to clean up.", "effects": { "wasteMult": 0.5 } },
|
||||||
{ "id": "biosphereresearchinitiative","name": "Biosphere Research Initiative","field": "planetology","tier": 2,"prereqs": ["controlledbarren"], "cost": 460, "iconFrame": 88, "desc": "Terraforming survey data doubles as pure science. Research output up 8%, though colonies keep producing waste at the old rate.", "effects": { "researchMult": 1.08 } },
|
{ "id": "biosphereresearchinitiative","name": "Biosphere Research Initiative","field": "planetology","tier": 2,"prereqs": ["controlledbarren"], "cost": 460, "iconFrame": 82, "desc": "Terraforming survey data doubles as pure science. Research output up 8%, though colonies keep producing waste at the old rate.", "effects": { "researchMult": 1.08 } },
|
||||||
{ "id": "controlleddead", "name": "Controlled Dead Environment","field": "planetology","tier": 3,"prereqs": ["enhancedeco"], "cost": 1000, "iconFrame": 39, "desc": "Colonise dead worlds stripped of atmosphere.", "effects": { "colonizeHostility": 2 } },
|
{ "id": "controlleddead", "name": "Controlled Dead Environment","field": "planetology","tier": 3,"prereqs": ["enhancedeco"], "cost": 1000, "iconFrame": 33, "desc": "Colonise dead worlds stripped of atmosphere.", "effects": { "colonizeHostility": 2 } },
|
||||||
{ "id": "soilenrichment", "name": "Soil Enrichment", "field": "planetology", "tier": 4, "prereqs": ["controlleddead"], "cost": 2000, "iconFrame": 40, "desc": "Every colony supports 15 more population.", "effects": { "maxPopBonus": 15 } },
|
{ "id": "soilenrichment", "name": "Soil Enrichment", "field": "planetology", "tier": 4, "prereqs": ["controlleddead"], "cost": 2000, "iconFrame": 34, "desc": "Every colony supports 15 more population.", "effects": { "maxPopBonus": 15 } },
|
||||||
{ "id": "efficientwastereclamation","name": "Efficient Waste Reclamation","field": "planetology","tier": 4,"prereqs": ["controlleddead"], "cost": 2000, "iconFrame": 72, "desc": "Industrial waste is recycled almost as fast as it's produced. Cleanup costs 40% less.", "effects": { "wasteMult": 0.6 } },
|
{ "id": "efficientwastereclamation","name": "Efficient Waste Reclamation","field": "planetology","tier": 4,"prereqs": ["controlleddead"], "cost": 2000, "iconFrame": 66, "desc": "Industrial waste is recycled almost as fast as it's produced. Cleanup costs 40% less.", "effects": { "wasteMult": 0.6 } },
|
||||||
{ "id": "controlledinferno", "name": "Controlled Inferno Environment","field": "planetology","tier": 5,"prereqs": ["soilenrichment"], "cost": 3500, "iconFrame": 41, "desc": "Colonise inferno worlds.", "effects": { "colonizeHostility": 3 } },
|
{ "id": "controlledinferno", "name": "Controlled Inferno Environment","field": "planetology","tier": 5,"prereqs": ["soilenrichment"], "cost": 3500, "iconFrame": 35, "desc": "Colonise inferno worlds.", "effects": { "colonizeHostility": 3 } },
|
||||||
{ "id": "atmosphericterraforming","name": "Atmospheric Terraforming","field": "planetology","tier": 6,"prereqs": ["controlledinferno"], "cost": 5800, "iconFrame": 42, "desc": "Every colony supports 25 more population.", "effects": { "maxPopBonus": 25 }, "gnnHeadline": true },
|
{ "id": "atmosphericterraforming","name": "Atmospheric Terraforming","field": "planetology","tier": 6,"prereqs": ["controlledinferno"], "cost": 5800, "iconFrame": 36, "desc": "Every colony supports 25 more population.", "effects": { "maxPopBonus": 25 }, "gnnHeadline": true },
|
||||||
{ "id": "xenobotanicalresearchstations","name": "Xenobotanical Research Stations","field": "planetology","tier": 6,"prereqs": ["controlledinferno"], "cost": 5800, "iconFrame": 89, "desc": "Terraforming labs double as pure research outposts. Research output up 10%, though colonies grow no larger for it.", "effects": { "researchMult": 1.1 } },
|
{ "id": "xenobotanicalresearchstations","name": "Xenobotanical Research Stations","field": "planetology","tier": 6,"prereqs": ["controlledinferno"], "cost": 5800, "iconFrame": 83, "desc": "Terraforming labs double as pure research outposts. Research output up 10%, though colonies grow no larger for it.", "effects": { "researchMult": 1.1 } },
|
||||||
{ "id": "controlledtoxic", "name": "Controlled Toxic Environment","field": "planetology","tier": 7,"prereqs": ["atmosphericterraforming"],"cost": 9200,"iconFrame": 43,"desc": "Colonise toxic worlds.", "effects": { "colonizeHostility": 4 } },
|
{ "id": "controlledtoxic", "name": "Controlled Toxic Environment","field": "planetology","tier": 7,"prereqs": ["atmosphericterraforming"],"cost": 9200,"iconFrame": 37,"desc": "Colonise toxic worlds.", "effects": { "colonizeHostility": 4 } },
|
||||||
{ "id": "advancedsoil", "name": "Advanced Soil Enrichment","field": "planetology","tier": 8, "prereqs": ["controlledtoxic"], "cost": 14500, "iconFrame": 44, "desc": "Every colony supports 40 more population.", "effects": { "maxPopBonus": 40 } },
|
{ "id": "advancedsoil", "name": "Advanced Soil Enrichment","field": "planetology","tier": 8, "prereqs": ["controlledtoxic"], "cost": 14500, "iconFrame": 38, "desc": "Every colony supports 40 more population.", "effects": { "maxPopBonus": 40 } },
|
||||||
{ "id": "deepterraforming", "name": "Deep Terraforming", "field": "planetology", "tier": 8, "prereqs": ["controlledtoxic"], "cost": 14500, "iconFrame": 73, "desc": "Near-total reclamation. Industrial waste costs 65% less to clean up.", "effects": { "wasteMult": 0.35 }, "gnnHeadline": true },
|
{ "id": "deepterraforming", "name": "Deep Terraforming", "field": "planetology", "tier": 8, "prereqs": ["controlledtoxic"], "cost": 14500, "iconFrame": 67, "desc": "Near-total reclamation. Industrial waste costs 65% less to clean up.", "effects": { "wasteMult": 0.35 }, "gnnHeadline": true },
|
||||||
{ "id": "controlledradiated", "name": "Controlled Radiated Environment","field": "planetology","tier": 9,"prereqs": ["advancedsoil"], "cost": 21500, "iconFrame": 45, "desc": "Colonise radiated worlds. Nowhere in the galaxy is closed.", "effects": { "colonizeHostility": 5, "maxPopBonus": 15 }, "gnnHeadline": true },
|
{ "id": "controlledradiated", "name": "Controlled Radiated Environment","field": "planetology","tier": 9,"prereqs": ["advancedsoil"], "cost": 21500, "iconFrame": 39, "desc": "Colonise radiated worlds. Nowhere in the galaxy is closed.", "effects": { "colonizeHostility": 5, "maxPopBonus": 15 }, "gnnHeadline": true },
|
||||||
|
|
||||||
{ "id": "nuclearengines", "name": "Nuclear Engines", "field": "propulsion", "tier": 0, "prereqs": [], "cost": 60, "iconFrame": 46, "desc": "Warp 1 drives. Fleets crawl, but they cross the dark.", "effects": { "engine": { "name": "Nuclear", "speed": 1 } } },
|
{ "id": "nuclearengines", "name": "Nuclear Engines", "field": "propulsion", "tier": 0, "prereqs": [], "cost": 60, "iconFrame": 40, "desc": "Warp 1 drives. Fleets crawl, but they cross the dark.", "effects": { "engine": { "name": "Nuclear", "speed": 1 } } },
|
||||||
{ "id": "deuteriumcells", "name": "Deuterium Fuel Cells", "field": "propulsion", "tier": 1, "prereqs": ["nuclearengines"], "cost": 170, "iconFrame": 47, "desc": "Fuel range 7 parsecs. The map opens a little.", "effects": { "fuelRange": 7 } },
|
{ "id": "deuteriumcells", "name": "Deuterium Fuel Cells", "field": "propulsion", "tier": 1, "prereqs": ["nuclearengines"], "cost": 170, "iconFrame": 41, "desc": "Fuel range 7 parsecs. The map opens a little.", "effects": { "fuelRange": 7 } },
|
||||||
{ "id": "reactionthrusters", "name": "Reaction Thrusters", "field": "propulsion", "tier": 1, "prereqs": ["nuclearengines"], "cost": 170, "iconFrame": 74, "desc": "Snap-fire manoeuvring jets. Fleets react a beat faster in combat, though range stays short.", "effects": { "initiative": 1 } },
|
{ "id": "reactionthrusters", "name": "Reaction Thrusters", "field": "propulsion", "tier": 1, "prereqs": ["nuclearengines"], "cost": 170, "iconFrame": 68, "desc": "Snap-fire manoeuvring jets. Fleets react a beat faster in combat, though range stays short.", "effects": { "initiative": 1 } },
|
||||||
{ "id": "fusiondrive", "name": "Fusion Drive", "field": "propulsion", "tier": 2, "prereqs": ["deuteriumcells"], "cost": 440, "iconFrame": 48, "desc": "Warp 2 drives.", "effects": { "engine": { "name": "Fusion", "speed": 2 } } },
|
{ "id": "fusiondrive", "name": "Fusion Drive", "field": "propulsion", "tier": 2, "prereqs": ["deuteriumcells"], "cost": 440, "iconFrame": 42, "desc": "Warp 2 drives.", "effects": { "engine": { "name": "Fusion", "speed": 2 } } },
|
||||||
{ "id": "auxiliaryfueltanks", "name": "Auxiliary Fuel Tanks", "field": "propulsion", "tier": 2, "prereqs": ["deuteriumcells"], "cost": 440, "iconFrame": 90, "desc": "Bolt-on tankage extends the envelope. Fuel range 8 parsecs, though the hull still tops out at Warp 1.", "effects": { "fuelRange": 8 } },
|
{ "id": "auxiliaryfueltanks", "name": "Auxiliary Fuel Tanks", "field": "propulsion", "tier": 2, "prereqs": ["deuteriumcells"], "cost": 440, "iconFrame": 84, "desc": "Bolt-on tankage extends the envelope. Fuel range 8 parsecs, though the hull still tops out at Warp 1.", "effects": { "fuelRange": 8 } },
|
||||||
{ "id": "iridiumcells", "name": "Iridium Fuel Cells", "field": "propulsion", "tier": 3, "prereqs": ["fusiondrive"], "cost": 980, "iconFrame": 49, "desc": "Fuel range 9 parsecs.", "effects": { "fuelRange": 9 } },
|
{ "id": "iridiumcells", "name": "Iridium Fuel Cells", "field": "propulsion", "tier": 3, "prereqs": ["fusiondrive"], "cost": 980, "iconFrame": 43, "desc": "Fuel range 9 parsecs.", "effects": { "fuelRange": 9 } },
|
||||||
{ "id": "sublightdrive", "name": "Sub-Light Drive", "field": "propulsion", "tier": 4, "prereqs": ["iridiumcells"], "cost": 2000, "iconFrame": 50, "desc": "Warp 3 drives.", "effects": { "engine": { "name": "Sub-Light", "speed": 3 } } },
|
{ "id": "sublightdrive", "name": "Sub-Light Drive", "field": "propulsion", "tier": 4, "prereqs": ["iridiumcells"], "cost": 2000, "iconFrame": 44, "desc": "Warp 3 drives.", "effects": { "engine": { "name": "Sub-Light", "speed": 3 } } },
|
||||||
{ "id": "uridiumcells", "name": "Uridium Fuel Cells", "field": "propulsion", "tier": 5, "prereqs": ["sublightdrive"], "cost": 3600, "iconFrame": 51, "desc": "Fuel range 12 parsecs. Half the galaxy lights up.", "effects": { "fuelRange": 12 } },
|
{ "id": "uridiumcells", "name": "Uridium Fuel Cells", "field": "propulsion", "tier": 5, "prereqs": ["sublightdrive"], "cost": 3600, "iconFrame": 45, "desc": "Fuel range 12 parsecs. Half the galaxy lights up.", "effects": { "fuelRange": 12 } },
|
||||||
{ "id": "tachyondrivecoils", "name": "Tachyon Drive Coils", "field": "propulsion", "tier": 5, "prereqs": ["sublightdrive"], "cost": 3600, "iconFrame": 75, "desc": "Precognitive drive telemetry. A further edge in combat tempo, at the cost of range.", "effects": { "initiative": 2 } },
|
{ "id": "tachyondrivecoils", "name": "Tachyon Drive Coils", "field": "propulsion", "tier": 5, "prereqs": ["sublightdrive"], "cost": 3600, "iconFrame": 69, "desc": "Precognitive drive telemetry. A further edge in combat tempo, at the cost of range.", "effects": { "initiative": 2 } },
|
||||||
{ "id": "impulsedrive", "name": "Impulse Drive", "field": "propulsion", "tier": 6, "prereqs": ["uridiumcells"], "cost": 5900, "iconFrame": 52, "desc": "Warp 4 drives.", "effects": { "engine": { "name": "Impulse", "speed": 4 } } },
|
{ "id": "impulsedrive", "name": "Impulse Drive", "field": "propulsion", "tier": 6, "prereqs": ["uridiumcells"], "cost": 5900, "iconFrame": 46, "desc": "Warp 4 drives.", "effects": { "engine": { "name": "Impulse", "speed": 4 } } },
|
||||||
{ "id": "deepscanrelays", "name": "Deep Scan Relays", "field": "propulsion", "tier": 6, "prereqs": ["uridiumcells"], "cost": 5900, "iconFrame": 91, "desc": "Automated relay buoys dropped along your travel lanes extend detection well past the fleet itself. +2 scan range empire-wide, though the drive itself stays at Warp 3.", "effects": { "scanRange": 2 } },
|
{ "id": "deepscanrelays", "name": "Deep Scan Relays", "field": "propulsion", "tier": 6, "prereqs": ["uridiumcells"], "cost": 5900, "iconFrame": 85, "desc": "Automated relay buoys dropped along your travel lanes extend detection well past the fleet itself. +2 scan range empire-wide, though the drive itself stays at Warp 3.", "effects": { "scanRange": 2 } },
|
||||||
{ "id": "thoriumcells", "name": "Thorium Fuel Cells", "field": "propulsion", "tier": 7, "prereqs": ["impulsedrive"], "cost": 9400, "iconFrame": 53, "desc": "Fuel range 18 parsecs. Range stops being a constraint.", "effects": { "fuelRange": 18 } },
|
{ "id": "thoriumcells", "name": "Thorium Fuel Cells", "field": "propulsion", "tier": 7, "prereqs": ["impulsedrive"], "cost": 9400, "iconFrame": 47, "desc": "Fuel range 18 parsecs. Range stops being a constraint.", "effects": { "fuelRange": 18 } },
|
||||||
{ "id": "interphaseddrive", "name": "Interphased Drive", "field": "propulsion", "tier": 8, "prereqs": ["thoriumcells"], "cost": 14800, "iconFrame": 54, "desc": "Warp 5 drives.", "effects": { "engine": { "name": "Interphased", "speed": 5 } }, "gnnHeadline": true },
|
{ "id": "interphaseddrive", "name": "Interphased Drive", "field": "propulsion", "tier": 8, "prereqs": ["thoriumcells"], "cost": 14800, "iconFrame": 48, "desc": "Warp 5 drives.", "effects": { "engine": { "name": "Interphased", "speed": 5 } }, "gnnHeadline": true },
|
||||||
{ "id": "deepspacefuelcells", "name": "Deep-Space Fuel Cells", "field": "propulsion", "tier": 8, "prereqs": ["thoriumcells"], "cost": 14800, "iconFrame": 92, "desc": "The last word in tankage before hyperspace renders range moot. Fuel range 22 parsecs, though the hull stays at Warp 4.", "effects": { "fuelRange": 22 } },
|
{ "id": "deepspacefuelcells", "name": "Deep-Space Fuel Cells", "field": "propulsion", "tier": 8, "prereqs": ["thoriumcells"], "cost": 14800, "iconFrame": 86, "desc": "The last word in tankage before hyperspace renders range moot. Fuel range 22 parsecs, though the hull stays at Warp 4.", "effects": { "fuelRange": 22 } },
|
||||||
{ "id": "hyperspacecomms", "name": "Hyperspace Communications","field": "propulsion","tier": 9,"prereqs": ["interphaseddrive"], "cost": 21000, "iconFrame": 55, "desc": "Warp 6 drives and fleets redirected in mid-flight.", "effects": { "engine": { "name": "Hyperspace", "speed": 6 }, "redirectInFlight": true }, "gnnHeadline": true },
|
{ "id": "hyperspacecomms", "name": "Hyperspace Communications","field": "propulsion","tier": 9,"prereqs": ["interphaseddrive"], "cost": 21000, "iconFrame": 49, "desc": "Warp 6 drives and fleets redirected in mid-flight.", "effects": { "engine": { "name": "Hyperspace", "speed": 6 }, "redirectInFlight": true }, "gnnHeadline": true },
|
||||||
|
|
||||||
{ "id": "lasercannon", "name": "Laser Cannon", "field": "weapons", "tier": 0, "prereqs": [], "cost": 60, "iconFrame": 56, "desc": "The first real gun. 1-4 damage.", "effects": { "weapon": { "id": "laser", "name": "Laser Cannon", "kind": "beam", "min": 1, "max": 4, "shots": 1, "space": 2, "cost": 8 } }, "gnnHeadline": true },
|
{ "id": "lasercannon", "name": "Laser Cannon", "field": "weapons", "tier": 0, "prereqs": [], "cost": 60, "iconFrame": 50, "desc": "The first real gun. 1-4 damage.", "effects": { "weapon": { "id": "laser", "name": "Laser Cannon", "kind": "beam", "min": 1, "max": 4, "shots": 1, "space": 2, "cost": 8 } }, "gnnHeadline": true },
|
||||||
{ "id": "hypervrockets", "name": "Hyper-V Rockets", "field": "weapons", "tier": 1, "prereqs": ["lasercannon"], "cost": 160, "iconFrame": 57, "desc": "Cheap standoff missiles. 6 damage, 2 salvoes.", "effects": { "weapon": { "id": "hyperv", "name": "Hyper-V Rocket", "kind": "missile", "min": 6, "max": 6, "shots": 4, "space": 4, "cost": 10 } } },
|
{ "id": "hypervrockets", "name": "Hyper-V Rockets", "field": "weapons", "tier": 1, "prereqs": ["lasercannon"], "cost": 160, "iconFrame": 51, "desc": "Cheap standoff missiles. 6 damage, 2 salvoes.", "effects": { "weapon": { "id": "hyperv", "name": "Hyper-V Rocket", "kind": "missile", "min": 6, "max": 6, "shots": 4, "space": 4, "cost": 10 } } },
|
||||||
{ "id": "pulselaser", "name": "Pulse Laser", "field": "weapons", "tier": 1, "prereqs": ["lasercannon"], "cost": 160, "iconFrame": 93, "desc": "A faster-cycling beam. 2-5 damage every round, forever — no rack to run empty.", "effects": { "weapon": { "id": "pulselaser", "name": "Pulse Laser", "kind": "beam", "min": 2, "max": 5, "shots": 1, "space": 3, "cost": 9 } } },
|
{ "id": "pulselaser", "name": "Pulse Laser", "field": "weapons", "tier": 1, "prereqs": ["lasercannon"], "cost": 160, "iconFrame": 87, "desc": "A faster-cycling beam. 2-5 damage every round, forever — no rack to run empty.", "effects": { "weapon": { "id": "pulselaser", "name": "Pulse Laser", "kind": "beam", "min": 2, "max": 5, "shots": 1, "space": 3, "cost": 9 } } },
|
||||||
{ "id": "neutronpellet", "name": "Neutron Pellet Gun", "field": "weapons", "tier": 2, "prereqs": ["hypervrockets"], "cost": 430, "iconFrame": 58, "desc": "Shield-piercing pellets. 2-6 damage.", "effects": { "weapon": { "id": "pellet", "name": "Neutron Pellet Gun", "kind": "beam", "min": 2, "max": 6, "shots": 1, "space": 3, "cost": 12, "shieldPierce": 2 } } },
|
{ "id": "neutronpellet", "name": "Neutron Pellet Gun", "field": "weapons", "tier": 2, "prereqs": ["hypervrockets"], "cost": 430, "iconFrame": 52, "desc": "Shield-piercing pellets. 2-6 damage.", "effects": { "weapon": { "id": "pellet", "name": "Neutron Pellet Gun", "kind": "beam", "min": 2, "max": 6, "shots": 1, "space": 3, "cost": 12, "shieldPierce": 2 } } },
|
||||||
{ "id": "protontorpedoes", "name": "Proton Torpedoes", "field": "weapons", "tier": 2, "prereqs": ["hypervrockets"], "cost": 430, "iconFrame": 76, "desc": "Slow, but heavier warheads than Hyper-V. 10 damage, 2 salvoes.", "effects": { "weapon": { "id": "protontorpedo", "name": "Proton Torpedo", "kind": "missile", "min": 10, "max": 10, "shots": 2, "space": 5, "cost": 14 } } },
|
{ "id": "protontorpedoes", "name": "Proton Torpedoes", "field": "weapons", "tier": 2, "prereqs": ["hypervrockets"], "cost": 430, "iconFrame": 70, "desc": "Slow, but heavier warheads than Hyper-V. 10 damage, 2 salvoes.", "effects": { "weapon": { "id": "protontorpedo", "name": "Proton Torpedo", "kind": "missile", "min": 10, "max": 10, "shots": 2, "space": 5, "cost": 14 } } },
|
||||||
{ "id": "ioncannon", "name": "Ion Cannon", "field": "weapons", "tier": 3, "prereqs": ["neutronpellet"], "cost": 960, "iconFrame": 59, "desc": "3-8 damage.", "effects": { "weapon": { "id": "ion", "name": "Ion Cannon", "kind": "beam", "min": 3, "max": 8, "shots": 1, "space": 4, "cost": 18 } } },
|
{ "id": "ioncannon", "name": "Ion Cannon", "field": "weapons", "tier": 3, "prereqs": ["neutronpellet"], "cost": 960, "iconFrame": 53, "desc": "3-8 damage.", "effects": { "weapon": { "id": "ion", "name": "Ion Cannon", "kind": "beam", "min": 3, "max": 8, "shots": 1, "space": 4, "cost": 18 } } },
|
||||||
{ "id": "merculitemissiles", "name": "Merculite Missiles", "field": "weapons", "tier": 4, "prereqs": ["ioncannon"], "cost": 1950, "iconFrame": 60, "desc": "10 damage, 2 salvoes, long reach.", "effects": { "weapon": { "id": "merculite", "name": "Merculite Missile", "kind": "missile", "min": 10, "max": 10, "shots": 4, "space": 6, "cost": 20 } } },
|
{ "id": "merculitemissiles", "name": "Merculite Missiles", "field": "weapons", "tier": 4, "prereqs": ["ioncannon"], "cost": 1950, "iconFrame": 54, "desc": "10 damage, 2 salvoes, long reach.", "effects": { "weapon": { "id": "merculite", "name": "Merculite Missile", "kind": "missile", "min": 10, "max": 10, "shots": 4, "space": 6, "cost": 20 } } },
|
||||||
{ "id": "disruptorcannon", "name": "Disruptor Cannon", "field": "weapons", "tier": 4, "prereqs": ["ioncannon"], "cost": 1950, "iconFrame": 94, "desc": "Phase-shifted beam bleeds straight through deflectors. 4-9 damage, ignores 1 point of shield.", "effects": { "weapon": { "id": "disruptor", "name": "Disruptor Cannon", "kind": "beam", "min": 4, "max": 9, "shots": 1, "space": 5, "cost": 22, "shieldPierce": 1 } } },
|
{ "id": "disruptorcannon", "name": "Disruptor Cannon", "field": "weapons", "tier": 4, "prereqs": ["ioncannon"], "cost": 1950, "iconFrame": 88, "desc": "Phase-shifted beam bleeds straight through deflectors. 4-9 damage, ignores 1 point of shield.", "effects": { "weapon": { "id": "disruptor", "name": "Disruptor Cannon", "kind": "beam", "min": 4, "max": 9, "shots": 1, "space": 5, "cost": 22, "shieldPierce": 1 } } },
|
||||||
{ "id": "neutronblaster", "name": "Neutron Blaster", "field": "weapons", "tier": 5, "prereqs": ["merculitemissiles"], "cost": 3400, "iconFrame": 61, "desc": "5-12 damage and it kills crew through the hull.", "effects": { "weapon": { "id": "neutronblaster", "name": "Neutron Blaster", "kind": "beam", "min": 5, "max": 12, "shots": 1, "space": 5, "cost": 30, "shieldPierce": 1 } } },
|
{ "id": "neutronblaster", "name": "Neutron Blaster", "field": "weapons", "tier": 5, "prereqs": ["merculitemissiles"], "cost": 3400, "iconFrame": 55, "desc": "5-12 damage and it kills crew through the hull.", "effects": { "weapon": { "id": "neutronblaster", "name": "Neutron Blaster", "kind": "beam", "min": 5, "max": 12, "shots": 1, "space": 5, "cost": 30, "shieldPierce": 1 } } },
|
||||||
{ "id": "hyperxrockets", "name": "Hyper-X Rockets", "field": "weapons", "tier": 6, "prereqs": ["neutronblaster"], "cost": 5700, "iconFrame": 62, "desc": "14 damage, 3 salvoes.", "effects": { "weapon": { "id": "hyperx", "name": "Hyper-X Rocket", "kind": "missile", "min": 16, "max": 16, "shots": 5, "space": 8, "cost": 32 } } },
|
{ "id": "hyperxrockets", "name": "Hyper-X Rockets", "field": "weapons", "tier": 6, "prereqs": ["neutronblaster"], "cost": 5700, "iconFrame": 56, "desc": "14 damage, 3 salvoes.", "effects": { "weapon": { "id": "hyperx", "name": "Hyper-X Rocket", "kind": "missile", "min": 16, "max": 16, "shots": 5, "space": 8, "cost": 32 } } },
|
||||||
{ "id": "gravitonbeam", "name": "Graviton Beam", "field": "weapons", "tier": 6, "prereqs": ["neutronblaster"], "cost": 5700, "iconFrame": 77, "desc": "Warps space directly onto the target. 12-20 damage, punches through shields.", "effects": { "weapon": { "id": "gravitonbeam", "name": "Graviton Beam", "kind": "beam", "min": 12, "max": 20, "shots": 1, "space": 7, "cost": 34, "shieldPierce": 2 } } },
|
{ "id": "gravitonbeam", "name": "Graviton Beam", "field": "weapons", "tier": 6, "prereqs": ["neutronblaster"], "cost": 5700, "iconFrame": 71, "desc": "Warps space directly onto the target. 12-20 damage, punches through shields.", "effects": { "weapon": { "id": "gravitonbeam", "name": "Graviton Beam", "kind": "beam", "min": 12, "max": 20, "shots": 1, "space": 7, "cost": 34, "shieldPierce": 2 } } },
|
||||||
{ "id": "fusionbeam", "name": "Fusion Beam", "field": "weapons", "tier": 7, "prereqs": ["hyperxrockets"], "cost": 9100, "iconFrame": 63, "desc": "8-16 damage.", "effects": { "weapon": { "id": "fusionbeam", "name": "Fusion Beam", "kind": "beam", "min": 8, "max": 16, "shots": 1, "space": 6, "cost": 45 } } },
|
{ "id": "fusionbeam", "name": "Fusion Beam", "field": "weapons", "tier": 7, "prereqs": ["hyperxrockets"], "cost": 9100, "iconFrame": 57, "desc": "8-16 damage.", "effects": { "weapon": { "id": "fusionbeam", "name": "Fusion Beam", "kind": "beam", "min": 8, "max": 16, "shots": 1, "space": 6, "cost": 45 } } },
|
||||||
{ "id": "deathray", "name": "Death Ray", "field": "weapons", "tier": 8, "prereqs": ["fusionbeam"], "cost": 14200, "iconFrame": 64, "desc": "15-40 damage. Fleets evaporate.", "effects": { "weapon": { "id": "deathray", "name": "Death Ray", "kind": "beam", "min": 15, "max": 40, "shots": 1, "space": 12, "cost": 90 } }, "gnnHeadline": true },
|
{ "id": "deathray", "name": "Death Ray", "field": "weapons", "tier": 8, "prereqs": ["fusionbeam"], "cost": 14200, "iconFrame": 58, "desc": "15-40 damage. Fleets evaporate.", "effects": { "weapon": { "id": "deathray", "name": "Death Ray", "kind": "beam", "min": 15, "max": 40, "shots": 1, "space": 12, "cost": 90 } }, "gnnHeadline": true },
|
||||||
{ "id": "antimattertorpedoes","name": "Antimatter Torpedoes", "field": "weapons", "tier": 8, "prereqs": ["fusionbeam"], "cost": 14200, "iconFrame": 95, "desc": "Warhead payloads with a dozen years of R&D behind them. 22 damage, 4 salvoes, then the racks are empty.", "effects": { "weapon": { "id": "antimattertorpedo", "name": "Antimatter Torpedo", "kind": "missile", "min": 22, "max": 22, "shots": 4, "space": 10, "cost": 70 } } },
|
{ "id": "antimattertorpedoes","name": "Antimatter Torpedoes", "field": "weapons", "tier": 8, "prereqs": ["fusionbeam"], "cost": 14200, "iconFrame": 89, "desc": "Warhead payloads with a dozen years of R&D behind them. 22 damage, 4 salvoes, then the racks are empty.", "effects": { "weapon": { "id": "antimattertorpedo", "name": "Antimatter Torpedo", "kind": "missile", "min": 22, "max": 22, "shots": 4, "space": 10, "cost": 70 } } },
|
||||||
{ "id": "disintegratorbeam", "name": "Disintegrator Beam", "field": "weapons", "tier": 8, "prereqs": ["fusionbeam"], "cost": 14200, "iconFrame": 96, "desc": "A narrower beam than Death Ray, but tuned to punch clean through deflector harmonics. 10-22 damage, ignores 3 points of shield.", "effects": { "weapon": { "id": "disintegratorbeam", "name": "Disintegrator Beam", "kind": "beam", "min": 10, "max": 22, "shots": 1, "space": 9, "cost": 60, "shieldPierce": 3 } } },
|
{ "id": "disintegratorbeam", "name": "Disintegrator Beam", "field": "weapons", "tier": 8, "prereqs": ["fusionbeam"], "cost": 14200, "iconFrame": 90, "desc": "A narrower beam than Death Ray, but tuned to punch clean through deflector harmonics. 10-22 damage, ignores 3 points of shield.", "effects": { "weapon": { "id": "disintegratorbeam", "name": "Disintegrator Beam", "kind": "beam", "min": 10, "max": 22, "shots": 1, "space": 9, "cost": 60, "shieldPierce": 3 } } },
|
||||||
{ "id": "stellarconverter", "name": "Stellar Converter", "field": "weapons", "tier": 9, "prereqs": ["deathray"], "cost": 21000, "iconFrame": 65, "desc": "30-70 damage, and it can crack a colony from orbit.", "effects": { "weapon": { "id": "stellarconverter", "name": "Stellar Converter", "kind": "beam", "min": 30, "max": 70, "shots": 1, "space": 20, "cost": 150, "planetCracker": true } }, "gnnHeadline": true }
|
{ "id": "stellarconverter", "name": "Stellar Converter", "field": "weapons", "tier": 9, "prereqs": ["deathray"], "cost": 21000, "iconFrame": 59, "desc": "30-70 damage, and it can crack a colony from orbit.", "effects": { "weapon": { "id": "stellarconverter", "name": "Stellar Converter", "kind": "beam", "min": 30, "max": 70, "shots": 1, "space": 20, "cost": 150, "planetCracker": true } }, "gnnHeadline": true }
|
||||||
],
|
],
|
||||||
|
|
||||||
"_hullsReadme": "Preset ship classes — there is no ship designer. space is the weapon capacity a Mark refit fills with the best available weapon; VegaShips.js derives every other stat from the owner's researched tech. sizeScale/sizeSpeedMult/turnRateBase (degrees/round)/brakeSeconds are only consumed by the VegaCombatV2 per-ship prototype (behind ?movsim's Live/V2 toggle) — the live engine (VegaCombat.js) ignores them entirely. brakeSeconds is how long (seconds) a hull takes to kill its own top speed under max thrust — small relative to a hull's speed means it can actually decelerate and hold position within weapon range (battleship/cruiser); large relative to speed means its stopping distance exceeds beamRange and it physically cannot stop before reaching a target, so it blows through and loops back for another pass instead (frigate/scout) — see VegaCombatV2.js's computeShipMove.",
|
"_hullsReadme": "Preset ship classes — there is no ship designer. space is the weapon capacity a Mark refit fills with the best available weapon; VegaShips.js derives every other stat from the owner's researched tech. sizeScale/sizeSpeedMult/turnRateBase (degrees/round)/brakeSeconds are only consumed by the VegaCombatV2 per-ship prototype (behind ?movsim's Live/V2 toggle) — the live engine (VegaCombat.js) ignores them entirely. brakeSeconds is how long (seconds) a hull takes to kill its own top speed under max thrust — small relative to a hull's speed means it can actually decelerate and hold position within weapon range (battleship/cruiser); large relative to speed means its stopping distance exceeds beamRange and it physically cannot stop before reaching a target, so it blows through and loops back for another pass instead (frigate/scout) — see VegaCombatV2.js's computeShipMove.",
|
||||||
|
|
@ -434,10 +434,9 @@
|
||||||
"aiProposeChance": 0.25
|
"aiProposeChance": 0.25
|
||||||
},
|
},
|
||||||
"espionage": {
|
"espionage": {
|
||||||
"_readme": "Sabotage magnitude and its attitude sting when a runEspionage roll succeeds with mission 'sabotage' (VegaLogic.js). Targeting/frequency/odds are unchanged from the steal-tech path; this only controls what a sabotage success does.",
|
"_readme": "Sabotage magnitude when a runEspionage roll succeeds with mission 'sabotage' (VegaLogic.js). Targeting/frequency/odds are unchanged from the steal-tech path; this only controls what a sabotage success does. A successful mission (sabotage or tech theft) carries no attitude penalty — nobody publicly knows who did it, so there's nobody for the victim to be angry at; getting CAUGHT is the one outcome that still costs relations.",
|
||||||
"sabotageFactoriesFraction": 0.25,
|
"sabotageFactoriesFraction": 0.25,
|
||||||
"sabotageDefenseFraction": 0.25,
|
"sabotageDefenseFraction": 0.25
|
||||||
"sabotageAttitudePenalty": 14
|
|
||||||
},
|
},
|
||||||
"borderTension": {
|
"borderTension": {
|
||||||
"colonyRange": 6,
|
"colonyRange": 6,
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@
|
||||||
|
|
||||||
import { empireColonies, colonyProduction, empireFleetPower } from './VegaLogic.js';
|
import { empireColonies, colonyProduction, empireFleetPower } from './VegaLogic.js';
|
||||||
import { pickLine } from './VegaChat.js';
|
import { pickLine } from './VegaChat.js';
|
||||||
|
import { attitudeOf, moodOf } from './VegaDiplomacy.js';
|
||||||
|
|
||||||
// The seven event types GNN ever turns into a story page. Council/election
|
// The nine event types GNN ever turns into a story page. Council/election
|
||||||
// events are deliberately excluded — Brian is handling that separately.
|
// events are deliberately excluded — Brian is handling that separately.
|
||||||
export const GNN_STORY_TYPES = new Set([
|
export const GNN_STORY_TYPES = new Set([
|
||||||
'warDeclared', 'peace', 'alliance', 'techDone', 'lastColony', 'eliminated', 'spyCaught',
|
'warDeclared', 'peace', 'alliance', 'techDone', 'lastColony', 'eliminated', 'spyCaught',
|
||||||
|
'sabotage', 'techStolen',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,22 +25,35 @@ export const GNN_STORY_TYPES = new Set([
|
||||||
* only research-sourced completions (never espionage-stolen or traded techs
|
* only research-sourced completions (never espionage-stolen or traded techs
|
||||||
* — nobody can headline a tech nobody publicly knows was taken) count, and
|
* — nobody can headline a tech nobody publicly knows was taken) count, and
|
||||||
* only techs hand-flagged `gnnHeadline` in the rules data count as
|
* only techs hand-flagged `gnnHeadline` in the rules data count as
|
||||||
* "breakthrough" news; the other ~75 techs stay ordinary. `sabotage`/
|
* "breakthrough" news; the other ~75 techs stay ordinary.
|
||||||
* `techStolen` are deliberately NOT in GNN_STORY_TYPES at all — a spy
|
*
|
||||||
* mission that *succeeds* leaves nobody publicly able to name the culprit;
|
* `sabotage`/`techStolen` are visibility-gated on the VIEWER (the human),
|
||||||
* only `spyCaught` ("exposed") is ever public.
|
* since a successful mission never reveals who did it (see
|
||||||
|
* describeGnnStory's `attributed` flag) but who gets to hear about it at all
|
||||||
|
* still depends on who's watching: the perpetrator and the victim always
|
||||||
|
* know (it happened to/because of them), but a true third party only picks
|
||||||
|
* up the rumor if they're in contact with the victim AND on decent terms
|
||||||
|
* with them (Brian's ask — hostile/cold empires don't share gossip). This is
|
||||||
|
* why isGnnStory needs `state`, unlike every other story type here.
|
||||||
*/
|
*/
|
||||||
export function isGnnStory(rules, ev) {
|
export function isGnnStory(rules, state, ev) {
|
||||||
if (!GNN_STORY_TYPES.has(ev.type)) return false;
|
if (!GNN_STORY_TYPES.has(ev.type)) return false;
|
||||||
if (ev.type === 'techDone') {
|
if (ev.type === 'techDone') {
|
||||||
return ev.source === 'research' && !!rules.techs[ev.techId]?.gnnHeadline;
|
return ev.source === 'research' && !!rules.techs[ev.techId]?.gnnHeadline;
|
||||||
}
|
}
|
||||||
|
if (ev.type === 'sabotage' || ev.type === 'techStolen') {
|
||||||
|
const me = state.humanIndex;
|
||||||
|
if (ev.empire === me || ev.target === me) return true;
|
||||||
|
if (!state.empires[me]?.contacted?.[ev.target]) return false;
|
||||||
|
const mood = moodOf(attitudeOf(state, me, ev.target));
|
||||||
|
return mood !== 'hostile' && mood !== 'cold';
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Every GNN-worthy event not yet turned into a story, in chronological order. */
|
/** Every GNN-worthy event not yet turned into a story, in chronological order. */
|
||||||
export function pendingGnnStories(rules, state) {
|
export function pendingGnnStories(rules, state) {
|
||||||
return state.events.filter((ev) => !ev.gnnAnnounced && isGnnStory(rules, ev));
|
return state.events.filter((ev) => !ev.gnnAnnounced && isGnnStory(rules, state, ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replay buffer for the on-demand HUD button when there's nothing new to
|
// Replay buffer for the on-demand HUD button when there's nothing new to
|
||||||
|
|
@ -112,6 +127,34 @@ export function describeGnnStory(rules, state, ev) {
|
||||||
headline: `${name(ev.empire)} agents caught red-handed targeting ${name(ev.target)}.`,
|
headline: `${name(ev.empire)} agents caught red-handed targeting ${name(ev.target)}.`,
|
||||||
sub: ev.mission === 'sabotage' ? 'Mission: sabotage.' : 'Mission: technology theft.',
|
sub: ev.mission === 'sabotage' ? 'Mission: sabotage.' : 'Mission: technology theft.',
|
||||||
};
|
};
|
||||||
|
// A SUCCESSFUL mission, unlike spyCaught above, never names a culprit —
|
||||||
|
// `attributed` is only true when the viewing human IS the perpetrator
|
||||||
|
// (isGnnStory's own contact/attitude gate is what decides whether a true
|
||||||
|
// third party ever sees this at all; the victim always does, same
|
||||||
|
// anonymous copy as a bystander since even the victim's own GNN never
|
||||||
|
// outs the thief — VegaTurnReport.js's ticker log is where the victim's
|
||||||
|
// *personal* briefing already names them in full).
|
||||||
|
case 'sabotage': {
|
||||||
|
const attributed = ev.empire === state.humanIndex;
|
||||||
|
return {
|
||||||
|
type: ev.type, kind: 'espionageResult', accent: 'espionage', attributed,
|
||||||
|
perpetrator: ev.empire, victim: ev.target,
|
||||||
|
headline: attributed
|
||||||
|
? `${name(ev.empire)} agents sabotage ${name(ev.target)}'s colony, destroying ${ev.factoriesLost} factories and ${ev.defenseLost} defense.`
|
||||||
|
: `${name(ev.target)} report a colony crippled by an unknown saboteur.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'techStolen': {
|
||||||
|
const attributed = ev.empire === state.humanIndex;
|
||||||
|
const techName = rules.techs[ev.techId]?.name ?? 'a technology';
|
||||||
|
return {
|
||||||
|
type: ev.type, kind: 'espionageResult', accent: 'espionage', attributed,
|
||||||
|
perpetrator: ev.empire, victim: ev.target, techId: ev.techId,
|
||||||
|
headline: attributed
|
||||||
|
? `${name(ev.empire)} agents successfully steal ${techName} from ${name(ev.target)}.`
|
||||||
|
: `${name(ev.target)} believe their technology '${techName}' has been stolen.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
default: return { type: ev.type, kind: 'unknown', accent: 'tech', headline: ev.type };
|
default: return { type: ev.type, kind: 'unknown', accent: 'tech', headline: ev.type };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,6 +290,16 @@ const ESPIONAGE_LINES = [
|
||||||
'Sources confirm {perp} spies caught attempting {mission} against {victim}.',
|
'Sources confirm {perp} spies caught attempting {mission} against {victim}.',
|
||||||
'Word from Galactic Security: {perp} agents apprehended mid-{mission} against {victim}.',
|
'Word from Galactic Security: {perp} agents apprehended mid-{mission} against {victim}.',
|
||||||
];
|
];
|
||||||
|
const ESPIONAGE_SUCCESS_LINES = [
|
||||||
|
'This just in, {perp} agents strike a covert blow against {victim}.',
|
||||||
|
'Breaking: {victim} left reeling after a covert operation by {perp}.',
|
||||||
|
'Sources confirm {perp} intelligence services scored a hit against {victim} tonight.',
|
||||||
|
];
|
||||||
|
const ESPIONAGE_RUMOR_LINES = [
|
||||||
|
'Rumors swirl tonight of an unexplained security breach in {victim} space.',
|
||||||
|
'Word on the street: {victim} suffered a covert attack of unknown origin.',
|
||||||
|
'Sources close to {victim} report foul play, though no culprit has been named.',
|
||||||
|
];
|
||||||
const RANKING_LINES = {
|
const RANKING_LINES = {
|
||||||
population: [
|
population: [
|
||||||
'This just in, {leader} population reaches new galactic heights at {value}.',
|
'This just in, {leader} population reaches new galactic heights at {value}.',
|
||||||
|
|
@ -315,6 +368,10 @@ function anchorLineForStory(rules, state, desc) {
|
||||||
const mission = desc.mission === 'sabotage' ? 'sabotage' : 'technology theft';
|
const mission = desc.mission === 'sabotage' ? 'sabotage' : 'technology theft';
|
||||||
return pickLine(ESPIONAGE_LINES, { perp: empName(desc.perpetrator), victim: empName(desc.victim), mission });
|
return pickLine(ESPIONAGE_LINES, { perp: empName(desc.perpetrator), victim: empName(desc.victim), mission });
|
||||||
}
|
}
|
||||||
|
case 'espionageResult': {
|
||||||
|
const vars = { perp: empName(desc.perpetrator), victim: empName(desc.victim) };
|
||||||
|
return pickLine(desc.attributed ? ESPIONAGE_SUCCESS_LINES : ESPIONAGE_RUMOR_LINES, vars);
|
||||||
|
}
|
||||||
default: return desc.headline ?? '';
|
default: return desc.headline ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,54 @@ function renderEspionageStory(scene, rules, state, art, container, desc) {
|
||||||
addHeadline(scene, container, cy + size / 2 + 116, desc.sub, '#ff9fd8', '20px');
|
addHeadline(scene, container, cy + size / 2 + 116, desc.sub, '#ff9fd8', '20px');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A SUCCESSFUL sabotage/theft — unlike renderEspionageStory above (a CAUGHT
|
||||||
|
// spy, perpetrator named and shamed), the culprit is never shown here except
|
||||||
|
// when the viewing human IS the culprit (desc.attributed). Anonymous case
|
||||||
|
// reuses `renderTerritoryStory`'s single-portrait-plus-vignette read but with
|
||||||
|
// a "?" mark standing in for the unknown hand behind it, rather than an
|
||||||
|
// attacker portrait.
|
||||||
|
function renderEspionageResultStory(scene, rules, state, art, container, desc) {
|
||||||
|
const color = STORY_COLORS.espionage;
|
||||||
|
const victim = state.empires[desc.victim];
|
||||||
|
|
||||||
|
if (desc.attributed) {
|
||||||
|
const perp = state.empires[desc.perpetrator];
|
||||||
|
const size = 380;
|
||||||
|
const cy = 70 + size / 2;
|
||||||
|
const leftCx = 60 + size / 2;
|
||||||
|
const rightCx = RIGHT_W - 60 - size / 2;
|
||||||
|
addPortraitBlock(scene, rules, art, container, leftCx, cy, size, color, perp.speciesId, perp.name, perp.color);
|
||||||
|
addPortraitBlock(scene, rules, art, container, rightCx, cy, size, color, victim.speciesId, victim.name, victim.color);
|
||||||
|
const g = scene.add.graphics();
|
||||||
|
g.lineStyle(4, color, 0.85);
|
||||||
|
g.lineBetween(leftCx + size / 2, cy, rightCx - size / 2, cy);
|
||||||
|
container.add(g);
|
||||||
|
const verb = scene.add.text((leftCx + rightCx) / 2, cy, desc.type === 'sabotage' ? 'SABOTAGED' : 'STOLEN', {
|
||||||
|
fontFamily: FONT, fontSize: '24px', color: hex(color), fontStyle: 'bold',
|
||||||
|
}).setOrigin(0.5);
|
||||||
|
container.add(verb);
|
||||||
|
addHeadline(scene, container, cy + size / 2 + 70, desc.headline);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = 420;
|
||||||
|
const cx = RIGHT_W / 2;
|
||||||
|
const cy = 60 + size / 2;
|
||||||
|
storyFrame(scene, container, cx - size / 2, cy - size / 2, size, size, color);
|
||||||
|
const portrait = makeSpeciesPortrait(scene, rules, art, victim.speciesId, cx, cy, size - 20);
|
||||||
|
container.add(portrait);
|
||||||
|
container.add(scene.add.rectangle(cx, cy, size - 20, size - 20, 0x000000, 0.35));
|
||||||
|
container.add(scene.add.text(cx, cy + size / 2 + 16, victim.name, {
|
||||||
|
fontFamily: FONT, fontSize: '22px', color: victim.color,
|
||||||
|
}).setOrigin(0.5, 0));
|
||||||
|
// The unknown culprit — a stark "?" in the corner rather than an attacker
|
||||||
|
// portrait, since there's nobody to name.
|
||||||
|
container.add(scene.add.text(cx + size / 2 - 34, cy - size / 2 + 34, '?', {
|
||||||
|
fontFamily: FONT, fontSize: '52px', color: hex(color), fontStyle: 'bold',
|
||||||
|
}).setOrigin(0.5));
|
||||||
|
addHeadline(scene, container, cy + size / 2 + 70, desc.headline, '#c7d3e0');
|
||||||
|
}
|
||||||
|
|
||||||
function renderUnknownStory(scene, rules, state, art, container, desc) {
|
function renderUnknownStory(scene, rules, state, art, container, desc) {
|
||||||
addHeadline(scene, container, RIGHT_H / 2 - 20, desc.headline ?? 'Unknown event.');
|
addHeadline(scene, container, RIGHT_H / 2 - 20, desc.headline ?? 'Unknown event.');
|
||||||
}
|
}
|
||||||
|
|
@ -259,6 +307,7 @@ const STORY_RENDERERS = {
|
||||||
tech: renderTechStory,
|
tech: renderTechStory,
|
||||||
territory: renderTerritoryStory,
|
territory: renderTerritoryStory,
|
||||||
espionage: renderEspionageStory,
|
espionage: renderEspionageStory,
|
||||||
|
espionageResult: renderEspionageResultStory,
|
||||||
unknown: renderUnknownStory,
|
unknown: renderUnknownStory,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1016,6 +1016,10 @@ function runEspionage(rules, state, e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A SUCCESSFUL mission carries no attitude penalty (Brian's ask) — nobody
|
||||||
|
// publicly knows who did it (see VegaGnn.js's `attributed` flag), so there
|
||||||
|
// is nobody for the victim to be angry AT. Getting CAUGHT above is the one
|
||||||
|
// espionage outcome that still costs relations, since that one names names.
|
||||||
if (mission === 'sabotage') {
|
if (mission === 'sabotage') {
|
||||||
const targetColonies = empireColonies(state, target.idx);
|
const targetColonies = empireColonies(state, target.idx);
|
||||||
if (!targetColonies.length) return;
|
if (!targetColonies.length) return;
|
||||||
|
|
@ -1029,7 +1033,6 @@ function runEspionage(rules, state, e) {
|
||||||
type: 'sabotage', empire: e, target: target.idx, starIdx: colony.starIdx,
|
type: 'sabotage', empire: e, target: target.idx, starIdx: colony.starIdx,
|
||||||
factoriesLost, defenseLost, turn: state.turn,
|
factoriesLost, defenseLost, turn: state.turn,
|
||||||
});
|
});
|
||||||
target.attitude[e] = Math.max(-100, Math.min(100, (target.attitude[e] ?? 0) - cfg.sabotageAttitudePenalty));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1038,7 +1041,6 @@ function runEspionage(rules, state, e) {
|
||||||
const tech = stealable[randInt(state, stealable.length)];
|
const tech = stealable[randInt(state, stealable.length)];
|
||||||
grantTech(rules, state, e, tech.id, 'espionage');
|
grantTech(rules, state, e, tech.id, 'espionage');
|
||||||
pushEvent(state, { type: 'techStolen', empire: e, target: target.idx, techId: tech.id, turn: state.turn });
|
pushEvent(state, { type: 'techStolen', empire: e, target: target.idx, techId: tech.id, turn: state.turn });
|
||||||
target.attitude[e] = Math.max(-100, Math.min(100, (target.attitude[e] ?? 0) - 8));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recomputeTotals(rules, state) {
|
function recomputeTotals(rules, state) {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ export function compileRules(json) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const techFields = byId(json.techFields, 'techField');
|
const techFields = byId(json.techFields, 'techField');
|
||||||
|
// Field icons live on their own sheet (data/mastervega-artwork.json's
|
||||||
|
// "techfields", split from the individual-tech "techicons" sheet), so
|
||||||
|
// validated the same way techs' own iconFrame is below.
|
||||||
|
for (const f of json.techFields) {
|
||||||
|
need(Number.isInteger(f.iconFrame) && f.iconFrame >= 0, `techField ${f.id} bad iconFrame`);
|
||||||
|
}
|
||||||
const techs = byId(json.techs, 'tech');
|
const techs = byId(json.techs, 'tech');
|
||||||
const hulls = byId(json.hulls, 'hull');
|
const hulls = byId(json.hulls, 'hull');
|
||||||
const buildings = byId(json.buildings, 'building');
|
const buildings = byId(json.buildings, 'building');
|
||||||
|
|
|
||||||
|
|
@ -295,19 +295,39 @@ Enrichment Facility.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. `techicons` — ⬜ `assets/images/vega/techicons.png`
|
## 7a. `techfields` — ⬜ `assets/images/vega/techfields.png`
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Sheet size | **480 × 336** |
|
| Sheet size | **144 × 96** |
|
||||||
| Frame | **48 × 48** |
|
| Frame | **48 × 48** |
|
||||||
| Grid | 10 cols × 7 rows = 70 frames |
|
| Grid | 3 cols × 2 rows = 6 frames |
|
||||||
|
|
||||||
- Frames **0–5**: the six tech *fields* — Computers, Construction, Force Fields,
|
One frame per tech *field* (`techFields[].iconFrame`), rules order: Computers,
|
||||||
Planetology, Propulsion, Weapons.
|
Construction, Force Fields, Planetology, Propulsion, Weapons. Exact fit, no
|
||||||
- Frames **6–65**: the sixty individual techs, in the order they appear in
|
spares.
|
||||||
`data/mastervega-rules.json` (`techs[].iconFrame`).
|
|
||||||
- Frames 66–69: spare.
|
Split out of the old combined `techicons` sheet (§7b) so the six field icons
|
||||||
|
— larger, simpler glyphs meant to read at a glance on the research screen —
|
||||||
|
can be painted and sized independently of the ~90 individual tech icons.
|
||||||
|
Nothing in the game draws a field icon yet; this sheet exists for whenever
|
||||||
|
that lands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7b. `techicons` — ⬜ `assets/images/vega/techicons.png`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Sheet size | **480 × 480** |
|
||||||
|
| Frame | **48 × 48** |
|
||||||
|
| Grid | 10 cols × 10 rows = 100 frames |
|
||||||
|
|
||||||
|
One frame per individual tech (`techs[].iconFrame`), 0–90, in the order techs
|
||||||
|
appear in `data/mastervega-rules.json` — the original 60 plus two later
|
||||||
|
MOO1-branching passes. 91–99 spare (the tech list has grown once already;
|
||||||
|
expect it to again). Drawn today by `VegaGnnScreen.js`'s tech-breakthrough
|
||||||
|
story page.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,15 @@ section('1. Rules integrity');
|
||||||
const techCap = techSheet.cols * techSheet.rows;
|
const techCap = techSheet.cols * techSheet.rows;
|
||||||
check('tech icon frames fit the sheet', Math.max(...techFrames) < techCap,
|
check('tech icon frames fit the sheet', Math.max(...techFrames) < techCap,
|
||||||
`max ${Math.max(...techFrames)} of ${techCap}`);
|
`max ${Math.max(...techFrames)} of ${techCap}`);
|
||||||
|
// Field icons split onto their own sheet (data/mastervega-artwork.json's
|
||||||
|
// "techfields") — same shape checks as the tech icons above.
|
||||||
|
const fieldFrames = RULES.techFieldList.map((f) => f.iconFrame);
|
||||||
|
check('tech field icon frames unique', new Set(fieldFrames).size === fieldFrames.length);
|
||||||
|
const fieldSheet = artJson.sheets.techfields;
|
||||||
|
const fieldCap = fieldSheet.cols * fieldSheet.rows;
|
||||||
|
check('tech field icon frames fit their own sheet', Math.max(...fieldFrames) < fieldCap,
|
||||||
|
`max ${Math.max(...fieldFrames)} of ${fieldCap}`);
|
||||||
|
check('tech field icons and tech icons are on separate sheets', fieldSheet.key !== techSheet.key);
|
||||||
const buildFrames = RULES.buildingList.map((b) => b.frame);
|
const buildFrames = RULES.buildingList.map((b) => b.frame);
|
||||||
check('building frames unique', new Set(buildFrames).size === buildFrames.length);
|
check('building frames unique', new Set(buildFrames).size === buildFrames.length);
|
||||||
check('building frames fit the sheet',
|
check('building frames fit the sheet',
|
||||||
|
|
@ -2736,44 +2745,70 @@ section('7b. Galactic News Network');
|
||||||
{
|
{
|
||||||
// --- classification: isGnnStory / pendingGnnStories, hand-built events so
|
// --- classification: isGnnStory / pendingGnnStories, hand-built events so
|
||||||
// every branch (research+headline, espionage-sourced, non-headline,
|
// every branch (research+headline, espionage-sourced, non-headline,
|
||||||
// silent-sabotage, spyCaught, already-announced) is exercised directly
|
// attributed/anonymous sabotage+theft, spyCaught, already-announced) is
|
||||||
// rather than waiting for a soak to happen to produce one of each.
|
// exercised directly rather than waiting for a soak to happen to produce
|
||||||
|
// one of each.
|
||||||
{
|
{
|
||||||
const headlineTech = 'lasercannon'; // gnnHeadline: true, tier 0 weapons
|
const headlineTech = 'lasercannon'; // gnnHeadline: true, tier 0 weapons
|
||||||
const plainTech = 'battlescanner'; // not flagged
|
const plainTech = 'battlescanner'; // not flagged
|
||||||
check('fixture headline tech is actually flagged', RULES.techs[headlineTech]?.gnnHeadline === true);
|
check('fixture headline tech is actually flagged', RULES.techs[headlineTech]?.gnnHeadline === true);
|
||||||
check('fixture plain tech is not flagged', !RULES.techs[plainTech]?.gnnHeadline);
|
check('fixture plain tech is not flagged', !RULES.techs[plainTech]?.gnnHeadline);
|
||||||
|
|
||||||
|
// humanIndex 0. Empire 3 is a bystander the human knows and likes, 4 a
|
||||||
|
// bystander the human knows and despises, 5 a bystander never met at
|
||||||
|
// all — exercises isGnnStory's contact+attitude gate for a successful
|
||||||
|
// mission that doesn't touch the human directly.
|
||||||
|
const fakeState = {
|
||||||
|
humanIndex: 0,
|
||||||
|
empires: [{ idx: 0, contacted: { 3: true, 4: true }, attitude: { 3: 10, 4: -80 } }],
|
||||||
|
events: [],
|
||||||
|
};
|
||||||
|
|
||||||
const evResearchHeadline = { type: 'techDone', empire: 0, techId: headlineTech, source: 'research' };
|
const evResearchHeadline = { type: 'techDone', empire: 0, techId: headlineTech, source: 'research' };
|
||||||
const evEspionageHeadline = { type: 'techDone', empire: 0, techId: headlineTech, source: 'espionage' };
|
const evEspionageHeadline = { type: 'techDone', empire: 0, techId: headlineTech, source: 'espionage' };
|
||||||
const evResearchPlain = { type: 'techDone', empire: 0, techId: plainTech, source: 'research' };
|
const evResearchPlain = { type: 'techDone', empire: 0, techId: plainTech, source: 'research' };
|
||||||
const evSabotage = { type: 'sabotage', empire: 0, target: 1, starIdx: 0 };
|
const evSabotageMine = { type: 'sabotage', empire: 0, target: 1, starIdx: 0, factoriesLost: 2, defenseLost: 3 };
|
||||||
const evTechStolen = { type: 'techStolen', empire: 0, target: 1, techId: headlineTech };
|
const evTechStolenMine = { type: 'techStolen', empire: 0, target: 1, techId: headlineTech };
|
||||||
|
const evTechStolenVictimIsMe = { type: 'techStolen', empire: 1, target: 0, techId: headlineTech };
|
||||||
|
const evTechStolenBystanderFriendly = { type: 'techStolen', empire: 1, target: 3, techId: headlineTech };
|
||||||
|
const evTechStolenBystanderHostile = { type: 'techStolen', empire: 1, target: 4, techId: headlineTech };
|
||||||
|
const evTechStolenBystanderUnknown = { type: 'techStolen', empire: 1, target: 5, techId: headlineTech };
|
||||||
const evSpyCaught = { type: 'spyCaught', empire: 0, target: 1, mission: 'sabotage' };
|
const evSpyCaught = { type: 'spyCaught', empire: 0, target: 1, mission: 'sabotage' };
|
||||||
const evPeace = { type: 'peace', empire: 0, other: 1 };
|
const evPeace = { type: 'peace', empire: 0, other: 1 };
|
||||||
const evWarAlreadyAnnounced = { type: 'warDeclared', empire: 0, other: 1, gnnAnnounced: true };
|
const evWarAlreadyAnnounced = { type: 'warDeclared', empire: 0, other: 1, gnnAnnounced: true };
|
||||||
|
|
||||||
check('research-sourced headline tech is a GNN story', Gnn.isGnnStory(RULES, evResearchHeadline));
|
check('research-sourced headline tech is a GNN story', Gnn.isGnnStory(RULES, fakeState, evResearchHeadline));
|
||||||
check('espionage-sourced headline tech is NOT a GNN story (nobody can publicly name the thief\'s target)',
|
check('espionage-sourced headline tech is NOT a GNN story (nobody can publicly name the thief\'s target)',
|
||||||
!Gnn.isGnnStory(RULES, evEspionageHeadline));
|
!Gnn.isGnnStory(RULES, fakeState, evEspionageHeadline));
|
||||||
check('research-sourced non-headline tech is NOT a GNN story', !Gnn.isGnnStory(RULES, evResearchPlain));
|
check('research-sourced non-headline tech is NOT a GNN story', !Gnn.isGnnStory(RULES, fakeState, evResearchPlain));
|
||||||
check('a successful sabotage is never a GNN story (silent theft is unattributable)',
|
check('the player\'s own successful sabotage IS a GNN story, attributed to them',
|
||||||
!Gnn.isGnnStory(RULES, evSabotage));
|
Gnn.isGnnStory(RULES, fakeState, evSabotageMine));
|
||||||
check('a successful tech theft is never a GNN story', !Gnn.isGnnStory(RULES, evTechStolen));
|
check('the player\'s own successful tech theft IS a GNN story, attributed to them',
|
||||||
check('a caught spy ("exposed") is a GNN story', Gnn.isGnnStory(RULES, evSpyCaught));
|
Gnn.isGnnStory(RULES, fakeState, evTechStolenMine));
|
||||||
check('peace is a GNN story', Gnn.isGnnStory(RULES, evPeace));
|
check('the player being victimized IS a GNN story regardless of contact/attitude',
|
||||||
|
Gnn.isGnnStory(RULES, fakeState, evTechStolenVictimIsMe));
|
||||||
|
check('a bystander in contact and on decent terms with the victim hears the rumor',
|
||||||
|
Gnn.isGnnStory(RULES, fakeState, evTechStolenBystanderFriendly));
|
||||||
|
check('a bystander hostile/cold toward the victim does not hear the rumor',
|
||||||
|
!Gnn.isGnnStory(RULES, fakeState, evTechStolenBystanderHostile));
|
||||||
|
check('a bystander with no contact with the victim does not hear the rumor',
|
||||||
|
!Gnn.isGnnStory(RULES, fakeState, evTechStolenBystanderUnknown));
|
||||||
|
check('a caught spy ("exposed") is a GNN story', Gnn.isGnnStory(RULES, fakeState, evSpyCaught));
|
||||||
|
check('peace is a GNN story', Gnn.isGnnStory(RULES, fakeState, evPeace));
|
||||||
|
|
||||||
const fakeState = {
|
fakeState.events = [
|
||||||
events: [
|
evResearchHeadline, evEspionageHeadline, evResearchPlain, evSabotageMine, evTechStolenMine,
|
||||||
evResearchHeadline, evEspionageHeadline, evResearchPlain, evSabotage,
|
evTechStolenVictimIsMe, evTechStolenBystanderFriendly, evTechStolenBystanderHostile,
|
||||||
evTechStolen, evSpyCaught, evPeace, evWarAlreadyAnnounced,
|
evTechStolenBystanderUnknown, evSpyCaught, evPeace, evWarAlreadyAnnounced,
|
||||||
],
|
];
|
||||||
};
|
|
||||||
const pending = Gnn.pendingGnnStories(RULES, fakeState);
|
const pending = Gnn.pendingGnnStories(RULES, fakeState);
|
||||||
check('pendingGnnStories excludes an already-gnnAnnounced event', !pending.includes(evWarAlreadyAnnounced));
|
check('pendingGnnStories excludes an already-gnnAnnounced event', !pending.includes(evWarAlreadyAnnounced));
|
||||||
|
const expectedPending = [
|
||||||
|
evResearchHeadline, evSabotageMine, evTechStolenMine, evTechStolenVictimIsMe,
|
||||||
|
evTechStolenBystanderFriendly, evSpyCaught, evPeace,
|
||||||
|
];
|
||||||
check('pendingGnnStories returns exactly the expected set',
|
check('pendingGnnStories returns exactly the expected set',
|
||||||
pending.length === 3 && pending.includes(evResearchHeadline)
|
pending.length === expectedPending.length && expectedPending.every((e) => pending.includes(e)),
|
||||||
&& pending.includes(evSpyCaught) && pending.includes(evPeace),
|
|
||||||
`got ${pending.length}: ${pending.map((e) => e.type).join(',')}`);
|
`got ${pending.length}: ${pending.map((e) => e.type).join(',')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2819,6 +2854,10 @@ section('7b. Galactic News Network');
|
||||||
{ type: 'eliminated', empire: 1, attacker: 0 },
|
{ type: 'eliminated', empire: 1, attacker: 0 },
|
||||||
{ type: 'eliminated', empire: 1, attacker: -1 },
|
{ type: 'eliminated', empire: 1, attacker: -1 },
|
||||||
{ type: 'spyCaught', empire: 0, target: 1, mission: 'sabotage' },
|
{ type: 'spyCaught', empire: 0, target: 1, mission: 'sabotage' },
|
||||||
|
{ type: 'sabotage', empire: 0, target: 1, starIdx: 0, factoriesLost: 2, defenseLost: 3 },
|
||||||
|
{ type: 'sabotage', empire: 1, target: 0, starIdx: 0, factoriesLost: 2, defenseLost: 3 },
|
||||||
|
{ type: 'techStolen', empire: 0, target: 1, techId: 'lasercannon' },
|
||||||
|
{ type: 'techStolen', empire: 1, target: 0, techId: 'lasercannon' },
|
||||||
];
|
];
|
||||||
for (const ev of sampleEvents) {
|
for (const ev of sampleEvents) {
|
||||||
const desc = Gnn.describeGnnStory(RULES, stD, ev);
|
const desc = Gnn.describeGnnStory(RULES, stD, ev);
|
||||||
|
|
@ -2830,6 +2869,18 @@ section('7b. Galactic News Network');
|
||||||
check('describeGnnStory names the attacker when known',
|
check('describeGnnStory names the attacker when known',
|
||||||
Gnn.describeGnnStory(RULES, stD, { type: 'eliminated', empire: 1, attacker: 0 })
|
Gnn.describeGnnStory(RULES, stD, { type: 'eliminated', empire: 1, attacker: 0 })
|
||||||
.headline.includes(stD.empires[0].name));
|
.headline.includes(stD.empires[0].name));
|
||||||
|
|
||||||
|
// A successful mission's headline names the actor ONLY when it was
|
||||||
|
// attributed to the viewing human — never the actual perpetrator when it
|
||||||
|
// wasn't (Brian's ask: "no information on who was responsible").
|
||||||
|
const mineTheft = Gnn.describeGnnStory(RULES, stD, { type: 'techStolen', empire: 0, target: 1, techId: 'lasercannon' });
|
||||||
|
const othersTheft = Gnn.describeGnnStory(RULES, stD, { type: 'techStolen', empire: 1, target: 0, techId: 'lasercannon' });
|
||||||
|
check('a mission the player carried out is marked attributed', mineTheft.attributed === true);
|
||||||
|
check('a mission the player carried out names the player as the actor',
|
||||||
|
mineTheft.headline.includes(stD.empires[0].name));
|
||||||
|
check('a mission carried out against the player is NOT marked attributed', othersTheft.attributed === false);
|
||||||
|
check('a mission carried out against the player never names the actual perpetrator',
|
||||||
|
!othersTheft.headline.includes(stD.empires[1].name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- anchorLine: the green-screen ticker narration under GNN's anchor
|
// --- anchorLine: the green-screen ticker narration under GNN's anchor
|
||||||
|
|
@ -2854,6 +2905,10 @@ section('7b. Galactic News Network');
|
||||||
{ type: 'eliminated', empire: 1, attacker: -1 },
|
{ type: 'eliminated', empire: 1, attacker: -1 },
|
||||||
{ type: 'spyCaught', empire: 0, target: 1, mission: 'sabotage' },
|
{ type: 'spyCaught', empire: 0, target: 1, mission: 'sabotage' },
|
||||||
{ type: 'spyCaught', empire: 0, target: 1, mission: 'steal' },
|
{ type: 'spyCaught', empire: 0, target: 1, mission: 'steal' },
|
||||||
|
{ type: 'sabotage', empire: 0, target: 1, starIdx: 0, factoriesLost: 2, defenseLost: 3 },
|
||||||
|
{ type: 'sabotage', empire: 1, target: 0, starIdx: 0, factoriesLost: 2, defenseLost: 3 },
|
||||||
|
{ type: 'techStolen', empire: 0, target: 1, techId: 'lasercannon' },
|
||||||
|
{ type: 'techStolen', empire: 1, target: 0, techId: 'lasercannon' },
|
||||||
];
|
];
|
||||||
for (const ev of sampleEvents) {
|
for (const ev of sampleEvents) {
|
||||||
const desc = Gnn.describeGnnStory(RULES, stD, ev);
|
const desc = Gnn.describeGnnStory(RULES, stD, ev);
|
||||||
|
|
@ -3359,7 +3414,7 @@ section('10. AI self-play soak');
|
||||||
for (const g of games) {
|
for (const g of games) {
|
||||||
for (const ev of g.st.events) {
|
for (const ev of g.st.events) {
|
||||||
if (ev.type === 'lastColony') lastColonyCount += 1;
|
if (ev.type === 'lastColony') lastColonyCount += 1;
|
||||||
if (!Gnn.isGnnStory(RULES, ev)) continue;
|
if (!Gnn.isGnnStory(RULES, g.st, ev)) continue;
|
||||||
storyEventCount += 1;
|
storyEventCount += 1;
|
||||||
try {
|
try {
|
||||||
const desc = Gnn.describeGnnStory(RULES, g.st, ev);
|
const desc = Gnn.describeGnnStory(RULES, g.st, ev);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue