Skip to content

Commit 75676a1

Browse files
authored
Fix token scaling (#408)
1 parent ea5c0e9 commit 75676a1

8 files changed

Lines changed: 155 additions & 77 deletions

src/app/botc/actions-modal.tsx

Lines changed: 81 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { useState } from 'react';
2-
import { CharacterId, CHARACTERS } from './characters';
1+
import { useMemo, useState } from 'react';
2+
import { CharacterId, CHARACTERS, Reminder } from './characters';
33
import ReminderToken from './reminder-token';
44
import Modal from 'components/modal';
55
import { BotcPlayer } from './blood-on-the-clocktower-game';
66
import Button from 'components/input/button';
77
import Switch from 'components/input/switch';
8+
import Divider from 'components/divider';
89

9-
type SelectMode = 'none' | 'reminder' | 'player';
10+
type SelectMode = 'none' | 'player';
1011

1112
interface BotcActionsModalProps {
1213
open: boolean;
@@ -37,16 +38,19 @@ const BotcActionsModal = ({
3738
const [addMultipleReminders, setAddMultipleReminders] = useState(false);
3839

3940
const characters = new Set(players.map((p) => p.characterId));
40-
const reminderTokens = (
41-
showAllReminders
42-
? allCharacters
43-
: allCharacters.filter((id) => characters.has(id))
44-
).flatMap(
45-
(id) =>
46-
CHARACTERS[id].reminderTokens?.map((reminderText) => ({
47-
id,
48-
reminderText,
49-
})) ?? [],
41+
const reminderTokens = useMemo(
42+
() =>
43+
(showAllReminders
44+
? allCharacters
45+
: allCharacters.filter((id) => characters.has(id))
46+
).flatMap(
47+
(id) =>
48+
CHARACTERS[id].reminderTokens?.map((reminderText) => ({
49+
characterId: id,
50+
message: reminderText,
51+
})) ?? [],
52+
),
53+
[showAllReminders, allCharacters, characters],
5054
);
5155

5256
const killOrRevivePlayer = () => {
@@ -60,13 +64,30 @@ const BotcActionsModal = ({
6064

6165
const canKill = character.reminderTokens?.includes('Killed by') ?? false;
6266

67+
const reminderHash = (reminder: Reminder) =>
68+
`id:${reminder.characterId},message:${reminder.message}`;
69+
const alreadyAdded = new Set(player.reminders.map(reminderHash));
70+
71+
const filterReminderTokens = (reminder: Reminder) => {
72+
if (alreadyAdded.has(reminderHash(reminder))) {
73+
return false;
74+
}
75+
76+
if (
77+
reminder.message === 'No ability' &&
78+
reminder.characterId !== player.characterId
79+
) {
80+
return false;
81+
}
82+
83+
return true;
84+
};
85+
6386
return (
6487
<Modal
6588
title={
6689
selectMode === 'none'
6790
? character.name + (player.name ? ` (${player.name})` : '')
68-
: selectMode === 'reminder'
69-
? 'Add reminder token'
7091
: 'Select player'
7192
}
7293
withCloseButton
@@ -86,31 +107,64 @@ const BotcActionsModal = ({
86107
<Button fullWidth onClick={killOrRevivePlayer}>
87108
{player.isAlive ? 'Kill' : 'Revive'} this player
88109
</Button>
89-
<Button fullWidth disabled={!canKill} onClick={killOrRevivePlayer}>
110+
<Button fullWidth disabled={!canKill}>
90111
Kill another player
91112
</Button>
92-
<Button
93-
fullWidth
94-
onClick={() => {
95-
setSelectMode('reminder');
96-
}}
97-
>
98-
Add reminder
99-
</Button>
113+
</div>
114+
<Divider />
115+
<div className='flex flex-col gap-2 px-2'>
116+
<div className='flex flex-col gap-4 md:flex-row'>
117+
<h4>Add Reminder</h4>
118+
<Switch
119+
label='Show tokens not in play'
120+
value={showAllReminders}
121+
onChange={setShowAllReminders}
122+
/>
123+
<Switch
124+
label='Add multiple tokens'
125+
value={addMultipleReminders}
126+
onChange={setAddMultipleReminders}
127+
/>
128+
</div>
129+
<div className='grid grid-cols-3 gap-4 md:grid-cols-5 lg:grid-cols-7'>
130+
{reminderTokens
131+
.filter(filterReminderTokens)
132+
.map(({ characterId, message }) => (
133+
<ReminderToken
134+
key={characterId + message}
135+
onClick={() => {
136+
const newPlayers = players.slice();
137+
newPlayers[playerIndex]?.reminders.push({
138+
characterId,
139+
message,
140+
});
141+
setPlayers(newPlayers);
142+
if (!addMultipleReminders) {
143+
setOpen(false);
144+
}
145+
}}
146+
characterId={characterId}
147+
text={message}
148+
/>
149+
))}
150+
</div>
100151
</div>
101152
{player.reminders.length > 0 && (
102153
<>
103-
<h5>Added reminders (click to remove)</h5>
104-
<div className='grid grid-cols-4 gap-y-2 md:grid-cols-5 lg:grid-cols-7'>
154+
<h4>Added reminders (click to remove)</h4>
155+
<div className='grid grid-cols-3 gap-4 px-2 md:grid-cols-5 lg:grid-cols-7'>
105156
{player.reminders.map((reminder, i) => (
106157
<ReminderToken
107-
key={`index:${playerIndex}${reminder.characterId}${reminder.message}`}
158+
key={`player:${playerIndex}${reminder.characterId}${reminder.message}`}
108159
characterId={reminder.characterId}
109160
text={reminder.message}
110161
onClick={() => {
111162
const newPlayers = players.slice();
112163
newPlayers[playerIndex]?.reminders.splice(i, 1);
113164
setPlayers(newPlayers);
165+
if (!addMultipleReminders) {
166+
setOpen(false);
167+
}
114168
}}
115169
/>
116170
))}
@@ -119,50 +173,6 @@ const BotcActionsModal = ({
119173
)}
120174
</div>
121175
)}
122-
{selectMode === 'reminder' && (
123-
<div className='flex flex-col gap-4'>
124-
<div className='flex gap-4'>
125-
<Switch
126-
label='Show all'
127-
value={showAllReminders}
128-
onChange={setShowAllReminders}
129-
/>
130-
<Switch
131-
label='Add multiple'
132-
value={addMultipleReminders}
133-
onChange={setAddMultipleReminders}
134-
/>
135-
</div>
136-
<div className='grid grid-cols-4 gap-y-2 md:grid-cols-5 lg:grid-cols-7'>
137-
{reminderTokens.map(({ id, reminderText }) => (
138-
<ReminderToken
139-
key={id + reminderText}
140-
onClick={() => {
141-
const newPlayers = players.slice();
142-
newPlayers[playerIndex]?.reminders.push({
143-
characterId: id,
144-
message: reminderText,
145-
});
146-
setPlayers(newPlayers);
147-
if (!addMultipleReminders) {
148-
setOpen(false);
149-
setSelectMode('none');
150-
}
151-
}}
152-
characterId={id}
153-
text={reminderText}
154-
/>
155-
))}
156-
</div>
157-
<Button
158-
onClick={() => {
159-
setSelectMode('none');
160-
}}
161-
>
162-
Cancel
163-
</Button>
164-
</div>
165-
)}
166176
{selectMode === 'player' && 'test'}
167177
</Modal>
168178
);

src/app/botc/blood-on-the-clocktower-game.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { shuffle, zip } from 'utils/array';
1+
import { chooseRandom, shuffle, zip } from 'utils/array';
22
import {
33
Alignment,
44
CHARACTER_TYPES,
@@ -15,6 +15,7 @@ export class BotcGame {
1515
edition: Edition;
1616
lobby: { name: string; corpsId?: string }[];
1717
players: BotcPlayer[];
18+
demonBluffs: CharacterId[];
1819

1920
constructor(initial: {
2021
edition: Edition;
@@ -24,6 +25,7 @@ export class BotcGame {
2425
this.edition = initial.edition;
2526
this.lobby = initial.lobby ?? [];
2627
this.players = initial.players ?? [];
28+
this.demonBluffs = [];
2729
}
2830

2931
static fromJSON(json: string) {
@@ -59,12 +61,36 @@ export class BotcGame {
5961
return CHARACTER_TYPES.flatMap((t) => this.edition[t]);
6062
}
6163

64+
charactersInPlay() {
65+
return this.players.map((p) => p.characterId);
66+
}
67+
6268
assignCharacters(characters: CharacterId[]) {
6369
this.players = zip(shuffle(characters.slice()), this.lobby).map(
6470
([characterId, player], index) =>
6571
new BotcPlayer({ ...player, characterId, index }),
6672
);
6773
}
74+
75+
generateDemonBluffs(numberOfBluffs: number = 3) {
76+
const chosenCharacterSet = new Set(this.charactersInPlay());
77+
const validTownsfolk = shuffle(
78+
this.edition.townsfolk.filter((t) => !chosenCharacterSet.has(t)),
79+
);
80+
const validOutsiders = this.edition.outsiders.filter(
81+
(t) => !chosenCharacterSet.has(t),
82+
);
83+
84+
const outsiderBluff = chooseRandom(validOutsiders);
85+
86+
if (!outsiderBluff || numberOfBluffs === 1) {
87+
return validTownsfolk.slice(0, numberOfBluffs);
88+
} else {
89+
const bluffs = validTownsfolk.slice(0, numberOfBluffs - 1);
90+
bluffs.push(outsiderBluff);
91+
return bluffs;
92+
}
93+
}
6894
}
6995

7096
// interface Neighbours {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Grimoire from './grimoire';
1313
import ParamsTextInput from 'components/input/params-text-input';
1414
import { BotcGame } from './blood-on-the-clocktower-game';
1515
import BotcActionsModal from './actions-modal';
16+
import InfoTokenList from './info-token-list';
1617

1718
export const metadata: Metadata = {
1819
title: 'Blood on the Clocktower',
@@ -185,6 +186,14 @@ const BloodOnTheClocktowerElement = () => {
185186
allCharacters={getAllCharacters(edition)}
186187
/>
187188
</details>
189+
<details open={detailsStartOpen} className='border p-2 shadow-md'>
190+
<summary className='select-none'>Info Tokens</summary>
191+
192+
<InfoTokenList
193+
chosenCharacters={gameState.charactersInPlay()}
194+
allCharacters={gameState.characters()}
195+
/>
196+
</details>
188197
</>
189198
)}
190199
</div>

src/app/botc/character-token.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const CharacterToken = ({
3232
return (
3333
<div
3434
className={cn(
35-
'relative mt-1 h-32 w-32 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[repeat] bg-[url(/botc/token-noise.webp)] bg-auto text-center shadow-md transition-all hover:scale-105 hover:cursor-pointer',
35+
'relative mt-1 aspect-square w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-[repeat] bg-[url(/botc/token-noise.webp)] bg-auto text-center shadow-md transition-all hover:scale-105 hover:cursor-pointer',
3636
dead && 'grayscale',
3737
)}
3838
onClick={onClick}

src/app/botc/grimoire.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const Grimoire = ({ players, setCurrentPlayerIndex }: GrimoireProps) => {
2626
return (
2727
<React.Fragment key={key}>
2828
<div
29-
className='absolute'
29+
className='absolute w-[18%] min-w-[92px]'
3030
style={{
3131
left: toPercent(point.left),
3232
top: toPercent(point.top),
@@ -45,7 +45,7 @@ const Grimoire = ({ players, setCurrentPlayerIndex }: GrimoireProps) => {
4545
.concat(player.automaticReminders)
4646
.map((reminder, i) => (
4747
<div
48-
className='absolute -translate-x-1/2 -translate-y-1/2'
48+
className='absolute w-[10%] min-w-[64px] -translate-x-1/2 -translate-y-1/2'
4949
key={key + reminder.characterId + reminder.message}
5050
style={{
5151
left: toPercent(

src/app/botc/info-token-list.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { CharacterId } from './characters';
2+
3+
interface InfoTokenListProps {
4+
chosenCharacters: CharacterId[];
5+
allCharacters: CharacterId[];
6+
}
7+
8+
const InfoTokenList = ({
9+
chosenCharacters,
10+
allCharacters,
11+
}: InfoTokenListProps) => {
12+
return (
13+
<div className='text-wrap grid grid-cols-2 lg:grid-cols-3'>
14+
PLACE INFO TOKENS HERE
15+
{` chosenCharacters: [${chosenCharacters.join(', ')}]\n\n`}
16+
{`allCharacters: [${allCharacters.join(', ')}]`}
17+
</div>
18+
);
19+
};
20+
21+
export default InfoTokenList;

src/app/botc/reminder-token.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { cn } from 'utils/class-names';
2-
import { CharacterId, CHARACTERS, getImagePathFromId } from './characters';
2+
import {
3+
CharacterId,
4+
CHARACTERS,
5+
getDefaultAlignment,
6+
getImagePathFromId,
7+
} from './characters';
38

49
interface ReminderTokenProps {
510
characterId: CharacterId;
@@ -9,10 +14,14 @@ interface ReminderTokenProps {
914

1015
const ReminderToken = ({ characterId, text, onClick }: ReminderTokenProps) => {
1116
const character = CHARACTERS[characterId];
17+
const defaultAlignment = getDefaultAlignment(character.id);
1218
const imgSrc = getImagePathFromId(characterId);
1319
return (
1420
<div
15-
className='relative h-24 w-24 scale-75 rounded-full bg-blue-900 shadow-md hover:cursor-pointer'
21+
className={cn(
22+
'relative aspect-square w-full rounded-full shadow-md hover:cursor-pointer',
23+
defaultAlignment === 'good' ? 'bg-blue-900' : 'bg-red-900',
24+
)}
1625
onClick={onClick}
1726
>
1827
<img

src/utils/array.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export const intersection = <T>(a: T[], b: T[]) => {
4949
export const filterNone = <T>(list: (T | null | undefined)[]): T[] =>
5050
list.flatMap((e) => (e !== null && e !== undefined ? [e] : []));
5151

52+
export const chooseRandom = <T>(list: T[]) =>
53+
list[Math.floor(Math.random() * list.length)];
54+
5255
export const shuffle = <T>(list: T[]) => {
5356
for (let i = list.length - 1; i >= 0; i--) {
5457
const randomIndex = Math.floor(Math.random() * (i + 1));

0 commit comments

Comments
 (0)