This commit is contained in:
33333-33333 2026-07-16 22:12:03 +09:00
commit 2cc2f003df
71 changed files with 3359 additions and 1027 deletions

View file

@ -20,6 +20,15 @@
return dialog;
}
function bindEditorClose(dialog, close, selectors = []) {
if (!dialog || typeof close !== "function") return;
for (const selector of selectors) {
const button = dialog.querySelector(selector);
if (button) button.onclick = close;
}
dialog.onclick = (event) => { if (event.target === dialog) close(); };
}
function ensureSignboardEditor() {
return ensureEditorDialog("signboardEditor", "signboard-editor hidden", `
<div class="signboard-editor-panel" role="dialog" aria-modal="true" aria-labelledby="signboardEditorTitle">
@ -139,9 +148,7 @@
}
if (apply) apply.onclick = () => commit(area?.value || "");
if (clear) clear.onclick = () => commit("");
if (cancel) cancel.onclick = close;
if (closeBtn) closeBtn.onclick = close;
dialog.onclick = (e) => { if (e.target === dialog) close(); };
bindEditorClose(dialog, close, ["#signboardEditorCancel", "#signboardEditorClose"]);
dialog.classList.remove("hidden");
syncCount();
return true;
@ -211,11 +218,9 @@
};
writeMask(api?.robotCleanerMask?.(robot) ?? robot.robotCleanerMask ?? defaultMask);
if (highSpeed) highSpeed.checked = Boolean(api?.robotCleanerHighSpeed?.(robot) ?? robot.robotCleanerHighSpeed);
if (closeBtn) closeBtn.onclick = close;
if (cancel) cancel.onclick = close;
bindEditorClose(dialog, close, ["#robotCleanerEditorClose", "#robotCleanerCancel"]);
if (def) def.onclick = () => { writeMask(defaultMask); if (highSpeed) highSpeed.checked = false; };
if (apply) apply.onclick = () => commit(readMask());
dialog.onclick = (e) => { if (e.target === dialog) close(); };
dialog.classList.remove("hidden");
return true;
}
@ -310,9 +315,7 @@
};
if (stop) stop.onclick = () => commit(true);
if (apply) apply.onclick = () => commit(false);
if (cancel) cancel.onclick = close;
if (closeBtn) closeBtn.onclick = close;
dialog.onclick = (e) => { if (e.target === dialog) close(); };
bindEditorClose(dialog, close, ["#fanEditorCancel", "#fanEditorClose"]);
dialog.classList.remove("hidden");
return true;
}
@ -445,9 +448,7 @@
global.render?.();
close();
};
if (cancel) cancel.onclick = close;
if (closeBtn) closeBtn.onclick = close;
dialog.onclick = (e) => { if (e.target === dialog) close(); };
bindEditorClose(dialog, close, ["#rotatorCancel", "#rotatorEditorClose"]);
controller.draw();
dialog.classList.remove("hidden");
return true;
@ -591,9 +592,7 @@
global.render?.();
close();
};
if (cancel) cancel.onclick = close;
if (closeBtn) closeBtn.onclick = close;
dialog.onclick = (e) => { if (e.target === dialog) close(); };
bindEditorClose(dialog, close, ["#reciprocatorCancel", "#reciprocatorEditorClose"]);
controller.draw();
dialog.classList.remove("hidden");
return true;
@ -659,13 +658,14 @@
fuseMax: Number.isFinite(Number(item.fuseMax)) ? Number(item.fuseMax) : null,
};
if (item.type === "signboard") data.text = item.text || "";
if (item.type === "circuit_board") data.circuitConfig = global.TarinaiCircuitBoardSystem?.serializeConfig?.(item) || null;
if (item.type === "gate_fence") data.gateOpen = Boolean(item.gateOpen);
if (item.type === "duplicator") { data.storedFoodType = item.storedFoodType || ""; data.storedFoodLabel = item.storedFoodLabel || ""; }
if (item.type === "robot_cleaner") { data.robotCleanerMask = Number(item.robotCleanerMask ?? 3) | 0; data.robotCleanerHighSpeed = Boolean(item.robotCleanerHighSpeed); }
if (item.type === "pressure_switch") {
data.pressureTarget = item.pressureTarget || "tarinai";
data.pressureComparator = item.pressureComparator === "lte" ? "lte" : "gte";
data.pressureThreshold = Math.max(-999, Math.min(999, Number(item.pressureThreshold ?? 1) || 0));
data.pressureMin = Math.max(-999, Math.min(999, Number(item.pressureMin ?? 1) || 0));
data.pressureMax = Math.max(-999, Math.min(999, Number(item.pressureMax ?? 300) || 0));
data.pressureWidth = Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0));
data.pressureHeight = Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0));
data.pressureTimeStart = Math.max(0, Math.min(1435, Number(item.pressureTimeStart ?? 360) | 0));
@ -725,6 +725,7 @@
item.fanBodyAngle = Number(data.fanBodyAngle ?? item.fanBodyAngle ?? item.angle ?? 0) || 0;
}
if (item.type === "signboard") item.text = String(data.text || "");
if (item.type === "circuit_board") global.TarinaiCircuitBoardSystem?.applySerializedConfig?.(item, data.circuitConfig || {});
if (item.type === "gate_fence") item.gateOpen = Boolean(data.gateOpen);
if (item.type === "duplicator") {
item.storedFoodType = String(data.storedFoodType || "");
@ -734,8 +735,9 @@
if (item.type === "pressure_switch") {
const targets = new Set(["tarinai", "item", "time", "hunger_avg", "stress_avg", "sleep_avg", "low_hp_count", "sick_count", "temperature", "zunchi_count", "ant_count", "water_count"]);
item.pressureTarget = targets.has(data.pressureTarget) ? data.pressureTarget : "tarinai";
item.pressureComparator = data.pressureComparator === "lte" ? "lte" : "gte";
item.pressureThreshold = Math.max(-999, Math.min(999, Number(data.pressureThreshold ?? 1) || 0));
item.pressureMin = Math.max(-999, Math.min(999, Number(data.pressureMin ?? 1) || 0));
item.pressureMax = Math.max(-999, Math.min(999, Number(data.pressureMax ?? 300) || 0));
if (item.pressureMin > item.pressureMax) { const swap = item.pressureMin; item.pressureMin = item.pressureMax; item.pressureMax = swap; }
item.pressureWidth = Math.max(60, Math.min(840, Number(data.pressureWidth || 220) | 0));
item.pressureHeight = Math.max(60, Math.min(840, Number(data.pressureHeight || 160) | 0));
item.pressureTimeStart = Math.max(0, Math.min(1435, Number(data.pressureTimeStart ?? 360) | 0));
@ -835,10 +837,17 @@
<div class="rotator-editor-titlebar"><h2 id="signalItemEditorTitle">\u691c\u77e5\u5668\u8a2d\u5b9a</h2><button id="signalItemEditorClose" class="rotator-editor-close" type="button">\u00d7</button></div>
<div class="rotator-editor-controls">
<label>\u691c\u77e5\u5bfe\u8c61 <select id="pressureSwitchTarget">${options}</select></label>
<label id="pressureSwitchComparatorRow">\u6bd4\u8f03 <select id="pressureSwitchComparator"><option value="gte">\u4ee5\u4e0a</option><option value="lte">\u4ee5\u4e0b</option></select></label>
<div id="pressureSwitchCountRow" class="pressure-numeric-control">
<div class="pressure-time-summary"><span>\u3057\u304d\u3044\u5024</span><output id="pressureSwitchCountLabel">1</output></div>
<input id="pressureSwitchCount" class="pressure-value-slider" type="range" min="0" max="300" step="1">
<div id="pressureSwitchCountRow" class="pressure-time-control">
<div class="pressure-time-summary"><span>\u4f5c\u52d5\u7bc4\u56f2</span><span><output id="pressureSwitchValueMinLabel">1</output><span aria-hidden="true"> \u301c </span><output id="pressureSwitchValueMaxLabel">300</output></span></div>
<div id="pressureSwitchValueRange" class="pressure-time-range">
<div id="pressureSwitchValueTrack" class="pressure-time-track" aria-hidden="true"></div>
<input id="pressureSwitchValueMin" class="pressure-time-thumb pressure-time-thumb-start" type="range" min="0" max="300" step="1" aria-label="\u4f5c\u52d5\u7bc4\u56f2\u306e\u4e0b\u9650">
<input id="pressureSwitchValueMax" class="pressure-time-thumb pressure-time-thumb-end" type="range" min="0" max="300" step="1" aria-label="\u4f5c\u52d5\u7bc4\u56f2\u306e\u4e0a\u9650">
</div>
<div class="pressure-number-inputs">
<label>\u4e0b\u9650 <input id="pressureSwitchValueMinNumber" type="number" inputmode="decimal"></label>
<label>\u4e0a\u9650 <input id="pressureSwitchValueMaxNumber" type="number" inputmode="decimal"></label>
</div>
</div>
<div id="pressureSwitchWidthRow" class="pressure-numeric-control pressure-spatial-control">
<div class="pressure-time-summary"><span>\u6a2a\u5e45</span><output id="pressureSwitchWidthLabel">220px</output></div>
@ -865,15 +874,19 @@
if (!item || item.type !== "pressure_switch") return false;
const dialog = ensureSignalItemEditor();
const target = dialog.querySelector("#pressureSwitchTarget");
const comparator = dialog.querySelector("#pressureSwitchComparator");
const count = dialog.querySelector("#pressureSwitchCount");
const countLabel = dialog.querySelector("#pressureSwitchCountLabel");
const valueRow = dialog.querySelector("#pressureSwitchCountRow");
const valueRange = dialog.querySelector("#pressureSwitchValueRange");
const valueTrack = dialog.querySelector("#pressureSwitchValueTrack");
const valueMin = dialog.querySelector("#pressureSwitchValueMin");
const valueMax = dialog.querySelector("#pressureSwitchValueMax");
const valueMinNumber = dialog.querySelector("#pressureSwitchValueMinNumber");
const valueMaxNumber = dialog.querySelector("#pressureSwitchValueMaxNumber");
const valueMinLabel = dialog.querySelector("#pressureSwitchValueMinLabel");
const valueMaxLabel = dialog.querySelector("#pressureSwitchValueMaxLabel");
const width = dialog.querySelector("#pressureSwitchWidth");
const widthLabel = dialog.querySelector("#pressureSwitchWidthLabel");
const height = dialog.querySelector("#pressureSwitchHeight");
const heightLabel = dialog.querySelector("#pressureSwitchHeightLabel");
const comparatorRow = dialog.querySelector("#pressureSwitchComparatorRow");
const countRow = dialog.querySelector("#pressureSwitchCountRow");
const widthRow = dialog.querySelector("#pressureSwitchWidthRow");
const heightRow = dialog.querySelector("#pressureSwitchHeightRow");
const timeRow = dialog.querySelector("#pressureSwitchTimeRow");
@ -885,13 +898,24 @@
const timeEndLabel = dialog.querySelector("#pressureSwitchTimeEndLabel");
const normalizeMinute = (value, fallback) => Math.max(0, Math.min(1435, Math.round((Number.isFinite(Number(value)) ? Number(value) : fallback) / 5) * 5));
const formatMinute = value => { const minute = normalizeMinute(value, 0); return `${String(Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`; };
const targetDef = () => DETECTOR_TARGETS[target.value] || DETECTOR_TARGETS.tarinai;
const normalizeValue = (value, fallback, def = targetDef()) => {
const min = Number(def.min ?? 0), max = Number(def.max ?? 100), step = Math.max(0.0001, Number(def.step ?? 1));
const numeric = Number.isFinite(Number(value)) ? Number(value) : fallback;
const rounded = Math.round((numeric - min) / step) * step + min;
return Math.max(min, Math.min(max, Number(rounded.toFixed(6))));
};
const formatDetectorValue = (value, def) => `${Number(value) || 0}${def?.unit || ""}`;
target.value = DETECTOR_TARGETS[item.pressureTarget] ? item.pressureTarget : "tarinai";
comparator.value = item.pressureComparator === "lte" ? "lte" : "gte";
count.value = String(Number(item.pressureThreshold ?? DETECTOR_TARGETS[target.value].threshold));
const initialDef = targetDef();
valueMin.value = String(normalizeValue(item.pressureMin, initialDef.threshold, initialDef));
valueMax.value = String(normalizeValue(item.pressureMax, initialDef.max, initialDef));
width.value = String(item.pressureWidth || 220);
height.value = String(item.pressureHeight || 160);
timeStart.value = String(normalizeMinute(item.pressureTimeStart, 360));
timeEnd.value = String(normalizeMinute(item.pressureTimeEnd, 1080));
const updateTimeRange = () => {
const start = normalizeMinute(timeStart.value, 360), end = normalizeMinute(timeEnd.value, 1080);
timeStart.value = String(start); timeEnd.value = String(end);
@ -903,8 +927,25 @@
: `linear-gradient(to right, ${active} 0%, ${active} ${endPct}%, ${idle} ${endPct}%, ${idle} ${startPct}%, ${active} ${startPct}%, ${active} 100%)`;
if (timeRange) timeRange.dataset.wraps = start > end ? "1" : "0";
};
const updateValueRange = source => {
const def = targetDef();
let low = normalizeValue(source === "min-number" ? valueMinNumber.value : valueMin.value, def.threshold, def);
let high = normalizeValue(source === "max-number" ? valueMaxNumber.value : valueMax.value, def.max, def);
if (low > high) {
if (source === "min" || source === "min-number") high = low;
else low = high;
}
valueMin.value = String(low); valueMax.value = String(high);
valueMinNumber.value = String(low); valueMaxNumber.value = String(high);
valueMinLabel.textContent = formatDetectorValue(low, def); valueMaxLabel.textContent = formatDetectorValue(high, def);
const span = Math.max(0.0001, Number(def.max) - Number(def.min));
const lowPct = (low - Number(def.min)) / span * 100, highPct = (high - Number(def.min)) / span * 100;
const idle = "rgba(94,112,128,0.22)", active = "rgba(67,158,218,0.78)";
valueTrack.style.background = `linear-gradient(to right, ${idle} 0%, ${idle} ${lowPct}%, ${active} ${lowPct}%, ${active} ${highPct}%, ${idle} ${highPct}%, ${idle} 100%)`;
valueRange.dataset.wraps = "0";
};
const updateOverlay = () => {
const def = DETECTOR_TARGETS[target.value] || DETECTOR_TARGETS.tarinai;
const def = targetDef();
if (!def.spatial) {
if (worldRef._pressureSwitchOverlay?.item === item) worldRef._pressureSwitchOverlay = null;
} else {
@ -912,13 +953,7 @@
}
global.render?.();
};
const formatDetectorValue = (value, def) => `${Number(value) || 0}${def?.unit || ""}`;
const updateNumericDisplays = () => {
const def = DETECTOR_TARGETS[target.value] || DETECTOR_TARGETS.tarinai;
const threshold = Math.max(Number(count.min), Math.min(Number(count.max), Number(count.value) || 0));
count.value = String(threshold);
countLabel.textContent = formatDetectorValue(threshold, def);
count.setAttribute("aria-valuetext", countLabel.textContent);
widthLabel.textContent = `${Number(width.value) || 0}px`;
heightLabel.textContent = `${Number(height.value) || 0}px`;
width.setAttribute("aria-valuetext", widthLabel.textContent);
@ -926,32 +961,59 @@
};
let previousTarget = target.value;
const updateMode = (targetChanged = false) => {
const def = DETECTOR_TARGETS[target.value] || DETECTOR_TARGETS.tarinai;
count.min = String(def.min ?? 0);
count.max = String(def.max ?? 100);
count.step = String(def.step ?? 1);
if (targetChanged && previousTarget !== target.value) count.value = String(def.threshold);
const def = targetDef();
for (const control of [valueMin, valueMax, valueMinNumber, valueMaxNumber]) {
control.min = String(def.min ?? 0); control.max = String(def.max ?? 100); control.step = String(def.step ?? 1);
}
if (targetChanged && previousTarget !== target.value) {
valueMin.value = String(def.threshold);
valueMax.value = String(def.max);
}
previousTarget = target.value;
comparatorRow.hidden = Boolean(def.time);
countRow.hidden = Boolean(def.time);
valueRow.hidden = Boolean(def.time);
widthRow.hidden = !def.spatial;
heightRow.hidden = !def.spatial;
timeRow.hidden = !def.time;
updateNumericDisplays(); updateTimeRange(); updateOverlay();
updateValueRange(); updateNumericDisplays(); updateTimeRange(); updateOverlay();
};
count.oninput = updateNumericDisplays;
valueMin.oninput = () => updateValueRange("min");
valueMax.oninput = () => updateValueRange("max");
valueMinNumber.oninput = () => updateValueRange("min-number");
valueMaxNumber.oninput = () => updateValueRange("max-number");
width.oninput = () => { updateNumericDisplays(); updateOverlay(); };
height.oninput = () => { updateNumericDisplays(); updateOverlay(); };
timeStart.oninput = updateTimeRange; timeEnd.oninput = updateTimeRange;
const bindNearestThumbTeleport = (range, first, second, normalize, update) => {
if (!range || !first || !second) return;
range.onpointerdown = event => {
if (event.button !== 0 || event.target === first || event.target === second) return;
const rect = range.getBoundingClientRect();
if (!rect.width) return;
event.preventDefault();
const min = Number(first.min || 0), max = Number(first.max || 100);
const ratio = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));
const raw = min + (max - min) * ratio;
const next = normalize(raw);
const firstValue = Number(first.value), secondValue = Number(second.value);
const targetThumb = Math.abs(next - firstValue) <= Math.abs(next - secondValue) ? first : second;
targetThumb.value = String(next);
update(targetThumb === first ? "min" : "max");
targetThumb.focus({ preventScroll: true });
};
};
bindNearestThumbTeleport(valueRange, valueMin, valueMax, value => normalizeValue(value, targetDef().threshold, targetDef()), source => updateValueRange(source));
bindNearestThumbTeleport(timeRange, timeStart, timeEnd, value => normalizeMinute(value, 0), updateTimeRange);
target.onchange = () => updateMode(true);
updateMode(false);
const close = () => { dialog.classList.add("hidden"); if (worldRef._pressureSwitchOverlay?.item === item) worldRef._pressureSwitchOverlay = null; global.render?.(); };
dialog.querySelector("#signalItemEditorApply").onclick = () => {
global.TarinaiHistory.capture(worldRef, "detector-edit");
item.pressureTarget = DETECTOR_TARGETS[target.value] ? target.value : "tarinai";
item.pressureComparator = comparator.value === "lte" ? "lte" : "gte";
const targetDef = DETECTOR_TARGETS[item.pressureTarget] || DETECTOR_TARGETS.tarinai;
item.pressureThreshold = Math.max(Number(targetDef.min ?? 0), Math.min(Number(targetDef.max ?? 100), Number(count.value || 0)));
const def = targetDef();
item.pressureMin = normalizeValue(valueMin.value, def.threshold, def);
item.pressureMax = normalizeValue(valueMax.value, def.max, def);
if (item.pressureMin > item.pressureMax) { const swap = item.pressureMin; item.pressureMin = item.pressureMax; item.pressureMax = swap; }
item.pressureWidth = Math.max(60, Math.min(840, Number(width.value || 220) | 0));
item.pressureHeight = Math.max(60, Math.min(840, Number(height.value || 160) | 0));
item.pressureTimeStart = normalizeMinute(timeStart.value, 360);
@ -966,7 +1028,7 @@
}
function isEditableItem(item) {
return item && !item.dead && (item.type === "signboard" || item.type === "robot_cleaner" || item.type === "gate_fence" || item.type === "rotator" || item.type === "poison_block" || item.type === "reciprocator" || item.type === "fan" || item.type === "pressure_switch");
return item && !item.dead && (item.type === "signboard" || item.type === "robot_cleaner" || item.type === "gate_fence" || item.type === "rotator" || item.type === "poison_block" || item.type === "reciprocator" || item.type === "fan" || item.type === "pressure_switch" || item.type === "circuit_board");
}
function findEditableItemAt(worldRef, x, y) {
@ -995,6 +1057,7 @@
if (item.type === "reciprocator") return openReciprocatorEditor(item, worldRef);
if (item.type === "fan") return openFanEditor(item, worldRef);
if (item.type === "pressure_switch") return openSignalItemEditor(item, worldRef);
if (item.type === "circuit_board") return global.TarinaiCircuitBoardSystem?.openEditor?.(item, worldRef) || false;
if (item.type === "gate_fence") {
item.gateOpen = !item.gateOpen;
worldRef?.markSpatialDirty?.("gate-toggle");
@ -1474,6 +1537,9 @@
return;
}
}
if (!found && !this.findDeleteToolTargetAt?.(x, y)) {
global.TarinaiAchievements?.recordEmptyClick?.({ world: this, x, y });
}
const wasSelectionEmpty = !this.selected;
if (found && wasSelectionEmpty) this._scrollSelectedDataTopOnNextSelection = true;
else this._scrollSelectedDataTopOnNextSelection = false;