Commit bed7fff1 authored by Toast Engineer's avatar Toast Engineer

Combat is technically playable now.

parent 392814ac
......@@ -30,9 +30,18 @@ class Battle:
def active_participants(self):
return [c for c in self.participants if not c.dead]
def teams(self, me):
"""Returns my_team, enemy_team, as lists"""
if me in self.player_characters:
return self.player_characters, self.enemies
elif me in self.enemies:
return self.enemies, self.player_characters
raise ValueError(f"{me} isn't part of either team")
def is_over(self):
return (not self.participants or
return (not self.player_characters or not self.enemies or
all(pc.incapacitated for pc in self.player_characters) or
all(npc.incapacitated for npc in self.enemies))
......@@ -42,14 +51,37 @@ class Battle:
return retdict
def turn(self):
from character import StartActionFindingOver
next_turn = deepcopy(self)
actions = []
print("-" * 80)
for the_character in self.participants:
actions = []
for the_character in self.enemies:
if the_character.incapacitated: continue
action = the_character.get_next_battle_action(next_turn)
print(f"{the_character} intends to {action}.")
while True:
for the_character in self.player_characters:
if the_character.incapacitated:
print(f"{the_character} is incapacitated.")
action = the_character.get_next_battle_action(next_turn)
if isinstance(action, StartActionFindingOver):
print(f"{the_character} intends to {action}.")
for the_action in actions:
next_turn.predecessor = self
......@@ -2,10 +2,13 @@ import functools
import itertools
import pprint
import random
import typing
import minigames
import rendering
if typing.TYPE_CHECKING:
import battle
class LimitedList(list):
......@@ -44,6 +47,50 @@ class BleedingOut(StatusEffect):
def time_up(self, character):
class BattleAction:
def __call__(self, battle: 'battle.Battle'):
class UseItem(BattleAction):
def __init__(self, item, user, target):
self.item = item
self.user = user = target
def __str__(self):
if self.item.harmful:
return f"attack {} with {self.item}"
return f"use {self.item} on {}"
def __call__(self, battle):
should_remain = self.item.use_in_combat(self.user,
if not should_remain:
except IndexError:
class RunAway(BattleAction):
def __init__(self, runner):
self.runner = runner
def __str__(self):
return "run away"
def __call__(self, battle):
if self.runner in battle.player_characters:
print(f"{self.runner} runs away!")
elif self.runner in battle.enemies:
print(f"{self.runner} runs away!")
class StartActionFindingOver(BattleAction):
def __str__(self):
return """Wait, I changed my mind!"""
class Character:
class_name = "Generic"
......@@ -52,6 +99,8 @@ class Character:
magic_gain_factor = 1
tech_gain_factor = 1
is_pc = False
def __init__(self, name = "noname", strength = 1, agility = 1, magic = 1, tech = 1, max_physical_stamina = 2, max_mental_stamina = 2): = name
......@@ -136,19 +185,26 @@ Mntl Stam: {self.mental_stamina:.1f} / {self.max_mental_stamina:.1f}
if self.dead: return []
actions = []
for the_item in self.items_usable_in_combat:
for the_other in battle.active_participants:
the_action = functools.partial(the_item.use_in_combat, self, the_other)
def run_away():
if self in battle.player_characters:
print(f"{self} ran away!")
elif self in battle.enemies:
print(f"{self} ran away!")
combat_items = self.items_usable_in_combat
attacks = [i for i in combat_items if i.harmful]
utilities = [i for i in combat_items if not i.harmful]
us, them = battle.teams(self)
for the_attack in attacks:
for the_enemy in them:
actions.append(UseItem(the_attack, self, the_enemy))
for the_helpful_thing in utilities:
for the_friend in us:
actions.append(UseItem(the_helpful_thing, self, the_friend))
if not actions or self.is_pc: #npcs should only run if they have no other choice
if self.is_pc:
return actions
......@@ -158,27 +214,36 @@ Mntl Stam: {self.mental_stamina:.1f} / {self.max_mental_stamina:.1f}
return lambda: print(f"{self} couldn't do anything.")
return random.choice(actions)
class MurderousJerk(Character):
class_name = "Murderous Jerk"
class Wizard(Character):
class PlayerCharacter(Character):
is_pc = True
def get_next_battle_action(self, battle):
import text_menu
return text_menu.PopUpMenu(self.collect_battle_actions(battle), dontcall=True)()
class Wizard(PlayerCharacter):
class_name = "Wizard"
magic_gain_factor = 2.5
class Warrior(Character):
class Warrior(PlayerCharacter):
class_name = "Warrior"
magic_gain_factor = 2.5
class Rifleman(Character):
class Rifleman(PlayerCharacter):
class_name = "Rifleman"
agility_gain_factor = 2.5
class Technologist(Character):
class Technologist(PlayerCharacter):
class_name = "Technologist"
tech_gain_factor = 2.5
class Monster(Character):
class Monster(PlayerCharacter):
class_name = "Monster"
class Robot(Character):
class Robot(PlayerCharacter):
class_name = "Robot"
class Party(LimitedList):
......@@ -191,6 +256,7 @@ class Item:
sale_price = 100
minigame = minigames.typing_tutor
skill = "tech"
harmful = False
def name(self):
......@@ -224,7 +290,7 @@ class Item:
#Used in combat
def use_in_combat(self, character: Character, target: Character):
self.use(character, target)
def usable_in_combat(self):
......@@ -234,10 +300,16 @@ class FirstAidKit(Item):
usable_in_combat = False
def use(self, character, target):
effects = target.status_effects
if not any(isinstance(the_effect, BleedingOut) for the_effect in target.status_effects):
print(f"{target} isn't bleeding out - a first aid kit won't fix anything other than that.")
return True
effects[:] = [the_effect for the_effect in effects
if not isinstance(the_effect, BleedingOut)]
class CombatFirstAidKit(FirstAidKit):
"""A fancy way of saying 'styptic powder cut with novocaine'.
Designed to stop someone from bleeding out while whoever stabbed him
is still around."""
usable_in_combat = True
class LazarusInProgress(StatusEffect):
......@@ -259,7 +331,9 @@ class LazarusPump(Item):
sale_price = 1_000_000
def use(self, character, target):
if not target.dead: return True
if not target.dead:
print(f"{target} isn't even mostly dead.")
return True
......@@ -281,10 +355,15 @@ class BootlegLazarusPump(Item):
sale_price = 80_000
def use(self, character, target):
if not target.dead: return True
if not target.dead:
print(f"{target} isn't even mostly dead (and glad for it!)")
return True
class MeleeWeapon(Item):
class Weapon(Item):
harmful = True
class MeleeWeapon(Weapon):
minigame = minigames.test_your_strength
base_power = 1
sharpness = 1 #divisor for stamina loss by user - if it's 1, then if you get 0.5 on the minigame it does the exact same thing to the enemy as you
......@@ -108,8 +108,6 @@ class GameState:
def run_game(self) -> typing.NoReturn:
clock = pygame.time.Clock()
while True:
self.mainwin.world_view_active = self.current_level is not None
......@@ -126,6 +124,11 @@ class GameState:
elif self.current_level is not None:
if all(c.incapacitated for c in
text_menu.pop_up_notification("Your party has fallen.")
self.current_level = None
......@@ -141,8 +144,18 @@ class GameState:
print("You may or may not have to restart the game. Please tell me about this.")
def start_battle(self):
enemies = [character.Warrior() for t in range(random.randint(1,6))]
def start_battle(self, enemies = None):
if not enemies:
enemies = []
for t in range(random.randint(1, 4)):
enemy = character.MurderousJerk(f"Murderous Jerk {t}")
for i in range(1, 3):
text_menu.pop_up_notification("You are attacked by: " + "\n".join(str(e) for e in enemies))
self.battle = battle.Battle(, enemies)
def abandon_party(self):
......@@ -191,6 +204,8 @@ class GameState:
elif theevent.key == pygame.K_ESCAPE:
elif theevent.key == pygame.K_SPACE:
if __name__ == "__main__":
All images from, CC-BY
Ancient prophesies foretold of the tower that rose out of the ground one day.
They said that legendary adventurers from across the land would unite, to face
its challenges, and die like the chumps they are.
It's not really causing anyone any problems, it's just full of treasure and monsters
for some reason. There's even a nice little town built up around it. I mean, there's
_probably_ something awful at the top, but no-one worries about that and the thing
is like a thousand floors high, no-one's been up past the first hundred.
The layout of the floors is different for every group who go in, and there's always
more loot to be had, so really, Base Town is the place to be if you're looking to get
rich quick and aren't afraid of a few agitated skeletons.
Go in. Get the money. Don't die.
By TOASTEngineer. Music by tieff.
All images from, CC-BY
#Probably won't get to this in time, but I want the infrastructure to be there at least
def play_sfx(sfx):
"""Play the sound described by the given string, i.e. 'low beep' plays... a low beep."""
\ No newline at end of file
......@@ -51,12 +51,14 @@ class PopUpMenu(dict):
case it will be called and its return value will be returned. Arguments to the menu will be passed
through to the callable value. This can be used to nest menus.
If dontcall is true, this behavior is overridden and it just returns the callable.
If only keyword arguments are provided, converts _s to spaces in the keys.
If noreturn is True, does not return the result and just shows options forever. Break out by raising an
def __init__(self, *args, loop_until_non_callable = False, prompt = None, **kwargs):
def __init__(self, *args, loop_until_non_callable = False, prompt = None, dontcall = False, **kwargs):
if kwargs and not args:
kwargs = {k.replace("_", ' ') : v for k, v in kwargs.items()}
if len(args) == 1 and not isinstance(args[0], dict):
......@@ -64,6 +66,7 @@ class PopUpMenu(dict):
super().__init__(*args, **kwargs)
self.loop_until_non_callable = loop_until_non_callable
self.prompt = prompt
self.dontcall = dontcall
def __call__(self, *args, **kwargs):
maxwidth = max(len(k) for k in self.keys())
......@@ -130,7 +133,7 @@ class PopUpMenu(dict):
if return_hit:
result = values[selected_y]
if callable(result):
if callable(result) and not self.dontcall:
result = result(*args, **kwargs)
except BreakOutOfMenu:
......@@ -82,6 +82,12 @@ class TownMenu(PopUpMenu):
def enter_tower(gamestate):
testman = characters.Warrior("Testman")
if len( == 0:
pop_up_notification("You need to assemble a party before you can begin the adventure.")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment