Compare commits

..

14 Commits

Author SHA1 Message Date
3b1c0af916 Merge branch 'main' into fix-mapgen 2025-02-23 05:41:08 +00:00
70c2fcc491
autoformat code 2025-02-22 21:39:45 -08:00
b4aa9329ad
add commented-out cheat and test buttons
looks like mapgen is now fixed. here are the buttons I used to test it
2025-02-22 21:36:55 -08:00
ec2e21c712
check for connectedness in mapgen 2025-02-22 21:11:24 -08:00
0b7d447c5b
map connectedness checker (floodfill) 2025-02-22 21:09:11 -08:00
025b1c9333
fix fencepost error when merging regions 2025-02-22 20:32:35 -08:00
e2aa4a3ee7
also show final result with region numbers 2025-02-22 20:06:31 -08:00
b302538ade
stop using the "dark shade" character for standard walls
now uses inverse bullet for sealed walls and full block otherwise
2025-02-22 20:05:34 -08:00
764d1e4892
handle negative region IDs
also catches some missed semis
2025-02-22 19:59:04 -08:00
4117608073
not all sealed walls are walls. okay 2025-02-22 19:56:33 -08:00
ba151a76fd
more distinct wall chars 2025-02-22 19:55:00 -08:00
898a93d8e5
use detailed debugging in map gen 2025-02-22 19:48:52 -08:00
234a42b1e3
merge state debug dumper 2025-02-22 19:48:00 -08:00
8bf7f0f151
improve errors 2025-02-22 19:47:15 -08:00
68 changed files with 408 additions and 1630 deletions

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>FLEDGLING</title> <title>Prototype 2</title>
</head> </head>
<body> <body>
<canvas id="game" style="cursor: none"></canvas> <canvas id="game" style="cursor: none"></canvas>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 B

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 B

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 B

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 648 B

View File

@ -1,6 +1,12 @@
import { DrawPile } from "./drawpile.ts"; import { DrawPile } from "./drawpile.ts";
import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts";
import { C } from "./colors.ts"; import {
BG_INSET,
FG_BOLD,
FG_TEXT,
FG_TEXT_DISABLED,
FG_TEXT_ENDORSED,
} from "./colors.ts";
import { D } from "./engine/public.ts"; import { D } from "./engine/public.ts";
export function addButton( export function addButton(
@ -25,18 +31,18 @@ export function addButton(
drawpile.addClickable( drawpile.addClickable(
0, 0,
(hover) => { (hover) => {
let [bg, fg, fgLabel] = [C.BG_UI, C.FG_TEXT, C.FG_BOLD]; let [bg, fg, fgLabel] = [BG_INSET, FG_TEXT, FG_BOLD];
if (!enabled) { if (!enabled) {
fgLabel = C.FG_TEXT_DISABLED; fgLabel = FG_TEXT_DISABLED;
} }
if (enabled && options?.endorse) { if (enabled && options?.endorse) {
fg = C.FG_TEXT_ENDORSED; fg = FG_TEXT_ENDORSED;
fgLabel = C.FG_TEXT_ENDORSED; fgLabel = FG_TEXT_ENDORSED;
} }
if (hover) { if (hover) {
[bg, fg, fgLabel] = [C.FG_BOLD, C.BG_UI, C.BG_UI]; [bg, fg, fgLabel] = [FG_BOLD, BG_INSET, BG_INSET];
} }
D.fillRect( D.fillRect(
topLeftPadded.offset(new Point(-1, -1)), topLeftPadded.offset(new Point(-1, -1)),

View File

@ -3,10 +3,10 @@ import { CheckData, CheckDataOption, ChoiceOption } from "./newmap.ts";
import { getPartLocation, withCamera } from "./layout.ts"; import { getPartLocation, withCamera } from "./layout.ts";
import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts";
import { D } from "./engine/public.ts"; import { D } from "./engine/public.ts";
import { BG_INSET, FG_BOLD } from "./colors.ts";
import { addButton } from "./button.ts"; import { addButton } from "./button.ts";
import { getSkills } from "./skills.ts"; import { getSkills } from "./skills.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { C } from "./colors.ts";
export class CheckModal { export class CheckModal {
#drawpile: DrawPile; #drawpile: DrawPile;
@ -54,22 +54,17 @@ export class CheckModal {
let size = this.#size; let size = this.#size;
this.#drawpile.add(0, () => { this.#drawpile.add(0, () => {
D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), C.BG_UI); D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET);
}); });
let success = this.#success; let success = this.#success;
if (success) { if (success) {
this.#drawpile.add(0, () => { this.#drawpile.add(0, () => {
D.drawText( D.drawText(success, new Point(size.w / 2, (size.h - 64) / 2), FG_BOLD, {
success, forceWidth: size.w,
new Point(size.w / 2, (size.h - 64) / 2), alignX: AlignX.Center,
C.FG_BOLD, alignY: AlignY.Middle,
{ });
forceWidth: size.w,
alignX: AlignX.Center,
alignY: AlignY.Middle,
},
);
}); });
addButton( addButton(
this.#drawpile, this.#drawpile,
@ -85,16 +80,11 @@ export class CheckModal {
let labelText = check.label; let labelText = check.label;
this.#drawpile.add(0, () => { this.#drawpile.add(0, () => {
D.drawText( D.drawText(labelText, new Point(size.w / 2, (size.h - 64) / 2), FG_BOLD, {
labelText, forceWidth: size.w,
new Point(size.w / 2, (size.h - 64) / 2), alignX: AlignX.Center,
C.FG_BOLD, alignY: AlignY.Middle,
{ });
forceWidth: size.w,
alignX: AlignX.Center,
alignY: AlignY.Middle,
},
);
}); });
let options = check.options; let options = check.options;

View File

@ -1,206 +1,46 @@
import { Color } from "./engine/datatypes.ts"; import { Color } from "./engine/datatypes.ts";
import { Stat } from "./datatypes.ts"; import { Stat } from "./datatypes.ts";
import { maybeGetHuntMode } from "./huntmode.ts";
import { getEndgameModal } from "./endgamemodal.ts";
import { getVNModal } from "./vnmodal.ts";
export type Microtheme = { export const BG_OUTER = Color.parseHexCode("#143464");
SKY0: Color; // outer, less dark export const BG_WALL_OR_UNREVEALED = Color.parseHexCode("#143464");
FLOOR0: Color; // floor, even less dark export const BG_INSET = Color.parseHexCode("#242234");
export const FG_TEXT = Color.parseHexCode("#c0c0c0");
export const FG_TEXT_DISABLED = Color.parseHexCode("#808080");
export const FG_TOO_EXPENSIVE = Color.parseHexCode("#ff8080");
export const FG_TEXT_ENDORSED = Color.parseHexCode("#80ff80");
export const FG_BOLD = Color.parseHexCode("#ffffff");
export const BG_CEILING = Color.parseHexCode("#143464");
export const FG_MOULDING = FG_TEXT;
WALL0: Color; // darkest (ex. the underside of something) // stat colors
WALL1: Color; // darkest (ex. the underside of something) export const SWATCH_EXP: [Color, Color] = [
Color.parseHexCode("#b9bffb"),
Color.parseHexCode("#e3e6ff"),
];
BG0: Color; // UI background -- should be highly readable and similar to SKY or FLOOR export const SWATCH_AGI: [Color, Color] = [
FG1: Color; // dark (ex. disabled text) Color.parseHexCode("#df3e23"),
FG2: Color; // normal (ex. normal text) Color.parseHexCode("#fa6a0a"),
FG3: Color; // brightest (ex. bold text) ];
export const SWATCH_INT: [Color, Color] = [
Color.parseHexCode("#285cc4"),
Color.parseHexCode("#249fde"),
];
export const SWATCH_CHA: [Color, Color] = [
Color.parseHexCode("#793a80"),
Color.parseHexCode("#bc4a9b"),
];
export const SWATCH_PSI: [Color, Color] = [
Color.parseHexCode("#9cdb43"),
Color.parseHexCode("#d6f264"),
];
export const SWATCH_STAT: Record<Stat, [Color, Color]> = {
AGI: SWATCH_AGI,
INT: SWATCH_INT,
CHA: SWATCH_CHA,
PSI: SWATCH_PSI,
}; };
/*
const MICROTHEME_OLD: MicroTheme = {
BG0: Color.parseHexCode("#242234"),
BG1: Color.parseHexCode("#143464"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
}
*/
export const MICROTHEME_DEFAULT: Microtheme = {
// outdoors
SKY0: Color.parseHexCode("#242234"),
FLOOR0: Color.parseHexCode("#141013"),
WALL0: Color.parseHexCode("#5d758d"),
WALL1: Color.parseHexCode("#8b93af"),
BG0: Color.parseHexCode("#000000"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
};
export const MICROTHEME_BLACK: Microtheme = {
// manor
SKY0: Color.parseHexCode("#141013"),
FLOOR0: Color.parseHexCode("#3b1725"),
WALL0: Color.parseHexCode("#221c1a"),
WALL1: Color.parseHexCode("#322b28"),
BG0: Color.parseHexCode("#000000"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
};
export const MICROTHEME_PURPLE_TAN: Microtheme = {
// library
SKY0: Color.parseHexCode("#403353"),
FLOOR0: Color.parseHexCode("#242234"),
WALL0: Color.parseHexCode("#5a4e44"),
WALL1: Color.parseHexCode("#c7b08b"),
BG0: Color.parseHexCode("#000000"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
};
export const MICROTHEME_GREEN: Microtheme = {
// zoo
SKY0: Color.parseHexCode("#24523b"),
FLOOR0: Color.parseHexCode("#122020"),
WALL0: Color.parseHexCode("#328464"),
WALL1: Color.parseHexCode("#5daf8d"),
BG0: Color.parseHexCode("#000000"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
};
export const MICROTHEME_TEAL: Microtheme = {
// optometrist
SKY0: Color.parseHexCode("#143464"),
FLOOR0: Color.parseHexCode("#122020"),
WALL0: Color.parseHexCode("#477d85"),
WALL1: Color.parseHexCode("#588dbe"),
BG0: Color.parseHexCode("#000000"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
};
export const MICROTHEME_PINK: Microtheme = {
// coffee shop
SKY0: Color.parseHexCode("#793a80"),
FLOOR0: Color.parseHexCode("#221c1a"),
WALL0: Color.parseHexCode("#e86a73"),
WALL1: Color.parseHexCode("#f5a097"),
BG0: Color.parseHexCode("#000000"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
};
export const MICROTHEME_NEON: Microtheme = {
// club
SKY0: Color.parseHexCode("#141013"),
FLOOR0: Color.parseHexCode("#221c1a"),
WALL0: Color.parseHexCode("#9cdb43"),
WALL1: Color.parseHexCode("#d6f264"),
BG0: Color.parseHexCode("#000000"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
};
export const MICROTHEME_RED: Microtheme = {
// blood bank
SKY0: Color.parseHexCode("#73172d"),
FLOOR0: Color.parseHexCode("#141013"),
WALL0: Color.parseHexCode("#df3e23"),
WALL1: Color.parseHexCode("#f9a31b"),
BG0: Color.parseHexCode("#000000"),
FG1: Color.parseHexCode("#8b93af"),
FG2: Color.parseHexCode("#b3b9d1"),
FG3: Color.parseHexCode("#ffffff"),
};
export class ColorSystem {
get BG_UI() {
return this.#microtheme.BG0;
}
get BG_FLOOR() {
return this.#microtheme.FLOOR0;
}
get FG_TEXT() {
return this.#microtheme.FG2;
}
get FG_TEXT_DISABLED() {
return this.#microtheme.FG1;
}
readonly FG_TOO_EXPENSIVE = Color.parseHexCode("#f5a097");
readonly FG_TEXT_ENDORSED = Color.parseHexCode("#d6f264");
get FG_BOLD() {
return this.#microtheme.FG3;
}
get BG_OUTER() {
return this.#microtheme.SKY0;
}
get BG_WALL_OR_UNREVEALED() {
return this.#microtheme.SKY0;
}
get BG_CEILING() {
return this.#microtheme.SKY0;
}
get BG_INNERWALL() {
return this.#microtheme.WALL0;
}
get BG_OUTERWALL() {
return this.#microtheme.WALL1;
}
get #microtheme(): Microtheme {
if (getEndgameModal().isShown || getVNModal().isShown) {
return MICROTHEME_RED;
}
let option = maybeGetHuntMode()?.getActiveMicrotheme();
if (option) {
return option;
}
return MICROTHEME_DEFAULT;
}
// stat colors
readonly SWATCH_EXP: [Color, Color] = [
Color.parseHexCode("#b9bffb"),
Color.parseHexCode("#e3e6ff"),
];
readonly SWATCH_AGI: [Color, Color] = [
Color.parseHexCode("#df3e23"),
Color.parseHexCode("#fa6a0a"),
];
readonly SWATCH_INT: [Color, Color] = [
Color.parseHexCode("#285cc4"),
Color.parseHexCode("#249fde"),
];
readonly SWATCH_CHA: [Color, Color] = [
Color.parseHexCode("#793a80"),
Color.parseHexCode("#bc4a9b"),
];
readonly SWATCH_PSI: [Color, Color] = [
Color.parseHexCode("#9cdb43"),
Color.parseHexCode("#d6f264"),
];
readonly SWATCH_STAT: Record<Stat, [Color, Color]> = {
AGI: this.SWATCH_AGI,
INT: this.SWATCH_INT,
CHA: this.SWATCH_CHA,
PSI: this.SWATCH_PSI,
};
}
export let C = new ColorSystem();

View File

@ -1,5 +1,4 @@
import { VNScene } from "./vnscene.ts"; import { VNScene } from "./vnscene.ts";
import { Thrall } from "./thralls.ts";
export type Stat = "AGI" | "INT" | "CHA" | "PSI"; export type Stat = "AGI" | "INT" | "CHA" | "PSI";
export const ALL_STATS: Array<Stat> = ["AGI", "INT", "CHA", "PSI"]; export const ALL_STATS: Array<Stat> = ["AGI", "INT", "CHA", "PSI"];
@ -105,8 +104,6 @@ export type EndingAnalytics = {
export type SuccessorOption = { export type SuccessorOption = {
name: string; name: string;
title: string; title: string;
template: Thrall;
nImprovements: number;
note: string | null; // ex "already a vampire" note: string | null; // ex "already a vampire"
stats: Record<Stat, number>; stats: Record<Stat, number>;
talents: Record<Stat, number>; talents: Record<Stat, number>;

View File

@ -1,13 +1,12 @@
import { withCamera } from "./layout.ts"; import { withCamera } from "./layout.ts";
import { D } from "./engine/public.ts"; import { D } from "./engine/public.ts";
import { BG_INSET, FG_BOLD, FG_TEXT } from "./colors.ts";
import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts";
import { DrawPile } from "./drawpile.ts"; import { DrawPile } from "./drawpile.ts";
import { addButton } from "./button.ts"; import { addButton } from "./button.ts";
import { ALL_STATS, Ending } from "./datatypes.ts"; import { ALL_STATS, Ending } from "./datatypes.ts";
import { getStateManager } from "./statemanager.ts"; import { getStateManager } from "./statemanager.ts";
import { getWishes } from "./wishes.ts"; import { getWishes } from "./wishes.ts";
import { sndEnding } from "./sounds.ts";
import { C } from "./colors.ts";
const WIDTH = 384; const WIDTH = 384;
const HEIGHT = 384; const HEIGHT = 384;
@ -20,8 +19,6 @@ export class EndgameModal {
#selectedWish: number | null; #selectedWish: number | null;
#ending: Ending | null; #ending: Ending | null;
#playedSound: boolean;
constructor() { constructor() {
this.#drawpile = new DrawPile(); this.#drawpile = new DrawPile();
this.#page = 0; this.#page = 0;
@ -29,7 +26,8 @@ export class EndgameModal {
this.#selectedSuccessor = null; this.#selectedSuccessor = null;
this.#selectedWish = null; this.#selectedWish = null;
this.#ending = null; this.#ending = null;
this.#playedSound = false;
// this.show(getScorer().pickEnding());
} }
get isShown(): boolean { get isShown(): boolean {
@ -41,7 +39,6 @@ export class EndgameModal {
this.#selectedSuccessor = null; this.#selectedSuccessor = null;
this.#selectedWish = null; this.#selectedWish = null;
this.#ending = ending; this.#ending = ending;
this.#playedSound = false;
} }
update() { update() {
@ -69,11 +66,6 @@ export class EndgameModal {
#update() { #update() {
this.#fixCompulsory(); this.#fixCompulsory();
if (!this.#playedSound) {
sndEnding.play({ bgm: true });
this.#playedSound = true;
}
this.#drawpile.clear(); this.#drawpile.clear();
if (this.#page == 0) { if (this.#page == 0) {
let analytics = this.#ending?.analytics; let analytics = this.#ending?.analytics;
@ -87,22 +79,22 @@ export class EndgameModal {
D.drawText( D.drawText(
"It is time to announce the sentence of fate.", "It is time to announce the sentence of fate.",
new Point(0, 0), new Point(0, 0),
C.FG_TEXT, FG_TEXT,
); );
D.drawText( D.drawText(
"You are no longer a fledgling. Your new rank:", "You are no longer a fledgling. Your new rank:",
new Point(0, 32), new Point(0, 32),
C.FG_TEXT, FG_TEXT,
); );
D.drawText(rank, new Point(WIDTH / 2, 64), C.FG_BOLD, { D.drawText(rank, new Point(WIDTH / 2, 64), FG_BOLD, {
alignX: AlignX.Center, alignX: AlignX.Center,
}); });
D.drawText( D.drawText(
"You have achieved a DOMICILE STATUS of:", "You have achieved a DOMICILE STATUS of:",
new Point(0, 96), new Point(0, 96),
C.FG_TEXT, FG_TEXT,
); );
D.drawText(domicile, new Point(WIDTH / 2, 128), C.FG_BOLD, { D.drawText(domicile, new Point(WIDTH / 2, 128), FG_BOLD, {
alignX: AlignX.Center, alignX: AlignX.Center,
}); });
let whereLabel = let whereLabel =
@ -111,8 +103,8 @@ export class EndgameModal {
: mortalServants >= 1 : mortalServants >= 1
? "where you live with a couple of friends." ? "where you live with a couple of friends."
: "where you live without friends."; : "where you live without friends.";
D.drawText(whereLabel, new Point(0, 160), C.FG_TEXT); D.drawText(whereLabel, new Point(0, 160), FG_TEXT);
D.drawText("You have achieved:", new Point(0, 192), C.FG_TEXT); D.drawText("You have achieved:", new Point(0, 192), FG_TEXT);
let itemsPurloinedText = let itemsPurloinedText =
itemsPurloined == 1 ? "item purloined" : "items purloined"; itemsPurloined == 1 ? "item purloined" : "items purloined";
let vampiricSkillsText = let vampiricSkillsText =
@ -129,13 +121,13 @@ export class EndgameModal {
D.drawText( D.drawText(
`${itemsPurloined} ${itemsPurloinedText}\n${vampiricSkills} ${vampiricSkillsText}\n${mortalServants} ${mortalServantsText}`, `${itemsPurloined} ${itemsPurloinedText}\n${vampiricSkills} ${vampiricSkillsText}\n${mortalServants} ${mortalServantsText}`,
new Point(WIDTH / 2, 224), new Point(WIDTH / 2, 224),
C.FG_TEXT, FG_TEXT,
{ alignX: AlignX.Center }, { alignX: AlignX.Center },
); );
D.drawText( D.drawText(
`${itemsPurloined} ${itemsPurloinedSpcr}\n${vampiricSkills} ${vampiricSkillsSpcr}\n${mortalServants} ${mortalServantsSpcr}`, `${itemsPurloined} ${itemsPurloinedSpcr}\n${vampiricSkills} ${vampiricSkillsSpcr}\n${mortalServants} ${mortalServantsSpcr}`,
new Point(WIDTH / 2, 224), new Point(WIDTH / 2, 224),
C.FG_BOLD, FG_BOLD,
{ alignX: AlignX.Center }, { alignX: AlignX.Center },
); );
let msg = "That's pretty dreadful."; let msg = "That's pretty dreadful.";
@ -145,14 +137,14 @@ export class EndgameModal {
if (mortalServants >= 30) { if (mortalServants >= 30) {
msg = "That feels like a lot!"; msg = "That feels like a lot!";
} }
D.drawText(msg, new Point(0, 288), C.FG_TEXT); D.drawText(msg, new Point(0, 288), FG_TEXT);
let reignSentence = let reignSentence =
this.#ending?.personal?.reignSentence ?? this.#ending?.personal?.reignSentence ??
"Your reign is in an unknown state."; "Your reign is in an unknown state.";
D.drawText( D.drawText(
`${reignSentence} It is now time to`, `${reignSentence} It is now time to`,
new Point(0, 320), new Point(0, 320),
C.FG_TEXT, FG_TEXT,
{ forceWidth: WIDTH }, { forceWidth: WIDTH },
); );
}); });
@ -167,7 +159,7 @@ export class EndgameModal {
); );
} else if (this.#page == 1) { } else if (this.#page == 1) {
this.#drawpile.add(0, () => { this.#drawpile.add(0, () => {
D.drawText("Choose your successor:", new Point(0, 0), C.FG_TEXT); D.drawText("Choose your successor:", new Point(0, 0), FG_TEXT);
}); });
this.#addCandidate(0, new Point(0, 16)); this.#addCandidate(0, new Point(0, 16));
@ -182,7 +174,7 @@ export class EndgameModal {
D.drawText( D.drawText(
`Plan their destiny:${optionalNote}`, `Plan their destiny:${optionalNote}`,
new Point(0, 224), new Point(0, 224),
C.FG_TEXT, FG_TEXT,
); );
}); });
@ -291,9 +283,9 @@ export class EndgameModal {
this.#drawpile.addClickable( this.#drawpile.addClickable(
0, 0,
(hover) => { (hover) => {
let [bg, fg, fgBold] = [C.BG_UI, C.FG_TEXT, C.FG_BOLD]; let [bg, fg, fgBold] = [BG_INSET, FG_TEXT, FG_BOLD];
if (hover || selected) { if (hover || selected) {
[bg, fg, fgBold] = [C.FG_BOLD, C.BG_UI, C.BG_UI]; [bg, fg, fgBold] = [FG_BOLD, BG_INSET, BG_INSET];
} }
D.fillRect(at.offset(new Point(0, 4)), new Size(w, h - 8), bg); 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.drawRect(at.offset(new Point(0, 4)), new Size(w, h - 8), fg);
@ -381,9 +373,9 @@ export class EndgameModal {
this.#drawpile.addClickable( this.#drawpile.addClickable(
0, 0,
(hover) => { (hover) => {
let [bg, fg, fgBold] = [C.BG_UI, C.FG_TEXT, C.FG_BOLD]; let [bg, fg, fgBold] = [BG_INSET, FG_TEXT, FG_BOLD];
if (hover || selected) { if (hover || selected) {
[bg, fg, fgBold] = [C.FG_BOLD, C.BG_UI, C.BG_UI]; [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.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.drawRect(at.offset(new Point(2, 4)), new Size(w - 4, h - 8), fg);
@ -401,7 +393,7 @@ export class EndgameModal {
D.drawText( D.drawText(
wishData.profile.note, wishData.profile.note,
at.offset(new Point(w / 2, h)), at.offset(new Point(w / 2, h)),
C.FG_TEXT, FG_TEXT,
{ {
alignX: AlignX.Center, alignX: AlignX.Center,
}, },

View File

@ -1,18 +1,9 @@
import { compile, VNScene, VNSceneBasisPart } from "./vnscene.ts"; import { compile, VNScene, VNSceneBasisPart } from "./vnscene.ts";
import {
sndVnBat,
sndVnBreath,
sndVnDance,
sndVnDoorbell,
sndVnGhost,
sndVnPage,
sndVnPhone,
} from "./sounds.ts";
const squeak: VNSceneBasisPart = { const squeak: VNSceneBasisPart = {
type: "message", type: "message",
text: "...", text: "...",
sfx: sndVnBat, sfx: "squeak.mp3",
}; };
export const sceneBat: VNScene = compile([ export const sceneBat: VNScene = compile([
@ -34,7 +25,7 @@ export const sceneBat: VNScene = compile([
const doorbell: VNSceneBasisPart = { const doorbell: VNSceneBasisPart = {
type: "message", type: "message",
text: "...", text: "...",
sfx: sndVnDoorbell, sfx: "doorbell.mp3",
}; };
export const sceneStealth: VNScene = compile([ export const sceneStealth: VNScene = compile([
@ -42,7 +33,7 @@ export const sceneStealth: VNScene = compile([
"Yeah, you can let yourself in.", "Yeah, you can let yourself in.",
doorbell, doorbell,
"I'll have it moved.", "I'll have it moved.",
"Just -- don't call Liz, OK?", "Just -- don't call Susan, OK?",
doorbell, doorbell,
"Believe me, I'm good for the money.", "Believe me, I'm good for the money.",
"I'm doing... a lot better than it looks like.", "I'm doing... a lot better than it looks like.",
@ -55,7 +46,7 @@ export const sceneStealth: VNScene = compile([
const phoneBeep: VNSceneBasisPart = { const phoneBeep: VNSceneBasisPart = {
type: "message", type: "message",
text: "...", text: "...",
sfx: sndVnPhone, sfx: "phonebeep.mp3",
}; };
export const sceneCharm: VNScene = compile([ export const sceneCharm: VNScene = compile([
@ -70,7 +61,7 @@ export const sceneCharm: VNScene = compile([
"Can you put me through?", "Can you put me through?",
phoneBeep, phoneBeep,
"I really want it.", "I really want it.",
"It's for my boyfriend. My old boyfriend, sorry.", "It's for my boyfriend. First boyfriend, sorry.",
phoneBeep, phoneBeep,
"*chuckle*", "*chuckle*",
"Yeah. I guess I do.", "Yeah. I guess I do.",
@ -81,7 +72,7 @@ export const sceneCharm: VNScene = compile([
const sleepyBreath: VNSceneBasisPart = { const sleepyBreath: VNSceneBasisPart = {
type: "message", type: "message",
text: "...", text: "...",
sfx: sndVnBreath, sfx: "sleepyBreath.mp3",
}; };
export const sceneStare: VNScene = compile([ export const sceneStare: VNScene = compile([
@ -102,7 +93,7 @@ export const sceneStare: VNScene = compile([
const party: VNSceneBasisPart = { const party: VNSceneBasisPart = {
type: "message", type: "message",
text: "...", text: "...",
sfx: sndVnDance, sfx: "party.mp3",
}; };
export const sceneParty: VNScene = compile([ export const sceneParty: VNScene = compile([
@ -120,7 +111,7 @@ export const sceneParty: VNScene = compile([
const ghost: VNSceneBasisPart = { const ghost: VNSceneBasisPart = {
type: "message", type: "message",
text: "...", text: "...",
sfx: sndVnGhost, sfx: "ghost.mp3",
}; };
export const sceneLore: VNScene = compile([ export const sceneLore: VNScene = compile([
@ -136,37 +127,3 @@ export const sceneLore: VNScene = compile([
"Yeah. They remember.", "Yeah. They remember.",
ghost, ghost,
]); ]);
const page: VNSceneBasisPart = {
type: "message",
text: "...",
sfx: sndVnPage,
};
export const sceneTrueEnding: VNScene = compile([
page,
"(This is a taxonomy. It's what nerds write instead of poetry.)",
page,
"INSECTIVORE. INSECTIPHAGE. INSECT-EATER. INSECT-EATING INSECT BAT.",
"CONSUMER OF BUGS.",
"We eat -- flies? They eat beetles.",
"We eat -- various bugs. Yes. And we hang in caves --",
"We deposit the shells in a heap. An absolutely massive heap --",
page,
"FRUCTIVORE. FRUITIPHAGE. FRUIT-EATING FRUIT BAT.",
"We eat -- grapes, melons, that kind of thing.",
"We unearth the heap.",
"We lay the shells in trenches in the furrows of a vineyard.",
page,
"There are two clades and in addition to that is a secret clade.",
page,
"(The pages are stuck together.)",
page,
"HEMOPHAGE. HEMOVORE. HEMATOPHAGE. BLOOD EATER.",
"It is not yet time to announce the sentence of fate.",
"We -- take the wine that grows from the branches.",
"That's a simplification.",
"This is the night deeper than any night that cannot be spoken of.",
page,
"OK -- now roll it.",
]);

View File

@ -144,63 +144,6 @@ export class Size {
} }
} }
export class Circle {
readonly center: Point;
readonly radius: number;
constructor(center: Point, radius: number) {
this.center = center;
this.radius = radius;
}
getContactWithRect(rect: Rect): Point | null {
// port: https://www.jeffreythompson.org/collision-detection/circle-rect.php
let cx = this.center.x;
let cy = this.center.y;
let testX = this.center.x;
let testY = this.center.y;
let rx = rect.top.x;
let ry = rect.top.y;
let rw = rect.size.w;
let rh = rect.size.h;
if (cx < rx) {
testX = rx;
} else if (cx > rx + rw) {
testX = rx + rw;
}
if (cy < ry) {
testY = ry;
} else if (cy > ry + rh) {
testY = ry + rh;
}
let distX = cx - testX;
let distY = cy - testY;
let sqDistance = distX * distX + distY * distY;
if (sqDistance <= this.radius * this.radius) {
return new Point(testX, testY);
}
return null;
}
overlappedCells(size: Size): Rect[] {
let meAsRect = new Rect(
this.center.offset(new Point(-this.radius, -this.radius)),
new Size(this.radius * 2, this.radius * 2),
);
let all: Rect[] = [];
for (let cell of meAsRect.overlappedCells(size).values()) {
if (this.getContactWithRect(cell) != null) {
all.push(cell);
}
}
return all;
}
}
export class Rect { export class Rect {
readonly top: Point; readonly top: Point;
readonly size: Size; readonly size: Size;

View File

@ -111,14 +111,6 @@ class Input {
return this.#mousePosition; return this.#mousePosition;
} }
isAnyKeyDown(...keys: string[]) : boolean {
for (const k of keys) {
if(this.isKeyDown(k)) {
return true
}
}
return false
}
isKeyDown(key: string): boolean { isKeyDown(key: string): boolean {
return this.#keyDown[key]; return this.#keyDown[key];
} }

View File

@ -1,11 +1,9 @@
import { BreakableBlockPickupCallbacks } from "./pickups.ts"; import { BreakableBlockPickupCallbacks } from "./pickups.ts";
import { Circle, Point } from "./engine/datatypes.ts"; import { Point, Rect, Size } from "./engine/datatypes.ts";
import { displace } from "./physics.ts"; import { displace } from "./physics.ts";
import { getHuntMode } from "./huntmode.ts"; import { getHuntMode } from "./huntmode.ts";
import { DrawPile } from "./drawpile.ts"; import { DrawPile } from "./drawpile.ts";
import { FLOOR_CELL_SIZE } from "./gridart.ts"; import { FLOOR_CELL_SIZE } from "./gridart.ts";
import { sndCollect } from "./sounds.ts";
import { choose } from "./utils.ts";
export class Floater { export class Floater {
xy: Point; xy: Point;
@ -25,7 +23,7 @@ export class Floater {
this.z = z; this.z = z;
this.velZ = 0; this.velZ = 0;
this.frame = choose([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); this.frame = 0;
this.spin = 0; this.spin = 0;
this.collected = false; this.collected = false;
@ -38,7 +36,7 @@ export class Floater {
let { displacement, dxy } = displace( let { displacement, dxy } = displace(
bbox, bbox,
this.velocity, this.velocity,
(r) => getHuntMode().getContact(r), (r) => getHuntMode().isBlocked(r),
{ bounce: 0.6 }, { bounce: 0.6 },
); );
@ -81,7 +79,6 @@ export class Floater {
return; return;
} }
this.collected = true; this.collected = true;
sndCollect.play({ volume: 0.1 });
this.#callbacks.obtain(); this.#callbacks.obtain();
} }
@ -122,9 +119,10 @@ export class Floater {
return !this.collected && this.frame < 1440; return !this.collected && this.frame < 1440;
} }
get bbox(): Circle { get bbox(): Rect {
let sz = 0.25; let w = 0.25;
return new Circle(this.xy, sz / 2); let h = 0.25;
return new Rect(this.xy.offset(new Point(-w / 2, -h / 2)), new Size(w, h));
} }
drawParticle(projected: Point, isShadow: boolean): any { drawParticle(projected: Point, isShadow: boolean): any {
this.#callbacks.drawParticle( this.#callbacks.drawParticle(

View File

@ -1,3 +1,4 @@
import { BG_OUTER } from "./colors.ts";
import { D, I } from "./engine/public.ts"; import { D, I } from "./engine/public.ts";
import { IGame, Point, Size } from "./engine/datatypes.ts"; import { IGame, Point, Size } from "./engine/datatypes.ts";
import { getHotbar, Hotbar } from "./hotbar.ts"; import { getHotbar, Hotbar } from "./hotbar.ts";
@ -6,7 +7,6 @@ import { getVNModal, VNModal } from "./vnmodal.ts";
import { Gameplay, getGameplay } from "./gameplay.ts"; import { Gameplay, getGameplay } from "./gameplay.ts";
import { getEndgameModal } from "./endgamemodal.ts"; import { getEndgameModal } from "./endgamemodal.ts";
import { CheckModal, getCheckModal } from "./checkmodal.ts"; import { CheckModal, getCheckModal } from "./checkmodal.ts";
import { C } from "./colors.ts";
export class Game implements IGame { export class Game implements IGame {
#mainThing: Gameplay | VNModal | null; #mainThing: Gameplay | VNModal | null;
@ -28,7 +28,7 @@ export class Game implements IGame {
// draw screen background // draw screen background
let oldCamera = D.camera; let oldCamera = D.camera;
D.camera = new Point(0, 0); D.camera = new Point(0, 0);
D.fillRect(new Point(0, 0), D.size, C.BG_OUTER); D.fillRect(new Point(0, 0), D.size, BG_OUTER);
D.camera = oldCamera; D.camera = oldCamera;
this.drawGameplay(); this.drawGameplay();

View File

@ -6,6 +6,8 @@ import { addButton } from "./button.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { getStateManager } from "./statemanager.ts"; import { getStateManager } from "./statemanager.ts";
import { getCheckModal } from "./checkmodal.ts"; import { getCheckModal } from "./checkmodal.ts";
//import { LadderPickup } from "./pickups.ts";
// import { generateMap } from "./mapgen.ts";
type Button = { type Button = {
label: string; label: string;
@ -84,12 +86,12 @@ export class Hotbar {
#offerSleep() { #offerSleep() {
let bloodAmount = getPlayerProgress().getBlood(); let bloodAmount = getPlayerProgress().getBlood();
let sleepText = "You're exhausted. Sleep and save your game?"; let sleepText = "You're exhausted.";
if (bloodAmount > 100) { if (bloodAmount > 100) {
sleepText = sleepText =
"You've got some energy left -- are you sure you want to sleep and save your game?"; "You've got some energy left -- are you sure you want to sleep?";
} else if (bloodAmount > 2000) { } else if (bloodAmount > 2000) {
sleepText = "Are you sure you want to sleep and save your game? You have so much energy."; sleepText = "Are you sure you want to sleep? You have so much energy.";
} }
getCheckModal().show( getCheckModal().show(

View File

@ -1,11 +1,17 @@
import { D } from "./engine/public.ts"; import { D } from "./engine/public.ts";
import { Point, Size } from "./engine/datatypes.ts"; import { Point, Size } from "./engine/datatypes.ts";
import {
BG_OUTER,
FG_BOLD,
FG_TEXT,
FG_TEXT_ENDORSED,
FG_TOO_EXPENSIVE,
} from "./colors.ts";
import { ALL_STATS } from "./datatypes.ts"; import { ALL_STATS } from "./datatypes.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { getHuntMode } from "./huntmode.ts"; import { getHuntMode } from "./huntmode.ts";
import { getStateManager } from "./statemanager.ts"; import { getStateManager } from "./statemanager.ts";
import { withCamera } from "./layout.ts"; import { withCamera } from "./layout.ts";
import { C } from "./colors.ts";
export class Hud { export class Hud {
get size(): Size { get size(): Size {
@ -27,45 +33,45 @@ export class Hud {
#update() {} #update() {}
#draw() { #draw() {
D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), C.BG_OUTER); D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), BG_OUTER);
D.drawText(getPlayerProgress().name, new Point(0, 0), C.FG_BOLD); D.drawText(getPlayerProgress().name, new Point(0, 0), FG_BOLD);
let levelText = `Level ${getHuntMode().getDepth()}`; let levelText = `Level ${getHuntMode().getDepth()}`;
let zoneLabel = getHuntMode().getZoneLabel(); let zoneLabel = getHuntMode().getZoneLabel();
if (zoneLabel != null) { if (zoneLabel != null) {
levelText += ": " + zoneLabel; levelText += ": " + zoneLabel;
} }
D.drawText(levelText, new Point(0, 16), C.FG_TEXT); D.drawText(levelText, new Point(0, 16), FG_TEXT);
D.drawText( D.drawText(
`Turn ${getStateManager().getTurn()}/${getStateManager().getMaxTurns()}`, `Turn ${getStateManager().getTurn()}/${getStateManager().getMaxTurns()}`,
new Point(0, 32), new Point(0, 32),
C.FG_TEXT, FG_TEXT,
); );
let y = 64; let y = 64;
let prog = getPlayerProgress(); let prog = getPlayerProgress();
for (let s of ALL_STATS.values()) { for (let s of ALL_STATS.values()) {
D.drawText(`${s}`, new Point(0, y), C.FG_BOLD); D.drawText(`${s}`, new Point(0, y), FG_BOLD);
D.drawText(`${prog.getStat(s)}`, new Point(32, y), C.FG_TEXT); D.drawText(`${prog.getStat(s)}`, new Point(32, y), FG_TEXT);
let talent = prog.getTalent(s); let talent = prog.getTalent(s);
if (talent > 0) { if (talent > 0) {
D.drawText(`(+${talent})`, new Point(56, y), C.FG_TEXT); D.drawText(`(+${talent})`, new Point(56, y), FG_TEXT);
} }
if (talent < 0) { if (talent < 0) {
D.drawText(`(${talent})`, new Point(56, y), C.FG_TEXT); D.drawText(`(${talent})`, new Point(56, y), FG_TEXT);
} }
y += 16; y += 16;
} }
D.drawText("EXP", new Point(0, 144), C.FG_BOLD); D.drawText("EXP", new Point(0, 144), FG_BOLD);
D.drawText(`${prog.getExperience()}`, new Point(32, 144), C.FG_TEXT); D.drawText(`${prog.getExperience()}`, new Point(32, 144), FG_TEXT);
D.drawText("BLD", new Point(0, 160), C.FG_BOLD); D.drawText("BLD", new Point(0, 160), FG_BOLD);
let bloodAmount = prog.getBlood(); let bloodAmount = prog.getBlood();
let bloodColor = C.FG_TEXT; let bloodColor = FG_TEXT;
if (bloodAmount >= 2000) { if (bloodAmount > 2000) {
bloodColor = C.FG_TEXT_ENDORSED; bloodColor = FG_TEXT_ENDORSED;
} }
if (bloodAmount < 100) { if (bloodAmount < 100) {
bloodColor = C.FG_TOO_EXPENSIVE; bloodColor = FG_TOO_EXPENSIVE;
} }
D.drawText(`${prog.getBlood()}cc`, new Point(32, 160), bloodColor); D.drawText(`${prog.getBlood()}cc`, new Point(32, 160), bloodColor);
} }

View File

@ -1,17 +1,23 @@
import { Circle, Point, Size } from "./engine/datatypes.ts"; import { Point, Rect, Size } from "./engine/datatypes.ts";
import { DrawPile } from "./drawpile.ts"; import { DrawPile } from "./drawpile.ts";
import { D, I } from "./engine/public.ts"; import { D, I } from "./engine/public.ts";
import { sprThrallLore } from "./sprites.ts";
import {
BG_INSET,
FG_TEXT,
FG_TEXT_ENDORSED,
FG_TOO_EXPENSIVE,
} from "./colors.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { Architecture, LoadedNewMap } from "./newmap.ts"; import { Architecture, LoadedNewMap } from "./newmap.ts";
import { FLOOR_CELL_SIZE, GridArt } from "./gridart.ts"; import { FLOOR_CELL_SIZE, GridArt } from "./gridart.ts";
import { shadowcast } from "./shadowcast.ts"; import { shadowcast } from "./shadowcast.ts";
import { withCamera } from "./layout.ts"; import { withCamera } from "./layout.ts";
import { getCheckModal } from "./checkmodal.ts"; import { getCheckModal } from "./checkmodal.ts";
import { CARDINAL_DIRECTIONS } from "./mapgen.ts";
import { Block3D, Floor3D, World3D } from "./world3d.ts"; import { Block3D, Floor3D, World3D } from "./world3d.ts";
import { Floater } from "./floater.ts"; import { Floater } from "./floater.ts";
import { displace } from "./physics.ts"; import { displace } from "./physics.ts";
import { getThralls } from "./thralls.ts";
import { C, Microtheme } from "./colors.ts";
export class HuntMode { export class HuntMode {
map: LoadedNewMap; map: LoadedNewMap;
@ -87,10 +93,6 @@ export class HuntMode {
return this.map.get(this.gridifiedPlayer).zoneLabel; return this.map.get(this.gridifiedPlayer).zoneLabel;
} }
getActiveMicrotheme(): Microtheme | null {
return this.map.get(this.gridifiedPlayer).microtheme;
}
// draw // draw
update() { update() {
withCamera("Gameplay", () => { withCamera("Gameplay", () => {
@ -155,19 +157,19 @@ export class HuntMode {
let mvdx = 0; let mvdx = 0;
let mvdy = 0; let mvdy = 0;
if (I.isAnyKeyDown("w", "k", "ArrowUp")) { if (I.isKeyDown("w")) {
touched = true; touched = true;
mvdy -= amt; mvdy -= amt;
} }
if (I.isAnyKeyDown("s", "j", "ArrowDown")) { if (I.isKeyDown("s")) {
touched = true; touched = true;
mvdy += amt; mvdy += amt;
} }
if (I.isAnyKeyDown("a", "h", "ArrowLeft")) { if (I.isKeyDown("a")) {
touched = true; touched = true;
mvdx -= amt; mvdx -= amt;
} }
if (I.isAnyKeyDown("d", "l", "ArrowRight")) { if (I.isKeyDown("d")) {
touched = true; touched = true;
mvdx += amt; mvdx += amt;
} }
@ -194,22 +196,45 @@ export class HuntMode {
this.faceLeft = false; this.faceLeft = false;
} }
let sz = getThralls().get(getPlayerProgress().template).hitboxSize; let szX = 0.5;
let szY = 0.5;
this.velocity = new Point(dx, dy); this.velocity = new Point(dx, dy);
let bbox = new Circle(this.floatingPlayer, sz / 2); // try to push us away from walls if we're close
let { displacement, dxy } = displace(bbox, this.velocity, (b: Circle) => for (let offset of CARDINAL_DIRECTIONS.values()) {
this.getContact(b), let bigBbox = new Rect(
this.floatingPlayer
.offset(offset.scale(new Size(0.12, 0.12)))
.offset(new Point(-szX / 2, -szY / 2)),
new Size(szX, szY),
);
let hitsWall = false;
for (let cell of bigBbox.overlappedCells(new Size(1, 1)).values()) {
if (this.#blocksMovement(cell.top)) {
hitsWall = true;
break;
}
}
if (hitsWall) {
this.velocity = this.velocity.offset(
offset.scale(new Point(0.005, 0.005)).negate(),
);
}
}
let origin = new Point(szX / 2, szY / 2);
let bbox = new Rect(
this.floatingPlayer.offset(origin.negate()),
new Size(szX, szY),
);
let { displacement, dxy } = displace(bbox, this.velocity, (b: Rect) =>
this.isBlocked(b),
); );
this.floatingPlayer = this.floatingPlayer.offset(displacement); this.floatingPlayer = this.floatingPlayer.offset(displacement);
this.velocity = dxy; this.velocity = dxy;
// let friction do it getPlayerProgress().spendBlood(displacement.distance(new Point(0, 0)) * 10);
if (this.map.imposesBloodCosts) {
getPlayerProgress().spendBlood(
(displacement.distance(new Point(0, 0)) * 10) / 3,
);
}
} }
#updateFov() { #updateFov() {
@ -299,11 +324,11 @@ export class HuntMode {
highlighted = false; highlighted = false;
} }
let color = C.BG_FLOOR; let color = BG_INSET;
if (highlighted) { if (highlighted) {
color = C.FG_TEXT; color = FG_TEXT;
if (tooExpensive) { if (tooExpensive) {
color = C.FG_TOO_EXPENSIVE; color = FG_TOO_EXPENSIVE;
} }
} }
@ -373,9 +398,8 @@ export class HuntMode {
}); });
}); });
*/ */
let sprite = getThralls().get(getPlayerProgress().template).sprite;
this.drawpile.add(1024, () => { this.drawpile.add(1024, () => {
D.drawSprite(sprite, new Point(192, 192), 1, { D.drawSprite(sprThrallLore, new Point(192, 192), 1, {
xScale: this.faceLeft ? -2 : 2, xScale: this.faceLeft ? -2 : 2,
yScale: 2, yScale: 2,
}); });
@ -425,18 +449,18 @@ export class HuntMode {
D.fillRect( D.fillRect(
cellOffset.offset(new Point(-4, -4)), cellOffset.offset(new Point(-4, -4)),
new Size(8, 8), new Size(8, 8),
C.FG_TEXT_ENDORSED, FG_TEXT_ENDORSED,
); );
}); });
} }
getContact(bbox: Circle): Point | null { isBlocked(bbox: Rect): boolean {
for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) { for (let cell of bbox.overlappedCells(new Size(1, 1)).values()) {
if (this.#blocksMovement(cell.top)) { if (this.#blocksMovement(cell.top)) {
return bbox.getContactWithRect(cell)!; return true;
} }
} }
return null; return false;
} }
#blocksMovement(xy: Point) { #blocksMovement(xy: Point) {
@ -465,7 +489,3 @@ export function getHuntMode() {
} }
return active; return active;
} }
export function maybeGetHuntMode(): HuntMode | null {
return active;
}

View File

@ -2,5 +2,17 @@ import { hostGame } from "./engine/internal/host.ts";
import { game } from "./game.ts"; import { game } from "./game.ts";
import { getStateManager } from "./statemanager.ts"; import { getStateManager } from "./statemanager.ts";
getStateManager().startOrLoadFirstGame(); getStateManager().startGame(
{
name: "Pyrex",
title: "",
note: null,
stats: { AGI: 10, INT: 10, CHA: 10, PSI: 10 },
talents: { AGI: 0, INT: 0, CHA: 0, PSI: 0 },
skills: [],
isCompulsory: false,
inPenance: false,
},
null,
);
hostGame(game); hostGame(game);

View File

@ -8,7 +8,6 @@ import {
ThrallRecruitedPickup, ThrallRecruitedPickup,
} from "./pickups.ts"; } from "./pickups.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { MICROTHEME_BLACK } from "./colors.ts";
const BASIC_PLAN = Grid.createGridFromMultilineString(` const BASIC_PLAN = Grid.createGridFromMultilineString(`
##################### #####################
@ -17,7 +16,7 @@ const BASIC_PLAN = Grid.createGridFromMultilineString(`
##### A # # D ##### ##### A # # D #####
##### ## ## ##### ##### ## ## #####
# ## ## ## ## # # ## ## ## ## #
#bB . Ee# #bB Ee#
# ## ## ## ## # # ## ## ## ## #
##### ## ## ##### ##### ## ## #####
##### C # # F ##### ##### C # # F #####
@ -28,7 +27,6 @@ const BASIC_PLAN = Grid.createGridFromMultilineString(`
export function generateManor(): LoadedNewMap { export function generateManor(): LoadedNewMap {
let map = new LoadedNewMap("manor", BASIC_PLAN.size); let map = new LoadedNewMap("manor", BASIC_PLAN.size);
map.imposesBloodCosts = false;
let thralls = getThralls().getAll(); let thralls = getThralls().getAll();
for (let y = 0; y < BASIC_PLAN.size.h; y++) { for (let y = 0; y < BASIC_PLAN.size.h; y++) {
@ -54,7 +52,6 @@ export function generateManor(): LoadedNewMap {
}; };
cell.zoneLabel = "Manor"; cell.zoneLabel = "Manor";
cell.microtheme = MICROTHEME_BLACK;
switch (BASIC_PLAN.get(xy)) { switch (BASIC_PLAN.get(xy)) {
case "#": case "#":
break; break;
@ -66,11 +63,6 @@ export function generateManor(): LoadedNewMap {
cell.architecture = Architecture.Floor; cell.architecture = Architecture.Floor;
cell.pickup = new LadderPickup(); cell.pickup = new LadderPickup();
break; break;
case ".":
cell.architecture = Architecture.Floor;
// TODO: Debug objects can be spawned here
// cell.pickup = new ThrallPickup({id: 5});
break;
case " ": case " ":
cell.architecture = Architecture.Floor; cell.architecture = Architecture.Floor;
break; break;

View File

@ -14,23 +14,20 @@ import {
} from "./pickups.ts"; } from "./pickups.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { ItemStage } from "./thralls.ts"; import { ItemStage } from "./thralls.ts";
import { Microtheme } from "./colors.ts";
const WIDTH = 19; const WIDTH = 19;
const HEIGHT = 19; const HEIGHT = 19;
const MIN_VAULTS = 1; const MIN_VAULTS = 1;
const MAX_VAULTS = 2; const MAX_VAULTS = 1;
const NUM_VAULT_TRIES = 90; const NUM_VAULT_TRIES = 90;
const NUM_ROOM_TRIES = 90; const NUM_ROOM_TRIES = 90;
const NUM_STAIRCASE_TRIES = 90; const NUM_STAIRCASE_TRIES = 90;
const NUM_STAIRCASES_DESIRED = 3; const NUM_STAIRCASES_DESIRED = 3;
const NUM_ROOMS_DESIRED = 1; const NUM_ROOMS_DESIRED = 0; // 4;
const EXTRA_CONNECTOR_CHANCE = 0.15; const EXTRA_CONNECTOR_CHANCE = 0.15;
const WINDING_PERCENT = 50; const WINDING_PERCENT = 0;
const DEBUG = false;
// This is an implementation of Nystrom's algorithm: // This is an implementation of Nystrom's algorithm:
// https://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/ // https://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/
@ -71,22 +68,16 @@ class Knife {
this.#region += 1; this.#region += 1;
} }
carve(point: Point, theme?: Microtheme | null, label?: string) { carve(point: Point, label?: string) {
this.#regions.set(point, this.#region); this.#regions.set(point, this.#region);
this.map.get(point).architecture = Architecture.Floor; this.map.get(point).architecture = Architecture.Floor;
this.map.get(point).microtheme = theme ?? null;
this.map.get(point).zoneLabel = label ?? null; this.map.get(point).zoneLabel = label ?? null;
} }
carveRoom( carveRoom(room: Rect, protect?: boolean, label?: string) {
room: Rect,
protect?: boolean,
theme?: Microtheme | null,
label?: string,
) {
for (let y = room.top.y; y < room.top.y + room.size.h; y++) { for (let y = room.top.y; y < room.top.y + room.size.h; y++) {
for (let x = room.top.x; x < room.top.x + room.size.w; x++) { for (let x = room.top.x; x < room.top.x + room.size.w; x++) {
this.carve(new Point(x, y), theme, label); this.carve(new Point(x, y), label);
} }
} }
@ -100,7 +91,7 @@ class Knife {
} }
showDebug(merged: Record<number, number>) { showDebug(merged: Record<number, number>) {
if (DEBUG) { if (true) {
let out = ""; let out = "";
let errors: string[] = []; let errors: string[] = [];
const size = this.#regions.size; const size = this.#regions.size;
@ -112,7 +103,7 @@ class Knife {
return this.#sealedWalls.get(loc) ? "◘" : "█"; return this.#sealedWalls.get(loc) ? "◘" : "█";
} }
let r = this.#regions.get(loc); let r = this.#regions.get(loc);
if (r !== null) { if (typeof r === "number") {
const resolved = merged[r]; const resolved = merged[r];
if (typeof resolved === "number") { if (typeof resolved === "number") {
r = resolved; r = resolved;
@ -344,24 +335,9 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) {
let ab = mergeRects(a, b); let ab = mergeRects(a, b);
knife.startRegion(); knife.startRegion();
knife.carveRoom( knife.carveRoom(ab, false, vaultTemplate.roomLabels.hall);
ab, knife.carveRoom(c, true, vaultTemplate.roomLabels.backroom);
false, knife.carveRoom(d, true, vaultTemplate.roomLabels.closet);
vaultTemplate.microtheme(),
vaultTemplate.roomLabels.hall,
);
knife.carveRoom(
c,
true,
vaultTemplate.microtheme(),
vaultTemplate.roomLabels.backroom,
);
knife.carveRoom(
d,
true,
vaultTemplate.microtheme(),
vaultTemplate.roomLabels.closet,
);
// now place standard pickups // now place standard pickups
for (let dy = 0; dy < ab.size.h; dy++) { for (let dy = 0; dy < ab.size.h; dy++) {
@ -415,11 +391,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) {
if (check != null) { if (check != null) {
knife.map.get(connector).pickup = new LockPickup(check); knife.map.get(connector).pickup = new LockPickup(check);
} }
knife.carve( knife.carve(connector, vaultTemplate.roomLabels.backroom);
connector,
vaultTemplate.microtheme(),
vaultTemplate.roomLabels.backroom,
);
} }
if (mergeRects(c, d).contains(connector)) { if (mergeRects(c, d).contains(connector)) {
// TODO: Put check 2 here // TODO: Put check 2 here
@ -427,11 +399,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) {
if (check != null) { if (check != null) {
knife.map.get(connector).pickup = new LockPickup(check); knife.map.get(connector).pickup = new LockPickup(check);
} }
knife.carve( knife.carve(connector, vaultTemplate.roomLabels.closet);
connector,
vaultTemplate.microtheme(),
vaultTemplate.roomLabels.closet,
);
} }
} }
@ -477,7 +445,7 @@ function carveVault(knife: Knife, room: Rect, vaultTemplate: VaultTemplate) {
} }
function carveStaircase(knife: Knife, room: Rect, ix: number) { function carveStaircase(knife: Knife, room: Rect, ix: number) {
carveRoom(knife, room, null, "Stairwell"); carveRoom(knife, room, "Stairwell");
let x = Math.floor(room.top.x + room.size.w / 2); let x = Math.floor(room.top.x + room.size.w / 2);
let y = Math.floor(room.top.y + room.size.h / 2); let y = Math.floor(room.top.y + room.size.h / 2);
@ -492,14 +460,9 @@ function carveStaircase(knife: Knife, room: Rect, ix: number) {
} }
} }
function carveRoom( function carveRoom(knife: Knife, room: Rect, label?: string) {
knife: Knife,
room: Rect,
theme?: Microtheme | null,
label?: string,
) {
knife.startRegion(); knife.startRegion();
knife.carveRoom(room, false, theme, label); knife.carveRoom(room, false, label);
for (let dy = 0; dy < Math.ceil(room.size.h / 2); dy++) { for (let dy = 0; dy < Math.ceil(room.size.h / 2); dy++) {
for (let dx = 0; dx < Math.ceil(room.size.w / 2); dx++) { for (let dx = 0; dx < Math.ceil(room.size.w / 2); dx++) {
@ -510,16 +473,18 @@ function carveRoom(
new Point(room.size.w - dx - 1, room.size.h - dy - 1), new Point(room.size.w - dx - 1, room.size.h - dy - 1),
); );
let stat = choose(ALL_STATS); let stat = choose(ALL_STATS);
let cb = choose([ knife.map.get(xy0).pickup = new BreakableBlockPickup(
() => new StatPickupCallbacks(stat), new StatPickupCallbacks(stat),
() => new StatPickupCallbacks(stat), );
() => new StatPickupCallbacks(stat), knife.map.get(xy1).pickup = new BreakableBlockPickup(
() => new ExperiencePickupCallbacks(), new StatPickupCallbacks(stat),
]); );
knife.map.get(xy0).pickup = new BreakableBlockPickup(cb()); knife.map.get(xy2).pickup = new BreakableBlockPickup(
knife.map.get(xy1).pickup = new BreakableBlockPickup(cb()); new StatPickupCallbacks(stat),
knife.map.get(xy2).pickup = new BreakableBlockPickup(cb()); );
knife.map.get(xy3).pickup = new BreakableBlockPickup(cb()); knife.map.get(xy3).pickup = new BreakableBlockPickup(
new StatPickupCallbacks(stat),
);
} }
} }
} }
@ -757,7 +722,7 @@ function dedup(items: number[]): number[] {
} }
function showDebug(grid: LoadedNewMap) { function showDebug(grid: LoadedNewMap) {
if (DEBUG) { if (true) {
let out = ""; let out = "";
for (let y = 0; y < grid.size.h; y++) { for (let y = 0; y < grid.size.h; y++) {
for (let x = 0; x < grid.size.w; x++) { for (let x = 0; x < grid.size.w; x++) {

View File

@ -42,14 +42,14 @@ const names = [
"Thisby", "Thisby",
"Calloway", "Calloway",
"Fenna", "Fenna",
// "Lupin", "Lupin",
"Finlo", "Finlo",
"Tycho", "Tycho",
"Talmadge", "Talmadge",
// others // others
"Jeff", "Jeff",
"Jon", "Jon",
// "Garrett", "Garrett",
"Russell", "Russell",
"Tyson", "Tyson",
"Gervase", "Gervase",

View File

@ -1,7 +1,6 @@
import { Grid, Point, Size } from "./engine/datatypes.ts"; import { Grid, Point, Size } from "./engine/datatypes.ts";
import { Pickup } from "./pickups.ts"; import { Pickup } from "./pickups.ts";
import { Skill } from "./datatypes.ts"; import { Skill } from "./datatypes.ts";
import { Microtheme } from "./colors.ts";
export enum Architecture { export enum Architecture {
Wall, Wall,
@ -31,25 +30,21 @@ export class LoadedNewMap {
#id: string; #id: string;
#size: Size; #size: Size;
#entrance: Point | null; #entrance: Point | null;
#imposesBloodCosts: boolean;
#architecture: Grid<Architecture>; #architecture: Grid<Architecture>;
#pickups: Grid<Pickup | null>; #pickups: Grid<Pickup | null>;
#provinces: Grid<string | null>; // TODO: Does this just duplicate zoneLabels #provinces: Grid<string | null>; // TODO: Does this just duplicate zoneLabels
#revealed: Grid<boolean>; #revealed: Grid<boolean>;
#zoneLabels: Grid<string | null>; #zoneLabels: Grid<string | null>;
#microthemes: Grid<Microtheme | null>;
constructor(id: string, size: Size) { constructor(id: string, size: Size) {
this.#id = id; this.#id = id;
this.#size = size; this.#size = size;
this.#entrance = null; this.#entrance = null;
this.#imposesBloodCosts = true;
this.#architecture = new Grid<Architecture>(size, () => Architecture.Wall); this.#architecture = new Grid<Architecture>(size, () => Architecture.Wall);
this.#pickups = new Grid<Pickup | null>(size, () => null); this.#pickups = new Grid<Pickup | null>(size, () => null);
this.#provinces = new Grid<string | null>(size, () => null); this.#provinces = new Grid<string | null>(size, () => null);
this.#revealed = new Grid<boolean>(size, () => false); this.#revealed = new Grid<boolean>(size, () => false);
this.#zoneLabels = new Grid<string | null>(size, () => null); this.#zoneLabels = new Grid<string | null>(size, () => null);
this.#microthemes = new Grid<Microtheme | null>(size, () => null);
} }
set entrance(point: Point) { set entrance(point: Point) {
@ -63,14 +58,6 @@ export class LoadedNewMap {
return this.#entrance; return this.#entrance;
} }
set imposesBloodCosts(value: boolean) {
this.#imposesBloodCosts = value;
}
get imposesBloodCosts() {
return this.#imposesBloodCosts;
}
get size(): Size { get size(): Size {
return this.#size; return this.#size;
} }
@ -119,14 +106,6 @@ export class LoadedNewMap {
return this.#zoneLabels.get(point); return this.#zoneLabels.get(point);
} }
setMicrotheme(point: Point, value: Microtheme | null) {
this.#microthemes.set(point, value);
}
getMicrotheme(point: Point): Microtheme | null {
return this.#microthemes.get(point);
}
isConnected(): boolean { isConnected(): boolean {
const size = this.#size; const size = this.#size;
let reached = new Grid<boolean>(size, () => false); let reached = new Grid<boolean>(size, () => false);
@ -227,13 +206,6 @@ export class CellView {
return this.#map.getZoneLabel(this.#point); return this.#map.getZoneLabel(this.#point);
} }
set microtheme(value: Microtheme | null) {
this.#map.setMicrotheme(this.#point, value);
}
get microtheme(): Microtheme | null {
return this.#map.getMicrotheme(this.#point);
}
copyFrom(cell: CellView) { copyFrom(cell: CellView) {
this.architecture = cell.architecture; this.architecture = cell.architecture;
this.pickup = cell.pickup; this.pickup = cell.pickup;

View File

@ -1,21 +0,0 @@
import { compile, VNScene } from "./vnscene.ts";
export let openingScene: VNScene = compile([
`Mortal!
... I can't call you that anymore, can I?
Listen up:
You've been given a gift! Your life is now over. There's no going back... In nine days you will be one of us.
You probably think this is like a final, but it's more like an entrance exam!
Soon I will forget about you,
Your Progenitor
PS: Left mouse + WASD. Like Quake! Arrows or HJKL work too.`,
]);

View File

@ -1,48 +1,36 @@
import { Circle, lerp, Point } from "./engine/datatypes.ts"; import { Point, Rect } from "./engine/datatypes.ts";
export function displace( export function displace(
bbox: Circle, bbox: Rect,
dxy: Point, dxy: Point,
getContact: (where: Circle) => Point | null, blocked: (where: Rect) => boolean,
options?: { bounce?: number }, options?: { bounce?: number },
): { bbox: Circle; displacement: Point; dxy: Point } { ): { bbox: Rect; displacement: Point; dxy: Point } {
let nSteps = 40; let nSteps = 40;
let nRedirections = 40;
let bounce = options?.bounce ?? 0; let bounce = options?.bounce ?? 0;
let xy = bbox.center; let xy = bbox.top;
let redirections = 0;
for (let i = 0; i < nSteps; i++) { for (let i = 0; i < nSteps; i++) {
let trialXy = xy.offset(new Point(dxy.x / nSteps, dxy.y / nSteps)); let trialXy = xy.offset(new Point(dxy.x / nSteps, 0));
let trialBbox = new Circle(trialXy, bbox.radius); let trialBbox = new Rect(trialXy, bbox.size);
let contact = getContact(trialBbox); if (blocked(trialBbox)) {
if (contact) { dxy = new Point(bounce * -dxy.x, dxy.y);
let normal = contact.offset(trialXy.negate()); } else {
let mag = normal.distance(new Point(0, 0)); xy = trialXy;
let nx = mag == 0 ? 0 : normal.x / mag; }
let ny = mag == 0 ? 0 : normal.y / mag;
let dot = dxy.x * nx + dxy.y * ny; trialXy = xy.offset(new Point(0, dxy.y / nSteps));
if (redirections < nRedirections) { trialBbox = new Rect(trialXy, bbox.size);
dxy = new Point( if (blocked(trialBbox)) {
dxy.x - lerp(bounce, 1, 2) * dot * nx, dxy = new Point(dxy.x, bounce * -dxy.y);
dxy.y - lerp(bounce, 1, 2) * dot * ny,
);
i -= 1; // try again with reflection
redirections += 1;
} else {
dxy = new Point(0, 0);
break;
}
} else { } else {
xy = trialXy; xy = trialXy;
} }
} }
return { return {
bbox: new Circle(xy, bbox.radius), bbox: new Rect(xy, bbox.size),
displacement: xy.offset(bbox.center.negate()), displacement: xy.offset(bbox.top.negate()),
dxy, dxy,
}; };
} }

View File

@ -15,18 +15,10 @@ import { GridArt } from "./gridart.ts";
import { getCheckModal } from "./checkmodal.ts"; import { getCheckModal } from "./checkmodal.ts";
import { Point, Size } from "./engine/datatypes.ts"; import { Point, Size } from "./engine/datatypes.ts";
import { choose } from "./utils.ts"; import { choose } from "./utils.ts";
import { FG_BOLD, FG_TEXT, SWATCH_EXP, SWATCH_STAT } from "./colors.ts";
import { Block3D } from "./world3d.ts"; import { Block3D } from "./world3d.ts";
import { DrawPile } from "./drawpile.ts"; import { DrawPile } from "./drawpile.ts";
import { Floater } from "./floater.ts"; import { Floater } from "./floater.ts";
import {
sndBite,
sndDeath,
sndDig,
sndRecruit,
sndRewardFor,
sndRewardHuge,
} from "./sounds.ts";
import { C } from "./colors.ts";
export type Pickup = export type Pickup =
| LockPickup | LockPickup
@ -81,10 +73,7 @@ export class LockPickup {
update() {} update() {}
onClick(cell: CellView): boolean { onClick(cell: CellView): boolean {
getCheckModal().show(this.check, () => { getCheckModal().show(this.check, () => (cell.pickup = null));
cell.pickup = null;
sndRecruit.play();
});
return true; return true;
} }
@ -157,7 +146,6 @@ export class BreakableBlockPickup {
cellData.pickup = null; cellData.pickup = null;
let n = choose([1, 1, 1, 1, 1, 2, 3]); let n = choose([1, 1, 1, 1, 1, 2, 3]);
sndRewardFor(n).play();
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
let floater = new Floater( let floater = new Floater(
cellData.xy.offset(new Point(0.5, 0.5)), cellData.xy.offset(new Point(0.5, 0.5)),
@ -183,9 +171,6 @@ export class BreakableBlockPickup {
} }
onSqueeze(_cellData: CellView) { onSqueeze(_cellData: CellView) {
if (this.breakProgress == 0) {
sndDig.play({ volume: 0.5 });
}
this.breakProgress = Math.min( this.breakProgress = Math.min(
this.breakProgress + 0.02 + RECOVERY_PER_TICK, this.breakProgress + 0.02 + RECOVERY_PER_TICK,
1.0, 1.0,
@ -201,7 +186,7 @@ export class StatPickupCallbacks {
} }
get cost(): number { get cost(): number {
return 30; return 100;
} }
obtain() { obtain() {
@ -211,9 +196,9 @@ export class StatPickupCallbacks {
getBlock(progress: number) { getBlock(progress: number) {
return new Block3D( return new Block3D(
progress > 0.6 ? C.FG_BOLD : C.SWATCH_STAT[this.#stat][1], progress > 0.6 ? FG_BOLD : SWATCH_STAT[this.#stat][1],
progress > 0.6 ? C.FG_TEXT : C.SWATCH_STAT[this.#stat][0], progress > 0.6 ? FG_TEXT : SWATCH_STAT[this.#stat][0],
progress > 0.6 ? C.FG_BOLD : C.SWATCH_STAT[this.#stat][1], progress > 0.6 ? FG_BOLD : SWATCH_STAT[this.#stat][1],
); );
} }
@ -248,19 +233,19 @@ export class ExperiencePickupCallbacks {
constructor() {} constructor() {}
get cost(): number { get cost(): number {
return 30; return 100;
} }
obtain() { obtain() {
getPlayerProgress().addExperience(10); getPlayerProgress().addExperience(250);
getPlayerProgress().purloinItem(); getPlayerProgress().purloinItem();
} }
getBlock(progress: number) { getBlock(progress: number) {
return new Block3D( return new Block3D(
progress > 0.6 ? C.FG_BOLD : C.SWATCH_EXP[1], progress > 0.6 ? FG_BOLD : SWATCH_EXP[1],
progress > 0.6 ? C.FG_TEXT : C.SWATCH_EXP[0], progress > 0.6 ? FG_TEXT : SWATCH_EXP[0],
progress > 0.6 ? C.FG_BOLD : C.SWATCH_EXP[1], progress > 0.6 ? FG_BOLD : SWATCH_EXP[1],
); );
} }
@ -296,7 +281,7 @@ export class LadderPickup {
} }
blocksMovement() { blocksMovement() {
return false; return true;
} }
obstructsVision() { obstructsVision() {
@ -319,9 +304,7 @@ export class LadderPickup {
update() {} update() {}
onClick(): boolean { onClick(): boolean {
if (getHuntMode().map.imposesBloodCosts) { getPlayerProgress().addBlood(1000);
getPlayerProgress().addBlood(100); // this used to award 1k; 100 now is equivalent to what 300 blood used to be
}
initHuntMode(new HuntMode(getHuntMode().depth + 1, generateMap())); initHuntMode(new HuntMode(getHuntMode().depth + 1, generateMap()));
return false; return false;
} }
@ -375,7 +358,6 @@ export class ThrallPickup {
onClick(cell: CellView): boolean { onClick(cell: CellView): boolean {
let data = getThralls().get(this.thrall); let data = getThralls().get(this.thrall);
getCheckModal().show(data.initialCheck, () => { getCheckModal().show(data.initialCheck, () => {
sndRecruit.play();
getPlayerProgress().unlockThrall(this.thrall); getPlayerProgress().unlockThrall(this.thrall);
cell.pickup = null; cell.pickup = null;
}); });
@ -538,13 +520,6 @@ export class ThrallRecruitedPickup {
100, 100,
); );
getPlayerProgress().damageThrall(this.thrall, choose([0.9])); getPlayerProgress().damageThrall(this.thrall, choose([0.9]));
let newLifeStage = getPlayerProgress().getThrallLifeStage(this.thrall);
if (lifeStage != LifeStage.Dead && newLifeStage == LifeStage.Dead) {
sndDeath.play({ volume: 1.0 });
} else {
sndBite.play({ volume: 1.0 });
}
}, },
); );
return true; return true;
@ -608,7 +583,7 @@ export class ThrallCollectionPlatePickup {
D.drawRect( D.drawRect(
gridArt.project(0).offset(new Point(-18, -18)), gridArt.project(0).offset(new Point(-18, -18)),
new Size(36, 36), new Size(36, 36),
C.FG_TEXT, FG_TEXT,
); );
} else { } else {
D.drawSprite(data.sprite, gridArt.project(2), 3, { D.drawSprite(data.sprite, gridArt.project(2), 3, {
@ -676,7 +651,6 @@ export class ThrallCollectionPlatePickup {
null, null,
); );
data.rewardCallback((what) => this.spawn(cell.xy, what)); data.rewardCallback((what) => this.spawn(cell.xy, what));
sndRewardHuge.play();
} }
} }
@ -691,10 +665,12 @@ export class ThrallCollectionPlatePickup {
} else { } else {
callbacks = new StatPickupCallbacks(what); callbacks = new StatPickupCallbacks(what);
} }
if (callbacks == null) { if (callbacks == null) { return; }
return; let floater = new Floater(
} xy.offset(new Point(0.5, 0.5)),
let floater = new Floater(xy.offset(new Point(0.5, 0.5)), 50, callbacks); 50,
callbacks,
);
let speed = 0.015; let speed = 0.015;
let direction = Math.random() * Math.PI * 2; let direction = Math.random() * Math.PI * 2;
floater.velocity = new Point( floater.velocity = new Point(

View File

@ -1,17 +1,9 @@
import { ALL_STATS, Skill, Stat, SuccessorOption, Wish } from "./datatypes.ts"; import { ALL_STATS, Skill, Stat, SuccessorOption, Wish } from "./datatypes.ts";
import { getSkills } from "./skills.ts"; import { getSkills } from "./skills.ts";
import { getThralls, ItemStage, LifeStage, Thrall } from "./thralls.ts"; import { getThralls, ItemStage, LifeStage, Thrall } from "./thralls.ts";
import { SaveFileV1, mustBeSaveFileV1 } from "./saveformat.ts";
interface NewRoundConfig {
asSuccessor: SuccessorOption;
withWish: Wish | null;
}
export class PlayerProgress { export class PlayerProgress {
#name: string; #name: string;
#thrallTemplate: number;
#nImprovements: number;
#stats: Record<Stat, number>; #stats: Record<Stat, number>;
#talents: Record<Stat, number>; #talents: Record<Stat, number>;
#isInPenance: boolean; #isInPenance: boolean;
@ -26,65 +18,23 @@ export class PlayerProgress {
#thrallsObtainedItem: number[]; #thrallsObtainedItem: number[];
#thrallsDeliveredItem: number[]; #thrallsDeliveredItem: number[];
constructor(args: NewRoundConfig | SaveFileV1) { constructor(asSuccessor: SuccessorOption, withWish: Wish | null) {
if ("asSuccessor" in args) { this.#name = asSuccessor.name;
//asSuccessor: SuccessorOption, withWish: Wish | null) { this.#stats = { ...asSuccessor.stats };
const config = args as NewRoundConfig; this.#talents = { ...asSuccessor.talents };
const asSuccessor = config.asSuccessor; this.#isInPenance = asSuccessor.inPenance;
this.#name = asSuccessor.name; this.#wish = withWish;
this.#thrallTemplate = asSuccessor.template.id; this.#exp = 0;
this.#nImprovements = asSuccessor.nImprovements; this.#blood = 0;
this.#stats = { ...asSuccessor.stats }; this.#itemsPurloined = 0;
this.#talents = { ...asSuccessor.talents }; this.#skillsLearned = [];
this.#isInPenance = asSuccessor.inPenance; this.#untrimmedSkillsAvailable = [];
this.#wish = config.withWish; this.#thrallsUnlocked = [];
this.#exp = 0; this.#thrallDamage = {};
this.#blood = 0; this.#thrallsObtainedItem = [];
this.#itemsPurloined = 0; this.#thrallsDeliveredItem = [];
this.#skillsLearned = [];
this.#untrimmedSkillsAvailable = [];
this.#thrallsUnlocked = [];
this.#thrallDamage = {};
this.#thrallsObtainedItem = [];
this.#thrallsDeliveredItem = [];
this.refill(); this.refill();
} else {
const file = mustBeSaveFileV1(args);
this.#name = file.name;
this.#thrallTemplate = file.thrallTemplateId;
this.#nImprovements = file.nImprovements;
this.#stats = {
AGI: file.stats.agi,
INT: file.stats.int,
CHA: file.stats.cha,
PSI: file.stats.psi,
};
this.#talents = {
AGI: file.talents.agi,
INT: file.talents.int,
CHA: file.talents.cha,
PSI: file.talents.psi,
};
(this.#isInPenance = file.isInPenance),
(this.#wish = file.wishId >= 0 ? { id: file.wishId } : null);
this.#exp = file.exp;
this.#blood = file.blood;
this.#itemsPurloined = file.itemsPurloined;
this.#skillsLearned = file.skillsLearned;
this.#untrimmedSkillsAvailable = file.untrimmedSkillsAvailableIds.map(
(id) => {
return { id: id };
},
);
this.#thrallsUnlocked = file.thrallsUnlocked;
this.#thrallDamage = {};
for (let i = 0; i < file.thrallDamage.length; ++i) {
this.#thrallDamage[i] = file.thrallDamage[i];
}
this.#thrallsObtainedItem = file.thrallsObtainedItem;
this.#thrallsDeliveredItem = file.thrallsDeliveredItem;
}
} }
applyEndOfTurn() { applyEndOfTurn() {
@ -97,20 +47,12 @@ export class PlayerProgress {
return this.#name; return this.#name;
} }
get template(): Thrall {
return { id: this.#thrallTemplate };
}
get nImprovements(): number {
return this.#nImprovements;
}
get isInPenance(): boolean { get isInPenance(): boolean {
return this.#isInPenance; return this.#isInPenance;
} }
refill() { refill() {
this.#blood = 1000; this.#blood = 2000;
let learnableSkills = []; // TODO: Also include costing info let learnableSkills = []; // TODO: Also include costing info
for (let skill of getSkills() for (let skill of getSkills()
@ -252,11 +194,7 @@ export class PlayerProgress {
return skillsAvailable.slice(0, 6); return skillsAvailable.slice(0, 6);
} }
getUntrimmedAvailableSkillIds(): number[] { getLearnedSkills() {
return this.#untrimmedSkillsAvailable.map((s) => s.id);
}
getLearnedSkills(): Skill[] {
let learnedSkills = []; let learnedSkills = [];
for (let s of this.#skillsLearned.values()) { for (let s of this.#skillsLearned.values()) {
learnedSkills.push({ id: s }); learnedSkills.push({ id: s });
@ -264,10 +202,6 @@ export class PlayerProgress {
return learnedSkills; return learnedSkills;
} }
getRawLearnedSkills(): number[] {
return [...this.#skillsLearned];
}
getStats() { getStats() {
return { ...this.#stats }; return { ...this.#stats };
} }
@ -287,10 +221,6 @@ export class PlayerProgress {
return this.#thrallsUnlocked.indexOf(thrall.id) != -1; return this.#thrallsUnlocked.indexOf(thrall.id) != -1;
} }
getUnlockedThrallIds(): number[] {
return [...this.#thrallsUnlocked];
}
damageThrall(thrall: Thrall, amount: number) { damageThrall(thrall: Thrall, amount: number) {
if (amount <= 0.0) { if (amount <= 0.0) {
throw new Error(`damage must be some positive amount, not ${amount}`); throw new Error(`damage must be some positive amount, not ${amount}`);
@ -304,10 +234,6 @@ export class PlayerProgress {
(this.#thrallDamage[thrall.id] ?? 0.0) + amount; (this.#thrallDamage[thrall.id] ?? 0.0) + amount;
} }
getThrallDamage(thrall: Thrall): number {
return this.#thrallDamage[thrall.id] ?? 0.0;
}
getThrallLifeStage(thrall: Thrall): LifeStage { getThrallLifeStage(thrall: Thrall): LifeStage {
let damage = this.#thrallDamage[thrall.id] ?? 0; let damage = this.#thrallDamage[thrall.id] ?? 0;
if (damage < 0.5) { if (damage < 0.5) {
@ -332,10 +258,6 @@ export class PlayerProgress {
this.#thrallsObtainedItem.push(thrall.id); this.#thrallsObtainedItem.push(thrall.id);
} }
getThrallObtainedItemIds(): number[] {
return [...this.#thrallsObtainedItem];
}
deliverThrallItem(thrall: Thrall) { deliverThrallItem(thrall: Thrall) {
if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) { if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) {
return; return;
@ -343,10 +265,6 @@ export class PlayerProgress {
this.#thrallsDeliveredItem.push(thrall.id); this.#thrallsDeliveredItem.push(thrall.id);
} }
getThrallDeliveredItemIds(): number[] {
return [...this.#thrallsDeliveredItem];
}
getThrallItemStage(thrall: Thrall): ItemStage { getThrallItemStage(thrall: Thrall): ItemStage {
if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) { if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) {
return ItemStage.Delivered; return ItemStage.Delivered;
@ -378,11 +296,7 @@ export function initPlayerProgress(
asSuccessor: SuccessorOption, asSuccessor: SuccessorOption,
withWish: Wish | null, withWish: Wish | null,
) { ) {
active = new PlayerProgress({ asSuccessor: asSuccessor, withWish: withWish }); active = new PlayerProgress(asSuccessor, withWish);
}
export function rehydratePlayerProgress(savefile: SaveFileV1) {
active = new PlayerProgress(savefile);
} }
export function getPlayerProgress(): PlayerProgress { export function getPlayerProgress(): PlayerProgress {

View File

@ -1,147 +0,0 @@
import { getPlayerProgress } from "./playerprogress";
import { getStateManager } from "./statemanager";
import { getThralls } from "./thralls";
import { SaveFileV1, mustBeSaveFileV1 } from "./saveformat";
import { getHuntMode } from "./huntmode.ts";
export interface SaveFile {
version: string;
revision: number;
}
type SaveSlot = "FLEDGLING_SLOT_1" | "FLEDGLING_SLOT_2";
/// The result of attempting to load a V1 save file.
interface SaveFileV1LoadResult {
// If present and valid, the loaded file.
file: SaveFileV1 | null;
/// A file loading error, if any. If `file` is present, this refers
/// to an error reading from the *other* slot.
error: string | null;
/// The slot this file was loaded from, or that a load attempt failed from.
/// If multiple load attempts failed and none succeeded, this refers to
/// any one attempted slot.
slot: SaveSlot;
}
function readFromSlot(slot: SaveSlot): SaveFileV1LoadResult {
var serialized = localStorage.getItem(slot);
if (serialized === null) {
return {
file: null,
error: null,
slot: slot,
};
}
try {
return {
file: mustBeSaveFileV1(JSON.parse(serialized)),
error: null,
slot: slot,
};
} catch (e) {
let message = "unidentifiable error";
if (e instanceof Error) {
message = e.message;
}
return {
file: null,
error: message,
slot: slot,
};
}
}
/// Reads the more recent valid save file, if either is valid. If no save files
/// are present, `file` and `error` will both be absent. If an invalid save
/// file is discovered, `error` will refer to the issue(s) detected.
export function readBestSave(): SaveFileV1LoadResult {
const from1 = readFromSlot("FLEDGLING_SLOT_1");
const from2 = readFromSlot("FLEDGLING_SLOT_2");
if (from1.file && from2.file) {
return from1.file.revision > from2.file.revision ? from1 : from2;
}
var errors: string[] = [];
if (from1.error) {
errors = ["slot 1 error: " + from1.error];
}
if (from2.error) {
errors.push("slot 2 error: " + from2.error);
}
var msg: string | null = errors.length > 0 ? errors.join("\n") : null;
if (from1.file) {
return {
file: from1.file,
error: msg,
slot: "FLEDGLING_SLOT_1",
};
}
return {
file: from2.file,
error: msg,
slot: "FLEDGLING_SLOT_2",
};
}
export function saveGame() {
const targetSlot: SaveSlot =
readBestSave().slot === "FLEDGLING_SLOT_1"
? "FLEDGLING_SLOT_2"
: "FLEDGLING_SLOT_1";
return saveIntoSlot(targetSlot);
}
function extractCurrentState(): SaveFileV1 {
const progress = getPlayerProgress();
const stateManager = getStateManager();
const huntMode = getHuntMode();
var thrallDamage: number[] = [];
const nThralls = getThralls().length;
for (let i = 0; i < nThralls; ++i) {
thrallDamage.push(progress.getThrallDamage({ id: i }));
}
return {
version: "fledgling_save_v1",
revision: stateManager.nextRevision(),
turn: stateManager.getTurn(),
name: progress.name,
thrallTemplateId: progress.template.id,
nImprovements: progress.nImprovements,
stats: {
agi: progress.getStat("AGI"),
int: progress.getStat("INT"),
cha: progress.getStat("CHA"),
psi: progress.getStat("PSI"),
},
talents: {
agi: progress.getTalent("AGI"),
int: progress.getTalent("INT"),
cha: progress.getTalent("CHA"),
psi: progress.getTalent("PSI"),
},
isInPenance: progress.isInPenance,
wishId: progress.getWish()?.id ?? -1,
exp: progress.getExperience(),
blood: progress.getBlood(),
itemsPurloined: progress.getItemsPurloined(),
skillsLearned: progress.getRawLearnedSkills(),
untrimmedSkillsAvailableIds: progress.getUntrimmedAvailableSkillIds(),
thrallsUnlocked: progress.getUnlockedThrallIds(),
thrallDamage: thrallDamage,
thrallsObtainedItem: progress.getThrallObtainedItemIds(),
thrallsDeliveredItem: progress.getThrallDeliveredItemIds(),
depth: huntMode.getDepth(),
};
}
function saveIntoSlot(slot: SaveSlot) {
localStorage.setItem(slot, JSON.stringify(extractCurrentState()));
}
export function wipeSaves() {
localStorage.removeItem("FLEDGLING_SLOT_1");
localStorage.removeItem("FLEDGLING_SLOT_2");
}

View File

@ -1,185 +0,0 @@
export interface StatCounterV1 {
agi: number;
int: number;
cha: number;
psi: number;
}
export interface SaveFileV1 {
version: "fledgling_save_v1";
revision: number;
turn: number;
name: string;
thrallTemplateId: number;
nImprovements: number;
stats: StatCounterV1;
talents: StatCounterV1;
isInPenance: boolean;
wishId: number; // negative: Wish is absent
exp: number;
blood: number;
itemsPurloined: number;
skillsLearned: number[];
untrimmedSkillsAvailableIds: number[];
thrallsUnlocked: number[];
thrallDamage: number[]; // 0: thrall is absent or undamaged
thrallsObtainedItem: number[];
thrallsDeliveredItem: number[];
depth: number;
}
/// Checks whether obj is a valid save file, as far as we can tell, and returns
/// it unchanged if it is, or throws an error if it's not valid.
export function mustBeSaveFileV1(obj: unknown): SaveFileV1 {
if (obj === undefined || obj === null) {
throw new Error("nonexistent");
}
if (typeof obj !== "object") {
throw new Error(`not an object; was ${typeof obj}`);
}
if (!("version" in obj)) {
throw new Error("no magic number");
}
if (obj.version !== "fledgling_save_v1") {
throw new Error(`bad magic number: ${obj.version}`);
}
return {
version: "fledgling_save_v1",
revision: mustGetNumber(obj, "revision"),
turn: mustGetNumber(obj, "turn"),
name: mustGetString(obj, "name"),
thrallTemplateId: mustGetNumber(obj, "thrallTemplateId"),
nImprovements: mustGetNumber(obj, "nImprovements"),
stats: mustGetStatCounterV1(obj, "stats"),
talents: mustGetStatCounterV1(obj, "talents"),
isInPenance: mustGetBoolean(obj, "isInPenance"),
wishId: mustGetNumber(obj, "wishId"),
exp: mustGetNumber(obj, "exp"),
blood: mustGetNumber(obj, "blood"),
itemsPurloined: mustGetNumber(obj, "itemsPurloined"),
skillsLearned: mustGetNumberArray(obj, "skillsLearned"),
untrimmedSkillsAvailableIds: mustGetNumberArray(
obj,
"untrimmedSkillsAvailableIds",
),
thrallsUnlocked: mustGetNumberArray(obj, "thrallsUnlocked"),
thrallDamage: mustGetNumberArray(obj, "thrallDamage"),
thrallsObtainedItem: mustGetNumberArray(obj, "thrallsObtainedItem"),
thrallsDeliveredItem: mustGetNumberArray(obj, "thrallsDeliveredItem"),
depth: mustGetNumber(obj, "depth"),
};
}
function mustGetNumber(obj: object, key: string): number {
if (obj === null || obj === undefined) {
throw new Error("container absent");
}
if (typeof obj !== "object") {
throw new Error(`container was not an object; was ${typeof obj}`);
}
if (!(key in obj)) {
throw new Error(`missing number: ${key}`);
}
const dict = obj as { [key: string]: any };
const val = dict[key];
if (typeof val !== "number") {
throw new Error(`not a number: ${key}: ${val}`);
}
return val;
}
function mustGetString(obj: object, key: string): string {
if (obj === null || obj === undefined) {
throw new Error("container absent");
}
if (typeof obj !== "object") {
throw new Error(`container was not an object; was ${typeof obj}`);
}
if (!(key in obj)) {
throw new Error(`missing number: ${key}`);
}
const dict = obj as { [key: string]: any };
const val = dict[key];
if (typeof val !== "string") {
throw new Error(`not a string: ${key}: ${val}`);
}
return val;
}
function mustGetStatCounterV1(obj: object, key: string): StatCounterV1 {
if (obj === null || obj === undefined) {
throw new Error("container absent");
}
if (typeof obj !== "object") {
throw new Error(`container was not an object; was ${typeof obj}`);
}
if (!(key in obj)) {
throw new Error(`missing number: ${key}`);
}
const dict = obj as { [key: string]: any };
const val = dict[key];
if (typeof val !== "object") {
throw new Error(`not an object: ${key}: ${val}`);
}
try {
return {
agi: mustGetNumber(val, "agi"),
int: mustGetNumber(val, "int"),
cha: mustGetNumber(val, "cha"),
psi: mustGetNumber(val, "psi"),
};
} catch (e) {
let message = "unrecognizable error";
if (e instanceof Error) {
message = e.message;
}
throw new Error(`reading ${key}: ${message}`);
}
}
function mustGetBoolean(obj: object, key: string): boolean {
if (obj === null || obj === undefined) {
throw new Error("container absent");
}
if (typeof obj !== "object") {
throw new Error(`container was not an object; was ${typeof obj}`);
}
if (!(key in obj)) {
throw new Error(`missing number: ${key}`);
}
const dict = obj as { [key: string]: any };
const val = dict[key];
if (typeof val !== "boolean") {
throw new Error(`not boolean: ${key}: ${val}`);
}
return val;
}
function mustGetNumberArray(obj: object, key: string): number[] {
if (obj === null || obj === undefined) {
throw new Error("container absent");
}
if (typeof obj !== "object") {
throw new Error(`container was not an object; was ${typeof obj}`);
}
if (!(key in obj)) {
throw new Error(`missing number: ${key}`);
}
const dict = obj as { [key: string]: any };
const val = dict[key];
if (typeof val !== "object") {
throw new Error(`not an object: ${key}: ${val}`);
}
for (const x of val) {
if (typeof x !== "number") {
throw new Error(`contained non-number item in ${key}: ${val}`);
}
}
return val;
}

View File

@ -9,7 +9,6 @@ import {
sceneParty, sceneParty,
sceneStare, sceneStare,
sceneStealth, sceneStealth,
sceneTrueEnding,
} from "./endings.ts"; } from "./endings.ts";
import { generateWishes, getWishes, isWishCompleted } from "./wishes.ts"; import { generateWishes, getWishes, isWishCompleted } from "./wishes.ts";
import { generateSuccessors } from "./successors.ts"; import { generateSuccessors } from "./successors.ts";
@ -82,12 +81,7 @@ class Scorer {
// TODO: Award different ranks depending on second-to-top skill // TODO: Award different ranks depending on second-to-top skill
// TODO: Award different domiciles based on overall score // TODO: Award different domiciles based on overall score
// TODO: Force the rank to match the wish if one existed // TODO: Force the rank to match the wish if one existed
else if (vampiricSkills >= 24) { else if (isMax("stare", 3)) {
scene = sceneTrueEnding;
rank = "Master Vampire";
domicile = "Third Clade";
reignSentence = "You know the truth, or at least your character does.";
} else if (isMax("stare", 3)) {
scene = sceneStare; scene = sceneStare;
rank = "Hypno-Chiropteran"; rank = "Hypno-Chiropteran";
domicile = "Village of Brainwashed Mortals"; domicile = "Village of Brainwashed Mortals";
@ -128,10 +122,7 @@ class Scorer {
vampiricSkills, vampiricSkills,
mortalServants, mortalServants,
}; };
let successorOptions = generateSuccessors( let successorOptions = generateSuccessors(0, penance); // TODO: generate nImprovements from mortalServants and the player's bsae improvements
getPlayerProgress().nImprovements + 2,
penance,
); // TODO: generate nImprovements from mortalServants and the player's bsae improvements
let wishOptions = generateWishes(penance); let wishOptions = generateWishes(penance);
let progenerateVerb = penance ? "Repent" : "Progenerate"; let progenerateVerb = penance ? "Repent" : "Progenerate";

View File

@ -38,23 +38,16 @@ class SkillsTable {
} }
computeCost(skill: Skill) { computeCost(skill: Skill) {
const _STAT_TO_TRIPS: Record<Stat, number> = {
AGI: 1 / 7.2, // 8.4 is what I measured, but this seems very overpriced in practice
INT: 1 / 5.4,
CHA: 1 / 4.8,
PSI: 1 / 7.0,
};
let data = this.get(skill); let data = this.get(skill);
let governingStatValue = 0; let governingStatValue = 0;
for (let stat of data.governing.stats.values()) { for (let stat of data.governing.stats.values()) {
governingStatValue += governingStatValue +=
(getPlayerProgress().getStat(stat) * _STAT_TO_TRIPS[stat]) / getPlayerProgress().getStat(stat) / data.governing.stats.length;
data.governing.stats.length;
} }
if (data.governing.flipped) { if (data.governing.flipped) {
governingStatValue = -governingStatValue + 1; governingStatValue = -governingStatValue + 10;
} }
let mult = getCostMultiplier(getPlayerProgress().getWish(), skill); let mult = getCostMultiplier(getPlayerProgress().getWish(), skill);
@ -101,7 +94,7 @@ function geomInterpolate(
return lowOut * Math.pow(highOut / lowOut, proportion); return lowOut * Math.pow(highOut / lowOut, proportion);
} }
type Difficulty = -0.25 | -0.125 | 0 | 1 | 1.25 | 2 | 3; type Difficulty = 0 | 1 | 1.25 | 2 | 3;
type GoverningTemplate = { type GoverningTemplate = {
stats: Stat[]; stats: Stat[];
note: string; note: string;
@ -165,46 +158,34 @@ function governing(
let cost: number; let cost: number;
let mortalServantValue: number; let mortalServantValue: number;
switch (difficulty) { switch (difficulty) {
case -0.25:
underTarget = 0.0;
target = 3.9;
cost = 50;
mortalServantValue = 1;
break;
case -0.125:
underTarget = 0.25;
target = 4.25;
cost = 50;
mortalServantValue = 1;
break;
case 0: case 0:
underTarget = 0.5; underTarget = 5;
target = 4.5; target = 15;
cost = 50; cost = 50;
mortalServantValue = 1; mortalServantValue = 1;
break; break;
case 1: case 1:
underTarget = 4; underTarget = 15;
target = 10; target = 40;
cost = 50; cost = 100;
mortalServantValue = 2; mortalServantValue = 2;
break; break;
case 1.25: case 1.25:
underTarget = 5; underTarget = 17;
target = 12; target = 42;
cost = 50; cost = 100;
mortalServantValue = 2; mortalServantValue = 2;
break; break;
case 2: case 2:
underTarget = 10; underTarget = 30;
target = 18; target = 70;
cost = 75; cost = 125;
mortalServantValue = 3; mortalServantValue = 3;
break; break;
case 3: case 3:
underTarget = 14; underTarget = 50;
target = 23; target = 100;
cost = 100; cost = 150;
mortalServantValue = 10; mortalServantValue = 10;
break; break;
} }
@ -266,7 +247,7 @@ export let bat3 = table.add({
}); });
export let stealth0 = table.add({ export let stealth0 = table.add({
governing: governing("stealth", -0.25), governing: governing("stealth", 0),
profile: { profile: {
name: "Be Quiet", name: "Be Quiet",
description: description:
@ -303,7 +284,7 @@ export let stealth3 = table.add({
}); });
export let charm0 = table.add({ export let charm0 = table.add({
governing: governing("charm", -0.125), governing: governing("charm", 0),
profile: { profile: {
name: "Flatter", name: "Flatter",
description: description:

View File

@ -2,11 +2,16 @@ import { getPartLocation, withCamera } from "./layout.ts";
import { AlignX, Point, Rect, Size } from "./engine/datatypes.ts"; import { AlignX, Point, Rect, Size } from "./engine/datatypes.ts";
import { DrawPile } from "./drawpile.ts"; import { DrawPile } from "./drawpile.ts";
import { D } from "./engine/public.ts"; import { D } from "./engine/public.ts";
import {
BG_INSET,
FG_BOLD,
FG_TEXT_DISABLED,
FG_TEXT_ENDORSED,
} from "./colors.ts";
import { addButton } from "./button.ts"; import { addButton } from "./button.ts";
import { getSkills } from "./skills.ts"; import { getSkills } from "./skills.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { Skill, SkillData } from "./datatypes.ts"; import { Skill, SkillData } from "./datatypes.ts";
import { C } from "./colors.ts";
export class SkillsModal { export class SkillsModal {
#drawpile: DrawPile; #drawpile: DrawPile;
@ -45,7 +50,7 @@ export class SkillsModal {
this.#drawpile.clear(); this.#drawpile.clear();
let size = this.#size; let size = this.#size;
this.#drawpile.add(0, () => { this.#drawpile.add(0, () => {
D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), C.BG_UI); D.fillRect(new Point(-4, -4), size.add(new Size(8, 8)), BG_INSET);
}); });
// draw skills // draw skills
@ -66,24 +71,24 @@ export class SkillsModal {
0, 0,
(hover) => { (hover) => {
// two column layout // two column layout
let [bg, fg] = [C.BG_UI, C.FG_BOLD]; let [bg, fg] = [BG_INSET, FG_BOLD];
let overpriced = let overpriced =
getSkills().computeCost(skill) > getSkills().computeCost(skill) >
getPlayerProgress().getExperience(); getPlayerProgress().getExperience();
let atMinimum = getSkills().isAtMinimum(skill); let atMinimum = getSkills().isAtMinimum(skill);
if (overpriced) { if (overpriced) {
fg = C.FG_TEXT_DISABLED; fg = FG_TEXT_DISABLED;
} else if (atMinimum) { } else if (atMinimum) {
fg = C.FG_TEXT_ENDORSED; fg = FG_TEXT_ENDORSED;
} }
if (selected || hover) { if (selected || hover) {
[bg, fg] = [C.FG_BOLD, C.BG_UI]; [bg, fg] = [FG_BOLD, BG_INSET];
if (overpriced) { if (overpriced) {
// still use the same BG, for contrast // still use the same BG, for contrast
} else if (atMinimum) { } else if (atMinimum) {
bg = C.FG_TEXT_ENDORSED; bg = FG_TEXT_ENDORSED;
} }
} }
D.fillRect(skillRect.top, skillRect.size, bg); D.fillRect(skillRect.top, skillRect.size, bg);
@ -112,8 +117,8 @@ export class SkillsModal {
let remainingWidth = size.w - 160; let remainingWidth = size.w - 160;
this.#drawpile.add(0, () => { this.#drawpile.add(0, () => {
D.fillRect(new Point(160, 0), new Size(remainingWidth, 96), C.FG_BOLD); D.fillRect(new Point(160, 0), new Size(remainingWidth, 96), FG_BOLD);
D.drawText(createFullDescription(data), new Point(164, 0), C.BG_UI, { D.drawText(createFullDescription(data), new Point(164, 0), BG_INSET, {
forceWidth: remainingWidth - 8, forceWidth: remainingWidth - 8,
}); });
}); });

View File

@ -1,53 +0,0 @@
class SoundShared {
readonly context: AudioContext;
bgmSource: AudioBufferSourceNode | null;
bgmGain: GainNode | null;
constructor() {
this.context = new AudioContext();
this.bgmSource = null;
this.bgmGain = null;
}
}
const shared = new SoundShared();
export class Sound {
#link: string;
#audioBufferPromise: Promise<AudioBuffer>;
constructor(link: string) {
this.#link = link;
this.#audioBufferPromise = this.#unsafeGetAudioBuffer();
}
async #unsafeGetAudioBuffer(): Promise<AudioBuffer> {
let resp = await fetch(this.#link);
let buf = await resp.arrayBuffer();
return await shared.context.decodeAudioData(buf);
}
async #getAudioBuffer(): Promise<AudioBuffer> {
return await this.#audioBufferPromise;
}
play(options?: { volume?: number; bgm?: boolean }) {
this.#getAudioBuffer().then((adata) => {
let source = shared.context.createBufferSource();
source.buffer = adata;
let gain = shared.context.createGain();
gain.gain.value = options?.volume ?? 1.0;
source.connect(gain);
gain.connect(shared.context.destination);
source.start();
if (options?.bgm) {
shared.bgmSource?.stop(shared.context.currentTime + 1);
shared.bgmGain?.gain?.linearRampToValueAtTime(
0.0,
shared.context.currentTime + 1,
);
shared.bgmSource = source;
shared.bgmGain = gain;
}
});
}
}

View File

@ -1,53 +0,0 @@
import audBite from "./art/sounds/bite.mp3";
import audCollect from "./art/sounds/collect.mp3";
import audDeath from "./art/sounds/death.mp3";
import audDig from "./art/sounds/dig.mp3";
import audEnding from "./art/sounds/ending.mp3";
import audRecruit from "./art/sounds/recruit.mp3";
import audRewardBig from "./art/sounds/reward_big.mp3";
import audRewardHuge from "./art/sounds/reward_huge.mp3";
import audRewardMedium from "./art/sounds/reward_medium.mp3";
import audRewardSmall from "./art/sounds/reward_small.mp3";
import audSilence from "./art/sounds/silence.mp3";
import audSleep from "./art/sounds/sleep.mp3";
import audVnBat from "./art/sounds/vn_bat.mp3";
import audVnBreath from "./art/sounds/vn_breath.mp3";
import audVnDance from "./art/sounds/vn_dance.mp3";
import audVnDoorbell from "./art/sounds/vn_doorbell.mp3";
import audVnGhost from "./art/sounds/vn_ghost.mp3";
import audVnPage from "./art/sounds/vn_page.mp3";
import audVnPhone from "./art/sounds/vn_phone.mp3";
import { Sound } from "./sound.ts";
export let sndBite = new Sound(audBite);
export let sndCollect = new Sound(audCollect);
export let sndDeath = new Sound(audDeath);
export let sndDig = new Sound(audDig);
export let sndEnding = new Sound(audEnding);
export let sndRecruit = new Sound(audRecruit);
export let sndRewardBig = new Sound(audRewardBig);
export let sndRewardHuge = new Sound(audRewardHuge);
export let sndRewardMedium = new Sound(audRewardMedium);
export let sndRewardSmall = new Sound(audRewardSmall);
export let sndSilence = new Sound(audSilence);
export let sndSleep = new Sound(audSleep);
export let sndVnBat = new Sound(audVnBat);
export let sndVnBreath = new Sound(audVnBreath);
export let sndVnDance = new Sound(audVnDance);
export let sndVnDoorbell = new Sound(audVnDoorbell);
export let sndVnGhost = new Sound(audVnGhost);
export let sndVnPage = new Sound(audVnPage);
export let sndVnPhone = new Sound(audVnPhone);
export function sndRewardFor(amount: number) {
if (amount <= 1) {
return sndRewardSmall;
}
if (amount <= 2) {
return sndRewardMedium;
}
if (amount <= 3) {
return sndRewardBig;
}
return sndRewardHuge;
}

View File

@ -1,111 +1,38 @@
import { import { getPlayerProgress, initPlayerProgress } from "./playerprogress.ts";
getPlayerProgress,
initPlayerProgress,
rehydratePlayerProgress,
} from "./playerprogress.ts";
import { getHuntMode, HuntMode, initHuntMode } from "./huntmode.ts"; import { getHuntMode, HuntMode, initHuntMode } from "./huntmode.ts";
import { getVNModal } from "./vnmodal.ts"; import { getVNModal } from "./vnmodal.ts";
import { getScorer } from "./scorer.ts"; import { getScorer } from "./scorer.ts";
import { getEndgameModal } from "./endgamemodal.ts"; import { getEndgameModal } from "./endgamemodal.ts";
import { SuccessorOption, Wish } from "./datatypes.ts"; import { SuccessorOption, Wish } from "./datatypes.ts";
import { generateManor } from "./manormap.ts"; import { generateManor } from "./manormap.ts";
import { sndSilence, sndSleep } from "./sounds.ts";
import { openingScene } from "./openingscene.ts";
import { generateName } from "./namegen.ts";
import { photogenicThralls } from "./thralls.ts";
import { choose } from "./utils.ts";
import { SaveFileV1 } from "./saveformat.ts";
import { readBestSave, saveGame } from "./save.ts";
const N_TURNS: number = 9; const N_TURNS: number = 9;
export class StateManager { export class StateManager {
#turn: number; #turn: number;
#revision: number;
constructor(file?: SaveFileV1) { constructor() {
this.#turn = file?.turn ?? 1; this.#turn = 1;
this.#revision = file?.revision ?? 1;
} }
getTurn(): number { getTurn(): number {
return this.#turn; return this.#turn;
} }
nextRevision(): number {
this.#revision++;
return this.#revision;
}
startOrLoadFirstGame() {
let save = readBestSave();
if (save.file != null || save.error != null) {
const file = save.file;
const error = save.error;
getVNModal().play([
{
type: "saveGameScreen",
file: file,
error: error,
},
]);
return;
}
this.startFirstGame();
}
startFirstGame() {
getVNModal().play([
...openingScene,
{
type: "callback",
callback: () => {
this.startGame(
{
name: generateName(),
template: choose(photogenicThralls),
nImprovements: 0,
title: "",
note: null,
stats: { AGI: 10, INT: 10, CHA: 10, PSI: 10 },
talents: { AGI: 0, INT: 0, CHA: 0, PSI: 0 },
skills: [],
isCompulsory: false,
inPenance: false,
},
null,
);
},
},
]);
}
startGame(asSuccessor: SuccessorOption, withWish: Wish | null) { startGame(asSuccessor: SuccessorOption, withWish: Wish | null) {
this.#turn = 1; this.#turn = 1;
initPlayerProgress(asSuccessor, withWish); initPlayerProgress(asSuccessor, withWish);
initHuntMode(new HuntMode(1, generateManor())); initHuntMode(new HuntMode(1, generateManor()));
sndSleep.play({ bgm: true });
}
resumeGame(saveFile: SaveFileV1) {
// hack: prepare depth which advance() uses
this.#turn = saveFile.turn;
this.#revision = saveFile.revision;
rehydratePlayerProgress(saveFile);
initHuntMode(new HuntMode(saveFile.depth, generateManor()));
this.advance();
} }
advance() { advance() {
saveGame();
if (this.#turn + 1 <= N_TURNS) { if (this.#turn + 1 <= N_TURNS) {
this.#turn += 1; this.#turn += 1;
getPlayerProgress().applyEndOfTurn(); getPlayerProgress().applyEndOfTurn();
getPlayerProgress().refill(); getPlayerProgress().refill();
initHuntMode(new HuntMode(getHuntMode().depth, generateManor())); initHuntMode(new HuntMode(getHuntMode().depth, generateManor()));
sndSleep.play({ bgm: true });
} else { } else {
sndSilence.play({ bgm: true }); // TODO: Play a specific scene
let ending = getScorer().pickEnding(); let ending = getScorer().pickEnding();
getVNModal().play(ending.scene); getVNModal().play(ending.scene);
getEndgameModal().show(ending); getEndgameModal().show(ending);

View File

@ -2,7 +2,6 @@ import { ALL_STATS, Skill, Stat, SuccessorOption } from "./datatypes.ts";
import { generateName, generateTitle } from "./namegen.ts"; import { generateName, generateTitle } from "./namegen.ts";
import { choose } from "./utils.ts"; import { choose } from "./utils.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { photogenicThralls } from "./thralls.ts";
export function generateSuccessors( export function generateSuccessors(
nImprovements: number, nImprovements: number,
@ -36,8 +35,6 @@ export function generateSuccessorFromPlayer(): SuccessorOption {
let progress = getPlayerProgress(); let progress = getPlayerProgress();
let successor = { let successor = {
name: progress.name, name: progress.name,
template: progress.template,
nImprovements: progress.nImprovements - 2,
title: "Penitent", title: "Penitent",
note: "Failed at Master's bidding", note: "Failed at Master's bidding",
stats: { ...progress.getStats() }, stats: { ...progress.getStats() },
@ -55,7 +52,6 @@ export function generateSuccessorFromPlayer(): SuccessorOption {
export function generateSuccessor(nImprovements: number): SuccessorOption { export function generateSuccessor(nImprovements: number): SuccessorOption {
let name = generateName(); let name = generateName();
let template = choose(photogenicThralls);
let title = generateTitle(); let title = generateTitle();
let note = null; let note = null;
let stats: Record<Stat, number> = { let stats: Record<Stat, number> = {
@ -79,9 +75,8 @@ export function generateSuccessor(nImprovements: number): SuccessorOption {
talents[choose(ALL_STATS)] += 1; talents[choose(ALL_STATS)] += 1;
}, },
]; ];
let nTotalImprovements = nImprovements; let nTotalImprovements = nImprovements + 5;
let mult = 1; for (let i = 0; i < nTotalImprovements; i++) {
for (let i = 0; i < nTotalImprovements * mult; i++) {
let improvement = let improvement =
improvements[Math.floor(Math.random() * improvements.length)]; improvements[Math.floor(Math.random() * improvements.length)];
improvement(); improvement();
@ -90,16 +85,5 @@ export function generateSuccessor(nImprovements: number): SuccessorOption {
let skills: Skill[] = []; let skills: Skill[] = [];
let inPenance = false; let inPenance = false;
let isCompulsory = false; let isCompulsory = false;
return { return { name, title, note, stats, talents, skills, inPenance, isCompulsory };
name,
template,
nImprovements,
title,
note,
stats,
talents,
skills,
inPenance,
isCompulsory,
};
} }

View File

@ -22,7 +22,7 @@ import {
sprThrallStealth, sprThrallStealth,
} from "./sprites.ts"; } from "./sprites.ts";
import { Sprite } from "./engine/internal/sprite.ts"; import { Sprite } from "./engine/internal/sprite.ts";
import { Stat } from "./datatypes.ts"; import {Stat} from "./datatypes.ts";
export type Thrall = { export type Thrall = {
id: number; id: number;
@ -52,15 +52,10 @@ class ThrallsTable {
} }
return thralls; return thralls;
} }
get length(): number {
return this.#thralls.length;
}
} }
export type ThrallData = { export type ThrallData = {
label: string; label: string;
sprite: Sprite; sprite: Sprite;
hitboxSize: number;
posterCheck: CheckData; posterCheck: CheckData;
initialCheck: CheckData; initialCheck: CheckData;
itemHint: string; itemHint: string;
@ -101,14 +96,9 @@ export function getThralls() {
// Their initial check is, generally, the initial check of the // Their initial check is, generally, the initial check of the
// thrall n-2 or thrall n+1 (ex: Party's initial check is Stealth // thrall n-2 or thrall n+1 (ex: Party's initial check is Stealth
// or Lore) // or Lore)
//
// I then made some swaps:
// - Garrett gets Flatter instead of Respect Elders
// - Monica gets Respect Elders instead of Flatter
export let thrallParty = table.add({ export let thrallParty = table.add({
label: "Garrett", label: "Garrett",
sprite: sprThrallParty, sprite: sprThrallParty,
hitboxSize: 0.7,
posterCheck: { posterCheck: {
label: label:
"This room would be perfect for someone with an ostensibly managed gambling addiction.", "This room would be perfect for someone with an ostensibly managed gambling addiction.",
@ -120,19 +110,18 @@ export let thrallParty = table.add({
options: [ options: [
{ {
skill: () => stealth1, // Disguise skill: () => stealth1, // Disguise
locked: '"What\'s up?"', locked: '"What\'s wrong, Garrett?"',
failure: failure:
"\"If you're not a large pile of money, don't talk to me.\"\n\nHe sobs into his ice cream.", "\"If you're not a large pile of money, don't talk to me.\"\n\nHe sobs into his ice cream.",
unlockable: "*look like a large pile of money*", unlockable: "*look like a large pile of money*",
success: "He scoops you eagerly into his wallet.", success: "He scoops you eagerly into his wallet.",
}, },
{ {
skill: () => charm0, // Flatter skill: () => lore0, // Respect Elders
locked: "Ask him how much he's winning", locked: "TODO",
failure: 'He rolls his eyes at you. "A billion."', failure: "TODO",
unlockable: "Tell him he's cute", unlockable: "TODO",
success: success: "TODO",
"He looks at you like no one has ever told him that before, and blushes. You hold his wing and feel his pulse rise.",
}, },
], ],
}, },
@ -141,12 +130,10 @@ export let thrallParty = table.add({
itemPickupMessage: itemPickupMessage:
"This antique wedding ring looks like it was worth at least fifty big blinds.", "This antique wedding ring looks like it was worth at least fifty big blinds.",
deliveryMessage: deliveryMessage:
'"Oh, that? Yeah, I won it." And then lost it, apparently.\n\nHe\'s elated.', '"Oh, that? Yeah, I won it." And then lost it, apparently.\n\nHe\'s elated. He will never leave.',
rewardMessage: "Garrett showers you with INT!", rewardMessage: "Garrett showers you with INT!",
rewardCallback: (spawn) => { rewardCallback: (spawn) => {
for (let i = 0; i < 12; i++) { for (let i = 0; i < 30; i++) { spawn("INT"); }
spawn("INT");
}
}, },
lifeStageText: { lifeStageText: {
fresh: { fresh: {
@ -183,7 +170,6 @@ export let thrallParty = table.add({
export let thrallLore = table.add({ export let thrallLore = table.add({
label: "Lupin", label: "Lupin",
sprite: sprThrallLore, sprite: sprThrallLore,
hitboxSize: 0.65,
posterCheck: { posterCheck: {
label: label:
"This room would be perfect for someone with a love of nature and screaming.", "This room would be perfect for someone with a love of nature and screaming.",
@ -195,17 +181,16 @@ export let thrallLore = table.add({
options: [ options: [
{ {
skill: () => stare1, // Hypnotize skill: () => stare1, // Hypnotize
locked: '"I\'m a wolf too."', locked: "TODO",
failure: '"AROO?" He shakes his head no.', failure: "TODO",
unlockable: 'Zonk him. "I\'m a wolf now."', unlockable: '"I\'m a wolf too."',
success: success:
"He blinks a few times under your gaze -- then touches your muzzle -- then his own -- then arfs submissively.", "He blinks a few times under your gaze -- then touches your muzzle -- then his own -- then arfs submissively.",
}, },
{ {
skill: () => bat0, // Screech skill: () => bat0, // Screech
locked: "Try to yowl", locked: "TODO",
failure: failure: "TODO",
"He sniffs at your collar. You don't _seem_ like a Wolf Scout.",
unlockable: '"Wolf Scouts AWOO!"', unlockable: '"Wolf Scouts AWOO!"',
success: success:
"Taken aback at how well you know the cheer, he freezes -- then joins you with a similar howl.", "Taken aback at how well you know the cheer, he freezes -- then joins you with a similar howl.",
@ -217,12 +202,10 @@ export let thrallLore = table.add({
itemPickupMessage: itemPickupMessage:
"You can't see yourself in this antique silver mirror. On the other hand, they say silver is effective against wolves.", "You can't see yourself in this antique silver mirror. On the other hand, they say silver is effective against wolves.",
deliveryMessage: deliveryMessage:
"Lupin looks for his own reflection -- with interest, confusion, dismissal, and then deep satisfaction.", "Lupin looks at his own reflection -- with interest, confusion, dismissal, and then deep satisfaction. He loves it. He will never leave.",
rewardMessage: "Lupin showers you with AGI!", rewardMessage: "Lupin showers you with AGI!",
rewardCallback: (spawn) => { rewardCallback: (spawn) => {
for (let i = 0; i < 12; i++) { for (let i = 0; i < 30; i++) { spawn("AGI"); }
spawn("AGI");
}
}, },
lifeStageText: { lifeStageText: {
fresh: { fresh: {
@ -257,31 +240,29 @@ export let thrallLore = table.add({
export let thrallBat = table.add({ export let thrallBat = table.add({
label: "Monica", label: "Monica",
sprite: sprThrallBat, sprite: sprThrallBat,
hitboxSize: 0.5,
posterCheck: { posterCheck: {
label: "This room would be perfect for some kind of television chef.", label: "This room would be perfect for some kind of television chef.",
options: [], options: [],
}, },
initialCheck: { initialCheck: {
label: label:
"That's Monica, the evil judge from MasterCook. A bit older than you, and you don't age anymore. She's enjoying a kiwi flan -- it looks good.", "That's Monica. You've seen her cook on TV! Looks like she's enjoying a kiwi flan.",
options: [ options: [
{ {
skill: () => party1, // Rave skill: () => party1, // Rave
locked: "Act weird to get her attention", locked: "TODO",
failure: '"I -- you -- you know we\'re not being filmed, right?"', failure: "TODO",
unlockable: "Flash your eyes like a TV camera", unlockable: "Slide her a sachet of cocaine.",
success: success:
"The clash of light and color causes some kind of flashback.\nYou convince her that she's on set. She agrees to follow you backstage.", "\"No way. Ketamine if you've got it.\" You do.\n\n(It's not effective on vampires.)",
}, },
{ {
skill: () => lore0, // Respect Elders skill: () => charm0, // Flatter
locked: '"That looks good."', locked: "TODO",
failure: failure: "TODO",
"\"It's not.\" She eats it with such relish it's hard to tell.", unlockable: '"You\'re the best cook ever!"',
unlockable: "Not as good as they used to be",
success: success:
'"Certainly." She seems pleased that you understand.\n\n"Teach me to make a real one?"', '"Settle down!" she says, lowering your volume with a sweep of her hand. "It\'s true though."',
}, },
], ],
}, },
@ -290,15 +271,11 @@ export let thrallBat = table.add({
itemPickupMessage: itemPickupMessage:
"This particular instance of gator food resembles an infamous Aotearoan entree: colonial goose.", "This particular instance of gator food resembles an infamous Aotearoan entree: colonial goose.",
deliveryMessage: deliveryMessage:
'Monica salivates. "This is... this is... simply exquisite!"', 'Monica salivates. "This is... this is... simply exquisite!"\n\nShe is happy. She will never leave.',
rewardMessage: "Monica showers you with CHA and INT!", rewardMessage: "Monica showers you with CHA and INT!",
rewardCallback: (spawn) => { rewardCallback: (spawn) => {
for (let i = 0; i < 8; i++) { for (let i = 0; i < 15; i++) { spawn("CHA"); }
spawn("CHA"); for (let i = 0; i < 15; i++) { spawn("INT"); }
}
for (let i = 0; i < 4; i++) {
spawn("INT");
}
}, },
lifeStageText: { lifeStageText: {
fresh: { fresh: {
@ -333,7 +310,6 @@ export let thrallBat = table.add({
export let thrallCharm = table.add({ export let thrallCharm = table.add({
label: "Renfield", label: "Renfield",
sprite: sprThrallCharm, sprite: sprThrallCharm,
hitboxSize: 0.85,
posterCheck: { posterCheck: {
label: label:
"This room would be perfect for someone who likes vampires even more than you enjoy being a vampire.", "This room would be perfect for someone who likes vampires even more than you enjoy being a vampire.",
@ -345,18 +321,17 @@ export let thrallCharm = table.add({
options: [ options: [
{ {
skill: () => lore1, // Brick by Brick skill: () => lore1, // Brick by Brick
locked: `Ask for vampire facts`, locked: "TODO",
failure: `He eagerly describes the cape, the coffin, the crypt, the castle -- a whole lot of stuff you don't necessarily don't have.`, failure: "TODO",
unlockable: `Talk about your manor`, unlockable: '"Wanna see my crypt?"',
success: success:
"You describe your manor for a while without inviting him. He salivates -- swallowing hard before he manages, in response to your comments, to beg for a peek.", 'He salivates -- swallowing hard before he manages, in response to the prospect, a firm "YES!"',
}, },
{ {
skill: () => stealth0, // Be Quiet skill: () => stealth0, // Be Quiet
locked: "Get your fangs in his face", locked: "TODO",
failure: failure: "TODO",
'"Wow. You\'re -- wow! Wow! Wow!"\n\n"And what\'s more --" you say, giving him far too much of a desirable thing.', unlockable: "Say absolutely nothing.",
unlockable: "Say absolutely nothing",
success: 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.", "His mind overflows with fantasy, and when you let a glint of fang peek through, he claps his arms affectionately around your supercold torso.",
}, },
@ -366,12 +341,10 @@ export let thrallCharm = table.add({
itemPickupMessage: itemPickupMessage:
"Your photo is going to be in a lot of places if it gets out, but you've got the original.", "Your photo is going to be in a lot of places if it gets out, but you've got the original.",
deliveryMessage: deliveryMessage:
"Renfield inhales sharply and widens his stance, trying to hide his physical reaction to your face. He is elated.", "Renfield inhales sharply and widens his stance, trying to hide his physical reaction to your face. He is elated and will never leave.",
rewardMessage: "Renfield showers you with PSI!", rewardMessage: "Renfield showers you with PSI!",
rewardCallback: (spawn) => { rewardCallback: (spawn) => {
for (let i = 0; i < 12; i++) { for (let i = 0; i < 24; i++) { spawn("PSI"); }
spawn("PSI");
}
}, },
lifeStageText: { lifeStageText: {
fresh: { fresh: {
@ -406,7 +379,6 @@ export let thrallCharm = table.add({
export let thrallStealth = table.add({ export let thrallStealth = table.add({
label: "Narthyss", label: "Narthyss",
sprite: sprThrallStealth, sprite: sprThrallStealth,
hitboxSize: 0.85,
posterCheck: { posterCheck: {
label: "This room would be perfect for someone who can breathe fire.", label: "This room would be perfect for someone who can breathe fire.",
options: [], options: [],
@ -417,21 +389,17 @@ export let thrallStealth = table.add({
options: [ options: [
{ {
skill: () => bat1, // Flap skill: () => bat1, // Flap
locked: "Drink cherry soda with your fangs", locked: "TODO",
failure: failure: "TODO",
'"Wow! That\'s incredibly cool," she says. Her eyes scan the other patrons. "Can I get you anything?"', unlockable: "Hang upside-down and offer her a martini.",
unlockable: "Hang upside-down and offer a drink", success: "\"You're ADORABLE!\" She's yours forever.",
success:
'"You\'re ADORABLE!" Her attention is yours alone and she offers to follow you home.',
}, },
{ {
skill: () => stare0, // Dazzle skill: () => stare0, // Dazzle
locked: "Show her some sparks", locked: "TODO",
failure: failure: "TODO",
'"Neat trick!" she says. She blinds you with a glowstick, then heads back into the crowd.', unlockable: "TODO",
unlockable: "Trap her in a tunnel of light", success: "TODO",
success:
"She blushes bright peach, unable to perceive any exit until you approach -- close enough to feel her warm, warm breath. She plants a kiss on your lips.",
}, },
], ],
}, },
@ -440,15 +408,11 @@ export let thrallStealth = table.add({
itemPickupMessage: itemPickupMessage:
"The freezer is empty except for this frozen kobold, who mutters something about collecting blood for its master.", "The freezer is empty except for this frozen kobold, who mutters something about collecting blood for its master.",
deliveryMessage: deliveryMessage:
"\"That? That's not mine.\" But she wants it. Now it's hers.", "\"That? That's not mine.\" But she wants it. Now it's hers. She will never leave.",
rewardMessage: "Narthyss showers you with CHA and AGI!", rewardMessage: "Narthyss showers you with CHA and AGI!",
rewardCallback: (spawn) => { rewardCallback: (spawn) => {
for (let i = 0; i < 8; i++) { for (let i = 0; i < 15; i++) { spawn("CHA"); }
spawn("CHA"); for (let i = 0; i < 15; i++) { spawn("AGI"); }
}
for (let i = 0; i < 4; i++) {
spawn("AGI");
}
}, },
lifeStageText: { lifeStageText: {
fresh: { fresh: {
@ -483,7 +447,6 @@ export let thrallStealth = table.add({
export let thrallStare = table.add({ export let thrallStare = table.add({
label: "Ridley", label: "Ridley",
sprite: sprThrallStare, sprite: sprThrallStare,
hitboxSize: 0.85,
posterCheck: { posterCheck: {
label: "This room would be perfect for a soulless robot.", label: "This room would be perfect for a soulless robot.",
options: [], options: [],
@ -497,18 +460,15 @@ export let thrallStare = table.add({
locked: "\"How many Rs in 'strawberry'?\"", locked: "\"How many Rs in 'strawberry'?\"",
failure: failure:
"It generates an image of a sad fruit shrugging in a muddy plantation.", "It generates an image of a sad fruit shrugging in a muddy plantation.",
unlockable: '"Do you want to come home with me?"', unlockable: "TODO",
success: success: "TODO",
"It generates an image of a happy robot riding the shoulders of a tiny little bat.",
}, },
{ {
skill: () => party0, // Chug skill: () => party0, // Chug
locked: "Check its fluid levels", locked: "TODO",
failure: failure: "TODO",
"It submits to your examination, revealing ordinary quantities of robot blood, robot lubricant, Ener-G, and headlight fluid.", unlockable: "Drink a whole bottle of ink.",
unlockable: "Drink its battery", success: "TODO",
success:
"The sulfuric acid doesn't burn you. This somehow makes Ridley want to hang out with you.\n\n(Ridley is weird.)",
}, },
], ],
}, },
@ -517,12 +477,10 @@ export let thrallStare = table.add({
itemPickupMessage: itemPickupMessage:
"This glinting gear would be perfect for a malfunctioning robot.", "This glinting gear would be perfect for a malfunctioning robot.",
deliveryMessage: deliveryMessage:
"Ridley admires the gear but -- to your surprise -- refuses to jam it into its brain.\n\nThe pup is elated.", "Ridley admires the gear but -- to your surprise -- refuses to jam it into its brain.\n\nThe pup is elated and will never leave.",
rewardMessage: "Ridley showers you with EXP!", rewardMessage: "Ridley showers you with EXP!",
rewardCallback: (spawn) => { rewardCallback: (spawn) => {
for (let i = 0; i < 12; i++) { for (let i = 0; i < 6; i++) { spawn("EXP"); }
spawn("EXP");
}
}, },
lifeStageText: { lifeStageText: {
fresh: { fresh: {
@ -551,19 +509,3 @@ export let thrallStare = table.add({
}, },
}, },
}); });
export let photogenicThralls = [
thrallParty,
thrallParty,
thrallParty,
thrallLore,
thrallLore,
thrallLore,
thrallCharm,
thrallCharm,
thrallCharm,
thrallStealth,
thrallStealth,
thrallStealth,
thrallBat,
];

View File

@ -29,18 +29,8 @@ import {
thrallStare, thrallStare,
thrallStealth, thrallStealth,
} from "./thralls.ts"; } from "./thralls.ts";
import {
Microtheme,
MICROTHEME_GREEN,
MICROTHEME_NEON,
MICROTHEME_PINK,
MICROTHEME_PURPLE_TAN,
MICROTHEME_RED,
MICROTHEME_TEAL,
} from "./colors.ts";
export type VaultTemplate = { export type VaultTemplate = {
microtheme: () => Microtheme | null;
stats: { primary: Stat; secondary: Stat }; stats: { primary: Stat; secondary: Stat };
roomLabels: { roomLabels: {
hall: string; hall: string;
@ -55,7 +45,6 @@ export type VaultTemplate = {
export const standardVaultTemplates: VaultTemplate[] = [ export const standardVaultTemplates: VaultTemplate[] = [
{ {
// zoo // zoo
microtheme: () => MICROTHEME_GREEN,
stats: { primary: "AGI", secondary: "PSI" }, stats: { primary: "AGI", secondary: "PSI" },
roomLabels: { roomLabels: {
hall: "Zoo", hall: "Zoo",
@ -108,7 +97,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
}, },
{ {
// blood bank // blood bank
microtheme: () => MICROTHEME_RED,
stats: { primary: "AGI", secondary: "INT" }, stats: { primary: "AGI", secondary: "INT" },
roomLabels: { roomLabels: {
hall: "Blood Bank", hall: "Blood Bank",
@ -160,7 +148,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
}, },
{ {
// coffee shop // coffee shop
microtheme: () => MICROTHEME_PINK,
stats: { primary: "PSI", secondary: "CHA" }, stats: { primary: "PSI", secondary: "CHA" },
roomLabels: { roomLabels: {
hall: "Coffee Shop", hall: "Coffee Shop",
@ -213,7 +200,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
}, },
{ {
// optometrist // optometrist
microtheme: () => MICROTHEME_TEAL,
stats: { primary: "PSI", secondary: "PSI" }, stats: { primary: "PSI", secondary: "PSI" },
roomLabels: { roomLabels: {
hall: "Optometrist", hall: "Optometrist",
@ -266,7 +252,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
}, },
{ {
// club, // club,
microtheme: () => MICROTHEME_NEON,
stats: { primary: "CHA", secondary: "PSI" }, stats: { primary: "CHA", secondary: "PSI" },
roomLabels: { roomLabels: {
hall: "Nightclub", hall: "Nightclub",
@ -319,7 +304,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
}, },
{ {
// library // library
microtheme: () => MICROTHEME_PURPLE_TAN,
stats: { primary: "INT", secondary: "CHA" }, stats: { primary: "INT", secondary: "CHA" },
roomLabels: { roomLabels: {
hall: "Library", hall: "Library",

View File

@ -1,13 +1,8 @@
import { D, I } from "./engine/public.ts"; import { D, I } from "./engine/public.ts";
import { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts"; import { AlignX, AlignY, Point } from "./engine/datatypes.ts";
import { FG_BOLD } from "./colors.ts";
import { withCamera } from "./layout.ts"; import { withCamera } from "./layout.ts";
import { VNScene, VNSceneMessage, VNScenePart } from "./vnscene.ts"; import { VNScene, VNSceneMessage, VNScenePart } from "./vnscene.ts";
import { C } from "./colors.ts";
import { DrawPile } from "./drawpile.ts";
import { wipeSaves } from "./save.ts";
import { SaveFileV1 } from "./saveformat.ts";
import { addButton } from "./button.ts";
import { getStateManager } from "./statemanager.ts";
const WIDTH = 384; const WIDTH = 384;
const HEIGHT = 384; const HEIGHT = 384;
@ -90,13 +85,7 @@ interface SceneCathexis {
function createCathexis(part: VNScenePart): SceneCathexis { function createCathexis(part: VNScenePart): SceneCathexis {
switch (part.type) { switch (part.type) {
case "message": case "message":
part?.sfx?.play({ volume: 0.5 });
return new SceneMessageCathexis(part); return new SceneMessageCathexis(part);
case "callback":
part?.callback();
return new SkipCathexis();
case "saveGameScreen":
return new SaveGameCathexis(part.file, part.error);
} }
} }
@ -119,22 +108,18 @@ class SceneMessageCathexis {
let firstFrame = !this.#gotOneFrame; let firstFrame = !this.#gotOneFrame;
this.#gotOneFrame = true; this.#gotOneFrame = true;
if (!firstFrame && I.isMouseClicked("leftMouse")) { // TODO: SFX
if (!firstFrame && I.isAnythingPressed()) {
this.#done = true; this.#done = true;
} }
} }
draw() { draw() {
D.drawText( D.drawText(this.#message.text, new Point(WIDTH / 2, HEIGHT / 2), FG_BOLD, {
this.#message.text, alignX: AlignX.Center,
new Point(WIDTH / 2, HEIGHT / 2), alignY: AlignY.Middle,
C.FG_BOLD, forceWidth: WIDTH,
{ });
alignX: AlignX.Center,
alignY: AlignY.Middle,
forceWidth: WIDTH,
},
);
} }
} }
@ -142,104 +127,3 @@ let active: VNModal = new VNModal();
export function getVNModal() { export function getVNModal() {
return active; return active;
} }
class SkipCathexis {
constructor() {}
isDone() {
return true;
}
update() {
throw new Error("shouldn't be updated");
}
draw() {
throw new Error("shouldn't ever be drawn");
}
}
class SaveGameCathexis {
#drawpile: DrawPile;
#file: SaveFileV1 | null;
#error: string | null;
#done: boolean;
constructor(file: SaveFileV1 | null, error: string | null) {
this.#drawpile = new DrawPile();
this.#file = file;
this.#error = error;
this.#done = false;
}
isDone() {
return this.#done;
}
update() {
let name = this.#file?.name;
let turn = this.#file?.turn ?? 0;
let turnText = turn < 9 ? `${name}, Turn ${turn + 1}` : "Sentence of Fate";
this.#drawpile.clear();
this.#drawpile.add(0, () => {
D.drawText(
this.#error && this.#file
? `A save was invalid. Continue from an alternate save?
${this.#error}`
: this.#error
? `Your save was invalid:
${this.#error}`
: "Resume from save?",
new Point(WIDTH / 2, HEIGHT / 2),
C.FG_BOLD,
{
alignX: AlignX.Center,
alignY: AlignY.Middle,
forceWidth: WIDTH,
},
);
});
addButton(
this.#drawpile,
"Clear Save",
new Rect(new Point(0, HEIGHT - 32), new Size(128, 32)),
this.#file != null,
() => {
wipeSaves();
this.#file = null;
},
);
if (this.#file) {
let file = this.#file;
addButton(
this.#drawpile,
`Continue (${turnText})`,
new Rect(new Point(128, HEIGHT - 32), new Size(WIDTH - 128, 32)),
true,
() => {
getStateManager().resumeGame(file);
this.#done = true;
},
);
} else {
addButton(
this.#drawpile,
`Start New Game`,
new Rect(new Point(128, HEIGHT - 32), new Size(WIDTH - 128, 32)),
true,
() => {
getStateManager().startFirstGame();
this.#done = true;
},
);
}
this.#drawpile.executeOnClick();
}
draw() {
this.#drawpile.draw();
}
}

View File

@ -1,29 +1,12 @@
import { Sound } from "./sound.ts";
import { SaveFileV1 } from "./saveformat.ts";
export type VNSceneMessage = { export type VNSceneMessage = {
type: "message"; type: "message";
text: string; text: string;
sfx?: Sound; sfx?: string;
}; };
export type VNSceneCallback = { export type VNSceneBasisPart = string | VNSceneMessage;
type: "callback";
callback: () => void;
};
export type VNSceneSaveGameScreen = {
type: "saveGameScreen";
file: SaveFileV1 | null;
error: string | null;
};
export type VNSceneBasisPart = string | VNSceneMessage | VNSceneCallback;
export type VNSceneBasis = VNSceneBasisPart[]; export type VNSceneBasis = VNSceneBasisPart[];
export type VNScenePart = export type VNScenePart = VNSceneMessage;
| VNSceneMessage
| VNSceneCallback
| VNSceneSaveGameScreen;
export type VNScene = VNScenePart[]; export type VNScene = VNScenePart[];
export function compile(basis: VNSceneBasis): VNScene { export function compile(basis: VNSceneBasis): VNScene {

View File

@ -31,7 +31,6 @@ import {
} from "./skills.ts"; } from "./skills.ts";
import { compile, VNSceneBasisPart } from "./vnscene.ts"; import { compile, VNSceneBasisPart } from "./vnscene.ts";
import { getPlayerProgress } from "./playerprogress.ts"; import { getPlayerProgress } from "./playerprogress.ts";
import { sndVnBreath } from "./sounds.ts";
class WishesTable { class WishesTable {
#wishes: WishData[]; #wishes: WishData[];
@ -69,7 +68,7 @@ export function getWishes(): WishesTable {
const whisper: VNSceneBasisPart = { const whisper: VNSceneBasisPart = {
type: "message", type: "message",
text: "...", text: "...",
sfx: sndVnBreath, sfx: "whisper.mp3",
}; };
export const celebritySocialite = table.add({ export const celebritySocialite = table.add({

View File

@ -1,7 +1,12 @@
import { Color, Grid, Point, Rect, Size } from "./engine/datatypes.ts"; import { Color, Grid, Point, Rect, Size } from "./engine/datatypes.ts";
import { DrawPile } from "./drawpile.ts"; import { DrawPile } from "./drawpile.ts";
import { GridArt } from "./gridart.ts"; import { GridArt } from "./gridart.ts";
import { C } from "./colors.ts"; import {
BG_CEILING,
BG_WALL_OR_UNREVEALED,
FG_BOLD,
FG_TEXT,
} from "./colors.ts";
export class World3D { export class World3D {
#grid: Grid<Element3D>; #grid: Grid<Element3D>;
@ -22,7 +27,7 @@ export class World3D {
if (here == null) { if (here == null) {
drawpile.add(OFFSET_TOP, () => { drawpile.add(OFFSET_TOP, () => {
gridArt.drawCeiling(C.BG_CEILING); gridArt.drawCeiling(BG_CEILING);
}); });
return; return;
} }
@ -130,6 +135,6 @@ export class Block3D {
} }
static standardWall(): Block3D { static standardWall(): Block3D {
return new Block3D(C.BG_OUTERWALL, C.BG_INNERWALL, C.BG_WALL_OR_UNREVEALED); return new Block3D(FG_BOLD, FG_TEXT, BG_WALL_OR_UNREVEALED);
} }
} }

View File

@ -1,9 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2023", "target": "ES2020",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"lib": ["ES2023", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */

View File

@ -1,10 +0,0 @@
// vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
base: "",
minify: false,
build: {
target: "esnext",
},
});