From 71f0f381de64e045f91da19450adacf10fbc7902 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 8 Jul 2026 01:22:38 -0400 Subject: feat: multi-tool gather objects correctly handled. hide more irrelevant fishing fields. --- internal/admin/static/objecteditor.js | 134 ++++++++++++++++++++++++++++++---- internal/behavior/behavior.go | 27 +++---- internal/behavior/types.go | 2 + internal/game/act_gather.go | 76 ++++++++++++++----- internal/game/core_production.go | 10 +-- internal/validate/checks.go | 38 ++++++++-- 6 files changed, 228 insertions(+), 59 deletions(-) (limited to 'internal') diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index fdaefd2..29ad66b 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -45,9 +45,10 @@ var SECTIONS = [ ['base', 'Base', 'number', '0.5', 'narrow'], ['per_level', 'Per Lvl', 'number', '0.01', 'narrow'], ['cap', 'Cap', 'number', '0.95', 'narrow'] ]}, {label:'Other', row:0, fields:[ - ['respawn_timer', 'Respawn', 'number', '50', 'narrow'], + ['respawn_timer', 'Respawn', 'number', '50', 'narrow', function(d) { return d.skill !== 'fishing'; }], ['deplete_timer', 'Deplete', 'number', '45', 'narrow', function(d) { return d.skill === 'woodcutting'; }], ['nest_chance', 'Nest 1/n', 'number', '256', 'narrow', function(d) { return d.skill === 'woodcutting'; }], + ['no_tool_speed', 'Bare Speed', 'number', '3', 'narrow', function(d) { return !d.tools || d.tools.length === 0; }], ]}, {label:'Tools', key:'tools', type:'tools_checkbox', hint:'any one is sufficient (OR)'}, ], @@ -55,7 +56,7 @@ var SECTIONS = [ {label:'Drops', key:'drops', fields:[ ['item_id', 'Item ID', 'search', 'copper_ore', '', 'items'], ['table', 'Table', 'search', 'gem_table', '', 'drops'], ['weight', 'Weight', 'number', '90', 'narrow'], ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '17', 'narrow'], - ['quantity', 'Quantity', 'number', '1', 'narrow'], ['depletes', 'Depletes node', 'checkbox'], + ['quantity', 'Quantity', 'number', '1', 'narrow'], ['tool', 'Tool', 'select'], ['depletes', 'Depletes node', 'checkbox'], ['message', 'Message', 'text', 'You manage to mine some copper ore.'], ]} ] @@ -74,7 +75,7 @@ var SECTIONS = [ ['respawn_on_hide', 'Respawn on hide', 'checkbox'], ], subtables: [ - {label:'Levels of Decay', key:'levels', fields:[ + {label:'Levels of Decay (lower is more decayed)', key:'levels', vertical: true, fields:[ ['message', 'Look Message', 'text'], ['degrade_message', 'Degrade Message', 'text'] ]} ] @@ -93,7 +94,7 @@ var SECTIONS = [ id: 'on_look', label: 'On Look', labelHint: '(what happens when you "look ")', path: 'on_look', detect: function(d) { return d.on_look; }, fields: [ - ['set_flags', 'Set Flags', 'json', '{"flag":"value"}'], + ['set_flags', 'Set Global Flags', 'json', '{"flag":"value"}'], ['set_player_flags', 'Set Player Flags', 'json', '{"pflag":"value"}'], ['give_item', 'Give Item', 'search', 'e.g. keycard', '', null, 'items'], ['take_item', 'Take Item', 'search', 'e.g. bomb', '', null, 'items'], @@ -233,6 +234,28 @@ function renderCard(sec, data) { } else { h += '
'; sec.fields.forEach(function(f) { + if (sec.id === 'gather' && f[0] === 'bait') { + window._gatherVis = window._gatherVis || {}; + var gd = objectData.gather || {}; + var isFishing = gd.skill === 'fishing'; + var showBait = isFishing && (window._gatherVis.bait || (gd.bait && gd.bait !== '')); + if (isFishing && showBait) { + var bv = gd.bait || ''; + h += ''; + } else if (isFishing) { + h += ''; + } + return; + } h += renderField(sec, f, data, -1); }); h += '
'; @@ -497,6 +520,27 @@ function renderOnLookCard(data) { h += renderField(sec, sec.fields[5], data, -1); h += renderField(sec, sec.fields[6], data, -1); h += ''; + var helpCollapsed = window._onLookHelpCollapsed === undefined ? true : window._onLookHelpCollapsed; + h += '
'; + h += '
'; + h += '' + (helpCollapsed ? '▶' : '▼') + ''; + h += 'How Set Global Flags & Set Player Flags Work'; + h += '
'; + if (!helpCollapsed) { + h += '
'; + h += 'When a player looks at this object, these fields apply effects.

'; + h += 'Set Global Flags — Sets world flags shared by all players. JSON object mapping flag names to values:
'; + h += '  {"gate_open":true,"boss_summoned":false}
'; + h += '  Flags persist until changed by another on_look, room script, or interact action.

'; + h += 'Set Player Flags — Sets per-character flags saved to the player\'s YAML. JSON object mapping flag names to values:
'; + h += '  {"has_seen_cave":true,"dialog_spoke":1}
'; + h += '  These flags can be used in conditions (player_flag) to track player progress.

'; + h += 'Other FieldsGive Item grants an item, Take Item removes one, '; + h += 'Teleport sends player to a room, Heal restores HP, Credits adds/removes credits (use negative for cost).

'; + h += 'All effects fire simultaneously when the player looks at this object.'; + h += '
'; + } + h += '
'; return h; } @@ -602,10 +646,15 @@ function renderSubtable(sec, st, data) { arr = sectionRoot[st.key] || []; } var isDrops = sec.id === 'gather' && st.key === 'drops'; + var isVertical = st.vertical; + var gatherTools = []; + if (isDrops) { + gatherTools = (sectionRoot.tools || []).filter(function(t) { return t && t !== ''; }); + } var h = '
'; h += '
' + esc(st.label) + '
'; arr.forEach(function(item, idx) { - h += '
'; + h += '
'; if (isDrops) { if (!st.single) { h += ''; @@ -628,7 +677,21 @@ function renderSubtable(sec, st, data) { var dp = cardPath(sec) + '.' + st.key + '[' + idx + '].' + f[0]; h += ''; } else if (f[2] === 'checkbox') { - h += ''; + var hideStyle = ''; + if (isDrops && f[0] === 'depletes' && sectionRoot.skill === 'fishing') { + hideStyle = ' style="display:none"'; + } + h += ''; + } else if (f[2] === 'select' && f[0] === 'tool') { + if (gatherTools.length >= 2) { + var dpTool = cardPath(sec) + '.' + st.key + '[' + idx + '].' + f[0]; + h += ''; + } } else { h += ''; } @@ -641,6 +704,9 @@ function renderSubtable(sec, st, data) { h += ''; h += '
'; } else { + if (isVertical && !st.single) { + h += ''; + } st.fields.forEach(function(f) { var fid = fieldIDSub(sec, st.key, idx, f[0]); var val = item[f[0]]; @@ -659,7 +725,7 @@ function renderSubtable(sec, st, data) { h += ''; } }); - if (!st.single) { + if (!st.single && !isVertical) { h += ''; } } @@ -718,7 +784,7 @@ function renderToolsCheckbox(sec, il, data) { h += '
'; arr.forEach(function(t, idx) { h += '
'; - h += ''; choices.forEach(function(c) { var sel = c === t ? ' selected' : ''; h += '' + esc(c) + ''; @@ -738,14 +804,18 @@ function addToolRow(secID, key) { if (!sec) return; var p = cardPath(sec); if (!objectData[p]) objectData[p] = {}; - objectData[p][key] = (objectData[p][key] || []).concat(['']); - renderCurrent(); + ced_syncDOMToData(objectData); + syncGatherToolsFromDOM(); + var val = (toolTypes && toolTypes.length > 0) ? toolTypes[0] : ''; + objectData[p][key] = (objectData[p][key] || []).concat([val]); + renderObjectEditor(objectID, objectData); } function removeToolRow(secID, key, idx) { var sec = SECTIONS.find(function(s) { return s.id === secID; }); if (!sec) return; ced_syncDOMToData(objectData); + syncGatherToolsFromDOM(); var p = cardPath(sec); var arr = objectData[p] && objectData[p][key]; if (!arr) return; @@ -764,6 +834,19 @@ function fetchToolTypes() { }); } +function syncGatherToolsFromDOM() { + if (!objectData || !objectData.gather) return; + var selects = document.querySelectorAll('.obj-tool-select'); + if (selects.length === 0) return; + var tools = []; + selects.forEach(function(s) { if (s.value) tools.push(s.value); }); + if (tools.length > 0) { + objectData.gather.tools = tools; + } else if (objectData.gather.tools && objectData.gather.tools.length > 0) { + delete objectData.gather.tools; + } +} + function toggleCard(id) { expandedCards[id] = expandedCards[id] === false ? true : false; renderCurrent(); @@ -774,6 +857,20 @@ function toggleCardWidth(id) { renderCurrent(); } +function showBaitField() { + window._gatherVis = window._gatherVis || {}; + window._gatherVis.bait = true; + renderCurrent(); +} + +function hideBaitField() { + window._gatherVis = window._gatherVis || {}; + window._gatherVis.bait = false; + ced_syncDOMToData(objectData); + if (objectData.gather) objectData.gather.bait = ''; + renderObjectEditor(objectID, objectData); +} + function toggleSafespotHelp() { if (window._safespotHelpCollapsed === undefined) { window._safespotHelpCollapsed = false; @@ -884,7 +981,7 @@ function addDropRow(cardID, subKey, dropType) { if (!sec) return; var sectionRoot = objectData[sec.path || ''] || objectData; var arr = sectionRoot[subKey] || []; - var empty = { item_id: '', table: '', weight: 0, level: 0, xp: 0, quantity: 0, depletes: false, message: '', _type: (dropType === 'table' ? 't' : 'i') }; + var empty = { item_id: '', table: '', weight: 0, level: 0, xp: 0, quantity: 0, depletes: false, tool: '', message: '', _type: (dropType === 'table' ? 't' : 'i') }; arr.push(empty); sectionRoot[subKey] = arr; renderCurrent(); @@ -896,6 +993,7 @@ function validateDropRow(idx) { function renderCurrent() { ced_syncDOMToData(objectData); + syncGatherToolsFromDOM(); if (!objectData || !objectID) return; renderObjectEditor(objectID, objectData); } @@ -961,13 +1059,22 @@ function toggleUseInterHelp() { renderCurrent(); } +function toggleOnLookHelp() { + if (window._onLookHelpCollapsed === undefined) { + window._onLookHelpCollapsed = false; + } else { + window._onLookHelpCollapsed = !window._onLookHelpCollapsed; + } + renderCurrent(); +} + function toggleGatherConditionals() { var skillEl = document.getElementById('f_gather_skill'); var skill = skillEl ? skillEl.value : ''; var fishing = skill === 'fishing'; var wood = skill === 'woodcutting'; - var els = ['bait', 'exhausted_message', 'depleted_message', 'deplete_timer', 'nest_chance', 'respawn_broadcast']; - var vis = {bait: fishing, exhausted_message: wood, depleted_message: !fishing, deplete_timer: wood, nest_chance: wood, respawn_broadcast: !fishing}; + var els = ['exhausted_message', 'depleted_message', 'deplete_timer', 'nest_chance', 'respawn_broadcast', 'respawn_timer']; + var vis = {exhausted_message: wood, depleted_message: !fishing, deplete_timer: wood, nest_chance: wood, respawn_broadcast: !fishing, respawn_timer: !fishing}; els.forEach(function(key) { var el = document.getElementById('f_gather_' + key); if (el) el.closest('.obj-field').style.display = vis[key] ? '' : 'none'; @@ -975,6 +1082,7 @@ function toggleGatherConditionals() { document.querySelectorAll('[id^="f_gather_drops_"][id$="_depletes"]').forEach(function(el) { el.closest('.obj-field').style.display = fishing ? 'none' : ''; }); + renderCurrent(); } function setupSearchFields() { ced_setupSearchFields(); } diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go index ec9fc7c..d31a0a5 100644 --- a/internal/behavior/behavior.go +++ b/internal/behavior/behavior.go @@ -1,20 +1,21 @@ package behavior type GatherConfig struct { - Skill string `yaml:"skill"` - Tools []string `yaml:"tools"` - Bait string `yaml:"bait"` + Skill string `yaml:"skill"` + Tools []string `yaml:"tools"` + NoToolSpeed float64 `yaml:"no_tool_speed,omitempty"` + Bait string `yaml:"bait"` Success SuccessFormula `yaml:"success"` - GatherMessage string `yaml:"gather_message"` - DepletedMessage string `yaml:"depleted_message"` - ExhaustedMessage string `yaml:"exhausted_message"` - FailMessage string `yaml:"fail_message"` - Drops []DropEntry `yaml:"drops"` - RespawnTimer float64 `yaml:"respawn_timer"` - RespawnBroadcast string `yaml:"respawn_broadcast"` - DepleteTimer float64 `yaml:"deplete_timer"` - NestChance int `yaml:"nest_chance"` - BroadcastMessage string `yaml:"broadcast_message"` + GatherMessage string `yaml:"gather_message"` + DepletedMessage string `yaml:"depleted_message"` + ExhaustedMessage string `yaml:"exhausted_message"` + FailMessage string `yaml:"fail_message"` + Drops []DropEntry `yaml:"drops"` + RespawnTimer float64 `yaml:"respawn_timer"` + RespawnBroadcast string `yaml:"respawn_broadcast"` + DepleteTimer float64 `yaml:"deplete_timer"` + NestChance int `yaml:"nest_chance"` + BroadcastMessage string `yaml:"broadcast_message"` } type SuccessFormula struct { diff --git a/internal/behavior/types.go b/internal/behavior/types.go index c43a0d9..7d95cc3 100644 --- a/internal/behavior/types.go +++ b/internal/behavior/types.go @@ -79,6 +79,7 @@ type GatherData struct { Step int Verb string ToolName string + ToolType string } type ProductionData struct { @@ -213,6 +214,7 @@ type TriggerModuleData struct { type DropEntry struct { ItemID string `yaml:"item_id"` Table string `yaml:"table"` + Tool string `yaml:"tool,omitempty"` Weight int `yaml:"weight"` Quantity int `yaml:"quantity"` Depletes bool `yaml:"depletes"` diff --git a/internal/game/act_gather.go b/internal/game/act_gather.go index f7a63f6..8743a46 100644 --- a/internal/game/act_gather.go +++ b/internal/game/act_gather.go @@ -31,24 +31,6 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje return } - skillLevel := p.Level(player.SkillName(cfg.Skill)) - - eligible := filterDropsByLevel(cfg.Drops, skillLevel) - if len(eligible) == 0 { - minLevel := 0 - for _, d := range cfg.Drops { - if d.Level > 0 && (minLevel == 0 || d.Level < minLevel) { - minLevel = d.Level - } - } - if minLevel > 0 { - sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", minLevel, cfg.Skill)) - } else { - sess.WriteLine("You are not skilled enough to do that.") - } - return - } - if st.Depleted { if cfg.DepletedMessage != "" { msg := cfg.DepletedMessage @@ -65,10 +47,10 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje } var wait float64 - var toolName string + var toolName, toolType string if len(cfg.Tools) > 0 { - toolSpeed, name, found := g.findTool(p, cfg.Tools) + toolSpeed, name, tType, found := g.findTool(p, cfg.Tools) if !found { names := make([]string, len(cfg.Tools)) for i, t := range cfg.Tools { @@ -79,10 +61,43 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje return } toolName = name + toolType = tType wait = toolSpeed if wait < 1 { wait = 1 } + } else { + wait = cfg.NoToolSpeed + if wait <= 0 { + wait = 3 + } + } + + skillLevel := p.Level(player.SkillName(cfg.Skill)) + + toolEligible := filterDropsByTool(cfg.Drops, toolType, len(cfg.Tools)) + eligible := filterDropsByLevel(toolEligible, skillLevel) + if len(eligible) == 0 { + if len(toolEligible) == 0 { + if len(cfg.Tools) > 0 { + sess.WriteLine("Your tool can't yield anything from this object.") + } else { + sess.WriteLine("Your skill can't yield anything from this object.") + } + } else { + minLevel := 0 + for _, d := range toolEligible { + if d.Level > 0 && (minLevel == 0 || d.Level < minLevel) { + minLevel = d.Level + } + } + if minLevel > 0 { + sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", minLevel, cfg.Skill)) + } else { + sess.WriteLine("You are not skilled enough to do that.") + } + } + return } if cfg.Bait != "" { @@ -120,6 +135,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje Wait: wait, Verb: verb, ToolName: toolName, + ToolType: toolType, } if cfg.DepleteTimer > 0 { d.DepleteTimer = true @@ -198,6 +214,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { if rand.Float64() < chance { eligible := filterDropsByLevel(cfg.Drops, skillLevel) + eligible = filterDropsByTool(eligible, d.ToolType, len(cfg.Tools)) drop := behavior.ResolveDrop(g.DataDir, eligible) if drop != nil { freeSlot := p.FirstFreeSlot() @@ -321,6 +338,25 @@ func filterDropsByLevel(drops []behavior.DropEntry, level int) []behavior.DropEn return eligible } +func filterDropsByTool(drops []behavior.DropEntry, activeToolType string, toolCount int) []behavior.DropEntry { + if toolCount <= 1 { + out := make([]behavior.DropEntry, 0, len(drops)) + for _, d := range drops { + if d.Tool == "" || d.Tool == activeToolType { + out = append(out, d) + } + } + return out + } + out := make([]behavior.DropEntry, 0, len(drops)) + for _, d := range drops { + if d.Tool == activeToolType { + out = append(out, d) + } + } + return out +} + func (g *Game) depleteSharedTree(st *world.ObjState, cfg *behavior.GatherConfig, targetName string, playerNames []string) { st.Depleted = true st.DepleteTimer = float64(engine.ToTicks(cfg.RespawnTimer)) diff --git a/internal/game/core_production.go b/internal/game/core_production.go index e88f512..192f8ec 100644 --- a/internal/game/core_production.go +++ b/internal/game/core_production.go @@ -6,13 +6,13 @@ import ( "thehouseoficarus/internal/player" ) -func (g *Game) findTool(p *player.Player, toolTypes []string) (toolSpeed float64, toolName string, found bool) { +func (g *Game) findTool(p *player.Player, toolTypes []string) (toolSpeed float64, toolName string, toolType string, found bool) { toolSpeed = -1 if itemID, ok := p.Equipment[item.SlotMainHand]; ok { if def, err := g.ItemStore.Load(itemID); err == nil { for _, t := range toolTypes { if def.ToolType() == t { - return def.ToolSpeed(), def.Name, true + return def.ToolSpeed(), def.Name, t, true } } } @@ -28,15 +28,15 @@ func (g *Game) findTool(p *player.Player, toolTypes []string) (toolSpeed float64 } for _, t := range toolTypes { if def.ToolType() == t { - return def.ToolSpeed(), def.Name, true + return def.ToolSpeed(), def.Name, t, true } } } - return -1, "", false + return -1, "", "", false } func (g *Game) hasToolType(p *player.Player, toolType string) bool { - _, _, found := g.findTool(p, []string{toolType}) + _, _, _, found := g.findTool(p, []string{toolType}) return found } diff --git a/internal/validate/checks.go b/internal/validate/checks.go index 360f97c..7cb5914 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -1113,14 +1113,6 @@ func validateTechs(s Source) []Issue { func validateGather(prefix string, cfg *behavior.GatherConfig, itemIDs map[string]bool, dropIDs map[string]bool) []Issue { var issues []Issue - if len(cfg.Tools) == 0 { - issues = append(issues, Issue{ - Level: "ERROR", - Type: "config", - Message: fmt.Sprintf("%s: gather must have at least one tool", prefix), - }) - } - if cfg.Bait != "" && !itemIDs[cfg.Bait] { issues = append(issues, Issue{ Level: "ERROR", @@ -1147,11 +1139,41 @@ func validateGather(prefix string, cfg *behavior.GatherConfig, itemIDs map[strin prefix, d.Table), }) } + if d.Tool != "" { + if !toolInList(cfg.Tools, d.Tool) { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("%s: drop tool %q is not in the object's tools list", + prefix, d.Tool), + }) + } + } else if len(cfg.Tools) >= 2 { + label := d.ItemID + if label == "" { + label = "" + } + issues = append(issues, Issue{ + Level: "ERROR", + Type: "config", + Message: fmt.Sprintf("%s: drop %q has no tool; required because object has %d tools", + prefix, label, len(cfg.Tools)), + }) + } } return issues } +func toolInList(list []string, target string) bool { + for _, t := range list { + if t == target { + return true + } + } + return false +} + func validateTalkConfig(prefix string, cfg *behavior.TalkConfig, itemIDs map[string]bool, roomIndex map[int]bool) []Issue { var issues []Issue if cfg == nil { -- cgit v1.2.3