commit d9dc0e390c432b2bd358a3de27e1e0fed6c896d0 Author: Cutieguwu Date: Wed Feb 26 14:40:25 2025 -0500 Initial Commit diff --git a/.vscode2/launch.json b/.vscode2/launch.json new file mode 100644 index 0000000..a2f4e61 --- /dev/null +++ b/.vscode2/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File with Arguments", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/main.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/TESTING/Time.py b/TESTING/Time.py new file mode 100644 index 0000000..75f8247 --- /dev/null +++ b/TESTING/Time.py @@ -0,0 +1,27 @@ +import pygame +from icecream import ic +from time import sleep + + +CLOCK = pygame.time.Clock() + +testOn = True +timeStart = pygame.time.get_ticks() + +while testOn: # Pretend game loop. + if ic(pygame.time.get_ticks() - timeStart) >= 2000: # Once 2000 ms has elapsed. + ic("Time has elapsed.") + testOn = False + +del testOn, timeStart + +# --------------------------------------------------------- +sleep(2) +# --------------------------------------------------------- + +timeStart = pygame.time.get_ticks() + +while ic(pygame.time.get_ticks() - timeStart) < 2000: + pass + +ic("Time has elapsed.") \ No newline at end of file diff --git a/TESTING/Xlib_utilization.py b/TESTING/Xlib_utilization.py new file mode 100755 index 0000000..e1fd2c9 --- /dev/null +++ b/TESTING/Xlib_utilization.py @@ -0,0 +1,21 @@ +# Taken from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate +# Deconstruction, Commenting and Renaming by Cutieguwu. + +from Xlib import display +from Xlib.ext import randr + +displayGlobal = display.Display() # Get every display thing. Maybe a WM object? +default_screen = displayGlobal.get_default_screen() # Find default display for some reason. +info = displayGlobal.screen(default_screen) # Use this as a point to get information about all screens. + +displayConfigs = randr.get_screen_resources(info.root) # Get every screen's possible configurations. +ConfigsActive = set() # Create an empty set to add configurations to. + +for config in displayConfigs.crtcs: # For every display configuration, determine if it's active and add its identifier to the configurations in use. + crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp) + if crtc_info.mode: + ConfigsActive.add(crtc_info.mode) + +for config in displayConfigs.modes: + if config.id in ConfigsActive: # For every active display mode, figure out its framerate. + print(config.dot_clock / (config.h_total * config.v_total)) \ No newline at end of file diff --git a/TESTING/clean python version b/TESTING/clean python version new file mode 100644 index 0000000..19e3151 --- /dev/null +++ b/TESTING/clean python version @@ -0,0 +1,31 @@ +from sys import version + +versionStr = "" + +for c in version: + if c == " ": + break + else: + versionStr = versionStr + c + +versionTuple = tuple(versionStr.split(".")) + +print(versionTuple) + +# GCC CLEANUP + +versionStr = "" +startFlag = False + +for c in version: + if startFlag: + if c not in ["G", "C", "]"]: + versionStr = versionStr + c + elif c == "[": + startFlag = True + +print(versionStr) + +versionTuple = tuple(versionStr.split()) + +print(versionTuple) \ No newline at end of file diff --git a/TESTING/for loop debugging.py b/TESTING/for loop debugging.py new file mode 100644 index 0000000..fdc2702 --- /dev/null +++ b/TESTING/for loop debugging.py @@ -0,0 +1,15 @@ +myList = ["A", "A", "A", "D"] + +for i in myList: + + if i == "A": + myList.append("A") + else: + myList[2] = "C" + +print(myList) + + +for button in buttonList: + if button.name in ["Start", "Back"]: + button.remove() \ No newline at end of file diff --git a/TESTING/refreshed spawned classes.py b/TESTING/refreshed spawned classes.py new file mode 100644 index 0000000..91970dd --- /dev/null +++ b/TESTING/refreshed spawned classes.py @@ -0,0 +1,39 @@ +import pygame +from os import path + +class Guy(): + def __init__(self, mouseCoords): + self.coords = mouseCoords + self.image = pygame.image.load(path.dirname(__file__) + "/../data/image/ui/icons/settings_neutral.png") + + def draw(self): + """ + Draws character. + """ + WINDOW.blit(self.image, (self.coords)) + +WINDOW = pygame.display.set_mode((720, 480)) +CLOCK = pygame.time.Clock() + +gameOn = True +guys = [] + +while gameOn: + + CLOCK.tick(30) + + WINDOW.fill((150, 150, 150)) + + for guy in guys: + guy.draw() + + pygame.display.flip() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + gameOn = False + pygame.quit() + elif event.type == pygame.MOUSEBUTTONDOWN: + if 0 in pygame.mouse.get_pressed(): + # Left button pressed + guys.append(Guy(pygame.mouse.get_pos())) diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..4766c7e Binary files /dev/null and b/__pycache__/main.cpython-311.pyc differ diff --git a/config/features.json b/config/features.json new file mode 100644 index 0000000..1644a67 --- /dev/null +++ b/config/features.json @@ -0,0 +1,37 @@ +{ + "fullscreen": { + "libs": { + "pygame": { + "ver_implemented": [2, 0, 0] + } + }, + "is_active": false + }, + + "vsync": { + "libs": { + "pygame": { + "ver_implemented": [2, 0, 0] + } + }, + "is_active": false + }, + + "matchcase": { + "libs": { + "python": { + "ver_implemented": [3, 10, 0] + } + }, + "is_active": false + }, + + "resizeable": { + "libs": { + "pygame": { + "ver_implemented": [2, 0, 0] + } + }, + "is_active": false + } +} \ No newline at end of file diff --git a/config/settings/default.json b/config/settings/default.json new file mode 100644 index 0000000..08c82fb --- /dev/null +++ b/config/settings/default.json @@ -0,0 +1,9 @@ +{ + "window": { + "framerate": 30, + "size": { + "width": 1440, + "height": 720 + } + } +} \ No newline at end of file diff --git a/config/settings/user.json b/config/settings/user.json new file mode 100644 index 0000000..833b8b6 --- /dev/null +++ b/config/settings/user.json @@ -0,0 +1,26 @@ +{ + "window": { + "framerate": 165, + "size": { + "width": 2280, + "height": 1440 + } + }, + + "controls": { + "keyboard": { + "move_left": "K_LEFT", + "move_right": "K_RIGHT", + "move_up": "K_UP", + "move_down": "K_DOWN" + }, + + "mouse": { + "is_inverted": + false + } + }, + "active_features": [ + "fullscreen" + ] +} \ No newline at end of file diff --git a/data/credits.txt b/data/credits.txt new file mode 100644 index 0000000..8625af9 --- /dev/null +++ b/data/credits.txt @@ -0,0 +1,58 @@ +Coding: + +- Olivia Brooks (Cutieguwu) + + +StackOverflow Thanks: + +https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate + +- Glyph (StackOverflow) +- Eevee (StackOverflow) +- Anurag Uniyal (StackOverflow) + +https://stackoverflow.com/questions/46419607/how-to-automatically-install-required-packages-from-a-python-script-as-necessary + +- Serge Stroobandt (StackOverflow) + + +Conceptualisation: + +- Olivia Brooks +- Jayde Paquette + + +Artwork: + +- Jayde Paquette (Lead) +- Olivia Brooks (Development Images) +- Mojang (Development Images) + + +Open Libraries: + +Used in the Making + +- Pygame +- IceCream +- Python built-ins and packaged libs + + +Sounds and Effects: + +Soundbible.com + +- WEL +- Jim Rogers +- Mark DiAngelo +- GoodSoundForYou +- Marcel +- Mike Koenig +- FreqMan +- thecheeseman +- Vladimir + +Voiceover + +- Jayde Paquette (Commencing Slaughter) +- Jenna Allen (Death Cough) \ No newline at end of file diff --git a/data/font/Another_Danger___Demo_Regular.otf b/data/font/Another_Danger___Demo_Regular.otf new file mode 100644 index 0000000..d99830a Binary files /dev/null and b/data/font/Another_Danger___Demo_Regular.otf differ diff --git a/data/font/Domestic_Manners.ttf b/data/font/Domestic_Manners.ttf new file mode 100644 index 0000000..31bac14 Binary files /dev/null and b/data/font/Domestic_Manners.ttf differ diff --git a/data/font/Who_asks_Satan_Regular.ttf b/data/font/Who_asks_Satan_Regular.ttf new file mode 100644 index 0000000..83cd7c5 Binary files /dev/null and b/data/font/Who_asks_Satan_Regular.ttf differ diff --git a/data/image/game/blocks/puddle_blood/puddle_blood_0.png b/data/image/game/blocks/puddle_blood/puddle_blood_0.png new file mode 100644 index 0000000..7404071 Binary files /dev/null and b/data/image/game/blocks/puddle_blood/puddle_blood_0.png differ diff --git a/data/image/game/blocks/puddle_blood/puddle_blood_1.png b/data/image/game/blocks/puddle_blood/puddle_blood_1.png new file mode 100644 index 0000000..6a69305 Binary files /dev/null and b/data/image/game/blocks/puddle_blood/puddle_blood_1.png differ diff --git a/data/image/game/blocks/puddle_blood/puddle_blood_2.png b/data/image/game/blocks/puddle_blood/puddle_blood_2.png new file mode 100644 index 0000000..ccb1b2f Binary files /dev/null and b/data/image/game/blocks/puddle_blood/puddle_blood_2.png differ diff --git a/data/image/game/blocks/puddle_blood/puddle_blood_3.png b/data/image/game/blocks/puddle_blood/puddle_blood_3.png new file mode 100644 index 0000000..0aeaccf Binary files /dev/null and b/data/image/game/blocks/puddle_blood/puddle_blood_3.png differ diff --git a/data/image/game/blocks/puddle_blood/puddle_blood_4.png b/data/image/game/blocks/puddle_blood/puddle_blood_4.png new file mode 100644 index 0000000..1d093ad Binary files /dev/null and b/data/image/game/blocks/puddle_blood/puddle_blood_4.png differ diff --git a/data/image/game/blocks/puddle_blood/puddle_blood_neutral.png b/data/image/game/blocks/puddle_blood/puddle_blood_neutral.png new file mode 100644 index 0000000..59c4144 Binary files /dev/null and b/data/image/game/blocks/puddle_blood/puddle_blood_neutral.png differ diff --git a/data/image/game/blocks/puddle_souls/puddle_souls_rich.png b/data/image/game/blocks/puddle_souls/puddle_souls_rich.png new file mode 100644 index 0000000..6409ff2 Binary files /dev/null and b/data/image/game/blocks/puddle_souls/puddle_souls_rich.png differ diff --git a/data/image/game/blocks/puddle_souls/puddle_souls_spent.png b/data/image/game/blocks/puddle_souls/puddle_souls_spent.png new file mode 100644 index 0000000..756d9b0 Binary files /dev/null and b/data/image/game/blocks/puddle_souls/puddle_souls_spent.png differ diff --git a/data/image/game/blocks/wool_colored_blue.png b/data/image/game/blocks/wool_colored_blue.png new file mode 100644 index 0000000..ce9515f Binary files /dev/null and b/data/image/game/blocks/wool_colored_blue.png differ diff --git a/data/image/game/blocks/wool_colored_brown.png b/data/image/game/blocks/wool_colored_brown.png new file mode 100644 index 0000000..b4dc3c5 Binary files /dev/null and b/data/image/game/blocks/wool_colored_brown.png differ diff --git a/data/image/game/blocks/wool_colored_cyan.png b/data/image/game/blocks/wool_colored_cyan.png new file mode 100644 index 0000000..ca0800a Binary files /dev/null and b/data/image/game/blocks/wool_colored_cyan.png differ diff --git a/data/image/game/blocks/wool_colored_gray.png b/data/image/game/blocks/wool_colored_gray.png new file mode 100644 index 0000000..6409ff2 Binary files /dev/null and b/data/image/game/blocks/wool_colored_gray.png differ diff --git a/data/image/game/blocks/wool_colored_green.png b/data/image/game/blocks/wool_colored_green.png new file mode 100644 index 0000000..a7be6d7 Binary files /dev/null and b/data/image/game/blocks/wool_colored_green.png differ diff --git a/data/image/game/blocks/wool_colored_light_blue.png b/data/image/game/blocks/wool_colored_light_blue.png new file mode 100644 index 0000000..72d9d9e Binary files /dev/null and b/data/image/game/blocks/wool_colored_light_blue.png differ diff --git a/data/image/game/blocks/wool_colored_lime.png b/data/image/game/blocks/wool_colored_lime.png new file mode 100644 index 0000000..bf56389 Binary files /dev/null and b/data/image/game/blocks/wool_colored_lime.png differ diff --git a/data/image/game/blocks/wool_colored_magenta.png b/data/image/game/blocks/wool_colored_magenta.png new file mode 100644 index 0000000..3af6747 Binary files /dev/null and b/data/image/game/blocks/wool_colored_magenta.png differ diff --git a/data/image/game/blocks/wool_colored_orange.png b/data/image/game/blocks/wool_colored_orange.png new file mode 100644 index 0000000..eefe6de Binary files /dev/null and b/data/image/game/blocks/wool_colored_orange.png differ diff --git a/data/image/game/blocks/wool_colored_pink.png b/data/image/game/blocks/wool_colored_pink.png new file mode 100644 index 0000000..c2785af Binary files /dev/null and b/data/image/game/blocks/wool_colored_pink.png differ diff --git a/data/image/game/blocks/wool_colored_purple.png b/data/image/game/blocks/wool_colored_purple.png new file mode 100644 index 0000000..76f68d6 Binary files /dev/null and b/data/image/game/blocks/wool_colored_purple.png differ diff --git a/data/image/game/blocks/wool_colored_red.png b/data/image/game/blocks/wool_colored_red.png new file mode 100644 index 0000000..0cff7a9 Binary files /dev/null and b/data/image/game/blocks/wool_colored_red.png differ diff --git a/data/image/game/blocks/wool_colored_silver.png b/data/image/game/blocks/wool_colored_silver.png new file mode 100644 index 0000000..756d9b0 Binary files /dev/null and b/data/image/game/blocks/wool_colored_silver.png differ diff --git a/data/image/game/blocks/wool_colored_white.png b/data/image/game/blocks/wool_colored_white.png new file mode 100644 index 0000000..abc7999 Binary files /dev/null and b/data/image/game/blocks/wool_colored_white.png differ diff --git a/data/image/game/blocks/wool_colored_yellow.png b/data/image/game/blocks/wool_colored_yellow.png new file mode 100644 index 0000000..4babaaa Binary files /dev/null and b/data/image/game/blocks/wool_colored_yellow.png differ diff --git a/data/image/game/entity/unnamed/neutral_1.png b/data/image/game/entity/unnamed/neutral_1.png new file mode 100755 index 0000000..528ba22 Binary files /dev/null and b/data/image/game/entity/unnamed/neutral_1.png differ diff --git a/data/image/game/entity/unnamed/neutral_2.png b/data/image/game/entity/unnamed/neutral_2.png new file mode 100755 index 0000000..901d72b Binary files /dev/null and b/data/image/game/entity/unnamed/neutral_2.png differ diff --git a/data/image/game/entity/unnamed/run_d_1.png b/data/image/game/entity/unnamed/run_d_1.png new file mode 100755 index 0000000..aef3f89 Binary files /dev/null and b/data/image/game/entity/unnamed/run_d_1.png differ diff --git a/data/image/game/entity/unnamed/run_d_2.png b/data/image/game/entity/unnamed/run_d_2.png new file mode 100755 index 0000000..86c9e35 Binary files /dev/null and b/data/image/game/entity/unnamed/run_d_2.png differ diff --git a/data/image/game/entity/unnamed/run_l_1.png b/data/image/game/entity/unnamed/run_l_1.png new file mode 100755 index 0000000..3cf2ffe Binary files /dev/null and b/data/image/game/entity/unnamed/run_l_1.png differ diff --git a/data/image/game/entity/unnamed/run_l_2.png b/data/image/game/entity/unnamed/run_l_2.png new file mode 100755 index 0000000..883244f Binary files /dev/null and b/data/image/game/entity/unnamed/run_l_2.png differ diff --git a/data/image/game/entity/unnamed/run_r_1.png b/data/image/game/entity/unnamed/run_r_1.png new file mode 100755 index 0000000..b3dd846 Binary files /dev/null and b/data/image/game/entity/unnamed/run_r_1.png differ diff --git a/data/image/game/entity/unnamed/run_r_2.png b/data/image/game/entity/unnamed/run_r_2.png new file mode 100755 index 0000000..732b98d Binary files /dev/null and b/data/image/game/entity/unnamed/run_r_2.png differ diff --git a/data/image/game/entity/unnamed/run_u_1.png b/data/image/game/entity/unnamed/run_u_1.png new file mode 100755 index 0000000..13b4604 Binary files /dev/null and b/data/image/game/entity/unnamed/run_u_1.png differ diff --git a/data/image/game/entity/unnamed/run_u_2.png b/data/image/game/entity/unnamed/run_u_2.png new file mode 100755 index 0000000..4315c42 Binary files /dev/null and b/data/image/game/entity/unnamed/run_u_2.png differ diff --git a/data/image/game/icon.png b/data/image/game/icon.png new file mode 100755 index 0000000..8e68d0e Binary files /dev/null and b/data/image/game/icon.png differ diff --git a/data/image/game/player/default_a/base/neutral_1.png b/data/image/game/player/default_a/base/neutral_1.png new file mode 100755 index 0000000..8c1fef3 Binary files /dev/null and b/data/image/game/player/default_a/base/neutral_1.png differ diff --git a/data/image/game/player/default_a/base/neutral_2.png b/data/image/game/player/default_a/base/neutral_2.png new file mode 100755 index 0000000..c2c7224 Binary files /dev/null and b/data/image/game/player/default_a/base/neutral_2.png differ diff --git a/data/image/game/player/default_a/base/run_d_1.png b/data/image/game/player/default_a/base/run_d_1.png new file mode 100755 index 0000000..ef031bc Binary files /dev/null and b/data/image/game/player/default_a/base/run_d_1.png differ diff --git a/data/image/game/player/default_a/base/run_d_2.png b/data/image/game/player/default_a/base/run_d_2.png new file mode 100755 index 0000000..5f60242 Binary files /dev/null and b/data/image/game/player/default_a/base/run_d_2.png differ diff --git a/data/image/game/player/default_a/base/run_l_1.png b/data/image/game/player/default_a/base/run_l_1.png new file mode 100755 index 0000000..80d76ea Binary files /dev/null and b/data/image/game/player/default_a/base/run_l_1.png differ diff --git a/data/image/game/player/default_a/base/run_l_2.png b/data/image/game/player/default_a/base/run_l_2.png new file mode 100755 index 0000000..b9014c6 Binary files /dev/null and b/data/image/game/player/default_a/base/run_l_2.png differ diff --git a/data/image/game/player/default_a/base/run_r_1.png b/data/image/game/player/default_a/base/run_r_1.png new file mode 100755 index 0000000..282ffc7 Binary files /dev/null and b/data/image/game/player/default_a/base/run_r_1.png differ diff --git a/data/image/game/player/default_a/base/run_r_2.png b/data/image/game/player/default_a/base/run_r_2.png new file mode 100755 index 0000000..c2e4db4 Binary files /dev/null and b/data/image/game/player/default_a/base/run_r_2.png differ diff --git a/data/image/game/player/default_a/base/run_u_1.png b/data/image/game/player/default_a/base/run_u_1.png new file mode 100755 index 0000000..10f9f9f Binary files /dev/null and b/data/image/game/player/default_a/base/run_u_1.png differ diff --git a/data/image/game/player/default_a/base/run_u_2.png b/data/image/game/player/default_a/base/run_u_2.png new file mode 100755 index 0000000..9eb58d1 Binary files /dev/null and b/data/image/game/player/default_a/base/run_u_2.png differ diff --git a/data/image/game/player/default_a/war_paint/neutral_1.png b/data/image/game/player/default_a/war_paint/neutral_1.png new file mode 100755 index 0000000..2ba8c19 Binary files /dev/null and b/data/image/game/player/default_a/war_paint/neutral_1.png differ diff --git a/data/image/game/player/default_a/war_paint/neutral_2.png b/data/image/game/player/default_a/war_paint/neutral_2.png new file mode 100755 index 0000000..50cdf03 Binary files /dev/null and b/data/image/game/player/default_a/war_paint/neutral_2.png differ diff --git a/data/image/game/player/default_a/war_paint/run_d_1.png b/data/image/game/player/default_a/war_paint/run_d_1.png new file mode 100755 index 0000000..3b16d4f Binary files /dev/null and b/data/image/game/player/default_a/war_paint/run_d_1.png differ diff --git a/data/image/game/player/default_a/war_paint/run_d_2.png b/data/image/game/player/default_a/war_paint/run_d_2.png new file mode 100755 index 0000000..68a355b Binary files /dev/null and b/data/image/game/player/default_a/war_paint/run_d_2.png differ diff --git a/data/image/game/player/default_a/war_paint/run_l_1.png b/data/image/game/player/default_a/war_paint/run_l_1.png new file mode 100755 index 0000000..8d7dd33 Binary files /dev/null and b/data/image/game/player/default_a/war_paint/run_l_1.png differ diff --git a/data/image/game/player/default_a/war_paint/run_l_2.png b/data/image/game/player/default_a/war_paint/run_l_2.png new file mode 100755 index 0000000..d98db32 Binary files /dev/null and b/data/image/game/player/default_a/war_paint/run_l_2.png differ diff --git a/data/image/game/player/default_a/war_paint/run_r_1.png b/data/image/game/player/default_a/war_paint/run_r_1.png new file mode 100755 index 0000000..eac4b8e Binary files /dev/null and b/data/image/game/player/default_a/war_paint/run_r_1.png differ diff --git a/data/image/game/player/default_a/war_paint/run_r_2.png b/data/image/game/player/default_a/war_paint/run_r_2.png new file mode 100755 index 0000000..0340b4a Binary files /dev/null and b/data/image/game/player/default_a/war_paint/run_r_2.png differ diff --git a/data/image/game/player/default_a/war_paint/run_u_1.png b/data/image/game/player/default_a/war_paint/run_u_1.png new file mode 100755 index 0000000..f0e3daf Binary files /dev/null and b/data/image/game/player/default_a/war_paint/run_u_1.png differ diff --git a/data/image/game/player/default_a/war_paint/run_u_2.png b/data/image/game/player/default_a/war_paint/run_u_2.png new file mode 100755 index 0000000..22e88ec Binary files /dev/null and b/data/image/game/player/default_a/war_paint/run_u_2.png differ diff --git a/data/image/game/scenes/commencing_slaughter/Commencing_Slaughter_1.png b/data/image/game/scenes/commencing_slaughter/Commencing_Slaughter_1.png new file mode 100644 index 0000000..627b2a1 Binary files /dev/null and b/data/image/game/scenes/commencing_slaughter/Commencing_Slaughter_1.png differ diff --git a/data/image/game/scenes/commencing_slaughter/Commencing_Slaughter_2.png b/data/image/game/scenes/commencing_slaughter/Commencing_Slaughter_2.png new file mode 100644 index 0000000..cd2565e Binary files /dev/null and b/data/image/game/scenes/commencing_slaughter/Commencing_Slaughter_2.png differ diff --git a/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png b/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png new file mode 100644 index 0000000..d5a09f4 Binary files /dev/null and b/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png differ diff --git a/data/image/game/scenes/commencing_slaughter/commencing_slaughter_4.png b/data/image/game/scenes/commencing_slaughter/commencing_slaughter_4.png new file mode 100644 index 0000000..5f3d6ac Binary files /dev/null and b/data/image/game/scenes/commencing_slaughter/commencing_slaughter_4.png differ diff --git a/data/image/ui/icons/checkbox_active.png b/data/image/ui/icons/checkbox_active.png new file mode 100644 index 0000000..9722bab Binary files /dev/null and b/data/image/ui/icons/checkbox_active.png differ diff --git a/data/image/ui/icons/checkbox_neutral.png b/data/image/ui/icons/checkbox_neutral.png new file mode 100644 index 0000000..9dffd30 Binary files /dev/null and b/data/image/ui/icons/checkbox_neutral.png differ diff --git a/data/image/ui/icons/settings_hover.png b/data/image/ui/icons/settings_hover.png new file mode 100644 index 0000000..afeee97 Binary files /dev/null and b/data/image/ui/icons/settings_hover.png differ diff --git a/data/image/ui/icons/settings_neutral.png b/data/image/ui/icons/settings_neutral.png new file mode 100644 index 0000000..b46ff1d Binary files /dev/null and b/data/image/ui/icons/settings_neutral.png differ diff --git a/data/image/ui/menu_main/start_hover.png b/data/image/ui/menu_main/start_hover.png new file mode 100644 index 0000000..b79d007 Binary files /dev/null and b/data/image/ui/menu_main/start_hover.png differ diff --git a/data/image/ui/menu_main/start_neutral.png b/data/image/ui/menu_main/start_neutral.png new file mode 100644 index 0000000..22a799d Binary files /dev/null and b/data/image/ui/menu_main/start_neutral.png differ diff --git a/data/image/ui/screen_init/init_bar_clear.png b/data/image/ui/screen_init/init_bar_clear.png new file mode 100644 index 0000000..1c4a528 Binary files /dev/null and b/data/image/ui/screen_init/init_bar_clear.png differ diff --git a/data/image/ui/screen_init/init_bar_fill.png b/data/image/ui/screen_init/init_bar_fill.png new file mode 100644 index 0000000..806cff9 Binary files /dev/null and b/data/image/ui/screen_init/init_bar_fill.png differ diff --git a/data/image/ui/screen_init/init_bar_left.png b/data/image/ui/screen_init/init_bar_left.png new file mode 100644 index 0000000..7b029a7 Binary files /dev/null and b/data/image/ui/screen_init/init_bar_left.png differ diff --git a/data/image/ui/screen_init/init_bar_right.png b/data/image/ui/screen_init/init_bar_right.png new file mode 100644 index 0000000..092b98a Binary files /dev/null and b/data/image/ui/screen_init/init_bar_right.png differ diff --git a/data/image/ui/screen_init/mushroom_silhouette_1.png b/data/image/ui/screen_init/mushroom_silhouette_1.png new file mode 100755 index 0000000..6f489b2 Binary files /dev/null and b/data/image/ui/screen_init/mushroom_silhouette_1.png differ diff --git a/data/image/ui/screen_init/mushroom_silhouette_2.png b/data/image/ui/screen_init/mushroom_silhouette_2.png new file mode 100755 index 0000000..bf3bdc0 Binary files /dev/null and b/data/image/ui/screen_init/mushroom_silhouette_2.png differ diff --git a/data/sound/game/entity/ambient/Evil Laugh Cackle-SoundBible.com-957382653.wav b/data/sound/game/entity/ambient/Evil Laugh Cackle-SoundBible.com-957382653.wav new file mode 100644 index 0000000..945767a Binary files /dev/null and b/data/sound/game/entity/ambient/Evil Laugh Cackle-SoundBible.com-957382653.wav differ diff --git a/data/sound/game/entity/ambient/Monster Growl-SoundBible.com-344645592.wav b/data/sound/game/entity/ambient/Monster Growl-SoundBible.com-344645592.wav new file mode 100644 index 0000000..0b046bb Binary files /dev/null and b/data/sound/game/entity/ambient/Monster Growl-SoundBible.com-344645592.wav differ diff --git a/data/sound/game/entity/death/Bone Crushing.mp3-SoundBible.com-537581287.wav b/data/sound/game/entity/death/Bone Crushing.mp3-SoundBible.com-537581287.wav new file mode 100644 index 0000000..01380e5 Binary files /dev/null and b/data/sound/game/entity/death/Bone Crushing.mp3-SoundBible.com-537581287.wav differ diff --git a/data/sound/game/entity/death/Decapitation Head Blood-SoundBible.com-310864499.mp3 b/data/sound/game/entity/death/Decapitation Head Blood-SoundBible.com-310864499.mp3 new file mode 100644 index 0000000..ca0e8bf Binary files /dev/null and b/data/sound/game/entity/death/Decapitation Head Blood-SoundBible.com-310864499.mp3 differ diff --git a/data/sound/game/entity/death/Psycho Scream-SoundBible.com-1441943673.wav b/data/sound/game/entity/death/Psycho Scream-SoundBible.com-1441943673.wav new file mode 100644 index 0000000..81f1619 Binary files /dev/null and b/data/sound/game/entity/death/Psycho Scream-SoundBible.com-1441943673.wav differ diff --git a/data/sound/game/entity/death/Zombie Long Death-SoundBible.com-554299929.wav b/data/sound/game/entity/death/Zombie Long Death-SoundBible.com-554299929.wav new file mode 100644 index 0000000..21a17ed Binary files /dev/null and b/data/sound/game/entity/death/Zombie Long Death-SoundBible.com-554299929.wav differ diff --git a/data/sound/game/entity/death/jenna_cough.mp3 b/data/sound/game/entity/death/jenna_cough.mp3 new file mode 100644 index 0000000..93fca95 Binary files /dev/null and b/data/sound/game/entity/death/jenna_cough.mp3 differ diff --git a/data/sound/game/entity/death/neck_snap-Vladimir-719669812.wav b/data/sound/game/entity/death/neck_snap-Vladimir-719669812.wav new file mode 100644 index 0000000..f0e7bc0 Binary files /dev/null and b/data/sound/game/entity/death/neck_snap-Vladimir-719669812.wav differ diff --git a/data/sound/game/environment/Killer Movie Scene-SoundBible.com-208071934.wav b/data/sound/game/environment/Killer Movie Scene-SoundBible.com-208071934.wav new file mode 100644 index 0000000..a236756 Binary files /dev/null and b/data/sound/game/environment/Killer Movie Scene-SoundBible.com-208071934.wav differ diff --git a/data/sound/game/environment/Tornado Siren-SoundBible.com-897026957.wav b/data/sound/game/environment/Tornado Siren-SoundBible.com-897026957.wav new file mode 100644 index 0000000..6f9988f Binary files /dev/null and b/data/sound/game/environment/Tornado Siren-SoundBible.com-897026957.wav differ diff --git a/data/sound/game/player/Shells_falls-Marcel-829263474.wav b/data/sound/game/player/Shells_falls-Marcel-829263474.wav new file mode 100644 index 0000000..d412579 Binary files /dev/null and b/data/sound/game/player/Shells_falls-Marcel-829263474.wav differ diff --git a/data/sound/game/scenes/commencing_slaughter.mp3 b/data/sound/game/scenes/commencing_slaughter.mp3 new file mode 100644 index 0000000..786ca55 Binary files /dev/null and b/data/sound/game/scenes/commencing_slaughter.mp3 differ diff --git a/data/sound/game/weapon/9_mm_gunshot-mike-koenig-123.wav b/data/sound/game/weapon/9_mm_gunshot-mike-koenig-123.wav new file mode 100644 index 0000000..241dec1 Binary files /dev/null and b/data/sound/game/weapon/9_mm_gunshot-mike-koenig-123.wav differ diff --git a/data/sound/game/weapon/Chain Saw-SoundBible.com-247695753.wav b/data/sound/game/weapon/Chain Saw-SoundBible.com-247695753.wav new file mode 100644 index 0000000..35bc6de Binary files /dev/null and b/data/sound/game/weapon/Chain Saw-SoundBible.com-247695753.wav differ diff --git a/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav b/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav new file mode 100644 index 0000000..c8994eb Binary files /dev/null and b/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav differ diff --git a/data/sound/game/weapon/GUN_FIRE-GoodSoundForYou-820112263.wav b/data/sound/game/weapon/GUN_FIRE-GoodSoundForYou-820112263.wav new file mode 100644 index 0000000..548b15e Binary files /dev/null and b/data/sound/game/weapon/GUN_FIRE-GoodSoundForYou-820112263.wav differ diff --git a/data/sound/game/weapon/High_Definition_Machine_gun-WEL-2006923900.wav b/data/sound/game/weapon/High_Definition_Machine_gun-WEL-2006923900.wav new file mode 100644 index 0000000..28cd538 Binary files /dev/null and b/data/sound/game/weapon/High_Definition_Machine_gun-WEL-2006923900.wav differ diff --git a/data/sound/game/weapon/Minigun-Jim_Rogers-633894726.wav b/data/sound/game/weapon/Minigun-Jim_Rogers-633894726.wav new file mode 100644 index 0000000..28ef370 Binary files /dev/null and b/data/sound/game/weapon/Minigun-Jim_Rogers-633894726.wav differ diff --git a/data/sound/game/weapon/Shotgun_Blast-Jim_Rogers-1914772763.wav b/data/sound/game/weapon/Shotgun_Blast-Jim_Rogers-1914772763.wav new file mode 100644 index 0000000..bb2e6cd Binary files /dev/null and b/data/sound/game/weapon/Shotgun_Blast-Jim_Rogers-1914772763.wav differ diff --git a/data/sound/ui/menu_main/Backwards-Souls-SoundBible.com-87826574.wav b/data/sound/ui/menu_main/Backwards-Souls-SoundBible.com-87826574.wav new file mode 100644 index 0000000..b7981d0 Binary files /dev/null and b/data/sound/ui/menu_main/Backwards-Souls-SoundBible.com-87826574.wav differ diff --git a/lib/__pycache__/effect.cpython-311.pyc b/lib/__pycache__/effect.cpython-311.pyc new file mode 100644 index 0000000..b42615c Binary files /dev/null and b/lib/__pycache__/effect.cpython-311.pyc differ diff --git a/lib/__pycache__/entities.cpython-311.pyc b/lib/__pycache__/entities.cpython-311.pyc new file mode 100644 index 0000000..6543c1e Binary files /dev/null and b/lib/__pycache__/entities.cpython-311.pyc differ diff --git a/lib/__pycache__/entity.cpython-311.pyc b/lib/__pycache__/entity.cpython-311.pyc new file mode 100644 index 0000000..5b045c7 Binary files /dev/null and b/lib/__pycache__/entity.cpython-311.pyc differ diff --git a/lib/__pycache__/interface.cpython-311.pyc b/lib/__pycache__/interface.cpython-311.pyc new file mode 100644 index 0000000..179d04d Binary files /dev/null and b/lib/__pycache__/interface.cpython-311.pyc differ diff --git a/lib/__pycache__/level.cpython-311.pyc b/lib/__pycache__/level.cpython-311.pyc new file mode 100644 index 0000000..7ff8633 Binary files /dev/null and b/lib/__pycache__/level.cpython-311.pyc differ diff --git a/lib/__pycache__/levels.cpython-311.pyc b/lib/__pycache__/levels.cpython-311.pyc new file mode 100644 index 0000000..d860cad Binary files /dev/null and b/lib/__pycache__/levels.cpython-311.pyc differ diff --git a/lib/__pycache__/scene.cpython-311.pyc b/lib/__pycache__/scene.cpython-311.pyc new file mode 100644 index 0000000..8e38db8 Binary files /dev/null and b/lib/__pycache__/scene.cpython-311.pyc differ diff --git a/lib/__pycache__/scenes.cpython-311.pyc b/lib/__pycache__/scenes.cpython-311.pyc new file mode 100644 index 0000000..32b2b4e Binary files /dev/null and b/lib/__pycache__/scenes.cpython-311.pyc differ diff --git a/lib/__pycache__/section.cpython-311.pyc b/lib/__pycache__/section.cpython-311.pyc new file mode 100644 index 0000000..ca15bf0 Binary files /dev/null and b/lib/__pycache__/section.cpython-311.pyc differ diff --git a/lib/__pycache__/sections.cpython-311.pyc b/lib/__pycache__/sections.cpython-311.pyc new file mode 100644 index 0000000..763983f Binary files /dev/null and b/lib/__pycache__/sections.cpython-311.pyc differ diff --git a/lib/__pycache__/system.cpython-311.pyc b/lib/__pycache__/system.cpython-311.pyc new file mode 100644 index 0000000..ae825db Binary files /dev/null and b/lib/__pycache__/system.cpython-311.pyc differ diff --git a/lib/effect.py b/lib/effect.py new file mode 100644 index 0000000..88b5229 --- /dev/null +++ b/lib/effect.py @@ -0,0 +1,51 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: Intro to Pygame. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: Effects for entities. +# +# @Script: effect.py +# @Date Created: 11 Jun, 2024 +# @Last Modified: 11 Jun, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +from pygame.image import load +from pygame.transform import scale +from lib.system import DIRWORKING + +class War_Paint(): + def __init__(self, game, character): + """ + War paint for player. + """ + + self.GAME = game + + self.imagePaths = {} + self.images = {} + + self.update_images(character) + + def function(): + """ + War Paint is just a texture effect. + """ + + pass + + def update_images(self, character): + """ + Updates effect images. + """ + + for key in ["neutral_1", "neutral_2", "run_l_1", "run_l_2", "run_r_1", "run_r_2", "run_u_1", "run_u_2", "run_d_1", "run_d_2"]: + self.imagePaths[key] = DIRWORKING + f"/data/image/game/player/{character}/war_paint/{key}.png" + + for key, value in self.imagePaths.items(): + self.images[key] = load(self.imagePaths[key]).convert_alpha() + self.images[key] = scale(self.images[key], (self.GAME.SCALE.unitBlockPx, self.GAME.SCALE.unitBlockPx)) \ No newline at end of file diff --git a/lib/entity.py b/lib/entity.py new file mode 100644 index 0000000..f3d1336 --- /dev/null +++ b/lib/entity.py @@ -0,0 +1,617 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: Players and Characters. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: AIs for players and characters. +# +# @Script: entity.py +# @Date Created: 22 Mar, 2024 +# @Last Modified: 19 Jun, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +import pygame +from pygame import mixer +from icecream import ic +from random import randint +from lib.system import DIRWORKING, FEATURES +from lib.section import Blocks + + +class Player(): + """ + Handles the player character and associated animations and states. + """ + + def __init__(self, game, character:str = "default_a"): + self.GAME = game + self.WINDOW = self.GAME.WINDOW + self.SCALE = self.GAME.SCALE + self.LEVEL = self.GAME.LEVEL + + self.images = {} + + self.x = 0 + self.y = 0 + self.layer = 1 + + self.health = 100 + self.effects = [] + + self.update_images(character) + + self.animationClock = 0 + self.stateDirection = "neutral" # String of character state + self.stateDirectionOld = "run_d" # Last movement action by player. + self.stateDirectionOldX = "run_l" # Last x movement action for rotating to neutral from run_u + self.stateFrame = 1 # Animation frame number + self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"] # Default inital character state. + self.timeSinceInteraction = 0 + self.moveClock = 0 + + self.update_scale() + + def adjust_center(self, x, y): + """ + Determines if the player can be centered on the level instead of shifting position from center. + """ + + # if newPosition is greater than half the viewable area relative to dx if it were adjusted. + # less efficient: + # if dx were adjusted, if any wall becomes only void, adjust player position. + + dCenterX = self.SCALE.gameX / 2 + dCenterY = self.SCALE.gameY / 2 + + if x <= dCenterX and self.LEVEL.dx == 0: + # Player in left region of layout. + # Player must move. + pass + elif x > (self.LEVEL.width - dCenterX) + 1: + # Player in right region of layout. + # Player must move. + pass + else: + # Level should move. + + newDx = self.LEVEL.dx + (x - self.x) + + if newDx >= 0: + self.LEVEL.dx = newDx + + if y <= dCenterY and self.LEVEL.dy == 0: + # Player in top region of layout + # Player must move. + pass + elif y > (self.LEVEL.height - dCenterY) + 1: + # Player in bottom region of layout + # Player must move. + pass + else: + # Level should move. + newDy = self.LEVEL.dy + (y - self.y) + + if newDy >= 0: + self.LEVEL.dy = newDy + + self.x = x + self.y = y + + def attack(self): + """ + Makes the player attack. + """ + + attackPositions = [] + + if self.stateDirection == "run_l": + x = self.x - 1 + + attackPositions.append((x, self.y - 1)) + attackPositions.append((x, self.y)) + attackPositions.append((x, self.y + 1)) + + elif self.stateDirection == "run_r": + x = self.x + 1 + + attackPositions.append((x, self.y - 1)) + attackPositions.append((x, self.y)) + attackPositions.append((x, self.y + 1)) + + elif self.stateDirection == "run_u": + y = self.y - 1 + + attackPositions.append((self.x - 1, y)) + attackPositions.append((self.x, y)) + attackPositions.append((self.x + 1, y)) + + elif self.stateDirection == "run_d": + y = self.y + 1 + + attackPositions.append((self.x - 1, y)) + attackPositions.append((self.x, y)) + attackPositions.append((self.x + 1, y)) + + for entity in self.GAME.entityList: + if (entity.x, entity.y) in attackPositions: + entity.health = entity.health - 25 + + def can_interact(self, x, y): + """ + Checks if the player can interact with the section in front of it. + """ + + try: # Try to set the section based on coords. + coords = (x, y) + section = self.LEVEL.layout.LAYOUT[coords] + except KeyError: + for coords, block in self.LEVEL.layout.LAYOUT.items(): # Deeper search, check if block in an area fill. + if block.layer == self.layer: + try: + if y in range(coords[1], coords[3] + 1): + if x in range(coords[0], coords[2] + 1): + section = self.LEVEL.layout.LAYOUT[coords] # Set the definition as the desired section. + break + except IndexError: # Block doesn't define a range. + pass + + try: + if section.TEXTURE[section.state]["IS_INTERACTABLE"]: + return coords + else: + return False + except UnboundLocalError: # Could not find a block drawn at those coords. + return False + + def can_move(self, x, y): + """ + Checks if the player can move to a position. + """ + + try: # Try to set the section based on coords. + section = self.LEVEL.layout.LAYOUT[(x, y)] + except KeyError: + layerLast = -1 + + for coords, block in self.LEVEL.layout.LAYOUT.items(): # Deeper search, check if block in an area fill. + if block.layer <= self.layer and block.layer >= layerLast: + try: + if y in range(coords[1], coords[3] + 1): + if x in range(coords[0], coords[2] + 1): + section = self.LEVEL.layout.LAYOUT[coords] # Set the definition as the desired section. + layerLast = block.layer + except IndexError: # Block doesn't define a range. + pass + + try: + if section.TEXTURE[section.state]["IS_COLLIDABLE"]: + return False + else: + for entity in self.GAME.entityList: + if entity.x == x and entity.y == y: # If there is an entity at this location, cannot move. + return False + return True + except UnboundLocalError: # Could not find a block drawn at those coords. + return False + + def dash(self, direction): + """ + Dashes player in gameX and gameY. + """ + + x, y = self.x, self.y + + dashDist = 0 + + canDash = False + + # If Player is not moving to same position as currently located + while dashDist != 5: + dashDist = dashDist + 1 + + if FEATURES["matchcase"]["is_active"]: + match direction: + case "run_l": + x = self.x - dashDist + self.stateDirectionOldX = direction + case "run_r": + x = self.x + dashDist + self.stateDirectionOldX = direction + case "run_u": + y = self.y - dashDist + case "run_d": + y = self.y + dashDist + + # Iterate until player is trying to move to a position that it can move. + elif direction == "run_l": + x = self.x - dashDist + self.stateDirectionOldX = direction + elif direction == "run_r": + x = self.x + dashDist + self.stateDirectionOldX = direction + elif direction == "run_u": + y = self.y - dashDist + elif direction == "run_d": + y = self.y + dashDist + + if self.can_move(x, y): + canDash = True + moveX, moveY = x, y + else: # Prevent warping through blocks if dashing immediately against them. + moveX, moveY = self.x, self.y + canDash = False + break + + if canDash: + self.adjust_center(moveX, moveY) + self.moveClock = 0 + + self.update_animation(direction, is_updateMove = True) + + def draw(self, direction): + """ + Draw the player on the screen. + """ + + self.update_move(direction) + + location = ((self.x - self.LEVEL.dx) * self.SCALE.unitBlockPx, (self.y - self.LEVEL.dy) * self.SCALE.unitBlockPx) + + self.WINDOW.window.blit(self.stateImage, location) + + for effect in self.effects: + self.WINDOW.window.blit(effect.images[f"{self.stateDirection}_{self.stateFrame}"], location) + + def interact(self): + """ + Interacts with the object in front of the player. + """ + + x = self.x + y = self.y + + if self.stateDirection == "neutral": + if self.stateDirectionOld == "run_l": + x = self.x - 1 + elif self.stateDirectionOld == "run_r": + x = self.x + 1 + elif self.stateDirectionOld == "run_u": + y = self.y - 1 + elif self.stateDirectionOld == "run_d": + y = self.y + 1 + else: + if self.stateDirection == "run_l": + x = self.x - 1 + elif self.stateDirection == "run_r": + x = self.x + 1 + elif self.stateDirection == "run_u": + y = self.y - 1 + elif self.stateDirection == "run_d": + y = self.y + 1 + + coords = self.can_interact(x, y) + + if isinstance(coords, tuple): + section = self.LEVEL.layout.LAYOUT[coords] + + if section.layer == self.layer: + section.on_interact() + + def update_animation(self, direction: str = None, is_updateMove = False, ovrrScaling = False): + """ + Sets the current animation image of the player. + """ + + self.animationClock = self.animationClock + 1 + + # Check time since interacted. + if is_updateMove == True: + self.timeSinceInteraction = 0 + elif self.stateDirection != "neutral" and is_updateMove == False: + self.timeSinceInteraction = self.timeSinceInteraction + 1 + + if ovrrScaling: # Replace current image with scaled. + self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"] + elif self.animationClock >= (self.GAME.clock.get_fps() / 3) or (direction is not None and self.stateDirection != direction): + # Update animation every half a second + # Or when direction has changed. + + if self.stateFrame == 1: + self.stateFrame = 2 + elif self.stateFrame == 2: + self.stateFrame = 1 + + # Change Direction + if direction is not None and self.stateDirection != direction: # If direction has changed, update the animation at the same time. + self.stateDirection = direction + + # Make player idle if not moving for 3 frame updates. + if self.timeSinceInteraction > (3 * (self.GAME.clock.get_fps() / 2)): + if self.stateDirection == "run_u": + self.stateDirection = self.stateDirectionOldX + #ic("Returning to neutral via the last L/R direction") + else: + self.stateDirection = "neutral" + self.timeSinceInteraction = 0 + #ic("Returning to neutral and resetting.") + + self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"] + + self.animationClock = 0 + + def update_images(self, character): + """ + Updates animation images based on the character. + """ + + self.character = character + + for key in ["neutral_1", "neutral_2", "run_l_1", "run_l_2", "run_r_1", "run_r_2", "run_u_1", "run_u_2", "run_d_1", "run_d_2"]: + self.images[key] = pygame.image.load(DIRWORKING + f"/data/image/game/player/{self.character}/base/{key}.png").convert_alpha() + + for effect in self.effects: + effect.update_images(self.character) + + def update_move(self, direction:str): + """ + Moves player in gameX and gameY. + """ + + x = self.x + y = self.y + + self.stateDirectionOld = self.stateDirection + + if direction == "run_l": + x = self.x - 1 + self.stateDirectionOldX = direction + elif direction == "run_r": + x = self.x + 1 + self.stateDirectionOldX = direction + elif direction == "run_u": + y = self.y - 1 + elif direction == "run_d": + y = self.y + 1 + + if direction != "neutral": + if self.can_move(x, y) and self.moveClock >= self.GAME.clock.get_fps() / 30: + self.adjust_center(x, y) + self.moveClock = 0 + else: + self.moveClock = self.moveClock + 1 + + self.update_animation(direction, is_updateMove = True) + else: + self.update_animation() + + def update_scale(self): + """ + Updates the scaling of all player textures. + """ + + self.update_images(self.character) + + self.rect = (self.SCALE.unitBlockPx, self.SCALE.unitBlockPx) + + # Scale each image + for key, value in self.images.items(): + self.images[key] = pygame.transform.scale(value, self.rect) + + for effect in self.effects: + effect.update_scale(self.character) + + self.update_animation(ovrrScaling=True) + +class Unnamed(): + """ + Spawns one of The Unnamed. + """ + + def __init__(self, game, x, y, layer=1): + self.GAME = game + self.WINDOW = self.GAME.WINDOW + self.SCALE = self.GAME.SCALE + self.LEVEL = self.GAME.LEVEL + self.PLAYER = self.GAME.PLAYER + + self.images = {} + + self.x = x + self.y = y + self.layer = layer + + self.health = 25 + + for k in ["neutral_1", "neutral_2", "run_l_1", "run_l_2", "run_r_1", "run_r_2", "run_u_1", "run_u_2", "run_d_1", "run_d_2"]: + self.images[k] = "" + + self.update_images() + + self.animationClock = 0 + self.stateDirection = "neutral" # String of character state + self.stateDirectionOld = "run_d" # Last movement action by player. + self.stateFrame = 1 # Animation frame number + self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"] # Default inital character state. + self.timeSinceInteraction = 0 + self.actionClock = 0 + + self.update_scale() + + self.GAME.entityList.append(self) + + def attack(self): + """ + Makes the unnamed attack player. + """ + + self.PLAYER.health = self.PLAYER.health - 25 + self.actionClock = pygame.time.get_ticks() + + def calc_x(self): + """ + Calculates an x value movement based on PLAYER. + """ + + if self.x > self.PLAYER.x: + self.stateDirectionOld = self.stateDirection + self.stateDirection = "run_l" + return self.x - 1, self.y + elif self.x < self.PLAYER.x: + self.stateDirectionOld = self.stateDirection + self.stateDirection = "run_r" + return self.x + 1, self.y + elif self.x == self.PLAYER.x and self.y == self.PLAYER.y: + return self.x, self.y + else: + return self.calc_y() + + def calc_y(self): + """ + Calculates a y value movement based on PLAYER. + """ + + if self.y > self.PLAYER.y: + self.stateDirectionOld = self.stateDirection + self.stateDirection = "run_u" + return self.x, self.y - 1 + elif self.y < self.PLAYER.y: + self.stateDirectionOld = self.stateDirection + self.stateDirection = "run_d" + return self.x, self.y + 1 + elif self.x == self.PLAYER.x and self.y == self.PLAYER.y: + return self.x, self.y + else: + return self.calc_x() + + def can_move(self, x:int, y:int): + """ + Checks if the player can move to a position. + """ + + try: # Try to set the section based on coords. + section = self.LEVEL.layout.LAYOUT[(x, y)] + except KeyError: + layerLast = -1 + + for coords, block in self.LEVEL.layout.LAYOUT.items(): # Deeper search, check if block in an area fill. + if block.layer <= self.layer and block.layer >= layerLast: + try: + if y in range(coords[1], coords[3] + 1): + if x in range(coords[0], coords[2] + 1): + section = self.LEVEL.layout.LAYOUT[coords] # Set the definition as the desired section. + layerLast = block.layer + except IndexError: # Block doesn't define a range. + pass + + try: + if section.TEXTURE[section.state]["IS_COLLIDABLE"]: + return False + elif self.PLAYER.x == x and self.PLAYER.y == y: # If player at this location, attack. + self.attack() + return False + else: + for entity in self.GAME.entityList: + if entity.x == x and entity.y == y: # If there is an entity at this location, cannot move. + return False + return True + except UnboundLocalError: # Could not find a block drawn at those coords. + return False + + def draw(self): + """ + Draw the unnamed on the screen. + """ + + self.update_move() + self.update_animation() + + self.WINDOW.window.blit(self.stateImage, ((self.x - self.LEVEL.dx) * self.SCALE.unitBlockPx, (self.y - self.LEVEL.dy) * self.SCALE.unitBlockPx)) + + def destroy(self): + """ + Removes the unnamed from the entityList. + """ + + if self.GAME.levelOn and len(self.GAME.entityList) > 1: + self.LEVEL.layout.LAYOUT[(self.x, self.y)] = Blocks.puddle_of_blood(self.GAME, self.layer) + deathSound = mixer.Sound(DIRWORKING + "/data/sound/game/entity/death/jenna_cough.mp3") + deathSound.set_volume(0.2) + deathSound.play() + + temp = [] + + for entity in self.GAME.entityList: + if entity != self: + temp.append(entity) + + self.GAME.entityList = temp + + del self + + def update_animation(self, ovrrScaling = False): + """ + Sets the current animation image of the unnamed. + """ + + self.animationClock = self.animationClock + 1 + + if ovrrScaling: # Replace current image with scaled. + self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"] + elif self.animationClock >= (self.GAME.clock.get_fps() / 2): # Update animation every half a second. + + if self.stateFrame == 1: + self.stateFrame = 2 + elif self.stateFrame == 2: + self.stateFrame = 1 + + self.stateImage = self.images[f"{self.stateDirection}_{self.stateFrame}"] + + self.animationClock = 0 + + def update_images(self): + """ + Updates animation images . + """ + + for key, value in self.images.items(): + self.images[key] = pygame.image.load(DIRWORKING + f"/data/image/game/entity/unnamed/{key}.png").convert_alpha() + + def update_move(self): + """ + Updates the position of the unnamed. + """ + + if pygame.time.get_ticks() - self.actionClock >= 1000: + + if bool(randint(0, 1)): + x, y = self.calc_x() + else: + x, y = self.calc_y() + + if self.can_move(x, y): + self.x = x + self.y = y + self.actionClock = pygame.time.get_ticks() + else: + self.stateDirection = self.stateDirectionOld + + self.update_animation() + + def update_scale(self): + """ + Updates the scaling of all unnamed textures. + """ + + self.update_images() + + self.rect = (self.SCALE.unitBlockPx, self.SCALE.unitBlockPx) + + # Scale each image + for key, value in self.images.items(): + self.images[key] = pygame.transform.scale(value, self.rect) + + self.update_animation(ovrrScaling=True) \ No newline at end of file diff --git a/lib/interface.py b/lib/interface.py new file mode 100644 index 0000000..61e3fb7 --- /dev/null +++ b/lib/interface.py @@ -0,0 +1,300 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: Interface Items. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: Interface items for menus and scenes in a pygame game. +# +# @Script: interface.py +# @Date Created: 22 Mar, 2024 +# @Last Modified: 13 Jun, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +import pygame +from lib.system import FEATURES + + +class Button(): + """ + Creates a button on a window. + """ + + def __init__(self, game, name, x, y, image_neutral, image_hover, function, args=None): + self.TYPE = "button" + + width = image_neutral.get_width() # Can use neutral image as all images should have identical sizes. + height = image_neutral.get_height() + + self.GAME = game + self.WINDOW = self.GAME.WINDOW + scale = self.GAME.SCALE.scaleUi + + self.function = function + self.functionArgs = args + + self.ID = name + + self.IMAGES = { + "neutral": pygame.transform.scale(image_neutral, (width * scale, height * scale)), + "hover": pygame.transform.scale(image_hover, (width * scale, height * scale)) + } + + Interface_Functions.convert_images(self) + + self.rect = self.IMAGES["neutral"].get_rect() + self.rect.topleft = (x * scale, y * scale) + + # Cannot overwrite rect.collidepoint with Interface_Functions.collidepoint. + + self.time_delay = 0 + + self.GAME.gameInteractables.append(self) # Add to button to refresh. + + def draw(self): + """ + Draws button on screen. + """ + + Interface_Functions.draw(self) + + def is_hovered(self): + """ + Checks if button is hovered. + """ + + return Interface_Functions.collidepoint(self, pygame.mouse.get_pos()) + + def remove(self): + """ + Clears button from screen. + """ + + Interface_Functions.destroy(self) + +class Progress_bar(): + """ + Creates a progress bar that updates based on a scale. + """ + + def __init__(self, game, x, y, barWidth:int, bobberImages:list, barImageLeft, barImageRight, barImageClear, barImageFill): + """ + x: distance in percent from window origin. + y: distance in percent from window origin. + barWidth: Percent of window width filled. + """ + + self.GAME = game + self.WINDOW = self.GAME.WINDOW + + self.x = (x / 100) * self.WINDOW.width + self.y = (y / 100) * self.WINDOW.height + + for image in bobberImages: + image.convert_alpha() + + self.bobberImages = bobberImages + + self.barImages = { + "left": barImageLeft.convert_alpha(), + "right": barImageRight.convert_alpha(), + "clear": barImageClear.convert_alpha(), + "fill": barImageFill.convert_alpha() + } + + self.bobberState = 0 + self.bobberStateRange = len(bobberImages) + + self.widthLeft = self.barImages["left"].get_width() + self.widthRight = self.barImages["right"].get_width() + + self.barFillWidth = int("%.0f" % ((self.WINDOW.width * (barWidth / 100)) - (self.widthLeft + self.widthRight))) + + self.barHeight = self.barImages["left"].get_height() + self.bobberHeight = self.bobberImages[self.bobberState].get_height() + + self.barCenterOffset = (self.barHeight / 2) - (self.barImages["clear"].get_height() / 2) + + self.bobberTimer = 0 + + def draw(self, rangeComplete, rangeTotal, framerate=30): + + self.bobberTimer = self.bobberTimer + 1 + + rangeComplete = int("%.0f" % (rangeComplete * self.barFillWidth / rangeTotal)) + + self.WINDOW.window.blit(self.barImages["left"], (self.x, self.y)) + self.WINDOW.window.blit(self.barImages["right"], (self.x + self.widthLeft + self.barFillWidth, self.y)) + + for i in range(0, rangeComplete): + self.WINDOW.window.blit(self.barImages["fill"], (self.x + self.widthLeft + i, self.y + self.barCenterOffset)) + + for i in range(rangeComplete, self.barFillWidth): + self.WINDOW.window.blit(self.barImages["clear"], (self.x + self.widthLeft + i, self.y + self.barCenterOffset)) + + if (self.bobberTimer / framerate) > 0.25: + self.bobberTimer = 0 + if self.bobberState == self.bobberStateRange - 1: + self.bobberState = 0 + else: + self.bobberState = self.bobberState + 1 + + self.WINDOW.window.blit(self.bobberImages[self.bobberState], (self.x + rangeComplete, self.y - (self.bobberHeight / 2))) + +class Checkbox(): + """ + Creates a Checkbox on a window. + """ + + def __init__(self, game, name, x, y, image_neutral, image_hover, image_active, condition, function=None, args=None): + self.TYPE = "checkbox" + + width = image_neutral.get_width() # Can use neutral image as all images should have identical sizes. + height = image_neutral.get_height() + + self.GAME = game + self.WINDOW = self.GAME.WINDOW + scale = self.GAME.SCALE.scaleUi + + self.function = function + self.functionArgs = args + + self.ID = name + + self.IMAGES = { + "neutral": pygame.transform.scale(image_neutral, (width * scale, height * scale)), + "hover": pygame.transform.scale(image_hover, (width * scale, height * scale)), + "active": pygame.transform.scale(image_active, (width * scale, height * scale)) + } + + Interface_Functions.convert_images(self) + + self.rect = self.image_neutral.get_rect() + self.rect.topleft = (x * scale, y * scale) + + self.time_delay = 0 + + self.condition = condition + + self.GAME.gameInteractables.append(self) # Add to checkbox to refresh. + + def draw(self): + """ + Draws checkbox on screen. + """ + + Interface_Functions.draw(self) + + def is_hovered(self): + """ + Checks if checkbox is hovered. + """ + + return Interface_Functions.is_hovered(self) + + def remove(self): + """ + Clears checkbox from screen. + """ + + Interface_Functions.destroy(self) + + +class Interface_Functions(): + """ + All functions called by interface objects. + """ + + def __init__(self, game): + self.GAME = game + + def convert_images(object): + """ + Converts the object's images to pygame format increasing performance. + """ + + temp = {} + + for image in object.IMAGES.items(): + temp[image[0]] = image[1].convert_alpha() + + object.IMAGES = temp + + def destroy(object): + """ + Destroys the interface object. + """ + + temp = [] + + for interactable in object.GAME.gameInteractables: + if interactable != object: + temp.append(interactable) + + object.GAME.gameInteractables = temp + + del object + + def draw(object): + """ + Draws interface object. + """ + + if object.TYPE == "checkbox" and object.condition(): + object.WINDOW.window.blit(object.IMAGES["active"], (object.rect.x, object.rect.y)) + elif object.is_hovered(): + object.WINDOW.window.blit(object.IMAGES["hover"], (object.rect.x, object.rect.y)) + else: + object.WINDOW.window.blit(object.IMAGES["neutral"], (object.rect.x, object.rect.y)) + + def enter_fullscreen(self): + """ + Enters Fullscreen if possible. + """ + + if self.GAME.WINDOW.toggle_fullscreen(): + FEATURES["Fullscreen"]["is_active"] = True + + def exit_to_desktop(self): + + self.GAME.levelOn = False + self.GAME.menuOn = False + self.GAME.sceneOn = False + self.GAME.gameOn = False + + def collidepoint(object, coords:tuple): + """ + Rewritten version of pygame rect.collidepoint. + """ + + x, y, width, height = object.rect + dx = width + x + dy = height + y + + + if coords[0] in range(x, dx + 1) and coords[1] in range(y, dy + 1): + return True + else: + return False + + def is_hovered(object): + """ + Checks if mouse is hovering interface object. + """ + + return Interface_Functions.collidepoint(object, pygame.mouse.get_pos()) + + def return_to_game(self): + """ + Exits a pause state. + """ + + for button in self.GAME.LEVEL.layout.INTERACTABLES: + button.remove() + + self.GAME.menuOn = False + + self.GAME.LEVEL.load_level_state() \ No newline at end of file diff --git a/lib/level.py b/lib/level.py new file mode 100644 index 0000000..43dadc8 --- /dev/null +++ b/lib/level.py @@ -0,0 +1,572 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: Levels in a game. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: Classes descibing different levels in a pygame game. +# +# @Script: level.py +# @Date Created: 16 Apr, 2024 +# @Last Modified: 19 Jun, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +import pygame +from pygame import mixer +from pygame.image import load +from icecream import ic +from lib.system import DIRWORKING, FEATURES +from lib.section import Section, Blocks +from lib.interface import Button, Checkbox +from lib.entity import * # Bad practise and loads player class, but loads all entities for easy reference. + + +class Level(): + """ + Creates and manages the level scape. + """ + + def __init__(self, game): + self.GAME = game + self.WINDOW = self.GAME.WINDOW + self.SCALE = self.GAME.SCALE + self.SECTION = Section(self.GAME) + self.FRAMES = Frames(self.GAME) + + self.layout = Menus.Blank() + + self.dx = 0 + self.dy = 0 + + self.musicStartTime = 0 + + if self.layout.TYPE == "level": + self.get_level_size() + + def draw(self): + """ + Draws the level on the screen. + """ + + if self.layout.TYPE == "level": + self.SECTION.draw(self.layout.LAYOUT) + elif self.layout.TYPE == "scene": + self.FRAMES.draw() + + try: + if pygame.time.get_ticks() - self.musicStartTime >= self.layout.MUSIC.get_length() * 1000: + ic(self.layout.MUSIC.play()) + self.musicStartTime = pygame.time.get_ticks() + except AttributeError: + pass + + def load_layout(self, layout:object, startPositionKey="default"): + """ + Load a new layout configuration. + """ + + self.GAME.playerActionMove = "neutral" + + try: + ic(self.layout.MUSIC.stop()) + except AttributeError: + pass + + if self.layout.TYPE == "menu": + for interactable in self.layout.INTERACTABLES: + interactable.remove() + + layout = layout(game=self.GAME) + + if isinstance(layout, Menus.Pause): # Save the level state before loading something that isn't the next level. + self.save_level_state() + + if self.layout.TYPE == "level" and layout.TYPE != "level": + self.GAME.levelOn = False + elif self.layout.TYPE == "menu" and layout.TYPE != "menu": + self.GAME.menuOn = False + elif self.layout.TYPE == "scene" and layout.TYPE != "scene": + self.GAME.sceneOn = False + + if self.layout.TYPE == "level": + for entity in self.GAME.entityList: + entity.destroy() + + self.layout = layout + ic(self.layout.__class__) + + if self.layout.TYPE == "level": + self.update_level_size() + + self.GAME.PLAYER.health = 100 + self.GAME.PLAYER.effects = [] + self.GAME.PLAYER.x = self.layout.STARTPOSITIONS[startPositionKey][0] + self.GAME.PLAYER.y = self.layout.STARTPOSITIONS[startPositionKey][1] + + self.update_deltas_relative_to_player() + + elif self.layout.TYPE == "scene": + self.FRAMES.load_images() + + # The following shouldn't need to exist, however pygame does not like using a low musicStartTime to trigger the first audio playback. + try: + self.layout.MUSIC.play() + self.musicStartTime = pygame.time.get_ticks() + except AttributeError: + pass + + def load_level_state(self): + """ + Loads the last state of a level. + """ + + self.layout = self.layoutOld + self.GAME.entityList = self.entityOld + self.GAME.levelLoad = True + + def save_level_state(self): + """ + Saves the last state of a level. + """ + + self.layoutOld = self.layout + self.entityOld = self.GAME.entityList + + def update_deltas_relative_to_player(self): + """ + Called with a scale update. + Recalculates self.dx and self.dy so that player is centered properly. + """ + + borderWidthX = (self.SCALE.gameX // 2) + 1 + borderWidthY = (self.SCALE.gameY // 2) + 1 + + if self.GAME.PLAYER.x < borderWidthX: + # Player is in left border + self.dx = 0 + + elif self.GAME.PLAYER.x > self.width - borderWidthX: + # Player is in right border + self.dx = self.width - self.SCALE.gameX + + else: + # Player is within x boundaries + self.dx = self.GAME.PLAYER.x - borderWidthX + + + if self.GAME.PLAYER.y < borderWidthY: + # Player is in the top border + self.dy = 0 + + elif self.GAME.PLAYER.y > self.height - borderWidthY: + # Player is in bottom border. + self.dy = self.height - self.SCALE.gameY + + else: + # Player is within y boundaries. + self.dy = self.GAME.PLAYER.y - borderWidthY + + self.dx = int(self.dx) + self.dy = int(self.dy) + + def update_level_size(self): + """ + Updates the size of the active layout + """ + width = 0 + height = 0 + + for coords, block in self.layout.LAYOUT.items(): + try: + if coords[2] > width: + width = coords[2] + + if coords[3] > height: + height = coords[3] + + except IndexError: + if coords[0] > width: + width = coords[0] + + if coords[1] > height: + height = coords[1] + + self.width = width + self.height = height + +""" +NAME :str +HAS_PLAYER :bool +TYPE :str = "scene", "menu", "level" + +scene # Does not scale with scaleGame or scaleUi + # Each obj in LAYOUT is a different frame, not block sections. + # Does not have a player, so HAS_PLAYER is not checked, and can be removed from definition. + +menu # Does not scale with scaleGame + # Has a custom event handler used while menu is active. Handler defined in run(). + +LAYOUT :dict = {(x, y, rangeX, rangeY): obj} +BGFILL :tuple = (r, g, b) + :pygame.surface.Surface +FOLLOW_UP_LEVEL :level.Levels, level.Menus, level.Scenes +""" + +class Scenes(): + """ + Class for organising scenes. + """ + + """ + class Example(): + def __init__(self): + self.IS_SKIPPABLE = True + self.LAYOUT = [ + { + "frame": Textures.EXAMPLE_0, # Frame image + "frameTime": 5 # Time in seconds that this frame is displayed. + }, + { + "frame": Textures.EXAMPLE_1, + "frameTime": 2 + } + ] + + """ + + class Intro(): + def __init__(self, **kwargs): + + self.TYPE = "scene" + self.LAYOUT = [ + DIRWORKING + "/data/image/game/blocks/wool_colored_blue.png", + DIRWORKING + "/data/image/game/blocks/wool_colored_cyan.png", + DIRWORKING + "/data/image/game/blocks/wool_colored_light_blue.png" + ] + self.FOLLOW_UP_LEVEL = Menus.Main + self.IS_SKIPPABLE = True + self.TIME_PER_FRAME = 1 + self.BG_FILL = (0, 0, 0) + + class Commencing_Slaughter(): + def __init__(self, **kwargs): + + self.TYPE = "scene" + self.LAYOUT = [ + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_4.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png", + DIRWORKING + "/data/image/game/scenes/commencing_slaughter/commencing_slaughter_3.png" + ] + self.FOLLOW_UP_LEVEL = Levels.Example + self.IS_SKIPPABLE = False + self.TIME_PER_FRAME = 0.25 + + self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/scenes/commencing_slaughter.mp3") + + class Player_Died(): + def __init__(self, **kwargs): + + self.TYPE = "scene" + self.LAYOUT = [ + DIRWORKING + "/data/image/game/blocks/wool_colored_red.png" + ] + self.FOLLOW_UP_LEVEL = Menus.Player_Died + self.IS_SKIPPABLE = False + + self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav") + + self.TIME_PER_FRAME = self.MUSIC.get_length() + + class Player_Won(): + def __init__(self, **kwargs): + + self.TYPE = "scene" + self.LAYOUT = [ + DIRWORKING + "/data/image/game/blocks/wool_colored_lime.png" + ] + self.FOLLOW_UP_LEVEL = Menus.Player_Won + self.IS_SKIPPABLE = True + + self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/game/weapon/Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav") + + self.TIME_PER_FRAME = self.MUSIC.get_length() + +class Menus(): + """ + Class for organising menus. + """ + + class Blank(): + def __init__(self, **kwargs): + self.TYPE = "menu" + self.HAS_PLAYER = False + self.INTERACTABLES = [] + self.BG_FILL = (0, 0, 0) + + class Main(): + def __init__(self, game): + + self.TYPE = "menu" + self.HAS_PLAYER = False + self.INTERACTABLES = [ + Button( + game, + "start", + 100, + 100, + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), + game.LEVEL.load_layout, + args=Scenes.Commencing_Slaughter + ), + Button( + game, + "settings", + 100, + 200, + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), + game.LEVEL.load_layout, + #args=Menus.Settings + args=Menus.Main + ), + Button( + game, + "exit_to_desktop", + 100, + 300, + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), + game.MENU_FUNCTIONS.exit_to_desktop + ) + ] + + self.BG_FILL = pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert() + self.MUSIC = mixer.Sound(DIRWORKING + "/data/sound/ui/menu_main/Backwards-Souls-SoundBible.com-87826574.wav") + + class Settings(): + def __init__(self, game): + + self.TYPE = "menu" + self.HAS_PLAYER = False + self.INTERACTABLES = [ + Checkbox( + game, + "fullscreen", + 100, + 100, + pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_hover.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/icons/checkbox_active.png").convert_alpha(), + lambda: FEATURES["fullscreen"]["is_active"], # Needs to be re-evaluated. Thus lambda function. + game.MENU_FUNCTIONS.enter_fullscreen + ) + ] + + self.BG_FILL = pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert() + + class Pause(): + def __init__(self, game): + + self.TYPE = "menu" + self.HAS_PLAYER = False + self.INTERACTABLES = [ + Button( + game, + "return_to_game", + 100, + 100, + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), + game.MENU_FUNCTIONS.return_to_game + ), + Button( + game, + "settings", + 100, + 200, + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), + game.MENU_FUNCTIONS.return_to_game + #game.LEVEL.load_layout, + #args=Menus.Settings + ), + Button( + game, + "exit_to_menu", + 100, + 300, + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), + game.LEVEL.load_layout, + args=Menus.Main + ) + ] + + # Save a snapshot of the window to overlay the pause menu on. + pygame.image.save(game.WINDOW.window, DIRWORKING + "/temp/pausescreen.png") + self.BG_FILL = pygame.image.load(DIRWORKING + "/temp/pausescreen.png").convert() + + class Player_Died(): + def __init__(self, game): + self.TYPE = "menu" + self.HAS_PLAYER = False + self.INTERACTABLES = [ + Button( + game, + "exit_to_menu", + 100, + 300, + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), + game.LEVEL.load_layout, + args=Menus.Main + ) + ] + self.BG_FILL = load(DIRWORKING + "/data/image/game/blocks/wool_colored_red.png") + + class Player_Won(): + def __init__(self, game): + self.TYPE = "menu" + self.HAS_PLAYER = False + self.INTERACTABLES = [ + Button( + game, + "exit_to_menu", + 100, + 300, + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_neutral.png").convert_alpha(), + pygame.image.load(DIRWORKING + "/data/image/ui/menu_main/start_hover.png").convert_alpha(), + game.LEVEL.load_layout, + args=Menus.Main + ) + ] + self.BG_FILL = load(DIRWORKING + "/data/image/game/blocks/wool_colored_lime.png") + +class Levels(): + """ + Class for organising levels. + """ + + class Example(): + def __init__(self, game, **kwargs): + + self.TYPE = "level" + self.HAS_PLAYER = True + self.LAYOUT = { + (0, 0, 96, 64): Blocks.block_blue(), + (2, 1): Blocks.block_rainbow(layer=1), + (14, 5): Blocks.block_rainbow(layer=1), + (3, 3, 5, 5): Blocks.puddle_of_souls(game, layer=1), + (7, 7): Blocks.puddle_of_souls(game, layer=1), + (4, 9): Blocks.puddle_of_souls(game, layer=1) + } + self.ENTITIES = [ + Unnamed(game, 16, 16), + Unnamed(game, 16, 17), + Unnamed(game, 17, 16), + Unnamed(game, 17, 17), + Unnamed(game, 15, 23), + Unnamed(game, 41, 52), + Unnamed(game, 5, 17) + ] + self.STARTPOSITIONS = {"default": (0, 0)} + self.BG_FILL = (0, 0, 0) + self.FOLLOW_UP_LEVEL = Scenes.Player_Won + +class Frames(): + """ + Class for methods pertaining to scenes. + """ + + def __init__(self, game): + + self.GAME = game + self.WINDOW = self.GAME.WINDOW + self.clear() + + def clear(self): + """ + Clear the frames from memory. + """ + + self.images = [ + pygame.image.load(DIRWORKING + "/data/image/game/blocks/wool_colored_silver.png").convert() + ] + self.imageNumber = -1 + self.frames = 0 # Frames rendered by system + + def draw(self): + """ + Draws the scene. + """ + + if self.frames / self.GAME.clock.get_fps() > self.GAME.LEVEL.layout.TIME_PER_FRAME: # Use number of frames displaying an image / framerate to get time in sec. + self.imageNumber = self.imageNumber + 1 # Increment the image index + self.frames = 0 + + if len(self.images) == self.imageNumber: + self.GAME.LEVEL.load_layout(self.GAME.LEVEL.layout.FOLLOW_UP_LEVEL) + self.clear() + else: + self.WINDOW.window.blit(self.images[self.imageNumber], (0, 0)) + elif self.imageNumber == -1: + self.imageNumber = 0 + self.WINDOW.window.blit(self.images[self.imageNumber], (0, 0)) + + else: + self.frames = self.frames + 1 + + def load_images(self): + """ + Load frames for a scene. + """ + + self.images = [] + + for path in self.GAME.LEVEL.layout.LAYOUT: + self.images.append(load(path).convert_alpha()) + + self.scale_images() + + def scale_images(self): + """ + Scales all images currently loaded. + """ + + for image in self.images: + self.images[self.images.index(image)] = pygame.transform.scale(image, (self.WINDOW.width, self.WINDOW.height)) + + def skip(self): + """ + Skips the level. + """ + + self.clear() + self.GAME.LEVEL.load_layout(self.GAME.LEVEL.layout.FOLLOW_UP_LEVEL) + + def update_scale(self): + """ + Updates the scaling of the active frames. + """ + + if self.WINDOW.width > self.images[0].get_width() or self.WINDOW.height > self.images[0].get_height(): + self.load_images() + else: + self.scale_images() \ No newline at end of file diff --git a/lib/section.py b/lib/section.py new file mode 100644 index 0000000..765724c --- /dev/null +++ b/lib/section.py @@ -0,0 +1,424 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: Sections of layers in a game. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: Classes descibing different sections in a pygame game. +# +# @Script: section.py +# @Date Created: 16 Apr, 2024 +# @Last Modified: 19 Jun, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +from icecream import ic +from random import randint +from pygame.image import load +from pygame.transform import scale +from lib import effect +from lib.system import DIRWORKING + +class Section(): + """ + A 16x16 area of the layer. + If this section can be interacted with by a player, `Section` will store the function. + """ + + def __init__(self, game): + self.GAME = game # Cannot shortcut self.LEVEL due to Section befing used as part the init + self.SCALE = game.SCALE + self.WINDOW = game.WINDOW + + def draw(self, layout): + """ + Draw the section-built layout on the screen. + """ + + blocksInView = {} + blocksDrawn = 0 + is_sorted = False + layers = [] + currentLayerIndex = 0 + + # Improve processing time, only blocks on display will be drawn. + # Improve engine to support drawing areas with a single block, and not blitting blocks without alpha over blocks of lower layers. + for coords, block in layout.items(): + try: # Does this block define a range fill. + for y in range(coords[1], coords[3] + 1): + if y in range(self.GAME.LEVEL.dy, self.GAME.LEVEL.dy + self.SCALE.gameY + 1): # Only try to blit blocks on a displayed row. + for x in range(coords[0], coords[2] + 1): + if x in range(self.GAME.LEVEL.dx, self.GAME.LEVEL.dx + self.SCALE.gameX + 1): + blocksInView[(x, y, block.layer)] = block # Add block layer to prevent overwriting blocks underneath. + + if block.layer not in layers: # Get all the layers present of displayed blocks + layers.append(block.layer) + + except IndexError: + if coords[0] in range(self.GAME.LEVEL.dx, self.GAME.LEVEL.dx + self.SCALE.gameX + 1) and coords[1] in range(self.GAME.LEVEL.dy, self.GAME.LEVEL.dy + self.SCALE.gameY + 1): # If block is in display area. + + blocksInView[coords] = block + + if block.layer not in layers: # Get all the layers present of displayed blocks + layers.append(block.layer) + + while not is_sorted: # Sort layers to draw their blocks at the correct heights. + is_sorted = True + for i in range(0, len(layers) - 1): + if layers[i] > layers[i + 1]: + is_sorted = False + + temp = layers[i] + layers[i] = layers[i + 1] + layers[i + 1] = temp + + while blocksDrawn < len(blocksInView): # While there are blocks still not drawn + for location, block in blocksInView.items(): # Go through each block and draw it if it is on the layer currently being drawn. + if block.layer == layers[currentLayerIndex]: # Use a list of layers so that if no blocks in view are on a layer, no time is wasted parsing blocks on a layer that doesn't show. + + x = (location[0] - self.GAME.LEVEL.dx) * self.SCALE.unitBlockPx # Get its x, y location in pixels. + y = (location[1] - self.GAME.LEVEL.dy) * self.SCALE.unitBlockPx + if block.TEXTURE[block.state]["IS_ANIMATED"]: # Get the current texture frame for animated textures. + block.animationClock = block.animationClock + 1 + + if block.animationClock >= (self.GAME.clock.get_fps() * block.TEXTURE[block.state]["TIME_PER_FRAME"]): # If the time in seconds passed is equal or greater than the time per frame of the animated texture + + block.animationClock = 0 + textures = len(block.TEXTURE[block.state]["texture"]) + currentTexture = block.textureIndex # Fetch value to marginally reduce time to reach value repeatedly from dict. + + if (currentTexture + 1) > (textures - 1): + block.textureIndex = 0 + + if block.TEXTURE[block.state]["ANIMATION_REPEATS"] != -1: # If a limited animation + block.animationRepeats = block.animationRepeats + 1 + if block.animationRepeats == block.TEXTURE[block.state]["ANIMATION_REPEATS"]: # Block has animated for its state's full time. + Section.update_state(block) # Cannot call self.update_state for some reason. + # Causes two positional arugments by witchcraft. + + else: + block.textureIndex = currentTexture + 1 + + texture = block.TEXTURE[block.state]["texture"][block.textureIndex] + + else: # Otherwise print its static texture for its state. + texture = block.TEXTURE[block.state]["texture"][0] + + if texture.get_width() != self.SCALE.unitBlockPx: + texture = scale(texture, (self.SCALE.unitBlockPx, self.SCALE.unitBlockPx)) + + self.WINDOW.window.blit(texture, (x, y)) + + blocksDrawn = blocksDrawn + 1 + + #ic(f"Printed blocks {blocksDrawn}/{len(blocksInView)}") + + currentLayerIndex = currentLayerIndex + 1 + + def update_state(section): + """ + Updates the state of the section. + """ + + section.state = section.TEXTURE[section.state]["FOLLOW_UP_STATE"] + section.textureIndex = 0 + +class Textures(): + """ + Contains all textures used by the game. + Not the most memory-efficient. May want to rework this simple memory hog. + """ + + def __init__(self, unitBlockPx): + + self.load_textures() + + self.update_scale(unitBlockPx) + + def load_textures(self): + """ + Loads textures from file. + """ + + BLOCKSDIR = DIRWORKING + "/data/image/game/blocks/" + + self.TEXTURES = { + "BLOCK_BLUE": load(BLOCKSDIR + "wool_colored_blue.png").convert(), + "BLOCK_BROWN": load(BLOCKSDIR + "wool_colored_brown.png").convert(), + "BLOCK_CYAN": load(BLOCKSDIR + "wool_colored_cyan.png").convert(), + "BLOCK_GRAY": load(BLOCKSDIR + "wool_colored_gray.png").convert(), + "BLOCK_GREEN": load(BLOCKSDIR + "wool_colored_green.png").convert(), + "BLOCK_LIGHT_BLUE": load(BLOCKSDIR + "wool_colored_light_blue.png").convert(), + "BLOCK_LIME": load(BLOCKSDIR + "wool_colored_lime.png").convert(), + "BLOCK_MAGENTA": load(BLOCKSDIR + "wool_colored_magenta.png").convert(), + "BLOCK_ORANGE": load(BLOCKSDIR + "wool_colored_orange.png").convert(), + "BLOCK_PINK": load(BLOCKSDIR + "wool_colored_pink.png").convert(), + "BLOCK_PURPLE": load(BLOCKSDIR + "wool_colored_purple.png").convert(), + "BLOCK_RED": load(BLOCKSDIR + "wool_colored_red.png").convert(), + "BLOCK_SILVER": load(BLOCKSDIR + "wool_colored_silver.png").convert(), + "BLOCK_WHITE": load(BLOCKSDIR + "wool_colored_white.png").convert(), + "BLOCK_YELLOW": load(BLOCKSDIR + "wool_colored_yellow.png").convert(), + "PUDDLE_SOULS_RICH": load(BLOCKSDIR + "puddle_souls/puddle_souls_rich.png").convert_alpha(), + "PUDDLE_SOULS_SPENT": load(BLOCKSDIR + "puddle_souls/puddle_souls_spent.png").convert_alpha(), + "PUDDLE_BLOOD_0": load(BLOCKSDIR + "puddle_blood/puddle_blood_0.png").convert_alpha(), + "PUDDLE_BLOOD_1": load(BLOCKSDIR + "puddle_blood/puddle_blood_1.png").convert_alpha(), + "PUDDLE_BLOOD_2": load(BLOCKSDIR + "puddle_blood/puddle_blood_2.png").convert_alpha(), + "PUDDLE_BLOOD_3": load(BLOCKSDIR + "puddle_blood/puddle_blood_3.png").convert_alpha(), + "PUDDLE_BLOOD_4": load(BLOCKSDIR + "puddle_blood/puddle_blood_4.png").convert_alpha(), + "PUDDLE_BLOOD_NEUTRAL": load(BLOCKSDIR + "puddle_blood/puddle_blood_neutral.png").convert_alpha() + } + + def update_scale(self, unitBlockPx): + """ + Updates the scaling of all textures. + """ + + if unitBlockPx > self.TEXTURES["BLOCK_BLUE"].get_width(): + self.load_textures() + + temp = {} + + for texture in self.TEXTURES.items(): + temp[texture[0]] = scale(texture[1], (unitBlockPx, unitBlockPx)) + + self.TEXTURES = temp + +class Blocks(): + """ + Contains all section block definitions. + """ + + """ + class block_demo(): + def __init__(self): + self.TEXTURE :dict = { + "neutral": # Should be used as standard naming. + # NOTE: Can remove keys that are not in STATES, unlike shown. + { + "texture": [ + Textures.TEXTURES["BLOCK_EXAMPLE_0"], + Textures.TEXTURES["BLOCK_EXAMPLE_1"] + ], + "IS_ANIMATED": True, # Does this state has an animation, if so execute additional code to iterate through texture list. + "ANIMATION_REPEATS":-1, # Value of -1 indicates that animation runs infinitely. + # Can be left out if not IS_ANIMATED. + # Indicates the number of times the animation will loop in this state before passing to FOLLOW_UP_STATE. + "IS_INTERACTABLE": True, # Can be interacted with by the player during this state. + "IS_COLLIDABLE": False, # Player and entities will collide with block during this state. + "TIME_PER_FRAME": 1.0, # Amount of time (s) given to display each frame. Can be left out if not IS_ANIMATED. + "FOLLOW_UP_STATE": "on_trigger" # State that the block should progress to next. Can be left out if not IS_INTERACTABLE. + }, + "on_trigger": # Should be used as standard naming. + { + "texture": [ + Textures.TEXTURES["BLOCK_EXAMPLE"] + ], + "IS_ANIMATED": False, + "IS_INTERACTABLE": False, + "IS_COLLIDABLE": False, + "TIME_PER_FRAME": 1.0, # TFP remains due to special temporary nature of on_trigger and off_trigger states. + "FOLLOW_UP_STATE": "while_trigger" # FUS remains due to special temporary nature of on_trigger and off_trigger states. + }, + "while_trigger": # Should be used as standard naming. + { + "texture": [ + Textures.TEXTURES["BLOCK_EXAMPLE"] + ], + "IS_ANIMATED": False, + "IS_INTERACTABLE": True, + "IS_COLLIDABLE": False, + "TIME_PER_FRAME": 1.0, + "FOLLOW_UP_STATE": "off_trigger" + }, + "off_trigger": # Should be used as standard naming. + { + "texture": [ + Textures.TEXTURES["BLOCK_EXAMPLE"] + ], + "IS_ANIMATED": False, + "IS_INTERACTABLE": False, + "IS_COLLIDABLE": False, + "TIME_PER_FRAME": 1.00, + "FOLLOW_UP_STATE": "neutral", + "ANIMATION_REPEATS":1 + } + } + self.state :str = "neutral", # Variable for tracking the current texture. + self.textureIndex :int = "textureIndex": 0, # Tracks the index of the current animation frame. + self.animationClock :int = "animationClock": 0 # Used for tracking the time in frames, that a frame has been shown. + self.animationRepeats:int = 0 # Number of times that a limited animation has been run. + self.is_triggered :bool = False # Only needed if block in current state IS_INTERACTABLE + self.time :float = 0.0 # Used to track how long each frame has been displayed for animated textures. + self.layer :int = 0 # Used to track the layer of the block. For example, a door will be layer 1. + # while the floor below it is layer 0, so door gets interaction check priority. + def on_interact(self): + ''' + Set of actions to execute upon interaction. + ''' + """ + + class block_blue(): + def __init__(self, layer:int = 0): + self.TEXTURE = { + "neutral":{ + "texture": [ + Textures.TEXTURES["BLOCK_BLUE"] + ], + "IS_ANIMATED": False, + "IS_INTERACTABLE": False, + "IS_COLLIDABLE": False + } + } + self.state = "neutral" + self.layer = layer + + class block_rainbow(): + def __init__(self, layer:int = 1): + self.TEXTURE = { + "neutral": { + "texture": [ + Textures.TEXTURES["BLOCK_RED"], + Textures.TEXTURES["BLOCK_ORANGE"], + Textures.TEXTURES["BLOCK_YELLOW"], + Textures.TEXTURES["BLOCK_LIME"], + Textures.TEXTURES["BLOCK_GREEN"], + Textures.TEXTURES["BLOCK_CYAN"], + Textures.TEXTURES["BLOCK_LIGHT_BLUE"], + Textures.TEXTURES["BLOCK_BLUE"], + Textures.TEXTURES["BLOCK_PURPLE"], + Textures.TEXTURES["BLOCK_MAGENTA"], + Textures.TEXTURES["BLOCK_PINK"] + ], + "IS_ANIMATED": True, + "ANIMATION_REPEATS": -1, + "IS_INTERACTABLE": True, + "IS_COLLIDABLE": True, + "TIME_PER_FRAME": 1.0, + "FOLLOW_UP_STATE": "on_trigger" + }, + "on_trigger": { + "texture": [ + Textures.TEXTURES["BLOCK_YELLOW"], + Textures.TEXTURES["BLOCK_ORANGE"], + Textures.TEXTURES["BLOCK_RED"] + ], + "IS_ANIMATED": True, + "IS_INTERACTABLE": False, + "IS_COLLIDABLE": True, + "TIME_PER_FRAME": 1.0, + "ANIMATION_REPEATS": 5, + "FOLLOW_UP_STATE": "while_trigger" + }, + "while_trigger": { + "texture": [ + Textures.TEXTURES["BLOCK_PINK"], + Textures.TEXTURES["BLOCK_MAGENTA"], + Textures.TEXTURES["BLOCK_PURPLE"], + Textures.TEXTURES["BLOCK_BLUE"], + Textures.TEXTURES["BLOCK_LIGHT_BLUE"], + Textures.TEXTURES["BLOCK_CYAN"], + Textures.TEXTURES["BLOCK_GREEN"], + Textures.TEXTURES["BLOCK_LIME"], + Textures.TEXTURES["BLOCK_YELLOW"], + Textures.TEXTURES["BLOCK_ORANGE"], + Textures.TEXTURES["BLOCK_RED"] + ], + "IS_ANIMATED": True, + "ANIMATION_REPEATS": -1, + "IS_INTERACTABLE": False, + "IS_COLLIDABLE": True, + "TIME_PER_FRAME": 1.0 + } + } + self.state = "neutral" + self.textureIndex = 0 + self.animationClock = 0 + self.animationRepeats = 0 + self.layer = layer + + def on_interact(self): + """ + Set of actions to execute on interaction + """ + + Section.update_state(section=self) + + class puddle_of_souls(): + def __init__(self, game, layer:int = 1): + self.TEXTURE = { + "neutral": { + "texture": [ + Textures.TEXTURES["PUDDLE_SOULS_RICH"] + ], + "IS_ANIMATED": False, + "IS_INTERACTABLE": True, + "IS_COLLIDABLE": True, + "FOLLOW_UP_STATE": "while_trigger" + }, + "while_trigger": { + "texture": [ + Textures.TEXTURES["PUDDLE_SOULS_SPENT"] + ], + "IS_ANIMATED": False, + "IS_INTERACTABLE": False, + "IS_COLLIDABLE": False, + } + } + self.state = "neutral" + self.layer = layer + + self.GAME = game + + def on_interact(self): + """ + Set of actions to execute on interaction + """ + + Section.update_state(section=self) + + if randint(1, 100) <= 25: + self.GAME.PLAYER.health = self.GAME.PLAYER.health + 25 + + class puddle_of_blood(): + def __init__(self, game, layer:int = 1): + self.TEXTURE = { + "on_death": { + "texture": [ + Textures.TEXTURES["PUDDLE_BLOOD_0"], + Textures.TEXTURES["PUDDLE_BLOOD_1"], + Textures.TEXTURES["PUDDLE_BLOOD_2"], + Textures.TEXTURES["PUDDLE_BLOOD_3"], + Textures.TEXTURES["PUDDLE_BLOOD_4"] + ], + "IS_ANIMATED": True, + "ANIMATION_REPEATS": 1, + "IS_INTERACTABLE": True, + "IS_COLLIDABLE": False, + "TIME_PER_FRAME": 0.25, + "FOLLOW_UP_STATE": "neutral" + }, + "neutral": { + "texture": [ + Textures.TEXTURES["PUDDLE_BLOOD_NEUTRAL"] + ], + "IS_ANIMATED": False, + "IS_INTERACTABLE": True, + "IS_COLLIDABLE": False + } + } + self.state = "on_death" + self.textureIndex = 0 + self.animationClock = 0 + self.animationRepeats = 0 + self.layer = layer + + self.GAME = game + + def on_interact(self): + """ + Set of actions to execute on interaction. + """ + + self.GAME.PLAYER.effects.append(effect.War_Paint(self.GAME, self.GAME.PLAYER.character)) \ No newline at end of file diff --git a/lib/system.py b/lib/system.py new file mode 100755 index 0000000..87d2390 --- /dev/null +++ b/lib/system.py @@ -0,0 +1,252 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: System utilities. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: Utilities for a PyGame game. +# +# @Script: system.py +# @Date Created: 17 Apr, 2024 +# @Last Modified: 14 Jun, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +""" + Modules that are loaded will always be loaded globally. + Some will only be loaded based on conditions, but made global. + Thus, they can be checked for in globals() and be easily accessible anywhere. + Even if a module were loaded and unloaded, it would remain in cache, so unloading it is not really worth it. + Imports will be loaded as the program needs them, thus reducing the initial memory consumption. +""" + +# Although they won't be reloaded and the imports will be skipped if called again, +# avoid wasting processing time by keeping out of import_dependencies() +from pkg_resources import working_set +from subprocess import check_call, CalledProcessError +from sys import executable + + +def install_dependencies(dependencies:dict): + """ + Tries to install and import any missing dependencies from list. + """ + + # Adapted from Serge Stroobandt on https://stackoverflow.com/questions/46419607/how-to-automatically-install-required-packages-from-a-python-script-as-necessary + + libsInst = { + pkg.key for pkg in working_set # Find required and installed libraries. + } + + libsMiss = dependencies - libsInst # Remove the already installed libraries from the dependency list. + + try: + if len(libsMiss) != 0: + check_call([executable, "-m", "pip", "install", "--upgrade", "pip"]) # Ensure pip is up-to-date. + for library in libsMiss: + check_call([executable, "-m", "pip", "install", library]) # Install all missing libraries. + + except CalledProcessError: + print("Error | CANNOT FIND OR INSTALL DEPENDENCY:") + print(library) + raise SystemExit + +install_dependencies({"icecream", "pygame"}) + +from icecream import ic +from pygame.version import vernum +from pygame.display import get_driver +from os import path +from sys import version as pyver +from platform import uname +from json import load as jload + + +DISPLAY = {} +PLATFORM = {"libraries": {}} +PLATFORM["kernel"] = uname().system # `system` is a variable of uname() +PLATFORM["libraries"]["pygame"] = vernum + +# Python version cleanup. +versionStr = "" + +for c in pyver: + if c == " ": + break + else: + versionStr = versionStr + c + +versionTuple = tuple(versionStr.split(".")) + +versionTuple = (int(versionTuple[0]), int(versionTuple[1]), int(versionTuple[2])) + +PLATFORM["libraries"]["python"] = versionTuple + +# GCC version cleanup. +versionStr = "" +startFlag = False + +for c in pyver: + if startFlag: + if c not in ["G", "C", "]"]: + versionStr = versionStr + c + elif c == "[": + startFlag = True + +PLATFORM["libraries"]["GCC"] = versionStr + +del pyver, versionStr, startFlag, versionTuple + +# Fetch compatability settings. +DIRWORKING = path.dirname(__file__) + "/.." + +with open(DIRWORKING + "/config/features.json") as f: + FEATURES = jload(f) + +# All features in file default to is_active: false +# Enable supported features. + +for feature in FEATURES.items(): + is_active = False + + for lib in feature[1]["libs"].items(): + if PLATFORM["libraries"][lib[0]] > tuple(lib[1]["ver_implemented"]): + is_active = True + + FEATURES[feature[0]]["is_active"] = is_active + +class Compat(): + def linux(): + # Adapted from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate + + """ + if "environ" not in globals(): # Import if not already. + global environ + from os import environ + + try: + if environ["WAYLAND_DISPLAY"] != "" and globals["XDG_SESSION_TYPE"] != "x11": # Check for environment variable present on Wayland systems. + print("Sorry, you are running Wayland. Configuring FPS on Wayland is not yet supported.") + + except KeyError: # Wayland keys won't exist on X11 systems (mostly). + install_dependencies({"Xlib"}) + global display + from Xlib import display + global randr + from Xlib.ext import randr + + displayGlobal = display.Display() # Get every display thing. Maybe a WM object? + default_screen = displayGlobal.get_default_screen() # Find default display for some reason. + info = displayGlobal.screen(default_screen) # Use this as a point to get information about all screens. + + displayConfigs = randr.get_screen_resources(info.root) # Get every screen's possible configurations. + configsActive = set() # Create an empty set to add configurations to. + + for config in displayConfigs.crtcs: # For every display configuration, determine if it's active and add its identifier to the configurations in use. + crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp) + if crtc_info.mode: + configsActive.add(crtc_info.mode) + + for config in displayConfigs.modes: + if config.id in configsActive: # For every active display mode, figure out its framerate. + framerate = config.dot_clock / (config.h_total * config.v_total) + """ + + if get_driver() == "wayland": + ic("Sorry, you are running Wayland. Configuring FPS on Wayland is not yet supported.") + elif get_driver() == "x11": + install_dependencies({"Xlib"}) + global display + from Xlib import display + global randr + from Xlib.ext import randr + + displayGlobal = display.Display() # Get every display thing. Maybe a WM object? + default_screen = displayGlobal.get_default_screen() # Find default display for some reason. + info = displayGlobal.screen(default_screen) # Use this as a point to get information about all screens. + + displayConfigs = randr.get_screen_resources(info.root) # Get every screen's possible configurations. + configsActive = set() # Create an empty set to add configurations to. + + for config in displayConfigs.crtcs: # For every display configuration, determine if it's active and add its identifier to the configurations in use. + crtc_info = randr.get_crtc_info(info.root, config, displayConfigs.config_timestamp) + if crtc_info.mode: + configsActive.add(crtc_info.mode) + + framerate = 30 + + # With multiple displays, get the highest framerate that any one monitor can do. + for config in displayConfigs.modes: + if config.id in configsActive: # For every active display mode, figure out its framerate. + framerateDisplay = (config.dot_clock / (config.h_total * config.v_total)) + if framerateDisplay > framerate: + framerate = framerateDisplay + + return framerate + + def nt(): + # Cannot use pygame.display.get_driver for windows or macos in pygame < v2 + + # Adapted from Anurag Uniyal on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate + + install_dependencies({"pywin32"}) + + if "EnumDisplayDevices" not in globals(): # Import if not already. + global EnumDisplayDevices + from win32api import EnumDisplayDevices + if "EnumDisplaySettings" not in globals(): # Import if not already. + global EnumDisplaySettings + from win32api import EnumDisplaySettings + + display_info = EnumDisplayDevices() # Returns PyDISPLAY_DEVICE object + + display_settings = EnumDisplaySettings(display_info.DeviceName, -1) # Picks display that seems to have highest framerate?... + return getattr(display_settings, "DisplayFrequency") + + def darwin(): + # Cannot use pygame.display.get_driver for windows or macos in pygame < v2 + + # Adapted from Glyph, Eevee on https://stackoverflow.com/questions/1225057/how-can-i-determine-the-monitor-refresh-rate + + install_dependencies({"Appkit"}) + + if "NSScreen" not in globals(): # Import if not already. + global NSScreen + from AppKit import NSScreen + + for screen in NSScreen.screens(): + if framerate < screen.maximumFramesPerSecond(): # I the framerate of this display is higher, set its as the new maximum rate. + framerate = screen.maximumFramesPerSecond() + + return framerate + +def update_framerate(): + """ + Used for mock vsync to support Pygame < v2.0.0 + """ + + if FEATURES["matchcase"]["is_active"]: + match PLATFORM["kernel"]: + case "Linux": + framerate = Compat.linux() + case "Windows": + framerate = Compat.nt() + case "Darwin": + framerate = Compat.darwin() + + elif PLATFORM["kernel"] == "Linux": + framerate = Compat.linux() + + elif PLATFORM["kernel"] == "Windows": + framerate = Compat.nt() + + elif PLATFORM["kernel"] == "Darwin": # I'm trusting that the info I found for MacOS does actually still work this way. + framerate = Compat.darwin() + + else: + print("Unsupported or unable to detect system kernel. Defaulting to 30 FPS. This can be overridden in the user settings file.") + + framerate = int("%.0f" % framerate) + DISPLAY["framerate"] = framerate diff --git a/logs/debug.txt b/logs/debug.txt new file mode 100644 index 0000000..093a00d --- /dev/null +++ b/logs/debug.txt @@ -0,0 +1,2 @@ +DEBUG | level.py:99 in load_layout() + self.layout.__class__: diff --git a/main.py b/main.py new file mode 100755 index 0000000..4165782 --- /dev/null +++ b/main.py @@ -0,0 +1,969 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: Intro to Pygame. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: Getting started with using PyGame engine. +# +# @Script: main.py +# @Date Created: 22 Mar, 2024 +# @Last Modified: 19 Jun, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +# Load lib.system to ensure everything is good to go dependency and system config wise. +from lib import system + +# Knowing that all external dependencies are acessible, import the rest. +import pygame, json, threading +from pygame import freetype, mixer # Requires explicit importing. +from icecream import ic +from time import sleep +from lib import interface, level, entity, section + + +# Configure IceCream a bit. +# Use for release version. + +def debug(msg, file="debug.txt"): + open(system.DIRWORKING + "/logs/" + file, "w") # Build a debug file if one doesn't exist. + with open(system.DIRWORKING + "/logs/" + file, "a") as f: # Append each debugging report to file + f.write(msg + "\n") + +ic.configureOutput(prefix="DEBUG | ", outputFunction=debug, includeContext=True) + +#ic.configureOutput(prefix="DEBUG | ") + +class Game(): + def __init__(self): + + self.initOn = True + self.initSkip = False + + self.clock = pygame.time.Clock() + + # Set game icon for system. + gameIcon = pygame.image.load(system.DIRWORKING + "/data/image/game/icon.png") + pygame.display.set_icon(gameIcon) + + self.VERSION = "0.0.1-alpha" + + pygame.display.set_caption(f"Shroom of Doom: The Massacring Mushroom v{self.VERSION}") + + self.SETTINGS = {} + + self.load_settings(reset=False) + + # Initialise the window. + self.WINDOW = self.Window( + self, + self.SETTINGS["window"]["size"]["width"], + self.SETTINGS["window"]["size"]["height"] + ) + + self.bgFill = (255, 255, 255) + self.WINDOW.window.fill(self.bgFill) + + self.SCALE = self.Scale(game=self) + + self.LEVEL = level.Level(self) + + # Set up a font + freetype.init() + self.REPORTFONT = freetype.Font(system.DIRWORKING + "/data/font/Domestic_Manners.ttf", self.WINDOW.height * 0.03) + + # Start audio mixer + mixer.init() + + # Prep for BS loading screen. + self.gameProgressBars = [] + + self.bgImages = [] + self.bgImagesIndex = 0 + + # Add more images to animate bg. + for imageDir in [ + "/data/image/game/blocks/wool_colored_gray.png" + ]: + image = pygame.image.load(system.DIRWORKING + imageDir) + + image = pygame.transform.scale(image, (self.WINDOW.width, self.WINDOW.height)) + + self.bgImages.append(image) + + # The following variables are created so that the progress bar is initialized to display immediately with the loading screen. + # If not listed, can run into a condition where these are undefined as the thread hasn't reached defining them fast enough. + self.initJobsComplete = 0 + self.initJobsTotal = 1 + self.initJobsImportIncomplete = 3 # Variable posessing the total number of BS Import report groups to fix scaling on progress bar if init is sped up. + self.loadStatus = "" + self.is_initSkipFirst = False # Variable describing if it is the first time that update_report() has seen the initSkip. + + self.initProgressBar = interface.Progress_bar( + self, + 5, + 90, + 90, + [ + pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/mushroom_silhouette_1.png"), + pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/mushroom_silhouette_2.png") + ], + pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/init_bar_left.png"), + pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/init_bar_right.png"), + pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/init_bar_clear.png"), + pygame.image.load(system.DIRWORKING + "/data/image/ui/screen_init/init_bar_fill.png") + ) + + windowRefresh = threading.Thread(target=self.init_jobs) # Create a thread to run the mostly bs readouts. + windowRefresh.start() # Start that thread. + + p = 0 # Number of times that pygame.K_p has been pressed. + + # Event handler and display rendering must be done on master thread due to pygame structure. + while self.initOn: + + self.clock.tick(30) # Run at a lower framerate to reduce system resource consumption when not needed. + + self.draw_background() + + # Update objects on screen. + pygame.display.flip() + + # Event handler + for event in pygame.event.get(): # Short event handler to allow the game to be exited during startup. + if event.type == pygame.QUIT: + + # Cannot currently kill thread. Must let it run out, otherwise employ spaghetti code in its target function. + self.initOn = False + self.gameOn = False + + raise SystemExit + + elif event.type == pygame.VIDEORESIZE: + self.WINDOW.update_window(event.w, event.h) + + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_p: + p = p + 1 + + if p == 2: + # Cannot currently kill thread. Must let it run out, otherwise employ spaghetti code in its target function. + self.initSkip = True + + # Clean memory + del self.initJobsComplete, self.initJobsTotal, self.loadStatus, self.is_initSkipFirst, self.initSkip, self.initProgressBar, self.bgImagesIndex, self.bgImages, self.textRendered + + def init_jobs(self): + """ + The actual init while the main thread handles the window rendering. + """ + + # Dicts in list to alow iteration as dicts not iterable in python 3.3.1 + self.reportsImports1 = [ + {"report": "pygame", "time": 0.25}, + {"report": "pygame.freetype", "time": 0.25}, + {"report": "icecream.ic", "time": 0.25}, + {"report": "json", "time": 0.25}, + {"report": "threading", "time": 0.5}, + {"report": "math", "time": 0.5}, + {"report": "time.sleep ;)", "time": 0.5}, + {"report": "levels", "time": 0.25}, + {"report": "entities", "time": 0.25}, + {"report": "system", "time": 0.25}, + {"report": "urmom", "time": 0.5}, + {"report": "sections.Section", "time": 0.4}, + {"report": "scenes.Scene", "time": 0.25}, + {"report": "smingusdinglebus.Dinglebob", "time": 0.5}, + {"report": "pills.BLUE - Yes, this is all a lie.", "time": 1}, + {"report": "thebritish.Food.OldeTimeFishandChips - Wait until I'm done lying.", "time": 1}, + {"report": "Cannot find randos-free-ram-bin in system repos", "time": 2}, + {"report": "Cannot find randos-free-ram-git in system repos", "time": 2} + ] + + self.reportsImports2 = [ + {"report": "001%", "time": 2}, + {"report": "002%", "time": 1}, + {"report": "003%", "time": 2}, + {"report": "030%", "time": 0.5}, + {"report": "031%", "time": 0.5}, + {"report": "099%", "time": 4}, + {"report": "105%", "time": 2}, + {"report": "400%", "time": 3}, + {"report": "666%", "time": 3}, + {"report": "6546588795009842089420845126960248328684206489564465987951613174159489636682%", "time": 0.75} + ] + + self.reportsImports3 = [ + {"report": "Decompressing with gzip", "time": 1}, + {"report": "Running build", "time": 1}, + {"report": "Aquiring additional permissions without permission", "time": 2}, + {"report": "Running makefile", "time": 1}, + {"report": "Build Complete.", "time": 2} + ] + + self.initJobsTotal = len(self.reportsImports1) + len(self.reportsImports2) + len(self.reportsImports3) + 7 # 7 additional config reports. + self.initJobsComplete = 0 + + for i in range(0, 3): # Report all import lies. + self.update_init_load("import", i) + + self.update_init_load("config", 0) + + self.update_init_load("config", 1) + + # Bad practice, but overwrite the class to load the textures once only. + section.Textures = section.Textures(self.SCALE.unitBlockPx) + + self.update_init_load("config", 2) + + self.PLAYER = entity.Player(self) + + self.update_init_load("config", 3) + + self.entityList = [] + + self.update_init_load("config", 4) + + # Temp hardcoded player control buttons. + self.controlsPlayerMovement = { + "run_u": [pygame.K_UP, pygame.K_w], + "run_d": [pygame.K_DOWN, pygame.K_s], + "run_l": [pygame.K_LEFT, pygame.K_a], + "run_r": [pygame.K_RIGHT, pygame.K_d], + "dash": [pygame.K_LSHIFT, pygame.K_RSHIFT] + } + + self.controlsPlayerInteraction = { + "trigger": [pygame.K_RETURN, pygame.K_e] + } + + self.update_init_load("config", 5) + + self.MENU_FUNCTIONS = interface.Interface_Functions(game=self) + + self.update_init_load("config", 6) + + self.gameInteractables = [] # Contains button classes that must have their conditions updated. + + self.LEVEL.load_layout(level.Scenes.Intro) + + # Temp override for user settings + self.WINDOW.auto_configure() + + self.update_init_load("config", 7) + + self.gameOn = True + self.initOn = False + + def update_init_load(self, type:str, item:int = 0): + """ + Initial load reports and lies. + """ + + try: + if not self.initSkip: + if system.FEATURES["matchcase"]["is_active"]: + match type: + case "import": + match item: + case 0: + reports = self.reportsImports1 + msg = "Importing Libraries: " + case 1: + reports = self.reportsImports2 + msg = "Importing Libraries: ftp://www.ram.sus/downloads/nightlybuilds - Cloning... " + case 2: + reports = self.reportsImports3 + msg = "Importing Libraries: randos-free-ram - " + case _: + pass + + self.initJobsImportIncomplete = self.initJobsImportIncomplete - 1 + + case "config": + msg = "Configuration: " + + match item: + case 0: + reports = [{"report": "Not entirely a lie... if only.", "time": 2}] + case 1: + reports = [{"report": "Section - Textures", "time": 0.25}] + case 2: + reports = [{"report": "Entity - Player", "time": 0.25}] + case 3: + reports = [{"report": "Entity - Other", "time": 0.25}] + case 4: + reports = [{"report": "Game - Controls", "time": 0.25}] + case 5: + reports = [{"report": "Interface - Control Functions", "time": 0.25}] + case 6: + reports = [{"report": "Loading Menu Interface...", "time": 0.25}] + case 7: + # Final report time is very quick as a manual wait is applied afterwards so that there is time for reader to register the "Complete." on display. + reports = [{"report": "Complete.", "time": 2}] + case _: + pass + + case _: + reports = [{"report": "Skipping BS", "time": 0.1}] + msg = "ha ha, you typed pp: " + + self.update_report(reports, msg) + + else: + if type == "import": + + if item == 0: + reports = self.reportsImports1 + msg = "Importing Libraries: " + elif item == 1: + reports = self.reportsImports2 + msg = "Importing Libraries: ftp://www.ram.sus/downloads/nightlybuilds - Cloning... " + elif item == 2: + reports = self.reportsImports3 + msg = "Importing Libraries: randos-free-ram - " + + self.initJobsImportIncomplete = self.initJobsImportIncomplete - 1 + + elif type == "config": + msg = "Configuration: " + + if item == 0: + reports = [{"report": "Not entirely a lie... if only.", "time": 2}] + elif item == 1: + reports = [{"report": "Section - Textures", "time": 0.5}] + elif item == 2: + reports = [{"report": "Entity - Player", "time": 0.5}] + elif item == 3: + reports = [{"report": "Entity - Other", "time": 0.25}] + elif item == 4: + reports = [{"report": "Game - Controls", "time": 0.5}] + elif item == 5: + reports = [{"report": "Interface - Control Functions", "time": 1}] + elif item == 6: + reports = [{"report": "Loading Menu Interface...", "time": 1}] + elif item == 7: + # Final report time is very quick as a manual wait is applied afterwards so that there is time for reader to register the "Complete." on display. + reports = [{"report": "Complete.", "time": 1}] + + else: + reports = [{"report": "Skipping BS", "time": 0.1}] + msg = "ha ha, you typed pp: " + + self.update_report(reports, msg) + + except AttributeError: # If game stopped during init, avoid erroring out. + pass + + def update_report(self, reports, msg): + """ + Updates the displayed reports. + """ + + for report in reports: + if self.is_initSkipFirst: # If skipping and done the action below, just pass. Nothing to waste time processing. + pass + + elif self.initSkip and not self.is_initSkipFirst: # If supposed to skip BS, but in the middle of a series of reports, recalculate and skip. Only ic skipping report to sdtout once. + self.initJobsTotal = self.initJobsComplete + 8 + self.initJobsImportIncomplete + + # Clean memory + del self.initJobsImportIncomplete, self.reportsImports1, self.reportsImports2, self.reportsImports3 + + report = {"report": "Skipping BS", "time": 0.1} + msg = "ha ha, you typed pp: " + + self.loadStatus = msg + report["report"] + ic(self.loadStatus) + + self.is_initSkipFirst = True + + break + + else: # Otherwise ic the bs reports. + + self.loadStatus = msg + report["report"] + + ic(self.loadStatus) + sleep(report["time"]) + + self.initJobsComplete = self.initJobsComplete + 1 + + + def draw_background(self): + """ + Sets and redraws the background. + """ + + if self.initOn: + + self.bgImagesIndex = self.bgImagesIndex + 1 + + if self.bgImagesIndex >= len(self.bgImages): + self.bgImagesIndex = 0 + + self.WINDOW.window.blit(self.bgImages[self.bgImagesIndex], (0, 0)) + + self.initProgressBar.draw(self.initJobsComplete, self.initJobsTotal) + + # Render and display the mostly bs reports. + self.textRendered = self.REPORTFONT.render(self.loadStatus, (255, 255, 255))[0] + + self.WINDOW.window.blit( + self.textRendered, + ( + self.initProgressBar.x + self.initProgressBar.barImages["left"].get_width(), + self.initProgressBar.y - self.textRendered.get_height() + ) + ) + + else: + # Clear window + if self.bgFill != self.LEVEL.layout.BG_FILL: # If the background has changed (may or may not change between levels) + self.bgFill = self.LEVEL.layout.BG_FILL # Update it locally. + + if isinstance(self.bgFill, pygame.surface.Surface): # If it's an image, scale it accordingly. + + self.bgFill = pygame.transform.scale(self.bgFill, (self.WINDOW.width, self.WINDOW.height)) + + if isinstance(self.bgFill, tuple): # If background is a solid colour fill + self.WINDOW.window.fill(self.bgFill) + else: # Else background is an image fill. + self.WINDOW.window.blit(self.bgFill, (0, 0)) + + def load_settings(self, reset:bool = False): + """ + Loads user settings from file. + """ + + if reset is False: # If not told to reset settings to default, load user settings. + with open(system.DIRWORKING + "/config/settings/user.json") as f: + self.SETTINGS = json.load(f) + else: + with open(system.DIRWORKING + "/config/settings/default.json") as f: + self.INFO["settings"] = json.load(f) + + ic(self.SETTINGS) + + def run(self): + """ + Main run loop. + """ + + self.playerActionMove = "neutral" + self.levelLoad = False + self.menuOn = True + + # Required for self.update_framerate_average + #self.framerates = [] + + while self.gameOn: + + # For menus that pause the game, self.levelLoad must be made True on resume. + if self.levelLoad: + self.LEVEL.load_level_state() + self.levelLoad = False + + if system.FEATURES["matchcase"]["is_active"]: + match self.LEVEL.layout.TYPE: + case "level": + self.levelOn = True + + while self.levelOn: + + self.clock.tick(self.WINDOW.framerate) + + if self.PLAYER.health <= 0: + # Player died, end game. + self.levelOn = False + self.LEVEL.load_layout(level.Scenes.Player_Died) + break + + self.draw_background() + + self.LEVEL.draw() + + self.PLAYER.draw(self.playerActionMove) + + if len(self.entityList) != 0: # There are still entities to kill. + for entity in self.entityList: + if entity.health > 0: # Draw entities that are alive.. + entity.draw() + else: + entity.destroy() # Remove entities that have died. + else: + # All entities killed, end game. + self.LEVEL.load_layout(self.LEVEL.layout.FOLLOW_UP_LEVEL) + + # Render Framerate + textRendered = self.REPORTFONT.render(str(self.clock.get_fps()), (255, 255, 255))[0] + self.WINDOW.window.blit(textRendered, (0, 0)) + + # Update objects on screen + pygame.display.flip() + + # Event handler + for event in pygame.event.get(): + match event.type: + case pygame.QUIT: + self.MENU_FUNCTIONS.exit_to_desktop() + + case pygame.VIDEORESIZE: + self.WINDOW.update_window(event.w, event.h) + + case pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: # Pause game on ESC + self.LEVEL.load_layout(level.Menus.Pause) + + else: + for controlsDict in [ + self.controlsPlayerMovement, + self.controlsPlayerInteraction + ]: + for action, keyList in controlsDict.items(): # Check movement triggers + if event.key in keyList: # Key pressed is associated with a movement action. + match controlsDict: + case self.controlsPlayerMovement: + if action in ["run_l", "run_r", "run_u", "run_d"]: + self.playerActionMove = action + elif action == "dash": + self.PLAYER.dash(self.playerActionMove) + case self.controlsPlayerInteraction: + if action == "trigger": + self.PLAYER.interact() + + case pygame.KEYUP: + if self.LEVEL.layout.HAS_PLAYER: # If layout shows a player + for controlsDict in [ + self.controlsPlayerMovement, + self.controlsPlayerInteraction + ]: + for action, keyList in controlsDict.items(): # Check movement triggers + if event.key in keyList: # Key released is associated with a movement action. + if controlsDict == self.controlsPlayerMovement: + if action == self.playerActionMove: # Only if the most recent movement action key was released + self.playerActionMove = "neutral" # stop the player. + + case pygame.MOUSEBUTTONDOWN: + if 1 in pygame.mouse.get_pressed(): + self.PLAYER.attack() + + case pygame.MOUSEWHEEL: + + # Scale the level + if self.SETTINGS["controls"]["mouse"]["is_inverted"]: + scaleNew = self.SCALE.scaleGame + (event.y * 2) + else: + scaleNew = self.SCALE.scaleGame + (event.y * 2) + + if scaleNew in self.SCALE.scaleRange: # Limit the scaling range (limit level zoom). + + self.SCALE.scaleGame = scaleNew + + ic(self.SCALE.update_units_game()) + + ic(self.PLAYER.update_scale()) + + for entity in self.entityList: + entity.update_scale() + + section.Textures.update_scale(self.SCALE.unitBlockPx) + + self.LEVEL.update_deltas_relative_to_player() + + if isinstance(self.bgFill, pygame.surface.Surface): # Update the scaling of the bgFill if it's an image fill. + width = self.WINDOW.width / self.GAME.gameX + height = self.WINDOW.height / self.GAME.gameY + + pygame.transform.scale(self.bgFill, (width, height)) + + case _: # Except all other events. + pass + + case "menu": + self.menuOn = True + + while self.menuOn: + self.clock.tick(self.WINDOW.framerate) + + self.draw_background() + + self.LEVEL.draw() + + if self.LEVEL.layout.HAS_PLAYER: + self.PLAYER.draw() + + # Refresh interactables. + for interactable in self.gameInteractables: + interactable.draw() + + pygame.display.flip() + + for event in pygame.event.get(): + match event.type: + case pygame.QUIT: # Exit game. + self.MENU_FUNCTIONS.exit_to_desktop() + + case pygame.VIDEORESIZE: + self.WINDOW.update_window(event.w, event.h) + + case pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + if isinstance(self.LEVEL.layout, level.Menus.Pause): + self.MENU_FUNCTIONS.return_to_game() + + case pygame.MOUSEBUTTONDOWN: + if 1 in pygame.mouse.get_pressed(): + for interactable in self.gameInteractables: + if interactable.is_hovered(): + if interactable.functionArgs is not None: + interactable.function(interactable.functionArgs) + else: + interactable.function() + + case _: + pass + + case "scene": + self.sceneOn = True + + escapes = 0 + + while self.sceneOn: + self.clock.tick(self.WINDOW.framerate) + + self.LEVEL.FRAMES.draw() + + pygame.display.flip() + + for event in pygame.event.get(): + match event.type: + case pygame.QUIT: # Exit game. + self.MENU_FUNCTIONS.exit_to_desktop() + + case pygame.VIDEORESIZE: + self.WINDOW.update_window(event.w, event.h) + + self.LEVEL.FRAMES.update_scale() + + case pygame.KEYDOWN: + if self.LEVEL.layout.IS_SKIPPABLE and event.key == pygame.K_ESCAPE: + if escapes == 1: + self.LEVEL.FRAMES.skip() + else: + escapes = escapes + 1 + + case _: + pass + + elif self.LEVEL.layout.TYPE == "level": # Run the general level rederer and event handler. + self.levelOn = True + + while self.levelOn: + + self.clock.tick(self.WINDOW.framerate) + + if self.PLAYER.health <= 0: + # Player died, end game. + self.LEVEL.load_layout(level.Scenes.Player_Died) + + self.draw_background() + + self.LEVEL.draw() + + self.PLAYER.draw(self.playerActionMove) + + if len(self.entityList) != 0: + for entity in self.entityList: + if entity.health > 0: + entity.draw() + else: + entity.destroy() + else: + # All entities killed, end game. + self.LEVEL.load_layout(self.LEVEL.layout.FOLLOW_UP_LEVEL) + + # Render Framerate + textRendered = self.REPORTFONT.render(str(self.clock.get_fps()), (255, 255, 255))[0] + self.WINDOW.window.blit(textRendered, (0, 0)) + + # Update objects on screen + pygame.display.flip() + + # Event handler + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.MENU_FUNCTIONS.exit_to_desktop() + + elif event.type == pygame.VIDEORESIZE: + self.WINDOW.update_window(event.w, event.h) + + elif event.type == pygame.KEYDOWN: + + if event.key == pygame.K_ESCAPE: # Pause game on ESC + + self.LEVEL.load_layout(level.Menus.Pause) + + else: + for controlsDict in [ + self.controlsPlayerMovement, + self.controlsPlayerInteraction + ]: + for action, keyList in controlsDict.items(): # Check movement triggers + if event.key in keyList: # Key pressed is associated with a movement action. + if controlsDict == self.controlsPlayerMovement: + if action in ["run_l", "run_r", "run_u", "run_d"]: + self.playerActionMove = action # Keep track of the current movement action. + ic(self.playerActionMove) + elif action == "dash": + self.PLAYER.dash(self.playerActionMove) + elif controlsDict == self.controlsPlayerInteraction: + if action == "trigger": + self.PLAYER.interact() + + elif event.type == pygame.KEYUP: + if self.LEVEL.layout.HAS_PLAYER: # If layout shows a player + for controlsDict in [ + self.controlsPlayerMovement, + self.controlsPlayerInteraction + ]: + for action, keyList in controlsDict.items(): # Check movement triggers + if event.key in keyList: # Key released is associated with a movement action. + if controlsDict == self.controlsPlayerMovement: + if action == self.playerActionMove: # Only if the most recent movement action key was released + self.playerActionMove = "neutral" # stop the player. + + elif event.type == pygame.MOUSEBUTTONDOWN: + if 1 in pygame.mouse.get_pressed(): + self.PLAYER.attack() + + elif event.type == pygame.MOUSEWHEEL: + + # Scale the level + if self.SETTINGS["controls"]["mouse"]["is_inverted"]: + scaleNew = self.SCALE.scaleGame + (event.y * 2) + else: + scaleNew = self.SCALE.scaleGame + (event.y * 2) + + if scaleNew in self.SCALE.scaleRange: # Limit the scaling range (limit level zoom). + + self.SCALE.scaleGame = scaleNew + + ic(self.SCALE.update_units_game()) + + ic(self.PLAYER.update_scale()) + + for entity in self.entityList: + entity.update_scale() + + section.Textures.update_scale(self.SCALE.unitBlockPx) + self.LEVEL.FRAMES.update_scale() + + self.LEVEL.update_deltas_relative_to_player() + + if isinstance(self.bgFill, pygame.surface.Surface): # Update the scaling of the bgFill if it's an image fill. + width = self.WINDOW.width / self.GAME.gameX + height = self.WINDOW.height / self.GAME.gameY + + pygame.transform.scale(self.bgFill, (width, height)) + + elif self.LEVEL.layout.TYPE == "menu": # Run the general menu renderer and event handler. + self.menuOn = True + + while self.menuOn: + self.clock.tick(self.WINDOW.framerate) + + self.draw_background() + + self.LEVEL.draw() + + if self.LEVEL.layout.HAS_PLAYER: + self.PLAYER.draw() + + # Refresh interactables. + for interactable in self.gameInteractables: + interactable.draw() + + pygame.display.flip() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: # Exit game. + self.MENU_FUNCTIONS.exit_to_desktop() + + elif event.type == pygame.VIDEORESIZE: + self.WINDOW.update_window(event.w, event.h) + + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + if isinstance(self.LEVEL.layout, level.Menus.Pause): + self.MENU_FUNCTIONS.return_to_game() + + elif event.type == pygame.MOUSEBUTTONDOWN: + if 1 in pygame.mouse.get_pressed(): + for interactable in self.gameInteractables: + if interactable.is_hovered(): + if interactable.functionArgs is not None: + interactable.function(interactable.functionArgs) + else: + interactable.function() + + elif self.LEVEL.layout.TYPE == "scene": # Run the general scene renderer and event handler. + self.sceneOn = True + + escapes = 0 + + while self.sceneOn: + self.clock.tick(self.WINDOW.framerate) + + self.LEVEL.FRAMES.draw() + + pygame.display.flip() + + for event in pygame.event.get(): + if event.type == pygame.QUIT: # Exit game. + self.MENU_FUNCTIONS.exit_to_desktop() + + elif event.type == pygame.VIDEORESIZE: + self.WINDOW.update_window(event.w, event.h) + + elif event.type == pygame.KEYDOWN: + if self.LEVEL.layout.IS_SKIPPABLE and event.key == pygame.K_ESCAPE: + if escapes == 1: + self.LEVEL.FRAMES.skip() + else: + escapes = escapes + 1 + + def update_framerate_average(self): + framerateAverage = 0 + + for i in self.framerates: + framerateAverage += i + + return framerateAverage / len(self.framerates) + + + class Window(): + """ + Creates a window. + """ + + def __init__(self, game, width:int = 1440, height:int = 720, framerate:int = 30): + + self.GAME = game + + self.width = width + self.height = height + self.framerate = framerate + + self.window = pygame.display.set_mode((self.width, self.height), pygame.RESIZABLE) + + def auto_configure(self): + """ + Autoconfigures display settings. + """ + + system.update_framerate() + + framerate = system.DISPLAY["framerate"] + + ic("Detected highest framerate for this window as", framerate) + ic("Applying...") + + self.framerate = framerate + self.GAME.clock.tick(self.framerate) + + ic("Done.") + + def toggle_fullscreen(self): + """ + Toggles window fullscreening. + Returns False if cannot. + """ + + # Isolate Linux platforms as this method has always worked as get_driver has supported x11 and wayland since pagame v1. + if system.PLATFORM["kernel"] == "Linux" or system.PLATFORM["libraries"]["pygame"] >= (2, 0, 0): + pygame.display.toggle_fullscreen() + return True + else: + ic("Cannot enter fullscreen. Please update pygame to v2.0.0 or newer.") + return False + + def toggle_vsync(self): + """ + Sets up vsync if possible. Falls back to pseudo vsync from system library. + """ + + if system.FEATURES["vsync"]["is_active"]: + self.window = pygame.display.set_mode((self.width, self.height), pygame.RESIZABLE, vsync=1) + else: + system.update_framerate() + + def update_window(self, width, height): + """ + Updates the window. + """ + + # Window resizing handling changed in pygame v2 to be handled automatically. + if system.FEATURES["resizeable"]["is_active"]: + self.width = self.window.get_width() + self.height = self.window.get_height() + + else: + self.width = width + self.height = height + + if system.FEATURES["vsync"]["is_active"]: + self.window = pygame.display.set_mode((self.width, self.height), pygame.RESIZABLE, vsync=1) + else: + self.window = pygame.display.set_mode((self.width, self.height), pygame.RESIZABLE) + + self.GAME.SCALE.update_units_game() + + class Scale(): + """ + Handles all globally referenced scaling. + """ + + def __init__(self, game): + self.GAME = game + self.WINDOW = self.GAME.WINDOW + + self.scaleGlobal = 2 # Adjusts other scales for HiDPI screens. + self.scaleGame = 36 * self.scaleGlobal + self.scaleUi = 1 * self.scaleGlobal + + self.scaleRange = range(int("%.0f" % (self.scaleGame * (1/2))), int("%.0f" % (self.scaleGame * 2))) + + self.update_units_game() + + def update_units_game(self): + """ + Gets game units. + """ + + self.gameY = (1 / self.scaleGame) * self.WINDOW.height # Blocks along Y + + if (self.WINDOW.height / self.gameY) % 2 != 0: # If not filling full screen, add a block. + self.gameY = int((self.gameY // 1) + 2) # Produce a clean integer. + else: + self.gameY = int((self.gameY // 1) + 1) + + self.unitBlockPx = self.WINDOW.height / self.gameY # Defines the edge length in pixels of a section. + + if (self.unitBlockPx % 2) != 0: # Is not an integer. + self.unitBlockPx = (self.unitBlockPx // 1) + 1 # Round up. + + self.gameX = self.WINDOW.width / self.unitBlockPx # Blocks along X + + if (self.WINDOW.width / self.gameX) % 2 != 0: # If not filling full screen, add a block. + self.gameX = int((self.gameX // 1) + 2) # Produce a clean integer. + else: + self.gameX = int((self.gameX // 1) + 1) + +game = Game() +game.run() + +pygame.quit() \ No newline at end of file diff --git a/mods/LevelEditor/__pycache__/level.cpython-311.pyc b/mods/LevelEditor/__pycache__/level.cpython-311.pyc new file mode 100644 index 0000000..1b07440 Binary files /dev/null and b/mods/LevelEditor/__pycache__/level.cpython-311.pyc differ diff --git a/mods/LevelEditor/data/image/editor/icon.png b/mods/LevelEditor/data/image/editor/icon.png new file mode 100755 index 0000000..8e68d0e Binary files /dev/null and b/mods/LevelEditor/data/image/editor/icon.png differ diff --git a/mods/LevelEditor/level.py b/mods/LevelEditor/level.py new file mode 100644 index 0000000..a2b12ce --- /dev/null +++ b/mods/LevelEditor/level.py @@ -0,0 +1,174 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: Levels in a game. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: Classes descibing different levels in a pygame game. +# +# @Script: level.py +# @Date Created: 16 Apr, 2024 +# @Last Modified: 09 May, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +from icecream import ic +from lib.section import Section, Blocks +from lib.scene import Scene + +# Note: opt for importing IceCream in each library instead of using install() in case it is unavailable. +# Taken from IceCream devs github at https://github.com/gruns/icecream +try: + from icecream import ic +except ImportError: # Graceful fallback if IceCream isn't installed. + ic = lambda *a: None if not a else (a[0] if len(a) == 1 else a) # Whatever the heck this filler function does. + + +class Level(): + """ + Creates and manages the level scape. + """ + + def __init__(self, parent): + self.PARENT = parent + self.WINDOW = self.PARENT.WINDOW + self.SCALE = self.PARENT.SCALE + self.SECTION = Section(self.PARENT) + self.SCENE = Scene() + + self.layout = Example() # Should be Main(), but Example for testing. + + self.dx = 0 + self.dy = 0 + + self.get_layout_size() + + def draw(self): + """ + Draws the level on the screen. + """ + + if self.layout.TYPE == "scene": + self.SCENE.draw() + elif self.layout.TYPE == "level": + self.SECTION.draw(self.layout.LAYOUT) + + def load_layout(self, layout:object): + """ + Load a new layout configuration. + """ + + self.layout = layout + self.draw() + self.get_layout_size() + + def get_layout_size(self): + """ + Fetches the size of the active layout + """ + width = 0 + height = 0 + + for coords, block in self.layout.LAYOUT.items(): + try: + if coords[2] > width: + width = coords[2] + + if coords[3] > height: + height = coords[3] + + except IndexError: + if coords[0] > width: + width = coords[0] + + if coords[1] > height: + height = coords[1] + + self.width = width + self.height = height + + def update_deltas_relative_to_player(self): + """ + Called with a scale update. + Recalculates self.dx and self.dy so that player is centered properly. + """ + + borderWidthX = (self.SCALE.gameX // 2) + 1 + borderWidthY = (self.SCALE.gameY // 2) + 1 + + if self.PARENT.PLAYER.x < borderWidthX: + # Player is in left border + self.dx = 0 + + elif self.PARENT.PLAYER.x > self.width - borderWidthX: + # Player is in right border + self.dx = self.width - self.SCALE.gameX + + else: + # Player is within x boundaries + self.dx = self.PARENT.PLAYER.x - borderWidthX + + + if self.PARENT.PLAYER.y < borderWidthY: + # Player is in the top border + self.dy = 0 + + elif self.PARENT.PLAYER.y > self.height - borderWidthY: + # Player is in bottom border. + self.dy = self.height - self.SCALE.gameY + + else: + # Player is within y boundaries. + self.dy = self.PARENT.PLAYER.y - borderWidthY + + self.dx = int(self.dx) + self.dy = int(self.dy) + +""" +NAME :str +HAS_PLAYER :bool +TYPE :str = "scene", "menu", "level" + +scene # Does not scale with gameScale or uiScale + # Each obj in LAYOUT is a different frame, not block sections. + # Does not have a player, so HAS_PLAYER is not checked, and can be removed from definition. + +menu # Does not scale with gameScale + +LAYOUT :dict = {(x, y, rangeX, rangeY): obj} +BGFILL :tuple = (r, g, b) + :pygame.surface.Surface +""" + +class Intro_Load(): + def __init__(self): + + self.TYPE = "scene" + self.LAYOUT = {} + +class Main(): + def __init__(self): + + self.TYPE = "menu" + self.HAS_PLAYER = False + self.LAYOUT = {} + +class Settings(): + def __init__(self): + + self.TYPE = "menu" + self.HAS_PLAYER = False + self.LAYOUT = {} + +class Example(): + def __init__(self): + + self.TYPE = "level" + self.HAS_PLAYER = True + self.LAYOUT = {(0, 0, 32, 32): Blocks.block_blue(), + (2, 1): Blocks.block_rainbow(layer=1) + } + self.STARTPOSITIONS = {"center": (0, 0), "left": (-10, 0), "right": (10, 0), "up": (0, 10), "down": (0, -10)} + self.BGFILL = (0, 0, 0) \ No newline at end of file diff --git a/mods/LevelEditor/main.py b/mods/LevelEditor/main.py new file mode 100644 index 0000000..df89261 --- /dev/null +++ b/mods/LevelEditor/main.py @@ -0,0 +1,274 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: SoD: The Massacring Mushroom Level Editor. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: A Level Editor for SoD: The Massacring Mushroom. +# +# @Script: main.py +# @Date Created: 10 May, 2024 +# @Last Modified: 10 May, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +# Adapted from Serge Stroobandt on https://stackoverflow.com/questions/46419607/how-to-automatically-install-required-packages-from-a-python-script-as-necessary + +from pkg_resources import working_set +from subprocess import check_call, CalledProcessError +from sys import executable + + +# Non built-ins +libsReq = { + "pygame", + "icecream" +} + +libsInst = { + pkg.key for pkg in working_set # Find required and installed libraries. +} + +libsMiss = libsReq - libsInst # Remove the already installed libraries from the dependency list. + +try: + if len(libsMiss) != 0: + check_call([executable, "-m", "pip", "install", "--upgrade", "pip"]) # Ensure pip is up-to-date. + check_call([executable, "-m", "pip", "install", *libsMiss]) # Install all missing libraries. +except CalledProcessError: + print("Debug | CANNOT FIND OR INSTALL DEPENDENCIES - EXITING") + raise SystemExit + + +import pygame, level +from icecream import ic +from os import path +import math + + +ic.configureOutput(prefix="DEBUG | ") + +class Editor(): + """ + Level Editor + """ + + def __init__(self): + + self.clock = pygame.time.Clock() + + editorIcon = pygame.image.load(path.dirname(__file__) + "/data/image/editor/icon.png") + pygame.display.set_icon(editorIcon) + + self.VERSION = "0.0.1-alpha" + + pygame.display.set_caption(f"SoD: TMM Level Editor v{self.VERSION}") + + # Initialise the window. + self.WINDOW = self.Window(self, + 1440, + 720) + + self.bgFill = (255, 255, 255) + self.WINDOW.window.fill(self.bgFill) + + self.SCALE = self.Scale(parent=self) + + self.LEVEL = level.Level(self) + + self.editorOn = True + + def run(self): + """ + Editor run loop. + """ + + while self.editorOn: + self.clock.tick(self.WINDOW.framerate) + + # Clear window + if self.bgFill != self.LEVEL.layout.BGFILL: # If the background has changed (may or may not change between levels) + bgFill = self.LEVEL.layout.BGFILL # Update it locally. + + if isinstance(self.bgFill, pygame.surface.Surface): # If it's an image, scale it accordingly. + width = bgFill.get_width() * self.PARENT.gameScale + height = bgFill.get_height() * self.PARENT.gameScale + + pygame.transform.scale(self.bgFill, (width, height)) + + if isinstance(bgFill, tuple): # If background is a solid colour fill + self.WINDOW.window.fill(bgFill) + elif isinstance(bgFill, pygame.surface.Surface): # Elif background is an image fill. + self.WINDOW.window.blit(bgFill, (self.LEVEL.dx * self.PARENT.gameScale, self.LEVEL.dy * self.PARENT.gameScale)) + + self.LEVEL.draw() + + self.PLAYER.update_move(playerActionMove) + + self.PLAYER.draw() + + # Refresh buttons. + for b in self.gameButtons: + b.draw() + + # Update objects on screen. + pygame.display.flip() + + # Event handler + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.gameOn = False + + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: # Window exit via esc. + ic(event.key) + self.gameOn = False + + if self.LEVEL.layout.HAS_PLAYER: # If layout shows a player + for controlsDict in [ + self.controlsPlayerMovement, + self.controlsPlayerInteraction + ]: + for action, key in controlsDict.items(): # Check movement triggers + if key == event.key: # Key pressed is associated with a movement action. + if controlsDict == self.controlsPlayerMovement: + playerActionMove = action + elif controlsDict == self.controlsPlayerInteraction: + if action == "trigger": + self.PLAYER.interact() + + elif event.type == pygame.KEYUP: + if self.LEVEL.layout.HAS_PLAYER: # If layout shows a player + for controlsDict in [ + self.controlsPlayerMovement, + self.controlsPlayerInteraction + ]: + for action, key in controlsDict.items(): # Check movement triggers + if key == event.key: # Key released is associated with a movement action. + if controlsDict == self.controlsPlayerMovement: + if action == playerActionMove: # Only if the most recent movement action key was released + playerActionMove = "neutral" # stop the player. + + elif event.type == pygame.MOUSEBUTTONDOWN: + for button in self.gameButtons: + if button.is_clicked: + if button.ID == "settings": + # open settings panel. + pass + + elif event.type == pygame.MOUSEWHEEL: + + # Scale the level + if self.SETTINGS["controls"]["mouse"]["is_inverted"]: + scaleNew = self.SCALE.gameScale + event.y # event.y should be +1 or -1 generally. I have seen 0 and others before somehow. + else: + scaleNew = self.SCALE.gameScale - event.y + + if scaleNew in range(4, 30): # Limit the scaling range (limit level zoom). + + if self.SCALE.gameScale < scaleNew: + # Source images need to be refreshed if character is growing in size. + ic(self.PLAYER.update_images(self.PLAYER.character)) + else: + ic(self.PLAYER.update_scale()) + + self.SCALE.gameScale = scaleNew + + ic(self.SCALE.update_units_game()) + + ic(self.PLAYER.update_scale()) + + self.LEVEL.update_deltas_relative_to_player() + + if isinstance(self.bgFill, pygame.surface.Surface): # Update the scaling of the bgFill if it's an image fill. + width = self.WINDOW.width / self.PARENT.gameX + height = self.WINDOW.height / self.PARENT.gameY + + pygame.transform.scale(self.bgFill, (width, height)) + + class Window(): + """ + Creates a window. + """ + + def __init__(self, parent, width:int = 1440, height:int = 720, framerate:int = 30): + + self.PARENT = parent + + self.width = width + self.height = height + self.size = self.width, self.height + self.framerate = framerate + + self.update_aspect_ratio() + + ic(self.aspectX, self.aspectY) + + self.window = pygame.display.set_mode(self.size) + + def update_aspect_ratio(self): + """ + Updates the aspect ratio for correct block scaling. + """ + + gcd = math.gcd(self.height, self.width) + + self.aspectX = self.width / gcd + self.aspectY = self.height / gcd + + def get_smallest_window_edge(self): + """ + Returns the smallest edge of the window. + """ + + if self.height < self.width: + return self.height + else: + return self.width + + class Scale(): + """ + Handles all globally referenced scaling. + """ + + def __init__(self, parent): + self.PARENT = parent + self.WINDOW = self.PARENT.WINDOW + + self.editorScale = 4 + self.uiScale = 1 + + self.update_units_editor() + + def update_units_editor(self): + """ + Gets editor units. + """ + + self.WINDOW.update_aspect_ratio() + + self.editorX = self.editorScale * self.WINDOW.aspectX # Blocks along X + self.editorY = self.editorScale * self.WINDOW.aspectY # Blocks along Y + + ic(self.editorX, self.editorY) + + if self.editorX > self.editorY: # Determine the blocks along the shortest edge. + blocksAlongEdge = self.editorY + else: + blocksAlongEdge = self.editorX + + self.unitBlockPx = self.WINDOW.get_smallest_window_edge() / blocksAlongEdge # Defines the edge length in pixels of a section. + + """ + if (self.unitBlockPx % 2) != 0: # Is not an integer. + self.unitBlockPx = (self.unitBlockPx // 1) + 1 # Round up. + """ + + ic(self.unitBlockPx) + +editor = Editor() +editor.run() + +pygame.quit() \ No newline at end of file diff --git a/structure.txt b/structure.txt new file mode 100644 index 0000000..158453b --- /dev/null +++ b/structure.txt @@ -0,0 +1,193 @@ +#!~/.pyenv/versions/3.11.6/bin/python +# +# Copyright (c) 2024 Cutieguwu | Olivia Brooks +# +# -*- coding:utf-8 -*- +# @Title: Project Structure. +# @Author: Cutieguwu | Olivia Brooks +# @Email: owen.brooks77@gmail.com | obroo2@ocdsb.ca +# @Description: PyGame summative project structure. +# +# @Script: structure.txt +# @Date Created: 22 Mar, 2024 +# @Last Modified: 13 Jun, 2024 +# @Last Modified by: Cutieguwu | Olivia Brooks +# ---------------------------------------------------------- + +. +├── config +│ ├── features.json +│ └── settings +│ ├── default.json +│ └── user.json +├── data +│ ├── credits.txt +│ ├── font +│ │ ├── Another_Danger___Demo_Regular.otf +│ │ ├── Domestic_Manners.ttf +│ │ └── Who_asks_Satan_Regular.ttf +│ ├── image +│ │ ├── game +│ │ │ ├── blocks +│ │ │ │ ├── puddle_blood +│ │ │ │ │ ├── puddle_blood_0.png +│ │ │ │ │ ├── puddle_blood_1.png +│ │ │ │ │ ├── puddle_blood_2.png +│ │ │ │ │ ├── puddle_blood_3.png +│ │ │ │ │ ├── puddle_blood_4.png +│ │ │ │ │ └── puddle_blood_neutral.png +│ │ │ │ ├── puddle_souls +│ │ │ │ │ ├── puddle_souls_rich.png +│ │ │ │ │ └── puddle_souls_spent.png +│ │ │ │ ├── wool_colored_blue.png +│ │ │ │ ├── wool_colored_brown.png +│ │ │ │ ├── wool_colored_cyan.png +│ │ │ │ ├── wool_colored_gray.png +│ │ │ │ ├── wool_colored_green.png +│ │ │ │ ├── wool_colored_light_blue.png +│ │ │ │ ├── wool_colored_lime.png +│ │ │ │ ├── wool_colored_magenta.png +│ │ │ │ ├── wool_colored_orange.png +│ │ │ │ ├── wool_colored_pink.png +│ │ │ │ ├── wool_colored_purple.png +│ │ │ │ ├── wool_colored_red.png +│ │ │ │ ├── wool_colored_silver.png +│ │ │ │ ├── wool_colored_white.png +│ │ │ │ └── wool_colored_yellow.png +│ │ │ ├── entity +│ │ │ │ └── unnamed +│ │ │ │ ├── neutral_1.png +│ │ │ │ ├── neutral_2.png +│ │ │ │ ├── run_d_1.png +│ │ │ │ ├── run_d_2.png +│ │ │ │ ├── run_l_1.png +│ │ │ │ ├── run_l_2.png +│ │ │ │ ├── run_r_1.png +│ │ │ │ ├── run_r_2.png +│ │ │ │ ├── run_u_1.png +│ │ │ │ └── run_u_2.png +│ │ │ ├── icon.png +│ │ │ ├── player +│ │ │ │ └── default_a +│ │ │ │ ├── base +│ │ │ │ │ ├── neutral_1.png +│ │ │ │ │ ├── neutral_2.png +│ │ │ │ │ ├── run_d_1.png +│ │ │ │ │ ├── run_d_2.png +│ │ │ │ │ ├── run_l_1.png +│ │ │ │ │ ├── run_l_2.png +│ │ │ │ │ ├── run_r_1.png +│ │ │ │ │ ├── run_r_2.png +│ │ │ │ │ ├── run_u_1.png +│ │ │ │ │ └── run_u_2.png +│ │ │ │ └── war_paint +│ │ │ │ ├── neutral_1.png +│ │ │ │ ├── neutral_2.png +│ │ │ │ ├── run_d_1.png +│ │ │ │ ├── run_d_2.png +│ │ │ │ ├── run_l_1.png +│ │ │ │ ├── run_l_2.png +│ │ │ │ ├── run_r_1.png +│ │ │ │ ├── run_r_2.png +│ │ │ │ ├── run_u_1.png +│ │ │ │ └── run_u_2.png +│ │ │ └── scenes +│ │ │ └── commencing_slaughter +│ │ │ ├── Commencing_Slaughter_1.png +│ │ │ ├── Commencing_Slaughter_2.png +│ │ │ ├── commencing_slaughter_3.png +│ │ │ └── commencing_slaughter_4.png +│ │ └── ui +│ │ ├── icons +│ │ │ ├── checkbox_active.png +│ │ │ ├── checkbox_neutral.png +│ │ │ ├── settings_hover.png +│ │ │ └── settings_neutral.png +│ │ ├── menu_main +│ │ │ ├── start_hover.png +│ │ │ └── start_neutral.png +│ │ ├── menu_pause +│ │ ├── menu_settings +│ │ └── screen_init +│ │ ├── init_bar_clear.png +│ │ ├── init_bar_fill.png +│ │ ├── init_bar_left.png +│ │ ├── init_bar_right.png +│ │ ├── mushroom_silhouette_1.png +│ │ └── mushroom_silhouette_2.png +│ └── sound +│ ├── game +│ │ ├── entity +│ │ │ ├── ambient +│ │ │ │ ├── Evil Laugh Cackle-SoundBible.com-957382653.wav +│ │ │ │ └── Monster Growl-SoundBible.com-344645592.wav +│ │ │ └── death +│ │ │ ├── Bone Crushing.mp3-SoundBible.com-537581287.wav +│ │ │ ├── Decapitation Head Blood-SoundBible.com-310864499.mp3 +│ │ │ ├── jenna_cough.mp3 +│ │ │ ├── neck_snap-Vladimir-719669812.wav +│ │ │ ├── Psycho Scream-SoundBible.com-1441943673.wav +│ │ │ └── Zombie Long Death-SoundBible.com-554299929.wav +│ │ ├── environment +│ │ │ ├── Killer Movie Scene-SoundBible.com-208071934.wav +│ │ │ └── Tornado Siren-SoundBible.com-897026957.wav +│ │ ├── player +│ │ │ └── Shells_falls-Marcel-829263474.wav +│ │ ├── scenes +│ │ │ └── commencing_slaughter.mp3 +│ │ └── weapon +│ │ ├── 9_mm_gunshot-mike-koenig-123.wav +│ │ ├── Chain Saw-SoundBible.com-247695753.wav +│ │ ├── Explosion_Ultra_Bass-Mark_DiAngelo-1810420658.wav +│ │ ├── GUN_FIRE-GoodSoundForYou-820112263.wav +│ │ ├── High_Definition_Machine_gun-WEL-2006923900.wav +│ │ ├── Minigun-Jim_Rogers-633894726.wav +│ │ └── Shotgun_Blast-Jim_Rogers-1914772763.wav +│ └── ui +│ ├── menu_main +│ │ └── Backwards-Souls-SoundBible.com-87826574.wav +│ ├── scene_player-died +│ └── scene_player-won +├── lib +│ ├── effect.py +│ ├── entity.py +│ ├── interface.py +│ ├── level.py +│ ├── __pycache__ +│ │ ├── effect.cpython-311.pyc +│ │ ├── entities.cpython-311.pyc +│ │ ├── entity.cpython-311.pyc +│ │ ├── interface.cpython-311.pyc +│ │ ├── level.cpython-311.pyc +│ │ ├── levels.cpython-311.pyc +│ │ ├── scene.cpython-311.pyc +│ │ ├── scenes.cpython-311.pyc +│ │ ├── section.cpython-311.pyc +│ │ ├── sections.cpython-311.pyc +│ │ └── system.cpython-311.pyc +│ ├── section.py +│ └── system.py +├── logs +│ └── debug.txt +├── main.py +├── mods +│ └── LevelEditor +│ ├── data +│ │ └── image +│ │ └── editor +│ │ └── icon.png +│ ├── level.py +│ ├── main.py +│ └── __pycache__ +│ └── level.cpython-311.pyc +├── __pycache__ +│ └── main.cpython-311.pyc +├── structure.txt +├── temp +│ └── pausescreen.png +└── TESTING + ├── clean python version + ├── myList = ["A", "B", "C"].py + ├── refreshed spawned classes.py + ├── Time.py + └── Xlib_utilization.py \ No newline at end of file diff --git a/temp/pausescreen.png b/temp/pausescreen.png new file mode 100644 index 0000000..2ff6cad Binary files /dev/null and b/temp/pausescreen.png differ