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.
This commit is contained in:
Noah 2022-05-01 15:18:23 -07:00
parent ad67e2b42b
commit fc736abd5f
74 changed files with 665 additions and 36 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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{},

View File

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