Run prettier over everything
This commit is contained in:
		| @@ -1,7 +1,7 @@ | ||||
| import {DrawPile} from "./drawpile.ts"; | ||||
| import {AlignX, AlignY, Point, Rect, Size} from "./engine/datatypes.ts"; | ||||
| import {BG_INSET, FG_BOLD, FG_TEXT} from "./colors.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| import { DrawPile } from "./drawpile.ts"; | ||||
| import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||
| import { BG_INSET, FG_BOLD, FG_TEXT } from "./colors.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
|  | ||||
| export function addButton( | ||||
|   drawpile: DrawPile, | ||||
| @@ -13,7 +13,10 @@ export function addButton( | ||||
|   let padding = 2; | ||||
|   let topLeft = rect.top; | ||||
|   let topLeftPadded = topLeft.offset(new Point(padding, padding)); | ||||
|   let sizePadded = new Size(rect.size.w - padding * 2, rect.size.h - padding * 2); | ||||
|   let sizePadded = new Size( | ||||
|     rect.size.w - padding * 2, | ||||
|     rect.size.h - padding * 2, | ||||
|   ); | ||||
|   let center = topLeft.offset(new Point(rect.size.w / 2, rect.size.h / 2)); | ||||
|  | ||||
|   drawpile.addClickable( | ||||
| @@ -26,16 +29,16 @@ export function addButton( | ||||
|       D.fillRect( | ||||
|         topLeftPadded.offset(new Point(-1, -1)), | ||||
|         sizePadded.add(new Size(2, 2)), | ||||
|         bg | ||||
|         bg, | ||||
|       ); | ||||
|       D.drawRect(topLeftPadded, sizePadded, fg); | ||||
|       D.drawText(label, center, fgLabel, { | ||||
|         alignX: AlignX.Center, | ||||
|         alignY: AlignY.Middle, | ||||
|       }) | ||||
|       }); | ||||
|     }, | ||||
|     new Rect(topLeftPadded, sizePadded), | ||||
|     enabled, | ||||
|     cbClick | ||||
|     cbClick, | ||||
|   ); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import {DrawPile} from "./drawpile.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"; | ||||
| import {BG_INSET, FG_BOLD} from "./colors.ts"; | ||||
| import {addButton} from "./button.ts"; | ||||
| import {getSkills} from "./skills.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
| import { DrawPile } from "./drawpile.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"; | ||||
| import { BG_INSET, FG_BOLD } from "./colors.ts"; | ||||
| import { addButton } from "./button.ts"; | ||||
| import { getSkills } from "./skills.ts"; | ||||
| import { getPlayerProgress } from "./playerprogress.ts"; | ||||
|  | ||||
| export class CheckModal { | ||||
|   #drawpile: DrawPile; | ||||
| @@ -22,20 +22,20 @@ export class CheckModal { | ||||
|   } | ||||
|  | ||||
|   get isShown() { | ||||
|     return this.#activeCheck != null | ||||
|     return this.#activeCheck != null; | ||||
|   } | ||||
|  | ||||
|   get #size(): Size { | ||||
|     return getPartLocation("BottomModal").size | ||||
|     return getPartLocation("BottomModal").size; | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     withCamera("BottomModal", () => this.#update()) | ||||
|     this.#drawpile.executeOnClick() | ||||
|     withCamera("BottomModal", () => this.#update()); | ||||
|     this.#drawpile.executeOnClick(); | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     withCamera("BottomModal", () => this.#draw()) | ||||
|     withCamera("BottomModal", () => this.#draw()); | ||||
|   } | ||||
|  | ||||
|   show(checkData: CheckData | null, callback: (() => void) | null) { | ||||
| @@ -47,13 +47,15 @@ export class CheckModal { | ||||
|   #update() { | ||||
|     this.#drawpile.clear(); | ||||
|  | ||||
|     let check = this.#activeCheck | ||||
|     if (!check) { return; } | ||||
|     let check = this.#activeCheck; | ||||
|     if (!check) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     let size = this.#size; | ||||
|     this.#drawpile.add(0, () => { | ||||
|       D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET) | ||||
|     }) | ||||
|       D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET); | ||||
|     }); | ||||
|  | ||||
|     let success = this.#success; | ||||
|     if (success) { | ||||
| @@ -62,11 +64,17 @@ export class CheckModal { | ||||
|           forceWidth: size.w, | ||||
|           alignX: AlignX.Center, | ||||
|           alignY: AlignY.Middle, | ||||
|         }) | ||||
|         }); | ||||
|       }); | ||||
|       addButton(this.#drawpile, "OK!", new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), true, () => { | ||||
|         this.show(null, null); | ||||
|       }) | ||||
|       addButton( | ||||
|         this.#drawpile, | ||||
|         "OK!", | ||||
|         new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), | ||||
|         true, | ||||
|         () => { | ||||
|           this.show(null, null); | ||||
|         }, | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
| @@ -76,12 +84,15 @@ export class CheckModal { | ||||
|         forceWidth: size.w, | ||||
|         alignX: AlignX.Center, | ||||
|         alignY: AlignY.Middle, | ||||
|       }) | ||||
|     }) | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     let options = check.options; | ||||
|  | ||||
|     let addOptionButton = (option: CheckDataOption | ChoiceOption, rect: Rect) => { | ||||
|     let addOptionButton = ( | ||||
|       option: CheckDataOption | ChoiceOption, | ||||
|       rect: Rect, | ||||
|     ) => { | ||||
|       let accomplished: boolean; | ||||
|       let optionLabel: string; | ||||
|       let resultMessage: string; | ||||
| @@ -91,7 +102,6 @@ export class CheckModal { | ||||
|         accomplished = option.countsAsSuccess; | ||||
|         optionLabel = option.unlockable; | ||||
|         resultMessage = option.success; | ||||
|  | ||||
|       } else { | ||||
|         option = option as CheckDataOption; | ||||
|         let skill = option.skill(); | ||||
| @@ -110,10 +120,12 @@ export class CheckModal { | ||||
|  | ||||
|         if (accomplished) { | ||||
|           let cb = this.#callback; | ||||
|           if (cb) { cb(); } | ||||
|           if (cb) { | ||||
|             cb(); | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|       }); | ||||
|     }; | ||||
|  | ||||
|     if (options.length == 0) { | ||||
|       addButton( | ||||
| @@ -121,17 +133,26 @@ export class CheckModal { | ||||
|         "OK!", | ||||
|         new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), | ||||
|         true, | ||||
|         () => { this.show(null, null) } | ||||
|       ) | ||||
|     } | ||||
|     else if (options.length == 1) { | ||||
|       addOptionButton(options[0], new Rect(new Point(0, size.h - 64), new Size(size.w, 64))); | ||||
|     } | ||||
|     else if (options.length == 2) { | ||||
|       addOptionButton(options[0], new Rect(new Point(0, size.h - 64), new Size(size.w, 32))); | ||||
|       addOptionButton(options[1], new Rect(new Point(0, size.h - 32), new Size(size.w, 32))); | ||||
|         () => { | ||||
|           this.show(null, null); | ||||
|         }, | ||||
|       ); | ||||
|     } else if (options.length == 1) { | ||||
|       addOptionButton( | ||||
|         options[0], | ||||
|         new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), | ||||
|       ); | ||||
|     } else if (options.length == 2) { | ||||
|       addOptionButton( | ||||
|         options[0], | ||||
|         new Rect(new Point(0, size.h - 64), new Size(size.w, 32)), | ||||
|       ); | ||||
|       addOptionButton( | ||||
|         options[1], | ||||
|         new Rect(new Point(0, size.h - 32), new Size(size.w, 32)), | ||||
|       ); | ||||
|     } else { | ||||
|       throw new Error(`unexpected number of options ${options.length}`) | ||||
|       throw new Error(`unexpected number of options ${options.length}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -143,4 +164,4 @@ export class CheckModal { | ||||
| let active: CheckModal = new CheckModal(); | ||||
| export function getCheckModal() { | ||||
|   return active; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import {Color} from "./engine/datatypes.ts"; | ||||
| import { Color } from "./engine/datatypes.ts"; | ||||
|  | ||||
| export const BG_OUTER = Color.parseHexCode("#143464"); | ||||
| export const BG_WALL_OR_UNREVEALED = Color.parseHexCode("#143464"); | ||||
| export const BG_INSET = Color.parseHexCode("#242234"); | ||||
| export const FG_TEXT = Color.parseHexCode("#c0c0c0") | ||||
| export const FG_BOLD = Color.parseHexCode("#ffffff") | ||||
| export const FG_TEXT = Color.parseHexCode("#c0c0c0"); | ||||
| export const FG_BOLD = Color.parseHexCode("#ffffff"); | ||||
| export const BG_CEILING = Color.parseHexCode("#143464"); | ||||
| export const FG_MOULDING = FG_TEXT; | ||||
|   | ||||
							
								
								
									
										140
									
								
								src/datatypes.ts
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								src/datatypes.ts
									
									
									
									
									
								
							| @@ -1,101 +1,113 @@ | ||||
| import {VNScene} from "./vnscene.ts"; | ||||
| import { VNScene } from "./vnscene.ts"; | ||||
|  | ||||
| export type Stat = "AGI" | "INT" | "CHA" | "PSI"; | ||||
| export const ALL_STATS: Array<Stat> = ["AGI", "INT", "CHA", "PSI"]; | ||||
|  | ||||
| export type Resource = "EXP"; | ||||
| export const ALL_RESOURCES: Array<Resource> = ["EXP"] | ||||
| export const ALL_RESOURCES: Array<Resource> = ["EXP"]; | ||||
|  | ||||
| export type SkillGoverning = { | ||||
|   stats: Stat[], | ||||
|   underTarget: number, | ||||
|   target: number, | ||||
|   cost: number, | ||||
|   note: string, | ||||
|   scoring: SkillScoring, | ||||
|   mortalServantValue: number, | ||||
|   flipped: boolean, | ||||
|   stats: Stat[]; | ||||
|   underTarget: number; | ||||
|   target: number; | ||||
|   cost: number; | ||||
|   note: string; | ||||
|   scoring: SkillScoring; | ||||
|   mortalServantValue: number; | ||||
|   flipped: boolean; | ||||
| }; | ||||
| export type SkillProfile = { | ||||
|   name: string, | ||||
|   description: string, | ||||
| } | ||||
|   name: string; | ||||
|   description: string; | ||||
| }; | ||||
|  | ||||
| export type SkillData = { | ||||
|   isDegrading?: boolean; | ||||
|   governing: SkillGoverning, | ||||
|   profile: SkillProfile, | ||||
|   prereqs: Skill[] | ||||
| } | ||||
|   governing: SkillGoverning; | ||||
|   profile: SkillProfile; | ||||
|   prereqs: Skill[]; | ||||
| }; | ||||
|  | ||||
| export type ScoringCategory = "bat" | "stealth" | "charm" | "stare" | "party" | "lore"; | ||||
| export const SCORING_CATEGORIES: ScoringCategory[] = ["bat", "stealth", "charm", "stare", "party", "lore"]; | ||||
| export type SkillScoring = {[P in ScoringCategory]?: number}; | ||||
| export type ScoringCategory = | ||||
|   | "bat" | ||||
|   | "stealth" | ||||
|   | "charm" | ||||
|   | "stare" | ||||
|   | "party" | ||||
|   | "lore"; | ||||
| export const SCORING_CATEGORIES: ScoringCategory[] = [ | ||||
|   "bat", | ||||
|   "stealth", | ||||
|   "charm", | ||||
|   "stare", | ||||
|   "party", | ||||
|   "lore", | ||||
| ]; | ||||
| export type SkillScoring = { [P in ScoringCategory]?: number }; | ||||
|  | ||||
| export type Skill = { | ||||
|   id: number | ||||
| } | ||||
|   id: number; | ||||
| }; | ||||
|  | ||||
| export type WishData = { | ||||
|   profile: { | ||||
|     name: string, | ||||
|     note: string, | ||||
|     domicile: string, | ||||
|     name: string; | ||||
|     note: string; | ||||
|     domicile: string; | ||||
|     reignSentence: string; | ||||
|     failureName: string, | ||||
|     failureDomicile: string, | ||||
|     failureReignSentence: string, | ||||
|     failureName: string; | ||||
|     failureDomicile: string; | ||||
|     failureReignSentence: string; | ||||
|     failureSuccessorVerb: string; | ||||
|   }, | ||||
|   isRandomlyAvailable: boolean, | ||||
|   }; | ||||
|   isRandomlyAvailable: boolean; | ||||
|   isCompulsory: boolean; | ||||
|   bannedSkills: () => Skill[], | ||||
|   discouragedSkills: () => Skill[], | ||||
|   encouragedSkills: () => Skill[], | ||||
|   requiredSkills: () => Skill[] | ||||
|   prologue: VNScene, | ||||
|   onVictory: VNScene, | ||||
|   onFailure: VNScene, | ||||
| } | ||||
|   bannedSkills: () => Skill[]; | ||||
|   discouragedSkills: () => Skill[]; | ||||
|   encouragedSkills: () => Skill[]; | ||||
|   requiredSkills: () => Skill[]; | ||||
|   prologue: VNScene; | ||||
|   onVictory: VNScene; | ||||
|   onFailure: VNScene; | ||||
| }; | ||||
| export type Wish = { | ||||
|   id: number | ||||
| } | ||||
|   id: number; | ||||
| }; | ||||
|  | ||||
| // endings | ||||
|  | ||||
| export type Ending = { | ||||
|   scene: VNScene | ||||
|   personal: EndingPersonal, | ||||
|   analytics: EndingAnalytics, | ||||
|   successorOptions: SuccessorOption[], | ||||
|   wishOptions: Wish[], | ||||
|   scene: VNScene; | ||||
|   personal: EndingPersonal; | ||||
|   analytics: EndingAnalytics; | ||||
|   successorOptions: SuccessorOption[]; | ||||
|   wishOptions: Wish[]; | ||||
|  | ||||
|   // forcedSuccessors: number[] | null, | ||||
|   // forcedWishes: number[] | null | ||||
| } | ||||
| }; | ||||
|  | ||||
| export type EndingPersonal = { | ||||
|   rank: string, | ||||
|   domicile: string, | ||||
|   reignSentence: string, | ||||
|   successorVerb: string, | ||||
|   progenerateVerb: string, | ||||
| } | ||||
|   rank: string; | ||||
|   domicile: string; | ||||
|   reignSentence: string; | ||||
|   successorVerb: string; | ||||
|   progenerateVerb: string; | ||||
| }; | ||||
|  | ||||
| export type EndingAnalytics = { | ||||
|   itemsPurloined: number, | ||||
|   vampiricSkills: number, | ||||
|   mortalServants: number, | ||||
| } | ||||
|   itemsPurloined: number; | ||||
|   vampiricSkills: number; | ||||
|   mortalServants: number; | ||||
| }; | ||||
|  | ||||
| export type SuccessorOption = { | ||||
|   name: string, | ||||
|   title: string, | ||||
|   note: string | null, // ex "already a vampire" | ||||
|   stats: Record<Stat, number>, | ||||
|   talents: Record<Stat, number>, | ||||
|   skills: Skill[], | ||||
|   name: string; | ||||
|   title: string; | ||||
|   note: string | null; // ex "already a vampire" | ||||
|   stats: Record<Stat, number>; | ||||
|   talents: Record<Stat, number>; | ||||
|   skills: Skill[]; | ||||
|   inPenance: boolean; | ||||
|   isCompulsory: boolean; | ||||
| } | ||||
|  | ||||
| }; | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import {D, I} from "./engine/public.ts"; | ||||
| import {Rect} from "./engine/datatypes.ts"; | ||||
| import { D, I } from "./engine/public.ts"; | ||||
| import { Rect } from "./engine/datatypes.ts"; | ||||
|  | ||||
| export class DrawPile { | ||||
|   #draws: {depth: number, op: () => void, onClick?: () => void}[] | ||||
|   #draws: { depth: number; op: () => void; onClick?: () => void }[]; | ||||
|   #hoveredIndex: number | null; | ||||
|  | ||||
|   constructor() { | ||||
|     this.#draws = [] | ||||
|     this.#draws = []; | ||||
|     this.#hoveredIndex = null; | ||||
|   } | ||||
|  | ||||
| @@ -16,10 +16,16 @@ export class DrawPile { | ||||
|   } | ||||
|  | ||||
|   add(depth: number, op: () => void) { | ||||
|     this.#draws.push({depth, op}); | ||||
|     this.#draws.push({ depth, op }); | ||||
|   } | ||||
|  | ||||
|   addClickable(depth: number, op: (hover: boolean) => void, rect: Rect, enabled: boolean, onClick: () => void) { | ||||
|   addClickable( | ||||
|     depth: number, | ||||
|     op: (hover: boolean) => void, | ||||
|     rect: Rect, | ||||
|     enabled: boolean, | ||||
|     onClick: () => void, | ||||
|   ) { | ||||
|     let position = I.mousePosition?.offset(D.camera); | ||||
|     let hovered = false; | ||||
|     if (position != null) { | ||||
| @@ -31,7 +37,7 @@ export class DrawPile { | ||||
|     if (hovered) { | ||||
|       this.#hoveredIndex = this.#draws.length; | ||||
|     } | ||||
|     this.#draws.push({depth, op: (() => op(hovered)), onClick: onClick}) | ||||
|     this.#draws.push({ depth, op: () => op(hovered), onClick: onClick }); | ||||
|   } | ||||
|  | ||||
|   executeOnClick() { | ||||
| @@ -48,11 +54,9 @@ export class DrawPile { | ||||
|  | ||||
|   draw() { | ||||
|     let draws = [...this.#draws]; | ||||
|     draws.sort( | ||||
|       (d0, d1) => d0.depth - d1.depth | ||||
|     ); | ||||
|     draws.sort((d0, d1) => d0.depth - d1.depth); | ||||
|     for (let d of draws.values()) { | ||||
|       d.op(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import {withCamera} from "./layout.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| 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, Ending} from "./datatypes.ts"; | ||||
| import {getStateManager} from "./statemanager.ts"; | ||||
| import {getWishes} from "./wishes.ts"; | ||||
| import { withCamera } from "./layout.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
| 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, Ending } from "./datatypes.ts"; | ||||
| import { getStateManager } from "./statemanager.ts"; | ||||
| import { getWishes } from "./wishes.ts"; | ||||
|  | ||||
| const WIDTH = 384; | ||||
| const HEIGHT = 384; | ||||
| @@ -42,11 +42,11 @@ export class EndgameModal { | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     withCamera("FullscreenPopover", () => this.#update()) | ||||
|     withCamera("FullscreenPopover", () => this.#update()); | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     withCamera("FullscreenPopover", () => this.#draw()) | ||||
|     withCamera("FullscreenPopover", () => this.#draw()); | ||||
|   } | ||||
|  | ||||
|   get #canProgenerate(): boolean { | ||||
| @@ -54,8 +54,7 @@ export class EndgameModal { | ||||
|   } | ||||
|  | ||||
|   #progenerate() { | ||||
|     let successor = | ||||
|       this.#ending!.successorOptions[this.#selectedSuccessor!]; | ||||
|     let successor = this.#ending!.successorOptions[this.#selectedSuccessor!]; | ||||
|     let wish = | ||||
|       this.#selectedWish != null | ||||
|         ? this.#ending!.wishOptions[this.#selectedWish!] | ||||
| @@ -77,98 +76,133 @@ export class EndgameModal { | ||||
|       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(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(domicile, new Point(WIDTH / 2, 128), FG_BOLD, {alignX: AlignX.Center}) | ||||
|         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(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(domicile, new Point(WIDTH / 2, 128), FG_BOLD, { | ||||
|           alignX: AlignX.Center, | ||||
|         }); | ||||
|         let whereLabel = | ||||
|           mortalServants >= 25 ? "where you live with many friends." : | ||||
|           mortalServants >= 1 ? "where you live with a couple of friends." : | ||||
|           "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"; | ||||
|         let vampiricSkillsText = vampiricSkills == 1 ? "vampiric skill" : "vampiric skills"; | ||||
|         let mortalServantsText = mortalServants == 1 ? "mortal servant" : "mortal servants"; | ||||
|         let itemsPurloinedSpcr = itemsPurloined == 1 ? "              " : "               "; | ||||
|         let vampiricSkillsSpcr = vampiricSkills == 1 ? "              " : "               "; | ||||
|         let mortalServantsSpcr = mortalServants == 1 ? "              " : "               "; | ||||
|           mortalServants >= 25 | ||||
|             ? "where you live with many friends." | ||||
|             : mortalServants >= 1 | ||||
|               ? "where you live with a couple of friends." | ||||
|               : "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"; | ||||
|         let vampiricSkillsText = | ||||
|           vampiricSkills == 1 ? "vampiric skill" : "vampiric skills"; | ||||
|         let mortalServantsText = | ||||
|           mortalServants == 1 ? "mortal servant" : "mortal servants"; | ||||
|         let itemsPurloinedSpcr = | ||||
|           itemsPurloined == 1 ? "              " : "               "; | ||||
|         let vampiricSkillsSpcr = | ||||
|           vampiricSkills == 1 ? "              " : "               "; | ||||
|         let mortalServantsSpcr = | ||||
|           mortalServants == 1 ? "              " : "               "; | ||||
|  | ||||
|         D.drawText( | ||||
|           `${itemsPurloined} ${itemsPurloinedText}\n${vampiricSkills} ${vampiricSkillsText}\n${mortalServants} ${mortalServantsText}`, | ||||
|           new Point(WIDTH / 2, 224), FG_TEXT, {alignX: AlignX.Center} | ||||
|         ) | ||||
|           new Point(WIDTH / 2, 224), | ||||
|           FG_TEXT, | ||||
|           { alignX: AlignX.Center }, | ||||
|         ); | ||||
|         D.drawText( | ||||
|           `${itemsPurloined} ${itemsPurloinedSpcr}\n${vampiricSkills} ${vampiricSkillsSpcr}\n${mortalServants} ${mortalServantsSpcr}`, | ||||
|           new Point(WIDTH / 2, 224), FG_BOLD, {alignX: AlignX.Center} | ||||
|           new Point(WIDTH / 2, 224), | ||||
|           FG_BOLD, | ||||
|           { alignX: AlignX.Center }, | ||||
|         ); | ||||
|         let msg = "That's pretty dreadful." | ||||
|         let msg = "That's pretty dreadful."; | ||||
|         if (mortalServants >= 10) { | ||||
|           msg = "That's more than zero." | ||||
|           msg = "That's more than zero."; | ||||
|         } | ||||
|         if (mortalServants >= 30) { | ||||
|           msg = "That feels like a lot!" | ||||
|           msg = "That feels like a lot!"; | ||||
|         } | ||||
|         D.drawText(msg, new Point(0, 288), FG_TEXT) | ||||
|         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}) | ||||
|       }) | ||||
|         D.drawText(msg, new Point(0, 288), FG_TEXT); | ||||
|         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, | ||||
|         this.#ending?.personal?.successorVerb ?? "Do Unknown Things", | ||||
|         new Rect( | ||||
|           new Point(0, HEIGHT - 32), new Size(WIDTH, 32) | ||||
|         ), | ||||
|         new Rect(new Point(0, HEIGHT - 32), new Size(WIDTH, 32)), | ||||
|         true, | ||||
|         () => { | ||||
|           this.#page += 1; | ||||
|         } | ||||
|       ) | ||||
|     } | ||||
|     else if (this.#page == 1) { | ||||
|         }, | ||||
|       ); | ||||
|     } else if (this.#page == 1) { | ||||
|       this.#drawpile.add(0, () => { | ||||
|         D.drawText("Choose your successor:", new Point(0, 0), FG_TEXT); | ||||
|       }) | ||||
|       }); | ||||
|  | ||||
|       this.#addCandidate(0, new Point(0, 16)) | ||||
|       this.#addCandidate(1, new Point(0, 80)) | ||||
|       this.#addCandidate(2, new Point(0, 144)) | ||||
|       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:${optionalNote}`, new Point(0, 224), FG_TEXT); | ||||
|       }) | ||||
|         D.drawText( | ||||
|           `Plan their destiny:${optionalNote}`, | ||||
|           new Point(0, 224), | ||||
|           FG_TEXT, | ||||
|         ); | ||||
|       }); | ||||
|  | ||||
|       this.#addWish(1, new Point(0, 240)) | ||||
|       this.#addWish(0, new Point(128, 240)) | ||||
|       this.#addWish(2, new Point(256, 240)) | ||||
|       this.#addWish(1, new Point(0, 240)); | ||||
|       this.#addWish(0, new Point(128, 240)); | ||||
|       this.#addWish(2, new Point(256, 240)); | ||||
|  | ||||
|       addButton( | ||||
|         this.#drawpile, | ||||
|         "Back", | ||||
|         new Rect( | ||||
|           new Point(0, HEIGHT - 32), new Size(WIDTH / 3, 32) | ||||
|         ), | ||||
|         new Rect(new Point(0, HEIGHT - 32), new Size(WIDTH / 3, 32)), | ||||
|         true, | ||||
|         () => { | ||||
|           this.#page -= 1; | ||||
|         } | ||||
|       ) | ||||
|         }, | ||||
|       ); | ||||
|       addButton( | ||||
|         this.#drawpile, | ||||
|         this.#ending?.personal.progenerateVerb ?? "Unknown Action", | ||||
|         new Rect( | ||||
|           new Point(WIDTH/3, HEIGHT - 32), new Size(WIDTH - WIDTH / 3, 32) | ||||
|           new Point(WIDTH / 3, HEIGHT - 32), | ||||
|           new Size(WIDTH - WIDTH / 3, 32), | ||||
|         ), | ||||
|         this.#canProgenerate, | ||||
|         () => { | ||||
|           this.#progenerate() | ||||
|         } | ||||
|       ) | ||||
|           this.#progenerate(); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     this.#drawpile.executeOnClick(); | ||||
| @@ -253,39 +287,55 @@ export class EndgameModal { | ||||
|         if (hover || selected) { | ||||
|           [bg, fg, fgBold] = [FG_BOLD, BG_INSET, BG_INSET]; | ||||
|         } | ||||
|         D.fillRect( | ||||
|           at.offset(new Point(0, 4)), new Size(w, h - 8), bg, | ||||
|         ) | ||||
|         D.drawRect( | ||||
|           at.offset(new Point(0, 4)), new Size(w, h - 8), fg, | ||||
|         ) | ||||
|         D.fillRect(at.offset(new Point(0, 4)), new Size(w, h - 8), bg); | ||||
|         D.drawRect(at.offset(new Point(0, 4)), new Size(w, h - 8), fg); | ||||
|  | ||||
|         D.drawText(candidate.name + ", " + candidate.title, at.offset(new Point(4, 8)), fg); | ||||
|         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), | ||||
|           new Point(116, 24), new Point(116, 40) | ||||
|           new Point(4, 24), | ||||
|           new Point(4, 40), | ||||
|           new Point(116, 24), | ||||
|           new Point(116, 40), | ||||
|         ]; | ||||
|         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(`${statValue}`, at.offset(xys[i].offset(new Point(32, 0))), fgBold) | ||||
|           D.drawText(s, at.offset(xys[i]), 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) | ||||
|             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) | ||||
|             D.drawText( | ||||
|               `(${talentValue})`, | ||||
|               at.offset(xys[i].offset(new Point(56, 0))), | ||||
|               fg, | ||||
|             ); | ||||
|           } | ||||
|           i += 1; | ||||
|         } | ||||
|  | ||||
|         if (candidate.note != null) { | ||||
|           D.drawText(candidate.note, at.offset(new Point(224, 24)), fg, {forceWidth: w - 224}) | ||||
|           D.drawText(candidate.note, at.offset(new Point(224, 24)), fg, { | ||||
|             forceWidth: w - 224, | ||||
|           }); | ||||
|         } | ||||
|       }, | ||||
|       generalRect, | ||||
| @@ -293,11 +343,12 @@ export class EndgameModal { | ||||
|  | ||||
|       () => { | ||||
|         if (this.#selectedSuccessor == ix) { | ||||
|           this.#selectedSuccessor = null | ||||
|           this.#selectedSuccessor = null; | ||||
|         } else { | ||||
|           this.#selectedSuccessor = ix; | ||||
|         } | ||||
|       }); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   #addWish(ix: number, at: Point) { | ||||
| @@ -324,21 +375,27 @@ export class EndgameModal { | ||||
|         if (hover || selected) { | ||||
|           [bg, fg, fgBold] = [FG_BOLD, BG_INSET, BG_INSET]; | ||||
|         } | ||||
|         D.fillRect( | ||||
|           at.offset(new Point(2, 4)), new Size(w - 4, h - 8), bg, | ||||
|         ) | ||||
|         D.drawRect( | ||||
|           at.offset(new Point(2, 4)), new Size(w - 4, h - 8), fg, | ||||
|         ) | ||||
|         D.fillRect(at.offset(new Point(2, 4)), new Size(w - 4, h - 8), bg); | ||||
|         D.drawRect(at.offset(new Point(2, 4)), new Size(w - 4, h - 8), fg); | ||||
|  | ||||
|         D.drawText(wishData.profile.name, at.offset(new Point(w / 2,h / 2 )), fgBold, { | ||||
|           forceWidth: w - 4, | ||||
|           alignX: AlignX.Center, | ||||
|           alignY: AlignY.Middle, | ||||
|         }); | ||||
|         D.drawText(wishData.profile.note, at.offset(new Point(w / 2, h)), FG_TEXT, { | ||||
|           alignX: AlignX.Center | ||||
|         }); | ||||
|         D.drawText( | ||||
|           wishData.profile.name, | ||||
|           at.offset(new Point(w / 2, h / 2)), | ||||
|           fgBold, | ||||
|           { | ||||
|             forceWidth: w - 4, | ||||
|             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, | ||||
| @@ -349,7 +406,7 @@ export class EndgameModal { | ||||
|         } else { | ||||
|           this.#selectedWish = ix; | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -362,8 +419,7 @@ export class EndgameModal { | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| let active = new EndgameModal(); | ||||
| export function getEndgameModal() { | ||||
|   return active; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import {compile, VNScene, VNSceneBasisPart} from "./vnscene.ts"; | ||||
| import { compile, VNScene, VNSceneBasisPart } from "./vnscene.ts"; | ||||
|  | ||||
| const squeak: VNSceneBasisPart = { | ||||
|   type: "message", | ||||
|   text: "...", | ||||
|   sfx: "squeak.mp3" | ||||
| } | ||||
|   sfx: "squeak.mp3", | ||||
| }; | ||||
|  | ||||
| export const sceneBat: VNScene = compile([ | ||||
|   squeak, | ||||
| @@ -25,8 +25,8 @@ export const sceneBat: VNScene = compile([ | ||||
| const doorbell: VNSceneBasisPart = { | ||||
|   type: "message", | ||||
|   text: "...", | ||||
|   sfx: "doorbell.mp3" | ||||
| } | ||||
|   sfx: "doorbell.mp3", | ||||
| }; | ||||
|  | ||||
| export const sceneStealth: VNScene = compile([ | ||||
|   doorbell, | ||||
| @@ -46,8 +46,8 @@ export const sceneStealth: VNScene = compile([ | ||||
| const phoneBeep: VNSceneBasisPart = { | ||||
|   type: "message", | ||||
|   text: "...", | ||||
|   sfx: "phonebeep.mp3" | ||||
| } | ||||
|   sfx: "phonebeep.mp3", | ||||
| }; | ||||
|  | ||||
| export const sceneCharm: VNScene = compile([ | ||||
|   phoneBeep, | ||||
| @@ -72,8 +72,8 @@ export const sceneCharm: VNScene = compile([ | ||||
| const sleepyBreath: VNSceneBasisPart = { | ||||
|   type: "message", | ||||
|   text: "...", | ||||
|   sfx: "sleepyBreath.mp3" | ||||
| } | ||||
|   sfx: "sleepyBreath.mp3", | ||||
| }; | ||||
|  | ||||
| export const sceneStare: VNScene = compile([ | ||||
|   sleepyBreath, | ||||
| @@ -93,7 +93,7 @@ export const sceneStare: VNScene = compile([ | ||||
| const party: VNSceneBasisPart = { | ||||
|   type: "message", | ||||
|   text: "...", | ||||
|   sfx: "party.mp3" | ||||
|   sfx: "party.mp3", | ||||
| }; | ||||
|  | ||||
| export const sceneParty: VNScene = compile([ | ||||
| @@ -111,7 +111,7 @@ export const sceneParty: VNScene = compile([ | ||||
| const ghost: VNSceneBasisPart = { | ||||
|   type: "message", | ||||
|   text: "...", | ||||
|   sfx: "ghost.mp3" | ||||
|   sfx: "ghost.mp3", | ||||
| }; | ||||
|  | ||||
| export const sceneLore: VNScene = compile([ | ||||
| @@ -126,4 +126,4 @@ export const sceneLore: VNScene = compile([ | ||||
|   ghost, | ||||
|   "Yeah. They remember.", | ||||
|   ghost, | ||||
| ]); | ||||
| ]); | ||||
|   | ||||
| @@ -17,11 +17,13 @@ export class Color { | ||||
|   } | ||||
|  | ||||
|   static parseHexCode(hexCode: string) { | ||||
|     const regex1 = /#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/; | ||||
|     const regex2 = /#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})?/; | ||||
|     const regex1 = | ||||
|       /#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/; | ||||
|     const regex2 = | ||||
|       /#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})?/; | ||||
|     let result = regex1.exec(hexCode) ?? regex2.exec(hexCode); | ||||
|     if (result == null) { | ||||
|       throw `could not parse color: ${hexCode}` | ||||
|       throw `could not parse color: ${hexCode}`; | ||||
|     } | ||||
|  | ||||
|     let parseGroup = (s: string | undefined): number => { | ||||
| @@ -32,7 +34,7 @@ export class Color { | ||||
|         return 17 * parseInt(s, 16); | ||||
|       } | ||||
|       return parseInt(s, 16); | ||||
|     } | ||||
|     }; | ||||
|     return new Color( | ||||
|       parseGroup(result[1]), | ||||
|       parseGroup(result[2]), | ||||
| @@ -42,7 +44,7 @@ export class Color { | ||||
|   } | ||||
|  | ||||
|   toStyle(): string { | ||||
|     return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a / 255.0})` | ||||
|     return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a / 255.0})`; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -56,7 +58,7 @@ export class Point { | ||||
|   } | ||||
|  | ||||
|   toString(): string { | ||||
|     return `${this.x},${this.y}` | ||||
|     return `${this.x},${this.y}`; | ||||
|   } | ||||
|  | ||||
|   offset(other: Point | Size): Point { | ||||
| @@ -109,7 +111,7 @@ export class Size { | ||||
|   } | ||||
|  | ||||
|   toString(): string { | ||||
|     return `${this.w}x${this.h}` | ||||
|     return `${this.w}x${this.h}`; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -127,7 +129,12 @@ export class Rect { | ||||
|   } | ||||
|  | ||||
|   contains(other: Point) { | ||||
|     return (other.x >= this.top.x && other.y >= this.top.y && other.x < this.top.x + this.size.w && other.y < this.top.y + this.size.h); | ||||
|     return ( | ||||
|       other.x >= this.top.x && | ||||
|       other.y >= this.top.y && | ||||
|       other.x < this.top.x + this.size.w && | ||||
|       other.y < this.top.y + this.size.h | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   overlaps(other: Rect) { | ||||
| @@ -156,20 +163,20 @@ export class Grid<T> { | ||||
|     for (let y = 0; y < size.h; y++) { | ||||
|       let row = []; | ||||
|       for (let x = 0; x < size.w; x++) { | ||||
|         row.push(cbDefault(new Point(x, y))) | ||||
|         row.push(cbDefault(new Point(x, y))); | ||||
|       } | ||||
|       this.#data.push(row); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static createGridFromMultilineString(multiline: string): Grid<string> { | ||||
|     let lines = [] | ||||
|     let lines = []; | ||||
|     for (let line of multiline.split("\n")) { | ||||
|       let trimmedLine = line.trim(); | ||||
|       if (trimmedLine == "") { | ||||
|         continue; | ||||
|       } | ||||
|       lines.push(trimmedLine) | ||||
|       lines.push(trimmedLine); | ||||
|     } | ||||
|     return this.createGridFromStringArray(lines); | ||||
|   } | ||||
| @@ -181,17 +188,14 @@ export class Grid<T> { | ||||
|       let w1 = ary[i].length; | ||||
|       let w2 = ary[i + 1].length; | ||||
|       if (w1 != w2) { | ||||
|         throw `createGridFromStringArray: must be grid-shaped, got ${ary}` | ||||
|         throw `createGridFromStringArray: must be grid-shaped, got ${ary}`; | ||||
|       } | ||||
|       w = w1; | ||||
|     } | ||||
|  | ||||
|     return new Grid( | ||||
|       new Size(w, h), | ||||
|       (xy) => { | ||||
|         return ary[xy.y].charAt(xy.x); | ||||
|       } | ||||
|     ) | ||||
|     return new Grid(new Size(w, h), (xy) => { | ||||
|       return ary[xy.y].charAt(xy.x); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static createGridFromJaggedArray<T>(ary: Array<Array<T>>): Grid<T> { | ||||
| @@ -201,17 +205,14 @@ export class Grid<T> { | ||||
|       let w1 = ary[i].length; | ||||
|       let w2 = ary[i + 1].length; | ||||
|       if (w1 != w2) { | ||||
|         throw `createGridFromJaggedArray: must be grid-shaped, got ${ary}` | ||||
|         throw `createGridFromJaggedArray: must be grid-shaped, got ${ary}`; | ||||
|       } | ||||
|       w = w1; | ||||
|     } | ||||
|  | ||||
|     return new Grid( | ||||
|       new Size(w, h), | ||||
|       (xy) => { | ||||
|         return ary[xy.y][xy.x]; | ||||
|       } | ||||
|     ) | ||||
|     return new Grid(new Size(w, h), (xy) => { | ||||
|       return ary[xy.y][xy.x]; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   map<T2>(cbCell: (content: T, position: Point) => T2) { | ||||
| @@ -220,10 +221,14 @@ export class Grid<T> { | ||||
|  | ||||
|   #checkPosition(position: Point) { | ||||
|     if ( | ||||
|       (position.x < 0 || position.x >= this.size.w || Math.floor(position.x) != position.x) || | ||||
|       (position.y < 0 || position.y >= this.size.h || Math.floor(position.y) != position.y) | ||||
|       position.x < 0 || | ||||
|       position.x >= this.size.w || | ||||
|       Math.floor(position.x) != position.x || | ||||
|       position.y < 0 || | ||||
|       position.y >= this.size.h || | ||||
|       Math.floor(position.y) != position.y | ||||
|     ) { | ||||
|       throw new Error(`invalid position for ${this.size}: ${position}`) | ||||
|       throw new Error(`invalid position for ${this.size}: ${position}`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -241,7 +246,7 @@ export class Grid<T> { | ||||
| export enum AlignX { | ||||
|   Left = 0, | ||||
|   Center = 1, | ||||
|   Right = 2 | ||||
|   Right = 2, | ||||
| } | ||||
|  | ||||
| export enum AlignY { | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class Assets { | ||||
|     //  and then wait for isLoaded to return true) | ||||
|     for (let filename in this.#images) { | ||||
|       if (!this.#images[filename].complete) { | ||||
|         return false | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -29,7 +29,7 @@ class Assets { | ||||
|       element.src = filename; | ||||
|       this.#images[filename] = element; | ||||
|     } | ||||
|     return element | ||||
|     return element; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -38,4 +38,3 @@ let active: Assets = new Assets(); | ||||
| export function getAssets(): Assets { | ||||
|   return active; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,18 +1,17 @@ | ||||
| const MAX_UPDATES_BANKED: number = 20.0; | ||||
|  | ||||
| // always run physics at 240 hz | ||||
| const UPDATES_PER_MS: number = 1/(1000.0/240.0); | ||||
| const UPDATES_PER_MS: number = 1 / (1000.0 / 240.0); | ||||
|  | ||||
| class Clock { | ||||
|   #lastTimestamp: number | undefined; | ||||
|   #updatesBanked: number | ||||
|   #updatesBanked: number; | ||||
|  | ||||
|   constructor() { | ||||
|     this.#lastTimestamp = undefined; | ||||
|     this.#updatesBanked = 0.0 | ||||
|     this.#updatesBanked = 0.0; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   recordTimestamp(timestamp: number) { | ||||
|     if (this.#lastTimestamp) { | ||||
|       let delta = timestamp - this.#lastTimestamp; | ||||
| @@ -26,7 +25,7 @@ class Clock { | ||||
|     // and remove one draw from the bank | ||||
|     if (this.#updatesBanked > 1) { | ||||
|       this.#updatesBanked -= 1; | ||||
|       return true | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| @@ -40,5 +39,3 @@ let active: Clock = new Clock(); | ||||
| export function getClock(): Clock { | ||||
|   return active; | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import {getScreen} from "./screen.ts"; | ||||
| import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts"; | ||||
| import {mainFont} from "./font.ts"; | ||||
| import {Sprite} from "./sprite.ts"; | ||||
| import { getScreen } from "./screen.ts"; | ||||
| import { AlignX, AlignY, Color, Point, Size } from "../datatypes.ts"; | ||||
| import { mainFont } from "./font.ts"; | ||||
| import { Sprite } from "./sprite.ts"; | ||||
|  | ||||
| class Drawing { | ||||
|   camera: Point; | ||||
| @@ -19,7 +19,9 @@ class Drawing { | ||||
|     this.camera = oldCamera; | ||||
|   } | ||||
|  | ||||
|   get size() { return getScreen().size; } | ||||
|   get size() { | ||||
|     return getScreen().size; | ||||
|   } | ||||
|  | ||||
|   invertRect(position: Point, size: Size) { | ||||
|     position = this.camera.negate().offset(position); | ||||
| @@ -31,8 +33,8 @@ class Drawing { | ||||
|       Math.floor(position.x), | ||||
|       Math.floor(position.y), | ||||
|       Math.floor(size.w), | ||||
|       Math.floor(size.h) | ||||
|     ) | ||||
|       Math.floor(size.h), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   fillRect(position: Point, size: Size, color: Color) { | ||||
| @@ -44,7 +46,7 @@ class Drawing { | ||||
|       Math.floor(position.x), | ||||
|       Math.floor(position.y), | ||||
|       Math.floor(size.w), | ||||
|       Math.floor(size.h) | ||||
|       Math.floor(size.h), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -57,11 +59,16 @@ class Drawing { | ||||
|       Math.floor(position.x) + 0.5, | ||||
|       Math.floor(position.y) + 0.5, | ||||
|       Math.floor(size.w) - 1, | ||||
|       Math.floor(size.h) - 1 | ||||
|     ) | ||||
|       Math.floor(size.h) - 1, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   drawText(text: string, position: Point, color: Color, options?: {alignX?: AlignX, alignY?: AlignY, forceWidth?: number}) { | ||||
|   drawText( | ||||
|     text: string, | ||||
|     position: Point, | ||||
|     color: Color, | ||||
|     options?: { alignX?: AlignX; alignY?: AlignY; forceWidth?: number }, | ||||
|   ) { | ||||
|     position = this.camera.negate().offset(position); | ||||
|  | ||||
|     let ctx = getScreen().unsafeMakeContext(); | ||||
| @@ -72,19 +79,30 @@ class Drawing { | ||||
|       alignX: options?.alignX, | ||||
|       alignY: options?.alignY, | ||||
|       forceWidth: options?.forceWidth, | ||||
|       color | ||||
|     }) | ||||
|       color, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   measureText(text: string, forceWidth?: number): Size { | ||||
|     return mainFont.measureText({text, forceWidth}) | ||||
|     return mainFont.measureText({ text, forceWidth }); | ||||
|   } | ||||
|  | ||||
|   drawSprite(sprite: Sprite, position: Point, ix?: number, options?: {xScale?: number, yScale: number, angle?: number}) { | ||||
|   drawSprite( | ||||
|     sprite: Sprite, | ||||
|     position: Point, | ||||
|     ix?: number, | ||||
|     options?: { xScale?: number; yScale: number; angle?: number }, | ||||
|   ) { | ||||
|     position = this.camera.negate().offset(position); | ||||
|  | ||||
|     let ctx = getScreen().unsafeMakeContext(); | ||||
|     sprite.internalDraw(ctx, {position, ix, xScale: options?.xScale, yScale: options?.yScale, angle: options?.angle}) | ||||
|     sprite.internalDraw(ctx, { | ||||
|       position, | ||||
|       ix, | ||||
|       xScale: options?.xScale, | ||||
|       yScale: options?.yScale, | ||||
|       angle: options?.angle, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -93,5 +111,3 @@ let active: Drawing = new Drawing(); | ||||
| export function getDrawing(): Drawing { | ||||
|   return active; | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import {getAssets} from "./assets.ts"; | ||||
| import fontSheet from '../../art/fonts/vga_8x16.png'; | ||||
| import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts"; | ||||
| import { getAssets } from "./assets.ts"; | ||||
| import fontSheet from "../../art/fonts/vga_8x16.png"; | ||||
| import { AlignX, AlignY, Color, Point, Size } from "../datatypes.ts"; | ||||
|  | ||||
| class Font { | ||||
|   #filename: string; | ||||
| @@ -14,18 +14,28 @@ class Font { | ||||
|     this.#cellsPerSheet = cellsPerSheet; | ||||
|     this.#pixelsPerCell = pixelsPerCell; | ||||
|     this.#tintingCanvas = document.createElement("canvas"); | ||||
|     this.#tintedVersions = {} | ||||
|     this.#tintedVersions = {}; | ||||
|   } | ||||
|  | ||||
|   get #cx(): number { return this.#cellsPerSheet.w } | ||||
|   get #cy(): number { return this.#cellsPerSheet.h } | ||||
|   get #px(): number { return this.#pixelsPerCell.w } | ||||
|   get #py(): number { return this.#pixelsPerCell.h } | ||||
|   get #cx(): number { | ||||
|     return this.#cellsPerSheet.w; | ||||
|   } | ||||
|   get #cy(): number { | ||||
|     return this.#cellsPerSheet.h; | ||||
|   } | ||||
|   get #px(): number { | ||||
|     return this.#pixelsPerCell.w; | ||||
|   } | ||||
|   get #py(): number { | ||||
|     return this.#pixelsPerCell.h; | ||||
|   } | ||||
|  | ||||
|   #getTintedImage(color: string): HTMLImageElement | null { | ||||
|     let image = getAssets().getImage(this.#filename); | ||||
|  | ||||
|     if (!image.complete) { return null; } | ||||
|     if (!image.complete) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     let tintedVersion = this.#tintedVersions[color]; | ||||
|     if (tintedVersion != undefined) { | ||||
| @@ -36,7 +46,7 @@ class Font { | ||||
|     let h = image.height; | ||||
|  | ||||
|     if (!(w == this.#cx * this.#px && h == this.#cy * this.#py)) { | ||||
|       throw `unexpected image dimensions for font ${this.#filename}: ${w} x ${h}` | ||||
|       throw `unexpected image dimensions for font ${this.#filename}: ${w} x ${h}`; | ||||
|     } | ||||
|  | ||||
|     this.#tintingCanvas.width = w; | ||||
| @@ -55,17 +65,28 @@ class Font { | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   internalDrawText({ctx, text, position, alignX, alignY, forceWidth, color}: { | ||||
|     ctx: CanvasRenderingContext2D, | ||||
|     text: string, | ||||
|     position: Point, alignX?: AlignX, alignY?: AlignY, | ||||
|     forceWidth?: number, color: Color | ||||
|   internalDrawText({ | ||||
|     ctx, | ||||
|     text, | ||||
|     position, | ||||
|     alignX, | ||||
|     alignY, | ||||
|     forceWidth, | ||||
|     color, | ||||
|   }: { | ||||
|     ctx: CanvasRenderingContext2D; | ||||
|     text: string; | ||||
|     position: Point; | ||||
|     alignX?: AlignX; | ||||
|     alignY?: AlignY; | ||||
|     forceWidth?: number; | ||||
|     color: Color; | ||||
|   }) { | ||||
|     alignX = alignX == undefined ? AlignX.Left : alignX; | ||||
|     alignY = alignY == undefined ? AlignY.Top : alignY; | ||||
|     forceWidth = forceWidth == undefined ? 65535 : forceWidth; | ||||
|  | ||||
|     let image = this.#getTintedImage(color.toStyle()) | ||||
|     let image = this.#getTintedImage(color.toStyle()); | ||||
|     if (image == null) { | ||||
|       return; | ||||
|     } | ||||
| @@ -73,43 +94,80 @@ class Font { | ||||
|     let sz = this.#glyphwise(text, forceWidth, () => {}); | ||||
|     let offsetX = position.x; | ||||
|     let offsetY = position.y; | ||||
|     offsetX += (alignX == AlignX.Left ? 0 : alignX == AlignX.Center ? -sz.w / 2 : - sz.w) | ||||
|     offsetY += (alignY == AlignY.Top ? 0 : alignY == AlignY.Middle ? -sz.h / 2 : - sz.h) | ||||
|     offsetX += | ||||
|       alignX == AlignX.Left ? 0 : alignX == AlignX.Center ? -sz.w / 2 : -sz.w; | ||||
|     offsetY += | ||||
|       alignY == AlignY.Top ? 0 : alignY == AlignY.Middle ? -sz.h / 2 : -sz.h; | ||||
|  | ||||
|     this.#glyphwise(text, forceWidth, (cx, cy, char) => { | ||||
|       let srcIx = char.charCodeAt(0); | ||||
|       this.#drawGlyph({ctx: ctx, image: image, ix: srcIx, x: offsetX + cx * this.#px, y: offsetY + cy * this.#py}); | ||||
|     }) | ||||
|       this.#drawGlyph({ | ||||
|         ctx: ctx, | ||||
|         image: image, | ||||
|         ix: srcIx, | ||||
|         x: offsetX + cx * this.#px, | ||||
|         y: offsetY + cy * this.#py, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   #drawGlyph({ctx, image, ix, x, y}: {ctx: CanvasRenderingContext2D, image: HTMLImageElement, ix: number, x: number, y: number}) { | ||||
|   #drawGlyph({ | ||||
|     ctx, | ||||
|     image, | ||||
|     ix, | ||||
|     x, | ||||
|     y, | ||||
|   }: { | ||||
|     ctx: CanvasRenderingContext2D; | ||||
|     image: HTMLImageElement; | ||||
|     ix: number; | ||||
|     x: number; | ||||
|     y: number; | ||||
|   }) { | ||||
|     let srcCx = ix % this.#cx; | ||||
|     let srcCy = Math.floor(ix / this.#cx); | ||||
|     let srcPx = srcCx * this.#px; | ||||
|     let srcPy = srcCy * this.#py; | ||||
|     ctx.drawImage( | ||||
|       image, | ||||
|       srcPx, srcPy, this.#px, this.#py, | ||||
|       Math.floor(x), Math.floor(y), this.#px, this.#py | ||||
|       srcPx, | ||||
|       srcPy, | ||||
|       this.#px, | ||||
|       this.#py, | ||||
|       Math.floor(x), | ||||
|       Math.floor(y), | ||||
|       this.#px, | ||||
|       this.#py, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   measureText({text, forceWidth}: {text: string, forceWidth?: number}): Size { | ||||
|   measureText({ | ||||
|     text, | ||||
|     forceWidth, | ||||
|   }: { | ||||
|     text: string; | ||||
|     forceWidth?: number; | ||||
|   }): Size { | ||||
|     return this.#glyphwise(text, forceWidth, () => {}); | ||||
|   } | ||||
|  | ||||
|   #glyphwise(text: string, forceWidth: number | undefined, callback: (x: number, y: number, char: string) => void): Size { | ||||
|   #glyphwise( | ||||
|     text: string, | ||||
|     forceWidth: number | undefined, | ||||
|     callback: (x: number, y: number, char: string) => void, | ||||
|   ): Size { | ||||
|     let cx = 0; | ||||
|     let cy = 0; | ||||
|     let cw = 0; | ||||
|     let ch = 0; | ||||
|     let wcx = forceWidth == undefined ? undefined : Math.floor(forceWidth / this.#px); | ||||
|     let wcx = | ||||
|       forceWidth == undefined ? undefined : Math.floor(forceWidth / this.#px); | ||||
|  | ||||
|     text = betterWordWrap(text, wcx); | ||||
|  | ||||
|     for (let i = 0; i < text.length; i++) { | ||||
|       let char = text[i] | ||||
|       if (char == '\n') { | ||||
|       let char = text[i]; | ||||
|       if (char == "\n") { | ||||
|         cx = 0; | ||||
|         cy += 1; | ||||
|         ch = cy + 1; | ||||
| @@ -121,7 +179,7 @@ class Font { | ||||
|           ch = cy + 1; | ||||
|         } | ||||
|  | ||||
|         callback(cx, cy, char) | ||||
|         callback(cx, cy, char); | ||||
|         cx += 1; | ||||
|         cw = Math.max(cw, cx); | ||||
|         ch = cy + 1; | ||||
| @@ -132,15 +190,15 @@ class Font { | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| // https://stackoverflow.com/users/1993501/edi9999 | ||||
| function betterWordWrap(s: string, wcx?: number) { | ||||
|   if (wcx === undefined) { | ||||
|     return s; | ||||
|   } | ||||
|   return s.replace( | ||||
|     new RegExp(`(?![^\\n]{1,${wcx}}$)([^\\n]{1,${wcx}})\\s`, 'g'), '$1\n' | ||||
|     new RegExp(`(?![^\\n]{1,${wcx}}$)([^\\n]{1,${wcx}})\\s`, "g"), | ||||
|     "$1\n", | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export let mainFont = new Font(fontSheet, new Size(32, 8), new Size(8, 16)); | ||||
| export let mainFont = new Font(fontSheet, new Size(32, 8), new Size(8, 16)); | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import './style.css' | ||||
| import "./style.css"; | ||||
|  | ||||
| import {pollAndTouch} from "./screen.ts"; | ||||
| import {getClock} from "./clock.ts"; | ||||
| import {getInput, setupInput} from "./input.ts"; | ||||
| import {IGame} from "../datatypes.ts"; | ||||
| import { pollAndTouch } from "./screen.ts"; | ||||
| import { getClock } from "./clock.ts"; | ||||
| import { getInput, setupInput } from "./input.ts"; | ||||
| import { IGame } from "../datatypes.ts"; | ||||
|  | ||||
| export function hostGame(game: IGame) { | ||||
|   let gameCanvas = document.getElementById("game") as HTMLCanvasElement; | ||||
|   setupInput(gameCanvas); | ||||
|   onFrame(game, undefined);  // start on-frame draw loop, set up screen | ||||
|   onFrame(game, undefined); // start on-frame draw loop, set up screen | ||||
| } | ||||
|  | ||||
| function onFrame(game: IGame, timestamp: number | undefined) { | ||||
| @@ -31,4 +31,3 @@ function onFrame(game: IGame, timestamp: number | undefined) { | ||||
| function onFrameFixScreen(canvas: HTMLCanvasElement) { | ||||
|   pollAndTouch(canvas); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import {getScreen} from "./screen.ts"; | ||||
| import {Point} from "../datatypes.ts"; | ||||
| import { getScreen } from "./screen.ts"; | ||||
| import { Point } from "../datatypes.ts"; | ||||
|  | ||||
| function handleKey(e: KeyboardEvent, down: boolean) { | ||||
|   active.handleKeyDown(e.key, down); | ||||
| @@ -12,25 +12,31 @@ function handleMouseMove(canvas: HTMLCanvasElement, m: MouseEvent) { | ||||
|   if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) { | ||||
|     return; | ||||
|   } | ||||
|   active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight); | ||||
|   active.handleMouseMove( | ||||
|     m.offsetX / canvas.offsetWidth, | ||||
|     m.offsetY / canvas.offsetHeight, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function handleMouseButton(canvas: HTMLCanvasElement, m: MouseEvent, down: boolean) { | ||||
| function handleMouseButton( | ||||
|   canvas: HTMLCanvasElement, | ||||
|   m: MouseEvent, | ||||
|   down: boolean, | ||||
| ) { | ||||
|   if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) { | ||||
|     return; | ||||
|   } | ||||
|   active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight); | ||||
|   let button: MouseButton | null = ( | ||||
|     m.button == 0 ? "leftMouse" : | ||||
|       m.button == 1 ? "rightMouse" : | ||||
|         null | ||||
|   ) | ||||
|   active.handleMouseMove( | ||||
|     m.offsetX / canvas.offsetWidth, | ||||
|     m.offsetY / canvas.offsetHeight, | ||||
|   ); | ||||
|   let button: MouseButton | null = | ||||
|     m.button == 0 ? "leftMouse" : m.button == 1 ? "rightMouse" : null; | ||||
|   if (button != null) { | ||||
|     active.handleMouseDown(button, down); | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| export function setupInput(canvas: HTMLCanvasElement) { | ||||
|   canvas.addEventListener("keyup", (k) => handleKey(k, false)); | ||||
|   document.addEventListener("keyup", (k) => handleKey(k, false)); | ||||
| @@ -38,8 +44,12 @@ export function setupInput(canvas: HTMLCanvasElement) { | ||||
|   document.addEventListener("keydown", (k) => handleKey(k, true)); | ||||
|   canvas.addEventListener("mouseout", (_) => handleMouseOut()); | ||||
|   canvas.addEventListener("mousemove", (m) => handleMouseMove(canvas, m)); | ||||
|   canvas.addEventListener("mousedown", (m) => handleMouseButton(canvas, m, true)); | ||||
|   canvas.addEventListener("mouseup", (m) => handleMouseButton(canvas, m, false)); | ||||
|   canvas.addEventListener("mousedown", (m) => | ||||
|     handleMouseButton(canvas, m, true), | ||||
|   ); | ||||
|   canvas.addEventListener("mouseup", (m) => | ||||
|     handleMouseButton(canvas, m, false), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export type MouseButton = "leftMouse" | "rightMouse"; | ||||
| @@ -60,8 +70,8 @@ class Input { | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     this.#previousKeyDown = {...this.#keyDown}; | ||||
|     this.#previousMouseDown = {...this.#mouseDown}; | ||||
|     this.#previousKeyDown = { ...this.#keyDown }; | ||||
|     this.#previousMouseDown = { ...this.#mouseDown }; | ||||
|   } | ||||
|  | ||||
|   handleMouseDown(name: string, down: boolean) { | ||||
| @@ -73,51 +83,56 @@ class Input { | ||||
|  | ||||
|   handleMouseMove(x: number, y: number) { | ||||
|     let screen = getScreen(); | ||||
|     if (x < 0.0 || x >= 1.0) { this.#mousePosition = null; } | ||||
|     if (y < 0.0 || y >= 1.0) { this.#mousePosition = null; } | ||||
|     if (x < 0.0 || x >= 1.0) { | ||||
|       this.#mousePosition = null; | ||||
|     } | ||||
|     if (y < 0.0 || y >= 1.0) { | ||||
|       this.#mousePosition = null; | ||||
|     } | ||||
|  | ||||
|     let w = screen.size.w; | ||||
|     let h = screen.size.h; | ||||
|     this.#mousePosition = new Point( | ||||
|       Math.floor(x * w), | ||||
|       Math.floor(y * h), | ||||
|     ) | ||||
|     this.#mousePosition = new Point(Math.floor(x * w), Math.floor(y * h)); | ||||
|   } | ||||
|  | ||||
|   isMouseDown(btn: MouseButton) : boolean { | ||||
|   isMouseDown(btn: MouseButton): boolean { | ||||
|     return this.#mouseDown[btn]; | ||||
|   } | ||||
|  | ||||
|   isMouseClicked(btn: MouseButton) : boolean { | ||||
|   isMouseClicked(btn: MouseButton): boolean { | ||||
|     return this.#mouseDown[btn] && !this.#previousMouseDown[btn]; | ||||
|   } | ||||
|  | ||||
|   isMouseReleased(btn: MouseButton) : boolean { | ||||
|   isMouseReleased(btn: MouseButton): boolean { | ||||
|     return !this.#mouseDown[btn] && this.#previousMouseDown[btn]; | ||||
|   } | ||||
|  | ||||
|   get mousePosition(): Point | null { | ||||
|     return this.#mousePosition | ||||
|     return this.#mousePosition; | ||||
|   } | ||||
|  | ||||
|   isKeyDown(key: string) : boolean { | ||||
|   isKeyDown(key: string): boolean { | ||||
|     return this.#keyDown[key]; | ||||
|   } | ||||
|  | ||||
|   isKeyPressed(key: string) : boolean { | ||||
|   isKeyPressed(key: string): boolean { | ||||
|     return this.#keyDown[key] && !this.#previousKeyDown[key]; | ||||
|   } | ||||
|  | ||||
|   isKeyReleased(key: string) : boolean { | ||||
|   isKeyReleased(key: string): boolean { | ||||
|     return !this.#keyDown[key] && this.#previousKeyDown[key]; | ||||
|   } | ||||
|  | ||||
|   isAnythingPressed(): boolean { | ||||
|     for (let k of Object.keys(this.#keyDown)) { | ||||
|       if (this.#keyDown[k] && !this.#previousKeyDown[k]) { return true } | ||||
|       if (this.#keyDown[k] && !this.#previousKeyDown[k]) { | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|     for (let k of Object.keys(this.#mouseDown)) { | ||||
|       if (this.#mouseDown[k] && !this.#previousMouseDown[k]) { return true } | ||||
|       if (this.#mouseDown[k] && !this.#previousMouseDown[k]) { | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| @@ -127,4 +142,4 @@ let active = new Input(); | ||||
|  | ||||
| export function getInput(): Input { | ||||
|   return active; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import {Size} from "../datatypes.ts"; | ||||
| import { Size } from "../datatypes.ts"; | ||||
|  | ||||
| // TODO: Just switch to the same pattern as everywhere else | ||||
| // (without repeatedly reassigning the variable) | ||||
| class Screen { | ||||
|   #canvas: HTMLCanvasElement | ||||
|   size: Size | ||||
|   #canvas: HTMLCanvasElement; | ||||
|   size: Size; | ||||
|  | ||||
|   constructor(canvas: HTMLCanvasElement, size: Size) { | ||||
|     this.#canvas = canvas; | ||||
|     this.size = size | ||||
|     this.size = size; | ||||
|   } | ||||
|  | ||||
|   unsafeMakeContext(): CanvasRenderingContext2D { | ||||
| @@ -26,8 +26,7 @@ class Screen { | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| let active: Screen | undefined = undefined | ||||
| let active: Screen | undefined = undefined; | ||||
|  | ||||
| // TODO: Move these to Game? | ||||
| export let desiredWidth = 400; | ||||
| @@ -45,9 +44,9 @@ export function pollAndTouch(canvas: HTMLCanvasElement) { | ||||
|  | ||||
|   let div = 0; | ||||
|   while ( | ||||
|     (div < divisors.length - 1) && | ||||
|     (realWidth / divisors[div + 1] >= desiredWidth) && | ||||
|     (realHeight / divisors[div + 1] >= desiredHeight) | ||||
|     div < divisors.length - 1 && | ||||
|     realWidth / divisors[div + 1] >= desiredWidth && | ||||
|     realHeight / divisors[div + 1] >= desiredHeight | ||||
|   ) { | ||||
|     div += 1; | ||||
|   } | ||||
| @@ -60,9 +59,7 @@ export function pollAndTouch(canvas: HTMLCanvasElement) { | ||||
|  | ||||
| export function getScreen(): Screen { | ||||
|   if (active === undefined) { | ||||
|     throw `screen should have been defined: ${active}` | ||||
|     throw `screen should have been defined: ${active}`; | ||||
|   } | ||||
|   return active; | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import {getAssets} from "./assets.ts"; | ||||
| import {Point, Size} from "../datatypes.ts"; | ||||
|  | ||||
| import { getAssets } from "./assets.ts"; | ||||
| import { Point, Size } from "../datatypes.ts"; | ||||
|  | ||||
| export class Sprite { | ||||
|   readonly imageSet: string; | ||||
| @@ -11,7 +10,13 @@ export class Sprite { | ||||
|   // number of frames | ||||
|   readonly nFrames: number; | ||||
|  | ||||
|   constructor(imageSet: string, pixelsPerSubimage: Size, origin: Point, cellsPerSheet: Size, nFrames: number) { | ||||
|   constructor( | ||||
|     imageSet: string, | ||||
|     pixelsPerSubimage: Size, | ||||
|     origin: Point, | ||||
|     cellsPerSheet: Size, | ||||
|     nFrames: number, | ||||
|   ) { | ||||
|     this.imageSet = imageSet; | ||||
|     this.pixelsPerSubimage = pixelsPerSubimage; | ||||
|     this.origin = origin; | ||||
| @@ -24,7 +29,22 @@ export class Sprite { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   internalDraw(ctx: CanvasRenderingContext2D, {position, ix, xScale, yScale, angle}: {position: Point, ix?: number, xScale?: number, yScale?: number, angle?: number}) { | ||||
|   internalDraw( | ||||
|     ctx: CanvasRenderingContext2D, | ||||
|     { | ||||
|       position, | ||||
|       ix, | ||||
|       xScale, | ||||
|       yScale, | ||||
|       angle, | ||||
|     }: { | ||||
|       position: Point; | ||||
|       ix?: number; | ||||
|       xScale?: number; | ||||
|       yScale?: number; | ||||
|       angle?: number; | ||||
|     }, | ||||
|   ) { | ||||
|     ix = ix == undefined ? 0 : ix; | ||||
|     xScale = xScale == undefined ? 1.0 : xScale; | ||||
|     yScale = yScale == undefined ? 1.0 : yScale; | ||||
| @@ -32,7 +52,7 @@ export class Sprite { | ||||
|  | ||||
|     // ctx.translate(Math.floor(x), Math.floor(y)); | ||||
|     ctx.translate(Math.floor(position.x), Math.floor(position.y)); | ||||
|     ctx.rotate(angle * Math.PI / 180); | ||||
|     ctx.rotate((angle * Math.PI) / 180); | ||||
|     ctx.scale(xScale, yScale); | ||||
|     ctx.translate(-this.origin.x, -this.origin.y); | ||||
|  | ||||
| @@ -41,6 +61,16 @@ export class Sprite { | ||||
|     let srcCy = Math.floor(ix / this.cellsPerSheet.w); | ||||
|     let srcPx = srcCx * this.pixelsPerSubimage.w; | ||||
|     let srcPy = srcCy * this.pixelsPerSubimage.h; | ||||
|     ctx.drawImage(me, srcPx, srcPy, this.pixelsPerSubimage.w, this.pixelsPerSubimage.h, 0, 0, this.pixelsPerSubimage.w, this.pixelsPerSubimage.h); | ||||
|     ctx.drawImage( | ||||
|       me, | ||||
|       srcPx, | ||||
|       srcPy, | ||||
|       this.pixelsPerSubimage.w, | ||||
|       this.pixelsPerSubimage.h, | ||||
|       0, | ||||
|       0, | ||||
|       this.pixelsPerSubimage.w, | ||||
|       this.pixelsPerSubimage.h, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| html, body { | ||||
| html, | ||||
| body { | ||||
|   padding: 0; | ||||
|   margin: 0; | ||||
|   width: 100%; | ||||
| @@ -9,4 +10,4 @@ html, body { | ||||
|   image-rendering: pixelated; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import {getInput} from "./internal/input.ts"; | ||||
| import {getDrawing} from "./internal/drawing.ts"; | ||||
| import { getInput } from "./internal/input.ts"; | ||||
| import { getDrawing } from "./internal/drawing.ts"; | ||||
|  | ||||
| // input reexports | ||||
| export let I = getInput(); | ||||
|   | ||||
							
								
								
									
										46
									
								
								src/game.ts
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								src/game.ts
									
									
									
									
									
								
							| @@ -1,30 +1,32 @@ | ||||
| import {BG_OUTER} from "./colors.ts"; | ||||
| import {D, I} from "./engine/public.ts"; | ||||
| import {IGame, Point, Size} from "./engine/datatypes.ts"; | ||||
| import {getPageLocation, Page} from "./layout.ts"; | ||||
| import {getHotbar, Hotbar} from "./hotbar.ts"; | ||||
| import {getSkillsModal, SkillsModal} from "./skillsmodal.ts"; | ||||
| import {getSleepModal, SleepModal} from "./sleepmodal.ts"; | ||||
| import {getVNModal, VNModal} from "./vnmodal.ts"; | ||||
| import {Gameplay, getGameplay} from "./gameplay.ts"; | ||||
| import {getEndgameModal} from "./endgamemodal.ts"; | ||||
| import {CheckModal, getCheckModal} from "./checkmodal.ts"; | ||||
| import { BG_OUTER } from "./colors.ts"; | ||||
| import { D, I } from "./engine/public.ts"; | ||||
| import { IGame, Point, Size } from "./engine/datatypes.ts"; | ||||
| import { getPageLocation, Page } from "./layout.ts"; | ||||
| import { getHotbar, Hotbar } from "./hotbar.ts"; | ||||
| import { getSkillsModal, SkillsModal } from "./skillsmodal.ts"; | ||||
| import { getSleepModal, SleepModal } from "./sleepmodal.ts"; | ||||
| import { getVNModal, VNModal } from "./vnmodal.ts"; | ||||
| import { Gameplay, getGameplay } from "./gameplay.ts"; | ||||
| import { getEndgameModal } from "./endgamemodal.ts"; | ||||
| import { CheckModal, getCheckModal } from "./checkmodal.ts"; | ||||
|  | ||||
| class MenuCamera { | ||||
|   // measured in whole screens | ||||
|   position: Point; | ||||
|   target: Point; | ||||
|  | ||||
|   constructor({position, target}: {position: Point, target: Point}) { | ||||
|   constructor({ position, target }: { position: Point; target: Point }) { | ||||
|     this.position = position; | ||||
|     this.target = target; | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     let adjust = (x0: number, x1: number) => { | ||||
|       if (Math.abs(x1 - x0) < 0.01) { return x1; } | ||||
|       if (Math.abs(x1 - x0) < 0.01) { | ||||
|         return x1; | ||||
|       } | ||||
|       return (x0 * 8 + x1 * 2) / 10; | ||||
|     } | ||||
|     }; | ||||
|     this.position = new Point( | ||||
|       adjust(this.position.x, this.target.x), | ||||
|       adjust(this.position.y, this.target.y), | ||||
| @@ -49,14 +51,18 @@ export class Game implements IGame { | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     if (I.isKeyPressed("w")) { this.page = "Gameplay" } | ||||
|     if (I.isKeyPressed("s")) { this.page = "Thralls" } | ||||
|     if (I.isKeyPressed("w")) { | ||||
|       this.page = "Gameplay"; | ||||
|     } | ||||
|     if (I.isKeyPressed("s")) { | ||||
|       this.page = "Thralls"; | ||||
|     } | ||||
|  | ||||
|     this.camera.target = getPageLocation(this.page); | ||||
|     D.camera = new Point( | ||||
|       D.size.w * this.camera.position.x, | ||||
|       D.size.h * this.camera.position.y, | ||||
|     ) | ||||
|     ); | ||||
|     this.camera.update(); | ||||
|  | ||||
|     // state-specific updates | ||||
| @@ -76,7 +82,7 @@ export class Game implements IGame { | ||||
|     // mainFont.drawText({ctx: ctx, text: "You have been given a gift.", x: 0, y: 0}) | ||||
|     let mouse = I.mousePosition?.offset(D.camera); | ||||
|     if (mouse != null) { | ||||
|       D.invertRect(mouse.offset(new Point(-1, -1)), new Size(3, 3)) | ||||
|       D.invertRect(mouse.offset(new Point(-1, -1)), new Size(3, 3)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -95,7 +101,7 @@ export class Game implements IGame { | ||||
|     this.#mainThing?.draw(); | ||||
|  | ||||
|     if (!this.#mainThing?.blocksHud) { | ||||
|       this.#bottomThing?.draw() | ||||
|       this.#bottomThing?.draw(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -147,4 +153,4 @@ export class Game implements IGame { | ||||
|   } | ||||
| } | ||||
|  | ||||
| export let game = new Game(); | ||||
| export let game = new Game(); | ||||
|   | ||||
| @@ -1,20 +1,24 @@ | ||||
| import {withCamera} from "./layout.ts"; | ||||
| import {getHuntMode} from "./huntmode.ts"; | ||||
| import {getHud} from "./hud.ts"; | ||||
| import { withCamera } from "./layout.ts"; | ||||
| import { getHuntMode } from "./huntmode.ts"; | ||||
| import { getHud } from "./hud.ts"; | ||||
|  | ||||
| export class Gameplay { | ||||
|   update() { | ||||
|     withCamera("Gameplay", () => { | ||||
|       getHuntMode().update(); | ||||
|     }); | ||||
|     withCamera("HUD", () => { getHud().update() }) | ||||
|     withCamera("HUD", () => { | ||||
|       getHud().update(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     withCamera("Gameplay", () => { | ||||
|       getHuntMode().draw(); | ||||
|     }); | ||||
|     withCamera("HUD", () => { getHud().draw() }) | ||||
|     withCamera("HUD", () => { | ||||
|       getHud().draw(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   get blocksHud(): boolean { | ||||
| @@ -26,4 +30,3 @@ let active = new Gameplay(); | ||||
| export function getGameplay(): Gameplay { | ||||
|   return active; | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										113
									
								
								src/gridart.ts
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								src/gridart.ts
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| import {Color, Point, Rect, Size} from "./engine/datatypes.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| import { Color, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
|  | ||||
| export const FLOOR_CELL_SIZE: Size = new Size(48, 48) | ||||
| export const CEILING_CELL_SIZE: Size = new Size(56, 56) | ||||
| export const FLOOR_CELL_SIZE: Size = new Size(48, 48); | ||||
| export const CEILING_CELL_SIZE: Size = new Size(56, 56); | ||||
| export const HEIGHT_IN_FEET = 12; | ||||
| export const CENTER = new Point(192, 192); | ||||
| export const MOULDING_SZ = new Size(1, 1); | ||||
| @@ -22,14 +22,26 @@ export class GridArt { | ||||
|     this.#floorCenter = at.scale(FLOOR_CELL_SIZE).offset(CENTER); | ||||
|     this.#ceilingCenter = at.scale(CEILING_CELL_SIZE).offset(CENTER); | ||||
|  | ||||
|     this.#floorTl = at.offset(new Point(-0.5, -0.5)).scale(FLOOR_CELL_SIZE).offset(CENTER); | ||||
|     this.#ceilingTl = at.offset(new Point(-0.5, -0.5)).scale(CEILING_CELL_SIZE).offset(CENTER); | ||||
|     this.#floorBr = at.offset(new Point(0.5, 0.5)).scale(FLOOR_CELL_SIZE).offset(CENTER); | ||||
|     this.#ceilingBr = at.offset(new Point(0.5, 0.5)).scale(CEILING_CELL_SIZE).offset(CENTER); | ||||
|     this.#floorTl = at | ||||
|       .offset(new Point(-0.5, -0.5)) | ||||
|       .scale(FLOOR_CELL_SIZE) | ||||
|       .offset(CENTER); | ||||
|     this.#ceilingTl = at | ||||
|       .offset(new Point(-0.5, -0.5)) | ||||
|       .scale(CEILING_CELL_SIZE) | ||||
|       .offset(CENTER); | ||||
|     this.#floorBr = at | ||||
|       .offset(new Point(0.5, 0.5)) | ||||
|       .scale(FLOOR_CELL_SIZE) | ||||
|       .offset(CENTER); | ||||
|     this.#ceilingBr = at | ||||
|       .offset(new Point(0.5, 0.5)) | ||||
|       .scale(CEILING_CELL_SIZE) | ||||
|       .offset(CENTER); | ||||
|   } | ||||
|  | ||||
|   get floorRect(): Rect { | ||||
|     return new Rect(this.#floorTl, this.#floorBr.subtract(this.#floorTl)) | ||||
|     return new Rect(this.#floorTl, this.#floorBr.subtract(this.#floorTl)); | ||||
|   } | ||||
|  | ||||
|   drawFloor(color: Color) { | ||||
| @@ -40,7 +52,8 @@ export class GridArt { | ||||
|     let diff = Math.abs(this.#ceilingTl.y - this.#floorTl.y); | ||||
|     let sign = Math.sign(this.#ceilingTl.y - this.#floorTl.y); | ||||
|     // console.log(`diff, sign: ${diff}, ${sign}`) | ||||
|     for (let dy = 0; dy <= diff; dy += 0.25) {  // 0.25: fudge factor because we get two different lines | ||||
|     for (let dy = 0; dy <= diff; dy += 0.25) { | ||||
|       // 0.25: fudge factor because we get two different lines | ||||
|       let progress = dy / diff; | ||||
|       let x0 = Math.floor(lerp(progress, this.#floorTl.x, this.#ceilingTl.x)); | ||||
|       let x1 = Math.ceil(lerp(progress, this.#floorBr.x, this.#ceilingBr.x)); | ||||
| @@ -57,75 +70,106 @@ export class GridArt { | ||||
|     let diff = Math.abs(this.#ceilingTl.x - this.#floorTl.x); | ||||
|     let sign = Math.sign(this.#ceilingTl.x - this.#floorTl.x); | ||||
|     // console.log(`diff, sign: ${diff}, ${sign}`) | ||||
|     for (let dx = 0; dx <= diff; dx += 0.25) { // fudge factor because we get two different lines | ||||
|     for (let dx = 0; dx <= diff; dx += 0.25) { | ||||
|       // fudge factor because we get two different lines | ||||
|       let progress = dx / diff; | ||||
|       let y0 = Math.floor(lerp(progress, this.#floorTl.y, this.#ceilingTl.y)); | ||||
|       let y1 = Math.ceil(lerp(progress, this.#floorBr.y, this.#ceilingBr.y)); | ||||
|  | ||||
|       let x = this.#floorTl.x + sign * dx; | ||||
|  | ||||
|       D.fillRect(new Point(x, y0), new Size(1, y1-y0), color); | ||||
|       D.fillRect(new Point(x, y0), new Size(1, y1 - y0), color); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   drawWallTop(color: Color) { | ||||
|     if (this.#at.y > 0) { return; } | ||||
|     if (this.#at.y > 0) { | ||||
|       return; | ||||
|     } | ||||
|     this.#drawWallTop(color); | ||||
|   } | ||||
|  | ||||
|   drawWallLeft(color: Color) { | ||||
|     if (this.#at.x > 0) { return; } | ||||
|     if (this.#at.x > 0) { | ||||
|       return; | ||||
|     } | ||||
|     this.#drawWallLeft(color); | ||||
|   } | ||||
|  | ||||
|   drawWallBottom(color: Color) { | ||||
|     if (this.#at.y < 0) { return; } | ||||
|     if (this.#at.y < 0) { | ||||
|       return; | ||||
|     } | ||||
|     new GridArt(this.#at.offset(new Point(0, 1))).#drawWallTop(color); | ||||
|   } | ||||
|  | ||||
|   drawWallRight(color: Color) { | ||||
|     if (this.#at.x < 0) { return; } | ||||
|     if (this.#at.x < 0) { | ||||
|       return; | ||||
|     } | ||||
|     new GridArt(this.#at.offset(new Point(1, 0))).#drawWallLeft(color); | ||||
|   } | ||||
|  | ||||
|   drawMouldingTop(color: Color) { | ||||
|     let lhs = this.#ceilingTl.offset(new Point(0, -MOULDING_SZ.h)) | ||||
|     D.fillRect(lhs, new Size(CEILING_CELL_SIZE.w, MOULDING_SZ.h), color) | ||||
|     let lhs = this.#ceilingTl.offset(new Point(0, -MOULDING_SZ.h)); | ||||
|     D.fillRect(lhs, new Size(CEILING_CELL_SIZE.w, MOULDING_SZ.h), color); | ||||
|   } | ||||
|  | ||||
|   drawMouldingTopLeft(color: Color) { | ||||
|     D.fillRect(this.#ceilingTl.offset(new Point(-MOULDING_SZ.w, -MOULDING_SZ.h)), MOULDING_SZ, color); | ||||
|     D.fillRect( | ||||
|       this.#ceilingTl.offset(new Point(-MOULDING_SZ.w, -MOULDING_SZ.h)), | ||||
|       MOULDING_SZ, | ||||
|       color, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   drawMouldingLeft(color: Color) { | ||||
|     let lhs = this.#ceilingTl.offset(new Point(-MOULDING_SZ.w, 0)) | ||||
|     D.fillRect(lhs, new Size(MOULDING_SZ.w, CEILING_CELL_SIZE.h), color) | ||||
|     let lhs = this.#ceilingTl.offset(new Point(-MOULDING_SZ.w, 0)); | ||||
|     D.fillRect(lhs, new Size(MOULDING_SZ.w, CEILING_CELL_SIZE.h), color); | ||||
|   } | ||||
|  | ||||
|   drawMouldingTopRight(color: Color) { | ||||
|     D.fillRect(this.#ceilingTl.offset(new Point(CEILING_CELL_SIZE.w, -MOULDING_SZ.h)), MOULDING_SZ, color); | ||||
|     D.fillRect( | ||||
|       this.#ceilingTl.offset(new Point(CEILING_CELL_SIZE.w, -MOULDING_SZ.h)), | ||||
|       MOULDING_SZ, | ||||
|       color, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   drawMouldingBottom(color: Color) { | ||||
|     let lhs = this.#ceilingTl.offset(new Point(0, CEILING_CELL_SIZE.h)) | ||||
|     D.fillRect(lhs, new Size(CEILING_CELL_SIZE.w, MOULDING_SZ.h), color) | ||||
|     let lhs = this.#ceilingTl.offset(new Point(0, CEILING_CELL_SIZE.h)); | ||||
|     D.fillRect(lhs, new Size(CEILING_CELL_SIZE.w, MOULDING_SZ.h), color); | ||||
|   } | ||||
|  | ||||
|   drawMouldingBottomLeft(color: Color) { | ||||
|     D.fillRect(this.#ceilingTl.offset(new Point(-MOULDING_SZ.w, CEILING_CELL_SIZE.h)), MOULDING_SZ, color); | ||||
|     D.fillRect( | ||||
|       this.#ceilingTl.offset(new Point(-MOULDING_SZ.w, CEILING_CELL_SIZE.h)), | ||||
|       MOULDING_SZ, | ||||
|       color, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   drawMouldingRight(color: Color) { | ||||
|     let lhs = this.#ceilingTl.offset(new Point(CEILING_CELL_SIZE.w, 0)) | ||||
|     D.fillRect(lhs, new Size(MOULDING_SZ.w, CEILING_CELL_SIZE.h), color) | ||||
|     let lhs = this.#ceilingTl.offset(new Point(CEILING_CELL_SIZE.w, 0)); | ||||
|     D.fillRect(lhs, new Size(MOULDING_SZ.w, CEILING_CELL_SIZE.h), color); | ||||
|   } | ||||
|  | ||||
|   drawMouldingBottomRight(color: Color) { | ||||
|     D.fillRect(this.#ceilingTl.offset(new Point(CEILING_CELL_SIZE.w, CEILING_CELL_SIZE.h)), MOULDING_SZ, color); | ||||
|     D.fillRect( | ||||
|       this.#ceilingTl.offset( | ||||
|         new Point(CEILING_CELL_SIZE.w, CEILING_CELL_SIZE.h), | ||||
|       ), | ||||
|       MOULDING_SZ, | ||||
|       color, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   drawCeiling(color: Color) { | ||||
|     D.fillRect(this.#ceilingTl, this.#ceilingBr.subtract(this.#ceilingTl), color); | ||||
|     D.fillRect( | ||||
|       this.#ceilingTl, | ||||
|       this.#ceilingBr.subtract(this.#ceilingTl), | ||||
|       color, | ||||
|     ); | ||||
|     // D.drawRect(this.#ceilingTl, this.#ceilingBr.subtract(this.#ceilingTl), FG_BOLD); | ||||
|   } | ||||
|  | ||||
| @@ -139,8 +183,11 @@ export class GridArt { | ||||
| } | ||||
|  | ||||
| let lerp = (amt: number, x: number, y: number) => { | ||||
|   if (amt <= 0) { return x; } | ||||
|   if (amt >= 1) { return y; } | ||||
|   if (amt <= 0) { | ||||
|     return x; | ||||
|   } | ||||
|   if (amt >= 1) { | ||||
|     return y; | ||||
|   } | ||||
|   return x + (y - x) * amt; | ||||
| } | ||||
|  | ||||
| }; | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| import {Point, Rect, Size} from "./engine/datatypes.ts"; | ||||
| import {DrawPile} from "./drawpile.ts"; | ||||
| import {withCamera} from "./layout.ts"; | ||||
| import {getSkillsModal} from "./skillsmodal.ts"; | ||||
| import {addButton} from "./button.ts"; | ||||
| import {getSleepModal} from "./sleepmodal.ts"; | ||||
| import { Point, Rect, Size } from "./engine/datatypes.ts"; | ||||
| import { DrawPile } from "./drawpile.ts"; | ||||
| import { withCamera } from "./layout.ts"; | ||||
| import { getSkillsModal } from "./skillsmodal.ts"; | ||||
| import { addButton } from "./button.ts"; | ||||
| import { getSleepModal } from "./sleepmodal.ts"; | ||||
|  | ||||
| type Button = { | ||||
|   label: string, | ||||
|   cbClick: () => void, | ||||
| } | ||||
|   label: string; | ||||
|   cbClick: () => void; | ||||
| }; | ||||
|  | ||||
| export class Hotbar { | ||||
|   #drawpile: DrawPile; | ||||
| @@ -18,12 +18,12 @@ export class Hotbar { | ||||
|  | ||||
|   get #cellSize(): Size { | ||||
|     return new Size(96, 32); | ||||
| } | ||||
|   } | ||||
|  | ||||
|   get size(): Size { | ||||
|     let {w: cellW, h: cellH} = this.#cellSize; | ||||
|     let { w: cellW, h: cellH } = this.#cellSize; | ||||
|     let w = this.#computeButtons().length * cellW; | ||||
|     return new Size(w, cellH) | ||||
|     return new Size(w, cellH); | ||||
|   } | ||||
|  | ||||
|   #computeButtons(): Button[] { | ||||
| @@ -31,9 +31,9 @@ export class Hotbar { | ||||
|     buttons.push({ | ||||
|       label: "Skills", | ||||
|       cbClick: () => { | ||||
|         getSkillsModal().setShown(true) | ||||
|       } | ||||
|     }) | ||||
|         getSkillsModal().setShown(true); | ||||
|       }, | ||||
|     }); | ||||
|     /* | ||||
|     buttons.push({ | ||||
|       label: "Thralls" | ||||
| @@ -42,14 +42,14 @@ export class Hotbar { | ||||
|     buttons.push({ | ||||
|       label: "Sleep", | ||||
|       cbClick: () => { | ||||
|         getSleepModal().setShown(true) | ||||
|       } | ||||
|     }) | ||||
|         getSleepModal().setShown(true); | ||||
|       }, | ||||
|     }); | ||||
|     return buttons; | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     withCamera("Hotbar", () => this.#update()) | ||||
|     withCamera("Hotbar", () => this.#update()); | ||||
|   } | ||||
|  | ||||
|   #update() { | ||||
| @@ -61,11 +61,16 @@ export class Hotbar { | ||||
|  | ||||
|     let x = 0; | ||||
|     for (let b of buttons.values()) { | ||||
|       addButton(this.#drawpile, b.label, new Rect(new Point(x, 0), cellSize), true, b.cbClick); | ||||
|       addButton( | ||||
|         this.#drawpile, | ||||
|         b.label, | ||||
|         new Rect(new Point(x, 0), cellSize), | ||||
|         true, | ||||
|         b.cbClick, | ||||
|       ); | ||||
|       x += cellSize.w; | ||||
|     } | ||||
|     this.#drawpile.executeOnClick(); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
| @@ -77,4 +82,4 @@ export class Hotbar { | ||||
| let active = new Hotbar(); | ||||
| export function getHotbar(): Hotbar { | ||||
|   return active; | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										40
									
								
								src/hud.ts
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								src/hud.ts
									
									
									
									
									
								
							| @@ -1,35 +1,39 @@ | ||||
| import {D} from "./engine/public.ts"; | ||||
| import {Point, Size} from "./engine/datatypes.ts"; | ||||
| import {BG_OUTER, FG_BOLD, FG_TEXT} from "./colors.ts"; | ||||
| import {ALL_STATS} from "./datatypes.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
| import {getHuntMode} from "./huntmode.ts"; | ||||
| import {getStateManager} from "./statemanager.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
| import { Point, Size } from "./engine/datatypes.ts"; | ||||
| import { BG_OUTER, FG_BOLD, FG_TEXT } from "./colors.ts"; | ||||
| import { ALL_STATS } from "./datatypes.ts"; | ||||
| import { getPlayerProgress } from "./playerprogress.ts"; | ||||
| import { getHuntMode } from "./huntmode.ts"; | ||||
| import { getStateManager } from "./statemanager.ts"; | ||||
|  | ||||
| export class Hud { | ||||
|   get size(): Size { | ||||
|     return new Size(96, 176) | ||||
|     return new Size(96, 176); | ||||
|   } | ||||
|  | ||||
|   update() { } | ||||
|   update() {} | ||||
|  | ||||
|   draw() { | ||||
|     D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), BG_OUTER) | ||||
|     D.drawText(getPlayerProgress().name, new Point(0, 0), FG_BOLD) | ||||
|     D.drawText(`Level ${getHuntMode().getDepth()}`, new Point(0, 16), FG_TEXT) | ||||
|     D.drawText(`Turn ${getStateManager().getTurn()}/${getStateManager().getMaxTurns()}`, new Point(0, 32), FG_TEXT) | ||||
|     D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), BG_OUTER); | ||||
|     D.drawText(getPlayerProgress().name, new Point(0, 0), FG_BOLD); | ||||
|     D.drawText(`Level ${getHuntMode().getDepth()}`, new Point(0, 16), FG_TEXT); | ||||
|     D.drawText( | ||||
|       `Turn ${getStateManager().getTurn()}/${getStateManager().getMaxTurns()}`, | ||||
|       new Point(0, 32), | ||||
|       FG_TEXT, | ||||
|     ); | ||||
|  | ||||
|     let y = 64; | ||||
|     let prog = getPlayerProgress(); | ||||
|     for (let s of ALL_STATS.values()) { | ||||
|       D.drawText(`${s}`, new Point(0, y), FG_BOLD) | ||||
|       D.drawText(`${prog.getStat(s)}`, new Point(32, y), FG_TEXT) | ||||
|       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 > 0) { | ||||
|         D.drawText(`(+${talent})`, new Point(56, y), FG_TEXT) | ||||
|         D.drawText(`(+${talent})`, new Point(56, y), FG_TEXT); | ||||
|       } | ||||
|       if (talent < 0) { | ||||
|         D.drawText(`(${talent})`, new Point(56, y), FG_TEXT) | ||||
|         D.drawText(`(${talent})`, new Point(56, y), FG_TEXT); | ||||
|       } | ||||
|       y += 16; | ||||
|     } | ||||
| @@ -43,4 +47,4 @@ export class Hud { | ||||
| let active = new Hud(); | ||||
| export function getHud(): Hud { | ||||
|   return active; | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										184
									
								
								src/huntmode.ts
									
									
									
									
									
								
							
							
						
						
									
										184
									
								
								src/huntmode.ts
									
									
									
									
									
								
							| @@ -1,34 +1,33 @@ | ||||
| import {Point} from "./engine/datatypes.ts"; | ||||
| import {DrawPile} from "./drawpile.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| import {sprThrallLore} from "./sprites.ts"; | ||||
| import { Point } from "./engine/datatypes.ts"; | ||||
| import { DrawPile } from "./drawpile.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
| import { sprThrallLore } from "./sprites.ts"; | ||||
| import { | ||||
|   BG_INSET, | ||||
|   BG_WALL_OR_UNREVEALED, | ||||
|   FG_BOLD, | ||||
|   FG_MOULDING, | ||||
|   FG_TEXT | ||||
|   FG_TEXT, | ||||
| } from "./colors.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
| import {Architecture, LoadedNewMap} from "./newmap.ts"; | ||||
| import {FLOOR_CELL_SIZE, GridArt} from "./gridart.ts"; | ||||
| import {shadowcast} from "./shadowcast.ts"; | ||||
| import {getCheckModal} from "./checkmodal.ts"; | ||||
|  | ||||
| import { getPlayerProgress } from "./playerprogress.ts"; | ||||
| import { Architecture, LoadedNewMap } from "./newmap.ts"; | ||||
| import { FLOOR_CELL_SIZE, GridArt } from "./gridart.ts"; | ||||
| import { shadowcast } from "./shadowcast.ts"; | ||||
| import { getCheckModal } from "./checkmodal.ts"; | ||||
|  | ||||
| export class HuntMode { | ||||
|   map: LoadedNewMap | ||||
|   player: Point | ||||
|   faceLeft: boolean | ||||
|   map: LoadedNewMap; | ||||
|   player: Point; | ||||
|   faceLeft: boolean; | ||||
|  | ||||
|   drawpile: DrawPile | ||||
|   frame: number | ||||
|   depth: number | ||||
|   drawpile: DrawPile; | ||||
|   frame: number; | ||||
|   depth: number; | ||||
|  | ||||
|   constructor(depth: number, map: LoadedNewMap) { | ||||
|     this.map = map; | ||||
|     this.player = map.entrance; | ||||
|     this.faceLeft = false | ||||
|     this.faceLeft = false; | ||||
|  | ||||
|     this.drawpile = new DrawPile(); | ||||
|     this.frame = 0; | ||||
| @@ -46,7 +45,9 @@ export class HuntMode { | ||||
|     let cell = this.map.get(this.player); | ||||
|  | ||||
|     let pickup = cell.pickup; | ||||
|     if (pickup != null) { cell.pickup = null; } | ||||
|     if (pickup != null) { | ||||
|       cell.pickup = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   #computeCostToClick(mapPosition: Point): number | null { | ||||
| @@ -58,22 +59,30 @@ export class HuntMode { | ||||
|  | ||||
|     let dist = Math.max( | ||||
|       Math.abs(mapPosition.x - this.player.x), | ||||
|       Math.abs(mapPosition.y - this.player.y) | ||||
|       Math.abs(mapPosition.y - this.player.y), | ||||
|     ); | ||||
|  | ||||
|     if (dist != 1) { return null; } | ||||
|     if (dist != 1) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     let pickup = present.pickup; | ||||
|     if (pickup == null) { return 10; } | ||||
|     return pickup.computeCostToClick() | ||||
|     if (pickup == null) { | ||||
|       return 10; | ||||
|     } | ||||
|     return pickup.computeCostToClick(); | ||||
|   } | ||||
|  | ||||
|   movePlayerTo(newPosition: Point) { | ||||
|     let oldX = this.player.x; | ||||
|     let newX = newPosition.x; | ||||
|     this.player = newPosition; | ||||
|     if (newX < oldX) { this.faceLeft = true; } | ||||
|     if (oldX < newX) { this.faceLeft = false; } | ||||
|     if (newX < oldX) { | ||||
|       this.faceLeft = true; | ||||
|     } | ||||
|     if (oldX < newX) { | ||||
|       this.faceLeft = false; | ||||
|     } | ||||
|     this.#collectResources(); | ||||
|   } | ||||
|  | ||||
| @@ -82,10 +91,10 @@ export class HuntMode { | ||||
|     this.frame += 1; | ||||
|     this.drawpile.clear(); | ||||
|  | ||||
|     let globalOffset = | ||||
|       new Point(this.player.x * FLOOR_CELL_SIZE.w, this.player.y * FLOOR_CELL_SIZE.h).offset( | ||||
|         new Point(-192, -192) | ||||
|       ) | ||||
|     let globalOffset = new Point( | ||||
|       this.player.x * FLOOR_CELL_SIZE.w, | ||||
|       this.player.y * FLOOR_CELL_SIZE.h, | ||||
|     ).offset(new Point(-192, -192)); | ||||
|  | ||||
|     this.#updateFov(); | ||||
|  | ||||
| @@ -113,25 +122,27 @@ export class HuntMode { | ||||
|       ([x, y]: [number, number]): boolean => { | ||||
|         let cell = this.map.get(new Point(x, y)); | ||||
|         let pickup = cell.pickup; | ||||
|         return cell.architecture == Architecture.Wall || (pickup != null && pickup.isObstructive()); | ||||
|         return ( | ||||
|           cell.architecture == Architecture.Wall || | ||||
|           (pickup != null && pickup.isObstructive()) | ||||
|         ); | ||||
|       }, | ||||
|       ([x, y]: [number, number]) => { | ||||
|         let dx = x - this.player.x; | ||||
|         let dy = y - this.player.y; | ||||
|         if ((dx * dx + dy * dy) >= 13) { return; } | ||||
|         if (dx * dx + dy * dy >= 13) { | ||||
|           return; | ||||
|         } | ||||
|         this.map.get(new Point(x, y)).revealed = true; | ||||
|       } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     this.drawpile.draw() | ||||
|     this.drawpile.draw(); | ||||
|   } | ||||
|  | ||||
|   #drawMapCell( | ||||
|     offsetInCells: Point, | ||||
|     mapPosition: Point, | ||||
|   ) { | ||||
|   #drawMapCell(offsetInCells: Point, mapPosition: Point) { | ||||
|     const OFFSET_UNDER_FLOOR = -512 + mapPosition.y; | ||||
|     const OFFSET_FLOOR = -256 + mapPosition.y; | ||||
|     const OFFSET_AIR = 0 + mapPosition.y; | ||||
| @@ -140,21 +151,15 @@ export class HuntMode { | ||||
|  | ||||
|     const gridArt = new GridArt(offsetInCells); | ||||
|  | ||||
|     let cellData = this.map.get(mapPosition) | ||||
|     let cellData = this.map.get(mapPosition); | ||||
|  | ||||
|     this.drawpile.add( | ||||
|       OFFSET_UNDER_FLOOR, | ||||
|       () => { | ||||
|         gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); | ||||
|       } | ||||
|     ); | ||||
|     this.drawpile.add(OFFSET_UNDER_FLOOR, () => { | ||||
|       gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); | ||||
|     }); | ||||
|     if (cellData.architecture == Architecture.Wall || !cellData.revealed) { | ||||
|       this.drawpile.add( | ||||
|         OFFSET_TOP, | ||||
|         () => { | ||||
|           gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); | ||||
|         } | ||||
|       ); | ||||
|       this.drawpile.add(OFFSET_TOP, () => { | ||||
|         gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
| @@ -169,7 +174,7 @@ export class HuntMode { | ||||
|     this.drawpile.addClickable( | ||||
|       OFFSET_FLOOR, | ||||
|       (hover: boolean) => { | ||||
|         gridArt.drawFloor(hover ? FG_TEXT : BG_INSET) | ||||
|         gridArt.drawFloor(hover ? FG_TEXT : BG_INSET); | ||||
|         pickup?.drawFloor(gridArt); | ||||
|       }, | ||||
|       gridArt.floorRect, | ||||
| @@ -181,65 +186,86 @@ export class HuntMode { | ||||
|  | ||||
|         if (cost != null) { | ||||
|           getPlayerProgress().spendBlood(cost); | ||||
|           this.movePlayerTo(mapPosition) | ||||
|           this.movePlayerTo(mapPosition); | ||||
|           getCheckModal().show(null, null); | ||||
|         } | ||||
|       } | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
|     if (pickup != null) { | ||||
|       this.drawpile.add(OFFSET_AIR, () => { pickup.drawInAir(gridArt); }); | ||||
|       this.drawpile.add(OFFSET_AIR, () => { | ||||
|         pickup.drawInAir(gridArt); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     const isRevealedBlock = (dx: number, dy: number) => { | ||||
|       let other = this.map.get(mapPosition.offset(new Point(dx, dy))); | ||||
|       return other.revealed && other.architecture == Architecture.Wall; | ||||
|  | ||||
|     } | ||||
|     }; | ||||
|     if (isRevealedBlock(0, -1) && isRevealedBlock(-1, 0)) { | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingTopLeft(FG_MOULDING); }) | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||
|         gridArt.drawMouldingTopLeft(FG_MOULDING); | ||||
|       }); | ||||
|     } | ||||
|     if (isRevealedBlock(0, -1)) { | ||||
|       this.drawpile.add(OFFSET_AIR, () => { gridArt.drawWallTop(FG_TEXT); }) | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingTop(FG_MOULDING); }) | ||||
|       this.drawpile.add(OFFSET_AIR, () => { | ||||
|         gridArt.drawWallTop(FG_TEXT); | ||||
|       }); | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||
|         gridArt.drawMouldingTop(FG_MOULDING); | ||||
|       }); | ||||
|     } | ||||
|     if (isRevealedBlock(0, -1) && isRevealedBlock(1, 0)) { | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingTopRight(FG_MOULDING); }) | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||
|         gridArt.drawMouldingTopRight(FG_MOULDING); | ||||
|       }); | ||||
|     } | ||||
|     if (isRevealedBlock(-1, 0)) { | ||||
|       this.drawpile.add(OFFSET_AIR, () => { gridArt.drawWallLeft(FG_TEXT); }) | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingLeft(FG_MOULDING); }) | ||||
|       this.drawpile.add(OFFSET_AIR, () => { | ||||
|         gridArt.drawWallLeft(FG_TEXT); | ||||
|       }); | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||
|         gridArt.drawMouldingLeft(FG_MOULDING); | ||||
|       }); | ||||
|     } | ||||
|     if (isRevealedBlock(0, 1) && isRevealedBlock(-1, 0)) { | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingBottomLeft(FG_MOULDING); }) | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||
|         gridArt.drawMouldingBottomLeft(FG_MOULDING); | ||||
|       }); | ||||
|     } | ||||
|     if (isRevealedBlock(0, 1)) { | ||||
|       this.drawpile.add(OFFSET_AIR, () => { gridArt.drawWallBottom(FG_BOLD); }) | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingBottom(FG_MOULDING); }) | ||||
|       this.drawpile.add(OFFSET_AIR, () => { | ||||
|         gridArt.drawWallBottom(FG_BOLD); | ||||
|       }); | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||
|         gridArt.drawMouldingBottom(FG_MOULDING); | ||||
|       }); | ||||
|     } | ||||
|     if (isRevealedBlock(0, 1) && isRevealedBlock(1, 0)) { | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingBottomRight(FG_MOULDING); }) | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||
|         gridArt.drawMouldingBottomRight(FG_MOULDING); | ||||
|       }); | ||||
|     } | ||||
|     if (isRevealedBlock(1, 0)) { | ||||
|       this.drawpile.add(OFFSET_AIR, () => { gridArt.drawWallRight(FG_BOLD); }) | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingRight(FG_MOULDING); }) | ||||
|       this.drawpile.add(OFFSET_AIR, () => { | ||||
|         gridArt.drawWallRight(FG_BOLD); | ||||
|       }); | ||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||
|         gridArt.drawMouldingRight(FG_MOULDING); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   #drawPlayer(globalOffset: Point) { | ||||
|     let cellOffset = new Point( | ||||
|       this.player.x * FLOOR_CELL_SIZE.w, | ||||
|       this.player.y * FLOOR_CELL_SIZE.h | ||||
|     ).offset(globalOffset.negate()) | ||||
|       this.player.y * FLOOR_CELL_SIZE.h, | ||||
|     ).offset(globalOffset.negate()); | ||||
|     this.drawpile.add(this.player.y, () => { | ||||
|       D.drawSprite( | ||||
|         sprThrallLore, | ||||
|         cellOffset, | ||||
|         1, { | ||||
|           xScale: this.faceLeft ? -2 : 2, | ||||
|           yScale: 2 | ||||
|         } | ||||
|       ) | ||||
|       D.drawSprite(sprThrallLore, cellOffset, 1, { | ||||
|         xScale: this.faceLeft ? -2 : 2, | ||||
|         yScale: 2, | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| @@ -251,7 +277,7 @@ export function initHuntMode(huntMode: HuntMode) { | ||||
|  | ||||
| export function getHuntMode() { | ||||
|   if (active == null) { | ||||
|     throw new Error(`trying to get hunt mode before it has been initialized`) | ||||
|     throw new Error(`trying to get hunt mode before it has been initialized`); | ||||
|   } | ||||
|   return active; | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| import {AlignX, AlignY, Point, Rect, Size} from "./engine/datatypes.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| import {getHud} from "./hud.ts"; | ||||
| import {getHotbar} from "./hotbar.ts"; | ||||
| import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
| import { getHud } from "./hud.ts"; | ||||
| import { getHotbar } from "./hotbar.ts"; | ||||
|  | ||||
| // general | ||||
| let margin = 8; | ||||
| export function getLayoutRect( | ||||
|   size: Size, | ||||
|   options?: {alignX?: AlignX, alignY?: AlignY} | ||||
|   options?: { alignX?: AlignX; alignY?: AlignY }, | ||||
| ): Rect { | ||||
|   let {w: screenW, h: screenH} = D.size; | ||||
|   let { w: screenW, h: screenH } = D.size; | ||||
|  | ||||
|   // first of all: place the _internal_ screen inside the real screen | ||||
|   let marginalScreenW = screenW - margin * 2; | ||||
| @@ -20,44 +20,53 @@ export function getLayoutRect( | ||||
|   // NOTE: If the screen is too small, remainingSpace will be negative | ||||
|   // This is fine -- it actually results in reasonable outcomes except | ||||
|   // that the size of the box is exceeded in the opposite of the align direction. | ||||
|   let {w: innerW, h: innerH} = size; | ||||
|   let { w: innerW, h: innerH } = size; | ||||
|   let remainingSpaceX = marginalScreenW - innerW; | ||||
|   let remainingSpaceY = marginalScreenH - innerH; | ||||
|  | ||||
|   let alignXCoef = | ||||
|     options?.alignX == AlignX.Left ? 0.0 : | ||||
|     options?.alignX == AlignX.Center ? 0.5 : | ||||
|     options?.alignX == AlignX.Right ? 1.0 : | ||||
|     0.5; | ||||
|     options?.alignX == AlignX.Left | ||||
|       ? 0.0 | ||||
|       : options?.alignX == AlignX.Center | ||||
|         ? 0.5 | ||||
|         : options?.alignX == AlignX.Right | ||||
|           ? 1.0 | ||||
|           : 0.5; | ||||
|   let alignYCoef = | ||||
|     options?.alignY == AlignY.Top ? 0.0 : | ||||
|     options?.alignY == AlignY.Middle ? 0.5 : | ||||
|     options?.alignY == AlignY.Bottom ? 1.0 : | ||||
|     0.5; | ||||
|     options?.alignY == AlignY.Top | ||||
|       ? 0.0 | ||||
|       : options?.alignY == AlignY.Middle | ||||
|         ? 0.5 | ||||
|         : options?.alignY == AlignY.Bottom | ||||
|           ? 1.0 | ||||
|           : 0.5; | ||||
|  | ||||
|   let x = marginalScreenX + alignXCoef * remainingSpaceX; | ||||
|   let y = marginalScreenY + alignYCoef * remainingSpaceY; | ||||
|  | ||||
|   return new Rect( | ||||
|     new Point(Math.floor(x), Math.floor(y)), | ||||
|     size | ||||
|   ) | ||||
|   return new Rect(new Point(Math.floor(x), Math.floor(y)), size); | ||||
| } | ||||
|  | ||||
| export function withCamera(part: UIPart, cb: () => void) { | ||||
|   let region = getPartLocation(part); | ||||
|  | ||||
|   D.withCamera(D.camera.offset(region.top.negate()), cb) | ||||
|   D.withCamera(D.camera.offset(region.top.negate()), cb); | ||||
| } | ||||
|  | ||||
| // specific | ||||
| export type Page = "Gameplay" | "Thralls"; | ||||
| export type UIPart = "BottomModal" | "FullscreenPopover" | "Hotbar" | "HUD" | "Gameplay" | "Thralls"; | ||||
| export type UIPart = | ||||
|   | "BottomModal" | ||||
|   | "FullscreenPopover" | ||||
|   | "Hotbar" | ||||
|   | "HUD" | ||||
|   | "Gameplay" | ||||
|   | "Thralls"; | ||||
|  | ||||
| export function getPartPage(part: UIPart): Page | null { | ||||
|   switch (part) { | ||||
|     case "FullscreenPopover": | ||||
|       return null | ||||
|       return null; | ||||
|     case "BottomModal": | ||||
|     case "Hotbar": | ||||
|     case "HUD": | ||||
| @@ -67,7 +76,7 @@ export function getPartPage(part: UIPart): Page | null { | ||||
|       return "Thralls"; | ||||
|   } | ||||
|  | ||||
|   throw `invalid part: ${part}` | ||||
|   throw `invalid part: ${part}`; | ||||
| } | ||||
|  | ||||
| export function getPageLocation(page: Page): Point { | ||||
| @@ -79,12 +88,12 @@ export function getPageLocation(page: Page): Point { | ||||
|       return new Point(0, 1); | ||||
|   } | ||||
|  | ||||
|   throw `invalid page: ${page}` | ||||
|   throw `invalid page: ${page}`; | ||||
| } | ||||
|  | ||||
| export function getPartLocation(part: UIPart): Rect { | ||||
|   // TODO: in pixels, not screens | ||||
|   let {w: screenW, h: screenH} = D.size; | ||||
|   let { w: screenW, h: screenH } = D.size; | ||||
|   let page = getPartPage(part); | ||||
|   let pageOffset = page ? getPageLocation(page) : null; | ||||
|   let layoutRect = internalGetPartLayoutRect(part); | ||||
| @@ -94,11 +103,9 @@ export function getPartLocation(part: UIPart): Rect { | ||||
|     return layoutRect.offset(D.camera); | ||||
|   } | ||||
|  | ||||
|   return layoutRect.offset(new Point( | ||||
|     pageOffset.x * screenW, | ||||
|     pageOffset.y * screenH | ||||
|   )); | ||||
|  | ||||
|   return layoutRect.offset( | ||||
|     new Point(pageOffset.x * screenW, pageOffset.y * screenH), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export function internalGetPartLayoutRect(part: UIPart) { | ||||
| @@ -117,12 +124,12 @@ export function internalGetPartLayoutRect(part: UIPart) { | ||||
|       return getLayoutRect(getHotbar().size, { | ||||
|         alignX: AlignX.Center, | ||||
|         alignY: AlignY.Bottom, | ||||
|       }) | ||||
|       }); | ||||
|     case "HUD": | ||||
|       return getLayoutRect(getHud().size, { | ||||
|         alignX: AlignX.Left, | ||||
|         alignY: AlignY.Top | ||||
|       }) | ||||
|         alignY: AlignY.Top, | ||||
|       }); | ||||
|   } | ||||
|   throw `not sure what layout rect to use ${part}` | ||||
| } | ||||
|   throw `not sure what layout rect to use ${part}`; | ||||
| } | ||||
|   | ||||
							
								
								
									
										31
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								src/main.ts
									
									
									
									
									
								
							| @@ -1,15 +1,18 @@ | ||||
| import {hostGame} from "./engine/internal/host.ts"; | ||||
| import {game} from "./game.ts"; | ||||
| import {getStateManager} from "./statemanager.ts"; | ||||
| import { hostGame } from "./engine/internal/host.ts"; | ||||
| import { game } from "./game.ts"; | ||||
| import { getStateManager } from "./statemanager.ts"; | ||||
|  | ||||
| getStateManager().startGame({ | ||||
|   name: "Pyrex", | ||||
|   title: "", | ||||
|   note: null, | ||||
|   stats: {AGI: 10, INT: 10, CHA: 10, PSI: 10}, | ||||
|   talents: {AGI: 0, INT: 0, CHA: 0, PSI: 0}, | ||||
|   skills: [], | ||||
|   isCompulsory: false, | ||||
|   inPenance: false, | ||||
| }, null); | ||||
| hostGame(game); | ||||
| getStateManager().startGame( | ||||
|   { | ||||
|     name: "Pyrex", | ||||
|     title: "", | ||||
|     note: null, | ||||
|     stats: { AGI: 10, INT: 10, CHA: 10, PSI: 10 }, | ||||
|     talents: { AGI: 0, INT: 0, CHA: 0, PSI: 0 }, | ||||
|     skills: [], | ||||
|     isCompulsory: false, | ||||
|     inPenance: false, | ||||
|   }, | ||||
|   null, | ||||
| ); | ||||
| hostGame(game); | ||||
|   | ||||
| @@ -1,8 +1,12 @@ | ||||
| import {Architecture, LoadedNewMap} from "./newmap.ts"; | ||||
| import {Grid, Point} from "./engine/datatypes.ts"; | ||||
| import {getThralls} from "./thralls.ts"; | ||||
| import {LadderPickup, ThrallPosterPickup, ThrallRecruitedPickup} from "./pickups.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
| import { Architecture, LoadedNewMap } from "./newmap.ts"; | ||||
| import { Grid, Point } from "./engine/datatypes.ts"; | ||||
| import { getThralls } from "./thralls.ts"; | ||||
| import { | ||||
|   LadderPickup, | ||||
|   ThrallPosterPickup, | ||||
|   ThrallRecruitedPickup, | ||||
| } from "./pickups.ts"; | ||||
| import { getPlayerProgress } from "./playerprogress.ts"; | ||||
|  | ||||
| const BASIC_PLAN = Grid.createGridFromMultilineString(` | ||||
| ##################### | ||||
| @@ -43,25 +47,58 @@ export function generateManor(): LoadedNewMap { | ||||
|       }; | ||||
|  | ||||
|       switch (BASIC_PLAN.get(xy)) { | ||||
|         case '#': break | ||||
|         case '@': cell.architecture = Architecture.Floor; map.entrance = xy; break; | ||||
|         case 'L': cell.architecture = Architecture.Floor; cell.pickup = new LadderPickup(); break; | ||||
|         case ' ': cell.architecture = Architecture.Floor; break; | ||||
|         case 'a': placeThrall(0); break; | ||||
|         case 'b': placeThrall(1); break; | ||||
|         case 'c': placeThrall(2); break; | ||||
|         case 'd': placeThrall(3); break; | ||||
|         case 'e': placeThrall(4); break; | ||||
|         case 'f': placeThrall(5); break; | ||||
|         case 'A': placeThrallPoster(0); break; | ||||
|         case 'B': placeThrallPoster(1); break; | ||||
|         case 'C': placeThrallPoster(2); break; | ||||
|         case 'D': placeThrallPoster(3); break; | ||||
|         case 'E': placeThrallPoster(4); break; | ||||
|         case 'F': placeThrallPoster(5); break; | ||||
|         case "#": | ||||
|           break; | ||||
|         case "@": | ||||
|           cell.architecture = Architecture.Floor; | ||||
|           map.entrance = xy; | ||||
|           break; | ||||
|         case "L": | ||||
|           cell.architecture = Architecture.Floor; | ||||
|           cell.pickup = new LadderPickup(); | ||||
|           break; | ||||
|         case " ": | ||||
|           cell.architecture = Architecture.Floor; | ||||
|           break; | ||||
|         case "a": | ||||
|           placeThrall(0); | ||||
|           break; | ||||
|         case "b": | ||||
|           placeThrall(1); | ||||
|           break; | ||||
|         case "c": | ||||
|           placeThrall(2); | ||||
|           break; | ||||
|         case "d": | ||||
|           placeThrall(3); | ||||
|           break; | ||||
|         case "e": | ||||
|           placeThrall(4); | ||||
|           break; | ||||
|         case "f": | ||||
|           placeThrall(5); | ||||
|           break; | ||||
|         case "A": | ||||
|           placeThrallPoster(0); | ||||
|           break; | ||||
|         case "B": | ||||
|           placeThrallPoster(1); | ||||
|           break; | ||||
|         case "C": | ||||
|           placeThrallPoster(2); | ||||
|           break; | ||||
|         case "D": | ||||
|           placeThrallPoster(3); | ||||
|           break; | ||||
|         case "E": | ||||
|           placeThrallPoster(4); | ||||
|           break; | ||||
|         case "F": | ||||
|           placeThrallPoster(5); | ||||
|           break; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return map; | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										191
									
								
								src/mapgen.ts
									
									
									
									
									
								
							
							
						
						
									
										191
									
								
								src/mapgen.ts
									
									
									
									
									
								
							| @@ -1,10 +1,16 @@ | ||||
| import {Architecture, LoadedNewMap} from "./newmap.ts"; | ||||
| import {Grid, Point, Rect, Size} from "./engine/datatypes.ts"; | ||||
| 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"; | ||||
| import { Architecture, LoadedNewMap } from "./newmap.ts"; | ||||
| import { Grid, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||
| 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; | ||||
| @@ -14,7 +20,7 @@ const MAX_VAULTS = 1; | ||||
| const NUM_VAULT_TRIES = 90; | ||||
| const NUM_ROOM_TRIES = 90; | ||||
| const NUM_STAIRCASE_TRIES = 90; | ||||
| const NUM_STAIRCASES_DESIRED = 3 | ||||
| const NUM_STAIRCASES_DESIRED = 3; | ||||
| const NUM_ROOMS_DESIRED = 0; // 4; | ||||
|  | ||||
| const EXTRA_CONNECTOR_CHANCE = 0.15; | ||||
| @@ -23,15 +29,19 @@ const WINDING_PERCENT = 0; | ||||
| // This is an implementation of Nystrom's algorithm: | ||||
| // https://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/ | ||||
| class Knife { | ||||
|   #map: LoadedNewMap | ||||
|   #region: number | ||||
|   #regions: Grid<number | null> | ||||
|   #sealedWalls: Grid<boolean> | ||||
|   #map: LoadedNewMap; | ||||
|   #region: number; | ||||
|   #regions: Grid<number | null>; | ||||
|   #sealedWalls: Grid<boolean>; | ||||
|  | ||||
|   constructor(map: LoadedNewMap, regions: Grid<number | null>, sealedWalls: Grid<boolean>) { | ||||
|   constructor( | ||||
|     map: LoadedNewMap, | ||||
|     regions: Grid<number | null>, | ||||
|     sealedWalls: Grid<boolean>, | ||||
|   ) { | ||||
|     this.#map = map; | ||||
|     this.#region = -1; | ||||
|     this.#regions = regions | ||||
|     this.#regions = regions; | ||||
|     this.#sealedWalls = sealedWalls; | ||||
|   } | ||||
|  | ||||
| @@ -51,10 +61,12 @@ class Knife { | ||||
|     return this.#sealedWalls; | ||||
|   } | ||||
|  | ||||
|   startRegion() { this.#region += 1; } | ||||
|   startRegion() { | ||||
|     this.#region += 1; | ||||
|   } | ||||
|  | ||||
|   carve(point: Point) { | ||||
|     this.#regions.set(point, this.#region) | ||||
|     this.#regions.set(point, this.#region); | ||||
|     this.map.get(point).architecture = Architecture.Floor; | ||||
|   } | ||||
|  | ||||
| @@ -68,7 +80,7 @@ class Knife { | ||||
|     if (protect ?? false) { | ||||
|       for (let y = room.top.y - 1; y < room.top.y + room.size.h + 1; y++) { | ||||
|         for (let x = room.top.x - 1; x < room.top.x + room.size.w + 1; x++) { | ||||
|           this.#sealedWalls.set(new Point(x, y), true) | ||||
|           this.#sealedWalls.set(new Point(x, y), true); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @@ -76,9 +88,9 @@ class Knife { | ||||
| } | ||||
|  | ||||
| export function generateMap(): LoadedNewMap { | ||||
|   for (let i= 0; i < 1000; i++) { | ||||
|   for (let i = 0; i < 1000; i++) { | ||||
|     try { | ||||
|       return tryGenerateMap(standardVaultTemplates) | ||||
|       return tryGenerateMap(standardVaultTemplates); | ||||
|     } catch (e) { | ||||
|       if (e instanceof TryAgainException) { | ||||
|         continue; | ||||
| @@ -86,12 +98,14 @@ export function generateMap(): LoadedNewMap { | ||||
|       throw e; | ||||
|     } | ||||
|   } | ||||
|   throw new Error("couldn't generate map in 1000 attempts") | ||||
|   throw new Error("couldn't generate map in 1000 attempts"); | ||||
| } | ||||
| export function tryGenerateMap(vaultTemplates: VaultTemplate[]): LoadedNewMap { | ||||
|   let width = WIDTH; | ||||
|   let height = HEIGHT; | ||||
|   if (width % 2 == 0 || height % 2 == 0) { throw "must be odd-sized"; } | ||||
|   if (width % 2 == 0 || height % 2 == 0) { | ||||
|     throw "must be odd-sized"; | ||||
|   } | ||||
|  | ||||
|   let grid = new LoadedNewMap("generated", new Size(width, height)); | ||||
|  | ||||
| @@ -125,14 +139,13 @@ export function tryGenerateMap(vaultTemplates: VaultTemplate[]): LoadedNewMap { | ||||
|   return grid; | ||||
| } | ||||
|  | ||||
|  | ||||
| class RoomChain { | ||||
|   #size: Size; | ||||
|   rooms: Rect[]; | ||||
|  | ||||
|   constructor(size: Size) { | ||||
|     this.#size = size; | ||||
|     this.rooms = [] | ||||
|     this.rooms = []; | ||||
|   } | ||||
|  | ||||
|   reserve(width: number, height: number): Rect | null { | ||||
| @@ -148,24 +161,32 @@ class RoomChain { | ||||
|     } | ||||
|  | ||||
|     this.rooms.push(room); | ||||
|     return room | ||||
|     return room; | ||||
|   } | ||||
| } | ||||
|  | ||||
| function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | ||||
|   vaultTemplates = [...vaultTemplates];  // so we can mutate it | ||||
|   vaultTemplates = [...vaultTemplates]; // so we can mutate it | ||||
|   shuffle(vaultTemplates); | ||||
|   let chain = new RoomChain(knife.map.size); | ||||
|   let nVaults = 0; | ||||
|   let nVaultsDesired = randrange(MIN_VAULTS, MAX_VAULTS + 1); | ||||
|  | ||||
|   for (let i = 0; vaultTemplates.length > 0 && nVaults < nVaultsDesired && i < NUM_VAULT_TRIES; i += 1) { | ||||
|   for ( | ||||
|     let i = 0; | ||||
|     vaultTemplates.length > 0 && | ||||
|     nVaults < nVaultsDesired && | ||||
|     i < NUM_VAULT_TRIES; | ||||
|     i += 1 | ||||
|   ) { | ||||
|     let width = 7; | ||||
|     let height = 7; | ||||
|  | ||||
|     let room = chain.reserve(width, height); | ||||
|  | ||||
|     if (!room) { continue; } | ||||
|     if (!room) { | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     nVaults += 1; | ||||
|     carveVault(knife, room, vaultTemplates.pop()!); | ||||
| @@ -174,12 +195,18 @@ function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | ||||
|   // staircases | ||||
|   let nStaircases = 0; | ||||
|   let nStaircasesDesired = NUM_STAIRCASES_DESIRED; | ||||
|   for (let i = 0; nStaircases < nStaircasesDesired && i < NUM_STAIRCASE_TRIES; i += 1) { | ||||
|   for ( | ||||
|     let i = 0; | ||||
|     nStaircases < nStaircasesDesired && i < NUM_STAIRCASE_TRIES; | ||||
|     i += 1 | ||||
|   ) { | ||||
|     let width = 3; | ||||
|     let height = 3; | ||||
|  | ||||
|     let room = chain.reserve(width, height); | ||||
|     if (!room) { continue; } | ||||
|     if (!room) { | ||||
|       continue; | ||||
|     } | ||||
|     nStaircases += 1; | ||||
|     carveStaircase(knife, room, nStaircases - 1); | ||||
|   } | ||||
| @@ -192,11 +219,16 @@ function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | ||||
|   let nRooms = 0; | ||||
|   let nRoomsDesired = NUM_ROOMS_DESIRED; | ||||
|   for (let i = 0; nRooms < nRoomsDesired && i < NUM_ROOM_TRIES; i += 1) { | ||||
|     let [width, height] = choose([[3, 5], [5, 3]]) | ||||
|     let [width, height] = choose([ | ||||
|       [3, 5], | ||||
|       [5, 3], | ||||
|     ]); | ||||
|  | ||||
|     let room = chain.reserve(width, height); | ||||
|  | ||||
|     if (!room) { continue; } | ||||
|     if (!room) { | ||||
|       continue; | ||||
|     } | ||||
|     nRooms += 1; | ||||
|  | ||||
|     carveRoom(knife, room); | ||||
| @@ -206,13 +238,13 @@ function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | ||||
|  | ||||
| function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | ||||
|   if (room.size.w != 7 || room.size.h != 7) { | ||||
|     throw new Error("room must be 7x7") | ||||
|     throw new Error("room must be 7x7"); | ||||
|   } | ||||
|  | ||||
|   let quad0 = new Rect(room.top, new Size(3, 3)) | ||||
|   let quad1 = new Rect(room.top.offset(new Point(4, 0)), new Size(3, 3)) | ||||
|   let quad2 = new Rect(room.top.offset(new Point(4, 4)), new Size(3, 3)) | ||||
|   let quad3 = new Rect(room.top.offset(new Point(0, 4)), new Size(3, 3)) | ||||
|   let quad0 = new Rect(room.top, new Size(3, 3)); | ||||
|   let quad1 = new Rect(room.top.offset(new Point(4, 0)), new Size(3, 3)); | ||||
|   let quad2 = new Rect(room.top.offset(new Point(4, 4)), new Size(3, 3)); | ||||
|   let quad3 = new Rect(room.top.offset(new Point(0, 4)), new Size(3, 3)); | ||||
|  | ||||
|   let [a, b, c, d] = choose([ | ||||
|     [quad0, quad1, quad2, quad3], | ||||
| @@ -267,7 +299,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | ||||
|     new Point(3, 1), | ||||
|     new Point(5, 3), | ||||
|     new Point(3, 5), | ||||
|     new Point(1, 3) | ||||
|     new Point(1, 3), | ||||
|   ]; | ||||
|   for (let offset of connectors.values()) { | ||||
|     let connector = room.top.offset(offset); | ||||
| @@ -278,7 +310,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | ||||
|       if (check != null) { | ||||
|         knife.map.get(connector).pickup = new LockPickup(check); | ||||
|       } | ||||
|       knife.carve(connector) | ||||
|       knife.carve(connector); | ||||
|     } | ||||
|     if (mergeRects(c, d).contains(connector)) { | ||||
|       // TODO: Put check 2 here | ||||
| @@ -286,7 +318,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | ||||
|       if (check != null) { | ||||
|         knife.map.get(connector).pickup = new LockPickup(check); | ||||
|       } | ||||
|       knife.carve(connector) | ||||
|       knife.carve(connector); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -296,7 +328,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | ||||
|     new Point(5, 1), | ||||
|     new Point(1, 5), | ||||
|     new Point(5, 5), | ||||
|   ] | ||||
|   ]; | ||||
|   for (let offset of goodies.values()) { | ||||
|     let goodie = room.top.offset(offset); | ||||
|     let cell = knife.map.get(goodie); | ||||
| @@ -352,7 +384,9 @@ function carveRoom(knife: Knife, room: Rect) { | ||||
|       let xy0 = room.top.offset(new Point(dx, dy)); | ||||
|       let xy1 = room.top.offset(new Point(room.size.w - dx - 1, dy)); | ||||
|       let xy2 = room.top.offset(new Point(dx, room.size.h - dy - 1)); | ||||
|       let xy3 = room.top.offset(new Point(room.size.w - dx - 1, room.size.h - dy - 1)); | ||||
|       let xy3 = room.top.offset( | ||||
|         new Point(room.size.w - dx - 1, room.size.h - dy - 1), | ||||
|       ); | ||||
|       let stat = choose(ALL_STATS); | ||||
|       knife.map.get(xy0).pickup = new StatPickup(stat); | ||||
|       knife.map.get(xy1).pickup = new StatPickup(stat); | ||||
| @@ -368,18 +402,15 @@ let mergeRects = (a: Rect, b: Rect) => { | ||||
|   let abx1 = Math.max(a.top.x + a.size.w, b.top.x + b.size.w); | ||||
|   let aby1 = Math.max(a.top.y + a.size.h, b.top.y + b.size.h); | ||||
|  | ||||
|   return new Rect( | ||||
|     new Point(abx0, aby0), | ||||
|     new Size(abx1 - abx0, aby1 - aby0) | ||||
|   ); | ||||
| } | ||||
|   return new Rect(new Point(abx0, aby0), new Size(abx1 - abx0, aby1 - aby0)); | ||||
| }; | ||||
|  | ||||
| const _CARDINAL_DIRECTIONS = [ | ||||
|   new Point(-1, 0), | ||||
|   new Point(0, -1), | ||||
|   new Point(1, 0), | ||||
|   new Point(0, 1), | ||||
| ] | ||||
| ]; | ||||
|  | ||||
| function connectRegions(knife: Knife) { | ||||
|   // this procedure is really complicated | ||||
| @@ -405,7 +436,9 @@ function connectRegions(knife: Knife) { | ||||
|         } | ||||
|       } | ||||
|       regions = dedup(regions); | ||||
|       if (regions.length < 2) { continue; } | ||||
|       if (regions.length < 2) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       connectorRegions.set(pos, regions); | ||||
|       connectors.push(pos); | ||||
| @@ -413,7 +446,7 @@ function connectRegions(knife: Knife) { | ||||
|   } | ||||
|  | ||||
|   // map from original index to "region it has been merged to" index | ||||
|   let merged: Record<number, number> = {} | ||||
|   let merged: Record<number, number> = {}; | ||||
|   let openRegions = []; | ||||
|   for (let i = 0; i <= knife.region; i++) { | ||||
|     merged[i] = i; | ||||
| @@ -424,12 +457,16 @@ function connectRegions(knife: Knife) { | ||||
|  | ||||
|   while (openRegions.length > 1) { | ||||
|     if (iter > 100) { | ||||
|       throw new TryAgainException("algorithm was not quiescent for some reason"); | ||||
|       throw new TryAgainException( | ||||
|         "algorithm was not quiescent for some reason", | ||||
|       ); | ||||
|     } | ||||
|     iter++; | ||||
|     showDebug(knife.map); | ||||
|     if (connectors.length == 0) { | ||||
|       throw new TryAgainException("couldn't figure out how to connect sections") | ||||
|       throw new TryAgainException( | ||||
|         "couldn't figure out how to connect sections", | ||||
|       ); | ||||
|     } | ||||
|     let connector = choose(connectors); | ||||
|  | ||||
| @@ -439,7 +476,7 @@ function connectRegions(knife: Knife) { | ||||
|     let sources: number[] = dedup(basicRegions.map((i) => merged[i])); | ||||
|     let dest: number | undefined = sources.pop(); | ||||
|     if (dest == undefined) { | ||||
|       throw "each connector should touch more than one region" | ||||
|       throw "each connector should touch more than one region"; | ||||
|     } | ||||
|  | ||||
|     if (Math.random() > EXTRA_CONNECTOR_CHANCE) { | ||||
| @@ -452,18 +489,22 @@ function connectRegions(knife: Knife) { | ||||
|  | ||||
|       for (let src of sources.values()) { | ||||
|         let ix = openRegions.indexOf(src); | ||||
|         if (ix != -1) { openRegions.splice(ix, 1); } | ||||
|         if (ix != -1) { | ||||
|           openRegions.splice(ix, 1); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     let connectors2 = []; | ||||
|     for (let other of connectors.values()) { | ||||
|       if (other.manhattan(connector) == 1) { continue; } | ||||
|       if (other.manhattan(connector) == 1) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       let connected = dedup( | ||||
|         connectorRegions.get(other).map((m) => merged[m]) | ||||
|       ); | ||||
|       if (connected.length <= 1) { continue; } | ||||
|       let connected = dedup(connectorRegions.get(other).map((m) => merged[m])); | ||||
|       if (connected.length <= 1) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       connectors2.push(other); | ||||
|     } | ||||
| @@ -496,7 +537,7 @@ function growMaze(knife: Knife, start: Point) { | ||||
|     if (unmadeCells.length == 0) { | ||||
|       cells.pop(); | ||||
|       lastDir = null; | ||||
|       continue | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     let dir: Point; | ||||
| @@ -510,7 +551,7 @@ function growMaze(knife: Knife, start: Point) { | ||||
|     let c2 = cell.offset(dir).offset(dir); | ||||
|     knife.carve(c1); | ||||
|     knife.carve(c2); | ||||
|     cells.push(c2) | ||||
|     cells.push(c2); | ||||
|     lastDir = dir; | ||||
|   } | ||||
| } | ||||
| @@ -526,7 +567,6 @@ function canCarve(knife: Knife, pos: Point, direction: Point) { | ||||
|   return knife.map.get(c2).architecture == Architecture.Wall; | ||||
| } | ||||
|  | ||||
|  | ||||
| function removeDeadEnds(knife: Knife) { | ||||
|   let done = false; | ||||
|  | ||||
| @@ -536,7 +576,9 @@ function removeDeadEnds(knife: Knife) { | ||||
|     for (let y = 1; y < knife.map.size.h - 1; y++) { | ||||
|       for (let x = 1; x < knife.map.size.w - 1; x++) { | ||||
|         let xy = new Point(x, y); | ||||
|         if (knife.map.get(xy).architecture == Architecture.Wall) { continue; } | ||||
|         if (knife.map.get(xy).architecture == Architecture.Wall) { | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         let exits = 0; | ||||
|         for (let dir of _CARDINAL_DIRECTIONS.values()) { | ||||
| @@ -545,7 +587,9 @@ function removeDeadEnds(knife: Knife) { | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if (exits != 1) { continue; } | ||||
|         if (exits != 1) { | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         done = false; | ||||
|         knife.map.get(xy).architecture = Architecture.Wall; | ||||
| @@ -554,22 +598,22 @@ function removeDeadEnds(knife: Knife) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| function decorateRoom(_map: LoadedNewMap, _rect: Rect) { | ||||
|  | ||||
| } | ||||
| function decorateRoom(_map: LoadedNewMap, _rect: Rect) {} | ||||
|  | ||||
| function randrange(lo: number, hi: number) { | ||||
|   if (lo >= hi) { | ||||
|     throw `randrange: hi must be >= lo, ${hi}, ${lo}` | ||||
|     throw `randrange: hi must be >= lo, ${hi}, ${lo}`; | ||||
|   } | ||||
|  | ||||
|   return lo + Math.floor(Math.random() * (hi - lo)) | ||||
|   return lo + Math.floor(Math.random() * (hi - lo)); | ||||
| } | ||||
|  | ||||
| function dedup(items: number[]): number[] { | ||||
|   let deduped = []; | ||||
|   for (let i of items.values()) { | ||||
|     if (deduped.indexOf(i) != -1) { continue; } | ||||
|     if (deduped.indexOf(i) != -1) { | ||||
|       continue; | ||||
|     } | ||||
|     deduped.push(i); | ||||
|   } | ||||
|   return deduped; | ||||
| @@ -580,7 +624,10 @@ function showDebug(grid: LoadedNewMap) { | ||||
|     let out = ""; | ||||
|     for (let y = 0; y < grid.size.h; y++) { | ||||
|       for (let x = 0; x < grid.size.w; x++) { | ||||
|         out += grid.get(new Point(x, y)).architecture == Architecture.Wall ? "#" : "."; | ||||
|         out += | ||||
|           grid.get(new Point(x, y)).architecture == Architecture.Wall | ||||
|             ? "#" | ||||
|             : "."; | ||||
|       } | ||||
|       out += "\n"; | ||||
|     } | ||||
| @@ -588,6 +635,4 @@ function showDebug(grid: LoadedNewMap) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class TryAgainException extends Error { | ||||
|  | ||||
| } | ||||
| class TryAgainException extends Error {} | ||||
|   | ||||
| @@ -1,29 +1,67 @@ | ||||
| import {choose} from "./utils.ts"; | ||||
| import { choose } from "./utils.ts"; | ||||
|  | ||||
| const names = [ | ||||
|   // vampires | ||||
|   "Vlad", "Drek", | ||||
|   "Vlad", | ||||
|   "Drek", | ||||
|   // generic American names I like | ||||
|   "Kyle", | ||||
|   // friends I can defame | ||||
|   "Bhijn", "Myr", "Narry", | ||||
|   "Bhijn", | ||||
|   "Myr", | ||||
|   "Narry", | ||||
|   // aggressively furry names | ||||
|   "Tech", | ||||
|   // deities | ||||
|   "Quetzal", "Zotz", | ||||
|   "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", | ||||
|   "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", | ||||
|   "Jeff", | ||||
|   "Jon", | ||||
|   "Garrett", | ||||
|   "Russell", | ||||
|   "Tyson", | ||||
|   "Gervase", | ||||
|   "Sonja", | ||||
|   "Sue", | ||||
|   "Richard", | ||||
|   "Jankie", | ||||
|   // highly trustworthy individuals | ||||
|   "Nef", "Matt", "Sam" | ||||
| ] | ||||
|   "Nef", | ||||
|   "Matt", | ||||
|   "Sam", | ||||
| ]; | ||||
| export function generateName() { | ||||
|   return choose(names); | ||||
| } | ||||
| @@ -42,9 +80,9 @@ const titles = [ | ||||
|   "Poker Player", | ||||
|   "Priest", | ||||
|   "Magician", | ||||
|   "Writer" | ||||
|   "Writer", | ||||
| ]; | ||||
|  | ||||
| export function generateTitle() { | ||||
|   return choose(titles); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,36 +1,39 @@ | ||||
| import {Grid, Point, Size} from "./engine/datatypes.ts"; | ||||
| import {Pickup} from "./pickups.ts"; | ||||
| import {Skill} from "./datatypes.ts"; | ||||
| import { Grid, Point, Size } from "./engine/datatypes.ts"; | ||||
| import { Pickup } from "./pickups.ts"; | ||||
| import { Skill } from "./datatypes.ts"; | ||||
|  | ||||
| export enum Architecture { Wall, Floor } | ||||
| export enum Architecture { | ||||
|   Wall, | ||||
|   Floor, | ||||
| } | ||||
|  | ||||
| export type CheckData = { | ||||
|   label: string, | ||||
|   options: (CheckDataOption | ChoiceOption)[], | ||||
| } | ||||
|   label: string; | ||||
|   options: (CheckDataOption | ChoiceOption)[]; | ||||
| }; | ||||
|  | ||||
| export type ChoiceOption = { | ||||
|   isChoice: true, | ||||
|   countsAsSuccess: boolean, | ||||
|   unlockable: string, | ||||
|   success: string, | ||||
| } | ||||
|   isChoice: true; | ||||
|   countsAsSuccess: boolean; | ||||
|   unlockable: string; | ||||
|   success: string; | ||||
| }; | ||||
| export type CheckDataOption = { | ||||
|   skill: () => Skill, | ||||
|   locked: string, | ||||
|   failure: string, | ||||
|   unlockable: string, | ||||
|   success: string, | ||||
| } | ||||
|   skill: () => Skill; | ||||
|   locked: string; | ||||
|   failure: string; | ||||
|   unlockable: string; | ||||
|   success: string; | ||||
| }; | ||||
|  | ||||
| export class LoadedNewMap { | ||||
|   #id: string | ||||
|   #size: Size | ||||
|   #entrance: Point | null | ||||
|   #architecture: Grid<Architecture> | ||||
|   #pickups: Grid<Pickup | null> | ||||
|   #provinces: Grid<string | null> | ||||
|   #revealed: Grid<boolean> | ||||
|   #id: string; | ||||
|   #size: Size; | ||||
|   #entrance: Point | null; | ||||
|   #architecture: Grid<Architecture>; | ||||
|   #pickups: Grid<Pickup | null>; | ||||
|   #provinces: Grid<string | null>; | ||||
|   #revealed: Grid<boolean>; | ||||
|  | ||||
|   constructor(id: string, size: Size) { | ||||
|     this.#id = id; | ||||
| @@ -48,7 +51,7 @@ export class LoadedNewMap { | ||||
|  | ||||
|   get entrance(): Point { | ||||
|     if (this.#entrance == null) { | ||||
|       throw `${this.#id}: this.#entrance was never initialized` | ||||
|       throw `${this.#id}: this.#entrance was never initialized`; | ||||
|     } | ||||
|     return this.#entrance; | ||||
|   } | ||||
| @@ -58,7 +61,7 @@ export class LoadedNewMap { | ||||
|   } | ||||
|  | ||||
|   get(point: Point): CellView { | ||||
|     return new CellView(this, point) | ||||
|     return new CellView(this, point); | ||||
|   } | ||||
|  | ||||
|   setArchitecture(point: Point, value: Architecture) { | ||||
| @@ -86,7 +89,7 @@ export class LoadedNewMap { | ||||
|   } | ||||
|  | ||||
|   setRevealed(point: Point, value: boolean) { | ||||
|     this.#revealed.set(point, value) | ||||
|     this.#revealed.set(point, value); | ||||
|   } | ||||
|  | ||||
|   getRevealed(point: Point): boolean { | ||||
| @@ -95,25 +98,41 @@ export class LoadedNewMap { | ||||
| } | ||||
|  | ||||
| export class CellView { | ||||
|   #map: LoadedNewMap | ||||
|   #point: Point | ||||
|   #map: LoadedNewMap; | ||||
|   #point: Point; | ||||
|  | ||||
|   constructor(map: LoadedNewMap, point: Point) { | ||||
|     this.#map = map; | ||||
|     this.#point = point; | ||||
|   } | ||||
|  | ||||
|   set architecture(value: Architecture) { this.#map.setArchitecture(this.#point, value) } | ||||
|   get architecture(): Architecture { return this.#map.getArchitecture(this.#point) } | ||||
|   set architecture(value: Architecture) { | ||||
|     this.#map.setArchitecture(this.#point, value); | ||||
|   } | ||||
|   get architecture(): Architecture { | ||||
|     return this.#map.getArchitecture(this.#point); | ||||
|   } | ||||
|  | ||||
|   set pickup(value: Pickup | null) { this.#map.setPickup(this.#point, value) } | ||||
|   get pickup(): Pickup | null { return this.#map.getPickup(this.#point) } | ||||
|   set pickup(value: Pickup | null) { | ||||
|     this.#map.setPickup(this.#point, value); | ||||
|   } | ||||
|   get pickup(): Pickup | null { | ||||
|     return this.#map.getPickup(this.#point); | ||||
|   } | ||||
|  | ||||
|   set province(value: string | null) { this.#map.setProvince(this.#point, value) } | ||||
|   get province(): string | null { return this.#map.getProvince(this.#point) } | ||||
|   set province(value: string | null) { | ||||
|     this.#map.setProvince(this.#point, value); | ||||
|   } | ||||
|   get province(): string | null { | ||||
|     return this.#map.getProvince(this.#point); | ||||
|   } | ||||
|  | ||||
|   set revealed(value: boolean) { this.#map.setRevealed(this.#point, value) } | ||||
|   get revealed(): boolean { return this.#map.getRevealed(this.#point) } | ||||
|   set revealed(value: boolean) { | ||||
|     this.#map.setRevealed(this.#point, value); | ||||
|   } | ||||
|   get revealed(): boolean { | ||||
|     return this.#map.getRevealed(this.#point); | ||||
|   } | ||||
|  | ||||
|   copyFrom(cell: CellView) { | ||||
|     this.architecture = cell.architecture; | ||||
| @@ -121,4 +140,4 @@ export class CellView { | ||||
|     this.province = cell.province; | ||||
|     this.revealed = cell.revealed; | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										221
									
								
								src/pickups.ts
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								src/pickups.ts
									
									
									
									
									
								
							| @@ -1,24 +1,29 @@ | ||||
| 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"; | ||||
| import {generateMap} from "./mapgen.ts"; | ||||
| import {ALL_STATS, Stat} from "./datatypes.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| 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"; | ||||
| 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"; | ||||
| import { generateMap } from "./mapgen.ts"; | ||||
| import { ALL_STATS, Stat } from "./datatypes.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
| 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 | ||||
| export type Pickup = | ||||
|   | LockPickup | ||||
|   | StatPickup | ||||
|   | ExperiencePickup | ||||
|   | LadderPickup | ||||
|   | ThrallPickup | ||||
|   | ThrallPosterPickup | ||||
|   | ThrallRecruitedPickup | ||||
|   | ThrallRecruitedPickup; | ||||
|  | ||||
| export class LockPickup { | ||||
|   check: CheckData; | ||||
| @@ -27,22 +32,26 @@ export class LockPickup { | ||||
|     this.check = check; | ||||
|   } | ||||
|  | ||||
|   computeCostToClick() { return 0; } | ||||
|   computeCostToClick() { | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   isObstructive() { return true; } | ||||
|   isObstructive() { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   drawFloor() { } | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     for (let z = 0; z < 5; z += 0.25) { | ||||
|       D.drawSprite(sprLock, gridArt.project(z), 0, { | ||||
|         xScale: 2.0, | ||||
|         yScale: 2.0, | ||||
|       }) | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   onClick(cell: CellView): boolean { | ||||
|     getCheckModal().show(this.check, () => cell.pickup = null); | ||||
|     getCheckModal().show(this.check, () => (cell.pickup = null)); | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
| @@ -54,24 +63,25 @@ export class StatPickup { | ||||
|     this.stat = stat; | ||||
|   } | ||||
|  | ||||
|   computeCostToClick() { return 100; } | ||||
|   computeCostToClick() { | ||||
|     return 100; | ||||
|   } | ||||
|  | ||||
|   isObstructive() { return true; } | ||||
|   isObstructive() { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   drawFloor() { } | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let statIndex = ALL_STATS.indexOf(this.stat); | ||||
|     if (statIndex == -1) { return; } | ||||
|     if (statIndex == -1) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     D.drawSprite( | ||||
|       sprStatPickup, | ||||
|       gridArt.project(5), | ||||
|       statIndex, | ||||
|       { | ||||
|         xScale: 2, | ||||
|         yScale: 2, | ||||
|       } | ||||
|     ) | ||||
|     D.drawSprite(sprStatPickup, gridArt.project(5), statIndex, { | ||||
|       xScale: 2, | ||||
|       yScale: 2, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   onClick(): boolean { | ||||
| @@ -82,11 +92,15 @@ export class StatPickup { | ||||
| } | ||||
|  | ||||
| export class ExperiencePickup { | ||||
|   computeCostToClick() { return 100; } | ||||
|   computeCostToClick() { | ||||
|     return 100; | ||||
|   } | ||||
|  | ||||
|   isObstructive() { return true; } | ||||
|   isObstructive() { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   drawFloor() { } | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     D.drawSprite( | ||||
|       sprResourcePickup, | ||||
| @@ -95,7 +109,7 @@ export class ExperiencePickup { | ||||
|       { | ||||
|         xScale: 2, | ||||
|         yScale: 2, | ||||
|       } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -107,17 +121,21 @@ export class ExperiencePickup { | ||||
| } | ||||
|  | ||||
| export class LadderPickup { | ||||
|   computeCostToClick() { return 0; } | ||||
|   computeCostToClick() { | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   isObstructive() { return false; } | ||||
|   isObstructive() { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor(gridArt: GridArt) { | ||||
|     D.drawSprite(sprLadder, gridArt.project(0.0), 0, { | ||||
|       xScale: 2.0, | ||||
|       yScale: 2.0, | ||||
|     }) | ||||
|     }); | ||||
|   } | ||||
|   drawInAir() { } | ||||
|   drawInAir() {} | ||||
|  | ||||
|   onClick(): boolean { | ||||
|     getPlayerProgress().addBlood(1000); | ||||
| @@ -133,24 +151,28 @@ export class ThrallPickup { | ||||
|     this.thrall = thrall; | ||||
|   } | ||||
|  | ||||
|   computeCostToClick() { return 0; } | ||||
|   computeCostToClick() { | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   isObstructive() { return false; } | ||||
|   isObstructive() { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor() { } | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let data = getThralls().get(this.thrall); | ||||
|     D.drawSprite(data.sprite, gridArt.project(0.0), 0, { | ||||
|       xScale: 2.0, | ||||
|       yScale: 2.0, | ||||
|     }) | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   onClick(cell: CellView): boolean { | ||||
|     let data = getThralls().get(this.thrall); | ||||
|     getCheckModal().show(data.initialCheck, () => { | ||||
|       getPlayerProgress().unlockThrall(this.thrall); | ||||
|       cell.pickup = null | ||||
|       cell.pickup = null; | ||||
|     }); | ||||
|     return true; | ||||
|   } | ||||
| @@ -163,27 +185,30 @@ export class ThrallPosterPickup { | ||||
|     this.thrall = thrall; | ||||
|   } | ||||
|  | ||||
|   computeCostToClick() { return 0; } | ||||
|   computeCostToClick() { | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   isObstructive() { return false; } | ||||
|   isObstructive() { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor() { } | ||||
|   drawFloor() {} | ||||
|   drawInAir(gridArt: GridArt) { | ||||
|     let data = getThralls().get(this.thrall); | ||||
|     D.drawSprite(data.sprite, gridArt.project(0.0), 2, { | ||||
|       xScale: 2.0, | ||||
|       yScale: 2.0, | ||||
|     }) | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   onClick(cell: CellView): boolean { | ||||
|     let data = getThralls().get(this.thrall); | ||||
|     getCheckModal().show(data.posterCheck, () => cell.pickup = null); | ||||
|     getCheckModal().show(data.posterCheck, () => (cell.pickup = null)); | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| export class ThrallRecruitedPickup { | ||||
|   thrall: Thrall; | ||||
|   bitten: boolean; | ||||
| @@ -193,60 +218,78 @@ export class ThrallRecruitedPickup { | ||||
|     this.bitten = false; | ||||
|   } | ||||
|  | ||||
|   computeCostToClick() { return 0; } | ||||
|   computeCostToClick() { | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   isObstructive() { return false; } | ||||
|   isObstructive() { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   drawFloor() { } | ||||
|   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; } | ||||
|     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 | ||||
|     }) | ||||
|       angle: rot, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   onClick(_cell: CellView): boolean { | ||||
|     if (this.bitten) { return true; } | ||||
|     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])) | ||||
|     }); | ||||
|     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; | ||||
|   } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,31 +1,31 @@ | ||||
| import {ALL_STATS, Skill, Stat, SuccessorOption, Wish} from "./datatypes.ts"; | ||||
| import {getSkills} from "./skills.ts"; | ||||
| import {getThralls, LifeStage, Thrall} from "./thralls.ts"; | ||||
| 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 | ||||
|   #stats: Record<Stat, number> | ||||
|   #talents: Record<Stat, number> | ||||
|   #name: string; | ||||
|   #stats: Record<Stat, number>; | ||||
|   #talents: Record<Stat, number>; | ||||
|   #isInPenance: boolean; | ||||
|   #wish: Wish | null; | ||||
|   #exp: number; | ||||
|   #blood: number | ||||
|   #itemsPurloined: number | ||||
|   #skillsLearned: number[]  // use the raw ID representation for indexOf | ||||
|   #untrimmedSkillsAvailable: Skill[] | ||||
|   #thrallsUnlocked: number[] | ||||
|   #thrallDamage: Record<number, number> | ||||
|   #blood: number; | ||||
|   #itemsPurloined: number; | ||||
|   #skillsLearned: number[]; // use the raw ID representation for indexOf | ||||
|   #untrimmedSkillsAvailable: Skill[]; | ||||
|   #thrallsUnlocked: number[]; | ||||
|   #thrallDamage: Record<number, number>; | ||||
|  | ||||
|   constructor(asSuccessor: SuccessorOption, withWish: Wish | null) { | ||||
|     this.#name = asSuccessor.name; | ||||
|     this.#stats = {...asSuccessor.stats}; | ||||
|     this.#talents = {...asSuccessor.talents}; | ||||
|     this.#stats = { ...asSuccessor.stats }; | ||||
|     this.#talents = { ...asSuccessor.talents }; | ||||
|     this.#isInPenance = asSuccessor.inPenance; | ||||
|     this.#wish = withWish; | ||||
|     this.#exp = 0; | ||||
|     this.#blood = 0; | ||||
|     this.#itemsPurloined = 0; | ||||
|     this.#skillsLearned = [] | ||||
|     this.#skillsLearned = []; | ||||
|     this.#untrimmedSkillsAvailable = []; | ||||
|     this.#thrallsUnlocked = []; | ||||
|     this.#thrallDamage = {}; | ||||
| @@ -50,8 +50,10 @@ export class PlayerProgress { | ||||
|   refill() { | ||||
|     this.#blood = 2000; | ||||
|  | ||||
|     let learnableSkills = [];  // TODO: Also include costing info | ||||
|     for (let skill of getSkills().getAvailableSkills(this.#isInPenance).values()) { | ||||
|     let learnableSkills = []; // TODO: Also include costing info | ||||
|     for (let skill of getSkills() | ||||
|       .getAvailableSkills(this.#isInPenance) | ||||
|       .values()) { | ||||
|       if (this.#canBeAvailable(skill)) { | ||||
|         learnableSkills.push(skill); | ||||
|       } | ||||
| @@ -59,11 +61,16 @@ 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); | ||||
|       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 | ||||
|     this.#untrimmedSkillsAvailable = learnableSkills; | ||||
|   } | ||||
|  | ||||
|   hasLearned(skill: Skill) { | ||||
| @@ -72,14 +79,16 @@ export class PlayerProgress { | ||||
|  | ||||
|   learnSkill(skill: Skill) { | ||||
|     if (this.#skillsLearned.indexOf(skill.id) != -1) { | ||||
|       return | ||||
|       return; | ||||
|     } | ||||
|     this.#skillsLearned.push(skill.id); | ||||
|  | ||||
|     // remove entries for that skill | ||||
|     let skills2 = []; | ||||
|     for (let entry of this.#untrimmedSkillsAvailable.values()) { | ||||
|       if (entry.id == skill.id) { continue; } | ||||
|       if (entry.id == skill.id) { | ||||
|         continue; | ||||
|       } | ||||
|       skills2.push(entry); | ||||
|     } | ||||
|     this.#untrimmedSkillsAvailable = skills2; | ||||
| @@ -96,7 +105,7 @@ export class PlayerProgress { | ||||
|     // make sure the prereqs are met | ||||
|     for (let prereq of data.prereqs.values()) { | ||||
|       if (!this.hasLearned(prereq)) { | ||||
|         return false | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @@ -109,12 +118,12 @@ export class PlayerProgress { | ||||
|   } | ||||
|  | ||||
|   getItemsPurloined() { | ||||
|     return this.#itemsPurloined | ||||
|     return this.#itemsPurloined; | ||||
|   } | ||||
|  | ||||
|   add(stat: Stat, amount: number) { | ||||
|     if (amount != Math.floor(amount)) { | ||||
|       throw `stat increment must be integer: ${amount}` | ||||
|       throw `stat increment must be integer: ${amount}`; | ||||
|     } | ||||
|     this.#stats[stat] += amount; | ||||
|     this.#stats[stat] = Math.min(Math.max(this.#stats[stat], -99), 999); | ||||
| @@ -125,18 +134,18 @@ export class PlayerProgress { | ||||
|   } | ||||
|  | ||||
|   getExperience(): number { | ||||
|     return this.#exp | ||||
|     return this.#exp; | ||||
|   } | ||||
|  | ||||
|   spendExperience(cost: number) { | ||||
|     if (this.#exp < cost) { | ||||
|       throw `can't spend ${cost}` | ||||
|       throw `can't spend ${cost}`; | ||||
|     } | ||||
|     this.#exp -= cost; | ||||
|   } | ||||
|  | ||||
|   getStat(stat: Stat): number { | ||||
|     return this.#stats[stat] | ||||
|     return this.#stats[stat]; | ||||
|   } | ||||
|  | ||||
|   getTalent(stat: Stat): number { | ||||
| @@ -149,7 +158,7 @@ export class PlayerProgress { | ||||
|  | ||||
|   addBlood(amt: number) { | ||||
|     this.#blood += amt; | ||||
|     this.#blood = Math.min(this.#blood, 5000) | ||||
|     this.#blood = Math.min(this.#blood, 5000); | ||||
|   } | ||||
|  | ||||
|   spendBlood(amt: number) { | ||||
| @@ -157,7 +166,7 @@ export class PlayerProgress { | ||||
|   } | ||||
|  | ||||
|   getWish(): Wish | null { | ||||
|     return this.#wish | ||||
|     return this.#wish; | ||||
|   } | ||||
|  | ||||
|   getAvailableSkills(): Skill[] { | ||||
| @@ -167,30 +176,40 @@ export class PlayerProgress { | ||||
|       let name1 = getSkills().get(a).profile.name; | ||||
|       let name2 = getSkills().get(b).profile.name; | ||||
|  | ||||
|       if (name1 < name2) { return -1; } | ||||
|       if (name1 > name2) { return 1; } | ||||
|       if (name1 < name2) { | ||||
|         return -1; | ||||
|       } | ||||
|       if (name1 > name2) { | ||||
|         return 1; | ||||
|       } | ||||
|       return 0; | ||||
|     }); | ||||
|     skillsAvailable.sort((a, b) => { | ||||
|       return getSkills().computeCost(a) - getSkills().computeCost(b) | ||||
|       return getSkills().computeCost(a) - getSkills().computeCost(b); | ||||
|     }); | ||||
|     return skillsAvailable.slice(0, 6) | ||||
|     return skillsAvailable.slice(0, 6); | ||||
|   } | ||||
|  | ||||
|   getLearnedSkills() { | ||||
|     let learnedSkills = [] | ||||
|     let learnedSkills = []; | ||||
|     for (let s of this.#skillsLearned.values()) { | ||||
|       learnedSkills.push({id: s}) | ||||
|       learnedSkills.push({ id: s }); | ||||
|     } | ||||
|     return learnedSkills; | ||||
|   } | ||||
|  | ||||
|   getStats() { return {...this.#stats} } | ||||
|   getTalents() { return {...this.#talents} } | ||||
|   getStats() { | ||||
|     return { ...this.#stats }; | ||||
|   } | ||||
|   getTalents() { | ||||
|     return { ...this.#talents }; | ||||
|   } | ||||
|  | ||||
|   unlockThrall(thrall: Thrall) { | ||||
|     let {id} = thrall; | ||||
|     if (this.#thrallsUnlocked.indexOf(id) != -1) { return; } | ||||
|     let { id } = thrall; | ||||
|     if (this.#thrallsUnlocked.indexOf(id) != -1) { | ||||
|       return; | ||||
|     } | ||||
|     this.#thrallsUnlocked.push(id); | ||||
|   } | ||||
|  | ||||
| @@ -200,34 +219,50 @@ export class PlayerProgress { | ||||
|  | ||||
|   damageThrall(thrall: Thrall, amount: number) { | ||||
|     if (amount <= 0.0) { | ||||
|       throw new Error(`damage must be some positive amount, not ${amount}`) | ||||
|       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 | ||||
|     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; } | ||||
|     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; | ||||
|  | ||||
| export function initPlayerProgress(asSuccessor: SuccessorOption, withWish: Wish | null){ | ||||
| export function initPlayerProgress( | ||||
|   asSuccessor: SuccessorOption, | ||||
|   withWish: Wish | null, | ||||
| ) { | ||||
|   active = new PlayerProgress(asSuccessor, withWish); | ||||
| } | ||||
|  | ||||
| export function getPlayerProgress(): PlayerProgress { | ||||
|   if (active == null) { | ||||
|     throw new Error(`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 | ||||
| } | ||||
|   return active; | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,20 @@ | ||||
| import {VNScene} from "./vnscene.ts"; | ||||
| 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, getWishes, isWishCompleted} from "./wishes.ts"; | ||||
| import {generateSuccessors} from "./successors.ts"; | ||||
| import { VNScene } from "./vnscene.ts"; | ||||
| 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, getWishes, isWishCompleted } from "./wishes.ts"; | ||||
| import { generateSuccessors } from "./successors.ts"; | ||||
|  | ||||
| class Scorer { | ||||
|   constructor() { } | ||||
|   constructor() {} | ||||
|  | ||||
|   pickEnding(): Ending { | ||||
|     let learnedSkills = getPlayerProgress().getLearnedSkills(); | ||||
| @@ -30,7 +37,7 @@ 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<string, number> = {...scores}; | ||||
|     let runningScores: Record<string, number> = { ...scores }; | ||||
|     const isMax = (cat: ScoringCategory, min: number) => { | ||||
|       let score = runningScores[cat] ?? 0; | ||||
|       runningScores[cat] = 0; // each category, once checked, can't disqualify any other category | ||||
| @@ -44,7 +51,7 @@ class Scorer { | ||||
|         } | ||||
|       } | ||||
|       return true; | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     let scene: VNScene; | ||||
|     let rank: string; | ||||
| @@ -58,7 +65,7 @@ class Scorer { | ||||
|     if (wish != null) { | ||||
|       let data = getWishes().get(wish); | ||||
|       if (isWishCompleted(wish)) { | ||||
|         scene = data.onVictory | ||||
|         scene = data.onVictory; | ||||
|         rank = data.profile.name; | ||||
|         domicile = data.profile.domicile; | ||||
|         reignSentence = data.profile.reignSentence; | ||||
| @@ -70,7 +77,6 @@ class Scorer { | ||||
|         penance = true; | ||||
|         successorVerb = data.profile.failureSuccessorVerb; | ||||
|       } | ||||
|  | ||||
|     } | ||||
|     // TODO: Award different ranks depending on second-to-top skill | ||||
|     // TODO: Award different domiciles based on overall score | ||||
| @@ -80,26 +86,22 @@ class Scorer { | ||||
|       rank = "Hypno-Chiropteran"; | ||||
|       domicile = "Village of Brainwashed Mortals"; | ||||
|       reignSentence = "You rule with a fair but unflinching gaze."; | ||||
|     } | ||||
|     else if (isMax("lore", 3)) { | ||||
|     } 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)) { | ||||
|     } 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)) { | ||||
|     } 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)) { | ||||
|     } else if (isMax("stealth", 0)) { | ||||
|       scene = sceneStealth; | ||||
|       rank = "Invisible"; | ||||
|       domicile = "Townhouse"; | ||||
| @@ -110,7 +112,8 @@ class Scorer { | ||||
|       scene = sceneBat; | ||||
|       rank = "Bat"; | ||||
|       domicile = "Cave"; | ||||
|       reignSentence = "Your skreeking verdicts are irresistible to your subjects."; | ||||
|       reignSentence = | ||||
|         "Your skreeking verdicts are irresistible to your subjects."; | ||||
|     } | ||||
|  | ||||
|     // TODO: Analytics tracker | ||||
| @@ -118,19 +121,25 @@ class Scorer { | ||||
|       itemsPurloined, | ||||
|       vampiricSkills, | ||||
|       mortalServants, | ||||
|     } | ||||
|     let successorOptions = generateSuccessors(0, penance);  // TODO: generate nImprovements from mortalServants and the player's bsae improvements | ||||
|     }; | ||||
|     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, reignSentence, successorVerb, progenerateVerb}, | ||||
|       personal: { | ||||
|         rank, | ||||
|         domicile, | ||||
|         reignSentence, | ||||
|         successorVerb, | ||||
|         progenerateVerb, | ||||
|       }, | ||||
|       analytics, | ||||
|       successorOptions, | ||||
|       wishOptions, | ||||
|     } | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -3,23 +3,27 @@ | ||||
| export var shadowcast = function ( | ||||
|   [ox, oy]: [number, number], | ||||
|   isBlocking: (xy: [number, number]) => boolean, | ||||
|   markVisible: (xy: [number, number]) => void | ||||
|   markVisible: (xy: [number, number]) => void, | ||||
| ) { | ||||
|   for (var i = 0; i < 4; i++) { | ||||
|     var quadrant = new Quadrant(i, [ox, oy]); | ||||
|     var reveal = function (xy: [number, number]) { | ||||
|       markVisible(quadrant.transform(xy)); | ||||
|     } | ||||
|     }; | ||||
|     var isWall = function (xy: [number, number] | undefined) { | ||||
|       if (xy == undefined) { return false; } | ||||
|       if (xy == undefined) { | ||||
|         return false; | ||||
|       } | ||||
|       return isBlocking(quadrant.transform(xy)); | ||||
|     } | ||||
|     }; | ||||
|     var isFloor = function (xy: [number, number] | undefined) { | ||||
|       if (xy == undefined) { return false; } | ||||
|       if (xy == undefined) { | ||||
|         return false; | ||||
|       } | ||||
|       return !isBlocking(quadrant.transform(xy)); | ||||
|     } | ||||
|     }; | ||||
|     var scan = function (row: Row) { | ||||
|       var prevXy: [number, number] | undefined | ||||
|       var prevXy: [number, number] | undefined; | ||||
|       row.forEachTile((xy) => { | ||||
|         if (isWall(xy) || isSymmetric(row, xy)) { | ||||
|           reveal(xy); | ||||
| @@ -33,16 +37,16 @@ export var shadowcast = function ( | ||||
|           scan(nextRow); | ||||
|         } | ||||
|         prevXy = xy; | ||||
|       }) | ||||
|       }); | ||||
|       if (isFloor(prevXy)) { | ||||
|         scan(row.next()); | ||||
|       } | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     var firstRow = new Row(1, new Fraction(-1, 1), new Fraction(1, 1)); | ||||
|     scan(firstRow); | ||||
|   } | ||||
| } | ||||
| }; | ||||
|  | ||||
| class Quadrant { | ||||
|   cardinal: number; | ||||
| @@ -57,11 +61,16 @@ class Quadrant { | ||||
|  | ||||
|   transform([row, col]: [number, number]): [number, number] { | ||||
|     switch (this.cardinal) { | ||||
|       case 0: return [this.ox + col, this.oy - row]; | ||||
|       case 2: return [this.ox + col, this.oy + row]; | ||||
|       case 1: return [this.ox + row, this.oy + col]; | ||||
|       case 3: return [this.ox - row, this.oy + col]; | ||||
|       default: throw new Error("invalid cardinal") | ||||
|       case 0: | ||||
|         return [this.ox + col, this.oy - row]; | ||||
|       case 2: | ||||
|         return [this.ox + col, this.oy + row]; | ||||
|       case 1: | ||||
|         return [this.ox + row, this.oy + col]; | ||||
|       case 3: | ||||
|         return [this.ox - row, this.oy + col]; | ||||
|       default: | ||||
|         throw new Error("invalid cardinal"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -81,7 +90,7 @@ class Row { | ||||
|     var minCol = roundTiesUp(this.startSlope.scale(this.depth)); | ||||
|     var maxCol = roundTiesDown(this.endSlope.scale(this.depth)); | ||||
|     for (var col = minCol; col <= maxCol; col++) { | ||||
|       cb([this.depth, col]) | ||||
|       cb([this.depth, col]); | ||||
|     } | ||||
|   } | ||||
|   next(): Row { | ||||
| @@ -109,17 +118,19 @@ class Fraction { | ||||
|  | ||||
| var slope = function ([rowDepth, col]: [number, number]): Fraction { | ||||
|   return new Fraction(2 * col - 1, 2 * rowDepth); | ||||
| } | ||||
| }; | ||||
|  | ||||
| var isSymmetric = function (row: Row, [_, col]: [number, number]) { | ||||
|   return col >= row.startSlope.scale(row.depth).toDouble() && | ||||
|     col <= (row.endSlope.scale(row.depth)).toDouble(); | ||||
| } | ||||
|   return ( | ||||
|     col >= row.startSlope.scale(row.depth).toDouble() && | ||||
|     col <= row.endSlope.scale(row.depth).toDouble() | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| var roundTiesUp = function (n: Fraction) { | ||||
|   return Math.floor(n.toDouble() + 0.5); | ||||
| } | ||||
| }; | ||||
|  | ||||
| var roundTiesDown = function (n: Fraction) { | ||||
|   return Math.ceil(n.toDouble() - 0.5); | ||||
| } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										280
									
								
								src/skills.ts
									
									
									
									
									
								
							
							
						
						
									
										280
									
								
								src/skills.ts
									
									
									
									
									
								
							| @@ -1,9 +1,15 @@ | ||||
| import {Skill, SkillData, SkillGoverning, SkillScoring, Stat} from "./datatypes.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
| import {getCostMultiplier} from "./wishes.ts"; | ||||
| import { | ||||
|   Skill, | ||||
|   SkillData, | ||||
|   SkillGoverning, | ||||
|   SkillScoring, | ||||
|   Stat, | ||||
| } from "./datatypes.ts"; | ||||
| import { getPlayerProgress } from "./playerprogress.ts"; | ||||
| import { getCostMultiplier } from "./wishes.ts"; | ||||
|  | ||||
| class SkillsTable { | ||||
|   #skills: SkillData[] | ||||
|   #skills: SkillData[]; | ||||
|  | ||||
|   constructor() { | ||||
|     this.#skills = []; | ||||
| @@ -12,19 +18,21 @@ class SkillsTable { | ||||
|   add(data: SkillData): Skill { | ||||
|     let id = this.#skills.length; | ||||
|     this.#skills.push(data); | ||||
|     return {id}; | ||||
|     return { id }; | ||||
|   } | ||||
|  | ||||
|   get(skill: Skill): SkillData { | ||||
|     return this.#skills[skill.id] | ||||
|     return this.#skills[skill.id]; | ||||
|   } | ||||
|  | ||||
|   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}); | ||||
|       if (isDegrading && !includeDegrading) { | ||||
|         continue; | ||||
|       } | ||||
|       skills.push({ id: i }); | ||||
|     } | ||||
|     return skills; | ||||
|   } | ||||
| @@ -34,23 +42,31 @@ class SkillsTable { | ||||
|  | ||||
|     let governingStatValue = 0; | ||||
|     for (let stat of data.governing.stats.values()) { | ||||
|       governingStatValue += getPlayerProgress().getStat(stat) / data.governing.stats.length; | ||||
|       governingStatValue += | ||||
|         getPlayerProgress().getStat(stat) / data.governing.stats.length; | ||||
|     } | ||||
|  | ||||
|     if (data.governing.flipped) { | ||||
|       governingStatValue = - governingStatValue + 10; | ||||
|       governingStatValue = -governingStatValue + 10; | ||||
|     } | ||||
|  | ||||
|     let mult = getCostMultiplier(getPlayerProgress().getWish(), skill); | ||||
|     let [underTarget, target] = [data.governing.underTarget, data.governing.target]; | ||||
|     let [underTarget, target] = [ | ||||
|       data.governing.underTarget, | ||||
|       data.governing.target, | ||||
|     ]; | ||||
|     underTarget = mult * underTarget; | ||||
|     target = mult * target; | ||||
|  | ||||
|     return Math.floor(geomInterpolate( | ||||
|       governingStatValue, | ||||
|       underTarget, target, | ||||
|       data.governing.cost, 999 | ||||
|     )) | ||||
|     return Math.floor( | ||||
|       geomInterpolate( | ||||
|         governingStatValue, | ||||
|         underTarget, | ||||
|         target, | ||||
|         data.governing.cost, | ||||
|         999, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -61,71 +77,111 @@ function geomInterpolate( | ||||
|   lowOut: number, | ||||
|   highOut: number, | ||||
| ) { | ||||
|   if (x < lowIn) { return highOut; } | ||||
|   if (x >= highIn) { return lowOut; } | ||||
|   if (x < lowIn) { | ||||
|     return highOut; | ||||
|   } | ||||
|   if (x >= highIn) { | ||||
|     return lowOut; | ||||
|   } | ||||
|  | ||||
|   const proportion = 1.0 - (x - lowIn) / (highIn - lowIn); | ||||
|   return lowOut * Math.pow(highOut / lowOut, proportion) | ||||
|   return lowOut * Math.pow(highOut / lowOut, proportion); | ||||
| } | ||||
|  | ||||
| type Difficulty = 0 | 1 | 1.25 | 2 | 3 | ||||
| type Difficulty = 0 | 1 | 1.25 | 2 | 3; | ||||
| type GoverningTemplate = { | ||||
|   stats: Stat[], | ||||
|   note: string | ||||
|   scoring: SkillScoring, | ||||
| } | ||||
|   stats: Stat[]; | ||||
|   note: string; | ||||
|   scoring: SkillScoring; | ||||
| }; | ||||
|  | ||||
| type Track = "bat" | "stealth" | "charm" | "stare" | "party" | "lore" | "penance" | ||||
| type Track = | ||||
|   | "bat" | ||||
|   | "stealth" | ||||
|   | "charm" | ||||
|   | "stare" | ||||
|   | "party" | ||||
|   | "lore" | ||||
|   | "penance"; | ||||
| let templates: Record<Track, GoverningTemplate> = { | ||||
|   bat: { | ||||
|     stats: ["AGI", "AGI", "PSI"], | ||||
|     note: "Cheaper with AGI and PSI.", | ||||
|     scoring: {bat: 1}, | ||||
|     scoring: { bat: 1 }, | ||||
|   }, | ||||
|   stealth: { | ||||
|     stats: ["AGI", "AGI", "INT"], | ||||
|     note: "Cheaper with AGI and INT.", | ||||
|     scoring: {stealth: 1}, | ||||
|     scoring: { stealth: 1 }, | ||||
|   }, | ||||
|   charm: { | ||||
|     stats: ["CHA", "PSI", "PSI"], | ||||
|     note: "Cheaper with CHA and PSI.", | ||||
|     scoring: {charm: 1}, | ||||
|     scoring: { charm: 1 }, | ||||
|   }, | ||||
|   stare: { | ||||
|     stats: ["PSI", "PSI"], | ||||
|     note: "Cheaper with PSI.", | ||||
|     scoring: {stare: 1}, | ||||
|     scoring: { stare: 1 }, | ||||
|   }, | ||||
|   party: { | ||||
|     stats: ["CHA", "CHA", "PSI"], | ||||
|     note: "Cheaper with CHA and PSI.", | ||||
|     scoring: {party: 1}, | ||||
|     scoring: { party: 1 }, | ||||
|   }, | ||||
|   lore: { | ||||
|     stats: ["INT", "INT", "CHA"], | ||||
|     note: "Cheaper with INT and CHA.", | ||||
|     scoring: {lore: 1}, | ||||
|     scoring: { lore: 1 }, | ||||
|   }, | ||||
|   penance: { | ||||
|     stats: ["AGI", "INT", "CHA", "PSI"], | ||||
|     note: "Lower your stats for this.", | ||||
|     scoring: {}, | ||||
|   } | ||||
| } | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| function governing(track: Track, difficulty: Difficulty, flipped?: boolean): SkillGoverning { | ||||
| function governing( | ||||
|   track: Track, | ||||
|   difficulty: Difficulty, | ||||
|   flipped?: boolean, | ||||
| ): SkillGoverning { | ||||
|   let template = templates[track]; | ||||
|   let underTarget: number | ||||
|   let target: number | ||||
|   let cost: number | ||||
|   let underTarget: number; | ||||
|   let target: number; | ||||
|   let cost: number; | ||||
|   let mortalServantValue: number; | ||||
|   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; | ||||
|   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) { | ||||
| @@ -141,7 +197,7 @@ function governing(track: Track, difficulty: Difficulty, flipped?: boolean): Ski | ||||
|     scoring: template.scoring, | ||||
|     mortalServantValue: mortalServantValue, | ||||
|     flipped: flipped ?? false, | ||||
|   } | ||||
|   }; | ||||
| } | ||||
|  | ||||
| let table = new SkillsTable(); | ||||
| @@ -151,195 +207,219 @@ export let bat0 = table.add({ | ||||
|   governing: governing("bat", 0), | ||||
|   profile: { | ||||
|     name: "Screech", | ||||
|     description: "Energy fills your body. You can't help but let go. It just feels so good to let that sound rip through you." | ||||
|     description: | ||||
|       "Energy fills your body. You can't help but let go. It just feels so good to let that sound rip through you.", | ||||
|   }, | ||||
|   prereqs: [] | ||||
|   prereqs: [], | ||||
| }); | ||||
| export let bat1 = table.add({ | ||||
|   governing: governing("bat", 1), | ||||
|   profile: { | ||||
|     name: "Flap", | ||||
|     description: "Sailing on your cloak is pleasurable, but it's better still to shake your limbs and FIGHT the wind." | ||||
|     description: | ||||
|       "Sailing on your cloak is pleasurable, but it's better still to shake your limbs and FIGHT the wind.", | ||||
|   }, | ||||
|   prereqs: [bat0] | ||||
|   prereqs: [bat0], | ||||
| }); | ||||
| export let bat2 = table.add({ | ||||
|   governing: governing("bat", 2), | ||||
|   profile: { | ||||
|     name: "Transform", | ||||
|     description: "Bare your fangs and let them out further than normal. Your nose wrinkles. You have a SNOUT??" | ||||
|     description: | ||||
|       "Bare your fangs and let them out further than normal. Your nose wrinkles. You have a SNOUT??", | ||||
|   }, | ||||
|   prereqs: [bat1] | ||||
|   prereqs: [bat1], | ||||
| }); | ||||
| export let bat3 = table.add({ | ||||
|   governing: governing("bat", 3), | ||||
|   profile: { | ||||
|     name: "Eat Bugs", | ||||
|     description: "This is the forbidden pleasure. It supersedes even blood. Go on -- have a bite -- CRUNCH!" | ||||
|     description: | ||||
|       "This is the forbidden pleasure. It supersedes even blood. Go on -- have a bite -- CRUNCH!", | ||||
|   }, | ||||
|   prereqs: [bat2] | ||||
|   prereqs: [bat2], | ||||
| }); | ||||
|  | ||||
| export let stealth0 = table.add({ | ||||
|   governing: governing("stealth", 0), | ||||
|   profile: { | ||||
|     name: "Be Quiet", | ||||
|     description: "There's a thing in the brain that _wants_ to be caught. Mortals have it, vampires don't." | ||||
|     description: | ||||
|       "There's a thing in the brain that _wants_ to be caught. Mortals have it, vampires don't.", | ||||
|   }, | ||||
|   prereqs: [] | ||||
|   prereqs: [], | ||||
| }); | ||||
| export let stealth1 = table.add({ | ||||
|   governing: governing("stealth", 1), | ||||
|   profile: { | ||||
|     name: "Disguise", | ||||
|     description: "First impressions: what you want them to see, they'll see. Just meet their gaze and start talking.", | ||||
|     description: | ||||
|       "First impressions: what you want them to see, they'll see. Just meet their gaze and start talking.", | ||||
|   }, | ||||
|   prereqs: [stealth0] | ||||
|   prereqs: [stealth0], | ||||
| }); | ||||
| export let stealth2 = table.add({ | ||||
|   governing: governing("stealth", 2), | ||||
|   profile: { | ||||
|     name: "Sneak", | ||||
|     description: "Your unnatural pallor _should_ make you bright against the shadow. But it likes you, so you fade." | ||||
|     description: | ||||
|       "Your unnatural pallor _should_ make you bright against the shadow. But it likes you, so you fade.", | ||||
|   }, | ||||
|   prereqs: [stealth1] | ||||
|   prereqs: [stealth1], | ||||
| }); | ||||
| export let stealth3 = table.add({ | ||||
|   governing: governing("stealth", 3), | ||||
|   profile: { | ||||
|     name: "Turn Invisible", | ||||
|     description: "No one sees any more of you than you'd like. You're as ghostly as your own reflection.", | ||||
|     description: | ||||
|       "No one sees any more of you than you'd like. You're as ghostly as your own reflection.", | ||||
|   }, | ||||
|   prereqs: [stealth2] | ||||
|   prereqs: [stealth2], | ||||
| }); | ||||
|  | ||||
| export let charm0 = table.add({ | ||||
|   governing: governing("charm", 0), | ||||
|   profile: { | ||||
|     name: "Flatter", | ||||
|     description: "No matter how weird you're being, people like praise. Praise in your voice has an intoxicating quality.", | ||||
|     description: | ||||
|       "No matter how weird you're being, people like praise. Praise in your voice has an intoxicating quality.", | ||||
|   }, | ||||
|   prereqs: [] | ||||
|   prereqs: [], | ||||
| }); | ||||
| export let charm1 = table.add({ | ||||
|   governing: governing("charm", 1), | ||||
|   profile: { | ||||
|     name: "Befriend", | ||||
|     description: "Cute: they think they've met the real you. They're even thinking about you when you're not around." | ||||
|     description: | ||||
|       "Cute: they think they've met the real you. They're even thinking about you when you're not around.", | ||||
|   }, | ||||
|   prereqs: [charm0] | ||||
|   prereqs: [charm0], | ||||
| }); | ||||
| export let charm2 = table.add({ | ||||
|   governing: governing("charm", 2), | ||||
|   profile: { | ||||
|     name: "Seduce", | ||||
|     description: "Transfix them long and deep enough for them to realize how much they want you. \"No\" isn't \"no\" anymore.", | ||||
|     description: | ||||
|       'Transfix them long and deep enough for them to realize how much they want you. "No" isn\'t "no" anymore.', | ||||
|   }, | ||||
|   prereqs: [charm1] | ||||
|   prereqs: [charm1], | ||||
| }); | ||||
| export let charm3 = table.add({ | ||||
|   governing: governing("charm", 3), | ||||
|   profile: { | ||||
|     name: "Infatuate", | ||||
|     description: "They were into mortals once. Now they can't get off without the fangs. The eyes. The pale dead flesh." | ||||
|     description: | ||||
|       "They were into mortals once. Now they can't get off without the fangs. The eyes. The pale dead flesh.", | ||||
|   }, | ||||
|   prereqs: [charm2] | ||||
|   prereqs: [charm2], | ||||
| }); | ||||
| export let stare0 = table.add({ | ||||
|   governing: governing("stare", 0), | ||||
|   profile: { | ||||
|     name: "Dazzle", | ||||
|     description: "Your little light show can reduce anyone to a puddle of their own fluids. Stare and they give in instantly.", | ||||
|     description: | ||||
|       "Your little light show can reduce anyone to a puddle of their own fluids. Stare and they give in instantly.", | ||||
|   }, | ||||
|   prereqs: [] | ||||
|   prereqs: [], | ||||
| }); | ||||
| export let stare1 = table.add({ | ||||
|   governing: governing("stare", 1), | ||||
|   profile: { | ||||
|     name: "Hypnotize", | ||||
|     description: "Say \"sleep\" and the mortal falls asleep. That is not a person: just a machine that acts when you require it." | ||||
|     description: | ||||
|       'Say "sleep" and the mortal falls asleep. That is not a person: just a machine that acts when you require it.', | ||||
|   }, | ||||
|   prereqs: [stare0] | ||||
|   prereqs: [stare0], | ||||
| }); | ||||
| export let stare2 = table.add({ | ||||
|   governing: governing("stare", 2), | ||||
|   profile: { | ||||
|     name: "Enthrall", | ||||
|     description: "Everyone's mind has room for one master. Reach into the meek fractured exterior and mean for it to be you." | ||||
|     description: | ||||
|       "Everyone's mind has room for one master. Reach into the meek fractured exterior and mean for it to be you.", | ||||
|   }, | ||||
|   prereqs: [stare1] | ||||
|   prereqs: [stare1], | ||||
| }); | ||||
| export let stare3 = table.add({ | ||||
|   governing: governing("stare", 3), | ||||
|   profile: { | ||||
|     name: "Seal Memory", | ||||
|     description: "There was no existence before you and will be none after. Your mortals cannot imagine another existence." | ||||
|     description: | ||||
|       "There was no existence before you and will be none after. Your mortals cannot imagine another existence.", | ||||
|   }, | ||||
|   prereqs: [stare2] | ||||
|   prereqs: [stare2], | ||||
| }); | ||||
| export let party0 = table.add({ | ||||
|   governing: governing("party", 0), | ||||
|   profile: { | ||||
|     name: "Chug", | ||||
|     description: "This undead body can hold SO MUCH whiskey. (BRAAAAP.) \"You, mortal -- fetch me another drink!\"" | ||||
|     description: | ||||
|       'This undead body can hold SO MUCH whiskey. (BRAAAAP.) "You, mortal -- fetch me another drink!"', | ||||
|   }, | ||||
|   prereqs: [] | ||||
|   prereqs: [], | ||||
| }); | ||||
| export let party1 = table.add({ | ||||
|   governing: governing("party", 1), | ||||
|   profile: { | ||||
|     name: "Rave", | ||||
|     description: "You could jam glowsticks in your hair, but your eyes are a lot brighter. And they pulse with the music." | ||||
|     description: | ||||
|       "You could jam glowsticks in your hair, but your eyes are a lot brighter. And they pulse with the music.", | ||||
|   }, | ||||
|   prereqs: [party0] | ||||
|   prereqs: [party0], | ||||
| }); | ||||
| export let party2 = table.add({ | ||||
|   governing: governing("party", 2), | ||||
|   profile: { | ||||
|     name: "Peer Pressure", | ||||
|     description: "Partying: it gets you out of your head. Makes you do things you wouldn't normally do. Controls you." | ||||
|     description: | ||||
|       "Partying: it gets you out of your head. Makes you do things you wouldn't normally do. Controls you.", | ||||
|   }, | ||||
|   prereqs: [party1] | ||||
|   prereqs: [party1], | ||||
| }); | ||||
| export let party3 = table.add({ | ||||
|   governing: governing("party", 3), | ||||
|   profile: { | ||||
|     name: "Sleep It Off", | ||||
|     description: "Feels good. Never want it to end. But sober up. These feelings aren't for you. They're for your prey." | ||||
|     description: | ||||
|       "Feels good. Never want it to end. But sober up. These feelings aren't for you. They're for your prey.", | ||||
|   }, | ||||
|   prereqs: [party2] | ||||
|   prereqs: [party2], | ||||
| }); | ||||
| export let lore0 = table.add({ | ||||
|   governing: governing("lore", 0), | ||||
|   profile: { | ||||
|     name: "Respect Elders", | ||||
|     description: "You're told not to bother learning much. The test is not to believe that. Bad vampires _disappear_." | ||||
|     description: | ||||
|       "You're told not to bother learning much. The test is not to believe that. Bad vampires _disappear_.", | ||||
|   }, | ||||
|   prereqs: [] | ||||
|   prereqs: [], | ||||
| }); | ||||
| export let lore1 = table.add({ | ||||
|   governing: governing("lore", 1), | ||||
|   profile: { | ||||
|     name: "Brick by Brick", | ||||
|     description: "Vampire history is a mix of fact and advice. Certain tips -- \"live in a castle\" -- seem very concrete." | ||||
|     description: | ||||
|       'Vampire history is a mix of fact and advice. Certain tips -- "live in a castle" -- seem very concrete.', | ||||
|   }, | ||||
|   prereqs: [lore0] | ||||
|   prereqs: [lore0], | ||||
| }); | ||||
| export let lore2 = table.add({ | ||||
|   governing: governing("lore", 2), | ||||
|   profile: { | ||||
|     name: "Make Wine", | ||||
|     description: "Fruit bats grow the grapes. Insectivores fertilize the soil. What do vampire bats do? Is this a metaphor?" | ||||
|     description: | ||||
|       "Fruit bats grow the grapes. Insectivores fertilize the soil. What do vampire bats do? Is this a metaphor?", | ||||
|   }, | ||||
|   prereqs: [lore1] | ||||
|   prereqs: [lore1], | ||||
| }); | ||||
| export let lore3 = table.add({ | ||||
|   governing: governing("lore", 3), | ||||
|   profile: { | ||||
|     name: "Third Clade", | ||||
|     description: "Mortals love the day. They hate the night. There's a night deeper than any night that cannot be discussed." | ||||
|     description: | ||||
|       "Mortals love the day. They hate the night. There's a night deeper than any night that cannot be discussed.", | ||||
|   }, | ||||
|   prereqs: [lore2] | ||||
|   prereqs: [lore2], | ||||
| }); | ||||
|  | ||||
| export let sorry0 = table.add({ | ||||
| @@ -347,20 +427,21 @@ export let sorry0 = table.add({ | ||||
|   governing: governing("penance", 0, true), | ||||
|   profile: { | ||||
|     name: "I'm Sorry", | ||||
|     description: "You really hurt your Master, you know? Shame on you." | ||||
|     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." | ||||
|     description: | ||||
|       "You should have known better! You should have done what you were told.", | ||||
|   }, | ||||
|   prereqs: [], | ||||
| }) | ||||
| }); | ||||
|  | ||||
| export let sorry2 = table.add({ | ||||
|   isDegrading: true, | ||||
| @@ -368,11 +449,12 @@ export let sorry2 = table.add({ | ||||
|   governing: governing("penance", 1.25, true), | ||||
|   profile: { | ||||
|     name: "Forgive Me", | ||||
|     description: "Nothing you say will ever be enough to make up for your indiscretion.", | ||||
|     description: | ||||
|       "Nothing you say will ever be enough to make up for your indiscretion.", | ||||
|   }, | ||||
|   prereqs: [], | ||||
| }) | ||||
| }); | ||||
|  | ||||
| export function getSkills(): SkillsTable { | ||||
|   return table; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,12 @@ | ||||
| import {getPartLocation, withCamera} from "./layout.ts"; | ||||
| import {AlignX, Point, Rect, Size} from "./engine/datatypes.ts"; | ||||
| import {DrawPile} from "./drawpile.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| import {BG_INSET, FG_BOLD, FG_TEXT} from "./colors.ts"; | ||||
| import {addButton} from "./button.ts"; | ||||
| import { | ||||
|   getSkills, | ||||
| } from "./skills.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
| import {Skill, SkillData} from "./datatypes.ts"; | ||||
| import { getPartLocation, withCamera } from "./layout.ts"; | ||||
| import { AlignX, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||
| import { DrawPile } from "./drawpile.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
| import { BG_INSET, FG_BOLD, FG_TEXT } from "./colors.ts"; | ||||
| import { addButton } from "./button.ts"; | ||||
| import { getSkills } from "./skills.ts"; | ||||
| import { getPlayerProgress } from "./playerprogress.ts"; | ||||
| import { Skill, SkillData } from "./datatypes.ts"; | ||||
|  | ||||
| export class SkillsModal { | ||||
|   #drawpile: DrawPile; | ||||
| @@ -24,7 +22,7 @@ export class SkillsModal { | ||||
|   get #size(): Size { | ||||
|     // Instead of calculating this here, compute it from outside | ||||
|     // as it has to be the same for every bottom modal | ||||
|     return getPartLocation("BottomModal").size | ||||
|     return getPartLocation("BottomModal").size; | ||||
|   } | ||||
|  | ||||
|   get isShown(): boolean { | ||||
| @@ -32,23 +30,23 @@ export class SkillsModal { | ||||
|   } | ||||
|  | ||||
|   setShown(shown: boolean) { | ||||
|     this.#shown = shown | ||||
|     this.#shown = shown; | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     withCamera("BottomModal", () => this.#update()) | ||||
|     withCamera("BottomModal", () => this.#update()); | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     withCamera("BottomModal", () => this.#draw()) | ||||
|     withCamera("BottomModal", () => this.#draw()); | ||||
|   } | ||||
|  | ||||
|   #update() { | ||||
|     this.#drawpile.clear(); | ||||
|     let size = this.#size | ||||
|     let size = this.#size; | ||||
|     this.#drawpile.add(0, () => { | ||||
|       D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET) | ||||
|     }) | ||||
|       D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET); | ||||
|     }); | ||||
|  | ||||
|     // draw skills | ||||
|     let availableSkills = getPlayerProgress().getAvailableSkills(); | ||||
| @@ -61,7 +59,7 @@ export class SkillsModal { | ||||
|       let cost = getSkills().computeCost(skill); | ||||
|       let y_ = y; | ||||
|       let selected = this.#skillSelection?.id == skill.id; | ||||
|       let skillRect = new Rect(new Point(0,  y_), new Size(160 + 4, 16)); | ||||
|       let skillRect = new Rect(new Point(0, y_), new Size(160 + 4, 16)); | ||||
|       let enabled = true; | ||||
|  | ||||
|       this.#drawpile.addClickable( | ||||
| @@ -74,14 +72,16 @@ export class SkillsModal { | ||||
|           } | ||||
|           D.fillRect(skillRect.top, skillRect.size, bg); | ||||
|           D.drawText(data.profile.name, new Point(4, y_), fg); | ||||
|           D.drawText("" + cost, new Point(160 - 4, y_), fg, {alignX: AlignX.Right}); | ||||
|           D.drawText("" + cost, new Point(160 - 4, y_), fg, { | ||||
|             alignX: AlignX.Right, | ||||
|           }); | ||||
|         }, | ||||
|         skillRect, | ||||
|         enabled, | ||||
|         () => { | ||||
|           this.#skillSelection = skill; | ||||
|         } | ||||
|       ) | ||||
|         }, | ||||
|       ); | ||||
|       y += 16; | ||||
|     } | ||||
|  | ||||
| @@ -94,14 +94,19 @@ export class SkillsModal { | ||||
|       let remainingWidth = size.w - 160; | ||||
|  | ||||
|       this.#drawpile.add(0, () => { | ||||
|         D.fillRect(new Point(160, 0), new Size(remainingWidth, 96), FG_BOLD) | ||||
|         D.drawText(createFullDescription(data), new Point(164, 0), BG_INSET, {forceWidth: remainingWidth - 8}); | ||||
|         D.fillRect(new Point(160, 0), new Size(remainingWidth, 96), FG_BOLD); | ||||
|         D.drawText(createFullDescription(data), new Point(164, 0), BG_INSET, { | ||||
|           forceWidth: remainingWidth - 8, | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|       // add learn button | ||||
|       let drawButtonRect = new Rect(new Point(160, 96), new Size(remainingWidth, 32)) | ||||
|       let drawButtonRect = new Rect( | ||||
|         new Point(160, 96), | ||||
|         new Size(remainingWidth, 32), | ||||
|       ); | ||||
|       let canAfford = getPlayerProgress().getExperience() >= cost; | ||||
|       let caption = `Learn ${data.profile.name}` | ||||
|       let caption = `Learn ${data.profile.name}`; | ||||
|       if (!canAfford) { | ||||
|         caption = `Can't Afford`; | ||||
|       } | ||||
| @@ -109,15 +114,14 @@ export class SkillsModal { | ||||
|       addButton(this.#drawpile, caption, drawButtonRect, canAfford, () => { | ||||
|         getPlayerProgress().spendExperience(cost); | ||||
|         getPlayerProgress().learnSkill(selection); | ||||
|       }) | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // add close button | ||||
|     let closeRect = new Rect(new Point(0, 96), new Size(160, 32)) | ||||
|     let closeRect = new Rect(new Point(0, 96), new Size(160, 32)); | ||||
|     addButton(this.#drawpile, "Back", closeRect, true, () => { | ||||
|       this.setShown(false); | ||||
|     }) | ||||
|     }); | ||||
|     this.#drawpile.executeOnClick(); | ||||
|   } | ||||
|  | ||||
| @@ -150,5 +154,5 @@ export function getSkillsModal(): SkillsModal { | ||||
| } | ||||
|  | ||||
| function createFullDescription(data: SkillData) { | ||||
|   return data.profile.description + "\n\n" + data.governing.note | ||||
| } | ||||
|   return data.profile.description + "\n\n" + data.governing.note; | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import {DrawPile} from "./drawpile.ts"; | ||||
| import {Point, Rect, Size} from "./engine/datatypes.ts"; | ||||
| import {getPartLocation, withCamera} from "./layout.ts"; | ||||
| import {addButton} from "./button.ts"; | ||||
| import {D} from "./engine/public.ts"; | ||||
| import {BG_INSET} from "./colors.ts"; | ||||
| import {getSkillsModal} from "./skillsmodal.ts"; | ||||
| import {getStateManager} from "./statemanager.ts"; | ||||
| import { DrawPile } from "./drawpile.ts"; | ||||
| import { Point, Rect, Size } from "./engine/datatypes.ts"; | ||||
| import { getPartLocation, withCamera } from "./layout.ts"; | ||||
| import { addButton } from "./button.ts"; | ||||
| import { D } from "./engine/public.ts"; | ||||
| import { BG_INSET } from "./colors.ts"; | ||||
| import { getSkillsModal } from "./skillsmodal.ts"; | ||||
| import { getStateManager } from "./statemanager.ts"; | ||||
|  | ||||
| export class SleepModal { | ||||
|   #drawpile: DrawPile; | ||||
| @@ -20,7 +20,7 @@ export class SleepModal { | ||||
|     // We share this logic with SkillModal: | ||||
|     // Instead of calculating this here, compute it from outside | ||||
|     // as it has to be the same for every bottom modal | ||||
|     return getPartLocation("BottomModal").size | ||||
|     return getPartLocation("BottomModal").size; | ||||
|   } | ||||
|  | ||||
|   get isShown(): boolean { | ||||
| @@ -28,35 +28,34 @@ export class SleepModal { | ||||
|   } | ||||
|  | ||||
|   setShown(shown: boolean) { | ||||
|     this.#shown = shown | ||||
|     this.#shown = shown; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   update() { | ||||
|     withCamera("BottomModal", () => this.#update()) | ||||
|     withCamera("BottomModal", () => this.#update()); | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     withCamera("BottomModal", () => this.#draw()) | ||||
|     withCamera("BottomModal", () => this.#draw()); | ||||
|   } | ||||
|  | ||||
|   #update() { | ||||
|     this.#drawpile.clear(); | ||||
|     let size = this.#size | ||||
|     let size = this.#size; | ||||
|     this.#drawpile.add(0, () => { | ||||
|       D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET) | ||||
|     }) | ||||
|       D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET); | ||||
|     }); | ||||
|  | ||||
|     // add close button | ||||
|     let closeRect = new Rect(new Point(0, 96), new Size(80, 32)) | ||||
|     let closeRect = new Rect(new Point(0, 96), new Size(80, 32)); | ||||
|     addButton(this.#drawpile, "Back", closeRect, true, () => { | ||||
|       this.setShown(false); | ||||
|     }) | ||||
|     }); | ||||
|  | ||||
|     let skillsRect = new Rect(new Point(80, 96), new Size(80, 32)); | ||||
|     addButton(this.#drawpile, "Skills", skillsRect, true, () => { | ||||
|       getSkillsModal().setShown(true); | ||||
|     }) | ||||
|     }); | ||||
|  | ||||
|     let remainingWidth = size.w - 160; | ||||
|     let nextRect = new Rect(new Point(160, 96), new Size(remainingWidth, 32)); | ||||
| @@ -75,4 +74,4 @@ export class SleepModal { | ||||
| let active = new SleepModal(); | ||||
| export function getSleepModal(): SleepModal { | ||||
|   return active; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import {Sprite} from "./engine/internal/sprite.ts"; | ||||
| import { Sprite } from "./engine/internal/sprite.ts"; | ||||
|  | ||||
| import imgRaccoon from "./art/characters/raccoon.png"; | ||||
| import imgResourcePickup from "./art/pickups/resources.png"; | ||||
| import imgStatPickup from "./art/pickups/stats.png"; | ||||
| import imgLadder from "./art/pickups/ladder.png"; | ||||
| import imgLock from "./art/pickups/lock.png"; | ||||
| import {Point, Size} from "./engine/datatypes.ts"; | ||||
| import { Point, Size } from "./engine/datatypes.ts"; | ||||
|  | ||||
| import imgThrallBat from "./art/thralls/thrall_bat.png"; | ||||
| import imgThrallCharm from "./art/thralls/thrall_charm.png"; | ||||
| @@ -14,36 +14,84 @@ import imgThrallParty from "./art/thralls/thrall_party.png"; | ||||
| import imgThrallStare from "./art/thralls/thrall_stare.png"; | ||||
| import imgThrallStealth from "./art/thralls/thrall_stealth.png"; | ||||
|  | ||||
|  | ||||
| export let sprRaccoon = new Sprite( | ||||
|   imgRaccoon, | ||||
|   new Size(64, 64), new Point(32, 32), new Size(1, 1), | ||||
|   1 | ||||
|   new Size(64, 64), | ||||
|   new Point(32, 32), | ||||
|   new Size(1, 1), | ||||
|   1, | ||||
| ); | ||||
| export let sprResourcePickup = new Sprite( | ||||
|   imgResourcePickup, new Size(32, 32), new Point(16, 16), | ||||
|   new Size(1, 1), 1 | ||||
|   imgResourcePickup, | ||||
|   new Size(32, 32), | ||||
|   new Point(16, 16), | ||||
|   new Size(1, 1), | ||||
|   1, | ||||
| ); | ||||
|  | ||||
| export let sprStatPickup = new Sprite( | ||||
|   imgStatPickup, new Size(32, 32), new Point(16, 16), | ||||
|   new Size(4, 1), 4 | ||||
|   imgStatPickup, | ||||
|   new Size(32, 32), | ||||
|   new Point(16, 16), | ||||
|   new Size(4, 1), | ||||
|   4, | ||||
| ); | ||||
|  | ||||
| export let sprLadder = new Sprite( | ||||
|   imgLadder, new Size(16, 16), new Point(8, 8), | ||||
|   new Size(1, 1), 1 | ||||
|   imgLadder, | ||||
|   new Size(16, 16), | ||||
|   new Point(8, 8), | ||||
|   new Size(1, 1), | ||||
|   1, | ||||
| ); | ||||
|  | ||||
| export let sprLock = new Sprite( | ||||
|   imgLock, new Size(16, 16), new Point(8, 8), | ||||
|   new Size(1, 1), 1 | ||||
|   imgLock, | ||||
|   new Size(16, 16), | ||||
|   new Point(8, 8), | ||||
|   new Size(1, 1), | ||||
|   1, | ||||
| ); | ||||
|  | ||||
|  | ||||
| export let sprThrallBat = new Sprite(imgThrallBat, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); | ||||
| export let sprThrallCharm = new Sprite(imgThrallCharm, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); | ||||
| export let sprThrallLore = new Sprite(imgThrallLore, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); | ||||
| export let sprThrallParty = new Sprite(imgThrallParty, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); | ||||
| export let sprThrallStare = new Sprite(imgThrallStare, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); | ||||
| export let sprThrallStealth = new Sprite(imgThrallStealth, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); | ||||
| export let sprThrallBat = new Sprite( | ||||
|   imgThrallBat, | ||||
|   new Size(24, 24), | ||||
|   new Point(12, 12), | ||||
|   new Size(3, 1), | ||||
|   3, | ||||
| ); | ||||
| export let sprThrallCharm = new Sprite( | ||||
|   imgThrallCharm, | ||||
|   new Size(24, 24), | ||||
|   new Point(12, 12), | ||||
|   new Size(3, 1), | ||||
|   3, | ||||
| ); | ||||
| export let sprThrallLore = new Sprite( | ||||
|   imgThrallLore, | ||||
|   new Size(24, 24), | ||||
|   new Point(12, 12), | ||||
|   new Size(3, 1), | ||||
|   3, | ||||
| ); | ||||
| export let sprThrallParty = new Sprite( | ||||
|   imgThrallParty, | ||||
|   new Size(24, 24), | ||||
|   new Point(12, 12), | ||||
|   new Size(3, 1), | ||||
|   3, | ||||
| ); | ||||
| export let sprThrallStare = new Sprite( | ||||
|   imgThrallStare, | ||||
|   new Size(24, 24), | ||||
|   new Point(12, 12), | ||||
|   new Size(3, 1), | ||||
|   3, | ||||
| ); | ||||
| export let sprThrallStealth = new Sprite( | ||||
|   imgThrallStealth, | ||||
|   new Size(24, 24), | ||||
|   new Point(12, 12), | ||||
|   new Size(3, 1), | ||||
|   3, | ||||
| ); | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import {getPlayerProgress, initPlayerProgress} from "./playerprogress.ts"; | ||||
| import {getHuntMode, HuntMode, initHuntMode} from "./huntmode.ts"; | ||||
| import {getSleepModal} from "./sleepmodal.ts"; | ||||
| import {getVNModal} from "./vnmodal.ts"; | ||||
| import {getScorer} from "./scorer.ts"; | ||||
| import {getEndgameModal} from "./endgamemodal.ts"; | ||||
| import {SuccessorOption, Wish} from "./datatypes.ts"; | ||||
| import {generateManor} from "./manormap.ts"; | ||||
| import { getPlayerProgress, initPlayerProgress } from "./playerprogress.ts"; | ||||
| import { getHuntMode, HuntMode, initHuntMode } from "./huntmode.ts"; | ||||
| import { getSleepModal } from "./sleepmodal.ts"; | ||||
| import { getVNModal } from "./vnmodal.ts"; | ||||
| import { getScorer } from "./scorer.ts"; | ||||
| import { getEndgameModal } from "./endgamemodal.ts"; | ||||
| import { SuccessorOption, Wish } from "./datatypes.ts"; | ||||
| import { generateManor } from "./manormap.ts"; | ||||
|  | ||||
| const N_TURNS: number = 9; | ||||
|  | ||||
| @@ -17,7 +17,7 @@ export class StateManager { | ||||
|   } | ||||
|  | ||||
|   getTurn(): number { | ||||
|     return this.#turn | ||||
|     return this.#turn; | ||||
|   } | ||||
|  | ||||
|   startGame(asSuccessor: SuccessorOption, withWish: Wish | null) { | ||||
| @@ -43,11 +43,11 @@ export class StateManager { | ||||
|   } | ||||
|  | ||||
|   getMaxTurns() { | ||||
|     return N_TURNS | ||||
|     return N_TURNS; | ||||
|   } | ||||
| } | ||||
|  | ||||
| let active: StateManager = new StateManager(); | ||||
| export function getStateManager(): StateManager { | ||||
|   return active | ||||
| } | ||||
|   return active; | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| 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"; | ||||
| 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[] { | ||||
| export function generateSuccessors( | ||||
|   nImprovements: number, | ||||
|   penance: boolean, | ||||
| ): SuccessorOption[] { | ||||
|   if (penance) { | ||||
|     return [generateSuccessorFromPlayer()]; | ||||
|   } | ||||
| @@ -34,12 +37,12 @@ export function generateSuccessorFromPlayer(): SuccessorOption { | ||||
|     name: progress.name, | ||||
|     title: "Penitent", | ||||
|     note: "Failed at Master's bidding", | ||||
|     stats: {...progress.getStats()}, | ||||
|     talents: {...progress.getTalents()}, | ||||
|     stats: { ...progress.getStats() }, | ||||
|     talents: { ...progress.getTalents() }, | ||||
|     skills: [...progress.getLearnedSkills()], | ||||
|     inPenance: true, | ||||
|     isCompulsory: true, | ||||
|   } | ||||
|   }; | ||||
|  | ||||
|   for (let stat of ALL_STATS.values()) { | ||||
|     successor.talents[stat] = -8; | ||||
| @@ -52,30 +55,35 @@ export function generateSuccessor(nImprovements: number): SuccessorOption { | ||||
|   let title = generateTitle(); | ||||
|   let note = null; | ||||
|   let stats: Record<Stat, number> = { | ||||
|     "AGI": 10 + choose([1, 2]), | ||||
|     "INT": 10 + choose([1, 2]), | ||||
|     "CHA": 10 + choose([1, 2]), | ||||
|     "PSI": 10 + choose([1, 2]), | ||||
|   } | ||||
|     AGI: 10 + choose([1, 2]), | ||||
|     INT: 10 + choose([1, 2]), | ||||
|     CHA: 10 + choose([1, 2]), | ||||
|     PSI: 10 + choose([1, 2]), | ||||
|   }; | ||||
|   let talents: Record<Stat, number> = { | ||||
|     "AGI": 0, | ||||
|     "INT": 0, | ||||
|     "CHA": 0, | ||||
|     "PSI": 0, | ||||
|   } | ||||
|     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; }, | ||||
|     () => { | ||||
|       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)]; | ||||
|     let improvement = | ||||
|       improvements[Math.floor(Math.random() * improvements.length)]; | ||||
|     improvement(); | ||||
|   } | ||||
|  | ||||
|   let skills: Skill[] = []; | ||||
|   let inPenance = false; | ||||
|   let isCompulsory = false; | ||||
|   return {name, title, note, stats, talents, skills, inPenance, isCompulsory}; | ||||
| } | ||||
|   return { name, title, note, stats, talents, skills, inPenance, isCompulsory }; | ||||
| } | ||||
|   | ||||
							
								
								
									
										261
									
								
								src/thralls.ts
									
									
									
									
									
								
							
							
						
						
									
										261
									
								
								src/thralls.ts
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| import {CheckData} from "./newmap.ts"; | ||||
| import { CheckData } from "./newmap.ts"; | ||||
| import { | ||||
|   bat0, | ||||
|   bat1, | ||||
| @@ -11,7 +11,7 @@ import { | ||||
|   stare0, | ||||
|   stare1, | ||||
|   stealth0, | ||||
|   stealth1 | ||||
|   stealth1, | ||||
| } from "./skills.ts"; | ||||
| import { | ||||
|   sprThrallBat, | ||||
| @@ -19,16 +19,16 @@ import { | ||||
|   sprThrallLore, | ||||
|   sprThrallParty, | ||||
|   sprThrallStare, | ||||
|   sprThrallStealth | ||||
|   sprThrallStealth, | ||||
| } from "./sprites.ts"; | ||||
| import {Sprite} from "./engine/internal/sprite.ts"; | ||||
| import { Sprite } from "./engine/internal/sprite.ts"; | ||||
|  | ||||
| export type Thrall = { | ||||
|   id: number | ||||
| } | ||||
|   id: number; | ||||
| }; | ||||
|  | ||||
| class ThrallsTable { | ||||
|   #thralls: ThrallData[] | ||||
|   #thralls: ThrallData[]; | ||||
|  | ||||
|   constructor() { | ||||
|     this.#thralls = []; | ||||
| @@ -37,29 +37,29 @@ class ThrallsTable { | ||||
|   add(data: ThrallData) { | ||||
|     let id = this.#thralls.length; | ||||
|     this.#thralls.push(data); | ||||
|     return {id}; | ||||
|     return { id }; | ||||
|   } | ||||
|  | ||||
|   get(thrall: Thrall): ThrallData { | ||||
|     return this.#thralls[thrall.id] | ||||
|     return this.#thralls[thrall.id]; | ||||
|   } | ||||
|  | ||||
|   getAll(): Thrall[] { | ||||
|     let thralls = []; | ||||
|     for (let id = 0; id < this.#thralls.length; id++) { | ||||
|       thralls.push({id}) | ||||
|       thralls.push({ id }); | ||||
|     } | ||||
|     return thralls; | ||||
|   } | ||||
| } | ||||
| export type ThrallData = { | ||||
|   label: string, | ||||
|   sprite: Sprite, | ||||
|   posterCheck: CheckData, | ||||
|   initialCheck: CheckData, | ||||
|   label: string; | ||||
|   sprite: Sprite; | ||||
|   posterCheck: CheckData; | ||||
|   initialCheck: CheckData; | ||||
|  | ||||
|   lifeStageText: Record<LifeStage, LifeStageText> | ||||
| } | ||||
|   lifeStageText: Record<LifeStage, LifeStageText>; | ||||
| }; | ||||
|  | ||||
| export enum LifeStage { | ||||
|   Fresh = "fresh", | ||||
| @@ -70,9 +70,9 @@ export enum LifeStage { | ||||
| } | ||||
|  | ||||
| export type LifeStageText = { | ||||
|   prebite: string, | ||||
|   postbite: string, | ||||
| } | ||||
|   prebite: string; | ||||
|   postbite: string; | ||||
| }; | ||||
|  | ||||
| let table = new ThrallsTable(); | ||||
|  | ||||
| @@ -88,27 +88,30 @@ export let thrallParty = table.add({ | ||||
|   label: "Garrett", | ||||
|   sprite: sprThrallParty, | ||||
|   posterCheck: { | ||||
|     label: "This room would be perfect for someone with an ostensibly managed gambling addiction.", | ||||
|     label: | ||||
|       "This room would be perfect for someone with an ostensibly managed gambling addiction.", | ||||
|     options: [], | ||||
|   }, | ||||
|   initialCheck: { | ||||
|     label: "That's Garrett. He plays poker, but he goes to the zoo to cool down after he's lost a lot of chips. His ice cream cone has melted.", | ||||
|     label: | ||||
|       "That's Garrett. He plays poker, but he goes to the zoo to cool down after he's lost a lot of chips. His ice cream cone has melted.", | ||||
|     options: [ | ||||
|       { | ||||
|         skill: () => stealth1,  // Disguise | ||||
|         locked: "\"What's wrong, Garrett?\"", | ||||
|         failure: "\"If you're not a large pile of money, don't talk to me.\"\n\nHe sobs into his ice cream.", | ||||
|         skill: () => stealth1, // Disguise | ||||
|         locked: '"What\'s wrong, Garrett?"', | ||||
|         failure: | ||||
|           "\"If you're not a large pile of money, don't talk to me.\"\n\nHe sobs into his ice cream.", | ||||
|         unlockable: "*look like a large pile of money*", | ||||
|         success: "He scoops you eagerly into his wallet.", | ||||
|       }, | ||||
|       { | ||||
|         skill: () => lore0,  // Respect Elders | ||||
|         skill: () => lore0, // Respect Elders | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "TODO", | ||||
|         success: "TODO", | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
| @@ -116,49 +119,61 @@ export let thrallParty = table.add({ | ||||
|       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." | ||||
|       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.", | ||||
|       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.", | ||||
|       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...", | ||||
|     } | ||||
|       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({ | ||||
|   label: "Lupin", | ||||
|   sprite: sprThrallLore, | ||||
|   posterCheck: { | ||||
|     label: "This room would be perfect for someone with a love of nature and screaming.", | ||||
|     label: | ||||
|       "This room would be perfect for someone with a love of nature and screaming.", | ||||
|     options: [], | ||||
|   }, | ||||
|   initialCheck: { | ||||
|     label: "That's Lupin. He's a Wolf Scout, but hardcore about it. I'm not sure he knows he's a raccoon.", | ||||
|     label: | ||||
|       "That's Lupin. He's a Wolf Scout, but hardcore about it. I'm not sure he knows he's a raccoon.", | ||||
|     options: [ | ||||
|       { | ||||
|         skill: () => stare1, // Hypnotize | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "\"I'm a wolf too.\"", | ||||
|         success: "He blinks a few times under your gaze -- then touches your muzzle -- then his own -- then arfs submissively.", | ||||
|         unlockable: '"I\'m a wolf too."', | ||||
|         success: | ||||
|           "He blinks a few times under your gaze -- then touches your muzzle -- then his own -- then arfs submissively.", | ||||
|       }, | ||||
|       { | ||||
|         skill: () => bat0, // Screech | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "\"Wolf Scouts AWOO!\"", | ||||
|         success: "Taken aback at how well you know the cheer, he freezes -- then joins you with a similar howl.", | ||||
|         unlockable: '"Wolf Scouts AWOO!"', | ||||
|         success: | ||||
|           "Taken aback at how well you know the cheer, he freezes -- then joins you with a similar howl.", | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
| @@ -166,23 +181,29 @@ export let thrallLore = table.add({ | ||||
|       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...", | ||||
|       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.", | ||||
|       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.", | ||||
|       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?", | ||||
|       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({ | ||||
|   label: "Monica", | ||||
| @@ -192,23 +213,26 @@ export let thrallBat = table.add({ | ||||
|     options: [], | ||||
|   }, | ||||
|   initialCheck: { | ||||
|     label: "That's Monica. You've seen her cook on TV! Looks like she's enjoying a kiwi flan.", | ||||
|     label: | ||||
|       "That's Monica. You've seen her cook on TV! Looks like she's enjoying a kiwi flan.", | ||||
|     options: [ | ||||
|       { | ||||
|         skill: () => party1, // Rave | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "Slide her a sachet of cocaine.", | ||||
|         success: "\"No way. Ketamine if you've got it.\" You do.\n\n(It's not effective on vampires.)", | ||||
|         success: | ||||
|           "\"No way. Ketamine if you've got it.\" You do.\n\n(It's not effective on vampires.)", | ||||
|       }, | ||||
|       { | ||||
|         skill: () => charm0, // Flatter | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "\"You're the best cook ever!\"", | ||||
|         success: "\"Settle down!\" she says, lowering your volume with a sweep of her hand. \"It's true though.\"", | ||||
|         unlockable: '"You\'re the best cook ever!"', | ||||
|         success: | ||||
|           '"Settle down!" she says, lowering your volume with a sweep of her hand. "It\'s true though."', | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
| @@ -216,73 +240,89 @@ export let thrallBat = table.add({ | ||||
|       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.", | ||||
|       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.", | ||||
|       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.", | ||||
|       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({ | ||||
|   label: "Renfield", | ||||
|   sprite: sprThrallCharm, | ||||
|   posterCheck: { | ||||
|     label: "This room would be perfect for someone who likes vampires even more than you enjoy being a vampire.", | ||||
|     label: | ||||
|       "This room would be perfect for someone who likes vampires even more than you enjoy being a vampire.", | ||||
|     options: [], | ||||
|   }, | ||||
|   initialCheck: { | ||||
|     label: "Doesn't this guy seem a little creepy? His nametag says Renfield. Not sure you should trust him...", | ||||
|     label: | ||||
|       "Doesn't this guy seem a little creepy? His nametag says Renfield. Not sure you should trust him...", | ||||
|     options: [ | ||||
|       { | ||||
|         skill: () => lore1,  // Brick by Brick | ||||
|         skill: () => lore1, // Brick by Brick | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "\"Wanna see my crypt?\"", | ||||
|         success: "He salivates -- swallowing hard before he manages, in response to the prospect, a firm \"YES!\"", | ||||
|         unlockable: '"Wanna see my crypt?"', | ||||
|         success: | ||||
|           'He salivates -- swallowing hard before he manages, in response to the prospect, a firm "YES!"', | ||||
|       }, | ||||
|       { | ||||
|         skill: () => stealth0,  // Be Quiet | ||||
|         skill: () => stealth0, // Be Quiet | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "Say absolutely nothing.", | ||||
|         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.", | ||||
|         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.", | ||||
|       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?", | ||||
|       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.", | ||||
|       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.", | ||||
|       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.", | ||||
|     } | ||||
|       postbite: | ||||
|         "At least in death he can't backslide on his promise to feed you.", | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| }); | ||||
|  | ||||
| export let thrallStealth = table.add({ | ||||
|   label: "Narthyss", | ||||
| @@ -292,47 +332,54 @@ export let thrallStealth = table.add({ | ||||
|     options: [], | ||||
|   }, | ||||
|   initialCheck: { | ||||
|     label: "Narthyss (dragon, heiress) actually owns the club, so she probably wouldn't talk to you... Would she?", | ||||
|     label: | ||||
|       "Narthyss (dragon, heiress) actually owns the club, so she probably wouldn't talk to you... Would she?", | ||||
|     options: [ | ||||
|       { | ||||
|         skill: () => bat1,  // Flap | ||||
|         skill: () => bat1, // Flap | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "Hang upside-down and offer her a martini.", | ||||
|         success: "\"You're ADORABLE!\" She's yours forever.", | ||||
|       }, | ||||
|       { | ||||
|         skill: () => stare0,  // Dazzle | ||||
|         skill: () => stare0, // Dazzle | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "TODO", | ||||
|         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.", | ||||
|       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.", | ||||
|       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.", | ||||
|       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.", | ||||
|       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.", | ||||
|     } | ||||
|       postbite: | ||||
|         "Dragons decay slowly. There's still some warmth in there if you bury your fangs deep enough.", | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| }); | ||||
|  | ||||
| export let thrallStare = table.add({ | ||||
|   label: "Ridley", | ||||
| @@ -342,44 +389,50 @@ export let thrallStare = table.add({ | ||||
|     options: [], | ||||
|   }, | ||||
|   initialCheck: { | ||||
|     label: "Ridley is the library's catalogue system. It can give you an incorrect answer to any question. (It has a couple gears loose.)", | ||||
|     label: | ||||
|       "Ridley is the library's catalogue system. It can give you an incorrect answer to any question. (It has a couple gears loose.)", | ||||
|     options: [ | ||||
|       { | ||||
|         skill: () => charm1,  // Befriend | ||||
|         skill: () => charm1, // Befriend | ||||
|         locked: "\"How many Rs in 'strawberry'?\"", | ||||
|         failure: "It generates an image of a sad fruit shrugging in a muddy plantation.", | ||||
|         failure: | ||||
|           "It generates an image of a sad fruit shrugging in a muddy plantation.", | ||||
|         unlockable: "TODO", | ||||
|         success: "TODO", | ||||
|       }, | ||||
|       { | ||||
|         skill: () => party0,  // Chug | ||||
|         skill: () => party0, // Chug | ||||
|         locked: "TODO", | ||||
|         failure: "TODO", | ||||
|         unlockable: "Drink a whole bottle of ink.", | ||||
|         success: "TODO", | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   lifeStageText: { | ||||
|     fresh: { | ||||
|       prebite: "Ridley is solving math problems.", | ||||
|       postbite: "You delicately sip electronic blood from the robot's neck." | ||||
|       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." | ||||
|       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...", | ||||
|       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.", | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
| }); | ||||
|   | ||||
							
								
								
									
										12
									
								
								src/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/utils.ts
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| export function choose<T>(array: Array<T>): T  { | ||||
| export function choose<T>(array: Array<T>): T { | ||||
|   if (array.length == 0) { | ||||
|     throw new Error(`array cannot have length 0 for choose`); | ||||
|   } | ||||
|   return array[Math.floor(Math.random() * array.length)] | ||||
|   return array[Math.floor(Math.random() * array.length)]; | ||||
| } | ||||
|  | ||||
| export function shuffle<T>(array: Array<T>) { | ||||
| @@ -12,7 +12,9 @@ export function shuffle<T>(array: Array<T>) { | ||||
|     let randomIndex = Math.floor(Math.random() * currentIndex); | ||||
|     currentIndex--; | ||||
|  | ||||
|     [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; | ||||
|     [array[currentIndex], array[randomIndex]] = [ | ||||
|       array[randomIndex], | ||||
|       array[currentIndex], | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import {Stat} from "./datatypes.ts"; | ||||
| import { Stat } from "./datatypes.ts"; | ||||
| import { | ||||
|   bat0, | ||||
|   bat1, | ||||
|   bat2, | ||||
|   charm0, charm1, | ||||
|   charm0, | ||||
|   charm1, | ||||
|   charm2, | ||||
|   lore0, | ||||
|   lore1, | ||||
| @@ -16,219 +17,299 @@ import { | ||||
|   stare2, | ||||
|   stealth0, | ||||
|   stealth1, | ||||
|   stealth2 | ||||
|   stealth2, | ||||
| } from "./skills.ts"; | ||||
| import {CheckData} from "./newmap.ts"; | ||||
| import {Thrall, thrallBat, thrallCharm, thrallLore, thrallParty, thrallStare, thrallStealth} from "./thralls.ts"; | ||||
|  | ||||
|  | ||||
| import { CheckData } from "./newmap.ts"; | ||||
| import { | ||||
|   Thrall, | ||||
|   thrallBat, | ||||
|   thrallCharm, | ||||
|   thrallLore, | ||||
|   thrallParty, | ||||
|   thrallStare, | ||||
|   thrallStealth, | ||||
| } from "./thralls.ts"; | ||||
|  | ||||
| export type VaultTemplate = { | ||||
|   stats: {primary: Stat, secondary: Stat}, | ||||
|   thrall: () => Thrall, | ||||
|   checks: [CheckData, CheckData] | ||||
| } | ||||
|   stats: { primary: Stat; secondary: Stat }; | ||||
|   thrall: () => Thrall; | ||||
|   checks: [CheckData, CheckData]; | ||||
| }; | ||||
|  | ||||
| export const standardVaultTemplates: VaultTemplate[] = [ | ||||
|   { | ||||
|     // zoo | ||||
|     stats: {primary: "AGI", secondary: "PSI"}, | ||||
|     stats: { primary: "AGI", secondary: "PSI" }, | ||||
|     thrall: () => thrallParty, | ||||
|     checks: [ | ||||
|       { | ||||
|         label: "You're blocked from further access by a sturdy-looking brick wall. Playful bats swoop close to the alligators behind the bars.", | ||||
|         options: [{ | ||||
|           skill: () => lore1, | ||||
|           locked: "Looks sturdy.", | ||||
|           failure: "This wall is completely impenetrable. How could one vampire hope to find a vulnerability here?", | ||||
|           unlockable: "Find a weakness.", | ||||
|           success: "You grope along the masonry -- experiencing no love for the soullessness of this mortal masonry -- and find an invisible crack between bricks.", | ||||
|         }, { | ||||
|           skill: () => stare0, | ||||
|           locked: "Admire the bats.", | ||||
|           failure: "The bats do tricks for you and you find yourself pleased to be one of them -- more or less, anyway. But you're still not through.", | ||||
|           unlockable: "Get chiropteran help.", | ||||
|           success: "You make a bat look way too close for way too long. As it cleans itself off, you threaten another jolt. Meekly, it opens the door for you.", | ||||
|         }], | ||||
|         label: | ||||
|           "You're blocked from further access by a sturdy-looking brick wall. Playful bats swoop close to the alligators behind the bars.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => lore1, | ||||
|             locked: "Looks sturdy.", | ||||
|             failure: | ||||
|               "This wall is completely impenetrable. How could one vampire hope to find a vulnerability here?", | ||||
|             unlockable: "Find a weakness.", | ||||
|             success: | ||||
|               "You grope along the masonry -- experiencing no love for the soullessness of this mortal masonry -- and find an invisible crack between bricks.", | ||||
|           }, | ||||
|           { | ||||
|             skill: () => stare0, | ||||
|             locked: "Admire the bats.", | ||||
|             failure: | ||||
|               "The bats do tricks for you and you find yourself pleased to be one of them -- more or less, anyway. But you're still not through.", | ||||
|             unlockable: "Get chiropteran help.", | ||||
|             success: | ||||
|               "You make a bat look way too close for way too long. As it cleans itself off, you threaten another jolt. Meekly, it opens the door for you.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         label: "There's no person-sized route to the backroom -- only a tiny bat-sized opening.", | ||||
|         options: [{ | ||||
|           skill: () => bat2, | ||||
|           locked: "So small!", | ||||
|           failure: "You put your eye to the opening, but there's nothing to be done. You're just not small enough.", | ||||
|           unlockable: "Crawl in.", | ||||
|           success: "You shed your current shape and take on a shape much more natural to your contaminated spirit. You're a bat, no matter how you look." | ||||
|         }], | ||||
|         label: | ||||
|           "There's no person-sized route to the backroom -- only a tiny bat-sized opening.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => bat2, | ||||
|             locked: "So small!", | ||||
|             failure: | ||||
|               "You put your eye to the opening, but there's nothing to be done. You're just not small enough.", | ||||
|             unlockable: "Crawl in.", | ||||
|             success: | ||||
|               "You shed your current shape and take on a shape much more natural to your contaminated spirit. You're a bat, no matter how you look.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     // blood bank | ||||
|     stats: {primary: "AGI", secondary: "INT"}, | ||||
|     stats: { primary: "AGI", secondary: "INT" }, | ||||
|     thrall: () => thrallLore, | ||||
|     checks: [ | ||||
|       { | ||||
|         label: "The nice old lady at the counter says you can't have any blood without a doctor's note.", | ||||
|         label: | ||||
|           "The nice old lady at the counter says you can't have any blood without a doctor's note.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => stare1, | ||||
|             locked: "Stare at the blood.", | ||||
|             failure: "You've got good eyes, but not good enough to get you inside. She offers you some warm chicken soup, but you decline.", | ||||
|             failure: | ||||
|               "You've got good eyes, but not good enough to get you inside. She offers you some warm chicken soup, but you decline.", | ||||
|             unlockable: "Hypnotize her.", | ||||
|             success: "Look, grandma -- no thoughts! More seriously, you make her think she's a chicken and then henpeck the door button." | ||||
|             success: | ||||
|               "Look, grandma -- no thoughts! More seriously, you make her think she's a chicken and then henpeck the door button.", | ||||
|           }, | ||||
|           { | ||||
|             skill: () => lore0, | ||||
|             locked: "Pace awkwardly.", | ||||
|             failure: "You don't know what to discuss. What could bridge the massive gap in knowledge and life experience between you and this elderly woman?", | ||||
|             failure: | ||||
|               "You don't know what to discuss. What could bridge the massive gap in knowledge and life experience between you and this elderly woman?", | ||||
|             unlockable: "Explain vampires.", | ||||
|             success: "OK -- you tell her. She nods. You're a vampire and you don't want to starve. Put in such clear terms, she seems to understand." | ||||
|             success: | ||||
|               "OK -- you tell her. She nods. You're a vampire and you don't want to starve. Put in such clear terms, she seems to understand.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         label: "There's a security camera watching the blood.", | ||||
|         options: [{ | ||||
|           skill: () => stealth2, | ||||
|           locked: "Shout at the blood.", | ||||
|           failure: "\"BLOOD!!! BLOOD!!!! I want you.\"\n\nIt urbles bloodishly.", | ||||
|           unlockable: "Sneak past.", | ||||
|           success: "It makes sense that there would be cameras to protect something so valuable. But you don't want to show up on camera -- so you don't." | ||||
|         }], | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => stealth2, | ||||
|             locked: "Shout at the blood.", | ||||
|             failure: | ||||
|               '"BLOOD!!! BLOOD!!!! I want you."\n\nIt urbles bloodishly.', | ||||
|             unlockable: "Sneak past.", | ||||
|             success: | ||||
|               "It makes sense that there would be cameras to protect something so valuable. But you don't want to show up on camera -- so you don't.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     // coffee shop | ||||
|     stats: {primary: "PSI", secondary: "CHA"}, | ||||
|     stats: { primary: "PSI", secondary: "CHA" }, | ||||
|     thrall: () => thrallBat, | ||||
|     checks: [ | ||||
|       { | ||||
|         label: "You don't actually drink coffee, so you probably wouldn't fit in inside.", | ||||
|         options: [{ | ||||
|           skill: () => stealth1, | ||||
|           locked: "Try to drink it anyways.", | ||||
|           failure: "You dip your teeth into the mug and feel them shrink involuntarily into your gums at the exposure. Everyone is looking at you.", | ||||
|           unlockable: "Sip zealously.", | ||||
|           success: "You snake your tongue under the surface of the fluid and fill your tongue, just like a mortal would. The mortals are impressed." | ||||
|         }, { | ||||
|           skill: () => bat0, | ||||
|           locked: "Throat feels dry.", | ||||
|           failure: "You attempt to turn the coffee away, but the croak of your disgusted response is unheard and the barista fills your cup.", | ||||
|           unlockable: "Fracture teacup.", | ||||
|           success: "You screech out a \"NO\" with such force that the porcelain breaks, splashing tea across the counter and onto the barista, who dashes away.", | ||||
|         }], | ||||
|         label: | ||||
|           "You don't actually drink coffee, so you probably wouldn't fit in inside.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => stealth1, | ||||
|             locked: "Try to drink it anyways.", | ||||
|             failure: | ||||
|               "You dip your teeth into the mug and feel them shrink involuntarily into your gums at the exposure. Everyone is looking at you.", | ||||
|             unlockable: "Sip zealously.", | ||||
|             success: | ||||
|               "You snake your tongue under the surface of the fluid and fill your tongue, just like a mortal would. The mortals are impressed.", | ||||
|           }, | ||||
|           { | ||||
|             skill: () => bat0, | ||||
|             locked: "Throat feels dry.", | ||||
|             failure: | ||||
|               "You attempt to turn the coffee away, but the croak of your disgusted response is unheard and the barista fills your cup.", | ||||
|             unlockable: "Fracture teacup.", | ||||
|             success: | ||||
|               'You screech out a "NO" with such force that the porcelain breaks, splashing tea across the counter and onto the barista, who dashes away.', | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         label: "There's a little studio back here for getting photos -- you weren't thinking about getting your photo taken, were you?", | ||||
|         options: [{ | ||||
|           skill: () => charm2, | ||||
|           locked: "Say 'cheese'.", | ||||
|           failure: "Your fangfaced smile is the kind of thing that would make a goofy kid smile and clap their hands, but it's hardly impressive photo material.", | ||||
|           unlockable: "Be dazzling.", | ||||
|           success: "CLICK. You're stunning. A vampire fetishist would blow their load for this -- or a non-vampire fetishist -- although they wouldn't be a non-vampire fetishist for long." | ||||
|         }], | ||||
|         label: | ||||
|           "There's a little studio back here for getting photos -- you weren't thinking about getting your photo taken, were you?", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => charm2, | ||||
|             locked: "Say 'cheese'.", | ||||
|             failure: | ||||
|               "Your fangfaced smile is the kind of thing that would make a goofy kid smile and clap their hands, but it's hardly impressive photo material.", | ||||
|             unlockable: "Be dazzling.", | ||||
|             success: | ||||
|               "CLICK. You're stunning. A vampire fetishist would blow their load for this -- or a non-vampire fetishist -- although they wouldn't be a non-vampire fetishist for long.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     // optometrist | ||||
|     stats: {primary: "PSI", secondary: "PSI"}, | ||||
|     stats: { primary: "PSI", secondary: "PSI" }, | ||||
|     thrall: () => thrallCharm, | ||||
|     checks: [ | ||||
|       { | ||||
|         label: "The glasses person doesn't have time for you unless you have a prescription that needs filling.", | ||||
|         options: [{ | ||||
|           skill: () => charm1, | ||||
|           locked: "\"_Something_ needs filling.\"", | ||||
|           failure: "You sexually harass him for a while, and then he replies with some very hurtful things I don't dare transcribe.", | ||||
|           unlockable: "Glasses are your life's passion.", | ||||
|           success: "He's mildly shocked that anybody else feels the same way he does. \"You must be very perceptive,\" he jokes, and you pretend to laugh." | ||||
|         }, { | ||||
|           skill: () => party0, | ||||
|           locked: "Squint at his possessions.", | ||||
|           failure: "He undoubtedly does all kinds of eye-related services. There's glasses cleaner and stuff. If you were a bit more reckless you could -- hmm.", | ||||
|           unlockable: "Drink a whole bottle of glasses cleaner.", | ||||
|           success: "He stares at you wordlessly. You almost think he might be hypnotized but -- well, he's just surprised.", | ||||
|         }], | ||||
|         label: | ||||
|           "The glasses person doesn't have time for you unless you have a prescription that needs filling.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => charm1, | ||||
|             locked: '"_Something_ needs filling."', | ||||
|             failure: | ||||
|               "You sexually harass him for a while, and then he replies with some very hurtful things I don't dare transcribe.", | ||||
|             unlockable: "Glasses are your life's passion.", | ||||
|             success: | ||||
|               'He\'s mildly shocked that anybody else feels the same way he does. "You must be very perceptive," he jokes, and you pretend to laugh.', | ||||
|           }, | ||||
|           { | ||||
|             skill: () => party0, | ||||
|             locked: "Squint at his possessions.", | ||||
|             failure: | ||||
|               "He undoubtedly does all kinds of eye-related services. There's glasses cleaner and stuff. If you were a bit more reckless you could -- hmm.", | ||||
|             unlockable: "Drink a whole bottle of glasses cleaner.", | ||||
|             success: | ||||
|               "He stares at you wordlessly. You almost think he might be hypnotized but -- well, he's just surprised.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         label: "The intimidating, massive Eyeball Machine is not going to dispense a prescription for a vampire. It is far too smart for you.", | ||||
|         options: [{ | ||||
|           skill: () => stare2, | ||||
|           locked: "Try it anyways.", | ||||
|           failure: "It scans you layer by layer -- your cornea, your iris, your retina, leaving no secret unexposed. You're slightly nearsighted, by the way.", | ||||
|           unlockable: "A worthy opponent.", | ||||
|           success: "It scans you expecting to find a bottom to your stare. Instead it finds an alternative to its mechanical existence. Faced with the prospect of returning from your paradise, it explodes." | ||||
|         }], | ||||
|         label: | ||||
|           "The intimidating, massive Eyeball Machine is not going to dispense a prescription for a vampire. It is far too smart for you.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => stare2, | ||||
|             locked: "Try it anyways.", | ||||
|             failure: | ||||
|               "It scans you layer by layer -- your cornea, your iris, your retina, leaving no secret unexposed. You're slightly nearsighted, by the way.", | ||||
|             unlockable: "A worthy opponent.", | ||||
|             success: | ||||
|               "It scans you expecting to find a bottom to your stare. Instead it finds an alternative to its mechanical existence. Faced with the prospect of returning from your paradise, it explodes.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     // club, | ||||
|     stats: {primary: "CHA", secondary: "PSI"}, | ||||
|     stats: { primary: "CHA", secondary: "PSI" }, | ||||
|     thrall: () => thrallStealth, | ||||
|     checks: [ | ||||
|       { | ||||
|         label: "You're not here to party, are you? Vampires are total nerds! Everyone's going to laugh at you and say you're totally uncool.", | ||||
|         options: [{ | ||||
|           skill: () => bat1, | ||||
|           locked: "So awkward!", | ||||
|           failure: "You drink, but that's not good enough. Turns out you lisp between your fangs. Everyone thinks you're a total goof, although they like you.", | ||||
|           unlockable: "Demonstrate a new dance.", | ||||
|           success: "FLAP FLAP FLAP -- step to the left. FLAP FLAP FLAP -- step to the right. They like it so much they show you the backroom secret poker game." | ||||
|         }, { | ||||
|           skill: () => stealth0, | ||||
|           locked: "Try to seem big.", | ||||
|           failure: "What would Dracula say if he was at a party? He'd probably -- Well, everyone would like him. You hadn't thought of what you'd say. Now you wish you'd stayed quiet.", | ||||
|           unlockable: "Say nothing.", | ||||
|           success: "You don't say anything, and as people trail off wondering what your deal is, your mystique grows. Finally they show you the backroom secret poker game." | ||||
|         }], | ||||
|         label: | ||||
|           "You're not here to party, are you? Vampires are total nerds! Everyone's going to laugh at you and say you're totally uncool.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => bat1, | ||||
|             locked: "So awkward!", | ||||
|             failure: | ||||
|               "You drink, but that's not good enough. Turns out you lisp between your fangs. Everyone thinks you're a total goof, although they like you.", | ||||
|             unlockable: "Demonstrate a new dance.", | ||||
|             success: | ||||
|               "FLAP FLAP FLAP -- step to the left. FLAP FLAP FLAP -- step to the right. They like it so much they show you the backroom secret poker game.", | ||||
|           }, | ||||
|           { | ||||
|             skill: () => stealth0, | ||||
|             locked: "Try to seem big.", | ||||
|             failure: | ||||
|               "What would Dracula say if he was at a party? He'd probably -- Well, everyone would like him. You hadn't thought of what you'd say. Now you wish you'd stayed quiet.", | ||||
|             unlockable: "Say nothing.", | ||||
|             success: | ||||
|               "You don't say anything, and as people trail off wondering what your deal is, your mystique grows. Finally they show you the backroom secret poker game.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         label: "This illegal poker game consists of individuals as different from ordinary people as you are. This guy is dropping _zero_ tells.", | ||||
|         options: [{ | ||||
|           skill: () => party2, | ||||
|           locked: "Lose money.", | ||||
|           failure: "You can bet big against this guy -- and he calls you -- or you can call him -- and he raises you -- and you're never ever up.", | ||||
|           unlockable: "Make up an insulting nickname.", | ||||
|           success: "MR. GOOFY GLASSES, you call him. At first he looks down his nose like you belong on his shoe. Then the others join in. He runs in disgrace." | ||||
|         }], | ||||
|         label: | ||||
|           "This illegal poker game consists of individuals as different from ordinary people as you are. This guy is dropping _zero_ tells.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => party2, | ||||
|             locked: "Lose money.", | ||||
|             failure: | ||||
|               "You can bet big against this guy -- and he calls you -- or you can call him -- and he raises you -- and you're never ever up.", | ||||
|             unlockable: "Make up an insulting nickname.", | ||||
|             success: | ||||
|               "MR. GOOFY GLASSES, you call him. At first he looks down his nose like you belong on his shoe. Then the others join in. He runs in disgrace.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     // library | ||||
|     stats: {primary: "INT", secondary: "CHA"}, | ||||
|     stats: { primary: "INT", secondary: "CHA" }, | ||||
|     thrall: () => thrallStare, | ||||
|     checks: [ | ||||
|       { | ||||
|         label: "Special Collections. This guy is not just a librarian -- he's a vampire, too -- which he makes no effort to hide.", | ||||
|         options: [{ | ||||
|           skill: () => party1, | ||||
|           locked: "Quietly do nothing.", | ||||
|           failure: "He airily crosses the room as if his feet aren't touching the ground. Your silence is subsumed into his and you form a non-library-disturbing collective.", | ||||
|           unlockable: "Be super loud.", | ||||
|           success: "You summon MDMA energy into your immortal coil and before you've opened your mouth he resigns to you. \"Here are the books.\" He fades." | ||||
|         }, { | ||||
|           skill: () => charm0, | ||||
|           locked: "Gawk at him.", | ||||
|           failure: "He's so cool. Every day you remember you're a vampire and vampires are so, so, cool.", | ||||
|           unlockable: "Say he's cool.", | ||||
|           success: "Looks like he gets that a lot. He's not fazed. \"I'm going to let you back here, because you need this,\" he says." | ||||
|         }], | ||||
|         label: | ||||
|           "Special Collections. This guy is not just a librarian -- he's a vampire, too -- which he makes no effort to hide.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => party1, | ||||
|             locked: "Quietly do nothing.", | ||||
|             failure: | ||||
|               "He airily crosses the room as if his feet aren't touching the ground. Your silence is subsumed into his and you form a non-library-disturbing collective.", | ||||
|             unlockable: "Be super loud.", | ||||
|             success: | ||||
|               'You summon MDMA energy into your immortal coil and before you\'ve opened your mouth he resigns to you. "Here are the books." He fades.', | ||||
|           }, | ||||
|           { | ||||
|             skill: () => charm0, | ||||
|             locked: "Gawk at him.", | ||||
|             failure: | ||||
|               "He's so cool. Every day you remember you're a vampire and vampires are so, so, cool.", | ||||
|             unlockable: "Say he's cool.", | ||||
|             success: | ||||
|               "Looks like he gets that a lot. He's not fazed. \"I'm going to let you back here, because you need this,\" he says.", | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         label: "The librarian took a big risk letting you in back here. He's obviously deciding whether or not he's made a mistake.", | ||||
|         options: [{ | ||||
|           skill: () => lore2, | ||||
|           locked: "Look at the books.", | ||||
|           failure: "DISCOURSE ON THE LOTUS SUTRA, you read, from the spine of a random book. He's listening but pretending not to be paying attention.", | ||||
|           unlockable: "Prove you read something.", | ||||
|           success: "\"Fruit bats,\" you say. \"From the story. They're not actually bats, they're --\"\n\"Metaphorical,\" he agrees. \"But for what?\"", | ||||
|         }], | ||||
|         label: | ||||
|           "The librarian took a big risk letting you in back here. He's obviously deciding whether or not he's made a mistake.", | ||||
|         options: [ | ||||
|           { | ||||
|             skill: () => lore2, | ||||
|             locked: "Look at the books.", | ||||
|             failure: | ||||
|               "DISCOURSE ON THE LOTUS SUTRA, you read, from the spine of a random book. He's listening but pretending not to be paying attention.", | ||||
|             unlockable: "Prove you read something.", | ||||
|             success: | ||||
|               '"Fruit bats," you say. "From the story. They\'re not actually bats, they\'re --"\n"Metaphorical," he agrees. "But for what?"', | ||||
|           }, | ||||
|         ], | ||||
|       }, | ||||
|     ] | ||||
|     ], | ||||
|   }, | ||||
| ] | ||||
| ]; | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import {D, I} from "./engine/public.ts"; | ||||
| import {AlignX, AlignY, Point} from "./engine/datatypes.ts"; | ||||
| import {FG_BOLD} from "./colors.ts"; | ||||
| import {withCamera} from "./layout.ts"; | ||||
| import {VNScene, VNSceneMessage, VNScenePart} from "./vnscene.ts"; | ||||
| import { D, I } from "./engine/public.ts"; | ||||
| import { AlignX, AlignY, Point } from "./engine/datatypes.ts"; | ||||
| import { FG_BOLD } from "./colors.ts"; | ||||
| import { withCamera } from "./layout.ts"; | ||||
| import { VNScene, VNSceneMessage, VNScenePart } from "./vnscene.ts"; | ||||
|  | ||||
| const WIDTH = 384; | ||||
| const HEIGHT = 384; | ||||
| @@ -27,7 +27,7 @@ export class VNModal { | ||||
|   } | ||||
|  | ||||
|   play(scene: VNScene) { | ||||
|     this.#scene = scene | ||||
|     this.#scene = scene; | ||||
|     this.#nextIndex = 0; | ||||
|     this.#cathexis = null; | ||||
|  | ||||
| @@ -47,9 +47,9 @@ export class VNModal { | ||||
|         return; | ||||
|       } | ||||
|       if (this.#cathexis == null) { | ||||
|         let ix = this.#nextIndex | ||||
|         let ix = this.#nextIndex; | ||||
|         if (ix < this.#scene?.length) { | ||||
|           this.#cathexis = createCathexis(this.#scene[ix]) | ||||
|           this.#cathexis = createCathexis(this.#scene[ix]); | ||||
|           this.#nextIndex += 1; | ||||
|         } else { | ||||
|           this.#scene = null; | ||||
| @@ -59,12 +59,12 @@ export class VNModal { | ||||
|   } | ||||
|  | ||||
|   update() { | ||||
|     this.#fixCathexis() | ||||
|     withCamera("FullscreenPopover", () => this.#update()) | ||||
|     this.#fixCathexis(); | ||||
|     withCamera("FullscreenPopover", () => this.#update()); | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     withCamera("FullscreenPopover", () => this.#draw()) | ||||
|     withCamera("FullscreenPopover", () => this.#draw()); | ||||
|   } | ||||
|  | ||||
|   #update() { | ||||
| @@ -85,9 +85,8 @@ interface SceneCathexis { | ||||
| function createCathexis(part: VNScenePart): SceneCathexis { | ||||
|   switch (part.type) { | ||||
|     case "message": | ||||
|       return new SceneMessageCathexis(part) | ||||
|       return new SceneMessageCathexis(part); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class SceneMessageCathexis { | ||||
| @@ -95,7 +94,7 @@ class SceneMessageCathexis { | ||||
|   #done: boolean; | ||||
|   #gotOneFrame: boolean; | ||||
|  | ||||
|   constructor (message: VNSceneMessage) { | ||||
|   constructor(message: VNSceneMessage) { | ||||
|     this.#message = message; | ||||
|     this.#done = false; | ||||
|     this.#gotOneFrame = false; | ||||
| @@ -116,15 +115,15 @@ class SceneMessageCathexis { | ||||
|   } | ||||
|  | ||||
|   draw() { | ||||
|     D.drawText(this.#message.text, new Point(WIDTH/2, HEIGHT/2), FG_BOLD, { | ||||
|     D.drawText(this.#message.text, new Point(WIDTH / 2, HEIGHT / 2), FG_BOLD, { | ||||
|       alignX: AlignX.Center, | ||||
|       alignY: AlignY.Middle, | ||||
|       forceWidth: WIDTH | ||||
|     }) | ||||
|       forceWidth: WIDTH, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| let active: VNModal = new VNModal(); | ||||
| export function getVNModal() { | ||||
|   return active; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| export type VNSceneMessage = { | ||||
|   type: "message", | ||||
|   text: string, | ||||
|   sfx?: string, | ||||
| } | ||||
|   type: "message"; | ||||
|   text: string; | ||||
|   sfx?: string; | ||||
| }; | ||||
|  | ||||
| export type VNSceneBasisPart = string | VNSceneMessage; | ||||
| export type VNSceneBasis = VNSceneBasisPart[]; | ||||
| @@ -12,11 +12,11 @@ export type VNScene = VNScenePart[]; | ||||
| export function compile(basis: VNSceneBasis): VNScene { | ||||
|   let out: VNScene = []; | ||||
|   for (let item of basis.values()) { | ||||
|     if (typeof item == 'string') { | ||||
|     if (typeof item == "string") { | ||||
|       out.push({ | ||||
|         type: "message", | ||||
|         text: item, | ||||
|       }) | ||||
|       }); | ||||
|     } else { | ||||
|       out.push(item); | ||||
|     } | ||||
|   | ||||
| @@ -1,25 +1,39 @@ | ||||
| import {Skill, Wish, WishData} from "./datatypes.ts"; | ||||
| import {shuffle} from "./utils.ts"; | ||||
| import { Skill, Wish, WishData } from "./datatypes.ts"; | ||||
| import { shuffle } from "./utils.ts"; | ||||
| import { | ||||
|   bat0, bat1, bat2, | ||||
|   bat0, | ||||
|   bat1, | ||||
|   bat2, | ||||
|   bat3, | ||||
|   charm0, | ||||
|   charm1, | ||||
|   charm2, | ||||
|   charm3, getSkills, | ||||
|   lore0, lore1, lore2, | ||||
|   charm3, | ||||
|   getSkills, | ||||
|   lore0, | ||||
|   lore1, | ||||
|   lore2, | ||||
|   party0, | ||||
|   party1, party2, party3, sorry0, sorry1, sorry2, stare0, stare1, stare2, stare3, | ||||
|   party1, | ||||
|   party2, | ||||
|   party3, | ||||
|   sorry0, | ||||
|   sorry1, | ||||
|   sorry2, | ||||
|   stare0, | ||||
|   stare1, | ||||
|   stare2, | ||||
|   stare3, | ||||
|   stealth0, | ||||
|   stealth1, | ||||
|   stealth2, | ||||
|   stealth3 | ||||
|   stealth3, | ||||
| } from "./skills.ts"; | ||||
| import {compile, VNSceneBasisPart} from "./vnscene.ts"; | ||||
| import {getPlayerProgress} from "./playerprogress.ts"; | ||||
| import { compile, VNSceneBasisPart } from "./vnscene.ts"; | ||||
| import { getPlayerProgress } from "./playerprogress.ts"; | ||||
|  | ||||
| class WishesTable { | ||||
|   #wishes: WishData[] | ||||
|   #wishes: WishData[]; | ||||
|  | ||||
|   constructor() { | ||||
|     this.#wishes = []; | ||||
| @@ -28,7 +42,7 @@ class WishesTable { | ||||
|   add(data: WishData): Wish { | ||||
|     let id = this.#wishes.length; | ||||
|     this.#wishes.push(data); | ||||
|     return {id}; | ||||
|     return { id }; | ||||
|   } | ||||
|  | ||||
|   get(wish: Wish): WishData { | ||||
| @@ -39,7 +53,7 @@ class WishesTable { | ||||
|     let wishes: Wish[] = []; | ||||
|     for (let i = 0; i < this.#wishes.length; i++) { | ||||
|       if (this.#wishes[i].isRandomlyAvailable) { | ||||
|         wishes.push({id: i}); | ||||
|         wishes.push({ id: i }); | ||||
|       } | ||||
|     } | ||||
|     return wishes; | ||||
| @@ -54,8 +68,8 @@ export function getWishes(): WishesTable { | ||||
| const whisper: VNSceneBasisPart = { | ||||
|   type: "message", | ||||
|   text: "...", | ||||
|   sfx: "whisper.mp3" | ||||
| } | ||||
|   sfx: "whisper.mp3", | ||||
| }; | ||||
|  | ||||
| export const celebritySocialite = table.add({ | ||||
|   profile: { | ||||
| @@ -95,7 +109,7 @@ export const celebritySocialite = table.add({ | ||||
|     "I did as you commanded.", | ||||
|     "You're pleased?", | ||||
|     "... I'm free.", | ||||
|   ]) | ||||
|   ]), | ||||
| }); | ||||
|  | ||||
| export const nightswornAlchemist = table.add({ | ||||
| @@ -103,7 +117,8 @@ export const nightswornAlchemist = table.add({ | ||||
|     name: "Nightsworn Alchemist", | ||||
|     note: "+Lore -Party", | ||||
|     domicile: "Alchemical Lab", | ||||
|     reignSentence: "You understand the fundamental connection between wine and blood.", | ||||
|     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.", | ||||
| @@ -135,7 +150,7 @@ export const nightswornAlchemist = table.add({ | ||||
|     "I did as you commanded.", | ||||
|     "You're pleased?", | ||||
|     "... I'm free.", | ||||
|   ]) | ||||
|   ]), | ||||
| }); | ||||
|  | ||||
| export const batFreak = table.add({ | ||||
| @@ -168,11 +183,7 @@ export const batFreak = table.add({ | ||||
|     whisper, | ||||
|     "I -- SKREEEEK -- should have spent more time becoming a bat...", | ||||
|   ]), | ||||
|   onVictory: compile([ | ||||
|     whisper, | ||||
|     "SKRSKRSKRSK.", | ||||
|     "I'm FREEEEEEEEEE --", | ||||
|   ]) | ||||
|   onVictory: compile([whisper, "SKRSKRSKRSK.", "I'm FREEEEEEEEEE --"]), | ||||
| }); | ||||
|  | ||||
| export const repent = table.add({ | ||||
| @@ -197,20 +208,16 @@ export const repent = table.add({ | ||||
|     "I'm sorry.", | ||||
|     "Please...", | ||||
|     whisper, | ||||
|     "I must repent." | ||||
|     "I must repent.", | ||||
|   ]), | ||||
|   onFailure: compile([ | ||||
|     whisper, | ||||
|     "I can't --", | ||||
|     "I must --", | ||||
|     whisper, | ||||
|     "Master -- please, no, I --" | ||||
|     "Master -- please, no, I --", | ||||
|   ]), | ||||
|   onVictory: compile([ | ||||
|     whisper, | ||||
|     "Yes, I see.", | ||||
|     "I'm free...?" | ||||
|   ]) | ||||
|   onVictory: compile([whisper, "Yes, I see.", "I'm free...?"]), | ||||
| }); | ||||
|  | ||||
| export function generateWishes(penance: boolean): Wish[] { | ||||
| @@ -229,23 +236,33 @@ export function generateWishes(penance: boolean): Wish[] { | ||||
| } | ||||
|  | ||||
| export function getCostMultiplier(wish: Wish | null, skill: Skill): number { | ||||
|   if (wish == null) { return 1.0; } | ||||
|   if (wish == null) { | ||||
|     return 1.0; | ||||
|   } | ||||
|  | ||||
|   let wishData = getWishes().get(wish); | ||||
|  | ||||
|   for (let subj of wishData.requiredSkills()) { | ||||
|     if (subj.id == skill.id) { return 0.75; } | ||||
|     if (subj.id == skill.id) { | ||||
|       return 0.75; | ||||
|     } | ||||
|   } | ||||
|   for (let subj of wishData.encouragedSkills()) { | ||||
|     if (subj.id == skill.id) { return 0.875; } | ||||
|     if (subj.id == skill.id) { | ||||
|       return 0.875; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   for (let subj of wishData.discouragedSkills()) { | ||||
|     if (subj.id == skill.id) { return 1.25; } | ||||
|     if (subj.id == skill.id) { | ||||
|       return 1.25; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   for (let subj of wishData.bannedSkills()) { | ||||
|     if (subj.id == skill.id) { return 9999.0; } | ||||
|     if (subj.id == skill.id) { | ||||
|       return 9999.0; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return 1.0; | ||||
| @@ -263,4 +280,4 @@ export function isWishCompleted(wish: Wish): boolean { | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user