From 7866f618da6155ae4f20cdcc4fe19cc06f82328d Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Fri, 3 Sep 2021 20:39:44 -0700 Subject: [PATCH] First-class Doodad Hitboxes + Generic Item Script A new property is added to the Doodad struct: Hitbox (Rect). The uix.Actor for Play Mode will defer to the Doodad.Hitbox until the JavaScript has manually set its own via Self.SetHitbox(). So in effect, scripts no longer need to worry about their hitbox! The one assigned to the Doodad will be the default. Scripts can check if their hitbox is zero before setting a default: if (Self.Hitbox().IsZero()) { var size = Self.Size() // get doodad canvas size Self.SetHitbox(0, 0, size, size) // the full square } The built-in generic doodad scripts have made this change, so that your simple doodad can have a custom hitbox defined easily using in-game tools. Other changes: * New script: Generic Collectible Item. Selecting it will add a "quantity" tag to your doodad, to easily configure the script. * JavaScript API: "Self.Hitbox()" returns your doodad's current hitbox. You can check "Self.Hitbox.IsZero()" to check if it's empty. --- assets/scripts/generic-anvil.js | 6 ++- assets/scripts/generic-fire.js | 6 ++- assets/scripts/generic-item.js | 35 +++++++++++++ assets/scripts/generic-solid.js | 6 ++- pkg/doodads/doodad.go | 2 + pkg/uix/actor.go | 6 ++- pkg/uix/scripting.go | 1 + pkg/windows/doodad_properties.go | 84 ++++++++++++++++++++++++++++++-- 8 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 assets/scripts/generic-item.js diff --git a/assets/scripts/generic-anvil.js b/assets/scripts/generic-anvil.js index 5caa7dc..369e5b6 100644 --- a/assets/scripts/generic-anvil.js +++ b/assets/scripts/generic-anvil.js @@ -10,8 +10,10 @@ var falling = false; function main() { // Make the hitbox be the full canvas size of this doodad. // Adjust if you want a narrower hitbox. - var size = Self.Size() - Self.SetHitbox(0, 0, size.W, size.H) + if (Self.Hitbox().IsZero()) { + var size = Self.Size() + Self.SetHitbox(0, 0, size.W, size.H) + } // Note: doodad is not "solid" but hurts if it falls on you. Self.SetMobile(true); diff --git a/assets/scripts/generic-fire.js b/assets/scripts/generic-fire.js index aad0fa7..5b4f36d 100644 --- a/assets/scripts/generic-fire.js +++ b/assets/scripts/generic-fire.js @@ -9,8 +9,10 @@ Can be attached to any doodad. function main() { // Make the hitbox be the full canvas size of this doodad. // Adjust if you want a narrower hitbox. - var size = Self.Size() - Self.SetHitbox(0, 0, size.W, size.H) + if (Self.Hitbox().IsZero()) { + var size = Self.Size() + Self.SetHitbox(0, 0, size.W, size.H) + } Events.OnCollide(function (e) { if (!e.Settled || !e.InHitbox) { diff --git a/assets/scripts/generic-item.js b/assets/scripts/generic-item.js new file mode 100644 index 0000000..aaf3922 --- /dev/null +++ b/assets/scripts/generic-item.js @@ -0,0 +1,35 @@ +// Generic Item Script +/* +A script that makes your item pocket-able, like the Keys. + +Your doodad sprite will appear in the Inventory menu if the +player picks it up. + +Configure it with tags: +- quantity: integer quantity value, default is 1, + set to 0 to make it a 'key item' + +Can be attached to any doodad. +*/ + +function main() { + // Make the hitbox be the full canvas size of this doodad. + // Adjust if you want a narrower hitbox. + if (Self.Hitbox().IsZero()) { + var size = Self.Size() + Self.SetHitbox(0, 0, size.W, size.H) + } + + var qtySetting = Self.GetTag("quantity") + var quantity = qtySetting === "" ? 1 : parseInt(qtySetting); + + Events.OnCollide(function (e) { + if (e.Settled) { + if (e.Actor.HasInventory()) { + Sound.Play("item-get.wav") + e.Actor.AddItem(Self.Filename, quantity); + Self.Destroy(); + } + } + }) +} diff --git a/assets/scripts/generic-solid.js b/assets/scripts/generic-solid.js index 49cd62d..154bd2b 100644 --- a/assets/scripts/generic-solid.js +++ b/assets/scripts/generic-solid.js @@ -9,8 +9,10 @@ Can be attached to any doodad. function main() { // Make the hitbox be the full canvas size of this doodad. // Adjust if you want a narrower hitbox. - var size = Self.Size() - Self.SetHitbox(0, 0, size.W, size.H) + if (Self.Hitbox().IsZero()) { + var size = Self.Size() + Self.SetHitbox(0, 0, size.W, size.H) + } // Solid to all collisions. Events.OnCollide(function (e) { diff --git a/pkg/doodads/doodad.go b/pkg/doodads/doodad.go index b95f511..10d07ad 100644 --- a/pkg/doodads/doodad.go +++ b/pkg/doodads/doodad.go @@ -14,6 +14,7 @@ type Doodad struct { Hidden bool `json:"hidden,omitempty"` Palette *level.Palette `json:"palette"` Script string `json:"script"` + Hitbox render.Rect `json:"hitbox"` Layers []Layer `json:"layers"` Tags map[string]string `json:"data"` // arbitrary key/value data storage } @@ -35,6 +36,7 @@ func New(size int) *Doodad { Version: 1, }, Palette: level.DefaultPalette(), + Hitbox: render.NewRect(size, size), Layers: []Layer{ { Name: "main", diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 2b0643a..9852b90 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -326,8 +326,12 @@ func (a *Actor) SetHitbox(x, y, w, h int) { } } -// Hitbox returns the actor's elected hitbox. +// Hitbox returns the actor's elected hitbox. If the JavaScript did not set +// a hitbox, it defers to the Doodad's metadata hitbox. func (a *Actor) Hitbox() render.Rect { + if a.hitbox.IsZero() && !a.Drawing.Doodad.Hitbox.IsZero() { + return a.Drawing.Doodad.Hitbox + } return a.hitbox } diff --git a/pkg/uix/scripting.go b/pkg/uix/scripting.go index f4f3cf5..cbb5ab2 100644 --- a/pkg/uix/scripting.go +++ b/pkg/uix/scripting.go @@ -24,6 +24,7 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} { actor.SetGrounded(false) }, "SetHitbox": actor.SetHitbox, + "Hitbox": actor.Hitbox, "SetVelocity": actor.SetVelocity, "SetMobile": actor.SetMobile, "SetInventory": actor.SetInventory, diff --git a/pkg/windows/doodad_properties.go b/pkg/windows/doodad_properties.go index 2837c96..d7eac6e 100644 --- a/pkg/windows/doodad_properties.go +++ b/pkg/windows/doodad_properties.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" "sort" + "strconv" + "strings" "git.kirsle.net/apps/doodle/assets" "git.kirsle.net/apps/doodle/pkg/balance" @@ -14,6 +16,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/modal" "git.kirsle.net/apps/doodle/pkg/native" "git.kirsle.net/apps/doodle/pkg/shmem" + "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" ) @@ -23,6 +26,7 @@ var GenericScripts = []struct { Label string Help string Filename string + SetTags map[string]string }{ { Label: "Generic Solid", @@ -47,6 +51,16 @@ var GenericScripts = []struct { "'Watch out for (title)!'", Filename: "assets/scripts/generic-anvil.js", }, + { + Label: "Generic Collectible Item", + Help: "This doodad will behave like a pocketable item, like\n" + + "the Keys. Tip: set a Doodad tag like quantity=0 to set\n" + + "the item quantity when picked up (default is 1).", + Filename: "assets/scripts/generic-item.js", + SetTags: map[string]string{ + "quantity": "1", + }, + }, } // DoodadProperties window. @@ -120,8 +134,10 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) ////////////// // Draw the editable metadata form. + var hitboxString = c.EditDoodad.Hitbox.String() for _, data := range []struct { Label string + Prompt string // optional Variable *string Update func(string) }{ @@ -139,6 +155,40 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) c.EditDoodad.Author = v }, }, + { + Label: "Hitbox:", + Prompt: "Enter hitbox in X,Y,W,H or just W,H format: ", + Variable: &hitboxString, + Update: func(v string) { + // Parse it. + parts := strings.Split(v, ",") + var ints []int + for _, part := range parts { + a, err := strconv.Atoi(strings.TrimSpace(part)) + if err != nil { + shmem.Flash("Invalid format for hitbox, using the default") + return + } + ints = append(ints, a) + } + + if len(ints) == 2 { + c.EditDoodad.Hitbox = render.NewRect(ints[0], ints[1]) + } else if len(ints) == 4 { + c.EditDoodad.Hitbox = render.Rect{ + X: ints[0], + Y: ints[1], + W: ints[2], + H: ints[3], + } + } else { + shmem.Flash("Hitbox should be in X,Y,W,H or just W,H format, 2 or 4 numbers.") + return + } + + hitboxString = c.EditDoodad.Hitbox.String() + }, + }, } { data := data frame := ui.NewFrame("Metadata " + data.Label + " Frame") @@ -166,7 +216,12 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) Font: balance.MenuFont, })) btn.Handle(ui.Click, func(ed ui.EventData) error { - shmem.Prompt("Enter a new "+data.Label+" ", func(answer string) { + var prompt = data.Prompt + if prompt == "" { + prompt = "Enter a new " + data.Label + ": " + } + + shmem.Prompt(prompt, func(answer string) { if answer != "" { data.Update(answer) } @@ -236,13 +291,23 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) PadX: 2, }) - // Save Button - saveBtn := ui.NewButton("Save", ui.NewLabel(ui.Label{ - Text: "Save", + // Open Button + saveBtn := ui.NewButton("Open", ui.NewLabel(ui.Label{ + Text: "View", Font: balance.MenuFont, })) saveBtn.SetStyle(&balance.ButtonPrimary) saveBtn.Handle(ui.Click, func(ed ui.EventData) error { + // Write the js file to cache and try and open it in the user's + // native text editor program. + outname := filepath.Join(userdir.CacheDirectory, c.EditDoodad.Filename+".js") + err := ioutil.WriteFile(outname, []byte(c.EditDoodad.Script), 0644) + if err == nil { + native.OpenLocalURL(outname) + return nil + } + + // Otherwise, prompt the user for their filepath. shmem.Prompt("Save script as (*.js): ", func(answer string) { if answer != "" { cwd, _ := os.Getwd() @@ -251,6 +316,7 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) shmem.Flash(err.Error()) } else { shmem.Flash("Written to: %s (%d bytes)", filepath.Join(cwd, answer), len(c.EditDoodad.Script)) + native.OpenLocalURL(filepath.Join(cwd, answer)) } } }) @@ -383,10 +449,12 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) // Find the data from the builtins. var label, help string + var setTags map[string]string for _, script := range GenericScripts { if script.Filename == filename { label = script.Label help = script.Help + setTags = script.SetTags break } } @@ -408,6 +476,14 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) shmem.Flash("Attached %s to your doodad", filepath.Base(filename)) + // Set any tags that come with this script. + if setTags != nil && len(setTags) > 0 { + for k, v := range setTags { + log.Info("Set doodad tag %s=%s", k, v) + c.EditDoodad.Tags[k] = v + } + } + // Toggle the if/else frames. ifScript.Show() elseScript.Hide()