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:
parent
feea703d0c
commit
1a8a5eb94b
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
Normal file
58
pkg/collision/collide_test.go
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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{}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
// to the linked VMs.
|
||||
type Message struct {
|
||||
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()
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -55,6 +58,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
|
|||
for _, channel := range vm.Outbound {
|
||||
channel <- Message{
|
||||
Name: name,
|
||||
SenderID: vm.Name,
|
||||
Args: v,
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +74,7 @@ func RegisterPublishHooks(s *Supervisor, vm *VM) {
|
|||
|
||||
toVM.Inbound <- Message{
|
||||
Name: name,
|
||||
SenderID: vm.Name,
|
||||
Args: v,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
******************/
|
||||
|
|
Loading…
Reference in New Issue
Block a user