diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..695a4a2 --- /dev/null +++ b/Justfile @@ -0,0 +1,54 @@ +set quiet := true + +MAIN_PACKAGE_PATH := "." +BINARY_NAME := "rougelike-demo" + +[private] +help: + just --list --unsorted + +_confirm: + echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ] + +# Run dev server +dev: + go run . -debug + +# Run all Go test files +test: + go test -v -race -buildvcs ./... + +# Prepare and audit code, then push to Git Remote +push: tidy audit no-dirty + git push + +# Verify and Vet all Go files in project +audit: + go mod verify + go vet ./... + +[private] +no-dirty: + git diff --exit-code + +# Run formatter and tidy over all Go files in project +tidy: + go fmt ./... + go mod tidy -v + +# Build for current OS/Arch +build: + # Include additional build steps, like TypeScript, SCSS or Tailwind compilation here... + go build -o=/tmp/bin/{{ BINARY_NAME }} {{ MAIN_PACKAGE_PATH }} + +# Build for current OS/Arch and run the resulting binary +run: build + /tmp/bin/{{ BINARY_NAME }} + +# Matrix build for all OS/Architectures +production-deploy: _confirm tidy audit + GOOS=darwin GOARCH=arm64 go build -ldflags='-s' -o=/tmp/bin/macos_arm64/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + GOOS=windows GOARCH=amd64 go build -ldflags='-s' -o=/tmp/bin/windows_amd64/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + GOOS=windows GOARCH=arm go build -ldflags='-s' -o=/tmp/bin/windows_arm/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + GOOS=windows GOARCH=arm64 go build -ldflags='-s' -o=/tmp/bin/windows_arm64/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + # Include additional deployment steps here... diff --git a/Makefile b/Makefile deleted file mode 100644 index 7494d9b..0000000 --- a/Makefile +++ /dev/null @@ -1,101 +0,0 @@ -# Change these variables as necessary. -MAIN_PACKAGE_PATH := . -BINARY_NAME := rougelike-demo - -# ==================================================================================== # -# HELPERS -# ==================================================================================== # - -## help: print this help message -.PHONY: help -help: - @echo 'Usage:' - @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' - -.PHONY: confirm -confirm: - @echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ] - -.PHONY: no-dirty -no-dirty: - git diff --exit-code - - -# ==================================================================================== # -# QUALITY CONTROL -# ==================================================================================== # - -## tidy: format code and tidy modfile -.PHONY: tidy -tidy: - go fmt ./... - go mod tidy -v - -## audit: run quality control checks -.PHONY: audit -audit: - go mod verify - go vet ./... - go run golang.org/x/vuln/cmd/govulncheck@latest ./... - go test -race -buildvcs -vet=off ./... - - -# ==================================================================================== # -# DEVELOPMENT -# ==================================================================================== # - -## Run with dev flags -.PHONY: dev -dev: - go run . -debug - -## test: run all tests -.PHONY: test -test: - go test -v -race -buildvcs ./... - -## test/cover: run all tests and display coverage -.PHONY: test/cover -test/cover: - go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... - go tool cover -html=/tmp/coverage.out - -## build: build the application -.PHONY: build -build: - # Include additional build steps, like TypeScript, SCSS or Tailwind compilation here... - go build -o=/tmp/bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH} - -## run: run the rapplication -.PHONY: run -run: build - /tmp/bin/${BINARY_NAME} - -## run/live: run the application with reloading on file changes -.PHONY: run/live -run/live: - go run github.com/cosmtrek/air \ - --build.cmd "make build" --build.bin "/tmp/bin/${BINARY_NAME}" --build.delay "100" \ - --build.exclude_dir "" \ - --build.include_ext "go, tpl, tmpl, html, css, scss, js, ts, sql, jpeg, jpg, gif, png, bmp, svg, webp, ico" \ - --misc.clean_on_exit "true" - - -# ==================================================================================== # -# OPERATIONS -# ==================================================================================== # - -## push: push changes to the remote Git repository -.PHONY: push -push: tidy audit no-dirty - git push - -## production/deploy: deploy the application to production -.PHONY: production/deploy -production/deploy: confirm tidy audit - GOOS=darwin GOARCH=arm64 go build -ldflags='-s' -o=/tmp/bin/macos_arm64/${BINARY_NAME} ${MAIN_PACKAGE_PATH} - GOOS=windows GOARCH=amd64 go build -ldflags='-s' -o=/tmp/bin/windows_amd64/${BINARY_NAME} ${MAIN_PACKAGE_PATH} - GOOS=windows GOARCH=arm go build -ldflags='-s' -o=/tmp/bin/windows_arm/${BINARY_NAME} ${MAIN_PACKAGE_PATH} - GOOS=windows GOARCH=arm64 go build -ldflags='-s' -o=/tmp/bin/windows_arm64/${BINARY_NAME} ${MAIN_PACKAGE_PATH} - # Include additional deployment steps here... - \ No newline at end of file diff --git a/archetype/armor.go b/archetype/armor.go index afc262d..ee66ad9 100644 --- a/archetype/armor.go +++ b/archetype/armor.go @@ -1,19 +1,17 @@ package archetype import ( + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/items/armors" + "github.com/kensonjohnson/roguelike-game-go/items" "github.com/yohamta/donburi" ) -var ArmorTag = donburi.NewTag("armor") - -func CreateNewArmor(world donburi.World, armorId armors.ArmorId) *donburi.Entry { - armorData := armors.Data[armorId] - entry := CreateNewItem(world, int(armorId), armorData.Name, armorData.Sprite) +func CreateNewArmor(world donburi.World, armorData items.ArmorData) *donburi.Entry { + entry := CreateNewItem(world, &armorData.ItemData) // Mark as an armor - entry.AddComponent(ArmorTag) + entry.AddComponent(tags.ArmorTag) // Add defense data entry.AddComponent(component.Defense) @@ -27,5 +25,5 @@ func CreateNewArmor(world donburi.World, armorId armors.ArmorId) *donburi.Entry } func IsArmor(entry *donburi.Entry) bool { - return entry.HasComponent(ArmorTag) + return entry.HasComponent(tags.ArmorTag) } diff --git a/archetype/camera.go b/archetype/camera.go index f19b048..e7e6999 100644 --- a/archetype/camera.go +++ b/archetype/camera.go @@ -2,32 +2,22 @@ package archetype import ( "github.com/hajimehoshi/ebiten/v2" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/config" - "github.com/kensonjohnson/roguelike-game-go/internal/logger" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/setanarut/kamera/v2" "github.com/yohamta/donburi" ) -var CameraTag = donburi.NewTag("camera") - func CreateNewCamera(world donburi.World) { - var entry *donburi.Entry - var ok bool - if entry, ok = PlayerTag.First(world); !ok { - logger.ErrorLogger.Panic("CreateNewCamera failed: Player not found") - } - playerPosition := component.Position.Get(entry) - - entry = world.Entry(world.Create( - CameraTag, + entry := world.Entry(world.Create( + tags.CameraTag, component.Camera, )) cameraData := &component.CameraData{ MainCamera: kamera.NewCamera( - float64((playerPosition.X*config.TileWidth)+config.TileWidth/2), - float64((playerPosition.Y*config.TileHeight)+config.TileHeight/2), + 0, 0, config.ScreenWidth*config.TileWidth, (config.ScreenHeight-config.UIHeight)*config.TileHeight, ), @@ -45,3 +35,17 @@ func CreateNewCamera(world donburi.World) { component.Camera.Set(entry, cameraData) } + +func ReplaceCamera(world donburi.World, playerX, playerY float64) { + entry := tags.CameraTag.MustFirst(world) + camera := component.Camera.Get(entry) + camera.MainCamera = kamera.NewCamera( + playerX, playerY, + config.ScreenWidth*config.TileWidth, + (config.ScreenHeight-config.UIHeight)*config.TileHeight, + ) + camera.MainCamera.Lerp = true + camera.MainCamera.ZoomFactor = 100 + camera.MainCamera.ShakeOptions.MaxShakeAngle = 0 + camera.MainCamera.ShakeOptions.Decay = 0.5 +} diff --git a/archetype/consumable.go b/archetype/consumable.go index 9b1b452..53fe570 100644 --- a/archetype/consumable.go +++ b/archetype/consumable.go @@ -1,20 +1,18 @@ package archetype import ( + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/items/consumables" + "github.com/kensonjohnson/roguelike-game-go/items" "github.com/yohamta/donburi" ) -var ConsumableTag = donburi.NewTag("consumable") +func CreateNewConsumable(world donburi.World, consumableData items.ConsumableData) *donburi.Entry { -func CreateNewConsumable(world donburi.World, consumablesId consumables.ConsumablesId) *donburi.Entry { - consumableData := consumables.Data[consumablesId] - - entry := CreateNewItem(world, int(consumablesId), consumableData.Name, consumableData.Sprite) + entry := CreateNewItem(world, &consumableData.ItemData) // Mark as a consumable - entry.AddComponent(ConsumableTag) + entry.AddComponent(tags.ConsumableTag) // Add heal data entry.AddComponent(component.Heal) @@ -27,5 +25,5 @@ func CreateNewConsumable(world donburi.World, consumablesId consumables.Consumab } func IsConsumable(entry *donburi.Entry) bool { - return entry.HasComponent(ConsumableTag) + return entry.HasComponent(tags.ConsumableTag) } diff --git a/archetype/item.go b/archetype/item.go index c23bc28..bda5de8 100644 --- a/archetype/item.go +++ b/archetype/item.go @@ -3,38 +3,34 @@ package archetype import ( "errors" - "github.com/hajimehoshi/ebiten/v2" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" + "github.com/kensonjohnson/roguelike-game-go/items" "github.com/yohamta/donburi" ) -func CreateNewItem(world donburi.World, itemId int, itemName string, itemImage *ebiten.Image) *donburi.Entry { - item := world.Entry(world.Create( - component.ItemId, +func CreateNewItem(world donburi.World, itemData *items.ItemData) *donburi.Entry { + entry := world.Entry(world.Create( + tags.ItemTag, component.Name, component.Sprite, )) - id := component.ItemIdData{ - Id: itemId, - } - component.ItemId.SetValue(item, id) - name := component.NameData{ - Value: itemName, + Value: itemData.Name, } - component.Name.SetValue(item, name) + component.Name.SetValue(entry, name) sprite := component.SpriteData{ - Image: itemImage, + Image: itemData.Sprite, } - component.Sprite.SetValue(item, sprite) + component.Sprite.SetValue(entry, sprite) - return item + return entry } func isItem(entry *donburi.Entry) bool { - return entry.HasComponent(component.ItemId) + return entry.HasComponent(tags.ItemTag) } func PlaceItemInWorld(entry *donburi.Entry, x, y int, discoverable bool) error { @@ -42,6 +38,8 @@ func PlaceItemInWorld(entry *donburi.Entry, x, y int, discoverable bool) error { return errors.New("entry is not an Item Entity") } + entry.AddComponent(tags.PickupTag) + entry.AddComponent(component.Position) position := component.PositionData{ X: x, @@ -59,6 +57,7 @@ func PlaceItemInWorld(entry *donburi.Entry, x, y int, discoverable bool) error { } func RemoveItemFromWorld(entry *donburi.Entry) { + entry.RemoveComponent(tags.PickupTag) entry.RemoveComponent(component.Position) if entry.HasComponent(component.Discoverable) { entry.RemoveComponent(component.Discoverable) diff --git a/archetype/level.go b/archetype/level.go index 028b2c2..20ddc05 100644 --- a/archetype/level.go +++ b/archetype/level.go @@ -1,25 +1,23 @@ package archetype import ( + "log" + + "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/config" - "github.com/kensonjohnson/roguelike-game-go/engine" - "github.com/kensonjohnson/roguelike-game-go/internal/logger" - "github.com/kensonjohnson/roguelike-game-go/items/armors" - "github.com/kensonjohnson/roguelike-game-go/items/consumables" - "github.com/kensonjohnson/roguelike-game-go/items/weapons" + "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/kensonjohnson/roguelike-game-go/internal/engine" + "github.com/kensonjohnson/roguelike-game-go/items" "github.com/yohamta/donburi" ) const levelHeight = config.ScreenHeight - config.UIHeight -var LevelTag = donburi.NewTag("level") - // Creates a new Dungeon func GenerateLevel(world donburi.World) *component.LevelData { entry := world.Entry(world.Create( - LevelTag, + tags.LevelTag, component.Level, )) @@ -167,16 +165,12 @@ func min(x, y int) int { func seedRooms(world donburi.World, level *component.LevelData) { for index, room := range level.Rooms { if index == 0 { - CreateNewPlayer( - world, - level, - room, - weapons.BattleAxe, - armors.PlateArmor, - ) + playerEntry := tags.PlayerTag.MustFirst(world) + playerPosition := component.Position.Get(playerEntry) + playerPosition.X, playerPosition.Y = room.Center() } else { CreateMonster(world, level, room) - addRandomPickupToRoom(world, room) + addRandomPickupsToRoom(world, level, room, 3) } } exitRoomIndex := engine.GetDiceRoll(len(level.Rooms) - 1) @@ -187,17 +181,64 @@ func seedRooms(world donburi.World, level *component.LevelData) { exitTile.TileType = component.STAIR_DOWN } -func addRandomPickupToRoom(world donburi.World, room engine.Rect) { - // TODO: add a random chance for an item to appear - // TODO: create a random distribution of items - // for now, we'll just put a single health potion in each room +func addRandomPickupsToRoom( + world donburi.World, + level *component.LevelData, + room engine.Rect, + generosity int, +) { + d10Roll := engine.GetDiceRoll(10) + if d10Roll > generosity { + return + } width := room.X2 - room.X1 - 2 height := room.Y2 - room.Y1 - 2 - offsetX := engine.GetRandomInt(width) - offsetY := engine.GetRandomInt(height) - potion := CreateNewConsumable(world, consumables.HealthPotion) - err := PlaceItemInWorld(potion, room.X1+offsetX+1, room.Y1+offsetY+1, true) - if err != nil { - logger.ErrorLogger.Panic("Failed to place consumable in the world") + for i := 0; i < d10Roll; i++ { + + offsetX := engine.GetRandomInt(width) + offsetY := engine.GetRandomInt(height) + x := room.X1 + offsetX + 1 + y := room.Y1 + offsetY + 1 + + tile := level.GetFromXY(x, y) + if tile.Blocked { + continue + } + + spotTaken := false + for entry := range tags.PickupTag.Iter(world) { + position := component.Position.Get(entry) + if position.X == x && position.Y == y { + spotTaken = true + } + } + + if spotTaken { + continue + } + + entry := createRandomPickup(world) + + err := PlaceItemInWorld(entry, x, y, true) + if err != nil { + log.Panic("Failed to place consumable in the world") + } } } + +func createRandomPickup(world donburi.World) *donburi.Entry { + // This is where we can manipulate the randomness of which item drops + switch roll := engine.GetDiceRoll(10); roll { + case 1, 4: + return CreateNewConsumable(world, items.Consumables.HealthPotion) + case 2, 3, 5, 6: + return CreateCoins(world, items.Valuables.SmallCoin()) + case 7, 9, 10: + return CreateCoins(world, items.Valuables.CoinStack()) + case 8: + return CreateNewValuable(world, items.Valuables.Alcohol) + default: + return CreateNewConsumable(world, items.Consumables.HealthPotion) + } + +} diff --git a/archetype/monster.go b/archetype/monster.go index c4baf2f..b7ff3e6 100644 --- a/archetype/monster.go +++ b/archetype/monster.go @@ -1,20 +1,30 @@ 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/engine" - "github.com/kensonjohnson/roguelike-game-go/items/armors" - "github.com/kensonjohnson/roguelike-game-go/items/weapons" + "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" ) -var MonsterTag = donburi.NewTag("monster") - func CreateMonster(world donburi.World, level *component.LevelData, room engine.Rect) { + + innerRoomWidth := room.X2 - room.X1 - 2 + innerRoomHeight := room.Y2 - room.Y1 - 2 + offsetX := engine.GetRandomInt(innerRoomWidth) + offsetY := engine.GetRandomInt(innerRoomHeight) + startingX := room.X1 + offsetX + 1 + startingY := room.Y1 + offsetY + 1 + tile := level.GetFromXY(startingX, startingY) + if tile.Blocked { + return + } + monster := world.Entry(world.Create( - MonsterTag, + tags.MonsterTag, component.Position, component.Sprite, component.Name, @@ -29,7 +39,6 @@ func CreateMonster(world donburi.World, level *component.LevelData, room engine. )) // Set position - startingX, startingY := room.Center() position := component.PositionData{ X: startingX, Y: startingY, @@ -49,13 +58,13 @@ func CreateMonster(world donburi.World, level *component.LevelData, room engine. if coinflip == 2 { sprite.Image = assets.Orc name.Value = "Orc" - equipment.Armor = CreateNewArmor(world, armors.PaddedArmor) - equipment.Weapon = CreateNewWeapon(world, weapons.ShortSword) + equipment.Armor = CreateNewArmor(world, items.Armor.PaddedArmor) + equipment.Weapon = CreateNewWeapon(world, items.Weapons.ShortSword) } else { sprite.Image = assets.Skelly name.Value = "Skeleton" - equipment.Armor = CreateNewArmor(world, armors.Bones) - equipment.Weapon = CreateNewWeapon(world, weapons.ShortSword) + equipment.Armor = CreateNewArmor(world, items.Armor.Bones) + equipment.Weapon = CreateNewWeapon(world, items.Weapons.ShortSword) } component.Sprite.SetValue(monster, sprite) component.Name.SetValue(monster, name) diff --git a/archetype/player.go b/archetype/player.go index b3a2f48..a01d3ca 100644 --- a/archetype/player.go +++ b/archetype/player.go @@ -1,31 +1,28 @@ 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/engine" - "github.com/kensonjohnson/roguelike-game-go/items/armors" - "github.com/kensonjohnson/roguelike-game-go/items/weapons" + "github.com/kensonjohnson/roguelike-game-go/items" "github.com/norendren/go-fov/fov" "github.com/yohamta/donburi" ) -var PlayerTag = donburi.NewTag("player") - func CreateNewPlayer( world donburi.World, - level *component.LevelData, - startingRoom engine.Rect, - weaponId weapons.WeaponId, - armorId armors.ArmorId, -) { + weapon items.WeaponData, + armorId items.ArmorData, +) *donburi.Entry { player := world.Entry(world.Create( - PlayerTag, + tags.PlayerTag, component.Position, component.Sprite, component.Name, component.Fov, component.Equipment, + component.Inventory, + component.Wallet, component.Health, component.UserMessage, component.Attack, @@ -33,17 +30,11 @@ func CreateNewPlayer( component.Defense, )) - // Set starting position - startingX, startingY := startingRoom.Center() - position := component.PositionData{ - X: startingX, - Y: startingY, - } + position := component.PositionData{} component.Position.SetValue(player, position) // Update player's field of view vision := component.FovData{VisibleTiles: fov.New()} - vision.VisibleTiles.Compute(level, startingX, startingY, 8) component.Fov.SetValue(player, vision) // Set sprite @@ -65,11 +56,18 @@ func CreateNewPlayer( // Add gear equipment := component.EquipmentData{ - Weapon: CreateNewWeapon(world, weaponId), - Armor: CreateNewArmor(world, armorId), + Weapon: CreateNewWeapon(world, items.Weapons.BattleAxe), + Armor: CreateNewArmor(world, items.Armor.PlateArmor), } component.Equipment.SetValue(player, equipment) + // Setup inventory + inventory := component.NewInventory(30) + component.Inventory.SetValue(player, inventory) + + wallet := component.WalletData{} + component.Wallet.SetValue(player, wallet) + // Set default messages component.UserMessage.SetValue( player, @@ -96,4 +94,5 @@ func CreateNewPlayer( defense := component.Defense.Get(equipment.Armor) component.Defense.SetValue(player, *defense) + return player } diff --git a/archetype/tags/tags.go b/archetype/tags/tags.go new file mode 100644 index 0000000..2df3de3 --- /dev/null +++ b/archetype/tags/tags.go @@ -0,0 +1,18 @@ +package tags + +import "github.com/yohamta/donburi" + +var ( + ArmorTag = donburi.NewTag("armor") + CameraTag = donburi.NewTag("camera") + CoinTag = donburi.NewTag("coin") + ConsumableTag = donburi.NewTag("consumable") + ItemTag = donburi.NewTag("item") + LevelTag = donburi.NewTag("level") + MonsterTag = donburi.NewTag("monster") + PickupTag = donburi.NewTag("pickup") + PlayerTag = donburi.NewTag("player") + UITag = donburi.NewTag("ui") + ValuableTag = donburi.NewTag("valuable") + WeaponTag = donburi.NewTag("weapon") +) diff --git a/archetype/ui.go b/archetype/ui.go index 181efac..3fbce2f 100644 --- a/archetype/ui.go +++ b/archetype/ui.go @@ -1,17 +1,16 @@ 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/config" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/yohamta/donburi" ) -var UITag = donburi.NewTag("ui") - func CreateNewUI(world donburi.World) { entity := world.Create( - UITag, + tags.UITag, component.UI, ) entry := world.Entry(entity) @@ -33,7 +32,7 @@ func CreateNewUI(world donburi.World) { // Configure player HUD playerHUDTopPixel := (config.ScreenHeight * config.TileHeight) - 220 - playerEntry := PlayerTag.MustFirst(world) + playerEntry := tags.PlayerTag.MustFirst(world) ui.PlayerHUD.Position = component.PositionData{ X: config.ScreenWidth * config.TileWidth / 2, Y: playerHUDTopPixel, diff --git a/archetype/valuable.go b/archetype/valuable.go new file mode 100644 index 0000000..ff25596 --- /dev/null +++ b/archetype/valuable.go @@ -0,0 +1,34 @@ +package archetype + +import ( + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" + "github.com/kensonjohnson/roguelike-game-go/component" + "github.com/kensonjohnson/roguelike-game-go/items" + "github.com/yohamta/donburi" +) + +func CreateNewValuable(world donburi.World, valuableData *items.ValuableData) *donburi.Entry { + entry := CreateNewItem(world, &valuableData.ItemData) + + entry.AddComponent(tags.ValuableTag) + + entry.AddComponent(component.Value) + value := component.ValueData{ + Amount: valuableData.Value, + } + component.Value.SetValue(entry, value) + + return entry +} + +func CreateCoins(world donburi.World, valuableData *items.ValuableData) *donburi.Entry { + entry := CreateNewValuable(world, valuableData) + + entry.AddComponent(tags.CoinTag) + + return entry +} + +func IsCoin(entry donburi.Entry) bool { + return entry.HasComponent(tags.CoinTag) +} diff --git a/archetype/weapon.go b/archetype/weapon.go index b89aea9..e1b4540 100644 --- a/archetype/weapon.go +++ b/archetype/weapon.go @@ -1,19 +1,17 @@ package archetype import ( + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/items/weapons" + "github.com/kensonjohnson/roguelike-game-go/items" "github.com/yohamta/donburi" ) -var WeaponTag = donburi.NewTag("weapon") - -func CreateNewWeapon(world donburi.World, weaponId weapons.WeaponId) *donburi.Entry { - weaponData := weapons.Data[weaponId] - entry := CreateNewItem(world, int(weaponId), weaponData.Name, weaponData.Sprite) +func CreateNewWeapon(world donburi.World, weaponData items.WeaponData) *donburi.Entry { + entry := CreateNewItem(world, &weaponData.ItemData) // Mark as a weapon - entry.AddComponent(WeaponTag) + entry.AddComponent(tags.WeaponTag) // Add attack information entry.AddComponent(component.Attack) @@ -35,5 +33,5 @@ func CreateNewWeapon(world donburi.World, weaponId weapons.WeaponId) *donburi.En } func IsWeapon(entry *donburi.Entry) bool { - return entry.HasComponent(WeaponTag) + return entry.HasComponent(tags.WeaponTag) } diff --git a/assets/efs.go b/assets/efs.go index 05de464..3a818ab 100644 --- a/assets/efs.go +++ b/assets/efs.go @@ -3,12 +3,13 @@ package assets import ( "bytes" "embed" + "log" + "log/slog" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/text/v2" - "github.com/kensonjohnson/roguelike-game-go/config" - "github.com/kensonjohnson/roguelike-game-go/internal/logger" + "github.com/kensonjohnson/roguelike-game-go/internal/config" ) var ( @@ -26,6 +27,7 @@ var ( // UI UIPanel *ebiten.Image UIPanelWithMinimap *ebiten.Image + UICorner *ebiten.Image KenneyMiniFont *text.GoTextFace KenneyMiniSquaredFont *text.GoTextFace KenneyPixelFont *text.GoTextFace @@ -135,6 +137,7 @@ var ( // Loads all required assets, panics if any one fails. func init() { + slog.Debug("Loading Assets...") /*----------------------- --------- Tiles --------- -----------------------*/ @@ -150,20 +153,21 @@ func init() { -----------------------*/ 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 { - logger.ErrorLogger.Panic(err) + log.Panic(err) } KenneyMiniFont = mustLoadFont(kenneyMiniFontBytes) kenneyMiniSquaredFontBytes, err := assetsFS.ReadFile("fonts/KenneyMiniSquared.ttf") if err != nil { - logger.ErrorLogger.Panic(err) + log.Panic(err) } KenneyMiniSquaredFont = mustLoadFont(kenneyMiniSquaredFontBytes) kenneyPixelFontBytes, err := assetsFS.ReadFile("fonts/KenneyPixel.ttf") if err != nil { - logger.ErrorLogger.Panic(err) + log.Panic(err) } KenneyPixelFont = mustLoadFont(kenneyPixelFontBytes) // For some reason, the KenneyPixel shows up as half the size of the other fonts. @@ -284,11 +288,11 @@ func init() { func mustLoadImage(filePath string) *ebiten.Image { imgSource, err := assetsFS.ReadFile(filePath) if err != nil { - logger.ErrorLogger.Panic(err) + log.Panic(err) } image, _, err := ebitenutil.NewImageFromReader(bytes.NewReader(imgSource)) if err != nil { - logger.ErrorLogger.Panic(err) + log.Panic(err) } return image } @@ -297,10 +301,17 @@ func mustLoadImage(filePath string) *ebiten.Image { func mustLoadFont(font []byte) *text.GoTextFace { source, err := text.NewGoTextFaceSource(bytes.NewReader(font)) if err != nil { - logger.ErrorLogger.Panic(err) + log.Panic(err) } return &text.GoTextFace{ Source: source, Size: float64(config.FontSize), } } + +func MustBeValidImage(image *ebiten.Image, name string) *ebiten.Image { + if image == nil { + log.Panicf("%s asset not loaded!", name) + } + return image +} diff --git a/assets/images/ui/UICorner.png b/assets/images/ui/UICorner.png new file mode 100644 index 0000000..966235e Binary files /dev/null and b/assets/images/ui/UICorner.png differ diff --git a/component/inventory.go b/component/inventory.go new file mode 100644 index 0000000..02804d3 --- /dev/null +++ b/component/inventory.go @@ -0,0 +1,78 @@ +package component + +import ( + "errors" + "log" + + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" + "github.com/yohamta/donburi" +) + +type InventoryData struct { + Items []*donburi.Entry + capacity int + holding int +} + +var Inventory = donburi.NewComponentType[InventoryData]() + +func NewInventory(capacity int) InventoryData { + return InventoryData{ + Items: make([]*donburi.Entry, capacity), + capacity: capacity, + holding: 0, + } +} + +func (i *InventoryData) GetCapacityInfo() (holding, capacity int) { + return i.holding, i.capacity +} + +func (i *InventoryData) IncreaseCapacityByAmount(amount int) { + i.capacity += amount + newStorage := make([]*donburi.Entry, i.capacity) + copy(newStorage, i.Items) + i.Items = newStorage +} + +func (i *InventoryData) DecreaseCapacityByAmount(amount int) error { + if i.capacity-i.holding > amount { + return errors.New("holding too many items to reduce capacity") + } + + return nil +} + +func (i *InventoryData) AddItem(item *donburi.Entry) error { + if i.holding >= i.capacity { + return errors.New("inventory full") + } + + if !item.HasComponent(tags.ItemTag) { + log.Panic("Entry is not an item: ", item) + } + + var targetIndex = -1 + + for index, element := range i.Items { + if element == nil { + targetIndex = index + break + } + } + + if targetIndex == -1 { + return errors.New("failed to find empty index for item") + } + + i.Items[targetIndex] = item + return nil +} + +func (i *InventoryData) RemoveItem(index int) error { + if index >= i.capacity { + log.Panic("index out of range in RemoveItem. Recieved: ", index) + } + i.Items[index] = nil + return nil +} diff --git a/component/item-id.go b/component/item-id.go deleted file mode 100644 index 6e86dd7..0000000 --- a/component/item-id.go +++ /dev/null @@ -1,9 +0,0 @@ -package component - -import "github.com/yohamta/donburi" - -type ItemIdData struct { - Id int -} - -var ItemId = donburi.NewComponentType[ItemIdData]() diff --git a/component/level.go b/component/level.go index e4d86f8..89e9f45 100644 --- a/component/level.go +++ b/component/level.go @@ -1,10 +1,11 @@ package component import ( + "log" + "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/config" - "github.com/kensonjohnson/roguelike-game-go/engine" - "github.com/kensonjohnson/roguelike-game-go/internal/logger" + "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/kensonjohnson/roguelike-game-go/internal/engine" "github.com/yohamta/donburi" ) @@ -46,7 +47,7 @@ func (l *LevelData) GetIndexFromXY(x int, y int) int { // This coordinate is logical tiles, not pixels. func (l *LevelData) GetFromXY(x, y int) *MapTile { if len(l.Tiles) == 0 { - logger.ErrorLogger.Panic("levelData.Tiles has no length") + log.Panic("levelData.Tiles has no length") } return l.Tiles[(y*config.ScreenWidth)+x] } diff --git a/component/sprite.go b/component/sprite.go index 1d36e1b..54f5dff 100644 --- a/component/sprite.go +++ b/component/sprite.go @@ -2,7 +2,7 @@ package component import ( "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/config" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/yohamta/donburi" ) @@ -15,8 +15,8 @@ type SpriteData struct { Animating bool } -// Returns the X and Y offset for the current animation frame. TODO: Add -// support for animation frames. +// Returns the X and Y offset for the current animation frame. +// TODO: Add support for animation frames. func (sd *SpriteData) GetAnimationStep() (float64, float64) { offsetX := (1 - sd.progress) * config.TileWidth * float64(sd.OffestX) offsetY := (1 - sd.progress) * config.TileHeight * float64(sd.OffestY) diff --git a/component/value.go b/component/value.go new file mode 100644 index 0000000..837d835 --- /dev/null +++ b/component/value.go @@ -0,0 +1,9 @@ +package component + +import "github.com/yohamta/donburi" + +type ValueData struct { + Amount int +} + +var Value = donburi.NewComponentType[ValueData]() diff --git a/component/wallet.go b/component/wallet.go new file mode 100644 index 0000000..94e5cee --- /dev/null +++ b/component/wallet.go @@ -0,0 +1,20 @@ +package component + +import "github.com/yohamta/donburi" + +type WalletData struct { + Amount int +} + +var Wallet = donburi.NewComponentType[WalletData]() + +func (w *WalletData) AddAmount(amount int) { + w.Amount += amount +} + +func (w *WalletData) SubtractAmount(amount int) { + w.Amount -= amount + if w.Amount < 0 { + w.Amount = 0 + } +} diff --git a/event/inventory-events.go b/event/inventory-events.go new file mode 100644 index 0000000..71f9dc5 --- /dev/null +++ b/event/inventory-events.go @@ -0,0 +1,11 @@ +package event + +import "github.com/yohamta/donburi/features/events" + +type OpenInventory struct{} + +var OpenInventoryEvent = events.NewEventType[OpenInventory]() + +type CloseInventory struct{} + +var CloseInventoryEvent = events.NewEventType[CloseInventory]() diff --git a/event/progress-level.go b/event/level-events.go similarity index 100% rename from event/progress-level.go rename to event/level-events.go diff --git a/go.mod b/go.mod index ef3e732..0c4a14c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/kensonjohnson/roguelike-game-go -go 1.22.5 +go 1.23.0 require ( github.com/hajimehoshi/ebiten/v2 v2.7.8 @@ -8,17 +8,20 @@ require ( github.com/setanarut/kamera/v2 v2.5.1 ) -require github.com/ojrac/opensimplex-go v1.0.2 // indirect +require ( + github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 // indirect + github.com/ojrac/opensimplex-go v1.0.2 // indirect +) require ( - github.com/ebitengine/gomobile v0.0.0-20240802043200-192f051f4fcc // indirect + github.com/ebitengine/gomobile v0.0.0-20240825043811-96c531f5bd83 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect github.com/ebitengine/purego v0.7.1 // indirect github.com/go-text/typesetting v0.1.1 // indirect github.com/jezek/xgb v1.1.1 // indirect - github.com/yohamta/donburi v1.4.4 + github.com/yohamta/donburi v1.15.3 golang.org/x/image v0.19.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect ) diff --git a/go.sum b/go.sum index 6ad3332..71c8b1a 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ -github.com/ebitengine/gomobile v0.0.0-20240802043200-192f051f4fcc h1:76TYsaP1F48tiQRlrr71NsbfxBcFM9/8bEHS9/JbsQg= -github.com/ebitengine/gomobile v0.0.0-20240802043200-192f051f4fcc/go.mod h1:RM/c3pvru6dRqgGEW7RCTb6czFXYAa3MxbXu3u8/dcI= +github.com/ebitengine/gomobile v0.0.0-20240825043811-96c531f5bd83 h1:yA0CtFKYZI/db1snCOInRS0Z18QGZU6aBYkqUT0H6RI= +github.com/ebitengine/gomobile v0.0.0-20240825043811-96c531f5bd83/go.mod h1:n2NbB/F4d9wOXFzC7FT1ipERidmYWC5I4YNOYRs5N7I= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo= github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= -github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= -github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= +github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY= +github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4= github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA= github.com/hajimehoshi/ebiten/v2 v2.7.8 h1:QrlvF2byCzMuDsbxFReJkOCbM3O2z1H/NKQaGcA8PKk= @@ -20,13 +20,47 @@ github.com/ojrac/opensimplex-go v1.0.2 h1:l4vs0D+JCakcu5OV0kJ99oEaWJfggSc9jiLpxa github.com/ojrac/opensimplex-go v1.0.2/go.mod h1:NwbXFFbXcdGgIFdiA7/REME+7n/lOf1TuEbLiZYOWnM= github.com/setanarut/kamera/v2 v2.5.1 h1:+bBPaUSoUsxBoZupRqWTTrzt0IetWCsIuZOk4oyKXZs= github.com/setanarut/kamera/v2 v2.5.1/go.mod h1:XxIMAowz4s+6uTR+4B8YkinDrvOechvhCAH3AlzP9iU= -github.com/yohamta/donburi v1.4.4 h1:j29uSVIherEsBGV1/MzGckxBdFoCMmRbIv9Gva80zMM= -github.com/yohamta/donburi v1.4.4/go.mod h1:cx7C0ucl1ugqXSR+OpaCgfezWJXxh7BjTceaTxzO+3E= +github.com/yohamta/donburi v1.15.3 h1:b5Z0hUez8eCnjuoDzecsj5epQflRgyqXpiT/Ppz+Pw0= +github.com/yohamta/donburi v1.15.3/go.mod h1:g5P6MQ7zQcFjiPTU3m5Ox1I+Yw8rqmwvQuxFkryQ8os= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/colors/colors.go b/internal/colors/colors.go new file mode 100644 index 0000000..0e6b017 --- /dev/null +++ b/internal/colors/colors.go @@ -0,0 +1,24 @@ +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} + Peru = color.RGBA{205, 133, 63, 255} + Gray = color.RGBA{128, 128, 128, 255} + LightGray = color.RGBA{211, 211, 211, 255} + DarkGray = color.RGBA{169, 169, 169, 255} + SlateGray = color.RGBA{112, 128, 144, 255} + Silver = color.RGBA{192, 192, 192, 255} + Yellow = color.RGBA{255, 255, 0, 255} + Orange = color.RGBA{255, 165, 0, 255} + Red = color.RGBA{255, 0, 0, 255} +) diff --git a/config/globals.go b/internal/config/globals.go similarity index 100% rename from config/globals.go rename to internal/config/globals.go diff --git a/engine/pathing/astar.go b/internal/engine/pathing/astar.go similarity index 98% rename from engine/pathing/astar.go rename to internal/engine/pathing/astar.go index ebc2a83..989c006 100644 --- a/engine/pathing/astar.go +++ b/internal/engine/pathing/astar.go @@ -5,7 +5,7 @@ import ( "reflect" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/config" + "github.com/kensonjohnson/roguelike-game-go/internal/config" ) type node struct { diff --git a/engine/random.go b/internal/engine/random.go similarity index 100% rename from engine/random.go rename to internal/engine/random.go diff --git a/engine/rect.go b/internal/engine/rect.go similarity index 100% rename from engine/rect.go rename to internal/engine/rect.go diff --git a/internal/logger/logger.go b/internal/logger/logger.go deleted file mode 100644 index 93a8d8a..0000000 --- a/internal/logger/logger.go +++ /dev/null @@ -1,25 +0,0 @@ -package logger - -import ( - "log" - "os" -) - -var ( - DebugLogger *log.Logger - InfoLogger *log.Logger - WarnLogger *log.Logger - ErrorLogger *log.Logger - DebugOn bool -) - -func init() { - DebugLogger = log.New(os.Stdout, "DEBUG: ", log.Ltime|log.Lshortfile) - InfoLogger = log.New(os.Stdout, "INFO: ", log.Ltime|log.Lshortfile) - WarnLogger = log.New(os.Stdout, "WARN: ", log.Ltime|log.Lshortfile) - ErrorLogger = log.New(os.Stderr, "ERROR: ", log.Ltime|log.Lshortfile) -} - -func SetDebug(flag bool) { - DebugOn = flag -} diff --git a/items/armors.go b/items/armors.go new file mode 100644 index 0000000..74de68d --- /dev/null +++ b/items/armors.go @@ -0,0 +1,53 @@ +package items + +import ( + "github.com/kensonjohnson/roguelike-game-go/assets" +) + +type ArmorData struct { + ItemData + Defense int + ArmorClass int +} + +type armors struct { + LinenShirt ArmorData + PaddedArmor ArmorData + Bones ArmorData + PlateArmor ArmorData +} + +var Armor = armors{ + LinenShirt: ArmorData{ + ItemData: ItemData{ + Name: "Linen Shirt", + Sprite: assets.MustBeValidImage(assets.LinenShirt, "LinenShirt"), + }, + Defense: 1, + ArmorClass: 1, + }, + PaddedArmor: ArmorData{ + ItemData: ItemData{ + Name: "Padded Armor", + Sprite: assets.MustBeValidImage(assets.PaddedArmor, "PaddedArmor"), + }, + Defense: 5, + ArmorClass: 6, + }, + Bones: ArmorData{ + ItemData: ItemData{ + Name: "Bone", + Sprite: assets.MustBeValidImage(assets.Bones, "Bones"), + }, + Defense: 3, + ArmorClass: 4, + }, + PlateArmor: ArmorData{ + ItemData: ItemData{ + Name: "Plate Armor", + Sprite: assets.MustBeValidImage(assets.PlateArmor, "PlateArmor"), + }, + Defense: 15, + ArmorClass: 18, + }, +} diff --git a/items/armors/armors.go b/items/armors/armors.go deleted file mode 100644 index 70905a5..0000000 --- a/items/armors/armors.go +++ /dev/null @@ -1,65 +0,0 @@ -package armors - -import ( - "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/assets" - "github.com/kensonjohnson/roguelike-game-go/internal/logger" -) - -type armorData struct { - Name string - Defense int - ArmorClass int - Sprite *ebiten.Image -} - -type ArmorId int - -const ( - LinenShirt ArmorId = iota - PaddedArmor - Bones - PlateArmor -) - -type armorList []armorData - -// MAKE SURE THAT THIS NUMBER MATCHES THE NUMBER OF ARMORS DEFINED! -var Data = make(armorList, 4) - -func init() { - Data[LinenShirt] = armorData{ - Name: "Linen Shirt", - Defense: 1, - ArmorClass: 1, - Sprite: mustBeValidImage(assets.LinenShirt, "LinenShirt"), - } - - Data[PaddedArmor] = armorData{ - Name: "Padded Armor", - Defense: 5, - ArmorClass: 6, - Sprite: mustBeValidImage(assets.PaddedArmor, "PaddedArmor"), - } - - Data[Bones] = armorData{ - Name: "Bone", - Defense: 3, - ArmorClass: 4, - Sprite: mustBeValidImage(assets.Bones, "Bones"), - } - - Data[PlateArmor] = armorData{ - Name: "Plate Armor", - Defense: 15, - ArmorClass: 18, - Sprite: mustBeValidImage(assets.PlateArmor, "PlateArmor"), - } -} - -func mustBeValidImage(image *ebiten.Image, name string) *ebiten.Image { - if image == nil { - logger.ErrorLogger.Panicf("%s asset not loaded!", name) - } - return image -} diff --git a/items/consumables.go b/items/consumables.go new file mode 100644 index 0000000..1757478 --- /dev/null +++ b/items/consumables.go @@ -0,0 +1,126 @@ +package items + +import ( + "github.com/kensonjohnson/roguelike-game-go/assets" +) + +type ConsumableData struct { + ItemData + AmountHeal int +} + +type ConsumablesId int + +type consumables struct { + HealthPotion ConsumableData + GreatHealthPotion ConsumableData + RoyalHealthPotion ConsumableData + + Apple ConsumableData + Bread ConsumableData + Carrot ConsumableData + Cheese ConsumableData + Egg ConsumableData + Fish ConsumableData + Ham ConsumableData + Milk ConsumableData + Pear ConsumableData + Steak ConsumableData +} + +var Consumables = consumables{ + HealthPotion: ConsumableData{ + ItemData: ItemData{ + Name: "Health Potion", + Sprite: assets.MustBeValidImage(assets.WorldHealthPotion, "WorldHealthPotion"), + }, + AmountHeal: 10, + }, + + GreatHealthPotion: ConsumableData{ + ItemData: ItemData{ + Name: "Great Heath Potion", + Sprite: assets.MustBeValidImage(assets.WorldGreatHealthPotion, "WorldGreatHealthPotion"), + }, + AmountHeal: 20, + }, + + RoyalHealthPotion: ConsumableData{ + ItemData: ItemData{ + Name: "Royal Heath Potion", + Sprite: assets.MustBeValidImage(assets.WorldRoyalHealthPotion, "WorldRoyalHealthPotion"), + }, + AmountHeal: 40, + }, + + Apple: ConsumableData{ + ItemData: ItemData{ + Name: "Apple", + Sprite: assets.MustBeValidImage(assets.Apple, "Apple"), + }, + AmountHeal: 3, + }, + Bread: ConsumableData{ + ItemData: ItemData{ + Name: "Bread", + Sprite: assets.MustBeValidImage(assets.Bread, "Bread"), + }, + AmountHeal: 5, + }, + Carrot: ConsumableData{ + ItemData: ItemData{ + Name: "Carrot", + Sprite: assets.MustBeValidImage(assets.Carrot, "Carrot"), + }, + AmountHeal: 2, + }, + Cheese: ConsumableData{ + ItemData: ItemData{ + Name: "Cheese", + Sprite: assets.MustBeValidImage(assets.Cheese, "Cheese"), + }, + AmountHeal: 6, + }, + Egg: ConsumableData{ + ItemData: ItemData{ + Name: "Egg", + Sprite: assets.MustBeValidImage(assets.Egg, "Egg"), + }, + AmountHeal: 6, + }, + Fish: ConsumableData{ + ItemData: ItemData{ + Name: "Fish", + Sprite: assets.MustBeValidImage(assets.Fish, "Fish"), + }, + AmountHeal: 9, + }, + Ham: ConsumableData{ + ItemData: ItemData{ + Name: "Ham", + Sprite: assets.MustBeValidImage(assets.Ham, "Ham"), + }, + AmountHeal: 12, + }, + Milk: ConsumableData{ + ItemData: ItemData{ + Name: "Milk", + Sprite: assets.MustBeValidImage(assets.Milk, "Milk"), + }, + AmountHeal: 6, + }, + Pear: ConsumableData{ + ItemData: ItemData{ + Name: "Pear", + Sprite: assets.MustBeValidImage(assets.Pear, "Pear"), + }, + AmountHeal: 3, + }, + Steak: ConsumableData{ + ItemData: ItemData{ + Name: "Steak", + Sprite: assets.MustBeValidImage(assets.Steak, "Steak"), + }, + AmountHeal: 15, + }, +} diff --git a/items/consumables/consumables.go b/items/consumables/consumables.go deleted file mode 100644 index 8ca8d4c..0000000 --- a/items/consumables/consumables.go +++ /dev/null @@ -1,41 +0,0 @@ -package consumables - -import ( - "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/assets" - "github.com/kensonjohnson/roguelike-game-go/internal/logger" -) - -type consumable struct { - Name string - AmountHeal int - Sprite *ebiten.Image -} - -type consumablesList []*consumable - -type ConsumablesId int - -const ( - HealthPotion ConsumablesId = iota -) - -// MAKE SURE THAT THIS NUMBER MATCHES THE NUMBER OF WEAPONS DEFINED! -var Data consumablesList = make(consumablesList, 1) - -func init() { - - Data[HealthPotion] = &consumable{ - Name: "Health Potion", - AmountHeal: 10, - Sprite: mustBeValidImage(assets.WorldHealthPotion, "WorldHealthPotion"), - } - -} - -func mustBeValidImage(image *ebiten.Image, name string) *ebiten.Image { - if image == nil { - logger.ErrorLogger.Panicf("%s asset not loaded!", name) - } - return image -} diff --git a/items/items.go b/items/items.go new file mode 100644 index 0000000..2551650 --- /dev/null +++ b/items/items.go @@ -0,0 +1,8 @@ +package items + +import "github.com/hajimehoshi/ebiten/v2" + +type ItemData struct { + Name string + Sprite *ebiten.Image +} diff --git a/items/valuables.go b/items/valuables.go new file mode 100644 index 0000000..4b8d832 --- /dev/null +++ b/items/valuables.go @@ -0,0 +1,47 @@ +package items + +import ( + "github.com/kensonjohnson/roguelike-game-go/assets" + "github.com/kensonjohnson/roguelike-game-go/internal/engine" +) + +type ValuableData struct { + ItemData + Value int +} + +type valuables struct { + Alcohol *ValuableData +} + +var Valuables valuables = valuables{ + Alcohol: &ValuableData{ + ItemData: ItemData{ + Name: "Alcohol", + Sprite: assets.WorldAlcohol, + }, + Value: 20, + }, +} + +func (v valuables) SmallCoin() *ValuableData { + value := engine.GetRandomBetween(3, 11) + return &ValuableData{ + ItemData: ItemData{ + Name: "some coins", + Sprite: assets.WorldSmallCoin, + }, + Value: value, + } +} + +func (v valuables) CoinStack() *ValuableData { + value := engine.GetRandomBetween(15, 35) + return &ValuableData{ + ItemData: ItemData{ + Name: "a stack of coins", + Sprite: assets.WorldCoinStack, + }, + Value: value, + } +} diff --git a/items/weapons.go b/items/weapons.go new file mode 100644 index 0000000..fa4741b --- /dev/null +++ b/items/weapons.go @@ -0,0 +1,41 @@ +package items + +import ( + "github.com/kensonjohnson/roguelike-game-go/assets" +) + +type WeaponData struct { + ItemData + ActionText string + MinimumDamage int + MaximumDamage int + ToHitBonus int +} + +type weapons struct { + ShortSword WeaponData + BattleAxe WeaponData +} + +var Weapons = weapons{ + ShortSword: WeaponData{ + ItemData: ItemData{ + Name: "Short Sword", + Sprite: assets.MustBeValidImage(assets.ShortSword, "ShortSword"), + }, + ActionText: "swings a short sword at", + MinimumDamage: 2, + MaximumDamage: 6, + ToHitBonus: 0, + }, + BattleAxe: WeaponData{ + ItemData: ItemData{ + Name: "Battle Axe", + Sprite: assets.MustBeValidImage(assets.BattleAxe, "BattleAxe"), + }, + ActionText: "cleaves a battle axe at", + MinimumDamage: 10, + MaximumDamage: 20, + ToHitBonus: 3, + }, +} diff --git a/items/weapons/weapons.go b/items/weapons/weapons.go deleted file mode 100644 index 78af3f8..0000000 --- a/items/weapons/weapons.go +++ /dev/null @@ -1,56 +0,0 @@ -package weapons - -import ( - "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/assets" - "github.com/kensonjohnson/roguelike-game-go/internal/logger" -) - -type weaponData struct { - Name string - ActionText string - MinimumDamage int - MaximumDamage int - ToHitBonus int - Sprite *ebiten.Image -} - -type weaponList []weaponData - -type WeaponId int - -const ( - ShortSword WeaponId = iota - BattleAxe -) - -// MAKE SURE THAT THIS NUMBER MATCHES THE NUMBER OF WEAPONS DEFINED! -var Data weaponList = make(weaponList, 2) - -func init() { - - Data[ShortSword] = weaponData{ - Name: "Short Sword", - ActionText: "swings a short sword at", - MinimumDamage: 2, - MaximumDamage: 6, - ToHitBonus: 0, - Sprite: mustBeValidImage(assets.ShortSword, "ShortSword"), - } - - Data[BattleAxe] = weaponData{ - Name: "Battle Axe", - ActionText: "cleaves a battle axe at", - MinimumDamage: 10, - MaximumDamage: 20, - ToHitBonus: 3, - Sprite: mustBeValidImage(assets.BattleAxe, "BattleAxe"), - } -} - -func mustBeValidImage(image *ebiten.Image, name string) *ebiten.Image { - if image == nil { - logger.ErrorLogger.Panicf("%s asset not loaded!", name) - } - return image -} diff --git a/main.go b/main.go index b1b9813..90794ca 100644 --- a/main.go +++ b/main.go @@ -2,11 +2,12 @@ package main import ( "flag" + "log" + "log/slog" "github.com/hajimehoshi/ebiten/v2" "github.com/kensonjohnson/roguelike-game-go/assets" - "github.com/kensonjohnson/roguelike-game-go/config" - "github.com/kensonjohnson/roguelike-game-go/internal/logger" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/kensonjohnson/roguelike-game-go/system" "github.com/kensonjohnson/roguelike-game-go/system/scene" ) @@ -17,6 +18,7 @@ type Game struct { func (g *Game) configure() { g.sceneManager = scene.SceneManager + g.sceneManager.Setup() g.sceneManager.GoTo(&scene.TitleScene{ ImageBackground: assets.Floor, PixelWidth: config.ScreenWidth * config.TileWidth, @@ -48,17 +50,17 @@ func main() { if DebugOn != nil && *DebugOn { ebiten.SetVsyncEnabled(false) system.Debug.On = true - logger.SetDebug(*DebugOn) + slog.SetLogLoggerLevel(slog.LevelDebug) } + log.SetFlags(log.Lshortfile) + g := &Game{} g.configure() - if logger.DebugOn { - logger.DebugLogger.Println("Starting Game") - } + slog.Debug("Starting Game") if err := ebiten.RunGame(g); err != nil { - logger.ErrorLogger.Panic(err) + log.Panic(err) } } diff --git a/system/action/monster.go b/system/action/monster.go index 7ecf922..e60e7f9 100644 --- a/system/action/monster.go +++ b/system/action/monster.go @@ -1,24 +1,23 @@ package action import ( - "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/engine/pathing" + "github.com/kensonjohnson/roguelike-game-go/internal/engine/pathing" "github.com/kensonjohnson/roguelike-game-go/system/combat" - "github.com/yohamta/donburi" "github.com/yohamta/donburi/ecs" ) func TakeMonsterAction(ecs *ecs.ECS) { // Grab level data - entry := archetype.LevelTag.MustFirst(ecs.World) + entry := tags.LevelTag.MustFirst(ecs.World) level := component.Level.Get(entry) // Grab player data - playerEntry := archetype.PlayerTag.MustFirst(ecs.World) + playerEntry := tags.PlayerTag.MustFirst(ecs.World) playerPos := component.Position.Get(playerEntry) - archetype.MonsterTag.Each(ecs.World, func(entry *donburi.Entry) { + for entry = range tags.MonsterTag.Iter(ecs.World) { position := component.Position.Get(entry) sprite := component.Sprite.Get(entry) monsterVision := component.Fov.Get(entry).VisibleTiles @@ -49,5 +48,5 @@ func TakeMonsterAction(ecs *ecs.ECS) { } } } - }) + } } diff --git a/system/action/player.go b/system/action/player.go index 802b2b1..1957f1f 100644 --- a/system/action/player.go +++ b/system/action/player.go @@ -1,16 +1,14 @@ package action import ( - "fmt" - "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" "github.com/kensonjohnson/roguelike-game-go/event" "github.com/kensonjohnson/roguelike-game-go/system/combat" "github.com/yohamta/donburi" "github.com/yohamta/donburi/ecs" - "github.com/yohamta/donburi/filter" ) func TakePlayerAction(ecs *ecs.ECS) bool { @@ -32,6 +30,9 @@ func TakePlayerAction(ecs *ecs.ECS) bool { if ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyS) { moveY += 1 } + if inpututil.IsKeyJustPressed(ebiten.KeyI) { + event.OpenInventoryEvent.Publish(ecs.World, event.OpenInventory{}) + } if ebiten.IsKeyPressed(ebiten.KeyQ) { turnTaken = true } @@ -45,11 +46,11 @@ func TakePlayerAction(ecs *ecs.ECS) bool { } // Grab current level - levelEntry := archetype.LevelTag.MustFirst(ecs.World) + levelEntry := tags.LevelTag.MustFirst(ecs.World) level := component.Level.Get(levelEntry) // Grab player data - playerEntry := archetype.PlayerTag.MustFirst(ecs.World) + playerEntry := tags.PlayerTag.MustFirst(ecs.World) position := component.Position.Get(playerEntry) sprite := component.Sprite.Get(playerEntry) vision := component.Fov.Get(playerEntry) @@ -72,33 +73,13 @@ func TakePlayerAction(ecs *ecs.ECS) bool { // Update the player's field of view vision.VisibleTiles.Compute(level, position.X, position.Y, 8) // Update any discoverable entities - component.Discoverable.Each(ecs.World, func(entry *donburi.Entry) { + for entry := range component.Discoverable.Iter(ecs.World) { discoverablePosition := component.Position.Get(entry) if vision.VisibleTiles.IsVisible(discoverablePosition.X, discoverablePosition.Y) { discoverable := component.Discoverable.Get(entry) discoverable.SeenByPlayer = true } - }) - - query := donburi.NewQuery( - filter.Contains( - component.ItemId, - component.Position, - component.Name, - )) - - query.Each(ecs.World, func(entry *donburi.Entry) { - itemPosition := component.Position.Get(entry) - if position.X == itemPosition.X && position.Y == itemPosition.Y { - // The character has moved on top of a pickup - archetype.RemoveItemFromWorld(entry) - itemName := component.Name.Get(entry) - playerMessages := component.UserMessage.Get(playerEntry) - playerMessages.WorldInteractionMessage = fmt.Sprintf("Picked up %s!", itemName.Value) - // TODO: add pickup message to UIs - // TODO: place in player's inventory - } - }) + } if tile.TileType == component.STAIR_DOWN { // Move to the next level @@ -113,12 +94,12 @@ func TakePlayerAction(ecs *ecs.ECS) bool { Y: position.Y + moveY, } var monsterEntry *donburi.Entry - archetype.MonsterTag.Each(ecs.World, func(entry *donburi.Entry) { + for entry := range tags.MonsterTag.Iter(ecs.World) { position := component.Position.Get(entry) if position.IsEqual(&enemyPosition) { monsterEntry = entry } - }) + } combat.AttackSystem(ecs.World, playerEntry, monsterEntry) } diff --git a/system/background.go b/system/background.go index 53d0be2..307b353 100644 --- a/system/background.go +++ b/system/background.go @@ -2,23 +2,23 @@ package system import ( "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/config" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/yohamta/donburi/ecs" ) func (r *render) DrawBackground(ecs *ecs.ECS, screen *ebiten.Image) { - entry := archetype.LevelTag.MustFirst(ecs.World) + entry := tags.LevelTag.MustFirst(ecs.World) level := component.Level.Get(entry) - entry = archetype.CameraTag.MustFirst(ecs.World) + entry = tags.CameraTag.MustFirst(ecs.World) camera := component.Camera.Get(entry) if !level.Redraw { camera.MainCamera.Draw(r.backgroundImage, camera.CamImageOptions, screen) return } r.backgroundImage.Clear() - entry = archetype.PlayerTag.MustFirst(ecs.World) + entry = tags.PlayerTag.MustFirst(ecs.World) playerVision := component.Fov.Get(entry).VisibleTiles maxTiles := config.ScreenWidth * (config.ScreenHeight - config.UIHeight) diff --git a/system/camera.go b/system/camera.go index e4ef0c3..8e0e67a 100644 --- a/system/camera.go +++ b/system/camera.go @@ -1,9 +1,9 @@ package system import ( - "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/config" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/yohamta/donburi/ecs" ) @@ -12,9 +12,9 @@ type camera struct{} var Camera = &camera{} func (c *camera) Update(ecs *ecs.ECS) { - entry := archetype.CameraTag.MustFirst(ecs.World) + entry := tags.CameraTag.MustFirst(ecs.World) camera := component.Camera.Get(entry) - entry = archetype.PlayerTag.MustFirst(ecs.World) + entry = tags.PlayerTag.MustFirst(ecs.World) position := component.Position.Get(entry) camera.MainCamera.LookAt( diff --git a/system/combat/combat.go b/system/combat/combat.go index 10711f7..8876d67 100644 --- a/system/combat/combat.go +++ b/system/combat/combat.go @@ -3,9 +3,9 @@ package combat import ( "fmt" - "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/engine" + "github.com/kensonjohnson/roguelike-game-go/internal/engine" "github.com/yohamta/donburi" ) @@ -40,7 +40,7 @@ func AttackSystem(world donburi.World, attacker, defender *donburi.Entry) { if defenderHealth.CurrentHealth <= 0 { defenderMessages.DeadMessage = fmt.Sprintf("%s has died!\n", defenderName.Value) } - entry := archetype.CameraTag.MustFirst(world) + entry := tags.CameraTag.MustFirst(world) camera := component.Camera.Get(entry) camera.MainCamera.AddTrauma(0.2) } else { diff --git a/system/debug.go b/system/debug.go index 7e8da5f..bf44a9b 100644 --- a/system/debug.go +++ b/system/debug.go @@ -5,7 +5,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" - "github.com/kensonjohnson/roguelike-game-go/config" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/yohamta/donburi/ecs" donburiDebug "github.com/yohamta/donburi/features/debug" ) diff --git a/system/inventory.go b/system/inventory.go new file mode 100644 index 0000000..71db103 --- /dev/null +++ b/system/inventory.go @@ -0,0 +1,172 @@ +package system + +import ( + "image/color" + "log/slog" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/kensonjohnson/roguelike-game-go/internal/colors" + "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/yohamta/donburi/ecs" +) + +type inventoryUi struct { + open bool +} + +var InventoryUI = inventoryUi{open: false} + +func (i *inventoryUi) Update(ecs *ecs.ECS) { + if !i.open { + return + } + + if inpututil.IsKeyJustPressed(ebiten.KeyI) || inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + slog.Debug("Close Inventory") + Turn.TurnState = PlayerTurn + i.open = false + } +} + +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, + ) + options.GeoM.Reset() + options.GeoM.Translate(float64(5*config.TileWidth), float64(25*config.TileHeight)) + screen.DrawImage(image, options) +} + +func (i *inventoryUi) Open() { + i.open = true +} +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 { + + image := ebiten.NewImage(w, h) + corner := makeCornerImage(border, fill) + 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 == 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) + } + + // 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) + + return image +} + +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) + } + image.DrawImage(block, options) + } + } + + return image +} + +func degreesToRadians(degrees int) float64 { + return float64(degrees) * math.Pi / 180 +} + +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}, +} diff --git a/layer/layer.go b/system/layer/layer.go similarity index 100% rename from layer/layer.go rename to system/layer/layer.go diff --git a/system/minimap.go b/system/minimap.go index 3c04778..332690a 100644 --- a/system/minimap.go +++ b/system/minimap.go @@ -5,17 +5,17 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" - "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/config" - "github.com/yohamta/donburi" + "github.com/kensonjohnson/roguelike-game-go/internal/colors" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/yohamta/donburi/ecs" ) const blipSize = 4 func DrawMinimap(ecs *ecs.ECS, screen *ebiten.Image) { - entry := archetype.LevelTag.MustFirst(ecs.World) + 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. @@ -33,16 +33,17 @@ func DrawMinimap(ecs *ecs.ECS, screen *ebiten.Image) { } if tile.TileType == component.WALL { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, color.RGBA{R: 202, G: 146, B: 74, A: 255}, false) + vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.Peru, false) } else if tile.TileType == component.STAIR_DOWN { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, color.RGBA{R: 46, G: 204, B: 113, A: 255}, false) - } else { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, color.RGBA{R: 178, G: 182, B: 194, A: 255}, false) + vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.Lime, false) + } else /* floor */ { + vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.LightGray, false) } } // Draw all discovered entities - component.Discoverable.Each(ecs.World, func(entry *donburi.Entry) { + for entry = range component.Discoverable.Iter(ecs.World) { + position := component.Position.Get(entry) if !level.InBounds(position.X, position.Y) { return @@ -52,16 +53,16 @@ func DrawMinimap(ecs *ecs.ECS, screen *ebiten.Image) { y := startingYPixel + (position.Y * blipSize) if component.Discoverable.Get(entry).SeenByPlayer { - if entry.HasComponent(component.ItemId) { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, color.RGBA{R: 15, G: 10, B: 222, A: 255}, false) + if entry.HasComponent(tags.ItemTag) { + vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.DeepSkyBlue, false) } else { - vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, color.RGBA{R: 255, G: 0, B: 0, A: 255}, false) + vector.DrawFilledRect(screen, float32(x), float32(y), blipSize, blipSize, colors.Red, false) } } - }) + } // Draw the player - playerEntry := archetype.PlayerTag.MustFirst(ecs.World) + playerEntry := tags.PlayerTag.MustFirst(ecs.World) playerPosition := component.Position.Get(playerEntry) x := startingXPixel + (playerPosition.X * blipSize) y := startingYPixel + (playerPosition.Y * blipSize) diff --git a/system/render.go b/system/render.go index aae0c3c..e4977e1 100644 --- a/system/render.go +++ b/system/render.go @@ -2,22 +2,31 @@ package system import ( "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" - "github.com/kensonjohnson/roguelike-game-go/config" + "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/norendren/go-fov/fov" "github.com/yohamta/donburi" "github.com/yohamta/donburi/ecs" "github.com/yohamta/donburi/filter" ) type render struct { - query *donburi.Query + enemyQuery *donburi.Query + pickupsQuery *donburi.Query backgroundImage *ebiten.Image } var Render = &render{ - query: donburi.NewQuery( + enemyQuery: donburi.NewQuery( filter.Contains( + tags.MonsterTag, + component.Sprite, + component.Position, + )), + pickupsQuery: donburi.NewQuery( + filter.Contains( + tags.ItemTag, component.Position, component.Sprite, )), @@ -25,28 +34,43 @@ var Render = &render{ } func (r *render) Draw(ecs *ecs.ECS, screen *ebiten.Image) { - entry := archetype.PlayerTag.MustFirst(ecs.World) - playerVision := component.Fov.Get(entry).VisibleTiles - entry = archetype.CameraTag.MustFirst(ecs.World) + playerEntry := tags.PlayerTag.MustFirst(ecs.World) + playerVision := component.Fov.Get(playerEntry).VisibleTiles + entry := tags.CameraTag.MustFirst(ecs.World) camera := component.Camera.Get(entry) - r.query.Each(ecs.World, func(entry *donburi.Entry) { - position := component.Position.Get(entry) - sprite := component.Sprite.Get(entry) - - if playerVision.IsVisible(position.X, position.Y) { - camera.CamImageOptions.GeoM.Reset() - if sprite.Animating { - offsetX, offsetY := sprite.GetAnimationStep() - camera.CamImageOptions.GeoM.Translate(float64(position.X*config.TileWidth)+offsetX, float64(position.Y*config.TileHeight)+offsetY) - } else { - camera.CamImageOptions.GeoM.Translate(float64(position.X*config.TileWidth), float64(position.Y*config.TileHeight)) - } - camera.MainCamera.Draw(sprite.Image, camera.CamImageOptions, screen) - } - }) + for entry = range r.pickupsQuery.Iter(ecs.World) { + renderEntity(screen, camera, entry, playerVision) + } + + for entry = range r.enemyQuery.Iter(ecs.World) { + renderEntity(screen, camera, entry, playerVision) + } + + renderEntity(screen, camera, playerEntry, playerVision) camera.CamImageOptions.GeoM.Reset() camera.CamImageOptions.GeoM.Translate(0, 0) camera.MainCamera.Draw(camera.CamScreen, camera.CamImageOptions, screen) } + +func renderEntity( + screen *ebiten.Image, + camera *component.CameraData, + entry *donburi.Entry, + playerVision *fov.View, +) { + position := component.Position.Get(entry) + sprite := component.Sprite.Get(entry) + + if playerVision.IsVisible(position.X, position.Y) { + camera.CamImageOptions.GeoM.Reset() + if sprite.Animating { + offsetX, offsetY := sprite.GetAnimationStep() + camera.CamImageOptions.GeoM.Translate(float64(position.X*config.TileWidth)+offsetX, float64(position.Y*config.TileHeight)+offsetY) + } else { + camera.CamImageOptions.GeoM.Translate(float64(position.X*config.TileWidth), float64(position.Y*config.TileHeight)) + } + camera.MainCamera.Draw(sprite.Image, camera.CamImageOptions, screen) + } +} diff --git a/system/scene/levelscene.go b/system/scene/levelscene.go index 6c7d69a..5ebadd4 100644 --- a/system/scene/levelscene.go +++ b/system/scene/levelscene.go @@ -1,105 +1,129 @@ package scene import ( + "log/slog" + "github.com/hajimehoshi/ebiten/v2" "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" "github.com/kensonjohnson/roguelike-game-go/event" - "github.com/kensonjohnson/roguelike-game-go/items/armors" - "github.com/kensonjohnson/roguelike-game-go/items/weapons" - "github.com/kensonjohnson/roguelike-game-go/layer" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/kensonjohnson/roguelike-game-go/system" + "github.com/kensonjohnson/roguelike-game-go/system/layer" "github.com/yohamta/donburi" "github.com/yohamta/donburi/ecs" + "github.com/yohamta/donburi/features/events" ) type LevelScene struct { - ecs ecs.ECS + ecs ecs.ECS + ready bool } -func (level *LevelScene) Update() { - level.ecs.Update() - event.ProgressLevelEvent.ProcessEvents(level.ecs.World) +func (ls *LevelScene) Update() { + ls.ecs.Update() + events.ProcessAllEvents(ls.ecs.World) } -func (level *LevelScene) Draw(screen *ebiten.Image) { - level.ecs.Draw(screen) +func (ls *LevelScene) Draw(screen *ebiten.Image) { + ls.ecs.Draw(screen) } -func CreateFirstLevel() *LevelScene { - level := &LevelScene{} - level.configureECS(createWorld()) - return level +func (ls *LevelScene) Ready() bool { + return ls.ready } -func createWorld() donburi.World { - world := donburi.NewWorld() +func (ls *LevelScene) Setup(world donburi.World) { + ls.ready = false - // Create current level - archetype.GenerateLevel(world) + slog.Debug("LevelScene setup") - // Create the UI - archetype.CreateNewUI(world) + go func() { - // Create the camera - archetype.CreateNewCamera(world) + levelData := archetype.GenerateLevel(world) - return world -} + if _, ok := tags.UITag.First(world); !ok { + archetype.CreateNewUI(world) + } -func progressLevel(world donburi.World, eventData event.ProgressLevel) { + playerEntry := tags.PlayerTag.MustFirst(world) + playerPosition := component.Position.Get(playerEntry) + startingRoom := levelData.Rooms[0] + playerPosition.X, playerPosition.Y = startingRoom.Center() - // Create a new world - newWorld := createWorld() + playerSprite := component.Sprite.Get(playerEntry) + playerSprite.OffestX = 0 + playerSprite.OffestY = 0 - // Apply the player's data to the new world - copyPlayerInstance(world, newWorld) + component.Fov.Get(playerEntry). + VisibleTiles.Compute(levelData, playerPosition.X, playerPosition.Y, 8) - level := &LevelScene{} - level.configureECS(newWorld) + // FIX: This is a workaround to the kamera camera keeping a 'memory' of + // previous location, even after lerp is turned off. + archetype.ReplaceCamera( + world, + float64((playerPosition.X*config.TileWidth)+config.TileWidth/2), + float64((playerPosition.Y*config.TileHeight)+config.TileHeight/2), + ) - SceneManager.GoTo(level) + ls.configureECS(world) + + ls.ready = true + }() } -func (l *LevelScene) configureECS(world donburi.World) { - l.ecs = *ecs.NewECS(world) +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() + } + + for entry := range tags.PickupTag.Iter(ls.ecs.World) { + slog.Debug("Removing entry.", "entry", entry.String()) + entry.Remove() + } + + ls.ready = true + }() +} + +func (ls *LevelScene) configureECS(world donburi.World) { + ls.ecs = *ecs.NewECS(world) // Add systems - l.ecs.AddSystem(system.Camera.Update) - l.ecs.AddSystem(system.Turn.Update) - l.ecs.AddSystem(system.UI.Update) + ls.ecs.AddSystem(system.Camera.Update) + ls.ecs.AddSystem(system.Turn.Update) + ls.ecs.AddSystem(system.UI.Update) + ls.ecs.AddSystem(system.InventoryUI.Update) // Add renderers - l.ecs.AddRenderer(layer.Background, system.Render.DrawBackground) - l.ecs.AddRenderer(layer.Foreground, system.Render.Draw) - l.ecs.AddRenderer(layer.UI, system.UI.Draw) - l.ecs.AddRenderer(layer.UI, system.DrawMinimap) + 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.InventoryUI.Draw) if system.Debug.On { - l.ecs.AddRenderer(layer.UI, system.Debug.Draw) + ls.ecs.AddRenderer(layer.UI, system.Debug.Draw) } // Add event listeners - event.ProgressLevelEvent.Subscribe(l.ecs.World, progressLevel) + event.ProgressLevelEvent.Subscribe(ls.ecs.World, progressLevel) + event.OpenInventoryEvent.Subscribe(ls.ecs.World, openInventory) } -func copyPlayerInstance( - oldWorld donburi.World, - newWorld donburi.World, -) { - currentPlayerEntry := archetype.PlayerTag.MustFirst(oldWorld) - currentPlayerHealth := component.Health.Get(currentPlayerEntry) - currentPlayerEquipment := component.Equipment.Get(currentPlayerEntry) - - weaponId := component.ItemId.Get(currentPlayerEquipment.Weapon) - armorId := component.ItemId.Get(currentPlayerEquipment.Armor) - - playerEntry := archetype.PlayerTag.MustFirst(newWorld) - component.Health.SetValue(playerEntry, *currentPlayerHealth) - component.Equipment.SetValue(playerEntry, component.EquipmentData{ - Weapon: archetype.CreateNewWeapon(newWorld, weapons.WeaponId(weaponId.Id)), - // Sheild: currentPlayerEquipment.Sheild, - // Gloves: currentPlayerEquipment.Gloves, - Armor: archetype.CreateNewArmor(newWorld, armors.ArmorId(armorId.Id)), - // Boots: currentPlayerEquipment.Boots, - }) +func progressLevel(world donburi.World, eventData event.ProgressLevel) { + slog.Debug("Progress Level") + newLevelScene := &LevelScene{} + SceneManager.GoTo(newLevelScene) +} +func openInventory(world donburi.World, eventData event.OpenInventory) { + slog.Debug("Open Inventory") + system.Turn.TurnState = system.UIOpen + system.InventoryUI.Open() } diff --git a/system/scene/scenemanager.go b/system/scene/scenemanager.go index e1c41e6..b13bff0 100644 --- a/system/scene/scenemanager.go +++ b/system/scene/scenemanager.go @@ -2,76 +2,114 @@ package scene import ( "image/color" + "log/slog" "github.com/hajimehoshi/ebiten/v2" - "github.com/kensonjohnson/roguelike-game-go/config" -) - -var ( - transitionFrom = ebiten.NewImage(config.ScreenWidth*config.TileWidth, config.ScreenHeight*config.TileHeight) - transitionTo = ebiten.NewImage(config.ScreenWidth*config.TileWidth, config.ScreenHeight*config.TileHeight) + "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/kensonjohnson/roguelike-game-go/items" + "github.com/yohamta/donburi" ) type Scene interface { Update() - Draw(screen *ebiten.Image) + Draw(*ebiten.Image) + Setup(donburi.World) + Teardown() + Ready() bool } const transitionMaxCount = 30 type SceneManagerData struct { - current Scene - next Scene - transitionCount int + current Scene + next Scene + fadeOut bool + transitionCount int + transitionTo *ebiten.Image + transitionFrom *ebiten.Image + transitionImagesCached bool + world donburi.World +} + +var SceneManager = &SceneManagerData{ + transitionTo: ebiten.NewImage(config.ScreenWidth*config.TileWidth, config.ScreenHeight*config.TileHeight), + transitionFrom: ebiten.NewImage(config.ScreenWidth*config.TileWidth, config.ScreenHeight*config.TileHeight), } -var SceneManager = &SceneManagerData{} +func (sm *SceneManagerData) Setup() { + slog.Debug("SceneManager Setup") + sm.world = donburi.NewWorld() + archetype.CreateNewPlayer(sm.world, items.Weapons.BattleAxe, items.Armor.PlateArmor) + archetype.CreateNewCamera(sm.world) +} + +func (sm *SceneManagerData) Update() { + if sm.next == nil && sm.transitionCount == 0 { + sm.current.Update() + return + } + + sm.transitionCount-- + if sm.transitionCount > 0 { + return + } -func (s *SceneManagerData) Update() { - if s.transitionCount == 0 { - s.current.Update() + if sm.fadeOut { + slog.Debug("Running Teardown on current scene") + sm.fadeOut = false + sm.current.Teardown() return } - s.transitionCount-- - if s.transitionCount > 0 { + if sm.transitionCount != 0 && sm.next != nil && sm.current.Ready() { + slog.Debug("Running Setup on next scene") + sm.current = sm.next + sm.next = nil + sm.current.Setup(sm.world) return } - if s.next != nil { - s.current = s.next - s.next = nil - s.transitionCount = transitionMaxCount + if sm.transitionCount != 0 && sm.next == nil && sm.current.Ready() { + sm.transitionCount = transitionMaxCount + sm.transitionImagesCached = false } } -func (s *SceneManagerData) Draw(screen *ebiten.Image) { - if s.transitionCount == 0 { - s.current.Draw(screen) +func (sm *SceneManagerData) Draw(screen *ebiten.Image) { + + if sm.next == nil && sm.transitionCount == 0 { + sm.current.Draw(screen) return } - if s.next != nil { - s.current.Draw(transitionFrom) - transitionTo.Fill(color.Black) - screen.DrawImage(transitionFrom, nil) - } else { - s.current.Draw(transitionTo) - transitionFrom.Fill(color.Black) - screen.DrawImage(transitionFrom, nil) + if sm.fadeOut && !sm.transitionImagesCached { + sm.current.Draw(sm.transitionFrom) + sm.transitionTo.Fill(color.Black) + sm.transitionImagesCached = true + } + + if !sm.fadeOut && !sm.transitionImagesCached { + sm.current.Draw(sm.transitionTo) + sm.transitionFrom.Fill(color.Black) + sm.transitionImagesCached = true } - alpha := 1 - float32(s.transitionCount)/float32(transitionMaxCount) + + screen.DrawImage(sm.transitionFrom, nil) + alpha := 1 - float32(sm.transitionCount)/float32(transitionMaxCount) op := &ebiten.DrawImageOptions{} op.ColorScale.ScaleAlpha(alpha) - screen.DrawImage(transitionTo, op) + screen.DrawImage(sm.transitionTo, op) } -func (s *SceneManagerData) GoTo(scene Scene) { - if s.current == nil { - s.current = scene +func (sm *SceneManagerData) GoTo(scene Scene) { + if sm.current == nil { + sm.current = scene } else { - s.next = scene - s.transitionCount = transitionMaxCount + sm.next = scene + sm.fadeOut = true + sm.transitionCount = transitionMaxCount + sm.transitionImagesCached = false } } diff --git a/system/scene/titlescene.go b/system/scene/titlescene.go index c15933d..5e1fe96 100644 --- a/system/scene/titlescene.go +++ b/system/scene/titlescene.go @@ -2,12 +2,15 @@ package scene import ( "image/color" + "log/slog" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text/v2" "github.com/kensonjohnson/roguelike-game-go/assets" - "github.com/kensonjohnson/roguelike-game-go/config" + "github.com/kensonjohnson/roguelike-game-go/internal/colors" + "github.com/kensonjohnson/roguelike-game-go/internal/config" + "github.com/yohamta/donburi" ) type TitleScene struct { @@ -15,15 +18,15 @@ type TitleScene struct { ImageBackground *ebiten.Image PixelWidth int PixelHeight int + ready bool } func (s *TitleScene) Update() { s.count++ if inpututil.IsKeyJustPressed(ebiten.KeySpace) { - SceneManager.GoTo(CreateFirstLevel()) + SceneManager.GoTo(&LevelScene{}) return } - } const scale = 4 @@ -40,7 +43,7 @@ func (s *TitleScene) Draw(screen *ebiten.Image) { screen, message, x, y, - color.RGBA{R: 178, G: 182, B: 194, A: 255}, + colors.DarkGray, text.AlignCenter, text.AlignStart, ) @@ -57,6 +60,20 @@ func (s *TitleScene) Draw(screen *ebiten.Image) { drawOrc(screen, x, y) } +func (s *TitleScene) Ready() bool { + return s.ready +} + +func (s *TitleScene) Teardown() { + slog.Debug("TitleScene teardown") + s.ready = true +} + +func (s *TitleScene) Setup(world donburi.World) { + slog.Debug("TitleScene setup") + s.ready = true +} + func (s *TitleScene) drawTitleBackground(screen *ebiten.Image, count int) { options := &ebiten.DrawImageOptions{} @@ -74,7 +91,7 @@ func (s *TitleScene) drawTitleBackground(screen *ebiten.Image, count int) { } func drawLogo(screen *ebiten.Image, str string, x, y float64) { - drawTextWithShadow(screen, str, x, y, color.RGBA{R: 202, G: 146, B: 74, A: 255}, text.AlignCenter, text.AlignStart) + drawTextWithShadow(screen, str, x, y, colors.Peru, text.AlignCenter, text.AlignStart) } func drawCharacter(screen *ebiten.Image, x, y float64) { diff --git a/system/turn.go b/system/turn.go index 0003eba..3a40fe8 100644 --- a/system/turn.go +++ b/system/turn.go @@ -1,10 +1,12 @@ package system import ( + "fmt" + "github.com/kensonjohnson/roguelike-game-go/archetype" + "github.com/kensonjohnson/roguelike-game-go/archetype/tags" "github.com/kensonjohnson/roguelike-game-go/component" "github.com/kensonjohnson/roguelike-game-go/system/action" - "github.com/yohamta/donburi" "github.com/yohamta/donburi/ecs" ) @@ -22,6 +24,7 @@ var Turn = TurnData{ const ( BeforePlayerAction = iota + UIOpen PlayerTurn MonsterTurn GameOver @@ -32,7 +35,7 @@ func (td *TurnData) Update(ecs *ecs.ECS) { if td.TurnState == BeforePlayerAction { td.TurnCounter++ // Check if player survived the last cycle of monster turns - entry := archetype.PlayerTag.MustFirst(ecs.World) + entry := tags.PlayerTag.MustFirst(ecs.World) playerHealth := component.Health.Get(entry) if playerHealth.CurrentHealth <= 0 { td.gameOver() @@ -40,9 +43,9 @@ func (td *TurnData) Update(ecs *ecs.ECS) { playerMessages.GameStateMessage = "Game over!" } - level := component.Level.Get(archetype.LevelTag.MustFirst(ecs.World)) + level := component.Level.Get(tags.LevelTag.MustFirst(ecs.World)) // Remove any enemies that died during the last turn - archetype.MonsterTag.Each(ecs.World, func(entry *donburi.Entry) { + for entry = range tags.MonsterTag.Iter(ecs.World) { health := component.Health.Get(entry) if health.CurrentHealth <= 0 { position := component.Position.Get(entry) @@ -50,27 +53,72 @@ func (td *TurnData) Update(ecs *ecs.ECS) { tile.Blocked = false ecs.World.Remove(entry.Entity()) } - }) - component.Sprite.Each(ecs.World, func(entry *donburi.Entry) { - sprite := component.Sprite.Get(entry) + } + + for spriteEntry := range component.Sprite.Iter(ecs.World) { + sprite := component.Sprite.Get(spriteEntry) sprite.SetProgress(float64(td.TurnCounter) / 12) - }) - - if td.TurnCounter > 12 { - // Reset the progress of all sprites - component.Sprite.Each(ecs.World, func(entry *donburi.Entry) { - sprite := component.Sprite.Get(entry) - sprite.SetProgress(0) - sprite.Animating = false - sprite.OffestX = 0 - sprite.OffestY = 0 - }) - - level.Redraw = true - td.progressTurnState() - td.resetCounter() + + } + + if td.TurnCounter < 13 { + return + } + // Reset the progress of all sprites + for entry = range component.Sprite.Iter(ecs.World) { + sprite := component.Sprite.Get(entry) + sprite.SetProgress(0) + sprite.Animating = false + sprite.OffestX = 0 + sprite.OffestY = 0 } + + playerEntry := tags.PlayerTag.MustFirst(ecs.World) + playerPosition := component.Position.Get(playerEntry) + playerMessages := component.UserMessage.Get(playerEntry) + + for entry = range tags.PickupTag.Iter(ecs.World) { + if !entry.HasComponent(component.Position) { + continue + } + pickupPosition := component.Position.Get(entry) + + if pickupPosition.X == playerPosition.X && pickupPosition.Y == playerPosition.Y { + + // If pickup is coinage, add to wallet + if entry.HasComponent(tags.CoinTag) { + 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) + break + } + + // Otherwise, must be item, place in inventory + err := component.Inventory.Get(playerEntry).AddItem(entry) + if err != nil { + playerMessages.WorldInteractionMessage = "Inventory full! Can't pick up anymore items!" + } else { + archetype.RemoveItemFromWorld(entry) + itemName := component.Name.Get(entry) + playerMessages.WorldInteractionMessage = fmt.Sprintf("Picked up %s!", itemName.Value) + } + + // Only one pickup can fill a tile + break + } + } + + level.Redraw = true + td.progressTurnState() + td.resetCounter() + } + + if td.TurnState == UIOpen { + // Do some input detection for inventory } if td.TurnState == PlayerTurn { diff --git a/system/ui.go b/system/ui.go index cfbf220..e200a37 100644 --- a/system/ui.go +++ b/system/ui.go @@ -6,10 +6,10 @@ import ( "github.com/hajimehoshi/ebiten/v2" "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/config" + "github.com/kensonjohnson/roguelike-game-go/internal/config" "github.com/yohamta/donburi" "github.com/yohamta/donburi/ecs" "github.com/yohamta/donburi/filter" @@ -36,7 +36,7 @@ var UI = &ui{ func (u *ui) Update(ecs *ecs.ECS) { // Get attack messages first - u.query.Each(ecs.World, func(entry *donburi.Entry) { + for entry := range u.query.Iter(ecs.World) { messages := component.UserMessage.Get(entry) if messages.AttackMessage != "" { u.lastMessages = append(u.lastMessages, messages.AttackMessage) @@ -46,9 +46,10 @@ func (u *ui) Update(ecs *ecs.ECS) { u.lastMessages = append(u.lastMessages, messages.WorldInteractionMessage) messages.WorldInteractionMessage = "" } - }) + } + // Then process any deaths, including the player's - u.query.Each(ecs.World, func(entry *donburi.Entry) { + for entry := range u.query.Iter(ecs.World) { messages := component.UserMessage.Get(entry) if messages.DeadMessage != "" { u.lastMessages = append(u.lastMessages, messages.DeadMessage) @@ -58,7 +59,7 @@ func (u *ui) Update(ecs *ecs.ECS) { u.lastMessages = append(u.lastMessages, messages.GameStateMessage) messages.GameStateMessage = "" } - }) + } if len(u.lastMessages) > 6 { // Save just the last 6 messages @@ -68,7 +69,7 @@ func (u *ui) Update(ecs *ecs.ECS) { } func (u *ui) Draw(ecs *ecs.ECS, screen *ebiten.Image) { - entry := archetype.UITag.MustFirst(ecs.World) + entry := tags.UITag.MustFirst(ecs.World) ui := component.UI.Get(entry) // Draw the user message box