feat: add specialized visual editors for items, creatures, areas, and dialogs

This commit is contained in:
plenarius
2026-04-20 20:11:44 -04:00
parent 5620e38282
commit 6e3abb0b07
5 changed files with 1064 additions and 10 deletions
@@ -0,0 +1,185 @@
import { useMemo } from "react";
import {
GffEditor,
getFieldValue,
type FieldOverrideProps,
} from "./GffEditor";
interface CreatureEditorProps {
repo: string;
filePath: string;
content: string;
onSave?: (content: string) => void;
onSwitchToRaw?: () => void;
}
function AbilityScoresOverride({ data, onChange }: FieldOverrideProps) {
const abilities = [
["Str", "Dex", "Con"],
["Int", "Wis", "Cha"],
];
const displayNames: Record<string, string> = {
Str: "STR", Dex: "DEX", Con: "CON",
Int: "INT", Wis: "WIS", Cha: "CHA",
};
return (
<div className="space-y-2">
<label className="text-sm" style={{ color: "var(--forge-text-secondary)" }}>
Ability Scores
</label>
<div className="grid grid-cols-3 gap-3">
{abilities.flat().map((ab) => {
const val = getFieldValue(data, ab);
const num = typeof val === "number" ? val : 0;
return (
<div
key={ab}
className="flex flex-col items-center rounded border px-3 py-2"
style={{ borderColor: "var(--forge-border)", backgroundColor: "var(--forge-bg)" }}
>
<span className="text-xs font-bold" style={{ color: "var(--forge-text-secondary)" }}>
{displayNames[ab]}
</span>
<input
type="number"
value={num}
min={1}
max={99}
onChange={(e) => onChange(ab, parseInt(e.target.value, 10))}
className="mt-1 w-16 rounded border px-1 py-1 text-center text-lg font-semibold"
style={{
backgroundColor: "var(--forge-surface)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
/>
<span className="mt-0.5 text-xs" style={{ color: "var(--forge-text-secondary)" }}>
mod {num >= 10 ? "+" : ""}{Math.floor((num - 10) / 2)}
</span>
</div>
);
})}
</div>
</div>
);
}
function RaceGenderOverride({ data, onChange }: FieldOverrideProps) {
const race = getFieldValue(data, "Race");
const gender = getFieldValue(data, "Gender");
const raceNum = typeof race === "number" ? race : 0;
const genderNum = typeof gender === "number" ? gender : 0;
return (
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm" style={{ color: "var(--forge-text-secondary)" }}>Race</label>
<input
type="number"
value={raceNum}
min={0}
onChange={(e) => onChange("Race", parseInt(e.target.value, 10))}
className="w-20 rounded border px-2 py-1.5 text-sm"
style={{
backgroundColor: "var(--forge-bg)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
/>
<span className="text-xs" style={{ color: "var(--forge-text-secondary)" }}>
(racialtypes.2da)
</span>
</div>
<div className="flex items-center gap-2">
<label className="text-sm" style={{ color: "var(--forge-text-secondary)" }}>Gender</label>
<select
value={genderNum}
onChange={(e) => onChange("Gender", parseInt(e.target.value, 10))}
className="rounded border px-2 py-1.5 text-sm"
style={{
backgroundColor: "var(--forge-bg)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
>
<option value={0}>Male</option>
<option value={1}>Female</option>
</select>
</div>
</div>
);
}
function ScriptsOverride({ data, onChange }: FieldOverrideProps) {
const scripts = [
{ label: "ScriptHeartbeat", display: "Heartbeat" },
{ label: "ScriptOnDamaged", display: "On Damaged" },
{ label: "ScriptDeath", display: "On Death" },
{ label: "ScriptSpawn", display: "On Spawn" },
];
return (
<div className="space-y-2">
{scripts.map((s) => {
const val = getFieldValue(data, s.label);
const str = typeof val === "string" ? val : "";
return (
<div key={s.label} className="flex items-center gap-3">
<label className="w-28 shrink-0 text-sm" style={{ color: "var(--forge-text-secondary)" }}>
{s.display}
</label>
<input
type="text"
value={str}
maxLength={16}
onChange={(e) => onChange(s.label, e.target.value)}
className="flex-1 rounded border px-2 py-1.5 font-mono text-sm"
style={{
backgroundColor: "var(--forge-bg)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
placeholder="(none)"
/>
</div>
);
})}
</div>
);
}
export function CreatureEditor({ repo, filePath, content, onSave, onSwitchToRaw }: CreatureEditorProps) {
const fieldOverrides = useMemo(() => {
const overrides = new Map<string, (props: FieldOverrideProps) => React.ReactNode>();
overrides.set("Str", (props) => <AbilityScoresOverride {...props} />);
overrides.set("Dex", () => null);
overrides.set("Con", () => null);
overrides.set("Int", () => null);
overrides.set("Wis", () => null);
overrides.set("Cha", () => null);
overrides.set("Race", (props) => <RaceGenderOverride {...props} />);
overrides.set("Gender", () => null);
overrides.set("ScriptHeartbeat", (props) => <ScriptsOverride {...props} />);
overrides.set("ScriptOnDamaged", () => null);
overrides.set("ScriptDeath", () => null);
overrides.set("ScriptSpawn", () => null);
return overrides;
}, []);
return (
<GffEditor
repo={repo}
filePath={filePath}
content={content}
onSave={onSave}
onSwitchToRaw={onSwitchToRaw}
fieldOverrides={fieldOverrides}
/>
);
}