Skip to content

Commit 7687d19

Browse files
authored
Add reminder tokens (#401)
1 parent 5bf3974 commit 7687d19

9 files changed

Lines changed: 1004 additions & 571 deletions

File tree

src/app/botc/blood-on-the-clocktower.tsx

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ const BloodOnTheClocktowerElement = ({
5858
const players = zip(
5959
shuffle(gameState.characters.slice()),
6060
gameState.lobby,
61-
).map(([characterId, player]) => createPlayer({ ...player, characterId }));
61+
).map(([characterId, player], index) =>
62+
createPlayer({ ...player, characterId, index }),
63+
);
6264
setGameState({
6365
...gameState,
6466
lobby: gameState.lobby.slice(players.length),
@@ -99,7 +101,7 @@ const BloodOnTheClocktowerElement = ({
99101
)}
100102
<div className='h-2' />
101103
{edition && (
102-
<div className='flex gap-4'>
104+
<div className='flex flex-col gap-2 lg:flex-row'>
103105
<Modal
104106
title={`Select Characters - ${edition.name}`}
105107
target={
@@ -117,9 +119,15 @@ const BloodOnTheClocktowerElement = ({
117119
}}
118120
/>
119121
</Modal>
120-
{gameState.characters.length > 0 && (
121-
<Button onClick={assignCharacters}>Assign characters</Button>
122-
)}
122+
<Button
123+
disabled={
124+
gameState.characters.length === 0 &&
125+
'Select some characters first'
126+
}
127+
onClick={assignCharacters}
128+
>
129+
Assign characters
130+
</Button>
123131
</div>
124132
)}
125133
<div className='h-2' />
@@ -152,24 +160,27 @@ const BloodOnTheClocktowerElement = ({
152160
</Button>
153161
</div>
154162
</details>
155-
<details open={detailsStartOpen} className='border p-2 shadow-md'>
156-
<summary className='select-none'>Grimoire</summary>
157-
<Grimoire
158-
players={gameState.players}
159-
setPlayers={(players) => {
160-
setGameState({ ...gameState, players });
161-
}}
162-
/>
163-
</details>
164163
{edition && gameState.players.length > 0 && (
165-
<details open={detailsStartOpen} className='border p-2 shadow-md'>
166-
<summary className='select-none'>Night Order</summary>
167-
<div className='h-2' />
168-
<NightOrder
169-
players={gameState.players}
170-
allCharacters={getAllCharacters(edition)}
171-
/>
172-
</details>
164+
<>
165+
<details open={detailsStartOpen} className='border p-2 shadow-md'>
166+
<summary className='select-none'>Grimoire</summary>
167+
<Grimoire
168+
edition={edition}
169+
players={gameState.players}
170+
setPlayers={(players) => {
171+
setGameState({ ...gameState, players });
172+
}}
173+
/>
174+
</details>
175+
<details open={detailsStartOpen} className='border p-2 shadow-md'>
176+
<summary className='select-none'>Night Order</summary>
177+
<div className='h-2' />
178+
<NightOrder
179+
players={gameState.players}
180+
allCharacters={getAllCharacters(edition)}
181+
/>
182+
</details>
183+
</>
173184
)}
174185
</div>
175186
);

src/app/botc/character-panel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ const BOTCCharacterPanel = ({
1515
<div className='flex flex-col gap-2'>
1616
<div className='flex items-center gap-4'>
1717
<img
18-
className='h-10 w-10 scale-150 lg:h-12 lg:w-12'
18+
className='h-8 w-8 scale-150 lg:h-12 lg:w-12'
1919
loading='lazy'
2020
src={imgSrc}
2121
/>
2222
<h4 className='hidden lg:block'>{name}</h4>
23-
<h5 className='lg:hidden'>{name}</h5>
23+
<h6 className='lg:hidden'>{name}</h6>
2424
</div>
25-
{showDescription && <p className='text-xs'>{description}</p>}
25+
{showDescription && <p className='text-xs lg:text-sm'>{description}</p>}
2626
</div>
2727
);
2828
};

src/app/botc/character-token.tsx

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
import { cn } from 'utils/class-names';
1111
import Button from 'components/input/button';
1212
import { useState } from 'react';
13+
import ReminderToken from './reminder-token';
14+
import Switch from 'components/input/switch';
1315

1416
interface CharacterTokenProps {
1517
playerName?: string;
@@ -18,10 +20,13 @@ interface CharacterTokenProps {
1820
players: BOTCPlayer[];
1921
playerIndex: number;
2022
setPlayers: (newPlayers: BOTCPlayer[]) => void;
23+
allCharacters?: CharacterId[];
2124
}
2225

23-
const FIRST_NIGHT_CHARACTERS = new Set(FIRST_NIGHT_TEXT.map(([id, _]) => id));
24-
const OTHER_NIGHT_CHARACTERS = new Set(OTHER_NIGHTS_TEXT.map(([id, _]) => id));
26+
const FIRST_NIGHT_CHARACTERS = new Set(FIRST_NIGHT_TEXT.map(({ id }) => id));
27+
const OTHER_NIGHT_CHARACTERS = new Set(OTHER_NIGHTS_TEXT.map(({ id }) => id));
28+
29+
type SelectMode = 'none' | 'reminder';
2530

2631
const CharacterToken = ({
2732
playerName,
@@ -30,11 +35,14 @@ const CharacterToken = ({
3035
players,
3136
playerIndex,
3237
setPlayers,
38+
allCharacters = [],
3339
}: CharacterTokenProps) => {
3440
const character = characterId ? CHARACTERS[characterId] : undefined;
3541
const hasLeftLeaf = characterId && FIRST_NIGHT_CHARACTERS.has(characterId);
3642
const hasRightLeaf = characterId && OTHER_NIGHT_CHARACTERS.has(characterId);
3743
const [modalOpen, setModalOpen] = useState(false);
44+
const [selectMode, setSelectMode] = useState<SelectMode>('none');
45+
const [showAllReminders, setShowAllReminders] = useState(false);
3846

3947
const killOrRevivePlayer = () => {
4048
const newPlayers = players.slice();
@@ -45,15 +53,18 @@ const CharacterToken = ({
4553
setPlayers(newPlayers);
4654
};
4755

56+
const canKill = character?.reminderTokens?.includes('Killed by') ?? false;
57+
4858
return (
4959
<Modal
5060
title={
51-
playerName
52-
? playerName + (character ? `, the ${character.name}` : '')
53-
: character
54-
? character.name
55-
: 'Token'
61+
selectMode === 'none'
62+
? character
63+
? character.name + (playerName ? ` (${playerName})` : '')
64+
: 'Empty Token'
65+
: 'Add reminder token'
5666
}
67+
withCloseButton
5768
open={modalOpen}
5869
onFocus={() => {
5970
setModalOpen(true);
@@ -88,7 +99,6 @@ const CharacterToken = ({
8899
src='/botc/leaf-right.webp'
89100
/>
90101
)}
91-
92102
{character && (
93103
<>
94104
<img
@@ -101,7 +111,7 @@ const CharacterToken = ({
101111
d='M 13 75 C 13 160, 138 160, 138 75'
102112
id='curve'
103113
fill='transparent'
104-
></path>
114+
/>
105115
<text textAnchor='middle'>
106116
<textPath startOffset='50%' href='#curve'>
107117
{character.name}
@@ -113,9 +123,58 @@ const CharacterToken = ({
113123
</div>
114124
}
115125
>
116-
<Button onClick={killOrRevivePlayer}>
117-
{dead ? 'Revive' : 'Kill'} player
118-
</Button>
126+
{character?.description}
127+
{selectMode === 'none' && (
128+
<div className='grid grid-cols-2 gap-x-4 gap-y-2'>
129+
<Button fullWidth onClick={killOrRevivePlayer}>
130+
{dead ? 'Revive' : 'Kill'} this player
131+
</Button>
132+
<Button fullWidth disabled={!canKill} onClick={killOrRevivePlayer}>
133+
Kill another player
134+
</Button>
135+
<Button
136+
fullWidth
137+
onClick={() => {
138+
setSelectMode('reminder');
139+
}}
140+
>
141+
Add reminder
142+
</Button>
143+
</div>
144+
)}
145+
{selectMode === 'reminder' && (
146+
<div className='flex flex-col gap-4'>
147+
<div className='grid grid-cols-4 gap-y-2 md:grid-cols-5 lg:grid-cols-7'>
148+
{(showAllReminders
149+
? allCharacters
150+
: players.flatMap((p) => p.characterId)
151+
).flatMap(
152+
(id) =>
153+
CHARACTERS[id].reminderTokens?.map((reminderText) => (
154+
<ReminderToken
155+
key={id + reminderText}
156+
onClick={() => {
157+
const newPlayers = players.slice();
158+
newPlayers[playerIndex]?.reminders.push({
159+
characterId: id,
160+
message: reminderText,
161+
});
162+
setPlayers(newPlayers);
163+
setModalOpen(false);
164+
}}
165+
characterId={id}
166+
text={reminderText}
167+
/>
168+
)) ?? [],
169+
)}
170+
</div>
171+
<Switch
172+
label='Show all reminders'
173+
value={showAllReminders}
174+
onChange={setShowAllReminders}
175+
/>
176+
</div>
177+
)}
119178
</Modal>
120179
);
121180
};

0 commit comments

Comments
 (0)