summaryrefslogtreecommitdiffstats
path: root/crawl-ref/source/dat/clua/lm_trig.lua
diff options
context:
space:
mode:
Diffstat (limited to 'crawl-ref/source/dat/clua/lm_trig.lua')
-rw-r--r--crawl-ref/source/dat/clua/lm_trig.lua352
1 files changed, 352 insertions, 0 deletions
diff --git a/crawl-ref/source/dat/clua/lm_trig.lua b/crawl-ref/source/dat/clua/lm_trig.lua
new file mode 100644
index 0000000000..f435fd2424
--- /dev/null
+++ b/crawl-ref/source/dat/clua/lm_trig.lua
@@ -0,0 +1,352 @@
+------------------------------------------------------------------------------
+-- dungeon.lua:
+-- DgnTriggerers and triggerables:
+--
+-- This is similar to the overvable/observer design pattern: a triggerable
+-- class which does something and a triggerrer which sets it off. As an
+-- example, the ChangeFlags class (clua/lm_flags.lua), rather than having
+-- three subclasses (for monster death, feature change and item pickup)
+-- needs no subclasses, but is just one class which is given triggerers
+-- which listen for different events. Additionally, new types of triggerers
+-- can be developed and used without have to update the ChangeFlags code.
+--
+-- Unlike with the overvable/observer design pattern, each triggerer is
+-- associated with a signle triggerable, rather than there being one observable
+-- and multiple observers, since each triggerer might have a data payload which
+-- is meant to be different for each triggerable.
+--
+-- A triggerable class just needs to subclass Triggerable and define an
+-- "on_trigger" method.
+------------------------------------------------------------------------------
+
+Triggerable = { CLASS = "Triggerable" }
+Triggerable.__index = Triggerable
+
+function Triggerable:new()
+ local tr = { }
+ setmetatable(tr, self)
+ self.__index = self
+
+ tr.triggerers = { }
+ tr.dgn_trigs_by_type = { }
+
+ return tr
+end
+
+function Triggerable:add_triggerer(triggerer)
+ if not triggerer.type then
+ error("triggerer has no type")
+ end
+
+ table.insert(self.triggerers, triggerer)
+
+ if (triggerer.method == "dgn_event") then
+ local et = dgn.dgn_event_type(triggerer.type)
+ if not self.dgn_trigs_by_type[et] then
+ self.dgn_trigs_by_type[et] = {}
+ end
+
+ table.insert(self.dgn_trigs_by_type[et], #self.triggerers)
+ else
+ local method = triggerer.method or "(nil)"
+
+ local class
+ local meta = getmetatable(triggerer)
+ if not meta then
+ class = "(no meta table)"
+ elseif not meta.CLASS then
+ class = "(no class name)"
+ end
+
+ error("Unknown triggerer method '" .. method .. "' for trigger class '"
+ .. class .. "'")
+ end
+
+ triggerer:added(self)
+end
+
+function Triggerable:move(marker, dest, y)
+ local was_activated = self.activated
+
+ self:remove_all_triggerers(marker)
+
+ -- XXX: Are coordinated passed around as single objects?
+ if y then
+ marker:move(dest, y)
+ else
+ marker:move(dest)
+ end
+
+ if was_activated then
+ self.activated = false
+ self:activate(marker)
+ end
+end
+
+function Triggerable:remove(marker)
+ if self.removed then
+ error("Trigerrable already removed")
+ end
+
+ self:remove_all_triggerers(marker)
+ dgn.remove_marker(marker)
+
+ self.removed = true
+end
+
+function Triggerable:remove_all_triggerers(marker)
+ for _, trig in ipairs(self.triggerers) do
+ trig:remove(self, marker)
+ end
+end
+
+function Triggerable:activate(marker)
+ if self.removed then
+ error("Can't activate, trigerrable removed")
+ end
+
+ if self.activating then
+ error("Triggerable already activating")
+ end
+
+ if self.activated then
+ error("Triggerable already activated")
+ end
+
+ self.activating = true
+ for _, trig in ipairs(self.triggerers) do
+ trig:activate(self, marker)
+ end
+ self.activating = false
+ self.activated = true
+end
+
+function Triggerable:event(marker, ev)
+ local et = ev:type()
+
+ local trig_list = self.dgn_trigs_by_type[et]
+
+ if not trig_list then
+ local class = getmetatable(self).CLASS
+ local x, y = marker:pos()
+ local e_type = dgn.dgn_event_type(et)
+
+ error("Triggerable type " .. class .. " at (" ..x .. ", " .. y .. ") " ..
+ "has no triggerers for dgn_event " .. e_type )
+ end
+
+ for _, trig_idx in ipairs(trig_list) do
+ self.triggerers[trig_idx]:event(self, marker, ev)
+
+ if self.removed then
+ return
+ end
+ end
+end
+
+function Triggerable:write(marker, th)
+ file.marshall(th, #self.triggerers)
+ for _, trig in ipairs(self.triggerers) do
+ -- We'll be handling the de-serialization of the triggerer, so we need to
+ -- save the class name.
+ file.marshall(th, getmetatable(trig).CLASS)
+ trig:write(marker, th)
+ end
+
+ lmark.marshall_table(th, self.dgn_trigs_by_type)
+end
+
+function Triggerable:read(marker, th)
+ self.triggerers = {}
+
+ local num_trigs = file.unmarshall_number(th)
+ for i = 1, num_trigs do
+ -- Hack to let triggerer classes de-serialize themselves.
+ local trig_class = file.unmarshall_string(th)
+
+ -- _G is the global symbol table, and the class name of the triggerer is
+ -- the name of that class's class object
+ local trig_table = _G[trig_class].read(nil, marker, th)
+ table.insert(self.triggerers, trig_table)
+ end
+
+ self.dgn_trigs_by_type = lmark.unmarshall_table(th)
+
+ setmetatable(self, Triggerable)
+ return self
+end
+
+-------------------------------------------------------------------------------
+-- NOTE: The CLASS string of a triggerer class *MUST* be exactly the same as
+-- the triggerer class name, or it won't be able to deserialize properly.
+--
+-- NOTE: A triggerer shouldn't store a reference to the triggerable it
+-- belongs to, and if it does then it must not save/restore that reference.
+-------------------------------------------------------------------------------
+
+-- DgnTriggerer listens for dungeon events of these types:
+--
+-- * monster_dies: Waits for a monster to die. Needs the parameter
+-- "target", who's value is the name of the monster who's death
+-- we're wating for. Doesn't matter where the triggerable/marker
+-- is placed.
+--
+-- * feat_change: Waits for a cell's feature to change. Accepts the
+-- optional parameter "target", which if set delays the trigger
+-- until the feature the cell turns into contains the target as a
+-- substring. The triggerable/marker must be placed on top of the
+-- cell who's feature you wish to monitor.
+--
+-- * item_moved: Wait for an item to move from one cell to another.
+-- Needs the parameter "target", who's value is the name of the
+-- item that is being tracked. The triggerable/marker must be placed
+-- on top of the cell containing the item.
+--
+-- * item_pickup: Wait for an item to be picked up. Needs the parameter
+-- "target", who's value is the name of the item that is being tracked.
+-- The triggerable/marker must be placed on top of the cell containing
+-- the item. Automatically takes care of the item moving from one
+-- square to another without being picked up.
+
+DgnTriggerer = { CLASS = "DgnTriggerer" }
+DgnTriggerer.__index = DgnTriggerer
+
+function DgnTriggerer:new(pars)
+ pars = pars or {}
+
+ if not pars.type then
+ error("DgnTriggerer must have a type")
+ end
+
+ if pars.type == "monster_dies" or pars.type == "item_moved"
+ or pars.type == "item_pickup"
+ then
+ if not pars.target then
+ error(pars.type .. " DgnTriggerer must have parameter 'target'")
+ end
+ end
+
+ local tr = util.copy_table(pars)
+ setmetatable(tr, self)
+ self.__index = self
+
+ tr:setup()
+
+ return tr
+end
+
+function DgnTriggerer:setup()
+ self.method = "dgn_event"
+end
+
+function DgnTriggerer:added(triggerable)
+ if self.type == "item_pickup" then
+ -- Automatically move the triggerable if the item we're watching is moved
+ -- before it it's picked up.
+ local mover = util.copy_table(self)
+
+ mover.type = "item_moved"
+ mover.marker_mover = true
+
+ triggerable:add_triggerer( DgnTriggerer:new(mover) )
+ end
+end
+
+function DgnTriggerer:activate(triggerable, marker)
+ if not (triggerable.activated or triggerable.activating) then
+ error("DgnTriggerer:activate(): triggerable is not activated")
+ end
+
+ local et = dgn.dgn_event_type(self.type)
+
+ if (dgn.dgn_event_is_position(et)) then
+ dgn.register_listener(et, marker, marker:pos())
+ else
+ dgn.register_listener(et, marker)
+ end
+end
+
+function DgnTriggerer:remove(triggerable, marker)
+ if not triggerable.activated then
+ return
+ end
+
+ local et = dgn.dgn_event_type(self.type)
+
+ if (dgn.dgn_event_is_position(et)) then
+ dgn.remove_listener(marker, marker:pos())
+ else
+ dgn.remove_listener(marker)
+ end
+end
+
+function DgnTriggerer:event(triggerable, marker, ev)
+ if self.type == "monster_dies" then
+ local midx = ev:arg1()
+ local mons = dgn.mons_from_index(midx)
+
+ if not mons then
+ error("DgnTriggerer:event() didn't get a valid monster index")
+ end
+
+ if mons.name == self.target then
+ triggerable:on_trigger(self, marker, ev)
+ end
+ elseif self.type == "feat_change" then
+ if self.target and self.target ~= "" then
+ local feat = dgn.feature_name(dgn.grid(ev:pos()))
+ if not string.find(feat, self.target) then
+ return
+ end
+ end
+ triggerable:on_trigger(self, marker, ev)
+ elseif self.type == "item_moved" then
+ local obj_idx = ev:arg1()
+ local it = dgn.item_from_index(obj_idx)
+
+ if not it then
+ error("DgnTriggerer:event() didn't get a valid item index")
+ end
+
+ if item.name(it) == self.target then
+ if self.marker_mover then
+ -- We only exist to move the triggerable if the item moves
+ triggerable:move(marker, ev:dest())
+ else
+ triggerable:on_trigger(self, marker, ev)
+ end
+ end
+ elseif self.type == "item_pickup" then
+ local obj_idx = ev:arg1()
+ local it = dgn.item_from_index(obj_idx)
+
+ if not it then
+ error("DgnTriggerer:event() didn't get a valid item index")
+ end
+
+ if item.name(it) == self.target then
+ triggerable:on_trigger(self, marker, ev)
+ end
+ else
+ local e_type = dgn.dgn_event_type(et)
+
+ error("DgnTriggerer can't handle events of type " .. e_type)
+ end
+end
+
+function DgnTriggerer:write(marker, th)
+ -- Will always be "dgn_event", so we don't need to save it.
+ self.method = nil
+
+ lmark.marshall_table(th, self)
+end
+
+function DgnTriggerer:read(marker, th)
+ local tr = lmark.unmarshall_table(th)
+
+ setmetatable(tr, DgnTriggerer)
+
+ tr:setup()
+
+ return tr
+end