Compare commits
No commits in common. "fb9722f028540ea0fc0b8bfdc3bb422a2d9c72ed" and "30af365c680de928a7f275f481360b688ade309d" have entirely different histories.
fb9722f028
...
30af365c68
22
README.md
22
README.md
|
|
@ -40,27 +40,15 @@ python3 -m http.server 8080
|
|||
population, and an `owner` seam reserved for the factions/pirates to come.
|
||||
The current system's dossier (name, identity, what's there) shows
|
||||
top-left in the game scene.
|
||||
- **Names with a point of view** (`data/naming.json`): planets draw from a
|
||||
deep curated bank — colonial "New Denver" worlds beside alien "Klaxoria"
|
||||
worlds — and stations from a bank of official designations ("Deep Space
|
||||
SC-145") and smuggler hangouts ("Hell's Hideout"). Names are dealt out
|
||||
randomly per system **without repeats until the bank is exhausted** (a
|
||||
seeded shuffle, so the first N draws are N distinct names). Stars and the
|
||||
galaxy itself are still synthesised from syllable pools, so you never run
|
||||
out of those. Within a system a planet never shares a name with another
|
||||
planet and a station never with another station.
|
||||
- Game screen with a top-down ship (real spritesheet art — frame 0 of
|
||||
`assets/images/ships-player.png`, see `data/ship.json`): **click anywhere to fly there**
|
||||
in the current system's open space (system boundaries/jumps come next).
|
||||
Discovered worlds get a screen-edge arrow + a name tag showing the
|
||||
world's **name** (e.g. `HOME WORLD · ESHKAELURA`); **clicking the
|
||||
Discovered worlds get a screen-edge arrow + name tag; **clicking the
|
||||
name tag autopilots the ship there** (it arrives on the keep-out rim,
|
||||
facing the world)
|
||||
- **The home planet** — the player's world — in the system you start in:
|
||||
a 1024 px disc rendered 1:1 from the `assets/images/planets.png`
|
||||
spritesheet, with a **proper name drawn from the planet name bank** (the
|
||||
first name off the starting system's deck, so it never clashes with one
|
||||
of the other worlds). Which Terran face it shows (frames 0–2) is a
|
||||
- **The home planet** — the player's Terran world — in the system you
|
||||
start in: a 1024 px disc rendered 1:1 from the `assets/images/planets.png`
|
||||
spritesheet. Which Terran face it shows (frames 0–2) is a
|
||||
seed-deterministic pick, so the same galaxy always yields the same home
|
||||
world. The ship spawns ~150 px (edge-to-edge) off its rim in a
|
||||
seed-derived direction, may fly in as close as 50 px from the rim, and
|
||||
|
|
@ -100,7 +88,7 @@ orbit/
|
|||
│ ├── research.json # RESEARCH: time-based, one at a time, gates builds/research
|
||||
│ ├── builds.json # BUILDING: credits + minerals, ship/planet/station upgrades
|
||||
│ ├── actionbar.json # the command deck: 6 slots (Research, Build, Ship, ·, ·, Menu)
|
||||
│ └── naming.json # names: star/galaxy syllable pools + the curated PLANET & STATION name banks
|
||||
│ └── naming.json # syllable pools for names
|
||||
├── assets/images/ # art: planets.png (1024×1024 frames), ships-player.png (256×256 frames)
|
||||
├── assets/fonts/ # UI typefaces: Ethnocentric (headers), Centauri (body)
|
||||
├── lib/ # vendored third-party libs (Phaser 4.2.1)
|
||||
|
|
|
|||
Binary file not shown.
101
data/naming.json
101
data/naming.json
|
|
@ -1,106 +1,25 @@
|
|||
{
|
||||
"_comment": "Names. Stars and the galaxy are still SYNTHEISED from syllable pools (star / galaxy below) — short, consistent, unlimited. PLANETS and STATIONS draw from finite, curated NAME BANKS instead: a deep pool of hand-picked names, dealt out without repeats until the pool is exhausted (see js/utils/NameGenerator.js → planetDeck / stationDeck). Edit the banks freely — names are drawn per-system from the galaxy seed, so changing the banks changes every name, consistently. Within a system a planet never repeats another planet's name and a station never repeats another station's name; across the 40,000-system galaxy a finite pool must eventually recur (that's the price of lazy, order-independent generation — see docs/PROJECT_NOTES.md).",
|
||||
"_comment": "Syllable pools for deterministic name synthesis (NameGenerator.js). Edit freely — names are drawn per-entity from the galaxy seed, so changing pools changes every name, consistently.",
|
||||
"star": {
|
||||
"syllables": ["ka", "vel", "thu", "ori", "an", "esh", "mar", "dy", "neth", "avi", "cor", "lu", "tan", "ys", "brei", "hal", "ion", "sol", "qua", "ren"],
|
||||
"minParts": 2,
|
||||
"maxParts": 4
|
||||
},
|
||||
"planet": {
|
||||
"roots": ["aurel", "bryn", "cald", "dross", "emrys", "fen", "gale", "hyra", "ilv", "jor", "kest", "lun", "mora", "neth", "orb", "pyre", "quill", "ross", "sable", "tarn", "ulric", "vex", "wren", "xan", "yrra", "zeph"]
|
||||
},
|
||||
"galaxy": {
|
||||
"syllables": ["an", "dro", "me", "mil", "ky", "cen", "tau", "rhi", "sa", "vel", "pyr", "os", "eth", "ni"],
|
||||
"minParts": 3,
|
||||
"maxParts": 5
|
||||
},
|
||||
"banks": {
|
||||
"_comment": "planet.colonial = familiar, Earth-flavored names (colonial 'New <city>' + American place-names). planet.alien = unfamiliar, exotic names. station.procedural = official designations. station.smuggler = underworld hangouts. A system's worlds/stations are drawn from the COMBINED pool for each category (colonial+alien, procedural+smuggler) without replacement.",
|
||||
"planet": {
|
||||
"colonial": [
|
||||
"New Denver", "New Austin", "New Phoenix", "New Portland", "New Dallas", "New Houston",
|
||||
"New Chicago", "New Detroit", "New Boston", "New Atlanta", "New Nashville", "New Memphis",
|
||||
"New Kansas", "New Omaha", "New Tulsa", "New Fargo", "New Duluth", "New Billings",
|
||||
"New Cheyenne", "New Amarillo", "New Laredo", "New Albuquerque", "New Flagstaff", "New Prescott",
|
||||
"New Yakima", "New Spokane", "New Tacoma", "New Salem", "New Provo", "New Reno",
|
||||
"New Elko", "New Medford", "New Eugene", "New Corvallis", "New Astoria", "New Olympia",
|
||||
"New Wenatchee", "New Missoula", "New Butte", "New Casper", "New Sheridan", "New Bismarck",
|
||||
"New Superior", "New Milwaukee", "New Madison", "New Rockford", "New Peoria", "New Columbus",
|
||||
"New Cincinnati", "New Cleveland", "New Buffalo", "New Rochester", "New Syracuse", "New Albany",
|
||||
"New Binghamton", "New Harrisburg", "New Scranton", "New Elmira", "New Ithaca", "New Kingston",
|
||||
"New Yonkers", "New Greenwich", "New Stamford", "New Hartford", "New Bridgeport", "New Portsmouth",
|
||||
"New Manchester", "New Concord", "New Bangor", "New Lewiston", "New Auburn", "New Waterville",
|
||||
"New Saco", "New Exeter", "New Keene", "New Plymouth", "New Brockton", "New Lowell",
|
||||
"New Haverhill", "New Gloucester", "New Ipswich", "New Andover", "New Quincy", "New Dedham",
|
||||
"New Natick", "New Worcester", "New Pittsfield", "New Greenfield", "New Chelsea", "New Holyoke",
|
||||
"New Westfield", "Fort Boise", "Port Tucson", "Denver City", "Austin Falls", "Phoenix Ridge",
|
||||
"Tucson Mesa", "Dallas Harbor", "Houston Gulf", "Chicago Point", "Boston Landing", "Nashville Bend",
|
||||
"Kansas Plains", "Cheyenne Gap", "Brownsville Bay", "Salem Harbor", "Provo Canyon", "Elko Basin",
|
||||
"Butte Ridge", "Havre Plains", "Rawlins Basin", "Wausau River", "Springfield Falls", "Champaign Valley",
|
||||
"Erie Bay", "Albany Point", "Poughkeepsie Point", "Norwalk Cove", "Danbury Ridge", "Waterbury Falls",
|
||||
"Kennebunk Cove", "Newburyport Cove", "Braintree Cove", "Worcester River", "Northampton River"
|
||||
],
|
||||
"alien": [
|
||||
"Klaxoria", "Zyneatha", "Draonvexus", "Zorvraquaax", "Vexithunhal", "Ythorialzyr",
|
||||
"Elithanzra", "Zaxaviion", "Quavex", "Avivryxyl", "Ananora", "Kakethilith",
|
||||
"Zynera", "Koren", "Zorath", "Velvrazr", "Alkhaqo", "Velqavalax",
|
||||
"Xylzolenori", "Eshkaelura", "Kadraisia", "Kordrasul", "Suldra", "Ketosera",
|
||||
"Elisyth", "Xyresh", "Rendymar", "Ythera", "Anera", "Khaora",
|
||||
"Khaqor", "Oridravexia", "Zalax", "Zalqor", "Vrazalthu", "Zalvexkha",
|
||||
"Xanzranvry", "Qavnydraziv", "Aviusys", "Corsyl", "Zynziv", "Drathu",
|
||||
"Xylhalil", "Sylxanys", "Breivexyrr", "Quaqor", "Ysvexys", "Qulxyl",
|
||||
"Zorzalthuea", "Xevquazax", "Thaeaithxyl", "Exvexvryzax", "Zyrxun", "Nyilsul",
|
||||
"Ketqavkor", "Ketzra", "Uravex", "Breiqe", "Iaoriythxun", "Vryvex",
|
||||
"Yrryrrxan", "Umothora", "Alnyth", "Thaunketsol", "Oraka", "Oriqavyldys",
|
||||
"Kethothixys", "Kaelyld", "Zrakoris", "Nytholonzyn", "Thazrqor", "Axos",
|
||||
"Zorqul", "Korvex", "Envra", "Exqav", "Nythkor", "Thairayth",
|
||||
"Qalsul", "Olnythhal", "Ustan", "Nyith", "Korvrady", "Nethzolyld",
|
||||
"Solil", "Vryin", "Zaxzyr", "Xylan", "Draqosol", "Ixzal",
|
||||
"Quaketxyl", "Xuniazaxan", "Athkha", "Haldy", "Ysorayldum", "Ketzyl",
|
||||
"Zrathu", "Umzynzr", "Ketqavxan", "Nethys", "Korvane", "Sulthar",
|
||||
"Vrynnex", "Zaxolis", "Xanthea", "Ylthar", "Zorvax", "Vraxol",
|
||||
"Nymthea", "Qorvyn", "Zorvane", "Kethar", "Xanthys", "Zorvyn",
|
||||
"Klaxys", "Dravys", "Korvys", "Vexxys", "Sulthys", "Qorvax",
|
||||
"Xylthys", "Ylthys", "Zynthys", "Vrynnys", "Zaxolys", "Draquis",
|
||||
"Sulvar", "Korthex", "Vrynnal", "Xanthis", "Ylthral", "Zorvael",
|
||||
"Kethral", "Vraxalis", "Nymthal", "Qorvael", "Xylthral", "Zynthral",
|
||||
"Klaxalis", "Vexxalis", "Dravael", "Sulvaris", "Korveth", "Vrynthal"
|
||||
]
|
||||
},
|
||||
"station": {
|
||||
"procedural": [
|
||||
"Deep Space SC-145", "Orbital Station KX-2210", "Nav Relay V-77", "Forward Base SC-09", "Survey Camp 8812",
|
||||
"Transit Gate SG-4", "Habitat Ring 330", "Dock Platform 12", "Relay Node 210", "Anchor Point 55",
|
||||
"Beacon 4491", "Waypoint 7", "Station 9012", "Outpost Delta-7", "Habitat 44",
|
||||
"Ring Station 12", "Platform 44", "Module 7", "Sector 8", "Node 44",
|
||||
"Port 88", "Dock 11", "Berth 22", "Pad 33", "Bay 55",
|
||||
"Quay 66", "Spire 77", "Tower 88", "Rig 44", "Yard 55",
|
||||
"Works 66", "Terminal 11", "Depot 22", "Trading Post 88", "Bourse 99",
|
||||
"Observatory 77", "Research Station 99", "Institute 11", "Archive 77", "Vault 88",
|
||||
"Warehouse 11", "Reserve 33", "Cache 44", "Supply Depot 66", "Outfitting 99",
|
||||
"Repair Bay 33", "Service Station 55", "Salvage Yard 22", "Lighthouse 12", "Signal 34",
|
||||
"Pulse 56", "Echo 78", "Resonance 90", "Frequency 12", "Band 34",
|
||||
"Channel 56", "Carrier 78", "Wave 90", "Phase 12", "Orbit 78",
|
||||
"Apogee 12", "Perigee 34", "Sidereal 34", "Synodic 56", "Tropical 78",
|
||||
"Draconic 12", "Lunar 34", "Solar 56", "Planetary 78", "Stellar 90",
|
||||
"Galactic 12", "Cosmic 34", "Universal 56", "Absolute 78", "Relative 90"
|
||||
],
|
||||
"smuggler": [
|
||||
"Hell's Hideout", "The Rusty Anchor", "Black Tide", "The Gilded Cage", "Widow's Rest",
|
||||
"The Broken Compass", "The Salted Barrel", "The Drowned Bell", "The Cursed Cargo", "The Phantom Rig",
|
||||
"The Ghost Dock", "The Midnight Berth", "The Shadow Quay", "The Black Market", "The Hidden Hold",
|
||||
"The Secret Stow", "The Smuggler's Den", "The Contraband Bay", "The Night Runner", "The Shadow Fleet",
|
||||
"The Dark Harbor", "The Hidden Cove", "The Smuggler's Cove", "The Cursed Anchor", "The Wandering Rig",
|
||||
"The Drifting Hulk", "The Broken Mast", "The Tattered Sail", "The Rusty Helm", "The Salted Keel",
|
||||
"The Drowned Rig", "The Phantom Ship", "The Last Light", "The Fading Star", "The Dying Sun",
|
||||
"The Waning Moon", "The Hollow Crown", "The Broken Chain", "The Shattered Wheel", "The Lost Helm",
|
||||
"The Cursed Compass", "The Wandering Star", "The Drifting Light", "The Fading Flame", "The Dying Ember",
|
||||
"The Last Ember", "The Cold Hearth", "The Dead Hearth", "The Frozen Flame", "The Drowned Flame",
|
||||
"The Buried Light", "The Hidden Fire", "The Secret Flame", "The Smuggler's Flame", "The Contraband Light",
|
||||
"The Black Flame", "The Cursed Fire", "The Wandering Fire", "The Drifting Flame", "The Broken Light",
|
||||
"The Tattered Ember", "The Rusty Ember", "The Salted Flame", "The Drowned Light", "The Phantom Flame",
|
||||
"The Ghost Fire", "The Midnight Ember", "The Shadow Flame", "The Hidden Ember", "The Secret Fire",
|
||||
"The Night Flame", "The Dark Ember", "The Cursed Flame", "The Wandering Ember", "The Wrecked Beacon",
|
||||
"The Tattered Flame", "The Rusty Flame", "The Salted Light", "The Drowned Ember", "The Phantom Fire",
|
||||
"The Ghost Light", "The Midnight Flame", "The Shadow Ember", "The Smuggler's Light", "The Secret Light",
|
||||
"The Night Ember", "The Dark Flame", "The Cursed Light", "The Lost Lantern", "The Broken Flame"
|
||||
]
|
||||
"nouns": {
|
||||
"colony": ["Reach", "Haven", "Landing", "Hold", "City", "Gate"],
|
||||
"miningStation": ["Rig", "Dredge", "Claim", "Works", "Yard"],
|
||||
"cloudBase": ["Skyspire", "Drift", "Aerie", "Balcony", "Cloudspire"],
|
||||
"deepSpaceStation": ["Relay", "Gate", "Beacon", "Haven", "Anchor"],
|
||||
"waypoint": ["Waypoint", "Beacon", "Marker", "Light"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,15 +292,9 @@ let big;
|
|||
const avg = (arr) => arr.reduce((s, x) => s + x.n, 0) / arr.length;
|
||||
check(`core→rim settlement gradient (inner ${avg(inner).toFixed(2)}/system > outer ${avg(outer).toFixed(2)}/system)`, avg(inner) > avg(outer));
|
||||
|
||||
// Station + planet naming now draws from the curated BANKS (no repeats
|
||||
// within a system until the pool is exhausted). The deck is a seeded
|
||||
// shuffle of the whole bank ⇒ the first N draws are N distinct names.
|
||||
const stDeck = NameGenerator.stationDeck(Rng.derive('x', 'names', 'S1', 'stations'));
|
||||
check('station deck is a non-empty list of names', Array.isArray(stDeck) && stDeck.length >= 10 && stDeck.every((n) => typeof n === 'string' && n.length > 0));
|
||||
check('station deck has no duplicates', new Set(stDeck).size === stDeck.length);
|
||||
const plDeck = NameGenerator.planetDeck(Rng.derive('x', 'names', 'S1', 'planets'));
|
||||
check('planet deck is a non-empty list of names', Array.isArray(plDeck) && plDeck.length >= 10 && plDeck.every((n) => typeof n === 'string' && n.length > 0));
|
||||
check('planet deck has no duplicates', new Set(plDeck).size === plDeck.length);
|
||||
// Station naming + the pure report formatter.
|
||||
const stName = NameGenerator.station(Rng.derive('x', 'station', 'S1'), 'cloudBase', 'Kest');
|
||||
check('settlement names are anchor + noun ("Kest Skyspire"-style)', /Kest [A-Z]\w+/.test(stName));
|
||||
|
||||
const { formatSystemReport, formatPop } = await import(
|
||||
pathToFileURL(join(__dirname, '../js/galaxy/SystemReport.js')).href
|
||||
|
|
@ -308,7 +302,7 @@ let big;
|
|||
const anySys = big2.currentSystem();
|
||||
const report = formatSystemReport(big2.ensureContent(anySys.id));
|
||||
check('report has title/subtitle/settlements/summary', !!report.title && !!report.subtitle && Array.isArray(report.settlements) && typeof report.summary === 'string');
|
||||
check('report lines name their anchor world or open space', report.settlements.every((s) => /on .+|in open space/.test(s.text)));
|
||||
check('report lines name their anchor world or open space', report.settlements.every((s) => /on \w+ [IVX]+|in open space/.test(s.text)));
|
||||
check('report population sums match', report.population === report.settlements.reduce((s, x) => s + x.population, 0));
|
||||
check('formatPop() scales (1.2k / 9.0M)', formatPop(1234) === '1.2k' && formatPop(9000000) === '9.0M' && formatPop(12) === '12');
|
||||
|
||||
|
|
@ -316,28 +310,6 @@ let big;
|
|||
const unclaimedRec = sample.map((r) => big2.ensureContent(r.id)).find((c) => c.settlements.length === 0);
|
||||
check('unclaimed systems report "charted · unclaimed"', unclaimedRec && formatSystemReport(unclaimedRec).summary === 'charted · unclaimed');
|
||||
|
||||
// No-duplicate guarantee: within a system, planet names and station names
|
||||
// never repeat (drawn without replacement from the bank).
|
||||
const noDupOk = sample.every((r) => {
|
||||
const c = big2.ensureContent(r.id);
|
||||
const pnames = c.planets.map((p) => p.name);
|
||||
const snames = c.settlements.map((s) => s.name);
|
||||
return new Set(pnames).size === pnames.length && new Set(snames).size === snames.length;
|
||||
});
|
||||
check('within a system: planet names & station names are all distinct', noDupOk);
|
||||
|
||||
// The starting system's HOME world gets a bank name distinct from its
|
||||
// planets; only the starting system has one.
|
||||
const homeRec = big2.currentSystem();
|
||||
const homeC = big2.ensureContent(homeRec.id);
|
||||
check(
|
||||
'starting system has a home-world bank name, distinct from its planets',
|
||||
typeof homeC.homeName === 'string' && homeC.homeName.length > 0 &&
|
||||
!homeC.planets.some((p) => p.name === homeC.homeName),
|
||||
);
|
||||
const nonHomeRec = big2.records.find((r) => r.id !== homeRec.id);
|
||||
check('other systems have no home world', !('homeName' in big2.ensureContent(nonHomeRec.id)));
|
||||
|
||||
// Lazy === eager still holds with the lived-in layer (spot check).
|
||||
const fresh2 = Galaxy.create(SEED);
|
||||
check('settlements: lazy content === content from a fresh galaxy', deepEq(fresh2.ensureContent(big2.records[0].id).settlements, big2.ensureContent(big2.records[0].id).settlements));
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ trading/economy, stations, quests — to be scoped as we go.
|
|||
*parameters*.
|
||||
- Split config by concern as the game grows: world generation already has
|
||||
`data/galaxy.json` (shape/scale), `data/systems.json` (archetypes), and
|
||||
`data/naming.json` (syllable pools + name banks); next up might be `data/economy.json`,
|
||||
`data/naming.json` (name pools); next up might be `data/economy.json`,
|
||||
`data/ships.json`, `data/crew.json`, etc. One file per system beats one
|
||||
giant file.
|
||||
|
||||
|
|
@ -146,26 +146,6 @@ unclaimed". Model & seams:
|
|||
of interest: the data already says what's there and where (anchor =
|
||||
planet ordinal or open space).
|
||||
|
||||
**Naming** — planets and stations get their names from **curated banks** in
|
||||
`data/naming.json → banks` (distinct from the star/galaxy *syllable pools*):
|
||||
- **`banks.planet`** — a deep list mixing **colonial** names ("New Denver",
|
||||
"Port Tucson") and **alien** names ("Klaxoria", "Vexithunhal").
|
||||
- **`banks.station`** — a deep list mixing **official/procedural**
|
||||
designations ("Deep Space SC-145", "Nav Relay V-77") and **smuggler** /
|
||||
outlaw hangouts ("Hell's Hideout", "The Rusty Anchor").
|
||||
- **Assignment** — `NameGenerator.planetDeck(rng)` / `stationDeck(rng)`
|
||||
build one seeded shuffle of the whole bank; `SystemGenerator` deals names
|
||||
out of it in order. So **within a system there are no repeats** (the first
|
||||
N draws are N distinct names), and a system only reuses a name if it has
|
||||
more bodies than the bank holds (impossible at these sizes). The decks are
|
||||
dealt from dedicated derived streams (`Rng.derive(seed,'system',id,'names',
|
||||
'planets'|'stations')`) so they stay order-independent (lazy === eager).
|
||||
The **home world** (the player's planet, in the starting system only) takes
|
||||
the first name off that system's planet deck — so it gets a real bank name
|
||||
too, never clashing with one of the planets (`content.homeName`; the old
|
||||
`planets.homeName` is now just a fallback). Stars and the galaxy are still
|
||||
synthesised from syllable pools (unbounded).
|
||||
|
||||
## The current system is a place, not just a dossier (discovery + compass)
|
||||
|
||||
The player starts in a solar system, and the other worlds **exist in the
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { config } from '../config/Config.js';
|
||||
import { Rng } from '../utils/Rng.js';
|
||||
import { NameGenerator } from '../utils/NameGenerator.js';
|
||||
import { NameGenerator, ordinalLabel } from '../utils/NameGenerator.js';
|
||||
|
||||
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
||||
|
||||
|
|
@ -62,18 +62,6 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
clamp(pc.mean + (rng.next() - 0.5) * (pc.max - pc.min), pc.min, pc.max),
|
||||
);
|
||||
const classWeights = attr.planetClasses ?? { rocky: 45, gas: 25, ice: 18, lava: 12 };
|
||||
// Planet names come from a curated bank (data/naming.json → banks.planet),
|
||||
// dealt out per-system without repeats (see NameGenerator.planetDeck).
|
||||
// A dedicated derived stream keeps this order-independent (lazy === eager).
|
||||
const planetDeck = NameGenerator.planetDeck(
|
||||
Rng.derive(galaxy.seed, 'system', record.id, 'names', 'planets')
|
||||
);
|
||||
// The player's home world sits at the origin of the STARTING system and is
|
||||
// not one of the generated planets. It takes the first name off that
|
||||
// system's deck so it can never clash with a planet; the planets then draw
|
||||
// from the rest of the deck (all distinct within the system).
|
||||
const isHome = record.id === galaxy?.currentSystemId;
|
||||
const homeName = isHome ? planetDeck[0] : null;
|
||||
const planets = [];
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const pclass = rng.weighted(classWeights, 'rocky');
|
||||
|
|
@ -82,9 +70,11 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
// Jovian/ice worlds drag moon systems; terrestrials mostly don't.
|
||||
moons = pclass === 'gas' || pclass === 'ice' ? rng.int(1, 6) : rng.int(0, 2);
|
||||
}
|
||||
const root = NameGenerator.planetRoot(rng);
|
||||
planets.push({
|
||||
name: planetDeck[(isHome ? i : i - 1) % planetDeck.length],
|
||||
name: `${root} ${ordinalLabel(i)}`,
|
||||
ordinal: i,
|
||||
root,
|
||||
class: pclass,
|
||||
moons,
|
||||
habitable: pclass === 'rocky' && rng.chance(attr.habitability ?? 0.1),
|
||||
|
|
@ -98,9 +88,7 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
kindDefs: config.get('settlements.kinds', {}),
|
||||
spec: attr.settlements ?? {},
|
||||
planets,
|
||||
stationDeck: NameGenerator.stationDeck(
|
||||
Rng.derive(galaxy.seed, 'system', record.id, 'names', 'stations')
|
||||
),
|
||||
starName: star.name,
|
||||
density: settlementDensity(galaxy, record),
|
||||
});
|
||||
|
||||
|
|
@ -111,7 +99,7 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
};
|
||||
const hazard = rng.chance(attr.hazard ?? 0.1);
|
||||
|
||||
const content = {
|
||||
return {
|
||||
name: record.name,
|
||||
type: record.type,
|
||||
star,
|
||||
|
|
@ -120,8 +108,6 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
belt,
|
||||
hazard,
|
||||
};
|
||||
if (isHome) content.homeName = homeName; // the player's home world (starting system only)
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -199,15 +185,14 @@ function settlementDensity(galaxy, record) {
|
|||
* orbital order (colony, mining, cloud), then free-floating (deep-space
|
||||
* station, waypoint). Every roll goes through the system's own stream.
|
||||
*/
|
||||
function generateSettlements({ rng, kindDefs, spec, planets, stationDeck, density }) {
|
||||
function generateSettlements({ rng, kindDefs, spec, planets, starName, density }) {
|
||||
const out = [];
|
||||
let nameIndex = 0; // next station name from the system's deck (no repeats)
|
||||
|
||||
const make = (kind, anchor) => {
|
||||
const make = (kind, anchor, rootWord) => {
|
||||
const def = kindDefs[kind] ?? {};
|
||||
out.push({
|
||||
kind,
|
||||
name: stationDeck[nameIndex++ % stationDeck.length],
|
||||
name: NameGenerator.station(rng, kind, rootWord),
|
||||
anchor,
|
||||
population: logPopulation(rng, def.population),
|
||||
owner: null, // reserved: factions / pirates claim settlements later
|
||||
|
|
@ -217,24 +202,24 @@ function generateSettlements({ rng, kindDefs, spec, planets, stationDeck, densit
|
|||
// Planet-bound, in orbital order.
|
||||
for (const p of planets) {
|
||||
if (p.habitable && roll(rng, spec.colony?.chance ?? 0.3, density)) {
|
||||
make('colony', { type: 'planet', ordinal: p.ordinal });
|
||||
make('colony', { type: 'planet', ordinal: p.ordinal }, p.root);
|
||||
}
|
||||
if (needs(p, spec.miningStation?.needs, ['rocky', 'lava', 'ice']) &&
|
||||
roll(rng, spec.miningStation?.chance ?? 0.2, density)) {
|
||||
make('miningStation', { type: 'planet', ordinal: p.ordinal });
|
||||
make('miningStation', { type: 'planet', ordinal: p.ordinal }, p.root);
|
||||
}
|
||||
if (needs(p, spec.cloudBase?.needs, ['gas']) &&
|
||||
roll(rng, spec.cloudBase?.chance ?? 0.15, density)) {
|
||||
make('cloudBase', { type: 'planet', ordinal: p.ordinal });
|
||||
make('cloudBase', { type: 'planet', ordinal: p.ordinal }, p.root);
|
||||
}
|
||||
}
|
||||
|
||||
// Free-floating, out in the dark.
|
||||
if (roll(rng, spec.deepSpaceStation?.chance ?? 0.12, density)) {
|
||||
make('deepSpaceStation', { type: 'space' });
|
||||
make('deepSpaceStation', { type: 'space' }, starName);
|
||||
}
|
||||
if (roll(rng, spec.waypoint?.chance ?? 0.2, density)) {
|
||||
make('waypoint', { type: 'space' });
|
||||
make('waypoint', { type: 'space' }, starName);
|
||||
}
|
||||
|
||||
return out;
|
||||
|
|
|
|||
|
|
@ -83,11 +83,6 @@ export class GameScene extends Phaser.Scene {
|
|||
this.ensureGalaxy();
|
||||
this.systemRecord = this.galaxy.currentSystem();
|
||||
this.systemContent = this.galaxy.ensureContent(this.systemRecord.id);
|
||||
// The home world's name — dealt from the planet name bank by the
|
||||
// generator (the starting system is the only one with a home world), so
|
||||
// it reads as a real place rather than a generic "Terra". Fallback keeps
|
||||
// the old configured name if the bank is ever unavailable.
|
||||
this.homeWorldName = this.systemContent.homeName || config.get('planets.homeName', 'Terra');
|
||||
this.createSystemHud();
|
||||
|
||||
// The home planet — the player's Terran world, always present in the
|
||||
|
|
@ -163,7 +158,7 @@ export class GameScene extends Phaser.Scene {
|
|||
config.get('tether.homeId', 'home'),
|
||||
0, 0, // home world's center (it sits at the system origin)
|
||||
config.get('tether.homeLevel', 1),
|
||||
config.get('tether.homeLabel', '') || this.homeWorldName,
|
||||
config.get('tether.homeLabel', '') || config.get('planets.homeName', 'Terra'),
|
||||
);
|
||||
this.tetherToastAt = null;
|
||||
this.refreshTetherHud();
|
||||
|
|
@ -473,7 +468,7 @@ export class GameScene extends Phaser.Scene {
|
|||
y: this.planet.y,
|
||||
radius: this.planet.radius,
|
||||
typeLabel: config.get('planets.homeTypeLabel', 'Home World'),
|
||||
name: this.homeWorldName,
|
||||
name: config.get('planets.homeName', 'Terra'),
|
||||
});
|
||||
for (const p of this.systemPlanets) {
|
||||
out.push({
|
||||
|
|
|
|||
|
|
@ -1,31 +1,13 @@
|
|||
import { config } from '../config/Config.js';
|
||||
|
||||
/**
|
||||
* Names.
|
||||
* Deterministic name synthesis. Everything is drawn from the caller's
|
||||
* Rng (so names are part of the seed), with syllable pools coming from
|
||||
* data/naming.json. Code-level fallbacks keep this working even if the
|
||||
* file is missing.
|
||||
*
|
||||
* Two different strategies, both seed-deterministic:
|
||||
*
|
||||
* STARS & THE GALAXY — synthesised from syllable pools (data/naming.json →
|
||||
* star / galaxy). Short, consistent, and effectively unlimited: there are
|
||||
* 40,000 systems and no reason to run out of star names.
|
||||
*
|
||||
* PLANETS & STATIONS — drawn from finite, curated NAME BANKS (data/
|
||||
* naming.json → banks). A deep pool of hand-picked names — colonial
|
||||
* "New Denver" worlds and alien "Klaxoria" worlds; official designations
|
||||
* like "Deep Space SC-145" and smuggler hangouts like "Hell's Hideout".
|
||||
* Within a single system a planet never repeats another planet's name and
|
||||
* a station never repeats another station's name (no-duplicate drawing).
|
||||
* Across the galaxy a finite pool must eventually recur — that's the cost
|
||||
* of keeping generation LAZY and ORDER-INDEPENDENT (see
|
||||
* docs/PROJECT_NOTES.md → "Determinism rules").
|
||||
*
|
||||
* How the banks are dealt out:
|
||||
* const deck = NameGenerator.planetDeck(Rng.derive(seed, 'system', id, 'names', 'planets'));
|
||||
* const name = deck[i % deck.length]; // i = the system's i-th planet
|
||||
*
|
||||
* The deck is a seeded SHUFFLE of the whole bank, so the first N draws are
|
||||
* N distinct names until the bank is exhausted. `Rng.derive(..., 'system',
|
||||
* id, ...)` keeps it independent per system ⇒ lazy === eager.
|
||||
* const rng = Rng.derive(seed, 'name', 'S000123');
|
||||
* const name = NameGenerator.star(rng); // "Kethavoryn"
|
||||
*/
|
||||
|
||||
const FALLBACK = {
|
||||
|
|
@ -34,15 +16,23 @@ const FALLBACK = {
|
|||
minParts: 2,
|
||||
maxParts: 4,
|
||||
},
|
||||
planet: {
|
||||
roots: ['aurel', 'bryn', 'cald', 'dross', 'emrys', 'fen', 'gale', 'hyra', 'ilv', 'jor', 'kest', 'lun', 'mora', 'neth', 'orb', 'pyre', 'quill', 'ross', 'sable', 'tarn', 'ulric', 'vex', 'wren', 'xan', 'yrra', 'zeph'],
|
||||
},
|
||||
galaxy: {
|
||||
syllables: ['an', 'dro', 'me', 'mil', 'ky', 'cen', 'tau', 'rhi', 'sa', 'vel', 'pyr', 'os'],
|
||||
minParts: 3,
|
||||
maxParts: 5,
|
||||
},
|
||||
// Tiny built-in pools so the game still names things if data/naming.json
|
||||
// is missing. The real banks live in data/naming.json → banks.
|
||||
planet: ['New Denver', 'Klaxoria', 'New Austin', 'Zyneatha', 'New Phoenix', 'Vexithunhal', 'New Dallas', 'Kordrasul'],
|
||||
station: ['Deep Space SC-145', "Hell's Hideout", 'Beacon 4491', 'The Rusty Anchor', 'Station 9012', 'The Shadow Quay'],
|
||||
station: {
|
||||
nouns: {
|
||||
colony: ['Reach', 'Haven', 'Landing'],
|
||||
miningStation: ['Rig', 'Dredge', 'Claim'],
|
||||
cloudBase: ['Skyspire', 'Drift', 'Aerie'],
|
||||
deepSpaceStation: ['Relay', 'Beacon', 'Anchor'],
|
||||
waypoint: ['Waypoint', 'Beacon', 'Light'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX'];
|
||||
|
|
@ -63,26 +53,6 @@ function joinSyllables(rng, syllables, minParts, maxParts) {
|
|||
return capitalize(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* The combined name bank for a category: the curated sub-pools concatenated
|
||||
* (planets: colonial + alien; stations: procedural + smuggler). Deduplicated
|
||||
* defensively in case the JSON has an accidental repeat.
|
||||
*/
|
||||
function combinedBank(pools) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const pool of pools) {
|
||||
if (!Array.isArray(pool)) continue;
|
||||
for (const name of pool) {
|
||||
if (typeof name !== 'string' || !name) continue;
|
||||
if (seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** "1" → "I" … "20" → "XX"; beyond that, plain digits. */
|
||||
export function ordinalLabel(n) {
|
||||
return ROMAN[n - 1] ?? String(n);
|
||||
|
|
@ -96,6 +66,13 @@ export const NameGenerator = {
|
|||
return joinSyllables(rng, syllables, s.minParts ?? 2, s.maxParts ?? 4);
|
||||
},
|
||||
|
||||
/** A planet root word: "Aurel" (compose the ordinal yourself). */
|
||||
planetRoot(rng) {
|
||||
const p = section('planet', FALLBACK.planet);
|
||||
const roots = Array.isArray(p.roots) && p.roots.length ? p.roots : FALLBACK.planet.roots;
|
||||
return capitalize(rng.pick(roots) ?? 'Novus');
|
||||
},
|
||||
|
||||
/** A galaxy name: "Vethani". */
|
||||
galaxy(rng) {
|
||||
const g = section('galaxy', FALLBACK.galaxy);
|
||||
|
|
@ -104,29 +81,15 @@ export const NameGenerator = {
|
|||
},
|
||||
|
||||
/**
|
||||
* A shuffled deck of PLANET names for one system. The first N draws are N
|
||||
* distinct names (until the bank is exhausted). Pure function of (rng) ⇒
|
||||
* deterministic and order-independent when the rng is a per-system derive.
|
||||
* A settlement name from its anchor + kind: "Kest Reach", "Cortan Relay".
|
||||
* `anchorWord` is the planet's root word (planet-bound settlements) or the
|
||||
* star's name (free-floating ones).
|
||||
*/
|
||||
planetDeck(rng) {
|
||||
const b = config.get('naming.banks.planet', null);
|
||||
const pools = (b && (Array.isArray(b.colonial) || Array.isArray(b.alien)))
|
||||
? [b.colonial, b.alien]
|
||||
: [FALLBACK.planet];
|
||||
const names = combinedBank(pools);
|
||||
return names.length ? rng.shuffle(names) : Array.from(FALLBACK.planet);
|
||||
},
|
||||
|
||||
/**
|
||||
* A shuffled deck of STATION names for one system (official designations +
|
||||
* smuggler hangouts). Same no-duplicate-until-exhausted guarantee.
|
||||
*/
|
||||
stationDeck(rng) {
|
||||
const b = config.get('naming.banks.station', null);
|
||||
const pools = (b && (Array.isArray(b.procedural) || Array.isArray(b.smuggler)))
|
||||
? [b.procedural, b.smuggler]
|
||||
: [FALLBACK.station];
|
||||
const names = combinedBank(pools);
|
||||
return names.length ? rng.shuffle(names) : Array.from(FALLBACK.station);
|
||||
station(rng, kind, anchorWord) {
|
||||
const cfg = section('station', FALLBACK.station);
|
||||
const all = cfg.nouns ?? FALLBACK.station.nouns;
|
||||
const pool = Array.isArray(all?.[kind]) && all[kind].length ? all[kind] : (FALLBACK.station.nouns[kind] ?? ['Post']);
|
||||
const noun = rng.pick(pool) ?? 'Post';
|
||||
return `${anchorWord ?? 'Outer'} ${noun}`;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue