Penance cycle
This commit is contained in:
		| @@ -14,6 +14,7 @@ export type SkillGoverning = { | ||||
|   note: string, | ||||
|   scoring: SkillScoring, | ||||
|   mortalServantValue: number, | ||||
|   flipped: boolean, | ||||
| }; | ||||
| export type SkillProfile = { | ||||
|   name: string, | ||||
| @@ -21,6 +22,7 @@ export type SkillProfile = { | ||||
| } | ||||
|  | ||||
| export type SkillData = { | ||||
|   isDegrading?: boolean; | ||||
|   governing: SkillGoverning, | ||||
|   profile: SkillProfile, | ||||
|   prereqs: Skill[] | ||||
| @@ -35,11 +37,25 @@ export type Skill = { | ||||
| } | ||||
|  | ||||
| export type WishData = { | ||||
|   profile: {name: string}, | ||||
|   profile: { | ||||
|     name: string, | ||||
|     note: string, | ||||
|     domicile: string, | ||||
|     reignSentence: string; | ||||
|     failureName: string, | ||||
|     failureDomicile: string, | ||||
|     failureReignSentence: string, | ||||
|     failureSuccessorVerb: string; | ||||
|   }, | ||||
|   isRandomlyAvailable: boolean, | ||||
|   isCompulsory: boolean; | ||||
|   bannedSkills: () => Skill[], | ||||
|   discouragedSkills: () => Skill[], | ||||
|   encouragedSkills: () => Skill[], | ||||
|   requiredSkills: () => Skill[] | ||||
|   prologue: VNScene, | ||||
|   onVictory: VNScene, | ||||
|   onFailure: VNScene, | ||||
| } | ||||
| export type Wish = { | ||||
|   id: number | ||||
| @@ -61,6 +77,9 @@ export type Ending = { | ||||
| export type EndingPersonal = { | ||||
|   rank: string, | ||||
|   domicile: string, | ||||
|   reignSentence: string, | ||||
|   successorVerb: string, | ||||
|   progenerateVerb: string, | ||||
| } | ||||
|  | ||||
| export type EndingAnalytics = { | ||||
| @@ -74,6 +93,9 @@ export type SuccessorOption = { | ||||
|   title: string, | ||||
|   note: string | null, // ex "already a vampire" | ||||
|   stats: Record<Stat, number>, | ||||
|   talents: Record<Stat, number> | ||||
|   talents: Record<Stat, number>, | ||||
|   skills: Skill[], | ||||
|   inPenance: boolean; | ||||
|   isCompulsory: boolean; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -50,19 +50,23 @@ export class EndgameModal { | ||||
|   } | ||||
|  | ||||
|   get #canProgenerate(): boolean { | ||||
|     return this.#selectedSuccessor != null && this.#selectedWish != null; | ||||
|     return this.#selectedSuccessor != null; | ||||
|   } | ||||
|  | ||||
|   #progenerate() { | ||||
|     let successor = | ||||
|       this.#ending!.successorOptions[this.#selectedSuccessor!]; | ||||
|     let wish = | ||||
|       this.#ending!.wishOptions[this.#selectedWish!]; | ||||
|       this.#selectedWish != null | ||||
|         ? this.#ending!.wishOptions[this.#selectedWish!] | ||||
|         : null; | ||||
|     this.#ending = null; | ||||
|     getStateManager().startGame(successor, wish); | ||||
|   } | ||||
|  | ||||
|   #update() { | ||||
|     this.#fixCompulsory(); | ||||
|  | ||||
|     this.#drawpile.clear(); | ||||
|     if (this.#page == 0) { | ||||
|       let analytics = this.#ending?.analytics; | ||||
| @@ -81,7 +85,7 @@ export class EndgameModal { | ||||
|         let whereLabel = | ||||
|           mortalServants >= 25 ? "where you live with many friends." : | ||||
|           mortalServants >= 1 ? "where you live with a couple of friends." : | ||||
|           "where you live completely alone."; | ||||
|           "where you live without friends."; | ||||
|         D.drawText(whereLabel, new Point(0, 160), FG_TEXT) | ||||
|         D.drawText("You have achieved:", new Point(0, 192), FG_TEXT) | ||||
|         let itemsPurloinedText = itemsPurloined == 1 ? "item purloined" : "items purloined"; | ||||
| @@ -107,11 +111,12 @@ export class EndgameModal { | ||||
|           msg = "That feels like a lot!" | ||||
|         } | ||||
|         D.drawText(msg, new Point(0, 288), FG_TEXT) | ||||
|         D.drawText("Your reign continues unimpeded from the shadows. It is now time to", new Point(0, 320), FG_TEXT, {forceWidth: WIDTH}) | ||||
|         let reignSentence = this.#ending?.personal?.reignSentence ?? "Your reign is in an unknown state."; | ||||
|         D.drawText(`${reignSentence} It is now time to`, new Point(0, 320), FG_TEXT, {forceWidth: WIDTH}) | ||||
|       }) | ||||
|       addButton( | ||||
|         this.#drawpile, | ||||
|         "Appoint a Successor", | ||||
|         this.#ending?.personal?.successorVerb ?? "Do Unknown Things", | ||||
|         new Rect( | ||||
|           new Point(0, HEIGHT - 32), new Size(WIDTH, 32) | ||||
|         ), | ||||
| @@ -126,17 +131,21 @@ export class EndgameModal { | ||||
|         D.drawText("Choose your successor:", new Point(0, 0), FG_TEXT); | ||||
|       }) | ||||
|  | ||||
|       this.#addCandidate(0, new Point(0, 32)) | ||||
|       this.#addCandidate(1, new Point(0, 96)) | ||||
|       this.#addCandidate(2, new Point(0, 160)) | ||||
|       this.#addCandidate(0, new Point(0, 16)) | ||||
|       this.#addCandidate(1, new Point(0, 80)) | ||||
|       this.#addCandidate(2, new Point(0, 144)) | ||||
|  | ||||
|       let optionalNote = " (optional, punishes failure)"; | ||||
|       if (this.#hasCompulsoryWish) { | ||||
|         optionalNote = ""; | ||||
|       } | ||||
|       this.#drawpile.add(0, () => { | ||||
|         D.drawText("Plan their destiny:", new Point(0, 240), FG_TEXT); | ||||
|         D.drawText(`Plan their destiny:${optionalNote}`, new Point(0, 224), FG_TEXT); | ||||
|       }) | ||||
|  | ||||
|       this.#addWish(0, new Point(0, 272)) | ||||
|       this.#addWish(1, new Point(128, 272)) | ||||
|       this.#addWish(2, new Point(256, 272)) | ||||
|       this.#addWish(1, new Point(0, 240)) | ||||
|       this.#addWish(0, new Point(128, 240)) | ||||
|       this.#addWish(2, new Point(256, 240)) | ||||
|  | ||||
|       addButton( | ||||
|         this.#drawpile, | ||||
| @@ -151,7 +160,7 @@ export class EndgameModal { | ||||
|       ) | ||||
|       addButton( | ||||
|         this.#drawpile, | ||||
|         "Progenerate", | ||||
|         this.#ending?.personal.progenerateVerb ?? "Unknown Action", | ||||
|         new Rect( | ||||
|           new Point(WIDTH/3, HEIGHT - 32), new Size(WIDTH - WIDTH / 3, 32) | ||||
|         ), | ||||
| @@ -163,6 +172,61 @@ export class EndgameModal { | ||||
|     } | ||||
|  | ||||
|     this.#drawpile.executeOnClick(); | ||||
|     this.#fixCompulsory(); | ||||
|   } | ||||
|  | ||||
|   #fixCompulsory() { | ||||
|     // allow player to select freely between compulsory options | ||||
|     { | ||||
|       let candidates = this.#ending?.successorOptions ?? []; | ||||
|       let selectedSuccessor = this.#selectedSuccessor; | ||||
|       let compulsorySelected = false; | ||||
|       if (selectedSuccessor) { | ||||
|         compulsorySelected = candidates[selectedSuccessor]?.isCompulsory; | ||||
|       } | ||||
|  | ||||
|       if (!compulsorySelected) { | ||||
|         for (let c = 0; c < candidates.length; c++) { | ||||
|           if (candidates[c]?.isCompulsory) { | ||||
|             this.#selectedSuccessor = c; | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     { | ||||
|       let wishes = this.#ending?.wishOptions ?? []; | ||||
|       let selectedWish = this.#selectedWish; | ||||
|       let compulsorySelected = false; | ||||
|       if (selectedWish) { | ||||
|         let wish = wishes[selectedWish]; | ||||
|         if (wish) { | ||||
|           let data = getWishes().get(wish); | ||||
|           compulsorySelected = data.isCompulsory; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (!compulsorySelected) { | ||||
|         for (let w = 0; w < wishes.length; w++) { | ||||
|           if (getWishes().get(wishes[w]).isCompulsory) { | ||||
|             this.#selectedWish = w; | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   get #hasCompulsoryWish(): boolean { | ||||
|     let wishes = this.#ending?.wishOptions ?? []; | ||||
|  | ||||
|     for (let w = 0; w < wishes.length; w++) { | ||||
|       if (getWishes().get(wishes[w]).isCompulsory) { | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   #addCandidate(ix: number, at: Point) { | ||||
| @@ -214,6 +278,9 @@ export class EndgameModal { | ||||
|           if (talentValue > 0) { | ||||
|             D.drawText(`(+${talentValue})`, at.offset(xys[i].offset(new Point(56, 0))), fg) | ||||
|           } | ||||
|           if (talentValue < 0) { | ||||
|             D.drawText(`(${talentValue})`, at.offset(xys[i].offset(new Point(56, 0))), fg) | ||||
|           } | ||||
|           i += 1; | ||||
|         } | ||||
|  | ||||
| @@ -225,7 +292,11 @@ export class EndgameModal { | ||||
|       enabled, | ||||
|  | ||||
|       () => { | ||||
|         this.#selectedSuccessor = ix; | ||||
|         if (this.#selectedSuccessor == ix) { | ||||
|           this.#selectedSuccessor = null | ||||
|         } else { | ||||
|           this.#selectedSuccessor = ix; | ||||
|         } | ||||
|       }); | ||||
|   } | ||||
|  | ||||
| @@ -235,9 +306,12 @@ export class EndgameModal { | ||||
|       return; | ||||
|     } | ||||
|     let wishOption = wishOptions[ix]; | ||||
|     if (wishOption == null) { | ||||
|       return; | ||||
|     } | ||||
|     let selected = this.#selectedWish == ix; | ||||
|     let w = 128; | ||||
|     let h = 72; | ||||
|     let h = 88; | ||||
|     let generalRect = new Rect(at, new Size(w, h)); | ||||
|     let enabled = true; | ||||
|  | ||||
| @@ -262,12 +336,19 @@ export class EndgameModal { | ||||
|           alignX: AlignX.Center, | ||||
|           alignY: AlignY.Middle, | ||||
|         }); | ||||
|         D.drawText(wishData.profile.note, at.offset(new Point(w / 2, h)), FG_TEXT, { | ||||
|           alignX: AlignX.Center | ||||
|         }); | ||||
|       }, | ||||
|       generalRect, | ||||
|       enabled, | ||||
|  | ||||
|       () => { | ||||
|         this.#selectedWish = ix; | ||||
|         if (this.#selectedWish == ix) { | ||||
|           this.#selectedWish = null; | ||||
|         } else { | ||||
|           this.#selectedWish = ix; | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -25,9 +25,12 @@ export class Hud { | ||||
|       D.drawText(`${s}`, new Point(0, y), FG_BOLD) | ||||
|       D.drawText(`${prog.getStat(s)}`, new Point(32, y), FG_TEXT) | ||||
|       let talent = prog.getTalent(s); | ||||
|       if (talent) { | ||||
|       if (talent > 0) { | ||||
|         D.drawText(`(+${talent})`, new Point(56, y), FG_TEXT) | ||||
|       } | ||||
|       if (talent < 0) { | ||||
|         D.drawText(`(${talent})`, new Point(56, y), FG_TEXT) | ||||
|       } | ||||
|       y += 16; | ||||
|     } | ||||
|     D.drawText("EXP", new Point(0, 144), FG_BOLD); | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import {hostGame} from "./engine/internal/host.ts"; | ||||
| import {game} from "./game.ts"; | ||||
| import {getStateManager} from "./statemanager.ts"; | ||||
| import {batFreak} from "./wishes.ts"; | ||||
|  | ||||
| getStateManager().startGame({ | ||||
|   name: "Pyrex", | ||||
| @@ -9,5 +8,8 @@ getStateManager().startGame({ | ||||
|   note: null, | ||||
|   stats: {AGI: 10, INT: 10, CHA: 10, PSI: 10}, | ||||
|   talents: {AGI: 0, INT: 0, CHA: 0, PSI: 0}, | ||||
| }, batFreak); | ||||
|   skills: [], | ||||
|   isCompulsory: false, | ||||
|   inPenance: false, | ||||
| }, null); | ||||
| hostGame(game); | ||||
| @@ -5,6 +5,7 @@ export class PlayerProgress { | ||||
|   #name: string | ||||
|   #stats: Record<Stat, number> | ||||
|   #talents: Record<Stat, number> | ||||
|   #isInPenance: boolean; | ||||
|   #wish: Wish | null; | ||||
|   #exp: number; | ||||
|   #blood: number | ||||
| @@ -16,6 +17,7 @@ export class PlayerProgress { | ||||
|     this.#name = asSuccessor.name; | ||||
|     this.#stats = {...asSuccessor.stats}; | ||||
|     this.#talents = {...asSuccessor.talents}; | ||||
|     this.#isInPenance = asSuccessor.inPenance; | ||||
|     this.#wish = withWish; | ||||
|     this.#exp = 0; | ||||
|     this.#blood = 0; | ||||
| @@ -28,7 +30,7 @@ export class PlayerProgress { | ||||
|  | ||||
|   applyEndOfTurn() { | ||||
|     for (let stat of ALL_STATS.values()) { | ||||
|       this.#stats[stat] += this.#talents[stat]; | ||||
|       this.add(stat, this.#talents[stat]); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -36,11 +38,15 @@ export class PlayerProgress { | ||||
|     return this.#name; | ||||
|   } | ||||
|  | ||||
|   get isInPenance(): boolean { | ||||
|     return this.#isInPenance; | ||||
|   } | ||||
|  | ||||
|   refill() { | ||||
|     this.#blood = 2000; | ||||
|  | ||||
|     let learnableSkills = [];  // TODO: Also include costing info | ||||
|     for (let skill of getSkills().getAllAvailableSkills().values()) { | ||||
|     for (let skill of getSkills().getAvailableSkills(this.#isInPenance).values()) { | ||||
|       if (this.#canBeAvailable(skill)) { | ||||
|         learnableSkills.push(skill); | ||||
|       } | ||||
| @@ -99,10 +105,8 @@ export class PlayerProgress { | ||||
|     if (amount != Math.floor(amount)) { | ||||
|       throw `stat increment must be integer: ${amount}` | ||||
|     } | ||||
|     if (amount <= 0) { | ||||
|       throw `stat increment must be >0: ${amount}` | ||||
|     } | ||||
|     this.#stats[stat] += amount; | ||||
|     this.#stats[stat] = Math.min(Math.max(this.#stats[stat], -99), 999); | ||||
|   } | ||||
|  | ||||
|   addExperience(amt: number) { | ||||
| @@ -169,7 +173,9 @@ export class PlayerProgress { | ||||
|     } | ||||
|     return learnedSkills; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   getStats() { return {...this.#stats} } | ||||
|   getTalents() { return {...this.#talents} } } | ||||
|  | ||||
| let active: PlayerProgress | null = null; | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import {getPlayerProgress} from "./playerprogress.ts"; | ||||
| import {getSkills} from "./skills.ts"; | ||||
| import {Ending, SCORING_CATEGORIES, ScoringCategory} from "./datatypes.ts"; | ||||
| import {sceneBat, sceneCharm, sceneLore, sceneParty, sceneStare, sceneStealth} from "./endings.ts"; | ||||
| import {generateWishes} from "./wishes.ts"; | ||||
| import {generateWishes, getWishes, isWishCompleted} from "./wishes.ts"; | ||||
| import {generateSuccessors} from "./successors.ts"; | ||||
|  | ||||
| class Scorer { | ||||
| @@ -26,7 +26,7 @@ class Scorer { | ||||
|       vampiricSkills += 1; | ||||
|     } | ||||
|  | ||||
|     mortalServants = Math.floor(mortalServants); | ||||
|     mortalServants = Math.max(Math.floor(mortalServants), 0); | ||||
|  | ||||
|     // NOTE: This approach isn't efficient but it's easy to understand | ||||
|     // and it allows me to arbitrate ties however I want | ||||
| @@ -49,40 +49,68 @@ class Scorer { | ||||
|     let scene: VNScene; | ||||
|     let rank: string; | ||||
|     let domicile: string; | ||||
|     let reignSentence: string; | ||||
|     let penance: boolean = false; | ||||
|     let successorVerb: string = "Appoint a Successor"; | ||||
|  | ||||
|     // Let the player | ||||
|     let wish = getPlayerProgress().getWish(); | ||||
|     if (wish != null) { | ||||
|       let data = getWishes().get(wish); | ||||
|       if (isWishCompleted(wish)) { | ||||
|         scene = data.onVictory | ||||
|         rank = data.profile.name; | ||||
|         domicile = data.profile.domicile; | ||||
|         reignSentence = data.profile.reignSentence; | ||||
|       } else { | ||||
|         scene = data.onFailure; | ||||
|         rank = data.profile.failureName; | ||||
|         domicile = data.profile.failureDomicile; | ||||
|         reignSentence = data.profile.failureReignSentence; | ||||
|         penance = true; | ||||
|         successorVerb = data.profile.failureSuccessorVerb; | ||||
|       } | ||||
|  | ||||
|     } | ||||
|     // TODO: Award different ranks depending on second-to-top skill | ||||
|     // TODO: Award different domiciles based on overall score | ||||
|     // TODO: Force the rank to match the wish if one existed | ||||
|     if (isMax("stare", 3)) { | ||||
|     else if (isMax("stare", 3)) { | ||||
|       scene = sceneStare; | ||||
|       rank = "Hypno-Chiropteran"; | ||||
|       domicile = "Village of Brainwashed Mortals"; | ||||
|       reignSentence = "You rule with a fair but unflinching gaze."; | ||||
|     } | ||||
|     else if (isMax("lore", 3)) { | ||||
|       scene = sceneLore; | ||||
|       rank = "Loremaster"; | ||||
|       domicile = "Vineyard"; | ||||
|       reignSentence = "You're well on the path to ultimate knowledge."; | ||||
|     } | ||||
|     else if (isMax("charm", 2)) { | ||||
|       scene = sceneCharm; | ||||
|       rank = "Seducer"; | ||||
|       domicile = "Guest House"; | ||||
|       reignSentence = "You get to sink your fangs into anyone you want."; | ||||
|     } | ||||
|     else if (isMax("party", 1)) { | ||||
|       scene = sceneParty; | ||||
|       rank = "Party Animal"; | ||||
|       domicile = "Nightclub"; | ||||
|       reignSentence = "Everyone thinks you're too cool to disobey."; | ||||
|     } | ||||
|     else if (isMax("stealth", 0)) { | ||||
|       scene = sceneStealth; | ||||
|       rank = "Invisible"; | ||||
|       domicile = "Townhouse"; | ||||
|       reignSentence = "People don't see you but they do most of what you want."; | ||||
|     } | ||||
|     // if (isMax("bat")) { | ||||
|     else { | ||||
|       scene = sceneBat; | ||||
|       rank = "Bat"; | ||||
|       domicile = "Cave"; | ||||
|       reignSentence = "Your skreeking verdicts are irresistible to your subjects."; | ||||
|     } | ||||
|  | ||||
|     // TODO: Analytics tracker | ||||
| @@ -91,12 +119,14 @@ class Scorer { | ||||
|       vampiricSkills, | ||||
|       mortalServants, | ||||
|     } | ||||
|     let successorOptions = generateSuccessors(0);  // TODO: generate nImprovements from mortalServants and the player's bsae improvements | ||||
|     let wishOptions = generateWishes(); | ||||
|     let successorOptions = generateSuccessors(0, penance);  // TODO: generate nImprovements from mortalServants and the player's bsae improvements | ||||
|     let wishOptions = generateWishes(penance); | ||||
|  | ||||
|     let progenerateVerb = penance ? "Repent" : "Progenerate"; | ||||
|  | ||||
|     return { | ||||
|       scene, | ||||
|       personal: {rank, domicile}, | ||||
|       personal: {rank, domicile, reignSentence, successorVerb, progenerateVerb}, | ||||
|       analytics, | ||||
|       successorOptions, | ||||
|       wishOptions, | ||||
|   | ||||
| @@ -19,9 +19,11 @@ class SkillsTable { | ||||
|     return this.#skills[skill.id] | ||||
|   } | ||||
|  | ||||
|   getAllAvailableSkills(): Skill[] { | ||||
|   getAvailableSkills(includeDegrading: boolean): Skill[] { | ||||
|     let skills = []; | ||||
|     for (let i = 0; i < this.#skills.length; i++) { | ||||
|       let isDegrading = this.#skills[i].isDegrading ?? false; | ||||
|       if (isDegrading && !includeDegrading) { continue; } | ||||
|       skills.push({id: i}); | ||||
|     } | ||||
|     return skills; | ||||
| @@ -35,6 +37,10 @@ class SkillsTable { | ||||
|       governingStatValue += getPlayerProgress().getStat(stat) / data.governing.stats.length; | ||||
|     } | ||||
|  | ||||
|     if (data.governing.flipped) { | ||||
|       governingStatValue = - governingStatValue + 10; | ||||
|     } | ||||
|  | ||||
|     let mult = getCostMultiplier(getPlayerProgress().getWish(), skill); | ||||
|     let [underTarget, target] = [data.governing.underTarget, data.governing.target]; | ||||
|     underTarget = mult * underTarget; | ||||
| @@ -62,14 +68,14 @@ function geomInterpolate( | ||||
|   return lowOut * Math.pow(highOut / lowOut, proportion) | ||||
| } | ||||
|  | ||||
| type Difficulty = 0 | 1 | 2 | 3 | ||||
| type Difficulty = 0 | 1 | 1.25 | 2 | 3 | ||||
| type GoverningTemplate = { | ||||
|   stats: Stat[], | ||||
|   note: string | ||||
|   scoring: SkillScoring, | ||||
| } | ||||
|  | ||||
| type Track = "bat" | "stealth" | "charm" | "stare" | "party" | "lore" | ||||
| type Track = "bat" | "stealth" | "charm" | "stare" | "party" | "lore" | "penance" | ||||
| let templates: Record<Track, GoverningTemplate> = { | ||||
|   bat: { | ||||
|     stats: ["AGI", "AGI", "PSI"], | ||||
| @@ -101,9 +107,14 @@ let templates: Record<Track, GoverningTemplate> = { | ||||
|     note: "Cheaper with INT and CHA.", | ||||
|     scoring: {lore: 1}, | ||||
|   }, | ||||
|   penance: { | ||||
|     stats: ["AGI", "INT", "CHA", "PSI"], | ||||
|     note: "Lower your stats for this.", | ||||
|     scoring: {}, | ||||
|   } | ||||
| } | ||||
|  | ||||
| function governing(track: Track, difficulty: Difficulty): SkillGoverning { | ||||
| function governing(track: Track, difficulty: Difficulty, flipped?: boolean): SkillGoverning { | ||||
|   let template = templates[track]; | ||||
|   let underTarget: number | ||||
|   let target: number | ||||
| @@ -112,9 +123,15 @@ function governing(track: Track, difficulty: Difficulty): SkillGoverning { | ||||
|   switch(difficulty) { | ||||
|     case 0: underTarget = 5; target = 15; cost = 50; mortalServantValue = 1; break; | ||||
|     case 1: underTarget = 15; target = 40; cost = 100; mortalServantValue = 2; break; | ||||
|     case 1.25: underTarget = 17; target = 42; cost = 100; mortalServantValue = 2; break; | ||||
|     case 2: underTarget = 30; target = 70; cost = 125; mortalServantValue = 3; break; | ||||
|     case 3: underTarget = 50; target = 100; cost = 150; mortalServantValue = 10; break; | ||||
|   } | ||||
|  | ||||
|   if (flipped) { | ||||
|     mortalServantValue = -mortalServantValue; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     stats: template.stats, | ||||
|     underTarget: underTarget, | ||||
| @@ -122,13 +139,15 @@ function governing(track: Track, difficulty: Difficulty): SkillGoverning { | ||||
|     cost: cost, | ||||
|     note: template.note, | ||||
|     scoring: template.scoring, | ||||
|     mortalServantValue: mortalServantValue | ||||
|     mortalServantValue: mortalServantValue, | ||||
|     flipped: flipped ?? false, | ||||
|   } | ||||
| } | ||||
|  | ||||
| let table = new SkillsTable(); | ||||
|  | ||||
| export let bat0 = table.add({ | ||||
|   isDegrading: false, | ||||
|   governing: governing("bat", 0), | ||||
|   profile: { | ||||
|     name: "Screech", | ||||
| @@ -323,6 +342,37 @@ export let lore3 = table.add({ | ||||
|   prereqs: [lore2] | ||||
| }); | ||||
|  | ||||
| export let sorry0 = table.add({ | ||||
|   isDegrading: true, | ||||
|   governing: governing("penance", 0, true), | ||||
|   profile: { | ||||
|     name: "I'm Sorry", | ||||
|     description: "You really hurt your Master, you know? Shame on you." | ||||
|   }, | ||||
|   prereqs: [], | ||||
| }) | ||||
|  | ||||
| export let sorry1 = table.add({ | ||||
|   isDegrading: true, | ||||
|   governing: governing("penance", 1, true), | ||||
|   profile: { | ||||
|     name: "I'm So Sorry", | ||||
|     description: "You should have known better! You should have done what you were told." | ||||
|   }, | ||||
|   prereqs: [], | ||||
| }) | ||||
|  | ||||
| export let sorry2 = table.add({ | ||||
|   isDegrading: true, | ||||
|   // difficulty 2 is genuinely brutal | ||||
|   governing: governing("penance", 1.25, true), | ||||
|   profile: { | ||||
|     name: "Forgive Me", | ||||
|     description: "Nothing you say will ever be enough to make up for your indiscretion.", | ||||
|   }, | ||||
|   prereqs: [], | ||||
| }) | ||||
|  | ||||
| export function getSkills(): SkillsTable { | ||||
|   return table; | ||||
| } | ||||
| @@ -1,8 +1,13 @@ | ||||
| import {ALL_STATS, Stat, SuccessorOption} from "./datatypes.ts"; | ||||
| import {ALL_STATS, Skill, Stat, SuccessorOption} from "./datatypes.ts"; | ||||
| import {generateName, generateTitle} from "./namegen.ts"; | ||||
| import {choose} from "./utils.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
|  | ||||
| export function generateSuccessors(nImprovements: number, penance: boolean): SuccessorOption[] { | ||||
|   if (penance) { | ||||
|     return [generateSuccessorFromPlayer()]; | ||||
|   } | ||||
|  | ||||
| export function generateSuccessors(nImprovements: number): SuccessorOption[] { | ||||
|   let options = []; | ||||
|   while (options.length < 3) { | ||||
|     let option = generateSuccessor(nImprovements); | ||||
| @@ -23,6 +28,25 @@ function isEligible(existing: SuccessorOption[], added: SuccessorOption) { | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| export function generateSuccessorFromPlayer(): SuccessorOption { | ||||
|   let progress = getPlayerProgress(); | ||||
|   let successor = { | ||||
|     name: progress.name, | ||||
|     title: "Penitent", | ||||
|     note: "Failed at Master's bidding", | ||||
|     stats: {...progress.getStats()}, | ||||
|     talents: {...progress.getTalents()}, | ||||
|     skills: [...progress.getLearnedSkills()], | ||||
|     inPenance: true, | ||||
|     isCompulsory: true, | ||||
|   } | ||||
|  | ||||
|   for (let stat of ALL_STATS.values()) { | ||||
|     successor.talents[stat] = -8; | ||||
|   } | ||||
|   return successor; | ||||
| } | ||||
|  | ||||
| export function generateSuccessor(nImprovements: number): SuccessorOption { | ||||
|   let name = generateName(); | ||||
|   let title = generateTitle(); | ||||
| @@ -50,5 +74,8 @@ export function generateSuccessor(nImprovements: number): SuccessorOption { | ||||
|     improvement(); | ||||
|   } | ||||
|  | ||||
|   return {name, title, note, stats, talents}; | ||||
|   let skills: Skill[] = []; | ||||
|   let inPenance = false; | ||||
|   let isCompulsory = false; | ||||
|   return {name, title, note, stats, talents, skills, inPenance, isCompulsory}; | ||||
| } | ||||
							
								
								
									
										182
									
								
								src/wishes.ts
									
									
									
									
									
								
							
							
						
						
									
										182
									
								
								src/wishes.ts
									
									
									
									
									
								
							| @@ -6,15 +6,17 @@ import { | ||||
|   charm0, | ||||
|   charm1, | ||||
|   charm2, | ||||
|   charm3, | ||||
|   charm3, getSkills, | ||||
|   lore0, lore1, lore2, | ||||
|   party0, | ||||
|   party1, party2, party3, stare0, stare1, stare2, stare3, | ||||
|   party1, party2, party3, sorry0, sorry1, sorry2, stare0, stare1, stare2, stare3, | ||||
|   stealth0, | ||||
|   stealth1, | ||||
|   stealth2, | ||||
|   stealth3 | ||||
| } from "./skills.ts"; | ||||
| import {compile, VNSceneBasisPart} from "./vnscene.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
|  | ||||
| class WishesTable { | ||||
|   #wishes: WishData[] | ||||
| @@ -33,10 +35,12 @@ class WishesTable { | ||||
|     return this.#wishes[wish.id]; | ||||
|   } | ||||
|  | ||||
|   getAllPossibleWishes(): Wish[] { | ||||
|   getAllRandomWishes(): Wish[] { | ||||
|     let wishes: Wish[] = []; | ||||
|     for (let i = 0; i < this.#wishes.length; i++) { | ||||
|       wishes.push({id: i}); | ||||
|       if (this.#wishes[i].isRandomlyAvailable) { | ||||
|         wishes.push({id: i}); | ||||
|       } | ||||
|     } | ||||
|     return wishes; | ||||
|   } | ||||
| @@ -47,32 +51,174 @@ export function getWishes(): WishesTable { | ||||
|   return table; | ||||
| } | ||||
|  | ||||
| const whisper: VNSceneBasisPart = { | ||||
|   type: "message", | ||||
|   text: "...", | ||||
|   sfx: "whisper.mp3" | ||||
| } | ||||
|  | ||||
| export const celebritySocialite = table.add({ | ||||
|   profile: {name: "Celebrity Socialite"}, | ||||
|   profile: { | ||||
|     name: "Celebrity Socialite", | ||||
|     note: "+Charm -Lore", | ||||
|     domicile: "Party Mansion", | ||||
|     reignSentence: "A lot of people know who you are and like you.", | ||||
|     failureName: "Z-List Bloodstarver", | ||||
|     failureDomicile: "Obscure Soap Ad", | ||||
|     failureReignSentence: "Nobody really knows who you are.", | ||||
|     failureSuccessorVerb: "Apologize For Your Failure", | ||||
|   }, | ||||
|   isRandomlyAvailable: true, | ||||
|   isCompulsory: false, | ||||
|   bannedSkills: () => [lore0], | ||||
|   discouragedSkills: () => [stealth0, stealth1, stealth2, stealth3], | ||||
|   encouragedSkills: () => [party0, party1, party2, party3], | ||||
|   requiredSkills: () => [charm0, charm1, charm2], | ||||
|   prologue: compile([ | ||||
|     whisper, | ||||
|     "Master?", | ||||
|     whisper, | ||||
|     "I see.", | ||||
|     "You. I -- should I buy a guitar or something?", | ||||
|     whisper, | ||||
|     "My looks and my party skills...", | ||||
|   ]), | ||||
|   onFailure: compile([ | ||||
|     whisper, | ||||
|     "You're displeased...", | ||||
|     whisper, | ||||
|     "I'm not popular enough?", | ||||
|     "I see.", | ||||
|   ]), | ||||
|   onVictory: compile([ | ||||
|     whisper, | ||||
|     "I did as you commanded.", | ||||
|     "You're pleased?", | ||||
|     "... I'm free.", | ||||
|   ]) | ||||
| }); | ||||
|  | ||||
| export const nightswornAlchemist = table.add({ | ||||
|   profile: {name: "Nightsworn Alchemist"}, | ||||
|   profile: { | ||||
|     name: "Nightsworn Alchemist", | ||||
|     note: "+Lore -Party", | ||||
|     domicile: "Alchemical Lab", | ||||
|     reignSentence: "You understand the fundamental connection between wine and blood.", | ||||
|     failureName: "Failure of Science", | ||||
|     failureDomicile: "Remedial College", | ||||
|     failureReignSentence: "You don't understand much of anything.", | ||||
|     failureSuccessorVerb: "Apologize For Your Failure", | ||||
|   }, | ||||
|   isRandomlyAvailable: true, | ||||
|   isCompulsory: false, | ||||
|   bannedSkills: () => [party0], | ||||
|   discouragedSkills: () => [charm0, charm1, charm2, charm3], | ||||
|   encouragedSkills: () => [stare0, stare1, stare2, stare3], | ||||
|   requiredSkills: () => [lore0, lore1, lore2] | ||||
|   requiredSkills: () => [lore0, lore1, lore2], | ||||
|   prologue: compile([ | ||||
|     whisper, | ||||
|     "Master?", | ||||
|     whisper, | ||||
|     "I see.", | ||||
|     "You. I -- should dedicate my life to the vampiric sciences.", | ||||
|     whisper, | ||||
|     "My looks and my party skills...", | ||||
|   ]), | ||||
|   onFailure: compile([ | ||||
|     whisper, | ||||
|     "You're displeased...", | ||||
|     whisper, | ||||
|     "I should have learned more lore.", | ||||
|   ]), | ||||
|   onVictory: compile([ | ||||
|     whisper, | ||||
|     "I did as you commanded.", | ||||
|     "You're pleased?", | ||||
|     "... I'm free.", | ||||
|   ]) | ||||
| }); | ||||
|  | ||||
| export const batFreak = table.add({ | ||||
|   profile: {name: "Bat Freak"}, | ||||
|   profile: { | ||||
|     name: "Bat Freak", | ||||
|     note: "++Bat -All", | ||||
|     domicile: "Master's Chiropteriary", | ||||
|     reignSentence: "You're an idol among bats.", | ||||
|     failureName: "Practically Mortal", | ||||
|     failureDomicile: "Right Side Up", | ||||
|     failureReignSentence: "Bats can tell you don't skreek correctly.", | ||||
|     failureSuccessorVerb: "Apologize -- SKREEK!", | ||||
|   }, | ||||
|   isRandomlyAvailable: true, | ||||
|   isCompulsory: false, | ||||
|   bannedSkills: () => [charm0, stare0, party0, lore0], | ||||
|   discouragedSkills: () => [], | ||||
|   encouragedSkills: () => [stealth0, stealth1, stealth2, stealth3], | ||||
|   requiredSkills: () => [bat0, bat1, bat2, bat3] | ||||
|   requiredSkills: () => [bat0, bat1, bat2, bat3], | ||||
|   prologue: compile([ | ||||
|     whisper, | ||||
|     "Master?", | ||||
|     whisper, | ||||
|     "I see.", | ||||
|     "You -- SKKREEK -- want me to become a -- SKKREEK --", | ||||
|   ]), | ||||
|   onFailure: compile([ | ||||
|     whisper, | ||||
|     "You're displeased...", | ||||
|     whisper, | ||||
|     "I -- SKREEEEK -- should have spent more time becoming a bat...", | ||||
|   ]), | ||||
|   onVictory: compile([ | ||||
|     whisper, | ||||
|     "SKRSKRSKRSK.", | ||||
|     "I'm FREEEEEEEEEE --", | ||||
|   ]) | ||||
| }); | ||||
|  | ||||
| export function generateWishes(): Wish[] { | ||||
|   let possibleWishes = table.getAllPossibleWishes(); | ||||
| export const repent = table.add({ | ||||
|   profile: { | ||||
|     name: "Not Even Fit To Be Bat Food", | ||||
|     note: "--All", | ||||
|     domicile: "Master's Home", | ||||
|     reignSentence: "You are almost, but not quite loved.", | ||||
|     failureName: "Can't Even Repent Correctly", | ||||
|     failureDomicile: "Homeless", | ||||
|     failureReignSentence: "You are unloved and disrespected.", | ||||
|     failureSuccessorVerb: "Apologize Again", | ||||
|   }, | ||||
|   isRandomlyAvailable: false, | ||||
|   isCompulsory: true, | ||||
|   bannedSkills: () => getSkills().getAvailableSkills(false), | ||||
|   discouragedSkills: () => [], | ||||
|   encouragedSkills: () => [], | ||||
|   requiredSkills: () => [sorry0, sorry1, sorry2], | ||||
|   prologue: compile([ | ||||
|     whisper, | ||||
|     "I'm sorry.", | ||||
|     "Please...", | ||||
|     whisper, | ||||
|     "I must repent." | ||||
|   ]), | ||||
|   onFailure: compile([ | ||||
|     whisper, | ||||
|     "I can't --", | ||||
|     "I must --", | ||||
|     whisper, | ||||
|     "Master -- please, no, I --" | ||||
|   ]), | ||||
|   onVictory: compile([ | ||||
|     whisper, | ||||
|     "Yes, I see.", | ||||
|     "I'm free...?" | ||||
|   ]) | ||||
| }); | ||||
|  | ||||
| export function generateWishes(penance: boolean): Wish[] { | ||||
|   if (penance) { | ||||
|     return [repent]; | ||||
|   } | ||||
|  | ||||
|   let possibleWishes = table.getAllRandomWishes(); | ||||
|   shuffle(possibleWishes); | ||||
|  | ||||
|   let selectedWishes: Wish[] = []; | ||||
| @@ -103,4 +249,18 @@ export function getCostMultiplier(wish: Wish | null, skill: Skill): number { | ||||
|   } | ||||
|  | ||||
|   return 1.0; | ||||
| } | ||||
|  | ||||
| export function isWishCompleted(wish: Wish): boolean { | ||||
|   let player = getPlayerProgress(); | ||||
|  | ||||
|   let wishData = getWishes().get(wish); | ||||
|  | ||||
|   for (let subj of wishData.requiredSkills()) { | ||||
|     if (!player.hasLearned(subj)) { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user