Skip to content

Commit

Permalink
Added has_indicator_bar() to ComponentBase, so components can be repr…
Browse files Browse the repository at this point in the history
…esented using the indicator bar.

Started on adding the framework for the player attacking.
  • Loading branch information
JackAshwell11 committed Mar 19, 2024
1 parent fa7461a commit 3503942
Show file tree
Hide file tree
Showing 16 changed files with 216 additions and 44 deletions.
3 changes: 2 additions & 1 deletion hades_extensions/game_objects/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ class SteeringMovementState(Enum):
Footprint = ...
Target = ...

class ComponentBase: ...
class ComponentBase:
def has_indicator_bar(self: ComponentBase) -> bool: ...

class SystemBase:
def __init__(self: SystemBase, registry: Registry) -> None: ...
Expand Down
2 changes: 1 addition & 1 deletion src/hades/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class GameObjectType(Enum):

# Indicator bar constants
INDICATOR_BAR_BORDER_SIZE: Final[int] = 4
INDICATOR_BAR_DISTANCE: Final[int] = 32
INDICATOR_BAR_DISTANCE: Final[int] = 16
INDICATOR_BAR_HEIGHT: Final[int] = 10
INDICATOR_BAR_WIDTH: Final[int] = 50

Expand Down
18 changes: 17 additions & 1 deletion src/hades/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@

# Custom
from hades.constants import GameObjectType
from hades_extensions.game_objects import SteeringBehaviours, SteeringMovementState
from hades_extensions.game_objects import (
AttackAlgorithm,
SteeringBehaviours,
SteeringMovementState,
)
from hades_extensions.game_objects.components import (
Armour,
Attacks,
EffectApplier,
Footprints,
Health,
Inventory,
KeyboardMovement,
MovementForce,
Expand Down Expand Up @@ -64,7 +71,14 @@ class GameObjectConstructor(NamedTuple):
"Player",
["player_idle.png"],
[
Health(200, 5),
Armour(100, 5),
Inventory(6, 5),
Attacks([
AttackAlgorithm.Ranged,
AttackAlgorithm.Melee,
AttackAlgorithm.AreaOfEffect,
]),
MovementForce(5000, 5),
KeyboardMovement(),
Footprints(),
Expand All @@ -76,6 +90,8 @@ class GameObjectConstructor(NamedTuple):
"Enemy",
["enemy_idle.png"],
[
Health(100, 5),
Armour(50, 5),
MovementForce(1000, 5),
SteeringMovement(
{
Expand Down
6 changes: 3 additions & 3 deletions src/hades/indicator_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ class IndicatorBar:
"""

__slots__ = (
"target_sprite",
"target_component",
"background_box",
"actual_bar",
"background_box",
"offset",
"target_component",
"target_sprite",
)

def __init__(
Expand Down
37 changes: 34 additions & 3 deletions src/hades/sprite.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,29 @@
from typing import TYPE_CHECKING

# Pip
from arcade import Sprite, Texture, load_texture, load_texture_pair
from arcade import (
Sprite,
SpriteSolidColor,
Texture,
color,
load_texture,
load_texture_pair,
)

# Custom
from hades_extensions.game_objects import SPRITE_SCALE, SPRITE_SIZE

if TYPE_CHECKING:
from hades.constructors import GameObjectType
from hades.constants import GameObjectType
from hades_extensions.game_objects import Vec2d

__all__ = ("AnimatedSprite", "BiggerThanError", "HadesSprite", "grid_pos_to_pixel")
__all__ = (
"AnimatedSprite",
"BiggerThanError",
"Bullet",
"HadesSprite",
"grid_pos_to_pixel",
)

# Create the texture path
texture_path = Path(__file__).resolve().parent / "resources" / "textures"
Expand Down Expand Up @@ -53,6 +67,23 @@ def grid_pos_to_pixel(x: int, y: int) -> tuple[float, float]:
)


class Bullet(SpriteSolidColor):
"""Represents a bullet sprite object in the game."""

def __init__(self: Bullet, position: Vec2d) -> None:
"""Initialise the object.
Args:
position: The position of the sprite object in the grid.
"""
super().__init__(
SPRITE_SIZE,
SPRITE_SIZE,
color=color.RED,
)
self.center_x, self.center_y = grid_pos_to_pixel(*position)


class HadesSprite(Sprite):
"""Represents a sprite object in the game.
Expand Down
112 changes: 85 additions & 27 deletions src/hades/views/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
import logging
import math
import random
from typing import TYPE_CHECKING

# Pip
from arcade import (
MOUSE_BUTTON_LEFT,
Camera,
PymunkPhysicsEngine,
SpriteList,
SpriteSolidColor,
Text,
View,
color,
Expand All @@ -35,7 +35,8 @@
GameObjectType,
)
from hades.constructors import ENEMY, FLOOR, PLAYER, POTION, WALL, GameObjectConstructor
from hades.sprite import AnimatedSprite, HadesSprite, grid_pos_to_pixel
from hades.indicator_bar import IndicatorBar
from hades.sprite import AnimatedSprite, Bullet, HadesSprite, grid_pos_to_pixel
from hades_extensions.game_objects import SPRITE_SIZE, Registry, Vec2d
from hades_extensions.game_objects.components import KeyboardMovement, SteeringMovement
from hades_extensions.game_objects.systems import (
Expand All @@ -47,9 +48,6 @@
)
from hades_extensions.generation import TileType, create_map

if TYPE_CHECKING:
from hades.indicator_bar import IndicatorBar

__all__ = ("Game",)

# Get the logger
Expand Down Expand Up @@ -128,6 +126,20 @@ def _create_sprite(
max_velocity=MAX_VELOCITY,
collision_type=constructor.game_object_type.name,
)

# Add all the indicator bars to the game
indicator_bar_offset = 0
for component in constructor.components:
if component.has_indicator_bar():
self.indicator_bars.append(
IndicatorBar(
sprite,
component,
self.indicator_bar_sprites,
indicator_bar_offset,
),
)
indicator_bar_offset += 1
return sprite

def __init__(self: Game, level: int) -> None:
Expand All @@ -144,6 +156,9 @@ def __init__(self: Game, level: int) -> None:
self.tile_sprites: SpriteList[HadesSprite] = SpriteList[HadesSprite]()
self.entity_sprites: SpriteList[HadesSprite] = SpriteList[HadesSprite]()
self.item_sprites: SpriteList[HadesSprite] = SpriteList[HadesSprite]()
self.indicator_bar_sprites: SpriteList[SpriteSolidColor] = SpriteList[
SpriteSolidColor
]()
self.nearest_item: list[HadesSprite] = []
self.player_status_text: Text = Text(
"Money: 0",
Expand Down Expand Up @@ -218,6 +233,7 @@ def on_draw(self: Game) -> None:
self.tile_sprites.draw(pixelated=True) # type: ignore[no-untyped-call]
self.item_sprites.draw(pixelated=True) # type: ignore[no-untyped-call]
self.entity_sprites.draw(pixelated=True) # type: ignore[no-untyped-call]
self.indicator_bar_sprites.draw() # type: ignore[no-untyped-call]

# Draw the gui on the screen
self.gui_camera.use()
Expand Down Expand Up @@ -300,26 +316,6 @@ def on_key_press(self: Game, symbol: int, modifiers: int) -> None:
player_movement.moving_west = True
case key.D:
player_movement.moving_east = True
case key.C:
if (
self.nearest_item
and self.nearest_item[0].game_object_type in COLLECTIBLE_TYPES
and self.registry.get_system(InventorySystem).add_item_to_inventory(
self.ids[GameObjectType.PLAYER][0].game_object_id,
self.nearest_item[0].game_object_id,
)
):
self.nearest_item[0].remove_from_sprite_lists()
case key.E:
if (
self.nearest_item
and self.nearest_item[0].game_object_type in USABLE_TYPES
and self.registry.get_system(EffectSystem).apply_effects(
self.nearest_item[0].game_object_id,
self.ids[GameObjectType.PLAYER][0].game_object_id,
)
):
self.nearest_item[0].remove_from_sprite_lists()

def on_key_release(self: Game, symbol: int, modifiers: int) -> None:
"""Process key release functionality.
Expand Down Expand Up @@ -347,6 +343,34 @@ def on_key_release(self: Game, symbol: int, modifiers: int) -> None:
player_movement.moving_west = False
case key.D:
player_movement.moving_east = False
case key.C:
if (
self.nearest_item
and self.nearest_item[0].game_object_type in COLLECTIBLE_TYPES
and self.registry.get_system(InventorySystem).add_item_to_inventory(
self.ids[GameObjectType.PLAYER][0].game_object_id,
self.nearest_item[0].game_object_id,
)
):
self.nearest_item[0].remove_from_sprite_lists()
case key.E:
if (
self.nearest_item
and self.nearest_item[0].game_object_type in USABLE_TYPES
and self.registry.get_system(EffectSystem).apply_effects(
self.nearest_item[0].game_object_id,
self.ids[GameObjectType.PLAYER][0].game_object_id,
)
):
self.nearest_item[0].remove_from_sprite_lists()
case key.Z:
self.registry.get_system(AttackSystem).previous_attack(
self.ids[GameObjectType.PLAYER][0].game_object_id,
)
case key.X:
self.registry.get_system(AttackSystem).next_attack(
self.ids[GameObjectType.PLAYER][0].game_object_id,
)

def on_mouse_press(self: Game, x: int, y: int, button: int, modifiers: int) -> None:
"""Process mouse button functionality.
Expand All @@ -366,13 +390,47 @@ def on_mouse_press(self: Game, x: int, y: int, button: int, modifiers: int) -> N
modifiers,
)
if button is MOUSE_BUTTON_LEFT:
self.registry.get_system(AttackSystem).do_attack(
if bullet_details := self.registry.get_system(AttackSystem).do_attack(
self.ids[GameObjectType.PLAYER][0].game_object_id,
[
game_object.game_object_id
for game_object in self.ids[GameObjectType.ENEMY]
],
)
):
bullet = Bullet(bullet_details[0])
self.indicator_bar_sprites.append(bullet)
self.physics_engine.add_sprite(
bullet,
moment_of_inertia=self.physics_engine.MOMENT_INF,
body_type=self.physics_engine.KINEMATIC,
collision_type="bullet",
)
self.physics_engine.set_velocity(
bullet,
(bullet_details[1], bullet_details[2]),
)

def on_mouse_motion(self: Game, x: int, y: int, *_: tuple[int, int]) -> None:
"""Process mouse motion functionality.
Args:
x: The x position of the mouse.
y: The y position of the mouse.
"""
# Calculate the new angle in degrees
camera_x, camera_y = self.game_camera.position
kinematic_object = self.registry.get_kinematic_object(
self.ids[GameObjectType.PLAYER][0].game_object_id,
)
angle = math.degrees(
math.atan2(
x - kinematic_object.position.x + camera_x,
y - kinematic_object.position.y + camera_y,
),
)
if angle < 0:
angle += 360
kinematic_object.rotation = angle

def generate_enemy(self: Game, _: float = 1 / 60) -> None:
"""Generate an enemy outside the player's fov."""
Expand Down
5 changes: 5 additions & 0 deletions src/hades_extensions/include/game_objects/registry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ struct ComponentBase {

/// The move constructor.
ComponentBase(ComponentBase &&) = default;

/// Checks if the component can have an indicator bar or not.
///
/// @return Whether the component can have an indicator bar or not.
[[nodiscard]] virtual auto has_indicator_bar() const -> bool { return false; }
};

/// The base class for all systems.
Expand Down
10 changes: 10 additions & 0 deletions src/hades_extensions/include/game_objects/stats.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ struct Armour final : Stat {
/// @param value - The initial and maximum value of the armour stat.
/// @param maximum_level - The maximum level of the armour stat.
Armour(const double value, const int maximum_level) : Stat(value, maximum_level) {}

/// Checks if the component can have an indicator bar or not.
///
/// @return Whether the component can have an indicator bar or not.
[[nodiscard]] bool has_indicator_bar() const override { return true; }
};

/// Allows a game object to regenerate armour.
Expand All @@ -89,6 +94,11 @@ struct Health final : Stat {
/// @param value - The initial and maximum value of the health stat.
/// @param maximum_level - The maximum level of the health stat.
Health(const double value, const int maximum_level) : Stat(value, maximum_level) {}

/// Checks if the component can have an indicator bar or not.
///
/// @return Whether the component can have an indicator bar or not.
[[nodiscard]] bool has_indicator_bar() const override { return true; }
};

/// Allows a game object to determine how fast it can move.
Expand Down
8 changes: 6 additions & 2 deletions src/hades_extensions/src/binding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,12 @@ PYBIND11_MODULE(hades_extensions, module) { // NOLINT
// Add the global constants and the base classes
game_objects.attr("SPRITE_SCALE") = SPRITE_SCALE;
game_objects.attr("SPRITE_SIZE") = SPRITE_SIZE;
const pybind11::class_<ComponentBase, std::shared_ptr<ComponentBase>> component_base(
game_objects, "ComponentBase", "The base class for all components.");
pybind11::class_<ComponentBase, std::shared_ptr<ComponentBase>> component_base(game_objects, "ComponentBase",
"The base class for all components.");
component_base.def("has_indicator_bar", &ComponentBase::has_indicator_bar,
"Checks if the component can have an indicator bar or not.\n\n"
"Returns:\n"
" Whether the component can have an indicator bar or not.");
pybind11::class_<SystemBase, std::shared_ptr<SystemBase>>(game_objects, "SystemBase",
"The base class for all systems.")
.def(pybind11::init<Registry *>(), pybind11::arg("registry"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ class ArmourRegenSystemFixture : public testing::Test {
};

// ----- TESTS ----------------------------------
/// Test that the required components return the correct value for has_indicator_bar.
TEST(Tests, TestArmourRegenSystemComponentsHasIndicatorBar) {
ASSERT_TRUE(Armour(-1, -1).has_indicator_bar());
ASSERT_FALSE(ArmourRegen(-1, -1).has_indicator_bar());
}

/// Test that the armour regen component is updated correctly when armour is full.
TEST_F(ArmourRegenSystemFixture, TestArmourRegenSystemUpdateFullArmour) {
get_armour_regen_system()->update(5);
Expand Down
Loading

0 comments on commit 3503942

Please sign in to comment.