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'
36import { GameRenderer } from './playback/GameRenderer'
47import { 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
618export 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
2752const 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
4268export 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+
69231const 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