''' act.py An open-ended handler for player and NPC actions within NakedMud. This module handles the execution and requirement-checking of all actions. Each action has a name, a required item type (possibly none), an execution function, a requirement-checking function, an energy cost, and a cooldown (possibly zero). Actions are added to the game by calling add_action() Actions are initiated through the "perform" and "use" commands. Documentation for these commands also doubles for their in-game helpfiles. ''' import mud, hooks, storage, auxiliary, char, event, utils, mudsys from mudsys import add_cmd, add_cmd_check ################################################################################ # local variables ################################################################################ # hooks that are run after an action resolves. The list is cleared at the # beginning of each action __action_resolution_hooks__ = [ ] ################################################################################ # auxiliary data ################################################################################ class ActAuxData: def __init__(self, set = None): self.cooldowns = [] def copyTo(self, to): to.cooldowns = [] for skill, time in self.cooldowns: to.cooldowns.append((skill, time)) def copy(self): newdata = ActAuxData() self.copyTo(newdata) return newdata def store(self): return storage.StorageSet() def read(self, set): return ################################################################################ # variables ################################################################################ action_table = { } # functions that can modify energy cost of skills. # Takes the actor, weapon, and energy cost as arguments. energy_mods = [ ] ################################################################################ # events ################################################################################ def update_cooldown_event(owner, data, arg): '''Every second, reduces all cooldowns of all (N)PCs by 1 sec''' for ch in char.char_list(): list = ch.getAuxiliary("act_data").cooldowns i = 0 while i < len(list): skill, timer = list[i] if timer <= 1: list.pop(i) ch.send("{cYou can use " + skill + " again.{n") else: list[i] = (skill, timer - 1) i += 1 # throw us back in queue event.start_event(None, 1, update_cooldown_event) ################################################################################ # action methods ################################################################################ def add_action_help(name): '''adds a help file to the game for the given action''' func, item_type, check, energy, cooldown = action_table[name] # format our display info info = func.__doc__ # add info about our energy info += " " + name + (" has a base energy cost of %d." % energy) # do we have cooldown information to append if cooldown > 0: info += " It is on a %d second cooldown." % cooldown # format it for display as part of a helpfile info = mud.format_string(info, False) # finally, add our helpfile mudsys.add_help(name, info) def add_action(name, item_type, func, check, energy, cooldown = 0): '''Adds an action to the action table. Performs a check to see if the action can be performed. If it can, calls the action function''' action_table[name] = (func, item_type, check, energy, cooldown) if func.__doc__ != None: add_action_help(name) def action_exists(action): '''returns whether or not an action with the given name exists''' return action_table.has_key(action) def list_actions(): '''returns all currently registered actions''' return action_table.keys() def on_cooldown(ch, action): '''returns whether or not the action is on cooldown for the character''' for skill, time in ch.getAuxiliary("act_data").cooldowns: if action == skill: return time return 0 def end_cooldown(ch, action): '''if an action is on cooldown, reset that cooldown''' for pair in ch.getAuxiliary("act_data").cooldowns: skill, timer = pair if action == skill: ch.getAuxiliary("act_data").cooldowns.remove(pair) ch.send("{cYou can use " + skill + " again.{n") break def can_act(ch, action, obj = None): '''returns true/false if the person can use the action''' func, item_type, check, energy, cooldown = action_table[action] if item_type == None and obj != None: return False elif item_type != None and (obj == None or not obj.istype(item_type)): return False return (obj == None or check == None or check(ch, obj)) def action_energy(action): '''returns how much energy an action costs''' func, item_type, check, energy, cooldown = action_table[action] return energy def energy_cost(ch, action, obj): e_cost = action_energy(action) return e_cost + int(sum([x(ch, obj, e_cost) for x in energy_mods])) def can_act_now(ch, action, obj = None): '''Returns true/false if the person can use the action, and has enough energy to perform it''' func, item_type, check, energy, cooldown = action_table[action] return (not (item_type == "weapon" and ch.affected("disarm")) and can_act(ch, action, obj) and ch.get_stat("energy") >= energy_cost(ch, action, obj) and not on_cooldown(ch, action) and ch.room != None) def register_energy_mod(mod): energy_mods.append(mod) def queue_action_resolution_hook(hook, info): '''queues a new action resolution hook. These are performed after an action resolves, but not before.''' __action_resolution_hooks__.append((hook, info)) def try_action(ch, action, obj = None): '''Tries to perform the action with the given object''' global __action_resolution_hooks__ if not action_exists(action): ch.send("No action named %s exists." % action) else: # clear our action resolution hooks __action_resolution_hooks__ = [] # find the variables we'll need timer = on_cooldown(ch, action) func, item_type, check, energy, cooldown = action_table[action] e_cost = energy_cost(ch, action, obj) # perform our checks if timer > 0: ch.send(action + " is on cooldown for %d more seconds." % timer) elif ch.affected("disarm") and item_type == "weapon": ch.send("You cannot perform that action while disarmed.") elif ((obj == None and item_type != None) or (obj != None and item_type!=None and not obj.istype(item_type)) or (check != None and not check(ch, obj))): ch.send_raw("You cannot perform " + action) if obj != None: ch.send_raw(" with " + obj.name) else: ch.send_raw(". You may need to specify an item to use") ch.send(".") elif ch.get_stat("energy") < e_cost: ch.send(("You need %d energy to perform " + action + ".") % e_cost) # attempt to perform an action elif func(ch, obj, action) != False: ch.set_stat("energy", ch.get_stat("energy") - e_cost) if cooldown > 0: ch.getAuxiliary("act_data").cooldowns.append((action, cooldown)) # run our action resolution hooks for hook, info in __action_resolution_hooks__: hooks.run(hook, info) return True # provide feedback to scripters, make sure their NPC AI is working elif ch.is_npc: mud.log_string("%s failed to perform %s. May be caused by a faulty script." % (ch.mob_class, action)) return False ################################################################################ # commands ################################################################################ def cmd_use(ch, cmd, arg): '''Usage: use <item> [[to] <action>] Many items have associated actions. These actions can be executed through the use and perform commands. With perform, you specify the action, and a suitable item you have equipped is found. With use, you specify the item, and a suitable action is found. Weapons and wands tend to have multiple actions. As such, it is usually better to specify the action rather than the item. However, some items (such as bracers, or rare magical artifacts) only have single, specific actions. In which case, it is often more transparent (and less cumbersome) to simply \'use\' the item. For example: > use bracers Will automatically begin deflecting attacks with your bracers, as deflect is the only action available to bracers. Optionally, you might also execute: > perform deflect However, if you have two items that can perform such an action (e.g., shield and bracers), you might end up performing the action with the wrong item! ''' if ch.affected("disorient") or ch.affected("stun"): ch.send("You cannot perform actions while disoriented or stunned.") return try: obj, act = mud.parse_args(ch, True, cmd, arg, "obj.eq | [to] word") except: return # if we have no action, try to parse out a single default one if act == None: if not obj.istype("usable"): mud.message(ch, None, obj, None, False, "to_char", "You must supply the action you want to use $o for.") return else: use_list = obj.usable_uses.split(",") if len(use_list) == 1: act = use_list[0] else: mud.message(ch, None, obj, None, False, "to_char", "$o has multiple uses -- which would you like to use?") return # if it's a weapon, make sure it's wielded if obj.istype("weapon") and not (obj == ch.mainhand or obj == ch.offhand): ch.send("Weapons must be wielded to use.") # if we did not supply an action, try to parse out a single action for it else: try_action(ch, act, obj) def cmd_perform(ch, cmd, arg): '''Usage: perform <action> [[with] <object>] Various skills, weapons, armors, and wearable miscellanea confer actions upon their users. To make these actions, one must \'perform\' them. Attacking actions are made against whichever opponent has been targeted. For example: > wield shortsword > target goblin > perform "swing" with shortsword Notably, the object being used does not need to be specified. The game will intuit which item you are refering to; if more than one item is a valid candidate for an action, the one held in your mainhand takes priority. In the heat of combat, it is sometimes cumbersome to type out full action commands. As such, it is highly reccomended that players {calias{n important actions before engaging a foe. Beneficial actions default to being performed on yourself, or a friendly ally if they are your target. A list of performable actions an item confers can be gained from looking at it. Some actions are only unlocked upon attaining requisite skill or stat ranks. Some items only have one action -- in which case, you can also use the {cuse{n command to perform them. see also: combat, alias ''' if ch.affected("disorient") or ch.affected("stun"): ch.send("You cannot perform actions while disoriented or stunned.") return try: act, obj = mud.parse_args(ch, True, cmd, arg, "word | [with] obj.eq") except: return # If we're performing a weapon action, default to perform it with the item # in our mainhand. If that weapon cannot perform it, then default to # perform it with our offhand. if obj == None and action_exists(act): func, item_type, check, energy, cooldown = action_table[act] if (ch.mainhand != None and item_type != None and ch.mainhand.istype(item_type) and can_act(ch, act, ch.mainhand)): obj = ch.mainhand elif (ch.offhand != None and item_type != None and ch.offhand.istype(item_type) and can_act(ch, act, ch.offhand)): obj = ch.offhand # We can only perform with weapons in our main or offhand if (obj != None and obj.istype("weapon") and not (obj == ch.mainhand or obj == ch.offhand)): ch.send("Weapons must be wielded to perform with.") else: try_action(ch, act, obj) ################################################################################ # hooks ################################################################################ def item_act_info(info): '''Appends action information to an object look buffer.''' obj, ch = hooks.parse_info(info) actions = [] for action in list_actions(): if can_act(ch, action, obj): actions.append(action) actions.sort() if len(actions) > 0: ch.look_buf += " With this item you can perform " + ", ".join(actions) + "." ################################################################################ # initialization ################################################################################ # auxiliary data auxiliary.install("act_data", ActAuxData, "character") # action updaters event.start_event(None, 1, update_cooldown_event) # hooks hooks.add("append_obj_desc", item_act_info) # commands add_cmd("perform", None, cmd_perform, "player", True) add_cmd("use", None, cmd_use, "player", True) for cmd in ["perform", "use"]: add_cmd_check(cmd, utils.chk_conscious) # misc initialization mud.can_act = can_act mud.can_act_now = can_act_now