Vibecode central
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { ABILITIES, Ability, Skill, SKILL_ABILITY } from "@/types/content";
|
||||
import type { AdvantageScope, Effect, EffectTarget } from "@/types/core";
|
||||
import SpellProgressionTable from "./SpellProgressionTable";
|
||||
|
||||
/**
|
||||
* Comma-list input with a local string buffer so typing spaces and multi-word items
|
||||
* (like "thieves' tools") doesn't get eaten while typing.
|
||||
*/
|
||||
function CommaListInput({
|
||||
value,
|
||||
onCommit,
|
||||
placeholder,
|
||||
className,
|
||||
}: {
|
||||
value: string[];
|
||||
onCommit: (next: string[]) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [buf, setBuf] = useState(value.join(", "));
|
||||
|
||||
useEffect(() => {
|
||||
setBuf(value.join(", "));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value.join("\u0001")]);
|
||||
|
||||
const commit = () => {
|
||||
const parsed = buf.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
onCommit(parsed);
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={buf}
|
||||
onChange={(e) => setBuf(e.target.value)}
|
||||
onBlur={commit}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SKILLS = Object.keys(SKILL_ABILITY) as Skill[];
|
||||
|
||||
const TARGET_KINDS: EffectTarget["kind"][] = [
|
||||
"ability",
|
||||
"abilityScoreChoice",
|
||||
"speed",
|
||||
"hpMax",
|
||||
"armorClass",
|
||||
"initiative",
|
||||
"savingThrowProficiency",
|
||||
"skillProficiency",
|
||||
"skillProficiencyChoice",
|
||||
"skillExpertise",
|
||||
"toolExpertise",
|
||||
"expertiseChoice",
|
||||
"armorProficiency",
|
||||
"weaponProficiency",
|
||||
"toolProficiency",
|
||||
"language",
|
||||
"resistance",
|
||||
"immunity",
|
||||
"vulnerability",
|
||||
"grantSpell",
|
||||
"grantSpellcasting",
|
||||
"grantFeat",
|
||||
"extraAttack",
|
||||
"grantSubclassChoice",
|
||||
"advantage",
|
||||
"disadvantage",
|
||||
"action",
|
||||
"bonusAction",
|
||||
"reaction",
|
||||
"custom",
|
||||
];
|
||||
|
||||
const KIND_LABELS: Record<EffectTarget["kind"], string> = {
|
||||
ability: "Ability score",
|
||||
abilityScoreChoice: "Ability score improvement (choose)",
|
||||
speed: "Speed",
|
||||
hpMax: "HP Max",
|
||||
armorClass: "Armor class",
|
||||
initiative: "Initiative",
|
||||
savingThrowProficiency: "Saving throw prof.",
|
||||
skillProficiency: "Skill proficiency",
|
||||
skillProficiencyChoice: "Skill proficiency (choose N)",
|
||||
skillExpertise: "Skill expertise",
|
||||
toolExpertise: "Tool expertise",
|
||||
expertiseChoice: "Expertise (choose N)",
|
||||
armorProficiency: "Armor proficiency",
|
||||
weaponProficiency: "Weapon proficiency",
|
||||
toolProficiency: "Tool proficiency",
|
||||
language: "Language",
|
||||
resistance: "Damage resistance",
|
||||
immunity: "Damage immunity",
|
||||
vulnerability: "Damage vulnerability",
|
||||
grantSpell: "Grant spell",
|
||||
grantSpellcasting: "Grant spellcasting",
|
||||
grantFeat: "Grant feat",
|
||||
extraAttack: "Extra attack",
|
||||
grantSubclassChoice: "Grant subclass choice",
|
||||
advantage: "Advantage",
|
||||
disadvantage: "Disadvantage",
|
||||
action: "Action",
|
||||
bonusAction: "Bonus Action",
|
||||
reaction: "Reaction",
|
||||
custom: "Custom note",
|
||||
};
|
||||
|
||||
const ADV_SCOPES = ["attack", "save", "skill", "check"] as const;
|
||||
const SCOPE_LABELS: Record<(typeof ADV_SCOPES)[number], string> = {
|
||||
attack: "attack rolls",
|
||||
save: "saving throws",
|
||||
skill: "skill check",
|
||||
check: "ability checks",
|
||||
};
|
||||
|
||||
function defaultTargetFor(kind: EffectTarget["kind"]): EffectTarget {
|
||||
switch (kind) {
|
||||
case "ability": return { kind, ability: "strength" };
|
||||
case "abilityScoreChoice": return { kind, points: 2, maxPerAbility: 2, cap: 20 };
|
||||
case "speed":
|
||||
case "hpMax":
|
||||
case "armorClass":
|
||||
case "initiative": return { kind };
|
||||
case "savingThrowProficiency": return { kind, ability: "strength" };
|
||||
case "skillProficiency":
|
||||
case "skillExpertise": return { kind, skill: "athletics" };
|
||||
case "skillProficiencyChoice": return { kind, count: 2, from: [] };
|
||||
case "toolExpertise": return { kind, tool: "" };
|
||||
case "expertiseChoice": return { kind, count: 2, skillPool: "characterSkills", toolPool: [] };
|
||||
case "armorProficiency":
|
||||
case "weaponProficiency":
|
||||
case "toolProficiency":
|
||||
case "language": return { kind, value: "" };
|
||||
case "resistance":
|
||||
case "immunity":
|
||||
case "vulnerability": return { kind, damageType: "fire" };
|
||||
case "grantSpell": return { kind, spellId: "" };
|
||||
case "grantSpellcasting": return {
|
||||
kind,
|
||||
ability: "intelligence",
|
||||
progression: [{ level: 1, cantripsKnown: 0, spellsKnown: 0, slots: [] }],
|
||||
};
|
||||
case "grantFeat": return { kind, featId: "" };
|
||||
case "extraAttack": return { kind, count: 1 };
|
||||
case "grantSubclassChoice": return { kind, label: "" };
|
||||
case "advantage":
|
||||
case "disadvantage": return { kind, scope: { on: "attack" }, condition: "" };
|
||||
case "action":
|
||||
case "bonusAction":
|
||||
case "reaction": return { kind, note: "" };
|
||||
case "custom": return { kind, note: "" };
|
||||
}
|
||||
}
|
||||
|
||||
function defaultScope(on: AdvantageScope["on"]): AdvantageScope {
|
||||
switch (on) {
|
||||
case "attack": return { on };
|
||||
case "save": return { on, ability: undefined };
|
||||
case "skill": return { on, skill: "athletics" };
|
||||
case "check": return { on, ability: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
effects: Effect[];
|
||||
onChange: (next: Effect[]) => void;
|
||||
}
|
||||
|
||||
export default function EffectEditor({ effects, onChange }: Props) {
|
||||
const update = (idx: number, patch: Partial<Effect>) => {
|
||||
onChange(effects.map((e, i) => (i === idx ? { ...e, ...patch } : e)));
|
||||
};
|
||||
const updateTarget = (idx: number, target: EffectTarget) => update(idx, { target });
|
||||
const remove = (idx: number) => onChange(effects.filter((_, i) => i !== idx));
|
||||
const add = () =>
|
||||
onChange([...effects, { target: { kind: "ability", ability: "strength" }, operation: "increase", value: 1 }]);
|
||||
|
||||
const needsValue = (kind: EffectTarget["kind"]) =>
|
||||
["ability", "speed", "hpMax", "armorClass", "initiative"].includes(kind);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{effects.length === 0 && (
|
||||
<p className="text-sm text-neutral-500 italic">No effects. Click below to add one.</p>
|
||||
)}
|
||||
{effects.map((eff, i) => {
|
||||
const kind = eff.target.kind;
|
||||
return (
|
||||
<div key={i} className="flex flex-wrap items-center gap-2 p-3 border border-neutral-700 rounded bg-neutral-950/50">
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={kind}
|
||||
onChange={(e) => updateTarget(i, defaultTargetFor(e.target.value as EffectTarget["kind"]))}
|
||||
>
|
||||
{TARGET_KINDS.map((k) => (
|
||||
<option key={k} value={k}>{KIND_LABELS[k]}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{eff.target.kind === "ability" && (
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={eff.target.ability}
|
||||
onChange={(e) => updateTarget(i, { kind: "ability", ability: e.target.value as Ability })}
|
||||
>
|
||||
{ABILITIES.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
)}
|
||||
{eff.target.kind === "abilityScoreChoice" && (() => {
|
||||
const t = eff.target;
|
||||
const setPatch = (patch: Partial<typeof t>) =>
|
||||
updateTarget(i, { ...t, ...patch } as EffectTarget);
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-neutral-500">distribute</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={t.points}
|
||||
onChange={(e) => setPatch({ points: Math.max(1, Number(e.target.value) || 1) })}
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-16"
|
||||
/>
|
||||
<span className="text-xs text-neutral-500">points, max</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={t.maxPerAbility}
|
||||
onChange={(e) => setPatch({ maxPerAbility: Math.max(1, Number(e.target.value) || 1) })}
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-16"
|
||||
/>
|
||||
<span className="text-xs text-neutral-500">per ability, cap</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="none"
|
||||
value={t.cap ?? ""}
|
||||
onChange={(e) =>
|
||||
setPatch({ cap: e.target.value ? Math.max(1, Number(e.target.value)) : undefined })
|
||||
}
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-16"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{eff.target.kind === "savingThrowProficiency" && (
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={eff.target.ability}
|
||||
onChange={(e) => updateTarget(i, { kind: "savingThrowProficiency", ability: e.target.value as Ability })}
|
||||
>
|
||||
{ABILITIES.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
)}
|
||||
{(eff.target.kind === "skillProficiency" || eff.target.kind === "skillExpertise") && (
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={eff.target.skill}
|
||||
onChange={(e) => updateTarget(i, { kind: eff.target.kind as "skillProficiency" | "skillExpertise", skill: e.target.value as Skill })}
|
||||
>
|
||||
{SKILLS.map((s) => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
)}
|
||||
{eff.target.kind === "toolExpertise" && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="tool name (e.g. thieves' tools)"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-56"
|
||||
value={eff.target.tool}
|
||||
onChange={(e) => updateTarget(i, { kind: "toolExpertise", tool: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{eff.target.kind === "expertiseChoice" && (() => {
|
||||
const t = eff.target;
|
||||
const setPatch = (patch: Partial<typeof t>) =>
|
||||
updateTarget(i, { ...t, ...patch } as EffectTarget);
|
||||
const skillPoolMode = t.skillPool === "characterSkills" ? "characterSkills" : "specific";
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-neutral-500">choose</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
value={t.count}
|
||||
onChange={(e) => setPatch({ count: Math.max(1, Number(e.target.value) || 1) })}
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-16"
|
||||
/>
|
||||
<span className="text-xs text-neutral-500">total from:</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-neutral-400">Skill pool:</span>
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={skillPoolMode}
|
||||
onChange={(e) =>
|
||||
setPatch({
|
||||
skillPool: e.target.value === "characterSkills" ? "characterSkills" : [],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="characterSkills">Any skill character is proficient in</option>
|
||||
<option value="specific">Specific skills</option>
|
||||
</select>
|
||||
</div>
|
||||
{Array.isArray(t.skillPool) && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{SKILLS.map((s) => {
|
||||
const arr = t.skillPool as Skill[];
|
||||
const on = arr.includes(s);
|
||||
return (
|
||||
<label
|
||||
key={s}
|
||||
className={`px-2 py-0.5 text-xs rounded border cursor-pointer ${on ? "bg-amber-600/20 border-amber-500" : "border-neutral-700"}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="hidden"
|
||||
checked={on}
|
||||
onChange={() =>
|
||||
setPatch({
|
||||
skillPool: on ? arr.filter((x) => x !== s) : [...arr, s],
|
||||
})
|
||||
}
|
||||
/>
|
||||
{s}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-neutral-400">Also eligible tools (comma):</span>
|
||||
<CommaListInput
|
||||
value={t.toolPool}
|
||||
onCommit={(next) => setPatch({ toolPool: next })}
|
||||
placeholder="thieves' tools"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{eff.target.kind === "skillProficiencyChoice" && (() => {
|
||||
const t = eff.target;
|
||||
const setCount = (n: number) =>
|
||||
updateTarget(i, { kind: "skillProficiencyChoice", count: n, from: t.from });
|
||||
const toggleFrom = (s: Skill) => {
|
||||
const next = t.from.includes(s) ? t.from.filter((x) => x !== s) : [...t.from, s];
|
||||
updateTarget(i, { kind: "skillProficiencyChoice", count: t.count, from: next });
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-neutral-500">choose</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={t.from.length || 18}
|
||||
value={t.count}
|
||||
onChange={(e) => setCount(Math.max(1, Number(e.target.value) || 1))}
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-16"
|
||||
/>
|
||||
<span className="text-xs text-neutral-500">of {t.from.length} option(s)</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{SKILLS.map((s) => {
|
||||
const on = t.from.includes(s);
|
||||
return (
|
||||
<label
|
||||
key={s}
|
||||
className={`px-2 py-0.5 text-xs rounded border cursor-pointer ${on ? "bg-amber-600/20 border-amber-500" : "border-neutral-700"}`}
|
||||
>
|
||||
<input type="checkbox" className="hidden" checked={on} onChange={() => toggleFrom(s)} />
|
||||
{s}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{(eff.target.kind === "armorProficiency" ||
|
||||
eff.target.kind === "weaponProficiency" ||
|
||||
eff.target.kind === "toolProficiency" ||
|
||||
eff.target.kind === "language") && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="value"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-40"
|
||||
value={eff.target.value}
|
||||
onChange={(e) => updateTarget(i, { kind: eff.target.kind as "armorProficiency" | "weaponProficiency" | "toolProficiency" | "language", value: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{(eff.target.kind === "resistance" ||
|
||||
eff.target.kind === "immunity" ||
|
||||
eff.target.kind === "vulnerability") && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="damage type"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-32"
|
||||
value={eff.target.damageType}
|
||||
onChange={(e) => updateTarget(i, { kind: eff.target.kind as "resistance" | "immunity" | "vulnerability", damageType: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{eff.target.kind === "grantSpell" && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="spell id"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-40"
|
||||
value={eff.target.spellId}
|
||||
onChange={(e) => updateTarget(i, { kind: "grantSpell", spellId: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{eff.target.kind === "grantSpellcasting" && (() => {
|
||||
const t = eff.target;
|
||||
const setPatch = (patch: Partial<typeof t>) =>
|
||||
updateTarget(i, { ...t, ...patch } as EffectTarget);
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-neutral-500">Ability:</span>
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={t.ability}
|
||||
onChange={(e) => setPatch({ ability: e.target.value as Ability })}
|
||||
>
|
||||
{ABILITIES.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
<label className="text-xs text-neutral-500 flex items-center gap-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={t.ritual === true}
|
||||
onChange={(e) => setPatch({ ritual: e.target.checked })}
|
||||
/>
|
||||
Ritual casting
|
||||
</label>
|
||||
</div>
|
||||
<SpellProgressionTable
|
||||
progression={t.progression}
|
||||
onChange={(rows) => setPatch({ progression: rows })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{eff.target.kind === "grantFeat" && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="feat id"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-40"
|
||||
value={eff.target.featId}
|
||||
onChange={(e) => updateTarget(i, { kind: "grantFeat", featId: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{eff.target.kind === "grantSubclassChoice" && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="label (e.g. Roguish Archetype)"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 flex-1"
|
||||
value={eff.target.label ?? ""}
|
||||
onChange={(e) => updateTarget(i, { kind: "grantSubclassChoice", label: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{eff.target.kind === "extraAttack" && (
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-20"
|
||||
value={eff.target.count}
|
||||
onChange={(e) => updateTarget(i, { kind: "extraAttack", count: Number(e.target.value) || 1 })}
|
||||
/>
|
||||
)}
|
||||
{(eff.target.kind === "advantage" || eff.target.kind === "disadvantage") && (() => {
|
||||
const kindLocal = eff.target.kind;
|
||||
const scope = eff.target.scope;
|
||||
const cond = eff.target.condition ?? "";
|
||||
const setScope = (next: AdvantageScope) =>
|
||||
updateTarget(i, { kind: kindLocal, scope: next, condition: cond });
|
||||
const setCond = (c: string) =>
|
||||
updateTarget(i, { kind: kindLocal, scope, condition: c });
|
||||
return (
|
||||
<>
|
||||
<span className="text-xs text-neutral-500">on</span>
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={scope.on}
|
||||
onChange={(e) => setScope(defaultScope(e.target.value as AdvantageScope["on"]))}
|
||||
>
|
||||
{ADV_SCOPES.map((s) => <option key={s} value={s}>{SCOPE_LABELS[s]}</option>)}
|
||||
</select>
|
||||
{scope.on === "skill" && (
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={scope.skill}
|
||||
onChange={(e) => setScope({ on: "skill", skill: e.target.value as Skill })}
|
||||
>
|
||||
{SKILLS.map((s) => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
)}
|
||||
{(scope.on === "save" || scope.on === "check") && (
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={scope.ability ?? ""}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setScope({ on: scope.on, ability: val ? (val as Ability) : undefined } as AdvantageScope);
|
||||
}}
|
||||
>
|
||||
<option value="">all</option>
|
||||
{ABILITIES.map((a) => <option key={a} value={a}>{a}</option>)}
|
||||
</select>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="condition (optional, e.g. 'vs poison')"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 flex-1 min-w-[10rem]"
|
||||
value={cond}
|
||||
onChange={(e) => setCond(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
{(eff.target.kind === "action" || eff.target.kind === "bonusAction" || eff.target.kind === "reaction") && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="note (e.g. 'Dash, Disengage, or Hide')"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 flex-1"
|
||||
value={eff.target.note}
|
||||
onChange={(e) => updateTarget(i, { kind: eff.target.kind as "action" | "bonusAction" | "reaction", note: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{eff.target.kind === "custom" && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="note"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 flex-1"
|
||||
value={eff.target.note}
|
||||
onChange={(e) => updateTarget(i, { kind: "custom", note: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{needsValue(kind) && (
|
||||
<>
|
||||
<select
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700"
|
||||
value={eff.operation}
|
||||
onChange={(e) => update(i, { operation: e.target.value as Effect["operation"] })}
|
||||
>
|
||||
<option value="increase">+</option>
|
||||
<option value="decrease">−</option>
|
||||
<option value="set">=</option>
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-20"
|
||||
value={eff.value ?? 0}
|
||||
onChange={(e) => update(i, { value: Number(e.target.value) })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="ml-auto flex items-center gap-1">
|
||||
<label className="text-[10px] text-neutral-500 uppercase">Lv</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
placeholder="1"
|
||||
value={eff.level ?? ""}
|
||||
onChange={(e) => update(i, { level: e.target.value ? Number(e.target.value) : undefined })}
|
||||
className="bg-neutral-800 text-sm rounded px-2 py-1 border border-neutral-700 w-14 text-center"
|
||||
title="Minimum level for this effect to apply"
|
||||
/>
|
||||
<button
|
||||
onClick={() => remove(i)}
|
||||
className="text-red-400 hover:text-red-300 text-sm px-2"
|
||||
type="button"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button
|
||||
onClick={add}
|
||||
type="button"
|
||||
className="text-sm text-amber-400 border border-amber-400/40 hover:bg-amber-400/10 rounded px-3 py-1"
|
||||
>
|
||||
+ Add effect
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user