486 lines
12 KiB
TypeScript
486 lines
12 KiB
TypeScript
import {
|
|
Skill,
|
|
SkillData,
|
|
SkillGoverning,
|
|
SkillScoring,
|
|
Stat,
|
|
} from "./datatypes.ts";
|
|
import { getPlayerProgress } from "./playerprogress.ts";
|
|
import { getCostMultiplier } from "./wishes.ts";
|
|
|
|
class SkillsTable {
|
|
#skills: SkillData[];
|
|
|
|
constructor() {
|
|
this.#skills = [];
|
|
}
|
|
|
|
add(data: SkillData): Skill {
|
|
let id = this.#skills.length;
|
|
this.#skills.push(data);
|
|
return { id };
|
|
}
|
|
|
|
get(skill: Skill): SkillData {
|
|
return this.#skills[skill.id];
|
|
}
|
|
|
|
getAvailableSkills(includeDegrading: boolean): Skill[] {
|
|
let skills = [];
|
|
for (let i = 0; i < this.#skills.length; i++) {
|
|
let isDegrading = this.#skills[i].isDegrading ?? false;
|
|
if (isDegrading && !includeDegrading) {
|
|
continue;
|
|
}
|
|
skills.push({ id: i });
|
|
}
|
|
return skills;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (data.governing.flipped) {
|
|
governingStatValue = -governingStatValue + 1;
|
|
}
|
|
|
|
let mult = getCostMultiplier(getPlayerProgress().getWish(), skill);
|
|
let [underTarget, target] = [
|
|
data.governing.underTarget,
|
|
data.governing.target,
|
|
];
|
|
underTarget = mult * underTarget;
|
|
target = mult * target;
|
|
|
|
return Math.floor(
|
|
geomInterpolate(
|
|
governingStatValue,
|
|
underTarget,
|
|
target,
|
|
data.governing.cost,
|
|
999,
|
|
),
|
|
);
|
|
}
|
|
|
|
isAtMinimum(skill: Skill) {
|
|
let minimumCost = this.get(skill).governing.cost;
|
|
let currentCost = this.computeCost(skill);
|
|
return currentCost <= minimumCost;
|
|
}
|
|
}
|
|
|
|
function geomInterpolate(
|
|
x: number,
|
|
lowIn: number,
|
|
highIn: number,
|
|
lowOut: number,
|
|
highOut: number,
|
|
) {
|
|
if (x < lowIn) {
|
|
return highOut;
|
|
}
|
|
if (x >= highIn) {
|
|
return lowOut;
|
|
}
|
|
|
|
const proportion = 1.0 - (x - lowIn) / (highIn - lowIn);
|
|
return lowOut * Math.pow(highOut / lowOut, proportion);
|
|
}
|
|
|
|
type Difficulty = -0.25 | -0.125 | 0 | 1 | 1.25 | 2 | 3;
|
|
type GoverningTemplate = {
|
|
stats: Stat[];
|
|
note: string;
|
|
scoring: SkillScoring;
|
|
};
|
|
|
|
type Track =
|
|
| "bat"
|
|
| "stealth"
|
|
| "charm"
|
|
| "stare"
|
|
| "party"
|
|
| "lore"
|
|
| "penance";
|
|
let templates: Record<Track, GoverningTemplate> = {
|
|
bat: {
|
|
stats: ["AGI", "AGI", "PSI"],
|
|
note: "Cheaper with AGI and PSI.",
|
|
scoring: { bat: 1 },
|
|
},
|
|
stealth: {
|
|
stats: ["AGI", "AGI", "INT"],
|
|
note: "Cheaper with AGI and INT.",
|
|
scoring: { stealth: 1 },
|
|
},
|
|
charm: {
|
|
stats: ["CHA", "PSI", "PSI"],
|
|
note: "Cheaper with CHA and PSI.",
|
|
scoring: { charm: 1 },
|
|
},
|
|
stare: {
|
|
stats: ["PSI", "PSI"],
|
|
note: "Cheaper with PSI.",
|
|
scoring: { stare: 1 },
|
|
},
|
|
party: {
|
|
stats: ["CHA", "CHA", "PSI"],
|
|
note: "Cheaper with CHA and PSI.",
|
|
scoring: { party: 1 },
|
|
},
|
|
lore: {
|
|
stats: ["INT", "INT", "CHA"],
|
|
note: "Cheaper with INT and CHA.",
|
|
scoring: { lore: 1 },
|
|
},
|
|
penance: {
|
|
stats: ["AGI", "INT", "CHA", "PSI"],
|
|
note: "Lower your stats for this.",
|
|
scoring: {},
|
|
},
|
|
};
|
|
|
|
function governing(
|
|
track: Track,
|
|
difficulty: Difficulty,
|
|
flipped?: boolean,
|
|
): SkillGoverning {
|
|
let template = templates[track];
|
|
let underTarget: number;
|
|
let target: number;
|
|
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;
|
|
cost = 50;
|
|
mortalServantValue = 1;
|
|
break;
|
|
case 1:
|
|
underTarget = 4;
|
|
target = 10;
|
|
cost = 50;
|
|
mortalServantValue = 2;
|
|
break;
|
|
case 1.25:
|
|
underTarget = 5;
|
|
target = 12;
|
|
cost = 50;
|
|
mortalServantValue = 2;
|
|
break;
|
|
case 2:
|
|
underTarget = 10;
|
|
target = 18;
|
|
cost = 75;
|
|
mortalServantValue = 3;
|
|
break;
|
|
case 3:
|
|
underTarget = 14;
|
|
target = 23;
|
|
cost = 100;
|
|
mortalServantValue = 10;
|
|
break;
|
|
}
|
|
|
|
if (flipped) {
|
|
mortalServantValue = -mortalServantValue;
|
|
}
|
|
|
|
return {
|
|
stats: template.stats,
|
|
underTarget: underTarget,
|
|
target: target,
|
|
cost: cost,
|
|
note: template.note,
|
|
scoring: template.scoring,
|
|
mortalServantValue: mortalServantValue,
|
|
flipped: flipped ?? false,
|
|
};
|
|
}
|
|
|
|
let table = new SkillsTable();
|
|
|
|
export let bat0 = table.add({
|
|
isDegrading: false,
|
|
governing: governing("bat", 0),
|
|
profile: {
|
|
name: "Screech",
|
|
description:
|
|
"Energy fills your body. You can't help but let go. It just feels so good to let that sound rip through you.",
|
|
},
|
|
prereqs: [],
|
|
});
|
|
export let bat1 = table.add({
|
|
governing: governing("bat", 1),
|
|
profile: {
|
|
name: "Flap",
|
|
description:
|
|
"Sailing on your cloak is pleasurable, but it's better still to shake your limbs and FIGHT the wind.",
|
|
},
|
|
prereqs: [bat0],
|
|
});
|
|
export let bat2 = table.add({
|
|
governing: governing("bat", 2),
|
|
profile: {
|
|
name: "Transform",
|
|
description:
|
|
"Bare your fangs and let them out further than normal. Your nose wrinkles. You have a SNOUT??",
|
|
},
|
|
prereqs: [bat1],
|
|
});
|
|
export let bat3 = table.add({
|
|
governing: governing("bat", 3),
|
|
profile: {
|
|
name: "Eat Bugs",
|
|
description:
|
|
"This is the forbidden pleasure. It supersedes even blood. Go on -- have a bite -- CRUNCH!",
|
|
},
|
|
prereqs: [bat2],
|
|
});
|
|
|
|
export let stealth0 = table.add({
|
|
governing: governing("stealth", -0.25),
|
|
profile: {
|
|
name: "Be Quiet",
|
|
description:
|
|
"There's a thing in the brain that _wants_ to be caught. Mortals have it, vampires don't.",
|
|
},
|
|
prereqs: [],
|
|
});
|
|
export let stealth1 = table.add({
|
|
governing: governing("stealth", 1),
|
|
profile: {
|
|
name: "Disguise",
|
|
description:
|
|
"First impressions: what you want them to see, they'll see. Just meet their gaze and start talking.",
|
|
},
|
|
prereqs: [stealth0],
|
|
});
|
|
export let stealth2 = table.add({
|
|
governing: governing("stealth", 2),
|
|
profile: {
|
|
name: "Sneak",
|
|
description:
|
|
"Your unnatural pallor _should_ make you bright against the shadow. But it likes you, so you fade.",
|
|
},
|
|
prereqs: [stealth1],
|
|
});
|
|
export let stealth3 = table.add({
|
|
governing: governing("stealth", 3),
|
|
profile: {
|
|
name: "Turn Invisible",
|
|
description:
|
|
"No one sees any more of you than you'd like. You're as ghostly as your own reflection.",
|
|
},
|
|
prereqs: [stealth2],
|
|
});
|
|
|
|
export let charm0 = table.add({
|
|
governing: governing("charm", -0.125),
|
|
profile: {
|
|
name: "Flatter",
|
|
description:
|
|
"No matter how weird you're being, people like praise. Praise in your voice has an intoxicating quality.",
|
|
},
|
|
prereqs: [],
|
|
});
|
|
export let charm1 = table.add({
|
|
governing: governing("charm", 1),
|
|
profile: {
|
|
name: "Befriend",
|
|
description:
|
|
"Cute: they think they've met the real you. They're even thinking about you when you're not around.",
|
|
},
|
|
prereqs: [charm0],
|
|
});
|
|
export let charm2 = table.add({
|
|
governing: governing("charm", 2),
|
|
profile: {
|
|
name: "Seduce",
|
|
description:
|
|
'Transfix them long and deep enough for them to realize how much they want you. "No" isn\'t "no" anymore.',
|
|
},
|
|
prereqs: [charm1],
|
|
});
|
|
export let charm3 = table.add({
|
|
governing: governing("charm", 3),
|
|
profile: {
|
|
name: "Infatuate",
|
|
description:
|
|
"They were into mortals once. Now they can't get off without the fangs. The eyes. The pale dead flesh.",
|
|
},
|
|
prereqs: [charm2],
|
|
});
|
|
export let stare0 = table.add({
|
|
governing: governing("stare", 0),
|
|
profile: {
|
|
name: "Dazzle",
|
|
description:
|
|
"Your little light show can reduce anyone to a puddle of their own fluids. Stare and they give in instantly.",
|
|
},
|
|
prereqs: [],
|
|
});
|
|
export let stare1 = table.add({
|
|
governing: governing("stare", 1),
|
|
profile: {
|
|
name: "Hypnotize",
|
|
description:
|
|
'Say "sleep" and the mortal falls asleep. That is not a person: just a machine that acts when you require it.',
|
|
},
|
|
prereqs: [stare0],
|
|
});
|
|
export let stare2 = table.add({
|
|
governing: governing("stare", 2),
|
|
profile: {
|
|
name: "Enthrall",
|
|
description:
|
|
"Everyone's mind has room for one master. Reach into the meek fractured exterior and mean for it to be you.",
|
|
},
|
|
prereqs: [stare1],
|
|
});
|
|
export let stare3 = table.add({
|
|
governing: governing("stare", 3),
|
|
profile: {
|
|
name: "Seal Memory",
|
|
description:
|
|
"There was no existence before you and will be none after. Your mortals cannot imagine another existence.",
|
|
},
|
|
prereqs: [stare2],
|
|
});
|
|
export let party0 = table.add({
|
|
governing: governing("party", 0),
|
|
profile: {
|
|
name: "Chug",
|
|
description:
|
|
'This undead body can hold SO MUCH whiskey. (BRAAAAP.) "You, mortal -- fetch me another drink!"',
|
|
},
|
|
prereqs: [],
|
|
});
|
|
export let party1 = table.add({
|
|
governing: governing("party", 1),
|
|
profile: {
|
|
name: "Rave",
|
|
description:
|
|
"You could jam glowsticks in your hair, but your eyes are a lot brighter. And they pulse with the music.",
|
|
},
|
|
prereqs: [party0],
|
|
});
|
|
export let party2 = table.add({
|
|
governing: governing("party", 2),
|
|
profile: {
|
|
name: "Peer Pressure",
|
|
description:
|
|
"Partying: it gets you out of your head. Makes you do things you wouldn't normally do. Controls you.",
|
|
},
|
|
prereqs: [party1],
|
|
});
|
|
export let party3 = table.add({
|
|
governing: governing("party", 3),
|
|
profile: {
|
|
name: "Sleep It Off",
|
|
description:
|
|
"Feels good. Never want it to end. But sober up. These feelings aren't for you. They're for your prey.",
|
|
},
|
|
prereqs: [party2],
|
|
});
|
|
export let lore0 = table.add({
|
|
governing: governing("lore", 0),
|
|
profile: {
|
|
name: "Respect Elders",
|
|
description:
|
|
"You're told not to bother learning much. The test is not to believe that. Bad vampires _disappear_.",
|
|
},
|
|
prereqs: [],
|
|
});
|
|
export let lore1 = table.add({
|
|
governing: governing("lore", 1),
|
|
profile: {
|
|
name: "Brick by Brick",
|
|
description:
|
|
'Vampire history is a mix of fact and advice. Certain tips -- "live in a castle" -- seem very concrete.',
|
|
},
|
|
prereqs: [lore0],
|
|
});
|
|
export let lore2 = table.add({
|
|
governing: governing("lore", 2),
|
|
profile: {
|
|
name: "Make Wine",
|
|
description:
|
|
"Fruit bats grow the grapes. Insectivores fertilize the soil. What do vampire bats do? Is this a metaphor?",
|
|
},
|
|
prereqs: [lore1],
|
|
});
|
|
export let lore3 = table.add({
|
|
governing: governing("lore", 3),
|
|
profile: {
|
|
name: "Third Clade",
|
|
description:
|
|
"Mortals love the day. They hate the night. There's a night deeper than any night that cannot be discussed.",
|
|
},
|
|
prereqs: [lore2],
|
|
});
|
|
|
|
export let sorry0 = table.add({
|
|
isDegrading: true,
|
|
governing: governing("penance", 0, true),
|
|
profile: {
|
|
name: "I'm Sorry",
|
|
description: "You really hurt your Master, you know? Shame on you.",
|
|
},
|
|
prereqs: [],
|
|
});
|
|
|
|
export let sorry1 = table.add({
|
|
isDegrading: true,
|
|
governing: governing("penance", 1, true),
|
|
profile: {
|
|
name: "I'm So Sorry",
|
|
description:
|
|
"You should have known better! You should have done what you were told.",
|
|
},
|
|
prereqs: [],
|
|
});
|
|
|
|
export let sorry2 = table.add({
|
|
isDegrading: true,
|
|
// difficulty 2 is genuinely brutal
|
|
governing: governing("penance", 1.25, true),
|
|
profile: {
|
|
name: "Forgive Me",
|
|
description:
|
|
"Nothing you say will ever be enough to make up for your indiscretion.",
|
|
},
|
|
prereqs: [],
|
|
});
|
|
|
|
export function getSkills(): SkillsTable {
|
|
return table;
|
|
}
|