diff --git a/src/checkmodal.ts b/src/checkmodal.ts index f58f06a..5f40651 100644 --- a/src/checkmodal.ts +++ b/src/checkmodal.ts @@ -1,5 +1,5 @@ import {DrawPile} from "./drawpile.ts"; -import {CheckData, CheckDataOption} from "./newmap.ts"; +import {CheckData, CheckDataOption, ChoiceOption} from "./newmap.ts"; import {getPartLocation, withCamera} from "./layout.ts"; import {AlignX, AlignY, Point, Rect, Size} from "./engine/datatypes.ts"; import {D} from "./engine/public.ts"; @@ -81,21 +81,34 @@ export class CheckModal { let options = check.options; - let addOptionButton = (option: CheckDataOption, rect: Rect) => { - let skill = option.skill(); - let skillName = getSkills().get(skill).profile.name; - let hasSkill = getPlayerProgress().hasLearned(skill); - // hasSkill ||= true; - let optionLabel: string - if (hasSkill) { - optionLabel = `[${skillName}] ${option.unlockable}`; + let addOptionButton = (option: CheckDataOption | ChoiceOption, rect: Rect) => { + let accomplished: boolean; + let optionLabel: string; + let resultMessage: string; + if ((option as ChoiceOption).isChoice) { + // TODO: Use OOP here + option = option as ChoiceOption; + accomplished = option.countsAsSuccess; + optionLabel = option.unlockable; + resultMessage = option.success; + } else { - optionLabel = `[Needs ${skillName}] ${option.locked}`; + option = option as CheckDataOption; + let skill = option.skill(); + let skillName = getSkills().get(skill).profile.name; + let hasSkill = getPlayerProgress().hasLearned(skill); + // hasSkill ||= true; + if (hasSkill) { + optionLabel = `[${skillName}] ${option.unlockable}`; + } else { + optionLabel = `[Needs ${skillName}] ${option.locked}`; + } + resultMessage = hasSkill ? option.success : option.failure; } addButton(this.#drawpile, optionLabel, rect, true, () => { - this.#success = hasSkill ? option.success : option.failure; + this.#success = resultMessage; - if (hasSkill) { + if (accomplished) { let cb = this.#callback; if (cb) { cb(); } } diff --git a/src/huntmode.ts b/src/huntmode.ts index bdc202f..e49395f 100644 --- a/src/huntmode.ts +++ b/src/huntmode.ts @@ -251,7 +251,7 @@ export function initHuntMode(huntMode: HuntMode) { export function getHuntMode() { if (active == null) { - throw `trying to get player progress before it has been initialized` + throw new Error(`trying to get hunt mode before it has been initialized`) } return active; } diff --git a/src/manormap.ts b/src/manormap.ts index d28b62c..1425cf3 100644 --- a/src/manormap.ts +++ b/src/manormap.ts @@ -1,7 +1,8 @@ import {Architecture, LoadedNewMap} from "./newmap.ts"; import {Grid, Point} from "./engine/datatypes.ts"; import {getThralls} from "./thralls.ts"; -import {LadderPickup, ThrallPosterPickup} from "./pickups.ts"; +import {LadderPickup, ThrallPosterPickup, ThrallRecruitedPickup} from "./pickups.ts"; +import {getPlayerProgress} from "./playerprogress.ts"; const BASIC_PLAN = Grid.createGridFromMultilineString(` ##################### @@ -29,12 +30,16 @@ export function generateManor(): LoadedNewMap { let cell = map.get(xy); let placeThrall = (ix: number) => { - // TODO cell.architecture = Architecture.Floor; + if (true || getPlayerProgress().isThrallUnlocked(thralls[ix])) { + cell.pickup = new ThrallRecruitedPickup(thralls[ix]); + } }; let placeThrallPoster = (ix: number) => { cell.architecture = Architecture.Floor; - cell.pickup = new ThrallPosterPickup(thralls[ix]); + if (!getPlayerProgress().isThrallUnlocked(thralls[ix])) { + cell.pickup = new ThrallPosterPickup(thralls[ix]); + } }; switch (BASIC_PLAN.get(xy)) { diff --git a/src/mapgen.ts b/src/mapgen.ts index 759fdc6..051a15e 100644 --- a/src/mapgen.ts +++ b/src/mapgen.ts @@ -4,6 +4,7 @@ import {choose, shuffle} from "./utils.ts"; import {standardVaultTemplates, VaultTemplate} from "./vaulttemplate.ts"; import {ALL_STATS} from "./datatypes.ts"; import {ExperiencePickup, LadderPickup, LockPickup, StatPickup, ThrallPickup} from "./pickups.ts"; +import {getPlayerProgress} from "./playerprogress.ts"; const WIDTH = 19; const HEIGHT = 19; @@ -301,9 +302,10 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { let cell = knife.map.get(goodie); if (a.contains(goodie)) { - // TODO: Place the zone's NPC here let thrall = vaultTemplate.thrall(); - cell.pickup = new ThrallPickup(thrall); + if (!getPlayerProgress().isThrallUnlocked(thrall)) { + cell.pickup = new ThrallPickup(thrall); + } } if (b.contains(goodie)) { diff --git a/src/newmap.ts b/src/newmap.ts index e3c8131..53a49dd 100644 --- a/src/newmap.ts +++ b/src/newmap.ts @@ -6,7 +6,14 @@ export enum Architecture { Wall, Floor } export type CheckData = { label: string, - options: CheckDataOption[], + options: (CheckDataOption | ChoiceOption)[], +} + +export type ChoiceOption = { + isChoice: true, + countsAsSuccess: boolean, + unlockable: string, + success: string, } export type CheckDataOption = { skill: () => Skill, diff --git a/src/pickups.ts b/src/pickups.ts index 367df03..156082d 100644 --- a/src/pickups.ts +++ b/src/pickups.ts @@ -1,4 +1,4 @@ -import {getThralls, Thrall} from "./thralls.ts"; +import {getThralls, LifeStage, Thrall} from "./thralls.ts"; import {CellView, CheckData} from "./newmap.ts"; import {getPlayerProgress} from "./playerprogress.ts"; import {getHuntMode, HuntMode, initHuntMode} from "./huntmode.ts"; @@ -9,6 +9,7 @@ import {sprLadder, sprLock, sprResourcePickup, sprStatPickup} from "./sprites.ts import {GridArt} from "./gridart.ts"; import {getCheckModal} from "./checkmodal.ts"; import {Point} from "./engine/datatypes.ts"; +import {choose} from "./utils.ts"; export type Pickup = LockPickup @@ -17,6 +18,7 @@ export type Pickup | LadderPickup | ThrallPickup | ThrallPosterPickup + | ThrallRecruitedPickup export class LockPickup { check: CheckData; @@ -146,7 +148,10 @@ export class ThrallPickup { onClick(cell: CellView): boolean { let data = getThralls().get(this.thrall); - getCheckModal().show(data.initialCheck, () => cell.pickup = null); + getCheckModal().show(data.initialCheck, () => { + getPlayerProgress().unlockThrall(this.thrall); + cell.pickup = null + }); return true; } } @@ -177,3 +182,71 @@ export class ThrallPosterPickup { return true; } } + + +export class ThrallRecruitedPickup { + thrall: Thrall; + bitten: boolean; + + constructor(thrall: Thrall) { + this.thrall = thrall; + this.bitten = false; + } + + computeCostToClick() { return 0; } + + isObstructive() { return false; } + + drawFloor() { } + drawInAir(gridArt: GridArt) { + let data = getThralls().get(this.thrall); + let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); + let ix = 0; + let rot = 0; + + if (lifeStage == LifeStage.Vampirized) { ix = 1; } + if (lifeStage == LifeStage.Dead) { ix = 1; rot = 270; } + D.drawSprite(data.sprite, gridArt.project(0.0), ix, { + xScale: 2.0, + yScale: 2.0, + angle: rot + }) + } + + onClick(_cell: CellView): boolean { + if (this.bitten) { return true; } + + let data = getThralls().get(this.thrall); + let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); + let text = data.lifeStageText[lifeStage]; + getCheckModal().show({ + label: `${text.prebite}`, + options: [ + { + isChoice: true, + countsAsSuccess: true, + unlockable: "Bite!", + success: text.postbite, + }, + { + isChoice: true, + countsAsSuccess: false, + unlockable: "Refrain", + success: "Maybe next time." + } + ] + }, () => { + this.bitten = true; + getPlayerProgress().addBlood( + lifeStage == LifeStage.Fresh ? 1000 : + lifeStage == LifeStage.Average ? 500 : + lifeStage == LifeStage.Poor ? 300 : + lifeStage == LifeStage.Vampirized ? 1500 : // lethal bite + // lifeStage == LifeStage.Dead ? + 100 + ); + getPlayerProgress().damageThrall(this.thrall, choose([0.9])) + }); + return true; + } +} \ No newline at end of file diff --git a/src/playerprogress.ts b/src/playerprogress.ts index 7d952e1..7a8d362 100644 --- a/src/playerprogress.ts +++ b/src/playerprogress.ts @@ -1,5 +1,6 @@ import {ALL_STATS, Skill, Stat, SuccessorOption, Wish} from "./datatypes.ts"; import {getSkills} from "./skills.ts"; +import {getThralls, LifeStage, Thrall} from "./thralls.ts"; export class PlayerProgress { #name: string @@ -12,6 +13,8 @@ export class PlayerProgress { #itemsPurloined: number #skillsLearned: number[] // use the raw ID representation for indexOf #untrimmedSkillsAvailable: Skill[] + #thrallsUnlocked: number[] + #thrallDamage: Record constructor(asSuccessor: SuccessorOption, withWish: Wish | null) { this.#name = asSuccessor.name; @@ -24,6 +27,8 @@ export class PlayerProgress { this.#itemsPurloined = 0; this.#skillsLearned = [] this.#untrimmedSkillsAvailable = []; + this.#thrallsUnlocked = []; + this.#thrallDamage = {}; this.refill(); } @@ -52,6 +57,12 @@ export class PlayerProgress { } } + for (let thrall of getThralls().getAll()) { + let stage = this.getThrallLifeStage(thrall); + if (stage == LifeStage.Vampirized || stage == LifeStage.Dead) { continue; } + this.#thrallDamage[thrall.id] = Math.max(this.#thrallDamage[thrall.id] ?? 0 - 0.2, 0.0); + } + this.#untrimmedSkillsAvailable = learnableSkills } @@ -175,7 +186,38 @@ export class PlayerProgress { } getStats() { return {...this.#stats} } - getTalents() { return {...this.#talents} } } + getTalents() { return {...this.#talents} } + + unlockThrall(thrall: Thrall) { + let {id} = thrall; + if (this.#thrallsUnlocked.indexOf(id) != -1) { return; } + this.#thrallsUnlocked.push(id); + } + + isThrallUnlocked(thrall: Thrall) { + return this.#thrallsUnlocked.indexOf(thrall.id) != -1; + } + + damageThrall(thrall: Thrall, amount: number) { + if (amount <= 0.0) { + throw new Error(`damage must be some positive amount, not ${amount}`) + } + let stage = this.getThrallLifeStage(thrall); + + if (stage == LifeStage.Vampirized) { this.#thrallDamage[thrall.id] = 4.0; } + this.#thrallDamage[thrall.id] = (this.#thrallDamage[thrall.id] ?? 0.0) + amount + } + + getThrallLifeStage(thrall: Thrall): LifeStage { + let damage = this.#thrallDamage[thrall.id] ?? 0; + console.log(`damage: ${damage}`) + if (damage < 0.5) { return LifeStage.Fresh; } + if (damage < 1.75) { return LifeStage.Average; } + if (damage < 3.0) { return LifeStage.Poor; } + if (damage < 4.0) { return LifeStage.Vampirized; } + return LifeStage.Dead; + } +} let active: PlayerProgress | null = null; @@ -185,7 +227,7 @@ export function initPlayerProgress(asSuccessor: SuccessorOption, withWish: Wish export function getPlayerProgress(): PlayerProgress { if (active == null) { - throw `trying to get player progress before it has been initialized` + throw new Error(`trying to get player progress before it has been initialized`) } return active } \ No newline at end of file diff --git a/src/statemanager.ts b/src/statemanager.ts index 774b930..36d978f 100644 --- a/src/statemanager.ts +++ b/src/statemanager.ts @@ -22,8 +22,8 @@ export class StateManager { startGame(asSuccessor: SuccessorOption, withWish: Wish | null) { this.#turn = 1; - initHuntMode(new HuntMode(1, generateManor())); initPlayerProgress(asSuccessor, withWish); + initHuntMode(new HuntMode(1, generateManor())); } advance() { diff --git a/src/thralls.ts b/src/thralls.ts index 47b7ca1..2c0c539 100644 --- a/src/thralls.ts +++ b/src/thralls.ts @@ -57,6 +57,21 @@ export type ThrallData = { sprite: Sprite, posterCheck: CheckData, initialCheck: CheckData, + + lifeStageText: Record +} + +export enum LifeStage { + Fresh = "fresh", + Average = "average", + Poor = "poor", + Vampirized = "vampirized", + Dead = "dead", +} + +export type LifeStageText = { + prebite: string, + postbite: string, } let table = new ThrallsTable(); @@ -94,7 +109,29 @@ export let thrallParty = table.add({ success: "TODO", }, ] - } + }, + lifeStageText: { + fresh: { + prebite: "Garrett flips a poker chip and mutters to himself.", + postbite: "You plunge your fangs into his feathered neck and feed.", + }, + average: { + prebite: "Garrett looks a little less fresh than last time. He's resigned to the fate of being bitten.", + postbite: "You puncture him in almost the same place as before and take a moderate amount of blood from his veins." + }, + poor: { + prebite: "Garrett, limp in bed, doesn't look like he's doing so well. He's pale and he's breathing heavily.", + postbite: "\"Please...\" you hear him moan as you force him into the state of ecstasy that brings compliance.", + }, + vampirized: { + prebite: "Garrett looks about as cold and pale as you. Another bite may kill him.", + postbite: "The final bite is always the most satisfying. You feel little emotion as you hold the body of a dead crow in your arms.", + }, + dead: { + prebite: "This bird is dead, on account of the fact that you killed him with your teeth.", + postbite: "The blood in his veins hasn't coagulated yet. There's still more. Still more...", + } + }, }) export let thrallLore = table.add({ @@ -122,7 +159,29 @@ export let thrallLore = table.add({ success: "Taken aback at how well you know the cheer, he freezes -- then joins you with a similar howl.", }, ] - } + }, + lifeStageText: { + fresh: { + prebite: "Lupin awoos quietly to himself.", + postbite: "You bite the raccoon and drink his blood.", + }, + average: { + prebite: "The color in Lupin's cheeks is beginning to fade. He's becoming accustomed to your bite.", + postbite: "He'll let you do anything to him if you make him feel good, so you make him feel good. Fresh blood...", + }, + poor: { + prebite: "Lupin is barely conscious. There's drool at the edges of his mouth and his eyes are glassy.", + postbite: "This is no concern to you. You're hungry. You need this.", + }, + vampirized: { + prebite: "Lupin's fangs have erupted partially from his jaw. You've taken enough. More will kill him.", + postbite: "His life is less valuable to you than his warm, delicious blood. You need sustenance.", + }, + dead: { + prebite: "This dead raccoon used to be full of blood. Now he's empty. Isn't that a shame?", + postbite: "You root around in his neck. His decaying muscle is soft.", + } + }, }) export let thrallBat = table.add({ @@ -150,7 +209,29 @@ export let thrallBat = table.add({ success: "\"Settle down!\" she says, lowering your volume with a sweep of her hand. \"It's true though.\"", }, ] - } + }, + lifeStageText: { + fresh: { + prebite: "Monica nibbles a pastry.", + postbite: "You dig your teeth into the koala's mortal flesh.", + }, + average: { + prebite: "Monica doesn't look as fresh and vibrant as you recall from her TV show.", + postbite: "A little bite seems to improve her mood, even though she twitches involuntarily as if you're hurting her.", + }, + poor: { + prebite: "Monica weakly raises a hand as if to stop you from approaching for a bite.", + postbite: "You press yourself to her body and embrace her. Her fingers curl around you and she lets you drink your fill.", + }, + vampirized: { + prebite: "Monica shows no interest in food. She's lethargic, apathetic. A bite would kill her, but you're thirsty.", + postbite: "Her last words are too quiet to make out, but you're not interested in them. Nothing matters except blood.", + }, + dead: { + prebite: "This used to be Monica. Now it's just her corpse.", + postbite: "She's very delicate, even as a corpse.", + } + }, }) export let thrallCharm = table.add({ @@ -178,7 +259,29 @@ export let thrallCharm = table.add({ success: "His mind overflows with fantasy, and when you let a glint of fang peek through, he claps his arms affectionately around your supercold torso.", }, ] - } + }, + lifeStageText: { + fresh: { + prebite: "Renfield exposes the underside of his jaw.", + postbite: "You press your face flat to his armorlike scales and part them with your teeth.", + }, + average: { + prebite: "Renfield seems relieved to be free of all that extra blood.", + postbite: "You taste a little bit of fear as you press yourself to him. Is he less devoted than you thought?", + }, + poor: { + prebite: "Renfield presses his face to the window. He won't resist you and won't look at you. He does not want your bite.", + postbite: "Does it matter that he doesn't want your bite? You're hungry. He should have known you would do this.", + }, + vampirized: { + prebite: "Renfield is repulsed by the vampiric features that his body has begun to display. Another bite would kill him.", + postbite: "Better to free him if he's going to behave like this anyways.", + }, + dead: { + prebite: "Here lies a crocodile who really, really liked vampires.", + postbite: "At least in death he can't backslide on his promise to feed you.", + } + }, }) export let thrallStealth = table.add({ @@ -206,7 +309,29 @@ export let thrallStealth = table.add({ success: "TODO", }, ] - } + }, + lifeStageText: { + fresh: { + prebite: "Narthyss is producing a new track on her gamer PC.", + postbite: "You push her mouse and keyboard aside and focus her attention on your eyes.", + }, + average: { + prebite: "Narthyss has no desire to be interrupted, but you're thirsty.", + postbite: "You dazzle her with your eyes and nip her neck with erotic enthusiasm.", + }, + poor: { + prebite: "Narthyss knows better than to resist you -- but you sense that you've taken more than she wants.", + postbite: "Her response to your approach is automatic. No matter what she tells you, you show fang -- she shows neck.", + }, + vampirized: { + prebite: "Narthyss' fire has gone out. She's a creature of venom and blood now. Another bite would kill her.", + postbite: "Now she is a creature of nothing at all.", + }, + dead: { + prebite: "Narthyss used to be a dragon. Now she's dead.", + postbite: "Dragons decay slowly. There's still some warmth in there if you bury your fangs deep enough.", + } + }, }) export let thrallStare = table.add({ @@ -234,5 +359,27 @@ export let thrallStare = table.add({ success: "TODO", }, ] - } + }, + lifeStageText: { + fresh: { + prebite: "Ridley is solving math problems.", + postbite: "You delicately sip electronic blood from the robot's neck." + }, + average: { + prebite: "Ridley's display brightens at your presence. It looks damaged.", + postbite: "Damaged or not -- the robot has blood and you need it badly.", + }, + poor: { + prebite: "The symbols on Ridley's screen have less and less rational connection. It's begging to be fed upon.", + postbite: "The quality of the robot's blood decreases with every bite, but the taste is still pleasurable." + }, + vampirized: { + prebite: "With no concern for its survival, the now-fanged robot begs you for one more bite. This would kill it.", + postbite: "Nothing is stronger than your need for blood -- and its desperation has put you in quite a state...", + }, + dead: { + prebite: "Ridley was a robot and now Ridley is a dead robot.", + postbite: "Tastes zappy.", + } + }, }) \ No newline at end of file