83 |
Class
84 |
85 | { ClassesList.map(className => (
86 | { setClass(className) }}
90 | >
91 |
92 | {capitalize(className)}
93 |
94 | )) }
95 |
96 |
97 |
Level
98 |
99 | { range(20).map(i => i+1).map(lvl => (
100 | setLevel(lvl)}
104 | >
105 | {lvl}
106 |
107 | )) }
108 |
109 |
110 | { !chosenClass ? null : (
111 | (chosenClass.type === 'barbarian') ? (
112 | <>
113 |
Barbarian-specific Options
114 |
115 | setClassOptions<'barbarian'>(options => { options.gwm = !options.gwm })}>
118 | Use Great Weapon Master
119 |
120 |
121 | Weapon:
122 | setClassOptions<'barbarian'>(options => { options.weaponBonus = newValue })}
127 | label={`+${chosenClass.options.weaponBonus}`}
128 | />
129 |
130 |
131 | >
132 | ) : (chosenClass.type === 'bard') ? (
133 | <>>
134 | ) : (chosenClass.type === 'cleric') ? (
135 | <>>
136 | ) : (chosenClass.type === 'druid') ? (
137 | <>>
138 | ) : (chosenClass.type === 'fighter') ? (
139 | <>
140 |
Fighter-specific Options
141 |
142 | setClassOptions<'fighter'>(options => { options.gwm = !options.gwm })}>
145 | Use Great Weapon Master
146 |
147 |
148 | Weapon:
149 | setClassOptions<'fighter'>(options => { options.weaponBonus = newValue })}
154 | label={`+${chosenClass.options.weaponBonus}`}
155 | />
156 |
157 |
158 | >
159 | ) : (chosenClass.type === 'monk') ? (
160 | <>>
161 | ) : (chosenClass.type === 'paladin') ? (
162 | <>
163 |
Paladin-specific Options
164 |
165 | setClassOptions<'paladin'>(options => { options.gwm = !options.gwm })}>
168 | Use Great Weapon Master
169 |
170 |
171 | Weapon:
172 | setClassOptions<'paladin'>(options => { options.weaponBonus = newValue })}
177 | label={`+${chosenClass.options.weaponBonus}`}
178 | />
179 |
180 |
181 | >
182 | ) : (chosenClass.type === 'ranger') ? (
183 | <>
184 |
Ranger-specific Options
185 |
186 | setClassOptions<'ranger'>(options => { options.ss = !options.ss })}>
189 | Use Sharpshooter
190 |
191 |
192 | Weapon:
193 | setClassOptions<'ranger'>(options => { options.weaponBonus = newValue })}
198 | label={`+${chosenClass.options.weaponBonus}`}
199 | />
200 |
201 |
202 | >
203 | ) : (chosenClass.type === 'rogue') ? (
204 | <>
205 |
Rogue-specific Options
206 |
207 | setClassOptions<'ranger'>(options => { options.ss = !options.ss })}>
210 | Use Sharpshooter
211 |
212 |
213 | Weapon:
214 | setClassOptions<'ranger'>(options => { options.weaponBonus = newValue })}
219 | label={`+${chosenClass.options.weaponBonus}`}
220 | />
221 |
222 |
223 | >
224 | ) : (chosenClass.type === 'sorcerer') ? (
225 | <>>
226 | ) : (chosenClass.type === 'warlock') ? (
227 | <>>
228 | ) : (chosenClass.type === 'wizard') ? (
229 | <>>
230 | ) : null
231 | ) }
232 |
233 | )
234 | }
235 |
236 | export default PlayerForm
--------------------------------------------------------------------------------
/src/components/simulation/adventuringDayForm.module.scss:
--------------------------------------------------------------------------------
1 | @import '/styles/mixins';
2 |
3 | .adventuringDay {
4 | display: flex;
5 | flex-direction: column;
6 | gap: 1.5em;
7 |
8 | section {
9 | display: grid;
10 | grid-template-columns: 200px calc(100% - 200px);
11 | align-items: center;
12 | gap: 8px;
13 |
14 | @media (width < 800px) { grid-template-columns: 1fr }
15 | }
16 |
17 | h3 { user-select: none; white-space: nowrap; margin: 0.5em 0 }
18 |
19 | .save {
20 | align-items: center;
21 | display: grid;
22 | grid-template-columns: 1fr 1fr 50px;
23 |
24 | .fileName {
25 | font-weight: bold;
26 | }
27 |
28 | button {
29 | padding: 0;
30 | background: none;
31 | transition: color 0.3s;
32 | &:hover { color: #aaa }
33 | }
34 | }
35 |
36 | .buttons {
37 | display: flex;
38 | flex-direction: row;
39 |
40 | button, label {
41 | flex: 1 1 0;
42 | }
43 |
44 | label {
45 | @include clickable;
46 | }
47 | }
48 |
49 | .error {
50 | text-align: center;
51 | color: #c88;
52 | }
53 | }
--------------------------------------------------------------------------------
/src/components/simulation/adventuringDayForm.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState } from "react"
2 | import { Creature, CreatureSchema, Encounter, EncounterSchema } from "../../model/model"
3 | import styles from './adventuringDayForm.module.scss'
4 | import { sharedStateGenerator, useCalculatedState } from "../../model/utils"
5 | import {z} from 'zod'
6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
7 | import { faDownload, faFolder, faSave, faTrash, faUpload } from "@fortawesome/free-solid-svg-icons"
8 | import { PlayerTemplates } from "../../data/data"
9 | import { getMonster } from "../../data/monsters"
10 | import SortTable from "../utils/sortTable"
11 | import Modal from "../utils/modal"
12 |
13 | type PropType = {
14 | players: Creature[],
15 | encounters: Encounter[],
16 | onCancel: () => void,
17 | onLoad?: (players: Creature[], encounters: Encounter[]) => void,
18 | }
19 |
20 | function carefulSave(key: string, value: string) {
21 | if (!localStorage.getItem('useLocalStorage')) return
22 | localStorage.setItem(key, value)
23 | }
24 |
25 | const SaveFileSchema = z.object({
26 | updated: z.number(),
27 | name: z.string(),
28 | players: z.array(CreatureSchema),
29 | encounters: z.array(EncounterSchema),
30 | })
31 | type SaveFile = z.infer