Rebuild/cleanup evadventure combat handler
This commit is contained in:
parent
96c8e78aba
commit
a5afa75f59
3 changed files with 521 additions and 6 deletions
|
|
@ -99,16 +99,19 @@ Choose who to block:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
|
|
||||||
from evennia.scripts.scripts import DefaultScript
|
from evennia.scripts.scripts import DefaultScript
|
||||||
from evennia.typeclasses.attributes import AttributeProperty
|
from evennia.typeclasses.attributes import AttributeProperty
|
||||||
from evennia.utils import dbserialize, delay, evmenu, evtable, logger
|
from evennia.utils import dbserialize, delay, evmenu, evtable, logger
|
||||||
from evennia.utils.utils import inherits_from
|
from evennia.utils.utils import inherits_from, list_to_string
|
||||||
|
|
||||||
from . import rules
|
from . import rules
|
||||||
from .enums import Ability
|
from .characters import EvAdventureCharacter
|
||||||
|
from .enums import Ability, ObjType
|
||||||
from .npcs import EvAdventureNPC
|
from .npcs import EvAdventureNPC
|
||||||
|
from .objects import EvAdventureObject
|
||||||
|
|
||||||
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
||||||
COMBAT_HANDLER_INTERVAL = 30
|
COMBAT_HANDLER_INTERVAL = 30
|
||||||
|
|
@ -123,6 +126,346 @@ class CombatFailure(RuntimeError):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Combat action classes
|
||||||
|
|
||||||
|
|
||||||
|
class CombatAction:
|
||||||
|
"""
|
||||||
|
Parent class for all actions.
|
||||||
|
|
||||||
|
This represents the executable code to run to perform an action. It is initialized from an
|
||||||
|
'action-dict', a set of properties stored in the action queue by each combatant.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, combathandler, combatant, action_dict):
|
||||||
|
"""
|
||||||
|
Each key-value pair in the action-dict is stored as a property on this class
|
||||||
|
for later access.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
combatant (EvAdventureCharacter, EvAdventureNPC): The combatant performing
|
||||||
|
the action.
|
||||||
|
action_dict (dict): A dict containing all properties to initialize on this
|
||||||
|
class. This should not be any keys with `_` prefix, since these are
|
||||||
|
used internally by the class.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.combathandler = combathandler
|
||||||
|
self.combatant = combatant
|
||||||
|
|
||||||
|
for key, val in action_dict.items():
|
||||||
|
setattr(self, key, val)
|
||||||
|
|
||||||
|
# advantage / disadvantage
|
||||||
|
# These should be read as 'does <recipient> have dis/advantaget against <target>'.
|
||||||
|
def give_advantage(self, recipient, target, **kwargs):
|
||||||
|
self.combathandler.advantage_matrix[recipient][target] = True
|
||||||
|
|
||||||
|
def give_disadvantage(self, recipient, target, **kwargs):
|
||||||
|
self.combathandler.disadvantage_matrix[recipient][target] = True
|
||||||
|
|
||||||
|
def has_advantage(self, recipient, target):
|
||||||
|
return bool(self.combathandler.advantage_matrix[recipient].pop(target, False))
|
||||||
|
|
||||||
|
def has_disadvantage(self, recipient, target):
|
||||||
|
return bool(self.combathandler.disadvantage_matrix[recipient].pop(target, False))
|
||||||
|
|
||||||
|
def lose_advantage(self, recipient, target):
|
||||||
|
self.combathandler.advantage_matrix[recipient][target] = False
|
||||||
|
|
||||||
|
def lose_disadvantage(self, recipient, target):
|
||||||
|
self.combathandler.disadvantage_matrix[recipient][target] = False
|
||||||
|
|
||||||
|
def flee(self, fleer):
|
||||||
|
if fleer not in self.combathandler.fleeing_combatants:
|
||||||
|
# we record the turn on which we started fleeing
|
||||||
|
self.combathandler.fleeing_combatants[fleer] = self.combathandler.turn
|
||||||
|
|
||||||
|
def unflee(self, fleer):
|
||||||
|
self.combathandler.fleeing_combatants.pop(fleer, None)
|
||||||
|
|
||||||
|
def msg(self, message, broadcast=True):
|
||||||
|
"""
|
||||||
|
Convenience route to the combathandler msg-sender mechanism.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): Message to send; use `$You()` and `$You(other.key)` to refer to
|
||||||
|
the combatant doing the action and other combatants, respectively.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.combathandler.msg(self, message, combatant=self.combatant, broadcast=broadcast)
|
||||||
|
|
||||||
|
def can_use(self):
|
||||||
|
"""
|
||||||
|
Called to determine if the action is usable with the current settings. This does not
|
||||||
|
actually perform the action.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: If this action can be used at this time.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
"""
|
||||||
|
Perform the action as the combatant. Should normally make use of the properties
|
||||||
|
stored on the class during initialization.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionDoNothing(CombatAction):
|
||||||
|
"""
|
||||||
|
Action that does nothing.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Refer to as 'nothing'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionAttack(CombatAction):
|
||||||
|
"""
|
||||||
|
A regular attack, using a wielded weapon.
|
||||||
|
|
||||||
|
action-dict ('attack')
|
||||||
|
{
|
||||||
|
"defender": Character/Object
|
||||||
|
}
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Refer to as 'attack'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
attacker = self.combatant
|
||||||
|
weapon = attacker.weapon
|
||||||
|
defender = self.defender
|
||||||
|
|
||||||
|
is_hit, quality, txt = rules.dice.opposed_saving_throw(
|
||||||
|
attacker,
|
||||||
|
defender,
|
||||||
|
attack_type=weapon.attack_type,
|
||||||
|
defense_type=attacker.weapon.defense_type,
|
||||||
|
advantage=self.has_advantage(attacker, defender),
|
||||||
|
disadvantage=self.has_disadvantage(attacker, defender),
|
||||||
|
)
|
||||||
|
self.msg(f"$You() $conj(attack) $You({defender.key}) with {weapon.key}: {txt}")
|
||||||
|
if is_hit:
|
||||||
|
# enemy hit, calculate damage
|
||||||
|
weapon_dmg_roll = attacker.weapon.damage_roll
|
||||||
|
|
||||||
|
dmg = rules.dice.roll(weapon_dmg_roll)
|
||||||
|
|
||||||
|
if quality is Ability.CRITICAL_SUCCESS:
|
||||||
|
dmg += rules.dice.roll(weapon_dmg_roll)
|
||||||
|
message = (
|
||||||
|
f" $You() |ycritically|n $conj(hit) $You({defender.key}) for |r{dmg}|n damage!"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = f" $You() $conj(hit) $You({defender.key}) for |r{dmg}|n damage!"
|
||||||
|
self.msg(message)
|
||||||
|
|
||||||
|
# call hook
|
||||||
|
defender.at_damage(dmg, attacker=attacker)
|
||||||
|
|
||||||
|
# note that we mustn't remove anyone from combat yet, because this is
|
||||||
|
# happening simultaneously. So checking of the final hp
|
||||||
|
# and rolling of death etc happens in the combathandler at the end of the turn.
|
||||||
|
|
||||||
|
else:
|
||||||
|
# a miss
|
||||||
|
message = f" $You() $conj(miss) $You({defender.key})."
|
||||||
|
if quality is Ability.CRITICAL_FAILURE:
|
||||||
|
attacker.weapon.quality -= 1
|
||||||
|
message += ".. it's a |rcritical miss!|n, damaging the weapon."
|
||||||
|
self.msg(message)
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionStunt(CombatAction):
|
||||||
|
"""
|
||||||
|
Perform a stunt the grants a beneficiary (can be self) advantage on their next action against a
|
||||||
|
target. Whenever performing a stunt that would affect another negatively (giving them disadvantage
|
||||||
|
against an ally, or granting an advantage against them, we need to make a check first. We don't
|
||||||
|
do a check if giving an advantage to an ally or ourselves.
|
||||||
|
|
||||||
|
action_dict:
|
||||||
|
{
|
||||||
|
"recipient": Character/NPC,
|
||||||
|
"target": Character/NPC,
|
||||||
|
"advantage": bool, # if False, it's a disadvantage
|
||||||
|
"stunt_type": Ability, # what ability (like STR, DEX etc) to use to perform this stunt.
|
||||||
|
"defense_type": Ability, # what ability to use to defend against (negative) effects of this
|
||||||
|
stunt.
|
||||||
|
}
|
||||||
|
|
||||||
|
Note:
|
||||||
|
refer to as 'stunt'.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
attacker = self.combatant
|
||||||
|
recipient = self.recipient # the one to receive the effect of the stunt
|
||||||
|
target = self.target # the affected by the stunt (can be the same as recipient/combatant)
|
||||||
|
is_success = False
|
||||||
|
|
||||||
|
if target == self.combatant:
|
||||||
|
# can always grant dis/advantage against yourself
|
||||||
|
defender = attacker
|
||||||
|
is_success = True
|
||||||
|
elif recipient == target:
|
||||||
|
# grant another entity dis/advantage against themselves
|
||||||
|
defender = recipient
|
||||||
|
else:
|
||||||
|
# recipient not same as target; who will defend depends on disadvantage or advantage
|
||||||
|
# to give.
|
||||||
|
defender = target if self.advantage else recipient
|
||||||
|
|
||||||
|
if not is_success:
|
||||||
|
# trying to give advantage to recipient against target. Target defends against caller
|
||||||
|
is_success, _, txt = rules.dice.opposed_saving_throw(
|
||||||
|
attacker,
|
||||||
|
defender,
|
||||||
|
attack_type=self.stunt_type,
|
||||||
|
defense_type=self.defense_type,
|
||||||
|
advantage=self.has_advantage(attacker, defender),
|
||||||
|
disadvantage=self.has_disadvantage(attacker, defender),
|
||||||
|
)
|
||||||
|
|
||||||
|
# deal with results
|
||||||
|
self.msg(f"$You() $conj(attempt) stunt on $You(defender.key). {txt}")
|
||||||
|
if is_success:
|
||||||
|
if self.advantage:
|
||||||
|
self.give_advantage(recipient, target)
|
||||||
|
else:
|
||||||
|
self.give_disadvantage(recipient, target)
|
||||||
|
self.msg(
|
||||||
|
f"%You() $conj(cause) $You({recipient.key}) "
|
||||||
|
f"to gain {'advantage' if self.advantage else 'disadvantage'} "
|
||||||
|
f"against $You({target.key})!"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.msg(f"$You({target.key}) resists! $You() $conj(fail) the stunt.")
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionUseItem(CombatAction):
|
||||||
|
"""
|
||||||
|
Use an item in combat. This is meant for one-off or limited-use items (so things like
|
||||||
|
scrolls and potions, not swords and shields). If this is some sort of weapon or spell rune,
|
||||||
|
we refer to the item to determine what to use for attack/defense rolls.
|
||||||
|
|
||||||
|
action_dict: }
|
||||||
|
"item": Object
|
||||||
|
"target": Character/NPC/Object
|
||||||
|
}
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Refer to as 'use'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
|
||||||
|
item = self.item
|
||||||
|
user = self.combatant
|
||||||
|
target = self.target
|
||||||
|
|
||||||
|
if user == target:
|
||||||
|
# always manage to use the item on yourself
|
||||||
|
is_success = True
|
||||||
|
else:
|
||||||
|
if item.has_obj_type(ObjType.WEAPON):
|
||||||
|
# this is something that harms the target. We need to roll defense
|
||||||
|
is_success, _, txt = rules.dice.opposed_saving_throw(
|
||||||
|
user,
|
||||||
|
target,
|
||||||
|
attack_type=item.attack_type,
|
||||||
|
defense_type=item.defense_type,
|
||||||
|
advantage=self.has_advantage(user, target),
|
||||||
|
disadvantage=self.has_disadvantage(user, target),
|
||||||
|
)
|
||||||
|
|
||||||
|
item.at_use(self.combatant, self.target)
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionWield(CombatAction):
|
||||||
|
"""
|
||||||
|
Wield a new weapon (or spell) from your inventory. This will swap out the one you are currently
|
||||||
|
wielding, if any.
|
||||||
|
|
||||||
|
action_dict = {
|
||||||
|
"item": Object
|
||||||
|
}
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Refer to as 'wield'.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
self.combatant.equipment.move(self.item)
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionFlee(CombatAction):
|
||||||
|
"""
|
||||||
|
Start (or continue) fleeing/disengaging from combat.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Refer to as 'flee'.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
self.msg(
|
||||||
|
"$You() $conj(retreat), and will leave combat next round unless someone successfully "
|
||||||
|
"blocks the escape."
|
||||||
|
)
|
||||||
|
self.flee(self.combatant)
|
||||||
|
|
||||||
|
|
||||||
|
class CombatActionHinder(CombatAction):
|
||||||
|
"""
|
||||||
|
Hinder a fleeing opponent from fleeing/disengaging from combat.
|
||||||
|
|
||||||
|
action_dict = {
|
||||||
|
"target": Character/NPC
|
||||||
|
}
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Refer to as 'hinder'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
|
||||||
|
hinderer = self.combatant
|
||||||
|
target = self.target
|
||||||
|
|
||||||
|
is_success, _, txt = rules.dice.opposed_saving_throw(
|
||||||
|
hinderer,
|
||||||
|
target,
|
||||||
|
attack_type=Ability.DEX,
|
||||||
|
defense_type=Ability.DEX,
|
||||||
|
advantage=self.has_advantage(hinderer, target),
|
||||||
|
disadvantage=self.has_disadvantage(hinderer, target),
|
||||||
|
)
|
||||||
|
|
||||||
|
# handle result
|
||||||
|
self.msg(
|
||||||
|
f"$You() $conj(try) to block the retreat of $You({target.key}). {txt}",
|
||||||
|
)
|
||||||
|
if is_success:
|
||||||
|
# managed to stop the target from fleeing/disengaging
|
||||||
|
self.unflee(target)
|
||||||
|
self.msg(f"$You() $conj(block) the retreat of $You({target.key})")
|
||||||
|
else:
|
||||||
|
# failed to hinder the target
|
||||||
|
self.msg(f"$You({target.key}) $conj(dodge) away from you $You()!")
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureCombatHandler(DefaultScript):
|
class EvAdventureCombatHandler(DefaultScript):
|
||||||
"""
|
"""
|
||||||
This script is created when a combat starts. It 'ticks' the combat and tracks
|
This script is created when a combat starts. It 'ticks' the combat and tracks
|
||||||
|
|
@ -134,12 +477,59 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
max_action_queue_size = 1
|
max_action_queue_size = 1
|
||||||
|
|
||||||
# available actions
|
# available actions
|
||||||
action_classes = {}
|
action_classes = {
|
||||||
|
"nothing": CombatActionDoNothing,
|
||||||
|
"attack": CombatActionAttack,
|
||||||
|
}
|
||||||
|
|
||||||
|
# fallback action if not selecting anything
|
||||||
|
fallback_action = "attack"
|
||||||
|
|
||||||
|
# persistent storage
|
||||||
|
turn = AttributeProperty(0)
|
||||||
|
|
||||||
# who is involved in combat, and their action queue,
|
# who is involved in combat, and their action queue,
|
||||||
# as {combatant: [actiondict, actiondict,...]}
|
# as {combatant: [actiondict, actiondict,...]}
|
||||||
combatants = AttributeProperty(defaultdict(list))
|
combatants = AttributeProperty(defaultdict(list))
|
||||||
|
|
||||||
|
advantage_matrix = AttributeProperty(defaultdict(dict))
|
||||||
|
disadvantage_matrix = AttributeProperty(defaultdict(dict))
|
||||||
|
|
||||||
|
fleeing_combatants = AttributeProperty(dict)
|
||||||
|
defeated_combatants = AttributeProperty(dict)
|
||||||
|
|
||||||
|
def msg(self, message, combatant=None, broadcast=True):
|
||||||
|
"""
|
||||||
|
Central place for sending messages to combatants. This allows
|
||||||
|
for adding any combat-specific text-decoration in one place.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): The message to send.
|
||||||
|
combatant (Object): The 'You' in the message, if any.
|
||||||
|
broadcast (bool): If `False`, `combatant` must be included and
|
||||||
|
will be the only one to see the message. If `True`, send to
|
||||||
|
everyone in the location.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If `combatant` is given, use `$You/you()` markup to create
|
||||||
|
a message that looks different depending on who sees it. Use
|
||||||
|
`$You(combatant_key)` to refer to other combatants.
|
||||||
|
|
||||||
|
"""
|
||||||
|
location = self.obj
|
||||||
|
location_objs = location.contents
|
||||||
|
|
||||||
|
exclude = []
|
||||||
|
if not broadcast and combatant:
|
||||||
|
exclude = [obj for obj in location_objs if obj is not combatant]
|
||||||
|
|
||||||
|
location.msg_contents(
|
||||||
|
message,
|
||||||
|
exclude=exclude,
|
||||||
|
from_obj=combatant,
|
||||||
|
mapping={locobj.key: locobj for locobj in location_objs},
|
||||||
|
)
|
||||||
|
|
||||||
def add_combatant(self, combatant):
|
def add_combatant(self, combatant):
|
||||||
"""
|
"""
|
||||||
Add a new combatant to the battle.
|
Add a new combatant to the battle.
|
||||||
|
|
@ -163,6 +553,52 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
"""
|
"""
|
||||||
self.combatants.pop(combatant, None)
|
self.combatants.pop(combatant, None)
|
||||||
|
|
||||||
|
def stop_combat(self):
|
||||||
|
"""
|
||||||
|
Stop the combat immediately.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for combatant in self.combatants:
|
||||||
|
self.remove_combatant(combatant)
|
||||||
|
self.stop()
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
def get_sides(self, combatant):
|
||||||
|
"""
|
||||||
|
Get a listing of the two 'sides' of this combat, from the perspective of the provided
|
||||||
|
combatant. The sides don't need to be balanced.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
combatant (Character or NPC): The one whose sides are to determined.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple of lists `(allies, enemies)`, from the perspective of `combatant`.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The sides are found by checking PCs vs NPCs. PCs can normally not attack other PCs, so
|
||||||
|
are naturally allies. If the current room has the `allow_pvp` Attribute set, then _all_
|
||||||
|
other combatants (PCs and NPCs alike) are considered valid enemies (one could expand
|
||||||
|
this with group mechanics).
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.obj.allow_pvp:
|
||||||
|
# in pvp, everyone else is an ememy
|
||||||
|
allies = [combatant]
|
||||||
|
enemies = [comb for comb in self.combatants if comb != combatant]
|
||||||
|
else:
|
||||||
|
# otherwise, enemies/allies depend on who combatant is
|
||||||
|
pcs = [comb for comb in self.combatants if inherits_from(comb, EvAdventureCharacter)]
|
||||||
|
npcs = [comb for comb in self.combatants if comb not in pcs]
|
||||||
|
if combatant in pcs:
|
||||||
|
# combatant is a PC, so NPCs are all enemies
|
||||||
|
allies = [comb for comb in pcs if comb != combatant]
|
||||||
|
enemies = npcs
|
||||||
|
else:
|
||||||
|
# combatant is an NPC, so PCs are all enemies
|
||||||
|
allies = [comb for comb in npcs if comb != combatant]
|
||||||
|
enemies = pcs
|
||||||
|
return allies, enemies
|
||||||
|
|
||||||
def queue_action(self, combatant, action_dict):
|
def queue_action(self, combatant, action_dict):
|
||||||
"""
|
"""
|
||||||
Queue an action by adding the new actiondict to the back of the queue. If the
|
Queue an action by adding the new actiondict to the back of the queue. If the
|
||||||
|
|
@ -181,7 +617,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
"""
|
"""
|
||||||
self.combatants[combatant].append(action_dict)
|
self.combatants[combatant].append(action_dict)
|
||||||
|
|
||||||
def do_next_action(self, combatant):
|
def execute_next_action(self, combatant):
|
||||||
"""
|
"""
|
||||||
Perform a combatant's next queued action. Note that there is _always_ an action queued,
|
Perform a combatant's next queued action. Note that there is _always_ an action queued,
|
||||||
even if this action is 'do nothing'. We don't pop anything from the queue, instead we keep
|
even if this action is 'do nothing'. We don't pop anything from the queue, instead we keep
|
||||||
|
|
@ -204,9 +640,72 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
|
|
||||||
# use the action-dict to select and create an action from an action class
|
# use the action-dict to select and create an action from an action class
|
||||||
action_class = self.action_classes[action_dict["key"]]
|
action_class = self.action_classes[action_dict["key"]]
|
||||||
action = action_class(**action_dict)
|
action = action_class(combatant, action_dict)
|
||||||
|
|
||||||
action.execute(combatant)
|
action.execute()
|
||||||
|
|
||||||
|
def execute_full_turn(self):
|
||||||
|
"""
|
||||||
|
Perform a full turn of combat, performing everyone's actions in random order.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.turn += 1
|
||||||
|
# random turn order
|
||||||
|
combatants = random.shuffle(list(self.combatants.keys()))
|
||||||
|
|
||||||
|
# do everyone's next queued combat action
|
||||||
|
for combatant in combatants:
|
||||||
|
self.execute_next_action(combatant)
|
||||||
|
|
||||||
|
# check if anyone is defeated
|
||||||
|
for combatant in list(self.combatants.keys()):
|
||||||
|
if combatant.hp <= 0:
|
||||||
|
# PCs roll on the death table here, NPCs die. Even if PCs survive, they
|
||||||
|
# are still out of the fight.
|
||||||
|
combatant.at_defeat()
|
||||||
|
self.defeated_combatants.append(self.combatant.pop(combatant))
|
||||||
|
self.msg("|r$You() $conj(fall) to the ground, defeated.|n", combatant=combatant)
|
||||||
|
|
||||||
|
# check if anyone managed to flee
|
||||||
|
for combatant, started_fleeing in dict(self.fleeing_combatants):
|
||||||
|
if self.turn - started_fleeing > 1:
|
||||||
|
# if they are still alive/fleeing and started fleeing >1 round ago, they succeed
|
||||||
|
self.msg("|y$You() successfully $conj(flee) from combat.|n", combatant=combatant)
|
||||||
|
self.remove_combatant(combatant)
|
||||||
|
|
||||||
|
# check if one side won the battle
|
||||||
|
if not self.combatants:
|
||||||
|
# noone left in combat - maybe they killed each other or all fled
|
||||||
|
surviving_combatant = None
|
||||||
|
allies, enemies = (), ()
|
||||||
|
else:
|
||||||
|
# grab a random survivor and check of they have any living enemies.
|
||||||
|
surviving_combatant = random.choice(list(self.combatant.keys()))
|
||||||
|
allies, enemies = self.get_sides(surviving_combatant)
|
||||||
|
|
||||||
|
if not enemies:
|
||||||
|
# one way or another, there are no more enemies to fight
|
||||||
|
still_standing = list_to_string(f"$You({comb.key})" for comb in allies)
|
||||||
|
knocked_out = list_to_string(
|
||||||
|
f"$You({comb.key})" for comb in self.defeated_combatants if comb.hp > 0
|
||||||
|
)
|
||||||
|
killed = list_to_string(
|
||||||
|
comb for comb in self.defeated_combatants if comb not in knocked_out
|
||||||
|
)
|
||||||
|
|
||||||
|
if still_standing:
|
||||||
|
txt = [f"The combat is over. {still_standing} are still standing."]
|
||||||
|
else:
|
||||||
|
txt = ["The combat is over. No-one stands as the victor."]
|
||||||
|
if knocked_out:
|
||||||
|
txt.append(f"{knocked_out} were taken down, but will live.")
|
||||||
|
if killed:
|
||||||
|
txt.append(f"{killed} were killed.")
|
||||||
|
self.msg(txt)
|
||||||
|
self.stop_combat()
|
||||||
|
|
||||||
|
|
||||||
|
# Command-based combat commands
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ class ObjType(Enum):
|
||||||
HELMET = "helmet"
|
HELMET = "helmet"
|
||||||
CONSUMABLE = "consumable"
|
CONSUMABLE = "consumable"
|
||||||
GEAR = "gear"
|
GEAR = "gear"
|
||||||
|
THROWABLE = "throwable"
|
||||||
MAGIC = "magic"
|
MAGIC = "magic"
|
||||||
QUEST = "quest"
|
QUEST = "quest"
|
||||||
TREASURE = "treasure"
|
TREASURE = "treasure"
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,21 @@ class EvAdventureConsumable(EvAdventureObject):
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
|
|
||||||
|
class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable):
|
||||||
|
"""
|
||||||
|
Something you can throw at an enemy to harm them once, like a knife or exploding potion/grenade.
|
||||||
|
|
||||||
|
Note: In Knave, ranged attacks are done with WIS (representing the stillness of your mind?)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
obj_type = (ObjType.THROWABLE, ObjType.WEAPON, ObjType.CONSUMABLE)
|
||||||
|
|
||||||
|
attack_type = AttributeProperty(Ability.WIS)
|
||||||
|
defense_type = AttributeProperty(Ability.DEX)
|
||||||
|
damage_roll = AttributeProperty("1d6")
|
||||||
|
|
||||||
|
|
||||||
class WeaponEmptyHand:
|
class WeaponEmptyHand:
|
||||||
"""
|
"""
|
||||||
This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it.
|
This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue