Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.818
0.7.819
1 change: 1 addition & 0 deletions src/display/palette/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export const basePalette = {
house_sign: { glyph: "!", fg: "#d8c08a", glow: "#8b6f3f" },
audio_sign: { glyph: "!", fg: "#ffe066", glow: "#ccaa00" },
alchemy_bench: { glyph: "⚗", fg: "#93def6", glow: "#4f7fa1" },
enchanting_bench: { glyph: "✧", fg: "#d8b8ff", glow: "#7f5ac8" },
potion_shelf: { glyph: "=", fg: "#7986cb", glow: "#3949ab" },
herb_chest: { glyph: "]", fg: "#66bb6a", glow: "#2e7d32" },
tavern_chest: { glyph: "]", fg: "#d7a15d", glow: "#8f5225" },
Expand Down
2 changes: 1 addition & 1 deletion src/display/palette/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const _CORPSE_SKIP_PREFIXES = [
const _CORPSE_SKIP_KEYS = new Set([
'gold', 'monster', 'bone', 'engraving', 'spawner', 'tombstone',
'chest', 'mill_chest', 'smithy_chest', 'lumber_chest', 'herb_chest', 'tavern_chest',
'bed_home', 'house_sign', 'alchemy_bench',
'bed_home', 'house_sign', 'alchemy_bench', 'enchanting_bench',
'berry_bush', 'herb_patch', 'thorn_bramble', 'venom_fern', 'moonleaf_cluster', 'ember_root_patch', 'venom_spores',
'anvil', 'anvil_active', 'furnace', 'furnace_unlit', 'cooking_fire',
'crop_wheat', 'crop_carrot', 'crop_corn',
Expand Down
190 changes: 190 additions & 0 deletions src/display/ui/enchantingBenchOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
const INGREDIENT_LABELS = Object.freeze({
emberRoot: "Ember Root",
moonleaf: "Moonleaf",
thornPods: "Thorn Pods",
venomFronds: "Venom Fronds",
spiderLeg: "Spider Leg",
venomGland: "Venom Gland",
resin: "Binding Resin",
boneDust: "Bone Dust",
ectoplasm: "Ectoplasm",
runeFragment: "Rune Fragment",
frostCore: "Frost Core",
beastClaw: "Beast Claw",
cursedThread: "Cursed Thread",
oil: "Flask of Oil",
water: "Water Flask",
ashes: "Ashes",
gold: "Gold",
});

function ingredientLabel(key) {
return INGREDIENT_LABELS[key] || key;
}

function normalizeRequirements(recipe) {
const req = {};
const src = (recipe?.requirements && typeof recipe.requirements === "object") ? recipe.requirements : {};
for (const [key, raw] of Object.entries(src)) {
const need = Math.max(0, Number(raw || 0) | 0);
if (need > 0) req[key] = need;
}
return req;
}

function canCraftRecipe(recipe, ingredients) {
if (typeof recipe?.canCraft === "boolean") return recipe.canCraft;
const req = normalizeRequirements(recipe);
for (const [key, need] of Object.entries(req)) {
const have = Math.max(0, Number(ingredients?.[key] || 0) | 0);
if (have < need) return false;
}
return true;
}

function formatRequirementLine(requirements, outputLabel) {
const parts = [];
for (const [key, need] of Object.entries(requirements)) {
parts.push(`${need} ${ingredientLabel(key).toLowerCase()}`);
}
return `Needs ${parts.join(" + ")} → ${outputLabel}`;
}

export function renderEnchantingBench(panel, state) {
const el = /** @type {HTMLDivElement} */ (/** @type {any} */ (panel)._inner);
el.innerHTML = "";

const title = document.createElement("div");
title.textContent = String(state?.title || "✧ Enchanting Bench");
Object.assign(title.style, {
fontWeight: "bold",
marginBottom: "8px",
color: "#d9c3ff",
fontSize: "18px",
});
el.appendChild(title);

const subtitle = document.createElement("div");
subtitle.textContent = String(state?.subtitle || "Bind reagents and gold into a scroll, then apply it to your gear.");
Object.assign(subtitle.style, {
opacity: "0.86",
marginBottom: "10px",
fontSize: "12px",
});
el.appendChild(subtitle);

const ingredients = (state?.ingredients && typeof state.ingredients === "object")
? state.ingredients
: {};
const stock = document.createElement("div");
Object.assign(stock.style, {
display: "flex",
flexWrap: "wrap",
gap: "12px",
marginBottom: "10px",
padding: "8px",
border: "1px solid #4f3b67",
borderRadius: "6px",
background: "#16111e",
});
for (const key of Object.keys(INGREDIENT_LABELS)) {
const item = document.createElement("div");
const count = Math.max(0, Number(ingredients[key] || 0) | 0);
item.textContent = `${ingredientLabel(key)}: ${count}`;
stock.appendChild(item);
}
el.appendChild(stock);

const list = document.createElement("div");
Object.assign(list.style, {
display: "flex",
flexDirection: "column",
gap: "8px",
});
el.appendChild(list);

const recipes = Array.isArray(state?.recipes) ? state.recipes : [];
if (!recipes.length) {
const empty = document.createElement("div");
empty.textContent = "The vellum waits for a worthy binding.";
empty.style.opacity = "0.7";
list.appendChild(empty);
return;
}

for (const recipe of recipes) {
const row = document.createElement("div");
Object.assign(row.style, {
display: "grid",
gridTemplateColumns: "1fr auto",
gap: "8px",
alignItems: "center",
border: "1px solid #4f3b67",
borderRadius: "6px",
padding: "8px",
background: "#130f1c",
});

const textWrap = document.createElement("div");
const label = document.createElement("div");
label.textContent = String(recipe?.label || "Recipe");
label.style.fontWeight = "bold";
label.style.color = "#f1e1ff";

const req = document.createElement("div");
req.textContent = formatRequirementLine(normalizeRequirements(recipe), String(recipe?.outputName || "scroll"));
req.style.opacity = "0.78";
req.style.fontSize = "12px";

const effect = document.createElement("div");
effect.textContent = String(recipe?.effectSummary || "");
effect.style.opacity = "0.72";
effect.style.fontSize = "11px";

const flavor = document.createElement("div");
flavor.textContent = String(recipe?.flavor || "");
flavor.style.opacity = "0.58";
flavor.style.fontSize = "11px";

textWrap.appendChild(label);
textWrap.appendChild(req);
if (String(recipe?.effectSummary || "").trim()) textWrap.appendChild(effect);
if (String(recipe?.flavor || "").trim()) textWrap.appendChild(flavor);

const craftBtn = document.createElement("button");
const canCraft = canCraftRecipe(recipe, ingredients);
craftBtn.textContent = canCraft ? "Scribe" : "Missing";
craftBtn.disabled = !canCraft || !(state?.benchId > 0);
Object.assign(craftBtn.style, {
minWidth: "92px",
height: "34px",
border: "1px solid #5b4480",
borderRadius: "6px",
background: canCraft ? "#2d1b4f" : "#1a1a1a",
color: canCraft ? "#f1d7ff" : "#8892a0",
fontWeight: "bold",
cursor: canCraft ? "pointer" : "default",
});
craftBtn.addEventListener("click", () => {
const key = String(recipe?.key || "");
if (!key || !(state?.benchId > 0)) return;
window.dispatchEvent(new CustomEvent("ui:requestCraftEnchant", {
detail: { benchId: state.benchId, recipe: key },
}));
});

row.appendChild(textWrap);
row.appendChild(craftBtn);
list.appendChild(row);
}

const hint = document.createElement("div");
hint.textContent = "Esc closes the bench.";
Object.assign(hint.style, {
marginTop: "10px",
opacity: "0.6",
fontSize: "11px",
textAlign: "center",
});
el.appendChild(hint);
}
4 changes: 4 additions & 0 deletions src/display/ui/inventoryOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ export function renderInventory(panel, items, ground, slotFilter = '', scrollOfI
return;
}
if (actionKey === 'use') {
if (getInventoryDefaultAction(it) === 'apply') {
triggerApplyForTool(it);
return;
}
if (!isInventoryItemUsable(it) || !Number.isInteger(it.id) || it.id <= 0) return;
signalPulse(); pulseRow(row, 'use', restoreBg);
if (it.type === 'potion') {
Expand Down
20 changes: 19 additions & 1 deletion src/display/ui/inventoryUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,33 @@ export function isInventoryItemUsable(it) {
|| it.type === 'tool';
}

/**
* @param {any} it
* @returns {boolean}
*/
export function canInventoryItemApply(it) {
return !!it && !!it.canApply && Number(it.applyTargetCount || 0) > 0;
}

/**
* @param {any} it
* @returns {boolean}
*/
export function shouldInventoryItemPreferApply(it) {
if (!canInventoryItemApply(it)) return false;
return it.type === 'scroll' || it.type === 'tool' || it.type === 'utility';
}

/**
* @param {any} it
* @returns {"none"|"apply"|"equip"|"use"|"set-spell"}
*/
export function getInventoryDefaultAction(it) {
if (!it) return 'none';
if (isInventoryItemEquippable(it)) return 'equip';
if (shouldInventoryItemPreferApply(it)) return 'apply';
if (isInventoryItemUsable(it)) return 'use';
if (it.canApply && Number(it.applyTargetCount || 0) > 0) return 'apply';
if (canInventoryItemApply(it)) return 'apply';
if (it.type === 'spell') return 'set-spell';
return 'none';
}
57 changes: 57 additions & 0 deletions src/display/ui/overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createDebugGraph } from './debugGraph.js';
import { createTileInspector } from './tileInspector.js';
import { renderAlchemyBench } from './alchemyBenchOverlay.js';
import { renderAnvil } from './anvilOverlay.js';
import { renderEnchantingBench } from './enchantingBenchOverlay.js';
import { renderCookingFire } from './cookingFireOverlay.js';
import { renderDialog } from './dialogOverlay.js';
import { playDeathJingle } from '../fx/deathJingle.js';
Expand Down Expand Up @@ -783,6 +784,62 @@ export function initOverlays() {
renderAlchemyBench(alchemy, _alchemyState);
});

// Enchanting bench overlay
let _enchantingState = {
benchId: 0,
ingredients: { emberRoot: 0, moonleaf: 0, thornPods: 0, venomFronds: 0, spiderLeg: 0, venomGland: 0, resin: 0, boneDust: 0, ectoplasm: 0, runeFragment: 0, frostCore: 0, beastClaw: 0, cursedThread: 0, oil: 0, water: 0, ashes: 0, gold: 0 },
recipes: [],
title: "✧ Enchanting Bench",
subtitle: "Bind reagents and gold into a scroll, then apply it to your gear.",
};
window.addEventListener('ui:openEnchantingBench', (ev) => {
/** @type {CustomEvent} */ // @ts-ignore
const e = ev;
const d = e?.detail || {};
_craftPanelMode = 'enchanting';
_enchantingState.benchId = Number(d.benchId || 0) | 0;
show(alchemy);
renderEnchantingBench(alchemy, _enchantingState);
const escKey = (/** @type {KeyboardEvent} */ ke) => {
if (alchemy.style.display !== 'block') return;
if (_craftPanelMode !== 'enchanting') return;
if (ke.key === 'Escape') {
window.dispatchEvent(new CustomEvent('ui:closeEnchantingBench'));
ke.preventDefault();
}
};
window.addEventListener('keydown', escKey);
const obs = new MutationObserver(() => {
if (alchemy.style.display === 'none') {
window.removeEventListener('keydown', escKey);
obs.disconnect();
}
});
obs.observe(alchemy, { attributes: true, attributeFilter: ['style'] });
});
window.addEventListener('ui:closeEnchantingBench', () => {
_enchantingState.benchId = 0;
if (_craftPanelMode === 'enchanting') {
_craftPanelMode = '';
hide(alchemy);
}
});
window.addEventListener('ui:enchantingBenchData', (ev) => {
/** @type {CustomEvent} */ // @ts-ignore
const e = ev;
const d = e?.detail || {};
_enchantingState = {
benchId: Number(d.benchId || _enchantingState.benchId || 0) | 0,
ingredients: d.ingredients && typeof d.ingredients === 'object'
? d.ingredients
: { emberRoot: 0, moonleaf: 0, thornPods: 0, venomFronds: 0, spiderLeg: 0, venomGland: 0, resin: 0, boneDust: 0, ectoplasm: 0, runeFragment: 0, frostCore: 0, beastClaw: 0, cursedThread: 0, oil: 0, water: 0, ashes: 0, gold: 0 },
recipes: Array.isArray(d.recipes) ? d.recipes : [],
title: String(d.title || _enchantingState.title || "✧ Enchanting Bench"),
subtitle: String(d.subtitle || _enchantingState.subtitle || "Bind reagents and gold into a scroll, then apply it to your gear."),
};
renderEnchantingBench(alchemy, _enchantingState);
});

// Anvil overlay
let _anvilState = {
anvilId: 0,
Expand Down
Loading
Loading