Basic game system editor
This commit is contained in:
@@ -12,12 +12,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/recoilize": "^0.8.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.24",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.12.0",
|
||||
"recoil": "^0.7.7",
|
||||
"recoilize": "^3.2.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"vite-plugin-svgr": "^3.2.0"
|
||||
},
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import { RecoilRoot } from 'recoil'
|
||||
import { SchemaBuilder } from './components/SchemaBuilder'
|
||||
import { GameSystemEditor } from './components/GameSystemEditor'
|
||||
import RecoilizeDebugger from 'recoilize';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<SchemaBuilder />
|
||||
<RecoilizeDebugger />
|
||||
{/* <SchemaBuilder /> */}
|
||||
<GameSystemEditor />
|
||||
</RecoilRoot>
|
||||
)
|
||||
}
|
||||
|
87
project-warstone/src/components/GameSystemEditor/index.tsx
Normal file
87
project-warstone/src/components/GameSystemEditor/index.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { GameSystem } from '../../types/gameSystem';
|
||||
import { useObjectState } from '../../hooks/useObjectState';
|
||||
import { GameSystemsService } from '../../services/game-systems';
|
||||
|
||||
export const GameSystemEditor: FC = () => {
|
||||
const { state: gameSystem, update: updateGameSystem, bindProperty: bindGameSystemProperty, setState: setGameSystem } = useObjectState<GameSystem>({
|
||||
id: '',
|
||||
schema: {
|
||||
id: '',
|
||||
name: '',
|
||||
templates: {},
|
||||
types: {}
|
||||
},
|
||||
schemaId: '286f4c18-d280-444b-8d7e-9a3dd09f64ef',
|
||||
name: 'Asshammer 40x a day',
|
||||
accolades: [],
|
||||
});
|
||||
const [schemas, setSchemas] = useState<{ name: string, id: string }[]>([]);
|
||||
|
||||
const [lastSaved, setLastSaved] = useState(gameSystem);
|
||||
|
||||
const fetchSchema = useCallback(async (id: string) => {
|
||||
const res = await GameSystemsService.getSchema(id);
|
||||
|
||||
if (res.status !== 200) return;
|
||||
const schema = await res.json()
|
||||
|
||||
updateGameSystem({
|
||||
schema
|
||||
});
|
||||
}, [updateGameSystem])
|
||||
|
||||
useEffect(() => {
|
||||
if (gameSystem.schemaId === gameSystem.schema.id) return;
|
||||
fetchSchema(gameSystem.schemaId);
|
||||
}, [fetchSchema, gameSystem.schema.id, gameSystem.schemaId]);
|
||||
|
||||
useEffect(() => {
|
||||
GameSystemsService.getSchemaList()
|
||||
.then(res => res.json())
|
||||
.then(schemas => setSchemas(schemas));
|
||||
}, []);
|
||||
|
||||
const saveGameSystem = useCallback(() => {
|
||||
GameSystemsService.saveGameSystem(gameSystem);
|
||||
setLastSaved(gameSystem);
|
||||
}, [gameSystem])
|
||||
|
||||
const fetchGameSystem = useCallback(async () => {
|
||||
const res = await GameSystemsService.getGameSystem('');
|
||||
const gs = await res.json();
|
||||
setGameSystem(gs);
|
||||
setLastSaved(gs);
|
||||
}, [setGameSystem]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGameSystem()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container p-8">
|
||||
<div className="flex gap-1">
|
||||
<input className="no-default text-white bg-transparent header border-b-2 border-falcon w-1/2" type="text" {...bindGameSystemProperty('name')} placeholder="Game Name" />
|
||||
<select className="no-default text-white bg-transparent header border-b-2 border-falcon" {...bindGameSystemProperty('schemaId')}>
|
||||
<option value=""></option>
|
||||
{schemas.map(s => (
|
||||
<option value={s.id}>{s.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button onClick={saveGameSystem} disabled={lastSaved === gameSystem}>Save Game System</button>
|
||||
</div>
|
||||
<div>
|
||||
<p>Accolades</p>
|
||||
<div className="grid grid-cols-3">
|
||||
{gameSystem.accolades.map(a => (
|
||||
<div>
|
||||
<p>{a.name}</p>
|
||||
<p>{a.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@@ -9,7 +9,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const FieldTypeInput: FCC<IProps> = ({ bind }) => {
|
||||
const Schema = useRecoilValue(SchemaEditAtom);
|
||||
const schema = useRecoilValue(SchemaEditAtom);
|
||||
|
||||
return (
|
||||
<label className="w-min">
|
||||
@@ -19,7 +19,7 @@ export const FieldTypeInput: FCC<IProps> = ({ bind }) => {
|
||||
{Object.keys(TEMPLATE_TYPES).map(k => (
|
||||
<option className="capitalize" value={k}>{k}</option>
|
||||
))}
|
||||
{Object.keys(Schema.types).map(k => (
|
||||
{Object.keys(schema.types).map(k => (
|
||||
<option className="capitalize" value={k}>{k}</option>
|
||||
))}
|
||||
</datalist>
|
||||
|
@@ -22,7 +22,7 @@ export const SchemaBuilder: FC = () => {
|
||||
const [lastSaved, setLastSaved] = useState(schema);
|
||||
|
||||
const fetchSchema = useCallback(async () => {
|
||||
const result = await GameSystemsService.getSchema('')
|
||||
const result = await GameSystemsService.getSchema('286f4c18-d280-444b-8d7e-9a3dd09f64ef')
|
||||
if (result.status !== 200) return;
|
||||
const fetchedSchema = await result.json();
|
||||
// if (fetchedSchema.name === schema.name) return;
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import { FC, useCallback } from 'react'
|
||||
import { Template } from '../../types/schema';
|
||||
import { useObjectStateWrapper } from '../../hooks/useObjectState';
|
||||
import { TEMPLATE_TYPES } from '../../constants/TemplateTypes';
|
||||
import { SchemaEditAtom } from '../../recoil/atoms/schema';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
interface IProps {
|
||||
templateKey: string;
|
||||
@@ -9,7 +12,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const TemplateEditor: FC<IProps> = ({ templateKey, update, template }) => {
|
||||
|
||||
const schema = useRecoilValue(SchemaEditAtom);
|
||||
const updateTemplate = useCallback((t: Template | ((arg: Template) => Template)) => {
|
||||
update(templateKey, typeof t === 'function' ? t(template) : t)
|
||||
}, [templateKey, update, template])
|
||||
@@ -22,7 +25,15 @@ export const TemplateEditor: FC<IProps> = ({ templateKey, update, template }) =>
|
||||
<div className="flex gap-4">
|
||||
<label className="w-min">
|
||||
Type
|
||||
<input type="text" {...bindProperty('type')} />
|
||||
<input type="text" {...bindProperty('type')} list="type-editor-type-list" />
|
||||
<datalist id="type-editor-type-list">
|
||||
{Object.keys(TEMPLATE_TYPES).map(k => (
|
||||
<option className="capitalize" value={k}>{k}</option>
|
||||
))}
|
||||
{Object.keys(schema.types).map(k => (
|
||||
<option className="capitalize" value={k}>{k}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" {...bindPropertyCheck('publishable')} />
|
||||
|
@@ -11,7 +11,16 @@
|
||||
|
||||
input,
|
||||
select {
|
||||
@apply p-1 rounded-lg text-cinder-500 interactive
|
||||
@apply p-1
|
||||
}
|
||||
|
||||
option {
|
||||
@apply text-cinder-500
|
||||
}
|
||||
|
||||
input:not(.no-default),
|
||||
select:not(.no-default) {
|
||||
@apply rounded-lg text-cinder-500 interactive
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -52,6 +61,7 @@
|
||||
animation: fade 300ms forwards ease-in;
|
||||
animation-delay: 300ms;
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
animation: fade 300ms forwards ease-in reverse;
|
||||
}
|
||||
@@ -63,6 +73,7 @@
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@@ -3,5 +3,5 @@ import { Schema } from '../../types/schema';
|
||||
|
||||
export const SchemaEditAtom = atom<Schema>({
|
||||
key: 'schema-edit',
|
||||
default: {name: '', types: {}, templates: {}}
|
||||
default: {name: '', types: {}, templates: {}, id: ''}
|
||||
});
|
@@ -1,15 +1,18 @@
|
||||
import { GameSystem } from '../types/gameSystem';
|
||||
import { Schema } from '../types/schema';
|
||||
|
||||
const emptySchema = { name: '', types: {}, templates: {}, id: crypto.randomUUID() };
|
||||
|
||||
export const GameSystemsService = {
|
||||
// todo - connect to service to save schema for game
|
||||
saveSchema: async (schema: Schema) => {
|
||||
localStorage.setItem('schema', JSON.stringify(schema));
|
||||
localStorage.setItem('schema ' + schema.id, JSON.stringify(schema));
|
||||
|
||||
return { status: 200 }
|
||||
},
|
||||
// todo - connect to service to fetch schema for game
|
||||
getSchema: async (id: string): Promise<{status: 200 | 404, json: () => Promise<Schema>}> => {
|
||||
const schema = localStorage.getItem('schema');
|
||||
getSchema: async (id: string): Promise<{ status: 200 | 404, json: () => Promise<Schema> }> => {
|
||||
const schema = localStorage.getItem('schema ' + id);
|
||||
|
||||
if (schema)
|
||||
return {
|
||||
@@ -19,7 +22,30 @@ export const GameSystemsService = {
|
||||
|
||||
return {
|
||||
status: 404,
|
||||
json: async () => ({name:'', types: {}, templates: {}})
|
||||
json: async () => (emptySchema)
|
||||
}
|
||||
},
|
||||
getSchemaList: async () => {
|
||||
return {
|
||||
status: 200, json: async () => [{
|
||||
name: 'Test Schema',
|
||||
id: '286f4c18-d280-444b-8d7e-9a3dd09f64ef'
|
||||
}]
|
||||
}
|
||||
},
|
||||
saveGameSystem: async (gs: GameSystem) => {
|
||||
localStorage.setItem('game-system ' + gs.id, JSON.stringify(gs));
|
||||
return { status: 200 }
|
||||
},
|
||||
getGameSystem: async (id: string): Promise<{ status: 200 | 404, json: () => Promise<GameSystem> }> => {
|
||||
const gs = localStorage.getItem('game-system ' + id);
|
||||
if (!gs) return {status: 404, json:async () => ({
|
||||
accolades: [],
|
||||
name: '',
|
||||
id: crypto.randomUUID(),
|
||||
schema: emptySchema,
|
||||
schemaId: ''
|
||||
})}
|
||||
return { status: 200, json:async () => (JSON.parse(gs))}
|
||||
}
|
||||
}
|
15
project-warstone/src/types/gameSystem.ts
Normal file
15
project-warstone/src/types/gameSystem.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Schema } from './schema';
|
||||
|
||||
export type Accolade = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type GameSystem = {
|
||||
id: string;
|
||||
schemaId: string;
|
||||
schema: Schema;
|
||||
name: string;
|
||||
accolades: Accolade[];
|
||||
}
|
@@ -17,6 +17,7 @@ export type Template = {
|
||||
}
|
||||
|
||||
export type Schema = {
|
||||
id: string;
|
||||
name: string;
|
||||
templates: Record<string, Template>;
|
||||
types: Record<string, TypeType>;
|
||||
|
@@ -630,6 +630,11 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/recoilize@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/recoilize/-/recoilize-0.8.0.tgz#7351f7a048bcef9c712f39d05696ce7fd2a414fe"
|
||||
integrity sha512-h4Rm8J/8XH1wZIZC5gOeN9EDJWBhK2sLEs5voSHyZKB5cynIDbohuYS6rEdv2kNPrwoYQyt7D3yeXI9ODvZDlw==
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||
@@ -1825,6 +1830,11 @@ recoil@^0.7.7:
|
||||
dependencies:
|
||||
hamt_plus "1.0.2"
|
||||
|
||||
recoilize@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/recoilize/-/recoilize-3.2.0.tgz#aaf4c76390293a105a73b74dced7a49dfbeb1c84"
|
||||
integrity sha512-2J0SEzefl8rKe5iaJ2vVg6AgWy5aQ8NFPfZbZfXagycF5ohUzCQA2c2SZDz5jtZfYmendR8hsILstPyofxfg7g==
|
||||
|
||||
resolve-from@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||
|
Reference in New Issue
Block a user