Compare commits

...

5 Commits

Author SHA1 Message Date
Emi Simpson 4e408135d8
Rework the protoeffect system
& Add ActingUnit/TargettedUnit newtypes
2023-12-09 22:45:39 -05:00
Emi Simpson 7a501ac927
Rework triggers & add status effects 2023-12-09 12:56:43 -05:00
Emi Simpson a555ed4ea0
Implement a new status effect system 2023-12-08 09:37:40 -05:00
Emi Simpson a6a7473f05
Do hints more 2023-12-07 11:24:50 -05:00
Emi Simpson fd88082d77
Cleanup spare trace 2023-12-06 16:15:38 -05:00
8 changed files with 423 additions and 129 deletions

70
.hlint.yaml Normal file
View File

@ -0,0 +1,70 @@
# HLint configuration file
# https://github.com/ndmitchell/hlint
##########################
# This file contains a template configuration file, which is typically
# placed as .hlint.yaml in the root of your project
# Specify additional command line arguments
#
# - arguments: [--color, --cpp-simple, -XQuasiQuotes]
# Control which extensions/flags/modules/functions can be used
#
# - extensions:
# - default: false # all extension are banned by default
# - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
# - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
#
# - flags:
# - {name: -w, within: []} # -w is allowed nowhere
#
# - modules:
# - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
# - {name: Control.Arrow, within: []} # Certain modules are banned entirely
#
# - functions:
# - {name: unsafePerformIO, within: []} # unsafePerformIO can only appear in no modules
# Add custom hints for this project
#
# Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
# - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
# The hints are named by the string they display in warning messages.
# For example, if you see a warning starting like
#
# Main.hs:116:51: Warning: Redundant ==
#
# You can refer to that hint with `{name: Redundant ==}` (see below).
# Turn on hints that are off by default
#
# Ban "module X(module X) where", to require a real export list
# - warn: {name: Use explicit module export list}
#
# Replace a $ b $ c with a . b $ c
# - group: {name: dollar, enabled: true}
#
# Generalise map to fmap, ++ to <>
# - group: {name: generalise, enabled: true}
- group: {name: use-lens, enabled: true}
- group: {name: generalise-for-conciseness, enabled: true}
- group: {name: teaching, enabled: true}
# Ignore some builtin hints
# - ignore: {name: Use let}
# - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
# Define some custom infix operators
# - fixity: infixr 3 ~^#^~
# To generate a suitable file for HLint do:
# $ hlint --default > .hlint.yaml

View File

@ -51,6 +51,7 @@ library:
- ScopedTypeVariables - ScopedTypeVariables
- EmptyCase - EmptyCase
- DuplicateRecordFields - DuplicateRecordFields
- StandaloneDeriving
executables: executables:
maleghast-exe: maleghast-exe:

View File

@ -1,4 +1,3 @@
{-# OPTIONS_GHC -Wno-type-defaults #-}
module GameLogic module GameLogic
( chooseCharacter ( chooseCharacter
, applyEffect , applyEffect
@ -27,16 +26,16 @@ import Numeric.Natural (Natural)
import Lens.Micro import Lens.Micro
import Safe (atMay) import Safe (atMay)
import Debug.Trace
import Mechanics (universalModifiers, globalHooks) import Mechanics (universalModifiers, globalHooks)
import Data.Monoid (Any(getAny), getSum) import Data.Monoid (Any(getAny), getSum)
import Data.Coerce
-- | A version of `computeStat_` using `universalModifiers` as global modifiers -- | A version of `computeStat_` using `universalModifiers` as global modifiers
computeStat :: BoardState -> CharacterIdentifier -> Stat a -> a computeStat :: (Coercible cid c, c ~ CharacterIdentifier) => BoardState -> cid -> Stat a -> a
computeStat = computeStat_ universalModifiers computeStat = computeStat_ universalModifiers
-- | A version of `runHooks_` using `globalHooks` as the global hooks -- | A version of `runHooks_` using `globalHooks` as the global hooks
runHooks :: Trigger -> BoardState -> BoardState runHooks :: Broadcast -> BoardState -> BoardState
runHooks = runHooks_ globalHooks runHooks = runHooks_ globalHooks
data EngineState data EngineState
@ -51,7 +50,7 @@ data EngineState
playerChoice :: Player -> [Choice] -> EngineState playerChoice :: Player -> [Choice] -> EngineState
playerChoice = PlayerChoice playerChoice = PlayerChoice
unitChoice :: CharacterIdentifier -> [Choice] -> EngineState unitChoice :: (Coercible cid c, c ~ CharacterIdentifier) => cid -> [Choice] -> EngineState
unitChoice = playerChoice . owner unitChoice = playerChoice . owner
activePlayerChoice :: BoardState -> [Choice] -> EngineState activePlayerChoice :: BoardState -> [Choice] -> EngineState
@ -66,8 +65,8 @@ chooseCharacter board = playerChoice player <$> fmap toList (nonEmpty $ board ^.
player :: Player player :: Player
player = board ^. activePlayer player = board ^. activePlayer
possibleActivations :: SimpleFold BoardState Choice possibleActivations :: SimpleFold BoardState Choice
possibleActivations = untappedUnits . filtered ((== player) . owner) . to activateUnit possibleActivations = untappedUnits . filtered ((== player) . owner) . to (activateUnit . ActingUnit)
activateUnit :: CharacterIdentifier -> Choice activateUnit :: ActingUnit -> Choice
activateUnit cid = mkChoice ("Activate unit " ++ c) [ChooseActMove cid] activateUnit cid = mkChoice ("Activate unit " ++ c) [ChooseActMove cid]
where where
c = renderCharacterHandle board cid c = renderCharacterHandle board cid
@ -81,6 +80,7 @@ forcedMovementValidDirections w h currentLocation (locus, moveType, _) =
where where
directionToLocus = identifyCardinalDirection w h currentLocation locus directionToLocus = identifyCardinalDirection w h currentLocation locus
{-# ANN computePossibleSteps "HLint: ignore Apply De Morgan law" #-}
computePossibleSteps :: BoardState -> MovementSpecs -> Point -> [(Point, Natural)] computePossibleSteps :: BoardState -> MovementSpecs -> Point -> [(Point, Natural)]
computePossibleSteps board (MovementSpecs {..}) currentLocation = mapMaybe getRemainingMov validDestinations computePossibleSteps board (MovementSpecs {..}) currentLocation = mapMaybe getRemainingMov validDestinations
where where
@ -110,8 +110,8 @@ computePossibleSteps board (MovementSpecs {..}) currentLocation = mapMaybe getRe
endOfMovementEffects :: MovementSpecs -> CharacterIdentifier -> [Effect] endOfMovementEffects :: MovementSpecs -> CharacterIdentifier -> [Effect]
endOfMovementEffects (MovementSpecs {..}) cid = endOfMovementEffects (MovementSpecs {..}) cid =
[ConfirmMove] [ConfirmMove]
++ [DropToken SpeedSlow cid | movSpendTokens ] ++ [DropToken SpeedSlow (TargettedUnit cid) | movSpendTokens ]
++ [Event $ EndMove cid | movEndMoveTrigger] ++ [Event $ Broadcast EndMove (ActingUnit cid) | movEndMoveTrigger]
generateMovementChoice :: BoardState -> MovementSpecs -> CharacterIdentifier -> Point -> (Point, Natural) -> Choice generateMovementChoice :: BoardState -> MovementSpecs -> CharacterIdentifier -> Point -> (Point, Natural) -> Choice
generateMovementChoice board specs@(MovementSpecs {..}) cid originalLocation (dest, remainingMov) = generateMovementChoice board specs@(MovementSpecs {..}) cid originalLocation (dest, remainingMov) =
@ -192,7 +192,7 @@ applyEffect (Move specs@(MovementSpecs{..}) cid) board = case unitPosition board
then continue $ board <++ if movMinimum then continue $ board <++ if movMinimum
then dontMoveEffects then dontMoveEffects
else finishMoveEffects else finishMoveEffects
else choiceBuilder $ (++ movementChoices) $ if movCompelled else choiceBuilder . (++ movementChoices) $ if movCompelled
then [] then []
else if movMinimum else if movMinimum
then [dontMoveChoice] then [dontMoveChoice]
@ -202,7 +202,7 @@ applyEffect (Move specs@(MovementSpecs{..}) cid) board = case unitPosition board
dontMoveChoice = mkChoice "Don't move" dontMoveEffects dontMoveChoice = mkChoice "Don't move" dontMoveEffects
finishMoveEffects = endOfMovementEffects specs cid finishMoveEffects = endOfMovementEffects specs cid
finishMoveChoice = mkChoice "Finish moving" finishMoveEffects finishMoveChoice = mkChoice "Finish moving" finishMoveEffects
endMoveTrigger = Event $ EndMove cid endMoveTrigger = Event $ Broadcast EndMove (ActingUnit cid)
choiceBuilder = maybe (unitChoice cid) playerChoice $ movForced ^? _Just . _3 choiceBuilder = maybe (unitChoice cid) playerChoice $ movForced ^? _Just . _3
applyEffect (MoveTo dest) board = continue $ moveUnit dest board applyEffect (MoveTo dest) board = continue $ moveUnit dest board
applyEffect (DropToken token unit) board = continue $ applyEffect (DropToken token unit) board = continue $
@ -214,22 +214,22 @@ applyEffect (Act cid) board = case board ^.. ixCharacter cid . baseStats . actio
applyEffect (Target fromPerspective range eligability ultimateEffect) board = unitChoice fromPerspective choices applyEffect (Target fromPerspective range eligability ultimateEffect) board = unitChoice fromPerspective choices
where where
locus = unitPosition board fromPerspective locus = unitPosition board fromPerspective
potentialUnits = maybe [] (cidsInRange board range) locus potentialUnits = TargettedUnit <$> (maybe [] (cidsInRange board range) locus)
eligableUnits = filter (eligability board) potentialUnits eligableUnits = filter (eligability board) potentialUnits
buildChoice targetCid = buildChoice targetCid =
mkChoice mkChoice
("Target " ++ renderCharacterHandle board targetCid) ("Target " ++ renderCharacterHandle board targetCid)
[BodyBlock targetCid ultimateEffect] [BodyBlock targetCid ultimateEffect]
choices = buildChoice <$> eligableUnits choices = buildChoice <$> eligableUnits
applyEffect (BodyBlock targettedUnit ultimateEffect) board = if canBB then allChoices else cantBBResult applyEffect (BodyBlock originalTarget ultimateEffect) board = if canBB then allChoices else cantBBResult
where where
potentialBBers = fromMaybe [] $ adjacentAllies board targettedUnit potentialBBers = fromMaybe [] $ adjacentAllies board originalTarget
canBB = isNecromancer targettedUnit && potentialBBers /= [] canBB = isNecromancer originalTarget && not (null potentialBBers)
buildChoice bber = mkChoice ("Bodyblock with " ++ renderCharacterHandle board bber) (ultimateEffect bber) buildChoice bber = mkChoice ("Bodyblock with " ++ renderCharacterHandle board bber) (ultimateEffect bber)
dontBodyblock = mkChoice "Take the hit to your necromancer" (ultimateEffect targettedUnit) dontBodyblock = mkChoice "Take the hit to your necromancer" (ultimateEffect originalTarget)
bbChoices = buildChoice <$> potentialBBers bbChoices = buildChoice . TargettedUnit <$> potentialBBers
allChoices = unitChoice targettedUnit $ dontBodyblock : bbChoices allChoices = unitChoice originalTarget $ dontBodyblock : bbChoices
cantBBResult = continue $ board <++ ultimateEffect targettedUnit cantBBResult = continue $ board <++ ultimateEffect originalTarget
applyEffect (ResolveAttack attacker attack defender) board = Roll actualDiceRolled keepHighest consequence applyEffect (ResolveAttack attacker attack defender) board = Roll actualDiceRolled keepHighest consequence
where where
attacker' = board ^? ixCharacter attacker attacker' = board ^? ixCharacter attacker
@ -252,7 +252,7 @@ applyEffect (ResolveAttack attacker attack defender) board = Roll actualDiceRoll
actualDiceRolled = fromIntegral $ if netDice <= 0 then 2 else netDice actualDiceRolled = fromIntegral $ if netDice <= 0 then 2 else netDice
keepHighest = netDice > 0 keepHighest = netDice > 0
toHit = maybe 1 getDefense defender' toHit = maybe 1 getDefense defender'
attackerStrWeak = fromMaybe 0 $ attacker' ^? _Just . tokenCount . ofToken StrWeak attackerStrWeak = sum $ attacker' ^? _Just . tokenCount . ofToken StrWeak
tokenDamageModifier tokenDamageModifier
| attackerStrWeak < 0 = \x -> x-1 | attackerStrWeak < 0 = \x -> x-1
| attackerStrWeak > 0 = (+1) | attackerStrWeak > 0 = (+1)
@ -263,7 +263,7 @@ applyEffect (ResolveAttack attacker attack defender) board = Roll actualDiceRoll
onHit = hitDamage : otherEffects attack onHit = hitDamage : otherEffects attack
onHeadshot = onHit ++ headshotEffects attack onHeadshot = onHit ++ headshotEffects attack
consequence Six consequence Six
| not $ null $ headshotEffects attack = mkChoice "Headshot!" onHeadshot | not . null $ headshotEffects attack = mkChoice "Headshot!" onHeadshot
consequence r consequence r
| r >= toHit = mkChoice "Hit!" onHit | r >= toHit = mkChoice "Hit!" onHit
| otherwise = mkChoice "Graze" onMiss | otherwise = mkChoice "Graze" onMiss
@ -273,28 +273,29 @@ applyEffect (InflictDamage damageType incomingAmount recipient) board = continue
recipient' = board ^? ixCharacter recipient recipient' = board ^? ixCharacter recipient
armorType = maybe NoArmor (^. baseStats . armL) recipient' armorType = maybe NoArmor (^. baseStats . armL) recipient'
armorReduction = if armorType `blocks` damageType then 1 else 0 armorReduction = if armorType `blocks` damageType then 1 else 0
tokenReduction = signum $ fromMaybe 0 $ recipient' ^? _Just . tokenCount . ofToken VitalVulnr tokenReduction = signum . sum $ (recipient' ^? _Just . tokenCount . ofToken VitalVulnr)
totalReduction = if damageType == DevilDamage then min 0 tokenReduction else armorReduction + tokenReduction totalReduction = if damageType == DevilDamage then min 0 tokenReduction else armorReduction + tokenReduction
netDamage netDamage
| totalReduction >= incomingAmount' = 0 | totalReduction >= incomingAmount' = 0
| otherwise = fromIntegral $ incomingAmount' - totalReduction | otherwise = fromIntegral $ incomingAmount' - totalReduction
board' = trace ("iA': " ++ show incomingAmount' ++ " | aR: " ++ show armorReduction ++ " | r: " ++ show recipient) $ board board' = board
& ixCharacter recipient %~ clearUpToNTokens Health netDamage & ixCharacter recipient %~ clearUpToNTokens Health netDamage
& if incomingAmount' > armorReduction & if incomingAmount' > armorReduction
then ixCharacter recipient %~ removeTokenInCategory VitalVulnr then ixCharacter recipient %~ removeTokenInCategory VitalVulnr
else id else id
updatedHealth = fromMaybe 9999 $ board' ^? ixCharacter recipient . tokenCount . ofToken Health updatedHealth = fromMaybe 9999 $ board' ^? ixCharacter recipient . tokenCount . ofToken Health
dead = updatedHealth == 0 dead = updatedHealth == 0
damageEffects = [Event $ TookDamage recipient, if dead then Kill recipient else NoOp] damageEffects = [Event $ Broadcast TookDamage recipient, if dead then Kill recipient else NoOp]
applyEffect (InflictStatusEffect se target) board = continue $ board & ixCharacter target . statusEffects %~ (se :)
applyEffect (InflictTokens tokenType numberToAdd target) board = continue $ board & applyEffect (InflictTokens tokenType numberToAdd target) board = continue $ board &
ixCharacter target . tokenCount . ofToken tokenType +~ numberToAdd ixCharacter target . tokenCount . ofToken tokenType +~ numberToAdd
applyEffect (Kill unit) board = if isNecromancer unit applyEffect (Kill unit) board = if isNecromancer unit
then Victory $ otherPlayer $ owner unit then Victory . otherPlayer $ owner unit
else continue $ unitRemoved <++ [deathAlert] else continue $ unitRemoved <++ [deathAlert]
where where
deathLocation = unitPosition board unit deathLocation = unitPosition board unit
unitRemoved = removeUnit unit board unitRemoved = removeUnit unit board
deathAlert = maybe NoOp (Event . Died unit) deathLocation deathAlert = maybe NoOp (Event . Broadcast Died . (unit,)) deathLocation
applyEffect (EndTurn cid) board = continue $ afterPlayerSwitched <++ [StartTurn] applyEffect (EndTurn cid) board = continue $ afterPlayerSwitched <++ [StartTurn]
where where
afterUnitTapped = board & ixCharacter cid %~ tap afterUnitTapped = board & ixCharacter cid %~ tap
@ -319,7 +320,7 @@ instance Show (ChoiceTree' a) where
choicesText :: [String] choicesText :: [String]
choicesText = firstDecisions choices choicesText = firstDecisions choices
numberedChoices :: [String] numberedChoices :: [String]
numberedChoices = zipWith (\n c -> show (1+n) ++ ") " ++ c) [0..] choicesText numberedChoices = zipWith (\n c -> show n ++ ") " ++ c) ([1..] :: [Int]) choicesText
viewChoices :: String viewChoices :: String
viewChoices = mconcat $ intersperse "\n" numberedChoices viewChoices = mconcat $ intersperse "\n" numberedChoices
show (Random numDice rollType _) = "Roll " ++ show numDice ++ "d6 and take the " ++ (if rollType then "highest" else "lowest") show (Random numDice rollType _) = "Roll " ++ show numDice ++ "d6 and take the " ++ (if rollType then "highest" else "lowest")
@ -353,11 +354,11 @@ buildChoiceTree board = case popEffect board of
PlayerChoice chooser choices -> ChoiceTree $ Node board' chooser (convertChoice board' <$> choices) PlayerChoice chooser choices -> ChoiceTree $ Node board' chooser (convertChoice board' <$> choices)
Roll n d outcomes -> ChoiceTree $ Random n d (convertChoice board' <$> outcomes) Roll n d outcomes -> ChoiceTree $ Random n d (convertChoice board' <$> outcomes)
Continue board'' -> buildChoiceTree board'' Continue board'' -> buildChoiceTree board''
Victory player -> ChoiceTree $ EndOfGame $ Just player Victory player -> ChoiceTree . EndOfGame $ Just player
makeChoice :: n -> ChoiceTree' n -> Maybe ChoiceTree makeChoice :: n -> ChoiceTree' n -> Maybe ChoiceTree
makeChoice n (EndOfGame _) = never n makeChoice n (EndOfGame _) = never n
makeChoice die (Random _ _ outcomes) = Just $ ctTree $ outcomes die makeChoice die (Random _ _ outcomes) = Just . ctTree $ outcomes die
makeChoice indx (Node b p options) = trimmedChoices <$> providedDecision makeChoice indx (Node b p options) = trimmedChoices <$> providedDecision
where where
providedDecision :: Maybe String providedDecision :: Maybe String

View File

@ -1,5 +1,3 @@
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# HLINT ignore "Eta reduce" #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-} {-# OPTIONS_GHC -Wno-name-shadowing #-}
module GameModel module GameModel
( DieFace(..) ( DieFace(..)
@ -8,6 +6,10 @@ module GameModel
, Armor(..) , Armor(..)
, DamageType(..) , DamageType(..)
, Attack(Attack) , Attack(Attack)
, ActingUnit(..)
, actingUnit
, TargettedUnit(..)
, targettedUnit
, headshotEffectsL , headshotEffectsL
, meleeL , meleeL
, otherEffectsL , otherEffectsL
@ -49,6 +51,8 @@ module GameModel
, otherPlayer , otherPlayer
, Token(..) , Token(..)
, ofToken , ofToken
, tokenEq
, Broadcast(..)
, Stat(..) , Stat(..)
, BaseStats(..) , BaseStats(..)
, nameL , nameL
@ -67,7 +71,9 @@ module GameModel
, clearUpToNTokens , clearUpToNTokens
, getSpeed , getSpeed
, getDefense , getDefense
, statusEffects
, CharacterIdentifier , CharacterIdentifier
, cidEq
, owner , owner
, ownerL , ownerL
, isNecromancer , isNecromancer
@ -128,8 +134,8 @@ module GameModel
, allStatsAreMonoids , allStatsAreMonoids
, HasMonoidInstance , HasMonoidInstance
, Hook(..) , Hook(..)
, hookTriggerL , hookToFunc
, hookEffectL , hooksToFunc
, Modifier(..) , Modifier(..)
, Trait(..) , Trait(..)
, traitNameL , traitNameL
@ -137,6 +143,7 @@ module GameModel
, traitModifiersL , traitModifiersL
, computeStat_ , computeStat_
, runHooks_ , runHooks_
, StatusEffect(..)
) where ) where
import Util (toMaybe, dup, secondClassLensNames, (??)) import Util (toMaybe, dup, secondClassLensNames, (??))
@ -156,6 +163,7 @@ import Lens.Micro.TH (makeLenses, makeLensesWith, generateUpdateableOptics)
import Lens.Micro.Extras (preview) import Lens.Micro.Extras (preview)
import Data.Data ((:~:)(..)) import Data.Data ((:~:)(..))
import System.Random (Random(..)) import System.Random (Random(..))
import Data.Coerce
data DieFace = One | Two | Three | Four | Five | Six data DieFace = One | Two | Three | Four | Five | Six
deriving (Eq, Ord, Enum, Read) deriving (Eq, Ord, Enum, Read)
@ -166,7 +174,7 @@ instance Random DieFace where
lowN = fromEnum low lowN = fromEnum low
hiN = fromEnum hi hiN = fromEnum hi
(r, g') = randomR (lowN, hiN) g (r, g') = randomR (lowN, hiN) g
random g = randomR (One, Six) g random = randomR (One, Six)
bestOrWorst :: Bool -> [DieFace] -> DieFace bestOrWorst :: Bool -> [DieFace] -> DieFace
bestOrWorst True = maximum bestOrWorst True = maximum
@ -222,14 +230,23 @@ instance Num DieFace where
type CharacterIdentifier = (Player, Int) type CharacterIdentifier = (Player, Int)
owner :: CharacterIdentifier -> Player cidEq :: (Coercible a b, Eq b) => a -> b -> Bool
owner = fst cidEq = (==) . coerce
owner :: (Coercible cid c, c ~ CharacterIdentifier) => cid -> Player
owner cid' = fst cid
where
cid :: CharacterIdentifier
cid = coerce cid'
ownerL :: Lens' CharacterIdentifier Player ownerL :: Lens' CharacterIdentifier Player
ownerL = _1 ownerL = _1
isNecromancer :: CharacterIdentifier -> Bool isNecromancer :: (Coercible cid c, c ~ CharacterIdentifier) => cid -> Bool
isNecromancer = snd >>> (== 0) isNecromancer cid' = (==0) $ snd cid
where
cid :: CharacterIdentifier
cid = coerce cid'
showCID :: CharacterIdentifier -> Char showCID :: CharacterIdentifier -> Char
showCID (cidOwner, idx) = (if cidOwner == Max then upperLetters else lowerLetters) !! idx showCID (cidOwner, idx) = (if cidOwner == Max then upperLetters else lowerLetters) !! idx
@ -296,12 +313,29 @@ newtype Point = Point Natural
enumeratePoints :: Natural -> Natural -> [Point] enumeratePoints :: Natural -> Natural -> [Point]
enumeratePoints w h = Point <$> [0..w * h - 1] enumeratePoints w h = Point <$> [0..w * h - 1]
data Trigger data Trigger a where
= TurnStart TurnStart :: Trigger ()
| TookDamage CharacterIdentifier TookDamage :: Trigger TargettedUnit
| Died CharacterIdentifier Point Died :: Trigger (TargettedUnit, Point)
| EndMove CharacterIdentifier EndMove :: Trigger ActingUnit
deriving (Eq, Show)
deriving instance Eq (Trigger a)
deriving instance Show (Trigger a)
-- | Compare to triggers to check if they're equal
--
-- If they are, you get a proof of the equality of their associated data types.
triggerEq :: Trigger a -> Trigger b -> Maybe (a :~: b)
triggerEq TurnStart TurnStart = Just Refl
triggerEq TookDamage TookDamage = Just Refl
triggerEq Died Died = Just Refl
triggerEq EndMove EndMove = Just Refl
triggerEq _ _ = Nothing
-- | A pair containing a 'Trigger' and its associated data
--
-- Effectively an event as a whole
data Broadcast = forall d. Broadcast (Trigger d) d
data OrthagonalDirection data OrthagonalDirection
= North = North
@ -404,8 +438,8 @@ data ProtoMovementSpecs = ProtoMovementSpecs
, movCompelled :: Bool -- ^ If movement can be stopped prematurely , movCompelled :: Bool -- ^ If movement can be stopped prematurely
} }
basicMove :: CharacterIdentifier -> Effect basicMove :: ActingUnit -> Effect
basicMove = InitMove $ ProtoMovementSpecs basicMove = (InitMove $ ProtoMovementSpecs
{ movVerb = "Move" { movVerb = "Move"
, movFree' = Nothing , movFree' = Nothing
, movMinimum = True , movMinimum = True
@ -414,10 +448,10 @@ basicMove = InitMove $ ProtoMovementSpecs
, movSpendTokens = True , movSpendTokens = True
, movEndMoveTrigger = True , movEndMoveTrigger = True
, movCompelled = False , movCompelled = False
} }) . (^. actingUnit)
forcedMove :: ForcedMoveType -> Natural -> Player -> Either CharacterIdentifier Point -> CharacterIdentifier -> Effect forcedMove :: ForcedMoveType -> Natural -> Player -> Either CharacterIdentifier Point -> TargettedUnit -> Effect
forcedMove fmType amt compeller locus = InitMove $ ProtoMovementSpecs forcedMove fmType amt compeller locus = (InitMove $ ProtoMovementSpecs
{ movVerb = show fmType { movVerb = show fmType
, movFree' = Just True , movFree' = Just True
, movMinimum = False , movMinimum = False
@ -426,7 +460,25 @@ forcedMove fmType amt compeller locus = InitMove $ ProtoMovementSpecs
, movSpendTokens = False , movSpendTokens = False
, movEndMoveTrigger = False , movEndMoveTrigger = False
, movCompelled = True , movCompelled = True
} }) . (^. targettedUnit)
-- | Designates a character identifier of the unit acting in an effect/event
--
-- Acting includes moving, attacking, or using an ability
newtype ActingUnit = ActingUnit CharacterIdentifier
deriving (Eq, Show)
-- | A lens reaching into `ActingUnit`
actingUnit :: Lens' ActingUnit CharacterIdentifier
actingUnit f (ActingUnit au) = ActingUnit <$> f au
-- | Designates a character identifier of the unit being targetted by an effect/attack
newtype TargettedUnit = TargettedUnit CharacterIdentifier
deriving (Eq, Show)
-- | A lens reaching into `TargettedUnit`
targettedUnit :: Lens' TargettedUnit CharacterIdentifier
targettedUnit f (TargettedUnit au) = TargettedUnit <$> f au
data Effect data Effect
@ -436,13 +488,13 @@ data Effect
| StartTurn | StartTurn
-- | Send some trigger to every Unit -- | Send some trigger to every Unit
| Event Trigger | Event Broadcast
-- | Active player chooses whether they want to move or act first -- | Active player chooses whether they want to move or act first
| ChooseActMove CharacterIdentifier | ChooseActMove ActingUnit
-- | Active player may choose whether they want to act or move -- | Active player may choose whether they want to act or move
| ActOrMove CharacterIdentifier | ActOrMove ActingUnit
-- | Mark the start of movement -- | Mark the start of movement
-- Can be finalized later with FinalizeMove -- Can be finalized later with FinalizeMove
@ -464,46 +516,51 @@ data Effect
-- | Remove up to one token from the given category from a unit -- | Remove up to one token from the given category from a unit
| forall n. Num n => DropToken | forall n. Num n => DropToken
(Token n) -- ^ The token category to drop from (Token n) -- ^ The token category to drop from
CharacterIdentifier -- ^ Which character drops a token TargettedUnit -- ^ Which character drops a token
-- | Confirms a Move, placing the unit in the target space -- | Confirms a Move, placing the unit in the target space
| ConfirmMove | ConfirmMove
-- | Allow a character to Act -- | Allow a character to Act
| Act CharacterIdentifier | Act ActingUnit
-- | Target a unit in a given range, then run a different event -- | Target a unit in a given range, then run a different event
| Target | Target
CharacterIdentifier -- ^ ACTing unit ActingUnit -- ^ ACTing unit
(Natural, Natural) -- ^ Range (Natural, Natural) -- ^ Range
(BoardState -> CharacterIdentifier -> Bool) -- ^ Target filter (BoardState -> TargettedUnit -> Bool) -- ^ Target filter
(CharacterIdentifier -> [Effect]) -- ^ Ultimate effect (TargettedUnit -> [Effect]) -- ^ Ultimate effect
-- | Check if a character can body block -- | Check if a character can body block
-- If they can, offer a choice of target to the targetted player. Pass result on to -- If they can, offer a choice of target to the targetted player. Pass result on to
-- the effect. -- the effect.
| BodyBlock CharacterIdentifier (CharacterIdentifier -> [Effect]) | BodyBlock TargettedUnit (TargettedUnit -> [Effect])
-- | Resolve an attack -- | Resolve an attack
| ResolveAttack | ResolveAttack
CharacterIdentifier -- ^ Attacker ActingUnit -- ^ Attacker
Attack -- ^ Attack information Attack -- ^ Attack information
CharacterIdentifier -- ^ Target TargettedUnit -- ^ Target
| InflictDamage | InflictDamage
DamageType DamageType
Natural -- ^ Damage amount Natural -- ^ Damage amount
CharacterIdentifier -- ^ Target TargettedUnit -- ^ Target
-- | Add a status effect to a character
| InflictStatusEffect
StatusEffect -- ^ Effect to inflict
TargettedUnit -- ^ Target
| forall n. Num n => InflictTokens | forall n. Num n => InflictTokens
(Token n) -- ^ Token kind (Token n) -- ^ Token kind
n -- ^ Token amount n -- ^ Token amount
CharacterIdentifier -- ^ Target TargettedUnit -- ^ Target
| Kill CharacterIdentifier | Kill TargettedUnit
-- | Tap the active unit, change the active player -- | Tap the active unit, change the active player
| EndTurn CharacterIdentifier | EndTurn ActingUnit
data Attack = Attack data Attack = Attack
{ headshotEffects :: [Effect] { headshotEffects :: [Effect]
@ -553,16 +610,28 @@ data BaseStats = BaseStats
, df :: DieFace , df :: DieFace
, arm :: Armor , arm :: Armor
, traits :: [Trait] , traits :: [Trait]
, actions :: [CharacterIdentifier -> Choice] , actions :: [ActingUnit -> Choice]
} }
-- | Some effect which is activated by some trigger event -- | Some effect which is activated by some trigger event
data Hook h = Hook data Hook o = forall d. Hook
{ hookTrigger :: Trigger -- ^ The trigger which should activate this effect { hookTrigger :: Trigger d -- ^ The trigger which should activate this effect
-- | What this effect does. Recieves board & this character's CID -- | What this effect does. Recieves board & this character's CID
, hookEffect :: BoardState -> h , hookEffect :: d -> BoardState -> o
} }
-- | Evaluate a hook for a given trigger
--
-- If the trigger does not apply to this hook, then `mempty` is returned.
hookToFunc :: Monoid h => Hook h -> Broadcast -> BoardState -> h
hookToFunc (Hook {..}) (Broadcast trigger assocData) = case hookTrigger `triggerEq` trigger of
Just Refl -> hookEffect assocData
Nothing -> mempty
-- | A version of `hookToFunc` which evaluates several hooks at once
hooksToFunc :: Monoid h => [Hook h] -> Broadcast -> BoardState -> h
hooksToFunc = mconcat . fmap hookToFunc
-- | A modifier which conditionally affects thet value of some stat for a unit -- | A modifier which conditionally affects thet value of some stat for a unit
data Modifier = forall a. Monoid a => Modifier data Modifier = forall a. Monoid a => Modifier
{ modifierStat :: Stat a -- ^ Which stat this modifier effects { modifierStat :: Stat a -- ^ Which stat this modifier effects
@ -584,13 +653,47 @@ data Trait = Trait
, traitModifiers :: [Modifier] -- ^ Any modifiers this trait imposes , traitModifiers :: [Modifier] -- ^ Any modifiers this trait imposes
} }
-- | Some inflicted/bestowed trait that applies to a character
--
-- Some status effects track internal state (here, the type variable a), which affect
-- either what the effect does or how long it exists.
--
-- A couple examples of status effects:
-- - Gargamox's Leech's "superate", which adds a Deathburst to a unit, and lasts for the
-- whole game
-- - C.A.R.C.A.S.'s Ammo Goblin's "bone shards" which causes the unit to take damage when
-- pushed or pulled, and expires after dealing three damage or the end of the goblin's
-- next turn.
data StatusEffect = forall a. StatusEffect
{ seName :: String -- ^ The name of the effect
, seHooks :: [Hook (a -> CharacterIdentifier -> [Effect])] -- ^ Triggers which produce effects
, seModifiers :: [Modifier] -- ^ Any stat modifiers inflicted by this effect
-- | Triggers which update the internal state or remove the effect
--
-- Returning `Just` updates the internal state, and returning `Nothing` removes the
-- effect completely.
, seUpdate :: [Hook (a -> CharacterIdentifier -> Maybe a)]
-- | Whatever internal state is needed by this effect
, seState :: a
-- | Render the internal state
--
-- Suffixed with the name when `show`ing this status effect
, seShowState :: a -> String
}
instance Show StatusEffect where
show (StatusEffect {..}) = '+' : seName ++ ' ' : seShowState seState
instance Show BaseStats where instance Show BaseStats where
show stats = "===== " ++ name stats ++ " =====\nMV: " ++ show (mov stats) ++ " DF: " ++ show (df stats) ++ "+ ARM: " ++ show (arm stats) show stats = "===== " ++ name stats ++ " =====\nMV: " ++ show (mov stats) ++ " DF: " ++ show (df stats) ++ "+ ARM: " ++ show (arm stats)
newtype TokenCount = TokenCount (forall n. Token n -> n) newtype TokenCount = TokenCount (forall n. Token n -> n)
instance Show TokenCount where instance Show TokenCount where
show tc = mconcat $ intersperse "\n" $ filter (/=[]) show tc = mconcat . intersperse "\n" $ filter (not . null)
[ showHealth [ showHealth
, showReload , showReload
, " " , " "
@ -652,10 +755,16 @@ data Character = Character
{ _baseStats :: BaseStats { _baseStats :: BaseStats
, _movedThisRound :: Bool , _movedThisRound :: Bool
, _tokenCount :: TokenCount , _tokenCount :: TokenCount
, _statusEffects :: [StatusEffect]
} }
instance Show Character where instance Show Character where
show c = show (_baseStats c) ++ (if _movedThisRound c then "\n(already moved this round)" else "\n") ++ '\n' : show (_tokenCount c) show c =
show (_baseStats c)
++ (if _movedThisRound c then "\n(already moved this round)" else "\n")
++ '\n' : show (_tokenCount c)
++ '\n' : mconcat (intersperse "\n" (show <$> _statusEffects c))
++ "\n"
noTokens :: TokenCount noTokens :: TokenCount
noTokens = TokenCount noTokens' noTokens = TokenCount noTokens'
@ -699,8 +808,30 @@ makeLensesWith secondClassLensNames ''Hook
makeLensesWith secondClassLensNames ''Trait makeLensesWith secondClassLensNames ''Trait
instance Functor Hook where
fmap f (Hook {..}) = Hook {hookEffect = hookEffect <&> (<&> f), ..}
v <$ Hook {..} = Hook {hookEffect = const $ const v, ..}
-- | Extract the hooks from a status effect, hiding internal state
seGetHooks :: StatusEffect -> [Hook (CharacterIdentifier -> [Effect])]
seGetHooks (StatusEffect {..}) = seHooks <&> (<&> ($ seState))
seDoUpdate :: Broadcast -> BoardState -> CharacterIdentifier -> StatusEffect -> Maybe StatusEffect
seDoUpdate (Broadcast trigger assocData) board cid (StatusEffect {..}) = case relevantUpdates of
[] -> Just $ StatusEffect {..}
Nothing : _ -> Nothing
Just newState : _ -> Just $ StatusEffect {seState = newState, ..}
where
seUpdate' = seUpdate & each . mapped %~ (($ cid) . ($ seState))
getRelevantHookEffects :: Hook t -> Maybe (BoardState -> t)
getRelevantHookEffects (Hook {..}) = case hookTrigger `triggerEq` trigger of
Just Refl -> Just $ hookEffect assocData
Nothing -> Nothing
relevantHooks = seUpdate' ^.. each . to getRelevantHookEffects . _Just
relevantUpdates = sequence relevantHooks board
instantiate :: BaseStats -> Character instantiate :: BaseStats -> Character
instantiate stats = Character stats False noTokens & tokenCount . ofToken Health .~ hp stats instantiate stats = Character stats False noTokens [] & tokenCount . ofToken Health .~ hp stats
clearUpToNTokens :: (Num a, Ord a) => Token a -> a -> Character -> Character clearUpToNTokens :: (Num a, Ord a) => Token a -> a -> Character -> Character
clearUpToNTokens t n = tokenCount . ofToken t %~ clearUpToN n clearUpToNTokens t n = tokenCount . ofToken t %~ clearUpToN n
@ -740,10 +871,11 @@ untap = movedThisRound .~ False
-- --
-- This includes so called global modifiers, but in order to keep game rules seperate from -- This includes so called global modifiers, but in order to keep game rules seperate from
-- the model, global modifiers must be provided as the first argument. -- the model, global modifiers must be provided as the first argument.
computeStat_ :: [Modifier] -> BoardState -> CharacterIdentifier -> Stat a -> a computeStat_ :: (Coercible cid c, c ~ CharacterIdentifier) => [Modifier] -> BoardState -> cid -> Stat a -> a
computeStat_ globalModifiers board cid stat = case allStatsAreMonoids stat of computeStat_ globalModifiers board cid' stat = case allStatsAreMonoids stat of
HasMonoidInstance Refl -> HasMonoidInstance Refl ->
let let
cid = coerce cid'
queryStatsL = each . to (queryModifier board cid stat) queryStatsL = each . to (queryModifier board cid stat)
globalBonus = globalModifiers ^. queryStatsL globalBonus = globalModifiers ^. queryStatsL
traitBonus = board ^. ixCharacter cid . baseStats . traitsL . each . traitModifiersL . queryStatsL traitBonus = board ^. ixCharacter cid . baseStats . traitsL . each . traitModifiersL . queryStatsL
@ -751,24 +883,28 @@ computeStat_ globalModifiers board cid stat = case allStatsAreMonoids stat of
-- | Activate all of the relevant hooks on the board -- | Activate all of the relevant hooks on the board
-- --
-- Searches through all hooks in traits and global hooks, accumulates all their effects, -- Searches through all hooks in traits, statuse effects and global hooks, accumulates all
-- then adds all the resulting effects to the effect stack. Similar to `computeStat_`, -- their effects, then adds all the resulting effects to the effect stack. Then, all
-- this is additionall searches so called global hooks, but only recognizes global hooks -- necessary status effect updates are applied. Similar to `computeStat_`, this is
-- passed as an argument. -- additionall searches so called global hooks, but only recognizes global hooks passed as
runHooks_ :: [Hook [Effect]] -> Trigger -> BoardState -> BoardState -- an argument.
runHooks_ globalHooks trigger board = board <++ characterEffects <++ globalEffects runHooks_ :: [Hook [Effect]] -> Broadcast -> BoardState -> BoardState
runHooks_ globalHooks trigger board = boardWithUpdates <++ allEffects
where where
filterActivatedL :: Traversal' (Hook a) (Hook a) unitAllEffectHooks :: (CharacterIdentifier, Character) -> [Hook [Effect]]
filterActivatedL = filtered ((==trigger) . hookTrigger) unitAllEffectHooks (cid, c) = allHooks <&> (?? cid)
unitHooksL :: SimpleFold Character (BoardState -> CharacterIdentifier -> [Effect]) where
unitHooksL = baseStats . traitsL . each . traitHooksL . each . filterActivatedL . hookEffectL statusHooks :: [Hook (CharacterIdentifier -> [Effect])]
charRunHooks :: (CharacterIdentifier, Character) -> [Effect] statusHooks = c ^.. statusEffects . each . to seGetHooks . each
charRunHooks (cid, c) = classHooks :: [Hook (CharacterIdentifier -> [Effect])]
(c ^. unitHooksL) board cid classHooks = c ^.. baseStats . traitsL . each . traitHooksL . each
characterEffects :: [Effect] allHooks = statusHooks ++ classHooks
characterEffects = board ^. enumerateUnits . to charRunHooks unitUpdateStatuses :: (CharacterIdentifier, Character) -> (CharacterIdentifier, Character)
globalEffects :: [Effect] unitUpdateStatuses i@(cid, _) = i & _2 . statusEffects %~ mapMaybe (seDoUpdate trigger board cid)
globalEffects = (globalHooks ^. each . filterActivatedL . hookEffectL) board unitHooks = board ^.. enumerateUnits . to unitAllEffectHooks . each
allHooks = unitHooks ++ globalHooks
allEffects = hooksToFunc allHooks trigger board
boardWithUpdates = board & enumerateUnits %~ unitUpdateStatuses
instance Show BoardState where instance Show BoardState where
show board = join (intersperse "\n" showTiles) ++ '\n':showRound ++ "\n" ++ showCharacters show board = join (intersperse "\n" showTiles) ++ '\n':showRound ++ "\n" ++ showCharacters
@ -914,11 +1050,13 @@ offsetB b = usingBoardDimensions b offset
eachCID :: Monoid m => Getting m BoardState CharacterIdentifier eachCID :: Monoid m => Getting m BoardState CharacterIdentifier
eachCID = tiles . each . _1 . _Just eachCID = tiles . each . _1 . _Just
ixCharacter :: CharacterIdentifier -> Traversal' BoardState Character ixCharacter :: (Coercible cid c, c ~ CharacterIdentifier) => cid -> Traversal' BoardState Character
ixCharacter (player, indx) = characters . forPlayer player . ix indx ixCharacter cid = case coerce cid of (player, indx) -> characters . forPlayer player . ix indx
renderCharacterHandle :: BoardState -> CharacterIdentifier -> String renderCharacterHandle :: (Coercible a b, b ~ CharacterIdentifier) => BoardState -> a -> String
renderCharacterHandle board cid = maybe "[💀]" (characterHandle cid) $ board ^? ixCharacter cid renderCharacterHandle board cid' = maybe "[💀]" (characterHandle cid) $ board ^? ixCharacter cid
where
cid = coerce cid'
eachCharacter :: Traversal' BoardState Character eachCharacter :: Traversal' BoardState Character
eachCharacter = characters . everybody . traverse eachCharacter = characters . everybody . traverse
@ -955,9 +1093,9 @@ enumerateUnits = enumerateUnits'
fUnit :: Player -> Int -> Character -> f Character fUnit :: Player -> Int -> Character -> f Character
fUnit player indx c = snd <$> f ((player, indx), c) fUnit player indx c = snd <$> f ((player, indx), c)
fRoster :: Player -> [Character] -> f [Character] fRoster :: Player -> [Character] -> f [Character]
fRoster player roster = zipWithM (fUnit player) [0..] roster
fCharacters :: PPair [Character] -> f (PPair [Character]) fCharacters :: PPair [Character] -> f (PPair [Character])
fCharacters (PPair p1 p2) = PPair <$> fRoster Min p1 <*> fRoster Max p2 fCharacters (PPair p1 p2) = PPair <$> fRoster Min p1 <*> fRoster Max p2
fRoster player = zipWithM (fUnit player) [0..]
untappedUnits :: SimpleFold BoardState CharacterIdentifier untappedUnits :: SimpleFold BoardState CharacterIdentifier
untappedUnits = enumerateUnits . filtered (untapped . snd) . _1 untappedUnits = enumerateUnits . filtered (untapped . snd) . _1
@ -968,14 +1106,14 @@ isAlive board cid = has (eachCID . filtered (== cid)) board
pushEffects :: [Effect] -> BoardState -> BoardState pushEffects :: [Effect] -> BoardState -> BoardState
pushEffects newEffects = effectStack %~ (newEffects ++) pushEffects newEffects = effectStack %~ (newEffects ++)
unitPosition :: BoardState -> CharacterIdentifier -> Maybe Point unitPosition :: (Coercible cid c, c ~ CharacterIdentifier) => BoardState -> cid -> Maybe Point
unitPosition (BoardState {_movingUnit=Just (movingCid, movingPoint)}) cid unitPosition (BoardState {_movingUnit=Just (movingCid, movingPoint)}) cid
| movingCid == cid = Just movingPoint | movingCid == coerce cid = Just movingPoint
unitPosition (BoardState {_tiles}) cid = headMay $ catMaybes $ zipWith aux [0..] _tiles unitPosition (BoardState {_tiles}) cid = headMay . catMaybes $ zipWith aux [0..] _tiles
where where
aux :: Natural -> (Maybe CharacterIdentifier, [EnvTile]) -> Maybe Point aux :: Natural -> (Maybe CharacterIdentifier, [EnvTile]) -> Maybe Point
aux p (potentialCid, _) aux p (potentialCid, _)
| potentialCid == Just cid = Just $ Point p | potentialCid == Just (coerce cid) = Just $ Point p
| otherwise = Nothing | otherwise = Nothing
atPoint :: Point -> Traversal' BoardState (Maybe CharacterIdentifier, [EnvTile]) atPoint :: Point -> Traversal' BoardState (Maybe CharacterIdentifier, [EnvTile])
@ -990,7 +1128,7 @@ characterAt p f board = inner f board
inner :: Traversal' BoardState (Maybe CharacterIdentifier) inner :: Traversal' BoardState (Maybe CharacterIdentifier)
inner = atPoint p . _1 . filtered (/= board ^? movingUnit . _Just . _1) inner = atPoint p . _1 . filtered (/= board ^? movingUnit . _Just . _1)
adjacentUnits :: BoardState -> CharacterIdentifier -> Maybe [CharacterIdentifier] adjacentUnits :: (Coercible cid c, c ~ CharacterIdentifier) => BoardState -> cid -> Maybe [CharacterIdentifier]
adjacentUnits board cid = do adjacentUnits board cid = do
originalLocation <- unitPosition board cid originalLocation <- unitPosition board cid
let adjacentTiles = usingBoardDimensions board adjacentPoints originalLocation let adjacentTiles = usingBoardDimensions board adjacentPoints originalLocation
@ -998,14 +1136,14 @@ adjacentUnits board cid = do
let unitsAdjacent = mapMaybe (flip preview board . characterAt') adjacentTiles let unitsAdjacent = mapMaybe (flip preview board . characterAt') adjacentTiles
return unitsAdjacent return unitsAdjacent
adjacentAllies :: BoardState -> CharacterIdentifier -> Maybe [CharacterIdentifier] adjacentAllies :: (Coercible cid c, c ~ CharacterIdentifier) => BoardState -> cid -> Maybe [CharacterIdentifier]
adjacentAllies board cid = filter (owner >>> (owner cid ==)) <$> adjacentUnits board cid adjacentAllies board cid = filter (owner >>> (owner cid ==)) <$> adjacentUnits board cid
isElevated :: BoardState -> CharacterIdentifier -> Bool isElevated :: (Coercible cid c, c ~ CharacterIdentifier) => BoardState -> cid -> Bool
isElevated board cid = maybe False (\p -> elem Elevation $ board ^. terrainAt p) $ unitPosition board cid isElevated board cid = any (\p -> elem Elevation $ board ^. terrainAt p) $ unitPosition board cid
removeUnit :: CharacterIdentifier -> BoardState -> BoardState removeUnit :: (Coercible cid c, c ~ CharacterIdentifier) => cid -> BoardState -> BoardState
removeUnit cid = tiles . each . _1 %~ mfilter (/= cid) removeUnit cid = tiles . each . _1 %~ mfilter (/= coerce cid)
setUnit :: CharacterIdentifier -> Point -> BoardState -> BoardState setUnit :: CharacterIdentifier -> Point -> BoardState -> BoardState
setUnit character point = atPoint point . _1 ?~ character setUnit character point = atPoint point . _1 ?~ character

View File

@ -5,16 +5,15 @@ module Units.Carcass
import GameModel import GameModel
( adjacentAllies ( adjacentAllies
, ActingUnit
, Armor(..) , Armor(..)
, BaseStats(..) , BaseStats(..)
, BoardState , BoardState
, CharacterIdentifier , CharacterIdentifier
, Choice , Choice
, DamageType(..) , DamageType(..)
, Effect(..)
, Stat(..) , Stat(..)
, Token(..) , Token(..)
, Trigger(..)
, Trait(..) , Trait(..)
, Modifier(..) , Modifier(..)
) )
@ -23,6 +22,7 @@ import Units.Components
, anyTarget , anyTarget
, buildAttack, inflictTokens, push , buildAttack, inflictTokens, push
) )
import Data.Monoid (Sum)
gunwight :: BaseStats gunwight :: BaseStats
gunwight = BaseStats gunwight = BaseStats
@ -35,7 +35,7 @@ gunwight = BaseStats
, traits = [formation] , traits = [formation]
} }
gunwightActions :: [CharacterIdentifier -> Choice] gunwightActions :: [ActingUnit -> Choice]
gunwightActions = gunwightActions =
[ buildAttack $ AttackT [ buildAttack $ AttackT
{ tName = "OL45" { tName = "OL45"
@ -66,6 +66,7 @@ formation = Trait
, traitModifiers = [formationModifier] , traitModifiers = [formationModifier]
} }
where where
formationF :: BoardState -> CharacterIdentifier -> Sum Int
formationF board cid = if adjacentAllies board cid /= Just [] then 1 else 0 formationF board cid = if adjacentAllies board cid /= Just [] then 1 else 0
formationModifier = Modifier formationModifier = Modifier
{ modifierStat = AttackDice { modifierStat = AttackDice

View File

@ -8,6 +8,7 @@ module Units.Components
, inflictTokens , inflictTokens
, push , push
, pull , pull
, inflictStatusEffect
) )
where where
@ -18,10 +19,13 @@ import GameModel
, Choice , Choice
, DamageType , DamageType
, Effect(..) , Effect(..)
, mkChoice, Token, forcedMove, ForcedMoveType (..), owner , ActingUnit(..), TargettedUnit(..)
, actingUnit
, mkChoice, Token, forcedMove, ForcedMoveType (..), owner, StatusEffect, DieFace
) )
import Numeric.Natural (Natural) import Numeric.Natural (Natural)
import Lens.Micro
------------------------- -------------------------
-- Attacks & Abilities -- -- Attacks & Abilities --
@ -30,7 +34,7 @@ import Numeric.Natural (Natural)
data AttackT = AttackT data AttackT = AttackT
{ tName :: String { tName :: String
, tRange :: (Natural, Natural) , tRange :: (Natural, Natural)
, tValidTargets :: BoardState -> CharacterIdentifier -> Bool , tValidTargets :: BoardState -> TargettedUnit -> Bool
, tMelee :: Bool , tMelee :: Bool
, tDamageType :: DamageType , tDamageType :: DamageType
, tDamageAmount :: Natural , tDamageAmount :: Natural
@ -38,16 +42,16 @@ data AttackT = AttackT
, tStandardEffects :: [ProtoEffect] , tStandardEffects :: [ProtoEffect]
} }
anyTarget :: BoardState -> CharacterIdentifier -> Bool anyTarget :: BoardState -> TargettedUnit -> Bool
anyTarget = const $ const True anyTarget = const $ const True
buildAttack :: AttackT -> CharacterIdentifier -> Choice buildAttack :: AttackT -> ActingUnit -> Choice
buildAttack (AttackT {..}) attacker = mkChoice tName [targetEffect] buildAttack (AttackT {..}) attacker = mkChoice tName [targetEffect]
where where
attackDetails target = Attack attackDetails target = Attack
((sequence $ sequence tHeadshotEffects attacker) target) (mkEffect attacker target tHeadshotEffects)
tMelee tMelee
((sequence $ sequence tStandardEffects attacker) target) (mkEffect attacker target tStandardEffects)
tDamageType tDamageType
tDamageAmount tDamageAmount
attackEffect target = [ResolveAttack attacker (attackDetails target) target] attackEffect target = [ResolveAttack attacker (attackDetails target) target]
@ -58,20 +62,64 @@ data SelfAbilityT = SelfAbilityT
, tEffects :: [ProtoEffect] , tEffects :: [ProtoEffect]
} }
mkSelfAbility :: SelfAbilityT -> CharacterIdentifier -> Choice mkSelfAbility :: SelfAbilityT -> ActingUnit -> Choice
mkSelfAbility (SelfAbilityT {..}) cid = mkChoice tName (sequence (sequence tEffects cid) cid) mkSelfAbility (SelfAbilityT {..}) cid = mkChoice tName (mkEffect cid (TargettedUnit $ cid ^. actingUnit) tEffects)
----------------------------- -----------------------------
--------- Effects ----------- --------- Effects -----------
----------------------------- -----------------------------
type ProtoEffect = CharacterIdentifier -> CharacterIdentifier -> Effect -- | Indicates a possible argument to a protoeffect
--
-- A valid argument to a protoeffect is any argument with which provide by placing the
-- protoeffect in an `Effect`. For example, a `DieFace` is a valid question because there
-- exists an `Effect` which accepts a type @`DieFace` -> [`Effect`]@.
--
-- In additon, the actor and target (respectively) are available in the context, to enable
-- asking questions about those things
class Question t where
askQuestion :: ActingUnit -> TargettedUnit -> (t -> [Effect]) -> [Effect]
instance Question ActingUnit where
askQuestion actor _ f = f actor
instance Question TargettedUnit where
askQuestion _ target f = f target
-- | A function which can be converted into a sequence of effects
--
-- Used in `buildProtoEffect` to create a `ProtoEffect`
class ProtoEffectF t where
mkEffect :: ActingUnit -> TargettedUnit -> t -> [Effect]
instance ProtoEffectF f => ProtoEffectF [f] where
mkEffect a t = (mkEffect a t =<<)
instance ProtoEffectF Effect where
mkEffect _ _ = pure
instance ProtoEffectF ProtoEffect where
mkEffect attacker target pe = mkEffect' attacker target pe
instance (ProtoEffectF a, Question q) => ProtoEffectF (q -> a) where
mkEffect attacker target f = askQuestion attacker target (mkEffect attacker target <$> f)
data ProtoEffect = forall f. ProtoEffectF f => MkProtoEffect f
mkEffect' :: ActingUnit -> TargettedUnit -> ProtoEffect -> [Effect]
mkEffect' attacker target (MkProtoEffect pe) = mkEffect attacker target pe
mkEffects' :: ActingUnit -> TargettedUnit -> [ProtoEffect] -> [Effect]
mkEffects' a t = (mkEffect a t =<<)
inflictStatusEffect :: StatusEffect -> ProtoEffect
inflictStatusEffect se = MkProtoEffect $ InflictStatusEffect se
inflictTokens :: Num n => Token n -> n -> ProtoEffect inflictTokens :: Num n => Token n -> n -> ProtoEffect
inflictTokens tokenType tokenCount _ = InflictTokens tokenType tokenCount inflictTokens tokenType tokenCount = MkProtoEffect $ InflictTokens tokenType tokenCount
genericShift :: ForcedMoveType -> Natural -> ProtoEffect genericShift :: ForcedMoveType -> Natural -> ProtoEffect
genericShift fmType amount puller = forcedMove fmType amount (owner puller) (Left puller) genericShift fmType amount = MkProtoEffect $ (\(ActingUnit puller) -> forcedMove fmType amount (owner puller) (Left puller))
push :: Natural -> ProtoEffect push :: Natural -> ProtoEffect
push = genericShift Push push = genericShift Push

View File

@ -4,12 +4,16 @@ module Units.Debug
where where
import GameModel import GameModel
( Armor(..) ( Armor(..), ActingUnit
, BaseStats(..) , BaseStats(..)
, CharacterIdentifier , CharacterIdentifier
, Choice , Choice
, DamageType(..) , DamageType(..)
, Token(..) , Token(..), StatusEffect (..)
, Hook(..), Effect(..)
, Trigger(..), BoardState
, TargettedUnit(..)
, targettedUnit
) )
import Units.Components import Units.Components
@ -18,8 +22,11 @@ import Units.Components
, buildAttack , buildAttack
, SelfAbilityT(..) , SelfAbilityT(..)
, mkSelfAbility, inflictTokens, pull, push , mkSelfAbility, inflictTokens, pull, push
, inflictStatusEffect
) )
import Lens.Micro
basic :: BaseStats basic :: BaseStats
basic = BaseStats basic = BaseStats
{ name = "Basic Debug Unit" { name = "Basic Debug Unit"
@ -31,7 +38,7 @@ basic = BaseStats
, traits = [] , traits = []
} }
basicActions :: [CharacterIdentifier -> Choice] basicActions :: [ActingUnit -> Choice]
basicActions = basicActions =
[ buildAttack $ AttackT [ buildAttack $ AttackT
{ tName = "Peashooter" { tName = "Peashooter"
@ -102,7 +109,36 @@ basicActions =
, tEffects = [inflictTokens SpeedSlow 1] , tEffects = [inflictTokens SpeedSlow 1]
} }
, mkSelfAbility $ SelfAbilityT , mkSelfAbility $ SelfAbilityT
{ tName = "Get String" { tName = "Blood Shield"
, tEffects = [inflictTokens StrWeak 1] , tEffects = [inflictStatusEffect bloodShield]
} }
] ]
bloodShield :: StatusEffect
bloodShield = StatusEffect
{ seName = "Blood Shield"
, seHooks =
[ Hook
{ hookTrigger = TookDamage
, hookEffect = damageHookEffect
}
]
, seModifiers = []
, seUpdate =
[ Hook
{ hookTrigger = TookDamage
, hookEffect = damageHookDecrement
}
]
, seState = 1
, seShowState = \n -> '(' : replicate n '*' ++ ")"
}
where
damageHookEffect :: TargettedUnit -> BoardState -> Int -> CharacterIdentifier -> [Effect]
damageHookEffect (TargettedUnit injuredUnit) _ _ us =
[InflictTokens VitalVulnr 1 (TargettedUnit us) | injuredUnit == us ]
damageHookDecrement :: TargettedUnit -> BoardState -> Int -> CharacterIdentifier -> Maybe Int
damageHookDecrement damagedUnit _ n us
| damagedUnit ^. targettedUnit == us && n > 1 = Just $ pred n
| damagedUnit ^. targettedUnit == us = Nothing
| otherwise = Just n

View File

@ -8,7 +8,6 @@ module Util
, note , note
) where ) where
import Data.Bool (bool)
import Lens.Micro.TH (LensRules, lensRules, lensField, DefName (TopName)) import Lens.Micro.TH (LensRules, lensRules, lensField, DefName (TopName))
import Language.Haskell.TH (mkName, nameBase) import Language.Haskell.TH (mkName, nameBase)
import Lens.Micro import Lens.Micro
@ -24,7 +23,7 @@ infixl 4 ??
f ?? a = ($ a) <$> f f ?? a = ($ a) <$> f
secondClassLensNames :: LensRules secondClassLensNames :: LensRules
secondClassLensNames = lensRules & lensField .~ (\_ _ n -> [TopName $ mkName $ nameBase n ++ "L"]) secondClassLensNames = lensRules & lensField .~ (\_ _ n -> [TopName . mkName $ nameBase n ++ "L"])
data Never data Never