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 = { 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 = { 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; }