squirrel-battle/squirrelbattle/entities/player.py

193 lines
8.2 KiB
Python
Raw Normal View History

2020-11-27 15:33:17 +00:00
# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse
# SPDX-License-Identifier: GPL-3.0-or-later
from functools import reduce
from queue import PriorityQueue
2020-11-10 23:50:47 +00:00
from random import randint
2020-12-18 16:30:03 +00:00
from typing import Dict, Optional, Tuple
2020-11-10 23:50:47 +00:00
2020-12-18 16:30:03 +00:00
from .items import Item
from ..interfaces import FightingEntity, InventoryHolder
2020-11-06 14:33:26 +00:00
class Player(InventoryHolder, FightingEntity):
"""
The class of the player
"""
2020-11-06 20:23:17 +00:00
current_xp: int = 0
max_xp: int = 10
xp_buff: float = 1
paths: Dict[Tuple[int, int], Tuple[int, int]]
equipped_main: Optional[Item]
equipped_secondary: Optional[Item]
equipped_helmet: Optional[Item]
2020-12-18 16:30:03 +00:00
equipped_armor: Optional[Item]
2020-11-06 17:12:17 +00:00
def __init__(self, name: str = "player", maxhealth: int = 20,
strength: int = 5, intelligence: int = 1, charisma: int = 1,
dexterity: int = 1, constitution: int = 1, level: int = 1,
current_xp: int = 0, max_xp: int = 10, inventory: list = None,
hazel: int = 42, equipped_main: Optional[Item] = None,
equipped_armor: Optional[Item] = None, critical: int = 5,\
equipped_secondary: Optional[Item] = None, \
equipped_helmet: Optional[Item] = None, xp_buff: float = 1,\
*args, **kwargs) -> None:
super().__init__(name=name, maxhealth=maxhealth, strength=strength,
2020-11-18 23:10:37 +00:00
intelligence=intelligence, charisma=charisma,
dexterity=dexterity, constitution=constitution,
level=level, critical=critical, *args, **kwargs)
2020-11-18 23:10:37 +00:00
self.current_xp = current_xp
self.max_xp = max_xp
self.xp_buff = xp_buff
self.inventory = self.translate_inventory(inventory or [])
self.paths = dict()
self.hazel = hazel
if isinstance(equipped_main, dict):
equipped_main = self.dict_to_item(equipped_main)
2020-12-18 16:30:03 +00:00
if isinstance(equipped_armor, dict):
equipped_armor = self.dict_to_item(equipped_armor)
if isinstance(equipped_secondary, dict):
equipped_secondary = self.dict_to_item(equipped_secondary)
if isinstance(equipped_helmet, dict):
equipped_helmet = self.dict_to_item(equipped_helmet)
self.equipped_main = equipped_main
2020-12-18 16:30:03 +00:00
self.equipped_armor = equipped_armor
self.equipped_secondary = equipped_secondary
self.equipped_helmet = equipped_helmet
2020-11-10 21:02:41 +00:00
def move(self, y: int, x: int) -> None:
"""
Moves the view of the map (the point on which the camera is centered)
according to the moves of the player.
2020-11-10 21:02:41 +00:00
"""
super().move(y, x)
self.map.currenty = y
self.map.currentx = x
self.recalculate_paths()
2020-11-10 21:02:41 +00:00
2020-11-06 20:15:09 +00:00
def level_up(self) -> None:
"""
Add levels to the player as much as it is possible.
"""
2020-11-06 20:23:17 +00:00
while self.current_xp > self.max_xp:
2020-11-06 20:15:09 +00:00
self.level += 1
2020-11-06 20:23:17 +00:00
self.current_xp -= self.max_xp
2020-11-06 20:15:09 +00:00
self.max_xp = self.level * 10
2020-11-10 23:50:47 +00:00
self.health = self.maxhealth
self.strength = self.strength + 1
# TODO Remove it, that's only fun
2020-11-11 15:23:27 +00:00
self.map.spawn_random_entities(randint(3 * self.level,
10 * self.level))
2020-11-06 20:15:09 +00:00
def add_xp(self, xp: int) -> None:
"""
Add some experience to the player.
If the required amount is reached, level up.
"""
self.current_xp += int(xp*self.xp_buff)
2020-11-06 17:12:17 +00:00
self.level_up()
2020-11-10 23:50:47 +00:00
def remove_from_inventory(self, obj: Item) -> None:
"""
Remove the given item from the inventory, even if the item is equipped.
"""
if obj == self.equipped_main:
self.equipped_main = None
elif obj == self.equipped_armor:
self.equipped_armor = None
elif obj == self.equipped_secondary:
self.equipped_secondary = None
elif obj == self.equipped_helmet:
self.equipped_helmet = None
else:
return super().remove_from_inventory(obj)
2020-11-11 15:47:19 +00:00
# noinspection PyTypeChecker,PyUnresolvedReferences
def check_move(self, y: int, x: int, move_if_possible: bool = False) \
-> bool:
2020-11-10 23:50:47 +00:00
"""
If the player tries to move but a fighting entity is there,
the player fights this entity.
If the entity dies, the player is rewarded with some XP
2020-11-10 23:50:47 +00:00
"""
2020-11-11 00:17:00 +00:00
# Don't move if we are dead
if self.dead:
return False
2020-11-10 23:50:47 +00:00
for entity in self.map.entities:
2020-11-11 15:47:19 +00:00
if entity.y == y and entity.x == x:
if entity.is_fighting_entity():
self.map.logs.add_message(self.hit(entity))
2020-11-11 15:47:19 +00:00
if entity.dead:
self.add_xp(randint(3, 7))
return True
elif entity.is_item():
entity.hold(self)
return super().check_move(y, x, move_if_possible)
def recalculate_paths(self, max_distance: int = 8) -> None:
"""
Use Dijkstra algorithm to calculate best paths for monsters to go to
the player. Actually, the paths are computed for each tile adjacent to
the player then for each step the monsters use the best path avaliable.
"""
distances = []
predecessors = []
# four Dijkstras, one for each adjacent tile
for dir_y, dir_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
queue = PriorityQueue()
new_y, new_x = self.y + dir_y, self.x + dir_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
queue.put(((1, 0), (new_y, new_x)))
visited = [(self.y, self.x)]
distances.append({(self.y, self.x): (0, 0), (new_y, new_x): (1, 0)})
predecessors.append({(new_y, new_x): (self.y, self.x)})
while not queue.empty():
dist, (y, x) = queue.get()
if dist[0] >= max_distance or (y, x) in visited:
continue
visited.append((y, x))
for diff_y, diff_x in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
new_y, new_x = y + diff_y, x + diff_x
if not 0 <= new_y < self.map.height or \
not 0 <= new_x < self.map.width or \
not self.map.tiles[new_y][new_x].can_walk():
continue
new_distance = (dist[0] + 1,
dist[1] + (not self.map.is_free(y, x)))
if not (new_y, new_x) in distances[-1] or \
distances[-1][(new_y, new_x)] > new_distance:
predecessors[-1][(new_y, new_x)] = (y, x)
distances[-1][(new_y, new_x)] = new_distance
queue.put((new_distance, (new_y, new_x)))
# For each tile that is reached by at least one Dijkstra, sort the
# different paths by distance to the player. For the technical bits :
# The reduce function is a fold starting on the first element of the
# iterable, and we associate the points to their distance, sort
# along the distance, then only keep the points.
self.paths = {}
for y, x in reduce(set.union,
[set(p.keys()) for p in predecessors], set()):
self.paths[(y, x)] = [p for d, p in sorted(
[(distances[i][(y, x)], predecessors[i][(y, x)])
for i in range(len(distances)) if (y, x) in predecessors[i]])]
2020-11-18 23:10:37 +00:00
def save_state(self) -> dict:
"""
Saves the state of the entity into a dictionary
"""
d = super().save_state()
d["current_xp"] = self.current_xp
d["max_xp"] = self.max_xp
d["equipped_main"] = self.equipped_main.save_state()\
if self.equipped_main else None
2020-12-18 16:50:26 +00:00
d["equipped_armor"] = self.equipped_armor.save_state()\
if self.equipped_armor else None
d["equipped_secondary"] = self.equipped_secondary.save_state()\
if self.equipped_secondary else None
d["equipped_helmet"] = self.equipped_helmet.save_state()\
if self.equipped_helmet else None
return d