The game is "playable" (but really limited) since a few days now and it's time to move on new tough topics. I see a few "next steps" that I'm going to throw there in no particular order:
- adding items on the floor and have the ability to pick them up / drop them down
- map transitions (being able to go from map to map)
- NPCs fighting back
- merchants / stores
- spells
- consumables
- infos on a given tile (name of the NPC if there is one, does he/it looks bad, ...)
- map generation (later on probably)
- "Fog of war" in dungeons
So I created the issues for all those features in gocastle/issues
I'll try to tackle the first one first because, let's be honest, inventory is a bit dull right now.
The first issue is to create an display items on the ground. Ideally, I'd like NPCs to go OVER then, and objects be display over tiles (or else we won't see anything). This was also working like this in "Castle of the winds" and since I'm taking my inspiration from it...
We I think about it, it's really close to the Avatar functions and maybe copying/tweaking them will be enough.
// Object represents an object with its properties.
type Object struct {
Name string // Object name.
Category string // Object category.
Weight int // Object weight in grams
InInventory bool // Is Object in inventory
Equipped bool // Is Object equipped
PosX int // Object position
PosY int // Object position
Stats []ObjectStat // Object stats (e.g., strength, health, etc.).
CanvasImage *canvas.Image // Object image
CanvasPath string // Image path for Object
}
// DrawObject displays an object's image on the mapContainer
func (subject *Object) DrawObject(mapContainer *fyne.Container) {
// don't put object in container is object is in inventory
if !subject.InInventory {
subject.CanvasImage.FillMode = canvas.ImageFillOriginal
subject.CanvasImage.Resize(fyneTileSize)
subject.MoveObject(subject.PosX, subject.PosY)
mapContainer.Add(subject.CanvasImage)
}
}
// MoveObject moves object's coordinates and updates image position on map
func (subject *Object) MoveObject(futurePosX int, futurePosY int) {
// assign new values for subject position
subject.PosX = futurePosX
subject.PosY = futurePosY
subject.CanvasImage.Move(fyne.NewPos(float32(futurePosX*tileSize), float32(futurePosY*tileSize)))
}
I basically copied the Avatar logic in the object logic (added InInventory, PosX|Y, CanvasImage|Path)
I also had modification to make to existing function like AddObjectToInventory
func (player *CharacterStats) AddObjectToInventory(obj Object, equip bool) int {
player.Inventory = append(player.Inventory, obj)
index := len(player.Inventory) - 1
// TODO rework
player.Inventory[index].InInventory = true
if equip {
player.EquipItem(index)
}
player.ComputeWeight()
return index
}
Last thing I needed to make sure logic was sound was to create in game.go a knife and display it with DrawObject.
knife2, err := model.CreateObject(model.HuntingKnife, 10, 10)
if err != nil {
err = fmt.Errorf("unable to create knife: %w", err)
log.Fatalf("NewGame error: %s", err)
}
knife2.DrawObject(mapContainer)
And it kind of works...
First issue I have is that I can't manipulate it easily, especially if items go to/from inventory. I'll then have to make a ObjectsOnMap list like the NPC list to persist and manipulate them.
Second issue is that the knife is display on top of my character, which is something I feared. I have to make sure NPCs and characters are always on top. But that nice progress anyway :)
One trick is to keep a reference of the Avatar as an Object stored in the mapContainer.Objects, and remove/re-add the Avatar each time we move. This way, player and NPCs are always latest objects in mapContainer and thus on top.
type Avatar struct {
CanvasImage *canvas.Image
CanvasPath string
PosX int
PosY int
+ ObjectInMapContainer *fyne.CanvasObject
}
// MoveAvatar moves avatar's coordinates and updates image position on map
func (subject *Avatar) MoveAvatar(mapContainer *fyne.Container, futurePosX, futurePosY int) {
// assign new values for subject position
subject.PosX = futurePosX
subject.PosY = futurePosY
subject.CanvasImage.Move(fyne.NewPos(float32(futurePosX*tileSize), float32(futurePosY*tileSize)))
// remove/re-add Avatar from mapContainer to redraw it on top
mapContainer.Remove(*subject.ObjectInMapContainer)
mapContainer.Add(*subject.ObjectInMapContainer)
}
Now that the 2nd problem is fixed, I can create a new attribute to the Map struct.
type Map struct {
Name string
PlayerStart Coord
spawnNPC SpawnNPC
NPCList model.NPCsOnCurrentMap
+ ObjectList []*model.Object
MapMatrix [][]int
}
Contrary to the NPCList (model.NPCsOnCurrentMap.List is a slice of NPCStats), I used a slice of model.Object pointers, which I hope will help me manipulate objects afterward. If it works well, I'll rework NPCList later.
Now, I can add my knife to this ObjectList in init.go and just loop on object list like for NPCs
// drawObjectList draws the "Objects on map" images on the mapContainer
func drawObjectList(mapContainer *fyne.Container) {
// Loop through the ObjectList slice and create/draw each Object
for _, object := range currentMap.ObjectList {
object.DrawObject(mapContainer)
}
}
A first trivial implementation on the "floor" part of the inventory screen may look like this
func displayFloorItems() (floorVBox *fyne.Container) {
floorVBox = container.NewVBox()
for _, item := range currentMap.ObjectList {
if item.PosX == player.Avatar.PosX && item.PosY == player.Avatar.PosY {
// item is on the same tile as player, display it in inventory
currentItemContainer := container.NewVBox()
nameLabel := widget.NewLabel(item.Name)
takeButton := widget.NewButton("Take", func() {
player.AddObjectToInventory(*item, false)
// Remove object from currentMap ObjectList
currentMap.FindObjectToRemove(item)
// Remove current container as well from floor container
floorVBox.Remove(currentItemContainer)
// TODO Refresh items in inventory
})
detailsButton := widget.NewButton("Details", func() {
// TODO display object statistics
})
currentItemContainer.Add(nameLabel)
currentItemContainer.Add(container.NewGridWithColumns(2, takeButton, detailsButton))
floorVBox.Add(currentItemContainer)
}
}
return floorVBox
}