Skip to content

Commit 769ac4f

Browse files
authored
Merge pull request #309 from noahm/pick-placeholders
2 parents 916d25d + 1050b00 commit 769ac4f

15 files changed

+217
-78
lines changed

docs/images/settings-drawer.png

-6.96 KB
Loading

docs/readme.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ Some of the less self-explanatory settings are described below.
3434
![Screenshot of the settings drawer](images/settings-drawer.png)
3535

3636
<dl>
37+
<dt>Free picks
38+
<dd>Adds extra empty placeholder cards to the beginning of each draw which can be replaced with player picks.
39+
3740
<dt>Reorder by pick/ban
3841
<dd>Moves charts to the beginning or end of the set when charts are protected or vetoed.
3942

40-
<dt>Pocket picks must match draw options
41-
<dd>Limits charts selected in a pocket pick action to those that match current draw settings.
43+
<dt>Picks must match draw options
44+
<dd>Limits charts selected in a free/pocket pick action to those that match current draw settings.
4245

4346
<dt>Sort by chart level
4447
<dd>Sorts the drawn charts by difficulty level, from lowest on the left, to highest on the right. When not enabled the chart order is randomized.

src/assets/i18n.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@
2525
"drawerTitle": "Settings",
2626
"hideShowFilters": "Chart Filters",
2727
"chartCount": "Number to draw",
28+
"playerPicks": "Free picks",
2829
"upperBoundLvl": "Lvl Max",
2930
"lowerBoundLvl": "Lvl Min",
3031
"upperBoundTier": "Tier Max",
3132
"lowerBoundTier": "Tier Min",
32-
"constrainPocketPicks": "Pocket picks must match draw options",
33+
"constrainPocketPicks": "Picks must match draw options",
3334
"useWeightedDistributions": "Use Weighted Distributions",
3435
"sortByLevel": "Sort by chart level",
3536
"orderByAction": "Re-order by pick/ban",

src/card-draw.ts

+30-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import { chunkInPieces, pickRandomItem, rangeI, shuffle, times } from "./utils";
44
import { CountingSet } from "./utils/counting-set";
55
import { DefaultingMap } from "./utils/defaulting-set";
66
import { Fraction } from "./utils/fraction";
7-
import { DrawnChart, EligibleChart, Drawing } from "./models/Drawing";
7+
import {
8+
DrawnChart,
9+
EligibleChart,
10+
Drawing,
11+
PlayerPickPlaceholder,
12+
CHART_PLACEHOLDER,
13+
CHART_DRAWN,
14+
} from "./models/Drawing";
815
import { ConfigState } from "./config-state";
916
import { getDifficultyColor } from "./hooks/useDifficultyColor";
1017
import {
@@ -322,6 +329,7 @@ export function draw(gameData: GameData, configData: ConfigState): Drawing {
322329
...randomChart,
323330
// Give this random chart a unique id within this drawing
324331
id: `drawn_chart-${nanoid(5)}`,
332+
type: CHART_DRAWN,
325333
});
326334
// remove drawn chart from deck so it cannot be re-drawn
327335
selectableCharts.splice(randomIndex, 1);
@@ -342,15 +350,29 @@ export function draw(gameData: GameData, configData: ConfigState): Drawing {
342350
}
343351
}
344352

353+
const charts: Drawing["charts"] = configData.sortByLevel
354+
? drawnCharts.sort(
355+
(a, b) =>
356+
chartLevelOrTier(a, useGranularLevels, false) -
357+
chartLevelOrTier(b, useGranularLevels, false),
358+
)
359+
: shuffle(drawnCharts);
360+
361+
if (configData.playerPicks) {
362+
charts.unshift(
363+
...times(
364+
configData.playerPicks,
365+
(): PlayerPickPlaceholder => ({
366+
id: `pick_placeholder-` + nanoid(5),
367+
type: CHART_PLACEHOLDER,
368+
}),
369+
),
370+
);
371+
}
372+
345373
return {
346374
id: `draw-${nanoid(10)}`,
347-
charts: configData.sortByLevel
348-
? drawnCharts.sort(
349-
(a, b) =>
350-
chartLevelOrTier(a, useGranularLevels, false) -
351-
chartLevelOrTier(b, useGranularLevels, false),
352-
)
353-
: shuffle(drawnCharts),
375+
charts,
354376
players: times(defaultPlayersPerDraw, () => ""),
355377
bans: [],
356378
protects: [],

src/config-state.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createWithEqualityFn } from "zustand/traditional";
33

44
export interface ConfigState {
55
chartCount: number;
6+
playerPicks: number;
67
upperBound: number;
78
lowerBound: number;
89
useWeights: boolean;
@@ -28,6 +29,7 @@ export interface ConfigState {
2829
export const useConfigState = createWithEqualityFn<ConfigState>(
2930
(set) => ({
3031
chartCount: 5,
32+
playerPicks: 0,
3133
upperBound: 0,
3234
lowerBound: 0,
3335
useWeights: false,

src/controls/controls-drawer.tsx

+65-41
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
People,
2020
CaretDown,
2121
CaretRight,
22+
Plus,
2223
} from "@blueprintjs/icons";
2324
import { useMemo, useState } from "react";
2425
import { shallow } from "zustand/shallow";
@@ -171,6 +172,7 @@ function GeneralSettings() {
171172
chartCount,
172173
sortByLevel,
173174
useGranularLevels,
175+
playerPicks,
174176
} = configState;
175177
const availableDifficulties = useMemo(() => {
176178
if (!gameData) {
@@ -241,7 +243,7 @@ function GeneralSettings() {
241243
<Divider />
242244
</>
243245
)}
244-
<div className={isNarrow ? undefined : styles.inlineControls}>
246+
<div className={styles.inlineControls}>
245247
<FormGroup
246248
label={t("controls.chartCount")}
247249
contentClassName={styles.narrowInput}
@@ -263,46 +265,68 @@ function GeneralSettings() {
263265
}}
264266
/>
265267
</FormGroup>
266-
<div className={styles.inlineControls}>
267-
<FormGroup
268-
label={
269-
usesDrawGroups
270-
? t("controls.lowerBoundTier")
271-
: t("controls.lowerBoundLvl")
272-
}
273-
contentClassName={styles.narrowInput}
274-
>
275-
<NumericInput
276-
large
277-
fill
278-
type="number"
279-
inputMode="numeric"
280-
value={useGranularLevels ? lowerBound.toFixed(2) : lowerBound}
281-
min={availableLevels[0]}
282-
max={Math.max(upperBound, lowerBound, 1)}
283-
onValueChange={handleLowerBoundChange}
284-
/>
285-
</FormGroup>
286-
<FormGroup
287-
label={
288-
usesDrawGroups
289-
? t("controls.upperBoundTier")
290-
: t("controls.upperBoundLvl")
291-
}
292-
contentClassName={styles.narrowInput}
293-
>
294-
<NumericInput
295-
large
296-
fill
297-
type="number"
298-
inputMode="numeric"
299-
value={useGranularLevels ? upperBound.toFixed(2) : upperBound}
300-
min={lowerBound}
301-
max={availableLevels[availableLevels.length - 1]}
302-
onValueChange={handleUpperBoundChange}
303-
/>
304-
</FormGroup>
305-
</div>
268+
<Plus className={styles.plus} size={20} />
269+
<FormGroup
270+
label={t("controls.playerPicks")}
271+
contentClassName={styles.narrowInput}
272+
>
273+
<NumericInput
274+
large
275+
fill
276+
type="number"
277+
inputMode="numeric"
278+
value={playerPicks}
279+
min={0}
280+
clampValueOnBlur
281+
onValueChange={(playerPicks) => {
282+
if (!isNaN(playerPicks)) {
283+
updateState(() => {
284+
return { playerPicks };
285+
});
286+
}
287+
}}
288+
/>
289+
</FormGroup>
290+
</div>
291+
<div className={styles.inlineControls}>
292+
<FormGroup
293+
label={
294+
usesDrawGroups
295+
? t("controls.lowerBoundTier")
296+
: t("controls.lowerBoundLvl")
297+
}
298+
contentClassName={styles.narrowInput}
299+
>
300+
<NumericInput
301+
large
302+
fill
303+
type="number"
304+
inputMode="numeric"
305+
value={useGranularLevels ? lowerBound.toFixed(2) : lowerBound}
306+
min={availableLevels[0]}
307+
max={Math.max(upperBound, lowerBound, 1)}
308+
onValueChange={handleLowerBoundChange}
309+
/>
310+
</FormGroup>
311+
<FormGroup
312+
label={
313+
usesDrawGroups
314+
? t("controls.upperBoundTier")
315+
: t("controls.upperBoundLvl")
316+
}
317+
contentClassName={styles.narrowInput}
318+
>
319+
<NumericInput
320+
large
321+
fill
322+
type="number"
323+
inputMode="numeric"
324+
value={useGranularLevels ? upperBound.toFixed(2) : upperBound}
325+
min={lowerBound}
326+
max={availableLevels[availableLevels.length - 1]}
327+
onValueChange={handleUpperBoundChange}
328+
/>
329+
</FormGroup>
306330
</div>
307331
<Button
308332
alignText="left"

src/controls/controls.css

+7-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@
1717
margin-bottom: 0;
1818
}
1919

20-
.inlineControls > * {
20+
.inlineControls > div {
2121
margin-right: 25px;
2222
}
2323

24+
.plus {
25+
margin-top: 30px;
26+
margin-left: -23px;
27+
margin-right: 1px;
28+
}
29+
2430
.narrowInput {
2531
width: 110px;
2632
}

src/controls/degrs-tester.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import {
2020
import { SongCard, SongCardProps } from "../song-card/song-card";
2121
import { useState } from "react";
2222
import { Rain, Repeat, WarningSign } from "@blueprintjs/icons";
23+
import { EligibleChart, PlayerPickPlaceholder } from "../models/Drawing";
2324

24-
export function isDegrs(thing: { name: string; artist: string }) {
25-
return thing.name.startsWith('DEAD END("GROOVE');
25+
export function isDegrs(thing: EligibleChart | PlayerPickPlaceholder) {
26+
return "name" in thing && thing.name.startsWith('DEAD END("GROOVE');
2627
}
2728

2829
function* oneMillionDraws() {

src/drawing-context.tsx

+34-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useConfigState } from "./config-state";
55
import { createContextualStore } from "./zustand/contextual-zustand";
66
import { useDrawState } from "./draw-state";
77
import {
8+
CHART_PLACEHOLDER,
89
Drawing,
910
EligibleChart,
1011
PlayerActionOnChart,
@@ -115,7 +116,9 @@ const {
115116
...self.pocketPicks.map((pick) => pick.chartId),
116117
...self.protects.map((pick) => pick.chartId),
117118
]);
118-
const keepCharts = self.charts.filter((c) => keepChartIds.has(c.id));
119+
const keepCharts = self.charts.filter(
120+
(c) => keepChartIds.has(c.id) || c.type === CHART_PLACEHOLDER,
121+
);
119122
const newCharts = draw(useDrawState.getState().gameData!, {
120123
...useConfigState.getState(),
121124
chartCount: get().charts.length - keepCharts.length,
@@ -130,19 +133,35 @@ const {
130133
const charts = drawing.charts.slice();
131134
const key = keyFromAction(action);
132135
const arr = drawing[key].slice() as PlayerActionOnChart[] | PocketPick[];
136+
const targetChartIdx = charts.findIndex((chart) => chart.id === chartId);
137+
const targetChart = charts[targetChartIdx];
133138

134-
if (useConfigState.getState().orderByAction) {
135-
const indexToCut = charts.findIndex((chart) => chart.id === chartId);
136-
const [shiftedChart] = charts.splice(indexToCut, 1);
139+
if (
140+
useConfigState.getState().orderByAction &&
141+
targetChart?.type !== CHART_PLACEHOLDER
142+
) {
143+
charts.splice(targetChartIdx, 1);
137144
if (action === "ban") {
138145
// insert at tail of list
139146
const insertPoint = charts.length;
140-
charts.splice(insertPoint, 0, shiftedChart);
147+
charts.splice(insertPoint, 0, targetChart);
141148
} else {
142-
// insert at head of list, behind other picks
143-
const insertPoint =
144-
drawing.protects.length + drawing.pocketPicks.length;
145-
charts.splice(insertPoint, 0, shiftedChart);
149+
const frontLockedCardCount =
150+
// number of placeholder cards total (picked and unpicked)
151+
charts.reduce<number>(
152+
(total, curr) =>
153+
total + (curr.type === CHART_PLACEHOLDER ? 1 : 0),
154+
0,
155+
) +
156+
// number of protects
157+
drawing.protects.length +
158+
// number of picks NOT targeting placeholder cards
159+
drawing.pocketPicks.filter(
160+
(p) => p.targetType !== CHART_PLACEHOLDER,
161+
).length;
162+
163+
// insert at head of list, behind other picks/placeholders
164+
charts.splice(frontLockedCardCount, 0, targetChart);
146165
}
147166
set({
148167
charts,
@@ -154,7 +173,12 @@ const {
154173
arr.splice(existingIndex, 1);
155174
} else {
156175
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
157-
arr.push({ player, pick: newChart!, chartId });
176+
arr.push({
177+
player,
178+
pick: newChart!,
179+
chartId,
180+
targetType: targetChart.type,
181+
});
158182
}
159183
set({
160184
[key]: arr,

src/models/Drawing.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,17 @@ export interface EligibleChart {
1717
song: Song;
1818
}
1919

20+
export const CHART_PLACEHOLDER = "PLACEHOLDER";
21+
22+
export interface PlayerPickPlaceholder {
23+
id: string;
24+
type: typeof CHART_PLACEHOLDER;
25+
}
26+
27+
export const CHART_DRAWN = "DRAWN";
2028
export interface DrawnChart extends EligibleChart {
2129
id: string;
30+
type: typeof CHART_DRAWN;
2231
}
2332

2433
export interface PlayerActionOnChart {
@@ -28,13 +37,14 @@ export interface PlayerActionOnChart {
2837

2938
export interface PocketPick extends PlayerActionOnChart {
3039
pick: EligibleChart;
40+
targetType: typeof CHART_PLACEHOLDER | typeof CHART_DRAWN;
3141
}
3242

3343
export interface Drawing {
3444
id: string;
3545
title?: string;
3646
players: string[];
37-
charts: DrawnChart[];
47+
charts: Array<DrawnChart | PlayerPickPlaceholder>;
3848
bans: Array<PlayerActionOnChart>;
3949
protects: Array<PlayerActionOnChart>;
4050
winners: Array<PlayerActionOnChart>;

0 commit comments

Comments
 (0)