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
|
ChannelDB = None
|
||||||
Msg = None
|
Msg = None
|
||||||
|
|
||||||
|
# Properties
|
||||||
|
AttributeProperty = None
|
||||||
|
TagProperty = None
|
||||||
|
|
||||||
# commands
|
# commands
|
||||||
Command = None
|
Command = None
|
||||||
CmdSet = None
|
CmdSet = None
|
||||||
|
|
@ -106,7 +110,7 @@ def _create_version():
|
||||||
Helper function for building the version string
|
Helper function for building the version string
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from subprocess import check_output, CalledProcessError, STDOUT
|
from subprocess import STDOUT, CalledProcessError, check_output
|
||||||
|
|
||||||
version = "Unknown"
|
version = "Unknown"
|
||||||
root = os.path.dirname(os.path.abspath(__file__))
|
root = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
@ -153,70 +157,63 @@ def _init():
|
||||||
global GLOBAL_SCRIPTS, OPTION_CLASSES
|
global GLOBAL_SCRIPTS, OPTION_CLASSES
|
||||||
global EvMenu, EvTable, EvForm, EvMore, EvEditor
|
global EvMenu, EvTable, EvForm, EvMore, EvEditor
|
||||||
global ANSIString
|
global ANSIString
|
||||||
|
global AttributeProperty, TagProperty
|
||||||
|
|
||||||
# Parent typeclasses
|
# 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
|
# utilities
|
||||||
from django.conf import settings
|
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 . import contrib
|
||||||
from .scripts.tickerhandler import TICKER_HANDLER
|
from .accounts.accounts import DefaultAccount, DefaultGuest
|
||||||
from .scripts.taskhandler import TASK_HANDLER
|
from .accounts.models import AccountDB
|
||||||
from .server.sessionhandler import SESSION_HANDLER
|
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.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
|
# containers
|
||||||
from .utils.containers import GLOBAL_SCRIPTS
|
from .utils.containers import GLOBAL_SCRIPTS, OPTION_CLASSES
|
||||||
from .utils.containers import 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
|
# API containers
|
||||||
|
|
||||||
|
|
@ -252,11 +249,11 @@ def _init():
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .help.models import HelpEntry
|
|
||||||
from .accounts.models import AccountDB
|
from .accounts.models import AccountDB
|
||||||
from .scripts.models import ScriptDB
|
from .comms.models import ChannelDB, Msg
|
||||||
from .comms.models import Msg, ChannelDB
|
from .help.models import HelpEntry
|
||||||
from .objects.models import ObjectDB
|
from .objects.models import ObjectDB
|
||||||
|
from .scripts.models import ScriptDB
|
||||||
from .server.models import ServerConfig
|
from .server.models import ServerConfig
|
||||||
from .typeclasses.attributes import Attribute
|
from .typeclasses.attributes import Attribute
|
||||||
from .typeclasses.tags import Tag
|
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_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.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):
|
def __init__(self):
|
||||||
"populate the object with commands"
|
"populate the object with commands"
|
||||||
|
|
@ -305,12 +302,12 @@ def _init():
|
||||||
self.__dict__.update(dict([(c.__name__, c) for c in cmdlist]))
|
self.__dict__.update(dict([(c.__name__, c) for c in cmdlist]))
|
||||||
|
|
||||||
from .commands.default import (
|
from .commands.default import (
|
||||||
|
account,
|
||||||
admin,
|
admin,
|
||||||
batchprocess,
|
batchprocess,
|
||||||
building,
|
building,
|
||||||
comms,
|
comms,
|
||||||
general,
|
general,
|
||||||
account,
|
|
||||||
help,
|
help,
|
||||||
system,
|
system,
|
||||||
unloggedin,
|
unloggedin,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ from evennia.contrib.tutorials.evadventure.objects import (
|
||||||
EvAdventureObject,
|
EvAdventureObject,
|
||||||
EvAdventureObjectFiller,
|
EvAdventureObjectFiller,
|
||||||
EvAdventureRunestone,
|
EvAdventureRunestone,
|
||||||
|
EvAdventureWeapon,
|
||||||
)
|
)
|
||||||
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom
|
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom
|
||||||
|
|
||||||
|
|
@ -65,10 +66,6 @@ create_object(
|
||||||
# with a static enemy
|
# with a static enemy
|
||||||
|
|
||||||
combat_room = create_object(EvAdventureRoom, key="Combat Arena", aliases=("evtechdemo#01",))
|
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
|
# link to/back to hub
|
||||||
hub_room = search_object("evtechdemo#00")[0]
|
hub_room = search_object("evtechdemo#00")[0]
|
||||||
create_object(
|
create_object(
|
||||||
|
|
@ -81,3 +78,9 @@ create_object(
|
||||||
location=combat_room,
|
location=combat_room,
|
||||||
destination=hub_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
|
@property
|
||||||
def hurt_level(self):
|
def hurt_level(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -406,6 +408,46 @@ class LivingMixin:
|
||||||
"""
|
"""
|
||||||
pass
|
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):
|
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
|
# these are the ability bonuses. Defense is always 10 higher
|
||||||
strength = AttributeProperty(default=1)
|
strength = AttributeProperty(default=1)
|
||||||
dexterity = AttributeProperty(default=1)
|
dexterity = AttributeProperty(default=1)
|
||||||
|
|
@ -429,6 +473,7 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
hp_max = AttributeProperty(default=4)
|
hp_max = AttributeProperty(default=4)
|
||||||
level = AttributeProperty(default=1)
|
level = AttributeProperty(default=1)
|
||||||
xp = AttributeProperty(default=0)
|
xp = AttributeProperty(default=0)
|
||||||
|
coins = AttributeProperty(default=0) # copper coins
|
||||||
|
|
||||||
morale = AttributeProperty(default=9) # only used for NPC/monster morale checks
|
morale = AttributeProperty(default=9) # only used for NPC/monster morale checks
|
||||||
|
|
||||||
|
|
@ -501,13 +546,11 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
rules.dice.roll_death(self)
|
rules.dice.roll_death(self)
|
||||||
if hp <= 0:
|
if self.hp > 0:
|
||||||
# this means we rolled death on the table
|
# still alive, but lost some stats
|
||||||
self.at_death()
|
|
||||||
else:
|
|
||||||
# still alive, but lost in some stats
|
|
||||||
self.location.msg_contents(
|
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):
|
def at_death(self):
|
||||||
|
|
@ -516,5 +559,6 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.location.msg_contents(
|
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.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 make_iter
|
from evennia.utils.utils import inherits_from, make_iter
|
||||||
|
|
||||||
from . import rules
|
from . import rules
|
||||||
|
from .characters import EvAdventureCharacter
|
||||||
from .enums import Ability
|
from .enums import Ability
|
||||||
|
from .npcs import EvAdventureNPC
|
||||||
|
|
||||||
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
|
||||||
COMBAT_HANDLER_INTERVAL = 60
|
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
|
# 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)
|
# 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
|
max_uses = None # None for unlimited
|
||||||
# in which order (highest first) to perform the action. If identical, use random order
|
# 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>"
|
desc = "[A]ttack/[C]ast spell at <target>"
|
||||||
aliases = ("a", "c", "attack", "cast")
|
aliases = ("a", "c", "attack", "cast")
|
||||||
help_text = "Make an attack using your currently equipped weapon/spell rune"
|
help_text = "Make an attack using your currently equipped weapon/spell rune"
|
||||||
|
next_menu_node = "node_select_enemy_target"
|
||||||
|
|
||||||
priority = 1
|
priority = 1
|
||||||
|
|
||||||
|
|
@ -255,7 +258,7 @@ class CombatActionAttack(CombatAction):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
attacker = self.combatant
|
attacker = self.combatant
|
||||||
weapon = self.combatant.equipment.weapon
|
weapon = self.combatant.weapon
|
||||||
|
|
||||||
# figure out advantage (gained by previous stunts)
|
# figure out advantage (gained by previous stunts)
|
||||||
advantage = bool(self.combathandler.advantage_matrix[attacker].pop(defender, False))
|
advantage = bool(self.combathandler.advantage_matrix[attacker].pop(defender, False))
|
||||||
|
|
@ -266,14 +269,14 @@ class CombatActionAttack(CombatAction):
|
||||||
attacker,
|
attacker,
|
||||||
defender,
|
defender,
|
||||||
attack_type=weapon.attack_type,
|
attack_type=weapon.attack_type,
|
||||||
defense_type=attacker.equipment.weapon.defense_type,
|
defense_type=attacker.weapon.defense_type,
|
||||||
advantage=advantage,
|
advantage=advantage,
|
||||||
disadvantage=disadvantage,
|
disadvantage=disadvantage,
|
||||||
)
|
)
|
||||||
self.msg(f"$You() $conj(attack) $You({defender.key}) with {weapon.key}: {txt}")
|
self.msg(f"$You() $conj(attack) $You({defender.key}) with {weapon.key}: {txt}")
|
||||||
if is_hit:
|
if is_hit:
|
||||||
# enemy hit, calculate damage
|
# 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)
|
dmg = rules.dice.roll(weapon_dmg_roll)
|
||||||
|
|
||||||
|
|
@ -299,7 +302,7 @@ class CombatActionAttack(CombatAction):
|
||||||
# a miss
|
# a miss
|
||||||
message = f" $You() $conj(miss) $You({defender.key})."
|
message = f" $You() $conj(miss) $You({defender.key})."
|
||||||
if quality is Ability.CRITICAL_FAILURE:
|
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."
|
message += ".. it's a |rcritical miss!|n, damaging the weapon."
|
||||||
self.msg(message)
|
self.msg(message)
|
||||||
|
|
||||||
|
|
@ -325,6 +328,7 @@ class CombatActionStunt(CombatAction):
|
||||||
"A stunt does not cause damage but grants/gives advantage/disadvantage to future "
|
"A stunt does not cause damage but grants/gives advantage/disadvantage to future "
|
||||||
"actions. The effect needs to be used up within 5 turns."
|
"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
|
give_advantage = True # if False, give_disadvantage
|
||||||
max_uses = 1
|
max_uses = 1
|
||||||
|
|
@ -392,6 +396,7 @@ class CombatActionUseItem(CombatAction):
|
||||||
desc = "[U]se item"
|
desc = "[U]se item"
|
||||||
aliases = ("u", "item", "use item")
|
aliases = ("u", "item", "use item")
|
||||||
help_text = "Use an item from your inventory."
|
help_text = "Use an item from your inventory."
|
||||||
|
next_menu_node = "node_select_friendly_target"
|
||||||
|
|
||||||
def get_help(self, item, *args):
|
def get_help(self, item, *args):
|
||||||
return item.get_help(*args)
|
return item.get_help(*args)
|
||||||
|
|
@ -456,7 +461,7 @@ class CombatActionFlee(CombatAction):
|
||||||
aliases = ("d", "disengage", "flee")
|
aliases = ("d", "disengage", "flee")
|
||||||
|
|
||||||
# this only affects us
|
# this only affects us
|
||||||
next_menu_node = "node_register_action"
|
next_menu_node = "node_confirm_register_action"
|
||||||
|
|
||||||
help_text = (
|
help_text = (
|
||||||
"Disengage from combat. Use successfully two times in a row to leave combat at the "
|
"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 "
|
"Move to block a target from fleeing combat. If you succeed "
|
||||||
"in a DEX vs DEX challenge, they don't get away."
|
"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!
|
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."
|
help_text = "Hold you position, doing nothing."
|
||||||
|
|
||||||
# affects noone else
|
# affects noone else
|
||||||
next_menu_node = "node_register_action"
|
next_menu_node = "node_confirm_register_action"
|
||||||
|
|
||||||
post_action_text = "{combatant} does nothing this turn."
|
post_action_text = "{combatant} does nothing this turn."
|
||||||
|
|
||||||
|
|
@ -587,6 +593,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
disadvantage_matrix = AttributeProperty(defaultdict(dict))
|
disadvantage_matrix = AttributeProperty(defaultdict(dict))
|
||||||
|
|
||||||
fleeing_combatants = AttributeProperty(list())
|
fleeing_combatants = AttributeProperty(list())
|
||||||
|
defeated_combatants = AttributeProperty(list())
|
||||||
|
|
||||||
_warn_time_task = None
|
_warn_time_task = None
|
||||||
|
|
||||||
|
|
@ -621,8 +628,10 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
combatant,
|
combatant,
|
||||||
{
|
{
|
||||||
"node_wait_start": node_wait_start,
|
"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_action": node_select_action,
|
||||||
|
"node_select_wield_from_inventory": node_select_wield_from_inventory,
|
||||||
"node_wait_turn": node_wait_turn,
|
"node_wait_turn": node_wait_turn,
|
||||||
},
|
},
|
||||||
startnode="node_wait_turn",
|
startnode="node_wait_turn",
|
||||||
|
|
@ -660,18 +669,24 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
self.msg(f"|y_______________________ start turn {self.turn} ___________________________|n")
|
self.msg(f"|y_______________________ start turn {self.turn} ___________________________|n")
|
||||||
|
|
||||||
for combatant in self.combatants:
|
for combatant in self.combatants:
|
||||||
# cycle combat menu
|
if hasattr(combatant, "ai_combat_next_action"):
|
||||||
self._init_menu(combatant)
|
# NPC needs to get a decision from the AI
|
||||||
combatant.ndb._evmenu.goto("node_select_action", "")
|
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):
|
def _end_turn(self):
|
||||||
"""
|
"""
|
||||||
End of turn operations.
|
End of turn operations.
|
||||||
|
|
||||||
1. Do all regular actions
|
1. Do all regular actions
|
||||||
2. Roll for any death events
|
2. Check if fleeing combatants got away - remove them from combat
|
||||||
2. Remove combatants that disengaged successfully
|
3. Check if anyone has hp <= - defeated
|
||||||
3. Timeout advantages/disadvantages
|
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(
|
self.msg(
|
||||||
|
|
@ -702,29 +717,66 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
|
|
||||||
# handle disengaging combatants
|
# handle disengaging combatants
|
||||||
|
|
||||||
to_remove = []
|
to_flee = []
|
||||||
|
to_defeat = []
|
||||||
|
|
||||||
for combatant in self.combatants:
|
for combatant in self.combatants:
|
||||||
# see if fleeing characters managed to do two flee actions in a row.
|
# see if fleeing characters managed to do two flee actions in a row.
|
||||||
if (combatant in self.fleeing_combatants) and (combatant in already_fleeing):
|
if (combatant in self.fleeing_combatants) and (combatant in already_fleeing):
|
||||||
self.fleeing_combatants.remove(combatant)
|
self.fleeing_combatants.remove(combatant)
|
||||||
to_remove.append(combatant)
|
to_flee.append(combatant)
|
||||||
|
|
||||||
if combatant.hp <= 0:
|
if combatant.hp <= 0:
|
||||||
# check characters that are beaten down.
|
# 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()
|
combatant.at_defeat()
|
||||||
if combatant.hp <= 0:
|
to_defeat.append(combatant)
|
||||||
# if character still < 0 after at_defeat, it means they are dead.
|
|
||||||
# force-remove from combat.
|
|
||||||
to_remove.append(combatant)
|
|
||||||
|
|
||||||
for combatant in to_remove:
|
for combatant in to_flee:
|
||||||
# for clarity, we remove here rather than modifying the combatant list
|
# combatant leaving combat by fleeing
|
||||||
# inside the previous loop
|
self.msg(f"|y$You() successfully $conj(flee) from combat.|n", combatant=combatant)
|
||||||
self.msg(f"|y$You() $conj(are) out of combat.|n", combatant=combatant)
|
|
||||||
self.remove_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
|
# 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
|
# all stunts; # for more complex use we could store the action and let action have a
|
||||||
# 'duration' property to use instead.
|
# 'duration' property to use instead.
|
||||||
|
|
@ -753,10 +805,6 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
self.advantage_matrix = new_advantage_matrix
|
self.advantage_matrix = new_advantage_matrix
|
||||||
self.disadvantage_matrix = new_disadvantage_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):
|
def add_combatant(self, combatant, session=None):
|
||||||
"""
|
"""
|
||||||
Add combatant to battle.
|
Add combatant to battle.
|
||||||
|
|
@ -775,7 +823,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
"""
|
"""
|
||||||
if combatant not in self.combatants:
|
if combatant not in self.combatants:
|
||||||
self.combatants.append(combatant)
|
self.combatants.append(combatant)
|
||||||
combatant.db.turnbased_combathandler = self
|
combatant.db.combathandler = self
|
||||||
|
|
||||||
# allow custom character actions (not used by default)
|
# allow custom character actions (not used by default)
|
||||||
custom_action_classes = combatant.db.custom_combat_actions or []
|
custom_action_classes = combatant.db.custom_combat_actions or []
|
||||||
|
|
@ -798,7 +846,7 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
self.combatants.remove(combatant)
|
self.combatants.remove(combatant)
|
||||||
self.combatant_actions.pop(combatant, None)
|
self.combatant_actions.pop(combatant, None)
|
||||||
combatant.ndb._evmenu.close_menu()
|
combatant.ndb._evmenu.close_menu()
|
||||||
del combatant.db.turnbased_combathandler
|
del combatant.db.combathandler
|
||||||
|
|
||||||
def start_combat(self):
|
def start_combat(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -821,6 +869,73 @@ class EvAdventureCombatHandler(DefaultScript):
|
||||||
for combatant in self.combatants:
|
for combatant in self.combatants:
|
||||||
self.remove_combatant(combatant)
|
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):
|
def get_combat_summary(self, combatant):
|
||||||
"""
|
"""
|
||||||
Get a summary of the current combat state from the perspective of a
|
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):
|
def _register_action(caller, raw_string, **kwargs):
|
||||||
"""
|
"""
|
||||||
Register action with handler.
|
Actually register action with handler.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
action_key = kwargs.pop("action_key")
|
action_key = kwargs.pop("action_key")
|
||||||
|
|
@ -987,22 +1102,41 @@ def _register_action(caller, raw_string, **kwargs):
|
||||||
return "node_wait_turn"
|
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
|
Node where one can confirm registering the action or change one's mind.
|
||||||
with all other actions.
|
|
||||||
|
"""
|
||||||
|
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
|
combat = caller.ndb._evmenu.combathandler
|
||||||
action_key = kwargs["action_key"]
|
action_key = kwargs["action_key"]
|
||||||
|
friendly_target = kwargs.get("target_friendly", False)
|
||||||
text = f"Select target for |w{action_key}|n."
|
text = f"Select target for |w{action_key}|n."
|
||||||
|
|
||||||
# make the apply-self option always the first one, give it key 0
|
# make the apply-self option always the first one, give it key 0
|
||||||
kwargs["action_target"] = caller
|
kwargs["action_target"] = caller
|
||||||
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
|
options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
|
||||||
# filter out ourselves and then make options for everyone else
|
# 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(targets):
|
||||||
for inum, combatant in enumerate(combatants):
|
|
||||||
kwargs["action_target"] = combatant
|
kwargs["action_target"] = combatant
|
||||||
options.append(
|
options.append(
|
||||||
{"key": str(inum + 1), "desc": combatant.key, "goto": (_register_action, kwargs)}
|
{"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
|
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):
|
def _item_broken(caller, raw_string, **kwargs):
|
||||||
caller.msg("|rThis item is broken and unusable!|n")
|
caller.msg("|rThis item is broken and unusable!|n")
|
||||||
return None # back to previous node
|
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)})
|
options.append({"desc": str(obj), "goto": (_register_action, kwargs)})
|
||||||
|
|
||||||
# add ability to cancel
|
# add ability to cancel
|
||||||
options.append(
|
options.append({"key": "_default", "goto": "node_select_action"})
|
||||||
{"key": "_default", "desc": "(No input to Abort and go back)", "goto": "node_select_action"}
|
|
||||||
)
|
|
||||||
|
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
|
@ -1236,6 +1387,9 @@ def join_combat(caller, *targets, session=None):
|
||||||
if not location:
|
if not location:
|
||||||
raise CombatFailure("Must have a location to start combat.")
|
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:
|
if not targets:
|
||||||
raise CombatFailure("Must have an opponent to start combat.")
|
raise CombatFailure("Must have an opponent to start combat.")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ class Ability(Enum):
|
||||||
CRITICAL_FAILURE = "critical_failure"
|
CRITICAL_FAILURE = "critical_failure"
|
||||||
CRITICAL_SUCCESS = "critical_success"
|
CRITICAL_SUCCESS = "critical_success"
|
||||||
|
|
||||||
|
ALLEGIANCE_HOSTILE = "hostile"
|
||||||
|
ALLEGIANCE_NEUTRAL = "neutral"
|
||||||
|
ALLEGIANCE_FRIENDLY = "friendly"
|
||||||
|
|
||||||
|
|
||||||
class WieldLocation(Enum):
|
class WieldLocation(Enum):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@
|
||||||
EvAdventure NPCs. This includes both friends and enemies, only separated by their AI.
|
EvAdventure NPCs. This includes both friends and enemies, only separated by their AI.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from random import choice
|
||||||
|
|
||||||
from evennia import DefaultCharacter
|
from evennia import DefaultCharacter
|
||||||
from evennia.typeclasses.attributes import AttributeProperty
|
from evennia.typeclasses.attributes import AttributeProperty
|
||||||
|
|
||||||
from .characters import LivingMixin
|
from .characters import LivingMixin
|
||||||
|
from .enums import Ability
|
||||||
|
from .objects import WeaponEmptyHand
|
||||||
|
|
||||||
|
|
||||||
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
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
|
If wanting monsters or NPCs that can level and work the same as PCs, base them off the
|
||||||
EvAdventureCharacter class instead.
|
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)
|
is_pc = False
|
||||||
armor = AttributeProperty(default=1) # +10 to get armor defense
|
|
||||||
morale = AttributeProperty(default=9)
|
hit_dice = AttributeProperty(default=1, autocreate=False)
|
||||||
hp = AttributeProperty(default=8)
|
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
|
@property
|
||||||
def strength(self):
|
def strength(self):
|
||||||
|
|
@ -60,7 +76,7 @@ class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hp_max(self):
|
def hp_max(self):
|
||||||
return self.hit_dice * 4
|
return self.hit_dice * self.hp_multiplier
|
||||||
|
|
||||||
def at_object_creation(self):
|
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):
|
def at_defeat(self):
|
||||||
"""
|
"""
|
||||||
Mobs die right away when defeated, no death-table rolls.
|
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):
|
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)
|
result = self.roll_random_table("1d8", death_table)
|
||||||
if result == "dead":
|
if result == "dead":
|
||||||
character.handle_death()
|
character.at_death()
|
||||||
else:
|
else:
|
||||||
# survives with degraded abilities (1d4 roll)
|
# survives with degraded abilities (1d4 roll)
|
||||||
abi = self.death_map[result]
|
abi = self.death_map[result]
|
||||||
|
|
@ -339,7 +339,7 @@ class EvAdventureRollEngine:
|
||||||
|
|
||||||
if current_abi < -10:
|
if current_abi < -10:
|
||||||
# can't lose more - die
|
# can't lose more - die
|
||||||
character.handle_death()
|
character.at_death()
|
||||||
else:
|
else:
|
||||||
# refresh health, but get permanent ability loss
|
# refresh health, but get permanent ability loss
|
||||||
new_hp = max(character.hp_max, self.roll("1d4"))
|
new_hp = max(character.hp_max, self.roll("1d4"))
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,14 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
)
|
)
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
self.location.allow_combat = True
|
||||||
|
self.location.allow_death = True
|
||||||
self.combatant = self.character
|
self.combatant = self.character
|
||||||
self.target = create.create_object(
|
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
|
# this already starts turn 1
|
||||||
|
|
@ -56,10 +61,10 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
self.target.delete()
|
self.target.delete()
|
||||||
|
|
||||||
def test_remove_combatant(self):
|
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.combathandler.remove_combatant(self.combatant)
|
||||||
self.assertFalse(self.combatant in self.combathandler.combatants)
|
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):
|
def test_start_turn(self):
|
||||||
self.combathandler._start_turn()
|
self.combathandler._start_turn()
|
||||||
|
|
@ -150,9 +155,13 @@ class EvAdventureTurnbasedCombatActionTest(EvAdventureMixin, BaseEvenniaTest):
|
||||||
)
|
)
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
self.location.allow_combat = True
|
||||||
|
self.location.allow_death = True
|
||||||
self.combatant = self.character
|
self.combatant = self.character
|
||||||
self.combatant2 = create.create_object(EvAdventureCharacter, key="testcharacter2")
|
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
|
self.target.hp = 4
|
||||||
|
|
||||||
# this already starts turn 1
|
# 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
|
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
|
||||||
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
self._run_action(combat_turnbased.CombatActionAttack, self.target)
|
||||||
self.assertEqual(self.target.hp, -7)
|
self.assertEqual(self.target.hp, -7)
|
||||||
|
self.assertTrue(self.target not in self.combathandler.combatants)
|
||||||
|
|
||||||
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
@patch("evennia.contrib.tutorials.evadventure.combat_turnbased.rules.randint")
|
||||||
def test_stunt_fail(self, mock_randint):
|
def test_stunt_fail(self, mock_randint):
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,8 @@ from collections import defaultdict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
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.locks.lockfuncs import perm as perm_lockfunc
|
||||||
|
from evennia.utils.utils import make_iter, to_str
|
||||||
|
|
||||||
_TYPECLASS_AGGRESSIVE_CACHE = settings.TYPECLASS_AGGRESSIVE_CACHE
|
_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()
|
with an existing method/property on the class. If it does, you must use tags.add()
|
||||||
instead.
|
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:
|
Example:
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue