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
Este commit está contenido en:
Noah 2021-10-09 20:45:38 -07:00
padre feea703d0c
commit 1a8a5eb94b
Se han modificado 13 ficheros con 231 adiciones y 30 borrados

Ver fichero

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

Ver fichero

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

Ver fichero

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

58
pkg/collision/collide_test.go Archivo normal
Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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

Ver fichero

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