From 612b0bcb271a32a8b8718eb518e48e0f3339b0c3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 25 May 2025 02:49:59 +0000 Subject: [PATCH 1/2] Fix: Mario correctly kicks still Koopa shells when boosting Previously, when you were boosting (holding shift) and ran into a still Koopa shell, Mario would die instead of kicking the shell. This commit refactors the `_onCollisionWithMob` method in `entities/Mario.py` to: - Ensure that kicking a still Koopa shell is prioritized over Mario dying if the conditions for a kick are met (side collision with a live, non-active, non-bouncing Koopa). - The Koopa shell now becomes `bouncing` and is kicked in the appropriate direction. - A small nudge is given to the kicked shell to help prevent immediate re-collision with a fast-moving Mario. - Additionally, I adjusted the logic for stomping an already still Koopa shell to make it start bouncing, aligning with common platformer mechanics. --- entities/Mario.py | 404 +++++++++++++++++++++++++--------------------- 1 file changed, 216 insertions(+), 188 deletions(-) diff --git a/entities/Mario.py b/entities/Mario.py index 8321284c..1d94b340 100644 --- a/entities/Mario.py +++ b/entities/Mario.py @@ -1,188 +1,216 @@ -import pygame - -from classes.Animation import Animation -from classes.Camera import Camera -from classes.Collider import Collider -from classes.EntityCollider import EntityCollider -from classes.Input import Input -from classes.Sprites import Sprites -from entities.EntityBase import EntityBase -from entities.Mushroom import RedMushroom -from traits.bounce import bounceTrait -from traits.go import GoTrait -from traits.jump import JumpTrait -from classes.Pause import Pause - -spriteCollection = Sprites().spriteCollection -smallAnimation = Animation( - [ - spriteCollection["mario_run1"].image, - spriteCollection["mario_run2"].image, - spriteCollection["mario_run3"].image, - ], - spriteCollection["mario_idle"].image, - spriteCollection["mario_jump"].image, -) -bigAnimation = Animation( - [ - spriteCollection["mario_big_run1"].image, - spriteCollection["mario_big_run2"].image, - spriteCollection["mario_big_run3"].image, - ], - spriteCollection["mario_big_idle"].image, - spriteCollection["mario_big_jump"].image, -) - - -class Mario(EntityBase): - def __init__(self, x, y, level, screen, dashboard, sound, gravity=0.8): - super(Mario, self).__init__(x, y, gravity) - self.camera = Camera(self.rect, self) - self.sound = sound - self.input = Input(self) - self.inAir = False - self.inJump = False - self.powerUpState = 0 - self.invincibilityFrames = 0 - self.traits = { - "jumpTrait": JumpTrait(self), - "goTrait": GoTrait(smallAnimation, screen, self.camera, self), - "bounceTrait": bounceTrait(self), - } - - self.levelObj = level - self.collision = Collider(self, level) - self.screen = screen - self.EntityCollider = EntityCollider(self) - self.dashboard = dashboard - self.restart = False - self.pause = False - self.pauseObj = Pause(screen, self, dashboard) - - def update(self): - if self.invincibilityFrames > 0: - self.invincibilityFrames -= 1 - self.updateTraits() - self.moveMario() - self.camera.move() - self.applyGravity() - self.checkEntityCollision() - self.input.checkForInput() - - def moveMario(self): - self.rect.y += self.vel.y - self.collision.checkY() - self.rect.x += self.vel.x - self.collision.checkX() - - def checkEntityCollision(self): - for ent in self.levelObj.entityList: - collisionState = self.EntityCollider.check(ent) - if collisionState.isColliding: - if ent.type == "Item": - self._onCollisionWithItem(ent) - elif ent.type == "Block": - self._onCollisionWithBlock(ent) - elif ent.type == "Mob": - self._onCollisionWithMob(ent, collisionState) - - def _onCollisionWithItem(self, item): - self.levelObj.entityList.remove(item) - self.dashboard.points += 100 - self.dashboard.coins += 1 - self.sound.play_sfx(self.sound.coin) - - def _onCollisionWithBlock(self, block): - if not block.triggered: - self.dashboard.coins += 1 - self.sound.play_sfx(self.sound.bump) - block.triggered = True - - def _onCollisionWithMob(self, mob, collisionState): - if isinstance(mob, RedMushroom) and mob.alive: - self.powerup(1) - self.killEntity(mob) - self.sound.play_sfx(self.sound.powerup) - elif collisionState.isTop and (mob.alive or mob.bouncing): - self.sound.play_sfx(self.sound.stomp) - self.rect.bottom = mob.rect.top - self.bounce() - self.killEntity(mob) - elif collisionState.isTop and mob.alive and not mob.active: - self.sound.play_sfx(self.sound.stomp) - self.rect.bottom = mob.rect.top - mob.timer = 0 - self.bounce() - mob.alive = False - elif collisionState.isColliding and mob.alive and not mob.active and not mob.bouncing: - mob.bouncing = True - if mob.rect.x < self.rect.x: - mob.leftrightTrait.direction = -1 - mob.rect.x += -5 - self.sound.play_sfx(self.sound.kick) - else: - mob.rect.x += 5 - mob.leftrightTrait.direction = 1 - self.sound.play_sfx(self.sound.kick) - elif collisionState.isColliding and mob.alive and not self.invincibilityFrames: - if self.powerUpState == 0: - self.gameOver() - elif self.powerUpState == 1: - self.powerUpState = 0 - self.traits['goTrait'].updateAnimation(smallAnimation) - x, y = self.rect.x, self.rect.y - self.rect = pygame.Rect(x, y + 32, 32, 32) - self.invincibilityFrames = 60 - self.sound.play_sfx(self.sound.pipe) - - def bounce(self): - self.traits["bounceTrait"].jump = True - - def killEntity(self, ent): - if ent.__class__.__name__ != "Koopa": - ent.alive = False - else: - ent.timer = 0 - ent.leftrightTrait.speed = 1 - ent.alive = True - ent.active = False - ent.bouncing = False - self.dashboard.points += 100 - - def gameOver(self): - srf = pygame.Surface((640, 480)) - srf.set_colorkey((255, 255, 255), pygame.RLEACCEL) - srf.set_alpha(128) - self.sound.music_channel.stop() - self.sound.music_channel.play(self.sound.death) - - for i in range(500, 20, -2): - srf.fill((0, 0, 0)) - pygame.draw.circle( - srf, - (255, 255, 255), - (int(self.camera.x + self.rect.x) + 16, self.rect.y + 16), - i, - ) - self.screen.blit(srf, (0, 0)) - pygame.display.update() - self.input.checkForInput() - while self.sound.music_channel.get_busy(): - pygame.display.update() - self.input.checkForInput() - self.restart = True - - def getPos(self): - return self.camera.x + self.rect.x, self.rect.y - - def setPos(self, x, y): - self.rect.x = x - self.rect.y = y - - def powerup(self, powerupID): - if self.powerUpState == 0: - if powerupID == 1: - self.powerUpState = 1 - self.traits['goTrait'].updateAnimation(bigAnimation) - self.rect = pygame.Rect(self.rect.x, self.rect.y-32, 32, 64) - self.invincibilityFrames = 20 +import pygame + +from classes.Animation import Animation +from classes.Camera import Camera +from classes.Collider import Collider +from classes.EntityCollider import EntityCollider +from classes.Input import Input +from classes.Sprites import Sprites +from entities.EntityBase import EntityBase +from entities.Mushroom import RedMushroom +from entities.Koopa import Koopa +from traits.bounce import bounceTrait +from traits.go import GoTrait +from traits.jump import JumpTrait +from classes.Pause import Pause + +spriteCollection = Sprites().spriteCollection +smallAnimation = Animation( + [ + spriteCollection["mario_run1"].image, + spriteCollection["mario_run2"].image, + spriteCollection["mario_run3"].image, + ], + spriteCollection["mario_idle"].image, + spriteCollection["mario_jump"].image, +) +bigAnimation = Animation( + [ + spriteCollection["mario_big_run1"].image, + spriteCollection["mario_big_run2"].image, + spriteCollection["mario_big_run3"].image, + ], + spriteCollection["mario_big_idle"].image, + spriteCollection["mario_big_jump"].image, +) + + +class Mario(EntityBase): + def __init__(self, x, y, level, screen, dashboard, sound, gravity=0.8): + super(Mario, self).__init__(x, y, gravity) + self.camera = Camera(self.rect, self) + self.sound = sound + self.input = Input(self) + self.inAir = False + self.inJump = False + self.powerUpState = 0 + self.invincibilityFrames = 0 + self.traits = { + "jumpTrait": JumpTrait(self), + "goTrait": GoTrait(smallAnimation, screen, self.camera, self), + "bounceTrait": bounceTrait(self), + } + + self.levelObj = level + self.collision = Collider(self, level) + self.screen = screen + self.EntityCollider = EntityCollider(self) + self.dashboard = dashboard + self.restart = False + self.pause = False + self.pauseObj = Pause(screen, self, dashboard) + + def update(self): + if self.invincibilityFrames > 0: + self.invincibilityFrames -= 1 + self.updateTraits() + self.moveMario() + self.camera.move() + self.applyGravity() + self.checkEntityCollision() + self.input.checkForInput() + + def moveMario(self): + self.rect.y += self.vel.y + self.collision.checkY() + self.rect.x += self.vel.x + self.collision.checkX() + + def checkEntityCollision(self): + for ent in self.levelObj.entityList: + collisionState = self.EntityCollider.check(ent) + if collisionState.isColliding: + if ent.type == "Item": + self._onCollisionWithItem(ent) + elif ent.type == "Block": + self._onCollisionWithBlock(ent) + elif ent.type == "Mob": + self._onCollisionWithMob(ent, collisionState) + + def _onCollisionWithItem(self, item): + self.levelObj.entityList.remove(item) + self.dashboard.points += 100 + self.dashboard.coins += 1 + self.sound.play_sfx(self.sound.coin) + + def _onCollisionWithBlock(self, block): + if not block.triggered: + self.dashboard.coins += 1 + self.sound.play_sfx(self.sound.bump) + block.triggered = True + + def _onCollisionWithMob(self, mob, collisionState): + is_koopa = isinstance(mob, Koopa) + + # Handle item-like mobs first (e.g., Mushrooms) + if isinstance(mob, RedMushroom) and mob.alive: + self.powerup(1) + self.killEntity(mob) # Removes mushroom + self.sound.play_sfx(self.sound.powerup) + return # Collision handled + + # Top collision (stomp) + if collisionState.isTop: + if mob.alive or mob.bouncing: # Stomping a live, moving mob or an already bouncing shell + self.sound.play_sfx(self.sound.stomp) + self.rect.bottom = mob.rect.top + self.bounce() + self.killEntity(mob) # For Koopa, makes it a still shell. For others, typically sets alive=False. + elif mob.alive and not mob.active: # Stomping a shell that is ALREADY still (e.g., koopa.active is false) + self.sound.play_sfx(self.sound.stomp) + self.rect.bottom = mob.rect.top + self.bounce() + if is_koopa: + mob.bouncing = True # Make the still shell start bouncing + # Determine kick direction based on Mario's center relative to shell's center + if self.rect.centerx < mob.rect.centerx: # Mario is to the left of shell's center + mob.leftrightTrait.direction = 1 # Shell moves right + else: # Mario is to the right of shell's center (or exactly centered) + mob.leftrightTrait.direction = -1 # Shell moves left + mob.leftrightTrait.speed = 4 # Standard shell speed + else: + mob.alive = False # For non-Koopa mobs that are stompable when still + return # Collision handled + + # Side collision (isColliding is true, but isTop is false) + if collisionState.isColliding: + # Special handling for kicking a still Koopa shell + if is_koopa and mob.alive and not mob.active and not mob.bouncing: + self.sound.play_sfx(self.sound.kick) + mob.bouncing = True + mob.leftrightTrait.speed = 4 # Standard shell speed + # Determine kick direction based on Mario's center relative to shell's center + if self.rect.centerx < mob.rect.centerx: # Mario is to the left of shell's center + mob.leftrightTrait.direction = 1 # Shell moves right + else: # Mario is to the right of shell's center (or exactly centered) + mob.leftrightTrait.direction = -1 # Shell moves left + + # Nudge the shell slightly to prevent immediate re-collision due to Mario's momentum + mob.rect.x += mob.leftrightTrait.direction * 8 # Increased nudge distance + + # Generic collision with a live, dangerous mob (includes active/bouncing shells or other mobs) + # This block is reached if it's not a kickable Koopa shell, or if it's another type of mob. + elif mob.alive and not self.invincibilityFrames: + if self.powerUpState == 0: # Small Mario + self.gameOver() + else: # Big Mario + self.powerUpState = 0 + # Assuming smallAnimation is defined globally or accessible (like smallAnimation from the top of Mario.py) + self.traits['goTrait'].updateAnimation(smallAnimation) + self.rect.height = 32 # Adjust height + self.rect.y += 32 # Adjust y-position due to height change from top + self.invincibilityFrames = 60 # Brief invincibility after power down + self.sound.play_sfx(self.sound.pipe) + # Note: If invincibilityFrames > 0, Mario doesn't die or power down. Implicitly handled. + + def bounce(self): + self.traits["bounceTrait"].jump = True + + def killEntity(self, ent): + if ent.__class__.__name__ != "Koopa": + ent.alive = False + else: + ent.timer = 0 + ent.leftrightTrait.speed = 1 + ent.alive = True + ent.active = False + ent.bouncing = False + self.dashboard.points += 100 + + def gameOver(self): + srf = pygame.Surface((640, 480)) + srf.set_colorkey((255, 255, 255), pygame.RLEACCEL) + srf.set_alpha(128) + self.sound.music_channel.stop() + self.sound.music_channel.play(self.sound.death) + + for i in range(500, 20, -2): + srf.fill((0, 0, 0)) + pygame.draw.circle( + srf, + (255, 255, 255), + (int(self.camera.x + self.rect.x) + 16, self.rect.y + 16), + i, + ) + self.screen.blit(srf, (0, 0)) + pygame.display.update() + self.input.checkForInput() + while self.sound.music_channel.get_busy(): + pygame.display.update() + self.input.checkForInput() + self.restart = True + + def getPos(self): + return self.camera.x + self.rect.x, self.rect.y + + def setPos(self, x, y): + self.rect.x = x + self.rect.y = y + + def powerup(self, powerupID): + if self.powerUpState == 0: + if powerupID == 1: + self.powerUpState = 1 + self.traits['goTrait'].updateAnimation(bigAnimation) + self.rect = pygame.Rect(self.rect.x, self.rect.y-32, 32, 64) + self.invincibilityFrames = 20 From 8471538a96fabb47111d2679da75693c2545b3ba Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 25 May 2025 03:00:27 +0000 Subject: [PATCH 2/2] Feat: Add three new levels (Level1-3, Level1-4, Level1-5) Three new levels have been added to the game: - Level1-3.json - Level1-4.json - Level1-5.json These levels introduce varied designs, platforming challenges, and enemy placements. The game's menu system dynamically loads levels from the 'levels' directory, so these new levels will be available for selection automatically. --- levels/Level1-3.json | 63 +++++++++++++++++++++++++++++++++++ levels/Level1-4.json | 72 ++++++++++++++++++++++++++++++++++++++++ levels/Level1-5.json | 78 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 levels/Level1-3.json create mode 100644 levels/Level1-4.json create mode 100644 levels/Level1-5.json diff --git a/levels/Level1-3.json b/levels/Level1-3.json new file mode 100644 index 00000000..01a55a27 --- /dev/null +++ b/levels/Level1-3.json @@ -0,0 +1,63 @@ +{ + "id": 2, + "length": 70, + "level": { + "objects": { + "bush": [ + [5, 12], [25, 12], [50, 12], [65,12] + ], + "sky": [], + "cloud": [ + [8, 4], [22, 3], [35, 5], [52, 3], [63,4] + ], + "pipe": [ + [15, 10, 4], + [30, 12, 2], + [45, 11, 3] + ], + "ground": [ + [0,9],[1,9],[2,9], + [10,9],[11,9], + [20,10],[21,10],[22,10], + [38,9],[39,9],[40,9], + [50,10],[51,10], + [60,9],[61,9],[62,9],[63,9],[64,9],[65,9],[66,9],[67,9],[68,9],[69,9] + ] + }, + "layers": { + "sky": { + "x": [0, 70], + "y": [0, 13] + }, + "ground": { + "x": [0, 70], + "y": [14, 16] + } + }, + "entities": { + "CoinBox": [ + [10, 6], + [39, 6] + ], + "coinBrick": [ + [21,7] + ], + "coin": [ + [20,7],[22,7], + [50,7],[51,7] + ], + "Goomba": [ + [11, 14], + [25, 14], + [40, 14], + [55, 14] + ], + "Koopa": [ + [62, 14] + ], + "RandomBox": [ + [4, 6, "RedMushroom"] + ] + } + } +} diff --git a/levels/Level1-4.json b/levels/Level1-4.json new file mode 100644 index 00000000..283a50f5 --- /dev/null +++ b/levels/Level1-4.json @@ -0,0 +1,72 @@ +{ + "id": 3, + "length": 65, + "level": { + "objects": { + "bush": [ + [3, 12], [22, 12], [40, 12], [58, 12] + ], + "sky": [], + "cloud": [ + [6, 3], [18, 5], [30, 2], [45, 4], [55, 3] + ], + "pipe": [ + [12, 11, 3], + [35, 9, 5], + [50, 12, 2] + ], + "ground": [ + [0,9],[1,9],[2,9],[3,9], + [8,10],[9,10], + [10,9],[11,9], + [18,10],[19,10],[20,10],[21,10], + [25,9],[26,9],[27,9], + [26,8],[27,8], + [27,7], + [40,10],[41,10], + [42,9],[43,9], + [55,9],[56,9],[57,9],[58,9],[59,9],[60,9],[61,9],[62,9],[63,9],[64,9] + ] + }, + "layers": { + "sky": { + "x": [0, 65], + "y": [0, 13] + }, + "ground": { + "x": [0, 65], + "y": [14, 16] + } + }, + "entities": { + "CoinBox": [ + [2,6], + [26,4], + [42,6] + ], + "coinBrick": [ + [9,7], + [19,7] + ], + "coin": [ + [8,7],[10,6], + [18,7],[20,7], + [25,6],[27,4], + [57,7],[58,7],[59,7] + ], + "Goomba": [ + [3,14], + [19,14],[20,14], + [41,14], + [56,14],[57,14] + ], + "Koopa": [ + [27,14], + [45,14] + ], + "RandomBox": [ + [1, 6, "RedMushroom"] + ] + } + } +} diff --git a/levels/Level1-5.json b/levels/Level1-5.json new file mode 100644 index 00000000..76727713 --- /dev/null +++ b/levels/Level1-5.json @@ -0,0 +1,78 @@ +{ + "id": 4, + "length": 75, + "level": { + "objects": { + "bush": [ + [10, 12], [30, 12], [55, 12] + ], + "sky": [], + "cloud": [ + [5, 5], [15, 3], [28, 6], [40, 2], [50, 4], [65, 3] + ], + "pipe": [ + [12, 10, 4], + [25, 8, 6], + [45, 11, 3], + [60, 9, 5] + ], + "ground": [ + [0,9],[1,9],[2,9], + [8,10], + [16,9],[17,9], + [22,10], + [30,9],[31,9], + [36,10], + [42,9], + [50,10],[51,10], + [56,9], + [65,10],[66,10],[67,10],[68,10],[69,10],[70,10],[71,10],[72,10],[73,10],[74,10] + ] + }, + "layers": { + "sky": { + "x": [0, 75], + "y": [0, 13] + }, + "ground": { + "x": [0, 75], + "y": [14, 16] + } + }, + "entities": { + "CoinBox": [ + [2,6] + ], + "coinBrick": [ + [31,6], + [51,7] + ], + "coin": [ + [8,8], + [16,7],[17,7], + [22,8], + [30,7], + [36,8], + [42,7], + [50,8],[56,7], + [65,8],[66,8],[67,8] + ], + "Goomba": [ + [17,14], + [38,14], + [52,14], + [66,14],[67,14] + ], + "Koopa": [ + [23,14], + [43,14], + [57,14], + [70,14] + ], + "RandomBox": [ + [1, 6, "RedMushroom"], + [62, 6, "RedMushroom"] + ] + } + } +}