'''
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