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
pull/84/head
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?
var stickyDown = false;
Message.Subscribe("sticky:down", function(down) {
Message.Subscribe("sticky:down", function (down) {
stickyDown = down;
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) {
return;
}
if (colliders[e.Actor.ID()] == undefined) {
colliders[e.Actor.ID()] = true;
}
// If a linked Sticky Button is pressed, button stays down too and
// doesn't interact.
if (stickyDown) {
@ -37,12 +44,17 @@ function main() {
}
Self.ShowLayer(1);
timer = setTimeout(function() {
});
Events.OnLeave(function (e) {
delete colliders[e.Actor.ID()];
if (Object.keys(colliders).length === 0) {
Sound.Play("button-up.wav")
Self.ShowLayer(0);
Message.Publish("power", false);
timer = 0;
pressed = false;
}, 200);
}
});
}

View File

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

View File

@ -5,6 +5,12 @@ function main() {
Events.OnCollide(function (e) {
if (e.Settled) {
if (e.Actor.HasInventory()) {
// If we don't have a quantity, and the actor already has
// one of us, don't pick it up so the player can get it.
if (quantity === 0 && e.Actor.HasItem(Self.Filename) === 0 && !e.Actor.IsPlayer()) {
return;
}
Sound.Play("item-get.wav")
e.Actor.AddItem(Self.Filename, quantity);
Self.Destroy();

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) {
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
// negative coordinates.
drawAt := render.NewPoint(actor.Point.X, actor.Point.Y)
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 {
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
@ -124,7 +127,7 @@ func GiantScreenshot(lvl *level.Level) (image.Image, error) {
if !ok {
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/license"
"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/scripting"
"git.kirsle.net/apps/doodle/pkg/shmem"
@ -221,6 +222,20 @@ func (s *MainScene) Setup(d *Doodle) error {
// Check for update in the background.
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
}

View File

@ -292,7 +292,7 @@ func (s *PlayScene) setupPlayer() {
}
// 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)
}
}

View File

@ -46,6 +46,11 @@ func NewJSProxy(vm *VM) JSProxy {
"Add": func(t time.Time, ms int64) time.Time {
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.

View File

@ -8,8 +8,9 @@ import (
// Message holds data being published from one script VM with information sent
// to the linked VMs.
type Message struct {
Name string
Args []interface{}
Name string
SenderID string
Args []interface{}
}
/*
@ -23,14 +24,16 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
// for any matching messages received.
go func() {
for msg := range vm.Inbound {
vm.muSubscribe.RLock()
defer vm.muSubscribe.RUnlock()
vm.muSubscribe.Lock()
if _, ok := vm.subscribe[msg.Name]; ok {
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...)
}
}
vm.muSubscribe.Unlock()
}
}()
@ -54,8 +57,9 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
"Publish": func(name string, v ...interface{}) {
for _, channel := range vm.Outbound {
channel <- Message{
Name: name,
Args: v,
Name: name,
SenderID: vm.Name,
Args: v,
}
}
},
@ -69,8 +73,9 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
}
toVM.Inbound <- Message{
Name: name,
Args: v,
Name: name,
SenderID: vm.Name,
Args: v,
}
}
},

View File

@ -41,7 +41,7 @@ func (s *Supervisor) Loop() error {
// InstallScripts loads scripts for all actors in the level.
func (s *Supervisor) InstallScripts(level *level.Level) error {
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
}
}
@ -71,12 +71,14 @@ func (s *Supervisor) InstallScripts(level *level.Level) error {
}
// 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 {
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])
RegisterEventHooks(s, s.scripts[id])
if err := s.scripts[id].RegisterLevelHooks(); err != nil {

View File

@ -116,15 +116,19 @@ func (w *Canvas) loopConstrainScroll() error {
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.
if w.wallpaper.pageType >= level.Bounded &&
w.wallpaper.maxWidth+w.wallpaper.maxHeight > 0 {
maxWidth+maxHeight > 0 {
var (
// TODO: downcast from int64!
mw = int(w.wallpaper.maxWidth)
mh = int(w.wallpaper.maxHeight)
mw = int(maxWidth)
mh = int(maxHeight)
Viewport = w.Viewport()
vw = w.ZoomDivide(Viewport.W)
vh = w.ZoomDivide(Viewport.H)

View File

@ -1,6 +1,8 @@
package windows
import (
"strconv"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/modal"
@ -139,6 +141,79 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
typeBtn.Supervise(config.Supervisor)
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
******************/