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.
This commit is contained in:
Noah 2020-04-02 23:09:46 -07:00
parent 3cb99ad5f8
commit c3d7348843
5 changed files with 245 additions and 5 deletions

View File

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

View File

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

121
pkg/play_inventory.go Normal file
View File

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

View File

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

View File

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