Inventory screen

This commit is contained in:
Miroslav Vasilev 2024-05-03 13:46:32 +03:00
parent 099155c186
commit 5864ab41ad
19 changed files with 1145 additions and 94 deletions

View file

@ -29,3 +29,9 @@
- Underground City ( Caverns + Dungeon combo )
- Objective: Pick up the Last Light and bring it to its Altar ( Altar of the Last Light )
- The light is always on level 9, but the Altar can be anywhere
Items:
- `»o>` - fish
- `╪══` - longsword
- `o─╖`
- `█▄`

BIN
__debug_bin2404317323 Executable file

Binary file not shown.

View file

@ -0,0 +1,323 @@
package model
import (
"math/rand"
"mvvasilev/last_light/util"
)
type splitDirection bool
const (
splitDirectionVertical splitDirection = true
splitDirectionHorizontal splitDirection = false
)
type bspNode struct {
origin util.Position
size util.Size
room Room
hasRoom bool
left *bspNode
right *bspNode
splitDir splitDirection
}
type Room struct {
position util.Position
size util.Size
}
func (r Room) Size() util.Size {
return r.size
}
func (r Room) Position() util.Position {
return r.position
}
type BSPDungeonLevel struct {
level *BasicMap
playerSpawnPoint util.Position
rooms []Room
}
func CreateBSPDungeonLevel(width, height int, numSplits int) *BSPDungeonLevel {
root := new(bspNode)
root.origin = util.PositionAt(0, 0)
root.size = util.SizeOf(width, height)
split(root, numSplits)
tiles := make([][]Tile, height)
for h := range height {
tiles[h] = make([]Tile, width)
}
rooms := make([]Room, 0, 2^numSplits)
iterateBspLeaves(root, func(leaf *bspNode) {
x := util.RandInt(leaf.origin.X(), leaf.origin.X()+leaf.size.Width()/4)
y := util.RandInt(leaf.origin.Y(), leaf.origin.Y()+leaf.size.Height()/4)
w := util.RandInt(3, leaf.size.Width()-1)
h := util.RandInt(3, leaf.size.Height()-1)
if x+w >= width {
w = w - (x + w - width) - 1
}
if y+h >= height {
h = h - (y + h - height) - 1
}
room := Room{
position: util.PositionAt(x, y),
size: util.SizeOf(w, h),
}
rooms = append(rooms, room)
makeRoom(tiles, room)
leaf.room = room
leaf.hasRoom = true
})
iterateBspParents(root, func(parent *bspNode) {
roomLeft := findRoom(parent.left)
roomRight := findRoom(parent.right)
zCorridor(
tiles,
util.PositionAt(
roomLeft.position.X()+roomLeft.size.Width()/2,
roomLeft.position.Y()+roomLeft.size.Height()/2,
),
util.PositionAt(
roomRight.position.X()+roomRight.size.Width()/2,
roomRight.position.Y()+roomRight.size.Height()/2,
),
parent.splitDir,
)
})
bsp := new(BSPDungeonLevel)
spawnRoom := findRoom(root.left)
bsp.rooms = rooms
bsp.level = CreateBasicMap(tiles)
bsp.playerSpawnPoint = util.PositionAt(
spawnRoom.position.X()+spawnRoom.size.Width()/2,
spawnRoom.position.Y()+spawnRoom.size.Height()/2,
)
return bsp
}
func (bsp *BSPDungeonLevel) PlayerSpawnPoint() util.Position {
return bsp.playerSpawnPoint
}
func findRoom(parent *bspNode) Room {
if parent.hasRoom {
return parent.room
}
if rand.Float32() > 0.5 {
return findRoom(parent.left)
} else {
return findRoom(parent.right)
}
}
func zCorridor(tiles [][]Tile, from util.Position, to util.Position, direction splitDirection) {
switch direction {
case splitDirectionHorizontal:
xMidPoint := (from.X() + to.X()) / 2
horizontalTunnel(tiles, from.X(), xMidPoint, from.Y())
horizontalTunnel(tiles, to.X(), xMidPoint, to.Y())
verticalTunnel(tiles, from.Y(), to.Y(), xMidPoint)
case splitDirectionVertical:
yMidPoint := (from.Y() + to.Y()) / 2
verticalTunnel(tiles, from.Y(), yMidPoint, from.X())
verticalTunnel(tiles, to.Y(), yMidPoint, to.X())
horizontalTunnel(tiles, from.X(), to.X(), yMidPoint)
}
}
func iterateBspParents(parent *bspNode, iter func(parent *bspNode)) {
if parent.left != nil && parent.right != nil {
iter(parent)
}
if parent.left != nil {
iterateBspParents(parent.left, iter)
}
if parent.right != nil {
iterateBspParents(parent.right, iter)
}
}
func iterateBspLeaves(parent *bspNode, iter func(leaf *bspNode)) {
if parent.left == nil && parent.right == nil {
iter(parent)
return
}
if parent.left != nil {
iterateBspLeaves(parent.left, iter)
}
if parent.right != nil {
iterateBspLeaves(parent.right, iter)
}
}
func split(parent *bspNode, numSplits int) {
if numSplits <= 0 {
return
}
// split vertically
if parent.size.Width() > parent.size.Height() {
// New splits will be between 45% and 65% of the parent's width
leftSplitWidth := util.RandInt(int(float32(parent.size.Width())*0.45), int(float32(parent.size.Width())*0.65))
parent.splitDir = splitDirectionVertical
parent.left = new(bspNode)
parent.left.origin = parent.origin
parent.left.size = util.SizeOf(leftSplitWidth, parent.size.Height())
parent.right = new(bspNode)
parent.right.origin = parent.origin.WithOffset(leftSplitWidth, 0)
parent.right.size = util.SizeOf(parent.size.Width()-leftSplitWidth, parent.size.Height())
} else { // split horizontally
// New splits will be between 45% and 65% of the parent's height
leftSplitHeight := util.RandInt(int(float32(parent.size.Height())*0.45), int(float32(parent.size.Height())*0.65))
parent.splitDir = splitDirectionHorizontal
parent.left = new(bspNode)
parent.left.origin = parent.origin
parent.left.size = util.SizeOf(parent.size.Width(), leftSplitHeight)
parent.right = new(bspNode)
parent.right.origin = parent.origin.WithOffset(0, leftSplitHeight)
parent.right.size = util.SizeOf(parent.size.Width(), parent.size.Height()-leftSplitHeight)
}
split(parent.left, numSplits-1)
split(parent.right, numSplits-1)
}
func horizontalTunnel(tiles [][]Tile, x1, x2, y int) {
if x1 > x2 {
tx := x2
x2 = x1
x1 = tx
}
placeWallAtIfNotPassable(tiles, x1, y-1)
placeWallAtIfNotPassable(tiles, x1, y)
placeWallAtIfNotPassable(tiles, x1, y+1)
for x := x1; x <= x2; x++ {
if tiles[y][x] != nil && tiles[y][x].Passable() {
continue
}
tiles[y][x] = CreateStaticTile(x, y, TileTypeGround())
placeWallAtIfNotPassable(tiles, x, y-1)
placeWallAtIfNotPassable(tiles, x, y+1)
}
placeWallAtIfNotPassable(tiles, x2, y-1)
placeWallAtIfNotPassable(tiles, x2, y)
placeWallAtIfNotPassable(tiles, x2, y+1)
}
func verticalTunnel(tiles [][]Tile, y1, y2, x int) {
if y1 > y2 {
ty := y2
y2 = y1
y1 = ty
}
placeWallAtIfNotPassable(tiles, x-1, y1)
placeWallAtIfNotPassable(tiles, x, y1)
placeWallAtIfNotPassable(tiles, x+1, y1)
for y := y1; y <= y2; y++ {
if tiles[y][x] != nil && tiles[y][x].Passable() {
continue
}
tiles[y][x] = CreateStaticTile(x, y, TileTypeGround())
placeWallAtIfNotPassable(tiles, x-1, y)
placeWallAtIfNotPassable(tiles, x+1, y)
}
placeWallAtIfNotPassable(tiles, x-1, y2)
placeWallAtIfNotPassable(tiles, x, y2)
placeWallAtIfNotPassable(tiles, x+1, y2)
}
func placeWallAtIfNotPassable(tiles [][]Tile, x, y int) {
if tiles[y][x] != nil && tiles[y][x].Passable() {
return
}
tiles[y][x] = CreateStaticTile(x, y, TileTypeWall())
}
func makeRoom(tiles [][]Tile, room Room) {
width := room.size.Width()
height := room.size.Height()
x := room.position.X()
y := room.position.Y()
for w := x; w < x+width+1; w++ {
tiles[y][w] = CreateStaticTile(w, y, TileTypeWall())
tiles[y+height][w] = CreateStaticTile(w, y+height, TileTypeWall())
}
for h := y; h < y+height+1; h++ {
tiles[h][x] = CreateStaticTile(x, h, TileTypeWall())
tiles[h][x+width] = CreateStaticTile(x+width, h, TileTypeWall())
}
for h := y + 1; h < y+height; h++ {
for w := x + 1; w < x+width; w++ {
tiles[h][w] = CreateStaticTile(w, h, TileTypeGround())
}
}
}
func (bsp *BSPDungeonLevel) Size() util.Size {
return bsp.level.Size()
}
func (bsp *BSPDungeonLevel) SetTileAt(x int, y int, t Tile) {
bsp.level.SetTileAt(x, y, t)
}
func (bsp *BSPDungeonLevel) TileAt(x int, y int) Tile {
return bsp.level.TileAt(x, y)
}
func (bsp *BSPDungeonLevel) Tick() {
}
func (bsp *BSPDungeonLevel) Rooms() []Room {
return bsp.rooms
}

View file

@ -1 +1,7 @@
package model
type Dungeon struct {
player *Player
levels []Map
}

View file

@ -3,43 +3,33 @@ package model
import "mvvasilev/last_light/util"
type EmptyDungeonLevel struct {
tiles [][]Tile
level *BasicMap
}
func CreateEmptyDungeonLevel(width, height int) *EmptyDungeonLevel {
m := new(EmptyDungeonLevel)
m.tiles = make([][]Tile, height)
tiles := make([][]Tile, height)
for h := range height {
m.tiles[h] = make([]Tile, width)
tiles[h] = make([]Tile, width)
}
m.level = CreateBasicMap(tiles)
return m
}
func (edl *EmptyDungeonLevel) Size() util.Size {
return util.SizeOf(len(edl.tiles[0]), len(edl.tiles))
return edl.level.Size()
}
func (edl *EmptyDungeonLevel) SetTileAt(x int, y int, t Tile) {
if len(edl.tiles) <= y || len(edl.tiles[0]) <= x {
return
}
edl.tiles[y][x] = t
edl.level.SetTileAt(x, y, t)
}
func (edl *EmptyDungeonLevel) TileAt(x int, y int) Tile {
if y < 0 || y >= len(edl.tiles) {
return nil
}
if x < 0 || x >= len(edl.tiles[y]) {
return nil
}
return edl.tiles[y][x]
return edl.level.TileAt(x, y)
}
func (edl *EmptyDungeonLevel) Tick() {

View file

@ -21,7 +21,7 @@ func CreateFlatGroundDungeonLevel(width, height int) *FlatGroundDungeonLevel {
for w := range width {
if w == 0 || h == 0 || w >= width-1 || h >= height-1 {
level.tiles[h][w] = CreateStaticTile(w, h, Rock())
level.tiles[h][w] = CreateStaticTile(w, h, TileTypeRock())
continue
}
@ -35,16 +35,16 @@ func CreateFlatGroundDungeonLevel(width, height int) *FlatGroundDungeonLevel {
func genRandomGroundTile(width, height int) Tile {
switch rand.Intn(2) {
case 0:
return CreateStaticTile(width, height, Ground())
return CreateStaticTile(width, height, TileTypeGround())
case 1:
return CreateStaticTile(width, height, Grass())
return CreateStaticTile(width, height, TileTypeGrass())
default:
return CreateStaticTile(width, height, Ground())
return CreateStaticTile(width, height, TileTypeGround())
}
}
func (edl *FlatGroundDungeonLevel) Size() util.Size {
return util.SizeOfInt(len(edl.tiles[0]), len(edl.tiles))
return util.SizeOf(len(edl.tiles[0]), len(edl.tiles))
}
func (edl *FlatGroundDungeonLevel) SetTileAt(x int, y int, t Tile) {

81
game/model/inventory.go Normal file
View file

@ -0,0 +1,81 @@
package model
import "mvvasilev/last_light/util"
type Inventory struct {
contents []*Item
shape util.Size
}
func CreateInventory(shape util.Size) *Inventory {
inv := new(Inventory)
inv.contents = make([]*Item, 0, shape.Height()*shape.Width())
inv.shape = shape
return inv
}
func (i *Inventory) Items() (items []*Item) {
return i.contents
}
func (i *Inventory) Shape() util.Size {
return i.shape
}
func (i *Inventory) Push(item Item) (success bool) {
if len(i.contents) == i.shape.Area() {
return false
}
itemType := item.Type()
// Try to first find a matching item with capacity
for index, existingItem := range i.contents {
if existingItem != nil && existingItem.itemType == itemType {
if existingItem.Quantity()+1 > existingItem.Type().MaxStack() {
continue
}
it := CreateItem(itemType, existingItem.Quantity()+1)
i.contents[index] = &it
return true
}
}
// Next, try to find an intermediate empty slot to fit this item into
for index, existingItem := range i.contents {
if existingItem == nil {
i.contents[index] = &item
return true
}
}
// Finally, just append the new item at the end
i.contents = append(i.contents, &item)
return true
}
func (i *Inventory) Drop(x, y int) {
index := y*i.shape.Width() + x
if index > len(i.contents)-1 {
return
}
i.contents[index] = nil
}
func (i *Inventory) ItemAt(x, y int) (item *Item) {
index := y*i.shape.Width() + x
if index > len(i.contents)-1 {
return nil
}
return i.contents[index]
}

185
game/model/item.go Normal file
View file

@ -0,0 +1,185 @@
package model
import (
"math/rand"
"github.com/gdamore/tcell/v2"
)
type ItemType struct {
name string
description string
tileIcon rune
itemIcon string
maxStack int
style tcell.Style
}
func (it *ItemType) Name() string {
return it.name
}
func (it *ItemType) Description() string {
return it.description
}
func (it *ItemType) TileIcon() rune {
return it.tileIcon
}
func (it *ItemType) Icon() string {
return it.itemIcon
}
func (it *ItemType) Style() tcell.Style {
return it.style
}
func (it *ItemType) MaxStack() int {
return it.maxStack
}
func ItemTypeFish() *ItemType {
return &ItemType{
name: "Fish",
description: "What's a fish doing down here?",
tileIcon: '>',
itemIcon: "»o>",
style: tcell.StyleDefault.Foreground(tcell.ColorDarkCyan),
maxStack: 16,
}
}
func ItemTypeGold() *ItemType {
return &ItemType{
name: "Gold",
description: "Not all those who wander are lost",
tileIcon: '¤',
itemIcon: " ¤ ",
style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod),
maxStack: 255,
}
}
func ItemTypeArrow() *ItemType {
return &ItemType{
name: "Arrow",
description: "Ammunition for a bow",
tileIcon: '-',
itemIcon: "»->",
style: tcell.StyleDefault.Foreground(tcell.ColorGoldenrod),
maxStack: 32,
}
}
func ItemTypeBow() *ItemType {
return &ItemType{
name: "Bow",
description: "To shoot arrows with",
tileIcon: ')',
itemIcon: " |)",
style: tcell.StyleDefault.Foreground(tcell.ColorBrown),
maxStack: 1,
}
}
func ItemTypeLongsword() *ItemType {
return &ItemType{
name: "Longsword",
description: "You know nothing.",
tileIcon: '/',
itemIcon: "╪══",
style: tcell.StyleDefault.Foreground(tcell.ColorSilver),
maxStack: 1,
}
}
func ItemTypeKey() *ItemType {
return &ItemType{
name: "Key",
description: "Indispensable for unlocking things",
tileIcon: '¬',
itemIcon: " o╖",
style: tcell.StyleDefault.Foreground(tcell.ColorDarkGoldenrod),
maxStack: 1,
}
}
type ItemTypeGenTable struct {
}
func GenerateItemType(genTable map[float32]*ItemType) *ItemType {
num := rand.Float32()
for k, v := range genTable {
if num > k {
return v
}
}
return nil
}
type Item struct {
name string
description string
itemType *ItemType
quantity int
}
func EmptyItem() Item {
return Item{
itemType: &ItemType{
name: "",
description: "",
tileIcon: ' ',
itemIcon: " ",
style: tcell.StyleDefault,
maxStack: 0,
},
}
}
func CreateItem(itemType *ItemType, quantity int) Item {
return Item{
itemType: itemType,
quantity: quantity,
}
}
func (i Item) WithName(name string) Item {
i.name = name
return i
}
func (i Item) Name() string {
if i.name == "" {
return i.itemType.name
}
return i.name
}
func (i Item) Description() string {
if i.description == "" {
return i.itemType.description
}
return i.description
}
func (i Item) WithDescription(description string) Item {
i.description = description
return i
}
func (i Item) Type() *ItemType {
return i.itemType
}
func (i Item) Quantity() int {
return i.quantity
}

View file

@ -10,3 +10,52 @@ type Map interface {
TileAt(x, y int) Tile
Tick()
}
type BasicMap struct {
tiles [][]Tile
}
func CreateBasicMap(tiles [][]Tile) *BasicMap {
bm := new(BasicMap)
bm.tiles = tiles
return bm
}
func (bm *BasicMap) Tick() {
}
func (bm *BasicMap) Size() util.Size {
return util.SizeOf(len(bm.tiles[0]), len(bm.tiles))
}
func (bm *BasicMap) SetTileAt(x int, y int, t Tile) {
if len(bm.tiles) <= y || len(bm.tiles[0]) <= x {
return
}
if x < 0 || y < 0 {
return
}
bm.tiles[y][x] = t
}
func (bm *BasicMap) TileAt(x int, y int) Tile {
if x < 0 || y < 0 {
return CreateStaticTile(x, y, TileTypeVoid())
}
if x >= bm.Size().Width() || y >= bm.Size().Height() {
return CreateStaticTile(x, y, TileTypeVoid())
}
tile := bm.tiles[y][x]
if tile == nil {
return CreateStaticTile(x, y, TileTypeVoid())
}
return tile
}

View file

@ -42,16 +42,64 @@ func (mm *MultilevelMap) SetTileAtHeight(x, y, height int, t Tile) {
mm.layers[height].SetTileAt(x, y, t)
}
func (mm *MultilevelMap) TileAt(x int, y int) Tile {
func (mm *MultilevelMap) CollectTilesAt(x, y int, filter func(t Tile) bool) []Tile {
tiles := make([]Tile, len(mm.layers))
if x < 0 || y < 0 {
return tiles
}
if x >= mm.Size().Width() || y >= mm.Size().Height() {
return tiles
}
for i := len(mm.layers) - 1; i >= 0; i-- {
tile := mm.layers[i].TileAt(x, y)
if tile != nil {
return tile
}
if tile != nil && !tile.Transparent() && filter(tile) {
tiles = append(tiles, tile)
}
return nil
}
return tiles
}
func (mm *MultilevelMap) TileAt(x int, y int) Tile {
if x < 0 || y < 0 {
return CreateStaticTile(x, y, TileTypeVoid())
}
if x >= mm.Size().Width() || y >= mm.Size().Height() {
return CreateStaticTile(x, y, TileTypeVoid())
}
for i := len(mm.layers) - 1; i >= 0; i-- {
tile := mm.layers[i].TileAt(x, y)
if tile != nil && !tile.Transparent() {
return tile
}
}
return CreateStaticTile(x, y, TileTypeVoid())
}
func (mm *MultilevelMap) TileAtHeight(x, y, height int) Tile {
if x < 0 || y < 0 {
return CreateStaticTile(x, y, TileTypeVoid())
}
if x >= mm.Size().Width() || y >= mm.Size().Height() {
return CreateStaticTile(x, y, TileTypeVoid())
}
if height > len(mm.layers)-1 {
return CreateStaticTile(x, y, TileTypeVoid())
}
return mm.layers[height].TileAt(x, y)
}
func (mm *MultilevelMap) Tick() {

View file

@ -10,6 +10,8 @@ import (
type Player struct {
id uuid.UUID
position util.Position
inventory *Inventory
}
func CreatePlayer(x, y int) *Player {
@ -17,6 +19,7 @@ func CreatePlayer(x, y int) *Player {
p.id = uuid.New()
p.position = util.PositionAt(x, y)
p.inventory = CreateInventory(util.SizeOf(8, 4))
return p
}
@ -33,14 +36,22 @@ func (p *Player) Move(dir Direction) {
p.position = p.Position().WithOffset(MovementDirectionOffset(dir))
}
func (p *Player) Presentation() rune {
return '@'
func (p *Player) Presentation() (rune, tcell.Style) {
return '@', tcell.StyleDefault
}
func (p *Player) Passable() bool {
return false
}
func (p *Player) Transparent() bool {
return false
}
func (p *Player) Inventory() *Inventory {
return p.inventory
}
func (p *Player) Input(e *tcell.EventKey) {
}

View file

@ -1,12 +1,17 @@
package model
import "mvvasilev/last_light/util"
import (
"mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2"
)
type Material uint
const (
MaterialGround Material = iota
MaterialRock
MaterialWall
MaterialGrass
MaterialVoid
)
@ -15,44 +20,65 @@ type TileType struct {
Material Material
Passable bool
Presentation rune
Transparent bool
Style tcell.Style
}
func Ground() TileType {
func TileTypeGround() TileType {
return TileType{
Material: MaterialGround,
Passable: true,
Presentation: '.',
Transparent: false,
Style: tcell.StyleDefault,
}
}
func Rock() TileType {
func TileTypeRock() TileType {
return TileType{
Material: MaterialRock,
Passable: false,
Presentation: '█',
Transparent: false,
Style: tcell.StyleDefault,
}
}
func Grass() TileType {
func TileTypeGrass() TileType {
return TileType{
Material: MaterialGrass,
Passable: true,
Presentation: ',',
Transparent: false,
Style: tcell.StyleDefault,
}
}
func Void() TileType {
func TileTypeVoid() TileType {
return TileType{
Material: MaterialVoid,
Passable: false,
Presentation: ' ',
Transparent: true,
Style: tcell.StyleDefault,
}
}
func TileTypeWall() TileType {
return TileType{
Material: MaterialWall,
Passable: false,
Presentation: '#',
Transparent: false,
Style: tcell.StyleDefault.Background(tcell.ColorGray),
}
}
type Tile interface {
Position() util.Position
Presentation() rune
Presentation() (rune, tcell.Style)
Passable() bool
Transparent() bool
}
type StaticTile struct {
@ -73,14 +99,58 @@ func (st *StaticTile) Position() util.Position {
return st.position
}
func (st *StaticTile) Presentation() rune {
return st.t.Presentation
func (st *StaticTile) Presentation() (rune, tcell.Style) {
return st.t.Presentation, st.t.Style
}
func (st *StaticTile) Passable() bool {
return st.t.Passable
}
func (st *StaticTile) Transparent() bool {
return st.t.Transparent
}
func (st *StaticTile) Type() TileType {
return st.t
}
type ItemTile struct {
position util.Position
itemType *ItemType
quantity int
}
func CreateItemTile(position util.Position, itemType *ItemType, quantity int) *ItemTile {
it := new(ItemTile)
it.position = position
it.itemType = itemType
it.quantity = quantity
return it
}
func (it *ItemTile) Type() *ItemType {
return it.itemType
}
func (it *ItemTile) Quantity() int {
return it.quantity
}
func (it *ItemTile) Position() util.Position {
return it.position
}
func (it *ItemTile) Presentation() (rune, tcell.Style) {
return it.itemType.tileIcon, it.itemType.style
}
func (it *ItemTile) Passable() bool {
return true
}
func (it *ItemTile) Transparent() bool {
return false
}

View file

@ -1,30 +1,126 @@
package state
import (
"fmt"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/render"
"mvvasilev/last_light/ui"
"mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
)
type InventoryScreenState struct {
prevState GameState
prevState PausableState
exitMenu bool
inventoryMenu *ui.UIWindow
armourLabel *ui.UILabel
armourGrid *render.Grid
leftHandLabel *ui.UILabel
leftHandBox render.Rectangle
rightHandLabel *ui.UILabel
rightHandBox render.Rectangle
inventoryGrid *render.Grid
playerItems *render.ArbitraryDrawable
selectedItem *render.ArbitraryDrawable
help *ui.UILabel
player *model.Player
moveInventorySlotDirection model.Direction
selectedInventorySlot util.Position
dropSelectedInventorySlot bool
}
func CreateInventoryScreenState(player *model.Player, prevState GameState) *InventoryScreenState {
func CreateInventoryScreenState(player *model.Player, prevState PausableState) *InventoryScreenState {
iss := new(InventoryScreenState)
iss.prevState = prevState
iss.player = player
iss.exitMenu = false
iss.selectedInventorySlot = util.PositionAt(0, 0)
iss.inventoryMenu = ui.CreateWindow(40, 0, 40, 24, "INVENTORY", tcell.StyleDefault)
iss.inventoryMenu = ui.CreateWindow(43, 0, 37, 24, "INVENTORY", tcell.StyleDefault)
iss.armourLabel = ui.CreateSingleLineUILabel(58, 1, "ARMOUR", tcell.StyleDefault)
iss.armourGrid = render.CreateGrid(
53, 2, 3, 1, 4, 1, '┌', '─', '┬', '┐', '│', ' ', '│', '│', '├', '─', '┼', '┤', '└', '─', '┴', '┘', tcell.StyleDefault, tcell.StyleDefault.Background(tcell.ColorDarkSlateGray),
)
iss.leftHandLabel = ui.CreateUILabel(
46, 1, 5, 1, "OFF", tcell.StyleDefault,
)
iss.leftHandBox = render.CreateRectangle(
45, 2, 5, 3,
'┌', '─', '┐',
'│', ' ', '│',
'└', '─', '┘',
false, true,
tcell.StyleDefault,
)
iss.rightHandLabel = ui.CreateUILabel(
74, 1, 5, 1, "DOM", tcell.StyleDefault,
)
iss.rightHandBox = render.CreateRectangle(
73, 2, 5, 3,
'┌', '─', '┐',
'│', ' ', '│',
'└', '─', '┘',
false, true,
tcell.StyleDefault,
)
iss.inventoryGrid = render.CreateGrid(
45, 5, 3, 1, 8, 4, '┌', '─', '┬', '┐', '│', ' ', '│', '│', '├', '─', '┼', '┤', '└', '─', '┴', '┘', tcell.StyleDefault, tcell.StyleDefault.Background(tcell.ColorDarkSlateGray),
)
iss.playerItems = render.CreateDrawingInstructions(func(v views.View) {
for y := range player.Inventory().Shape().Height() {
for x := range player.Inventory().Shape().Width() {
item := player.Inventory().ItemAt(x, y)
isHighlighted := x == iss.selectedInventorySlot.X() && y == iss.selectedInventorySlot.Y()
if item == nil {
if isHighlighted {
ui.CreateSingleLineUILabel(45+1+x*4, 5+1+y*2, " ", tcell.StyleDefault.Background(tcell.ColorDarkSlateGray)).Draw(v)
}
continue
}
style := item.Type().Style()
if isHighlighted {
style = style.Background(tcell.ColorDarkSlateGray)
}
ui.CreateSingleLineUILabel(45+1+x*4, 5+y*2, fmt.Sprintf("%03d", item.Quantity()), style).Draw(v)
ui.CreateSingleLineUILabel(45+1+x*4, 5+1+y*2, item.Type().Icon(), style).Draw(v)
}
}
})
iss.selectedItem = render.CreateDrawingInstructions(func(v views.View) {
ui.CreateWindow(45, 14, 33, 8, "ITEM", tcell.StyleDefault).Draw(v)
item := player.Inventory().ItemAt(iss.selectedInventorySlot.XY())
if item == nil {
return
}
ui.CreateSingleLineUILabel(46, 15, fmt.Sprintf("Name: %v", item.Name()), tcell.StyleDefault).Draw(v)
ui.CreateSingleLineUILabel(46, 16, fmt.Sprintf("Desc: %v", item.Description()), tcell.StyleDefault).Draw(v)
})
iss.help = ui.CreateSingleLineUILabel(45, 22, "hjkl - move, x - drop, e - equip", tcell.StyleDefault)
return iss
}
@ -33,19 +129,89 @@ func (iss *InventoryScreenState) OnInput(e *tcell.EventKey) {
if e.Key() == tcell.KeyEsc || (e.Key() == tcell.KeyRune && e.Rune() == 'i') {
iss.exitMenu = true
}
if e.Key() == tcell.KeyRune && e.Rune() == 'x' {
iss.dropSelectedInventorySlot = true
}
if e.Key() != tcell.KeyRune {
return
}
switch e.Rune() {
case 'k':
iss.moveInventorySlotDirection = model.DirectionUp
case 'j':
iss.moveInventorySlotDirection = model.DirectionDown
case 'h':
iss.moveInventorySlotDirection = model.DirectionLeft
case 'l':
iss.moveInventorySlotDirection = model.DirectionRight
}
}
func (iss *InventoryScreenState) OnTick(dt int64) GameState {
if iss.exitMenu {
iss.prevState.Unpause()
return iss.prevState
}
if iss.dropSelectedInventorySlot {
iss.player.Inventory().Drop(iss.selectedInventorySlot.XY())
iss.dropSelectedInventorySlot = false
}
if iss.moveInventorySlotDirection != model.DirectionNone {
switch iss.moveInventorySlotDirection {
case model.DirectionUp:
if iss.selectedInventorySlot.Y() == 0 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, -1)
case model.DirectionDown:
if iss.selectedInventorySlot.Y() == iss.player.Inventory().Shape().Height()-1 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(0, +1)
case model.DirectionLeft:
if iss.selectedInventorySlot.X() == 0 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(-1, 0)
case model.DirectionRight:
if iss.selectedInventorySlot.X() == iss.player.Inventory().Shape().Width()-1 {
break
}
iss.selectedInventorySlot = iss.selectedInventorySlot.WithOffset(+1, 0)
}
iss.inventoryGrid.Highlight(iss.selectedInventorySlot)
iss.moveInventorySlotDirection = model.DirectionNone
}
return iss
}
func (iss *InventoryScreenState) CollectDrawables() []render.Drawable {
return append(
drawables := append(
iss.prevState.CollectDrawables(),
iss.inventoryMenu,
iss.armourLabel,
iss.armourGrid,
iss.leftHandLabel,
iss.leftHandBox,
iss.rightHandLabel,
iss.rightHandBox,
iss.inventoryGrid,
iss.playerItems,
iss.selectedItem,
iss.help,
)
return drawables
}

View file

@ -1,6 +1,7 @@
package state
import (
"math/rand"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/render"
"mvvasilev/last_light/util"
@ -18,6 +19,7 @@ type PlayingState struct {
movePlayerDirection model.Direction
pauseGame bool
openInventory bool
pickUpUnderPlayer bool
}
func BeginPlayingState() *PlayingState {
@ -25,18 +27,29 @@ func BeginPlayingState() *PlayingState {
mapSize := util.SizeOf(128, 128)
s.player = model.CreatePlayer(40, 12)
dungeonLevel := model.CreateBSPDungeonLevel(mapSize.Width(), mapSize.Height(), 4)
itemTiles := spawnItems(dungeonLevel)
itemLevel := model.CreateEmptyDungeonLevel(mapSize.Width(), mapSize.Height())
for _, it := range itemTiles {
itemLevel.SetTileAt(it.Position().X(), it.Position().Y(), it)
}
s.player = model.CreatePlayer(dungeonLevel.PlayerSpawnPoint().XY())
s.level = model.CreateMultilevelMap(
model.CreateFlatGroundDungeonLevel(mapSize.WH()),
dungeonLevel,
itemLevel,
model.CreateEmptyDungeonLevel(mapSize.WH()),
)
s.level.SetTileAtHeight(40, 12, 1, s.player)
s.level.SetTileAtHeight(dungeonLevel.PlayerSpawnPoint().X(), dungeonLevel.PlayerSpawnPoint().Y(), 2, s.player)
s.viewport = render.CreateViewport(
util.PositionAt(0, 0),
util.PositionAt(40, 12),
dungeonLevel.PlayerSpawnPoint(),
util.SizeOf(80, 24),
tcell.StyleDefault,
)
@ -44,6 +57,48 @@ func BeginPlayingState() *PlayingState {
return s
}
func spawnItems(level *model.BSPDungeonLevel) []model.Tile {
rooms := level.Rooms()
genTable := make(map[float32]*model.ItemType)
genTable[0.2] = model.ItemTypeFish()
genTable[0.05] = model.ItemTypeBow()
genTable[0.051] = model.ItemTypeLongsword()
genTable[0.052] = model.ItemTypeKey()
itemTiles := make([]model.Tile, 0, 10)
for _, r := range rooms {
maxItems := int(0.10 * float64(r.Size().Area()))
if maxItems < 1 {
continue
}
numItems := rand.Intn(maxItems)
for range numItems {
itemType := model.GenerateItemType(genTable)
if itemType == nil {
continue
}
pos := util.PositionAt(
util.RandInt(r.Position().X()+1, r.Position().X()+r.Size().Width()-1),
util.RandInt(r.Position().Y()+1, r.Position().Y()+r.Size().Height()-1),
)
itemTiles = append(itemTiles, model.CreateItemTile(
pos, itemType, 1,
))
}
}
return itemTiles
}
func (ps *PlayingState) Pause() {
ps.pauseGame = true
}
@ -66,15 +121,36 @@ func (ps *PlayingState) MovePlayer() {
tileAtMovePos := ps.level.TileAt(newPlayerPos.XY())
if tileAtMovePos.Passable() {
ps.level.SetTileAtHeight(ps.player.Position().X(), ps.player.Position().Y(), 1, nil)
ps.level.SetTileAtHeight(ps.player.Position().X(), ps.player.Position().Y(), 2, nil)
ps.player.Move(ps.movePlayerDirection)
ps.viewport.SetCenter(ps.player.Position())
ps.level.SetTileAtHeight(ps.player.Position().X(), ps.player.Position().Y(), 1, ps.player)
ps.level.SetTileAtHeight(ps.player.Position().X(), ps.player.Position().Y(), 2, ps.player)
}
ps.movePlayerDirection = model.DirectionNone
}
func (ps *PlayingState) PickUpItemUnderPlayer() {
pos := ps.player.Position()
tile := ps.level.TileAtHeight(pos.X(), pos.Y(), 1)
itemTile, ok := tile.(*model.ItemTile)
if !ok {
return
}
item := model.CreateItem(itemTile.Type(), itemTile.Quantity())
success := ps.player.Inventory().Push(item)
if !success {
return
}
ps.level.SetTileAtHeight(pos.X(), pos.Y(), 1, nil)
}
func (ps *PlayingState) OnInput(e *tcell.EventKey) {
ps.player.Input(e)
@ -88,6 +164,11 @@ func (ps *PlayingState) OnInput(e *tcell.EventKey) {
return
}
if e.Key() == tcell.KeyRune && e.Rune() == 'p' {
ps.pickUpUnderPlayer = true
return
}
switch e.Key() {
case tcell.KeyUp:
ps.movePlayerDirection = model.DirectionUp
@ -119,6 +200,7 @@ func (ps *PlayingState) OnTick(dt int64) GameState {
}
if ps.openInventory {
ps.openInventory = false
return CreateInventoryScreenState(ps.player, ps)
}
@ -126,19 +208,24 @@ func (ps *PlayingState) OnTick(dt int64) GameState {
ps.MovePlayer()
}
if ps.pickUpUnderPlayer {
ps.pickUpUnderPlayer = false
ps.PickUpItemUnderPlayer()
}
return ps
}
func (ps *PlayingState) CollectDrawables() []render.Drawable {
return render.Multidraw(render.CreateDrawingInstructions(func(v views.View) {
ps.viewport.DrawFromProvider(v, func(x, y int) rune {
ps.viewport.DrawFromProvider(v, func(x, y int) (rune, tcell.Style) {
tile := ps.level.TileAt(x, y)
if tile != nil {
return tile.Presentation()
}
return ' '
return ' ', tcell.StyleDefault
})
}))
}

View file

@ -16,6 +16,7 @@ type Grid struct {
numCellsVertical int
position util.Position
style tcell.Style
highlightStyle tcell.Style
northBorder rune
westBorder rune
@ -36,6 +37,9 @@ type Grid struct {
horizontalRightTJunction rune
crossJunction rune
isHighlighted bool
highlightedGrid util.Position
fillRune rune
}
@ -44,15 +48,15 @@ func CreateSimpleGrid(
cellWidth, cellHeight int,
numCellsHorizontal, numCellsVertical int,
borderRune, fillRune rune,
style tcell.Style,
) Grid {
style tcell.Style, highlightStyle tcell.Style,
) *Grid {
return CreateGrid(
x, y, cellWidth, cellHeight, numCellsHorizontal, numCellsVertical,
borderRune, borderRune, borderRune, borderRune,
borderRune, fillRune, borderRune, borderRune,
borderRune, borderRune, borderRune, borderRune,
borderRune, borderRune, borderRune, borderRune,
style,
style, highlightStyle,
)
}
@ -71,15 +75,17 @@ func CreateGrid(
westBorder, fillRune, internalVerticalBorder, eastBorder,
horizontalRightTJunction, internalHorizontalBorder, crossJunction, horizontalLeftTJunction,
swCorner, southBorder, verticalUpwardsTJunction, seCorner rune,
style tcell.Style,
) Grid {
return Grid{
style tcell.Style, highlightStyle tcell.Style,
) *Grid {
return &Grid{
id: uuid.New(),
internalCellSize: util.SizeOf(cellWidth, cellHeight),
numCellsHorizontal: numCellsHorizontal,
numCellsVertical: numCellsVertical,
isHighlighted: false,
position: util.PositionAt(x, y),
style: style,
highlightStyle: highlightStyle,
northBorder: northBorder,
eastBorder: eastBorder,
southBorder: southBorder,
@ -100,10 +106,19 @@ func CreateGrid(
}
}
func (g Grid) UniqueId() uuid.UUID {
func (g *Grid) UniqueId() uuid.UUID {
return g.id
}
func (g *Grid) Highlight(highlightedGrid util.Position) {
g.isHighlighted = true
g.highlightedGrid = highlightedGrid
}
func (g *Grid) Unhighlight() {
g.isHighlighted = false
}
// C###T###T###C
// # # # #
// # # # #
@ -117,7 +132,7 @@ func (g Grid) UniqueId() uuid.UUID {
// # # # #
// # # # #
// C###T###T###C
func (g Grid) drawBorders(v views.View) {
func (g *Grid) drawBorders(v views.View) {
iCellSizeWidth := g.internalCellSize.Width()
iCellSizeHeight := g.internalCellSize.Height()
width := 1 + (iCellSizeWidth * int(g.numCellsHorizontal)) + (int(g.numCellsHorizontal))
@ -125,59 +140,61 @@ func (g Grid) drawBorders(v views.View) {
x := g.position.X()
y := g.position.Y()
v.SetContent(x, y, g.nwCorner, nil, g.style)
v.SetContent(x+width-1, y, g.neCorner, nil, g.style)
v.SetContent(x, y+height-1, g.swCorner, nil, g.style)
v.SetContent(x+width-1, y+height-1, g.seCorner, nil, g.style)
style := g.style
for w := 1; w < width-1; w++ {
for iw := 1; iw < int(g.numCellsVertical); iw++ {
if w%(iCellSizeWidth+1) == 0 {
v.SetContent(x+w, y+(iw*iCellSizeHeight+iw), g.crossJunction, nil, g.style)
continue
}
v.SetContent(x+w, y+(iw*iCellSizeHeight+iw), g.internalHorizontalBorder, nil, g.style)
for w := 0; w < width; w++ {
for iw := 1; iw < g.numCellsVertical; iw++ {
v.SetContent(x+w, y+(iw*iCellSizeHeight+iw), g.internalHorizontalBorder, nil, style)
}
if w%(iCellSizeWidth+1) == 0 {
v.SetContent(x+w, y, g.verticalDownwardsTJunction, nil, g.style)
v.SetContent(x+w, y+height-1, g.verticalUpwardsTJunction, nil, g.style)
v.SetContent(x+w, y, g.verticalDownwardsTJunction, nil, style)
v.SetContent(x+w, y+height-1, g.verticalUpwardsTJunction, nil, style)
continue
}
v.SetContent(x+w, y, g.northBorder, nil, g.style)
v.SetContent(x+w, y+height-1, g.southBorder, nil, g.style)
v.SetContent(x+w, y, g.northBorder, nil, style)
v.SetContent(x+w, y+height-1, g.southBorder, nil, style)
}
for h := 1; h < height-1; h++ {
for h := 0; h < height; h++ {
if h == 0 {
v.SetContent(x, y, g.nwCorner, nil, style)
v.SetContent(x, y+height-1, g.swCorner, nil, style)
continue
}
for ih := 1; ih < int(g.numCellsHorizontal); ih++ {
if h == height-1 {
v.SetContent(x+width-1, y, g.neCorner, nil, style)
v.SetContent(x+width-1, y+height-1, g.seCorner, nil, style)
continue
}
for ih := 1; ih < g.numCellsHorizontal; ih++ {
if h%(iCellSizeHeight+1) == 0 {
v.SetContent(x+(ih*iCellSizeHeight+ih), y+h, g.crossJunction, nil, g.style)
v.SetContent(x+(ih*iCellSizeWidth+ih), y+h, g.crossJunction, nil, style)
continue
}
v.SetContent(x+(ih*iCellSizeHeight+ih), y+h, g.internalVerticalBorder, nil, g.style)
v.SetContent(x+(ih*iCellSizeWidth+ih), y+h, g.internalVerticalBorder, nil, style)
}
if h%(iCellSizeHeight+1) == 0 {
v.SetContent(x, y+h, g.horizontalRightTJunction, nil, g.style)
v.SetContent(x+width-1, y+h, g.horizontalLeftTJunction, nil, g.style)
v.SetContent(x, y+h, g.horizontalRightTJunction, nil, style)
v.SetContent(x+width-1, y+h, g.horizontalLeftTJunction, nil, style)
continue
}
v.SetContent(x, y+h, g.westBorder, nil, g.style)
v.SetContent(x+width-1, y+h, g.eastBorder, nil, g.style)
v.SetContent(x, y+h, g.westBorder, nil, style)
v.SetContent(x+width-1, y+h, g.eastBorder, nil, style)
}
}
func (g Grid) drawFill(v views.View) {
func (g *Grid) drawFill(v views.View) {
}
func (g Grid) Draw(v views.View) {
func (g *Grid) Draw(v views.View) {
g.drawBorders(v)
g.drawFill(v)
}

View file

@ -149,7 +149,9 @@ func (c *RenderContext) Draw(deltaTime int64, drawables []Drawable) {
c.view.Clear()
fpsText := CreateText(0, 0, 16, 1, fmt.Sprintf("%v FPS", fps), tcell.StyleDefault)
msPerFrame := float32(fps) / 1000.0
fpsText := CreateText(0, 0, 16, 1, fmt.Sprintf("%vms", msPerFrame), tcell.StyleDefault)
for _, d := range drawables {
d.Draw(c.view)

View file

@ -69,8 +69,11 @@ func (t *Text) Draw(s views.View) {
currentVPos := 0
drawText := func(text string) {
for i, r := range text {
s.SetContent(x+currentHPos+i, y+currentVPos, r, nil, t.style)
lastPos := 0
for _, r := range text {
s.SetContent(x+currentHPos+lastPos, y+currentVPos, r, nil, t.style)
lastPos++
}
}

View file

@ -50,14 +50,15 @@ func (vp *Viewport) ScreenLocation() util.Position {
return vp.screenLocation
}
func (vp *Viewport) DrawFromProvider(v views.View, provider func(x, y int) rune) {
func (vp *Viewport) DrawFromProvider(v views.View, provider func(x, y int) (rune, tcell.Style)) {
width, height := vp.viewportSize.WH()
originX, originY := vp.viewportCenter.WithOffset(-width/2, -height/2).XY()
screenX, screenY := vp.screenLocation.XY()
for h := originY; h < originY+height; h++ {
for w := originX; w < originX+width; w++ {
v.SetContent(screenX, screenY, provider(w, h), nil, vp.style)
r, style := provider(w, h)
v.SetContent(screenX, screenY, r, nil, style)
screenX += 1
}

View file

@ -1,5 +1,7 @@
package util
import "math/rand"
type Position struct {
x int
y int
@ -36,10 +38,6 @@ func SizeOf(width int, height int) Size {
return Size{int(width), int(height)}
}
func SizeOfInt(width int, height int) Size {
return Size{width, height}
}
func (s Size) Width() int {
return s.width
}
@ -52,6 +50,10 @@ func (s Size) WH() (int, int) {
return s.width, s.height
}
func (s Size) Area() int {
return s.width * s.height
}
func LimitIncrement(i int, limit int) int {
if (i + 1) > limit {
return i
@ -67,3 +69,7 @@ func LimitDecrement(i int, limit int) int {
return i - 1
}
func RandInt(min, max int) int {
return min + rand.Intn(max-min)
}