diff --git a/CREDITS.txt b/CREDITS.txt new file mode 100644 index 0000000..3f90861 --- /dev/null +++ b/CREDITS.txt @@ -0,0 +1 @@ +Font used -> NicerNightie.ttf by unfilledflag \ No newline at end of file diff --git a/res/barrier/door.png b/res/barrier/door.png new file mode 100644 index 0000000..3d107fe Binary files /dev/null and b/res/barrier/door.png differ diff --git a/res/barrier/door.yaml b/res/barrier/door.yaml new file mode 100644 index 0000000..1da74b3 --- /dev/null +++ b/res/barrier/door.yaml @@ -0,0 +1,9 @@ +sprite: res/barrier/door.png +fx_sprite: none +scale_origin: [16, 16] +animations: +- name: "default" + loop: false + frames: + - clip: [0, 0, 80, 100] + duration: 1.0 \ No newline at end of file diff --git a/res/barrier/door_lock.png b/res/barrier/door_lock.png new file mode 100644 index 0000000..d0f0fbe Binary files /dev/null and b/res/barrier/door_lock.png differ diff --git a/res/barrier/exit.mp3 b/res/barrier/exit.mp3 new file mode 100644 index 0000000..8529cf8 Binary files /dev/null and b/res/barrier/exit.mp3 differ diff --git a/res/barrier/wood_break.mp3 b/res/barrier/wood_break.mp3 new file mode 100644 index 0000000..295779a Binary files /dev/null and b/res/barrier/wood_break.mp3 differ diff --git a/res/barrier/wood_hurt.mp3 b/res/barrier/wood_hurt.mp3 new file mode 100644 index 0000000..a6b5a6e Binary files /dev/null and b/res/barrier/wood_hurt.mp3 differ diff --git a/res/enemies/deadrockman.yaml b/res/enemies/deadrockman.yaml index 4783ccc..c02b0c3 100644 --- a/res/enemies/deadrockman.yaml +++ b/res/enemies/deadrockman.yaml @@ -1,4 +1,6 @@ sprite: res/enemies/rockman.png +fx_sprite: none +scale_origin: [16, 16] animations: - name: "default" loop: false diff --git a/res/enemies/rockman.png b/res/enemies/rockman.png index a26098d..d30901e 100644 Binary files a/res/enemies/rockman.png and b/res/enemies/rockman.png differ diff --git a/res/level1/map.png b/res/level1/map.png index c3fe80c..8801706 100644 Binary files a/res/level1/map.png and b/res/level1/map.png differ diff --git a/res/level1/map.yaml b/res/level1/map.yaml index 4525305..818c9b4 100644 --- a/res/level1/map.yaml +++ b/res/level1/map.yaml @@ -45,3 +45,6 @@ points: - name: rockman color: [255, 100, 50] + +- name: door + color: [0, 51, 255] diff --git a/res/level1/map_ent.png b/res/level1/map_ent.png index 0089fb6..c6995ac 100644 Binary files a/res/level1/map_ent.png and b/res/level1/map_ent.png differ diff --git a/res/tutorial/tut0.png b/res/tutorial/tut0.png new file mode 100644 index 0000000..2c2fd1a Binary files /dev/null and b/res/tutorial/tut0.png differ diff --git a/res/tutorial/tut00.png b/res/tutorial/tut00.png new file mode 100644 index 0000000..12bc570 Binary files /dev/null and b/res/tutorial/tut00.png differ diff --git a/res/tutorial/tut000.png b/res/tutorial/tut000.png new file mode 100644 index 0000000..b9d73e9 Binary files /dev/null and b/res/tutorial/tut000.png differ diff --git a/res/tutorial/tut1.png b/res/tutorial/tut1.png new file mode 100644 index 0000000..870b681 Binary files /dev/null and b/res/tutorial/tut1.png differ diff --git a/res/tutorial/tut2.png b/res/tutorial/tut2.png new file mode 100644 index 0000000..2d42c67 Binary files /dev/null and b/res/tutorial/tut2.png differ diff --git a/res/tutorial/tut3.png b/res/tutorial/tut3.png new file mode 100644 index 0000000..54fffe9 Binary files /dev/null and b/res/tutorial/tut3.png differ diff --git a/src/game/entities/barrier.nim b/src/game/entities/barrier.nim index a7d2f89..3e7d09a 100644 --- a/src/game/entities/barrier.nim +++ b/src/game/entities/barrier.nim @@ -14,23 +14,25 @@ const BARRIER_COLL = 42 type Barrier* = ref object health: float hurt_wav: WavHandle + break_wav: WavHandle sprites*: seq[AnimatedSprite] broken*: bool - phys_shape: Shape + phys_shape*: Shape + phys_space: Space user_data*: UserData # via physical impact at speed enemies_damage: bool objects_damage: bool min_energy: float -proc hurt(this: Barrier, energy: float) = - echo "Was hurt" +proc hurt(this: var Barrier, energy: float) = + discard this.hurt_wav.play_sound() + this.health -= (energy - this.min_energy) / 300000.0 + echo this.health -proc on_collide(this: Barrier, other: BodyKind, energy: float) = +proc on_collide*(this: var Barrier, other: BodyKind, energy: float) = if (other == bkEnemy and this.enemies_damage) or (other == bkObject and this.objects_damage): - if energy > 0: - echo "Collide with energy: " & $energy if energy > this.min_energy: this.hurt(energy) @@ -74,7 +76,7 @@ proc create_barrier(sprite: string, area: Vec4f, size: int, space: Space, id: in # The collider is a simple static segment let hs = size.toFloat * 0.5 - result.phys_shape = newSegmentShape(space.staticBody, v(area.x + hs, area.y + hs), v(area.z + hs, area.w + hs), 1) + result.phys_shape = newSegmentShape(space.staticBody, v(area.x + hs, area.y + hs), v(area.z + hs, area.w + hs), 8) result.user_data = make_barrier_userdata(id) result.phys_shape.userData = addr result.user_data # We sign up for collision callbacks between everything and barriers @@ -85,16 +87,27 @@ proc create_barrier(sprite: string, area: Vec4f, size: int, space: Space, id: in handler.postSolveFunc = barrier_handler handler.userData = barriers + result.phys_space = space + discard space.addShape(result.phys_shape) proc create_wooden_barrier*(area: Vec4f, size: int, space: Space, id: int, barriers: ptr seq[Barrier]): Barrier = result = create_barrier("res/level1/break_wood.yaml", area, size, space, id, barriers) result.health = 10.0 - result.min_energy = 1.0 + result.min_energy = 1000000.0 + result.enemies_damage = true + result.objects_damage = true + result.hurt_wav = load_sound("res/barrier/wood_hurt.mp3") + result.break_wav = load_sound("res/barrier/wood_break.mp3") proc update*(this: var Barrier) = for sprite in mitems(this.sprites): sprite.animate(dt) + if this.health < 0.0: + discard this.break_wav.play_sound() + this.phys_space.removeShape(this.phys_shape) + this.broken = true + proc draw*(this: var Barrier) = for sprite in this.sprites: diff --git a/src/game/entities/door.nim b/src/game/entities/door.nim new file mode 100644 index 0000000..cf1d27e --- /dev/null +++ b/src/game/entities/door.nim @@ -0,0 +1,33 @@ +include ../../engine/base + +import player +import glm +import math + +import ../userdata +# Doors are positioned using their bottom left corner +type Door* = ref object + open_wav: WavHandle + sprite*: AnimatedSprite + sound: WavHandle + requires_key: bool + + +proc create_door*(pos: Vec2f): Door = + result = new(Door) + result.sprite = create_animated_sprite("res/barrier/door.yaml") + result.open_wav = load_sound("res/barrier/exit.mp3") + result.requires_key = false + result.sprite.position = vec2f(pos.x, pos.y - result.sprite.sprite.texture_height.toFloat) + +proc update*(this: Door, player: Player): bool = + let diff = player.sprite.center_position - this.sprite.center_position + let dist = length(diff) + + if dist < 50.0: + + return true + return false + +proc draw*(this: Door) = + renderer.draw(this.sprite) \ No newline at end of file diff --git a/src/game/entities/enemy.nim b/src/game/entities/enemy.nim index eaa1ce5..89da48e 100644 --- a/src/game/entities/enemy.nim +++ b/src/game/entities/enemy.nim @@ -22,7 +22,7 @@ type hurt_wav: WavHandle sprite*: AnimatedSprite phys_body*: Body - phys_shape: Shape + phys_shape*: Shape user_data*: UserData toss_timer: float @@ -46,10 +46,14 @@ proc create_rockman*(pos: Vec2f, space: Space, id: int): Enemy = proc die(this: var Enemy, objects: var seq[PhysicalObject], space: Space) = + let pos = this.phys_body.position + let vpos = vec2f(pos.x, pos.y) space.removeShape(this.phys_shape) space.removeBody(this.phys_body) this.dead = true - discard + if this.kind == ekRockman: + # Spawn a dead rockman + objects.add(create_deadrockman(vpos, space, objects.len)) proc update*(this: var Enemy, player: Player, objects: var seq[PhysicalObject], space: Space) = this.sprite.center_position = vec2f(this.phys_body.position.x, this.phys_body.position.y) @@ -59,6 +63,7 @@ proc update*(this: var Enemy, player: Player, objects: var seq[PhysicalObject], this.die(objects, space) if this.toss_timer < 0.0: + this.phys_body.angle = 0.0 if this.kind == ekRockman: # Simple moving towards player behaviour, with random retreats if this.retreat: @@ -82,6 +87,7 @@ proc update*(this: var Enemy, player: Player, objects: var seq[PhysicalObject], else: this.toss_timer -= dt + this.sprite.rotation = this.phys_body.angle proc hurt*(this: var Enemy, point: Vect) = this.health -= 1.0 @@ -93,6 +99,9 @@ proc hurt*(this: var Enemy, point: Vect) = proc toss*(this: var Enemy) = this.toss_timer = 2.0 +proc is_tossable*(this: Enemy): bool = + return true + proc draw*(this: var Enemy) = renderer.draw(this.sprite) diff --git a/src/game/entities/physical_object.nim b/src/game/entities/physical_object.nim index ab38505..137d4c6 100644 --- a/src/game/entities/physical_object.nim +++ b/src/game/entities/physical_object.nim @@ -8,13 +8,14 @@ import ../userdata type ObjectKind = enum okRock, - okMagmaRock + okMagmaRock, + okDeadRockman PhysicalObject* = ref object kind: ObjectKind sprite*: AnimatedSprite phys_body*: Body - phys_shape: Shape + phys_shape*: Shape user_data: UserData proc update*(this: var PhysicalObject) = @@ -28,12 +29,12 @@ proc draw*(this: var PhysicalObject) = proc create_rock*(pos: Vec2f, space: Space, id: int): PhysicalObject = result = new(PhysicalObject) result.sprite = create_animated_sprite("res/objects/rock.yaml") - let mass = 50.0 + let mass = 40.0 let moment = momentForCircle(mass, 0.0, 58.0, vzero) result.phys_body = space.addBody(newBody(mass, moment)) result.phys_shape = space.addShape(newCircleShape(result.phys_body, 58.0, vzero)) - result.user_data = make_enemy_userdata(id) + result.user_data = make_object_userdata(id) result.phys_shape.userData = addr result.user_data result.phys_body.position = v(pos.x, pos.y) result.phys_shape.friction = 0.1 @@ -43,14 +44,32 @@ proc create_rock*(pos: Vec2f, space: Space, id: int): PhysicalObject = proc create_magmarock*(pos: Vec2f, space: Space, id: int): PhysicalObject = result = new(PhysicalObject) result.sprite = create_animated_sprite("res/objects/magmarock.yaml") - let mass = 60.0 + let mass = 40.0 let moment = momentForCircle(mass, 0.0, 58.0, vzero) result.phys_body = space.addBody(newBody(mass, moment)) result.phys_shape = space.addShape(newCircleShape(result.phys_body, 58.0, vzero)) - result.user_data = make_enemy_userdata(id) + result.user_data = make_object_userdata(id) result.phys_shape.userData = addr result.user_data result.phys_body.position = v(pos.x, pos.y) result.phys_shape.friction = 0.1 - result.kind = okMagmaRock \ No newline at end of file + result.kind = okMagmaRock + +proc create_deadrockman*(pos: Vec2f, space: Space, id: int): PhysicalObject = + result = new(PhysicalObject) + result.sprite = create_animated_sprite("res/enemies/deadrockman.yaml") + let mass = 12.0 + let moment = momentForBox(mass, 12.0, 12.0) + + result.phys_body = space.addBody(newBody(mass, moment)) + result.phys_shape = space.addShape(newBoxShape(result.phys_body, 12.0, 12.0, 0.0)) + result.user_data = make_object_userdata(id) + result.phys_shape.userData = addr result.user_data + result.phys_body.position = v(pos.x, pos.y) + result.phys_shape.friction = 0.7 + + result.kind = okDeadRockman + +proc is_tossable*(this: PhysicalObject): bool = + return this.kind == okDeadRockman \ No newline at end of file diff --git a/src/game/entities/player.nim b/src/game/entities/player.nim index 04f34cb..046030e 100644 --- a/src/game/entities/player.nim +++ b/src/game/entities/player.nim @@ -1,14 +1,15 @@ include ../../engine/base import nimgl/glfw import ../userdata +import random type Player* = ref object sprite*: AnimatedSprite lantern*: Sprite - phys_body: Body - phys_shape: Shape + phys_body*: Body + phys_shape*: Shape phys_space: Space last_jump: bool @@ -89,8 +90,9 @@ proc query_hit(sh: Shape, p: Vect, n: Vect, a: Float, data: pointer) {.cdecl.} = if udata.kind == bkEnemy: let enemy_idx = udata.point var datac = cast[ptr ToHitData](data) - datac[].hit = true - hurt(datac[].enemies[enemy_idx], p) + if not datac[].enemies[enemy_idx].dead: + datac[].hit = true + hurt(datac[].enemies[enemy_idx], p) proc hit(this: Player, enemies: seq[Enemy]): bool = @@ -114,6 +116,7 @@ type ToTossData = object enemies: seq[Enemy] objects: seq[PhysicalObject] force: Vect + tossed: seq[pointer] proc query_toss(sh: Shape, p: Vect, n: Vect, a: Float, data: pointer) {.cdecl.} = # There's this weird 38 user data that we must ignore @@ -121,21 +124,31 @@ proc query_toss(sh: Shape, p: Vect, n: Vect, a: Float, data: pointer) {.cdecl.} var datac = cast[ptr ToTossData](data) if cast[int](sh.userData) > 100000: let udata = cast[ptr UserData](sh.userData)[] + if datac[].tossed.contains(cast[pointer](udata)): + return if udata.kind == bkEnemy: let enemy_idx = udata.point - body = datac[].enemies[enemy_idx].phys_body - datac[].enemies[enemy_idx].toss() + let enemy = datac[].enemies[enemy_idx] + if enemy.is_tossable() and not enemy.dead: + body = enemy.phys_body + datac[].enemies[enemy_idx].toss() + datac[].tossed.add(cast[pointer](udata)) elif udata.kind == bkObject: let object_idx = udata.point - body = datac[].objects[object_idx].phys_body + if datac[].objects[object_idx].is_tossable(): + body = datac[].objects[object_idx].phys_body + datac[].tossed.add(cast[pointer](udata)) if body != nil: - let fp = body.position - body.applyImpulseAtWorldPoint(fp, datac[].force) + var cm = body.centerOfGravity + var fp = body.localToWorld(cm) + fp = v(fp.x + rand(10.0), fp.y + rand(10.0)) + body.applyImpulseAtWorldPoint(datac[].force, fp) proc toss(this: Player, enemies: seq[Enemy], objects: seq[PhysicalObject]) = let rays = this.phys_body.position + v(0.0, 0.0) - var raye = this.phys_body.position + v(34.0, 23.0) + var raye = this.phys_body.position + v(48.0, 23.0) + var raye2 = this.phys_body.position + v(48.0, 0.0) if this.sprite.scale.x < 0.0: raye = this.phys_body.position + v(-34.0, 23.0) @@ -161,16 +174,20 @@ proc toss(this: Player, enemies: seq[Enemy], objects: seq[PhysicalObject]) = return # Direction - var ver = 0.5 * sin(PI * toss_prog) + var ver = 1.0 * (sin(PI * toss_prog) + 0.2) if this.sprite.scale.x < 0.0: query_data.force = v(-1.0, -ver) else: query_data.force = v(1.0, -ver) + let len = query_data.force.vlength + query_data.force = v(query_data.force.x / len, query_data.force.y / len) - var force = 300.0 * (cos(PI * 0.5 * toss_prog - PI * 0.5) + 0.3) + var force = 7000.0 * (cos(PI * 0.5 * toss_prog - PI * 0.5) + 0.3) query_data.force = v(query_data.force.x * force, query_data.force.y * force) - segmentQuery(this.phys_space, rays, raye, Float(4.0), filter, query_toss, addr query_data) + # We do two querys + segmentQuery(this.phys_space, rays, raye, Float(14.0), filter, query_toss, addr query_data) + segmentQuery(this.phys_space, rays, raye2, Float(14.0), filter, query_toss, addr query_data) proc update*(this: var Player, enemies: seq[Enemy], objects: seq[PhysicalObject]) = # Ground check @@ -233,11 +250,11 @@ proc update*(this: var Player, enemies: seq[Enemy], objects: seq[PhysicalObject] else: var lateral = false if glfw_window.getKey(GLFWKey.A) == GLFW_PRESS: - this.phys_body.position = this.phys_body.position + v(-dt * 100.0, 0.0) + this.phys_body.position = this.phys_body.position + v(-dt * 120.0, 0.0) this.sprite.scale = vec2f(-1.0, 1.0) lateral = true if glfw_window.getKey(GLFWKey.D) == GLFW_PRESS: - this.phys_body.position = this.phys_body.position + v(dt * 100.0, 0.0) + this.phys_body.position = this.phys_body.position + v(dt * 120.0, 0.0) this.sprite.scale = vec2f(1.0, 1.0) lateral = true if glfw_window.getKey(GLFWKey.V) == GLFW_PRESS: diff --git a/src/game/scenes/level.nim b/src/game/scenes/level.nim index cda8fa3..0f63dd7 100644 --- a/src/game/scenes/level.nim +++ b/src/game/scenes/level.nim @@ -2,6 +2,7 @@ import ../entities/player import ../entities/enemy import ../entities/barrier +import ../entities/door import ../../engine/map/map_loader import ../entities/physical_object @@ -10,6 +11,8 @@ import ../../engine/graphics/sprite import ../../engine/graphics/shader import ../userdata +import nimgl/glfw + import glm # A level consist of the player, a set of live enemies @@ -17,28 +20,17 @@ import glm type Level* = ref object player*: Player map*: Map + scale: int terr_udata: UserData physics_space*: Space physical_objects*: seq[PhysicalObject] barriers*: seq[Barrier] enemies: seq[Enemy] + doors: seq[Door] -proc init*(this: var Level, map: string, scale: int) = - this = new(Level) - renderer.fullscreen_shader = load_shader("res/shader/fullscreen") - renderer.camera.scale = 1.0 - this.physics_space = newSpace() - this.physics_space.gravity = v(0, 400) - this.map = load_map(map, scale, this.physics_space) - - this.terr_udata = make_terrain_userdata() - # Assign the userdata to all segments - for segment in mitems(this.map.segments): - # TODO: FIx this - ##segment.userData = addr this.terr_udata - segment.friction = 0.5 - + +proc init_no_map(this: var Level, scale: int) = this.player = create_player(this.map.points["player"][0], this.physics_space) # Create all types of enemies / objects @@ -58,19 +50,83 @@ proc init*(this: var Level, map: string, scale: int) = if this.map.areas.hasKey("break_wood"): for area in this.map.areas["break_wood"]: this.barriers.add(create_wooden_barrier(area, scale, this.physics_space, this.barriers.len, addr this.barriers)) + + # Load doors + if this.map.points.hasKey("door"): + for point in this.map.points["door"]: + this.doors.add(create_door(point)) + +# Removes all physical objects EXCEPT the world, and reinits +proc restart(this: var Level) = + for obj in this.physical_objects: + this.physics_space.removeBody(obj.phys_body) + this.physics_space.removeShape(obj.phys_shape) + for barrier in this.barriers: + if not barrier.broken: + this.physics_space.removeShape(barrier.phys_shape) + for enemy in this.enemies: + if not enemy.dead: + this.physics_space.removeBody(enemy.phys_body) + this.physics_space.removeShape(enemy.phys_shape) + + this.physics_space.removeBody(this.player.phys_body) + this.physics_space.removeShape(this.player.phys_shape) + + this.physical_objects.setLen(0) + this.barriers.setLen(0) + this.enemies.setLen(0) + this.doors.setLen(0) + this.init_no_map(this.scale) + + +proc init*(this: var Level, map: string, scale: int) = + this = new(Level) + this.scale = scale + renderer.fullscreen_shader = load_shader("res/shader/fullscreen") + renderer.camera.scale = 1.0 + this.physics_space = newSpace() + this.physics_space.gravity = v(0, 400) + this.map = load_map(map, scale, this.physics_space) + + this.terr_udata = make_terrain_userdata() + # Assign the userdata to all segments + for segment in mitems(this.map.segments): + # TODO: FIx this + ##segment.userData = addr this.terr_udata + segment.friction = 0.5 + + init_no_map(this, this.scale) proc update*(this: var Level) = - this.physics_space.step(dt) + # we do multiple substeps to prevent pass-through + # TODO: Tune this + const steps = 4 + for i in countup(0, steps - 1): + this.physics_space.step(dt / steps) for enemy in mitems(this.enemies): if not enemy.dead: enemy.update(this.player, this.physical_objects, this.physics_space) for phys_obj in mitems(this.physical_objects): phys_obj.update() + + for barrier in mitems(this.barriers): + if not barrier.broken: + barrier.update() this.player.update(this.enemies, this.physical_objects) + var exit = false + for door in this.doors: + exit = exit or door.update(this.player) + + if exit: + quit(1) + renderer.camera.center = this.player.sprite.position renderer.camera.scale = 1.0 + if glfw_window.getKey(GLFWKey.R) == GLFW_PRESS: + this.restart() + proc draw*(this: var Level) = this.map.drawer.draw_tiles() @@ -85,5 +141,7 @@ proc draw*(this: var Level) = barrier.draw() this.player.draw() + for door in this.doors: + door.draw() diff --git a/src/game/scenes/level1.nim b/src/game/scenes/level1.nim index 6224c75..b0f3a63 100644 --- a/src/game/scenes/level1.nim +++ b/src/game/scenes/level1.nim @@ -7,6 +7,7 @@ import level type Level1Scene* = ref object of Scene music: WavHandle level: Level + tut: seq[Sprite] method init(this: Level1Scene) = echo "Init!" @@ -14,10 +15,25 @@ method init(this: Level1Scene) = discard play_sound(this.music, true) this.level.init("res/level1/map.yaml", 20) + this.tut.add(create_sprite("res/tutorial/tut000.png")) + this.tut.add(create_sprite("res/tutorial/tut0.png")) + this.tut.add(create_sprite("res/tutorial/tut00.png")) + this.tut.add(create_sprite("res/tutorial/tut1.png")) + this.tut.add(create_sprite("res/tutorial/tut2.png")) + this.tut.add(create_sprite("res/tutorial/tut3.png")) + + this.tut[0].center_position = vec2f(52 * 20.0, 21 * 20.0) + this.tut[1].center_position = vec2f(49 * 20.0, 13 * 20.0) + this.tut[2].center_position = vec2f(73 * 20.0, 23 * 20.0) + this.tut[3].center_position = vec2f(152 * 20.0, 17 * 20.0) + this.tut[4].center_position = vec2f(176 * 20.0, 29 * 20.0) + this.tut[5].center_position = vec2f(207 * 20.0, 13 * 20.0) method update(this: Level1Scene) = this.level.update() return method render(this: Level1Scene) = - this.level.draw() \ No newline at end of file + this.level.draw() + for tut in this.tut: + renderer.draw(tut) \ No newline at end of file diff --git a/src/game/scenes/level2.nim b/src/game/scenes/level2.nim new file mode 100644 index 0000000..e69de29