packages feed

LambdaHack 0.11.0.0 → 0.11.0.1

raw patch · 40 files changed

+498/−411 lines, 40 filesdep ~hsiniPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: hsini

API changes (from Hackage documentation)

- Game.LambdaHack.Client.Bfs: instance Data.Bits.Bits Game.LambdaHack.Client.Bfs.BfsDistance
- Game.LambdaHack.Client.UI.Slideshow: unsnoc :: Slideshow -> Maybe (Slideshow, OKX)
- Game.LambdaHack.Common.PointArray: type family UnboxRep c;
+ Game.LambdaHack.Client.Bfs: instance GHC.Bits.Bits Game.LambdaHack.Client.Bfs.BfsDistance
+ Game.LambdaHack.Client.UI.MsgM: tutorialHintMsgAdd :: MonadClientUI m => TutorialHints -> m ()
+ Game.LambdaHack.Client.UI.Overlay: attrStringToString :: AttrString -> String
+ Game.LambdaHack.Client.UI.Slideshow: unsnocSlideshow :: Slideshow -> Maybe (Slideshow, OKX)
+ Game.LambdaHack.Client.UI.TutorialHints: AlotOfDamageFromOneSource :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: AvoidWalkingEnemies :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: CannotHarmYouInMelee :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: CaughtProjectile :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: DamageOfDifferentKind :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: HearingRadius :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: HitsWithNoDirectDamage :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: MeleeEnemies :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: NewFloorNewOpportunity :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: OutOfSightEvents :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: SwitchPointmanAndAvoidMeleeAlone :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: SwitchPointmanAndSoftenFoes :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: SwitchTeammate :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: TemporaryConditions :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: TerrainNotFullyKnown :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: UseTerrainEffect :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: WokenUpActors :: TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: data TutorialHints
+ Game.LambdaHack.Client.UI.TutorialHints: renderTutorialHints :: TutorialHints -> Text
+ Game.LambdaHack.Core.Prelude: (!?) :: [a] -> Int -> Maybe a
+ Game.LambdaHack.Core.Prelude: class a ~# b => (a :: k) ~ (b :: k)
+ Game.LambdaHack.Core.Prelude: liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
+ Game.LambdaHack.Core.Prelude: unsnoc :: [a] -> Maybe ([a], a)
- Game.LambdaHack.Core.Prelude: (!!) :: [a] -> Int -> a
+ Game.LambdaHack.Core.Prelude: (!!) :: HasCallStack => [a] -> Int -> a
- Game.LambdaHack.Core.Prelude: allB :: Show a => (a -> Bool) -> [a] -> Bool
+ Game.LambdaHack.Core.Prelude: allB :: Show v => (v -> Bool) -> [v] -> Bool
- Game.LambdaHack.Core.Prelude: blame :: Show a => Bool -> a -> Bool
+ Game.LambdaHack.Core.Prelude: blame :: Show v => Bool -> v -> Bool
- Game.LambdaHack.Core.Prelude: class Bounded a
+ Game.LambdaHack.Core.Prelude: class () => Bounded a
- Game.LambdaHack.Core.Prelude: class Enum a
+ Game.LambdaHack.Core.Prelude: class () => Enum a
- Game.LambdaHack.Core.Prelude: class Eq a
+ Game.LambdaHack.Core.Prelude: class () => Eq a
- Game.LambdaHack.Core.Prelude: class Foldable (t :: Type -> Type)
+ Game.LambdaHack.Core.Prelude: class () => Foldable (t :: Type -> Type)
- Game.LambdaHack.Core.Prelude: class Functor (f :: Type -> Type)
+ Game.LambdaHack.Core.Prelude: class () => Functor (f :: Type -> Type)
- Game.LambdaHack.Core.Prelude: class Num a
+ Game.LambdaHack.Core.Prelude: class () => Num a
- Game.LambdaHack.Core.Prelude: class Read a
+ Game.LambdaHack.Core.Prelude: class () => Read a
- Game.LambdaHack.Core.Prelude: class Semigroup a
+ Game.LambdaHack.Core.Prelude: class () => Semigroup a
- Game.LambdaHack.Core.Prelude: class Show a
+ Game.LambdaHack.Core.Prelude: class () => Show a
- Game.LambdaHack.Core.Prelude: cycle :: [a] -> [a]
+ Game.LambdaHack.Core.Prelude: cycle :: HasCallStack => [a] -> [a]
- Game.LambdaHack.Core.Prelude: data Bool
+ Game.LambdaHack.Core.Prelude: data () => Bool
- Game.LambdaHack.Core.Prelude: data Char
+ Game.LambdaHack.Core.Prelude: data () => Char
- Game.LambdaHack.Core.Prelude: data Double
+ Game.LambdaHack.Core.Prelude: data () => Double
- Game.LambdaHack.Core.Prelude: data Either a b
+ Game.LambdaHack.Core.Prelude: data () => Either a b
- Game.LambdaHack.Core.Prelude: data Float
+ Game.LambdaHack.Core.Prelude: data () => Float
- Game.LambdaHack.Core.Prelude: data IO a
+ Game.LambdaHack.Core.Prelude: data () => IO a
- Game.LambdaHack.Core.Prelude: data Int
+ Game.LambdaHack.Core.Prelude: data () => Int
- Game.LambdaHack.Core.Prelude: data Integer
+ Game.LambdaHack.Core.Prelude: data () => Integer
- Game.LambdaHack.Core.Prelude: data Maybe a
+ Game.LambdaHack.Core.Prelude: data () => Maybe a
- Game.LambdaHack.Core.Prelude: data Ordering
+ Game.LambdaHack.Core.Prelude: data () => Ordering
- Game.LambdaHack.Core.Prelude: data Text
+ Game.LambdaHack.Core.Prelude: data () => Text
- Game.LambdaHack.Core.Prelude: data Word
+ Game.LambdaHack.Core.Prelude: data () => Word
- Game.LambdaHack.Core.Prelude: foldl1' :: (a -> a -> a) -> [a] -> a
+ Game.LambdaHack.Core.Prelude: foldl1' :: HasCallStack => (a -> a -> a) -> [a] -> a
- Game.LambdaHack.Core.Prelude: head :: [a] -> a
+ Game.LambdaHack.Core.Prelude: head :: HasCallStack => [a] -> a
- Game.LambdaHack.Core.Prelude: infix 2 `showFailure`
+ Game.LambdaHack.Core.Prelude: infix 2 `swith`
- Game.LambdaHack.Core.Prelude: infixl 1 >>
+ Game.LambdaHack.Core.Prelude: infixl 1 >>=
- Game.LambdaHack.Core.Prelude: infixr 1 =<<
+ Game.LambdaHack.Core.Prelude: infixr 1 <=<
- Game.LambdaHack.Core.Prelude: infixr 8 ^
+ Game.LambdaHack.Core.Prelude: infixr 8 ^^
- Game.LambdaHack.Core.Prelude: init :: [a] -> [a]
+ Game.LambdaHack.Core.Prelude: init :: HasCallStack => [a] -> [a]
- Game.LambdaHack.Core.Prelude: last :: [a] -> a
+ Game.LambdaHack.Core.Prelude: last :: HasCallStack => [a] -> a
- Game.LambdaHack.Core.Prelude: mapAccumL :: Traversable t => (a -> b -> (a, c)) -> a -> t b -> (a, t c)
+ Game.LambdaHack.Core.Prelude: mapAccumL :: Traversable t => (s -> a -> (s, b)) -> s -> t a -> (s, t b)
- Game.LambdaHack.Core.Prelude: mapAccumR :: Traversable t => (a -> b -> (a, c)) -> a -> t b -> (a, t c)
+ Game.LambdaHack.Core.Prelude: mapAccumR :: Traversable t => (s -> a -> (s, b)) -> s -> t a -> (s, t b)
- Game.LambdaHack.Core.Prelude: seq :: forall (r :: RuntimeRep) a (b :: TYPE r). a -> b -> b
+ Game.LambdaHack.Core.Prelude: seq :: forall {r :: RuntimeRep} a (b :: TYPE r). a -> b -> b
- Game.LambdaHack.Core.Prelude: tail :: [a] -> [a]
+ Game.LambdaHack.Core.Prelude: tail :: HasCallStack => [a] -> [a]

Files

CHANGELOG.md view
@@ -1,3 +1,23 @@+## [v0.11.0.1](https://github.com/LambdaHack/LambdaHack/compare/v0.11.0.0...v0.11.0.1)++- Split the long Content.ModeKind texts into lines, [#283](https://github.com/LambdaHack/LambdaHack/issues/283)+- In unit tests, re-use values from UnitTestHelpers and Game.LambdaHack.Common.Kind, [#286](https://github.com/LambdaHack/LambdaHack/issues/286)+- Rename unsnoc (see #300)+- Bump the bound on hsini to handle trailing whitespace properly+- Hack around a crash when TABbing during item manipulation+- Document better that macros start with Cmd_+- Correct a typo in test/Spec.hs+- Add the communication overhead explanation proposed by fYnch+- Compress detail levels and don't show paths at DetailHigh+- Balance speedrunning and explain it better in player docs+- Change failed alter message to less confusing; thanks go to Pug+- Tweak tutorial hint enums and tests+- Make lore menu with all tutorial hints+- Don't punish naughty window managers with spammy warning+- Try harder to prevent resizing in SDL+- Change install from Hackage instructions so that they work outside package dir+- Interrupt sleep and running a bit less+ ## [v0.11.0.0](https://github.com/LambdaHack/LambdaHack/compare/v0.10.3.0...v0.11.0.0)  - Partially work around regression in libsdl2 2.0.16 (https://github.com/LambdaHack/LambdaHack/issues/281); to also avoid deformed boxes around tiles on the game map, please switch to a different SDL2 version
COPYLEFT view
@@ -5,19 +5,19 @@  Files: * Copyright: 2008-2011 Andres Loeh-           2010-2021 Mikolaj Konarski and others (see git history)+           2010-2023 Mikolaj Konarski and others (see git history) License: BSD-3-Clause  Files: GameDefinition/fonts/*.fnt GameDefinition/fonts/*.bdf GameDefinition/fonts/16x16xw.woff Copyright: 1997-2016 Leon Marrick            1997-2016 Sheldon Simms III            1997-2016 Nick McConnell-           2016-2021 Mikolaj Konarski+           2016-2023 Mikolaj Konarski License: GPL-2.0-or-later  Files: GameDefinition/fonts/Binary*.woff Copyright 2010-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'-Copyright 2021 Mikolaj Konarski+Copyright 2023 Mikolaj Konarski License: OFL-1.1  Files: GameDefinition/fonts/DejaVu*.woff
CREDITS view
@@ -13,13 +13,14 @@ bulbousBullfrog Alex Byaly Jamie Fristrom+Kayvan Kazeminejad    Binary distributions of this package may be linked or bundled with libraries such as SDL2, SDL_ttf, FreeType and many others. These libraries are copyright of their respective owners, with all rights reserved.-In particular, portions of this software are copyright © 2021 The FreeType+In particular, portions of this software are copyright © 2023 The FreeType Project (www.freetype.org). All rights reserved.  
GameDefinition/Content/ModeKind.hs view
@@ -125,7 +125,11 @@   , mdesc   = "Your research team disagrees over a drink with some gentlemen scientists about premises of a relative completeness theorem and there's only one way to settle that."       -- Not enough space with square fonts and also this is more of a hint than a flavour: Remember to keep your party together when opponents are spotted, or they might be tempted to silence solitary disputants one by one and so win the altercation.   , mreason = "In addition to advancing game plot, this encounter trains melee, squad formation and stealth. The battle is symmetric in goals (incapacitate all enemies) and in squad capabilities (only the pointman moves, others either melee or wait)."-  , mhint   = "Run a short distance with Shift or LMB, switch the pointman with Tab, repeat. In open terrain, if you keep distance between teammates, this resembles the leap frog infantry tactics. For best effects, end each sprint behind a cover or concealment.\nObserve and mimic the enemies. If you can't see an enemy that apparently can see you, in reversed circumstances you would have the same advantage. Savour the relative fairness --- you won't find any in the main crawl adventure that follows.\nIf you get beaten repeatedly, try using all consumables you find. Ponder the hints from the defeat message, in particular the one about keeping your party together once the opponents are spotted. However, if you want to discover a winning tactics on your own, make sure to ignore any such tips until you succeed."+  , mhint   =  T.intercalate "\n"+    [ "Run a short distance with Shift or LMB, switch the pointman with Tab, repeat. In open terrain, if you keep distance between teammates, this resembles the leap frog infantry tactics. For best effects, end each sprint behind a cover or concealment."+    , "Observe and mimic the enemies. If you can't see an enemy that apparently can see you, in reversed circumstances you would have the same advantage. Savour the relative fairness --- you won't find any in the main crawl adventure that follows."+    , "If you get beaten repeatedly, try using all consumables you find. Ponder the hints from the defeat message, in particular the one about keeping your party together once the opponents are spotted. However, if you want to discover a winning tactics on your own, make sure to ignore any such tips until you succeed."+    ]   }  crawl = ModeKind@@ -135,7 +139,7 @@   , mattract = False   , mroster = rosterCrawl   , mcaves  = cavesCrawl-  , mendMsg = [ (Killed, "To think that followers of science and agents of enlightenment would earn death as their reward! Where did we err in our ways? Perhaps nature should not have been disturbed so brashly and the fell beasts woken up from their slumber so eagerly?\nPerhaps the gathered items should have been used for scientific experiments on the spot rather than hoarded as if of base covetousness? Or perhaps the challenge, chosen freely but without the foreknowledge of the grisly difficulty, was insurmountable and forlorn from the start, despite the enormous power of educated reason at out disposal?")+  , mendMsg = [ (Killed, killedMsg)               , (Escape, "It's better to live to tell the tale than to choke on more than one can swallow. There was no more exquisite cultural artifacts and glorious scientific wonders in these forbidding tunnels anyway. Or were there?") ]   , mrules  = T.intercalate "\n"       [ "* Many levels"@@ -144,9 +148,14 @@       , "* Find a way out and escape ASAP"       ]   , mdesc   = "Enjoy the peaceful seclusion of these cold austere tunnels, but don't let wanton curiosity, greed and the ever-creeping abstraction madness keep you down there for too long. If you find survivors (whole or perturbed or segmented) of the past scientific missions, exercise extreme caution and engage or ignore at your discretion."-  , mreason = "This is the main, longest and most replayable scenario of the game."+  , mreason = "This is the main, longest and most replayable scenario of the game. It's crucial that you gather the most interesting cultural artifacts such as gold, gems and elixirs. Equally importantly, you have to limit the permanent sanity deterioration of your scientific expedition members by minimizing the time they are exposed to the horrors of the underworld."   , mhint   = "If you keep dying, attempt the subsequent adventures as a breather (perhaps at lowered difficulty). They fill the gaps in the plot and teach particular skills that may come in handy and help you discover new tactics of your own or come up with a strategy for staving off the attrition. Also experimenting with the initial adventures may answer some questions you didn't have when you attempted them originally."   }+ where+   killedMsg = T.intercalate "\n"+     [ "To think that followers of science and agents of enlightenment would earn death as their reward! Where did we err in our ways? Perhaps nature should not have been disturbed so brashly and the fell beasts woken up from their slumber so eagerly?"+     , "Perhaps the gathered items should have been used for scientific experiments on the spot rather than hoarded as if of base covetousness? Or perhaps the challenge, chosen freely but without the foreknowledge of the grisly difficulty, was insurmountable and forlorn from the start, despite the enormous power of educated reason at out disposal?"+     ]  -- The trajectory tip is important because of tactics of scout looking from -- behind a bush and others hiding in mist. If no suitable bushes,@@ -163,7 +172,7 @@   , mattract = False   , mroster = rosterShootout   , mcaves  = cavesShootout-  , mendMsg = [ (Killed, "This is a disgrace. What have we missed in our theoretic models of this fight? Did we miss a human lookout placed in a covered but unobstructed spot that lets the rest of the squad snipe from concealment or from a safe distance?\nBarring that, would we end up in a better shape even if we all hid and only fired blindly? We'd listen to impact sounds and wait vigilantly for incoming enemy missiles in order to register their trajectories and derive hints of enemy location. Apparently, ranged combat requires a change of pace and better planning than our previous simple but efficient calculations accustomed us to.")+  , mendMsg = [ (Killed, killedMsg)               , (Conquer, "That was a good fight, with scientifically accurate application of missiles, cover and concealment. Not even skilled logicians can routinely deduce enemy position from the physical trajectory of their projectiles nor by firing without line of sight and interpreting auditory cues. However, while this steep hurdle is overcome, the dispute is not over yet.") ]   , mrules  = T.intercalate "\n"       [ "* One level only"@@ -175,6 +184,11 @@   , mreason = "This adventure teaches the ranged combat skill in the simplified setup of fully symmetric battle."   , mhint   = "Try to come up with the best squad formation for this tactical challenge. Don't despair if you run out of ammo, because if you aim truly, enemy has few hit points left at this point. In turn, when trying to avoid enemy projectiles, you can display the trajectory of any soaring entity by pointing it with the crosshair in aiming mode."   }+ where+   killedMsg = T.intercalate "\n"+     [ "This is a disgrace. What have we missed in our theoretic models of this fight? Did we miss a human lookout placed in a covered but unobstructed spot that lets the rest of the squad snipe from concealment or from a safe distance?"+     , "Barring that, would we end up in a better shape even if we all hid and only fired blindly? We'd listen to impact sounds and wait vigilantly for incoming enemy missiles in order to register their trajectories and derive hints of enemy location. Apparently, ranged combat requires a change of pace and better planning than our previous simple but efficient calculations accustomed us to."+     ]  hunt = ModeKind  -- melee vs ranged with reaction fire in daylight   { mname   = "perilous hunt (4)"@@ -204,7 +218,7 @@   , mattract = False   , mroster = rosterFlight   , mcaves  = cavesFlight-  , mendMsg = [ (Killed, "Somebody must have tipped the enemies of free inquiry off. However, us walking along a lit trail, yelling, could have been a contributing factor. Also, it's worth noting that the torches prepared for this assault are best used as thrown makeshift flares.\nOn the other hand, equipping a lit torch makes one visible in the dark, regrettably but not quite unexpectedly to a scientific mind. Lastly, the goal of this foray was to definitely disengage from the fruitless dispute, via a way out marked by a yellow '>' sign, and to gather treasure that would support our future research. Not to harass every nearby scientific truth denier, as much as they do deserve it.")+  , mendMsg = [ (Killed, killedMsg)               , (Conquer, "It was enough to reach the escape area marked by yellow '>' symbol. Spilling that much blood was risky. unnecessary and alerted the authorities. Having said that --- impressive indeed.") ]   , mrules  = T.intercalate "\n"       [ "* One level only"@@ -217,6 +231,11 @@   , mreason = "The focus of this installment is on stealthy exploration under the threat of numerically superior enemy."   , mhint   = ""   }+ where+   killedMsg = T.intercalate "\n"+     [ "Somebody must have tipped the enemies of free inquiry off. However, us walking along a lit trail, yelling, could have been a contributing factor. Also, it's worth noting that the torches prepared for this assault are best used as thrown makeshift flares."+     , "On the other hand, equipping a lit torch makes one visible in the dark, regrettably but not quite unexpectedly to a scientific mind. Lastly, the goal of this foray was to definitely disengage from the fruitless dispute, via a way out marked by a yellow '>' sign, and to gather treasure that would support our future research. Not to harass every nearby scientific truth denier, as much as they do deserve it."+     ]  zoo = ModeKind  -- asymmetric crowd melee at night   { mname   = "burning zoo (6)"@@ -253,7 +272,7 @@   , mattract = False   , mroster = rosterAmbush   , mcaves  = cavesAmbush-  , mendMsg = [ (Killed, "You turned out to be the prey, this time, not the hunter. In fact, you are not even in the hunters' league. When fighting against such odds, passively waiting for enemy to spring a trap is to no avail, because a professional team can sneak in darkness and ambush the ambushers.\nGranted, good positioning is crucial, so that each squad member can overwatch the battlefield and fire opportunistically, using the recently recovered instant telegraphy equipment. However, there is no hope without active scouting, throwing lit objects and probing suspect areas with missiles while paying attention to sounds. And that may still not be enough.")+  , mendMsg = [ (Killed, killedMsg)               , (Conquer, "The new instant telegraphy equipment enabling simultaneous ranged attacks with indirect triangulation and aiming proved effective beyond expectation. Your ideas are safe, your research programme on track, your chartered company ready to launch and introduce progress and science into every household of the nation.") ]   , mrules  = T.intercalate "\n"       [ "* One level only"@@ -265,6 +284,11 @@   , mreason = "In this adventure, finally, your heroes are able to all use ranged attacks at once, given enough ammunition."   , mhint   = "Beware of friendly fire, particularly from explosives. But you need no more hints. Go fulfill your destiny! For Science!"   }+ where+   killedMsg = T.intercalate "\n"+     [ "You turned out to be the prey, this time, not the hunter. In fact, you are not even in the hunters' league. When fighting against such odds, passively waiting for enemy to spring a trap is to no avail, because a professional team can sneak in darkness and ambush the ambushers."+     , "Granted, good positioning is crucial, so that each squad member can overwatch the battlefield and fire opportunistically, using the recently recovered instant telegraphy equipment. However, there is no hope without active scouting, throwing lit objects and probing suspect areas with missiles while paying attention to sounds. And that may still not be enough."+     ]  safari = ModeKind  -- Easter egg available only via screensaver   { mname   = "safari"
GameDefinition/PLAYING.md view
@@ -410,9 +410,9 @@ This time, the *ranged* armor skill of the defender is taken into account and, additionally, the speed of the missile (based on shape and weight) figures in the calculation. You may propel any item from your inventory-(by default you are offered only the appropriate items; press `+` to open-all choices). Only items of a few kinds inflict direct damage, but some-have other effects, beneficial, detrimental or mixed.+(by default you are offered exclusively the appropriate items; press `+`+to open all choices). Only items of a few kinds inflict direct damage,+but some have other effects, beneficial, detrimental or mixed.  In-game detailed item descriptions contain melee and ranged damage estimates. They do not take into account enemy armor nor damage from effects and,@@ -420,19 +420,20 @@ in question. The displayed figures are rounded, but the game internally keeps track of minute fractions of HP for all actors in all calculations. -The combat stress drains Calm, gradually limiting viewing radius and,-if Calm reaches zero and the actor is sufficiently impressed by his foes,-making him defect and surrender unto their domination. Whenever the monster's-or hero's hit points reach zero, the combatant falls down and quickly-gets permanently incapacitated. When the last hero is disabled or dominated,-the adventure ends in defeat.+The combat stress drains Calm, gradually limiting viewing radius.+If Calm reaches zero and the actor is sufficiently impressed by his foes,+the actor defects and surrenders unto their domination. Whenever+the monster's or hero's hit points reach zero, the combatant falls down+and gets permanently incapacitated. When the last hero is disabled+or dominated, the adventure ends in defeat. Eliminating the last+monster at a given time not always results in victory.   Attacking from a distance ------------------------- -Before the player presses `f` to make a ranged attack, he may move-and set the aiming crosshair in aiming mode. However, this is+Before the player presses `f` to make a ranged attack, he may spend time+moving and setting the aiming crosshair in aiming mode. However, this is not often needed, since crosshair is set automatically as soon as a monster comes into view and can still be adjusted for as long as the missile to fling is not chosen.@@ -458,25 +459,72 @@ known position.  +Combat and Speed+----------------++During combat there are some more nuances to movement and actions that+are worth considering. Every action takes an amount of time (equal for+most of the actions). The time that it takes a unit to act is relative+to its speed and additionally affected by the team's communication overhead.++Any member of the squad that has not idled (either moved, used inventory,+attacked or lurked attentively) will incur a 10% speed penalty+on all members of the team on the same level, including themselves,+but excluding the pointman. This is a penalty for the time+and energy they had to spend to take note of any changes+that could be relevant to the squad and for the overhead needed+to communicate it and get it acknowledged by the other squad members.+In this way, a squad where 4 teammates take actions every turn will work+at 60% speed. By the same token, regardless of how numerous the team is,+never more than 10 normal speed teammates can act in the same turn+(the moves of the others are going to spill over to the next turn).++The pointman is an exception to this rule. The pointman may perform+melee attacks or any other action without incurring a team overhead penalty.+This may make it preferable to melee with the pointman, even though+any team member can melee and pointmen can also perform many other actions.+Also, this prevents a pointman from being slowed by the team residing+elsewhere on the level, making any solo pointman vs pointman duels fairer.++In a short scenario this may not be terribly relevant, but in longer+engagements the overhead can make a big difference in damage per second+that a team can put out. E.g., it's a crucial and context-dependent choice+whether to hurry towards a lonely meleeing teammate, thus lowering+the damage output, stay put, fling a projectile, or perhaps get the meleeing+teammate to retreat towards the team. Over a long run, such choices+influence the speed of HP attrition and if it outweighs the benefits+of exploration.++ Winning and dying ----------------- -You win an adventure if you escape the location alive (which may prove-difficult, because your foes tend to gradually build up the ambush squad-blocking your escape route) or, in scenarios with no escape locations,-if you eliminate all opposition. In the former case, your score-is based predominantly on the gold and precious gems you've plundered.-In the latter case, your score is most influenced by the number+You win an adventure if you escape the situation alive, which may prove+difficult, because your foes tend to gradually build up an ambush squad+blocking your escape route. On the other hand, in scenarios with no open+escape routes, the only way to survive is to eliminate all opposition,+which is not a small feat, either.++If escape without a fight to the death is possible, your score is based+in equal parts on the speed with which you manage to leave+the deadly environment and the proportion of gold and precious gems+(including the elixirs that are stored in precious crystal boxes)+you manage to plunder and carry off. Note that engaging in wanton slaughter+is counterproductive in such a setting, because it wastes time+and non-unique enemies never carry valuables, but stash them instead.++If killing all foes is required, your score is most influenced by the number of turns you spent overcoming your foes (the quicker the victory, the better; the slower the demise, the better). Bonus points, affected by the number-of heroes lost, are awarded only if you win. The score is heavily-modified by the chosen game difficulty, but not by any other challenges-(which are, however, proudly displayed in the high score listing).+of heroes lost, are awarded only if you win. The score, in all cases, is+heavily modified by the chosen game difficulty, but not by any other challenges+(which are, however, distinctly announced in the high score listing). -When all your heroes fall, you are going to invariably see a new foolhardy-party of adventurers clamoring to be led into the unknown perils.-They start their conquest afresh, with no experience, no supplies-for survival and no equipment, and new undaunted enemies bar their way.+When all your heroes fall, you lose, but you are going to invariably+see a new foolhardy party of adventurers clamoring to be led+into the unknown perils. They start their conquest afresh,+with no experience, no supplies for survival and no equipment,+and new undaunted enemies bar their way. Lead the new hopeful explorers with wisdom and fortitude!  
GameDefinition/config.ui.default view
@@ -17,13 +17,15 @@ Cmd_2 = ("KP_Insert", ([CmdAim], "", ByAimMode AimModeCmd {exploration = Dashboard, aiming = Accept})) ; Custom key rebinding example: ;-;   Cmd_3 = ("a", ([CmdItem], "My Happy Cmd", Macro ["t"]))+;   Cmd_anyName_youLike = ("a", ([CmdItem], "My Happy Cmd", Macro ["t"])) ; ; Explanation: to (t)rigger a consumable item for use, you normally use ;              the key press 't'. ; Suppose you would like to use the key press 'a' instead of 't'.-;   Cmd_3          - can be anything as long as it doesn't conflict or-;                    overlap with a binding already defined+;   Cmd_anyName_youLike+;                  - can be anything starting with 'Cmd_' (the "Cmd_" prefix+;                    specifies that you are assigning a new command), as long+;                    as it doesn't conflict with a binding already defined ;   "a"            - the new key to use ;   [CmdItem]      - can either be [] or [CmdItem]. If using [CmdItem] ;                    the new keybinding will be displayed in the help menu@@ -32,6 +34,17 @@ ;                    in the help menu, it is recommended to leave this empty [] ;   "My Happy Cmd" - Is a name given to your binding, which would be dispayed ;                    in the help page in-game if you use 'CmdItem' from above+;   Macro ["t"]    - it specifies that you create a new command by combining+;                    or using the existing ones bound to keys. You can combine+;                    several key names within these squared brackets.+;                    Each key name must be enveloped in quotation marks.+;                    If you want to create a command that combines several+;                    simple actions, you should create a sequence of keys+;                    separated by the "," mark.+;                    For example, you may write something like ["r", "*"],+;                    which will remove all items from the shared inventory+;                    directly to the floor. Using Macro commands is the easiest+;                    way to create custom commands.  [hero_names] HeroName_0 = ("Haskell Alvin", "he")
LambdaHack.cabal view
@@ -6,7 +6,7 @@ -- PVP summary:+-+------- breaking API changes --             | |  +----- minor or non-breaking API additions --             | |  | +--- code changes with no API change-version:       0.11.0.0+version:       0.11.0.1 synopsis:      A game engine library for tactical squad ASCII roguelike dungeon crawlers description: LambdaHack is a Haskell game engine library for ASCII roguelike              games of arbitrary theme, size and complexity, with optional@@ -44,8 +44,7 @@ bug-reports:   http://github.com/LambdaHack/LambdaHack/issues license:       BSD-3-Clause license-file:  COPYLEFT-tested-with:   GHC==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.4, GHC==8.10.7,-               GHC==9.0.1+tested-with:   GHC==8.10.*, GHC==9.0.*, GHC==9.2.*, GHC==9.4.* -- Enough files is included in the Hackage package to run CI and manual -- tests off it, to be presentable on Hackage and to create user-friendly -- binary packages.@@ -84,11 +83,6 @@   default:            False   manual:             True -flag with_costly_optimizations-  description:        turn on costly (mostly GHC heap size) optimizations (that give 15-25% speedup)-  default:            True-  manual:             True- flag release   description:        prepare for a release (expose internal functions and types, etc.)   default:            True@@ -121,9 +115,10 @@ -- TODO: remove -Winvalid-haddock when added to -Wall in a GHC I use for haddock   ghc-options:        -fno-ignore-asserts -  if flag(with_costly_optimizations)-    ghc-options:        -fexpose-all-unfoldings -fspecialise-aggressively -fsimpl-tick-factor=200+  ghc-options:        -fexpose-all-unfoldings -fspecialise-aggressively -fsimpl-tick-factor=200 +  ghc-options:        -Wmissed-specialisations+   if flag(with_expensive_assertions)     cpp-options:      -DWITH_EXPENSIVE_ASSERTIONS @@ -230,6 +225,7 @@                       Game.LambdaHack.Client.UI.SessionUI                       Game.LambdaHack.Client.UI.Slideshow                       Game.LambdaHack.Client.UI.SlideshowM+                      Game.LambdaHack.Client.UI.TutorialHints                       Game.LambdaHack.Client.UI.UIOptions                       Game.LambdaHack.Client.UI.UIOptionsParse                       Game.LambdaHack.Client.UI.Watch@@ -318,7 +314,7 @@                       file-embed >= 0.0.11,                       filepath   >= 1.2.0.1,                       hashable   >= 1.1.2.5,-                      hsini      >= 0.2,+                      hsini      >= 0.2 && < 0.5.2 || >= 0.5.2.2,                       witch      >= 0.3,                       keys       >= 3,                       miniutter  >= 0.5.0.0,
Makefile view
@@ -32,10 +32,10 @@ 	google-chrome --no-sandbox --js-flags="--logfile=%t.log --prof" ../lambdahack.github.io/index.html  minific:-	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js+	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.1/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.1/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js  minificForNode:-	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/assert.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/child_process.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/crypto.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dns.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/events.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/globals.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/https.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/os.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/punycode.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/readline.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/stream.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tls.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/url.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/vm.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/buffer.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/cluster.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dgram.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/domain.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/fs.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/http.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/net.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/path.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/querystring.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/repl.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/string_decoder.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tty.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/util.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/zlib.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js+	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.1/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.1/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/assert.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/child_process.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/crypto.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dns.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/events.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/globals.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/https.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/os.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/punycode.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/readline.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/stream.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tls.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/url.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/vm.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/buffer.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/cluster.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dgram.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/domain.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/fs.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/http.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/net.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/path.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/querystring.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/repl.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/string_decoder.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tty.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/util.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/zlib.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js  # Low delay to display animations swiftly and not bore the public too much. # Delay can't be lower than 2, because browsers sometimes treat delay 1
README.md view
@@ -174,10 +174,11 @@ which may already be a part of your OS distribution, but if it's too old (version 3.4 or later is required) you can download the whole current compilation suite as described at https://www.haskell.org/downloads/.-Get the LambdaHack package from Hackage[3] as follows+You can get and run the LambdaHack package from Hackage[3] as follows      cabal update-    cabal run LambdaHack+    cabal install LambdaHack+    ~/.cabal/bin/LambdaHack  For a newer snapshot, clone the source code from github[5] and run `cabal run LambdaHack` from the main directory.
definition-src/Game/LambdaHack/Content/FactionKind.hs view
@@ -126,7 +126,7 @@     , [minBound..maxBound] )   , ( [(HiConst, 200), (HiLoss, -10)]  -- normally, always positive     , victoryOutcomes )-  , ( [(HiSprint, -500)]  -- speed matters, but only if fast enough+  , ( [(HiSprint, -1000)]  -- speed matters, but only if fast enough     , victoryOutcomes )   , ( [(HiBlitz, -100)]  -- speed matters always     , victoryOutcomes )@@ -134,7 +134,7 @@     , deafeatOutcomes )   ] --- Heroes in long crawls rejoice in loot.+-- Heroes in long crawls rejoice in loot. But speedrunning just as valuable. hiHeroLong =   [ ( [(HiLoot, 10000)]  -- multiplied by fraction of collected     , [minBound..maxBound] )
engine-src/Game/LambdaHack/Client/UI.hs view
@@ -157,7 +157,7 @@   report <- getsSession $ newReport . shistory   modifySession $ \sess -> sess {sreportNull = nullVisibleReport report}   slides <- reportToSlideshowKeepHalt False []-  ovs <- case unsnoc slides of+  ovs <- case unsnocSlideshow slides of     Nothing -> return EM.empty     Just (allButLast, (ov, _)) ->       if allButLast == emptySlideshow
engine-src/Game/LambdaHack/Client/UI/DrawM.hs view
@@ -232,7 +232,9 @@  sreportNull <- getsSession sreportNull  let frameForallId = FrameForall $ const $ return ()  case saimMode of-   Just AimMode{detailLevel} | not sreportNull -> do+   Just AimMode{detailLevel} | not sreportNull+                               && detailLevel /= DetailHigh+                               && detailLevel /= DetailLow -> do      COps{corule=RuleContent{rWidthMax, rHeightMax}, coTileSpeedup}        <- getsState scops      StateClient{seps} <- getClient@@ -576,7 +578,7 @@   return #ifdef WITH_EXPENSIVE_ASSERTIONS     $ assert (length status == 2 * _rWidthMax-              `blame` map Color.charFromW32 status)+              `blame` attrStringToString status) #endif         status @@ -777,7 +779,7 @@               [] -> []  -- no timeout-free organ, e.g., rattlesnake or hornet               (ldmg, lextra) : rest -> (ldmg ++ lbonus, lextra) : rest             displayDmgAndExtra (ldmg, lextra) =-              if map Color.charFromW32 ldmg == "0"+              if attrStringToString ldmg == "0"               then case lextra of                 [] -> ldmg                 _plus : lextraRest -> lextraRest
engine-src/Game/LambdaHack/Client/UI/EffectDescription.hs view
@@ -151,7 +151,7 @@       in case ts of         [] -> ""         [wonder] -> wonder <+> sometimes-        _ | detailLevel < DetailAll -> header+        _ | detailLevel < DetailHigh -> header         _ -> header <+> "[" <> T.intercalate ", " ts <> "]" <+> sometimes     OneOf effs ->       let ts = filter (/= "") $ map (effectToSuffix detailLevel) effs@@ -161,7 +161,7 @@       in case ts of         [] -> ""         [wonder] -> wonder <+> sometimes-        _ | detailLevel < DetailAll -> header+        _ | detailLevel < DetailHigh -> header         _ -> header <+> "[" <> T.intercalate ", " ts <> "]" <+> sometimes     OnSmash _ -> ""  -- printed inside a separate section     OnCombine _ -> ""  -- printed inside a separate section@@ -169,11 +169,11 @@                   in if T.null t then "" else "(on user:" <+> t <> ")"     NopEffect -> ""  -- never printed     AndEffect (ConsumeItems tools raw) eff -> case detailLevel of-      DetailAll ->+     DetailLow -> "of crafting"+     DetailMedium -> "of crafting (recipes in lore menu)"+     _ ->        let (tcraft, traw, ttools) = describeCrafting tools raw eff        in tcraft <+> traw <+> ttools-      DetailHigh -> "of crafting (recipes in lore menu)"-      _ -> "of crafting"     AndEffect eff1 eff2 ->       let t = T.intercalate " and then "               $ nub $ filter (not . T.null)
engine-src/Game/LambdaHack/Client/UI/Frontend/Sdl.hs view
@@ -222,7 +222,10 @@         , SDL.windowMode = case fromMaybe NotFullscreen sfullscreenMode of             ModeChange -> SDL.Fullscreen             BigBorderlessWindow -> SDL.FullscreenDesktop-            NotFullscreen -> SDL.Windowed }+            NotFullscreen -> SDL.Windowed+        , SDL.windowResizable = False  -- the default, but just in case...+        , SDL.windowHighDPI = True  -- possibly prevents resize for Retina+        }       rendererConfig = SDL.RendererConfig         { rendererType          = if sbenchmark                                   then SDL.AcceleratedRenderer@@ -372,11 +375,13 @@         SDL.QuitEvent -> forceShutdown sess         SDL.WindowRestoredEvent{} -> redraw  -- e.g., unminimize         SDL.WindowExposedEvent{} -> redraw  -- needed on Windows-        SDL.WindowResizedEvent{} -> do-          -- Eome window managers apparently are able to resize.-          SDL.showSimpleMessageBox Nothing SDL.Warning-            "Windows resize detected"-            "Please resize the game and/or make it fullscreen via 'allFontsScale' and 'fullscreenMode' settings in the 'config.ui.ini' file. Resizing fonts via generic scaling algorithms gives poor results."+        SDL.WindowResizedEvent{} ->+          -- Some window managers apparently are able to resize.+          -- And some send resize events at startup, even though+          -- they don't resize eventually, so this is too much spam:+          -- SDL.showSimpleMessageBox Nothing SDL.Warning+          --  "Windows resize detected"+          --  "Please resize the game and/or make it fullscreen via 'allFontsScale' and 'fullscreenMode' settings in the 'config.ui.ini' file. Resizing fonts via generic scaling algorithms gives poor results."           redraw         -- Probably not needed, because no textures nor their content lost:         -- SDL.WindowShownEvent{} -> redraw@@ -687,7 +692,7 @@                                     , Color.HighlightNoneCursor ]) ()             fg | even row && fgRaw == Color.White = Color.AltWhite                | otherwise = fgRaw-            t = T.pack $ map Color.charFromW32 $ w : sameRest+            t = T.pack . attrStringToString  $ w : sameRest         width <- drawPropChunk x row fg t         drawPropLine (x + width) row otherRest       drawPropChunk :: Int -> Int -> Color.Color -> T.Text -> IO Int
engine-src/Game/LambdaHack/Client/UI/HandleHelperM.hs view
@@ -120,10 +120,14 @@     _ | banned && lidV /= blid body ->       failMsg $ showReqFailure NoChangeDunLeader     [] -> failMsg "cannot pick any other pointman on this level"-    (np, b, _) : _ -> do-      success <- pickLeader verbose np-      let !_A = assert (success `blame` "same leader"-                                `swith` (leader, np, b)) ()+    (np, _b, _) : _ -> do+      _success <- pickLeader verbose np+-- This apparently crashes rarely, probably becuase @leader@ gets out of sync+-- with the leader in client state, during nested UI manipulations.+-- The worst that can happen is TAB doing nothing and having to be repeated+-- or perhaps item manipulation needs to be exited for TAB to work again.+--      let !_A = assert (success `blame` "same leader"+--                                `swith` (leader, np, b)) ()       return Nothing  -- | Switches current pointman to the previous in the whole dungeon, wrapping.@@ -207,7 +211,7 @@         (Just (iid, _, _), Just pos, Just leader) -> do           b <- getsState $ getActorBody leader           if lidV /= blid b  -- no range warnings on remote levels-             || detailLevel aimMode < DetailAll  -- no spam+             || detailLevel aimMode < DetailHigh  -- no spam           then return []           else do             itemFull <- getsState $ itemToFull iid@@ -278,7 +282,7 @@               else viewItem itemFull             phrase = makePhrase               [partItemWsRanged rwidth side factionD displayRanged-                                DetailMedium 4 k localTime itemFull kit]+                                DetailLow 4 k localTime itemFull kit]             ncha = ncharges localTime kit             periodic = IA.checkFlag Ability.Periodic arItem             !cLab = Color.AttrChar { acAttr = attrCursor@@ -712,7 +716,7 @@                   | isJust $ btrajectory body = "move through here"                   | otherwise = resideVerb           verbs = flyVerb : guardVerbs-          projDesc | not (bproj body) || detail < DetailAll = ""+          projDesc | not (bproj body) || detail < DetailHigh = ""                    | otherwise =             let kit = beqp body EM.! btrunk body                 ps = [partItemMediumAW rwidth side factionD localTime@@ -727,11 +731,11 @@                   tfact = factionD EM.! tfid               in "Originally of" <+> gname tfact                  <> ", now fighting for" <+> dominatedBy <> "."-            _ | detail < DetailAll -> ""  -- only domination worth spamming+            _ | detail < DetailHigh -> ""  -- only domination worth spamming             _ | bfid body == side -> ""  -- just one of us             _ | bproj body -> "Launched by" <+> gname bfact <> "."             _ -> "One of" <+> gname bfact <> "."-          idesc = if detail < DetailAll+          idesc = if detail < DetailHigh                   then ""                   else IK.idesc $ itemKind itemFull           -- If many different actors, only list names.@@ -823,7 +827,7 @@       -- that looks stand over the items, because then he can check details       -- with inventory commands (or look in aiming mode).       detailExploration = if standingOn && Just side == (bfid <$> mb)-                          then DetailMedium+                          then DetailLow                           else DetailAll       detail = maybe detailExploration detailLevel saimMode   localTime <- getsState $ getLocalTime lidV@@ -850,9 +854,6 @@         partItemWsDetail detail                          rwidth side factionD k localTime (itemToF iid) kit       (object, person) = case EM.assocs is of-        [(_, (k, _))] | detail == DetailLow ->-          (if k == 1 then "an item" else "an item stack", MU.Sg3rd)-        _ | detail == DetailLow -> ("some items", MU.PlEtc)         ii : _ : _ : _ | detail <= DetailMedium ->           (MU.Phrase [nWs ii, "and other items"], MU.PlEtc)         [ii@(_, (1, _))] -> (nWs ii, MU.Sg3rd)@@ -951,7 +952,7 @@                        else "The following items on the ground or in equipment enable special transformations:"                             <+> tItems <> "."  -- not telling to what terrain       modifyBlurb = alterBlurb <+> transformBlurb-      midEOL = if detail < DetailHigh+      midEOL = if detail < DetailMedium                   || T.null stashBlurb && T.null actorsDesc                   || T.null smellBlurb && T.null itemsBlurb                   || null embedsList && T.null modifyBlurb@@ -960,26 +961,18 @@       ms = [ (MsgPromptAction, stashBlurb)            , (actorMsgClass, actorsBlurb)            , (MsgPromptGeneric, actorsDesc <> midEOL) ]-           ++ [(MsgPromptGeneric, smellBlurb) | detail >= DetailHigh]+           ++ [(MsgPromptGeneric, smellBlurb) | detail >= DetailMedium]            ++ [(MsgPromptItems, itemsBlurb <> midEOL)]-           ++ [(MsgPromptFocus, tileBlurb) | detail >= DetailHigh-                                             || detail == DetailMedium+           ++ [(MsgPromptFocus, tileBlurb) | detail >= DetailMedium+                                             || detail == DetailLow                                                 && not (null embedsList)]-           ++ [(MsgPromptGeneric, placeBlurb) | detail >= DetailHigh]+           ++ [(MsgPromptGeneric, placeBlurb) | detail >= DetailMedium]            ++ case detail of                 DetailLow -> []  -- not to obscure aiming line-                DetailMedium ->-                  [(MsgPromptMention, case embedsList of-                    [] -> ""-                    [(k, _)] ->-                      ppEmbedName (1, if k == 1-                                      then "an embedded item"-                                      else "a stack of embedded items")-                    _ -> ppEmbedName (9, "some embedded items"))]                 _ -> let n = sum $ map fst embedsList                          wWandW = MU.WWandW $ map snd embedsList                      in [(MsgPromptMention, ppEmbedName (n, wWandW)) | n > 0]-           ++ [(MsgPromptModify, modifyBlurb) | detail == DetailAll]+           ++ [(MsgPromptModify, modifyBlurb) | detail >= DetailHigh]   return $! if all (T.null . snd) ms && detail > DetailLow             then [(MsgPromptFocus, tileBlurb)]             else ms
engine-src/Game/LambdaHack/Client/UI/HandleHumanGlobalM.hs view
@@ -518,7 +518,7 @@                map (\(iid, kit) -> (getKind iid, (iid, kit))) (EM.assocs embeds)              ilooks = map itemLook $ sortEmbeds cops t embedKindList          failWith $ makePhrase $-           ["there is no point kicking", MU.AW name]+           ["there is no way to activate or modify", MU.AW name]            ++ if EM.null embeds               then []               else ["with", MU.WWandW ilooks]
engine-src/Game/LambdaHack/Client/UI/HandleHumanLocalM.hs view
@@ -966,7 +966,7 @@   doLook  detailCycle :: DetailLevel -> DetailLevel-detailCycle detail = if detail == maxBound then minBound else succ detail+detailCycle detail = if detail == minBound then maxBound else pred detail  -- * ClearTargetIfItemClear 
engine-src/Game/LambdaHack/Client/UI/InventoryM.hs view
@@ -200,7 +200,6 @@                   Just (lid, _) ->                     map MU.Text ["on level", tshow $ abs $ fromEnum lid]                   Nothing -> [] )-            _ -> ("see", [])           ownObject = case cstore of             CStash -> ["our", MU.Text t]             _ -> [MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t]
engine-src/Game/LambdaHack/Client/UI/ItemDescription.hs view
@@ -138,7 +138,7 @@         let ppA = kindAspectToSuffix             ppE = effectToSuffix detLev             reduce_a = maybe "?" tshow . Dice.reduceDice-            restEs | detLev >= DetailHigh+            restEs | detLev >= DetailMedium                      || not (IA.checkFlag Ability.MinorEffects arItem) =                      IK.ieffects itemKind                    | otherwise = []@@ -154,7 +154,7 @@             onCombineRawTs = T.intercalate " " $ filter (not . T.null)                              $ map (ppE . unCombine) combineEffsRaw             onCombineRawTsTooLarge =-              detailLevel >= DetailAll && T.length onCombineRawTs > 120+              detailLevel >= DetailHigh && T.length onCombineRawTs > 120             (combineEffs, noSmashCombineEffs) =               if onCombineRawTsTooLarge               then (combineEffsRaw, noSmashCombineEffsRaw)@@ -227,7 +227,7 @@           , [damageText]             ++ [timeoutText | detLev > DetailLow && not periodic]             ++ aes-            ++ if detLev >= DetailAll+            ++ if detLev >= DetailHigh                then [onCombine, onSmash]                else [onCombineRawTs] )       hurtMult = armorHurtCalculation True (IA.aSkills arItem)@@ -259,7 +259,7 @@            , filter (/= "")              $ elab                : tsSplit-               ++ if detailLevel >= DetailAll+               ++ if detailLevel >= DetailHigh                   then map kindAspectToSuffix aspectsAux                   else [] )   in (onCombineTsAss, aspectDescs, rangedDamageDesc)@@ -268,7 +268,7 @@ partItem :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant          -> (MU.Part, MU.Part) partItem width side factionD =-  partItemN width side factionD False DetailMedium 4+  partItemN width side factionD False DetailLow 4  partItemShort :: Int -> FactionId -> FactionDict -> Time -> ItemFull               -> ItemQuant@@ -285,7 +285,7 @@ partItemHigh :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant              -> ([Text], MU.Part, MU.Part) partItemHigh width side factionD =-  partItemN3 width side factionD False DetailAll 100+  partItemN3 width side factionD False DetailHigh 100  -- The @count@ can be different than @itemK@ in @ItemFull@, e.g., when picking -- a subset of items to drop.@@ -318,16 +318,16 @@                  -> Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull                  -> ItemQuant                  -> MU.Part-partItemWsDetail DetailLow = \_ _ _ _ _ _ _ -> ""-partItemWsDetail DetailMedium = partItemWsShortest-partItemWsDetail DetailHigh = partItemWs+partItemWsDetail DetailLow = partItemWsShortest+partItemWsDetail DetailMedium = partItemWs+partItemWsDetail DetailHigh = partItemWsLong partItemWsDetail DetailAll = partItemWsLong  partItemWs :: Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull            -> ItemQuant            -> MU.Part partItemWs width side factionD =-  partItemWsRanged width side factionD False DetailMedium 4+  partItemWsRanged width side factionD False DetailLow 4  partItemWsShortest :: Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull                    -> ItemQuant@@ -345,7 +345,7 @@                -> ItemQuant                -> MU.Part partItemWsLong width side factionD =-  partItemWsRanged width side factionD False DetailHigh 100+  partItemWsRanged width side factionD False DetailMedium 100  partItemShortAW :: Int -> FactionId -> FactionDict -> Time -> ItemFull                 -> ItemQuant@@ -360,7 +360,7 @@                  -> MU.Part partItemMediumAW width side factionD localTime itemFull kit =   let (name, powers) =-        partItemN width side factionD False DetailMedium 100 localTime+        partItemN width side factionD False DetailLow 100 localTime                   itemFull kit       arItem = aspectRecordFull itemFull       phrase = MU.Phrase [name, powers]
engine-src/Game/LambdaHack/Client/UI/MonadClientUI.hs view
@@ -128,10 +128,9 @@ -- | Push frames or delays to the frame queue. The frames depict -- the @lid@ level. displayFrames :: MonadClientUI m => LevelId -> PreFrames3 -> m ()-displayFrames _ [] = return ()+displayFrames _ [] = return ()  -- shortcut displayFrames lid frs = do   let framesRaw = case frs of-        [] -> []         [Just ((bfr, ffr), (ovProp, ovSquare, ovMono))] ->           [Just ( (FrameBase $ U.unsafeThaw bfr, ffr)                 , (ovProp, ovSquare, ovMono) )]
engine-src/Game/LambdaHack/Client/UI/Msg.hs view
@@ -241,6 +241,7 @@     MsgHeardFaraway -> False     -- MsgHeardNearby interrupts, even if running started while hearing close     MsgRangedOthers -> False+    MsgNeutralEvent -> False     MsgAtFeetMinor -> False     _ -> True   MsgClassShow x -> case x of@@ -447,7 +448,7 @@      else as ++ stringToAS ("<x" ++ show n ++ ">")  anyInReport :: (MsgClass -> Bool) -> Report -> Bool-anyInReport f (Report xns) = any (f . msgClass. repMsg) xns+anyInReport f (Report xns) = any (f . msgClass . repMsg) xns  -- * History 
engine-src/Game/LambdaHack/Client/UI/MsgM.hs view
@@ -1,7 +1,7 @@ -- | Monadic operations on game messages. module Game.LambdaHack.Client.UI.MsgM   ( msgAddDuplicate, msgAddDistinct, msgAdd, msgLnAdd-  , promptMainKeys, recordHistory+  , promptMainKeys, recordHistory, tutorialHintMsgAdd   ) where  import Prelude ()@@ -15,6 +15,8 @@ import           Game.LambdaHack.Client.UI.MonadClientUI import           Game.LambdaHack.Client.UI.Msg import           Game.LambdaHack.Client.UI.SessionUI+import           Game.LambdaHack.Client.UI.TutorialHints+  (TutorialHints, renderTutorialHints) import           Game.LambdaHack.Client.UI.UIOptions import           Game.LambdaHack.Common.ActorState import           Game.LambdaHack.Common.MonadStateRead@@ -69,6 +71,10 @@ -- | Add a message to the current report. msgAdd :: (MonadClientUI m, MsgShared a) => a -> Text -> m () msgAdd msgClass t = void $ msgAddDuplicate msgClass t++-- | Add a tutorial hint message to the current report.+tutorialHintMsgAdd :: MonadClientUI m => TutorialHints -> m ()+tutorialHintMsgAdd = msgAdd MsgTutorialHint . renderTutorialHints  -- | Add a message to the current report. End previously collected report, -- if any, with newline.
engine-src/Game/LambdaHack/Client/UI/Overlay.hs view
@@ -7,6 +7,7 @@     FontSetup(..), multiFontSetup, singleFontSetup   , -- * AttrString     AttrString, blankAttrString, textToAS, textFgToAS, stringToAS+  , attrStringToString   , (<+:>), (<\:>)     -- * AttrLine   , AttrLine, attrLine, emptyAttrLine, attrStringToAL, firstParagraph@@ -106,6 +107,10 @@ stringToAS :: String -> AttrString stringToAS = map Color.attrChar1ToW32 +-- | Transform 'AttrString' type to 'String'.+attrStringToString :: AttrString -> String+attrStringToString = map Color.charFromW32+ -- Follows minimorph.<+>. infixr 6 <+:>  -- matches Monoid.<> (<+:>) :: AttrString -> AttrString -> AttrString@@ -135,7 +140,7 @@         Nothing -> False         Just [] -> True         Just (c : _) -> isSpace c-  in any (isPrefixOfNb $ map Color.charFromW32 s) nonbreakableRev+  in any (isPrefixOfNb $ attrStringToString s) nonbreakableRev  breakAtSpace :: AttrString -> (AttrString, AttrString) breakAtSpace lRev =@@ -163,7 +168,7 @@ #ifdef WITH_EXPENSIVE_ASSERTIONS   assert (allB (\ac -> Color.charFromW32 ac /= '\n') s) $  -- expensive in menus   assert (null s || last s /= Color.spaceAttrW32-          `blame` map Color.charFromW32 s) $+          `blame` attrStringToString s) $     -- only expensive for menus, but often violated by code changes, so disabled     -- outside test runs #endif
engine-src/Game/LambdaHack/Client/UI/Slideshow.hs view
@@ -6,7 +6,7 @@   , ButtonWidth(..)   , KYX, xytranslateKXY, xtranslateKXY, ytranslateKXY, yrenumberKXY   , OKX, emptyOKX, xytranslateOKX, sideBySideOKX, labDescOKX-  , Slideshow(slideshow), emptySlideshow, unsnoc, toSlideshow+  , Slideshow(slideshow), emptySlideshow, unsnocSlideshow, toSlideshow   , attrLinesToFontMap, menuToSlideshow, wrapOKX, splitOverlay, splitOKX   , highSlideshow #ifdef EXPOSE_INTERNAL@@ -118,8 +118,8 @@ emptySlideshow :: Slideshow emptySlideshow = Slideshow [] -unsnoc :: Slideshow -> Maybe (Slideshow, OKX)-unsnoc Slideshow{slideshow} =+unsnocSlideshow :: Slideshow -> Maybe (Slideshow, OKX)+unsnocSlideshow Slideshow{slideshow} =   case reverse slideshow of     [] -> Nothing     okx : rest -> Just (Slideshow $ reverse rest, okx)
+ engine-src/Game/LambdaHack/Client/UI/TutorialHints.hs view
@@ -0,0 +1,65 @@+module Game.LambdaHack.Client.UI.TutorialHints+  ( TutorialHints (..)+  , renderTutorialHints+  ) where++import Prelude ()++import Game.LambdaHack.Core.Prelude++data TutorialHints =+    DamageOfDifferentKind+  | NewFloorNewOpportunity+  | CannotHarmYouInMelee+  | CaughtProjectile+  | HitsWithNoDirectDamage+  | TemporaryConditions+  | WokenUpActors+  | AvoidWalkingEnemies+  | AlotOfDamageFromOneSource+  | TerrainNotFullyKnown+  | OutOfSightEvents+  | HearingRadius+  | SwitchTeammate+  | MeleeEnemies+  | UseTerrainEffect+  | SwitchPointmanAndAvoidMeleeAlone+  | SwitchPointmanAndSoftenFoes++-- | Generate the standard textual representation for the tutorial hints.+renderTutorialHints :: TutorialHints -> Text+renderTutorialHints = \case+  DamageOfDifferentKind ->+    "You took damage of a different kind than the normal piercing hit, which means your armor couldn't block any part of it. Normally, your HP (hit points, health) do not regenerate, so losing them is a big deal. Apply healing concoctions or take a long sleep to replenish your HP (but in this hectic environment not even uninterrupted resting that leads to sleep is easy)."+  NewFloorNewOpportunity ->+    "New floor is new opportunities, though the old level is still there and others may roam it after you left. Viewing all floors, without moving between them, can be done using the '<' and '>' keys."+  CannotHarmYouInMelee ->+    "This enemy can't harm you in melee. Left alone could it possibly be of some use?"+  CaughtProjectile ->+    "You managed to catch a projectile, thanks to being braced and hitting it exactly when it was at arm's reach. The obtained item has been put into the shared stash of your party."+  HitsWithNoDirectDamage ->+    "Some hits don't cause piercing, impact, burning nor any other direct damage. However, they can have other effects, bad, good or both."+  WokenUpActors ->+    "Woken up actors regain stats and skills, including sight radius and melee armor, over several turns."+  AvoidWalkingEnemies ->+    "To avoid waking enemies up, make sure they don't lose HP nor too much Calm through noises, particularly close ones. Beware, however, that they slowly regenerate HP as they sleep and eventually wake up at full HP."+  AlotOfDamageFromOneSource ->+    "You took a lot of damage from one source. If the danger persists, consider retreating towards your teammates or buffing up or an instant escape, if consumables permit."+  TemporaryConditions ->+    "Temporary conditions, especially the bad ones, pass quickly, usually after just a few turns. While active, they are listed in the '@' organ menu and the effects of most of them are seen in the '#' skill menu."+  TerrainNotFullyKnown ->+    "Solid terrain drawn in pink is not fully known until searched. This is usually done by bumping into it, which also triggers effects and transformations the terrain is capable of. Once revealed, the terrain can be inspected in aiming mode started with the '*' key or with mouse."+  OutOfSightEvents ->+    "Events out of your sight radius (as listed in the '#' skill menu) can sometimes be heard, depending on your hearing radius skill. Some, such as death shrieks, can always be heard regardless of skill and distance, including when they come from a different floor."+  HearingRadius ->+    "Enemies you can't see are sometimes heard yelling and emitting other noises. Whether you can hear them, depends on their distance and your hearing radius, as listed in the '#' skill menu."+  SwitchTeammate ->+    "You survive this mission, or die trying, as a team. After a few moves, feel free to switch the controlled teammate (marked on the map with the yellow box) using the Tab key to another party member (marked with a green box)."  -- assuming newbies don't remap their keys+  MeleeEnemies ->+    "Enemies are normally dealt with using melee (by bumping when adjacent) or ranged combat (by 'f'linging items at them)."+  UseTerrainEffect ->+    "Enemies can be dealt with not only via combat, but also with clever use of terrain effects, stealth (not emitting nor reflecting light) or hasty retreat (particularly when foes are asleep or drowsy)."+  SwitchPointmanAndAvoidMeleeAlone ->+    "When dealing with groups of enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Avoid meleeing alone."+  SwitchPointmanAndSoftenFoes ->+    "When dealing with groups of armed enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Retreat, if necessary to form a front line. Soften the foes with missiles, especially of exploding kind."
engine-src/Game/LambdaHack/Client/UI/UIOptionsParse.hs view
@@ -48,7 +48,10 @@                 Just _ ->                   let (key, def) = readError keydef                   in (K.mkKM key, def :: CmdTriple)-                Nothing -> configError $ "wrong macro id" `showFailure` ident+                Nothing ->+                  configError+                    $ "macro id should start with Cmd_ and it does not:"+                      `showFailure` ident             section = Ini.allItems "additional_commands" cfg         in map mkCommand section       uHeroNames =
engine-src/Game/LambdaHack/Client/UI/Watch/WatchSfxAtomicM.hs view
@@ -16,7 +16,7 @@ import qualified Data.Text as T import qualified NLP.Miniutter.English as MU -import           Game.LambdaHack.Atomic+import           Game.LambdaHack.Atomic (SfxAtomic (..), SfxMsg (..)) import           Game.LambdaHack.Client.MonadClient import           Game.LambdaHack.Client.State import           Game.LambdaHack.Client.UI.ActorUI@@ -35,6 +35,7 @@ import           Game.LambdaHack.Client.UI.MsgM import           Game.LambdaHack.Client.UI.SessionUI import           Game.LambdaHack.Client.UI.SlideshowM+import           Game.LambdaHack.Client.UI.TutorialHints (TutorialHints (..)) import           Game.LambdaHack.Client.UI.Watch.WatchCommonM import           Game.LambdaHack.Common.Actor import           Game.LambdaHack.Common.ActorState@@ -155,7 +156,7 @@           -- out only in Allure.           when (isOurCharacter                 && Ability.getSk Ability.SkArmorMelee actorMaxSk > 0) $-            msgAdd MsgTutorialHint "You took damage of a different kind than the normal piercing hit, which means your armor couldn't block any part of it. Normally, your HP (hit points, health) do not regenerate, so losing them is a big deal. Apply healing concoctions or take a long sleep to replenish your HP (but in this hectic environment not even uninterrupted resting that leads to sleep is easy)."+            tutorialHintMsgAdd DamageOfDifferentKind         feelLookHPGood = feelLook MsgGoodMiscEvent MsgBadMiscEvent         feelLookCalm bigAdj projAdj = when isAlive $           feelLook MsgEffectMinor MsgEffectMinor bigAdj projAdj@@ -260,7 +261,7 @@               let desc = cdesc $ okind cocave $ lkind lvl               unless (T.null desc) $                 msgAdd MsgBackdropInfo $ desc <> "\n"-              msgAdd MsgTutorialHint "New floor is new opportunities, though the old level is still there and others may roam it after you left. Viewing all floors, without moving between them, can be done using the '<' and '>' keys."+              tutorialHintMsgAdd NewFloorNewOpportunity             [] -> return ()  -- spell fizzles; normally should not be sent       IK.Escape{} | isOurCharacter -> do         ours <- getsState $ fidActorNotProjGlobalAssocs side@@ -871,7 +872,7 @@         tutorialHintBenignFoe =           when (bfid sb == side                 && not (actorCanMeleeToHarm actorMaxSkills target tb)) $-            msgAdd MsgTutorialHint "This enemy can't harm you in melee. Left alone could it possibly be of some use?"+            tutorialHintMsgAdd CannotHarmYouInMelee     -- The messages about parrying and immediately afterwards dying     -- sound goofy, but there is no easy way to prevent that.     -- And it's consistent.@@ -883,7 +884,7 @@                      [MU.SubjectVerbSg spart "catch", tpart, "skillfully"]          msgAdd MsgSpecialEvent msg          when (bfid sb == side) $-           msgAdd MsgTutorialHint "You managed to catch a projectile, thanks to being braced and hitting it exactly when it was at arm's reach. The obtained item has been put into the shared stash of your party."+           tutorialHintMsgAdd CaughtProjectile          animate (blid tb) $ blockHit ps Color.BrGreen Color.Green        | not (hasCharge localTime kitWeapon) -> do          -- Can easily happen with a thrown discharged item.@@ -923,7 +924,7 @@                ++ if bproj sb then [] else weaponNameWith          msgAdd msgClassMelee msg  -- too common for color          when (bfid sb == side || bfid tb == side) $-           msgAdd MsgTutorialHint "Some hits don't cause piercing, impact, burning nor any other direct damage. However, they can have other effects, bad, good or both."+           tutorialHintMsgAdd HitsWithNoDirectDamage          animate (blid tb) $ subtleHit ps        | bproj sb -> do  -- more terse than melee, because sometimes very spammy          let msgRangedPowerful | targetIsFoe = MsgRangedMightyWe
engine-src/Game/LambdaHack/Client/UI/Watch/WatchUpdAtomicM.hs view
@@ -40,6 +40,7 @@ import           Game.LambdaHack.Client.UI.MsgM import           Game.LambdaHack.Client.UI.SessionUI import           Game.LambdaHack.Client.UI.SlideshowM+import           Game.LambdaHack.Client.UI.TutorialHints (TutorialHints (..)) import           Game.LambdaHack.Client.UI.UIOptions import           Game.LambdaHack.Client.UI.Watch.WatchCommonM import           Game.LambdaHack.Client.UI.Watch.WatchQuitM@@ -137,7 +138,7 @@                  when (bfid b == side && not good) $                    -- Others get conditions too often and good ones are not                    -- dire enough and also too common.-                   msgAdd MsgTutorialHint "Temporary conditions, especially the bad ones, pass quickly, usually after just a few turns. While active, they are listed in the '@' organ menu and the effects of most of them are seen in the '#' skill menu."+                   tutorialHintMsgAdd TemporaryConditions                | otherwise -> do                  wown <- ppContainerWownW partActorLeader True c                  itemVerbMU MsgItemCreation iid kit@@ -186,12 +187,12 @@   UpdMoveActor aid source target -> moveActor aid source target   UpdWaitActor aid WSleep _ -> do     aidVerbMU MsgStatusWakeup aid "wake up"-    msgAdd MsgTutorialHint "Woken up actors regain stats and skills, including sight radius and melee armor, over several turns."+    tutorialHintMsgAdd WokenUpActors   UpdWaitActor aid WWake _ -> do     side <- getsClient sside     b <- getsState $ getActorBody aid     unless (bfid b == side) $-      msgAdd MsgTutorialHint "To avoid waking enemies up, make sure they don't lose HP nor too much Calm through noises, particularly close ones. Beware, however, that they slowly regenerate HP as they sleep and eventually wake up at full HP."+      tutorialHintMsgAdd AvoidWalkingEnemies   UpdWaitActor{} -> return ()  -- falling asleep handled uniformly elsewhere   UpdDisplaceActor source target -> displaceActorUI source target   UpdMoveItem iid k aid c1 c2 -> moveItemUI iid k aid c1 c2@@ -261,7 +262,7 @@          when (bfid b == side && not (bproj b)) $ do            when (abs hpDelta >= oneM) $ markDisplayNeeded (blid b)            when (hpDelta < 0) $ do-             when (hpDelta <= xM (-3)) $ msgAdd MsgTutorialHint "You took a lot of damage from one source. If the danger persists, consider retreating towards your teammates or buffing up or an instant escape, if consumables permit."+             when (hpDelta <= xM (-3)) $ tutorialHintMsgAdd AlotOfDamageFromOneSource              sUIOptions <- getsSession sUIOptions              currentWarning <-                getsState $ checkWarningHP sUIOptions aid (bhp b)@@ -283,7 +284,7 @@              let bPrev = b {bcalm = bcalm b - calmDelta}              when (calmEnough b actorMaxSk                    && not (calmEnough bPrev actorMaxSk)) $-               msgAdd MsgNeutralEvent "You are again calm enough to manage your equipment outfit."+               msgAdd MsgSpecialEvent "You are again calm enough to manage your equipment outfit."            -- If the leader regenerates Calm more often than once per            -- standard game turn, this will not be reflected, for smoother            -- and faster display. However, every halt for keypress@@ -443,7 +444,7 @@                            , MU.AW object ]     unless (subject2 == object) $ do       msgAdd MsgTerrainReveal msg-      msgAdd MsgTutorialHint "Solid terrain drawn in pink is not fully known until searched. This is usually done by bumping into it, which also triggers effects and transformations the terrain is capable of. Once revealed, the terrain can be inspected in aiming mode started with the '*' key or with mouse."+      tutorialHintMsgAdd TerrainNotFullyKnown   UpdHideTile{} -> return ()   UpdSpotTile{} -> return ()   UpdLoseTile{} -> return ()@@ -615,11 +616,11 @@     msgAdd msgClass msg     case hearMsg of       HearUpd UpdDestroyActor{} ->-        msgAdd MsgTutorialHint "Events out of your sight radius (as listed in the '#' skill menu) can sometimes be heard, depending on your hearing radius skill. Some, such as death shrieks, can always be heard regardless of skill and distance, including when they come from a different floor."+        tutorialHintMsgAdd OutOfSightEvents       HearTaunt{} -> do         globalTime <- getsState stime         when (globalTime > timeTurn) $  -- avoid too many hints at the start-          msgAdd MsgTutorialHint "Enemies you can't see are sometimes heard yelling and emitting other noises. Whether you can hear them, depends on their distance and your hearing radius, as listed in the '#' skill menu."+          tutorialHintMsgAdd HearingRadius       _ -> return ()   UpdMuteMessages _ smuteMessages ->     modifySession $ \sess -> sess {smuteMessages}@@ -722,7 +723,7 @@          when born $ do            let verb = "join you"            aidVerbMU MsgSpottedActor aid verb-           msgAdd MsgTutorialHint "You survive this mission, or die trying, as a team. After a few moves, feel free to switch the controlled teammate (marked on the map with the yellow box) using the Tab key to another party member (marked with a green box)."  -- assuming newbies don't remap their keys+           tutorialHintMsgAdd SwitchTeammate            animate (blid body) $ actorX (bpos body)      | otherwise -> do        -- Don't spam if the actor was already visible@@ -771,14 +772,14 @@          case threat of            ThreatNone -> return ()  -- too rare to care ATM            ThreatUnarmed ->-             msgAdd MsgTutorialHint "Enemies are normally dealt with using melee (by bumping when adjacent) or ranged combat (by 'f'linging items at them)."+             tutorialHintMsgAdd MeleeEnemies            ThreatArmed ->-             msgAdd MsgTutorialHint "Enemies can be dealt with not only via combat, but also with clever use of terrain effects, stealth (not emitting nor reflecting light) or hasty retreat (particularly when foes are asleep or drowsy)."+             tutorialHintMsgAdd UseTerrainEffect            _ | length friendAssocs <= 1 -> return ()  -- one member on level            ThreatAnotherUnarmed ->-             msgAdd MsgTutorialHint "When dealing with groups of enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Avoid meleeing alone."+             tutorialHintMsgAdd SwitchPointmanAndAvoidMeleeAlone            ThreatAnotherArmed ->-             msgAdd MsgTutorialHint "When dealing with groups of armed enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Retreat, if necessary to form a front line. Soften the foes with missiles, especially of exploding kind."+             tutorialHintMsgAdd SwitchPointmanAndSoftenFoes          animate (blid body) $ actorX (bpos body)  destroyActorUI :: MonadClientUI m => Bool -> ActorId -> Actor -> m ()
engine-src/Game/LambdaHack/Common/HighScore.hs view
@@ -25,9 +25,9 @@ import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Time+import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind (ItemKind) import Game.LambdaHack.Content.ModeKind-import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Definition.Defs  -- | A single score record. Records are ordered in the highscore table,@@ -38,7 +38,7 @@   , date         :: POSIXTime  -- ^ date of the last game interruption   , status       :: Status     -- ^ reason of the game interruption   , challenge    :: Challenge  -- ^ challenge setup of the game-  , gkindName  :: Text       -- ^ name of the faction's gkind+  , gkindName    :: Text       -- ^ name of the faction's gkind   , ourVictims   :: EM.EnumMap (ContentId ItemKind) Int  -- ^ allies lost   , theirVictims :: EM.EnumMap (ContentId ItemKind) Int  -- ^ foes killed   }@@ -89,7 +89,10 @@         HiLoot | dungeonTotal == 0 -> c  -- a fluke; no gold generated         HiLoot -> c * intToDouble total / intToDouble dungeonTotal         HiSprint -> -- Up to -c turns matter.-                    max 0 (-c - turnsSpent)+          let speedup = max 0 (-c - turnsSpent)+          in if c >= -10000+             then speedup  -- every turn matters+             else 10000 * speedup / (-c)  -- prevent exploit speedruns         HiBlitz -> -- Up to 1000000/-c turns matter.                    sqrt $ max 0 (1000000 + c * turnsSpent)         HiSurvival -> -- Up to 1000000/c turns matter.
engine-src/Game/LambdaHack/Common/Kind.hs view
@@ -8,10 +8,10 @@   , getKindMean, speedupItem   , okind, omemberGroup, oisSingletonGroup, ouniqGroup, opick   , ofoldlWithKey', ofoldlGroup', omapVector, oimapVector-  , olength, linearInterpolation+  , olength, linearInterpolation, emptyMultiGroupMode, emptyMultiGroupItem #ifdef EXPOSE_INTERNAL-  , emptyMultiGroupItem, emptyUnknownTile-  , emptyUIFactionGroupName, emptyMultiGroupMode+  , emptyUnknownTile+  , emptyUIFactionGroupName #endif     -- * Operations both internal and used in unit tests   , emptyUIFaction
test/CommonMUnitTests.hs view
@@ -4,42 +4,16 @@  import Game.LambdaHack.Core.Prelude -import qualified Data.EnumMap.Strict as EM-import           Test.Tasty-import           Test.Tasty.HUnit+import Test.Tasty+import Test.Tasty.HUnit -import           Game.LambdaHack.Client.CommonM-import           Game.LambdaHack.Common.Area-import           Game.LambdaHack.Common.Kind-import           Game.LambdaHack.Common.Level-import           Game.LambdaHack.Common.Perception-import           Game.LambdaHack.Common.Point-import           Game.LambdaHack.Common.State-import           Game.LambdaHack.Common.Time-import           Game.LambdaHack.Content.TileKind-import qualified Game.LambdaHack.Core.Dice as Dice+import Game.LambdaHack.Client.CommonM+import Game.LambdaHack.Common.Kind (emptyCOps)+import Game.LambdaHack.Common.Perception (emptyPer)+import Game.LambdaHack.Common.Point (Point (..))  import UnitTestHelpers--testLevel :: Level-testLevel = Level-  { lkind = toEnum 0-  , ldepth = Dice.AbsDepth 1-  , lfloor = EM.empty-  , lembed = EM.empty-  , lbig = EM.empty-  , lproj = EM.empty-  , ltile = unknownTileMap (fromJust (toArea (0,0,0,0))) unknownId 10 10-  , lentry = EM.empty-  , larea = trivialArea (Point 0 0)-  , lsmell = EM.empty-  , lstair = ([],[])-  , lescape = []-  , lseen = 0-  , lexpl = 0-  , ltime = timeZero-  , lnight = False-  }+  (executorCli, stubCliState, testActor, testLevel, testLevelId)  commonMUnitTests :: TestTree commonMUnitTests = testGroup "commonMUnitTests"
test/HandleHumanLocalMUnitTests.hs view
@@ -5,15 +5,22 @@ import Game.LambdaHack.Core.Prelude  import qualified Data.EnumMap.Strict as EM-import           Test.Tasty-import           Test.Tasty.HUnit+import qualified Data.Text as T +import Test.Tasty+import Test.Tasty.HUnit++import           Game.LambdaHack.Client.UI (SessionUI (..), modifySession) import           Game.LambdaHack.Client.UI.HandleHelperM import           Game.LambdaHack.Client.UI.HandleHumanLocalM import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd+import           Game.LambdaHack.Client.UI.Msg+import           Game.LambdaHack.Client.UI.MsgM+import           Game.LambdaHack.Client.UI.TutorialHints import           Game.LambdaHack.Common.ActorState import           Game.LambdaHack.Common.Item import           Game.LambdaHack.Common.ItemAspect+import           Game.LambdaHack.Common.Kind (emptyMultiGroupItem) import           Game.LambdaHack.Common.Level import           Game.LambdaHack.Common.Point import           Game.LambdaHack.Common.PointArray as PointArray@@ -22,15 +29,11 @@ import           Game.LambdaHack.Content.TileKind import           Game.LambdaHack.Definition.DefsInternal   (toContentId, toContentSymbol)-import           Game.LambdaHack.Definition.Flavour  import UnitTestHelpers -stubItem :: Item-stubItem = Item { jkind = IdentityObvious (toContentId 0), jfid = Nothing, jflavour = dummyFlavour }- testItemFull :: ItemFull-testItemFull = ItemFull { itemBase = stubItem, itemKindId = toContentId 0, itemKind = testItemKind, itemDisco = ItemDiscoFull emptyAspectRecord, itemSuspect = False }+testItemFull = ItemFull { itemBase = stubItem, itemKindId = toContentId 0, itemKind = emptyMultiGroupItem, itemDisco = ItemDiscoFull emptyAspectRecord, itemSuspect = False }  handleHumanLocalMUnitTests :: TestTree handleHumanLocalMUnitTests = testGroup "handleHumanLocalMUnitTests"@@ -54,6 +57,25 @@                    in chooseItemProjectHuman testActorId triggerItems       result <- executorCli testFn testCliStateWithItem       showFailError (fromJust (fst result)) @?= "*aiming obstructed by terrain*"+  , testCase "tutorialHints-msg-in-history-report" $ do+      let testFn = do+            modifySession (\sess -> sess {scurTutorial = True})+              -- permit the client not to ignore tutorial hints+            tutorialHintMsgAdd CannotHarmYouInMelee+      result <- executorCli testFn testCliStateWithItem+      let maybeHistory = shistory <$> (cliSession . snd) result+      case maybeHistory of+        Nothing -> assertFailure "History is empty"+        Just history -> assertBool testFailureMsg isHintThere+         where+          renderedNewReports = reportToTexts . newReport $ history+          renderedHint = renderTutorialHints CannotHarmYouInMelee+          isHintThere = renderedHint `elem` renderedNewReports+          testFailureMsg = "Expected to find tutorial hint '"+            <> (T.unpack . renderTutorialHints $ CannotHarmYouInMelee)+            <> "' in SessionUI.shistory.newReport '"+            <> T.unpack (T.unlines renderedNewReports)+            <> "'"   , testCase "psuitReq" $  do       let testFn = psuitReq testActorId       mpsuitReqMonad <- executorCli testFn testCliStateWithItem
test/ItemDescriptionUnitTests.hs view
@@ -8,58 +8,41 @@ import           Test.Tasty import           Test.Tasty.HUnit -import           Game.LambdaHack.Client.UI.ItemDescription-import           Game.LambdaHack.Common.Item-import           Game.LambdaHack.Common.ItemAspect-import           Game.LambdaHack.Content.ItemKind-import           Game.LambdaHack.Core.Dice-import qualified Game.LambdaHack.Definition.Ability as Ability-import           Game.LambdaHack.Definition.Color-import           Game.LambdaHack.Definition.Flavour+import Game.LambdaHack.Client.UI.ItemDescription+  (viewItem, viewItemBenefitColored)+import Game.LambdaHack.Common.Item+import Game.LambdaHack.Common.ItemAspect (emptyAspectRecord)+import Game.LambdaHack.Common.Kind (emptyMultiGroupItem)+import Game.LambdaHack.Content.ItemKind (ItemKind (isymbol))+import Game.LambdaHack.Definition.Color+  (Color (BrGreen, BrRed, Green), attrChar2ToW32)+import Game.LambdaHack.Definition.Flavour (zipPlain)+import UnitTestHelpers (stubItem)  itemDescriptionUnitTests :: TestTree itemDescriptionUnitTests = testGroup "itemDescriptionUnitTests" $-  let greenFlavour = head $ zipPlain [Green]-      testItemBase = Item-        { jkind = IdentityObvious (toEnum 667)-        , jfid = Nothing-        , jflavour = greenFlavour-        }-      testItemKind = ItemKind-        { isymbol  = 'x'-        , iname    = "12345678901234567890123"-        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]-        , iflavour = zipPlain [Green]-        , icount   = 1 + 1 `d` 2-        , irarity  = [(1, 50), (10, 1)]-        , iverbHit = "hit"-        , iweight  = 300-        , idamage  = 1 `d` 1-        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5-                     , SetFlag Ability.Fragile-                     , toVelocity 70 ]-        , ieffects = []-        , idesc    = "A really cool test item."-        , ikit     = []-        }-      testItemFull = ItemFull-        { itemBase = testItemBase+  let testItemFull = ItemFull+        { itemBase = stubItem+          {+            jkind = IdentityObvious (toEnum 667)+          , jflavour = head $ zipPlain [Green]+          }         , itemKindId = toEnum 667-        , itemKind = testItemKind+        , itemKind = emptyMultiGroupItem         , itemDisco = ItemDiscoFull emptyAspectRecord         , itemSuspect = True         }   in   [ testCase "testItem_viewItem_Blackx" $-      viewItem testItemFull+      viewItem testItemFull {itemKind = emptyMultiGroupItem {isymbol = 'x'}}       @?= attrChar2ToW32 Green 'x'   , testCase "testItem_viewItem_Black!" $-      viewItem testItemFull {itemKind = testItemKind {isymbol = '!'}}+      viewItem testItemFull {itemKind = emptyMultiGroupItem {isymbol = '!'}}       @?= attrChar2ToW32 Green '!'   , testCase "testItem_viewItemBenefitColored_isEquip_Greenx" $-      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit True 0 0 0 0)) (toEnum 42) testItemFull+      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit True 0 0 0 0)) (toEnum 42) testItemFull {itemKind = emptyMultiGroupItem {isymbol = 'x'}}       @?= attrChar2ToW32 BrGreen 'x'   , testCase "testItem_viewItemBenefitColored_isNotEquip_Redx" $-      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit False 0 0 0 0)) (toEnum 42) testItemFull+      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit False 0 0 0 0)) (toEnum 42) testItemFull {itemKind = emptyMultiGroupItem {isymbol = 'x'}}       @?= attrChar2ToW32 BrRed 'x'   ]
test/ItemKindUnitTests.hs view
@@ -7,72 +7,53 @@ import Test.Tasty import Test.Tasty.HUnit +import qualified Content.RuleKind+import           Game.LambdaHack.Common.Kind (emptyMultiGroupItem) import           Game.LambdaHack.Content.ItemKind import qualified Game.LambdaHack.Content.RuleKind as RK-import           Game.LambdaHack.Core.Dice import qualified Game.LambdaHack.Definition.Ability as Ability-import           Game.LambdaHack.Definition.Color-import           Game.LambdaHack.Definition.Flavour -import qualified Content.RuleKind- itemKindUnitTests :: TestTree itemKindUnitTests = testGroup "itemKindUnitTests" $   let standardSymbols = RK.ritemSymbols Content.RuleKind.standardRules       customSymbols = emptyItemSymbolsUsedInEngine {rsymbolNecklace = '*'}-      testItemKind = ItemKind-        { isymbol  = 'x'-        , iname    = "12345678901234567890123"-        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]-        , iflavour = zipPlain [Green]-        , icount   = 1 + 1 `d` 2-        , irarity  = [(1, 50), (10, 1)]-        , iverbHit = "hit"-        , iweight  = 300-        , idamage  = 1 `d` 1-        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5-                     , SetFlag Ability.Fragile-                     , toVelocity 70 ]-        , ieffects = []-        , idesc    = "A really cool test item."-        , ikit     = []-        }+   in   [ testCase "overlonginame_validateSingle_errs" $       validateSingle standardSymbols-                     testItemKind { iname = "123456789012345678901234" }+                     emptyMultiGroupItem { iname = "123456789012345678901234" }       @?= ["iname longer than 23"]   , testCase "shortEnoughiname_validateSingle_noErr" $       validateSingle standardSymbols-                     testItemKind+                     emptyMultiGroupItem       @?= []   , testCase "equipableNoSlotxSymbol_validateSingle_errs" $       validateSingle standardSymbols-                     testItemKind { iaspects = [ SetFlag Ability.Equipable ] }+                     emptyMultiGroupItem { iaspects = [ SetFlag Ability.Equipable ] }       @?= ["EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template"]   , testCase "equipableNoSlot,Symbol_validateSingle_noErr" $       validateSingle standardSymbols-                     testItemKind { isymbol = ','+                     emptyMultiGroupItem { isymbol = ','                                   , iaspects = [ SetFlag Ability.Equipable ] }       @?= []   , testCase "equipableNoSlot\"Symbol_validateSingle_noErr" $       validateSingle standardSymbols-                     testItemKind { isymbol = '"'+                     emptyMultiGroupItem { isymbol = '"'                                   , iaspects = [ SetFlag Ability.Equipable ] }       @?= []   , testCase "equipableNoSlot/Symbol_validateSingle_noErr" $       validateSingle standardSymbols-                     testItemKind { isymbol = '/'+                     emptyMultiGroupItem { isymbol = '/'                                   , iaspects = [ SetFlag Ability.Equipable ] }       @?= []   , testCase "equipableNoSlot*CustomRules_validateSingle_noErr" $       validateSingle customSymbols-                     testItemKind { isymbol = '*'+                     emptyMultiGroupItem { isymbol = '*'                                   , iaspects = [ SetFlag Ability.Equipable ] }       @?= []   , testCase "equipableNoSlot\"CustomRules_validateSingle_errs" $       validateSingle customSymbols-                     testItemKind { isymbol = '"'+                     emptyMultiGroupItem { isymbol = '"'                                   , iaspects = [ SetFlag Ability.Equipable ] }       @?= ["EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template"]   ]
test/ItemRevUnitTests.hs view
@@ -12,35 +12,17 @@ import           Test.Tasty import           Test.Tasty.HUnit -import           Game.LambdaHack.Content.ItemKind-import           Game.LambdaHack.Core.Dice-import           Game.LambdaHack.Core.Random-import qualified Game.LambdaHack.Definition.Ability as Ability-import           Game.LambdaHack.Definition.Color-import           Game.LambdaHack.Definition.DefsInternal-import           Game.LambdaHack.Definition.Flavour-import           Game.LambdaHack.Server.ItemRev+import Game.LambdaHack.Common.Kind (emptyMultiGroupItem)+import Game.LambdaHack.Content.ItemKind+import Game.LambdaHack.Core.Random+import Game.LambdaHack.Definition.Color+import Game.LambdaHack.Definition.DefsInternal+import Game.LambdaHack.Definition.Flavour+import Game.LambdaHack.Server.ItemRev  itemRevUnitTests :: TestTree itemRevUnitTests = testGroup "itemRevUnitTests" $-  let testItemKind = ItemKind-        { isymbol  = 'x'-        , iname    = "12345678901234567890123"-        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]-        , iflavour = zipStory [Black]-        , icount   = 1 + 1 `d` 2-        , irarity  = [(1, 50), (10, 1)]-        , iverbHit = "hit"-        , iweight  = 300-        , idamage  = 1 `d` 1-        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5-                     , SetFlag Ability.Fragile-                     , toVelocity 70 ]-        , ieffects = []-        , idesc    = "A really cool test item."-        , ikit     = []-        }-      testItemKind2Flavours = testItemKind+  let testItemKind2Flavours = emptyMultiGroupItem         { iflavour = zipStory [Black,Green] }       emptyIdToFlavourSymbolToFlavourSetPair = ( EM.empty, EM.empty )       singletonIdToFlavourSymbolToFlavourSetPair =@@ -51,19 +33,19 @@   in   [ testCase "empty & default initializers -> first is single dummy result" $       let rndMapPair0 = return emptyIdToFlavourSymbolToFlavourSetPair-          mapPair1 = St.evalState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1+          mapPair1 = St.evalState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) emptyMultiGroupItem) $ SM.mkSMGen 1         in fst mapPair1 @?= EM.singleton (toContentId 0) dummyFlavour   , testCase "empty & default initializers -> second is empty" $       let rndMapPair0 = return emptyIdToFlavourSymbolToFlavourSetPair-          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) emptyMultiGroupItem) $ SM.mkSMGen 1         in snd mapPair1 @?= EM.empty   , testCase "singleton initializers -> first is single dummy result" $       let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair-          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) emptyMultiGroupItem) $ SM.mkSMGen 1         in fst mapPair1 @?= EM.singleton (toContentId 0) dummyFlavour   , testCase "singleton initializers -> second is single dummy result" $       let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair-          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) emptyMultiGroupItem) $ SM.mkSMGen 1         in snd mapPair1 @?= EM.singleton 'x' (ES.singleton dummyFlavour)   , testCase "rollFlavourMap on two flavours -> first flavour can be rolled" $  -- relies on us not messing with RNG       let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair
test/LevelUnitTests.hs view
@@ -8,35 +8,10 @@ import           Test.Tasty import           Test.Tasty.HUnit -import           Game.LambdaHack.Common.Area-import           Game.LambdaHack.Common.Level-import           Game.LambdaHack.Common.Point-import           Game.LambdaHack.Common.PointArray as PointArray-import           Game.LambdaHack.Common.State-import           Game.LambdaHack.Common.Time-import qualified Game.LambdaHack.Core.Dice as Dice--import UnitTestHelpers+import Game.LambdaHack.Common.Level+import Game.LambdaHack.Common.State -testLevel :: Level-testLevel = Level-  { lkind = toEnum 0-  , ldepth = Dice.AbsDepth 1-  , lfloor = EM.empty-  , lembed = EM.empty-  , lbig = EM.empty-  , lproj = EM.empty-  , ltile = PointArray.empty-  , lentry = EM.empty-  , larea = trivialArea (Point 0 0)-  , lsmell = EM.empty-  , lstair = ([],[])-  , lescape = []-  , lseen = 0-  , lexpl = 0-  , ltime = timeZero-  , lnight = False-  }+import UnitTestHelpers (testLevel, testLevelId)  testDungeonWithLevel :: State testDungeonWithLevel =
test/ReqFailureUnitTests.hs view
@@ -7,46 +7,22 @@ import Test.Tasty import Test.Tasty.HUnit +import qualified Content.RuleKind import           Game.LambdaHack.Common.Item import           Game.LambdaHack.Common.ItemAspect+import           Game.LambdaHack.Common.Kind (emptyMultiGroupItem) import           Game.LambdaHack.Common.ReqFailure import           Game.LambdaHack.Common.Time import           Game.LambdaHack.Content.ItemKind-import           Game.LambdaHack.Core.Dice-import qualified Game.LambdaHack.Definition.Ability as Ability-import           Game.LambdaHack.Definition.Color import           Game.LambdaHack.Definition.Defs-import           Game.LambdaHack.Definition.Flavour--import qualified Content.RuleKind+import           UnitTestHelpers (stubItem)  reqFailureUnitTests :: TestTree reqFailureUnitTests = testGroup "reqFailureUnitTests" $-  let testItemKind = ItemKind-        { isymbol  = 'x'-        , iname    = "12345678901234567890123"-        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]-        , iflavour = zipPlain [Green]-        , icount   = 1 + 1 `d` 2-        , irarity  = [(1, 50), (10, 1)]-        , iverbHit = "hit"-        , iweight  = 300-        , idamage  = 1 `d` 1-        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5-                     , SetFlag Ability.Fragile-                     , toVelocity 70 ]-        , ieffects = []-        , idesc    = "A really cool test item."-        , ikit     = []-        }-      testItemFull = ItemFull-        { itemBase = Item-            { jkind = IdentityObvious (toEnum 667)-            , jfid = Nothing-            , jflavour = dummyFlavour-            }+  let testItemFull = ItemFull+        { itemBase = stubItem -- Item { jkind = IdentityObvious (toEnum 667) , jfid = Nothing , jflavour = dummyFlavour}         , itemKindId = toEnum 667-        , itemKind = testItemKind+        , itemKind = emptyMultiGroupItem         , itemDisco = ItemDiscoFull emptyAspectRecord         , itemSuspect = True         }@@ -58,22 +34,22 @@       @?= Left ApplyFood   , testCase "permittedApply: One Skill and , symbol And CGround -> True" $       permittedApply standardRules timeZero 1 True (Just CGround)-                     testItemFull {itemKind = testItemKind{isymbol = ','}}+                     testItemFull {itemKind = emptyMultiGroupItem{isymbol = ','}}                      quantSingle       @?= Right True   , testCase "permittedApply: One Skill and \" symbol -> True" $       permittedApply standardRules timeZero 1 True Nothing-                     testItemFull {itemKind = testItemKind{isymbol = '"'}}+                     testItemFull {itemKind = emptyMultiGroupItem{isymbol = '"'}}                      quantSingle       @?= Right True   , testCase "permittedApply: Two Skill and ? symbol -> FailureApplyRead" $       permittedApply standardRules timeZero 2 True Nothing-                     testItemFull {itemKind = testItemKind{isymbol = '?'}}+                     testItemFull {itemKind = emptyMultiGroupItem{isymbol = '?'}}                      quantSingle       @?= Left ApplyRead   , testCase "permittedApply: Two Skill and , symbol -> True" $       permittedApply standardRules timeZero 2 True Nothing-                     testItemFull {itemKind = testItemKind{isymbol = ','}}+                     testItemFull {itemKind = emptyMultiGroupItem{isymbol = ','}}                      quantSingle       @?= Right True   ]
test/SessionUIMock.hs view
@@ -53,8 +53,8 @@           in sess { smacroFrame = smacroFrameNew                   , smacroStack = smacroStackMew }   case abortOrCmd of-    Left Looped -> tell [(abuffs, "Macro looped")] >> pure ()-    Left HeadEmpty -> tell [(abuffs, "")] >> pure ()  -- exit loop+    Left Looped -> void $ tell [(abuffs, "Macro looped")]+    Left HeadEmpty -> void $ tell [(abuffs, "")]  -- exit loop     Right Nothing -> tell [(abuffs, "")] >> humanCommandMock     Right (Just out) -> tell [(abuffs, show out)] >> humanCommandMock 
test/Spec.hs view
@@ -66,7 +66,7 @@       uiOptions = unsafePerformIO $ mkUIOptions corule defClientOptions       testFontset :: Int -> String -> TestTree       testFontset n fontsetName =-        testCase ("SDL fronted; init only; " ++ fontsetName ++ " fontset") $ do+        testCase ("SDL frontend; init only; " ++ fontsetName ++ " fontset") $ do           -- This test only works when run from the same directory that           -- the .cabal file is in. And this is what Debian needs, so OK.           -- The hacky log priority 0 tells SDL frontend to init
test/UnitTestHelpers.hs view
@@ -4,21 +4,23 @@   ( CliState(..)   , emptyCliState   , executorCli+  , reportToTexts   , stubLevel   , stubState   , stubCliState+  , stubItem   , testActor   , testActorId   , testActorWithItem   , testCliStateWithItem   , testFactionId   , testItemId-  , testItemKind+  , testLevel   , testLevelId #ifdef EXPOSE_INTERNAL     -- * Internal operations-  , CliMock(..)   , fchanFrontendStub+  , CliMock(..) #endif   ) where @@ -30,6 +32,7 @@ import           Control.Monad.Trans.State.Strict   (StateT (StateT, runStateT), gets, state) import qualified Data.EnumMap.Strict as EM+import qualified Data.Text as Text  import           Game.LambdaHack.Atomic (MonadStateWrite (..)) import           Game.LambdaHack.Client@@ -37,34 +40,53 @@ import           Game.LambdaHack.Client.HandleResponseM import           Game.LambdaHack.Client.MonadClient import           Game.LambdaHack.Client.State+  (StateClient (..), TGoal (..), Target (..), emptyStateClient, updateLeader) import           Game.LambdaHack.Client.UI-import           Game.LambdaHack.Client.UI.ActorUI+  (MonadClientUI (..), SessionUI (..), emptySessionUI)+import           Game.LambdaHack.Client.UI.ActorUI (ActorUI (..)) import           Game.LambdaHack.Client.UI.Content.Screen-import           Game.LambdaHack.Client.UI.ContentClientUI+  (emptyScreenContent, rheight, rwidth)+import           Game.LambdaHack.Client.UI.ContentClientUI (coscreen, emptyCCUI) import           Game.LambdaHack.Client.UI.Frontend+  (ChanFrontend (..), FrontReq (..)) import           Game.LambdaHack.Client.UI.Key (KMP (..)) import qualified Game.LambdaHack.Client.UI.Key as K-import           Game.LambdaHack.Client.UI.PointUI-import           Game.LambdaHack.Client.UI.UIOptions+import           Game.LambdaHack.Client.UI.Msg+import           Game.LambdaHack.Client.UI.Overlay+import           Game.LambdaHack.Client.UI.PointUI (PointUI (..))+import           Game.LambdaHack.Client.UI.UIOptions (UIOptions (..)) import           Game.LambdaHack.Common.Actor-import           Game.LambdaHack.Common.Area+  (Actor (..), ResDelta (..), Watchfulness (..))+import           Game.LambdaHack.Common.Area (Area, toArea, trivialArea) import           Game.LambdaHack.Common.ClientOptions-import           Game.LambdaHack.Common.Faction-import           Game.LambdaHack.Common.Kind-import           Game.LambdaHack.Common.Level-import           Game.LambdaHack.Common.Misc+  (ClientOptions (..), FullscreenMode (..), defClientOptions)+import           Game.LambdaHack.Common.Faction (Faction (..))+import           Game.LambdaHack.Common.Item+import           Game.LambdaHack.Common.Kind (COps (..), emptyUIFaction)+import           Game.LambdaHack.Common.Level (Level (..))+import           Game.LambdaHack.Common.Misc (FontSet (..)) import           Game.LambdaHack.Common.MonadStateRead-import           Game.LambdaHack.Common.Perception-import           Game.LambdaHack.Common.Point+import           Game.LambdaHack.Common.Perception (emptyPer)+import           Game.LambdaHack.Common.Point (Point (..)) import           Game.LambdaHack.Common.State-import           Game.LambdaHack.Common.Time+  ( State+  , emptyState+  , unknownTileMap+  , updateActorD+  , updateActorMaxSkills+  , updateCOpsAndCachedData+  , updateDungeon+  , updateFactionD+  )+import           Game.LambdaHack.Common.Time (timeZero) import           Game.LambdaHack.Common.Types-import           Game.LambdaHack.Content.ItemKind-import           Game.LambdaHack.Content.RuleKind-import           Game.LambdaHack.Content.TileKind+  (ActorId, FactionId, ItemId, LevelId)+import           Game.LambdaHack.Content.RuleKind (RuleContent (..))+import           Game.LambdaHack.Content.TileKind (unknownId) import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability-import           Game.LambdaHack.Definition.Color+import           Game.LambdaHack.Definition.Color (Color (..))+import           Game.LambdaHack.Definition.DefsInternal (toContentId) import           Game.LambdaHack.Definition.Flavour  -- * UI frontend stub@@ -128,6 +150,29 @@                           , fontMono = "mono" })]   } +stubItem :: Item+stubItem = Item { jkind = IdentityObvious (toContentId 0), jfid = Nothing, jflavour = dummyFlavour }++testLevel :: Level+testLevel = Level+  { lkind = toEnum 0+  , ldepth = Dice.AbsDepth 1+  , lfloor = EM.empty+  , lembed = EM.empty+  , lbig = EM.empty+  , lproj = EM.empty+  , ltile = unknownTileMap (fromJust (toArea (0,0,0,0))) unknownId 10 10  --PointArray.empty+  , lentry = EM.empty+  , larea = trivialArea (Point 0 0)+  , lsmell = EM.empty+  , lstair = ([],[])+  , lescape = []+  , lseen = 0+  , lexpl = 0+  , ltime = timeZero+  , lnight = False+  }+ -- * Stub identifiers  -- Using different arbitrary numbers for these so that if tests fail@@ -171,7 +216,6 @@   , ltime = timeZero   , lnight = False   }- testFaction :: Faction testFaction =   Faction@@ -209,25 +253,6 @@   , bproj = False   } -testItemKind :: ItemKind-testItemKind = ItemKind-  { isymbol  = 'x'-  , iname    = "12345678901234567890123"-  , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]-  , iflavour = zipPlain [Green]-  , icount   = 1 + 1 `Dice.d` 2-  , irarity  = [(1, 50), (10, 1)]-  , iverbHit = "hit"-  , iweight  = 300-  , idamage  = 1 `Dice.d` 1-  , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5-                , SetFlag Ability.Fragile-                , toVelocity 70 ]-  , ieffects = []-  , idesc    = "A really cool test item."-  , ikit     = []-  }- testActorWithItem :: Actor testActorWithItem =   testActor { beqp = EM.singleton testItemId (1,[])}@@ -293,6 +318,7 @@ testCliStateWithItem :: CliState testCliStateWithItem = stubCliState { cliState = testStateWithItem } + -- * Monad harness mock  -- | Client state transformation monad mock.@@ -328,13 +354,6 @@     let !newCliS = cliS {cliClient = f $ cliClient cliS}     in ((), newCliS) --- instance MonadClientSetup CliMock where---   saveClient = CliMock $ do---     --toSave <- gets cliToSave---     cli <- gets cliClient---     msess <- gets cliSession---     IO.liftIO $ Save.saveToChan toSave (cli, msess)- instance MonadClientUI CliMock where   {-# INLINE getsSession #-}   getsSession f = CliMock $ gets $ f . fromJust . cliSession@@ -349,22 +368,6 @@   getCacheBfs = BfsM.getCacheBfs   getCachePath = BfsM.getCachePath --- instance MonadClientReadResponse CliMock where---   receiveResponse = CliMock $ do---     ChanServer{responseS} <- gets cliDict---     IO.liftIO $ takeMVar responseS---- instance MonadClientWriteRequest CliMock where---   sendRequestAI scmd = CliMock $ do---     ChanServer{requestAIS} <- gets cliDict---     IO.liftIO $ putMVar requestAIS scmd---   sendRequestUI scmd = CliMock $ do---     ChanServer{requestUIS} <- gets cliDict---     IO.liftIO $ putMVar (fromJust requestUIS) scmd---   clientHasUI = CliMock $ do---     mSession <- gets cliSession---     return $! isJust mSession- instance MonadClientAtomic CliMock where   {-# INLINE execUpdAtomic #-}   execUpdAtomic _ = return ()  -- handleUpdAtomic, until needed, save resources@@ -374,3 +377,8 @@  executorCli :: CliMock a -> CliState -> IO (a, CliState) executorCli = runStateT . runCliMock+++-- | Transform 'Report' type to a list of 'Text'.+reportToTexts :: Report -> [Text.Text]+reportToTexts report = Text.pack . attrStringToString <$> renderReport False report