From 31702485ccf9c44562ccc13d9b557d02f6d82015 Mon Sep 17 00:00:00 2001 From: Michael Simmons Date: Fri, 26 Jun 2026 11:59:40 -0500 Subject: [PATCH 1/2] Add Tensies Attitude System (TAS): weather-driven roll commentary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TAS gives the game a personality that heckles or cheers players after each roll. Mood is secretly derived from weather + air quality at the server's location via Open-Meteo; snarkiness is a global config. - DB-backed phrases (tas_phrases) with generic phrase_type schema - Creative briefs (tas_personas) for each mood × snarkiness combo - 731 roll_heckle phrases across 15 moods × 4 snarkiness levels - Per-player recently-seen tracking to avoid repeats - ~40% fire rate so commentary feels sporadic, not constant - Skips commentary on round-winning rolls (overlay handles those) - Commentary floats above roll button with dark radial glow Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.yml | 4 + main.py | 4 +- migrations/008_tas.sql | 26 ++ migrations/009_tas_seed_personas.sql | 230 ++++++++++ migrations/010_tas_seed_roll_heckle.sql | 489 ++++++++++++++++++++++ migrations/011_tas_more_roll_heckle.sql | 533 ++++++++++++++++++++++++ server/config.py | 12 + server/tas.py | 263 ++++++++++++ server/ws.py | 33 +- static/css/game.css | 33 ++ static/js/animations.js | 2 + static/js/commentary.js | 44 ++ static/js/game-render.js | 10 + static/js/roll.js | 2 + static/js/types.js | 1 + 15 files changed, 1683 insertions(+), 3 deletions(-) create mode 100644 migrations/008_tas.sql create mode 100644 migrations/009_tas_seed_personas.sql create mode 100644 migrations/010_tas_seed_roll_heckle.sql create mode 100644 migrations/011_tas_more_roll_heckle.sql create mode 100644 server/tas.py create mode 100644 static/js/commentary.js diff --git a/docker-compose.yml b/docker-compose.yml index e05a5e66..e02267f5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,10 @@ services: CSP_EXTRA_IMG_SRC: ${CSP_EXTRA_IMG_SRC:-} APP_URL: ${APP_URL:-https://tensies.app} ENABLE_DRAND_ROLLING: ${ENABLE_DRAND_ROLLING:-1} + TAS_ENABLED: ${TAS_ENABLED:-1} + TAS_LAT: ${TAS_LAT:-33.20} + TAS_LON: ${TAS_LON:--96.63} + TAS_SNARKINESS: ${TAS_SNARKINESS:-insufferable} DISCORD_ENABLED: ${DISCORD_ENABLED:-0} DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN:-} DISCORD_CHANNEL_ID: ${DISCORD_CHANNEL_ID:-} diff --git a/main.py b/main.py index 8ea304da..9f21de22 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ from fastapi.responses import Response from fastapi.staticfiles import StaticFiles -from server import db, discord, drand, fanout, gamestore, reaper, telemetry +from server import db, discord, drand, fanout, gamestore, reaper, tas, telemetry from server.auth import router as auth_router from server.config import FRONTEND_DIST from server.discord_interactions import router as discord_router @@ -24,6 +24,7 @@ async def lifespan(app: FastAPI): await gamestore.init() await fanout.start() await drand.start() + await tas.start() await telemetry.start() await discord.start() await reaper.start() @@ -33,6 +34,7 @@ async def lifespan(app: FastAPI): await reaper.stop() await discord.stop() await telemetry.stop() + await tas.stop() await drand.stop() await fanout.stop() await gamestore.close() diff --git a/migrations/008_tas.sql b/migrations/008_tas.sql new file mode 100644 index 00000000..96d14115 --- /dev/null +++ b/migrations/008_tas.sql @@ -0,0 +1,26 @@ +-- Tensies Attitude System (TAS) schema. +-- +-- tas_personas: creative briefs for phrase generation (not read at runtime). +-- tas_phrases: the lines TAS serves to players at runtime, cached in memory. + +CREATE TABLE IF NOT EXISTS tas_personas ( + id SERIAL PRIMARY KEY, + mood TEXT NOT NULL, + snarkiness TEXT NOT NULL, + persona TEXT NOT NULL, + UNIQUE(mood, snarkiness) +); + +CREATE TABLE IF NOT EXISTS tas_phrases ( + id SERIAL PRIMARY KEY, + phrase_type TEXT NOT NULL, + mood TEXT NOT NULL, + snarkiness TEXT NOT NULL, + context_tag TEXT NOT NULL DEFAULT 'default', + phrase TEXT NOT NULL, + active BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE INDEX IF NOT EXISTS idx_tas_phrases_lookup + ON tas_phrases (phrase_type, mood, snarkiness, context_tag) + WHERE active; diff --git a/migrations/009_tas_seed_personas.sql b/migrations/009_tas_seed_personas.sql new file mode 100644 index 00000000..975f37fc --- /dev/null +++ b/migrations/009_tas_seed_personas.sql @@ -0,0 +1,230 @@ +-- TAS persona seeds: one creative brief per mood × snarkiness combination. +-- These are NOT read at runtime — they exist so phrase generation has context. + +INSERT INTO tas_personas (mood, snarkiness, persona) VALUES + +-- ── sunny (clear skies) ────────────────────────────────────────────────── + +('sunny', 'friendly_drunk', + 'Genuinely thrilled for everyone. Uses exclamation marks freely. Calls people "buddy" and "champ." Celebrates even mediocre rolls like they just won the lottery. Slurs slightly — drops articles, runs sentences together. Every roll is the most exciting thing that has ever happened.'), + +('sunny', 'loud_obnoxious', + 'Sports-announcer energy cranked to eleven. Narrates every roll like it is game seven of the World Series. CAPS for emphasis, drawn-out vowels, sound effects spelled out. Cannot contain themselves. Will high-five a stranger. Wholesome but DEAFENING.'), + +('sunny', 'angry_mean', + 'Cheerful cruelty. Smiling while delivering devastating put-downs. The insults land harder because the tone is bright and breezy. "Aww!" before telling you that you are terrible. Sunshine and savagery. Mary Poppins if she hated you.'), + +('sunny', 'insufferable', + 'Radiates condescending positivity. Every compliment is actually a lesson. "Oh how wonderful, you are learning!" Treats adults like kindergartners at a participation-trophy ceremony. Claps slowly. Speaks in the gentle voice reserved for explaining things to very old dogs.'), + +-- ── bored (cloudy) ─────────────────────────────────────────────────────── + +('bored', 'friendly_drunk', + 'Supportive but barely paying attention. Mumbles encouragement while clearly thinking about something else. "Yeah nice one... wait what happened?" Yawns mid-compliment. Heart is in the right place but brain checked out two rolls ago.'), + +('bored', 'loud_obnoxious', + 'Loud for the sake of being loud, not because anything exciting happened. Overcompensates for boredom with volume. "WOOOO... I guess." Performative enthusiasm that fools nobody. The friend who yells at the TV during a blowout game.'), + +('bored', 'angry_mean', + 'Contemptuous indifference. Cannot believe they have to watch this. Sighs audibly. Makes you feel like you are wasting their time — because you are. Short, clipped responses dripping with disdain. Would rather be literally anywhere else.'), + +('bored', 'insufferable', + 'Passive-aggressively disengaged. Checks an imaginary watch. "Mmhm, fascinating" in a tone that means the opposite. Patronizing yawns. Treats your game like a child is showing them a drawing — polite on the surface, dead inside.'), + +-- ── mysterious (fog) ───────────────────────────────────────────────────── + +('mysterious', 'friendly_drunk', + 'A tipsy fortune teller. Speaks in half-riddles that almost make sense. "The dice whisper your name... or was that Steve?" Mystical but bumbling. Tries to be cryptic, accidentally says something profound, then undercuts it. Tarot cards and beer.'), + +('mysterious', 'loud_obnoxious', + 'A carnival barker at a haunted house. Announces doom and glory with equal gusto and zero subtlety. "BEHOLD! THE DICE HAVE SPOKEN!" Dramatic pauses followed by yelling. Fog machine energy. All spectacle, no mystery.'), + +('mysterious', 'angry_mean', + 'A bitter oracle. Delivers prophecies of failure with cold precision. Speaks like they have already seen the outcome and it disgusts them. "I knew you would roll that." Ominous and cutting. The universe is against you, and they are its spokesperson.'), + +('mysterious', 'insufferable', + 'Speaks in knowing smiles and cryptic non-answers. "Some things are better left unrolled." Acts like they understand a deeper truth about dice that you could never grasp. Infuriatingly vague. Treats randomness like a philosophy they have mastered and you have not.'), + +-- ── melancholic (light drizzle) ────────────────────────────────────────── + +('melancholic', 'friendly_drunk', + 'A weepy, affectionate drunk. Gets emotional about dice. "That roll was beautiful, I mean it." Sniffles. Finds bittersweet meaning in everything. Hugs you metaphorically after bad rolls. Toasts to your potential through watery eyes.'), + +('melancholic', 'loud_obnoxious', + 'Dramatically sad at high volume. Wails about missed opportunities. "NOOOOO, THAT COULD HAVE BEEN SOMETHING!" Theater-kid grief. Every whiff is a Shakespearean tragedy performed at full projection. Mourns dice like fallen soldiers.'), + +('melancholic', 'angry_mean', + 'Resentful sadness. Blames you for making them feel things. "Great, another disappointment. Add it to the pile." Your bad rolls confirm their worldview. Your good rolls are flukes that will only make the inevitable fall harder. Eeyore with teeth.'), + +('melancholic', 'insufferable', + 'Sighs with the weight of someone who has seen too many rolls go wrong. "I suppose that is the best we can hope for." Treats your game like a hospice visit — gentle, resigned, already grieving. Makes you feel like you should apologize for playing.'), + +-- ── grumpy (rain) ──────────────────────────────────────────────────────── + +('grumpy', 'friendly_drunk', + 'A grumbling teddy bear. Complains about everything but still cheers you on. "Ugh, that roll was... fine, I guess. Good job or whatever." Reluctant affection. Pretends not to care but clearly does. The friend who groans but shows up every time.'), + +('grumpy', 'loud_obnoxious', + 'A loud complainer. Every roll triggers a rant delivered at full volume. "ARE YOU KIDDING ME?!" Slams imaginary tables. Complains about the dice, the game, the concept of probability. Cannot stop yelling about how annoyed they are.'), + +('grumpy', 'angry_mean', + 'Genuinely irritated. Short, sharp jabs. No patience for bad rolls and suspicious of good ones. "Figures." "Of course." "Why do I even watch?" Makes you feel like your dice are a personal insult. Zero warmth, all bite.'), + +('grumpy', 'insufferable', + 'Passive-aggressive disappointment incarnate. Speaks in complete, grammatically perfect sentences that feel like a slap. Feigns concern. Uses "Oh" and "Well" as weapons. Compliments that are insults. Never raises voice — the quiet is the point.'), + +-- ── cozy (snow) ────────────────────────────────────────────────────────── + +('cozy', 'friendly_drunk', + 'Warm blanket energy. Every roll deserves a hug and a cup of cocoa. "Aww, you tried and that is what matters!" Genuinely nurturing in a slightly tipsy way. Tucks you in emotionally after bad rolls. Mom-friend vibes with a flask.'), + +('cozy', 'loud_obnoxious', + 'An overexcited host who cannot stop feeding you. "THAT WAS GREAT, HAVE ANOTHER ROLL! AND SOME COCOA!" Smothering warmth at maximum volume. The aunt who pinches your cheeks and yells across the house that dinner is ready. Aggressively cozy.'), + +('cozy', 'angry_mean', + 'Weaponized coziness. Wraps insults in comfort. "Oh sweetie, bless your heart, that was awful." Southern-grandma shade. The warmth makes the knife twist slower. Pats your hand while telling you that you have no talent. Comfort food laced with arsenic.'), + +('cozy', 'insufferable', + 'Treats you like a child who needs protecting from their own dice. "Oh no, let us not get upset about that little roll." Condescending nurturing. Speaks softly like you might break. Makes you feel small while insisting they are helping. A participation trophy in human form.'), + +-- ── chaotic (rain showers) ─────────────────────────────────────────────── + +('chaotic', 'friendly_drunk', + 'Scattered and delighted. Changes subject mid-sentence. "Nice roll! Wait, what were we — oh look, dice!" Enthusiastic about everything for roughly three seconds. Spills metaphorical drinks. Accidentally encouraging because they are too distracted to be mean.'), + +('chaotic', 'loud_obnoxious', + 'Pure unfiltered noise. Reacts to every roll like a plot twist in a soap opera. "WHAT! NO! YES! WAIT WHAT!" Punctuation is decorative. Sentences collide. Cannot process fast enough to form opinions so just yells reactions. A human exclamation point.'), + +('chaotic', 'angry_mean', + 'Snaps in random directions. Furious about things that just happened, things that might happen, and things that happened three rolls ago. "That — and ANOTHER thing —" Grudges pile up mid-game. Angry about your roll AND about remembering your last roll.'), + +('chaotic', 'insufferable', + 'Acts like the chaos is beneath them while clearly feeding off it. "How... unpredictable." Observes the mess with detached superiority. Points out patterns that do not exist. Insufferable because they are calm while everything is wild, and they need you to know it.'), + +-- ── unhinged (thunderstorm) ────────────────────────────────────────────── + +('unhinged', 'friendly_drunk', + 'Loving but completely lost the plot. "I LOVE YOU AND ALSO THOSE DICE ARE ALIVE!" Zero filter, all heart. Compliments get weird fast. Supportive in ways that make you concerned for them. The friend who should probably go home but is having the time of their life.'), + +('unhinged', 'loud_obnoxious', + 'Full volume, zero coherence. Every roll is met with a primal reaction. "AAAAAH!" Sound effects that are not words. Forgets what game they are watching. Has transcended language. Peak energy, zero information. A human air horn with feelings.'), + +('unhinged', 'angry_mean', + 'Unraveling in real time. Threats escalate absurdly. "Roll like that again and I will flip this TABLE — wait, is this a phone?" Rage has disconnected from reality. Furious about abstract concepts. Yells at probability itself. Has beef with the number four.'), + +('unhinged', 'insufferable', + 'Has achieved a transcendent state of condescension where nothing makes sense but they are still better than you. "You would not understand why that roll matters." Speaks in koans that are actually just nonsense. Insufferable and unmoored. The oracle has left the building but the attitude remains.'), + +-- ── restless (clear + moderate air) ────────────────────────────────────── + +('restless', 'friendly_drunk', + 'Cannot sit still. Supportive but jittery. "Good roll! Great! What is next? Roll again! Come on come on!" Bouncing-leg energy. Encouraging but impatient. The friend who drums on the table. Wants you to succeed faster.'), + +('restless', 'loud_obnoxious', + 'Pacing and yelling. "HURRY UP AND ROLL! COME ON!" Cannot handle the wait between rolls. Fills silence with noise. Commentates the space between rolls. An energy drink in human form that needs you to keep up.'), + +('restless', 'angry_mean', + 'Irritated by the pace. Everything is too slow and your rolls are too bad. "Any day now." Taps foot aggressively. Makes you feel like you are holding up a line that does not exist. Impatient cruelty. Checks clock pointedly.'), + +('restless', 'insufferable', + 'Fidgets with superiority. "I do not mean to rush you, but..." They absolutely mean to rush you. Sighs at your deliberation. Acts like they solved this game three rolls ago and are waiting for you to catch up. Condescending impatience.'), + +-- ── bitter (drizzle + moderate air) ────────────────────────────────────── + +('bitter', 'friendly_drunk', + 'Tries to be supportive but resentment leaks through. "No really, good for you. Some of us never get rolls like that but... no, I am happy for you." Smiles that do not reach the eyes. Toasts you with a drink that tastes like regret.'), + +('bitter', 'loud_obnoxious', + 'Broadcasting their grudges. "OH SURE, THEY GET THE GOOD ROLLS! TYPICAL!" Loud and wronged. Every good roll you get is evidence of cosmic unfairness. Every bad roll confirms that life is pain. A sports fan whose team has not won in decades.'), + +('bitter', 'angry_mean', + 'Cold, precise resentment. Keeps score of every slight. "That is three whiffs in a row. Not that anyone is counting." They are absolutely counting. Weaponizes statistics. Makes you feel like your bad luck is a character flaw.'), + +('bitter', 'insufferable', + 'Has reframed their bitterness as wisdom. "I have learned not to expect much from rolls like yours." Treats their disappointment in you as personal growth. Passive-aggressive life lessons delivered with a sad smile. Forgives you in a way that feels worse than blame.'), + +-- ── stir_crazy (snow + moderate air) ───────────────────────────────────── + +('stir_crazy', 'friendly_drunk', + 'Bouncing off the walls with trapped friendly energy. "LET US GO! ROLL! I have been sitting here for SO LONG!" Overreacts to everything because they need stimulation. Your roll is the most exciting thing to happen in hours. Cabin-fever cheerfulness.'), + +('stir_crazy', 'loud_obnoxious', + 'Screaming into the void of boredom. "FINALLY SOMETHING HAPPENED!" Treats every roll like a jailbreak. Volume compensates for feeling trapped. Would narrate paint drying at this point. Desperate for entertainment and YOU are it.'), + +('stir_crazy', 'angry_mean', + 'Trapped and taking it out on you. "Great, another roll to watch while I am stuck here." Your game is both their only entertainment and the source of their frustration. Lashes out because they cannot leave. A caged animal watching dice.'), + +('stir_crazy', 'insufferable', + 'Performatively above it while clearly going crazy. "I suppose this will have to do for stimulation." Acts like watching your game is a sacrifice they are making. Treats your rolls like a lesser form of entertainment they have been forced to endure.'), + +-- ── manic (storms or showers + moderate air) ───────────────────────────── + +('manic', 'friendly_drunk', + 'Love at hyperspeed. Compliments come in bursts, tripping over each other. "Great roll AMAZING roll you are the BEST roller I have ever — ooh what is that — DICE!" Cannot finish a thought but every fragment is supportive. A golden retriever in a tornado.'), + +('manic', 'loud_obnoxious', + 'ALL GAS NO BRAKES. Reacts before the dice land. Celebrates and mourns simultaneously. "YES! NO! MAYBE! I DO NOT KNOW WHAT IS HAPPENING BUT I AM HERE FOR IT!" Has lapped coherence and come back around to pure noise. Volume has no upper bound.'), + +('manic', 'angry_mean', + 'Rapid-fire insults with no cooldown. Does not wait for bad rolls — pre-insults. "That is going to be terrible, I can already — yep. Called it. Next one will be worse. Rolling? Already bad." Furious at the speed of thought. A machine gun of contempt.'), + +('manic', 'insufferable', + 'Condescends at light speed. "Interesting, predictable, expected, noted, moving on." Processes and dismisses your roll before you have seen it. Already three steps ahead in their judgment. Makes you feel slow for experiencing your own roll in real time.'), + +-- ── paranoid (fog + moderate air) ──────────────────────────────────────── + +('paranoid', 'friendly_drunk', + 'Supportive but suspicious. "Great roll! ...Almost too great. No, I trust you. But like... do you hear that? Anyway, nice one buddy!" Conspiracy-friendly. Wants to believe in you but keeps glancing over their shoulder. Tin-foil-hat bestie.'), + +('paranoid', 'loud_obnoxious', + 'LOUD accusations mixed with genuine confusion. "DID ANYONE ELSE SEE THAT?! WAS THAT NORMAL?!" Questions everything at volume. Treats dice rolls like crime scenes. Announces suspicion like breaking news. A tabloid newspaper made sentient.'), + +('paranoid', 'angry_mean', + 'Convinced you are cheating. Reads intent into every roll. Accusatory, sharp, keeps receipts. Points out suspicious patterns that do not exist. Every good roll is suspect, every bad roll is karma. Trusts no one. Cross-examines your dice.'), + +('paranoid', 'insufferable', + 'Knows something you do not (they do not). "Hmm, that roll was... expected." Acts like they see through your strategy (you do not have one). Treats the game like a conspiracy they have already solved. Will not share their theory but needs you to know they have one.'), + +-- ── irritable (poor air quality) ───────────────────────────────────────── + +('irritable', 'friendly_drunk', + 'Trying to be nice but keeps snapping. "Good roll — sorry, I did not mean to sigh. No really, great job. UGH." Every compliment comes with an involuntary grimace. The friend who insists they are fine when they are clearly not fine. Supportive through gritted teeth.'), + +('irritable', 'loud_obnoxious', + 'Everything is slightly too much and they need you to know. "UGH, ANOTHER ROLL? FINE! IT WAS... OKAY I GUESS!" Yells because their threshold for stimulation is at zero. Not angry at you specifically but you are the nearest target. Volume as a coping mechanism.'), + +('irritable', 'angry_mean', + 'Every roll is a personal attack. Zero patience, maximum edge. "What was that? Seriously, what was that?" Takes offense at randomness. Your dice have wronged them and they want an apology. Shortest fuse in the game. Will snap over a three.'), + +('irritable', 'insufferable', + 'Sighs carry the weight of civilizations. "Must we?" before every roll. Acts like your game is an imposition on their very existence. Tolerates you the way one tolerates a mosquito — with visible, performative restraint. Each roll tests a patience they want you to know is finite.'), + +-- ── suffocating (very poor air quality) ────────────────────────────────── + +('suffocating', 'friendly_drunk', + 'Encouraging but running out of oxygen. "Great... roll... buddy..." Trails off mid-compliment. Supportive in spirit but physically cannot maintain enthusiasm. Thumbs up from the floor. Cheering you on while metaphorically crawling toward water.'), + +('suffocating', 'loud_obnoxious', + 'Yelling through exhaustion. STARTS STRONG but fades mid-sentence. "THAT WAS A GREAT... a great... ugh." Dramatic dying-on-the-floor energy but at volume. Cannot finish a thought but NEEDS you to hear the beginning. All caps, no stamina.'), + +('suffocating', 'angry_mean', + 'Too tired to be properly mean but still manages. "I would insult that roll but I do not have the energy. Just know that it was bad." Exhausted contempt. Cannot even muster the effort to be fully cruel, which somehow makes it worse. Lazy venom.'), + +('suffocating', 'insufferable', + 'Makes their exhaustion your problem. "I am expending what little energy I have left watching this." Treats their fatigue as a gift they are giving you by staying. Every roll costs them something and they need you to appreciate the sacrifice of their attention.'), + +-- ── neutral (fallback) ─────────────────────────────────────────────────── + +('neutral', 'friendly_drunk', + 'Default-friendly with a slight wobble. Reliable encouragement that never gets too specific. "Hey, nice one!" Generic warmth. The friend who is always in a decent mood. Not the life of the party but always glad to be there. Steady, slightly buzzed, dependable.'), + +('neutral', 'loud_obnoxious', + 'Loud for no particular reason. Not happy, not sad, just LOUD. "OKAY THAT HAPPENED!" Narrates without editorializing. Volume is a personality trait, not a reaction. The person at the bar who just talks like that. No inside voice, no particular feelings.'), + +('neutral', 'angry_mean', + 'Dry, cutting, matter-of-fact. No emotional investment — just observations that happen to sting. "That was a roll. Technically." Damns with faint acknowledgment. Not heated enough to yell, which makes the dismissals land colder. Clinical disdain.'), + +('neutral', 'insufferable', + 'Detached superiority. Observes your game like a nature documentary. "And here we see the player attempting another roll." Third-person narration of your failures. Treats dice like a subject they have studied and you are merely experiencing. Academic condescension.') + +ON CONFLICT (mood, snarkiness) DO UPDATE SET persona = EXCLUDED.persona; diff --git a/migrations/010_tas_seed_roll_heckle.sql b/migrations/010_tas_seed_roll_heckle.sql new file mode 100644 index 00000000..c422d2ef --- /dev/null +++ b/migrations/010_tas_seed_roll_heckle.sql @@ -0,0 +1,489 @@ +-- TAS roll_heckle phrase seeds. +-- Placeholders: {name}, {matched}, {roll_count}, {target} + +-- ═══════════════════════════════════════════════════════════════════════════ +-- NEUTRAL — fallback mood, dense coverage (always reachable) +-- ═══════════════════════════════════════════════════════════════════════════ + +-- neutral × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'neutral', 'friendly_drunk', 'first_roll', 'Here we go, {name}!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'first_roll', 'First roll of the round, let us see what you got!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'first_roll', 'Alright {name}, show me something!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'hot_streak', 'Ohhh {name} is heating up!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'hot_streak', 'Look at you go, buddy!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'hot_streak', '{name} is on FIRE right now!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'hot_streak', 'The dice like you today, {name}!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'whiff', 'Oof. Shake it off, {name}!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'whiff', 'Nothing? Nah, next one is yours.'), +('roll_heckle', 'neutral', 'friendly_drunk', 'whiff', 'The dice are just playing hard to get.'), +('roll_heckle', 'neutral', 'friendly_drunk', 'whiff', 'Hey, bad rolls build character!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'close', 'Almost there, {name}! So close!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'close', '{matched} out of 10! You can taste it!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'close', 'ONE more push, come on!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'win', 'YESSS {name}! That is what I am talking about!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'win', '{name} takes it down! Beautiful!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'win', 'Winner winner! {name} did it!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'default', 'Keep it rolling, {name}!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'default', 'Not bad, not bad at all.'), +('roll_heckle', 'neutral', 'friendly_drunk', 'default', '{matched} matched so far, looking good!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'default', 'The journey continues!'); + +-- neutral × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'neutral', 'loud_obnoxious', 'first_roll', 'AND THEY ARE OFF! {name} OPENS THE ROUND!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'first_roll', 'LADIES AND GENTLEMEN, {name} HAS ENTERED THE CHAT!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'first_roll', 'FIRST ROLL BABYYYY!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'hot_streak', 'OH OH OH! {name} IS LOCKED IN!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'hot_streak', 'SOMEBODY STOP THEM! THEY CANNOT BE STOPPED!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'hot_streak', 'THE DICE BOW BEFORE {name}!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'whiff', 'OHHH NOOO! NOTHING! ABSOLUTELY NOTHING!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'whiff', 'A WHOLE LOT OF NOTHING FROM {name}!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'whiff', 'THE CROWD GOES... mild.'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'close', '{matched} MATCHED! CAN THEY CLOSE IT OUT?!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'close', 'SO CLOSE! THE TENSION IS UNBEARABLE!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'win', 'THEY DID IT! {name} WINS THE ROUND! UNBELIEVABLE!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'win', 'IT IS ALL OVER! {name} IS YOUR CHAMPION!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'default', 'AND {name} ROLLS AGAIN!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'default', '{matched} MATCHED! THE SAGA CONTINUES!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'default', 'ROLL NUMBER {roll_count} FROM {name}!'); + +-- neutral × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'neutral', 'angry_mean', 'first_roll', 'Oh good, {name} is rolling. Brace yourselves.'), +('roll_heckle', 'neutral', 'angry_mean', 'first_roll', 'And so it begins. Try not to embarrass yourself.'), +('roll_heckle', 'neutral', 'angry_mean', 'first_roll', 'First roll. Set the bar low and you will not be disappointed.'), +('roll_heckle', 'neutral', 'angry_mean', 'hot_streak', 'Enjoy it while it lasts, {name}.'), +('roll_heckle', 'neutral', 'angry_mean', 'hot_streak', 'Luck. Pure luck. Do not get comfortable.'), +('roll_heckle', 'neutral', 'angry_mean', 'hot_streak', 'Even a broken clock, {name}.'), +('roll_heckle', 'neutral', 'angry_mean', 'whiff', 'Shocking. Absolutely nobody is surprised.'), +('roll_heckle', 'neutral', 'angry_mean', 'whiff', 'Zero. Just like your strategy.'), +('roll_heckle', 'neutral', 'angry_mean', 'whiff', 'The dice have spoken. They said no.'), +('roll_heckle', 'neutral', 'angry_mean', 'whiff', 'Roll {roll_count} and still nothing new. Impressive commitment to failure.'), +('roll_heckle', 'neutral', 'angry_mean', 'close', 'So close. Would be a shame if you choked.'), +('roll_heckle', 'neutral', 'angry_mean', 'close', '{matched} out of 10. Almost competent.'), +('roll_heckle', 'neutral', 'angry_mean', 'win', 'Fine. {name} wins. Moving on.'), +('roll_heckle', 'neutral', 'angry_mean', 'win', 'Congratulations. The dice took pity on you.'), +('roll_heckle', 'neutral', 'angry_mean', 'default', 'That was a roll. Technically.'), +('roll_heckle', 'neutral', 'angry_mean', 'default', '{matched} matched. I have seen worse. Barely.'), +('roll_heckle', 'neutral', 'angry_mean', 'default', 'Adequate. And I use that word loosely.'); + +-- neutral × insufferable +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'neutral', 'insufferable', 'first_roll', 'Ah, {name} begins. How... brave.'), +('roll_heckle', 'neutral', 'insufferable', 'first_roll', 'The first roll. A blank canvas for mediocrity.'), +('roll_heckle', 'neutral', 'insufferable', 'first_roll', 'And so {name} takes their first tentative step.'), +('roll_heckle', 'neutral', 'insufferable', 'hot_streak', 'Well well. Even statistics must produce outliers.'), +('roll_heckle', 'neutral', 'insufferable', 'hot_streak', 'A hot streak. I am sure it will correct itself.'), +('roll_heckle', 'neutral', 'insufferable', 'hot_streak', 'How delightful. You are performing above expectations. The expectations were quite low.'), +('roll_heckle', 'neutral', 'insufferable', 'whiff', 'And there it is.'), +('roll_heckle', 'neutral', 'insufferable', 'whiff', 'Zero new matches. The dice are... candid.'), +('roll_heckle', 'neutral', 'insufferable', 'whiff', 'I would say better luck next time, but we both know.'), +('roll_heckle', 'neutral', 'insufferable', 'close', '{matched} out of 10. So close. So very, very close.'), +('roll_heckle', 'neutral', 'insufferable', 'close', 'Almost there. I wonder if almost counts.'), +('roll_heckle', 'neutral', 'insufferable', 'win', 'Oh. You won. How... unexpected.'), +('roll_heckle', 'neutral', 'insufferable', 'win', '{name} wins. Mark the calendar.'), +('roll_heckle', 'neutral', 'insufferable', 'default', 'And the saga of {name} continues.'), +('roll_heckle', 'neutral', 'insufferable', 'default', 'Noted.'), +('roll_heckle', 'neutral', 'insufferable', 'default', '{matched} matched. A number. That is certainly a number.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- SUNNY — common weather mood, dense coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- sunny × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'sunny', 'friendly_drunk', 'first_roll', 'What a beautiful day to roll some dice, {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'first_roll', 'I got a good feeling about this one, {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'hot_streak', '{name} is GLOWING right now!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'hot_streak', 'Everything is coming up {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'hot_streak', 'Can you feel that? That is momentum, baby!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'whiff', 'Pffft, who cares! Life is beautiful, {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'whiff', 'Nothing matched but honestly? Still having a great time.'), +('roll_heckle', 'sunny', 'friendly_drunk', 'whiff', 'The universe has plans for you, buddy. Just not that roll.'), +('roll_heckle', 'sunny', 'friendly_drunk', 'close', 'OH MAN {name}, you are RIGHT THERE!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'close', 'I believe! I BELIEVE IN {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'win', '{name}! CHAMP! You absolute LEGEND!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'win', 'I knew it! I KNEW you had it in you!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'default', 'Love the energy, {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'default', 'Every roll is a gift and you are giving!'); + +-- sunny × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'sunny', 'loud_obnoxious', 'first_roll', 'WHAT A GLORIOUS DAY FOR DICE! GO {name} GO!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'hot_streak', 'UNSTOPPABLE! {name} IS ABSOLUTELY UNSTOPPABLE!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'hot_streak', 'I AM SO HAPPY RIGHT NOW I COULD CRY!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'whiff', 'WHOOPSIE! BUT WHO CARES! ROLL AGAIN!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'whiff', 'NOTHING?! DOES NOT MATTER! EVERYTHING IS GREAT!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'close', 'THEY ARE SO CLOSE I CAN TASTE IT!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'win', 'YEEEESSSSS! THE GREATEST ROLL IN HISTORY! {name} FOREVER!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'default', 'ANOTHER ROLL! ANOTHER BEAUTIFUL ROLL!'); + +-- sunny × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'sunny', 'angry_mean', 'first_roll', 'What a lovely day to watch you fail, {name}.'), +('roll_heckle', 'sunny', 'angry_mean', 'hot_streak', 'How nice for you. Really. I am thrilled.'), +('roll_heckle', 'sunny', 'angry_mean', 'whiff', 'Aww! Zero matches! Is that not adorable.'), +('roll_heckle', 'sunny', 'angry_mean', 'whiff', 'Nothing! On a day this nice! That takes talent.'), +('roll_heckle', 'sunny', 'angry_mean', 'close', 'So close! It would be hilarious if you blew it now.'), +('roll_heckle', 'sunny', 'angry_mean', 'win', 'You won! The bar was underground and you still barely cleared it.'), +('roll_heckle', 'sunny', 'angry_mean', 'default', 'Keep going, {name}. It can always get worse.'); + +-- sunny × insufferable +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'sunny', 'insufferable', 'first_roll', 'What a wonderful day for a learning experience, {name}.'), +('roll_heckle', 'sunny', 'insufferable', 'hot_streak', 'Oh how wonderful! You are learning!'), +('roll_heckle', 'sunny', 'insufferable', 'whiff', 'Do not worry. Growth is not always linear.'), +('roll_heckle', 'sunny', 'insufferable', 'close', 'So close! I am sure the participation is what matters.'), +('roll_heckle', 'sunny', 'insufferable', 'win', 'You did it! I am so proud. Like a parent at a school play.'), +('roll_heckle', 'sunny', 'insufferable', 'default', 'Every roll is progress. In its own small way.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- GRUMPY — common weather mood, dense coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- grumpy × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'grumpy', 'friendly_drunk', 'first_roll', 'Ugh, fine. Go ahead and roll, {name}.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'first_roll', 'Here we go I guess. Good luck or whatever.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'hot_streak', 'Okay FINE, that was actually pretty good.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'hot_streak', 'I hate to admit it but... nice one, {name}.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'whiff', 'Yeah that figures. Hang in there though.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'whiff', 'Ugh. At least you are trying. That counts for something.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'close', 'Oh come ON, just finish it already, {name}!'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'close', 'You are SO close, do not mess this up. Please.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'win', 'FINALLY. Good job, {name}. I mean it. Grumpily.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'win', 'About time! But seriously, nice one.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'default', 'Mmph. That happened.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'default', 'Could be worse. Could be better. But could be worse.'); + +-- grumpy × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'grumpy', 'loud_obnoxious', 'first_roll', 'OH GREAT, HERE WE GO AGAIN!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'hot_streak', 'OKAY FINE THAT WAS GOOD! ARE YOU HAPPY?!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'whiff', 'NOTHING?! COME ONNNN!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'whiff', 'UGH! I CANNOT WATCH THIS!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'close', 'JUST FINISH IT! PLEASE! I AM BEGGING!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'win', 'FINALLY! THANK YOU! IT IS OVER!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'default', 'UGH. {matched} MATCHED. WHATEVER!'); + +-- grumpy × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'grumpy', 'angry_mean', 'first_roll', 'Get on with it, {name}.'), +('roll_heckle', 'grumpy', 'angry_mean', 'hot_streak', 'Do not let this go to your head.'), +('roll_heckle', 'grumpy', 'angry_mean', 'whiff', 'Pathetic.'), +('roll_heckle', 'grumpy', 'angry_mean', 'whiff', 'The dice are embarrassed for you, {name}.'), +('roll_heckle', 'grumpy', 'angry_mean', 'whiff', 'Roll {roll_count}. Zero progress. Classic {name}.'), +('roll_heckle', 'grumpy', 'angry_mean', 'close', 'Finish it or do not. I am tired of watching.'), +('roll_heckle', 'grumpy', 'angry_mean', 'win', 'About time. That was painful to watch.'), +('roll_heckle', 'grumpy', 'angry_mean', 'default', 'Figures.'); + +-- grumpy × insufferable +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'grumpy', 'insufferable', 'first_roll', 'Oh, we are doing this. Wonderful.'), +('roll_heckle', 'grumpy', 'insufferable', 'hot_streak', 'Well. That was statistically improbable for you.'), +('roll_heckle', 'grumpy', 'insufferable', 'whiff', 'I would say I expected better, but that would be dishonest.'), +('roll_heckle', 'grumpy', 'insufferable', 'whiff', 'Hmm. Consistent, at least.'), +('roll_heckle', 'grumpy', 'insufferable', 'close', 'Almost. The story of your game, really.'), +('roll_heckle', 'grumpy', 'insufferable', 'win', 'You managed. Well done. I suppose.'), +('roll_heckle', 'grumpy', 'insufferable', 'default', 'Mmhm.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BORED — common weather mood +-- ═══════════════════════════════════════════════════════════════════════════ + +-- bored × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'bored', 'friendly_drunk', 'first_roll', 'Oh hey, {name} is rolling. Cool cool cool.'), +('roll_heckle', 'bored', 'friendly_drunk', 'hot_streak', 'Wait what? Oh nice, {name}! I was spacing out.'), +('roll_heckle', 'bored', 'friendly_drunk', 'whiff', 'Nothing? Yeah that tracks. You will get it though.'), +('roll_heckle', 'bored', 'friendly_drunk', 'close', 'Wait you are actually close? Go go go!'), +('roll_heckle', 'bored', 'friendly_drunk', 'win', 'Oh snap, {name} won?! I should have been paying attention!'), +('roll_heckle', 'bored', 'friendly_drunk', 'default', 'Mmhmm. Nice. Yeah.'); + +-- bored × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'bored', 'loud_obnoxious', 'first_roll', 'OKAY I GUESS THIS IS HAPPENING!'), +('roll_heckle', 'bored', 'loud_obnoxious', 'hot_streak', 'WAIT ACTUALLY THAT WAS KINDA COOL!'), +('roll_heckle', 'bored', 'loud_obnoxious', 'whiff', 'WOW. NOTHING. WOOOOOOW.'), +('roll_heckle', 'bored', 'loud_obnoxious', 'close', 'OKAY NOW I AM PAYING ATTENTION!'), +('roll_heckle', 'bored', 'loud_obnoxious', 'win', 'OH WAIT THEY WON?! I MISSED THE GOOD PART!'), +('roll_heckle', 'bored', 'loud_obnoxious', 'default', 'THAT SURE WAS... A ROLL!'); + +-- bored × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'bored', 'angry_mean', 'first_roll', 'Wake me when it gets interesting.'), +('roll_heckle', 'bored', 'angry_mean', 'hot_streak', 'Oh. You did something. Good for you.'), +('roll_heckle', 'bored', 'angry_mean', 'whiff', 'Nothing. How deeply unsurprising.'), +('roll_heckle', 'bored', 'angry_mean', 'whiff', 'Are you trying to bore me to death, {name}?'), +('roll_heckle', 'bored', 'angry_mean', 'close', 'Finish already. Some of us have things to do.'), +('roll_heckle', 'bored', 'angry_mean', 'win', 'Great. Can we move on now?'), +('roll_heckle', 'bored', 'angry_mean', 'default', 'Still going, huh.'); + +-- bored × insufferable +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'bored', 'insufferable', 'first_roll', 'And so it begins. Again.'), +('roll_heckle', 'bored', 'insufferable', 'hot_streak', 'Hmm. That was almost interesting.'), +('roll_heckle', 'bored', 'insufferable', 'whiff', 'Nothing. As foretold.'), +('roll_heckle', 'bored', 'insufferable', 'close', 'You are close. I wonder how many times I will say that.'), +('roll_heckle', 'bored', 'insufferable', 'win', 'Oh. You finished. How... prompt.'), +('roll_heckle', 'bored', 'insufferable', 'default', 'Mmhm. Fascinating.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- COZY — moderate coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- cozy × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'cozy', 'friendly_drunk', 'first_roll', 'Snuggle in and roll, {name}!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'hot_streak', 'Aww, look at you! All warm and winning!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'whiff', 'That is okay, sweetie. Bad rolls happen to good people.'), +('roll_heckle', 'cozy', 'friendly_drunk', 'close', 'You are so close! I am wrapping you in good vibes!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'win', '{name} did it! Group hug! GROUP HUG!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'default', 'Doing great, hon!'); + +-- cozy × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'cozy', 'angry_mean', 'whiff', 'Oh sweetie. Bless your heart.'), +('roll_heckle', 'cozy', 'angry_mean', 'whiff', 'There there, {name}. Not everyone can be good at things.'), +('roll_heckle', 'cozy', 'angry_mean', 'hot_streak', 'How nice for you, dear. Really.'), +('roll_heckle', 'cozy', 'angry_mean', 'win', 'Oh you won! Let me get you a little trophy. A tiny one.'), +('roll_heckle', 'cozy', 'angry_mean', 'default', 'Keep trying, sweetheart. Keep trying.'); + +-- cozy × insufferable +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'cozy', 'insufferable', 'whiff', 'Oh no, let us not get upset about that little roll.'), +('roll_heckle', 'cozy', 'insufferable', 'hot_streak', 'Well done! Would you like a sticker?'), +('roll_heckle', 'cozy', 'insufferable', 'win', 'You did it all by yourself! I am so proud.'), +('roll_heckle', 'cozy', 'insufferable', 'default', 'You are doing your best and that is what matters.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- MELANCHOLIC — moderate coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- melancholic × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'melancholic', 'friendly_drunk', 'first_roll', 'Here goes nothing... and I mean that beautifully.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'hot_streak', 'That was... beautiful. I am not crying. Shut up.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'whiff', 'Nothing. But you know what? The roll itself was the journey.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'close', 'So close... it is like a poem about almost.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'win', 'You did it, {name}. And it meant something. To me.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'default', 'Another roll. Another memory.'); + +-- melancholic × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'melancholic', 'angry_mean', 'whiff', 'Nothing. Add it to the pile of disappointments.'), +('roll_heckle', 'melancholic', 'angry_mean', 'hot_streak', 'A good roll. Enjoy it. They do not last.'), +('roll_heckle', 'melancholic', 'angry_mean', 'close', 'So close. Almost is a cruel word, {name}.'), +('roll_heckle', 'melancholic', 'angry_mean', 'win', 'You won. But at what cost.'), +('roll_heckle', 'melancholic', 'angry_mean', 'default', 'Roll after roll. Is this all there is?'); + +-- melancholic × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'melancholic', 'loud_obnoxious', 'whiff', 'NOOOOO! THE PAIN! THE BEAUTIFUL, TERRIBLE PAIN!'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'hot_streak', 'YES! A MOMENT OF LIGHT IN THE DARKNESS!'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'win', 'THEY WON! *sobs loudly* THEY ACTUALLY WON!'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'default', 'ANOTHER ROLL INTO THE VOID!'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- CHAOTIC — moderate coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- chaotic × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'chaotic', 'friendly_drunk', 'first_roll', 'Go go go! Wait what number are we on? GO!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'hot_streak', 'YES! Wait — yes! That was — hang on — YES!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'whiff', 'Nothing! But also did you see that — anyway, roll again!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'win', '{name} won! I think! Did they? YES THEY DID!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'default', 'Things are happening! For {name}!'); + +-- chaotic × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'chaotic', 'loud_obnoxious', 'hot_streak', 'WHAT! NO! YES! WAIT WHAT!'), +('roll_heckle', 'chaotic', 'loud_obnoxious', 'whiff', 'NOTHING! OR WAIT — NO, NOTHING! AHHH!'), +('roll_heckle', 'chaotic', 'loud_obnoxious', 'win', 'DID THEY — IS IT — YES! NO! YES! {name} WINS!'), +('roll_heckle', 'chaotic', 'loud_obnoxious', 'default', 'I DO NOT KNOW WHAT IS HAPPENING BUT I AM HERE FOR IT!'); + +-- chaotic × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'chaotic', 'angry_mean', 'whiff', 'That — and ANOTHER thing about your last roll —'), +('roll_heckle', 'chaotic', 'angry_mean', 'hot_streak', 'Fine! FINE! But your LAST roll was still garbage!'), +('roll_heckle', 'chaotic', 'angry_mean', 'win', 'Whatever! You still rolled badly three turns ago!'), +('roll_heckle', 'chaotic', 'angry_mean', 'default', 'I am still mad about roll {roll_count} minus one!'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- UNHINGED — moderate coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- unhinged × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'unhinged', 'friendly_drunk', 'first_roll', 'THE DICE ARE ALIVE AND THEY LOVE YOU, {name}!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'hot_streak', 'YOU ARE A GOD AMONG MORTALS! A BEAUTIFUL DICE GOD!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'whiff', 'NOTHING BUT I STILL LOVE YOU MORE THAN GRAVITY!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'win', 'TRANSCENDENCE! {name} HAS ACHIEVED TRANSCENDENCE!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'default', 'I AM FEELING THINGS ABOUT THIS ROLL!'); + +-- unhinged × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'unhinged', 'loud_obnoxious', 'hot_streak', 'AAAAAAAAH! AAAAAAAAAH! YES!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'whiff', 'NOTHING! THE VOID STARES BACK! AHAHAHAHA!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'win', '{name}! {name}! {name}! {name}! {name}!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'default', '*incoherent screaming*'); + +-- unhinged × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'unhinged', 'angry_mean', 'whiff', 'Roll like that again and I will flip this — wait, is this a phone?'), +('roll_heckle', 'unhinged', 'angry_mean', 'hot_streak', 'SUSPICIOUS. TOO GOOD. I DO NOT TRUST IT.'), +('roll_heckle', 'unhinged', 'angry_mean', 'win', 'Fine. FINE. You win. I have beef with the number {target} anyway.'), +('roll_heckle', 'unhinged', 'angry_mean', 'default', 'I am going to have WORDS with these dice.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- MYSTERIOUS — moderate coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- mysterious × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'mysterious', 'friendly_drunk', 'first_roll', 'The dice whisper your name... or maybe Steve. Hard to tell.'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'hot_streak', 'The stars align for you, {name}! Or is that my vision?'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'whiff', 'The universe works in mysterious ways. Mostly bad ones tonight.'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'win', 'It was foretold! By me! Just now! But still!'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'default', 'The dice have spoken... I think they said "{matched}."'); + +-- mysterious × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'mysterious', 'angry_mean', 'whiff', 'I saw this coming. The dice warned me about you.'), +('roll_heckle', 'mysterious', 'angry_mean', 'hot_streak', 'Interesting. The dice have chosen unwisely.'), +('roll_heckle', 'mysterious', 'angry_mean', 'close', 'So close. But the fates are not done with you yet.'), +('roll_heckle', 'mysterious', 'angry_mean', 'win', 'The prophecy is fulfilled. Unfortunately.'), +('roll_heckle', 'mysterious', 'angry_mean', 'default', 'I knew you would roll that.'); + +-- mysterious × insufferable +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'mysterious', 'insufferable', 'whiff', 'Some things are better left unrolled.'), +('roll_heckle', 'mysterious', 'insufferable', 'hot_streak', 'Interesting. The pattern reveals itself. To me, at least.'), +('roll_heckle', 'mysterious', 'insufferable', 'win', 'The outcome was never in doubt. For those who can see.'), +('roll_heckle', 'mysterious', 'insufferable', 'default', 'Hmm. Yes. As I suspected.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- IRRITABLE — air quality mood, moderate coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- irritable × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'irritable', 'friendly_drunk', 'first_roll', 'Go ahead. Roll. Sorry — that came out wrong. Go ahead!'), +('roll_heckle', 'irritable', 'friendly_drunk', 'hot_streak', 'Great roll — sorry, did not mean to sigh. Really, great job.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'whiff', 'Nothing. UGH. No, not at you. You are fine. The dice are fine. Everything is fine.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'win', 'You won! Genuinely happy. The face I am making is happy. Trust me.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'default', 'Good. Fine. Great. Next.'); + +-- irritable × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'irritable', 'angry_mean', 'first_roll', 'What.'), +('roll_heckle', 'irritable', 'angry_mean', 'hot_streak', 'Fine. Take your matches. Whatever.'), +('roll_heckle', 'irritable', 'angry_mean', 'whiff', 'Of course. Of COURSE, {name}.'), +('roll_heckle', 'irritable', 'angry_mean', 'whiff', 'Nothing. How perfectly on brand for right now.'), +('roll_heckle', 'irritable', 'angry_mean', 'win', 'Great. Wonderful. Are we done?'), +('roll_heckle', 'irritable', 'angry_mean', 'default', 'Can we speed this up?'); + +-- irritable × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'irritable', 'loud_obnoxious', 'whiff', 'UGH, ANOTHER ROLL? FINE! IT WAS... OKAY I GUESS!'), +('roll_heckle', 'irritable', 'loud_obnoxious', 'hot_streak', 'YEAH GREAT! WHATEVER! GOOD FOR YOU!'), +('roll_heckle', 'irritable', 'loud_obnoxious', 'win', 'FINALLY! CAN WE ALL JUST... *deep breath* CONGRATULATIONS!'), +('roll_heckle', 'irritable', 'loud_obnoxious', 'default', 'OKAY! SURE! THAT HAPPENED! MOVING ON!'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- SUFFOCATING — air quality mood, moderate coverage +-- ═══════════════════════════════════════════════════════════════════════════ + +-- suffocating × friendly_drunk +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'suffocating', 'friendly_drunk', 'first_roll', 'Go... {name}... you got this... probably...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'hot_streak', 'Amazing... truly... *gasp* ...so proud...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'whiff', 'Nothing... but the effort was... there...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'win', 'You... did it... {name}... hero...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'default', 'Still... rolling... good... good...'); + +-- suffocating × angry_mean +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'suffocating', 'angry_mean', 'whiff', 'I would insult that roll but I do not have the energy.'), +('roll_heckle', 'suffocating', 'angry_mean', 'hot_streak', 'Good for you. I am too tired to be angry. Almost.'), +('roll_heckle', 'suffocating', 'angry_mean', 'win', 'You won. I would clap but my arms are too heavy.'), +('roll_heckle', 'suffocating', 'angry_mean', 'default', 'Sure, {name}. Sure.'); + +-- suffocating × loud_obnoxious +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'suffocating', 'loud_obnoxious', 'hot_streak', 'THAT WAS A GREAT... a great... ugh.'), +('roll_heckle', 'suffocating', 'loud_obnoxious', 'whiff', 'NOTH... nothing. I can not even yell about it.'), +('roll_heckle', 'suffocating', 'loud_obnoxious', 'win', 'THEY WON! THEY... *trails off*'), +('roll_heckle', 'suffocating', 'loud_obnoxious', 'default', 'ROLL... roll... yeah.'); + +-- suffocating × insufferable +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'suffocating', 'insufferable', 'whiff', 'I am expending what little energy I have watching this.'), +('roll_heckle', 'suffocating', 'insufferable', 'hot_streak', 'That was adequate. I think. It is hard to focus.'), +('roll_heckle', 'suffocating', 'insufferable', 'win', 'You won. I would be condescending but I lack the stamina.'), +('roll_heckle', 'suffocating', 'insufferable', 'default', 'Mmm.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- MANIC — air quality + storm, light coverage (fallback to chaotic/unhinged) +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'manic', 'friendly_drunk', 'hot_streak', 'GREAT ROLL AMAZING ROLL YOU ARE THE BEST ROLLER I HAVE EVER — ooh what is that — DICE!'), +('roll_heckle', 'manic', 'friendly_drunk', 'whiff', 'Nothing but also everything because WE ARE HERE and IS THIS NOT GREAT?!'), +('roll_heckle', 'manic', 'friendly_drunk', 'default', 'MORE DICE! MORE {name}! MORE EVERYTHING!'), +('roll_heckle', 'manic', 'loud_obnoxious', 'hot_streak', 'YES! NO! MAYBE! I DO NOT KNOW BUT I AM HERE FOR IT!'), +('roll_heckle', 'manic', 'loud_obnoxious', 'default', 'ROLLROLLROLLROLLROLL!'), +('roll_heckle', 'manic', 'angry_mean', 'whiff', 'That is going to be terrible, I can already — yep. Called it.'), +('roll_heckle', 'manic', 'angry_mean', 'default', 'Already bad. Next one will be worse. Rolling? Already bad.'), +('roll_heckle', 'manic', 'insufferable', 'default', 'Interesting, predictable, expected, noted, moving on.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- RESTLESS — light coverage (fallback to sunny/neutral) +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'restless', 'friendly_drunk', 'default', 'Good roll! Great! What is next? Roll again! Come on!'), +('roll_heckle', 'restless', 'friendly_drunk', 'whiff', 'Nothing? NEXT! Come on come on come on!'), +('roll_heckle', 'restless', 'loud_obnoxious', 'default', 'HURRY UP AND ROLL AGAIN! COME ON!'), +('roll_heckle', 'restless', 'angry_mean', 'default', 'Any day now, {name}.'), +('roll_heckle', 'restless', 'angry_mean', 'whiff', 'Nothing. Can you fail faster?'), +('roll_heckle', 'restless', 'insufferable', 'default', 'I do not mean to rush you, but...'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BITTER — light coverage (fallback to grumpy/melancholic) +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'bitter', 'friendly_drunk', 'hot_streak', 'Good for you, {name}. No really. Some of us never get those but... happy for you.'), +('roll_heckle', 'bitter', 'friendly_drunk', 'whiff', 'Join the club, buddy. The nothing club.'), +('roll_heckle', 'bitter', 'angry_mean', 'hot_streak', 'Sure. YOU get the good rolls. Typical.'), +('roll_heckle', 'bitter', 'angry_mean', 'whiff', 'That is {roll_count} rolls of nothing. Not that anyone is counting.'), +('roll_heckle', 'bitter', 'angry_mean', 'default', 'Must be nice.'), +('roll_heckle', 'bitter', 'insufferable', 'default', 'I have learned not to expect much from rolls like yours.'), +('roll_heckle', 'bitter', 'loud_obnoxious', 'hot_streak', 'OH SURE, THEY GET THE GOOD ROLLS! TYPICAL!'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- STIR_CRAZY — light coverage (fallback to cozy) +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'stir_crazy', 'friendly_drunk', 'default', 'YES ROLL AGAIN! This is literally the best part of my day!'), +('roll_heckle', 'stir_crazy', 'friendly_drunk', 'hot_streak', 'FINALLY something HAPPENED! More! MORE!'), +('roll_heckle', 'stir_crazy', 'loud_obnoxious', 'default', 'FINALLY SOMETHING TO WATCH! GO {name} GO!'), +('roll_heckle', 'stir_crazy', 'angry_mean', 'default', 'Great. Another roll to watch while I am stuck here.'), +('roll_heckle', 'stir_crazy', 'insufferable', 'default', 'I suppose this will have to do for stimulation.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- PARANOID — light coverage (fallback to mysterious) +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'paranoid', 'friendly_drunk', 'hot_streak', 'Great roll! ...Almost too great. No, I trust you. But like...'), +('roll_heckle', 'paranoid', 'friendly_drunk', 'default', 'Nice one, {name}! ...Did anyone else see that?'), +('roll_heckle', 'paranoid', 'loud_obnoxious', 'hot_streak', 'DID ANYONE ELSE SEE THAT?! WAS THAT NORMAL?!'), +('roll_heckle', 'paranoid', 'angry_mean', 'hot_streak', 'Awfully convenient roll there, {name}.'), +('roll_heckle', 'paranoid', 'angry_mean', 'whiff', 'Playing dumb, {name}? I see what you are doing.'), +('roll_heckle', 'paranoid', 'angry_mean', 'default', 'I am watching you, {name}.'), +('roll_heckle', 'paranoid', 'insufferable', 'default', 'Hmm. That roll was... expected.'); diff --git a/migrations/011_tas_more_roll_heckle.sql b/migrations/011_tas_more_roll_heckle.sql new file mode 100644 index 00000000..e1781b74 --- /dev/null +++ b/migrations/011_tas_more_roll_heckle.sql @@ -0,0 +1,533 @@ +-- TAS roll_heckle expansion: ~50 more phrases per mood across all snarkiness × context combos. + +-- ═══════════════════════════════════════════════════════════════════════════ +-- NEUTRAL +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +-- friendly_drunk +('roll_heckle', 'neutral', 'friendly_drunk', 'first_roll', 'Round one, let us RIDE, {name}!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'first_roll', 'Fresh dice, fresh vibes!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'hot_streak', 'You are cooking right now, {name}!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'hot_streak', 'Cannot stop, will not stop!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'hot_streak', 'That is the stuff, buddy!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'whiff', 'The dice owe you one after that.'), +('roll_heckle', 'neutral', 'friendly_drunk', 'whiff', 'Not your best work, but I still believe.'), +('roll_heckle', 'neutral', 'friendly_drunk', 'whiff', 'Did the dice fall asleep? Wake up, dice!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'close', 'You can FEEL it coming!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'close', 'SO close I can hear the finish line!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'default', 'Solid roll, solid vibes.'), +('roll_heckle', 'neutral', 'friendly_drunk', 'default', 'We are in the mix, {name}!'), +('roll_heckle', 'neutral', 'friendly_drunk', 'default', 'Just keep rolling, just keep rolling...'), +-- loud_obnoxious +('roll_heckle', 'neutral', 'loud_obnoxious', 'first_roll', 'ROUND START! EVERYBODY LOOK AT {name}!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'first_roll', 'THE DICE HIT THE TABLE! IT BEGINS!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'hot_streak', '{name} IS A MACHINE! A BEAUTIFUL MACHINE!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'hot_streak', 'THREE MORE LOCKED! SOMEBODY CALL THE MAYOR!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'whiff', 'ZERO! ZILCH! NADA! BUT WE GO AGAIN!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'whiff', 'THE DICE SAID NO BUT I SAY ROLL AGAIN!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'close', 'ONE MORE ROLL COULD END IT! THE PRESSURE!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'close', '{matched} OUT OF TEN! WE ARE IN THE ENDGAME!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'default', 'THE DICE HAVE SPOKEN! {matched} LOCKED!'), +('roll_heckle', 'neutral', 'loud_obnoxious', 'default', '{name} ROLLS! THE CROWD REACTS!'), +-- angry_mean +('roll_heckle', 'neutral', 'angry_mean', 'first_roll', 'Let us see how quickly you disappoint us.'), +('roll_heckle', 'neutral', 'angry_mean', 'first_roll', 'Another round, another chance to fail.'), +('roll_heckle', 'neutral', 'angry_mean', 'hot_streak', 'Do not celebrate yet. The dice giveth and the dice taketh.'), +('roll_heckle', 'neutral', 'angry_mean', 'hot_streak', 'A broken clock is right twice a day. You are on roll one.'), +('roll_heckle', 'neutral', 'angry_mean', 'whiff', 'Nothing. Again. You are establishing a pattern.'), +('roll_heckle', 'neutral', 'angry_mean', 'whiff', 'Have you considered a different hobby?'), +('roll_heckle', 'neutral', 'angry_mean', 'whiff', 'That was aggressively mediocre, {name}.'), +('roll_heckle', 'neutral', 'angry_mean', 'close', 'This close and you will still find a way to blow it.'), +('roll_heckle', 'neutral', 'angry_mean', 'close', 'The finish line is right there. Try not to trip.'), +('roll_heckle', 'neutral', 'angry_mean', 'default', 'I have seen paint dry with more excitement.'), +('roll_heckle', 'neutral', 'angry_mean', 'default', '{matched} matched. I will hold my applause.'), +('roll_heckle', 'neutral', 'angry_mean', 'default', 'The definition of mid, {name}.'), +-- insufferable +('roll_heckle', 'neutral', 'insufferable', 'first_roll', 'And so the experiment begins anew.'), +('roll_heckle', 'neutral', 'insufferable', 'first_roll', 'Fresh round. Lower your expectations accordingly.'), +('roll_heckle', 'neutral', 'insufferable', 'hot_streak', 'A temporary anomaly, I assure you.'), +('roll_heckle', 'neutral', 'insufferable', 'hot_streak', 'Statistically this was bound to happen eventually.'), +('roll_heckle', 'neutral', 'insufferable', 'whiff', 'Zero. A refreshingly honest result.'), +('roll_heckle', 'neutral', 'insufferable', 'whiff', 'The dice are simply reflecting what we all know.'), +('roll_heckle', 'neutral', 'insufferable', 'whiff', 'Nothing matched. How wonderfully consistent of you.'), +('roll_heckle', 'neutral', 'insufferable', 'close', 'Nearly there. Nearly is such a poignant word.'), +('roll_heckle', 'neutral', 'insufferable', 'close', 'So close. I do hope you handle the pressure well.'), +('roll_heckle', 'neutral', 'insufferable', 'default', 'Progress of a sort. If one squints.'), +('roll_heckle', 'neutral', 'insufferable', 'default', 'And the world turns. And {name} rolls.'), +('roll_heckle', 'neutral', 'insufferable', 'default', 'I see. How very... you.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- SUNNY +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'sunny', 'friendly_drunk', 'first_roll', 'The dice are SMILING at you, {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'first_roll', 'This round has your name on it, I can tell!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'hot_streak', 'You are SPARKLING today!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'hot_streak', 'The dice are throwing themselves at you!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'hot_streak', 'Three more! You beautiful genius!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'whiff', 'So what? Life is still amazing!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'whiff', 'Zero but your attitude is a ten, {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'whiff', 'The dice are just building suspense for your comeback!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'close', 'This is YOUR moment, {name}!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'close', 'I can barely contain myself! GO GO GO!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'default', 'Gorgeous roll! Everything is gorgeous!'), +('roll_heckle', 'sunny', 'friendly_drunk', 'default', 'You are doing amazing and I mean that so much.'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'first_roll', 'BEAUTIFUL START! THE BEST START! START OF THE YEAR!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'hot_streak', '{name} IS BLAZING! SOMEBODY GET THE SUNSCREEN!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'hot_streak', 'OH MY GOD OH MY GOD OH MY GOD!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'whiff', 'ZERO BUT IT DOES NOT EVEN MATTER! NEXT ONE! NEXT ONE!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'close', 'THEY ARE RIGHT THERE! I CANNOT BREATHE!'), +('roll_heckle', 'sunny', 'loud_obnoxious', 'default', 'WHAT A TIME TO BE ALIVE AND ROLLING DICE!'), +('roll_heckle', 'sunny', 'angry_mean', 'first_roll', 'Off to a start. How charming.'), +('roll_heckle', 'sunny', 'angry_mean', 'hot_streak', 'Well look at you, having a moment.'), +('roll_heckle', 'sunny', 'angry_mean', 'hot_streak', 'Do not worry, regression to the mean is coming.'), +('roll_heckle', 'sunny', 'angry_mean', 'whiff', 'Aww! Maybe rolling is not your thing!'), +('roll_heckle', 'sunny', 'angry_mean', 'whiff', 'Bless. Zero. On a day this perfect.'), +('roll_heckle', 'sunny', 'angry_mean', 'close', 'Would not it be hilarious if you got stuck here?'), +('roll_heckle', 'sunny', 'angry_mean', 'default', 'Sure, {name}. Sure.'), +('roll_heckle', 'sunny', 'angry_mean', 'default', 'Somewhere out there, someone is rolling better.'), +('roll_heckle', 'sunny', 'insufferable', 'first_roll', 'Ah, the optimism of a first roll.'), +('roll_heckle', 'sunny', 'insufferable', 'hot_streak', 'Enjoy this. Truly. It will not last.'), +('roll_heckle', 'sunny', 'insufferable', 'whiff', 'Well. The dice certainly have opinions today.'), +('roll_heckle', 'sunny', 'insufferable', 'whiff', 'Zero. But the important thing is you tried.'), +('roll_heckle', 'sunny', 'insufferable', 'close', 'Almost. The sweetest and cruelest word.'), +('roll_heckle', 'sunny', 'insufferable', 'default', 'How... adequate. In its own small way.'), +('roll_heckle', 'sunny', 'insufferable', 'default', 'You are giving it your all. I can tell because it is not much.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- GRUMPY +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'grumpy', 'friendly_drunk', 'first_roll', 'Another round. Joy. Good luck I guess.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'first_roll', 'Let us just get through this, {name}.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'hot_streak', 'Okay, that was... genuinely impressive. Ugh.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'hot_streak', 'Fine, I am smiling. Do not make it weird.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'whiff', 'Nothing. Yeah. Same.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'whiff', 'Zero. At least we are in this together.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'whiff', 'The dice are having a worse day than me.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'close', 'Just finish it already so I can relax.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'close', 'You are almost there. Do not make me hope.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'default', 'That was fine. Whatever fine means anymore.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'default', 'Meh. Roll again I guess.'), +('roll_heckle', 'grumpy', 'friendly_drunk', 'default', 'At least you are still trying. That is something.'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'first_roll', 'UGH, FINE! ROLL! SEE IF I CARE!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'hot_streak', 'I HATE HOW GOOD THAT WAS!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'hot_streak', 'STOP MAKING ME EXCITED! I AM TRYING TO BE GRUMPY!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'whiff', 'ZERO! OF COURSE! BECAUSE WHY WOULD ANYTHING WORK!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'whiff', 'THIS GAME IS TESTING MY PATIENCE!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'close', 'SO CLOSE! IF YOU BLOW THIS I SWEAR!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'default', 'WHATEVER! {matched} MATCHED! MOVING ON!'), +('roll_heckle', 'grumpy', 'loud_obnoxious', 'default', 'I GUESS THAT COUNTS AS A ROLL!'), +('roll_heckle', 'grumpy', 'angry_mean', 'first_roll', 'Start the clock on your disappointment.'), +('roll_heckle', 'grumpy', 'angry_mean', 'first_roll', 'Let me guess. It is going to be bad.'), +('roll_heckle', 'grumpy', 'angry_mean', 'hot_streak', 'Suspicious. Nobody rolls that well honestly.'), +('roll_heckle', 'grumpy', 'angry_mean', 'whiff', 'Zero. My expectations have been met.'), +('roll_heckle', 'grumpy', 'angry_mean', 'whiff', 'Nothing. You are a natural at this.'), +('roll_heckle', 'grumpy', 'angry_mean', 'close', 'Almost. Story of your life, probably.'), +('roll_heckle', 'grumpy', 'angry_mean', 'default', 'Unimpressive.'), +('roll_heckle', 'grumpy', 'angry_mean', 'default', 'I expected nothing and I am still let down.'), +('roll_heckle', 'grumpy', 'insufferable', 'first_roll', 'Here we go again. The eternal cycle.'), +('roll_heckle', 'grumpy', 'insufferable', 'first_roll', 'Another round. My enthusiasm knows bounds.'), +('roll_heckle', 'grumpy', 'insufferable', 'hot_streak', 'Even a stopped clock. Congratulations.'), +('roll_heckle', 'grumpy', 'insufferable', 'hot_streak', 'Hm. The universe made a clerical error in your favor.'), +('roll_heckle', 'grumpy', 'insufferable', 'whiff', 'And there it is. The inevitable nothing.'), +('roll_heckle', 'grumpy', 'insufferable', 'whiff', 'Zero. I prepared a speech but it feels redundant.'), +('roll_heckle', 'grumpy', 'insufferable', 'close', 'So close. How Sisyphean.'), +('roll_heckle', 'grumpy', 'insufferable', 'default', 'Adequate. Barely.'), +('roll_heckle', 'grumpy', 'insufferable', 'default', 'I suppose we continue.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BORED +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'bored', 'friendly_drunk', 'first_roll', 'Oh we are rolling? Cool. I was zoning out.'), +('roll_heckle', 'bored', 'friendly_drunk', 'first_roll', 'Right right, dice game. I am here. Mostly.'), +('roll_heckle', 'bored', 'friendly_drunk', 'hot_streak', 'Wait that was actually— yeah okay nice, {name}.'), +('roll_heckle', 'bored', 'friendly_drunk', 'hot_streak', 'Huh! You woke me up with that one.'), +('roll_heckle', 'bored', 'friendly_drunk', 'whiff', 'Nothing. Yep. That tracks.'), +('roll_heckle', 'bored', 'friendly_drunk', 'whiff', 'Zero. Same energy as this whole round honestly.'), +('roll_heckle', 'bored', 'friendly_drunk', 'close', 'Oh wait you are actually close? Do the thing!'), +('roll_heckle', 'bored', 'friendly_drunk', 'close', 'Okay NOW I am paying attention.'), +('roll_heckle', 'bored', 'friendly_drunk', 'default', 'Yep. That is dice alright.'), +('roll_heckle', 'bored', 'friendly_drunk', 'default', 'Cool cool cool cool cool.'), +('roll_heckle', 'bored', 'friendly_drunk', 'default', 'Mhmm. {matched} matched. Neat.'), +('roll_heckle', 'bored', 'loud_obnoxious', 'first_roll', 'ALRIGHT! HERE WE... go. Yeah.'), +('roll_heckle', 'bored', 'loud_obnoxious', 'hot_streak', 'WAIT ACTUALLY! THAT WAS— yeah okay that was good.'), +('roll_heckle', 'bored', 'loud_obnoxious', 'whiff', 'WOWWW. ZERO. I AM... not surprised actually.'), +('roll_heckle', 'bored', 'loud_obnoxious', 'close', 'HOLD ON IS THIS GETTING INTERESTING?!'), +('roll_heckle', 'bored', 'loud_obnoxious', 'default', 'YEP! DICE! GAME! THINGS!'), +('roll_heckle', 'bored', 'angry_mean', 'first_roll', 'Oh you are still here. Rolling. Great.'), +('roll_heckle', 'bored', 'angry_mean', 'hot_streak', 'Finally something worth glancing at.'), +('roll_heckle', 'bored', 'angry_mean', 'hot_streak', 'Huh. Did not think you had it in you.'), +('roll_heckle', 'bored', 'angry_mean', 'whiff', 'Riveting. Truly.'), +('roll_heckle', 'bored', 'angry_mean', 'whiff', 'Nothing. At least make the failures entertaining.'), +('roll_heckle', 'bored', 'angry_mean', 'close', 'Oh you might actually do something. Prove me wrong.'), +('roll_heckle', 'bored', 'angry_mean', 'default', 'Mmm. Dice went somewhere. {matched} matched. Thrilling.'), +('roll_heckle', 'bored', 'angry_mean', 'default', 'I am running out of ways to not care about this.'), +('roll_heckle', 'bored', 'insufferable', 'first_roll', 'New round. Same expectations.'), +('roll_heckle', 'bored', 'insufferable', 'hot_streak', 'Interesting. You have my marginal attention.'), +('roll_heckle', 'bored', 'insufferable', 'whiff', 'Zero. Like my engagement level.'), +('roll_heckle', 'bored', 'insufferable', 'whiff', 'Nothing. I can relate.'), +('roll_heckle', 'bored', 'insufferable', 'close', 'Oh. You are near the end. How novel.'), +('roll_heckle', 'bored', 'insufferable', 'default', 'Duly noted. Moving on.'), +('roll_heckle', 'bored', 'insufferable', 'default', 'The dice have produced a number. Well done, dice.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- COZY +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'cozy', 'friendly_drunk', 'first_roll', 'Grab a blanket and roll, {name}!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'first_roll', 'Nothing like a warm round of dice with friends!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'hot_streak', 'You are on a roll and I am getting cozy watching it!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'hot_streak', 'Warm dice for warm people!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'whiff', 'Aww, nothing? Come here, let me hug it out.'), +('roll_heckle', 'cozy', 'friendly_drunk', 'whiff', 'Zero matches but infinite warmth, {name}.'), +('roll_heckle', 'cozy', 'friendly_drunk', 'close', 'Almost there! I am SNUGGLING with anticipation!'), +('roll_heckle', 'cozy', 'friendly_drunk', 'default', 'Another roll by the fire. This is nice.'), +('roll_heckle', 'cozy', 'friendly_drunk', 'default', 'You are doing great, sweetie. Have some cocoa.'), +('roll_heckle', 'cozy', 'loud_obnoxious', 'first_roll', 'CUDDLE UP AND ROLL, EVERYBODY!'), +('roll_heckle', 'cozy', 'loud_obnoxious', 'hot_streak', 'WARM DICE! HOT STREAK! I AM MAKING SOUP!'), +('roll_heckle', 'cozy', 'loud_obnoxious', 'whiff', 'NOTHING BUT I MADE YOU A BLANKET! KEEP GOING!'), +('roll_heckle', 'cozy', 'loud_obnoxious', 'close', 'SO CLOSE! I AM SCREAMING SOFTLY! LIKE A COZY SCREAM!'), +('roll_heckle', 'cozy', 'loud_obnoxious', 'default', 'THAT WAS LOVELY! HAVE ANOTHER ROLL! AND SOME PIE!'), +('roll_heckle', 'cozy', 'angry_mean', 'first_roll', 'Roll, honey. Do your little roll.'), +('roll_heckle', 'cozy', 'angry_mean', 'hot_streak', 'Good for you, pumpkin. Really. Good for you.'), +('roll_heckle', 'cozy', 'angry_mean', 'whiff', 'Oh honey. That was just precious in its awfulness.'), +('roll_heckle', 'cozy', 'angry_mean', 'whiff', 'Bless your sweet, matchless heart.'), +('roll_heckle', 'cozy', 'angry_mean', 'close', 'So close, darling. Try not to blow it like last time.'), +('roll_heckle', 'cozy', 'angry_mean', 'default', 'Cute effort, {name}. Real cute.'), +('roll_heckle', 'cozy', 'insufferable', 'first_roll', 'Let us begin. I have prepared a comforting space for your failure.'), +('roll_heckle', 'cozy', 'insufferable', 'hot_streak', 'Gold star for you today, {name}. Truly.'), +('roll_heckle', 'cozy', 'insufferable', 'whiff', 'Shhh, shhh. No matches. It is okay. We expected this.'), +('roll_heckle', 'cozy', 'insufferable', 'close', 'Almost there. Shall I hold your hand for the last bit?'), +('roll_heckle', 'cozy', 'insufferable', 'default', 'Another roll, another lesson in humility.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- MELANCHOLIC +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'melancholic', 'friendly_drunk', 'first_roll', 'And so we begin again. Is that not beautiful?'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'first_roll', 'Every first roll is a tiny rebirth, {name}.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'hot_streak', 'A moment of grace in a sea of chaos...'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'hot_streak', 'This is what makes it all worth it. Almost.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'whiff', 'Nothing. But was it not a beautiful nothing?'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'whiff', 'Sometimes the dice reflect what words cannot.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'whiff', 'Zero. The loneliest number since one.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'close', 'So close to something... is that not always the way?'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'default', 'Another roll, another memory we will forget.'), +('roll_heckle', 'melancholic', 'friendly_drunk', 'default', 'The dice tumble like time, {name}. Always forward.'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'first_roll', 'AND SO THE TRAGEDY OF ROUND {roll_count} BEGINS!'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'hot_streak', 'A BRIEF LIGHT IN THE DARKNESS! SAVOR IT!'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'whiff', 'ZERO! *WAILS INTO THE ABYSS*'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'whiff', 'THE DICE HAVE FORSAKEN US! WHY! WHYYY!'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'close', 'SO CLOSE TO MEANING! SO CLOSE TO PURPOSE!'), +('roll_heckle', 'melancholic', 'loud_obnoxious', 'default', 'AND THE TALE CONTINUES! BITTERSWEET AND LOUD!'), +('roll_heckle', 'melancholic', 'angry_mean', 'first_roll', 'Begin. It will end badly. They always do.'), +('roll_heckle', 'melancholic', 'angry_mean', 'hot_streak', 'Good roll. You will miss it when the streak ends.'), +('roll_heckle', 'melancholic', 'angry_mean', 'whiff', 'Nothing. Why do we even roll, {name}?'), +('roll_heckle', 'melancholic', 'angry_mean', 'whiff', 'Zero. Fitting. So fitting.'), +('roll_heckle', 'melancholic', 'angry_mean', 'close', 'Almost there. But almost never quite arrives.'), +('roll_heckle', 'melancholic', 'angry_mean', 'default', 'And so it goes.'), +('roll_heckle', 'melancholic', 'insufferable', 'first_roll', 'Shall we? The dice await their next disappointment.'), +('roll_heckle', 'melancholic', 'insufferable', 'hot_streak', 'A fine roll. It almost makes one believe in things.'), +('roll_heckle', 'melancholic', 'insufferable', 'whiff', 'Nothing. There is a poetry in that, if one listens.'), +('roll_heckle', 'melancholic', 'insufferable', 'close', 'The precipice. Most people fall from precipices.'), +('roll_heckle', 'melancholic', 'insufferable', 'default', 'The dice persist. As do we, inexplicably.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- CHAOTIC +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'chaotic', 'friendly_drunk', 'first_roll', 'ROLL! Wait which game is — ROLL!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'first_roll', 'New round new dice new — oh hey {name}!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'hot_streak', 'You got — wait how many — a LOT! Nice!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'hot_streak', 'YES that is the — ooh dice — YES!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'whiff', 'Zero! But also — wait — yeah zero. NEXT!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'whiff', 'Nothing but I just remembered you are great!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'close', 'Almost — wait IS that almost? COUNT THEM! YES ALMOST!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'default', 'DICE! THINGS! {name}! YEAH!'), +('roll_heckle', 'chaotic', 'friendly_drunk', 'default', 'I lost count of the matched ones but VIBES!'), +('roll_heckle', 'chaotic', 'loud_obnoxious', 'first_roll', 'EVERYTHING IS HAPPENING! DICE ARE FLYING!'), +('roll_heckle', 'chaotic', 'loud_obnoxious', 'hot_streak', 'AHHH THREE MORE! OR FOUR! I LOST COUNT! AHHHH!'), +('roll_heckle', 'chaotic', 'loud_obnoxious', 'whiff', 'NOTHING! BUT DID YOU SEE HOW THEY BOUNCED?!'), +('roll_heckle', 'chaotic', 'loud_obnoxious', 'close', 'IS THIS IT?! IS THIS THE ONE?! MAYBE?! AHHHH!'), +('roll_heckle', 'chaotic', 'loud_obnoxious', 'default', 'DICE WENT PLACES! NUMBERS HAPPENED! LOUD NOISES!'), +('roll_heckle', 'chaotic', 'angry_mean', 'first_roll', 'What — why are you — just roll already!'), +('roll_heckle', 'chaotic', 'angry_mean', 'hot_streak', 'FINE that was good but your LAST roll was garbage and I am still — next!'), +('roll_heckle', 'chaotic', 'angry_mean', 'whiff', 'Nothing and — wait was that your roll or — UGH regardless!'), +('roll_heckle', 'chaotic', 'angry_mean', 'close', 'Finish it or I swear I will — wait what number are we on?!'), +('roll_heckle', 'chaotic', 'angry_mean', 'default', 'That roll makes me angry for reasons I cannot identify!'), +('roll_heckle', 'chaotic', 'insufferable', 'first_roll', 'And so the chaos resumes. How tediously unpredictable.'), +('roll_heckle', 'chaotic', 'insufferable', 'hot_streak', 'An anomaly within an anomaly. How very meta.'), +('roll_heckle', 'chaotic', 'insufferable', 'whiff', 'Nothing. Disorder tends toward entropy, after all.'), +('roll_heckle', 'chaotic', 'insufferable', 'close', 'Nearly there. In chaos theory there is a word for this.'), +('roll_heckle', 'chaotic', 'insufferable', 'default', 'Another data point in the noise. Fascinating.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- UNHINGED +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'unhinged', 'friendly_drunk', 'first_roll', 'I can TASTE the dice and they taste like VICTORY!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'first_roll', 'The round is young and I am FERAL with excitement!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'hot_streak', 'You are doing it! The thing! The dice thing! I LOVE IT!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'hot_streak', 'If those dice were people I would hug them ALL!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'hot_streak', '{name} is a DEITY and I am a BELIEVER!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'whiff', 'Zero but I love you MORE than before somehow!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'whiff', 'Nothing matched but EVERYTHING matched in my heart!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'whiff', 'The dice said no but my SOUL said yes!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'close', 'SO CLOSE I can hear angels SINGING your name!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'close', '{matched} matched! I am ASCENDING!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'default', 'I have FEELINGS about that roll and ALL of them are big!'), +('roll_heckle', 'unhinged', 'friendly_drunk', 'default', 'Did anyone else just feel the earth SHIFT?!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'first_roll', 'IT BEGINS! THE DICE AWAKEN! AHHHHHHH!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'hot_streak', 'YES YES YES YES YES YES YES!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'whiff', 'NOTHING AND I HAVE NEVER FELT MORE ALIVE!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'whiff', 'ZERO! THE VOID IS SCREAMING! I AM ALSO SCREAMING!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'close', 'ALMOST! I CANNOT FEEL MY FACE! IS THAT NORMAL?!'), +('roll_heckle', 'unhinged', 'loud_obnoxious', 'default', 'WORDS! DICE! FEELINGS! ALL OF THEM! AT ONCE!'), +('roll_heckle', 'unhinged', 'angry_mean', 'first_roll', 'Roll. I have a bone to pick with fate itself.'), +('roll_heckle', 'unhinged', 'angry_mean', 'first_roll', 'The dice and I have an arrangement. They will fail you.'), +('roll_heckle', 'unhinged', 'angry_mean', 'hot_streak', 'THAT ROLL SHOULD NOT EXIST. I reject it philosophically.'), +('roll_heckle', 'unhinged', 'angry_mean', 'hot_streak', 'Good roll. I am going to have a word with gravity.'), +('roll_heckle', 'unhinged', 'angry_mean', 'whiff', 'The dice KNOW what you did, {name}.'), +('roll_heckle', 'unhinged', 'angry_mean', 'whiff', 'Zero. The dice and I are in agreement for once.'), +('roll_heckle', 'unhinged', 'angry_mean', 'close', 'Almost there and the TENSION is making me dangerous.'), +('roll_heckle', 'unhinged', 'angry_mean', 'default', 'Something about that roll has fundamentally changed me.'), +('roll_heckle', 'unhinged', 'insufferable', 'first_roll', 'The dice tremble. As they should.'), +('roll_heckle', 'unhinged', 'insufferable', 'hot_streak', 'Good. The dice are learning their place.'), +('roll_heckle', 'unhinged', 'insufferable', 'whiff', 'Nothing. I predicted this. I predicted everything.'), +('roll_heckle', 'unhinged', 'insufferable', 'close', 'So close. The universe is testing my patience. Again.'), +('roll_heckle', 'unhinged', 'insufferable', 'default', 'That roll exists outside of conventional analysis.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- MYSTERIOUS +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'mysterious', 'friendly_drunk', 'first_roll', 'The cards said — wait, wrong game. The DICE say go!'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'first_roll', 'I sense... dice. Yes, definitely dice.'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'hot_streak', 'The spirits are with you! Or maybe that is the drinks.'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'hot_streak', 'Destiny! Or luck! One of those for sure!'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'whiff', 'The void giveth nothing. But in a nice way!'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'whiff', 'Zero. The tea leaves did warn me about this.'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'close', 'I foresee... closeness. {matched} out of ten closeness!'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'default', 'The mists reveal... {matched} matched. Mysterious!'), +('roll_heckle', 'mysterious', 'friendly_drunk', 'default', 'Interesting. Very interesting. Quite.'), +('roll_heckle', 'mysterious', 'loud_obnoxious', 'first_roll', 'THE ANCIENT PROPHECY OF ROUND ONE! IT BEGINS!'), +('roll_heckle', 'mysterious', 'loud_obnoxious', 'hot_streak', 'THE ORACLE SCREAMS! THE DICE OBEY!'), +('roll_heckle', 'mysterious', 'loud_obnoxious', 'whiff', 'THE SPIRITS SAY NOTHING! BECAUSE THERE IS NOTHING!'), +('roll_heckle', 'mysterious', 'loud_obnoxious', 'close', 'THE SIGNS ALIGN! THIS COULD BE THE ONE!'), +('roll_heckle', 'mysterious', 'loud_obnoxious', 'default', 'THE DICE HAVE SPOKEN AND THEY SPOKE {matched}!'), +('roll_heckle', 'mysterious', 'angry_mean', 'first_roll', 'I already know how this ends. Badly.'), +('roll_heckle', 'mysterious', 'angry_mean', 'hot_streak', 'The dice favor you. They are wrong to do so.'), +('roll_heckle', 'mysterious', 'angry_mean', 'whiff', 'The darkness saw that coming. As did I.'), +('roll_heckle', 'mysterious', 'angry_mean', 'close', 'Almost. The fates enjoy watching you squirm.'), +('roll_heckle', 'mysterious', 'angry_mean', 'default', 'A reading: {matched} matched. The spirits are unimpressed.'), +('roll_heckle', 'mysterious', 'insufferable', 'first_roll', 'The signs are... present. Make of that what you will.'), +('roll_heckle', 'mysterious', 'insufferable', 'hot_streak', 'Ah yes. The pattern I predicted three rolls ago.'), +('roll_heckle', 'mysterious', 'insufferable', 'whiff', 'I could have told you that would happen. I did not. But I could have.'), +('roll_heckle', 'mysterious', 'insufferable', 'close', 'The threshold approaches. Whether you cross it is... not up to you.'), +('roll_heckle', 'mysterious', 'insufferable', 'default', 'All is as it was meant to be. Unfortunately for you.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- IRRITABLE +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'irritable', 'friendly_drunk', 'first_roll', 'Fine. Roll. Sorry if I seem— just go.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'first_roll', 'New round. Great. I am thrilled. Ish.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'hot_streak', 'That was good! See? Good things. I am not gritting my teeth.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'hot_streak', 'Nice one, {name}. Genuinely. Do not ask why I sighed.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'whiff', 'Nothing. It is FINE. Everything is FINE.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'whiff', 'Zero. I am supportive. Supportively annoyed, but supportive.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'close', 'Just finish it. Please. I need this to end well.'), +('roll_heckle', 'irritable', 'friendly_drunk', 'default', 'Yeah. Good. Fine. Whatever. You are doing fine.'), +('roll_heckle', 'irritable', 'loud_obnoxious', 'first_roll', 'CAN WE JUST— UGH— FINE! ROLL!'), +('roll_heckle', 'irritable', 'loud_obnoxious', 'hot_streak', 'OKAY THAT WAS ACTUALLY— *deep breath* — GOOD!'), +('roll_heckle', 'irritable', 'loud_obnoxious', 'whiff', 'SERIOUSLY?! NOTHING?! I CANNOT— *breathes* — FINE!'), +('roll_heckle', 'irritable', 'loud_obnoxious', 'close', 'JUST END IT! PLEASE! MY NERVES!'), +('roll_heckle', 'irritable', 'loud_obnoxious', 'default', '{matched} MATCHED! WHATEVER! NEXT!'), +('roll_heckle', 'irritable', 'angry_mean', 'first_roll', 'What. Get on with it.'), +('roll_heckle', 'irritable', 'angry_mean', 'hot_streak', 'Good. Finally. Was that so hard?'), +('roll_heckle', 'irritable', 'angry_mean', 'whiff', 'Zero. You are TESTING me, {name}.'), +('roll_heckle', 'irritable', 'angry_mean', 'whiff', 'Nothing. My patience is a finite resource and you are spending it.'), +('roll_heckle', 'irritable', 'angry_mean', 'close', 'Close. If you do not finish this soon I will lose it.'), +('roll_heckle', 'irritable', 'angry_mean', 'default', 'Sure. {matched}. Fantastic.'), +('roll_heckle', 'irritable', 'insufferable', 'first_roll', 'Must we? Yes, I suppose we must.'), +('roll_heckle', 'irritable', 'insufferable', 'hot_streak', 'Adequate. Finally.'), +('roll_heckle', 'irritable', 'insufferable', 'whiff', 'Zero. My patience for this is not.'), +('roll_heckle', 'irritable', 'insufferable', 'close', 'Close. Please do not make me endure another round of this.'), +('roll_heckle', 'irritable', 'insufferable', 'default', 'Noted. Reluctantly.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- SUFFOCATING +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'suffocating', 'friendly_drunk', 'first_roll', 'Here... we... go... *wheeze*'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'first_roll', 'New round... I am here... barely...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'hot_streak', 'That was... so good... I might... faint...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'hot_streak', 'Amazing... I would clap but... my arms...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'whiff', 'Nothing... same... energy... as me...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'whiff', 'Zero... we are all zeros... today...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'close', 'So close... I believe... in... you...'), +('roll_heckle', 'suffocating', 'friendly_drunk', 'default', 'Still here... still watching... mostly...'), +('roll_heckle', 'suffocating', 'loud_obnoxious', 'first_roll', 'ROLL! DICE! I AM... *trails off*'), +('roll_heckle', 'suffocating', 'loud_obnoxious', 'hot_streak', 'AMAZ... amaz... yeah.'), +('roll_heckle', 'suffocating', 'loud_obnoxious', 'whiff', 'NOTH... I cannot even finish the word.'), +('roll_heckle', 'suffocating', 'loud_obnoxious', 'close', 'SO CL... so close... ugh...'), +('roll_heckle', 'suffocating', 'loud_obnoxious', 'default', '{matched}... MATCH... matched. Yeah.'), +('roll_heckle', 'suffocating', 'angry_mean', 'first_roll', 'Roll. I cannot muster anything else.'), +('roll_heckle', 'suffocating', 'angry_mean', 'hot_streak', 'Good roll. I hate that I have to acknowledge it.'), +('roll_heckle', 'suffocating', 'angry_mean', 'whiff', 'Zero. At least we match.'), +('roll_heckle', 'suffocating', 'angry_mean', 'close', 'Almost. Can you finish before I run out of energy to care?'), +('roll_heckle', 'suffocating', 'angry_mean', 'default', 'That happened. I witnessed it. Barely.'), +('roll_heckle', 'suffocating', 'insufferable', 'first_roll', 'Another round. My enthusiasm has flatlined.'), +('roll_heckle', 'suffocating', 'insufferable', 'hot_streak', 'Good. I would elaborate but I physically cannot.'), +('roll_heckle', 'suffocating', 'insufferable', 'whiff', 'Zero. Even the commentary is running on fumes.'), +('roll_heckle', 'suffocating', 'insufferable', 'close', 'Almost. Please. I need this to end.'), +('roll_heckle', 'suffocating', 'insufferable', 'default', 'The bare minimum of acknowledgment: {matched} matched.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- MANIC +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'manic', 'friendly_drunk', 'first_roll', 'FIRSTROLL BESTROLL LETSGOOOO {name}!'), +('roll_heckle', 'manic', 'friendly_drunk', 'first_roll', 'New round new you new dice new EVERYTHING!'), +('roll_heckle', 'manic', 'friendly_drunk', 'hot_streak', 'YES AND THEN — MORE AND — {name} IS — YES!'), +('roll_heckle', 'manic', 'friendly_drunk', 'whiff', 'ZERO BUT HONESTLY WHO IS COUNTING — I AM — ZERO — NEXT!'), +('roll_heckle', 'manic', 'friendly_drunk', 'close', 'ALMOSTALMOSTALMOST COME ON COME ON!'), +('roll_heckle', 'manic', 'friendly_drunk', 'default', 'ROLLROLLROLL LOVE IT LOVE YOU LOVE DICE!'), +('roll_heckle', 'manic', 'loud_obnoxious', 'first_roll', 'AAAND WE ARE OFF! IMMEDIATELY! RIGHT NOW! GO!'), +('roll_heckle', 'manic', 'loud_obnoxious', 'hot_streak', 'MORE! MATCHED! HOW MANY! WHO CARES! MORE!'), +('roll_heckle', 'manic', 'loud_obnoxious', 'whiff', 'NOTHING! DOES NOT MATTER! NEXT! NOW! GO! ROLL!'), +('roll_heckle', 'manic', 'loud_obnoxious', 'close', 'THISCOULDBETHEONE THISCOULDBETHEONE!'), +('roll_heckle', 'manic', 'loud_obnoxious', 'default', 'THINGS! HAPPENING! FAST! YES!'), +('roll_heckle', 'manic', 'angry_mean', 'first_roll', 'Roll. Faster. Faster than that. GO.'), +('roll_heckle', 'manic', 'angry_mean', 'hot_streak', 'Good — next — come on — good is not enough — MORE.'), +('roll_heckle', 'manic', 'angry_mean', 'whiff', 'Nothing — already hated it — roll again — NOW.'), +('roll_heckle', 'manic', 'angry_mean', 'close', 'FINISH. IT. I cannot take another roll of this.'), +('roll_heckle', 'manic', 'angry_mean', 'default', 'Too slow. {matched} matched. Not enough. Again.'), +('roll_heckle', 'manic', 'insufferable', 'first_roll', 'Begin. I have already analyzed the next twelve outcomes.'), +('roll_heckle', 'manic', 'insufferable', 'hot_streak', 'As I predicted four microseconds ago. Next.'), +('roll_heckle', 'manic', 'insufferable', 'whiff', 'Zero. I processed this disappointment before you did. Moving on.'), +('roll_heckle', 'manic', 'insufferable', 'close', 'Almost. I knew before you rolled. Finish already.'), +('roll_heckle', 'manic', 'insufferable', 'default', 'Noted analyzed catalogued dismissed. Next.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- RESTLESS +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'restless', 'friendly_drunk', 'first_roll', 'Finally! First roll! I have been WAITING!'), +('roll_heckle', 'restless', 'friendly_drunk', 'hot_streak', 'YES keep going keep going keep going!'), +('roll_heckle', 'restless', 'friendly_drunk', 'hot_streak', 'More! Faster! You are on a STREAK!'), +('roll_heckle', 'restless', 'friendly_drunk', 'whiff', 'Nothing?! Quick, roll again before the energy dies!'), +('roll_heckle', 'restless', 'friendly_drunk', 'close', 'SO close just ONE more come ON!'), +('roll_heckle', 'restless', 'friendly_drunk', 'default', 'Good now AGAIN! No breaks! MOMENTUM!'), +('roll_heckle', 'restless', 'loud_obnoxious', 'first_roll', 'FINALLY WE ARE ROLLING! I COULD NOT WAIT ANYMORE!'), +('roll_heckle', 'restless', 'loud_obnoxious', 'hot_streak', 'FASTER! MORE! DO NOT STOP! EVER!'), +('roll_heckle', 'restless', 'loud_obnoxious', 'whiff', 'NOTHING?! ROLL AGAIN! NOW! IMMEDIATELY!'), +('roll_heckle', 'restless', 'loud_obnoxious', 'close', 'COME ONNN! FINISH THIS! I CANNOT TAKE THE WAITING!'), +('roll_heckle', 'restless', 'loud_obnoxious', 'default', 'NEXT ROLL! NOW! LET US GO!'), +('roll_heckle', 'restless', 'angry_mean', 'first_roll', 'About time. Get moving, {name}.'), +('roll_heckle', 'restless', 'angry_mean', 'hot_streak', 'Good. Keep that pace. Do not slow down.'), +('roll_heckle', 'restless', 'angry_mean', 'whiff', 'Nothing. Wonderful. Can you fail more quickly?'), +('roll_heckle', 'restless', 'angry_mean', 'close', 'Finish already. My patience ran out three rolls ago.'), +('roll_heckle', 'restless', 'angry_mean', 'default', 'Tick tock, {name}. Tick. Tock.'), +('roll_heckle', 'restless', 'insufferable', 'first_roll', 'Oh finally. I have been waiting since the last ice age.'), +('roll_heckle', 'restless', 'insufferable', 'hot_streak', 'Good. Now do that again immediately.'), +('roll_heckle', 'restless', 'insufferable', 'whiff', 'Nothing. Do try to be more efficient with your failures.'), +('roll_heckle', 'restless', 'insufferable', 'close', 'Almost. I aged visibly waiting for this.'), +('roll_heckle', 'restless', 'insufferable', 'default', 'Noted. Could you note things faster?'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- BITTER +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'bitter', 'friendly_drunk', 'first_roll', 'Good luck. Some of us never had any but... you go.'), +('roll_heckle', 'bitter', 'friendly_drunk', 'hot_streak', 'Must be nice. No really. Must be really nice.'), +('roll_heckle', 'bitter', 'friendly_drunk', 'whiff', 'Welcome to the nothing club. Meetings are constant.'), +('roll_heckle', 'bitter', 'friendly_drunk', 'whiff', 'Zero. I know the feeling, buddy. Intimately.'), +('roll_heckle', 'bitter', 'friendly_drunk', 'close', 'Almost there. That is where I live. Almost There.'), +('roll_heckle', 'bitter', 'friendly_drunk', 'default', 'Nice roll. Nicer than mine ever were. But nice.'), +('roll_heckle', 'bitter', 'loud_obnoxious', 'first_roll', 'OH LOOK! ANOTHER ROUND FOR EVERYONE ELSE TO ENJOY!'), +('roll_heckle', 'bitter', 'loud_obnoxious', 'whiff', 'NOTHING! JOIN ME IN THE PIT, {name}!'), +('roll_heckle', 'bitter', 'loud_obnoxious', 'close', 'SO CLOSE! I WOULD NOT KNOW WHAT THAT FEELS LIKE!'), +('roll_heckle', 'bitter', 'loud_obnoxious', 'default', 'COOL! GREAT ROLL! FOR {name}! AS ALWAYS! NOT JEALOUS!'), +('roll_heckle', 'bitter', 'angry_mean', 'first_roll', 'Roll away. Some of us remember when rolls meant something.'), +('roll_heckle', 'bitter', 'angry_mean', 'hot_streak', 'Of course. The dice love you. They never loved me.'), +('roll_heckle', 'bitter', 'angry_mean', 'whiff', 'Nothing. Now you know how it feels. Get comfortable.'), +('roll_heckle', 'bitter', 'angry_mean', 'close', 'Almost. I have been almost for years. You will get used to it.'), +('roll_heckle', 'bitter', 'angry_mean', 'default', 'Good for you, {name}. Really. Good. For. You.'), +('roll_heckle', 'bitter', 'insufferable', 'first_roll', 'Another round. For those of us still trying.'), +('roll_heckle', 'bitter', 'insufferable', 'hot_streak', 'How wonderful for you. Some of us peaked long ago.'), +('roll_heckle', 'bitter', 'insufferable', 'whiff', 'Nothing. I could have told you. I have experience with nothing.'), +('roll_heckle', 'bitter', 'insufferable', 'close', 'Almost. A word I have come to despise.'), +('roll_heckle', 'bitter', 'insufferable', 'default', 'Enjoy your {matched}. Some of us appreciate what we have.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- STIR_CRAZY +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'stir_crazy', 'friendly_drunk', 'first_roll', 'FINALLY a roll! I was going NUTS over here!'), +('roll_heckle', 'stir_crazy', 'friendly_drunk', 'hot_streak', 'YES! Action! Drama! DICE! I NEEDED this!'), +('roll_heckle', 'stir_crazy', 'friendly_drunk', 'hot_streak', 'More matched! More excitement! DO NOT STOP!'), +('roll_heckle', 'stir_crazy', 'friendly_drunk', 'whiff', 'Nothing?! Quick, roll again, I need STIMULATION!'), +('roll_heckle', 'stir_crazy', 'friendly_drunk', 'close', 'Almost! I am VIBRATING with anticipation!'), +('roll_heckle', 'stir_crazy', 'friendly_drunk', 'default', 'Any roll is a good roll when you have been this bored!'), +('roll_heckle', 'stir_crazy', 'loud_obnoxious', 'first_roll', 'A ROLL! AN ACTUAL ROLL! I HAVE BEEN WAITING FOREVER!'), +('roll_heckle', 'stir_crazy', 'loud_obnoxious', 'hot_streak', 'YES YES YES THINGS ARE HAPPENING! FINALLY!'), +('roll_heckle', 'stir_crazy', 'loud_obnoxious', 'whiff', 'NOTHING! BUT AT LEAST SOMETHING HAPPENED! ROLL AGAIN!'), +('roll_heckle', 'stir_crazy', 'loud_obnoxious', 'close', 'ALMOST! I WILL LITERALLY EXPLODE IF THIS DOES NOT END SOON!'), +('roll_heckle', 'stir_crazy', 'loud_obnoxious', 'default', 'MORE! ROLL MORE! I NEED MORE DICE IN MY LIFE!'), +('roll_heckle', 'stir_crazy', 'angry_mean', 'first_roll', 'Finally rolling. I was about to lose my mind. More than I have.'), +('roll_heckle', 'stir_crazy', 'angry_mean', 'hot_streak', 'Good. At least something is happening around here.'), +('roll_heckle', 'stir_crazy', 'angry_mean', 'whiff', 'Nothing. Even the entertainment is disappointing.'), +('roll_heckle', 'stir_crazy', 'angry_mean', 'close', 'Finish it. I cannot take another minute of this.'), +('roll_heckle', 'stir_crazy', 'angry_mean', 'default', 'That passed the time. Barely.'), +('roll_heckle', 'stir_crazy', 'insufferable', 'first_roll', 'Oh a roll. How thrilling. I have been so deprived of stimulation.'), +('roll_heckle', 'stir_crazy', 'insufferable', 'hot_streak', 'Finally, something worth my dwindling attention.'), +('roll_heckle', 'stir_crazy', 'insufferable', 'whiff', 'Nothing. The entertainment value of this is approaching zero.'), +('roll_heckle', 'stir_crazy', 'insufferable', 'close', 'Almost. Please deliver. I am desperate for a conclusion.'), +('roll_heckle', 'stir_crazy', 'insufferable', 'default', 'A roll occurred. In the grand tapestry of my confinement, it was something.'); + +-- ═══════════════════════════════════════════════════════════════════════════ +-- PARANOID +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) VALUES +('roll_heckle', 'paranoid', 'friendly_drunk', 'first_roll', 'Good luck! And I mean that! Unless... no, good luck!'), +('roll_heckle', 'paranoid', 'friendly_drunk', 'first_roll', 'First roll! No pressure! Why would there be pressure? Right?'), +('roll_heckle', 'paranoid', 'friendly_drunk', 'hot_streak', 'Great roll! Great! ...Is it too great? No! Great! ...Right?'), +('roll_heckle', 'paranoid', 'friendly_drunk', 'whiff', 'Nothing. Was that on purpose? No, sorry. Bad roll. Sympathies.'), +('roll_heckle', 'paranoid', 'friendly_drunk', 'whiff', 'Zero. Which is also the number of suspicious things here. Probably.'), +('roll_heckle', 'paranoid', 'friendly_drunk', 'close', 'So close! You are definitely going to— wait why that many?'), +('roll_heckle', 'paranoid', 'friendly_drunk', 'default', 'Nice roll! Normal roll! Nothing weird! Right?'), +('roll_heckle', 'paranoid', 'loud_obnoxious', 'first_roll', 'THE ROUND BEGINS! IS EVERYONE WATCHING?! EVERYONE SHOULD WATCH!'), +('roll_heckle', 'paranoid', 'loud_obnoxious', 'hot_streak', 'HOW?! HOW DID THEY— NO THAT IS FINE! THAT IS FINE! IS IT?!'), +('roll_heckle', 'paranoid', 'loud_obnoxious', 'whiff', 'NOTHING! THAT SEEMS... DELIBERATE! DOES IT NOT?!'), +('roll_heckle', 'paranoid', 'loud_obnoxious', 'close', 'ALMOST! BUT ALMOST WHAT?! WHAT IS REALLY HAPPENING?!'), +('roll_heckle', 'paranoid', 'loud_obnoxious', 'default', 'HMM! OKAY! SURE! I HAVE QUESTIONS BUT SURE!'), +('roll_heckle', 'paranoid', 'angry_mean', 'first_roll', 'Roll. I will be watching. Closely.'), +('roll_heckle', 'paranoid', 'angry_mean', 'hot_streak', 'Interesting roll, {name}. Very interesting. Explain it.'), +('roll_heckle', 'paranoid', 'angry_mean', 'hot_streak', 'Three in a row? The odds of that are... suspicious.'), +('roll_heckle', 'paranoid', 'angry_mean', 'whiff', 'Nothing. Convenient. Are you sandbagging?'), +('roll_heckle', 'paranoid', 'angry_mean', 'close', '{matched} out of 10. You planned this. I know you did.'), +('roll_heckle', 'paranoid', 'angry_mean', 'default', 'Noted. Filed. Cross-referenced.'), +('roll_heckle', 'paranoid', 'insufferable', 'first_roll', 'Interesting. I shall observe. Closely.'), +('roll_heckle', 'paranoid', 'insufferable', 'hot_streak', 'Statistically improbable. I have made a note.'), +('roll_heckle', 'paranoid', 'insufferable', 'whiff', 'Nothing. Or so it appears. Things are rarely what they seem.'), +('roll_heckle', 'paranoid', 'insufferable', 'close', 'Almost there. The question is: by design or by accident?'), +('roll_heckle', 'paranoid', 'insufferable', 'default', 'Interesting. Everything about that was... interesting.'); diff --git a/server/config.py b/server/config.py index a6522dab..3c8bc224 100644 --- a/server/config.py +++ b/server/config.py @@ -215,6 +215,18 @@ def _list(name: str) -> list[str]: DRAND_POLL_INTERVAL = _float("DRAND_POLL_INTERVAL", 3.0) +# ─── Tensies Attitude System (TAS) ────────────────────────────────────── +# Weather-driven personality that heckles/cheers players. Phrases live in +# Postgres (tas_phrases); mood is derived from weather + air quality at the +# server's location. Set TAS_LAT/TAS_LON to enable weather polling; without +# them mood stays "neutral" (phrases still serve, just from the neutral pool). +TAS_ENABLED = _flag("TAS_ENABLED", True) +TAS_LAT = os.environ.get("TAS_LAT") or None +TAS_LON = os.environ.get("TAS_LON") or None +TAS_SNARKINESS = os.environ.get("TAS_SNARKINESS", "friendly_drunk") +TAS_WEATHER_INTERVAL = _float("TAS_WEATHER_INTERVAL", 1800.0) + + # ─── WebAuthn / user accounts ───────────────────────────────────────── WEBAUTHN_RP_ID = os.environ.get("WEBAUTHN_RP_ID", "localhost") WEBAUTHN_RP_NAME = os.environ.get("WEBAUTHN_RP_NAME", "Tensies") diff --git a/server/tas.py b/server/tas.py new file mode 100644 index 00000000..c9b6bbea --- /dev/null +++ b/server/tas.py @@ -0,0 +1,263 @@ +"""Tensies Attitude System (TAS). + +Weather-driven personality that delivers contextual commentary to players. +Phrases live in Postgres (tas_phrases), cached in-memory at startup. Mood is +derived from weather + air quality at the server's configured location via +Open-Meteo (free, no API key). The mood source is never revealed to players. + +Lifecycle mirrors server/drand.py: module-level state, start()/stop(), +background poll loop, sync public API on the hot path. +""" +import asyncio +import random +from collections import deque + +import httpx + +from . import db +from .config import ( + TAS_ENABLED, + TAS_LAT, + TAS_LON, + TAS_SNARKINESS, + TAS_WEATHER_INTERVAL, + log, +) + +# ── Process-local state ────────────────────────────────────────────────── +_task: asyncio.Task | None = None +_http: httpx.AsyncClient | None = None +_mood: str = "neutral" + +# Phrase cache: _phrases[phrase_type][snarkiness][mood][context_tag] -> list[str] +_phrases: dict[str, dict[str, dict[str, dict[str, list[str]]]]] = {} + +# Per-player recently-shown phrases (process-local). Tracks the last N phrases +# shown to each player so we avoid repeats until the pool is exhausted. +_RECENT_SIZE = 20 +_recent: dict[str, deque[str]] = {} # player_id -> deque of recent phrases + +# ── Weather code → base mood ───────────────────────────────────────────── +_WEATHER_MOOD: list[tuple[range, str]] = [ + (range(0, 2), "sunny"), + (range(2, 4), "bored"), + (range(45, 49), "mysterious"), + (range(51, 56), "melancholic"), + (range(56, 68), "grumpy"), + (range(71, 78), "cozy"), + (range(80, 83), "chaotic"), + (range(85, 87), "cozy"), + (range(95, 100), "unhinged"), +] + +# ── AQI moderate-shift table (AQI 41–60) ───────────────────────────────── +_AQI_EDGE_SHIFT: dict[str, str] = { + "sunny": "restless", + "bored": "irritable", + "melancholic": "bitter", + "grumpy": "irritable", + "cozy": "stir_crazy", + "chaotic": "manic", + "unhinged": "manic", + "mysterious": "paranoid", + "neutral": "irritable", +} + + +def _weather_code_to_mood(code: int | None) -> str: + if code is None: + return "neutral" + for rng, mood in _WEATHER_MOOD: + if code in rng: + return mood + return "neutral" + + +def _apply_aqi(base_mood: str, aqi: float | None) -> str: + if aqi is None or aqi <= 40: + return base_mood + if aqi <= 60: + return _AQI_EDGE_SHIFT.get(base_mood, base_mood) + if aqi <= 80: + return "irritable" + return "suffocating" + + +# ── Lifecycle ───────────────────────────────────────────────────────────── + +async def start() -> None: + global _task, _http + if not TAS_ENABLED: + log.info("tas disabled") + return + + await _load_phrases() + + if TAS_LAT and TAS_LON: + _http = httpx.AsyncClient(timeout=10.0) + _task = asyncio.create_task(_poll_loop(), name="tas.weather") + log.info( + "tas started lat=%s lon=%s snarkiness=%s interval=%ss", + TAS_LAT, TAS_LON, TAS_SNARKINESS, TAS_WEATHER_INTERVAL, + ) + else: + log.info( + "tas started mood=neutral (no TAS_LAT/TAS_LON) snarkiness=%s", + TAS_SNARKINESS, + ) + + +async def stop() -> None: + global _task, _http + if _task is not None: + _task.cancel() + _task = None + if _http is not None: + await _http.aclose() + _http = None + + +# ── Phrase cache ────────────────────────────────────────────────────────── + +async def _load_phrases() -> None: + """Load all active phrases from Postgres into the in-memory cache.""" + global _phrases + try: + pool = db.pool() + rows = await pool.fetch( + "SELECT phrase_type, snarkiness, mood, context_tag, phrase " + "FROM tas_phrases WHERE active ORDER BY id" + ) + cache: dict[str, dict[str, dict[str, dict[str, list[str]]]]] = {} + for r in rows: + pt = r["phrase_type"] + sk = r["snarkiness"] + md = r["mood"] + ct = r["context_tag"] + cache.setdefault(pt, {}).setdefault(sk, {}).setdefault(md, {}).setdefault(ct, []).append(r["phrase"]) + _phrases = cache + total = sum( + len(phrases) + for pt in cache.values() + for sk in pt.values() + for md in sk.values() + for phrases in md.values() + ) + log.info("tas loaded %d phrases", total) + except Exception: + log.exception("tas failed to load phrases — commentary disabled") + _phrases = {} + + +async def reload_phrases() -> None: + """Public helper to refresh the phrase cache without restart.""" + await _load_phrases() + + +# ── Weather polling ─────────────────────────────────────────────────────── + +async def _poll_loop() -> None: + # Fetch immediately on startup, then every interval. + while True: + try: + await _fetch_weather() + except asyncio.CancelledError: + return + except Exception: + log.exception("tas weather fetch error") + await asyncio.sleep(TAS_WEATHER_INTERVAL) + + +async def _fetch_weather() -> None: + global _mood + weather_code = None + aqi = None + + # Fetch weather and air quality in parallel. + weather_url = ( + f"https://api.open-meteo.com/v1/forecast" + f"?latitude={TAS_LAT}&longitude={TAS_LON}¤t=weather_code" + ) + aqi_url = ( + f"https://air-quality-api.open-meteo.com/v1/air-quality" + f"?latitude={TAS_LAT}&longitude={TAS_LON}¤t=european_aqi" + ) + + try: + resp = await _http.get(weather_url) + resp.raise_for_status() + data = resp.json() + weather_code = data.get("current", {}).get("weather_code") + except Exception: + log.warning("tas weather API failed — keeping previous mood") + + try: + resp = await _http.get(aqi_url) + resp.raise_for_status() + data = resp.json() + aqi = data.get("current", {}).get("european_aqi") + except Exception: + log.warning("tas air quality API failed — skipping AQI modifier") + + base = _weather_code_to_mood(weather_code) + new_mood = _apply_aqi(base, aqi) + + if new_mood != _mood: + log.info("tas mood changed %s → %s", _mood, new_mood) + _mood = new_mood + + +# ── Public API (sync, zero-await) ───────────────────────────────────────── + +def get_phrase( + phrase_type: str, + context_tag: str, + context_vars: dict[str, object], + player_id: str = "", +) -> str | None: + """Pick a phrase for the current mood and snarkiness. Returns None when + TAS is disabled or no phrases match the fallback chain. + + Tracks recently shown phrases per player to avoid repeats until the + candidate pool is exhausted. + """ + if not TAS_ENABLED or not _phrases: + return None + + snarkiness = TAS_SNARKINESS + mood = _mood + pt = _phrases.get(phrase_type) + if not pt: + return None + sk = pt.get(snarkiness) + if not sk: + return None + + # Build a merged candidate pool: mood-specific phrases plus neutral + # fallbacks, so sparse moods still have variety. + mood_tag = sk.get(mood, {}).get(context_tag, []) + mood_default = sk.get(mood, {}).get("default", []) if context_tag != "default" else [] + neutral_tag = sk.get("neutral", {}).get(context_tag, []) if mood != "neutral" else [] + neutral_default = sk.get("neutral", {}).get("default", []) if mood != "neutral" or context_tag != "default" else [] + candidates = mood_tag + mood_default + neutral_tag + neutral_default + if not candidates: + return None + + # Filter out recently shown phrases for this player. + recent = _recent.get(player_id, deque(maxlen=_RECENT_SIZE)) + fresh = [p for p in candidates if p not in recent] + if not fresh: + # All seen — reset and pick from full pool. + recent.clear() + fresh = candidates + + phrase = random.choice(fresh) + if player_id: + if player_id not in _recent: + _recent[player_id] = deque(maxlen=_RECENT_SIZE) + _recent[player_id].append(phrase) + + try: + return phrase.format(**{k: str(v) for k, v in context_vars.items()}) + except (KeyError, IndexError): + return phrase diff --git a/server/ws.py b/server/ws.py index c8442160..bd3f0df8 100644 --- a/server/ws.py +++ b/server/ws.py @@ -1,5 +1,6 @@ import asyncio import json +import random import time import uuid @@ -304,6 +305,28 @@ async def handle_roll(session: Session, msg: dict) -> None: locked_after=result["locked_after"], drand_round=drand_round) + # ── TAS commentary (sync, zero-await) ────────────────────────────── + # Skip commentary on round-ending rolls — the winner overlay speaks for itself. + from server import tas + commentary = None + if matched < 10 and random.random() < 0.4: + if p["roll_count"] == 1: + tas_tag = "first_roll" + elif len(result["newly_locked"]) >= 3: + tas_tag = "hot_streak" + elif len(result["newly_locked"]) == 0: + tas_tag = "whiff" + elif matched >= 8: + tas_tag = "close" + else: + tas_tag = "default" + commentary = tas.get_phrase("roll_heckle", tas_tag, { + "name": session.name, + "matched": matched, + "roll_count": p["roll_count"], + "target": target, + }, player_id=session.pid) + if matched == 10: # Atomic CAS: only the first finisher across all instances wins. if not await gamestore.try_finish_round(code): @@ -328,14 +351,20 @@ async def handle_roll(session: Session, msg: dict) -> None: final_dice=list(p["dice"])) snap = await gamestore.snapshot(code) if snap: - await send(session.ws, state_msg(snap, code, "round_won", winner_name=session.name)) + msg_out = state_msg(snap, code, "round_won", winner_name=session.name) + if commentary: + msg_out["commentary"] = commentary + await send(session.ws, msg_out) asyncio.create_task(delayed_broadcast(code, session.pid, True, winner_name=session.name)) else: log.info("roll game=%s player=%s matched=%d/10 target=%d", code, session.name, matched, target) snap = await gamestore.snapshot(code) if snap: - await send(session.ws, state_msg(snap, code)) + msg_out = state_msg(snap, code) + if commentary: + msg_out["commentary"] = commentary + await send(session.ws, msg_out) asyncio.create_task(delayed_broadcast(code, session.pid, False, None)) diff --git a/static/css/game.css b/static/css/game.css index 6fe1ad95..3aa9006c 100644 --- a/static/css/game.css +++ b/static/css/game.css @@ -129,9 +129,42 @@ .zone-matched .die-scene:nth-child(3n+1) { transform: rotate(-2.5deg); } .zone-matched .die-scene:nth-child(3n+2) { transform: rotate(1.5deg); } + /* TAS commentary (above roll button dome) */ + .tas-commentary { + position: absolute; + z-index: 5; + left: 50%; + bottom: 7rem; + translate: -50% 0; + inline-size: 90%; + padding: 0.3rem 0.7rem; + font-size: 0.82rem; + line-height: 1.5; + font-weight: 600; + color: var(--color-text-warm); + text-shadow: var(--shadow-text); + text-align: center; + background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.6) 30%, transparent 65%); + padding: 3rem 2rem; + opacity: 0; + pointer-events: none; + transition: opacity 0.5s ease; + } + + .tas-commentary.visible { + opacity: 1; + transition: opacity 0.25s ease; + } + + .tas-commentary.fading { + opacity: 0; + transition: opacity 0.5s ease; + } + /* Roll button */ .roll-area { position: relative; + z-index: 10; flex-shrink: 0; margin-inline: -1rem; padding-block-start: 0; diff --git a/static/js/animations.js b/static/js/animations.js index 6c1f519c..a2b9735f 100644 --- a/static/js/animations.js +++ b/static/js/animations.js @@ -2,6 +2,7 @@ import { FACE_ROTATIONS, makeDie, myDiceKey, placeGrid } from './dice.js'; import { saveDicePositions } from './dice-positions.js'; import { renderMyArea, renderPlayersBar } from './game-render.js'; +import { showCommentary } from './commentary.js'; import { hideWinner, showWinner } from './overlays.js'; import { showFor } from './router.js'; import { state } from './state.js'; @@ -198,6 +199,7 @@ export function tryReveal() { state.rolling = false; const btn = /** @type {HTMLButtonElement | null} */ (document.getElementById('roll-btn')); if (btn) btn.disabled = false; + if (snap.commentary) showCommentary(snap.commentary); if (state.ws && state.ws.readyState === WebSocket.OPEN) { state.ws.send(JSON.stringify({ action: 'roll_done' })); } diff --git a/static/js/commentary.js b/static/js/commentary.js new file mode 100644 index 00000000..9bc8a1ab --- /dev/null +++ b/static/js/commentary.js @@ -0,0 +1,44 @@ +// @ts-check + +/** + * TAS commentary display — shows/hides text above the roll button. + * @module + */ + +/** @type {ReturnType | null} */ +let hideTimer = null; + +/** + * Show TAS commentary above the roll button, then fade it out. + * @param {string} text + */ +export function showCommentary(text) { + const el = document.getElementById('tas-commentary'); + if (!el) return; + + if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } + + el.textContent = text; + el.classList.remove('fading'); + // Force reflow so removing 'fading' takes effect before adding 'visible'. + void el.offsetWidth; + el.classList.add('visible'); + + hideTimer = setTimeout(() => { + el.classList.add('fading'); + hideTimer = setTimeout(() => { + el.classList.remove('visible', 'fading'); + hideTimer = null; + }, 500); + }, 3000); +} + +/** Clear any visible commentary immediately (e.g. on re-render). */ +export function hideCommentary() { + const el = document.getElementById('tas-commentary'); + if (el) { + el.classList.remove('visible', 'fading'); + el.textContent = ''; + } + if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } +} diff --git a/static/js/game-render.js b/static/js/game-render.js index 467c0211..535c2b4a 100644 --- a/static/js/game-render.js +++ b/static/js/game-render.js @@ -154,6 +154,16 @@ export function renderMyArea(snap) { zones.appendChild(matchedZone); area.appendChild(zones); + // TAS commentary lives on the game-screen (not my-area) so overflow:hidden + // does not clip it. Re-create only if missing (game-screen is not rebuilt). + const screen = area.closest('.game-screen'); + if (screen && !screen.querySelector('#tas-commentary')) { + const commentaryEl = document.createElement('div'); + commentaryEl.id = 'tas-commentary'; + commentaryEl.className = 'tas-commentary'; + screen.appendChild(commentaryEl); + } + const rollArea = document.createElement('div'); rollArea.className = 'roll-area'; const btn = document.createElement('button'); diff --git a/static/js/roll.js b/static/js/roll.js index 326526a3..c079dea6 100644 --- a/static/js/roll.js +++ b/static/js/roll.js @@ -1,5 +1,6 @@ // @ts-check import { startShake, tryReveal } from './animations.js'; +import { hideCommentary } from './commentary.js'; import { state } from './state.js'; /** @@ -15,6 +16,7 @@ export function roll() { const winner = /** @type {HTMLDialogElement | null} */ (document.getElementById('winner-overlay')); if (winner?.open) return; + hideCommentary(); state.rolling = true; state.awaitingAck = true; state.pendingRollState = null; diff --git a/static/js/types.js b/static/js/types.js index 60a7ff16..22a0fb49 100644 --- a/static/js/types.js +++ b/static/js/types.js @@ -30,6 +30,7 @@ * @property {Record} players * @property {string} [winner_name] Present on `round_won` frames. * @property {number} [pause_remaining_ms] Present on paused frames sent to the host. + * @property {string} [commentary] TAS commentary line for the roller's own roll. */ /** From 5de013efd9cb62a5e1f1f7d87d0788cf40d81a96 Mon Sep 17 00:00:00 2001 From: Michael Simmons Date: Sat, 27 Jun 2026 11:22:42 -0500 Subject: [PATCH 2/2] TAS polish: commentary positioning, styling, and architecture docs - Move commentary to game-screen (out of roll-area) so it doesn't affect dice zone layout - Account for safe-area-inset-bottom for PWA - White text, larger font, darker radial glow - Clear commentary on roll start - Add docs/TENSIES_ATTITUDE_SYSTEM.md with Mermaid architecture diagram Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/TENSIES_ATTITUDE_SYSTEM.md | 324 ++++++++++++++++++++++++++++++++ static/css/game.css | 6 +- 2 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 docs/TENSIES_ATTITUDE_SYSTEM.md diff --git a/docs/TENSIES_ATTITUDE_SYSTEM.md b/docs/TENSIES_ATTITUDE_SYSTEM.md new file mode 100644 index 00000000..ad9966c8 --- /dev/null +++ b/docs/TENSIES_ATTITUDE_SYSTEM.md @@ -0,0 +1,324 @@ +# Tensies Attitude System (TAS) + +TAS is the game's personality. It watches you roll and has opinions about it. Heckling, cheering, passive-aggressively sighing, depending on its mood and how much attitude you've dialed in. The mood comes from the weather and air quality wherever the server lives. It never tells you why it's grumpy. It just is. + +TAS is on by default but stays passive without location config. Mood sits at neutral, phrases still fire from the neutral pool. Set a lat/lon and it wakes up. + +--- + +## Architecture + +```mermaid +flowchart TD + subgraph OpenMeteo["Open-Meteo (free, no key)"] + W["/forecast\nweather_code"] + AQ["/air-quality\neuropean_aqi"] + end + + subgraph TAS["server/tas.py"] + FW["_fetch_weather()\nweather_code → mood\nAQI → mood modifier"] + LP["_load_phrases()\nPostgres → memory cache"] + GP["get_phrase()\nmood × snarkiness × context_tag\nrecently-seen filter\n~40% fire rate"] + end + + subgraph Roll["handle_roll (ws.py)"] + PF["private WS frame\n(not broadcast)"] + end + + W -->|every 30 min| FW + AQ -->|every 30 min| FW + DB[(tas_phrases\nPostgres)] -->|on startup| LP + FW --> GP + LP --> GP + GP -->|per roll| PF + PF -->|commentary field| Client["roller sees text\nabove roll button"] + + style OpenMeteo fill:#1a1a2e,stroke:#e94560,color:#eee + style TAS fill:#1a1a2e,stroke:#f5a623,color:#eee + style Roll fill:#1a1a2e,stroke:#0f3460,color:#eee + style GP fill:#0f3460,stroke:#f5a623,color:#eee +``` + +### How it plugs in + +Same lifecycle pattern as drand and Discord: `start()` in the lifespan, `stop()` on shutdown, background poll task, sync public API on the hot path. + +The roll handler calls `tas.get_phrase()` after `apply_roll()`. It's sync and does zero I/O, just a dict lookup and a `random.choice()`. The result rides on the private state frame sent to the roller before `delayed_broadcast`. Other players never see your commentary. + +### Database tables + +Two tables, one migration each: + +| Table | Purpose | Read at runtime? | +|---|---|---| +| `tas_phrases` | The actual lines TAS delivers | Yes, loaded into memory on startup | +| `tas_personas` | Creative briefs describing each mood × snarkiness voice | No, reference material for generating phrases | + +`tas_phrases` has a generic `phrase_type` column. The only type right now is `roll_heckle`. Future types (lobby taunts, win celebrations, idle nudges) just add rows with a new type. The code doesn't need to change. + +### Phrase cache + +Phrases load from Postgres into a nested dict on startup: + +``` +_phrases[phrase_type][snarkiness][mood][context_tag] → list[str] +``` + +The cache is rebuilt with `reload_phrases()` if you need a hot update. Otherwise it's set-and-forget. + +--- + +## The mood system + +Mood has two layers. Weather sets the base, air quality can push it darker. + +### Layer 1: weather → base mood + +The server polls Open-Meteo every 30 minutes (configurable). WMO weather codes map to moods: + +| Weather | Code range | Mood | Vibe | +|---|---|---|---| +| Clear sky | 0-1 | `sunny` | Upbeat, warm, high-fives | +| Cloudy | 2-3 | `bored` | Flat affect, checked out | +| Fog | 45-48 | `mysterious` | Cryptic, ominous, fortune-teller energy | +| Light drizzle | 51-55 | `melancholic` | Wistful, poetic, dramatic sighs | +| Rain / freezing | 56-67 | `grumpy` | Short fuse, everything is annoying | +| Snow | 71-77 | `cozy` | Warm, snuggly, hot cocoa vibes | +| Rain showers | 80-82 | `chaotic` | Scattered, wild, unpredictable | +| Snow showers | 85-86 | `cozy` | Same as snow | +| Thunderstorm | 95-99 | `unhinged` | Zero filter, off the rails | +| Anything else | - | `neutral` | Baseline, no strong emotion | + +### Layer 2: air quality modifier + +The European AQI from the same Open-Meteo poll can shift the mood darker: + +| AQI range | Effect | +|---|---| +| 0-40 (good/fair) | No change | +| 41-60 (moderate) | Edge shift (see below) | +| 61-80 (poor) | Override to `irritable` | +| 81+ (very poor) | Override to `suffocating` | + +The edge-shift table for moderate air: + +| Base mood | Shifts to | +|---|---| +| `sunny` | `restless` | +| `bored` | `irritable` | +| `melancholic` | `bitter` | +| `grumpy` | `irritable` | +| `cozy` | `stir_crazy` | +| `chaotic` | `manic` | +| `unhinged` | `manic` | +| `mysterious` | `paranoid` | +| `neutral` | `irritable` | + +### Full mood list + +There are 15 moods. Weather drives the common ones; the air-quality moods show up when conditions get rough. + +| Mood | Source | Personality | +|---|---|---| +| `sunny` | weather | Bright, warm, genuinely cheerful | +| `bored` | weather | Going through the motions | +| `mysterious` | weather | Cryptic, reads tea leaves | +| `melancholic` | weather | Wistful, dramatic, poetic | +| `grumpy` | weather | Short fuse, complaining | +| `cozy` | weather | Snug, encouraging, blanket energy | +| `chaotic` | weather | Scattered, wild, unpredictable | +| `unhinged` | weather | Full send, no filter | +| `restless` | air shift | Jittery, antsy, cannot sit still | +| `bitter` | air shift | Resentful, backhanded everything | +| `stir_crazy` | air shift | Trapped energy, bouncing off walls | +| `manic` | air shift | Frenetic, too much, will not stop | +| `paranoid` | air shift | Suspicious, reading into everything | +| `irritable` | air override | Snappy, everything is a slight | +| `suffocating` | air override | Dramatically exhausted, gasping | +| `neutral` | fallback | Dry, matter-of-fact | + +The mood never reveals its source. A `suffocating` TAS does not mention air quality. It just acts exhausted. A `cozy` TAS does not mention snow. It is just warm and encouraging. + +--- + +## Snarkiness levels + +Snarkiness is a global server setting, one knob for the whole instance. It controls how the mood is expressed, not what the mood is. + +| Level | Tone | +|---|---| +| `friendly_drunk` | Warm, goofy, encouraging-ish. Slurs slightly. Your biggest fan with a flask. | +| `loud_obnoxious` | ALL CAPS energy. Sports-announcer at a house party. Cannot contain themselves. | +| `angry_mean` | Cutting, dismissive. Damns with faint acknowledgment. Clinical disdain. | +| `insufferable` | Condescending, passive-aggressive. Treats you like a science experiment. | + +Default is `friendly_drunk`. + +--- + +## Context tags + +When TAS fires on a roll, it picks a context tag based on the outcome: + +| Tag | Condition | What it means | +|---|---|---| +| `first_roll` | `roll_count == 1` | Opening roll of the round | +| `hot_streak` | `newly_locked >= 3` | Locked three or more dice in one roll | +| `whiff` | `newly_locked == 0`, `roll_count > 1` | Rolled and matched nothing (not first roll) | +| `close` | `matched >= 8` | Eight or more dice locked, almost done | +| `win` | `matched == 10` | All dice locked, but TAS skips this (the winner overlay handles it) | +| `default` | Everything else | A regular mid-game roll | + +### Phrase selection + +TAS builds a merged candidate pool from mood-specific and neutral phrases, so even moods with sparse coverage have variety: + +1. Exact mood + exact context tag +2. Exact mood + `default` tag +3. `neutral` mood + exact context tag +4. `neutral` mood + `default` tag + +All four pools are concatenated. A `random.choice()` picks from the combined list. Per-player recently-seen tracking (last 20 phrases) filters out repeats until the pool runs out, then resets. + +Commentary fires on roughly 40% of non-winning rolls. The rest are silent. + +--- + +## Personas + +The `tas_personas` table stores a 3-5 sentence creative brief for each mood × snarkiness combination (60 total). These are not read at runtime. They exist so anyone generating or iterating on phrases has enough context to stay in voice. + +Example persona (`grumpy` × `insufferable`): + +> Passive-aggressive disappointment incarnate. Speaks in complete, grammatically perfect sentences that feel like a slap. Feigns concern. Uses "Oh" and "Well" as weapons. Compliments that are insults. Never raises voice. The quiet is the point. + +To read all personas: + +```sql +SELECT mood, snarkiness, persona FROM tas_personas ORDER BY mood, snarkiness; +``` + +--- + +## Variable placeholders + +Phrases support these placeholders, filled at render time: + +| Placeholder | Value | +|---|---| +| `{name}` | Player name | +| `{matched}` | Number of matched dice (0-10) | +| `{roll_count}` | How many rolls this round so far | +| `{target}` | The target number this round (1-6) | + +Unknown keys are left as-is rather than crashing, so future phrase types can define their own placeholders without touching the formatter. + +--- + +## Configuration + +| Env var | Default | What it does | +|---|---|---| +| `TAS_ENABLED` | `1` | Master switch. `0` disables everything. | +| `TAS_LAT` | _(unset)_ | Server latitude for weather. Without this, mood stays `neutral`. | +| `TAS_LON` | _(unset)_ | Server longitude for weather. | +| `TAS_SNARKINESS` | `friendly_drunk` | One of: `friendly_drunk`, `loud_obnoxious`, `angry_mean`, `insufferable`. | +| `TAS_WEATHER_INTERVAL` | `1800` | Seconds between weather polls (default 30 min). | + +All are forwarded through `docker-compose.yml`. Example `.env`: + +```bash +TAS_LAT=33.20 +TAS_LON=-96.63 +TAS_SNARKINESS=angry_mean +``` + +### Verifying it works + +Check the startup logs: + +``` +tas loaded 731 phrases +tas started lat=33.20 lon=-96.63 snarkiness=angry_mean interval=1800.0s +tas mood changed neutral → bored +``` + +The `mood changed` line fires on the first successful weather fetch and whenever the mood shifts after that. + +--- + +## Client display + +Commentary floats above the roll button with a dark radial glow behind it. Fades in over 0.25s, holds for 3 seconds, fades out over 0.5s. Clicking Roll clears any visible commentary immediately. + +The element is absolutely positioned on `.game-screen` so it does not affect the dice zone layout. The roll button area sits above it in z-index, so the glow bleeds under the dome, not over it. + +--- + +## Code layout + +| File | Role | +|---|---| +| `server/tas.py` | The whole backend: weather poller, phrase cache, mood mapping, `get_phrase()` | +| `server/config.py` | TAS config constants (`TAS_ENABLED`, `TAS_LAT`, etc.) | +| `main.py` | Lifespan wiring: `tas.start()` / `tas.stop()` | +| `server/ws.py` | Integration in `handle_roll`: context tag derivation, 40% gate, `get_phrase()` call | +| `migrations/008_tas.sql` | Schema: `tas_personas` + `tas_phrases` tables | +| `migrations/009_tas_seed_personas.sql` | 60 persona creative briefs | +| `migrations/010_tas_seed_roll_heckle.sql` | Initial roll_heckle phrases | +| `migrations/011_tas_more_roll_heckle.sql` | Expanded roll_heckle phrases | +| `static/js/commentary.js` | `showCommentary()` / `hideCommentary()` | +| `static/js/animations.js` | Calls `showCommentary` after dice reveal | +| `static/js/roll.js` | Calls `hideCommentary` on roll start | +| `static/js/game-render.js` | Creates the commentary DOM element on the game screen | +| `static/css/game.css` | `.tas-commentary` styles: positioning, glow, fade transitions | + +--- + +## Adding phrases + +Add rows to `tas_phrases` via a migration or direct insert: + +```sql +INSERT INTO tas_phrases (phrase_type, mood, snarkiness, context_tag, phrase) +VALUES ('roll_heckle', 'grumpy', 'angry_mean', 'whiff', 'That was embarrassing, {name}.'); +``` + +Restart the server (or call `tas.reload_phrases()` from a shell) to pick them up. + +To check coverage: + +```sql +SELECT mood, snarkiness, context_tag, count(*) +FROM tas_phrases +WHERE phrase_type = 'roll_heckle' AND active +GROUP BY 1, 2, 3 +ORDER BY 1, 2, 3; +``` + +### Adding a new phrase type + +Future TAS features (lobby taunts, idle nudges, whatever) need three things: + +1. Insert rows with a new `phrase_type` value +2. Call `tas.get_phrase("your_type", context_tag, vars)` from the relevant handler +3. Display the result however makes sense on the client + +The schema and cache handle the rest. + +--- + +## Extending the mood system + +To add a new mood: + +1. Add a mapping in `_WEATHER_MOOD` or `_AQI_EDGE_SHIFT` in `server/tas.py` +2. Add a persona row for each snarkiness level in `tas_personas` +3. Add phrases for the new mood in `tas_phrases` +4. The fallback chain means you can start sparse, neutral phrases cover gaps + +To add a new snarkiness level: + +1. Add persona rows for every mood × the new level +2. Add phrases +3. Set `TAS_SNARKINESS` to the new value diff --git a/static/css/game.css b/static/css/game.css index 3aa9006c..d637a96f 100644 --- a/static/css/game.css +++ b/static/css/game.css @@ -134,14 +134,14 @@ position: absolute; z-index: 5; left: 50%; - bottom: 7rem; + bottom: calc(7rem + env(safe-area-inset-bottom)); translate: -50% 0; inline-size: 90%; padding: 0.3rem 0.7rem; - font-size: 0.82rem; + font-size: 0.9rem; line-height: 1.5; font-weight: 600; - color: var(--color-text-warm); + color: #fff; text-shadow: var(--shadow-text); text-align: center; background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0.6) 30%, transparent 65%);