Polish and bugfixes

- Fix a memory sharing bug in the Giant Screenshot feature.
- Main Menu to eagerload chunks in the background to make scrolling less
  jittery. No time for a loadscreen!
- Extra script debugging: names/IDs of doodads are shown when they send
  messages to one another.
- Level Properties: you can edit the Bounded max width/height values for
  the level.

Doodad changes:

- Buttons: fix a timing bug and keep better track of who is stepping on it,
  only popping up when all colliders have left. The effect: they pop up
  immediately (not after 200ms) and are more reliable.
- Keys: zero-qty keys will no longer put themselves into the inventory of
  characters who already have one except for the player character. So
  the Thief will not steal them if she already has the key.

Added to the JavaScript API:

* time.Hour, time.Minute, time.Second, time.Millisecond, time.Microsecond
This commit is contained in:
Noah 2021-10-09 20:45:38 -07:00
parent feea703d0c
commit 1a8a5eb94b
13 changed files with 231 additions and 30 deletions

View File

@ -4,16 +4,23 @@ function main() {
// Has a linked Sticky Button been pressed permanently down? // Has a linked Sticky Button been pressed permanently down?
var stickyDown = false; var stickyDown = false;
Message.Subscribe("sticky:down", function(down) { Message.Subscribe("sticky:down", function (down) {
stickyDown = down; stickyDown = down;
Self.ShowLayer(stickyDown ? 1 : 0); Self.ShowLayer(stickyDown ? 1 : 0);
}); });
Events.OnCollide(function(e) { // Track who all is colliding with us.
var colliders = {};
Events.OnCollide(function (e) {
if (!e.Settled) { if (!e.Settled) {
return; return;
} }
if (colliders[e.Actor.ID()] == undefined) {
colliders[e.Actor.ID()] = true;
}
// If a linked Sticky Button is pressed, button stays down too and // If a linked Sticky Button is pressed, button stays down too and
// doesn't interact. // doesn't interact.
if (stickyDown) { if (stickyDown) {
@ -37,12 +44,17 @@ function main() {
} }
Self.ShowLayer(1); Self.ShowLayer(1);
timer = setTimeout(function() { });
Events.OnLeave(function (e) {
delete colliders[e.Actor.ID()];
if (Object.keys(colliders).length === 0) {
Sound.Play("button-up.wav") Sound.Play("button-up.wav")
Self.ShowLayer(0); Self.ShowLayer(0);
Message.Publish("power", false); Message.Publish("power", false);
timer = 0; timer = 0;
pressed = false; pressed = false;
}, 200); }
}); });
} }

View File

@ -6,7 +6,6 @@ var powerState = false;
function setPoweredState(powered) { function setPoweredState(powered) {
powerState = powered; powerState = powered;
console.log("setPoweredState: %+v", powered)
if (powered) { if (powered) {
if (animating || opened) { if (animating || opened) {
return; return;
@ -14,14 +13,14 @@ function setPoweredState(powered) {
animating = true; animating = true;
Sound.Play("electric-door.wav") Sound.Play("electric-door.wav")
Self.PlayAnimation("open", function() { Self.PlayAnimation("open", function () {
opened = true; opened = true;
animating = false; animating = false;
}); });
} else { } else {
animating = true; animating = true;
Sound.Play("electric-door.wav") Sound.Play("electric-door.wav")
Self.PlayAnimation("close", function() { Self.PlayAnimation("close", function () {
opened = false; opened = false;
animating = false; animating = false;
}) })
@ -41,13 +40,12 @@ function main() {
// power sources like Buttons will work as normal, as they emit only a power // power sources like Buttons will work as normal, as they emit only a power
// signal. // signal.
var ignoreNextPower = false; var ignoreNextPower = false;
Message.Subscribe("switch:toggle", function(powered) { Message.Subscribe("switch:toggle", function (powered) {
console.log("A switch powered %+v, setPoweredState(%+v) to opposite", powered, powerState);
ignoreNextPower = true; ignoreNextPower = true;
setPoweredState(!powerState); setPoweredState(!powerState);
}) })
Message.Subscribe("power", function(powered) { Message.Subscribe("power", function (powered) {
if (ignoreNextPower) { if (ignoreNextPower) {
ignoreNextPower = false; ignoreNextPower = false;
return; return;
@ -56,7 +54,7 @@ function main() {
setPoweredState(powered); setPoweredState(powered);
}); });
Events.OnCollide(function(e) { Events.OnCollide(function (e) {
if (e.InHitbox) { if (e.InHitbox) {
if (!opened) { if (!opened) {
return false; return false;

View File

@ -5,6 +5,12 @@ function main() {
Events.OnCollide(function (e) { Events.OnCollide(function (e) {
if (e.Settled) { if (e.Settled) {
if (e.Actor.HasInventory()) { 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") Sound.Play("item-get.wav")
e.Actor.AddItem(Self.Filename, quantity); e.Actor.AddItem(Self.Filename, quantity);
Self.Destroy(); Self.Destroy();

View File

@ -0,0 +1,58 @@
package collision_test
import (
"testing"
"git.kirsle.net/apps/doodle/pkg/collision"
"git.kirsle.net/go/render"
)
func TestBetweenBoxes(t *testing.T) {
mkrect := func(x, y, w, h int) render.Rect {
return render.Rect{
X: x,
Y: y,
W: w,
H: h,
}
}
table := []struct {
A render.Rect
B render.Rect
Expect bool
}{
{
A: mkrect(0, 0, 32, 32),
B: mkrect(32, 0, 32, 32),
Expect: true,
},
{
A: mkrect(0, 0, 32, 32),
B: mkrect(100, 100, 40, 40),
Expect: false,
},
{
A: mkrect(100, 100, 50, 50),
B: mkrect(80, 110, 100, 30),
Expect: true,
},
}
var actual bool
for i, test := range table {
actual = false
for range collision.BetweenBoxes([]render.Rect{test.A, test.B}) {
actual = true
break
}
if test.Expect != actual {
t.Errorf(
"Test %d BetweenBoxes: %s cmp %s\n"+
"Expected: %+v\n"+
"Actually: %+v",
i, test.A, test.B, test.Expect, actual,
)
}
}
}

View File

@ -27,3 +27,21 @@ func (d *Drawing) Size() render.Rect {
func (d *Drawing) MoveTo(to render.Point) { func (d *Drawing) MoveTo(to render.Point) {
d.Drawing.MoveTo(to) d.Drawing.MoveTo(to)
} }
// Grounded satisfies the collision.Actor interface.
func (d *Drawing) Grounded() bool {
return false
}
// SetGrounded satisfies the collision.Actor interface.
func (d *Drawing) SetGrounded(v bool) {}
// Position satisfies the collision.Actor interface.
func (d *Drawing) Position() render.Point {
return render.Point{}
}
// Hitbox satisfies the collision.Actor interface.
func (d *Drawing) Hitbox() render.Rect {
return render.Rect{}
}

View File

@ -103,11 +103,14 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) {
// Offset the doodad position if the image is displaying // Offset the doodad position if the image is displaying
// negative coordinates. // negative coordinates.
drawAt := render.NewPoint(actor.Point.X, actor.Point.Y)
if worldSize.X < 0 { if worldSize.X < 0 {
actor.Point.X += render.AbsInt(worldSize.X) * chunkSize var offset = render.AbsInt(worldSize.X) * chunkSize
drawAt.X += offset
} }
if worldSize.Y < 0 { if worldSize.Y < 0 {
actor.Point.Y += render.AbsInt(worldSize.Y) * chunkSize var offset = render.AbsInt(worldSize.Y) * chunkSize
drawAt.Y += offset
} }
// TODO: usually doodad sprites start at 0,0 and the chunkSize // TODO: usually doodad sprites start at 0,0 and the chunkSize
@ -124,7 +127,7 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) {
if !ok { if !ok {
log.Error("GiantScreenshot: couldn't turn chunk to RGBA") log.Error("GiantScreenshot: couldn't turn chunk to RGBA")
} }
img = blotImage(img, rgba, image.Pt(actor.Point.X, actor.Point.Y)) img = blotImage(img, rgba, image.Pt(drawAt.X, drawAt.Y))
} }
} }

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/license" "git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/modal/loadscreen"
"git.kirsle.net/apps/doodle/pkg/native" "git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/scripting" "git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
@ -221,6 +222,20 @@ func (s *MainScene) Setup(d *Doodle) error {
// Check for update in the background. // Check for update in the background.
go s.checkUpdate() go s.checkUpdate()
// Eager load the level in background, no time for load screen.
go func() {
if err := s.setupAsync(d); err != nil {
log.Error("MainScene.setupAsync: %s", err)
}
}()
return nil
}
// setupAsync runs background tasks from setup, e.g. eager load
// chunks of the level for cache.
func (s *MainScene) setupAsync(d *Doodle) error {
loadscreen.PreloadAllChunkBitmaps(s.canvas.Chunker())
return nil return nil
} }

View File

@ -292,7 +292,7 @@ func (s *PlayScene) setupPlayer() {
} }
// Set up the player character's script in the VM. // Set up the player character's script in the VM.
if err := s.scripting.AddLevelScript(s.Player.ID()); err != nil { if err := s.scripting.AddLevelScript(s.Player.ID(), s.Player.Actor.Filename); err != nil {
log.Error("PlayScene.Setup: scripting.InstallActor(player) failed: %s", err) log.Error("PlayScene.Setup: scripting.InstallActor(player) failed: %s", err)
} }
} }

View File

@ -46,6 +46,11 @@ func NewJSProxy(vm *VM) JSProxy {
"Add": func(t time.Time, ms int64) time.Time { "Add": func(t time.Time, ms int64) time.Time {
return t.Add(time.Duration(ms) * time.Millisecond) return t.Add(time.Duration(ms) * time.Millisecond)
}, },
"Hour": time.Hour,
"Minute": time.Minute,
"Second": time.Second,
"Millisecond": time.Millisecond,
"Microsecond": time.Microsecond,
}, },
// Bindings into the VM. // Bindings into the VM.

View File

@ -9,6 +9,7 @@ import (
// to the linked VMs. // to the linked VMs.
type Message struct { type Message struct {
Name string Name string
SenderID string
Args []interface{} Args []interface{}
} }
@ -23,14 +24,16 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
// for any matching messages received. // for any matching messages received.
go func() { go func() {
for msg := range vm.Inbound { for msg := range vm.Inbound {
vm.muSubscribe.RLock() vm.muSubscribe.Lock()
defer vm.muSubscribe.RUnlock()
if _, ok := vm.subscribe[msg.Name]; ok { if _, ok := vm.subscribe[msg.Name]; ok {
for _, callback := range vm.subscribe[msg.Name] { for _, callback := range vm.subscribe[msg.Name] {
log.Debug("PubSub: %s receives from %s: %s", vm.Name, msg.SenderID, msg.Name)
callback.Call(otto.Value{}, msg.Args...) callback.Call(otto.Value{}, msg.Args...)
} }
} }
vm.muSubscribe.Unlock()
} }
}() }()
@ -55,6 +58,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
for _, channel := range vm.Outbound { for _, channel := range vm.Outbound {
channel <- Message{ channel <- Message{
Name: name, Name: name,
SenderID: vm.Name,
Args: v, Args: v,
} }
} }
@ -70,6 +74,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
toVM.Inbound <- Message{ toVM.Inbound <- Message{
Name: name, Name: name,
SenderID: vm.Name,
Args: v, Args: v,
} }
} }

View File

@ -41,7 +41,7 @@ func (s *Supervisor) Loop() error {
// InstallScripts loads scripts for all actors in the level. // InstallScripts loads scripts for all actors in the level.
func (s *Supervisor) InstallScripts(level *level.Level) error { func (s *Supervisor) InstallScripts(level *level.Level) error {
for _, actor := range level.Actors { for _, actor := range level.Actors {
if err := s.AddLevelScript(actor.ID()); err != nil { if err := s.AddLevelScript(actor.ID(), actor.Filename); err != nil {
return err return err
} }
} }
@ -71,12 +71,14 @@ func (s *Supervisor) InstallScripts(level *level.Level) error {
} }
// AddLevelScript adds a script to the supervisor with level hooks. // AddLevelScript adds a script to the supervisor with level hooks.
func (s *Supervisor) AddLevelScript(id string) error { // The `id` will key the VM and should be the Actor ID in the level.
// The `name` is used to name the VM for debug logging.
func (s *Supervisor) AddLevelScript(id string, name string) error {
if _, ok := s.scripts[id]; ok { if _, ok := s.scripts[id]; ok {
return fmt.Errorf("duplicate actor ID %s in level", id) return fmt.Errorf("duplicate actor ID %s in level", id)
} }
s.scripts[id] = NewVM(id) s.scripts[id] = NewVM(fmt.Sprintf("%s#%s", name, id))
RegisterPublishHooks(s, s.scripts[id]) RegisterPublishHooks(s, s.scripts[id])
RegisterEventHooks(s, s.scripts[id]) RegisterEventHooks(s, s.scripts[id])
if err := s.scripts[id].RegisterLevelHooks(); err != nil { if err := s.scripts[id].RegisterLevelHooks(); err != nil {

View File

@ -116,15 +116,19 @@ func (w *Canvas) loopConstrainScroll() error {
return errors.New("NoLimitScroll enabled") return errors.New("NoLimitScroll enabled")
} }
var capped bool var (
capped bool
maxWidth = w.level.MaxWidth
maxHeight = w.level.MaxHeight
)
// Constrain the bottom and right for limited world sizes. // Constrain the bottom and right for limited world sizes.
if w.wallpaper.pageType >= level.Bounded && if w.wallpaper.pageType >= level.Bounded &&
w.wallpaper.maxWidth+w.wallpaper.maxHeight > 0 { maxWidth+maxHeight > 0 {
var ( var (
// TODO: downcast from int64! // TODO: downcast from int64!
mw = int(w.wallpaper.maxWidth) mw = int(maxWidth)
mh = int(w.wallpaper.maxHeight) mh = int(maxHeight)
Viewport = w.Viewport() Viewport = w.Viewport()
vw = w.ZoomDivide(Viewport.W) vw = w.ZoomDivide(Viewport.W)
vh = w.ZoomDivide(Viewport.H) vh = w.ZoomDivide(Viewport.H)

View File

@ -1,6 +1,8 @@
package windows package windows
import ( import (
"strconv"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/modal" "git.kirsle.net/apps/doodle/pkg/modal"
@ -139,6 +141,79 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
typeBtn.Supervise(config.Supervisor) typeBtn.Supervise(config.Supervisor)
config.Supervisor.Add(typeBtn) config.Supervisor.Add(typeBtn)
/******************
* Frame for selecting Bounded Level Limits.
******************/
if config.EditLevel != nil {
boundsFrame := ui.NewFrame("Bounds Frame")
frame.Pack(boundsFrame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 2,
})
label := ui.NewLabel(ui.Label{
Text: "Bounded limits:",
Font: balance.LabelFont,
})
boundsFrame.Pack(label, ui.Pack{
Side: ui.W,
PadY: 2,
})
var forms = []struct {
label string
number *int64
}{
{
label: "Width:",
number: &config.EditLevel.MaxWidth,
},
{
label: "Height:",
number: &config.EditLevel.MaxHeight,
},
}
for _, form := range forms {
form := form
label := ui.NewLabel(ui.Label{
Text: form.label,
Font: ui.MenuFont,
})
var intvar = int(*form.number)
button := ui.NewButton(form.label, ui.NewLabel(ui.Label{
IntVariable: &intvar,
Font: ui.MenuFont,
}))
button.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt("Enter new "+form.label+" ", func(answer string) {
if answer == "" {
return
}
if i, err := strconv.Atoi(answer); err == nil {
*form.number = int64(i)
intvar = i
}
})
return nil
})
config.Supervisor.Add(button)
boundsFrame.Pack(label, ui.Pack{
Side: ui.W,
PadX: 1,
})
boundsFrame.Pack(button, ui.Pack{
Side: ui.W,
PadX: 1,
})
}
}
/****************** /******************
* Frame for selecting Level Wallpaper * Frame for selecting Level Wallpaper
******************/ ******************/