mirror of
https://github.com/evennia/evennia.git
synced 2026-04-03 22:47:16 +02:00
Start adding quest module
This commit is contained in:
parent
aecfa082f9
commit
9f5eaa6a2e
2 changed files with 315 additions and 330 deletions
|
|
@ -10,8 +10,11 @@ The combat is handled with a `Script` shared between all combatants; this tracks
|
|||
of combat and handles all timing elements.
|
||||
|
||||
Unlike in base _Knave_, the MUD version's combat is simultaneous; everyone plans and executes
|
||||
their turns simultaneously with minimum downtime. This version also includes a stricter
|
||||
handling of optimal distances than base _Knave_ (this would be handled by the GM normally).
|
||||
their turns simultaneously with minimum downtime.
|
||||
|
||||
This version is simplified to not worry about things like optimal range etc. So a bow can be used
|
||||
the same as a sword in battle. One could add a 1D range mechanism to add more strategy by requiring
|
||||
optimizal positioning.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -21,21 +24,11 @@ from evennia.scripts.scripts import DefaultScript
|
|||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
from evennia.utils.utils import make_iter
|
||||
from evennia.utils import evmenu, evtable
|
||||
from .enums import Ability
|
||||
from . import rules
|
||||
|
||||
MIN_RANGE = 0
|
||||
MAX_RANGE = 4
|
||||
MAX_MOVE_RATE = 2
|
||||
STUNT_DURATION = 2
|
||||
|
||||
RANGE_NAMES = {
|
||||
0: "close", # melee, short weapons, fists. long weapons with disadvantage
|
||||
1: "near", # melee, long weapons, short weapons with disadvantage
|
||||
2: "medium", # thrown, ranged with disadvantage
|
||||
3: "far", # ranged, thrown with disadvantage
|
||||
4: "disengaging" # no weapons
|
||||
}
|
||||
|
||||
|
||||
class CombatFailure(RuntimeError):
|
||||
"""
|
||||
|
|
@ -44,19 +37,17 @@ class CombatFailure(RuntimeError):
|
|||
|
||||
class CombatAction:
|
||||
"""
|
||||
This describes a combat-action, like 'attack'.
|
||||
This is the base of a combat-action, like 'attack' or defend.
|
||||
Inherit from this to make new actions.
|
||||
|
||||
"""
|
||||
key = 'action'
|
||||
help_text = "Combat action to perform."
|
||||
# action to echo to everyone.
|
||||
post_action_text = "{combatant} performed an action."
|
||||
optimal_range = 0
|
||||
# None for unlimited
|
||||
max_uses = None
|
||||
suboptimal_range = 1
|
||||
# move actions can be combined with other actions
|
||||
is_move_action = False
|
||||
max_uses = None # None for unlimited
|
||||
# in which order (highest first) to perform the action. If identical, use random order
|
||||
priority = 0
|
||||
|
||||
def __init__(self, combathandler, combatant):
|
||||
self.combathandler = combathandler
|
||||
|
|
@ -71,33 +62,13 @@ class CombatAction:
|
|||
# send only to the combatant.
|
||||
self.combatant.msg(message)
|
||||
|
||||
def get_help(self):
|
||||
return ""
|
||||
|
||||
def check_distance(self, distance, optimal_range=None, suboptimal_range=None):
|
||||
"""Call to easily check and warn for out-of-bound distance"""
|
||||
|
||||
if optimal_range is None:
|
||||
optimal_range = self.optimal_range
|
||||
if suboptimal_range is None:
|
||||
suboptimal_range = self.suboptimal_range
|
||||
|
||||
if distance not in (self.suboptimal_distance, self.optimal_distance):
|
||||
# if we are neither at optimal nor suboptimal distance, we can't do the stunt
|
||||
# from here.
|
||||
self.msg(f"|rYou can't perform {self.key} from {range_names[distance]} distance "
|
||||
"(must be {range_names[suboptimal_distance]} or, even better, "
|
||||
"{range_names[optimal_distance]}).|n")
|
||||
return False
|
||||
elif self.distance == self.suboptimal_distance:
|
||||
self.msg(f"|yNote: Performing {self.key} from {range_names[distance]} works, but "
|
||||
f"the optimal range is {range_names[optimal_range]} (you'll "
|
||||
"act with disadvantage).")
|
||||
return True
|
||||
def get_help(self, *args, **kwargs):
|
||||
return self.help_text
|
||||
|
||||
def can_use(self, combatant, *args, **kwargs):
|
||||
"""
|
||||
Determine if combatant can use this action.
|
||||
Determine if combatant can use this action. In this implementation,
|
||||
it fails if already use all of a usage-limited action.
|
||||
|
||||
Args:
|
||||
combatant (Object): The one performing the action.
|
||||
|
|
@ -111,13 +82,13 @@ class CombatAction:
|
|||
"""
|
||||
return True if self.uses is None else self.uses < self.max_uses
|
||||
|
||||
def pre_perform(self, *args, **kwargs):
|
||||
def pre_use(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def perform(self, *args, **kwargs):
|
||||
def use(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def post_perform(self, *args, **kwargs):
|
||||
def post_use(self, *args, **kwargs):
|
||||
self.uses += 1
|
||||
self.combathandler.msg(self.post_action_text.format(combatant=combatant))
|
||||
|
||||
|
|
@ -134,21 +105,31 @@ class CombatActionDoNothing(CombatAction):
|
|||
|
||||
class CombatActionStunt(CombatAction):
|
||||
"""
|
||||
Perform a stunt.
|
||||
Perform a stunt. A stunt grants an advantage to yours or another player for their next
|
||||
action, or a disadvantage to yours or an enemy's next action.
|
||||
|
||||
Note that while the check happens between the user and a target, another (the 'beneficiary'
|
||||
could still gain the effect. This allows for boosting allies or making them better
|
||||
defend against an enemy.
|
||||
|
||||
Note: We only count a use if the stunt is successful; they will still spend their turn, but won't
|
||||
spend a use unless they succeed.
|
||||
|
||||
"""
|
||||
optimal_distance = 0
|
||||
suboptimal_distance = 1
|
||||
give_advantage = True
|
||||
give_disadvantage = False
|
||||
uses = 1
|
||||
attack_type = "dexterity"
|
||||
defense_type = "dexterity"
|
||||
max_uses = 1
|
||||
priority = -1
|
||||
# how many turns the stunt's effect apply (that is, how quickly it must be used before the
|
||||
# advantage/disadvantage is lost).
|
||||
duration = 5
|
||||
attack_type = Ability.DEX
|
||||
defense_type = Ability.DEX
|
||||
help_text = ("Perform a stunt against a target. This will give you or an ally advantage "
|
||||
"on your next action against the same target [range 0-1, one use per combat. "
|
||||
"Bonus lasts for two turns].")
|
||||
|
||||
def perform(self, attacker, defender, *args, beneficiary=None, **kwargs):
|
||||
def use(self, attacker, defender, *args, beneficiary=None, **kwargs):
|
||||
# quality doesn't matter for stunts, they are either successful or not
|
||||
|
||||
is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw(
|
||||
|
|
@ -160,11 +141,125 @@ class CombatActionStunt(CombatAction):
|
|||
if is_success:
|
||||
beneficiary = beneficiary if beneficiary else attacker
|
||||
if advantage:
|
||||
self.gain_advantage(beneficiary, defender)
|
||||
self.combathandler.gain_advantage(beneficiary, defender)
|
||||
else:
|
||||
self.gain_disadvantage(defender, beneficiary)
|
||||
self.combathandler.gain_disadvantage(defender, beneficiary)
|
||||
|
||||
self.msg
|
||||
# only spend a use after being successful
|
||||
uses += 1
|
||||
|
||||
|
||||
class CombatActionAttack(CombatAction):
|
||||
"""
|
||||
A regular attack, using a wielded melee weapon.
|
||||
|
||||
"""
|
||||
key = "attack"
|
||||
priority = 1
|
||||
|
||||
def use(self, attacker, defender, *args, **kwargs):
|
||||
"""
|
||||
Make an attack against a defender.
|
||||
|
||||
"""
|
||||
# figure out advantage (gained by previous stunts)
|
||||
advantage = bool(self.combathandler.advantage_matrix[attacker].pop(defender, False))
|
||||
|
||||
# figure out disadvantage (by distance or by previous action)
|
||||
disadvantage = bool(self.combathandler.disadvantage_matrix[attacker].pop(defender, False))
|
||||
|
||||
is_hit, quality = rules.EvAdventureRollEngine.opposed_saving_throw(
|
||||
attacker, defender,
|
||||
attack_type=attacker.weapon.attack_type,
|
||||
defense_type=attacker.weapon.defense_type,
|
||||
advantage=advantage, disadvantage=disadvantage
|
||||
)
|
||||
if is_hit:
|
||||
self.combathandler.resolve_damage(attacker, defender,
|
||||
critical=quality == "critical success")
|
||||
|
||||
# TODO messaging here
|
||||
|
||||
|
||||
class CombatActionUseItem(CombatAction):
|
||||
"""
|
||||
Use an item in combat. This is meant for one-off or limited-use items, like potions, scrolls or
|
||||
wands. We offload the usage checks and usability to the item's own hooks. It's generated dynamically
|
||||
from the items in the character's inventory (you could also consider using items in the room this way).
|
||||
|
||||
Each usable item results in one possible action.
|
||||
|
||||
It relies on the combat_* hooks on the item:
|
||||
combat_get_help
|
||||
combat_can_use
|
||||
combat_pre_use
|
||||
combat_pre
|
||||
combat_post_use
|
||||
|
||||
"""
|
||||
def get_help(self, item, *args):
|
||||
return item.combat_get_help(*args)
|
||||
|
||||
def can_use(self, item, combatant, *args, **kwargs):
|
||||
return item.combat_can_use(combatant, self.combathandler, *args, **kwargs)
|
||||
|
||||
def pre_use(self, item, *args, **kwargs):
|
||||
item.combat_pre_use(*args, **kwargs)
|
||||
|
||||
def use(self, item, combatant, target, *args, **kwargs):
|
||||
item.combat_use(combatant, target, *args, **kwargs)
|
||||
|
||||
def post_use(self, item, *args, **kwargs):
|
||||
item.combat_post_use(*args, **kwargs)
|
||||
|
||||
|
||||
class CombatActionFlee(CombatAction):
|
||||
"""
|
||||
Fleeing/disengaging from combat means doing nothing but 'running away' for two turn. Unless
|
||||
someone attempts and succeeds in their 'chase' action, you will leave combat by fleeing at the
|
||||
end of the second turn.
|
||||
|
||||
"""
|
||||
key = "flee"
|
||||
priority = -1
|
||||
|
||||
def use(self, combatant, target, *args, **kwargs):
|
||||
# it's safe to do this twice
|
||||
self.combathandler.flee(combatant)
|
||||
|
||||
class CombatActionChase(CombatAction):
|
||||
|
||||
"""
|
||||
Chasing is a way to counter a 'flee' action. It is a maximum movement towards the target
|
||||
and will mean a DEX contest, if the fleeing target loses, they are moved back from
|
||||
'disengaging' range and remain in combat at the new distance (likely 2 if max movement
|
||||
is 2). Advantage/disadvantage are considered.
|
||||
|
||||
"""
|
||||
key = "chase"
|
||||
priority = -5 # checked last
|
||||
|
||||
attack_type = Ability.DEX # or is it CON?
|
||||
defense_type = Ability.DEX
|
||||
|
||||
def use(self, combatant, fleeing_target, *args, **kwargs):
|
||||
|
||||
advantage = bool(self.advantage_matrix[attacker].pop(fleeing_target, False))
|
||||
disadvantage = bool(self.disadvantage_matrix[attacker].pop(fleeing_target, False))
|
||||
|
||||
is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw(
|
||||
combatant, fleeing_target,
|
||||
attack_type=self.attack_type, defense_type=self.defense_type,
|
||||
advantage=advantage, disadvantage=disadvantage
|
||||
)
|
||||
|
||||
if is_success:
|
||||
# managed to stop the target from fleeing/disengaging
|
||||
self.combatant.unflee(fleeing_target)
|
||||
else:
|
||||
pass # they are getting away!
|
||||
|
||||
|
||||
|
||||
class EvAdventureCombatHandler(DefaultScript):
|
||||
|
|
@ -180,13 +275,11 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
|
||||
# turn counter - abstract time
|
||||
turn = AttributeProperty(default=0)
|
||||
# symmetric distance matrix (handled dynamically). Mapping {combatant1: {combatant2: dist}, ...}
|
||||
distance_matrix = defaultdict(dict)
|
||||
# advantages or disadvantages gained against different targets
|
||||
advantage_matrix = AttributeProperty(defaultdict(dict))
|
||||
disadvantage_matrix = AttributeProperty(defaultdict(dict))
|
||||
|
||||
disengaging_combatants = AttributeProperty(default=list())
|
||||
fleeing_combatants = AttributeProperty(default=list())
|
||||
|
||||
# actions that will be performed before a normal action
|
||||
move_actions = ("approach", "withdraw")
|
||||
|
|
@ -197,50 +290,6 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
}
|
||||
|
||||
|
||||
def _refresh_distance_matrix(self):
|
||||
"""
|
||||
Refresh the distance matrix, either after movement or when a
|
||||
new combatant enters combat - everyone must have a symmetric
|
||||
distance to every other combatant (that is, if you are 'near' an opponent,
|
||||
they are also 'near' to you).
|
||||
|
||||
Distances are abstract and divided into four steps:
|
||||
|
||||
0. Close (melee, short weapons, fists, long weapons with disadvantage)
|
||||
1. Near (melee, long weapons, short weapons with disadvantage)
|
||||
2. Medium (thrown, ranged with disadvantage)
|
||||
3. Far (ranged, thrown with disadvantage)
|
||||
4. Disengaging/fleeing (no weapons can be used)
|
||||
|
||||
Distance is tracked to each opponent individually. One can move 1 step and attack
|
||||
or up to 2 steps (closer or further away) without attacking.
|
||||
|
||||
New combatants will start at a distance averaged between the optimal ranges
|
||||
of them and their opponents.
|
||||
|
||||
"""
|
||||
combatants = self.combatants
|
||||
distance_matrix = self.distance_matrix
|
||||
|
||||
for combatant1 in combatants:
|
||||
for combatant2 in combatants:
|
||||
|
||||
if combatant1 == combatant2:
|
||||
continue
|
||||
|
||||
combatant1_distances = distance_matrix[combatant1]
|
||||
combatant2_distances = distance_matrix[combatant2]
|
||||
|
||||
if combatant2 not in combatant1_distances or combatant1 not in combatant2_distances:
|
||||
# this happens on initialization or when a new combatant is added.
|
||||
# we make sure to update both sides to the distance of the longest
|
||||
# optimal weapon range. So ranged weapons have advantage going in.
|
||||
start_optimal = max(combatant1.weapon.distance_optimal,
|
||||
combatant2.weapon.distance_optimal)
|
||||
|
||||
combatant1_distances[combatant2] = start_optimal
|
||||
combatant2_distances[combatant1] = start_optimal
|
||||
|
||||
def _update_turn_stats(self, combatant, message):
|
||||
"""
|
||||
Store combat messages to display at the end of turn.
|
||||
|
|
@ -261,22 +310,15 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
"""
|
||||
End of turn operations.
|
||||
|
||||
1. Do all moves
|
||||
2. Do all regular actions
|
||||
3. Remove combatants that disengaged successfully
|
||||
4. Timeout advantages/disadvantages set for longer than STUNT_DURATION
|
||||
1. Do all regular actions
|
||||
2. Remove combatants that disengaged successfully
|
||||
3. Timeout advantages/disadvantages set for longer than STUNT_DURATION
|
||||
|
||||
"""
|
||||
# first do all moves
|
||||
# do all actions
|
||||
for combatant in self.combatants:
|
||||
action, args, kwargs = self.action_queue[combatant].get(
|
||||
"move", ("do_nothing", (), {}))
|
||||
getattr(self, f"action_{action}")(combatant, *args, **kwargs)
|
||||
# next do all regular actions
|
||||
for combatant in self.combatants:
|
||||
action, args, kwargs = self.action_qeueue[combatant].get(
|
||||
"action", ("do_nothing", (), {}))
|
||||
getattr(self, f"action_{action}")(combatant, *args, **kwargs)
|
||||
action, args, kwargs = self.action_queue[combatant]
|
||||
action.use(combatant, *args, **kwargs)
|
||||
|
||||
# handle disengaging combatants
|
||||
|
||||
|
|
@ -285,13 +327,8 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
for combatant in self.combatants:
|
||||
# check disengaging combatants (these are combatants that managed
|
||||
# to stay at disengaging distance for a turn)
|
||||
if combatant in self.disengaging_combatants:
|
||||
if combatant in self.fleeing_combatants:
|
||||
self.disengaging_combatants.remove(combatant)
|
||||
to_remove.append(combatant)
|
||||
elif all(1 for distance in self.distance_matrix[combatant].values()
|
||||
if distance == MAX_RANGE):
|
||||
# if at max distance (disengaging) from everyone, they are disengaging
|
||||
self.disengaging_combatants.append(combatant)
|
||||
|
||||
for combatant in to_remove:
|
||||
# for clarity, we remove here rather than modifying the combatant list
|
||||
|
|
@ -327,27 +364,29 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
def add_combatant(self, combatant):
|
||||
if combatant not in self.combatants:
|
||||
self.combatants.append(combatant)
|
||||
self._refresh_distance_matrix()
|
||||
|
||||
def remove_combatant(self, combatant):
|
||||
if combatant in self.combatants:
|
||||
self.combatants.remove(combatant)
|
||||
self._refresh_distance_matrix()
|
||||
|
||||
def get_combat_summary(self, combatant):
|
||||
"""
|
||||
Get a summary of the current combat state.
|
||||
Get a summary of the current combat state from the perspective of a
|
||||
given combatant.
|
||||
|
||||
You (5/10 health)
|
||||
Foo (Hurt) distance: You__0__1___X____3_____4 (medium)
|
||||
Bar (Perfect health): You__X__1___2____3_____4 (close)
|
||||
Foo (Hurt) [Running away - use 'chase' to stop them!]
|
||||
Bar (Perfect health)
|
||||
|
||||
"""
|
||||
table = evtable.EvTable(border_width=0)
|
||||
|
||||
table.add_row(f"You ({combatant.hp} / {combatant.hp_max} health)")
|
||||
# 'You' display
|
||||
fleeing = ""
|
||||
if combatant in self.fleeing_combatants:
|
||||
fleeing = " You are running away! Use 'flee' again next turn."
|
||||
|
||||
dist_template = "|x(You)__{0}|x__{1}|x___{2}|x____{3}|x_____|R{4} |x({distname})"
|
||||
table.add_row(f"You ({combatant.hp} / {combatant.hp_max} health){fleeing}")
|
||||
|
||||
for comb in self.combatants:
|
||||
|
||||
|
|
@ -355,20 +394,19 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
continue
|
||||
|
||||
name = combatant.key
|
||||
distance = self.distance_matrix[combatant][comb]
|
||||
dist_map = {i: '|wX' if i == distance else i for i in range(MAX_RANGE)}
|
||||
dist_map["distname"] = RANGE_NAMES[distance]
|
||||
health = f"{comb.hurt_level}"
|
||||
distance_string = dist_template.format(**dist_map)
|
||||
fleeing = ""
|
||||
if comb in self.fleeing_combatants:
|
||||
fleeing = " [Running away! Use 'chase' to stop them!"
|
||||
|
||||
table.add_row(f"{name} ({health})", distance_string)
|
||||
table.add_row(f"{name} ({health}){fleeing}")
|
||||
|
||||
return str(table)
|
||||
|
||||
def msg(self, message, targets=None):
|
||||
"""
|
||||
Central place for sending messages to combatants. This allows
|
||||
for decorating the output in one place if needed.
|
||||
for adding any combat-specific text-decoration in one place.
|
||||
|
||||
Args:
|
||||
message (str): The message to send.
|
||||
|
|
@ -384,26 +422,6 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
for target in self.combatants:
|
||||
target.msg(message)
|
||||
|
||||
def move_relative_to(self, combatant, target_combatant, change,
|
||||
min_dist=MIN_RANGE, max_dist=MAX_RANGE):
|
||||
"""
|
||||
Change the distance to a target.
|
||||
|
||||
Args:
|
||||
combatant (Character): The one doing the change.
|
||||
target_combatant (Character): The one distance is changed to.
|
||||
change (int): A +/- change value. Result is always in range 0..4.
|
||||
|
||||
"""
|
||||
current_dist = self.distance_matrix[combatant][target_combatant]
|
||||
|
||||
change = max(0, min(MAX_MOVE_RATE, change))
|
||||
|
||||
new_dist = max(min_dist, min(max_dist, current_dist + change))
|
||||
|
||||
self.distance_matrix[combatant][target_combatant] = new_dist
|
||||
self.distance_matrix[target_combatant][combatant] = new_dist
|
||||
|
||||
def gain_advantage(self, combatant, target):
|
||||
"""
|
||||
Gain advantage against target. Spent by actions.
|
||||
|
|
@ -418,6 +436,14 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
"""
|
||||
self.disadvantage_matrix[combatant][target] = self.turn
|
||||
|
||||
def flee(self, combatant):
|
||||
if combatant not in self.fleeing_combatants:
|
||||
self.fleeing_combatants.append(combatant)
|
||||
|
||||
def unflee(self, combatant):
|
||||
if combatant in self.fleeing_combatants:
|
||||
self.fleeing_combatants.remove(combatant)
|
||||
|
||||
def resolve_damage(self, attacker, defender, critical=False):
|
||||
"""
|
||||
Apply damage to defender. On a critical hit, the damage die
|
||||
|
|
@ -457,7 +483,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
# defender still alive
|
||||
self.msg(defender)
|
||||
|
||||
def register_action(self, combatant, action="do_nothing", *args, **kwargs):
|
||||
def register_action(self, combatant, action=None, *args, **kwargs):
|
||||
"""
|
||||
Register an action by-name.
|
||||
|
||||
|
|
@ -465,186 +491,24 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
combatant (Object): The one performing the action.
|
||||
action (str): An available action, will be prepended with `action_` and
|
||||
used to call the relevant handler on this script.
|
||||
*args: Will be passed to the action method `action_<action>`.
|
||||
**kwargs: Will be passed into the action method `action_<action>`.
|
||||
|
||||
"""
|
||||
if action in self.move_actions:
|
||||
self.action_queue[combatant]["move"] = (action, args, kwargs)
|
||||
else:
|
||||
self.action_queue[combatant]["action"] = (action, args, kwargs)
|
||||
|
||||
# action verbs. All of these start with action_* and should also accept
|
||||
# *args, **kwargs so that we can make the call-mechanism generic.
|
||||
|
||||
def action_do_nothing(self, combatant, *args, **kwargs):
|
||||
"""Do nothing for a turn."""
|
||||
|
||||
def action_stunt(self, attacker, defender, attack_type="agility",
|
||||
defense_type="agility", optimal_distance=0, suboptimal_distance=1,
|
||||
advantage=True, beneficiary=None, *args, **kwargs):
|
||||
"""
|
||||
Stunts does not cause damage but are used to give advantage/disadvantage to combatants
|
||||
for later turns. The 'attacker' here is the one attemting the stunt against the 'defender'.
|
||||
If successful, advantage is given to attacker against defender and disadvantage to
|
||||
defender againt attacker. It's also possible to replace the attacker with another combatant
|
||||
against the defender - allowing to aid/hinder others on the battlefield.
|
||||
|
||||
Stunt-modifers last a maximum of two turns and are not additive. Advantages and
|
||||
disadvantages relative to the same target cancel each other out.
|
||||
|
||||
Args:
|
||||
attacker (Object): The one attempting the stunt.
|
||||
defender (Object): The one affected by the stunt.
|
||||
attack_type (str): The ability tested to do the stunt.
|
||||
defense_type (str): The ability used to defend against the stunt.
|
||||
optimal_distance (int): At which distance the stunt works normally.
|
||||
suboptimal_distance (int): At this distance, the stunt is performed at disadvantage.
|
||||
advantage (bool): If False, try to apply disadvantage to defender
|
||||
rather than advantage to attacker.
|
||||
beneficiary (bool): If stunt succeeds, it may benefit another
|
||||
combatant than the `attacker` doing the stunt. This allows for helping
|
||||
allies.
|
||||
|
||||
"""
|
||||
# check if stunt-attacker is at optimal distance
|
||||
distance = self.distance_matrix[attacker][defender]
|
||||
disadvantage = False
|
||||
if suboptimal_distance == distance:
|
||||
# stunts need to be within range
|
||||
disadvantage = True
|
||||
elif self._get_optimal_distance(attacker) != distance:
|
||||
# if we are neither at optimal nor suboptimal distance, we can't do the stunt
|
||||
# from here.
|
||||
raise combatfailure(f"you can't perform this stunt "
|
||||
f"from {range_names[distance]} distance (must be "
|
||||
f"{range_names[suboptimal_distance]} or, even better, "
|
||||
f"{range_names[optimal_distance]}).")
|
||||
# quality doesn't matter for stunts, they are either successful or not
|
||||
is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw(
|
||||
attacker, defender,
|
||||
attack_type=attack_type,
|
||||
defense_type=defense_type,
|
||||
advantage=False, disadvantage=disadvantage,
|
||||
)
|
||||
if is_success:
|
||||
beneficiary = beneficiary if beneficiary else attacker
|
||||
if advantage:
|
||||
self.gain_advantage(beneficiary, defender)
|
||||
else:
|
||||
self.gain_disadvantage(defender, beneficiary)
|
||||
|
||||
return is_success
|
||||
|
||||
def action_attack(self, attacker, defender, *args, **kwargs):
|
||||
"""
|
||||
Make an attack against a defender. This takes into account distance. The
|
||||
attack type/defense depends on the weapon/spell/whatever used.
|
||||
|
||||
"""
|
||||
# check if attacker is at optimal distance
|
||||
distance = self.distance_matrix[attacker][defender]
|
||||
|
||||
# figure out advantage (gained by previous stunts)
|
||||
advantage = bool(self.advantage_matrix[attacker].pop(defender, False))
|
||||
|
||||
# figure out disadvantage (by distance or by previous action)
|
||||
disadvantage = bool(self.disadvantage_matrix[attacker].pop(defender, False))
|
||||
if self._get_suboptimal_distance(attacker) == distance:
|
||||
# fighting at the wrong range is not good
|
||||
disadvantage = True
|
||||
elif self._get_optimal_distance(attacker) != distance:
|
||||
# if we are neither at optimal nor suboptimal distance, we can't
|
||||
# attack from this range
|
||||
raise CombatFailure(f"You can't attack with {attacker.weapon.key} "
|
||||
f"from {RANGE_NAMES[distance]} distance.")
|
||||
|
||||
is_hit, quality = rules.EvAdventureRollEngine.opposed_saving_throw(
|
||||
attacker, defender,
|
||||
attack_type=attacker.weapon.attack_type,
|
||||
defense_type=attacker.weapon.defense_type,
|
||||
advantage=advantage, disadvantage=disadvantage
|
||||
)
|
||||
if is_hit:
|
||||
self.resolve_damage(attacker, defender, critical=quality == "critical success")
|
||||
|
||||
return is_hit
|
||||
|
||||
def action_heal(self, combatant, target, max_distance=1, healing_roll="1d6", *args, **kwargs):
|
||||
"""
|
||||
Heal a target. Target can be the combatant itself.
|
||||
|
||||
Args:
|
||||
combatant (Object): The one performing the heal.
|
||||
target (Object): The one to be healed (can be the same as combatant).
|
||||
max_distance (int): Distances *up to* this range allow for healing.
|
||||
healing_roll (str): The die roll for how many HP to heal.
|
||||
|
||||
Raises:
|
||||
CombatFailure: If too far away to heal target.
|
||||
|
||||
"""
|
||||
if target is not combatant:
|
||||
distance = self.distance_matrix[attacker][defender]
|
||||
if distance > max_distance:
|
||||
raise CombatFailure(f"Too far away to heal {target.key}.")
|
||||
|
||||
target.heal(rules.EvAdventureRollEngine.roll(healing_roll), healer=combatant)
|
||||
|
||||
def action_approach(self, combatant, other_combatant, change, *args, **kwargs):
|
||||
"""
|
||||
Approach target. Closest is 0. This can be combined with another action.
|
||||
|
||||
"""
|
||||
self.move_relative_to(combatant, other_combatant, -abs(change), min_dist=MIN_RANGE)
|
||||
|
||||
def action_withdraw(self, combatant, other_combatant, change):
|
||||
"""
|
||||
Withdraw from target. Most distant is range 3 - further and you'll be disengaging.
|
||||
This can be combined with another action.
|
||||
|
||||
"""
|
||||
self.move_relative_to(combatant, other_combatant, abs(change), max_dist=3)
|
||||
|
||||
def action_flee(self, combatant, *args, **kwargs):
|
||||
"""
|
||||
Fleeing/disengaging from combat means moving towards 'disengaging' range from
|
||||
everyone else and staying there for one turn.
|
||||
|
||||
"""
|
||||
for other_combatant in self.combatants:
|
||||
self.move_relative_to(combatant, other_combatant, MAX_MOVE_RATE, max_dist=MAX_RANGE)
|
||||
|
||||
def action_chase(self, combatant, fleeing_target, *args, **kwargs):
|
||||
"""
|
||||
Chasing is a way to counter a 'flee' action. It is a maximum movement towards the target
|
||||
and will mean a DEX contest, if the fleeing target loses, they are moved back from
|
||||
'disengaging' range and remain in combat at the new distance (likely 2 if max movement
|
||||
is 2). Advantage/disadvantage are considered.
|
||||
|
||||
"""
|
||||
ability = "dexterity"
|
||||
|
||||
advantage = bool(self.advantage_matrix[attacker].pop(fleeing_target, False))
|
||||
disadvantage = bool(self.disadvantage_matrix[attacker].pop(fleeing_target, False))
|
||||
|
||||
is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw(
|
||||
combatant, fleeing_target,
|
||||
attack_type=ability, defense_type=ability,
|
||||
advantage=advantage, disadvantage=disadvantage
|
||||
)
|
||||
|
||||
if is_success:
|
||||
# managed to stop the target from fleeing/disengaging - move closer
|
||||
if fleeing_target in self.disengaging_combatants:
|
||||
self.disengaging_combatants.remove(fleeing_target)
|
||||
self.approach(combatant, fleeing_target, change=MAX_MOVE_RATE)
|
||||
|
||||
return is_success
|
||||
if not action:
|
||||
action = CombatActionDoNothing
|
||||
self.action_queue[combatant] = (action, args, kwargs)
|
||||
|
||||
|
||||
# combat menu
|
||||
|
||||
combat_script = """
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def _register_action(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Register action with handler.
|
||||
|
|
|
|||
121
evennia/contrib/tutorials/evadventure/quests.py
Normal file
121
evennia/contrib/tutorials/evadventure/quests.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""
|
||||
A simple quest system for EvAdventure.
|
||||
|
||||
A quest is represented by a quest-handler sitting as
|
||||
.quest on a Character. Individual Quests are objects
|
||||
that track the state and can have multiple steps, each
|
||||
of which are checked off during the quest's progress.
|
||||
|
||||
The player can use the quest handler to track the
|
||||
progress of their quests.
|
||||
|
||||
A quest ending can mean a reward or the start of
|
||||
another quest.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class EvAdventureQuest:
|
||||
"""
|
||||
This represents a single questing unit of quest.
|
||||
|
||||
Properties:
|
||||
name (str): Main identifier for the quest.
|
||||
category (str, optional): This + name must be globally unique.
|
||||
steps (list): A list of strings, representing how many steps are
|
||||
in the quest. The first step is always the beginning, when the quest is presented.
|
||||
The last step is always the end of the quest. It is possible to abort the quest before
|
||||
it ends - it then pauses after the last completed step.
|
||||
|
||||
each step is represented by two methods on this object:
|
||||
check_<name> and complete_<name>
|
||||
|
||||
"""
|
||||
# name + category must be globally unique. They are
|
||||
# queried as name:category or just name, if category is empty.
|
||||
name = ""
|
||||
category = ""
|
||||
# example: steps = ["start", "step1", "step2", "end"]
|
||||
steps = []
|
||||
|
||||
def __init__(self):
|
||||
step = 0
|
||||
|
||||
def check():
|
||||
pass
|
||||
|
||||
|
||||
def progress(self, quester, *args, **kwargs):
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
class EvAdventureQuestHandler:
|
||||
"""
|
||||
This sits on the Character, as `.quests`.
|
||||
|
||||
It's initiated using a lazy property on the Character:
|
||||
|
||||
```
|
||||
@lazy_property
|
||||
def quests(self):
|
||||
return EvAdventureQuestHandler(self)
|
||||
```
|
||||
|
||||
"""
|
||||
quest_storage_attribute = "_quests"
|
||||
quest_storage_attribute_category = "evadventure"
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self.storage = obj.attributes.get(
|
||||
self.quest_storage_attribute,
|
||||
category=self.quest_storage_attribute_category,
|
||||
default={}
|
||||
)
|
||||
|
||||
def quest_storage_key(self, name, category):
|
||||
return f"{name}:{category}"
|
||||
|
||||
def has(self, quest_name, quest_category=""):
|
||||
"""
|
||||
Check if a given quest is registered with the Character.
|
||||
|
||||
Args:
|
||||
quest_name (str): The name of the quest to check for.
|
||||
quest_category (str, optional): Quest category, if any.
|
||||
|
||||
Returns:
|
||||
bool: If the character is following this quest or not.
|
||||
|
||||
"""
|
||||
return bool(self.get(quest_name, quest_category))
|
||||
|
||||
def get(self, quest_name, quest_category=""):
|
||||
"""
|
||||
Get the quest stored on character, if any.
|
||||
|
||||
Args:
|
||||
quest_name (str): The name of the quest to check for.
|
||||
quest_category (str, optional): Quest category, if any.
|
||||
|
||||
Returns:
|
||||
EvAdventureQuest or None: The quest stored, or None if
|
||||
Character is not on this quest.
|
||||
|
||||
"""
|
||||
return self.storage.get(self.quest_key(quest_storage_name, quest_category))
|
||||
|
||||
def add(self, quest, autostart=True):
|
||||
"""
|
||||
Add a new quest
|
||||
|
||||
Args:
|
||||
quest (EvAdventureQuest): The quest to start.
|
||||
autostart (bool, optional): If set, the quest will
|
||||
start immediately.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue