diff options
Diffstat (limited to 'crawl-ref/source/dat/clua/lm_trig.lua')
-rw-r--r-- | crawl-ref/source/dat/clua/lm_trig.lua | 352 |
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 |