diff --git a/README.md b/README.md index 50dd581..47bc22e 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Built with TypeScript + Vite as an installable PWA. Rendering is a single ` { expect(game.npcTiles.some(n => n.npcId === '__rescue_goban__')).toBe(true); }); - it("Bricriu's Champion's Portion grants ATK until the next descent, one helping per floor", () => { + it("Nuada's draught grants ATK until the next descent, one cup per floor", () => { const onFloorEvent = vi.fn(); const cb = { ...makeCallbacks(), onFloorEvent }; const game = new Game(cb); - game.rescuedIds.add('bricriu'); + game.rescuedIds.add('nuada'); (game as unknown as { enterWaystation(): void }).enterWaystation(); - const resident = game.npcTiles.find(n => n.npcId === '__rescue_bricriu__')!; + const resident = game.npcTiles.find(n => n.npcId === '__rescue_nuada__')!; const atkBefore = game.player.atk; const bump = (): void => { game.player.x = resident.x - 1; game.player.y = resident.y; game.map[resident.x - 1]![resident.y] = Tile.FLOOR; - game.npcTiles = game.npcTiles.filter(n => n.npcId === '__rescue_bricriu__'); + game.npcTiles = game.npcTiles.filter(n => n.npcId === '__rescue_nuada__'); onFloorEvent.mockClear(); game.paused = false; game.handleHeroMove(1, 0); }; bump(); let [event, onChoice] = onFloorEvent.mock.calls[0]!; - expect(event.options[0].label).toContain('Portion'); + expect(event.options[0].label).toContain('draught'); onChoice(0); expect(game.player.atk).toBe(atkBefore + Balance.CONFIG.rescues.portionAtk); - // Second helping refused this floor. + // Second cup refused this floor. bump(); [event, onChoice] = onFloorEvent.mock.calls[0]!; - expect(event.options.some((o: { label: string }) => o.label.includes('Portion'))).toBe(false); + expect(event.options.some((o: { label: string }) => o.label.includes('draught'))).toBe(false); onChoice(0); expect(game.player.atk).toBe(atkBefore + Balance.CONFIG.rescues.portionAtk); // The portion ends at the descent. diff --git a/src/__tests__/vendorOffers.test.ts b/src/__tests__/vendorOffers.test.ts index 7070c38..370c3bf 100644 --- a/src/__tests__/vendorOffers.test.ts +++ b/src/__tests__/vendorOffers.test.ts @@ -117,8 +117,8 @@ describe('VendorOffers', () => { expect(game.nextType).toBe('I'); }); - it("Fedelm (seer) only reads the floors ahead — a single dismissable line", () => { - game.vendorOffers.rescueService(rescue('fedelm')); + it("Bé Chuille (seer) only reads the floors ahead — a single dismissable line", () => { + game.vendorOffers.rescueService(rescue('bechuille')); const ev = cb.ev()!; expect(ev.event.options).toHaveLength(1); ev.onChoice(0); @@ -150,23 +150,23 @@ describe('VendorOffers', () => { expect(game.harperLullFloor).toBe(game.dungeonLevel + 1); }); - it("Bricriu (cook) serves the Champion's Portion — +ATK until next descent", () => { + it("Nuada (cook) pours the king's draught — +ATK until next descent", () => { const before = game.player.atk; - game.vendorOffers.rescueService(rescue('bricriu')); - cb.ev()!.onChoice(0); // eat the portion + game.vendorOffers.rescueService(rescue('nuada')); + cb.ev()!.onChoice(0); // drink the draught expect(game.portionAtkBonus).toBeGreaterThan(0); expect(game.player.atk).toBe(before + game.portionAtkBonus); }); - it("Bricriu offers nothing a second time (the portion is one helping)", () => { - game.vendorOffers.rescueService(rescue('bricriu')); + it("Nuada offers nothing a second time (the cup answers once per descent)", () => { + game.vendorOffers.rescueService(rescue('nuada')); cb.ev()!.onChoice(0); const atkAfterFirst = game.player.atk; - game.vendorOffers.rescueService(rescue('bricriu')); + game.vendorOffers.rescueService(rescue('nuada')); const ev = cb.ev()!; - expect(ev.event.flavor).toMatch(/one portion|entire point/i); + expect(ev.event.flavor).toMatch(/answers once|only ale/i); ev.onChoice(0); - expect(game.player.atk).toBe(atkAfterFirst); // no second helping + expect(game.player.atk).toBe(atkAfterFirst); // no second cup }); }); }); diff --git a/src/data/floor-events.json b/src/data/floor-events.json index c533d64..81a41c0 100644 --- a/src/data/floor-events.json +++ b/src/data/floor-events.json @@ -46,8 +46,8 @@ { "id": "fallen_champion", "emoji": "sprite_equip_iron_sword", - "title": "The Fallen Fian", - "flavor": "A warrior of the Fianna lies here, spear still in hand, felled far from Tara.", + "title": "The Fallen of Mag Tuired", + "flavor": "A warrior of the Tuatha Dé lies here, spear still in hand, carried down into the deep long after the great battle ended.", "options": [ { "label": "Take their boon", @@ -214,8 +214,8 @@ { "id": "salmon_of_knowledge", "emoji": "sprite_crystal", - "title": "The Bradán Feasa", - "flavor": "A salmon glimmers beneath black water, said to hold all the world's wisdom in its flesh — whoever tastes it first will know all things.", + "title": "The Salmon of Segais", + "flavor": "A salmon glimmers beneath black water. Nine hazels overhang the Well of Segais and drop their nuts into it, and the salmon that eat them take up all the wisdom of the world — every drop of which runs down here eventually.", "options": [ { "label": "Taste the salmon", @@ -235,13 +235,13 @@ ] }, { - "id": "warp_spasm_echo", + "id": "neit_fury", "emoji": "sprite_equip_war_axe", - "title": "The Ríastrad's Echo", - "flavor": "The echo of the Hound of Ulster's battle-frenzy lingers here — the warp-spasm that turned Cú Chulainn monstrous in war, one eye swallowed and the other bulging forth.", + "title": "Neit's Fury", + "flavor": "Old war hangs in this stone like smoke. Neit, god of battle, was grandfather to Balor and husband to Nemain, and the rage he lent the Tuatha Dé at Mag Tuired never entirely left the ground it was spent on.", "options": [ { - "label": "Embrace the ríastrad", + "label": "Take up Neit's rage", "desc": "+14 ATK, −20 Max HP.", "handler": "bargain_accept", "params": { @@ -253,7 +253,7 @@ "label": "Master your fury", "desc": "Nothing happens.", "handler": "static_message", - "resultMsg": "You steady your breath. The warp-spasm passes you by, this time." + "resultMsg": "You steady your breath. The war-god's rage passes you by, this time." } ] }, diff --git a/src/data/npcs.json b/src/data/npcs.json index 15a734b..ff941c3 100644 --- a/src/data/npcs.json +++ b/src/data/npcs.json @@ -80,14 +80,15 @@ "returnLine": "Still the same shape as last time, I see. A pity. You'd make a passable eagle." }, { - "id": "scathach", - "char": "npc_scathach", - "name": "Scáthach of the Shadows", + "id": "ogma", + "char": "npc_ogma", + "name": "Ogma Grianainech", "kind": "flavor", "lines": [ - "I trained the Hound of Ulster himself on the Isle of Shadows, and even he needed the salmon-leap shown twice. Keep your feet under you when the stone falls — that is the whole of war, in the end.", + "I cut the first ogham into birch and stood it up as a warning, and men have been reading warnings ever since. Keep your feet under you when the stone falls — that is the whole of war, in the end.", "You grip your weapon like a farmer. Loosen the wrist. There — now you might live past the next floor.", - "I gave Cú Chulainn the Gáe Bolg and half a mind to keep it. Bres's deep is full of old students of mine, wanderer. Most of them are bones. Do better." + "At Mag Tuired I took Orna from Tethra's own hand, and when I unsheathed it the blade recited every deed it had done. A boastful thing. But it had earned the boasting.", + "They call me sun-faced. It is the strain of hauling the Fomorian dead off the field, if you want the truth of it. Bres's deep is filling with fresh ones. Do better than they did." ], "returnLine": "Back again, and still breathing. My teaching sticks to you better than it did to most." }, diff --git a/src/data/rescues.json b/src/data/rescues.json index 858ad75..9520a45 100644 --- a/src/data/rescues.json +++ b/src/data/rescues.json @@ -9,22 +9,22 @@ "serviceFlavor": "The Gobán Saor sizes up the rift's falling stone with a builder's contempt. \"Bres sends rubble. Tell me what you need and I'll send you something with a shape to it.\"" }, { - "id": "fedelm", - "char": "npc_fedelm", - "name": "Fedelm the Seeress", + "id": "bechuille", + "char": "npc_bechuille", + "name": "Bé Chuille the Enchantress", "service": "seer", - "captiveLine": "A woman sits perfectly still among her captors, eyes closed, a cold blue flame guttering at her fingertips. \"I saw you coming,\" she murmurs. \"I saw them falling. Get on with it.\"", - "thanksLine": "Fedelm opens her eyes at last. \"I see crimson on them, and none on you. Well cut. Look for me by the hearth — the future is easier to read out of the wind.\"", - "serviceFlavor": "Fedelm turns her cold flame over in her hands, and shapes appear in it." + "captiveLine": "A woman sits perfectly still among her captors, eyes closed, turning a small grey pebble over and over in her bound hands. \"I asked the stones who was coming,\" she murmurs. \"They said you. Get on with it.\"", + "thanksLine": "Bé Chuille opens her eyes at last. \"At Mag Tuired I swore to Lugh that I would wake the trees and the stones and the very sods of the earth into a host under arms. I keep my oaths. Look for me by the hearth — falling stone has a great deal to say, and I have the ear for it.\"", + "serviceFlavor": "Bé Chuille sets a pebble spinning on her palm. It does not stop, and where it points the dark thins." }, { - "id": "bricriu", - "char": "npc_bricriu", - "name": "Bricriu of the Feast", + "id": "nuada", + "char": "npc_nuada", + "name": "Nuada Airgetlám", "service": "cook", - "captiveLine": "A man in a gold chain manages, somehow, to look put-upon rather than terrified. \"These gentlemen have no appreciation for hospitality. Remove them and my table is yours.\"", - "thanksLine": "Bricriu straightens his chain and bows just slightly too deep to be sincere. \"My savior. Come to my table in the mounds — the Champion's Portion is yours by right, and I never poison the guest of honor.\"", - "serviceFlavor": "Bricriu sweeps a hand over a table that wasn't there a moment ago — steam, fat, honey, and a cut of meat that is unmistakably the hero's." + "captiveLine": "A tall man stands bound among his captors, unbowed, one hand catching the light wrongly — silver, jointed, finer than any hand of flesh. \"They took my kingdom with this arm,\" he says evenly. \"They will not take the arm. Cut them down.\"", + "thanksLine": "Nuada flexes the silver hand and looks at it without fondness. \"Bres held my throne while I healed, and gave the Tuatha Dé nothing but hunger for it — no ale, no fire, no grease on any knife at his table. Come to the mound and I will show you how a king is supposed to feed his people.\"", + "serviceFlavor": "Nuada sets out the ale of Goibniu's feast — the draught that kept the Tuatha Dé from age and from wounds through the long day at Mag Tuired." }, { "id": "airmed", diff --git a/src/data/sprite-map.json b/src/data/sprite-map.json index 9e972cd..dca672d 100644 --- a/src/data/sprite-map.json +++ b/src/data/sprite-map.json @@ -105,12 +105,12 @@ "npc_emissary": { "sheet": "rogues", "sx": 128, "sy": 96, "sw": 32, "sh": 32 }, "npc_stranger": { "sheet": "rogues", "sx": 96, "sy": 0, "sw": 32, "sh": 32 }, "npc_goban": { "sheet": "rogues", "sx": 0, "sy": 96, "sw": 32, "sh": 32 }, - "npc_fedelm": { "sheet": "rogues", "sx": 160, "sy": 128, "sw": 32, "sh": 32 }, - "npc_bricriu": { "sheet": "rogues", "sx": 96, "sy": 64, "sw": 32, "sh": 32 }, + "npc_bechuille": { "sheet": "rogues", "sx": 160, "sy": 128, "sw": 32, "sh": 32 }, + "npc_nuada": { "sheet": "rogues", "sx": 96, "sy": 64, "sw": 32, "sh": 32 }, "npc_airmed": { "sheet": "rogues", "sx": 32, "sy": 64, "sw": 32, "sh": 32 }, "npc_abcan": { "sheet": "rogues", "sx": 0, "sy": 64, "sw": 32, "sh": 32 }, "npc_tuan": { "sheet": "rogues", "sx": 128, "sy": 64, "sw": 32, "sh": 32 }, - "npc_scathach": { "sheet": "rogues", "sx": 160, "sy": 96, "sw": 32, "sh": 32 }, + "npc_ogma": { "sheet": "rogues", "sx": 160, "sy": 96, "sw": 32, "sh": 32 }, "npc_cliodhna": { "sheet": "rogues", "sx": 64, "sy": 64, "sw": 32, "sh": 32 }, "npc_dagda": { "sheet": "rogues", "sx": 32, "sy": 128, "sw": 32, "sh": 32 }, diff --git a/src/game.ts b/src/game.ts index b796d14..fcd09ca 100644 --- a/src/game.ts +++ b/src/game.ts @@ -139,8 +139,8 @@ export class Game { public pendingGuardKey: string | null = null; /** The live captor monsters — the captive can't be freed until every one is dead. */ public rescueGuards: Monster[] = []; // public: read/written by SaveGame - /** ATK granted by Bricriu's Champion's Portion, reverted on the next descent. */ - public portionAtkBonus = 0; // public: read/written by VendorOffers (Bricriu) and descent revert + /** ATK granted by Nuada's draught from Goibniu's feast, reverted on the next descent. */ + public portionAtkBonus = 0; // public: read/written by VendorOffers (Nuada) and descent revert /** While the first-run tutorial is teaching, natural enemy spawns are suppressed — the tutorial introduces its own single practice foe (see spawnTutorialFoe). */ public tutorialSafety = false; @@ -1425,7 +1425,7 @@ export class Game { this.pendingRescueId = null; this.pendingGuardKey = null; this.rescueGuards = []; - // Bricriu's Champion's Portion is a single meal — it ends at the descent. + // Nuada's draught is a single cup — it ends at the descent. if (this.portionAtkBonus > 0) { this.player.atk -= this.portionAtkBonus; this.portionAtkBonus = 0; diff --git a/src/vendorOffers.ts b/src/vendorOffers.ts index 7144f11..e59e3b2 100644 --- a/src/vendorOffers.ts +++ b/src/vendorOffers.ts @@ -7,8 +7,8 @@ import type { Game } from './game'; /** * The run's in-world vendors and reward stalls: the altar (boon pick), the * tattoo artist (Ogham brands), the Fear Dearg's shop (gold sink), and the - * rescued-NPC services (the Gobán Saor's shaped stone, Fedelm's boss omen, - * Airmed's herbs, Abcán's lullaby, Bricriu's Champion's Portion). Each builds a + * rescued-NPC services (the Gobán Saor's shaped stone, Bé Chuille's boss omen, + * Airmed's herbs, Abcán's lullaby, Nuada's draught). Each builds a * modal payload and hands it to the host via `cb`. Composed onto {@link Game}; * holds no state of its own — gold, boons, brands and the run flags it touches * all live on Game. @@ -148,7 +148,7 @@ export class VendorOffers { event = { id: `__service_${rescue.id}__`, emoji: rescue.char, title: rescue.name, flavor: `${rescue.serviceFlavor} "I see crimson at floor ${nextBossFloor} — ${boss.name} waits there, and knows you are coming.${smithLine}"`, - options: [{ label: 'Thank her', desc: '', apply: (): string => 'The flame gutters out. Fedelm is already looking at something else — something further down.' }], + options: [{ label: 'Thank her', desc: '', apply: (): string => 'The pebble drops into her palm and lies still. Bé Chuille is already listening to something else — something further down.' }], }; } else if (rescue.service === 'healer') { const herbs = g.herbsCarried; @@ -214,22 +214,22 @@ export class VendorOffers { event = { id: `__service_${rescue.id}__`, emoji: rescue.char, title: rescue.name, flavor: fed - ? 'Bricriu spreads his hands over an empty table. "The Champion\'s Portion is one portion. That is the entire point of it, hero."' + ? 'Nuada turns the empty cup over with his silver hand. "Goibniu\'s ale answers once between descents. Drink it twice and it is only ale."' : rescue.serviceFlavor, options: fed - ? [{ label: 'Leave the table', desc: '', apply: (): string => 'You leave the table before he starts a feud about it.' }] + ? [{ label: 'Leave the table', desc: '', apply: (): string => 'You leave the king to his fire and his silver hand.' }] : [ { - label: "Eat the Champion's Portion", + label: "Drink the king's draught", desc: `+${atk} ATK until your next descent.`, apply: (game: Game): string => { game.portionAtkBonus = atk; game.player.atk += atk; game.pushUI(); - return `You eat the hero's cut while Bricriu watches everyone else not eating it. +${atk} ATK until the next descent.`; + return `The ale of Goibniu's feast goes down like a forge-wind, and Nuada pours for you the way a king ought to. +${atk} ATK until the next descent.`; }, }, - { label: 'Decline politely', desc: '', apply: (): string => '"Extraordinary," Bricriu says, delighted. "A hero with manners. The portion keeps."' }, + { label: 'Decline politely', desc: '', apply: (): string => '"A guest who refuses is still a guest," Nuada says. "The cup keeps."' }, ], }; }