Skip to content

Commit dd126f1

Browse files
Merge branch 'master' into public-release
2 parents 676049a + 3126c2e commit dd126f1

File tree

17 files changed

+363
-87
lines changed

17 files changed

+363
-87
lines changed

client/package-lock.json

Lines changed: 69 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
"path": "^0.12.7",
142142
"process": "^0.11.10",
143143
"react": "^18.2.0",
144+
"react-color": "^2.19.3",
144145
"react-custom-scrollbars-2": "^4.5.0",
145146
"react-dom": "^18.2.0",
146147
"react-icons": "^4.8.0",
@@ -154,6 +155,7 @@
154155
"@tauri-apps/cli": "1.6.3",
155156
"@types/pako": "^2.0.0",
156157
"@types/react": "^18.2.5",
158+
"@types/react-color": "^3.0.12",
157159
"@types/react-dom": "^18.2.3",
158160
"@types/react-window": "^1.8.8",
159161
"concurrently": "^8.2.2",

client/src/app.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import React from 'react'
22
import ReactDom from 'react-dom/client'
33
import { MainPage } from './pages/main-page'
44
import { BrowserRouter } from 'react-router-dom'
5+
import { AppContextProvider } from './app-context'
56

67
import '../style.css'
78

89
const elem = document.getElementById('root')
910
const root = ReactDom.createRoot(elem!)
10-
root.render(<BrowserRouter><MainPage /></BrowserRouter>)
11+
root.render(<BrowserRouter><AppContextProvider><MainPage /></AppContextProvider></BrowserRouter>)

client/src/client-config.tsx

Lines changed: 168 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
import React, { useEffect } from 'react'
2-
import { useAppContext } from './app-context'
1+
import React, { useEffect, useState, MouseEvent, PropsWithChildren, useRef } from 'react'
2+
import { IconContext } from 'react-icons'
3+
import { IoCloseCircle, IoCloseCircleOutline } from 'react-icons/io5'
4+
import { ChromePicker } from 'react-color'
5+
import { AppContextProvider, useAppContext } from './app-context'
36
import { GameRenderer } from './playback/GameRenderer'
47
import { NumInput } from './components/forms'
8+
import {
9+
Colors,
10+
currentColors,
11+
updateGlobalColor,
12+
getGlobalColor,
13+
resetGlobalColors,
14+
DEFAULT_GLOBAL_COLORS
15+
} from './colors'
16+
import { BrightButton, Button } from './components/button'
517

618
export type ClientConfig = typeof DEFAULT_CONFIG
719

@@ -21,7 +33,20 @@ const DEFAULT_CONFIG = {
2133
streamRunnerGames: true,
2234
profileGames: false,
2335
validateMaps: false,
24-
resolutionScale: 100
36+
resolutionScale: 100,
37+
colors: {
38+
[Colors.TEAM_ONE]: '#cdcdcc',
39+
[Colors.TEAM_TWO]: '#fee493',
40+
41+
[Colors.PAINT_TEAMONE_ONE]: '#666666',
42+
[Colors.PAINT_TEAMONE_TWO]: '#565656',
43+
[Colors.PAINT_TEAMTWO_ONE]: '#b28b52',
44+
[Colors.PAINT_TEAMTWO_TWO]: '#997746',
45+
[Colors.WALLS_COLOR]: '#547f31',
46+
[Colors.TILE_COLOR]: '#4c301e',
47+
[Colors.GAMEAREA_BACKGROUND]: '#2e2323',
48+
[Colors.SIDEBAR_BACKGROUND]: '#3f3131'
49+
} as Record<Colors, string>
2550
}
2651

2752
const configDescription: Record<keyof ClientConfig, string> = {
@@ -36,7 +61,8 @@ const configDescription: Record<keyof ClientConfig, string> = {
3661
streamRunnerGames: 'Stream each round from the runner live as the game is being played',
3762
profileGames: 'Enable saving profiling data when running games',
3863
validateMaps: 'Validate maps before running a game',
39-
resolutionScale: 'Resolution scale for the game area. Decrease to help performance.'
64+
resolutionScale: 'Resolution scale for the game area. Decrease to help performance.',
65+
colors: ''
4066
}
4167

4268
export function getDefaultConfig(): ClientConfig {
@@ -47,6 +73,15 @@ export function getDefaultConfig(): ClientConfig {
4773
;(config[key as keyof ClientConfig] as any) = JSON.parse(value)
4874
}
4975
}
76+
77+
for (const key in config.colors) {
78+
const value = localStorage.getItem('config-colors' + key)
79+
if (value) {
80+
config.colors[key as Colors] = JSON.parse(value)
81+
updateGlobalColor(key as Colors, JSON.parse(value))
82+
}
83+
}
84+
5085
return config
5186
}
5287

@@ -62,22 +97,149 @@ export const ConfigPage: React.FC<Props> = (props) => {
6297
if (typeof v === 'boolean') return <ConfigBooleanElement configKey={key} key={key} />
6398
if (typeof v === 'number') return <ConfigNumberElement configKey={key} key={key} />
6499
})}
100+
101+
<ColorConfig />
65102
</div>
66103
)
67104
}
68105

106+
const ColorConfig = () => {
107+
const context = useAppContext()
108+
109+
/* TODO: [future] do this dynamically rather than hardcoding sections */
110+
111+
return (
112+
<>
113+
<div className="m-0 mt-4">
114+
Customize Colors:
115+
<div className="text-sm pb-1 pt-1">Interface</div>
116+
<SingleColorPicker displayName={'Background'} colorName={Colors.GAMEAREA_BACKGROUND} />
117+
<SingleColorPicker displayName={'Sidebar'} colorName={Colors.SIDEBAR_BACKGROUND} />
118+
<div className="text-sm pb-1">General</div>
119+
<SingleColorPicker displayName={'Walls'} colorName={Colors.WALLS_COLOR} />
120+
<SingleColorPicker displayName={'Tiles'} colorName={Colors.TILE_COLOR} />
121+
<div className="text-sm pb-1">Silver</div>
122+
<SingleColorPicker displayName={'Text'} colorName={Colors.TEAM_ONE} />
123+
<SingleColorPicker displayName={'Primary Paint'} colorName={Colors.PAINT_TEAMONE_ONE} />
124+
<SingleColorPicker displayName={'Secondary Paint'} colorName={Colors.PAINT_TEAMONE_TWO} />
125+
<div className="text-sm pb-1">Gold</div>
126+
<SingleColorPicker displayName={'Text'} colorName={Colors.TEAM_TWO} />
127+
<SingleColorPicker displayName={'Primary Paint'} colorName={Colors.PAINT_TEAMTWO_ONE} />
128+
<SingleColorPicker displayName={'Secondary Paint'} colorName={Colors.PAINT_TEAMTWO_TWO} />
129+
</div>
130+
<div className="flex flex-row mt-8">
131+
<BrightButton
132+
className=""
133+
onClick={() => {
134+
resetGlobalColors()
135+
136+
context.setState((prevState) => ({
137+
...prevState,
138+
config: { ...prevState.config, colors: { ...DEFAULT_GLOBAL_COLORS } }
139+
}))
140+
}}
141+
>
142+
Reset Colors
143+
</BrightButton>
144+
</div>
145+
</>
146+
)
147+
}
148+
149+
const SingleColorPicker = (props: { displayName: string; colorName: Colors }) => {
150+
const context = useAppContext()
151+
const value = context.state.config.colors[props.colorName]
152+
const ref = useRef<HTMLDivElement>(null)
153+
const buttonRef = useRef<HTMLButtonElement>(null)
154+
const [hoveredClose, setHoveredClose] = useState(false)
155+
156+
const [displayColorPicker, setDisplayColorPicker] = useState(false)
157+
158+
const handleClick = () => {
159+
setDisplayColorPicker(!displayColorPicker)
160+
}
161+
162+
const handleClose = () => {
163+
setDisplayColorPicker(false)
164+
}
165+
166+
const handleClickOutside = (event: any) => {
167+
if (
168+
ref.current &&
169+
buttonRef.current &&
170+
!ref.current.contains(event.target) &&
171+
!buttonRef.current.contains(event.target)
172+
) {
173+
handleClose()
174+
}
175+
}
176+
177+
const onChange = (newColor: any) => {
178+
updateGlobalColor(props.colorName, newColor.hex)
179+
context.setState((prevState) => ({
180+
...prevState,
181+
config: { ...prevState.config, colors: { ...prevState.config.colors, [props.colorName]: newColor.hex } }
182+
}))
183+
// hopefully after the setState is done
184+
setTimeout(() => GameRenderer.render(), 10)
185+
}
186+
187+
const resetColor = () => {
188+
onChange({ hex: DEFAULT_GLOBAL_COLORS[props.colorName as Colors] })
189+
}
190+
191+
useEffect(() => {
192+
window.addEventListener('mousedown', handleClickOutside)
193+
194+
return () => window.removeEventListener('mousedown', handleClickOutside)
195+
}, [])
196+
197+
return (
198+
<>
199+
<div className={'ml-2 mb-2 text-xs flex flex-start justify-start items-center'}>
200+
{/*Background:*/}
201+
{props.displayName}:
202+
<button
203+
ref={buttonRef}
204+
className={'text-xs ml-2 px-4 py-3 mr-2 flex flex-row hover:bg-cyanDark rounded-md text-white'}
205+
style={{ backgroundColor: value, border: '2px solid white' }}
206+
onClick={handleClick}
207+
></button>
208+
<div
209+
className="rounded-full overflow-clip"
210+
onClick={() => resetColor()}
211+
onMouseEnter={() => setHoveredClose(true)}
212+
onMouseLeave={() => setHoveredClose(false)}
213+
>
214+
<IconContext.Provider
215+
value={{
216+
color: 'white',
217+
className: 'w-5 h-5'
218+
}}
219+
>
220+
{hoveredClose ? <IoCloseCircle /> : <IoCloseCircleOutline />}
221+
</IconContext.Provider>
222+
</div>
223+
</div>
224+
<div ref={ref} className={'width: w-min'}>
225+
{displayColorPicker && <ChromePicker color={value} onChange={onChange} />}
226+
</div>
227+
</>
228+
)
229+
}
230+
69231
const ConfigBooleanElement: React.FC<{ configKey: keyof ClientConfig }> = ({ configKey }) => {
70232
const context = useAppContext()
71233
const value = context.state.config[configKey] as boolean
72234
return (
73235
<div className={'flex flex-row items-center mb-2'}>
74236
<input
75237
type={'checkbox'}
76-
checked={value}
238+
checked={value as any}
77239
onChange={(e) => {
78240
context.setState((prevState) => ({
79241
...prevState,
80-
config: { ...context.state.config, [configKey]: e.target.checked }
242+
config: { ...prevState.config, [configKey]: e.target.checked }
81243
}))
82244
localStorage.setItem('config' + configKey, JSON.stringify(e.target.checked))
83245
// hopefully after the setState is done

0 commit comments

Comments
 (0)