From 44bbb888367ff05f0c71cb653f359244993c2ecb Mon Sep 17 00:00:00 2001 From: Tim McCarthy Date: Sun, 14 Jun 2026 09:46:02 -0700 Subject: [PATCH] Add gat-description modal when a Pi-Rat earns a Gat Mirrors the name modal: earning the Gat objective sets Player.needs_gat_description, and GatModal.svelte (rendered by Dashboard across all phases) prompts for a one-of-a-kind Gat description, stored in Player.gat_description and shown on the character sheet. Like a stolen Name, the Gat's description travels with it through Gat Tax transfers. - Player.gat_description / needs_gat_description (+ migration) - toggle_objective personal_1 sets/clears the prompt - crud_challenge gat-tax paths move the description with the Gat - set-gat-description route; CharacterSheet displays it Co-Authored-By: Claude Opus 4.8 --- TODO.md | 2 +- frontend/src/components/GatModal.svelte | 88 +++++++++++++++++++ .../components/scene/CharacterSheet.svelte | 3 + frontend/src/pages/Dashboard.svelte | 2 + src/pirats/crud_challenge.py | 6 ++ src/pirats/crud_scene.py | 3 + ...132c8e583_add_gat_description_to_player.py | 34 +++++++ src/pirats/models.py | 2 + src/pirats/routes_scene.py | 16 ++++ 9 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/GatModal.svelte create mode 100644 src/pirats/migrations/versions/153132c8e583_add_gat_description_to_player.py diff --git a/TODO.md b/TODO.md index c3c3cdf..842418f 100644 --- a/TODO.md +++ b/TODO.md @@ -20,7 +20,7 @@ - [x] Add a toast pop-up for new events entering the event log, that then fades into the event log button -- [ ] When you get a gat, there should be a pop up to enter a description of it, as with your name +- [x] When you get a gat, there should be a pop up to enter a description of it, as with your name - [ ] In between scenes, there really only needs to be one panel with the roster and voting status. There doesn't need to be a whole extra panel to inform players that the deep will later get to refresh their hands. diff --git a/frontend/src/components/GatModal.svelte b/frontend/src/components/GatModal.svelte new file mode 100644 index 0000000..05b7c1a --- /dev/null +++ b/frontend/src/components/GatModal.svelte @@ -0,0 +1,88 @@ + + +{#if state.player.needs_gat_description} + +{/if} + + diff --git a/frontend/src/components/scene/CharacterSheet.svelte b/frontend/src/components/scene/CharacterSheet.svelte index 2020b63..1c01306 100644 --- a/frontend/src/components/scene/CharacterSheet.svelte +++ b/frontend/src/components/scene/CharacterSheet.svelte @@ -80,6 +80,9 @@

Look: {state.player.avatar_look}

Smell: {state.player.avatar_smell}

First Words: "{state.player.first_words}"

+ {#if state.player.gat_description} +

🔫 Gat: {state.player.gat_description}

+ {/if} {#if state.player.other_like || state.player.other_hate} diff --git a/frontend/src/pages/Dashboard.svelte b/frontend/src/pages/Dashboard.svelte index b879e80..09a4b64 100644 --- a/frontend/src/pages/Dashboard.svelte +++ b/frontend/src/pages/Dashboard.svelte @@ -17,6 +17,7 @@ import GameOverPhase from '../components/GameOverPhase.svelte'; import EventLog from '../components/EventLog.svelte'; import NameModal from '../components/NameModal.svelte'; + import GatModal from '../components/GatModal.svelte'; export let params = {}; let gameId = params.id; @@ -212,6 +213,7 @@ + {:else if !error}

Loading game state...

diff --git a/src/pirats/crud_challenge.py b/src/pirats/crud_challenge.py index 5ac45d3..9ae52c5 100644 --- a/src/pirats/crud_challenge.py +++ b/src/pirats/crud_challenge.py @@ -213,6 +213,9 @@ def resolve_challenge(db: Session, challenge_id: str, resolver_id: str) -> Tuple if challenge.tax_type == "gat": acting.completed_personal_1 = False refuser.completed_personal_1 = True + # The Gat (and its description) returns to its owner. + refuser.gat_description = acting.gat_description + acting.gat_description = "" else: # Return the stolen name string; the failed thief reverts to # their smell-based recruit identity. @@ -344,6 +347,9 @@ def respond_tax(db: Session, challenge_id: str, responder_id: str, accept: bool) if challenge.tax_type == "gat": requester.completed_personal_1 = True responder.completed_personal_1 = False + # The Gat changes hands along with its description (like a stolen Name). + requester.gat_description = responder.gat_description + responder.gat_description = "" event_msg = f"{responder.name} refused the Gat Tax and must hand over their Gat! {requester.name} completes that Objective and attempts the Challenge — succeed to keep it!" else: # The Name itself is stolen: the requester literally takes the responder's diff --git a/src/pirats/crud_scene.py b/src/pirats/crud_scene.py index 99a6100..79f0d90 100644 --- a/src/pirats/crud_scene.py +++ b/src/pirats/crud_scene.py @@ -335,8 +335,11 @@ def toggle_objective(db: Session, game_id: str, target_id: str, obj_type: str, s if obj_type == "personal_1": if status and not player.completed_personal_1: rank_diff = 1 + player.needs_gat_description = True elif not status and player.completed_personal_1: rank_diff = -1 + player.needs_gat_description = False + player.gat_description = "" player.completed_personal_1 = status elif obj_type == "personal_2": diff --git a/src/pirats/migrations/versions/153132c8e583_add_gat_description_to_player.py b/src/pirats/migrations/versions/153132c8e583_add_gat_description_to_player.py new file mode 100644 index 0000000..46b0422 --- /dev/null +++ b/src/pirats/migrations/versions/153132c8e583_add_gat_description_to_player.py @@ -0,0 +1,34 @@ +"""add gat description to player + +Revision ID: 153132c8e583 +Revises: 5a6e73d2bc4f +Create Date: 2026-06-14 09:42:58.953909 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +revision = '153132c8e583' +down_revision = '5a6e73d2bc4f' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('player', schema=None) as batch_op: + batch_op.add_column(sa.Column('gat_description', sqlmodel.sql.sqltypes.AutoString(), nullable=False)) + batch_op.add_column(sa.Column('needs_gat_description', sa.Boolean(), nullable=False)) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('player', schema=None) as batch_op: + batch_op.drop_column('needs_gat_description') + batch_op.drop_column('gat_description') + + # ### end Alembic commands ### diff --git a/src/pirats/models.py b/src/pirats/models.py index 845e88c..41dc4a9 100644 --- a/src/pirats/models.py +++ b/src/pirats/models.py @@ -68,8 +68,10 @@ class Player(SQLModel, table=True): completed_personal_2: bool = Field(default=False) # Earn a Name completed_personal_3: bool = Field(default=False) # Die like a Pirate + gat_description: str = Field(default="") # Free-text description of the Pi-Rat's Gat, entered via a modal when they earn one # States for Milestones & Death needs_name: bool = Field(default=False) + needs_gat_description: bool = Field(default=False) # Prompts the gat-description modal after earning a Gat objective needs_rank_3_bonus: bool = Field(default=False) is_dead: bool = Field(default=False) is_ghost: bool = Field(default=False) diff --git a/src/pirats/routes_scene.py b/src/pirats/routes_scene.py index ce0e7c5..25c613e 100644 --- a/src/pirats/routes_scene.py +++ b/src/pirats/routes_scene.py @@ -130,6 +130,22 @@ def set_name_route( db.commit() return {"status": "ok"} +# Pi-Rat describes the Gat they just earned +@router.post("/game/{game_id}/player/{player_id}/set-gat-description") +def set_gat_description_route( + game_id: str, + player_id: str, + description: str = Form(...), + db: Session = Depends(get_session) +): + player = crud.get_player(db, player_id) + if player and player.needs_gat_description: + player.gat_description = description + player.needs_gat_description = False + db.add(player) + db.commit() + return {"status": "ok"} + # Pi-Rat bonus rank up for another player @router.post("/game/{game_id}/player/{player_id}/bonus-rank-up") def bonus_rank_up_route(