Implement a first pass at a "fire" command, which allows throwing rocks, the max distance and the damage of which is based on the weight of the item and the strength of the player. Currently the actual numbers here likely need some tweaking, as the rocks are easily throwable at good distances but don't really deal any damage. Change-Id: Ic6ad0599444af44d8438b834237a1997b67f220f Reviewed-on: https://cl.tvl.fyi/c/depot/+/3764 Reviewed-by: grfn <grfn@gws.fyi> Tested-by: BuildkiteCI
190 lines
6.6 KiB
Haskell
190 lines
6.6 KiB
Haskell
{-# LANGUAGE TemplateHaskell #-}
|
|
--------------------------------------------------------------------------------
|
|
module Xanthous.Generators.Level.Dungeon
|
|
( Params(..)
|
|
, defaultParams
|
|
, parseParams
|
|
, generate
|
|
) where
|
|
--------------------------------------------------------------------------------
|
|
import Xanthous.Prelude hiding ((:>))
|
|
--------------------------------------------------------------------------------
|
|
import Control.Monad.Random
|
|
import Data.Array.ST
|
|
import Data.Array.IArray (amap)
|
|
import Data.Stream.Infinite (Stream(..))
|
|
import qualified Data.Stream.Infinite as Stream
|
|
import qualified Data.Graph.Inductive.Graph as Graph
|
|
import Data.Graph.Inductive.PatriciaTree
|
|
import qualified Data.List.NonEmpty as NE
|
|
import Data.Maybe (fromJust)
|
|
import Linear.V2
|
|
import Linear.Metric
|
|
import qualified Options.Applicative as Opt
|
|
--------------------------------------------------------------------------------
|
|
import Xanthous.Random
|
|
import Xanthous.Data hiding (x, y, _x, _y, edges, distance)
|
|
import Xanthous.Generators.Level.Util
|
|
import Xanthous.Util.Graphics (delaunay, straightLine)
|
|
import Xanthous.Util.Graph (mstSubGraph)
|
|
--------------------------------------------------------------------------------
|
|
|
|
data Params = Params
|
|
{ _numRoomsRange :: (Word, Word)
|
|
, _roomDimensionRange :: (Word, Word)
|
|
, _connectednessRatioRange :: (Double, Double)
|
|
}
|
|
deriving stock (Show, Eq, Ord, Generic)
|
|
makeLenses ''Params
|
|
|
|
defaultParams :: Params
|
|
defaultParams = Params
|
|
{ _numRoomsRange = (6, 8)
|
|
, _roomDimensionRange = (3, 12)
|
|
, _connectednessRatioRange = (0.1, 0.15)
|
|
}
|
|
|
|
parseParams :: Opt.Parser Params
|
|
parseParams = Params
|
|
<$> parseRange
|
|
"num-rooms"
|
|
"number of rooms to generate in the dungeon"
|
|
"ROOMS"
|
|
(defaultParams ^. numRoomsRange)
|
|
<*> parseRange
|
|
"room-size"
|
|
"size in tiles of one of the sides of a room"
|
|
"TILES"
|
|
(defaultParams ^. roomDimensionRange)
|
|
<*> parseRange
|
|
"connectedness-ratio"
|
|
( "ratio of edges from the delaunay triangulation to re-add to the "
|
|
<> "minimum-spanning-tree")
|
|
"RATIO"
|
|
(defaultParams ^. connectednessRatioRange)
|
|
<**> Opt.helper
|
|
where
|
|
parseRange name desc metavar (defMin, defMax) =
|
|
(,)
|
|
<$> Opt.option Opt.auto
|
|
( Opt.long ("min-" <> name)
|
|
<> Opt.value defMin
|
|
<> Opt.showDefault
|
|
<> Opt.help ("Minimum " <> desc)
|
|
<> Opt.metavar metavar
|
|
)
|
|
<*> Opt.option Opt.auto
|
|
( Opt.long ("max-" <> name)
|
|
<> Opt.value defMax
|
|
<> Opt.showDefault
|
|
<> Opt.help ("Maximum " <> desc)
|
|
<> Opt.metavar metavar
|
|
)
|
|
|
|
generate :: RandomGen g => Params -> Dimensions -> g -> Cells
|
|
generate params dims gen
|
|
= amap not
|
|
$ runSTUArray
|
|
$ fmap fst
|
|
$ flip runRandT gen
|
|
$ generate' params dims
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
generate' :: RandomGen g => Params -> Dimensions -> CellM g s (MCells s)
|
|
generate' params dims = do
|
|
cells <- initializeEmpty dims
|
|
rooms <- genRooms params dims
|
|
for_ rooms $ fillRoom cells
|
|
|
|
let fullRoomGraph = delaunayRoomGraph rooms
|
|
mst = mstSubGraph fullRoomGraph
|
|
mstEdges = Graph.edges mst
|
|
nonMSTEdges = filter (\(n₁, n₂, _) -> (n₁, n₂) `notElem` mstEdges)
|
|
$ Graph.labEdges fullRoomGraph
|
|
|
|
reintroEdgeCount <- floor . (* fromIntegral (length nonMSTEdges))
|
|
<$> getRandomR (params ^. connectednessRatioRange)
|
|
let reintroEdges = take reintroEdgeCount nonMSTEdges
|
|
corridorGraph = Graph.insEdges reintroEdges mst
|
|
|
|
corridors <- traverse
|
|
( uncurry corridorBetween
|
|
. over both (fromJust . Graph.lab corridorGraph)
|
|
) $ Graph.edges corridorGraph
|
|
|
|
for_ (join corridors) $ \pt -> lift $ writeArray cells pt True
|
|
|
|
pure cells
|
|
|
|
type Room = Box Word
|
|
|
|
genRooms :: MonadRandom m => Params -> Dimensions -> m [Room]
|
|
genRooms params dims = do
|
|
numRooms <- fromIntegral <$> getRandomR (params ^. numRoomsRange)
|
|
subRand . fmap (Stream.take numRooms . removeIntersecting []) . infinitely $ do
|
|
roomWidth <- getRandomR $ params ^. roomDimensionRange
|
|
roomHeight <- getRandomR $ params ^. roomDimensionRange
|
|
xPos <- getRandomR (0, dims ^. width - roomWidth)
|
|
yPos <- getRandomR (0, dims ^. height - roomHeight)
|
|
pure Box
|
|
{ _topLeftCorner = V2 xPos yPos
|
|
, _dimensions = V2 roomWidth roomHeight
|
|
}
|
|
where
|
|
removeIntersecting seen (room :> rooms)
|
|
| any (boxIntersects room) seen
|
|
= removeIntersecting seen rooms
|
|
| otherwise
|
|
= room :> removeIntersecting (room : seen) rooms
|
|
streamRepeat x = x :> streamRepeat x
|
|
infinitely = sequence . streamRepeat
|
|
|
|
delaunayRoomGraph :: [Room] -> Gr Room Double
|
|
delaunayRoomGraph rooms =
|
|
Graph.insEdges edges . Graph.insNodes nodes $ Graph.empty
|
|
where
|
|
edges = map (\((n₁, room₁), (n₂, room₂)) -> (n₁, n₂, roomDist room₁ room₂))
|
|
. over (mapped . both) snd
|
|
. delaunay @Double
|
|
. NE.fromList
|
|
. map (\p@(_, room) -> (boxCenter $ fromIntegral <$> room, p))
|
|
$ nodes
|
|
nodes = zip [0..] rooms
|
|
roomDist = distance `on` (boxCenter . fmap fromIntegral)
|
|
|
|
fillRoom :: MCells s -> Room -> CellM g s ()
|
|
fillRoom cells room =
|
|
let V2 posx posy = room ^. topLeftCorner
|
|
V2 dimx dimy = room ^. dimensions
|
|
in for_ [posx .. posx + dimx] $ \x ->
|
|
for_ [posy .. posy + dimy] $ \y ->
|
|
lift $ writeArray cells (V2 x y) True
|
|
|
|
corridorBetween :: MonadRandom m => Room -> Room -> m [V2 Word]
|
|
corridorBetween originRoom destinationRoom
|
|
= straightLine <$> origin <*> destination
|
|
where
|
|
origin = choose . NE.fromList =<< originEdge
|
|
destination = choose . NE.fromList =<< destinationEdge
|
|
originEdge = pickEdge originRoom originCorner
|
|
destinationEdge = pickEdge destinationRoom destinationCorner
|
|
pickEdge room corner = choose . over both (boxEdge room) $ cornerEdges corner
|
|
originCorner =
|
|
case ( compare (originRoom ^. topLeftCorner . _x)
|
|
(destinationRoom ^. topLeftCorner . _x)
|
|
, compare (originRoom ^. topLeftCorner . _y)
|
|
(destinationRoom ^. topLeftCorner . _y)
|
|
) of
|
|
(LT, LT) -> BottomRight
|
|
(LT, GT) -> TopRight
|
|
(GT, LT) -> BottomLeft
|
|
(GT, GT) -> TopLeft
|
|
|
|
(EQ, LT) -> BottomLeft
|
|
(EQ, GT) -> TopRight
|
|
(GT, EQ) -> TopLeft
|
|
(LT, EQ) -> BottomRight
|
|
(EQ, EQ) -> TopLeft -- should never happen
|
|
|
|
destinationCorner = opposite originCorner
|