diff --git a/src/datatypes.ts b/src/datatypes.ts index 7fd51c4..f21528b 100644 --- a/src/datatypes.ts +++ b/src/datatypes.ts @@ -1,3 +1,4 @@ +import {VNScene} from "./vnscene.ts"; export type Stat = "AGI" | "INT" | "CHA" | "PSI"; export const ALL_STATS: Array = ["AGI", "INT", "CHA", "PSI"]; @@ -31,3 +32,39 @@ export type SkillScoring = {[P in ScoringCategory]?: number}; export type Skill = { id: number } + + +export type Wish = "celebritySocialite" | "nightswornAlchemist" | "batFreak"; + +// endings + +export type Ending = { + scene: VNScene + personal: EndingPersonal, + analytics: EndingAnalytics, + successorOptions: SuccessorOption[], + wishOptions: Wish[], + + // forcedSuccessors: number[] | null, + // forcedWishes: number[] | null +} + +export type EndingPersonal = { + rank: string, + domicile: string, +} + +export type EndingAnalytics = { + itemsPurloined: number, + vampiricSkills: number, + mortalServants: number, +} + +export type SuccessorOption = { + name: string, + title: string, + note: string | null, // ex "already a vampire" + stats: Record, + talents: Record +} + diff --git a/src/endgamemodal.ts b/src/endgamemodal.ts index 485453e..0b0d6e9 100644 --- a/src/endgamemodal.ts +++ b/src/endgamemodal.ts @@ -4,33 +4,34 @@ import {BG_INSET, FG_BOLD, FG_TEXT} from "./colors.ts"; import {AlignX, AlignY, Point, Rect, Size} from "./engine/datatypes.ts"; import {DrawPile} from "./drawpile.ts"; import {addButton} from "./button.ts"; -import {ALL_STATS} from "./datatypes.ts"; +import {ALL_STATS, Ending, Wish} from "./datatypes.ts"; +import {getScorer} from "./scorer.ts"; const WIDTH = 384; const HEIGHT = 384; export class EndgameModal { - #isShown: boolean; #drawpile: DrawPile; #page: number; - constructor() { - this.#isShown = false; - this.#drawpile = new DrawPile(); + #ending: Ending | null; + constructor() { + this.#drawpile = new DrawPile(); this.#page = 0; + this.show(getScorer().pickEnding()); + // debug - this.show(); } get isShown(): boolean { - return this.#isShown; + return this.#ending != null; } - show() { - this.#isShown = true; + show(ending: Ending) { this.#page = 0; + this.#ending = ending; } update() { @@ -44,17 +45,30 @@ export class EndgameModal { #update() { this.#drawpile.clear(); if (this.#page == 0) { + let analytics = this.#ending?.analytics; + let rank = this.#ending?.personal?.rank ?? "No Rank"; + let domicile = this.#ending?.personal?.domicile ?? "No Domicile"; + let itemsPurloined = analytics?.itemsPurloined ?? 0; + let vampiricSkills = analytics?.vampiricSkills ?? 0; + let mortalServants = analytics?.mortalServants ?? 0; + this.#drawpile.add(0, () => { D.drawText("It is time to announce the sentence of fate.", new Point(0, 0), FG_TEXT) D.drawText("You are no longer a fledgling. Your new rank:", new Point(0, 32), FG_TEXT) - D.drawText("Progenitor", new Point(WIDTH / 2, 64), FG_BOLD, {alignX: AlignX.Center}) + D.drawText(rank, new Point(WIDTH / 2, 64), FG_BOLD, {alignX: AlignX.Center}) D.drawText("You have achieved a DOMICILE STATUS of:", new Point(0, 96), FG_TEXT) - D.drawText("Guest House", new Point(WIDTH / 2, 128), FG_BOLD, {alignX: AlignX.Center}) - D.drawText("where you live with many friends.", new Point(0, 160), FG_TEXT) + D.drawText(domicile, new Point(WIDTH / 2, 128), FG_BOLD, {alignX: AlignX.Center}) + D.drawText("where you live with many friends.", new Point(0, 160), FG_TEXT) // TODO: Vary this text D.drawText("You have achieved:", new Point(0, 192), FG_TEXT) - D.drawText("48 items purloined\n96 vampiric skills\n50 mortal servants", new Point(WIDTH / 2, 224), FG_TEXT, {alignX: AlignX.Center}) - D.drawText("48 \n96 \n50 ", new Point(WIDTH / 2, 224), FG_BOLD, {alignX: AlignX.Center}) - D.drawText("That feels like a lot!", new Point(0, 288), FG_TEXT) + D.drawText( + `${itemsPurloined} items purloined\n${vampiricSkills} vampiric skills\n${mortalServants} mortal servants`, + new Point(WIDTH / 2, 224), FG_TEXT, {alignX: AlignX.Center} + ) + D.drawText( + `${itemsPurloined} \n${vampiricSkills} \n${mortalServants} `, + new Point(WIDTH / 2, 224), FG_BOLD, {alignX: AlignX.Center} + ); + D.drawText("That feels like a lot!", new Point(0, 288), FG_TEXT) // TODO: Vary this text D.drawText("Your reign continues unimpeded from the shadows. It is now time to", new Point(0, 320), FG_TEXT, {forceWidth: WIDTH}) }) addButton( @@ -114,7 +128,15 @@ export class EndgameModal { } #addCandidate(ix: number, at: Point) { - let names = ["Marty", "Karla", "Steve"]; + let candidates = this.#ending?.successorOptions; + if (candidates == null) { + return; + } + let candidate = candidates[ix]; + if (candidate == null) { + return; + } + let w = WIDTH; let h = 64; @@ -136,7 +158,8 @@ export class EndgameModal { at.offset(new Point(0, 4)), new Size(w, h - 8), fg, ) - D.drawText(names[ix], at.offset(new Point(4, 8)), fgBold) + D.drawText(candidate.name + ", " + candidate.title, at.offset(new Point(4, 8)), fg); + D.drawText(candidate.name, at.offset(new Point(4, 8)), fgBold); let xys = [ new Point(4, 24), new Point(4, 40), @@ -144,15 +167,20 @@ export class EndgameModal { ]; let i = 0; for (let s of ALL_STATS.values()) { + let statValue = candidate.stats[s]; + let talentValue = candidate.talents[s]; + D.drawText(s, at.offset(xys[i]), fg) - D.drawText("10", at.offset(xys[i].offset(new Point(32, 0))), fgBold) - D.drawText("(+4)", at.offset(xys[i].offset(new Point(56, 0))), fg) + D.drawText(`${statValue}`, at.offset(xys[i].offset(new Point(32, 0))), fgBold) + + if (talentValue > 0) { + D.drawText(`(+${talentValue})`, at.offset(xys[i].offset(new Point(56, 0))), fg) + } i += 1; } - D.drawText("Former barista", at.offset(new Point(224, 24)), fg) - if (ix == 2) { - D.drawText("Already a vampire", at.offset(new Point(224, 40)), fg) + if (candidate.note != null) { + D.drawText(candidate.note, at.offset(new Point(224, 24)), fg, {forceWidth: w - 224}) } }, generalRect, @@ -164,17 +192,19 @@ export class EndgameModal { } #addWish(ix: number, at: Point) { - let wishes = [ - "Celebrity Socialite", - "Nightsworn Alchemist", - "Bat Freak" - ] + let wishOptions = this.#ending?.wishOptions; + if (wishOptions == null) { + return; + } + let wishOption = wishOptions[ix]; let selected = ix == 1; let w = 128; let h = 72; let generalRect = new Rect(at, new Size(w, h)); let enabled = true; + let wishLabel = wishLabels[wishOption]; + this.#drawpile.addClickable( 0, (hover) => { @@ -189,7 +219,7 @@ export class EndgameModal { at.offset(new Point(2, 4)), new Size(w - 4, h - 8), fg, ) - D.drawText(wishes[ix], at.offset(new Point(w / 2,h / 2 )), fgBold, { + D.drawText(wishLabel, at.offset(new Point(w / 2,h / 2 )), fgBold, { forceWidth: w - 4, alignX: AlignX.Center, alignY: AlignY.Middle, @@ -213,6 +243,12 @@ export class EndgameModal { } } +const wishLabels: Record = { + celebritySocialite: "Celebrity Socialite", + nightswornAlchemist: "Nightsworn Alchemist", + batFreak: "Bat Freak" +} + let active = new EndgameModal(); export function getEndgameModal() { return active; diff --git a/src/mapgen.ts b/src/mapgen.ts index 91b1c0b..697e630 100644 --- a/src/mapgen.ts +++ b/src/mapgen.ts @@ -2,6 +2,7 @@ import {ConceptualCell, maps} from "./maps.ts"; import {Grid, Point, Size} from "./engine/datatypes.ts"; import {ALL_STATS} from "./datatypes.ts"; import {LoadedMap, MapCell, MapCellContent} from "./huntmode.ts"; +import {choose} from "./utils.ts"; export function generate(): LoadedMap { let mapNames: Array = Object.keys(maps); @@ -89,9 +90,3 @@ function generateContent(): MapCellContent { ])(); } -function choose(array: Array): T { - if (array.length == 0) { - throw `array cannot have length 0 for choose` - } - return array[Math.floor(Math.random() * array.length)] -} diff --git a/src/namegen.ts b/src/namegen.ts new file mode 100644 index 0000000..3ebcbb3 --- /dev/null +++ b/src/namegen.ts @@ -0,0 +1,50 @@ +import {choose} from "./utils.ts"; + +const names = [ + // vampires + "Vlad", "Drek", + // generic American names I like + "Kyle", + // friends I can defame + "Bhijn", "Myr", "Narry", + // aggressively furry names + "Tech", + // deities + "Quetzal", "Zotz", + // Nameberry's unique names + "Teleri", "Artis", "Lautaro", "Corbett", "Kestrel", + "Averil", "Sparrow", "Quillan", "Pipit", "Capella", + "Altair", "Lowell", "Leonie", "Vega", "Kea", + "Shai", "Teddy", "Howard", "Khalid", "Ozias", + "Zuko", "Ezio", "Zeno", "Thisby", "Calloway", + "Fenna", "Lupin", "Finlo", "Tycho", "Talmadge", + // others + "Jeff", "Jon", "Garrett", "Russell", "Tyson", + "Gervase", "Sonja", "Sue", "Richard", "Jankie", + // highly trustworthy individuals + "Nef", "Matt", "Sam" +] +export function generateName() { + return choose(names); +} + +// TOOD: Associate these with a stat difference +// mix of Liberal Crime Squad jobs, SS13 jobs, and jobs that amuse me +const titles = [ + "Artist", + "Barista", + "Bartender", + "Blogger", + "Dragon Handler", + "Game Developer", + "Hypnotist", + "Journalist", + "Poker Player", + "Priest", + "Magician", + "Writer" +]; + +export function generateTitle() { + return choose(titles); +} \ No newline at end of file diff --git a/src/scorer.ts b/src/scorer.ts index 1c67dd3..eaf41c1 100644 --- a/src/scorer.ts +++ b/src/scorer.ts @@ -1,13 +1,14 @@ import {VNScene} from "./vnscene.ts"; import {getPlayerProgress} from "./playerprogress.ts"; import {getSkills} from "./skills.ts"; -import {SCORING_CATEGORIES, ScoringCategory} from "./datatypes.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 {generateSuccessors} from "./successors.ts"; class Scorer { constructor() { } - pickEnding(): Ending { let learnedSkills = getPlayerProgress().getLearnedSkills(); let scores: Record = {}; @@ -21,49 +22,81 @@ class Scorer { // NOTE: This approach isn't efficient but it's easy to understand // and it allows me to arbitrate ties however I want + let runningScores: Record = {...scores}; const isMax = (cat: ScoringCategory, min: number) => { - let score = scores[cat] ?? 0; - scores[cat] = 0; // each category, once checked, can't disqualify any other category + let score = runningScores[cat] ?? 0; + runningScores[cat] = 0; // each category, once checked, can't disqualify any other category if (score < min) { return false; } for (let cat of SCORING_CATEGORIES.values()) { - if (scores[cat] > score) { + if (runningScores[cat] > score) { return false; } } return true; } + let scene: VNScene; + let rank: string; + let domicile: string; + + // 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)) { - return {scene: sceneStare} + scene = sceneStare; + rank = "Hypno-Chiropteran"; + domicile = "Village of Brainwashed Mortals"; } - if (isMax("lore", 3)) { - return {scene: sceneLore}; + else if (isMax("lore", 3)) { + scene = sceneLore; + rank = "Loremaster"; + domicile = "Vineyard"; } - if (isMax("charm", 2)) { - return {scene: sceneCharm} + else if (isMax("charm", 2)) { + scene = sceneCharm; + rank = "Seducer"; + domicile = "Guest House"; } - if (isMax("party", 1)) { - return {scene: sceneParty}; + else if (isMax("party", 1)) { + scene = sceneParty; + rank = "Party Animal"; + domicile = "Nightclub"; } - if (isMax("stealth", 0)) { - return {scene: sceneStealth} + else if (isMax("stealth", 0)) { + scene = sceneStealth; + rank = "Invisible"; + domicile = "Townhouse"; } // if (isMax("bat")) { - { - return {scene: sceneBat}; + else { + scene = sceneBat; + rank = "Bat"; + domicile = "Cave"; + } + + // TODO: Analytics tracker + let analytics = { + itemsPurloined: 0, + vampiricSkills: 0, + mortalServants: 0, + } + let successorOptions = generateSuccessors(0); // TODO: generate nImprovements from score + let wishOptions = generateWishes(); + + return { + scene, + personal: {rank, domicile}, + analytics, + successorOptions, + wishOptions, } } - -} - -type Ending = { - scene: VNScene } let active = new Scorer(); export function getScorer(): Scorer { return active; -} \ No newline at end of file +} diff --git a/src/statemanager.ts b/src/statemanager.ts index 15e97af..8293ed6 100644 --- a/src/statemanager.ts +++ b/src/statemanager.ts @@ -29,7 +29,7 @@ export class StateManager { // TODO: Play a specific scene let ending = getScorer().pickEnding(); getVNModal().play(ending.scene); - getEndgameModal().show(); + getEndgameModal().show(ending); } } diff --git a/src/successors.ts b/src/successors.ts new file mode 100644 index 0000000..65f7dd1 --- /dev/null +++ b/src/successors.ts @@ -0,0 +1,56 @@ +import {ALL_STATS, Stat, SuccessorOption} from "./datatypes.ts"; +import {generateName, generateTitle} from "./namegen.ts"; +import {choose} from "./utils.ts"; + +// TODO: Take a "number of improvements", use that to improve +// each successor N times +export function generateSuccessors(nImprovements: number): SuccessorOption[] { + let options = []; + while (options.length < 3) { + let option = generateSuccessor(nImprovements); + if (!isEligible(options, option)) { + continue; + } + options.push(option); + } + return options; +} + +function isEligible(existing: SuccessorOption[], added: SuccessorOption) { + for (let e of existing.values()) { + if (e.name == added.name || e.title == added.title) { + return false; + } + } + return true; +} + +export function generateSuccessor(nImprovements: number): SuccessorOption { + let name = generateName(); + let title = generateTitle(); + let note = null; + let stats: Record = { + "AGI": 10 + choose([1, 2]), + "INT": 10 + choose([1, 2]), + "CHA": 10 + choose([1, 2]), + "PSI": 10 + choose([1, 2]), + } + let talents: Record = { + "AGI": 0, + "INT": 0, + "CHA": 0, + "PSI": 0, + } + + let improvements = [ + () => { stats[choose(ALL_STATS)] += choose([3, 4, 5, 6]); }, // avg 4.5 + () => { talents[choose(ALL_STATS)] += 1; }, + ]; + let nTotalImprovements = nImprovements + 5; + for (let i = 0; i < nTotalImprovements; i++) { + let improvement = improvements[Math.floor(Math.random() * improvements.length)]; + improvement(); + } + + return {name, title, note, stats, talents}; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..3a77dcd --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,6 @@ +export function choose(array: Array): T { + if (array.length == 0) { + throw `array cannot have length 0 for choose` + } + return array[Math.floor(Math.random() * array.length)] +} diff --git a/src/wishes.ts b/src/wishes.ts new file mode 100644 index 0000000..3500428 --- /dev/null +++ b/src/wishes.ts @@ -0,0 +1,5 @@ +import {Wish} from "./datatypes.ts"; + +export function generateWishes(): Wish[] { + return ["celebritySocialite", "nightswornAlchemist", "batFreak"]; +} \ No newline at end of file