feat: add specialized visual editors for items, creatures, areas, and dialogs
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
GffEditor,
|
||||
GffFieldType,
|
||||
getFieldValue,
|
||||
getLocStringText,
|
||||
type FieldOverrideProps,
|
||||
} from "./GffEditor";
|
||||
|
||||
interface ItemEditorProps {
|
||||
repo: string;
|
||||
filePath: string;
|
||||
content: string;
|
||||
onSave?: (content: string) => void;
|
||||
onSwitchToRaw?: () => void;
|
||||
}
|
||||
|
||||
function BaseItemOverride({ value, onChange, field }: FieldOverrideProps) {
|
||||
const num = typeof value === "number" ? value : 0;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="w-44 shrink-0 text-sm" style={{ color: "var(--forge-text-secondary)" }}>
|
||||
{field.displayName}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={num}
|
||||
onChange={(e) => onChange(field.label, parseInt(e.target.value, 10))}
|
||||
className="w-24 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)" }}>
|
||||
(baseitems.2da row)
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CompactNumbersOverride({ value, onChange, field, data }: FieldOverrideProps) {
|
||||
const stackSize = typeof getFieldValue(data, "StackSize") === "number"
|
||||
? (getFieldValue(data, "StackSize") as number) : 0;
|
||||
const cost = typeof getFieldValue(data, "Cost") === "number"
|
||||
? (getFieldValue(data, "Cost") as number) : 0;
|
||||
const charges = typeof getFieldValue(data, "Charges") === "number"
|
||||
? (getFieldValue(data, "Charges") as number) : 0;
|
||||
|
||||
if (field.label !== "StackSize") return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-4">
|
||||
{[
|
||||
{ label: "StackSize", display: "Stack", value: stackSize, max: 99 },
|
||||
{ label: "Cost", display: "Cost (gp)", value: cost, max: 999999 },
|
||||
{ label: "Charges", display: "Charges", value: charges, max: 255 },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="flex items-center gap-2">
|
||||
<label className="text-sm" style={{ color: "var(--forge-text-secondary)" }}>
|
||||
{item.display}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={item.value}
|
||||
min={0}
|
||||
max={item.max}
|
||||
onChange={(e) => onChange(item.label, parseInt(e.target.value, 10))}
|
||||
className="w-24 rounded border px-2 py-1.5 text-sm"
|
||||
style={{
|
||||
backgroundColor: "var(--forge-bg)",
|
||||
borderColor: "var(--forge-border)",
|
||||
color: "var(--forge-text)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BooleanFlagsOverride({ data, onChange }: FieldOverrideProps) {
|
||||
const flags = [
|
||||
{ label: "Identified", display: "Identified" },
|
||||
{ label: "Plot", display: "Plot" },
|
||||
{ label: "Stolen", display: "Stolen" },
|
||||
{ label: "Cursed", display: "Cursed" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-6">
|
||||
{flags.map((flag) => {
|
||||
const val = getFieldValue(data, flag.label);
|
||||
const checked = typeof val === "number" ? val !== 0 : Boolean(val);
|
||||
return (
|
||||
<label key={flag.label} className="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(flag.label, e.target.checked ? 1 : 0)}
|
||||
className="h-4 w-4 rounded"
|
||||
style={{ accentColor: "var(--forge-accent)" }}
|
||||
/>
|
||||
<span style={{ color: "var(--forge-text)" }}>{flag.display}</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PropertiesListOverride({ value }: FieldOverrideProps) {
|
||||
const list = Array.isArray(value) ? value : [];
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm" style={{ color: "var(--forge-text)" }}>
|
||||
Item Properties
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className="rounded px-2 py-1 text-xs"
|
||||
style={{ backgroundColor: "var(--forge-bg)", color: "var(--forge-text-secondary)" }}
|
||||
>
|
||||
+ Add Property
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{list.map((prop, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-2 rounded border px-3 py-2"
|
||||
style={{ borderColor: "var(--forge-border)", backgroundColor: "var(--forge-bg)" }}
|
||||
>
|
||||
<span className="flex-1 font-mono text-xs" style={{ color: "var(--forge-text)" }}>
|
||||
{typeof prop === "object" && prop !== null
|
||||
? JSON.stringify(prop).slice(0, 80)
|
||||
: String(prop)}
|
||||
</span>
|
||||
<button
|
||||
className="text-xs"
|
||||
style={{ color: "#ef4444" }}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{list.length === 0 && (
|
||||
<p className="py-2 text-xs" style={{ color: "var(--forge-text-secondary)" }}>
|
||||
No item properties
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ItemEditor({ repo, filePath, content, onSave, onSwitchToRaw }: ItemEditorProps) {
|
||||
const fieldOverrides = useMemo(() => {
|
||||
const overrides = new Map<string, (props: FieldOverrideProps) => React.ReactNode>();
|
||||
|
||||
overrides.set("BaseItem", (props) => <BaseItemOverride {...props} />);
|
||||
overrides.set("StackSize", (props) => <CompactNumbersOverride {...props} />);
|
||||
overrides.set("Cost", () => null);
|
||||
overrides.set("Charges", () => null);
|
||||
overrides.set("Identified", (props) => <BooleanFlagsOverride {...props} />);
|
||||
overrides.set("Plot", () => null);
|
||||
overrides.set("Stolen", () => null);
|
||||
overrides.set("Cursed", () => null);
|
||||
overrides.set("PropertiesList", (props) => <PropertiesListOverride {...props} />);
|
||||
|
||||
return overrides;
|
||||
}, []);
|
||||
|
||||
const itemName = useMemo(() => {
|
||||
try {
|
||||
const data = JSON.parse(content);
|
||||
const nameField = data.LocalizedName;
|
||||
return getLocStringText(nameField) || "(unnamed item)";
|
||||
} catch {
|
||||
return "(unnamed item)";
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
const headerSlot = (
|
||||
<div
|
||||
className="border-b px-4 py-3"
|
||||
style={{ borderColor: "var(--forge-border)", backgroundColor: "var(--forge-surface)" }}
|
||||
>
|
||||
<h2 className="text-lg font-semibold" style={{ color: "var(--forge-text)" }}>
|
||||
{itemName}
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<GffEditor
|
||||
repo={repo}
|
||||
filePath={filePath}
|
||||
content={content}
|
||||
onSave={onSave}
|
||||
onSwitchToRaw={onSwitchToRaw}
|
||||
fieldOverrides={fieldOverrides}
|
||||
headerSlot={headerSlot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user