More work on tech demo area

This commit is contained in:
Griatch 2022-07-12 11:51:05 +02:00
parent 1ed7ffa095
commit afadb1001e
16 changed files with 275 additions and 252 deletions

View file

@ -172,6 +172,8 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal) now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal)
- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal) - Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal)
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal) - Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
- Allow `# CODE`, `# HEADER` etc as well as `#CODE`/`#HEADER` in batchcode
files - this works better with black linting.
## Evennia 0.9.5 ## Evennia 0.9.5

View file

@ -4,19 +4,17 @@ The base Command class.
All commands in Evennia inherit from the 'Command' class in this module. All commands in Evennia inherit from the 'Command' class in this module.
""" """
import re
import math
import inspect import inspect
import math
import re
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from evennia.locks.lockhandler import LockHandler from evennia.locks.lockhandler import LockHandler
from evennia.utils.utils import is_iter, fill, lazy_property, make_iter
from evennia.utils.evtable import EvTable
from evennia.utils.ansi import ANSIString from evennia.utils.ansi import ANSIString
from evennia.utils.evtable import EvTable
from evennia.utils.utils import fill, is_iter, lazy_property, make_iter
CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES

View file

@ -0,0 +1,7 @@
"""
EvAdventure - a complete game in Evennia.
This is an implementation of, and reference code to, the game created in the
documentation's beginner tutorial.
"""

View file

@ -15,45 +15,69 @@ You can also build/rebuild individiaul #CODE blocks in the `batchcode/interactiv
""" """
#HEADER # HEADER
# this is loaded at the top of every #CODE block # this is loaded at the top of every #CODE block
from evennia import create_object, search_object from evennia import DefaultExit, create_object, search_object
from evennia import DefaultExit
from evennia.contrib.tutorials import evadventure from evennia.contrib.tutorials import evadventure
from evennia.contrib.tutorials.evadventure.objects import (
EvAdventureObject, EvAdventureRunestone, EvAdventureRunestone, EvAdventureConsumable,
EvAdventureObjectFiller)
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom
from evennia.contrib.tutorials.evadventure.combat_turnbasedA import EvAdventureCombatHandler
from evennia.contrib.tutorials.evadventure import npcs from evennia.contrib.tutorials.evadventure import npcs
from evennia.contrib.tutorials.evadventure.combat_turnbased import EvAdventureCombatHandler
from evennia.contrib.tutorials.evadventure.objects import (
EvAdventureConsumable,
EvAdventureObject,
EvAdventureObjectFiller,
EvAdventureRunestone,
)
from evennia.contrib.tutorials.evadventure.rooms import EvAdventureRoom
#CODE # CODE
# Hub room evtechdemo#00 # Hub room evtechdemo#00
# for other test areas to link back to. Connects in turn back to Limbo. # for other test areas to link back to. Connects in turn back to Limbo.
limbo = search_object("Limbo") limbo = search_object("Limbo")[0]
hub_room = create_object(EvAdventureRoom, key="Techdemo Hub", aliases=("evtechdemo#00",), hub_room = create_object(
attributes=[("desc", "Central hub for EvAdventure tech demo.")]) EvAdventureRoom,
create_object(DefaultExit, key="EvAdventure Techdemo", aliases=("techdemo",), key="Techdemo Hub",
location=limbo, destination=hub_room) aliases=("evtechdemo#00",),
create_object(DefaultExit, key="Back to Limbo", aliases=("limbo", "back"), attributes=[("desc", "Central hub for EvAdventure tech demo.")],
location=hub_room, destination=limbo) )
create_object(
DefaultExit,
key="EvAdventure Techdemo",
aliases=("techdemo", "demo", "evadventure"),
location=limbo,
destination=hub_room,
)
create_object(
DefaultExit,
key="Back to Limbo",
aliases=("limbo", "back"),
location=hub_room,
destination=limbo,
)
#CODE # CODE
# A combat room evtechdemo#01 # A combat room evtechdemo#01
# 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") 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") hub_room = search_object("evtechdemo#00")[0]
create_object(DefaultExit, key="Back to Hub", aliases=("back", "hub"), create_object(
location=combat_room, destination=hub_room) DefaultExit, key="combat test", aliases=("combat",), location=hub_room, destination=combat_room
create_object(DefaultExit, key="combat test", aliases=("combat"), )
location=combat_room, destination=hub_room) create_object(
DefaultExit,
key="Back to Hub",
aliases=("back", "hub"),
location=combat_room,
destination=hub_room,
)

View file

@ -5,10 +5,11 @@ Base Character and NPCs.
from evennia.objects.objects import DefaultCharacter, DefaultObject from evennia.objects.objects import DefaultCharacter, DefaultObject
from evennia.typeclasses.attributes import AttributeProperty from evennia.typeclasses.attributes import AttributeProperty
from evennia.utils.utils import lazy_property, int2str from evennia.utils.utils import int2str, lazy_property
from .objects import EvAdventureObject
from . import rules from . import rules
from .enums import Ability, WieldLocation from .enums import Ability, WieldLocation
from .objects import EvAdventureObject
class EquipmentError(TypeError): class EquipmentError(TypeError):
@ -290,11 +291,12 @@ class EquipmentHandler:
in the list after all). in the list after all).
""" """
return [obj for obj in slots[WieldLocation.BACKPACK] return [
if obj.inventory_use_slot in ( obj
WieldLocation.WEAPON_HAND, for obj in slots[WieldLocation.BACKPACK]
WieldLocation.TWO_HANDS, if obj.inventory_use_slot
WieldLocation.SHIELD_HAND)] in (WieldLocation.WEAPON_HAND, WieldLocation.TWO_HANDS, WieldLocation.SHIELD_HAND)
]
def get_wearable_objects_from_backpack(self): def get_wearable_objects_from_backpack(self):
""" """
@ -307,11 +309,11 @@ class EquipmentHandler:
in the list after all). in the list after all).
""" """
return [obj for obj in slots[WieldLocation.BACKPACK] return [
if obj.inventory_use_slot in ( obj
WieldLocation.BODY, for obj in slots[WieldLocation.BACKPACK]
WieldLocation.HEAD if obj.inventory_use_slot in (WieldLocation.BODY, WieldLocation.HEAD)
)] ]
def get_usable_objects_from_backpack(self): def get_usable_objects_from_backpack(self):
""" """
@ -327,7 +329,7 @@ class EquipmentHandler:
class LivingMixin: class LivingMixin:
""" """
Helpers shared between all living things. Mixin class to use for all living things.
""" """
@ -488,64 +490,3 @@ class EvAdventureCharacter(LivingMixin, DefaultCharacter):
Called when character dies. Called when character dies.
""" """
class EvAdventureNPC(LivingMixin, DefaultCharacter):
"""
This is the base class for all non-player entities, including monsters. These
generally don't advance in level but uses a simplified, abstract measure of how
dangerous or competent they are - the 'hit dice' (HD).
HD indicates how much health they have and how hard they hit. In _Knave_, HD also
defaults to being the bonus for all abilities. HP is 4 x Hit die (this can then be
customized per-entity of course).
Morale is set explicitly per-NPC, usually between 7 and 9.
Monsters don't use equipment in the way PCs do, instead they have a fixed armor
value, and their Abilities are dynamically generated from the HD (hit_dice).
If wanting monsters or NPCs that can level and work the same as PCs, base them off the
EvAdventureCharacter class instead.
"""
hit_dice = AttributeProperty(default=1)
armor = AttributeProperty(default=11)
morale = AttributeProperty(default=9)
hp = AttributeProperty(default=8)
@property
def strength(self):
return self.hit_dice
@property
def dexterity(self):
return self.hit_dice
@property
def constitution(self):
return self.hit_dice
@property
def intelligence(self):
return self.hit_dice
@property
def wisdom(self):
return self.hit_dice
@property
def charisma(self):
return self.hit_dice
@property
def hp_max(self):
return self.hit_dice * 4
def at_object_creation(self):
"""
Start with max health.
"""
self.hp = self.hp_max

View file

@ -99,15 +99,16 @@ Choose who to block:
""" """
from datetime import datetime
from collections import defaultdict from collections import defaultdict
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
from evennia.utils.utils import make_iter from evennia.utils.utils import make_iter
from evennia.utils import evtable, dbserialize, delay, evmenu
from .enums import Ability
from . import rules
from . import rules
from .enums import Ability
COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler" COMBAT_HANDLER_KEY = "evadventure_turnbased_combathandler"
COMBAT_HANDLER_INTERVAL = 60 COMBAT_HANDLER_INTERVAL = 60
@ -242,7 +243,7 @@ class CombatActionAttack(CombatAction):
# figure out disadvantage (gained by enemy stunts/actions) # figure out disadvantage (gained by enemy stunts/actions)
disadvantage = bool(self.combathandler.disadvantage_matrix[attacker].pop(defender, False)) disadvantage = bool(self.combathandler.disadvantage_matrix[attacker].pop(defender, False))
is_hit, quality = rules.EvAdventureRollEngine.opposed_saving_throw( is_hit, quality = rules.dice.opposed_saving_throw(
attacker, attacker,
defender, defender,
attack_type=attacker.weapon.attack_type, attack_type=attacker.weapon.attack_type,
@ -295,9 +296,9 @@ class CombatActionStunt(CombatAction):
# quality doesn't matter for stunts, they are either successful or not # quality doesn't matter for stunts, they are either successful or not
attacker = self.combatant attacker = self.combatant
advantage, disadvantage = False advantage, disadvantage = False, False
is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw( is_success, _ = rules.dice.opposed_saving_throw(
attacker, attacker,
defender, defender,
attack_type=self.attack_type, attack_type=self.attack_type,
@ -333,6 +334,7 @@ class CombatActionUseItem(CombatAction):
combat_post_use combat_post_use
""" """
key = "Use Item" key = "Use Item"
desc = "[U]se item" desc = "[U]se item"
aliases = ("u", "item", "use item") aliases = ("u", "item", "use item")
@ -406,7 +408,7 @@ class CombatActionBlock(CombatAction):
advantage = bool(self.advantage_matrix[combatant].pop(fleeing_target, False)) advantage = bool(self.advantage_matrix[combatant].pop(fleeing_target, False))
disadvantage = bool(self.disadvantage_matrix[combatant].pop(fleeing_target, False)) disadvantage = bool(self.disadvantage_matrix[combatant].pop(fleeing_target, False))
is_success, _ = rules.EvAdventureRollEngine.opposed_saving_throw( is_success, _ = rules.dice.opposed_saving_throw(
combatant, combatant,
fleeing_target, fleeing_target,
attack_type=self.attack_type, attack_type=self.attack_type,
@ -427,12 +429,23 @@ class CombatActionSwapWieldedWeaponOrSpell(CombatAction):
Swap Wielded weapon or spell. Swap Wielded weapon or spell.
""" """
key = "Swap weapon/rune/shield" key = "Swap weapon/rune/shield"
desc = "Swap currently wielded weapon, shield or spell-rune." desc = "Swap currently wielded weapon, shield or spell-rune."
aliases = ("s", "swap", "draw", "swap weapon", "draw weapon", aliases = (
"swap rune", "draw rune", "swap spell", "draw spell") "s",
help_text = ("Draw a new weapon or spell-rune from your inventory, " "swap",
"replacing your current loadout") "draw",
"swap weapon",
"draw weapon",
"swap rune",
"draw rune",
"swap spell",
"draw spell",
)
help_text = (
"Draw a new weapon or spell-rune from your inventory, replacing your current loadout"
)
next_menu_node = "node_select_wield_from_inventory" next_menu_node = "node_select_wield_from_inventory"
@ -448,6 +461,7 @@ class CombatActionUseItem(CombatAction):
Use an item from inventory. Use an item from inventory.
""" """
key = "Use an item from backpack" key = "Use an item from backpack"
desc = "Use an item from your inventory." desc = "Use an item from your inventory."
aliases = ("u", "use", "use item") aliases = ("u", "use", "use item")
@ -543,6 +557,29 @@ class EvAdventureCombatHandler(DefaultScript):
self._end_turn() self._end_turn()
self._start_turn() self._start_turn()
def _init_menu(self, combatant, session=None):
"""
Make sure combatant is in the menu. This is safe to call on a combatant already in a menu.
"""
if not combatant.ndb._evmenu:
# re-joining the menu is useful during testing
evmenu.EvMenu(
combatant,
{
"node_wait_start": node_wait_start,
"node_select_target": node_select_target,
"node_select_action": node_select_action,
"node_wait_turn": node_wait_turn,
},
startnode="node_wait_turn",
auto_quit=True,
persistent=True,
cmdset_mergetype="Union",
session=session,
combathandler=self, # makes this available as combatant.ndb._evmenu.combathandler
)
def _reset_menu(self): def _reset_menu(self):
""" """
Move menu to the action-selection node. Move menu to the action-selection node.
@ -577,10 +614,12 @@ class EvAdventureCombatHandler(DefaultScript):
# set -1 for unit tests # set -1 for unit tests
warning_time = 15 warning_time = 15
self._warn_time_task = delay( self._warn_time_task = delay(
self.interval - warning_time, self._warn_time, warning_time) self.interval - warning_time, self._warn_time, warning_time
)
for combatant in self.combatants: for combatant in self.combatants:
# cycle combat menu # cycle combat menu
self._init_menu(combatant)
combatant.ndb._evmenu.goto("node_select_action", "") combatant.ndb._evmenu.goto("node_select_action", "")
def _end_turn(self): def _end_turn(self):
@ -633,12 +672,12 @@ class EvAdventureCombatHandler(DefaultScript):
for combatant in self.combatants: for combatant in self.combatants:
new_advantage_matrix[combatant] = { new_advantage_matrix[combatant] = {
target: set_at_turn target: set_at_turn
for target, set_at_turn in advantage_matrix.items() for target, set_at_turn in advantage_matrix[combatant].items()
if set_at_turn > oldest_stunt_age if set_at_turn > oldest_stunt_age
} }
new_disadvantage_matrix[combatant] = { new_disadvantage_matrix[combatant] = {
target: set_at_turn target: set_at_turn
for target, set_at_turn in disadvantage_matrix.items() for target, set_at_turn in disadvantage_matrix[combatant].items()
if set_at_turn > oldest_stunt_age if set_at_turn > oldest_stunt_age
} }
@ -676,23 +715,7 @@ class EvAdventureCombatHandler(DefaultScript):
action_class.key: action_class(self, combatant) action_class.key: action_class(self, combatant)
for action_class in self.default_action_classes + custom_action_classes for action_class in self.default_action_classes + custom_action_classes
} }
self._init_menu(combatant, session=session)
# start evmenu (menu node definitions at the end of this module)
evmenu.EvMenu(
combatant,
{
"node_wait_start": node_wait_start,
"node_select_target": node_select_target,
"node_select_action": node_select_action,
"node_wait_turn": node_wait_turn,
},
startnode="node_wait_turn",
auto_quit=False,
persistent=True,
session=session,
combathandler=self # makes this available as combatant.ndb._evmenu.combathandler
)
def remove_combatant(self, combatant): def remove_combatant(self, combatant):
""" """
@ -823,9 +846,9 @@ class EvAdventureCombatHandler(DefaultScript):
""" """
weapon_dmg_roll = attacker.weapon.damage_roll weapon_dmg_roll = attacker.weapon.damage_roll
dmg = rules.EvAdventureRollEngine.roll(weapon_dmg_roll) dmg = rules.dice.roll(weapon_dmg_roll)
if critical: if critical:
dmg += rules.EvAdventureRollEngine.roll(weapon_dmg_roll) dmg += rules.dice.roll(weapon_dmg_roll)
defender.hp -= dmg defender.hp -= dmg
@ -834,7 +857,7 @@ class EvAdventureCombatHandler(DefaultScript):
if defender.hp <= 0: if defender.hp <= 0:
# roll on death table. This may or may not kill you # roll on death table. This may or may not kill you
rules.EvAdventureRollEngine.roll_death(self) rules.dice.roll_death(self)
# tell everyone # tell everyone
self.msg(defender.defeat_message(attacker, dmg)) self.msg(defender.defeat_message(attacker, dmg))
@ -870,8 +893,7 @@ class EvAdventureCombatHandler(DefaultScript):
""" """
# get the instantiated action for this combatant # get the instantiated action for this combatant
action = self.combatant_actions[combatant].get( action = self.combatant_actions[combatant].get(
action_key, action_key, CombatActionDoNothing(self, combatant)
CombatActionDoNothing(self, combatant)
) )
# store the action in the queue # store the action in the queue
@ -912,13 +934,13 @@ def _register_action(caller, raw_string, **kwargs):
Register action with handler. Register action with handler.
""" """
action_key = kwargs.get["action_key"] action_key = kwargs.pop("action_key")
action_args = kwargs["action_args"] action_args = kwargs["action_args"]
action_kwargs = kwargs["action_kwargs"] action_kwargs = kwargs["action_kwargs"]
action_target = kwargs.get("action_target") action_target = kwargs.pop("action_target", None)
combat_handler = caller._evmenu.combathandler combat_handler = caller.ndb._evmenu.combathandler
combat_handler.register_action( print("action_args", action_args, "action_kwargs", action_kwargs)
caller, action_key, action_target, *action_args, **action_kwargs) combat_handler.register_action(caller, action_key, action_target, *action_args, **action_kwargs)
# move into waiting # move into waiting
return "node_wait_turn" return "node_wait_turn"
@ -930,41 +952,22 @@ def node_select_target(caller, raw_string, **kwargs):
with all other actions. with all other actions.
""" """
action_key = kwargs.get("action_key")
action_args = kwargs.get("action_args")
action_kwargs = kwargs.get("action_kwargs")
combat = caller.ndb._evmenu.combathandler combat = caller.ndb._evmenu.combathandler
text = "Select target for |w{action_key}|n." text = "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 = [ options = [{"key": "0", "desc": "(yourself)", "goto": (_register_action, kwargs)}]
{
"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] combatants = [combatant for combatant in combat.combatants if combatant is not caller]
for combatant in combatants: for inum, combatant in enumerate(combatants):
# automatic menu numbering starts from 1
kwargs["action_target"] = combatant kwargs["action_target"] = combatant
options.append( options.append(
{ {"key": str(inum + 1), "desc": combatant.key, "goto": (_register_action, kwargs)}
"desc": combatant.key,
"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
@ -981,8 +984,10 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs):
""" """
combat = caller.ndb._evmenu.combathandler combat = caller.ndb._evmenu.combathandler
loadout = caller.inventory.display_loadout() loadout = caller.inventory.display_loadout()
text = (f"{loadout}\nSelect weapon, spell or shield to draw. It will swap out " text = (
"anything already in the same hand (you can't change armor or helmet in combat).") f"{loadout}\nSelect weapon, spell or shield to draw. It will swap out "
"anything already in the same hand (you can't change armor or helmet in combat)."
)
# get a list of all suitable weapons/spells/shields # get a list of all suitable weapons/spells/shields
options = [] options = []
@ -997,21 +1002,12 @@ def node_select_wield_from_inventory(caller, raw_string, **kwargs):
) )
else: else:
# normally working item # normally working item
kwargs['action_args'] = (obj,) kwargs["action_args"] = (obj,)
options.append( options.append({"desc": str(obj), "goto": (_register_action, kwargs)})
{
"desc": str(obj),
"goto": (_register_action, kwargs)
}
)
# add ability to cancel # add ability to cancel
options.append( options.append(
{ {"key": "_default", "desc": "(No input to Abort and go back)", "goto": "node_select_action"}
"key": "_default",
"desc": "(No input to Abort and go back)",
"goto": "node_select_action"
}
) )
return text, options return text, options
@ -1038,21 +1034,12 @@ def node_select_use_item_from_inventory(caller, raw_string, **kwargs):
) )
else: else:
# normally working item # normally working item
kwargs['action_args'] = (obj,) kwargs["action_args"] = (obj,)
options.append( options.append({"desc": str(obj), "goto": (_register_action, kwargs)})
{
"desc": str(obj),
"goto": (_register_action, kwargs)
}
)
# add ability to cancel # add ability to cancel
options.append( options.append(
{ {"key": "_default", "desc": "(No input to Abort and go back)", "goto": "node_select_action"}
"key": "_default",
"desc": "(No input to Abort and go back)",
"goto": "node_select_action"
}
) )
return text, options return text, options
@ -1063,8 +1050,8 @@ def _action_unavailable(caller, raw_string, **kwargs):
Selecting an unavailable action. Selecting an unavailable action.
""" """
action_key = kwargs.get["action_key"] action_key = kwargs["action_key"]
caller.msg(f"Action '{action_key}' is currently not available.") caller.msg(f"|rAction |w{action_key}|r is currently not available.|n")
# go back to previous node # go back to previous node
return return
@ -1093,12 +1080,7 @@ def node_select_action(caller, raw_string, **kwargs):
{ {
"key": key, "key": key,
"desc": desc, "desc": desc,
"goto": ( "goto": (_action_unavailable, {"action_key": action.key}),
_action_unavailable,
{
"action_key": action.key
}
)
} }
) )
elif action.next_menu_node is None: elif action.next_menu_node is None:
@ -1113,7 +1095,7 @@ def node_select_action(caller, raw_string, **kwargs):
{ {
"action_key": action.key, "action_key": action.key,
"action_args": (), "action_args": (),
"action_kwargs": kwargs, "action_kwargs": {},
"action_target": None, "action_target": None,
}, },
), ),
@ -1130,7 +1112,8 @@ def node_select_action(caller, raw_string, **kwargs):
{ {
"action_key": action.key, "action_key": action.key,
"action_args": (), "action_args": (),
"action_kwargs": kwargs, "action_kwargs": {},
"action_target": None,
}, },
), ),
} }
@ -1139,8 +1122,7 @@ def node_select_action(caller, raw_string, **kwargs):
options.append( options.append(
{ {
"key": "_default", "key": "_default",
"desc": "(No input to Abort and go back)", "goto": "node_select_action",
"goto": "node_select_action"
} }
) )
@ -1160,7 +1142,7 @@ def node_wait_turn(caller, raw_string, **kwargs):
options = { options = {
"key": "_default", "key": "_default",
"desc": "(next round will start automatically)", "desc": "(next round will start automatically)",
"goto": "node_wait_turn" "goto": "node_wait_turn",
} }
return text, options return text, options
@ -1177,7 +1159,7 @@ def node_wait_start(caller, raw_string, **kwargs):
options = { options = {
"key": "_default", "key": "_default",
"desc": "(combat will start automatically)", "desc": "(combat will start automatically)",
"goto": "node_wait_start" "goto": "node_wait_start",
} }
return text, options return text, options
@ -1218,10 +1200,13 @@ def join_combat(caller, *targets, session=None):
combathandler = location.scripts.add(EvAdventureCombatHandler, autostart=False) combathandler = location.scripts.add(EvAdventureCombatHandler, autostart=False)
created = True created = True
if not hasattr(caller, "hp"):
raise CombatFailure("You have no hp and so can't attack anyone.")
# it's safe to add a combatant to the same combat more than once # it's safe to add a combatant to the same combat more than once
combathandler.add_combatant(caller, session=session) combathandler.add_combatant(caller, session=session)
for target in targets: for target in targets:
combathandler.add_combatant(target, session=session) combathandler.add_combatant(target)
if created: if created:
combathandler.start_combat() combathandler.start_combat()

View file

@ -1,11 +1,11 @@
""" """
EvAdventure commands and cmdsets. nextEvAdventure commands and cmdsets.
""" """
from evennia import Command, default_cmds from evennia import Command, default_cmds
from . combat_turnbased import join_combat
from .combat_turnbased import CombatFailure, join_combat
class EvAdventureCommand(Command): class EvAdventureCommand(Command):
@ -17,6 +17,7 @@ class EvAdventureCommand(Command):
where whitespace around the argument(s) are stripped. where whitespace around the argument(s) are stripped.
""" """
def parse(self): def parse(self):
self.args = self.args.strip() self.args = self.args.strip()
@ -38,6 +39,9 @@ class CmdAttackTurnBased(EvAdventureCommand):
""" """
key = "attack"
aliases = ("hit",)
def parse(self): def parse(self):
super().parse() super().parse()
self.targets = [name.strip() for name in self.args.split(",")] self.targets = [name.strip() for name in self.args.split(",")]
@ -49,10 +53,15 @@ class CmdAttackTurnBased(EvAdventureCommand):
target_objs = [] target_objs = []
for target in self.targets: for target in self.targets:
target_obj = self.caller.search(target) target_obj = self.caller.search(target)
if target_obj: if not target_obj:
# show a warning but don't abort # show a warning but don't abort
continue continue
target_objs.append(target_obj) target_objs.append(target_obj)
if target_objs: if target_objs:
join_combat(self.caller, *target_objs, session=self.session) try:
join_combat(self.caller, *target_objs, session=self.session)
except CombatFailure as err:
self.caller.msg(f"|r{err}|n")
else:
self.caller.msg("|rFound noone to attack.|n")

View file

@ -3,15 +3,72 @@ EvAdventure NPCs. This includes both friends and enemies, only separated by thei
""" """
from .characters import EvAdventureCharacter from evennia import DefaultCharacter
from evennia.typeclasses.attributes import AttributeProperty
class EvAdventureNPC(EvAdventureCharacter): from .characters import LivingMixin
class EvAdventureNPC(LivingMixin, DefaultCharacter):
""" """
Base typeclass for NPCs. They have the features of a Character except This is the base class for all non-player entities, including monsters. These
they have tooling for AI and for acting as quest-gives and shop-keepers. generally don't advance in level but uses a simplified, abstract measure of how
dangerous or competent they are - the 'hit dice' (HD).
HD indicates how much health they have and how hard they hit. In _Knave_, HD also
defaults to being the bonus for all abilities. HP is 4 x Hit die (this can then be
customized per-entity of course).
Morale is set explicitly per-NPC, usually between 7 and 9.
Monsters don't use equipment in the way PCs do, instead they have a fixed armor
value, and their Abilities are dynamically generated from the HD (hit_dice).
If wanting monsters or NPCs that can level and work the same as PCs, base them off the
EvAdventureCharacter class instead.
""" """
hit_dice = AttributeProperty(default=1)
armor = AttributeProperty(default=11)
morale = AttributeProperty(default=9)
hp = AttributeProperty(default=8)
@property
def strength(self):
return self.hit_dice
@property
def dexterity(self):
return self.hit_dice
@property
def constitution(self):
return self.hit_dice
@property
def intelligence(self):
return self.hit_dice
@property
def wisdom(self):
return self.hit_dice
@property
def charisma(self):
return self.hit_dice
@property
def hp_max(self):
return self.hit_dice * 4
def at_object_creation(self):
"""
Start with max health.
"""
self.hp = self.hp_max
class EvAdventureShopKeeper(EvAdventureNPC): class EvAdventureShopKeeper(EvAdventureNPC):
""" """

View file

@ -10,7 +10,7 @@ Tags.
from evennia.objects.objects import DefaultObject from evennia.objects.objects import DefaultObject
from evennia.typeclasses.attributes import AttributeProperty from evennia.typeclasses.attributes import AttributeProperty
from .enums import WieldLocation, Ability from .enums import Ability, WieldLocation
class EvAdventureObject(DefaultObject): class EvAdventureObject(DefaultObject):
@ -44,6 +44,7 @@ class EvAdventureObjectFiller(EvAdventureObject):
meaning it's unusable. meaning it's unusable.
""" """
quality = AttributeProperty(0) quality = AttributeProperty(0)
@ -53,6 +54,7 @@ class EvAdventureConsumable(EvAdventureObject):
have a limited usage in this way. have a limited usage in this way.
""" """
inventory_use_slot = AttributeProperty(WieldLocation.BACKPACK) inventory_use_slot = AttributeProperty(WieldLocation.BACKPACK)
size = AttributeProperty(0.25) size = AttributeProperty(0.25)
uses = AttributeProperty(1) uses = AttributeProperty(1)
@ -91,6 +93,7 @@ class EvAdventureRunestone(EvAdventureWeapon):
they are quite powerful (and scales with caster level). they are quite powerful (and scales with caster level).
""" """
inventory_use_slot = AttributeProperty(WieldLocation.TWO_HANDS) inventory_use_slot = AttributeProperty(WieldLocation.TWO_HANDS)
attack_type = AttributeProperty(Ability.INT) attack_type = AttributeProperty(Ability.INT)

View file

@ -23,14 +23,13 @@ This module presents several singletons to import
""" """
from random import randint from random import randint
from evennia.utils.evform import EvForm from evennia.utils.evform import EvForm
from evennia.utils.evtable import EvTable from evennia.utils.evtable import EvTable
from .enums import Ability
from .random_tables import (
character_generation as chargen_table,
death_and_dismemberment as death_table,
)
from .enums import Ability
from .random_tables import character_generation as chargen_table
from .random_tables import death_and_dismemberment as death_table
# Basic rolls # Basic rolls

View file

@ -19,8 +19,9 @@ class EvAdventureMixin:
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.location = create.create_object(EvAdventureRoom, key="testroom") self.location = create.create_object(EvAdventureRoom, key="testroom")
self.character = create.create_object(EvAdventureCharacter, key="testchar", self.character = create.create_object(
location=self.location) EvAdventureCharacter, key="testchar", location=self.location
)
self.helmet = create.create_object( self.helmet = create.create_object(
EvAdventureObject, EvAdventureObject,
key="helmet", key="helmet",

View file

@ -16,6 +16,7 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
Test the turn-based combat-handler implementation. Test the turn-based combat-handler implementation.
""" """
maxDiff = None maxDiff = None
@patch( @patch(
@ -52,10 +53,7 @@ class EvAdventureTurnbasedCombatHandlerTest(EvAdventureMixin, BaseEvenniaTest):
self.combathandler.register_action(self.combatant, action.key) self.combathandler.register_action(self.combatant, action.key)
self.assertEqual( self.assertEqual(self.combathandler.action_queue[self.combatant], (action, (), {}))
self.combathandler.action_queue[self.combatant],
(action, (), {})
)
action.use = MagicMock() action.use = MagicMock()

View file

@ -13,7 +13,6 @@ from collections import defaultdict
import inflect import inflect
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from evennia.commands import cmdset from evennia.commands import cmdset
from evennia.commands.cmdsethandler import CmdSetHandler from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.objects.manager import ObjectManager from evennia.objects.manager import ObjectManager
@ -22,15 +21,9 @@ from evennia.scripts.scripthandler import ScriptHandler
from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler
from evennia.typeclasses.models import TypeclassBase from evennia.typeclasses.models import TypeclassBase
from evennia.utils import ansi, create, funcparser, logger, search from evennia.utils import ansi, create, funcparser, logger, search
from evennia.utils.utils import ( from evennia.utils.utils import (class_from_module, is_iter, lazy_property,
class_from_module, list_to_string, make_iter, to_str,
is_iter, variable_from_module)
lazy_property,
list_to_string,
make_iter,
to_str,
variable_from_module,
)
_INFLECT = inflect.engine() _INFLECT = inflect.engine()
_MULTISESSION_MODE = settings.MULTISESSION_MODE _MULTISESSION_MODE = settings.MULTISESSION_MODE

View file

@ -1,10 +1,11 @@
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
from evennia.typeclasses.attributes import AttributeProperty
from evennia.typeclasses.tags import TagProperty, AliasProperty, PermissionProperty
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.objects.objects import DefaultObject from evennia.objects.objects import DefaultObject
from evennia.typeclasses.attributes import AttributeProperty
from evennia.typeclasses.tags import (AliasProperty, PermissionProperty,
TagProperty)
from evennia.utils import create from evennia.utils import create
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
class DefaultObjectTest(BaseEvenniaTest): class DefaultObjectTest(BaseEvenniaTest):

View file

@ -126,7 +126,7 @@ class ScriptHandler(object):
""" """
return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key) return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key)
def delete(self, key=None): def remove(self, key=None):
""" """
Forcibly delete a script from this object. Forcibly delete a script from this object.
@ -149,7 +149,8 @@ class ScriptHandler(object):
num += 1 num += 1
return num return num
# alias to delete # legacy aliases to remove
delete = remove
stop = delete stop = delete
def all(self): def all(self):

View file

@ -5,13 +5,13 @@ ability to run timers.
""" """
from django.utils.translation import gettext as _
from evennia.scripts.manager import ScriptManager
from evennia.scripts.models import ScriptDB
from evennia.typeclasses.models import TypeclassBase
from evennia.utils import create, logger
from twisted.internet.defer import Deferred, maybeDeferred from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from django.utils.translation import gettext as _
from evennia.typeclasses.models import TypeclassBase
from evennia.scripts.models import ScriptDB
from evennia.scripts.manager import ScriptManager
from evennia.utils import create, logger
__all__ = ["DefaultScript", "DoNothing", "Store"] __all__ = ["DefaultScript", "DoNothing", "Store"]
@ -366,7 +366,11 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
return return
# call hook # call hook
self.at_repeat() try:
self.at_repeat()
except Exception:
logger.log_trace()
raise
# check repeats # check repeats
if self.ndb._task: if self.ndb._task: