├── .gitignore ├── README.md ├── images └── megaball-prev00.gif └── megaball ├── assets ├── img0.png └── my_resource.pyxres ├── audio.py ├── circle.py ├── constants.py ├── game.py ├── globals.py ├── hud.py ├── icon.ico ├── input.py ├── light.py ├── main.py ├── mainmenu.py ├── palette.py ├── player.py ├── readme.txt ├── rect.py ├── screenshake.py ├── spinner.py ├── stage.py ├── stagedata.py ├── utils.py └── weapon.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | wheels/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | MANIFEST 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Compiled source # 32 | ################### 33 | *.com 34 | *.class 35 | *.dll 36 | *.exe 37 | *.o 38 | *.so 39 | 40 | # Packages # 41 | ############ 42 | # it's better to unpack these files and commit the raw source 43 | # git has its own built in compression methods 44 | *.7z 45 | *.dmg 46 | *.gz 47 | *.iso 48 | *.jar 49 | *.rar 50 | *.tar 51 | *.zip 52 | 53 | # Logs and databases # 54 | ###################### 55 | *.log 56 | *.sql 57 | *.sqlite 58 | 59 | # OS generated files # 60 | ###################### 61 | .DS_Store 62 | .DS_Store? 63 | ._* 64 | .Spotlight-V100 65 | .Trashes 66 | ehthumbs.db 67 | Thumbs.db 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Megaball 2 | 3 | ## Introduction 4 | Megaball is a ball physics game written in Python using the Pyxel game engine. It was made for [Game Boy Jam 8](https://itch.io/jam/gbjam-8). It is insprired by the Taito classic arcade game "Xyzolog." 5 | 6 | You can see the jam entry and download a Windows binary [here](https://badcomputer0.itch.io/megaball). 7 | 8 | ![](/images/megaball-prev00.gif?raw=true "") 9 | 10 | ## Dependencies 11 | - [Python](https://www.python.org/) 3.6.8 or higher. 12 | - [Pyxel](https://github.com/kitao/pyxel) 1.4.2. 13 | 14 | ## Build & Run 15 | - Inside the "megaball" directory, run "python main.py" 16 | 17 | ## Controls 18 | - WASD, Arrow keys, or gamepad D-pad to move. 19 | - Z key, K key, or gamepad Button A to fire weapon, or confirm in menu. 20 | - Enter key or gamepad Start button to Start or Pause. 21 | - F1 key to toggle sound. 22 | - F2 key to toggle music. 23 | 24 | ## Credits 25 | - Game design and art by [badcomputer](https://twitter.com/badcomputer0) 26 | - Sound and music by [Mike Richmond](https://twitter.com/richmondmike) 27 | - Font by [Damien Guard](https://twitter.com/damienguard) 28 | 29 | ## License 30 | [MIT license](http://en.wikipedia.org/wiki/MIT_License) 31 | -------------------------------------------------------------------------------- /images/megaball-prev00.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpcomputer/megaball/6b15e1d378f5aa8c2a94b892404d569d64fb72f0/images/megaball-prev00.gif -------------------------------------------------------------------------------- /megaball/assets/img0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpcomputer/megaball/6b15e1d378f5aa8c2a94b892404d569d64fb72f0/megaball/assets/img0.png -------------------------------------------------------------------------------- /megaball/assets/my_resource.pyxres: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpcomputer/megaball/6b15e1d378f5aa8c2a94b892404d569d64fb72f0/megaball/assets/my_resource.pyxres -------------------------------------------------------------------------------- /megaball/audio.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | import globals 5 | 6 | MUS_IN_GAME = 0 7 | MUS_TITLE = 1 8 | MUS_START = 2 9 | MUS_STAGE_COMPLETE = 3 10 | MUS_DEATH = 4 11 | MUS_GAME_OVER = 5 12 | 13 | MUSIC = [ 14 | MUS_IN_GAME, 15 | MUS_TITLE, 16 | MUS_START, 17 | MUS_STAGE_COMPLETE, 18 | MUS_DEATH, 19 | #MUS_GAME_OVER 20 | ] 21 | 22 | SND_MENU_MOVE = 16 23 | SND_MENU_SELECT = 17 24 | SND_HIT_WALL = 18 25 | SND_HIT_TARGET = 19 26 | SND_USED_WEAPON = 20 27 | 28 | SOUNDS = [ 29 | SND_MENU_MOVE, 30 | SND_MENU_SELECT, 31 | SND_HIT_WALL, 32 | SND_HIT_TARGET, 33 | SND_USED_WEAPON, 34 | ] 35 | 36 | def play_sound(snd, looping=False): 37 | if globals.g_sound_on == False: 38 | return 39 | 40 | if snd not in SOUNDS: 41 | return 42 | 43 | if pyxel.play_pos(3) != -1: 44 | return 45 | 46 | pyxel.play(3, snd, loop=looping) 47 | 48 | def play_music(msc, looping=False): 49 | if globals.g_music_on == False: 50 | return 51 | 52 | if msc not in MUSIC: 53 | return 54 | 55 | pyxel.stop() 56 | 57 | pyxel.playm(msc, loop=looping) 58 | -------------------------------------------------------------------------------- /megaball/circle.py: -------------------------------------------------------------------------------- 1 | 2 | def overlap(x1, y1, r1, x2, y2, r2): 3 | dx = x1 - x2 4 | dy = y1 - y2 5 | dist = dx * dx + dy * dy 6 | radiusSum = r1 + r2 7 | return dist < radiusSum * radiusSum 8 | 9 | def contains_other(x1, y1, r1, x2, y2, r2): 10 | radiusDiff = r1 - r2 11 | if radiusDiff < 0: 12 | return False 13 | 14 | dx = x1 - x2 15 | dy = y1 - y2 16 | dist = dx * dx + dy * dy 17 | radiusSum = r1 + r2 18 | return (not(radiusDiff * radiusDiff < dist) and (dist < radiusSum * radiusSum)) 19 | 20 | def contains_point(x, y, radius, px, py): 21 | dx = x - px 22 | dy = y - py 23 | return dx * dx + dy * dy <= radius * radius 24 | 25 | class Circle: 26 | def __init__(self, x, y, radius): 27 | self.x = x 28 | self.y = y 29 | self.radius = radius 30 | -------------------------------------------------------------------------------- /megaball/constants.py: -------------------------------------------------------------------------------- 1 | 2 | GAME_TITLE = "MEGABALL" 3 | GAME_WIDTH = 160 4 | GAME_HEIGHT = 144 5 | GAME_FPS = 60 6 | GAME_SCALE = 4 7 | 8 | IMAGE_BANK_0_FILE = "assets/img0.png" 9 | RESOURCE_FILE = "assets/my_resource.pyxres" 10 | 11 | COLLIDE_TOP_LEFT = [ 12 | [1, 1, 1, 1, 1, 1, 1, 1], 13 | [1, 1, 1, 1, 1, 1, 1, 0], 14 | [1, 1, 1, 1, 1, 1, 0, 0], 15 | [1, 1, 1, 1, 1, 0, 0, 0], 16 | [1, 1, 1, 1, 0, 0, 0, 0], 17 | [1, 1, 1, 0, 0, 0, 0, 0], 18 | [1, 1, 0, 0, 0, 0, 0, 0], 19 | [1, 0, 0, 0, 0, 0, 0, 0], 20 | ] 21 | 22 | COLLIDE_TOP_RIGHT = [ 23 | [1, 1, 1, 1, 1, 1, 1, 1], 24 | [0, 1, 1, 1, 1, 1, 1, 1], 25 | [0, 0, 1, 1, 1, 1, 1, 1], 26 | [0, 0, 0, 1, 1, 1, 1, 1], 27 | [0, 0, 0, 0, 1, 1, 1, 1], 28 | [0, 0, 0, 0, 0, 1, 1, 1], 29 | [0, 0, 0, 0, 0, 0, 1, 1], 30 | [0, 0, 0, 0, 0, 0, 0, 1], 31 | ] 32 | 33 | COLLIDE_BOTTOM_RIGHT = [ 34 | [0, 0, 0, 0, 0, 0, 0, 1], 35 | [0, 0, 0, 0, 0, 0, 1, 1], 36 | [0, 0, 0, 0, 0, 1, 1, 1], 37 | [0, 0, 0, 0, 1, 1, 1, 1], 38 | [0, 0, 0, 1, 1, 1, 1, 1], 39 | [0, 0, 1, 1, 1, 1, 1, 1], 40 | [0, 1, 1, 1, 1, 1, 1, 1], 41 | [1, 1, 1, 1, 1, 1, 1, 1], 42 | ] 43 | 44 | COLLIDE_BOTTOM_LEFT = [ 45 | [1, 0, 0, 0, 0, 0, 0, 0], 46 | [1, 1, 0, 0, 0, 0, 0, 0], 47 | [1, 1, 1, 0, 0, 0, 0, 0], 48 | [1, 1, 1, 1, 0, 0, 0, 0], 49 | [1, 1, 1, 1, 1, 0, 0, 0], 50 | [1, 1, 1, 1, 1, 1, 0, 0], 51 | [1, 1, 1, 1, 1, 1, 1, 0], 52 | [1, 1, 1, 1, 1, 1, 1, 1], 53 | ] 54 | 55 | COLLIDE_MATRIX_ALL = [ 56 | COLLIDE_TOP_LEFT, 57 | COLLIDE_TOP_RIGHT, 58 | COLLIDE_BOTTOM_RIGHT, 59 | COLLIDE_BOTTOM_LEFT 60 | ] 61 | 62 | def is_colliding_matrix(x, y, matrix): 63 | if matrix not in COLLIDE_MATRIX_ALL: 64 | return False 65 | 66 | if x < 0 or x > 7 or y < 0 or y > 7: 67 | return False 68 | 69 | return matrix[y][x] 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /megaball/game.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | import constants 5 | import palette 6 | import hud 7 | import input 8 | import stage 9 | import screenshake 10 | import mainmenu 11 | import globals 12 | import audio 13 | 14 | class Game: 15 | def __init__(self): 16 | self.pal_control = palette.PaletteControl() 17 | 18 | self.screen_shake = screenshake.ScreenShake(self) 19 | 20 | self.main_menu = mainmenu.MainMenu(self) 21 | self.stage = stage.Stage(self, 0) 22 | self.hud = hud.Hud(self) 23 | 24 | self.pal_index = 0 25 | 26 | audio.play_music(audio.MUS_TITLE, True) 27 | 28 | def restart_music(self): 29 | if self.main_menu.is_visible: 30 | audio.play_music(audio.MUS_TITLE) 31 | else: 32 | self.stage.restart_music() 33 | 34 | def quit_to_main_menu(self): 35 | del self.stage 36 | self.stage = stage.Stage(self, 0) 37 | globals.set_high_score() 38 | globals.reset() 39 | self.main_menu.reset() 40 | self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) 41 | 42 | def go_to_next_stage(self): 43 | globals.g_stage_num += 1 44 | del self.stage 45 | self.stage = stage.Stage(self, globals.g_stage_num) 46 | self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) 47 | 48 | def go_to_game_complete_stage(self): 49 | del self.stage 50 | self.stage = stage.Stage(self, stage.MAX_STAGE_NUM + 1) 51 | self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) 52 | 53 | def restart_stage(self): 54 | del self.stage 55 | self.stage = stage.Stage(self, globals.g_stage_num) 56 | self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) 57 | 58 | def start_game(self): 59 | self.main_menu.hide() 60 | del self.stage 61 | self.stage = stage.Stage(self, globals.g_stage_num) 62 | self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) 63 | 64 | def add_screen_shake(self, ticks, magnitude, queue=False): 65 | self.screen_shake.add_event(ticks, magnitude, queue) 66 | 67 | def cycle_palette(self): 68 | self.pal_index += 1 69 | if self.pal_index == len(palette.ALL): 70 | self.pal_index = 0 71 | self.pal_control.add_palette_event(1, palette.ALL[self.pal_index]) 72 | self.add_fade(palette.FADE_STEP_TICKS_DEFAULT, palette.FADE_LEVEL_3) 73 | 74 | def add_fade(self, ticks_per_level, target_level, callback=None): 75 | self.pal_control.add_fade_event(ticks_per_level, target_level, callback) 76 | 77 | def update(self, last_inputs): 78 | if pyxel.btnp(pyxel.KEY_F1): 79 | globals.toggle_sound() 80 | 81 | if pyxel.btnp(pyxel.KEY_F2): 82 | globals.toggle_music(self) 83 | 84 | self.main_menu.update(last_inputs) 85 | 86 | self.stage.update(last_inputs) 87 | 88 | self.pal_control.update() 89 | self.screen_shake.update() 90 | 91 | def draw(self): 92 | for c in range(palette.NUM_COLOURS): 93 | pyxel.pal(palette.DEFAULT[c], self.pal_control.get_col(c)) 94 | 95 | pyxel.cls(self.pal_control.get_col(0)) 96 | 97 | self.stage.draw(self.screen_shake.x, self.screen_shake.y) 98 | self.hud.draw(self.screen_shake.x, self.screen_shake.y) 99 | 100 | self.main_menu.draw(self.screen_shake.x, self.screen_shake.y) 101 | 102 | pyxel.pal() 103 | -------------------------------------------------------------------------------- /megaball/globals.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | import constants 5 | import game 6 | 7 | STARTING_LIVES = 2 8 | MAX_SCORE = 999999 9 | MAX_LIVES = 99 10 | 11 | SCORE_HIT_LIGHT = 200 12 | SCORE_STAGE_COMPLETE = 1000 13 | SCORE_USE_WEAPON = 4000 14 | SCORE_KILLED_SPINNER = 200 15 | SCORE_KILLED_ALL_SPINNERS = 10000 16 | 17 | g_lives = STARTING_LIVES 18 | g_score = 0 19 | g_highscore = 0 20 | g_stage_num = 1 21 | g_sound_on = True 22 | g_music_on = True 23 | 24 | def reset(): 25 | global g_lives 26 | global g_score 27 | global g_stage_num 28 | 29 | g_lives = STARTING_LIVES 30 | g_score = 0 31 | g_stage_num = 1 32 | 33 | def toggle_sound(): 34 | global g_sound_on 35 | 36 | g_sound_on = not g_sound_on 37 | 38 | if g_sound_on == False: 39 | pyxel.stop() 40 | 41 | def toggle_music(game_obj): 42 | global g_music_on 43 | 44 | g_music_on = not g_music_on 45 | 46 | if g_music_on == False: 47 | pyxel.stop() 48 | else: 49 | game_obj.restart_music() 50 | 51 | def set_high_score(): 52 | global g_score 53 | global g_highscore 54 | 55 | g_highscore = max(g_score, g_highscore) 56 | 57 | def add_lives(amt): 58 | global g_lives 59 | 60 | g_lives = max(0, min(g_lives + amt, MAX_LIVES)) 61 | 62 | def add_score(amt): 63 | global g_score 64 | 65 | g_score = max(0, min(g_score + amt, MAX_SCORE)) 66 | -------------------------------------------------------------------------------- /megaball/hud.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | import game 5 | import constants 6 | import globals 7 | import utils 8 | import stage 9 | 10 | class Hud: 11 | def __init__(self, game): 12 | self.game = game 13 | 14 | def update(self): 15 | pass 16 | 17 | def draw(self, shake_x, shake_y): 18 | # top bar 19 | pyxel.blt(shake_x + 0, shake_y + 0, 0, 0, 0, constants.GAME_WIDTH, 16) 20 | # bottom bar 21 | pyxel.blt(shake_x + 0, shake_y + 136, 0, 0, 16, constants.GAME_WIDTH, 8) 22 | # left bar 23 | pyxel.blt(shake_x + 0, shake_y + 16, 0, 0, 24, 8, 120) 24 | # right bar 25 | pyxel.blt(shake_x + 152, shake_y + 16, 0, 8, 24, 8, 120) 26 | 27 | utils.draw_number_shadowed(shake_x + 31, shake_y + 5, globals.g_lives, zeropad=2) 28 | utils.draw_number_shadowed(shake_x + 57, shake_y + 5, globals.g_score, zeropad=6) 29 | utils.draw_number_shadowed(shake_x + 113, shake_y + 5, globals.g_stage_num, zeropad=2) 30 | utils.draw_number_shadowed(shake_x + 137, shake_y + 5, stage.MAX_STAGE_NUM, zeropad=2) 31 | -------------------------------------------------------------------------------- /megaball/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helpcomputer/megaball/6b15e1d378f5aa8c2a94b892404d569d64fb72f0/megaball/icon.ico -------------------------------------------------------------------------------- /megaball/input.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | UP = 0 5 | DOWN = 1 6 | LEFT = 2 7 | RIGHT = 3 8 | BUTTON_A = 4 9 | BUTTON_B = 5 10 | BUTTON_START = 6 11 | BUTTON_SELECT = 7 12 | 13 | class Input: 14 | 15 | def __init__(self): 16 | self.pressing = [] 17 | self.pressed = [] 18 | 19 | def get(self): 20 | self.pressing.clear() 21 | self.pressed.clear() 22 | 23 | # pressing 24 | if pyxel.btn(pyxel.KEY_UP) or pyxel.btn(pyxel.KEY_W) or \ 25 | pyxel.btn(pyxel.GAMEPAD_1_UP): 26 | self.pressing.append(UP) 27 | elif pyxel.btn(pyxel.KEY_DOWN) or pyxel.btn(pyxel.KEY_S) or \ 28 | pyxel.btn(pyxel.GAMEPAD_1_DOWN): 29 | self.pressing.append(DOWN) 30 | 31 | if pyxel.btn(pyxel.KEY_LEFT) or pyxel.btn(pyxel.KEY_A) or \ 32 | pyxel.btn(pyxel.GAMEPAD_1_LEFT): 33 | self.pressing.append(LEFT) 34 | elif pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.KEY_D) or \ 35 | pyxel.btn(pyxel.GAMEPAD_1_RIGHT): 36 | self.pressing.append(RIGHT) 37 | 38 | if pyxel.btn(pyxel.KEY_Z) or pyxel.btn(pyxel.KEY_K) or \ 39 | pyxel.btn(pyxel.GAMEPAD_1_A): 40 | self.pressing.append(BUTTON_A) 41 | 42 | if pyxel.btn(pyxel.KEY_X) or pyxel.btn(pyxel.KEY_L) or \ 43 | pyxel.btn(pyxel.GAMEPAD_1_B): 44 | self.pressing.append(BUTTON_B) 45 | 46 | if pyxel.btn(pyxel.KEY_ENTER) or \ 47 | pyxel.btn(pyxel.GAMEPAD_1_START): 48 | self.pressing.append(BUTTON_START) 49 | 50 | if pyxel.btn(pyxel.KEY_SPACE) or \ 51 | pyxel.btn(pyxel.GAMEPAD_1_SELECT): 52 | self.pressing.append(BUTTON_SELECT) 53 | 54 | # pressed 55 | if pyxel.btnp(pyxel.KEY_UP) or pyxel.btnp(pyxel.KEY_W) or \ 56 | pyxel.btnp(pyxel.GAMEPAD_1_UP): 57 | self.pressed.append(UP) 58 | elif pyxel.btnp(pyxel.KEY_DOWN) or pyxel.btnp(pyxel.KEY_S) or \ 59 | pyxel.btnp(pyxel.GAMEPAD_1_DOWN): 60 | self.pressed.append(DOWN) 61 | 62 | if pyxel.btnp(pyxel.KEY_LEFT) or pyxel.btnp(pyxel.KEY_A) or \ 63 | pyxel.btnp(pyxel.GAMEPAD_1_LEFT): 64 | self.pressed.append(LEFT) 65 | elif pyxel.btnp(pyxel.KEY_RIGHT) or pyxel.btnp(pyxel.KEY_D) or \ 66 | pyxel.btnp(pyxel.GAMEPAD_1_RIGHT): 67 | self.pressed.append(RIGHT) 68 | 69 | if pyxel.btnp(pyxel.KEY_Z) or pyxel.btnp(pyxel.KEY_K) or \ 70 | pyxel.btnp(pyxel.GAMEPAD_1_A): 71 | self.pressed.append(BUTTON_A) 72 | 73 | if pyxel.btnp(pyxel.KEY_X) or pyxel.btnp(pyxel.KEY_L) or \ 74 | pyxel.btnp(pyxel.GAMEPAD_1_B): 75 | self.pressed.append(BUTTON_B) 76 | 77 | if pyxel.btnp(pyxel.KEY_ENTER) or \ 78 | pyxel.btnp(pyxel.GAMEPAD_1_START): 79 | self.pressed.append(BUTTON_START) 80 | 81 | if pyxel.btnp(pyxel.KEY_SPACE) or \ 82 | pyxel.btnp(pyxel.GAMEPAD_1_SELECT): 83 | self.pressed.append(BUTTON_SELECT) 84 | 85 | -------------------------------------------------------------------------------- /megaball/light.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | import globals 5 | 6 | TICKS_PER_FRAME = 10 7 | MAX_FRAMES = 5 8 | 9 | class Light: 10 | def __init__(self, x, y): 11 | self.x = x 12 | self.y = y 13 | 14 | self.frame = 0 15 | self.frame_ticks = 0 16 | self.anim_dir = 1 17 | 18 | self.is_hit = False 19 | 20 | def got_hit(self): 21 | if self.is_hit == False: 22 | self.frame = 4 23 | self.is_hit = True 24 | globals.add_score(globals.SCORE_HIT_LIGHT) 25 | return True 26 | return False 27 | 28 | def update(self, stage): 29 | if not self.is_hit: 30 | self.frame_ticks += 1 31 | 32 | if self.frame_ticks == TICKS_PER_FRAME: 33 | self.frame_ticks = 0 34 | self.frame += self.anim_dir 35 | 36 | if self.frame == 0 or self.frame == MAX_FRAMES - 1: 37 | self.anim_dir *= -1 38 | 39 | def draw(self, shake_x, shake_y): 40 | pyxel.blt(shake_x + self.x, shake_y + self.y, 0, 160 + self.frame*8, 0, 8, 8) 41 | -------------------------------------------------------------------------------- /megaball/main.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | import constants 5 | import input 6 | import game 7 | 8 | class App: 9 | def __init__(self): 10 | pyxel.init( 11 | constants.GAME_WIDTH, 12 | constants.GAME_HEIGHT, 13 | caption=constants.GAME_TITLE, 14 | fps=constants.GAME_FPS, 15 | scale=constants.GAME_SCALE 16 | ) 17 | 18 | pyxel.load(constants.RESOURCE_FILE) 19 | pyxel.image(0).load(0, 0, constants.IMAGE_BANK_0_FILE) 20 | 21 | self.input = input.Input() 22 | self.game = game.Game() 23 | pyxel.mouse(False) 24 | #pyxel.mouse(True) 25 | 26 | pyxel.run(self.update, self.draw) 27 | 28 | def update(self): 29 | self.input.get() 30 | self.game.update(self.input) 31 | 32 | def draw(self): 33 | self.game.draw() 34 | 35 | App() 36 | -------------------------------------------------------------------------------- /megaball/mainmenu.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | import utils 5 | import globals 6 | import game 7 | import input 8 | import palette 9 | import audio 10 | 11 | SEL_START_GAME = 0 12 | SEL_PALETTE = 1 13 | SEL_EXIT_GAME = 2 14 | 15 | SELECTIONS = { 16 | SEL_START_GAME : [40,87,80,8], # [x, y, w, h] 17 | SEL_PALETTE : [52,103,56,8], 18 | SEL_EXIT_GAME : [44,119,72,8] 19 | } 20 | 21 | class MainMenu: 22 | def __init__(self, game): 23 | self.game = game 24 | 25 | self.is_visible = True 26 | 27 | self.show_press_start = True 28 | self.press_start_flash_ticks = 0 29 | self.sel_index = 0 30 | 31 | def hide(self): 32 | self.is_visible = False 33 | 34 | def reset(self): 35 | self.is_visible = True 36 | self.show_press_start = True 37 | self.press_start_flash_ticks = 0 38 | self.sel_index = 0 39 | audio.play_music(audio.MUS_TITLE, True) 40 | 41 | def _pressed_select(self): 42 | audio.play_sound(audio.SND_MENU_SELECT) 43 | if self.sel_index == SEL_START_GAME: 44 | self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, 45 | palette.FADE_LEVEL_6, self.game.start_game) 46 | elif self.sel_index == SEL_PALETTE: 47 | self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, 48 | palette.FADE_LEVEL_6, self.game.cycle_palette) 49 | elif self.sel_index == SEL_EXIT_GAME: 50 | self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, 51 | palette.FADE_LEVEL_0, pyxel.quit) 52 | 53 | def _change_selection(self, dir): 54 | audio.play_sound(audio.SND_MENU_MOVE) 55 | self.sel_index += dir 56 | if self.sel_index < 0: 57 | self.sel_index = len(SELECTIONS) - 1 58 | elif self.sel_index >= len(SELECTIONS): 59 | self.sel_index = 0 60 | 61 | def update(self, last_inputs): 62 | if not self.is_visible: 63 | return 64 | 65 | if self.show_press_start: 66 | self.press_start_flash_ticks += 1 67 | if self.press_start_flash_ticks == 50: 68 | self.press_start_flash_ticks = 0 69 | if input.BUTTON_START in last_inputs.pressed: 70 | self.show_press_start = False 71 | self.sel_index = 0 72 | else: 73 | if input.BUTTON_A in last_inputs.pressed: 74 | self._pressed_select() 75 | elif input.UP in last_inputs.pressed: 76 | self._change_selection(-1) 77 | elif input.DOWN in last_inputs.pressed: 78 | self._change_selection(1) 79 | 80 | def draw(self, shake_x, shake_y): 81 | if not self.is_visible: 82 | return 83 | 84 | if self.show_press_start: 85 | if self.press_start_flash_ticks < 30: 86 | pyxel.blt(shake_x + 36, shake_y + 104, 0, 16, 72, 40, 8, 8) # press 87 | pyxel.blt(shake_x + 84, shake_y + 104, 0, 56, 72, 40, 8, 8) # start 88 | else: 89 | pyxel.blt(shake_x + 24, shake_y + 84, 0, 0, 144, 116, 52, 8) # panel bg 90 | 91 | pyxel.blt(shake_x + 40, shake_y + 88, 0, 56, 72, 40, 8, 8) # start 92 | pyxel.blt(shake_x + 88, shake_y + 88, 0, 40, 80, 32, 8, 8) # game 93 | 94 | pyxel.blt(shake_x + 52, shake_y + 104, 0, 104, 80, 56, 8, 8) # palette 95 | 96 | pyxel.blt(shake_x + 44, shake_y + 120, 0, 96, 72, 32, 8, 8) # exit 97 | pyxel.blt(shake_x + 84, shake_y + 120, 0, 40, 80, 32, 8, 8) # game 98 | 99 | pyxel.blt( 100 | shake_x + SELECTIONS[self.sel_index][0]-12, 101 | shake_y + SELECTIONS[self.sel_index][1], 102 | 0, 103 | 16, 33, 9, 9, 8 104 | ) # selection ball left 105 | pyxel.blt( 106 | shake_x + SELECTIONS[self.sel_index][0] + SELECTIONS[self.sel_index][2] + 2, 107 | shake_y + SELECTIONS[self.sel_index][1], 108 | 0, 109 | 16, 33, 9, 9, 8 110 | ) # selection ball right 111 | 112 | pyxel.blt(shake_x + 44, shake_y + 20, 0, 16, 80, 24, 8, 8) # hi- 113 | utils.draw_number_shadowed(shake_x + 68, shake_y + 20, 114 | globals.g_highscore, zeropad=6) # highscore number 115 | pyxel.blt(shake_x + 13, shake_y + 36, 0, 16, 88, 135, 44, 8) # logo 116 | 117 | 118 | -------------------------------------------------------------------------------- /megaball/palette.py: -------------------------------------------------------------------------------- 1 | 2 | import pyxel 3 | 4 | NUM_COLOURS = 4 5 | 6 | DEFAULT = [ pyxel.COLOR_NAVY, pyxel.COLOR_GREEN, pyxel.COLOR_LIME, pyxel.COLOR_WHITE ] 7 | RED = [ pyxel.COLOR_PURPLE, pyxel.COLOR_RED, pyxel.COLOR_PINK, pyxel.COLOR_WHITE ] 8 | BLUE = [ pyxel.COLOR_NAVY, pyxel.COLOR_DARKBLUE, pyxel.COLOR_CYAN, pyxel.COLOR_WHITE ] 9 | BROWN = [ pyxel.COLOR_BROWN, pyxel.COLOR_ORANGE, pyxel.COLOR_PEACH, pyxel.COLOR_WHITE ] 10 | GREY = [ pyxel.COLOR_BLACK, pyxel.COLOR_DARKBLUE, pyxel.COLOR_GRAY, pyxel.COLOR_WHITE ] 11 | 12 | ALL = [ 13 | DEFAULT, 14 | RED, 15 | BLUE, 16 | BROWN, 17 | GREY 18 | ] 19 | 20 | FADE_LEVEL_0 = -3 # all colours to darkest colour. 21 | FADE_LEVEL_1 = -2 # all but brightest to darkest colour. 22 | FADE_LEVEL_2 = -1 # all but two brightest to darkest colour. 23 | FADE_LEVEL_3 = 0 # no modification 24 | FADE_LEVEL_4 = 1 # all but two darkest to brightest colour. 25 | FADE_LEVEL_5 = 2 # all but darkest to brightest colour. 26 | FADE_LEVEL_6 = 3 # all colours to brightest colour. 27 | 28 | FADE_LEVELS = [ 29 | FADE_LEVEL_0, 30 | FADE_LEVEL_1, 31 | FADE_LEVEL_2, 32 | FADE_LEVEL_3, 33 | FADE_LEVEL_4, 34 | FADE_LEVEL_5, 35 | FADE_LEVEL_6 36 | ] 37 | 38 | FADE_STEP_TICKS_DEFAULT = 5 39 | FADE_STEP_TICKS_SLOW = 10 40 | 41 | class FadeEvent: 42 | def __init__(self, ticks_per_level, new_level, callback=None): 43 | self.ticks_per_level = ticks_per_level 44 | self.ticks = 0 45 | self.new_level = new_level 46 | self.callback = callback 47 | 48 | class FadeControl: 49 | def __init__(self): 50 | self.current_level = FADE_LEVEL_3 51 | 52 | self.events = [] 53 | 54 | def add_event(self, ticks_per_level, new_level, callback=None): 55 | if ticks_per_level <= 0 or new_level not in FADE_LEVELS: 56 | return 57 | 58 | self.events.append(FadeEvent(ticks_per_level, new_level, callback)) 59 | 60 | def get_level(self): 61 | return self.current_level 62 | 63 | def update(self): 64 | if len(self.events) > 0: 65 | e = self.events[0] 66 | e.ticks += 1 67 | 68 | if e.ticks == e.ticks_per_level: 69 | e.ticks = 0 70 | if self.current_level < e.new_level: 71 | self.current_level += 1 72 | elif self.current_level > e.new_level: 73 | self.current_level -= 1 74 | 75 | if self.current_level == e.new_level: 76 | if e.callback is not None: 77 | e.callback() 78 | self.events.pop(0) 79 | 80 | class PaletteEvent: 81 | def __init__(self, ticks, new_pal, callback=None): 82 | self.ticks = ticks 83 | self.new_pal = DEFAULT 84 | self.callback = callback 85 | if new_pal in ALL: 86 | self.new_pal = new_pal 87 | 88 | class PaletteControl: 89 | def __init__(self): 90 | self.current_palette = DEFAULT 91 | 92 | self.events = [] 93 | 94 | self.fade_control = FadeControl() 95 | 96 | def add_fade_event(self, ticks_per_level, new_level, callback=None): 97 | self.fade_control.add_event(ticks_per_level, new_level, callback) 98 | 99 | def add_palette_event(self, ticks, new_pal, callback=None): 100 | if ticks <= 0 or new_pal not in ALL: 101 | return 102 | 103 | #print("added palette event") 104 | self.events.append(PaletteEvent(ticks, new_pal, callback)) 105 | 106 | def update(self): 107 | self.fade_control.update() 108 | 109 | if len(self.events) > 0: 110 | e = self.events[0] 111 | e.ticks -= 1 112 | if e.ticks == 0: 113 | self.current_palette = e.new_pal 114 | if e.callback is not None: 115 | e.callback() 116 | self.events.pop(0) 117 | 118 | #print("Removed pal event, queue size now: " + str(len(self.events))) 119 | 120 | def set_pal(self, pal): 121 | if pal in ALL: 122 | self.current_palette = pal 123 | 124 | def get_pal(self): 125 | return self.current_palette 126 | 127 | def get_col(self, index): 128 | if index < 0 or index >= NUM_COLOURS: 129 | return self.current_palette[0] 130 | 131 | index = max(0, min(NUM_COLOURS-1, index + self.fade_control.get_level())) 132 | 133 | return self.current_palette[index] 134 | 135 | -------------------------------------------------------------------------------- /megaball/player.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | import pyxel 5 | 6 | import utils 7 | import input 8 | import rect 9 | import circle 10 | import light 11 | import weapon 12 | import globals 13 | import audio 14 | 15 | MAX_SPEED = 1.2 16 | DECEL = 0.01 17 | ACCEL = 0.06 18 | SLOPE_ACCEL = 0.10 19 | 20 | HIT_SOLID_DAMP = 0.7 21 | 22 | INTRO_TICKS_PER_FRAME = 10 23 | DEAD_TICKS_PER_FRAME = 10 24 | 25 | STATE_INTRO = 0 26 | STATE_PLAY = 1 27 | STATE_DEAD = 2 28 | STATE_STAGE_COMPLETE = 3 29 | STATE_GAME_COMPLETE = 4 30 | STATE_WEAPON = 5 31 | 32 | class Player: 33 | def __init__(self, x, y): 34 | self.x = x 35 | self.y = y 36 | 37 | self.vx = 0 38 | self.vy = 0 39 | 40 | self.radius = 4 41 | 42 | self.state = STATE_INTRO 43 | 44 | self.intro_frame = 4 45 | self.dead_frame = 0 46 | 47 | self.anim_ticks = 0 48 | 49 | self.weapon = weapon.Weapon() 50 | 51 | def _do_solid_collisions(self, stage): 52 | new_x = self.x + self.vx 53 | 54 | for b in stage.solid_rects: 55 | if utils.circle_rect_overlap(new_x, self.y, self.radius, 56 | b[0], b[1], b[2], b[3]): 57 | if self.x > b[0] + b[2]: # was prev to right of border. 58 | new_x = b[0] + b[2] + self.radius 59 | elif self.x < b[0]: # was prev to left of border. 60 | new_x = b[0] - self.radius 61 | 62 | self.vx *= -HIT_SOLID_DAMP 63 | stage.player_hit_solid() 64 | break 65 | 66 | new_y = self.y + self.vy 67 | 68 | for b in stage.solid_rects: 69 | if utils.circle_rect_overlap(self.x, new_y, self.radius, 70 | b[0], b[1], b[2], b[3]): 71 | if self.y > b[1] + b[3]: # was prev below border. 72 | new_y = b[1] + b[3] + self.radius 73 | elif self.y < b[1]: # was prev above border. 74 | new_y = b[1] - self.radius 75 | 76 | self.vy *= -HIT_SOLID_DAMP 77 | stage.player_hit_solid() 78 | break 79 | 80 | self.x = new_x 81 | self.y = new_y 82 | 83 | def _get_input_angle(self, last_inputs): 84 | press_angle = None 85 | if input.LEFT in last_inputs.pressing: 86 | press_angle = 180 87 | elif input.RIGHT in last_inputs.pressing: 88 | press_angle = 0 89 | 90 | if input.UP in last_inputs.pressing: 91 | if press_angle == 0: 92 | press_angle = 315 93 | elif press_angle == 180: 94 | press_angle = 225 95 | else: 96 | press_angle = 270 97 | elif input.DOWN in last_inputs.pressing: 98 | if press_angle == 0: 99 | press_angle = 45 100 | elif press_angle == 180: 101 | press_angle = 135 102 | else: 103 | press_angle = 90 104 | 105 | return press_angle 106 | 107 | # a "force" is a list of lists: [[speed, angle]... etc]. 108 | def _apply_forces(self, forces): 109 | for f in forces: 110 | self.vx = max(-MAX_SPEED, 111 | min(MAX_SPEED, 112 | self.vx + f[0] * math.cos(math.radians(f[1])))) 113 | self.vy = max(-MAX_SPEED, 114 | min(MAX_SPEED, 115 | self.vy + f[0] * math.sin(math.radians(f[1])))) 116 | #print("py after: " + str(self.y)) 117 | 118 | def _get_tile_force(self, stage, forces): 119 | angle = stage.get_tile_angle(self.x, self.y) 120 | if angle is not None: 121 | forces.append([SLOPE_ACCEL, angle]) 122 | #print("Got tile force: spd:{a}, accl:{b}".format(a=SLOPE_ACCEL, b=angle)) 123 | 124 | def _is_stuck_in_pocket(self, stage): 125 | if abs(self.vx) > 0.01 or abs(self.vy) > 0.01: 126 | return False 127 | 128 | for p in stage.pockets: 129 | if rect.contains_point(p[0], p[1], p[2], p[3], self.x, self.y): 130 | return True 131 | 132 | def _do_enemy_collisions(self, stage): 133 | for s in stage.spinners: 134 | if s.is_dead: 135 | continue 136 | if circle.overlap(s.x, s.y, s.radius, self.x, self.y, self.radius): 137 | self.state = STATE_DEAD 138 | stage.player_hit() 139 | return 140 | 141 | def _do_light_collisions(self, stage): 142 | for s in stage.lights: 143 | if rect.contains_point(s.x, s.y, 8, 8, self.x, self.y): 144 | if s.got_hit() == True: 145 | audio.play_sound(audio.SND_HIT_TARGET) 146 | if stage.is_complete(): 147 | self.state = STATE_STAGE_COMPLETE 148 | globals.add_score(globals.SCORE_STAGE_COMPLETE) 149 | return 150 | 151 | def fire_weapon(self, stage): 152 | self.weapon.fire(self.x, self.y) 153 | self.state = STATE_WEAPON 154 | globals.g_lives -= 1 155 | stage.player_used_weapon() 156 | globals.add_score(globals.SCORE_USE_WEAPON) 157 | 158 | def weapon_done(self): 159 | self.state = STATE_INTRO 160 | self.intro_frame = 4 161 | self.anim_ticks = 0 162 | self.vx = 0 163 | self.vy = 0 164 | 165 | def update(self, stage, last_inputs): 166 | if self.state == STATE_INTRO: 167 | self.anim_ticks += 1 168 | if self.anim_ticks == INTRO_TICKS_PER_FRAME: 169 | self.anim_ticks = 0 170 | if self.intro_frame > -1: 171 | self.intro_frame -= 1 172 | 173 | if self.intro_frame == -1: 174 | self.intro_frame = 4 175 | self.state = STATE_PLAY 176 | stage.player_intro_done() 177 | return 178 | elif self.state == STATE_DEAD: 179 | self.anim_ticks += 1 180 | if self.anim_ticks == DEAD_TICKS_PER_FRAME: 181 | self.anim_ticks = 0 182 | 183 | if self.dead_frame < 11: 184 | self.dead_frame += 1 185 | 186 | if self.dead_frame == 11: 187 | stage.player_death_anim_done() 188 | return 189 | elif self.state == STATE_STAGE_COMPLETE: 190 | return 191 | elif self.state == STATE_WEAPON: 192 | self.weapon.update(self, stage) 193 | return 194 | 195 | forces = [] 196 | 197 | #print("py after: " + str(self.y)) 198 | 199 | input_angle = self._get_input_angle(last_inputs) 200 | if input_angle is not None: 201 | forces.append([ACCEL, input_angle]) 202 | 203 | if not self._is_stuck_in_pocket(stage): 204 | self._get_tile_force(stage, forces) 205 | 206 | self._apply_forces(forces) 207 | 208 | if self.vx > 0: 209 | self.vx = max(0, self.vx - DECEL) 210 | elif self.vx < 0: 211 | self.vx = min(0, self.vx + DECEL) 212 | 213 | if self.vy > 0: 214 | self.vy = max(0, self.vy - DECEL) 215 | elif self.vy < 0: 216 | self.vy = min(0, self.vy + DECEL) 217 | 218 | #print("vx,vy: {a},{b}".format(a=self.vx, b=self.vy)) 219 | 220 | self._do_solid_collisions(stage) 221 | 222 | self._do_enemy_collisions(stage) 223 | 224 | if self.state != STATE_DEAD and self.state != STATE_GAME_COMPLETE: 225 | self._do_light_collisions(stage) 226 | 227 | if input.BUTTON_A in last_inputs.pressed and \ 228 | self.state != STATE_WEAPON and \ 229 | globals.g_lives > 0: 230 | self.fire_weapon(stage) 231 | 232 | #print("py after: " + str(self.y)) 233 | 234 | #if pyxel.mouse_x >= 8 and pyxel.mouse_x < 152 and \ 235 | # pyxel.mouse_y >= 16 and pyxel.mouse_y < 136: 236 | # ang = stage.get_tile_angle(pyxel.mouse_x, pyxel.mouse_y) 237 | #if ang is not None: 238 | # print("Hit slope angle {a},{b}: ".format(a=pyxel.mouse_x,b=pyxel.mouse_y)\ 239 | # + str(ang) + ", " + str(pyxel.frame_count)) 240 | 241 | 242 | def draw(self, shake_x, shake_y): 243 | if self.state == STATE_INTRO: 244 | pyxel.blt(shake_x + self.x-10, shake_y + self.y-10, 0, 245 | self.intro_frame*21, 231, 21, 21, 8) 246 | elif self.state == STATE_DEAD: 247 | pyxel.blt(shake_x + self.x-10, shake_y + self.y-10, 0, 248 | self.dead_frame*21, 231, 21, 21, 8) 249 | elif self.state == STATE_WEAPON: 250 | self.weapon.draw(shake_x, shake_y) 251 | else: 252 | pyxel.blt(shake_x + self.x-self.radius, shake_y + self.y-self.radius, 0, 253 | 16, 33, 9, 9, 8) 254 | 255 | #pyxel.circb(self.x, self.y, self.radius, 8) 256 | -------------------------------------------------------------------------------- /megaball/readme.txt: -------------------------------------------------------------------------------- 1 | Megaball was made in a week for GBJam 8. 2 | 3 | The source code is also available here: https://github.com/helpcomputer/megaball 4 | 5 | 6 | Gameplay 7 | 8 | The goal of the game is to roll the ball over every flashing panel to complete the stage. 9 | 10 | There are 15 stages that become increasingly more difficult, presenting you with tougher terrain and more enemies to avoid. 11 | 12 | For each stage you complete you gain one extra life. 13 | 14 | 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. 15 | 16 | 17 | Controls 18 | 19 | WASD, Arrow keys, or gamepad D-pad to move. 20 | 21 | Z key, K key, or gamepad Button A to fire weapon, or confirm in menu. 22 | 23 | Enter key or gamepad Start button to Start or Pause. 24 | 25 | F1 key to toggle sound. 26 | F2 key to toggle music. 27 | 28 | 29 | Credits 30 | 31 | Design & Art: 32 | 33 | https://helpcomputer.itch.io/ 34 | 35 | https://twitter.com/helpcomputer0 36 | 37 | Sound and Music: 38 | 39 | https://mikerichmond.itch.io/ 40 | 41 | https://twitter.com/richmondmike 42 | 43 | Font by Damien Guard: 44 | 45 | https://damieng.com/typography/zx-origins/ 46 | 47 | https://twitter.com/damienguard -------------------------------------------------------------------------------- /megaball/rect.py: -------------------------------------------------------------------------------- 1 | 2 | def overlap(x1, y1, w1, h1, x2, y2, w2, h2): 3 | return x1 < x2 + w2 and \ 4 | x1 + w1 > x2 and \ 5 | y1 < y2 + h2 and \ 6 | y1 + h1 > y2 7 | 8 | def contains_point(x, y, w, h, px, py): 9 | return x <= px and \ 10 | x + w >= px and \ 11 | y <= py and \ 12 | y + h >= py 13 | 14 | class Rect: 15 | def __init__(self, x, y, w, h): 16 | self.x = x 17 | self.y = y 18 | self.w = w 19 | self.h = h 20 | 21 | def is_overlapping(self, x, y, w, h): 22 | return self.x < x + w and \ 23 | self.x + self.w > x and \ 24 | self.y < y + h and \ 25 | self.y + self.h > y 26 | 27 | def is_overlapping_other(self, other): 28 | return self.x < other.x + other.w and \ 29 | self.x + self.w > other.x and \ 30 | self.y < other.y + other.h and \ 31 | self.y + self.h > other.y 32 | -------------------------------------------------------------------------------- /megaball/screenshake.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | class Event: 5 | def __init__(self, ticks, mag): 6 | self.ticks = ticks 7 | self.magnitude = mag 8 | 9 | class ScreenShake: 10 | def __init__(self, game): 11 | self.game = game 12 | 13 | self.x = 0 14 | self.y = 0 15 | 16 | self.events = [] 17 | 18 | def add_event(self, ticks, magnitude, queue=False): 19 | if ticks < 0 or magnitude <= 0: 20 | return 21 | 22 | if len(self.events) > 0 and not queue: 23 | return 24 | 25 | self.events.append(Event(ticks, magnitude)) 26 | 27 | def update(self): 28 | if len(self.events) > 0: 29 | e = self.events[0] 30 | e.ticks -= 1 31 | if e.ticks == 0: 32 | self.events.pop(0) 33 | self.x = 0 34 | self.y = 0 35 | else: 36 | self.x = random.randint(-e.magnitude, e.magnitude) 37 | self.y = random.randint(-e.magnitude, e.magnitude) 38 | -------------------------------------------------------------------------------- /megaball/spinner.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | import pyxel 5 | 6 | import utils 7 | import stage 8 | 9 | TYPE_AGGRESSIVE = 0 10 | TYPE_MILD = 1 11 | TYPE_RANDOM_SLOW = 2 12 | TYPE_RANDOM_FAST = 3 13 | 14 | TYPES = [ 15 | TYPE_AGGRESSIVE, 16 | TYPE_MILD, 17 | TYPE_RANDOM_SLOW, 18 | TYPE_RANDOM_FAST 19 | ] 20 | 21 | TICKS_PER_FRAME = 10 22 | MAX_FRAME = 4 23 | 24 | MAX_SPEED = 0.4 25 | MAX_RESPAWN_TICKS = 300 # 5 secs 26 | 27 | class Spinner: 28 | def __init__(self, x, y, type): 29 | self.x = x 30 | self.y = y 31 | self.type = 2 32 | if type in TYPES: 33 | self.type = type 34 | 35 | self.vx = random.choice([-MAX_SPEED, MAX_SPEED]) 36 | self.vy = random.choice([-MAX_SPEED, MAX_SPEED]) 37 | 38 | self.radius = 4 39 | 40 | self.frame = 0 41 | self.frame_ticks = 0 42 | 43 | self.is_dead = False 44 | 45 | self.respawn_ticks = MAX_RESPAWN_TICKS 46 | 47 | def _set_new_position(self, stageObj): 48 | px = stageObj.player.x 49 | py = stageObj.player.y 50 | loc = None 51 | loclist = [ 52 | stage.SPAWN_SECTOR_TOPLEFT, 53 | stage.SPAWN_SECTOR_BOTTOMLEFT, 54 | stage.SPAWN_SECTOR_TOPRIGHT, 55 | stage.SPAWN_SECTOR_BOTTOMRIGHT 56 | ] 57 | if px < 80: 58 | if py < 75: 59 | loclist.remove(stage.SPAWN_SECTOR_TOPLEFT) 60 | else: 61 | loclist.remove(stage.SPAWN_SECTOR_BOTTOMLEFT) 62 | else: 63 | if py < 75: 64 | loclist.remove(stage.SPAWN_SECTOR_TOPRIGHT) 65 | else: 66 | loclist.remove(stage.SPAWN_SECTOR_BOTTOMRIGHT) 67 | 68 | loc = stageObj.get_random_spawn_loc(random.choice(loclist)) 69 | self.x = loc[0] 70 | self.y = loc[1] 71 | 72 | def kill(self): 73 | self.is_dead = True 74 | self.respawn_ticks = MAX_RESPAWN_TICKS 75 | 76 | def _do_collisions(self, stage): 77 | new_x = self.x + self.vx 78 | 79 | for b in stage.solid_rects: 80 | if utils.circle_rect_overlap(new_x, self.y, self.radius, 81 | b[0], b[1], b[2], b[3]): 82 | if self.x > b[0] + b[2]: # was prev to right of border. 83 | new_x = b[0] + b[2] + self.radius 84 | elif self.x < b[0]: # was prev to left of border. 85 | new_x = b[0] - self.radius 86 | 87 | self.vx *= -1 88 | break 89 | 90 | new_y = self.y + self.vy 91 | 92 | for b in stage.solid_rects: 93 | if utils.circle_rect_overlap(self.x, new_y, self.radius, 94 | b[0], b[1], b[2], b[3]): 95 | if self.y > b[1] + b[3]: # was prev below border. 96 | new_y = b[1] + b[3] + self.radius 97 | elif self.y < b[1]: # was prev above border. 98 | new_y = b[1] - self.radius 99 | 100 | self.vy *= -1 101 | break 102 | 103 | self.x = new_x 104 | self.y = new_y 105 | 106 | def respawn(self): 107 | self.is_dead = False 108 | 109 | def update(self, stage): 110 | if self.is_dead: 111 | self.respawn_ticks -= 1 112 | if self.respawn_ticks == 0: 113 | self.respawn() 114 | elif self.respawn_ticks == 30: 115 | self._set_new_position(stage) 116 | else: 117 | self._do_collisions(stage) 118 | 119 | self.frame_ticks += 1 120 | if self.frame_ticks == TICKS_PER_FRAME: 121 | self.frame_ticks = 0 122 | self.frame += 1 123 | if self.frame == MAX_FRAME: 124 | self.frame = 0 125 | 126 | def draw(self, shake_x, shake_y): 127 | if self.is_dead: 128 | framex = None 129 | if self.respawn_ticks < 10: 130 | framex = 42 131 | elif self.respawn_ticks < 20: 132 | framex = 63 133 | elif self.respawn_ticks < 30: 134 | framex = 84 135 | if framex is not None: 136 | pyxel.blt( 137 | self.x + shake_x - 10, 138 | self.y + shake_y - 10, 139 | 0, 140 | framex, 141 | 231, 142 | 21, 21, 143 | 8 144 | ) 145 | else: 146 | pyxel.blt( 147 | self.x + shake_x - 4, 148 | self.y + shake_y - 4, 149 | 0, 150 | 160 + self.frame*9, 151 | 8, 152 | 9, 9, 153 | 8 154 | ) 155 | 156 | -------------------------------------------------------------------------------- /megaball/stage.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | import random 4 | 5 | import pyxel 6 | 7 | import player 8 | import utils 9 | import constants 10 | import stage 11 | import light 12 | import input 13 | import game 14 | import palette 15 | import spinner 16 | import globals 17 | import stagedata 18 | import audio 19 | 20 | MAX_STAGE_NUM = 15 21 | 22 | WIDTH_TILES = 18 23 | HEIGHT_TILES = 15 24 | 25 | POST_TILE = utils.get_tile_index(40, 32) 26 | 27 | # tile_index : [angle, collision matrix if triangle] 28 | SLOPE_TILES = { 29 | utils.get_tile_index(56,32): [225, constants.COLLIDE_BOTTOM_RIGHT], # top-left 30 | utils.get_tile_index(64,32): [270, None], # top 31 | utils.get_tile_index(72,32): [315, constants.COLLIDE_BOTTOM_LEFT], # top-right 32 | utils.get_tile_index(56,40): [180, None], # left 33 | utils.get_tile_index(72,40): [0, None], # right 34 | utils.get_tile_index(56,48): [135, constants.COLLIDE_TOP_RIGHT], # bottom-left 35 | utils.get_tile_index(64,48): [90, None], # bottom 36 | utils.get_tile_index(72,48): [45, constants.COLLIDE_TOP_LEFT], # bottom-right 37 | utils.get_tile_index(80,32): [225, constants.COLLIDE_TOP_LEFT], # top-left 2 38 | utils.get_tile_index(88,32): [135, constants.COLLIDE_BOTTOM_LEFT], # bottom-left 2 39 | utils.get_tile_index(80,40): [45, constants.COLLIDE_BOTTOM_RIGHT], # bottom-right 2 40 | utils.get_tile_index(88,40): [315, constants.COLLIDE_TOP_RIGHT] # top-right 2 41 | #utils.get_tile_index(), 42 | } 43 | 44 | POCKET_TILE_NW = utils.get_tile_index(80,40) 45 | POCKET_TILE_NE = utils.get_tile_index(88,32) 46 | POCKET_TILE_SE = utils.get_tile_index(80,32) 47 | POCKET_TILE_SW = utils.get_tile_index(88,40) 48 | 49 | LIGHT_TILE = utils.get_tile_index(160,0) 50 | BLANK_TILE = utils.get_tile_index(32,24) 51 | 52 | class PauseMenu: 53 | 54 | SEL_RESUME = 0 55 | SEL_PALETTE = 1 56 | SEL_QUIT = 2 57 | 58 | SELECTIONS = { 59 | SEL_RESUME : [56,55,48,8], 60 | SEL_PALETTE : [52,71,56,8], 61 | SEL_QUIT : [64,87,32,8] 62 | } 63 | 64 | def __init__(self, stage): 65 | self.stage = stage 66 | 67 | self.is_visible = False 68 | 69 | self.sel_index = 0 70 | 71 | self.quitting = False 72 | 73 | def _pressed_select(self): 74 | if self.sel_index == self.SEL_RESUME: 75 | self.is_visible = False 76 | elif self.sel_index == self.SEL_PALETTE: 77 | self.stage.game.add_fade(5, palette.FADE_LEVEL_6, self.stage.game.cycle_palette) 78 | elif self.sel_index == self.SEL_QUIT: 79 | self.quitting = True 80 | self.stage.quit() 81 | 82 | def _change_selection(self, dir): 83 | self.sel_index += dir 84 | if self.sel_index < 0: 85 | self.sel_index = len(self.SELECTIONS) - 1 86 | elif self.sel_index >= len(self.SELECTIONS): 87 | self.sel_index = 0 88 | 89 | def update(self, last_inputs): 90 | if not self.is_visible or self.quitting: 91 | return 92 | 93 | if input.BUTTON_START in last_inputs.pressed: 94 | self.is_visible = False 95 | self.sel_index = 0 96 | elif input.BUTTON_A in last_inputs.pressed: 97 | self._pressed_select() 98 | elif input.UP in last_inputs.pressed: 99 | self._change_selection(-1) 100 | elif input.DOWN in last_inputs.pressed: 101 | self._change_selection(1) 102 | 103 | def draw(self, shake_x, shake_y): 104 | if not self.is_visible: 105 | return 106 | 107 | pyxel.blt(shake_x + 24, shake_y + 52, 0, 0, 144, 116, 52, 8) # panel bg 108 | 109 | pyxel.blt(shake_x + 56, shake_y + 56, 0, 128, 72, 48, 8, 8) # resume 110 | pyxel.blt(shake_x + 52, shake_y + 72, 0, 104, 80, 56, 8, 8) # palette 111 | pyxel.blt(shake_x + 64, shake_y + 88, 0, 96, 64, 32, 8, 8) # quit 112 | 113 | pyxel.blt( 114 | shake_x + self.SELECTIONS[self.sel_index][0]-12, 115 | shake_y + self.SELECTIONS[self.sel_index][1], 116 | 0, 117 | 16, 33, 9, 9, 8 118 | ) # selection ball left 119 | pyxel.blt( 120 | shake_x + self.SELECTIONS[self.sel_index][0] + self.SELECTIONS[self.sel_index][2] + 2, 121 | shake_y + self.SELECTIONS[self.sel_index][1], 122 | 0, 123 | 16, 33, 9, 9, 8 124 | ) # selection ball right 125 | 126 | STATE_INTRO = 0 127 | STATE_PLAY = 1 128 | STATE_DIED = 2 129 | STATE_DEMO = 3 130 | STATE_GAME_OVER = 4 131 | STATE_STAGE_COMPLETE = 5 132 | STATE_GAME_COMPLETE = 6 133 | STATE_PLAYER_WEAPON = 7 134 | 135 | MAX_SHOW_GAME_OVER_TICKS = 300 # 5 secs 136 | MAX_SHOW_GAME_COMPLETE_TICKS = 300 # 5 secs 137 | 138 | SPAWN_SECTOR_TOPLEFT = 0 139 | SPAWN_SECTOR_TOPRIGHT = 1 140 | SPAWN_SECTOR_BOTTOMLEFT = 2 141 | SPAWN_SECTOR_BOTTOMRIGHT = 3 142 | 143 | class Stage: 144 | def __init__(self, game, num): 145 | self.game = game 146 | self.num = num 147 | self.tm = 0 148 | self.tmu = 0 149 | self.tmv = num * 16 150 | 151 | self.state = STATE_INTRO 152 | if self.num <= 0: 153 | self.state = STATE_DEMO 154 | elif self.num == MAX_STAGE_NUM + 1: 155 | self.tm = 1 156 | self.tmu = 0 157 | self.tmv = 0 158 | self.state = STATE_GAME_COMPLETE 159 | 160 | self.solid_rects = [ 161 | [0, 0, 160, 16], # [x, y, w, h] 162 | [0, 16, 8, 128], 163 | [152, 16, 8, 128], 164 | [0, 136, 160, 8] 165 | ] 166 | 167 | self.slopes = [] # [x, y] 168 | self.pockets = [] # [x, y, w, h] 169 | self.lights = [] # Light objects 170 | self.spinners = [] # Spinner objects 171 | 172 | self.en_spawn_locs_topleft = [] # [[x,y],[x,y],[x,y]...] 173 | self.en_spawn_locs_topright = [] # [[x,y],[x,y],[x,y]...] 174 | self.en_spawn_locs_bottomleft = [] # [[x,y],[x,y],[x,y]...] 175 | self.en_spawn_locs_bottomright = [] # [[x,y],[x,y],[x,y]...] 176 | 177 | if self.state != STATE_GAME_COMPLETE: 178 | for yc in range(HEIGHT_TILES): 179 | y = self.tmv + yc 180 | for xc in range(WIDTH_TILES): 181 | x = self.tmu + xc 182 | tile = pyxel.tilemap(self.tm).get(x, y) 183 | if tile == POST_TILE: 184 | self.solid_rects.append([xc*8 + 8, yc*8 + 16, 8, 8]) 185 | elif tile in SLOPE_TILES: 186 | self.slopes.append([xc*8 + 8, yc*8 + 16]) 187 | elif tile == LIGHT_TILE: 188 | self.lights.append(light.Light(xc*8 + 8, yc*8 + 16)) 189 | 190 | if tile == POCKET_TILE_NW: 191 | if x < self.tmu + WIDTH_TILES-1 and y < self.tmv + HEIGHT_TILES-1: 192 | if pyxel.tilemap(self.tm).get(x+1, y) == POCKET_TILE_NE and\ 193 | pyxel.tilemap(self.tm).get(x+1, y+1) == POCKET_TILE_SE and\ 194 | pyxel.tilemap(self.tm).get(x, y+1) == POCKET_TILE_SW: 195 | self.pockets.append([xc*8 + 8, yc*8 + 16, 16, 16]) 196 | 197 | if tile != POST_TILE and \ 198 | xc > 0 and \ 199 | xc < WIDTH_TILES-1 and \ 200 | yc > 0 and \ 201 | yc < HEIGHT_TILES-1 and \ 202 | (xc < 5 or xc > WIDTH_TILES-6) and \ 203 | (yc < 5 or yc > HEIGHT_TILES-6): 204 | 205 | loc = [xc*8 + 8 + 4, yc*8 + 16 + 4] 206 | 207 | if xc < 9: 208 | if yc < 7: 209 | self.en_spawn_locs_topleft.append(loc) 210 | else: 211 | self.en_spawn_locs_bottomleft.append(loc) 212 | else: 213 | if yc < 7: 214 | self.en_spawn_locs_topright.append(loc) 215 | else: 216 | self.en_spawn_locs_bottomright.append(loc) 217 | 218 | #print(self.pockets) 219 | num_spinners = 0 220 | stage_diff_name = stagedata.STAGE_DIFFICULTY[self.num] 221 | for i in range(len(spinner.TYPES)): 222 | en_qty = stagedata.ENEMIES[stage_diff_name][stagedata.SPINNER_KEY][i] 223 | for sq in range(en_qty): 224 | loc = self.get_random_spawn_loc(-1) 225 | self.spinners.append(spinner.Spinner(loc[0], loc[1], i)) 226 | 227 | self.player = player.Player(75,75)#(12, 20) 228 | if self.state == STATE_GAME_COMPLETE: 229 | self.player.state = player.STATE_GAME_COMPLETE 230 | audio.play_music(audio.MUS_IN_GAME, True) 231 | else: 232 | if self.state != STATE_DEMO: 233 | audio.play_music(audio.MUS_START, False) 234 | 235 | self.pause_menu = PauseMenu(self) 236 | 237 | self.stage_over_ticks = 0 238 | 239 | self.next_stage_flash_num = 0 240 | 241 | def restart_music(self): 242 | if self.state == STATE_PLAY: 243 | audio.play_music(audio.MUS_IN_GAME) 244 | 245 | def get_random_spawn_loc(self, sector): 246 | if sector == SPAWN_SECTOR_TOPLEFT: 247 | return random.choice(self.en_spawn_locs_topleft) 248 | elif sector == SPAWN_SECTOR_TOPRIGHT: 249 | return random.choice(self.en_spawn_locs_topright) 250 | elif sector == SPAWN_SECTOR_BOTTOMLEFT: 251 | return random.choice(self.en_spawn_locs_bottomleft) 252 | elif sector == SPAWN_SECTOR_BOTTOMRIGHT: 253 | return random.choice(self.en_spawn_locs_bottomright) 254 | else: 255 | ranlist = random.choice([ 256 | self.en_spawn_locs_topleft, 257 | self.en_spawn_locs_topright, 258 | self.en_spawn_locs_bottomleft, 259 | self.en_spawn_locs_bottomright 260 | ]) 261 | return random.choice(ranlist) 262 | 263 | def player_used_weapon(self): 264 | audio.play_sound(audio.SND_USED_WEAPON) 265 | self.state = STATE_PLAYER_WEAPON 266 | 267 | def player_intro_done(self): 268 | if self.state != STATE_PLAYER_WEAPON: 269 | audio.play_music(audio.MUS_IN_GAME, True) 270 | 271 | self.state = STATE_PLAY 272 | 273 | def player_hit(self): 274 | self.state = STATE_DIED 275 | audio.play_music(audio.MUS_DEATH, False) 276 | 277 | def is_complete(self): 278 | for i in self.lights: 279 | if i.is_hit == False: 280 | return False 281 | 282 | audio.play_music(audio.MUS_STAGE_COMPLETE, False) 283 | self._check_next_stage() 284 | 285 | return True 286 | 287 | def player_death_anim_done(self): 288 | if globals.g_lives >= 1: 289 | globals.g_lives -= 1 290 | self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, 291 | palette.FADE_LEVEL_6, self.game.restart_stage) 292 | else: 293 | self.state = STATE_GAME_OVER 294 | audio.play_music(audio.MUS_GAME_OVER, False) 295 | 296 | def _check_next_stage(self): 297 | #if self.num < MAX_STAGE_NUM: 298 | self.state = STATE_STAGE_COMPLETE 299 | self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, 300 | palette.FADE_LEVEL_0, self.go_to_next_stage) 301 | #else: 302 | # self.state = STATE_GAME_COMPLETE 303 | 304 | def go_to_next_stage(self): 305 | if self.next_stage_flash_num == 0: 306 | self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, 307 | palette.FADE_LEVEL_3, self.go_to_next_stage) 308 | elif self.next_stage_flash_num == 1: 309 | self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, 310 | palette.FADE_LEVEL_0, self.go_to_next_stage) 311 | #elif self.next_stage_flash_num == 2: 312 | # self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, 313 | # palette.FADE_LEVEL_3, self.go_to_next_stage) 314 | #elif self.next_stage_flash_num == 3: 315 | # self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, 316 | # palette.FADE_LEVEL_0, self.go_to_next_stage) 317 | else: 318 | if self.num == stage.MAX_STAGE_NUM: 319 | self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, 320 | palette.FADE_LEVEL_6, self.game.go_to_game_complete_stage) 321 | else: 322 | globals.add_lives(1) 323 | self.game.add_fade(palette.FADE_STEP_TICKS_SLOW, 324 | palette.FADE_LEVEL_6, self.game.go_to_next_stage) 325 | 326 | self.next_stage_flash_num += 1 327 | 328 | def quit(self): 329 | self.game.add_fade(palette.FADE_STEP_TICKS_DEFAULT, 330 | palette.FADE_LEVEL_6, self.game.quit_to_main_menu) 331 | 332 | def player_hit_solid(self): 333 | audio.play_sound(audio.SND_HIT_WALL) 334 | self.game.add_screen_shake(5, 1, queue=False) 335 | 336 | # returns None or angle 337 | def get_tile_angle(self, x, y): # x,y is screen pixels. 338 | tile = pyxel.tilemap(self.tm).get( 339 | self.tmu + math.floor((x-8)/8), 340 | self.tmv + math.floor((y-16)/8) 341 | ) 342 | 343 | if tile in SLOPE_TILES: 344 | # check if triangle matrix collision check needed. 345 | if SLOPE_TILES[tile][1] is not None: 346 | t = SLOPE_TILES[tile] 347 | 348 | tx = math.floor(abs(x - math.floor(x/8)*8)) 349 | ty = math.floor(abs(y - math.floor(y/8)*8)) 350 | 351 | #print("Checking matrix x,y: {a},{b} ...".format(a=tx, b=ty)) 352 | 353 | if constants.is_colliding_matrix(tx, ty, t[1]): 354 | #print("{a}, {b} hit triangle".format(a=x, b=y)) 355 | #print("... collides.") 356 | return t[0] 357 | else: 358 | #print("... no collision.") 359 | return None 360 | else: 361 | return SLOPE_TILES[tile][0] 362 | else: 363 | return None 364 | 365 | def update(self, last_inputs): 366 | if self.num > 0: # dont allow inputs on demo/main menu stage 0. 367 | if self.pause_menu.is_visible: 368 | self.pause_menu.update(last_inputs) 369 | else: 370 | if input.BUTTON_START in last_inputs.pressed: 371 | if self.state == STATE_PLAY or \ 372 | self.state == STATE_PLAYER_WEAPON: 373 | self.pause_menu.is_visible = True 374 | else: 375 | self.player.update(self, last_inputs) 376 | 377 | if self.state == STATE_PLAY or\ 378 | self.state == STATE_DEMO: 379 | for s in self.spinners: 380 | s.update(self) 381 | 382 | if self.state == STATE_GAME_OVER: 383 | self.stage_over_ticks += 1 384 | if self.stage_over_ticks == MAX_SHOW_GAME_OVER_TICKS: 385 | self.quit() 386 | elif self.state == STATE_GAME_COMPLETE: 387 | self.stage_over_ticks += 1 388 | if self.stage_over_ticks >= MAX_SHOW_GAME_COMPLETE_TICKS: 389 | if input.BUTTON_A in last_inputs.pressed: 390 | self.quit() 391 | 392 | if self.state == STATE_PLAY or\ 393 | self.state == STATE_DEMO: 394 | for i in self.lights: 395 | i.update(self) 396 | 397 | def draw(self, shake_x, shake_y): 398 | pyxel.bltm(shake_x + 8, shake_y + 16, self.tm, self.tmu, self.tmv, 399 | WIDTH_TILES, HEIGHT_TILES, 8) 400 | 401 | for i in self.lights: 402 | i.draw(shake_x, shake_y) 403 | 404 | if self.state == STATE_GAME_COMPLETE: 405 | pyxel.blt(24 + shake_x, 32 + shake_y, 0, 136, 136, 112, 88) 406 | 407 | if self.num > 0: 408 | self.player.draw(shake_x, shake_y) 409 | 410 | for s in self.spinners: 411 | s.draw(shake_x, shake_y) 412 | 413 | if self.num > 0: 414 | self.pause_menu.draw(shake_x, shake_y) 415 | 416 | if self.state == STATE_GAME_OVER and self.stage_over_ticks > 30: 417 | pyxel.blt(32 + shake_x, 66 + shake_y, 0, 0, 196, 100, 26, 8) # game over bg 418 | pyxel.blt(44 + shake_x, 72 + shake_y, 0, 40, 80, 32, 8, 8) # "game" 419 | pyxel.blt(84 + shake_x, 72 + shake_y, 0, 72, 80, 32, 8, 8) # "over" 420 | 421 | -------------------------------------------------------------------------------- /megaball/stagedata.py: -------------------------------------------------------------------------------- 1 | 2 | ''' 3 | Each difficulty has a dictionary: 4 | easy : { 5 | ... 6 | } 7 | 8 | With that dictionary is another dictionary of quantity of each object type: 9 | 10 | "spinners" : [personalityA, personalityB, ... etc] 11 | ''' 12 | 13 | SPINNER_KEY = "spinners" 14 | DIFF_NONE_KEY = "none" 15 | DIFF_VERY_EASY_KEY = "very easy" 16 | DIFF_EASY_KEY = "easy" 17 | DIFF_MEDIUM_KEY = "medium" 18 | DIFF_HARD_KEY = "hard" 19 | DIFF_VERY_HARD_KEY = "very hard" 20 | 21 | #[aggressive, mildly aggressive, slow random, fast random] 22 | 23 | ENEMIES = { 24 | 25 | DIFF_NONE_KEY : { 26 | SPINNER_KEY : [0,0,0,0] 27 | }, 28 | 29 | DIFF_VERY_EASY_KEY : { 30 | SPINNER_KEY : [2,1,1,0]#[1,1,1,0] 31 | }, 32 | 33 | DIFF_EASY_KEY : { 34 | SPINNER_KEY : [2,1,2,0]#[1,1,2,0] 35 | }, 36 | 37 | DIFF_MEDIUM_KEY : { 38 | SPINNER_KEY : [3,1,1,1]#[2,1,1,1] 39 | }, 40 | 41 | DIFF_HARD_KEY : { 42 | SPINNER_KEY : [3,1,1,2]#[2,1,1,2] 43 | }, 44 | 45 | DIFF_VERY_HARD_KEY : { 46 | SPINNER_KEY : [3,2,1,1]#[2,2,1,1] 47 | } 48 | 49 | } 50 | 51 | STAGE_DIFFICULTY = [ 52 | DIFF_NONE_KEY, # 0 53 | DIFF_VERY_EASY_KEY, # 1 54 | DIFF_EASY_KEY, # 2 55 | DIFF_EASY_KEY, # 3 56 | DIFF_EASY_KEY, # 4 57 | DIFF_EASY_KEY, # 5 58 | DIFF_MEDIUM_KEY, # 6 59 | DIFF_EASY_KEY, # 7 60 | DIFF_MEDIUM_KEY, # 8 61 | DIFF_VERY_EASY_KEY, # 9 62 | DIFF_VERY_HARD_KEY, # 10 63 | DIFF_HARD_KEY, # 11 64 | DIFF_VERY_HARD_KEY, # 12 65 | DIFF_EASY_KEY, # 13 66 | DIFF_VERY_HARD_KEY, # 14 67 | DIFF_VERY_HARD_KEY # 15 68 | ] 69 | 70 | -------------------------------------------------------------------------------- /megaball/utils.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | import pyxel 5 | 6 | def angle_reflect(incidenceAngle, surfaceAngle): 7 | a = surfaceAngle * 2 - incidenceAngle 8 | return (a + 360) % 360 9 | 10 | def sign_triangle(p1, p2, p3): 11 | return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]) 12 | 13 | def is_point_in_triangle(px, py, ax, ay, bx, by, cx, cy): 14 | #print("Checking point [{a},{b}] in tri [{c}][{d}], [{e}][{f}], [{g}][{h}] ({i})".format( 15 | # a=px, b=py, c=ax, d=ay, e=bx, f=by, g=cx, h=cy, i=pyxel.frame_count 16 | #)) 17 | d1 = sign_triangle([px, py], [ax,ay], [bx,by]) 18 | d2 = sign_triangle([px, py], [bx,by], [cx,cy]) 19 | d3 = sign_triangle([px, py], [cx,cy], [ax,ay]) 20 | 21 | has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0) 22 | has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0) 23 | 24 | #print("return: " + str(not(has_neg and has_pos))) 25 | 26 | return not(has_neg and has_pos) 27 | 28 | def circle_rect_overlap(cx, cy, cr, rx, ry, rw, rh): 29 | closestX = cx 30 | closestY = cy 31 | 32 | if cx < rx: 33 | closestX = rx 34 | elif cx > rx + rw: 35 | closestX = rx + rw 36 | 37 | if cy < ry: 38 | closestY = ry 39 | elif cy > ry + rh: 40 | closestY = ry + rh 41 | 42 | closestX = closestX - cx 43 | closestX *= closestX 44 | closestY = closestY - cy 45 | closestY *= closestY 46 | 47 | return closestX + closestY < cr * cr 48 | 49 | def get_angle_deg(x1, y1, x2, y2): 50 | degs = math.degrees(math.atan2(y2 - y1, x2 - x1)) 51 | return (degs + 360) % 360 52 | 53 | def get_tile_x(index): 54 | return math.floor(index % 32) * 8 55 | 56 | def get_tile_y(index): 57 | return math.floor(index / 32) * 8 58 | 59 | def get_tile_index(x, y): 60 | return x/8 + (y / 8) * 32 61 | 62 | def lerp(v, d): 63 | #print("delta: " + str(d) + ", v: " + str(v[0]) + "," + str(v[1])) 64 | #print() 65 | return (v[0] * (1.0 - d)) + (v[1] * d) 66 | 67 | def ease_out_expo(x): 68 | if x == 1: 69 | return 1 70 | 71 | return 1 - math.pow(2, -10 * x) 72 | 73 | def ease_out_cubic(x): 74 | return 1 - math.pow(1 - x, 3) 75 | 76 | def draw_number_shadowed(x, y, num, zeropad=0): 77 | strnum = str(num) 78 | if zeropad > 0: 79 | strnum = strnum.zfill(zeropad) 80 | 81 | for i in range(len(strnum)): 82 | pyxel.blt(x + i*8, y, 0, 16 + int(strnum[i])*8, 56, 8, 8, 8) 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /megaball/weapon.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | import pyxel 5 | 6 | import rect 7 | import constants 8 | import player 9 | import stage 10 | import spinner 11 | import circle 12 | import globals 13 | 14 | MAX_SHOTS = 10 15 | SHOT_RADIUS = 3 16 | SHOT_SPEED = 1.5 17 | 18 | VEL = [] 19 | for i in range(MAX_SHOTS): 20 | VEL.append( 21 | [ 22 | SHOT_SPEED * math.cos(math.radians(i*36)), 23 | SHOT_SPEED * math.sin(math.radians(i*36)), 24 | ] 25 | ) 26 | 27 | class Weapon: 28 | def __init__(self): 29 | self.active = False 30 | 31 | self.shots = [] 32 | for i in range(MAX_SHOTS): 33 | self.shots.append([0,0]) 34 | 35 | def fire(self, from_x, from_y): 36 | self.active = True 37 | for s in self.shots: 38 | s[0] = from_x 39 | s[1] = from_y 40 | 41 | def update(self, player, stage): 42 | if not self.active: 43 | return 44 | 45 | done = True 46 | 47 | for i, s in enumerate(self.shots): 48 | s[0] += VEL[i][0] 49 | s[1] += VEL[i][1] 50 | 51 | if done != False and\ 52 | rect.contains_point(0, 0, 53 | constants.GAME_WIDTH, constants.GAME_HEIGHT, 54 | s[0], s[1]): 55 | done = False 56 | 57 | for spin in stage.spinners: 58 | if not spin.is_dead: 59 | if circle.overlap( 60 | s[0], s[1], SHOT_RADIUS, 61 | spin.x, spin.y, spin.radius): 62 | globals.add_score(globals.SCORE_KILLED_SPINNER) 63 | spin.kill() 64 | 65 | if done: 66 | spinners_killed = sum(s.is_dead == True for s in stage.spinners) 67 | if spinners_killed == len(stage.spinners): 68 | globals.add_score(globals.SCORE_KILLED_ALL_SPINNERS) 69 | self.active = False 70 | player.weapon_done() 71 | 72 | def draw(self, shake_x, shake_y): 73 | if not self.active: 74 | return 75 | 76 | for s in self.shots: 77 | pyxel.blt(shake_x + s[0] - 10, 78 | shake_y + s[1] - 10, 79 | 0, 21, 231, 21, 21, 8) 80 | 81 | --------------------------------------------------------------------------------