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.
This commit is contained in:
Noah 2021-09-03 20:39:44 -07:00
parent c499a15c71
commit 7866f618da
8 changed files with 135 additions and 11 deletions

View File

@ -10,8 +10,10 @@ var falling = false;
function main() { function main() {
// Make the hitbox be the full canvas size of this doodad. // Make the hitbox be the full canvas size of this doodad.
// Adjust if you want a narrower hitbox. // Adjust if you want a narrower hitbox.
if (Self.Hitbox().IsZero()) {
var size = Self.Size() var size = Self.Size()
Self.SetHitbox(0, 0, size.W, size.H) Self.SetHitbox(0, 0, size.W, size.H)
}
// Note: doodad is not "solid" but hurts if it falls on you. // Note: doodad is not "solid" but hurts if it falls on you.
Self.SetMobile(true); Self.SetMobile(true);

View File

@ -9,8 +9,10 @@ Can be attached to any doodad.
function main() { function main() {
// Make the hitbox be the full canvas size of this doodad. // Make the hitbox be the full canvas size of this doodad.
// Adjust if you want a narrower hitbox. // Adjust if you want a narrower hitbox.
if (Self.Hitbox().IsZero()) {
var size = Self.Size() var size = Self.Size()
Self.SetHitbox(0, 0, size.W, size.H) Self.SetHitbox(0, 0, size.W, size.H)
}
Events.OnCollide(function (e) { Events.OnCollide(function (e) {
if (!e.Settled || !e.InHitbox) { if (!e.Settled || !e.InHitbox) {

View File

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

View File

@ -9,8 +9,10 @@ Can be attached to any doodad.
function main() { function main() {
// Make the hitbox be the full canvas size of this doodad. // Make the hitbox be the full canvas size of this doodad.
// Adjust if you want a narrower hitbox. // Adjust if you want a narrower hitbox.
if (Self.Hitbox().IsZero()) {
var size = Self.Size() var size = Self.Size()
Self.SetHitbox(0, 0, size.W, size.H) Self.SetHitbox(0, 0, size.W, size.H)
}
// Solid to all collisions. // Solid to all collisions.
Events.OnCollide(function (e) { Events.OnCollide(function (e) {

View File

@ -14,6 +14,7 @@ type Doodad struct {
Hidden bool `json:"hidden,omitempty"` Hidden bool `json:"hidden,omitempty"`
Palette *level.Palette `json:"palette"` Palette *level.Palette `json:"palette"`
Script string `json:"script"` Script string `json:"script"`
Hitbox render.Rect `json:"hitbox"`
Layers []Layer `json:"layers"` Layers []Layer `json:"layers"`
Tags map[string]string `json:"data"` // arbitrary key/value data storage Tags map[string]string `json:"data"` // arbitrary key/value data storage
} }
@ -35,6 +36,7 @@ func New(size int) *Doodad {
Version: 1, Version: 1,
}, },
Palette: level.DefaultPalette(), Palette: level.DefaultPalette(),
Hitbox: render.NewRect(size, size),
Layers: []Layer{ Layers: []Layer{
{ {
Name: "main", Name: "main",

View File

@ -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 { func (a *Actor) Hitbox() render.Rect {
if a.hitbox.IsZero() && !a.Drawing.Doodad.Hitbox.IsZero() {
return a.Drawing.Doodad.Hitbox
}
return a.hitbox return a.hitbox
} }

View File

@ -24,6 +24,7 @@ func (w *Canvas) MakeSelfAPI(actor *Actor) map[string]interface{} {
actor.SetGrounded(false) actor.SetGrounded(false)
}, },
"SetHitbox": actor.SetHitbox, "SetHitbox": actor.SetHitbox,
"Hitbox": actor.Hitbox,
"SetVelocity": actor.SetVelocity, "SetVelocity": actor.SetVelocity,
"SetMobile": actor.SetMobile, "SetMobile": actor.SetMobile,
"SetInventory": actor.SetInventory, "SetInventory": actor.SetInventory,

View File

@ -6,6 +6,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv"
"strings"
"git.kirsle.net/apps/doodle/assets" "git.kirsle.net/apps/doodle/assets"
"git.kirsle.net/apps/doodle/pkg/balance" "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/modal"
"git.kirsle.net/apps/doodle/pkg/native" "git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
) )
@ -23,6 +26,7 @@ var GenericScripts = []struct {
Label string Label string
Help string Help string
Filename string Filename string
SetTags map[string]string
}{ }{
{ {
Label: "Generic Solid", Label: "Generic Solid",
@ -47,6 +51,16 @@ var GenericScripts = []struct {
"'Watch out for (title)!'", "'Watch out for (title)!'",
Filename: "assets/scripts/generic-anvil.js", 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. // DoodadProperties window.
@ -120,8 +134,10 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int)
////////////// //////////////
// Draw the editable metadata form. // Draw the editable metadata form.
var hitboxString = c.EditDoodad.Hitbox.String()
for _, data := range []struct { for _, data := range []struct {
Label string Label string
Prompt string // optional
Variable *string Variable *string
Update func(string) Update func(string)
}{ }{
@ -139,6 +155,40 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int)
c.EditDoodad.Author = v 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 data := data
frame := ui.NewFrame("Metadata " + data.Label + " Frame") frame := ui.NewFrame("Metadata " + data.Label + " Frame")
@ -166,7 +216,12 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int)
Font: balance.MenuFont, Font: balance.MenuFont,
})) }))
btn.Handle(ui.Click, func(ed ui.EventData) error { 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 != "" { if answer != "" {
data.Update(answer) data.Update(answer)
} }
@ -236,13 +291,23 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int)
PadX: 2, PadX: 2,
}) })
// Save Button // Open Button
saveBtn := ui.NewButton("Save", ui.NewLabel(ui.Label{ saveBtn := ui.NewButton("Open", ui.NewLabel(ui.Label{
Text: "Save", Text: "View",
Font: balance.MenuFont, Font: balance.MenuFont,
})) }))
saveBtn.SetStyle(&balance.ButtonPrimary) saveBtn.SetStyle(&balance.ButtonPrimary)
saveBtn.Handle(ui.Click, func(ed ui.EventData) error { 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) { shmem.Prompt("Save script as (*.js): ", func(answer string) {
if answer != "" { if answer != "" {
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
@ -251,6 +316,7 @@ func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int)
shmem.Flash(err.Error()) shmem.Flash(err.Error())
} else { } else {
shmem.Flash("Written to: %s (%d bytes)", filepath.Join(cwd, answer), len(c.EditDoodad.Script)) 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. // Find the data from the builtins.
var label, help string var label, help string
var setTags map[string]string
for _, script := range GenericScripts { for _, script := range GenericScripts {
if script.Filename == filename { if script.Filename == filename {
label = script.Label label = script.Label
help = script.Help help = script.Help
setTags = script.SetTags
break 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)) 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. // Toggle the if/else frames.
ifScript.Show() ifScript.Show()
elseScript.Hide() elseScript.Hide()