Run prettier over everything

This commit is contained in:
Pyrex 2025-02-17 18:38:40 -08:00
parent 462f5ce751
commit 5939384b7c
46 changed files with 2315 additions and 1471 deletions

View File

@ -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,
);
}

View File

@ -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, () => {
});
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}`);
}
}

View File

@ -3,7 +3,7 @@ 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;

View File

@ -4,98 +4,110 @@ 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 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;
}
};

View File

@ -2,11 +2,11 @@ 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;
}
@ -19,7 +19,13 @@ export class DrawPile {
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();
}

View File

@ -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, {
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.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;

View File

@ -3,8 +3,8 @@ 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([

View File

@ -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 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 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 {

View File

@ -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;
}

View File

@ -5,14 +5,13 @@ 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;
}

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
import { getAssets } from "./assets.ts";
import fontSheet from '../../art/fonts/vga_8x16.png';
import fontSheet from "../../art/fonts/vga_8x16.png";
import { AlignX, AlignY, Color, Point, Size } from "../datatypes.ts";
class Font {
@ -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",
);
}

View File

@ -1,4 +1,4 @@
import './style.css'
import "./style.css";
import { pollAndTouch } from "./screen.ts";
import { getClock } from "./clock.ts";
@ -31,4 +31,3 @@ function onFrame(game: IGame, timestamp: number | undefined) {
function onFrameFixScreen(canvas: HTMLCanvasElement) {
pollAndTouch(canvas);
}

View File

@ -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";
@ -73,15 +83,16 @@ 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 {
@ -97,7 +108,7 @@ class Input {
}
get mousePosition(): Point | null {
return this.#mousePosition
return this.#mousePosition;
}
isKeyDown(key: string): boolean {
@ -114,10 +125,14 @@ class Input {
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;
}

View File

@ -3,12 +3,12 @@ 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;
}

View File

@ -1,7 +1,6 @@
import { getAssets } from "./assets.ts";
import { Point, Size } from "../datatypes.ts";
export class Sprite {
readonly imageSet: string;
// spritesheet params
@ -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,
);
}
}

View File

@ -1,4 +1,5 @@
html, body {
html,
body {
padding: 0;
margin: 0;
width: 100%;

View File

@ -15,16 +15,18 @@ class MenuCamera {
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; }
return (x0 * 8 + x1 * 2) / 10;
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();
}
}

View File

@ -7,14 +7,18 @@ export class Gameplay {
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;
}

View File

@ -1,8 +1,8 @@
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,7 +70,8 @@ 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));
@ -69,63 +83,93 @@ export class GridArt {
}
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; }
return x + (y - x) * amt;
if (amt <= 0) {
return x;
}
if (amt >= 1) {
return y;
}
return x + (y - x) * amt;
};

View File

@ -6,9 +6,9 @@ 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;
@ -23,7 +23,7 @@ export class Hotbar {
get size(): Size {
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() {

View File

@ -8,28 +8,32 @@ import {getStateManager} from "./statemanager.ts";
export class Hud {
get size(): Size {
return new Size(96, 176)
return new Size(96, 176);
}
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;
}

View File

@ -7,7 +7,7 @@ import {
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";
@ -15,20 +15,19 @@ 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; }
this.map.get(new Point(x, y)).revealed = true;
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,
() => {
this.drawpile.add(OFFSET_UNDER_FLOOR, () => {
gridArt.drawCeiling(BG_WALL_OR_UNREVEALED);
}
);
});
if (cellData.architecture == Architecture.Wall || !cellData.revealed) {
this.drawpile.add(
OFFSET_TOP,
() => {
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, {
D.drawSprite(sprThrallLore, cellOffset, 1, {
xScale: this.faceLeft ? -2 : 2,
yScale: 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;
}

View File

@ -7,7 +7,7 @@ import {getHotbar} from "./hotbar.ts";
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;
@ -25,39 +25,48 @@ export function getLayoutRect(
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,7 +88,7 @@ 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 {
@ -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}`;
}

View File

@ -2,7 +2,8 @@ import {hostGame} from "./engine/internal/host.ts";
import { game } from "./game.ts";
import { getStateManager } from "./statemanager.ts";
getStateManager().startGame({
getStateManager().startGame(
{
name: "Pyrex",
title: "",
note: null,
@ -11,5 +12,7 @@ getStateManager().startGame({
skills: [],
isCompulsory: false,
inPenance: false,
}, null);
},
null,
);
hostGame(game);

View File

@ -1,7 +1,11 @@
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 {
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;
}
}
}

View File

@ -3,7 +3,13 @@ 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 {
ExperiencePickup,
LadderPickup,
LockPickup,
StatPickup,
ThrallPickup,
} from "./pickups.ts";
import { getPlayerProgress } from "./playerprogress.ts";
const WIDTH = 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);
}
}
}
@ -78,7 +90,7 @@ class Knife {
export function generateMap(): LoadedNewMap {
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,7 +161,7 @@ class RoomChain {
}
this.rooms.push(room);
return room
return room;
}
}
@ -159,13 +172,21 @@ function addRooms(knife: Knife, vaultTemplates: VaultTemplate[]): Rect[] {
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 {}

View File

@ -2,28 +2,66 @@ 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() {

View File

@ -2,35 +2,38 @@ 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;

View File

@ -5,20 +5,25 @@ 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 {
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,9 +32,13 @@ export class LockPickup {
this.check = check;
}
computeCostToClick() { return 0; }
computeCostToClick() {
return 0;
}
isObstructive() { return true; }
isObstructive() {
return true;
}
drawFloor() {}
drawInAir(gridArt: GridArt) {
@ -37,12 +46,12 @@ export class LockPickup {
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() {}
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,
{
D.drawSprite(sprStatPickup, gridArt.project(5), statIndex, {
xScale: 2,
yScale: 2,
}
)
});
}
onClick(): boolean {
@ -82,9 +92,13 @@ export class StatPickup {
}
export class ExperiencePickup {
computeCostToClick() { return 100; }
computeCostToClick() {
return 100;
}
isObstructive() { return true; }
isObstructive() {
return true;
}
drawFloor() {}
drawInAir(gridArt: GridArt) {
@ -95,7 +109,7 @@ export class ExperiencePickup {
{
xScale: 2,
yScale: 2,
}
},
);
}
@ -107,15 +121,19 @@ 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() {}
@ -133,9 +151,13 @@ export class ThrallPickup {
this.thrall = thrall;
}
computeCostToClick() { return 0; }
computeCostToClick() {
return 0;
}
isObstructive() { return false; }
isObstructive() {
return false;
}
drawFloor() {}
drawInAir(gridArt: GridArt) {
@ -143,14 +165,14 @@ export class ThrallPickup {
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,9 +185,13 @@ export class ThrallPosterPickup {
this.thrall = thrall;
}
computeCostToClick() { return 0; }
computeCostToClick() {
return 0;
}
isObstructive() { return false; }
isObstructive() {
return false;
}
drawFloor() {}
drawInAir(gridArt: GridArt) {
@ -173,17 +199,16 @@ export class ThrallPosterPickup {
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,9 +218,13 @@ export class ThrallRecruitedPickup {
this.bitten = false;
}
computeCostToClick() { return 0; }
computeCostToClick() {
return 0;
}
isObstructive() { return false; }
isObstructive() {
return false;
}
drawFloor() {}
drawInAir(gridArt: GridArt) {
@ -204,22 +233,30 @@ export class ThrallRecruitedPickup {
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({
getCheckModal().show(
{
label: `${text.prebite}`,
options: [
{
@ -232,21 +269,27 @@ export class ThrallRecruitedPickup {
isChoice: true,
countsAsSuccess: false,
unlockable: "Refrain",
success: "Maybe next time."
}
]
}, () => {
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
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]));
},
);
getPlayerProgress().damageThrall(this.thrall, choose([0.9]))
});
return true;
}
}

View File

@ -3,18 +3,18 @@ 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;
@ -25,7 +25,7 @@ export class PlayerProgress {
this.#exp = 0;
this.#blood = 0;
this.#itemsPurloined = 0;
this.#skillsLearned = []
this.#skillsLearned = [];
this.#untrimmedSkillsAvailable = [];
this.#thrallsUnlocked = [];
this.#thrallDamage = {};
@ -51,7 +51,9 @@ export class PlayerProgress {
this.#blood = 2000;
let learnableSkills = []; // TODO: Also include costing info
for (let skill of getSkills().getAvailableSkills(this.#isInPenance).values()) {
for (let skill of getSkills()
.getAvailableSkills(this.#isInPenance)
.values()) {
if (this.#canBeAvailable(skill)) {
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; }
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;
}

View File

@ -2,7 +2,14 @@ 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 {
sceneBat,
sceneCharm,
sceneLore,
sceneParty,
sceneStare,
sceneStealth,
} from "./endings.ts";
import { generateWishes, getWishes, isWishCompleted } from "./wishes.ts";
import { generateSuccessors } from "./successors.ts";
@ -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,7 +121,7 @@ class Scorer {
itemsPurloined,
vampiricSkills,
mortalServants,
}
};
let successorOptions = generateSuccessors(0, penance); // TODO: generate nImprovements from mortalServants and the player's bsae improvements
let wishOptions = generateWishes(penance);
@ -126,11 +129,17 @@ class Scorer {
return {
scene,
personal: {rank, domicile, reignSentence, successorVerb, progenerateVerb},
personal: {
rank,
domicile,
reignSentence,
successorVerb,
progenerateVerb,
},
analytics,
successorOptions,
wishOptions,
}
};
}
}

View File

@ -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; }
return !isBlocking(quadrant.transform(xy));
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);
}
};

View File

@ -1,9 +1,15 @@
import {Skill, SkillData, SkillGoverning, SkillScoring, Stat} from "./datatypes.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 = [];
@ -16,14 +22,16 @@ class SkillsTable {
}
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; }
if (isDegrading && !includeDegrading) {
continue;
}
skills.push({ id: i });
}
return skills;
@ -34,7 +42,8 @@ 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) {
@ -42,15 +51,22 @@ class SkillsTable {
}
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(
return Math.floor(
geomInterpolate(
governingStatValue,
underTarget, target,
data.governing.cost, 999
))
underTarget,
target,
data.governing.cost,
999,
),
);
}
}
@ -61,21 +77,32 @@ 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"],
@ -111,21 +138,50 @@ let templates: Record<Track, GoverningTemplate> = {
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;
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;

View File

@ -4,9 +4,7 @@ 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 { getSkills } from "./skills.ts";
import { getPlayerProgress } from "./playerprogress.ts";
import { Skill, SkillData } from "./datatypes.ts";
@ -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();
@ -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;
}

View File

@ -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));

View File

@ -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,
);

View File

@ -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;
}

View File

@ -3,7 +3,10 @@ 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()];
}
@ -39,7 +42,7 @@ export function generateSuccessorFromPlayer(): SuccessorOption {
skills: [...progress.getLearnedSkills()],
inPenance: true,
isCompulsory: true,
}
};
for (let stat of ALL_STATS.values()) {
successor.talents[stat] = -8;
@ -52,25 +55,30 @@ 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();
}

View File

@ -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";
export type Thrall = {
id: number
}
id: number;
};
class ThrallsTable {
#thralls: ThrallData[]
#thralls: ThrallData[];
constructor() {
this.#thralls = [];
@ -41,25 +41,25 @@ class ThrallsTable {
}
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,16 +88,19 @@ 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.",
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.",
},
@ -108,7 +111,7 @@ export let thrallParty = table.add({
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
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
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,7 +332,8 @@ 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
@ -308,31 +349,37 @@ export let thrallStealth = table.add({
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,12 +389,14 @@ 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
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",
},
@ -358,28 +407,32 @@ export let thrallStare = table.add({
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.",
}
},
})
},
});

View File

@ -2,7 +2,7 @@ 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],
];
}
}

View File

@ -3,7 +3,8 @@ import {
bat0,
bat1,
bat2,
charm0, charm1,
charm0,
charm1,
charm2,
lore0,
lore1,
@ -16,18 +17,24 @@ 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 {
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[] = [
{
@ -36,32 +43,45 @@ export const standardVaultTemplates: VaultTemplate[] = [
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: [{
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?",
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.",
}],
success:
"You grope along the masonry -- experiencing no love for the soullessness of this mortal masonry -- and find an invisible crack between bricks.",
},
{
label: "There's no person-sized route to the backroom -- only a tiny bat-sized opening.",
options: [{
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.",
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."
}],
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
@ -69,35 +89,44 @@ export const standardVaultTemplates: VaultTemplate[] = [
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: [{
options: [
{
skill: () => stealth2,
locked: "Shout at the blood.",
failure: "\"BLOOD!!! BLOOD!!!! I want you.\"\n\nIt urbles bloodishly.",
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."
}],
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
@ -105,32 +134,45 @@ export const standardVaultTemplates: VaultTemplate[] = [
thrall: () => thrallBat,
checks: [
{
label: "You don't actually drink coffee, so you probably wouldn't fit in inside.",
options: [{
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.",
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.",
}],
success:
"You snake your tongue under the surface of the fluid and fill your tongue, just like a mortal would. The mortals are impressed.",
},
{
label: "There's a little studio back here for getting photos -- you weren't thinking about getting your photo taken, were you?",
options: [{
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.",
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."
}],
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
@ -138,32 +180,45 @@ export const standardVaultTemplates: VaultTemplate[] = [
thrall: () => thrallCharm,
checks: [
{
label: "The glasses person doesn't have time for you unless you have a prescription that needs filling.",
options: [{
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.",
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.",
}],
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.',
},
{
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: () => 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.",
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."
}],
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,
@ -171,32 +226,45 @@ export const standardVaultTemplates: VaultTemplate[] = [
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: [{
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.",
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."
}],
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.",
},
{
label: "This illegal poker game consists of individuals as different from ordinary people as you are. This guy is dropping _zero_ tells.",
options: [{
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.",
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."
}],
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
@ -204,31 +272,44 @@ export const standardVaultTemplates: VaultTemplate[] = [
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: [{
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.",
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."
}],
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.',
},
{
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: () => 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.",
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?\"",
}],
success:
'"Fruit bats," you say. "From the story. They\'re not actually bats, they\'re --"\n"Metaphorical," he agrees. "But for what?"',
},
]
],
},
]
],
},
];

View File

@ -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 {
@ -119,8 +118,8 @@ class SceneMessageCathexis {
D.drawText(this.#message.text, new Point(WIDTH / 2, HEIGHT / 2), FG_BOLD, {
alignX: AlignX.Center,
alignY: AlignY.Middle,
forceWidth: WIDTH
})
forceWidth: WIDTH,
});
}
}

View File

@ -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);
}

View File

@ -1,25 +1,39 @@
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";
class WishesTable {
#wishes: WishData[]
#wishes: WishData[];
constructor() {
this.#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;