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:
parent
3cb99ad5f8
commit
c3d7348843
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
var color = Self.Doodad.Tag("color");
|
var color = Self.Doodad.Tag("color");
|
||||||
|
var keyname = "key-" + color + ".doodad";
|
||||||
|
|
||||||
// Layers in the doodad image.
|
// Layers in the doodad image.
|
||||||
var layer = {
|
var layer = {
|
||||||
|
@ -34,8 +35,9 @@ function main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = e.Actor.GetData("key:" + color);
|
// Do they have our key?
|
||||||
if (data === "") {
|
var hasKey = e.Actor.HasItem(keyname) >= 0;
|
||||||
|
if (!hasKey) {
|
||||||
// Door is locked.
|
// Door is locked.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@ function main() {
|
||||||
var color = Self.Doodad.Tag("color");
|
var color = Self.Doodad.Tag("color");
|
||||||
|
|
||||||
Events.OnCollide(function(e) {
|
Events.OnCollide(function(e) {
|
||||||
e.Actor.SetData("key:" + color, "true");
|
if (e.Settled) {
|
||||||
Self.Destroy();
|
e.Actor.AddItem(Self.Doodad.Filename, 0);
|
||||||
|
Self.Destroy();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
121
pkg/play_inventory.go
Normal file
121
pkg/play_inventory.go
Normal 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)
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ type PlayScene struct {
|
||||||
|
|
||||||
// UI widgets.
|
// UI widgets.
|
||||||
supervisor *ui.Supervisor
|
supervisor *ui.Supervisor
|
||||||
|
screen *ui.Frame // A window sized invisible frame to position UI elements.
|
||||||
editButton *ui.Button
|
editButton *ui.Button
|
||||||
|
|
||||||
// The alert box shows up when the level goal is reached and includes
|
// 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
|
antigravity bool // Cheat: disable player gravity
|
||||||
noclip bool // Cheat: disable player clipping
|
noclip bool // Cheat: disable player clipping
|
||||||
playerJumpCounter int // limit jump length
|
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.
|
// Name of the scene.
|
||||||
|
@ -66,6 +72,10 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
||||||
s.scripting = scripting.NewSupervisor()
|
s.scripting = scripting.NewSupervisor()
|
||||||
s.supervisor = ui.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.
|
// Level Exit handler.
|
||||||
s.SetupAlertbox()
|
s.SetupAlertbox()
|
||||||
s.scripting.OnLevelExit(func() {
|
s.scripting.OnLevelExit(func() {
|
||||||
|
@ -112,6 +122,9 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
||||||
})
|
})
|
||||||
s.supervisor.Add(s.editButton)
|
s.supervisor.Add(s.editButton)
|
||||||
|
|
||||||
|
// Set up the inventory HUD.
|
||||||
|
s.setupInventoryHud()
|
||||||
|
|
||||||
// Initialize the drawing canvas.
|
// Initialize the drawing canvas.
|
||||||
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
|
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
|
||||||
s.drawing.Name = "play-canvas"
|
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 {
|
if err := s.drawing.Loop(ev); err != nil {
|
||||||
log.Error("Drawing loop error: %s", err.Error())
|
log.Error("Drawing loop error: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the inventory HUD.
|
||||||
|
s.computeInventory()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -398,6 +414,10 @@ func (s *PlayScene) Draw(d *Doodle) error {
|
||||||
// Draw out bounding boxes.
|
// Draw out bounding boxes.
|
||||||
d.DrawCollisionBox(s.Player)
|
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.
|
// Draw the Edit button.
|
||||||
var (
|
var (
|
||||||
canSize = s.drawing.Size()
|
canSize = s.drawing.Size()
|
||||||
|
|
|
@ -3,6 +3,8 @@ package uix
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"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
|
isMobile bool // Mobile character, such as the player or an enemy
|
||||||
noclip bool // Disable collision detection
|
noclip bool // Disable collision detection
|
||||||
hitbox render.Rect
|
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.
|
// Animation variables.
|
||||||
animations map[string]*Animation
|
animations map[string]*Animation
|
||||||
activeAnimation *Animation
|
activeAnimation *Animation
|
||||||
animationCallback otto.Value
|
animationCallback otto.Value
|
||||||
|
|
||||||
|
// Mutex.
|
||||||
|
muInventory sync.RWMutex
|
||||||
|
muData sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewActor sets up a uix.Actor.
|
// NewActor sets up a uix.Actor.
|
||||||
|
@ -64,6 +71,7 @@ func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor
|
||||||
Actor: levelActor,
|
Actor: levelActor,
|
||||||
Canvas: can,
|
Canvas: can,
|
||||||
animations: map[string]*Animation{},
|
animations: map[string]*Animation{},
|
||||||
|
inventory: map[string]int{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the Canvas a pointer to its (parent) Actor so it can draw its debug
|
// 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
|
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.
|
// GetBoundingRect gets the bounding box of the actor's doodad.
|
||||||
func (a *Actor) GetBoundingRect() render.Rect {
|
func (a *Actor) GetBoundingRect() render.Rect {
|
||||||
return doodads.GetBoundingRect(a)
|
return doodads.GetBoundingRect(a)
|
||||||
|
@ -121,7 +210,10 @@ func (a *Actor) SetData(key, value string) {
|
||||||
if a.data == nil {
|
if a.data == nil {
|
||||||
a.data = map[string]string{}
|
a.data = map[string]string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.muData.Lock()
|
||||||
a.data[key] = value
|
a.data[key] = value
|
||||||
|
a.muData.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetData gets an arbitrary field from the actor's K/V storage.
|
// GetData gets an arbitrary field from the actor's K/V storage.
|
||||||
|
@ -132,7 +224,10 @@ func (a *Actor) GetData(key string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.muData.RLock()
|
||||||
v, _ := a.data[key]
|
v, _ := a.data[key]
|
||||||
|
a.muData.RUnlock()
|
||||||
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user