Files
inkwell/ui.inventory.go
T
mr.zeroandClaude Opus 5 2429ada090
ci/woodpecker/push/woodpecker Pipeline was successful
A widget declares its layer
Registration order was the only thing deciding what a widget was drawn
over, which forced a domain to keep one ordered list of every widget it
owns — the one shape that cannot be split into a file per widget.

A widget now says where it belongs: Layer, the eight LayerScene..LayerCursor
constants, the optional Layered interface and LayerOf for wrappers. Draw
runs from the bottom layer up, Tick from the top down, and registration
order only breaks ties inside a layer. Every built-in declares its own;
anything that stays quiet sits on LayerHUD, so existing HUDs come out where
they were.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 22:19:17 +02:00

150 lines
3.6 KiB
Go

package inkwell
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
// InventoryBar shows the player's items as clickable slots. Left-click on
// a slot under the "look" verb prints the item's description; under any
// other verb, it toggles selection (the cursor "picks up" the item).
type InventoryBar struct {
Name string
Origin Point
Slots int
Cols int
SlotSize int
Gap int
PanelBG bool
}
func (b *InventoryBar) GetName() string { return b.Name }
func (b *InventoryBar) Layer() Layer { return LayerHUD }
func (b *InventoryBar) slotRect(idx int) Rectangle {
cols := b.Cols
if cols <= 0 {
cols = b.Slots
}
step := b.SlotSize + b.Gap
col := idx % cols
row := idx / cols
x := b.Origin.X + float64(col*step)
y := b.Origin.Y + float64(row*step)
return Rect(x, y, float64(b.SlotSize), float64(b.SlotSize))
}
func (b *InventoryBar) Tick(ctx *UICtx) {
g := ctx.Game
mp := g.Input.Point()
items := g.Inventory.Items()
// hover label: when over a slot with an item, show its description
for i := 0; i < min(b.Slots, len(items)); i++ {
if b.slotRect(i).Contains(mp) {
if it, ok := g.ItemManager.Get(items[i]); ok && it.Description != "" {
g.SetHoverLabel(it.Description)
} else {
g.SetHoverLabel(items[i])
}
break
}
}
if !g.Input.LeftClicked() {
return
}
for i := 0; i < min(b.Slots, len(items)); i++ {
if b.slotRect(i).Contains(mp) {
g.Input.ConsumeLeft()
name := items[i]
if g.SelectedVerb() == "look" {
if it, ok := g.ItemManager.Get(name); ok && it.Description != "" {
g.queueAction(Say("player", it.Description), "inv-look")
return
}
}
if g.Inventory.Selected() == name {
g.Inventory.Select("")
} else {
g.Inventory.Select(name)
}
return
}
}
}
func (b *InventoryBar) Draw(dst *ebiten.Image, ctx *UICtx) {
g := ctx.Game
th := g.Theme()
items := g.Inventory.Items()
if b.PanelBG {
last := b.slotRect(b.Slots - 1)
first := b.slotRect(0)
x := first.X - 2
y := first.Y - 2
w := (last.X + last.W) - first.X + 4
h := (last.Y + last.H) - first.Y + 4
vector.DrawFilledRect(dst, float32(x), float32(y), float32(w), float32(h), th.PanelBG, false)
}
for i := 0; i < b.Slots; i++ {
r := b.slotRect(i)
bg := th.InventorySlotBG
if i < len(items) && items[i] == g.Inventory.Selected() {
bg = th.InventorySlotSelectedBG
}
vector.DrawFilledRect(dst, float32(r.X), float32(r.Y), float32(r.W), float32(r.H), bg, false)
if i < len(items) {
it, ok := g.ItemManager.Get(items[i])
if !ok {
continue
}
img := g.loaded.image(g.AssetManager, it.Sprite)
sw, sh := img.Bounds().Dx(), img.Bounds().Dy()
if sw == 0 || sh == 0 {
continue
}
if g.loaded.isPlaceholder(it.Sprite) {
// draw a small colored square as a stand-in
inset := float32(2)
vector.DrawFilledRect(dst,
float32(r.X)+inset, float32(r.Y)+inset,
float32(r.W)-2*inset, float32(r.H)-2*inset,
th.VerbButtonSelectedBG, false)
continue
}
op := &ebiten.DrawImageOptions{}
inset := 1.0
tgtW := r.W - 2*inset
tgtH := r.H - 2*inset
op.GeoM.Scale(tgtW/float64(sw), tgtH/float64(sh))
op.GeoM.Translate(r.X+inset, r.Y+inset)
dst.DrawImage(img, op)
}
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// BlocksClickAt — clickBlocker contract for the RadialVerbs widget.
func (b *InventoryBar) BlocksClickAt(p Point) bool {
if b.Slots <= 0 {
return false
}
first := b.slotRect(0)
last := b.slotRect(b.Slots - 1)
x := first.X
y := first.Y
w := last.X + last.W - first.X
h := last.Y + last.H - first.Y
return Rect(x, y, w, h).Contains(p)
}