diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 354d136..6dce473 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -31,7 +31,7 @@ jobs:
uses: Silleellie/pylint-github-action@v2
with:
# The path, relative to the root of the repo, of the package(s) or pyton file(s) to lint
- lint-path: danmaku
+ lint-path: danmaku/**/*.py
# Python version which will install all dependencies and lint package(s)
python-version: "3.10"
# The path, relative to the root of the repo, of the requirements to install
diff --git a/README.md b/README.md
index baaf0d7..0f8fa17 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,31 @@
# 弾幕 (Danmaku)
-
+
[](https://github.com/psf/black)
## Goal
To create a bullet hell game similar to TouHou Project, Undertale, etc.
+## How to build
+
+```
+poetry shell
+poetry install
+```
+or
+```
+python3 -m venv .venv
+.\.venv\Scripts\activate
+pip install --upgrade -r .\requirements.txt
+```
+Then,
+```
+.\build
+```
+
+## For developers
+
+Most important things is written down [here](./docs/mecs.md)
+
## Refactoring
- [x] main.py
- [x] bullet.py
@@ -18,7 +39,7 @@ To create a bullet hell game similar to TouHou Project, Undertale, etc.
- [x] Game mechanics
- [x] Drops
- - [ ] HP (?)
+ - [ ] HP (?)
- [x] XP
- [x] Powerups
- [ ] Coins (?)
@@ -38,13 +59,13 @@ To create a bullet hell game similar to TouHou Project, Undertale, etc.
- [ ] Effects (particles)
- [ ] Scaling
- [ ] Fullscreen
- - [ ] Boss HP bar
+ - [x] Boss HP bar
- [x] Player HP/Bomb info
- [x] Player points info
- - [ ] UI
+ - [x] UI
- [x] Main menu
- [x] Main menu style
- - [ ] Background
+ - [x] Background
- [x] Leaderboard
- [x] Settings
- [x] DB
@@ -66,6 +87,6 @@ To create a bullet hell game similar to TouHou Project, Undertale, etc.
- [x] Replace resource path strings with constants from db
- [x] Sounds
- [x] Death
- - [ ] Shoot
- - [ ] Hit
+ - [x] Shoot
+ - [x] Hit
- [x] Music
diff --git a/assets/DataBase.db b/assets/DataBase.db
index 8d336d6..1751fa6 100644
Binary files a/assets/DataBase.db and b/assets/DataBase.db differ
diff --git a/assets/sounds/game.wav b/assets/sounds/game.wav
new file mode 100644
index 0000000..2a0ae46
Binary files /dev/null and b/assets/sounds/game.wav differ
diff --git a/assets/sounds/hit.wav b/assets/sounds/hit.wav
new file mode 100644
index 0000000..357b8e0
Binary files /dev/null and b/assets/sounds/hit.wav differ
diff --git a/assets/sounds/lose.wav b/assets/sounds/lose.wav
new file mode 100644
index 0000000..0674173
Binary files /dev/null and b/assets/sounds/lose.wav differ
diff --git a/assets/sounds/menu.wav b/assets/sounds/menu.wav
new file mode 100644
index 0000000..5df96c0
Binary files /dev/null and b/assets/sounds/menu.wav differ
diff --git a/assets/sounds/shoot.wav b/assets/sounds/shoot.wav
new file mode 100644
index 0000000..c08b642
Binary files /dev/null and b/assets/sounds/shoot.wav differ
diff --git a/assets/sounds/win.wav b/assets/sounds/win.wav
new file mode 100644
index 0000000..b62f38c
Binary files /dev/null and b/assets/sounds/win.wav differ
diff --git a/assets/textures/basic_enemy.png b/assets/textures/basic_enemy.png
deleted file mode 100644
index 7e5b205..0000000
Binary files a/assets/textures/basic_enemy.png and /dev/null differ
diff --git a/assets/textures/enemy/basic_enemy_1.png b/assets/textures/enemy/basic_enemy_down_1.png
similarity index 100%
rename from assets/textures/enemy/basic_enemy_1.png
rename to assets/textures/enemy/basic_enemy_down_1.png
diff --git a/assets/textures/enemy/basic_enemy_3.png b/assets/textures/enemy/basic_enemy_down_3.png
similarity index 100%
rename from assets/textures/enemy/basic_enemy_3.png
rename to assets/textures/enemy/basic_enemy_down_3.png
diff --git a/assets/textures/enemy/basic_enemy_2.png b/assets/textures/enemy/basic_enemy_down_static_2.png
similarity index 100%
rename from assets/textures/enemy/basic_enemy_2.png
rename to assets/textures/enemy/basic_enemy_down_static_2.png
diff --git a/assets/textures/enemy/strong_enemy_1.png b/assets/textures/enemy/strong_enemy_down_1.png
similarity index 100%
rename from assets/textures/enemy/strong_enemy_1.png
rename to assets/textures/enemy/strong_enemy_down_1.png
diff --git a/assets/textures/enemy/strong_enemy_3.png b/assets/textures/enemy/strong_enemy_down_3.png
similarity index 100%
rename from assets/textures/enemy/strong_enemy_3.png
rename to assets/textures/enemy/strong_enemy_down_3.png
diff --git a/assets/textures/enemy/strong_enemy_2.png b/assets/textures/enemy/strong_enemy_down_static_2.png
similarity index 100%
rename from assets/textures/enemy/strong_enemy_2.png
rename to assets/textures/enemy/strong_enemy_down_static_2.png
diff --git a/assets/textures/menu.png b/assets/textures/menu.png
new file mode 100644
index 0000000..1798e68
Binary files /dev/null and b/assets/textures/menu.png differ
diff --git a/assets/textures/player.png b/assets/textures/player.png
deleted file mode 100644
index 7addc68..0000000
Binary files a/assets/textures/player.png and /dev/null differ
diff --git a/assets/textures/strong_enemy.png b/assets/textures/strong_enemy.png
deleted file mode 100644
index cc6c0e7..0000000
Binary files a/assets/textures/strong_enemy.png and /dev/null differ
diff --git a/danmaku/animated.py b/danmaku/animated.py
deleted file mode 100644
index 1d2182d..0000000
--- a/danmaku/animated.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""Declaration of Animated class"""
-
-import pygame
-
-from danmaku.gameobject import GameObject
-
-
-class Animated(GameObject):
- """Base class for animated objects
-
- Args:
- xy (tuple[int | float, int | float]): Position of the object.
- width_height (tuple[int | float, int | float]): Width and height of the object.
- speed (int | float): Speed of the object.
- frames (list[str]): List of frames
- freq (int | float): Frequency of animation
- period (int | float | None, optional): Period of animation. Defaults to None.
-
- You can pass freq as '0' and just use period
-
- """
-
- def __init__(
- self,
- xy: tuple[int | float, int | float],
- width_height: tuple[int | float, int | float],
- speed: int | float,
- frames: list[str],
- freq: int | float,
- period: int | float | None = None,
- ) -> None:
- super().__init__(xy, width_height, speed)
-
- if len(frames):
- self.animation_frames = frames
- if period is not None:
- self.animation_period = period
- self.animation_freq = 1 / period
- else:
- self.animation_freq = freq
- self.animation_period = 1 / freq
- self.animation_current = 0
- self.animation_last = 0
-
- if len(frames):
- self.texture_file = self.animation_frames[self.animation_current]
-
- def can_animate(self) -> bool:
- """Check if possible to animate"""
- time = pygame.time.get_ticks() / 1000
-
- if time - self.animation_last >= self.animation_period:
- self.animation_last = time
- return True
- return False
-
- def animate(self) -> None:
- """Animate one frame if possible"""
- if self.can_animate():
- self.animation_current = (self.animation_current + 1) % len(
- self.animation_frames
- )
- self.texture_file = self.animation_frames[self.animation_current]
diff --git a/danmaku/database/construct.py b/danmaku/database/construct.py
index 54eafea..4fdfed3 100644
--- a/danmaku/database/construct.py
+++ b/danmaku/database/construct.py
@@ -16,8 +16,8 @@
basic_enemy = EnemyTypes.create(
name="basic enemy",
- texture_file="basic_enemy_2.png;basic_enemy_1.png;"
- "basic_enemy_2.png;basic_enemy_3.png",
+ texture_file="basic_enemy_down_static_2.png;basic_enemy_down_1.png;"
+ "basic_enemy_down_static_2.png;basic_enemy_down_3.png",
texture_size_width=50,
texture_size_height=65,
speed=30,
@@ -31,8 +31,8 @@
strong_enemy = EnemyTypes.create(
name="strong enemy",
- texture_file="strong_enemy_2.png;strong_enemy_1.png;"
- "strong_enemy_2.png;strong_enemy_3.png",
+ texture_file="strong_enemy_down_static_2.png;strong_enemy_down_1.png;"
+ "strong_enemy_down_static_2.png;strong_enemy_down_3.png",
texture_size_width=50,
texture_size_height=65,
speed=20,
@@ -46,8 +46,8 @@
boss = EnemyTypes.create(
name="boss",
- texture_file="strong_enemy_2.png;strong_enemy_1.png;"
- "strong_enemy_2.png;strong_enemy_3.png",
+ texture_file="strong_enemy_down_static_2.png;strong_enemy_down_1.png;"
+ "strong_enemy_down_static_2.png;strong_enemy_down_3.png",
texture_size_width=60,
texture_size_height=85,
speed=10,
@@ -128,7 +128,7 @@
texture_size_height=50,
speed=200,
shoot_v=250,
- hp=1300,
+ hp=500,
dm=10,
endurance=1,
hitbox_radius=10,
diff --git a/danmaku/database/database.py b/danmaku/database/database.py
index e0aeb62..87e8742 100644
--- a/danmaku/database/database.py
+++ b/danmaku/database/database.py
@@ -88,43 +88,63 @@ def get_saved_objects() -> list:
def get_saved_game() -> dict:
"""Get saved game from database
- Returns dict: {"score", "level", "power"}
+ Returns dict: {"score", "level", "power", "bombs"}
"""
game = tuple(iter(SavedGame.select().dicts()))[-1]
return game
-def get_game_history() -> list:
+def get_game_history() -> list[dict[str, int]]:
"""Get game history from database
- Returns list: [{"score", "level"}]
+ Returns list: [{"score", "level", "time"}]
"""
games = tuple(iter(SavedGame.select()))
res = []
for i in games:
- objects = {
- "score": i.score,
- "level": i.level,
- }
+ objects = {"score": i.score, "level": i.level, "time": i.time}
res.append(objects)
return res
+def delete_last_game():
+ """Remove last game record from database"""
+ games = list(iter(SavedGame.select()))
+ SavedGame.delete_by_id(games[-1])
+ SavedGame.update()
+
+
def set_saved_objects(name: str, objects: Iterable) -> None:
"""Set saved objects to database"""
for e in objects:
+ if hasattr(e, "my_type"):
+ my_type = e.my_type
+ else:
+ my_type = ""
+ if hasattr(e, "health"):
+ health = e.health
+ else:
+ health = 0
+ if hasattr(e, "damage"):
+ damage = e.damage
+ else:
+ damage = 0
n = SavedObjects.create(
object=name,
- object_type=e.my_type,
+ object_type=my_type,
object_position=f"{e.x}, {e.y}",
- object_hp=e.health,
- object_damage=e.damage,
+ object_hp=health,
+ object_damage=damage,
)
n.save()
-def set_saved_game(cur_level: int, score: int, power: int) -> None:
+def set_saved_game(
+ cur_level: int, score: int, power: int, bombs: int, time: float
+) -> None:
"""Set saved game to database"""
- n = SavedGame.create(score=score, level=cur_level, power=power)
+ n = SavedGame.create(
+ score=score, level=cur_level, power=power, bombs=bombs, time=time
+ )
n.save()
@@ -136,6 +156,7 @@ def delete_saved_objects() -> None:
def get_settings() -> dict:
+ """Get all settings"""
settings = {}
for setting in Settings.select():
match setting.type:
@@ -143,7 +164,7 @@ def get_settings() -> dict:
value = int(setting.value)
possible_values = list(map(int, setting.possible_values.split(";")))
case "bool":
- value = bool(setting.value)
+ value = setting.value == "True"
possible_values = [True, False]
case "str":
value = setting.value
@@ -159,17 +180,15 @@ def get_settings() -> dict:
def delete_settings():
+ """Delete all settings"""
for setting in Settings.select():
Settings.delete_by_id(setting)
Settings.update()
def set_settings(settings: dict) -> None:
+ """Set settings from dictionary"""
for key, value in settings.items():
s = Settings.get(Settings.name == key)
s.value = value
s.save()
-
-
-if __name__ == "__main__":
- print(get_saved_game())
diff --git a/danmaku/database/models.py b/danmaku/database/models.py
index f491b49..2366e74 100644
--- a/danmaku/database/models.py
+++ b/danmaku/database/models.py
@@ -6,6 +6,7 @@
CharField,
IntegerField,
BooleanField,
+ FloatField,
)
from danmaku.utils import resource_path
@@ -13,7 +14,7 @@
db = SqliteDatabase(resource_path("DataBase.db"))
-# pylint: disable=missing-class-docstring
+# pylint: disable=missing-class-docstring,too-few-public-methods
class BaseModel(Model):
@@ -71,6 +72,8 @@ class SavedGame(BaseModel):
level = IntegerField()
score = IntegerField()
power = IntegerField()
+ bombs = IntegerField()
+ time = FloatField()
class Settings(BaseModel):
diff --git a/danmaku/game.py b/danmaku/game.py
deleted file mode 100644
index 36b5238..0000000
--- a/danmaku/game.py
+++ /dev/null
@@ -1,315 +0,0 @@
-"""Game scene."""
-
-import random
-
-import vgame
-from vgame import Keys
-import pygame
-
-from danmaku.utils import not_in_border, resource_path
-from danmaku.enemy import Enemy
-from danmaku.player import Player
-from danmaku.bullet import Bullet
-from danmaku.database import (
- get_saved_objects,
- get_saved_game,
- set_saved_objects,
- set_saved_game,
- delete_saved_objects,
- get_settings,
-)
-from danmaku.pause import Pause
-from danmaku.background import Background
-from danmaku.drop import Drop, PowerUp, Points
-
-
-LEVEL1 = (Enemy((150, 15), "basic enemy"),)
-LEVEL2 = (
- Enemy((50, -25), "basic enemy"),
- Enemy((200, -50), "basic enemy"),
-)
-LEVEL3 = (Enemy((110, 5), "strong enemy"),)
-LEVEL4 = (
- Enemy((50, -25), "strong enemy"),
- Enemy((200, -50), "strong enemy"),
-)
-LEVEL5 = (
- Enemy((50, -20), "basic enemy"),
- Enemy((200, -50), "basic enemy"),
- Enemy((110, -35), "strong enemy"),
-)
-LEVEL6 = (
- Enemy((50, -15), "strong enemy"),
- Enemy((200, -50), "basic enemy"),
- Enemy((110, -35), "strong enemy"),
-)
-LEVEL7 = (Enemy((150, -40), "boss"),)
-LEVEL8 = (
- Enemy((50, -15), "strong enemy"),
- Enemy((200, -50), "strong enemy"),
- Enemy((300 - 50, -35), "strong enemy"),
-)
-LEVEL9 = (
- Enemy((50, -15), "strong enemy"),
- Enemy((200, -50), "strong enemy"),
- Enemy((300 - 50, -35), "strong enemy"),
-)
-LEVEL10 = (
- Enemy((50, -15), "strong enemy"),
- Enemy((200, -50), "boss"),
- Enemy((300 - 50, -35), "strong enemy"),
-)
-LEVELS = (
- LEVEL1,
- LEVEL2,
- LEVEL3,
- LEVEL4,
- LEVEL5,
- LEVEL6,
- LEVEL7,
- LEVEL8,
- LEVEL9,
- LEVEL10,
-)
-
-
-# pylint: disable=attribute-defined-outside-init, missing-class-docstring
-class Game(vgame.Scene):
- new_game: bool = True
-
- def load(self):
- self.graphics.library.path = resource_path("textures")
-
- self.settings = get_settings()
-
- pygame.mixer.init()
- pygame.mixer.music.set_volume(self.settings["music_volume"]["value"] / 100)
- pygame.mixer.music.load(resource_path("sounds/bgm.wav"))
- pygame.mixer.music.play(loops=-1)
-
- self.paused = False
- self.pause_object = Pause()
- self.exit_status = ""
-
- self.background_object = Background(0, 0, self.width, self.height)
-
- self.bullets: list[Bullet] = []
- self.drops: list[Drop] = []
-
- if self.new_game:
- self.cur_level = 0
- self.enemies: list[Enemy] = list(LEVELS[self.cur_level])
- self.player = Player((self.width // 2, self.height - 50), "player")
-
- else:
- self.enemies: list[Enemy] = []
- for entity in get_saved_objects():
- match entity["object"]:
- case "enemy":
- self.enemies.append(
- Enemy(
- entity["object_position"],
- entity["object_type"],
- start_hp=entity["object_hp"],
- )
- )
- case "bullet":
- self.bullets.append(
- Bullet(
- entity["object_position"],
- entity["object_damage"],
- entity["object_type"],
- )
- )
- case "player":
- self.player = Player(
- entity["object_position"],
- entity["object_type"],
- updated_hp=entity["object_hp"],
- )
-
- saved_game = get_saved_game()
- self.cur_level = saved_game["level"]
- self.player.score = saved_game["score"]
- self.player.power = saved_game["power"]
- delete_saved_objects()
-
- self.player.set_bounds(0, 0, self.width, self.height)
-
- def update_pause(self):
- """Called from update loop if paused"""
- self.pause_object.update(self.pressed_keys)
- status = self.pause_object.exit_status
- match status:
- case "continue":
- self.paused = False
- case "settings":
- raise NotImplementedError()
- case "menu":
- delete_saved_objects()
- set_saved_objects("enemy", self.enemies)
- set_saved_objects("bullet", self.bullets)
- set_saved_objects("player", [self.player])
- set_saved_game(self.cur_level, self.player.score, self.player.power)
- self.stop()
-
- def update_game(self):
- """Called from update loop if *not* paused"""
-
- vx = (Keys.RIGHT in self.pressed_keys) - (Keys.LEFT in self.pressed_keys)
- vy = (Keys.DOWN in self.pressed_keys) - (Keys.UP in self.pressed_keys)
-
- if {Keys.SPACE, Keys.Z} & self.pressed_keys:
- self.bullets += self.player.shoot()
- if Keys.X in self.pressed_keys:
- self.bullets += self.player.bomb()
- if Keys.LEFT_SHIFT in self.pressed_keys:
- self.player.slow = True
- else:
- self.player.slow = False
-
- self.player.vx, self.player.vy = vx, vy
-
- self.player.update(self.delta)
- self.player.animate()
-
- for enemy in self.enemies:
- if self.player.collision(enemy):
- self.player.get_damage(enemy.damage / 100)
- self.bullets += enemy.shoot()
- enemy.animate()
- enemy.update(self.delta)
- if (
- enemy.y > self.height / 2 and not 0 <= enemy.x < self.width
- ) or enemy.y > self.height + enemy.height / 2:
- self.enemies.remove(enemy)
-
- for bullet in filter(lambda b: b.enemy, self.bullets):
- if self.player.collision(bullet):
- self.player.get_damage(bullet.damage)
- self.bullets.remove(bullet)
- continue
-
- for bullet in filter(lambda b: not b.enemy, self.bullets):
- for enemy in self.enemies:
- if enemy.collision(bullet):
- enemy.get_damage(bullet.damage)
- if enemy.health <= 0:
- self.player.score += enemy.cost
- self.drops += enemy.generate_drops()
- self.enemies.remove(enemy)
-
- self.bullets.remove(bullet)
- break
-
- for bullet in self.bullets:
- bullet.update(self.delta)
-
- if not not_in_border(
- bullet.x, bullet.y, bullet.vx, bullet.vy, self.width, self.height
- ):
- self.bullets.remove(bullet)
-
- for drop in self.drops:
- if self.player.collision(drop):
- if isinstance(drop, PowerUp):
- self.player.power += 2
- elif isinstance(drop, Points):
- self.player.score += 10
- self.drops.remove(drop)
- continue
-
- drop.update(self.delta)
-
- if not not_in_border(
- drop.x, drop.y, drop.vx, drop.vy, self.width, self.height
- ):
- self.drops.remove(drop)
-
- self.background_object.animate()
-
- if len(self.enemies) == 0:
- if len(LEVELS) > self.cur_level + 1:
- self.cur_level += 1
- self.enemies = list(LEVELS[self.cur_level])
- else:
- set_saved_game(self.cur_level, self.player.score, self.player.power)
- self.exit_status = "win"
- self.stop()
-
- if self.player.health <= 0:
- set_saved_game(self.cur_level, self.player.score, self.player.power)
- self.exit_status = "lose"
- death_sfx = pygame.mixer.Sound(resource_path("sounds/death.wav"))
- death_sfx.set_volume(self.settings["sfx_volume"]["value"] / 100)
- channel = death_sfx.play()
- while channel.get_busy():
- pygame.time.wait(10)
- self.stop()
-
- def update(self):
- # self.print_stats()
- if Keys.ESCAPE in self.pressed_keys:
- self.pressed_keys.remove(Keys.ESCAPE)
- self.paused = not self.paused
- if self.paused:
- self.pause_object.load(self.width, self.height)
-
- if self.paused:
- self.update_pause()
- else:
- self.update_game()
-
- def draw(self):
- self.graphics.draw_sprite(self.background_object)
-
- self.player.draw(self.graphics)
-
- for enemy in self.enemies:
- self.graphics.draw_sprite(enemy)
-
- for bullet in self.bullets:
- self.graphics.draw_sprite(bullet)
-
- for drop in self.drops:
- self.graphics.draw_sprite(drop)
-
- self.graphics.text(f"HP: {self.player.health}", (0, 0))
- self.graphics.text(f"Score: {self.player.score}", (150, 0))
-
- if self.paused:
- self.pause_object.draw(self.graphics)
-
- def exit(self):
- pygame.mixer.music.stop()
-
- def print_stats(self):
- """Dev debug"""
- print("\x1b[?25l", end="") # hide cursor
- print("\x1b[2J\x1b[0;0H", end="") # clear console
-
- # Tech stuff
- print(
- f"\x1b[{32 if self.fps >= self.framerate else 31}mFPS:\t{self.fps:.2f}\x1b[0m",
- f"Gr. dT:\t{self.graphics_delta*1000}ms",
- "",
- f"\x1b[{32 if self.tps >= self.tickrate else 31}mTPS:\t{self.tps:.2f}\x1b[0m",
- f"dT:\t{self.delta*1000}ms",
- sep="\n",
- )
-
- print(end="\n\n")
-
- # Game stats
- print(
- f"HP: {self.player.health}",
- f"Score: {self.player.score}",
- f"Level: {self.cur_level}",
- "",
- f"Enemies: {len(self.enemies)}",
- f"Bullets: {len(self.bullets)}",
- f"Drops: {len(self.drops)}",
- sep="\n",
- )
-
- print("\x1b[?25h", end="") # show cursor
diff --git a/danmaku/game/animated.py b/danmaku/game/animated.py
new file mode 100644
index 0000000..c39ef68
--- /dev/null
+++ b/danmaku/game/animated.py
@@ -0,0 +1,197 @@
+"""Declaration of Animated class"""
+
+import pygame
+
+from danmaku.game.gameobject import GameObject
+from danmaku.utils import Direction
+
+
+class Animated(GameObject):
+ """Base class for animated objects
+
+ Args:
+ xy (tuple[int | float, int | float]): Position of the object.
+ width_height (tuple[int | float, int | float]): Width and height of the object.
+ speed (int | float): Speed of the object.
+ frames (list[str]): List of frames
+ freq (int | float | None, optional): Frequency of animation
+ period (int | float | None, optional): Period of animation. Defaults to None.
+
+ You can pass freq as '0' and just use period
+
+ """
+
+ def __init__(
+ self,
+ xy: tuple[int | float, int | float],
+ width_height: tuple[int | float, int | float],
+ speed: int | float,
+ frames: list[str],
+ freq: int | float | None = None,
+ period: int | float | None = None,
+ ) -> None:
+ super().__init__(xy, width_height, speed)
+
+ if len(frames):
+ self.animation_frames = frames
+ if period is not None:
+ self.animation_period = period
+ self.animation_freq = 1 / period
+ elif freq is not None:
+ self.animation_freq = freq
+ self.animation_period = 1 / freq
+ else:
+ raise ValueError("You must pass period or freq")
+ self.animation_current = 0
+ self.animation_last = 0
+ self.last_direction = 0
+
+ if len(frames):
+ self.texture_file = self.animation_frames[self.animation_current]
+
+ def can_animate(self) -> bool:
+ """Check if possible to animate"""
+ time = pygame.time.get_ticks() / 1000
+
+ if time - self.animation_last >= self.animation_period:
+ self.animation_last = time
+ return True
+ return False
+
+ def animate(self) -> None:
+ """Animate one frame if possible"""
+ if self.can_animate():
+ self.animation_current = (self.animation_current + 1) % len(
+ self.animation_frames
+ )
+ self.texture_file = self.animation_frames[self.animation_current]
+
+ def animate_absolute(self, time: int) -> None:
+ """Set frame according to time (milliseconds)"""
+ frame_duration = self.animation_period * 1000
+
+ self.animation_current = int(time // frame_duration) % len(
+ self.animation_frames
+ )
+
+ self.texture_file = self.animation_frames[self.animation_current]
+
+
+class AnimatedDirectional(Animated):
+ """Base class for animated objects, that change animations based on direction"""
+
+ def __init__(
+ self,
+ xy: tuple[int | float, int | float],
+ width_height: tuple[int | float, int | float],
+ speed: int | float,
+ frames: list[str],
+ freq: int | float | None = None,
+ period: int | float | None = None,
+ ) -> None:
+ super().__init__(xy, width_height, speed, [], freq, period)
+ self.last_direction = 0
+
+ self.animation_frames = {
+ Direction.LEFT: [],
+ Direction.RIGHT: [],
+ Direction.UP: [],
+ Direction.DOWN: [],
+ Direction.STATIC: [],
+ }
+
+ for file in frames:
+ if "left" in file:
+ self.animation_frames[Direction.LEFT].append(file)
+ if "right" in file:
+ self.animation_frames[Direction.RIGHT].append(file)
+ if "up" in file:
+ self.animation_frames[Direction.UP].append(file)
+ if "down" in file:
+ self.animation_frames[Direction.DOWN].append(file)
+ if "static" in file or "idle" in file:
+ self.animation_frames[Direction.STATIC].append(file)
+
+ self.texture_file = self.animation_frames[Direction.STATIC][
+ self.animation_current
+ ]
+
+ def animate(
+ self, direction_vector: tuple[int | float, int | float] = (0, 0)
+ ) -> None:
+ """Animate one frame if possible"""
+
+ if self.can_animate():
+ direction = None
+ if direction_vector[0] == direction_vector[1] and direction_vector[0] == 0:
+ direction = Direction.STATIC
+ elif direction_vector[0] > 0:
+ direction = Direction.RIGHT
+ elif direction_vector[1] > 0:
+ direction = Direction.DOWN
+ elif direction_vector[0] < 0:
+ direction = Direction.LEFT
+ elif direction_vector[1] < 0:
+ direction = Direction.UP
+
+ if direction == Direction.STATIC:
+ a = ""
+ if self.last_direction == Direction.UP:
+ a = "up"
+ elif self.last_direction == Direction.DOWN:
+ a = "down"
+ elif self.last_direction == Direction.RIGHT:
+ a = "right"
+ elif self.last_direction == Direction.LEFT:
+ a = "left"
+ for i in self.animation_frames[direction]:
+ if a in i:
+ self.texture_file = i
+ elif direction is not None:
+ self.animation_current = (self.animation_current + 1) % len(
+ self.animation_frames[direction]
+ )
+ self.texture_file = self.animation_frames[direction][
+ self.animation_current
+ ]
+ self.last_direction = direction
+
+ def animate_absolute(
+ self, time: int, direction_vector: tuple[int | float, int | float] = (0, 0)
+ ) -> None:
+ """Set frame according to time (milliseconds)"""
+
+ direction = None
+ if direction_vector[0] == direction_vector[1] and direction_vector[0] == 0:
+ direction = Direction.STATIC
+ elif direction_vector[0] > 0:
+ direction = Direction.RIGHT
+ elif direction_vector[1] > 0:
+ direction = Direction.DOWN
+ elif direction_vector[0] < 0:
+ direction = Direction.LEFT
+ elif direction_vector[1] < 0:
+ direction = Direction.UP
+
+ if direction == Direction.STATIC:
+ a = ""
+ if self.last_direction == Direction.UP:
+ a = "up"
+ elif self.last_direction == Direction.DOWN:
+ a = "down"
+ elif self.last_direction == Direction.RIGHT:
+ a = "right"
+ elif self.last_direction == Direction.LEFT:
+ a = "left"
+ for i in self.animation_frames[direction]:
+ if a in i:
+ self.texture_file = i
+
+ elif direction is not None:
+ frame_duration = self.animation_period * 1000
+
+ self.animation_current = int(time // frame_duration) % len(
+ self.animation_frames[direction]
+ )
+ self.texture_file = self.animation_frames[direction][self.animation_current]
+ self.last_direction = direction
diff --git a/danmaku/background.py b/danmaku/game/background.py
similarity index 62%
rename from danmaku/background.py
rename to danmaku/game/background.py
index 29c6d92..9b0e780 100644
--- a/danmaku/background.py
+++ b/danmaku/game/background.py
@@ -2,7 +2,7 @@
import vgame
-from danmaku.animated import Animated
+from danmaku.game.animated import Animated
class Background(Animated):
@@ -14,11 +14,16 @@ def __init__(
y: int | float,
width: int | float,
height: int | float,
+ frames: list[str],
):
- self.frame_count = 48
- frames = [f"background/background_{i}.png" for i in range(self.frame_count)]
+ if not frames:
+ self.frame_count = 48
+ frames = [f"background/background_{i}.png" for i in range(self.frame_count)]
+ else:
+ pass
+
super().__init__((x, y), (width, height), 0, frames, 0, period=0.1)
- self.texture_size = self.width, self.height
+ self.texture_size = int(self.width), int(self.height)
def draw(self, graphics: vgame.graphics.Graphics):
graphics.draw_sprite(self)
diff --git a/danmaku/bullet.py b/danmaku/game/bullet.py
similarity index 85%
rename from danmaku/bullet.py
rename to danmaku/game/bullet.py
index 95e50c4..b07c0c4 100644
--- a/danmaku/bullet.py
+++ b/danmaku/game/bullet.py
@@ -1,27 +1,27 @@
-"""Entity's bullet declaration."""
-
-from danmaku.entity import Entity
-from danmaku.database import get_bullet_type
-
-
-class Bullet(Entity):
- """Bullet object."""
-
- def __init__(
- self, xy: tuple[int | float, int | float], damage: int | float, object_type
- ):
- args = get_bullet_type(object_type)
- super().__init__(
- xy,
- (args["texture_size"][0], args["texture_size"][1]),
- args["speed"],
- 0,
- damage,
- )
- self.enemy = args["enemy"]
- self.vx, self.vy = args["vx_vy"]
- self.hitbox_radius = args["hitbox_radius"]
-
- self.texture_file = args["texture_file"]
- self.texture_size = args["texture_size"]
- self.my_type = object_type
+"""Entity's bullet declaration."""
+
+from danmaku.game.entity import Entity
+from danmaku.database import get_bullet_type
+
+
+class Bullet(Entity):
+ """Bullet object."""
+
+ def __init__(
+ self, xy: tuple[int | float, int | float], damage: int | float, object_type: str
+ ):
+ args = get_bullet_type(object_type)
+ super().__init__(
+ xy,
+ (args["texture_size"][0], args["texture_size"][1]),
+ args["speed"],
+ 0,
+ damage,
+ )
+ self.enemy: bool = args["enemy"]
+ self.vx, self.vy = args["vx_vy"]
+ self.hitbox_radius = args["hitbox_radius"]
+
+ self.texture_file = args["texture_file"]
+ self.texture_size = args["texture_size"]
+ self.my_type = object_type
diff --git a/danmaku/drop.py b/danmaku/game/drop.py
similarity index 94%
rename from danmaku/drop.py
rename to danmaku/game/drop.py
index 6796b5a..2a2bd93 100644
--- a/danmaku/drop.py
+++ b/danmaku/game/drop.py
@@ -1,6 +1,6 @@
"""Entity's bullet declaration."""
-from danmaku.gameobject import GameObject
+from danmaku.game.gameobject import GameObject
# from danmaku.database import ...
diff --git a/danmaku/enemy.py b/danmaku/game/enemy.py
similarity index 71%
rename from danmaku/enemy.py
rename to danmaku/game/enemy.py
index a6121ec..2ded3aa 100644
--- a/danmaku/enemy.py
+++ b/danmaku/game/enemy.py
@@ -1,109 +1,125 @@
-"""Enemy object declaration."""
-
-from random import randint, choices
-from math import sin, cos, pi
-
-from danmaku.animated import Animated
-from danmaku.bullet import Bullet
-from danmaku.database import get_enemy_type
-from danmaku.shooter import Shooter
-from danmaku.drop import PowerUp, Points
-
-
-class Enemy(Shooter, Animated):
- """Enemy object."""
-
- def __init__(
- self,
- xy: tuple[int | float, int | float],
- object_type: str,
- start_hp: int | float = 0,
- ):
- args = get_enemy_type(object_type)
-
- health = start_hp or args["hp"]
-
- super().__init__(
- xy,
- args["texture_size"],
- args["speed"],
- health,
- args["dm"],
- args["endurance"],
- "basic enemy bullet",
- 0,
- args["shoot_v"] / 1000,
- hitbox_radius=args["texture_size"][0] // 2,
- direction=(0, 1),
- )
-
- frames = list(map(lambda x: f"/enemy/{x}", args["texture_file"].split(";")))
- Animated.__init__(
- self, xy, args["texture_size"], args["speed"], frames, 0, period=0.1
- )
- self.texture_size = args["texture_size"]
- self.my_type = object_type
- self.cost = args["cost"]
-
- self.vx, self.vy = 0, 1
-
- def shoot(self) -> list[Bullet]:
- if self.can_shoot():
- match self.my_type:
- case "boss":
- if randint(0, 6) == 0:
- return self.shoot_cluster()
- return self.shoot_radial(waves=2, base_angle=randint(0, 359))
- case "basic enemy":
- bullet = Bullet((self.x, self.y), self.damage, self.bullet_type)
- bullet.vx = randint(-100, 100) / 100
- bullet.vy = (1 - bullet.vx**2) ** 0.5
- return [bullet]
- case "strong enemy":
- return self.shoot_radial(waves=3, n=5)
- return []
-
- def shoot_radial(self, base_angle=0, angle_step=0, waves=1, n=6) -> list[Bullet]:
- """Shoot circle of bullets"""
-
- bullets = []
-
- for wave in range(waves):
- first_angle = base_angle + wave * angle_step
- for add_angle in range(0, 360, 360 // n):
- angle = pi * ((first_angle + add_angle) % 360) / 180
- bullet = Bullet((self.x, self.y), self.damage, self.bullet_type)
- bullet.vx = cos(angle)
- bullet.vy = sin(angle)
- for _ in range(wave):
- bullet.update(0.3)
- bullets.append(bullet)
- return bullets
-
- def shoot_cluster(self, waves=1, n=10, base_angle=0, arc=180):
- bullets = []
- for wave in range(waves):
- for i, a in enumerate(range(0, arc, arc // n)):
- angle = pi * ((base_angle + a) % 360) / 180
- bullet = Bullet((self.x, self.y), self.damage, self.bullet_type)
- bullet.vx = cos(angle) * (i + 1) * 0.2
- bullet.vy = sin(angle) * (i + 1) * 0.2
- for _ in range(wave):
- bullet.update(0.3)
- bullets.append(bullet)
- return bullets
-
- def generate_drops(self) -> list:
- drops = []
- count = 1
- if self.my_type == "boss":
- count = 5
- for _ in range(count):
- pos = (self.x + randint(-10, 10), self.y + randint(-10, 10))
- match choices(("powerup", "points", None), (1, 1, 2))[0]:
- case "powerup":
- drops.append(PowerUp(pos))
- case "points":
- drops.append(Points(pos))
-
- return drops
+"""Enemy object declaration."""
+
+from random import randint, choices
+from math import sin, cos, pi
+
+from danmaku.game.animated import AnimatedDirectional
+from danmaku.game.bullet import Bullet
+from danmaku.database import get_enemy_type
+from danmaku.game.shooter import Shooter
+from danmaku.game.drop import PowerUp, Points, Drop
+
+
+class Enemy(Shooter, AnimatedDirectional):
+ """Enemy object."""
+
+ def __init__(
+ self,
+ xy: tuple[int | float, int | float],
+ object_type: str,
+ start_hp: int | float = 0,
+ ):
+ args = get_enemy_type(object_type)
+
+ health: int | float = start_hp or args["hp"]
+
+ super().__init__(
+ xy,
+ args["texture_size"],
+ args["speed"],
+ health,
+ args["dm"],
+ args["endurance"],
+ "basic enemy bullet",
+ 0,
+ args["shoot_v"] / 1000,
+ hitbox_radius=args["texture_size"][0] // 2,
+ direction=(0, 1),
+ )
+
+ frames = list(map(lambda x: f"/enemy/{x}", args["texture_file"].split(";")))
+ AnimatedDirectional.__init__(
+ self,
+ xy,
+ args["texture_size"],
+ args["speed"],
+ frames,
+ period=0.1,
+ )
+ self.texture_size = args["texture_size"]
+ self.my_type = object_type
+ self.cost: int = args["cost"]
+
+ self.vx, self.vy = 0, 1
+
+ def shoot(self) -> list[Bullet]:
+ if self.can_shoot():
+ match self.my_type:
+ case "boss":
+ if randint(0, 6) == 0:
+ return self.shoot_cluster()
+ return self.shoot_radial(waves=2, base_angle=randint(0, 359))
+ case "basic enemy":
+ self.shoot_sound(2)
+ bullet = Bullet((self.x, self.y), self.damage, self.bullet_type)
+ bullet.vx = randint(-100, 100) / 100
+ bullet.vy = (1 - bullet.vx**2) ** 0.5
+ return [bullet]
+ case "strong enemy":
+ return self.shoot_radial(waves=3, n=5, base_angle=randint(0, 359))
+ case _:
+ pass
+ return []
+
+ def shoot_radial(
+ self, base_angle: int = 0, angle_step: int = 0, waves: int = 1, n: int = 6
+ ) -> list[Bullet]:
+ """Shoot circle of bullets"""
+
+ bullets: list[Bullet] = []
+
+ for wave in range(waves):
+ first_angle = base_angle + wave * angle_step
+ for add_angle in range(0, 360, 360 // n):
+ angle = pi * ((first_angle + add_angle) % 360) / 180
+ bullet = Bullet((self.x, self.y), self.damage, self.bullet_type)
+ bullet.vx = cos(angle)
+ bullet.vy = sin(angle)
+ for _ in range(wave):
+ bullet.update(0.3)
+ bullets.append(bullet)
+ return bullets
+
+ def shoot_cluster(
+ self, waves: int = 1, n: int = 10, base_angle: int = 0, arc: int = 180
+ ) -> list[Bullet]:
+ """Shoot spiral wave of bullets"""
+ bullets: list[Bullet] = []
+ for wave in range(waves):
+ for i, a in enumerate(range(0, arc, arc // n)):
+ angle = pi * ((base_angle + a) % 360) / 180
+ bullet = Bullet((self.x, self.y), self.damage, self.bullet_type)
+ bullet.vx = cos(angle) * (i + 1) * 0.2
+ bullet.vy = sin(angle) * (i + 1) * 0.2
+ for _ in range(wave):
+ bullet.update(0.3)
+ bullets.append(bullet)
+ return bullets
+
+ def generate_drops(self) -> list[Drop]:
+ """Generate drops"""
+ drops: list[Drop] = []
+ count = 1
+ if self.my_type == "boss":
+ count = 5
+ for _ in range(count):
+ pos = (self.x + randint(-10, 10), self.y + randint(-10, 10))
+ match choices(("powerup", "points", None), (1, 1, 2))[0]:
+ case "powerup":
+ drops.append(PowerUp(pos))
+ case "points":
+ drops.append(Points(pos))
+ case _:
+ pass
+
+ return drops
diff --git a/danmaku/entity.py b/danmaku/game/entity.py
similarity index 59%
rename from danmaku/entity.py
rename to danmaku/game/entity.py
index 629af96..63d6432 100644
--- a/danmaku/entity.py
+++ b/danmaku/game/entity.py
@@ -1,6 +1,10 @@
"""Base class for alive objects"""
-from danmaku.gameobject import GameObject
+import pygame
+
+from danmaku.database import get_settings
+from danmaku.game.gameobject import GameObject
+from danmaku.utils import resource_path
class Entity(GameObject):
@@ -25,3 +29,11 @@ def __init__(
def get_damage(self, damage: int | float):
"""Decrease health point."""
self.health -= damage / self.endurance
+ self.damage_sound(3)
+
+ def damage_sound(self, channel: int):
+ """Play damage sound"""
+ pygame.mixer.init()
+ sound = pygame.mixer.Sound(resource_path("sounds/hit.wav"))
+ sound.set_volume(get_settings()["sfx_volume"]["value"] / 100)
+ pygame.mixer.Channel(channel).play(sound)
diff --git a/danmaku/game/game.py b/danmaku/game/game.py
new file mode 100644
index 0000000..9142fc8
--- /dev/null
+++ b/danmaku/game/game.py
@@ -0,0 +1,413 @@
+"""Game scene."""
+
+import vgame
+from vgame import Keys
+import pygame
+
+from danmaku.utils import not_in_border, resource_path
+from danmaku.game.enemy import Enemy
+from danmaku.game.player import Player
+from danmaku.game.bullet import Bullet
+from danmaku.database import (
+ get_saved_objects,
+ get_saved_game,
+ set_saved_objects,
+ set_saved_game,
+ delete_saved_objects,
+ get_settings,
+ delete_last_game,
+)
+from danmaku.game.pause import Pause
+from danmaku.game.background import Background
+from danmaku.game.drop import Drop, PowerUp, Points
+from danmaku.game.level import Level, Stage, BossStage
+
+
+# pylint: disable=attribute-defined-outside-init, missing-class-docstring
+class Game(vgame.Scene):
+ new_game: bool = True
+
+ def load(self):
+ # self.game_border = self.width // 4 * 3
+
+ # game field aspect ratio is 3:4
+ self.game_border = self.height // 4 * 3
+
+ stage_1 = Stage([Enemy((self.game_border // 2, 15), "basic enemy")])
+ stage_2 = Stage(
+ [
+ Enemy((50, -25), "basic enemy"),
+ Enemy((self.game_border - 50, -50), "basic enemy"),
+ ]
+ )
+ stage_3 = Stage([Enemy((self.game_border // 2.5, 5), "strong enemy")])
+ stage_4 = Stage(
+ [
+ Enemy((50, -25), "strong enemy"),
+ Enemy((self.game_border - 50, -50), "strong enemy"),
+ ]
+ )
+ stage_5 = Stage(
+ [
+ Enemy((50, -80), "basic enemy"),
+ Enemy((self.game_border // 2, -20), "strong enemy"),
+ Enemy((self.game_border - 50, -80), "basic enemy"),
+ ]
+ )
+ stage_6 = Stage(
+ [
+ Enemy((50, -15), "strong enemy"),
+ Enemy((self.game_border // 2, -35), "strong enemy"),
+ Enemy((self.game_border - 50, -50), "basic enemy"),
+ ]
+ )
+ stage_7 = BossStage(
+ enemies=[], boss=Enemy((self.game_border // 2, -40), "boss"), actions=[]
+ )
+
+ stage_8 = Stage(
+ [
+ Enemy((50, -15), "strong enemy"),
+ Enemy((self.game_border // 2, -50), "strong enemy"),
+ Enemy((self.game_border - 50, -35), "strong enemy"),
+ ]
+ )
+ stage_9 = Stage(
+ [
+ Enemy((50, -15), "strong enemy"),
+ Enemy((self.game_border // 2, -50), "strong enemy"),
+ Enemy((self.game_border - 50, -35), "strong enemy"),
+ ]
+ )
+ stage_10 = BossStage(
+ enemies=[
+ Enemy((50, -15), "strong enemy"),
+ Enemy((self.game_border - 50, -35), "strong enemy"),
+ ],
+ boss=Enemy((self.game_border // 2, -50), "boss"),
+ actions=[],
+ )
+
+ level_1 = Level(
+ stages=[
+ stage_1,
+ stage_2,
+ stage_3,
+ stage_4,
+ stage_5,
+ stage_6,
+ stage_7,
+ ]
+ )
+
+ level_2 = Level(stages=[stage_8, stage_9, stage_10])
+
+ self.levels = level_1, level_2
+
+ self.graphics.library.path = resource_path("textures")
+
+ self.settings = get_settings()
+
+ self.start_time = pygame.time.get_ticks()
+ self.current_time = 0
+
+ pygame.mixer.init()
+ pygame.mixer.music.set_volume(self.settings["music_volume"]["value"] / 100)
+ pygame.mixer.music.load(resource_path("sounds/game.wav"))
+ pygame.mixer.music.play(loops=-1)
+
+ self.paused = False
+ self.pause_object = Pause()
+ self.exit_status = ""
+
+ self.background_object = Background(0, 0, self.game_border, self.height, [])
+
+ self.bullets: list[Bullet] = []
+ self.drops: list[Drop] = []
+
+ self.boss_hp: int | float | None = None
+
+ if self.new_game:
+ self.current_level: int = 0
+ self.last_time = 0
+ self.enemies: list[Enemy] = list(self.levels[self.current_level].enemies)
+ self.player = Player(
+ (self.game_border // 2, self.height - 50),
+ "player",
+ bombs=self.settings["bombs"]["value"],
+ lives=self.settings["lives"]["value"],
+ )
+
+ else:
+ self.enemies: list[Enemy] = []
+ for entity in get_saved_objects():
+ match entity["object"]:
+ case "enemy":
+ self.enemies.append(
+ Enemy(
+ entity["object_position"],
+ entity["object_type"],
+ start_hp=entity["object_hp"],
+ )
+ )
+ case "bullet":
+ self.bullets.append(
+ Bullet(
+ entity["object_position"],
+ entity["object_damage"],
+ entity["object_type"],
+ )
+ )
+ case "player":
+ self.player = Player(
+ entity["object_position"],
+ entity["object_type"],
+ updated_hp=entity["object_hp"],
+ )
+ case "powerup":
+ self.drops.append(PowerUp(entity["object_position"]))
+ case "points":
+ self.drops.append(Points(entity["object_position"]))
+
+ saved_game = get_saved_game()
+ self.current_level: int = saved_game["level"]
+ self.player.score = saved_game["score"]
+ self.player.power = saved_game["power"]
+ self.player.bombs = saved_game["bombs"]
+ self.last_time = saved_game["time"]
+ self.start_time = pygame.time.get_ticks()
+ delete_saved_objects()
+ delete_last_game()
+
+ self.player.set_bounds(0, 0, self.game_border, self.height)
+
+ self.animation_start_time = pygame.time.get_ticks()
+
+ def update_pause(self):
+ """Called from update loop if paused"""
+ self.pause_object.update(self.pressed_keys)
+ status = self.pause_object.exit_status
+ match status:
+ case "continue":
+ self.paused = False
+ case "menu":
+ delete_saved_objects()
+ set_saved_objects("enemy", self.enemies)
+ set_saved_objects("bullet", self.bullets)
+ set_saved_objects("player", [self.player])
+ set_saved_objects(
+ "points", [x for x in self.drops if isinstance(x, Points)]
+ )
+ set_saved_objects(
+ "powerup", [x for x in self.drops if isinstance(x, PowerUp)]
+ )
+ set_saved_game(
+ self.current_level,
+ self.player.score,
+ self.player.power,
+ self.player.bombs,
+ self.current_time,
+ )
+ self.stop()
+
+ def update_game(self):
+ """Called from update loop if *not* paused"""
+
+ vx = (Keys.RIGHT in self.pressed_keys) - (Keys.LEFT in self.pressed_keys)
+ vy = (Keys.DOWN in self.pressed_keys) - (Keys.UP in self.pressed_keys)
+
+ if {Keys.SPACE, Keys.Z} & self.pressed_keys:
+ self.bullets += self.player.shoot()
+ if Keys.X in self.pressed_keys:
+ self.bullets += self.player.bomb()
+ if Keys.LEFT_SHIFT in self.pressed_keys:
+ self.player.slow = True
+ else:
+ self.player.slow = False
+
+ self.player.vx, self.player.vy = vx, vy
+
+ self.player.update(self.delta)
+
+ stage = self.levels[self.current_level].stage
+ stage.update()
+ if isinstance(stage, BossStage):
+ self.boss_hp = stage.boss.health
+ if stage.boss.health < 0:
+ self.boss_hp = None
+ else:
+ self.boss_hp = None
+
+ for enemy in self.enemies:
+ if self.player.collision(enemy):
+ self.player.get_damage(enemy.damage / 100)
+ self.bullets += enemy.shoot()
+ enemy.update(self.delta)
+ if (
+ enemy.y > self.height / 2 and not 0 <= enemy.x < self.game_border
+ ) or enemy.y > self.height + enemy.height / 2:
+ self.enemies.remove(enemy)
+
+ for bullet in filter(lambda b: b.enemy, self.bullets):
+ if self.player.collision(bullet):
+ self.player.get_damage(bullet.damage)
+ self.bullets.remove(bullet)
+ continue
+
+ for bullet in filter(lambda b: not b.enemy, self.bullets):
+ for enemy in self.enemies:
+ if enemy.collision(bullet):
+ enemy.get_damage(bullet.damage)
+ if enemy.health <= 0:
+ self.player.score += enemy.cost
+ self.drops += enemy.generate_drops()
+ self.enemies.remove(enemy)
+
+ self.bullets.remove(bullet)
+ break
+
+ for bullet in self.bullets:
+ bullet.update(self.delta)
+
+ if not not_in_border(
+ bullet.x, bullet.y, bullet.vx, bullet.vy, self.game_border, self.height
+ ):
+ self.bullets.remove(bullet)
+
+ for drop in self.drops:
+ if self.player.collision(drop):
+ if isinstance(drop, PowerUp):
+ self.player.power += 2
+ elif isinstance(drop, Points):
+ self.player.score += 10
+ self.drops.remove(drop)
+ continue
+
+ drop.update(self.delta)
+
+ if not not_in_border(
+ drop.x, drop.y, drop.vx, drop.vy, self.game_border, self.height
+ ):
+ self.drops.remove(drop)
+
+ if len(self.enemies) == 0:
+ self.next_level()
+
+ if self.player.health <= 0:
+ set_saved_game(
+ self.current_level,
+ self.player.score,
+ self.player.power,
+ self.player.bombs,
+ self.current_time,
+ )
+ self.exit_status = "lose"
+ # death_sfx = pygame.mixer.Sound(resource_path("sounds/death.wav"))
+ # death_sfx.set_volume(self.settings["sfx_volume"]["value"] / 100)
+ # channel = death_sfx.play()
+ # while channel.get_busy():
+ # pygame.time.wait(10)
+ self.stop()
+
+ def next_level(self) -> None:
+ """Start next level if possible"""
+ if self.levels[self.current_level].next_stage():
+ self.enemies = list(self.levels[self.current_level].enemies)
+ elif len(self.levels) > self.current_level + 1:
+ self.current_level += 1
+ self.enemies = list(self.levels[self.current_level].enemies)
+ else:
+ set_saved_game(
+ self.current_level,
+ self.player.score,
+ self.player.power,
+ self.player.bombs,
+ self.current_time,
+ )
+ self.exit_status = "win"
+ self.stop()
+
+ def update(self):
+ # self.print_stats()
+ if Keys.ESCAPE in self.pressed_keys:
+ self.pressed_keys.remove(Keys.ESCAPE)
+ self.paused = not self.paused
+ if self.paused:
+ self.pause_object.load(self.width, self.height)
+
+ if self.paused:
+ self.update_pause()
+ else:
+ self.current_time = (
+ round((pygame.time.get_ticks() - self.start_time) / 1000, 1)
+ + self.last_time
+ )
+ self.update_game()
+
+ def draw(self):
+ animation_time = pygame.time.get_ticks() - self.animation_start_time
+ self.background_object.animate_absolute(animation_time)
+
+ self.graphics.rectangle((0, 0), (self.width, self.height), (30, 157, 214, 180))
+ self.graphics.draw_sprite(self.background_object)
+
+ self.player.animate_absolute(animation_time, (self.player.vx, self.player.vy))
+ self.player.draw(self.graphics)
+
+ for enemy in self.enemies:
+ enemy.animate_absolute(animation_time, (enemy.vx, enemy.vy))
+ self.graphics.draw_sprite(enemy)
+
+ for bullet in self.bullets:
+ self.graphics.draw_sprite(bullet)
+
+ for drop in self.drops:
+ self.graphics.draw_sprite(drop)
+
+ self.graphics.text(f"HP: {self.player.health}", (self.game_border + 10, 0))
+ self.graphics.text(f"Score: {self.player.score}", (self.game_border + 10, 50))
+ self.graphics.text(
+ f"Time: {round(self.current_time, 1)}", (self.game_border + 10, 100)
+ )
+ self.graphics.text(f"Bombs: {self.player.bombs}", (self.game_border + 10, 150))
+ if self.boss_hp is not None:
+ self.graphics.text(f"BOSS: {self.boss_hp}", (self.game_border + 10, 200))
+
+ if self.paused:
+ self.pause_object.draw(self.graphics)
+
+ def exit(self):
+ pygame.mixer.music.stop()
+
+ def print_stats(self):
+ """Dev debug"""
+ print("\x1b[?25l", end="") # hide cursor
+ print("\x1b[2J\x1b[0;0H", end="") # clear console
+
+ # Tech stuff
+ print(
+ f"\x1b[{32 if self.fps >= self.framerate else 31}mFPS:\t{self.fps:.2f}\x1b[0m",
+ f"Gr. dT:\t{self.graphics_delta*1000}ms",
+ "",
+ f"\x1b[{32 if self.tps >= self.tickrate else 31}mTPS:\t{self.tps:.2f}\x1b[0m",
+ f"dT:\t{self.delta*1000}ms",
+ sep="\n",
+ )
+
+ print(end="\n\n")
+
+ # Game stats
+ print(
+ f"HP: {self.player.health}",
+ f"Score: {self.player.score}",
+ f"Level: {self.current_level}",
+ f"Power: {self.player.power}",
+ f"Boss: {self.boss_hp}",
+ "",
+ f"Enemies: {len(self.enemies)}",
+ f"Bullets: {len(self.bullets)}",
+ f"Drops: {len(self.drops)}",
+ sep="\n",
+ )
+
+ print("\x1b[?25h", end="") # show cursor
diff --git a/danmaku/gameobject.py b/danmaku/game/gameobject.py
similarity index 92%
rename from danmaku/gameobject.py
rename to danmaku/game/gameobject.py
index 211fdff..9af208c 100644
--- a/danmaku/gameobject.py
+++ b/danmaku/game/gameobject.py
@@ -1,49 +1,51 @@
-"""Base game object."""
-
-import math
-
-from vgame.graphics import Graphics, Sprite
-
-
-class GameObject(Sprite):
- """
- A base game entity object.
- """
-
- hitbox_radius: int
-
- def __init__(
- self,
- xy: tuple[int | float, int | float],
- width_height: tuple[int | float, int | float],
- speed: int | float = 0,
- hitbox_radius: int = 0,
- direction: tuple[int | float, int | float] = (0, 0),
- ):
- super().__init__()
- self.x, self.y = xy
- self.speed = speed
- self.width, self.height = width_height
- self.vx, self.vy = direction
- self.hitbox_radius = hitbox_radius
-
- def update(self, delta: int | float):
- self.x += self.vx * delta * self.speed
- self.y += self.vy * delta * self.speed
-
- self.rect.centerx, self.rect.centery, self.rect.w, self.rect.h = (
- int(self.x),
- int(self.y),
- int(self.width),
- int(self.height),
- )
-
- def collision(self, other) -> bool:
- """Check collision."""
- return (
- math.hypot(self.x - other.x, self.y - other.y)
- < self.hitbox_radius + other.hitbox_radius
- )
-
- def draw(self, graphics: Graphics):
- graphics.draw_sprite(self)
+"""Base game object."""
+
+import math
+
+from vgame.graphics import Graphics, Sprite
+
+
+class GameObject(Sprite):
+ """
+ A base game entity object.
+ """
+
+ hitbox_radius: int
+ vx: int | float
+ vy: int | float
+
+ def __init__(
+ self,
+ xy: tuple[int | float, int | float],
+ width_height: tuple[int | float, int | float],
+ speed: int | float = 0,
+ hitbox_radius: int = 0,
+ direction: tuple[int | float, int | float] = (0, 0),
+ ):
+ super().__init__()
+ self.x, self.y = xy
+ self.speed = speed
+ self.width, self.height = width_height
+ self.vx, self.vy = direction
+ self.hitbox_radius = hitbox_radius
+
+ def update(self, delta: int | float):
+ self.x += self.vx * delta * self.speed
+ self.y += self.vy * delta * self.speed
+
+ self.rect.centerx, self.rect.centery, self.rect.w, self.rect.h = (
+ int(self.x),
+ int(self.y),
+ int(self.width),
+ int(self.height),
+ )
+
+ def collision(self, other: "GameObject") -> bool:
+ """Check collision."""
+ return (
+ math.hypot(self.x - other.x, self.y - other.y)
+ < self.hitbox_radius + other.hitbox_radius
+ )
+
+ def draw(self, graphics: Graphics):
+ graphics.draw_sprite(self)
diff --git a/danmaku/game/level.py b/danmaku/game/level.py
new file mode 100644
index 0000000..b85a60c
--- /dev/null
+++ b/danmaku/game/level.py
@@ -0,0 +1,122 @@
+"""
+
+Game contains levels
+Level contains stages
+Stage contains enemies and/or boss
+
+"""
+
+from typing import Any
+import pygame
+
+from danmaku.game.enemy import Enemy
+
+
+class Stage:
+ """Base stage
+
+ Contains enemies"""
+
+ _enemies: list[Enemy]
+
+ _start_time: int
+ # appearance of enemies can be bound to time
+
+ def __init__(self, enemies: list[Enemy]) -> None:
+ self._enemies = list(enemies)
+
+ self._start_time = pygame.time.get_ticks()
+
+ def update(self) -> None:
+ """WIP
+
+ Update all enemies on the stage"""
+
+ @property
+ def enemies(self) -> list[Enemy]:
+ """Enemies on the stage"""
+ return self._enemies
+
+
+class BossStage(Stage):
+ """Stage centered on boss"""
+
+ # +enemies: list[Enemy]
+ _boss: Enemy
+
+ ## Boss actions ##
+ # Boss actions are bound to time passed from stage start
+ # Actions are being stored in tuples in format:
+ # (time in milliseconds, action, args)
+
+ # ( 1000, "move_to", (10, 20) )
+ # ( 2000, "shoot_radial", () )
+
+ def __init__(
+ self,
+ enemies: list[Enemy],
+ boss: Enemy,
+ actions: list[tuple[int, str, tuple[Any]]],
+ ) -> None:
+ super().__init__(enemies)
+ self._boss = boss
+ self._actions = list(actions)
+
+ def update(self) -> None:
+ if self._actions:
+ if pygame.time.get_ticks() - self._start_time >= self._actions[0][0]:
+ print(f"Function: {self._actions[0][1]}\tArgs: {self._actions[0][2]}")
+ self._actions.pop(0)
+
+ @property
+ def boss(self) -> Enemy:
+ """Boss property"""
+ return self._boss
+
+ @property
+ def enemies(self) -> list[Enemy]:
+ return self._enemies + [self._boss]
+
+
+class Level:
+ """Game level
+
+ Contains stages
+ For chapter separation by theme
+ (can contain only one boss, but with multiple boss stages)"""
+
+ stages: list[Stage]
+ current_stage: int
+
+ enemies: list[Enemy] # enemies from current stage
+
+ def __init__(self, stages: list[Stage]) -> None:
+ self.stages = list(stages)
+ self.current_stage = 0
+ self.enemies = list(self.stages[self.current_stage].enemies)
+
+ def __len__(self) -> int:
+ return len(self.stages)
+
+ def __getitem__(self, index: int) -> Stage:
+ return self.stages[index]
+
+ @property
+ def stage(self) -> Stage:
+ """Current stage"""
+ return self.stages[self.current_stage]
+
+ def next_stage(self) -> bool:
+ """Switch stage to next if possible
+
+ Returns: bool - success"""
+ if len(self.stages) > self.current_stage + 1:
+ self.current_stage += 1
+ self.enemies = list(self.stages[self.current_stage].enemies)
+ return True
+ return False
+
+ def set_stage(self, index: int) -> None:
+ """Set stage by index"""
+ self.current_stage = index
+ self.enemies = list(self.stages[self.current_stage].enemies)
diff --git a/danmaku/pause.py b/danmaku/game/pause.py
similarity index 52%
rename from danmaku/pause.py
rename to danmaku/game/pause.py
index 8cf4bc4..a1d1fd2 100644
--- a/danmaku/pause.py
+++ b/danmaku/game/pause.py
@@ -1,25 +1,26 @@
"""In-game pause menu."""
import vgame
+from danmaku.game.background import Background
+from danmaku.ui.button import Button, Cursor
# pylint: disable=attribute-defined-outside-init, missing-class-docstring
class Pause:
- def load(self, width, height):
+ def load(self, width: int, height: int):
"""Load pause menu."""
self.selection_index = 0
+ self.cursor = Cursor((10, 100))
+
self.width, self.height = width, height
+ self.background_object = Background(0, 0, self.width, self.height, ["menu.png"])
- self.buttons = (
- ("Continue", "continue"),
- ("Settings", "settings"),
- ("Main menu", "menu"),
- )
+ self.buttons = (Button("Continue", "continue"), Button("Main menu", "menu"))
self.exit_status: str = ""
- def update(self, pressed_keys):
+ def update(self, pressed_keys: set[int]):
"""Update pause menu."""
if vgame.Keys.UP in pressed_keys:
pressed_keys.discard(vgame.Keys.UP)
@@ -28,16 +29,23 @@ def update(self, pressed_keys):
pressed_keys.discard(vgame.Keys.DOWN)
self.selection_index = (self.selection_index + 1) % len(self.buttons)
if {vgame.Keys.RETURN, vgame.Keys.Z, vgame.Keys.SPACE} & pressed_keys:
- self.exit_status = self.buttons[self.selection_index][1]
+ self.exit_status = self.buttons[self.selection_index].codename
+ self.cursor.y = 100 + self.selection_index * 50
+ self.cursor.update(0)
def draw(self, graphics: vgame.graphics.Graphics):
"""Draw pause menu."""
- graphics.rectangle((0, 0), (self.width, self.height), (0, 0, 0, 180), alpha=1)
- graphics.text("Danmaku", (0, 10), (255, 255, 180))
+ graphics.draw_sprite(self.background_object)
+ graphics.text("Danmaku", (self.width // 2 - 70, 10), (0, 74, 127))
+
+ self.cursor.draw(graphics)
for i, button in enumerate(self.buttons):
+ selected_color = (0, 74, 127)
+ color = selected_color if i == self.selection_index else (255, 255, 255)
+
graphics.text(
- button[0],
- (0, 100 + i * 50),
- (255, 200, 180) if i == self.selection_index else (255, 255, 255),
+ button.text,
+ (70, 100 + i * 50),
+ color,
)
diff --git a/danmaku/player.py b/danmaku/game/player.py
similarity index 52%
rename from danmaku/player.py
rename to danmaku/game/player.py
index 17dbac7..5b0afd4 100644
--- a/danmaku/player.py
+++ b/danmaku/game/player.py
@@ -1,176 +1,147 @@
-"""Player object declaration."""
-
-import math
-import vgame
-from danmaku.bullet import Bullet
-from danmaku.database import get_player_type
-from danmaku.utils import constrain, Direction
-from danmaku.shooter import Shooter
-from danmaku.animated import Animated
-
-
-class Player(Shooter, Animated):
- """Player object."""
-
- def __init__(
- self, xy: tuple[int | float, int | float], object_type: str, updated_hp=0
- ) -> None:
- args = get_player_type(object_type)
-
- health = updated_hp or args["hp"]
-
- super().__init__(
- xy,
- args["texture_size"],
- args["speed"],
- health,
- args["dm"],
- args["endurance"],
- "basic player bullet",
- 0,
- args["shoot_v"] / 1000,
- )
-
- # Animation
- files = args["texture_file"].split(";")
- self.animation_frames = {
- Direction.LEFT: [],
- Direction.RIGHT: [],
- Direction.UP: [],
- Direction.DOWN: [],
- }
-
- for i in files:
- path = f"/player/{i}"
- if "left" in i:
- self.animation_frames[Direction.LEFT].append(path)
- if "right" in i:
- self.animation_frames[Direction.RIGHT].append(path)
- if "up" in i:
- self.animation_frames[Direction.UP].append(path)
- if "down" in i:
- self.animation_frames[Direction.DOWN].append(path)
-
- Animated.__init__(
- self, xy, args["texture_size"], args["speed"], [], 0, period=0.1
- )
-
- self.texture_file = self.animation_frames[Direction.LEFT][
- self.animation_current
- ]
- self.texture_size = args["texture_size"]
-
- self.my_type = object_type
- self.score = 0
- self.power = 1
-
- self.hitbox_radius = args["hitbox_radius"]
- self.slow = False
-
- # Bounds
- self.left = self.top = 0
- self.right = self.bottom = 10e6
-
- def shoot(self) -> list[Bullet]:
- res: list[Bullet] = []
-
- if self.can_shoot():
-
- bullet = Bullet(
- (self.x, self.y),
- self.damage + self.power,
- self.bullet_type,
- )
-
- res.append(bullet)
-
- if self.power > 4:
- vx = math.cos(math.pi * 85 / 180)
- vy = -math.sin(math.pi * 85 / 180)
-
- b1 = Bullet(
- (self.x, self.y),
- self.damage + self.power,
- self.bullet_type,
- )
- b2 = Bullet(
- (self.x, self.y),
- self.damage + self.power,
- self.bullet_type,
- )
- b1.vx = vx
- b2.vx = -vx
- b1.vy = b2.vy = vy
- res.extend([b1, b2])
-
- return res
-
- def bomb(self) -> list[Bullet]:
- """Spawn bomb, AKA super-bullet"""
- res: list[Bullet] = []
- if self.can_shoot():
- bullet = Bullet(
- (self.x, self.y),
- self.damage + self.power + 50,
- "player bomb",
- )
- res.append(bullet)
- return res
-
- def set_bounds(
- self,
- left: int | float,
- top: int | float,
- right: int | float,
- bottom: int | float,
- ) -> None:
- """Set movement bounds"""
- self.left = left
- self.top = top
- self.right = right
- self.bottom = bottom
-
- def update(self, delta: int | float) -> None:
- speed = self.speed if not self.slow else self.speed * 0.5
-
- self.x += self.vx * delta * speed
- self.x = constrain(
- self.x, self.left + self.width / 2, self.right - self.width / 2
- )
-
- self.y += self.vy * delta * speed
- self.y = constrain(
- self.y, self.top + self.height / 2, self.bottom - self.height / 2
- )
-
- self.rect.centerx, self.rect.centery, self.rect.w, self.rect.h = (
- int(self.x),
- int(self.y),
- int(self.width),
- int(self.height),
- )
-
- def animate(self) -> None:
- """Animate one frame."""
- if self.can_animate():
- direction = None
- if self.vx > 0:
- direction = Direction.RIGHT
- elif self.vy > 0:
- direction = Direction.DOWN
- elif self.vx < 0:
- direction = Direction.LEFT
- elif self.vy < 0:
- direction = Direction.UP
- if direction is not None:
- self.animation_current = (self.animation_current + 1) % len(
- self.animation_frames[direction]
- )
- self.texture_file = self.animation_frames[direction][
- self.animation_current
- ]
-
- def draw(self, graphics: vgame.graphics.Graphics) -> None:
- graphics.draw_sprite(self)
- if self.slow:
- graphics.circle((self.x, self.y), self.hitbox_radius, (200, 0, 200))
+"""Player object declaration."""
+
+import math
+import vgame
+from danmaku.game.bullet import Bullet
+from danmaku.database import get_player_type
+from danmaku.utils import constrain
+from danmaku.game.shooter import Shooter
+from danmaku.game.animated import AnimatedDirectional
+
+
+class Player(Shooter, AnimatedDirectional):
+ """Player object."""
+
+ def __init__(
+ self,
+ xy: tuple[int | float, int | float],
+ object_type: str,
+ bombs: int = 0,
+ lives: int = 1,
+ updated_hp: int = 0,
+ ) -> None:
+ args = get_player_type(object_type)
+
+ health: int | float = updated_hp or args["hp"] * lives
+
+ super().__init__(
+ xy,
+ args["texture_size"],
+ args["speed"],
+ health,
+ args["dm"],
+ args["endurance"],
+ "basic player bullet",
+ 0,
+ args["shoot_v"] / 1000,
+ )
+
+ # Animation
+
+ address = "player"
+
+ AnimatedDirectional.__init__(
+ self,
+ xy,
+ args["texture_size"],
+ args["speed"],
+ [f"{address}/{i}" for i in args["texture_file"].split(";")],
+ period=0.1,
+ )
+
+ self.texture_size = args["texture_size"]
+
+ self.my_type = object_type
+ self.score: int = 0
+ self.power: int = 1
+ self.bombs = bombs
+
+ self.hitbox_radius = args["hitbox_radius"]
+ self.slow = False
+
+ # Bounds
+ self.left = self.top = 0
+ self.right = self.bottom = 10e6
+
+ def shoot(self) -> list[Bullet]:
+ self.shoot_sound(1)
+ res: list[Bullet] = []
+
+ if self.can_shoot():
+
+ bullet = Bullet(
+ (self.x, self.y),
+ self.damage + self.power,
+ self.bullet_type,
+ )
+
+ res.append(bullet)
+
+ if self.power > 4:
+ vx = math.cos(math.pi * 85 / 180)
+ vy = -math.sin(math.pi * 85 / 180)
+
+ b1 = Bullet(
+ (self.x, self.y),
+ self.damage + self.power,
+ self.bullet_type,
+ )
+ b2 = Bullet(
+ (self.x, self.y),
+ self.damage + self.power,
+ self.bullet_type,
+ )
+ b1.vx = vx
+ b2.vx = -vx
+ b1.vy = b2.vy = vy
+ res.extend([b1, b2])
+
+ return res
+
+ def bomb(self) -> list[Bullet]:
+ """Spawn bomb, AKA super-bullet"""
+ self.shoot_sound(1)
+ res: list[Bullet] = []
+ if self.bombs != 0:
+ if self.can_shoot():
+ bullet = Bullet(
+ (self.x, self.y),
+ (self.damage + self.power) * 30,
+ "player bomb",
+ )
+ res.append(bullet)
+ self.bombs -= 1
+ return res
+
+ def set_bounds(
+ self,
+ left: int | float,
+ top: int | float,
+ right: int | float,
+ bottom: int | float,
+ ) -> None:
+ """Set movement bounds"""
+ self.left = left
+ self.top = top
+ self.right = right
+ self.bottom = bottom
+
+ def update(self, delta: int | float) -> None:
+ speed = self.speed if not self.slow else self.speed * 0.5
+
+ self.x += self.vx * delta * speed
+ self.x = constrain(
+ self.x, self.left + self.width / 2, self.right - self.width / 2
+ )
+
+ self.y += self.vy * delta * speed
+ self.y = constrain(
+ self.y, self.top + self.height / 2, self.bottom - self.height / 2
+ )
+
+ super().update(0)
+
+ def draw(self, graphics: vgame.graphics.Graphics) -> None:
+ graphics.draw_sprite(self)
+ if self.slow:
+ graphics.circle((self.x, self.y), self.hitbox_radius, (200, 0, 200))
diff --git a/danmaku/shooter.py b/danmaku/game/shooter.py
similarity index 80%
rename from danmaku/shooter.py
rename to danmaku/game/shooter.py
index 48ea645..67ab43e 100644
--- a/danmaku/shooter.py
+++ b/danmaku/game/shooter.py
@@ -4,8 +4,10 @@
import pygame
-from danmaku.entity import Entity
-from danmaku.bullet import Bullet
+from danmaku.game.entity import Entity
+from danmaku.game.bullet import Bullet
+from danmaku.database import get_settings
+from danmaku.utils import resource_path
class Shooter(Entity):
@@ -59,6 +61,13 @@ def can_shoot(self) -> bool:
return True
return False
+ def shoot_sound(self, channel: int):
+ """Play sound of shooting"""
+ pygame.mixer.init()
+ sound = pygame.mixer.Sound(resource_path("sounds/shoot.wav"))
+ sound.set_volume(get_settings()["sfx_volume"]["value"] / 100)
+ pygame.mixer.Channel(channel).play(sound)
+
@abstractmethod
def shoot(self) -> list[Bullet]:
"""Generate bullets."""
diff --git a/danmaku/main.py b/danmaku/main.py
index 6778de1..94e2528 100644
--- a/danmaku/main.py
+++ b/danmaku/main.py
@@ -2,12 +2,14 @@
from vgame import Runner
-from danmaku.menu import Menu
-from danmaku.game import Game
-from danmaku.history import History
-from danmaku.settings import Settings
+from danmaku.ui.menu import Menu
+from danmaku.game.game import Game
+from danmaku.ui.history import History
+from danmaku.ui.settings import Settings
+from danmaku.ui.game_end import GameEnd
+from danmaku.database import get_settings
-WIDTH, HEIGHT = 300, 500
+WIDTH, HEIGHT = 640, 480
TICKRATE = 120
@@ -16,28 +18,63 @@
def run_game(is_new: bool):
"Game exit handling"
- game = Game(width=WIDTH, height=HEIGHT, title="Danmaku | Game", tickrate=TICKRATE)
+ game = Game(
+ width=WIDTH,
+ height=HEIGHT,
+ title="Danmaku | Game",
+ tickrate=TICKRATE,
+ fullscreen=get_settings()["fullscreen"]["value"],
+ )
game.new_game = is_new
runner.run(game)
match game.exit_status:
case "win":
- # show win screen
- ...
+ end = GameEnd(
+ width=WIDTH,
+ height=HEIGHT,
+ title="Danmaku | Game Over",
+ tickrate=TICKRATE,
+ fullscreen=get_settings()["fullscreen"]["value"],
+ )
+ end.set("You win", (30, 157, 214), "sounds/win.wav")
+ runner.run(end)
case "lose":
- # show lose screen
- ...
+ end = GameEnd(
+ width=WIDTH,
+ height=HEIGHT,
+ title="Danmaku | Game Over",
+ tickrate=TICKRATE,
+ fullscreen=get_settings()["fullscreen"]["value"],
+ )
+ end.set("Game over", (30, 157, 214), "sounds/lose.wav")
+ runner.run(end)
while runner.running:
- menu = Menu(width=WIDTH, height=HEIGHT, title="Danmaku | Menu")
+ menu = Menu(
+ width=WIDTH,
+ height=HEIGHT,
+ title="Danmaku | Menu",
+ fullscreen=get_settings()["fullscreen"]["value"],
+ )
runner.run(menu)
match menu.exit_status:
case "game", new_game:
run_game(new_game)
case "settings":
- settings = Settings(width=WIDTH, height=HEIGHT, title="Danmaku | Settings")
+ settings = Settings(
+ width=WIDTH,
+ height=HEIGHT,
+ title="Danmaku | Settings",
+ fullscreen=get_settings()["fullscreen"]["value"],
+ )
runner.run(settings)
case "history":
- history = History(width=WIDTH, height=HEIGHT, title="Danmaku | History")
+ history = History(
+ width=WIDTH,
+ height=HEIGHT,
+ title="Danmaku | History",
+ fullscreen=get_settings()["fullscreen"]["value"],
+ )
runner.run(history)
diff --git a/danmaku/button.py b/danmaku/ui/button.py
similarity index 81%
rename from danmaku/button.py
rename to danmaku/ui/button.py
index 7b7c458..854628d 100644
--- a/danmaku/button.py
+++ b/danmaku/ui/button.py
@@ -1,12 +1,13 @@
"""Menu button class declaration."""
from dataclasses import dataclass
+from typing import Any
import pygame
-import vgame
+from vgame.graphics import Graphics, Sprite
-class ClickableButton(vgame.graphics.Sprite):
+class ClickableButton(Sprite):
"""Menu button class clickable by mouse"""
def __init__(
@@ -24,7 +25,7 @@ def __init__(
self.text = text
self.font_size = font_size
- def draw(self, graphics: vgame.graphics.Graphics):
+ def draw(self, graphics: Graphics):
graphics.rectangle(self.rect.topleft, self.rect.size, self.button_color)
graphics.text(f" {self.text} ", self.rect.topleft, self.text_color)
@@ -43,10 +44,10 @@ class Button:
codename: str
-class Cursor(vgame.graphics.Sprite):
+class Cursor(Sprite):
"""Cursor class"""
- def __init__(self, xy):
+ def __init__(self, xy: tuple[int, int]):
super().__init__()
self.x, self.y = xy
self.width, self.height = 42, 40
@@ -61,12 +62,16 @@ def update(self, delta: int | float):
int(self.height),
)
- def draw(self, graphics: vgame.graphics.Graphics):
+ def draw(self, graphics: Graphics):
graphics.draw_sprite(self)
class SettingsValue:
- def __init__(self, name: str, display_name: str, possible_values, value) -> None:
+ """An interactive value bar for settings"""
+
+ def __init__(
+ self, name: str, display_name: str, possible_values: tuple[Any], value: Any
+ ) -> None:
self.codename = name
self.text = display_name
self.possible_values = possible_values
@@ -74,9 +79,11 @@ def __init__(self, name: str, display_name: str, possible_values, value) -> None
self.selection_index = self.possible_values.index(self.value)
def increase(self):
+ """Select next value"""
self.selection_index = (self.selection_index + 1) % len(self.possible_values)
self.value = self.possible_values[self.selection_index]
def decrease(self):
+ """Select previous value"""
self.selection_index = (self.selection_index - 1) % len(self.possible_values)
self.value = self.possible_values[self.selection_index]
diff --git a/danmaku/ui/game_end.py b/danmaku/ui/game_end.py
new file mode 100644
index 0000000..e244a73
--- /dev/null
+++ b/danmaku/ui/game_end.py
@@ -0,0 +1,53 @@
+"""Main menu scene."""
+
+import pygame
+import vgame
+
+from danmaku.database import get_settings
+from danmaku.utils import resource_path
+
+
+# pylint: disable=attribute-defined-outside-init, missing-class-docstring
+class GameEnd(vgame.Scene):
+ def load(self):
+ self.graphics.library.path = resource_path("textures")
+
+ pygame.mixer.init()
+ pygame.mixer.music.set_volume(get_settings()["music_volume"]["value"] / 100)
+ pygame.mixer.music.load(resource_path(self.music))
+ pygame.mixer.music.play()
+
+ def set(self, text: str, background: tuple[int, int, int], music: str):
+ """Set end screen attributes"""
+ self.text = text
+ if "win" in text:
+ self.delta_color = 2
+ else:
+ self.delta_color = 0
+ self.background_color = background
+ self.text_color: list[int] = list(background)
+ self.v = (255 - self.text_color[self.delta_color]) / 500
+ self.music = music
+
+ def update(self):
+ for i in range(3):
+ if i == self.delta_color:
+ self.text_color[i] += self.v
+ else:
+ if self.text_color[i] - self.v > 0:
+ self.text_color[i] -= self.v
+ if self.text_color[self.delta_color] + self.v >= 255:
+ pygame.time.wait(1000)
+ self.stop()
+
+ def draw(self) -> None:
+ self.graphics.rectangle(
+ (0, 0), (self.width, self.height), self.background_color
+ )
+
+ # NOTE: update vgame to version 1.6.4
+ self.graphics.text(
+ self.text, (30, self.height // 3), tuple(self.text_color), font_size=90
+ )
+
+ def exit(self): ...
diff --git a/danmaku/history.py b/danmaku/ui/history.py
similarity index 77%
rename from danmaku/history.py
rename to danmaku/ui/history.py
index bfd1e10..fa63702 100644
--- a/danmaku/history.py
+++ b/danmaku/ui/history.py
@@ -10,7 +10,7 @@ class History(vgame.Scene):
def load(self):
self.selection_index = 0
- self.history = get_game_history()
+ self.history: list[dict[str, int]] = get_game_history()
self.record_count = len(self.history)
def update(self):
@@ -27,12 +27,14 @@ def update(self):
self.stop()
def draw(self):
- self.graphics.text("History", (0, 0), (255, 255, 180))
+ self.graphics.rectangle((0, 0), (self.width, self.height), (30, 157, 214, 180))
+
+ self.graphics.text("History", (self.width // 2 - 30, 0), (0, 74, 127))
for i, game in enumerate(self.history[self.selection_index :]):
self.graphics.text(
- f"Level: {game['level'] + 1}, Score: {game['score']}",
- (0, 100 + 50 * i),
+ f"Level: {game['level'] + 1}, Score: {game['score']}, Time: {game['time']}",
+ (20, 50 + 50 * i),
(255, 255, 255),
)
diff --git a/danmaku/menu.py b/danmaku/ui/menu.py
similarity index 79%
rename from danmaku/menu.py
rename to danmaku/ui/menu.py
index 91b4ade..a0a79fa 100644
--- a/danmaku/menu.py
+++ b/danmaku/ui/menu.py
@@ -3,9 +3,10 @@
import pygame
import vgame
-from danmaku.database import get_saved_objects
-from danmaku.button import Button, Cursor
+from danmaku.database import get_saved_objects, get_settings
+from danmaku.ui.button import Button, Cursor
from danmaku.utils import resource_path
+from danmaku.game.background import Background
# pylint: disable=attribute-defined-outside-init, missing-class-docstring
@@ -13,8 +14,15 @@ class Menu(vgame.Scene):
def load(self):
self.graphics.library.path = resource_path("textures")
+ pygame.mixer.init()
+ pygame.mixer.music.set_volume(get_settings()["music_volume"]["value"] / 100)
+ pygame.mixer.music.load(resource_path("sounds/menu.wav"))
+ pygame.mixer.music.play(loops=-1)
+
self.selection_index = 0
+ self.background_object = Background(0, 0, self.width, self.height, ["menu.png"])
+
self.buttons = (
Button("New game", "new_game"),
Button("Continue", "continue"),
@@ -56,16 +64,20 @@ def update(self):
case "quit":
# Maybe rework to quit through exit status
pygame.event.post(pygame.event.Event(pygame.constants.QUIT))
+ case _:
+ pass
self.cursor.y = 100 + self.selection_index * 50
self.cursor.update(self.delta)
def draw(self):
- self.graphics.text("Danmaku", (0, 10), (255, 255, 180))
+ self.graphics.draw_sprite(self.background_object)
+
+ self.graphics.text("Danmaku", (self.width // 2 - 70, 10), (0, 74, 127))
self.cursor.draw(self.graphics)
for i, button in enumerate(self.buttons):
- selected_color = (180, 255, 255)
+ selected_color = (0, 74, 127)
if button.codename == "continue":
if not get_saved_objects():
selected_color = (255, 100, 100)
diff --git a/danmaku/settings.py b/danmaku/ui/settings.py
similarity index 84%
rename from danmaku/settings.py
rename to danmaku/ui/settings.py
index 760935b..bb3312f 100644
--- a/danmaku/settings.py
+++ b/danmaku/ui/settings.py
@@ -4,7 +4,7 @@
from danmaku.database import get_settings
-from danmaku.button import SettingsValue, Button
+from danmaku.ui.button import SettingsValue, Button
from danmaku.database.database import set_settings
@@ -15,9 +15,7 @@ def load(self):
settings_dict = get_settings()
- self.buttons: list[object] = [
- SettingsValue("music_volume", "Music Volume", (0, 25, 50, 75, 100), 50),
- ]
+ self.buttons: list[object] = []
for key, value in settings_dict.items():
self.buttons.append(
@@ -52,23 +50,25 @@ def update(self):
self.stop()
def draw(self):
- self.graphics.text("Danmaku", (0, 10), (255, 255, 180))
+ self.graphics.rectangle((0, 0), (self.width, self.height), (30, 157, 214, 180))
+
+ self.graphics.text("Settings", (self.width // 2 - 40, 0), (0, 74, 127))
for i, button in enumerate(self.buttons):
- color = (255, 200, 180) if i == self.selection_index else (255, 255, 255)
+ color = (0, 74, 127) if i == self.selection_index else (255, 255, 255)
if isinstance(button, Button):
self.graphics.text(
button.text,
- (0, 100 + i * 50),
+ (20, 100 + i * 50),
color,
)
if isinstance(button, SettingsValue):
self.graphics.text(
f"{button.text}: < {button.value} >",
- (0, 100 + i * 50),
+ (20, 100 + i * 50),
color,
)
diff --git a/danmaku/utils.py b/danmaku/utils.py
index 4cadd11..5fd61d9 100644
--- a/danmaku/utils.py
+++ b/danmaku/utils.py
@@ -25,7 +25,7 @@ def not_in_border(
return True
-def resource_path(relative_path) -> str:
+def resource_path(relative_path: str) -> str:
"""Get absolute path to resource, works for dev and for PyInstaller"""
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
@@ -51,3 +51,12 @@ class Direction(IntEnum):
RIGHT = 1
UP = 2
DOWN = 3
+ STATIC = 4
+
+
+# def str_to_stage(stage_str: str) -> Stage: ...
+
+
+# def str_to_level(level_str: str) -> Level:
+# """Convert level string to level object."""
+# ...
diff --git a/docs/levels_prototoype.json b/docs/levels_prototoype.json
new file mode 100644
index 0000000..ea07e32
--- /dev/null
+++ b/docs/levels_prototoype.json
@@ -0,0 +1,26 @@
+{
+ "levels": [
+ {
+ "stages": [
+ {
+ "type": "stage",
+ "enemies": [
+ {
+ "type": "basic enemy",
+ "scheme": [
+ [1000, "appear", null],
+ [1500, "shoot", null]
+ ]
+ },
+ {
+ "type": "basic enemy",
+ "scheme": [
+ [1000, "appear", null]
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/mecs.md b/docs/mecs.md
index 97bb771..be57081 100644
--- a/docs/mecs.md
+++ b/docs/mecs.md
@@ -8,8 +8,8 @@
- Player
- Enemies
-
## Player movement
+
```
-1
^
@@ -19,10 +19,10 @@
v
1
```
+
Player has two axis (x, y) which vary from -1 to 1.
Player also has speed module (pixels / second).
-Player's move distance calculation is (speed module) * (axis) * (time delta)
-
+Player's move distance calculation is (speed module) _ (axis) _ (time delta)
## Scenes
@@ -45,14 +45,12 @@ graph LR
As we can see, all other scenes return to the main menu
-
-
## Position, hitboxes
-
Position of an object is a position of it's center point
-*placement:*
+_placement:_
+
```
/-----\
| |
@@ -60,11 +58,13 @@ Position of an object is a position of it's center point
| |
\-----/
```
+
(where the star is at)
**NOT** coordinates of left top corner
-*wrong placement:*
+_wrong placement:_
+
```
*-----\
| |
@@ -87,6 +87,9 @@ classDiagram
GameObject <-- Animated
Animated <-- Background
Entity <-- Shooter
+ GameObject <-- Drop
+ Drop <-- Points
+ Drop <-- PowerUp
class Sprite {
texture_file: str
@@ -94,8 +97,12 @@ classDiagram
rect: Rect
}
class GameObject {
- x: int
- y: int
+ x, y: int
+ vx, vy: float [-1;1]
+ width, height: int
+ hitbox_radius: int
+ speed: int
+
update()
draw()
collision() -> bool
@@ -103,31 +110,279 @@ classDiagram
class Entity {
health: int
damage: int
+ endurance: float
+
get_damage()
}
class Shooter {
shoot_freq: float
last_shot: float
+
can_shoot() -> bool
- shoot()
+ shoot() -> list[Bullet]
}
class Player {
+ player_type: str
score: int
power: int
}
class Enemy {
+ enemy_type: str
cost: int
}
class Animated {
animation_frames: list[str]
animation_current: int
animation_period: float
+
+ can_animate() -> bool
animate()
}
class Bullet {
-
+ bullet_type: str
}
class Background {
}
+ class Drop {
+
+ }
+ class PowerUp {
+
+ }
+ class Points
+```
+
+## Enemies' actions processing
+
+```mermaid
+graph TB
+
+A([Start])
+B["Merge all actions (with links to objects)"]
+C[Sort by timing]
+E{Is it time yet?}
+F[Execute action]
+G[Remove from queue]
+H{While actions left}
+Z([End])
+
+A --> B
+B --> C
+C --> H
+
+subgraph LOOP
+ direction TB
+
+ D[Pick first]
+
+ D --> E
+ E --> |Yes| F
+ E --> |No| D
+ F --> G
+end
+
+
+LOOP --> H
+H --> |Yes| LOOP
+H --> |No| Z
+
+```
+
+## File hierarchy (import diagram)
+
+
+ Old
+
+ ```mermaid
+ %%{init: {"flowchart": {"curve": "basis"}} }%%
+ graph TB
+
+ GAME("game.py")
+ MAIN("main.py")
+
+
+ animated --> gameobject
+
+ background --> animated
+
+ bullet --> entity
+ bullet --> database
+
+ button
+
+ drop --> gameobject
+
+ enemy --> shooter
+ enemy --> database
+ enemy --> animated
+ enemy --> bullet
+ enemy --> drop
+
+ entity --> gameobject
+
+ GAME --> enemy
+ GAME --> player
+ GAME --> level
+ GAME --> database
+ GAME --> drop
+ GAME --> background
+ GAME --> pause
+ GAME --> utils
+ GAME --> bullet
+
+ gameobject
+
+ history --> database
+
+ level --> enemy
+
+ MAIN --> GAME
+ MAIN --> menu
+ MAIN --> settings
+ MAIN --> history
+
+ menu --> background
+ menu --> button
+ menu --> database
+ menu --> utils
+
+ pause
+
+ player --> shooter
+ player --> database
+ player --> animated
+ player --> bullet
+ player --> utils
+
+ settings --> button
+ settings --> database
+
+
+ shooter --> entity
+ shooter --> bullet
+
+ utils
+
+ subgraph S_UI
+ direction TB
+ end
+
+ subgraph S_GAME
+ direction TB
+ end
+
+ ```
+
+
+
+---
+
+
+New
+
+```mermaid
+graph TB
+
+ main --> game
+ main --> menu
+ main --> settings
+ main --> history
+
+ animated --> gameobject
+
+ background --> animated
+
+ bullet --> entity
+ bullet --> database
+
+ drop --> gameobject
+
+ enemy --> animated
+ enemy --> bullet
+ enemy --> drop
+ enemy --> shooter
+ enemy --> database
+
+ entity --> gameobject
+
+ game --> background
+ game --> bullet
+ game --> database
+ game --> drop
+ game --> enemy
+ game --> level
+ game --> pause
+ game --> player
+ game --> utils
+
+ player --> animated
+ player --> database
+ player --> bullet
+ player --> shooter
+ player --> utils
+
+ shooter --> bullet
+ shooter --> entity
+
+ level --> enemy
+
+ history --> database
+
+ settings --> database
+ settings --> button
+
+ menu --> database
+ menu --> button
+ menu --> utils
+
+ subgraph S_UI
+ direction TB
+ button
+ history
+ menu
+ settings
+ end
+
+ subgraph S_GAME
+ direction TB
+ animated
+ background
+ bullet
+ drop
+ enemy
+ entity
+ game
+ pause
+ player
+ shooter
+ level
+ gameobject
+ end
+```
+
+
+
+## Levels
+
+
+
+```mermaid
+graph LR
+
+B([Все враги исчезли])
+C{Остались волны?}
+D[Новая волна]
+E{Остались уровни?}
+F[Следующий уровень]
+G[Победа]
+
+B --> C
+
+C --> |да| D
+C --> |нет| E
+
+E --> |да| F
+E --> |нет| G
+
```
\ No newline at end of file
diff --git a/docs/presentation.pptx b/docs/presentation.pptx
index e03d94a..bb81b6d 100644
Binary files a/docs/presentation.pptx and b/docs/presentation.pptx differ
diff --git a/poetry.lock b/poetry.lock
index e225afb..26f0907 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -201,7 +201,7 @@ testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jar
[[package]]
name = "vgame"
-version = "1.6.0"
+version = "1.6.4"
description = ""
optional = false
python-versions = "^3.10"
@@ -215,7 +215,7 @@ pygame = "^2.5.2"
type = "git"
url = "https://github.com/virashu/pygametest"
reference = "HEAD"
-resolved_reference = "e9093767c0f93afacae45c1be1d7cafe233483bc"
+resolved_reference = "aeeece542a60aa0594cab01f2a4a20489d93a585"
[metadata]
lock-version = "2.0"
diff --git a/pyproject.toml b/pyproject.toml
index 5ac362f..4d5bf37 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "danmaku"
-version = "0.11.0"
+version = "1.0.0"
description = ""
authors = ["Vlad <89295404+Virashu@users.noreply.github.com>"]
readme = "README.md"
diff --git a/requirements.txt b/requirements.txt
index 8094c62..2017619 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-peewee==3.17.0 ; python_version >= "3.10" and python_version < "3.13"
+peewee==3.17.1 ; python_version >= "3.10" and python_version < "3.13"
pygame==2.5.2 ; python_version >= "3.10" and python_version < "3.13"
vgame @ git+https://github.com/virashu/pygametest ; python_version >= "3.10" and python_version < "3.13"