Files
layonara-forge/packages/frontend/src/components/gff/CreatureEditor.tsx
T
plenarius f851d8b8f2 Layonara Forge — NWN Development IDE
Electron desktop application for Neverwinter Nights module development.
Clone, edit, build, and run a complete Layonara NWNX server with only
Docker required.

- React 19 + Vite frontend with Monaco editor and NWScript LSP
- Node.js + Express backend managing Docker sibling containers
- Electron shell with Docker availability check and auto-setup
- Builder image auto-builds on first use from bundled Dockerfile
- Cross-platform: Windows (.exe), macOS (.dmg), Linux (.AppImage)
- Gitea Actions CI for automated release builds
2026-04-21 12:14:38 -04:00

208 lines
6.9 KiB
TypeScript

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 style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<label style={{ fontSize: "var(--text-sm)", color: "var(--forge-text-secondary)" }}>
Ability Scores
</label>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "0.75rem" }}>
{abilities.flat().map((ab) => {
const val = getFieldValue(data, ab);
const num = typeof val === "number" ? val : 0;
return (
<div
key={ab}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
borderRadius: "0.25rem",
border: "1px solid var(--forge-border)",
padding: "0.5rem 0.75rem",
backgroundColor: "var(--forge-bg)",
}}
>
<span style={{ fontSize: "var(--text-xs)", fontWeight: 700, 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))}
style={{
marginTop: "0.25rem",
width: "4rem",
borderRadius: "0.25rem",
border: "1px solid var(--forge-border)",
padding: "0.25rem",
textAlign: "center",
fontSize: "var(--text-lg)",
fontWeight: 600,
backgroundColor: "var(--forge-surface)",
color: "var(--forge-text)",
}}
/>
<span style={{ marginTop: "0.125rem", fontSize: "var(--text-xs)", 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 style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<label style={{ fontSize: "var(--text-sm)", color: "var(--forge-text-secondary)" }}>Race</label>
<input
type="number"
value={raceNum}
min={0}
onChange={(e) => onChange("Race", parseInt(e.target.value, 10))}
style={{
width: "5rem",
borderRadius: "0.25rem",
border: "1px solid var(--forge-border)",
padding: "0.375rem 0.5rem",
fontSize: "var(--text-sm)",
backgroundColor: "var(--forge-bg)",
color: "var(--forge-text)",
}}
/>
<span style={{ fontSize: "var(--text-xs)", color: "var(--forge-text-secondary)" }}>
(racialtypes.2da)
</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<label style={{ fontSize: "var(--text-sm)", color: "var(--forge-text-secondary)" }}>Gender</label>
<select
value={genderNum}
onChange={(e) => onChange("Gender", parseInt(e.target.value, 10))}
style={{
borderRadius: "0.25rem",
border: "1px solid var(--forge-border)",
padding: "0.375rem 0.5rem",
fontSize: "var(--text-sm)",
backgroundColor: "var(--forge-bg)",
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 style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
{scripts.map((s) => {
const val = getFieldValue(data, s.label);
const str = typeof val === "string" ? val : "";
return (
<div key={s.label} style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
<label style={{ width: "7rem", flexShrink: 0, fontSize: "var(--text-sm)", color: "var(--forge-text-secondary)" }}>
{s.display}
</label>
<input
type="text"
value={str}
maxLength={16}
onChange={(e) => onChange(s.label, e.target.value)}
style={{
flex: 1,
borderRadius: "0.25rem",
border: "1px solid var(--forge-border)",
padding: "0.375rem 0.5rem",
fontFamily: "var(--font-mono)",
fontSize: "var(--text-sm)",
backgroundColor: "var(--forge-bg)",
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}
/>
);
}