Compare commits

...

5 Commits

Author SHA1 Message Date
Noah 142ed7d4c5 Bugfix: Crusher to fall indefinitely w/o a time limit 2022-05-08 12:09:09 -07:00
Noah 94c4ed24d2 Idle animations for Boy 2022-05-07 20:18:44 -07:00
Noah 7005786a31 Swimming Physics and Bubble Pattern
Water pixels finally do something other than turn your character blue!

* When the player character is "wet" (touching water pixels, and so appearing in
  a blue mask), water physics apply: gravity is slower, your jump height is
  halved, but you get infinite jumps to swim higher in the water.
* Holding the jump key under water will incur a short delay between jumps, so
  that you don't just fly straight up to the surface. Tap the jump button to
  move up quicker, you can spam it all you want.

Azulians are also able to handle being under water:

* They'll sink to the bottom and keep walking back and forth normally.
* If you are above them and noticed, they'll jump (swim) up towards you,
  aware of the water and it jumps like you do.
* The Blue Azulian has the poorest vertical aggro range so it isn't a
  very good swimmer. The White Azulian is very good at navigating water
  as it can pursue the player from the furthest distance of them all.

Changes to the editor:

* New brush pattern added: bubbles.png
  * It's the default pattern now for the "water" color of all
    of the built-in palettes instead of ink.png
  * A repeating pattern of bubbles carved out showing the
    level wallpaper.
  * The old "Bubbles (circles.png)" is renamed "Circles"
* The last scroll position is saved with the Level file, so when you reload
  the level later it's scrolled at where you left it.
2022-05-05 21:35:32 -07:00
Noah 4f5ea15e46 Spit and polish
* New built-in wallpaper: "Dotted paper (dark)" is a dark-themed wallpaper.
* New built-in palette: "Neon Bright" with bright colors for dark levels.
* New cheat: "warp whistle" to automatically win the level.
* In case the user has a VERY LARGE screen resolution bigger than the full
  bounds of a Bounded level, the Play Scene will cap the size and center
  the level canvas onto the window. This is preferable to being able to see
  beyond the level's boundaries and hitting an invisible wall in-game.
* Make the titlescreen Lazy Scroll work on unbounded levels. It can't bounce
  off scroll boundaries but it will reverse course if it reaches the level's
  furthest limits.
* Bugfix: characters' white eyes were transparent in-game. Multiple culprits
  from the `doodad convert` tool defaulting the chroma key to white, to the
  SDL2 textures considering white to be transparent. For the latter, the game
  offsets the color by -1 blue.
2022-05-03 21:15:39 -07:00
Noah 50980caebb Doodads: Gems, Snake and Crusher
Adds several new doodads to the game and 5 new wallpapers (parchment
paper in blue, green, red, white and yellow).

New doodads:

* Crusher: A purple block-headed mob wearing an iron helmet. It tries
  to crush the player when you get underneath. Its flat helmet can be
  ridden on like an elevator back up.
* Snake: A green stationary mob that always faces toward the player.
  If the player is nearby and jumps, the Snake will jump too and hope
  to catch the player in mid-air.
* Gems and Totems: A new key & lock collectible. Gems have quantity so
  you can collect multiple, and place them into matching Totems. A
  Totem gives off a power signal when its gem is placed and all other
  Totems it is linked to have also been activated. A single Totem may
  link to an Electric Door and require only one gem to open it, or it
  can link to other Totems and they all require gems before the power
  signal is sent out.
2022-05-01 15:18:23 -07:00
71 changed files with 627 additions and 12 deletions

View File

@ -2,9 +2,11 @@
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";
@ -14,7 +16,9 @@ if (color === 'white') {
aggroX = 1000;
aggroY = 400;
playerSpeed = 8;
swimSpeed = playerSpeed * 0.4;
jumpSpeed = 20;
swimJumpSpeed = jumpSpeed * 0.4;
}
function setupAnimations(color) {
@ -30,6 +34,9 @@ function setupAnimations(color) {
function main() {
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);
@ -95,8 +102,25 @@ function main() {
sampleTick++;
}
let Vx = parseFloat(playerSpeed * (direction === "left" ? -1 : 1)),
Vy = jump && Self.Grounded() ? parseFloat(-jumpSpeed) : Self.GetVelocity().Y;
// 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

View File

@ -19,7 +19,14 @@ function main() {
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.

View File

@ -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

View File

@ -1,8 +1,9 @@
const playerSpeed = 12;
let Vx = Vy = 0,
animating = false,
animStart = animEnd = 0;
walking = false,
direction = "right",
lastDirection = direction;
function main() {
Self.SetMobile(true);
@ -11,6 +12,8 @@ function main() {
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);
@ -25,20 +28,35 @@ function main() {
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;
}
// And play what animation?
if (!Self.IsAnimating()) {
if (walking) {
Self.PlayAnimation("walk-"+direction, null);
} else {
Self.PlayAnimation("idle-"+direction, null);
}
}
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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

View File

@ -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/

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -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");
}
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -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/

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

View File

@ -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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1005 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

View File

@ -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/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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);
}
});
}