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