This commit is contained in:
2026-06-26 20:32:33 -04:00
commit 48bf9edb65
146 changed files with 19805 additions and 0 deletions
@@ -0,0 +1,275 @@
import { useContext } from "react";
import {
Grid,
TextField,
MenuItem,
FormControl,
FormLabel,
FormGroup,
FormControlLabel,
Checkbox,
} from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import {
ConwayConfig,
GameContext,
Preset,
RulesetName,
} from "../../../../internal";
import { presetDisplayNameMap } from "./const";
import { ConwayColorProp } from "./types";
import { useGameSpecificOptions } from "../hooks";
const conwayColorProps = [
"liveColor",
"deadColor",
"startingEnvelopeColor",
"finalEnvelopeColor",
] as ConwayColorProp[];
const ConwayControls = () => {
const { grid } = useContext(GameContext);
const { config, handleUpdate } = useGameSpecificOptions<ConwayConfig>(
RulesetName.CONWAY
);
const neighborNumbers =
grid.config.cellShape === "hex"
? [0, 1, 2, 3, 4, 5, 6]
: [0, 1, 2, 3, 4, 5, 6, 7, 8];
return (
<>
<Grid item xs={12}>
<TextField
fullWidth
label="Preset"
onChange={(e) => {
handleUpdate({ ...config, preset: e.target.value as Preset });
}}
select
value={config.preset}
>
{Object.keys(Preset).map((preset) => (
<MenuItem key={preset} value={Preset[preset]}>
{presetDisplayNameMap[Preset[preset]]}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="Average % Alive at Start"
type="number"
onChange={(e) =>
handleUpdate({
...config,
liveStartPercent: parseInt(e.target.value),
})
}
value={config.liveStartPercent}
/>
</Grid>
<Grid item xs={12}>
<FormControl component="fieldset" variant="standard">
<FormLabel component="legend">
Live Neighbors Required (Survival)
</FormLabel>
<FormGroup row>
{neighborNumbers.map((num) => (
<FormControlLabel
control={
<Checkbox
checked={config.neighborsNeededToSurvive.includes(num)}
onChange={(e) => {
let newNeighborsNeededToSurvive = [
...config.neighborsNeededToSurvive,
];
if (e.target.checked) {
newNeighborsNeededToSurvive.push(num);
} else {
newNeighborsNeededToSurvive =
newNeighborsNeededToSurvive.filter(
(val) => val !== num
);
}
handleUpdate({
...config,
neighborsNeededToSurvive: newNeighborsNeededToSurvive,
});
}}
name={num.toString()}
/>
}
label={num.toString()}
key={`conway-survival-${num}`}
/>
))}
</FormGroup>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl component="fieldset" variant="standard">
<FormLabel component="legend">
Live Neighbors Required (Reproduction)
</FormLabel>
<FormGroup row>
{neighborNumbers.map((num) => (
<FormControlLabel
control={
<Checkbox
checked={config.neighborsNeededToReproduce.includes(num)}
onChange={(e) => {
let newNeighborsNeededToReproduce = [
...config.neighborsNeededToReproduce,
];
if (e.target.checked) {
newNeighborsNeededToReproduce.push(num);
} else {
newNeighborsNeededToReproduce =
newNeighborsNeededToReproduce.filter(
(val) => val !== num
);
}
handleUpdate({
...config,
neighborsNeededToReproduce:
newNeighborsNeededToReproduce,
});
}}
name={num.toString()}
/>
}
label={num.toString()}
key={`conway-reproduction-${num}`}
/>
))}
</FormGroup>
</FormControl>
</Grid>
<Grid item md={6} xs={12}>
<FormGroup row>
<FormControlLabel
control={
<Checkbox
checked={config.showEnvelope}
onChange={(e) => {
handleUpdate({
...config,
showEnvelope: e.target.checked,
});
}}
name={"show-envelope-checkbox"}
/>
}
label={"Show Envelope"}
/>
{config.showEnvelope && (
<FormControlLabel
control={
<Checkbox
checked={config.showEnvelopeGradient}
onChange={(e) => {
handleUpdate({
...config,
showEnvelopeGradient: e.target.checked,
});
}}
name={"envelope-gradient-checkbox"}
/>
}
label={"Envelope Gradient"}
/>
)}
</FormGroup>
</Grid>
<Grid item md={6} xs={12}>
{config.showEnvelope && config.showEnvelopeGradient && (
<TextField
fullWidth
label="Envelope Gradient Steps"
type="number"
onChange={(e) =>
handleUpdate({
...config,
envelopeGradientSteps: parseInt(e.target.value),
})
}
value={config.envelopeGradientSteps}
/>
)}
</Grid>
<Grid container item xs={12} spacing={2}>
{conwayColorProps.map((color: ConwayColorProp) => {
if (
(!config.showEnvelope && color === "startingEnvelopeColor") ||
((!config.showEnvelope || !config.showEnvelopeGradient) &&
color === "finalEnvelopeColor")
)
return null;
let label = "Alive Color";
if (color === "deadColor") {
label = "Dead Color";
} else if (color === "startingEnvelopeColor") {
label = config.showEnvelopeGradient
? "Initial Envelope Color"
: "Envelope Color";
} else if (color === "finalEnvelopeColor") {
label = "Final Envelope Color";
}
return (
<Grid item key={`conway-color-config-${color}`}>
<MuiColorInput
format="hex"
label={label}
value={config[color]}
onChange={(newColor) =>
handleUpdate({
...config,
[color]: newColor,
})
}
/>
</Grid>
);
})}
</Grid>
<Grid item md={6} xs={12}>
<FormControlLabel
control={
<Checkbox
checked={config.mortalCells}
onChange={(e) => {
handleUpdate({
...config,
mortalCells: e.target.checked,
});
}}
name={"mortal-cells-checkbox"}
/>
}
label={"Mortal Cells (Cells Die After X Ticks)"}
/>
</Grid>
<Grid item md={6} xs={12}>
{config.mortalCells && (
<TextField
fullWidth
label="Cell Lifespan"
type="number"
onChange={(e) =>
handleUpdate({
...config,
cellLifespan: parseInt(e.target.value),
})
}
value={config.cellLifespan}
/>
)}
</Grid>
</>
);
};
export default ConwayControls;
@@ -0,0 +1,5 @@
import { Preset } from "../../../../internal";
export const presetDisplayNameMap = {
[Preset.DEFAULT]: "Random Distribution",
};
@@ -0,0 +1,3 @@
import ConwayControls from "./ConwayControls";
export { ConwayControls };
@@ -0,0 +1,5 @@
export type ConwayColorProp =
| "liveColor"
| "deadColor"
| "startingEnvelopeColor"
| "finalEnvelopeColor";
@@ -0,0 +1,72 @@
import { Grid, Typography, Button } from "@mui/material";
import {
defaultGameOptions,
GameContext,
RulesetName,
} from "../../../internal";
import { ConwayControls } from "./ConwayControls";
import { PokemonControls } from "./PokemonControls";
import { RockPaperScissorsControls } from "./RockPaperScissorsControls";
import { MazeControls } from "./MazeControls";
import { SnowflakeControls } from "./SnowflakeControls";
import { WarControls } from "./WarControls.ts";
import { WaterFlowControls } from "./WaterFlowControls";
import { useContext, useMemo } from "react";
import { ControlsContext } from "../context";
const GameSpecificControls = () => {
const { currentGame } = useContext(GameContext);
const { newGameSpecificConfigs, setNewGameSpecificConfigs } =
useContext(ControlsContext);
const Component = useMemo(() => {
switch (currentGame) {
case RulesetName.CONWAY:
return () => <ConwayControls />;
case RulesetName.MAZE_GENERATOR:
return () => <MazeControls />;
case RulesetName.POKEMON:
return () => <PokemonControls />;
case RulesetName.ROCK_PAPER_SCISSORS:
return () => <RockPaperScissorsControls />;
case RulesetName.SNOWFLAKE:
return () => <SnowflakeControls />;
case RulesetName.WAR:
return () => <WarControls />;
case RulesetName.WATER_FLOW:
return () => <WaterFlowControls />;
default:
return null;
}
}, [currentGame]);
return (
<>
{Component === null ? (
<Grid item xs={12}>
<Typography>
No settings have been developed for this game yet.
</Typography>
</Grid>
) : (
<Component />
)}
{Component !== null && (
<Grid item xs={12}>
<Button
onClick={() => {
setNewGameSpecificConfigs({
...newGameSpecificConfigs,
[currentGame]: defaultGameOptions[currentGame],
});
}}
>
Reset to Default
</Button>
</Grid>
)}
</>
);
};
export default GameSpecificControls;
@@ -0,0 +1,43 @@
import { Grid } from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import { MazeConfig, RulesetName } from "../../../../internal";
import { MazeColorProp } from "./types";
import { useGameSpecificOptions } from "../hooks";
const mazeColorProps = ["liveColor", "deadColor"] as MazeColorProp[];
const MazeControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<MazeConfig>(
RulesetName.MAZE_GENERATOR
);
return (
<>
<Grid container item xs={12} spacing={2}>
{mazeColorProps.map((color: MazeColorProp) => {
let label = "Wall Color";
if (color === "deadColor") {
label = "Background Color";
}
return (
<Grid item key={`maze-color-config-${color}`}>
<MuiColorInput
format="hex"
label={label}
value={config[color]}
onChange={(newColor) =>
handleUpdate({
...config,
[color]: newColor,
})
}
/>
</Grid>
);
})}
</Grid>
</>
);
};
export default MazeControls;
@@ -0,0 +1,3 @@
import MazeControls from "./MazeControls";
export { MazeControls };
@@ -0,0 +1 @@
export type MazeColorProp = "liveColor" | "deadColor";
@@ -0,0 +1,136 @@
import { useState } from "react";
import {
Grid,
TextField,
FormControlLabel,
Checkbox,
FormGroup,
FormControl,
FormLabel,
MenuItem,
} from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import {
PokemonGameConfig,
PokemonType,
RulesetName,
} from "../../../../internal";
import { useGameSpecificOptions } from "../hooks";
const PokemonControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<PokemonGameConfig>(
RulesetName.POKEMON
);
const [typeToChangeColor, setTypeToChangeColor] = useState<PokemonType>(
PokemonType.BUG
);
const { allowedTypes, typeColors } = config;
const allTypesAllowed =
allowedTypes.length === Object.keys(PokemonType).length;
return (
<>
<Grid item xs={12}>
<TextField
fullWidth
label="Mutation Odds (1 in X)"
type="number"
onChange={(e) =>
handleUpdate({
...config,
randomMutationChance: parseInt(e.target.value),
})
}
value={config.randomMutationChance}
/>
</Grid>
<Grid container item xs={12}>
<FormControl>
<FormLabel>Enable / Disable Pokemon Types</FormLabel>
<FormGroup row>
<FormControlLabel
control={
<Checkbox
checked={allTypesAllowed}
onChange={() => {
let newTypes = Object.keys(PokemonType).map(
(key) => PokemonType[key]
);
handleUpdate({
...config,
allowedTypes: allTypesAllowed ? [] : newTypes,
});
}}
name={`enable-type-select-all-checkbox`}
value={!allTypesAllowed}
/>
}
label={`${allTypesAllowed ? "Deselect" : "Select"} All`}
/>
{Object.keys(PokemonType).map((key) => {
const type = PokemonType[key];
return (
<FormControlLabel
control={
<Checkbox
checked={allowedTypes.includes(type)}
onChange={(e) => {
let newTypes = [...allowedTypes];
if (e.target.checked) {
newTypes.push(type);
} else {
newTypes = newTypes.filter(
(newType) => newType !== type
);
}
handleUpdate({
...config,
allowedTypes: newTypes,
});
}}
name={`enable-type-${type}-checkbox`}
value={allowedTypes.includes(type)}
/>
}
key={`pokemon-type-form-${key}`}
label={type.toString()}
/>
);
})}
</FormGroup>
</FormControl>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Change Color of Type"
onChange={(e) => {
setTypeToChangeColor(e.target.value as PokemonType);
}}
select
value={typeToChangeColor}
>
{Object.keys(PokemonType).map((key) => (
<MenuItem key={key} value={PokemonType[key]}>
{PokemonType[key]}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item md={6} xs={12}>
<MuiColorInput
format="hex"
label={`${typeToChangeColor} Color`}
value={typeColors[typeToChangeColor]}
onChange={(newColor) =>
handleUpdate({
...config,
typeColors: { ...typeColors, [typeToChangeColor]: newColor },
})
}
/>
</Grid>
</>
);
};
export default PokemonControls;
@@ -0,0 +1,3 @@
import PokemonControls from "./PokemonControls";
export { PokemonControls };
@@ -0,0 +1,34 @@
import { Grid } from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import { RPSGameConfig, RulesetName } from "../../../../internal";
import { useGameSpecificOptions } from "../hooks";
const RockPaperScissorsControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<RPSGameConfig>(
RulesetName.ROCK_PAPER_SCISSORS
);
return (
<>
{["rockColor", "paperColor", "scissorsColor"].map(
(color: keyof RPSGameConfig, i) => (
<Grid item xs={12} key={`rpg-color-config-${color}`}>
<MuiColorInput
format="hex"
label={`Color ${i + 1}`}
value={config[color]}
onChange={(newColor) =>
handleUpdate({
...config,
[color]: newColor,
})
}
/>
</Grid>
)
)}
</>
);
};
export default RockPaperScissorsControls;
@@ -0,0 +1,3 @@
import RockPaperScissorsControls from "./RockPaperScissorsControls";
export { RockPaperScissorsControls };
@@ -0,0 +1,43 @@
import { Grid } from "@mui/material";
import { MuiColorInput } from "mui-color-input";
import { SnowflakeGameConfig, RulesetName } from "../../../../internal";
import { SnowflakeColorProp } from "./types";
import { useGameSpecificOptions } from "../hooks";
const mazeColorProps = ["liveColor", "deadColor"] as SnowflakeColorProp[];
const SnowflakeControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<SnowflakeGameConfig>(
RulesetName.SNOWFLAKE
);
return (
<>
<Grid container item xs={12} spacing={2}>
{mazeColorProps.map((color: SnowflakeColorProp) => {
let label = "Alive Color";
if (color === "deadColor") {
label = "Dead Color";
}
return (
<Grid item key={`snowflake-color-config-${color}`}>
<MuiColorInput
format="hex"
label={label}
value={config[color]}
onChange={(newColor) =>
handleUpdate({
...config,
[color]: newColor,
})
}
/>
</Grid>
);
})}
</Grid>
</>
);
};
export default SnowflakeControls;
@@ -0,0 +1,3 @@
import SnowflakeControls from "./SnowflakeControls";
export { SnowflakeControls };
@@ -0,0 +1 @@
export type SnowflakeColorProp = "liveColor" | "deadColor";
@@ -0,0 +1,68 @@
import { Grid, IconButton, Typography } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
import { MuiColorInput } from "mui-color-input";
import {
RulesetName,
WarGameConfig,
getRandomColor,
} from "../../../../internal";
import { useState } from "react";
import { useGameSpecificOptions } from "../hooks";
const WarControls = () => {
const { config, handleUpdate } = useGameSpecificOptions<WarGameConfig>(
RulesetName.WAR
);
const [newArmyColor, setNewArmyColor] = useState<string>(getRandomColor());
const { factionColors } = config;
return (
<>
<Grid container item xs={12} spacing={2}>
{factionColors.map((color, i) => {
return (
<Grid item key={`army-${color}`} alignItems="center">
<Typography sx={{ color, display: "inline" }}>
Army {i + 1}
</Typography>
<IconButton
onClick={() => {
const newFactions = factionColors.filter(
(existingColor) => existingColor !== color
);
handleUpdate({ factionColors: newFactions });
}}
>
<CloseIcon />
</IconButton>
</Grid>
);
})}
</Grid>
<Grid container item xs={12} alignItems="center">
<Grid item>
<MuiColorInput
format="hex"
label={"New Army Color"}
value={newArmyColor}
onChange={(newColor) => setNewArmyColor(newColor)}
/>
</Grid>
<Grid item>
<IconButton
onClick={() => {
const newFactions = [...factionColors, newArmyColor];
handleUpdate({ factionColors: newFactions });
setNewArmyColor(getRandomColor());
}}
>
<AddIcon />
</IconButton>
</Grid>
</Grid>
</>
);
};
export default WarControls;
@@ -0,0 +1,3 @@
import WarControls from "./WarControls";
export { WarControls };
@@ -0,0 +1,182 @@
import { useState } from "react";
import {
Checkbox,
FormControlLabel,
Grid,
IconButton,
TextField,
Typography,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import AddIcon from "@mui/icons-material/Add";
import { MuiColorInput } from "mui-color-input";
import {
RulesetName,
WaterFlowGameConfig,
getRandomColor,
} from "../../../../internal";
import { useGameSpecificOptions } from "../hooks";
const WaterFlowControls = () => {
const [newWaterColor, setNewWaterColor] = useState(getRandomColor());
const { config, handleUpdate } = useGameSpecificOptions<WaterFlowGameConfig>(
RulesetName.WATER_FLOW
);
const { waterColors } = config;
return (
<>
<Grid container item xs={12} spacing={2}>
<Grid item>
<TextField
fullWidth
label="Average Number of Starting Streams"
type="number"
onChange={(e) =>
handleUpdate({
...config,
averageStartingWaterCells: parseInt(e.target.value),
})
}
value={config.averageStartingWaterCells}
/>
</Grid>
<Grid item>
<TextField
fullWidth
label="Branching-Off Chance"
type="number"
onChange={(e) =>
handleUpdate({
...config,
branchingChance: parseInt(e.target.value),
})
}
value={config.branchingChance}
/>
</Grid>
<Grid item>
<TextField
fullWidth
label="Rock Health Variation"
type="number"
onChange={(e) =>
handleUpdate({
...config,
rockHealthVariance: parseInt(e.target.value),
})
}
value={config.rockHealthVariance}
/>
</Grid>
<Grid item>
<FormControlLabel
control={
<Checkbox
checked={config.trueRandom}
onChange={(e) => {
handleUpdate({
...config,
trueRandom: e.target.checked,
});
}}
name={"true-random-checkbox"}
value={config.trueRandom}
/>
}
label={"Random Flow Direction"}
/>
</Grid>
</Grid>
<Grid container item xs={12} spacing={2} alignItems="center">
<Grid item>
<FormControlLabel
control={
<Checkbox
checked={config.blurBackground}
onChange={(e) => {
handleUpdate({
...config,
blurBackground: e.target.checked,
});
}}
name={"blur-background-checkbox"}
/>
}
label={"Blur Background"}
/>
</Grid>
<Grid item>
{config.blurBackground && (
<TextField
fullWidth
label="Blur Amount"
type="number"
onChange={(e) =>
handleUpdate({
...config,
backgroundBlurAmount: parseInt(e.target.value),
})
}
value={config.backgroundBlurAmount}
/>
)}
</Grid>
</Grid>
<Grid container item xs={12} spacing={2}>
{waterColors.map((color, i) => {
return (
<Grid item key={`water-color-${color}`} alignItems="center">
<Typography sx={{ color, display: "inline" }}>
Color {i + 1}
</Typography>
<IconButton
onClick={() => {
const newWaterColors = waterColors.filter(
(existingColor) => existingColor !== color
);
handleUpdate({ ...config, waterColors: newWaterColors });
}}
>
<CloseIcon />
</IconButton>
</Grid>
);
})}
</Grid>
<Grid container item xs={12} spacing={2} alignItems="center">
<Grid item>
<MuiColorInput
format="hex"
label={"New Water Color"}
value={newWaterColor}
onChange={(newColor) => setNewWaterColor(newColor)}
/>
</Grid>
<Grid item>
<IconButton
onClick={() => {
const newWaterColors = [...waterColors, newWaterColor];
handleUpdate({ ...config, waterColors: newWaterColors });
setNewWaterColor(getRandomColor());
}}
>
<AddIcon />
</IconButton>
</Grid>
<Grid item>
<MuiColorInput
format="hex"
label={"Rock Color"}
value={config.baseBackgroundColor}
onChange={(newColor) =>
handleUpdate({ ...config, baseBackgroundColor: newColor })
}
/>
</Grid>
</Grid>
</>
);
};
export default WaterFlowControls;
@@ -0,0 +1,3 @@
import WaterFlowControls from "./WaterFlowControls";
export { WaterFlowControls };
@@ -0,0 +1 @@
export { default as useGameSpecificOptions } from "./useGameSpecificOptions";
@@ -0,0 +1,25 @@
import { useCallback, useContext, useMemo } from "react";
import { ControlsContext } from "../../context";
import { RulesetName } from "../../../../internal";
const useGameSpecificOptions = <T>(rulesetName: RulesetName) => {
const { newGameSpecificConfigs, setNewGameSpecificConfigs } =
useContext(ControlsContext);
const config = newGameSpecificConfigs[rulesetName] as T;
const handleUpdate = useCallback(
(newConfig: T) => {
setNewGameSpecificConfigs({
...newGameSpecificConfigs,
[rulesetName]: newConfig,
});
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[newGameSpecificConfigs, setNewGameSpecificConfigs]
);
return useMemo(() => ({ config, handleUpdate }), [config, handleUpdate]);
};
export default useGameSpecificOptions;
@@ -0,0 +1,4 @@
import GameSpecificControls from "./GameSpecificControls";
export * from "./types";
export { GameSpecificControls };
@@ -0,0 +1,8 @@
import { GameSpecificConfig, RulesetName } from "../../../internal";
export type UpdateConfigFn = (
prop: RulesetName,
value: GameSpecificConfig
) => void;
export interface GameSpecificControlsProps {}