init: board game phaser start
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { h } from 'preact';
|
||||
import { Signal } from '@preact/signals-core';
|
||||
|
||||
interface CommandLogProps {
|
||||
entries: Signal<Array<{ input: string; result: string; timestamp: number }>>;
|
||||
maxEntries?: number;
|
||||
}
|
||||
|
||||
export function CommandLog({ entries, maxEntries = 50 }: CommandLogProps) {
|
||||
const displayEntries = entries.value.slice(-maxEntries).reverse();
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 text-green-400 font-mono text-xs p-3 rounded-lg overflow-y-auto max-h-48">
|
||||
{displayEntries.length === 0 ? (
|
||||
<div className="text-gray-500 italic">No commands yet</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{displayEntries.map((entry, i) => (
|
||||
<div key={entry.timestamp + '-' + i} className="flex gap-2">
|
||||
<span className="text-gray-500">
|
||||
{new Date(entry.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
<span className="text-yellow-300">> {entry.input}</span>
|
||||
<span className={entry.result.startsWith('OK') ? 'text-green-400' : 'text-red-400'}>
|
||||
{entry.result}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { effect } from '@preact/signals-core';
|
||||
|
||||
type DisposeFn = () => void;
|
||||
|
||||
export interface GameUIOptions {
|
||||
container: HTMLElement;
|
||||
root: any;
|
||||
}
|
||||
|
||||
export class GameUI {
|
||||
private container: HTMLElement;
|
||||
private root: any;
|
||||
private effects: DisposeFn[] = [];
|
||||
|
||||
constructor(options: GameUIOptions) {
|
||||
this.container = options.container;
|
||||
this.root = options.root;
|
||||
}
|
||||
|
||||
mount(): void {
|
||||
import('preact').then(({ render }) => {
|
||||
render(this.root, this.container);
|
||||
});
|
||||
}
|
||||
|
||||
watch(fn: () => void): DisposeFn {
|
||||
const e = effect(fn);
|
||||
this.effects.push(e);
|
||||
return e;
|
||||
}
|
||||
|
||||
unmount(): void {
|
||||
import('preact').then(({ render }) => {
|
||||
render(null, this.container);
|
||||
});
|
||||
for (const e of this.effects) {
|
||||
e();
|
||||
}
|
||||
this.effects = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { h } from 'preact';
|
||||
import { useState, useCallback } from 'preact/hooks';
|
||||
import type { PromptEvent, CommandSchema, CommandParamSchema } from 'boardgame-core';
|
||||
|
||||
interface PromptDialogProps {
|
||||
prompt: PromptEvent | null;
|
||||
onSubmit: (input: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function schemaToPlaceholder(schema: CommandSchema): string {
|
||||
const parts: string[] = [schema.name];
|
||||
for (const param of schema.params) {
|
||||
if (param.required) {
|
||||
parts.push(`<${param.name}>`);
|
||||
} else {
|
||||
parts.push(`[${param.name}]`);
|
||||
}
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function schemaToFields(schema: CommandSchema): Array<{ param: CommandParamSchema; label: string }> {
|
||||
return schema.params
|
||||
.filter(p => p.required)
|
||||
.map(p => ({ param: p, label: p.name }));
|
||||
}
|
||||
|
||||
export function PromptDialog({ prompt, onSubmit, onCancel }: PromptDialogProps) {
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!prompt) return;
|
||||
|
||||
const fieldValues = schemaToFields(prompt.schema).map(f => values[f.label] || '');
|
||||
const cmdString = [prompt.schema.name, ...fieldValues].join(' ');
|
||||
|
||||
const err = prompt.tryCommit(cmdString);
|
||||
if (err) {
|
||||
setError(err);
|
||||
} else {
|
||||
onSubmit(cmdString);
|
||||
setValues({});
|
||||
setError(null);
|
||||
}
|
||||
}, [prompt, values, onSubmit]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
onCancel();
|
||||
setValues({});
|
||||
setError(null);
|
||||
}, [onCancel]);
|
||||
|
||||
if (!prompt) return null;
|
||||
|
||||
const fields = schemaToFields(prompt.schema);
|
||||
const placeholder = schemaToPlaceholder(prompt.schema);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl p-6 min-w-[320px] max-w-md">
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-800">{prompt.schema.name}</h3>
|
||||
<p className="text-sm text-gray-500 mb-4 font-mono">{placeholder}</p>
|
||||
|
||||
{fields.length > 0 ? (
|
||||
<div className="space-y-3 mb-4">
|
||||
{fields.map(({ param, label }) => (
|
||||
<div key={label}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={values[label] || ''}
|
||||
onInput={(e) => setValues(prev => ({ ...prev, [label]: (e.target as HTMLInputElement).value }))}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }}
|
||||
autoFocus={fields.indexOf({ param, label }) === 0}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Enter command..."
|
||||
onInput={(e) => {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
setValues({ _raw: val });
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-red-600 mb-3">{error}</p>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user