ci/woodpecker/push/woodpecker Pipeline was successful
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>
150 lines
3.6 KiB
Go
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)
|
|
}
|