last_light/game/world/dungeon.go

296 lines
5.9 KiB
Go

package world
import (
"math/rand"
"mvvasilev/last_light/engine"
"mvvasilev/last_light/game/item"
"mvvasilev/last_light/game/model"
"mvvasilev/last_light/game/rpg"
"slices"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
)
type DungeonType int
const (
DungeonTypeBSP DungeonType = iota
DungeonTypeCaverns
DungeonTypeMine
DungeonTypeUndercity
)
func randomDungeonType() DungeonType {
return DungeonType(rand.Intn(4))
}
type Dungeon struct {
levels []*DungeonLevel
current int
}
func CreateDungeon(width, height int, depth int) *Dungeon {
levels := make([]*DungeonLevel, 0, depth)
for range depth {
levels = append(levels, CreateDungeonLevel(width, height, randomDungeonType()))
}
return &Dungeon{
levels: levels,
current: 0,
}
}
func (d *Dungeon) CurrentLevel() *DungeonLevel {
return d.levels[d.current]
}
func (d *Dungeon) MoveToNextLevel() (moved bool) {
if !d.HasNextLevel() {
return false
}
d.current++
return true
}
func (d *Dungeon) MoveToPreviousLevel() (moved bool) {
if !d.HasPreviousLevel() {
return false
}
d.current--
return true
}
func (d *Dungeon) NextLevel() *DungeonLevel {
if !d.HasNextLevel() {
return nil
}
return d.levels[d.current+1]
}
func (d *Dungeon) PreviousLevel() *DungeonLevel {
if !d.HasPreviousLevel() {
return nil
}
return d.levels[d.current-1]
}
func (d *Dungeon) HasPreviousLevel() bool {
return d.current-1 >= 0
}
func (d *Dungeon) HasNextLevel() bool {
return d.current+1 < len(d.levels)
}
type DungeonLevel struct {
groundLevel interface {
Map
WithPlayerSpawnPoint
WithNextLevelStaircasePosition
WithPreviousLevelStaircasePosition
}
entityLevel *EntityMap
itemLevel Map
multilevel Map
}
func CreateDungeonLevel(width, height int, dungeonType DungeonType) *DungeonLevel {
genTable := rpg.CreateLootTable()
genTable.Add(10, func() item.Item {
return item.CreateBasicItem(item.ItemTypeFish(), 1)
})
genTable.Add(1, func() item.Item {
itemTypes := []rpg.RPGItemType{
rpg.ItemTypeBow(),
rpg.ItemTypeLongsword(),
rpg.ItemTypeClub(),
rpg.ItemTypeDagger(),
rpg.ItemTypeHandaxe(),
rpg.ItemTypeJavelin(),
rpg.ItemTypeLightHammer(),
rpg.ItemTypeMace(),
rpg.ItemTypeQuarterstaff(),
rpg.ItemTypeSickle(),
rpg.ItemTypeSpear(),
}
itemType := itemTypes[rand.Intn(len(itemTypes))]
rarities := []rpg.ItemRarity{
rpg.ItemRarity_Common,
rpg.ItemRarity_Uncommon,
rpg.ItemRarity_Rare,
rpg.ItemRarity_Epic,
rpg.ItemRarity_Legendary,
}
return rpg.GenerateItemOfTypeAndRarity(itemType, rarities[rand.Intn(len(rarities))])
})
var groundLevel interface {
Map
WithRooms
WithPlayerSpawnPoint
WithNextLevelStaircasePosition
WithPreviousLevelStaircasePosition
}
switch dungeonType {
case DungeonTypeBSP:
groundLevel = CreateBSPDungeonMap(width, height, 4)
default:
groundLevel = CreateBSPDungeonMap(width, height, 4)
}
items := SpawnItems(groundLevel.Rooms(), 0.02, genTable, []engine.Position{
groundLevel.NextLevelStaircasePosition(),
groundLevel.PlayerSpawnPoint(),
groundLevel.PreviousLevelStaircasePosition(),
})
itemLevel := CreateEmptyDungeonLevel(width, height)
for _, it := range items {
if !groundLevel.TileAt(it.Position().XY()).Passable() {
continue
}
itemLevel.SetTileAt(it.Position().X(), it.Position().Y(), it)
}
d := &DungeonLevel{
groundLevel: groundLevel,
entityLevel: CreateEntityMap(width, height),
itemLevel: itemLevel,
}
d.multilevel = CreateMultilevelMap(
d.groundLevel,
d.itemLevel,
d.entityLevel,
)
return d
}
func SpawnItems(spawnableAreas []engine.BoundingBox, maxItemRatio float32, genTable *rpg.LootTable, forbiddenPositions []engine.Position) []Tile {
rooms := spawnableAreas
itemTiles := make([]Tile, 0, 10)
for _, r := range rooms {
maxItems := int(maxItemRatio * float32(r.Size().Area()))
if maxItems < 1 {
continue
}
numItems := rand.Intn(maxItems)
for range numItems {
itemType := genTable.Generate()
if itemType == nil {
continue
}
pos := engine.PositionAt(
engine.RandInt(r.Position().X()+1, r.Position().X()+r.Size().Width()-1),
engine.RandInt(r.Position().Y()+1, r.Position().Y()+r.Size().Height()-1),
)
if slices.Contains(forbiddenPositions, pos) {
continue
}
itemTiles = append(itemTiles, CreateItemTile(pos, itemType))
}
}
return itemTiles
}
func (d *DungeonLevel) PlayerSpawnPoint() engine.Position {
return d.groundLevel.PlayerSpawnPoint()
}
func (d *DungeonLevel) NextLevelStaircase() engine.Position {
return d.groundLevel.NextLevelStaircasePosition()
}
func (d *DungeonLevel) PreviousLevelStaircase() engine.Position {
return d.groundLevel.PreviousLevelStaircasePosition()
}
func (d *DungeonLevel) DropEntity(uuid uuid.UUID) {
d.entityLevel.DropEntity(uuid)
}
func (d *DungeonLevel) AddEntity(entity model.MovableEntity, presentation rune, style tcell.Style) {
d.entityLevel.AddEntity(entity, presentation, style)
}
func (d *DungeonLevel) MoveEntity(uuid uuid.UUID, dx, dy int) {
d.entityLevel.MoveEntity(uuid, dx, dy)
}
func (d *DungeonLevel) MoveEntityTo(uuid uuid.UUID, x, y int) {
d.entityLevel.MoveEntityTo(uuid, x, y)
}
func (d *DungeonLevel) RemoveItemAt(x, y int) item.Item {
if !d.groundLevel.Size().Contains(x, y) {
return nil
}
tile := d.itemLevel.TileAt(x, y)
itemTile, ok := tile.(*ItemTile)
if !ok {
return nil
}
d.itemLevel.SetTileAt(x, y, nil)
return itemTile.Item()
}
func (d *DungeonLevel) SetItemAt(x, y int, it item.Item) (success bool) {
if !d.TileAt(x, y).Passable() {
return false
}
d.itemLevel.SetTileAt(x, y, CreateItemTile(engine.PositionAt(x, y), it))
return true
}
func (d *DungeonLevel) TileAt(x, y int) Tile {
return d.multilevel.TileAt(x, y)
}
func (d *DungeonLevel) IsTilePassable(x, y int) bool {
if !d.groundLevel.Size().Contains(x, y) {
return false
}
return d.TileAt(x, y).Passable()
}
func (d *DungeonLevel) Flatten() Map {
return d.multilevel
}