Skip to content

Commit d633129

Browse files
authored
Update v0.26.1 (#73)
* make rat quadruped * add body parts for quadrupeds some more tests * support for targeting body parts * fix wrong callback for spawner refactor do_on_death to be defered * fix body type parsing * fix parsing race and body type * change up the order of tells for actions * commit tests * fix spawner tests
1 parent 5139b2e commit d633129

File tree

14 files changed

+159
-60
lines changed

14 files changed

+159
-60
lines changed

stories/combat_sandbox/world.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@
9898
"xp": 0,
9999
"strength": 3,
100100
"dexterity": 3,
101-
"unarmed_attack": "BITE"
101+
"unarmed_attack": "BITE",
102+
"race": "giant rat"
102103
}
103104
}
104105
]

tale/base.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,7 +1270,11 @@ def do_socialize_cmd(self, parsed: ParseResult) -> None:
12701270
if attacking:
12711271
for thing in who:
12721272
if isinstance(thing, Living):
1273-
pending_actions.send(lambda victim=thing: self.start_attack(victim))
1273+
body_part = None
1274+
if len(parsed.args) == 2:
1275+
arg = parsed.args[1].upper()
1276+
body_part = arg if arg in wearable.WearLocation.__members__ else None
1277+
pending_actions.send(lambda victim=thing: self.start_attack(victim, target_body_part=body_part))
12741278
elif parsed.verb in verbdefs.AGGRESSIVE_VERBS:
12751279
# usually monsters immediately attack,
12761280
# other npcs may choose to attack or to ignore it
@@ -1415,11 +1419,11 @@ def locate_item(self, name: str, include_inventory: bool=True, include_location:
14151419
break
14161420
return (found, containing_object) if found else (None, None)
14171421

1418-
def start_attack(self, victim: 'Living') -> combat.Combat:
1422+
def start_attack(self, defender: 'Living', target_body_part: wearable.WearLocation= None) -> combat.Combat:
14191423
"""Starts attacking the given living for one round."""
14201424
attacker_name = lang.capital(self.title)
1421-
victim_name = lang.capital(victim.title)
1422-
c = combat.Combat(self, victim)
1425+
victim_name = lang.capital(defender.title)
1426+
c = combat.Combat(self, defender, target_body_part=target_body_part)
14231427
result, damage_to_attacker, damage_to_defender = c.resolve_attack()
14241428

14251429
room_msg = "%s attacks %s! %s" % (attacker_name, victim_name, result)
@@ -1428,30 +1432,30 @@ def start_attack(self, victim: 'Living') -> combat.Combat:
14281432
#victim.tell(victim_msg, evoke=True, short_len=False)
14291433

14301434
combat_prompt, attacker_msg = mud_context.driver.prepare_combat_prompt(attacker=self,
1431-
defender=victim,
1435+
defender=defender,
14321436
location_title = self.location.title,
14331437
combat_result = result,
14341438
attacker_msg = attacker_msg)
14351439

14361440
combat_context = CombatContext(attacker_name=self.name,
14371441
attacker_health=self.stats.hp / self.stats.max_hp,
14381442
attacker_weapon=self.wielding.name,
1439-
defender_name=victim.name,
1440-
defender_health=victim.stats.hp / victim.stats.max_hp,
1441-
defender_weapon=victim.wielding.name,
1443+
defender_name=defender.name,
1444+
defender_health=defender.stats.hp / defender.stats.max_hp,
1445+
defender_weapon=defender.wielding.name,
14421446
location_description=self.location.description)
14431447

1444-
victim.location.tell(room_msg,
1448+
defender.location.tell(room_msg,
14451449
evoke=True,
14461450
short_len=False,
14471451
alt_prompt=combat_prompt,
14481452
extra_context=combat_context.to_prompt_string())
14491453
self.stats.hp -= damage_to_attacker
1450-
victim.stats.hp -= damage_to_defender
1454+
defender.stats.hp -= damage_to_defender
14511455
if self.stats.hp < 1:
1452-
self.do_on_death(util.Context)
1453-
if victim.stats.hp < 1:
1454-
victim.do_on_death(util.Context)
1456+
mud_context.driver.defer(0.1, self.do_on_death)
1457+
if defender.stats.hp < 1:
1458+
mud_context.driver.defer(0.1, defender.do_on_death)
14551459
return c
14561460

14571461
def allow_give_money(self, amount: float, actor: Optional['Living']) -> None:
@@ -1585,7 +1589,7 @@ def get_worn_items(self) -> Iterable[Wearable]:
15851589
"""Return all items that are currently worn."""
15861590
return self.__wearing.values()
15871591

1588-
def do_on_death(self, ctx: util.Context) -> 'Container':
1592+
def do_on_death(self) -> 'Container':
15891593
"""Called when the living dies."""
15901594
remains = None
15911595
self.alive = False
@@ -1595,7 +1599,7 @@ def do_on_death(self, ctx: util.Context) -> 'Container':
15951599
self.location.insert(remains, None)
15961600
if self.on_death_callback:
15971601
self.on_death_callback()
1598-
self.destroy(ctx)
1602+
self.destroy(util.Context)
15991603
return remains
16001604

16011605

tale/combat.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,27 @@
66

77
import random
88
from tale import weapon_type
9-
from tale.races import BodyType
10-
import tale.util as util
119
import tale.base as base
12-
from tale.util import Context
13-
from tale.wearable import WearLocation
10+
from tale.wearable import WearLocation, body_parts_for_bodytype
1411
from tale.wearable import WearLocation
1512
import random
1613
from collections import Counter
1714
import random
1815

1916
class Combat():
2017

21-
def __init__(self, attacker: 'base.Living', defender: 'base.Living') -> None:
18+
def __init__(self, attacker: 'base.Living', defender: 'base.Living', target_body_part: WearLocation = None) -> None:
2219
self.attacker = attacker
2320
self.defender = defender
21+
self.target_body_part = target_body_part
2422

2523
def _calculate_attack_success(self, actor: 'base.Living') -> int:
26-
""" Calculate the success of an attack. <5 is a critical hit."""
27-
return random.randrange(0, 100) - actor.stats.get_weapon_skill(actor.wielding.type)
24+
""" Calculate the success of an attack. <5 is a critical hit.
25+
Lower chance for attacker if trying to hit a specific body part."""
26+
chance = actor.stats.get_weapon_skill(actor.wielding.type)
27+
if self.target_body_part and actor == self.attacker:
28+
chance *= 1.2
29+
return random.randrange(0, 100) - chance
2830

2931
def _calculate_block_success(self, actor1: 'base.Living', actor2: 'base.Living') -> int:
3032
""" Calculate the chance of blocking an attack.
@@ -51,10 +53,10 @@ def _calculate_armor_bonus(self, actor: 'base.Living', body_part: WearLocation =
5153

5254
def resolve_body_part(self, defender: 'base.Living', size_factor: float, target_part: WearLocation = None) -> WearLocation:
5355
""" Resolve the body part that was hit. """
54-
if defender.stats.bodytype != BodyType.HUMANOID:
55-
return WearLocation.FULL_BODY
56-
57-
locations = list(WearLocation)[1:-1]
56+
body_parts = body_parts_for_bodytype(defender.stats.bodytype)
57+
if not body_parts:
58+
body_parts = [WearLocation.FULL_BODY]
59+
locations = body_parts
5860
probability_distribution = self.create_probability_distribution(locations, size_factor=size_factor, target_part=target_part)
5961

6062
return random.choices(list(probability_distribution.keys()), list(probability_distribution.values()))[0]
@@ -64,10 +66,14 @@ def create_probability_distribution(self, locations, size_factor: float = 1.0, t
6466
total_items = sum(distribution.values())
6567

6668
if size_factor != 1.0:
67-
distribution[WearLocation.HEAD] *= size_factor
68-
distribution[WearLocation.TORSO] *= size_factor
69-
distribution[WearLocation.LEGS] /= size_factor
70-
distribution[WearLocation.FEET] /= size_factor
69+
if WearLocation.HEAD in distribution:
70+
distribution[WearLocation.HEAD] *= size_factor
71+
if WearLocation.TORSO in distribution:
72+
distribution[WearLocation.TORSO] *= size_factor
73+
if WearLocation.LEGS in distribution:
74+
distribution[WearLocation.LEGS] /= size_factor
75+
if WearLocation.FEET in distribution:
76+
distribution[WearLocation.FEET] /= size_factor
7177

7278
if target_part:
7379
distribution[target_part] *= 2
@@ -114,7 +120,7 @@ def _round(self, actor1: 'base.Living', actor2: 'base.Living') -> ([str], int):
114120
texts.append(f'but {actor2.title} blocks')
115121
else:
116122
actor1_strength = self._calculate_weapon_bonus(actor1) * actor1.stats.size.order
117-
body_part = self.resolve_body_part(actor2, actor1.stats.size.order / actor2.stats.size.order)
123+
body_part = self.resolve_body_part(actor2, actor1.stats.size.order / actor2.stats.size.order, target_part=self.target_body_part)
118124
actor2_strength = self._calculate_armor_bonus(actor2, body_part) * actor2.stats.size.order
119125
damage_to_defender = int(max(0, actor1_strength - actor2_strength))
120126
if damage_to_defender > 0:

tale/llm/LivingNpc.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,12 @@ def _parse_action(self, action):
238238
if action.target:
239239
target = self.location.search_living(action.target)
240240
if target:
241+
target.tell(text, evoke=False)
241242
target.notify_action(ParseResult(verb='say', unparsed=text, who_list=[target]), actor=self)
242-
defered_actions.append(f'"{text}"')
243+
else:
244+
defered_actions.append(f'"{text}"')
245+
else:
246+
defered_actions.append(f'"{text}"')
243247
if not action.action:
244248
return defered_actions
245249
if action.action == 'move':
@@ -282,8 +286,8 @@ def _defer_result(self, action: str, verb: str="idle-action"):
282286
def tell_action_deferred(self):
283287
actions = '\n'.join(self.deferred_actions) + '\n'
284288
deferred_action = ParseResult(verb='idle-action', unparsed=actions, who_info=None)
285-
self.location._notify_action_all(deferred_action, actor=self)
286289
self.tell_others(actions)
290+
self.location._notify_action_all(deferred_action, actor=self)
287291
self.deferred_actions.clear()
288292

289293
def get_observed_events(self, amount: int) -> list:

tale/npc_defs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def do_idle_action(self, ctx: Context) -> None:
4141
self.idle_action()
4242

4343
def do_attack(self, target: Living) -> None:
44-
self.start_attack(victim=target)
44+
self.start_attack(defender=target)
4545

4646
class RoamingMob(StationaryMob):
4747

tale/parse_utils.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,23 +148,20 @@ def load_npcs(json_npcs: list, locations = {}) -> dict:
148148
name = npc['name']
149149
new_npc = _load_npc(npc, name, npc_type)
150150

151-
if npc.get('stats', None):
152-
new_npc.stats = load_stats(npc['stats'])
153-
154151
if locations and npc['location']:
155152
_insert(new_npc, locations, npc['location'])
156153

157-
if npc.get('memory', None):
158-
new_npc.load_memory(npc['memory'])
159-
160154
npcs[name] = new_npc
161155
return npcs
162156

163157
def _load_npc(npc: dict, name: str = None, npc_type: str = 'Mob'):
158+
race = None
159+
if npc.get('stats', None):
160+
race = npc['stats'].get('race', None)
164161
if 'npc' in npc_type.lower():
165162
new_npc = StationaryNpc(name=name,
166163
gender=lang.validate_gender(npc.get('gender', 'm')[0]),
167-
race=npc.get('race', 'human').lower(),
164+
race=race,
168165
title=npc.get('title', name),
169166
descr=npc.get('descr', ''),
170167
short_descr=npc.get('short_descr', npc.get('description', '')),
@@ -174,17 +171,26 @@ def _load_npc(npc: dict, name: str = None, npc_type: str = 'Mob'):
174171
new_npc.aliases.add(name.split(' ')[0].lower())
175172
new_npc.stats.set_weapon_skill(WeaponType.UNARMED, random.randint(10, 30))
176173
new_npc.stats.level = npc.get('level', 1)
174+
175+
177176
else:
178177

179178
new_npc = StationaryMob(name=npc['name'],
180179
gender=lang.validate_gender(npc.get('gender', 'm')[0]),
181-
race=npc.get('race', 'human').lower(),
180+
race=race,
182181
title=npc.get('title', npc['name']),
183182
descr=npc.get('descr', ''),
184183
short_descr=npc.get('short_descr', npc.get('description', '')))
185184
new_npc.aliases.add(name.split(' ')[0].lower())
186185
new_npc.stats.set_weapon_skill(WeaponType.UNARMED, random.randint(10, 30))
187186
new_npc.stats.level = npc.get('level', 1)
187+
188+
if npc.get('stats', None):
189+
new_npc.stats = load_stats(npc['stats'])
190+
191+
if npc.get('memory', None):
192+
new_npc.load_memory(npc['memory'])
193+
188194
new_npc.autonomous = npc.get('autonomous', False)
189195
new_npc.aggressive = npc.get('aggressive', False)
190196
return new_npc
@@ -625,7 +631,7 @@ def save_stats(stats: Stats) -> dict:
625631
json_stats['dexterity'] = stats.dexterity
626632
json_stats['unarmed_attack'] = stats.unarmed_attack.name.upper()
627633
json_stats['race'] = stats.race
628-
json_stats['bodytype'] = stats.bodytype.name
634+
json_stats['bodytype'] = stats.bodytype.name.upper()
629635
return json_stats
630636

631637

@@ -644,7 +650,7 @@ def load_stats(json_stats: dict) -> Stats:
644650
stats.dexterity = json_stats.get('dexterity')
645651
stats.race = json_stats.get('race', 'human')
646652
if json_stats.get('bodytype'):
647-
stats.bodytype = BodyType[json_stats.get('bodytype', BodyType.HUMANOID.name)]
653+
stats.bodytype = BodyType[json_stats['bodytype'].upper()]
648654
if json_stats.get('unarmed_attack'):
649655
stats.unarmed_attack = Weapon(UnarmedAttack[json_stats['unarmed_attack'].upper()], WeaponType.UNARMED)
650656
if json_stats.get('weapon_skills'):

tale/player.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def tell_object_location(self, obj: base.MudObject, known_container: Union[base.
210210
else:
211211
self.tell("%s was found in %s." % (lang.capital(obj.name), known_container.name))
212212

213-
def do_on_death(self, ctx: util.Context) -> Optional[base.Item]:
213+
def do_on_death(self) -> Optional[base.Item]:
214214
"""Called when the player dies. Returns the remains of the player."""
215215
self.tell("You die.")
216216
self.location.tell("%s dies." % self.title, exclude_living=self)

tale/races.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,6 @@ class UnarmedAttack(str, enum.Enum):
763763
}
764764
}
765765

766-
767766
# Races that can be chosen by players. Can be changed in story configuration.
768767
playable_races = {'human', 'dwarf', 'elf', 'dark-elf', 'half-elf', 'half-orc', 'halfling', 'orc', 'goblin', 'hobbit'}
769768

tale/spawner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def spawn(self):
3333
if self.max_spawns > 0:
3434
self.max_spawns -= 1
3535
mob = self._clone_mob()
36-
mob.do_on_death = lambda ctx: self.remove_mob()
36+
mob.on_death_callback = lambda: self.remove_mob()
3737
self.location.insert(mob)
3838
self.location.tell("%s arrives." % mob.title, extra_context=f'Location:{self.location.description}; {mob.title}: {mob.description}')
3939
return mob
@@ -73,7 +73,7 @@ def _clone_mob(self):
7373
gender = self.mob_type.gender
7474
if self.randomize_gender:
7575
gender = "m" if random.randint(0, 1) == 0 else "f"
76-
mob = self.mob_type.__class__(self.mob_type.name, gender)
76+
mob = self.mob_type.__class__(self.mob_type.name, gender, race=self.mob_type.stats.race)
7777
mob.aggressive = self.mob_type.aggressive
7878
mob.should_produce_remains = self.mob_type.should_produce_remains
7979
return mob

tale/verbdefs.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,5 +428,9 @@ def adjust_available_verbs(allowed_verbs: Sequence[str]=None, remove_verbs: Sequ
428428
"cheek": "on the cheek",
429429
"side": "in the side",
430430
"everywhere": "everywhere",
431-
"shoulder": "on the shoulder"
431+
"shoulder": "on the shoulder",
432+
"feet": "on the feet",
433+
"tail": "on the tail",
434+
"wing": "on the wing",
435+
"wings": "on the wings"
432436
}

0 commit comments

Comments
 (0)