From 2bbe6b8e912bbc5f4a61a33988fb27aca7bd5eb3 Mon Sep 17 00:00:00 2001
From: historia <[not public]>
Date: Wed, 8 Jul 2026 02:23:59 -0400
Subject: feat: object and mob steal tables can include both tables and items
---
internal/admin/static/mobeditor.js | 33 ++++++++++++++++-------
internal/admin/static/objecteditor.js | 51 +++++++++++++++++++++--------------
internal/behavior/types.go | 2 +-
internal/game/act_steal.go | 23 ++++++++--------
internal/game/cmd_verbs.go | 4 +--
internal/object/object.go | 10 +++----
internal/validate/checks.go | 50 ++++++++++++++++++++++++----------
internal/world/mob.go | 16 +++++------
8 files changed, 117 insertions(+), 72 deletions(-)
(limited to 'internal')
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 = '
';
@@ -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 = '
';
@@ -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 += '
';
});
h += '
';
- h += '';
- h += '';
+ h += '';
+ h += '';
h += '
';
h += '
';
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 = '';
- h += '
' + renderField(sec, sec.fields[0], data, -1) + '
';
+ var h = '
';
+ 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 += '
';
-
if (showGuard) {
- h += '
';
- h += '
' + renderField(sec, sec.fields[4], data, -1) + '
';
+ h += '
';
+ h += '
' + renderField(sec, sec.fields[3], data, -1) + '
';
h += '
';
h += '
';
} else {
- h += '
';
- h += '';
- h += '
';
+ h += '
';
}
+ h += '
';
+ 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 += '
';
- h += '';
- 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 += '';
- h += '
';
+ if (msgField) {
+ h += '';
+ var fidMsg = fieldIDSub(sec, st.key, idx, 'message');
+ var msgVal = item.message !== undefined && item.message !== null ? item.message : '';
+ h += '';
+ h += '
';
+ }
} else {
if (isVertical && !st.single) {
h += '';
@@ -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,
--
cgit v1.2.3