Dev mode revamp

This commit is contained in:
2026-06-12 11:50:34 -07:00
parent f61201144f
commit 3395b127b4
13 changed files with 347 additions and 132 deletions

View File

@@ -428,26 +428,3 @@
gap: 0.75rem;
}
/* --- Dev mode --- */
.dev-toggle {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--text-muted);
margin-top: 0.5rem;
}
.dev-banner {
background: var(--well);
border: 1px solid var(--accent);
border-radius: var(--radius-md);
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin: 0 auto 1.5rem;
max-width: 56rem;
}

View File

@@ -1,7 +1,7 @@
<script>
import { onMount } from 'svelte';
import { apiRequest } from '../lib/api';
import { getSuggestion, getTechniqueSuggestion, getTechniqueSuggestions } from '../lib/suggestions';
import { getSuggestion, getTechniqueSuggestion } from '../lib/suggestions';
import { displayName } from '../lib/cards';
import FaceCardAssigner from './FaceCardAssigner.svelte';
@@ -110,61 +110,9 @@
$: delegateComplete = !!(state.player.other_like_from_player_id && state.player.other_hate_from_player_id);
$: techniquesComplete = !!(state.player.tech_jack && state.player.tech_queen && state.player.tech_king);
// Dev Mode
let devMode = false;
let autoFilling = false;
async function autoFillAll() {
autoFilling = true;
try {
// 1. Basic details — drawn from the same pools as the Suggest buttons
if (!basicComplete) {
basicDetails = {
avatar_look: getSuggestion('look'),
avatar_smell: getSuggestion('smell'),
first_words: getSuggestion('first_words'),
good_at_math: getSuggestion('good_at_math')
};
await saveBasicDetails();
}
// 2. Answer pending inbox questions with pool suggestions
for (const p of state.players) {
if (p.other_like_from_player_id === state.player.id && !p.other_like) {
inboxAnswers[`${p.id}:like`] = getSuggestion('like');
await submitDelegatedAnswer('like', p.id);
}
if (p.other_hate_from_player_id === state.player.id && !p.other_hate) {
inboxAnswers[`${p.id}:hate`] = getSuggestion('hate');
await submitDelegatedAnswer('hate', p.id);
}
}
// 3. Techniques — 3 distinct pool picks; retry if another player
// grabbed the same name first (the backend rejects collisions)
if (createdTechniques.length === 0) {
for (let attempt = 0; attempt < 5; attempt++) {
[tech1, tech2, tech3] = await getTechniqueSuggestions(3);
await submitTechniques();
if (!techError) break;
}
}
// 4. Face Techniques - we need the swapped ones. We can't do this synchronously without waiting for the server
// to process the state, because swapped_techniques comes from the server once everyone is done.
if (!techniquesComplete && swappedTechniques.length === 3) {
jackTech = swappedTechniques[0];
queenTech = swappedTechniques[1];
kingTech = swappedTechniques[2];
await assignFaceTechniques();
}
} catch (e) {
console.error(e);
} finally {
autoFilling = false;
}
}
// Dev Mode (toggled by the creator from the corner menu) reveals the
// Suggest buttons; in real games players write their own material.
$: devMode = !!state.game.dev_mode;
</script>
@@ -172,24 +120,8 @@
<div class="view-header text-center">
<h2>Character Sheet Creation</h2>
<p>Fill in your Rat Records, delegate questions to your crew, and prepare your Math Magic Techniques.</p>
<div class="dev-toggle">
<input type="checkbox" id="devModeToggle" bind:checked={devMode}/>
<label for="devModeToggle">Dev Mode</label>
</div>
</div>
{#if devMode}
<div class="dev-banner">
<span class="text-accent font-bold">🛠 Dev Mode Tools</span>
<button
class="btn btn-primary"
on:click={autoFillAll}
disabled={autoFilling}>
{autoFilling ? 'Auto-Filling...' : 'Auto-Fill Available Fields'}
</button>
</div>
{/if}
<!-- MAIN PANEL: Grid for Layout -->
<div class="creation-grid max-w-5xl mx-auto">
@@ -212,28 +144,28 @@
<label for="avatar_look">What does your Pi-Rat look like?</label>
<div class="input-row">
<input type="text" id="avatar_look" placeholder="e.g. A blue bandana, scarred snout" bind:value={basicDetails.avatar_look} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>{/if}
</div>
</div>
<div class="form-group">
<label for="avatar_smell">What does your Pi-Rat smell like?</label>
<div class="input-row">
<input type="text" id="avatar_smell" placeholder="e.g. Damp gunpowder, salty cheese" bind:value={basicDetails.avatar_smell} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>{/if}
</div>
</div>
<div class="form-group">
<label for="first_words">What were your first words after transforming?</label>
<div class="input-row">
<input type="text" id="first_words" placeholder="e.g. Where is my gat?!" bind:value={basicDetails.first_words} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>{/if}
</div>
</div>
<div class="form-group">
<label for="good_at_math">Is your Pi-Rat good at Math?</label>
<div class="input-row">
<input type="text" id="good_at_math" placeholder="e.g. Yes, but only geometry; No, thinks Pi is a dessert" bind:value={basicDetails.good_at_math} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>{/if}
</div>
</div>
<button type="submit" class="btn btn-primary btn-full" disabled={savingBasic}>Save Records</button>
@@ -279,7 +211,7 @@
<label>What do you LIKE about {displayName(p)}?</label>
<div class="input-row">
<input type="text" class="input-field" bind:value={inboxAnswers[`${p.id}:like`]} placeholder="e.g. They always share their cheese"/>
<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:like`] = getSuggestion('like')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:like`] = getSuggestion('like')}>Suggest</button>{/if}
<button class="btn btn-primary" disabled={!(inboxAnswers[`${p.id}:like`] || '').trim()} on:click={() => submitDelegatedAnswer('like', p.id)}>Submit</button>
</div>
</div>
@@ -289,7 +221,7 @@
<label>What do you HATE about {displayName(p)}?</label>
<div class="input-row">
<input type="text" class="input-field" bind:value={inboxAnswers[`${p.id}:hate`]} placeholder="e.g. They snore too loudly"/>
<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:hate`] = getSuggestion('hate')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:hate`] = getSuggestion('hate')}>Suggest</button>{/if}
<button class="btn btn-primary" disabled={!(inboxAnswers[`${p.id}:hate`] || '').trim()} on:click={() => submitDelegatedAnswer('hate', p.id)}>Submit</button>
</div>
</div>
@@ -315,21 +247,21 @@
<label for="tech1">Technique #1:</label>
<div class="input-row">
<input type="text" id="tech1" placeholder="e.g. Carlo's cheese shield" bind:value={tech1} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={async () => tech1 = await getTechniqueSuggestion([tech2, tech3])}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={async () => tech1 = await getTechniqueSuggestion([tech2, tech3])}>Suggest</button>{/if}
</div>
</div>
<div class="form-group">
<label for="tech2">Technique #2:</label>
<div class="input-row">
<input type="text" id="tech2" placeholder="e.g. Parabolic boarding bounce" bind:value={tech2} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={async () => tech2 = await getTechniqueSuggestion([tech1, tech3])}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={async () => tech2 = await getTechniqueSuggestion([tech1, tech3])}>Suggest</button>{/if}
</div>
</div>
<div class="form-group">
<label for="tech3">Technique #3:</label>
<div class="input-row">
<input type="text" id="tech3" placeholder="e.g. Look, a three-headed gull!" bind:value={tech3} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={async () => tech3 = await getTechniqueSuggestion([tech1, tech2])}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={async () => tech3 = await getTechniqueSuggestion([tech1, tech2])}>Suggest</button>{/if}
</div>
</div>
<button type="submit" class="btn btn-primary btn-full glow-effect" disabled={savingTechs}>Submit Techniques to Swap Pool</button>

View File

@@ -25,13 +25,22 @@
{#if open}
<nav class="menu-dropdown">
{#each links as link}
<a
href={link.href}
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener' : undefined}
title={link.title}
on:click={() => (open = false)}
>{link.label}</a>
{#if link.action}
<button
class="menu-item"
title={link.title}
on:click={() => { open = false; link.action(); }}
>{link.label}</button>
{:else}
<a
class="menu-item"
href={link.href}
target={link.external ? '_blank' : undefined}
rel={link.external ? 'noopener' : undefined}
title={link.title}
on:click={() => (open = false)}
>{link.label}</a>
{/if}
{/each}
</nav>
{/if}
@@ -68,7 +77,7 @@
border-radius: var(--radius-md);
overflow: hidden;
}
.menu-dropdown a {
.menu-item {
padding: 8px 16px;
color: var(--accent);
font-size: 0.85rem;
@@ -77,11 +86,14 @@
font-family: var(--font-heading);
text-align: right;
white-space: nowrap;
background: none;
border: none;
cursor: pointer;
}
.menu-dropdown a:hover {
.menu-item:hover {
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.menu-dropdown a + a {
.menu-item + .menu-item {
border-top: 1px solid var(--edge-soft);
}
</style>

View File

@@ -16,6 +16,10 @@
return p.incoming_techniques ? JSON.parse(p.incoming_techniques) : [];
}
// Dev Mode (toggled by the creator from the corner menu) reveals the
// Suggest buttons; in real games players write their own material.
$: devMode = !!state.game.dev_mode;
$: me = state.player;
$: recruits = state.players.filter(p => p.needs_reroll);
$: iAmRecruit = me.needs_reroll;
@@ -150,28 +154,28 @@
<label for="avatar_look">What does your Pi-Rat look like?</label>
<div class="input-row">
<input type="text" id="avatar_look" placeholder="e.g. A blue bandana, scarred snout" bind:value={basicDetails.avatar_look} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>{/if}
</div>
</div>
<div class="form-group">
<label for="avatar_smell">What does your Pi-Rat smell like? (Determines name)</label>
<div class="input-row">
<input type="text" id="avatar_smell" placeholder="e.g. Damp gunpowder, salty cheese" bind:value={basicDetails.avatar_smell} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>{/if}
</div>
</div>
<div class="form-group">
<label for="first_words">What were your first words after transforming?</label>
<div class="input-row">
<input type="text" id="first_words" placeholder="e.g. Where is my gat?!" bind:value={basicDetails.first_words} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>{/if}
</div>
</div>
<div class="form-group">
<label for="good_at_math">Is your Pi-Rat good at Math?</label>
<div class="input-row">
<input type="text" id="good_at_math" placeholder="e.g. Thinks Pi is a dessert" bind:value={basicDetails.good_at_math} required class="input-field">
<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>{/if}
</div>
</div>
<button type="submit" class="btn btn-primary btn-full" disabled={savingBasic}>Save Records</button>
@@ -244,7 +248,7 @@
<label>What do you LIKE about {displayName(task.recruit)}?</label>
<div class="input-row">
<input type="text" class="input-field" bind:value={inboxAnswers[`${task.recruit.id}:like`]} placeholder="e.g. They always share their cheese"/>
<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:like`] = getSuggestion('like')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:like`] = getSuggestion('like')}>Suggest</button>{/if}
<button class="btn btn-primary" disabled={!(inboxAnswers[`${task.recruit.id}:like`] || '').trim()} on:click={() => submitDelegatedAnswer('like', task.recruit.id)}>Submit</button>
</div>
</div>
@@ -254,7 +258,7 @@
<label>What do you HATE about {displayName(task.recruit)}?</label>
<div class="input-row">
<input type="text" class="input-field" bind:value={inboxAnswers[`${task.recruit.id}:hate`]} placeholder="e.g. They snore too loudly"/>
<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:hate`] = getSuggestion('hate')}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:hate`] = getSuggestion('hate')}>Suggest</button>{/if}
<button class="btn btn-primary" disabled={!(inboxAnswers[`${task.recruit.id}:hate`] || '').trim()} on:click={() => submitDelegatedAnswer('hate', task.recruit.id)}>Submit</button>
</div>
</div>
@@ -270,7 +274,7 @@
{/if}
<div class="input-row">
<input type="text" class="input-field" bind:value={techDrafts[`${task.recruit.id}:${slot.slot}`]} placeholder="e.g. Parabolic boarding bounce"/>
<button type="button" class="btn btn-secondary btn-small" on:click={() => suggestTechnique(task.recruit.id, slot.slot)}>Suggest</button>
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggestTechnique(task.recruit.id, slot.slot)}>Suggest</button>{/if}
<button class="btn btn-primary" disabled={!(techDrafts[`${task.recruit.id}:${slot.slot}`] || '').trim()} on:click={() => contributeTechnique(task.recruit.id, slot.slot)}>Submit</button>
</div>
</div>

View File

@@ -2,6 +2,7 @@
import { onMount, onDestroy } from 'svelte';
import { apiRequest } from '../lib/api';
import { extraMenuLinks } from '../lib/menu';
import { getSuggestion } from '../lib/suggestions';
import LobbyPhase from '../components/LobbyPhase.svelte';
import CharacterCreationPhase from '../components/CharacterCreationPhase.svelte';
@@ -58,12 +59,60 @@
intervalId = setInterval(fetchState, 30000);
});
// Surface the creator-only Admin link in the corner menu
$: extraMenuLinks.set(
state?.player?.is_creator
? [{ label: '🛠️ Admin', href: `#/game/${gameId}/admin`, title: 'Open the Admin Panel' }]
: []
);
// Creator-only corner menu entries: Dev Mode toggle (all phases), the
// Skip Character Creation shortcut (Dev Mode only), and the Admin link.
async function toggleDevMode() {
try {
await apiRequest(`/game/${gameId}/player/${playerId}/dev-mode`, 'POST', {
enabled: !state.game.dev_mode
});
} catch (err) {
error = err.message;
}
}
async function skipCharacterCreation() {
// Suggested values for everyone's free-text fields; the backend only
// applies them to fields players haven't filled in themselves.
const fills = {};
for (const p of state.players) {
fills[p.id] = {
avatar_look: getSuggestion('look'),
avatar_smell: getSuggestion('smell'),
first_words: getSuggestion('first_words'),
good_at_math: getSuggestion('good_at_math'),
like: getSuggestion('like'),
hate: getSuggestion('hate'),
};
}
try {
await apiRequest(`/game/${gameId}/player/${playerId}/skip-character-creation`, 'POST', {
fills: JSON.stringify(fills)
});
} catch (err) {
error = err.message;
}
}
$: {
const items = [];
if (state?.player?.is_creator) {
items.push({
label: `🧪 Dev Mode: ${state.game.dev_mode ? 'On' : 'Off'}`,
action: toggleDevMode,
title: 'Toggle Dev Mode testing shortcuts for the whole table',
});
if (state.game.dev_mode && (state.game.phase === 'character_creation' || state.game.phase === 'swap_techniques')) {
items.push({
label: '⏭️ Skip Character Creation',
action: skipCharacterCreation,
title: 'Auto-fill anything unfinished for all players and jump to scene setup',
});
}
items.push({ label: '🛠️ Admin', href: `#/game/${gameId}/admin`, title: 'Open the Admin Panel' });
}
extraMenuLinks.set(items);
}
onDestroy(() => {
destroyed = true;