Fable will fix it! Pt 2

This commit is contained in:
2026-06-11 21:32:00 -07:00
parent a7cab7bcb6
commit a074ff77b1
44 changed files with 1547 additions and 4505 deletions

View File

@@ -1,5 +0,0 @@
<script>
let count = $state(0)
</script>
<button type="button" class="counter" onclick={() => count++}>Count is {count}</button>

27
frontend/src/lib/cards.js Normal file
View File

@@ -0,0 +1,27 @@
// Card-code helpers shared across components.
// Codes look like "10D", "KH", "Joker1" (black) / "Joker2" (red).
export const SUIT_EMOJI = { C: '♣️', S: '♠️', H: '♥️', D: '♦️' };
export function isJoker(code) {
return !!code && code.startsWith('Joker');
}
// "10D" -> "10"
export function cardValue(code) {
return code.slice(0, -1);
}
// "10D" -> "D"
export function cardSuit(code) {
return code.slice(-1);
}
export function getCardDisplay(code) {
if (!code) return '';
if (isJoker(code)) return '🃏 JOKER';
return `${cardValue(code)}${SUIT_EMOJI[cardSuit(code)] || cardSuit(code)}`;
}
export function playerName(players, id) {
return players.find(p => p.id === id)?.name || '???';
}

View File

@@ -1,3 +1,5 @@
import { apiRequest } from './api';
// Auto-generated math-pirate suggestions pool for character creation
const SUGGESTIONS = {
"look": [
@@ -1211,227 +1213,42 @@ const SUGGESTIONS = {
"They stubbornly argues about the slide rule and has the abacus tattoos to prove it.",
"They accidently misplaces the dry gunpowder bags and has a certificate from the Stowaway Academy.",
"They frequently drops the slide rule but gets confused by negative numbers."
],
"techniques": [
"I meant to do that",
"The floor was always a trap",
"Carlos owes me one",
"Barry already disarmed it",
"Jaspar took the blame",
"Greg always comes back",
"We left Greg on purpose",
"The seagulls work for me",
"My whiskers told me so",
"I was behind you the whole time",
"I am wearing two eyepatches",
"Nobody suspects the small one",
"I licked it, so it's mine",
"The grog made me do it",
"I know a guy",
"The guy knows another guy",
"I greased the entire ship",
"I hid the cannonball in my cheeks",
"I taught the parrot everything it knows",
"Hold my grog",
"Watch this",
"I counted to Pi",
"Pi is exactly three when I'm in a hurry",
"I cast Pie, again",
"I memorized the wrong map on purpose",
"I sneezed at exactly the right moment",
"I read about this in a book I ate",
"I learned this from a ghost",
"The barrel was empty on purpose",
"This is my emotional support cannon",
"That wasn't even my final form",
"I've been holding my breath since Tuesday",
"It's a rental",
"The ship likes me better",
"My cousin is a cannonball",
"My scars spell out a map",
"My other gat is a crossbow",
"I prepared this speech in advance",
"I read the rulebook upside down",
"Technically, that's legal",
"Objection!",
"It's called a heist now",
"You wouldn't hit a rat with glasses",
"These aren't even my real glasses",
"I have the high ground",
"You activated my trap card",
"It was me all along",
"Behold, a distraction",
"We meet again, stairs",
"I've seen this in a dream",
"The prophecy said nothing about Tuesdays",
"Catch!",
"Think fast",
"Look behind you",
"No, seriously, look behind you",
"This one goes to eleven",
"Critical squeak",
"Nothing up my sleeves but more sleeves",
"The anchor was a decoy",
"Six rats in a coat",
"The Deep blinked first",
"The Squid owed me a favor",
"The Albatross vouches for me",
"The Mermaid pinky-swore",
"It worked in the bathtub",
"We rehearsed this exactly once",
"The smell did the negotiating",
"Carry me, I'm dramatic",
"It's only stealing if you get caught",
"Borrowed permanently",
"Never trust a dry deck",
"Never trust a barefoot vampire",
"Never duel before lunch",
"Never sing the second verse",
"Never play cards with a Goblin",
"Never wake a sleeping Bean Whale",
"Never bite the hand that feeds you cheese",
"Never bring a sword to a math fight",
"A rat always has an exit",
"Two exits, actually",
"A wet rat fears no rain",
"A captain never looks up",
"A good plan smells like cheese",
"The loudest rat is rarely the problem",
"Dead men file no complaints",
"The best sword is a longer sword",
"If you can't tie a knot, tie a lot",
"When in doubt, scream and leap",
"Every storm is someone's fault",
"All maps lead to treasure if you're stubborn enough",
"The sea forgives nothing but forgets everything",
"Steal the boat first, the hearts will follow",
"You miss every shot you don't blame on the wind",
"There's no such thing as too much rope",
"It is always squid o'clock somewhere",
"Fortune favors the furry",
"Don't count your doubloons until they're stolen",
"Cheese before glory",
"Glory before breakfast",
"Gravity is a suggestion",
"Rule one: there are no rules",
"Rule two: see rule one",
"The knot unties itself if you believe",
"Smile until they get nervous",
"Just keep nodding",
"Pretend it's Tuesday",
"Speak softly and carry a loud gat",
"Improvise, adapt, overboard",
"Cry first, ask questions later",
"The tide pays its debts",
"Apologize mid-swing",
"Bite first, monologue later",
"Lose dramatically, win quietly",
"Fight like the ghosts are watching",
"Trip over the right thing",
"Be the cannonball",
"Always fall on the soft pirate",
"Flee now, gloat later",
"Dramatic timing is a weapon",
"Cheese knows the way home",
"Trust the mustache",
"The plank is a state of mind",
"Every plank is a trapdoor if you stomp hard enough",
"The pointy end goes in the other guy",
"An apology and a gat beats an apology",
"It's not a bribe, it's a gift",
"The fourth wall is load-bearing",
"Vampires can't resist counting spilled rice",
"Distract them with interpretive dance",
"Sing the shanty of unreasonable confidence",
"The Cheddar Gambit",
"The Gouda Guillotine",
"The Brie Breach",
"The Camembert Cannonade",
"The Reverse Plank Walk",
"The Drunken Compass Defense",
"The Whisker Feint",
"The Triple Barrel Roll",
"The Stowaway Shuffle",
"The Bilge Rat Backflip",
"The Captain's Distraction Waltz",
"The Forbidden Knot",
"The Unforgivable Cannonball",
"The Polite Mutiny",
"The Reverse Mutiny",
"The Squeak of a Thousand Regrets",
"The Tail-First Retreat",
"The Upside-Down Boarding Party",
"The Invisible Rope Trick",
"The Two-Hat Bamboozle",
"The Ghost Pepper Defense",
"The Barnacle Embrace",
"The Crow's Nest Cannonball",
"The Rigging Tango",
"The Anchor Yo-Yo",
"The Powder Keg Lullaby",
"The Mermaid's IOU",
"The Goblin Refund Policy",
"The Fifth Ace",
"The Sixth Ace",
"The Emergency Wedding",
"The Slow-Motion Walk Away",
"The Borrowed Thunder",
"The Cheese Wheel of Fortune",
"The Last Rat Standing",
"The First Rat Running",
"The Old Switcheroo",
"The Old Double Switcheroo",
"The Long Con (Short Version)",
"The Accidental Masterpiece",
"The Sympathy Limp",
"The Decoy Funeral",
"The Surprise Encore",
"The Overly Specific Alibi",
"The Group Hug Ambush",
"The Cannonball Curveball",
"The Soup of Truth",
"The Hat Trick (With Actual Hats)",
"The Thousand-Yard Squint",
"The Completely Legal Salvage",
"Carry the one",
"Round down, shoot up",
"Divide and run away",
"Subtract yourself from the situation",
"Multiply by chaos",
"Add insult to injury",
"The remainder goes in my pocket",
"Long division, short fuse",
"Triangulate, then detonate",
"Roll for cheese",
"Yo ho ho and a barrel of math",
"Ask the cook, he's seen worse",
"Barry has a boat for this",
"Tell Jaspar I said hello",
"The first mate signs my alibis",
"Piss Whiskers takes the watch",
"We voted, you lost",
"The cheese was a lie",
"One more verse won't hurt",
"Abandon ship, but stylishly"
]
};
export function getSuggestion(type, exclude = []) {
let arr = SUGGESTIONS[type];
if (!arr) return "";
function pickFrom(arr, exclude = []) {
const taken = new Set(exclude.map(s => s.trim().toLowerCase()));
const available = arr.filter(s => !taken.has(s.trim().toLowerCase()));
if (available.length > 0) arr = available;
return arr[Math.floor(Math.random() * arr.length)];
const pool = available.length > 0 ? available : arr;
return pool[Math.floor(Math.random() * pool.length)];
}
// Draws `count` distinct suggestions from a pool.
export function getSuggestions(type, count) {
export function getSuggestion(type, exclude = []) {
const arr = SUGGESTIONS[type];
if (!arr) return "";
return pickFrom(arr, exclude);
}
// Secret Pirate Technique names live in the backend (it also uses them to equip
// re-rolled recruits); fetch the pool once and pick from it client-side.
let techniquePoolPromise = null;
function loadTechniquePool() {
if (!techniquePoolPromise) {
techniquePoolPromise = apiRequest('/suggestions/techniques').then(d => d.techniques);
}
return techniquePoolPromise;
}
export async function getTechniqueSuggestion(exclude = []) {
return pickFrom(await loadTechniquePool(), exclude);
}
// Draws `count` distinct technique suggestions.
export async function getTechniqueSuggestions(count, exclude = []) {
const pool = await loadTechniquePool();
const picked = [];
for (let i = 0; i < count; i++) {
const s = getSuggestion(type, picked);
if (!s) break;
picked.push(s);
picked.push(pickFrom(pool, [...exclude, ...picked]));
}
return picked;
}