diff --git a/ports/megaball/Megaball.sh b/ports/megaball/Megaball.sh new file mode 100755 index 000000000..8057f547e --- /dev/null +++ b/ports/megaball/Megaball.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} + +if [ -d "/opt/system/Tools/PortMaster/" ]; then + controlfolder="/opt/system/Tools/PortMaster" +elif [ -d "/opt/tools/PortMaster/" ]; then + controlfolder="/opt/tools/PortMaster" +elif [ -d "$XDG_DATA_HOME/PortMaster/" ]; then + controlfolder="$XDG_DATA_HOME/PortMaster" +else + controlfolder="/roms/ports/PortMaster" +fi + +source $controlfolder/control.txt +[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt" +get_controls + +GAMEDIR="/$directory/ports/megaball" +CONFDIR="$GAMEDIR/conf" +PYXEL_PKG="main.py" + +cd "${GAMEDIR}" + +> "${GAMEDIR}/log.txt" && exec > >(tee "${GAMEDIR}/log.txt") 2>&1 + +mkdir -p "$GAMEDIR/conf" +bind_directories "$HOME/.config/.pyxel/megaball" "$CONFDIR" + +# Load Pyxel runtime +runtime="pyxel_2.2.8_python_3.11" +export pyxel_dir="$HOME/pyxel" +mkdir -p "${pyxel_dir}" + +if [ ! -f "$controlfolder/libs/${runtime}.squashfs" ]; then + # Check for runtime if not downloaded via PM + if [ ! -f "$controlfolder/harbourmaster" ]; then + pm_message "This port requires the latest PortMaster to run, please go to https://portmaster.games/ for more info." + sleep 5 + exit 1 + fi + + $ESUDO $controlfolder/harbourmaster --quiet --no-check runtime_check "${runtime}.squashfs" +fi + +if [[ "$PM_CAN_MOUNT" != "N" ]]; then + $ESUDO umount "${pyxel_dir}" +fi + +$ESUDO mount "$controlfolder/libs/${runtime}.squashfs" "${pyxel_dir}" + +export SDL_GAMECONTROLLERCONFIG="$sdl_controllerconfig" + +$GPTOKEYB "pyxel" & + +pm_platform_helper "${pyxel_dir}/bin/pyxel" + +# Enable Pyxel virtual env +source "${pyxel_dir}/bin/activate" +export PYTHONHOME="${pyxel_dir}" +export PYTHONPYCACHEPREFIX="${GAMEDIR}/${runtime}.cache" + +"${pyxel_dir}/bin/pyxel" run "${GAMEDIR}/gamedata/${PYXEL_PKG}" + +if [[ "$PM_CAN_MOUNT" != "N" ]]; then + $ESUDO umount "${pyxel_dir}" +fi + +pm_finish diff --git a/ports/megaball/README.md b/ports/megaball/README.md new file mode 100644 index 000000000..731294e3e --- /dev/null +++ b/ports/megaball/README.md @@ -0,0 +1,30 @@ +## Notes + +Thanks [helpcomputer](https://helpcomputer.itch.io) (Adam) for creating this fantastic game and releasing it under an MIT license. + + +## Controls + +| Button | Action | +| -------| ------------- | +| D-PAD | Movement | +| START | Menu | +| A | Self-destruct | + + +## Compile + +```shell +apt update +apt install wget git python3-venv # python >=3.8 is required + +# Setup pyxel virtual env +python3 -m venv pyxel-venv +source pyxel-venv/bin/activate +pip install pyxel + +# Test the game +git clone https://github.com/PortsMaster/PortMaster-New.git +cd ports/megaball +pyxel run megaball/gamedata/main.py +``` diff --git a/ports/megaball/gameinfo.xml b/ports/megaball/gameinfo.xml new file mode 100644 index 000000000..62d80b4d5 --- /dev/null +++ b/ports/megaball/gameinfo.xml @@ -0,0 +1,13 @@ + + + + ./Megaball.sh + Megaball + The goal is to roll the ball over every flashing panel to complete the stage. For each stage you complete, you gain one extra life. If you need to, you can self-destruct (and lose a life) and shatter into ten pieces, killing any enemy which collides with these pieces; you will immediately re-spawn, and the enemy will reappear in 5 seconds. + 20200906T000000 + helpcomputer (Adam) + helpcomputer + Arcade + ./megaball/screenshot.png + + diff --git a/ports/megaball/megaball/gamedata/.gitignore b/ports/megaball/megaball/gamedata/.gitignore new file mode 100644 index 000000000..c18dd8d83 --- /dev/null +++ b/ports/megaball/megaball/gamedata/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/ports/megaball/megaball/gamedata/assets/img0.png b/ports/megaball/megaball/gamedata/assets/img0.png new file mode 100644 index 000000000..e0db8309e Binary files /dev/null and b/ports/megaball/megaball/gamedata/assets/img0.png differ diff --git a/ports/megaball/megaball/gamedata/assets/my_resource.pyxres b/ports/megaball/megaball/gamedata/assets/my_resource.pyxres new file mode 100644 index 000000000..369b2d277 Binary files /dev/null and b/ports/megaball/megaball/gamedata/assets/my_resource.pyxres differ diff --git a/ports/megaball/megaball/gamedata/audio.py b/ports/megaball/megaball/gamedata/audio.py new file mode 100644 index 000000000..1229365b4 --- /dev/null +++ b/ports/megaball/megaball/gamedata/audio.py @@ -0,0 +1,57 @@ + +import pyxel + +import globals + +MUS_IN_GAME = 0 +MUS_TITLE = 1 +MUS_START = 2 +MUS_STAGE_COMPLETE = 3 +MUS_DEATH = 4 +MUS_GAME_OVER = 5 + +MUSIC = [ + MUS_IN_GAME, + MUS_TITLE, + MUS_START, + MUS_STAGE_COMPLETE, + MUS_DEATH, + #MUS_GAME_OVER +] + +SND_MENU_MOVE = 16 +SND_MENU_SELECT = 17 +SND_HIT_WALL = 18 +SND_HIT_TARGET = 19 +SND_USED_WEAPON = 20 + +SOUNDS = [ + SND_MENU_MOVE, + SND_MENU_SELECT, + SND_HIT_WALL, + SND_HIT_TARGET, + SND_USED_WEAPON, +] + +def play_sound(snd, looping=False): + if globals.g_sound_on == False: + return + + if snd not in SOUNDS: + return + + if pyxel.play_pos(3) != -1: + return + + pyxel.play(3, snd, loop=looping) + +def play_music(msc, looping=False): + if globals.g_music_on == False: + return + + if msc not in MUSIC: + return + + pyxel.stop() + + pyxel.playm(msc, loop=looping) diff --git a/ports/megaball/megaball/gamedata/circle.py b/ports/megaball/megaball/gamedata/circle.py new file mode 100644 index 000000000..4bbb72682 --- /dev/null +++ b/ports/megaball/megaball/gamedata/circle.py @@ -0,0 +1,29 @@ + +def overlap(x1, y1, r1, x2, y2, r2): + dx = x1 - x2 + dy = y1 - y2 + dist = dx * dx + dy * dy + radiusSum = r1 + r2 + return dist < radiusSum * radiusSum + +def contains_other(x1, y1, r1, x2, y2, r2): + radiusDiff = r1 - r2 + if radiusDiff < 0: + return False + + dx = x1 - x2 + dy = y1 - y2 + dist = dx * dx + dy * dy + radiusSum = r1 + r2 + return (not(radiusDiff * radiusDiff < dist) and (dist < radiusSum * radiusSum)) + +def contains_point(x, y, radius, px, py): + dx = x - px + dy = y - py + return dx * dx + dy * dy <= radius * radius + +class Circle: + def __init__(self, x, y, radius): + self.x = x + self.y = y + self.radius = radius diff --git a/ports/megaball/megaball/gamedata/constants.py b/ports/megaball/megaball/gamedata/constants.py new file mode 100644 index 000000000..8d0e753fc --- /dev/null +++ b/ports/megaball/megaball/gamedata/constants.py @@ -0,0 +1,73 @@ + +GAME_TITLE = "MEGABALL" +GAME_WIDTH = 160 +GAME_HEIGHT = 144 +GAME_FPS = 60 +GAME_SCALE = 4 + +IMAGE_BANK_0_FILE = "assets/img0.png" +RESOURCE_FILE = "assets/my_resource.pyxres" + +COLLIDE_TOP_LEFT = [ + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0], +] + +COLLIDE_TOP_RIGHT = [ + [1, 1, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 1], +] + +COLLIDE_BOTTOM_RIGHT = [ + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 1, 1, 1, 1, 1], + [0, 0, 1, 1, 1, 1, 1, 1], + [0, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1], +] + +COLLIDE_BOTTOM_LEFT = [ + [1, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 0], + [1, 1, 1, 1, 1, 1, 1, 1], +] + +COLLIDE_MATRIX_ALL = [ + COLLIDE_TOP_LEFT, + COLLIDE_TOP_RIGHT, + COLLIDE_BOTTOM_RIGHT, + COLLIDE_BOTTOM_LEFT +] + +def is_colliding_matrix(x, y, matrix): + if matrix not in COLLIDE_MATRIX_ALL: + return False + + if x < 0 or x > 7 or y < 0 or y > 7: + return False + + return matrix[y][x] + + + + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/game.py b/ports/megaball/megaball/gamedata/game.py new file mode 100644 index 000000000..30d10fdf9 --- /dev/null +++ b/ports/megaball/megaball/gamedata/game.py @@ -0,0 +1,103 @@ + +import pyxel + +import constants +import palette +import hud +import input +import stage +import screenshake +import mainmenu +import globals +import audio + +class Game: + def __init__(self): + self.pal_control = palette.PaletteControl() + + self.screen_shake = screenshake.ScreenShake(self) + + self.main_menu = mainmenu.MainMenu(self) + self.stage = stage.Stage(self, 0) + self.hud = hud.Hud(self) + + self.pal_index = 0 + + audio.play_music(audio.MUS_TITLE, True) + + def restart_music(self): + if self.main_menu.is_visible: + audio.play_music(audio.MUS_TITLE) + else: + self.stage.restart_music() + + def quit_to_main_menu(self): + del self.stage + self.stage = stage.Stage(self, 0) + globals.set_high_score() + globals.reset() + self.main_menu.reset() + self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) + + def go_to_next_stage(self): + globals.g_stage_num += 1 + del self.stage + self.stage = stage.Stage(self, globals.g_stage_num) + self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) + + def go_to_game_complete_stage(self): + del self.stage + self.stage = stage.Stage(self, stage.MAX_STAGE_NUM + 1) + self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) + + def restart_stage(self): + del self.stage + self.stage = stage.Stage(self, globals.g_stage_num) + self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) + + def start_game(self): + self.main_menu.hide() + del self.stage + self.stage = stage.Stage(self, globals.g_stage_num) + self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) + + def add_screen_shake(self, ticks, magnitude, queue=False): + self.screen_shake.add_event(ticks, magnitude, queue) + + def cycle_palette(self): + self.pal_index += 1 + if self.pal_index == len(palette.ALL): + self.pal_index = 0 + self.pal_control.add_palette_event(1, palette.ALL[self.pal_index]) + self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) + + def add_fade(self, ticks_per_level, target_level, callback=None): + self.pal_control.add_fade_event(ticks_per_level, target_level, callback) + + def update(self, last_inputs): + if pyxel.btnp(pyxel.KEY_F1): + globals.toggle_sound() + + if pyxel.btnp(pyxel.KEY_F2): + globals.toggle_music(self) + + self.main_menu.update(last_inputs) + + self.stage.update(last_inputs) + + self.pal_control.update() + self.screen_shake.update() + + def draw(self): + for c in range(palette.NUM_COLOURS): + pyxel.pal(palette.DEFAULT[c], self.pal_control.get_col(c)) + + pyxel.cls(self.pal_control.get_col(0)) + + self.stage.draw(self.screen_shake.x, self.screen_shake.y) + self.hud.draw(self.screen_shake.x, self.screen_shake.y) + + self.main_menu.draw(self.screen_shake.x, self.screen_shake.y) + + pyxel.pal() + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/globals.py b/ports/megaball/megaball/gamedata/globals.py new file mode 100644 index 000000000..bc0bb198b --- /dev/null +++ b/ports/megaball/megaball/gamedata/globals.py @@ -0,0 +1,66 @@ + +import pyxel + +import constants +import game + +STARTING_LIVES = 2 +MAX_SCORE = 999999 +MAX_LIVES = 99 + +SCORE_HIT_LIGHT = 200 +SCORE_STAGE_COMPLETE = 1000 +SCORE_USE_WEAPON = 4000 +SCORE_KILLED_SPINNER = 200 +SCORE_KILLED_ALL_SPINNERS = 10000 + +g_lives = STARTING_LIVES +g_score = 0 +g_highscore = 0 +g_stage_num = 1 +g_sound_on = True +g_music_on = True + +def reset(): + global g_lives + global g_score + global g_stage_num + + g_lives = STARTING_LIVES + g_score = 0 + g_stage_num = 1 + +def toggle_sound(): + global g_sound_on + + g_sound_on = not g_sound_on + + if g_sound_on == False: + pyxel.stop() + +def toggle_music(game_obj): + global g_music_on + + g_music_on = not g_music_on + + if g_music_on == False: + pyxel.stop() + else: + game_obj.restart_music() + +def set_high_score(): + global g_score + global g_highscore + + g_highscore = max(g_score, g_highscore) + +def add_lives(amt): + global g_lives + + g_lives = max(0, min(g_lives + amt, MAX_LIVES)) + +def add_score(amt): + global g_score + + g_score = max(0, min(g_score + amt, MAX_SCORE)) + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/hud.py b/ports/megaball/megaball/gamedata/hud.py new file mode 100644 index 000000000..51885d69a --- /dev/null +++ b/ports/megaball/megaball/gamedata/hud.py @@ -0,0 +1,31 @@ + +import pyxel + +import game +import constants +import globals +import utils +import stage + +class Hud: + def __init__(self, game): + self.game = game + + def update(self): + pass + + def draw(self, shake_x, shake_y): + # top bar + pyxel.blt(shake_x + 0, shake_y + 0, 0, 0, 0, constants.GAME_WIDTH, 16) + # bottom bar + pyxel.blt(shake_x + 0, shake_y + 136, 0, 0, 16, constants.GAME_WIDTH, 8) + # left bar + pyxel.blt(shake_x + 0, shake_y + 16, 0, 0, 24, 8, 120) + # right bar + pyxel.blt(shake_x + 152, shake_y + 16, 0, 8, 24, 8, 120) + + utils.draw_number_shadowed(shake_x + 31, shake_y + 5, globals.g_lives, zeropad=2) + utils.draw_number_shadowed(shake_x + 57, shake_y + 5, globals.g_score, zeropad=6) + utils.draw_number_shadowed(shake_x + 113, shake_y + 5, globals.g_stage_num, zeropad=2) + utils.draw_number_shadowed(shake_x + 137, shake_y + 5, stage.MAX_STAGE_NUM, zeropad=2) + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/icon.ico b/ports/megaball/megaball/gamedata/icon.ico new file mode 100644 index 000000000..defd61b9d Binary files /dev/null and b/ports/megaball/megaball/gamedata/icon.ico differ diff --git a/ports/megaball/megaball/gamedata/input.py b/ports/megaball/megaball/gamedata/input.py new file mode 100644 index 000000000..fa13def5c --- /dev/null +++ b/ports/megaball/megaball/gamedata/input.py @@ -0,0 +1,85 @@ + +import pyxel + +UP = 0 +DOWN = 1 +LEFT = 2 +RIGHT = 3 +BUTTON_A = 4 +BUTTON_B = 5 +BUTTON_START = 6 +BUTTON_SELECT = 7 + +class Input: + + def __init__(self): + self.pressing = [] + self.pressed = [] + + def get(self): + self.pressing.clear() + self.pressed.clear() + + # pressing + if pyxel.btn(pyxel.KEY_UP) or pyxel.btn(pyxel.KEY_W) or \ + pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_UP): + self.pressing.append(UP) + elif pyxel.btn(pyxel.KEY_DOWN) or pyxel.btn(pyxel.KEY_S) or \ + pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_DOWN): + self.pressing.append(DOWN) + + if pyxel.btn(pyxel.KEY_LEFT) or pyxel.btn(pyxel.KEY_A) or \ + pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_LEFT): + self.pressing.append(LEFT) + elif pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D) or \ + pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_RIGHT): + self.pressing.append(RIGHT) + + if pyxel.btn(pyxel.KEY_Z) or pyxel.btn(pyxel.KEY_K) or \ + pyxel.btn(pyxel.GAMEPAD1_BUTTON_A): + self.pressing.append(BUTTON_A) + + if pyxel.btn(pyxel.KEY_X) or pyxel.btn(pyxel.KEY_L) or \ + pyxel.btn(pyxel.GAMEPAD1_BUTTON_B): + self.pressing.append(BUTTON_B) + + if pyxel.btn(pyxel.KEY_RETURN) or \ + pyxel.btn(pyxel.GAMEPAD1_BUTTON_START): + self.pressing.append(BUTTON_START) + + if pyxel.btn(pyxel.KEY_SPACE) or \ + pyxel.btn(pyxel.GAMEPAD1_BUTTON_BACK): + self.pressing.append(BUTTON_SELECT) + + # pressed + if pyxel.btnp(pyxel.KEY_UP) or pyxel.btnp(pyxel.KEY_W) or \ + pyxel.btnp(pyxel.GAMEPAD1_BUTTON_DPAD_UP): + self.pressed.append(UP) + elif pyxel.btnp(pyxel.KEY_DOWN) or pyxel.btnp(pyxel.KEY_S) or \ + pyxel.btnp(pyxel.GAMEPAD1_BUTTON_DPAD_DOWN): + self.pressed.append(DOWN) + + if pyxel.btnp(pyxel.KEY_LEFT) or pyxel.btnp(pyxel.KEY_A) or \ + pyxel.btnp(pyxel.GAMEPAD1_BUTTON_DPAD_LEFT): + self.pressed.append(LEFT) + elif pyxel.btnp(pyxel.KEY_RIGHT) or pyxel.btnp(pyxel.KEY_D) or \ + pyxel.btnp(pyxel.GAMEPAD1_BUTTON_DPAD_RIGHT): + self.pressed.append(RIGHT) + + if pyxel.btnp(pyxel.KEY_Z) or pyxel.btnp(pyxel.KEY_K) or \ + pyxel.btnp(pyxel.GAMEPAD1_BUTTON_A): + self.pressed.append(BUTTON_A) + + if pyxel.btnp(pyxel.KEY_X) or pyxel.btnp(pyxel.KEY_L) or \ + pyxel.btnp(pyxel.GAMEPAD1_BUTTON_B): + self.pressed.append(BUTTON_B) + + if pyxel.btnp(pyxel.KEY_RETURN) or \ + pyxel.btnp(pyxel.GAMEPAD1_BUTTON_START): + self.pressed.append(BUTTON_START) + + if pyxel.btnp(pyxel.KEY_SPACE) or \ + pyxel.btnp(pyxel.GAMEPAD1_BUTTON_BACK): + self.pressed.append(BUTTON_SELECT) + + diff --git a/ports/megaball/megaball/gamedata/light.py b/ports/megaball/megaball/gamedata/light.py new file mode 100644 index 000000000..b48f659e4 --- /dev/null +++ b/ports/megaball/megaball/gamedata/light.py @@ -0,0 +1,41 @@ + +import pyxel + +import globals + +TICKS_PER_FRAME = 10 +MAX_FRAMES = 5 + +class Light: + def __init__(self, x, y): + self.x = x + self.y = y + + self.frame = 0 + self.frame_ticks = 0 + self.anim_dir = 1 + + self.is_hit = False + + def got_hit(self): + if self.is_hit == False: + self.frame = 4 + self.is_hit = True + globals.add_score(globals.SCORE_HIT_LIGHT) + return True + return False + + def update(self, stage): + if not self.is_hit: + self.frame_ticks += 1 + + if self.frame_ticks == TICKS_PER_FRAME: + self.frame_ticks = 0 + self.frame += self.anim_dir + + if self.frame == 0 or self.frame == MAX_FRAMES - 1: + self.anim_dir *= -1 + + def draw(self, shake_x, shake_y): + pyxel.blt(shake_x + self.x, shake_y + self.y, 0, 160 + self.frame*8, 0, 8, 8) + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/main.py b/ports/megaball/megaball/gamedata/main.py new file mode 100644 index 000000000..ec94e6229 --- /dev/null +++ b/ports/megaball/megaball/gamedata/main.py @@ -0,0 +1,36 @@ + +import pyxel + +import constants +import input +import game + +class App: + def __init__(self): + pyxel.init( + width=constants.GAME_WIDTH, + height=constants.GAME_HEIGHT, + fps=constants.GAME_FPS + ) + pyxel.scale=constants.GAME_SCALE + pyxel.caption = constants.GAME_TITLE + + pyxel.load(constants.RESOURCE_FILE) + pyxel.images[0].load(0, 0, constants.IMAGE_BANK_0_FILE) + + self.input = input.Input() + self.game = game.Game() + pyxel.mouse(False) + #pyxel.mouse(True) + + pyxel.run(self.update, self.draw) + + def update(self): + self.input.get() + self.game.update(self.input) + + def draw(self): + self.game.draw() + +App() + diff --git a/ports/megaball/megaball/gamedata/mainmenu.py b/ports/megaball/megaball/gamedata/mainmenu.py new file mode 100644 index 000000000..496a502aa --- /dev/null +++ b/ports/megaball/megaball/gamedata/mainmenu.py @@ -0,0 +1,118 @@ + +import pyxel + +import utils +import globals +import game +import input +import palette +import audio + +SEL_START_GAME = 0 +SEL_PALETTE = 1 +SEL_EXIT_GAME = 2 + +SELECTIONS = { + SEL_START_GAME : [40,87,80,8], # [x, y, w, h] + SEL_PALETTE : [52,103,56,8], + SEL_EXIT_GAME : [44,119,72,8] +} + +class MainMenu: + def __init__(self, game): + self.game = game + + self.is_visible = True + + self.show_press_start = True + self.press_start_flash_ticks = 0 + self.sel_index = 0 + + def hide(self): + self.is_visible = False + + def reset(self): + self.is_visible = True + self.show_press_start = True + self.press_start_flash_ticks = 0 + self.sel_index = 0 + audio.play_music(audio.MUS_TITLE, True) + + def _pressed_select(self): + audio.play_sound(audio.SND_MENU_SELECT) + if self.sel_index == SEL_START_GAME: + self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, + palette.FADE_LEVEL_6, self.game.start_game) + elif self.sel_index == SEL_PALETTE: + self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, + palette.FADE_LEVEL_6, self.game.cycle_palette) + elif self.sel_index == SEL_EXIT_GAME: + self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, + palette.FADE_LEVEL_0, pyxel.quit) + + def _change_selection(self, dir): + audio.play_sound(audio.SND_MENU_MOVE) + self.sel_index += dir + if self.sel_index < 0: + self.sel_index = len(SELECTIONS) - 1 + elif self.sel_index >= len(SELECTIONS): + self.sel_index = 0 + + def update(self, last_inputs): + if not self.is_visible: + return + + if self.show_press_start: + self.press_start_flash_ticks += 1 + if self.press_start_flash_ticks == 50: + self.press_start_flash_ticks = 0 + if input.BUTTON_START in last_inputs.pressed: + self.show_press_start = False + self.sel_index = 0 + else: + if input.BUTTON_A in last_inputs.pressed: + self._pressed_select() + elif input.UP in last_inputs.pressed: + self._change_selection(-1) + elif input.DOWN in last_inputs.pressed: + self._change_selection(1) + + def draw(self, shake_x, shake_y): + if not self.is_visible: + return + + if self.show_press_start: + if self.press_start_flash_ticks < 30: + pyxel.blt(shake_x + 36, shake_y + 104, 0, 16, 72, 40, 8, 8) # press + pyxel.blt(shake_x + 84, shake_y + 104, 0, 56, 72, 40, 8, 8) # start + else: + pyxel.blt(shake_x + 24, shake_y + 84, 0, 0, 144, 116, 52, 8) # panel bg + + pyxel.blt(shake_x + 40, shake_y + 88, 0, 56, 72, 40, 8, 8) # start + pyxel.blt(shake_x + 88, shake_y + 88, 0, 40, 80, 32, 8, 8) # game + + pyxel.blt(shake_x + 52, shake_y + 104, 0, 104, 80, 56, 8, 8) # palette + + pyxel.blt(shake_x + 44, shake_y + 120, 0, 96, 72, 32, 8, 8) # exit + pyxel.blt(shake_x + 84, shake_y + 120, 0, 40, 80, 32, 8, 8) # game + + pyxel.blt( + shake_x + SELECTIONS[self.sel_index][0]-12, + shake_y + SELECTIONS[self.sel_index][1], + 0, + 16, 33, 9, 9, 8 + ) # selection ball left + pyxel.blt( + shake_x + SELECTIONS[self.sel_index][0] + SELECTIONS[self.sel_index][2] + 2, + shake_y + SELECTIONS[self.sel_index][1], + 0, + 16, 33, 9, 9, 8 + ) # selection ball right + + pyxel.blt(shake_x + 44, shake_y + 20, 0, 16, 80, 24, 8, 8) # hi- + utils.draw_number_shadowed(shake_x + 68, shake_y + 20, + globals.g_highscore, zeropad=6) # highscore number + pyxel.blt(shake_x + 13, shake_y + 36, 0, 16, 88, 135, 44, 8) # logo + + + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/palette.py b/ports/megaball/megaball/gamedata/palette.py new file mode 100644 index 000000000..5684833d9 --- /dev/null +++ b/ports/megaball/megaball/gamedata/palette.py @@ -0,0 +1,135 @@ + +import pyxel + +NUM_COLOURS = 4 + +DEFAULT = [ pyxel.COLOR_NAVY, pyxel.COLOR_GREEN, pyxel.COLOR_LIME, pyxel.COLOR_WHITE ] +RED = [ pyxel.COLOR_PURPLE, pyxel.COLOR_RED, pyxel.COLOR_PINK, pyxel.COLOR_WHITE ] +BLUE = [ pyxel.COLOR_NAVY, pyxel.COLOR_DARK_BLUE, pyxel.COLOR_CYAN, pyxel.COLOR_WHITE ] +BROWN = [ pyxel.COLOR_BROWN, pyxel.COLOR_ORANGE, pyxel.COLOR_PEACH, pyxel.COLOR_WHITE ] +GREY = [ pyxel.COLOR_BLACK, pyxel.COLOR_DARK_BLUE, pyxel.COLOR_GRAY, pyxel.COLOR_WHITE ] + +ALL = [ + DEFAULT, + RED, + BLUE, + BROWN, + GREY +] + +FADE_LEVEL_0 = -3 # all colours to darkest colour. +FADE_LEVEL_1 = -2 # all but brightest to darkest colour. +FADE_LEVEL_2 = -1 # all but two brightest to darkest colour. +FADE_LEVEL_3 = 0 # no modification +FADE_LEVEL_4 = 1 # all but two darkest to brightest colour. +FADE_LEVEL_5 = 2 # all but darkest to brightest colour. +FADE_LEVEL_6 = 3 # all colours to brightest colour. + +FADE_LEVELS = [ + FADE_LEVEL_0, + FADE_LEVEL_1, + FADE_LEVEL_2, + FADE_LEVEL_3, + FADE_LEVEL_4, + FADE_LEVEL_5, + FADE_LEVEL_6 +] + +FADE_STEP_TICKS_DEFAULT = 5 +FADE_STEP_TICKS_SLOW = 10 + +class FadeEvent: + def __init__(self, ticks_per_level, new_level, callback=None): + self.ticks_per_level = ticks_per_level + self.ticks = 0 + self.new_level = new_level + self.callback = callback + +class FadeControl: + def __init__(self): + self.current_level = FADE_LEVEL_3 + + self.events = [] + + def add_event(self, ticks_per_level, new_level, callback=None): + if ticks_per_level <= 0 or new_level not in FADE_LEVELS: + return + + self.events.append(FadeEvent(ticks_per_level, new_level, callback)) + + def get_level(self): + return self.current_level + + def update(self): + if len(self.events) > 0: + e = self.events[0] + e.ticks += 1 + + if e.ticks == e.ticks_per_level: + e.ticks = 0 + if self.current_level < e.new_level: + self.current_level += 1 + elif self.current_level > e.new_level: + self.current_level -= 1 + + if self.current_level == e.new_level: + if e.callback is not None: + e.callback() + self.events.pop(0) + +class PaletteEvent: + def __init__(self, ticks, new_pal, callback=None): + self.ticks = ticks + self.new_pal = DEFAULT + self.callback = callback + if new_pal in ALL: + self.new_pal = new_pal + +class PaletteControl: + def __init__(self): + self.current_palette = DEFAULT + + self.events = [] + + self.fade_control = FadeControl() + + def add_fade_event(self, ticks_per_level, new_level, callback=None): + self.fade_control.add_event(ticks_per_level, new_level, callback) + + def add_palette_event(self, ticks, new_pal, callback=None): + if ticks <= 0 or new_pal not in ALL: + return + + #print("added palette event") + self.events.append(PaletteEvent(ticks, new_pal, callback)) + + def update(self): + self.fade_control.update() + + if len(self.events) > 0: + e = self.events[0] + e.ticks -= 1 + if e.ticks == 0: + self.current_palette = e.new_pal + if e.callback is not None: + e.callback() + self.events.pop(0) + + #print("Removed pal event, queue size now: " + str(len(self.events))) + + def set_pal(self, pal): + if pal in ALL: + self.current_palette = pal + + def get_pal(self): + return self.current_palette + + def get_col(self, index): + if index < 0 or index >= NUM_COLOURS: + return self.current_palette[0] + + index = max(0, min(NUM_COLOURS-1, index + self.fade_control.get_level())) + + return self.current_palette[index] + + diff --git a/ports/megaball/megaball/gamedata/player.py b/ports/megaball/megaball/gamedata/player.py new file mode 100644 index 000000000..a7d95d50e --- /dev/null +++ b/ports/megaball/megaball/gamedata/player.py @@ -0,0 +1,256 @@ + +import math + +import pyxel + +import utils +import input +import rect +import circle +import light +import weapon +import globals +import audio + +MAX_SPEED = 1.2 +DECEL = 0.01 +ACCEL = 0.06 +SLOPE_ACCEL = 0.10 + +HIT_SOLID_DAMP = 0.7 + +INTRO_TICKS_PER_FRAME = 10 +DEAD_TICKS_PER_FRAME = 10 + +STATE_INTRO = 0 +STATE_PLAY = 1 +STATE_DEAD = 2 +STATE_STAGE_COMPLETE = 3 +STATE_GAME_COMPLETE = 4 +STATE_WEAPON = 5 + +class Player: + def __init__(self, x, y): + self.x = x + self.y = y + + self.vx = 0 + self.vy = 0 + + self.radius = 4 + + self.state = STATE_INTRO + + self.intro_frame = 4 + self.dead_frame = 0 + + self.anim_ticks = 0 + + self.weapon = weapon.Weapon() + + def _do_solid_collisions(self, stage): + new_x = self.x + self.vx + + for b in stage.solid_rects: + if utils.circle_rect_overlap(new_x, self.y, self.radius, + b[0], b[1], b[2], b[3]): + if self.x > b[0] + b[2]: # was prev to right of border. + new_x = b[0] + b[2] + self.radius + elif self.x < b[0]: # was prev to left of border. + new_x = b[0] - self.radius + + self.vx *= -HIT_SOLID_DAMP + stage.player_hit_solid() + break + + new_y = self.y + self.vy + + for b in stage.solid_rects: + if utils.circle_rect_overlap(self.x, new_y, self.radius, + b[0], b[1], b[2], b[3]): + if self.y > b[1] + b[3]: # was prev below border. + new_y = b[1] + b[3] + self.radius + elif self.y < b[1]: # was prev above border. + new_y = b[1] - self.radius + + self.vy *= -HIT_SOLID_DAMP + stage.player_hit_solid() + break + + self.x = new_x + self.y = new_y + + def _get_input_angle(self, last_inputs): + press_angle = None + if input.LEFT in last_inputs.pressing: + press_angle = 180 + elif input.RIGHT in last_inputs.pressing: + press_angle = 0 + + if input.UP in last_inputs.pressing: + if press_angle == 0: + press_angle = 315 + elif press_angle == 180: + press_angle = 225 + else: + press_angle = 270 + elif input.DOWN in last_inputs.pressing: + if press_angle == 0: + press_angle = 45 + elif press_angle == 180: + press_angle = 135 + else: + press_angle = 90 + + return press_angle + + # a "force" is a list of lists: [[speed, angle]... etc]. + def _apply_forces(self, forces): + for f in forces: + self.vx = max(-MAX_SPEED, + min(MAX_SPEED, + self.vx + f[0] * math.cos(math.radians(f[1])))) + self.vy = max(-MAX_SPEED, + min(MAX_SPEED, + self.vy + f[0] * math.sin(math.radians(f[1])))) + #print("py after: " + str(self.y)) + + def _get_tile_force(self, stage, forces): + angle = stage.get_tile_angle(self.x, self.y) + if angle is not None: + forces.append([SLOPE_ACCEL, angle]) + #print("Got tile force: spd:{a}, accl:{b}".format(a=SLOPE_ACCEL, b=angle)) + + def _is_stuck_in_pocket(self, stage): + if abs(self.vx) > 0.01 or abs(self.vy) > 0.01: + return False + + for p in stage.pockets: + if rect.contains_point(p[0], p[1], p[2], p[3], self.x, self.y): + return True + + def _do_enemy_collisions(self, stage): + for s in stage.spinners: + if s.is_dead: + continue + if circle.overlap(s.x, s.y, s.radius, self.x, self.y, self.radius): + self.state = STATE_DEAD + stage.player_hit() + return + + def _do_light_collisions(self, stage): + for s in stage.lights: + if rect.contains_point(s.x, s.y, 8, 8, self.x, self.y): + if s.got_hit() == True: + audio.play_sound(audio.SND_HIT_TARGET) + if stage.is_complete(): + self.state = STATE_STAGE_COMPLETE + globals.add_score(globals.SCORE_STAGE_COMPLETE) + return + + def fire_weapon(self, stage): + self.weapon.fire(self.x, self.y) + self.state = STATE_WEAPON + globals.g_lives -= 1 + stage.player_used_weapon() + globals.add_score(globals.SCORE_USE_WEAPON) + + def weapon_done(self): + self.state = STATE_INTRO + self.intro_frame = 4 + self.anim_ticks = 0 + self.vx = 0 + self.vy = 0 + + def update(self, stage, last_inputs): + if self.state == STATE_INTRO: + self.anim_ticks += 1 + if self.anim_ticks == INTRO_TICKS_PER_FRAME: + self.anim_ticks = 0 + if self.intro_frame > -1: + self.intro_frame -= 1 + + if self.intro_frame == -1: + self.intro_frame = 4 + self.state = STATE_PLAY + stage.player_intro_done() + return + elif self.state == STATE_DEAD: + self.anim_ticks += 1 + if self.anim_ticks == DEAD_TICKS_PER_FRAME: + self.anim_ticks = 0 + + if self.dead_frame < 11: + self.dead_frame += 1 + + if self.dead_frame == 11: + stage.player_death_anim_done() + return + elif self.state == STATE_STAGE_COMPLETE: + return + elif self.state == STATE_WEAPON: + self.weapon.update(self, stage) + return + + forces = [] + + #print("py after: " + str(self.y)) + + input_angle = self._get_input_angle(last_inputs) + if input_angle is not None: + forces.append([ACCEL, input_angle]) + + if not self._is_stuck_in_pocket(stage): + self._get_tile_force(stage, forces) + + self._apply_forces(forces) + + if self.vx > 0: + self.vx = max(0, self.vx - DECEL) + elif self.vx < 0: + self.vx = min(0, self.vx + DECEL) + + if self.vy > 0: + self.vy = max(0, self.vy - DECEL) + elif self.vy < 0: + self.vy = min(0, self.vy + DECEL) + + #print("vx,vy: {a},{b}".format(a=self.vx, b=self.vy)) + + self._do_solid_collisions(stage) + + self._do_enemy_collisions(stage) + + if self.state != STATE_DEAD and self.state != STATE_GAME_COMPLETE: + self._do_light_collisions(stage) + + if input.BUTTON_A in last_inputs.pressed and \ + self.state != STATE_WEAPON and \ + globals.g_lives > 0: + self.fire_weapon(stage) + + #print("py after: " + str(self.y)) + + #if pyxel.mouse_x >= 8 and pyxel.mouse_x < 152 and \ + # pyxel.mouse_y >= 16 and pyxel.mouse_y < 136: + # ang = stage.get_tile_angle(pyxel.mouse_x, pyxel.mouse_y) + #if ang is not None: + # print("Hit slope angle {a},{b}: ".format(a=pyxel.mouse_x,b=pyxel.mouse_y)\ + # + str(ang) + ", " + str(pyxel.frame_count)) + + + def draw(self, shake_x, shake_y): + if self.state == STATE_INTRO: + pyxel.blt(shake_x + self.x-10, shake_y + self.y-10, 0, + self.intro_frame*21, 231, 21, 21, 8) + elif self.state == STATE_DEAD: + pyxel.blt(shake_x + self.x-10, shake_y + self.y-10, 0, + self.dead_frame*21, 231, 21, 21, 8) + elif self.state == STATE_WEAPON: + self.weapon.draw(shake_x, shake_y) + else: + pyxel.blt(shake_x + self.x-self.radius, shake_y + self.y-self.radius, 0, + 16, 33, 9, 9, 8) + + #pyxel.circb(self.x, self.y, self.radius, 8) + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/readme.txt b/ports/megaball/megaball/gamedata/readme.txt new file mode 100644 index 000000000..4fa60734d --- /dev/null +++ b/ports/megaball/megaball/gamedata/readme.txt @@ -0,0 +1,47 @@ +Megaball was made in a week for GBJam 8. + +The source code is also available here: https://github.com/helpcomputer/megaball + + +Gameplay + +The goal of the game is to roll the ball over every flashing panel to complete the stage. + +There are 15 stages that become increasingly more difficult, presenting you with tougher terrain and more enemies to avoid. + +For each stage you complete you gain one extra life. + +Your main aim should be to avoid enemies, but if you need to you can use your special weapon which will cause you to self-destruct and shatter into 10 pieces, killing any enemy which collides with these pieces. You will immediately re-spawn, and the enemy will reappear in 5 seconds. + + +Controls + +WASD, Arrow keys, or gamepad D-pad to move. + +Z key, K key, or gamepad Button A to fire weapon, or confirm in menu. + +Enter key or gamepad Start button to Start or Pause. + +F1 key to toggle sound. +F2 key to toggle music. + + +Credits + +Design & Art: + +https://helpcomputer.itch.io/ + +https://twitter.com/helpcomputer0 + +Sound and Music: + +https://mikerichmond.itch.io/ + +https://twitter.com/richmondmike + +Font by Damien Guard: + +https://damieng.com/typography/zx-origins/ + +https://twitter.com/damienguard \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/rect.py b/ports/megaball/megaball/gamedata/rect.py new file mode 100644 index 000000000..e6c854e98 --- /dev/null +++ b/ports/megaball/megaball/gamedata/rect.py @@ -0,0 +1,32 @@ + +def overlap(x1, y1, w1, h1, x2, y2, w2, h2): + return x1 < x2 + w2 and \ + x1 + w1 > x2 and \ + y1 < y2 + h2 and \ + y1 + h1 > y2 + +def contains_point(x, y, w, h, px, py): + return x <= px and \ + x + w >= px and \ + y <= py and \ + y + h >= py + +class Rect: + def __init__(self, x, y, w, h): + self.x = x + self.y = y + self.w = w + self.h = h + + def is_overlapping(self, x, y, w, h): + return self.x < x + w and \ + self.x + self.w > x and \ + self.y < y + h and \ + self.y + self.h > y + + def is_overlapping_other(self, other): + return self.x < other.x + other.w and \ + self.x + self.w > other.x and \ + self.y < other.y + other.h and \ + self.y + self.h > other.y + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/screenshake.py b/ports/megaball/megaball/gamedata/screenshake.py new file mode 100644 index 000000000..c741761b6 --- /dev/null +++ b/ports/megaball/megaball/gamedata/screenshake.py @@ -0,0 +1,38 @@ + +import random + +class Event: + def __init__(self, ticks, mag): + self.ticks = ticks + self.magnitude = mag + +class ScreenShake: + def __init__(self, game): + self.game = game + + self.x = 0 + self.y = 0 + + self.events = [] + + def add_event(self, ticks, magnitude, queue=False): + if ticks < 0 or magnitude <= 0: + return + + if len(self.events) > 0 and not queue: + return + + self.events.append(Event(ticks, magnitude)) + + def update(self): + if len(self.events) > 0: + e = self.events[0] + e.ticks -= 1 + if e.ticks == 0: + self.events.pop(0) + self.x = 0 + self.y = 0 + else: + self.x = random.randint(-e.magnitude, e.magnitude) + self.y = random.randint(-e.magnitude, e.magnitude) + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/spinner.py b/ports/megaball/megaball/gamedata/spinner.py new file mode 100644 index 000000000..da7aa14ca --- /dev/null +++ b/ports/megaball/megaball/gamedata/spinner.py @@ -0,0 +1,156 @@ + +import random + +import pyxel + +import utils +import stage + +TYPE_AGGRESSIVE = 0 +TYPE_MILD = 1 +TYPE_RANDOM_SLOW = 2 +TYPE_RANDOM_FAST = 3 + +TYPES = [ + TYPE_AGGRESSIVE, + TYPE_MILD, + TYPE_RANDOM_SLOW, + TYPE_RANDOM_FAST +] + +TICKS_PER_FRAME = 10 +MAX_FRAME = 4 + +MAX_SPEED = 0.4 +MAX_RESPAWN_TICKS = 300 # 5 secs + +class Spinner: + def __init__(self, x, y, type): + self.x = x + self.y = y + self.type = 2 + if type in TYPES: + self.type = type + + self.vx = random.choice([-MAX_SPEED, MAX_SPEED]) + self.vy = random.choice([-MAX_SPEED, MAX_SPEED]) + + self.radius = 4 + + self.frame = 0 + self.frame_ticks = 0 + + self.is_dead = False + + self.respawn_ticks = MAX_RESPAWN_TICKS + + def _set_new_position(self, stageObj): + px = stageObj.player.x + py = stageObj.player.y + loc = None + loclist = [ + stage.SPAWN_SECTOR_TOPLEFT, + stage.SPAWN_SECTOR_BOTTOMLEFT, + stage.SPAWN_SECTOR_TOPRIGHT, + stage.SPAWN_SECTOR_BOTTOMRIGHT + ] + if px < 80: + if py < 75: + loclist.remove(stage.SPAWN_SECTOR_TOPLEFT) + else: + loclist.remove(stage.SPAWN_SECTOR_BOTTOMLEFT) + else: + if py < 75: + loclist.remove(stage.SPAWN_SECTOR_TOPRIGHT) + else: + loclist.remove(stage.SPAWN_SECTOR_BOTTOMRIGHT) + + loc = stageObj.get_random_spawn_loc(random.choice(loclist)) + self.x = loc[0] + self.y = loc[1] + + def kill(self): + self.is_dead = True + self.respawn_ticks = MAX_RESPAWN_TICKS + + def _do_collisions(self, stage): + new_x = self.x + self.vx + + for b in stage.solid_rects: + if utils.circle_rect_overlap(new_x, self.y, self.radius, + b[0], b[1], b[2], b[3]): + if self.x > b[0] + b[2]: # was prev to right of border. + new_x = b[0] + b[2] + self.radius + elif self.x < b[0]: # was prev to left of border. + new_x = b[0] - self.radius + + self.vx *= -1 + break + + new_y = self.y + self.vy + + for b in stage.solid_rects: + if utils.circle_rect_overlap(self.x, new_y, self.radius, + b[0], b[1], b[2], b[3]): + if self.y > b[1] + b[3]: # was prev below border. + new_y = b[1] + b[3] + self.radius + elif self.y < b[1]: # was prev above border. + new_y = b[1] - self.radius + + self.vy *= -1 + break + + self.x = new_x + self.y = new_y + + def respawn(self): + self.is_dead = False + + def update(self, stage): + if self.is_dead: + self.respawn_ticks -= 1 + if self.respawn_ticks == 0: + self.respawn() + elif self.respawn_ticks == 30: + self._set_new_position(stage) + else: + self._do_collisions(stage) + + self.frame_ticks += 1 + if self.frame_ticks == TICKS_PER_FRAME: + self.frame_ticks = 0 + self.frame += 1 + if self.frame == MAX_FRAME: + self.frame = 0 + + def draw(self, shake_x, shake_y): + if self.is_dead: + framex = None + if self.respawn_ticks < 10: + framex = 42 + elif self.respawn_ticks < 20: + framex = 63 + elif self.respawn_ticks < 30: + framex = 84 + if framex is not None: + pyxel.blt( + self.x + shake_x - 10, + self.y + shake_y - 10, + 0, + framex, + 231, + 21, 21, + 8 + ) + else: + pyxel.blt( + self.x + shake_x - 4, + self.y + shake_y - 4, + 0, + 160 + self.frame*9, + 8, + 9, 9, + 8 + ) + + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/stage.py b/ports/megaball/megaball/gamedata/stage.py new file mode 100644 index 000000000..660c2c033 --- /dev/null +++ b/ports/megaball/megaball/gamedata/stage.py @@ -0,0 +1,497 @@ + +import math +import random + +import pyxel + +import player +import utils +import constants +import stage +import light +import input +import game +import palette +import spinner +import globals +import stagedata +import audio + +TILEMAP_SCALE = 8 +# Mokeypatch in get() to simulate old tilemap API +def custom_get_tile(self, x, y): + tile_x = x // TILEMAP_SCALE + tile_y = y // TILEMAP_SCALE + if 0 <= tile_x < self.width and 0 <= tile_y < self.height: + return self.pget(tile_x, tile_y) + return None +pyxel.Tilemap.get = custom_get_tile + +MAX_STAGE_NUM = 15 + +WIDTH_TILES = 18 +HEIGHT_TILES = 15 + +POST_TILE = 37 # utils.get_tile_index(40, 32) + +# tile_index : [angle, collision matrix if triangle] +''' +SLOPE_TILES = { + utils.get_tile_index(56,32): [225, constants.COLLIDE_BOTTOM_RIGHT], # top-left + utils.get_tile_index(64,32): [270, None], # top + utils.get_tile_index(72,32): [315, constants.COLLIDE_BOTTOM_LEFT], # top-right + utils.get_tile_index(56,40): [180, None], # left + utils.get_tile_index(72,40): [0, None], # right + utils.get_tile_index(56,48): [135, constants.COLLIDE_TOP_RIGHT], # bottom-left + utils.get_tile_index(64,48): [90, None], # bottom + utils.get_tile_index(72,48): [45, constants.COLLIDE_TOP_LEFT], # bottom-right + utils.get_tile_index(80,32): [225, constants.COLLIDE_TOP_LEFT], # top-left 2 + utils.get_tile_index(88,32): [135, constants.COLLIDE_BOTTOM_LEFT], # bottom-left 2 + utils.get_tile_index(80,40): [45, constants.COLLIDE_BOTTOM_RIGHT], # bottom-right 2 + utils.get_tile_index(88,40): [315, constants.COLLIDE_TOP_RIGHT] # top-right 2 + #utils.get_tile_index(), +} +''' +SLOPE_TILES = { + 39: [225, constants.COLLIDE_BOTTOM_RIGHT], # top-left + 40: [270, None], # top + 41: [315, constants.COLLIDE_BOTTOM_LEFT], # top-right + 47: [180, None], # left + 49: [0, None], # right + 55: [135, constants.COLLIDE_TOP_RIGHT], # bottom-left + 56: [90, None], # bottom + 57: [45, constants.COLLIDE_TOP_LEFT], # bottom-right + 42: [225, constants.COLLIDE_TOP_LEFT], # top-left 2 + 43: [135, constants.COLLIDE_BOTTOM_LEFT], # bottom-left 2 + 50: [45, constants.COLLIDE_BOTTOM_RIGHT], # bottom-right 2 + 51: [315, constants.COLLIDE_TOP_RIGHT] # top-right 2 + #utils.get_tile_index(), +} + +POCKET_TILE_NW = 50 #utils.get_tile_index(80,40) +POCKET_TILE_NE = 43 #utils.get_tile_index(88,32) +POCKET_TILE_SE = 42 #utils.get_tile_index(80,32) +POCKET_TILE_SW = 51 #utils.get_tile_index(88,40) + +LIGHT_TILE = 20 #utils.get_tile_index(160,0) +BLANK_TILE = 100 #utils.get_tile_index(32,24) + +class PauseMenu: + + SEL_RESUME = 0 + SEL_PALETTE = 1 + SEL_QUIT = 2 + + SELECTIONS = { + SEL_RESUME : [56,55,48,8], + SEL_PALETTE : [52,71,56,8], + SEL_QUIT : [64,87,32,8] + } + + def __init__(self, stage): + self.stage = stage + + self.is_visible = False + + self.sel_index = 0 + + self.quitting = False + + def _pressed_select(self): + if self.sel_index == self.SEL_RESUME: + self.is_visible = False + elif self.sel_index == self.SEL_PALETTE: + self.stage.game.add_fade(5, palette.FADE_LEVEL_6, self.stage.game.cycle_palette) + elif self.sel_index == self.SEL_QUIT: + self.quitting = True + self.stage.quit() + + def _change_selection(self, dir): + self.sel_index += dir + if self.sel_index < 0: + self.sel_index = len(self.SELECTIONS) - 1 + elif self.sel_index >= len(self.SELECTIONS): + self.sel_index = 0 + + def update(self, last_inputs): + if not self.is_visible or self.quitting: + return + + if input.BUTTON_START in last_inputs.pressed: + self.is_visible = False + self.sel_index = 0 + elif input.BUTTON_A in last_inputs.pressed: + self._pressed_select() + elif input.UP in last_inputs.pressed: + self._change_selection(-1) + elif input.DOWN in last_inputs.pressed: + self._change_selection(1) + + def draw(self, shake_x, shake_y): + if not self.is_visible: + return + + pyxel.blt(shake_x + 24, shake_y + 52, 0, 0, 144, 116, 52, 8) # panel bg + + pyxel.blt(shake_x + 56, shake_y + 56, 0, 128, 72, 48, 8, 8) # resume + pyxel.blt(shake_x + 52, shake_y + 72, 0, 104, 80, 56, 8, 8) # palette + pyxel.blt(shake_x + 64, shake_y + 88, 0, 96, 64, 32, 8, 8) # quit + + pyxel.blt( + shake_x + self.SELECTIONS[self.sel_index][0]-12, + shake_y + self.SELECTIONS[self.sel_index][1], + 0, + 16, 33, 9, 9, 8 + ) # selection ball left + pyxel.blt( + shake_x + self.SELECTIONS[self.sel_index][0] + self.SELECTIONS[self.sel_index][2] + 2, + shake_y + self.SELECTIONS[self.sel_index][1], + 0, + 16, 33, 9, 9, 8 + ) # selection ball right + +STATE_INTRO = 0 +STATE_PLAY = 1 +STATE_DIED = 2 +STATE_DEMO = 3 +STATE_GAME_OVER = 4 +STATE_STAGE_COMPLETE = 5 +STATE_GAME_COMPLETE = 6 +STATE_PLAYER_WEAPON = 7 + +MAX_SHOW_GAME_OVER_TICKS = 300 # 5 secs +MAX_SHOW_GAME_COMPLETE_TICKS = 300 # 5 secs + +SPAWN_SECTOR_TOPLEFT = 0 +SPAWN_SECTOR_TOPRIGHT = 1 +SPAWN_SECTOR_BOTTOMLEFT = 2 +SPAWN_SECTOR_BOTTOMRIGHT = 3 + +class Stage: + def __init__(self, game, num): + self.game = game + self.num = num + self.tm = 0 + self.tmu = 0 + self.tmv = num * 16 *TILEMAP_SCALE + + self.state = STATE_INTRO + if self.num <= 0: + self.state = STATE_DEMO + elif self.num == MAX_STAGE_NUM + 1: + self.tm = 1 + self.tmu = 0 + self.tmv = 0 + self.state = STATE_GAME_COMPLETE + + self.solid_rects = [ + [0, 0, 160, 16], # [x, y, w, h] + [0, 16, 8, 128], + [152, 16, 8, 128], + [0, 136, 160, 8] + ] + + self.slopes = [] # [x, y] + self.pockets = [] # [x, y, w, h] + self.lights = [] # Light objects + self.spinners = [] # Spinner objects + + #self.en_spawn_locs_topleft = [] # [[x,y],[x,y],[x,y]...] + #self.en_spawn_locs_topright = [] # [[x,y],[x,y],[x,y]...] + #self.en_spawn_locs_bottomleft = [] # [[x,y],[x,y],[x,y]...] + #self.en_spawn_locs_bottomright = [] # [[x,y],[x,y],[x,y]...] + + # Hardcode spawn locations (becuase they stopped working) + self.en_spawn_locs_topleft = [ + [12, 20], [20, 20], [28, 20], [36, 20], [44, 20], + [12, 28], [20, 28], [28, 28], [36, 28], [44, 28], + [12, 36], [20, 36], [28, 36], [36, 36], [44, 36], + [12, 44], [20, 44], [28, 44], [36, 44], [44, 44] + ] + self.en_spawn_locs_topright = [ + [92, 20], [100, 20], [108, 20], [116, 20], [124, 20], + [92, 28], [100, 28], [108, 28], [116, 28], [124, 28], + [92, 36], [100, 36], [108, 36], [116, 36], [124, 36], + [92, 44], [100, 44], [108, 44], [116, 44], [124, 44] + ] + self.en_spawn_locs_bottomleft = [ + [12, 76], [20, 76], [28, 76], [36, 76], [44, 76], + [12, 84], [20, 84], [28, 84], [36, 84], [44, 84], + [12, 92], [20, 92], [28, 92], [36, 92], [44, 92], + [12, 100], [20, 100], [28, 100], [36, 100], [44, 100] + ] + self.en_spawn_locs_bottomright = [ + [92, 76], [100, 76], [108, 76], [116, 76], [124, 76], + [92, 84], [100, 84], [108, 84], [116, 84], [124, 84], + [92, 92], [100, 92], [108, 92], [116, 92], [124, 92], + [92, 100], [100, 100], [108, 100], [116, 100], [124, 100] + ] + + if self.state != STATE_GAME_COMPLETE: + for yc in range(0, HEIGHT_TILES *TILEMAP_SCALE, TILEMAP_SCALE): + y = self.tmv + yc + for xc in range(0, WIDTH_TILES *TILEMAP_SCALE, TILEMAP_SCALE): + x = self.tmu + xc + tile = pyxel.tilemaps[self.tm].get(x, y) + tile_index = tile[1] * TILEMAP_SCALE + tile[0] + + if tile_index == POST_TILE: #if tile == POST_TILE: + #self.solid_rects.append([xc*8 + 8, yc*8 + 16, 8, 8]) + self.solid_rects.append([xc*8//TILEMAP_SCALE + 8, yc*8//TILEMAP_SCALE + 16, 8, 8]) + elif tile_index in SLOPE_TILES: #if tile == SLOPE_TILES: + #self.slopes.append([xc*8 + 8, yc*8 + 16]) + self.slopes.append([xc*8//TILEMAP_SCALE + 8, yc*8//TILEMAP_SCALE + 16]) + elif tile_index == LIGHT_TILE: #if tile == LIGHT_TILE: + #self.lights.append(light.Light(xc*8 + 8, yc*8 + 16)) + self.lights.append(light.Light(xc*8//TILEMAP_SCALE + 8, yc*8//TILEMAP_SCALE + 16)) + + if tile == POCKET_TILE_NW: + if x < self.tmu + WIDTH_TILES-1 and y < self.tmv + HEIGHT_TILES-1: + if pyxel.tilemaps[self.tm].get(x+1, y) == POCKET_TILE_NE and\ + pyxel.tilemaps[self.tm].get(x+1, y+1) == POCKET_TILE_SE and\ + pyxel.tilemaps[self.tm].get(x, y+1) == POCKET_TILE_SW: + self.pockets.append([xc*8 + 8, yc*8 + 16, 16, 16]) + + if tile != POST_TILE and \ + xc > 0 and \ + xc < WIDTH_TILES-1 and \ + yc > 0 and \ + yc < HEIGHT_TILES-1 and \ + (xc < 5 or xc > WIDTH_TILES-6) and \ + (yc < 5 or yc > HEIGHT_TILES-6): + + loc = [xc*8 + 8 + 4, yc*8 + 16 + 4] + + if xc < 9: + if yc < 7: + self.en_spawn_locs_topleft.append(loc) + else: + self.en_spawn_locs_bottomleft.append(loc) + else: + if yc < 7: + self.en_spawn_locs_topright.append(loc) + else: + self.en_spawn_locs_bottomright.append(loc) + + #print(self.pockets) + num_spinners = 0 + stage_diff_name = stagedata.STAGE_DIFFICULTY[self.num] + for i in range(len(spinner.TYPES)): + en_qty = stagedata.ENEMIES[stage_diff_name][stagedata.SPINNER_KEY][i] + for sq in range(en_qty): + loc = self.get_random_spawn_loc(-1) + self.spinners.append(spinner.Spinner(loc[0], loc[1], i)) + + self.player = player.Player(75,75)#(12, 20) + if self.state == STATE_GAME_COMPLETE: + self.player.state = player.STATE_GAME_COMPLETE + audio.play_music(audio.MUS_IN_GAME, True) + else: + if self.state != STATE_DEMO: + audio.play_music(audio.MUS_START, False) + + self.pause_menu = PauseMenu(self) + + self.stage_over_ticks = 0 + + self.next_stage_flash_num = 0 + + def restart_music(self): + if self.state == STATE_PLAY: + audio.play_music(audio.MUS_IN_GAME) + + def get_random_spawn_loc(self, sector): + if sector == SPAWN_SECTOR_TOPLEFT: + return random.choice(self.en_spawn_locs_topleft) + elif sector == SPAWN_SECTOR_TOPRIGHT: + return random.choice(self.en_spawn_locs_topright) + elif sector == SPAWN_SECTOR_BOTTOMLEFT: + return random.choice(self.en_spawn_locs_bottomleft) + elif sector == SPAWN_SECTOR_BOTTOMRIGHT: + return random.choice(self.en_spawn_locs_bottomright) + else: + ranlist = random.choice([ + self.en_spawn_locs_topleft, + self.en_spawn_locs_topright, + self.en_spawn_locs_bottomleft, + self.en_spawn_locs_bottomright + ]) + return random.choice(ranlist) + + def player_used_weapon(self): + audio.play_sound(audio.SND_USED_WEAPON) + self.state = STATE_PLAYER_WEAPON + + def player_intro_done(self): + if self.state != STATE_PLAYER_WEAPON: + audio.play_music(audio.MUS_IN_GAME, True) + + self.state = STATE_PLAY + + def player_hit(self): + self.state = STATE_DIED + audio.play_music(audio.MUS_DEATH, False) + + def is_complete(self): + for i in self.lights: + if i.is_hit == False: + return False + + audio.play_music(audio.MUS_STAGE_COMPLETE, False) + self._check_next_stage() + + return True + + def player_death_anim_done(self): + if globals.g_lives >= 1: + globals.g_lives -= 1 + self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, + palette.FADE_LEVEL_6, self.game.restart_stage) + else: + self.state = STATE_GAME_OVER + audio.play_music(audio.MUS_GAME_OVER, False) + + def _check_next_stage(self): + #if self.num < MAX_STAGE_NUM: + self.state = STATE_STAGE_COMPLETE + self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, + palette.FADE_LEVEL_0, self.go_to_next_stage) + #else: + # self.state = STATE_GAME_COMPLETE + + def go_to_next_stage(self): + if self.next_stage_flash_num == 0: + self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, + palette.FADE_LEVEL_3, self.go_to_next_stage) + elif self.next_stage_flash_num == 1: + self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, + palette.FADE_LEVEL_0, self.go_to_next_stage) + #elif self.next_stage_flash_num == 2: + # self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, + # palette.FADE_LEVEL_3, self.go_to_next_stage) + #elif self.next_stage_flash_num == 3: + # self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, + # palette.FADE_LEVEL_0, self.go_to_next_stage) + else: + if self.num == stage.MAX_STAGE_NUM: + self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, + palette.FADE_LEVEL_6, self.game.go_to_game_complete_stage) + else: + globals.add_lives(1) + self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, + palette.FADE_LEVEL_6, self.game.go_to_next_stage) + + self.next_stage_flash_num += 1 + + def quit(self): + self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, + palette.FADE_LEVEL_6, self.game.quit_to_main_menu) + + def player_hit_solid(self): + audio.play_sound(audio.SND_HIT_WALL) + self.game.add_screen_shake(5, 1, queue=False) + + # returns None or angle + def get_tile_angle(self, x, y): # x, y is screen pixels + tile = pyxel.tilemaps[self.tm].get( + self.tmu + math.floor((x - 8) / 8 * TILEMAP_SCALE), + self.tmv + math.floor((y - 16) / 8 * TILEMAP_SCALE) + ) + tile_index = tile[1] * TILEMAP_SCALE + tile[0] + #print(tile_index) + + if tile_index in SLOPE_TILES: + # Check if triangle matrix collision is needed + if SLOPE_TILES[tile_index][1] is not None: + t = SLOPE_TILES[tile_index] + + tx = math.floor(abs(x - math.floor(x / 8) * 8 * TILEMAP_SCALE)) + ty = math.floor(abs(y - math.floor(y / 8) * 8 * TILEMAP_SCALE)) + + #print(f"Checking matrix x,y: {tx},{ty} ...") + + if constants.is_colliding_matrix(tx, ty, t[1]): + #print(f"{x}, {y} hit triangle") + #print("... collides.") + return t[0] + else: + #print("... no collision.") + return None + else: + #print(SLOPE_TILES[tile_index][0]) + return SLOPE_TILES[tile_index][0] + else: + return None + + def update(self, last_inputs): + if self.num > 0: # dont allow inputs on demo/main menu stage 0. + if self.pause_menu.is_visible: + self.pause_menu.update(last_inputs) + else: + if input.BUTTON_START in last_inputs.pressed: + if self.state == STATE_PLAY or \ + self.state == STATE_PLAYER_WEAPON: + self.pause_menu.is_visible = True + else: + self.player.update(self, last_inputs) + + if self.state == STATE_PLAY or\ + self.state == STATE_DEMO: + for s in self.spinners: + s.update(self) + + if self.state == STATE_GAME_OVER: + self.stage_over_ticks += 1 + if self.stage_over_ticks == MAX_SHOW_GAME_OVER_TICKS: + self.quit() + elif self.state == STATE_GAME_COMPLETE: + self.stage_over_ticks += 1 + if self.stage_over_ticks >= MAX_SHOW_GAME_COMPLETE_TICKS: + if input.BUTTON_A in last_inputs.pressed: + self.quit() + + if self.state == STATE_PLAY or\ + self.state == STATE_DEMO: + for i in self.lights: + i.update(self) + + def draw(self, shake_x, shake_y): + pyxel.bltm(shake_x + 8, shake_y + 16, self.tm, self.tmu, self.tmv, + WIDTH_TILES *TILEMAP_SCALE, HEIGHT_TILES *TILEMAP_SCALE, 8) + + for i in self.lights: + i.draw(shake_x, shake_y) + + if self.state == STATE_GAME_COMPLETE: + pyxel.blt(24 + shake_x, 32 + shake_y, 0, 136, 136, 112, 88) + + if self.num > 0: + self.player.draw(shake_x, shake_y) + + for s in self.spinners: + s.draw(shake_x, shake_y) + + if self.num > 0: + self.pause_menu.draw(shake_x, shake_y) + + if self.state == STATE_GAME_OVER and self.stage_over_ticks > 30: + pyxel.blt(32 + shake_x, 66 + shake_y, 0, 0, 196, 100, 26, 8) # game over bg + pyxel.blt(44 + shake_x, 72 + shake_y, 0, 40, 80, 32, 8, 8) # "game" + pyxel.blt(84 + shake_x, 72 + shake_y, 0, 72, 80, 32, 8, 8) # "over" + + # DEBUGGING + ''' + for solid_rect in self.solid_rects: + x, y, w, h = solid_rect + pyxel.rectb(shake_x + x, shake_y + y, TILEMAP_SCALE, TILEMAP_SCALE, pyxel.COLOR_BLACK) + + for slope in self.slopes: + x = slope[0] + y = slope[1] + pyxel.rectb(shake_x + x, shake_y + y, TILEMAP_SCALE, TILEMAP_SCALE, pyxel.COLOR_YELLOW) + + for light in self.lights: + x = light.x + y = light.y + pyxel.rectb(shake_x + x, shake_y + y, TILEMAP_SCALE, TILEMAP_SCALE, pyxel.COLOR_RED) + ''' diff --git a/ports/megaball/megaball/gamedata/stagedata.py b/ports/megaball/megaball/gamedata/stagedata.py new file mode 100644 index 000000000..c93848b95 --- /dev/null +++ b/ports/megaball/megaball/gamedata/stagedata.py @@ -0,0 +1,69 @@ + +''' +Each difficulty has a dictionary: +easy : { + ... +} + +With that dictionary is another dictionary of quantity of each object type: + +"spinners" : [personalityA, personalityB, ... etc] +''' + +SPINNER_KEY = "spinners" +DIFF_NONE_KEY = "none" +DIFF_VERY_EASY_KEY = "very easy" +DIFF_EASY_KEY = "easy" +DIFF_MEDIUM_KEY = "medium" +DIFF_HARD_KEY = "hard" +DIFF_VERY_HARD_KEY = "very hard" + +#[aggressive, mildly aggressive, slow random, fast random] + +ENEMIES = { + + DIFF_NONE_KEY : { + SPINNER_KEY : [0,0,0,0] + }, + + DIFF_VERY_EASY_KEY : { + SPINNER_KEY : [2,1,1,0]#[1,1,1,0] + }, + + DIFF_EASY_KEY : { + SPINNER_KEY : [2,1,2,0]#[1,1,2,0] + }, + + DIFF_MEDIUM_KEY : { + SPINNER_KEY : [3,1,1,1]#[2,1,1,1] + }, + + DIFF_HARD_KEY : { + SPINNER_KEY : [3,1,1,2]#[2,1,1,2] + }, + + DIFF_VERY_HARD_KEY : { + SPINNER_KEY : [3,2,1,1]#[2,2,1,1] + } + +} + +STAGE_DIFFICULTY = [ + DIFF_NONE_KEY, # 0 + DIFF_VERY_EASY_KEY, # 1 + DIFF_EASY_KEY, # 2 + DIFF_EASY_KEY, # 3 + DIFF_EASY_KEY, # 4 + DIFF_EASY_KEY, # 5 + DIFF_MEDIUM_KEY, # 6 + DIFF_EASY_KEY, # 7 + DIFF_MEDIUM_KEY, # 8 + DIFF_VERY_EASY_KEY, # 9 + DIFF_VERY_HARD_KEY, # 10 + DIFF_HARD_KEY, # 11 + DIFF_VERY_HARD_KEY, # 12 + DIFF_EASY_KEY, # 13 + DIFF_VERY_HARD_KEY, # 14 + DIFF_VERY_HARD_KEY # 15 +] + diff --git a/ports/megaball/megaball/gamedata/utils.py b/ports/megaball/megaball/gamedata/utils.py new file mode 100644 index 000000000..aa91ba672 --- /dev/null +++ b/ports/megaball/megaball/gamedata/utils.py @@ -0,0 +1,87 @@ + +import math + +import pyxel + +def angle_reflect(incidenceAngle, surfaceAngle): + a = surfaceAngle * 2 - incidenceAngle + return (a + 360) % 360 + +def sign_triangle(p1, p2, p3): + return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]) + +def is_point_in_triangle(px, py, ax, ay, bx, by, cx, cy): + #print("Checking point [{a},{b}] in tri [{c}][{d}], [{e}][{f}], [{g}][{h}] ({i})".format( + # a=px, b=py, c=ax, d=ay, e=bx, f=by, g=cx, h=cy, i=pyxel.frame_count + #)) + d1 = sign_triangle([px, py], [ax,ay], [bx,by]) + d2 = sign_triangle([px, py], [bx,by], [cx,cy]) + d3 = sign_triangle([px, py], [cx,cy], [ax,ay]) + + has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) + has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) + + #print("return: " + str(not(has_neg and has_pos))) + + return not(has_neg and has_pos) + +def circle_rect_overlap(cx, cy, cr, rx, ry, rw, rh): + closestX = cx + closestY = cy + + if cx < rx: + closestX = rx + elif cx > rx + rw: + closestX = rx + rw + + if cy < ry: + closestY = ry + elif cy > ry + rh: + closestY = ry + rh + + closestX = closestX - cx + closestX *= closestX + closestY = closestY - cy + closestY *= closestY + + return closestX + closestY < cr * cr + +def get_angle_deg(x1, y1, x2, y2): + degs = math.degrees(math.atan2(y2 - y1, x2 - x1)) + return (degs + 360) % 360 + +def get_tile_x(index): + return math.floor(index % 32) * 8 + +def get_tile_y(index): + return math.floor(index / 32) * 8 + +def get_tile_index(x, y): + return x/8 + (y / 8) * 32 + +def lerp(v, d): + #print("delta: " + str(d) + ", v: " + str(v[0]) + "," + str(v[1])) + #print() + return (v[0] * (1.0 - d)) + (v[1] * d) + +def ease_out_expo(x): + if x == 1: + return 1 + + return 1 - math.pow(2, -10 * x) + +def ease_out_cubic(x): + return 1 - math.pow(1 - x, 3) + +def draw_number_shadowed(x, y, num, zeropad=0): + strnum = str(num) + if zeropad > 0: + strnum = strnum.zfill(zeropad) + + for i in range(len(strnum)): + pyxel.blt(x + i*8, y, 0, 16 + int(strnum[i])*8, 56, 8, 8, 8) + + + + + \ No newline at end of file diff --git a/ports/megaball/megaball/gamedata/weapon.py b/ports/megaball/megaball/gamedata/weapon.py new file mode 100644 index 000000000..df2f43b36 --- /dev/null +++ b/ports/megaball/megaball/gamedata/weapon.py @@ -0,0 +1,81 @@ + +import math + +import pyxel + +import rect +import constants +import player +import stage +import spinner +import circle +import globals + +MAX_SHOTS = 10 +SHOT_RADIUS = 3 +SHOT_SPEED = 1.5 + +VEL = [] +for i in range(MAX_SHOTS): + VEL.append( + [ + SHOT_SPEED * math.cos(math.radians(i*36)), + SHOT_SPEED * math.sin(math.radians(i*36)), + ] + ) + +class Weapon: + def __init__(self): + self.active = False + + self.shots = [] + for i in range(MAX_SHOTS): + self.shots.append([0,0]) + + def fire(self, from_x, from_y): + self.active = True + for s in self.shots: + s[0] = from_x + s[1] = from_y + + def update(self, player, stage): + if not self.active: + return + + done = True + + for i, s in enumerate(self.shots): + s[0] += VEL[i][0] + s[1] += VEL[i][1] + + if done != False and\ + rect.contains_point(0, 0, + constants.GAME_WIDTH, constants.GAME_HEIGHT, + s[0], s[1]): + done = False + + for spin in stage.spinners: + if not spin.is_dead: + if circle.overlap( + s[0], s[1], SHOT_RADIUS, + spin.x, spin.y, spin.radius): + globals.add_score(globals.SCORE_KILLED_SPINNER) + spin.kill() + + if done: + spinners_killed = sum(s.is_dead == True for s in stage.spinners) + if spinners_killed == len(stage.spinners): + globals.add_score(globals.SCORE_KILLED_ALL_SPINNERS) + self.active = False + player.weapon_done() + + def draw(self, shake_x, shake_y): + if not self.active: + return + + for s in self.shots: + pyxel.blt(shake_x + s[0] - 10, + shake_y + s[1] - 10, + 0, 21, 231, 21, 21, 8) + + \ No newline at end of file diff --git a/ports/megaball/megaball/licenses/LICENSE.megaball.txt b/ports/megaball/megaball/licenses/LICENSE.megaball.txt new file mode 100644 index 000000000..683638cec --- /dev/null +++ b/ports/megaball/megaball/licenses/LICENSE.megaball.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 helpcomputer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ports/megaball/port.json b/ports/megaball/port.json new file mode 100644 index 000000000..4e0f42cd0 --- /dev/null +++ b/ports/megaball/port.json @@ -0,0 +1,28 @@ +{ + "version": 3, + "name": "megaball.zip", + "items": [ + "Megaball.sh", + "megaball" + ], + "items_opt": null, + "attr": { + "title": "Megaball", + "porter": [ + "tabreturn" + ], + "desc": "The goal is to roll the ball over every flashing panel to complete the stage. For each stage you complete, you gain one extra life. If you need to, you can self-destruct (and lose a life) and shatter into ten pieces, killing any enemy which collides with these pieces; you will immediately re-spawn, and the enemy will reappear in 5 seconds.", + "desc_md": null, + "inst": "Ready to run! Thanks helpcomputer (https://helpcomputer.itch.io) for releasing this game under an MIT license.", + "inst_md": "Ready to run! Thanks [helpcomputer](https://helpcomputer.itch.io) for releasing this game under an MIT license.", + "genres": [ + "arcade" + ], + "image": {}, + "rtr": true, + "exp": false, + "runtime": "pyxel_2.2.8_python_3.11.squashfs", + "reqs": [], + "arch": [] + } +} diff --git a/ports/megaball/screenshot.png b/ports/megaball/screenshot.png new file mode 100644 index 000000000..ef0175dda Binary files /dev/null and b/ports/megaball/screenshot.png differ