diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..453feee --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,47 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + # - name: Install dependencies + # run: | + # python -m pip install --upgrade pip + # pip install pylint + # - name: Analysing the code with pylint + # run: | + # pylint $(git ls-files '*.py') + - name: PyLint with dynamic badge + # You may pin to the exact commit or the version. + # uses: Silleellie/pylint-github-action@f5341ef210a203c2c7bbfe5440c03a06b9328866 + 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 + # 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 + # requirements-path: # optional, default is requirements.txt + # The path, relative to the root of the repo, of the README.md to update with the pylint badge + # readme-path: # optional, default is README.md + # Text to display in the badge + # badge-text: # optional, default is PyLint + # Color of the badge for pylint scores < 5. Hex, rgb, rgba, hsl, hsla and css named colors can all be used + #color-bad-score: # optional, default is red + # Color of the badge for pylint scores in range [5,8). Hex, rgb, rgba, hsl, hsla and css named colors can all be used + #color-ok-score: # optional, default is orange + # Color of the badge for pylint scores in range [8,10). Hex, rgb, rgba, hsl, hsla and css named colors can all be used + #color-good-score: # optional, default is yellow + # Color of the badge for pylint scores == 10. Hex, rgb, rgba, hsl, hsla and css named colors can all be used + #color-perfect-score: # optional, default is brightgreen + diff --git a/.gitignore b/.gitignore index 354ae1c..df8d867 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +_tools + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index e8e1df1..62253b2 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,37 @@ -# 弾幕 (Danmaku) - - -## Goal -To create a bullet hell game similar to TouHou Project, Undertale, etc. - -## Refactoring -- [x] main.py -- [x] bullet.py -- [x] enemy.py -- [x] gameobject.py -- [x] player.py -- [x] utils.py - - -## TODO -- [x] Levels -- [ ] Boss HP bar -- [ ] Player HP/Bomb info -- [ ] Player points info -- [ ] Main menu -- [ ] Leaderboard -- [x] Sounds -- [x] Music -- [x] Graphics - - [x] Images - - [ ] Level background - - [ ] Effects (particles) -- [x] Bullets - - [ ] Trajectories -- [x] Enemies -- [x] Controls - - [x] Change controls to classic (shift, z, x) -- [ ] Settings -- [ ] Make player hitbox smaller -- [ ] Replace enemies strings with enum -- [ ] Replace resource path strings with constants from db +# 弾幕 (Danmaku) +![pylint](https://img.shields.io/badge/PyLint-9.79-yellow?logo=python&logoColor=white) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +## Goal +To create a bullet hell game similar to TouHou Project, Undertale, etc. + +## Refactoring +- [x] main.py +- [x] bullet.py +- [x] enemy.py +- [x] gameobject.py +- [x] player.py +- [x] utils.py + + +## TODO +- [x] Levels +- [ ] Boss HP bar +- [x] Player HP/Bomb info +- [x] Player points info +- [x] Main menu +- [x] Leaderboard +- [x] Sounds +- [x] Music +- [x] Graphics + - [x] Images + - [x] Level background + - [ ] Effects (particles) +- [x] Bullets + - [ ] Trajectories +- [x] Enemies +- [x] Controls + - [x] Change controls to classic (shift, z, x) +- [ ] Settings +- [ ] Make player hitbox smaller +- [x] Replace resource path strings with constants from db diff --git a/assets/DataBase.db b/assets/DataBase.db index 7e473cc..c7769f5 100644 Binary files a/assets/DataBase.db and b/assets/DataBase.db differ diff --git a/assets/textures/background/background_0.png b/assets/textures/background/background_0.png new file mode 100644 index 0000000..100d8d6 Binary files /dev/null and b/assets/textures/background/background_0.png differ diff --git a/assets/textures/background/background_1.png b/assets/textures/background/background_1.png new file mode 100644 index 0000000..783f861 Binary files /dev/null and b/assets/textures/background/background_1.png differ diff --git a/assets/textures/background/background_10.png b/assets/textures/background/background_10.png new file mode 100644 index 0000000..72406a4 Binary files /dev/null and b/assets/textures/background/background_10.png differ diff --git a/assets/textures/background/background_11.png b/assets/textures/background/background_11.png new file mode 100644 index 0000000..111fe23 Binary files /dev/null and b/assets/textures/background/background_11.png differ diff --git a/assets/textures/background/background_12.png b/assets/textures/background/background_12.png new file mode 100644 index 0000000..829f363 Binary files /dev/null and b/assets/textures/background/background_12.png differ diff --git a/assets/textures/background/background_13.png b/assets/textures/background/background_13.png new file mode 100644 index 0000000..bd9bb4a Binary files /dev/null and b/assets/textures/background/background_13.png differ diff --git a/assets/textures/background/background_14.png b/assets/textures/background/background_14.png new file mode 100644 index 0000000..dedbbba Binary files /dev/null and b/assets/textures/background/background_14.png differ diff --git a/assets/textures/background/background_15.png b/assets/textures/background/background_15.png new file mode 100644 index 0000000..b4204c4 Binary files /dev/null and b/assets/textures/background/background_15.png differ diff --git a/assets/textures/background/background_16.png b/assets/textures/background/background_16.png new file mode 100644 index 0000000..63a4965 Binary files /dev/null and b/assets/textures/background/background_16.png differ diff --git a/assets/textures/background/background_17.png b/assets/textures/background/background_17.png new file mode 100644 index 0000000..3abac7b Binary files /dev/null and b/assets/textures/background/background_17.png differ diff --git a/assets/textures/background/background_18.png b/assets/textures/background/background_18.png new file mode 100644 index 0000000..40ff340 Binary files /dev/null and b/assets/textures/background/background_18.png differ diff --git a/assets/textures/background/background_19.png b/assets/textures/background/background_19.png new file mode 100644 index 0000000..a410a00 Binary files /dev/null and b/assets/textures/background/background_19.png differ diff --git a/assets/textures/background/background_2.png b/assets/textures/background/background_2.png new file mode 100644 index 0000000..abcd7dc Binary files /dev/null and b/assets/textures/background/background_2.png differ diff --git a/assets/textures/background/background_20.png b/assets/textures/background/background_20.png new file mode 100644 index 0000000..1684369 Binary files /dev/null and b/assets/textures/background/background_20.png differ diff --git a/assets/textures/background/background_21.png b/assets/textures/background/background_21.png new file mode 100644 index 0000000..6e4603e Binary files /dev/null and b/assets/textures/background/background_21.png differ diff --git a/assets/textures/background/background_22.png b/assets/textures/background/background_22.png new file mode 100644 index 0000000..d1b03d1 Binary files /dev/null and b/assets/textures/background/background_22.png differ diff --git a/assets/textures/background/background_23.png b/assets/textures/background/background_23.png new file mode 100644 index 0000000..40f061e Binary files /dev/null and b/assets/textures/background/background_23.png differ diff --git a/assets/textures/background/background_24.png b/assets/textures/background/background_24.png new file mode 100644 index 0000000..c638588 Binary files /dev/null and b/assets/textures/background/background_24.png differ diff --git a/assets/textures/background/background_25.png b/assets/textures/background/background_25.png new file mode 100644 index 0000000..4a34a89 Binary files /dev/null and b/assets/textures/background/background_25.png differ diff --git a/assets/textures/background/background_26.png b/assets/textures/background/background_26.png new file mode 100644 index 0000000..08421ec Binary files /dev/null and b/assets/textures/background/background_26.png differ diff --git a/assets/textures/background/background_27.png b/assets/textures/background/background_27.png new file mode 100644 index 0000000..4471dac Binary files /dev/null and b/assets/textures/background/background_27.png differ diff --git a/assets/textures/background/background_28.png b/assets/textures/background/background_28.png new file mode 100644 index 0000000..9153847 Binary files /dev/null and b/assets/textures/background/background_28.png differ diff --git a/assets/textures/background/background_29.png b/assets/textures/background/background_29.png new file mode 100644 index 0000000..2b60868 Binary files /dev/null and b/assets/textures/background/background_29.png differ diff --git a/assets/textures/background/background_3.png b/assets/textures/background/background_3.png new file mode 100644 index 0000000..03df0fd Binary files /dev/null and b/assets/textures/background/background_3.png differ diff --git a/assets/textures/background/background_30.png b/assets/textures/background/background_30.png new file mode 100644 index 0000000..0689931 Binary files /dev/null and b/assets/textures/background/background_30.png differ diff --git a/assets/textures/background/background_31.png b/assets/textures/background/background_31.png new file mode 100644 index 0000000..ac27830 Binary files /dev/null and b/assets/textures/background/background_31.png differ diff --git a/assets/textures/background/background_32.png b/assets/textures/background/background_32.png new file mode 100644 index 0000000..74393a6 Binary files /dev/null and b/assets/textures/background/background_32.png differ diff --git a/assets/textures/background/background_33.png b/assets/textures/background/background_33.png new file mode 100644 index 0000000..1d272e7 Binary files /dev/null and b/assets/textures/background/background_33.png differ diff --git a/assets/textures/background/background_34.png b/assets/textures/background/background_34.png new file mode 100644 index 0000000..51b94c5 Binary files /dev/null and b/assets/textures/background/background_34.png differ diff --git a/assets/textures/background/background_35.png b/assets/textures/background/background_35.png new file mode 100644 index 0000000..d5f48c2 Binary files /dev/null and b/assets/textures/background/background_35.png differ diff --git a/assets/textures/background/background_36.png b/assets/textures/background/background_36.png new file mode 100644 index 0000000..754b1b1 Binary files /dev/null and b/assets/textures/background/background_36.png differ diff --git a/assets/textures/background/background_37.png b/assets/textures/background/background_37.png new file mode 100644 index 0000000..4a7755d Binary files /dev/null and b/assets/textures/background/background_37.png differ diff --git a/assets/textures/background/background_38.png b/assets/textures/background/background_38.png new file mode 100644 index 0000000..dccbc83 Binary files /dev/null and b/assets/textures/background/background_38.png differ diff --git a/assets/textures/background/background_39.png b/assets/textures/background/background_39.png new file mode 100644 index 0000000..82379f7 Binary files /dev/null and b/assets/textures/background/background_39.png differ diff --git a/assets/textures/background/background_4.png b/assets/textures/background/background_4.png new file mode 100644 index 0000000..644d155 Binary files /dev/null and b/assets/textures/background/background_4.png differ diff --git a/assets/textures/background/background_40.png b/assets/textures/background/background_40.png new file mode 100644 index 0000000..5507ddf Binary files /dev/null and b/assets/textures/background/background_40.png differ diff --git a/assets/textures/background/background_41.png b/assets/textures/background/background_41.png new file mode 100644 index 0000000..79b2c53 Binary files /dev/null and b/assets/textures/background/background_41.png differ diff --git a/assets/textures/background/background_42.png b/assets/textures/background/background_42.png new file mode 100644 index 0000000..d34b7ff Binary files /dev/null and b/assets/textures/background/background_42.png differ diff --git a/assets/textures/background/background_43.png b/assets/textures/background/background_43.png new file mode 100644 index 0000000..b25cbf5 Binary files /dev/null and b/assets/textures/background/background_43.png differ diff --git a/assets/textures/background/background_44.png b/assets/textures/background/background_44.png new file mode 100644 index 0000000..5ea8219 Binary files /dev/null and b/assets/textures/background/background_44.png differ diff --git a/assets/textures/background/background_45.png b/assets/textures/background/background_45.png new file mode 100644 index 0000000..9f1dcf4 Binary files /dev/null and b/assets/textures/background/background_45.png differ diff --git a/assets/textures/background/background_46.png b/assets/textures/background/background_46.png new file mode 100644 index 0000000..a037377 Binary files /dev/null and b/assets/textures/background/background_46.png differ diff --git a/assets/textures/background/background_47.png b/assets/textures/background/background_47.png new file mode 100644 index 0000000..2bf49bc Binary files /dev/null and b/assets/textures/background/background_47.png differ diff --git a/assets/textures/background/background_5.png b/assets/textures/background/background_5.png new file mode 100644 index 0000000..f535aac Binary files /dev/null and b/assets/textures/background/background_5.png differ diff --git a/assets/textures/background/background_6.png b/assets/textures/background/background_6.png new file mode 100644 index 0000000..e3cf67f Binary files /dev/null and b/assets/textures/background/background_6.png differ diff --git a/assets/textures/background/background_7.png b/assets/textures/background/background_7.png new file mode 100644 index 0000000..ce5a4c5 Binary files /dev/null and b/assets/textures/background/background_7.png differ diff --git a/assets/textures/background/background_8.png b/assets/textures/background/background_8.png new file mode 100644 index 0000000..b894d32 Binary files /dev/null and b/assets/textures/background/background_8.png differ diff --git a/assets/textures/background/background_9.png b/assets/textures/background/background_9.png new file mode 100644 index 0000000..86f8a79 Binary files /dev/null and b/assets/textures/background/background_9.png differ diff --git a/danmaku.spec b/danmaku.spec index 6c79544..eb73ce1 100644 --- a/danmaku.spec +++ b/danmaku.spec @@ -27,7 +27,7 @@ exe = EXE( bootloader_ignore_signals=False, strip=False, upx=True, - console=True, + console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, diff --git a/danmaku/background.py b/danmaku/background.py new file mode 100644 index 0000000..0ae2e09 --- /dev/null +++ b/danmaku/background.py @@ -0,0 +1,47 @@ +"""Game level background class declaration.""" + +import vgame +import pygame + + +class Background(vgame.graphics.Sprite): + """Game level background class.""" + + def __init__( + self, + x: int | float, + y: int | float, + width: int | float, + height: int | float, + ): + super().__init__() + + self.x = x + self.y = y + self.width = width + self.height = height + + # Making it animated + self.last_animation_time = 0 + self.current_frame = 0 + self.frame_count = 48 + self.frame_duration = 100 + self.frames = [ + f"background/background_{i}.png" for i in range(self.frame_count) + ] + self.texture_file = self.frames[0] + self.texture_size = self.width, self.height + + def draw(self, graphics: vgame.graphics.Graphics): + graphics.draw_sprite(self) + + def animation(self): + """Animate the sprite.""" + t = pygame.time.get_ticks() + if t - self.last_animation_time >= self.frame_duration: + self.texture_file = self.frames[self.current_frame] + self.current_frame = (self.current_frame + 1) % self.frame_count + self.last_animation_time = t + + def update(self, delta: int | float): + self.animation() diff --git a/danmaku/bullet.py b/danmaku/bullet.py index 2584ee3..14c4e87 100644 --- a/danmaku/bullet.py +++ b/danmaku/bullet.py @@ -1,10 +1,18 @@ +"""Entity's bullet declaration.""" + +import vgame + from danmaku.gameobject import GameObject from danmaku.database import get_bullet_type class Bullet(GameObject): - def __init__(self, xy: tuple[int | float, int | float], damage: int | float, type): - args = get_bullet_type(type) + """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, (2 * args["radius"], 2 * args["radius"]), args["speed"], 0, damage, 1 ) @@ -14,7 +22,7 @@ def __init__(self, xy: tuple[int | float, int | float], damage: int | float, typ self.texture_file = args["texture_file"] self.texture_size = (2 * self.r, 2 * self.r) - self.my_type = type + self.my_type = object_type def update(self, delta: int | float): self.x += self.vx * delta * self.speed @@ -25,3 +33,11 @@ def update(self, delta: int | float): self.width, self.height, ) + + def collision(self, other) -> bool: + # Need to change this because of bombs + # (The bullets that can damage bullets) + return other.collision(self) + + def draw(self, graphics: vgame.graphics.Graphics): + graphics.draw_sprite(self) diff --git a/danmaku/button.py b/danmaku/button.py index 88fae87..113210a 100644 --- a/danmaku/button.py +++ b/danmaku/button.py @@ -1,8 +1,12 @@ +"""Menu button class declaration.""" + import pygame import vgame class Button(vgame.graphics.Sprite): + """Menu button class.""" + def __init__( self, coords: tuple[int | float, int | float], @@ -13,6 +17,7 @@ def __init__( text_color: tuple[int, int, int] = (0, 0, 0), font_size: int = 24, ): + super().__init__() self.x, self.y = coords self.set_rect(pygame.Rect(coords, (width, height))) self.button_color = button_color @@ -26,6 +31,8 @@ def draw(self, graphics: vgame.graphics.Graphics): ) graphics.text(self.text, (self.x, self.y), self.text_color) - def is_clicked(self, mouse_pos): - if self.rect.collidepoint(mouse_pos): - return True + def is_clicked(self, mouse_pos: tuple[int | float, int | float]) -> bool: + """Check if button is being clicked.""" + return self.rect.collidepoint(mouse_pos) + + def update(self, delta: int | float): ... diff --git a/danmaku/database/construct.py b/danmaku/database/construct.py index 4a5b980..81326a7 100644 --- a/danmaku/database/construct.py +++ b/danmaku/database/construct.py @@ -1,11 +1,19 @@ -from danmaku.database.models import * +"""Construct database with default values. + +To be used in development process.""" + +from danmaku.database.models import db, EnemyTypes, BulletTypes, PlayerTypes db.connect() + +# +# EnemyTypes +# + db.drop_tables([EnemyTypes]) db.create_tables([EnemyTypes]) - basic_enemy = EnemyTypes.create( name="basic enemy", texture_file="basic_enemy_2.png;basic_enemy_1.png;" @@ -51,6 +59,12 @@ ) boss.save() +# +# BulletTypes +# + +db.drop_tables([BulletTypes]) +db.create_tables([BulletTypes]) basic_enemy_bullet = BulletTypes.create( name="basic enemy bullet", @@ -68,12 +82,19 @@ enemy=False, texture_file="bullet.png", radius=10, - speed=150, + speed=300, vx=0, vy=-1, ) basic_player_bullet.save() +# +# PlayerTypes +# + +db.drop_tables([PlayerTypes]) +db.create_tables([PlayerTypes]) + player = PlayerTypes.create( name="player", texture_file="player_idle_left.png;player_left_1.png;player_left_2.png;player_left_3.png;" diff --git a/danmaku/database/database.py b/danmaku/database/database.py index 9668e1b..eff2ee9 100644 --- a/danmaku/database/database.py +++ b/danmaku/database/database.py @@ -1,7 +1,21 @@ -from danmaku.database.models import * +"""Functions for work with database.""" +from typing import Iterable +from danmaku.database.models import ( + db, + BulletTypes, + EnemyTypes, + PlayerTypes, + SavedObjects, + SavedGame, +) -def get_enemy_type(name): + +def get_enemy_type(name: str) -> dict: + """ + Get enemy parameters by name + Returns dict: {"texture_file", "texture_size", "speed", "shoot_v", "hp", "dm", "endurance"} + """ with db.atomic(): a = EnemyTypes.get(EnemyTypes.name == name) return { @@ -16,7 +30,11 @@ def get_enemy_type(name): } -def get_player_type(name): +def get_player_type(name) -> dict: + """ + Get player parameters by name + Returns dict: {"texture_file", "texture_size", "speed", "shoot_v", "hp", "dm", "endurance"} + """ a = PlayerTypes.get(PlayerTypes.name == name) return { "texture_file": a.texture_file, @@ -44,7 +62,8 @@ def get_bullet_type(name: str) -> dict: } -def get_saved_objects(): +def get_saved_objects() -> list: + """Get saved objects from database""" objects = [] for el in SavedObjects.select(): objects.append( @@ -62,7 +81,10 @@ def get_saved_objects(): return objects -def get_saved_game(): +def get_saved_game() -> dict: + """Get saved game from database + Returns dict: {"score", "level"} + """ games = tuple(iter(SavedGame.select())) game = games[-1] objects = { @@ -72,7 +94,10 @@ def get_saved_game(): return objects -def get_game_history(): +def get_game_history() -> list: + """Get game history from database + Returns list: [{"score", "level"}] + """ games = tuple(iter(SavedGame.select())) res = [] for i in games: @@ -84,7 +109,8 @@ def get_game_history(): return res -def set_saved_objects(name, objects): +def set_saved_objects(name: str, objects: Iterable) -> None: + """Set saved objects to database""" for e in objects: n = SavedObjects.create( object=name, @@ -96,12 +122,14 @@ def set_saved_objects(name, objects): n.save() -def set_saved_game(cur_level, score): +def set_saved_game(cur_level: int, score: int) -> None: + """Set saved game to database""" n = SavedGame.create(score=score, level=cur_level) n.save() -def delete_saved_objects(): +def delete_saved_objects() -> None: + """Delete all saved objects from database""" for e in SavedObjects.select(): SavedObjects.delete_by_id(e) SavedObjects.update() diff --git a/danmaku/database/models.py b/danmaku/database/models.py index 851a228..1e22f1b 100644 --- a/danmaku/database/models.py +++ b/danmaku/database/models.py @@ -1,3 +1,5 @@ +"""Peewee database models""" + from peewee import ( SqliteDatabase, Model, @@ -12,6 +14,9 @@ db = SqliteDatabase(resource_path("DataBase.db")) +# pylint: disable=missing-class-docstring + + class BaseModel(Model): class Meta: database = db diff --git a/danmaku/enemy.py b/danmaku/enemy.py index d72d733..6ff6229 100644 --- a/danmaku/enemy.py +++ b/danmaku/enemy.py @@ -1,12 +1,21 @@ +"""Enemy object declaration.""" + +from random import randint +from math import sin, cos, pi + +import pygame +from vgame.graphics import Graphics + +from danmaku.bullet import Bullet from danmaku.database import get_enemy_type from danmaku.gameobject import GameObject -from danmaku.bullet import Bullet -import pygame class Enemy(GameObject): - def __init__(self, xy, type, updated_hp=0): - args = get_enemy_type(type) + """Enemy object.""" + + def __init__(self, xy, object_type, updated_hp=0): + args = get_enemy_type(object_type) if updated_hp == 0: hp = args["hp"] else: @@ -24,20 +33,42 @@ def __init__(self, xy, type, updated_hp=0): self.last_animation_time = 0 self.texture_file = self.textures[self.last_animation] self.texture_size = args["texture_size"] - self.my_type = type + self.my_type = object_type self.cost = args["cost"] def shoot(self) -> list[Bullet]: t = pygame.time.get_ticks() if t - self.last_shoot >= self.shoot_v: + self.last_shoot = t + if self.my_type == "boss": + return self.shoot_radial() bullet = Bullet( (self.x + self.width // 2, self.y), self.damage, "basic enemy bullet" ) - self.last_shoot = t + bullet.vx = randint(-100, 100) / 100 + bullet.vy = (1 - bullet.vx**2) ** 0.5 return [bullet] return [] + def shoot_radial(self) -> list[Bullet]: + """Shoot circle of bullets""" + + bullets = [] + + a = randint(0, 359) + + for i in range(0, 360, 60): + angle = pi * ((a + i) % 360) / 180 + bullet = Bullet( + (self.x + self.width // 2, self.y), self.damage, "basic enemy bullet" + ) + bullet.vx = cos(angle) + bullet.vy = sin(angle) + bullets.append(bullet) + return bullets + def animation(self): + """Animate sprite.""" t = pygame.time.get_ticks() if t - self.last_animation_time >= self.animation_v: self.last_animation += 1 @@ -46,16 +77,14 @@ def animation(self): self.texture_file = self.textures[self.last_animation] self.last_animation_time = t - def collision(self, other): - if not other.enemy: - e = pygame.Rect( - other.x - other.r, other.y - other.r, 2 * other.r, 2 * other.r - ) - s = pygame.Rect( - self.x - (self.width // 2), - self.y - self.height * 2, - self.width, - self.height, - ) - if e.colliderect(s): - return True + def collision(self, other) -> bool: + e = pygame.Rect(other.x - other.r, other.y - other.r, 2 * other.r, 2 * other.r) + s = pygame.Rect( + self.x - (self.width // 2), + self.y - self.height * 2, + self.width, + self.height, + ) + return e.colliderect(s) + + def draw(self, graphics: Graphics): ... diff --git a/danmaku/game.py b/danmaku/game.py index f19614e..eb370d4 100644 --- a/danmaku/game.py +++ b/danmaku/game.py @@ -1,3 +1,5 @@ +"""Game scene.""" + import vgame from vgame import Keys import pygame @@ -14,6 +16,7 @@ delete_saved_objects, ) from danmaku.pause import Pause +from danmaku.background import Background WIDTH, HEIGHT = 300, 500 @@ -55,7 +58,8 @@ def load(self): self.paused = False self.pause_object = Pause() - self.pause_object.load() + + self.background_object = Background(0, 0, self.width, self.height) self.bullets: list[Bullet] = [] @@ -96,104 +100,79 @@ def load(self): self.player.score = saved_game["score"] delete_saved_objects() - def update(self): - if Keys.ESCAPE in self.pressed_keys: - self.pressed_keys.remove(Keys.ESCAPE) - self.paused = not self.paused + 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.stop() + + def update_game(self): + """Called from update loop if *not* paused""" + for bullet in self.bullets: + bullet.update(self.delta) - if self.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.stop() + if not not_in_border( + bullet.x, bullet.y, bullet.vx, bullet.vy, WIDTH, HEIGHT + ): + self.bullets.remove(bullet) + 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.LEFT_SHIFT in self.pressed_keys: + self.player.speed = 50 else: - vx = vy = 0 - if Keys.RIGHT in self.pressed_keys: - vx += 1 - if Keys.LEFT in self.pressed_keys: - vx -= 1 - if Keys.UP in self.pressed_keys: - vy -= 1 - if Keys.DOWN in self.pressed_keys: - vy += 1 - if Keys.SPACE in self.pressed_keys or Keys.Z in self.pressed_keys: - self.bullets += self.player.shoot() - if Keys.LEFT_SHIFT in self.pressed_keys: - self.player.speed = 50 - else: - self.player.speed = 100 - - self.player.vx, self.player.vy = vx, vy - - # TODO: Check separately x and y - if not_in_border( - self.player.x, - self.player.y, - self.player.vx, - self.player.vy, - WIDTH, - HEIGHT, - ) and not_in_border( - self.player.x + self.player.width, - self.player.y + self.player.height, - self.player.vx, - self.player.vy, - WIDTH, - HEIGHT, - ): - self.player.update(self.delta) - self.player.animation() + self.player.speed = 100 + + self.player.vx, self.player.vy = vx, vy + self.player.update(self.delta) + self.player.animation() + + for enemy in self.enemies: + self.bullets += enemy.shoot() + enemy.animation() + enemy.update(self.delta) + if not not_in_border(enemy.x, enemy.y, enemy.vx, enemy.vy, WIDTH, HEIGHT): + 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: - self.bullets += enemy.shoot() - enemy.animation() - enemy.update(self.delta) - if not not_in_border( - enemy.x, enemy.y, enemy.vx, enemy.vy, WIDTH, HEIGHT - ): - self.enemies.remove(enemy) - - dell = set() - - for bullet in self.bullets: - if self.player.collision(bullet): - self.player.get_damage(bullet.damage) - dell.add(bullet) - - for enemy in self.enemies: - if enemy.collision(bullet): - enemy.get_damage(bullet.damage) - dell.add(bullet) - if enemy.hp <= 0: - self.player.score += enemy.cost - self.enemies.remove(enemy) - - bullet.update(self.delta) - bullet.draw(self.graphics) - - if not not_in_border( - bullet.x, bullet.y, bullet.vx, bullet.vy, WIDTH, HEIGHT - ): - dell.add(bullet) - - for i in dell: - self.bullets.remove(i) - - if len(self.enemies) == 0: + if enemy.collision(bullet): + enemy.get_damage(bullet.damage) + if enemy.hp <= 0: + self.player.score += enemy.cost + self.enemies.remove(enemy) + self.bullets.remove(bullet) + break + + self.background_object.animation() + + if len(self.enemies) == 0: + if len(LEVELS) > self.cur_level + 1: self.cur_level += 1 - if len(LEVELS) > self.cur_level: - self.enemies = LEVELS[self.cur_level] + self.enemies = LEVELS[self.cur_level] if self.player.hp <= 0: set_saved_game(self.cur_level, self.player.score) @@ -203,7 +182,22 @@ def update(self): 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() + + if self.paused: + self.update_pause() + else: + self.update_game() + def draw(self): + self.graphics.draw_sprite(self.background_object) + self.graphics.draw_sprite(self.player) for enemy in self.enemies: @@ -220,3 +214,30 @@ def draw(self): 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.hp}", + f"Score: {self.player.score}", + f"Level: {self.cur_level}", + sep="\n", + ) + + print("\x1b[?25h", end="") # show cursor diff --git a/danmaku/gameobject.py b/danmaku/gameobject.py index 3544400..bd19759 100644 --- a/danmaku/gameobject.py +++ b/danmaku/gameobject.py @@ -1,6 +1,9 @@ -from vgame.graphics import Graphics, Sprite +"""Base game object.""" + from abc import abstractmethod +from vgame.graphics import Graphics, Sprite + class GameObject(Sprite): """ @@ -36,13 +39,16 @@ def update(self, delta: int | float): ) def get_damage(self, damage: int | float): + """Decrease health point.""" self.hp -= damage * self.endurance @abstractmethod - def shoot(self): ... + def shoot(self) -> list: + """Generate bullets.""" @abstractmethod def draw(self, graphics: Graphics): ... @abstractmethod - def collision(self, other): ... + def collision(self, other) -> bool: + """Check collision.""" diff --git a/danmaku/history.py b/danmaku/history.py index 92b38cf..bfd1e10 100644 --- a/danmaku/history.py +++ b/danmaku/history.py @@ -1,6 +1,9 @@ -from danmaku.database import get_game_history +"""Game history scene.""" + import vgame +from danmaku.database import get_game_history + # pylint: disable=attribute-defined-outside-init, missing-class-docstring class History(vgame.Scene): @@ -11,11 +14,9 @@ def load(self): self.record_count = len(self.history) def update(self): - if vgame.Keys.UP in self.pressed_keys: - self.pressed_keys.discard(vgame.Keys.UP) + if self.get_click(vgame.Keys.UP): self.selection_index = (self.selection_index - 1) % self.record_count - if vgame.Keys.DOWN in self.pressed_keys: - self.pressed_keys.discard(vgame.Keys.DOWN) + if self.get_click(vgame.Keys.DOWN): self.selection_index = (self.selection_index + 1) % self.record_count if { vgame.Keys.RETURN, diff --git a/danmaku/main.py b/danmaku/main.py index a0e574f..2c58c1c 100644 --- a/danmaku/main.py +++ b/danmaku/main.py @@ -1,28 +1,31 @@ -import vgame +"""Application entry point.""" + +from vgame import Runner from danmaku.menu import Menu from danmaku.game import Game from danmaku.history import History +from danmaku.settings import Settings WIDTH, HEIGHT = 300, 500 +TICKRATE = 120 -runner = vgame.Runner() +runner = Runner() while runner.running: menu = Menu(width=WIDTH, height=HEIGHT, title="Danmaku | Menu") runner.run(menu) match menu.exit_status: - case "game_new": - runner.run(Game(width=WIDTH, height=HEIGHT, title="Danmaku | Game")) - case "game_continue": - game = Game(width=WIDTH, height=HEIGHT, title="Danmaku | Game") - game.new_game = False + case "game", new_game: + game = Game( + width=WIDTH, height=HEIGHT, title="Danmaku | Game", tickrate=TICKRATE + ) + game.new_game = new_game runner.run(game) case "settings": - # settings = Settings(width=WIDTH, height=HEIGHT, title="Danmaku | Settings") - # runner.run(settings) - raise NotImplementedError() + settings = Settings(width=WIDTH, height=HEIGHT, title="Danmaku | Settings") + runner.run(settings) case "history": history = History(width=WIDTH, height=HEIGHT, title="Danmaku | History") runner.run(history) diff --git a/danmaku/menu.py b/danmaku/menu.py index c51964d..efeeece 100644 --- a/danmaku/menu.py +++ b/danmaku/menu.py @@ -1,7 +1,10 @@ +"""Main menu scene.""" + import pygame -from danmaku.database import get_saved_objects import vgame +from danmaku.database import get_saved_objects + # pylint: disable=attribute-defined-outside-init, missing-class-docstring class Menu(vgame.Scene): @@ -16,25 +19,23 @@ def load(self): ("Quit", "quit"), ) - self.exit_status: str = "" + self.exit_status = "" def update(self): - if vgame.Keys.UP in self.pressed_keys: - self.pressed_keys.discard(vgame.Keys.UP) + if self.get_click(vgame.Keys.UP): self.selection_index = (self.selection_index - 1) % len(self.buttons) - if vgame.Keys.DOWN in self.pressed_keys: - self.pressed_keys.discard(vgame.Keys.DOWN) + if self.get_click(vgame.Keys.DOWN): self.selection_index = (self.selection_index + 1) % len(self.buttons) if {vgame.Keys.RETURN, vgame.Keys.Z, vgame.Keys.SPACE} & self.pressed_keys: match self.buttons[self.selection_index][1]: case "new_game": # Delete game from db & go to game scene - self.exit_status = "game_new" + self.exit_status = "game", True self.stop() case "continue": # Just go to game scene if get_saved_objects(): - self.exit_status = "game_continue" + self.exit_status = "game", False self.stop() case "settings": # Go to settings scene @@ -48,7 +49,7 @@ def update(self): self.stop() case "quit": # Maybe rework to quit through exit status - pygame.event.post(pygame.event.Event(pygame.QUIT)) + pygame.event.post(pygame.event.Event(pygame.constants.QUIT)) def draw(self): self.graphics.text("Danmaku", (0, 10), (255, 255, 180)) diff --git a/danmaku/pause.py b/danmaku/pause.py index 6ee803b..ece33b2 100644 --- a/danmaku/pause.py +++ b/danmaku/pause.py @@ -1,10 +1,12 @@ -import pygame +"""In-game pause menu.""" + import vgame # pylint: disable=attribute-defined-outside-init, missing-class-docstring class Pause: def load(self): + """Load pause menu.""" self.selection_index = 0 self.buttons = ( @@ -16,6 +18,7 @@ def load(self): self.exit_status: str = "" def update(self, pressed_keys): + """Update pause menu.""" if vgame.Keys.UP in pressed_keys: pressed_keys.discard(vgame.Keys.UP) self.selection_index = (self.selection_index - 1) % len(self.buttons) @@ -26,6 +29,7 @@ def update(self, pressed_keys): self.exit_status = self.buttons[self.selection_index][1] def draw(self, graphics: vgame.graphics.Graphics): + """Draw pause menu.""" graphics.text("Danmaku", (0, 10), (255, 255, 180)) for i, button in enumerate(self.buttons): diff --git a/danmaku/player.py b/danmaku/player.py index 76ee248..f6104d2 100644 --- a/danmaku/player.py +++ b/danmaku/player.py @@ -1,17 +1,28 @@ +"""Player object declaration.""" + import pygame import vgame from danmaku.gameobject import GameObject from danmaku.bullet import Bullet from danmaku.database import get_player_type +from danmaku.utils import constrain class Player(GameObject): - def __init__(self, xy: tuple[int | float, int | float], type, updated_hp=0): - args = get_player_type(type) + """Player object.""" + + def __init__( + self, xy: tuple[int | float, int | float], object_type: str, updated_hp=0 + ) -> None: + args = get_player_type(object_type) + if updated_hp == 0: hp = args["hp"] else: hp = updated_hp + # Can replace with: + # hp = updated_hp or args["hp"] + super().__init__( xy, args["texture_size"], @@ -37,13 +48,16 @@ def __init__(self, xy: tuple[int | float, int | float], type, updated_hp=0): self.last_animation = 0 self.texture_file = self.textures["left"][self.last_animation] self.texture_size = args["texture_size"] - self.my_type = type + self.my_type = object_type self.animation_v = 100 self.last_animation_time = 0 self.last_shoot = 0 self.shoot_v = args["shoot_v"] self.score = 0 + self.left = self.top = 0 + self.right = self.bottom = 10e6 + def shoot(self) -> list[Bullet]: t = pygame.time.get_ticks() if t - self.last_shoot >= self.shoot_v: @@ -56,7 +70,35 @@ def shoot(self) -> list[Bullet]: return [bullet] return [] - def animation(self): + 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: + self.x += self.vx * delta * self.speed + self.x = constrain(self.x, self.left, self.right - self.width) + + self.y += self.vy * delta * self.speed + self.y = constrain(self.y, self.top, self.bottom - self.height) + + self.rect.x, self.rect.y, self.rect.w, self.rect.h = ( + self.x, + self.y, + self.width, + self.height, + ) + + def animation(self) -> None: + """Animate one frame.""" t = pygame.time.get_ticks() if t - self.last_animation_time >= self.animation_v: self.last_animation += 1 @@ -75,14 +117,10 @@ def animation(self): self.texture_file = self.textures[direction][self.last_animation] self.last_animation_time = t - def draw(self, graphics: vgame.graphics.Graphics): + def draw(self, graphics: vgame.graphics.Graphics) -> None: graphics.draw_sprite(self) - def collision(self, other): - if other.enemy: - e = pygame.Rect( - other.x - other.r, other.y - other.r, 2 * other.r, 2 * other.r - ) - s = pygame.Rect(self.x, self.y, self.width, self.height) - if e.colliderect(s): - return True + def collision(self, other) -> bool: + e = pygame.Rect(other.x - other.r, other.y - other.r, 2 * other.r, 2 * other.r) + s = pygame.Rect(self.x, self.y, self.width, self.height) + return e.colliderect(s) diff --git a/danmaku/settings.py b/danmaku/settings.py new file mode 100644 index 0000000..681fc12 --- /dev/null +++ b/danmaku/settings.py @@ -0,0 +1,55 @@ +"""Settings scene.""" + +import vgame + + +# pylint: disable=attribute-defined-outside-init, missing-class-docstring +class Settings(vgame.Scene): + def load(self): + self.selection_index = 0 + + # Buttons + # ("Text", "codename",) + + # Settings + # ("Text", "codename", (current_value_index, (*possible_values))) + + self.buttons = ( + ("Volume", "volume", (3, (0, 1, 2, 3, 4, 5))), + ("Quit", "quit"), + ) + + self.exit_status: str = "" + + def update(self): + if self.get_click(vgame.Keys.UP): + self.selection_index = (self.selection_index - 1) % len(self.buttons) + if self.get_click(vgame.Keys.DOWN): + self.selection_index = (self.selection_index + 1) % len(self.buttons) + if self.get_click(vgame.Keys.RIGHT): + # Update settings values left<->right + # Like: music [x x _ _ _] + # sfx [x _ _ _ _] + ... + if self.get_click(vgame.Keys.LEFT): + # Same + ... + if {vgame.Keys.RETURN, vgame.Keys.Z, vgame.Keys.SPACE} & self.pressed_keys: + match self.buttons[self.selection_index][1]: + case "quit": + self.stop() + + def draw(self): + self.graphics.text("Danmaku", (0, 10), (255, 255, 180)) + + for i, button in enumerate(self.buttons): + + color = (255, 200, 180) if i == self.selection_index else (255, 255, 255) + + self.graphics.text( + button[0], + (0, 100 + i * 50), + color, + ) + + def exit(self): ... diff --git a/danmaku/utils.py b/danmaku/utils.py index a12b087..526ae6d 100644 --- a/danmaku/utils.py +++ b/danmaku/utils.py @@ -1,3 +1,5 @@ +"""Helper functions.""" + import sys from pathlib import Path @@ -13,22 +15,29 @@ def not_in_border( """Check if the object is in screen boundary""" if vy < 0 and y <= 0: return False - elif vy > 0 and y >= height: + if vy > 0 and y >= height: return False - elif vx < 0 and x <= 0: + if vx < 0 and x <= 0: return False - elif vx > 0 and x >= width: + if vx > 0 and x >= width: return False - else: - return True + return True def resource_path(relative_path) -> str: """Get absolute path to resource, works for dev and for PyInstaller""" try: # PyInstaller creates a temp folder and stores path in _MEIPASS - base_path = Path(sys._MEIPASS) # pylint: disable=protected-access + # pylint: disable=protected-access + base_path = Path(sys._MEIPASS) # type: ignore except AttributeError: base_path = Path(__file__).parent.parent return str(base_path / "assets" / relative_path) + + +def constrain( + value: int | float, min_value: int | float, max_value: int | float +) -> int | float: + """Constrain value between min_value and max_value.""" + return max(min(value, max_value), min_value) diff --git a/poetry.lock b/poetry.lock index 65cc59a..e6462d2 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.3.0" +version = "1.5.0" 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 = "974b129364f49ba03da0a270a3e44e5ec15c582f" +resolved_reference = "e11be2384d171e521781d12e241030d79fb2c936" [metadata] lock-version = "2.0" diff --git a/pyproject.toml b/pyproject.toml index 060b14f..4fc55fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "danmaku" -version = "0.9.0" +version = "0.10.0" description = "" authors = ["Vlad <89295404+Virashu@users.noreply.github.com>"] readme = "README.md"