From c3d7348843eb78c26973408d4a6d5573ab91aed2 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 2 Apr 2020 23:09:46 -0700 Subject: [PATCH] Inventory System for Level Actors * Added an inventory system for actors as a replacement to the arbitrary key/value data store. Colored keys now add themselves to the player's inventory, and colored doors check the inventory. * Inventory is a map[string]int between doodad filenames (red-key.doodad) and quantity (0 for key items/unlimited qty). * API methods to add and remove inventory. * Items HUD appears in Play Mode in lower-left corner showing doodad sprites of all the items in the Player's inventory. --- dev-assets/doodads/doors/colored-door.js | 6 +- dev-assets/doodads/doors/keys.js | 6 +- pkg/play_inventory.go | 121 +++++++++++++++++++++++ pkg/play_scene.go | 20 ++++ pkg/uix/actor.go | 97 +++++++++++++++++- 5 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 pkg/play_inventory.go diff --git a/dev-assets/doodads/doors/colored-door.js b/dev-assets/doodads/doors/colored-door.js index baf7ac4..48015d8 100644 --- a/dev-assets/doodads/doors/colored-door.js +++ b/dev-assets/doodads/doors/colored-door.js @@ -1,6 +1,7 @@ function main() { var color = Self.Doodad.Tag("color"); + var keyname = "key-" + color + ".doodad"; // Layers in the doodad image. var layer = { @@ -34,8 +35,9 @@ function main() { return; } - var data = e.Actor.GetData("key:" + color); - if (data === "") { + // Do they have our key? + var hasKey = e.Actor.HasItem(keyname) >= 0; + if (!hasKey) { // Door is locked. return false; } diff --git a/dev-assets/doodads/doors/keys.js b/dev-assets/doodads/doors/keys.js index 2f7fba1..667022e 100644 --- a/dev-assets/doodads/doors/keys.js +++ b/dev-assets/doodads/doors/keys.js @@ -2,7 +2,9 @@ function main() { var color = Self.Doodad.Tag("color"); Events.OnCollide(function(e) { - e.Actor.SetData("key:" + color, "true"); - Self.Destroy(); + if (e.Settled) { + e.Actor.AddItem(Self.Doodad.Filename, 0); + Self.Destroy(); + } }) } diff --git a/pkg/play_inventory.go b/pkg/play_inventory.go new file mode 100644 index 0000000..d92c091 --- /dev/null +++ b/pkg/play_inventory.go @@ -0,0 +1,121 @@ +package doodle + +import ( + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/doodads" + "git.kirsle.net/apps/doodle/pkg/uix" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" +) + +// setupInventoryHud configures the Inventory HUD. +func (s *PlayScene) setupInventoryHud() { + s.invenFrame = ui.NewFrame("Inventory") + s.invenDoodads = map[string]*uix.Canvas{} + s.invenFrame.Configure(ui.Config{ + BorderStyle: ui.BorderRaised, + BorderSize: 2, + Background: render.RGBA(128, 128, 128, 60), + }) + + // Items label. + label := ui.NewLabel(ui.Label{ + Text: "Items:", + Font: balance.LabelFont, + }) + label.Compute(s.d.Engine) + + // Configure the label tall enough to cover typical 32x32 item doodads. + // This pushes the Frame height tall enough. + label.Configure(ui.Config{ + Height: 36, + Width: label.Size().W + 2, + }) + s.invenFrame.Pack(label, ui.Pack{ + Side: ui.W, + PadX: 2, + PadY: 4, + }) + + // Add the inventory frame to the screen frame. + s.screen.Place(s.invenFrame, ui.Place{ + Left: 40, + Bottom: 40, + }) + + // Hide inventory if empty. + if len(s.invenItems) == 0 { + s.invenFrame.Hide() + } +} + +// computeInventory adjusts the inventory HUD when the player's inventory changes. +func (s *PlayScene) computeInventory() { + items := s.Player.ListItems() + if len(items) != len(s.invenItems) { + // Inventory has changed! See which doodads we have + // and which we need to load. + var seen = map[string]interface{}{} + for _, filename := range items { + seen[filename] = nil + + if _, ok := s.invenDoodads[filename]; !ok { + // Cache miss. Load the doodad here. + doodad, err := doodads.LoadFile(filename) + if err != nil { + s.d.Flash("Inventory item '%s' error: %s", filename, err) + continue + } + + canvas := uix.NewCanvas(doodad.ChunkSize(), false) + canvas.LoadDoodad(doodad) + canvas.Resize(render.NewRect( + doodad.ChunkSize(), doodad.ChunkSize(), + )) + s.invenFrame.Pack(canvas, ui.Pack{ + Side: ui.W, + + // TODO: work around a weird padding bug. item had too + // tall a top margin when added to the inventory frame! + PadX: 8, + }) + s.invenDoodads[filename] = canvas + } + + s.invenDoodads[filename].Show() + } + + // Hide any doodad that used to be in the inventory but now is not. + for filename, canvas := range s.invenDoodads { + if _, ok := seen[filename]; !ok { + canvas.Hide() + } + } + + // Recompute the size of the inventory frame. + // TODO: this works around a bug in ui.Frame, at the bottom of + // compute_packed, a frame Resize's itself to fit the children but this + // trips the "manually set size" boolean... packing more items after a + // computer doesn't resize the frame. So here, we resize-auto it to + // reset that boolean so the next compute, picks the right size. + s.invenFrame.Configure(ui.Config{ + AutoResize: true, + Width: 1, + Height: 1, + }) + s.invenFrame.Compute(s.d.Engine) + + // If we removed all items, hide the frame. + if len(items) == 0 { + s.invenFrame.Hide() + } else { + s.invenFrame.Show() + } + + // Cache the item list so we don't run the above logic every single tick. + s.invenItems = items + } + + // Compute the inventory frame so it positions and wraps the items. + s.screen.Compute(s.d.Engine) +} diff --git a/pkg/play_scene.go b/pkg/play_scene.go index da4ec86..cd39d93 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -31,6 +31,7 @@ type PlayScene struct { // UI widgets. supervisor *ui.Supervisor + screen *ui.Frame // A window sized invisible frame to position UI elements. editButton *ui.Button // The alert box shows up when the level goal is reached and includes @@ -53,6 +54,11 @@ type PlayScene struct { antigravity bool // Cheat: disable player gravity noclip bool // Cheat: disable player clipping playerJumpCounter int // limit jump length + + // Inventory HUD. Impl. in play_inventory.go + invenFrame *ui.Frame + invenItems []string // item list + invenDoodads map[string]*uix.Canvas } // Name of the scene. @@ -66,6 +72,10 @@ func (s *PlayScene) Setup(d *Doodle) error { s.scripting = scripting.NewSupervisor() s.supervisor = ui.NewSupervisor() + // Create an invisible 'screen' frame for UI elements to use for positioning. + s.screen = ui.NewFrame("Screen") + s.screen.Resize(render.NewRect(d.width, d.height)) + // Level Exit handler. s.SetupAlertbox() s.scripting.OnLevelExit(func() { @@ -112,6 +122,9 @@ func (s *PlayScene) Setup(d *Doodle) error { }) s.supervisor.Add(s.editButton) + // Set up the inventory HUD. + s.setupInventoryHud() + // Initialize the drawing canvas. s.drawing = uix.NewCanvas(balance.ChunkSize, false) s.drawing.Name = "play-canvas" @@ -382,6 +395,9 @@ func (s *PlayScene) Loop(d *Doodle, ev *event.State) error { if err := s.drawing.Loop(ev); err != nil { log.Error("Drawing loop error: %s", err.Error()) } + + // Update the inventory HUD. + s.computeInventory() } return nil @@ -398,6 +414,10 @@ func (s *PlayScene) Draw(d *Doodle) error { // Draw out bounding boxes. d.DrawCollisionBox(s.Player) + // Draw the UI screen and any widgets that attached to it. + s.screen.Compute(d.Engine) + s.screen.Present(d.Engine, render.Origin) + // Draw the Edit button. var ( canSize = s.drawing.Size() diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 1a6ffb1..62096af 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -3,6 +3,8 @@ package uix import ( "errors" "fmt" + "sort" + "sync" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" @@ -33,12 +35,17 @@ type Actor struct { isMobile bool // Mobile character, such as the player or an enemy noclip bool // Disable collision detection hitbox render.Rect - data map[string]string + inventory map[string]int // item inventory. doodad name -> quantity, 0 for key item. + data map[string]string // arbitrary key/value store. DEPRECATED ?? // Animation variables. animations map[string]*Animation activeAnimation *Animation animationCallback otto.Value + + // Mutex. + muInventory sync.RWMutex + muData sync.RWMutex } // NewActor sets up a uix.Actor. @@ -64,6 +71,7 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor Actor: levelActor, Canvas: can, animations: map[string]*Animation{}, + inventory: map[string]int{}, } // Give the Canvas a pointer to its (parent) Actor so it can draw its debug @@ -96,6 +104,87 @@ func (a *Actor) SetNoclip(v bool) { a.noclip = v } +// AddItem adds an item doodad to the actor's inventory. +// Item name is usually the doodad filename. +func (a *Actor) AddItem(itemName string, quantity int) { + a.muInventory.Lock() + a.inventory[itemName] = quantity + a.muInventory.Unlock() +} + +// RemoveItem removes a quantity of an item from the actor's inventory. +// +// Provide a quantity of 0 to remove the item completely. +// Otherwise provides a number greater than zero and you will subtract this +// quantity from the item. If the item then is at <= zero, it is removed from +// inventory. +func (a *Actor) RemoveItem(itemName string, quantity int) bool { + a.muInventory.RLock() + defer a.muInventory.RUnlock() + + if _, ok := a.inventory[itemName]; ok { + // If quantity is zero, remove the item entirely. + if quantity <= 0 { + delete(a.inventory, itemName) + } else { + // Subtract the quantity from inventory. If we have run down to + // zero left, remove the item entirely. + a.inventory[itemName] -= quantity + if a.inventory[itemName] <= 0 { + delete(a.inventory, itemName) + } + } + return true + } + return false +} + +// HasItem checks the actor's inventory for the item and returns the quantity. +// +// A return value of -1 means the item was not found. +// The value 0 indicates a key item (one with no quantity). +// Values >= 1 would be consumable items. +func (a *Actor) HasItem(itemName string) int { + a.muInventory.RLock() + defer a.muInventory.RUnlock() + + if quantity, ok := a.inventory[itemName]; ok { + return quantity + } + return -1 +} + +// ListItems returns a sorted list of the items in the actor's inventory. +func (a *Actor) ListItems() []string { + a.muInventory.RLock() + defer a.muInventory.RUnlock() + + var ( + result = make([]string, len(a.inventory)) + i = 0 + ) + for k := range a.inventory { + result[i] = k + i++ + } + + sort.Strings(result) + return result +} + +// Inventory returns a copy of the actor's inventory struct. +func (a *Actor) Inventory() map[string]int { + a.muInventory.RLock() + defer a.muInventory.RUnlock() + + var result = map[string]int{} + for k, v := range a.inventory { + result[k] = v + } + + return result +} + // GetBoundingRect gets the bounding box of the actor's doodad. func (a *Actor) GetBoundingRect() render.Rect { return doodads.GetBoundingRect(a) @@ -121,7 +210,10 @@ func (a *Actor) SetData(key, value string) { if a.data == nil { a.data = map[string]string{} } + + a.muData.Lock() a.data[key] = value + a.muData.Unlock() } // GetData gets an arbitrary field from the actor's K/V storage. @@ -132,7 +224,10 @@ func (a *Actor) GetData(key string) string { return "" } + a.muData.RLock() v, _ := a.data[key] + a.muData.RUnlock() + return v }