diff --git a/game/sim/player.py b/game/sim/player.py new file mode 100644 index 0000000..516b3de --- /dev/null +++ b/game/sim/player.py @@ -0,0 +1,231 @@ +from .. import constants +from .entity import Actor +from .geometry import sign, approach +from . import physics +from .inventory import Inventory + + +class Player(Actor): + def __init__(self, x, y): + super().__init__(x, y, 12, 16) + self.maxHp = constants.playerMaxHp + self.hp = self.maxHp + self.iframes = 0 + self.coyoteTimer = 0 + self.jumpBufferTimer = 0 + self.airJumps = constants.maxAirJumps + self.dashTimer = 0 + self.dashCooldown = 0 + self.wallStickTimer = 0 + self.wallSliding = False + self.onLadder = False + self.jumpHeldPrev = False + self.inventory = Inventory() + self.score = 0 + self.coins = 0 + self.spawnX = float(x) + self.spawnY = float(y) + + def hurtBox(self): + return self.aabb.inflated(-1.0, 0.0) + + def isInvulnerable(self): + return self.iframes > 0 or self.dashTimer > 0 + + def setCheckpoint(self, x, y): + self.spawnX = float(x) + self.spawnY = float(y) + + def respawn(self): + self.x = self.spawnX + self.y = self.spawnY + self.vx = 0.0 + self.vy = 0.0 + self.iframes = constants.iframesOnHit + self.dashTimer = 0 + self.alive = True + + def takeDamage(self, amount, sourceX, world): + if self.isInvulnerable() or not self.alive: + return False + self.hp -= amount + self.iframes = constants.iframesOnHit + away = sign(self.centerX - sourceX) or self.facing + self.vx = constants.knockbackX * away + self.vy = constants.knockbackY + self.dashTimer = 0 + world.playSound("hurt", self.centerX) + world.spawnBurst("hurt", self.centerX, self.centerY, 8) + if self.hp <= 0: + self.hp = 0 + self.alive = False + world.playSound("die", self.centerX) + return True + + def heal(self, amount): + self.hp = min(self.maxHp, self.hp + amount) + + def update(self, world, command): + if not self.alive: + return + if self.iframes > 0: + self.iframes -= 1 + if self.dashCooldown > 0: + self.dashCooldown -= 1 + + terrain = world.level + platforms = world.platforms + + self.updateLadder(world, command, terrain) + if self.onLadder: + self.moveOnLadder(world, command, terrain, platforms) + return + + self.applyHorizontal(command) + self.updateTimers(command, terrain, platforms) + self.handleDash(world, command) + if self.dashTimer > 0: + self.runDash(world, terrain, platforms) + return + self.handleJump(world, command, platforms) + self.applyGravity(command) + self.applyWallSlide(command, terrain, platforms) + + physics.moveHorizontal(self, terrain, platforms) + wasOnGround = self.onGround + physics.moveVertical(self, terrain, platforms, dropThrough=self.wantsDropThrough(command)) + physics.resolveSlope(self, terrain, wasOnGround) + + if self.onGround: + self.airJumps = constants.maxAirJumps + self.coyoteTimer = constants.coyoteFrames + self.jumpHeldPrev = command.jumpHeld + + def wantsDropThrough(self, command): + return command.down and command.jump + + def applyHorizontal(self, command): + axis = command.moveAxis + topSpeed = constants.runSpeed if command.run else constants.moveSpeed + accel = constants.moveAccel if self.onGround else constants.airAccel + if axis != 0: + self.vx += accel * axis + self.facing = axis + if self.vx > topSpeed: + self.vx = topSpeed + if self.vx < -topSpeed: + self.vx = -topSpeed + else: + damping = constants.friction if self.onGround else constants.airFriction + self.vx *= damping + if abs(self.vx) < 0.1: + self.vx = 0.0 + + def updateTimers(self, command, terrain, platforms): + if self.coyoteTimer > 0: + self.coyoteTimer -= 1 + if self.jumpBufferTimer > 0: + self.jumpBufferTimer -= 1 + if command.jump and not command.down: + self.jumpBufferTimer = constants.jumpBufferFrames + if self.wallStickTimer > 0: + self.wallStickTimer -= 1 + + def handleDash(self, world, command): + if command.dash and self.dashCooldown == 0 and self.dashTimer == 0: + direction = command.moveAxis or self.facing + self.facing = direction + self.dashTimer = constants.dashFrames + self.dashCooldown = constants.dashCooldownFrames + self.vx = constants.dashSpeed * direction + self.vy = 0.0 + world.playSound("dash", self.centerX) + world.spawnBurst("dash", self.centerX, self.centerY, 6) + + def runDash(self, world, terrain, platforms): + self.dashTimer -= 1 + self.vy = 0.0 + physics.moveHorizontal(self, terrain, platforms) + wasOnGround = self.onGround + physics.moveVertical(self, terrain, platforms) + physics.resolveSlope(self, terrain, wasOnGround) + if self.onGround: + self.airJumps = constants.maxAirJumps + self.coyoteTimer = constants.coyoteFrames + + def handleJump(self, world, command, platforms): + if command.down: + return + touchLeft = physics.touchingSolidSide(self, world.level, platforms, -1) + touchRight = physics.touchingSolidSide(self, world.level, platforms, 1) + onWall = (touchLeft or touchRight) and not self.onGround + + if self.jumpBufferTimer > 0: + if self.onGround or self.coyoteTimer > 0: + self.vy = constants.jumpVelocity + self.jumpBufferTimer = 0 + self.coyoteTimer = 0 + world.playSound("jump", self.centerX) + world.spawnBurst("jump", self.centerX, self.bottom, 4) + elif onWall: + awayDir = 1 if touchLeft else -1 + self.vx = constants.wallJumpVelocityX * awayDir + self.vy = constants.wallJumpVelocityY + self.facing = awayDir + self.jumpBufferTimer = 0 + self.wallStickTimer = 0 + world.playSound("jump", self.centerX) + elif self.airJumps > 0: + self.vy = constants.doubleJumpVelocity + self.airJumps -= 1 + self.jumpBufferTimer = 0 + world.playSound("doubleJump", self.centerX) + world.spawnBurst("jump", self.centerX, self.centerY, 6) + + if self.jumpHeldPrev and not command.jumpHeld and self.vy < 0: + self.vy *= constants.jumpCutMultiplier + + def applyGravity(self, command): + pull = constants.gravity + cap = constants.fastFallSpeed if command.down and not self.onGround else constants.maxFallSpeed + self.vy += pull + if self.vy > cap: + self.vy = cap + + def applyWallSlide(self, command, terrain, platforms): + self.wallSliding = False + if self.onGround or self.vy <= 0: + return + touchLeft = physics.touchingSolidSide(self, terrain, platforms, -1) + touchRight = physics.touchingSolidSide(self, terrain, platforms, 1) + pressingIntoWall = (command.left and touchLeft) or (command.right and touchRight) + if pressingIntoWall: + self.wallSliding = True + self.wallStickTimer = constants.wallStickFrames + if self.vy > constants.wallSlideSpeed: + self.vy = constants.wallSlideSpeed + + def updateLadder(self, world, command, terrain): + onLadderTile = physics.overlapsLadder(self, terrain) + if not onLadderTile: + self.onLadder = False + return + if command.up or command.down: + self.onLadder = True + if self.onLadder and self.onGround and command.down and not onLadderTile: + self.onLadder = False + + def moveOnLadder(self, world, command, terrain, platforms): + climbSpeed = 1.4 + self.vy = command.verticalAxis * climbSpeed + self.vx = command.moveAxis * (constants.moveSpeed * 0.6) + if command.jump: + self.onLadder = False + self.vy = constants.jumpVelocity * 0.7 + return + physics.moveHorizontal(self, terrain, platforms) + physics.moveVertical(self, terrain, platforms) + if self.onGround and command.verticalAxis >= 0: + self.onLadder = False + if not physics.overlapsLadder(self, terrain): + self.onLadder = False