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 <noreply@anthropic.com>
This commit is contained in:
2
TODO.md
2
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.
|
||||
|
||||
|
||||
88
frontend/src/components/GatModal.svelte
Normal file
88
frontend/src/components/GatModal.svelte
Normal file
@@ -0,0 +1,88 @@
|
||||
<script>
|
||||
import { apiRequest } from '../lib/api';
|
||||
|
||||
export let state;
|
||||
|
||||
let descInput = '';
|
||||
let error = '';
|
||||
let submitting = false;
|
||||
|
||||
async function submitDescription() {
|
||||
if (!descInput.trim() || submitting) return;
|
||||
error = '';
|
||||
submitting = true;
|
||||
try {
|
||||
await apiRequest(`/game/${state.game.id}/player/${state.player.id}/set-gat-description`, 'POST', {
|
||||
description: descInput.trim()
|
||||
});
|
||||
descInput = '';
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onKeydown(e) {
|
||||
if (e.key === 'Enter') submitDescription();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if state.player.needs_gat_description}
|
||||
<div class="modal-backdrop">
|
||||
<div class="modal-box glass-panel">
|
||||
<h3>🔫 You Got a Gat!</h3>
|
||||
<p class="info-text">
|
||||
Every Pi-Rat's Gat is one of a kind. What does yours look like?
|
||||
</p>
|
||||
{#if error}
|
||||
<div class="alert alert-danger">{error}</div>
|
||||
{/if}
|
||||
<input
|
||||
type="text"
|
||||
bind:value={descInput}
|
||||
class="input-field"
|
||||
placeholder="e.g. A pearl-handled flintlock that smells of cheese"
|
||||
on:keydown={onKeydown}
|
||||
autofocus
|
||||
>
|
||||
<button
|
||||
class="btn btn-gold btn-full"
|
||||
on:click={submitDescription}
|
||||
disabled={!descInput.trim() || submitting}
|
||||
>
|
||||
Claim Gat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: color-mix(in srgb, var(--bg-deep) 70%, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
.modal-box {
|
||||
max-width: 440px;
|
||||
width: 100%;
|
||||
padding: 1.75rem;
|
||||
border: 1px solid var(--edge);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-deep);
|
||||
text-align: center;
|
||||
}
|
||||
.modal-box h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.modal-box .input-field {
|
||||
width: 100%;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
</style>
|
||||
@@ -80,6 +80,9 @@
|
||||
<p><strong>Look:</strong> {state.player.avatar_look}</p>
|
||||
<p><strong>Smell:</strong> {state.player.avatar_smell}</p>
|
||||
<p><strong>First Words:</strong> "{state.player.first_words}"</p>
|
||||
{#if state.player.gat_description}
|
||||
<p><strong>🔫 Gat:</strong> {state.player.gat_description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if state.player.other_like || state.player.other_hate}
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
<EventLog {state} />
|
||||
<NameModal {state} />
|
||||
<GatModal {state} />
|
||||
{:else if !error}
|
||||
<div class="loading-container">
|
||||
<p>Loading game state...</p>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 ###
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user