Basic game system editor
This commit is contained in:
@@ -12,12 +12,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"@types/recoilize": "^0.8.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.12.0",
|
"react-router-dom": "^6.12.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
|
"recoilize": "^3.2.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"vite-plugin-svgr": "^3.2.0"
|
"vite-plugin-svgr": "^3.2.0"
|
||||||
},
|
},
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
import { RecoilRoot } from 'recoil'
|
import { RecoilRoot } from 'recoil'
|
||||||
import { SchemaBuilder } from './components/SchemaBuilder'
|
import { SchemaBuilder } from './components/SchemaBuilder'
|
||||||
|
import { GameSystemEditor } from './components/GameSystemEditor'
|
||||||
|
import RecoilizeDebugger from 'recoilize';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<SchemaBuilder />
|
<RecoilizeDebugger />
|
||||||
|
{/* <SchemaBuilder /> */}
|
||||||
|
<GameSystemEditor />
|
||||||
</RecoilRoot>
|
</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 }) => {
|
export const FieldTypeInput: FCC<IProps> = ({ bind }) => {
|
||||||
const Schema = useRecoilValue(SchemaEditAtom);
|
const schema = useRecoilValue(SchemaEditAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="w-min">
|
<label className="w-min">
|
||||||
@@ -19,7 +19,7 @@ export const FieldTypeInput: FCC<IProps> = ({ bind }) => {
|
|||||||
{Object.keys(TEMPLATE_TYPES).map(k => (
|
{Object.keys(TEMPLATE_TYPES).map(k => (
|
||||||
<option className="capitalize" value={k}>{k}</option>
|
<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>
|
<option className="capitalize" value={k}>{k}</option>
|
||||||
))}
|
))}
|
||||||
</datalist>
|
</datalist>
|
||||||
|
@@ -22,7 +22,7 @@ export const SchemaBuilder: FC = () => {
|
|||||||
const [lastSaved, setLastSaved] = useState(schema);
|
const [lastSaved, setLastSaved] = useState(schema);
|
||||||
|
|
||||||
const fetchSchema = useCallback(async () => {
|
const fetchSchema = useCallback(async () => {
|
||||||
const result = await GameSystemsService.getSchema('')
|
const result = await GameSystemsService.getSchema('286f4c18-d280-444b-8d7e-9a3dd09f64ef')
|
||||||
if (result.status !== 200) return;
|
if (result.status !== 200) return;
|
||||||
const fetchedSchema = await result.json();
|
const fetchedSchema = await result.json();
|
||||||
// if (fetchedSchema.name === schema.name) return;
|
// if (fetchedSchema.name === schema.name) return;
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
import { FC, useCallback } from 'react'
|
import { FC, useCallback } from 'react'
|
||||||
import { Template } from '../../types/schema';
|
import { Template } from '../../types/schema';
|
||||||
import { useObjectStateWrapper } from '../../hooks/useObjectState';
|
import { useObjectStateWrapper } from '../../hooks/useObjectState';
|
||||||
|
import { TEMPLATE_TYPES } from '../../constants/TemplateTypes';
|
||||||
|
import { SchemaEditAtom } from '../../recoil/atoms/schema';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
templateKey: string;
|
templateKey: string;
|
||||||
@@ -9,7 +12,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateEditor: FC<IProps> = ({ templateKey, update, template }) => {
|
export const TemplateEditor: FC<IProps> = ({ templateKey, update, template }) => {
|
||||||
|
const schema = useRecoilValue(SchemaEditAtom);
|
||||||
const updateTemplate = useCallback((t: Template | ((arg: Template) => Template)) => {
|
const updateTemplate = useCallback((t: Template | ((arg: Template) => Template)) => {
|
||||||
update(templateKey, typeof t === 'function' ? t(template) : t)
|
update(templateKey, typeof t === 'function' ? t(template) : t)
|
||||||
}, [templateKey, update, template])
|
}, [templateKey, update, template])
|
||||||
@@ -22,7 +25,15 @@ export const TemplateEditor: FC<IProps> = ({ templateKey, update, template }) =>
|
|||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<label className="w-min">
|
<label className="w-min">
|
||||||
Type
|
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>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" {...bindPropertyCheck('publishable')} />
|
<input type="checkbox" {...bindPropertyCheck('publishable')} />
|
||||||
|
@@ -11,7 +11,16 @@
|
|||||||
|
|
||||||
input,
|
input,
|
||||||
select {
|
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: fade 300ms forwards ease-in;
|
||||||
animation-delay: 300ms;
|
animation-delay: 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-out {
|
.fade-out {
|
||||||
animation: fade 300ms forwards ease-in reverse;
|
animation: fade 300ms forwards ease-in reverse;
|
||||||
}
|
}
|
||||||
@@ -63,7 +73,8 @@
|
|||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -3,5 +3,5 @@ import { Schema } from '../../types/schema';
|
|||||||
|
|
||||||
export const SchemaEditAtom = atom<Schema>({
|
export const SchemaEditAtom = atom<Schema>({
|
||||||
key: 'schema-edit',
|
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';
|
import { Schema } from '../types/schema';
|
||||||
|
|
||||||
|
const emptySchema = { name: '', types: {}, templates: {}, id: crypto.randomUUID() };
|
||||||
|
|
||||||
export const GameSystemsService = {
|
export const GameSystemsService = {
|
||||||
// todo - connect to service to save schema for game
|
// todo - connect to service to save schema for game
|
||||||
saveSchema: async (schema: Schema) => {
|
saveSchema: async (schema: Schema) => {
|
||||||
localStorage.setItem('schema', JSON.stringify(schema));
|
localStorage.setItem('schema ' + schema.id, JSON.stringify(schema));
|
||||||
|
|
||||||
return { status: 200 }
|
return { status: 200 }
|
||||||
},
|
},
|
||||||
// todo - connect to service to fetch schema for game
|
// todo - connect to service to fetch schema for game
|
||||||
getSchema: async (id: string): Promise<{status: 200 | 404, json: () => Promise<Schema>}> => {
|
getSchema: async (id: string): Promise<{ status: 200 | 404, json: () => Promise<Schema> }> => {
|
||||||
const schema = localStorage.getItem('schema');
|
const schema = localStorage.getItem('schema ' + id);
|
||||||
|
|
||||||
if (schema)
|
if (schema)
|
||||||
return {
|
return {
|
||||||
@@ -19,7 +22,30 @@ export const GameSystemsService = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: 404,
|
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 = {
|
export type Schema = {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
templates: Record<string, Template>;
|
templates: Record<string, Template>;
|
||||||
types: Record<string, TypeType>;
|
types: Record<string, TypeType>;
|
||||||
|
@@ -630,6 +630,11 @@
|
|||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
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@*":
|
"@types/scheduler@*":
|
||||||
version "0.16.3"
|
version "0.16.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||||
@@ -1825,6 +1830,11 @@ recoil@^0.7.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
hamt_plus "1.0.2"
|
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:
|
resolve-from@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
|
||||||
|
Reference in New Issue
Block a user