WIP part 6
This commit is contained in:
parent
e1231a58e6
commit
f89c2bbdb8
@ -4,12 +4,12 @@
|
||||
|
||||
* "Fun value", inspired by `Undertale`, activates different secrets in different runs.
|
||||
* Color-based effects or biomes that only have an impact when in the FOV.
|
||||
* gobbos, da red gobbo
|
||||
* Gobbos, da red gobbo.
|
||||
|
||||
Stepwise needs a genre/setting. Some options are:
|
||||
|
||||
* Fantasy
|
||||
* SciFi
|
||||
* Sci-fi
|
||||
* Nuclear Bunker
|
||||
* Indiana Jones
|
||||
* Post-Apocalypse
|
||||
|
@ -1,13 +1,11 @@
|
||||
from entity import Entity
|
||||
|
||||
class Action:
|
||||
def __init__(self, entity: Entity):
|
||||
class BaseAction:
|
||||
def __init__(self, entity):
|
||||
self.entity = entity
|
||||
|
||||
def apply(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
class QuitAction(Action):
|
||||
class QuitAction(BaseAction):
|
||||
def __init__(self): #override the base __init__
|
||||
pass
|
||||
|
||||
@ -15,8 +13,13 @@ class QuitAction(Action):
|
||||
raise SystemExit()
|
||||
|
||||
|
||||
class DirectionAction(Action):
|
||||
def __init__(self, entity: Entity, xdir: int, ydir: int):
|
||||
class WaitAction(BaseAction):
|
||||
def apply(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class DirectionAction(BaseAction):
|
||||
def __init__(self, entity, xdir: int, ydir: int):
|
||||
super().__init__(entity)
|
||||
self.xdir = xdir
|
||||
self.ydir = ydir
|
||||
@ -52,7 +55,7 @@ class MeleeAction(DirectionAction):
|
||||
|
||||
print(f"You kicked the {target.name}, which was funny")
|
||||
|
||||
class MoveAction(DirectionAction):
|
||||
class BumpAction(DirectionAction): #bad name, deal with it
|
||||
def apply(self) -> None:
|
||||
dest_x = self.entity.x + self.xdir
|
||||
dest_y = self.entity.y + self.ydir
|
||||
|
64
source/components/base_ai.py
Normal file
64
source/components/base_ai.py
Normal file
@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
import tcod
|
||||
|
||||
from components.base_component import BaseComponent
|
||||
from actions import BaseAction, MeleeAction, MovementAction, WaitAction
|
||||
|
||||
|
||||
class BaseAI(BaseAction, BaseComponent):
|
||||
entity: Any
|
||||
|
||||
def apply(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]:
|
||||
#copy the walkables
|
||||
cost = np.array(self.entity.floor_map.tiles["walkable"], dtype=np.int8)
|
||||
|
||||
#higher numbers deter path-finding this way
|
||||
for entity in self.entity.floor_map.entities:
|
||||
if not entity.walkable and cost[entity.x, entity.y]:
|
||||
cost[entity.x, entity.y] += 10
|
||||
|
||||
graph = tcod.path.SimpleGraph(cost=cost, cardinal=2, diagonal=3)
|
||||
pathfinder = tcod.path.Pathfinder(graph)
|
||||
|
||||
pathfinder.add_root((self.entity.x, self.entity.y)) #start pos
|
||||
|
||||
#make the path, omitting the start pos
|
||||
path: List[List[int]] = pathfinder.path_to((dest_x, dest_y))[1:].tolist()
|
||||
|
||||
#return the path, after mapping it to a list of tuples
|
||||
return [(index[0], index[1]) for index in path]
|
||||
|
||||
class AttackOnSight(BaseAI):
|
||||
def __init__(self, entity: Actor):
|
||||
super().__init__(entity)
|
||||
self.path: List[Tuple[int, int]] = []
|
||||
|
||||
def apply(self) -> None:
|
||||
target = self.entity.floor_map.player
|
||||
xdir = target.x - self.entity.x
|
||||
ydir = target.y - self.entity.y
|
||||
distance = max(abs(xdir), abs(ydir))
|
||||
|
||||
#if the player can see me, and I'm close enough, attack
|
||||
if self.entity.floor_map.visible[self.entity.x, self.entity.y]:
|
||||
if distance <= 1:
|
||||
return MeleeAction(self.entity, xdir, ydir).apply()
|
||||
|
||||
self.path = self.get_path_to(target.x, target.y)
|
||||
|
||||
if self.path:
|
||||
dest_x, dest_y = self.path.pop(0)
|
||||
return MovementAction(
|
||||
entity = self.entity,
|
||||
xdir = dest_x - self.entity.x,
|
||||
ydir = dest_y - self.entity.y,
|
||||
).apply()
|
||||
|
||||
return WaitAction(self.entity).apply()
|
5
source/components/base_component.py
Normal file
5
source/components/base_component.py
Normal file
@ -0,0 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
class BaseComponent:
|
||||
entity: Any
|
||||
|
23
source/components/fighter.py
Normal file
23
source/components/fighter.py
Normal file
@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from components.base_component import BaseComponent
|
||||
|
||||
class Fighter(BaseComponent):
|
||||
entity: Any
|
||||
|
||||
def __init__(self, hp: int, attack: int, defense: int):
|
||||
self._maximum_hp = hp
|
||||
self._current_hp = hp
|
||||
self.attack = attack
|
||||
self.defense = defense
|
||||
|
||||
@property
|
||||
def current_hp(self) -> int:
|
||||
return self._current_hp
|
||||
|
||||
@current_hp.setter
|
||||
def current_hp(self, value: int) -> None:
|
||||
self._current_hp = max(0, min(value, self._maximum_hp))
|
||||
|
@ -21,9 +21,10 @@ class Engine:
|
||||
def handle_entities(self) -> None:
|
||||
self.update_fov() #knowing the FOV lets entities mess with it
|
||||
|
||||
#all entities in the level
|
||||
for entity in self.floor_map.entities - {self.player}:
|
||||
pass #TODO: run entity AI
|
||||
#all *actors* in the level
|
||||
for actor in set(self.floor_map.actors) - {self.player}:
|
||||
if actor.ai:
|
||||
actor.ai.apply()
|
||||
|
||||
def handle_rendering(self, context: Context, console: Console) -> None:
|
||||
#map and all entities within
|
||||
|
@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import Tuple
|
||||
from typing import Optional, Tuple, Type
|
||||
|
||||
from components.base_ai import BaseAI
|
||||
from components.fighter import Fighter
|
||||
|
||||
class Entity:
|
||||
def __init__(
|
||||
@ -33,3 +36,37 @@ class Entity:
|
||||
def set_pos(self, x: int, y: int) -> None:
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
|
||||
#actors are entities that can act on their own
|
||||
class Actor(Entity):
|
||||
def __init__( #yep, this is ugly
|
||||
self,
|
||||
x: int = 0,
|
||||
y: int = 0,
|
||||
char: str = "?",
|
||||
color: Tuple[int, int, int] = (255, 255, 255),
|
||||
name: str = "<Unnamed>",
|
||||
walkable: bool = True,
|
||||
floor_map = None,
|
||||
|
||||
#actor-specific stuff
|
||||
ai_class: Type[BaseAI] = None,
|
||||
fighter: Fighter = None,
|
||||
):
|
||||
super().__init__(
|
||||
x = x,
|
||||
y = y,
|
||||
char = char,
|
||||
color = color,
|
||||
name = name,
|
||||
walkable = walkable,
|
||||
floor_map = floor_map,
|
||||
)
|
||||
|
||||
self.ai: Optional[BaseAI] = ai_class(self)
|
||||
self.fighter = fighter
|
||||
self.fighter.entity = self
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
return bool(self.ai)
|
||||
|
@ -1,8 +1,31 @@
|
||||
from entity import Entity
|
||||
from entity import Entity, Actor
|
||||
from components.base_ai import BaseAI, AttackOnSight
|
||||
from components.fighter import Fighter
|
||||
|
||||
player = Entity(char="@", color=(255, 255, 255), name="Player", walkable=False)
|
||||
player = Actor(
|
||||
char = "@",
|
||||
color = (255, 255, 255),
|
||||
name = "Player",
|
||||
walkable = False,
|
||||
ai_class = BaseAI,
|
||||
fighter = Fighter(hp = 10, attack = 2, defense = 2),
|
||||
)
|
||||
|
||||
#gobbos
|
||||
gobbo = Entity(char="g", color=(30, 168, 41), name="Gobbo", walkable=False)
|
||||
gobbo_red = Entity(char="g", color=(168, 41, 30), name="Red Gobbo", walkable=False)
|
||||
gobbo = Actor(
|
||||
char = "g",
|
||||
color = (30, 168, 41),
|
||||
name = "Gobbo",
|
||||
walkable = False,
|
||||
ai_class = AttackOnSight,
|
||||
fighter = Fighter(hp = 5, attack = 2, defense = 0),
|
||||
)
|
||||
|
||||
gobbo_red = Actor(
|
||||
char = "g",
|
||||
color = (168, 41, 30),
|
||||
name = "Red Gobbo",
|
||||
walkable = False,
|
||||
ai_class = AttackOnSight,
|
||||
fighter = Fighter(hp = 5, attack = 2, defense = 1),
|
||||
)
|
||||
|
@ -2,11 +2,11 @@ from typing import Optional
|
||||
|
||||
import tcod
|
||||
|
||||
from actions import Action, QuitAction, MoveAction
|
||||
from actions import BaseAction, QuitAction, BumpAction
|
||||
from engine import Engine
|
||||
|
||||
#event handler is one part of the engine
|
||||
class EventHandler(tcod.event.EventDispatch[Action]):
|
||||
class EventHandler(tcod.event.EventDispatch[BaseAction]):
|
||||
def __init__(self, engine: Engine):
|
||||
super().__init__()
|
||||
self.engine = engine
|
||||
@ -21,10 +21,10 @@ class EventHandler(tcod.event.EventDispatch[Action]):
|
||||
action.apply() #entity references the engine
|
||||
|
||||
#callbacks
|
||||
def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
|
||||
def ev_quit(self, event: tcod.event.Quit) -> Optional[BaseAction]:
|
||||
return QuitAction()
|
||||
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
|
||||
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[BaseAction]:
|
||||
key = event.sym #SDL stuff, neat.
|
||||
|
||||
player = self.engine.player
|
||||
@ -35,13 +35,13 @@ class EventHandler(tcod.event.EventDispatch[Action]):
|
||||
return QuitAction()
|
||||
|
||||
case tcod.event.KeySym.UP:
|
||||
return MoveAction(player, xdir = 0, ydir = -1)
|
||||
return BumpAction(player, xdir = 0, ydir = -1)
|
||||
case tcod.event.KeySym.DOWN:
|
||||
return MoveAction(player, xdir = 0, ydir = 1)
|
||||
return BumpAction(player, xdir = 0, ydir = 1)
|
||||
case tcod.event.KeySym.LEFT:
|
||||
return MoveAction(player, xdir = -1, ydir = 0)
|
||||
return BumpAction(player, xdir = -1, ydir = 0)
|
||||
case tcod.event.KeySym.RIGHT:
|
||||
return MoveAction(player, xdir = 1, ydir = 0)
|
||||
return BumpAction(player, xdir = 1, ydir = 0)
|
||||
|
||||
case _:
|
||||
return None
|
@ -1,11 +1,11 @@
|
||||
from __future__ import annotations
|
||||
from typing import Iterable, Optional
|
||||
from typing import Iterable, Iterator, Optional
|
||||
|
||||
import numpy as np
|
||||
from tcod.console import Console
|
||||
|
||||
import tile_types
|
||||
from entity import Entity
|
||||
from entity import Entity, Actor
|
||||
|
||||
class FloorMap:
|
||||
def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()):
|
||||
@ -24,6 +24,14 @@ class FloorMap:
|
||||
self.engine = None
|
||||
self.player = None
|
||||
|
||||
@property
|
||||
def actors(self) -> Iterator[Actor]:
|
||||
yield from (
|
||||
entity
|
||||
for entity in self.entities
|
||||
if isinstance(entity, Actor) and entity.is_alive()
|
||||
)
|
||||
|
||||
def in_bounds(self, x: int, y: int) -> bool:
|
||||
return 0 <= x < self.width and 0 <= y < self.height
|
||||
|
||||
@ -38,6 +46,13 @@ class FloorMap:
|
||||
|
||||
return None
|
||||
|
||||
def get_actor_at(self, x: int, y: int) -> Optional[Actor]:
|
||||
for actor in self.actors:
|
||||
if actor.x == x and actor.y == y:
|
||||
return actor
|
||||
|
||||
return None
|
||||
|
||||
def render(self, console: Console) -> None:
|
||||
console.rgb[0:self.width, 0:self.height] = np.select(
|
||||
condlist = [self.visible, self.explored],
|
||||
|
Loading…
x
Reference in New Issue
Block a user