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
This commit is contained in:
plenarius
2026-04-21 12:14:38 -04:00
parent f39f1d818b
commit f851d8b8f2
62 changed files with 5519 additions and 5687 deletions
@@ -25,21 +25,28 @@ function AbilityScoresOverride({ data, onChange }: FieldOverrideProps) {
};
return (
<div className="space-y-2">
<label className="text-sm" style={{ color: "var(--forge-text-secondary)" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<label style={{ fontSize: "var(--text-sm)", color: "var(--forge-text-secondary)" }}>
Ability Scores
</label>
<div className="grid grid-cols-3 gap-3">
<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}
className="flex flex-col items-center rounded border px-3 py-2"
style={{ borderColor: "var(--forge-border)", backgroundColor: "var(--forge-bg)" }}
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 className="text-xs font-bold" style={{ color: "var(--forge-text-secondary)" }}>
<span style={{ fontSize: "var(--text-xs)", fontWeight: 700, color: "var(--forge-text-secondary)" }}>
{displayNames[ab]}
</span>
<input
@@ -48,14 +55,20 @@ function AbilityScoresOverride({ data, onChange }: FieldOverrideProps) {
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={{
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)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
/>
<span className="mt-0.5 text-xs" style={{ color: "var(--forge-text-secondary)" }}>
<span style={{ marginTop: "0.125rem", fontSize: "var(--text-xs)", color: "var(--forge-text-secondary)" }}>
mod {num >= 10 ? "+" : ""}{Math.floor((num - 10) / 2)}
</span>
</div>
@@ -73,34 +86,39 @@ function RaceGenderOverride({ data, onChange }: FieldOverrideProps) {
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>
<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))}
className="w-20 rounded border px-2 py-1.5 text-sm"
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)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
/>
<span className="text-xs" style={{ color: "var(--forge-text-secondary)" }}>
<span style={{ fontSize: "var(--text-xs)", 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>
<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))}
className="rounded border px-2 py-1.5 text-sm"
style={{
borderRadius: "0.25rem",
border: "1px solid var(--forge-border)",
padding: "0.375rem 0.5rem",
fontSize: "var(--text-sm)",
backgroundColor: "var(--forge-bg)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
>
@@ -121,13 +139,13 @@ function ScriptsOverride({ data, onChange }: FieldOverrideProps) {
];
return (
<div className="space-y-2">
<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} className="flex items-center gap-3">
<label className="w-28 shrink-0 text-sm" style={{ color: "var(--forge-text-secondary)" }}>
<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
@@ -135,10 +153,14 @@ function ScriptsOverride({ data, onChange }: FieldOverrideProps) {
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={{
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)",
borderColor: "var(--forge-border)",
color: "var(--forge-text)",
}}
placeholder="(none)"