"""
Test EvAdventure combat.
"""
from unittest.mock import Mock, call, patch
from evennia.utils import create
from evennia.utils.ansi import strip_ansi
from evennia.utils.test_resources import (
BaseEvenniaTest,
EvenniaCommandTestMixin,
EvenniaTestCase,
)
from .. import combat_base, combat_turnbased, combat_twitch
from ..characters import EvAdventureCharacter
from ..enums import Ability, WieldLocation
from ..npcs import EvAdventureMob
from ..objects import EvAdventureConsumable, EvAdventureRunestone, EvAdventureWeapon
from ..rooms import EvAdventureRoom
class _CombatTestBase(EvenniaTestCase):
"""
Set up common entities for testing combat:
- `location` (key=testroom)
- `combatant` (key=testchar)
- `target` (key=testmonster)`
We also mock the `.msg` method of both `combatant` and `target` so we can
see what was sent.
"""
def setUp(self):
self.location = create.create_object(EvAdventureRoom, key="testroom")
self.combatant = create.create_object(
EvAdventureCharacter, key="testchar", location=self.location
)
self.location.allow_combat = True
self.location.allow_death = True
self.target = create.create_object(
EvAdventureMob,
key="testmonster",
location=self.location,
attributes=(("is_idle", True),),
)
# mock the msg so we can check what they were sent later
self.combatant.msg = Mock()
self.target.msg = Mock()
[docs]class TestEvAdventureCombatBaseHandler(_CombatTestBase):
"""
Test the base functionality of the base combat handler.
"""
[docs] def setUp(self):
"""This also tests the `get_or_create_combathandler` classfunc"""
super().setUp()
self.combathandler = combat_base.EvAdventureCombatBaseHandler.get_or_create_combathandler(
self.location, key="combathandler"
)
[docs] def test_combathandler_msg(self):
"""Test sending messages to all in handler"""
self.location.msg_contents = Mock()
self.combathandler.msg("test_message")
self.location.msg_contents.assert_called_with(
"test_message",
exclude=[],
from_obj=None,
mapping={"testchar": self.combatant, "testmonster": self.target},
)
[docs] def test_get_combat_summary(self):
"""Test combat summary"""
self.combathandler.get_sides = Mock(return_value=([self.combatant], [self.target]))
# as seen from one side
result = str(self.combathandler.get_combat_summary(self.combatant))
self.assertEqual(
strip_ansi(result),
" testchar (Perfect) vs testmonster (Perfect) ",
)
# as seen from other side
self.combathandler.get_sides = Mock(return_value=([self.target], [self.combatant]))
result = str(self.combathandler.get_combat_summary(self.target))
self.assertEqual(
strip_ansi(result),
" testmonster (Perfect) vs testchar (Perfect) ",
)
[docs]class TestCombatActionsBase(_CombatTestBase):
"""
A class for testing all subclasses of CombatAction in combat_base.py
"""
[docs] def setUp(self):
super().setUp()
self.combathandler = combat_base.EvAdventureCombatBaseHandler.get_or_create_combathandler(
self.location, key="combathandler"
)
# we need to mock all NotImplemented methods
self.combathandler.get_sides = Mock(return_value=([], [self.target]))
self.combathandler.give_advantage = Mock()
self.combathandler.give_disadvantage = Mock()
self.combathandler.remove_advantage = Mock()
self.combathandler.remove_disadvantage = Mock()
self.combathandler.get_advantage = Mock()
self.combathandler.get_disadvantage = Mock()
self.combathandler.has_advantage = Mock()
self.combathandler.has_disadvantage = Mock()
self.combathandler.queue_action = Mock()
[docs] def test_base_action(self):
action = combat_base.CombatAction(
self.combathandler, self.combatant, {"key": "hold", "foo": "bar"}
)
self.assertEqual(action.key, "hold")
self.assertEqual(action.foo, "bar")
self.assertEqual(action.combathandler, self.combathandler)
self.assertEqual(action.combatant, self.combatant)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_attack__miss(self, mock_randint):
actiondict = {"key": "attack", "target": self.target}
mock_randint.return_value = 8 # target has default armor 11, so 8+1 str will miss
action = combat_base.CombatActionAttack(self.combathandler, self.combatant, actiondict)
action.execute()
self.assertEqual(self.target.hp, 4)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_attack__success(self, mock_randint):
actiondict = {"key": "attack", "target": self.target}
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
self.target.hp = 20
action = combat_base.CombatActionAttack(self.combathandler, self.combatant, actiondict)
action.execute()
self.assertEqual(self.target.hp, 9)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_stunt_fail(self, mock_randint):
action_dict = {
"key": "stunt",
"recipient": self.combatant,
"target": self.target,
"advantage": True,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 8 # fails 8+1 dex vs DEX 11 defence
action = combat_base.CombatActionStunt(self.combathandler, self.combatant, action_dict)
action.execute()
self.combathandler.give_advantage.assert_not_called()
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_stunt_advantage__success(self, mock_randint):
action_dict = {
"key": "stunt",
"recipient": self.combatant,
"target": self.target,
"advantage": True,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
action = combat_base.CombatActionStunt(self.combathandler, self.combatant, action_dict)
action.execute()
self.combathandler.give_advantage.assert_called_with(self.combatant, self.target)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_stunt_disadvantage__success(self, mock_randint):
action_dict = {
"key": "stunt",
"recipient": self.target,
"target": self.combatant,
"advantage": False,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
action = combat_base.CombatActionStunt(self.combathandler, self.combatant, action_dict)
action.execute()
self.combathandler.give_disadvantage.assert_called_with(self.target, self.combatant)
[docs] def test_use_item(self):
"""
Use up a potion during combat.
"""
item = create.create_object(
EvAdventureConsumable, key="Healing potion", attributes=[("uses", 2)]
)
item.use = Mock()
action_dict = {
"key": "use",
"item": item,
"target": self.target,
}
self.assertEqual(item.uses, 2)
action = combat_base.CombatActionUseItem(self.combathandler, self.combatant, action_dict)
action.execute()
self.assertEqual(item.uses, 1)
action.execute()
self.assertEqual(item.pk, None) # deleted, it was used up
[docs] def test_swap_wielded_weapon_or_spell(self):
"""
First draw a weapon (from empty fists), then swap that out to another weapon, then
swap to a spell rune.
"""
sword = create.create_object(EvAdventureWeapon, key="sword")
zweihander = create.create_object(
EvAdventureWeapon,
key="zweihander",
attributes=(("inventory_use_slot", WieldLocation.TWO_HANDS),),
)
runestone = create.create_object(EvAdventureRunestone, key="ice rune")
# check hands are empty
self.assertEqual(self.combatant.weapon.key, "Bare hands")
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
# swap to sword
actiondict = {"key": "wield", "item": sword}
action = combat_base.CombatActionWield(self.combathandler, self.combatant, actiondict)
action.execute()
self.assertEqual(self.combatant.weapon, sword)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], sword)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
# swap to zweihander (two-handed sword)
actiondict["item"] = zweihander
action = combat_base.CombatActionWield(self.combathandler, self.combatant, actiondict)
action.execute()
self.assertEqual(self.combatant.weapon, zweihander)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], zweihander)
# swap to runestone (also using two hands)
actiondict["item"] = runestone
action = combat_base.CombatActionWield(self.combathandler, self.combatant, actiondict)
action.execute()
self.assertEqual(self.combatant.weapon, runestone)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], None)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], runestone)
# swap back to normal one-handed sword
actiondict["item"] = sword
action = combat_base.CombatActionWield(self.combathandler, self.combatant, actiondict)
action.execute()
self.assertEqual(self.combatant.weapon, sword)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.WEAPON_HAND], sword)
self.assertEqual(self.combatant.equipment.slots[WieldLocation.TWO_HANDS], None)
[docs]class EvAdventureTurnbasedCombatHandlerTest(_CombatTestBase):
"""
Test methods on the turn-based combat handler and actions
"""
maxDiff = None
# make sure to mock away all time-keeping elements
[docs] @patch(
(
"evennia.contrib.tutorials.evadventure."
"combat_turnbased.EvAdventureTurnbasedCombatHandler.interval"
),
new=-1,
)
def setUp(self):
super().setUp()
# add target to combat
self.combathandler = (
combat_turnbased.EvAdventureTurnbasedCombatHandler.get_or_create_combathandler(
self.location, key="combathandler"
)
)
self.combathandler.add_combatant(self.combatant)
self.combathandler.add_combatant(self.target)
def _get_action(self, action_dict={"key": "hold"}):
action_class = self.combathandler.action_classes[action_dict["key"]]
return action_class(self.combathandler, self.combatant, action_dict)
def _run_actions(
self, action_dict, action_dict2={"key": "hold"}, combatant_msg=None, target_msg=None
):
"""
Helper method to run an action and check so combatant saw the expected message.
"""
self.combathandler.queue_action(self.combatant, action_dict)
self.combathandler.queue_action(self.target, action_dict2)
self.combathandler.at_repeat()
if combatant_msg is not None:
# this works because we mock combatant.msg in SetUp
self.combatant.msg.assert_called_with(combatant_msg)
if target_msg is not None:
# this works because we mock target.msg in SetUp
self.combatant.msg.assert_called_with(target_msg)
[docs] def test_combatanthandler_setup(self):
"""Testing all is set up correctly in the combathandler"""
chandler = self.combathandler
self.assertEqual(
dict(chandler.combatants),
{self.combatant: {"key": "hold"}, self.target: {"key": "hold"}},
)
self.assertEqual(
dict(chandler.action_classes),
{
"hold": combat_turnbased.CombatActionHold,
"attack": combat_turnbased.CombatActionAttack,
"stunt": combat_turnbased.CombatActionStunt,
"use": combat_turnbased.CombatActionUseItem,
"wield": combat_turnbased.CombatActionWield,
"flee": combat_turnbased.CombatActionFlee,
},
)
self.assertEqual(chandler.flee_timeout, 1)
self.assertEqual(dict(chandler.advantage_matrix), {})
self.assertEqual(dict(chandler.disadvantage_matrix), {})
self.assertEqual(dict(chandler.fleeing_combatants), {})
self.assertEqual(dict(chandler.defeated_combatants), {})
[docs] def test_remove_combatant(self):
"""Remove a combatant."""
self.combathandler.remove_combatant(self.target)
self.assertEqual(dict(self.combathandler.combatants), {self.combatant: {"key": "hold"}})
[docs] def test_stop_combat(self):
"""Stopping combat, making sure combathandler is deleted."""
self.combathandler.stop_combat()
self.assertIsNone(self.combathandler.pk)
[docs] def test_get_sides(self):
"""Getting the sides of combat"""
combatant2 = create.create_object(
EvAdventureCharacter, key="testchar2", location=self.location
)
target2 = create.create_object(
EvAdventureMob,
key="testmonster2",
location=self.location,
attributes=(("is_idle", True),),
)
self.combathandler.add_combatant(combatant2)
self.combathandler.add_combatant(target2)
# allies to combatant
allies, enemies = self.combathandler.get_sides(self.combatant)
self.assertEqual((allies, enemies), ([self.combatant, combatant2], [self.target, target2]))
# allies to monster
allies, enemies = self.combathandler.get_sides(self.target)
self.assertEqual((allies, enemies), ([self.target, target2], [self.combatant, combatant2]))
[docs] def test_queue_and_execute_action(self):
"""Queue actions and execute"""
hold = {"key": "hold"}
self.combathandler.queue_action(self.combatant, hold)
self.assertEqual(
dict(self.combathandler.combatants),
{self.combatant: {"key": "hold"}, self.target: {"key": "hold"}},
)
mock_action = Mock()
self.combathandler.action_classes["hold"] = Mock(return_value=mock_action)
self.combathandler.execute_next_action(self.combatant)
self.combathandler.action_classes["hold"].assert_called_with(
self.combathandler, self.combatant, hold
)
mock_action.execute.assert_called_once()
[docs] def test_execute_full_turn(self):
"""Run a full (passive) turn"""
hold = {"key": "hold"}
self.combathandler.queue_action(self.combatant, hold)
self.combathandler.queue_action(self.target, hold)
self.combathandler.execute_next_action = Mock()
self.combathandler.at_repeat()
self.combathandler.execute_next_action.assert_has_calls(
[call(self.combatant), call(self.target)], any_order=True
)
[docs] def test_action__action_ticks_turn(self):
"""Test that action execution ticks turns"""
actiondict = {"key": "hold"}
self._run_actions(actiondict, actiondict)
self.assertEqual(self.combathandler.turn, 1)
self.combatant.msg.assert_not_called()
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_attack__success__kill(self, mock_randint):
"""Test that the combathandler is deleted once there are no more enemies"""
actiondict = {"key": "attack", "target": self.target}
mock_randint.return_value = 11 # 11 + 1 str will hit beat armor 11
self._run_actions(actiondict)
self.assertEqual(self.target.hp, -7)
# after this the combat is over
self.assertIsNone(self.combathandler.pk)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_stunt_fail(self, mock_randint):
action_dict = {
"key": "stunt",
"recipient": self.combatant,
"target": self.target,
"advantage": True,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 8 # fails 8+1 dex vs DEX 11 defence
self._run_actions(action_dict)
self.assertEqual(self.combathandler.advantage_matrix[self.combatant], {})
self.assertEqual(self.combathandler.disadvantage_matrix[self.combatant], {})
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_stunt_advantage__success(self, mock_randint):
"""Test so the advantage matrix is updated correctly"""
action_dict = {
"key": "stunt",
"recipient": self.combatant,
"target": self.target,
"advantage": True,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
self._run_actions(action_dict)
self.assertEqual(
bool(self.combathandler.advantage_matrix[self.combatant][self.target]), True
)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_base.rules.randint")
def test_stunt_disadvantage__success(self, mock_randint):
"""Test so the disadvantage matrix is updated correctly"""
action_dict = {
"key": "stunt",
"recipient": self.target,
"target": self.combatant,
"advantage": False,
"stunt_type": Ability.STR,
"defense_type": Ability.DEX,
}
mock_randint.return_value = 11 # 11+1 dex vs DEX 11 defence is success
self._run_actions(action_dict)
self.assertEqual(
bool(self.combathandler.disadvantage_matrix[self.target][self.combatant]), True
)
[docs] def test_flee__success(self):
"""
Test fleeing twice, leading to leaving combat.
"""
self.assertEqual(self.combathandler.turn, 0)
action_dict = {"key": "flee", "repeat": True}
# first flee records the fleeing state
self.combathandler.flee_timeout = 2 # to make sure
self._run_actions(action_dict)
self.assertEqual(self.combathandler.turn, 1)
self.assertEqual(self.combathandler.fleeing_combatants[self.combatant], 1)
# action_dict should still be in place due to repeat
self.assertEqual(self.combathandler.combatants[self.combatant], action_dict)
self.combatant.msg.assert_called_with(
text=(
"You retreat, being exposed to attack while doing so (will escape in 1 turn).",
{},
),
from_obj=self.combatant,
)
# Check that enemies have advantage against you now
action = combat_turnbased.CombatAction(self.combathandler, self.target, {"key": "hold"})
self.assertTrue(action.combathandler.has_advantage(self.target, self.combatant))
# second flee should remove combatant
self._run_actions(action_dict)
# this ends combat, so combathandler should be gone
self.assertIsNone(self.combathandler.pk)
[docs]class TestEvAdventureTwitchCombatHandler(EvenniaCommandTestMixin, _CombatTestBase):
[docs] def setUp(self):
super().setUp()
# in order to use the EvenniaCommandTestMixin we need these variables defined
self.char1 = self.combatant
self.account = None
self.combatant_combathandler = (
combat_twitch.EvAdventureCombatTwitchHandler.get_or_create_combathandler(
self.combatant, key="combathandler"
)
)
self.target_combathandler = (
combat_twitch.EvAdventureCombatTwitchHandler.get_or_create_combathandler(
self.target, key="combathandler"
)
)
[docs] def test_get_sides(self):
sides = self.combatant_combathandler.get_sides(self.combatant)
self.assertEqual(sides, ([self.combatant], [self.target]))
[docs] def test_give_advantage(self):
self.combatant_combathandler.give_advantage(self.combatant, self.target)
self.assertTrue(self.combatant_combathandler.advantage_against[self.target])
[docs] def test_give_disadvantage(self):
self.combatant_combathandler.give_disadvantage(self.combatant, self.target)
self.assertTrue(self.combatant_combathandler.disadvantage_against[self.target])
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
@patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock(return_value=999))
def test_queue_action(self):
"""Test so the queue action cleans up tickerhandler correctly"""
actiondict = {"key": "hold"}
self.combatant_combathandler.queue_action(actiondict)
self.assertIsNone(self.combatant_combathandler.current_ticker_ref)
actiondict = {"key": "hold", "dt": 5}
self.combatant_combathandler.queue_action(actiondict)
self.assertEqual(self.combatant_combathandler.current_ticker_ref, 999)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
@patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock())
def test_execute_next_action(self):
self.combatant_combathandler.action_dict = {
"key": "hold",
"dummy": "foo",
"repeat": False,
} # to separate from fallback
self.combatant_combathandler.execute_next_action()
# should now be back to fallback
self.assertEqual(
self.combatant_combathandler.action_dict,
self.combatant_combathandler.fallback_action_dict,
)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
def test_check_stop_combat(self):
"""Test combat-stop functionality"""
# noone remains (both combatant/target <0 hp
# get_sides does not include the caller
self.combatant_combathandler.get_sides = Mock(return_value=([], []))
self.combatant_combathandler.stop_combat = Mock()
self.combatant.hp = -1
self.target.hp = -1
self.combatant_combathandler.check_stop_combat()
self.combatant.msg.assert_called_with(
text=("Noone stands after the dust settles.", {}), from_obj=self.combatant
)
self.combatant_combathandler.stop_combat.assert_called()
# only one side wiped out
self.combatant.hp = 10
self.target.hp = -1
self.combatant_combathandler.get_sides = Mock(return_value=([self.combatant], []))
self.combatant_combathandler.check_stop_combat()
self.combatant.msg.assert_called_with(
text=("The combat is over.", {}), from_obj=self.combatant
)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
@patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock())
def test_hold(self):
self.call(combat_twitch.CmdHold(), "", "You hold back, doing nothing")
self.assertEqual(self.combatant_combathandler.action_dict, {"key": "hold"})
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
@patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock())
def test_attack(self):
"""Test attack action in the twitch combathandler"""
self.call(combat_twitch.CmdAttack(), self.target.key, "You attack testmonster!")
self.assertEqual(
self.combatant_combathandler.action_dict,
{"key": "attack", "target": self.target, "dt": 3, "repeat": True},
)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
@patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock())
def test_stunt(self):
boost_result = {
"key": "stunt",
"recipient": self.combatant,
"target": self.target,
"advantage": True,
"stunt_type": Ability.STR,
"defense_type": Ability.STR,
"dt": 3,
}
foil_result = {
"key": "stunt",
"recipient": self.target,
"target": self.combatant,
"advantage": False,
"stunt_type": Ability.STR,
"defense_type": Ability.STR,
"dt": 3,
}
self.call(
combat_twitch.CmdStunt(),
f"STR {self.target.key}",
"You prepare a stunt!",
cmdstring="boost",
)
self.assertEqual(self.combatant_combathandler.action_dict, boost_result)
self.call(
combat_twitch.CmdStunt(),
f"STR me {self.target.key}",
"You prepare a stunt!",
cmdstring="boost",
)
self.assertEqual(self.combatant_combathandler.action_dict, boost_result)
self.call(
combat_twitch.CmdStunt(),
f"STR {self.target.key}",
"You prepare a stunt!",
cmdstring="foil",
)
self.assertEqual(self.combatant_combathandler.action_dict, foil_result)
self.call(
combat_twitch.CmdStunt(),
f"STR {self.target.key} me",
"You prepare a stunt!",
cmdstring="foil",
)
self.assertEqual(self.combatant_combathandler.action_dict, foil_result)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
@patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock())
def test_useitem(self):
item = create.create_object(
EvAdventureConsumable, key="potion", attributes=[("uses", 2)], location=self.combatant
)
self.call(combat_twitch.CmdUseItem(), "potion", "You prepare to use potion!")
self.assertEqual(
self.combatant_combathandler.action_dict,
{"key": "use", "item": item, "target": self.combatant, "dt": 3},
)
self.call(
combat_twitch.CmdUseItem(),
f"potion on {self.target.key}",
"You prepare to use potion!",
)
self.assertEqual(
self.combatant_combathandler.action_dict,
{"key": "use", "item": item, "target": self.target, "dt": 3},
)
[docs] @patch("evennia.contrib.tutorials.evadventure.combat_twitch.unrepeat", new=Mock())
@patch("evennia.contrib.tutorials.evadventure.combat_twitch.repeat", new=Mock())
def test_wield(self):
sword = create.create_object(EvAdventureWeapon, key="sword", location=self.combatant)
runestone = create.create_object(
EvAdventureWeapon, key="runestone", location=self.combatant
)
self.call(combat_twitch.CmdWield(), "sword", "You reach for sword!")
self.assertEqual(
self.combatant_combathandler.action_dict, {"key": "wield", "item": sword, "dt": 3}
)
self.call(combat_twitch.CmdWield(), "runestone", "You reach for runestone!")
self.assertEqual(
self.combatant_combathandler.action_dict, {"key": "wield", "item": runestone, "dt": 3}
)