diff options
| author | historia <[not public]> | 2026-07-08 02:23:59 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-08 02:23:59 -0400 |
| commit | 2bbe6b8e912bbc5f4a61a33988fb27aca7bd5eb3 (patch) | |
| tree | ef4aef5a7393de51d765b24d565b414ddac4b9fe | |
| parent | 71f0f381de64e045f91da19450adacf10fbc7902 (diff) | |
| download | thehouseoficarus-2bbe6b8e912bbc5f4a61a33988fb27aca7bd5eb3.tar.gz | |
feat: object and mob steal tables can include both tables and items
| -rw-r--r-- | building_guide/mobs.md | 12 | ||||
| -rw-r--r-- | building_guide/objects.md | 40 | ||||
| -rw-r--r-- | data/mobs/examples/bioengineer.yaml | 4 | ||||
| -rw-r--r-- | data/mobs/examples/farmer.yaml | 4 | ||||
| -rw-r--r-- | data/mobs/examples/man.yaml | 4 | ||||
| -rw-r--r-- | data/objects/market_stall.yaml | 4 | ||||
| -rw-r--r-- | internal/admin/static/mobeditor.js | 33 | ||||
| -rw-r--r-- | internal/admin/static/objecteditor.js | 51 | ||||
| -rw-r--r-- | internal/behavior/types.go | 2 | ||||
| -rw-r--r-- | internal/game/act_steal.go | 23 | ||||
| -rw-r--r-- | internal/game/cmd_verbs.go | 4 | ||||
| -rw-r--r-- | internal/object/object.go | 10 | ||||
| -rw-r--r-- | internal/validate/checks.go | 50 | ||||
| -rw-r--r-- | internal/world/mob.go | 16 |
14 files changed, 163 insertions, 94 deletions
diff --git a/building_guide/mobs.md b/building_guide/mobs.md index 7a6f0aa..470a00e 100644 --- a/building_guide/mobs.md +++ b/building_guide/mobs.md @@ -215,10 +215,12 @@ Stealable mobs have a root-level `steal:` block. On failed steal, the mob turns ```yaml name: Farmer steal: - table: farmer_steal # Drop table for steal loot - level: 10 # Required thieving level - xp: 15 # XP per successful steal - speed: 4 # Ticks per steal attempt + level: 10 + speed: 4 + xp: 15 + drops: + - table: farmer_steal + weight: 1 combat: kind: combat attack_types: [slash] @@ -250,7 +252,7 @@ combat: | Field | Description | |---|---| -| `steal.table` | Drop table ID for loot when stealing | +| `steal.drops` | Array of drop entries: `item_id` vs `table` (mutually exclusive), `weight` (required for pity buckets), `quantity` (override; 0 = use sub-item's own for table / 1 for item) | | `steal.level` | Required thieving level | | `steal.xp` | XP awarded per successful steal | | `steal.speed` | Ticks per steal attempt (base wait) | diff --git a/building_guide/objects.md b/building_guide/objects.md index d377f23..a8877a7 100644 --- a/building_guide/objects.md +++ b/building_guide/objects.md @@ -275,25 +275,39 @@ use_interactions: ## Stealable Objects -Objects can be stealable via the `steal` command. These use the same drop table system as mobs and searches. +Objects can be stealable via the `steal` command. These use drop entries — either +item IDs or drop table references — resolved by weighted pick. The `steal` command +supports auto-selection when only one target is available, or `steal <name>` for +explicit targeting, including `steal <n>.<name>` to disambiguate identical defs. ```yaml name: Market Stall description: "A wooden stall piled with food and sundries." -steal_table: market_stall_steal -steal_level: 5 -steal_xp: 12 -steal_speed: 5 -guard_mob: guard +steal: + drops: + - table: market_stall_steal + weight: 1 + level: 5 + xp: 12 + speed: 5 + guard_mob: guard ``` -| Field | Description | -| ------------- | ------------------------------------------------ | -| `steal_table` | Drop table ID for loot when stealing | -| `steal_level` | Required thieving level | -| `steal_xp` | XP awarded per successful steal | -| `steal_speed` | Ticks per steal attempt (base wait) | -| `guard_mob` | Mob def ID that guards this object (watches it) | +| Field | Description | +| ---------------- | ------------------------------------------------ | +| `steal.drops` | Array of drop entries (item_id + weight, or table + weight) | +| `steal.level` | Required thieving level | +| `steal.xp` | XP awarded per successful steal | +| `steal.speed` | Ticks per steal attempt (base wait) | +| `steal.guard_mob`| Mob def ID that guards this object (watches it) | + +Each drop entry: +| Field | Description | +| ------------ | ------------------------------------------------ | +| `item_id` | Item ID (mutually exclusive with `table`) | +| `table` | Drop table ID to sub-resolve (mutually exclusive with `item_id`) | +| `weight` | Weight in the weighted-pick pool | +| `quantity` | Override quantity (0 = use sub-item's own qty for tables, or 1 for items) | When `guard_mob` is set, a mob with that def ID in the same room watches the object on a tick-based cycle (8 ticks watching, 4 ticks looking away). While the guard is diff --git a/data/mobs/examples/bioengineer.yaml b/data/mobs/examples/bioengineer.yaml index 6a7a74a..c5862fa 100644 --- a/data/mobs/examples/bioengineer.yaml +++ b/data/mobs/examples/bioengineer.yaml @@ -54,5 +54,7 @@ name: Bioengineer steal: level: 38 speed: 4 - table: bioengineer_steal xp: 45 + drops: + - table: bioengineer_steal + weight: 1 diff --git a/data/mobs/examples/farmer.yaml b/data/mobs/examples/farmer.yaml index a5f5e3d..15a54ef 100644 --- a/data/mobs/examples/farmer.yaml +++ b/data/mobs/examples/farmer.yaml @@ -54,5 +54,7 @@ name: Farmer steal: level: 10 speed: 4 - table: farmer_steal xp: 15 + drops: + - table: farmer_steal + weight: 1 diff --git a/data/mobs/examples/man.yaml b/data/mobs/examples/man.yaml index acd3239..21cd17f 100644 --- a/data/mobs/examples/man.yaml +++ b/data/mobs/examples/man.yaml @@ -58,5 +58,7 @@ name: man steal: level: 1 speed: 4 - table: man_steal xp: 8 + drops: + - table: man_steal + weight: 1 diff --git a/data/objects/market_stall.yaml b/data/objects/market_stall.yaml index 6fb30d1..36be9c9 100644 --- a/data/objects/market_stall.yaml +++ b/data/objects/market_stall.yaml @@ -4,7 +4,9 @@ color: "B3" inroom_description: "A bustling {B3}market stall{/} is set up here." hidden: false steal: - table: market_stall_steal + drops: + - table: market_stall_steal + weight: 1 level: 5 xp: 12 speed: 5 diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index c142fb4..b5d2395 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -61,10 +61,17 @@ var MOB_SECTIONS = [ id: 'steal', label: 'Steal', path: 'steal', detect: function(d) { return d.steal; }, fields: [ - ['table', 'Table ID', 'search', 'e.g. gem_table', '', null, 'drops'], ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '5', 'narrow'], ['speed', 'Speed', 'number', '4', 'narrow'], + ], + subtables: [ + {label:'Drops', key:'drops', fields:[ + ['item_id', 'Item', 'search', '', '', null, 'items'], + ['table', 'Table', 'search', '', '', null, 'drops'], + ['weight', 'Weight', 'number', '', 'narrow'], + ['quantity', 'Quantity', 'number', '', 'narrow'], + ]} ] }, { @@ -456,7 +463,7 @@ function renderMobSubtable(sec, st, data) { if (st.table) { return renderMobTableSubtable(sec, st, arr); } - if (sec.id === 'drops' && st.key === 'loot') { + if ((sec.id === 'drops' && st.key === 'loot') || (sec.id === 'steal' && st.key === 'drops')) { return renderMobLootSubtable(sec, st, arr); } var h = '<div class="obj-subtable">'; @@ -482,6 +489,7 @@ function renderMobSubtable(sec, st, data) { function renderMobLootSubtable(sec, st, arr) { var byKey = {}; st.fields.forEach(function(f) { byKey[f[0]] = f; }); + var isSteal = sec.id === 'steal'; var rollsField = ['quantity', 'Rolls', 'number', '', 'narrow']; var h = '<div class="obj-subtable">'; @@ -492,7 +500,7 @@ function renderMobLootSubtable(sec, st, arr) { if (kind === 'table') { h += renderMobSubField(sec, st.key, idx, byKey['table'], item.table); h += renderMobSubField(sec, st.key, idx, byKey['weight'], item.weight); - h += renderMobSubField(sec, st.key, idx, rollsField, item.quantity); + h += renderMobSubField(sec, st.key, idx, isSteal ? byKey['quantity'] : rollsField, item.quantity); } else { h += renderMobSubField(sec, st.key, idx, byKey['item_id'], item.item_id); h += renderMobSubField(sec, st.key, idx, byKey['weight'], item.weight); @@ -502,8 +510,8 @@ function renderMobLootSubtable(sec, st, arr) { h += '</div>'; }); h += '<div style="display:flex;gap:6px">'; - h += '<button class="btn btn-sm obj-sub-add" onclick="addMobLootRow(\'item\')">+ Add Item</button>'; - h += '<button class="btn btn-sm obj-sub-add" onclick="addMobLootRow(\'table\')">+ Add Table</button>'; + h += '<button class="btn btn-sm obj-sub-add" onclick="addMobLootRow(\'' + sec.id + '\',\'' + st.key + '\',\'item\')">+ Add Item</button>'; + h += '<button class="btn btn-sm obj-sub-add" onclick="addMobLootRow(\'' + sec.id + '\',\'' + st.key + '\',\'table\')">+ Add Table</button>'; h += '</div>'; h += '</div>'; return h; @@ -615,6 +623,8 @@ function mobAddSection() { mobData[sec.path] = {nodes:{start:{messages:[],options:[]}}}; } else if (id === 'shop') { mobData[sec.path] = {buy_percentage:40, change_percentage:3, buys_anything:true, items:[]}; + } else if (id === 'steal') { + mobData[sec.path] = {drops: []}; } else { mobData[sec.path] = {}; } @@ -641,13 +651,16 @@ function removeMobSubRow(cardID, subKey, idx) { renderMobEditor(mobID, mobData); } -function addMobLootRow(kind) { - if (!mobData.drops) mobData.drops = {}; - if (!Array.isArray(mobData.drops.loot)) mobData.drops.loot = []; +function addMobLootRow(cardID, subKey, kind) { + var sec = MOB_SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec) return; + var p = mobCardPath(sec); + var root = p ? (mobData[p] || (mobData[p] = {})) : mobData; + if (!Array.isArray(root[subKey])) root[subKey] = []; if (kind === 'table') { - mobData.drops.loot.push({table:'', weight:0, quantity:1, _kind:'table'}); + root[subKey].push({table:'', weight:0, quantity:0, _kind:'table'}); } else { - mobData.drops.loot.push({item_id:'', weight:0, quantity:1, _kind:'item'}); + root[subKey].push({item_id:'', weight:0, quantity:0, _kind:'item'}); } renderCurrent(); } diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index 29ad66b..6abec3e 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -21,11 +21,16 @@ var SECTIONS = [ id: 'steal', label: 'Steal', path: 'steal', detect: function(d) { return d.steal !== undefined; }, fields: [ - ['table', 'Table ID', 'search', 'e.g. gem_table', '', null, 'drops'], ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '5', 'narrow'], ['speed', 'Speed', 'number', '4', 'narrow'], ['guard_mob', 'Guard Mob', 'search', '', '', null, 'mobs'], + ], + subtables: [ + {label:'Drops', key:'drops', fields:[ + ['item_id', 'Item ID', 'search', '', '', 'items'], ['table', 'Table', 'search', '', '', 'drops'], + ['weight', 'Weight', 'number', '10', 'narrow'], ['quantity', 'Quantity', 'number', '', 'narrow'], + ]} ] }, { @@ -55,7 +60,7 @@ var SECTIONS = [ subtables: [ {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'], + ['weight', 'Weight', 'number', '10', 'narrow'], ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '17', 'narrow'], ['quantity', 'Quantity', 'number', '1', 'narrow'], ['tool', 'Tool', 'select'], ['depletes', 'Depletes node', 'checkbox'], ['message', 'Message', 'text', 'You manage to mine some copper ore.'], ]} @@ -484,24 +489,21 @@ function renderStealCard(data) { var stealData = data.steal || {}; var showGuard = vis.guard_mob || (stealData.guard_mob && stealData.guard_mob !== ''); - var h = '<div style="display:flex;gap:10px">'; - h += '<div style="flex:1">' + renderField(sec, sec.fields[0], data, -1) + '</div>'; + var h = '<div style="display:flex;gap:10px;align-items:flex-start">'; + h += renderField(sec, sec.fields[0], data, -1); h += renderField(sec, sec.fields[1], data, -1); h += renderField(sec, sec.fields[2], data, -1); - h += renderField(sec, sec.fields[3], data, -1); - h += '</div>'; - if (showGuard) { - h += '<div style="display:flex;gap:6px;align-items:flex-start;margin-top:8px">'; - h += '<div style="flex:1">' + renderField(sec, sec.fields[4], data, -1) + '</div>'; + h += '<div style="display:flex;gap:4px;align-items:flex-start">'; + h += '<div style="flex:1">' + renderField(sec, sec.fields[3], data, -1) + '</div>'; h += '<button class="btn btn-sm btn-danger" onclick="hideGuardMob()" style="margin-top:2px">X</button>'; h += '</div>'; } else { - h += '<div style="margin-top:8px">'; - h += '<button class="btn btn-sm" style="background:var(--accent);color:var(--text);border:1px solid #1a5a8e;white-space:nowrap;width:100%" onclick="showGuardMob()">+ Guard Mob</button>'; - h += '</div>'; + h += '<button class="btn btn-sm" style="background:var(--accent);color:var(--text);border:1px solid #1a5a8e;white-space:nowrap;padding:4px 6px;font-size:11px;font-family:monospace;align-self:flex-end;margin-bottom:3px" onclick="showGuardMob()">+ Add Guard Mob</button>'; } + h += '</div>'; + sec.subtables.forEach(function(st) { h += renderSubtable(sec, st, data); }); return h; } @@ -645,7 +647,7 @@ function renderSubtable(sec, st, data) { } else { arr = sectionRoot[st.key] || []; } - var isDrops = sec.id === 'gather' && st.key === 'drops'; + var isDrops = (sec.id === 'gather' || sec.id === 'steal') && st.key === 'drops'; var isVertical = st.vertical; var gatherTools = []; if (isDrops) { @@ -697,12 +699,14 @@ function renderSubtable(sec, st, data) { } }); h += '</div>'; - h += '<div class="obj-sub-row-line">'; - var fidMsg = fieldIDSub(sec, st.key, idx, 'message'); - var msgVal = item.message !== undefined && item.message !== null ? item.message : ''; var msgField = st.fields.find(function(f){return f[0]==='message';}); - 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>'; + if (msgField) { + h += '<div class="obj-sub-row-line">'; + var fidMsg = fieldIDSub(sec, st.key, idx, 'message'); + var msgVal = item.message !== undefined && item.message !== null ? item.message : ''; + h += '<label class="obj-field obj-grow"><span>' + esc(msgField[1]) + '</span><input id="' + fidMsg + '" value="' + escAttr(String(msgVal)) + '" placeholder="' + escAttr(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>'; @@ -912,7 +916,7 @@ function addSection() { objectData[sec.path] = {}; } } else if (id === 'steal') { - objectData.steal = {table: ''}; + objectData.steal = {drops: []}; } expandedCards[id] = true; renderCurrent(); @@ -981,7 +985,14 @@ 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, tool: '', message: '', _type: (dropType === 'table' ? 't' : 'i') }; + var st = (sec.subtables || []).find(function(s) { return s.key === subKey; }); + var empty = { _type: (dropType === 'table' ? 't' : 'i') }; + if (st) { + st.fields.forEach(function(f) { + empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' ? 0 : ''); + }); + } + empty.weight = 10; arr.push(empty); sectionRoot[subKey] = arr; renderCurrent(); diff --git a/internal/behavior/types.go b/internal/behavior/types.go index 7d95cc3..18d6363 100644 --- a/internal/behavior/types.go +++ b/internal/behavior/types.go @@ -93,7 +93,7 @@ type ProductionData struct { type StealData struct { TargetType string - StealTable string + StealDrops []DropEntry StealLevel int StealXP int StealSpeed float64 diff --git a/internal/game/act_steal.go b/internal/game/act_steal.go index 0aae6d1..d081d8b 100644 --- a/internal/game/act_steal.go +++ b/internal/game/act_steal.go @@ -93,7 +93,7 @@ func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input str roomMobs := g.MobStore.MobsInRoom(p.RoomID) var stealMobs []*world.MobInstance for _, m := range roomMobs { - if m.StealTable != "" { + if len(m.StealDrops) > 0 { stealMobs = append(stealMobs, m) } } @@ -102,7 +102,7 @@ func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input str var stealObjs []*object.ObjectDef for _, st := range roomObjs { def, err := g.ObjectStore.Load(st.DefID) - if err != nil || def.Steal == nil || def.Steal.Table == "" { + if err != nil || def.Steal == nil || len(def.Steal.Drops) == 0 { continue } stealObjs = append(stealObjs, def) @@ -183,7 +183,7 @@ func (g *Game) resolveStealTarget(sess *net.Session, p *player.Player, input str } func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string, mob *world.MobInstance, objDef *object.ObjectDef) { - var stealTable string + var stealDrops []behavior.DropEntry var stealLevel int var stealXP int var stealSpeed float64 @@ -195,7 +195,7 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string guardWatching := false if targetType == "mob" { - stealTable = mob.StealTable + stealDrops = mob.StealDrops stealLevel = mob.StealLevel stealXP = mob.StealXP stealSpeed = mob.StealSpeed @@ -207,7 +207,7 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string sess.WriteLine("They don't seem interested in you.") return } - if stealTable == "" { + if len(stealDrops) == 0 { sess.WriteLine(fmt.Sprintf("You can't steal from the %s.", targetName)) return } @@ -224,7 +224,7 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string return } } else { - stealTable = objDef.Steal.Table + stealDrops = objDef.Steal.Drops stealLevel = objDef.Steal.Level stealXP = objDef.Steal.XP stealSpeed = objDef.Steal.Speed @@ -233,7 +233,7 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string objDefID = objDef.ID guardMob = objDef.Steal.GuardMob - if stealTable == "" { + if len(stealDrops) == 0 { sess.WriteLine(fmt.Sprintf("You can't steal from the %s.", targetName)) return } @@ -270,7 +270,7 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string WaitLeft: engine.ToTicks(stealSpeed), Data: &behavior.StealData{ TargetType: targetType, - StealTable: stealTable, + StealDrops: stealDrops, StealLevel: stealLevel, StealXP: stealXP, StealSpeed: stealSpeed, @@ -286,7 +286,7 @@ func (g *Game) startSteal(sess *net.Session, p *player.Player, targetType string func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { d := p.Action.Data.(*behavior.StealData) targetType := d.TargetType - stealTable := d.StealTable + stealDrops := d.StealDrops stealLevel := d.StealLevel stealXP := d.StealXP stealSpeed := d.StealSpeed @@ -362,11 +362,10 @@ func (g *Game) advanceSteal(sess *net.Session, p *player.Player) { } if rand.Float64() < chance { - dt, err := behavior.LoadDropTable(g.DataDir, stealTable) - if err != nil || len(dt.Drops) == 0 { + if len(stealDrops) == 0 { sess.WriteLine("You steal nothing of value.") } else { - drop := behavior.ResolveDrop(g.DataDir, dt.Drops) + drop := behavior.ResolveDrop(g.DataDir, stealDrops) if drop == nil || drop.ItemID == "" { sess.WriteLine("You steal nothing of value.") } else { diff --git a/internal/game/cmd_verbs.go b/internal/game/cmd_verbs.go index b5c69ce..e32910a 100644 --- a/internal/game/cmd_verbs.go +++ b/internal/game/cmd_verbs.go @@ -114,7 +114,7 @@ func (g *Game) executeVerbs(sess *net.Session, args []string, rawInput string) { } } - if def.Steal != nil && def.Steal.Table != "" { + if def.Steal != nil && len(def.Steal.Drops) > 0 { addUnique(§ionObjs, &seen, "steal "+name) } @@ -130,7 +130,7 @@ func (g *Game) executeVerbs(sess *net.Session, args []string, rawInput string) { if m.IsTalkable() { addUnique(§ionMobs, &seen, "talk "+mName) } - if m.StealTable != "" { + if len(m.StealDrops) > 0 { addUnique(§ionMobs, &seen, "steal "+mName) } addUnique(§ionMobs, &seen, "look "+mName) diff --git a/internal/object/object.go b/internal/object/object.go index 89f01a4..47243ad 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -10,11 +10,11 @@ type UseInteraction struct { } type ObjectSteal struct { - Table string `yaml:"table"` - Level int `yaml:"level"` - XP int `yaml:"xp"` - Speed float64 `yaml:"speed"` - GuardMob string `yaml:"guard_mob,omitempty"` + Drops []behavior.DropEntry `yaml:"drops,omitempty"` + Level int `yaml:"level"` + XP int `yaml:"xp"` + Speed float64 `yaml:"speed"` + GuardMob string `yaml:"guard_mob,omitempty"` } type ObjectDef struct { diff --git a/internal/validate/checks.go b/internal/validate/checks.go index 7cb5914..64b1df4 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -414,13 +414,25 @@ func validateMobs(s Source) []Issue { } } - if def.Steal != nil && def.Steal.Table != "" && !dropIDs[def.Steal.Table] { - issues = append(issues, Issue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Mob %q: steal table %q does not exist", - id, def.Steal.Table), - }) + if def.Steal != nil { + for _, d := range def.Steal.Drops { + if d.ItemID != "" && !itemIDs[d.ItemID] { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: steal drop item %q does not exist", + id, d.ItemID), + }) + } + if d.Table != "" && !dropIDs[d.Table] { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Mob %q: steal drop table %q does not exist", + id, d.Table), + }) + } + } } if def.Combat != nil && def.Combat.FinishingBlow != "" && !itemIDs[def.Combat.FinishingBlow] { @@ -543,13 +555,23 @@ func validateObjects(s Source) []Issue { } if obj.Steal != nil { - if obj.Steal.Table != "" && !dropIDs[obj.Steal.Table] { - issues = append(issues, Issue{ - Level: "ERROR", - Type: "reference", - Message: fmt.Sprintf("Object %q: steal.table %q does not exist", - id, obj.Steal.Table), - }) + for _, d := range obj.Steal.Drops { + if d.ItemID != "" && !itemIDs[d.ItemID] { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Object %q: steal drop item %q does not exist", + id, d.ItemID), + }) + } + if d.Table != "" && !dropIDs[d.Table] { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Object %q: steal drop table %q does not exist", + id, d.Table), + }) + } } if obj.Steal.GuardMob != "" && !mobIDs[obj.Steal.GuardMob] { diff --git a/internal/world/mob.go b/internal/world/mob.go index 05650c0..6780c45 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -18,10 +18,10 @@ type MobDropTable struct { } type MobSteal struct { - Table string `yaml:"table"` - Level int `yaml:"level"` - XP int `yaml:"xp"` - Speed float64 `yaml:"speed"` + Drops []behavior.DropEntry `yaml:"drops"` + Level int `yaml:"level"` + XP int `yaml:"xp"` + Speed float64 `yaml:"speed"` } type MobTask struct { @@ -148,7 +148,7 @@ type MobInstance struct { MaxMeleeHit int MaxRangedHit int MaxScienceHit int - StealTable string + StealDrops []behavior.DropEntry StealLevel int StealXP int StealSpeed float64 @@ -222,7 +222,7 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in progressNoun := "" completeMessage := "" var combatDescriptions []string - stealTable := "" + var stealDrops []behavior.DropEntry stealLevel := 0 stealXP := 0 stealSpeed := 0.0 @@ -265,7 +265,7 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in } if def.Steal != nil { - stealTable = def.Steal.Table + stealDrops = def.Steal.Drops stealLevel = def.Steal.Level stealXP = def.Steal.XP stealSpeed = def.Steal.Speed @@ -318,7 +318,7 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in MaxMeleeHit: maxMeleeHit, MaxRangedHit: maxRangedHit, MaxScienceHit: maxScienceHit, - StealTable: stealTable, + StealDrops: stealDrops, StealLevel: stealLevel, StealXP: stealXP, StealSpeed: stealSpeed, |
