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 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"] + ] + } + } +}