diff --git a/doodads/azulian/Makefile b/doodads/azulian/Makefile index 1fde043..d9e4930 100644 --- a/doodads/azulian/Makefile +++ b/doodads/azulian/Makefile @@ -14,6 +14,11 @@ build: doodad edit-doodad --tag "color=red" azu-red.doodad doodad install-script azulian.js azu-red.doodad + doodad convert -t "White Azulian" white-front.png white-back.png \ + white-wr{1,2,3,4}.png white-wl{1,2,3,4}.png azu-white.doodad + doodad edit-doodad --tag "color=white" azu-white.doodad + doodad install-script azulian.js azu-white.doodad + # Tag the category for these doodads for i in *.doodad; do\ doodad edit-doodad --tag "category=creatures" $${i};\ diff --git a/doodads/azulian/azulian-red.js b/doodads/azulian/azulian-red.js deleted file mode 100644 index aeb5f91..0000000 --- a/doodads/azulian/azulian-red.js +++ /dev/null @@ -1,43 +0,0 @@ -// Azulian (Red) -// DEPRECATED: they both share azulian.js now. - -function main() { - var playerSpeed = 4; - var gravity = 4; - var Vx = Vy = 0; - - var direction = "right"; - - Self.SetHitbox(0, 0, 32, 32) - Self.SetMobile(true); - Self.SetInventory(true); - Self.SetGravity(true); - Self.AddAnimation("walk-left", 100, ["red-wl1", "red-wl2", "red-wl3", "red-wl4"]); - Self.AddAnimation("walk-right", 100, ["red-wr1", "red-wr2", "red-wr3", "red-wr4"]); - - // Sample our X position every few frames and detect if we've hit a solid wall. - var sampleTick = 0; - var sampleRate = 5; - var lastSampledX = 0; - - setInterval(function () { - if (sampleTick % sampleRate === 0) { - var curX = Self.Position().X; - var delta = Math.abs(curX - lastSampledX); - if (delta < 5) { - direction = direction === "right" ? "left" : "right"; - } - lastSampledX = curX; - } - sampleTick++; - - // TODO: Vector() requires floats, pain in the butt for JS, - // the JS API should be friendlier and custom... - var Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)); - Self.SetVelocity(Vector(Vx, 0.0)); - - if (!Self.IsAnimating()) { - Self.PlayAnimation("walk-" + direction, null); - } - }, 100); -} diff --git a/doodads/azulian/azulian.js b/doodads/azulian/azulian.js index bd63b64..73e0943 100644 --- a/doodads/azulian/azulian.js +++ b/doodads/azulian/azulian.js @@ -1,12 +1,29 @@ // Azulian (Red and Blue) -var playerSpeed = 12, + +const color = Self.GetTag("color"); +var playerSpeed = color === 'blue' ? 2 : 4, + swimSpeed = playerSpeed * 0.4, + aggroX = 250, // X/Y distance sensitivity from player + aggroY = color === 'blue' ? 100 : 200, + jumpSpeed = color === 'blue' ? 14 : 18, + swimJumpSpeed = jumpSpeed * 0.4, animating = false, direction = "right", lastDirection = "right"; +// white Azulian is faster yet than the red +if (color === 'white') { + aggroX = 1000; + aggroY = 400; + playerSpeed = 8; + swimSpeed = playerSpeed * 0.4; + jumpSpeed = 20; + swimJumpSpeed = jumpSpeed * 0.4; +} + function setupAnimations(color) { - var left = color === 'blue' ? 'blu-wl' : 'red-wl', - right = color === 'blue' ? 'blu-wr' : 'red-wr', + let left = color === 'blue' ? 'blu-wl' : color + '-wl', + right = color === 'blue' ? 'blu-wr' : color + '-wr', leftFrames = [left + '1', left + '2', left + '3', left + '4'], rightFrames = [right + '1', right + '2', right + '3', right + '4']; @@ -15,9 +32,11 @@ function setupAnimations(color) { } function main() { - var color = Self.GetTag("color"); playerSpeed = color === 'blue' ? 2 : 4; + let swimJumpCooldownTick = 0, // minimum Game Tick before we can jump while swimming + swimJumpCooldown = 10; // CONFIG: delta of ticks between jumps while swimming + Self.SetMobile(true); Self.SetGravity(true); Self.SetInventory(true); @@ -32,23 +51,77 @@ function main() { // when it meets resistance. // Sample our X position every few frames and detect if we've hit a solid wall. - var sampleTick = 0; - var sampleRate = 5; - var lastSampledX = 0; - - setInterval(function () { - if (sampleTick % sampleRate === 0) { - var curX = Self.Position().X; - var delta = Math.abs(curX - lastSampledX); - if (delta < 5) { - direction = direction === "right" ? "left" : "right"; + let sampleTick = 0; + let sampleRate = 5; + let lastSampledX = 0; + + // Get the player on touch. + Events.OnCollide((e) => { + // If we're diving and we hit the player, game over! + // Azulians are friendly to Thieves though! + if (e.Settled && isPlayerFood(e.Actor)) { + FailLevel("Watch out for the Azulians!"); + return; + } + }); + + setInterval(() => { + // If the player is nearby, walk towards them. Otherwise, default pattern + // is to walk back and forth. + let player = Actors.FindPlayer(), + followPlayer = false, + jump = false; + + // Don't follow boring players. + if (player !== null && isPlayerFood(player)) { + let playerPt = player.Position(), + myPt = Self.Position(); + + // If the player is within aggro range, move towards. + if ((Math.abs(playerPt.X - myPt.X) < aggroX && Math.abs(playerPt.Y - myPt.Y) < aggroY) + || (Level.Difficulty > 0)) { + direction = playerPt.X < myPt.X ? "left" : "right"; + followPlayer = true; + + if (playerPt.Y + player.Size().H < myPt.Y + Self.Size().H) { + jump = true; + } + } + } + + // Default AI: sample position so we turn around on obstacles. + if (!followPlayer) { + if (sampleTick % sampleRate === 0) { + let curX = Self.Position().X; + let delta = Math.abs(curX - lastSampledX); + if (delta < 5) { + direction = direction === "right" ? "left" : "right"; + } + lastSampledX = curX; } - lastSampledX = curX; + sampleTick++; } - sampleTick++; - var Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)); - Self.SetVelocity(Vector(Vx, 0.0)); + // Handle being underwater. + let canJump = Self.Grounded(); + if (Self.IsWet()) { + let tick = GetTick(); + if (tick > swimJumpCooldownTick) { + canJump = true; + swimJumpCooldownTick = tick + swimJumpCooldown; + } + } + + // How speedy would our movement and jump be? + let xspeed = playerSpeed, yspeed = jumpSpeed; + if (Self.IsWet()) { + xspeed = swimSpeed; + yspeed = swimJumpSpeed; + } + + let Vx = parseFloat(xspeed * (direction === "left" ? -1 : 1)), + Vy = jump && canJump ? parseFloat(-yspeed) : Self.GetVelocity().Y; + Self.SetVelocity(Vector(Vx, Vy)); // If we changed directions, stop animating now so we can // turn around quickly without moonwalking. @@ -61,12 +134,12 @@ function main() { } lastDirection = direction; - }, 100); + }, 10); } function playerControls() { // Note: player speed is controlled by the engine. - Events.OnKeypress(function (ev) { + Events.OnKeypress((ev) => { if (ev.Right) { if (!Self.IsAnimating()) { Self.PlayAnimation("walk-right", null); @@ -81,3 +154,25 @@ function playerControls() { } }) } + +// Logic to decide if the player is interesting to the Azulian (aka, if the Azulian +// will be hostile towards the player). Boring players will not be chased after and +// the Azulian will not harm them if they make contact. +function isPlayerFood(actor) { + // Not a player or is invulnerable, or Peaceful difficulty. + if (!actor.IsPlayer() || actor.Invulnerable() || Level.Difficulty < 0) { + return false; + } + + // On hard mode they are hostile to any player. + if (Level.Difficulty > 0) { + return true; + } + + // Azulians are friendly to Thieves and other Azulians. + if (actor.Doodad().Filename === "thief.doodad" || actor.Doodad().Title.indexOf("Azulian") > -1) { + return false; + } + + return true; +} diff --git a/doodads/azulian/white-back.png b/doodads/azulian/white-back.png new file mode 100644 index 0000000..12f0a74 Binary files /dev/null and b/doodads/azulian/white-back.png differ diff --git a/doodads/azulian/white-front.png b/doodads/azulian/white-front.png new file mode 100644 index 0000000..b2ffece Binary files /dev/null and b/doodads/azulian/white-front.png differ diff --git a/doodads/azulian/white-wl1.png b/doodads/azulian/white-wl1.png new file mode 100644 index 0000000..9f98a1f Binary files /dev/null and b/doodads/azulian/white-wl1.png differ diff --git a/doodads/azulian/white-wl2.png b/doodads/azulian/white-wl2.png new file mode 100644 index 0000000..ec900ce Binary files /dev/null and b/doodads/azulian/white-wl2.png differ diff --git a/doodads/azulian/white-wl3.png b/doodads/azulian/white-wl3.png new file mode 100644 index 0000000..a87537f Binary files /dev/null and b/doodads/azulian/white-wl3.png differ diff --git a/doodads/azulian/white-wl4.png b/doodads/azulian/white-wl4.png new file mode 100644 index 0000000..7401607 Binary files /dev/null and b/doodads/azulian/white-wl4.png differ diff --git a/doodads/azulian/white-wr1.png b/doodads/azulian/white-wr1.png new file mode 100644 index 0000000..d9c26d1 Binary files /dev/null and b/doodads/azulian/white-wr1.png differ diff --git a/doodads/azulian/white-wr2.png b/doodads/azulian/white-wr2.png new file mode 100644 index 0000000..1a1a566 Binary files /dev/null and b/doodads/azulian/white-wr2.png differ diff --git a/doodads/azulian/white-wr3.png b/doodads/azulian/white-wr3.png new file mode 100644 index 0000000..7eeb34d Binary files /dev/null and b/doodads/azulian/white-wr3.png differ diff --git a/doodads/azulian/white-wr4.png b/doodads/azulian/white-wr4.png new file mode 100644 index 0000000..ee29adb Binary files /dev/null and b/doodads/azulian/white-wr4.png differ diff --git a/doodads/bird/Makefile b/doodads/bird/Makefile index e70fedc..d0ff88d 100644 --- a/doodads/bird/Makefile +++ b/doodads/bird/Makefile @@ -2,9 +2,17 @@ ALL: build .PHONY: build build: - doodad convert -t "Bird (red)" left-1.png left-2.png right-1.png right-2.png \ - dive-left.png dive-right.png bird-red.doodad + doodad convert -t "Bird (red)" red/left-1.png red/left-2.png red/right-1.png \ + red/right-2.png red/dive-left.png red/dive-right.png \ + bird-red.doodad doodad install-script bird.js bird-red.doodad + doodad edit-doodad --tag "color=red" bird-red.doodad + + doodad convert -t "Bird (blue)" blue/left-1.png blue/left-2.png blue/right-1.png \ + blue/right-2.png blue/dive-left.png blue/dive-right.png \ + bird-blue.doodad + doodad install-script bird.js bird-blue.doodad + doodad edit-doodad --tag "color=blue" bird-blue.doodad # Tag the category for these doodads for i in *.doodad; do\ diff --git a/doodads/bird/bird.js b/doodads/bird/bird.js index 5d94ebe..9a236d8 100644 --- a/doodads/bird/bird.js +++ b/doodads/bird/bird.js @@ -1,18 +1,36 @@ -// Bird +// Bird (red and blue) -function main() { - var speed = 4; - var Vx = Vy = 0; - var altitude = Self.Position().Y; // original height in the level - - var direction = "left", - lastDirection = "left"; - var states = { - flying: 0, - diving: 1, - }; - var state = states.flying; +/* +Base A.I. behaviors (red bird) are: + +- Tries to maintain original altitude in level and flies left/right. +- Divebombs the player to attack, then climbs back to its original + altitude when it hits a floor/wall. + +Blue bird: + +- Flies in a sine wave pattern (its target altitude fluctuates + around the bird's original placement on the level). +- Longer aggro radius to dive. +*/ +let speed = 4, + Vx = Vy = 0, + color = Self.GetTag("color"), // informs our A.I. behaviors + searchStep = 12 // pixels to step while searching for a player + searchLimit = color === "blue" ? 24 : 12, // multiples of searchStep for aggro radius + altitude = Self.Position().Y, // original height in level + targetAltitude = altitude; // bird's target height to maintain + +let direction = "left", + lastDirection = "left"; +let states = { + flying: 0, + diving: 1, +}; +let state = states.flying; + +function main() { Self.SetMobile(true); Self.SetGravity(false); Self.SetHitbox(0, 0, 46, 32); @@ -24,33 +42,75 @@ function main() { return player(); } - Events.OnCollide(function (e) { - if (e.Actor.IsMobile() && e.InHitbox) { + Events.OnCollide((e) => { + // If we're diving and we hit the player, game over! + if (e.Settled && state === states.diving && e.Actor.IsPlayer()) { + FailLevel("Watch out for birds!"); + return; + } + + if (e.Actor.IsMobile() && e.Actor.HasGravity() && e.InHitbox) { return false; } }); // Sample our X position every few frames and detect if we've hit a solid wall. - var sampleTick = 0; - var sampleRate = 2; - var lastSampledX = 0; - var lastSampledY = 0; + let sampleTick = 0, + sampleRate = 2, + lastSampled = Point(0, 0); + + setInterval(() => { + // Run blue bird A.I. if we are blue: moves our target altitude along a sine wave. + if (color === "blue") { + AI_BlueBird(); + } - setInterval(function () { + // Sample how far we've moved to detect hitting a wall. if (sampleTick % sampleRate === 0) { - var curX = Self.Position().X; - var delta = Math.abs(curX - lastSampledX); + let curP = Self.Position() + let delta = Math.abs(curP.X - lastSampled.X); if (delta < 5) { direction = direction === "right" ? "left" : "right"; } - lastSampledX = curX; + + // If we were diving, check Y delta too for if we hit floor + if (state === states.diving && Math.abs(curP.Y - lastSampled.Y) < 5) { + state = states.flying; + } + lastSampled = curP } sampleTick++; + // Are we diving? + if (state === states.diving) { + Vy = speed + } else { + // If we are not flying at our original altitude, correct for that. + let curV = Self.Position(); + Vy = 0.0; + if (curV.Y != targetAltitude) { + Vy = curV.Y < targetAltitude ? 1 : -1; + } + + // Scan for the player character and dive. + try { + AI_ScanForPlayer() + } catch (e) { + console.error("Error in AI_ScanForPlayer: %s", e); + } + } + // TODO: Vector() requires floats, pain in the butt for JS, // the JS API should be friendlier and custom... - var Vx = parseFloat(speed * (direction === "left" ? -1 : 1)); - Self.SetVelocity(Vector(Vx, 0.0)); + let Vx = parseFloat(speed * (direction === "left" ? -1 : 1)); + Self.SetVelocity(Vector(Vx, Vy)); + + // If diving, exit - don't edit animation. + if (state === states.diving) { + Self.ShowLayerNamed("dive-" + direction); + lastDirection = direction; + return; + } // If we changed directions, stop animating now so we can // turn around quickly without moonwalking. @@ -66,34 +126,149 @@ function main() { }, 100); } +// A.I. subroutine: scan for the player character. +// The bird scans in a 45 degree angle downwards, if the +// player is seen nearby in that scan it will begin a dive. +// It's not hostile towards characters that can fly (having +// no gravity). +function AI_ScanForPlayer() { + // If Peaceful difficulty, do not attack. + if (Level.Difficulty < 0) { + return + } + + let stepY = searchStep, // number of pixels to skip + stepX = stepY, + limit = stepX * searchLimit, // furthest we'll scan + scanX = scanY = 0, + size = Self.Size(), + fromPoint = Self.Position(); + + // From what point do we begin the scan? + if (direction === 'left') { + stepX = -stepX; + fromPoint.Y += size.H; + } else { + fromPoint.Y += size.H; + fromPoint.X += size.W; + } + + scanX = fromPoint.X; + scanY = fromPoint.Y; + + for (let i = 0; i < limit; i += stepY) { + scanX += stepX; + scanY += stepY; + for (let actor of Actors.At(Point(scanX, scanY))) { + if (actor.IsPlayer() && actor.HasGravity()) { + state = states.diving; + return; + } + } + } + + return; +} + +// Sine wave controls for the Blue bird. +var sineLimit = 32, + sineCounter = 0, + sineDirection = 1, + sineFrequency = 5, // every 500ms + sineStep = 16; + +// A.I. Subroutine: sine wave pattern (Blue bird). +function AI_BlueBird() { + // The main loop runs on a 100ms interval, control how frequently + // to adjust the bird's target velocity. + if (sineCounter > 0 && (sineCounter % sineFrequency) > 1) { + sineCounter++; + return; + } + sineCounter++; + + targetAltitude += sineStep*sineDirection; + + // Cap the distance between our starting altitude and sine-wave target. + if (targetAltitude < altitude && altitude - targetAltitude >= sineLimit) { + targetAltitude = altitude - sineLimit; + sineDirection = 1 + } else if (targetAltitude > altitude && targetAltitude - altitude >= sineLimit) { + targetAltitude = altitude + sineLimit; + sineDirection = -1 + } +} + // If under control of the player character. function player() { - Self.SetInventory(true); - Events.OnKeypress(function (ev) { + let playerSpeed = 12, + diving = false, + falling = false; + + // The player can dive by moving downwards and laterally, but + // de-cheese their ability to just sweep across the ground; if + // they aren't seen to be moving downwards, cancel the dive. + let lastPoint = Self.Position(); + setInterval(() => { + let nowAt = Self.Position(); + if (nowAt.Y > lastPoint.Y) { + falling = true; + } else { + falling = false; + } + lastPoint = nowAt; + }, 100); + + Events.OnKeypress((ev) => { Vx = 0; Vy = 0; - if (ev.Up) { - Vy = -playerSpeed; - } else if (ev.Down) { - Vy = playerSpeed; + if (ev.Right) { + direction = "right"; + } else if (ev.Left) { + direction = "left"; } - if (ev.Right) { + // Dive! + if (ev.Down && ev.Right && falling) { + Self.StopAnimation(); + Self.ShowLayerNamed("dive-right"); + diving = falling; + } else if (ev.Down && ev.Left && falling) { + Self.StopAnimation(); + Self.ShowLayerNamed("dive-left"); + diving = falling; + } else if (ev.Right) { + // Fly right. if (!Self.IsAnimating()) { Self.PlayAnimation("fly-right", null); } Vx = playerSpeed; + diving = false; } else if (ev.Left) { + // Fly left. if (!Self.IsAnimating()) { Self.PlayAnimation("fly-left", null); } Vx = -playerSpeed; + diving = false; } else { - Self.StopAnimation(); - animating = false; + // Hover in place. + if (!Self.IsAnimating()) { + Self.PlayAnimation("fly-" + direction); + } + diving = false; } - Self.SetVelocity(Vector(Vx, Vy)); - }) -} \ No newline at end of file + // Player is invulnerable while diving. + Self.SetInvulnerable(diving); + }); + + Events.OnCollide((e) => { + // If the player is diving at an enemy mob, destroy it. + if (diving && e.Settled && e.Actor.IsMobile() && !e.Actor.Invulnerable()) { + Sound.Play("crumbly-break.wav"); + e.Actor.Destroy(); + } + }); +} diff --git a/doodads/bird/blue/dive-left.png b/doodads/bird/blue/dive-left.png new file mode 100644 index 0000000..1c2f7b7 Binary files /dev/null and b/doodads/bird/blue/dive-left.png differ diff --git a/doodads/bird/blue/dive-right.png b/doodads/bird/blue/dive-right.png new file mode 100644 index 0000000..04f0992 Binary files /dev/null and b/doodads/bird/blue/dive-right.png differ diff --git a/doodads/bird/blue/left-1.png b/doodads/bird/blue/left-1.png new file mode 100644 index 0000000..e988064 Binary files /dev/null and b/doodads/bird/blue/left-1.png differ diff --git a/doodads/bird/blue/left-2.png b/doodads/bird/blue/left-2.png new file mode 100644 index 0000000..f4e6fb5 Binary files /dev/null and b/doodads/bird/blue/left-2.png differ diff --git a/doodads/bird/blue/right-1.png b/doodads/bird/blue/right-1.png new file mode 100644 index 0000000..7c63982 Binary files /dev/null and b/doodads/bird/blue/right-1.png differ diff --git a/doodads/bird/blue/right-2.png b/doodads/bird/blue/right-2.png new file mode 100644 index 0000000..d812571 Binary files /dev/null and b/doodads/bird/blue/right-2.png differ diff --git a/doodads/bird/dive-left.png b/doodads/bird/red/dive-left.png similarity index 100% rename from doodads/bird/dive-left.png rename to doodads/bird/red/dive-left.png diff --git a/doodads/bird/dive-right.png b/doodads/bird/red/dive-right.png similarity index 100% rename from doodads/bird/dive-right.png rename to doodads/bird/red/dive-right.png diff --git a/doodads/bird/left-1.png b/doodads/bird/red/left-1.png similarity index 100% rename from doodads/bird/left-1.png rename to doodads/bird/red/left-1.png diff --git a/doodads/bird/left-2.png b/doodads/bird/red/left-2.png similarity index 100% rename from doodads/bird/left-2.png rename to doodads/bird/red/left-2.png diff --git a/doodads/bird/right-1.png b/doodads/bird/red/right-1.png similarity index 100% rename from doodads/bird/right-1.png rename to doodads/bird/red/right-1.png diff --git a/doodads/bird/right-2.png b/doodads/bird/red/right-2.png similarity index 100% rename from doodads/bird/right-2.png rename to doodads/bird/red/right-2.png diff --git a/doodads/box/box.js b/doodads/box/box.js index 30d3d6a..0ca950a 100644 --- a/doodads/box/box.js +++ b/doodads/box/box.js @@ -1,23 +1,32 @@ // Pushable Box. -var speed = 4; -var size = 75; + +const speed = 4, + size = 75; function main() { Self.SetMobile(true); + Self.SetInvulnerable(true); Self.SetGravity(true); Self.SetHitbox(0, 0, size, size); - Events.OnCollide(function (e) { + Events.OnCollide((e) => { // Ignore events from neighboring Boxes. if (e.Actor.Actor.Filename === "box.doodad") { return false; } if (e.Actor.IsMobile() && e.InHitbox) { - var overlap = e.Overlap; + let overlap = e.Overlap; if (overlap.Y === 0 && !(overlap.X === 0 && overlap.W < 5) && !(overlap.X === size)) { - // Standing on top, ignore. + // Be sure to position them snug on top. + // TODO: this might be a nice general solution in the + // collision detector... + e.Actor.MoveTo(Point( + e.Actor.Position().X, + Self.Position().Y - e.Actor.Hitbox().Y - e.Actor.Hitbox().H - 2, + )) + e.Actor.SetGrounded(true); return false; } else if (overlap.Y === size) { // From the bottom, boop it up. @@ -39,8 +48,8 @@ function main() { }); // When we receive power, we reset to our original position. - var origPoint = Self.Position(); - Message.Subscribe("power", function (powered) { + let origPoint = Self.Position(); + Message.Subscribe("power", (powered) => { Self.MoveTo(origPoint); Self.SetVelocity(Vector(0, 0)); }); @@ -52,8 +61,8 @@ function main() { function animate() { Self.AddAnimation("animate", 100, [0, 1, 2, 3, 2, 1]); - var running = false; - setInterval(function () { + let running = false; + setInterval(() => { if (!running) { running = true; Self.PlayAnimation("animate", function () { @@ -61,4 +70,4 @@ function animate() { }) } }, 100); -} \ No newline at end of file +} diff --git a/doodads/boy/Makefile b/doodads/boy/Makefile index 39d98a8..aaa98ed 100644 --- a/doodads/boy/Makefile +++ b/doodads/boy/Makefile @@ -5,6 +5,8 @@ build: doodad convert -t "Boy" stand-right.png stand-left.png \ walk-right-1.png walk-right-2.png walk-right-3.png \ walk-left-1.png walk-left-2.png walk-left-3.png \ + idle-right-1.png idle-right-2.png idle-right-3.png \ + idle-left-1.png idle-left-2.png idle-left-3.png \ boy.doodad doodad install-script boy.js boy.doodad diff --git a/doodads/boy/boy.js b/doodads/boy/boy.js index 02f877b..c62bf8d 100644 --- a/doodads/boy/boy.js +++ b/doodads/boy/boy.js @@ -1,38 +1,62 @@ -function main() { - var playerSpeed = 12; - var gravity = 4; - var Vx = Vy = 0; +const playerSpeed = 12; - var animating = false; - var animStart = animEnd = 0; - var animFrame = animStart; +let Vx = Vy = 0, + walking = false, + direction = "right", + lastDirection = direction; +function main() { Self.SetMobile(true); Self.SetInventory(true); Self.SetGravity(true); Self.SetHitbox(0, 0, 32, 52); Self.AddAnimation("walk-left", 200, ["stand-left", "walk-left-1", "walk-left-2", "walk-left-3", "walk-left-2", "walk-left-1"]); Self.AddAnimation("walk-right", 200, ["stand-right", "walk-right-1", "walk-right-2", "walk-right-3", "walk-right-2", "walk-right-1"]); + Self.AddAnimation("idle-left", 200, ["idle-left-1", "idle-left-2", "idle-left-3", "idle-left-2"]); + Self.AddAnimation("idle-right", 200, ["idle-right-1", "idle-right-2", "idle-right-3", "idle-right-2"]); + + // If the player suddenly changes direction, reset the animation state to quickly switch over. + let lastVelocity = Vector(0, 0); - Events.OnKeypress(function (ev) { + Events.OnKeypress((ev) => { Vx = 0; Vy = 0; + let curVelocity = Self.GetVelocity(); + if ((lastVelocity.X < 0 && curVelocity.X > 0) || + (lastVelocity.X > 0 && curVelocity.X < 0)) { + Self.StopAnimation(); + } + lastVelocity = curVelocity; + lastDirection = direction; + + let wasWalking = walking; if (ev.Right) { - if (!Self.IsAnimating()) { - Self.PlayAnimation("walk-right", null); - } + direction = "right"; Vx = playerSpeed; + walking = true; } else if (ev.Left) { - if (!Self.IsAnimating()) { - Self.PlayAnimation("walk-left", null); - } + direction = "left"; Vx = -playerSpeed; + walking = true; } else { + // Has stopped walking! + walking = false; + stoppedWalking = true; + } + + // Should we stop animating? (changed state) + if (direction !== lastDirection || wasWalking !== walking) { Self.StopAnimation(); - animating = false; } - // Self.SetVelocity(Point(Vx, Vy)); + // And play what animation? + if (!Self.IsAnimating()) { + if (walking) { + Self.PlayAnimation("walk-"+direction, null); + } else { + Self.PlayAnimation("idle-"+direction, null); + } + } }) } diff --git a/doodads/boy/idle-left-1.png b/doodads/boy/idle-left-1.png new file mode 100644 index 0000000..ca64929 Binary files /dev/null and b/doodads/boy/idle-left-1.png differ diff --git a/doodads/boy/idle-left-2.png b/doodads/boy/idle-left-2.png new file mode 100644 index 0000000..192388b Binary files /dev/null and b/doodads/boy/idle-left-2.png differ diff --git a/doodads/boy/idle-left-3.png b/doodads/boy/idle-left-3.png new file mode 100644 index 0000000..7c0aa43 Binary files /dev/null and b/doodads/boy/idle-left-3.png differ diff --git a/doodads/boy/idle-right-1.png b/doodads/boy/idle-right-1.png new file mode 100644 index 0000000..def2698 Binary files /dev/null and b/doodads/boy/idle-right-1.png differ diff --git a/doodads/boy/idle-right-2.png b/doodads/boy/idle-right-2.png new file mode 100644 index 0000000..ab55a73 Binary files /dev/null and b/doodads/boy/idle-right-2.png differ diff --git a/doodads/boy/idle-right-3.png b/doodads/boy/idle-right-3.png new file mode 100644 index 0000000..345b43f Binary files /dev/null and b/doodads/boy/idle-right-3.png differ diff --git a/doodads/build.sh b/doodads/build.sh index 6bc41eb..a668a3e 100755 --- a/doodads/build.sh +++ b/doodads/build.sh @@ -34,6 +34,10 @@ doors() { cd doors/ make cd .. + + cd gems/ + make + cd .. } trapdoors() { @@ -84,6 +88,16 @@ warpdoor() { cd .. } +creatures() { + cd snake/ + make + cd .. + + cd crusher/ + make + cd .. +} + boy buttons switches @@ -94,6 +108,7 @@ mobs objects onoff warpdoor +creatures doodad edit-doodad -quiet -lock -author "Noah" ../../assets/doodads/*.doodad doodad edit-doodad ../../assets/doodads/azu-blu.doodad doodad edit-doodad -hide ../../assets/doodads/boy.doodad diff --git a/doodads/buttons/button.js b/doodads/buttons/button.js index c31e342..a07bba2 100644 --- a/doodads/buttons/button.js +++ b/doodads/buttons/button.js @@ -1,19 +1,26 @@ function main() { - var timer = 0; - var pressed = false; + let timer = 0; + let pressed = false; // Has a linked Sticky Button been pressed permanently down? - var stickyDown = false; - Message.Subscribe("sticky:down", function(down) { + let stickyDown = false; + Message.Subscribe("sticky:down", (down) => { stickyDown = down; Self.ShowLayer(stickyDown ? 1 : 0); }); - Events.OnCollide(function(e) { + // Track who all is colliding with us. + let colliders = {}; + + Events.OnCollide((e) => { if (!e.Settled) { return; } + if (colliders[e.Actor.ID()] == undefined) { + colliders[e.Actor.ID()] = true; + } + // If a linked Sticky Button is pressed, button stays down too and // doesn't interact. if (stickyDown) { @@ -37,12 +44,17 @@ function main() { } Self.ShowLayer(1); - timer = setTimeout(function() { + }); + + Events.OnLeave((e) => { + delete colliders[e.Actor.ID()]; + + if (Object.keys(colliders).length === 0 && !stickyDown) { Sound.Play("button-up.wav") Self.ShowLayer(0); Message.Publish("power", false); timer = 0; pressed = false; - }, 200); + } }); } diff --git a/doodads/buttons/sticky.js b/doodads/buttons/sticky.js index 1702f8c..e9d6866 100644 --- a/doodads/buttons/sticky.js +++ b/doodads/buttons/sticky.js @@ -1,8 +1,8 @@ function main() { - var pressed = false; + let pressed = false; // When a sticky button receives power, it pops back up. - Message.Subscribe("power", function (powered) { + Message.Subscribe("power", (powered) => { if (powered && pressed) { Self.ShowLayer(0); pressed = false; @@ -12,7 +12,7 @@ function main() { } }) - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (!e.Settled) { return; } diff --git a/doodads/crumbly-floor/crumbly-floor.js b/doodads/crumbly-floor/crumbly-floor.js index c7bdf08..283bd65 100644 --- a/doodads/crumbly-floor/crumbly-floor.js +++ b/doodads/crumbly-floor/crumbly-floor.js @@ -6,19 +6,16 @@ function main() { Self.AddAnimation("fall", 100, ["fall1", "fall2", "fall3", "fall4"]); // Recover time for the floor to respawn. - var recover = 5000; + let recover = 5000; // States of the floor. - var stateSolid = 0; - var stateShaking = 1; - var stateFalling = 2; - var stateFallen = 3; - var state = stateSolid; + let stateSolid = 0; + let stateShaking = 1; + let stateFalling = 2; + let stateFallen = 3; + let state = stateSolid; - // Started the animation? - var startedAnimation = false; - - Events.OnCollide(function(e) { + Events.OnCollide((e) => { // If the floor is falling, the player passes right thru. if (state === stateFalling || state === stateFallen) { @@ -40,15 +37,15 @@ function main() { // Begin the animation sequence if we're in the solid state. if (state === stateSolid) { state = stateShaking; - Self.PlayAnimation("shake", function() { + Self.PlayAnimation("shake", () => { state = stateFalling; - Self.PlayAnimation("fall", function() { + Self.PlayAnimation("fall", () => { Sound.Play("crumbly-break.wav") state = stateFallen; Self.ShowLayerNamed("fallen"); // Recover after a while. - setTimeout(function() { + setTimeout(() => { Self.ShowLayer(0); state = stateSolid; }, recover); diff --git a/doodads/crusher/Makefile b/doodads/crusher/Makefile new file mode 100644 index 0000000..abbac86 --- /dev/null +++ b/doodads/crusher/Makefile @@ -0,0 +1,14 @@ +ALL: build + +.PHONY: build +build: + doodad convert -t "Crusher" sleep.png peek-left.png peek-right.png \ + angry.png ouch.png crusher.doodad + doodad install-script crusher.js crusher.doodad + + # Tag the category for these doodads + for i in *.doodad; do\ + doodad edit-doodad --tag "category=creatures" $${i};\ + done + + cp *.doodad ../../../assets/doodads/ \ No newline at end of file diff --git a/doodads/crusher/angry.png b/doodads/crusher/angry.png new file mode 100644 index 0000000..6fdc159 Binary files /dev/null and b/doodads/crusher/angry.png differ diff --git a/doodads/crusher/crusher.js b/doodads/crusher/crusher.js new file mode 100644 index 0000000..5aba2a2 --- /dev/null +++ b/doodads/crusher/crusher.js @@ -0,0 +1,207 @@ +// Crusher + +/* +A.I. Behaviors: + +- Sleeps and hangs in the air in a high place. +- When a player gets nearby, it begins "peeking" in their direction. +- When the player is below, tries to drop and crush them or any + other mobile doodad. +- The top edge is safe to walk on and ride back up like an elevator. +*/ + +let direction = "left", + dropSpeed = 12, + riseSpeed = 4, + watchRadius = 300, // player nearby distance to start peeking + fallRadius = 120, // player distance before it drops + helmetThickness = 48, // safe solid hitbox height + fireThickness = 12, // dangerous bottom thickness + targetAltitude = Self.Position() + lastAltitude = targetAltitude.Y + size = Self.Size(); + +const states = { + idle: 0, + peeking: 1, + drop: 2, + falling: 3, + hit: 4, + rising: 5, +}; +let state = states.idle; + +function main() { + Self.SetMobile(true); + Self.SetGravity(false); + Self.SetInvulnerable(true); + Self.SetHitbox(5, 2, 90, 73); + Self.AddAnimation("hit", 50, + ["angry", "ouch", "angry", "angry", "angry", "angry", + "sleep", "sleep", "sleep", "sleep", "sleep", "sleep", + "sleep", "sleep", "sleep", "sleep", "sleep", "sleep", + "sleep", "sleep", "sleep", "sleep", "sleep", "sleep"], + ) + + // Player Character controls? + if (Self.IsPlayer()) { + return player(); + } + + let hitbox = Self.Hitbox(); + + Events.OnCollide((e) => { + // The bottom is deadly if falling. + if (state === states.falling || state === states.hit && e.Settled) { + if (e.Actor.IsMobile() && e.InHitbox && !e.Actor.Invulnerable()) { + if (e.Overlap.H > 72) { + if (e.Actor.IsPlayer()) { + FailLevel("Don't get crushed!"); + return; + } else { + e.Actor.Destroy(); + } + } + } + } + + // Our top edge is always solid. + if (e.Actor.IsPlayer() && e.InHitbox) { + if (e.Overlap.Y < helmetThickness) { + // Be sure to position them snug on top. + // TODO: this might be a nice general solution in the + // collision detector... + e.Actor.MoveTo(Point( + e.Actor.Position().X, + Self.Position().Y - e.Actor.Hitbox().Y - e.Actor.Hitbox().H, + )) + e.Actor.SetGrounded(true); + } + } + + // The whole hitbox is ordinarily solid. + if (state !== state.falling) { + if (e.Actor.IsMobile() && e.InHitbox) { + return false; + } + } + }); + + setInterval(() => { + // Find the player. + let player = Actors.FindPlayer(), + playerPoint = player.Position(), + point = Self.Position(), + delta = 0, + nearby = false, + below = false; + + // Face the player. + if (playerPoint.X < point.X + (size.W / 2)) { + direction = "left"; + delta = Math.abs(playerPoint.X - (point.X + (size.W/2))); + } + else if (playerPoint.X > point.X + (size.W / 2)) { + direction = "right"; + delta = Math.abs(playerPoint.X - (point.X + (size.W/2))); + } + + if (delta < watchRadius) { + nearby = true; + } + if (delta < fallRadius) { + // Check if the player is below us. + if (playerPoint.Y > point.Y + size.H) { + below = true; + } + } + + switch (state) { + case states.idle: + if (nearby) { + Self.ShowLayerNamed("peek-"+direction); + } else { + Self.ShowLayerNamed("sleep"); + } + + if (below) { + state = states.drop; + } else if (nearby) { + state = states.peeking; + } + + break; + case states.peeking: + if (nearby) { + Self.ShowLayerNamed("peek-"+direction); + } else { + state = states.idle; + break; + } + + if (below) { + state = states.drop; + } + + break; + case states.drop: + // Begin the fall. + Self.ShowLayerNamed("angry"); + Self.SetVelocity(Vector(0.0, dropSpeed)); + lastAltitude = -point.Y; + state = states.falling; + case states.falling: + Self.ShowLayerNamed("angry"); + Self.SetVelocity(Vector(0.0, dropSpeed)); + + // Landed? + if (point.Y === lastAltitude) { + Sound.Play("crumbly-break.wav") + state = states.hit; + Self.PlayAnimation("hit", () => { + state = states.rising; + }); + } + break; + case states.hit: + // A transitory state while the hit animation + // plays out. + break; + case states.rising: + Self.ShowLayerNamed("sleep"); + Self.SetVelocity(Vector(0, -riseSpeed)); + + point = Self.Position(); + if (point.Y <= targetAltitude.Y+4 || point.Y === lastAltitude.Y) { + Self.MoveTo(targetAltitude); + Self.SetVelocity(Vector(0, 0)) + state = states.idle; + } + } + + lastAltitude = point.Y; + }, 100); +} + +// If under control of the player character. +function player() { + Events.OnKeypress((ev) => { + if (ev.Right) { + direction = "right"; + } else if (ev.Left) { + direction = "left"; + } + + // Jump! + if (ev.Down) { + Self.ShowLayerNamed("angry"); + return; + } else if (ev.Right && ev.Left) { + Self.ShowLayerNamed("ouch"); + } else if (ev.Right || ev.Left) { + Self.ShowLayerNamed("peek-"+direction); + } else { + Self.ShowLayerNamed("sleep"); + } + }); +} diff --git a/doodads/crusher/ouch.png b/doodads/crusher/ouch.png new file mode 100644 index 0000000..c982e6e Binary files /dev/null and b/doodads/crusher/ouch.png differ diff --git a/doodads/crusher/peek-left.png b/doodads/crusher/peek-left.png new file mode 100644 index 0000000..b2327ba Binary files /dev/null and b/doodads/crusher/peek-left.png differ diff --git a/doodads/crusher/peek-right.png b/doodads/crusher/peek-right.png new file mode 100644 index 0000000..b6c7115 Binary files /dev/null and b/doodads/crusher/peek-right.png differ diff --git a/doodads/crusher/sleep.png b/doodads/crusher/sleep.png new file mode 100644 index 0000000..796f656 Binary files /dev/null and b/doodads/crusher/sleep.png differ diff --git a/doodads/doors/colored-door.js b/doodads/doors/colored-door.js index 0bfba60..1c47ab6 100644 --- a/doodads/doors/colored-door.js +++ b/doodads/doors/colored-door.js @@ -1,9 +1,11 @@ -function main() { - var color = Self.GetTag("color"); - var keyname = color === "small" ? "small-key.doodad" : "key-" + color + ".doodad"; +// Colored Locked Doors. + +const color = Self.GetTag("color"), + keyname = color === "small" ? "small-key.doodad" : "key-" + color + ".doodad"; +function main() { // Layers in the doodad image. - var layer = { + let layer = { closed: 0, unlocked: 1, right: 2, @@ -11,13 +13,13 @@ function main() { }; // Variables that change in event handler. - var unlocked = false; // Key has been used to unlock the door (one time). - var opened = false; // If door is currently showing its opened state. - var enterSide = 0; // Side of player entering the door, -1 or 1, left or right. + let unlocked = false; // Key has been used to unlock the door (one time). + let opened = false; // If door is currently showing its opened state. + let enterSide = 0; // Side of player entering the door, -1 or 1, left or right. Self.SetHitbox(34, 0, 13, 76); - Events.OnCollide(function(e) { + Events.OnCollide((e) => { // Record the side that this actor has touched us, in case the door // needs to open. if (enterSide === 0) { @@ -37,7 +39,7 @@ function main() { } // Do they have our key? - var hasKey = e.Actor.HasItem(keyname) >= 0; + let hasKey = e.Actor.HasItem(keyname) >= 0; if (!hasKey) { // Door is locked. return false; @@ -55,7 +57,7 @@ function main() { } } }); - Events.OnLeave(function(e) { + Events.OnLeave((e) => { Self.ShowLayer(unlocked ? layer.unlocked : layer.closed); // Sound.Play("door-close.wav") diff --git a/doodads/doors/electric-door.js b/doodads/doors/electric-door.js index 1f4be7c..5c35201 100644 --- a/doodads/doors/electric-door.js +++ b/doodads/doors/electric-door.js @@ -1,27 +1,28 @@ -var animating = false; -var opened = false; -var powerState = false; +// Electric Door + +let animating = false; +let opened = false; +let powerState = false; // Function to handle the door opening or closing. function setPoweredState(powered) { powerState = powered; - console.log("setPoweredState: %+v", powered) if (powered) { if (animating || opened) { return; } animating = true; + opened = true; Sound.Play("electric-door.wav") - Self.PlayAnimation("open", function() { - opened = true; + Self.PlayAnimation("open", () => { animating = false; }); } else { animating = true; Sound.Play("electric-door.wav") - Self.PlayAnimation("close", function() { + Self.PlayAnimation("close", () => { opened = false; animating = false; }) @@ -32,7 +33,6 @@ function main() { Self.AddAnimation("open", 100, [0, 1, 2, 3]); Self.AddAnimation("close", 100, [3, 2, 1, 0]); - Self.SetHitbox(0, 0, 34, 76); // A linked Switch that activates the door will send the Toggle signal @@ -40,14 +40,13 @@ function main() { // state on this signal, and ignore the very next Power signal. Ordinary // power sources like Buttons will work as normal, as they emit only a power // signal. - var ignoreNextPower = false; - Message.Subscribe("switch:toggle", function(powered) { - console.log("A switch powered %+v, setPoweredState(%+v) to opposite", powered, powerState); + let ignoreNextPower = false; + Message.Subscribe("switch:toggle", (powered) => { ignoreNextPower = true; setPoweredState(!powerState); }) - Message.Subscribe("power", function(powered) { + Message.Subscribe("power", (powered) => { if (ignoreNextPower) { ignoreNextPower = false; return; @@ -56,7 +55,7 @@ function main() { setPoweredState(powered); }); - Events.OnCollide(function(e) { + Events.OnCollide((e) => { if (e.InHitbox) { if (!opened) { return false; diff --git a/doodads/doors/keys.js b/doodads/doors/keys.js index 469a158..c51154d 100644 --- a/doodads/doors/keys.js +++ b/doodads/doors/keys.js @@ -1,10 +1,18 @@ -function main() { - var color = Self.GetTag("color"); - var quantity = color === "small" ? 1 : 0; +// Colored Keys and Small Key + +const color = Self.GetTag("color"), + quantity = color === "small" ? 1 : 0; - Events.OnCollide(function (e) { +function main() { + Events.OnCollide((e) => { if (e.Settled) { if (e.Actor.HasInventory()) { + // If we don't have a quantity, and the actor already has + // one of us, don't pick it up so the player can get it. + if (quantity === 0 && e.Actor.HasItem(Self.Filename) === 0 && !e.Actor.IsPlayer()) { + return; + } + Sound.Play("item-get.wav") e.Actor.AddItem(Self.Filename, quantity); Self.Destroy(); diff --git a/doodads/doors/locked-door.js b/doodads/doors/locked-door.js deleted file mode 100644 index 820ae1a..0000000 --- a/doodads/doors/locked-door.js +++ /dev/null @@ -1,27 +0,0 @@ -// DEPRECATED: old locked door script. Superceded by colored-door.js. -function main() { - Self.AddAnimation("open", 0, [1]); - var unlocked = false; - var color = Self.GetTag("color"); - - Self.SetHitbox(16, 0, 32, 64); - - Events.OnCollide(function(e) { - if (unlocked) { - return; - } - - if (e.InHitbox) { - var data = e.Actor.GetData("key:" + color); - if (data === "") { - // Door is locked. - return false; - } - - if (e.Settled) { - unlocked = true; - Self.PlayAnimation("open", null); - } - } - }); -} diff --git a/doodads/gems/Makefile b/doodads/gems/Makefile new file mode 100644 index 0000000..f9e5d87 --- /dev/null +++ b/doodads/gems/Makefile @@ -0,0 +1,58 @@ +ALL: build + +.PHONY: build +build: + ### + # Gemstones + ### + + doodad convert -t "Gemstone (Green)" green-1.png green-2.png green-3.png green-4.png \ + gem-green.doodad + doodad install-script gemstone.js gem-green.doodad + doodad edit-doodad --tag "color=green" gem-green.doodad + + doodad convert -t "Gemstone (Red)" red-1.png red-2.png red-3.png red-4.png \ + gem-red.doodad + doodad install-script gemstone.js gem-red.doodad + doodad edit-doodad --tag "color=red" gem-red.doodad + + doodad convert -t "Gemstone (Blue)" blue-1.png blue-2.png blue-3.png blue-4.png \ + gem-blue.doodad + doodad install-script gemstone.js gem-blue.doodad + doodad edit-doodad --tag "color=blue" gem-blue.doodad + + doodad convert -t "Gemstone (Yellow)" yellow-1.png yellow-2.png yellow-3.png yellow-4.png \ + gem-yellow.doodad + doodad install-script gemstone.js gem-yellow.doodad + doodad edit-doodad --tag "color=yellow" gem-yellow.doodad + + ### + # Totems + ### + + doodad convert -t "Gemstone Totem (Green)" totem-green-1.png totem-green-2.png totem-green-3.png \ + totem-green-4.png totem-green-0.png gem-totem-green.doodad + doodad install-script totem.js gem-totem-green.doodad + doodad edit-doodad --tag "color=green" gem-totem-green.doodad + + doodad convert -t "Gemstone Totem (Yellow)" totem-yellow-1.png totem-yellow-2.png totem-yellow-3.png \ + totem-yellow-4.png totem-yellow-0.png gem-totem-yellow.doodad + doodad install-script totem.js gem-totem-yellow.doodad + doodad edit-doodad --tag "color=yellow" gem-totem-yellow.doodad + + doodad convert -t "Gemstone Totem (Blue)" totem-blue-1.png totem-blue-2.png totem-blue-3.png \ + totem-blue-4.png totem-blue-0.png gem-totem-blue.doodad + doodad install-script totem.js gem-totem-blue.doodad + doodad edit-doodad --tag "color=blue" gem-totem-blue.doodad + + doodad convert -t "Gemstone Totem (Red)" totem-red-1.png totem-red-2.png totem-red-3.png \ + totem-red-4.png totem-red-0.png gem-totem-red.doodad + doodad install-script totem.js gem-totem-red.doodad + doodad edit-doodad --tag "color=red" gem-totem-red.doodad + + # Tag the category for these doodads + for i in *.doodad; do\ + doodad edit-doodad --tag "category=doors" $${i};\ + done + + cp *.doodad ../../../assets/doodads/ \ No newline at end of file diff --git a/doodads/gems/blue-1.png b/doodads/gems/blue-1.png new file mode 100644 index 0000000..c74d062 Binary files /dev/null and b/doodads/gems/blue-1.png differ diff --git a/doodads/gems/blue-2.png b/doodads/gems/blue-2.png new file mode 100644 index 0000000..2760b45 Binary files /dev/null and b/doodads/gems/blue-2.png differ diff --git a/doodads/gems/blue-3.png b/doodads/gems/blue-3.png new file mode 100644 index 0000000..b40177c Binary files /dev/null and b/doodads/gems/blue-3.png differ diff --git a/doodads/gems/blue-4.png b/doodads/gems/blue-4.png new file mode 100644 index 0000000..63f8480 Binary files /dev/null and b/doodads/gems/blue-4.png differ diff --git a/doodads/gems/gemstone.js b/doodads/gems/gemstone.js new file mode 100644 index 0000000..efb9361 --- /dev/null +++ b/doodads/gems/gemstone.js @@ -0,0 +1,24 @@ +// Gem stone collectibles/keys. + +const color = Self.GetTag("color"), + shimmerFreq = 1000; + +function main() { + Self.SetMobile(true); + Self.SetGravity(true); + + Self.AddAnimation("shimmer", 100, [0, 1, 2, 3, 0]); + Events.OnCollide((e) => { + if (e.Settled) { + if (e.Actor.HasInventory()) { + Sound.Play("item-get.wav") + e.Actor.AddItem(Self.Filename, 1); + Self.Destroy(); + } + } + }); + + setInterval(() => { + Self.PlayAnimation("shimmer", null); + }, shimmerFreq); +} \ No newline at end of file diff --git a/doodads/gems/green-1.png b/doodads/gems/green-1.png new file mode 100644 index 0000000..ff76d09 Binary files /dev/null and b/doodads/gems/green-1.png differ diff --git a/doodads/gems/green-2.png b/doodads/gems/green-2.png new file mode 100644 index 0000000..996d565 Binary files /dev/null and b/doodads/gems/green-2.png differ diff --git a/doodads/gems/green-3.png b/doodads/gems/green-3.png new file mode 100644 index 0000000..8449974 Binary files /dev/null and b/doodads/gems/green-3.png differ diff --git a/doodads/gems/green-4.png b/doodads/gems/green-4.png new file mode 100644 index 0000000..a6e7064 Binary files /dev/null and b/doodads/gems/green-4.png differ diff --git a/doodads/gems/red-1.png b/doodads/gems/red-1.png new file mode 100644 index 0000000..9a14ea0 Binary files /dev/null and b/doodads/gems/red-1.png differ diff --git a/doodads/gems/red-2.png b/doodads/gems/red-2.png new file mode 100644 index 0000000..fdcf412 Binary files /dev/null and b/doodads/gems/red-2.png differ diff --git a/doodads/gems/red-3.png b/doodads/gems/red-3.png new file mode 100644 index 0000000..10ecf3c Binary files /dev/null and b/doodads/gems/red-3.png differ diff --git a/doodads/gems/red-4.png b/doodads/gems/red-4.png new file mode 100644 index 0000000..982ec5a Binary files /dev/null and b/doodads/gems/red-4.png differ diff --git a/doodads/gems/totem-blue-0.png b/doodads/gems/totem-blue-0.png new file mode 100644 index 0000000..0775a22 Binary files /dev/null and b/doodads/gems/totem-blue-0.png differ diff --git a/doodads/gems/totem-blue-1.png b/doodads/gems/totem-blue-1.png new file mode 100644 index 0000000..0f561a3 Binary files /dev/null and b/doodads/gems/totem-blue-1.png differ diff --git a/doodads/gems/totem-blue-2.png b/doodads/gems/totem-blue-2.png new file mode 100644 index 0000000..dbc0447 Binary files /dev/null and b/doodads/gems/totem-blue-2.png differ diff --git a/doodads/gems/totem-blue-3.png b/doodads/gems/totem-blue-3.png new file mode 100644 index 0000000..8373a3c Binary files /dev/null and b/doodads/gems/totem-blue-3.png differ diff --git a/doodads/gems/totem-blue-4.png b/doodads/gems/totem-blue-4.png new file mode 100644 index 0000000..8bb6a80 Binary files /dev/null and b/doodads/gems/totem-blue-4.png differ diff --git a/doodads/gems/totem-green-0.png b/doodads/gems/totem-green-0.png new file mode 100644 index 0000000..ff0f54d Binary files /dev/null and b/doodads/gems/totem-green-0.png differ diff --git a/doodads/gems/totem-green-1.png b/doodads/gems/totem-green-1.png new file mode 100644 index 0000000..4a2e33f Binary files /dev/null and b/doodads/gems/totem-green-1.png differ diff --git a/doodads/gems/totem-green-2.png b/doodads/gems/totem-green-2.png new file mode 100644 index 0000000..ae892e6 Binary files /dev/null and b/doodads/gems/totem-green-2.png differ diff --git a/doodads/gems/totem-green-3.png b/doodads/gems/totem-green-3.png new file mode 100644 index 0000000..b850991 Binary files /dev/null and b/doodads/gems/totem-green-3.png differ diff --git a/doodads/gems/totem-green-4.png b/doodads/gems/totem-green-4.png new file mode 100644 index 0000000..4aa4c7d Binary files /dev/null and b/doodads/gems/totem-green-4.png differ diff --git a/doodads/gems/totem-red-0.png b/doodads/gems/totem-red-0.png new file mode 100644 index 0000000..404245c Binary files /dev/null and b/doodads/gems/totem-red-0.png differ diff --git a/doodads/gems/totem-red-1.png b/doodads/gems/totem-red-1.png new file mode 100644 index 0000000..e7c1a35 Binary files /dev/null and b/doodads/gems/totem-red-1.png differ diff --git a/doodads/gems/totem-red-2.png b/doodads/gems/totem-red-2.png new file mode 100644 index 0000000..d08ce21 Binary files /dev/null and b/doodads/gems/totem-red-2.png differ diff --git a/doodads/gems/totem-red-3.png b/doodads/gems/totem-red-3.png new file mode 100644 index 0000000..bd56142 Binary files /dev/null and b/doodads/gems/totem-red-3.png differ diff --git a/doodads/gems/totem-red-4.png b/doodads/gems/totem-red-4.png new file mode 100644 index 0000000..98ddcc9 Binary files /dev/null and b/doodads/gems/totem-red-4.png differ diff --git a/doodads/gems/totem-yellow-0.png b/doodads/gems/totem-yellow-0.png new file mode 100644 index 0000000..5865701 Binary files /dev/null and b/doodads/gems/totem-yellow-0.png differ diff --git a/doodads/gems/totem-yellow-1.png b/doodads/gems/totem-yellow-1.png new file mode 100644 index 0000000..66c163b Binary files /dev/null and b/doodads/gems/totem-yellow-1.png differ diff --git a/doodads/gems/totem-yellow-2.png b/doodads/gems/totem-yellow-2.png new file mode 100644 index 0000000..9cb7081 Binary files /dev/null and b/doodads/gems/totem-yellow-2.png differ diff --git a/doodads/gems/totem-yellow-3.png b/doodads/gems/totem-yellow-3.png new file mode 100644 index 0000000..da774b7 Binary files /dev/null and b/doodads/gems/totem-yellow-3.png differ diff --git a/doodads/gems/totem-yellow-4.png b/doodads/gems/totem-yellow-4.png new file mode 100644 index 0000000..e227b7c Binary files /dev/null and b/doodads/gems/totem-yellow-4.png differ diff --git a/doodads/gems/totem.js b/doodads/gems/totem.js new file mode 100644 index 0000000..3598f50 --- /dev/null +++ b/doodads/gems/totem.js @@ -0,0 +1,100 @@ +// Gem stone totem socket. + +/* +The Totem is a type of key-door that holds onto its corresponding +Gemstone. When a doodad holding the right Gemstone touches the +totem, the totem takes the gemstone and activates. + +If the Totem is not linked to any other Totems, it immediately +sends a power(true) signal upon activation. + +If the Totem is linked to other Totems, it waits until all totems +have been activated before it will emit a power signal. Only one +such totem needs to be linked to e.g. an Electric Door - no matter +which totem is solved last, they'll all emit a power signal when +all of their linked totems are activated. +*/ + +let color = Self.GetTag("color"), + keyname = "gem-"+color+".doodad", + activated = false, + linkedReceiver = false, // is linked to a non-totem which might want power + totems = {}, // linked totems + shimmerFreq = 1000; + +function main() { + // Show the hollow socket on level load (last layer) + Self.ShowLayer(4); + + // Find any linked totems. + for (let link of Self.GetLinks()) { + if (link.Filename.indexOf("gem-totem") > -1) { + totems[link.ID()] = false; + } else { + linkedReceiver = true; + } + } + + // Shimmer animation is just like the gemstones: first 4 frames + // are the filled socket sprites. + Self.AddAnimation("shimmer", 100, [0, 1, 2, 3, 0]); + + Events.OnCollide((e) => { + if (activated) return; + + if (e.Actor.IsMobile() && e.Settled) { + // Do they have our gemstone? + let hasKey = e.Actor.HasItem(keyname) >= 0; + if (!hasKey) { + return; + } + + // Take the gemstone. + e.Actor.RemoveItem(keyname, 1); + Self.ShowLayer(0); + + // Emit to our linked totem neighbors. + activated = true; + Message.Publish("gem-totem:activated", Self.ID()); + tryPower(); + } + }); + + Message.Subscribe("gem-totem:activated", (totemId) => { + totems[totemId] = true; + tryPower(); + }) + + setInterval(() => { + if (activated) { + Self.PlayAnimation("shimmer", null); + } + }, shimmerFreq); +} + +// Try to send a power signal for an activated totem. +function tryPower() { + // Only emit power if we are linked to something other than a totem. + if (!linkedReceiver) { + return; + } + + // Can't if any of our linked totems aren't activated. + try { + for (let totemId of Object.keys(totems)) { + if (totems[totemId] === false) { + return; + } + } + } catch(e) { + console.error("Caught: %s", e); + } + + // Can't if we aren't powered. + if (activated === false) { + return; + } + + // Emit power! + Message.Publish("power", true); +} \ No newline at end of file diff --git a/doodads/gems/yellow-1.png b/doodads/gems/yellow-1.png new file mode 100644 index 0000000..016d42a Binary files /dev/null and b/doodads/gems/yellow-1.png differ diff --git a/doodads/gems/yellow-2.png b/doodads/gems/yellow-2.png new file mode 100644 index 0000000..f602927 Binary files /dev/null and b/doodads/gems/yellow-2.png differ diff --git a/doodads/gems/yellow-3.png b/doodads/gems/yellow-3.png new file mode 100644 index 0000000..fb9ac0f Binary files /dev/null and b/doodads/gems/yellow-3.png differ diff --git a/doodads/gems/yellow-4.png b/doodads/gems/yellow-4.png new file mode 100644 index 0000000..904c7c9 Binary files /dev/null and b/doodads/gems/yellow-4.png differ diff --git a/doodads/objects/anvil.js b/doodads/objects/anvil.js index 2359bca..8ea5d59 100644 --- a/doodads/objects/anvil.js +++ b/doodads/objects/anvil.js @@ -6,11 +6,12 @@ function main() { Self.SetHitbox(0, 0, 48, 25); Self.SetMobile(true); Self.SetGravity(true); + Self.SetInvulnerable(true); // Monitor our Y position to tell if we've been falling. - var lastPoint = Self.Position(); - setInterval(function () { - var nowAt = Self.Position(); + let lastPoint = Self.Position(); + setInterval(() => { + let nowAt = Self.Position(); if (nowAt.Y > lastPoint.Y) { falling = true; } else { @@ -19,7 +20,7 @@ function main() { lastPoint = nowAt; }, 100); - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (!e.Settled) { return; } @@ -33,7 +34,7 @@ function main() { FailLevel("Watch out for anvils!"); return; } - else if (e.Actor.IsMobile()) { + else if (e.Actor.IsMobile() && !e.Actor.Invulnerable()) { // Destroy mobile doodads. Sound.Play("crumbly-break.wav"); e.Actor.Destroy(); @@ -43,8 +44,8 @@ function main() { }); // When we receive power, we reset to our original position. - var origPoint = Self.Position(); - Message.Subscribe("power", function (powered) { + let origPoint = Self.Position(); + Message.Subscribe("power", (powered) => { Self.MoveTo(origPoint); Self.SetVelocity(Vector(0, 0)); }); diff --git a/doodads/objects/checkpoint-flag.js b/doodads/objects/checkpoint-flag.js index a50608a..5b28815 100644 --- a/doodads/objects/checkpoint-flag.js +++ b/doodads/objects/checkpoint-flag.js @@ -1,17 +1,28 @@ // Checkpoint Flag. -var isCurrentCheckpoint = false; +var isCurrentCheckpoint = false, + playerEntered = false + broadcastCooldown = time.Now(); function main() { Self.SetHitbox(22 + 16, 16, 75 - 16, 86); setActive(false); + // If the checkpoint is linked to any doodad, the player character will + // become that doodad when they cross this checkpoint. + let skin = null; + for (let actor of Self.GetLinks()) { + skin = actor.Filename; + actor.Destroy(); + } + // Checkpoints broadcast to all of their peers so they all // know which one is the most recently activated. - Message.Subscribe("broadcast:checkpoint", function (currentID) { + Message.Subscribe("broadcast:checkpoint", (currentID) => { setActive(false); + return "a ok"; }); - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (!e.Settled) { return; } @@ -21,10 +32,19 @@ function main() { return; } - // Set the player checkpoint. SetCheckpoint(Self.Position()); setActive(true); - Message.Broadcast("broadcast:checkpoint", Self.ID()) + + // Don't spam the PubSub queue or we get races and deadlocks. + if (time.Now().After(broadcastCooldown)) { + Message.Broadcast("broadcast:checkpoint", Self.ID()); + broadcastCooldown = time.Now().Add(5 * time.Second) + } + + // Are we setting a new player skin? + if (skin && e.Actor.Doodad().Filename !== skin) { + Actors.SetPlayerCharacter(skin); + } }); } @@ -35,4 +55,4 @@ function setActive(v) { isCurrentCheckpoint = v; Self.ShowLayerNamed(v ? "checkpoint-active" : "checkpoint-inactive"); -} \ No newline at end of file +} diff --git a/doodads/objects/exit-flag.js b/doodads/objects/exit-flag.js index 9e65bf1..d45a754 100644 --- a/doodads/objects/exit-flag.js +++ b/doodads/objects/exit-flag.js @@ -2,7 +2,7 @@ function main() { Self.SetHitbox(22 + 16, 16, 75 - 16, 86); - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (!e.Settled) { return; } diff --git a/doodads/objects/start-flag.js b/doodads/objects/start-flag.js index dc84a77..f215651 100644 --- a/doodads/objects/start-flag.js +++ b/doodads/objects/start-flag.js @@ -4,8 +4,7 @@ function main() { // Linking a doodad to the Start Flag sets the // player character. Destroy the original doodads. - var links = Self.GetLinks(); - for (var i = 0; i < links.length; i++) { - links[i].Destroy(); + for (var link of Self.GetLinks()) { + link.Destroy(); } } diff --git a/doodads/on-off/state-block-blue.js b/doodads/on-off/state-block-blue.js index b754ea7..16e1421 100644 --- a/doodads/on-off/state-block-blue.js +++ b/doodads/on-off/state-block-blue.js @@ -3,9 +3,9 @@ function main() { Self.SetHitbox(0, 0, 42, 42); // Blue block is ON by default. - var state = true; + let state = true; - Message.Subscribe("broadcast:state-change", function(newState) { + Message.Subscribe("broadcast:state-change", (newState) => { state = !newState; // Layer 0: ON @@ -13,7 +13,7 @@ function main() { Self.ShowLayer(state ? 0 : 1); }); - Events.OnCollide(function(e) { + Events.OnCollide((e) => { if (e.Actor.IsMobile() && e.InHitbox) { if (state) { return false; diff --git a/doodads/on-off/state-block-orange.js b/doodads/on-off/state-block-orange.js index 8989f73..7e3d518 100644 --- a/doodads/on-off/state-block-orange.js +++ b/doodads/on-off/state-block-orange.js @@ -3,9 +3,9 @@ function main() { Self.SetHitbox(0, 0, 42, 42); // Orange block is OFF by default. - var state = false; + let state = false; - Message.Subscribe("broadcast:state-change", function(newState) { + Message.Subscribe("broadcast:state-change", (newState) => { state = newState; // Layer 0: OFF @@ -13,7 +13,7 @@ function main() { Self.ShowLayer(state ? 1 : 0); }); - Events.OnCollide(function(e) { + Events.OnCollide((e) => { if (e.Actor.IsMobile() && e.InHitbox) { if (state) { return false; diff --git a/doodads/on-off/state-button.js b/doodads/on-off/state-button.js index f0b02a6..291260c 100644 --- a/doodads/on-off/state-button.js +++ b/doodads/on-off/state-button.js @@ -1,23 +1,23 @@ // State Block Control Button // Button is "OFF" by default. -var state = false; +let state = false; function main() { Self.SetHitbox(0, 0, 42, 42); // When the button is activated, don't keep toggling state until we're not // being touched again. - var colliding = false; + let colliding = false; // If we receive a state change event from a DIFFERENT on/off button, update // ourself to match the state received. - Message.Subscribe("broadcast:state-change", function(value) { + Message.Subscribe("broadcast:state-change", (value) => { state = value; showSprite(); }); - Events.OnCollide(function(e) { + Events.OnCollide((e) => { if (colliding) { return false; } @@ -40,7 +40,7 @@ function main() { return false; }); - Events.OnLeave(function(e) { + Events.OnLeave((e) => { colliding = false; }) } diff --git a/doodads/regions/Makefile b/doodads/regions/Makefile index 7d8c6b2..1c3f568 100644 --- a/doodads/regions/Makefile +++ b/doodads/regions/Makefile @@ -23,7 +23,16 @@ build: doodad convert -t "Power Source" power-64.png power-source.doodad doodad install-script power.js power-source.doodad + # Warp Door + doodad convert -t "Invisible Warp Door" warp-door-64.png reg-warp-door.doodad + doodad edit-doodad --tag "color=invisible" reg-warp-door.doodad + doodad install-script ../warp-door/warp-door.js reg-warp-door.doodad + + # Reset Level Timer + doodad convert -t "Reset Level Timer" timer-64.png reg-reset-timer.doodad + doodad install-script reset-timer.js reg-reset-timer.doodad + for i in *.doodad; do\ doodad edit-doodad --tag "category=technical" $${i};\ done - cp *.doodad ../../../assets/doodads/ \ No newline at end of file + cp *.doodad ../../../assets/doodads/ diff --git a/doodads/regions/checkpoint-128.png b/doodads/regions/checkpoint-128.png index bc1c34e..13766e6 100644 Binary files a/doodads/regions/checkpoint-128.png and b/doodads/regions/checkpoint-128.png differ diff --git a/doodads/regions/checkpoint.js b/doodads/regions/checkpoint.js index 47f5669..1645f69 100644 --- a/doodads/regions/checkpoint.js +++ b/doodads/regions/checkpoint.js @@ -8,11 +8,11 @@ function main() { // Checkpoints broadcast to all of their peers so they all // know which one is the most recently activated. - Message.Subscribe("broadcast:checkpoint", function (currentID) { + Message.Subscribe("broadcast:checkpoint", (currentID) => { setActive(false); }); - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (isCurrentCheckpoint || !e.Settled) { return; } @@ -35,4 +35,4 @@ function setActive(v) { } isCurrentCheckpoint = v; -} \ No newline at end of file +} diff --git a/doodads/regions/fire.js b/doodads/regions/fire.js index aae40c8..977537a 100644 --- a/doodads/regions/fire.js +++ b/doodads/regions/fire.js @@ -2,7 +2,7 @@ function main() { Self.Hide(); - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (!e.Settled) { return; } diff --git a/doodads/regions/goal-128.png b/doodads/regions/goal-128.png index b769917..c93dd3c 100644 Binary files a/doodads/regions/goal-128.png and b/doodads/regions/goal-128.png differ diff --git a/doodads/regions/goal.js b/doodads/regions/goal.js index 1d06f8f..6157c9d 100644 --- a/doodads/regions/goal.js +++ b/doodads/regions/goal.js @@ -2,7 +2,7 @@ function main() { Self.Hide(); - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (!e.Settled) { return; } diff --git a/doodads/regions/power-64.png b/doodads/regions/power-64.png index cfe47f5..14a743c 100644 Binary files a/doodads/regions/power-64.png and b/doodads/regions/power-64.png differ diff --git a/doodads/regions/power.js b/doodads/regions/power.js index 072a526..6b555a3 100644 --- a/doodads/regions/power.js +++ b/doodads/regions/power.js @@ -15,7 +15,7 @@ function main() { ); } - Message.Subscribe("broadcast:ready", function () { + Message.Subscribe("broadcast:ready", () => { Message.Publish("switch:toggle", true); Message.Publish("power", true); }); diff --git a/doodads/regions/reset-timer.js b/doodads/regions/reset-timer.js new file mode 100644 index 0000000..eb12c60 --- /dev/null +++ b/doodads/regions/reset-timer.js @@ -0,0 +1,30 @@ +// Reset Level Timer. +function main() { + Self.Hide(); + + // Reset the level timer only once. + let hasReset = false; + + Events.OnCollide((e) => { + if (!e.Settled) { + return; + } + + // Only care if it's the player. + if (!e.Actor.IsPlayer()) { + return; + } + + if (e.InHitbox && !hasReset) { + Level.ResetTimer(); + hasReset = true; + } + }); + + // Receive a power signal resets the doodad. + Message.Subscribe("power", (powered) => { + if (powered) { + hasReset = true; + } + }); +} diff --git a/doodads/regions/stall.js b/doodads/regions/stall.js index c174594..56cf5d4 100644 --- a/doodads/regions/stall.js +++ b/doodads/regions/stall.js @@ -4,7 +4,7 @@ function main() { Self.Hide(); - var active = true, + let active = true, timeout = 250, ms = Self.GetTag("ms"); @@ -12,7 +12,7 @@ function main() { timeout = parseInt(ms); } - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (!active || !e.Settled) { return; } @@ -25,7 +25,7 @@ function main() { if (e.InHitbox) { // Grab hold of the player. e.Actor.Freeze(); - setTimeout(function () { + setTimeout(() => { e.Actor.Unfreeze(); }, timeout); @@ -34,7 +34,7 @@ function main() { }); // Reset the trap if powered by a button. - Message.Subscribe("power", function (powered) { + Message.Subscribe("power", (powered) => { active = true; }); } diff --git a/doodads/regions/timer-64.png b/doodads/regions/timer-64.png new file mode 100644 index 0000000..4c55879 Binary files /dev/null and b/doodads/regions/timer-64.png differ diff --git a/doodads/regions/warp-door-64.png b/doodads/regions/warp-door-64.png new file mode 100644 index 0000000..f2b31d4 Binary files /dev/null and b/doodads/regions/warp-door-64.png differ diff --git a/doodads/snake/Makefile b/doodads/snake/Makefile new file mode 100644 index 0000000..6fa8016 --- /dev/null +++ b/doodads/snake/Makefile @@ -0,0 +1,15 @@ +ALL: build + +.PHONY: build +build: + doodad convert -t "Snake" left-1.png left-2.png left-3.png right-1.png right-2.png right-3.png \ + attack-left-1.png attack-left-2.png attack-left-3.png attack-right-1.png attack-right-2.png \ + attack-right-3.png snake.doodad + doodad install-script snake.js snake.doodad + + # Tag the category for these doodads + for i in *.doodad; do\ + doodad edit-doodad --tag "category=creatures" $${i};\ + done + + cp *.doodad ../../../assets/doodads/ \ No newline at end of file diff --git a/doodads/snake/attack-left-1.png b/doodads/snake/attack-left-1.png new file mode 100644 index 0000000..d7e3cdf Binary files /dev/null and b/doodads/snake/attack-left-1.png differ diff --git a/doodads/snake/attack-left-2.png b/doodads/snake/attack-left-2.png new file mode 100644 index 0000000..8fec20b Binary files /dev/null and b/doodads/snake/attack-left-2.png differ diff --git a/doodads/snake/attack-left-3.png b/doodads/snake/attack-left-3.png new file mode 100644 index 0000000..00948cd Binary files /dev/null and b/doodads/snake/attack-left-3.png differ diff --git a/doodads/snake/attack-right-1.png b/doodads/snake/attack-right-1.png new file mode 100644 index 0000000..06f935e Binary files /dev/null and b/doodads/snake/attack-right-1.png differ diff --git a/doodads/snake/attack-right-2.png b/doodads/snake/attack-right-2.png new file mode 100644 index 0000000..6bbef9c Binary files /dev/null and b/doodads/snake/attack-right-2.png differ diff --git a/doodads/snake/attack-right-3.png b/doodads/snake/attack-right-3.png new file mode 100644 index 0000000..e71d3cb Binary files /dev/null and b/doodads/snake/attack-right-3.png differ diff --git a/doodads/snake/left-1.png b/doodads/snake/left-1.png new file mode 100644 index 0000000..9c76386 Binary files /dev/null and b/doodads/snake/left-1.png differ diff --git a/doodads/snake/left-2.png b/doodads/snake/left-2.png new file mode 100644 index 0000000..f86887a Binary files /dev/null and b/doodads/snake/left-2.png differ diff --git a/doodads/snake/left-3.png b/doodads/snake/left-3.png new file mode 100644 index 0000000..9c8eedb Binary files /dev/null and b/doodads/snake/left-3.png differ diff --git a/doodads/snake/right-1.png b/doodads/snake/right-1.png new file mode 100644 index 0000000..571b070 Binary files /dev/null and b/doodads/snake/right-1.png differ diff --git a/doodads/snake/right-2.png b/doodads/snake/right-2.png new file mode 100644 index 0000000..37828ce Binary files /dev/null and b/doodads/snake/right-2.png differ diff --git a/doodads/snake/right-3.png b/doodads/snake/right-3.png new file mode 100644 index 0000000..6b7b90c Binary files /dev/null and b/doodads/snake/right-3.png differ diff --git a/doodads/snake/snake.js b/doodads/snake/snake.js new file mode 100644 index 0000000..952f742 --- /dev/null +++ b/doodads/snake/snake.js @@ -0,0 +1,131 @@ +// Snake + +/* +A.I. Behaviors: + +- Always turns to face the nearest player character +- Jumps up when the player tries to jump over them, + aiming to attack the player. +*/ + +let direction = "left", + jumpSpeed = 12, + watchRadius = 300, // player nearby distance for snake to jump + jumpCooldownStart = time.Now(), + size = Self.Size(); + +const states = { + idle: 0, + attacking: 1, +}; +let state = states.idle; + +function main() { + Self.SetMobile(true); + Self.SetGravity(true); + Self.SetHitbox(20, 0, 28, 58); + Self.AddAnimation("idle-left", 100, ["left-1", "left-2", "left-3", "left-2"]); + Self.AddAnimation("idle-right", 100, ["right-1", "right-2", "right-3", "right-2"]); + Self.AddAnimation("attack-left", 100, ["attack-left-1", "attack-left-2", "attack-left-3"]) + Self.AddAnimation("attack-right", 100, ["attack-right-1", "attack-right-2", "attack-right-3"]) + + // Player Character controls? + if (Self.IsPlayer()) { + return player(); + } + + Events.OnCollide((e) => { + // The snake is deadly to the touch. + if (e.Settled && e.Actor.IsPlayer() && e.InHitbox) { + // Friendly to fellow snakes. + if (e.Actor.Doodad().Filename.indexOf("snake") > -1) { + return; + } + + FailLevel("Watch out for snakes!"); + return; + } + }); + + setInterval(() => { + // Find the player. + let player = Actors.FindPlayer(), + playerPoint = player.Position(), + point = Self.Position(), + delta = 0, + nearby = false; + + // Face the player. + if (playerPoint.X < point.X + (size.W / 2)) { + direction = "left"; + delta = Math.abs(playerPoint.X - (point.X + (size.W/2))); + } + else if (playerPoint.X > point.X + (size.W / 2)) { + direction = "right"; + delta = Math.abs(playerPoint.X - (point.X + (size.W/2))); + } + + if (delta < watchRadius) { + nearby = true; + } + + // If we are idle and the player is jumping nearby... + if (state == states.idle && nearby && Self.Grounded()) { + if (playerPoint.Y - point.Y+(size.H/2) < 20) { + // Enter attack state. + if (time.Since(jumpCooldownStart) > 500 * time.Millisecond) { + state = states.attacking; + Self.SetVelocity(Vector(0, -jumpSpeed)); + Self.StopAnimation(); + Self.PlayAnimation("attack-"+direction, null); + return; + } + } + } + + // If we are attacking and gravity has claimed us back. + if (state === states.attacking && Self.Grounded()) { + state = states.idle; + jumpCooldownStart = time.Now(); + Self.StopAnimation(); + } + + // Ensure that the animations are always rolling. + if (state === states.idle && !Self.IsAnimating()) { + Self.PlayAnimation("idle-"+direction, null); + } + }, 100); +} + +// If under control of the player character. +function player() { + let jumping = false; + + Events.OnKeypress((ev) => { + Vx = 0; + Vy = 0; + + if (ev.Right) { + direction = "right"; + } else if (ev.Left) { + direction = "left"; + } + + // Jump! + if (ev.Up && !jumping) { + Self.StopAnimation(); + Self.PlayAnimation("attack-"+direction, null); + jumping = true; + return; + } + + if (jumping && Self.Grounded()) { + Self.StopAnimation(); + jumping = false; + } + + if (!jumping && !Self.IsAnimating()) { + Self.PlayAnimation("idle-"+direction, null); + } + }); +} diff --git a/doodads/switches/switch.js b/doodads/switches/switch.js index 973a7b3..99fa153 100644 --- a/doodads/switches/switch.js +++ b/doodads/switches/switch.js @@ -4,15 +4,15 @@ function main() { // 0: Off // 1: On - var state = false; - var collide = false; + let state = false; + let collide = false; - Message.Subscribe("power", function (powered) { + Message.Subscribe("power", (powered) => { state = powered; showState(state); }); - Events.OnCollide(function (e) { + Events.OnCollide((e) => { if (!e.Settled || !e.Actor.IsMobile()) { return; } @@ -21,7 +21,6 @@ function main() { Sound.Play("button-down.wav") state = !state; - var nonce = Math.random() * 2147483647; Message.Publish("switch:toggle", state); Message.Publish("power", state); showState(state); @@ -30,7 +29,7 @@ function main() { } }); - Events.OnLeave(function (e) { + Events.OnLeave((e) => { collide = false; }); } diff --git a/doodads/thief/thief.js b/doodads/thief/thief.js index bdc945a..77c2a70 100644 --- a/doodads/thief/thief.js +++ b/doodads/thief/thief.js @@ -21,8 +21,8 @@ function main() { // Common "steal" power between playable and A.I. thieves. function stealable() { // Steals your items. - Events.OnCollide(function (e) { - var victim = e.Actor; + Events.OnCollide((e) => { + let victim = e.Actor; if (!e.Settled) { return; } @@ -33,17 +33,17 @@ function stealable() { } // Steal inventory - var stolen = 0; + let stolen = 0; if (victim.HasInventory()) { - var myInventory = Self.Inventory(), + let myInventory = Self.Inventory(), theirInventory = victim.Inventory(); - for (var key in theirInventory) { + for (let key in theirInventory) { if (!theirInventory.hasOwnProperty(key)) { continue; } - var value = theirInventory[key]; + let value = theirInventory[key]; if (value > 0 || myInventory[key] === undefined) { victim.RemoveItem(key, value); Self.AddItem(key, value); @@ -68,7 +68,7 @@ function stealable() { // when it encounters and obstacle. function ai() { // Walks back and forth. - var Vx = Vy = 0.0, + let Vx = Vy = 0.0, playerSpeed = 4, direction = "right", lastDirection = "right", @@ -76,9 +76,9 @@ function ai() { sampleTick = 0, sampleRate = 2; - setInterval(function () { + setInterval(() => { if (sampleTick % sampleRate === 0) { - var curX = Self.Position().X, + let curX = Self.Position().X, delta = Math.abs(curX - lastSampledX); if (delta < 5) { direction = direction === "right" ? "left" : "right"; @@ -106,25 +106,18 @@ function ai() { // If under control of the player character. function playable() { - Events.OnKeypress(function (ev) { - Vx = 0; - Vy = 0; - + Events.OnKeypress((ev) => { if (ev.Right) { if (!Self.IsAnimating()) { Self.PlayAnimation("walk-right", null); } - Vx = playerSpeed; } else if (ev.Left) { if (!Self.IsAnimating()) { Self.PlayAnimation("walk-left", null); } - Vx = -playerSpeed; } else { Self.StopAnimation(); animating = false; } - - // Self.SetVelocity(Point(Vx, Vy)); }) -} \ No newline at end of file +} diff --git a/doodads/trapdoors/down.js b/doodads/trapdoors/down.js deleted file mode 100644 index 3ea5fba..0000000 --- a/doodads/trapdoors/down.js +++ /dev/null @@ -1,36 +0,0 @@ -function main() { - var timer = 0; - - Self.SetHitbox(0, 0, 72, 6); - - var animationSpeed = 100; - var opened = false; - Self.AddAnimation("open", animationSpeed, ["down1", "down2", "down3", "down4"]); - Self.AddAnimation("close", animationSpeed, ["down4", "down3", "down2", "down1"]); - - Events.OnCollide( function(e) { - if (opened) { - return; - } - - // Is the actor colliding our solid part? - if (e.InHitbox) { - // Touching the top or the bottom? - if (e.Overlap.Y > 0) { - return false; // solid wall when touched from below - } else { - opened = true; - Self.PlayAnimation("open", function() { - }); - } - } - }); - - Events.OnLeave(function() { - if (opened) { - Self.PlayAnimation("close", function() { - opened = false; - }); - } - }) -} diff --git a/doodads/trapdoors/trapdoor.js b/doodads/trapdoors/trapdoor.js index 54dca12..9bb6d70 100644 --- a/doodads/trapdoors/trapdoor.js +++ b/doodads/trapdoors/trapdoor.js @@ -1,12 +1,12 @@ -function main() { - // What direction is the trapdoor facing? - var direction = Self.GetTag("direction"); +// Trapdoors. - var timer = 0; +// What direction is the trapdoor facing? +const direction = Self.GetTag("direction"); +function main() { // Set our hitbox based on our orientation. - var thickness = 10; - var doodadSize = 86; + let thickness = 10; + let doodadSize = 86; if (direction === "left") { Self.SetHitbox(48, 0, doodadSize, doodadSize); } else if (direction === "right") { @@ -17,12 +17,12 @@ function main() { Self.SetHitbox(0, 0, doodadSize, thickness); } - var animationSpeed = 100; - var opened = false; + let animationSpeed = 100; + let opened = false; // Register our animations. - var frames = []; - for (var i = 1; i <= 4; i++) { + let frames = []; + for (let i = 1; i <= 4; i++) { frames.push(direction + i); } @@ -30,7 +30,7 @@ function main() { frames.reverse(); Self.AddAnimation("close", animationSpeed, frames); - Events.OnCollide( function(e) { + Events.OnCollide((e) => { if (opened) { return; } @@ -78,9 +78,9 @@ function main() { } }); - Events.OnLeave(function() { + Events.OnLeave(() => { if (opened) { - Self.PlayAnimation("close", function() { + Self.PlayAnimation("close", () => { opened = false; }); } diff --git a/doodads/warp-door/Makefile b/doodads/warp-door/Makefile index a4842e1..90d80b2 100644 --- a/doodads/warp-door/Makefile +++ b/doodads/warp-door/Makefile @@ -17,10 +17,10 @@ build: doodad install-script warp-door.js warp-door-orange.doodad for i in *.doodad; do\ - doodad edit-doodad --tag "category=doors" $${i};\ + doodad edit-doodad --tag "category=doors" --hitbox=34,76 $${i};\ done for i in warp-door-*.doodad; do\ doodad edit-doodad --tag "category=doors,gizmos" $${i};\ done - cp *.doodad ../../../assets/doodads/ \ No newline at end of file + cp *.doodad ../../../assets/doodads/ diff --git a/doodads/warp-door/warp-door.js b/doodads/warp-door/warp-door.js index 0cb6e1a..2bde9f3 100644 --- a/doodads/warp-door/warp-door.js +++ b/doodads/warp-door/warp-door.js @@ -1,18 +1,17 @@ // Warp Doors -function main() { - Self.SetHitbox(0, 0, 34, 76); - // Are we a blue or orange door? Regular warp door will be 'none' - var color = Self.GetTag("color"); - var isStateDoor = color === 'blue' || color === 'orange'; - var state = color === 'blue'; // Blue door is ON by default. +const color = Self.GetTag("color"), + isStateDoor = color === 'blue' || color === 'orange'; - var animating = false; - var collide = false; +// State in case we're a blue warp door. +let state = color === 'blue', + animating = false, + collide = false; +function main() { // Declare animations and sprite names. - var animSpeed = 100; - var spriteDefault, spriteDisabled; // the latter for state doors. + let animSpeed = 100; + let spriteDefault, spriteDisabled; // the latter for state doors. if (color === 'blue') { Self.AddAnimation("open", animSpeed, ["blue-2", "blue-3", "blue-4"]); Self.AddAnimation("close", animSpeed, ["blue-4", "blue-3", "blue-2", "blue-1"]); @@ -23,6 +22,11 @@ function main() { Self.AddAnimation("close", animSpeed, ["orange-4", "orange-3", "orange-2", "orange-1"]); spriteDefault = "orange-1"; spriteDisabled = "orange-off"; + } else if (color === 'invisible') { + // Invisible Warp Door region. + Self.Hide(); + Self.AddAnimation("open", animSpeed, [0, 0]); + Self.AddAnimation("close", animSpeed, [0, 0]); } else { Self.AddAnimation("open", animSpeed, ["door-2", "door-3", "door-4"]); Self.AddAnimation("close", animSpeed, ["door-4", "door-3", "door-2", "door-1"]); @@ -30,17 +34,17 @@ function main() { } // Find our linked Warp Door. - var links = Self.GetLinks() - var linkedDoor = null; - for (var i = 0; i < links.length; i++) { - if (links[i].Title.indexOf("Warp Door") > -1) { - linkedDoor = links[i]; + let linkedDoor = null; + for (let link of Self.GetLinks()) { + if (link.Title.indexOf("Warp Door") > -1) { + linkedDoor = link; + break; } } // Subscribe to the global state-change if we are a state door. if (isStateDoor) { - Message.Subscribe("broadcast:state-change", function(newState) { + Message.Subscribe("broadcast:state-change", (newState) => { state = color === 'blue' ? !newState : newState; // Activate or deactivate the door. @@ -48,9 +52,12 @@ function main() { }); } + // For player groundedness work-around + let playerLastY = []; // last sampling of Y values + // The player Uses the door. - var flashedCooldown = false; // "Locked Door" flashed message. - Events.OnUse(function(e) { + let flashedCooldown = false; // "Locked Door" flashed message. + Events.OnUse((e) => { if (animating) { return; } @@ -74,14 +81,48 @@ function main() { return; } + // The player must be grounded or have no gravity to open the door. + if (!e.Actor.Grounded() && e.Actor.HasGravity()) { + // Work-around: if two Boxes are stacked atop each other the player can + // get stuck if he jumps on top. He may not be Grounded but isn't changing + // effective Y position and a warp door may work as a good way out. + let yValue = e.Actor.Position().Y; + + // Collect a sampling of last few Y values. If the player Y position + // is constant the last handful of frames, treat them as if they're + // grounded (or else they can't activate the warp door). + playerLastY.unshift(yValue); + if (playerLastY.length < 6) { + return; + } + + // We have enough history. + playerLastY.pop(); + + // Hasn't moved? + let isGrounded = true; + for (let i = 0; i < playerLastY.length; i++) { + if (yValue !== playerLastY[i]) { + isGrounded = false; + break; + } + } + + if (!isGrounded) { + return; + } + + // Player was effectively grounded! No change in Y position. + } + // Freeze the player. e.Actor.Freeze() // Play the open and close animation. animating = true; - Self.PlayAnimation("open", function() { + Self.PlayAnimation("open", () => { e.Actor.Hide() - Self.PlayAnimation("close", function() { + Self.PlayAnimation("close", () => { Self.ShowLayerNamed(isStateDoor && !state ? spriteDisabled : spriteDefault); animating = false; @@ -98,12 +139,12 @@ function main() { }); // Respond to incoming warp events. - Message.Subscribe("warp-door:incoming", function(player) { + Message.Subscribe("warp-door:incoming", (player) => { animating = true; player.Unfreeze(); - Self.PlayAnimation("open", function() { + Self.PlayAnimation("open", () => { player.Show(); - Self.PlayAnimation("close", function() { + Self.PlayAnimation("close", () => { animating = false; // If the receiving door was a State Door, fix its state.