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.
BIN
assets/wallpapers/blue-parchment.png
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
assets/wallpapers/green-parchment.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
assets/wallpapers/red-parchment.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
assets/wallpapers/white-parchment.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
assets/wallpapers/yellow-parchment.png
Normal file
After Width: | Height: | Size: 84 KiB |
|
@ -19,7 +19,15 @@ function main() {
|
||||||
let overlap = e.Overlap;
|
let overlap = e.Overlap;
|
||||||
|
|
||||||
if (overlap.Y === 0 && !(overlap.X === 0 && overlap.W < 5) && !(overlap.X === size)) {
|
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...
|
||||||
|
console.log("new box code");
|
||||||
|
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;
|
return false;
|
||||||
} else if (overlap.Y === size) {
|
} else if (overlap.Y === size) {
|
||||||
// From the bottom, boop it up.
|
// From the bottom, boop it up.
|
||||||
|
|
|
@ -34,6 +34,10 @@ doors() {
|
||||||
cd doors/
|
cd doors/
|
||||||
make
|
make
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
|
cd gems/
|
||||||
|
make
|
||||||
|
cd ..
|
||||||
}
|
}
|
||||||
|
|
||||||
trapdoors() {
|
trapdoors() {
|
||||||
|
@ -84,6 +88,16 @@ warpdoor() {
|
||||||
cd ..
|
cd ..
|
||||||
}
|
}
|
||||||
|
|
||||||
|
creatures() {
|
||||||
|
cd snake/
|
||||||
|
make
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
cd crusher/
|
||||||
|
make
|
||||||
|
cd ..
|
||||||
|
}
|
||||||
|
|
||||||
boy
|
boy
|
||||||
buttons
|
buttons
|
||||||
switches
|
switches
|
||||||
|
@ -94,6 +108,7 @@ mobs
|
||||||
objects
|
objects
|
||||||
onoff
|
onoff
|
||||||
warpdoor
|
warpdoor
|
||||||
|
creatures
|
||||||
doodad edit-doodad -quiet -lock -author "Noah" ../../assets/doodads/*.doodad
|
doodad edit-doodad -quiet -lock -author "Noah" ../../assets/doodads/*.doodad
|
||||||
doodad edit-doodad ../../assets/doodads/azu-blu.doodad
|
doodad edit-doodad ../../assets/doodads/azu-blu.doodad
|
||||||
doodad edit-doodad -hide ../../assets/doodads/boy.doodad
|
doodad edit-doodad -hide ../../assets/doodads/boy.doodad
|
||||||
|
|
14
dev-assets/doodads/crusher/Makefile
Normal 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/
|
BIN
dev-assets/doodads/crusher/angry.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
200
dev-assets/doodads/crusher/crusher.js
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
// 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,
|
||||||
|
falling: 2,
|
||||||
|
hit: 3,
|
||||||
|
rising: 4,
|
||||||
|
};
|
||||||
|
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.falling;
|
||||||
|
} 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.falling;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
BIN
dev-assets/doodads/crusher/ouch.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
dev-assets/doodads/crusher/peek-left.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
dev-assets/doodads/crusher/peek-right.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
dev-assets/doodads/crusher/sleep.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
58
dev-assets/doodads/gems/Makefile
Normal 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/
|
BIN
dev-assets/doodads/gems/blue-1.png
Normal file
After Width: | Height: | Size: 891 B |
BIN
dev-assets/doodads/gems/blue-2.png
Normal file
After Width: | Height: | Size: 927 B |
BIN
dev-assets/doodads/gems/blue-3.png
Normal file
After Width: | Height: | Size: 903 B |
BIN
dev-assets/doodads/gems/blue-4.png
Normal file
After Width: | Height: | Size: 835 B |
24
dev-assets/doodads/gems/gemstone.js
Normal 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);
|
||||||
|
}
|
BIN
dev-assets/doodads/gems/green-1.png
Normal file
After Width: | Height: | Size: 785 B |
BIN
dev-assets/doodads/gems/green-2.png
Normal file
After Width: | Height: | Size: 807 B |
BIN
dev-assets/doodads/gems/green-3.png
Normal file
After Width: | Height: | Size: 864 B |
BIN
dev-assets/doodads/gems/green-4.png
Normal file
After Width: | Height: | Size: 835 B |
BIN
dev-assets/doodads/gems/red-1.png
Normal file
After Width: | Height: | Size: 711 B |
BIN
dev-assets/doodads/gems/red-2.png
Normal file
After Width: | Height: | Size: 702 B |
BIN
dev-assets/doodads/gems/red-3.png
Normal file
After Width: | Height: | Size: 712 B |
BIN
dev-assets/doodads/gems/red-4.png
Normal file
After Width: | Height: | Size: 724 B |
BIN
dev-assets/doodads/gems/totem-blue-0.png
Normal file
After Width: | Height: | Size: 798 B |
BIN
dev-assets/doodads/gems/totem-blue-1.png
Normal file
After Width: | Height: | Size: 958 B |
BIN
dev-assets/doodads/gems/totem-blue-2.png
Normal file
After Width: | Height: | Size: 1005 B |
BIN
dev-assets/doodads/gems/totem-blue-3.png
Normal file
After Width: | Height: | Size: 987 B |
BIN
dev-assets/doodads/gems/totem-blue-4.png
Normal file
After Width: | Height: | Size: 921 B |
BIN
dev-assets/doodads/gems/totem-green-0.png
Normal file
After Width: | Height: | Size: 711 B |
BIN
dev-assets/doodads/gems/totem-green-1.png
Normal file
After Width: | Height: | Size: 887 B |
BIN
dev-assets/doodads/gems/totem-green-2.png
Normal file
After Width: | Height: | Size: 938 B |
BIN
dev-assets/doodads/gems/totem-green-3.png
Normal file
After Width: | Height: | Size: 978 B |
BIN
dev-assets/doodads/gems/totem-green-4.png
Normal file
After Width: | Height: | Size: 956 B |
BIN
dev-assets/doodads/gems/totem-red-0.png
Normal file
After Width: | Height: | Size: 752 B |
BIN
dev-assets/doodads/gems/totem-red-1.png
Normal file
After Width: | Height: | Size: 811 B |
BIN
dev-assets/doodads/gems/totem-red-2.png
Normal file
After Width: | Height: | Size: 805 B |
BIN
dev-assets/doodads/gems/totem-red-3.png
Normal file
After Width: | Height: | Size: 790 B |
BIN
dev-assets/doodads/gems/totem-red-4.png
Normal file
After Width: | Height: | Size: 817 B |
BIN
dev-assets/doodads/gems/totem-yellow-0.png
Normal file
After Width: | Height: | Size: 837 B |
BIN
dev-assets/doodads/gems/totem-yellow-1.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dev-assets/doodads/gems/totem-yellow-2.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dev-assets/doodads/gems/totem-yellow-3.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
dev-assets/doodads/gems/totem-yellow-4.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
108
dev-assets/doodads/gems/totem.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Totem %s is linked to %d neighbors", Self.ID(), Object.keys(totems).length);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Totem %s (%s) tries power", Self.ID(), Self.Filename);
|
||||||
|
|
||||||
|
// Can't if any of our linked totems aren't activated.
|
||||||
|
try {
|
||||||
|
for (let totemId of Object.keys(totems)) {
|
||||||
|
console.log("Totem %s (%s) sees linked totem %s", Self.ID(), Self.Filename, totemId);
|
||||||
|
if (totems[totemId] === false) {
|
||||||
|
console.log("Can't, a linked totem not active!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Caught: %s", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't if we aren't powered.
|
||||||
|
if (activated === false) {
|
||||||
|
console.log("Can't, we are not active!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit power!
|
||||||
|
console.log("POWER!");
|
||||||
|
Message.Publish("power", true);
|
||||||
|
}
|
BIN
dev-assets/doodads/gems/yellow-1.png
Normal file
After Width: | Height: | Size: 971 B |
BIN
dev-assets/doodads/gems/yellow-2.png
Normal file
After Width: | Height: | Size: 921 B |
BIN
dev-assets/doodads/gems/yellow-3.png
Normal file
After Width: | Height: | Size: 898 B |
BIN
dev-assets/doodads/gems/yellow-4.png
Normal file
After Width: | Height: | Size: 879 B |
15
dev-assets/doodads/snake/Makefile
Normal 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/
|
BIN
dev-assets/doodads/snake/attack-left-1.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
dev-assets/doodads/snake/attack-left-2.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dev-assets/doodads/snake/attack-left-3.png
Normal file
After Width: | Height: | Size: 935 B |
BIN
dev-assets/doodads/snake/attack-right-1.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
dev-assets/doodads/snake/attack-right-2.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dev-assets/doodads/snake/attack-right-3.png
Normal file
After Width: | Height: | Size: 916 B |
BIN
dev-assets/doodads/snake/left-1.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dev-assets/doodads/snake/left-2.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
dev-assets/doodads/snake/left-3.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dev-assets/doodads/snake/right-1.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
dev-assets/doodads/snake/right-2.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dev-assets/doodads/snake/right-3.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
135
dev-assets/doodads/snake/snake.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
// 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) {
|
||||||
|
console.log("Player is nearby snake! %d", delta);
|
||||||
|
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) {
|
||||||
|
console.warn("Player is jumping near us!")
|
||||||
|
|
||||||
|
// 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()) {
|
||||||
|
console.log("Landed again after jump!");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ var (
|
||||||
CheatNoclip = "ghost mode"
|
CheatNoclip = "ghost mode"
|
||||||
CheatShowAllActors = "show all actors"
|
CheatShowAllActors = "show all actors"
|
||||||
CheatGiveKeys = "give all keys"
|
CheatGiveKeys = "give all keys"
|
||||||
|
CheatGiveGems = "give all gems"
|
||||||
CheatDropItems = "drop all items"
|
CheatDropItems = "drop all items"
|
||||||
CheatPlayAsBird = "fly like a bird"
|
CheatPlayAsBird = "fly like a bird"
|
||||||
CheatGodMode = "god mode"
|
CheatGodMode = "god mode"
|
||||||
|
@ -39,7 +40,7 @@ var (
|
||||||
// Actor replacement cheats
|
// Actor replacement cheats
|
||||||
var CheatActors = map[string]string{
|
var CheatActors = map[string]string{
|
||||||
"pinocchio": "boy",
|
"pinocchio": "boy",
|
||||||
"the cell": "azu-blue",
|
"the cell": "azu-blu",
|
||||||
"super azulian": "azu-red",
|
"super azulian": "azu-red",
|
||||||
"hyper azulian": "azu-white",
|
"hyper azulian": "azu-white",
|
||||||
"fly like a bird": "bird-red",
|
"fly like a bird": "bird-red",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package balance
|
package balance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
magicform "git.kirsle.net/apps/doodle/pkg/uix/magic-form"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
"git.kirsle.net/go/ui/style"
|
"git.kirsle.net/go/ui/style"
|
||||||
|
@ -214,6 +215,61 @@ var (
|
||||||
ButtonLightRed = style.DefaultButton
|
ButtonLightRed = style.DefaultButton
|
||||||
|
|
||||||
DefaultCrosshairColor = render.RGBA(0, 153, 255, 255)
|
DefaultCrosshairColor = render.RGBA(0, 153, 255, 255)
|
||||||
|
|
||||||
|
// Default built-in wallpapers.
|
||||||
|
Wallpapers = []magicform.Option{
|
||||||
|
{
|
||||||
|
Label: "Notebook",
|
||||||
|
Value: "notebook.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Legal Pad",
|
||||||
|
Value: "legal.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Graph paper",
|
||||||
|
Value: "graph.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Dotted paper",
|
||||||
|
Value: "dots.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Blueprint",
|
||||||
|
Value: "blueprint.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Red parchment",
|
||||||
|
Value: "red-parchment.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Green parchment",
|
||||||
|
Value: "green-parchment.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Blue parchment",
|
||||||
|
Value: "blue-parchment.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Yellow parchment",
|
||||||
|
Value: "yellow-parchment.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "White parchment",
|
||||||
|
Value: "white-parchment.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Pure white",
|
||||||
|
Value: "white.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Separator: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Custom wallpaper...",
|
||||||
|
Value: CustomWallpaperFilename,
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Customize the various button styles.
|
// Customize the various button styles.
|
||||||
|
|
|
@ -121,6 +121,18 @@ func (c Command) cheatCommand(d *Doodle) bool {
|
||||||
d.FlashError("Use this cheat in Play Mode to get all colored keys.")
|
d.FlashError("Use this cheat in Play Mode to get all colored keys.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case balance.CheatGiveGems:
|
||||||
|
if isPlay {
|
||||||
|
playScene.SetCheated()
|
||||||
|
playScene.Player.AddItem("gem-red.doodad", 1)
|
||||||
|
playScene.Player.AddItem("gem-green.doodad", 1)
|
||||||
|
playScene.Player.AddItem("gem-blue.doodad", 1)
|
||||||
|
playScene.Player.AddItem("gem-yellow.doodad", 1)
|
||||||
|
d.Flash("Given all gemstones to the player character.")
|
||||||
|
} else {
|
||||||
|
d.FlashError("Use this cheat in Play Mode to get all gemstones.")
|
||||||
|
}
|
||||||
|
|
||||||
case balance.CheatDropItems:
|
case balance.CheatDropItems:
|
||||||
if isPlay {
|
if isPlay {
|
||||||
playScene.SetCheated()
|
playScene.SetCheated()
|
||||||
|
|
|
@ -76,6 +76,12 @@ func (c *Chunker) MigrateZipfile(zf *zip.Writer) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify that this chunk file in the old ZIP was not empty.
|
||||||
|
if chunk, err := ChunkFromZipfile(c.Zipfile, c.Layer, point); err == nil && chunk.Len() == 0 {
|
||||||
|
log.Debug("Skip chunk %s (old zipfile chunk was empty)", coord)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
log.Info("Copy existing chunk %s", file.Name)
|
log.Info("Copy existing chunk %s", file.Name)
|
||||||
if err := zf.Copy(file); err != nil {
|
if err := zf.Copy(file); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -94,6 +100,10 @@ func (c *Chunker) MigrateZipfile(zf *zip.Writer) error {
|
||||||
|
|
||||||
// Flush in-memory chunks out to zipfile.
|
// Flush in-memory chunks out to zipfile.
|
||||||
for coord, chunk := range c.Chunks {
|
for coord, chunk := range c.Chunks {
|
||||||
|
if _, ok := erasedChunks[coord]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
filename := fmt.Sprintf("chunks/%d/%s.json", c.Layer, coord.String())
|
filename := fmt.Sprintf("chunks/%d/%s.json", c.Layer, coord.String())
|
||||||
log.Info("Flush in-memory chunks to %s", filename)
|
log.Info("Flush in-memory chunks to %s", filename)
|
||||||
chunk.ToZipfile(zf, filename)
|
chunk.ToZipfile(zf, filename)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package scripting
|
package scripting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/lib/debugging"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
)
|
)
|
||||||
|
@ -28,6 +29,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
// TODO EXCEPTIONS
|
// TODO EXCEPTIONS
|
||||||
log.Error("RegisterPublishHooks(%s): %s", vm.Name, err)
|
log.Error("RegisterPublishHooks(%s): %s", vm.Name, err)
|
||||||
|
debugging.PrintCallers()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -72,6 +74,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
|
||||||
},
|
},
|
||||||
|
|
||||||
"Publish": func(name string, v ...goja.Value) {
|
"Publish": func(name string, v ...goja.Value) {
|
||||||
|
vm.muPublish.Lock()
|
||||||
for _, channel := range vm.Outbound {
|
for _, channel := range vm.Outbound {
|
||||||
channel <- Message{
|
channel <- Message{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
@ -79,6 +82,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
|
||||||
Args: v,
|
Args: v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
vm.muPublish.Unlock()
|
||||||
},
|
},
|
||||||
|
|
||||||
"Broadcast": func(name string, v ...goja.Value) {
|
"Broadcast": func(name string, v ...goja.Value) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ type VM struct {
|
||||||
stop chan bool
|
stop chan bool
|
||||||
subscribe map[string][]goja.Value // Subscribed message handlers by name.
|
subscribe map[string][]goja.Value // Subscribed message handlers by name.
|
||||||
muSubscribe sync.RWMutex
|
muSubscribe sync.RWMutex
|
||||||
|
muPublish sync.Mutex // serialize PubSub publishes
|
||||||
|
|
||||||
vm *goja.Runtime
|
vm *goja.Runtime
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ func NewVM(name string) *VM {
|
||||||
timers: map[int]*Timer{},
|
timers: map[int]*Timer{},
|
||||||
|
|
||||||
// Pub/sub structs.
|
// Pub/sub structs.
|
||||||
Inbound: make(chan Message),
|
Inbound: make(chan Message, 100),
|
||||||
Outbound: []chan Message{},
|
Outbound: []chan Message{},
|
||||||
stop: make(chan bool, 1),
|
stop: make(chan bool, 1),
|
||||||
subscribe: map[string][]goja.Value{},
|
subscribe: map[string][]goja.Value{},
|
||||||
|
|
|
@ -169,39 +169,7 @@ func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) {
|
||||||
Label: "Wallpaper:",
|
Label: "Wallpaper:",
|
||||||
Font: balance.UIFont,
|
Font: balance.UIFont,
|
||||||
SelectValue: selectedWallpaper,
|
SelectValue: selectedWallpaper,
|
||||||
Options: []magicform.Option{
|
Options: balance.Wallpapers,
|
||||||
{
|
|
||||||
Label: "Notebook",
|
|
||||||
Value: "notebook.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Legal Pad",
|
|
||||||
Value: "legal.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Graph paper",
|
|
||||||
Value: "graph.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Dotted paper",
|
|
||||||
Value: "dots.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Blueprint",
|
|
||||||
Value: "blueprint.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Pure white",
|
|
||||||
Value: "white.png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Separator: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Label: "Custom wallpaper...",
|
|
||||||
Value: balance.CustomWallpaperFilename,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
OnSelect: func(v interface{}) {
|
OnSelect: func(v interface{}) {
|
||||||
if filename, ok := v.(string); ok {
|
if filename, ok := v.(string); ok {
|
||||||
// Picking the Custom option?
|
// Picking the Custom option?
|
||||||
|
|