mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-02 11:53:29 -07:00
334 lines
7.4 KiB
Python
334 lines
7.4 KiB
Python
from __future__ import annotations
|
|
|
|
from abc import abstractmethod
|
|
from typing import TYPE_CHECKING, ClassVar
|
|
|
|
from .graphics import Graphic
|
|
from .items import ITEM_TO_GRAPHIC, Item
|
|
from .locations import Location
|
|
|
|
if TYPE_CHECKING:
|
|
from .player import Player
|
|
|
|
|
|
class Entity:
|
|
solid: bool
|
|
graphic: Graphic
|
|
|
|
|
|
class InteractableMixin:
|
|
auto_move_attempt_passing_through = False
|
|
|
|
@abstractmethod
|
|
def interact(self, player: Player) -> bool:
|
|
pass
|
|
|
|
|
|
class ActivatableMixin:
|
|
@abstractmethod
|
|
def activate(self, player: Player) -> None:
|
|
pass
|
|
|
|
|
|
class LocationMixin:
|
|
location: Location
|
|
content: Item | None = None
|
|
remote: bool = False
|
|
has_given_content: bool = False
|
|
|
|
def force_clear(self) -> None:
|
|
if self.has_given_content:
|
|
return
|
|
|
|
self.has_given_content = True
|
|
self.content_success()
|
|
|
|
def give_content(self, player: Player) -> None:
|
|
if self.has_given_content:
|
|
return
|
|
|
|
if self.content is None:
|
|
self.content_failure()
|
|
return
|
|
|
|
if self.remote:
|
|
player.location_cleared(self.location.value)
|
|
else:
|
|
player.receive_item(self.content)
|
|
|
|
self.has_given_content = True
|
|
self.content_success()
|
|
|
|
def content_success(self) -> None:
|
|
pass
|
|
|
|
def content_failure(self) -> None:
|
|
pass
|
|
|
|
|
|
class Empty(Entity):
|
|
solid = False
|
|
graphic = Graphic.EMPTY
|
|
|
|
|
|
class Wall(Entity):
|
|
solid = True
|
|
graphic = Graphic.WALL
|
|
|
|
|
|
class Chest(Entity, InteractableMixin, LocationMixin):
|
|
solid = True
|
|
|
|
is_open: bool = False
|
|
|
|
def __init__(self, location: Location) -> None:
|
|
self.location = location
|
|
|
|
def update_solidity(self) -> None:
|
|
self.solid = not self.has_given_content
|
|
|
|
def open(self) -> None:
|
|
self.is_open = True
|
|
self.update_solidity()
|
|
|
|
def interact(self, player: Player) -> bool:
|
|
if self.has_given_content:
|
|
return False
|
|
|
|
if self.is_open:
|
|
self.give_content(player)
|
|
return True
|
|
|
|
self.open()
|
|
return True
|
|
|
|
def content_success(self) -> None:
|
|
self.update_solidity()
|
|
|
|
def content_failure(self) -> None:
|
|
self.update_solidity()
|
|
|
|
@property
|
|
def graphic(self) -> Graphic:
|
|
if self.has_given_content:
|
|
return Graphic.EMPTY
|
|
if self.is_open:
|
|
if self.content is None:
|
|
return Graphic.EMPTY
|
|
return ITEM_TO_GRAPHIC[self.content]
|
|
return Graphic.CHEST
|
|
|
|
|
|
class Door(Entity):
|
|
solid = True
|
|
|
|
is_open: bool = False
|
|
|
|
closed_graphic: ClassVar[Graphic]
|
|
|
|
def open(self) -> None:
|
|
self.is_open = True
|
|
self.solid = False
|
|
|
|
@property
|
|
def graphic(self) -> Graphic:
|
|
if self.is_open:
|
|
return Graphic.EMPTY
|
|
return self.closed_graphic
|
|
|
|
|
|
class KeyDoor(Door, InteractableMixin):
|
|
auto_move_attempt_passing_through = True
|
|
|
|
closed_graphic = Graphic.KEY_DOOR
|
|
|
|
def interact(self, player: Player) -> bool:
|
|
if self.is_open:
|
|
return False
|
|
|
|
if not player.has_item(Item.KEY):
|
|
return False
|
|
|
|
player.remove_item(Item.KEY)
|
|
|
|
self.open()
|
|
|
|
return True
|
|
|
|
|
|
class BreakableBlock(Door, InteractableMixin):
|
|
auto_move_attempt_passing_through = True
|
|
|
|
closed_graphic = Graphic.BREAKABLE_BLOCK
|
|
|
|
def interact(self, player: Player) -> bool:
|
|
if self.is_open:
|
|
return False
|
|
|
|
if not player.has_item(Item.HAMMER):
|
|
return False
|
|
|
|
player.remove_item(Item.HAMMER)
|
|
|
|
self.open()
|
|
|
|
return True
|
|
|
|
|
|
class Bush(Door, InteractableMixin):
|
|
auto_move_attempt_passing_through = True
|
|
|
|
closed_graphic = Graphic.BUSH
|
|
|
|
def interact(self, player: Player) -> bool:
|
|
if self.is_open:
|
|
return False
|
|
|
|
if not player.has_item(Item.SWORD):
|
|
return False
|
|
|
|
self.open()
|
|
|
|
return True
|
|
|
|
|
|
class Button(Entity, InteractableMixin):
|
|
solid = True
|
|
|
|
activates: ActivatableMixin
|
|
activated = False
|
|
|
|
def __init__(self, activates: ActivatableMixin) -> None:
|
|
self.activates = activates
|
|
|
|
def interact(self, player: Player) -> bool:
|
|
if self.activated:
|
|
return False
|
|
|
|
self.activated = True
|
|
self.activates.activate(player)
|
|
return True
|
|
|
|
@property
|
|
def graphic(self) -> Graphic:
|
|
if self.activated:
|
|
return Graphic.BUTTON_ACTIVATED
|
|
return Graphic.BUTTON_NOT_ACTIVATED
|
|
|
|
|
|
class ButtonDoor(Door, ActivatableMixin):
|
|
closed_graphic = Graphic.BUTTON_DOOR
|
|
|
|
def activate(self, player: Player) -> None:
|
|
self.is_open = True
|
|
self.solid = False
|
|
|
|
|
|
class Enemy(Entity, InteractableMixin):
|
|
solid = True
|
|
|
|
current_health: int
|
|
max_health: int
|
|
|
|
dead: bool = False
|
|
|
|
enemy_graphic_by_health: ClassVar[dict[int, Graphic]] = {
|
|
2: Graphic.NORMAL_ENEMY_2_HEALTH,
|
|
1: Graphic.NORMAL_ENEMY_1_HEALTH,
|
|
}
|
|
enemy_default_graphic = Graphic.NORMAL_ENEMY_1_HEALTH
|
|
|
|
def __init__(self, max_health: int) -> None:
|
|
self.max_health = max_health
|
|
self.respawn()
|
|
|
|
def die(self) -> None:
|
|
self.dead = True
|
|
self.solid = False
|
|
|
|
def respawn(self) -> None:
|
|
self.dead = False
|
|
self.solid = True
|
|
self.heal_if_not_dead()
|
|
|
|
def heal_if_not_dead(self) -> None:
|
|
if self.dead:
|
|
return
|
|
self.current_health = self.max_health
|
|
|
|
def interact(self, player: Player) -> bool:
|
|
if self.dead:
|
|
return False
|
|
|
|
if player.has_item(Item.SWORD):
|
|
self.current_health = max(0, self.current_health - 1)
|
|
|
|
if self.current_health == 0:
|
|
if not self.dead:
|
|
self.die()
|
|
return True
|
|
|
|
player.damage(2)
|
|
return True
|
|
|
|
@property
|
|
def graphic(self) -> Graphic:
|
|
if self.dead:
|
|
return Graphic.EMPTY
|
|
return self.enemy_graphic_by_health.get(self.current_health, self.enemy_default_graphic)
|
|
|
|
|
|
class EnemyWithLoot(Enemy, LocationMixin):
|
|
def __init__(self, max_health: int, location: Location) -> None:
|
|
super().__init__(max_health)
|
|
self.location = location
|
|
|
|
def die(self) -> None:
|
|
self.dead = True
|
|
self.solid = not self.has_given_content
|
|
|
|
def interact(self, player: Player) -> bool:
|
|
if self.dead:
|
|
if not self.has_given_content:
|
|
self.give_content(player)
|
|
return True
|
|
return False
|
|
|
|
super().interact(player)
|
|
return True
|
|
|
|
@property
|
|
def graphic(self) -> Graphic:
|
|
if self.dead and not self.has_given_content:
|
|
if self.content is None:
|
|
return Graphic.EMPTY
|
|
return ITEM_TO_GRAPHIC[self.content]
|
|
return super().graphic
|
|
|
|
def content_success(self) -> None:
|
|
self.die()
|
|
|
|
def content_failure(self) -> None:
|
|
self.die()
|
|
|
|
|
|
class FinalBoss(Enemy):
|
|
enemy_graphic_by_health: ClassVar[dict[int, Graphic]] = {
|
|
5: Graphic.BOSS_5_HEALTH,
|
|
4: Graphic.BOSS_4_HEALTH,
|
|
3: Graphic.BOSS_3_HEALTH,
|
|
2: Graphic.BOSS_2_HEALTH,
|
|
1: Graphic.BOSS_1_HEALTH,
|
|
}
|
|
enemy_default_graphic = Graphic.BOSS_1_HEALTH
|
|
|
|
def interact(self, player: Player) -> bool:
|
|
dead_before = self.dead
|
|
|
|
changed = super().interact(player)
|
|
|
|
if not dead_before and self.dead:
|
|
player.victory()
|
|
|
|
return changed
|