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">
<head>
<meta charset="UTF-8" />
<title>FLEDGLING</title>
<title>Prototype 2</title>
</head>
<body>
<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 { 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";
export function addButton(
@ -25,18 +31,18 @@ export function addButton(
drawpile.addClickable(
0,
(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) {
fgLabel = C.FG_TEXT_DISABLED;
fgLabel = FG_TEXT_DISABLED;
}
if (enabled && options?.endorse) {
fg = C.FG_TEXT_ENDORSED;
fgLabel = C.FG_TEXT_ENDORSED;
fg = FG_TEXT_ENDORSED;
fgLabel = FG_TEXT_ENDORSED;
}
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(
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 { AlignX, AlignY, Point, Rect, Size } from "./engine/datatypes.ts";
import { D } from "./engine/public.ts";
import { BG_INSET, FG_BOLD } from "./colors.ts";
import { addButton } from "./button.ts";
import { getSkills } from "./skills.ts";
import { getPlayerProgress } from "./playerprogress.ts";
import { C } from "./colors.ts";
export class CheckModal {
#drawpile: DrawPile;
@ -54,22 +54,17 @@ export class CheckModal {
let size = this.#size;
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;
if (success) {
this.#drawpile.add(0, () => {
D.drawText(
success,
new Point(size.w / 2, (size.h - 64) / 2),
C.FG_BOLD,
{
forceWidth: size.w,
alignX: AlignX.Center,
alignY: AlignY.Middle,
},
);
D.drawText(success, new Point(size.w / 2, (size.h - 64) / 2), FG_BOLD, {
forceWidth: size.w,
alignX: AlignX.Center,
alignY: AlignY.Middle,
});
});
addButton(
this.#drawpile,
@ -85,16 +80,11 @@ export class CheckModal {
let labelText = check.label;
this.#drawpile.add(0, () => {
D.drawText(
labelText,
new Point(size.w / 2, (size.h - 64) / 2),
C.FG_BOLD,
{
forceWidth: size.w,
alignX: AlignX.Center,
alignY: AlignY.Middle,
},
);
D.drawText(labelText, new Point(size.w / 2, (size.h - 64) / 2), FG_BOLD, {
forceWidth: size.w,
alignX: AlignX.Center,
alignY: AlignY.Middle,
});
});
let options = check.options;

View File

@ -1,206 +1,46 @@
import { Color } from "./engine/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 = {
SKY0: Color; // outer, less dark
FLOOR0: Color; // floor, even less dark
export const BG_OUTER = Color.parseHexCode("#143464");
export const BG_WALL_OR_UNREVEALED = Color.parseHexCode("#143464");
export const BG_INSET = Color.parseHexCode("#242234");
export const FG_TEXT = Color.parseHexCode("#c0c0c0");
export const FG_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)
WALL1: Color; // darkest (ex. the underside of something)
// stat colors
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
FG1: Color; // dark (ex. disabled text)
FG2: Color; // normal (ex. normal text)
FG3: Color; // brightest (ex. bold text)
export const SWATCH_AGI: [Color, Color] = [
Color.parseHexCode("#df3e23"),
Color.parseHexCode("#fa6a0a"),
];
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 { Thrall } from "./thralls.ts";
export type 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 = {
name: string;
title: string;
template: Thrall;
nImprovements: number;
note: string | null; // ex "already a vampire"
stats: Record<Stat, number>;
talents: Record<Stat, number>;

View File

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

View File

@ -1,18 +1,9 @@
import { compile, VNScene, VNSceneBasisPart } from "./vnscene.ts";
import {
sndVnBat,
sndVnBreath,
sndVnDance,
sndVnDoorbell,
sndVnGhost,
sndVnPage,
sndVnPhone,
} from "./sounds.ts";
const squeak: VNSceneBasisPart = {
type: "message",
text: "...",
sfx: sndVnBat,
sfx: "squeak.mp3",
};
export const sceneBat: VNScene = compile([
@ -34,7 +25,7 @@ export const sceneBat: VNScene = compile([
const doorbell: VNSceneBasisPart = {
type: "message",
text: "...",
sfx: sndVnDoorbell,
sfx: "doorbell.mp3",
};
export const sceneStealth: VNScene = compile([
@ -42,7 +33,7 @@ export const sceneStealth: VNScene = compile([
"Yeah, you can let yourself in.",
doorbell,
"I'll have it moved.",
"Just -- don't call Liz, OK?",
"Just -- don't call Susan, OK?",
doorbell,
"Believe me, I'm good for the money.",
"I'm doing... a lot better than it looks like.",
@ -55,7 +46,7 @@ export const sceneStealth: VNScene = compile([
const phoneBeep: VNSceneBasisPart = {
type: "message",
text: "...",
sfx: sndVnPhone,
sfx: "phonebeep.mp3",
};
export const sceneCharm: VNScene = compile([
@ -70,7 +61,7 @@ export const sceneCharm: VNScene = compile([
"Can you put me through?",
phoneBeep,
"I really want it.",
"It's for my boyfriend. My old boyfriend, sorry.",
"It's for my boyfriend. First boyfriend, sorry.",
phoneBeep,
"*chuckle*",
"Yeah. I guess I do.",
@ -81,7 +72,7 @@ export const sceneCharm: VNScene = compile([
const sleepyBreath: VNSceneBasisPart = {
type: "message",
text: "...",
sfx: sndVnBreath,
sfx: "sleepyBreath.mp3",
};
export const sceneStare: VNScene = compile([
@ -102,7 +93,7 @@ export const sceneStare: VNScene = compile([
const party: VNSceneBasisPart = {
type: "message",
text: "...",
sfx: sndVnDance,
sfx: "party.mp3",
};
export const sceneParty: VNScene = compile([
@ -120,7 +111,7 @@ export const sceneParty: VNScene = compile([
const ghost: VNSceneBasisPart = {
type: "message",
text: "...",
sfx: sndVnGhost,
sfx: "ghost.mp3",
};
export const sceneLore: VNScene = compile([
@ -136,37 +127,3 @@ export const sceneLore: VNScene = compile([
"Yeah. They remember.",
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 {
readonly top: Point;
readonly size: Size;

View File

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

View File

@ -1,11 +1,9 @@
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 { getHuntMode } from "./huntmode.ts";
import { DrawPile } from "./drawpile.ts";
import { FLOOR_CELL_SIZE } from "./gridart.ts";
import { sndCollect } from "./sounds.ts";
import { choose } from "./utils.ts";
export class Floater {
xy: Point;
@ -25,7 +23,7 @@ export class Floater {
this.z = z;
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.collected = false;
@ -38,7 +36,7 @@ export class Floater {
let { displacement, dxy } = displace(
bbox,
this.velocity,
(r) => getHuntMode().getContact(r),
(r) => getHuntMode().isBlocked(r),
{ bounce: 0.6 },
);
@ -81,7 +79,6 @@ export class Floater {
return;
}
this.collected = true;
sndCollect.play({ volume: 0.1 });
this.#callbacks.obtain();
}
@ -122,9 +119,10 @@ export class Floater {
return !this.collected && this.frame < 1440;
}
get bbox(): Circle {
let sz = 0.25;
return new Circle(this.xy, sz / 2);
get bbox(): Rect {
let w = 0.25;
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 {
this.#callbacks.drawParticle(

View File

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

View File

@ -6,6 +6,8 @@ import { addButton } from "./button.ts";
import { getPlayerProgress } from "./playerprogress.ts";
import { getStateManager } from "./statemanager.ts";
import { getCheckModal } from "./checkmodal.ts";
//import { LadderPickup } from "./pickups.ts";
// import { generateMap } from "./mapgen.ts";
type Button = {
label: string;
@ -84,12 +86,12 @@ export class Hotbar {
#offerSleep() {
let bloodAmount = getPlayerProgress().getBlood();
let sleepText = "You're exhausted. Sleep and save your game?";
let sleepText = "You're exhausted.";
if (bloodAmount > 100) {
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) {
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(

View File

@ -1,11 +1,17 @@
import { D } from "./engine/public.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 { getPlayerProgress } from "./playerprogress.ts";
import { getHuntMode } from "./huntmode.ts";
import { getStateManager } from "./statemanager.ts";
import { withCamera } from "./layout.ts";
import { C } from "./colors.ts";
export class Hud {
get size(): Size {
@ -27,45 +33,45 @@ export class Hud {
#update() {}
#draw() {
D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), C.BG_OUTER);
D.drawText(getPlayerProgress().name, new Point(0, 0), C.FG_BOLD);
D.fillRect(new Point(-4, -4), this.size.add(new Size(8, 8)), BG_OUTER);
D.drawText(getPlayerProgress().name, new Point(0, 0), FG_BOLD);
let levelText = `Level ${getHuntMode().getDepth()}`;
let zoneLabel = getHuntMode().getZoneLabel();
if (zoneLabel != null) {
levelText += ": " + zoneLabel;
}
D.drawText(levelText, new Point(0, 16), C.FG_TEXT);
D.drawText(levelText, new Point(0, 16), FG_TEXT);
D.drawText(
`Turn ${getStateManager().getTurn()}/${getStateManager().getMaxTurns()}`,
new Point(0, 32),
C.FG_TEXT,
FG_TEXT,
);
let y = 64;
let prog = getPlayerProgress();
for (let s of ALL_STATS.values()) {
D.drawText(`${s}`, new Point(0, y), C.FG_BOLD);
D.drawText(`${prog.getStat(s)}`, new Point(32, y), C.FG_TEXT);
D.drawText(`${s}`, new Point(0, y), FG_BOLD);
D.drawText(`${prog.getStat(s)}`, new Point(32, y), FG_TEXT);
let talent = prog.getTalent(s);
if (talent > 0) {
D.drawText(`(+${talent})`, new Point(56, y), C.FG_TEXT);
D.drawText(`(+${talent})`, new Point(56, y), FG_TEXT);
}
if (talent < 0) {
D.drawText(`(${talent})`, new Point(56, y), C.FG_TEXT);
D.drawText(`(${talent})`, new Point(56, y), FG_TEXT);
}
y += 16;
}
D.drawText("EXP", new Point(0, 144), C.FG_BOLD);
D.drawText(`${prog.getExperience()}`, new Point(32, 144), C.FG_TEXT);
D.drawText("BLD", new Point(0, 160), C.FG_BOLD);
D.drawText("EXP", new Point(0, 144), FG_BOLD);
D.drawText(`${prog.getExperience()}`, new Point(32, 144), FG_TEXT);
D.drawText("BLD", new Point(0, 160), FG_BOLD);
let bloodAmount = prog.getBlood();
let bloodColor = C.FG_TEXT;
if (bloodAmount >= 2000) {
bloodColor = C.FG_TEXT_ENDORSED;
let bloodColor = FG_TEXT;
if (bloodAmount > 2000) {
bloodColor = FG_TEXT_ENDORSED;
}
if (bloodAmount < 100) {
bloodColor = C.FG_TOO_EXPENSIVE;
bloodColor = FG_TOO_EXPENSIVE;
}
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 { 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 { Architecture, LoadedNewMap } from "./newmap.ts";
import { FLOOR_CELL_SIZE, GridArt } from "./gridart.ts";
import { shadowcast } from "./shadowcast.ts";
import { withCamera } from "./layout.ts";
import { getCheckModal } from "./checkmodal.ts";
import { CARDINAL_DIRECTIONS } from "./mapgen.ts";
import { Block3D, Floor3D, World3D } from "./world3d.ts";
import { Floater } from "./floater.ts";
import { displace } from "./physics.ts";
import { getThralls } from "./thralls.ts";
import { C, Microtheme } from "./colors.ts";
export class HuntMode {
map: LoadedNewMap;
@ -87,10 +93,6 @@ export class HuntMode {
return this.map.get(this.gridifiedPlayer).zoneLabel;
}
getActiveMicrotheme(): Microtheme | null {
return this.map.get(this.gridifiedPlayer).microtheme;
}
// draw
update() {
withCamera("Gameplay", () => {
@ -155,19 +157,19 @@ export class HuntMode {
let mvdx = 0;
let mvdy = 0;
if (I.isAnyKeyDown("w", "k", "ArrowUp")) {
if (I.isKeyDown("w")) {
touched = true;
mvdy -= amt;
}
if (I.isAnyKeyDown("s", "j", "ArrowDown")) {
if (I.isKeyDown("s")) {
touched = true;
mvdy += amt;
}
if (I.isAnyKeyDown("a", "h", "ArrowLeft")) {
if (I.isKeyDown("a")) {
touched = true;
mvdx -= amt;
}
if (I.isAnyKeyDown("d", "l", "ArrowRight")) {
if (I.isKeyDown("d")) {
touched = true;
mvdx += amt;
}
@ -194,22 +196,45 @@ export class HuntMode {
this.faceLeft = false;
}
let sz = getThralls().get(getPlayerProgress().template).hitboxSize;
let szX = 0.5;
let szY = 0.5;
this.velocity = new Point(dx, dy);
let bbox = new Circle(this.floatingPlayer, sz / 2);
let { displacement, dxy } = displace(bbox, this.velocity, (b: Circle) =>
this.getContact(b),
// try to push us away from walls if we're close
for (let offset of CARDINAL_DIRECTIONS.values()) {
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.velocity = dxy;
// let friction do it
if (this.map.imposesBloodCosts) {
getPlayerProgress().spendBlood(
(displacement.distance(new Point(0, 0)) * 10) / 3,
);
}
getPlayerProgress().spendBlood(displacement.distance(new Point(0, 0)) * 10);
}
#updateFov() {
@ -299,11 +324,11 @@ export class HuntMode {
highlighted = false;
}
let color = C.BG_FLOOR;
let color = BG_INSET;
if (highlighted) {
color = C.FG_TEXT;
color = FG_TEXT;
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, () => {
D.drawSprite(sprite, new Point(192, 192), 1, {
D.drawSprite(sprThrallLore, new Point(192, 192), 1, {
xScale: this.faceLeft ? -2 : 2,
yScale: 2,
});
@ -425,18 +449,18 @@ export class HuntMode {
D.fillRect(
cellOffset.offset(new Point(-4, -4)),
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()) {
if (this.#blocksMovement(cell.top)) {
return bbox.getContactWithRect(cell)!;
return true;
}
}
return null;
return false;
}
#blocksMovement(xy: Point) {
@ -465,7 +489,3 @@ export function getHuntMode() {
}
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 { 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);

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import { Grid, Point, Size } from "./engine/datatypes.ts";
import { Pickup } from "./pickups.ts";
import { Skill } from "./datatypes.ts";
import { Microtheme } from "./colors.ts";
export enum Architecture {
Wall,
@ -31,25 +30,21 @@ export class LoadedNewMap {
#id: string;
#size: Size;
#entrance: Point | null;
#imposesBloodCosts: boolean;
#architecture: Grid<Architecture>;
#pickups: Grid<Pickup | null>;
#provinces: Grid<string | null>; // TODO: Does this just duplicate zoneLabels
#revealed: Grid<boolean>;
#zoneLabels: Grid<string | null>;
#microthemes: Grid<Microtheme | null>;
constructor(id: string, size: Size) {
this.#id = id;
this.#size = size;
this.#entrance = null;
this.#imposesBloodCosts = true;
this.#architecture = new Grid<Architecture>(size, () => Architecture.Wall);
this.#pickups = new Grid<Pickup | null>(size, () => null);
this.#provinces = new Grid<string | null>(size, () => null);
this.#revealed = new Grid<boolean>(size, () => false);
this.#zoneLabels = new Grid<string | null>(size, () => null);
this.#microthemes = new Grid<Microtheme | null>(size, () => null);
}
set entrance(point: Point) {
@ -63,14 +58,6 @@ export class LoadedNewMap {
return this.#entrance;
}
set imposesBloodCosts(value: boolean) {
this.#imposesBloodCosts = value;
}
get imposesBloodCosts() {
return this.#imposesBloodCosts;
}
get size(): Size {
return this.#size;
}
@ -119,14 +106,6 @@ export class LoadedNewMap {
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 {
const size = this.#size;
let reached = new Grid<boolean>(size, () => false);
@ -227,13 +206,6 @@ export class CellView {
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) {
this.architecture = cell.architecture;
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(
bbox: Circle,
bbox: Rect,
dxy: Point,
getContact: (where: Circle) => Point | null,
blocked: (where: Rect) => boolean,
options?: { bounce?: number },
): { bbox: Circle; displacement: Point; dxy: Point } {
): { bbox: Rect; displacement: Point; dxy: Point } {
let nSteps = 40;
let nRedirections = 40;
let bounce = options?.bounce ?? 0;
let xy = bbox.center;
let redirections = 0;
let xy = bbox.top;
for (let i = 0; i < nSteps; i++) {
let trialXy = xy.offset(new Point(dxy.x / nSteps, dxy.y / nSteps));
let trialBbox = new Circle(trialXy, bbox.radius);
let trialXy = xy.offset(new Point(dxy.x / nSteps, 0));
let trialBbox = new Rect(trialXy, bbox.size);
let contact = getContact(trialBbox);
if (contact) {
let normal = contact.offset(trialXy.negate());
let mag = normal.distance(new Point(0, 0));
let nx = mag == 0 ? 0 : normal.x / mag;
let ny = mag == 0 ? 0 : normal.y / mag;
if (blocked(trialBbox)) {
dxy = new Point(bounce * -dxy.x, dxy.y);
} else {
xy = trialXy;
}
let dot = dxy.x * nx + dxy.y * ny;
if (redirections < nRedirections) {
dxy = new Point(
dxy.x - lerp(bounce, 1, 2) * dot * nx,
dxy.y - lerp(bounce, 1, 2) * dot * ny,
);
i -= 1; // try again with reflection
redirections += 1;
} else {
dxy = new Point(0, 0);
break;
}
trialXy = xy.offset(new Point(0, dxy.y / nSteps));
trialBbox = new Rect(trialXy, bbox.size);
if (blocked(trialBbox)) {
dxy = new Point(dxy.x, bounce * -dxy.y);
} else {
xy = trialXy;
}
}
return {
bbox: new Circle(xy, bbox.radius),
displacement: xy.offset(bbox.center.negate()),
bbox: new Rect(xy, bbox.size),
displacement: xy.offset(bbox.top.negate()),
dxy,
};
}

View File

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

View File

@ -1,17 +1,9 @@
import { ALL_STATS, Skill, Stat, SuccessorOption, Wish } from "./datatypes.ts";
import { getSkills } from "./skills.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 {
#name: string;
#thrallTemplate: number;
#nImprovements: number;
#stats: Record<Stat, number>;
#talents: Record<Stat, number>;
#isInPenance: boolean;
@ -26,65 +18,23 @@ export class PlayerProgress {
#thrallsObtainedItem: number[];
#thrallsDeliveredItem: number[];
constructor(args: NewRoundConfig | SaveFileV1) {
if ("asSuccessor" in args) {
//asSuccessor: SuccessorOption, withWish: Wish | null) {
const config = args as NewRoundConfig;
const asSuccessor = config.asSuccessor;
this.#name = asSuccessor.name;
this.#thrallTemplate = asSuccessor.template.id;
this.#nImprovements = asSuccessor.nImprovements;
this.#stats = { ...asSuccessor.stats };
this.#talents = { ...asSuccessor.talents };
this.#isInPenance = asSuccessor.inPenance;
this.#wish = config.withWish;
this.#exp = 0;
this.#blood = 0;
this.#itemsPurloined = 0;
this.#skillsLearned = [];
this.#untrimmedSkillsAvailable = [];
this.#thrallsUnlocked = [];
this.#thrallDamage = {};
this.#thrallsObtainedItem = [];
this.#thrallsDeliveredItem = [];
constructor(asSuccessor: SuccessorOption, withWish: Wish | null) {
this.#name = asSuccessor.name;
this.#stats = { ...asSuccessor.stats };
this.#talents = { ...asSuccessor.talents };
this.#isInPenance = asSuccessor.inPenance;
this.#wish = withWish;
this.#exp = 0;
this.#blood = 0;
this.#itemsPurloined = 0;
this.#skillsLearned = [];
this.#untrimmedSkillsAvailable = [];
this.#thrallsUnlocked = [];
this.#thrallDamage = {};
this.#thrallsObtainedItem = [];
this.#thrallsDeliveredItem = [];
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;
}
this.refill();
}
applyEndOfTurn() {
@ -97,20 +47,12 @@ export class PlayerProgress {
return this.#name;
}
get template(): Thrall {
return { id: this.#thrallTemplate };
}
get nImprovements(): number {
return this.#nImprovements;
}
get isInPenance(): boolean {
return this.#isInPenance;
}
refill() {
this.#blood = 1000;
this.#blood = 2000;
let learnableSkills = []; // TODO: Also include costing info
for (let skill of getSkills()
@ -252,11 +194,7 @@ export class PlayerProgress {
return skillsAvailable.slice(0, 6);
}
getUntrimmedAvailableSkillIds(): number[] {
return this.#untrimmedSkillsAvailable.map((s) => s.id);
}
getLearnedSkills(): Skill[] {
getLearnedSkills() {
let learnedSkills = [];
for (let s of this.#skillsLearned.values()) {
learnedSkills.push({ id: s });
@ -264,10 +202,6 @@ export class PlayerProgress {
return learnedSkills;
}
getRawLearnedSkills(): number[] {
return [...this.#skillsLearned];
}
getStats() {
return { ...this.#stats };
}
@ -287,10 +221,6 @@ export class PlayerProgress {
return this.#thrallsUnlocked.indexOf(thrall.id) != -1;
}
getUnlockedThrallIds(): number[] {
return [...this.#thrallsUnlocked];
}
damageThrall(thrall: Thrall, amount: number) {
if (amount <= 0.0) {
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;
}
getThrallDamage(thrall: Thrall): number {
return this.#thrallDamage[thrall.id] ?? 0.0;
}
getThrallLifeStage(thrall: Thrall): LifeStage {
let damage = this.#thrallDamage[thrall.id] ?? 0;
if (damage < 0.5) {
@ -332,10 +258,6 @@ export class PlayerProgress {
this.#thrallsObtainedItem.push(thrall.id);
}
getThrallObtainedItemIds(): number[] {
return [...this.#thrallsObtainedItem];
}
deliverThrallItem(thrall: Thrall) {
if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) {
return;
@ -343,10 +265,6 @@ export class PlayerProgress {
this.#thrallsDeliveredItem.push(thrall.id);
}
getThrallDeliveredItemIds(): number[] {
return [...this.#thrallsDeliveredItem];
}
getThrallItemStage(thrall: Thrall): ItemStage {
if (this.#thrallsDeliveredItem.indexOf(thrall.id) != -1) {
return ItemStage.Delivered;
@ -378,11 +296,7 @@ export function initPlayerProgress(
asSuccessor: SuccessorOption,
withWish: Wish | null,
) {
active = new PlayerProgress({ asSuccessor: asSuccessor, withWish: withWish });
}
export function rehydratePlayerProgress(savefile: SaveFileV1) {
active = new PlayerProgress(savefile);
active = new PlayerProgress(asSuccessor, withWish);
}
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,
sceneStare,
sceneStealth,
sceneTrueEnding,
} from "./endings.ts";
import { generateWishes, getWishes, isWishCompleted } from "./wishes.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 domiciles based on overall score
// TODO: Force the rank to match the wish if one existed
else if (vampiricSkills >= 24) {
scene = sceneTrueEnding;
rank = "Master Vampire";
domicile = "Third Clade";
reignSentence = "You know the truth, or at least your character does.";
} else if (isMax("stare", 3)) {
else if (isMax("stare", 3)) {
scene = sceneStare;
rank = "Hypno-Chiropteran";
domicile = "Village of Brainwashed Mortals";
@ -128,10 +122,7 @@ class Scorer {
vampiricSkills,
mortalServants,
};
let successorOptions = generateSuccessors(
getPlayerProgress().nImprovements + 2,
penance,
); // TODO: generate nImprovements from mortalServants and the player's bsae improvements
let successorOptions = generateSuccessors(0, penance); // TODO: generate nImprovements from mortalServants and the player's bsae improvements
let wishOptions = generateWishes(penance);
let progenerateVerb = penance ? "Repent" : "Progenerate";

View File

@ -38,23 +38,16 @@ class SkillsTable {
}
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 governingStatValue = 0;
for (let stat of data.governing.stats.values()) {
governingStatValue +=
(getPlayerProgress().getStat(stat) * _STAT_TO_TRIPS[stat]) /
data.governing.stats.length;
getPlayerProgress().getStat(stat) / data.governing.stats.length;
}
if (data.governing.flipped) {
governingStatValue = -governingStatValue + 1;
governingStatValue = -governingStatValue + 10;
}
let mult = getCostMultiplier(getPlayerProgress().getWish(), skill);
@ -101,7 +94,7 @@ function geomInterpolate(
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 = {
stats: Stat[];
note: string;
@ -165,46 +158,34 @@ function governing(
let cost: number;
let mortalServantValue: number;
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:
underTarget = 0.5;
target = 4.5;
underTarget = 5;
target = 15;
cost = 50;
mortalServantValue = 1;
break;
case 1:
underTarget = 4;
target = 10;
cost = 50;
underTarget = 15;
target = 40;
cost = 100;
mortalServantValue = 2;
break;
case 1.25:
underTarget = 5;
target = 12;
cost = 50;
underTarget = 17;
target = 42;
cost = 100;
mortalServantValue = 2;
break;
case 2:
underTarget = 10;
target = 18;
cost = 75;
underTarget = 30;
target = 70;
cost = 125;
mortalServantValue = 3;
break;
case 3:
underTarget = 14;
target = 23;
cost = 100;
underTarget = 50;
target = 100;
cost = 150;
mortalServantValue = 10;
break;
}
@ -266,7 +247,7 @@ export let bat3 = table.add({
});
export let stealth0 = table.add({
governing: governing("stealth", -0.25),
governing: governing("stealth", 0),
profile: {
name: "Be Quiet",
description:
@ -303,7 +284,7 @@ export let stealth3 = table.add({
});
export let charm0 = table.add({
governing: governing("charm", -0.125),
governing: governing("charm", 0),
profile: {
name: "Flatter",
description:

View File

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

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 {
getPlayerProgress,
initPlayerProgress,
rehydratePlayerProgress,
} from "./playerprogress.ts";
import { getPlayerProgress, initPlayerProgress } from "./playerprogress.ts";
import { getHuntMode, HuntMode, initHuntMode } from "./huntmode.ts";
import { getVNModal } from "./vnmodal.ts";
import { getScorer } from "./scorer.ts";
import { getEndgameModal } from "./endgamemodal.ts";
import { SuccessorOption, Wish } from "./datatypes.ts";
import { generateManor } from "./manormap.ts";
import { 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;
export class StateManager {
#turn: number;
#revision: number;
constructor(file?: SaveFileV1) {
this.#turn = file?.turn ?? 1;
this.#revision = file?.revision ?? 1;
constructor() {
this.#turn = 1;
}
getTurn(): number {
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) {
this.#turn = 1;
initPlayerProgress(asSuccessor, withWish);
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() {
saveGame();
if (this.#turn + 1 <= N_TURNS) {
this.#turn += 1;
getPlayerProgress().applyEndOfTurn();
getPlayerProgress().refill();
initHuntMode(new HuntMode(getHuntMode().depth, generateManor()));
sndSleep.play({ bgm: true });
} else {
sndSilence.play({ bgm: true });
// TODO: Play a specific scene
let ending = getScorer().pickEnding();
getVNModal().play(ending.scene);
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 { choose } from "./utils.ts";
import { getPlayerProgress } from "./playerprogress.ts";
import { photogenicThralls } from "./thralls.ts";
export function generateSuccessors(
nImprovements: number,
@ -36,8 +35,6 @@ export function generateSuccessorFromPlayer(): SuccessorOption {
let progress = getPlayerProgress();
let successor = {
name: progress.name,
template: progress.template,
nImprovements: progress.nImprovements - 2,
title: "Penitent",
note: "Failed at Master's bidding",
stats: { ...progress.getStats() },
@ -55,7 +52,6 @@ export function generateSuccessorFromPlayer(): SuccessorOption {
export function generateSuccessor(nImprovements: number): SuccessorOption {
let name = generateName();
let template = choose(photogenicThralls);
let title = generateTitle();
let note = null;
let stats: Record<Stat, number> = {
@ -79,9 +75,8 @@ export function generateSuccessor(nImprovements: number): SuccessorOption {
talents[choose(ALL_STATS)] += 1;
},
];
let nTotalImprovements = nImprovements;
let mult = 1;
for (let i = 0; i < nTotalImprovements * mult; i++) {
let nTotalImprovements = nImprovements + 5;
for (let i = 0; i < nTotalImprovements; i++) {
let improvement =
improvements[Math.floor(Math.random() * improvements.length)];
improvement();
@ -90,16 +85,5 @@ export function generateSuccessor(nImprovements: number): SuccessorOption {
let skills: Skill[] = [];
let inPenance = false;
let isCompulsory = false;
return {
name,
template,
nImprovements,
title,
note,
stats,
talents,
skills,
inPenance,
isCompulsory,
};
return { name, title, note, stats, talents, skills, inPenance, isCompulsory };
}

View File

@ -22,7 +22,7 @@ import {
sprThrallStealth,
} from "./sprites.ts";
import { Sprite } from "./engine/internal/sprite.ts";
import { Stat } from "./datatypes.ts";
import {Stat} from "./datatypes.ts";
export type Thrall = {
id: number;
@ -52,15 +52,10 @@ class ThrallsTable {
}
return thralls;
}
get length(): number {
return this.#thralls.length;
}
}
export type ThrallData = {
label: string;
sprite: Sprite;
hitboxSize: number;
posterCheck: CheckData;
initialCheck: CheckData;
itemHint: string;
@ -101,14 +96,9 @@ export function getThralls() {
// Their initial check is, generally, the initial check of the
// thrall n-2 or thrall n+1 (ex: Party's initial check is Stealth
// 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({
label: "Garrett",
sprite: sprThrallParty,
hitboxSize: 0.7,
posterCheck: {
label:
"This room would be perfect for someone with an ostensibly managed gambling addiction.",
@ -120,19 +110,18 @@ export let thrallParty = table.add({
options: [
{
skill: () => stealth1, // Disguise
locked: '"What\'s up?"',
locked: '"What\'s wrong, Garrett?"',
failure:
"\"If you're not a large pile of money, don't talk to me.\"\n\nHe sobs into his ice cream.",
unlockable: "*look like a large pile of money*",
success: "He scoops you eagerly into his wallet.",
},
{
skill: () => charm0, // Flatter
locked: "Ask him how much he's winning",
failure: 'He rolls his eyes at you. "A billion."',
unlockable: "Tell him he's cute",
success:
"He looks at you like no one has ever told him that before, and blushes. You hold his wing and feel his pulse rise.",
skill: () => lore0, // Respect Elders
locked: "TODO",
failure: "TODO",
unlockable: "TODO",
success: "TODO",
},
],
},
@ -141,12 +130,10 @@ export let thrallParty = table.add({
itemPickupMessage:
"This antique wedding ring looks like it was worth at least fifty big blinds.",
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!",
rewardCallback: (spawn) => {
for (let i = 0; i < 12; i++) {
spawn("INT");
}
for (let i = 0; i < 30; i++) { spawn("INT"); }
},
lifeStageText: {
fresh: {
@ -183,7 +170,6 @@ export let thrallParty = table.add({
export let thrallLore = table.add({
label: "Lupin",
sprite: sprThrallLore,
hitboxSize: 0.65,
posterCheck: {
label:
"This room would be perfect for someone with a love of nature and screaming.",
@ -195,17 +181,16 @@ export let thrallLore = table.add({
options: [
{
skill: () => stare1, // Hypnotize
locked: '"I\'m a wolf too."',
failure: '"AROO?" He shakes his head no.',
unlockable: 'Zonk him. "I\'m a wolf now."',
locked: "TODO",
failure: "TODO",
unlockable: '"I\'m a wolf too."',
success:
"He blinks a few times under your gaze -- then touches your muzzle -- then his own -- then arfs submissively.",
},
{
skill: () => bat0, // Screech
locked: "Try to yowl",
failure:
"He sniffs at your collar. You don't _seem_ like a Wolf Scout.",
locked: "TODO",
failure: "TODO",
unlockable: '"Wolf Scouts AWOO!"',
success:
"Taken aback at how well you know the cheer, he freezes -- then joins you with a similar howl.",
@ -217,12 +202,10 @@ export let thrallLore = table.add({
itemPickupMessage:
"You can't see yourself in this antique silver mirror. On the other hand, they say silver is effective against wolves.",
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!",
rewardCallback: (spawn) => {
for (let i = 0; i < 12; i++) {
spawn("AGI");
}
for (let i = 0; i < 30; i++) { spawn("AGI"); }
},
lifeStageText: {
fresh: {
@ -257,31 +240,29 @@ export let thrallLore = table.add({
export let thrallBat = table.add({
label: "Monica",
sprite: sprThrallBat,
hitboxSize: 0.5,
posterCheck: {
label: "This room would be perfect for some kind of television chef.",
options: [],
},
initialCheck: {
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: [
{
skill: () => party1, // Rave
locked: "Act weird to get her attention",
failure: '"I -- you -- you know we\'re not being filmed, right?"',
unlockable: "Flash your eyes like a TV camera",
locked: "TODO",
failure: "TODO",
unlockable: "Slide her a sachet of cocaine.",
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
locked: '"That looks good."',
failure:
"\"It's not.\" She eats it with such relish it's hard to tell.",
unlockable: "Not as good as they used to be",
skill: () => charm0, // Flatter
locked: "TODO",
failure: "TODO",
unlockable: '"You\'re the best cook ever!"',
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:
"This particular instance of gator food resembles an infamous Aotearoan entree: colonial goose.",
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!",
rewardCallback: (spawn) => {
for (let i = 0; i < 8; i++) {
spawn("CHA");
}
for (let i = 0; i < 4; i++) {
spawn("INT");
}
for (let i = 0; i < 15; i++) { spawn("CHA"); }
for (let i = 0; i < 15; i++) { spawn("INT"); }
},
lifeStageText: {
fresh: {
@ -333,7 +310,6 @@ export let thrallBat = table.add({
export let thrallCharm = table.add({
label: "Renfield",
sprite: sprThrallCharm,
hitboxSize: 0.85,
posterCheck: {
label:
"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: [
{
skill: () => lore1, // Brick by Brick
locked: `Ask for vampire facts`,
failure: `He eagerly describes the cape, the coffin, the crypt, the castle -- a whole lot of stuff you don't necessarily don't have.`,
unlockable: `Talk about your manor`,
locked: "TODO",
failure: "TODO",
unlockable: '"Wanna see my crypt?"',
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
locked: "Get your fangs in his face",
failure:
'"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",
locked: "TODO",
failure: "TODO",
unlockable: "Say absolutely nothing.",
success:
"His mind overflows with fantasy, and when you let a glint of fang peek through, he claps his arms affectionately around your supercold torso.",
},
@ -366,12 +341,10 @@ export let thrallCharm = table.add({
itemPickupMessage:
"Your photo is going to be in a lot of places if it gets out, but you've got the original.",
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!",
rewardCallback: (spawn) => {
for (let i = 0; i < 12; i++) {
spawn("PSI");
}
for (let i = 0; i < 24; i++) { spawn("PSI"); }
},
lifeStageText: {
fresh: {
@ -406,7 +379,6 @@ export let thrallCharm = table.add({
export let thrallStealth = table.add({
label: "Narthyss",
sprite: sprThrallStealth,
hitboxSize: 0.85,
posterCheck: {
label: "This room would be perfect for someone who can breathe fire.",
options: [],
@ -417,21 +389,17 @@ export let thrallStealth = table.add({
options: [
{
skill: () => bat1, // Flap
locked: "Drink cherry soda with your fangs",
failure:
'"Wow! That\'s incredibly cool," she says. Her eyes scan the other patrons. "Can I get you anything?"',
unlockable: "Hang upside-down and offer a drink",
success:
'"You\'re ADORABLE!" Her attention is yours alone and she offers to follow you home.',
locked: "TODO",
failure: "TODO",
unlockable: "Hang upside-down and offer her a martini.",
success: "\"You're ADORABLE!\" She's yours forever.",
},
{
skill: () => stare0, // Dazzle
locked: "Show her some sparks",
failure:
'"Neat trick!" she says. She blinds you with a glowstick, then heads back into the crowd.',
unlockable: "Trap her in a tunnel of light",
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.",
locked: "TODO",
failure: "TODO",
unlockable: "TODO",
success: "TODO",
},
],
},
@ -440,15 +408,11 @@ export let thrallStealth = table.add({
itemPickupMessage:
"The freezer is empty except for this frozen kobold, who mutters something about collecting blood for its master.",
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!",
rewardCallback: (spawn) => {
for (let i = 0; i < 8; i++) {
spawn("CHA");
}
for (let i = 0; i < 4; i++) {
spawn("AGI");
}
for (let i = 0; i < 15; i++) { spawn("CHA"); }
for (let i = 0; i < 15; i++) { spawn("AGI"); }
},
lifeStageText: {
fresh: {
@ -483,7 +447,6 @@ export let thrallStealth = table.add({
export let thrallStare = table.add({
label: "Ridley",
sprite: sprThrallStare,
hitboxSize: 0.85,
posterCheck: {
label: "This room would be perfect for a soulless robot.",
options: [],
@ -497,18 +460,15 @@ export let thrallStare = table.add({
locked: "\"How many Rs in 'strawberry'?\"",
failure:
"It generates an image of a sad fruit shrugging in a muddy plantation.",
unlockable: '"Do you want to come home with me?"',
success:
"It generates an image of a happy robot riding the shoulders of a tiny little bat.",
unlockable: "TODO",
success: "TODO",
},
{
skill: () => party0, // Chug
locked: "Check its fluid levels",
failure:
"It submits to your examination, revealing ordinary quantities of robot blood, robot lubricant, Ener-G, and headlight fluid.",
unlockable: "Drink its battery",
success:
"The sulfuric acid doesn't burn you. This somehow makes Ridley want to hang out with you.\n\n(Ridley is weird.)",
locked: "TODO",
failure: "TODO",
unlockable: "Drink a whole bottle of ink.",
success: "TODO",
},
],
},
@ -517,12 +477,10 @@ export let thrallStare = table.add({
itemPickupMessage:
"This glinting gear would be perfect for a malfunctioning robot.",
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!",
rewardCallback: (spawn) => {
for (let i = 0; i < 12; i++) {
spawn("EXP");
}
for (let i = 0; i < 6; i++) { spawn("EXP"); }
},
lifeStageText: {
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,
thrallStealth,
} from "./thralls.ts";
import {
Microtheme,
MICROTHEME_GREEN,
MICROTHEME_NEON,
MICROTHEME_PINK,
MICROTHEME_PURPLE_TAN,
MICROTHEME_RED,
MICROTHEME_TEAL,
} from "./colors.ts";
export type VaultTemplate = {
microtheme: () => Microtheme | null;
stats: { primary: Stat; secondary: Stat };
roomLabels: {
hall: string;
@ -55,7 +45,6 @@ export type VaultTemplate = {
export const standardVaultTemplates: VaultTemplate[] = [
{
// zoo
microtheme: () => MICROTHEME_GREEN,
stats: { primary: "AGI", secondary: "PSI" },
roomLabels: {
hall: "Zoo",
@ -108,7 +97,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
},
{
// blood bank
microtheme: () => MICROTHEME_RED,
stats: { primary: "AGI", secondary: "INT" },
roomLabels: {
hall: "Blood Bank",
@ -160,7 +148,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
},
{
// coffee shop
microtheme: () => MICROTHEME_PINK,
stats: { primary: "PSI", secondary: "CHA" },
roomLabels: {
hall: "Coffee Shop",
@ -213,7 +200,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
},
{
// optometrist
microtheme: () => MICROTHEME_TEAL,
stats: { primary: "PSI", secondary: "PSI" },
roomLabels: {
hall: "Optometrist",
@ -266,7 +252,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
},
{
// club,
microtheme: () => MICROTHEME_NEON,
stats: { primary: "CHA", secondary: "PSI" },
roomLabels: {
hall: "Nightclub",
@ -319,7 +304,6 @@ export const standardVaultTemplates: VaultTemplate[] = [
},
{
// library
microtheme: () => MICROTHEME_PURPLE_TAN,
stats: { primary: "INT", secondary: "CHA" },
roomLabels: {
hall: "Library",

View File

@ -1,13 +1,8 @@
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 { 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 HEIGHT = 384;
@ -90,13 +85,7 @@ interface SceneCathexis {
function createCathexis(part: VNScenePart): SceneCathexis {
switch (part.type) {
case "message":
part?.sfx?.play({ volume: 0.5 });
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;
this.#gotOneFrame = true;
if (!firstFrame && I.isMouseClicked("leftMouse")) {
// TODO: SFX
if (!firstFrame && I.isAnythingPressed()) {
this.#done = true;
}
}
draw() {
D.drawText(
this.#message.text,
new Point(WIDTH / 2, HEIGHT / 2),
C.FG_BOLD,
{
alignX: AlignX.Center,
alignY: AlignY.Middle,
forceWidth: WIDTH,
},
);
D.drawText(this.#message.text, new Point(WIDTH / 2, HEIGHT / 2), FG_BOLD, {
alignX: AlignX.Center,
alignY: AlignY.Middle,
forceWidth: WIDTH,
});
}
}
@ -142,104 +127,3 @@ let active: VNModal = new VNModal();
export function getVNModal() {
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 = {
type: "message";
text: string;
sfx?: Sound;
sfx?: string;
};
export type VNSceneCallback = {
type: "callback";
callback: () => void;
};
export type VNSceneSaveGameScreen = {
type: "saveGameScreen";
file: SaveFileV1 | null;
error: string | null;
};
export type VNSceneBasisPart = string | VNSceneMessage | VNSceneCallback;
export type VNSceneBasisPart = string | VNSceneMessage;
export type VNSceneBasis = VNSceneBasisPart[];
export type VNScenePart =
| VNSceneMessage
| VNSceneCallback
| VNSceneSaveGameScreen;
export type VNScenePart = VNSceneMessage;
export type VNScene = VNScenePart[];
export function compile(basis: VNSceneBasis): VNScene {

View File

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

View File

@ -1,7 +1,12 @@
import { Color, Grid, Point, Rect, Size } from "./engine/datatypes.ts";
import { DrawPile } from "./drawpile.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 {
#grid: Grid<Element3D>;
@ -22,7 +27,7 @@ export class World3D {
if (here == null) {
drawpile.add(OFFSET_TOP, () => {
gridArt.drawCeiling(C.BG_CEILING);
gridArt.drawCeiling(BG_CEILING);
});
return;
}
@ -130,6 +135,6 @@ export class 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": {
"target": "ES2023",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */

View File

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