diff --git a/archetype/item.go b/archetype/item.go index bda5de8..5da29f9 100644 --- a/archetype/item.go +++ b/archetype/item.go @@ -14,6 +14,7 @@ func CreateNewItem(world donburi.World, itemData *items.ItemData) *donburi.Entry tags.ItemTag, component.Name, component.Sprite, + component.Description, )) name := component.NameData{ @@ -26,6 +27,11 @@ func CreateNewItem(world donburi.World, itemData *items.ItemData) *donburi.Entry } component.Sprite.SetValue(entry, sprite) + description := component.DescriptionData{ + Value: itemData.Description, + } + component.Description.SetValue(entry, description) + return entry } diff --git a/archetype/monster.go b/archetype/monster.go index b7ff3e6..9d07174 100644 --- a/archetype/monster.go +++ b/archetype/monster.go @@ -106,3 +106,24 @@ func CreateMonster(world donburi.World, level *component.LevelData, room engine. defense := component.Defense.Get(equipment.Armor) component.Defense.SetValue(monster, *defense) } + +func RemoveMonster(entry *donburi.Entry, world donburi.World) { + equipment := component.Equipment.Get(entry) + + if equipment.Armor != nil { + equipment.Armor.Remove() + } + if equipment.Weapon != nil { + equipment.Weapon.Remove() + } + if equipment.Sheild != nil { + equipment.Sheild.Remove() + } + if equipment.Gloves != nil { + equipment.Gloves.Remove() + } + if equipment.Boots != nil { + equipment.Boots.Remove() + } + entry.Remove() +} diff --git a/archetype/player.go b/archetype/player.go index a01d3ca..9a8f1cf 100644 --- a/archetype/player.go +++ b/archetype/player.go @@ -4,6 +4,7 @@ import ( "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/assets" "github.com/kensonjohnson/roguelike-game-go/component" + "github.com/kensonjohnson/roguelike-game-go/internal/engine" "github.com/kensonjohnson/roguelike-game-go/items" "github.com/norendren/go-fov/fov" "github.com/yohamta/donburi" @@ -62,8 +63,14 @@ func CreateNewPlayer( component.Equipment.SetValue(player, equipment) // Setup inventory - inventory := component.NewInventory(30) + inventory := component.NewInventory(28) component.Inventory.SetValue(player, inventory) + if engine.Debug.On() { + inventory.AddItem(CreateNewValuable(world, items.Valuables.Alcohol)) + inventory.AddItem(CreateNewConsumable(world, items.Consumables.Apple)) + inventory.AddItem(CreateNewConsumable(world, items.Consumables.HealthPotion)) + inventory.AddItem(CreateNewConsumable(world, items.Consumables.Bread)) + } wallet := component.WalletData{} component.Wallet.SetValue(player, wallet) diff --git a/archetype/ui.go b/archetype/ui.go deleted file mode 100644 index 3fbce2f..0000000 --- a/archetype/ui.go +++ /dev/null @@ -1,48 +0,0 @@ -package archetype - -import ( - "github.com/kensonjohnson/roguelike-game-go/archetype/tags" - "github.com/kensonjohnson/roguelike-game-go/assets" - "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/internal/config" - "github.com/yohamta/donburi" -) - -func CreateNewUI(world donburi.World) { - entity := world.Create( - tags.UITag, - component.UI, - ) - entry := world.Entry(entity) - - ui := component.UIData{ - MessageBox: component.UserMessageBoxData{}, - PlayerHUD: component.PlayerHUDData{}, - } - - // Configure message box - messageBoxTopPixel := (config.ScreenHeight - config.UIHeight) * config.TileHeight - ui.MessageBox.Position = component.PositionData{ - X: 0, - Y: messageBoxTopPixel, - } - ui.MessageBox.FontX = config.FontSize - ui.MessageBox.FontY = messageBoxTopPixel + 10 - ui.MessageBox.Sprite = assets.UIPanel - - // Configure player HUD - playerHUDTopPixel := (config.ScreenHeight * config.TileHeight) - 220 - playerEntry := tags.PlayerTag.MustFirst(world) - ui.PlayerHUD.Position = component.PositionData{ - X: config.ScreenWidth * config.TileWidth / 2, - Y: playerHUDTopPixel, - } - ui.PlayerHUD.FontX = ui.PlayerHUD.Position.X + config.FontSize - ui.PlayerHUD.FontY = messageBoxTopPixel + 12 - ui.PlayerHUD.Health = component.Health.Get(playerEntry) - ui.PlayerHUD.Attack = component.Attack.Get(playerEntry) - ui.PlayerHUD.Defense = component.Defense.Get(playerEntry) - ui.PlayerHUD.Sprite = assets.UIPanelWithMinimap - - component.UI.SetValue(entry, ui) -} diff --git a/archetype/valuable.go b/archetype/valuable.go index ff25596..460ba88 100644 --- a/archetype/valuable.go +++ b/archetype/valuable.go @@ -21,6 +21,10 @@ func CreateNewValuable(world donburi.World, valuableData *items.ValuableData) *d return entry } +func IsValuable(entry *donburi.Entry) bool { + return entry.HasComponent(tags.ValuableTag) +} + func CreateCoins(world donburi.World, valuableData *items.ValuableData) *donburi.Entry { entry := CreateNewValuable(world, valuableData) diff --git a/assets/efs.go b/assets/efs.go index 3a818ab..8c0e644 100644 --- a/assets/efs.go +++ b/assets/efs.go @@ -25,13 +25,13 @@ var ( ChestClosed *ebiten.Image // UI - UIPanel *ebiten.Image - UIPanelWithMinimap *ebiten.Image - UICorner *ebiten.Image KenneyMiniFont *text.GoTextFace KenneyMiniSquaredFont *text.GoTextFace KenneyPixelFont *text.GoTextFace + // Icons + Heart *ebiten.Image + // Characters Player *ebiten.Image Skelly *ebiten.Image @@ -151,10 +151,6 @@ func init() { /*----------------------- ---------- UI ----------- -----------------------*/ - UIPanel = mustLoadImage("images/ui/UIPanel.png") - UIPanelWithMinimap = mustLoadImage("images/ui/UIPanelWithMinimap.png") - UICorner = mustLoadImage("images/ui/UICorner.png") - kenneyMiniFontBytes, err := assetsFS.ReadFile("fonts/KenneyMini.ttf") if err != nil { log.Panic(err) @@ -173,6 +169,12 @@ func init() { // For some reason, the KenneyPixel shows up as half the size of the other fonts. KenneyPixelFont.Size = float64(config.FontSize) * 1.5 + /*----------------------- + --------- Fonts --------- + -----------------------*/ + + Heart = mustLoadImage("images/icons/heart.png") + /*----------------------- ------ Characters ------- -----------------------*/ diff --git a/assets/images/icons/heart.png b/assets/images/icons/heart.png new file mode 100644 index 0000000..78a4d00 Binary files /dev/null and b/assets/images/icons/heart.png differ diff --git a/assets/images/ui/UICorner.png b/assets/images/ui/UICorner.png deleted file mode 100644 index 966235e..0000000 Binary files a/assets/images/ui/UICorner.png and /dev/null differ diff --git a/assets/images/ui/UIPanel.png b/assets/images/ui/UIPanel.png deleted file mode 100644 index dfaed01..0000000 Binary files a/assets/images/ui/UIPanel.png and /dev/null differ diff --git a/assets/images/ui/UIPanelWithMinimap.png b/assets/images/ui/UIPanelWithMinimap.png deleted file mode 100644 index d3a3506..0000000 Binary files a/assets/images/ui/UIPanelWithMinimap.png and /dev/null differ diff --git a/component/description.go b/component/description.go new file mode 100644 index 0000000..be90ee8 --- /dev/null +++ b/component/description.go @@ -0,0 +1,9 @@ +package component + +import "github.com/yohamta/donburi" + +type DescriptionData struct { + Value string +} + +var Description = donburi.NewComponentType[DescriptionData]() diff --git a/component/inventory.go b/component/inventory.go index 02804d3..e2b6cc6 100644 --- a/component/inventory.go +++ b/component/inventory.go @@ -2,6 +2,7 @@ package component import ( "errors" + "iter" "log" "github.com/kensonjohnson/roguelike-game-go/archetype/tags" @@ -9,7 +10,7 @@ import ( ) type InventoryData struct { - Items []*donburi.Entry + items []*donburi.Entry capacity int holding int } @@ -18,7 +19,7 @@ var Inventory = donburi.NewComponentType[InventoryData]() func NewInventory(capacity int) InventoryData { return InventoryData{ - Items: make([]*donburi.Entry, capacity), + items: make([]*donburi.Entry, capacity), capacity: capacity, holding: 0, } @@ -31,8 +32,8 @@ func (i *InventoryData) GetCapacityInfo() (holding, capacity int) { func (i *InventoryData) IncreaseCapacityByAmount(amount int) { i.capacity += amount newStorage := make([]*donburi.Entry, i.capacity) - copy(newStorage, i.Items) - i.Items = newStorage + copy(newStorage, i.items) + i.items = newStorage } func (i *InventoryData) DecreaseCapacityByAmount(amount int) error { @@ -43,6 +44,13 @@ func (i *InventoryData) DecreaseCapacityByAmount(amount int) error { return nil } +func (i *InventoryData) GetItem(index int) (*donburi.Entry, error) { + if index < 0 || index >= i.capacity { + return nil, errors.New("Index out of range") + } + return i.items[index], nil +} + func (i *InventoryData) AddItem(item *donburi.Entry) error { if i.holding >= i.capacity { return errors.New("inventory full") @@ -54,7 +62,7 @@ func (i *InventoryData) AddItem(item *donburi.Entry) error { var targetIndex = -1 - for index, element := range i.Items { + for index, element := range i.items { if element == nil { targetIndex = index break @@ -65,14 +73,24 @@ func (i *InventoryData) AddItem(item *donburi.Entry) error { return errors.New("failed to find empty index for item") } - i.Items[targetIndex] = item + i.items[targetIndex] = item return nil } func (i *InventoryData) RemoveItem(index int) error { - if index >= i.capacity { + if index < 0 || index >= i.capacity { log.Panic("index out of range in RemoveItem. Recieved: ", index) } - i.Items[index] = nil + i.items[index] = nil return nil } + +func (i *InventoryData) Iter() iter.Seq2[int, *donburi.Entry] { + return func(yield func(int, *donburi.Entry) bool) { + for index, entry := range i.items { + if !yield(index, entry) { + return + } + } + } +} diff --git a/component/ui.go b/component/ui.go deleted file mode 100644 index 64f7364..0000000 --- a/component/ui.go +++ /dev/null @@ -1,30 +0,0 @@ -package component - -import ( - "github.com/hajimehoshi/ebiten/v2" - "github.com/yohamta/donburi" -) - -type UIData struct { - MessageBox UserMessageBoxData - PlayerHUD PlayerHUDData -} - -type UserMessageBoxData struct { - Position PositionData - FontX int - FontY int - Sprite *ebiten.Image -} - -type PlayerHUDData struct { - Position PositionData - FontX int - FontY int - Health *HealthData - Attack *AttackData - Defense *DefenseData - Sprite *ebiten.Image -} - -var UI = donburi.NewComponentType[UIData]() diff --git a/component/wallet.go b/component/wallet.go index 94e5cee..f613b7c 100644 --- a/component/wallet.go +++ b/component/wallet.go @@ -10,6 +10,9 @@ var Wallet = donburi.NewComponentType[WalletData]() func (w *WalletData) AddAmount(amount int) { w.Amount += amount + if w.Amount > 99999 { + w.Amount = 99999 + } } func (w *WalletData) SubtractAmount(amount int) { diff --git a/internal/colors/colors.go b/internal/colors/colors.go index 0e6b017..b580efa 100644 --- a/internal/colors/colors.go +++ b/internal/colors/colors.go @@ -3,15 +3,13 @@ package colors import "image/color" var ( - Transparent = color.RGBA{} - Black = color.RGBA{0, 0, 0, 255} - White = color.RGBA{255, 255, 255, 255} Blue = color.RGBA{0, 0, 255, 255} CornflowerBlue = color.RGBA{100, 149, 237, 255} DeepSkyBlue = color.RGBA{0, 191, 255, 255} Green = color.RGBA{0, 128, 0, 255} Lime = color.RGBA{0, 255, 0, 255} Brown = color.RGBA{165, 42, 42, 255} + Smudgy = color.RGBA{71, 45, 60, 255} Peru = color.RGBA{205, 133, 63, 255} Gray = color.RGBA{128, 128, 128, 255} LightGray = color.RGBA{211, 211, 211, 255} diff --git a/internal/engine/debug.go b/internal/engine/debug.go new file mode 100644 index 0000000..04924e5 --- /dev/null +++ b/internal/engine/debug.go @@ -0,0 +1,21 @@ +package engine + +type debug struct { + on bool +} + +var Debug = &debug{ + on: false, +} + +func (d *debug) TurnOn() { + d.on = true +} + +func (d *debug) TurnOff() { + d.on = false +} + +func (d *debug) On() bool { + return d.on +} diff --git a/internal/engine/random.go b/internal/engine/helpers.go similarity index 60% rename from internal/engine/random.go rename to internal/engine/helpers.go index 772a9bc..30f8058 100644 --- a/internal/engine/random.go +++ b/internal/engine/helpers.go @@ -2,6 +2,7 @@ package engine import ( "crypto/rand" + "math" "math/big" ) @@ -17,7 +18,17 @@ func GetDiceRoll(num int) int { return int(x.Int64()) + 1 } -// Returns a number between low and high, inclusive. +// Returns a number between low and high, inclusive func GetRandomBetween(low, high int) int { return GetDiceRoll(high-low) + low } + +// Converts degrees to radians +func DegreesToRadians(degrees int) float64 { + return float64(degrees) * math.Pi / 180 +} + +// Normalize value between 0 and 1 +func Normalize(value, max int) float32 { + return 1 - float32(value)/float32(max) +} diff --git a/internal/engine/shapes/boxes.go b/internal/engine/shapes/boxes.go new file mode 100644 index 0000000..10e0620 --- /dev/null +++ b/internal/engine/shapes/boxes.go @@ -0,0 +1,209 @@ +package shapes + +import ( + "image/color" + "math" + + "github.com/hajimehoshi/ebiten/v2" +) + +type cornerVariant int +type cornerShape [][]int8 + +const ( + BasicCorner cornerVariant = iota + PointedCorner + PointedCornerTransparent + SimpleCorner + SimpleCornerTransparent + SmallPointedCorner + SmallPointedCornerTransparent + FancyItemCorner +) + +func MakeBox(w, h, scale int, border, fill color.Color, variant ...cornerVariant) *ebiten.Image { + + image := ebiten.NewImage(w, h) + corner := makeCornerImage(scale, border, fill, variant...) + size := corner.Bounds().Size() + options := &ebiten.DrawImageOptions{} + + // NW + image.DrawImage(corner, options) + + // NE + options.GeoM.Rotate(degreesToRadians(90)) + options.GeoM.Translate(float64(w), 0) + image.DrawImage(corner, options) + + // SE + options.GeoM.Reset() + options.GeoM.Rotate(degreesToRadians(180)) + options.GeoM.Translate(float64(w), float64(h)) + image.DrawImage(corner, options) + + // SW + options.GeoM.Reset() + options.GeoM.Rotate(degreesToRadians(270)) + options.GeoM.Translate(0, float64(h)) + image.DrawImage(corner, options) + + // Draw top and bottom lines, plus fill + line := ebiten.NewImage(w-(size.X*2), 1) + line.Fill(border) + for i := 0; i < size.Y; i++ { + if i == scale { + line.Fill(fill) + } + options.GeoM.Reset() + options.GeoM.Translate(float64(size.X), float64(i)) + image.DrawImage(line, options) + options.GeoM.Translate(0, float64(h-(i*2)-1)) + image.DrawImage(line, options) + } + + // Draw vertical lines and fill + line = ebiten.NewImage(w, h-(size.Y*2)) + line.Fill(fill) + ends := ebiten.NewImage(scale, h-(size.Y*2)) + ends.Fill(border) + options.GeoM.Reset() + options.GeoM.Translate(float64(w-scale), 0) + line.DrawImage(ends, options) + options.GeoM.Reset() + line.DrawImage(ends, options) + options.GeoM.Translate(0, float64(size.Y)) + image.DrawImage(line, options) + + return image +} + +func makeCornerImage(scale int, border, fill color.Color, variant ...cornerVariant) *ebiten.Image { + var shape cornerShape + if len(variant) <= 0 { + shape = pointedCorner + } else { + switch variant[0] { + case BasicCorner: + shape = basicCorner + case PointedCorner: + shape = pointedCorner + case PointedCornerTransparent: + shape = pointedCornerTransparent + case SimpleCorner: + shape = simpleCorner + case SimpleCornerTransparent: + shape = simpleCornerTransparent + case SmallPointedCorner: + shape = smallPointedCorner + case SmallPointedCornerTransparent: + shape = smallPointedCornerTransparent + case FancyItemCorner: + shape = fancyItemCorner + default: + shape = basicCorner + } + } + + width := len(shape[0]) * scale + height := len(shape) * scale + + image := ebiten.NewImage(width, height) + block := ebiten.NewImage(scale, scale) + options := &ebiten.DrawImageOptions{} + + for y, row := range shape { + for x, value := range row { + options.GeoM.Reset() + options.GeoM.Translate(float64(x*scale), float64(y*scale)) + switch value { + case -1: + continue + case 0: + block.Fill(border) + case 1: + block.Fill(fill) + } + image.DrawImage(block, options) + } + } + + return image +} + +func degreesToRadians(degrees int) float64 { + return float64(degrees) * math.Pi / 180 +} + +var basicCorner cornerShape = [][]int8{ + {0}, +} + +var pointedCorner cornerShape = [][]int8{ + {0, 0, 0, 0, 0, -1, 0, 0}, + {0, 1, 1, 1, 0, -1, 0, 1}, + {0, 1, 1, 1, 0, 0, 0, 1}, + {0, 1, 1, 1, 0, 1, 1, 1}, + {0, 0, 0, 0, 0, 1, 1, 1}, + {-1, -1, 0, 1, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 1, 1, 1}, + {0, 1, 1, 1, 1, 1, 1, 1}, +} + +var pointedCornerTransparent cornerShape = [][]int8{ + {0, 0, 0, 0, 0, -1, 0, 0}, + {0, -1, -1, -1, 0, -1, 0, 1}, + {0, -1, -1, -1, 0, 0, 0, 1}, + {0, -1, -1, -1, 0, 1, 1, 1}, + {0, 0, 0, 0, 0, 1, 1, 1}, + {-1, -1, 0, 1, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 1, 1, 1}, + {0, 1, 1, 1, 1, 1, 1, 1}, +} + +var simpleCorner cornerShape = [][]int8{ + {0, 0, 0, 0, 0, 0}, + {0, 1, 1, 1, 1, 0}, + {0, 1, 0, 0, 0, 0}, + {0, 1, 0, 1, 1, 1}, + {0, 1, 0, 1, 1, 1}, + {0, 0, 0, 1, 1, 1}, +} + +var simpleCornerTransparent cornerShape = [][]int8{ + {0, 0, 0, 0, 0, 0}, + {0, -1, -1, -1, -1, 0}, + {0, -1, 0, 0, 0, 0}, + {0, -1, 0, 1, 1, 1}, + {0, -1, 0, 1, 1, 1}, + {0, 0, 0, 1, 1, 1}, +} + +var smallPointedCorner cornerShape = [][]int8{ + {0, 0, 0, 0, 0, -1, 0}, + {0, 1, 1, 1, 0, -1, 0}, + {0, 1, 0, 0, 0, 0, 0}, + {0, 1, 0, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 1, 1}, + {-1, -1, 0, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 1, 1}, +} + +var smallPointedCornerTransparent cornerShape = [][]int8{ + {0, 0, 0, 0, 0, -1, 0}, + {0, -1, -1, -1, 0, -1, 0}, + {0, -1, 0, 0, 0, 0, 0}, + {0, -1, 0, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 1, 1}, + {-1, -1, 0, 1, 1, 1, 1}, + {0, 0, 0, 1, 1, 1, 1}, +} + +var fancyItemCorner cornerShape = [][]int8{ + {-1, -1, -1, -1, 0, 0}, + {-1, -1, -1, -1, -1, 0}, + {-1, -1, 0, 0, 0, 0}, + {-1, -1, 0, 1, 1, 1}, + {0, -1, 0, 1, 1, 1}, + {0, 0, 0, 1, 1, 1}, +} diff --git a/internal/engine/shapes/dividers.go b/internal/engine/shapes/dividers.go new file mode 100644 index 0000000..26156cc --- /dev/null +++ b/internal/engine/shapes/dividers.go @@ -0,0 +1,119 @@ +package shapes + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/kensonjohnson/roguelike-game-go/internal/engine" +) + +type dividerVariant int +type dividerShape [][]int8 + +const ( + SimpleDivider dividerVariant = iota +) + +func MakeDivider(h, scale int, border, fill color.Color, fade bool, variant ...dividerVariant) *ebiten.Image { + var shape dividerShape + if len(variant) <= 0 { + shape = simpleDivider + } else { + switch variant[0] { + case 0: + shape = simpleDivider + default: + shape = simpleDivider + } + } + + width := len(shape[0]) * scale + + image := ebiten.NewImage(width, h) + + // Draw the fancy bit at the top + block := ebiten.NewImage(scale, scale) + options := &ebiten.DrawImageOptions{} + for y, row := range shape { + for x, value := range row { + options.GeoM.Reset() + options.GeoM.Translate(float64(x*scale), float64(y*scale)) + switch value { + case -1: + continue + case 0: + block.Fill(border) + case 1: + block.Fill(fill) + } + image.DrawImage(block, options) + } + } + + // Create the slice that will fill out the rest of the divider + filler := ebiten.NewImage(width, 1) + line := ebiten.NewImage(scale, 1) + for x, value := range shape[len(shape)-1] { + options.GeoM.Reset() + options.GeoM.Translate(float64(x*scale), 0) + switch value { + case -1: + continue + case 0: + line.Fill(border) + case 1: + line.Fill(fill) + } + filler.DrawImage(line, options) + } + + if fade { + // Fill in the divider, up to the two thirds point + twoThirds := (h / 3) * 2 + for i := len(shape) * scale; i < twoThirds; i++ { + options.GeoM.Reset() + options.GeoM.Translate(0, float64(i)) + image.DrawImage(filler, options) + } + + // Fill in the divider, fading to full transparency + for i := twoThirds; i < h; i++ { + options.GeoM.Reset() + options.GeoM.Translate(0, float64(i)) + options.ColorScale.ScaleAlpha(engine.Normalize(i%twoThirds, h-twoThirds)) + image.DrawImage(filler, options) + options.ColorScale.Reset() + } + } else { + // Fill in the divider for remaining height + for i := len(shape) * scale; i < h; i++ { + options.GeoM.Reset() + options.GeoM.Translate(0, float64(i)) + image.DrawImage(filler, options) + } + } + + return image +} + +/* + For any given dividerShape, the final row must be the repeating pattern + that completes the part "under" the design. This last row will be repeated + in MakeDivider to satisfy the height requirement. +*/ + +var simpleDivider dividerShape = [][]int8{ + {-1, -1, 0, 1, 0, -1, -1}, + {-1, -1, 0, 1, 0, -1, -1}, + {0, 0, 0, 1, 0, 0, 0}, + {0, 1, 1, 1, 1, 1, 0}, + {0, 1, 1, 1, 1, 1, 0}, + {0, 1, 1, 1, 1, 1, 0}, + {0, 0, 0, 1, 0, 0, 0}, + {-1, -1, 0, 1, 0, -1, -1}, + {-1, 0, 0, 1, 0, 0, -1}, + {-1, 0, 1, 1, 1, 0, -1}, + {-1, 0, 1, 1, 1, 0, -1}, + {-1, 0, 0, 1, 0, 0, -1}, + {-1, -1, 0, 1, 0, -1, -1}, +} diff --git a/items/items.go b/items/items.go index 2551650..3057984 100644 --- a/items/items.go +++ b/items/items.go @@ -3,6 +3,7 @@ package items import "github.com/hajimehoshi/ebiten/v2" type ItemData struct { - Name string - Sprite *ebiten.Image + Name string + Sprite *ebiten.Image + Description string } diff --git a/items/valuables.go b/items/valuables.go index 4b8d832..a78d4fa 100644 --- a/items/valuables.go +++ b/items/valuables.go @@ -17,8 +17,9 @@ type valuables struct { var Valuables valuables = valuables{ Alcohol: &ValuableData{ ItemData: ItemData{ - Name: "Alcohol", - Sprite: assets.WorldAlcohol, + Name: "Alcohol", + Sprite: assets.WorldAlcohol, + Description: "A bottle of alcohol. Should be well aged by now.", }, Value: 20, }, diff --git a/main.go b/main.go index 90794ca..28269c0 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/kensonjohnson/roguelike-game-go/assets" "github.com/kensonjohnson/roguelike-game-go/internal/config" - "github.com/kensonjohnson/roguelike-game-go/system" + "github.com/kensonjohnson/roguelike-game-go/internal/engine" "github.com/kensonjohnson/roguelike-game-go/system/scene" ) @@ -49,7 +49,7 @@ func main() { ebiten.SetWindowTitle("Roguelike") if DebugOn != nil && *DebugOn { ebiten.SetVsyncEnabled(false) - system.Debug.On = true + engine.Debug.TurnOn() slog.SetLogLoggerLevel(slog.LevelDebug) } diff --git a/system/debug.go b/system/debug.go index bf44a9b..da504e7 100644 --- a/system/debug.go +++ b/system/debug.go @@ -5,37 +5,92 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" - "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/yohamta/donburi/ecs" donburiDebug "github.com/yohamta/donburi/features/debug" ) type debug struct { - On bool + frameCount int + totalEntities int + monsterCount int + itemCount int + pickupCount int + miscCount int } var Debug = &debug{ - On: false, + frameCount: 0, + totalEntities: 0, + monsterCount: 0, + itemCount: 0, + pickupCount: 0, + miscCount: 0, } func (d *debug) Draw(ecs *ecs.ECS, screen *ebiten.Image) { - width := config.ScreenWidth * config.TileWidth - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("TPS: %v\nFPS: %v", int(ebiten.ActualTPS()), int(ebiten.ActualFPS())), width-60, 10) + const spacing = 14 + offset := 8 + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("TPS: %v", int(ebiten.ActualTPS())), 8, offset) - archetypes := donburiDebug.GetEntityCounts(ecs.World) - allEntities := 0 - for _, system := range archetypes { - allEntities += system.Count - } + offset += spacing + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("FPS: %v", int(ebiten.ActualFPS())), 8, offset) + + offset += spacing + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Entities: %v", d.totalEntities), 8, offset) + + offset += spacing + ebitenutil.DebugPrintAt( + screen, + fmt.Sprintf("%v %v", "Monster: ", d.monsterCount), + 8, offset, + ) - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Entities: %v", allEntities), 8, 6) + offset += spacing + ebitenutil.DebugPrintAt( + screen, + fmt.Sprintf("%v %v", "Total Items: ", d.itemCount), + 8, offset, + ) - for i, system := range archetypes { - ebitenutil.DebugPrintAt( - screen, - fmt.Sprintf("%v Entities: %v", system.Archetype.Layout(), system.Count), - 8, 20+(i*14), - ) + offset += spacing + ebitenutil.DebugPrintAt( + screen, + fmt.Sprintf("%v %v", "Pickups: ", d.pickupCount), + 8, offset, + ) + + offset += spacing + ebitenutil.DebugPrintAt( + screen, + fmt.Sprintf("%v %v", "Uncategorized: ", d.miscCount), + 8, offset, + ) + + d.frameCount++ + if d.frameCount < 60 { + return } + d.frameCount = 0 + // Recalculate all numbers + d.totalEntities = ecs.World.Len() + d.monsterCount = 0 + d.itemCount = 0 + d.pickupCount = 0 + d.miscCount = 0 + archetypes := donburiDebug.GetEntityCounts(ecs.World) + for _, arch := range archetypes { + // List out the entities that you care to record + if arch.Archetype.Layout().HasComponent(tags.MonsterTag) { + d.monsterCount += arch.Count + } else if arch.Archetype.Layout().HasComponent(tags.PickupTag) { + d.pickupCount += arch.Count + d.itemCount += arch.Count + } else if arch.Archetype.Layout().HasComponent(tags.ItemTag) { + d.itemCount += arch.Count + } else { + d.miscCount += arch.Count + } + } } diff --git a/system/inventory.go b/system/inventory.go index 71db103..10f27d8 100644 --- a/system/inventory.go +++ b/system/inventory.go @@ -1,69 +1,253 @@ package system import ( + "fmt" "image/color" + "log" "log/slog" - "math" + "strings" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/text/v2" + "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" + "github.com/kensonjohnson/roguelike-game-go/assets" + "github.com/kensonjohnson/roguelike-game-go/component" "github.com/kensonjohnson/roguelike-game-go/internal/colors" "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/kensonjohnson/roguelike-game-go/internal/engine/shapes" "github.com/yohamta/donburi/ecs" ) +// How long until a pressed key registers a new event +const keyDelay = 10 + +// UI setting +const inset = 20 +const boxSize = 48 + 18 // (item sprite * scale) + (border size * 2) +const spacing = 10 +const totalBoxSpace = boxSize + spacing +const rows = 4 +const columns = 6 +const contextWindowWidth = 140 +const contextWindowHeight = 200 + type inventoryUi struct { - open bool + open bool + background *ebiten.Image + posX, posY int + selector *ebiten.Image + selectorX, selectorY int + keyDelayCount int + inContextMenu bool + contextWindow *ebiten.Image + contextFont *text.GoTextFace + contextWindowSelection contextSelection + inConfirmAction bool + confirmActionWindow *ebiten.Image + confirmAction bool + inInfoWindow bool + infoWindow *ebiten.Image + infoWindowText *infoWindowText +} + +var InventoryUI = inventoryUi{ + open: false, + background: buildInventorySprite(), + posX: 15 * config.TileWidth, + posY: (((config.ScreenHeight - config.UIHeight - 2) * config.TileHeight) - + (inset + (totalBoxSpace * rows) - spacing + inset)), + selector: makeItemBox(color.White, color.Transparent), + selectorX: 0, + selectorY: 0, + keyDelayCount: 0, + inContextMenu: false, + contextWindow: shapes.MakeBox( + contextWindowWidth, contextWindowHeight, 4, + colors.Peru, colors.LightGray, + shapes.BasicCorner, + ), + contextFont: &text.GoTextFace{ + Source: assets.KenneyMiniSquaredFont.Source, + Size: assets.KenneyMiniSquaredFont.Size * 1.5, + }, + contextWindowSelection: back, + inConfirmAction: false, + confirmActionWindow: shapes.MakeBox( + 130, 40, 4, + colors.Peru, colors.LightGray, + shapes.BasicCorner, + ), + confirmAction: false, + inInfoWindow: false, + infoWindow: shapes.MakeBox( + 200, contextWindowHeight, 4, + colors.Peru, colors.LightGray, + shapes.BasicCorner, + ), + infoWindowText: &infoWindowText{Name: "Info", Text: "No Description"}, } -var InventoryUI = inventoryUi{open: false} +type contextSelection int + +const ( + discard contextSelection = iota + info + use + back +) + +type infoWindowText struct { + Name string + Text string +} func (i *inventoryUi) Update(ecs *ecs.ECS) { if !i.open { return } + if i.keyDelayCount > 0 { + i.keyDelayCount-- + return + } - if inpututil.IsKeyJustPressed(ebiten.KeyI) || inpututil.IsKeyJustPressed(ebiten.KeyEscape) { - slog.Debug("Close Inventory") - Turn.TurnState = PlayerTurn - i.open = false + if i.inContextMenu { + i.handleContextWindow(ecs) + } else { + i.handleSelectionWindow() } + } func (i *inventoryUi) Draw(ecs *ecs.ECS, screen *ebiten.Image) { if !i.open { return } - image := makeBox( - (15)*config.TileWidth, - (8)*config.TileHeight, - colors.SlateGray, - colors.Black, - ) + options := &ebiten.DrawImageOptions{} - options.GeoM.Reset() - options.GeoM.Translate(float64(5*config.TileWidth), float64(5*config.TileHeight)) - screen.DrawImage(image, options) - - image = makeBox( - (15)*config.TileWidth, - (5)*config.TileHeight, - colors.White, - colors.Transparent, - ) - options.GeoM.Reset() - options.GeoM.Translate(float64(5*config.TileWidth), float64(15*config.TileHeight)) - screen.DrawImage(image, options) - image = makeBox( - (15)*config.TileWidth, - (8)*config.TileHeight, - colors.Peru, - colors.SlateGray, + // Draw the box + options.GeoM.Translate( + float64(i.posX), + float64(i.posY), ) + screen.DrawImage(i.background, options) + + // Draw each item + playerEntry := tags.PlayerTag.MustFirst(ecs.World) + playerInventory := component.Inventory.Get(playerEntry) + for index, entry := range playerInventory.Iter() { + if entry == nil { + continue + } + sprite := component.Sprite.Get(entry).Image + options.GeoM.Reset() + options.GeoM.Scale(3, 3) + options.GeoM.Translate( + float64(i.posX+((index%columns)*totalBoxSpace)+inset+9), + float64(i.posY+((index/columns)*totalBoxSpace)+inset+9), + ) + screen.DrawImage(sprite, options) + } + + // Draw selector options.GeoM.Reset() - options.GeoM.Translate(float64(5*config.TileWidth), float64(25*config.TileHeight)) - screen.DrawImage(image, options) + options.GeoM.Translate( + float64(i.posX+(i.selectorX*totalBoxSpace)+inset), + float64(i.posY+(i.selectorY*totalBoxSpace)+inset), + ) + screen.DrawImage(i.selector, options) + + // Draw context window + if !i.inContextMenu { + return + } + + // The position is already on the top left corner of the selection box; + // We just need to move up by the height of the context window. + options.GeoM.Translate(0, -float64(i.contextWindow.Bounds().Dy())) + screen.DrawImage(i.contextWindow, options) + + i.drawContextWindowOptions(screen) +} + +func (i *inventoryUi) handleSelectionWindow() { + + if inpututil.IsKeyJustPressed(ebiten.KeyI) || + inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + slog.Debug("Close Inventory") + Turn.TurnState = PlayerTurn + i.open = false + return + } + + if inpututil.IsKeyJustPressed(ebiten.KeyEnter) { + i.contextWindowSelection = back + i.inContextMenu = true + slog.Debug("Open context") + return + } + + moveX := 0 + moveY := 0 + + if ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeyUp) { + slog.Debug("Pressing Up in Inventory!") + moveY = -1 + } + + if ebiten.IsKeyPressed(ebiten.KeyS) || ebiten.IsKeyPressed(ebiten.KeyDown) { + slog.Debug("Pressing Down in Inventory!") + moveY = 1 + } + + if ebiten.IsKeyPressed(ebiten.KeyA) || ebiten.IsKeyPressed(ebiten.KeyLeft) { + slog.Debug("Pressing Left in Inventory!") + moveX = -1 + } + + if ebiten.IsKeyPressed(ebiten.KeyD) || ebiten.IsKeyPressed(ebiten.KeyRight) { + slog.Debug("Pressing Right in Inventory!") + moveX = 1 + } + + if moveX != 0 || moveY != 0 { + i.keyDelayCount = keyDelay + } + + i.selectorX = (i.selectorX + moveX + columns) % columns + i.selectorY = (i.selectorY + moveY + rows) % rows +} + +func (i *inventoryUi) handleContextWindow(ecs *ecs.ECS) { + if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + slog.Debug("Context window closed") + i.contextWindowSelection = back + i.inConfirmAction = false + i.inInfoWindow = false + i.inContextMenu = false + return + } + + if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || + i.inConfirmAction || i.inInfoWindow { + i.handleSelectionMade(ecs) + return + } + + // We use the `back` constant because it is the last in the enum. The magic + // +2 in the 'down' keypress is because the enum is zero indexed. Instead of + // writing `back - 1 + 1` and `back + 1 + 1`, we simplify. + if inpututil.IsKeyJustPressed(ebiten.KeyW) || + inpututil.IsKeyJustPressed(ebiten.KeyUp) { + i.contextWindowSelection = (i.contextWindowSelection + back) % (back + 1) + } + + if inpututil.IsKeyJustPressed(ebiten.KeyS) || + inpututil.IsKeyJustPressed(ebiten.KeyDown) { + i.contextWindowSelection = (i.contextWindowSelection + back + 2) % (back + 1) + } } func (i *inventoryUi) Open() { @@ -73,100 +257,229 @@ func (i *inventoryUi) Close() { i.open = false } -// TODO: Refactor to a proper place, probably internal -// TODO: Allow for corner variants -// w and h are pixel values -func makeBox(w, h int, border, fill color.RGBA) *ebiten.Image { +func buildInventorySprite() *ebiten.Image { + + image := shapes.MakeBox( + inset+(totalBoxSpace*columns)-spacing+inset, + inset+(totalBoxSpace*rows)-spacing+inset, + 4, + colors.SlateGray, + color.Black, + shapes.SmallPointedCorner, + ) - image := ebiten.NewImage(w, h) - corner := makeCornerImage(border, fill) - size := corner.Bounds().Size() + itemBox := makeItemBox(colors.Gray, colors.Smudgy) options := &ebiten.DrawImageOptions{} + for y := 0; y < rows; y++ { + options.GeoM.Translate(float64(inset), float64(inset+(y*totalBoxSpace))) + for x := 0; x < columns; x++ { + image.DrawImage(itemBox, options) + options.GeoM.Translate(totalBoxSpace, 0) + } + options.GeoM.Reset() + } - // NW - image.DrawImage(corner, options) + return image +} - // NE - options.GeoM.Rotate(degreesToRadians(90)) - options.GeoM.Translate(float64(w), 0) - image.DrawImage(corner, options) +func makeItemBox(border, fill color.Color) *ebiten.Image { + return shapes.MakeBox( + boxSize, boxSize, 3, + border, fill, + shapes.SimpleCorner, + ) +} - // SE - options.GeoM.Reset() - options.GeoM.Rotate(degreesToRadians(180)) - options.GeoM.Translate(float64(w), float64(h)) - image.DrawImage(corner, options) +func (i *inventoryUi) drawContextWindowOptions(screen *ebiten.Image) { + x := i.posX + (i.selectorX * totalBoxSpace) + inset + y := i.posY + (i.selectorY * totalBoxSpace) + inset - // SW - options.GeoM.Reset() - options.GeoM.Rotate(degreesToRadians(270)) - options.GeoM.Translate(0, float64(h)) - image.DrawImage(corner, options) - - // Draw top and bottom lines, plus fill - line := ebiten.NewImage(w-(size.X*2), 1) - line.Fill(border) - for i := 0; i < size.Y; i++ { - if i == 4 { - line.Fill(fill) - } - options.GeoM.Reset() - options.GeoM.Translate(float64(size.X), float64(i)) - image.DrawImage(line, options) - options.GeoM.Translate(0, float64(h-(i*2)-1)) - image.DrawImage(line, options) + lineHeight := i.contextWindow.Bounds().Dy() / 4 + + const inset = 10 + + options := &text.DrawOptions{} + options.GeoM.Translate( + float64(x+inset), + float64(y-i.contextWindow.Bounds().Dy()), + ) + + i.drawContextOption(screen, "Discard", discard, options) + + options.GeoM.Translate(0, float64(lineHeight)) + i.drawContextOption(screen, "Info", info, options) + + options.GeoM.Translate(0, float64(lineHeight)) + i.drawContextOption(screen, "Equip", use, options) + + options.GeoM.Translate(0, float64(lineHeight)) + i.drawContextOption(screen, "Back", back, options) + + if i.inConfirmAction { + i.drawConfirmWindow( + screen, + x+i.contextWindow.Bounds().Dx(), + y-i.contextWindow.Bounds().Dy(), + ) } - // Draw vertical lines and fill - line = ebiten.NewImage(w, h-(size.Y*2)) - line.Fill(fill) - ends := ebiten.NewImage(4, h-(size.Y*2)) - ends.Fill(border) - options.GeoM.Reset() - options.GeoM.Translate(float64(w-4), 0) - line.DrawImage(ends, options) - options.GeoM.Reset() - line.DrawImage(ends, options) - options.GeoM.Translate(0, float64(size.Y)) - image.DrawImage(line, options) + if i.inInfoWindow { + i.drawInfoWindow( + screen, + x+i.contextWindow.Bounds().Dx(), + y-i.contextWindow.Bounds().Dy(), + ) + } +} - return image +func (i *inventoryUi) drawContextOption( + screen *ebiten.Image, + label string, + selection contextSelection, + options *text.DrawOptions, +) { + if i.contextWindowSelection == selection { + options.ColorScale.ScaleWithColor(colors.DarkGray) + } else { + options.ColorScale.ScaleWithColor(color.Black) + } + text.Draw(screen, label, i.contextFont, options) + options.ColorScale.Reset() } -func makeCornerImage(border, fill color.RGBA) *ebiten.Image { - image := ebiten.NewImage(32, 32) - block := ebiten.NewImage(4, 4) - options := &ebiten.DrawImageOptions{} - for y, row := range cornerShape { - for x, value := range row { - options.GeoM.Reset() - options.GeoM.Translate(float64(x*4), float64(y*4)) - switch value { - case -1: - continue - case 0: - block.Fill(border) - case 1: - block.Fill(fill) +func (i *inventoryUi) handleSelectionMade(ecs *ecs.ECS) { + + switch i.contextWindowSelection { + case discard: + if i.inConfirmAction { + + if inpututil.IsKeyJustPressed(ebiten.KeyA) || + inpututil.IsKeyJustPressed(ebiten.KeyLeft) { + i.confirmAction = false + } + if inpututil.IsKeyJustPressed(ebiten.KeyD) || + inpututil.IsKeyJustPressed(ebiten.KeyRight) { + i.confirmAction = true + } + if inpututil.IsKeyJustPressed(ebiten.KeyEnter) && + i.confirmAction { + + slog.Debug("Discard item") + playerEntry := tags.PlayerTag.MustFirst(ecs.World) + playerInventory := component.Inventory.Get(playerEntry) + index := i.selectorX + (i.selectorY * columns) + playerInventory.RemoveItem(index) + // TODO: Send event message to ui + i.inConfirmAction = false + i.inContextMenu = false } - image.DrawImage(block, options) + if inpututil.IsKeyJustPressed(ebiten.KeyEnter) && + !i.confirmAction { + i.inConfirmAction = false + } + } else { + i.inConfirmAction = true + i.confirmAction = false + return } - } - return image + case info: + if i.inInfoWindow { + if inpututil.IsKeyJustPressed(ebiten.KeyEnter) { + i.inInfoWindow = false + } + } else { + slog.Debug("Item info") + playerEntry := tags.PlayerTag.MustFirst(ecs.World) + playerInventory := component.Inventory.Get(playerEntry) + itemEntry, err := playerInventory.GetItem(i.selectorX + (i.selectorY * columns)) + if err != nil { + log.Panic(err) + } + i.infoWindowText.Name = component.Name.Get(itemEntry).Value + var description = "No description" + if archetype.IsConsumable(itemEntry) { + value := component.Heal.Get(itemEntry).HealAmount + description = fmt.Sprintf("Heals for %v", value) + } + if archetype.IsValuable(itemEntry) { + itemDescription := component.Description.Get(itemEntry) + value := component.Value.Get(itemEntry).Amount + description = fmt.Sprintf("%v\nWorth %v gold", itemDescription.Value, value) + } + i.infoWindowText.Text = description + i.inInfoWindow = true + } + + case use: + slog.Debug("Item action") + + case back: + slog.Debug("Close context window") + i.inContextMenu = false + } } -func degreesToRadians(degrees int) float64 { - return float64(degrees) * math.Pi / 180 +func (i *inventoryUi) drawConfirmWindow(screen *ebiten.Image, x, y int) { + options := &ebiten.DrawImageOptions{} + options.GeoM.Translate(float64(x), float64(y)) + screen.DrawImage(i.confirmActionWindow, options) + + const inset = 10 + + textOptions := &text.DrawOptions{} + textOptions.GeoM.Translate(float64(x+inset), float64(y)) + if !i.confirmAction { + textOptions.ColorScale.ScaleWithColor(colors.DarkGray) + } else { + textOptions.ColorScale.ScaleWithColor(color.Black) + } + text.Draw(screen, "No", i.contextFont, textOptions) + textOptions.ColorScale.Reset() + + textOptions.GeoM.Translate(50.0, 0) + if i.confirmAction { + textOptions.ColorScale.ScaleWithColor(colors.DarkGray) + } else { + textOptions.ColorScale.ScaleWithColor(color.Black) + } + + text.Draw(screen, "Yes", i.contextFont, textOptions) + } -var cornerShape = [][]int8{ - {0, 0, 0, 0, 0, -1, 0, 0}, - {0, 1, 1, 1, 0, -1, 0, 1}, - {0, 1, 1, 1, 0, 0, 0, 1}, - {0, 1, 1, 1, 0, 1, 1, 1}, - {0, 0, 0, 0, 0, 1, 1, 1}, - {-1, -1, 0, 1, 1, 1, 1, 1}, - {0, 0, 0, 1, 1, 1, 1, 1}, - {0, 1, 1, 1, 1, 1, 1, 1}, +func (i *inventoryUi) drawInfoWindow(screen *ebiten.Image, x, y int) { + options := &ebiten.DrawImageOptions{} + options.GeoM.Translate(float64(x), float64(y)) + screen.DrawImage(i.infoWindow, options) + + const inset = 10 + textOptions := &text.DrawOptions{} + textOptions.GeoM.Translate(float64(x+inset), float64(y)) + textOptions.ColorScale.ScaleWithColor(color.Black) + textOptions.LineSpacing = 25 + text.Draw(screen, i.infoWindowText.Name, i.contextFont, textOptions) + + // Word wrapping + maxWidth := i.infoWindow.Bounds().Size().X - (inset * 2) + lines := make([]string, 0) + currentLine := "" + fields := strings.Fields(i.infoWindowText.Text) + + for index, str := range fields { + if index == 0 { + currentLine = str + continue + } + if text.Advance(currentLine+" "+str, assets.KenneyMiniSquaredFont) > float64(maxWidth) { + lines = append(lines, currentLine) + currentLine = str + } else { + currentLine += " " + str + } + } + lines = append(lines, currentLine) + + textOptions.GeoM.Translate(0, 40.0) + text.Draw(screen, strings.Join(lines, "\n"), assets.KenneyMiniSquaredFont, textOptions) } diff --git a/system/minimap.go b/system/minimap.go index 332690a..5929a82 100644 --- a/system/minimap.go +++ b/system/minimap.go @@ -9,35 +9,52 @@ import ( "github.com/kensonjohnson/roguelike-game-go/component" "github.com/kensonjohnson/roguelike-game-go/internal/colors" "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/kensonjohnson/roguelike-game-go/internal/engine/shapes" "github.com/yohamta/donburi/ecs" ) -const blipSize = 4 +type minimap struct { + boxSprite *ebiten.Image + blipSize float32 +} + +var Minimap = &minimap{ + boxSprite: shapes.MakeBox( + 260, 170, 4, + colors.Peru, color.Black, + shapes.FancyItemCorner, + ), + blipSize: 3.0, +} -func DrawMinimap(ecs *ecs.ECS, screen *ebiten.Image) { +func (m *minimap) Draw(ecs *ecs.ECS, screen *ebiten.Image) { entry := tags.LevelTag.MustFirst(ecs.World) level := component.Level.Get(entry) - // The values of 330 and 210 are based on the size of the minimap image. - // That image is 340x220 pixels, with a 10 pixel border, and is placed - // in the bottom right corner of the screen. - startingXPixel := (config.ScreenWidth * config.TileWidth) - 330 - startingYPixel := (config.ScreenHeight * config.TileWidth) - 210 + startingXPixel := (config.ScreenWidth * config.TileWidth) - config.TileWidth - 260 + startingYPixel := config.TileHeight + + options := &ebiten.DrawImageOptions{} + options.GeoM.Translate(float64(startingXPixel), float64(startingYPixel)) + screen.DrawImage(m.boxSprite, options) + + startingXPixel += 10 + startingYPixel += 10 // Draw the walls and floors for _, tile := range level.Tiles { - x := startingXPixel + (tile.TileX * blipSize) - y := startingYPixel + (tile.TileY * blipSize) + x := float32(startingXPixel + (tile.TileX * int(m.blipSize))) + y := float32(startingYPixel + (tile.TileY * int(m.blipSize))) if !tile.IsRevealed { continue } if tile.TileType == component.WALL { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.Peru, false) + vector.DrawFilledRect(screen, x, y, m.blipSize, m.blipSize, colors.Peru, false) } else if tile.TileType == component.STAIR_DOWN { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.Lime, false) + vector.DrawFilledRect(screen, x, y, m.blipSize, m.blipSize, colors.Lime, false) } else /* floor */ { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.LightGray, false) + vector.DrawFilledRect(screen, x, y, m.blipSize, m.blipSize, colors.LightGray, false) } } @@ -49,14 +66,14 @@ func DrawMinimap(ecs *ecs.ECS, screen *ebiten.Image) { return } - x := startingXPixel + (position.X * blipSize) - y := startingYPixel + (position.Y * blipSize) + x := float32(startingXPixel + (position.X * int(m.blipSize))) + y := float32(startingYPixel + (position.Y * int(m.blipSize))) if component.Discoverable.Get(entry).SeenByPlayer { if entry.HasComponent(tags.ItemTag) { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.DeepSkyBlue, false) + vector.DrawFilledRect(screen, x, y, m.blipSize, m.blipSize, colors.DeepSkyBlue, false) } else { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.Red, false) + vector.DrawFilledRect(screen, x, y, m.blipSize, m.blipSize, colors.Red, false) } } } @@ -64,7 +81,8 @@ func DrawMinimap(ecs *ecs.ECS, screen *ebiten.Image) { // Draw the player playerEntry := tags.PlayerTag.MustFirst(ecs.World) playerPosition := component.Position.Get(playerEntry) - x := startingXPixel + (playerPosition.X * blipSize) - y := startingYPixel + (playerPosition.Y * blipSize) - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, color.White, false) + x := float32(startingXPixel + (playerPosition.X * int(m.blipSize))) + y := float32(startingYPixel + (playerPosition.Y * int(m.blipSize))) + // TODO: Pick a better color for the player + vector.DrawFilledRect(screen, x, y, m.blipSize, m.blipSize, color.White, false) } diff --git a/system/scene/levelscene.go b/system/scene/levelscene.go index 5ebadd4..bc62f32 100644 --- a/system/scene/levelscene.go +++ b/system/scene/levelscene.go @@ -9,6 +9,7 @@ import ( "github.com/kensonjohnson/roguelike-game-go/component" "github.com/kensonjohnson/roguelike-game-go/event" "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/kensonjohnson/roguelike-game-go/internal/engine" "github.com/kensonjohnson/roguelike-game-go/system" "github.com/kensonjohnson/roguelike-game-go/system/layer" "github.com/yohamta/donburi" @@ -43,10 +44,6 @@ func (ls *LevelScene) Setup(world donburi.World) { levelData := archetype.GenerateLevel(world) - if _, ok := tags.UITag.First(world); !ok { - archetype.CreateNewUI(world) - } - playerEntry := tags.PlayerTag.MustFirst(world) playerPosition := component.Position.Get(playerEntry) startingRoom := levelData.Rooms[0] @@ -77,11 +74,12 @@ func (ls *LevelScene) Teardown() { ls.ready = false slog.Debug("LevelScene teardown") go func() { + tags.LevelTag.MustFirst(ls.ecs.World).Remove() for entry := range tags.MonsterTag.Iter(ls.ecs.World) { - slog.Debug("Removing entry.", "entry", entry.String()) - entry.Remove() + slog.Debug("Removing monster entitity: ", "entry: ", entry.String()) + archetype.RemoveMonster(entry, ls.ecs.World) } for entry := range tags.PickupTag.Iter(ls.ecs.World) { @@ -105,9 +103,9 @@ func (ls *LevelScene) configureECS(world donburi.World) { ls.ecs.AddRenderer(layer.Background, system.Render.DrawBackground) ls.ecs.AddRenderer(layer.Foreground, system.Render.Draw) ls.ecs.AddRenderer(layer.UI, system.UI.Draw) - ls.ecs.AddRenderer(layer.UI, system.DrawMinimap) + ls.ecs.AddRenderer(layer.UI, system.Minimap.Draw) ls.ecs.AddRenderer(layer.UI, system.InventoryUI.Draw) - if system.Debug.On { + if engine.Debug.On() { ls.ecs.AddRenderer(layer.UI, system.Debug.Draw) } diff --git a/system/turn.go b/system/turn.go index 3a40fe8..1dea4bf 100644 --- a/system/turn.go +++ b/system/turn.go @@ -51,7 +51,7 @@ func (td *TurnData) Update(ecs *ecs.ECS) { position := component.Position.Get(entry) tile := level.GetFromXY(position.X, position.Y) tile.Blocked = false - ecs.World.Remove(entry.Entity()) + archetype.RemoveMonster(entry, ecs.World) } } @@ -91,9 +91,9 @@ func (td *TurnData) Update(ecs *ecs.ECS) { component.Wallet.Get(playerEntry).AddAmount( component.Value.Get(entry).Amount, ) - archetype.RemoveItemFromWorld(entry) itemName := component.Name.Get(entry) playerMessages.WorldInteractionMessage = fmt.Sprintf("Picked up %s!", itemName.Value) + entry.Remove() break } diff --git a/system/ui.go b/system/ui.go index e200a37..4e78b49 100644 --- a/system/ui.go +++ b/system/ui.go @@ -9,7 +9,9 @@ import ( "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/assets" "github.com/kensonjohnson/roguelike-game-go/component" + "github.com/kensonjohnson/roguelike-game-go/internal/colors" "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/kensonjohnson/roguelike-game-go/internal/engine/shapes" "github.com/yohamta/donburi" "github.com/yohamta/donburi/ecs" "github.com/yohamta/donburi/filter" @@ -18,6 +20,10 @@ import ( type ui struct { query donburi.Query lastMessages []string + healthBox *ebiten.Image + coinBox *ebiten.Image + messageBox *ebiten.Image + posX, posY int } var defaultMessages = []string{ @@ -32,6 +38,15 @@ var UI = &ui{ component.UserMessage, )), lastMessages: defaultMessages, + healthBox: createHealthBox(), + coinBox: createCoinBox(), + messageBox: shapes.MakeBox( + 50*config.TileWidth, config.UIHeight*config.TileHeight, 4, + colors.Peru, color.Black, + shapes.SimpleCorner, + ), + posX: 15 * config.TileWidth, + posY: (config.ScreenHeight - config.UIHeight) * config.TileHeight, } func (u *ui) Update(ecs *ecs.ECS) { @@ -69,14 +84,12 @@ func (u *ui) Update(ecs *ecs.ECS) { } func (u *ui) Draw(ecs *ecs.ECS, screen *ebiten.Image) { - entry := tags.UITag.MustFirst(ecs.World) - ui := component.UI.Get(entry) - - // Draw the user message box - drawUserMessages(screen, &ui.MessageBox, u.lastMessages) // Draw the player HUD - drawPlayerHud(screen, &ui.PlayerHUD) + u.drawPlayerHud(screen, ecs.World) + + // Draw the user message box + u.drawUserMessages(screen, u.lastMessages) } @@ -87,105 +100,109 @@ func createTextDrawOptions(x, y int, color color.Color) *text.DrawOptions { return options } -func drawUserMessages(screen *ebiten.Image, messageBox *component.UserMessageBoxData, lastMessages []string) { - options := &ebiten.DrawImageOptions{} - options.GeoM.Translate( - float64(messageBox.Position.X), - float64(messageBox.Position.Y), - ) - screen.DrawImage(messageBox.Sprite, options) +func (u *ui) drawPlayerHud(screen *ebiten.Image, world donburi.World) { - // Draw the user messages - fontX := messageBox.FontX - fontY := messageBox.FontY - for _, message := range lastMessages { - if message != "" { - textOptions := &text.DrawOptions{} - textOptions.GeoM.Translate( - float64(fontX), - float64(fontY), - ) - textOptions.ColorScale.ScaleWithColor(color.White) - text.Draw(screen, message, assets.KenneyPixelFont, textOptions) - fontY += config.FontSize + 2 - } - } -} + // spacing := config.TileWidth -func drawPlayerHud(screen *ebiten.Image, playerHUD *component.PlayerHUDData) { options := &ebiten.DrawImageOptions{} options.GeoM.Translate( - float64(playerHUD.Position.X), - float64(playerHUD.Position.Y), + float64(u.posX), float64(u.posY-28), ) - screen.DrawImage(playerHUD.Sprite, options) + screen.DrawImage(u.healthBox, options) + + options.GeoM.Translate(float64(u.healthBox.Bounds().Dx()-4), 0) + screen.DrawImage(u.coinBox, options) + + playerEntry := tags.PlayerTag.MustFirst(world) + health := component.Health.Get(playerEntry) + wallet := component.Wallet.Get(playerEntry) // Draw the player's info - fontX := playerHUD.FontX - fontY := playerHUD.FontY + fontX := u.posX + 36 + fontY := u.posY - 26 + + textOptions := createTextDrawOptions(fontX, fontY, color.White) // Health + // TODO: Color text based on current health message := fmt.Sprintf( - "Health: %d / %d", - playerHUD.Health.CurrentHealth, - playerHUD.Health.MaxHealth, + "%d / %d", + health.CurrentHealth, + health.MaxHealth, ) text.Draw( screen, message, - assets.KenneyPixelFont, - createTextDrawOptions(fontX, fontY, color.White), + assets.KenneyMiniSquaredFont, + textOptions, ) - fontY += config.FontSize + 4 - // Armor - - message = fmt.Sprintf( - "Armor Class: %d", - playerHUD.Defense.ArmorClass, - ) + // Move cursor to next box + textOptions.GeoM.Translate(float64(u.healthBox.Bounds().Dx()), 0) + message = fmt.Sprintf("%d", wallet.Amount) text.Draw( screen, message, - assets.KenneyPixelFont, - createTextDrawOptions(fontX, fontY, color.White), + assets.KenneyMiniSquaredFont, + textOptions, ) - fontY += config.FontSize + 4 +} - message = fmt.Sprintf( - "Defense: %d", - playerHUD.Defense.Defense, - ) - text.Draw( - screen, - message, - assets.KenneyPixelFont, - createTextDrawOptions(fontX, fontY, color.White), - ) - fontY += config.FontSize + 4 +func (u *ui) drawUserMessages(screen *ebiten.Image, lastMessages []string) { - // Weapon - message = fmt.Sprintf( - "Damage: %d - %d", - playerHUD.Attack.MinimumDamage, - playerHUD.Attack.MaximumDamage, - ) - text.Draw( - screen, - message, - assets.KenneyPixelFont, - createTextDrawOptions(fontX, fontY, color.White), + options := &ebiten.DrawImageOptions{} + options.GeoM.Translate( + float64(u.posX), float64(u.posY), ) - fontY += config.FontSize + 4 + screen.DrawImage(u.messageBox, options) + + // Draw the user messages + fontX := u.posX + 28 + fontY := u.posY + 10 + for _, message := range lastMessages { + if message != "" { + textOptions := &text.DrawOptions{} + textOptions.GeoM.Translate( + float64(fontX), + float64(fontY), + ) + textOptions.ColorScale.ScaleWithColor(color.White) + text.Draw(screen, message, assets.KenneyPixelFont, textOptions) + fontY += config.FontSize + 2 + } + } +} - message = fmt.Sprintf( - "To Hit Bonus: %d", - playerHUD.Attack.ToHitBonus, +func createHealthBox() *ebiten.Image { + image := shapes.MakeBox( + 90+assets.Heart.Bounds().Dx()*2, + assets.Heart.Bounds().Dx()*2, + 4, + colors.Peru, color.Black, + shapes.BasicCorner, ) - text.Draw( - screen, - message, - assets.KenneyPixelFont, - createTextDrawOptions(fontX, fontY, color.White), + + options := &ebiten.DrawImageOptions{} + options.GeoM.Scale(2, 2) + options.GeoM.Translate(2, 0) + image.DrawImage(assets.Heart, options) + + return image +} + +func createCoinBox() *ebiten.Image { + image := shapes.MakeBox( + 90+assets.WorldSmallCoin.Bounds().Dx()*2, + assets.WorldSmallCoin.Bounds().Dx()*2, + 4, + colors.Peru, color.Black, + shapes.BasicCorner, ) + + options := &ebiten.DrawImageOptions{} + options.GeoM.Scale(2, 2) + options.GeoM.Translate(2, 0) + image.DrawImage(assets.WorldSmallCoin, options) + + return image }