mirror of
https://github.com/evennia/evennia.git
synced 2026-04-07 00:45:22 +02:00
Add defeat mode to tutorial combat
This commit is contained in:
parent
2daadca999
commit
43e4917501
10 changed files with 419 additions and 131 deletions
|
|
@ -48,6 +48,10 @@ ScriptDB = None
|
|||
ChannelDB = None
|
||||
Msg = None
|
||||
|
||||
# Properties
|
||||
AttributeProperty = None
|
||||
TagProperty = None
|
||||
|
||||
# commands
|
||||
Command = None
|
||||
CmdSet = None
|
||||
|
|
@ -106,7 +110,7 @@ def _create_version():
|
|||
Helper function for building the version string
|
||||
"""
|
||||
import os
|
||||
from subprocess import check_output, CalledProcessError, STDOUT
|
||||
from subprocess import STDOUT, CalledProcessError, check_output
|
||||
|
||||
version = "Unknown"
|
||||
root = os.path.dirname(os.path.abspath(__file__))
|
||||
|
|
@ -153,70 +157,63 @@ def _init():
|
|||
global GLOBAL_SCRIPTS, OPTION_CLASSES
|
||||
global EvMenu, EvTable, EvForm, EvMore, EvEditor
|
||||
global ANSIString
|
||||
global AttributeProperty, TagProperty
|
||||
|
||||
# Parent typeclasses
|
||||
from .accounts.accounts import DefaultAccount
|
||||
from .accounts.accounts import DefaultGuest
|
||||
from .objects.objects import DefaultObject
|
||||
from .objects.objects import DefaultCharacter
|
||||
from .objects.objects import DefaultRoom
|
||||
from .objects.objects import DefaultExit
|
||||
from .comms.comms import DefaultChannel
|
||||
from .scripts.scripts import DefaultScript
|
||||
|
||||
# Database models
|
||||
from .objects.models import ObjectDB
|
||||
from .accounts.models import AccountDB
|
||||
from .scripts.models import ScriptDB
|
||||
from .comms.models import ChannelDB
|
||||
from .comms.models import Msg
|
||||
|
||||
# commands
|
||||
from .commands.command import Command, InterruptCommand
|
||||
from .commands.cmdset import CmdSet
|
||||
|
||||
# search functions
|
||||
from .utils.search import search_object
|
||||
from .utils.search import search_script
|
||||
from .utils.search import search_account
|
||||
from .utils.search import search_message
|
||||
from .utils.search import search_channel
|
||||
from .utils.search import search_help
|
||||
from .utils.search import search_tag
|
||||
|
||||
# create functions
|
||||
from .utils.create import create_object
|
||||
from .utils.create import create_script
|
||||
from .utils.create import create_account
|
||||
from .utils.create import create_channel
|
||||
from .utils.create import create_message
|
||||
from .utils.create import create_help_entry
|
||||
|
||||
# utilities
|
||||
from django.conf import settings
|
||||
from .locks import lockfuncs
|
||||
from .utils import logger
|
||||
from .utils import gametime
|
||||
from .utils import ansi
|
||||
from .prototypes.spawner import spawn
|
||||
from . import contrib
|
||||
from .utils.evmenu import EvMenu
|
||||
from .utils.evtable import EvTable
|
||||
from .utils.evmore import EvMore
|
||||
from .utils.evform import EvForm
|
||||
from .utils.eveditor import EvEditor
|
||||
from .utils.ansi import ANSIString
|
||||
from .server import signals
|
||||
|
||||
# handlers
|
||||
from .scripts.tickerhandler import TICKER_HANDLER
|
||||
from .scripts.taskhandler import TASK_HANDLER
|
||||
from .server.sessionhandler import SESSION_HANDLER
|
||||
from . import contrib
|
||||
from .accounts.accounts import DefaultAccount, DefaultGuest
|
||||
from .accounts.models import AccountDB
|
||||
from .commands.cmdset import CmdSet
|
||||
from .commands.command import Command, InterruptCommand
|
||||
from .comms.comms import DefaultChannel
|
||||
from .comms.models import ChannelDB, Msg
|
||||
from .locks import lockfuncs
|
||||
from .objects.models import ObjectDB
|
||||
from .objects.objects import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
|
||||
from .prototypes.spawner import spawn
|
||||
from .scripts.models import ScriptDB
|
||||
from .scripts.monitorhandler import MONITOR_HANDLER
|
||||
from .scripts.scripts import DefaultScript
|
||||
from .scripts.taskhandler import TASK_HANDLER
|
||||
from .scripts.tickerhandler import TICKER_HANDLER
|
||||
from .server import signals
|
||||
from .server.sessionhandler import SESSION_HANDLER
|
||||
from .typeclasses.attributes import AttributeProperty
|
||||
from .typeclasses.tags import TagProperty
|
||||
from .utils import ansi, gametime, logger
|
||||
from .utils.ansi import ANSIString
|
||||
|
||||
# containers
|
||||
from .utils.containers import GLOBAL_SCRIPTS
|
||||
from .utils.containers import OPTION_CLASSES
|
||||
from .utils.containers import GLOBAL_SCRIPTS, OPTION_CLASSES
|
||||
|
||||
# create functions
|
||||
from .utils.create import (
|
||||
create_account,
|
||||
create_channel,
|
||||
create_help_entry,
|
||||
create_message,
|
||||
create_object,
|
||||
create_script,
|
||||
)
|
||||
from .utils.eveditor import EvEditor
|
||||
from .utils.evform import EvForm
|
||||
from .utils.evmenu import EvMenu
|
||||
from .utils.evmore import EvMore
|
||||
from .utils.evtable import EvTable
|
||||
|
||||
# search functions
|
||||
from .utils.search import (
|
||||
search_account,
|
||||
search_channel,
|
||||
search_help,
|
||||
search_message,
|
||||
search_object,
|
||||
search_script,
|
||||
search_tag,
|
||||
)
|
||||
|
||||
# API containers
|
||||
|
||||
|
|
@ -252,11 +249,11 @@ def _init():
|
|||
|
||||
"""
|
||||
|
||||
from .help.models import HelpEntry
|
||||
from .accounts.models import AccountDB
|
||||
from .scripts.models import ScriptDB
|
||||
from .comms.models import Msg, ChannelDB
|
||||
from .comms.models import ChannelDB, Msg
|
||||
from .help.models import HelpEntry
|
||||
from .objects.models import ObjectDB
|
||||
from .scripts.models import ScriptDB
|
||||
from .server.models import ServerConfig
|
||||
from .typeclasses.attributes import Attribute
|
||||
from .typeclasses.tags import Tag
|
||||
|
|
@ -288,11 +285,11 @@ def _init():
|
|||
|
||||
"""
|
||||
|
||||
from .commands.default.cmdset_character import CharacterCmdSet
|
||||
from .commands.default.cmdset_account import AccountCmdSet
|
||||
from .commands.default.cmdset_unloggedin import UnloggedinCmdSet
|
||||
from .commands.default.cmdset_character import CharacterCmdSet
|
||||
from .commands.default.cmdset_session import SessionCmdSet
|
||||
from .commands.default.muxcommand import MuxCommand, MuxAccountCommand
|
||||
from .commands.default.cmdset_unloggedin import UnloggedinCmdSet
|
||||
from .commands.default.muxcommand import MuxAccountCommand, MuxCommand
|
||||
|
||||
def __init__(self):
|
||||
"populate the object with commands"
|
||||
|
|
@ -305,12 +302,12 @@ def _init():
|
|||
self.__dict__.update(dict([(c.__name__, c) for c in cmdlist]))
|
||||
|
||||
from .commands.default import (
|
||||
account,
|
||||
admin,
|
||||
batchprocess,
|
||||
building,
|
||||
comms,
|
||||
general,
|
||||
account,
|
||||
help,
|
||||
system,
|
||||
unloggedin,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from evennia.contrib.tutorials.evadventure.objects import (
|
|||
EvAdventureObject,
|
||||
EvAdventureObjectFiller,
|
||||
EvAdventureRunestone,
|
||||
EvAdventureWeapon,
|
||||
)
|
||||
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom
|
||||
|
||||
|
|
@ -65,10 +66,6 @@ create_object(
|
|||
# with a static enemy
|
||||
|
||||
combat_room = create_object(EvAdventureRoom, key="Combat Arena", aliases=("evtechdemo#01",))
|
||||
combat_room_enemy = create_object(
|
||||
npcs.EvadventureMob, key="Training Dummy", aliases=("dummy",), location=combat_room
|
||||
)
|
||||
|
||||
# link to/back to hub
|
||||
hub_room = search_object("evtechdemo#00")[0]
|
||||
create_object(
|
||||
|
|
@ -81,3 +78,9 @@ create_object(
|
|||
location=combat_room,
|
||||
destination=hub_room,
|
||||
)
|
||||
# create training dummy with a stick
|
||||
combat_room_enemy = create_object(
|
||||
npcs.EvAdventureMob, key="Training Dummy", aliases=("dummy",), location=combat_room
|
||||
)
|
||||
weapon_stick = create_object(EvAdventureWeapon, key="stick", attributes=(("damage_roll", "1d2"),))
|
||||
combat_room_enemy.weapon = weapon_stick
|
||||
|
|
|
|||
|
|
@ -347,6 +347,8 @@ class LivingMixin:
|
|||
|
||||
"""
|
||||
|
||||
is_pc = False
|
||||
|
||||
@property
|
||||
def hurt_level(self):
|
||||
"""
|
||||
|
|
@ -406,6 +408,46 @@ class LivingMixin:
|
|||
"""
|
||||
pass
|
||||
|
||||
def get_loot(self, looter):
|
||||
"""
|
||||
Called when being looted (after defeat).
|
||||
|
||||
Args:
|
||||
looter (Object): The one doing the looting.
|
||||
|
||||
"""
|
||||
max_steal = rules.dice.roll("1d10")
|
||||
owned = self.coin
|
||||
stolen = max(max_steal, owned)
|
||||
self.coin -= stolen
|
||||
looter.coin += stolen
|
||||
|
||||
self.location.msg_contents(
|
||||
f"$You(looter) loots $You() for {stolen} coins!",
|
||||
from_obj=self,
|
||||
mapping={"looter": looter},
|
||||
)
|
||||
|
||||
def pre_loot(self, defeated_enemy):
|
||||
"""
|
||||
Called just before looting an enemy.
|
||||
|
||||
Args:
|
||||
defeated_enemy (Object): The enemy soon to loot.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def post_loot(self, defeated_enemy):
|
||||
"""
|
||||
Called just after having looted an enemy.
|
||||
|
||||
Args:
|
||||
defeated_enemy (Object): The enemy just looted.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
"""
|
||||
|
|
@ -414,6 +456,8 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
|
||||
"""
|
||||
|
||||
is_pc = True
|
||||
|
||||
# these are the ability bonuses. Defense is always 10 higher
|
||||
strength = AttributeProperty(default=1)
|
||||
dexterity = AttributeProperty(default=1)
|
||||
|
|
@ -429,6 +473,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
hp_max = AttributeProperty(default=4)
|
||||
level = AttributeProperty(default=1)
|
||||
xp = AttributeProperty(default=0)
|
||||
coins = AttributeProperty(default=0) # copper coins
|
||||
|
||||
morale = AttributeProperty(default=9) # only used for NPC/monster morale checks
|
||||
|
||||
|
|
@ -501,13 +546,11 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
|
||||
"""
|
||||
rules.dice.roll_death(self)
|
||||
if hp <= 0:
|
||||
# this means we rolled death on the table
|
||||
self.at_death()
|
||||
else:
|
||||
# still alive, but lost in some stats
|
||||
if self.hp > 0:
|
||||
# still alive, but lost some stats
|
||||
self.location.msg_contents(
|
||||
f"|y$You() $conj(stagger) back, weakened but still alive.|n", from_obj=self
|
||||
f"|y$You() $conj(stagger) back and fall to the ground - alive, but unable to move.|n",
|
||||
from_obj=self,
|
||||
)
|
||||
|
||||
def at_death(self):
|
||||
|
|
@ -516,5 +559,6 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
|||
|
||||
"""
|
||||
self.location.msg_contents(
|
||||
f"|r$You() $conj(collapse) in a heap. No getting back from that.|n", from_obj=self
|
||||
f"|r$You() $conj(collapse) in a heap.\nDeath embraces you ...|n",
|
||||
from_obj=self,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -105,10 +105,12 @@ from datetime import datetime
|
|||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
from evennia.utils import dbserialize, delay, evmenu, evtable, logger
|
||||
from evennia.utils.utils import make_iter
|
||||
from evennia.utils.utils import inherits_from, make_iter
|
||||
|
||||
from . import rules
|
||||
from .characters import EvAdventureCharacter
|
||||
from .enums import Ability
|
||||
from .npcs import EvAdventureNPC
|
||||
|
||||
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
||||
COMBAT_HANDLER_INTERVAL = 60
|
||||
|
|
@ -144,7 +146,7 @@ class CombatAction:
|
|||
|
||||
# the next combat menu node to go to - this ties the combat action into the UI
|
||||
# use None to do nothing (jump directly to registering the action)
|
||||
next_menu_node = "node_select_target"
|
||||
next_menu_node = "node_select_action"
|
||||
|
||||
max_uses = None # None for unlimited
|
||||
# in which order (highest first) to perform the action. If identical, use random order
|
||||
|
|
@ -246,6 +248,7 @@ class CombatActionAttack(CombatAction):
|
|||
desc = "[A]ttack/[C]ast spell at <target>"
|
||||
aliases = ("a", "c", "attack", "cast")
|
||||
help_text = "Make an attack using your currently equipped weapon/spell rune"
|
||||
next_menu_node = "node_select_enemy_target"
|
||||
|
||||
priority = 1
|
||||
|
||||
|
|
@ -255,7 +258,7 @@ class CombatActionAttack(CombatAction):
|
|||
|
||||
"""
|
||||
attacker = self.combatant
|
||||
weapon = self.combatant.equipment.weapon
|
||||
weapon = self.combatant.weapon
|
||||
|
||||
# figure out advantage (gained by previous stunts)
|
||||
advantage = bool(self.combathandler.advantage_matrix[attacker].pop(defender, False))
|
||||
|
|
@ -266,14 +269,14 @@ class CombatActionAttack(CombatAction):
|
|||
attacker,
|
||||
defender,
|
||||
attack_type=weapon.attack_type,
|
||||
defense_type=attacker.equipment.weapon.defense_type,
|
||||
defense_type=attacker.weapon.defense_type,
|
||||
advantage=advantage,
|
||||
disadvantage=disadvantage,
|
||||
)
|
||||
self.msg(f"$You() $conj(attack) $You({defender.key}) with {weapon.key}: {txt}")
|
||||
if is_hit:
|
||||
# enemy hit, calculate damage
|
||||
weapon_dmg_roll = attacker.equipment.weapon.damage_roll
|
||||
weapon_dmg_roll = attacker.weapon.damage_roll
|
||||
|
||||
dmg = rules.dice.roll(weapon_dmg_roll)
|
||||
|
||||
|
|
@ -299,7 +302,7 @@ class CombatActionAttack(CombatAction):
|
|||
# a miss
|
||||
message = f" $You() $conj(miss) $You({defender.key})."
|
||||
if quality is Ability.CRITICAL_FAILURE:
|
||||
attacker.equipment.weapon.quality -= 1
|
||||
attacker.weapon.quality -= 1
|
||||
message += ".. it's a |rcritical miss!|n, damaging the weapon."
|
||||
self.msg(message)
|
||||
|
||||
|
|
@ -325,6 +328,7 @@ class CombatActionStunt(CombatAction):
|
|||
"A stunt does not cause damage but grants/gives advantage/disadvantage to future "
|
||||
"actions. The effect needs to be used up within 5 turns."
|
||||
)
|
||||
next_menu_node = "node_select_enemy_target"
|
||||
|
||||
give_advantage = True # if False, give_disadvantage
|
||||
max_uses = 1
|
||||
|
|
@ -392,6 +396,7 @@ class CombatActionUseItem(CombatAction):
|
|||
desc = "[U]se item"
|
||||
aliases = ("u", "item", "use item")
|
||||
help_text = "Use an item from your inventory."
|
||||
next_menu_node = "node_select_friendly_target"
|
||||
|
||||
def get_help(self, item, *args):
|
||||
return item.get_help(*args)
|
||||
|
|
@ -456,7 +461,7 @@ class CombatActionFlee(CombatAction):
|
|||
aliases = ("d", "disengage", "flee")
|
||||
|
||||
# this only affects us
|
||||
next_menu_node = "node_register_action"
|
||||
next_menu_node = "node_confirm_register_action"
|
||||
|
||||
help_text = (
|
||||
"Disengage from combat. Use successfully two times in a row to leave combat at the "
|
||||
|
|
@ -487,6 +492,7 @@ class CombatActionBlock(CombatAction):
|
|||
"Move to block a target from fleeing combat. If you succeed "
|
||||
"in a DEX vs DEX challenge, they don't get away."
|
||||
)
|
||||
next_menu_node = "node_select_enemy_target"
|
||||
|
||||
priority = -1 # must be checked BEFORE the flee action of the target!
|
||||
|
||||
|
|
@ -532,7 +538,7 @@ class CombatActionDoNothing(CombatAction):
|
|||
help_text = "Hold you position, doing nothing."
|
||||
|
||||
# affects noone else
|
||||
next_menu_node = "node_register_action"
|
||||
next_menu_node = "node_confirm_register_action"
|
||||
|
||||
post_action_text = "{combatant} does nothing this turn."
|
||||
|
||||
|
|
@ -587,6 +593,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
disadvantage_matrix = AttributeProperty(defaultdict(dict))
|
||||
|
||||
fleeing_combatants = AttributeProperty(list())
|
||||
defeated_combatants = AttributeProperty(list())
|
||||
|
||||
_warn_time_task = None
|
||||
|
||||
|
|
@ -621,8 +628,10 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
combatant,
|
||||
{
|
||||
"node_wait_start": node_wait_start,
|
||||
"node_select_target": node_select_target,
|
||||
"node_select_enemy_target": node_select_enemy_target,
|
||||
"node_select_friendly_target": node_select_friendly_target,
|
||||
"node_select_action": node_select_action,
|
||||
"node_select_wield_from_inventory": node_select_wield_from_inventory,
|
||||
"node_wait_turn": node_wait_turn,
|
||||
},
|
||||
startnode="node_wait_turn",
|
||||
|
|
@ -660,18 +669,24 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
self.msg(f"|y_______________________ start turn {self.turn} ___________________________|n")
|
||||
|
||||
for combatant in self.combatants:
|
||||
# cycle combat menu
|
||||
self._init_menu(combatant)
|
||||
combatant.ndb._evmenu.goto("node_select_action", "")
|
||||
if hasattr(combatant, "ai_combat_next_action"):
|
||||
# NPC needs to get a decision from the AI
|
||||
next_action_key, args, kwargs = combatant.ai_combat_next_action(self)
|
||||
self.register_action(combatant, next_action_key, *args, **kwargs)
|
||||
else:
|
||||
# cycle combat menu for PC
|
||||
self._init_menu(combatant)
|
||||
combatant.ndb._evmenu.goto("node_select_action", "")
|
||||
|
||||
def _end_turn(self):
|
||||
"""
|
||||
End of turn operations.
|
||||
|
||||
1. Do all regular actions
|
||||
2. Roll for any death events
|
||||
2. Remove combatants that disengaged successfully
|
||||
3. Timeout advantages/disadvantages
|
||||
2. Check if fleeing combatants got away - remove them from combat
|
||||
3. Check if anyone has hp <= - defeated
|
||||
4. Check if any one side is alone on the battlefield - they loot the defeated
|
||||
5. If combat is still on, update stunt timers
|
||||
|
||||
"""
|
||||
self.msg(
|
||||
|
|
@ -702,29 +717,66 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
|
||||
# handle disengaging combatants
|
||||
|
||||
to_remove = []
|
||||
to_flee = []
|
||||
to_defeat = []
|
||||
|
||||
for combatant in self.combatants:
|
||||
# see if fleeing characters managed to do two flee actions in a row.
|
||||
if (combatant in self.fleeing_combatants) and (combatant in already_fleeing):
|
||||
self.fleeing_combatants.remove(combatant)
|
||||
to_remove.append(combatant)
|
||||
to_flee.append(combatant)
|
||||
|
||||
if combatant.hp <= 0:
|
||||
# check characters that are beaten down.
|
||||
# characters roll on the death table here, npcs usually just die
|
||||
# characters roll on the death table here; but even if they survive, they
|
||||
# count as defeated (unconcious) for this combat.
|
||||
combatant.at_defeat()
|
||||
if combatant.hp <= 0:
|
||||
# if character still < 0 after at_defeat, it means they are dead.
|
||||
# force-remove from combat.
|
||||
to_remove.append(combatant)
|
||||
to_defeat.append(combatant)
|
||||
|
||||
for combatant in to_remove:
|
||||
# for clarity, we remove here rather than modifying the combatant list
|
||||
# inside the previous loop
|
||||
self.msg(f"|y$You() $conj(are) out of combat.|n", combatant=combatant)
|
||||
for combatant in to_flee:
|
||||
# combatant leaving combat by fleeing
|
||||
self.msg(f"|y$You() successfully $conj(flee) from combat.|n", combatant=combatant)
|
||||
self.remove_combatant(combatant)
|
||||
|
||||
for combatant in to_defeat:
|
||||
# combatants leaving combat by being defeated
|
||||
self.msg("|r$You() $conj(fall) to the ground, defeated.|n", combatant=combatant)
|
||||
self.combatants.remove(combatant)
|
||||
self.defeated_combatants.append(combatant)
|
||||
|
||||
# check if only one side remains, divide into allies and enemies based on the first
|
||||
# combatant,then check if either team is empty.
|
||||
if not self.combatants:
|
||||
# everyone's defeated at the same time. This is a tie where everyone loses and
|
||||
# no looting happens.
|
||||
self.msg("|yEveryone takes everyone else out. Today, noone wins.|n")
|
||||
self.stop_combat()
|
||||
return
|
||||
else:
|
||||
combatant = self.combatants[0]
|
||||
allies = self.get_friendly_targets(combatant) # will always contain at least combatant
|
||||
enemies = self.get_enemy_targets(combatant)
|
||||
|
||||
if not enemies:
|
||||
# no enemies left - allies to combatant won!
|
||||
defeated_enemies = self.get_enemy_targets(
|
||||
combatant, all_combatants=self.defeated_combatants
|
||||
)
|
||||
|
||||
# all surviving allies loot the fallen enemies
|
||||
for ally in allies:
|
||||
for enemy in defeated_enemies:
|
||||
try:
|
||||
ally.pre_loot(enemy)
|
||||
enemy.get_loot(ally)
|
||||
ally.post_loot(enemy)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
self.stop_combat()
|
||||
return
|
||||
|
||||
# if we get here, combat is still on
|
||||
|
||||
# refresh stunt timeouts (note - self.stunt_duration is the same for
|
||||
# all stunts; # for more complex use we could store the action and let action have a
|
||||
# 'duration' property to use instead.
|
||||
|
|
@ -753,10 +805,6 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
self.advantage_matrix = new_advantage_matrix
|
||||
self.disadvantage_matrix = new_disadvantage_matrix
|
||||
|
||||
if len(self.combatants) == 1:
|
||||
# only one combatant left - abort combat
|
||||
self.stop_combat()
|
||||
|
||||
def add_combatant(self, combatant, session=None):
|
||||
"""
|
||||
Add combatant to battle.
|
||||
|
|
@ -775,7 +823,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
"""
|
||||
if combatant not in self.combatants:
|
||||
self.combatants.append(combatant)
|
||||
combatant.db.turnbased_combathandler = self
|
||||
combatant.db.combathandler = self
|
||||
|
||||
# allow custom character actions (not used by default)
|
||||
custom_action_classes = combatant.db.custom_combat_actions or []
|
||||
|
|
@ -798,7 +846,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
self.combatants.remove(combatant)
|
||||
self.combatant_actions.pop(combatant, None)
|
||||
combatant.ndb._evmenu.close_menu()
|
||||
del combatant.db.turnbased_combathandler
|
||||
del combatant.db.combathandler
|
||||
|
||||
def start_combat(self):
|
||||
"""
|
||||
|
|
@ -821,6 +869,73 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
for combatant in self.combatants:
|
||||
self.remove_combatant(combatant)
|
||||
|
||||
def get_enemy_targets(self, combatant, excluded=None, all_combatants=None):
|
||||
"""
|
||||
Get all valid targets the given combatant can target for an attack. This does not apply for
|
||||
'friendly' targeting (like wanting to cast a heal on someone). We assume there are two types
|
||||
of combatants - PCs (player-controlled characters and NPCs (AI-controlled). Here, we assume
|
||||
npcs can never attack one another (or themselves)
|
||||
|
||||
For PCs to be able to target each other, the `allow_pvp`
|
||||
Attribute flag must be set on the current `Room`.
|
||||
|
||||
Args:
|
||||
combatant (Object): The combatant looking for targets.
|
||||
excluded (list, optional): If given, these are not valid targets - this can be used to
|
||||
avoid friendly NPCs.
|
||||
all_combatants (list, optional): If given, use this list to get all combatants, instead
|
||||
of using `self.combatants`.
|
||||
|
||||
"""
|
||||
is_pc = not inherits_from(combatant, EvAdventureNPC)
|
||||
allow_pvp = self.obj.allow_pvp
|
||||
targets = []
|
||||
combatants = all_combatants or self.combatants
|
||||
|
||||
if is_pc:
|
||||
if allow_pvp:
|
||||
# PCs may target everyone, including other PCs
|
||||
targets = combatants
|
||||
else:
|
||||
# PCs may only attack NPCs
|
||||
targets = [target for target in combatants if inherits_from(target, EvAdventureNPC)]
|
||||
|
||||
else:
|
||||
# NPCs may only attack PCs, not each other
|
||||
targets = [target for target in combatants if not inherits_from(target, EvAdventureNPC)]
|
||||
|
||||
if excluded:
|
||||
targets = [target for target in targets if target not in excluded]
|
||||
|
||||
return targets
|
||||
|
||||
def get_friendly_targets(self, combatant, extra=None, all_combatants=None):
|
||||
"""
|
||||
Get a list of all 'friendly' or neutral targets a combatant may target, including
|
||||
themselves.
|
||||
|
||||
Args:
|
||||
combatant (Object): The combatant looking for targets.
|
||||
extra (list, optional): If given, these are additional targets that can be
|
||||
considered target for allied effects (could be used for a friendly NPC).
|
||||
all_combatants (list, optional): If given, use this list to get all combatants, instead
|
||||
of using `self.combatants`.
|
||||
|
||||
"""
|
||||
is_pc = not inherits_from(combatant, EvAdventureNPC)
|
||||
combatants = all_combatants or self.combatants
|
||||
if is_pc:
|
||||
# can target other PCs
|
||||
targets = [target for target in combatants if not inherits_from(target, EvAdventureNPC)]
|
||||
else:
|
||||
# can target other NPCs
|
||||
targets = [target for target in combatants if inherits_from(target, EvAdventureNPC)]
|
||||
|
||||
if extra:
|
||||
targets = list(set(target + extra))
|
||||
|
||||
return targets
|
||||
|
||||
def get_combat_summary(self, combatant):
|
||||
"""
|
||||
Get a summary of the current combat state from the perspective of a
|
||||
|
|
@ -973,7 +1088,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
|||
|
||||
def _register_action(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Register action with handler.
|
||||
Actually register action with handler.
|
||||
|
||||
"""
|
||||
action_key = kwargs.pop("action_key")
|
||||
|
|
@ -987,22 +1102,41 @@ def _register_action(caller, raw_string, **kwargs):
|
|||
return "node_wait_turn"
|
||||
|
||||
|
||||
def node_select_target(caller, raw_string, **kwargs):
|
||||
def node_confirm_register_action(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Menu node allowing for selecting a target among all combatants. This combines
|
||||
with all other actions.
|
||||
Node where one can confirm registering the action or change one's mind.
|
||||
|
||||
"""
|
||||
action_key = kwargs["action_key"]
|
||||
action_target = kwargs.get("action_target", None) or ""
|
||||
if action_target:
|
||||
action_target = f", targeting {action_target.key}"
|
||||
|
||||
text = f"You will {action_key}{action_target}. Confirm? [Y]/n"
|
||||
options = (
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": (_register_action, kwargs),
|
||||
},
|
||||
{"key": ("Abort/Cancel", "abort", "cancel", "a", "no", "n"), "goto": "node_select_action"},
|
||||
)
|
||||
|
||||
|
||||
def _select_target_helper(caller, raw_string, targets, **kwargs):
|
||||
"""
|
||||
Helper to select among only friendly or enemy targets (given by the calling node).
|
||||
|
||||
"""
|
||||
combat = caller.ndb._evmenu.combathandler
|
||||
action_key = kwargs["action_key"]
|
||||
friendly_target = kwargs.get("target_friendly", False)
|
||||
text = f"Select target for |w{action_key}|n."
|
||||
|
||||
# make the apply-self option always the first one, give it key 0
|
||||
kwargs["action_target"] = caller
|
||||
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
|
||||
# filter out ourselves and then make options for everyone else
|
||||
combatants = [combatant for combatant in combat.combatants if combatant is not caller]
|
||||
for inum, combatant in enumerate(combatants):
|
||||
for inum, combatant in enumerate(targets):
|
||||
kwargs["action_target"] = combatant
|
||||
options.append(
|
||||
{"key": str(inum + 1), "desc": combatant.key, "goto": (_register_action, kwargs)}
|
||||
|
|
@ -1014,6 +1148,25 @@ def node_select_target(caller, raw_string, **kwargs):
|
|||
return text, options
|
||||
|
||||
|
||||
def node_select_enemy_target(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Menu node allowing for selecting an enemy target among all combatants. This combines
|
||||
with all other actions.
|
||||
|
||||
"""
|
||||
targets = combat.get_enemy_targets(caller)
|
||||
return _select_target_helper(caller, raw_string, targets, **kwargs)
|
||||
|
||||
|
||||
def node_select_friendly_target(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Menu node for selecting a friendly target among combatants (including oneself).
|
||||
|
||||
"""
|
||||
targets = combat.get_friendly_targets(caller)
|
||||
return _select_target_helper(caller, raw_string, targets, **kwargs)
|
||||
|
||||
|
||||
def _item_broken(caller, raw_string, **kwargs):
|
||||
caller.msg("|rThis item is broken and unusable!|n")
|
||||
return None # back to previous node
|
||||
|
|
@ -1080,9 +1233,7 @@ def node_select_use_item_from_inventory(caller, raw_string, **kwargs):
|
|||
options.append({"desc": str(obj), "goto": (_register_action, kwargs)})
|
||||
|
||||
# add ability to cancel
|
||||
options.append(
|
||||
{"key": "_default", "desc": "(No input to Abort and go back)", "goto": "node_select_action"}
|
||||
)
|
||||
options.append({"key": "_default", "goto": "node_select_action"})
|
||||
|
||||
return text, options
|
||||
|
||||
|
|
@ -1236,6 +1387,9 @@ def join_combat(caller, *targets, session=None):
|
|||
if not location:
|
||||
raise CombatFailure("Must have a location to start combat.")
|
||||
|
||||
if not getattr(location, "allow_combat", False):
|
||||
raise CombatFailure("This is not the time and place for picking a fight.")
|
||||
|
||||
if not targets:
|
||||
raise CombatFailure("Must have an opponent to start combat.")
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ class Ability(Enum):
|
|||
CRITICAL_FAILURE = "critical_failure"
|
||||
CRITICAL_SUCCESS = "critical_success"
|
||||
|
||||
ALLEGIANCE_HOSTILE = "hostile"
|
||||
ALLEGIANCE_NEUTRAL = "neutral"
|
||||
ALLEGIANCE_FRIENDLY = "friendly"
|
||||
|
||||
|
||||
class WieldLocation(Enum):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@
|
|||
EvAdventure NPCs. This includes both friends and enemies, only separated by their AI.
|
||||
|
||||
"""
|
||||
from random import choice
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
|
||||
from .characters import LivingMixin
|
||||
from .enums import Ability
|
||||
from .objects import WeaponEmptyHand
|
||||
|
||||
|
||||
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||
|
|
@ -27,12 +30,25 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
|||
If wanting monsters or NPCs that can level and work the same as PCs, base them off the
|
||||
EvAdventureCharacter class instead.
|
||||
|
||||
The weapon of the npc is stored as an Attribute instead of implementing a full
|
||||
inventory/equipment system. This means that the normal inventory can be used for
|
||||
non-combat purposes (or for loot to get when killing an enemy).
|
||||
|
||||
"""
|
||||
|
||||
hit_dice = AttributeProperty(default=1)
|
||||
armor = AttributeProperty(default=1) # +10 to get armor defense
|
||||
morale = AttributeProperty(default=9)
|
||||
hp = AttributeProperty(default=8)
|
||||
is_pc = False
|
||||
|
||||
hit_dice = AttributeProperty(default=1, autocreate=False)
|
||||
armor = AttributeProperty(default=1, autocreate=False) # +10 to get armor defense
|
||||
morale = AttributeProperty(default=9, autocreate=False)
|
||||
hp_multiplier = AttributeProperty(default=4, autocreate=False) # 4 default in Knave
|
||||
hp = AttributeProperty(default=None, autocreate=False) # internal tracking, use .hp property
|
||||
allegiance = AttributeProperty(default=Ability.ALLEGIANCE_HOSTILE, autocreate=False)
|
||||
|
||||
is_idle = AttributeProperty(default=False, autocreate=False)
|
||||
|
||||
weapon = AttributeProperty(default=WeaponEmptyHand, autocreate=False) # instead of inventory
|
||||
coins = AttributeProperty(default=1, autocreate=False) # coin loot
|
||||
|
||||
@property
|
||||
def strength(self):
|
||||
|
|
@ -60,7 +76,7 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
|||
|
||||
@property
|
||||
def hp_max(self):
|
||||
return self.hit_dice * 4
|
||||
return self.hit_dice * self.hp_multiplier
|
||||
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
|
|
@ -98,6 +114,36 @@ class EvAdventureMob(EvAdventureNPC):
|
|||
|
||||
"""
|
||||
|
||||
def ai_combat_next_action(self, combathandler):
|
||||
"""
|
||||
Called to get the next action in combat.
|
||||
|
||||
Args:
|
||||
combathandler (EvAdventureCombatHandler): The currently active combathandler.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple `(str, tuple, dict)`, being the `action_key`, and the `*args` and
|
||||
`**kwargs` for that action. The action-key is that of a CombatAction available to the
|
||||
combatant in the current combat handler.
|
||||
|
||||
"""
|
||||
from .combat_turnbased import CombatActionAttack, CombatActionDoNothing
|
||||
|
||||
if self.is_idle:
|
||||
# mob just stands around
|
||||
return CombatActionDoNothing.key, (), {}
|
||||
|
||||
target = choice(combathandler.get_enemy_targets(self))
|
||||
|
||||
# simply randomly decide what action to take
|
||||
action = choice(
|
||||
(
|
||||
CombatActionAttack,
|
||||
CombatActionDoNothing,
|
||||
)
|
||||
)
|
||||
return action.key, (target,), {}
|
||||
|
||||
def at_defeat(self):
|
||||
"""
|
||||
Mobs die right away when defeated, no death-table rolls.
|
||||
|
|
|
|||
|
|
@ -5,8 +5,35 @@ EvAdventure rooms.
|
|||
|
||||
"""
|
||||
|
||||
from evennia import DefaultRoom
|
||||
from evennia import AttributeProperty, DefaultRoom
|
||||
|
||||
|
||||
class EvAdventureRoom(DefaultRoom):
|
||||
pass
|
||||
"""
|
||||
Simple room supporting some EvAdventure-specifics.
|
||||
|
||||
"""
|
||||
|
||||
allow_combat = False
|
||||
allow_pvp = False
|
||||
allow_death = False
|
||||
|
||||
|
||||
class EvAdventurePvPRoom(DefaultRoom):
|
||||
"""
|
||||
Room where PvP can happen, but noone gets killed.
|
||||
|
||||
"""
|
||||
|
||||
allow_combat = True
|
||||
allow_pvp = True
|
||||
|
||||
|
||||
class EvAdventureDungeonRoom(EvAdventureRoom):
|
||||
"""
|
||||
Dangerous dungeon room.
|
||||
|
||||
"""
|
||||
|
||||
allow_combat = True
|
||||
allow_death = True
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ class EvAdventureRollEngine:
|
|||
|
||||
result = self.roll_random_table("1d8", death_table)
|
||||
if result == "dead":
|
||||
character.handle_death()
|
||||
character.at_death()
|
||||
else:
|
||||
# survives with degraded abilities (1d4 roll)
|
||||
abi = self.death_map[result]
|
||||
|
|
@ -339,7 +339,7 @@ class EvAdventureRollEngine:
|
|||
|
||||
if current_abi < -10:
|
||||
# can't lose more - die
|
||||
character.handle_death()
|
||||
character.at_death()
|
||||
else:
|
||||
# refresh health, but get permanent ability loss
|
||||
new_hp = max(character.hp_max, self.roll("1d4"))
|
||||
|
|
|
|||
|
|
@ -43,9 +43,14 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
|||
)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.location.allow_combat = True
|
||||
self.location.allow_death = True
|
||||
self.combatant = self.character
|
||||
self.target = create.create_object(
|
||||
EvAdventureMob, key="testmonster", location=self.location
|
||||
EvAdventureMob,
|
||||
key="testmonster",
|
||||
location=self.location,
|
||||
attributes=(("is_idle", True),),
|
||||
)
|
||||
|
||||
# this already starts turn 1
|
||||
|
|
@ -56,10 +61,10 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
|||
self.target.delete()
|
||||
|
||||
def test_remove_combatant(self):
|
||||
self.assertTrue(bool(self.combatant.db.turnbased_combathandler))
|
||||
self.assertTrue(bool(self.combatant.db.combathandler))
|
||||
self.combathandler.remove_combatant(self.combatant)
|
||||
self.assertFalse(self.combatant in self.combathandler.combatants)
|
||||
self.assertFalse(bool(self.combatant.db.turnbased_combathandler))
|
||||
self.assertFalse(bool(self.combatant.db.combathandler))
|
||||
|
||||
def test_start_turn(self):
|
||||
self.combathandler._start_turn()
|
||||
|
|
@ -150,9 +155,13 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
|
|||
)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.location.allow_combat = True
|
||||
self.location.allow_death = True
|
||||
self.combatant = self.character
|
||||
self.combatant2 = create.create_object(EvAdventureCharacter, key="testcharacter2")
|
||||
self.target = create.create_object(EvAdventureMob, key="testmonster")
|
||||
self.target = create.create_object(
|
||||
EvAdventureMob, key="testmonster", attributes=(("is_idle", True),)
|
||||
)
|
||||
self.target.hp = 4
|
||||
|
||||
# this already starts turn 1
|
||||
|
|
@ -186,6 +195,7 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
|
|||
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
|
||||
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
||||
self.assertEqual(self.target.hp, -7)
|
||||
self.assertTrue(self.target not in self.combathandler.combatants)
|
||||
|
||||
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||
def test_stunt_fail(self, mock_randint):
|
||||
|
|
|
|||
|
|
@ -13,9 +13,8 @@ from collections import defaultdict
|
|||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from evennia.utils.utils import to_str, make_iter
|
||||
from evennia.locks.lockfuncs import perm as perm_lockfunc
|
||||
|
||||
from evennia.utils.utils import make_iter, to_str
|
||||
|
||||
_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE
|
||||
|
||||
|
|
@ -105,6 +104,10 @@ class TagProperty:
|
|||
with an existing method/property on the class. If it does, you must use tags.add()
|
||||
instead.
|
||||
|
||||
Note that while you _can_ check e.g. `obj.tagname,this will give an AttributeError
|
||||
if the Tag is not set. Most often you want to use `obj.tags.get("tagname")` to check
|
||||
if a tag is set on an object.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue