Add tutorial doc for Rooms. Finish tests for combat

This commit is contained in:
Griatch 2023-04-30 00:09:03 +02:00
parent 998cbb870b
commit bdc3f37954
16 changed files with 627 additions and 275 deletions

View file

@ -2,6 +2,8 @@
#
# Set up a combat area for testing combat. Requires developer or superuser perm.
#
# Run from in-game as batchcmd contrib.tutorials.evadventure.batchscripts.combat_demo
#
# start from limbo
@ -13,13 +15,13 @@ type self = evennia.contrib.tutorials.evadventure.characters.EvAdventureCharacte
# assign us the twitch combat cmdset (requires superuser/developer perms)
py self.cmdset.add("evennia.contrib.tutorials.evadventure.combat.TwitchAttackCmdSet")
py self.cmdset.add("evennia.contrib.tutorials.evadventure.combat_twitch.TwitchAttackCmdSet", persistent=True)
# Create and give us a weapons (this will use defaults on the class)
create sword:evennia.contrib.tutorials.evadventure.objects.EvAdventureWeapon
# create a consumable to use
# create a consumable to use
create potion:evennia.contrib.tutorials.evadventure.objects.EvAdventureConsumable
@ -45,8 +47,8 @@ desc dummy = This is is an ugly training dummy made out of hay and wood.
# make the dummy crazy tough
dummy.hp_max = 1000
set dummy/hp_max = 1000
#
dummy.hp = 1000
set dummy/hp = 1000

View file

@ -89,8 +89,9 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
for comb in self.obj.location.contents
if hasattr(comb, "scripts") and comb.scripts.has(self.key)
]
location = self.obj.location
if self.obj.location.allow_pvp:
if hasattr(location, "allow_pvp") and location.allow_pvp:
# in pvp, everyone else is an enemy
allies = [combatant]
enemies = [comb for comb in combatants if comb != combatant]
@ -202,17 +203,21 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
self.action_dict = self.fallback_action_dict
self.queue_action(self.fallback_action_dict)
self.check_stop_combat()
def check_stop_combat(self):
"""
Check if the combat is over.
"""
allies, enemies = self.get_sides()
allies, enemies = self.get_sides(self.obj)
allies.append(self.obj)
# remove all dead combatants
allies = [comb for comb in allies if comb.hp > 0]
enemies = [comb for comb in enemies if comb.hp > 0]
location = self.obj.location
# only keep combatants that are alive and still in the same room
allies = [comb for comb in allies if comb.hp > 0 and comb.location == location]
enemies = [comb for comb in enemies if comb.hp > 0 and comb.location == location]
if not allies and not enemies:
self.msg("Noone stands after the dust settles.", broadcast=False)
@ -220,11 +225,14 @@ class EvAdventureCombatTwitchHandler(EvAdventureCombatHandlerBase):
return
if not allies or not enemies:
still_standing = list_to_string(f"$You({comb.key})" for comb in allies + enemies)
self.msg(
f"The combat is over. Still standing: {still_standing}.",
broadcast=False,
)
if allies + enemies == [self.obj]:
self.msg("The combat is over.")
else:
still_standing = list_to_string(f"$You({comb.key})" for comb in allies + enemies)
self.msg(
f"The combat is over. Still standing: {still_standing}.",
broadcast=False,
)
self.stop_combat()
def stop_combat(self):
@ -274,11 +282,18 @@ class _BaseTwitchCombatCommand(Command):
rhs = " ".join(rhs)
self.lhs, self.rhs = lhs.strip(), rhs.strip()
def get_or_create_combathandler(self, combathandler_name="combathandler"):
def get_or_create_combathandler(self, target=None, combathandler_name="combathandler"):
"""
Get or create the combathandler assigned to this combatant.
"""
if target:
# add/check combathandler to the target
if target.hp_max is None:
self.msg("You can't attack that!")
raise InterruptCommand()
EvAdventureCombatTwitchHandler.get_or_create_combathandler(target)
return EvAdventureCombatTwitchHandler.get_or_create_combathandler(self.caller)
@ -301,19 +316,19 @@ class CmdAttack(_BaseTwitchCombatCommand):
if not target:
return
combathandler = self.get_or_create_combathandler()
combathandler = self.get_or_create_combathandler(target)
# we use a fixed dt of 3 here, to mimic Diku style; one could also picture
# attacking at a different rate, depending on skills/weapon etc.
combathandler.queue_action({"key": "attack", "target": target, "dt": 3})
combathandler.msg(f"$You() $conj(attack) $You({target.key})!", self.caller)
class CmdLook(default_cmds.CmdLook):
class CmdLook(default_cmds.CmdLook, _BaseTwitchCombatCommand):
def func(self):
# get regular look, followed by a combat summary
super().func()
if not self.args:
combathandler = self.get_or_create_combathandler(self.caller.location)
combathandler = self.get_or_create_combathandler()
txt = str(combathandler.get_combat_summary(self.caller))
maxwidth = max(display_len(line) for line in txt.strip().split("\n"))
self.msg(f"|r{pad(' Combat Status ', width=maxwidth, fillchar='-')}|n\n{txt}")
@ -416,7 +431,7 @@ class CmdStunt(_BaseTwitchCombatCommand):
self.target = target.strip()
def func(self):
combathandler = self.get_or_create_combathandler()
combathandler = self.get_or_create_combathandler(self.target)
target = self.caller.search(self.target)
if not target:
@ -478,7 +493,7 @@ class CmdUseItem(_BaseTwitchCombatCommand):
if not target:
return
combathandler = self.get_or_create_combathandler()
combathandler = self.get_or_create_combathandler(self.target)
combathandler.queue_action({"key": "use", "item": item, "target": target})
combathandler.msg(
f"$You() prepare to use {item.get_display_name(self.caller)}!", self.caller

View file

@ -6,7 +6,7 @@ Knave has a system of Slots for its inventory.
from evennia.utils.utils import inherits_from
from .enums import Ability, WieldLocation
from .objects import EvAdventureObject, WeaponEmptyHand
from .objects import BARE_HANDS, EvAdventureObject
class EquipmentError(TypeError):
@ -167,7 +167,7 @@ class EquipmentHandler:
if not weapon:
weapon = slots[WieldLocation.WEAPON_HAND]
if not weapon:
weapon = WeaponEmptyHand()
weapon = BARE_HANDS
return weapon
def display_loadout(self):

View file

@ -18,7 +18,7 @@ rune sword (weapon+quest).
"""
from evennia import AttributeProperty
from evennia import AttributeProperty, create_object, search_object
from evennia.objects.objects import DefaultObject
from evennia.utils.utils import make_iter
@ -125,7 +125,7 @@ class EvAdventureTreasure(EvAdventureObject):
"""
obj_type = ObjType.TREASURE
value = AttributeProperty(100)
value = AttributeProperty(100, autocreate=False)
class EvAdventureConsumable(EvAdventureObject):
@ -136,14 +136,26 @@ class EvAdventureConsumable(EvAdventureObject):
"""
obj_type = ObjType.CONSUMABLE
size = AttributeProperty(0.25)
uses = AttributeProperty(1)
size = AttributeProperty(0.25, autocreate=False)
uses = AttributeProperty(1, autocreate=False)
def use(self, user, target, *args, **kwargs):
def at_pre_use(self, user, target=None, *args, **kwargs):
if target and user.location != target.location:
user.msg("You are not close enough to the target!")
return False
if self.uses <= 0:
user.msg(f"|w{self.key} is used up.|n")
return False
return super().at_pre_use(user, target=target, *args, **kwargs)
def use(self, user, target=None, *args, **kwargs):
"""
Use the consumable.
"""
if user.location:
user.location.msg_contents(
f"$You() $conj(use) {self.get_display_name(user)}.", from_obj=user
@ -193,11 +205,16 @@ class EvAdventureWeapon(EvAdventureObject):
return super().get_display_name(looker=looker, **kwargs) + quality_txt
def at_pre_use(self, user, *args, **kwargs):
def at_pre_use(self, user, target=None, *args, **kwargs):
if target and user.location != target.location:
# we assume weapons can only be used in the same location
user.msg("You are not close enough to the target!")
return False
if self.quality <= 0:
user.msg(f"{self.get_display_name(user)} is broken and can't be used!")
return False
return super().at_pre_use(user, *args, **kwargs)
return super().at_pre_use(user, target=target, *args, **kwargs)
def use(self, attacker, target, *args, advantage=False, disadvantage=False, **kwargs):
"""When a weapon is used, it attacks an opponent"""
@ -239,7 +256,8 @@ class EvAdventureWeapon(EvAdventureObject):
message = f" $You() $conj(miss) $You({target.key})."
if quality is Ability.CRITICAL_FAILURE:
message += ".. it's a |rcritical miss!|n, damaging the weapon."
self.quality -= 1
if self.quality is not None:
self.quality -= 1
location.msg_contents(message, from_obj=attacker, mapping={target.key: target})
def at_post_use(self, user, *args, **kwargs):
@ -262,21 +280,6 @@ class EvAdventureThrowable(EvAdventureWeapon, EvAdventureConsumable):
damage_roll = AttributeProperty("1d6")
class WeaponEmptyHand(EvAdventureWeapon):
"""
This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it.
"""
obj_type = ObjType.WEAPON
key = "Empty Fists"
inventory_use_slot = WieldLocation.WEAPON_HAND
attack_type = Ability.STR
defense_type = Ability.ARMOR
damage_roll = "1d4"
quality = 100000 # let's assume fists are always available ...
class EvAdventureRunestone(EvAdventureWeapon, EvAdventureConsumable):
"""
Base class for magic runestones. In _Knave_, every spell is represented by a rune stone
@ -335,3 +338,23 @@ class EvAdventureHelmet(EvAdventureArmor):
obj_type = ObjType.HELMET
inventory_use_slot = WieldLocation.HEAD
class WeaponBareHands(EvAdventureWeapon):
"""
This is a dummy-class loaded when you wield no weapons. We won't create any db-object for it.
"""
obj_type = ObjType.WEAPON
key = "Bare hands"
inventory_use_slot = WieldLocation.WEAPON_HAND
attack_type = Ability.STR
defense_type = Ability.ARMOR
damage_roll = "1d4"
quality = 100000 # let's assume fists are always available ...
BARE_HANDS = search_object("Bare hands", typeclass=WeaponBareHands)
if not BARE_HANDS:
BARE_HANDS = create_object(WeaponBareHands, key="Bare hands")

View file

@ -0,0 +1,54 @@
"""
Test of EvAdventure Rooms
"""
from evennia import DefaultExit, create_object
from evennia.utils.ansi import strip_ansi
from evennia.utils.test_resources import EvenniaTestCase
from ..characters import EvAdventureCharacter
from ..rooms import EvAdventureRoom
class EvAdventureRoomTest(EvenniaTestCase):
def setUp(self):
self.char = create_object(EvAdventureCharacter, key="TestChar")
def test_map(self):
center_room = create_object(EvAdventureRoom, key="room_center")
n_room = create_object(EvAdventureRoom, key="room_n")
create_object(DefaultExit, key="north", location=center_room, destination=n_room)
ne_room = create_object(EvAdventureRoom, key="room_ne")
create_object(DefaultExit, key="northeast", location=center_room, destination=ne_room)
e_room = create_object(EvAdventureRoom, key="room_e")
create_object(DefaultExit, key="east", location=center_room, destination=e_room)
se_room = create_object(EvAdventureRoom, key="room_se")
create_object(DefaultExit, key="southeast", location=center_room, destination=se_room)
s_room = create_object(EvAdventureRoom, key="room_")
create_object(DefaultExit, key="south", location=center_room, destination=s_room)
sw_room = create_object(EvAdventureRoom, key="room_sw")
create_object(DefaultExit, key="southwest", location=center_room, destination=sw_room)
w_room = create_object(EvAdventureRoom, key="room_w")
create_object(DefaultExit, key="west", location=center_room, destination=w_room)
nw_room = create_object(EvAdventureRoom, key="room_nw")
create_object(DefaultExit, key="northwest", location=center_room, destination=nw_room)
desc = center_room.return_appearance(self.char)
expected = r"""
o o o
\|/
o-@-o
/|\
o o o
room_center
You see nothing special.
Exits: north, northeast, east, southeast, south, southwest, west, and northwest"""
result = "\n".join(part.rstrip() for part in strip_ansi(desc).split("\n"))
expected = "\n".join(part.rstrip() for part in expected.split("\n"))
print(result)
print(expected)
self.assertEqual(result, expected)