diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/admin/static/objecteditor.js | 134 | ||||
| -rw-r--r-- | internal/behavior/behavior.go | 27 | ||||
| -rw-r--r-- | internal/behavior/types.go | 2 | ||||
| -rw-r--r-- | internal/game/act_gather.go | 76 | ||||
| -rw-r--r-- | internal/game/core_production.go | 10 | ||||
| -rw-r--r-- | internal/validate/checks.go | 38 |
6 files changed, 228 insertions, 59 deletions
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 <this>")', 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 += '<div class="obj-card-grid">'; 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 += '<label class="obj-field obj-search">'; + h += '<span>Bait Item</span>'; + h += '<div style="display:flex;gap:4px;align-items:flex-start">'; + h += '<div style="flex:1;position:relative">'; + h += '<input id="f_gather_bait" value="' + escAttr(bv) + '" class="search-input" data-search-type="items" data-field-path="gather.bait" placeholder="fishing_bait">'; + h += '<div class="search-results" style="display:none"></div>'; + h += '</div>'; + h += '<button class="btn btn-sm btn-danger" onclick="hideBaitField()" style="flex-shrink:0;padding:3px 7px;font-size:10px;margin-top:0">X</button>'; + h += '</div>'; + h += '</label>'; + } else if (isFishing) { + h += '<button class="btn btn-sm" style="background:var(--accent);color:var(--text);border:1px solid #1a5a8e;white-space:nowrap;width:100%;padding:4px 6px;font-size:11px;font-family:monospace" onclick="showBaitField()">+ Add Required Bait</button>'; + } + return; + } h += renderField(sec, f, data, -1); }); h += '</div>'; @@ -497,6 +520,27 @@ function renderOnLookCard(data) { h += renderField(sec, sec.fields[5], data, -1); h += renderField(sec, sec.fields[6], data, -1); h += '</div>'; + var helpCollapsed = window._onLookHelpCollapsed === undefined ? true : window._onLookHelpCollapsed; + h += '<div style="margin-top:8px;border:1px solid rgba(100,160,255,.15);border-radius:4px;overflow:hidden">'; + h += '<div style="padding:6px 10px;background:rgba(100,160,255,.08);cursor:pointer;font-size:10px;color:#6af;font-weight:bold;text-transform:uppercase;letter-spacing:.5px" onclick="toggleOnLookHelp()">'; + h += '<span style="font-size:8px;margin-right:4px">' + (helpCollapsed ? '▶' : '▼') + '</span>'; + h += 'How Set Global Flags & Set Player Flags Work'; + h += '</div>'; + if (!helpCollapsed) { + h += '<div style="padding:8px;background:rgba(100,160,255,.05);font-size:10px;color:#aaa;line-height:1.6">'; + h += 'When a player looks at this object, these fields apply effects.<br><br>'; + h += '<b>Set Global Flags</b> — Sets world flags shared by all players. JSON object mapping flag names to values:<br>'; + h += ' <code>{"gate_open":true,"boss_summoned":false}</code><br>'; + h += ' Flags persist until changed by another on_look, room script, or interact action.<br><br>'; + h += '<b>Set Player Flags</b> — Sets per-character flags saved to the player\'s YAML. JSON object mapping flag names to values:<br>'; + h += ' <code>{"has_seen_cave":true,"dialog_spoke":1}</code><br>'; + h += ' These flags can be used in conditions (<code>player_flag</code>) to track player progress.<br><br>'; + h += '<b>Other Fields</b> — <code>Give Item</code> grants an item, <code>Take Item</code> removes one, '; + h += '<code>Teleport</code> sends player to a room, <code>Heal</code> restores HP, <code>Credits</code> adds/removes credits (use negative for cost).<br><br>'; + h += 'All effects fire simultaneously when the player looks at this object.'; + h += '</div>'; + } + h += '</div>'; 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 = '<div class="obj-subtable">'; h += '<div class="obj-subtable-header">' + esc(st.label) + '</div>'; arr.forEach(function(item, idx) { - h += '<div class="obj-sub-row"' + (isDrops ? ' style="flex-direction:column;align-items:stretch;position:relative"' : '') + '>'; + h += '<div class="obj-sub-row"' + (isDrops || isVertical ? ' style="flex-direction:column;align-items:stretch;position:relative"' : '') + '>'; if (isDrops) { if (!st.single) { h += '<button class="btn btn-sm btn-danger" style="position:absolute;top:4px;right:6px" onclick="removeSubRow(\'' + sec.id + '\',\'' + st.key + '\',' + idx + ')">X</button>'; @@ -628,7 +677,21 @@ function renderSubtable(sec, st, data) { var dp = cardPath(sec) + '.' + st.key + '[' + idx + '].' + f[0]; h += '<label class="obj-field obj-search' + extraCls + '"><span>' + esc(f[1]) + '</span><div style="position:relative;width:100%"><input id="' + fid + '" value="' + escAttr(String(val)) + '" class="search-input" data-search-type="' + entity + '" data-field-path="' + escAttr(dp) + '"' + ph + '><div class="search-results" style="display:none"></div></div></label>'; } else if (f[2] === 'checkbox') { - h += '<label class="obj-field obj-check' + extraCls + '"><input type="checkbox" id="' + fid + '"' + (val ? ' checked' : '') + '> <span>' + esc(f[1]) + '</span></label>'; + var hideStyle = ''; + if (isDrops && f[0] === 'depletes' && sectionRoot.skill === 'fishing') { + hideStyle = ' style="display:none"'; + } + h += '<label class="obj-field obj-check' + extraCls + '"' + hideStyle + '><input type="checkbox" id="' + fid + '"' + (val ? ' checked' : '') + '> <span>' + esc(f[1]) + '</span></label>'; + } else if (f[2] === 'select' && f[0] === 'tool') { + if (gatherTools.length >= 2) { + var dpTool = cardPath(sec) + '.' + st.key + '[' + idx + '].' + f[0]; + h += '<label class="obj-field obj-narrow"><span>' + esc(f[1]) + '</span>' + + '<select id="' + fid + '" data-field-path="' + escAttr(dpTool) + '">'; + gatherTools.forEach(function(t) { + h += '<option value="' + escAttr(t) + '"' + (String(val) === t ? ' selected' : '') + '>' + esc(t) + '</option>'; + }); + h += '</select></label>'; + } } else { h += '<label class="obj-field' + extraCls + '"><span>' + esc(f[1]) + '</span><input id="' + fid + '" value="' + escAttr(String(val)) + '" placeholder="' + escAttr(f[3] || '') + '"></label>'; } @@ -641,6 +704,9 @@ function renderSubtable(sec, st, data) { h += '<label class="obj-field obj-grow"><span>' + esc(msgField ? msgField[1] : 'Message') + '</span><input id="' + fidMsg + '" value="' + escAttr(String(msgVal)) + '" placeholder="' + escAttr(msgField ? msgField[3] : '') + '"></label>'; h += '</div>'; } else { + if (isVertical && !st.single) { + h += '<button class="btn btn-sm btn-danger" style="position:absolute;top:4px;right:6px" onclick="removeSubRow(\'' + sec.id + '\',\'' + st.key + '\',' + idx + ')">X</button>'; + } 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 += '<label class="obj-field' + extraCls + '"><span>' + esc(f[1]) + '</span><input id="' + fid + '" value="' + escAttr(String(val)) + '"></label>'; } }); - if (!st.single) { + if (!st.single && !isVertical) { h += '<button class="btn btn-sm btn-danger obj-sub-remove" onclick="removeSubRow(\'' + sec.id + '\',\'' + st.key + '\',' + idx + ')">X</button>'; } } @@ -718,7 +784,7 @@ function renderToolsCheckbox(sec, il, data) { h += '<div style="display:flex;flex-direction:column;gap:3px;margin-top:4px">'; arr.forEach(function(t, idx) { h += '<div style="display:flex;gap:4px">'; - h += '<select class="obj-tool-select" style="flex:1;font-size:12px;padding:2px 4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:3px">'; + h += '<select class="obj-tool-select" onchange="renderCurrent()" style="flex:1;font-size:12px;padding:2px 4px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:3px">'; choices.forEach(function(c) { var sel = c === t ? ' selected' : ''; h += '<option' + sel + '>' + esc(c) + '</option>'; @@ -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 = "<table:" + d.Table + ">" + } + 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 { |
