Run prettier over everything
This commit is contained in:
		| @@ -1,7 +1,7 @@ | |||||||
| import {DrawPile} from "./drawpile.ts"; | import { DrawPile } from "./drawpile.ts"; | ||||||
| import {AlignX, AlignY, Point, Rect, Size} from "./engine/datatypes.ts"; | import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {BG_INSET, FG_BOLD, FG_TEXT} from "./colors.ts"; | import { BG_INSET, FG_BOLD, FG_TEXT } from "./colors.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
|  |  | ||||||
| export function addButton( | export function addButton( | ||||||
|   drawpile: DrawPile, |   drawpile: DrawPile, | ||||||
| @@ -13,7 +13,10 @@ export function addButton( | |||||||
|   let padding = 2; |   let padding = 2; | ||||||
|   let topLeft = rect.top; |   let topLeft = rect.top; | ||||||
|   let topLeftPadded = topLeft.offset(new Point(padding, padding)); |   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)); |   let center = topLeft.offset(new Point(rect.size.w / 2, rect.size.h / 2)); | ||||||
|  |  | ||||||
|   drawpile.addClickable( |   drawpile.addClickable( | ||||||
| @@ -26,16 +29,16 @@ export function addButton( | |||||||
|       D.fillRect( |       D.fillRect( | ||||||
|         topLeftPadded.offset(new Point(-1, -1)), |         topLeftPadded.offset(new Point(-1, -1)), | ||||||
|         sizePadded.add(new Size(2, 2)), |         sizePadded.add(new Size(2, 2)), | ||||||
|         bg |         bg, | ||||||
|       ); |       ); | ||||||
|       D.drawRect(topLeftPadded, sizePadded, fg); |       D.drawRect(topLeftPadded, sizePadded, fg); | ||||||
|       D.drawText(label, center, fgLabel, { |       D.drawText(label, center, fgLabel, { | ||||||
|         alignX: AlignX.Center, |         alignX: AlignX.Center, | ||||||
|         alignY: AlignY.Middle, |         alignY: AlignY.Middle, | ||||||
|       }) |       }); | ||||||
|     }, |     }, | ||||||
|     new Rect(topLeftPadded, sizePadded), |     new Rect(topLeftPadded, sizePadded), | ||||||
|     enabled, |     enabled, | ||||||
|     cbClick |     cbClick, | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import {DrawPile} from "./drawpile.ts"; | import { DrawPile } from "./drawpile.ts"; | ||||||
| import {CheckData, CheckDataOption, ChoiceOption} from "./newmap.ts"; | import { CheckData, CheckDataOption, ChoiceOption } from "./newmap.ts"; | ||||||
| import {getPartLocation, withCamera} from "./layout.ts"; | import { getPartLocation, withCamera } from "./layout.ts"; | ||||||
| import {AlignX, AlignY, Point, Rect, Size} from "./engine/datatypes.ts"; | import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
| import {BG_INSET, FG_BOLD} from "./colors.ts"; | import { BG_INSET, FG_BOLD } from "./colors.ts"; | ||||||
| import {addButton} from "./button.ts"; | import { addButton } from "./button.ts"; | ||||||
| import {getSkills} from "./skills.ts"; | import { getSkills } from "./skills.ts"; | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
|  |  | ||||||
| export class CheckModal { | export class CheckModal { | ||||||
|   #drawpile: DrawPile; |   #drawpile: DrawPile; | ||||||
| @@ -22,20 +22,20 @@ export class CheckModal { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   get isShown() { |   get isShown() { | ||||||
|     return this.#activeCheck != null |     return this.#activeCheck != null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get #size(): Size { |   get #size(): Size { | ||||||
|     return getPartLocation("BottomModal").size |     return getPartLocation("BottomModal").size; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     withCamera("BottomModal", () => this.#update()) |     withCamera("BottomModal", () => this.#update()); | ||||||
|     this.#drawpile.executeOnClick() |     this.#drawpile.executeOnClick(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     withCamera("BottomModal", () => this.#draw()) |     withCamera("BottomModal", () => this.#draw()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   show(checkData: CheckData | null, callback: (() => void) | null) { |   show(checkData: CheckData | null, callback: (() => void) | null) { | ||||||
| @@ -47,13 +47,15 @@ export class CheckModal { | |||||||
|   #update() { |   #update() { | ||||||
|     this.#drawpile.clear(); |     this.#drawpile.clear(); | ||||||
|  |  | ||||||
|     let check = this.#activeCheck |     let check = this.#activeCheck; | ||||||
|     if (!check) { return; } |     if (!check) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let size = this.#size; |     let size = this.#size; | ||||||
|     this.#drawpile.add(0, () => { |     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; |     let success = this.#success; | ||||||
|     if (success) { |     if (success) { | ||||||
| @@ -62,11 +64,17 @@ export class CheckModal { | |||||||
|           forceWidth: size.w, |           forceWidth: size.w, | ||||||
|           alignX: AlignX.Center, |           alignX: AlignX.Center, | ||||||
|           alignY: AlignY.Middle, |           alignY: AlignY.Middle, | ||||||
|         }) |         }); | ||||||
|       }); |       }); | ||||||
|       addButton(this.#drawpile, "OK!", new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), true, () => { |       addButton( | ||||||
|         this.show(null, null); |         this.#drawpile, | ||||||
|       }) |         "OK!", | ||||||
|  |         new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), | ||||||
|  |         true, | ||||||
|  |         () => { | ||||||
|  |           this.show(null, null); | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -76,12 +84,15 @@ export class CheckModal { | |||||||
|         forceWidth: size.w, |         forceWidth: size.w, | ||||||
|         alignX: AlignX.Center, |         alignX: AlignX.Center, | ||||||
|         alignY: AlignY.Middle, |         alignY: AlignY.Middle, | ||||||
|       }) |       }); | ||||||
|     }) |     }); | ||||||
|  |  | ||||||
|     let options = check.options; |     let options = check.options; | ||||||
|  |  | ||||||
|     let addOptionButton = (option: CheckDataOption | ChoiceOption, rect: Rect) => { |     let addOptionButton = ( | ||||||
|  |       option: CheckDataOption | ChoiceOption, | ||||||
|  |       rect: Rect, | ||||||
|  |     ) => { | ||||||
|       let accomplished: boolean; |       let accomplished: boolean; | ||||||
|       let optionLabel: string; |       let optionLabel: string; | ||||||
|       let resultMessage: string; |       let resultMessage: string; | ||||||
| @@ -91,7 +102,6 @@ export class CheckModal { | |||||||
|         accomplished = option.countsAsSuccess; |         accomplished = option.countsAsSuccess; | ||||||
|         optionLabel = option.unlockable; |         optionLabel = option.unlockable; | ||||||
|         resultMessage = option.success; |         resultMessage = option.success; | ||||||
|  |  | ||||||
|       } else { |       } else { | ||||||
|         option = option as CheckDataOption; |         option = option as CheckDataOption; | ||||||
|         let skill = option.skill(); |         let skill = option.skill(); | ||||||
| @@ -110,10 +120,12 @@ export class CheckModal { | |||||||
|  |  | ||||||
|         if (accomplished) { |         if (accomplished) { | ||||||
|           let cb = this.#callback; |           let cb = this.#callback; | ||||||
|           if (cb) { cb(); } |           if (cb) { | ||||||
|  |             cb(); | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       }) |       }); | ||||||
|     } |     }; | ||||||
|  |  | ||||||
|     if (options.length == 0) { |     if (options.length == 0) { | ||||||
|       addButton( |       addButton( | ||||||
| @@ -121,17 +133,26 @@ export class CheckModal { | |||||||
|         "OK!", |         "OK!", | ||||||
|         new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), |         new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), | ||||||
|         true, |         true, | ||||||
|         () => { this.show(null, null) } |         () => { | ||||||
|       ) |           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 == 1) { | ||||||
|     } |       addOptionButton( | ||||||
|     else if (options.length == 2) { |         options[0], | ||||||
|       addOptionButton(options[0], new Rect(new Point(0, size.h - 64), new Size(size.w, 32))); |         new Rect(new Point(0, size.h - 64), new Size(size.w, 64)), | ||||||
|       addOptionButton(options[1], new Rect(new Point(0, size.h - 32), new Size(size.w, 32))); |       ); | ||||||
|  |     } 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 { |     } 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(); | let active: CheckModal = new CheckModal(); | ||||||
| export function getCheckModal() { | export function getCheckModal() { | ||||||
|   return active; |   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_OUTER = Color.parseHexCode("#143464"); | ||||||
| export const BG_WALL_OR_UNREVEALED = Color.parseHexCode("#143464"); | export const BG_WALL_OR_UNREVEALED = Color.parseHexCode("#143464"); | ||||||
| export const BG_INSET = Color.parseHexCode("#242234"); | export const BG_INSET = Color.parseHexCode("#242234"); | ||||||
| export const FG_TEXT = Color.parseHexCode("#c0c0c0") | export const FG_TEXT = Color.parseHexCode("#c0c0c0"); | ||||||
| export const FG_BOLD = Color.parseHexCode("#ffffff") | export const FG_BOLD = Color.parseHexCode("#ffffff"); | ||||||
| export const BG_CEILING = Color.parseHexCode("#143464"); | export const BG_CEILING = Color.parseHexCode("#143464"); | ||||||
| export const FG_MOULDING = FG_TEXT; | 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 type Stat = "AGI" | "INT" | "CHA" | "PSI"; | ||||||
| export const ALL_STATS: Array<Stat> = ["AGI", "INT", "CHA", "PSI"]; | export const ALL_STATS: Array<Stat> = ["AGI", "INT", "CHA", "PSI"]; | ||||||
|  |  | ||||||
| export type Resource = "EXP"; | export type Resource = "EXP"; | ||||||
| export const ALL_RESOURCES: Array<Resource> = ["EXP"] | export const ALL_RESOURCES: Array<Resource> = ["EXP"]; | ||||||
|  |  | ||||||
| export type SkillGoverning = { | export type SkillGoverning = { | ||||||
|   stats: Stat[], |   stats: Stat[]; | ||||||
|   underTarget: number, |   underTarget: number; | ||||||
|   target: number, |   target: number; | ||||||
|   cost: number, |   cost: number; | ||||||
|   note: string, |   note: string; | ||||||
|   scoring: SkillScoring, |   scoring: SkillScoring; | ||||||
|   mortalServantValue: number, |   mortalServantValue: number; | ||||||
|   flipped: boolean, |   flipped: boolean; | ||||||
| }; | }; | ||||||
| export type SkillProfile = { | export type SkillProfile = { | ||||||
|   name: string, |   name: string; | ||||||
|   description: string, |   description: string; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export type SkillData = { | export type SkillData = { | ||||||
|   isDegrading?: boolean; |   isDegrading?: boolean; | ||||||
|   governing: SkillGoverning, |   governing: SkillGoverning; | ||||||
|   profile: SkillProfile, |   profile: SkillProfile; | ||||||
|   prereqs: Skill[] |   prereqs: Skill[]; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export type ScoringCategory = "bat" | "stealth" | "charm" | "stare" | "party" | "lore"; | export type ScoringCategory = | ||||||
| export const SCORING_CATEGORIES: ScoringCategory[] = ["bat", "stealth", "charm", "stare", "party", "lore"]; |   | "bat" | ||||||
| export type SkillScoring = {[P in ScoringCategory]?: number}; |   | "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 = { | export type Skill = { | ||||||
|   id: number |   id: number; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export type WishData = { | export type WishData = { | ||||||
|   profile: { |   profile: { | ||||||
|     name: string, |     name: string; | ||||||
|     note: string, |     note: string; | ||||||
|     domicile: string, |     domicile: string; | ||||||
|     reignSentence: string; |     reignSentence: string; | ||||||
|     failureName: string, |     failureName: string; | ||||||
|     failureDomicile: string, |     failureDomicile: string; | ||||||
|     failureReignSentence: string, |     failureReignSentence: string; | ||||||
|     failureSuccessorVerb: string; |     failureSuccessorVerb: string; | ||||||
|   }, |   }; | ||||||
|   isRandomlyAvailable: boolean, |   isRandomlyAvailable: boolean; | ||||||
|   isCompulsory: boolean; |   isCompulsory: boolean; | ||||||
|   bannedSkills: () => Skill[], |   bannedSkills: () => Skill[]; | ||||||
|   discouragedSkills: () => Skill[], |   discouragedSkills: () => Skill[]; | ||||||
|   encouragedSkills: () => Skill[], |   encouragedSkills: () => Skill[]; | ||||||
|   requiredSkills: () => Skill[] |   requiredSkills: () => Skill[]; | ||||||
|   prologue: VNScene, |   prologue: VNScene; | ||||||
|   onVictory: VNScene, |   onVictory: VNScene; | ||||||
|   onFailure: VNScene, |   onFailure: VNScene; | ||||||
| } | }; | ||||||
| export type Wish = { | export type Wish = { | ||||||
|   id: number |   id: number; | ||||||
| } | }; | ||||||
|  |  | ||||||
| // endings | // endings | ||||||
|  |  | ||||||
| export type Ending = { | export type Ending = { | ||||||
|   scene: VNScene |   scene: VNScene; | ||||||
|   personal: EndingPersonal, |   personal: EndingPersonal; | ||||||
|   analytics: EndingAnalytics, |   analytics: EndingAnalytics; | ||||||
|   successorOptions: SuccessorOption[], |   successorOptions: SuccessorOption[]; | ||||||
|   wishOptions: Wish[], |   wishOptions: Wish[]; | ||||||
|  |  | ||||||
|   // forcedSuccessors: number[] | null, |   // forcedSuccessors: number[] | null, | ||||||
|   // forcedWishes: number[] | null |   // forcedWishes: number[] | null | ||||||
| } | }; | ||||||
|  |  | ||||||
| export type EndingPersonal = { | export type EndingPersonal = { | ||||||
|   rank: string, |   rank: string; | ||||||
|   domicile: string, |   domicile: string; | ||||||
|   reignSentence: string, |   reignSentence: string; | ||||||
|   successorVerb: string, |   successorVerb: string; | ||||||
|   progenerateVerb: string, |   progenerateVerb: string; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export type EndingAnalytics = { | export type EndingAnalytics = { | ||||||
|   itemsPurloined: number, |   itemsPurloined: number; | ||||||
|   vampiricSkills: number, |   vampiricSkills: number; | ||||||
|   mortalServants: number, |   mortalServants: number; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export type SuccessorOption = { | export type SuccessorOption = { | ||||||
|   name: string, |   name: string; | ||||||
|   title: string, |   title: string; | ||||||
|   note: string | null, // ex "already a vampire" |   note: string | null; // ex "already a vampire" | ||||||
|   stats: Record<Stat, number>, |   stats: Record<Stat, number>; | ||||||
|   talents: Record<Stat, number>, |   talents: Record<Stat, number>; | ||||||
|   skills: Skill[], |   skills: Skill[]; | ||||||
|   inPenance: boolean; |   inPenance: boolean; | ||||||
|   isCompulsory: boolean; |   isCompulsory: boolean; | ||||||
| } | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import {D, I} from "./engine/public.ts"; | import { D, I } from "./engine/public.ts"; | ||||||
| import {Rect} from "./engine/datatypes.ts"; | import { Rect } from "./engine/datatypes.ts"; | ||||||
|  |  | ||||||
| export class DrawPile { | export class DrawPile { | ||||||
|   #draws: {depth: number, op: () => void, onClick?: () => void}[] |   #draws: { depth: number; op: () => void; onClick?: () => void }[]; | ||||||
|   #hoveredIndex: number | null; |   #hoveredIndex: number | null; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     this.#draws = [] |     this.#draws = []; | ||||||
|     this.#hoveredIndex = null; |     this.#hoveredIndex = null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -16,10 +16,16 @@ export class DrawPile { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   add(depth: number, op: () => void) { |   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 position = I.mousePosition?.offset(D.camera); | ||||||
|     let hovered = false; |     let hovered = false; | ||||||
|     if (position != null) { |     if (position != null) { | ||||||
| @@ -31,7 +37,7 @@ export class DrawPile { | |||||||
|     if (hovered) { |     if (hovered) { | ||||||
|       this.#hoveredIndex = this.#draws.length; |       this.#hoveredIndex = this.#draws.length; | ||||||
|     } |     } | ||||||
|     this.#draws.push({depth, op: (() => op(hovered)), onClick: onClick}) |     this.#draws.push({ depth, op: () => op(hovered), onClick: onClick }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   executeOnClick() { |   executeOnClick() { | ||||||
| @@ -48,11 +54,9 @@ export class DrawPile { | |||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     let draws = [...this.#draws]; |     let draws = [...this.#draws]; | ||||||
|     draws.sort( |     draws.sort((d0, d1) => d0.depth - d1.depth); | ||||||
|       (d0, d1) => d0.depth - d1.depth |  | ||||||
|     ); |  | ||||||
|     for (let d of draws.values()) { |     for (let d of draws.values()) { | ||||||
|       d.op(); |       d.op(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import {withCamera} from "./layout.ts"; | import { withCamera } from "./layout.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
| import {BG_INSET, FG_BOLD, FG_TEXT} from "./colors.ts"; | import { BG_INSET, FG_BOLD, FG_TEXT } from "./colors.ts"; | ||||||
| import {AlignX, AlignY, Point, Rect, Size} from "./engine/datatypes.ts"; | import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {DrawPile} from "./drawpile.ts"; | import { DrawPile } from "./drawpile.ts"; | ||||||
| import {addButton} from "./button.ts"; | import { addButton } from "./button.ts"; | ||||||
| import {ALL_STATS, Ending} from "./datatypes.ts"; | import { ALL_STATS, Ending } from "./datatypes.ts"; | ||||||
| import {getStateManager} from "./statemanager.ts"; | import { getStateManager } from "./statemanager.ts"; | ||||||
| import {getWishes} from "./wishes.ts"; | import { getWishes } from "./wishes.ts"; | ||||||
|  |  | ||||||
| const WIDTH = 384; | const WIDTH = 384; | ||||||
| const HEIGHT = 384; | const HEIGHT = 384; | ||||||
| @@ -42,11 +42,11 @@ export class EndgameModal { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     withCamera("FullscreenPopover", () => this.#update()) |     withCamera("FullscreenPopover", () => this.#update()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     withCamera("FullscreenPopover", () => this.#draw()) |     withCamera("FullscreenPopover", () => this.#draw()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get #canProgenerate(): boolean { |   get #canProgenerate(): boolean { | ||||||
| @@ -54,8 +54,7 @@ export class EndgameModal { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   #progenerate() { |   #progenerate() { | ||||||
|     let successor = |     let successor = this.#ending!.successorOptions[this.#selectedSuccessor!]; | ||||||
|       this.#ending!.successorOptions[this.#selectedSuccessor!]; |  | ||||||
|     let wish = |     let wish = | ||||||
|       this.#selectedWish != null |       this.#selectedWish != null | ||||||
|         ? this.#ending!.wishOptions[this.#selectedWish!] |         ? this.#ending!.wishOptions[this.#selectedWish!] | ||||||
| @@ -77,98 +76,133 @@ export class EndgameModal { | |||||||
|       let mortalServants = analytics?.mortalServants ?? 0; |       let mortalServants = analytics?.mortalServants ?? 0; | ||||||
|  |  | ||||||
|       this.#drawpile.add(0, () => { |       this.#drawpile.add(0, () => { | ||||||
|         D.drawText("It is time to announce the sentence of fate.", new Point(0, 0), FG_TEXT) |         D.drawText( | ||||||
|         D.drawText("You are no longer a fledgling. Your new rank:", new Point(0, 32), FG_TEXT) |           "It is time to announce the sentence of fate.", | ||||||
|         D.drawText(rank, new Point(WIDTH / 2, 64), FG_BOLD, {alignX: AlignX.Center}) |           new Point(0, 0), | ||||||
|         D.drawText("You have achieved a DOMICILE STATUS of:", new Point(0, 96), FG_TEXT) |           FG_TEXT, | ||||||
|         D.drawText(domicile, new Point(WIDTH / 2, 128), FG_BOLD, {alignX: AlignX.Center}) |         ); | ||||||
|  |         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 = |         let whereLabel = | ||||||
|           mortalServants >= 25 ? "where you live with many friends." : |           mortalServants >= 25 | ||||||
|           mortalServants >= 1 ? "where you live with a couple of friends." : |             ? "where you live with many friends." | ||||||
|           "where you live without friends."; |             : mortalServants >= 1 | ||||||
|         D.drawText(whereLabel, new Point(0, 160), FG_TEXT) |               ? "where you live with a couple of friends." | ||||||
|         D.drawText("You have achieved:", new Point(0, 192), FG_TEXT) |               : "where you live without friends."; | ||||||
|         let itemsPurloinedText = itemsPurloined == 1 ? "item purloined" : "items purloined"; |         D.drawText(whereLabel, new Point(0, 160), FG_TEXT); | ||||||
|         let vampiricSkillsText = vampiricSkills == 1 ? "vampiric skill" : "vampiric skills"; |         D.drawText("You have achieved:", new Point(0, 192), FG_TEXT); | ||||||
|         let mortalServantsText = mortalServants == 1 ? "mortal servant" : "mortal servants"; |         let itemsPurloinedText = | ||||||
|         let itemsPurloinedSpcr = itemsPurloined == 1 ? "              " : "               "; |           itemsPurloined == 1 ? "item purloined" : "items purloined"; | ||||||
|         let vampiricSkillsSpcr = vampiricSkills == 1 ? "              " : "               "; |         let vampiricSkillsText = | ||||||
|         let mortalServantsSpcr = mortalServants == 1 ? "              " : "               "; |           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( |         D.drawText( | ||||||
|           `${itemsPurloined} ${itemsPurloinedText}\n${vampiricSkills} ${vampiricSkillsText}\n${mortalServants} ${mortalServantsText}`, |           `${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( |         D.drawText( | ||||||
|           `${itemsPurloined} ${itemsPurloinedSpcr}\n${vampiricSkills} ${vampiricSkillsSpcr}\n${mortalServants} ${mortalServantsSpcr}`, |           `${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) { |         if (mortalServants >= 10) { | ||||||
|           msg = "That's more than zero." |           msg = "That's more than zero."; | ||||||
|         } |         } | ||||||
|         if (mortalServants >= 30) { |         if (mortalServants >= 30) { | ||||||
|           msg = "That feels like a lot!" |           msg = "That feels like a lot!"; | ||||||
|         } |         } | ||||||
|         D.drawText(msg, new Point(0, 288), FG_TEXT) |         D.drawText(msg, new Point(0, 288), FG_TEXT); | ||||||
|         let reignSentence = this.#ending?.personal?.reignSentence ?? "Your reign is in an unknown state."; |         let reignSentence = | ||||||
|         D.drawText(`${reignSentence} It is now time to`, new Point(0, 320), FG_TEXT, {forceWidth: WIDTH}) |           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( |       addButton( | ||||||
|         this.#drawpile, |         this.#drawpile, | ||||||
|         this.#ending?.personal?.successorVerb ?? "Do Unknown Things", |         this.#ending?.personal?.successorVerb ?? "Do Unknown Things", | ||||||
|         new Rect( |         new Rect(new Point(0, HEIGHT - 32), new Size(WIDTH, 32)), | ||||||
|           new Point(0, HEIGHT - 32), new Size(WIDTH, 32) |  | ||||||
|         ), |  | ||||||
|         true, |         true, | ||||||
|         () => { |         () => { | ||||||
|           this.#page += 1; |           this.#page += 1; | ||||||
|         } |         }, | ||||||
|       ) |       ); | ||||||
|     } |     } else if (this.#page == 1) { | ||||||
|     else if (this.#page == 1) { |  | ||||||
|       this.#drawpile.add(0, () => { |       this.#drawpile.add(0, () => { | ||||||
|         D.drawText("Choose your successor:", new Point(0, 0), FG_TEXT); |         D.drawText("Choose your successor:", new Point(0, 0), FG_TEXT); | ||||||
|       }) |       }); | ||||||
|  |  | ||||||
|       this.#addCandidate(0, new Point(0, 16)) |       this.#addCandidate(0, new Point(0, 16)); | ||||||
|       this.#addCandidate(1, new Point(0, 80)) |       this.#addCandidate(1, new Point(0, 80)); | ||||||
|       this.#addCandidate(2, new Point(0, 144)) |       this.#addCandidate(2, new Point(0, 144)); | ||||||
|  |  | ||||||
|       let optionalNote = " (optional, punishes failure)"; |       let optionalNote = " (optional, punishes failure)"; | ||||||
|       if (this.#hasCompulsoryWish) { |       if (this.#hasCompulsoryWish) { | ||||||
|         optionalNote = ""; |         optionalNote = ""; | ||||||
|       } |       } | ||||||
|       this.#drawpile.add(0, () => { |       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(1, new Point(0, 240)); | ||||||
|       this.#addWish(0, new Point(128, 240)) |       this.#addWish(0, new Point(128, 240)); | ||||||
|       this.#addWish(2, new Point(256, 240)) |       this.#addWish(2, new Point(256, 240)); | ||||||
|  |  | ||||||
|       addButton( |       addButton( | ||||||
|         this.#drawpile, |         this.#drawpile, | ||||||
|         "Back", |         "Back", | ||||||
|         new Rect( |         new Rect(new Point(0, HEIGHT - 32), new Size(WIDTH / 3, 32)), | ||||||
|           new Point(0, HEIGHT - 32), new Size(WIDTH / 3, 32) |  | ||||||
|         ), |  | ||||||
|         true, |         true, | ||||||
|         () => { |         () => { | ||||||
|           this.#page -= 1; |           this.#page -= 1; | ||||||
|         } |         }, | ||||||
|       ) |       ); | ||||||
|       addButton( |       addButton( | ||||||
|         this.#drawpile, |         this.#drawpile, | ||||||
|         this.#ending?.personal.progenerateVerb ?? "Unknown Action", |         this.#ending?.personal.progenerateVerb ?? "Unknown Action", | ||||||
|         new Rect( |         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.#canProgenerate, | ||||||
|         () => { |         () => { | ||||||
|           this.#progenerate() |           this.#progenerate(); | ||||||
|         } |         }, | ||||||
|       ) |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.#drawpile.executeOnClick(); |     this.#drawpile.executeOnClick(); | ||||||
| @@ -253,39 +287,55 @@ export class EndgameModal { | |||||||
|         if (hover || selected) { |         if (hover || selected) { | ||||||
|           [bg, fg, fgBold] = [FG_BOLD, BG_INSET, BG_INSET]; |           [bg, fg, fgBold] = [FG_BOLD, BG_INSET, BG_INSET]; | ||||||
|         } |         } | ||||||
|         D.fillRect( |         D.fillRect(at.offset(new Point(0, 4)), new Size(w, h - 8), bg); | ||||||
|           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.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); |         D.drawText(candidate.name, at.offset(new Point(4, 8)), fgBold); | ||||||
|  |  | ||||||
|         let xys = [ |         let xys = [ | ||||||
|           new Point(4, 24), new Point(4, 40), |           new Point(4, 24), | ||||||
|           new Point(116, 24), new Point(116, 40) |           new Point(4, 40), | ||||||
|  |           new Point(116, 24), | ||||||
|  |           new Point(116, 40), | ||||||
|         ]; |         ]; | ||||||
|         let i = 0; |         let i = 0; | ||||||
|         for (let s of ALL_STATS.values()) { |         for (let s of ALL_STATS.values()) { | ||||||
|           let statValue = candidate.stats[s]; |           let statValue = candidate.stats[s]; | ||||||
|           let talentValue = candidate.talents[s]; |           let talentValue = candidate.talents[s]; | ||||||
|  |  | ||||||
|           D.drawText(s, at.offset(xys[i]), fg) |           D.drawText(s, at.offset(xys[i]), fg); | ||||||
|           D.drawText(`${statValue}`, at.offset(xys[i].offset(new Point(32, 0))), fgBold) |           D.drawText( | ||||||
|  |             `${statValue}`, | ||||||
|  |             at.offset(xys[i].offset(new Point(32, 0))), | ||||||
|  |             fgBold, | ||||||
|  |           ); | ||||||
|  |  | ||||||
|           if (talentValue > 0) { |           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) { |           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; |           i += 1; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (candidate.note != null) { |         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, |       generalRect, | ||||||
| @@ -293,11 +343,12 @@ export class EndgameModal { | |||||||
|  |  | ||||||
|       () => { |       () => { | ||||||
|         if (this.#selectedSuccessor == ix) { |         if (this.#selectedSuccessor == ix) { | ||||||
|           this.#selectedSuccessor = null |           this.#selectedSuccessor = null; | ||||||
|         } else { |         } else { | ||||||
|           this.#selectedSuccessor = ix; |           this.#selectedSuccessor = ix; | ||||||
|         } |         } | ||||||
|       }); |       }, | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #addWish(ix: number, at: Point) { |   #addWish(ix: number, at: Point) { | ||||||
| @@ -324,21 +375,27 @@ export class EndgameModal { | |||||||
|         if (hover || selected) { |         if (hover || selected) { | ||||||
|           [bg, fg, fgBold] = [FG_BOLD, BG_INSET, BG_INSET]; |           [bg, fg, fgBold] = [FG_BOLD, BG_INSET, BG_INSET]; | ||||||
|         } |         } | ||||||
|         D.fillRect( |         D.fillRect(at.offset(new Point(2, 4)), new Size(w - 4, h - 8), bg); | ||||||
|           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.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, { |         D.drawText( | ||||||
|           forceWidth: w - 4, |           wishData.profile.name, | ||||||
|           alignX: AlignX.Center, |           at.offset(new Point(w / 2, h / 2)), | ||||||
|           alignY: AlignY.Middle, |           fgBold, | ||||||
|         }); |           { | ||||||
|         D.drawText(wishData.profile.note, at.offset(new Point(w / 2, h)), FG_TEXT, { |             forceWidth: w - 4, | ||||||
|           alignX: AlignX.Center |             alignX: AlignX.Center, | ||||||
|         }); |             alignY: AlignY.Middle, | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|  |         D.drawText( | ||||||
|  |           wishData.profile.note, | ||||||
|  |           at.offset(new Point(w / 2, h)), | ||||||
|  |           FG_TEXT, | ||||||
|  |           { | ||||||
|  |             alignX: AlignX.Center, | ||||||
|  |           }, | ||||||
|  |         ); | ||||||
|       }, |       }, | ||||||
|       generalRect, |       generalRect, | ||||||
|       enabled, |       enabled, | ||||||
| @@ -349,7 +406,7 @@ export class EndgameModal { | |||||||
|         } else { |         } else { | ||||||
|           this.#selectedWish = ix; |           this.#selectedWish = ix; | ||||||
|         } |         } | ||||||
|       } |       }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -362,8 +419,7 @@ export class EndgameModal { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| let active = new EndgameModal(); | let active = new EndgameModal(); | ||||||
| export function getEndgameModal() { | export function getEndgameModal() { | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import {compile, VNScene, VNSceneBasisPart} from "./vnscene.ts"; | import { compile, VNScene, VNSceneBasisPart } from "./vnscene.ts"; | ||||||
|  |  | ||||||
| const squeak: VNSceneBasisPart = { | const squeak: VNSceneBasisPart = { | ||||||
|   type: "message", |   type: "message", | ||||||
|   text: "...", |   text: "...", | ||||||
|   sfx: "squeak.mp3" |   sfx: "squeak.mp3", | ||||||
| } | }; | ||||||
|  |  | ||||||
| export const sceneBat: VNScene = compile([ | export const sceneBat: VNScene = compile([ | ||||||
|   squeak, |   squeak, | ||||||
| @@ -25,8 +25,8 @@ export const sceneBat: VNScene = compile([ | |||||||
| const doorbell: VNSceneBasisPart = { | const doorbell: VNSceneBasisPart = { | ||||||
|   type: "message", |   type: "message", | ||||||
|   text: "...", |   text: "...", | ||||||
|   sfx: "doorbell.mp3" |   sfx: "doorbell.mp3", | ||||||
| } | }; | ||||||
|  |  | ||||||
| export const sceneStealth: VNScene = compile([ | export const sceneStealth: VNScene = compile([ | ||||||
|   doorbell, |   doorbell, | ||||||
| @@ -46,8 +46,8 @@ export const sceneStealth: VNScene = compile([ | |||||||
| const phoneBeep: VNSceneBasisPart = { | const phoneBeep: VNSceneBasisPart = { | ||||||
|   type: "message", |   type: "message", | ||||||
|   text: "...", |   text: "...", | ||||||
|   sfx: "phonebeep.mp3" |   sfx: "phonebeep.mp3", | ||||||
| } | }; | ||||||
|  |  | ||||||
| export const sceneCharm: VNScene = compile([ | export const sceneCharm: VNScene = compile([ | ||||||
|   phoneBeep, |   phoneBeep, | ||||||
| @@ -72,8 +72,8 @@ export const sceneCharm: VNScene = compile([ | |||||||
| const sleepyBreath: VNSceneBasisPart = { | const sleepyBreath: VNSceneBasisPart = { | ||||||
|   type: "message", |   type: "message", | ||||||
|   text: "...", |   text: "...", | ||||||
|   sfx: "sleepyBreath.mp3" |   sfx: "sleepyBreath.mp3", | ||||||
| } | }; | ||||||
|  |  | ||||||
| export const sceneStare: VNScene = compile([ | export const sceneStare: VNScene = compile([ | ||||||
|   sleepyBreath, |   sleepyBreath, | ||||||
| @@ -93,7 +93,7 @@ export const sceneStare: VNScene = compile([ | |||||||
| const party: VNSceneBasisPart = { | const party: VNSceneBasisPart = { | ||||||
|   type: "message", |   type: "message", | ||||||
|   text: "...", |   text: "...", | ||||||
|   sfx: "party.mp3" |   sfx: "party.mp3", | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const sceneParty: VNScene = compile([ | export const sceneParty: VNScene = compile([ | ||||||
| @@ -111,7 +111,7 @@ export const sceneParty: VNScene = compile([ | |||||||
| const ghost: VNSceneBasisPart = { | const ghost: VNSceneBasisPart = { | ||||||
|   type: "message", |   type: "message", | ||||||
|   text: "...", |   text: "...", | ||||||
|   sfx: "ghost.mp3" |   sfx: "ghost.mp3", | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const sceneLore: VNScene = compile([ | export const sceneLore: VNScene = compile([ | ||||||
| @@ -126,4 +126,4 @@ export const sceneLore: VNScene = compile([ | |||||||
|   ghost, |   ghost, | ||||||
|   "Yeah. They remember.", |   "Yeah. They remember.", | ||||||
|   ghost, |   ghost, | ||||||
| ]); | ]); | ||||||
|   | |||||||
| @@ -17,11 +17,13 @@ export class Color { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   static parseHexCode(hexCode: string) { |   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 regex1 = | ||||||
|     const regex2 = /#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})?/; |       /#([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); |     let result = regex1.exec(hexCode) ?? regex2.exec(hexCode); | ||||||
|     if (result == null) { |     if (result == null) { | ||||||
|       throw `could not parse color: ${hexCode}` |       throw `could not parse color: ${hexCode}`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let parseGroup = (s: string | undefined): number => { |     let parseGroup = (s: string | undefined): number => { | ||||||
| @@ -32,7 +34,7 @@ export class Color { | |||||||
|         return 17 * parseInt(s, 16); |         return 17 * parseInt(s, 16); | ||||||
|       } |       } | ||||||
|       return parseInt(s, 16); |       return parseInt(s, 16); | ||||||
|     } |     }; | ||||||
|     return new Color( |     return new Color( | ||||||
|       parseGroup(result[1]), |       parseGroup(result[1]), | ||||||
|       parseGroup(result[2]), |       parseGroup(result[2]), | ||||||
| @@ -42,7 +44,7 @@ export class Color { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   toStyle(): string { |   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 { |   toString(): string { | ||||||
|     return `${this.x},${this.y}` |     return `${this.x},${this.y}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   offset(other: Point | Size): Point { |   offset(other: Point | Size): Point { | ||||||
| @@ -109,7 +111,7 @@ export class Size { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   toString(): string { |   toString(): string { | ||||||
|     return `${this.w}x${this.h}` |     return `${this.w}x${this.h}`; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -127,7 +129,12 @@ export class Rect { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   contains(other: Point) { |   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) { |   overlaps(other: Rect) { | ||||||
| @@ -156,20 +163,20 @@ export class Grid<T> { | |||||||
|     for (let y = 0; y < size.h; y++) { |     for (let y = 0; y < size.h; y++) { | ||||||
|       let row = []; |       let row = []; | ||||||
|       for (let x = 0; x < size.w; x++) { |       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); |       this.#data.push(row); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static createGridFromMultilineString(multiline: string): Grid<string> { |   static createGridFromMultilineString(multiline: string): Grid<string> { | ||||||
|     let lines = [] |     let lines = []; | ||||||
|     for (let line of multiline.split("\n")) { |     for (let line of multiline.split("\n")) { | ||||||
|       let trimmedLine = line.trim(); |       let trimmedLine = line.trim(); | ||||||
|       if (trimmedLine == "") { |       if (trimmedLine == "") { | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|       lines.push(trimmedLine) |       lines.push(trimmedLine); | ||||||
|     } |     } | ||||||
|     return this.createGridFromStringArray(lines); |     return this.createGridFromStringArray(lines); | ||||||
|   } |   } | ||||||
| @@ -181,17 +188,14 @@ export class Grid<T> { | |||||||
|       let w1 = ary[i].length; |       let w1 = ary[i].length; | ||||||
|       let w2 = ary[i + 1].length; |       let w2 = ary[i + 1].length; | ||||||
|       if (w1 != w2) { |       if (w1 != w2) { | ||||||
|         throw `createGridFromStringArray: must be grid-shaped, got ${ary}` |         throw `createGridFromStringArray: must be grid-shaped, got ${ary}`; | ||||||
|       } |       } | ||||||
|       w = w1; |       w = w1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return new Grid( |     return new Grid(new Size(w, h), (xy) => { | ||||||
|       new Size(w, h), |       return ary[xy.y].charAt(xy.x); | ||||||
|       (xy) => { |     }); | ||||||
|         return ary[xy.y].charAt(xy.x); |  | ||||||
|       } |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static createGridFromJaggedArray<T>(ary: Array<Array<T>>): Grid<T> { |   static createGridFromJaggedArray<T>(ary: Array<Array<T>>): Grid<T> { | ||||||
| @@ -201,17 +205,14 @@ export class Grid<T> { | |||||||
|       let w1 = ary[i].length; |       let w1 = ary[i].length; | ||||||
|       let w2 = ary[i + 1].length; |       let w2 = ary[i + 1].length; | ||||||
|       if (w1 != w2) { |       if (w1 != w2) { | ||||||
|         throw `createGridFromJaggedArray: must be grid-shaped, got ${ary}` |         throw `createGridFromJaggedArray: must be grid-shaped, got ${ary}`; | ||||||
|       } |       } | ||||||
|       w = w1; |       w = w1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return new Grid( |     return new Grid(new Size(w, h), (xy) => { | ||||||
|       new Size(w, h), |       return ary[xy.y][xy.x]; | ||||||
|       (xy) => { |     }); | ||||||
|         return ary[xy.y][xy.x]; |  | ||||||
|       } |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   map<T2>(cbCell: (content: T, position: Point) => T2) { |   map<T2>(cbCell: (content: T, position: Point) => T2) { | ||||||
| @@ -220,10 +221,14 @@ export class Grid<T> { | |||||||
|  |  | ||||||
|   #checkPosition(position: Point) { |   #checkPosition(position: Point) { | ||||||
|     if ( |     if ( | ||||||
|       (position.x < 0 || position.x >= this.size.w || Math.floor(position.x) != position.x) || |       position.x < 0 || | ||||||
|       (position.y < 0 || position.y >= this.size.h || Math.floor(position.y) != position.y) |       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 { | export enum AlignX { | ||||||
|   Left = 0, |   Left = 0, | ||||||
|   Center = 1, |   Center = 1, | ||||||
|   Right = 2 |   Right = 2, | ||||||
| } | } | ||||||
|  |  | ||||||
| export enum AlignY { | export enum AlignY { | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ class Assets { | |||||||
|     //  and then wait for isLoaded to return true) |     //  and then wait for isLoaded to return true) | ||||||
|     for (let filename in this.#images) { |     for (let filename in this.#images) { | ||||||
|       if (!this.#images[filename].complete) { |       if (!this.#images[filename].complete) { | ||||||
|         return false |         return false; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -29,7 +29,7 @@ class Assets { | |||||||
|       element.src = filename; |       element.src = filename; | ||||||
|       this.#images[filename] = element; |       this.#images[filename] = element; | ||||||
|     } |     } | ||||||
|     return element |     return element; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -38,4 +38,3 @@ let active: Assets = new Assets(); | |||||||
| export function getAssets(): Assets { | export function getAssets(): Assets { | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,18 +1,17 @@ | |||||||
| const MAX_UPDATES_BANKED: number = 20.0; | const MAX_UPDATES_BANKED: number = 20.0; | ||||||
|  |  | ||||||
| // always run physics at 240 hz | // 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 { | class Clock { | ||||||
|   #lastTimestamp: number | undefined; |   #lastTimestamp: number | undefined; | ||||||
|   #updatesBanked: number |   #updatesBanked: number; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     this.#lastTimestamp = undefined; |     this.#lastTimestamp = undefined; | ||||||
|     this.#updatesBanked = 0.0 |     this.#updatesBanked = 0.0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   recordTimestamp(timestamp: number) { |   recordTimestamp(timestamp: number) { | ||||||
|     if (this.#lastTimestamp) { |     if (this.#lastTimestamp) { | ||||||
|       let delta = timestamp - this.#lastTimestamp; |       let delta = timestamp - this.#lastTimestamp; | ||||||
| @@ -26,7 +25,7 @@ class Clock { | |||||||
|     // and remove one draw from the bank |     // and remove one draw from the bank | ||||||
|     if (this.#updatesBanked > 1) { |     if (this.#updatesBanked > 1) { | ||||||
|       this.#updatesBanked -= 1; |       this.#updatesBanked -= 1; | ||||||
|       return true |       return true; | ||||||
|     } |     } | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| @@ -40,5 +39,3 @@ let active: Clock = new Clock(); | |||||||
| export function getClock(): Clock { | export function getClock(): Clock { | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import {getScreen} from "./screen.ts"; | import { getScreen } from "./screen.ts"; | ||||||
| import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts"; | import { AlignX, AlignY, Color, Point, Size } from "../datatypes.ts"; | ||||||
| import {mainFont} from "./font.ts"; | import { mainFont } from "./font.ts"; | ||||||
| import {Sprite} from "./sprite.ts"; | import { Sprite } from "./sprite.ts"; | ||||||
|  |  | ||||||
| class Drawing { | class Drawing { | ||||||
|   camera: Point; |   camera: Point; | ||||||
| @@ -19,7 +19,9 @@ class Drawing { | |||||||
|     this.camera = oldCamera; |     this.camera = oldCamera; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get size() { return getScreen().size; } |   get size() { | ||||||
|  |     return getScreen().size; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   invertRect(position: Point, size: Size) { |   invertRect(position: Point, size: Size) { | ||||||
|     position = this.camera.negate().offset(position); |     position = this.camera.negate().offset(position); | ||||||
| @@ -31,8 +33,8 @@ class Drawing { | |||||||
|       Math.floor(position.x), |       Math.floor(position.x), | ||||||
|       Math.floor(position.y), |       Math.floor(position.y), | ||||||
|       Math.floor(size.w), |       Math.floor(size.w), | ||||||
|       Math.floor(size.h) |       Math.floor(size.h), | ||||||
|     ) |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   fillRect(position: Point, size: Size, color: Color) { |   fillRect(position: Point, size: Size, color: Color) { | ||||||
| @@ -44,7 +46,7 @@ class Drawing { | |||||||
|       Math.floor(position.x), |       Math.floor(position.x), | ||||||
|       Math.floor(position.y), |       Math.floor(position.y), | ||||||
|       Math.floor(size.w), |       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.x) + 0.5, | ||||||
|       Math.floor(position.y) + 0.5, |       Math.floor(position.y) + 0.5, | ||||||
|       Math.floor(size.w) - 1, |       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); |     position = this.camera.negate().offset(position); | ||||||
|  |  | ||||||
|     let ctx = getScreen().unsafeMakeContext(); |     let ctx = getScreen().unsafeMakeContext(); | ||||||
| @@ -72,19 +79,30 @@ class Drawing { | |||||||
|       alignX: options?.alignX, |       alignX: options?.alignX, | ||||||
|       alignY: options?.alignY, |       alignY: options?.alignY, | ||||||
|       forceWidth: options?.forceWidth, |       forceWidth: options?.forceWidth, | ||||||
|       color |       color, | ||||||
|     }) |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   measureText(text: string, forceWidth?: number): Size { |   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); |     position = this.camera.negate().offset(position); | ||||||
|  |  | ||||||
|     let ctx = getScreen().unsafeMakeContext(); |     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 { | export function getDrawing(): Drawing { | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import {getAssets} from "./assets.ts"; | import { getAssets } from "./assets.ts"; | ||||||
| import fontSheet from '../../art/fonts/vga_8x16.png'; | import fontSheet from "../../art/fonts/vga_8x16.png"; | ||||||
| import {AlignX, AlignY, Color, Point, Size} from "../datatypes.ts"; | import { AlignX, AlignY, Color, Point, Size } from "../datatypes.ts"; | ||||||
|  |  | ||||||
| class Font { | class Font { | ||||||
|   #filename: string; |   #filename: string; | ||||||
| @@ -14,18 +14,28 @@ class Font { | |||||||
|     this.#cellsPerSheet = cellsPerSheet; |     this.#cellsPerSheet = cellsPerSheet; | ||||||
|     this.#pixelsPerCell = pixelsPerCell; |     this.#pixelsPerCell = pixelsPerCell; | ||||||
|     this.#tintingCanvas = document.createElement("canvas"); |     this.#tintingCanvas = document.createElement("canvas"); | ||||||
|     this.#tintedVersions = {} |     this.#tintedVersions = {}; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get #cx(): number { return this.#cellsPerSheet.w } |   get #cx(): number { | ||||||
|   get #cy(): number { return this.#cellsPerSheet.h } |     return this.#cellsPerSheet.w; | ||||||
|   get #px(): number { return this.#pixelsPerCell.w } |   } | ||||||
|   get #py(): number { return this.#pixelsPerCell.h } |   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 { |   #getTintedImage(color: string): HTMLImageElement | null { | ||||||
|     let image = getAssets().getImage(this.#filename); |     let image = getAssets().getImage(this.#filename); | ||||||
|  |  | ||||||
|     if (!image.complete) { return null; } |     if (!image.complete) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let tintedVersion = this.#tintedVersions[color]; |     let tintedVersion = this.#tintedVersions[color]; | ||||||
|     if (tintedVersion != undefined) { |     if (tintedVersion != undefined) { | ||||||
| @@ -36,7 +46,7 @@ class Font { | |||||||
|     let h = image.height; |     let h = image.height; | ||||||
|  |  | ||||||
|     if (!(w == this.#cx * this.#px && h == this.#cy * this.#py)) { |     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; |     this.#tintingCanvas.width = w; | ||||||
| @@ -55,17 +65,28 @@ class Font { | |||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   internalDrawText({ctx, text, position, alignX, alignY, forceWidth, color}: { |   internalDrawText({ | ||||||
|     ctx: CanvasRenderingContext2D, |     ctx, | ||||||
|     text: string, |     text, | ||||||
|     position: Point, alignX?: AlignX, alignY?: AlignY, |     position, | ||||||
|     forceWidth?: number, color: Color |     alignX, | ||||||
|  |     alignY, | ||||||
|  |     forceWidth, | ||||||
|  |     color, | ||||||
|  |   }: { | ||||||
|  |     ctx: CanvasRenderingContext2D; | ||||||
|  |     text: string; | ||||||
|  |     position: Point; | ||||||
|  |     alignX?: AlignX; | ||||||
|  |     alignY?: AlignY; | ||||||
|  |     forceWidth?: number; | ||||||
|  |     color: Color; | ||||||
|   }) { |   }) { | ||||||
|     alignX = alignX == undefined ? AlignX.Left : alignX; |     alignX = alignX == undefined ? AlignX.Left : alignX; | ||||||
|     alignY = alignY == undefined ? AlignY.Top : alignY; |     alignY = alignY == undefined ? AlignY.Top : alignY; | ||||||
|     forceWidth = forceWidth == undefined ? 65535 : forceWidth; |     forceWidth = forceWidth == undefined ? 65535 : forceWidth; | ||||||
|  |  | ||||||
|     let image = this.#getTintedImage(color.toStyle()) |     let image = this.#getTintedImage(color.toStyle()); | ||||||
|     if (image == null) { |     if (image == null) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -73,43 +94,80 @@ class Font { | |||||||
|     let sz = this.#glyphwise(text, forceWidth, () => {}); |     let sz = this.#glyphwise(text, forceWidth, () => {}); | ||||||
|     let offsetX = position.x; |     let offsetX = position.x; | ||||||
|     let offsetY = position.y; |     let offsetY = position.y; | ||||||
|     offsetX += (alignX == AlignX.Left ? 0 : alignX == AlignX.Center ? -sz.w / 2 : - sz.w) |     offsetX += | ||||||
|     offsetY += (alignY == AlignY.Top ? 0 : alignY == AlignY.Middle ? -sz.h / 2 : - sz.h) |       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) => { |     this.#glyphwise(text, forceWidth, (cx, cy, char) => { | ||||||
|       let srcIx = char.charCodeAt(0); |       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 srcCx = ix % this.#cx; | ||||||
|     let srcCy = Math.floor(ix / this.#cx); |     let srcCy = Math.floor(ix / this.#cx); | ||||||
|     let srcPx = srcCx * this.#px; |     let srcPx = srcCx * this.#px; | ||||||
|     let srcPy = srcCy * this.#py; |     let srcPy = srcCy * this.#py; | ||||||
|     ctx.drawImage( |     ctx.drawImage( | ||||||
|       image, |       image, | ||||||
|       srcPx, srcPy, this.#px, this.#py, |       srcPx, | ||||||
|       Math.floor(x), Math.floor(y), this.#px, this.#py |       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, () => {}); |     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 cx = 0; | ||||||
|     let cy = 0; |     let cy = 0; | ||||||
|     let cw = 0; |     let cw = 0; | ||||||
|     let ch = 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); |     text = betterWordWrap(text, wcx); | ||||||
|  |  | ||||||
|     for (let i = 0; i < text.length; i++) { |     for (let i = 0; i < text.length; i++) { | ||||||
|       let char = text[i] |       let char = text[i]; | ||||||
|       if (char == '\n') { |       if (char == "\n") { | ||||||
|         cx = 0; |         cx = 0; | ||||||
|         cy += 1; |         cy += 1; | ||||||
|         ch = cy + 1; |         ch = cy + 1; | ||||||
| @@ -121,7 +179,7 @@ class Font { | |||||||
|           ch = cy + 1; |           ch = cy + 1; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         callback(cx, cy, char) |         callback(cx, cy, char); | ||||||
|         cx += 1; |         cx += 1; | ||||||
|         cw = Math.max(cw, cx); |         cw = Math.max(cw, cx); | ||||||
|         ch = cy + 1; |         ch = cy + 1; | ||||||
| @@ -132,15 +190,15 @@ class Font { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| // https://stackoverflow.com/users/1993501/edi9999 | // https://stackoverflow.com/users/1993501/edi9999 | ||||||
| function betterWordWrap(s: string, wcx?: number) { | function betterWordWrap(s: string, wcx?: number) { | ||||||
|   if (wcx === undefined) { |   if (wcx === undefined) { | ||||||
|     return s; |     return s; | ||||||
|   } |   } | ||||||
|   return s.replace( |   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 { pollAndTouch } from "./screen.ts"; | ||||||
| import {getClock} from "./clock.ts"; | import { getClock } from "./clock.ts"; | ||||||
| import {getInput, setupInput} from "./input.ts"; | import { getInput, setupInput } from "./input.ts"; | ||||||
| import {IGame} from "../datatypes.ts"; | import { IGame } from "../datatypes.ts"; | ||||||
|  |  | ||||||
| export function hostGame(game: IGame) { | export function hostGame(game: IGame) { | ||||||
|   let gameCanvas = document.getElementById("game") as HTMLCanvasElement; |   let gameCanvas = document.getElementById("game") as HTMLCanvasElement; | ||||||
|   setupInput(gameCanvas); |   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) { | function onFrame(game: IGame, timestamp: number | undefined) { | ||||||
| @@ -31,4 +31,3 @@ function onFrame(game: IGame, timestamp: number | undefined) { | |||||||
| function onFrameFixScreen(canvas: HTMLCanvasElement) { | function onFrameFixScreen(canvas: HTMLCanvasElement) { | ||||||
|   pollAndTouch(canvas); |   pollAndTouch(canvas); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import {getScreen} from "./screen.ts"; | import { getScreen } from "./screen.ts"; | ||||||
| import {Point} from "../datatypes.ts"; | import { Point } from "../datatypes.ts"; | ||||||
|  |  | ||||||
| function handleKey(e: KeyboardEvent, down: boolean) { | function handleKey(e: KeyboardEvent, down: boolean) { | ||||||
|   active.handleKeyDown(e.key, down); |   active.handleKeyDown(e.key, down); | ||||||
| @@ -12,25 +12,31 @@ function handleMouseMove(canvas: HTMLCanvasElement, m: MouseEvent) { | |||||||
|   if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) { |   if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) { | ||||||
|     return; |     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) { |   if (canvas.offsetWidth == 0 || canvas.offsetHeight == 0) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   active.handleMouseMove(m.offsetX / canvas.offsetWidth, m.offsetY / canvas.offsetHeight); |   active.handleMouseMove( | ||||||
|   let button: MouseButton | null = ( |     m.offsetX / canvas.offsetWidth, | ||||||
|     m.button == 0 ? "leftMouse" : |     m.offsetY / canvas.offsetHeight, | ||||||
|       m.button == 1 ? "rightMouse" : |   ); | ||||||
|         null |   let button: MouseButton | null = | ||||||
|   ) |     m.button == 0 ? "leftMouse" : m.button == 1 ? "rightMouse" : null; | ||||||
|   if (button != null) { |   if (button != null) { | ||||||
|     active.handleMouseDown(button, down); |     active.handleMouseDown(button, down); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| export function setupInput(canvas: HTMLCanvasElement) { | export function setupInput(canvas: HTMLCanvasElement) { | ||||||
|   canvas.addEventListener("keyup", (k) => handleKey(k, false)); |   canvas.addEventListener("keyup", (k) => handleKey(k, false)); | ||||||
|   document.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)); |   document.addEventListener("keydown", (k) => handleKey(k, true)); | ||||||
|   canvas.addEventListener("mouseout", (_) => handleMouseOut()); |   canvas.addEventListener("mouseout", (_) => handleMouseOut()); | ||||||
|   canvas.addEventListener("mousemove", (m) => handleMouseMove(canvas, m)); |   canvas.addEventListener("mousemove", (m) => handleMouseMove(canvas, m)); | ||||||
|   canvas.addEventListener("mousedown", (m) => handleMouseButton(canvas, m, true)); |   canvas.addEventListener("mousedown", (m) => | ||||||
|   canvas.addEventListener("mouseup", (m) => handleMouseButton(canvas, m, false)); |     handleMouseButton(canvas, m, true), | ||||||
|  |   ); | ||||||
|  |   canvas.addEventListener("mouseup", (m) => | ||||||
|  |     handleMouseButton(canvas, m, false), | ||||||
|  |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| export type MouseButton = "leftMouse" | "rightMouse"; | export type MouseButton = "leftMouse" | "rightMouse"; | ||||||
| @@ -60,8 +70,8 @@ class Input { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     this.#previousKeyDown = {...this.#keyDown}; |     this.#previousKeyDown = { ...this.#keyDown }; | ||||||
|     this.#previousMouseDown = {...this.#mouseDown}; |     this.#previousMouseDown = { ...this.#mouseDown }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   handleMouseDown(name: string, down: boolean) { |   handleMouseDown(name: string, down: boolean) { | ||||||
| @@ -73,51 +83,56 @@ class Input { | |||||||
|  |  | ||||||
|   handleMouseMove(x: number, y: number) { |   handleMouseMove(x: number, y: number) { | ||||||
|     let screen = getScreen(); |     let screen = getScreen(); | ||||||
|     if (x < 0.0 || x >= 1.0) { this.#mousePosition = null; } |     if (x < 0.0 || x >= 1.0) { | ||||||
|     if (y < 0.0 || y >= 1.0) { this.#mousePosition = null; } |       this.#mousePosition = null; | ||||||
|  |     } | ||||||
|  |     if (y < 0.0 || y >= 1.0) { | ||||||
|  |       this.#mousePosition = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let w = screen.size.w; |     let w = screen.size.w; | ||||||
|     let h = screen.size.h; |     let h = screen.size.h; | ||||||
|     this.#mousePosition = new Point( |     this.#mousePosition = new Point(Math.floor(x * w), Math.floor(y * h)); | ||||||
|       Math.floor(x * w), |  | ||||||
|       Math.floor(y * h), |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isMouseDown(btn: MouseButton) : boolean { |   isMouseDown(btn: MouseButton): boolean { | ||||||
|     return this.#mouseDown[btn]; |     return this.#mouseDown[btn]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isMouseClicked(btn: MouseButton) : boolean { |   isMouseClicked(btn: MouseButton): boolean { | ||||||
|     return this.#mouseDown[btn] && !this.#previousMouseDown[btn]; |     return this.#mouseDown[btn] && !this.#previousMouseDown[btn]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isMouseReleased(btn: MouseButton) : boolean { |   isMouseReleased(btn: MouseButton): boolean { | ||||||
|     return !this.#mouseDown[btn] && this.#previousMouseDown[btn]; |     return !this.#mouseDown[btn] && this.#previousMouseDown[btn]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get mousePosition(): Point | null { |   get mousePosition(): Point | null { | ||||||
|     return this.#mousePosition |     return this.#mousePosition; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isKeyDown(key: string) : boolean { |   isKeyDown(key: string): boolean { | ||||||
|     return this.#keyDown[key]; |     return this.#keyDown[key]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isKeyPressed(key: string) : boolean { |   isKeyPressed(key: string): boolean { | ||||||
|     return this.#keyDown[key] && !this.#previousKeyDown[key]; |     return this.#keyDown[key] && !this.#previousKeyDown[key]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isKeyReleased(key: string) : boolean { |   isKeyReleased(key: string): boolean { | ||||||
|     return !this.#keyDown[key] && this.#previousKeyDown[key]; |     return !this.#keyDown[key] && this.#previousKeyDown[key]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   isAnythingPressed(): boolean { |   isAnythingPressed(): boolean { | ||||||
|     for (let k of Object.keys(this.#keyDown)) { |     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)) { |     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; |     return false; | ||||||
|   } |   } | ||||||
| @@ -127,4 +142,4 @@ let active = new Input(); | |||||||
|  |  | ||||||
| export function getInput(): Input { | export function getInput(): Input { | ||||||
|   return active; |   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 | // TODO: Just switch to the same pattern as everywhere else | ||||||
| // (without repeatedly reassigning the variable) | // (without repeatedly reassigning the variable) | ||||||
| class Screen { | class Screen { | ||||||
|   #canvas: HTMLCanvasElement |   #canvas: HTMLCanvasElement; | ||||||
|   size: Size |   size: Size; | ||||||
|  |  | ||||||
|   constructor(canvas: HTMLCanvasElement, size: Size) { |   constructor(canvas: HTMLCanvasElement, size: Size) { | ||||||
|     this.#canvas = canvas; |     this.#canvas = canvas; | ||||||
|     this.size = size |     this.size = size; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   unsafeMakeContext(): CanvasRenderingContext2D { |   unsafeMakeContext(): CanvasRenderingContext2D { | ||||||
| @@ -26,8 +26,7 @@ class Screen { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | let active: Screen | undefined = undefined; | ||||||
| let active: Screen | undefined = undefined |  | ||||||
|  |  | ||||||
| // TODO: Move these to Game? | // TODO: Move these to Game? | ||||||
| export let desiredWidth = 400; | export let desiredWidth = 400; | ||||||
| @@ -45,9 +44,9 @@ export function pollAndTouch(canvas: HTMLCanvasElement) { | |||||||
|  |  | ||||||
|   let div = 0; |   let div = 0; | ||||||
|   while ( |   while ( | ||||||
|     (div < divisors.length - 1) && |     div < divisors.length - 1 && | ||||||
|     (realWidth / divisors[div + 1] >= desiredWidth) && |     realWidth / divisors[div + 1] >= desiredWidth && | ||||||
|     (realHeight / divisors[div + 1] >= desiredHeight) |     realHeight / divisors[div + 1] >= desiredHeight | ||||||
|   ) { |   ) { | ||||||
|     div += 1; |     div += 1; | ||||||
|   } |   } | ||||||
| @@ -60,9 +59,7 @@ export function pollAndTouch(canvas: HTMLCanvasElement) { | |||||||
|  |  | ||||||
| export function getScreen(): Screen { | export function getScreen(): Screen { | ||||||
|   if (active === undefined) { |   if (active === undefined) { | ||||||
|     throw `screen should have been defined: ${active}` |     throw `screen should have been defined: ${active}`; | ||||||
|   } |   } | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import {getAssets} from "./assets.ts"; | import { getAssets } from "./assets.ts"; | ||||||
| import {Point, Size} from "../datatypes.ts"; | import { Point, Size } from "../datatypes.ts"; | ||||||
|  |  | ||||||
|  |  | ||||||
| export class Sprite { | export class Sprite { | ||||||
|   readonly imageSet: string; |   readonly imageSet: string; | ||||||
| @@ -11,7 +10,13 @@ export class Sprite { | |||||||
|   // number of frames |   // number of frames | ||||||
|   readonly nFrames: number; |   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.imageSet = imageSet; | ||||||
|     this.pixelsPerSubimage = pixelsPerSubimage; |     this.pixelsPerSubimage = pixelsPerSubimage; | ||||||
|     this.origin = origin; |     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; |     ix = ix == undefined ? 0 : ix; | ||||||
|     xScale = xScale == undefined ? 1.0 : xScale; |     xScale = xScale == undefined ? 1.0 : xScale; | ||||||
|     yScale = yScale == undefined ? 1.0 : yScale; |     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(x), Math.floor(y)); | ||||||
|     ctx.translate(Math.floor(position.x), Math.floor(position.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.scale(xScale, yScale); | ||||||
|     ctx.translate(-this.origin.x, -this.origin.y); |     ctx.translate(-this.origin.x, -this.origin.y); | ||||||
|  |  | ||||||
| @@ -41,6 +61,16 @@ export class Sprite { | |||||||
|     let srcCy = Math.floor(ix / this.cellsPerSheet.w); |     let srcCy = Math.floor(ix / this.cellsPerSheet.w); | ||||||
|     let srcPx = srcCx * this.pixelsPerSubimage.w; |     let srcPx = srcCx * this.pixelsPerSubimage.w; | ||||||
|     let srcPy = srcCy * this.pixelsPerSubimage.h; |     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; |   padding: 0; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   width: 100%; |   width: 100%; | ||||||
| @@ -9,4 +10,4 @@ html, body { | |||||||
|   image-rendering: pixelated; |   image-rendering: pixelated; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 100%; |   height: 100%; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import {getInput} from "./internal/input.ts"; | import { getInput } from "./internal/input.ts"; | ||||||
| import {getDrawing} from "./internal/drawing.ts"; | import { getDrawing } from "./internal/drawing.ts"; | ||||||
|  |  | ||||||
| // input reexports | // input reexports | ||||||
| export let I = getInput(); | export let I = getInput(); | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								src/game.ts
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								src/game.ts
									
									
									
									
									
								
							| @@ -1,30 +1,32 @@ | |||||||
| import {BG_OUTER} from "./colors.ts"; | import { BG_OUTER } from "./colors.ts"; | ||||||
| import {D, I} from "./engine/public.ts"; | import { D, I } from "./engine/public.ts"; | ||||||
| import {IGame, Point, Size} from "./engine/datatypes.ts"; | import { IGame, Point, Size } from "./engine/datatypes.ts"; | ||||||
| import {getPageLocation, Page} from "./layout.ts"; | import { getPageLocation, Page } from "./layout.ts"; | ||||||
| import {getHotbar, Hotbar} from "./hotbar.ts"; | import { getHotbar, Hotbar } from "./hotbar.ts"; | ||||||
| import {getSkillsModal, SkillsModal} from "./skillsmodal.ts"; | import { getSkillsModal, SkillsModal } from "./skillsmodal.ts"; | ||||||
| import {getSleepModal, SleepModal} from "./sleepmodal.ts"; | import { getSleepModal, SleepModal } from "./sleepmodal.ts"; | ||||||
| import {getVNModal, VNModal} from "./vnmodal.ts"; | import { getVNModal, VNModal } from "./vnmodal.ts"; | ||||||
| import {Gameplay, getGameplay} from "./gameplay.ts"; | import { Gameplay, getGameplay } from "./gameplay.ts"; | ||||||
| import {getEndgameModal} from "./endgamemodal.ts"; | import { getEndgameModal } from "./endgamemodal.ts"; | ||||||
| import {CheckModal, getCheckModal} from "./checkmodal.ts"; | import { CheckModal, getCheckModal } from "./checkmodal.ts"; | ||||||
|  |  | ||||||
| class MenuCamera { | class MenuCamera { | ||||||
|   // measured in whole screens |   // measured in whole screens | ||||||
|   position: Point; |   position: Point; | ||||||
|   target: Point; |   target: Point; | ||||||
|  |  | ||||||
|   constructor({position, target}: {position: Point, target: Point}) { |   constructor({ position, target }: { position: Point; target: Point }) { | ||||||
|     this.position = position; |     this.position = position; | ||||||
|     this.target = target; |     this.target = target; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     let adjust = (x0: number, x1: number) => { |     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; |       return (x0 * 8 + x1 * 2) / 10; | ||||||
|     } |     }; | ||||||
|     this.position = new Point( |     this.position = new Point( | ||||||
|       adjust(this.position.x, this.target.x), |       adjust(this.position.x, this.target.x), | ||||||
|       adjust(this.position.y, this.target.y), |       adjust(this.position.y, this.target.y), | ||||||
| @@ -49,14 +51,18 @@ export class Game implements IGame { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     if (I.isKeyPressed("w")) { this.page = "Gameplay" } |     if (I.isKeyPressed("w")) { | ||||||
|     if (I.isKeyPressed("s")) { this.page = "Thralls" } |       this.page = "Gameplay"; | ||||||
|  |     } | ||||||
|  |     if (I.isKeyPressed("s")) { | ||||||
|  |       this.page = "Thralls"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     this.camera.target = getPageLocation(this.page); |     this.camera.target = getPageLocation(this.page); | ||||||
|     D.camera = new Point( |     D.camera = new Point( | ||||||
|       D.size.w * this.camera.position.x, |       D.size.w * this.camera.position.x, | ||||||
|       D.size.h * this.camera.position.y, |       D.size.h * this.camera.position.y, | ||||||
|     ) |     ); | ||||||
|     this.camera.update(); |     this.camera.update(); | ||||||
|  |  | ||||||
|     // state-specific updates |     // 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}) |     // mainFont.drawText({ctx: ctx, text: "You have been given a gift.", x: 0, y: 0}) | ||||||
|     let mouse = I.mousePosition?.offset(D.camera); |     let mouse = I.mousePosition?.offset(D.camera); | ||||||
|     if (mouse != null) { |     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(); |     this.#mainThing?.draw(); | ||||||
|  |  | ||||||
|     if (!this.#mainThing?.blocksHud) { |     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 { withCamera } from "./layout.ts"; | ||||||
| import {getHuntMode} from "./huntmode.ts"; | import { getHuntMode } from "./huntmode.ts"; | ||||||
| import {getHud} from "./hud.ts"; | import { getHud } from "./hud.ts"; | ||||||
|  |  | ||||||
| export class Gameplay { | export class Gameplay { | ||||||
|   update() { |   update() { | ||||||
|     withCamera("Gameplay", () => { |     withCamera("Gameplay", () => { | ||||||
|       getHuntMode().update(); |       getHuntMode().update(); | ||||||
|     }); |     }); | ||||||
|     withCamera("HUD", () => { getHud().update() }) |     withCamera("HUD", () => { | ||||||
|  |       getHud().update(); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     withCamera("Gameplay", () => { |     withCamera("Gameplay", () => { | ||||||
|       getHuntMode().draw(); |       getHuntMode().draw(); | ||||||
|     }); |     }); | ||||||
|     withCamera("HUD", () => { getHud().draw() }) |     withCamera("HUD", () => { | ||||||
|  |       getHud().draw(); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get blocksHud(): boolean { |   get blocksHud(): boolean { | ||||||
| @@ -26,4 +30,3 @@ let active = new Gameplay(); | |||||||
| export function getGameplay(): Gameplay { | export function getGameplay(): Gameplay { | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										113
									
								
								src/gridart.ts
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								src/gridart.ts
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| import {Color, Point, Rect, Size} from "./engine/datatypes.ts"; | import { Color, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
|  |  | ||||||
| export const FLOOR_CELL_SIZE: Size = new Size(48, 48) | export const FLOOR_CELL_SIZE: Size = new Size(48, 48); | ||||||
| export const CEILING_CELL_SIZE: Size = new Size(56, 56) | export const CEILING_CELL_SIZE: Size = new Size(56, 56); | ||||||
| export const HEIGHT_IN_FEET = 12; | export const HEIGHT_IN_FEET = 12; | ||||||
| export const CENTER = new Point(192, 192); | export const CENTER = new Point(192, 192); | ||||||
| export const MOULDING_SZ = new Size(1, 1); | 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.#floorCenter = at.scale(FLOOR_CELL_SIZE).offset(CENTER); | ||||||
|     this.#ceilingCenter = at.scale(CEILING_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.#floorTl = at | ||||||
|     this.#ceilingTl = at.offset(new Point(-0.5, -0.5)).scale(CEILING_CELL_SIZE).offset(CENTER); |       .offset(new Point(-0.5, -0.5)) | ||||||
|     this.#floorBr = at.offset(new Point(0.5, 0.5)).scale(FLOOR_CELL_SIZE).offset(CENTER); |       .scale(FLOOR_CELL_SIZE) | ||||||
|     this.#ceilingBr = at.offset(new Point(0.5, 0.5)).scale(CEILING_CELL_SIZE).offset(CENTER); |       .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 { |   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) { |   drawFloor(color: Color) { | ||||||
| @@ -40,7 +52,8 @@ export class GridArt { | |||||||
|     let diff = Math.abs(this.#ceilingTl.y - this.#floorTl.y); |     let diff = Math.abs(this.#ceilingTl.y - this.#floorTl.y); | ||||||
|     let sign = Math.sign(this.#ceilingTl.y - this.#floorTl.y); |     let sign = Math.sign(this.#ceilingTl.y - this.#floorTl.y); | ||||||
|     // console.log(`diff, sign: ${diff}, ${sign}`) |     // 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 progress = dy / diff; | ||||||
|       let x0 = Math.floor(lerp(progress, this.#floorTl.x, this.#ceilingTl.x)); |       let x0 = Math.floor(lerp(progress, this.#floorTl.x, this.#ceilingTl.x)); | ||||||
|       let x1 = Math.ceil(lerp(progress, this.#floorBr.x, this.#ceilingBr.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 diff = Math.abs(this.#ceilingTl.x - this.#floorTl.x); | ||||||
|     let sign = Math.sign(this.#ceilingTl.x - this.#floorTl.x); |     let sign = Math.sign(this.#ceilingTl.x - this.#floorTl.x); | ||||||
|     // console.log(`diff, sign: ${diff}, ${sign}`) |     // 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 progress = dx / diff; | ||||||
|       let y0 = Math.floor(lerp(progress, this.#floorTl.y, this.#ceilingTl.y)); |       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 y1 = Math.ceil(lerp(progress, this.#floorBr.y, this.#ceilingBr.y)); | ||||||
|  |  | ||||||
|       let x = this.#floorTl.x + sign * dx; |       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) { |   drawWallTop(color: Color) { | ||||||
|     if (this.#at.y > 0) { return; } |     if (this.#at.y > 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     this.#drawWallTop(color); |     this.#drawWallTop(color); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawWallLeft(color: Color) { |   drawWallLeft(color: Color) { | ||||||
|     if (this.#at.x > 0) { return; } |     if (this.#at.x > 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     this.#drawWallLeft(color); |     this.#drawWallLeft(color); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawWallBottom(color: 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); |     new GridArt(this.#at.offset(new Point(0, 1))).#drawWallTop(color); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawWallRight(color: 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); |     new GridArt(this.#at.offset(new Point(1, 0))).#drawWallLeft(color); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawMouldingTop(color: Color) { |   drawMouldingTop(color: Color) { | ||||||
|     let lhs = this.#ceilingTl.offset(new Point(0, -MOULDING_SZ.h)) |     let lhs = this.#ceilingTl.offset(new Point(0, -MOULDING_SZ.h)); | ||||||
|     D.fillRect(lhs, new Size(CEILING_CELL_SIZE.w, MOULDING_SZ.h), color) |     D.fillRect(lhs, new Size(CEILING_CELL_SIZE.w, MOULDING_SZ.h), color); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawMouldingTopLeft(color: 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) { |   drawMouldingLeft(color: Color) { | ||||||
|     let lhs = this.#ceilingTl.offset(new Point(-MOULDING_SZ.w, 0)) |     let lhs = this.#ceilingTl.offset(new Point(-MOULDING_SZ.w, 0)); | ||||||
|     D.fillRect(lhs, new Size(MOULDING_SZ.w, CEILING_CELL_SIZE.h), color) |     D.fillRect(lhs, new Size(MOULDING_SZ.w, CEILING_CELL_SIZE.h), color); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawMouldingTopRight(color: 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) { |   drawMouldingBottom(color: Color) { | ||||||
|     let lhs = this.#ceilingTl.offset(new Point(0, CEILING_CELL_SIZE.h)) |     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) |     D.fillRect(lhs, new Size(CEILING_CELL_SIZE.w, MOULDING_SZ.h), color); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawMouldingBottomLeft(color: 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) { |   drawMouldingRight(color: Color) { | ||||||
|     let lhs = this.#ceilingTl.offset(new Point(CEILING_CELL_SIZE.w, 0)) |     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) |     D.fillRect(lhs, new Size(MOULDING_SZ.w, CEILING_CELL_SIZE.h), color); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   drawMouldingBottomRight(color: 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) { |   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); |     // 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) => { | let lerp = (amt: number, x: number, y: number) => { | ||||||
|   if (amt <= 0) { return x; } |   if (amt <= 0) { | ||||||
|   if (amt >= 1) { return y; } |     return x; | ||||||
|  |   } | ||||||
|  |   if (amt >= 1) { | ||||||
|  |     return y; | ||||||
|  |   } | ||||||
|   return x + (y - x) * amt; |   return x + (y - x) * amt; | ||||||
| } | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| import {Point, Rect, Size} from "./engine/datatypes.ts"; | import { Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {DrawPile} from "./drawpile.ts"; | import { DrawPile } from "./drawpile.ts"; | ||||||
| import {withCamera} from "./layout.ts"; | import { withCamera } from "./layout.ts"; | ||||||
| import {getSkillsModal} from "./skillsmodal.ts"; | import { getSkillsModal } from "./skillsmodal.ts"; | ||||||
| import {addButton} from "./button.ts"; | import { addButton } from "./button.ts"; | ||||||
| import {getSleepModal} from "./sleepmodal.ts"; | import { getSleepModal } from "./sleepmodal.ts"; | ||||||
|  |  | ||||||
| type Button = { | type Button = { | ||||||
|   label: string, |   label: string; | ||||||
|   cbClick: () => void, |   cbClick: () => void; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export class Hotbar { | export class Hotbar { | ||||||
|   #drawpile: DrawPile; |   #drawpile: DrawPile; | ||||||
| @@ -18,12 +18,12 @@ export class Hotbar { | |||||||
|  |  | ||||||
|   get #cellSize(): Size { |   get #cellSize(): Size { | ||||||
|     return new Size(96, 32); |     return new Size(96, 32); | ||||||
| } |   } | ||||||
|  |  | ||||||
|   get size(): Size { |   get size(): Size { | ||||||
|     let {w: cellW, h: cellH} = this.#cellSize; |     let { w: cellW, h: cellH } = this.#cellSize; | ||||||
|     let w = this.#computeButtons().length * cellW; |     let w = this.#computeButtons().length * cellW; | ||||||
|     return new Size(w, cellH) |     return new Size(w, cellH); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #computeButtons(): Button[] { |   #computeButtons(): Button[] { | ||||||
| @@ -31,9 +31,9 @@ export class Hotbar { | |||||||
|     buttons.push({ |     buttons.push({ | ||||||
|       label: "Skills", |       label: "Skills", | ||||||
|       cbClick: () => { |       cbClick: () => { | ||||||
|         getSkillsModal().setShown(true) |         getSkillsModal().setShown(true); | ||||||
|       } |       }, | ||||||
|     }) |     }); | ||||||
|     /* |     /* | ||||||
|     buttons.push({ |     buttons.push({ | ||||||
|       label: "Thralls" |       label: "Thralls" | ||||||
| @@ -42,14 +42,14 @@ export class Hotbar { | |||||||
|     buttons.push({ |     buttons.push({ | ||||||
|       label: "Sleep", |       label: "Sleep", | ||||||
|       cbClick: () => { |       cbClick: () => { | ||||||
|         getSleepModal().setShown(true) |         getSleepModal().setShown(true); | ||||||
|       } |       }, | ||||||
|     }) |     }); | ||||||
|     return buttons; |     return buttons; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     withCamera("Hotbar", () => this.#update()) |     withCamera("Hotbar", () => this.#update()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #update() { |   #update() { | ||||||
| @@ -61,11 +61,16 @@ export class Hotbar { | |||||||
|  |  | ||||||
|     let x = 0; |     let x = 0; | ||||||
|     for (let b of buttons.values()) { |     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; |       x += cellSize.w; | ||||||
|     } |     } | ||||||
|     this.#drawpile.executeOnClick(); |     this.#drawpile.executeOnClick(); | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
| @@ -77,4 +82,4 @@ export class Hotbar { | |||||||
| let active = new Hotbar(); | let active = new Hotbar(); | ||||||
| export function getHotbar(): Hotbar { | export function getHotbar(): Hotbar { | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								src/hud.ts
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								src/hud.ts
									
									
									
									
									
								
							| @@ -1,35 +1,39 @@ | |||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
| import {Point, Size} from "./engine/datatypes.ts"; | import { Point, Size } from "./engine/datatypes.ts"; | ||||||
| import {BG_OUTER, FG_BOLD, FG_TEXT} from "./colors.ts"; | import { BG_OUTER, FG_BOLD, FG_TEXT } from "./colors.ts"; | ||||||
| import {ALL_STATS} from "./datatypes.ts"; | import { ALL_STATS } from "./datatypes.ts"; | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
| import {getHuntMode} from "./huntmode.ts"; | import { getHuntMode } from "./huntmode.ts"; | ||||||
| import {getStateManager} from "./statemanager.ts"; | import { getStateManager } from "./statemanager.ts"; | ||||||
|  |  | ||||||
| export class Hud { | export class Hud { | ||||||
|   get size(): Size { |   get size(): Size { | ||||||
|     return new Size(96, 176) |     return new Size(96, 176); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { } |   update() {} | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), BG_OUTER) |     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(getPlayerProgress().name, new Point(0, 0), FG_BOLD); | ||||||
|     D.drawText(`Level ${getHuntMode().getDepth()}`, new Point(0, 16), FG_TEXT) |     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.drawText( | ||||||
|  |       `Turn ${getStateManager().getTurn()}/${getStateManager().getMaxTurns()}`, | ||||||
|  |       new Point(0, 32), | ||||||
|  |       FG_TEXT, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     let y = 64; |     let y = 64; | ||||||
|     let prog = getPlayerProgress(); |     let prog = getPlayerProgress(); | ||||||
|     for (let s of ALL_STATS.values()) { |     for (let s of ALL_STATS.values()) { | ||||||
|       D.drawText(`${s}`, new Point(0, y), FG_BOLD) |       D.drawText(`${s}`, new Point(0, y), FG_BOLD); | ||||||
|       D.drawText(`${prog.getStat(s)}`, new Point(32, y), FG_TEXT) |       D.drawText(`${prog.getStat(s)}`, new Point(32, y), FG_TEXT); | ||||||
|       let talent = prog.getTalent(s); |       let talent = prog.getTalent(s); | ||||||
|       if (talent > 0) { |       if (talent > 0) { | ||||||
|         D.drawText(`(+${talent})`, new Point(56, y), FG_TEXT) |         D.drawText(`(+${talent})`, new Point(56, y), FG_TEXT); | ||||||
|       } |       } | ||||||
|       if (talent < 0) { |       if (talent < 0) { | ||||||
|         D.drawText(`(${talent})`, new Point(56, y), FG_TEXT) |         D.drawText(`(${talent})`, new Point(56, y), FG_TEXT); | ||||||
|       } |       } | ||||||
|       y += 16; |       y += 16; | ||||||
|     } |     } | ||||||
| @@ -43,4 +47,4 @@ export class Hud { | |||||||
| let active = new Hud(); | let active = new Hud(); | ||||||
| export function getHud(): Hud { | export function getHud(): Hud { | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										184
									
								
								src/huntmode.ts
									
									
									
									
									
								
							
							
						
						
									
										184
									
								
								src/huntmode.ts
									
									
									
									
									
								
							| @@ -1,34 +1,33 @@ | |||||||
| import {Point} from "./engine/datatypes.ts"; | import { Point } from "./engine/datatypes.ts"; | ||||||
| import {DrawPile} from "./drawpile.ts"; | import { DrawPile } from "./drawpile.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
| import {sprThrallLore} from "./sprites.ts"; | import { sprThrallLore } from "./sprites.ts"; | ||||||
| import { | import { | ||||||
|   BG_INSET, |   BG_INSET, | ||||||
|   BG_WALL_OR_UNREVEALED, |   BG_WALL_OR_UNREVEALED, | ||||||
|   FG_BOLD, |   FG_BOLD, | ||||||
|   FG_MOULDING, |   FG_MOULDING, | ||||||
|   FG_TEXT |   FG_TEXT, | ||||||
| } from "./colors.ts"; | } from "./colors.ts"; | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
| import {Architecture, LoadedNewMap} from "./newmap.ts"; | import { Architecture, LoadedNewMap } from "./newmap.ts"; | ||||||
| import {FLOOR_CELL_SIZE, GridArt} from "./gridart.ts"; | import { FLOOR_CELL_SIZE, GridArt } from "./gridart.ts"; | ||||||
| import {shadowcast} from "./shadowcast.ts"; | import { shadowcast } from "./shadowcast.ts"; | ||||||
| import {getCheckModal} from "./checkmodal.ts"; | import { getCheckModal } from "./checkmodal.ts"; | ||||||
|  |  | ||||||
|  |  | ||||||
| export class HuntMode { | export class HuntMode { | ||||||
|   map: LoadedNewMap |   map: LoadedNewMap; | ||||||
|   player: Point |   player: Point; | ||||||
|   faceLeft: boolean |   faceLeft: boolean; | ||||||
|  |  | ||||||
|   drawpile: DrawPile |   drawpile: DrawPile; | ||||||
|   frame: number |   frame: number; | ||||||
|   depth: number |   depth: number; | ||||||
|  |  | ||||||
|   constructor(depth: number, map: LoadedNewMap) { |   constructor(depth: number, map: LoadedNewMap) { | ||||||
|     this.map = map; |     this.map = map; | ||||||
|     this.player = map.entrance; |     this.player = map.entrance; | ||||||
|     this.faceLeft = false |     this.faceLeft = false; | ||||||
|  |  | ||||||
|     this.drawpile = new DrawPile(); |     this.drawpile = new DrawPile(); | ||||||
|     this.frame = 0; |     this.frame = 0; | ||||||
| @@ -46,7 +45,9 @@ export class HuntMode { | |||||||
|     let cell = this.map.get(this.player); |     let cell = this.map.get(this.player); | ||||||
|  |  | ||||||
|     let pickup = cell.pickup; |     let pickup = cell.pickup; | ||||||
|     if (pickup != null) { cell.pickup = null; } |     if (pickup != null) { | ||||||
|  |       cell.pickup = null; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #computeCostToClick(mapPosition: Point): number | null { |   #computeCostToClick(mapPosition: Point): number | null { | ||||||
| @@ -58,22 +59,30 @@ export class HuntMode { | |||||||
|  |  | ||||||
|     let dist = Math.max( |     let dist = Math.max( | ||||||
|       Math.abs(mapPosition.x - this.player.x), |       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; |     let pickup = present.pickup; | ||||||
|     if (pickup == null) { return 10; } |     if (pickup == null) { | ||||||
|     return pickup.computeCostToClick() |       return 10; | ||||||
|  |     } | ||||||
|  |     return pickup.computeCostToClick(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   movePlayerTo(newPosition: Point) { |   movePlayerTo(newPosition: Point) { | ||||||
|     let oldX = this.player.x; |     let oldX = this.player.x; | ||||||
|     let newX = newPosition.x; |     let newX = newPosition.x; | ||||||
|     this.player = newPosition; |     this.player = newPosition; | ||||||
|     if (newX < oldX) { this.faceLeft = true; } |     if (newX < oldX) { | ||||||
|     if (oldX < newX) { this.faceLeft = false; } |       this.faceLeft = true; | ||||||
|  |     } | ||||||
|  |     if (oldX < newX) { | ||||||
|  |       this.faceLeft = false; | ||||||
|  |     } | ||||||
|     this.#collectResources(); |     this.#collectResources(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -82,10 +91,10 @@ export class HuntMode { | |||||||
|     this.frame += 1; |     this.frame += 1; | ||||||
|     this.drawpile.clear(); |     this.drawpile.clear(); | ||||||
|  |  | ||||||
|     let globalOffset = |     let globalOffset = new Point( | ||||||
|       new Point(this.player.x * FLOOR_CELL_SIZE.w, this.player.y * FLOOR_CELL_SIZE.h).offset( |       this.player.x * FLOOR_CELL_SIZE.w, | ||||||
|         new Point(-192, -192) |       this.player.y * FLOOR_CELL_SIZE.h, | ||||||
|       ) |     ).offset(new Point(-192, -192)); | ||||||
|  |  | ||||||
|     this.#updateFov(); |     this.#updateFov(); | ||||||
|  |  | ||||||
| @@ -113,25 +122,27 @@ export class HuntMode { | |||||||
|       ([x, y]: [number, number]): boolean => { |       ([x, y]: [number, number]): boolean => { | ||||||
|         let cell = this.map.get(new Point(x, y)); |         let cell = this.map.get(new Point(x, y)); | ||||||
|         let pickup = cell.pickup; |         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]) => { |       ([x, y]: [number, number]) => { | ||||||
|         let dx = x - this.player.x; |         let dx = x - this.player.x; | ||||||
|         let dy = y - this.player.y; |         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; |         this.map.get(new Point(x, y)).revealed = true; | ||||||
|       } |       }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     this.drawpile.draw() |     this.drawpile.draw(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #drawMapCell( |   #drawMapCell(offsetInCells: Point, mapPosition: Point) { | ||||||
|     offsetInCells: Point, |  | ||||||
|     mapPosition: Point, |  | ||||||
|   ) { |  | ||||||
|     const OFFSET_UNDER_FLOOR = -512 + mapPosition.y; |     const OFFSET_UNDER_FLOOR = -512 + mapPosition.y; | ||||||
|     const OFFSET_FLOOR = -256 + mapPosition.y; |     const OFFSET_FLOOR = -256 + mapPosition.y; | ||||||
|     const OFFSET_AIR = 0 + mapPosition.y; |     const OFFSET_AIR = 0 + mapPosition.y; | ||||||
| @@ -140,21 +151,15 @@ export class HuntMode { | |||||||
|  |  | ||||||
|     const gridArt = new GridArt(offsetInCells); |     const gridArt = new GridArt(offsetInCells); | ||||||
|  |  | ||||||
|     let cellData = this.map.get(mapPosition) |     let cellData = this.map.get(mapPosition); | ||||||
|  |  | ||||||
|     this.drawpile.add( |     this.drawpile.add(OFFSET_UNDER_FLOOR, () => { | ||||||
|       OFFSET_UNDER_FLOOR, |       gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); | ||||||
|       () => { |     }); | ||||||
|         gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); |  | ||||||
|       } |  | ||||||
|     ); |  | ||||||
|     if (cellData.architecture == Architecture.Wall || !cellData.revealed) { |     if (cellData.architecture == Architecture.Wall || !cellData.revealed) { | ||||||
|       this.drawpile.add( |       this.drawpile.add(OFFSET_TOP, () => { | ||||||
|         OFFSET_TOP, |         gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); | ||||||
|         () => { |       }); | ||||||
|           gridArt.drawCeiling(BG_WALL_OR_UNREVEALED); |  | ||||||
|         } |  | ||||||
|       ); |  | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -169,7 +174,7 @@ export class HuntMode { | |||||||
|     this.drawpile.addClickable( |     this.drawpile.addClickable( | ||||||
|       OFFSET_FLOOR, |       OFFSET_FLOOR, | ||||||
|       (hover: boolean) => { |       (hover: boolean) => { | ||||||
|         gridArt.drawFloor(hover ? FG_TEXT : BG_INSET) |         gridArt.drawFloor(hover ? FG_TEXT : BG_INSET); | ||||||
|         pickup?.drawFloor(gridArt); |         pickup?.drawFloor(gridArt); | ||||||
|       }, |       }, | ||||||
|       gridArt.floorRect, |       gridArt.floorRect, | ||||||
| @@ -181,65 +186,86 @@ export class HuntMode { | |||||||
|  |  | ||||||
|         if (cost != null) { |         if (cost != null) { | ||||||
|           getPlayerProgress().spendBlood(cost); |           getPlayerProgress().spendBlood(cost); | ||||||
|           this.movePlayerTo(mapPosition) |           this.movePlayerTo(mapPosition); | ||||||
|           getCheckModal().show(null, null); |           getCheckModal().show(null, null); | ||||||
|         } |         } | ||||||
|       } |       }, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (pickup != 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) => { |     const isRevealedBlock = (dx: number, dy: number) => { | ||||||
|       let other = this.map.get(mapPosition.offset(new Point(dx, dy))); |       let other = this.map.get(mapPosition.offset(new Point(dx, dy))); | ||||||
|       return other.revealed && other.architecture == Architecture.Wall; |       return other.revealed && other.architecture == Architecture.Wall; | ||||||
|  |     }; | ||||||
|     } |  | ||||||
|     if (isRevealedBlock(0, -1) && isRevealedBlock(-1, 0)) { |     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)) { |     if (isRevealedBlock(0, -1)) { | ||||||
|       this.drawpile.add(OFFSET_AIR, () => { gridArt.drawWallTop(FG_TEXT); }) |       this.drawpile.add(OFFSET_AIR, () => { | ||||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingTop(FG_MOULDING); }) |         gridArt.drawWallTop(FG_TEXT); | ||||||
|  |       }); | ||||||
|  |       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||||
|  |         gridArt.drawMouldingTop(FG_MOULDING); | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|     if (isRevealedBlock(0, -1) && isRevealedBlock(1, 0)) { |     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)) { |     if (isRevealedBlock(-1, 0)) { | ||||||
|       this.drawpile.add(OFFSET_AIR, () => { gridArt.drawWallLeft(FG_TEXT); }) |       this.drawpile.add(OFFSET_AIR, () => { | ||||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingLeft(FG_MOULDING); }) |         gridArt.drawWallLeft(FG_TEXT); | ||||||
|  |       }); | ||||||
|  |       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||||
|  |         gridArt.drawMouldingLeft(FG_MOULDING); | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|     if (isRevealedBlock(0, 1) && isRevealedBlock(-1, 0)) { |     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)) { |     if (isRevealedBlock(0, 1)) { | ||||||
|       this.drawpile.add(OFFSET_AIR, () => { gridArt.drawWallBottom(FG_BOLD); }) |       this.drawpile.add(OFFSET_AIR, () => { | ||||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingBottom(FG_MOULDING); }) |         gridArt.drawWallBottom(FG_BOLD); | ||||||
|  |       }); | ||||||
|  |       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||||
|  |         gridArt.drawMouldingBottom(FG_MOULDING); | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|     if (isRevealedBlock(0, 1) && isRevealedBlock(1, 0)) { |     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)) { |     if (isRevealedBlock(1, 0)) { | ||||||
|       this.drawpile.add(OFFSET_AIR, () => { gridArt.drawWallRight(FG_BOLD); }) |       this.drawpile.add(OFFSET_AIR, () => { | ||||||
|       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { gridArt.drawMouldingRight(FG_MOULDING); }) |         gridArt.drawWallRight(FG_BOLD); | ||||||
|  |       }); | ||||||
|  |       this.drawpile.add(OFFSET_TOP_OF_TOP, () => { | ||||||
|  |         gridArt.drawMouldingRight(FG_MOULDING); | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #drawPlayer(globalOffset: Point) { |   #drawPlayer(globalOffset: Point) { | ||||||
|     let cellOffset = new Point( |     let cellOffset = new Point( | ||||||
|       this.player.x * FLOOR_CELL_SIZE.w, |       this.player.x * FLOOR_CELL_SIZE.w, | ||||||
|       this.player.y * FLOOR_CELL_SIZE.h |       this.player.y * FLOOR_CELL_SIZE.h, | ||||||
|     ).offset(globalOffset.negate()) |     ).offset(globalOffset.negate()); | ||||||
|     this.drawpile.add(this.player.y, () => { |     this.drawpile.add(this.player.y, () => { | ||||||
|       D.drawSprite( |       D.drawSprite(sprThrallLore, cellOffset, 1, { | ||||||
|         sprThrallLore, |         xScale: this.faceLeft ? -2 : 2, | ||||||
|         cellOffset, |         yScale: 2, | ||||||
|         1, { |       }); | ||||||
|           xScale: this.faceLeft ? -2 : 2, |  | ||||||
|           yScale: 2 |  | ||||||
|         } |  | ||||||
|       ) |  | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -251,7 +277,7 @@ export function initHuntMode(huntMode: HuntMode) { | |||||||
|  |  | ||||||
| export function getHuntMode() { | export function getHuntMode() { | ||||||
|   if (active == null) { |   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; |   return active; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,15 @@ | |||||||
| import {AlignX, AlignY, Point, Rect, Size} from "./engine/datatypes.ts"; | import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
| import {getHud} from "./hud.ts"; | import { getHud } from "./hud.ts"; | ||||||
| import {getHotbar} from "./hotbar.ts"; | import { getHotbar } from "./hotbar.ts"; | ||||||
|  |  | ||||||
| // general | // general | ||||||
| let margin = 8; | let margin = 8; | ||||||
| export function getLayoutRect( | export function getLayoutRect( | ||||||
|   size: Size, |   size: Size, | ||||||
|   options?: {alignX?: AlignX, alignY?: AlignY} |   options?: { alignX?: AlignX; alignY?: AlignY }, | ||||||
| ): Rect { | ): 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 |   // first of all: place the _internal_ screen inside the real screen | ||||||
|   let marginalScreenW = screenW - margin * 2; |   let marginalScreenW = screenW - margin * 2; | ||||||
| @@ -20,44 +20,53 @@ export function getLayoutRect( | |||||||
|   // NOTE: If the screen is too small, remainingSpace will be negative |   // NOTE: If the screen is too small, remainingSpace will be negative | ||||||
|   // This is fine -- it actually results in reasonable outcomes except |   // 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. |   // 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 remainingSpaceX = marginalScreenW - innerW; | ||||||
|   let remainingSpaceY = marginalScreenH - innerH; |   let remainingSpaceY = marginalScreenH - innerH; | ||||||
|  |  | ||||||
|   let alignXCoef = |   let alignXCoef = | ||||||
|     options?.alignX == AlignX.Left ? 0.0 : |     options?.alignX == AlignX.Left | ||||||
|     options?.alignX == AlignX.Center ? 0.5 : |       ? 0.0 | ||||||
|     options?.alignX == AlignX.Right ? 1.0 : |       : options?.alignX == AlignX.Center | ||||||
|     0.5; |         ? 0.5 | ||||||
|  |         : options?.alignX == AlignX.Right | ||||||
|  |           ? 1.0 | ||||||
|  |           : 0.5; | ||||||
|   let alignYCoef = |   let alignYCoef = | ||||||
|     options?.alignY == AlignY.Top ? 0.0 : |     options?.alignY == AlignY.Top | ||||||
|     options?.alignY == AlignY.Middle ? 0.5 : |       ? 0.0 | ||||||
|     options?.alignY == AlignY.Bottom ? 1.0 : |       : options?.alignY == AlignY.Middle | ||||||
|     0.5; |         ? 0.5 | ||||||
|  |         : options?.alignY == AlignY.Bottom | ||||||
|  |           ? 1.0 | ||||||
|  |           : 0.5; | ||||||
|  |  | ||||||
|   let x = marginalScreenX + alignXCoef * remainingSpaceX; |   let x = marginalScreenX + alignXCoef * remainingSpaceX; | ||||||
|   let y = marginalScreenY + alignYCoef * remainingSpaceY; |   let y = marginalScreenY + alignYCoef * remainingSpaceY; | ||||||
|  |  | ||||||
|   return new Rect( |   return new Rect(new Point(Math.floor(x), Math.floor(y)), size); | ||||||
|     new Point(Math.floor(x), Math.floor(y)), |  | ||||||
|     size |  | ||||||
|   ) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export function withCamera(part: UIPart, cb: () => void) { | export function withCamera(part: UIPart, cb: () => void) { | ||||||
|   let region = getPartLocation(part); |   let region = getPartLocation(part); | ||||||
|  |  | ||||||
|   D.withCamera(D.camera.offset(region.top.negate()), cb) |   D.withCamera(D.camera.offset(region.top.negate()), cb); | ||||||
| } | } | ||||||
|  |  | ||||||
| // specific | // specific | ||||||
| export type Page = "Gameplay" | "Thralls"; | 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 { | export function getPartPage(part: UIPart): Page | null { | ||||||
|   switch (part) { |   switch (part) { | ||||||
|     case "FullscreenPopover": |     case "FullscreenPopover": | ||||||
|       return null |       return null; | ||||||
|     case "BottomModal": |     case "BottomModal": | ||||||
|     case "Hotbar": |     case "Hotbar": | ||||||
|     case "HUD": |     case "HUD": | ||||||
| @@ -67,7 +76,7 @@ export function getPartPage(part: UIPart): Page | null { | |||||||
|       return "Thralls"; |       return "Thralls"; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   throw `invalid part: ${part}` |   throw `invalid part: ${part}`; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getPageLocation(page: Page): Point { | export function getPageLocation(page: Page): Point { | ||||||
| @@ -79,12 +88,12 @@ export function getPageLocation(page: Page): Point { | |||||||
|       return new Point(0, 1); |       return new Point(0, 1); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   throw `invalid page: ${page}` |   throw `invalid page: ${page}`; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getPartLocation(part: UIPart): Rect { | export function getPartLocation(part: UIPart): Rect { | ||||||
|   // TODO: in pixels, not screens |   // TODO: in pixels, not screens | ||||||
|   let {w: screenW, h: screenH} = D.size; |   let { w: screenW, h: screenH } = D.size; | ||||||
|   let page = getPartPage(part); |   let page = getPartPage(part); | ||||||
|   let pageOffset = page ? getPageLocation(page) : null; |   let pageOffset = page ? getPageLocation(page) : null; | ||||||
|   let layoutRect = internalGetPartLayoutRect(part); |   let layoutRect = internalGetPartLayoutRect(part); | ||||||
| @@ -94,11 +103,9 @@ export function getPartLocation(part: UIPart): Rect { | |||||||
|     return layoutRect.offset(D.camera); |     return layoutRect.offset(D.camera); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return layoutRect.offset(new Point( |   return layoutRect.offset( | ||||||
|     pageOffset.x * screenW, |     new Point(pageOffset.x * screenW, pageOffset.y * screenH), | ||||||
|     pageOffset.y * screenH |   ); | ||||||
|   )); |  | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export function internalGetPartLayoutRect(part: UIPart) { | export function internalGetPartLayoutRect(part: UIPart) { | ||||||
| @@ -117,12 +124,12 @@ export function internalGetPartLayoutRect(part: UIPart) { | |||||||
|       return getLayoutRect(getHotbar().size, { |       return getLayoutRect(getHotbar().size, { | ||||||
|         alignX: AlignX.Center, |         alignX: AlignX.Center, | ||||||
|         alignY: AlignY.Bottom, |         alignY: AlignY.Bottom, | ||||||
|       }) |       }); | ||||||
|     case "HUD": |     case "HUD": | ||||||
|       return getLayoutRect(getHud().size, { |       return getLayoutRect(getHud().size, { | ||||||
|         alignX: AlignX.Left, |         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 { hostGame } from "./engine/internal/host.ts"; | ||||||
| import {game} from "./game.ts"; | import { game } from "./game.ts"; | ||||||
| import {getStateManager} from "./statemanager.ts"; | import { getStateManager } from "./statemanager.ts"; | ||||||
|  |  | ||||||
| getStateManager().startGame({ | getStateManager().startGame( | ||||||
|   name: "Pyrex", |   { | ||||||
|   title: "", |     name: "Pyrex", | ||||||
|   note: null, |     title: "", | ||||||
|   stats: {AGI: 10, INT: 10, CHA: 10, PSI: 10}, |     note: null, | ||||||
|   talents: {AGI: 0, INT: 0, CHA: 0, PSI: 0}, |     stats: { AGI: 10, INT: 10, CHA: 10, PSI: 10 }, | ||||||
|   skills: [], |     talents: { AGI: 0, INT: 0, CHA: 0, PSI: 0 }, | ||||||
|   isCompulsory: false, |     skills: [], | ||||||
|   inPenance: false, |     isCompulsory: false, | ||||||
| }, null); |     inPenance: false, | ||||||
| hostGame(game); |   }, | ||||||
|  |   null, | ||||||
|  | ); | ||||||
|  | hostGame(game); | ||||||
|   | |||||||
| @@ -1,8 +1,12 @@ | |||||||
| import {Architecture, LoadedNewMap} from "./newmap.ts"; | import { Architecture, LoadedNewMap } from "./newmap.ts"; | ||||||
| import {Grid, Point} from "./engine/datatypes.ts"; | import { Grid, Point } from "./engine/datatypes.ts"; | ||||||
| import {getThralls} from "./thralls.ts"; | import { getThralls } from "./thralls.ts"; | ||||||
| import {LadderPickup, ThrallPosterPickup, ThrallRecruitedPickup} from "./pickups.ts"; | import { | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; |   LadderPickup, | ||||||
|  |   ThrallPosterPickup, | ||||||
|  |   ThrallRecruitedPickup, | ||||||
|  | } from "./pickups.ts"; | ||||||
|  | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
|  |  | ||||||
| const BASIC_PLAN = Grid.createGridFromMultilineString(` | const BASIC_PLAN = Grid.createGridFromMultilineString(` | ||||||
| ##################### | ##################### | ||||||
| @@ -43,25 +47,58 @@ export function generateManor(): LoadedNewMap { | |||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       switch (BASIC_PLAN.get(xy)) { |       switch (BASIC_PLAN.get(xy)) { | ||||||
|         case '#': break |         case "#": | ||||||
|         case '@': cell.architecture = Architecture.Floor; map.entrance = xy; break; |           break; | ||||||
|         case 'L': cell.architecture = Architecture.Floor; cell.pickup = new LadderPickup(); break; |         case "@": | ||||||
|         case ' ': cell.architecture = Architecture.Floor; break; |           cell.architecture = Architecture.Floor; | ||||||
|         case 'a': placeThrall(0); break; |           map.entrance = xy; | ||||||
|         case 'b': placeThrall(1); break; |           break; | ||||||
|         case 'c': placeThrall(2); break; |         case "L": | ||||||
|         case 'd': placeThrall(3); break; |           cell.architecture = Architecture.Floor; | ||||||
|         case 'e': placeThrall(4); break; |           cell.pickup = new LadderPickup(); | ||||||
|         case 'f': placeThrall(5); break; |           break; | ||||||
|         case 'A': placeThrallPoster(0); break; |         case " ": | ||||||
|         case 'B': placeThrallPoster(1); break; |           cell.architecture = Architecture.Floor; | ||||||
|         case 'C': placeThrallPoster(2); break; |           break; | ||||||
|         case 'D': placeThrallPoster(3); break; |         case "a": | ||||||
|         case 'E': placeThrallPoster(4); break; |           placeThrall(0); | ||||||
|         case 'F': placeThrallPoster(5); break; |           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; |   return map; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										191
									
								
								src/mapgen.ts
									
									
									
									
									
								
							
							
						
						
									
										191
									
								
								src/mapgen.ts
									
									
									
									
									
								
							| @@ -1,10 +1,16 @@ | |||||||
| import {Architecture, LoadedNewMap} from "./newmap.ts"; | import { Architecture, LoadedNewMap } from "./newmap.ts"; | ||||||
| import {Grid, Point, Rect, Size} from "./engine/datatypes.ts"; | import { Grid, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {choose, shuffle} from "./utils.ts"; | import { choose, shuffle } from "./utils.ts"; | ||||||
| import {standardVaultTemplates, VaultTemplate} from "./vaulttemplate.ts"; | import { standardVaultTemplates, VaultTemplate } from "./vaulttemplate.ts"; | ||||||
| import {ALL_STATS} from "./datatypes.ts"; | import { ALL_STATS } from "./datatypes.ts"; | ||||||
| import {ExperiencePickup, LadderPickup, LockPickup, StatPickup, ThrallPickup} from "./pickups.ts"; | import { | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; |   ExperiencePickup, | ||||||
|  |   LadderPickup, | ||||||
|  |   LockPickup, | ||||||
|  |   StatPickup, | ||||||
|  |   ThrallPickup, | ||||||
|  | } from "./pickups.ts"; | ||||||
|  | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
|  |  | ||||||
| const WIDTH = 19; | const WIDTH = 19; | ||||||
| const HEIGHT = 19; | const HEIGHT = 19; | ||||||
| @@ -14,7 +20,7 @@ const MAX_VAULTS = 1; | |||||||
| const NUM_VAULT_TRIES = 90; | const NUM_VAULT_TRIES = 90; | ||||||
| const NUM_ROOM_TRIES = 90; | const NUM_ROOM_TRIES = 90; | ||||||
| const NUM_STAIRCASE_TRIES = 90; | const NUM_STAIRCASE_TRIES = 90; | ||||||
| const NUM_STAIRCASES_DESIRED = 3 | const NUM_STAIRCASES_DESIRED = 3; | ||||||
| const NUM_ROOMS_DESIRED = 0; // 4; | const NUM_ROOMS_DESIRED = 0; // 4; | ||||||
|  |  | ||||||
| const EXTRA_CONNECTOR_CHANCE = 0.15; | const EXTRA_CONNECTOR_CHANCE = 0.15; | ||||||
| @@ -23,15 +29,19 @@ const WINDING_PERCENT = 0; | |||||||
| // This is an implementation of Nystrom's algorithm: | // This is an implementation of Nystrom's algorithm: | ||||||
| // https://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/ | // https://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/ | ||||||
| class Knife { | class Knife { | ||||||
|   #map: LoadedNewMap |   #map: LoadedNewMap; | ||||||
|   #region: number |   #region: number; | ||||||
|   #regions: Grid<number | null> |   #regions: Grid<number | null>; | ||||||
|   #sealedWalls: Grid<boolean> |   #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.#map = map; | ||||||
|     this.#region = -1; |     this.#region = -1; | ||||||
|     this.#regions = regions |     this.#regions = regions; | ||||||
|     this.#sealedWalls = sealedWalls; |     this.#sealedWalls = sealedWalls; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -51,10 +61,12 @@ class Knife { | |||||||
|     return this.#sealedWalls; |     return this.#sealedWalls; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   startRegion() { this.#region += 1; } |   startRegion() { | ||||||
|  |     this.#region += 1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   carve(point: Point) { |   carve(point: Point) { | ||||||
|     this.#regions.set(point, this.#region) |     this.#regions.set(point, this.#region); | ||||||
|     this.map.get(point).architecture = Architecture.Floor; |     this.map.get(point).architecture = Architecture.Floor; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -68,7 +80,7 @@ class Knife { | |||||||
|     if (protect ?? false) { |     if (protect ?? false) { | ||||||
|       for (let y = room.top.y - 1; y < room.top.y + room.size.h + 1; y++) { |       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++) { |         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 { | export function generateMap(): LoadedNewMap { | ||||||
|   for (let i= 0; i < 1000; i++) { |   for (let i = 0; i < 1000; i++) { | ||||||
|     try { |     try { | ||||||
|       return tryGenerateMap(standardVaultTemplates) |       return tryGenerateMap(standardVaultTemplates); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       if (e instanceof TryAgainException) { |       if (e instanceof TryAgainException) { | ||||||
|         continue; |         continue; | ||||||
| @@ -86,12 +98,14 @@ export function generateMap(): LoadedNewMap { | |||||||
|       throw e; |       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 { | export function tryGenerateMap(vaultTemplates: VaultTemplate[]): LoadedNewMap { | ||||||
|   let width = WIDTH; |   let width = WIDTH; | ||||||
|   let height = HEIGHT; |   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)); |   let grid = new LoadedNewMap("generated", new Size(width, height)); | ||||||
|  |  | ||||||
| @@ -125,14 +139,13 @@ export function tryGenerateMap(vaultTemplates: VaultTemplate[]): LoadedNewMap { | |||||||
|   return grid; |   return grid; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| class RoomChain { | class RoomChain { | ||||||
|   #size: Size; |   #size: Size; | ||||||
|   rooms: Rect[]; |   rooms: Rect[]; | ||||||
|  |  | ||||||
|   constructor(size: Size) { |   constructor(size: Size) { | ||||||
|     this.#size = size; |     this.#size = size; | ||||||
|     this.rooms = [] |     this.rooms = []; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   reserve(width: number, height: number): Rect | null { |   reserve(width: number, height: number): Rect | null { | ||||||
| @@ -148,24 +161,32 @@ class RoomChain { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.rooms.push(room); |     this.rooms.push(room); | ||||||
|     return room |     return room; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | ||||||
|   vaultTemplates = [...vaultTemplates];  // so we can mutate it |   vaultTemplates = [...vaultTemplates]; // so we can mutate it | ||||||
|   shuffle(vaultTemplates); |   shuffle(vaultTemplates); | ||||||
|   let chain = new RoomChain(knife.map.size); |   let chain = new RoomChain(knife.map.size); | ||||||
|   let nVaults = 0; |   let nVaults = 0; | ||||||
|   let nVaultsDesired = randrange(MIN_VAULTS, MAX_VAULTS + 1); |   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 width = 7; | ||||||
|     let height = 7; |     let height = 7; | ||||||
|  |  | ||||||
|     let room = chain.reserve(width, height); |     let room = chain.reserve(width, height); | ||||||
|  |  | ||||||
|     if (!room) { continue; } |     if (!room) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     nVaults += 1; |     nVaults += 1; | ||||||
|     carveVault(knife, room, vaultTemplates.pop()!); |     carveVault(knife, room, vaultTemplates.pop()!); | ||||||
| @@ -174,12 +195,18 @@ function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | |||||||
|   // staircases |   // staircases | ||||||
|   let nStaircases = 0; |   let nStaircases = 0; | ||||||
|   let nStaircasesDesired = NUM_STAIRCASES_DESIRED; |   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 width = 3; | ||||||
|     let height = 3; |     let height = 3; | ||||||
|  |  | ||||||
|     let room = chain.reserve(width, height); |     let room = chain.reserve(width, height); | ||||||
|     if (!room) { continue; } |     if (!room) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|     nStaircases += 1; |     nStaircases += 1; | ||||||
|     carveStaircase(knife, room, nStaircases - 1); |     carveStaircase(knife, room, nStaircases - 1); | ||||||
|   } |   } | ||||||
| @@ -192,11 +219,16 @@ function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | |||||||
|   let nRooms = 0; |   let nRooms = 0; | ||||||
|   let nRoomsDesired = NUM_ROOMS_DESIRED; |   let nRoomsDesired = NUM_ROOMS_DESIRED; | ||||||
|   for (let i = 0; nRooms < nRoomsDesired && i < NUM_ROOM_TRIES; i += 1) { |   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); |     let room = chain.reserve(width, height); | ||||||
|  |  | ||||||
|     if (!room) { continue; } |     if (!room) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|     nRooms += 1; |     nRooms += 1; | ||||||
|  |  | ||||||
|     carveRoom(knife, room); |     carveRoom(knife, room); | ||||||
| @@ -206,13 +238,13 @@ function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] { | |||||||
|  |  | ||||||
| function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | ||||||
|   if (room.size.w != 7 || room.size.h != 7) { |   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 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 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 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 quad3 = new Rect(room.top.offset(new Point(0, 4)), new Size(3, 3)); | ||||||
|  |  | ||||||
|   let [a, b, c, d] = choose([ |   let [a, b, c, d] = choose([ | ||||||
|     [quad0, quad1, quad2, quad3], |     [quad0, quad1, quad2, quad3], | ||||||
| @@ -267,7 +299,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | |||||||
|     new Point(3, 1), |     new Point(3, 1), | ||||||
|     new Point(5, 3), |     new Point(5, 3), | ||||||
|     new Point(3, 5), |     new Point(3, 5), | ||||||
|     new Point(1, 3) |     new Point(1, 3), | ||||||
|   ]; |   ]; | ||||||
|   for (let offset of connectors.values()) { |   for (let offset of connectors.values()) { | ||||||
|     let connector = room.top.offset(offset); |     let connector = room.top.offset(offset); | ||||||
| @@ -278,7 +310,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | |||||||
|       if (check != null) { |       if (check != null) { | ||||||
|         knife.map.get(connector).pickup = new LockPickup(check); |         knife.map.get(connector).pickup = new LockPickup(check); | ||||||
|       } |       } | ||||||
|       knife.carve(connector) |       knife.carve(connector); | ||||||
|     } |     } | ||||||
|     if (mergeRects(c, d).contains(connector)) { |     if (mergeRects(c, d).contains(connector)) { | ||||||
|       // TODO: Put check 2 here |       // TODO: Put check 2 here | ||||||
| @@ -286,7 +318,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) { | |||||||
|       if (check != null) { |       if (check != null) { | ||||||
|         knife.map.get(connector).pickup = new LockPickup(check); |         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(5, 1), | ||||||
|     new Point(1, 5), |     new Point(1, 5), | ||||||
|     new Point(5, 5), |     new Point(5, 5), | ||||||
|   ] |   ]; | ||||||
|   for (let offset of goodies.values()) { |   for (let offset of goodies.values()) { | ||||||
|     let goodie = room.top.offset(offset); |     let goodie = room.top.offset(offset); | ||||||
|     let cell = knife.map.get(goodie); |     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 xy0 = room.top.offset(new Point(dx, dy)); | ||||||
|       let xy1 = room.top.offset(new Point(room.size.w - dx - 1, 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 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); |       let stat = choose(ALL_STATS); | ||||||
|       knife.map.get(xy0).pickup = new StatPickup(stat); |       knife.map.get(xy0).pickup = new StatPickup(stat); | ||||||
|       knife.map.get(xy1).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 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); |   let aby1 = Math.max(a.top.y + a.size.h, b.top.y + b.size.h); | ||||||
|  |  | ||||||
|   return new Rect( |   return new Rect(new Point(abx0, aby0), new Size(abx1 - abx0, aby1 - aby0)); | ||||||
|     new Point(abx0, aby0), | }; | ||||||
|     new Size(abx1 - abx0, aby1 - aby0) |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const _CARDINAL_DIRECTIONS = [ | const _CARDINAL_DIRECTIONS = [ | ||||||
|   new Point(-1, 0), |   new Point(-1, 0), | ||||||
|   new Point(0, -1), |   new Point(0, -1), | ||||||
|   new Point(1, 0), |   new Point(1, 0), | ||||||
|   new Point(0, 1), |   new Point(0, 1), | ||||||
| ] | ]; | ||||||
|  |  | ||||||
| function connectRegions(knife: Knife) { | function connectRegions(knife: Knife) { | ||||||
|   // this procedure is really complicated |   // this procedure is really complicated | ||||||
| @@ -405,7 +436,9 @@ function connectRegions(knife: Knife) { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       regions = dedup(regions); |       regions = dedup(regions); | ||||||
|       if (regions.length < 2) { continue; } |       if (regions.length < 2) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       connectorRegions.set(pos, regions); |       connectorRegions.set(pos, regions); | ||||||
|       connectors.push(pos); |       connectors.push(pos); | ||||||
| @@ -413,7 +446,7 @@ function connectRegions(knife: Knife) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // map from original index to "region it has been merged to" index |   // map from original index to "region it has been merged to" index | ||||||
|   let merged: Record<number, number> = {} |   let merged: Record<number, number> = {}; | ||||||
|   let openRegions = []; |   let openRegions = []; | ||||||
|   for (let i = 0; i <= knife.region; i++) { |   for (let i = 0; i <= knife.region; i++) { | ||||||
|     merged[i] = i; |     merged[i] = i; | ||||||
| @@ -424,12 +457,16 @@ function connectRegions(knife: Knife) { | |||||||
|  |  | ||||||
|   while (openRegions.length > 1) { |   while (openRegions.length > 1) { | ||||||
|     if (iter > 100) { |     if (iter > 100) { | ||||||
|       throw new TryAgainException("algorithm was not quiescent for some reason"); |       throw new TryAgainException( | ||||||
|  |         "algorithm was not quiescent for some reason", | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|     iter++; |     iter++; | ||||||
|     showDebug(knife.map); |     showDebug(knife.map); | ||||||
|     if (connectors.length == 0) { |     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); |     let connector = choose(connectors); | ||||||
|  |  | ||||||
| @@ -439,7 +476,7 @@ function connectRegions(knife: Knife) { | |||||||
|     let sources: number[] = dedup(basicRegions.map((i) => merged[i])); |     let sources: number[] = dedup(basicRegions.map((i) => merged[i])); | ||||||
|     let dest: number | undefined = sources.pop(); |     let dest: number | undefined = sources.pop(); | ||||||
|     if (dest == undefined) { |     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) { |     if (Math.random() > EXTRA_CONNECTOR_CHANCE) { | ||||||
| @@ -452,18 +489,22 @@ function connectRegions(knife: Knife) { | |||||||
|  |  | ||||||
|       for (let src of sources.values()) { |       for (let src of sources.values()) { | ||||||
|         let ix = openRegions.indexOf(src); |         let ix = openRegions.indexOf(src); | ||||||
|         if (ix != -1) { openRegions.splice(ix, 1); } |         if (ix != -1) { | ||||||
|  |           openRegions.splice(ix, 1); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let connectors2 = []; |     let connectors2 = []; | ||||||
|     for (let other of connectors.values()) { |     for (let other of connectors.values()) { | ||||||
|       if (other.manhattan(connector) == 1) { continue; } |       if (other.manhattan(connector) == 1) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       let connected = dedup( |       let connected = dedup(connectorRegions.get(other).map((m) => merged[m])); | ||||||
|         connectorRegions.get(other).map((m) => merged[m]) |       if (connected.length <= 1) { | ||||||
|       ); |         continue; | ||||||
|       if (connected.length <= 1) { continue; } |       } | ||||||
|  |  | ||||||
|       connectors2.push(other); |       connectors2.push(other); | ||||||
|     } |     } | ||||||
| @@ -496,7 +537,7 @@ function growMaze(knife: Knife, start: Point) { | |||||||
|     if (unmadeCells.length == 0) { |     if (unmadeCells.length == 0) { | ||||||
|       cells.pop(); |       cells.pop(); | ||||||
|       lastDir = null; |       lastDir = null; | ||||||
|       continue |       continue; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let dir: Point; |     let dir: Point; | ||||||
| @@ -510,7 +551,7 @@ function growMaze(knife: Knife, start: Point) { | |||||||
|     let c2 = cell.offset(dir).offset(dir); |     let c2 = cell.offset(dir).offset(dir); | ||||||
|     knife.carve(c1); |     knife.carve(c1); | ||||||
|     knife.carve(c2); |     knife.carve(c2); | ||||||
|     cells.push(c2) |     cells.push(c2); | ||||||
|     lastDir = dir; |     lastDir = dir; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -526,7 +567,6 @@ function canCarve(knife: Knife, pos: Point, direction: Point) { | |||||||
|   return knife.map.get(c2).architecture == Architecture.Wall; |   return knife.map.get(c2).architecture == Architecture.Wall; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| function removeDeadEnds(knife: Knife) { | function removeDeadEnds(knife: Knife) { | ||||||
|   let done = false; |   let done = false; | ||||||
|  |  | ||||||
| @@ -536,7 +576,9 @@ function removeDeadEnds(knife: Knife) { | |||||||
|     for (let y = 1; y < knife.map.size.h - 1; y++) { |     for (let y = 1; y < knife.map.size.h - 1; y++) { | ||||||
|       for (let x = 1; x < knife.map.size.w - 1; x++) { |       for (let x = 1; x < knife.map.size.w - 1; x++) { | ||||||
|         let xy = new Point(x, y); |         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; |         let exits = 0; | ||||||
|         for (let dir of _CARDINAL_DIRECTIONS.values()) { |         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; |         done = false; | ||||||
|         knife.map.get(xy).architecture = Architecture.Wall; |         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) { | function randrange(lo: number, hi: number) { | ||||||
|   if (lo >= hi) { |   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[] { | function dedup(items: number[]): number[] { | ||||||
|   let deduped = []; |   let deduped = []; | ||||||
|   for (let i of items.values()) { |   for (let i of items.values()) { | ||||||
|     if (deduped.indexOf(i) != -1) { continue; } |     if (deduped.indexOf(i) != -1) { | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|     deduped.push(i); |     deduped.push(i); | ||||||
|   } |   } | ||||||
|   return deduped; |   return deduped; | ||||||
| @@ -580,7 +624,10 @@ function showDebug(grid: LoadedNewMap) { | |||||||
|     let out = ""; |     let out = ""; | ||||||
|     for (let y = 0; y < grid.size.h; y++) { |     for (let y = 0; y < grid.size.h; y++) { | ||||||
|       for (let x = 0; x < grid.size.w; x++) { |       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"; |       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 = [ | const names = [ | ||||||
|   // vampires |   // vampires | ||||||
|   "Vlad", "Drek", |   "Vlad", | ||||||
|  |   "Drek", | ||||||
|   // generic American names I like |   // generic American names I like | ||||||
|   "Kyle", |   "Kyle", | ||||||
|   // friends I can defame |   // friends I can defame | ||||||
|   "Bhijn", "Myr", "Narry", |   "Bhijn", | ||||||
|  |   "Myr", | ||||||
|  |   "Narry", | ||||||
|   // aggressively furry names |   // aggressively furry names | ||||||
|   "Tech", |   "Tech", | ||||||
|   // deities |   // deities | ||||||
|   "Quetzal", "Zotz", |   "Quetzal", | ||||||
|  |   "Zotz", | ||||||
|   // Nameberry's unique names |   // Nameberry's unique names | ||||||
|   "Teleri", "Artis", "Lautaro", "Corbett", "Kestrel", |   "Teleri", | ||||||
|   "Averil", "Sparrow", "Quillan", "Pipit", "Capella", |   "Artis", | ||||||
|   "Altair", "Lowell", "Leonie", "Vega", "Kea", |   "Lautaro", | ||||||
|   "Shai", "Teddy", "Howard", "Khalid", "Ozias", |   "Corbett", | ||||||
|   "Zuko", "Ezio", "Zeno", "Thisby", "Calloway", |   "Kestrel", | ||||||
|   "Fenna", "Lupin", "Finlo", "Tycho", "Talmadge", |   "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 |   // others | ||||||
|   "Jeff", "Jon", "Garrett", "Russell", "Tyson", |   "Jeff", | ||||||
|   "Gervase", "Sonja", "Sue", "Richard", "Jankie", |   "Jon", | ||||||
|  |   "Garrett", | ||||||
|  |   "Russell", | ||||||
|  |   "Tyson", | ||||||
|  |   "Gervase", | ||||||
|  |   "Sonja", | ||||||
|  |   "Sue", | ||||||
|  |   "Richard", | ||||||
|  |   "Jankie", | ||||||
|   // highly trustworthy individuals |   // highly trustworthy individuals | ||||||
|   "Nef", "Matt", "Sam" |   "Nef", | ||||||
| ] |   "Matt", | ||||||
|  |   "Sam", | ||||||
|  | ]; | ||||||
| export function generateName() { | export function generateName() { | ||||||
|   return choose(names); |   return choose(names); | ||||||
| } | } | ||||||
| @@ -42,9 +80,9 @@ const titles = [ | |||||||
|   "Poker Player", |   "Poker Player", | ||||||
|   "Priest", |   "Priest", | ||||||
|   "Magician", |   "Magician", | ||||||
|   "Writer" |   "Writer", | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| export function generateTitle() { | export function generateTitle() { | ||||||
|   return choose(titles); |   return choose(titles); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,36 +1,39 @@ | |||||||
| import {Grid, Point, Size} from "./engine/datatypes.ts"; | import { Grid, Point, Size } from "./engine/datatypes.ts"; | ||||||
| import {Pickup} from "./pickups.ts"; | import { Pickup } from "./pickups.ts"; | ||||||
| import {Skill} from "./datatypes.ts"; | import { Skill } from "./datatypes.ts"; | ||||||
|  |  | ||||||
| export enum Architecture { Wall, Floor } | export enum Architecture { | ||||||
|  |   Wall, | ||||||
|  |   Floor, | ||||||
|  | } | ||||||
|  |  | ||||||
| export type CheckData = { | export type CheckData = { | ||||||
|   label: string, |   label: string; | ||||||
|   options: (CheckDataOption | ChoiceOption)[], |   options: (CheckDataOption | ChoiceOption)[]; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export type ChoiceOption = { | export type ChoiceOption = { | ||||||
|   isChoice: true, |   isChoice: true; | ||||||
|   countsAsSuccess: boolean, |   countsAsSuccess: boolean; | ||||||
|   unlockable: string, |   unlockable: string; | ||||||
|   success: string, |   success: string; | ||||||
| } | }; | ||||||
| export type CheckDataOption = { | export type CheckDataOption = { | ||||||
|   skill: () => Skill, |   skill: () => Skill; | ||||||
|   locked: string, |   locked: string; | ||||||
|   failure: string, |   failure: string; | ||||||
|   unlockable: string, |   unlockable: string; | ||||||
|   success: string, |   success: string; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export class LoadedNewMap { | export class LoadedNewMap { | ||||||
|   #id: string |   #id: string; | ||||||
|   #size: Size |   #size: Size; | ||||||
|   #entrance: Point | null |   #entrance: Point | null; | ||||||
|   #architecture: Grid<Architecture> |   #architecture: Grid<Architecture>; | ||||||
|   #pickups: Grid<Pickup | null> |   #pickups: Grid<Pickup | null>; | ||||||
|   #provinces: Grid<string | null> |   #provinces: Grid<string | null>; | ||||||
|   #revealed: Grid<boolean> |   #revealed: Grid<boolean>; | ||||||
|  |  | ||||||
|   constructor(id: string, size: Size) { |   constructor(id: string, size: Size) { | ||||||
|     this.#id = id; |     this.#id = id; | ||||||
| @@ -48,7 +51,7 @@ export class LoadedNewMap { | |||||||
|  |  | ||||||
|   get entrance(): Point { |   get entrance(): Point { | ||||||
|     if (this.#entrance == null) { |     if (this.#entrance == null) { | ||||||
|       throw `${this.#id}: this.#entrance was never initialized` |       throw `${this.#id}: this.#entrance was never initialized`; | ||||||
|     } |     } | ||||||
|     return this.#entrance; |     return this.#entrance; | ||||||
|   } |   } | ||||||
| @@ -58,7 +61,7 @@ export class LoadedNewMap { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   get(point: Point): CellView { |   get(point: Point): CellView { | ||||||
|     return new CellView(this, point) |     return new CellView(this, point); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   setArchitecture(point: Point, value: Architecture) { |   setArchitecture(point: Point, value: Architecture) { | ||||||
| @@ -86,7 +89,7 @@ export class LoadedNewMap { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   setRevealed(point: Point, value: boolean) { |   setRevealed(point: Point, value: boolean) { | ||||||
|     this.#revealed.set(point, value) |     this.#revealed.set(point, value); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getRevealed(point: Point): boolean { |   getRevealed(point: Point): boolean { | ||||||
| @@ -95,25 +98,41 @@ export class LoadedNewMap { | |||||||
| } | } | ||||||
|  |  | ||||||
| export class CellView { | export class CellView { | ||||||
|   #map: LoadedNewMap |   #map: LoadedNewMap; | ||||||
|   #point: Point |   #point: Point; | ||||||
|  |  | ||||||
|   constructor(map: LoadedNewMap, point: Point) { |   constructor(map: LoadedNewMap, point: Point) { | ||||||
|     this.#map = map; |     this.#map = map; | ||||||
|     this.#point = point; |     this.#point = point; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   set architecture(value: Architecture) { this.#map.setArchitecture(this.#point, value) } |   set architecture(value: Architecture) { | ||||||
|   get architecture(): Architecture { return this.#map.getArchitecture(this.#point) } |     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) } |   set pickup(value: Pickup | null) { | ||||||
|   get pickup(): Pickup | null { return this.#map.getPickup(this.#point) } |     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) } |   set province(value: string | null) { | ||||||
|   get province(): string | null { return this.#map.getProvince(this.#point) } |     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) } |   set revealed(value: boolean) { | ||||||
|   get revealed(): boolean { return this.#map.getRevealed(this.#point) } |     this.#map.setRevealed(this.#point, value); | ||||||
|  |   } | ||||||
|  |   get revealed(): boolean { | ||||||
|  |     return this.#map.getRevealed(this.#point); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   copyFrom(cell: CellView) { |   copyFrom(cell: CellView) { | ||||||
|     this.architecture = cell.architecture; |     this.architecture = cell.architecture; | ||||||
| @@ -121,4 +140,4 @@ export class CellView { | |||||||
|     this.province = cell.province; |     this.province = cell.province; | ||||||
|     this.revealed = cell.revealed; |     this.revealed = cell.revealed; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										221
									
								
								src/pickups.ts
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								src/pickups.ts
									
									
									
									
									
								
							| @@ -1,24 +1,29 @@ | |||||||
| import {getThralls, LifeStage, Thrall} from "./thralls.ts"; | import { getThralls, LifeStage, Thrall } from "./thralls.ts"; | ||||||
| import {CellView, CheckData} from "./newmap.ts"; | import { CellView, CheckData } from "./newmap.ts"; | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
| import {getHuntMode, HuntMode, initHuntMode} from "./huntmode.ts"; | import { getHuntMode, HuntMode, initHuntMode } from "./huntmode.ts"; | ||||||
| import {generateMap} from "./mapgen.ts"; | import { generateMap } from "./mapgen.ts"; | ||||||
| import {ALL_STATS, Stat} from "./datatypes.ts"; | import { ALL_STATS, Stat } from "./datatypes.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
| import {sprLadder, sprLock, sprResourcePickup, sprStatPickup} from "./sprites.ts"; | import { | ||||||
| import {GridArt} from "./gridart.ts"; |   sprLadder, | ||||||
| import {getCheckModal} from "./checkmodal.ts"; |   sprLock, | ||||||
| import {Point} from "./engine/datatypes.ts"; |   sprResourcePickup, | ||||||
| import {choose} from "./utils.ts"; |   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 | export type Pickup = | ||||||
|   = LockPickup |   | LockPickup | ||||||
|   | StatPickup |   | StatPickup | ||||||
|   | ExperiencePickup |   | ExperiencePickup | ||||||
|   | LadderPickup |   | LadderPickup | ||||||
|   | ThrallPickup |   | ThrallPickup | ||||||
|   | ThrallPosterPickup |   | ThrallPosterPickup | ||||||
|   | ThrallRecruitedPickup |   | ThrallRecruitedPickup; | ||||||
|  |  | ||||||
| export class LockPickup { | export class LockPickup { | ||||||
|   check: CheckData; |   check: CheckData; | ||||||
| @@ -27,22 +32,26 @@ export class LockPickup { | |||||||
|     this.check = check; |     this.check = check; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   computeCostToClick() { return 0; } |   computeCostToClick() { | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   isObstructive() { return true; } |   isObstructive() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   drawFloor() { } |   drawFloor() {} | ||||||
|   drawInAir(gridArt: GridArt) { |   drawInAir(gridArt: GridArt) { | ||||||
|     for (let z = 0; z < 5; z += 0.25) { |     for (let z = 0; z < 5; z += 0.25) { | ||||||
|       D.drawSprite(sprLock, gridArt.project(z), 0, { |       D.drawSprite(sprLock, gridArt.project(z), 0, { | ||||||
|         xScale: 2.0, |         xScale: 2.0, | ||||||
|         yScale: 2.0, |         yScale: 2.0, | ||||||
|       }) |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onClick(cell: CellView): boolean { |   onClick(cell: CellView): boolean { | ||||||
|     getCheckModal().show(this.check, () => cell.pickup = null); |     getCheckModal().show(this.check, () => (cell.pickup = null)); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -54,24 +63,25 @@ export class StatPickup { | |||||||
|     this.stat = stat; |     this.stat = stat; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   computeCostToClick() { return 100; } |   computeCostToClick() { | ||||||
|  |     return 100; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   isObstructive() { return true; } |   isObstructive() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   drawFloor() { } |   drawFloor() {} | ||||||
|   drawInAir(gridArt: GridArt) { |   drawInAir(gridArt: GridArt) { | ||||||
|     let statIndex = ALL_STATS.indexOf(this.stat); |     let statIndex = ALL_STATS.indexOf(this.stat); | ||||||
|     if (statIndex == -1) { return; } |     if (statIndex == -1) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     D.drawSprite( |     D.drawSprite(sprStatPickup, gridArt.project(5), statIndex, { | ||||||
|       sprStatPickup, |       xScale: 2, | ||||||
|       gridArt.project(5), |       yScale: 2, | ||||||
|       statIndex, |     }); | ||||||
|       { |  | ||||||
|         xScale: 2, |  | ||||||
|         yScale: 2, |  | ||||||
|       } |  | ||||||
|     ) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onClick(): boolean { |   onClick(): boolean { | ||||||
| @@ -82,11 +92,15 @@ export class StatPickup { | |||||||
| } | } | ||||||
|  |  | ||||||
| export class ExperiencePickup { | export class ExperiencePickup { | ||||||
|   computeCostToClick() { return 100; } |   computeCostToClick() { | ||||||
|  |     return 100; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   isObstructive() { return true; } |   isObstructive() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   drawFloor() { } |   drawFloor() {} | ||||||
|   drawInAir(gridArt: GridArt) { |   drawInAir(gridArt: GridArt) { | ||||||
|     D.drawSprite( |     D.drawSprite( | ||||||
|       sprResourcePickup, |       sprResourcePickup, | ||||||
| @@ -95,7 +109,7 @@ export class ExperiencePickup { | |||||||
|       { |       { | ||||||
|         xScale: 2, |         xScale: 2, | ||||||
|         yScale: 2, |         yScale: 2, | ||||||
|       } |       }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -107,17 +121,21 @@ export class ExperiencePickup { | |||||||
| } | } | ||||||
|  |  | ||||||
| export class LadderPickup { | export class LadderPickup { | ||||||
|   computeCostToClick() { return 0; } |   computeCostToClick() { | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   isObstructive() { return false; } |   isObstructive() { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   drawFloor(gridArt: GridArt) { |   drawFloor(gridArt: GridArt) { | ||||||
|     D.drawSprite(sprLadder, gridArt.project(0.0), 0, { |     D.drawSprite(sprLadder, gridArt.project(0.0), 0, { | ||||||
|       xScale: 2.0, |       xScale: 2.0, | ||||||
|       yScale: 2.0, |       yScale: 2.0, | ||||||
|     }) |     }); | ||||||
|   } |   } | ||||||
|   drawInAir() { } |   drawInAir() {} | ||||||
|  |  | ||||||
|   onClick(): boolean { |   onClick(): boolean { | ||||||
|     getPlayerProgress().addBlood(1000); |     getPlayerProgress().addBlood(1000); | ||||||
| @@ -133,24 +151,28 @@ export class ThrallPickup { | |||||||
|     this.thrall = thrall; |     this.thrall = thrall; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   computeCostToClick() { return 0; } |   computeCostToClick() { | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   isObstructive() { return false; } |   isObstructive() { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   drawFloor() { } |   drawFloor() {} | ||||||
|   drawInAir(gridArt: GridArt) { |   drawInAir(gridArt: GridArt) { | ||||||
|     let data = getThralls().get(this.thrall); |     let data = getThralls().get(this.thrall); | ||||||
|     D.drawSprite(data.sprite, gridArt.project(0.0), 0, { |     D.drawSprite(data.sprite, gridArt.project(0.0), 0, { | ||||||
|       xScale: 2.0, |       xScale: 2.0, | ||||||
|       yScale: 2.0, |       yScale: 2.0, | ||||||
|     }) |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onClick(cell: CellView): boolean { |   onClick(cell: CellView): boolean { | ||||||
|     let data = getThralls().get(this.thrall); |     let data = getThralls().get(this.thrall); | ||||||
|     getCheckModal().show(data.initialCheck, () => { |     getCheckModal().show(data.initialCheck, () => { | ||||||
|       getPlayerProgress().unlockThrall(this.thrall); |       getPlayerProgress().unlockThrall(this.thrall); | ||||||
|       cell.pickup = null |       cell.pickup = null; | ||||||
|     }); |     }); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| @@ -163,27 +185,30 @@ export class ThrallPosterPickup { | |||||||
|     this.thrall = thrall; |     this.thrall = thrall; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   computeCostToClick() { return 0; } |   computeCostToClick() { | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   isObstructive() { return false; } |   isObstructive() { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   drawFloor() { } |   drawFloor() {} | ||||||
|   drawInAir(gridArt: GridArt) { |   drawInAir(gridArt: GridArt) { | ||||||
|     let data = getThralls().get(this.thrall); |     let data = getThralls().get(this.thrall); | ||||||
|     D.drawSprite(data.sprite, gridArt.project(0.0), 2, { |     D.drawSprite(data.sprite, gridArt.project(0.0), 2, { | ||||||
|       xScale: 2.0, |       xScale: 2.0, | ||||||
|       yScale: 2.0, |       yScale: 2.0, | ||||||
|     }) |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onClick(cell: CellView): boolean { |   onClick(cell: CellView): boolean { | ||||||
|     let data = getThralls().get(this.thrall); |     let data = getThralls().get(this.thrall); | ||||||
|     getCheckModal().show(data.posterCheck, () => cell.pickup = null); |     getCheckModal().show(data.posterCheck, () => (cell.pickup = null)); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| export class ThrallRecruitedPickup { | export class ThrallRecruitedPickup { | ||||||
|   thrall: Thrall; |   thrall: Thrall; | ||||||
|   bitten: boolean; |   bitten: boolean; | ||||||
| @@ -193,60 +218,78 @@ export class ThrallRecruitedPickup { | |||||||
|     this.bitten = false; |     this.bitten = false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   computeCostToClick() { return 0; } |   computeCostToClick() { | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   isObstructive() { return false; } |   isObstructive() { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   drawFloor() { } |   drawFloor() {} | ||||||
|   drawInAir(gridArt: GridArt) { |   drawInAir(gridArt: GridArt) { | ||||||
|     let data = getThralls().get(this.thrall); |     let data = getThralls().get(this.thrall); | ||||||
|     let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); |     let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); | ||||||
|     let ix = 0; |     let ix = 0; | ||||||
|     let rot = 0; |     let rot = 0; | ||||||
|  |  | ||||||
|     if (lifeStage == LifeStage.Vampirized) { ix = 1; } |     if (lifeStage == LifeStage.Vampirized) { | ||||||
|     if (lifeStage == LifeStage.Dead) { ix = 1; rot = 270; } |       ix = 1; | ||||||
|  |     } | ||||||
|  |     if (lifeStage == LifeStage.Dead) { | ||||||
|  |       ix = 1; | ||||||
|  |       rot = 270; | ||||||
|  |     } | ||||||
|     D.drawSprite(data.sprite, gridArt.project(0.0), ix, { |     D.drawSprite(data.sprite, gridArt.project(0.0), ix, { | ||||||
|       xScale: 2.0, |       xScale: 2.0, | ||||||
|       yScale: 2.0, |       yScale: 2.0, | ||||||
|       angle: rot |       angle: rot, | ||||||
|     }) |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onClick(_cell: CellView): boolean { |   onClick(_cell: CellView): boolean { | ||||||
|     if (this.bitten) { return true; } |     if (this.bitten) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let data = getThralls().get(this.thrall); |     let data = getThralls().get(this.thrall); | ||||||
|     let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); |     let lifeStage = getPlayerProgress().getThrallLifeStage(this.thrall); | ||||||
|     let text = data.lifeStageText[lifeStage]; |     let text = data.lifeStageText[lifeStage]; | ||||||
|     getCheckModal().show({ |     getCheckModal().show( | ||||||
|       label: `${text.prebite}`, |       { | ||||||
|       options: [ |         label: `${text.prebite}`, | ||||||
|         { |         options: [ | ||||||
|           isChoice: true, |           { | ||||||
|           countsAsSuccess: true, |             isChoice: true, | ||||||
|           unlockable: "Bite!", |             countsAsSuccess: true, | ||||||
|           success: text.postbite, |             unlockable: "Bite!", | ||||||
|         }, |             success: text.postbite, | ||||||
|         { |           }, | ||||||
|           isChoice: true, |           { | ||||||
|           countsAsSuccess: false, |             isChoice: true, | ||||||
|           unlockable: "Refrain", |             countsAsSuccess: false, | ||||||
|           success: "Maybe next time." |             unlockable: "Refrain", | ||||||
|         } |             success: "Maybe next time.", | ||||||
|       ] |           }, | ||||||
|     }, () => { |         ], | ||||||
|       this.bitten = true; |       }, | ||||||
|       getPlayerProgress().addBlood( |       () => { | ||||||
|         lifeStage == LifeStage.Fresh ? 1000 : |         this.bitten = true; | ||||||
|           lifeStage == LifeStage.Average ? 500 : |         getPlayerProgress().addBlood( | ||||||
|             lifeStage == LifeStage.Poor ? 300 : |           lifeStage == LifeStage.Fresh | ||||||
|               lifeStage == LifeStage.Vampirized ? 1500 : // lethal bite |             ? 1000 | ||||||
|                 // lifeStage == LifeStage.Dead ? |             : lifeStage == LifeStage.Average | ||||||
|                 100 |               ? 500 | ||||||
|       ); |               : lifeStage == LifeStage.Poor | ||||||
|       getPlayerProgress().damageThrall(this.thrall, choose([0.9])) |                 ? 300 | ||||||
|     }); |                 : lifeStage == LifeStage.Vampirized | ||||||
|  |                   ? 1500 // lethal bite | ||||||
|  |                   : // lifeStage == LifeStage.Dead ? | ||||||
|  |                     100, | ||||||
|  |         ); | ||||||
|  |         getPlayerProgress().damageThrall(this.thrall, choose([0.9])); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,31 +1,31 @@ | |||||||
| import {ALL_STATS, Skill, Stat, SuccessorOption, Wish} from "./datatypes.ts"; | import { ALL_STATS, Skill, Stat, SuccessorOption, Wish } from "./datatypes.ts"; | ||||||
| import {getSkills} from "./skills.ts"; | import { getSkills } from "./skills.ts"; | ||||||
| import {getThralls, LifeStage, Thrall} from "./thralls.ts"; | import { getThralls, LifeStage, Thrall } from "./thralls.ts"; | ||||||
|  |  | ||||||
| export class PlayerProgress { | export class PlayerProgress { | ||||||
|   #name: string |   #name: string; | ||||||
|   #stats: Record<Stat, number> |   #stats: Record<Stat, number>; | ||||||
|   #talents: Record<Stat, number> |   #talents: Record<Stat, number>; | ||||||
|   #isInPenance: boolean; |   #isInPenance: boolean; | ||||||
|   #wish: Wish | null; |   #wish: Wish | null; | ||||||
|   #exp: number; |   #exp: number; | ||||||
|   #blood: number |   #blood: number; | ||||||
|   #itemsPurloined: number |   #itemsPurloined: number; | ||||||
|   #skillsLearned: number[]  // use the raw ID representation for indexOf |   #skillsLearned: number[]; // use the raw ID representation for indexOf | ||||||
|   #untrimmedSkillsAvailable: Skill[] |   #untrimmedSkillsAvailable: Skill[]; | ||||||
|   #thrallsUnlocked: number[] |   #thrallsUnlocked: number[]; | ||||||
|   #thrallDamage: Record<number, number> |   #thrallDamage: Record<number, number>; | ||||||
|  |  | ||||||
|   constructor(asSuccessor: SuccessorOption, withWish: Wish | null) { |   constructor(asSuccessor: SuccessorOption, withWish: Wish | null) { | ||||||
|     this.#name = asSuccessor.name; |     this.#name = asSuccessor.name; | ||||||
|     this.#stats = {...asSuccessor.stats}; |     this.#stats = { ...asSuccessor.stats }; | ||||||
|     this.#talents = {...asSuccessor.talents}; |     this.#talents = { ...asSuccessor.talents }; | ||||||
|     this.#isInPenance = asSuccessor.inPenance; |     this.#isInPenance = asSuccessor.inPenance; | ||||||
|     this.#wish = withWish; |     this.#wish = withWish; | ||||||
|     this.#exp = 0; |     this.#exp = 0; | ||||||
|     this.#blood = 0; |     this.#blood = 0; | ||||||
|     this.#itemsPurloined = 0; |     this.#itemsPurloined = 0; | ||||||
|     this.#skillsLearned = [] |     this.#skillsLearned = []; | ||||||
|     this.#untrimmedSkillsAvailable = []; |     this.#untrimmedSkillsAvailable = []; | ||||||
|     this.#thrallsUnlocked = []; |     this.#thrallsUnlocked = []; | ||||||
|     this.#thrallDamage = {}; |     this.#thrallDamage = {}; | ||||||
| @@ -50,8 +50,10 @@ export class PlayerProgress { | |||||||
|   refill() { |   refill() { | ||||||
|     this.#blood = 2000; |     this.#blood = 2000; | ||||||
|  |  | ||||||
|     let learnableSkills = [];  // TODO: Also include costing info |     let learnableSkills = []; // TODO: Also include costing info | ||||||
|     for (let skill of getSkills().getAvailableSkills(this.#isInPenance).values()) { |     for (let skill of getSkills() | ||||||
|  |       .getAvailableSkills(this.#isInPenance) | ||||||
|  |       .values()) { | ||||||
|       if (this.#canBeAvailable(skill)) { |       if (this.#canBeAvailable(skill)) { | ||||||
|         learnableSkills.push(skill); |         learnableSkills.push(skill); | ||||||
|       } |       } | ||||||
| @@ -59,11 +61,16 @@ export class PlayerProgress { | |||||||
|  |  | ||||||
|     for (let thrall of getThralls().getAll()) { |     for (let thrall of getThralls().getAll()) { | ||||||
|       let stage = this.getThrallLifeStage(thrall); |       let stage = this.getThrallLifeStage(thrall); | ||||||
|       if (stage == LifeStage.Vampirized || stage == LifeStage.Dead) { continue; } |       if (stage == LifeStage.Vampirized || stage == LifeStage.Dead) { | ||||||
|       this.#thrallDamage[thrall.id] = Math.max(this.#thrallDamage[thrall.id] ?? 0 - 0.2, 0.0); |         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) { |   hasLearned(skill: Skill) { | ||||||
| @@ -72,14 +79,16 @@ export class PlayerProgress { | |||||||
|  |  | ||||||
|   learnSkill(skill: Skill) { |   learnSkill(skill: Skill) { | ||||||
|     if (this.#skillsLearned.indexOf(skill.id) != -1) { |     if (this.#skillsLearned.indexOf(skill.id) != -1) { | ||||||
|       return |       return; | ||||||
|     } |     } | ||||||
|     this.#skillsLearned.push(skill.id); |     this.#skillsLearned.push(skill.id); | ||||||
|  |  | ||||||
|     // remove entries for that skill |     // remove entries for that skill | ||||||
|     let skills2 = []; |     let skills2 = []; | ||||||
|     for (let entry of this.#untrimmedSkillsAvailable.values()) { |     for (let entry of this.#untrimmedSkillsAvailable.values()) { | ||||||
|       if (entry.id == skill.id) { continue; } |       if (entry.id == skill.id) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|       skills2.push(entry); |       skills2.push(entry); | ||||||
|     } |     } | ||||||
|     this.#untrimmedSkillsAvailable = skills2; |     this.#untrimmedSkillsAvailable = skills2; | ||||||
| @@ -96,7 +105,7 @@ export class PlayerProgress { | |||||||
|     // make sure the prereqs are met |     // make sure the prereqs are met | ||||||
|     for (let prereq of data.prereqs.values()) { |     for (let prereq of data.prereqs.values()) { | ||||||
|       if (!this.hasLearned(prereq)) { |       if (!this.hasLearned(prereq)) { | ||||||
|         return false |         return false; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -109,12 +118,12 @@ export class PlayerProgress { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   getItemsPurloined() { |   getItemsPurloined() { | ||||||
|     return this.#itemsPurloined |     return this.#itemsPurloined; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   add(stat: Stat, amount: number) { |   add(stat: Stat, amount: number) { | ||||||
|     if (amount != Math.floor(amount)) { |     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] += amount; | ||||||
|     this.#stats[stat] = Math.min(Math.max(this.#stats[stat], -99), 999); |     this.#stats[stat] = Math.min(Math.max(this.#stats[stat], -99), 999); | ||||||
| @@ -125,18 +134,18 @@ export class PlayerProgress { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   getExperience(): number { |   getExperience(): number { | ||||||
|     return this.#exp |     return this.#exp; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   spendExperience(cost: number) { |   spendExperience(cost: number) { | ||||||
|     if (this.#exp < cost) { |     if (this.#exp < cost) { | ||||||
|       throw `can't spend ${cost}` |       throw `can't spend ${cost}`; | ||||||
|     } |     } | ||||||
|     this.#exp -= cost; |     this.#exp -= cost; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getStat(stat: Stat): number { |   getStat(stat: Stat): number { | ||||||
|     return this.#stats[stat] |     return this.#stats[stat]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getTalent(stat: Stat): number { |   getTalent(stat: Stat): number { | ||||||
| @@ -149,7 +158,7 @@ export class PlayerProgress { | |||||||
|  |  | ||||||
|   addBlood(amt: number) { |   addBlood(amt: number) { | ||||||
|     this.#blood += amt; |     this.#blood += amt; | ||||||
|     this.#blood = Math.min(this.#blood, 5000) |     this.#blood = Math.min(this.#blood, 5000); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   spendBlood(amt: number) { |   spendBlood(amt: number) { | ||||||
| @@ -157,7 +166,7 @@ export class PlayerProgress { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   getWish(): Wish | null { |   getWish(): Wish | null { | ||||||
|     return this.#wish |     return this.#wish; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getAvailableSkills(): Skill[] { |   getAvailableSkills(): Skill[] { | ||||||
| @@ -167,30 +176,40 @@ export class PlayerProgress { | |||||||
|       let name1 = getSkills().get(a).profile.name; |       let name1 = getSkills().get(a).profile.name; | ||||||
|       let name2 = getSkills().get(b).profile.name; |       let name2 = getSkills().get(b).profile.name; | ||||||
|  |  | ||||||
|       if (name1 < name2) { return -1; } |       if (name1 < name2) { | ||||||
|       if (name1 > name2) { return 1; } |         return -1; | ||||||
|  |       } | ||||||
|  |       if (name1 > name2) { | ||||||
|  |         return 1; | ||||||
|  |       } | ||||||
|       return 0; |       return 0; | ||||||
|     }); |     }); | ||||||
|     skillsAvailable.sort((a, b) => { |     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() { |   getLearnedSkills() { | ||||||
|     let learnedSkills = [] |     let learnedSkills = []; | ||||||
|     for (let s of this.#skillsLearned.values()) { |     for (let s of this.#skillsLearned.values()) { | ||||||
|       learnedSkills.push({id: s}) |       learnedSkills.push({ id: s }); | ||||||
|     } |     } | ||||||
|     return learnedSkills; |     return learnedSkills; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getStats() { return {...this.#stats} } |   getStats() { | ||||||
|   getTalents() { return {...this.#talents} } |     return { ...this.#stats }; | ||||||
|  |   } | ||||||
|  |   getTalents() { | ||||||
|  |     return { ...this.#talents }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   unlockThrall(thrall: Thrall) { |   unlockThrall(thrall: Thrall) { | ||||||
|     let {id} = thrall; |     let { id } = thrall; | ||||||
|     if (this.#thrallsUnlocked.indexOf(id) != -1) { return; } |     if (this.#thrallsUnlocked.indexOf(id) != -1) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     this.#thrallsUnlocked.push(id); |     this.#thrallsUnlocked.push(id); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -200,34 +219,50 @@ export class PlayerProgress { | |||||||
|  |  | ||||||
|   damageThrall(thrall: Thrall, amount: number) { |   damageThrall(thrall: Thrall, amount: number) { | ||||||
|     if (amount <= 0.0) { |     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); |     let stage = this.getThrallLifeStage(thrall); | ||||||
|  |  | ||||||
|     if (stage == LifeStage.Vampirized) { this.#thrallDamage[thrall.id] = 4.0; } |     if (stage == LifeStage.Vampirized) { | ||||||
|     this.#thrallDamage[thrall.id] = (this.#thrallDamage[thrall.id] ?? 0.0) + amount |       this.#thrallDamage[thrall.id] = 4.0; | ||||||
|  |     } | ||||||
|  |     this.#thrallDamage[thrall.id] = | ||||||
|  |       (this.#thrallDamage[thrall.id] ?? 0.0) + amount; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getThrallLifeStage(thrall: Thrall): LifeStage { |   getThrallLifeStage(thrall: Thrall): LifeStage { | ||||||
|     let damage = this.#thrallDamage[thrall.id] ?? 0; |     let damage = this.#thrallDamage[thrall.id] ?? 0; | ||||||
|     console.log(`damage: ${damage}`) |     console.log(`damage: ${damage}`); | ||||||
|     if (damage < 0.5) { return LifeStage.Fresh; } |     if (damage < 0.5) { | ||||||
|     if (damage < 1.75) { return LifeStage.Average; } |       return LifeStage.Fresh; | ||||||
|     if (damage < 3.0) { return LifeStage.Poor; } |     } | ||||||
|     if (damage < 4.0) { return LifeStage.Vampirized; } |     if (damage < 1.75) { | ||||||
|  |       return LifeStage.Average; | ||||||
|  |     } | ||||||
|  |     if (damage < 3.0) { | ||||||
|  |       return LifeStage.Poor; | ||||||
|  |     } | ||||||
|  |     if (damage < 4.0) { | ||||||
|  |       return LifeStage.Vampirized; | ||||||
|  |     } | ||||||
|     return LifeStage.Dead; |     return LifeStage.Dead; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| let active: PlayerProgress | null = null; | 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); |   active = new PlayerProgress(asSuccessor, withWish); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getPlayerProgress(): PlayerProgress { | export function getPlayerProgress(): PlayerProgress { | ||||||
|   if (active == null) { |   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 { VNScene } from "./vnscene.ts"; | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
| import {getSkills} from "./skills.ts"; | import { getSkills } from "./skills.ts"; | ||||||
| import {Ending, SCORING_CATEGORIES, ScoringCategory} from "./datatypes.ts"; | import { Ending, SCORING_CATEGORIES, ScoringCategory } from "./datatypes.ts"; | ||||||
| import {sceneBat, sceneCharm, sceneLore, sceneParty, sceneStare, sceneStealth} from "./endings.ts"; | import { | ||||||
| import {generateWishes, getWishes, isWishCompleted} from "./wishes.ts"; |   sceneBat, | ||||||
| import {generateSuccessors} from "./successors.ts"; |   sceneCharm, | ||||||
|  |   sceneLore, | ||||||
|  |   sceneParty, | ||||||
|  |   sceneStare, | ||||||
|  |   sceneStealth, | ||||||
|  | } from "./endings.ts"; | ||||||
|  | import { generateWishes, getWishes, isWishCompleted } from "./wishes.ts"; | ||||||
|  | import { generateSuccessors } from "./successors.ts"; | ||||||
|  |  | ||||||
| class Scorer { | class Scorer { | ||||||
|   constructor() { } |   constructor() {} | ||||||
|  |  | ||||||
|   pickEnding(): Ending { |   pickEnding(): Ending { | ||||||
|     let learnedSkills = getPlayerProgress().getLearnedSkills(); |     let learnedSkills = getPlayerProgress().getLearnedSkills(); | ||||||
| @@ -30,7 +37,7 @@ class Scorer { | |||||||
|  |  | ||||||
|     // NOTE: This approach isn't efficient but it's easy to understand |     // NOTE: This approach isn't efficient but it's easy to understand | ||||||
|     // and it allows me to arbitrate ties however I want |     // 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) => { |     const isMax = (cat: ScoringCategory, min: number) => { | ||||||
|       let score = runningScores[cat] ?? 0; |       let score = runningScores[cat] ?? 0; | ||||||
|       runningScores[cat] = 0; // each category, once checked, can't disqualify any other category |       runningScores[cat] = 0; // each category, once checked, can't disqualify any other category | ||||||
| @@ -44,7 +51,7 @@ class Scorer { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       return true; |       return true; | ||||||
|     } |     }; | ||||||
|  |  | ||||||
|     let scene: VNScene; |     let scene: VNScene; | ||||||
|     let rank: string; |     let rank: string; | ||||||
| @@ -58,7 +65,7 @@ class Scorer { | |||||||
|     if (wish != null) { |     if (wish != null) { | ||||||
|       let data = getWishes().get(wish); |       let data = getWishes().get(wish); | ||||||
|       if (isWishCompleted(wish)) { |       if (isWishCompleted(wish)) { | ||||||
|         scene = data.onVictory |         scene = data.onVictory; | ||||||
|         rank = data.profile.name; |         rank = data.profile.name; | ||||||
|         domicile = data.profile.domicile; |         domicile = data.profile.domicile; | ||||||
|         reignSentence = data.profile.reignSentence; |         reignSentence = data.profile.reignSentence; | ||||||
| @@ -70,7 +77,6 @@ class Scorer { | |||||||
|         penance = true; |         penance = true; | ||||||
|         successorVerb = data.profile.failureSuccessorVerb; |         successorVerb = data.profile.failureSuccessorVerb; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|     } |     } | ||||||
|     // TODO: Award different ranks depending on second-to-top skill |     // TODO: Award different ranks depending on second-to-top skill | ||||||
|     // TODO: Award different domiciles based on overall score |     // TODO: Award different domiciles based on overall score | ||||||
| @@ -80,26 +86,22 @@ class Scorer { | |||||||
|       rank = "Hypno-Chiropteran"; |       rank = "Hypno-Chiropteran"; | ||||||
|       domicile = "Village of Brainwashed Mortals"; |       domicile = "Village of Brainwashed Mortals"; | ||||||
|       reignSentence = "You rule with a fair but unflinching gaze."; |       reignSentence = "You rule with a fair but unflinching gaze."; | ||||||
|     } |     } else if (isMax("lore", 3)) { | ||||||
|     else if (isMax("lore", 3)) { |  | ||||||
|       scene = sceneLore; |       scene = sceneLore; | ||||||
|       rank = "Loremaster"; |       rank = "Loremaster"; | ||||||
|       domicile = "Vineyard"; |       domicile = "Vineyard"; | ||||||
|       reignSentence = "You're well on the path to ultimate knowledge."; |       reignSentence = "You're well on the path to ultimate knowledge."; | ||||||
|     } |     } else if (isMax("charm", 2)) { | ||||||
|     else if (isMax("charm", 2)) { |  | ||||||
|       scene = sceneCharm; |       scene = sceneCharm; | ||||||
|       rank = "Seducer"; |       rank = "Seducer"; | ||||||
|       domicile = "Guest House"; |       domicile = "Guest House"; | ||||||
|       reignSentence = "You get to sink your fangs into anyone you want."; |       reignSentence = "You get to sink your fangs into anyone you want."; | ||||||
|     } |     } else if (isMax("party", 1)) { | ||||||
|     else if (isMax("party", 1)) { |  | ||||||
|       scene = sceneParty; |       scene = sceneParty; | ||||||
|       rank = "Party Animal"; |       rank = "Party Animal"; | ||||||
|       domicile = "Nightclub"; |       domicile = "Nightclub"; | ||||||
|       reignSentence = "Everyone thinks you're too cool to disobey."; |       reignSentence = "Everyone thinks you're too cool to disobey."; | ||||||
|     } |     } else if (isMax("stealth", 0)) { | ||||||
|     else if (isMax("stealth", 0)) { |  | ||||||
|       scene = sceneStealth; |       scene = sceneStealth; | ||||||
|       rank = "Invisible"; |       rank = "Invisible"; | ||||||
|       domicile = "Townhouse"; |       domicile = "Townhouse"; | ||||||
| @@ -110,7 +112,8 @@ class Scorer { | |||||||
|       scene = sceneBat; |       scene = sceneBat; | ||||||
|       rank = "Bat"; |       rank = "Bat"; | ||||||
|       domicile = "Cave"; |       domicile = "Cave"; | ||||||
|       reignSentence = "Your skreeking verdicts are irresistible to your subjects."; |       reignSentence = | ||||||
|  |         "Your skreeking verdicts are irresistible to your subjects."; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO: Analytics tracker |     // TODO: Analytics tracker | ||||||
| @@ -118,19 +121,25 @@ class Scorer { | |||||||
|       itemsPurloined, |       itemsPurloined, | ||||||
|       vampiricSkills, |       vampiricSkills, | ||||||
|       mortalServants, |       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 wishOptions = generateWishes(penance); | ||||||
|  |  | ||||||
|     let progenerateVerb = penance ? "Repent" : "Progenerate"; |     let progenerateVerb = penance ? "Repent" : "Progenerate"; | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       scene, |       scene, | ||||||
|       personal: {rank, domicile, reignSentence, successorVerb, progenerateVerb}, |       personal: { | ||||||
|  |         rank, | ||||||
|  |         domicile, | ||||||
|  |         reignSentence, | ||||||
|  |         successorVerb, | ||||||
|  |         progenerateVerb, | ||||||
|  |       }, | ||||||
|       analytics, |       analytics, | ||||||
|       successorOptions, |       successorOptions, | ||||||
|       wishOptions, |       wishOptions, | ||||||
|     } |     }; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,23 +3,27 @@ | |||||||
| export var shadowcast = function ( | export var shadowcast = function ( | ||||||
|   [ox, oy]: [number, number], |   [ox, oy]: [number, number], | ||||||
|   isBlocking: (xy: [number, number]) => boolean, |   isBlocking: (xy: [number, number]) => boolean, | ||||||
|   markVisible: (xy: [number, number]) => void |   markVisible: (xy: [number, number]) => void, | ||||||
| ) { | ) { | ||||||
|   for (var i = 0; i < 4; i++) { |   for (var i = 0; i < 4; i++) { | ||||||
|     var quadrant = new Quadrant(i, [ox, oy]); |     var quadrant = new Quadrant(i, [ox, oy]); | ||||||
|     var reveal = function (xy: [number, number]) { |     var reveal = function (xy: [number, number]) { | ||||||
|       markVisible(quadrant.transform(xy)); |       markVisible(quadrant.transform(xy)); | ||||||
|     } |     }; | ||||||
|     var isWall = function (xy: [number, number] | undefined) { |     var isWall = function (xy: [number, number] | undefined) { | ||||||
|       if (xy == undefined) { return false; } |       if (xy == undefined) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|       return isBlocking(quadrant.transform(xy)); |       return isBlocking(quadrant.transform(xy)); | ||||||
|     } |     }; | ||||||
|     var isFloor = function (xy: [number, number] | undefined) { |     var isFloor = function (xy: [number, number] | undefined) { | ||||||
|       if (xy == undefined) { return false; } |       if (xy == undefined) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|       return !isBlocking(quadrant.transform(xy)); |       return !isBlocking(quadrant.transform(xy)); | ||||||
|     } |     }; | ||||||
|     var scan = function (row: Row) { |     var scan = function (row: Row) { | ||||||
|       var prevXy: [number, number] | undefined |       var prevXy: [number, number] | undefined; | ||||||
|       row.forEachTile((xy) => { |       row.forEachTile((xy) => { | ||||||
|         if (isWall(xy) || isSymmetric(row, xy)) { |         if (isWall(xy) || isSymmetric(row, xy)) { | ||||||
|           reveal(xy); |           reveal(xy); | ||||||
| @@ -33,16 +37,16 @@ export var shadowcast = function ( | |||||||
|           scan(nextRow); |           scan(nextRow); | ||||||
|         } |         } | ||||||
|         prevXy = xy; |         prevXy = xy; | ||||||
|       }) |       }); | ||||||
|       if (isFloor(prevXy)) { |       if (isFloor(prevXy)) { | ||||||
|         scan(row.next()); |         scan(row.next()); | ||||||
|       } |       } | ||||||
|     } |     }; | ||||||
|  |  | ||||||
|     var firstRow = new Row(1, new Fraction(-1, 1), new Fraction(1, 1)); |     var firstRow = new Row(1, new Fraction(-1, 1), new Fraction(1, 1)); | ||||||
|     scan(firstRow); |     scan(firstRow); | ||||||
|   } |   } | ||||||
| } | }; | ||||||
|  |  | ||||||
| class Quadrant { | class Quadrant { | ||||||
|   cardinal: number; |   cardinal: number; | ||||||
| @@ -57,11 +61,16 @@ class Quadrant { | |||||||
|  |  | ||||||
|   transform([row, col]: [number, number]): [number, number] { |   transform([row, col]: [number, number]): [number, number] { | ||||||
|     switch (this.cardinal) { |     switch (this.cardinal) { | ||||||
|       case 0: return [this.ox + col, this.oy - row]; |       case 0: | ||||||
|       case 2: return [this.ox + col, this.oy + row]; |         return [this.ox + col, this.oy - row]; | ||||||
|       case 1: return [this.ox + row, this.oy + col]; |       case 2: | ||||||
|       case 3: return [this.ox - row, this.oy + col]; |         return [this.ox + col, this.oy + row]; | ||||||
|       default: throw new Error("invalid cardinal") |       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 minCol = roundTiesUp(this.startSlope.scale(this.depth)); | ||||||
|     var maxCol = roundTiesDown(this.endSlope.scale(this.depth)); |     var maxCol = roundTiesDown(this.endSlope.scale(this.depth)); | ||||||
|     for (var col = minCol; col <= maxCol; col++) { |     for (var col = minCol; col <= maxCol; col++) { | ||||||
|       cb([this.depth, col]) |       cb([this.depth, col]); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   next(): Row { |   next(): Row { | ||||||
| @@ -109,17 +118,19 @@ class Fraction { | |||||||
|  |  | ||||||
| var slope = function ([rowDepth, col]: [number, number]): Fraction { | var slope = function ([rowDepth, col]: [number, number]): Fraction { | ||||||
|   return new Fraction(2 * col - 1, 2 * rowDepth); |   return new Fraction(2 * col - 1, 2 * rowDepth); | ||||||
| } | }; | ||||||
|  |  | ||||||
| var isSymmetric = function (row: Row, [_, col]: [number, number]) { | var isSymmetric = function (row: Row, [_, col]: [number, number]) { | ||||||
|   return col >= row.startSlope.scale(row.depth).toDouble() && |   return ( | ||||||
|     col <= (row.endSlope.scale(row.depth)).toDouble(); |     col >= row.startSlope.scale(row.depth).toDouble() && | ||||||
| } |     col <= row.endSlope.scale(row.depth).toDouble() | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
| var roundTiesUp = function (n: Fraction) { | var roundTiesUp = function (n: Fraction) { | ||||||
|   return Math.floor(n.toDouble() + 0.5); |   return Math.floor(n.toDouble() + 0.5); | ||||||
| } | }; | ||||||
|  |  | ||||||
| var roundTiesDown = function (n: Fraction) { | var roundTiesDown = function (n: Fraction) { | ||||||
|   return Math.ceil(n.toDouble() - 0.5); |   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 { | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; |   Skill, | ||||||
| import {getCostMultiplier} from "./wishes.ts"; |   SkillData, | ||||||
|  |   SkillGoverning, | ||||||
|  |   SkillScoring, | ||||||
|  |   Stat, | ||||||
|  | } from "./datatypes.ts"; | ||||||
|  | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
|  | import { getCostMultiplier } from "./wishes.ts"; | ||||||
|  |  | ||||||
| class SkillsTable { | class SkillsTable { | ||||||
|   #skills: SkillData[] |   #skills: SkillData[]; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     this.#skills = []; |     this.#skills = []; | ||||||
| @@ -12,19 +18,21 @@ class SkillsTable { | |||||||
|   add(data: SkillData): Skill { |   add(data: SkillData): Skill { | ||||||
|     let id = this.#skills.length; |     let id = this.#skills.length; | ||||||
|     this.#skills.push(data); |     this.#skills.push(data); | ||||||
|     return {id}; |     return { id }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get(skill: Skill): SkillData { |   get(skill: Skill): SkillData { | ||||||
|     return this.#skills[skill.id] |     return this.#skills[skill.id]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getAvailableSkills(includeDegrading: boolean): Skill[] { |   getAvailableSkills(includeDegrading: boolean): Skill[] { | ||||||
|     let skills = []; |     let skills = []; | ||||||
|     for (let i = 0; i < this.#skills.length; i++) { |     for (let i = 0; i < this.#skills.length; i++) { | ||||||
|       let isDegrading = this.#skills[i].isDegrading ?? false; |       let isDegrading = this.#skills[i].isDegrading ?? false; | ||||||
|       if (isDegrading && !includeDegrading) { continue; } |       if (isDegrading && !includeDegrading) { | ||||||
|       skills.push({id: i}); |         continue; | ||||||
|  |       } | ||||||
|  |       skills.push({ id: i }); | ||||||
|     } |     } | ||||||
|     return skills; |     return skills; | ||||||
|   } |   } | ||||||
| @@ -34,23 +42,31 @@ class SkillsTable { | |||||||
|  |  | ||||||
|     let governingStatValue = 0; |     let governingStatValue = 0; | ||||||
|     for (let stat of data.governing.stats.values()) { |     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) { |     if (data.governing.flipped) { | ||||||
|       governingStatValue = - governingStatValue + 10; |       governingStatValue = -governingStatValue + 10; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let mult = getCostMultiplier(getPlayerProgress().getWish(), skill); |     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; |     underTarget = mult * underTarget; | ||||||
|     target = mult * target; |     target = mult * target; | ||||||
|  |  | ||||||
|     return Math.floor(geomInterpolate( |     return Math.floor( | ||||||
|       governingStatValue, |       geomInterpolate( | ||||||
|       underTarget, target, |         governingStatValue, | ||||||
|       data.governing.cost, 999 |         underTarget, | ||||||
|     )) |         target, | ||||||
|  |         data.governing.cost, | ||||||
|  |         999, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -61,71 +77,111 @@ function geomInterpolate( | |||||||
|   lowOut: number, |   lowOut: number, | ||||||
|   highOut: number, |   highOut: number, | ||||||
| ) { | ) { | ||||||
|   if (x < lowIn) { return highOut; } |   if (x < lowIn) { | ||||||
|   if (x >= highIn) { return lowOut; } |     return highOut; | ||||||
|  |   } | ||||||
|  |   if (x >= highIn) { | ||||||
|  |     return lowOut; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   const proportion = 1.0 - (x - lowIn) / (highIn - lowIn); |   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 = { | type GoverningTemplate = { | ||||||
|   stats: Stat[], |   stats: Stat[]; | ||||||
|   note: string |   note: string; | ||||||
|   scoring: SkillScoring, |   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> = { | let templates: Record<Track, GoverningTemplate> = { | ||||||
|   bat: { |   bat: { | ||||||
|     stats: ["AGI", "AGI", "PSI"], |     stats: ["AGI", "AGI", "PSI"], | ||||||
|     note: "Cheaper with AGI and PSI.", |     note: "Cheaper with AGI and PSI.", | ||||||
|     scoring: {bat: 1}, |     scoring: { bat: 1 }, | ||||||
|   }, |   }, | ||||||
|   stealth: { |   stealth: { | ||||||
|     stats: ["AGI", "AGI", "INT"], |     stats: ["AGI", "AGI", "INT"], | ||||||
|     note: "Cheaper with AGI and INT.", |     note: "Cheaper with AGI and INT.", | ||||||
|     scoring: {stealth: 1}, |     scoring: { stealth: 1 }, | ||||||
|   }, |   }, | ||||||
|   charm: { |   charm: { | ||||||
|     stats: ["CHA", "PSI", "PSI"], |     stats: ["CHA", "PSI", "PSI"], | ||||||
|     note: "Cheaper with CHA and PSI.", |     note: "Cheaper with CHA and PSI.", | ||||||
|     scoring: {charm: 1}, |     scoring: { charm: 1 }, | ||||||
|   }, |   }, | ||||||
|   stare: { |   stare: { | ||||||
|     stats: ["PSI", "PSI"], |     stats: ["PSI", "PSI"], | ||||||
|     note: "Cheaper with PSI.", |     note: "Cheaper with PSI.", | ||||||
|     scoring: {stare: 1}, |     scoring: { stare: 1 }, | ||||||
|   }, |   }, | ||||||
|   party: { |   party: { | ||||||
|     stats: ["CHA", "CHA", "PSI"], |     stats: ["CHA", "CHA", "PSI"], | ||||||
|     note: "Cheaper with CHA and PSI.", |     note: "Cheaper with CHA and PSI.", | ||||||
|     scoring: {party: 1}, |     scoring: { party: 1 }, | ||||||
|   }, |   }, | ||||||
|   lore: { |   lore: { | ||||||
|     stats: ["INT", "INT", "CHA"], |     stats: ["INT", "INT", "CHA"], | ||||||
|     note: "Cheaper with INT and CHA.", |     note: "Cheaper with INT and CHA.", | ||||||
|     scoring: {lore: 1}, |     scoring: { lore: 1 }, | ||||||
|   }, |   }, | ||||||
|   penance: { |   penance: { | ||||||
|     stats: ["AGI", "INT", "CHA", "PSI"], |     stats: ["AGI", "INT", "CHA", "PSI"], | ||||||
|     note: "Lower your stats for this.", |     note: "Lower your stats for this.", | ||||||
|     scoring: {}, |     scoring: {}, | ||||||
|   } |   }, | ||||||
| } | }; | ||||||
|  |  | ||||||
| function governing(track: Track, difficulty: Difficulty, flipped?: boolean): SkillGoverning { | function governing( | ||||||
|  |   track: Track, | ||||||
|  |   difficulty: Difficulty, | ||||||
|  |   flipped?: boolean, | ||||||
|  | ): SkillGoverning { | ||||||
|   let template = templates[track]; |   let template = templates[track]; | ||||||
|   let underTarget: number |   let underTarget: number; | ||||||
|   let target: number |   let target: number; | ||||||
|   let cost: number |   let cost: number; | ||||||
|   let mortalServantValue: number; |   let mortalServantValue: number; | ||||||
|   switch(difficulty) { |   switch (difficulty) { | ||||||
|     case 0: underTarget = 5; target = 15; cost = 50; mortalServantValue = 1; break; |     case 0: | ||||||
|     case 1: underTarget = 15; target = 40; cost = 100; mortalServantValue = 2; break; |       underTarget = 5; | ||||||
|     case 1.25: underTarget = 17; target = 42; cost = 100; mortalServantValue = 2; break; |       target = 15; | ||||||
|     case 2: underTarget = 30; target = 70; cost = 125; mortalServantValue = 3; break; |       cost = 50; | ||||||
|     case 3: underTarget = 50; target = 100; cost = 150; mortalServantValue = 10; break; |       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) { |   if (flipped) { | ||||||
| @@ -141,7 +197,7 @@ function governing(track: Track, difficulty: Difficulty, flipped?: boolean): Ski | |||||||
|     scoring: template.scoring, |     scoring: template.scoring, | ||||||
|     mortalServantValue: mortalServantValue, |     mortalServantValue: mortalServantValue, | ||||||
|     flipped: flipped ?? false, |     flipped: flipped ?? false, | ||||||
|   } |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| let table = new SkillsTable(); | let table = new SkillsTable(); | ||||||
| @@ -151,195 +207,219 @@ export let bat0 = table.add({ | |||||||
|   governing: governing("bat", 0), |   governing: governing("bat", 0), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Screech", |     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({ | export let bat1 = table.add({ | ||||||
|   governing: governing("bat", 1), |   governing: governing("bat", 1), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Flap", |     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({ | export let bat2 = table.add({ | ||||||
|   governing: governing("bat", 2), |   governing: governing("bat", 2), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Transform", |     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({ | export let bat3 = table.add({ | ||||||
|   governing: governing("bat", 3), |   governing: governing("bat", 3), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Eat Bugs", |     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({ | export let stealth0 = table.add({ | ||||||
|   governing: governing("stealth", 0), |   governing: governing("stealth", 0), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Be Quiet", |     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({ | export let stealth1 = table.add({ | ||||||
|   governing: governing("stealth", 1), |   governing: governing("stealth", 1), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Disguise", |     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({ | export let stealth2 = table.add({ | ||||||
|   governing: governing("stealth", 2), |   governing: governing("stealth", 2), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Sneak", |     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({ | export let stealth3 = table.add({ | ||||||
|   governing: governing("stealth", 3), |   governing: governing("stealth", 3), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Turn Invisible", |     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({ | export let charm0 = table.add({ | ||||||
|   governing: governing("charm", 0), |   governing: governing("charm", 0), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Flatter", |     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({ | export let charm1 = table.add({ | ||||||
|   governing: governing("charm", 1), |   governing: governing("charm", 1), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Befriend", |     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({ | export let charm2 = table.add({ | ||||||
|   governing: governing("charm", 2), |   governing: governing("charm", 2), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Seduce", |     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({ | export let charm3 = table.add({ | ||||||
|   governing: governing("charm", 3), |   governing: governing("charm", 3), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Infatuate", |     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({ | export let stare0 = table.add({ | ||||||
|   governing: governing("stare", 0), |   governing: governing("stare", 0), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Dazzle", |     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({ | export let stare1 = table.add({ | ||||||
|   governing: governing("stare", 1), |   governing: governing("stare", 1), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Hypnotize", |     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({ | export let stare2 = table.add({ | ||||||
|   governing: governing("stare", 2), |   governing: governing("stare", 2), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Enthrall", |     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({ | export let stare3 = table.add({ | ||||||
|   governing: governing("stare", 3), |   governing: governing("stare", 3), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Seal Memory", |     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({ | export let party0 = table.add({ | ||||||
|   governing: governing("party", 0), |   governing: governing("party", 0), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Chug", |     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({ | export let party1 = table.add({ | ||||||
|   governing: governing("party", 1), |   governing: governing("party", 1), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Rave", |     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({ | export let party2 = table.add({ | ||||||
|   governing: governing("party", 2), |   governing: governing("party", 2), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Peer Pressure", |     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({ | export let party3 = table.add({ | ||||||
|   governing: governing("party", 3), |   governing: governing("party", 3), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Sleep It Off", |     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({ | export let lore0 = table.add({ | ||||||
|   governing: governing("lore", 0), |   governing: governing("lore", 0), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Respect Elders", |     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({ | export let lore1 = table.add({ | ||||||
|   governing: governing("lore", 1), |   governing: governing("lore", 1), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Brick by Brick", |     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({ | export let lore2 = table.add({ | ||||||
|   governing: governing("lore", 2), |   governing: governing("lore", 2), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Make Wine", |     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({ | export let lore3 = table.add({ | ||||||
|   governing: governing("lore", 3), |   governing: governing("lore", 3), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Third Clade", |     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({ | export let sorry0 = table.add({ | ||||||
| @@ -347,20 +427,21 @@ export let sorry0 = table.add({ | |||||||
|   governing: governing("penance", 0, true), |   governing: governing("penance", 0, true), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "I'm Sorry", |     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: [], |   prereqs: [], | ||||||
| }) | }); | ||||||
|  |  | ||||||
| export let sorry1 = table.add({ | export let sorry1 = table.add({ | ||||||
|   isDegrading: true, |   isDegrading: true, | ||||||
|   governing: governing("penance", 1, true), |   governing: governing("penance", 1, true), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "I'm So Sorry", |     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: [], |   prereqs: [], | ||||||
| }) | }); | ||||||
|  |  | ||||||
| export let sorry2 = table.add({ | export let sorry2 = table.add({ | ||||||
|   isDegrading: true, |   isDegrading: true, | ||||||
| @@ -368,11 +449,12 @@ export let sorry2 = table.add({ | |||||||
|   governing: governing("penance", 1.25, true), |   governing: governing("penance", 1.25, true), | ||||||
|   profile: { |   profile: { | ||||||
|     name: "Forgive Me", |     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: [], |   prereqs: [], | ||||||
| }) | }); | ||||||
|  |  | ||||||
| export function getSkills(): SkillsTable { | export function getSkills(): SkillsTable { | ||||||
|   return table; |   return table; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,14 +1,12 @@ | |||||||
| import {getPartLocation, withCamera} from "./layout.ts"; | import { getPartLocation, withCamera } from "./layout.ts"; | ||||||
| import {AlignX, Point, Rect, Size} from "./engine/datatypes.ts"; | import { AlignX, Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {DrawPile} from "./drawpile.ts"; | import { DrawPile } from "./drawpile.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
| import {BG_INSET, FG_BOLD, FG_TEXT} from "./colors.ts"; | import { BG_INSET, FG_BOLD, FG_TEXT } from "./colors.ts"; | ||||||
| import {addButton} from "./button.ts"; | import { addButton } from "./button.ts"; | ||||||
| import { | import { getSkills } from "./skills.ts"; | ||||||
|   getSkills, | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
| } from "./skills.ts"; | import { Skill, SkillData } from "./datatypes.ts"; | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; |  | ||||||
| import {Skill, SkillData} from "./datatypes.ts"; |  | ||||||
|  |  | ||||||
| export class SkillsModal { | export class SkillsModal { | ||||||
|   #drawpile: DrawPile; |   #drawpile: DrawPile; | ||||||
| @@ -24,7 +22,7 @@ export class SkillsModal { | |||||||
|   get #size(): Size { |   get #size(): Size { | ||||||
|     // Instead of calculating this here, compute it from outside |     // Instead of calculating this here, compute it from outside | ||||||
|     // as it has to be the same for every bottom modal |     // as it has to be the same for every bottom modal | ||||||
|     return getPartLocation("BottomModal").size |     return getPartLocation("BottomModal").size; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get isShown(): boolean { |   get isShown(): boolean { | ||||||
| @@ -32,23 +30,23 @@ export class SkillsModal { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   setShown(shown: boolean) { |   setShown(shown: boolean) { | ||||||
|     this.#shown = shown |     this.#shown = shown; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     withCamera("BottomModal", () => this.#update()) |     withCamera("BottomModal", () => this.#update()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     withCamera("BottomModal", () => this.#draw()) |     withCamera("BottomModal", () => this.#draw()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #update() { |   #update() { | ||||||
|     this.#drawpile.clear(); |     this.#drawpile.clear(); | ||||||
|     let size = this.#size |     let size = this.#size; | ||||||
|     this.#drawpile.add(0, () => { |     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 |     // draw skills | ||||||
|     let availableSkills = getPlayerProgress().getAvailableSkills(); |     let availableSkills = getPlayerProgress().getAvailableSkills(); | ||||||
| @@ -61,7 +59,7 @@ export class SkillsModal { | |||||||
|       let cost = getSkills().computeCost(skill); |       let cost = getSkills().computeCost(skill); | ||||||
|       let y_ = y; |       let y_ = y; | ||||||
|       let selected = this.#skillSelection?.id == skill.id; |       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; |       let enabled = true; | ||||||
|  |  | ||||||
|       this.#drawpile.addClickable( |       this.#drawpile.addClickable( | ||||||
| @@ -74,14 +72,16 @@ export class SkillsModal { | |||||||
|           } |           } | ||||||
|           D.fillRect(skillRect.top, skillRect.size, bg); |           D.fillRect(skillRect.top, skillRect.size, bg); | ||||||
|           D.drawText(data.profile.name, new Point(4, y_), fg); |           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, |         skillRect, | ||||||
|         enabled, |         enabled, | ||||||
|         () => { |         () => { | ||||||
|           this.#skillSelection = skill; |           this.#skillSelection = skill; | ||||||
|         } |         }, | ||||||
|       ) |       ); | ||||||
|       y += 16; |       y += 16; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -94,14 +94,19 @@ export class SkillsModal { | |||||||
|       let remainingWidth = size.w - 160; |       let remainingWidth = size.w - 160; | ||||||
|  |  | ||||||
|       this.#drawpile.add(0, () => { |       this.#drawpile.add(0, () => { | ||||||
|         D.fillRect(new Point(160, 0), new Size(remainingWidth, 96), FG_BOLD) |         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.drawText(createFullDescription(data), new Point(164, 0), BG_INSET, { | ||||||
|  |           forceWidth: remainingWidth - 8, | ||||||
|  |         }); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       // add learn button |       // 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 canAfford = getPlayerProgress().getExperience() >= cost; | ||||||
|       let caption = `Learn ${data.profile.name}` |       let caption = `Learn ${data.profile.name}`; | ||||||
|       if (!canAfford) { |       if (!canAfford) { | ||||||
|         caption = `Can't Afford`; |         caption = `Can't Afford`; | ||||||
|       } |       } | ||||||
| @@ -109,15 +114,14 @@ export class SkillsModal { | |||||||
|       addButton(this.#drawpile, caption, drawButtonRect, canAfford, () => { |       addButton(this.#drawpile, caption, drawButtonRect, canAfford, () => { | ||||||
|         getPlayerProgress().spendExperience(cost); |         getPlayerProgress().spendExperience(cost); | ||||||
|         getPlayerProgress().learnSkill(selection); |         getPlayerProgress().learnSkill(selection); | ||||||
|       }) |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     // add close button |     // 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, () => { |     addButton(this.#drawpile, "Back", closeRect, true, () => { | ||||||
|       this.setShown(false); |       this.setShown(false); | ||||||
|     }) |     }); | ||||||
|     this.#drawpile.executeOnClick(); |     this.#drawpile.executeOnClick(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -150,5 +154,5 @@ export function getSkillsModal(): SkillsModal { | |||||||
| } | } | ||||||
|  |  | ||||||
| function createFullDescription(data: SkillData) { | 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 { DrawPile } from "./drawpile.ts"; | ||||||
| import {Point, Rect, Size} from "./engine/datatypes.ts"; | import { Point, Rect, Size } from "./engine/datatypes.ts"; | ||||||
| import {getPartLocation, withCamera} from "./layout.ts"; | import { getPartLocation, withCamera } from "./layout.ts"; | ||||||
| import {addButton} from "./button.ts"; | import { addButton } from "./button.ts"; | ||||||
| import {D} from "./engine/public.ts"; | import { D } from "./engine/public.ts"; | ||||||
| import {BG_INSET} from "./colors.ts"; | import { BG_INSET } from "./colors.ts"; | ||||||
| import {getSkillsModal} from "./skillsmodal.ts"; | import { getSkillsModal } from "./skillsmodal.ts"; | ||||||
| import {getStateManager} from "./statemanager.ts"; | import { getStateManager } from "./statemanager.ts"; | ||||||
|  |  | ||||||
| export class SleepModal { | export class SleepModal { | ||||||
|   #drawpile: DrawPile; |   #drawpile: DrawPile; | ||||||
| @@ -20,7 +20,7 @@ export class SleepModal { | |||||||
|     // We share this logic with SkillModal: |     // We share this logic with SkillModal: | ||||||
|     // Instead of calculating this here, compute it from outside |     // Instead of calculating this here, compute it from outside | ||||||
|     // as it has to be the same for every bottom modal |     // as it has to be the same for every bottom modal | ||||||
|     return getPartLocation("BottomModal").size |     return getPartLocation("BottomModal").size; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get isShown(): boolean { |   get isShown(): boolean { | ||||||
| @@ -28,35 +28,34 @@ export class SleepModal { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   setShown(shown: boolean) { |   setShown(shown: boolean) { | ||||||
|     this.#shown = shown |     this.#shown = shown; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     withCamera("BottomModal", () => this.#update()) |     withCamera("BottomModal", () => this.#update()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     withCamera("BottomModal", () => this.#draw()) |     withCamera("BottomModal", () => this.#draw()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #update() { |   #update() { | ||||||
|     this.#drawpile.clear(); |     this.#drawpile.clear(); | ||||||
|     let size = this.#size |     let size = this.#size; | ||||||
|     this.#drawpile.add(0, () => { |     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 |     // 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, () => { |     addButton(this.#drawpile, "Back", closeRect, true, () => { | ||||||
|       this.setShown(false); |       this.setShown(false); | ||||||
|     }) |     }); | ||||||
|  |  | ||||||
|     let skillsRect = new Rect(new Point(80, 96), new Size(80, 32)); |     let skillsRect = new Rect(new Point(80, 96), new Size(80, 32)); | ||||||
|     addButton(this.#drawpile, "Skills", skillsRect, true, () => { |     addButton(this.#drawpile, "Skills", skillsRect, true, () => { | ||||||
|       getSkillsModal().setShown(true); |       getSkillsModal().setShown(true); | ||||||
|     }) |     }); | ||||||
|  |  | ||||||
|     let remainingWidth = size.w - 160; |     let remainingWidth = size.w - 160; | ||||||
|     let nextRect = new Rect(new Point(160, 96), new Size(remainingWidth, 32)); |     let nextRect = new Rect(new Point(160, 96), new Size(remainingWidth, 32)); | ||||||
| @@ -75,4 +74,4 @@ export class SleepModal { | |||||||
| let active = new SleepModal(); | let active = new SleepModal(); | ||||||
| export function getSleepModal(): SleepModal { | export function getSleepModal(): SleepModal { | ||||||
|   return active; |   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 imgRaccoon from "./art/characters/raccoon.png"; | ||||||
| import imgResourcePickup from "./art/pickups/resources.png"; | import imgResourcePickup from "./art/pickups/resources.png"; | ||||||
| import imgStatPickup from "./art/pickups/stats.png"; | import imgStatPickup from "./art/pickups/stats.png"; | ||||||
| import imgLadder from "./art/pickups/ladder.png"; | import imgLadder from "./art/pickups/ladder.png"; | ||||||
| import imgLock from "./art/pickups/lock.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 imgThrallBat from "./art/thralls/thrall_bat.png"; | ||||||
| import imgThrallCharm from "./art/thralls/thrall_charm.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 imgThrallStare from "./art/thralls/thrall_stare.png"; | ||||||
| import imgThrallStealth from "./art/thralls/thrall_stealth.png"; | import imgThrallStealth from "./art/thralls/thrall_stealth.png"; | ||||||
|  |  | ||||||
|  |  | ||||||
| export let sprRaccoon = new Sprite( | export let sprRaccoon = new Sprite( | ||||||
|   imgRaccoon, |   imgRaccoon, | ||||||
|   new Size(64, 64), new Point(32, 32), new Size(1, 1), |   new Size(64, 64), | ||||||
|   1 |   new Point(32, 32), | ||||||
|  |   new Size(1, 1), | ||||||
|  |   1, | ||||||
| ); | ); | ||||||
| export let sprResourcePickup = new Sprite( | export let sprResourcePickup = new Sprite( | ||||||
|   imgResourcePickup, new Size(32, 32), new Point(16, 16), |   imgResourcePickup, | ||||||
|   new Size(1, 1), 1 |   new Size(32, 32), | ||||||
|  |   new Point(16, 16), | ||||||
|  |   new Size(1, 1), | ||||||
|  |   1, | ||||||
| ); | ); | ||||||
|  |  | ||||||
| export let sprStatPickup = new Sprite( | export let sprStatPickup = new Sprite( | ||||||
|   imgStatPickup, new Size(32, 32), new Point(16, 16), |   imgStatPickup, | ||||||
|   new Size(4, 1), 4 |   new Size(32, 32), | ||||||
|  |   new Point(16, 16), | ||||||
|  |   new Size(4, 1), | ||||||
|  |   4, | ||||||
| ); | ); | ||||||
|  |  | ||||||
| export let sprLadder = new Sprite( | export let sprLadder = new Sprite( | ||||||
|   imgLadder, new Size(16, 16), new Point(8, 8), |   imgLadder, | ||||||
|   new Size(1, 1), 1 |   new Size(16, 16), | ||||||
|  |   new Point(8, 8), | ||||||
|  |   new Size(1, 1), | ||||||
|  |   1, | ||||||
| ); | ); | ||||||
|  |  | ||||||
| export let sprLock = new Sprite( | export let sprLock = new Sprite( | ||||||
|   imgLock, new Size(16, 16), new Point(8, 8), |   imgLock, | ||||||
|   new Size(1, 1), 1 |   new Size(16, 16), | ||||||
|  |   new Point(8, 8), | ||||||
|  |   new Size(1, 1), | ||||||
|  |   1, | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | export let sprThrallBat = new Sprite( | ||||||
| export let sprThrallBat = new Sprite(imgThrallBat, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); |   imgThrallBat, | ||||||
| export let sprThrallCharm = new Sprite(imgThrallCharm, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); |   new Size(24, 24), | ||||||
| export let sprThrallLore = new Sprite(imgThrallLore, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); |   new Point(12, 12), | ||||||
| export let sprThrallParty = new Sprite(imgThrallParty, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); |   new Size(3, 1), | ||||||
| export let sprThrallStare = new Sprite(imgThrallStare, new Size(24, 24), new Point(12, 12), new Size(3, 1), 3); |   3, | ||||||
| export let sprThrallStealth = new Sprite(imgThrallStealth, 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 { getPlayerProgress, initPlayerProgress } from "./playerprogress.ts"; | ||||||
| import {getHuntMode, HuntMode, initHuntMode} from "./huntmode.ts"; | import { getHuntMode, HuntMode, initHuntMode } from "./huntmode.ts"; | ||||||
| import {getSleepModal} from "./sleepmodal.ts"; | import { getSleepModal } from "./sleepmodal.ts"; | ||||||
| import {getVNModal} from "./vnmodal.ts"; | import { getVNModal } from "./vnmodal.ts"; | ||||||
| import {getScorer} from "./scorer.ts"; | import { getScorer } from "./scorer.ts"; | ||||||
| import {getEndgameModal} from "./endgamemodal.ts"; | import { getEndgameModal } from "./endgamemodal.ts"; | ||||||
| import {SuccessorOption, Wish} from "./datatypes.ts"; | import { SuccessorOption, Wish } from "./datatypes.ts"; | ||||||
| import {generateManor} from "./manormap.ts"; | import { generateManor } from "./manormap.ts"; | ||||||
|  |  | ||||||
| const N_TURNS: number = 9; | const N_TURNS: number = 9; | ||||||
|  |  | ||||||
| @@ -17,7 +17,7 @@ export class StateManager { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   getTurn(): number { |   getTurn(): number { | ||||||
|     return this.#turn |     return this.#turn; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   startGame(asSuccessor: SuccessorOption, withWish: Wish | null) { |   startGame(asSuccessor: SuccessorOption, withWish: Wish | null) { | ||||||
| @@ -43,11 +43,11 @@ export class StateManager { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   getMaxTurns() { |   getMaxTurns() { | ||||||
|     return N_TURNS |     return N_TURNS; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| let active: StateManager = new StateManager(); | let active: StateManager = new StateManager(); | ||||||
| export function getStateManager(): StateManager { | export function getStateManager(): StateManager { | ||||||
|   return active |   return active; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,9 +1,12 @@ | |||||||
| import {ALL_STATS, Skill, Stat, SuccessorOption} from "./datatypes.ts"; | import { ALL_STATS, Skill, Stat, SuccessorOption } from "./datatypes.ts"; | ||||||
| import {generateName, generateTitle} from "./namegen.ts"; | import { generateName, generateTitle } from "./namegen.ts"; | ||||||
| import {choose} from "./utils.ts"; | import { choose } from "./utils.ts"; | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
|  |  | ||||||
| export function generateSuccessors(nImprovements: number, penance: boolean): SuccessorOption[] { | export function generateSuccessors( | ||||||
|  |   nImprovements: number, | ||||||
|  |   penance: boolean, | ||||||
|  | ): SuccessorOption[] { | ||||||
|   if (penance) { |   if (penance) { | ||||||
|     return [generateSuccessorFromPlayer()]; |     return [generateSuccessorFromPlayer()]; | ||||||
|   } |   } | ||||||
| @@ -34,12 +37,12 @@ export function generateSuccessorFromPlayer(): SuccessorOption { | |||||||
|     name: progress.name, |     name: progress.name, | ||||||
|     title: "Penitent", |     title: "Penitent", | ||||||
|     note: "Failed at Master's bidding", |     note: "Failed at Master's bidding", | ||||||
|     stats: {...progress.getStats()}, |     stats: { ...progress.getStats() }, | ||||||
|     talents: {...progress.getTalents()}, |     talents: { ...progress.getTalents() }, | ||||||
|     skills: [...progress.getLearnedSkills()], |     skills: [...progress.getLearnedSkills()], | ||||||
|     inPenance: true, |     inPenance: true, | ||||||
|     isCompulsory: true, |     isCompulsory: true, | ||||||
|   } |   }; | ||||||
|  |  | ||||||
|   for (let stat of ALL_STATS.values()) { |   for (let stat of ALL_STATS.values()) { | ||||||
|     successor.talents[stat] = -8; |     successor.talents[stat] = -8; | ||||||
| @@ -52,30 +55,35 @@ export function generateSuccessor(nImprovements: number): SuccessorOption { | |||||||
|   let title = generateTitle(); |   let title = generateTitle(); | ||||||
|   let note = null; |   let note = null; | ||||||
|   let stats: Record<Stat, number> = { |   let stats: Record<Stat, number> = { | ||||||
|     "AGI": 10 + choose([1, 2]), |     AGI: 10 + choose([1, 2]), | ||||||
|     "INT": 10 + choose([1, 2]), |     INT: 10 + choose([1, 2]), | ||||||
|     "CHA": 10 + choose([1, 2]), |     CHA: 10 + choose([1, 2]), | ||||||
|     "PSI": 10 + choose([1, 2]), |     PSI: 10 + choose([1, 2]), | ||||||
|   } |   }; | ||||||
|   let talents: Record<Stat, number> = { |   let talents: Record<Stat, number> = { | ||||||
|     "AGI": 0, |     AGI: 0, | ||||||
|     "INT": 0, |     INT: 0, | ||||||
|     "CHA": 0, |     CHA: 0, | ||||||
|     "PSI": 0, |     PSI: 0, | ||||||
|   } |   }; | ||||||
|  |  | ||||||
|   let improvements = [ |   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; |   let nTotalImprovements = nImprovements + 5; | ||||||
|   for (let i = 0; i < nTotalImprovements; i++) { |   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(); |     improvement(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   let skills: Skill[] = []; |   let skills: Skill[] = []; | ||||||
|   let inPenance = false; |   let inPenance = false; | ||||||
|   let isCompulsory = 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 { | import { | ||||||
|   bat0, |   bat0, | ||||||
|   bat1, |   bat1, | ||||||
| @@ -11,7 +11,7 @@ import { | |||||||
|   stare0, |   stare0, | ||||||
|   stare1, |   stare1, | ||||||
|   stealth0, |   stealth0, | ||||||
|   stealth1 |   stealth1, | ||||||
| } from "./skills.ts"; | } from "./skills.ts"; | ||||||
| import { | import { | ||||||
|   sprThrallBat, |   sprThrallBat, | ||||||
| @@ -19,16 +19,16 @@ import { | |||||||
|   sprThrallLore, |   sprThrallLore, | ||||||
|   sprThrallParty, |   sprThrallParty, | ||||||
|   sprThrallStare, |   sprThrallStare, | ||||||
|   sprThrallStealth |   sprThrallStealth, | ||||||
| } from "./sprites.ts"; | } from "./sprites.ts"; | ||||||
| import {Sprite} from "./engine/internal/sprite.ts"; | import { Sprite } from "./engine/internal/sprite.ts"; | ||||||
|  |  | ||||||
| export type Thrall = { | export type Thrall = { | ||||||
|   id: number |   id: number; | ||||||
| } | }; | ||||||
|  |  | ||||||
| class ThrallsTable { | class ThrallsTable { | ||||||
|   #thralls: ThrallData[] |   #thralls: ThrallData[]; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     this.#thralls = []; |     this.#thralls = []; | ||||||
| @@ -37,29 +37,29 @@ class ThrallsTable { | |||||||
|   add(data: ThrallData) { |   add(data: ThrallData) { | ||||||
|     let id = this.#thralls.length; |     let id = this.#thralls.length; | ||||||
|     this.#thralls.push(data); |     this.#thralls.push(data); | ||||||
|     return {id}; |     return { id }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get(thrall: Thrall): ThrallData { |   get(thrall: Thrall): ThrallData { | ||||||
|     return this.#thralls[thrall.id] |     return this.#thralls[thrall.id]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getAll(): Thrall[] { |   getAll(): Thrall[] { | ||||||
|     let thralls = []; |     let thralls = []; | ||||||
|     for (let id = 0; id < this.#thralls.length; id++) { |     for (let id = 0; id < this.#thralls.length; id++) { | ||||||
|       thralls.push({id}) |       thralls.push({ id }); | ||||||
|     } |     } | ||||||
|     return thralls; |     return thralls; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| export type ThrallData = { | export type ThrallData = { | ||||||
|   label: string, |   label: string; | ||||||
|   sprite: Sprite, |   sprite: Sprite; | ||||||
|   posterCheck: CheckData, |   posterCheck: CheckData; | ||||||
|   initialCheck: CheckData, |   initialCheck: CheckData; | ||||||
|  |  | ||||||
|   lifeStageText: Record<LifeStage, LifeStageText> |   lifeStageText: Record<LifeStage, LifeStageText>; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export enum LifeStage { | export enum LifeStage { | ||||||
|   Fresh = "fresh", |   Fresh = "fresh", | ||||||
| @@ -70,9 +70,9 @@ export enum LifeStage { | |||||||
| } | } | ||||||
|  |  | ||||||
| export type LifeStageText = { | export type LifeStageText = { | ||||||
|   prebite: string, |   prebite: string; | ||||||
|   postbite: string, |   postbite: string; | ||||||
| } | }; | ||||||
|  |  | ||||||
| let table = new ThrallsTable(); | let table = new ThrallsTable(); | ||||||
|  |  | ||||||
| @@ -88,27 +88,30 @@ export let thrallParty = table.add({ | |||||||
|   label: "Garrett", |   label: "Garrett", | ||||||
|   sprite: sprThrallParty, |   sprite: sprThrallParty, | ||||||
|   posterCheck: { |   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: [], |     options: [], | ||||||
|   }, |   }, | ||||||
|   initialCheck: { |   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: [ |     options: [ | ||||||
|       { |       { | ||||||
|         skill: () => stealth1,  // Disguise |         skill: () => stealth1, // Disguise | ||||||
|         locked: "\"What's wrong, Garrett?\"", |         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.", |         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*", |         unlockable: "*look like a large pile of money*", | ||||||
|         success: "He scoops you eagerly into his wallet.", |         success: "He scoops you eagerly into his wallet.", | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         skill: () => lore0,  // Respect Elders |         skill: () => lore0, // Respect Elders | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "TODO", |         unlockable: "TODO", | ||||||
|         success: "TODO", |         success: "TODO", | ||||||
|       }, |       }, | ||||||
|     ] |     ], | ||||||
|   }, |   }, | ||||||
|   lifeStageText: { |   lifeStageText: { | ||||||
|     fresh: { |     fresh: { | ||||||
| @@ -116,49 +119,61 @@ export let thrallParty = table.add({ | |||||||
|       postbite: "You plunge your fangs into his feathered neck and feed.", |       postbite: "You plunge your fangs into his feathered neck and feed.", | ||||||
|     }, |     }, | ||||||
|     average: { |     average: { | ||||||
|       prebite: "Garrett looks a little less fresh than last time. He's resigned to the fate of being bitten.", |       prebite: | ||||||
|       postbite: "You puncture him in almost the same place as before and take a moderate amount of blood from his veins." |         "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: { |     poor: { | ||||||
|       prebite: "Garrett, limp in bed, doesn't look like he's doing so well. He's pale and he's breathing heavily.", |       prebite: | ||||||
|       postbite: "\"Please...\" you hear him moan as you force him into the state of ecstasy that brings compliance.", |         "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: { |     vampirized: { | ||||||
|       prebite: "Garrett looks about as cold and pale as you. Another bite may kill him.", |       prebite: | ||||||
|       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.", |         "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: { |     dead: { | ||||||
|       prebite: "This bird is dead, on account of the fact that you killed him with your teeth.", |       prebite: | ||||||
|       postbite: "The blood in his veins hasn't coagulated yet. There's still more. Still more...", |         "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({ | export let thrallLore = table.add({ | ||||||
|   label: "Lupin", |   label: "Lupin", | ||||||
|   sprite: sprThrallLore, |   sprite: sprThrallLore, | ||||||
|   posterCheck: { |   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: [], |     options: [], | ||||||
|   }, |   }, | ||||||
|   initialCheck: { |   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: [ |     options: [ | ||||||
|       { |       { | ||||||
|         skill: () => stare1, // Hypnotize |         skill: () => stare1, // Hypnotize | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "\"I'm a wolf too.\"", |         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.", |         success: | ||||||
|  |           "He blinks a few times under your gaze -- then touches your muzzle -- then his own -- then arfs submissively.", | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         skill: () => bat0, // Screech |         skill: () => bat0, // Screech | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "\"Wolf Scouts AWOO!\"", |         unlockable: '"Wolf Scouts AWOO!"', | ||||||
|         success: "Taken aback at how well you know the cheer, he freezes -- then joins you with a similar howl.", |         success: | ||||||
|  |           "Taken aback at how well you know the cheer, he freezes -- then joins you with a similar howl.", | ||||||
|       }, |       }, | ||||||
|     ] |     ], | ||||||
|   }, |   }, | ||||||
|   lifeStageText: { |   lifeStageText: { | ||||||
|     fresh: { |     fresh: { | ||||||
| @@ -166,23 +181,29 @@ export let thrallLore = table.add({ | |||||||
|       postbite: "You bite the raccoon and drink his blood.", |       postbite: "You bite the raccoon and drink his blood.", | ||||||
|     }, |     }, | ||||||
|     average: { |     average: { | ||||||
|       prebite: "The color in Lupin's cheeks is beginning to fade. He's becoming accustomed to your bite.", |       prebite: | ||||||
|       postbite: "He'll let you do anything to him if you make him feel good, so you make him feel good. Fresh blood...", |         "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: { |     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.", |       postbite: "This is no concern to you. You're hungry. You need this.", | ||||||
|     }, |     }, | ||||||
|     vampirized: { |     vampirized: { | ||||||
|       prebite: "Lupin's fangs have erupted partially from his jaw. You've taken enough. More will kill him.", |       prebite: | ||||||
|       postbite: "His life is less valuable to you than his warm, delicious blood. You need sustenance.", |         "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: { |     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.", |       postbite: "You root around in his neck. His decaying muscle is soft.", | ||||||
|     } |     }, | ||||||
|   }, |   }, | ||||||
| }) | }); | ||||||
|  |  | ||||||
| export let thrallBat = table.add({ | export let thrallBat = table.add({ | ||||||
|   label: "Monica", |   label: "Monica", | ||||||
| @@ -192,23 +213,26 @@ export let thrallBat = table.add({ | |||||||
|     options: [], |     options: [], | ||||||
|   }, |   }, | ||||||
|   initialCheck: { |   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: [ |     options: [ | ||||||
|       { |       { | ||||||
|         skill: () => party1, // Rave |         skill: () => party1, // Rave | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "Slide her a sachet of cocaine.", |         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 |         skill: () => charm0, // Flatter | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "\"You're the best cook ever!\"", |         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.\"", |         success: | ||||||
|  |           '"Settle down!" she says, lowering your volume with a sweep of her hand. "It\'s true though."', | ||||||
|       }, |       }, | ||||||
|     ] |     ], | ||||||
|   }, |   }, | ||||||
|   lifeStageText: { |   lifeStageText: { | ||||||
|     fresh: { |     fresh: { | ||||||
| @@ -216,73 +240,89 @@ export let thrallBat = table.add({ | |||||||
|       postbite: "You dig your teeth into the koala's mortal flesh.", |       postbite: "You dig your teeth into the koala's mortal flesh.", | ||||||
|     }, |     }, | ||||||
|     average: { |     average: { | ||||||
|       prebite: "Monica doesn't look as fresh and vibrant as you recall from her TV show.", |       prebite: | ||||||
|       postbite: "A little bite seems to improve her mood, even though she twitches involuntarily as if you're hurting her.", |         "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: { |     poor: { | ||||||
|       prebite: "Monica weakly raises a hand as if to stop you from approaching for a bite.", |       prebite: | ||||||
|       postbite: "You press yourself to her body and embrace her. Her fingers curl around you and she lets you drink your fill.", |         "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: { |     vampirized: { | ||||||
|       prebite: "Monica shows no interest in food. She's lethargic, apathetic. A bite would kill her, but you're thirsty.", |       prebite: | ||||||
|       postbite: "Her last words are too quiet to make out, but you're not interested in them. Nothing matters except blood.", |         "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: { |     dead: { | ||||||
|       prebite: "This used to be Monica. Now it's just her corpse.", |       prebite: "This used to be Monica. Now it's just her corpse.", | ||||||
|       postbite: "She's very delicate, even as a corpse.", |       postbite: "She's very delicate, even as a corpse.", | ||||||
|     } |     }, | ||||||
|   }, |   }, | ||||||
| }) | }); | ||||||
|  |  | ||||||
| export let thrallCharm = table.add({ | export let thrallCharm = table.add({ | ||||||
|   label: "Renfield", |   label: "Renfield", | ||||||
|   sprite: sprThrallCharm, |   sprite: sprThrallCharm, | ||||||
|   posterCheck: { |   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: [], |     options: [], | ||||||
|   }, |   }, | ||||||
|   initialCheck: { |   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: [ |     options: [ | ||||||
|       { |       { | ||||||
|         skill: () => lore1,  // Brick by Brick |         skill: () => lore1, // Brick by Brick | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "\"Wanna see my crypt?\"", |         unlockable: '"Wanna see my crypt?"', | ||||||
|         success: "He salivates -- swallowing hard before he manages, in response to the prospect, a firm \"YES!\"", |         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", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "Say absolutely nothing.", |         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: { |   lifeStageText: { | ||||||
|     fresh: { |     fresh: { | ||||||
|       prebite: "Renfield exposes the underside of his jaw.", |       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: { |     average: { | ||||||
|       prebite: "Renfield seems relieved to be free of all that extra blood.", |       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: { |     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.", |       prebite: | ||||||
|       postbite: "Does it matter that he doesn't want your bite? You're hungry. He should have known you would do this.", |         "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: { |     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.", |       postbite: "Better to free him if he's going to behave like this anyways.", | ||||||
|     }, |     }, | ||||||
|     dead: { |     dead: { | ||||||
|       prebite: "Here lies a crocodile who really, really liked vampires.", |       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({ | export let thrallStealth = table.add({ | ||||||
|   label: "Narthyss", |   label: "Narthyss", | ||||||
| @@ -292,47 +332,54 @@ export let thrallStealth = table.add({ | |||||||
|     options: [], |     options: [], | ||||||
|   }, |   }, | ||||||
|   initialCheck: { |   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: [ |     options: [ | ||||||
|       { |       { | ||||||
|         skill: () => bat1,  // Flap |         skill: () => bat1, // Flap | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "Hang upside-down and offer her a martini.", |         unlockable: "Hang upside-down and offer her a martini.", | ||||||
|         success: "\"You're ADORABLE!\" She's yours forever.", |         success: "\"You're ADORABLE!\" She's yours forever.", | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         skill: () => stare0,  // Dazzle |         skill: () => stare0, // Dazzle | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "TODO", |         unlockable: "TODO", | ||||||
|         success: "TODO", |         success: "TODO", | ||||||
|       }, |       }, | ||||||
|     ] |     ], | ||||||
|   }, |   }, | ||||||
|   lifeStageText: { |   lifeStageText: { | ||||||
|     fresh: { |     fresh: { | ||||||
|       prebite: "Narthyss is producing a new track on her gamer PC.", |       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: { |     average: { | ||||||
|       prebite: "Narthyss has no desire to be interrupted, but you're thirsty.", |       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: { |     poor: { | ||||||
|       prebite: "Narthyss knows better than to resist you -- but you sense that you've taken more than she wants.", |       prebite: | ||||||
|       postbite: "Her response to your approach is automatic. No matter what she tells you, you show fang -- she shows neck.", |         "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: { |     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.", |       postbite: "Now she is a creature of nothing at all.", | ||||||
|     }, |     }, | ||||||
|     dead: { |     dead: { | ||||||
|       prebite: "Narthyss used to be a dragon. Now she's 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({ | export let thrallStare = table.add({ | ||||||
|   label: "Ridley", |   label: "Ridley", | ||||||
| @@ -342,44 +389,50 @@ export let thrallStare = table.add({ | |||||||
|     options: [], |     options: [], | ||||||
|   }, |   }, | ||||||
|   initialCheck: { |   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: [ |     options: [ | ||||||
|       { |       { | ||||||
|         skill: () => charm1,  // Befriend |         skill: () => charm1, // Befriend | ||||||
|         locked: "\"How many Rs in 'strawberry'?\"", |         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", |         unlockable: "TODO", | ||||||
|         success: "TODO", |         success: "TODO", | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         skill: () => party0,  // Chug |         skill: () => party0, // Chug | ||||||
|         locked: "TODO", |         locked: "TODO", | ||||||
|         failure: "TODO", |         failure: "TODO", | ||||||
|         unlockable: "Drink a whole bottle of ink.", |         unlockable: "Drink a whole bottle of ink.", | ||||||
|         success: "TODO", |         success: "TODO", | ||||||
|       }, |       }, | ||||||
|     ] |     ], | ||||||
|   }, |   }, | ||||||
|   lifeStageText: { |   lifeStageText: { | ||||||
|     fresh: { |     fresh: { | ||||||
|       prebite: "Ridley is solving math problems.", |       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: { |     average: { | ||||||
|       prebite: "Ridley's display brightens at your presence. It looks damaged.", |       prebite: "Ridley's display brightens at your presence. It looks damaged.", | ||||||
|       postbite: "Damaged or not -- the robot has blood and you need it badly.", |       postbite: "Damaged or not -- the robot has blood and you need it badly.", | ||||||
|     }, |     }, | ||||||
|     poor: { |     poor: { | ||||||
|       prebite: "The symbols on Ridley's screen have less and less rational connection. It's begging to be fed upon.", |       prebite: | ||||||
|       postbite: "The quality of the robot's blood decreases with every bite, but the taste is still pleasurable." |         "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: { |     vampirized: { | ||||||
|       prebite: "With no concern for its survival, the now-fanged robot begs you for one more bite. This would kill it.", |       prebite: | ||||||
|       postbite: "Nothing is stronger than your need for blood -- and its desperation has put you in quite a state...", |         "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: { |     dead: { | ||||||
|       prebite: "Ridley was a robot and now Ridley is a dead robot.", |       prebite: "Ridley was a robot and now Ridley is a dead robot.", | ||||||
|       postbite: "Tastes zappy.", |       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) { |   if (array.length == 0) { | ||||||
|     throw new Error(`array cannot have length 0 for choose`); |     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>) { | 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); |     let randomIndex = Math.floor(Math.random() * currentIndex); | ||||||
|     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 { | import { | ||||||
|   bat0, |   bat0, | ||||||
|   bat1, |   bat1, | ||||||
|   bat2, |   bat2, | ||||||
|   charm0, charm1, |   charm0, | ||||||
|  |   charm1, | ||||||
|   charm2, |   charm2, | ||||||
|   lore0, |   lore0, | ||||||
|   lore1, |   lore1, | ||||||
| @@ -16,219 +17,299 @@ import { | |||||||
|   stare2, |   stare2, | ||||||
|   stealth0, |   stealth0, | ||||||
|   stealth1, |   stealth1, | ||||||
|   stealth2 |   stealth2, | ||||||
| } from "./skills.ts"; | } from "./skills.ts"; | ||||||
| import {CheckData} from "./newmap.ts"; | import { CheckData } from "./newmap.ts"; | ||||||
| import {Thrall, thrallBat, thrallCharm, thrallLore, thrallParty, thrallStare, thrallStealth} from "./thralls.ts"; | import { | ||||||
|  |   Thrall, | ||||||
|  |   thrallBat, | ||||||
|  |   thrallCharm, | ||||||
|  |   thrallLore, | ||||||
|  |   thrallParty, | ||||||
|  |   thrallStare, | ||||||
|  |   thrallStealth, | ||||||
|  | } from "./thralls.ts"; | ||||||
|  |  | ||||||
| export type VaultTemplate = { | export type VaultTemplate = { | ||||||
|   stats: {primary: Stat, secondary: Stat}, |   stats: { primary: Stat; secondary: Stat }; | ||||||
|   thrall: () => Thrall, |   thrall: () => Thrall; | ||||||
|   checks: [CheckData, CheckData] |   checks: [CheckData, CheckData]; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export const standardVaultTemplates: VaultTemplate[] = [ | export const standardVaultTemplates: VaultTemplate[] = [ | ||||||
|   { |   { | ||||||
|     // zoo |     // zoo | ||||||
|     stats: {primary: "AGI", secondary: "PSI"}, |     stats: { primary: "AGI", secondary: "PSI" }, | ||||||
|     thrall: () => thrallParty, |     thrall: () => thrallParty, | ||||||
|     checks: [ |     checks: [ | ||||||
|       { |       { | ||||||
|         label: "You're blocked from further access by a sturdy-looking brick wall. Playful bats swoop close to the alligators behind the bars.", |         label: | ||||||
|         options: [{ |           "You're blocked from further access by a sturdy-looking brick wall. Playful bats swoop close to the alligators behind the bars.", | ||||||
|           skill: () => lore1, |         options: [ | ||||||
|           locked: "Looks sturdy.", |           { | ||||||
|           failure: "This wall is completely impenetrable. How could one vampire hope to find a vulnerability here?", |             skill: () => lore1, | ||||||
|           unlockable: "Find a weakness.", |             locked: "Looks sturdy.", | ||||||
|           success: "You grope along the masonry -- experiencing no love for the soullessness of this mortal masonry -- and find an invisible crack between bricks.", |             failure: | ||||||
|         }, { |               "This wall is completely impenetrable. How could one vampire hope to find a vulnerability here?", | ||||||
|           skill: () => stare0, |             unlockable: "Find a weakness.", | ||||||
|           locked: "Admire the bats.", |             success: | ||||||
|           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.", |               "You grope along the masonry -- experiencing no love for the soullessness of this mortal masonry -- and find an invisible crack between bricks.", | ||||||
|           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.", |           { | ||||||
|         }], |             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.", |         label: | ||||||
|         options: [{ |           "There's no person-sized route to the backroom -- only a tiny bat-sized opening.", | ||||||
|           skill: () => bat2, |         options: [ | ||||||
|           locked: "So small!", |           { | ||||||
|           failure: "You put your eye to the opening, but there's nothing to be done. You're just not small enough.", |             skill: () => bat2, | ||||||
|           unlockable: "Crawl in.", |             locked: "So small!", | ||||||
|           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." |             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 |     // blood bank | ||||||
|     stats: {primary: "AGI", secondary: "INT"}, |     stats: { primary: "AGI", secondary: "INT" }, | ||||||
|     thrall: () => thrallLore, |     thrall: () => thrallLore, | ||||||
|     checks: [ |     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: [ |         options: [ | ||||||
|           { |           { | ||||||
|             skill: () => stare1, |             skill: () => stare1, | ||||||
|             locked: "Stare at the blood.", |             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.", |             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, |             skill: () => lore0, | ||||||
|             locked: "Pace awkwardly.", |             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.", |             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.", |         label: "There's a security camera watching the blood.", | ||||||
|         options: [{ |         options: [ | ||||||
|           skill: () => stealth2, |           { | ||||||
|           locked: "Shout at the blood.", |             skill: () => stealth2, | ||||||
|           failure: "\"BLOOD!!! BLOOD!!!! I want you.\"\n\nIt urbles bloodishly.", |             locked: "Shout at the blood.", | ||||||
|           unlockable: "Sneak past.", |             failure: | ||||||
|           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." |               '"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 |     // coffee shop | ||||||
|     stats: {primary: "PSI", secondary: "CHA"}, |     stats: { primary: "PSI", secondary: "CHA" }, | ||||||
|     thrall: () => thrallBat, |     thrall: () => thrallBat, | ||||||
|     checks: [ |     checks: [ | ||||||
|       { |       { | ||||||
|         label: "You don't actually drink coffee, so you probably wouldn't fit in inside.", |         label: | ||||||
|         options: [{ |           "You don't actually drink coffee, so you probably wouldn't fit in inside.", | ||||||
|           skill: () => stealth1, |         options: [ | ||||||
|           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.", |             skill: () => stealth1, | ||||||
|           unlockable: "Sip zealously.", |             locked: "Try to drink it anyways.", | ||||||
|           success: "You snake your tongue under the surface of the fluid and fill your tongue, just like a mortal would. The mortals are impressed." |             failure: | ||||||
|         }, { |               "You dip your teeth into the mug and feel them shrink involuntarily into your gums at the exposure. Everyone is looking at you.", | ||||||
|           skill: () => bat0, |             unlockable: "Sip zealously.", | ||||||
|           locked: "Throat feels dry.", |             success: | ||||||
|           failure: "You attempt to turn the coffee away, but the croak of your disgusted response is unheard and the barista fills your cup.", |               "You snake your tongue under the surface of the fluid and fill your tongue, just like a mortal would. The mortals are impressed.", | ||||||
|           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.", |           { | ||||||
|         }], |             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?", |         label: | ||||||
|         options: [{ |           "There's a little studio back here for getting photos -- you weren't thinking about getting your photo taken, were you?", | ||||||
|           skill: () => charm2, |         options: [ | ||||||
|           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.", |             skill: () => charm2, | ||||||
|           unlockable: "Be dazzling.", |             locked: "Say 'cheese'.", | ||||||
|           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." |             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 |     // optometrist | ||||||
|     stats: {primary: "PSI", secondary: "PSI"}, |     stats: { primary: "PSI", secondary: "PSI" }, | ||||||
|     thrall: () => thrallCharm, |     thrall: () => thrallCharm, | ||||||
|     checks: [ |     checks: [ | ||||||
|       { |       { | ||||||
|         label: "The glasses person doesn't have time for you unless you have a prescription that needs filling.", |         label: | ||||||
|         options: [{ |           "The glasses person doesn't have time for you unless you have a prescription that needs filling.", | ||||||
|           skill: () => charm1, |         options: [ | ||||||
|           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.", |             skill: () => charm1, | ||||||
|           unlockable: "Glasses are your life's passion.", |             locked: '"_Something_ needs filling."', | ||||||
|           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." |             failure: | ||||||
|         }, { |               "You sexually harass him for a while, and then he replies with some very hurtful things I don't dare transcribe.", | ||||||
|           skill: () => party0, |             unlockable: "Glasses are your life's passion.", | ||||||
|           locked: "Squint at his possessions.", |             success: | ||||||
|           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.", |               '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.', | ||||||
|           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.", |           { | ||||||
|         }], |             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.", |         label: | ||||||
|         options: [{ |           "The intimidating, massive Eyeball Machine is not going to dispense a prescription for a vampire. It is far too smart for you.", | ||||||
|           skill: () => stare2, |         options: [ | ||||||
|           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.", |             skill: () => stare2, | ||||||
|           unlockable: "A worthy opponent.", |             locked: "Try it anyways.", | ||||||
|           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." |             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, |     // club, | ||||||
|     stats: {primary: "CHA", secondary: "PSI"}, |     stats: { primary: "CHA", secondary: "PSI" }, | ||||||
|     thrall: () => thrallStealth, |     thrall: () => thrallStealth, | ||||||
|     checks: [ |     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.", |         label: | ||||||
|         options: [{ |           "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.", | ||||||
|           skill: () => bat1, |         options: [ | ||||||
|           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.", |             skill: () => bat1, | ||||||
|           unlockable: "Demonstrate a new dance.", |             locked: "So awkward!", | ||||||
|           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." |             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.", | ||||||
|           skill: () => stealth0, |             unlockable: "Demonstrate a new dance.", | ||||||
|           locked: "Try to seem big.", |             success: | ||||||
|           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.", |               "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.", | ||||||
|           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." |           { | ||||||
|         }], |             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.", |         label: | ||||||
|         options: [{ |           "This illegal poker game consists of individuals as different from ordinary people as you are. This guy is dropping _zero_ tells.", | ||||||
|           skill: () => party2, |         options: [ | ||||||
|           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.", |             skill: () => party2, | ||||||
|           unlockable: "Make up an insulting nickname.", |             locked: "Lose money.", | ||||||
|           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." |             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 |     // library | ||||||
|     stats: {primary: "INT", secondary: "CHA"}, |     stats: { primary: "INT", secondary: "CHA" }, | ||||||
|     thrall: () => thrallStare, |     thrall: () => thrallStare, | ||||||
|     checks: [ |     checks: [ | ||||||
|       { |       { | ||||||
|         label: "Special Collections. This guy is not just a librarian -- he's a vampire, too -- which he makes no effort to hide.", |         label: | ||||||
|         options: [{ |           "Special Collections. This guy is not just a librarian -- he's a vampire, too -- which he makes no effort to hide.", | ||||||
|           skill: () => party1, |         options: [ | ||||||
|           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.", |             skill: () => party1, | ||||||
|           unlockable: "Be super loud.", |             locked: "Quietly do nothing.", | ||||||
|           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." |             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.", | ||||||
|           skill: () => charm0, |             unlockable: "Be super loud.", | ||||||
|           locked: "Gawk at him.", |             success: | ||||||
|           failure: "He's so cool. Every day you remember you're a vampire and vampires are so, so, cool.", |               '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.', | ||||||
|           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." |           { | ||||||
|         }], |             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.", |         label: | ||||||
|         options: [{ |           "The librarian took a big risk letting you in back here. He's obviously deciding whether or not he's made a mistake.", | ||||||
|           skill: () => lore2, |         options: [ | ||||||
|           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.", |             skill: () => lore2, | ||||||
|           unlockable: "Prove you read something.", |             locked: "Look at the books.", | ||||||
|           success: "\"Fruit bats,\" you say. \"From the story. They're not actually bats, they're --\"\n\"Metaphorical,\" he agrees. \"But for what?\"", |             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 { D, I } from "./engine/public.ts"; | ||||||
| import {AlignX, AlignY, Point} from "./engine/datatypes.ts"; | import { AlignX, AlignY, Point } from "./engine/datatypes.ts"; | ||||||
| import {FG_BOLD} from "./colors.ts"; | import { FG_BOLD } from "./colors.ts"; | ||||||
| import {withCamera} from "./layout.ts"; | import { withCamera } from "./layout.ts"; | ||||||
| import {VNScene, VNSceneMessage, VNScenePart} from "./vnscene.ts"; | import { VNScene, VNSceneMessage, VNScenePart } from "./vnscene.ts"; | ||||||
|  |  | ||||||
| const WIDTH = 384; | const WIDTH = 384; | ||||||
| const HEIGHT = 384; | const HEIGHT = 384; | ||||||
| @@ -27,7 +27,7 @@ export class VNModal { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   play(scene: VNScene) { |   play(scene: VNScene) { | ||||||
|     this.#scene = scene |     this.#scene = scene; | ||||||
|     this.#nextIndex = 0; |     this.#nextIndex = 0; | ||||||
|     this.#cathexis = null; |     this.#cathexis = null; | ||||||
|  |  | ||||||
| @@ -47,9 +47,9 @@ export class VNModal { | |||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       if (this.#cathexis == null) { |       if (this.#cathexis == null) { | ||||||
|         let ix = this.#nextIndex |         let ix = this.#nextIndex; | ||||||
|         if (ix < this.#scene?.length) { |         if (ix < this.#scene?.length) { | ||||||
|           this.#cathexis = createCathexis(this.#scene[ix]) |           this.#cathexis = createCathexis(this.#scene[ix]); | ||||||
|           this.#nextIndex += 1; |           this.#nextIndex += 1; | ||||||
|         } else { |         } else { | ||||||
|           this.#scene = null; |           this.#scene = null; | ||||||
| @@ -59,12 +59,12 @@ export class VNModal { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   update() { |   update() { | ||||||
|     this.#fixCathexis() |     this.#fixCathexis(); | ||||||
|     withCamera("FullscreenPopover", () => this.#update()) |     withCamera("FullscreenPopover", () => this.#update()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   draw() { | ||||||
|     withCamera("FullscreenPopover", () => this.#draw()) |     withCamera("FullscreenPopover", () => this.#draw()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   #update() { |   #update() { | ||||||
| @@ -85,9 +85,8 @@ interface SceneCathexis { | |||||||
| function createCathexis(part: VNScenePart): SceneCathexis { | function createCathexis(part: VNScenePart): SceneCathexis { | ||||||
|   switch (part.type) { |   switch (part.type) { | ||||||
|     case "message": |     case "message": | ||||||
|       return new SceneMessageCathexis(part) |       return new SceneMessageCathexis(part); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| class SceneMessageCathexis { | class SceneMessageCathexis { | ||||||
| @@ -95,7 +94,7 @@ class SceneMessageCathexis { | |||||||
|   #done: boolean; |   #done: boolean; | ||||||
|   #gotOneFrame: boolean; |   #gotOneFrame: boolean; | ||||||
|  |  | ||||||
|   constructor (message: VNSceneMessage) { |   constructor(message: VNSceneMessage) { | ||||||
|     this.#message = message; |     this.#message = message; | ||||||
|     this.#done = false; |     this.#done = false; | ||||||
|     this.#gotOneFrame = false; |     this.#gotOneFrame = false; | ||||||
| @@ -116,15 +115,15 @@ class SceneMessageCathexis { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   draw() { |   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, |       alignX: AlignX.Center, | ||||||
|       alignY: AlignY.Middle, |       alignY: AlignY.Middle, | ||||||
|       forceWidth: WIDTH |       forceWidth: WIDTH, | ||||||
|     }) |     }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| let active: VNModal = new VNModal(); | let active: VNModal = new VNModal(); | ||||||
| export function getVNModal() { | export function getVNModal() { | ||||||
|   return active; |   return active; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| export type VNSceneMessage = { | export type VNSceneMessage = { | ||||||
|   type: "message", |   type: "message"; | ||||||
|   text: string, |   text: string; | ||||||
|   sfx?: string, |   sfx?: string; | ||||||
| } | }; | ||||||
|  |  | ||||||
| export type VNSceneBasisPart = string | VNSceneMessage; | export type VNSceneBasisPart = string | VNSceneMessage; | ||||||
| export type VNSceneBasis = VNSceneBasisPart[]; | export type VNSceneBasis = VNSceneBasisPart[]; | ||||||
| @@ -12,11 +12,11 @@ export type VNScene = VNScenePart[]; | |||||||
| export function compile(basis: VNSceneBasis): VNScene { | export function compile(basis: VNSceneBasis): VNScene { | ||||||
|   let out: VNScene = []; |   let out: VNScene = []; | ||||||
|   for (let item of basis.values()) { |   for (let item of basis.values()) { | ||||||
|     if (typeof item == 'string') { |     if (typeof item == "string") { | ||||||
|       out.push({ |       out.push({ | ||||||
|         type: "message", |         type: "message", | ||||||
|         text: item, |         text: item, | ||||||
|       }) |       }); | ||||||
|     } else { |     } else { | ||||||
|       out.push(item); |       out.push(item); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,25 +1,39 @@ | |||||||
| import {Skill, Wish, WishData} from "./datatypes.ts"; | import { Skill, Wish, WishData } from "./datatypes.ts"; | ||||||
| import {shuffle} from "./utils.ts"; | import { shuffle } from "./utils.ts"; | ||||||
| import { | import { | ||||||
|   bat0, bat1, bat2, |   bat0, | ||||||
|  |   bat1, | ||||||
|  |   bat2, | ||||||
|   bat3, |   bat3, | ||||||
|   charm0, |   charm0, | ||||||
|   charm1, |   charm1, | ||||||
|   charm2, |   charm2, | ||||||
|   charm3, getSkills, |   charm3, | ||||||
|   lore0, lore1, lore2, |   getSkills, | ||||||
|  |   lore0, | ||||||
|  |   lore1, | ||||||
|  |   lore2, | ||||||
|   party0, |   party0, | ||||||
|   party1, party2, party3, sorry0, sorry1, sorry2, stare0, stare1, stare2, stare3, |   party1, | ||||||
|  |   party2, | ||||||
|  |   party3, | ||||||
|  |   sorry0, | ||||||
|  |   sorry1, | ||||||
|  |   sorry2, | ||||||
|  |   stare0, | ||||||
|  |   stare1, | ||||||
|  |   stare2, | ||||||
|  |   stare3, | ||||||
|   stealth0, |   stealth0, | ||||||
|   stealth1, |   stealth1, | ||||||
|   stealth2, |   stealth2, | ||||||
|   stealth3 |   stealth3, | ||||||
| } from "./skills.ts"; | } from "./skills.ts"; | ||||||
| import {compile, VNSceneBasisPart} from "./vnscene.ts"; | import { compile, VNSceneBasisPart } from "./vnscene.ts"; | ||||||
| import {getPlayerProgress} from "./playerprogress.ts"; | import { getPlayerProgress } from "./playerprogress.ts"; | ||||||
|  |  | ||||||
| class WishesTable { | class WishesTable { | ||||||
|   #wishes: WishData[] |   #wishes: WishData[]; | ||||||
|  |  | ||||||
|   constructor() { |   constructor() { | ||||||
|     this.#wishes = []; |     this.#wishes = []; | ||||||
| @@ -28,7 +42,7 @@ class WishesTable { | |||||||
|   add(data: WishData): Wish { |   add(data: WishData): Wish { | ||||||
|     let id = this.#wishes.length; |     let id = this.#wishes.length; | ||||||
|     this.#wishes.push(data); |     this.#wishes.push(data); | ||||||
|     return {id}; |     return { id }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get(wish: Wish): WishData { |   get(wish: Wish): WishData { | ||||||
| @@ -39,7 +53,7 @@ class WishesTable { | |||||||
|     let wishes: Wish[] = []; |     let wishes: Wish[] = []; | ||||||
|     for (let i = 0; i < this.#wishes.length; i++) { |     for (let i = 0; i < this.#wishes.length; i++) { | ||||||
|       if (this.#wishes[i].isRandomlyAvailable) { |       if (this.#wishes[i].isRandomlyAvailable) { | ||||||
|         wishes.push({id: i}); |         wishes.push({ id: i }); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return wishes; |     return wishes; | ||||||
| @@ -54,8 +68,8 @@ export function getWishes(): WishesTable { | |||||||
| const whisper: VNSceneBasisPart = { | const whisper: VNSceneBasisPart = { | ||||||
|   type: "message", |   type: "message", | ||||||
|   text: "...", |   text: "...", | ||||||
|   sfx: "whisper.mp3" |   sfx: "whisper.mp3", | ||||||
| } | }; | ||||||
|  |  | ||||||
| export const celebritySocialite = table.add({ | export const celebritySocialite = table.add({ | ||||||
|   profile: { |   profile: { | ||||||
| @@ -95,7 +109,7 @@ export const celebritySocialite = table.add({ | |||||||
|     "I did as you commanded.", |     "I did as you commanded.", | ||||||
|     "You're pleased?", |     "You're pleased?", | ||||||
|     "... I'm free.", |     "... I'm free.", | ||||||
|   ]) |   ]), | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export const nightswornAlchemist = table.add({ | export const nightswornAlchemist = table.add({ | ||||||
| @@ -103,7 +117,8 @@ export const nightswornAlchemist = table.add({ | |||||||
|     name: "Nightsworn Alchemist", |     name: "Nightsworn Alchemist", | ||||||
|     note: "+Lore -Party", |     note: "+Lore -Party", | ||||||
|     domicile: "Alchemical Lab", |     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", |     failureName: "Failure of Science", | ||||||
|     failureDomicile: "Remedial College", |     failureDomicile: "Remedial College", | ||||||
|     failureReignSentence: "You don't understand much of anything.", |     failureReignSentence: "You don't understand much of anything.", | ||||||
| @@ -135,7 +150,7 @@ export const nightswornAlchemist = table.add({ | |||||||
|     "I did as you commanded.", |     "I did as you commanded.", | ||||||
|     "You're pleased?", |     "You're pleased?", | ||||||
|     "... I'm free.", |     "... I'm free.", | ||||||
|   ]) |   ]), | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export const batFreak = table.add({ | export const batFreak = table.add({ | ||||||
| @@ -168,11 +183,7 @@ export const batFreak = table.add({ | |||||||
|     whisper, |     whisper, | ||||||
|     "I -- SKREEEEK -- should have spent more time becoming a bat...", |     "I -- SKREEEEK -- should have spent more time becoming a bat...", | ||||||
|   ]), |   ]), | ||||||
|   onVictory: compile([ |   onVictory: compile([whisper, "SKRSKRSKRSK.", "I'm FREEEEEEEEEE --"]), | ||||||
|     whisper, |  | ||||||
|     "SKRSKRSKRSK.", |  | ||||||
|     "I'm FREEEEEEEEEE --", |  | ||||||
|   ]) |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export const repent = table.add({ | export const repent = table.add({ | ||||||
| @@ -197,20 +208,16 @@ export const repent = table.add({ | |||||||
|     "I'm sorry.", |     "I'm sorry.", | ||||||
|     "Please...", |     "Please...", | ||||||
|     whisper, |     whisper, | ||||||
|     "I must repent." |     "I must repent.", | ||||||
|   ]), |   ]), | ||||||
|   onFailure: compile([ |   onFailure: compile([ | ||||||
|     whisper, |     whisper, | ||||||
|     "I can't --", |     "I can't --", | ||||||
|     "I must --", |     "I must --", | ||||||
|     whisper, |     whisper, | ||||||
|     "Master -- please, no, I --" |     "Master -- please, no, I --", | ||||||
|   ]), |   ]), | ||||||
|   onVictory: compile([ |   onVictory: compile([whisper, "Yes, I see.", "I'm free...?"]), | ||||||
|     whisper, |  | ||||||
|     "Yes, I see.", |  | ||||||
|     "I'm free...?" |  | ||||||
|   ]) |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export function generateWishes(penance: boolean): Wish[] { | 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 { | 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); |   let wishData = getWishes().get(wish); | ||||||
|  |  | ||||||
|   for (let subj of wishData.requiredSkills()) { |   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()) { |   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()) { |   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()) { |   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; |   return 1.0; | ||||||
| @@ -263,4 +280,4 @@ export function isWishCompleted(wish: Wish): boolean { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user