diff --git a/TATALETAK.txt b/TATALETAK.txt new file mode 100644 index 00000000..4f9b02b5 --- /dev/null +++ b/TATALETAK.txt @@ -0,0 +1,50 @@ +├──super-mario-python/ # root project +│ +├── README.md # dokumentasi dasar: cara running, dependensi, struktur +├── requirements.txt # daftar dependensi (pygame, etc) +├── .gitignore # file / folder yang di-ignore git (misalnya env, __pycache__, build, dsb) +├── setup.py (opsional) # jika kamu ingin struktur paket / distribusi +│ +├── src/ # kode utama sebagai package +│ ├── __main__.py # entry-point ketika menjalankan `python -m src` atau `python src/` +│ ├── game/ # modul utama game +│ │ ├── __init__.py +│ │ ├── main.py # game loop, init, state switching +│ │ ├── config.py # konfigurasi global (screen size, FPS, tile size, dsb) +│ │ ├── state_manager.py # jika kamu mengelola berbagai state: menu, playing, pause, game over +│ │ +│ ├── entities/ # kelas / definisi objek (player, enemy, item, dsb) +│ │ ├── __init__.py +│ │ ├── player.py +│ │ ├── enemy.py +│ │ └── ... +│ │ +│ ├── levels/ # definisi level / data level / loader level +│ │ ├── level1.py (atau level1.json / txt) +│ │ └── ... +│ │ +│ ├── rendering/ # kode berkaitan dengan gambar / drawing / camera / viewport +│ │ ├── __init__.py +│ │ ├── renderer.py +│ │ └── camera.py +│ │ +│ ├── utils/ # utilitas umum: helper functions, collision, math, dsb +│ │ ├── __init__.py +│ │ └── helper.py +│ │ +│ └── audio/ # pengaturan musik / sound effects +│ ├── __init__.py +│ └── sound_manager.py +│ +├── assets/ # semua resource: sprites, images, sounds, levels data +│ ├── sprites/ # sprite sheet, karakter, musuh, animasi +│ ├── images/ # background, tiles, UI, dsb +│ ├── sounds/ # efek suara, musik latar +│ └── levels/ # data level (tile map, layout, dsb) jika format external +│ +├── tests/ # (opsional tapi disarankan) unit / integration tests +│ ├── __init__.py +│ └── test_*.py +│ +└── docs/ # dokumentasi tambahan: design doc, panduan kontribusi, dsb + └── ... diff --git a/classes/Dashboard.py b/classes/Dashboard.py index aeb36e76..2753c7d4 100644 --- a/classes/Dashboard.py +++ b/classes/Dashboard.py @@ -13,8 +13,15 @@ def __init__(self, filePath, size, screen): self.coins = 0 self.ticks = 0 self.time = 0 + self.font = pygame.font.Font(None, 32) + self.font = pygame.font.Font(None, 32) + self.mario = None def update(self): + if self.mario: + lives_text = self.font.render(f"LIVES {self.mario.lives}", True, (255, 255, 255)) + self.screen.blit(lives_text, (0, 0)) + self.drawText("MARIO", 50, 20, 15) self.drawText(self.pointString(), 50, 37, 15) diff --git a/classes/Level.py b/classes/Level.py index 9283f6e3..6d47c0c7 100644 --- a/classes/Level.py +++ b/classes/Level.py @@ -10,7 +10,7 @@ from entities.Koopa import Koopa from entities.CoinBox import CoinBox from entities.RandomBox import RandomBox - +from entities.FireFlower import FireFlower class Level: def __init__(self, screen, sound, dashboard): @@ -38,6 +38,7 @@ def loadEntities(self, data): [self.addCoin(x, y) for x, y in data["level"]["entities"]["coin"]] [self.addCoinBrick(x, y) for x, y in data["level"]["entities"]["coinBrick"]] [self.addRandomBox(x, y, item) for x, y, item in data["level"]["entities"]["RandomBox"]] + [self.addFireFlower(x, y) for x, y in data["level"]["entities"].get("FireFlower", [])] except: # if no entities in Level pass @@ -203,3 +204,11 @@ def addRedMushroom(self, x, y): self.entityList.append( RedMushroom(self.screen, self.sprites.spriteCollection, x, y, self, self.sound) ) + def addFireFlower(self, x, y): + from entities.FireFlower import FireFlower + flower = FireFlower(self.screen, self.sprites.spriteCollection, x, y, self, self.sound) + self.entityList.append(flower) # wajib append ke entityList + + + + diff --git a/classes/Sprites.py b/classes/Sprites.py index 9e48020c..3e4cff27 100644 --- a/classes/Sprites.py +++ b/classes/Sprites.py @@ -15,7 +15,8 @@ def __init__(self): "./sprites/Animations.json", "./sprites/BackgroundSprites.json", "./sprites/ItemAnimations.json", - "./sprites/RedMushroom.json" + "./sprites/RedMushroom.json", + "./sprites/FireFlower.json", ] ) diff --git a/entities/CoinBox.py b/entities/CoinBox.py index e35d2b82..1ca76ba7 100644 --- a/entities/CoinBox.py +++ b/entities/CoinBox.py @@ -24,7 +24,14 @@ def update(self, cam): self.animation.update() else: self.animation.image = self.spriteCollection.get("empty").image - self.item.spawnCoin(cam, self.sound, self.dashboard) + # Jika CoinBox mengandung FireFlower + if hasattr(self, "contains") and self.contains == "fireflower": + from entities.Item import Item + fireflower = Item("FireFlower", self.rect.x, self.rect.y - 32) + self.screen.game.addEntity(fireflower) + else: + self.item.spawnCoin(cam, self.sound, self.dashboard) + if self.time < self.maxTime: self.time += 1 self.rect.y -= self.vel diff --git a/entities/FireFlower.py b/entities/FireFlower.py new file mode 100644 index 00000000..ea2e060b --- /dev/null +++ b/entities/FireFlower.py @@ -0,0 +1,33 @@ +from entities.EntityBase import EntityBase +from classes.Collider import Collider +from classes.EntityCollider import EntityCollider +from classes.Animation import Animation +from entities.EntityBase import EntityBase +from classes.Maths import Vec2D + +class FireFlower(EntityBase): + def __init__(self, screen, spriteColl, x, y, level, sound): + super(FireFlower, self).__init__(y, x , 0) + self.spriteCollection = spriteColl + self.animation = self.spriteCollection.get("FireFlower").animation + self.screen = screen + self.collision = Collider(self, level) + self.EntityCollider = EntityCollider(self) + self.levelObj = level + self.type = "item" + self.dashboard = level.dashboard + self.sound = sound + + def update(self, camera): + if self.alive: + self.drawFireFlower(camera) + else: + self.alive = None + + def drawFireFlower(self, camera): + self.screen.blit(self.animation.image, (self.rect.x + camera.x, self.rect.y)) + self.animation.update() + + + + \ No newline at end of file diff --git a/entities/Flower.py b/entities/Flower.py new file mode 100644 index 00000000..3bc457a0 --- /dev/null +++ b/entities/Flower.py @@ -0,0 +1,10 @@ +import pygame + +class Flower(pygame.sprite.Sprite): + def __init__(self, x, y, image): + super().__init__() + self.image = image + self.rect = self.image.get_rect(topleft=(x, y)) + + def update(self): + pass diff --git a/entities/Mario.py b/entities/Mario.py index 8321284c..8740facd 100644 --- a/entities/Mario.py +++ b/entities/Mario.py @@ -13,7 +13,11 @@ from traits.jump import JumpTrait from classes.Pause import Pause +# ------------------------------------------------- +# ANIMASI GLOBAL +# ------------------------------------------------- spriteCollection = Sprites().spriteCollection + smallAnimation = Animation( [ spriteCollection["mario_run1"].image, @@ -23,6 +27,7 @@ spriteCollection["mario_idle"].image, spriteCollection["mario_jump"].image, ) + bigAnimation = Animation( [ spriteCollection["mario_big_run1"].image, @@ -33,17 +38,44 @@ spriteCollection["mario_big_jump"].image, ) +putihAnimation = Animation( + [ + spriteCollection["mario_putih_run1"].image, + spriteCollection["mario_putih_run2"].image, + spriteCollection["mario_putih_run3"].image, + ], + spriteCollection["mario_putih_idle"].image, + spriteCollection["mario_putih_jump"].image, +) + + +# ============================================================ +# M A R I O C L A S S +# ============================================================ class Mario(EntityBase): + def __init__(self, x, y, level, screen, dashboard, sound, gravity=0.8): super(Mario, self).__init__(x, y, gravity) + + # Spawn checkpoint + self.spawn_x = x + self.spawn_y = y + + self.lives = 3 + self.powerup = None + self.dead = False + 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 + + # Traits self.traits = { "jumpTrait": JumpTrait(self), "goTrait": GoTrait(smallAnimation, screen, self.camera, self), @@ -52,16 +84,22 @@ def __init__(self, x, y, level, screen, dashboard, sound, gravity=0.8): self.levelObj = level self.collision = Collider(self, level) - self.screen = screen self.EntityCollider = EntityCollider(self) + + self.screen = screen self.dashboard = dashboard self.restart = False + self.pause = False self.pauseObj = Pause(screen, self, dashboard) + # ---------------------------------------------------------- + # UPDATE + # ---------------------------------------------------------- def update(self): if self.invincibilityFrames > 0: self.invincibilityFrames -= 1 + self.updateTraits() self.moveMario() self.camera.move() @@ -69,120 +107,182 @@ def update(self): self.checkEntityCollision() self.input.checkForInput() + # Save checkpoint + self.spawn_x = self.rect.x + self.spawn_y = self.rect.y + + # ---------------------------------------------------------- + # MOVEMENT + # ---------------------------------------------------------- def moveMario(self): self.rect.y += self.vel.y self.collision.checkY() + self.rect.x += self.vel.x self.collision.checkX() + # ---------------------------------------------------------- + # ENTITY COLLISION + # ---------------------------------------------------------- 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) + for ent in self.levelObj.entityList[:]: + state = self.EntityCollider.check(ent) + + if not state.isColliding: + continue + + if ent.type.lower() == "item": + self._onCollisionWithItem(ent) + + elif ent.type == "Block": + self._onCollisionWithBlock(ent) + elif ent.type == "Mob": + self._onCollisionWithMob(ent, state) + + # ---------------------------------------------------------- + # ITEM COLLISION + # ---------------------------------------------------------- def _onCollisionWithItem(self, item): - self.levelObj.entityList.remove(item) - self.dashboard.points += 100 - self.dashboard.coins += 1 - self.sound.play_sfx(self.sound.coin) + # Hindari item diambil 2x + if hasattr(item, "taken") and item.taken: + return + item.taken = True + + item.alive = False + + # mainkan suara (jika ada) + try: + item.sound.itemSound.play() + except: + pass + + cls = item.__class__.__name__ + + # FIRE FLOWER → Mario Putih + if cls == "FireFlower": + self.get_powerup("fireflower") + return + + # RED MUSHROOM + if cls == "RedMushroom": + self.get_powerup("mushroom") + return + + # COIN + if cls == "Coin": + self.dashboard.points += 100 + self.dashboard.coins += 1 + self.sound.play_sfx(self.sound.coin) + return + + # ---------------------------------------------------------- + # BLOCK COLLISION + # ---------------------------------------------------------- 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): + # ---------------------------------------------------------- + # MOB COLLISION + # ---------------------------------------------------------- + def _onCollisionWithMob(self, mob, state): if isinstance(mob, RedMushroom) and mob.alive: - self.powerup(1) + self.get_powerup("mushroom") self.killEntity(mob) self.sound.play_sfx(self.sound.powerup) - elif collisionState.isTop and (mob.alive or mob.bouncing): + + elif state.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: + + elif state.isColliding and mob.alive and not self.invincibilityFrames: if self.powerUpState == 0: - self.gameOver() - elif self.powerUpState == 1: + self.die() + else: 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) + + bottom = self.rect.bottom + self.rect = pygame.Rect(self.rect.x, bottom - 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 + ent.alive = 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 + # ---------------------------------------------------------- + # POWER UP SYSTEM + # ---------------------------------------------------------- + def get_powerup(self, powerup_type): + powerup_type = powerup_type.lower() + bottom = self.rect.bottom + + # Jamur → Mario Besar + if powerup_type == "mushroom": + self.powerup = "mushroom" + self.traits['goTrait'].updateAnimation(bigAnimation) + self.powerUpState = 1 + + self.rect = pygame.Rect(self.rect.x, bottom - 64, 32, 64) + self.invincibilityFrames = 20 + self.sound.play_sfx(self.sound.powerup) + + # Fire Flower → Mario Putih + elif powerup_type in ("fireflower", "fire"): + self.powerup = "fireflower" + self.traits['goTrait'].updateAnimation(putihAnimation) + self.powerUpState = 1 + + self.rect = pygame.Rect(self.rect.x, bottom - 64, 32, 64) + self.invincibilityFrames = 20 + self.sound.play_sfx(self.sound.powerup) + + # ---------------------------------------------------------- + # LIFE & DEATH + # ---------------------------------------------------------- + def die(self): + if not self.dead: + self.dead = True + self.lives -= 1 + self.sound.play_sfx(self.sound.stomp) + + if self.lives <= 0: + self.gameOver() + else: + self.invincibilityFrames = 60 + self.respawn() + + # ---------------------------------------------------------- + def respawn(self): + self.rect.x = self.spawn_x - 250 + self.rect.y = self.spawn_y - 500 + + self.vel.x = 0 + self.vel.y = 0 + + self.camera.x = max(self.spawn_x - 200, 0) + self.camera.target_rect = self.rect + + self.dead = False + # ---------------------------------------------------------- + # POSITION UTILITY + # ---------------------------------------------------------- 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/entities/RandomBox.py b/entities/RandomBox.py index 86dfa85f..c2f4893b 100644 --- a/entities/RandomBox.py +++ b/entities/RandomBox.py @@ -1,8 +1,6 @@ from copy import copy - from entities.EntityBase import EntityBase - class RandomBox(EntityBase): def __init__(self, screen, spriteCollection, x, y, item, sound, dashboard, level, gravity=0): super(RandomBox, self).__init__(x, y, gravity) @@ -19,15 +17,26 @@ def __init__(self, screen, spriteCollection, x, y, item, sound, dashboard, level self.item = item self.level = level + # simpan posisi asli blok untuk spawn item + self.original_x = x + 1 + self.original_y = y + 1 + def update(self, cam): if self.alive and not self.triggered: self.animation.update() else: self.animation.image = self.spriteCollection.get("empty").image + if self.item == 'RedMushroom': self.level.addRedMushroom(self.rect.y // 32 - 1, self.rect.x // 32) self.sound.play_sfx(self.sound.powerup_appear) + + elif self.item == 'FireFlower': + self.level.addFireFlower(self.rect.y // 32 - 1, self.rect.x // 32) + self.sound.play_sfx(self.sound.powerup_appear) + self.item = None + if self.time < self.maxTime: self.time += 1 self.rect.y -= self.vel @@ -35,6 +44,7 @@ def update(self, cam): if self.time < self.maxTime * 2: self.time += 1 self.rect.y += self.vel + self.screen.blit( self.spriteCollection.get("sky").image, (self.rect.x + cam.x, self.rect.y + 2), diff --git a/levels/Level1-3.json b/levels/Level1-3.json new file mode 100644 index 00000000..d28b3f32 --- /dev/null +++ b/levels/Level1-3.json @@ -0,0 +1,70 @@ +{ + "id": 1, + "length": 80, + "level": { + "objects": { + "bush": [ + [2, 12], [17, 12], [24, 12], + [34, 12], [40, 12], [44, 12], + [60, 12] + ], + "sky": [ + [48, 13], [49, 13], + [48, 14], [49, 14] + ], + "cloud": [ + [5, 5], [13, 3], [26, 5], + [32, 3], [42, 6], [55, 4], + [65, 3], [70, 5] + ], + "pipe": [ + [8, 10, 4], [12, 12, 4], [22, 12, 4], + [29, 9, 6], [45, 10, 5], [62, 12, 4] + ], + "ground": [ + [40, 9], [41, 9], [42, 9], [43, 9], + [44, 9], [45, 9], [46, 9], [47, 9], + [50, 9], [51, 9], [52, 9], [53, 9], + [54, 9], [55, 9], [54, 8], [55, 8], + [56, 8], [57, 8], [58, 8], [59, 8], + [60, 10], [61, 10], [62, 10], [63, 10], + [70, 8], [71, 8], [72, 8], + + [38, 9], [39, 9] + ] + }, + + "layers": { + "sky": { "x": [0, 80], "y": [0, 13] }, + "ground": { "x": [0, 80], "y": [14, 16] } + }, + + "entities": { + "CoinBox": [ + [3, 8], [15, 7], [35, 6], [52, 5], [70, 4] + ], + "coinBrick": [ + [37, 9], [4, 8], [20, 8], [21, 8], [22, 8] + ], + "coin": [ + [18, 7], [19, 7], [20, 7], [21, 7], + [30, 6], [31, 6], [32, 6], + [55, 9], [56, 9], [57, 9], + [75, 6], [76, 6], [77, 6], [78, 6] + ], + "Goomba": [ + [10, 13], [18, 13], [25, 13], + [30, 13], [43, 13], [50, 13], + [68, 13] + ], + "Koopa": [ + [12, 13], [28, 13], [45, 13], + [62, 13], [72, 13] + ], + "RandomBox": [ + [5, 8, "RedMushroom"], + [5, 4, "FireFlower"] + ] + } + } +} diff --git a/main.py b/main.py index 9a2aba34..d1849ee7 100644 --- a/main.py +++ b/main.py @@ -14,27 +14,43 @@ def main(): pygame.init() screen = pygame.display.set_mode(windowSize) max_frame_rate = 60 + + # Buat dashboard, sound, level, menu dashboard = Dashboard("./img/font.png", 8, screen) sound = Sound() level = Level(screen, sound, dashboard) menu = Menu(screen, dashboard, level, sound) + # Tunggu menu start while not menu.start: menu.update() + # Buat Mario dulu mario = Mario(0, 0, level, screen, dashboard, sound) + + # Hubungkan Mario ke level dan dashboard + level.mario = mario + dashboard.mario = mario + dashboard.level = level + clock = pygame.time.Clock() + # Loop utama game while not mario.restart: - pygame.display.set_caption("Super Mario running with {:d} FPS".format(int(clock.get_fps()))) + pygame.display.set_caption( + "Super Mario running with {:d} FPS".format(int(clock.get_fps())) + ) + if mario.pause: mario.pauseObj.update() else: level.drawLevel(mario.camera) dashboard.update() mario.update() + pygame.display.update() clock.tick(max_frame_rate) + return 'restart' diff --git a/requirements.txt b/requirements.txt index 30c14272..7301819e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -pygame==2.0.0.dev10 -scipy==1.4.1 +pygame>=2.5.0 +scipy>=1.10.0 +numpy>=1.21.0 \ No newline at end of file diff --git a/sprites/FireFlower.json b/sprites/FireFlower.json new file mode 100644 index 00000000..db09569e --- /dev/null +++ b/sprites/FireFlower.json @@ -0,0 +1,17 @@ +{ + "spriteSheetURL": "./img/Items.png", + "type":"animation", + "sprites":[ + { + "name": "FireFlower", + "images": [ + { "x": 1, "y": 2, "scale": 2 }, + { "x": 2, "y": 2, "scale": 2 }, + { "x": 3, "y": 2, "scale": 2 } + ], + "deltaTime": 10, + "colorKey": -1 + } + ] +} + diff --git a/sprites/Mario.json b/sprites/Mario.json index a9fe7e09..2430dc7f 100644 --- a/sprites/Mario.json +++ b/sprites/Mario.json @@ -119,7 +119,67 @@ "collision":false, "xsize":16, "ysize":32 + }, + { + "name":"mario_putih_idle", + "x":259, + "y":122, + "scalefactor":2, + "colorKey":-1, + "collision":false, + "xsize":16, + "ysize":32 + }, + { + "name":"mario_putih_run1", + "x":296, + "y":122, + "scalefactor":2, + "colorKey":-1, + "collision":false, + "xsize":16, + "ysize":32 + }, + { + "name":"mario_putih_run2", + "x":315, + "y":122, + "scalefactor":2, + "colorKey":-1, + "collision":false, + "xsize":16, + "ysize":32 + }, + { + "name":"mario_putih_run3", + "x":332, + "y":122, + "scalefactor":2, + "colorKey":-1, + "collision":false, + "xsize":16, + "ysize":32 + }, + { + "name":"mario_putih_jump", + "x":369, + "y":122, + "scalefactor":2, + "colorKey":-1, + "collision":false, + "xsize":16, + "ysize":32 + }, + { + "name":"mario_putih_break", + "x":369, + "y":122, + "scalefactor":2, + "colorKey":-1, + "collision":false, + "xsize":16, + "ysize":32 } - ] + ] }