LambdaHack-0.11.0.0/0000755000000000000000000000000007346545000012067 5ustar0000000000000000LambdaHack-0.11.0.0/CHANGELOG.md0000644000000000000000000033544607346545000013717 0ustar0000000000000000## [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 - Deduplicate UI code for exiting game, with extra style points - Create monadic test harness and use it for UI and other unit tests - Validate empty content and fix other soundness issues revealed by unit tests - Add extra hints in --more and similar lines when tutorial is on - Show full history at SPACE press and let second SPACE close it - Spawn insects in the swamp - Remove slot letter display in menus and instead display subtle bullets - Repurpose item slots to item roles - Redo display and control of main menu and its submenus - Make Teletype frontend a bit closer to playable - Switch right pane item description display from mono to prop font - Ensure score not zero if victory - Add a custom SDL cursor, working around a bug in SDL2 bindings - Gut out most content symbols; weren't used even in lore menus after all - Add a flag to disable the costly optimizations (that give 15-25% speedup) - Add faction kind content and redo game mode content to use it - Display seen faction lore - Simplify and fortify faction and client assignment code - Make a few unique items that were identified meta-game identifiable instead - Fix persistence of meta-game discoveries in save files - When assigning a faction, first try the group actor was picked from - When assigned faction is dead, don't spawn the actor - Don't use benign weapons on projectiles not to lose the fun - Make the unique harpoon worth saving for a unique foe - Spawn enemies closer and fix too random spawning location - Improve display of item's range - Warn when SDL game windows is resized not via config file - Ban or force sleep on levels - Protect against unset or primitive OS locale - Add temporary hearing aids - Disallow generating a door beside an opening in room's wall - Permit smaller caves and validate cave content more accurately - Try harder to generate escape from dungeon in a level corner - Avoid exit/escape confusion in content names and descriptions - Extend LH game mode a descriptions since people play it instead of Allure - Make sure every cave has a description - Many fixes, refactorings and tweaks ## [v0.10.3.0](https://github.com/LambdaHack/LambdaHack/compare/v0.10.2.0...v0.10.3.0) - Work around regression https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076 by making the scalable square font the default as the map font; the tiny map fonts, for which there is no such workaround, won't work for anybody with freetype 2.11.0 - Enable display of details in right pane in many menus - Switch mouse wheel to move selection now that it changes right pane display - Make the line where messages wrap configurable in config file - Remove rarely used options from config file and code - Prepare client-server for delays when operating over the network - Reveal all map at game over and make it explorable - Don't run with -threaded, increasing speed by a couple dozen percent - Don't show messages during enemy turn (except when under AI control) - Spawn many actors at once with probability correlated to spawning at all - Gut out Ubuntu Font Family fonts - Default to more old-fashioned small fonts in LambdaHack the game - Add a few tutorial messages, reword some in-game texts - Move with CI from Travis to GitHub Actions (but GHA can't keep up as well) - Add hlint to CI - Make place content directly define legends instead of overriding them - Add a warning that crosshair is out of flinging range - Add ANSI display to SDL2 frontend; remove vty, hcurses and GTK frontends - Make Teletype frontend more usable (input, speed, overlays) - Remove sight malus from light sources to avoid micromanagement - Keep convenience settings between new games - Add an option to mark FOV area with grey background - Refactor and clean up a lot of the code - Unhardwire various constants (hat tip to Jamie) - Fix a couple of bugs and improve documentation - Add whole new kinds of tests (even greater hat tip to Jamie) ## [v0.10.2.0](https://github.com/LambdaHack/LambdaHack/compare/v0.9.5.0...v0.10.2.0) - Let '?' scroll help - Handle Alt and Shift modifiers together - Advertise the C- Tab command instead of A- - Ignore empty lines in history, just in case - Don't keep trailing endlines in history - Don't include placeholders in history.txt - Retry explosions 10 instead of 100 times - Point crosshair to a modified tile to easily get lore info - Point crosshair to a bumped tile to easily get lore info - Don't spam when reporting that crafting is forbidden - Prevent AI from attempting to fling when challenge forbids it - Hint about absolute paths to fonts - Disable testing ubuntu fonts, because they can't be distributed in Debian - If any savefile corrupted, move aside all - Be more consistent about moving corrupted files aside - Stop running when passing a door, etc. - Make the game over lore headers sound more natual - Do not mention embeds in minimal detail - Tweak detail in one step to minimal - Don't let distant taunts interrupt running - Add a comment about fleeing from stash - Help AI not to leave a scout guarding the stash - Ensure some gems in escape scenario - Don't show embed descriptions by default now that ~ does that easily - Add one more typical organ symbol - Avoid cut off descriptions of conditional effects - Prefer weapons with burn or wound - Pick up bad weapons if decent projectiles - Slightly prefer weapons that have any effects - Permit cycling through pointed-at lore with tilde - Split chooseItemDialogMode into several functions - Permit pointed-at lore display also outside aiming mode - Reset the pointed at lore display sequence - Maintain current state of lore display - Save current state of being-aimed-at lore display - Don't use timeout weapon if non-timeout as strong - Add delay to pushed frames, to notice things shorter than a turn - Pretty-print activationFlag to make it more understandable - Replace the special periodic activation handling with effect conditions - Replace the special effect handling under ranged with effect condition - Set ActivationOnCombine in one case for now - Replace EffOnSmash by ActivationOnSmash - Add effect condition based on activation method - Add Unless effect - Introduce ActivationFlag in place of EffActivation type - Make server check that melee is done with a legal weapon - Add a failure about meleeing with not a weapon - Get rid of a lot of spurious instances - Don't say a hit was amusing if non-piercing damage was high - Vary also the block messages when high armor - Use 'a' and 'the' in dangerous item use confirmations - Prevent safe spots near centres of big explosions - Permit explosions to sometimes go off inside unwalkable terrain - Don't sleep if the only friend on level is guarding the stash - Shorten mreason text for crawl to fit in main menu with square fonts - Don't hint to press '?' when it does nothing yet - Avoid empty manual page at the end with square font - Make sure spot crosshair description fits on the status line - Don't display the aim mode prefix when inside a menu - Experiment with item menu not insisting on selecting an item - Talk about the relevant lore command in manual - Hint about the relevant lore command - Make lore display context sensitive in aiming mode - Signal that lore command is sometimes context-sensitive - Don't add the interruption message unless useful - Don't wipe key status that opens main menu after automation - Permit random flavour assignment with brightCol, etc. - Don't make detection modal if not performed by player's faction - Don't describe the goal tile when performing mouse goto - Simplify turn display code, removing most of the newest additions - Remove another spurious source of frames - Remove spurious sources of frames - Re-add refined *interrupted* via messages - Ensure all frames when inside macro have one-line report only - Display *interrupted* also when messages interrupt macros - Factor out oneLineBasicFrame - Declare the codebase lens-free - Avoid distributing test.exe in Windows packages - Use squareToUI for mouse handling in the browser - Introduce game map coordinates: PointSquare - Update wrt PointUI transition in other frontends - Define UI screen coords in a separate file - Silence messages about dead actors activating most embeds - Make it possible to prevent the sleep effect - Remove savefiles if config file too old - Force savegame load result to catch errors ASAP - Wait until browser finalizes Local Storage in the background - Throw a Haskel exception when JS decompression fails on an old savefile - Now that browser compresses savefiles, permit full history size - Limit DOM keyboard focus highjacking, etc. - Tell player when browser can be closed - Use lz-string.js externs for minification - Compress savefiles in GHCJS - Bump base-compat bound to include Data.Semigroup compat - Simplify containers for explosions - Start embedded explosions inside the tile, not inside projectile - Exclude meta game items from combat messages - Move snxtScenario to session - Avoid 'you are less more tidy' - Don't count valuables inside organs to dungeon total - Clarify that normal damage is piercing, as opposed to blunt wounding - Change the misleading 'guards a hoard' - When dropped, destroy any organs, not only conditions - Don't hint to press ? when it would skip some messages - Explain why the fadeout report is rendered in square font - Show each tutorial hint at most once per game - Require keypress when another faction killed off - Don't show question marks in HUD for MinorAspects items - Add MinorAspects flag - Get rid of the mechanism for referring expressions across messages - Commute freely messages about conditions lasting longer - Add NopEffect - Add AtMostOneOf effect constructor - Assign stable faction IDs to teams with continuity - Display also actor initial items, not only organs - Use Condition flag, not group, for speed and simplicity - Keep paragraphs of the same width, unless the first really short - Add the 'stomachs' irregular plural - Don't identify item if PutToSleep applied to sleeping actor - Permit question marks in verb effects - Do not announce item trigger if explicit and so already told - Do not report conditions triggered when they vanish - Inform differently when sleep induced by own organ, etc. - Reflect that item triggering message appears after the fact - Do not spam when backstory can't trigger yet - Give more details in logs when item triggering does not work - Do not spam about water being activated - Prevent server failure when triggering a gold piece - Log item application even from, e.g., periodic activation - Don't announce that a dying actors stopped being hungry - Add a temporary hack that prevents AI from stealing backstories in Allure - Hardwire the player team continuity token - Don't identify meta game items at game end - Implement conditional effects - At death, destroy all organs, not just the trunk - Permit non-unique non-singleton flavours - Render story flavours specially - Announce identification even during melee - Inform about discovering meta game organs - Make sure heroes spawned in safari mode are properly numbered - Add a few more hero names - Improve the descritpion of MetaGame flag - Record meta game gear for future games - Consume current, not persistent team gear - Create gear according to character preferences, if any - Use server-assigned actor numbers in the client - Assign index numbers to actors from continued teams - Add continued team character counter - Generalize hero gear to each team with continuity - Store team continuity in a faction - Add team continuity across scenarios to content - Add persistent sheroGear to server state - Add a comment about why aspects of meta game items are not preserved - Keep flavour of MetaGame items between games - Keep identity of MetaGame items between games - Simplify and speed up serverDiscos - Add a flag to keep item identified between games - Make sure pushed actors never hit before flying away - Use lookupActorTime instead of manually inlining - Only activate the first effect with UnderRanged and UnderMelee - Simplify deflection descriptions given they don't stack in practice, after all - Describe the new challenges in the manual - Implement disabled flinging challenge - Implement disabled crafting challenge - Add two more challenges - Display tutorial hint only for bad temporary conditions - Let AI use items with -1 speed, e.g., shields - Improve valuing of SkDeflectRanged and SkDeflectMelee - Try not to recompile dependencies between cabal-plan and LambdaHack - Try to work around packages not updated for GHC 9.0.1 - Add deflection temporary conditions - Add GHC 9.0.1 to travis and .cabal - Implement skills for ranged and melee invulnerability - Add skills for ranged and melee invulnerability - Implement flags for applying when under attack - Add flags for auto-activation when under attack - Let AI with low skill target suspect terrain - Prevent AI from trying to trigger blocked tiles - Explain why we crop surface, not texture, even for prop font - Explain the pointman/leader discrepancy - Gut out the last remain of 'cabal list-bin' - Add some list-bin debug to travis script - Explain away a text rendering artifact - Make sure speaking unique messages are not ignored - Don't interrupt for taunt messages - Move the tutorial hint about hearing taunts to the correct place - Add tutorial hints about taking damage - Only save to log any interruptible messages while in melee - Add some more tutorial hint messages - Write key names as on the keyboard, button names as in the button - When leader stands over an item, say so, not 'notices item' - Unify pluralization of skills and challenges - Add some tutorial hint messages - Add the first tutorial hint - Start with tutorial hints, but not in screensaver mode - Ignore tutorial hints, as needed - Give a more informative message when detecting that AI stuck - Move tutorial hint options from state to session because they are UI-only - Keep the tutorial override option from game to game - Implement the convenience option to override tutorial hint display - Add a convenience option to override tutorial hint display - Set tutorial messages according to game mode - Toggle tutorial messages when starting a game - Mark some game modes as tutorials - Display manual at the end of help - Keep the rest of the manual in content - Split manual into paragraphs aware of some markdown formatting - Simplify an address in manual not to spill over 80 columns - Make duplicate errors more informative (fixes #204) - Factor out movementDefinitions - Hint in config file to disable the special movement keys - Hint that stealth if possible and AI vs player symmetric - Save after game over, to preserve history - Limit mentions of 'playing' not to break immersion - Delimit with colour the known endings section of F1 screen - Change 'scenario' to 'adventure' to be more immersive - Dedupe special item and lore menu overlay handling - Strenghten the types in item and lore menu code - Display scenario lore (fixes #157) - Factor our scenario description code - Mention that scale needs to be configured for high dpi displays, see #164 - Shorten some travis tests - Make full map position info mode the default - Refer to the inspected map position as 'here', not 'there' - Use the actor pronoun when describing items at the same position (fixes #215) - Don't switch the subject from enemy actor to self for the same map position - Don't chain dependent sentences when examining map position - Improve the wording of actor blurbs when inspecting map postions - When inspecting map locations, make items subjects (see #215) - Make it more obvious the game outcomes are fixed terms - Compute BFS with Word16, not Word8; slower, but some paths are longs - Be more careful avoiding 32bit overflows in frequencies (for JS) - Don't interpret the common desktop-switching key combinations - Clear the margins also when redrawing after viewpane changed - Specify fullscreen in config file and override on commandline - Implement both kinds of fullscreen - Name variable same as record field - Remove help comment regarding SDL-only - Add fullscreen CLI option for SDL renderer - Indent by 4, not 2, proportional spaces - Disable brew-sdl2-osx in travis that now takes too long - Don't configure the unused data directories when invoking cabal - When installing don't create the now empty GameDefinition/fonts/ - Don't limit jobs to 1 now that cabal shows errors fine - Don't embed the fonts in the web frontend version - Get rid of datafiles from .cabal - Kill the font license files that are duplicated in COPYLEFT file - Get rid of the now unused fontDir option - Use embedded fonts, unless absolute path is given - Embed game-supplied font files - Try to be more verbose where running cabal test in appveyor - Don't create ~/.test directory when testing - Version config file; fixes #220 - Don't error out if the effect-causing item is not seen by strangers - Add assertionz that, normally, items are known by actors affected by them - Hardwire a few less colours - Mention the item responsible for an effect, unless spammy - Get rid of a left over no-repetition message helper - Pass on the item responsible for an effect - Start main menus at the first normal item - Open game homepage when clicked in Main Menu - Write the homepage address in full in Main Menu - Depend on open-browser package - Make the game over messages more colourful; fixes #213 - Tweak the display of game over messages - Make one kind of ending message more precise and colourful - Display camped and restarted endings in F1 scenario screen - Save which games are camped or restarted - Add default messages for Restart and Camping; fixes #212 - Sort outcome constructors for best display in F1 scenario screen - Colour only outcome name in game over header - Refactor renderSection in preparation of multi-color lines - Avoid 'restart in foggy mode' - Unify rendering of game outcomes; fixes #210 - Remove a leftover mention of rmainMenuArt - Improve the colours of the F1 scenario description screen - Expose the meaning of colours for the use in other modules - Specify also defeat endings - Make headers in F1 scenario screen monospace - Factor out attrLinesToFontMap - Render F1 scenario screen better; fixes #208 - Add tests for many game modes in one session; fixes #228 - Add slowCrawl make target to observe AI in detail - Mention in item description it's unique, because name may not underscore it - Ignore the placeholder messages when displaying one message - Don't overwrite prop with mono overlay even if the latter empty - Add placeholders to history display, fixing #236 - Add a comment about historyMax divisible by screenful - Improve message class of HP-affecting effects display - Mark with dots if the show distinct messages different than the longer - Implement distinct messages for becoming more affected by a condition - Implement distinct messages for SfxTimerExtended - Move messages that are hard to scrap to Distinct class, to fix it - Prepare ppSfxMsg for Distinct messages - Don't let ESC clear messages in aiming mode; was confusing - Add Discord and Matrix addresses to the manual - Add links to discord and matrix chat - Let MsgRunStopReason suffice for running interruption - Save spam, but don't show - Clean up scraping messages - Don't run if ordered to run into solid tile - Reword the short post-action messages - Don't show both terrain description and a warning about it in yellow - Typeset one line of history view the same as whole history - Make some messages longer to look better on a separate line - Reverse history to match the order of messages shown on screen normally - No longer specify initial repetition of a message - Eliminate not saving and not repeating messages using counts - Use allB in a couple more asserts - Assert message constructors have proper length - Get rid of message class GADT as designed by Alex Byaly, simplifying code - Wrap first line of overlay at half the screen, not earlier - Make indentSplitAttrString more accurate - Tweak unique entities in content, adding 'the' as needed; fixes #214 - Don't capitalize and add 'the' to unique entities - Be more verbose when displacing over laying items - Align single high score lines - Copy-paste-hacked enlarging line-wrap treshold if too long headers then fit - Fine-tune display of too long reports - Display message classes aligned - Rename message class constructors for better display - Display message class in grey - Indent wrapped messages - In messages, replace some magenta risks by bright red harms - Be careful measuring length of multi-line texts for wrapping - Use the config option for one message per line on screen - Hide the implementation of Msg, again - Display message class in history if one per line - Don't report HP changes of projectiles - Use the config option for one message per line in history - Pass along the config option for one message per line in history - Add config options for one message per line - Rename the item spotting message - Bring back filtering empty messages, to fix checking if report empty - Add Show instance to history and its components for debugging - Improve removing EOLs when deduplicating messages - Remove MsgItemMoveLog now that one message class suffices for two components - Get rid of isSavedToHistory and isDisplayed encoded now in the GADT - Customize messages more comfortably - Simplify restarting client - Remove the code enabling multiple UI clients - Don't spawn many UI clients, but re-assign a single one - Make MsgClass a GADT - Eliminate Binary instance of MsgClass in preparation of GADT - In config file, specify prefixes of message class names - Change colour of game start message to a nicer one - Issue special messages when actor fall to sleep or wake up - Tweak message class use and colour assignment again - Remove abuse of MsgTileDisco - Recolour enemy spotted animation according to colour meaning table - Redo colour assignment to messages - Update frontends to highlight changes - Change the colour of names of sleeping actors to blue - Reflect changes in highlighting in the manual - Swap blue and green highlights - Highlight dominated actors more distinctly, as proposed by @bulbousBullfrog - Rename confusing message classes - Make sure indented texts are wrapped at earlier position - Make most texts wrap before full screen width - Extend UI.Content.Screen validation - Tweak texts to fit on screen easily with square font - Permit wrapping messages after N columns - Prefer stairs in bottom right corners to leave space for messages - Prefer levels in bottom right corners to leave space for messages - Update documentation to the config changes - Prevent friendly AI from wasting time nosing around our stash - Rename an identifier to a consistent form - Limit the size of monospace prompts when keypress requested - Redo message colours given that some colours get a very thin font, possibly - Use a mixture of fonts in the challenges menu - Comment the config file some more - Render all titles in mono font - Typeset mouse command table in varied font - Gut out Ubuntu Family Fonts from Debian package, because non-free - Beautify CREDITS - Rand the fontsets differently - Make the fontset tests more random - Don't run noopt travis tests on normal length crawl - Test all fontsets - Extend client logging - Make the game reentrant - Prevent mangled newlines in concurrent screen writes - Permit text usage in tests - Refactor mkUIOptions to require less content - Settile on 'auxiliary fonts' instead of 'long text' - Hardwire GTK font size to fix breakage from config format change - Simplify FontSetup - Implement scaling all fonts easily and safely - Add font scaling option to config and commandline - Prevent overspill when prop font is, in fact, mono - Remove the now unused portions of config and commandline - Make getFontSetup more accurate (not used yet) - Eliminate really the last dependency on old config file fields - Add a comment about lack of optimization for duplicated fonts - Eliminate one last dependency on old config file fields - Hardwire the font families to use for GTK frontend - Use the two weights of prop font - Recover old SDL frontend functionality using new config - Move font types to make importing them in ClientOptions legal - Let fontset be chosen on commandline as well - Extend config file with font and fontset definitions - Molify Debian's lintian - Legalize all the long text fonts even more - Legalize all the long text fonts - Add the ubuntu font family set of long text fonts - Add another set of long text fonts - Add new long text fonts based Adobe Source - Remove old long text fonts - Add commented out TTF.setHinting directive - Crop too high fonts more at the top than bottom - Get rid of the last traces of 'exploiting' terrain - Simplify and speed up linearInterpolation (not benchmarked) - Change which test is run in -O0 travis - Make final version of integer casts hardening, using the type-level int-cast - Explain the communication overhead concept - Use toIntegralSized to crash when int wrap would occur during conversion - Parse config at compile time - Let foes occupiy exit in escape scenario, etc. - Add missing cskip specifications - If more info in history, end message with three dots - Bump GHC versions for testing - Bring back SPACE as clearning messages - Update the default config wrt changes to RET keybinding - Let ESC cancel the aiming line as well - Keep the order, modulo abs, of factions as in the roster - Don't switch level away from melee when positions not taken yet - Improve valuing AI displace action a bit - Increase expressiveness of initial actor generation - Permit tiny rooms if no stairs variety - Pick the sole actor for cwolf games ignoring empty specs - Remove length that forces the whole psFree, even though most unused - Don't drop initial actors if not enough initial faction positions - Remove a code check that is already in mode content check - Generate initial actors in the order given in content - Prepare types for more faithful initial actor generation - Rename ES.toList - Simplify populateDungeon and catch similar overcomplications - Generate initial actors in per-faction positions, not in alliances - Make the use of integral conversion easier to verify - Reword ESC description again - Settle on apostrophes to denote one-letter keyboard keys - Update command docs to command help changes - Let only ESC clear messages - Show newcomer aim mode help also when pressing ? in non-default detail level - Simplify how the hint prompt works - Align newcomer aiming help with ? help when aiming - Try harder to display errors in appveyor runs - Simplify and slightly fix linearInterpolation - Extend debug information when summoning fails for lack of space - Except for DetailLow, always show any description when examining - Give feedback of detail level also when item selected - Show item symbol in item lore view in square font - Tweak and fix item examination messages one last time - Change display of terrain more gradually as detail level rises - Simplify two cases of sortBy emulating sortOn performance-wise - Improve describing of items with words limit 1 - Use partItemWsDetail for item description when examining - Make the second detail level the default - Fine-tune shortest item examination message once more - Move SPACE to CmdAim category - Document that MMB and RMB cycle detail level - Let RMB and MMB cycle detail level - Add aiming help line for newcomers with SPACE hint - Don't warn about our own actors - Reformat lookAtActors - Implement detail level for actor descriptions at position - Show position description components according to detail level - Show detail level in HUD - Redraw after detail level changed - Implement detail level for item descriptions at position - Match the new order of descriptions in a couple more places - Bind SPACE to cycle detail level in aiming mode - Add DetailCycle human command - Record detail level in the aiming mode state - Add and improve headers for scenario endings messages - Change sortBy to sortOn in a couple of places - Optimize truncateOverlay with the best of the sortOn and sortBy worlds - Replace some performance-wise wrong uses of sortOn with sortBy - Mark the last major outcome in F1 scenario screen - Change unique actor names to match the upcoming new convention - Don't show even deafeat messages in F1 if not experienced - Update the '?' hints to the new Tab semantics - Add dumping history to a text file - Advise to replay initial scenarios - Adjust the order of spotting to match the order when exploring - Swap the order of intrusion warning and enemy spotted message - Announce specially foes that have non-trivial items equipped - Clean up Content.Input.makeData - Destroy the trunk when actor dies, for death effects - Make sure embedded item desc stands out from the following activation info - Make unique speech heard regardless of distance - Let uniques taunt in a big way - Add type signatures to AI action picking functions - Reorder consistenly mode kind component definitions in the file - Extend rarity specs to above 10 logical levels - Improve comments and messages about Rarity datatype - Avoid spoiling victory game over messges, until seen - Factor out victoryOutcomes - Display game over messages in F1 scenario screen - Shorten a field name to avoid >80 length lines - Colour header in the F1 scenario screen - Disable a travis test that still panics - Revert the scenario choice font frop prop to mono, for typographic consistency - Shorten a flavour blurb to make it fit when font is square - Extend wrong line break avoidance to colourful texts - Display also flavour in scenario choice screen - Don't attempt to display F1 scenario blurb in square fonts with two columns - Don't display the extra TAB commands in item menu headers - Mention the new TAB commands in manual and keys sheet - Represent a key not appearing in help by empty categories, not CmdNoHelp - Enable pointman cycling key bindings with Control - Factor out the pointman cycling key bindings - Make sure item spotting messages are of proper length - When spotting new items, write short message to screen and long to history - Don't merge messages with different classes - Enable messages that are not displayed unless from history - When spotting items, sort them only once - Don't report each item located in stash in a separate sentence - Add verbosity flag to UpdSpotItemBag and UpdLoseItemBag - Make new Direction type more general so that it can be used in other cases - Add direction type to make cycle function less verbose - Add functionality of back-tabbing both across floors and within single levels - Add bool flag to cyclying so that direction will be possible - Change naming of member cycling in-code to be keypress agnostic - Swap S-Tab for A-Tab as new binding for cycling members on the same level - Swap descriptions in help file so that tab functionality matches description - Swap funcionality of Tab and S-Tab without renaming in-code - Reverse order of S-Tab so that it doesn't first cycle to another level - Add up to two EOLs in tile examination message - Order tile examination message elements by decreasing importance - Make terrain names harder to confuse with items - Improve messages and order of stores when moving items - Make ItemNotCalm message applicable to flinging and triggering, too - Tighten the result type of moveItems - Add a comment to the default config with an example for rebinding a key - Clarify that nominal values of burning and wounding are used - Don't call robots living - Don't display TMI about total condition time if come from explosions - Let AI prefer a buckler over a fist - Nag about the F1 screen when starting or resuming - Display info about scenario in F1 screen - Split mode note into motivation and hints to choose where text is displayed - Restructure text displayed on challenge menu to make it more clear - Add scenario rule notes to be displayed instead of full notes - Don't let crosshair obscure terrain, items and actors - Work around SDL treating tilde as paragraph on some Mac keyboards - Speed up DOM rendering by ignoring highlights if possible - Permit specifying seeds in the config file - Make attacking animation more subtle to distinguish from harm animation - Even in the subtle hit animation indicate who is the victim - Add a missing space between 'modify terrain stat' and '5' with square font - Make Burn as valuable as other damage for consistent UI display - Show very good no-timeouts weapons at the correct position - Partially hash-cons ItemQuant, with very modest effect - Change some occurences of trivial Quant to quantSingle - Simplify skill and place menu code - Don't multiply initialPlaces when preparing place display - Consume places for places menu more eagerly - Eliminate space leak when creating items - Avoid memory leak due to caves used for levels on a list - Fit the short help blurb in 4 lines - Start the game with a confirmation prompt - Sort weapons wrt timeout and item kinds ID as well - Simplify querying skills of the leader - Avoid long animations before the extra burn or wound animations - Change combat animations to easily distinguish attacker from attacked - Let AI displace teammates when no risk of displacing back - Restrict AI use of stash guards less harshly - Add a comment about allies stealing each other's stashes - Let AI keep fleeing if no support gained - Remove a few redundant TypeFamilies pragmas - Explain in a comment why on a level with stash many sleeping may accumulate - Make safari scenario harder now that aliens are buffed up - Let AI displace friends less often - Unify the test for item being damaging - Improve module haddocs for content - Hide unneeded content group name patterns - Add short-caves debug game mode - Display melee damage even if there's no ranged damage - Reduce the number of calls to foeRegularAssocs - Make sure AI actions are not recursive, even shallowly - Reduce the number of calls to currentSkillsClient - Speed up skill arithmetic - Math order of cases in processWatchfulness with the order in type definition - Remove inlines that stopped helping in GHC 8.8 and that obstruct profiling - Compute friendAssocs only once for each actor AI processing - Limit and speed up invalidating BFS - Speed up a few significantly expensive functions - Use the JS splitmix optimization - Tweak random numbers code a bit - Improve haddocks of the dice mechanism - Improve help display a bit - Move ClientOptions.hs to adhere to the convention about module hierarchies - Meld LH and Allure content, continued - Port over oil explosions that harm the targeted actor from Allure - Meld LH and Allure cave and item content - State the number of item in equipment even when removing from it - Explain the goofy handling of recharging in stash - Regenerate tile even if kind not changed, but all embeds gone - Permit altering tiles with items on them - Don't leave stash unguarded if friends may spawn - Add stash detection effect - Cap stat bonuses when assigning AI value to items - Value skill bonuses differently for each skill - Be even more specific when describing what an actor does on a tile - Adjust the SDL test to run without installing fonts, as Debian requires - Don't let projectiles ever cause normal tile transitions - Tweak pushing via an embed - Add Q&A about hearing - Tweak item valuations to make awkward armours wearable by AI - When describing an actor, say if it's pushed - Push towards embed, not in direction of previous movement - Simplify key bindings display code - Clean up naming of column offset in UI - Ensure no missing space between columns of help text - Prevent a trailing space - Don't activate barrels via mist - Don't treat blasts that damage through effects as mists - Don't let on noise distance if nobody can hear it directly - Differentiate the sound of projectile and actor hitting a tile - Emit sound also when an actor hits a tile - Display close and out-of-level noises with special colours - Display noise distance information - Send noise distance information - Refactor hearing - Mock up extended hearing - Give better message when not enough skill to melee - Disturb resting when calm enough again - Don't summon by hitting a projectile - Make actor aggresive if only one is not guarding stash on the level - Ban teleporting immobile foes; they come back and their loot is lost - Validate that item group names are short enough - Cath too long group names - Catch empty group names - Let more actors start sleeping - Consistently create on the ground all items looted from terrain - Mark game modes already won in this difficulty - Clear screen after start ASAP - Use the AttrLine smart constructor or assertions as appropriate - Rename firstParagraph - Tweak types to work with the stricter smart constructor - Move assertion about trailing whitespace to a smart constructor - Eliminate trailing spaces in help and item menus - Replace a momentarily trailing space with nbsp - Don't define special messages with space, they are added automatically - When splitting lines, remove the trailing spaces - Simplify a condition in splitAttrPhrase - Don't add a space when appending a report that starts with newline - Don't produce backdrop on black background - Optimize truncateAttrLine and truncateOverlay - Mark assertions expensive in menus - Make avoiding SDL frame drawing more fine-grained - Simplify SDL frame drawing based on previous frame - Optimize menu scrolling via an extra texture - Don't waste healing items when HP low ceiling - Don't display that foes don't know their weapon when only you don't - Reflect keybindings change in the start scenario menu - Permit hero AI to consume elixirs - Don't flee if can kill a blocking enemy instead - Make fast or hasted actors willing to close in for melee - Compensate for overhead of animals when creating conditions - Mark organs that are ready to expire once applyPeriodicLevel runs - Avoid stating that an organ will last for 0s - Tweak slightly AI item preferences - Get rid of DropBestWeapon - Don't rechare nor discharge projecile payloads - Start dicharging with strongest weapons - Let Discharge add to cooldown, not reset - Discharge only items with a timeout - Consider as support only actors that can harm in melee - Make organs (minimally) accessible from triggering - Let AI trigger items among organs - Relax trigger stat 1 restrictions - Tweak AI item use preferences valuation - Improve the gameover item menu message - Record duplication of items to avoid absurd gameover messages - Discharge not only equipment, but also organs - Don't paralyse the last AI stash guard - Give AI stash guard more freedom if friends adjacent - Make AI keep guarding even if teammates on the level may leave it - Don't let AI leave stash unless foes harm it - Don't abandon stash when teammates temporarily immobile - Prevent displacing a stash guard - Don't abandon stash if temporarily nonmoving - Abandon stash if under heavy fire - Don't abandon stash unless foes seen - Don't use aidTgtToPos unless necessary - If enemy in light, pelt him instead of leaving dark - Don't stop fleeing into hideout after 5 turns even if foes appear - Target dark if fleeing and no foes targetted - Efficiently find closest hideout for AI - Tabulate hideout tiles predicate - Factor out distanceBfs - Don't hog leadership just because weak and enemies close - When fleeing and soon after target only actors that can't melee - Stop fleeing if not hit and possibly friends managed to join - Take into account recent fleeing when taking off light - Simplify hinders - In AI action choice use recent fleeing instead of this and last turn distress - Don't overwrite fleeing record if fled recently - Don't chase foes if fled recently - GC fleeing record when actor lost - Extend fleeing state from 1 to 5 turns - Store not only fleeing position but also fleeing start time - Reorder organs - Make two organs not LH-specific - Don't undervalue weapons with drop condition - Make it possible for animals to mark levels as explored - Make slack tactics AI less erratic when focused or distressed - Add a comment about animals bad at changing levels - Let AI displace any sleeping blockers - Don't let AI actors fall asleep if not relaxed - Don't force animals to change target just before reaching it - Improve condAimCrucialM for vector targets - Flesh out the crawl survival test scenario - Make crafting act as if on bottom level - Move skill checks from pathfinding to targetting and restrict immovable actors - Forbid making missiles hungry, asleep, etc. - Optimize processTileActions - Make sure projectiles can modify terrain even if safe from effects - Don't reset TVector target, because it's set manually - Create a central staircase - Display ranged damage even when limited space in menu line - Shorten the display of weapons if not enough space - Don't consider enemy projectiles for determining if AI is in melee - Make poison less deadly in corridors - Eliminate the stash domination exploit - Make guessing if AI was hit by projectiles more accurate - Prevent wounded animals from closing in agains mutliple enemies - In messages tell discharge from recharge effects - Make smell more important that stairs, again - Clean up vector target code - Melee non-targets if target not worth killing - Don't perform all monadic actions that compute strategies before choosing one - Don't retarget actor if blocked by the very actor and so can melee it - Simplify target shoice for slack doctrine factions - Sort items in lists in normal texts - Help AI unlock the dungeon if stash guard helds the key - Prevent animals from eating their own meat - Prevent creating new items via throwing others, with ikit - Signal specially when an item is located in a stash, as well - Say when an item appears in a stash, etc. - Don't warn if selected missile from stash can't be picked up - Don't flee if back next turn - When fleeing don't remember the last targetted enemy - Move keybinding content definitions to match display order - Prevent, in UI, removing from equipment if not calm - Warn when illegal item movement attempted - Display stash blurb also when switching the leader - Don't let stash guards leave levels - Don't prevent displacing a friend if only half of a loop is present - Don't neglect guarding stash unless no buddies to help - Tweak AI conditions - Don't flee if own stash close unless completely overwhelmed - Easily displace teammates that guard the stash - Mention stash when walking over a tile - Do not flee and be reluctant to chase when guarding stash - Introduce guarding own stash - Make isDoor more accurate - Don't make a rumble when door closes - Tabulate isOpenable and isClosable - Make tall staircases more likely now that they are often broken - Make types a bit more strict in stairs calculation - Simplify stair number computation - Rename cextraStairs - Change the semantics of cextraStairs - Separate stair number computation and level generation - Compute abandoned stairs earlier - Correct the documentation of cextraStairs - Roll extra stairs earlier - Roll cave kinds earlier - Simplify shuffling caves - When transforming walkable tiles, don't insist on embed activations - Ban crafting and terrain transformation in the same action - Try to make the terrain transformation specs more readable - Prevent the exploit of using cover against non-moving shooters - Make makeLine non-monadic - Keep the data invariant of @arenas@ for longer - Make the *interrupted* running display look less corrupted - Add and tweak comments about item maps - Update the manual and keybindings printout - Tweak the wording of keybidings descriptions - Prevent marking inert items as (charging) - Add ANY_FLASK group - Make sure impression is dropped before other conditions - Handle failed bump-modification without embed activation - Change the key for new game, not to mix up with 'n'o - Remove misleading comment about tiles being explorable - In message fits in one long line, don't wrap it into small lines - Don't make consumables with Ascend effect worthless - Change division by zero into an assertion failure - Comment why 'seen' sometimes gets from 100% to 99% - Validate HideAs in content instead of asserting its property later on - Tweak (speedup the tiniest bit) updAlterTile - Stop leader before leaving dark in a more useful way - Don't equip light even if enemy only remembered - Simplify the interesting target conditions for AI - Flee through dark not only when starting in light - Chase and flee through dark even if not starting from light - Simplify the chasing ambient light condition - Prefer leaders that don't step into light - Detour when chasing, to avoid lit spots - Prefer fleeing into dark spots - Don't flee from projectiles if can't flee into the dark - Prevent AI from vainly chasing fast shooters - Tweak and simplify AI fleeing condition - Help AI sidestep a blocking meleeing friend - Let AI go towards enemy stash even if in melee - Join melee from twice longer distance - Rename CURIOUS_ITEM - Don't display 0 ranged damages - Add missing content group definitions - Simplify content validation - Check that content group names are all listed - Let the draft detector emit noise and sound - Tweak the engine to accomodate a detection device that pings - Prevent non-humans from hogging key items and so blocking progress - Reorder item definition to match those of Allure for diffing - Don't emit server leader change messages if known to client - Comment about the trade-offs of weapon benefit calculation - Mark the lore screen with crafting recipes - Don't confusingly refuse to display the pushing effect for shields - Increase minimal damage taken, regardless of armor, to 5% - Be pendantic when emitting messages about creating items - Don't say that a projectile is wounded - Show enemy HP under the selected item - Simplify strongestMelee - Add an alias for picking up all items, etc. - Sort out naming of item and embed activations - Simplify revCmdMap - Warn when embeds activated, but transformation failed - Check dangerous tools use even when nested in transformations - Don't transform terrain when bumping - Let pathfinding avoid vicinity of enemies - Make it easy to discern an attack on teammate among many messages - Don't display misleading condition drop messages - Visually distinguish embed names and descriptions - Permit fractional FPS, for debugging - Colour-code map position descriptions - Move the run macro to where all other engine macros are defined - Permit unknown command, for testing - Simplify and clarify the mock - Unify repeatHumanTransition 1 and macroHumanTransition - Reflect in types that macro frame stack is never empty - Make more space for explicit import lists - Rename components of the macro data structure - Drive the point home that repeatHumanTransition 1 /= macroHumanTransition - Add tests that illustrate not creating new buffers for in-game macros - Touch up the macro code a bit - Add tests showing lack of referential transparency of named macros - Don't prompt about recording a macro it's part of macro replay - Warn when pick up not to equipment due to low calm - Display how many items in equipment when equipping - Refuse to equip if equipment full - Separate testing harness import from game content import - Add a helper function to shorten tests - Rename the semantics functions - Implement the desired semantics of RepeatLast - Sketch the desired semantics of RepeatLast via tests - Use the same updateLastAction function in test mock - Collect a more precise last action key - Simplify slightly the mock - Grey out more (all?) illegal commands - Add special messages when illegally flinging or applying - Go even through stores that all factions have empty - Revert banning stores for all actors based on the first checked - Add special messages when illegally moving items from or to eqp or ground - Forbid unequipping items when not calm, again - Don't ignore illegal stores if once shown - Display the number of items in equipment even when not calm - Write client RNG seed to UI RNG seed numbers at game restart - Save UI random seed to avoid boring messages if many save/loads - Add a comment about stealth viable for tactics, but not strategy - Reset C-f when x-hair automatically changed - Pick as leaders also the actors that could melee but should not - Make animals unable to catch projectiles - Don't cycle xhair through own stash - Display slideshows normall unless really too long, not just excessively long - Give more time to view a frame when frame level changes - Be more verbose when kicking terrain - Prevent the inability of taking off max calm draining items - Re-assign action repeating keys - Clean up macro command's texts - Place ikit item in a semi-random way - Tweak content group names for display - Avoid 'of gain of gain' in item descriptions - Don't talk about blocking when no kinetic damage dealt - Match the order of game over lore screens with in-game lore menu - Prevent AI from imbibing a potion of panic - Don't call a tile alterable if embeds have no effects - Prefer leaders that target enemy stashes - Don't chase stashes of friends and neutrals - When creating items on the floor during dungeon generation, create ikit items - When embedding items in tiles, optionally generate more items - Redesign so that charging items can be determined even if timeout unknown - Clean up some instance deriving - Factor out two more pure functions about macros to use in unit tests - Permit creation of input content without parsing a config file - Factor out pure function about macros to use in unit tests - Record nested macros on stack - Snatch enemy stash ASAP if adjacent - Port tests to tasty - Undo the attempt to end game when player kills window - Remove a spurious recordHistory - Make sure verifyCaches error doesn't obscure RNG seed of the crashing run - Don't let big actors auto-craft at death - In menus show how long conditions will last - Tell how for long an actor is going to be slowed when first affected - Tell how for long an actor is slowed in total after extra slowness, etc. - Tell how much poisoned actor is in total after extra poison, etc. - Don't display the mandatory turn frame if a frame already displayed this turn - Don't draw (and printscreen) identical frames, even if small fonts used - Don't trigger terrain if projectile has lost its payload - Enable cooking of food thrown into fire - Don't spam when blasts transform terrain - Simplify dieSer - Ensure the last step of projectile flight is shown - Remove a repeated check - Be more verbose when transforming items with scrolls - Display level where the stash is, as a reminder - Don't displace sleeping immobile actors - Simplify checking if an item is identified - Prevent AI from putting most weapons in reserve - Let AI be eager to equip unidentified and good unique items - Don't be too verbose with tile altering when leader stands on it - Don't craft from equipment due to possible involuntary harm - Remove altering via dropping items; too disaster-prone - Refactor handleDir - Permit indicating the zero vector with mouse - Colour and reword the message about consuming items - Be a bit more verbose about tile changes - Don't inform about a projectile's equipment - Avoid hitting 'feebly' with a heavy hammer - Show, e.g., harpoon as an obvious choice for throwing despite usable for melee - Don't summon by hitting a friend - Don't hit delicately just because the weapon was not identified - Prevent AI from wasting time throwing very weak projectiles - Avoid 'chip of scientific explanation turns out to be', when identifying - Improve item destruction messages - Add OnUser effect constructor - Avoid 'the pair turns out to be' when identifying - When short name requested, don't append numbers and parens - Prevent VerbMsgFail being repeated 99 times - Describe how colours correspond to tile properties - Let even unskilled actors take over stash - Take over stash also when actor dominated or created - Target enemy stash in preference even to enemy actors - Add comments about salter and pathfinding - Avoid 'controlled by Controlled foo' - Avoid 'hits delicately' with a 14 HP wounding hammer - Ensure in content that Burn is positive - Mark weapon burning and wounding on HUD, to avoid hammers all look benign - Clarify that chosen weapon needs not be optimal for a foe - Improve display of places lore - Pick up enemy summoning periodic items to deny them to foes - Don't send the SfxTrigger messages to the client - Mention that stats menu summarizes the organ menu - Add sample cabal.project.local files - Introduce VerbMsgFail effect - Let crafting use up terrain, if successful - Don't split tile properties description by description of things on the tile - Avoid crafting via walking into a worshop tile - Introduce SeqEffect to use for crafting creation in place of AndEffect - Let components 'disappear', not 'be removed' during crafting - Improve crafting recipe description - Automatically identify crafted items - When cratfing, use tools but destroy components - Don't create items in equipment if not enough free slots - When crafting, apply as many copies of durable items as required, not just one - Don't crash if crafting fails due to unique already generated - Improve debugging of item creation - Don't display thrown damage for organs - Flesh out projectiles opening terrain and flying through - Require embed activation even for item-fueled terrain transformations - Don't spam if recording is a part of a macro - Make the summary of killed enemies less confusing - Let some explosions destroy terrain - Add black backdrop beneath prop font overlays, for readability - Be even less verbose in descriptions of items with crafting - Validate that item definitions have slots whenever expected - Rename the Macro type - Record also in-game menu navigation keys - Recover macro's recording order invariant - Document Macro, swap types in smacroBuffer's Either type - Wrap the macro buffer into a newtype - Encode recording state in type of macro buffer - Be more precise when reporting OnCombine effects - Format crafting recipes in a more readable way - Gather effects application flags into one record, for redability - Add an assertion that catches ItemQuant data invariant violation - Ignore timeouts when using tools or transforming terrain - Don't apply kinetic damage when using tools or transforming terrain - Generalize printing crafting recipes to many products - Rename the dagger item definition to match Allure - Help parsing recipes with bullets - Avoid spike 2d1 (cooldown 8) of cooldowns at 4 - Typeset crafting recipes slightly - Rename the whetstone item definition to match Allure - Don't repeat crafting recipes in OnCombine descriptions - Show full crafting recipes only in lore menu, not when targeting - Speed up and simplify the use of countIidConsumed - Alter ConsumeItems and unify with ChangeWith - Add optional count parameter to effect CreateItem - Validate that item definition parameters in content are positive - Display details of item crafting effects - Add ConsumeItems effect - Let AI ignore potentially unopenable entrances, to avoid AI loops - Improve messages related to Discharge effect - Simplify checking actor presence on a level - Add the Discharge effect - Be more verbose when loot created - Don't complain about failed transformation if embedded item triggered - Revert "Break thrown non-unique items, even if durable" - Let projectiles trigger only easiest embeds and alterings - In tile content specify if projectiles may trigger actions - Display when ending a flight, to make harpoon a sensible weapon - When actor lacks tools to transform a tile, inform him - Break thrown non-unique items, even if durable - Squash repeated terrain transformation tools in descriptions - Confirm that a weapon is to be used for transformation - List tools needed for transformation in terrain description - Factor out client altering conditions - Unify altering by bumping and altering by pointing and server conditions - Don't waste turn when altering fails, but waste when embed triggering fails - Factor out listToolsForAltering and subtractGrpfromBag - Permit the use of tile altering tools with different durabilities - Factor out parseTileAction - Don't count repetitions in OneOf effect description - Prefer non-durable altering tools, even in equipment - Avoid message about triggering a 'way' without any more details - Rename terrain patterns that denote possibly depleted resource - Consider durable tools last for terrain alteration - Process tile features in order to prefer trap disarming to triggering - Prevent embeds triggering each other in a loop - Simplify applying embeds - Display untruncated (and mangled) messages in teletype mode - Make frontendName to avoid losing message in teletype frontend - Clean up the presentation of group name patterns again - Verify that singleton group names are so - Separate singleton kind group names - Validate that some more kind groups are singletons - Rename HideAs to PresentAs for item kinds - Simplify individual content validation due to stronger global validation - Check that group names unique and not void - Supply group names to the content creation and validation function - Improve special gameover messages - Clean up the lists of hardwired patterns - Get rid of OVER in pattern names - Clean up the DEFENSELESS vs VULNERABLE mixup - Remove foldl from prelude to prevent space leaks - Group the patterns yet slightly differently - Catch empty OneOf during item content validation - Group the patterns slightly differently - Be consistent in taking only first word as game mode name - Rename PHD_DEFENSE_QUESTION to VULNERABILITY_BALM for diffing with Allure - Group TileKind patterns - Remove IsString instance of GroupName to catch typos - Reorder code in content files - Make embed content patterns different from tile patterns - Use PatternSynonyms for all content - Permit and use PatternSynonyms in some ItemKind-related code - Don't crash when some items consumed during dropping - Don't warn about untriggered embeds; altering may be the focus - Don't report exploiting if not triggered - Let terrain be changed with items even when embeds not untriggered - Be less verbose when losing items - Be more verbose when losing items due to tile altering - Report which embedded item was exploited so that 'Nothing happens' makes sense - Don't report items appearing under projectiles that are already spent - Reword command category headings - Don't move away staircase inhabitants if projectile is changing levels - Let only visible projectiles discover hidden tiles, etc. - Don't display vacuus menu mode switch symbols - Waste items for altering only if voluntary or released as projectile - Get rid of spam about inability to modify terrain - Identify item lost in order to modify terrain - Let projectiles alter terrain, but not when just flying over it - Don't check alter skill when projectiles do the altering - Improve comments about the 6 tile modifying constructors - Don't modify terrain by just walking over it - Permit the use of equipment for modifying terrain - Warn when no items to modify terrain - Prefer durable items for altering tiles - Don't destroy but apply durable items used to alter tiles - Add tile alteration that demands and consumes items - Improve description of cooking effects - Add OrEffect binary constructor - Replace Composite by a binary constructor - Display failure message when embed not under feet triggered vacuusly - Don't light adjacent tiles for free - Avoid 'you look less hungry' about the pointman - When smashed item exhibits no effect, don't warn - Report tile changes under a big actor - Generally never alter tile if under feet and embed not triggered - Detect when dropping or destroying items is vacuus - Only alter tiles via walking on them if any embed triggered - Don't spam when embed doesn't trigger when walked over - Implement DestroyItem effect - Let embedded items react to items dropped over them - Explain away only pointman moving - Inform about automatic melee in the in-game help - Mention that all factions and actors are equal - Reverse stars and underscores in HP bar in HUD - Stress suvival in the game manual - Make skill check for embedded items on the client consistent with server - Give hints when terrain can't be entered nor modified - Describe hunger removing items, etc., with more detail - Mention leap frog in game manual - When scrapping message repetitions, ignore trailing EOLs - Don't spam about items underfoot at game start, when they are also 'located' - Report moving player's stash on a new line, mostly for starting screen - Bring back scenario notes at game start and resume - Bring back notes in challenges menu - Handle the meta note about scenario separately - Mention challenges in the high score entries - Remove the Show instance of ScoreRecord - Invalidate inMelee when weapons dropped or picked up - Use --assertExplored in makefile tests - Verify the commandline assertion about explored level - Don't assert exploration if another game started in a debug run - Add a debug commandline option to verify explorers are not stuck - Make projectile-less actors randomly more aggressive - Add a bit of histeresis when fleeing - Destroy proportional font textures to avoid freezes when they pile up - Prevent AI from running away from helpless foe - Stop targetting foes once they lose all weapons - Let AI do more vs foe with HP <= 0 to avoid stalemate if foe regenerates fast - Don't spuriously check if TEnemy is a foe - Improve documentation of targeting - Don't attack hapless nonmoving foes at range also - Don't normally target nonmoving actors that can only melee - Use actorWorthMelee in inMelee - Melee a targeted foe even if not worth meleeing otherwise - Don't target hapless uninteresting foes - Don't melee a foe with only benign weapons - Don't consider foes with benign weapons a threat - Only consider actor in melee if adjacent foes worth meleeing - Don't interrupt running if benign melee actor adjacent - Introduce the count of benign weapons of an actor - Make sure tutorial scenarios have enough melee weapons - Avoid checking isModifiable once more, in verifyAlters - Consistently check isModifiable together with embeds - Mark the new request failures as impossible on the server - Add some internal operations, for future easier profiling - Removed TileOpenClosed error message - Further enhancements in tile closing - Removed TriggerTile - Remove commandline default that forced Just that is interpreted as game reset - Bring back the way dungeon generation perturbed random rolls - Make 64bit native and 32bit browser games play the same with the same RNG seed - Keep a separate random seed for UI - Use bitmaskWithRejection form randomR - Add some BENCHOPTS - Use splitmix - Suggest switching to another teammate if movement skill drained - Turned off showing of default --maxFps value - Additional logPriority value and defaultMaxFps - Couple command line options are now clamped or checked before they're set - Only run with selected actors that are not yet at goal - Hint about menus in movement stst too low message - Hint to wake up if movement skill drained by sleep - Display also own asleep actors green on HUD - Make the heading of item menu when inspecting an organ less confusing - Tell that second 'f' projects - Add a comment about the 'exploit' verb - Do not mention 'trunk' in weapon strike messages - Do not mention 'trunk' in armor blocking messages - Suggest in history menu to press RET - Get rid of Server.EndM - Make sure closing window in rage at defeat/win saves game - Mention in the manual that HP starts at half max - Reformat game peculiarities list in the manual - Don't announce pushing that has no effect - Try to make the under AI control prefix less confusing - Let animations be toggled in main menu - Don't display buttons on a separate line unless message is very long - Add a newline after scenario description - Avoid blank lines in history - Overhaul the order and blurbs of game modes - Make raid scenario squad-based - Display scenario descriptions in their submenu - Don't show backstory in submenus of the main menu - Rework new game start menus - Add a visual separator between new games in history - Reverse the order history messages are displayed in - Copy the list of distinguishing features from Allure homepage - Restructure chronologically the game manual with verbs as section titles - Avoid empty paragraphs - Ignore linebreaks when showing the condensed history line - Underline that mouse is optional - Make 'crosshair' on the status line uniform with other headers - Bring back the help prompt that doesn't confusingly mention advancing - Mark overful HP and Calm specially on HUD - Add some blank lines when stacking command lists - Stress that mouse is optional - Move mouse help screens earlier - Don't indent help parts by proportional space width - Clarify the structure of help information - Don't show 'crosshair' when not aiming - Get rid of 'x-hair' - Make collective running less prominent in help; tweak help - Mark some text files as out of date - Add a paragraph break before nearby item summary - Accept longer menu messages - Add a paragraph break before cave description - Warn that over-max HP gain is transient - Mark deaths with paragraphs and display 'Alas' already at incapacitation - Add a line break after gameover identification of items - Add a couple of line-breaks to reports - Rename emptyAttrString, which was misleading - Introduce a newtype of attribute lines with no linebreaks - Prevent backstory overflow in main menu with proportional font - Move O command just after I command in help - When toggling autoplay, as with y/n, not SPACE/ESC - Let left and right arrow keys move between sides of help screen - Fine-tune helps screens for display side-by-side - Naively cram help screens side by side - Merge the two item command help pages - Use proportional font for help, dashboard and item menu - Use square font for movement scheme help paragraph - Make it possible to use many fonts in help - Get rid of MoveKeys.txt - Set up main menu for both proportional and square fonts - Remove backstory from help screens - Add backstory to main menu, set up for square font, for now - With square font, add extra space before item symbol - Represent button width as a datatype - Parameterize all typesetting by the font setup (multi or single) - Pass along information about supported fonts - Let sdl frontend really handle the setup with no prop nor mono font - Gut out ascii art - Add blank space around some lines of overlays - Take all items with *, not ! - Update the item removal verb - Switch a single place lore display to mono font - Render skill menu and item lore menu with proper fonts - Display item symbol in item menu in square font - Display label and symbol in items overlay using square fonts - Display at most 3 lines of buttons when too little space for menu - Make a mouse misclick error easier to understand - Don't wrap Mono keys after a prompt - Overlay in square font when basic frame is under animations - Introduce a separate UI coordinate system - Make boxSize even - Don't highlight wrong overlays when buttons highlighted - Avoid proportional font in button-like UI areas - Permit overlays with gaps - Name font kinds consistently - Display some overlays in monospace font - Display history labels in mono font - Specify fonts also in slideshows - Move the definitions of DisplayFont and FontOverlayMap - Propagate the choice of fonts for overlays - Generalize drawOverlay to specify desired font for each overlay - Generalize overlays to let them start at arbitrary X offsets - Eliminate overlayFrameWithLines - Document better the overlay types - Move ColorMode to another module - Specify when to use which kind of main font - Add monospace rectangular font to game configuration - Remove the old woff font - Add new fonts, proportional and monospace - Specify size of the message font separately - Don't cursor highlight break up proportional font message chunks - Don't let space break up prorpotional font message chunks - Render the extra overlays in proportional font - If message font supported, pass overlay over instead of rendering - Initialize also the message font in SDL frontend - Add message overlay font to game configuration - Comment about why animals rarely eat food - Rename tactics to doctrine - Get rid of the henchman notion - Rename leader to pointman - Handle UpdTimeItem when container not visible (CStash), but item visible - Improve command descriptions - Redefine key bindings not to collide with new movement keys - Detect collisions of keybindings with movement keys - Use the keys freed by removing the right hand movement setup - Replace right hand with left hand movement keys - Main inventory the main store in game help and UI - Move total value display to all posessions menu - Identify items at any item move, in case they are thrown at stash - Avoid spam about actors getting braced - Mention in failure messages that hoard accessed when stading over it - Mention in failure messages that too low Calm for Eqp - Attempt to display handling of multiple items more succintly - Bring back UpdMoveItem to have better messages - Properly describe item move actions - Lose access to stash when enemy steps on it - Say who's stash is an item moved to, unless it's ours - Announce that enemy stash found - Make sure to let clients know even the human trinket items at gameover - Let new stash correctly overwrite old, even if old not seen - Introduce PosSightLevels and use for CStash containers - Get rid of seenAtomicGeneralCli - Simplify handleAndBroadcast - Make updTimeItem more loose when enemy CStash is considered - Don't carry item definitions in commands that don't create items - Register on the client the items necessary for commands - Analyze what items client needs to know to process a command - Clean up creation vs spotting of actors and items - Add UpdRegisterItems to be used instead of many ad-hoc calls currently - Be permissive when performing atomic action wrt CStash - Simplify SfxStrike, etc., and don't require access to store's bag - Simplify PosFidAndSight - Remove UpdMoveItem atomic command that is not too common without CInv any more - Simplify handling of CStash action visiblity - Display also enemy stashes in position description - Let actors learn stash positions when they come into view - Mark on the map enemy stashes as well - Refactor UpdStashFaction to let enemies see it sometimes - Add shared stash position to team's perception - Say in position description that stash is there - Mark own shared stash position with white box - Remove gstash handling in atomic commands - Move CStash in server code, not atomic commands - Avoid catch-all in cmdAtomicSemSer - Introduce UpdStashFaction - Do not produce a now unused ItemFull for inventory - When moving items, don't cycle to Ground, when over stash - In UI don't try to use CGround when over CStash - Don't let AI consider floor items at it shared stash location - When scanning a map, don't consider own stash an ordinary pile of items - Update atomic position information for shared stash - Make eqp, not stash, unavailable when low Calm - Make CStash the new implicit default when picking up items - Gut out CInv and CSha and replace it with CStash represented on the map - Remove the unused effect ActivateInv - Bump version, anticipating major inventory logic overhaul - Reword the MOwned item dialog mode blurbs - Simplify the header of the lore menus - Change meny keys / and ? to > and < - Update scenario names in the manual - Mention the ! key whenever KP_* is mentioned - Simplify and fix placement of --more- prompts - Change the AttrString word gluing operation and fix drawFrameStatus - Slightly fix speed calculation to agree with what's on the wiki - Fix a memory leak from tutorial hints repetition avoidance - Fix no frames displayed while resting - Fix missing dot at the end of taunt message - Fix heroes starting on exit in escape scenario - Fix the healing necklace better used from the backpack - Fix which are considered minor effects - Fix spurious double space suffix when rendring on blank - Fix mouse in area help, wrongly typeset with square font - Fix interruption message not appearing on screen, only in history - Fix wrong order of words in the detection effect message - Fix and simplify armor conditions message choice - Fix good conditions displayed in red - Fix broken combat description condition about armor - Fix not updated tutorial switch when game won - Fix a hint wrongly guessing the damage was piercing - Fix failures not stopping macro playback - Fix the lack of 'a' when hearing distant summoning - Fix using the reserved number 0 for faction ID - Fix backing up broken savegames - Fix a spurious space before a sentence ending dot - Fix lack of capitalization in verb messages - Fix genetic flaw activated at death - Fix a typo in identifier names - Fix first lines of message wrapped too late - Fix botched conditions for SfxFizzles, etc. - Fix includeMetaGame computed twice - Fix meta game identification not carrying through to the next game - Fix accumulating meta game item kinds - Fix scenarios with numbered actors but without continued team - Fix an optimization two lines too low in shuffleExcept - Fix a triple copy-pasto with Ability.MetaGame - Fix discoMetaGame for clients with state held by server - Fix displaying deflection when the perpetrator is not seen - Fix tutorial hints disabling - Fix a link to roguebasin - Fix short wrapping of adventure lore - Fix overflowing messages with more than 2 spaces - Fix 4 spaces message indentation with square font - Fix trash on fullscreen borders - Fix not showing aiming line when changing epsilon - Fix trying to describe an item that is not seen by the actor hit with it - Fix whitespace in displayRespSfxAtomicUI - Fix display of message log aligned to newest message - Fix extra SPACE needed to see history after game save command - Fix running broken due to vacuus MsgAtFeetMajor messages - Fix MsgStopPlayback not stopping running - Fix usage of two different widths in splitAttrString - Fix history lines starting with EOL - Fix monospace overlay whitespace spilling onto proportional - Fix wrongly enabled display of MsgRunStop message class - Fix deduplicating shown and saved messages separately - Fix, again, leftover UI clients killing frontend after already killed - Fix the numeric display of HP gain/loss - Fix wrong SDL wrapping markers when using mono fonts only - Fix a missing mouse command description - Fix Tasty tests broken by frontend fixes - Fix screensavers broken by UI faction not being the first - Fix a bug with two UI clients interspersing frames - Fix 'open main menu' command of Dashboard - Fix the GTK frontend again - Fix a syntax error in travis script - Fix license name - Fix linearInterpolation for inflated dungeon depths once more - Fix running disturbed by many boring tiles at open levels - Fix MsgItemMove messages not saved to history - Fix unintentional cap on actor generation level - Fix calling gameover and 'endgame' - Fix lack of EOL before initial separation line of history - Fix mouse coordinates in the browser - Fix assigning number symbols to hero faction projectiles - Fix and improve how deflections are displayed - Fix a 32bit system bug that intCast detected - Fix damage not displayed when other attributes are - Fix flinging from item menu cancelling item selection - Fix the comment about users able to overwrite keys - Fix a crash when impossible command chosen - Fix bitrot that corrupted curses frontend - Fix 'the fallen falling' projectiles - Fix wrong alignment of level percent seen - Fix autoexplore with changing crosshair - Fix goto not interrupted change of crosshair - Fix the valuation of OnUser effect - Fix the lack of the last backdrop line in the game greeting blurb - Fix memory leak in placesFromState - Fix double braced due the hack for skill-less yelling - Fix order of using weapons inconsistent with HUD - Fix actor that can't wait not ignored as a pointman even when inactive - Fix stash guard preferred as pointman - Fix AI leader choice to really prefer old leader and to avoid light more - Fix raid scenario starting with two faction close to escape - Fix random number out of range on 32bit JS - Fix teleport effect from stash item not identifying it - Fix warnings in gtk frontend - Fix the extra blank menu line starting too late - Fix section links in the game manual - Fix and simplify rules for disabling tile transformation - Fix missed blank prefixes of empty lines inside text - Fix and simplify projectiles activating and transforming terrain, again - Fix projectiles not able to lit up oil - Fix padded empty string overwriting UI elements - Fix the position of second column with square font - Fix trailing spaces in dashboard - Fix a trailing space in a message - Fix disable extra empty shadow line at the bottom of menus - Fix determining if item identified in permittedPrecious - Fix an actor pulling himself - Fix desynchronized copy-pasted actorVulnerable code - Fix history message display header - Fix Calm measurement code to match code documentation - Fix sleeping stash guard not considered a guard - Fix reaching escape from below when all levels explored - Fix hero AI sometimes not exploring levels fully - Fix weapon order of Smithhammer - Fix messages about melee-only armor deflecting missiles - Fix wrong condition, wrongly reducing fleeing behaviour - Fix comments about slack doctrine targets - Fix assertion failure when targetting a tile under oneself - Fix code documentation about inventory stores - Fix the unequipping failure message condition - Fix wrong cave reversal when generating dungeon - Fix non-pointman heroes meleeing healing geysers - Fix poisons never activating and never running out - Fix registering if altering failed due to bumping - Fix a bushy patch that can block starting actors - Fix diverging definitions of foes worth killing - Fix barrels not destroyed by bumping - Fix inability to open doors due to no embeds - Fix projectiles not to bump off, but to transform terrain - Fix the display of level in stash menu header - Fix alliance placing to put heroes over stairs - Fix a wrong message when displacing a waking foe - Fix AI not eating and not removing other bad conditions - Fix nested macros - Fix wrong game mode started due to only the first word inspected - Fix the result of AndEffect - Fix the warning when flinging benign items - Fix a loop when actor pushed to another level via stairs - Fix melee disrupting pushed flight - Fix unidentified weapons marked in HUD as without timeout - Fix modelling wear and tear with DestroyItem - Fix mixed up resistance conditions - Fix item dropping crashes when OnSmash effects remove them while dropped - Fix an AI loop when applying a recharging item - Fix some no-fence places not appearing in statistics - Fix display of empty lists of tools for terrain transformations - Fix referring in the server code to definitions from the client internals - Fix embedded items activated twice - Fix an attempt to consume more items than there exist - Fix broken running macro - Fix activating embeds - Fix repeating predefined macros - Fix unsafe recording - Fix in-game macro system - Fix discharging an item that is not recharged - Fix consumeItems missing in an export list - Fix missing spaces in describeToolsAlternative - Fix display of tool alternatives - Fix random results of sortEmbeds - Fix checks if actors in combat - Fix the semantics of Discharge effect - Fix a crash when updating invisible item timer - Fix altering skill check in the client - Fix omission of OpenWith when OpenTo is considered - Fix unidentified embedded items impossible to trigger - Fix usage of outdated state component when altering tiles - Fix an attempt to apply a used up embedded item - Fix the direction of < and > scrolling in ending screens - Fix rubble tile definition wrt order of activated features - Fix old actor body used after altering caused by collision with terrain - Fix wrong condition for alien captured at victory message - Fix moving only all or none items between containers - Fix crash when actor not visible after triggering an item - Fix crash when dopping previous may destroy next items - Fix projectile altering a tile too many times at once - Fix DropItem reporting no activation due to item vanishing earlier - Fix off-by-one when picking projectiles with enough range - Fix trying to destroy an empty item bag when modifying terrain with it - Fix assumption that if last actor is alien, game is won - Fix no identification message sometimes in the first turn - Fix compilation without EXPOSE_INTERNAL - Fix the trap of drain Calm item in equipment that can't be removed - Fix tiles never altered via walking on them - Fix articles incorrectly recognized in words that end similarly - Fix wrong calculation of number of non-durable weapons - Fix incorrect conjugation - Fix a corruption of backstory text - Fix first character of buttons sometimes overwritten by space - Fix wrongly displaying a special ending for small scenarios - Fix other frontends wrt engine and sdl2 frontend changes - Fix proportional overlay overwriting first character of mono one - Fix help setup for large screens - Fix broken item menu with square font - Fix wrong button initial position with square font - Fix distant overlays wrongly getting a highlighted line - Fix SDL frontend truncating proportional font lines - Fix and tweak measuring texts in various fonts - Fix empty menus in single font mode - Fix spurious ending prompt in single-font setup - Fix padding of line chunks overwriting subsequent chunks - Fix display of history - Fix buttons holding other locations of an item - Fix not shown highlight of prop font lines - Fix spurious empty line between header and menu - Fix use of fromAscList where fromDistinctAscList would do - Fix spacing in history display - Fix off-by-one crash in history - Fix mouse clicks on buttons in small font areas - Fix updateLine for the case of multiple overlays - Fix overrun in mouse help table - Fix history highlight restricted to 80 columns - Fix menu highlight splitting proportional text chunks - Fix proportional texts never wiped out - Fix horizontal starting points of message chunks - Fix a syntax error stemming from wrong CPP - Fix illegal containers creeping into item choice - Fix cmdAtomicSemSer for UpdMoveItem - Fix AI not sidestepping explosive tiles, even if it could - Fix AI not sidestepping nearby actors - Fix cmdAtomicSemSer in the presense of CStash that acts like CFloor - Fix broken atomic commands assigned wrong LevelId - Fix visible enemy stash position not updated, because foes not seen - Fix countless typos - Start using cabal-plan - Fix and improve Makefile, cabal file and CI scripts - Improve and update game manual and help texts wrt game changes - Tweak travis scripts and building docs in README ## [v0.9.5.0](https://github.com/LambdaHack/LambdaHack/compare/v0.9.4.0...v0.9.5.0) - Fix NumLock disabled in the browser - In screen reader frontend, highlight active menu line with the cursor - Clone the main main menu commands as map mode commands - Add C-RMB and C-S-LMB as alternatives of MMB - Announce prominently MMB binding for describing map positions - Clean up the default config file, keeping compatibility - Make scenario names longer and slighlty more informative - Make Vi movement keys the default in addition to keypad and mouse - Fix a bug where death prompt when autoplaying was capturing a keypress - Let ESC from main menu return to insert coin mode, if applicable - Make various small UI tweaks, especially to main menu and its submenu - Let main menu lines have 35, not 30, characters - Make the main menu ASCII art less intrusive (and easier for screen readers) - Don't invalidate the score file due to game minor (only) version bump ## [v0.9.4.0](https://github.com/LambdaHack/LambdaHack/compare/v0.9.3.0...v0.9.4.0) - In vty frontend highlight actors more - Clean up actor highlighting - Add yell/yawn to minimal command set, remove swerving the aiming line - Invoke yell/yawn exclusively with '%', due tor Windows and terminal woes - Move C-c command to C, not to mask C-c on console frontends - Tweak and fix vty console frontends, for screen-readers - React specially at gameover under certain special circumstances - Simpliy assignSlot now that slots are auto-sorted - Get rid of explicit item sorting; let lore and menu slots agree - Make DetectExit non-modal - Mark in a game end confirmation message that more treasure can be found - Add a description to the escape embedded item - Reword gameover text for raid scenario - Be more verbose when confirming escape from the game - Don't claim to summon, when not possible on this level - Fix missing 'no longer poisoned' when applying antidote - Don't ask confirmation for neutral (e.g., not IDed) items - Fix 'you fall down; you stand on a sword' - Prevent selecting regions via mouse down in web frontend - Deselect item if player declines to apply or fling - Hand-hold the player, warning if flung item is beneficial - Hand-hold the player, warning if applied item is harmful - Rewrite the condition in UI applyItem check - Improve the lobable item skill failure message - Let mouse buttons describe tiles, etc. - Unblock S-MouseButton in SDL2 frontend - Always describe things under mouse button - Make the message when hitting dead foe more varied ## [v0.9.3.0, aka 'Velvet smoking jacket'](https://github.com/LambdaHack/LambdaHack/compare/v0.8.3.0...v0.9.3.0) - Introduce message classes with configurable behaviour - Create a new 16x16 font and use it everywhere; tweak smaller fonts - Lock some levels or otherwise make ascending tricky - Add cooldown to most melee weapons, display that in HUD, adjust AI - Add per-scenario and per-outcome gameover messages in content - Add duplicate and reroll item effects in preparation for crafting - Add actor and item analytics as a preparation for XP gain quests - Implement piercing projectiles that may share a tile with a big actor - Increase the spawn speed now that monsters sleep a lot - Introduce actors falling asleep and yelling - Allow any level size and position - Mention places when looking at tiles and add place lore menu - Expand all kinds of content and rebalance - Create and rework all item, cave and plot prose (Dan Keefe @Peritract) - Make explosives in cramped spaces twice weaker - Tweak player fling command - Tweak equipping when equipment overfull - Start cycling stores at equipment since that's the one mentioned in help - Overhaul CI scripts - Restructure and clean up codebase - Extend balance debugging tools, using item and actor analytics, places, etc. - Drop the gameplay option that first death means defeat - Avoid idle-GC between slow keystrokes - Put content data into a compact region to limit GC - Remove the border around web frontend game screen; seems unneeded now - Don't draw aiming line nor path in vty frontend - Highlight xhair by cursor in vty frontend - Highlight player by cursor in vty frontend - Switch the default FPS to 24 for tradition's sake - Highlight current high score - Remove most stopPlayBack, now spurious, because message classes used - Overhaul cabal file: define common options, split into internal libraries - Fix confusion of nub and uniq - Rename short wait to lurk and many lurks to heed - Show a red message when HP or Calm dip too low or when foe appears - Lose Calm and so alert AI even at weakest non-zero HP draining attacks - Enable screenshots while in menus - Rename config options related to fonts - Recolour aiming line not to clash with the red crosshair - Exchange the functions of yellow and red highlight - Tweak all colours, in particular to differentiate blues/cyans - Cap bright colours at 85 CIELAB Lightness at D65 - Normalize dark colours to be between 42 and 57 CIELAB Lightness at D65 - Get rid of colorIsBold option; KISS - Tint white in alternating lines with different hue for long text readability - Don't split lines at articles - Set xhair to currently meleed foe to see his HP - Display speed on HUD; tweak status lines in other ways - Don't show description of leader target in HUD; TMI - Help AI flee in a consistent direction over many turns - Expose the save backup command, for browser games - Don't display target info when item selected - Let AI actors spawn even quite far from the player - Auto-select all new team members, to help new players - Replace O by zero on the map display; make zero distinct from O in all fonts - Flesh out the initial ? prompt - Add 'I' alias for pack-related commands, unless laptop key-scheme used - Turn off movementLaptopKeys by default not to confuse new players - Make sure AI attacks bosses even if distant and fleeing or non-moving - Lower bonus HP at extreme difficulty - Add a separate frame for each projectiles start - Don't go modal at the frequent and weak hidden tile detection effect - Make AI pick closest stairs more often - Let apply-unskilled actors activate embedded items - Don't boost damage by speed unless actor is projectile - If everything else fails, let AI flee by opening doors - Help AI actor prevent being dominated - Make computing gameplay benefit to items more accurate - Rename, clone and fine-tune effect Temporary - Simplify code and content by getting rid of Recharging effect - Let applying periodic items only produce the first effect - Tweak item detection to help in skipping boring level portions and in stealth - Invoke and display embedded items in the order specified in tile definitions - Let lit trails illuminate colonnades - Prevent an exploit for avoiding self-invoked firecrackers - Don't let AI attempt summoning if not enough Calm - Improve item label bracket codes in menus - Pick randomly destination stairs if teleporting level - Display the number of items in store - Summarize value of player loot in shared stash menu's header - Start history menu at the close-up of the last message - Make fast-dying insects aggressive - Overhaul game score DSL and particular scoring definitions in content - Add and extend messages, e.g., tell if victim blocks and with what armor - Extend and rework menu manipulation keys - Remove specialized quaff, read and throw commands; KISS - Split walls of text into more paragraphs and/or make them narrower - Extend and update help and manual - Don't let AI waste time looting distant lone projectiles - Make Enum instances of Point and Vector contiguous, hackily - Make dominated actor drop all his items, for ID and in case he defects ASAP - Try to find a non-waiting action, if better AI leader can't be found - Prevent summoning OoD actors - Let animals eat food and add several foods - Make Domination effect harder to activate - Let only actors and items with SkOdor property leave smell and add perfumes - Let spawning rate level out after a few dozen spawns - Describe smell, if present in an inspected tile - Let pushed actor fly after crashing a door open - Show passing time and heard events even if no actors in the UI faction - When movement impossible, describe the tile with SHIFT-direction - Catch and steal projectiles when braced instead of when weaponless - Let actors that are pushed perform any action in addition to movement - Improve deduplication of messages - When describing actor on map, tell if it has loot - Represent being braced as having an organ; also add other pseudo-organs - Overhaul hearing to facilitate triangulation based on sound cues - Prefer to spawn aquatic actors on aquatic tiles - Add swimming and flying skills and shallow water tile features - Boost/drain skills via many new items - Rework and extend skills and their effects as a preparation for XP rewards - Enable specifying each side of outer cave fence separately - Make definition of caves of a scenario more precise - Specify more properties of levels in content - Extend content validation - Improve placement and fitting stairs and rooms on levels - Don't hardwire level size - Simplify game rules content - Change the format of game client content - Fix an arbitrary delay in killing dying actors - Fix arbitrary branch of a corridor chosen when running - Fix bush patches blocking off a level's corner - Fix config file ignored at game reinit - Fix running disturbed by flavours of walls - Fix splitting lines one character too early - Fix Calm drain from nearby foes occurring only every other turn - Fix some AI looping movement, in particular when fleeing - Fix running into own periodic explosions, e.g., from necklaces - Fix 'she painfullies collide' - Fix AI with vector targets unwilling to change them - Fix crash when attempting to fling at a target on remote level - Fix wrong timestamps in history - Fix, again, various kinds of frames intruding between fadeout and fadein - Fix wrong pluralization of some item names, compound and exceptions - Fix disabled items benefit recalculation after item kind learned - Fix in many ways too close initial faction and item positions - Fix performance in many ways and places, particularly for JS translation - Fix missing perception updates, causing missed AI actions concerning us - Fix uninitialized sarenas, which was probably causing resume to change state - Fix weak AI actors fleeing even if enemy can't melee - Fix and optimize sifting free tiles for spawn/summon location - Fix various cases of excessive summoning - Fix recording of item first seen level - Fix many problems with item descriptions and other messages - Fix reporting of reduction and elimination of actor conditions - Fix reading and interpreting old format config files - Fix synced initial item timeouts and actor times, leading to artificial feel - Fix actors erratically following their leader - Fix lifts continuing as stars and the other way around - Fix various 32bit overflows - Fix other errors, probably not present or not visible in previous version ## [v0.8.3.0](https://github.com/LambdaHack/LambdaHack/compare/v0.8.1.2...v0.8.3.0) - Add a hack to run SDL2 on the main thread, fixing the OS X crash - Warn visually when impressed and Calm running low, risking domination - Display actor as red when low Calm and impressed or when low HP - Fix, complete and fine tune UI, AI and server skill and weapon checks - Fix a bug where item aspects look different to clients than to the server - Change the requirements for the main menu ASCII art ## [v0.8.1.2](https://github.com/LambdaHack/LambdaHack/compare/v0.8.1.1...v0.8.1.2) - Fix typos detected by lintian - Fix the code that runs in case of old async (bug introduced in v0.8.1.1) ## [v0.8.1.1](https://github.com/LambdaHack/LambdaHack/compare/v0.8.1.0...v0.8.1.1) - no player-visible changes - make it possible to compile with old async package - rewrite copyright information according to Debian format - make github display the correct main license ## [v0.8.1.0](https://github.com/LambdaHack/LambdaHack/compare/v0.8.0.0...v0.8.1.0) - no player-visible changes - significantly reduce RAM usage when compiling library - update and extend CI ## [v0.8.0.0, aka 'Explosive dashboard'](https://github.com/LambdaHack/LambdaHack/compare/v0.7.1.0...v0.8.0.0) - rework greying out menu items and permitting item application and projection - rework history collection; merge message repetitions more aggressively - display HP in red when below (configurable) warning threshold - tweak AI: actors remember they are fleeing; better leader choice, etc. - add to content specialized explosive projectiles; tune the effects - calculate loot score component based on fraction of dungeon loot collected - don't hardwire item price, but let it be specified in content - let all valuables glitter in the dark to avoid complete level exploration - teach AI to cure ailments and shake off impressions - rework detection effects; add detection of items embedded in tiles - automatically identify stolen items that only have minor effects - let projectiles hit each other if fragile and substantial enough - rework item kind identification code; change the way it's defined in content - make more item kinds (including some traps) secret - protect paralyzed actors with a stasis condition to avoid infinite paralysis - implement dumping screenshots in SDL2 and create animated GIFs in Makefile - generate most common consumables less often, but in depth-scaled bunches - make pushed actors alter tiles and trigger effects of embedded items - validate and cross-validate more content; reduce content creation boilerplate - make summoning more varied and prevent chain-summoning - add many ways to conditionally sequence effects - create large, merged rooms more often - generalize the terrain altering player command (C-c, mouse) - let RET, SPACE and ESC clear pending messages, if any - add dashboard with links to all menus and info screens - scale some organ and trap power with level depth - simplify level-scaled dice roll semantics - change scaled dice notation 'dl' to 'dL' for readability in-game - rebalance items and decrease dice variety to unclutter backpack - colour-code beneficial and harmful conditions in menu and in HUD - display item lore (also for organs, embedded items, explosions, etc.) - display embedded item descriptions as if they were tile descriptions - tweak blast visuals, lower particle counts, beautify their spread - tweak projectile visuals, e.g., display an extra frame when projectile dies - add intro screen and work on other ways to convey story - simplify a lot of code, including a bit of game rules - fix some bugs, tweak content, speed up some AI bottlenecks ## [v0.7.1.0, aka 'Ancient troubles'](https://github.com/LambdaHack/LambdaHack/compare/v0.7.0.0...v0.7.1.0) - add amazing cave and item (actor, blast, organ) descriptions - package for Windows as an installer and also as zip archives - fix a crash from SDL frontend under some OpenGL drivers (no thread-safety) - add WWW address to the Main Menu, for other sites that may run our JS blob ## [v0.7.0.0, aka 'The dice are cast'](https://github.com/LambdaHack/LambdaHack/compare/v0.6.2.0...v0.7.0.0) - decouple tile searching from tile alteration - refrain from identifying items that are not randomized - switch away from incapacitated leader to let others revive him - make rescue easier by not going into negative HP the first time - fix crowd of friends on another level slowing even actors that melee - fix missing report about items underneath an actor when changing levels - API breakage: change the syntax of dice in content - API addition: introduce cave descriptions - keep all client states in the server and optimize communication with clients - improve item choice for identification and item polymorphing - reset embedded items when altering tile - replace atomic command filtering with exception catching - reimplement dice as symbolic expressions inducing multiple RNG calls - switch to optparse-applicative and rewrite cli handling - add stack and cabal new-build project files - improve haddocks across the codebase ## [v0.6.2.0, aka 'Zoom out'](https://github.com/LambdaHack/LambdaHack/compare/v0.6.1.0...v0.6.2.0) - make fireworks slower and so easier to spot - make rattlesnake deeper but more common - announce no effect of activation - describe original and current faction of an actor - highlight dominated actors - mark organs with comma instead of percent and gems with dollar - make the healing cave dangerous to prevent camping - slightly balance various content - by default move item the same as last time - often spawn between heroes and stairs going deeper - fix totalUsefulness computation for negative effects - fix abandoning distant enemy target despite no alternatives - fix slow pushing of actors - fix a crash when many actors run towards stairs - hotfix: Pass zoom keys through to the browser - help players find the info about changing the font size - depend on GHC >= 8.0 and new vector - specialize client code already in SampleMonadClient.hs - enable StrictData in all modules - replace 'failure' with 'error' that now shows call stack ## [v0.6.1.0, aka 'Breaking one rule at a time'](https://github.com/LambdaHack/LambdaHack/compare/v0.6.0.0...v0.6.1.0) - fix redrawing after window minimized and restored - hack around vanishing texture on Windows - hack around SDL backends not thread-safe on Windows - the only breaking API change: specify font directory in game rules content - let the game use its own fonts, not fonts from the sample game in library - tweak some item creation to occur in character's pack, not on the ground - slightly balance various content - make sure the 'resolution' effect is not a drawback - make artifact weapon rarities more regular - avoid creating lit, open dungeon at the bottom, where foes have ranged weapons - number scenarios in user descriptions - correct, add and modify some in-game messages - let player hear unseen summonings performed by other actors - don't let actors hear blasts hitting walls, as opposed to hitting actors - when moving item out of shared stash, reset its timeouts - when ascending, shift timeouts of inventory as well - when creating item not on the ground, discover it - when dominating, auto-discover only if the item can't be discovered by use - let henchmen take into account their targets, as described in PLAYING.md - let only walkable tiles be explorable, for clear walls inside solid blocks - move to API 2.0.0 of sdl2-ttf and depend on corrected sdl2 (builds on Windows) - simplify code thanks to the new sdl2-ttf API - tweak travis scripts and building docs in README ## [v0.6.0.0, aka 'Too much to tell'](https://github.com/LambdaHack/LambdaHack/compare/v0.5.0.0...v0.6.0.0) - add and modify a lot of content: items, tiles, embedded items, scenarios - improve AI: targeting, stealth, moving in groups, item use, fleeing, etc. - make monsters more aggressive than animals - tie scenarios into a loose, optional storyline - add more level generators and more variety to room placement - make stairs not walkable and use them by bumping - align stair position on the levels they pass through - introduce noctovision - increase human vision to 12 so that normal speed missiles can be sidestepped - tweak and document weapon damage calculation - derive projectile damage mostly from their speed - make heavy projectiles better vs armor but easier to sidestep - improve hearing of unseen actions, actors and missiles impacts - let some missiles lit up on impact - make torches reusable flares and add blankets for dousing dynamic light - add detection effects and use them in items and tiles - make it possible to catch missiles, if not using weapons - make it possible to wait 0.1 of a turn, at the cost of no bracing - improve pathfinding, prefer less unknown, alterable and dark tiles on paths - slow down actors when acting at the same time, for speed with large factions - don't halve Calm at serious damage any more - eliminate alternative FOV modes, for speed - stop actors blocking FOV, for speed - let actor move diagonally to and from doors, for speed - improve blast (explosion) shapes visually and gameplay-wise - add SDL2 frontend and deprecate GTK frontend - add specialized square bitmap fonts and hack a scalable font - use middle dot instead of period on the map (except in teletype frontend) - add a browser frontend based on DOM, using ghcjs - improve targeting UI, e.g., cycle among items on the map - show an animation when actor teleports - add character stats menu and stat description texts - add item lore and organ lore menus - add a command to sort item slots and perform the sort at startup - add a single item manipulation menu and let it mark an item for later - make history display a menu and improve display of individual messages - display highscore dates according to the local timezone - make the help screen a menu, execute actions directly from it - rework the Main Menu - rework special positions highlight in all frontends - mark leader's target on the map (grey highlight) - visually mark currently chosen menu item and grey out impossible items - define mouse commands based on UI mode and screen area - let the game be fully playable only with mouse, use mouse wheel - pick menu items with mouse and with arrow keys - add more sanity checks for content - reorganize content in files to make rebasing on changed content easier - rework keybinding definition machinery - let clients, not the server, start frontends - version savefiles and move them aside if versions don't match - lots of bug fixes internal improvements and minor visual and text tweaks ## [v0.5.0.0, aka 'Halfway through space'](https://github.com/LambdaHack/LambdaHack/compare/v0.4.101.0...v0.5.0.0) - let AI put excess items in shared stash and use them out of shared stash - let UI multiple items pickup routine put items that don't fit into equipment into shared stash, if possible, not into inventory pack - re-enable the ability to hear close, invisible foes - add a few more AI and autonomous henchmen tactics (CTRL-T) - keep difficulty setting over session restart - change some game start keybindings - replace the Duel game mode with the Raid game mode - various bugfixes, minor improvements and balancing ## [v0.4.101.0, aka 'Officially fun'](https://github.com/LambdaHack/LambdaHack/compare/v0.4.100.0...v0.4.101.0) - the game is now officially fun to play - introduce unique boss monsters and unique artifact items - add animals that heal the player - let AI gang up, attempt stealth and react to player aggressiveness - spawn actors fast and close to the enemy - spawn actors less and less often on a given level, but with growing depth - prefer weapons with effects, if recharged - make the bracing melee bonus additive, not multiplicative - let explosions buffet actors around - make braced actors immune to translocation effects - use mouse for movement, actor selection, aiming - don't run straight with selected actors, but go-to cross-hair with them - speed up default frame rate, slow down projectiles visually - rework item manipulation UI - you can pick up many items at once and it costs only one turn - allow actors to apply and project from the shared stash - reverse messages shown in player diary - display actor organs and stats - split highscore tables wrt game modes - move score calculation formula to content - don't keep the default/example config file commented out; was misleading - I was naughty again and changed v0.5.0.0 of LambdaHack content API slightly one last time ## [v0.4.100.0, aka 'The last thaw'](https://github.com/LambdaHack/LambdaHack/compare/v0.4.99.0...v0.4.100.0) - unexpectedly thaw and freeze again v0.5.0.0 of LambdaHack content API - unexpectedly implement timeouts and temporary effects easily without FRP - make a couple of skill levels meaningful and tweak skills of some actors - make AI prefer exploration of easier levels - permit overfull HP and Calm - let non-projectile actors block view - make colorful characters bold (if it resizes your fonts, turn off via colorIsBold = False in config file or --noColorIsBold on commandline) - start the game with a screensaver safari mode - add i386 Linux and Windows compilation targets to Makefile ## [v0.4.99.0, aka 'Player escapes'](https://github.com/LambdaHack/LambdaHack/compare/v0.2.14...v0.4.99.0) - balance the example game content a bit (campaign still unbalanced) - various code and documentation tweaks and fixes - add cabal flag expose_internal that reveals internal library operations - merge FactionKind into ModeKind and rework completely the semantics - compatibility tweaks for Nixpkgs - define AI tactics, expose them to UI and add one more: follow-the-leader - share leader target between the UI and AI client of each faction - specify monster spawn rate per-cave - extend content validation and make it more user friendly - freeze v0.5.0.0 of LambdaHack content API ## [v0.2.14, aka 'Out of balance'](https://github.com/LambdaHack/LambdaHack/compare/v0.2.12...v0.2.14) - tons of new (unbalanced) content, content fields, effects and descriptions - add a simple cabal test in addition to make-test and travis-test - generate items and actors according to their rarities at various depths - redo weapon choice, combat bonuses and introduce armor - introduce skill levels for abilities (boolean for now, WIP) - remove regeneration, re-add through periodically activating items - ensure passable areas of randomly filled caves are well connected - make secondary factions leaderless - auto-tweak digital line epsilon to let projectiles evade obstacles - add shrapnel (explosions) and organs (body parts) - express actor kinds as item kinds (their trunk) - add dynamic lights through items, actors, projectiles - fix and improve item kind and item stats identification - make aspects additive from all equipment and organ items - split item effects into aspects, effects and item features - rework AI and structure it according to the Ability type - define Num instance for Dice to make writing it in content easier - remove the shared screen multiplayer mode and all support code, for now - rename all modules and nearly all other code entities - check and consume HP when calling friends and Calm when summoning - determine sight radius from items and cap it at current Calm/5 - introduce Calm; use to hear nearby enemies and limit item abuse before death - let AI actors manage items and share them with party members - completely revamp item manipulation UI - add a command to cede control to AI - separate actor inventory, 10-item actor equipment and shared party stash - vi movement keys (hjklyubn) are now disabled by default - new movement keyset: laptop movement keys (uk8o79jl) ## [v0.2.12](https://github.com/LambdaHack/LambdaHack/compare/v0.2.10...v0.2.12) - improve and simplify dungeon generation - simplify running and permit multi-actor runs - let items explode and generate shrapnel projectiles - add game difficulty setting (initial HP scaling right now) - allow recording, playing back and looping commands - implement pathfinding via per-actor BFS over the whole level - extend setting targets for actors in UI tremendously - implement autoexplore, go-to-target, etc., as macros - let AI use pathfinding, switch leaders, pick levels to swarm to - force level/leader changes on spawners (even when played by humans) - extend and redesign UI bottom status lines - get rid of CPS style monads, aborts and WriterT - benchmark and optimize the code, in particular using Data.Vector - split off and use the external library assert-failure - simplify config files and limit the number of external dependencies ## [v0.2.10](https://github.com/LambdaHack/LambdaHack/compare/v0.2.8...v0.2.10) - screensaver game modes (AI vs AI) - improved AI (can now climbs stairs, etc.) - multiple, multi-floor staircases - multiple savefiles - configurable framerate and combat animations ## [v0.2.8](https://github.com/LambdaHack/LambdaHack/compare/v0.2.6.5...v0.2.8) - cooperative and competitive multiplayer (shared-screen only in this version) - overhauled searching - rewritten engine code to have a single server that sends restricted game state updates to many fat clients, while a thin frontend layer multiplexes visuals from a subset of the clients ## [v0.2.6.5](https://github.com/LambdaHack/LambdaHack/compare/v0.2.6...v0.2.6.5) - this is a minor release, primarily intended to fix the broken haddock documentation on Hackage - changes since 0.2.6 are mostly unrelated to gameplay: - strictly typed config files split into UI and rules - a switch from Text to String throughout the codebase - use of the external library miniutter for English sentence generation ## [v0.2.6](https://github.com/LambdaHack/LambdaHack/compare/v0.2.1...v0.2.6) - the Main Menu - improved and configurable mode of squad combat ## [v0.2.1](https://github.com/LambdaHack/LambdaHack/compare/v0.2.0...v0.2.1) - missiles flying for three turns (by an old kosmikus' idea) - visual feedback for targeting - animations of combat and individual monster moves ## [v0.2.0](https://github.com/LambdaHack/LambdaHack/compare/release-0.1.20110918...v0.2.6) - the LambdaHack engine becomes a Haskell library - the LambdaHack game depends on the engine library LambdaHack-0.11.0.0/COPYLEFT0000644000000000000000000002734007346545000013245 0ustar0000000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: LambdaHack Upstream-Contact: Mikolaj Konarski Source: https://hackage.haskell.org/package/LambdaHack Files: * Copyright: 2008-2011 Andres Loeh 2010-2021 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 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 License: OFL-1.1 Files: GameDefinition/fonts/DejaVu*.woff Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. DejaVu changes are in public domain. License: bitstream-vera Comment: Bitstream Vera Sans Mono is Copyright Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera". . DejaVu modifications of the original Bitstream Vera Sans Mono typeface have been committed to the public domain. . The work in the Hack project is licensed under the MIT (Expat) License. . Copied from https://metadata.ftp-master.debian.org/changelogs//main/f/fonts-dejavu/fonts-dejavu_2.37-2_copyright Files: GameDefinition/fonts/Hack*.woff Copyright: 2003 Bitstream Inc. 2018 Christopher Simpkins License: Expat and bitstream-vera Comment: Bitstream Vera Sans Mono is Copyright Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera". . DejaVu modifications of the original Bitstream Vera Sans Mono typeface have been committed to the public domain. . The work in the Hack project is licensed under the MIT (Expat) License. . Copied from https://metadata.ftp-master.debian.org/changelogs//main/f/fonts-hack/fonts-hack_3.003-3_copyright Files: debian/* Copyright: held by the contributors mentioned in debian/changelog License: BSD-3-Clause License: BSD-3-Clause Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. . THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. License: GPL-2.0-or-later This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2, as published by the Free Software Foundation . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA . On Debian systems, the full text of the GNU General Public License version 2 can be found in the file `/usr/share/common-licenses/GPL-2'. License: OFL-1.1 SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 . PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. . The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. . DEFINITIONS Font Software refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. . Reserved Font Name refers to any names specified as such after the copyright statement(s). . Original Version refers to the collection of Font Software components as distributed by the Copyright Holder(s). . Modified Version refers to any derivative made by adding to, deleting, or substituting in part or in whole any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. . Author refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. . PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: . 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. . 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. . 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. . 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. . 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. . TERMINATION This license becomes null and void if any of the above conditions are not met. . DISCLAIMER THE FONT SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. License: bitstream-vera Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: . The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. . The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". . This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. . The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. . THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. . Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. LambdaHack-0.11.0.0/CREDITS0000644000000000000000000001143607346545000013114 0ustar0000000000000000All kinds of contributions to the LambdaHack engine are gratefully welcome! Some of the contributors are listed below, in chronological order. Andres Loeh Mikolaj Konarski Tuukka Turto Veronika Romashkina Daniel Keefe Pablo Reszczynski RafaÅ‚ Szczerski Oleg Grenrus Simon Michael bulbousBullfrog Alex Byaly Jamie Fristrom 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 Project (www.freetype.org). All rights reserved. Fonts 16x16xw.woff, 16x16xw.bdf, 16x16x.fnt, 8x8x.fnt and 8x8xb.fnt are are derived from fonts taken from https://github.com/angband/angband, copyrighted by Leon Marrick, Sheldon Simms III and Nick McConnell and released by them under GNU GPL, version 2 or any later version (confirmed at http://www.thangorodrim.net/development/opensource.html). Any further modifications by authors of LambdaHack are also released under GNU GPL version 2. Font 16x16xw.bdf is derived from 16x16x.fon by changing all but a few glyphs, converting to BDF format, extending character set and hacking the font file with bdftopcf and pcf2bdf to include full bitmaps, not only non-zero portions, for otherwise SDL2-ttf was not able to display the glyphs. Font 16x16xw.woff was derived from 16x16xw.bdf by changing format to TTF with bitsnpicas, faking descendent offsets to be 1 point lower to prevent freetype from adding an extra pixel to the descendent, tweaking with fontforge glyphs 3 5 6 8 A a S s b d h to prevent antialiasing of their vital parts when zoomed out, auto-hinting, manually simplifying hints in some glyphs and converting to WOFF format. Fonts BinarySansProLH-Regular.ttf.woff, BinarySansProLH-Semibold.ttf.woff and BinaryCodeProLH-Bold.ttf.woff are compiled from sources at https://github.com/adobe-fonts/source-sans-pro and https://github.com/adobe-fonts/source-code-pro published with the following copyright notice: Copyright 2010-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. This Font Software is licensed under the SIL Open Font License, Version 1.1. The sources are modified and processed as follows (see https://github.com/adobe-fonts/source-sans-pro/issues/193 for background): in Roman/Instances/Semibold/font.ufo/glyphs/s.glif put advance width="452" rm Roman/Instances/Semibold/font.ufo/data/com.adobe.type.processedHashMap bash build.sh pyftfeatfreeze -f 'cv03' -S -U LH -R 'Source/Binary' target/TTF/SourceSansPro-Semibold.ttf BinarySansProLH-Semibold.ttf pyftsubset BinarySansProLH-Semibold.ttf --unicodes="*" --flavor=woff --with-zopfli --output-file=BinarySansProLH-Semibold.ttf.woff and similarly for BinarySansProLH-Regular.ttf.woff, while for BinaryCodeProLH-Bold.ttf.woff the last steps are: pyftfeatfreeze -f 'cv02' -S -U LH -R 'Source/Binary' target/TTF/SourceCodePro-Bold.ttf BinaryCodeProLH-Bold.ttf pyftsubset BinaryCodeProLH-Bold.ttf --unicodes="*" --flavor=woff --with-zopfli --output-file=BinaryCodeProLH-Bold.ttf.woff Fonts DejaVuLGCSans.ttf.woff and DejaVuLGCSans-Bold.ttf.woff are downloaded from https://github.com/dejavu-fonts/dejavu-fonts and compresses with pyftsubset. Their copyright notice is DejaVu fonts 2.37 (c)2004-2016 DejaVu fonts team. Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. Font Hack-Bold.ttf.woff is downloaded from https://github.com/source-foundry/Hack and compresses with pyftsubset. Its copyright notice is Hack work is (c) 2018 Source Foundry Authors. MIT License. Bitstream Vera Sans Mono (c) 2003 Bitstream, Inc. (with Reserved Font Names _Bitstream_ and _Vera_). Bitstream Vera License. The Ubuntu Font Family fonts are currently removed, because they are a little too tall (some glyphs on consecutive lines are touching) and also considered non-free by Debian and so we can't include them in Debian and some other GNU/Linux distributions anyway. If there is sufficient interest, they may be brought back. They were fonts ubuntu-v14-latin-ext_latin-regular.ttf.woff, ubuntu-v14-latin-ext_latin-500.ttf.woff and ubuntu-mono-v9-latin-ext_latin-700.ttf.woff that were generated via google-webfonts-helper.herokuapp.com from 'Ubuntu Font Family' fonts with the following copyright notice: Copyright 2011 Canonical Ltd. Licensed under the Ubuntu Font Licence 1.0 The files were compressed with pyftsubset to save some space and make sure they are created from TTF, not coverted OTF fonts. Unfortunately, being TTF fonts and having broken hinting in the mono variant, they require forcing TTF.Light hinting in the SDL frontend. If ever OTF fonts emerge, they should be used instead (similarly compressed). LambdaHack-0.11.0.0/GameDefinition/Content/0000755000000000000000000000000007346545000016363 5ustar0000000000000000LambdaHack-0.11.0.0/GameDefinition/Content/CaveKind.hs0000644000000000000000000006011207346545000020403 0ustar0000000000000000-- | Definitions of of cave kinds. Every level in the game is an instantiated -- cave kind. module Content.CaveKind ( -- * Group name patterns pattern CAVE_ROGUE, pattern CAVE_ARENA, pattern CAVE_SMOKING, pattern CAVE_LABORATORY, pattern CAVE_NOISE, pattern CAVE_MINE, pattern CAVE_EMPTY, pattern CAVE_SHALLOW_ROGUE, pattern CAVE_OUTERMOST, pattern CAVE_RAID, pattern CAVE_BRAWL, pattern CAVE_SHOOTOUT, pattern CAVE_HUNT, pattern CAVE_FLIGHT, pattern CAVE_ZOO, pattern CAVE_AMBUSH, pattern CAVE_BATTLE, pattern CAVE_SAFARI_1, pattern CAVE_SAFARI_2, pattern CAVE_SAFARI_3 , groupNamesSingleton, groupNames , -- * Content content ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Ratio import Game.LambdaHack.Content.CaveKind import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Content.ItemKind hiding (content, groupNames, groupNamesSingleton) import Content.ItemKindActor import Content.PlaceKind hiding (content, groupNames, groupNamesSingleton) import Content.TileKind hiding (content, groupNames, groupNamesSingleton) -- * Group name patterns groupNamesSingleton :: [GroupName CaveKind] groupNamesSingleton = [] groupNames :: [GroupName CaveKind] groupNames = [CAVE_ROGUE, CAVE_ARENA, CAVE_SMOKING, CAVE_LABORATORY, CAVE_NOISE, CAVE_MINE, CAVE_EMPTY, CAVE_SHALLOW_ROGUE, CAVE_OUTERMOST, CAVE_RAID, CAVE_BRAWL, CAVE_SHOOTOUT, CAVE_HUNT, CAVE_FLIGHT, CAVE_ZOO, CAVE_AMBUSH, CAVE_BATTLE, CAVE_SAFARI_1, CAVE_SAFARI_2, CAVE_SAFARI_3] pattern CAVE_ROGUE, CAVE_ARENA, CAVE_SMOKING, CAVE_LABORATORY, CAVE_NOISE, CAVE_MINE, CAVE_EMPTY, CAVE_SHALLOW_ROGUE, CAVE_OUTERMOST, CAVE_RAID, CAVE_BRAWL, CAVE_SHOOTOUT, CAVE_HUNT, CAVE_FLIGHT, CAVE_ZOO, CAVE_AMBUSH, CAVE_BATTLE, CAVE_SAFARI_1, CAVE_SAFARI_2, CAVE_SAFARI_3 :: GroupName CaveKind pattern CAVE_ROGUE = GroupName "caveRogue" pattern CAVE_ARENA = GroupName "caveArena" pattern CAVE_SMOKING = GroupName "caveSmoking" pattern CAVE_LABORATORY = GroupName "caveLaboratory" pattern CAVE_NOISE = GroupName "caveNoise" pattern CAVE_MINE = GroupName "caveMine" pattern CAVE_EMPTY = GroupName "caveEmpty" pattern CAVE_SHALLOW_ROGUE = GroupName "caveShallowRogue" pattern CAVE_OUTERMOST = GroupName "caveOutermost" pattern CAVE_RAID = GroupName "caveRaid" pattern CAVE_BRAWL = GroupName "caveBrawl" pattern CAVE_SHOOTOUT = GroupName "caveShootout" pattern CAVE_HUNT = GroupName "caveHunt" pattern CAVE_FLIGHT = GroupName "caveFlight" pattern CAVE_ZOO = GroupName "caveZoo" pattern CAVE_AMBUSH = GroupName "caveAmbush" pattern CAVE_BATTLE = GroupName "caveBattle" pattern CAVE_SAFARI_1 = GroupName "caveSafari1" pattern CAVE_SAFARI_2 = GroupName "caveSafari2" pattern CAVE_SAFARI_3 = GroupName "caveSafari3" -- * Content content :: [CaveKind] content = [rogue, arena, smoking, laboratory, noise, mine, empty, outermost, shallowRogue, raid, brawl, shootout, hunt, flight, zoo, ambush, battle, safari1, safari2, safari3] rogue, arena, smoking, laboratory, noise, mine, empty, outermost, shallowRogue, raid, brawl, shootout, hunt, flight, zoo, ambush, battle, safari1, safari2, safari3 :: CaveKind -- * Underground caves; most of mediocre height and size rogue = CaveKind { cname = "A maze of twisty passages" , cfreq = [(DEFAULT_RANDOM, 100), (CAVE_ROGUE, 1)] , cXminSize = 80 , cYminSize = 21 , ccellSize = DiceXY (2 `d` 4 + 10) 6 , cminPlaceSize = DiceXY (2 `d` 2 + 4) 5 , cmaxPlaceSize = DiceXY 16 40 , cdarkOdds = 1 `d` 54 + 1 `dL` 20 -- most rooms lit, to compensate for dark corridors , cnightOdds = 51 -- always night , cauxConnects = 1%2 , cmaxVoid = 1%6 , cdoorChance = 3%4 , copenChance = 1%5 , chidden = 7 , cactorCoeff = 70 -- the maze requires time to explore , cactorFreq = [(MONSTER, 60), (ANIMAL, 40)] , citemNum = 6 `d` 5 + 10 - 10 `dL` 1 -- deep down quality over quantity; generally quite random, -- for interesting replays at the cost of unreliable balance , citemFreq = [(IK.COMMON_ITEM, 40), (IK.TREASURE, 60)] , cplaceFreq = [(ROGUE, 1)] , cpassable = False , clabyrinth = False , cdefTile = FILLER_WALL , cdarkCorTile = FLOOR_CORRIDOR_DARK , clitCorTile = FLOOR_CORRIDOR_LIT , cwallTile = FILLER_WALL , ccornerTile = FILLER_WALL , cfenceTileN = S_BASIC_OUTER_FENCE , cfenceTileE = S_BASIC_OUTER_FENCE , cfenceTileS = S_BASIC_OUTER_FENCE , cfenceTileW = S_BASIC_OUTER_FENCE , cfenceApart = False , cminStairDist = 20 , cmaxStairsNum = 1 + 1 `d` 2 , cescapeFreq = [] , cstairFreq = [ (WALLED_STAIRCASE, 50), (OPEN_STAIRCASE, 50) , (TINY_STAIRCASE, 1) ] , cstairAllowed = [] , cskip = [] , cinitSleep = InitSleepPermitted , cdesc = "Winding tunnels stretch into the dark." } -- no lit corridors cave alternative, since both lit # and . look bad here arena = rogue { cname = "Dusty underground library" , cfreq = [(DEFAULT_RANDOM, 60), (CAVE_ARENA, 1)] , cXminSize = 50 , cYminSize = 21 , ccellSize = DiceXY (3 `d` 3 + 17) (1 `d` 3 + 4) , cminPlaceSize = DiceXY (2 `d` 2 + 4) 6 , cmaxPlaceSize = DiceXY 16 12 , cdarkOdds = 49 + 1 `d` 10 -- almost all rooms dark (1 in 10 lit) -- Light is not too deadly, because not many obstructions and so -- foes visible from far away and few foes have ranged combat -- at shallow depth. , cnightOdds = 0 -- always day , cauxConnects = 1 , cmaxVoid = 1%8 , chidden = 0 , cactorCoeff = 70 -- small open level, don't rush the player , cactorFreq = [(MONSTER, 30), (ANIMAL, 70)] , citemNum = 4 `d` 5 -- few rooms , citemFreq = [ (IK.COMMON_ITEM, 20), (IK.TREASURE, 40) , (IK.ANY_SCROLL, 40) ] , cplaceFreq = [(ARENA, 1)] , cpassable = True , cdefTile = ARENA_SET_LIT , cdarkCorTile = TRAIL_LIT -- let trails give off light , clitCorTile = TRAIL_LIT -- may be rolled different than the above , cminStairDist = 15 , cmaxStairsNum = 1 `d` 2 , cstairFreq = [ (WALLED_STAIRCASE, 20), (CLOSED_STAIRCASE, 80) , (TINY_STAIRCASE, 1) ] , cinitSleep = InitSleepAlways , cdesc = "The shelves groan with dusty books and tattered scrolls. Subtle snoring can be heard from a distance." } smoking = arena { cname = "Smoking rooms" , cfreq = [(CAVE_SMOKING, 1)] , cdarkOdds = 41 + 1 `d` 10 -- almost all rooms lit (1 in 10 dark) -- Trails provide enough light for fun stealth. , cnightOdds = 51 -- always night , citemNum = 6 `d` 5 -- rare, so make it exciting , citemFreq = [(IK.COMMON_ITEM, 20), (IK.TREASURE, 40), (IK.ANY_GLASS, 40)] , cdefTile = ARENA_SET_DARK , cdesc = "Velvet couches exude the strong smell of tobacco." } laboratory = rogue { cname = "Burnt laboratory" , cfreq = [(CAVE_LABORATORY, 1)] , cXminSize = 60 , cYminSize = 21 , ccellSize = DiceXY (1 `d` 2 + 5) 6 , cminPlaceSize = DiceXY 7 5 , cmaxPlaceSize = DiceXY 10 40 , cnightOdds = 0 -- always day so that the corridor smoke is lit , cauxConnects = 1%5 , cmaxVoid = 1%10 , cdoorChance = 1 , copenChance = 1%2 , cactorFreq = [(MONSTER, 30), (ANIMAL, 70)] , citemNum = 6 `d` 5 -- reward difficulty , citemFreq = [ (IK.COMMON_ITEM, 20), (IK.TREASURE, 40) , (IK.EXPLOSIVE, 40) ] , cplaceFreq = [(LABORATORY, 1)] , cdarkCorTile = LAB_TRAIL_LIT -- let lab smoke give off light always , clitCorTile = LAB_TRAIL_LIT , cmaxStairsNum = 2 , cstairFreq = [ (WALLED_STAIRCASE, 50), (OPEN_STAIRCASE, 50) , (TINY_STAIRCASE, 1) ] , cdesc = "Shattered glassware and the sharp scent of spilt chemicals show that something terrible happened here." } noise = rogue { cname = "Leaky burrowed sediment" , cfreq = [(DEFAULT_RANDOM, 30), (CAVE_NOISE, 1)] , cXminSize = 50 , cYminSize = 21 , ccellSize = DiceXY (3 `d` 5 + 12) 6 , cminPlaceSize = DiceXY 8 5 , cmaxPlaceSize = DiceXY 20 20 , cdarkOdds = 51 -- Light is deadly, because nowhere to hide and pillars enable spawning -- very close to heroes. , cnightOdds = 0 -- harder variant, but looks cheerful , cauxConnects = 1%10 , cmaxVoid = 1%100 , cdoorChance = 1 -- to avoid lit quasi-door tiles , chidden = 0 , cactorCoeff = 100 -- the maze requires time to explore; also, small , cactorFreq = [(MONSTER, 80), (ANIMAL, 20)] , citemNum = 6 `d` 5 -- an incentive to explore the labyrinth , cpassable = True , cplaceFreq = [(NOISE, 1)] , clabyrinth = True , cdefTile = NOISE_SET_LIT , cfenceApart = True -- ensures no cut-off parts from collapsed , cdarkCorTile = DAMP_FLOOR_DARK , clitCorTile = DAMP_FLOOR_LIT , cminStairDist = 15 , cstairFreq = [ (CLOSED_STAIRCASE, 50), (OPEN_STAIRCASE, 50) , (TINY_STAIRCASE, 1) ] , cinitSleep = InitSleepBanned , cdesc = "Soon, these passages will be swallowed up by the mud." } mine = noise { cname = "Frozen derelict mine" , cfreq = [(CAVE_MINE, 1)] , cnightOdds = 51 -- easier variant, but looks sinister , citemNum = 10 `d` 4 -- an incentive to explore the final labyrinth , citemFreq = [(IK.COMMON_ITEM, 20), (GEM, 20)] -- can't be "valuable" or template items generated , cplaceFreq = [(NOISE, 1), (MINE, 99)] , clabyrinth = True , cdefTile = POWER_SET_DARK , cstairFreq = [ (GATED_CLOSED_STAIRCASE, 50) , (GATED_OPEN_STAIRCASE, 50) , (GATED_TINY_STAIRCASE, 1) ] , cinitSleep = InitSleepBanned , cdesc = "Pillars of shining ice create a frozen labyrinth." } empty = rogue { cname = "Tall cavern" , cfreq = [(CAVE_EMPTY, 1)] , ccellSize = DiceXY (2 `d` 2 + 11) (1 `d` 2 + 8) , cminPlaceSize = DiceXY 13 11 , cmaxPlaceSize = DiceXY 37 31 -- favour large rooms , cdarkOdds = 1 `d` 100 + 1 `dL` 100 , cnightOdds = 0 -- always day , cauxConnects = 3%2 , cmaxVoid = 0 -- too few rooms to have void and fog common anyway , cdoorChance = 0 , copenChance = 0 , chidden = 0 , cactorCoeff = 8 , cactorFreq = [(ANIMAL, 10), (IMMOBILE_ANIMAL, 90)] -- The healing geysers on lvl 3 act like HP resets. Needed to avoid -- cascading failure, if the particular starting conditions were -- very hard. Items are not reset, even if they are bad, which provides -- enough of a continuity. Gyesers on lvl 3 are not OP and can't be -- abused, because they spawn less and less often and also HP doesn't -- effectively accumulate over max. , citemNum = 4 `d` 5 -- few rooms and geysers are the boon , cplaceFreq = [(EMPTY, 1)] , cpassable = True , cdefTile = EMPTY_SET_LIT , cdarkCorTile = FLOOR_ARENA_DARK , clitCorTile = FLOOR_ARENA_LIT , cminStairDist = 30 , cmaxStairsNum = 1 , cstairFreq = [ (WALLED_STAIRCASE, 20), (CLOSED_STAIRCASE, 80) , (TINY_STAIRCASE, 1) ] , cdesc = "Swirls of warm fog fill the air, the hiss of geysers sounding all around." } outermost = shallowRogue { cname = "Cave entrance" , cfreq = [(CAVE_OUTERMOST, 100)] , cXminSize = 40 , cYminSize = 21 , cdarkOdds = 0 -- all rooms lit, for a gentle start , cactorCoeff = 100 -- already animals start there; also, pity on the noob , cactorFreq = filter ((/= MONSTER) . fst) $ cactorFreq rogue , citemNum = 12 `d` 2 -- lure them in with loot; relatively consisten , citemFreq = filter ((/= IK.TREASURE) . fst) $ citemFreq rogue , cminStairDist = 10 -- distance from the escape , cmaxStairsNum = 1 -- simplify at the start , cescapeFreq = [(INDOOR_ESCAPE_UP, 1)] , cdesc = "This close to the surface, the sunlight still illuminates the dungeon." } shallowRogue = rogue { cfreq = [(CAVE_SHALLOW_ROGUE, 100)] , cXminSize = 60 , cYminSize = 21 , cmaxStairsNum = 1 -- simplify at the start , cdesc = "The snorts and grunts of savage beasts can be clearly heard." } -- * Overground "caves"; no story-wise limits wrt height and size raid = rogue { cname = "Typing den" , cfreq = [(CAVE_RAID, 1)] , cXminSize = 50 , cYminSize = 21 , ccellSize = DiceXY (2 `d` 4 + 6) 6 , cminPlaceSize = DiceXY (2 `d` 2 + 4) 5 , cmaxPlaceSize = DiceXY 16 20 , cdarkOdds = 0 -- all rooms lit, for a gentle start , cmaxVoid = 1%10 , cdoorChance = 1 -- make sure enemies not seen on turn 1 , copenChance = 0 -- make sure enemies not seen on turn 1 , cactorCoeff = 300 -- deep level with no kit, so slow spawning , cactorFreq = [(ANIMAL, 100)] , citemNum = 18 -- first tutorial mode, so make it consistent , citemFreq = [ (IK.COMMON_ITEM, 100), (IK.S_CURRENCY, 500) , (STARTING_WEAPON, 100) ] , cmaxStairsNum = 0 , cescapeFreq = [(INDOOR_ESCAPE_UP, 1)] , cstairFreq = [] , cstairAllowed = [] , cdesc = "Mold spreads across the walls and scuttling sounds can be heard in the distance." } brawl = rogue -- many random solid tiles, to break LOS, since it's a day -- and this scenario is not focused on ranged combat; -- also, sanctuaries against missiles in shadow under trees { cname = "Sunny woodland" , cfreq = [(CAVE_BRAWL, 1)] , cXminSize = 60 , cYminSize = 21 , ccellSize = DiceXY (2 `d` 5 + 5) 6 , cminPlaceSize = DiceXY 3 3 , cmaxPlaceSize = DiceXY 7 5 , cdarkOdds = 51 , cnightOdds = 0 , cdoorChance = 1 , copenChance = 0 , chidden = 0 , cactorFreq = [] , citemNum = 4 `d` 6 , citemFreq = [ (IK.COMMON_ITEM, 50), (STARTING_WEAPON, 100) , (STARTING_ARMOR, 100) ] , cplaceFreq = [(BRAWL, 1)] , cpassable = True , cdefTile = BRAWL_SET_LIT , cdarkCorTile = DIRT_LIT , clitCorTile = DIRT_LIT , cstairFreq = [] , cfenceTileN = OUTDOOR_OUTER_FENCE , cfenceTileE = OUTDOOR_OUTER_FENCE , cfenceTileS = OUTDOOR_OUTER_FENCE , cfenceTileW = OUTDOOR_OUTER_FENCE , cmaxStairsNum = 0 , cdesc = "Sunlight falls through the trees and dapples on the ground." } shootout = rogue -- a scenario with strong missiles; -- few solid tiles, but only translucent tiles or walkable -- opaque tiles, to make scouting and sniping more interesting -- and to avoid obstructing view too much, since this -- scenario is about ranged combat at long range { cname = "Misty meadow" , cfreq = [(CAVE_SHOOTOUT, 1)] , ccellSize = DiceXY (1 `d` 2 + 6) 6 , cminPlaceSize = DiceXY 3 3 , cmaxPlaceSize = DiceXY 4 4 , cdarkOdds = 51 , cnightOdds = 0 , cauxConnects = 1%10 , cdoorChance = 1 , copenChance = 0 , chidden = 0 , cactorFreq = [] , citemNum = 5 `d` 16 -- less items in inventory, more to be picked up, -- to reward explorer and aggressor and punish camper , citemFreq = [ (IK.COMMON_ITEM, 30) , (ANY_ARROW, 400), (HARPOON, 300), (IK.EXPLOSIVE, 50) ] -- Many consumable buffs are needed in symmetric maps -- so that aggressor prepares them in advance and camper -- needs to waste initial turns to buff for the defence. , cplaceFreq = [(SHOOTOUT, 1)] , cpassable = True , cdefTile = SHOOTOUT_SET_LIT , cdarkCorTile = DIRT_LIT , clitCorTile = DIRT_LIT , cstairFreq = [] , cfenceTileN = OUTDOOR_OUTER_FENCE , cfenceTileE = OUTDOOR_OUTER_FENCE , cfenceTileS = OUTDOOR_OUTER_FENCE , cfenceTileW = OUTDOOR_OUTER_FENCE , cmaxStairsNum = 0 , cdesc = "The warmth has released fog and the wind brooms it away." } hunt = rogue -- a scenario with strong missiles for ranged and shade for melee { cname = "Afternoon swamp" , cfreq = [(CAVE_HUNT, 1)] , ccellSize = DiceXY (1 `d` 2 + 6) 6 , cminPlaceSize = DiceXY 3 3 , cmaxPlaceSize = DiceXY 4 4 , cdarkOdds = 51 , cnightOdds = 0 , cauxConnects = 1%10 , cdoorChance = 1 , copenChance = 0 , chidden = 0 , cactorCoeff = 400 -- spawn slowly , cactorFreq = [(INSECT, 100)] , citemNum = 5 `d` 10 , citemFreq = [ (IK.COMMON_ITEM, 30) , (ANY_ARROW, 400), (HARPOON, 300), (IK.EXPLOSIVE, 50) ] , cplaceFreq = [(BRAWL, 50), (SHOOTOUT, 100)] , cpassable = True , cdefTile = SHOOTOUT_SET_LIT , cdarkCorTile = DIRT_LIT , clitCorTile = DIRT_LIT , cstairFreq = [] , cfenceTileN = OUTDOOR_OUTER_FENCE , cfenceTileE = OUTDOOR_OUTER_FENCE , cfenceTileS = OUTDOOR_OUTER_FENCE , cfenceTileW = OUTDOOR_OUTER_FENCE , cmaxStairsNum = 0 , cdesc = "Tired after the day's heat, the insects gather strength in their hiding places." } flight = rogue -- a scenario with weak missiles, because heroes don't depend -- on them; dark, so solid obstacles are to hide from missiles, -- not view; obstacles are not lit, to frustrate the AI; -- lots of small lights to cross, to have some risks { cname = "Metropolitan park at dusk" -- "night" didn't fit , cfreq = [(CAVE_FLIGHT, 1)] , ccellSize = DiceXY (1 `d` 3 + 7) 6 , cminPlaceSize = DiceXY 5 3 , cmaxPlaceSize = DiceXY 9 9 -- bias towards larger lamp areas , cdarkOdds = 51 -- rooms always dark so that fence not visible from afar , cnightOdds = 51 -- always night , cauxConnects = 2 -- many lit trails, so easy to aim , cmaxVoid = 1%100 , chidden = 0 , cactorFreq = [] , citemNum = 6 `d` 8 , citemFreq = [ (IK.COMMON_ITEM, 30), (GEM, 500) , (WEAK_ARROW, 500), (HARPOON, 400) , (IK.EXPLOSIVE, 100) ] , cplaceFreq = [(FLIGHT, 1)] , cpassable = True , cdefTile = FLIGHT_SET_DARK -- unlike in ambush, tiles not burning yet , cdarkCorTile = SAFE_TRAIL_LIT -- let trails give off light , clitCorTile = SAFE_TRAIL_LIT , cfenceTileN = OUTDOOR_OUTER_FENCE , cfenceTileE = OUTDOOR_OUTER_FENCE , cfenceTileS = OUTDOOR_OUTER_FENCE , cfenceTileW = OUTDOOR_OUTER_FENCE , cmaxStairsNum = 0 , cescapeFreq = [(OUTDOOR_ESCAPE_DOWN, 1)] , cstairFreq = [] , cskip = [] , cdesc = "The darkening greyness is settling into silence." } zoo = rogue -- few lights and many solids, to help the less numerous heroes { cname = "Menagerie in flames" , cfreq = [(CAVE_ZOO, 1)] , ccellSize = DiceXY (1 `d` 3 + 7) 6 , cminPlaceSize = DiceXY 4 4 , cmaxPlaceSize = DiceXY 12 5 , cdarkOdds = 51 -- rooms always dark so that fence not visible from afar , cnightOdds = 51 -- always night , cauxConnects = 1%4 , cmaxVoid = 1%20 , cdoorChance = 7%10 , copenChance = 9%10 , chidden = 0 , cactorFreq = [] , citemNum = 7 `d` 8 , citemFreq = [ (IK.COMMON_ITEM, 100), (LIGHT_ATTENUATOR, 1000) , (STARTING_WEAPON, 1000) ] , cplaceFreq = [(ZOO, 1)] , cpassable = True , cdefTile = ZOO_SET_DARK , cdarkCorTile = SAFE_TRAIL_LIT -- let trails give off light , clitCorTile = SAFE_TRAIL_LIT , cstairFreq = [] , cfenceTileN = OUTDOOR_OUTER_FENCE , cfenceTileE = OUTDOOR_OUTER_FENCE , cfenceTileS = OUTDOOR_OUTER_FENCE , cfenceTileW = OUTDOOR_OUTER_FENCE , cmaxStairsNum = 0 , cdesc = "The night is filled with animal calls." } ambush = rogue -- a scenario with strong missiles; -- dark, so solid obstacles are to hide from missiles, -- not view, and they are all lit, because stopped missiles -- are frustrating, while a few LOS-only obstacles are not lit; -- few small lights to cross, giving a chance to snipe; -- crucial difference wrt shootout and hunt is that trajectories -- of missiles are usually not seen, so enemy can't be guessed; -- camping doesn't pay off, because enemies can sneak and only -- active scouting, throwing flares and shooting discovers them { cname = "Burning metropolitan park" , cfreq = [(CAVE_AMBUSH, 1)] , ccellSize = DiceXY (1 `d` 4 + 7) 6 , cminPlaceSize = DiceXY 5 3 , cmaxPlaceSize = DiceXY 9 9 -- bias towards larger lamp areas , cdarkOdds = 51 -- rooms always dark so that fence not visible from afar , cnightOdds = 51 -- always night , cauxConnects = 1%10 -- few lit trails, so hard to aim , chidden = 0 , cactorFreq = [] , citemNum = 5 `d` 8 , citemFreq = [ (IK.COMMON_ITEM, 30) , (ANY_ARROW, 400), (HARPOON, 300), (IK.EXPLOSIVE, 50) ] , cplaceFreq = [(AMBUSH, 1)] , cpassable = True , cdefTile = AMBUSH_SET_DARK , cdarkCorTile = TRAIL_LIT -- let trails give off light , clitCorTile = TRAIL_LIT , cstairFreq = [] , cfenceTileN = OUTDOOR_OUTER_FENCE , cfenceTileE = OUTDOOR_OUTER_FENCE , cfenceTileS = OUTDOOR_OUTER_FENCE , cfenceTileW = OUTDOOR_OUTER_FENCE , cmaxStairsNum = 0 , cdesc = "Fires have reached into the city, glowing in darkness." } -- * Other caves; testing, Easter egg, future work battle = rogue -- few lights and many solids, to help the less numerous heroes { cname = "Old battle ground" , cfreq = [(CAVE_BATTLE, 1)] , ccellSize = DiceXY (5 `d` 3 + 11) 5 -- cfenceApart results in 2 rows , cminPlaceSize = DiceXY 4 4 , cmaxPlaceSize = DiceXY 9 7 , cdarkOdds = 0 , cnightOdds = 51 -- always night , cauxConnects = 1%4 , cmaxVoid = 1%20 , cdoorChance = 2%10 , copenChance = 9%10 , chidden = 0 , cactorFreq = [] , citemNum = 5 `d` 8 , citemFreq = [(IK.COMMON_ITEM, 100), (LIGHT_ATTENUATOR, 200)] , cplaceFreq = [(BATTLE, 50), (ROGUE, 50)] , cpassable = True , cdefTile = BATTLE_SET_DARK , cdarkCorTile = SAFE_TRAIL_LIT -- let trails give off light , clitCorTile = SAFE_TRAIL_LIT , cfenceTileN = OUTDOOR_OUTER_FENCE , cfenceTileE = OUTDOOR_OUTER_FENCE , cfenceTileS = OUTDOOR_OUTER_FENCE , cfenceTileW = OUTDOOR_OUTER_FENCE , cfenceApart = True -- ensures no cut-off parts from collapsed , cmaxStairsNum = 0 , cstairFreq = [] , cdesc = "Eroded walls, rusted weapons and unidentifiable bones cruch underfoot all alike." } safari1 = brawl { cname = "Hunam habitat" , cfreq = [(CAVE_SAFARI_1, 1)] , cminPlaceSize = DiceXY 5 3 , cmaxStairsNum = 1 , cstairFreq = [ (OUTDOOR_WALLED_STAIRCASE, 20) , (OUTDOOR_CLOSED_STAIRCASE, 80) , (OUTDOOR_TINY_STAIRCASE, 1) ] , cskip = [0] , cdesc = "\"Act 1. Hunams scavenge in a forest in their usual disgusting way.\"" } safari2 = flight -- lamps instead of trees, but ok, it's only a simulation { cname = "Deep into the jungle" , cfreq = [(CAVE_SAFARI_2, 1)] , cmaxStairsNum = 1 , cescapeFreq = [] , cstairFreq = [ (OUTDOOR_WALLED_STAIRCASE, 20) , (OUTDOOR_CLOSED_STAIRCASE, 80) , (OUTDOOR_TINY_STAIRCASE, 1) ] , cskip = [0] , cdesc = "\"Act 2. In the dark pure heart of the jungle noble animals roam freely.\"" } safari3 = zoo -- glass rooms, but ok, it's only a simulation { cname = "Jungle in flames" , cfreq = [(CAVE_SAFARI_3, 1)] , cminPlaceSize = DiceXY 5 4 , cescapeFreq = [(OUTDOOR_ESCAPE_DOWN, 1)] , cmaxStairsNum = 1 , cstairFreq = [ (OUTDOOR_WALLED_STAIRCASE, 20) , (OUTDOOR_CLOSED_STAIRCASE, 80) , (OUTDOOR_TINY_STAIRCASE, 1) ] , cdesc = "\"Act 3. Jealous hunams set jungle on fire and flee.\"" } LambdaHack-0.11.0.0/GameDefinition/Content/FactionKind.hs0000644000000000000000000003560607346545000021122 0ustar0000000000000000-- | Definitions of kinds of factions present in a game, both human -- and computer-controlled. module Content.FactionKind ( -- * Group name patterns pattern EXPLORER_REPRESENTATIVE, pattern EXPLORER_SHORT, pattern EXPLORER_NO_ESCAPE, pattern EXPLORER_MEDIUM, pattern EXPLORER_TRAPPED, pattern EXPLORER_AUTOMATED, pattern EXPLORER_AUTOMATED_TRAPPED, pattern EXPLORER_CAPTIVE, pattern EXPLORER_PACIFIST, pattern COMPETITOR_REPRESENTATIVE, pattern COMPETITOR_SHORT, pattern COMPETITOR_NO_ESCAPE, pattern CIVILIAN_REPRESENTATIVE, pattern CONVICT_REPRESENTATIVE, pattern MONSTER_REPRESENTATIVE, pattern MONSTER_ANTI, pattern MONSTER_ANTI_CAPTIVE, pattern MONSTER_ANTI_PACIFIST, pattern MONSTER_TOURIST, pattern MONSTER_TOURIST_PASSIVE, pattern MONSTER_CAPTIVE, pattern MONSTER_CAPTIVE_NARRATING, pattern ANIMAL_REPRESENTATIVE, pattern ANIMAL_MAGNIFICENT, pattern ANIMAL_EXQUISITE, pattern ANIMAL_CAPTIVE, pattern ANIMAL_NARRATING, pattern ANIMAL_MAGNIFICENT_NARRATING, pattern ANIMAL_CAPTIVE_NARRATING, pattern HORROR_REPRESENTATIVE, pattern HORROR_CAPTIVE, pattern HORROR_PACIFIST , pattern REPRESENTATIVE , groupNamesSingleton, groupNames , -- * Content content #ifdef EXPOSE_INTERNAL -- * Group name patterns #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Content.ItemKindActor import Content.ItemKindOrgan -- * Group name patterns groupNamesSingleton :: [GroupName FactionKind] groupNamesSingleton = [EXPLORER_REPRESENTATIVE, EXPLORER_SHORT, EXPLORER_NO_ESCAPE, EXPLORER_MEDIUM, EXPLORER_TRAPPED, EXPLORER_AUTOMATED, EXPLORER_AUTOMATED_TRAPPED, EXPLORER_CAPTIVE, EXPLORER_PACIFIST, COMPETITOR_REPRESENTATIVE, COMPETITOR_SHORT, COMPETITOR_NO_ESCAPE, CIVILIAN_REPRESENTATIVE, CONVICT_REPRESENTATIVE, MONSTER_REPRESENTATIVE, MONSTER_ANTI, MONSTER_ANTI_CAPTIVE, MONSTER_ANTI_PACIFIST, MONSTER_TOURIST, MONSTER_TOURIST_PASSIVE, MONSTER_CAPTIVE, MONSTER_CAPTIVE_NARRATING, ANIMAL_REPRESENTATIVE, ANIMAL_MAGNIFICENT, ANIMAL_EXQUISITE, ANIMAL_CAPTIVE, ANIMAL_NARRATING, ANIMAL_MAGNIFICENT_NARRATING, ANIMAL_CAPTIVE_NARRATING, HORROR_REPRESENTATIVE, HORROR_CAPTIVE, HORROR_PACIFIST] pattern EXPLORER_REPRESENTATIVE, EXPLORER_SHORT, EXPLORER_NO_ESCAPE, EXPLORER_MEDIUM, EXPLORER_TRAPPED, EXPLORER_AUTOMATED, EXPLORER_AUTOMATED_TRAPPED, EXPLORER_CAPTIVE, EXPLORER_PACIFIST, COMPETITOR_REPRESENTATIVE, COMPETITOR_SHORT, COMPETITOR_NO_ESCAPE, CIVILIAN_REPRESENTATIVE, CONVICT_REPRESENTATIVE, MONSTER_REPRESENTATIVE, MONSTER_ANTI, MONSTER_ANTI_CAPTIVE, MONSTER_ANTI_PACIFIST, MONSTER_TOURIST, MONSTER_TOURIST_PASSIVE, MONSTER_CAPTIVE, MONSTER_CAPTIVE_NARRATING, ANIMAL_REPRESENTATIVE, ANIMAL_MAGNIFICENT, ANIMAL_EXQUISITE, ANIMAL_CAPTIVE, ANIMAL_NARRATING, ANIMAL_MAGNIFICENT_NARRATING, ANIMAL_CAPTIVE_NARRATING, HORROR_REPRESENTATIVE, HORROR_CAPTIVE, HORROR_PACIFIST :: GroupName FactionKind groupNames :: [GroupName FactionKind] groupNames = [REPRESENTATIVE] pattern REPRESENTATIVE :: GroupName FactionKind pattern REPRESENTATIVE = GroupName "representative" pattern EXPLORER_REPRESENTATIVE = GroupName "explorer" pattern EXPLORER_SHORT = GroupName "explorer short" pattern EXPLORER_NO_ESCAPE = GroupName "explorer no escape" pattern EXPLORER_MEDIUM = GroupName "explorer medium" pattern EXPLORER_TRAPPED = GroupName "explorer trapped" pattern EXPLORER_AUTOMATED = GroupName "explorer automated" pattern EXPLORER_AUTOMATED_TRAPPED = GroupName "explorer automated trapped" pattern EXPLORER_CAPTIVE = GroupName "explorer captive" pattern EXPLORER_PACIFIST = GroupName "explorer pacifist" pattern COMPETITOR_REPRESENTATIVE = GroupName "competitor" pattern COMPETITOR_SHORT = GroupName "competitor short" pattern COMPETITOR_NO_ESCAPE = GroupName "competitor no escape" pattern CIVILIAN_REPRESENTATIVE = GroupName "civilian" pattern CONVICT_REPRESENTATIVE = GroupName "convict" pattern MONSTER_REPRESENTATIVE = GroupName "monster" pattern MONSTER_ANTI = GroupName "monster anti" pattern MONSTER_ANTI_CAPTIVE = GroupName "monster anti captive" pattern MONSTER_ANTI_PACIFIST = GroupName "monster anti pacifist" pattern MONSTER_TOURIST = GroupName "monster tourist" pattern MONSTER_TOURIST_PASSIVE = GroupName "monster tourist passive" pattern MONSTER_CAPTIVE = GroupName "monster captive" pattern MONSTER_CAPTIVE_NARRATING = GroupName "monster captive narrating" pattern ANIMAL_REPRESENTATIVE = GroupName "animal" pattern ANIMAL_MAGNIFICENT = GroupName "animal magnificent" pattern ANIMAL_EXQUISITE = GroupName "animal exquisite" pattern ANIMAL_CAPTIVE = GroupName "animal captive" pattern ANIMAL_NARRATING = GroupName "animal narrating" pattern ANIMAL_MAGNIFICENT_NARRATING = GroupName "animal magnificent narrating" pattern ANIMAL_CAPTIVE_NARRATING = GroupName "animal captive narrating" pattern HORROR_REPRESENTATIVE = GroupName "horror" pattern HORROR_CAPTIVE = GroupName "horror captive" pattern HORROR_PACIFIST = GroupName "horror pacifist" -- * Teams teamCompetitor, teamCivilian, teamConvict, teamMonster, teamAnimal, teamHorror, teamOther :: TeamContinuity teamCompetitor = TeamContinuity 2 teamCivilian = TeamContinuity 3 teamConvict = TeamContinuity 4 teamMonster = TeamContinuity 5 teamAnimal = TeamContinuity 6 teamHorror = TeamContinuity 7 teamOther = TeamContinuity 10 -- * Content content :: [FactionKind] content = [factExplorer, factExplorerShort, factExplorerNoEscape, factExplorerMedium, factExplorerTrapped, factExplorerAutomated, factExplorerAutomatedTrapped, factExplorerCaptive, factExplorerPacifist, factCompetitor, factCompetitorShort, factCompetitorNoEscape, factCivilian, factConvict, factMonster, factMonsterAnti, factMonsterAntiCaptive, factMonsterAntiPacifist, factMonsterTourist, factMonsterTouristPassive, factMonsterCaptive, factMonsterCaptiveNarrating, factAnimal, factAnimalMagnificent, factAnimalExquisite, factAnimalCaptive, factAnimalNarrating, factAnimalMagnificentNarrating, factAnimalCaptiveNarrating, factHorror, factHorrorCaptive, factHorrorPacifist] factExplorer, factExplorerShort, factExplorerNoEscape, factExplorerMedium, factExplorerTrapped, factExplorerAutomated, factExplorerAutomatedTrapped, factExplorerCaptive, factExplorerPacifist, factCompetitor, factCompetitorShort, factCompetitorNoEscape, factCivilian, factConvict, factMonster, factMonsterAnti, factMonsterAntiCaptive, factMonsterAntiPacifist, factMonsterTourist, factMonsterTouristPassive, factMonsterCaptive, factMonsterCaptiveNarrating, factAnimal, factAnimalMagnificent, factAnimalExquisite, factAnimalCaptive, factAnimalNarrating, factAnimalMagnificentNarrating, factAnimalCaptiveNarrating, factHorror, factHorrorCaptive, factHorrorPacifist :: FactionKind -- * Content -- ** teamExplorer factExplorer = FactionKind { fname = "Explorer" , ffreq = [(EXPLORER_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)] , fteam = teamExplorer , fgroups = [(HERO, 100)] -- don't spam the escapists, etc., in description , fskillsOther = meleeAdjacent , fcanEscape = True , fneverEmpty = True , fhiCondPoly = hiHeroLong , fhasGender = True , finitDoctrine = TExplore , fspawnsFast = False , fhasPointman = True , fhasUI = True , finitUnderAI = False , fenemyTeams = [teamCompetitor, teamMonster, teamAnimal, teamHorror] , falliedTeams = [] } factExplorerShort = factExplorer { ffreq = [(EXPLORER_SHORT, 1)] , fhiCondPoly = hiHeroShort , fenemyTeams = [teamMonster, teamAnimal, teamHorror] } factExplorerNoEscape = factExplorer { ffreq = [(EXPLORER_NO_ESCAPE, 1)] , fcanEscape = False , fhiCondPoly = hiHeroMedium } factExplorerMedium = factExplorer { ffreq = [(EXPLORER_MEDIUM, 1)] , fhiCondPoly = hiHeroMedium } factExplorerTrapped = factExplorer { ffreq = [(EXPLORER_TRAPPED, 1)] , fcanEscape = False , fhiCondPoly = hiHeroLong } factExplorerAutomated = factExplorer { ffreq = [(EXPLORER_AUTOMATED, 1)] , fhasUI = False , finitUnderAI = True } factExplorerAutomatedTrapped = factExplorerAutomated { ffreq = [(EXPLORER_AUTOMATED_TRAPPED, 1)] , fcanEscape = False , fhiCondPoly = hiHeroLong } factExplorerCaptive = factExplorer { ffreq = [(EXPLORER_CAPTIVE, 1)] , fneverEmpty = True -- already there } factExplorerPacifist = factExplorerCaptive { ffreq = [(EXPLORER_PACIFIST, 1)] , fenemyTeams = [] , falliedTeams = [] } -- ** teamCompetitor, symmetric opponents of teamExplorer factCompetitor = factExplorer { fname = "Indigo Researcher" , ffreq = [(COMPETITOR_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)] , fteam = teamCompetitor , fhasUI = False , finitUnderAI = True , fenemyTeams = [teamExplorer, teamMonster, teamAnimal, teamHorror] , falliedTeams = [] } factCompetitorShort = factCompetitor { fname = "Indigo Founder" -- early , ffreq = [(COMPETITOR_SHORT, 1)] , fhiCondPoly = hiHeroShort , fenemyTeams = [teamMonster, teamAnimal, teamHorror] } factCompetitorNoEscape = factCompetitor { ffreq = [(COMPETITOR_NO_ESCAPE, 1)] , fcanEscape = False , fhiCondPoly = hiHeroMedium } -- ** teamCivilian factCivilian = FactionKind { fname = "Civilian" , ffreq = [(CIVILIAN_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)] , fteam = teamCivilian , fgroups = [(HERO, 100), (CIVILIAN, 100)] -- symmetric vs player , fskillsOther = zeroSkills -- not coordinated by any leadership , fcanEscape = False , fneverEmpty = True , fhiCondPoly = hiHeroMedium , fhasGender = True , finitDoctrine = TPatrol , fspawnsFast = False , fhasPointman = False -- unorganized , fhasUI = False , finitUnderAI = True , fenemyTeams = [teamMonster, teamAnimal, teamHorror] , falliedTeams = [] } -- ** teamConvict, different demographics factConvict = factCivilian { fname = "Hunam Convict" , ffreq = [(CONVICT_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)] , fteam = teamConvict , fhasPointman = True -- convicts organize better , finitUnderAI = True , fenemyTeams = [teamMonster, teamAnimal, teamHorror] , falliedTeams = [] } -- ** teamMonster factMonster = FactionKind { fname = "Monster Hive" , ffreq = [(MONSTER_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)] , fteam = teamMonster , fgroups = [ (MONSTER, 100) , (MOBILE_MONSTER, 1) ] , fskillsOther = zeroSkills , fcanEscape = False , fneverEmpty = False , fhiCondPoly = hiDweller , fhasGender = False , finitDoctrine = TExplore , fspawnsFast = True , fhasPointman = True , fhasUI = False , finitUnderAI = True , fenemyTeams = [teamExplorer, teamCompetitor, teamCivilian, teamConvict] , falliedTeams = [teamAnimal] } -- This has continuity @teamMonster@, despite being playable. factMonsterAnti = factMonster { ffreq = [(MONSTER_ANTI, 1)] , fhasUI = True , finitUnderAI = False } factMonsterAntiCaptive = factMonsterAnti { ffreq = [(MONSTER_ANTI_CAPTIVE, 1)] , fneverEmpty = True } factMonsterAntiPacifist = factMonsterAntiCaptive { ffreq = [(MONSTER_ANTI_PACIFIST, 1)] , fenemyTeams = [] , falliedTeams = [] } -- More flavour and special backstory, but the same team. factMonsterTourist = factMonsterAnti { fname = "Monster Tourist Office" , ffreq = [(MONSTER_TOURIST, 1)] , fcanEscape = True , fneverEmpty = True -- no spawning , fhiCondPoly = hiHeroMedium , finitDoctrine = TFollow -- follow-the-guide, as tourists do , fspawnsFast = False -- on a trip, so no spawning , finitUnderAI = False , fenemyTeams = [teamAnimal, teamExplorer, teamCompetitor, teamCivilian, teamConvict] , falliedTeams = [] } factMonsterTouristPassive = factMonsterTourist { ffreq = [(MONSTER_TOURIST_PASSIVE, 1)] , fhasUI = False , finitUnderAI = True } factMonsterCaptive = factMonster { ffreq = [(MONSTER_CAPTIVE, 1)] , fneverEmpty = True } factMonsterCaptiveNarrating = factMonsterAntiCaptive { ffreq = [(MONSTER_CAPTIVE_NARRATING, 1)] , fhasUI = True } -- ** teamAnimal factAnimal = FactionKind { fname = "Animal Kingdom" , ffreq = [(ANIMAL_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)] , fteam = teamAnimal , fgroups = [ (ANIMAL, 100), (INSECT, 100), (GEOPHENOMENON, 100) -- only the distinct enough ones , (MOBILE_ANIMAL, 1), (IMMOBILE_ANIMAL, 1), (SCAVENGER, 1) ] , fskillsOther = zeroSkills , fcanEscape = False , fneverEmpty = False , fhiCondPoly = hiDweller , fhasGender = False , finitDoctrine = TRoam -- can't pick up, so no point exploring , fspawnsFast = True , fhasPointman = False , fhasUI = False , finitUnderAI = True , fenemyTeams = [teamExplorer, teamCompetitor, teamCivilian, teamConvict] , falliedTeams = [teamMonster] } -- These two differ from outside, but share information and boasting -- about them tends to be general, too. factAnimalMagnificent = factAnimal { fname = "Animal Magnificent Specimen Variety" , ffreq = [(ANIMAL_MAGNIFICENT, 1)] , fneverEmpty = True , fenemyTeams = [teamMonster, teamExplorer, teamCompetitor, teamCivilian, teamConvict] , falliedTeams = [] } factAnimalExquisite = factAnimal { fname = "Animal Exquisite Herds and Packs Galore" , ffreq = [(ANIMAL_EXQUISITE, 1)] , fteam = teamOther -- in the same mode as @factAnimalMagnificent@, so borrow -- identity from horrors to avoid a clash , fneverEmpty = True , fenemyTeams = [teamMonster, teamExplorer, teamCompetitor, teamCivilian, teamConvict] , falliedTeams = [] } factAnimalCaptive = factAnimal { ffreq = [(ANIMAL_CAPTIVE, 1)] , fneverEmpty = True } factAnimalNarrating = factAnimal { ffreq = [(ANIMAL_NARRATING, 1)] , fhasUI = True } factAnimalMagnificentNarrating = factAnimalMagnificent { ffreq = [(ANIMAL_MAGNIFICENT_NARRATING, 1)] , fhasPointman = True , fhasUI = True , finitUnderAI = False } factAnimalCaptiveNarrating = factAnimalCaptive { ffreq = [(ANIMAL_CAPTIVE_NARRATING, 1)] , fhasUI = True } -- ** teamHorror, not much of a continuity intended, but can't be ignored -- | A special faction, for summoned actors that don't belong to any -- of the main factions of a given game. E.g., animals summoned during -- a brawl game between two hero factions land in the horror faction. -- In every game, either all factions for which summoning items exist -- should be present or a horror faction should be added to host them. factHorror = FactionKind { fname = "Horror Den" , ffreq = [(HORROR_REPRESENTATIVE, 1), (REPRESENTATIVE, 1)] , fteam = teamHorror , fgroups = [(IK.HORROR, 100)] , fskillsOther = zeroSkills , fcanEscape = False , fneverEmpty = False , fhiCondPoly = [] , fhasGender = False , finitDoctrine = TPatrol -- disoriented , fspawnsFast = False , fhasPointman = False , fhasUI = False , finitUnderAI = True , fenemyTeams = [teamExplorer, teamCompetitor, teamCivilian, teamConvict] , falliedTeams = [] } factHorrorCaptive = factHorror { ffreq = [(HORROR_CAPTIVE, 1)] , fneverEmpty = True } factHorrorPacifist = factHorrorCaptive { ffreq = [(HORROR_PACIFIST, 1)] , fenemyTeams = [] , falliedTeams = [] } LambdaHack-0.11.0.0/GameDefinition/Content/ItemKind.hs0000644000000000000000000024141307346545000020430 0ustar0000000000000000-- | Definitions of basic items. module Content.ItemKind ( -- * Group name patterns pattern HARPOON, pattern EDIBLE_PLANT, pattern RING_OF_OPPORTUNITY_GRENADIER, pattern ARMOR_LOOSE, pattern CLOTHING_MISC, pattern CHIC_GEAR , groupNamesSingleton, groupNames , -- * Content content, items, otherItemContent ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour import Content.ItemKindActor import Content.ItemKindBlast import Content.ItemKindEmbed import Content.ItemKindOrgan import Content.ItemKindTemporary import Content.RuleKind -- * Group name patterns groupNamesSingleton :: [GroupName ItemKind] groupNamesSingleton = [S_FRAGRANCE, S_SINGLE_SPARK, S_SPARK] ++ [FLASK_UNKNOWN, POTION_UNKNOWN, EDIBLE_PLANT_UNKNOWN, SCROLL_UNKNOWN, NECKLACE_UNKNOWN, RING_UNKNOWN, HAMMER_UNKNOWN, GEM_UNKNOWN, CURRENCY_UNKNOWN] ++ actorsGNSingleton ++ organsGNSingleton ++ blastsGNSingleton ++ temporariesGNSingleton pattern FLASK_UNKNOWN, POTION_UNKNOWN, EDIBLE_PLANT_UNKNOWN, SCROLL_UNKNOWN, NECKLACE_UNKNOWN, RING_UNKNOWN, HAMMER_UNKNOWN, GEM_UNKNOWN, CURRENCY_UNKNOWN :: GroupName ItemKind groupNames :: [GroupName ItemKind] groupNames = [TREASURE, ANY_SCROLL, ANY_GLASS, ANY_POTION, ANY_FLASK, EXPLOSIVE, ANY_JEWELRY, VALUABLE, UNREPORTED_INVENTORY] ++ [HARPOON, EDIBLE_PLANT, RING_OF_OPPORTUNITY_GRENADIER, ARMOR_LOOSE, CLOTHING_MISC, CHIC_GEAR] ++ embedsGN ++ actorsGN ++ organsGN ++ blastsGN pattern HARPOON, EDIBLE_PLANT, RING_OF_OPPORTUNITY_GRENADIER, ARMOR_LOOSE, CLOTHING_MISC, CHIC_GEAR :: GroupName ItemKind -- The @UNKNOWN@ patterns don't need to be exported. Used internally. -- They also represent singleton groups. pattern FLASK_UNKNOWN = GroupName "flask unknown" pattern POTION_UNKNOWN = GroupName "potion unknown" pattern EDIBLE_PLANT_UNKNOWN = GroupName "edible plant unknown" pattern SCROLL_UNKNOWN = GroupName "scroll unknown" pattern NECKLACE_UNKNOWN = GroupName "necklace unknown" pattern RING_UNKNOWN = GroupName "ring unknown" pattern HAMMER_UNKNOWN = GroupName "hammer unknown" pattern GEM_UNKNOWN = GroupName "gem unknown" pattern CURRENCY_UNKNOWN = GroupName "currency unknown" pattern HARPOON = GroupName "harpoon" pattern EDIBLE_PLANT = GroupName "edible plant" pattern RING_OF_OPPORTUNITY_GRENADIER = GroupName "ring of grenadier" pattern ARMOR_LOOSE = GroupName "loose armor" pattern CLOTHING_MISC = GroupName "miscellaneous clothing" pattern CHIC_GEAR = GroupName "chic gear" -- * Content content :: [ItemKind] content = items ++ otherItemContent otherItemContent :: [ItemKind] otherItemContent = embeds ++ actors ++ organs ++ blasts ++ temporaries items :: [ItemKind] items = [sandstoneRock, dart, spike, spike2, slingStone, slingBullet, paralizingProj, harpoon, harpoon2, net, fragmentationBomb, concussionBomb, flashBomb, firecrackerBomb, flaskTemplate, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, flask15, potionTemplate, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, potion9, potion10, potion11, potion12, potion13, potion14, potion15, scrollTemplate, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, scroll11, scroll12, scroll13, ediblePlantTemplate, ediblePlant1, ediblePlant2, ediblePlant3, ediblePlant4, ediblePlant5, ediblePlant6, ediblePlant7, light1, light2, light3, blanket, gorget, necklaceTemplate, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, necklace8, necklace9, necklace10, motionScanner, imageItensifier, sightSharpening, ringTemplate, ring1, ring2, ring3, ring4, ring5, ring6, ring7, ring8, ring9, ring10, armorLeather, armorMail, meleeEnhancement, gloveFencing, gloveGauntlet, gloveJousting, hatUshanka, capReinforced, helmArmored, smokingJacket, buckler, shield, shield2, shield3, hammerTemplate, hammer1, hammer2, hammer3, hammerParalyze, hammerSpark, knife, daggerDischarge, sword, swordImpress, swordNullify, halberd, halberd2, halberd3, halberdPushActor, gemTemplate, gem1, gem2, gem3, gem4, gem5, currencyTemplate, currency, jumpingPole, seeingItem] sandstoneRock, dart, spike, spike2, slingStone, slingBullet, paralizingProj, harpoon, harpoon2, net, fragmentationBomb, concussionBomb, flashBomb, firecrackerBomb, flaskTemplate, flask1, flask2, flask3, flask4, flask5, flask6, flask7, flask8, flask9, flask10, flask11, flask12, flask13, flask14, flask15, potionTemplate, potion1, potion2, potion3, potion4, potion5, potion6, potion7, potion8, potion9, potion10, potion11, potion12, potion13, potion14, potion15, scrollTemplate, scroll1, scroll2, scroll3, scroll4, scroll5, scroll6, scroll7, scroll8, scroll9, scroll10, scroll11, scroll12, scroll13, ediblePlantTemplate, ediblePlant1, ediblePlant2, ediblePlant3, ediblePlant4, ediblePlant5, ediblePlant6, ediblePlant7, light1, light2, light3, blanket, gorget, necklaceTemplate, necklace1, necklace2, necklace3, necklace4, necklace5, necklace6, necklace7, necklace8, necklace9, necklace10, motionScanner, imageItensifier, sightSharpening, ringTemplate, ring1, ring2, ring3, ring4, ring5, ring6, ring7, ring8, ring9, ring10, armorLeather, armorMail, meleeEnhancement, gloveFencing, gloveGauntlet, gloveJousting, hatUshanka, capReinforced, helmArmored, smokingJacket, buckler, shield, shield2, shield3, hammerTemplate, hammer1, hammer2, hammer3, hammerParalyze, hammerSpark, knife, daggerDischarge, sword, swordImpress, swordNullify, halberd, halberd2, halberd3, halberdPushActor, gemTemplate, gem1, gem2, gem3, gem4, gem5, currencyTemplate, currency, jumpingPole, seeingItem :: ItemKind -- Keep the dice rolls and sides in aspects small so that not too many -- distinct items are generated (for display in item lore and for narrative -- impact ("oh, I found the more powerful of the two variants of the item!", -- instead of "hmm, I found one of the countless variants, a decent one"). -- In particular, for unique items, unless they inherit aspects from -- a standard item, permit only a couple possible variants. -- This is especially important if an item kind has multiple random aspects. -- Instead multiply dice results, e.g., (1 `d` 3) * 5 instead of 1 `d` 15. -- -- Beware of non-periodic non-weapon durable items with beneficial effects -- and low timeout -- AI will starve applying such an item incessantly. -- * Item group symbols, partially from Nethack symbolProjectile, _symbolLauncher, symbolLight, symbolTool, symbolSpecial, symbolGold, symbolNecklace, symbolRing, symbolPotion, symbolFlask, symbolScroll, symbolTorsoArmor, symbolMiscArmor, symbolClothes, symbolShield, symbolPolearm, symbolEdged, symbolHafted, symbolWand, _symbolStaff, symbolFood :: ContentSymbol ItemKind symbolProjectile = rsymbolProjectile $ ritemSymbols standardRules _symbolLauncher = toContentSymbol '}' symbolLight = rsymbolLight $ ritemSymbols standardRules symbolTool = rsymbolTool $ ritemSymbols standardRules symbolSpecial = rsymbolSpecial $ ritemSymbols standardRules symbolGold = rsymbolGold $ ritemSymbols standardRules symbolNecklace = rsymbolNecklace $ ritemSymbols standardRules symbolRing = rsymbolRing $ ritemSymbols standardRules symbolPotion = rsymbolPotion $ ritemSymbols standardRules symbolFlask = rsymbolFlask $ ritemSymbols standardRules symbolScroll = rsymbolScroll $ ritemSymbols standardRules symbolTorsoArmor = rsymbolTorsoArmor $ ritemSymbols standardRules symbolMiscArmor = rsymbolMiscArmor $ ritemSymbols standardRules symbolClothes = rsymbolClothes $ ritemSymbols standardRules symbolShield = rsymbolShield $ ritemSymbols standardRules symbolPolearm = rsymbolPolearm $ ritemSymbols standardRules symbolEdged = rsymbolEdged $ ritemSymbols standardRules symbolHafted = rsymbolHafted $ ritemSymbols standardRules symbolWand = rsymbolWand $ ritemSymbols standardRules _symbolStaff = toContentSymbol '_' symbolFood = rsymbolFood $ ritemSymbols standardRules -- ** Thrown weapons sandstoneRock = ItemKind { isymbol = symbolProjectile , iname = "sandstone rock" , ifreq = [ (S_SANDSTONE_ROCK, 1) , (UNREPORTED_INVENTORY, 1) ] -- too weak to spam , iflavour = zipPlain [Green] , icount = 1 + 1 `d` 2 -- > 1, to let AI ignore sole pieces , irarity = [(1, 20), (10, 1)] -- a few already in starting stash , iverbHit = "hit" , iweight = 300 , idamage = 1 `d` 1 , iaspects = [ AddSkill SkHurtMelee $ -16 * 5 , SetFlag Fragile , toVelocity 70 ] -- not dense, irregular , ieffects = [] , idesc = "A lump of brittle sandstone rock." , ikit = [] } dart = ItemKind { isymbol = symbolProjectile , iname = "dart" , ifreq = [(COMMON_ITEM, 100), (ANY_ARROW, 50), (WEAK_ARROW, 50)] , iflavour = zipPlain [BrRed] , icount = 1 + 4 `dL` 5 , irarity = [(1, 15), (10, 5)] , iverbHit = "prick" , iweight = 40 , idamage = 1 `d` 1 , iaspects = [AddSkill SkHurtMelee $ (-15 + 1 `d` 2 + 1 `dL` 3) * 5] -- only good against leather , ieffects = [] , idesc = "A sharp delicate dart with fins." , ikit = [] } spike = ItemKind { isymbol = symbolProjectile , iname = "spike" , ifreq = [(COMMON_ITEM, 100), (ANY_ARROW, 50), (WEAK_ARROW, 50)] , iflavour = zipPlain [BrCyan] , icount = 1 + 4 `dL` 5 , irarity = [(1, 10), (10, 8)] , iverbHit = "nick" , iweight = 150 , idamage = 2 `d` 1 , iaspects = [ AddSkill SkHurtMelee $ (-10 + 1 `d` 2 + 1 `dL` 3) * 5 -- heavy vs armor , SetFlag MinorEffects , toVelocity 70 ] -- hitting with tip costs speed , ieffects = [ Explode S_SINGLE_SPARK -- when hitting enemy , OnSmash (Explode S_SINGLE_SPARK) ] -- at wall hit -- this results in a wordy item synopsis, but it's OK, the spark really -- is useful in some situations, not just a flavour , idesc = "A cruel long nail with small head." -- "Much inferior to arrows though, especially given the contravariance problems." -- funny, but destroy the suspension of disbelief; this is supposed to be a Lovecraftian horror and any hilarity must ensue from the failures in making it so and not from actively trying to be funny; also, mundane objects are not supposed to be scary or transcendental; the scare is in horrors from the abstract dimension visiting our ordinary reality; without the contrast there's no horror and no wonder, so also the magical items must be contrasted with ordinary XIX century and antique items , ikit = [] } spike2 = spike { ifreq = [(COMMON_ITEM, 2), (ANY_ARROW, 1), (WEAK_ARROW, 1)] , iflavour = zipPlain [Cyan] , iverbHit = "penetrate" , iweight = 200 , idamage = 4 `d` 1 , iaspects = [ AddSkill SkHurtMelee $ (-10 + 1 `d` 2 + 1 `dL` 3) * 5 , SetFlag MinorEffects , Odds (10 * 1 `dL` 10) [] [toVelocity 70] ] -- at deep levels sometimes even don't limit velocity , idesc = "A jagged skewer of rusty metal." } slingStone = ItemKind { isymbol = symbolProjectile , iname = "sling stone" , ifreq = [(COMMON_ITEM, 5), (ANY_ARROW, 100)] , iflavour = zipPlain [Blue] , icount = 1 + 3 `dL` 4 , irarity = [(1, 1), (10, 20)] , iverbHit = "batter" , iweight = 200 , idamage = 1 `d` 1 , iaspects = [ AddSkill SkHurtMelee $ (-10 + 1 `d` 2 + 1 `dL` 3) * 5 -- heavy, to bludgeon through armor , SetFlag MinorEffects , toVelocity 150 ] , ieffects = [ Explode S_SINGLE_SPARK -- when hitting enemy , OnSmash (Explode S_SINGLE_SPARK) ] -- at wall hit , idesc = "A round stone, carefully sized and smoothed to fit the pouch of a standard string and cloth sling." , ikit = [] } slingBullet = ItemKind { isymbol = symbolProjectile , iname = "sling bullet" , ifreq = [(COMMON_ITEM, 5), (ANY_ARROW, 100)] , iflavour = zipPlain [BrBlack] , icount = 1 + 6 `dL` 4 , irarity = [(1, 1), (10, 15)] , iverbHit = "slug" , iweight = 28 , idamage = 1 `d` 1 , iaspects = [ AddSkill SkHurtMelee $ (-17 + 1 `d` 2 + 1 `dL` 3) * 5 -- not too good against armor , ToThrow $ ThrowMod 200 100 2 -- piercing , SetFlag Fragile ] -- otherwise would rarely break and the player would have -- unlimited resource and would have to pick up constantly , ieffects = [] , idesc = "Small almond-shaped leaden projectile that weighs more than the sling used to tie the bag. It doesn't drop out of the sling's pouch when swung and doesn't snag when released. Known to pierce through flesh, at least at maximum speed." -- we lie, it doesn't slow down in our model; but it stops piercing alright , ikit = [] } -- ** Exotic thrown weapons -- Identified, because shape (and name) says it all. Detailed aspects id by use. -- This is an extremely large value for @Paralyze@. Normally for such values -- we should instead use condition that disables (almost) all stats, -- except @SkWait@, so that the player can switch leader and not be -- helpless nor experience instadeath (unless his party is 1-person -- or the actor is isolated, but that's usually player's fault). paralizingProj = ItemKind { isymbol = symbolProjectile , iname = "bolas set" , ifreq = [(COMMON_ITEM, 100)] , iflavour = zipPlain [BrGreen] , icount = 1 `dL` 4 , irarity = [(5, 5), (10, 5)] , iverbHit = "entangle" , iweight = 500 , idamage = 1 `d` 1 , iaspects = [AddSkill SkHurtMelee $ -14 * 5] , ieffects = [Paralyze 15, Discharge 1 100] , idesc = "Wood balls tied with hemp rope. The foe is unlikely to use its main weapon while fighting for balance." , ikit = [] } harpoon = ItemKind { isymbol = symbolProjectile , iname = "harpoon" , ifreq = [(COMMON_ITEM, 100), (HARPOON, 100)] , iflavour = zipPlain [Brown] , icount = 1 `dL` 5 , irarity = [(10, 10)] , iverbHit = "hook" , iweight = 750 , idamage = 5 `d` 1 , iaspects = [AddSkill SkHurtMelee $ (-10 + 1 `d` 2 + 1 `dL` 3) * 5] , ieffects = [ PullActor (ThrowMod 200 50 1) -- 1 step, fast , Yell ] -- yell, because brutal , idesc = "The cruel, barbed head lodges in its victim so painfully that the weakest tug of the thin line sends the victim flying." , ikit = [] } harpoon2 = harpoon { iname = "The whaling Harpoon" , ifreq = [(COMMON_ITEM, 10), (HARPOON, 2)] , icount = 2 `dL` 5 , iweight = 1000 , idamage = 21 `d` 1 , iaspects = SetFlag Unique : delete (SetFlag Durable) (iaspects harpoon) , idesc = "With a brittle, barbed head and thick cord, this ancient weapon is designed for formidable prey. The age has made the edge thinner and sharper, but brittle and splintering, so it won't last beyond a single hit. " } net = ItemKind { isymbol = symbolProjectile , iname = "net" , ifreq = [(COMMON_ITEM, 100)] , iflavour = zipPlain [BrGreen] , icount = 1 `dL` 3 , irarity = [(5, 5), (10, 7)] , iverbHit = "entangle" , iweight = 1000 , idamage = 2 `d` 1 , iaspects = [AddSkill SkHurtMelee $ -14 * 5] , ieffects = [ toOrganBad S_SLOWED (3 + 1 `d` 3) , DropItem maxBound 1 CEqp ARMOR_LOOSE -- only one of each kind is dropped, because no rubbish -- in this group and so no risk of exploit , SendFlying (ThrowMod 100 50 1) ] -- 1 step; painful , idesc = "A wide net with weights along the edges. Entangles armor and restricts movement." , ikit = [] } -- ** Explosives, with the only effect being @Explode@ fragmentationBomb = ItemKind { isymbol = symbolProjectile , iname = "clay pot" -- clay pot filled with black powder; fragmentation comes from the clay -- shards, so it's not obvious if it's a weapon or just storage method; -- deflagration, not detonation, so large mass and hard container -- required not to burn harmlessly; improvised short fuze , ifreq = [(COMMON_ITEM, 100), (EXPLOSIVE, 200)] , iflavour = zipPlain [Red] , icount = 1 `dL` 5 -- many, because not very intricate , irarity = [(5, 8), (10, 5)] , iverbHit = "thud" , iweight = 3000 -- low velocity due to weight , idamage = 0 -- heavy and hard, but let's not confuse with blast damage , iaspects = [ ELabel "of black powder" , SetFlag Lobable, SetFlag Fragile ] , ieffects = [ Explode S_FOCUSED_FRAGMENTATION , OnSmash (Explode S_VIOLENT_FRAGMENTATION) ] , idesc = "The practical application of science." , ikit = [] } concussionBomb = fragmentationBomb { iname = "satchel" -- slightly stabilized nitroglycerine in a soft satchel, hence -- no fragmentation, but huge shock wave despite small size and lack of -- strong container to build up pressure (hence only mild hearing loss); -- indoors helps the shock wave; unstable enough that no fuze required , iflavour = zipPlain [Magenta] , iverbHit = "flap" , iweight = 400 , iaspects = [ ELabel "of mining charges" , SetFlag Lobable, SetFlag Fragile , toVelocity 70 ] -- flappy and so slow , ieffects = [ Explode S_FOCUSED_CONCUSSION , OnSmash (Explode S_VIOLENT_CONCUSSION) ] , idesc = "Avoid sudden movements." } -- Not flashbang, because powerful bang without fragmentation is harder -- to manufacture (requires an oxidizer and steel canister with holes). -- The bang would also paralyze and/or lower the movement skill -- (out of balance due to ear trauma). flashBomb = fragmentationBomb { iname = "magnesium ribbon" -- filled with magnesium flash powder , iflavour = zipPlain [BrYellow] -- avoid @BrWhite@; looks wrong in dark , iverbHit = "flash" , iweight = 400 , iaspects = [ SetFlag Lobable, SetFlag Fragile , toVelocity 70 ] -- bad shape for throwing , ieffects = [Explode S_FOCUSED_FLASH, OnSmash (Explode S_VIOLENT_FLASH)] , idesc = "For dramatic entrances and urgent exits." } firecrackerBomb = fragmentationBomb { iname = "roll" -- not fireworks, as they require outdoors , iflavour = zipPlain [BrMagenta] , irarity = [(1, 5), (5, 6)] -- a toy, if harmful , iverbHit = "crack" -- a pun, matches the verb from "ItemKindBlast" , iweight = 1000 , iaspects = [SetFlag Lobable, SetFlag Fragile] , ieffects = [Explode S_FIRECRACKER, OnSmash (Explode S_FIRECRACKER)] , idesc = "String and paper, concealing a deadly surprise." } -- ** Exploding consumables. -- Not identified, because they are perfect for the id-by-use fun, -- due to effects. They are fragile and upon hitting the ground explode -- for effects roughly corresponding to their normal effects. -- Whether to hit with them or explode them close to the target -- is intended to be an interesting tactical decision. -- Flasks are intended to be thrown. They are often not natural: maths, magic, -- distillery. In fact, they cover all temporary conditions, except those -- for stats resistance and regeneration. They never heal, directly -- nor indirectly (regen), so may be thrown without the risk of wasting -- precious HP. -- -- There is no flask nor condition that only does Calm or max Calm depletion, -- because Calm reduced often via combat, etc. flaskTemplate = ItemKind { isymbol = symbolFlask , iname = "flask" , ifreq = [(FLASK_UNKNOWN, 1)] , iflavour = zipGlassPlain darkCol ++ zipGlassFancy darkCol ++ zipLiquid darkCol , icount = 1 `dL` 3 , irarity = [(1, 7), (10, 3)] , iverbHit = "splash" , iweight = 500 , idamage = 0 , iaspects = [ PresentAs FLASK_UNKNOWN, SetFlag Lobable, SetFlag Fragile , toVelocity 60 ] -- oily, rather bad grip , ieffects = [] , idesc = "A flask of oily liquid of a suspect color. Something seems to be moving inside. Double dose causes twice longer effect. Triple dose is not advisable, since the active substance is never without unhealty side-efects and often dissolved in large volumes of alcohol." , ikit = [] } flask1 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , icount = 1 `dL` 5 , irarity = [(10, 10)] , iaspects = ELabel "of strength brew" : iaspects flaskTemplate , ieffects = [ toOrganGood S_STRENGTHENED (20 + 1 `d` 5) , OnSmash (Explode S_DENSE_SHOWER) ] } flask2 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of weakness brew" : iaspects flaskTemplate , ieffects = [ toOrganBad S_WEAKENED (20 + 1 `d` 5) , OnSmash (Explode S_SPARSE_SHOWER) ] } flask3 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of melee protective balm" : iaspects flaskTemplate , ieffects = [ toOrganGood S_PROTECTED_FROM_MELEE (20 + 1 `d` 5) , OnSmash (Explode S_MELEE_PROTECTIVE_BALM) ] } flask4 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of ranged protective balm" : iaspects flaskTemplate , ieffects = [ toOrganGood S_PROTECTED_FROM_RANGED (20 + 1 `d` 5) , OnSmash (Explode S_RANGE_PROTECTIVE_BALM) ] } flask5 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of PhD defense questions" : iaspects flaskTemplate , ieffects = [ toOrganBad S_DEFENSELESS (20 + 1 `d` 5) , Impress , Detect DetectExit 20 , OnSmash (Explode S_DEFENSELESSNESS_RUNOUT) ] } flask6 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , irarity = [(1, 1)] -- not every playthrough needs one , iaspects = ELabel "of resolution" : iaspects flaskTemplate , ieffects = [ toOrganGood S_RESOLUTE (100 + 1 `d` 20) -- long, for scouting , RefillCalm 100 -- not to make it a drawback, via @calmEnough@ , OnSmash (Explode S_RESOLUTION_DUST) ] } flask7 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , icount = 1 `d` 2 -- too powerful en masse , iaspects = ELabel "of haste brew" : iaspects flaskTemplate , ieffects = [ toOrganGood S_HASTED (20 + 1 `d` 5) , OnSmash (Explode S_HASTE_SPRAY) ] } flask8 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of eye drops" : iaspects flaskTemplate , ieffects = [ toOrganGood S_FAR_SIGHTED (40 + 1 `d` 10) , OnSmash (Explode S_EYE_DROP) ] } flask9 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , irarity = [(10, 2)] -- not very useful right now , iaspects = ELabel "of smelly concoction" : iaspects flaskTemplate , ieffects = [ toOrganGood S_KEEN_SMELLING (40 + 1 `d` 10) , Detect DetectActor 10 -- make it at least slightly useful , OnSmash (Explode S_SMELLY_DROPLET) ] } flask10 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , irarity = [(10, 2)] -- not very useful right now , iaspects = ELabel "of cat tears" : iaspects flaskTemplate , ieffects = [ toOrganGood S_SHINY_EYED (40 + 1 `d` 10) , OnSmash (Explode S_EYE_SHINE) ] } flask11 = flaskTemplate { iname = "bottle" , ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , icount = 1 `d` 3 -- the only one sometimes giving away its identity , iaspects = ELabel "of whiskey" : iaspects flaskTemplate , ieffects = [ toOrganGood S_DRUNK (20 + 1 `d` 5) , Burn 10, RefillHP 10, Yell , OnSmash (Explode S_WHISKEY_SPRAY) ] } flask12 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of bait cocktail" : iaspects flaskTemplate , ieffects = [ toOrganGood S_DRUNK (20 + 1 `d` 5) , Burn 1, RefillHP 3 -- risky exploit possible, good , Summon MOBILE_ANIMAL 1 , OnSmash (Summon MOBILE_ANIMAL 1) , OnSmash Impress -- mildly useful when thrown , OnSmash (Explode S_WASTE) ] } flask13 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of poison" : iaspects flaskTemplate , ieffects = [ toOrganNoTimer S_POISONED, toOrganNoTimer S_POISONED -- x2 , OnSmash (Explode S_POISON_CLOUD) ] } flask14 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of calamity" : iaspects flaskTemplate , ieffects = [ toOrganNoTimer S_POISONED , toOrganBad S_WEAKENED (20 + 1 `d` 5) , toOrganBad S_DEFENSELESS (20 + 1 `d` 5) , OnSmash (Explode S_GLASS_HAIL) ] -- enough glass to cause that } flask15 = flaskTemplate { ifreq = [ (COMMON_ITEM, 100), (ANY_FLASK, 100), (EXPLOSIVE, 100) , (ANY_GLASS, 100) ] , iaspects = ELabel "of snail gel" : iaspects flaskTemplate , ieffects = [ toOrganBad S_SLOWED (3 + 1 `d` 3) , OnSmash (Explode S_FOCUSED_SLOWNESS_MIST) ] } -- Potions are often not intended to be thrown. They are usually natural, -- including natural stat boosts. They also include the only healing -- consumables in the game, apart of elixirs and, to a limited extent, fruits. -- They appear deeper than most flasks. Various configurations of effects. -- A different class of effects is on scrolls and mechanical items. -- Some are shared. potionTemplate = ItemKind { isymbol = symbolPotion , iname = "potion" , ifreq = [(POTION_UNKNOWN, 1)] , iflavour = zipLiquid brightCol ++ zipPlain brightCol ++ zipFancy brightCol , icount = 1 `dL` 3 , irarity = [(1, 10), (10, 6)] , iverbHit = "splash" , iweight = 200 , idamage = 0 , iaspects = [ PresentAs POTION_UNKNOWN, SetFlag Lobable, SetFlag Fragile , toVelocity 50 ] -- oily, small momentum due to small size , ieffects = [] , idesc = "A vial of bright, frothing concoction. The best medicine that nature has to offer for wounds, ailments and mood swings." , ikit = [] } potion1 = potionTemplate { iname = "vial" , ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , icount = 3 `dL` 1 -- very useful, despite appearances , iaspects = ELabel "of rose water" : iaspects potionTemplate , ieffects = [ Impress, toOrganGood S_ROSE_SMELLING (50 + 1 `d` 10) , OnSmash ApplyPerfume, OnSmash (Explode S_FRAGRANCE) ] } potion2 = potionTemplate { iname = "the Potion" , ifreq = [(TREASURE, 100), (ANY_GLASS, 100)] , icount = 1 , irarity = [(5, 8), (10, 8)] , iaspects = [SetFlag Unique, ELabel "of Attraction", SetFlag MetaGame] ++ iaspects potionTemplate , ieffects = [ Dominate , toOrganGood S_HASTED (20 + 1 `d` 5) , OnSmash (Explode S_PHEROMONE) , OnSmash (Explode S_HASTE_SPRAY) ] , idesc = "The liquid fizzes with energy." } potion3 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , ieffects = [ RefillHP 5, DropItem 1 maxBound COrgan S_POISONED , OnSmash (Explode S_HEALING_MIST) ] } potion4 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , irarity = [(1, 6), (10, 10)] , ieffects = [ RefillHP 10 , DropItem maxBound maxBound COrgan CONDITION , OnSmash (Explode S_HEALING_MIST_2) ] } potion5 = potionTemplate { iname = "ampoule" -- probably filled with nitroglycerine, but let's -- not mix fantasy with too much technical jargon , ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , icount = 3 `dL` 1 , ieffects = [ DropItem 1 maxBound COrgan CONDITION , OnSmash (Explode S_VIOLENT_CONCUSSION) ] -- not fragmentation nor glass hail, because not enough glass } potion6 = potionTemplate -- needs to be common to show at least a portion of effects { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , icount = 3 `dL` 1 -- always as many as possible on this level -- without giving away potion identity , irarity = [(1, 12)] , ieffects = [ OneOf [ RefillHP 10, RefillHP 5, Burn 5 , DropItem 1 maxBound COrgan S_POISONED , toOrganGood S_STRENGTHENED (20 + 1 `d` 5) ] , OnSmash (OneOf [ Explode S_DENSE_SHOWER , Explode S_SPARSE_SHOWER , Explode S_MELEE_PROTECTIVE_BALM , Explode S_RANGE_PROTECTIVE_BALM , Explode S_DEFENSELESSNESS_RUNOUT ]) ] } potion7 = potionTemplate -- needs to be common to show at least a portion of effects { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , icount = 3 `dL` 1 , irarity = [(10, 10)] , ieffects = [ Impress , OneOf [ RefillHP 20, RefillHP 10, Burn 10 , DropItem 1 maxBound COrgan S_POISONED , toOrganGood S_HASTED (20 + 1 `d` 5) , toOrganBad S_IMPATIENT (2 + 1 `d` 2) ] , OnSmash (OneOf [ Explode S_HEALING_MIST_2 , Explode S_WOUNDING_MIST , Explode S_DISTRESSING_ODOR , Explode $ blastNoStatOf S_IMPATIENT , Explode S_HASTE_SPRAY , Explode S_VIOLENT_SLOWNESS_MIST , Explode S_FRAGRANCE , Explode S_VIOLENT_FLASH ]) ] } potion8 = potionTemplate { iname = "the Potion" , ifreq = [(TREASURE, 100), (ANY_GLASS, 100)] , icount = 1 , irarity = [(10, 5)] , iaspects = [SetFlag Unique, ELabel "of Love", SetFlag MetaGame] ++ iaspects potionTemplate , ieffects = [ RefillHP 60, RefillCalm (-60) , toOrganGood S_ROSE_SMELLING (80 + 1 `d` 20) , OnSmash (Explode S_HEALING_MIST_2) , OnSmash (Explode S_DISTRESSING_ODOR) ] , idesc = "Perplexing swirls of intense, compelling colour." } potion9 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , irarity = [(10, 5)] , iaspects = ELabel "of grenadier focus" : iaspects potionTemplate , ieffects = [ toOrganGood S_MORE_PROJECTING (40 + 1 `d` 10) , toOrganBad S_PACIFIED (5 + 1 `d` 3) -- the malus has to be weak, or would be too good -- when thrown at foes , OnSmash (Explode $ blastBonusStatOf S_MORE_PROJECTING) , OnSmash (Explode $ blastNoStatOf S_PACIFIED) ] , idesc = "Thick, sluggish fluid with violently-bursting bubbles." } potion10 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , irarity = [(10, 8)] , iaspects = ELabel "of frenzy" : iaspects potionTemplate , ieffects = [ Yell , toOrganGood S_STRENGTHENED (20 + 1 `d` 5) , toOrganBad S_RETAINING (5 + 1 `d` 3) , toOrganBad S_FRENZIED (40 + 1 `d` 10) , OnSmash (Explode S_DENSE_SHOWER) , OnSmash (Explode $ blastNoStatOf S_RETAINING) -- more , OnSmash (Explode $ blastNoStatOf S_RETAINING) ] -- explosion } potion11 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , irarity = [(10, 8)] , iaspects = ELabel "of panic" : iaspects potionTemplate , ieffects = [ RefillCalm (-30) , toOrganGood S_HASTED (20 + 1 `d` 5) , toOrganBad S_WEAKENED (20 + 1 `d` 5) , toOrganBad S_WITHHOLDING (10 + 1 `d` 5) , OnSmash (Explode S_HASTE_SPRAY) , OnSmash (Explode S_SPARSE_SHOWER) , OnSmash (Explode $ blastNoStatOf S_WITHHOLDING) ] } potion12 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , irarity = [(10, 8)] , iaspects = ELabel "of quicksilver" : iaspects potionTemplate , ieffects = [ toOrganGood S_HASTED (20 + 1 `d` 5) , toOrganBad S_BLIND (10 + 1 `d` 5) , toOrganBad S_IMMOBILE (5 + 1 `d` 5) , OnSmash (Explode S_HASTE_SPRAY) , OnSmash (Explode S_IRON_FILING) , OnSmash (Explode $ blastNoStatOf S_IMMOBILE) ] } potion13 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , irarity = [(10, 4)] , iaspects = ELabel "of slow resistance" : iaspects potionTemplate , ieffects = [ toOrganNoTimer S_SLOW_RESISTANT , OnSmash (Explode S_ANTI_SLOW_MIST) ] } potion14 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , irarity = [(10, 4)] , iaspects = ELabel "of poison resistance" : iaspects potionTemplate , ieffects = [ toOrganNoTimer S_POISON_RESISTANT , OnSmash (Explode S_ANTIDOTE_MIST) ] } -- The player has full control over throwing the potion at his party, -- so he can milk the explosion, so it has to be much weaker, so a weak -- healing effect is enough. OTOH, throwing a harmful flask at many enemies -- at once is not easy to arrange, so these explosions can stay powerful. potion15 = potionTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_POTION, 100), (ANY_GLASS, 100)] , irarity = [(1, 2), (10, 12)] , iaspects = ELabel "of regeneration" : iaspects potionTemplate , ieffects = [ toOrganGood S_ROSE_SMELLING (80 + 1 `d` 20) , toOrganNoTimer S_REGENERATING , toOrganNoTimer S_REGENERATING -- x2 , OnSmash (Explode S_YOUTH_SPRINKLE) ] } -- ** Non-exploding consumables, not specifically designed for throwing -- Readable or otherwise communicating consumables require high apply skill -- to be consumed. scrollTemplate = ItemKind { isymbol = symbolScroll , iname = "scroll" , ifreq = [(SCROLL_UNKNOWN, 1)] , iflavour = zipFancy stdCol ++ zipPlain stdCol , icount = 1 `dL` 3 , irarity = [(1, 14), (10, 7)] , iverbHit = "thump" , iweight = 50 , idamage = 0 , iaspects = [ PresentAs SCROLL_UNKNOWN , toVelocity 30 ] -- bad shape, even rolled up , ieffects = [] , idesc = "Scraps of haphazardly scribbled mysteries from beyond. Is this equation an alchemical recipe? Is this diagram an extradimensional map? Is this formula a secret call sign?" , ikit = [] } scroll1 = scrollTemplate { iname = "the Scroll" , ifreq = [(TREASURE, 100), (ANY_SCROLL, 100)] , icount = 1 , irarity = [(5, 9), (10, 9)] -- mixed blessing, so found early for a unique , iaspects = [SetFlag Unique, ELabel "of Reckless Beacon"] ++ iaspects scrollTemplate , ieffects = [Summon HERO 1, Summon MOBILE_ANIMAL (2 + 1 `d` 2)] , idesc = "The bright flame and sweet-smelling smoke of this heavily infused scroll should attract natural creatures inhabiting the area, including human survivors, if any." } scroll2 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , irarity = [(1, 6), (10, 2)] , ieffects = [Ascend False] } scroll3 = scrollTemplate -- needs to be common to show at least a portion of effects { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , icount = 3 `dL` 1 , irarity = [(1, 14)] , ieffects = [OneOf [ Teleport 5, Paralyze 10, InsertMove 30 , Detect DetectEmbed 12, Detect DetectHidden 20 ]] } scroll4 = scrollTemplate -- needs to be common to show at least a portion of effects { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , icount = 3 `dL` 1 , irarity = [(10, 14)] , ieffects = [ Impress , OneOf [ Teleport 20, Ascend False, Ascend True , OneOf [Summon HERO 1, Summon MOBILE_ANIMAL $ 1 `d` 2] -- gaining a hero particularly uncommon , Detect DetectLoot 20 -- the most useful of detections , CreateItem Nothing CGround COMMON_ITEM timerNone ] ] } scroll5 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , irarity = [(1, 6)] -- powerful, but low counts at the depths it appears on , ieffects = [InsertMove $ 20 + 1 `dL` 20] } scroll6 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , irarity = [(10, 11)] , ieffects = [PullActor (ThrowMod 800 75 1)] -- 6 steps, 1.5 turns } scroll7 = scrollTemplate { iname = "the Scroll" , ifreq = [(TREASURE, 100), (ANY_SCROLL, 100)] , icount = 1 , irarity = [(10, 12)] , iaspects = [SetFlag Unique, ELabel "of Rescue Proclamation"] ++ iaspects scrollTemplate , ieffects = [Summon HERO 1] , idesc = "A survivor of past exploration missions is found that enjoys, apparently, complete physiological integrity. We can pronounce him a comrade in arms and let him join our party." } scroll8 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , irarity = [(10, 4)] -- powerful, even if not ideal; scares newbies , ieffects = [Detect DetectAll 20] } scroll9 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , iaspects = ELabel "of cue interpretation" : iaspects scrollTemplate , ieffects = [Detect DetectActor 20] } scroll10 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , icount = 3 `dL` 1 , irarity = [(1, 20)] -- uncommon deep down, where all is known , iaspects = ELabel "of scientific explanation" : iaspects scrollTemplate , ieffects = [Identify `AndEffect` RefillCalm 10] , idesc = "The most pressing existential concerns are met with a deeply satisfying scientific answer." } scroll11 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , irarity = [(10, 20)] -- at gameover a crucial item may be missing , iaspects = ELabel "of transmutation" : iaspects scrollTemplate , ieffects = [PolyItem `AndEffect` Explode S_FIRECRACKER] } scroll12 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , irarity = [(10, 15)] , iaspects = ELabel "of transfiguration" : iaspects scrollTemplate , ieffects = [RerollItem] } scroll13 = scrollTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_SCROLL, 100)] , irarity = [(10, 15)] , iaspects = ELabel "of similarity" : iaspects scrollTemplate , ieffects = [DupItem] } -- Foods require only minimal apply skill to consume. Many animals can eat them. ediblePlantTemplate = ItemKind { isymbol = symbolFood , iname = "edible plant" , ifreq = [(EDIBLE_PLANT_UNKNOWN, 1)] , iflavour = zipFancy stdCol , icount = 1 `dL` 5 , irarity = [(1, 12), (10, 6)] -- let's feed the animals , iverbHit = "thump" , iweight = 50 , idamage = 0 , iaspects = [ PresentAs EDIBLE_PLANT_UNKNOWN , toVelocity 30 ] -- low density, often falling apart , ieffects = [] , idesc = "Withered but fragrant bits of a colorful plant. Taste tolerably and break down easily, but only eating may reveal the full effects." , ikit = [] } ediblePlant1 = ediblePlantTemplate { iname = "overripe berry" , ifreq = [(COMMON_ITEM, 100), (EDIBLE_PLANT, 100)] , ieffects = [RefillHP 1, toOrganBad S_IMMOBILE (5 + 1 `d` 5)] } ediblePlant2 = ediblePlantTemplate { iname = "frayed fungus" , ifreq = [(COMMON_ITEM, 100), (EDIBLE_PLANT, 100)] , ieffects = [toOrganNoTimer S_POISONED] } ediblePlant3 = ediblePlantTemplate { iname = "thick leaf" , ifreq = [(COMMON_ITEM, 100), (EDIBLE_PLANT, 100)] , ieffects = [DropItem 1 maxBound COrgan S_POISONED] } ediblePlant4 = ediblePlantTemplate { iname = "shrunk fruit" , ifreq = [(COMMON_ITEM, 100), (EDIBLE_PLANT, 100)] , ieffects = [toOrganBad S_BLIND (10 + 1 `d` 10)] } ediblePlant5 = ediblePlantTemplate { iname = "fragrant herb" , ifreq = [(COMMON_ITEM, 100), (EDIBLE_PLANT, 100)] , icount = 1 `dL` 9 , irarity = [(1, 12), (10, 5)] , iaspects = ELabel "of lethargy" : iaspects ediblePlantTemplate , ieffects = [ toOrganBad S_SLOWED (20 + 1 `d` 5) , toOrganNoTimer S_REGENERATING , toOrganNoTimer S_REGENERATING -- x2 , RefillCalm 5 ] } ediblePlant6 = ediblePlantTemplate { iname = "dull flower" , ifreq = [(COMMON_ITEM, 100), (EDIBLE_PLANT, 100)] , ieffects = [PutToSleep] } ediblePlant7 = ediblePlantTemplate { iname = "spicy bark" , ifreq = [(COMMON_ITEM, 100), (EDIBLE_PLANT, 100)] , ieffects = [InsertMove 20, toOrganBad S_FRENZIED (40 + 1 `d` 10)] } -- ** Lights light1 = ItemKind { isymbol = symbolLight , iname = "wooden torch" , ifreq = [ (COMMON_ITEM, 100), (LIGHT_ATTENUATOR, 100) , (S_WOODEN_TORCH, 1) ] , iflavour = zipPlain [Brown] , icount = 1 `dL` 4 , irarity = [(1, 40), (4, 1)] , iverbHit = "scorch" , iweight = 1000 , idamage = 0 , iaspects = [ AddSkill SkShine 3 -- no malus, to lessen micromanagement , SetFlag Lobable, SetFlag Equipable , EqpSlot EqpSlotShine ] -- not Fragile; reusable flare , ieffects = [Burn 1] , idesc = "A heavy smoking wooden torch, improvised using a cloth soaked in tar, burning in an unsteady glow." , ikit = [] } light2 = ItemKind { isymbol = symbolLight , iname = "oil lamp" , ifreq = [(COMMON_ITEM, 100), (LIGHT_ATTENUATOR, 100)] , iflavour = zipPlain [BrYellow] , icount = 1 `dL` 2 , irarity = [(4, 10)] , iverbHit = "burn" , iweight = 1500 , idamage = 1 `d` 1 , iaspects = [ AddSkill SkShine 3 , SetFlag Lobable, SetFlag Fragile, SetFlag Equipable , EqpSlot EqpSlotShine ] , ieffects = [ Explode S_FOCUSED_BURNING_OIL_2 , OnSmash (Explode S_VIOLENT_BURNING_OIL_2) ] , idesc = "A small clay lamp filled with plant oil feeding a tiny wick." , ikit = [] } light3 = ItemKind { isymbol = symbolLight , iname = "brass lantern" , ifreq = [(COMMON_ITEM, 100), (LIGHT_ATTENUATOR, 100)] , iflavour = zipPlain [Red] , icount = 1 , irarity = [(10, 6)] , iverbHit = "burn" , iweight = 3000 , idamage = 2 `d` 1 , iaspects = [ AddSkill SkShine 4 , SetFlag Lobable, SetFlag Fragile, SetFlag Equipable , EqpSlot EqpSlotShine ] , ieffects = [ Explode S_FOCUSED_BURNING_OIL_4 , OnSmash (Explode S_VIOLENT_BURNING_OIL_4) ] , idesc = "Very bright and very heavy brass lantern." , ikit = [] } blanket = ItemKind { isymbol = symbolLight , iname = "wool blanket" , ifreq = [ (COMMON_ITEM, 100), (LIGHT_ATTENUATOR, 100) , (FIREPROOF_CLOTH, 1) ] , iflavour = zipPlain [BrBlack] , icount = 1 , irarity = [(1, 1)] -- not every playthrough needs one , iverbHit = "swoosh" , iweight = 1000 , idamage = 0 , iaspects = [ AddSkill SkShine (-10) , AddSkill SkArmorMelee 3, AddSkill SkMaxCalm 5 , SetFlag Lobable, SetFlag Equipable , EqpSlot EqpSlotArmorMelee ] -- not Fragile; reusable douse implement; -- douses torch, lamp and lantern in one action, -- both in equipment and when thrown at the floor , ieffects = [] , idesc = "Warm, comforting, and concealing, woven from soft wool." , ikit = [] } -- ** Periodic jewelry -- This looks like a necklace, but is not periodic. Instead, it auto-activates -- when under melee attack. gorget = necklaceTemplate { iname = "Old Gorget" , ifreq = [(COMMON_ITEM, 25), (TREASURE, 25)] , iflavour = zipFancy [BrCyan] -- looks exactly the same as one of necklaces, -- but it's OK, it's an artifact , iaspects = [ SetFlag Unique , Timeout $ 7 - 1 `dL` 4 -- the dL dice need to be in negative positions -- for negative stats, such as @Timeout@, so that -- the @RerollItem@ effect makes the item better, not worse , AddSkill SkArmorMelee 3, AddSkill SkHearing 3 , SetFlag UnderMelee, SetFlag Durable ] ++ delete (SetFlag Periodic) iaspects_necklaceTemplate , ieffects = [RefillCalm 15] , idesc = "Highly ornamental, cold, large steel medallion on a chain. Unlikely to offer much protection as an armor piece, but the old worn engraving reassures the wearer." } -- Morally these are the aspects, but we also need to add a fake @Timeout@, -- to let clients know that the not identified item is periodic jewelry. iaspects_necklaceTemplate :: [Aspect] iaspects_necklaceTemplate = [ PresentAs NECKLACE_UNKNOWN , SetFlag Periodic, SetFlag Precious, SetFlag Equipable , toVelocity 50 ] -- not dense enough -- Not identified, because id by use, e.g., via periodic activations. Fun. necklaceTemplate = ItemKind { isymbol = symbolNecklace , iname = "necklace" , ifreq = [(NECKLACE_UNKNOWN, 1)] , iflavour = zipFancy stdCol ++ zipPlain brightCol , icount = 1 , irarity = [(4, 3), (10, 6)] , iverbHit = "whip" , iweight = 30 , idamage = 0 , iaspects = Timeout 1000000 -- fake, needed to display "charging"; the timeout itself -- won't be displayed thanks to periodic; as a side-effect, -- it can't be activated until identified, which is better -- than letting the player try to activate before the real -- cooldown is over and waste turn : iaspects_necklaceTemplate , ieffects = [] , idesc = "Menacing Greek symbols shimmer with increasing speed along a chain of fine encrusted links. After a tense build-up, a prismatic arc shoots towards the ground and the iridescence subdues, becomes ordered and resembles a harmless ornament again, for a time." , ikit = [] } necklace1 = necklaceTemplate { iname = "the Necklace" , ifreq = [(TREASURE, 100), (ANY_JEWELRY, 100)] , irarity = [(10, 3)] , iaspects = [ SetFlag Unique, ELabel "of Aromata" , Timeout $ (4 - 1 `dL` 3) * 10 -- priceless, so worth the long wait and Calm drain , SetFlag Durable ] ++ iaspects_necklaceTemplate , ieffects = [ RefillCalm (-5) , When (TriggeredBy ActivationPeriodic) $ RefillHP 1 ] , idesc = "A cord of freshly dried herbs and healing berries." } necklace2 = necklaceTemplate { iname = "the Necklace" , ifreq = [(TREASURE, 100), (ANY_JEWELRY, 100)] -- too nasty to call it just a COMMON_ITEM , irarity = [(10, 3)] , iaspects = [ SetFlag Unique, ELabel "of Live Bait" , Timeout 30 , AddSkill SkOdor 2 , SetFlag Durable ] ++ iaspects_necklaceTemplate , ieffects = [ DropItem 1 1 COrgan CONDITION -- mildly useful when applied , When (TriggeredBy ActivationPeriodic) $ SeqEffect [ Impress , Summon MOBILE_ANIMAL $ 1 `dL` 2 , Explode S_WASTE ] ] , idesc = "A cord hung with lumps of decaying meat. It's better not to think about the source." } necklace3 = necklaceTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , iaspects = [ ELabel "of fearful listening" , Timeout 40 -- has to be larger than Calm drain or item not removable; -- equal is not enough if enemies drained Calm already , AddSkill SkHearing 6 ] ++ iaspects_necklaceTemplate , ieffects = [ Detect DetectActor 20 -- can be applied; destroys the item , When (TriggeredBy ActivationPeriodic) $ RefillCalm (-30) ] } necklace4 = necklaceTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , iaspects = [ ELabel "of escape" , Timeout $ (7 - 1 `dL` 5) * 10 ] ++ iaspects_necklaceTemplate , ieffects = [ Teleport $ 14 + 3 `d` 3 -- can be applied; destroys the item , Detect DetectExit 20 , Yell ] -- drawback when used for quick exploring , idesc = "A supple chain that slips through your fingers." } necklace5 = necklaceTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , iaspects = [ ELabel "of greed" , Timeout ((2 + 1 `d` 3) * 10) ] ++ iaspects_necklaceTemplate , ieffects = [ Detect DetectLoot 20 , toOrganBad S_PARSIMONIOUS (5 + 1 `d` 3) -- hard to flee , When (TriggeredBy ActivationPeriodic) $ Teleport 40 ] -- risky } necklace6 = necklaceTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , iaspects = Timeout ((3 + 1 `d` 3 - 1 `dL` 3) * 2) : iaspects_necklaceTemplate -- OP if Durable; free blink , ieffects = [Teleport $ 3 `d` 2] } necklace7 = necklaceTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , iaspects = Timeout ((1 `d` 3) * 2) : iaspects_necklaceTemplate , ieffects = [PushActor (ThrowMod 100 50 1)] -- 1 step, slow -- the @50@ is only for the case of very light actor, etc. } necklace8 = necklaceTemplate { iname = "the Necklace" , ifreq = [(TREASURE, 100), (ANY_JEWELRY, 100)] , irarity = [(10, 1)] -- different gameplay for the actor that wears it , iaspects = [ SetFlag Unique, ELabel "of Overdrive" , Timeout 4 , AddSkill SkMaxHP 25 -- give incentive to cope with impatience , SetFlag Durable ] ++ iaspects_necklaceTemplate , ieffects = [ InsertMove $ 9 + 1 `d` 11 -- unpredictable , toOrganBad S_IMPATIENT 4] -- The same duration as timeout, to avoid spurious messages -- as well as unlimited accumulation of the duration. , idesc = "A string of beads in various colours, with no discernable pattern." } necklace9 = necklaceTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , irarity = [(4, 3)] -- entirely optional , iaspects = Timeout ((1 + 1 `d` 3) * 5) -- low timeout for offensive use : iaspects_necklaceTemplate , ieffects = [Explode S_SPARK] } necklace10 = necklaceTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , iaspects = Timeout ((3 + 1 `d` 3) * 10) : iaspects_necklaceTemplate -- high timeout to prevent spam obscuring messages -- when other actors act and annoying bumping into -- projectiles caused by own necklace when walking , ieffects = [Explode S_FRAGRANCE] } motionScanner = necklaceTemplate { iname = "draft detector" , ifreq = [(COMMON_ITEM, 100), (ADD_NOCTO_1, 20)] , irarity = [(5, 2)] , iverbHit = "jingle" , iweight = 300 -- almost gives it away , iaspects = [ Timeout $ 4 + 1 `dL` 6 -- positive dL dice, since the periodic effect is detrimental , AddSkill SkNocto 1 , AddSkill SkArmorMelee $ (-4 + 1 `dL` 3) * 5 , EqpSlot EqpSlotMiscBonus ] ++ iaspects_necklaceTemplate , ieffects = [Explode S_PING_PLASH] , idesc = "A silk flag with a bell for detecting sudden draft changes. May indicate a nearby corridor crossing or a fast enemy approaching in the dark. The bell is very noisy and casts light reflection flashes." } -- ** Non-periodic jewelry imageItensifier = ItemKind { isymbol = symbolRing , iname = "light cone" , ifreq = [(TREASURE, 100), (ADD_NOCTO_1, 80)] , iflavour = zipFancy [BrYellow] , icount = 1 , irarity = [(5, 2)] , iverbHit = "bang" , iweight = 500 , idamage = 0 , iaspects = [ AddSkill SkNocto 1 , AddSkill SkArmorMelee $ (-6 + 1 `dL` 3) * 5 , SetFlag Precious, SetFlag Equipable , EqpSlot EqpSlotMiscBonus ] , ieffects = [] , idesc = "Contraption of lenses and mirrors on a polished brass headband for capturing and strengthening light in dark environment. Hampers vision in daylight. Stackable." , ikit = [] } sightSharpening = ringTemplate -- small and round, so mistaken for a ring { iname = "sharp monocle" , ifreq = [(TREASURE, 20), (ADD_SIGHT, 1)] -- it's has to be very rare, because it's powerful and not unique, -- and also because it looks exactly as one of necklaces, so it would -- be misleading when seen on the map , irarity = [(7, 1), (10, 12)] -- low @ifreq@ , iweight = 50 -- heavier that it looks, due to glass , iaspects = [ AddSkill SkSight $ 1 + 1 `dL` 2 , AddSkill SkHurtMelee $ (1 `d` 3) * 3 , EqpSlot EqpSlotSight ] ++ iaspects ringTemplate , idesc = "Lets you better focus your weaker eye." } -- Don't add standard effects to rings, because they go in and out -- of eqp and so activating them would require UI tedium: looking for -- them in eqp and stash or even activating a wrong item by mistake. -- -- By general mechanisms, due to not having effects that could identify -- them by observing the effect, rings are identified on pickup. -- That's unlike necklaces, which provide the fun of id-by-use, because they -- have effects and when the effects are triggered, they get identified. ringTemplate = ItemKind { isymbol = symbolRing , iname = "ring" , ifreq = [(RING_UNKNOWN, 1)] , iflavour = zipPlain stdCol ++ zipFancy darkCol , icount = 1 , irarity = [(10, 2)] -- the default very low , iverbHit = "knock" , iweight = 15 , idamage = 0 , iaspects = [PresentAs RING_UNKNOWN, SetFlag Precious, SetFlag Equipable] , ieffects = [] , idesc = "It looks like an ordinary object, but it's in fact a generator of exceptional effects: adding to some of your natural qualities and subtracting from others." , ikit = [] } ring1 = ringTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , irarity = [(8, 4)] , iaspects = [ AddSkill SkSpeed $ 1 `dL` 2 , AddSkill SkMaxHP (-20) , EqpSlot EqpSlotSpeed ] ++ iaspects ringTemplate } ring2 = ringTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , irarity = [(8, 4)] , iaspects = [ AddSkill SkSpeed $ 1 + 1 `dL` 3 , AddSkill SkArmorMelee (-40) , EqpSlot EqpSlotSpeed ] ++ iaspects ringTemplate } ring3 = ringTemplate { iname = "the Ring" , ifreq = [(TREASURE, 100), (ANY_JEWELRY, 100)] , iaspects = [ SetFlag Unique, ELabel "of Rush" , AddSkill SkSpeed $ (1 + 1 `dL` 2) * 2 , AddSkill SkMaxHP (-20) , AddSkill SkArmorMelee (-20) , SetFlag Durable, EqpSlot EqpSlotSpeed ] ++ iaspects ringTemplate , idesc = "Roughly-shaped metal with shallow scratches marking it." } ring4 = ringTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , irarity = [(3, 4), (10, 8)] , iaspects = [ AddSkill SkHurtMelee $ (2 + 1 `d` 3 + (1 `dL` 2) * 2 ) * 3 , AddSkill SkMaxHP (-10) , EqpSlot EqpSlotHurtMelee ] ++ iaspects ringTemplate } ring5 = ringTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , iaspects = [ AddSkill SkHurtMelee $ (4 + 1 `d` 3 + (1 `dL` 2) * 2 ) * 3 , AddSkill SkArmorMelee (-20) , EqpSlot EqpSlotHurtMelee ] ++ iaspects ringTemplate } ring6 = ringTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , irarity = [(10, 8)] , iaspects = [ AddSkill SkMaxHP $ 5 + (1 `d` 2 + 1 `dL` 2) * 5 , AddSkill SkMaxCalm $ -30 + (1 `dL` 3) * 5 , EqpSlot EqpSlotMaxHP ] ++ iaspects ringTemplate } ring7 = ringTemplate { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , irarity = [(5, 1), (10, 9)] -- needed after other items drop Calm , iaspects = [ AddSkill SkMaxCalm $ 30 + (1 `dL` 4) * 5 , AddSkill SkHearing 6 , EqpSlot EqpSlotMiscBonus ] ++ iaspects ringTemplate , idesc = "Cold, solid to the touch, perfectly round, engraved with solemn, strangely comforting, worn out words." } ring8 = ringTemplate -- weak skill per eqp slot, so can be without drawbacks { ifreq = [(COMMON_ITEM, 100), (ANY_JEWELRY, 100)] , irarity = [(10, 3)] , iaspects = [ AddSkill SkShine 1 , EqpSlot EqpSlotShine ] ++ iaspects ringTemplate , idesc = "A sturdy ring with a large, shining stone." } ring9 = ringTemplate { ifreq = [(RING_OF_OPPORTUNITY_SNIPER, 1) ] -- only for scenarios , irarity = [(1, 1)] , iaspects = [ ELabel "of opportunity sniper" , AddSkill SkProject 8 , EqpSlot EqpSlotProject ] ++ iaspects ringTemplate } ring10 = ringTemplate { ifreq = [(RING_OF_OPPORTUNITY_GRENADIER, 1) ] -- only for scenarios , irarity = [(1, 1)] , iaspects = [ ELabel "of opportunity grenadier" , AddSkill SkProject 11 , EqpSlot EqpSlotProject ] ++ iaspects ringTemplate } -- ** Armor armorLeather = ItemKind { isymbol = symbolTorsoArmor , iname = "leather armor" , ifreq = [(COMMON_ITEM, 100), (ARMOR_LOOSE, 1), (STARTING_ARMOR, 100)] , iflavour = zipPlain [Brown] , icount = 1 , irarity = [(1, 9), (10, 2)] , iverbHit = "thud" , iweight = 7000 , idamage = 0 , iaspects = [ AddSkill SkHurtMelee (-2) , AddSkill SkArmorMelee $ (2 + 1 `dL` 4) * 5 , AddSkill SkArmorRanged $ (1 + 1 `dL` 2) * 3 , SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotArmorMelee ] , ieffects = [] , idesc = "A stiff jacket formed from leather boiled in bee wax, padded linen and horse hair. Protects from anything that is not too sharp. Smells much better than the rest of your garment." , ikit = [] } armorMail = armorLeather { iname = "ring armor" , ifreq = [ (COMMON_ITEM, 100), (ARMOR_LOOSE, 1), (ARMOR_RANGED, 50) , (STARTING_ARMOR, 50) ] , iflavour = zipPlain [Cyan] , irarity = [(6, 9), (10, 3)] , iweight = 12000 , idamage = 0 , iaspects = [ AddSkill SkHurtMelee (-3) , AddSkill SkArmorMelee $ (2 + 1 `dL` 4) * 5 , AddSkill SkArmorRanged $ (4 + 1 `dL` 2) * 3 , AddSkill SkOdor 2 , SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotArmorRanged ] , ieffects = [] , idesc = "A long shirt with tiny iron rings sewn into it. Discourages foes from attacking your torso, especially with ranged weapons, which can't pierce the rings nor aim between them. The stiff fabric is hard to wash, though." } meleeEnhancement = ItemKind { isymbol = symbolTool , iname = "whetstone" , ifreq = [(COMMON_ITEM, 100)] , iflavour = zipPlain [Blue] , icount = 1 , irarity = [(10, 10)] , iverbHit = "smack" , iweight = 400 , idamage = 0 , iaspects = [ AddSkill SkHurtMelee $ (1 `dL` 7) * 5 , AddSkill SkArmorMelee 2 , SetFlag Equipable, EqpSlot EqpSlotHurtMelee ] , ieffects = [] , idesc = "A portable sharpening stone for keeping your weapons keen and true, without the need to set up camp, fish out tools and assemble a proper sharpening workshop. Provides an extra polish to amor, as well." , ikit = [] } gloveFencing = ItemKind { isymbol = symbolMiscArmor , iname = "leather glove" , ifreq = [ (COMMON_ITEM, 100), (ARMOR_MISC, 1), (ARMOR_RANGED, 50) , (STARTING_ARMOR, 50) ] , iflavour = zipPlain [White] , icount = 1 , irarity = [(5, 9), (10, 9)] , iverbHit = "flap" , iweight = 100 , idamage = 1 `d` 1 , iaspects = [ AddSkill SkHurtMelee $ (2 + 1 `d` 2 + 1 `dL` 2) * 3 , AddSkill SkArmorRanged $ (1 `dL` 2) * 3 , SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotHurtMelee , toVelocity 50 ] -- flaps and flutters , ieffects = [] , idesc = "A fencing glove from rough leather ensuring a good grip. Also quite effective in averting or even catching slow projectiles." , ikit = [] } gloveGauntlet = gloveFencing { iname = "steel gauntlet" , ifreq = [(COMMON_ITEM, 100), (ARMOR_MISC, 1), (STARTING_ARMOR, 50)] , iflavour = zipPlain [BrCyan] , irarity = [(1, 9), (10, 3)] , iverbHit = "mow" , iweight = 300 , idamage = 2 `d` 1 , iaspects = [ AddSkill SkArmorMelee $ (1 + 1 `dL` 4) * 5 , SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotArmorMelee , toVelocity 50 ] -- flaps and flutters , idesc = "Long leather gauntlet covered in overlapping steel plates." } gloveJousting = gloveFencing { iname = "Tournament Gauntlet" , ifreq = [(COMMON_ITEM, 100), (ARMOR_MISC, 1)] , iflavour = zipFancy [BrRed] , irarity = [(1, 3), (10, 3)] , iverbHit = "ram" , iweight = 3000 , idamage = 3 `d` 1 , iaspects = [ SetFlag Unique , AddSkill SkHurtMelee $ (-7 + 1 `dL` 5) * 3 , AddSkill SkArmorMelee $ (2 + 1 `d` 2 + 1 `dL` 2) * 5 , AddSkill SkArmorRanged $ (1 + 1 `dL` 2) * 3 -- very random on purpose and can even be good on occasion -- or when ItemRerolled enough times , SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotArmorMelee , toVelocity 50 ] -- flaps and flutters , idesc = "Rigid, steel jousting handgear. If only you had a lance. And a horse to carry it all." } hatUshanka = ItemKind { isymbol = symbolMiscArmor , iname = "ushanka hat" , ifreq = [ (COMMON_ITEM, 100), (ARMOR_MISC, 1), (CLOTHING_MISC, 1) , (STARTING_ARMOR, 50) ] , iflavour = zipPlain [Brown] , icount = 1 , irarity = [(1, 6), (10, 1)] , iverbHit = "tickle" , iweight = 500 , idamage = 0 , iaspects = [ Timeout $ (2 + 1 `d` 2) * 3 , AddSkill SkArmorMelee 5, AddSkill SkHearing (-6) , SetFlag Periodic, SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotArmorMelee , toVelocity 50 ] -- flaps and flutters , ieffects = [RefillCalm 1] , idesc = "Soft and warm fur. It keeps your ears warm." , ikit = [] } capReinforced = ItemKind { isymbol = symbolMiscArmor , iname = "leather cap" , ifreq = [(COMMON_ITEM, 100), (ARMOR_MISC, 1), (STARTING_ARMOR, 50)] , iflavour = zipPlain [BrYellow] , icount = 1 , irarity = [(6, 9), (10, 3)] , iverbHit = "cut" , iweight = 1000 , idamage = 0 , iaspects = [ AddSkill SkArmorMelee $ (1 `d` 2) * 5 , AddSkill SkProject 1 -- the brim shields against blinding by light sources, etc. , SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotProject ] , ieffects = [] , idesc = "Boiled leather with a wide brim. It might soften a blow." , ikit = [] } helmArmored = ItemKind { isymbol = symbolMiscArmor , iname = "bucket helm" , ifreq = [(COMMON_ITEM, 100), (ARMOR_MISC, 1), (STARTING_ARMOR, 50)] , iflavour = zipPlain [BrCyan] , icount = 1 , irarity = [(6, 9), (10, 3)] , iverbHit = "bounce" , iweight = 2000 , idamage = 0 , iaspects = [ AddSkill SkArmorMelee $ (1 + 1 `dL` 4) * 5 , AddSkill SkArmorRanged $ (2 + 1 `dL` 2) * 3 -- headshot , AddSkill SkSight (-2) , AddSkill SkHearing (-3), AddSkill SkSmell (-5) , SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotArmorRanged ] , ieffects = [] , idesc = "Blocks out everything, including your senses." , ikit = [] } smokingJacket = ItemKind { isymbol = symbolClothes , iname = "smoking jacket" , ifreq = [(COMMON_ITEM, 100), (CLOTHING_MISC, 1), (CHIC_GEAR, 100)] , iflavour = zipFancy [BrGreen] , icount = 1 , irarity = [(1, 9), (10, 3)] , iverbHit = "stroke" , iweight = 5000 , idamage = 0 , iaspects = [ Timeout $ (1 `d` 2) * 3 , AddSkill SkSpeed 2 , AddSkill SkOdor 2 , SetFlag Periodic, SetFlag Durable, SetFlag Equipable , EqpSlot EqpSlotSpeed ] , ieffects = [RefillCalm 1] , idesc = "Wearing this velvet jacket, anyone would look dashing." , ikit = [] } -- Shield doesn't protect against ranged attacks to prevent -- micromanagement: walking with shield, melee without. -- Their biggest power is pushing enemies, which however reduces -- to 1 extra damage point if no clear space behind enemy. -- So they require keen tactical management. -- Note that AI will pick them up but never wear and will use them at most -- as a way to push itself. Despite being @Meleeable@, they will not be used -- as weapons either. This is OK, using shields smartly is totally beyond AI. buckler = ItemKind { isymbol = symbolShield , iname = "buckler" , ifreq = [(COMMON_ITEM, 100), (ARMOR_LOOSE, 1)] , iflavour = zipPlain [Blue] , icount = 1 , irarity = [(4, 5)] , iverbHit = "bash" , iweight = 2000 , idamage = 0 -- safe to be used on self , iaspects = [ Timeout $ (3 + 1 `d` 3 - 1 `dL` 3) * 2 , AddSkill SkArmorMelee 40 -- not enough to compensate; won't be in AI's eqp , AddSkill SkHurtMelee (-30) -- too harmful; won't be wielded as weapon , SetFlag Durable, SetFlag Meleeable , EqpSlot EqpSlotArmorMelee ] , ieffects = [PushActor (ThrowMod 200 50 1)] -- 1 step, fast , idesc = "Heavy and unwieldy. Absorbs a percentage of melee damage, both dealt and sustained. Too small to intercept projectiles with. May serve as a counterweight to suddenly push forth." , ikit = [] } shield = buckler { iname = "shield" , irarity = [(8, 4)] -- the stronger variants add to total probability , iflavour = zipPlain [Green] , iweight = 4000 , idamage = 4 `d` 1 , iaspects = [ Timeout $ (3 + 1 `d` 3 - 1 `dL` 3) * 4 , AddSkill SkArmorMelee 80 -- not enough to compensate; won't be in AI's eqp , AddSkill SkHurtMelee (-70) -- too harmful; won't be wielded as weapon , SetFlag Durable, SetFlag Meleeable , EqpSlot EqpSlotArmorMelee , toVelocity 50 ] -- unwieldy to throw , ieffects = [PushActor (ThrowMod 400 50 1)] -- 2 steps, fast , idesc = "Large and unwieldy. Absorbs a percentage of melee damage, both dealt and sustained. Too heavy to intercept projectiles with. Useful to push foes out of the way." } shield2 = shield { ifreq = [(COMMON_ITEM, 3 * 3)] -- very low base rarity , iweight = 5000 , idamage = 8 `d` 1 , idesc = "A relic of long-past wars, heavy and with a central spike." } shield3 = shield2 { ifreq = [(COMMON_ITEM, 1 * 3)] -- very low base rarity , iweight = 6000 , idamage = 12 `d` 1 } -- ** Weapons knife = ItemKind { isymbol = symbolEdged , iname = "dagger" , ifreq = [(COMMON_ITEM, 100), (STARTING_WEAPON, 200)] , iflavour = zipPlain [BrCyan] , icount = 1 , irarity = [(2, 45), (4, 1)] , iverbHit = "cut" , iweight = 800 , idamage = 6 `d` 1 , iaspects = [ Timeout 2 , AddSkill SkHurtMelee $ (-1 + 1 `d` 2 + 1 `dL` 2) * 3 , AddSkill SkArmorMelee $ (1 `d` 2) * 5 -- very common, so don't make too random , SetFlag Durable, SetFlag Meleeable , EqpSlot EqpSlotWeaponFast , toVelocity 40 ] -- ensuring it hits with the tip costs speed , ieffects = [] , idesc = "A short dagger for thrusting and parrying blows. Does not penetrate deeply, but is quick to move and hard to block. Especially useful in conjunction with a larger weapon." , ikit = [] } daggerDischarge = knife { iname = "The Double Dagger" , ifreq = [(TREASURE, 20)] , irarity = [(1, 3), (10, 3)] , iaspects = SetFlag Unique : iaspects knife , ieffects = [Discharge 1 50, Yell] -- powerful and low timeout, but noisy -- and no effect if no weapons charged , idesc = "A double dagger that a focused fencer can use to catch and twist away an opponent's blade." } hammerTemplate = ItemKind { isymbol = symbolHafted , iname = "war hammer" , ifreq = [(HAMMER_UNKNOWN, 1)] , iflavour = zipFancy [BrMagenta] -- avoid "pink" , icount = 1 , irarity = [(3, 25), (5, 1)] , iverbHit = "club" , iweight = 1600 , idamage = 8 `d` 1 -- we are lying about the dice here, but the dungeon -- is too small and the extra-dice hammers too rare -- to subdivide this identification class by dice , iaspects = [ PresentAs HAMMER_UNKNOWN , SetFlag Durable, SetFlag Meleeable , toVelocity 40 ] -- ensuring it hits with the tip costs speed , ieffects = [] , idesc = "It may not cause extensive wounds, but neither does it harmlessly glance off heavy armour as blades and polearms tend to. There are so many shapes and types, some looking more like tools than weapons, that at a glance you can't tell what a particular specimen does. It's obvious, though, that any of them requires some time to recover after a swing." -- if it's really the average kind, the weak kind, the description stays; if not, it's replaced with one of the descriptions below at identification time , ikit = [] } hammer1 = hammerTemplate { ifreq = [(COMMON_ITEM, 100), (STARTING_WEAPON, 70)] , iaspects = [Timeout 5, EqpSlot EqpSlotWeaponBig] ++ iaspects hammerTemplate } hammer2 = hammerTemplate { ifreq = [(COMMON_ITEM, 20), (STARTING_WEAPON, 7)] , iverbHit = "gouge" , iaspects = [Timeout 3, EqpSlot EqpSlotWeaponFast] ++ iaspects hammerTemplate , idesc = "Upon closer inspection, this hammer turns out particularly handy and well balanced, with one thick and sturdy and two long and sharp points compensating the modest size." } hammer3 = hammerTemplate { ifreq = [(COMMON_ITEM, 3), (STARTING_WEAPON, 1)] , iverbHit = "puncture" , iweight = 2400 , idamage = 12 `d` 1 , iaspects = [ Timeout 12 -- balance, or @DupItem@ would break the game , SetFlag MetaGame -- weight gives it away after seen once , EqpSlot EqpSlotWeaponBig] ++ iaspects hammerTemplate , idesc = "This hammer sports a long metal handle that increases the momentum of the sharpened head's swing, at the cost of long recovery." } hammerParalyze = hammerTemplate { iname = "The Brute Hammer" , ifreq = [(TREASURE, 20)] , irarity = [(5, 1), (8, 6)] , iaspects = [ SetFlag Unique , Timeout 5 , EqpSlot EqpSlotWeaponBig ] ++ iaspects hammerTemplate , ieffects = [Paralyze 10] , idesc = "A huge shapeless lump of meteorite iron alloy on a sturdy pole. Nobody remains standing when this head connects." } hammerSpark = hammerTemplate { iname = "The Grand Smithhammer" , ifreq = [(TREASURE, 20)] , irarity = [(5, 1), (8, 6)] , iweight = 2400 , idamage = 12 `d` 1 , iaspects = [ SetFlag Unique , SetFlag MetaGame -- weight gives it away after seen once , Timeout 10 , EqpSlot EqpSlotWeaponBig , AddSkill SkShine 3] ++ iaspects hammerTemplate , ieffects = [Explode S_SPARK] -- we can't use a focused explosion, because it would harm the hammer -- wielder as well, unlike this one , idesc = "Smiths of old wielded this heavy hammer and its sparks christened many a potent blade." } sword = ItemKind { isymbol = symbolEdged , iname = "sword" , ifreq = [(COMMON_ITEM, 100), (STARTING_WEAPON, 30)] , iflavour = zipPlain [BrBlue] , icount = 1 , irarity = [(4, 1), (6, 15)] , iverbHit = "slash" , iweight = 2000 , idamage = 10 `d` 1 , iaspects = [ Timeout 7 , SetFlag Durable, SetFlag Meleeable , EqpSlot EqpSlotWeaponBig , toVelocity 40 ] -- ensuring it hits with the tip costs speed , ieffects = [] , idesc = "Difficult to master; deadly when used effectively. The steel is particularly hard and keen, but rusts quickly without regular maintenance." , ikit = [] } swordImpress = sword { iname = "The Master's Sword" , ifreq = [(TREASURE, 20)] , irarity = [(5, 1), (8, 6)] , iaspects = SetFlag Unique : iaspects sword , ieffects = [Impress] , idesc = "A particularly well-balance blade, lending itself to impressive shows of fencing skill." } swordNullify = sword { iname = "The Gutting Sword" , ifreq = [(TREASURE, 20)] , iverbHit = "pierce" , irarity = [(5, 1), (8, 6)] , iaspects = [SetFlag Unique, Timeout 3, EqpSlot EqpSlotWeaponFast] ++ (iaspects sword \\ [Timeout 7, EqpSlot EqpSlotWeaponBig]) , ieffects = [ DropItem 1 maxBound COrgan CONDITION , RefillCalm (-10) , Yell ] , idesc = "Cold, thin blade that pierces deeply and sends its victim into abrupt, sobering shock." } halberd = ItemKind { isymbol = symbolPolearm , iname = "war scythe" , ifreq = [(COMMON_ITEM, 100), (STARTING_WEAPON, 20)] , iflavour = zipPlain [BrYellow] , icount = 1 , irarity = [(5, 1), (8, 12)] , iverbHit = "impale" , iweight = 3000 , idamage = 12 `d` 1 , iaspects = [ Timeout 10 , AddSkill SkHurtMelee $ (-5 + 1 `dL` 3) * 5 -- useless against armor at game start , AddSkill SkArmorMelee 20 , SetFlag Durable, SetFlag Meleeable , EqpSlot EqpSlotWeaponBig , toVelocity 20 ] -- not balanced , ieffects = [] , idesc = "An improvised weapon made of scythe's blade attached to a long pole. Not often one succeeds in making enough space to swing it freely, but even when stuck between terrain obstacles it blocks approaches effectively and makes using other weapons difficult, both by friends and foes." , ikit = [] } halberd2 = halberd { iname = "halberd" , ifreq = [(COMMON_ITEM, 3 * 2), (STARTING_WEAPON, 1)] , iweight = 4000 , iaspects = AddSkill SkHurtMelee ((-6 + 1 `dL` 4) * 10) -- balance, or @DupItem@ would break the game; -- together with @RerollItem@, it's allowed to, though : (iaspects halberd \\ [AddSkill SkHurtMelee $ (-6 + 1 `dL` 4) * 5]) , idamage = 18 `d` 1 , idesc = "A long haft with a sharp blade. Designed and refined for war." } halberd3 = halberd2 { iname = "bardiche" , ifreq = [(COMMON_ITEM, 1 * 2)] -- compensating for low base rarity , iverbHit = "carve" , iweight = 5000 , idamage = 24 `d` 1 , idesc = "The reach of a spear but the edge of an axe." } halberdPushActor = halberd { iname = "The Swiss Halberd" , ifreq = [(TREASURE, 20)] , irarity = [(7, 0), (9, 15)] , iaspects = SetFlag Unique : iaspects halberd , ieffects = [PushActor (ThrowMod 200 100 1)] -- 2 steps, slow , idesc = "A versatile polearm, with great reach and leverage. Foes are held at a distance." } -- ** Treasure gemTemplate = ItemKind { isymbol = symbolGold , iname = "gem" , ifreq = [(GEM_UNKNOWN, 1), (VALUABLE, 100)] , iflavour = zipPlain $ delete BrYellow brightCol -- natural, so not fancy , icount = 1 , irarity = [(3, 0), (10, 24)] , iverbHit = "tap" , iweight = 50 , idamage = 0 , iaspects = [PresentAs GEM_UNKNOWN, SetFlag Precious] , ieffects = [] , idesc = "Useless, and still worth around 100 gold each. Would gems of thought and pearls of artful design be valued that much in our age of Science and Progress!" , ikit = [] } gem1 = gemTemplate { ifreq = [ (TREASURE, 100), (GEM, 100), (ANY_JEWELRY, 10) , (VALUABLE, 100) ] , irarity = [(3, 0), (6, 12), (10, 8)] , iaspects = [AddSkill SkShine 1, AddSkill SkSpeed (-1)] -- reflects strongly, distracts; so it glows in the dark, -- is visible on dark floor, but not too tempting to wear ++ iaspects gemTemplate } gem2 = gem1 { ifreq = [ (TREASURE, 150), (GEM, 100), (ANY_JEWELRY, 10) , (VALUABLE, 100) ] , irarity = [(5, 0), (7, 25), (10, 8)] } gem3 = gem1 { ifreq = [ (TREASURE, 150), (GEM, 100), (ANY_JEWELRY, 10) , (VALUABLE, 100) ] , irarity = [(7, 0), (8, 20), (10, 8)] } gem4 = gem1 { ifreq = [ (TREASURE, 150), (GEM, 100), (ANY_JEWELRY, 30) , (VALUABLE, 100) ] , irarity = [(9, 0), (10, 70)] } gem5 = gem1 { isymbol = symbolSpecial , iname = "elixir" , ifreq = [ (TREASURE, 100), (GEM, 25), (ANY_JEWELRY, 10) , (VALUABLE, 100) ] , iflavour = zipPlain [BrYellow] , irarity = [(1, 40), (10, 10)] , iaspects = [ ELabel "of youth", SetFlag Precious -- not hidden , AddSkill SkOdor (-1) ] , ieffects = [RefillCalm 10, RefillHP 40] , idesc = "A crystal vial of amber liquid, supposedly granting eternal youth and fetching 100 gold per piece. The main effect seems to be mild euphoria, but it admittedly smells good and heals minor ailments rather well." } currencyTemplate = ItemKind { isymbol = symbolGold , iname = "gold piece" , ifreq = [(CURRENCY_UNKNOWN, 1), (VALUABLE, 1)] , iflavour = zipPlain [BrYellow] , icount = 10 + 1 `d` 20 + 1 `dL` 20 , irarity = [(1, 25), (10, 10)] , iverbHit = "tap" , iweight = 31 , idamage = 0 , iaspects = [PresentAs CURRENCY_UNKNOWN, SetFlag Precious] , ieffects = [] , idesc = "Reliably valuable in every civilized plane of existence." , ikit = [] } currency = currencyTemplate { ifreq = [(TREASURE, 100), (S_CURRENCY, 100), (VALUABLE, 1)] , iaspects = [AddSkill SkShine 1, AddSkill SkSpeed (-1)] ++ iaspects currencyTemplate } -- ** Tools to be actively used, but not worn jumpingPole = ItemKind { isymbol = symbolWand , iname = "jumping pole" , ifreq = [(COMMON_ITEM, 90)] , iflavour = zipFancy [White] , icount = 1 , irarity = [(1, 3)] , iverbHit = "prod" , iweight = 10000 , idamage = 0 , iaspects = [ Timeout $ (2 + 1 `d` 2 - 1 `dL` 2) * 5 , SetFlag Durable ] , ieffects = [toOrganGood S_HASTED 1] -- This works and doesn't cause AI loops. @InsertMove@ -- would produce an activation that doesn't change game state. -- Hasting for an absolute number of turns would cause -- an explosion of time when several poles are accumulated. -- Here it speeds AI up for exactly the turn spent activating, -- so when AI applies it repeatedly, it gets its time back and -- is not stuck. In total, the exploration speed is unchanged, -- but it's useful when fleeing in the dark to make distance -- and when initiating combat, so it's OK that AI uses it. -- Timeout is rather high, because for factions with leaders -- some time is often gained, so this could be useful -- even during melee, which would be tiresome to employ. , idesc = "Makes you vulnerable at take-off, but then you are free like a bird." , ikit = [] } seeingItem = ItemKind { isymbol = symbolFood , iname = "giant pupil" , ifreq = [(COMMON_ITEM, 100)] , iflavour = zipPlain [Red] , icount = 1 , irarity = [(1, 2)] , iverbHit = "gaze at" , iweight = 100 , idamage = 0 , iaspects = [ Timeout 3 , AddSkill SkSight 10 -- a spyglass for quick wields , AddSkill SkMaxCalm 30 -- to diminish clipping sight by Calm , AddSkill SkShine 2 -- to lit corridors when flying , SetFlag Periodic ] , ieffects = [ Detect DetectActor 20 -- rare enough , When (TriggeredBy ActivationPeriodic) $ SeqEffect [ toOrganNoTimer S_POISONED -- really can't be worn , Summon MOBILE_MONSTER 1 ] ] , idesc = "A slimy, dilated green pupil torn out from some giant eye. Clear and focused, as if still alive." , ikit = [] } LambdaHack-0.11.0.0/GameDefinition/Content/ItemKindActor.hs0000644000000000000000000007123507346545000021424 0ustar0000000000000000-- | Actor (or rather actor body trunk) definitions. module Content.ItemKindActor ( -- * Group name patterns pattern S_WOODEN_TORCH, pattern S_SANDSTONE_ROCK , pattern HERO, pattern SCOUT_HERO, pattern RANGER_HERO, pattern ESCAPIST_HERO, pattern AMBUSHER_HERO, pattern BRAWLER_HERO, pattern SOLDIER_HERO, pattern CIVILIAN, pattern MONSTER, pattern MOBILE_MONSTER, pattern SCOUT_MONSTER, pattern ANIMAL, pattern MOBILE_ANIMAL, pattern IMMOBILE_ANIMAL, pattern INSECT, pattern GEOPHENOMENON , pattern ADD_SIGHT, pattern ARMOR_RANGED, pattern ADD_NOCTO_1, pattern WEAK_ARROW, pattern LIGHT_ATTENUATOR, pattern FIREPROOF_CLOTH, pattern RING_OF_OPPORTUNITY_SNIPER, pattern ANY_ARROW, pattern STARTING_ARMOR, pattern STARTING_WEAPON, pattern GEM , actorsGN, actorsGNSingleton , -- * Content actors ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour import Content.ItemKindOrgan -- * Group name patterns actorsGNSingleton :: [GroupName ItemKind] actorsGNSingleton = [S_WOODEN_TORCH, S_SANDSTONE_ROCK] pattern S_WOODEN_TORCH, S_SANDSTONE_ROCK :: GroupName ItemKind actorsGN :: [GroupName ItemKind] actorsGN = [HERO, SCOUT_HERO, RANGER_HERO, ESCAPIST_HERO, AMBUSHER_HERO, BRAWLER_HERO, SOLDIER_HERO, CIVILIAN, MONSTER, MOBILE_MONSTER, SCOUT_MONSTER, ANIMAL, MOBILE_ANIMAL, IMMOBILE_ANIMAL, INSECT, GEOPHENOMENON] ++ [ADD_SIGHT, ARMOR_RANGED, ADD_NOCTO_1, WEAK_ARROW, LIGHT_ATTENUATOR, FIREPROOF_CLOTH, RING_OF_OPPORTUNITY_SNIPER, ANY_ARROW, STARTING_ARMOR, STARTING_WEAPON, GEM] pattern HERO, SCOUT_HERO, RANGER_HERO, ESCAPIST_HERO, AMBUSHER_HERO, BRAWLER_HERO, SOLDIER_HERO, CIVILIAN, MONSTER, MOBILE_MONSTER, SCOUT_MONSTER, ANIMAL, MOBILE_ANIMAL, IMMOBILE_ANIMAL, INSECT, GEOPHENOMENON :: GroupName ItemKind pattern ADD_SIGHT, ARMOR_RANGED, ADD_NOCTO_1, WEAK_ARROW, LIGHT_ATTENUATOR, FIREPROOF_CLOTH, RING_OF_OPPORTUNITY_SNIPER, ANY_ARROW, STARTING_ARMOR, STARTING_WEAPON, GEM :: GroupName ItemKind pattern HERO = GroupName "adventurer" pattern SCOUT_HERO = GroupName "scout" pattern RANGER_HERO = GroupName "ranger" pattern ESCAPIST_HERO = GroupName "escapist" pattern AMBUSHER_HERO = GroupName "ambusher" pattern BRAWLER_HERO = GroupName "brawler" pattern SOLDIER_HERO = GroupName "soldier" pattern CIVILIAN = GroupName "civilian" pattern MONSTER = GroupName "monstrosity" pattern MOBILE_MONSTER = GroupName "mobile monstrosity" pattern SCOUT_MONSTER = GroupName "scout monstrosity" pattern ANIMAL = GroupName "animal" pattern MOBILE_ANIMAL = GroupName "mobile animal" pattern IMMOBILE_ANIMAL = GroupName "immobile animal" pattern INSECT = GroupName "insect" pattern GEOPHENOMENON = GroupName "geological phenomenon" pattern S_WOODEN_TORCH = GroupName "wooden torch" pattern S_SANDSTONE_ROCK = GroupName "sandstone rock" pattern ADD_SIGHT = GroupName "sight improvement" pattern ARMOR_RANGED = GroupName "ranged armor" pattern ADD_NOCTO_1 = GroupName "noctovision improvement" pattern WEAK_ARROW = GroupName "weak arrow" pattern LIGHT_ATTENUATOR = GroupName "light attenuator" pattern FIREPROOF_CLOTH = GroupName "fireproof cloth" pattern RING_OF_OPPORTUNITY_SNIPER = GroupName "ring of sniper" pattern ANY_ARROW = GroupName "arrow" pattern STARTING_ARMOR = GroupName "starting armor" pattern STARTING_WEAPON = GroupName "starting weapon" pattern GEM = GroupName "gem" -- * Content actors :: [ItemKind] actors = [warrior, warrior2, warrior3, warrior4, warrior5, scout, ranger, escapist, ambusher, brawler, soldier, civilian, civilian2, civilian3, civilian4, civilian5, eye, fastEye, nose, elbow, torsor, goldenJackal, griffonVulture, skunk, armadillo, gilaMonster, rattlesnake, hyena, komodoDragon, alligator, rhinoceros, beeSwarm, hornetSwarm, thornbush] -- LH-specific ++ [geyserBoiling, geyserArsenic, geyserSulfur] warrior, warrior2, warrior3, warrior4, warrior5, scout, ranger, escapist, ambusher, brawler, soldier, civilian, civilian2, civilian3, civilian4, civilian5, eye, fastEye, nose, elbow, torsor, goldenJackal, griffonVulture, skunk, armadillo, gilaMonster, rattlesnake, hyena, komodoDragon, alligator, rhinoceros, beeSwarm, hornetSwarm, thornbush :: ItemKind -- LH-specific geyserBoiling, geyserArsenic, geyserSulfur :: ItemKind -- Note that the actors that appear in the crawl scenario should -- be generated with at most ordinary ammo. Otherwise, farming them -- may be rational though boring endeavour. Any exceptions to that -- should be well thought of. E.g., unique guaranteed items on bosses -- are safe, just as restricted kinds of weak items. -- * Hunams -- TODO: bring back S_EAR_3 when character progression permits hearing boosts. humanOrgans :: [(GroupName ItemKind, CStore)] humanOrgans = [ (S_FIST, COrgan), (S_FOOT, COrgan) , (S_EYE_6, COrgan), (S_EAR_6, COrgan) , (S_SAPIENT_BRAIN, COrgan) ] warrior = ItemKind { isymbol = toContentSymbol '@' , iname = "adventurer" -- modified if initial actors in hero faction , ifreq = [(HERO, 100), (MOBILE, 1)] , iflavour = zipPlain [BrWhite] , icount = 1 , irarity = [(1, 5)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 80 -- partially from clothes and first aid , AddSkill SkMaxCalm 70 , AddSkill SkSpeed 20 , AddSkill SkNocto 2 , AddSkill SkWait 1 -- can lurk , AddSkill SkProject 2 -- can lob , AddSkill SkApply 2 -- can even apply periodic items , AddSkill SkOdor 1 , SetFlag Durable ] , ieffects = [] , ikit = humanOrgans ++ [(S_SANDSTONE_ROCK, CStash)] , idesc = "" -- "A hardened veteran of combat." } warrior2 = warrior { iname = "warrior" , ikit = humanOrgans ++ [(COMMON_ITEM, CStash)] -- , idesc = "" } warrior3 = warrior { iname = "blacksmith" -- , idesc = "" } warrior4 = warrior { iname = "forester" -- , idesc = "" } warrior5 = warrior { iname = "scientist" -- , idesc = "" } scout = warrior { ifreq = [(SCOUT_HERO, 100), (MOBILE, 1)] , ikit = humanOrgans ++ [ (ADD_SIGHT, CEqp) , (ARMOR_RANGED, CEqp) , (ADD_NOCTO_1, CStash) ] -- , idesc = "" } ranger = warrior { ifreq = [(RANGER_HERO, 100), (MOBILE, 1)] , ikit = humanOrgans ++ [ (ARMOR_RANGED, CEqp) , (WEAK_ARROW, CStash) ] -- , idesc = "" } escapist = warrior { ifreq = [(ESCAPIST_HERO, 100), (MOBILE, 1)] , ikit = humanOrgans ++ [ (ADD_SIGHT, CEqp) , (STARTING_ARMOR, CEqp) , (WEAK_ARROW, CStash) -- mostly for probing , (LIGHT_ATTENUATOR, CStash) , (S_WOODEN_TORCH, CStash) , (FIREPROOF_CLOTH, CStash) ] -- , idesc = "" } ambusher = warrior { ifreq = [(AMBUSHER_HERO, 100), (MOBILE, 1)] , ikit = humanOrgans -- dark and numerous, so more kit without exploring ++ [ (RING_OF_OPPORTUNITY_SNIPER, CEqp) , (ANY_ARROW, CStash), (ANY_ARROW, CStash) , (WEAK_ARROW, CStash) , (EXPLOSIVE, CStash) , (LIGHT_ATTENUATOR, CEqp) , (S_WOODEN_TORCH, CStash) ] -- , idesc = "" } brawler = warrior { ifreq = [(BRAWLER_HERO, 100), (MOBILE, 1)] , ikit = humanOrgans ++ [ (STARTING_WEAPON, CEqp) , (ANY_POTION, CStash) ] -- , idesc = "" } soldier = brawler { ifreq = [(SOLDIER_HERO, 100), (MOBILE, 1)] , ikit = ikit brawler ++ [(EXPLOSIVE, CStash)] -- , idesc = "" } civilian = warrior { iname = "clerk" , ifreq = [(CIVILIAN, 100), (MOBILE, 1)] , iflavour = zipPlain [BrBlack] -- , idesc = "" } civilian2 = civilian { iname = "hairdresser" -- , idesc = "" } civilian3 = civilian { iname = "lawyer" -- , idesc = "" } civilian4 = civilian { iname = "peddler" -- , idesc = "" } civilian5 = civilian { iname = "tax collector" -- , idesc = "" } -- * Monsters -- They have bright colours, because they are not natural. eye = ItemKind { isymbol = toContentSymbol 'e' , iname = "reducible eye" , ifreq = [ (MONSTER, 100), (MOBILE, 1) , (MOBILE_MONSTER, 100), (SCOUT_MONSTER, 10) ] , iflavour = zipFancy [BrRed] , icount = 1 , irarity = [(3, 0), (4, 10), (10, 8)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 16, AddSkill SkMaxCalm 70 , AddSkill SkSpeed 20, AddSkill SkNocto 2 , AddSkill SkAggression 1 , AddSkill SkProject 2 -- can lob , AddSkill SkApply 1 -- can use even cultural artifacts , SetFlag Durable ] , ieffects = [] , idesc = "Under your stare, it reduces to the bits that define its essence. Under introspection, the bits slow down and solidify into an arbitrary form again. It must be huge inside, for holographic principle to manifest so overtly." -- holographic principle is an anachronism for XIX or most of XX century, but "the cosmological scale effects" is too weak , ikit = [ (S_LASH, COrgan), (S_PUPIL, COrgan) -- at least one non-timed , (S_SAPIENT_BRAIN, COrgan) ] -- no hearing, it's all eyes } fastEye = ItemKind { isymbol = toContentSymbol 'j' , iname = "injective jaw" , ifreq = [ (MONSTER, 100), (MOBILE, 1) , (MOBILE_MONSTER, 100), (SCOUT_MONSTER, 60) ] , iflavour = zipFancy [BrBlue] , icount = 1 , irarity = [(3, 0), (4, 6), (10, 12)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 5, AddSkill SkMaxCalm 70 , AddSkill SkSpeed 30, AddSkill SkNocto 2 , AddSkill SkAggression 1 , SetFlag Durable ] , ieffects = [] , idesc = "Hungers but never eats. Bites but never swallows. Burrows its own image through, but never carries anything back." -- rather weak: not about injective objects, but puny, concrete, injective functions --- where's the madness in that? , ikit = [ (S_TOOTH, COrgan), (S_LIP, COrgan) -- at least one non-timed , (S_SPEED_GLAND_10, COrgan) , (S_VISION_6, COrgan), (S_EAR_3, COrgan) , (S_SAPIENT_BRAIN, COrgan) ] } nose = ItemKind -- depends solely on smell { isymbol = toContentSymbol 'n' , iname = "point-free nose" , ifreq = [(MONSTER, 100), (MOBILE, 1), (MOBILE_MONSTER, 100)] , iflavour = zipFancy [BrGreen] , icount = 1 , irarity = [(3, 0), (4, 5), (10, 7)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 30, AddSkill SkMaxCalm 30 , AddSkill SkSpeed 18, AddSkill SkNocto 2 , AddSkill SkAggression 1 , AddSkill SkProject (-1) -- can't project , SetFlag Durable ] , ieffects = [] , idesc = "No mouth, yet it devours everything around, constantly sniffing itself inward; pure movement structure, no constant point to focus one's maddened gaze on." , ikit = [ (S_TIP, COrgan), (S_LIP, COrgan) -- at least one non-timed , (S_NOSTRIL, COrgan) , (S_SAPIENT_BRAIN, COrgan) ] -- no sight nor hearing } elbow = ItemKind { isymbol = toContentSymbol 'e' , iname = "commutative elbow" , ifreq = [ (MONSTER, 100), (MOBILE, 1) , (MOBILE_MONSTER, 100), (SCOUT_MONSTER, 30) ] , iflavour = zipFancy [BrMagenta] , icount = 1 , irarity = [(3, 0), (4, 1), (10, 12)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 8, AddSkill SkMaxCalm 80 , AddSkill SkSpeed 20, AddSkill SkNocto 2 , AddSkill SkProject 2 -- can lob , AddSkill SkApply 1 -- can even use cultural artifacts , AddSkill SkMelee (-1) , SetFlag Durable ] , ieffects = [] , idesc = "An arm strung like a bow. A few edges, but none keen enough. A few points, but none piercing. Deadly objects zip out of the void." , ikit = [ (S_SPEED_GLAND_5, COrgan), (S_BARK, COrgan) , (S_VISION_12, COrgan), (S_EAR_8, COrgan) -- too powerful to get stronger sight , (S_SAPIENT_BRAIN, COrgan) , (ANY_ARROW, CStash), (ANY_ARROW, CStash) , (WEAK_ARROW, CStash), (WEAK_ARROW, CStash) ] } torsor = ItemKind { isymbol = toContentSymbol 'T' , iname = "The Forgetful Torsor" , ifreq = [(MONSTER, 100), (MOBILE, 1)] , iflavour = zipFancy [BrCyan] , icount = 1 , irarity = [(9, 0), (10, 1000)] -- unique , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ SetFlag Unique , AddSkill SkMaxHP 300, AddSkill SkMaxCalm 100 , AddSkill SkSpeed 15, AddSkill SkNocto 2 , AddSkill SkAggression 3 , AddSkill SkProject 2 -- can lob , AddSkill SkApply 1 -- can even use cultural artifacts , AddSkill SkAlter (-1) -- can't exit the gated level; a boss, -- but can dig rubble, ice , SetFlag Durable ] , ieffects = [] , idesc = "A principal homogeneous manifold, that acts freely and with enormous force, but whose stabilizers are trivial, making it rather helpless without a support group." , ikit = [ (S_RIGHT_TORSION, COrgan), (S_LEFT_TORSION, COrgan) , (S_PUPIL, COrgan) , (S_TENTACLE, COrgan) -- low timeout, so rarely a stall , (S_EAR_8, COrgan) , (S_SAPIENT_BRAIN, COrgan) , (GEM, CStash), (GEM, CStash) , (GEM, CStash), (GEM, CStash) ] } -- "ground x" --- for immovable monster that can only tele or prob travel -- pullback -- skeletal -- * Animals -- They need rather strong melee, because they don't use items. -- They have dull colors, except for yellow, because there is no dull variant. goldenJackal = ItemKind -- basically a much smaller, slower and nosy hyena { isymbol = toContentSymbol 'j' , iname = "golden jackal" , ifreq = [ (ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100) , (SCAVENGER, 50) ] , iflavour = zipPlain [BrYellow] , icount = 1 , irarity = [(1, 4), (10, 2)] , iverbHit = "thud" , iweight = 13000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 15, AddSkill SkMaxCalm 70 , AddSkill SkSpeed 24, AddSkill SkNocto 2 , AddSkill SkAggression 2 -- scout , AddSkill SkDisplace 1 -- scout , SetFlag Durable ] , ieffects = [] , idesc = "An opportunistic predator, feeding on carrion and the weak." , ikit = [ (S_SMALL_JAW, COrgan) , (S_EYE_6, COrgan), (S_NOSTRIL, COrgan), (S_EAR_8, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } griffonVulture = ItemKind -- keep it boring and weak, because it summons { isymbol = toContentSymbol 'v' , iname = "griffon vulture" , ifreq = [ (ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100) , (SCAVENGER, 30) ] , iflavour = zipPlain [BrYellow] , icount = 1 , irarity = [(1, 3), (10, 3)] , iverbHit = "thud" , iweight = 13000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 15, AddSkill SkMaxCalm 80 -- enough Calm to summon twice only if not attacked at all; -- loses a lot of sight after summoning , AddSkill SkSpeed 22, AddSkill SkNocto 2 , AddSkill SkAlter (-2) -- can't use hard stairs nor doors , AddSkill SkFlying 10 -- flies slowly, but far , SetFlag Durable ] -- Animals don't have leader, usually, so even if only one on level, -- it pays the communication overhead, so the speed is higher to get -- them on par with human leaders moving solo. , ieffects = [] , idesc = "It soars high above, searching for vulnerable prey." , ikit = [ (S_SCREECHING_BEAK, COrgan) -- in reality it grunts and hisses , (S_SMALL_CLAW, COrgan) , (S_EYE_8, COrgan), (S_EAR_8, COrgan) -- can't shoot, so strong sight is OK , (S_ANIMAL_BRAIN, COrgan) ] } skunk = ItemKind { isymbol = toContentSymbol 's' , iname = "hog-nosed skunk" , ifreq = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)] , iflavour = zipPlain [White] , icount = 1 , irarity = [(1, 8), (5, 1)] , iverbHit = "thud" , iweight = 4000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 13, AddSkill SkMaxCalm 30 , AddSkill SkSpeed 22, AddSkill SkNocto 2 , AddSkill SkAlter (-2) -- can't use hard stairs nor doors , AddSkill SkOdor 5 -- and no smell skill, to let it leave smell , SetFlag Durable ] , ieffects = [] , idesc = "Its only defence is the terrible stench." , ikit = [ (S_SCENT_GLAND, COrgan) , (S_SMALL_CLAW, COrgan), (S_SNOUT, COrgan) , (S_EYE_3, COrgan), (S_EAR_6, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } armadillo = ItemKind { isymbol = toContentSymbol 'a' , iname = "giant armadillo" , ifreq = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)] , iflavour = zipPlain [Brown] , icount = 1 , irarity = [(1, 7)] , iverbHit = "thud" , iweight = 54000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 25, AddSkill SkMaxCalm 30 , AddSkill SkSpeed 20, AddSkill SkNocto 2 , AddSkill SkHurtMelee (-70) -- quite harmless rolled in a ball , AddSkill SkAlter (-2) -- can't use hard stairs nor doors , SetFlag Durable ] , ieffects = [] , idesc = "When threatened, it rolls into a ball." , ikit = [ (S_HOOKED_CLAW, COrgan), (S_SNOUT, COrgan) , (S_ARMORED_SKIN, COrgan), (S_ARMORED_SKIN, COrgan) , (S_EYE_3, COrgan), (S_NOSTRIL, COrgan), (S_EAR_6, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } gilaMonster = ItemKind { isymbol = toContentSymbol 'g' , iname = "Gila monster" , ifreq = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)] , iflavour = zipPlain [Magenta] , icount = 1 , irarity = [(2, 5), (10, 2)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 15, AddSkill SkMaxCalm 50 , AddSkill SkSpeed 18, AddSkill SkNocto 2 , AddSkill SkAlter (-2) -- can't use hard stairs nor doors , SetFlag Durable ] , ieffects = [] , idesc = "Numbing venom ensures that even the fastest prey has no escape." , ikit = [ (S_VENOM_TOOTH, COrgan), (S_SMALL_CLAW, COrgan) , (S_EYE_3, COrgan), (S_NOSTRIL, COrgan), (S_EAR_6, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } rattlesnake = ItemKind { isymbol = toContentSymbol 's' , iname = "rattlesnake" , ifreq = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)] , iflavour = zipPlain [Brown] , icount = 1 , irarity = [(5, 1), (10, 7), (20, 10)] -- common among late spawns , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 28, AddSkill SkMaxCalm 60 , AddSkill SkSpeed 16, AddSkill SkNocto 2 , AddSkill SkAggression 2 -- often discharged. so flees anyway , AddSkill SkAlter (-2) -- can't use hard stairs nor doors , SetFlag Durable ] , ieffects = [] , idesc = "Beware its rattle - it serves as a warning of an agonising death." , ikit = [ (S_VENOM_FANG, COrgan) -- when discharged, it's weaponless , (S_RATLLE, COrgan) , (S_EYE_3, COrgan), (S_NOSTRIL, COrgan), (S_EAR_6, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } hyena = ItemKind { isymbol = toContentSymbol 'h' , iname = "spotted hyena" , ifreq = [ (ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100) , (SCAVENGER, 20) ] , iflavour = zipPlain [BrYellow] , icount = 1 , irarity = [(4, 1), (10, 5), (20, 10)] -- gets summoned often, so low base rarity, except among late spawns , iverbHit = "thud" , iweight = 60000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 23, AddSkill SkMaxCalm 70 , AddSkill SkSpeed 32, AddSkill SkNocto 2 , SetFlag Durable ] , ieffects = [] , idesc = "Skulking in the shadows, waiting for easy prey." , ikit = [ (S_JAW, COrgan), (S_SMALL_CLAW, COrgan) , (S_EYE_6, COrgan), (S_NOSTRIL, COrgan), (S_EAR_8, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } komodoDragon = ItemKind { isymbol = toContentSymbol 'k' , iname = "Komodo dragon" , ifreq = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)] , iflavour = zipPlain [BrRed] -- speedy, so bright red , icount = 1 , irarity = [(9, 0), (10, 11), (20, 20)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 40, AddSkill SkMaxCalm 60 -- regens , AddSkill SkSpeed 17, AddSkill SkNocto 2 , AddSkill SkHurtMelee 60 -- great fighter with low cooldowns , AddSkill SkAggression 1 -- match the description , SetFlag Durable ] , ieffects = [] , idesc = "Larger and more aggressive than any other lizard, but as easily recovering from wounds as its lesser cousins." , ikit = [ (S_LARGE_TAIL, COrgan), (S_JAW, COrgan) , (S_LIP, COrgan), (S_FOOT, COrgan) , (S_SPEED_GLAND_5, COrgan), (S_ARMORED_SKIN, COrgan) , (S_EYE_3, COrgan), (S_NOSTRIL, COrgan), (S_EAR_3, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } alligator = ItemKind -- late, slow, deadly semi-tank with some armor; -- too deadly to get more HP; bombs the only recourse { isymbol = toContentSymbol 'a' , iname = "alligator" , ifreq = [(ANIMAL, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)] , iflavour = zipPlain [Blue] , icount = 1 , irarity = [(9, 0), (10, 12), (20, 10), (40, 40)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 55, AddSkill SkMaxCalm 70 , AddSkill SkSpeed 18, AddSkill SkNocto 2 , AddSkill SkSwimming 100 -- swims better than walks , SetFlag Durable ] , ieffects = [] , idesc = "An armored predator from the dawn of time. You better not get within its reach." , ikit = [ (S_HUGE_TAIL, COrgan) -- the special trick, breaking frontline , (S_LARGE_JAW, COrgan) , (S_SMALL_CLAW, COrgan) , (S_ARMORED_SKIN, COrgan) , (S_EYE_6, COrgan), (S_EAR_8, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } rhinoceros = ItemKind { isymbol = toContentSymbol 'R' , iname = "The Maddened Rhinoceros" , ifreq = [(ANIMAL, 100), (MOBILE, 1)] , iflavour = zipPlain [Brown] , icount = 1 , irarity = [(2, 0), (3, 1000), (4, 0)] -- an early unique , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ SetFlag Unique , AddSkill SkMaxHP 90, AddSkill SkMaxCalm 60 , AddSkill SkSpeed 27, AddSkill SkNocto 2 , AddSkill SkAggression 2 , AddSkill SkAlter (-1) -- can't use hard stairs nor dig; -- a weak miniboss , SetFlag Durable ] , ieffects = [] , idesc = "The last of its kind. Blind with rage. Charges at deadly speed." , ikit = [ (S_RHINO_HORN, COrgan), (S_SNOUT, COrgan) , (S_ARMORED_SKIN, COrgan) , (S_EYE_3, COrgan), (S_EAR_8, COrgan) , (S_ANIMAL_BRAIN, COrgan) ] } -- * Non-animal animals beeSwarm = ItemKind { isymbol = toContentSymbol 'b' , iname = "bee swarm" , ifreq = [(ANIMAL, 100), (INSECT, 50), (MOBILE, 1)] , iflavour = zipPlain [Brown] , icount = 1 , irarity = [(1, 3), (10, 4)] , iverbHit = "buzz" , iweight = 1000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 10, AddSkill SkMaxCalm 60 , AddSkill SkSpeed 30, AddSkill SkNocto 2 -- armor in sting , AddSkill SkAlter (-2) -- can't use hard stairs nor doors , AddSkill SkWait (-2) -- can't brace, sleep and lurk , AddSkill SkFlying 10 -- flies slowly, but far , SetFlag Durable ] , ieffects = [] , idesc = "Every bee would die for the queen." , ikit = [ (S_BEE_STING, COrgan) -- weaponless when it's used up , (S_VISION_6, COrgan), (S_EAR_6, COrgan) , (S_INSECT_MORTALITY, COrgan), (S_ANIMAL_BRAIN, COrgan) ] } hornetSwarm = ItemKind -- kind of tank with armor, but short-lived { isymbol = toContentSymbol 'h' , iname = "hornet swarm" , ifreq = [(ANIMAL, 100), (INSECT, 100), (MOBILE, 1), (MOBILE_ANIMAL, 100)] , iflavour = zipPlain [Magenta] , icount = 1 , irarity = [(5, 1), (10, 4), (20, 10)] -- should be many, because die after a time , iverbHit = "buzz" , iweight = 1000 , idamage = 0 , iaspects = [ AddSkill SkArmorMelee 80, AddSkill SkArmorRanged 40 , AddSkill SkHurtMelee 50 , AddSkill SkMaxHP 10, AddSkill SkMaxCalm 70 , AddSkill SkSpeed 30, AddSkill SkNocto 2 , AddSkill SkAlter (-2) -- can't use hard stairs nor doors , AddSkill SkWait (-2) -- can't brace, sleep and lurk , AddSkill SkFlying 10 -- flies slowly, but far , SetFlag Durable ] , ieffects = [] , idesc = "A vicious cloud of stings and hate." , ikit = [ (S_STING, COrgan) -- when on cooldown, it's weaponless , (S_VISION_6, COrgan), (S_EAR_6, COrgan) , (S_INSECT_MORTALITY, COrgan), (S_ANIMAL_BRAIN, COrgan) ] } thornbush = ItemKind -- the wimpiest kind of early tank { isymbol = toContentSymbol 't' , iname = "thornbush" , ifreq = [(ANIMAL, 20), (IMMOBILE_ANIMAL, 20)] , iflavour = zipPlain [Brown] , icount = 1 , irarity = [(1, 13)] , iverbHit = "scrape" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 30, AddSkill SkMaxCalm 999 , AddSkill SkSpeed 22, AddSkill SkNocto 2 , AddSkill SkWait 1, AddSkill SkMelee 1 -- no brain , SetFlag Durable ] , ieffects = [] , idesc = "Each branch bears long, curved thorns." , ikit = [ (S_THORN, COrgan) -- after all run out, it's weaponless , (S_BARK, COrgan) ] } geyserBoiling = ItemKind { isymbol = toContentSymbol 'g' , iname = "geyser" , ifreq = [(ANIMAL, 8), (IMMOBILE_ANIMAL, 30), (GEOPHENOMENON, 1)] , iflavour = zipPlain [Blue] , icount = 1 , irarity = [(1, 10), (10, 6)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 10, AddSkill SkMaxCalm 999 , AddSkill SkSpeed 11, AddSkill SkNocto 2 , AddSkill SkWait 1, AddSkill SkMelee 1 -- no brain , SetFlag Durable ] , ieffects = [] , idesc = "A jet of acidic water, hot enough to melt flesh." , ikit = [(S_BOILING_VENT, COrgan), (S_BOILING_FISSURE, COrgan)] } geyserArsenic = ItemKind { isymbol = toContentSymbol 'g' , iname = "arsenic geyser" , ifreq = [(ANIMAL, 8), (IMMOBILE_ANIMAL, 40), (GEOPHENOMENON, 1)] , iflavour = zipPlain [Cyan] , icount = 1 , irarity = [(1, 10), (10, 6)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 20, AddSkill SkMaxCalm 999 , AddSkill SkSpeed 22, AddSkill SkNocto 2, AddSkill SkShine 3 , AddSkill SkWait 1, AddSkill SkMelee 1 -- no brain , SetFlag Durable ] , ieffects = [] , idesc = "The sharp scent betrays the poison within the spray." , ikit = [(S_ARSENIC_VENT, COrgan), (S_ARSENIC_FISSURE, COrgan)] } geyserSulfur = ItemKind { isymbol = toContentSymbol 'g' , iname = "sulfur geyser" , ifreq = [(ANIMAL, 8), (IMMOBILE_ANIMAL, 120), (GEOPHENOMENON, 1)] , iflavour = zipPlain [BrYellow] -- exception, animal with bright color , icount = 1 , irarity = [(1, 10), (10, 6)] , iverbHit = "thud" , iweight = 80000 , idamage = 0 , iaspects = [ AddSkill SkMaxHP 20, AddSkill SkMaxCalm 999 , AddSkill SkSpeed 22, AddSkill SkNocto 2, AddSkill SkShine 3 , AddSkill SkWait 1, AddSkill SkMelee 1 -- no brain , SetFlag Durable ] , ieffects = [] , idesc = "The pool boils and bubbles, stinking of rotten eggs. Despite the smell, these waters purify and strengthen." , ikit = [(S_SULFUR_VENT, COrgan), (S_SULFUR_FISSURE, COrgan)] } LambdaHack-0.11.0.0/GameDefinition/Content/ItemKindBlast.hs0000644000000000000000000011361507346545000021420 0ustar0000000000000000-- | Blast definitions. module Content.ItemKindBlast ( -- * Group name patterns pattern S_FIRECRACKER, pattern S_VIOLENT_FRAGMENTATION, pattern S_FRAGMENTATION, pattern S_FOCUSED_FRAGMENTATION, pattern S_VIOLENT_CONCUSSION, pattern S_CONCUSSION, pattern S_FOCUSED_CONCUSSION, pattern S_VIOLENT_FLASH, pattern S_FOCUSED_FLASH, pattern S_GLASS_HAIL, pattern S_FOCUSED_GLASS_HAIL, pattern S_PHEROMONE, pattern S_CALMING_MIST, pattern S_DISTRESSING_ODOR, pattern S_HEALING_MIST, pattern S_HEALING_MIST_2, pattern S_WOUNDING_MIST, pattern S_DISTORTION, pattern S_SMOKE, pattern S_BOILING_WATER, pattern S_GLUE, pattern S_WASTE, pattern S_ANTI_SLOW_MIST, pattern S_ANTIDOTE_MIST, pattern S_SLEEP_MIST, pattern S_DENSE_SHOWER, pattern S_SPARSE_SHOWER, pattern S_MELEE_PROTECTIVE_BALM, pattern S_RANGE_PROTECTIVE_BALM, pattern S_DEFENSELESSNESS_RUNOUT, pattern S_RESOLUTION_DUST, pattern S_HASTE_SPRAY, pattern S_VIOLENT_SLOWNESS_MIST, pattern S_SLOWNESS_MIST, pattern S_FOCUSED_SLOWNESS_MIST, pattern S_EYE_DROP, pattern S_IRON_FILING, pattern S_SMELLY_DROPLET, pattern S_EYE_SHINE, pattern S_WHISKEY_SPRAY, pattern S_YOUTH_SPRINKLE, pattern S_POISON_CLOUD, pattern S_PING_PLASH, pattern S_VIOLENT_BURNING_OIL_2, pattern S_VIOLENT_BURNING_OIL_3, pattern S_VIOLENT_BURNING_OIL_4, pattern S_BURNING_OIL_2, pattern S_BURNING_OIL_3, pattern S_BURNING_OIL_4, pattern S_FOCUSED_BURNING_OIL_2, pattern S_FOCUSED_BURNING_OIL_3, pattern S_FOCUSED_BURNING_OIL_4 , blastNoStatOf, blastBonusStatOf , pattern ARMOR_MISC , blastsGNSingleton, blastsGN , -- * Content blasts ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour import Content.ItemKindTemporary -- * Group name patterns blastsGNSingleton :: [GroupName ItemKind] blastsGNSingleton = [S_FIRECRACKER, S_VIOLENT_FRAGMENTATION, S_FRAGMENTATION, S_FOCUSED_FRAGMENTATION, S_VIOLENT_CONCUSSION, S_CONCUSSION, S_FOCUSED_CONCUSSION, S_VIOLENT_FLASH, S_FOCUSED_FLASH, S_GLASS_HAIL, S_FOCUSED_GLASS_HAIL, S_PHEROMONE, S_CALMING_MIST, S_DISTRESSING_ODOR, S_HEALING_MIST, S_HEALING_MIST_2, S_WOUNDING_MIST, S_DISTORTION, S_SMOKE, S_BOILING_WATER, S_GLUE, S_WASTE, S_ANTI_SLOW_MIST, S_ANTIDOTE_MIST, S_SLEEP_MIST, S_DENSE_SHOWER, S_SPARSE_SHOWER, S_MELEE_PROTECTIVE_BALM, S_RANGE_PROTECTIVE_BALM, S_DEFENSELESSNESS_RUNOUT, S_RESOLUTION_DUST, S_HASTE_SPRAY, S_VIOLENT_SLOWNESS_MIST, S_SLOWNESS_MIST, S_FOCUSED_SLOWNESS_MIST, S_EYE_DROP, S_IRON_FILING, S_SMELLY_DROPLET, S_EYE_SHINE, S_WHISKEY_SPRAY, S_YOUTH_SPRINKLE, S_POISON_CLOUD, S_PING_PLASH, S_VIOLENT_BURNING_OIL_2, S_VIOLENT_BURNING_OIL_3, S_VIOLENT_BURNING_OIL_4, S_BURNING_OIL_2, S_BURNING_OIL_3, S_BURNING_OIL_4, S_FOCUSED_BURNING_OIL_2, S_FOCUSED_BURNING_OIL_3, S_FOCUSED_BURNING_OIL_4] ++ map firecrackerAt [1..4] ++ map blastNoStatOf noStatGN ++ map blastBonusStatOf bonusStatGN pattern S_FIRECRACKER, S_VIOLENT_FRAGMENTATION, S_FRAGMENTATION, S_FOCUSED_FRAGMENTATION, S_VIOLENT_CONCUSSION, S_CONCUSSION, S_FOCUSED_CONCUSSION, S_VIOLENT_FLASH, S_FOCUSED_FLASH, S_GLASS_HAIL, S_FOCUSED_GLASS_HAIL, S_PHEROMONE, S_CALMING_MIST, S_DISTRESSING_ODOR, S_HEALING_MIST, S_HEALING_MIST_2, S_WOUNDING_MIST, S_DISTORTION, S_SMOKE, S_BOILING_WATER, S_GLUE, S_WASTE, S_ANTI_SLOW_MIST, S_ANTIDOTE_MIST, S_SLEEP_MIST, S_DENSE_SHOWER, S_SPARSE_SHOWER, S_MELEE_PROTECTIVE_BALM, S_RANGE_PROTECTIVE_BALM, S_DEFENSELESSNESS_RUNOUT, S_RESOLUTION_DUST, S_HASTE_SPRAY, S_VIOLENT_SLOWNESS_MIST, S_SLOWNESS_MIST, S_FOCUSED_SLOWNESS_MIST, S_EYE_DROP, S_IRON_FILING, S_SMELLY_DROPLET, S_EYE_SHINE, S_WHISKEY_SPRAY, S_YOUTH_SPRINKLE, S_POISON_CLOUD, S_PING_PLASH, S_VIOLENT_BURNING_OIL_2, S_VIOLENT_BURNING_OIL_3, S_VIOLENT_BURNING_OIL_4, S_BURNING_OIL_2, S_BURNING_OIL_3, S_BURNING_OIL_4, S_FOCUSED_BURNING_OIL_2, S_FOCUSED_BURNING_OIL_3, S_FOCUSED_BURNING_OIL_4 :: GroupName ItemKind blastsGN :: [GroupName ItemKind] blastsGN = [ARMOR_MISC] pattern ARMOR_MISC :: GroupName ItemKind pattern S_FIRECRACKER = GroupName "firecracker" pattern S_VIOLENT_FRAGMENTATION = GroupName "violent fragmentation" pattern S_FRAGMENTATION = GroupName "fragmentation" pattern S_FOCUSED_FRAGMENTATION = GroupName "focused fragmentation" pattern S_VIOLENT_CONCUSSION = GroupName "violent concussion" pattern S_CONCUSSION = GroupName "concussion" pattern S_FOCUSED_CONCUSSION = GroupName "focused concussion" pattern S_VIOLENT_FLASH = GroupName "violent flash" pattern S_FOCUSED_FLASH = GroupName "focused flash" pattern S_GLASS_HAIL = GroupName "glass hail" pattern S_FOCUSED_GLASS_HAIL = GroupName "focused glass hail" pattern S_PHEROMONE = GroupName "pheromone" pattern S_CALMING_MIST = GroupName "calming mist" pattern S_DISTRESSING_ODOR = GroupName "distressing odor" pattern S_HEALING_MIST = GroupName "healing mist" pattern S_HEALING_MIST_2 = GroupName "strong healing mist" pattern S_WOUNDING_MIST = GroupName "wounding mist" pattern S_DISTORTION = GroupName "distortion" pattern S_SMOKE = GroupName "smoke" pattern S_BOILING_WATER = GroupName "boiling water" pattern S_GLUE = GroupName "glue" pattern S_WASTE = GroupName "waste" pattern S_ANTI_SLOW_MIST = GroupName "anti-slow mist" pattern S_ANTIDOTE_MIST = GroupName "antidote mist" pattern S_SLEEP_MIST = GroupName "sleep mist" pattern S_DENSE_SHOWER = GroupName "dense shower" pattern S_SPARSE_SHOWER = GroupName "sparse shower" pattern S_MELEE_PROTECTIVE_BALM = GroupName "melee protective balm" pattern S_RANGE_PROTECTIVE_BALM = GroupName "ranged protective balm" pattern S_DEFENSELESSNESS_RUNOUT = GroupName "PhD defense question" pattern S_RESOLUTION_DUST = GroupName "resolution dust" pattern S_HASTE_SPRAY = GroupName "haste spray" pattern S_VIOLENT_SLOWNESS_MIST = GroupName "violent nitrogen mist" pattern S_SLOWNESS_MIST = GroupName "nitrogen mist" pattern S_FOCUSED_SLOWNESS_MIST = GroupName "focused nitrogen mist" pattern S_EYE_DROP = GroupName "eye drop" pattern S_IRON_FILING = GroupName "iron filing" pattern S_SMELLY_DROPLET = GroupName "smelly droplet" pattern S_EYE_SHINE = GroupName "eye shine" pattern S_WHISKEY_SPRAY = GroupName "whiskey spray" pattern S_YOUTH_SPRINKLE = GroupName "youth sprinkle" pattern S_POISON_CLOUD = GroupName "poison cloud" pattern S_PING_PLASH = GroupName "ping and flash" pattern S_VIOLENT_BURNING_OIL_2 = GroupName "violent burning oil 2" pattern S_VIOLENT_BURNING_OIL_3 = GroupName "violent burning oil 3" pattern S_VIOLENT_BURNING_OIL_4 = GroupName "violent burning oil 4" pattern S_BURNING_OIL_2 = GroupName "burning oil 2" pattern S_BURNING_OIL_3 = GroupName "burning oil 3" pattern S_BURNING_OIL_4 = GroupName "burning oil 4" pattern S_FOCUSED_BURNING_OIL_2 = GroupName "focused burning oil 2" pattern S_FOCUSED_BURNING_OIL_3 = GroupName "focused burning oil 3" pattern S_FOCUSED_BURNING_OIL_4 = GroupName "focused burning oil 4" firecrackerAt :: Int -> GroupName ItemKind firecrackerAt n = GroupName $ "firecracker" <+> tshow n blastNoStatOf :: GroupName ItemKind -> GroupName ItemKind blastNoStatOf grp = GroupName $ fromGroupName grp <+> "mist" blastBonusStatOf :: GroupName ItemKind -> GroupName ItemKind blastBonusStatOf grp = GroupName $ fromGroupName grp <+> "dew" pattern ARMOR_MISC = GroupName "miscellaneous armor" -- * Content blasts :: [ItemKind] blasts = [spreadBurningOil2, spreadBurningOil3, spreadBurningOil4, spreadBurningOil82, spreadBurningOil83, spreadBurningOil84, focusedBurningOil2, focusedBurningOil3, focusedBurningOil4, firecracker1, firecracker2, firecracker3, firecracker4, firecracker5, spreadFragmentation, spreadFragmentation8, focusedFragmentation, spreadConcussion, spreadConcussion8, focusedConcussion, spreadFlash, spreadFlash8, focusedFlash, singleSpark, glassPiece, focusedGlass, fragrance, pheromone, mistCalming, odorDistressing, mistHealing, mistHealing2, mistWounding, distortion, smoke, boilingWater, glue, waste, mistAntiSlow, mistAntidote, mistSleep, denseShower, sparseShower, protectingBalmMelee, protectingBalmRanged, defenselessnessRunout, resolutionDust, hasteSpray, spreadNitrogen, spreadNitrogen8, focusedNitrogen, eyeDrop, ironFiling, smellyDroplet, eyeShine, whiskeySpray, youthSprinkle, poisonCloud, pingFlash, blastNoSkMove, blastNoSkMelee, blastNoSkDisplace, blastNoSkAlter, blastNoSkWait, blastNoSkMoveItem, blastNoSkProject, blastNoSkApply, blastBonusSkMove, blastBonusSkMelee, blastBonusSkDisplace, blastBonusSkAlter, blastBonusSkWait, blastBonusSkMoveItem, blastBonusSkProject, blastBonusSkApply] spreadBurningOil2, spreadBurningOil3, spreadBurningOil4, spreadBurningOil82, spreadBurningOil83, spreadBurningOil84, focusedBurningOil2, focusedBurningOil3, focusedBurningOil4, firecracker1, firecracker2, firecracker3, firecracker4, firecracker5, spreadFragmentation, spreadFragmentation8, focusedFragmentation, spreadConcussion, spreadConcussion8, focusedConcussion, spreadFlash, spreadFlash8, focusedFlash, singleSpark, glassPiece, focusedGlass, fragrance, pheromone, mistCalming, odorDistressing, mistHealing, mistHealing2, mistWounding, distortion, smoke, boilingWater, glue, waste, mistAntiSlow, mistAntidote, mistSleep, denseShower, sparseShower, protectingBalmMelee, protectingBalmRanged, defenselessnessRunout, resolutionDust, hasteSpray, spreadNitrogen, spreadNitrogen8, focusedNitrogen, eyeDrop, ironFiling, smellyDroplet, eyeShine, whiskeySpray, youthSprinkle, poisonCloud, pingFlash, blastNoSkMove, blastNoSkMelee, blastNoSkDisplace, blastNoSkAlter, blastNoSkWait, blastNoSkMoveItem, blastNoSkProject, blastNoSkApply, blastBonusSkMove, blastBonusSkMelee, blastBonusSkDisplace, blastBonusSkAlter, blastBonusSkWait, blastBonusSkMoveItem, blastBonusSkProject, blastBonusSkApply :: ItemKind -- We take care (e.g., in burningOil below) that blasts are not faster -- than 100% fastest natural speed, or some frames would be skipped, -- which is a waste of perfectly good frames. -- * Parameterized blasts spreadBurningOil :: Int -> GroupName ItemKind -> ItemKind spreadBurningOil n grp = ItemKind { isymbol = toContentSymbol '*' , iname = "burning oil" , ifreq = [(grp, 1)] , iflavour = zipPlain [BrYellow] , icount = intToDice (4 + n * 2) , irarity = [(1, 1)] , iverbHit = "sear" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity (max 10 $ min 100 $ n `div` 2 * 10) , SetFlag Fragile, SetFlag Blast , AddSkill SkShine 2 ] , ieffects = [ Burn 1 , toOrganBad S_PACIFIED (2 + 1 `d` 2) ] -- slips and frantically puts out fire , idesc = "Sticky oil, burning brightly." , ikit = [] } spreadBurningOil2 = spreadBurningOil 2 S_VIOLENT_BURNING_OIL_2 -- 2 steps, 2 turns spreadBurningOil3 = spreadBurningOil 3 S_VIOLENT_BURNING_OIL_3 -- 2 steps, 2 turns spreadBurningOil4 = spreadBurningOil 4 S_VIOLENT_BURNING_OIL_4 -- 4 steps, 2 turns spreadBurningOil8 :: Int -> GroupName ItemKind -> ItemKind spreadBurningOil8 n grp = (spreadBurningOil (n `div` 2) grp) { icount = 7 -- 8 was too deadly } spreadBurningOil82 = spreadBurningOil8 2 S_BURNING_OIL_2 spreadBurningOil83 = spreadBurningOil8 3 S_BURNING_OIL_3 spreadBurningOil84 = spreadBurningOil8 4 S_BURNING_OIL_4 focusedBurningOil :: Int -> GroupName ItemKind -> GroupName ItemKind -> ItemKind focusedBurningOil n grp grpExplode = ItemKind { isymbol = toContentSymbol '`' , iname = "igniting oil" , ifreq = [(grp, 1)] , iflavour = zipPlain [BrYellow] -- all ignitions yellow to avoid appearing -- as a small vial explosion , icount = intToDice n , irarity = [(1, 1)] , iverbHit = "ignite" , iweight = 1 , idamage = 0 , iaspects = [ toLinger 0 -- 0 steps, 1 turn , SetFlag Fragile, SetFlag Blast ] -- when the target position is occupied, the explosion starts one step -- away, hence we set range to 0 steps, to limit dispersal , ieffects = [OnSmash $ Explode grpExplode] , idesc = idesc spreadBurningOil2 , ikit = [] } focusedBurningOil2 = focusedBurningOil 2 S_FOCUSED_BURNING_OIL_2 S_BURNING_OIL_2 focusedBurningOil3 = focusedBurningOil 3 S_FOCUSED_BURNING_OIL_3 S_BURNING_OIL_3 focusedBurningOil4 = focusedBurningOil 4 S_FOCUSED_BURNING_OIL_4 S_BURNING_OIL_4 firecracker :: Int -> ItemKind firecracker n = ItemKind { isymbol = toContentSymbol '*' , iname = "firecracker" , ifreq = [(if n == 5 then S_FIRECRACKER else firecrackerAt n, 1)] , iflavour = zipPlain [brightCol !! ((n + 2) `mod` length brightCol)] , icount = if n <= 3 then 1 `d` min 2 n else 2 + 1 `d` 2 , irarity = [(1, 1)] , iverbHit = if n >= 4 then "singe" else "crack" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast , AddSkill SkShine $ intToDice $ 1 + n `div` 2 ] , ieffects = [if n >= 4 then Burn 1 else RefillCalm (-2)] ++ [Discharge 1 30 | n >= 3] ++ [OnSmash $ Explode $ firecrackerAt (n - 1) | n >= 2] , idesc = "Scraps of burnt paper, covering little pockets of black powder, buffeted by colorful explosions." , ikit = [] } firecracker5 = firecracker 5 firecracker4 = firecracker 4 firecracker3 = firecracker 3 firecracker2 = firecracker 2 firecracker1 = firecracker 1 -- * Focused blasts spreadFragmentation = ItemKind { isymbol = toContentSymbol '*' , iname = "fragmentation burst" , ifreq = [(S_VIOLENT_FRAGMENTATION, 1)] , iflavour = zipPlain [Red] , icount = 8 -- strong but few, so not always hits target , irarity = [(1, 1)] , iverbHit = "tear apart" , iweight = 1 , idamage = 3 `d` 1 -- deadly and adjacent actor hit by 2 on average; -- however, moderate armour blocks completely , iaspects = [ ToThrow $ ThrowMod 100 20 4 -- 4 steps, 1 turn , SetFlag Lobable, SetFlag Fragile, SetFlag Blast , AddSkill SkShine 3, AddSkill SkHurtMelee $ -12 * 5 ] , ieffects = [DropItem 1 1 COrgan CONDITION] , idesc = "Flying shards, flame and smoke." , ikit = [] } spreadFragmentation8 = spreadFragmentation { ifreq = [(S_FRAGMENTATION, 1)] , icount = 5 , iaspects = [ ToThrow $ ThrowMod 100 10 2 -- 2 steps, 1 turn , SetFlag Lobable, SetFlag Fragile, SetFlag Blast , AddSkill SkShine 3, AddSkill SkHurtMelee $ -12 * 5 ] -- smaller radius, so worse for area effect, but twice the direct damage } focusedFragmentation = ItemKind { isymbol = toContentSymbol '`' , iname = "deflagration ignition" -- black powder , ifreq = [(S_FOCUSED_FRAGMENTATION, 1)] , iflavour = zipPlain [BrYellow] , icount = 3 -- 15 in total vs 8, higher spread , irarity = [(1, 1)] , iverbHit = "ignite" , iweight = 1 , idamage = 0 , iaspects = [ toLinger 0 -- 0 steps, 1 turn , SetFlag Fragile, SetFlag Blast ] -- when the target position is occupied, the explosion starts one step -- away, hence we set range to 0 steps, to limit dispersal , ieffects = [OnSmash $ Explode S_FRAGMENTATION] , idesc = idesc spreadFragmentation , ikit = [] } spreadConcussion = ItemKind { isymbol = toContentSymbol '*' , iname = "concussion blast" , ifreq = [(S_VIOLENT_CONCUSSION, 1)] , iflavour = zipPlain [Magenta] , icount = 12 -- pushing sometimes gets the victim out of attacker range, -- but also sometimes moves to a position hit again later , irarity = [(1, 1)] , iverbHit = "shock" , iweight = 1 , idamage = 1 `d` 1 -- only air pressure, so not as deadly as fragmentation, -- but armour can't block completely that easily , iaspects = [ ToThrow $ ThrowMod 100 20 4 -- 4 steps, 1 turn , SetFlag Lobable, SetFlag Fragile, SetFlag Blast , AddSkill SkShine 3, AddSkill SkHurtMelee $ -8 * 5 ] -- outdoors it has short range, but we only model indoors in the game; -- it's much faster than black powder shock wave, but we are beyond -- human-noticeable speed differences on short distances anyway , ieffects = [ DropItem maxBound 1 CEqp ARMOR_MISC , PushActor (ThrowMod 400 25 1) -- 1 step, fast; after DropItem -- this produces spam for braced actors; too bad , toOrganBad S_IMMOBILE 1 -- no balance , toOrganBad S_DEAFENED 23 ] , idesc = "Shock wave, hot gases, some fire and smoke." , ikit = [] } spreadConcussion8 = spreadConcussion { ifreq = [(S_CONCUSSION, 1)] , icount = 6 , iaspects = [ ToThrow $ ThrowMod 100 10 2 -- 2 steps, 1 turn , SetFlag Lobable, SetFlag Fragile, SetFlag Blast , AddSkill SkShine 3, AddSkill SkHurtMelee $ -8 * 5 ] } focusedConcussion = ItemKind { isymbol = toContentSymbol '`' , iname = "detonation ignition" -- nitroglycerine , ifreq = [(S_FOCUSED_CONCUSSION, 1)] , iflavour = zipPlain [BrYellow] , icount = 4 -- 24 in total vs 12, higher spread, less harm from pushing , irarity = [(1, 1)] , iverbHit = "ignite" , iweight = 1 , idamage = 0 , iaspects = [ toLinger 0 -- 0 steps, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [OnSmash $ Explode S_CONCUSSION] , idesc = idesc spreadConcussion , ikit = [] } spreadFlash = ItemKind { isymbol = toContentSymbol '`' , iname = "magnesium flash" , ifreq = [(S_VIOLENT_FLASH, 1)] , iflavour = zipPlain [BrWhite] , icount = 13 , irarity = [(1, 1)] , iverbHit = "dazzle" , iweight = 1 , idamage = 0 , iaspects = [ ToThrow $ ThrowMod 100 20 4 -- 4 steps, 1 turn , SetFlag Fragile, SetFlag Blast , AddSkill SkShine 5 ] , ieffects = [ toOrganBad S_BLIND 5 , toOrganBad S_WEAKENED 20 ] -- Wikipedia says: blind for five seconds and afterimage -- for much longer, harming aim , idesc = "A very bright flash of fire, causing long-lasting afterimages." , ikit = [] } spreadFlash8 = spreadFlash { iname = "spark" , ifreq = [(S_SPARK, 1)] , icount = 5 , iverbHit = "singe" , iaspects = [ ToThrow $ ThrowMod 100 10 2 -- 2 steps, 1 turn , SetFlag Fragile, SetFlag Blast , AddSkill SkShine 5 ] } focusedFlash = ItemKind { isymbol = toContentSymbol '`' , iname = "magnesium ignition" , ifreq = [(S_FOCUSED_FLASH, 1)] , iflavour = zipPlain [BrYellow] , icount = 5 -- 25 in total vs 13, higher spread , irarity = [(1, 1)] , iverbHit = "ignite" , iweight = 1 , idamage = 0 , iaspects = [ toLinger 0 -- 0 steps, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [OnSmash $ Explode S_SPARK] , idesc = idesc spreadFlash , ikit = [] } singleSpark = spreadFlash { iname = "single spark" , ifreq = [(S_SINGLE_SPARK, 1)] -- too weak to start a fire , icount = 1 , iverbHit = "spark" , iaspects = [ toLinger 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast , AddSkill SkShine 3 ] , ieffects = [] , idesc = "A glowing ember." , ikit = [] } glassPiece = ItemKind { isymbol = toContentSymbol '*' , iname = "glass piece" , ifreq = [(S_GLASS_HAIL, 1)] , iflavour = zipPlain [Blue] , icount = 6 , irarity = [(1, 1)] , iverbHit = "cut" , iweight = 1 , idamage = 2 `d` 1 , iaspects = [ ToThrow $ ThrowMod 100 10 4 -- 2 steps, 1 turn , SetFlag Fragile, SetFlag Blast , AddSkill SkHurtMelee $ -15 * 5 ] -- brittle, not too dense; armor blocks , ieffects = [] , idesc = "Swift, sharp edges." , ikit = [] } focusedGlass = glassPiece -- when blowing up windows { ifreq = [(S_FOCUSED_GLASS_HAIL, 1)] , icount = 2 , iaspects = [ toLinger 0 -- 0 steps, 1 turn , SetFlag Fragile, SetFlag Blast , AddSkill SkHurtMelee $ -15 * 5 ] , ieffects = [OnSmash $ Explode S_GLASS_HAIL] } -- * Assorted blasts that don't induce conditions or not used mainly for them fragrance = ItemKind { isymbol = toContentSymbol '`' , iname = "fragrance" -- instant, fast fragrance , ifreq = [(S_FRAGRANCE, 1)] , iflavour = zipPlain [Magenta] , icount = 8 , irarity = [(1, 1)] , iverbHit = "engulf" , iweight = 1 , idamage = 0 , iaspects = [ ToThrow $ ThrowMod 200 5 1 -- 2 steps, .5 turn (necklaces) , SetFlag Fragile, SetFlag Blast ] , ieffects = [Impress, toOrganGood S_ROSE_SMELLING 45] -- Linger 10, because sometimes it takes 2 turns due to starting just -- before actor turn's end (e.g., via a necklace). , idesc = "A pleasant scent." , ikit = [] } pheromone = ItemKind { isymbol = toContentSymbol '`' , iname = "musky whiff" -- a kind of mist rather than fragrance , ifreq = [(S_PHEROMONE, 1)] , iflavour = zipPlain [BrMagenta] , icount = 16 , irarity = [(1, 1)] , iverbHit = "tempt" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 10 -- 2 steps, 2 turns , SetFlag Fragile, SetFlag Blast ] , ieffects = [Dominate] , idesc = "A sharp, strong scent." , ikit = [] } mistCalming = ItemKind -- unused { isymbol = toContentSymbol '`' , iname = "mist" , ifreq = [(S_CALMING_MIST, 1)] , iflavour = zipPlain [BrGreen] , icount = 8 , irarity = [(1, 1)] , iverbHit = "sooth" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [RefillCalm 2] , idesc = "A soothing, gentle cloud." , ikit = [] } odorDistressing = ItemKind { isymbol = toContentSymbol '`' , iname = "distressing whiff" , ifreq = [(S_DISTRESSING_ODOR, 1)] , iflavour = zipFancy [BrRed] -- salmon , icount = 8 , irarity = [(1, 1)] , iverbHit = "distress" , iweight = 1 , idamage = 0 , iaspects = [ toLinger 10 -- 2 steps, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [ RefillCalm (-10) , toOrganBad S_FOUL_SMELLING (20 + 1 `d` 5) , toOrganBad S_IMPATIENT (2 + 1 `d` 2) ] , idesc = "It turns the stomach." -- and so can't stand still , ikit = [] } mistHealing = ItemKind { isymbol = toContentSymbol '`' , iname = "mist" -- powerful, so slow and narrow , ifreq = [(S_HEALING_MIST, 1)] , iflavour = zipFancy [BrGreen] , icount = 8 , irarity = [(1, 1)] , iverbHit = "revitalize" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast , AddSkill SkShine 1 ] , ieffects = [RefillHP 2] , idesc = "It fills the air with light and life." , ikit = [] } mistHealing2 = ItemKind { isymbol = toContentSymbol '`' , iname = "mist" , ifreq = [(S_HEALING_MIST_2, 1)] , iflavour = zipPlain [Green] , icount = 8 , irarity = [(1, 1)] , iverbHit = "revitalize" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast , AddSkill SkShine 2 ] , ieffects = [RefillHP 4] , idesc = "At its touch, wounds close and bruises fade." , ikit = [] } mistWounding = ItemKind { isymbol = toContentSymbol '`' , iname = "mist" , ifreq = [(S_WOUNDING_MIST, 1)] , iflavour = zipPlain [BrRed] , icount = 8 , irarity = [(1, 1)] , iverbHit = "devitalize" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [RefillHP (-2)] , idesc = "The air itself stings and itches." , ikit = [] } distortion = ItemKind { isymbol = toContentSymbol 'v' , iname = "vortex" , ifreq = [(S_DISTORTION, 1)] , iflavour = zipPlain [White] , icount = 8 -- braced are immune to Teleport; avoid failure messages , irarity = [(1, 1)] , iverbHit = "engulf" , iweight = 1 , idamage = 0 , iaspects = [ toLinger 10 -- 2 steps, 1 turn , SetFlag Lobable, SetFlag Fragile, SetFlag Blast ] , ieffects = [Teleport $ 15 + 1 `d` 10] , idesc = "The air shifts oddly, as though light is being warped." , ikit = [] } smoke = ItemKind -- when stuff burns out -- unused { isymbol = toContentSymbol '`' , iname = "smoke fume" -- pluralizes better than 'smokes' , ifreq = [(S_SMOKE, 1)] , iflavour = zipPlain [BrBlack] , icount = 16 , irarity = [(1, 1)] , iverbHit = "choke" -- or "obscure" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 20 -- 4 steps, 2 turns , SetFlag Fragile, SetFlag Blast ] , ieffects = [toOrganBad S_WITHHOLDING (5 + 1 `d` 3)] -- choking and tears, can roughly see, but not aim , idesc = "Twirling clouds of grey smoke." , ikit = [] } boilingWater = ItemKind { isymbol = toContentSymbol '*' , iname = "boiling water" , ifreq = [(S_BOILING_WATER, 1)] , iflavour = zipPlain [White] , icount = 17 -- 18 causes 3 particles to hit the same actor 3 tiles away , irarity = [(1, 1)] , iverbHit = "boil" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 30 -- 6 steps, 2 turns , SetFlag Fragile, SetFlag Blast ] , ieffects = [Burn 1] , idesc = "It bubbles and hisses." , ikit = [] } glue = ItemKind { isymbol = toContentSymbol '*' , iname = "hoof glue" , ifreq = [(S_GLUE, 1)] , iflavour = zipPlain [Cyan] , icount = 8 -- Paralyze doesn't stack; avoid failure messages , irarity = [(1, 1)] , iverbHit = "glue" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 10 -- 2 steps, 2 turns , SetFlag Fragile, SetFlag Blast ] , ieffects = [Paralyze 10] , idesc = "Thick and clinging." , ikit = [] } waste = ItemKind { isymbol = toContentSymbol '*' , iname = "waste piece" , ifreq = [(S_WASTE, 1)] , iflavour = zipPlain [Brown] , icount = 16 , irarity = [(1, 1)] , iverbHit = "splosh" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [ toOrganBad S_FOUL_SMELLING (30 + 1 `d` 10) , toOrganBad S_DISPOSSESSED (10 + 1 `d` 5) ] , idesc = "Sodden and foul-smelling." , ikit = [] } mistAntiSlow = ItemKind { isymbol = toContentSymbol '`' , iname = "mist" , ifreq = [(S_ANTI_SLOW_MIST, 1)] , iflavour = zipFancy [BrYellow] , icount = 8 , irarity = [(1, 1)] , iverbHit = "propel" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [DropItem 1 1 COrgan S_SLOWED] , idesc = "A cleansing rain." , ikit = [] } mistAntidote = ItemKind { isymbol = toContentSymbol '`' , iname = "mist" , ifreq = [(S_ANTIDOTE_MIST, 1)] , iflavour = zipFancy [BrBlue] , icount = 8 , irarity = [(1, 1)] , iverbHit = "cure" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [DropItem 1 maxBound COrgan S_POISONED] , idesc = "Washes away death's dew." , ikit = [] } mistSleep = ItemKind { isymbol = toContentSymbol '`' , iname = "mist" , ifreq = [(S_SLEEP_MIST, 1)] , iflavour = zipFancy [BrMagenta] , icount = 8 , irarity = [(1, 1)] , iverbHit = "put to sleep" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 5 -- 1 step, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [PutToSleep] , idesc = "Lulls weary warriors." , ikit = [] } -- * Condition-inducing blasts -- Almost all have @toLinger 10@, that travels 2 steps in 1 turn. -- These are very fast projectiles, not getting into the way of big -- actors and not burdening the engine for long. -- A few are slower 'mists'. denseShower = ItemKind { isymbol = toContentSymbol '`' , iname = "dense shower" , ifreq = [(S_DENSE_SHOWER, 1)] , iflavour = zipFancy [Green] , icount = 12 , irarity = [(1, 1)] , iverbHit = "strengthen" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganGood S_STRENGTHENED 5] , idesc = "A thick rain of droplets." , ikit = [] } sparseShower = ItemKind { isymbol = toContentSymbol '`' , iname = "sparse shower" , ifreq = [(S_SPARSE_SHOWER, 1)] , iflavour = zipFancy [Red] , icount = 8 , irarity = [(1, 1)] , iverbHit = "weaken" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganBad S_WEAKENED 7] , idesc = "Light droplets that cling to clothing." , ikit = [] } protectingBalmMelee = ItemKind { isymbol = toContentSymbol '`' , iname = "balm droplet" , ifreq = [(S_MELEE_PROTECTIVE_BALM, 1)] , iflavour = zipFancy [Brown] , icount = 6 , irarity = [(1, 1)] , iverbHit = "balm" , iweight = 1 , idamage = 0 , iaspects = [ toLinger 0 -- 0 steps, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [toOrganGood S_PROTECTED_FROM_MELEE (3 + 1 `d` 3)] , idesc = "A thick ointment that hardens the skin." , ikit = [] } protectingBalmRanged = ItemKind { isymbol = toContentSymbol '`' , iname = "balm droplet" , ifreq = [(S_RANGE_PROTECTIVE_BALM, 1)] , iflavour = zipPlain [BrYellow] , icount = 16 , irarity = [(1, 1)] , iverbHit = "balm" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganGood S_PROTECTED_FROM_RANGED (3 + 1 `d` 3)] , idesc = "Grease that protects from flying death." , ikit = [] } defenselessnessRunout = ItemKind { isymbol = toContentSymbol '?' , iname = "PhD defense question" , ifreq = [(S_DEFENSELESSNESS_RUNOUT, 1)] , iflavour = zipFancy [BrRed] , icount = 16 , irarity = [(1, 1)] , iverbHit = "nag" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganBad S_DEFENSELESS (3 + 1 `d` 3)] , idesc = "Only the most learned make use of this." , ikit = [] } resolutionDust = ItemKind { isymbol = toContentSymbol '`' , iname = "resolution dust" , ifreq = [(S_RESOLUTION_DUST, 1)] , iflavour = zipPlain [Brown] , icount = 16 , irarity = [(1, 1)] , iverbHit = "calm" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganGood S_RESOLUTE (3 + 1 `d` 3)] -- short enough duration that @calmEnough@ not a big problem , idesc = "A handful of honest earth, to strengthen the soul." , ikit = [] } hasteSpray = ItemKind { isymbol = toContentSymbol '`' , iname = "haste spray" , ifreq = [(S_HASTE_SPRAY, 1)] , iflavour = zipFancy [BrYellow] , icount = 16 , irarity = [(1, 1)] , iverbHit = "haste" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganGood S_HASTED (3 + 1 `d` 3)] , idesc = "A quick spurt." , ikit = [] } spreadNitrogen = ItemKind { isymbol = toContentSymbol '`' , iname = "slowness mist" , ifreq = [(S_VIOLENT_SLOWNESS_MIST, 1)] , iflavour = zipPlain [BrBlack] , icount = 15 , irarity = [(1, 1)] , iverbHit = "freeze" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 10 -- 2 steps, 2 turns, mist, slow , SetFlag Fragile, SetFlag Blast ] , ieffects = [toOrganBad S_SLOWED (2 + 1 `d` 3)] , idesc = "Colourless cold clammy fog, making each movement an effort." , ikit = [] } spreadNitrogen8 = spreadNitrogen { ifreq = [(S_SLOWNESS_MIST, 1)] , icount = 7 , iaspects = [ toVelocity 5 -- 1 step, 1 turn, mist, slow , SetFlag Fragile, SetFlag Blast ] } focusedNitrogen = ItemKind { isymbol = toContentSymbol '`' , iname = "slowness mist droplet" , ifreq = [(S_FOCUSED_SLOWNESS_MIST, 1)] , iflavour = zipFancy [White] , icount = 4 -- 28 in total vs 15, higher spread , irarity = [(1, 1)] , iverbHit = "freeze" , iweight = 1 , idamage = 0 , iaspects = [ toLinger 0 -- 0 steps, 1 turn , SetFlag Fragile, SetFlag Blast ] , ieffects = [ OnSmash $ Explode S_SLOWNESS_MIST , toOrganBad S_SLOWED (2 + 1 `d` 3) ] , idesc = "Colourless and colder than ice." , ikit = [] } eyeDrop = ItemKind { isymbol = toContentSymbol '`' , iname = "eye drop" , ifreq = [(S_EYE_DROP, 1)] , iflavour = zipFancy [BrCyan] , icount = 16 , irarity = [(1, 1)] , iverbHit = "cleanse" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganGood S_FAR_SIGHTED (3 + 1 `d` 3)] , idesc = "Not to be taken orally." , ikit = [] } ironFiling = ItemKind -- fast, short, strongly blinding blast { isymbol = toContentSymbol '`' , iname = "iron filing" , ifreq = [(S_IRON_FILING, 1)] , iflavour = zipPlain [Red] , icount = 16 , irarity = [(1, 1)] , iverbHit = "blind" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganBad S_BLIND (10 + 1 `d` 10)] , idesc = "A shaving of bright metal." , ikit = [] } smellyDroplet = ItemKind { isymbol = toContentSymbol '`' , iname = "smelly droplet" , ifreq = [(S_SMELLY_DROPLET, 1)] , iflavour = zipFancy [Blue] , icount = 16 , irarity = [(1, 1)] , iverbHit = "sensitize" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganGood S_KEEN_SMELLING (5 + 1 `d` 3)] , idesc = "A viscous lump that stains the skin." , ikit = [] } eyeShine = ItemKind { isymbol = toContentSymbol '`' , iname = "eye shine" , ifreq = [(S_EYE_SHINE, 1)] , iflavour = zipFancy [Cyan] , icount = 16 , irarity = [(1, 1)] , iverbHit = "smear" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganGood S_SHINY_EYED (3 + 1 `d` 3)] , idesc = "They almost glow in the dark." , ikit = [] } whiskeySpray = ItemKind { isymbol = toContentSymbol '`' , iname = "whiskey spray" , ifreq = [(S_WHISKEY_SPRAY, 1)] , iflavour = zipFancy [Brown] , icount = 16 , irarity = [(1, 1)] , iverbHit = "inebriate" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [toOrganGood S_DRUNK (3 + 1 `d` 3)] , idesc = "It burns in the best way." , ikit = [] } youthSprinkle = ItemKind { isymbol = toContentSymbol '`' , iname = "youth sprinkle" , ifreq = [(S_YOUTH_SPRINKLE, 1)] , iflavour = zipFancy [BrGreen] , icount = 16 , irarity = [(1, 1)] , iverbHit = "sprinkle" , iweight = 1 , idamage = 0 , iaspects = [toLinger 10, SetFlag Fragile, SetFlag Blast] , ieffects = [ toOrganGood S_ROSE_SMELLING (40 + 1 `d` 20) , toOrganNoTimer S_REGENERATING ] , idesc = "Bright and smelling of the Spring." , ikit = [] } poisonCloud = ItemKind { isymbol = toContentSymbol '`' , iname = "poison cloud" , ifreq = [(S_POISON_CLOUD, 1)] , iflavour = zipFancy [BrMagenta] , icount = 11 -- low, to be less deadly in a tunnel, compared to single hit , irarity = [(1, 1)] , iverbHit = "poison" , iweight = 0 -- lingers, blocking path , idamage = 0 , iaspects = [ ToThrow $ ThrowMod 10 100 2 -- 2 steps, 2 turns , SetFlag Fragile, SetFlag Blast ] , ieffects = [toOrganNoTimer S_POISONED] , idesc = "Choking gas that stings the eyes." , ikit = [] } pingFlash = ItemKind { isymbol = toContentSymbol '`' , iname = "flash" , ifreq = [(S_PING_PLASH, 1)] , iflavour = zipFancy [Green] , icount = 1 , irarity = [(1, 1)] , iverbHit = "ping" , iweight = 1 -- to prevent blocking the way , idamage = 0 , iaspects = [ ToThrow $ ThrowMod 200 0 1 -- 1 step, .5 turn (necklaces) , SetFlag Fragile, SetFlag Blast , AddSkill SkShine 2 ] , ieffects = [OnSmash Yell] , idesc = "A ping and a display flash from an echolocator out of sync momentarily." , ikit = [] } blastNoStat :: GroupName ItemKind -> ItemKind blastNoStat grp = ItemKind { isymbol = toContentSymbol '`' , iname = "mist" , ifreq = [(blastNoStatOf grp, 1)] , iflavour = zipFancy [White] , icount = 12 , irarity = [(1, 1)] , iverbHit = "drain" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 10 -- 2 steps, 2 turns , SetFlag Fragile, SetFlag Blast ] , ieffects = [toOrganBad grp (3 + 1 `d` 3)] , idesc = "Completely disables one personal faculty." , ikit = [] } blastNoSkMove = blastNoStat S_IMMOBILE blastNoSkMelee = blastNoStat S_PACIFIED blastNoSkDisplace = blastNoStat S_IRREPLACEABLE blastNoSkAlter = blastNoStat S_RETAINING blastNoSkWait = blastNoStat S_IMPATIENT blastNoSkMoveItem = blastNoStat S_DISPOSSESSED blastNoSkProject = blastNoStat S_WITHHOLDING blastNoSkApply = blastNoStat S_PARSIMONIOUS blastBonusStat :: GroupName ItemKind -> ItemKind blastBonusStat grp = ItemKind { isymbol = toContentSymbol '`' , iname = "dew" , ifreq = [(blastBonusStatOf grp, 1)] , iflavour = zipFancy [White] , icount = 12 , irarity = [(1, 1)] , iverbHit = "elevate" , iweight = 1 , idamage = 0 , iaspects = [ toVelocity 10 -- 2 steps, 2 turns , SetFlag Fragile, SetFlag Blast ] , ieffects = [toOrganGood grp (20 + 1 `d` 5)] , idesc = "Temporarily enhances the given personal faculty." , ikit = [] } blastBonusSkMove = blastBonusStat S_MORE_MOBILE blastBonusSkMelee = blastBonusStat S_MORE_COMBATIVE blastBonusSkDisplace = blastBonusStat S_MORE_DISPLACING blastBonusSkAlter = blastBonusStat S_MORE_MODIFYING blastBonusSkWait = blastBonusStat S_MORE_PATIENT blastBonusSkMoveItem = blastBonusStat S_MORE_TIDY blastBonusSkProject = blastBonusStat S_MORE_PROJECTING blastBonusSkApply = blastBonusStat S_MORE_PRACTICAL LambdaHack-0.11.0.0/GameDefinition/Content/ItemKindEmbed.hs0000644000000000000000000003524207346545000021366 0ustar0000000000000000-- | Definitions of items embedded in map tiles. module Content.ItemKindEmbed ( -- * Group name patterns pattern SCRATCH_ON_WALL, pattern OBSCENE_PICTOGRAM, pattern SUBTLE_FRESCO, pattern TREASURE_CACHE, pattern TREASURE_CACHE_TRAP, pattern SIGNAGE, pattern SMALL_FIRE, pattern BIG_FIRE, pattern FROST, pattern RUBBLE, pattern DOORWAY_TRAP_UNKNOWN, pattern DOORWAY_TRAP, pattern STAIRS_UP, pattern STAIRS_DOWN, pattern ESCAPE, pattern STAIRS_TRAP_UP, pattern STAIRS_TRAP_DOWN, pattern LECTERN, pattern SHALLOW_WATER, pattern STRAIGHT_PATH, pattern FROZEN_GROUND , embedsGN , -- * Content embeds ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour import Content.ItemKindActor import Content.ItemKindBlast import Content.ItemKindTemporary -- * Group name patterns embedsGN :: [GroupName ItemKind] embedsGN = [SCRATCH_ON_WALL, OBSCENE_PICTOGRAM, SUBTLE_FRESCO, TREASURE_CACHE, TREASURE_CACHE_TRAP, SIGNAGE, SMALL_FIRE, BIG_FIRE, FROST, RUBBLE, DOORWAY_TRAP_UNKNOWN, DOORWAY_TRAP, STAIRS_UP, STAIRS_DOWN, ESCAPE, STAIRS_TRAP_UP, STAIRS_TRAP_DOWN, LECTERN, SHALLOW_WATER, STRAIGHT_PATH, FROZEN_GROUND] pattern SCRATCH_ON_WALL, OBSCENE_PICTOGRAM, SUBTLE_FRESCO, TREASURE_CACHE, TREASURE_CACHE_TRAP, SIGNAGE, SMALL_FIRE, BIG_FIRE, FROST, RUBBLE, DOORWAY_TRAP_UNKNOWN, DOORWAY_TRAP, STAIRS_UP, STAIRS_DOWN, ESCAPE, STAIRS_TRAP_UP, STAIRS_TRAP_DOWN, LECTERN, SHALLOW_WATER, STRAIGHT_PATH, FROZEN_GROUND :: GroupName ItemKind pattern SCRATCH_ON_WALL = GroupName "scratch on wall" pattern OBSCENE_PICTOGRAM = GroupName "obscene pictogram" pattern SUBTLE_FRESCO = GroupName "subtle fresco" pattern TREASURE_CACHE = GroupName "treasure cache" pattern TREASURE_CACHE_TRAP = GroupName "treasure cache trap" pattern SIGNAGE = GroupName "signage" pattern SMALL_FIRE = GroupName "small fire" pattern BIG_FIRE = GroupName "big fire" pattern FROST = GroupName "frozen mass" pattern RUBBLE = GroupName "rubble" pattern DOORWAY_TRAP_UNKNOWN = GroupName "doorway trap unknown" pattern DOORWAY_TRAP = GroupName "doorway trap" pattern STAIRS_UP = GroupName "stairs up" pattern STAIRS_DOWN = GroupName "stairs down" pattern ESCAPE = GroupName "escape" pattern STAIRS_TRAP_UP = GroupName "stairs trap up" pattern STAIRS_TRAP_DOWN = GroupName "stairs trap down" pattern LECTERN = GroupName "lectern" pattern SHALLOW_WATER = GroupName "shallow water" pattern STRAIGHT_PATH = GroupName "straight path" pattern FROZEN_GROUND = GroupName "frozen ground" -- * Content embeds :: [ItemKind] embeds = [scratchOnWall, obscenePictogram, subtleFresco, treasureCache, treasureCacheTrap, signageExit, signageEmbed, signageMerchandise, fireSmall, fireBig, frost, rubble, doorwayTrapTemplate, doorwayTrap1, doorwayTrap2, doorwayTrap3, stairsUp, stairsDown, escape, stairsTrapUp, stairsTrapDown, lectern, shallowWater, straightPath, frozenGround] scratchOnWall, obscenePictogram, subtleFresco, treasureCache, treasureCacheTrap, signageExit, signageEmbed, signageMerchandise, fireSmall, fireBig, frost, rubble, doorwayTrapTemplate, doorwayTrap1, doorwayTrap2, doorwayTrap3, stairsUp, stairsDown, escape, stairsTrapUp, stairsTrapDown, lectern, shallowWater, straightPath, frozenGround :: ItemKind -- Make sure very few walls are substantially useful, e.g., caches, -- and none that are secret. Otherwise the player will spend a lot of time -- bumping walls, which is boring compared to fights or dialogues -- and ever worse, the player will bump all secret walls, wasting time -- and foregoing the fun of guessing how to find entrance to a disjoint part -- of the level by bumping the least number of secret walls. scratchOnWall = ItemKind { isymbol = toContentSymbol '?' , iname = "claw mark" , ifreq = [(SCRATCH_ON_WALL, 1)] , iflavour = zipPlain [BrBlack] , icount = 1 , irarity = [(1, 1)] , iverbHit = "scratch" , iweight = 1000 , idamage = 0 , iaspects = [SetFlag Durable] , ieffects = [ VerbMsg "start making sense of the scratches" "." , Detect DetectHidden 4 ] , idesc = "A seemingly random series of scratches, carved deep into the wall." , ikit = [] } obscenePictogram = ItemKind { isymbol = toContentSymbol '*' , iname = "obscene pictogram" , ifreq = [(OBSCENE_PICTOGRAM, 1)] , iflavour = zipPlain [BrMagenta] , icount = 1 , irarity = [(1, 1)] , iverbHit = "infuriate" , iweight = 1000 , idamage = 0 , iaspects = [Timeout 7, SetFlag Durable] , ieffects = [ VerbMsg "enter destructive rage at the sight of an obscene pictogram" "." , RefillCalm (-20) , OneOf [ toOrganGood S_STRENGTHENED (3 + 1 `d` 2) , CreateItem Nothing CGround S_SANDSTONE_ROCK timerNone ] ] , idesc = "It's not even anatomically possible." , ikit = [] } subtleFresco = ItemKind { isymbol = toContentSymbol '*' , iname = "subtle fresco" , ifreq = [(SUBTLE_FRESCO, 1)] , iflavour = zipPlain [BrGreen] , icount = 1 , irarity = [(1, 1)] , iverbHit = "sooth" , iweight = 1000 , idamage = 0 , iaspects = [Timeout 7, SetFlag Durable] , ieffects = [ VerbMsg "feel refreshed by the subtle fresco" "." , toOrganGood S_FAR_SIGHTED (3 + 1 `d` 2) , toOrganGood S_KEEN_SMELLING (3 + 1 `d` 2) ] -- hearing gets a boost through bracing, so no need here , idesc = "Expensive yet tasteful." , ikit = [] } treasureCache = ItemKind { isymbol = toContentSymbol 'o' , iname = "treasure cache" , ifreq = [(TREASURE_CACHE, 1)] , iflavour = zipPlain [BrBlue] , icount = 1 , irarity = [(1, 1)] , iverbHit = "crash" , iweight = 10000 , idamage = 0 , iaspects = [SetFlag Durable] , ieffects = [CreateItem Nothing CGround COMMON_ITEM timerNone] , idesc = "Glittering treasure, just waiting to be taken." , ikit = [] } reliefMsg :: Effect reliefMsg = VerbMsg "sigh with relief when nothing explodes in your face!" "" treasureCacheTrap = ItemKind { isymbol = toContentSymbol '^' , iname = "cache trap" , ifreq = [(TREASURE_CACHE_TRAP, 1)] , iflavour = zipPlain [Red] , icount = 1 , irarity = [(1, 1)] , iverbHit = "taint" , iweight = 1000 , idamage = 0 , iaspects = [] -- not Durable, springs at most once , ieffects = [OneOf [ toOrganBad S_BLIND (10 + 1 `d` 10) , RefillCalm (-99) , Explode S_FOCUSED_CONCUSSION , reliefMsg, reliefMsg ]] , idesc = "It's a trap!" , ikit = [] } signageExit = ItemKind { isymbol = toContentSymbol '?' , iname = "inscription" , ifreq = [(SIGNAGE, 100)] , iflavour = zipPlain [BrGreen] , icount = 1 , irarity = [(1, 1)] , iverbHit = "whack" , iweight = 10000 , idamage = 0 , iaspects = [SetFlag Durable] , ieffects = [Detect DetectExit 100] , idesc = "Crude big arrows hastily carved by unknown hands." , ikit = [] } signageEmbed = signageExit { iname = "notice" , ifreq = [(SIGNAGE, 100)] , iflavour = zipPlain [Cyan] , ieffects = [Detect DetectEmbed 12] , idesc = "The battered poster is untitled and unsigned." } signageMerchandise = signageExit { iname = "treasure map" , ifreq = [(SIGNAGE, 100)] , iflavour = zipPlain [BrCyan] , ieffects = [Detect DetectLoot 20] , idesc = "In equal parts cryptic and promising." } fireSmall = ItemKind { isymbol = toContentSymbol 'o' , iname = "small fire" , ifreq = [(SMALL_FIRE, 1)] , iflavour = zipPlain [BrRed] , icount = 1 , irarity = [(1, 1)] , iverbHit = "burn" , iweight = 10000 , idamage = 0 , iaspects = [SetFlag Durable] , ieffects = [Burn 1, Explode S_SINGLE_SPARK] , idesc = "A few small logs, burning brightly." , ikit = [] } fireBig = fireSmall { isymbol = toContentSymbol '0' , iname = "big fire" , ifreq = [(BIG_FIRE, 1)] , iflavour = zipPlain [Red] , ieffects = [ Burn 2 , CreateItem Nothing CGround S_WOODEN_TORCH timerNone , Explode S_SPARK ] , idesc = "Glowing with light and warmth." , ikit = [] } frost = ItemKind { isymbol = toContentSymbol '^' , iname = "frost" , ifreq = [(FROST, 1)] , iflavour = zipPlain [BrBlue] , icount = 1 , irarity = [(1, 1)] , iverbHit = "burn" , iweight = 10000 , idamage = 0 , iaspects = [SetFlag Durable] , ieffects = [ Burn 1 -- sensory ambiguity between hot and cold , RefillCalm 20 -- cold reason , PushActor (ThrowMod 400 10 1) ] -- slippery ice , idesc = "Intricate patterns of shining ice." , ikit = [] } rubble = ItemKind { isymbol = toContentSymbol '&' , iname = "rubble" , ifreq = [(RUBBLE, 1)] , iflavour = zipPlain [BrYellow] , icount = 1 , irarity = [(1, 1)] , iverbHit = "bury" , iweight = 100000 , idamage = 0 , iaspects = [SetFlag Durable] , ieffects = [OneOf [ Explode S_FOCUSED_GLASS_HAIL , Summon MOBILE_ANIMAL $ 1 `dL` 2 , toOrganNoTimer S_POISONED , CreateItem Nothing CGround ANY_ARROW timerNone , CreateItem Nothing CGround STARTING_WEAPON timerNone , reliefMsg, reliefMsg, reliefMsg , reliefMsg, reliefMsg, reliefMsg ]] , idesc = "Broken chunks of rock and glass." , ikit = [] } doorwayTrapTemplate = ItemKind { isymbol = toContentSymbol '+' , iname = "doorway trap" , ifreq = [(DOORWAY_TRAP_UNKNOWN, 1), (DOORWAY_TRAP, 0)] -- the void group needed to pick the item for tile triggering -- even when not yet identified , iflavour = zipPlain brightCol , icount = 1 , irarity = [(1, 1)] , iverbHit = "cripple" , iweight = 10000 , idamage = 0 , iaspects = [PresentAs DOORWAY_TRAP_UNKNOWN] -- not Durable, springs at most once , ieffects = [] , idesc = "Just turn the handle..." , ikit = [] } doorwayTrap1 = doorwayTrapTemplate { ifreq = [(DOORWAY_TRAP, 50)] , ieffects = [toOrganBad S_BLIND $ (1 `dL` 4) * 5] -- , idesc = "" } doorwayTrap2 = doorwayTrapTemplate { ifreq = [(DOORWAY_TRAP, 25)] , ieffects = [toOrganBad S_SLOWED $ (1 `dL` 4) * 10] -- , idesc = "" } doorwayTrap3 = doorwayTrapTemplate { ifreq = [(DOORWAY_TRAP, 25)] , ieffects = [toOrganBad S_WEAKENED $ (1 `dL` 4) * 10 ] -- , idesc = "" } stairsUp = ItemKind { isymbol = toContentSymbol '<' , iname = "flight" , ifreq = [(STAIRS_UP, 1)] , iflavour = zipPlain [BrWhite] , icount = 1 , irarity = [(1, 1)] , iverbHit = "crash" -- the verb is only used when the item hits, -- not when it's applied otherwise, e.g., from tile , iweight = 100000 , idamage = 0 , iaspects = [ELabel "of steps", SetFlag Durable] , ieffects = [Ascend True] , idesc = "Stairs that rise towards escape." , ikit = [] } stairsDown = stairsUp { isymbol = toContentSymbol '>' , ifreq = [(STAIRS_DOWN, 1)] , ieffects = [Ascend False] , idesc = "" } escape = stairsUp { isymbol = toContentSymbol '>' , iname = "way" , ifreq = [(ESCAPE, 1)] , iflavour = zipPlain [BrGreen] , iaspects = [SetFlag Durable] , ieffects = [Escape] , idesc = "May this nightmare have an end?" -- generic escape, so the text should be too; -- for moon outdoors, spaceship, everywhere } stairsTrapUp = ItemKind { isymbol = toContentSymbol '^' , iname = "staircase trap" , ifreq = [(STAIRS_TRAP_UP, 1)] , iflavour = zipPlain [BrRed] , icount = 1 , irarity = [(1, 1)] , iverbHit = "buffet" , iweight = 10000 , idamage = 0 , iaspects = [] -- not Durable, springs at most once , ieffects = [ VerbMsgFail "be caught in an updraft" "." , Teleport $ 3 + 1 `dL` 10 ] , idesc = "A hidden spring, to help the unwary soar." , ikit = [] } -- Needs to be separate from stairsTrapUp, to make sure the item is -- registered after up stairs (not only after down stairs) -- so that effects are invoked in the proper order and, e.g., teleport works. stairsTrapDown = stairsTrapUp { ifreq = [(STAIRS_TRAP_DOWN, 1)] , iflavour = zipPlain [Red] , iverbHit = "open up under" , ieffects = [ VerbMsgFail "tumble down the stairwell" "." , toOrganGood S_DRUNK (20 + 1 `d` 5) ] , idesc = "A treacherous slab, to teach those who are too proud." } lectern = ItemKind { isymbol = toContentSymbol '?' , iname = "lectern" , ifreq = [(LECTERN, 1)] , iflavour = zipFancy [BrYellow] , icount = 1 , irarity = [(1, 1)] , iverbHit = "ask" , iweight = 10000 , idamage = 0 , iaspects = [] -- not Durable, springs at most once , ieffects = [ OneOf [ CreateItem Nothing CGround ANY_SCROLL timerNone , Detect DetectAll 20 , toOrganBad S_DEFENSELESS $ (1 `dL` 6) * 10 , toOrganGood S_DRUNK (20 + 1 `d` 5) ] , Explode S_DEFENSELESSNESS_RUNOUT ] , idesc = "A dark wood stand, where strange priests once preached." , ikit = [] } shallowWater = ItemKind { isymbol = toContentSymbol '~' , iname = "shallow water" , ifreq = [(SHALLOW_WATER, 1)] , iflavour = zipFancy [BrCyan] , icount = 1 , irarity = [(1, 1)] , iverbHit = "impede" , iweight = 10000 , idamage = 0 , iaspects = [SetFlag Durable] , ieffects = [ParalyzeInWater 2] , idesc = "" , ikit = [] } straightPath = ItemKind { isymbol = toContentSymbol '.' , iname = "straight path" , ifreq = [(STRAIGHT_PATH, 1)] , iflavour = zipFancy [BrRed] , icount = 1 , irarity = [(1, 1)] , iverbHit = "propel" , iweight = 10000 , idamage = 0 , iaspects = [SetFlag Durable] , ieffects = [InsertMove 2] , idesc = "" , ikit = [] } frozenGround = ItemKind { isymbol = toContentSymbol '.' , iname = "shade" , ifreq = [(FROZEN_GROUND, 1)] , iflavour = zipFancy [BrBlue] , icount = 10 -- very thick ice and refreezes, but not too large and boring , irarity = [(1, 1)] , iverbHit = "betray" , iweight = 10000 , idamage = 0 , iaspects = [ELabel "of ice"] -- no Durable or some items would be impossible to pick up , ieffects = [PushActor (ThrowMod 400 10 1)] -- the high speed represents gliding rather than flying -- and so no need to lift actor's weight off the ground; -- low linger comes from abrupt halt over normal surface , idesc = "" , ikit = [] } LambdaHack-0.11.0.0/GameDefinition/Content/ItemKindOrgan.hs0000644000000000000000000006211007346545000021412 0ustar0000000000000000-- | Actor organ definitions. module Content.ItemKindOrgan ( -- * Group name patterns pattern S_FIST, pattern S_FOOT, pattern S_HOOKED_CLAW, pattern S_SMALL_CLAW, pattern S_SNOUT, pattern S_SMALL_JAW, pattern S_JAW, pattern S_LARGE_JAW, pattern S_ANTLER, pattern S_HORN, pattern S_RHINO_HORN, pattern S_TENTACLE, pattern S_TIP, pattern S_LIP, pattern S_THORN, pattern S_BOILING_FISSURE, pattern S_ARSENIC_FISSURE, pattern S_SULFUR_FISSURE, pattern S_BEE_STING, pattern S_STING, pattern S_VENOM_TOOTH, pattern S_VENOM_FANG, pattern S_SCREECHING_BEAK, pattern S_LARGE_TAIL, pattern S_HUGE_TAIL, pattern S_ARMORED_SKIN, pattern S_BARK, pattern S_NOSTRIL, pattern S_RATLLE, pattern S_INSECT_MORTALITY, pattern S_SAPIENT_BRAIN, pattern S_ANIMAL_BRAIN, pattern S_SCENT_GLAND, pattern S_BOILING_VENT, pattern S_ARSENIC_VENT, pattern S_SULFUR_VENT, pattern S_EYE_3, pattern S_EYE_6, pattern S_EYE_8, pattern S_VISION_6, pattern S_VISION_12, pattern S_VISION_16, pattern S_EAR_3, pattern S_EAR_6, pattern S_EAR_8, pattern S_SPEED_GLAND_5, pattern S_SPEED_GLAND_10 , pattern SCAVENGER , pattern S_TOOTH, pattern S_LASH, pattern S_RIGHT_TORSION, pattern S_LEFT_TORSION, pattern S_PUPIL , organsGNSingleton, organsGN , -- * Content organs ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour import Content.ItemKindBlast import Content.ItemKindTemporary import Content.RuleKind -- * Group name patterns organsGNSingleton :: [GroupName ItemKind] organsGNSingleton = [S_FIST, S_FOOT, S_HOOKED_CLAW, S_SMALL_CLAW, S_SNOUT, S_SMALL_JAW, S_JAW, S_LARGE_JAW, S_ANTLER, S_HORN, S_RHINO_HORN, S_TENTACLE, S_TIP, S_LIP, S_THORN, S_BOILING_FISSURE, S_ARSENIC_FISSURE, S_SULFUR_FISSURE, S_BEE_STING, S_STING, S_VENOM_TOOTH, S_VENOM_FANG, S_SCREECHING_BEAK, S_LARGE_TAIL, S_HUGE_TAIL, S_ARMORED_SKIN, S_BARK, S_NOSTRIL, S_RATLLE, S_INSECT_MORTALITY, S_SAPIENT_BRAIN, S_ANIMAL_BRAIN, S_SCENT_GLAND, S_BOILING_VENT, S_ARSENIC_VENT, S_SULFUR_VENT, S_EYE_3, S_EYE_6, S_EYE_8, S_VISION_6, S_VISION_12, S_VISION_16, S_EAR_3, S_EAR_6, S_EAR_8, S_SPEED_GLAND_5, S_SPEED_GLAND_10] ++ [S_TOOTH, S_LASH, S_RIGHT_TORSION, S_LEFT_TORSION, S_PUPIL] pattern S_FIST, S_FOOT, S_HOOKED_CLAW, S_SMALL_CLAW, S_SNOUT, S_SMALL_JAW, S_JAW, S_LARGE_JAW, S_ANTLER, S_HORN, S_RHINO_HORN, S_TENTACLE, S_TIP, S_LIP, S_THORN, S_BOILING_FISSURE, S_ARSENIC_FISSURE, S_SULFUR_FISSURE, S_BEE_STING, S_STING, S_VENOM_TOOTH, S_VENOM_FANG, S_SCREECHING_BEAK, S_LARGE_TAIL, S_HUGE_TAIL, S_ARMORED_SKIN, S_BARK, S_NOSTRIL, S_RATLLE, S_INSECT_MORTALITY, S_SAPIENT_BRAIN, S_ANIMAL_BRAIN, S_SCENT_GLAND, S_BOILING_VENT, S_ARSENIC_VENT, S_SULFUR_VENT, S_EYE_3, S_EYE_6, S_EYE_8, S_VISION_6, S_VISION_12, S_VISION_16, S_EAR_3, S_EAR_6, S_EAR_8, S_SPEED_GLAND_5, S_SPEED_GLAND_10 :: GroupName ItemKind pattern S_TOOTH, S_LASH, S_RIGHT_TORSION, S_LEFT_TORSION, S_PUPIL :: GroupName ItemKind organsGN :: [GroupName ItemKind] organsGN = [SCAVENGER] pattern SCAVENGER :: GroupName ItemKind pattern S_FIST = GroupName "fist" pattern S_FOOT = GroupName "foot" pattern S_HOOKED_CLAW = GroupName "hooked claw" pattern S_SMALL_CLAW = GroupName "small claw" pattern S_SNOUT = GroupName "snout" pattern S_SMALL_JAW = GroupName "small jaw" pattern S_JAW = GroupName "jaw" pattern S_LARGE_JAW = GroupName "large jaw" pattern S_ANTLER = GroupName "antler" pattern S_HORN = GroupName "horn" pattern S_RHINO_HORN = GroupName "rhino horn" pattern S_TENTACLE = GroupName "tentacle" pattern S_TIP = GroupName "tip" pattern S_LIP = GroupName "lip" pattern S_THORN = GroupName "thorn" pattern S_BOILING_FISSURE = GroupName "boiling fissure" pattern S_ARSENIC_FISSURE = GroupName "arsenic fissure" pattern S_SULFUR_FISSURE = GroupName "sulfur fissure" pattern S_BEE_STING = GroupName "bee sting" pattern S_STING = GroupName "sting" pattern S_VENOM_TOOTH = GroupName "venom tooth" pattern S_VENOM_FANG = GroupName "venom fang" pattern S_SCREECHING_BEAK = GroupName "screeching beak" pattern S_LARGE_TAIL = GroupName "large tail" pattern S_HUGE_TAIL = GroupName "huge tail" pattern S_ARMORED_SKIN = GroupName "armored skin" pattern S_BARK = GroupName "bark" pattern S_NOSTRIL = GroupName "nostril" pattern S_RATLLE = GroupName "rattle" pattern S_INSECT_MORTALITY = GroupName "insect mortality" pattern S_SAPIENT_BRAIN = GroupName "sapient brain" pattern S_ANIMAL_BRAIN = GroupName "animal brain" pattern S_SCENT_GLAND = GroupName "scent gland" pattern S_BOILING_VENT = GroupName "boiling vent" pattern S_ARSENIC_VENT = GroupName "arsenic vent" pattern S_SULFUR_VENT = GroupName "sulfur vent" pattern S_EYE_3 = GroupName "eye 3" pattern S_EYE_6 = GroupName "eye 6" pattern S_EYE_8 = GroupName "eye 8" pattern S_VISION_6 = GroupName "vision 6" pattern S_VISION_12 = GroupName "vision 12" pattern S_VISION_16 = GroupName "vision 16" pattern S_EAR_3 = GroupName "ear 3" pattern S_EAR_6 = GroupName "ear 6" pattern S_EAR_8 = GroupName "ear 8" pattern S_SPEED_GLAND_5 = GroupName "speed gland 5" pattern S_SPEED_GLAND_10 = GroupName "speed gland 10" pattern SCAVENGER = GroupName "scavenger" -- * LH-specific pattern S_TOOTH = GroupName "tooth" pattern S_LASH = GroupName "lash" pattern S_RIGHT_TORSION = GroupName "right torsion" pattern S_LEFT_TORSION = GroupName "left torsion" pattern S_PUPIL = GroupName "pupil" -- * Content organs :: [ItemKind] organs = [fist, foot, hookedClaw, smallClaw, snout, smallJaw, jaw, largeJaw, antler, horn, rhinoHorn, tentacle, tip, lip, thorn, boilingFissure, arsenicFissure, sulfurFissure, beeSting, sting, venomTooth, venomFang, screechingBeak, largeTail, hugeTail, armoredSkin, bark, eye3, eye6, eye8, vision6, vision12, vision16, nostril, ear3, ear6, ear8, rattleOrgan, insectMortality, sapientBrain, animalBrain, speedGland5, speedGland10, scentGland, boilingVent, arsenicVent, sulfurVent, bonusHP, braced, asleep, impressed] -- LH-specific ++ [tooth, lash, torsionRight, torsionLeft, pupil] fist, foot, hookedClaw, smallClaw, snout, smallJaw, jaw, largeJaw, antler, horn, rhinoHorn, tentacle, tip, lip, thorn, boilingFissure, arsenicFissure, sulfurFissure, beeSting, sting, venomTooth, venomFang, screechingBeak, largeTail, hugeTail, armoredSkin, bark, eye3, eye6, eye8, vision6, vision12, vision16, nostril, ear3, ear6, ear8, rattleOrgan, insectMortality, sapientBrain, animalBrain, speedGland5, speedGland10, scentGland, boilingVent, arsenicVent, sulfurVent, bonusHP, braced, asleep, impressed :: ItemKind -- LH-specific tooth, lash, torsionRight, torsionLeft, pupil :: ItemKind symbolWand :: ContentSymbol ItemKind symbolWand = rsymbolWand $ ritemSymbols standardRules -- * No-cooldown melee damage organs without effects thorn = fist { isymbol = symbolWand , iname = "thorn" , ifreq = [(S_THORN, 1)] , icount = 2 + 1 `d` 2 -- unrealistic, but not boring , iverbHit = "puncture" , idamage = 2 `d` 1 , iaspects = [SetFlag Meleeable] -- not Durable , ieffects = [VerbNoLonger "be not so thorny any more" "."] , idesc = "Sharp yet brittle." } tip = fist { iname = "tip" , ifreq = [(S_TIP, 1)] , icount = 1 , iverbHit = "poke" , idamage = 2 `d` 1 , idesc = "" } fist = ItemKind { isymbol = toContentSymbol ',' , iname = "fist" , ifreq = [(S_FIST, 1)] , iflavour = zipPlain [Red] , icount = 2 , irarity = [(1, 1)] , iverbHit = "punch" , iweight = 2000 , idamage = 4 `d` 1 , iaspects = [SetFlag Durable, SetFlag Meleeable] , ieffects = [] , idesc = "Simple but effective." , ikit = [] } foot = fist { iname = "foot" , ifreq = [(S_FOOT, 1)] , iverbHit = "kick" , idamage = 4 `d` 1 , idesc = "A weapon you can still use if disarmed." -- great example of tutorial hints inside a flavourful text } smallClaw = fist { iname = "small claw" , ifreq = [(S_SMALL_CLAW, 1)] , iverbHit = "slash" , idamage = 2 `d` 1 , idesc = "A pearly spike." } snout = fist { iname = "snout" , ifreq = [(S_SNOUT, 1)] , icount = 1 , iverbHit = "bite" , idamage = 2 `d` 1 , idesc = "Sensitive and wide-nostrilled." } smallJaw = fist { iname = "small jaw" , ifreq = [(S_SMALL_JAW, 1)] , icount = 1 , iverbHit = "rip" , idamage = 3 `d` 1 , idesc = "Filled with small, even teeth." } -- * Cooldown melee damage organs without effects tentacle = fist -- two copies only { iname = "tentacle" , ifreq = [(S_TENTACLE, 1)] , iverbHit = "slap" , idamage = 4 `d` 1 , iaspects = Timeout 3 -- minimal timeout that lets other organs show : iaspects fist , idesc = "Damp and dextrous." } jaw = fist { iname = "jaw" , ifreq = [(S_JAW, 1)] , icount = 1 , iverbHit = "rip" , idamage = 5 `d` 1 , iaspects = Timeout (2 + 1 `d` 2) -- no effect, but limit raw damage : iaspects fist , idesc = "Delivers a powerful bite." } horn = fist { iname = "horn" , ifreq = [(S_HORN, 1)] , iverbHit = "impale" , idamage = 5 `d` 1 , iaspects = [ Timeout 7 -- no effect, but limit raw damage; two copies , AddSkill SkArmorMelee 10 ] -- bonus doubled ++ iaspects fist , idesc = "Sharp and long, for defence or attack." } largeJaw = fist { iname = "large jaw" , ifreq = [(S_LARGE_JAW, 1)] , icount = 1 , iverbHit = "crush" , idamage = 10 `d` 1 , iaspects = Timeout (2 + 1 `d` 2) -- no effect, but limit raw damage : iaspects fist , idesc = "Enough to swallow anything in a single gulp." } -- * Direct damage organs with effects beeSting = fist { isymbol = symbolWand , iname = "bee sting" , ifreq = [(S_BEE_STING, 1)] , icount = 1 , iverbHit = "sting" , idamage = 0 , iaspects = [ AddSkill SkArmorMelee 200, AddSkill SkArmorRanged 45 , SetFlag Meleeable ] -- not Durable , ieffects = [Paralyze 6, RefillHP 4] -- no special message when runs out, because it's 1 copy , idesc = "Painful, but beneficial." } sting = fist { isymbol = symbolWand , iname = "sting" , ifreq = [(S_STING, 1)] , icount = 1 , iverbHit = "inject" , idamage = 1 `d` 1 , iaspects = [Timeout $ 10 - 1 `dL` 4, AddSkill SkHurtMelee 40] ++ iaspects fist , ieffects = [toOrganBad S_RETAINING (3 + 1 `d` 3)] , idesc = "Painful, debilitating and harmful." } lip = fist { iname = "lip" , ifreq = [(S_LIP, 1)] , icount = 1 , iverbHit = "lap" , idamage = 1 `d` 1 , iaspects = Timeout (3 + 1 `d` 2) : iaspects fist , ieffects = [toOrganBad S_WEAKENED (2 + 1 `dL` 3)] , idesc = "" } venomTooth = fist { isymbol = symbolWand , iname = "venom tooth" , ifreq = [(S_VENOM_TOOTH, 1)] , iverbHit = "bite" , idamage = 1 `d` 1 , iaspects = Timeout (7 - 1 `dL` 3) : iaspects fist , ieffects = [toOrganBad S_SLOWED (3 + 1 `d` 3)] , idesc = "A chilling numbness spreads from its bite." } hookedClaw = fist { isymbol = symbolWand , iname = "hooked claw" , ifreq = [(S_HOOKED_CLAW, 1)] , icount = 2 -- even if more, only the fore claws used for fighting , iverbHit = "hook" , idamage = 2 `d` 1 , iaspects = Timeout (12 - 1 `dL` 3) : iaspects fist , ieffects = [toOrganBad S_SLOWED 2] , idesc = "A curved talon." } screechingBeak = fist { isymbol = symbolWand , iname = "screeching beak" , ifreq = [(S_SCREECHING_BEAK, 1)] , icount = 1 , iverbHit = "peck" , idamage = 3 `d` 1 , iaspects = Timeout (7 - 1 `dL` 3) : iaspects fist , ieffects = [Summon SCAVENGER $ 1 `dL` 3] , idesc = "Both a weapon and a beacon, calling more scavengers to the meal." } antler = fist { isymbol = symbolWand , iname = "antler" , ifreq = [(S_ANTLER, 1)] , iverbHit = "ram" , idamage = 4 `d` 1 , iaspects = [ Timeout $ 3 + (1 `d` 3) * 3 , AddSkill SkArmorMelee 10 ] -- bonus doubled ++ iaspects fist , ieffects = [PushActor (ThrowMod 100 50 1)] -- 1 step, slow , idesc = "" } rhinoHorn = fist { isymbol = symbolWand , iname = "ugly horn" -- made of keratin, unlike real horns , ifreq = [(S_RHINO_HORN, 1)] , icount = 1 -- single, unlike real horns , iverbHit = "gore" , idamage = 5 `d` 1 , iaspects = [Timeout 5, AddSkill SkHurtMelee 20] ++ iaspects fist , ieffects = [Impress, Yell] -- the owner is a mid-boss, after all , idesc = "Very solid, considering it has the same composition as fingernails." } largeTail = fist { isymbol = symbolWand , iname = "large tail" , ifreq = [(S_LARGE_TAIL, 1)] , icount = 1 , iverbHit = "knock" , idamage = 7 `d` 1 , iaspects = [Timeout $ 2 + 1 `d` 2, AddSkill SkHurtMelee 20] ++ iaspects fist -- timeout higher, lest they regain push before closing again , ieffects = [PushActor (ThrowMod 200 50 1)] -- 1 step, fast , idesc = "Almost as long as the trunk." } hugeTail = largeTail { isymbol = symbolWand , iname = "huge tail" , ifreq = [(S_HUGE_TAIL, 1)] , iverbHit = "upend" , iaspects = [Timeout $ 3 + 1 `d` 2, AddSkill SkHurtMelee 20] ++ iaspects fist -- timeout higher, lest they regain push before closing again , ieffects = [PushActor (ThrowMod 400 50 1)] -- 2 steps, fast , idesc = "Slow but immensely heavy." } -- * Melee weapons without direct damage venomFang = fist { isymbol = symbolWand , iname = "venom fang" , ifreq = [(S_VENOM_FANG, 1)] , iverbHit = "bite" , idamage = 0 , iaspects = Timeout (10 - 1 `dL` 5) : iaspects fist , ieffects = [toOrganNoTimer S_POISONED] , idesc = "Dripping with deadly venom." } -- * Special melee weapons sulfurFissure = boilingFissure { iname = "fissure" , ifreq = [(S_SULFUR_FISSURE, 1)] , icount = 2 + 1 `d` 2 , idamage = 0 -- heal not via (negative) idamage, for armour would block it , iaspects = SetFlag Benign : iaspects boilingFissure , ieffects = [ RefillHP 5 , VerbNoLonger "run out of the healing fumes" "."] , idesc = "" } boilingFissure = fist { isymbol = symbolWand , iname = "fissure" , ifreq = [(S_BOILING_FISSURE, 1)] , icount = 5 + 1 `d` 5 , iverbHit = "hiss at" , idamage = 1 `d` 1 , iaspects = [ AddSkill SkHurtMelee 20 -- decreasing as count decreases , SetFlag Meleeable ] -- not Durable , ieffects = [ DropItem 1 1 COrgan CONDITION -- useful; limited , VerbNoLonger "widen the crack, releasing pressure" "."] , idesc = "A deep crack to the underworld." } arsenicFissure = boilingFissure { iname = "fissure" , ifreq = [(S_ARSENIC_FISSURE, 1)] , icount = 3 + 1 `d` 3 , idamage = 2 `d` 1 , ieffects = [ toOrganBad S_PARSIMONIOUS (5 + 1 `d` 3) -- weaken/poison, impacting intellectual abilities first , VerbNoLonger "stop exuding stupefying vapours" "."] , idesc = "" } -- * Armor organs armoredSkin = ItemKind { isymbol = toContentSymbol ',' , iname = "armored skin" , ifreq = [(S_ARMORED_SKIN, 1)] , iflavour = zipPlain [Red] , icount = 1 , irarity = [(1, 1)] , iverbHit = "bash" , iweight = 2000 , idamage = 0 , iaspects = [ AddSkill SkArmorMelee 30, AddSkill SkArmorRanged 15 , SetFlag Durable ] , ieffects = [] , idesc = "Homemade armour is just as good." -- hmm, it may get confused with leather armor jackets, etc. , ikit = [] } bark = armoredSkin { iname = "bark" , ifreq = [(S_BARK, 1)] , idesc = "" } -- * Sense organs eye :: Int -> GroupName ItemKind -> ItemKind eye n grp = armoredSkin { iname = "eye" , ifreq = [(grp, 1)] , icount = 2 , iverbHit = "glare at" , iaspects = [ AddSkill SkSight (intToDice n) , SetFlag Durable ] , idesc = "A piercing stare." } eye3 = eye 3 S_EYE_3 eye6 = eye 6 S_EYE_6 eye8 = eye 8 S_EYE_8 vision :: Int -> GroupName ItemKind -> ItemKind vision n grp = armoredSkin { iname = "vision" , ifreq = [(grp, 1)] , iverbHit = "visualize" , iaspects = [ AddSkill SkSight (intToDice n) , SetFlag Durable ] , idesc = "" } vision6 = vision 6 S_VISION_6 vision12 = vision 12 S_VISION_12 vision16 = vision 16 S_VISION_16 nostril = armoredSkin { iname = "nostril" , ifreq = [(S_NOSTRIL, 1)] , icount = 2 , iverbHit = "snuff" , iaspects = [ AddSkill SkSmell 1 -- times 2, from icount , SetFlag Durable ] , idesc = "" } ear :: Int -> GroupName ItemKind -> ItemKind ear n grp = armoredSkin { iname = "ear" , ifreq = [(grp, 1)] , icount = 2 , iverbHit = "overhear" , iaspects = [ AddSkill SkHearing (intToDice n) , SetFlag Durable ] , idesc = "" } ear3 = ear 3 S_EAR_3 ear6 = ear 6 S_EAR_6 ear8 = ear 8 S_EAR_8 -- * Assorted rattleOrgan = armoredSkin { iname = "rattle" , ifreq = [(S_RATLLE, 1)] , iverbHit = "announce" , iaspects = [ Timeout $ 10 + (1 `d` 3) * 10 -- long, to limit spam , SetFlag Periodic, SetFlag Durable ] , ieffects = [Yell, RefillCalm 5] , idesc = "" } insectMortality = armoredSkin { iname = "insect mortality" , ifreq = [(S_INSECT_MORTALITY, 1)] , iverbHit = "age" , iaspects = [ AddSkill SkAggression 2 -- try to attack before you die , Timeout $ 30 + (1 `d` 3) * 10 -- die very slowly , SetFlag Periodic, SetFlag Durable ] , ieffects = [RefillHP (-1), Yell] , idesc = "" } sapientBrain = armoredSkin { iname = "sapient brain" , ifreq = [(S_SAPIENT_BRAIN, 1)] , iverbHit = "outbrain" , iaspects = [AddSkill sk 1 | sk <- [SkMove .. SkApply]] ++ [AddSkill SkMove 4] -- can move at once when waking up ++ [AddSkill SkAlter 4] -- can use all stairs; dig rubble, ice ++ [AddSkill SkWait 2] -- can brace and sleep ++ [AddSkill SkApply 1] -- can use most items, not just foods ++ [SetFlag Durable] , idesc = "" } animalBrain = armoredSkin { iname = "animal brain" , ifreq = [(S_ANIMAL_BRAIN, 1)] , iverbHit = "blank" , iaspects = [AddSkill sk 1 | sk <- [SkMove .. SkApply]] ++ [AddSkill SkMove 4] -- can move at once when waking up ++ [AddSkill SkAlter 2] -- can use normal stairs; can't dig ++ [AddSkill SkWait 2] -- can brace and sleep -- No @SkApply@ bonus, so can only apply foods. Note, however, -- that AI doesn't risk applying unIded items, so in early -- game animals won't eat anything. ++ [AddSkill SkDisplace (-1)] -- no melee tactics ++ [AddSkill SkMoveItem (-1)] -- no item gathering ++ [AddSkill SkProject (-1)] -- nor item flinging ++ [SetFlag Durable] , idesc = "" } speedGland :: Int -> GroupName ItemKind -> ItemKind speedGland n grp = armoredSkin { isymbol = symbolWand , iname = "speed gland" , ifreq = [(grp, 1)] , iverbHit = "spit at" , iaspects = [ Timeout $ intToDice (100 `div` n) , AddSkill SkSpeed $ intToDice n , SetFlag Periodic, SetFlag Durable ] , ieffects = [RefillHP 1] , idesc = "" } speedGland5 = speedGland 5 S_SPEED_GLAND_5 speedGland10 = speedGland 10 S_SPEED_GLAND_10 scentGland = armoredSkin { isymbol = symbolWand , iname = "scent gland" , ifreq = [(S_SCENT_GLAND, 1)] , icount = 10 + 1 `d` 3 -- runs out , iverbHit = "spray at" , iaspects = [ Timeout $ (1 `d` 3) * 10 , SetFlag Periodic, SetFlag Fragile ] -- not Durable , ieffects = [ VerbNoLonger "look spent" "." , ApplyPerfume , Explode S_DISTRESSING_ODOR ] -- keep explosion at the end to avoid the ambiguity of -- "of ([foo explosion] of [bar])" , idesc = "" } sulfurVent = armoredSkin { isymbol = toContentSymbol 'v' , iname = "vent" , ifreq = [(S_SULFUR_VENT, 1)] , iflavour = zipPlain [BrYellow] , iverbHit = "menace" , iaspects = [ Timeout $ (2 + 1 `d` 3) * 5 , SetFlag Periodic, SetFlag Durable ] , ieffects = [RefillHP 2, Explode S_DENSE_SHOWER] , idesc = "" } boilingVent = armoredSkin { isymbol = toContentSymbol 'v' , iname = "vent" , ifreq = [(S_BOILING_VENT, 1)] , iflavour = zipPlain [Blue] , iverbHit = "menace" , iaspects = [ Timeout $ (2 + 1 `d` 3) * 5 , SetFlag Periodic, SetFlag Durable ] , ieffects = [RefillHP 2, Explode S_BOILING_WATER] , idesc = "" } arsenicVent = armoredSkin { isymbol = toContentSymbol 'v' , iname = "vent" , ifreq = [(S_ARSENIC_VENT, 1)] , iflavour = zipPlain [Cyan] , iverbHit = "menace" , iaspects = [ Timeout $ (2 + 1 `d` 3) * 5 , SetFlag Periodic, SetFlag Durable ] , ieffects = [RefillHP 2, Explode S_SPARSE_SHOWER] , idesc = "" } -- * Special bonusHP = armoredSkin { isymbol = toContentSymbol 'H' -- '+' reserved for conditions , iname = "extra HP" , ifreq = [(S_BONUS_HP, 1)] , iflavour = zipPlain [BrBlue] , iverbHit = "intimidate" , iweight = 0 , iaspects = [AddSkill SkMaxHP 1] , idesc = "Growing up in a privileged background gave you the training and the discrete garment accessories that improve your posture and resilience." } braced = armoredSkin { isymbol = toContentSymbol 'B' , iname = "braced" , ifreq = [(S_BRACED, 1)] , iflavour = zipPlain [BrGreen] , iverbHit = "brace" , iweight = 0 , iaspects = [ AddSkill SkArmorMelee 50, AddSkill SkArmorRanged 25 , AddSkill SkHearing 10 , SetFlag Condition ] -- hack: display as condition , idesc = "Apart of increased resilience to attacks, being braced protects from displacement by foes and other forms of forced translocation, e.g., pushing or pulling." } asleep = armoredSkin { isymbol = toContentSymbol 'S' , iname = "asleep" , ifreq = [(S_ASLEEP, 1)] , iflavour = zipPlain [BrGreen] -- regenerates HP (very slowly) , icount = 5 , iverbHit = "slay" , iweight = 0 , iaspects = [AddSkill sk (-1) | sk <- [SkMove .. SkApply]] ++ [ AddSkill SkMelee 1, AddSkill SkAlter 1, AddSkill SkWait 1 , AddSkill SkSight (-3), AddSkill SkArmorMelee (-10) , SetFlag Condition ] -- hack: display as condition , idesc = "Sleep helps to regain health, albeit extremely slowly. Being asleep makes you vulnerable, with gradually diminishing effects as the slumber wears off over several turns. Any non-idle action, not only combat but even yawning or stretching removes a sizable portion of the sleepiness." } impressed = armoredSkin { isymbol = toContentSymbol 'I' , iname = "impressed" -- keep the same as in @ifreq@, to simplify code , ifreq = [(S_IMPRESSED, 1), (CONDITION, 1)] , iflavour = zipPlain [BrRed] , iverbHit = "confuse" , iweight = 0 , iaspects = [ AddSkill SkMaxCalm (-1) -- to help player notice on HUD -- and to count as bad condition , SetFlag Fragile -- to announce "no longer" only when -- all copies gone , SetFlag Condition ] -- this is really a condition, -- just not a timed condition , ieffects = [ OnSmash $ verbMsgLess "impressed" , OnSmash $ verbMsgNoLonger "impressed" ] -- not periodic, so no wear each turn, so only @OnSmash@ , idesc = "Being impressed by one's adversary sounds like fun, but on battlefield it equals treason. Almost. Throw in depleted battle calm and it leads to mindless desertion outright." } -- * LH-specific tooth = fist { iname = "tooth" , ifreq = [(S_TOOTH, 1)] , icount = 3 , iverbHit = "nail" , idamage = 2 `d` 1 , idesc = "" } lash = fist { iname = "lash" , ifreq = [(S_LASH, 1)] , icount = 1 , iverbHit = "lash" , idamage = 3 `d` 1 , idesc = "" } torsionRight = fist { iname = "right torsion" , ifreq = [(S_RIGHT_TORSION, 1)] , icount = 1 , iverbHit = "twist" , idamage = 13 `d` 1 , iaspects = [Timeout $ 5 + 1 `d` 5, AddSkill SkHurtMelee 20] ++ iaspects fist , ieffects = [toOrganBad S_SLOWED (3 + 1 `d` 3)] , idesc = "" } torsionLeft = fist { iname = "left torsion" , ifreq = [(S_LEFT_TORSION, 1)] , icount = 1 , iverbHit = "untwist" , idamage = 13 `d` 1 , iaspects = [Timeout $ 5 + 1 `d` 5, AddSkill SkHurtMelee 20] ++ iaspects fist , ieffects = [toOrganBad S_WEAKENED (3 + 1 `d` 3)] , idesc = "" } pupil = fist { iname = "pupil" , ifreq = [(S_PUPIL, 1)] , icount = 1 , iverbHit = "gaze at" , idamage = 1 `d` 1 , iaspects = [AddSkill SkSight 12, Timeout 12] ++ iaspects fist , ieffects = [DropItem 1 maxBound COrgan CONDITION, RefillCalm (-10)] -- can be useful for the player, but Calm drain is a risk , idesc = "" } LambdaHack-0.11.0.0/GameDefinition/Content/ItemKindTemporary.hs0000644000000000000000000002623307346545000022334 0ustar0000000000000000-- | Temporary pseudo-organ (condition) definitions. module Content.ItemKindTemporary ( -- * Group name patterns pattern S_IMMOBILE, pattern S_PACIFIED, pattern S_IRREPLACEABLE, pattern S_RETAINING, pattern S_IMPATIENT, pattern S_DISPOSSESSED, pattern S_WITHHOLDING, pattern S_PARSIMONIOUS , pattern S_MORE_MOBILE, pattern S_MORE_COMBATIVE, pattern S_MORE_DISPLACING, pattern S_MORE_MODIFYING, pattern S_MORE_PATIENT, pattern S_MORE_TIDY, pattern S_MORE_PROJECTING, pattern S_MORE_PRACTICAL , pattern S_STRENGTHENED, pattern S_WEAKENED, pattern S_PROTECTED_FROM_MELEE, pattern S_PROTECTED_FROM_RANGED, pattern S_DEFENSELESS, pattern S_RESOLUTE, pattern S_HASTED, pattern S_SLOWED, pattern S_FAR_SIGHTED, pattern S_BLIND, pattern S_KEEN_SMELLING, pattern S_FOUL_SMELLING, pattern S_ROSE_SMELLING, pattern S_RANGED_DEFLECTING, pattern S_MELEE_DEFLECTING, pattern S_SHINY_EYED, pattern S_DEAFENED, pattern S_DEAF, pattern S_DRUNK, pattern S_FRENZIED, pattern S_REGENERATING, pattern S_POISONED, pattern S_SLOW_RESISTANT, pattern S_POISON_RESISTANT , temporariesGNSingleton, noStatGN, bonusStatGN , -- * Content temporaries ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour -- * Group name patterns noStatGN :: [GroupName ItemKind] noStatGN = [S_IMMOBILE, S_PACIFIED, S_IRREPLACEABLE, S_RETAINING, S_IMPATIENT, S_DISPOSSESSED, S_WITHHOLDING, S_PARSIMONIOUS] bonusStatGN :: [GroupName ItemKind] bonusStatGN = [S_MORE_MOBILE, S_MORE_COMBATIVE, S_MORE_DISPLACING, S_MORE_MODIFYING, S_MORE_PATIENT, S_MORE_TIDY, S_MORE_PROJECTING, S_MORE_PRACTICAL] temporariesGNSingleton :: [GroupName ItemKind] temporariesGNSingleton = [S_STRENGTHENED, S_WEAKENED, S_PROTECTED_FROM_MELEE, S_PROTECTED_FROM_RANGED, S_DEFENSELESS, S_RESOLUTE, S_HASTED, S_SLOWED, S_FAR_SIGHTED, S_BLIND, S_KEEN_SMELLING, S_FOUL_SMELLING, S_ROSE_SMELLING, S_RANGED_DEFLECTING, S_MELEE_DEFLECTING, S_SHINY_EYED, S_DEAFENED, S_DEAF, S_DRUNK, S_FRENZIED, S_REGENERATING, S_POISONED, S_SLOW_RESISTANT, S_POISON_RESISTANT] ++ noStatGN ++ bonusStatGN pattern S_IMMOBILE, S_PACIFIED, S_IRREPLACEABLE, S_RETAINING, S_IMPATIENT, S_DISPOSSESSED, S_WITHHOLDING, S_PARSIMONIOUS :: GroupName ItemKind pattern S_MORE_MOBILE, S_MORE_COMBATIVE, S_MORE_DISPLACING, S_MORE_MODIFYING, S_MORE_PATIENT, S_MORE_TIDY, S_MORE_PROJECTING, S_MORE_PRACTICAL :: GroupName ItemKind pattern S_STRENGTHENED, S_WEAKENED, S_PROTECTED_FROM_MELEE, S_PROTECTED_FROM_RANGED, S_DEFENSELESS, S_RESOLUTE, S_HASTED, S_SLOWED, S_FAR_SIGHTED, S_BLIND, S_KEEN_SMELLING, S_FOUL_SMELLING, S_ROSE_SMELLING, S_RANGED_DEFLECTING, S_MELEE_DEFLECTING, S_SHINY_EYED, S_DEAFENED, S_DEAF, S_DRUNK, S_FRENZIED, S_REGENERATING, S_POISONED, S_SLOW_RESISTANT, S_POISON_RESISTANT :: GroupName ItemKind pattern S_STRENGTHENED = GroupName "strengthened" pattern S_WEAKENED = GroupName "weakened" pattern S_PROTECTED_FROM_MELEE = GroupName "protected from melee" pattern S_PROTECTED_FROM_RANGED = GroupName "protected from ranged" pattern S_DEFENSELESS = GroupName "defenseless" pattern S_RESOLUTE = GroupName "resolute" pattern S_HASTED = GroupName "hasted" pattern S_SLOWED = GroupName "slowed" pattern S_FAR_SIGHTED = GroupName "far-sighted" pattern S_BLIND = GroupName "blind" pattern S_KEEN_SMELLING = GroupName "keen-smelling" pattern S_FOUL_SMELLING = GroupName "foul-smelling" pattern S_ROSE_SMELLING = GroupName "rose-smelling" pattern S_RANGED_DEFLECTING = GroupName "ranged-deflecting" pattern S_MELEE_DEFLECTING = GroupName "melee-deflecting" pattern S_SHINY_EYED = GroupName "shiny-eyed" pattern S_DEAFENED = GroupName "deafened" pattern S_DEAF = GroupName "deaf" pattern S_DRUNK = GroupName "drunk" pattern S_FRENZIED = GroupName "frenzied" pattern S_REGENERATING = GroupName "regenerating" pattern S_POISONED = GroupName "poisoned" pattern S_SLOW_RESISTANT = GroupName "slow resistant" pattern S_POISON_RESISTANT = GroupName "poison resistant" pattern S_IMMOBILE = GroupName "immobile" pattern S_PACIFIED = GroupName "pacified" pattern S_IRREPLACEABLE = GroupName "irreplaceable" pattern S_RETAINING = GroupName "retaining" pattern S_IMPATIENT = GroupName "impatient" pattern S_DISPOSSESSED = GroupName "dispossessed" pattern S_WITHHOLDING = GroupName "withholding" pattern S_PARSIMONIOUS = GroupName "parsimonious" pattern S_MORE_MOBILE = GroupName "super-mobile" pattern S_MORE_COMBATIVE = GroupName "super-combative" pattern S_MORE_DISPLACING = GroupName "super-displacing" pattern S_MORE_MODIFYING = GroupName "super-modifying" pattern S_MORE_PATIENT = GroupName "super-patient" pattern S_MORE_TIDY = GroupName "super-tidy" pattern S_MORE_PROJECTING = GroupName "super-projecting" pattern S_MORE_PRACTICAL = GroupName "super-practical" -- * Content temporaries :: [ItemKind] temporaries = [tmpStrengthened, tmpWeakened, tmpProtectedMelee, tmpProtectedRanged, tmpDefenseless, tmpResolute, tmpFast20, tmpSlow10, tmpFarSighted, tmpBlind, tmpKeenSmelling, tmpFoulSmelling, tmpRoseSmelling, tmpRangedDeflecting, tmpMeleeDeflecting, tmpNoctovision, tmpDeafened, tmpDeaf, tmpDrunk, tmpBonusSkAggresion, tmpRegenerating, tmpPoisoned, tmpSlow10Resistant, tmpPoisonResistant, tmpNoSkMove, tmpNoSkMelee, tmpNoSkDisplace, tmpNoSkAlter, tmpNoSkWait, tmpNoSkMoveItem, tmpNoSkProject, tmpNoSkApply, tmpBonusSkMove, tmpBonusSkMelee, tmpBonusSkDisplace, tmpBonusSkAlter, tmpBonusSkWait, tmpBonusSkMoveItem, tmpBonusSkProject, tmpBonusSkApply] tmpStrengthened, tmpWeakened, tmpProtectedMelee, tmpProtectedRanged, tmpDefenseless, tmpResolute, tmpFast20, tmpSlow10, tmpFarSighted, tmpBlind, tmpKeenSmelling, tmpFoulSmelling, tmpRoseSmelling, tmpRangedDeflecting, tmpMeleeDeflecting, tmpNoctovision, tmpDeafened, tmpDeaf, tmpDrunk, tmpBonusSkAggresion, tmpRegenerating, tmpPoisoned, tmpSlow10Resistant, tmpPoisonResistant, tmpNoSkMove, tmpNoSkMelee, tmpNoSkDisplace, tmpNoSkAlter, tmpNoSkWait, tmpNoSkMoveItem, tmpNoSkProject, tmpNoSkApply, tmpBonusSkMove, tmpBonusSkMelee, tmpBonusSkDisplace, tmpBonusSkAlter, tmpBonusSkWait, tmpBonusSkMoveItem, tmpBonusSkProject, tmpBonusSkApply :: ItemKind -- The @name@ is be used in item description, so it should be an adjective -- describing the temporary set of aspects. -- The messages are needed also under @OnSmash@ to display when item removed -- via @DropItem@ and not via natural periodic activation. tmpAspects :: GroupName ItemKind -> [Aspect] -> ItemKind tmpAspects grp aspects = let name = fromGroupName grp -- @iname@ must match @ifreq@, see @myBadGrps@ in ItemKind { isymbol = toContentSymbol '+' , iname = name , ifreq = [(grp, 1), (CONDITION, 1)] , iflavour = zipPlain [BrWhite] , icount = 1 , irarity = [(1, 1)] , iverbHit = "affect" , iweight = 0 , idamage = 0 , iaspects = -- timeout is 0; activates and vanishes soon, -- depending on initial timer setting aspects ++ [SetFlag Periodic, SetFlag Fragile, SetFlag Condition] , ieffects = [ OnSmash $ verbMsgLess name -- announce partial neutralization, but don't spam -- about normal periodic wear each turn , OnSmash $ verbMsgNoLonger name -- for forced neutralization , verbMsgNoLonger name ] -- for periodic wear of last copy , idesc = "" -- no description needed; powers are enough , ikit = [] } tmpEffects :: GroupName ItemKind -> Dice -> [Effect] -> ItemKind tmpEffects grp icount effects = let tmp = tmpAspects grp [] in tmp { icount , ieffects = effects ++ ieffects tmp } tmpStrengthened = tmpAspects S_STRENGTHENED [AddSkill SkHurtMelee 20] tmpWeakened = tmpAspects S_WEAKENED [AddSkill SkHurtMelee (-30)] -- don't cancel out ^ tmpProtectedMelee = tmpAspects S_PROTECTED_FROM_MELEE [AddSkill SkArmorMelee 50] tmpProtectedRanged = tmpAspects S_PROTECTED_FROM_RANGED [AddSkill SkArmorRanged 25] tmpDefenseless = tmpAspects S_DEFENSELESS [ AddSkill SkArmorMelee (-50) , AddSkill SkArmorRanged (-25) ] tmpResolute = tmpAspects S_RESOLUTE [ AddSkill SkMaxCalm 60 , AddSkill SkHearing 10 ] tmpFast20 = tmpAspects S_HASTED [AddSkill SkSpeed 20] tmpSlow10 = tmpAspects S_SLOWED [AddSkill SkSpeed (-10)] tmpFarSighted = tmpAspects S_FAR_SIGHTED [AddSkill SkSight 5] tmpBlind = tmpAspects S_BLIND [ AddSkill SkSight (-99) , AddSkill SkArmorMelee (-30) , AddSkill SkHearing 10 ] tmpKeenSmelling = tmpAspects S_KEEN_SMELLING [AddSkill SkSmell 2] tmpFoulSmelling = tmpAspects S_FOUL_SMELLING [AddSkill SkOdor 2] tmpRoseSmelling = tmpAspects S_ROSE_SMELLING [AddSkill SkOdor (-4)] tmpRangedDeflecting = tmpAspects S_RANGED_DEFLECTING [AddSkill SkDeflectRanged 1] tmpMeleeDeflecting = tmpAspects S_MELEE_DEFLECTING [AddSkill SkDeflectMelee 1] tmpNoctovision = tmpAspects S_SHINY_EYED [AddSkill SkNocto 2] tmpDeafened = tmpAspects S_DEAFENED [AddSkill SkHearing (-6)] tmpDeaf = tmpAspects S_DEAF [ AddSkill SkHearing (-99) , AddSkill SkArmorMelee (-30) ] tmpDrunk = tmpAspects S_DRUNK [ AddSkill SkHurtMelee 30 -- fury , AddSkill SkArmorRanged (-30) , AddSkill SkSight (-8) ] tmpBonusSkAggresion = tmpAspects S_FRENZIED [ AddSkill SkAggression 5 , AddSkill SkArmorMelee (-30) ] tmpRegenerating = tmpEffects S_REGENERATING (4 + 1 `d` 2) [RefillHP 1] tmpPoisoned = tmpEffects S_POISONED (3 + 1 `d` 2) [RefillHP (-1)] tmpSlow10Resistant = tmpEffects S_SLOW_RESISTANT (8 + 1 `d` 4) [DropItem 1 1 COrgan S_SLOWED] tmpPoisonResistant = tmpEffects S_POISON_RESISTANT (8 + 1 `d` 4) [DropItem 1 maxBound COrgan S_POISONED] tmpNoSkMove = tmpAspects S_IMMOBILE [AddSkill SkMove (-99)] tmpNoSkMelee = tmpAspects S_PACIFIED [AddSkill SkMelee (-99)] tmpNoSkDisplace = tmpAspects S_IRREPLACEABLE [AddSkill SkDisplace (-99)] tmpNoSkAlter = tmpAspects S_RETAINING [AddSkill SkAlter (-99)] tmpNoSkWait = tmpAspects S_IMPATIENT [AddSkill SkWait (-99)] tmpNoSkMoveItem = tmpAspects S_DISPOSSESSED [AddSkill SkMoveItem (-99)] tmpNoSkProject = tmpAspects S_WITHHOLDING [AddSkill SkProject (-99)] tmpNoSkApply = tmpAspects S_PARSIMONIOUS [AddSkill SkApply (-99)] tmpBonusSkMove = tmpAspects S_MORE_MOBILE [AddSkill SkMove 5] tmpBonusSkMelee = tmpAspects S_MORE_COMBATIVE [AddSkill SkMelee 5] tmpBonusSkDisplace = tmpAspects S_MORE_DISPLACING [AddSkill SkDisplace 5] tmpBonusSkAlter = tmpAspects S_MORE_MODIFYING [AddSkill SkAlter 5] tmpBonusSkWait = tmpAspects S_MORE_PATIENT [AddSkill SkWait 5] tmpBonusSkMoveItem = tmpAspects S_MORE_TIDY [AddSkill SkMoveItem 5] tmpBonusSkProject = tmpAspects S_MORE_PROJECTING [AddSkill SkProject 8] -- TODO: 11, but let player control potion throwing by non-pointmen; -- beware also of capReinforced and other sources of the skill tmpBonusSkApply = tmpAspects S_MORE_PRACTICAL [AddSkill SkApply 5] LambdaHack-0.11.0.0/GameDefinition/Content/ModeKind.hs0000644000000000000000000010165407346545000020420 0ustar0000000000000000-- | Definitions of game mode kinds. module Content.ModeKind ( -- * Group name patterns groupNamesSingleton, groupNames , -- * Content content #ifdef EXPOSE_INTERNAL -- * Group name patterns , pattern RAID, pattern BRAWL, pattern LONG, pattern CRAWL, pattern FOGGY, pattern SHOOTOUT, pattern PERILOUS, pattern HUNT, pattern NIGHT, pattern FLIGHT, pattern BURNING, pattern ZOO, pattern RANGED, pattern AMBUSH, pattern SAFARI, pattern DIG, pattern SEE, pattern SHORT, pattern CRAWL_EMPTY, pattern CRAWL_SURVIVAL, pattern SAFARI_SURVIVAL, pattern BATTLE, pattern BATTLE_DEFENSE, pattern BATTLE_SURVIVAL, pattern DEFENSE, pattern DEFENSE_EMPTY #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Text as T import Game.LambdaHack.Content.CaveKind (CaveKind, pattern DEFAULT_RANDOM) import Game.LambdaHack.Content.FactionKind (Outcome (..)) import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Core.Dice import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Content.CaveKind hiding (content, groupNames, groupNamesSingleton) import Content.FactionKind hiding (content, groupNames, groupNamesSingleton) import Content.ItemKindActor -- * Group name patterns groupNamesSingleton :: [GroupName ModeKind] groupNamesSingleton = [RAID, BRAWL, LONG, CRAWL, FOGGY, SHOOTOUT, PERILOUS, HUNT, NIGHT, FLIGHT, BURNING, ZOO, RANGED, AMBUSH, SAFARI, DIG, SEE, SHORT, CRAWL_EMPTY, CRAWL_SURVIVAL, SAFARI_SURVIVAL, BATTLE, BATTLE_DEFENSE, BATTLE_SURVIVAL, DEFENSE, DEFENSE_EMPTY] pattern RAID, BRAWL, LONG, CRAWL, FOGGY, SHOOTOUT, PERILOUS, HUNT, NIGHT, FLIGHT, BURNING, ZOO, RANGED, AMBUSH, SAFARI, DIG, SEE, SHORT, CRAWL_EMPTY, CRAWL_SURVIVAL, SAFARI_SURVIVAL, BATTLE, BATTLE_DEFENSE, BATTLE_SURVIVAL, DEFENSE, DEFENSE_EMPTY :: GroupName ModeKind groupNames :: [GroupName ModeKind] groupNames = [] pattern RAID = GroupName "raid" pattern BRAWL = GroupName "brawl" pattern LONG = GroupName "long crawl" pattern CRAWL = GroupName "crawl" pattern FOGGY = GroupName "foggy shootout" pattern SHOOTOUT = GroupName "shootout" pattern PERILOUS = GroupName "perilous hunt" pattern HUNT = GroupName "hunt" pattern NIGHT = GroupName "night flight" pattern FLIGHT = GroupName "flight" pattern BURNING = GroupName "burning zoo" pattern ZOO = GroupName "zoo" pattern RANGED = GroupName "ranged ambush" pattern AMBUSH = GroupName "ambush" pattern SAFARI = GroupName "safari" pattern DIG = GroupName "dig" pattern SEE = GroupName "see" pattern SHORT = GroupName "short" pattern CRAWL_EMPTY = GroupName "crawlEmpty" -- only the first word matters pattern CRAWL_SURVIVAL = GroupName "crawlSurvival" pattern SAFARI_SURVIVAL = GroupName "safariSurvival" pattern BATTLE = GroupName "battle" pattern BATTLE_DEFENSE = GroupName "battleDefense" pattern BATTLE_SURVIVAL = GroupName "battleSurvival" pattern DEFENSE = GroupName "defense" pattern DEFENSE_EMPTY = GroupName "defenseEmpty" -- * Content content :: [ModeKind] content = [raid, brawl, crawl, shootout, hunt, flight, zoo, ambush, safari, dig, see, short, crawlEmpty, crawlSurvival, safariSurvival, battle, battleDefense, battleSurvival, defense, defenseEmpty, screensaverRaid, screensaverBrawl, screensaverCrawl, screensaverShootout, screensaverHunt, screensaverFlight, screensaverZoo, screensaverAmbush, screensaverSafari] raid, brawl, crawl, shootout, hunt, flight, zoo, ambush, safari, dig, see, short, crawlEmpty, crawlSurvival, safariSurvival, battle, battleDefense, battleSurvival, defense, defenseEmpty, screensaverRaid, screensaverBrawl, screensaverCrawl, screensaverShootout, screensaverHunt, screensaverFlight, screensaverZoo, screensaverAmbush, screensaverSafari :: ModeKind -- What other symmetric (two only-one-moves factions) and asymmetric vs crowd -- scenarios make sense (e.g., are good for a tutorial or for standalone -- extreme fun or are impossible as part of a crawl)? -- sparse melee at night: no, shade ambush in brawl is enough -- dense melee: no, keeping big party together is a chore and big enemy -- party is less fun than huge enemy party -- crowd melee in daylight: no, possible in crawl and at night is more fun -- sparse ranged at night: no, less fun than dense and if no reaction fire, -- just a camp fest or firing blindly -- dense ranged in daylight: no, less fun than at night with flares -- crowd ranged: no, fish in a barrel, less predictable and more fun inside -- crawl, even without reaction fire raid = ModeKind { mname = "raid (tutorial, 1)" , mfreq = [(RAID, 1), (CAMPAIGN_SCENARIO, 1)] , mtutorial = True , mattract = False , mroster = rosterRaid , mcaves = cavesRaid , mendMsg = [ (Killed, "This expedition has gone wrong. However, scientific mind does not despair, but analyzes and corrects. Did you perchance awake one animal too many? Did you remember to try using all consumables at your disposal for your immediate survival? Did you choose a challenge with difficulty level within your means? Answer honestly, ponder wisely, experiment methodically.") , (Defeated, "Regrettably, the other team snatched the grant, while you were busy contemplating natural phenomena. Science is a competitive sport, as sad as it sounds. It's not enough to make a discovery, you have to get there first.") , (Escape, "You've got hold of the machine! Think of the hours of fun taking it apart and putting it back together again! That's a great first step on your quest to solve the typing problems of the world.") ] , mrules = T.intercalate "\n" [ "* One level only" , "* Two heroes vs. Spawned enemies" , "* Gather gold" , "* Find a way out and escape ASAP" ] , mdesc = "An incredibly advanced typing machine worth 100 gold is buried at the exit of this maze. Be the first to find it and fund a research team that makes typing accurate and dependable forever." , mreason = "In addition to initiating the (loose) game plot, this adventure provides an introductory tutorial. Relax, explore, gather loot, find the way out and escape. With some luck, you won't even need to fight anything." , mhint = "You can't use gathered items in your next encounters, so trigger any consumables at will. Feel free to scout with only one of the heroes and keep the other one immobile, e.g., standing guard over the squad's shared inventory stash. If in grave danger, retreat with the scout to join forces with the guard. The more gold collected and the faster the victory, the higher your score in this encounter." } brawl = ModeKind -- sparse melee in daylight, with shade for melee ambush { mname = "brawl (tutorial, 2)" , mfreq = [(BRAWL, 1), (CAMPAIGN_SCENARIO, 1)] , mtutorial = True , mattract = False , mroster = rosterBrawl , mcaves = cavesBrawl , mendMsg = [ (Killed, "The inquisitive scholars turned out to be envious of our deep insight to the point of outright violence. It would still not result in such a defeat and recanting of our thesis if we figured out to use terrain to protect us from missiles or even completely hide our presence. It would also help if we honourably kept our ground together to the end, at the same time preventing the overwhelming enemy forces from brutishly ganging up on our modest-sized, though valiant, research team.") , (Conquer, "That's settled: local compactness *is* necessary for relative completeness, given the assumptions.") ] , mrules = T.intercalate "\n" [ "* One level only" , "* Three heroes vs. Three human enemies" , "* Minimize losses" , "* Incapacitate all enemies ASAP" ] , 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." } crawl = ModeKind { mname = "long crawl (main)" , mfreq = [(LONG, 1), (CRAWL, 1), (CAMPAIGN_SCENARIO, 1)] , mtutorial = False , 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?") , (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" , "* Three heroes vs. Spawned enemies" , "* Gather gold, gems and elixirs" , "* 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." , 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." } -- The trajectory tip is important because of tactics of scout looking from -- behind a bush and others hiding in mist. If no suitable bushes, -- fire once and flee into mist or behind cover. Then whomever is out of LOS -- range or inside mist can shoot at the last seen enemy locations, -- adjusting aim according to sounds and incoming missile trajectories. -- If the scout can't find bushes or glass building to set a lookout, -- the other team members are more spotters and guardians than snipers -- and that's their only role, so a small party makes sense. shootout = ModeKind -- sparse ranged in daylight { mname = "foggy shootout (3)" , mfreq = [(FOGGY, 1), (SHOOTOUT, 1), (CAMPAIGN_SCENARIO, 1)] , mtutorial = False , 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.") , (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" , "* Three heroes vs. Three human enemies" , "* Minimize losses" , "* Incapacitate all enemies ASAP" ] , mdesc = "Whose arguments are most striking and whose ideas fly fastest? Let's scatter up, attack the problems from different angles and find out." , 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." } hunt = ModeKind -- melee vs ranged with reaction fire in daylight { mname = "perilous hunt (4)" , mfreq = [(PERILOUS, 1), (HUNT, 1), (CAMPAIGN_SCENARIO, 1)] , mtutorial = False , mattract = False , mroster = rosterHunt , mcaves = cavesHunt , mendMsg = [ (Killed, "Leaving concealment might have not been rational enough, leaving cover is hard to justify on a scientific basis and wandering off on an area of a heated dispute is foolhardy. All this is doubly regrettable, given that our cold-hearted opponents supported their weak arguments with inexplicably effective telegraphy and triangulation equipment. And we so deserve a complete intellectual victory, if only we strove to lower the difficulty of this altercation instead of raising it.") -- this is in the middle of the scenario list and the mission is not tricky, so a subtle reminder about lowering difficulty, in case the player struggles , (Conquer, "We chased them off and proved our argument, like we knew that we would. It feels efficient to stick together and prevail. We taught them a lesson in rationality, despite their superior scientific equipment. Scientific truth prevails over brute force.") ] , mrules = T.intercalate "\n" [ "* One level only" , "* Seven heroes vs. Seven human enemies capable of concurrent attacks" , "* Minimize losses" , "* Incapacitate all human enemies ASAP" ] , mdesc = "Who is the hunter and who is the prey? The only criterion is last man standing when the chase for truth ends." , mreason = "This adventure is quite a tactical challenge, because enemies are allowed to fling their ammo simultaneously at your team, which has no such ability." , mhint = "Try not to outshoot the enemy, but to instead focus more on melee tactics. A useful concept here is communication overhead. Any team member that is not waiting and spotting for everybody, but acts, e.g., melees or moves or manages items, slows down all other team members by roughly 10%, because they need to keep track of his actions. Therefore, if other heroes melee, consider carefully if it makes sense to come to their aid, slowing them while you move, or if it's better to stay put and monitor the perimeter. This is true for all factions and all actors on each level separately, except the pointman of each faction, if it has one." -- this also eliminates lag in big battles and helps the player to focus on combat and not get distracted by distant team members frantically trying to reach the battleground in time } flight = ModeKind -- asymmetric ranged and stealth race at night { mname = "night flight (5)" , mfreq = [(NIGHT, 1), (FLIGHT, 1), (CAMPAIGN_SCENARIO, 1)] , mtutorial = False , 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.") , (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" , "* Three heroes vs. Seven human enemies capable of concurrent attacks" , "* Minimize losses" , "* Gather gems" , "* Find a way out and escape ASAP" ] , mdesc = "Dwelling into dark matters is dangerous, so avoid the crowd of firebrand disputants, catch any gems of thought, find a way out and bring back a larger team to shed new light on the field." , mreason = "The focus of this installment is on stealthy exploration under the threat of numerically superior enemy." , mhint = "" } zoo = ModeKind -- asymmetric crowd melee at night { mname = "burning zoo (6)" , mfreq = [(BURNING, 1), (ZOO, 1), (CAMPAIGN_SCENARIO, 1)] , mtutorial = False , mattract = False , mroster = rosterZoo , mcaves = cavesZoo , mendMsg = [ (Killed, "Against such an onslaught, only clever positioning, use of terrain and patient vigilance gives any chance of survival.") , (Conquer, "That was a grim harvest. Science demands sacrifices.") ] , mrules = T.intercalate "\n" [ "* One level only" , "* Five heroes vs. Many enemies" , "* Minimize losses" , "* Incapacitate all enemies ASAP" ] , mdesc = "The heat of the dispute reaches the nearby Wonders of Science and Nature exhibition, igniting greenery, nets and cages. Crazed animals must be dissuaded from ruining precious scientific equipment and setting back the otherwise fruitful exchange of ideas." , mreason = "This is a crowd control exercise, at night, with a raging fire." , mhint = "Note that communication overhead, as explained in perilous hunt adventure hints, makes it impossible for any faction to hit your heroes by more than 10 normal speed actors each turn. However, this is still too much, so position is everything." } -- The tactic is to sneak in the dark, highlight enemy with thrown torches -- (and douse thrown enemy torches with blankets) and only if this fails, -- actually scout using extended noctovision. -- With reaction fire, larger team is more fun. -- -- For now, while we have no shooters with timeout, massive ranged battles -- without reaction fire don't make sense, because then usually only one hero -- shoots (and often also scouts) and others just gather ammo. ambush = ModeKind -- dense ranged with reaction fire vs melee at night { mname = "ranged ambush (7)" , mfreq = [(RANGED, 1), (AMBUSH, 1), (CAMPAIGN_SCENARIO, 1)] , mtutorial = False , 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.") , (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" , "* Three heroes with concurrent attacks vs. Unidentified foes" , "* Minimize losses" , "* Assert control of the situation ASAP" ] , mdesc = "Prevent hijacking of your ideas at all cost! Be stealthy, be observant, be aggressive. Fast execution is what makes or breaks a creative team." , 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!" } safari = ModeKind -- Easter egg available only via screensaver { mname = "safari" , mfreq = [(SAFARI, 1)] , mtutorial = False , mattract = False , mroster = rosterSafari , mcaves = cavesSafari , mendMsg = [] , mrules = T.intercalate "\n" [ "* Three levels" , "* Many teammates capable of concurrent action vs. Many enemies" , "* Minimize losses" , "* Find a way out and escape ASAP" ] , mdesc = "\"In this enactment you'll discover the joys of hunting the most exquisite of Earth's flora and fauna, both animal and semi-intelligent. Exit at the bottommost level.\" This is a drama script recovered from a monster nest debris." , mreason = "This is an Easter egg. The default squad doctrine is that all team members follow the pointman, but it can be changed from the settings submenu of the main menu." , mhint = "" } -- * Testing modes dig = ModeKind { mname = "dig" , mfreq = [(DIG, 1)] , mtutorial = False , mattract = False , mroster = rosterCrawlEmpty , mcaves = cavesDig , mendMsg = [] , mrules = "" , mdesc = "Delve deeper!" , mreason = "" , mhint = "" } see = ModeKind { mname = "see" , mfreq = [(SEE, 1)] , mtutorial = False , mattract = False , mroster = rosterCrawlEmpty , mcaves = cavesSee , mendMsg = [] , mrules = "" , mdesc = "See all!" , mreason = "" , mhint = "" } short = ModeKind { mname = "short" , mfreq = [(SHORT, 1)] , mtutorial = False , mattract = False , mroster = rosterCrawlEmpty , mcaves = cavesShort , mendMsg = [] , mrules = "" , mdesc = "See all short scenarios!" , mreason = "" , mhint = "" } crawlEmpty = ModeKind { mname = "crawl empty" , mfreq = [(CRAWL_EMPTY, 1)] , mtutorial = False , mattract = False , mroster = rosterCrawlEmpty , mcaves = cavesCrawlEmpty , mendMsg = [] , mrules = "" , mdesc = "Enjoy the extra legroom." , mreason = "" , mhint = "" } crawlSurvival = ModeKind { mname = "crawl survival" , mfreq = [(CRAWL_SURVIVAL, 1)] , mtutorial = False , mattract = False , mroster = rosterCrawlSurvival , mcaves = cavesCrawl , mendMsg = [] , mrules = "" , mdesc = "Lure the human intruders deeper and deeper." , mreason = "" , mhint = "" } safariSurvival = ModeKind { mname = "safari survival" , mfreq = [(SAFARI_SURVIVAL, 1)] , mtutorial = False , mattract = False , mroster = rosterSafariSurvival , mcaves = cavesSafari , mendMsg = [] , mrules = "" , mdesc = "In this enactment you'll discover the joys of being hunted among the most exquisite of Earth's flora and fauna, both animal and semi-intelligent." , mreason = "" , mhint = "" } battle = ModeKind { mname = "battle" , mfreq = [(BATTLE, 1)] , mtutorial = False , mattract = False , mroster = rosterBattle , mcaves = cavesBattle , mendMsg = [] , mrules = "" , mdesc = "Odds are stacked against those that unleash the horrors of abstraction." , mreason = "" , mhint = "" } battleDefense = ModeKind { mname = "battle defense" , mfreq = [(BATTLE_DEFENSE, 1)] , mtutorial = False , mattract = False , mroster = rosterBattleDefense , mcaves = cavesBattle , mendMsg = [] , mrules = "" , mdesc = "Odds are stacked for those that breathe mathematics." , mreason = "" , mhint = "" } battleSurvival = ModeKind { mname = "battle survival" , mfreq = [(BATTLE_SURVIVAL, 1)] , mtutorial = False , mattract = False , mroster = rosterBattleSurvival , mcaves = cavesBattle , mendMsg = [] , mrules = "" , mdesc = "Odds are stacked for those that ally with the strongest." , mreason = "" , mhint = "" } defense = ModeKind -- perhaps a real scenario in the future { mname = "defense" , mfreq = [(DEFENSE, 1)] , mtutorial = False , mattract = False , mroster = rosterDefense , mcaves = cavesCrawl , mendMsg = [] , mrules = "" , mdesc = "Don't let human interlopers defile your abstract secrets and flee unpunished!" , mreason = "This is an initial sketch of the reversed crawl game mode. Play on high difficulty to avoid guaranteed victories against the pitiful humans." , mhint = "" } defenseEmpty = ModeKind { mname = "defense empty" , mfreq = [(DEFENSE_EMPTY, 1)] , mtutorial = False , mattract = False , mroster = rosterDefenseEmpty , mcaves = cavesCrawlEmpty , mendMsg = [] , mrules = "" , mdesc = "Lord over empty halls." , mreason = "" , mhint = "" } -- * Screensaver modes screensaverRaid = raid { mname = "auto-raid (1)" , mfreq = [(INSERT_COIN, 2)] , mattract = True } screensaverBrawl = brawl { mname = "auto-brawl (2)" , mfreq = [] , mattract = True } screensaverCrawl = crawl { mname = "auto-crawl (long)" , mfreq = [] , mattract = True } screensaverShootout = shootout { mname = "auto-shootout (3)" , mfreq = [(INSERT_COIN, 2)] , mattract = True } screensaverHunt = hunt { mname = "auto-hunt (4)" , mfreq = [(INSERT_COIN, 2)] , mattract = True } screensaverFlight = flight { mname = "auto-flight (5)" , mfreq = [(INSERT_COIN, 2)] , mattract = True } screensaverZoo = zoo { mname = "auto-zoo (6)" , mfreq = [] , mattract = True } screensaverAmbush = ambush { mname = "auto-ambush (7)" , mfreq = [] , mattract = True } screensaverSafari = safari { mname = "auto-safari" , mfreq = [(INSERT_COIN, 1)] , mattract = True } rosterRaid, rosterBrawl, rosterCrawl, rosterShootout, rosterHunt, rosterFlight, rosterZoo, rosterAmbush, rosterSafari, rosterCrawlEmpty, rosterCrawlSurvival, rosterSafariSurvival, rosterBattle, rosterBattleDefense, rosterBattleSurvival, rosterDefense, rosterDefenseEmpty :: Roster rosterRaid = [ ( ANIMAL_REPRESENTATIVE -- starting over escape , [(-2, 2, ANIMAL)] ) , ( EXPLORER_SHORT , [(-2, 2, HERO)] ) , ( COMPETITOR_SHORT , [(-2, 1, HERO)] ) , (HORROR_REPRESENTATIVE, []) ] -- for summoned monsters rosterBrawl = [ ( EXPLORER_NO_ESCAPE , [(-2, 3, BRAWLER_HERO)] ) , ( COMPETITOR_NO_ESCAPE , [(-2, 3, BRAWLER_HERO)] ) , (HORROR_REPRESENTATIVE, []) ] rosterCrawl = [ ( ANIMAL_REPRESENTATIVE -- starting over escape , -- Fun from the start to avoid empty initial level: [ (-1, 1 + 1 `d` 2, ANIMAL) -- Huge battle at the end: , (-10, 100, MOBILE_ANIMAL) ] ) , ( EXPLORER_REPRESENTATIVE -- start on stairs so that stash is handy , [(-1, 3, HERO)] ) , ( MONSTER_REPRESENTATIVE , [(-4, 1, SCOUT_MONSTER), (-4, 3, MONSTER)] ) ] -- Exactly one scout gets a sight boost, to help the aggressor, because he uses -- the scout for initial attack, while camper (on big enough maps) -- can't guess where the attack would come and so can't position his single -- scout to counter the stealthy advance. rosterShootout = [ ( EXPLORER_NO_ESCAPE , [(-5, 2, RANGER_HERO), (-5, 1, SCOUT_HERO)] ) , ( COMPETITOR_NO_ESCAPE , [(-5, 2, RANGER_HERO), (-5, 1, SCOUT_HERO)] ) , (HORROR_REPRESENTATIVE, []) ] rosterHunt = [ ( EXPLORER_NO_ESCAPE , [(-6, 7, SOLDIER_HERO)] ) , ( COMPETITOR_NO_ESCAPE , [(-6, 6, AMBUSHER_HERO), (-6, 1, SCOUT_HERO)] ) , (HORROR_REPRESENTATIVE, []) ] rosterFlight = [ ( COMPETITOR_NO_ESCAPE -- start on escape , [(-7, 6, AMBUSHER_HERO), (-7, 1, SCOUT_HERO)] ) , ( EXPLORER_MEDIUM , [(-7, 2, ESCAPIST_HERO), (-7, 1, SCOUT_HERO)] ) -- second on the list to let foes occupy the escape , (HORROR_REPRESENTATIVE, []) ] rosterZoo = [ ( EXPLORER_TRAPPED , [(-8, 5, SOLDIER_HERO)] ) , ( ANIMAL_CAPTIVE , [(-8, 100, MOBILE_ANIMAL)] ) , (HORROR_REPRESENTATIVE, []) ] -- for summoned monsters rosterAmbush = [ ( EXPLORER_NO_ESCAPE , [(-9, 5, AMBUSHER_HERO), (-9, 1, SCOUT_HERO)] ) , ( COMPETITOR_NO_ESCAPE , [(-9, 12, SOLDIER_HERO)] ) , (HORROR_REPRESENTATIVE, []) ] -- No horrors faction needed, because spawned heroes land in civilian faction. rosterSafari = [ ( MONSTER_TOURIST , [(-4, 15, MONSTER)] ) , ( CONVICT_REPRESENTATIVE , [(-4, 2, CIVILIAN)] ) , ( ANIMAL_MAGNIFICENT , [(-7, 15, MOBILE_ANIMAL)] ) , ( ANIMAL_EXQUISITE -- start on escape , [(-10, 20, MOBILE_ANIMAL)] ) ] rosterCrawlEmpty = [ ( EXPLORER_PACIFIST , [(-1, 1, HERO)] ) , (HORROR_PACIFIST, []) ] -- for spawned and summoned monsters rosterCrawlSurvival = [ ( EXPLORER_AUTOMATED , [(-1, 3, HERO)] ) , ( MONSTER_REPRESENTATIVE , [(-4, 1, SCOUT_MONSTER), (-4, 3, MONSTER)] ) , ( ANIMAL_NARRATING , [(-5, 10, ANIMAL)] ) ] -- explore unopposed for some time rosterSafariSurvival = [ ( MONSTER_TOURIST_PASSIVE , [(-4, 15, MONSTER)] ) , ( CONVICT_REPRESENTATIVE , [(-4, 3, CIVILIAN)] ) , ( ANIMAL_MAGNIFICENT_NARRATING , [(-7, 20, MOBILE_ANIMAL)] ) , ( ANIMAL_EXQUISITE , [(-10, 30, MOBILE_ANIMAL)] ) ] rosterBattle = [ ( EXPLORER_TRAPPED , [(-5, 5, SOLDIER_HERO)] ) , ( MONSTER_CAPTIVE , [(-5, 35, MOBILE_MONSTER)] ) , ( ANIMAL_CAPTIVE , [(-5, 30, MOBILE_ANIMAL)] ) ] rosterBattleDefense = [ ( EXPLORER_AUTOMATED_TRAPPED , [(-5, 5, SOLDIER_HERO)] ) , ( MONSTER_CAPTIVE_NARRATING , [(-5, 35, MOBILE_MONSTER)] ) , ( ANIMAL_CAPTIVE , [(-5, 30, MOBILE_ANIMAL)] ) ] rosterBattleSurvival = [ ( EXPLORER_AUTOMATED_TRAPPED , [(-5, 5, SOLDIER_HERO)] ) , ( MONSTER_CAPTIVE , [(-5, 35, MOBILE_MONSTER)] ) , ( ANIMAL_CAPTIVE_NARRATING , [(-5, 30, MOBILE_ANIMAL)] ) ] rosterDefense = [ ( EXPLORER_AUTOMATED , [(-1, 3, HERO)] ) , ( MONSTER_ANTI , [(-4, 1, SCOUT_MONSTER), (-4, 3, MONSTER)] ) , ( ANIMAL_REPRESENTATIVE , [ (-1, 1 + 1 `d` 2, ANIMAL) , (-10, 100, MOBILE_ANIMAL) ] ) ] rosterDefenseEmpty = [ ( MONSTER_ANTI_PACIFIST , [(-4, 1, SCOUT_MONSTER)] ) , (HORROR_PACIFIST, []) ] -- for spawned and summoned animals cavesRaid, cavesBrawl, cavesCrawl, cavesShootout, cavesHunt, cavesFlight, cavesZoo, cavesAmbush, cavesSafari, cavesDig, cavesSee, cavesShort, cavesCrawlEmpty, cavesBattle :: Caves cavesRaid = [([-2], [CAVE_RAID])] cavesBrawl = [([-2], [CAVE_BRAWL])] listCrawl :: [([Int], [GroupName CaveKind])] listCrawl = [ ([-1], [CAVE_OUTERMOST]) , ([-2], [CAVE_SHALLOW_ROGUE]) , ([-3], [CAVE_EMPTY]) , ([-4, -5, -6], [DEFAULT_RANDOM, CAVE_ROGUE, CAVE_ARENA]) , ([-7, -8], [CAVE_ROGUE, CAVE_SMOKING]) , ([-9], [CAVE_LABORATORY]) , ([-10], [CAVE_MINE]) ] cavesCrawl = listCrawl cavesShootout = [([-5], [CAVE_SHOOTOUT])] cavesHunt = [([-6], [CAVE_HUNT])] cavesFlight = [([-7], [CAVE_FLIGHT])] cavesZoo = [([-8], [CAVE_ZOO])] cavesAmbush = [([-9], [CAVE_AMBUSH])] cavesSafari = [ ([-4], [CAVE_SAFARI_1]) , ([-7], [CAVE_SAFARI_2]) , ([-10], [CAVE_SAFARI_3]) ] cavesDig = concat $ zipWith (map . renumberCaves) [0, -10 ..] (replicate 100 listCrawl) renumberCaves :: Int -> ([Int], [GroupName CaveKind]) -> ([Int], [GroupName CaveKind]) renumberCaves offset (ns, l) = (map (+ offset) ns, l) cavesSee = let numberCaves n c = ([n], [c]) in zipWith numberCaves [-1, -2 ..] $ concatMap (replicate 10) allCaves cavesShort = let numberCaves n c = ([n], [c]) in zipWith numberCaves [-1, -2 ..] $ concatMap (replicate 100) $ take 7 allCaves allCaves :: [GroupName CaveKind] allCaves = [ CAVE_RAID, CAVE_BRAWL, CAVE_SHOOTOUT, CAVE_HUNT, CAVE_FLIGHT, CAVE_ZOO , CAVE_AMBUSH , CAVE_ROGUE, CAVE_LABORATORY, CAVE_EMPTY, CAVE_ARENA, CAVE_SMOKING , CAVE_NOISE, CAVE_MINE ] cavesCrawlEmpty = cavesCrawl cavesBattle = [([-5], [CAVE_BATTLE])] LambdaHack-0.11.0.0/GameDefinition/Content/PlaceKind.hs0000644000000000000000000010107607346545000020556 0ustar0000000000000000-- | Definitions of place kinds. Every room in the game is an instantiated -- place kind. module Content.PlaceKind ( -- * Group name patterns pattern ROGUE, pattern LABORATORY, pattern ZOO, pattern BRAWL, pattern SHOOTOUT, pattern ARENA, pattern FLIGHT, pattern AMBUSH, pattern BATTLE, pattern NOISE, pattern MINE, pattern EMPTY , pattern INDOOR_ESCAPE_DOWN, pattern INDOOR_ESCAPE_UP, pattern OUTDOOR_ESCAPE_DOWN, pattern TINY_STAIRCASE, pattern OPEN_STAIRCASE, pattern CLOSED_STAIRCASE, pattern WALLED_STAIRCASE, pattern GATED_TINY_STAIRCASE, pattern GATED_OPEN_STAIRCASE, pattern GATED_CLOSED_STAIRCASE, pattern OUTDOOR_TINY_STAIRCASE, pattern OUTDOOR_CLOSED_STAIRCASE, pattern OUTDOOR_WALLED_STAIRCASE , groupNamesSingleton, groupNames , -- * Content content ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import Game.LambdaHack.Content.PlaceKind import Game.LambdaHack.Content.TileKind (TileKind) import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Content.TileKind hiding (content, groupNames, groupNamesSingleton) -- * Group name patterns groupNamesSingleton :: [GroupName PlaceKind] groupNamesSingleton = [] -- TODO: if we stick to the current system of generating extra kinds and their -- group names, let's also add the generated group names to @groupNames@. groupNames :: [GroupName PlaceKind] groupNames = [ROGUE, LABORATORY, ZOO, BRAWL, SHOOTOUT, ARENA, FLIGHT, AMBUSH, BATTLE, NOISE, MINE, EMPTY] ++ [INDOOR_ESCAPE_DOWN, INDOOR_ESCAPE_UP, OUTDOOR_ESCAPE_DOWN, TINY_STAIRCASE, OPEN_STAIRCASE, CLOSED_STAIRCASE, WALLED_STAIRCASE] ++ fst generatedStairs pattern ROGUE, LABORATORY, ZOO, BRAWL, SHOOTOUT, ARENA, FLIGHT, AMBUSH, BATTLE, NOISE, MINE, EMPTY :: GroupName PlaceKind pattern INDOOR_ESCAPE_DOWN, INDOOR_ESCAPE_UP, OUTDOOR_ESCAPE_DOWN, TINY_STAIRCASE, OPEN_STAIRCASE, CLOSED_STAIRCASE, WALLED_STAIRCASE, GATED_TINY_STAIRCASE, GATED_OPEN_STAIRCASE, GATED_CLOSED_STAIRCASE, OUTDOOR_TINY_STAIRCASE, OUTDOOR_CLOSED_STAIRCASE, OUTDOOR_WALLED_STAIRCASE :: GroupName PlaceKind pattern ROGUE = GroupName "rogue" pattern LABORATORY = GroupName "laboratory" pattern ZOO = GroupName "zoo" pattern BRAWL = GroupName "brawl" pattern SHOOTOUT = GroupName "shootout" pattern ARENA = GroupName "arena" pattern FLIGHT = GroupName "flight" pattern AMBUSH = GroupName "ambush" pattern BATTLE = GroupName "battle" pattern NOISE = GroupName "noise" pattern MINE = GroupName "mine" pattern EMPTY = GroupName "empty" pattern INDOOR_ESCAPE_DOWN = GroupName "indoor escape down" pattern INDOOR_ESCAPE_UP = GroupName "indoor escape up" pattern OUTDOOR_ESCAPE_DOWN = GroupName "outdoor escape down" pattern TINY_STAIRCASE = GroupName "tiny staircase" pattern OPEN_STAIRCASE = GroupName "open staircase" pattern CLOSED_STAIRCASE = GroupName "closed staircase" pattern WALLED_STAIRCASE = GroupName "walled staircase" -- This is a rotten compromise, because these are synthesized below, -- so typos can happen. pattern GATED_TINY_STAIRCASE = GroupName "gated tiny staircase" pattern GATED_OPEN_STAIRCASE = GroupName "gated open staircase" pattern GATED_CLOSED_STAIRCASE = GroupName "gated closed staircase" pattern OUTDOOR_TINY_STAIRCASE = GroupName "outdoor tiny staircase" pattern OUTDOOR_CLOSED_STAIRCASE = GroupName "outdoor closed staircase" pattern OUTDOOR_WALLED_STAIRCASE = GroupName "outdoor walled staircase" -- * Content content :: [PlaceKind] content = [deadEnd, rect, rect2, rect3, rect4, rectWindows, glasshouse, glasshouse2, glasshouse3, pulpit, ruin, ruin2, collapsed, collapsed2, collapsed3, collapsed4, collapsed5, collapsed6, collapsed7, pillar, pillar2, pillar3, pillar4, pillar5, colonnade, colonnade2, colonnade3, colonnade4, colonnade5, colonnade6, lampPost, lampPost2, lampPost3, lampPost4, treeShade, fogClump, fogClump2, smokeClump, smokeClump2, smokeClump3FGround, bushClump, bushClump2, escapeDown, escapeDown2, escapeDown3, escapeDown4, escapeDown5, staircase1, staircase2, staircase3, staircase4, staircase5, staircase6, staircase7, staircase8, staircase9, staircase10, staircase11, staircase12, staircase13, staircase14, staircase15, staircase16, staircase17, staircase18, staircase19, staircase20, staircase21, staircase22, staircase23, staircase24, staircase25, staircase26, staircase27, staircase28, staircase29, staircase30, staircase31, staircase32, staircase33, staircase34, staircase35, staircase36, staircase37] -- automatically generated ++ snd generatedStairs ++ generatedEscapes deadEnd, rect, rect2, rect3, rect4, rectWindows, glasshouse, glasshouse2, glasshouse3, pulpit, ruin, ruin2, collapsed, collapsed2, collapsed3, collapsed4, collapsed5, collapsed6, collapsed7, pillar, pillar2, pillar3, pillar4, pillar5, colonnade, colonnade2, colonnade3, colonnade4, colonnade5, colonnade6, lampPost, lampPost2, lampPost3, lampPost4, treeShade, fogClump, fogClump2, smokeClump, smokeClump2, smokeClump3FGround, bushClump, bushClump2, escapeDown, escapeDown2, escapeDown3, escapeDown4, escapeDown5, staircase1, staircase2, staircase3, staircase4, staircase5, staircase6, staircase7, staircase8, staircase9, staircase10, staircase11, staircase12, staircase13, staircase14, staircase15, staircase16, staircase17, staircase18, staircase19, staircase20, staircase21, staircase22, staircase23, staircase24, staircase25, staircase26, staircase27, staircase28, staircase29, staircase30, staircase31, staircase32, staircase33, staircase34, staircase35, staircase36, staircase37 :: PlaceKind staircase :: PlaceKind -- template staircaseBasic :: [PlaceKind] staircaseBasic = [staircase1, staircase2, staircase3, staircase4, staircase5, staircase6, staircase7, staircase8, staircase9, staircase10, staircase11, staircase12, staircase13, staircase14, staircase15, staircase16, staircase17, staircase18, staircase19, staircase20, staircase21, staircase22, staircase23, staircase24, staircase25, staircase26, staircase27, staircase28, staircase29, staircase30, staircase31, staircase32, staircase33, staircase34, staircase35, staircase36, staircase37] generatedStairs :: ([GroupName PlaceKind], [PlaceKind]) generatedStairs = let gatedStairs = map switchStaircaseToGated staircaseBasic outdoorStairs = map switchStaircaseToOutdoor staircaseBasic stairsAll = staircaseBasic ++ gatedStairs ++ outdoorStairs upStairs = map switchStaircaseToUp stairsAll downStairs = map switchStaircaseToDown stairsAll genStairs = gatedStairs ++ outdoorStairs ++ upStairs ++ downStairs in ( nub $ sort $ concatMap (map fst . pfreq) genStairs , genStairs ) escapeDownBasic :: [PlaceKind] escapeDownBasic = [escapeDown, escapeDown2, escapeDown3, escapeDown4, escapeDown5] generatedEscapes :: [PlaceKind] generatedEscapes = let upEscapes = map switchEscapeToUp escapeDownBasic outdoorEscapes = map switchEscapeToOutdoorDown escapeDownBasic in upEscapes ++ outdoorEscapes -- The dots below are @'\x00B7'@, as defined in `TileKind.floorSymbol`. defaultLegendLit :: EM.EnumMap Char (GroupName TileKind) defaultLegendLit = EM.fromList [ (' ', FILLER_WALL) , ('|', S_WALL_LIT) , ('-', S_WALL_HORIZONTAL_LIT) , ('0', S_PILLAR) , ('&', S_RUBBLE_PILE) , ('<', TILE_INDOOR_ESCAPE_UP) , ('>', TILE_INDOOR_ESCAPE_DOWN) , ('·', FLOOR_ACTOR_ITEM_LIT) , ('~', S_SHALLOW_WATER_LIT) , ('I', SIGNBOARD) ] defaultLegendDark :: EM.EnumMap Char (GroupName TileKind) defaultLegendDark = EM.fromList [ (' ', FILLER_WALL) , ('|', S_WALL_DARK) , ('-', S_WALL_HORIZONTAL_DARK) , ('0', S_PILLAR) , ('&', S_RUBBLE_PILE) , ('<', TILE_INDOOR_ESCAPE_UP) , ('>', TILE_INDOOR_ESCAPE_DOWN) , ('·', FLOOR_ACTOR_ITEM_DARK) , ('~', S_SHALLOW_WATER_DARK) , ('I', SIGNBOARD) ] deadEnd = PlaceKind -- needs to have index 0 { pname = "a dead end" , pfreq = [] , prarity = [] , pcover = CStretch , pfence = FNone , ptopLeft = ["·"] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } rect = PlaceKind -- Valid for any nonempty area, hence low frequency. { pname = "a chamber" , pfreq = [(ROGUE, 30), (LABORATORY, 10)] , prarity = [(1, 10), (10, 6)] , pcover = CStretch , pfence = FNone , ptopLeft = [ "--" , "|·" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } rect2 = rect { pname = "a pen" , pfreq = [(ZOO, 3)] } rect3 = overridePlaceKind [ ('|', S_WALL_LIT) -- visible from afar , ('-', S_WALL_HORIZONTAL_LIT) ] $ rect { pname = "a shed" , pfreq = [(BRAWL, 10), (SHOOTOUT, 1)] } rect4 = rect3 { pname = "cabinet" , pfreq = [(ARENA, 10)] } rectWindows = override2PlaceKind [ ('=', RECT_WINDOWS_HORIZONTAL_DARK) , ('!', RECT_WINDOWS_VERTICAL_DARK) ] [ ('=', RECT_WINDOWS_HORIZONTAL_LIT) , ('!', RECT_WINDOWS_VERTICAL_LIT) ] $ PlaceKind { pname = "a hut" , pfreq = [(FLIGHT, 10), (AMBUSH, 7)] , prarity = [(1, 10), (10, 10)] , pcover = CStretch , pfence = FNone , ptopLeft = [ "-=" , "!·" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } glasshouse = overridePlaceKind [ ('=', GLASSHOUSE_HORIZONTAL_LIT) -- visible from afar , ('!', GLASSHOUSE_VERTICAL_LIT) ] $ PlaceKind { pname = "a glasshouse" , pfreq = [(SHOOTOUT, 4)] , prarity = [(1, 10), (10, 7)] , pcover = CStretch , pfence = FNone , ptopLeft = [ "==" , "!·" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } glasshouse2 = override2PlaceKind [ ('=', GLASSHOUSE_HORIZONTAL_DARK) , ('!', GLASSHOUSE_VERTICAL_DARK) ] [ ('=', GLASSHOUSE_HORIZONTAL_LIT) , ('!', GLASSHOUSE_VERTICAL_LIT) ] $ glasshouse { pname = "a glass cage" , pfreq = [(ZOO, 10)] } glasshouse3 = glasshouse { pname = "a reading room" , pfreq = [(ARENA, 40)] } pulpit = overridePlaceKind [ ('=', GLASSHOUSE_HORIZONTAL_LIT) , ('!', GLASSHOUSE_VERTICAL_LIT) , ('0', S_PULPIT) ] $ PlaceKind -- except for floor, all will be lit, regardless of night/dark; OK { pname = "a stand dais" , pfreq = [(ARENA, 200), (ZOO, 200)] , prarity = [(1, 1)] , pcover = CMirror , pfence = FGround , ptopLeft = [ "==·" , "!··" , "··0" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } ruin = PlaceKind { pname = "ruins" , pfreq = [(BATTLE, 330)] , prarity = [(1, 1)] , pcover = CStretch , pfence = FNone , ptopLeft = [ "--" , "|X" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } ruin2 = overridePlaceKind [ ('|', S_WALL_LIT) -- visible from afar , ('-', S_WALL_HORIZONTAL_LIT) ] $ ruin { pname = "blasted walls" , pfreq = [(AMBUSH, 50)] } collapsed = PlaceKind { pname = "a collapsed cavern" , pfreq = [(NOISE, 1)] -- no point taking up space if very little space taken, -- but if no other place can be generated, a failsafe is useful , prarity = [(1, 1)] , pcover = CStretch , pfence = FNone , ptopLeft = [ "0" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } collapsed2 = collapsed { pfreq = [(NOISE, 1000), (BATTLE, 200)] , ptopLeft = [ "X0" , "00" ] } collapsed3 = collapsed { pfreq = [(NOISE, 2000), (BATTLE, 200)] , ptopLeft = [ "XX0" , "000" ] } collapsed4 = collapsed { pfreq = [(NOISE, 2000), (BATTLE, 200)] , ptopLeft = [ "XXX0" , "0000" ] } collapsed5 = collapsed { pfreq = [(NOISE, 3000), (BATTLE, 500)] , ptopLeft = [ "XX0" , "X00" , "000" ] } collapsed6 = collapsed { pfreq = [(NOISE, 4000), (BATTLE, 1000)] , ptopLeft = [ "XXX0" , "X000" , "0000" ] } collapsed7 = collapsed { pfreq = [(NOISE, 4000), (BATTLE, 1000)] , ptopLeft = [ "XXX0" , "XX00" , "0000" ] } pillar = PlaceKind { pname = "a hall" , pfreq = [(ROGUE, 600), (LABORATORY, 2000)] , prarity = [(1, 1)] , pcover = CStretch , pfence = FNone -- Larger rooms require support pillars. , ptopLeft = [ "----" , "|···" , "|·0·" , "|···" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } pillar2 = pillar { pfreq = [(ROGUE, 60), (LABORATORY, 200)] , ptopLeft = [ "----" , "|0··" , "|···" , "|···" ] } pillar3 = pillar { pfreq = [(ROGUE, 8000), (LABORATORY, 25000)] , ptopLeft = [ "-----" , "|0···" , "|····" , "|··0·" , "|····" ] } pillar4 = overridePlaceKind [('&', CACHE)] $ pillar { pname = "an exquisite hall" , pfreq = [(ROGUE, 30000), (LABORATORY, 100000)] , ptopLeft = [ "-----" , "|&·0·" , "|····" , "|0·0·" , "|····" ] } pillar5 = overridePlaceKind [('&', CACHE)] $ pillar { pname = "a decorated hall" , pfreq = [(ROGUE, 30000), (LABORATORY, 100000)] , ptopLeft = [ "-----" , "|&·0·" , "|····" , "|0···" , "|····" ] } colonnade = PlaceKind { pname = "a colonnade" , pfreq = [ (ROGUE, 3), (ARENA, 20), (LABORATORY, 2) , (EMPTY, 10000), (MINE, 1000), (BRAWL, 4) , (FLIGHT, 40), (AMBUSH, 40) ] , prarity = [(1, 10), (10, 10)] , pcover = CAlternate , pfence = FFloor , ptopLeft = [ "0·" , "··" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } colonnade2 = colonnade { prarity = [(1, 15), (10, 15)] , ptopLeft = [ "0·" , "·0" ] } colonnade3 = colonnade { prarity = [(1, 800), (10, 800)] , ptopLeft = [ "··0" , "·0·" , "0··" ] } colonnade4 = colonnade { prarity = [(1, 200), (10, 200)] , ptopLeft = [ "0··" , "·0·" , "··0" ] } colonnade5 = colonnade { prarity = [(1, 10), (10, 10)] , ptopLeft = [ "0··" , "··0" ] } colonnade6 = colonnade { prarity = [(1, 100), (10, 100)] , ptopLeft = [ "0·" , "··" , "·0" ] } lampPost = overridePlaceKind [ ('0', S_LAMP_POST) , ('·', S_FLOOR_ACTOR_LIT) ] $ PlaceKind { pname = "a lamp-lit area" , pfreq = [(FLIGHT, 200), (AMBUSH, 200), (ZOO, 100), (BATTLE, 100)] , prarity = [(1, 1)] , pcover = CVerbatim , pfence = FNone , ptopLeft = [ "X·X" , "·0·" , "X·X" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } lampPost2 = lampPost { ptopLeft = [ "···" , "·0·" , "···" ] } lampPost3 = lampPost { pfreq = [ (FLIGHT, 3000), (AMBUSH, 3000), (ZOO, 50) , (BATTLE, 110) ] , ptopLeft = [ "XX·XX" , "X···X" , "··0··" , "X···X" , "XX·XX" ] } lampPost4 = lampPost { pfreq = [(FLIGHT, 3000), (AMBUSH, 3000), (ZOO, 50), (BATTLE, 60)] , ptopLeft = [ "X···X" , "·····" , "··0··" , "·····" , "X···X" ] } treeShade = override2PlaceKind [ ('0', S_TREE_DARK) , ('s', TREE_SHADE_WALKABLE_DARK) ] [ ('0', S_TREE_LIT) , ('s', TREE_SHADE_WALKABLE_LIT) ] $ overridePlaceKind [('·', S_SHADED_GROUND)] $ PlaceKind { pname = "a tree shade" , pfreq = [(BRAWL, 1000)] , prarity = [(1, 1)] , pcover = CMirror , pfence = FNone , ptopLeft = [ "··s" , "s0·" , "Xs·" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } fogClump = override2PlaceKind [('f', FOG_CLUMP_DARK)] [('f', FOG_CLUMP_LIT)] $ overridePlaceKind [(';', S_FOG_LIT)] $ PlaceKind { pname = "a foggy patch" , pfreq = [(SHOOTOUT, 150), (EMPTY, 15)] , prarity = [(1, 1)] , pcover = CMirror , pfence = FNone , ptopLeft = [ "f;" , ";f" , ";X" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } fogClump2 = fogClump { pfreq = [(SHOOTOUT, 500), (EMPTY, 50)] , ptopLeft = [ "X;f" , "f;f" , ";;f" , "Xff" ] } smokeClump = override2PlaceKind [ ('f', SMOKE_CLUMP_DARK) , ('·', S_FLOOR_ACTOR_DARK) ] [ ('f', SMOKE_CLUMP_LIT) , ('·', S_FLOOR_ACTOR_LIT) ] $ overridePlaceKind [(';', S_SMOKE_LIT)] $ PlaceKind { pname = "a smoky patch" , pfreq = [(ZOO, 50)] , prarity = [(1, 1)] , pcover = CMirror , pfence = FNone , ptopLeft = [ "f;" , ";f" , ";X" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } smokeClump2 = smokeClump { pfreq = [(ZOO, 500)] , ptopLeft = [ "X;f" , "f;f" , ";;f" , "Xff" ] } smokeClump3FGround = smokeClump { pname = "a burned out area" , pfreq = [(LABORATORY, 150)] , prarity = [(1, 1)] , pcover = CVerbatim , pfence = FGround , ptopLeft = [ ";f;" , "f·f" , "f·f" , ";f;" ] -- should not be used in caves with trails, because bushes should -- not grow over such artificial trails } bushClump = override2PlaceKind [('f', BUSH_CLUMP_DARK)] [('f', BUSH_CLUMP_LIT)] $ overridePlaceKind [(';', S_BUSH_LIT)] $ PlaceKind { pname = "a bushy patch" , pfreq = [(SHOOTOUT, 40)] , prarity = [(1, 1)] , pcover = CMirror , pfence = FNone , ptopLeft = [ "Xf" -- one sure exit needed not to block a corner , ";X" , ";;" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit -- should not be used in caves with trails, because bushes can't -- grow over such artificial trails } bushClump2 = bushClump { pfreq = [(SHOOTOUT, 80)] , ptopLeft = [ "Xf" -- one sure exit needed not to block a corner , ";X" , ";X" , ";;" ] } escapeDown = overridePlaceKind [ ('|', S_WALL_LIT) -- visible from afar , ('-', S_WALL_HORIZONTAL_LIT) ] $ PlaceKind { pname = "an escape down" , pfreq = [(INDOOR_ESCAPE_DOWN, 1)] , prarity = [(1, 1)] , pcover = CVerbatim , pfence = FGround , ptopLeft = [ ">" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } escapeDown2 = escapeDown { pfreq = [(INDOOR_ESCAPE_DOWN, 1000)] , pfence = FFloor , ptopLeft = [ "0·0" , "·>·" , "0·0" ] } escapeDown3 = escapeDown { pfreq = [(INDOOR_ESCAPE_DOWN, 2000)] , pfence = FNone , ptopLeft = [ "-----" , "|0·0|" , "|·>·|" , "|0·0|" , "-----" ] } escapeDown4 = escapeDown { pfreq = [(INDOOR_ESCAPE_DOWN, 1000)] , pcover = CMirror , pfence = FFloor , ptopLeft = [ "0··" , "·>·" , "··0" ] } escapeDown5 = escapeDown { pfreq = [(INDOOR_ESCAPE_DOWN, 2000)] , pcover = CMirror , pfence = FNone , ptopLeft = [ "-----" , "|0··|" , "|·>·|" , "|0·0|" , "-----" ] } staircase = overridePlaceKind [ ('<', STAIRCASE_UP) , ('>', STAIRCASE_DOWN) , ('|', S_WALL_LIT) -- visible from afar , ('-', S_WALL_HORIZONTAL_LIT) ] $ PlaceKind { pname = "a staircase" , pfreq = [(TINY_STAIRCASE, 1)] -- no cover when arriving; low freq , prarity = [(1, 100), (10, 100)] , pcover = CVerbatim , pfence = FGround , ptopLeft = [ "<·>" ] , plegendDark = defaultLegendDark , plegendLit = defaultLegendLit } staircase1 = staircase { prarity = [(1, 1)] -- no cover when arriving; so low rarity } staircase2 = staircase { pfreq = [(TINY_STAIRCASE, 3)] , prarity = [(1, 1)] , pfence = FGround , ptopLeft = [ "·<·>·" ] } staircase3 = staircase { prarity = [(1, 1)] , pfence = FFloor } staircase4 = staircase2 { pfence = FFloor , prarity = [(1, 1)] } staircase5 = staircase { pfreq = [(OPEN_STAIRCASE, 200)] -- no cover, open , pfence = FGround , ptopLeft = [ "0·0" , "···" , "<·>" , "···" , "0·0" ] } staircase6 = staircase { pfreq = [(OPEN_STAIRCASE, 300)] , pfence = FGround , ptopLeft = [ "0·0·0" , "·····" , "·<·>·" , "·····" , "0·0·0" ] } staircase7 = staircase { pfreq = [(OPEN_STAIRCASE, 500)] , pfence = FGround , ptopLeft = [ "0·0·0·0" , "·······" , "0·<·>·0" , "·······" , "0·0·0·0" ] } staircase8 = staircase { pfreq = [(OPEN_STAIRCASE, 2000)] , pfence = FGround , ptopLeft = [ "·0·I·0·" , "0·····0" , "··<·>··" , "0·····0" , "·0·0·0·" ] } staircase9 = staircase { pfreq = [(OPEN_STAIRCASE, 500)] , pfence = FGround , ptopLeft = [ "0·······0" , "···<·>···" , "0·······0" ] } staircase10 = staircase { pfreq = [(OPEN_STAIRCASE, 500)] , pfence = FGround , ptopLeft = [ "0·····0" , "··<·>··" , "0·····0" ] } staircase11 = staircase { pfreq = [(CLOSED_STAIRCASE, 2000)] -- weak cover, low freq , pfence = FFloor , ptopLeft = [ "·0·" , "0·0" , "···" , "<·>" , "···" , "0·0" , "·0·" ] } staircase12 = staircase { pfreq = [(CLOSED_STAIRCASE, 4000)] , pfence = FFloor , ptopLeft = [ "·0·0·" , "0·0·0" , "·····" , "·<·>·" , "·····" , "0·0·0" , "·0·0·" ] } staircase13 = staircase { pfreq = [(CLOSED_STAIRCASE, 6000)] , pfence = FFloor , ptopLeft = [ "·0·0·0·" , "0·0·0·0" , "·······" , "0·<·>·0" , "·······" , "0·0·0·0" , "·0·0·0·" ] } staircase14 = staircase { pfreq = [(CLOSED_STAIRCASE, 10000)] , pfence = FFloor , ptopLeft = [ "0·0·0·0" , "·0·0·0·" , "0·····0" , "··<·>··" , "0·····0" , "·0·0·0·" , "0·0·0·0" ] } staircase15 = staircase { pfreq = [(CLOSED_STAIRCASE, 20000)] , pfence = FFloor , ptopLeft = [ "·0·0·0·0·" , "0·0·0·0·0" , "·0·····0·" , "0··<·>··0" , "·0·····0·" , "0·0·0·0·0" , "·0·0·0·0·" ] } staircase16 = staircase { pfreq = [(CLOSED_STAIRCASE, 20000)] , pfence = FFloor , ptopLeft = [ "0·0·0·0·0" , "·0·0·0·0·" , "0·······0" , "·0·<·>·0·" , "0·······0" , "·0·0·0·0·" , "0·0·0·0·0" ] } staircase17 = staircase { pfreq = [(CLOSED_STAIRCASE, 20000)] , pfence = FFloor , ptopLeft = [ "0·0·0·0·0·0" , "·0·0·0·0·0·" , "0·0·····0·0" , "·0··<·>··0·" , "0·0·····0·0" , "·0·0·0·0·0·" , "0·0·0·0·0·0" ] } staircase18 = staircase { pfreq = [(CLOSED_STAIRCASE, 80000)] , pfence = FFloor , ptopLeft = [ "··0·0·0·0··" , "·0·0·0·0·0·" , "0·0·····0·0" , "·0··<·>··0·" , "0·0·····0·0" , "·0·0·0·0·0·" , "··0·0·0·0··" ] } staircase19 = staircase { pfreq = [(CLOSED_STAIRCASE, 20000)] , pfence = FFloor , ptopLeft = [ "·0·0·0·0·0·" , "0·0·0·0·0·0" , "·0·······0·" , "0·0·<·>·0·0" , "·0·······0·" , "0·0·0·0·0·0" , "·0·0·0·0·0·" ] } staircase20 = staircase { pfreq = [(CLOSED_STAIRCASE, 5000)] , pfence = FFloor , ptopLeft = [ "·0·0·0·0·0·" , "0·0·····0·0" , "·0··<·>··0·" , "0·0·····0·0" , "·0·0·I·0·0·" ] } staircase21 = staircase { pfreq = [(CLOSED_STAIRCASE, 5000)] , pfence = FFloor , ptopLeft = [ "0·0·I·0·0" , "·0·····0·" , "0··<·>··0" , "·0·····0·" , "0·0·0·0·0" ] } staircase22 = staircase { pfreq = [(CLOSED_STAIRCASE, 2000)] , pfence = FFloor , ptopLeft = [ "0·0·····0·0" , "·0··<·>··0·" , "0·0·····0·0" ] } staircase23 = staircase { pfreq = [(CLOSED_STAIRCASE, 1000)] , pfence = FFloor , ptopLeft = [ "·0·······0·" , "0·0·<·>·0·0" , "·0·······0·" ] } staircase24 = staircase { pfreq = [(CLOSED_STAIRCASE, 1000)] , pfence = FFloor , ptopLeft = [ "·0·····0·" , "0··<·>··0" , "·0·····0·" ] } staircase25 = staircase { pfreq = [(WALLED_STAIRCASE, 10)] , pfence = FNone , ptopLeft = [ "-------" , "|·····|" , "|·<·>·|" , "|·····|" , "-------" ] } staircase26 = staircase { pfreq = [(WALLED_STAIRCASE, 50)] , pfence = FNone , ptopLeft = [ "---------" , "|·······|" , "|··<·>··|" , "|·······|" , "---------" ] } staircase27 = staircase { pfreq = [(WALLED_STAIRCASE, 100)] , pfence = FNone , ptopLeft = [ "---------" , "|0·····0|" , "|··<·>··|" , "|0·····0|" , "---------" ] } staircase28 = staircase { pfreq = [(WALLED_STAIRCASE, 1000)] , pfence = FNone , ptopLeft = [ "-------" , "|·····|" , "|·····|" , "|·<·>·|" , "|·····|" , "|·····|" , "-------" ] } staircase29 = staircase { pfreq = [(WALLED_STAIRCASE, 1000)] , pfence = FNone , ptopLeft = [ "-------" , "|0···0|" , "|·····|" , "|·<·>·|" , "|·····|" , "|0···0|" , "-------" ] } staircase30 = staircase { pfreq = [(WALLED_STAIRCASE, 1000)] , pfence = FNone , ptopLeft = [ "-------" , "|0·0·0|" , "|·····|" , "|·<·>·|" , "|·····|" , "|0·0·0|" , "-------" ] } staircase31 = staircase { pfreq = [(WALLED_STAIRCASE, 2000)] , pfence = FNone , ptopLeft = [ "---------" , "|·······|" , "|·······|" , "|··<·>··|" , "|·······|" , "|·······|" , "---------" ] } staircase32 = staircase { pfreq = [(WALLED_STAIRCASE, 5000)] , pfence = FNone , ptopLeft = [ "---------" , "|0·····0|" , "|·······|" , "|··<·>··|" , "|·······|" , "|0·····0|" , "---------" ] } staircase33 = staircase { pfreq = [(WALLED_STAIRCASE, 5000)] , pfence = FNone , ptopLeft = [ "---------" , "|0·0·0·0|" , "|·······|" , "|0·<·>·0|" , "|·······|" , "|0·0·0·0|" , "---------" ] } staircase34 = staircase { pfreq = [(WALLED_STAIRCASE, 5000)] , pfence = FNone , ptopLeft = [ "---------" , "|·0·0·0·|" , "|0·····0|" , "|··<·>··|" , "|0·····0|" , "|·0·I·0·|" , "---------" ] } staircase35 = staircase { pfreq = [(WALLED_STAIRCASE, 200)] , pfence = FNone , ptopLeft = [ "-----------" , "|·········|" , "|···<·>···|" , "|·········|" , "-----------" ] } staircase36 = staircase { pfreq = [(WALLED_STAIRCASE, 500)] , pfence = FNone , ptopLeft = [ "-----------" , "|·0·····0·|" , "|0··<·>··0|" , "|·0·····0·|" , "-----------" ] } staircase37 = staircase { pfreq = [(WALLED_STAIRCASE, 500)] , pfence = FNone , ptopLeft = [ "-----------" , "|0·······0|" , "|·0·<·>·0·|" , "|0·······0|" , "-----------" ] } switchStaircaseToUp :: PlaceKind -> PlaceKind switchStaircaseToUp s = override2PlaceKind [('>', STAIR_TERMINAL_DARK)] [('>', STAIR_TERMINAL_LIT)] $ s { pname = pname s <+> "up" , pfreq = renameFreqs (<+> "up") $ pfreq s } switchStaircaseToDown :: PlaceKind -> PlaceKind switchStaircaseToDown s = override2PlaceKind [('<', STAIR_TERMINAL_DARK)] [('<', STAIR_TERMINAL_LIT)] $ s { pname = pname s <+> "down" , pfreq = renameFreqs (<+> "down") $ pfreq s } overrideGated :: [(Char, GroupName TileKind)] overrideGated = [ ('<', GATED_STAIRCASE_UP), ('>', GATED_STAIRCASE_DOWN) , ('|', S_WALL_LIT), ('-', S_WALL_HORIZONTAL_LIT) ] -- visible from afar switchStaircaseToGated :: PlaceKind -> PlaceKind switchStaircaseToGated s = overridePlaceKind overrideGated $ s { pname = T.unwords $ "a gated" : tail (T.words (pname s)) , pfreq = renameFreqs ("gated" <+>) $ pfreq s } overrideOutdoor :: [(Char, GroupName TileKind)] overrideOutdoor = [ ('<', STAIRCASE_OUTDOOR_UP), ('>', STAIRCASE_OUTDOOR_DOWN) , ('|', S_WALL_LIT), ('-', S_WALL_HORIZONTAL_LIT) ] -- visible from afar switchStaircaseToOutdoor :: PlaceKind -> PlaceKind switchStaircaseToOutdoor s = overridePlaceKind overrideOutdoor $ s { pname = "an outdoor area exit" , pfreq = renameFreqs ("outdoor" <+>) $ pfreq s } switchEscapeToUp :: PlaceKind -> PlaceKind switchEscapeToUp s = overridePlaceKind [('>', TILE_INDOOR_ESCAPE_UP)] $ s { pname = "an escape up" , pfreq = map (\(_, n) -> (INDOOR_ESCAPE_UP, n)) $ pfreq s } switchEscapeToOutdoorDown :: PlaceKind -> PlaceKind switchEscapeToOutdoorDown s = overridePlaceKind [('>', TILE_OUTDOOR_ESCAPE_DOWN)] $ s { pname = "outdoor escape route" , pfreq = map (\(_, n) -> (OUTDOOR_ESCAPE_DOWN, n)) $ pfreq s } LambdaHack-0.11.0.0/GameDefinition/Content/RuleKind.hs0000644000000000000000000000552607346545000020444 0ustar0000000000000000{-# LANGUAGE TemplateHaskell #-} -- | Game rules and assorted game setup data. module Content.RuleKind ( standardRules ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Ini.Reader as Ini import Instances.TH.Lift () import Language.Haskell.TH.Syntax import System.FilePath import System.IO (IOMode (ReadMode), hGetContents, hSetEncoding, openFile, utf8) -- Cabal import qualified Paths_LambdaHack as Self (version) import Game.LambdaHack.Content.ItemKind (ItemSymbolsUsedInEngine (..)) import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Definition.DefsInternal standardRules :: RuleContent standardRules = RuleContent { rtitle = "LambdaHack" , rWidthMax = 80 , rHeightMax = 21 , rexeVersion = Self.version -- The strings containing the default configuration file -- included from config.ui.default. , rcfgUIName = "config.ui" <.> "ini" , rcfgUIDefault = $(do let path = "GameDefinition" "config.ui" <.> "default" qAddDependentFile path !s <- qRunIO $ do inputHandle <- openFile path ReadMode hSetEncoding inputHandle utf8 hGetContents inputHandle let !cfgUIDefault = either (error . ("Ini.parse of default config" `showFailure`)) id $ Ini.parse s lift (s, cfgUIDefault)) , rwriteSaveClips = 1000 , rleadLevelClips = 50 , rscoresFileName = "LambdaHack.scores" , rnearby = 20 , rstairWordCarried = ["staircase"] -- only one, so inert , ritemSymbols = ItemSymbolsUsedInEngine { rsymbolProjectile = toContentSymbol '|' , rsymbolLight = toContentSymbol '(' , rsymbolTool = toContentSymbol '(' , rsymbolSpecial = toContentSymbol '*' -- don't overuse; it clashes with projectiles , rsymbolGold = toContentSymbol '$' -- also gems , rsymbolNecklace = toContentSymbol '"' , rsymbolRing = toContentSymbol '=' , rsymbolPotion = toContentSymbol '!' -- also concoction, bottle, jar, vial , rsymbolFlask = toContentSymbol '!' , rsymbolScroll = toContentSymbol '?' -- also book, note, tablet, card , rsymbolTorsoArmor = toContentSymbol '[' , rsymbolMiscArmor = toContentSymbol '[' , rsymbolClothes = toContentSymbol '[' , rsymbolShield = toContentSymbol ']' , rsymbolPolearm = toContentSymbol ')' , rsymbolEdged = toContentSymbol ')' , rsymbolHafted = toContentSymbol ')' , rsymbolWand = toContentSymbol '/' -- also magical rod, pistol, instrument , rsymbolFood = toContentSymbol ',' -- also body part; distinct enough from floor, which is middle dot } } LambdaHack-0.11.0.0/GameDefinition/Content/TileKind.hs0000644000000000000000000011426007346545000020426 0ustar0000000000000000-- | Definitions of tile kinds. Every terrain tile in the game is -- an instantiated tile kind. module Content.TileKind ( -- * Group name patterns -- ** Used in CaveKind and perhaps elsewhere. pattern FILLER_WALL, pattern FLOOR_CORRIDOR_LIT, pattern FLOOR_CORRIDOR_DARK, pattern TRAIL_LIT, pattern SAFE_TRAIL_LIT, pattern LAB_TRAIL_LIT, pattern DAMP_FLOOR_LIT, pattern DAMP_FLOOR_DARK, pattern OUTDOOR_OUTER_FENCE, pattern DIRT_LIT, pattern DIRT_DARK, pattern FLOOR_ARENA_LIT, pattern FLOOR_ARENA_DARK , pattern EMPTY_SET_LIT, pattern EMPTY_SET_DARK, pattern NOISE_SET_LIT, pattern POWER_SET_LIT, pattern POWER_SET_DARK, pattern BATTLE_SET_LIT, pattern BATTLE_SET_DARK, pattern BRAWL_SET_LIT, pattern SHOOTOUT_SET_LIT, pattern ZOO_SET_LIT, pattern ZOO_SET_DARK, pattern FLIGHT_SET_LIT, pattern FLIGHT_SET_DARK, pattern AMBUSH_SET_LIT, pattern AMBUSH_SET_DARK, pattern ARENA_SET_LIT, pattern ARENA_SET_DARK -- ** Used in PlaceKind, but not in CaveKind. , pattern RECT_WINDOWS_VERTICAL_LIT, pattern RECT_WINDOWS_VERTICAL_DARK, pattern RECT_WINDOWS_HORIZONTAL_LIT, pattern RECT_WINDOWS_HORIZONTAL_DARK, pattern TREE_SHADE_WALKABLE_LIT, pattern TREE_SHADE_WALKABLE_DARK, pattern SMOKE_CLUMP_LIT, pattern SMOKE_CLUMP_DARK, pattern GLASSHOUSE_VERTICAL_LIT, pattern GLASSHOUSE_VERTICAL_DARK, pattern GLASSHOUSE_HORIZONTAL_LIT, pattern GLASSHOUSE_HORIZONTAL_DARK, pattern BUSH_CLUMP_LIT, pattern BUSH_CLUMP_DARK, pattern FOG_CLUMP_LIT, pattern FOG_CLUMP_DARK, pattern STAIR_TERMINAL_LIT, pattern STAIR_TERMINAL_DARK, pattern CACHE, pattern SIGNBOARD, pattern STAIRCASE_UP, pattern ORDINARY_STAIRCASE_UP, pattern STAIRCASE_OUTDOOR_UP, pattern GATED_STAIRCASE_UP, pattern STAIRCASE_DOWN, pattern ORDINARY_STAIRCASE_DOWN, pattern STAIRCASE_OUTDOOR_DOWN, pattern GATED_STAIRCASE_DOWN, pattern TILE_INDOOR_ESCAPE_UP, pattern TILE_INDOOR_ESCAPE_DOWN, pattern TILE_OUTDOOR_ESCAPE_DOWN, pattern FLOOR_ACTOR_ITEM_LIT, pattern FLOOR_ACTOR_ITEM_DARK , pattern S_PILLAR, pattern S_RUBBLE_PILE, pattern S_LAMP_POST, pattern S_TREE_LIT, pattern S_TREE_DARK, pattern S_WALL_LIT, pattern S_WALL_DARK, pattern S_WALL_HORIZONTAL_LIT, pattern S_WALL_HORIZONTAL_DARK, pattern S_PULPIT, pattern S_BUSH_LIT, pattern S_FOG_LIT, pattern S_SMOKE_LIT, pattern S_FLOOR_ACTOR_LIT, pattern S_FLOOR_ACTOR_DARK, pattern S_FLOOR_ASHES_LIT, pattern S_FLOOR_ASHES_DARK, pattern S_SHADED_GROUND, pattern S_SHALLOW_WATER_LIT, pattern S_SHALLOW_WATER_DARK , groupNamesSingleton, groupNames -- * Content , content ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Text as T import Game.LambdaHack.Content.TileKind import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Content.ItemKindEmbed -- * Group name patterns -- Warning, many of these are also sythesized, so typos can happen. groupNamesSingleton :: [GroupName TileKind] groupNamesSingleton = [S_PILLAR, S_RUBBLE_PILE, S_LAMP_POST, S_TREE_LIT, S_TREE_DARK, S_WALL_LIT, S_WALL_DARK, S_WALL_HORIZONTAL_LIT, S_WALL_HORIZONTAL_DARK, S_PULPIT, S_BUSH_LIT, S_FOG_LIT, S_SMOKE_LIT, S_FLOOR_ACTOR_LIT, S_FLOOR_ACTOR_DARK, S_FLOOR_ASHES_LIT, S_FLOOR_ASHES_DARK, S_SHADED_GROUND, S_SHALLOW_WATER_LIT, S_SHALLOW_WATER_DARK] ++ [S_SUSPECT_VERTICAL_WALL_LIT, S_SUSPECT_HORIZONTAL_WALL_LIT, S_CLOSED_VERTICAL_DOOR_LIT, S_CLOSED_HORIZONTAL_DOOR_LIT, S_OPEN_VERTICAL_DOOR_LIT, S_OPEN_HORIZONTAL_DOOR_LIT, S_SIGNBOARD_UNREAD] ++ [S_BUSH_DARK, S_CLOSED_HORIZONTAL_DOOR_DARK, S_CLOSED_VERTICAL_DOOR_DARK, S_OPEN_HORIZONTAL_DOOR_DARK, S_OPEN_VERTICAL_DOOR_DARK, S_SUSPECT_HORIZONTAL_WALL_DARK, S_SUSPECT_VERTICAL_WALL_DARK] -- ** Used in PlaceKind, but not in CaveKind. pattern S_PILLAR, S_RUBBLE_PILE, S_LAMP_POST, S_TREE_LIT, S_TREE_DARK, S_WALL_LIT, S_WALL_DARK, S_WALL_HORIZONTAL_LIT, S_WALL_HORIZONTAL_DARK, S_PULPIT, S_BUSH_LIT, S_FOG_LIT, S_SMOKE_LIT, S_FLOOR_ACTOR_LIT, S_FLOOR_ACTOR_DARK, S_FLOOR_ASHES_LIT, S_FLOOR_ASHES_DARK, S_SHADED_GROUND, S_SHALLOW_WATER_LIT, S_SHALLOW_WATER_DARK :: GroupName TileKind -- ** Used only internally in other TileKind definitions or never used. pattern S_SUSPECT_VERTICAL_WALL_LIT, S_SUSPECT_HORIZONTAL_WALL_LIT, S_CLOSED_VERTICAL_DOOR_LIT, S_CLOSED_HORIZONTAL_DOOR_LIT, S_OPEN_VERTICAL_DOOR_LIT, S_OPEN_HORIZONTAL_DOOR_LIT, S_SIGNBOARD_UNREAD :: GroupName TileKind -- * Not used, but needed, because auto-generated. Singletons. pattern S_BUSH_DARK, S_CLOSED_HORIZONTAL_DOOR_DARK, S_CLOSED_VERTICAL_DOOR_DARK, S_OPEN_HORIZONTAL_DOOR_DARK, S_OPEN_VERTICAL_DOOR_DARK, S_SUSPECT_HORIZONTAL_WALL_DARK, S_SUSPECT_VERTICAL_WALL_DARK :: GroupName TileKind -- TODO: if we stick to the current system of generating extra kinds and their -- group names, let's also add the generated group names to @groupNames@. groupNames :: [GroupName TileKind] groupNames = [FILLER_WALL, FLOOR_CORRIDOR_LIT, FLOOR_CORRIDOR_DARK, TRAIL_LIT, SAFE_TRAIL_LIT, LAB_TRAIL_LIT, DAMP_FLOOR_LIT, DAMP_FLOOR_DARK, OUTDOOR_OUTER_FENCE, DIRT_LIT, DIRT_DARK, FLOOR_ARENA_LIT, FLOOR_ARENA_DARK] ++ [EMPTY_SET_LIT, EMPTY_SET_DARK, NOISE_SET_LIT, POWER_SET_LIT, POWER_SET_DARK, BATTLE_SET_LIT, BATTLE_SET_DARK, BRAWL_SET_LIT, SHOOTOUT_SET_LIT, ZOO_SET_LIT, ZOO_SET_DARK, FLIGHT_SET_LIT, FLIGHT_SET_DARK, AMBUSH_SET_LIT, AMBUSH_SET_DARK, ARENA_SET_LIT, ARENA_SET_DARK] ++ [RECT_WINDOWS_VERTICAL_LIT, RECT_WINDOWS_VERTICAL_DARK, RECT_WINDOWS_HORIZONTAL_LIT, RECT_WINDOWS_HORIZONTAL_DARK, TREE_SHADE_WALKABLE_LIT, TREE_SHADE_WALKABLE_DARK, SMOKE_CLUMP_LIT, SMOKE_CLUMP_DARK, GLASSHOUSE_VERTICAL_LIT, GLASSHOUSE_VERTICAL_DARK, GLASSHOUSE_HORIZONTAL_LIT, GLASSHOUSE_HORIZONTAL_DARK, BUSH_CLUMP_LIT, BUSH_CLUMP_DARK, FOG_CLUMP_LIT, FOG_CLUMP_DARK, STAIR_TERMINAL_LIT, STAIR_TERMINAL_DARK, CACHE, SIGNBOARD, STAIRCASE_UP, ORDINARY_STAIRCASE_UP, STAIRCASE_OUTDOOR_UP, GATED_STAIRCASE_UP, STAIRCASE_DOWN, ORDINARY_STAIRCASE_DOWN, STAIRCASE_OUTDOOR_DOWN, GATED_STAIRCASE_DOWN, TILE_INDOOR_ESCAPE_UP, TILE_INDOOR_ESCAPE_DOWN, TILE_OUTDOOR_ESCAPE_DOWN, FLOOR_ACTOR_ITEM_LIT, FLOOR_ACTOR_ITEM_DARK] ++ [OBSCURED_VERTICAL_WALL_LIT, OBSCURED_HORIZONTAL_WALL_LIT, TRAPPED_VERTICAL_DOOR_LIT, TRAPPED_HORIZONAL_DOOR_LIT, TREE_BURNING_OR_NOT, BUSH_BURNING_OR_NOT, CACHE_OR_NOT] ++ [BRAWL_SET_DARK, NOISE_SET_DARK, OBSCURED_HORIZONTAL_WALL_DARK, OBSCURED_VERTICAL_WALL_DARK, SHOOTOUT_SET_DARK, TRAPPED_HORIZONAL_DOOR_DARK, TRAPPED_VERTICAL_DOOR_DARK] pattern FILLER_WALL, FLOOR_CORRIDOR_LIT, FLOOR_CORRIDOR_DARK, TRAIL_LIT, SAFE_TRAIL_LIT, LAB_TRAIL_LIT, DAMP_FLOOR_LIT, DAMP_FLOOR_DARK, OUTDOOR_OUTER_FENCE, DIRT_LIT, DIRT_DARK, FLOOR_ARENA_LIT, FLOOR_ARENA_DARK :: GroupName TileKind pattern EMPTY_SET_LIT, EMPTY_SET_DARK, NOISE_SET_LIT, POWER_SET_LIT, POWER_SET_DARK, BATTLE_SET_LIT, BATTLE_SET_DARK, BRAWL_SET_LIT, SHOOTOUT_SET_LIT, ZOO_SET_LIT, ZOO_SET_DARK, FLIGHT_SET_LIT, FLIGHT_SET_DARK, AMBUSH_SET_LIT, AMBUSH_SET_DARK, ARENA_SET_LIT, ARENA_SET_DARK :: GroupName TileKind -- ** Used in PlaceKind, but not in CaveKind. pattern RECT_WINDOWS_VERTICAL_LIT, RECT_WINDOWS_VERTICAL_DARK, RECT_WINDOWS_HORIZONTAL_LIT, RECT_WINDOWS_HORIZONTAL_DARK, TREE_SHADE_WALKABLE_LIT, TREE_SHADE_WALKABLE_DARK, SMOKE_CLUMP_LIT, SMOKE_CLUMP_DARK, GLASSHOUSE_VERTICAL_LIT, GLASSHOUSE_VERTICAL_DARK, GLASSHOUSE_HORIZONTAL_LIT, GLASSHOUSE_HORIZONTAL_DARK, BUSH_CLUMP_LIT, BUSH_CLUMP_DARK, FOG_CLUMP_LIT, FOG_CLUMP_DARK, STAIR_TERMINAL_LIT, STAIR_TERMINAL_DARK, CACHE, SIGNBOARD, STAIRCASE_UP, ORDINARY_STAIRCASE_UP, STAIRCASE_OUTDOOR_UP, GATED_STAIRCASE_UP, STAIRCASE_DOWN, ORDINARY_STAIRCASE_DOWN, STAIRCASE_OUTDOOR_DOWN, GATED_STAIRCASE_DOWN, TILE_INDOOR_ESCAPE_UP, TILE_INDOOR_ESCAPE_DOWN, TILE_OUTDOOR_ESCAPE_DOWN, FLOOR_ACTOR_ITEM_LIT, FLOOR_ACTOR_ITEM_DARK :: GroupName TileKind -- ** Used only internally in other TileKind definitions or never used. pattern OBSCURED_VERTICAL_WALL_LIT, OBSCURED_HORIZONTAL_WALL_LIT, TRAPPED_VERTICAL_DOOR_LIT, TRAPPED_HORIZONAL_DOOR_LIT, TREE_BURNING_OR_NOT, BUSH_BURNING_OR_NOT, CACHE_OR_NOT :: GroupName TileKind -- * Not used, but needed, because auto-generated. Not singletons. pattern BRAWL_SET_DARK, NOISE_SET_DARK, OBSCURED_HORIZONTAL_WALL_DARK, OBSCURED_VERTICAL_WALL_DARK, SHOOTOUT_SET_DARK, TRAPPED_HORIZONAL_DOOR_DARK, TRAPPED_VERTICAL_DOOR_DARK :: GroupName TileKind -- ** Used in CaveKind and perhaps elsewhere (or a dark/lit version thereof). pattern FILLER_WALL = GroupName "fillerWall" pattern FLOOR_CORRIDOR_LIT = GroupName "floorCorridorLit" pattern FLOOR_CORRIDOR_DARK = GroupName "floorCorridorDark" pattern TRAIL_LIT = GroupName "trailLit" pattern SAFE_TRAIL_LIT = GroupName "safeTrailLit" pattern LAB_TRAIL_LIT = GroupName "labTrailLit" -- these three would work without @_LIT@, but it will be needed when -- in the future a lit trail is made from terrain that has an autogenerated -- dark variant pattern DAMP_FLOOR_LIT = GroupName "damp floor Lit" pattern DAMP_FLOOR_DARK = GroupName "damp floor Dark" pattern OUTDOOR_OUTER_FENCE = GroupName "outdoor outer fence" pattern DIRT_LIT = GroupName "dirt Lit" pattern DIRT_DARK = GroupName "dirt Dark" pattern FLOOR_ARENA_LIT = GroupName "floorArenaLit" pattern FLOOR_ARENA_DARK = GroupName "floorArenaDark" -- ** Used in CaveKind and perhaps elsewhere; sets of tiles for filling cave. pattern EMPTY_SET_LIT = GroupName "emptySetLit" pattern EMPTY_SET_DARK = GroupName "emptySetDark" pattern NOISE_SET_LIT = GroupName "noiseSetLit" pattern POWER_SET_LIT = GroupName "powerSetLit" pattern POWER_SET_DARK = GroupName "powerSetDark" pattern BATTLE_SET_LIT = GroupName "battleSetLit" pattern BATTLE_SET_DARK = GroupName "battleSetDark" pattern BRAWL_SET_LIT = GroupName "brawlSetLit" pattern SHOOTOUT_SET_LIT = GroupName "shootoutSetLit" pattern ZOO_SET_LIT = GroupName "zooSetLit" pattern ZOO_SET_DARK = GroupName "zooSetDark" pattern FLIGHT_SET_LIT = GroupName "flightSetLit" pattern FLIGHT_SET_DARK = GroupName "flightSetDark" pattern AMBUSH_SET_LIT = GroupName "ambushSetLit" pattern AMBUSH_SET_DARK = GroupName "ambushSetDark" pattern ARENA_SET_LIT = GroupName "arenaSetLit" pattern ARENA_SET_DARK = GroupName "arenaSetDark" -- ** Used in PlaceKind, but not in CaveKind. Not singletons. pattern RECT_WINDOWS_VERTICAL_LIT = GroupName "rectWindowsVerticalLit" pattern RECT_WINDOWS_VERTICAL_DARK = GroupName "rectWindowsVerticalDark" pattern RECT_WINDOWS_HORIZONTAL_LIT = GroupName "rectWindowsHorizontalLit" pattern RECT_WINDOWS_HORIZONTAL_DARK = GroupName "rectWindowsHorizontalDark" pattern TREE_SHADE_WALKABLE_LIT = GroupName "treeShadeWalkableLit" pattern TREE_SHADE_WALKABLE_DARK = GroupName "treeShadeWalkableDark" pattern SMOKE_CLUMP_LIT = GroupName "smokeClumpLit" pattern SMOKE_CLUMP_DARK = GroupName "smokeClumpDark" pattern GLASSHOUSE_VERTICAL_LIT = GroupName "glasshouseVerticalLit" pattern GLASSHOUSE_VERTICAL_DARK = GroupName "glasshouseVerticalDark" pattern GLASSHOUSE_HORIZONTAL_LIT = GroupName "glasshouseHorizontalLit" pattern GLASSHOUSE_HORIZONTAL_DARK = GroupName "glasshouseHorizontalDark" pattern BUSH_CLUMP_LIT = GroupName "bushClumpLit" pattern BUSH_CLUMP_DARK = GroupName "bushClumpDark" pattern FOG_CLUMP_LIT = GroupName "fogClumpLit" pattern FOG_CLUMP_DARK = GroupName "fogClumpDark" pattern STAIR_TERMINAL_LIT = GroupName "stair terminal Lit" pattern STAIR_TERMINAL_DARK = GroupName "stair terminal Dark" pattern CACHE = GroupName "cache" pattern SIGNBOARD = GroupName "signboard" pattern STAIRCASE_UP = GroupName "staircase up" pattern ORDINARY_STAIRCASE_UP = GroupName "ordinary staircase up" pattern STAIRCASE_OUTDOOR_UP = GroupName "staircase outdoor up" pattern GATED_STAIRCASE_UP = GroupName "gated staircase up" pattern STAIRCASE_DOWN = GroupName "staircase down" pattern ORDINARY_STAIRCASE_DOWN = GroupName "ordinary staircase down" pattern STAIRCASE_OUTDOOR_DOWN = GroupName "staircase outdoor down" pattern GATED_STAIRCASE_DOWN = GroupName "gated staircase down" pattern TILE_INDOOR_ESCAPE_UP = GroupName "indoor escape up" pattern TILE_INDOOR_ESCAPE_DOWN = GroupName "indoor escape down" pattern TILE_OUTDOOR_ESCAPE_DOWN = GroupName "outdoor escape down" pattern FLOOR_ACTOR_ITEM_LIT = GroupName "floorActorItemLit" pattern FLOOR_ACTOR_ITEM_DARK = GroupName "floorActorItemDark" -- ** Used in PlaceKind, but not in CaveKind. Singletons. pattern S_PILLAR = GroupName "pillar" pattern S_RUBBLE_PILE = GroupName "rubble pile" pattern S_LAMP_POST = GroupName "lamp post" pattern S_TREE_LIT = GroupName "tree Lit" pattern S_TREE_DARK = GroupName "tree Dark" pattern S_WALL_LIT = GroupName "wall Lit" pattern S_WALL_DARK = GroupName "wall Dark" pattern S_WALL_HORIZONTAL_LIT = GroupName "wall horizontal Lit" pattern S_WALL_HORIZONTAL_DARK = GroupName "wall horizontal Dark" pattern S_PULPIT = GroupName "pulpit" pattern S_BUSH_LIT = GroupName "bush Lit" pattern S_FOG_LIT = GroupName "fog Lit" pattern S_SMOKE_LIT = GroupName "smoke Lit" pattern S_FLOOR_ACTOR_LIT = GroupName "floor with actors Lit" pattern S_FLOOR_ACTOR_DARK = GroupName "floor with actors Dark" pattern S_FLOOR_ASHES_LIT = GroupName "floor with ashes Lit" pattern S_FLOOR_ASHES_DARK = GroupName "floor with ashes Dark" pattern S_SHADED_GROUND = GroupName "shaded ground" pattern S_SHALLOW_WATER_LIT = GroupName "shallow water Lit" pattern S_SHALLOW_WATER_DARK = GroupName "shallow water Dark" -- ** Used only internally in other TileKind definitions. Not singletons. pattern OBSCURED_VERTICAL_WALL_LIT = GroupName "obscured vertical wall Lit" pattern OBSCURED_HORIZONTAL_WALL_LIT = GroupName "obscured horizontal wall Lit" pattern TRAPPED_VERTICAL_DOOR_LIT = GroupName "trapped vertical door Lit" pattern TRAPPED_HORIZONAL_DOOR_LIT = GroupName "trapped horizontal door Lit" pattern TREE_BURNING_OR_NOT = GroupName "tree burning or not" pattern BUSH_BURNING_OR_NOT = GroupName "bush burning or not" pattern CACHE_OR_NOT = GroupName "cache or not" -- ** Used only internally in other TileKind definitions. Singletons. pattern S_SUSPECT_VERTICAL_WALL_LIT = GroupName "suspect vertical wall Lit" pattern S_SUSPECT_HORIZONTAL_WALL_LIT = GroupName "suspect horizontal wall Lit" pattern S_CLOSED_VERTICAL_DOOR_LIT = GroupName "closed vertical door Lit" pattern S_CLOSED_HORIZONTAL_DOOR_LIT = GroupName "closed horizontal door Lit" pattern S_OPEN_VERTICAL_DOOR_LIT = GroupName "open vertical door Lit" pattern S_OPEN_HORIZONTAL_DOOR_LIT = GroupName "open horizontal door Lit" pattern S_SIGNBOARD_UNREAD = GroupName "signboard unread" -- * Not used, but needed, because auto-generated. Not singletons. -- This is a rotten compromise, because these are synthesized below, -- so typos can happen. Similarly below pattern BRAWL_SET_DARK = GroupName "brawlSetDark" pattern NOISE_SET_DARK = GroupName "noiseSetDark" pattern OBSCURED_HORIZONTAL_WALL_DARK = GroupName "obscured horizontal wall Dark" pattern OBSCURED_VERTICAL_WALL_DARK = GroupName "obscured vertical wall Dark" pattern SHOOTOUT_SET_DARK = GroupName "shootoutSetDark" pattern TRAPPED_HORIZONAL_DOOR_DARK = GroupName "trapped horizontal door Dark" pattern TRAPPED_VERTICAL_DOOR_DARK = GroupName "trapped vertical door Dark" -- * Not used, but needed, because auto-generated. Singletons. pattern S_BUSH_DARK = GroupName "bush Dark" pattern S_CLOSED_HORIZONTAL_DOOR_DARK = GroupName "closed horizontal door Dark" pattern S_CLOSED_VERTICAL_DOOR_DARK = GroupName "closed vertical door Dark" pattern S_OPEN_HORIZONTAL_DOOR_DARK = GroupName "open horizontal door Dark" pattern S_OPEN_VERTICAL_DOOR_DARK = GroupName "open vertical door Dark" pattern S_SUSPECT_HORIZONTAL_WALL_DARK = GroupName "suspect horizontal wall Dark" pattern S_SUSPECT_VERTICAL_WALL_DARK = GroupName "suspect vertical wall Dark" -- * Content content :: [TileKind] content = [unknown, unknownOuterFence, basicOuterFence, bedrock, wall, wallSuspect, wallObscured, wallH, wallSuspectH, wallObscuredDefacedH, wallObscuredFrescoedH, pillar, pillarCache, lampPost, signboardUnread, signboardRead, tree, treeBurnt, treeBurning, rubble, rubbleSpice, doorTrapped, doorClosed, doorTrappedH, doorClosedH, stairsUp, stairsTrappedUp, stairsOutdoorUp, stairsGatedUp, stairsDown, stairsTrappedDown, stairsOutdoorDown, stairsGatedDown, escapeUp, escapeDown, escapeOutdoorDown, wallGlass, wallGlassSpice, wallGlassH, wallGlassHSpice, pillarIce, pulpit, bush, bushBurnt, bushBurning, fog, fogDark, smoke, smokeDark, doorOpen, doorOpenH, floorCorridor, floorArena, floorDamp, floorDirt, floorDirtSpice, floorActor, floorActorItem, floorAshes, shallowWater, shallowWaterSpice, floorRed, floorBlue, floorGreen, floorBrown, floorArenaShade, outdoorFence ] ++ map makeDark ldarkable ++ map makeDarkColor ldarkColorable unknown, unknownOuterFence, basicOuterFence, bedrock, wall, wallSuspect, wallObscured, wallH, wallSuspectH, wallObscuredDefacedH, wallObscuredFrescoedH, pillar, pillarCache, lampPost, signboardUnread, signboardRead, tree, treeBurnt, treeBurning, rubble, rubbleSpice, doorTrapped, doorClosed, doorTrappedH, doorClosedH, stairsUp, stairsTrappedUp, stairsOutdoorUp, stairsGatedUp, stairsDown, stairsTrappedDown, stairsOutdoorDown, stairsGatedDown, escapeUp, escapeDown, escapeOutdoorDown, wallGlass, wallGlassSpice, wallGlassH, wallGlassHSpice, pillarIce, pulpit, bush, bushBurnt, bushBurning, fog, fogDark, smoke, smokeDark, doorOpen, doorOpenH, floorCorridor, floorArena, floorDamp, floorDirt, floorDirtSpice, floorActor, floorActorItem, floorAshes, shallowWater, shallowWaterSpice, floorRed, floorBlue, floorGreen, floorBrown, floorArenaShade, outdoorFence :: TileKind ldarkable :: [TileKind] ldarkable = [wall, wallSuspect, wallObscured, wallH, wallSuspectH, wallObscuredDefacedH, wallObscuredFrescoedH, doorTrapped, doorClosed, doorTrappedH, doorClosedH, wallGlass, wallGlassSpice, wallGlassH, wallGlassHSpice, doorOpen, doorOpenH, floorCorridor, shallowWater, shallowWaterSpice] ldarkColorable :: [TileKind] ldarkColorable = [tree, bush, floorArena, floorDamp, floorDirt, floorDirtSpice, floorActor, floorActorItem] -- Symbols to be used (the Nethack visual tradition imposes inconsistency): -- LOS noLOS -- Walk .|-#~ :; -- noWalk %^-| -| O&<>+ -- -- can be opened ^&+ -- can be closed |- -- some noWalk can be changed without opening, regardless of symbol -- not used yet: -- : (curtain, etc., not flowing, but solid and static) -- `' (not visible enough when immobile) -- White, cyan and green terrain is usually inert, red is burning or trapped, -- blue activable or trapped, magenta searchable or activable. -- Note that for AI hints and UI comfort, most multiple-use @Embed@ tiles -- should have a variant, which after first use transforms into a different -- colour tile without @ChangeTo@ and similar (which then AI no longer touches). -- If a tile is supposed to be repeatedly activated by AI (e.g., cache), -- it should keep @ChangeTo@ for the whole time. -- * Main tiles, in other games modified and some removed -- ** Not walkable -- *** Not clear unknown = TileKind -- needs to have index 0 and alter 1; no other with 1 { tsymbol = ' ' , tname = "unknown space" , tfreq = [(S_UNKNOWN_SPACE, 1)] , tcolor = defFG , tcolor2 = defFG , talter = 1 , tfeature = [Dark] } unknownOuterFence = TileKind { tsymbol = ' ' , tname = "unknown space" , tfreq = [(S_UNKNOWN_OUTER_FENCE, 1)] , tcolor = defFG , tcolor2 = defFG , talter = maxBound -- impenetrable , tfeature = [Dark] } basicOuterFence = TileKind { tsymbol = ' ' , tname = "impenetrable bedrock" , tfreq = [(S_BASIC_OUTER_FENCE, 1)] , tcolor = defFG , tcolor2 = defFG , talter = maxBound -- impenetrable , tfeature = [Dark] } bedrock = TileKind { tsymbol = ' ' , tname = "bedrock" , tfreq = [(FILLER_WALL, 1)] , tcolor = defFG , tcolor2 = defFG , talter = 100 , tfeature = [Dark] -- Bedrock being dark is bad for AI (forces it to backtrack to explore -- bedrock at corridor turns) and induces human micromanagement -- if there can be corridors joined diagonally (humans have to check -- with the xhair if the dark space is bedrock or unexplored). -- Lit bedrock would be even worse for humans, because it's harder -- to guess which tiles are unknown and which can be explored bedrock. -- The setup of Allure is ideal, with lit bedrock that is easily -- distinguished from an unknown tile. However, LH follows the NetHack, -- not the Angband, visual tradition, so we can't improve the situation, -- unless we turn to subtle shades of black or non-ASCII glyphs, -- but that is yet different aesthetics. } wall = TileKind { tsymbol = '|' , tname = "granite wall" , tfreq = [ (S_WALL_LIT, 100) , (RECT_WINDOWS_VERTICAL_LIT, 80) ] , tcolor = BrWhite , tcolor2 = defFG , talter = 100 , tfeature = [BuildAs S_SUSPECT_VERTICAL_WALL_LIT] } wallSuspect = TileKind -- only on client { tsymbol = '|' , tname = "suspect uneven wall" , tfreq = [(S_SUSPECT_VERTICAL_WALL_LIT, 1)] , tcolor = BrWhite , tcolor2 = defFG , talter = 2 , tfeature = [ RevealAs TRAPPED_VERTICAL_DOOR_LIT , ObscureAs OBSCURED_VERTICAL_WALL_LIT ] } wallObscured = TileKind { tsymbol = '|' , tname = "scratched wall" , tfreq = [(OBSCURED_VERTICAL_WALL_LIT, 1)] , tcolor = BrWhite , tcolor2 = defFG , talter = 5 , tfeature = [ Embed SCRATCH_ON_WALL , HideAs S_SUSPECT_VERTICAL_WALL_LIT ] } wallH = TileKind { tsymbol = '-' , tname = "sandstone wall" , tfreq = [ (S_WALL_HORIZONTAL_LIT, 100) , (RECT_WINDOWS_HORIZONTAL_LIT, 80) ] , tcolor = BrWhite , tcolor2 = defFG , talter = 100 , tfeature = [BuildAs S_SUSPECT_HORIZONTAL_WALL_LIT] } wallSuspectH = TileKind -- only on client { tsymbol = '-' , tname = "suspect painted wall" , tfreq = [(S_SUSPECT_HORIZONTAL_WALL_LIT, 1)] , tcolor = BrWhite , tcolor2 = defFG , talter = 2 , tfeature = [ RevealAs TRAPPED_HORIZONAL_DOOR_LIT , ObscureAs OBSCURED_HORIZONTAL_WALL_LIT ] } wallObscuredDefacedH = TileKind { tsymbol = '-' , tname = "defaced wall" , tfreq = [(OBSCURED_HORIZONTAL_WALL_LIT, 90)] , tcolor = BrWhite , tcolor2 = defFG , talter = 5 , tfeature = [ Embed OBSCENE_PICTOGRAM , HideAs S_SUSPECT_HORIZONTAL_WALL_LIT ] } wallObscuredFrescoedH = TileKind { tsymbol = '-' , tname = "frescoed wall" , tfreq = [(OBSCURED_HORIZONTAL_WALL_LIT, 10)] , tcolor = BrWhite , tcolor2 = defFG , talter = 5 , tfeature = [ Embed SUBTLE_FRESCO , HideAs S_SUSPECT_HORIZONTAL_WALL_LIT ] -- a bit beneficial, but AI would loop if allowed to trigger -- so no @ConsideredByAI@ } pillar = TileKind { tsymbol = '0' , tname = "rock outcrop" , tfreq = [ (S_PILLAR, 1), (CACHE_OR_NOT, 70) , (STAIR_TERMINAL_LIT, 100), (STAIR_TERMINAL_DARK, 100) , (EMPTY_SET_LIT, 20), (NOISE_SET_LIT, 700) , (POWER_SET_DARK, 700) , (BATTLE_SET_DARK, 200), (BRAWL_SET_LIT, 50) , (SHOOTOUT_SET_LIT, 10), (ZOO_SET_DARK, 10) ] , tcolor = BrCyan -- not BrWhite, to tell from heroes , tcolor2 = Cyan , talter = 100 , tfeature = [] } pillarCache = TileKind { tsymbol = '0' , tname = "smoothed outcrop" , tfreq = [(CACHE_OR_NOT, 30), (CACHE, 1), (STAIR_TERMINAL_DARK, 4)] -- treasure only in dark staircases , tcolor = BrBlue , tcolor2 = Blue , talter = 5 , tfeature = [ Embed TREASURE_CACHE, Embed TREASURE_CACHE_TRAP , ChangeTo CACHE_OR_NOT, ConsideredByAI ] -- Not explorable, but prominently placed, so hard to miss. -- Very beneficial, so AI eager to trigger, unless wary of traps. } lampPost = TileKind { tsymbol = '0' , tname = "lamp post" , tfreq = [(S_LAMP_POST, 1)] , tcolor = BrYellow , tcolor2 = Brown , talter = 100 , tfeature = [] } signboardUnread = TileKind -- client only, indicates never used by this faction { tsymbol = '0' , tname = "signboard" , tfreq = [(S_SIGNBOARD_UNREAD, 1)] , tcolor = BrCyan , tcolor2 = Cyan , talter = 5 , tfeature = [ ConsideredByAI -- changes after use, so safe for AI , RevealAs SIGNBOARD -- to display as hidden ] } signboardRead = TileKind { tsymbol = '0' , tname = "signboard" , tfreq = [(SIGNBOARD, 1), (FLIGHT_SET_DARK, 1)] , tcolor = BrCyan , tcolor2 = Cyan , talter = 5 , tfeature = [Embed SIGNAGE, HideAs S_SIGNBOARD_UNREAD] } tree = TileKind { tsymbol = '0' , tname = "tree" , tfreq = [ (BRAWL_SET_LIT, 140), (SHOOTOUT_SET_LIT, 10) , (FLIGHT_SET_LIT, 35), (AMBUSH_SET_LIT, 3) , (S_TREE_LIT, 1) ] , tcolor = BrGreen , tcolor2 = Green , talter = 50 , tfeature = [] } treeBurnt = tree { tname = "burnt tree" , tfreq = [ (AMBUSH_SET_DARK, 3), (ZOO_SET_DARK, 7), (BATTLE_SET_DARK, 50) , (TREE_BURNING_OR_NOT, 30) ] , tcolor = BrBlack , tcolor2 = BrBlack , tfeature = Dark : tfeature tree } treeBurning = tree { tname = "burning tree" , tfreq = [ (AMBUSH_SET_DARK, 15), (ZOO_SET_DARK, 70) , (TREE_BURNING_OR_NOT, 70) ] , tcolor = BrRed , tcolor2 = Red , talter = 5 , tfeature = Embed BIG_FIRE : ChangeTo TREE_BURNING_OR_NOT : tfeature tree -- TODO: dousing off the tree will have more sense when it periodically -- explodes, hitting and lighting up the team and so betraying it } rubble = TileKind { tsymbol = '&' , tname = "rubble pile" , tfreq = [ (S_RUBBLE_PILE, 1) , (STAIR_TERMINAL_LIT, 4), (STAIR_TERMINAL_DARK, 4) , (EMPTY_SET_LIT, 10), (EMPTY_SET_DARK, 10) , (NOISE_SET_LIT, 50), (POWER_SET_DARK, 50) , (ZOO_SET_DARK, 100), (AMBUSH_SET_DARK, 10) ] , tcolor = BrYellow , tcolor2 = Brown , talter = 4 -- boss can dig through , tfeature = [Embed RUBBLE, OpenTo S_FLOOR_ASHES_LIT] -- Getting the item is risky and, e.g., AI doesn't attempt it. -- Also, AI doesn't go out of its way to clear the way for heroes. } rubbleSpice = rubble { tfreq = [(SMOKE_CLUMP_LIT, 1), (SMOKE_CLUMP_DARK, 1)] , tfeature = Spice : tfeature rubble } doorTrapped = TileKind { tsymbol = '+' , tname = "trapped door" , tfreq = [(TRAPPED_VERTICAL_DOOR_LIT, 1)] , tcolor = BrRed , tcolor2 = Red , talter = 2 , tfeature = [ Embed DOORWAY_TRAP , OpenTo S_OPEN_VERTICAL_DOOR_LIT , HideAs S_SUSPECT_VERTICAL_WALL_LIT ] } doorClosed = TileKind { tsymbol = '+' , tname = "closed door" , tfreq = [(S_CLOSED_VERTICAL_DOOR_LIT, 1)] , tcolor = Brown , tcolor2 = BrBlack , talter = 2 , tfeature = [OpenTo S_OPEN_VERTICAL_DOOR_LIT] -- never hidden } doorTrappedH = TileKind { tsymbol = '+' , tname = "trapped door" , tfreq = [(TRAPPED_HORIZONAL_DOOR_LIT, 1)] , tcolor = BrRed , tcolor2 = Red , talter = 2 , tfeature = [ Embed DOORWAY_TRAP , OpenTo S_OPEN_HORIZONTAL_DOOR_LIT , HideAs S_SUSPECT_HORIZONTAL_WALL_LIT ] } doorClosedH = TileKind { tsymbol = '+' , tname = "closed door" , tfreq = [(S_CLOSED_HORIZONTAL_DOOR_LIT, 1)] , tcolor = Brown , tcolor2 = BrBlack , talter = 2 , tfeature = [OpenTo S_OPEN_HORIZONTAL_DOOR_LIT] -- never hidden } stairsUp = TileKind { tsymbol = '<' , tname = "staircase up" , tfreq = [(STAIRCASE_UP, 9), (ORDINARY_STAIRCASE_UP, 1)] , tcolor = BrWhite , tcolor2 = defFG , talter = talterForStairs , tfeature = [Embed STAIRS_UP, ConsideredByAI] } stairsTrappedUp = TileKind { tsymbol = '<' , tname = "windy staircase up" , tfreq = [(STAIRCASE_UP, 1)] , tcolor = BrRed , tcolor2 = Red , talter = talterForStairs , tfeature = [ Embed STAIRS_UP, Embed STAIRS_TRAP_UP , ConsideredByAI, ChangeTo ORDINARY_STAIRCASE_UP ] -- AI uses despite the trap; exploration more important } stairsOutdoorUp = stairsUp { tname = "signpost pointing backward" , tfreq = [(STAIRCASE_OUTDOOR_UP, 1)] } stairsGatedUp = stairsUp { tname = "gated staircase up" , tfreq = [(GATED_STAIRCASE_UP, 1)] , talter = talterForStairs + 2 -- animals and bosses can't use } stairsDown = TileKind { tsymbol = '>' , tname = "staircase down" , tfreq = [(STAIRCASE_DOWN, 9), (ORDINARY_STAIRCASE_DOWN, 1)] , tcolor = BrWhite , tcolor2 = defFG , talter = talterForStairs , tfeature = [Embed STAIRS_DOWN, ConsideredByAI] } stairsTrappedDown = TileKind { tsymbol = '>' , tname = "crooked staircase down" , tfreq = [(STAIRCASE_DOWN, 1)] , tcolor = BrRed , tcolor2 = Red , talter = talterForStairs , tfeature = [ Embed STAIRS_DOWN, Embed STAIRS_TRAP_DOWN , ConsideredByAI, ChangeTo ORDINARY_STAIRCASE_DOWN ] } stairsOutdoorDown = stairsDown { tname = "signpost pointing forward" , tfreq = [(STAIRCASE_OUTDOOR_DOWN, 1)] } stairsGatedDown = stairsDown { tname = "gated staircase down" , tfreq = [(GATED_STAIRCASE_DOWN, 1)] , talter = talterForStairs + 2 -- animals and bosses can't use } escapeUp = TileKind { tsymbol = '<' , tname = "escape hatch up" , tfreq = [(TILE_INDOOR_ESCAPE_UP, 1)] , tcolor = BrYellow , tcolor2 = BrYellow , talter = 0 -- anybody can escape (or guard escape) , tfeature = [Embed ESCAPE, ConsideredByAI] } escapeDown = TileKind { tsymbol = '>' , tname = "escape trapdoor down" , tfreq = [(TILE_INDOOR_ESCAPE_DOWN, 1)] , tcolor = BrYellow , tcolor2 = BrYellow , talter = 0 -- anybody can escape (or guard escape) , tfeature = [Embed ESCAPE, ConsideredByAI] } escapeOutdoorDown = escapeDown { tname = "escape back to town" , tfreq = [(TILE_OUTDOOR_ESCAPE_DOWN, 1)] } -- *** Clear wallGlass = TileKind { tsymbol = '|' , tname = "polished crystal wall" , tfreq = [(GLASSHOUSE_VERTICAL_LIT, 1)] , tcolor = BrBlue , tcolor2 = Blue , talter = 10 , tfeature = [BuildAs S_CLOSED_VERTICAL_DOOR_LIT, Clear] } wallGlassSpice = wallGlass { tfreq = [(RECT_WINDOWS_VERTICAL_LIT, 20)] , tfeature = Spice : tfeature wallGlass } wallGlassH = TileKind { tsymbol = '-' , tname = "polished crystal wall" , tfreq = [(GLASSHOUSE_HORIZONTAL_LIT, 1)] , tcolor = BrBlue , tcolor2 = Blue , talter = 10 , tfeature = [BuildAs S_CLOSED_HORIZONTAL_DOOR_LIT, Clear] } wallGlassHSpice = wallGlassH { tfreq = [(RECT_WINDOWS_HORIZONTAL_LIT, 20)] , tfeature = Spice : tfeature wallGlassH } pillarIce = TileKind { tsymbol = '^' , tname = "icy outcrop" , tfreq = [(POWER_SET_DARK, 300)] , tcolor = BrBlue , tcolor2 = Blue , talter = 4 -- boss can dig through , tfeature = [Clear, Embed FROST, OpenTo S_SHALLOW_WATER_LIT] } pulpit = TileKind { tsymbol = '%' , tname = "pulpit" , tfreq = [(S_PULPIT, 1)] , tcolor = BrYellow , tcolor2 = Brown , talter = 5 , tfeature = [Clear, Embed LECTERN] -- mixed blessing, so AI ignores, saved for player fun } bush = TileKind { tsymbol = '%' , tname = "bush" , tfreq = [ (S_BUSH_LIT, 1), (SHOOTOUT_SET_LIT, 30), (FLIGHT_SET_LIT, 40) , (AMBUSH_SET_LIT, 3), (BUSH_CLUMP_LIT, 1) ] , tcolor = BrGreen , tcolor2 = Green , talter = 10 , tfeature = [Clear] } bushBurnt = bush { tname = "burnt bush" , tfreq = [ (BATTLE_SET_DARK, 30), (ZOO_SET_DARK, 30), (AMBUSH_SET_DARK, 3) , (BUSH_BURNING_OR_NOT, 70) ] , tcolor = BrBlack , tcolor2 = BrBlack , tfeature = Dark : tfeature bush } bushBurning = bush { tname = "burning bush" , tfreq = [ (AMBUSH_SET_DARK, 15), (ZOO_SET_DARK, 300) , (BUSH_BURNING_OR_NOT, 30) ] , tcolor = BrRed , tcolor2 = Red , talter = 5 , tfeature = Embed SMALL_FIRE : ChangeTo BUSH_BURNING_OR_NOT : tfeature bush } -- ** Walkable -- *** Not clear fog = TileKind { tsymbol = ';' , tname = "faint fog" , tfreq = [ (S_FOG_LIT, 1), (EMPTY_SET_LIT, 50), (NOISE_SET_LIT, 100) , (SHOOTOUT_SET_LIT, 20) , (FOG_CLUMP_LIT, 60), (FOG_CLUMP_DARK, 60) ] -- lit fog is OK for shootout, because LOS is mutual, as opposed -- to dark fog, and so camper has little advantage, especially -- on big maps, where he doesn't know on which side of fog patch to hide , tcolor = BrCyan , tcolor2 = Cyan , talter = 0 , tfeature = [Walkable, NoItem, OftenActor] } fogDark = fog { tname = "thick fog" , tfreq = [ (EMPTY_SET_DARK, 50), (POWER_SET_DARK, 100) , (FLIGHT_SET_DARK, 50) ] , tfeature = Dark : tfeature fog } smoke = TileKind { tsymbol = ';' , tname = "billowing smoke" , tfreq = [ (S_SMOKE_LIT, 1), (LAB_TRAIL_LIT, 1), (STAIR_TERMINAL_LIT, 4) , (SMOKE_CLUMP_LIT, 3), (SMOKE_CLUMP_DARK, 3) ] , tcolor = Brown , tcolor2 = BrBlack , talter = 0 , tfeature = [Walkable, NoItem] -- not dark, embers } smokeDark = smoke { tname = "lingering smoke" , tfreq = [ (STAIR_TERMINAL_DARK, 4), (AMBUSH_SET_DARK, 40) , (ZOO_SET_DARK, 20), (BATTLE_SET_DARK, 5) ] , tfeature = Dark : tfeature smoke } -- *** Clear doorOpen = TileKind { tsymbol = '-' , tname = "open door" , tfreq = [(S_OPEN_VERTICAL_DOOR_LIT, 1)] , tcolor = Brown , tcolor2 = BrBlack , talter = 4 , tfeature = [ Walkable, Clear, NoItem, NoActor , CloseTo S_CLOSED_VERTICAL_DOOR_LIT -- not explorable due to that ] } doorOpenH = TileKind { tsymbol = '|' , tname = "open door" , tfreq = [(S_OPEN_HORIZONTAL_DOOR_LIT, 1)] , tcolor = Brown , tcolor2 = BrBlack , talter = 4 , tfeature = [ Walkable, Clear, NoItem, NoActor , CloseTo S_CLOSED_HORIZONTAL_DOOR_LIT -- not explorable due to that ] } floorCorridor = TileKind { tsymbol = '#' , tname = "corridor" , tfreq = [(FLOOR_CORRIDOR_LIT, 1)] , tcolor = BrWhite , tcolor2 = defFG , talter = 0 , tfeature = [Walkable, Clear] } floorArena = floorCorridor { tsymbol = floorSymbol , tname = "stone floor" , tfreq = [ (FLOOR_ARENA_LIT, 1), (ARENA_SET_LIT, 1), (EMPTY_SET_LIT, 900) , (ZOO_SET_LIT, 600) ] } floorDamp = floorArena { tname = "damp stone floor" , tfreq = [ (NOISE_SET_LIT, 600), (POWER_SET_LIT, 600) , (DAMP_FLOOR_LIT, 1), (STAIR_TERMINAL_LIT, 20) ] } floorDirt = floorArena { tname = "dirt floor" , tfreq = [ (SHOOTOUT_SET_LIT, 1000), (FLIGHT_SET_LIT, 1000) , (AMBUSH_SET_LIT, 1000), (BATTLE_SET_LIT, 1000) , (BRAWL_SET_LIT, 1000), (DIRT_LIT, 1) ] } floorDirtSpice = floorDirt { tfreq = [(TREE_SHADE_WALKABLE_LIT, 1), (BUSH_CLUMP_LIT, 1)] , tfeature = Spice : tfeature floorDirt } floorActor = floorArena { tfreq = [(S_FLOOR_ACTOR_LIT, 1)] , tfeature = OftenActor : tfeature floorArena } floorActorItem = floorActor { tfreq = [(FLOOR_ACTOR_ITEM_LIT, 1)] , tfeature = VeryOftenItem : tfeature floorActor } floorAshes = floorActor { tfreq = [ (SMOKE_CLUMP_LIT, 2), (SMOKE_CLUMP_DARK, 2) , (S_FLOOR_ASHES_LIT, 1), (S_FLOOR_ASHES_DARK, 1) ] , tname = "dirt and ash pile" , tcolor = Brown , tcolor2 = Brown } shallowWater = TileKind { tsymbol = '~' , tname = "water puddle" , tfreq = [ (S_SHALLOW_WATER_LIT, 1) , (EMPTY_SET_LIT, 5), (NOISE_SET_LIT, 20) , (POWER_SET_LIT, 20), (SHOOTOUT_SET_LIT, 5) ] , tcolor = BrCyan , tcolor2 = Cyan , talter = 0 , tfeature = Embed SHALLOW_WATER : tfeature floorActor } shallowWaterSpice = shallowWater { tfreq = [(FOG_CLUMP_LIT, 40)] , tfeature = Spice : tfeature shallowWater } floorRed = floorCorridor { tsymbol = floorSymbol , tname = "brick pavement" , tfreq = [(TRAIL_LIT, 70), (SAFE_TRAIL_LIT, 70)] , tcolor = BrRed , tcolor2 = Red , tfeature = [Embed STRAIGHT_PATH, Trail, Walkable, Clear] } floorBlue = floorRed { tname = "frozen trail" , tfreq = [(TRAIL_LIT, 100)] , tcolor = BrBlue , tcolor2 = Blue , tfeature = [Embed FROZEN_GROUND, Trail, Walkable, Clear] } floorGreen = floorRed { tname = "mossy stone path" , tfreq = [(TRAIL_LIT, 70), (SAFE_TRAIL_LIT, 70)] , tcolor = BrGreen , tcolor2 = Green } floorBrown = floorRed { tname = "rotting mahogany deck" , tfreq = [(TRAIL_LIT, 50), (SAFE_TRAIL_LIT, 50)] , tcolor = BrMagenta , tcolor2 = Magenta } floorArenaShade = floorActor { tname = "shaded ground" , tfreq = [(S_SHADED_GROUND, 1), (TREE_SHADE_WALKABLE_LIT, 2)] , tcolor2 = BrBlack , tfeature = Dark : NoItem : tfeature floorActor } outdoorFence = TileKind { tsymbol = ' ' , tname = "event horizon" , tfreq = [(OUTDOOR_OUTER_FENCE, 1)] , tcolor = defFG , tcolor2 = defFG , talter = maxBound -- impenetrable , tfeature = [Dark] } -- * Helper functions makeDark :: TileKind -> TileKind makeDark k = let darkenText :: GroupName TileKind -> GroupName TileKind darkenText t = maybe t (GroupName . (<> "Dark")) $ T.stripSuffix "Lit" $ fromGroupName t darkFrequency :: Freqs TileKind darkFrequency = map (first darkenText) $ tfreq k darkFeat (OpenTo t) = Just $ OpenTo $ darkenText t darkFeat (CloseTo t) = Just $ CloseTo $ darkenText t darkFeat (ChangeTo t) = Just $ ChangeTo $ darkenText t darkFeat (OpenWith proj grps t) = Just $ OpenWith proj grps $ darkenText t darkFeat (CloseWith proj grps t) = Just $ CloseWith proj grps $ darkenText t darkFeat (ChangeWith proj grps t) = Just $ ChangeWith proj grps $ darkenText t darkFeat (HideAs t) = Just $ HideAs $ darkenText t darkFeat (BuildAs t) = Just $ BuildAs $ darkenText t darkFeat (RevealAs t) = Just $ RevealAs $ darkenText t darkFeat (ObscureAs t) = Just $ ObscureAs $ darkenText t darkFeat VeryOftenItem = Just OftenItem darkFeat OftenItem = Nothing -- items not common in the dark darkFeat feat = Just feat in k { tfreq = darkFrequency , tfeature = Dark : mapMaybe darkFeat (tfeature k) } makeDarkColor :: TileKind -> TileKind makeDarkColor k = (makeDark k) {tcolor2 = BrBlack} LambdaHack-0.11.0.0/GameDefinition/0000755000000000000000000000000007346545000014751 5ustar0000000000000000LambdaHack-0.11.0.0/GameDefinition/InGameHelp.txt0000644000000000000000000001721207346545000017466 0ustar0000000000000000This is a snapshot of in-game help, rendered with the default config file. For more general gameplay information see https://github.com/LambdaHack/LambdaHack/blob/master/GameDefinition/PLAYING.md Minimal cheat sheet for casual play Walk throughout a level with mouse or numeric keypad (right diagram below) or the Vi editor keys (middle) or the left-hand movement keys (left). Run until disturbed with Shift or Control. Go-to a position with LMB (left mouse button). In aiming mode, the same keys (and mouse) move the aiming crosshair. q w e y k u 7 8 9 \|/ \|/ \|/ a-s-d h-.-l 4-5-6 /|\ /|\ /|\ z x c b j n 1 2 3 Press `KP_5` (`5` on keypad) to wait, bracing for impact, which reduces any damage taken and prevents displacement by foes. Press `S-KP_5` or `C-KP_5` (the same key with Shift or Control) to lurk 0.1 of a turn, without bracing. Displace enemies by running into them with Shift/Control or S-LMB. Search, open, descend and melee by bumping into walls, doors, stairs and enemies. The best, and not on cooldown, melee weapon is automatically chosen for attack from your equipment and from among your body parts. The following few commands, joined with the movement and running keys, let you accomplish almost anything in the game, though not necessarily with the fewest keystrokes. You can also play the game exclusively with a mouse, or both mouse and keyboard (e.g., mouse for go-to and terrain inspection and keyboard for everything else). Lastly, you can select a command with arrows or mouse directly from the help screen or the dashboard and execute it on the spot. keys command I manage the shared inventory stash g or , grab item(s) ESC clear messages/open main menu/finish aiming RET or INS open dashboard/accept target SPACE clear messages/show history/cycle detail level TAB cycle among all party members * cycle crosshair among enemies / cycle crosshair among items M modify any admissible terrain % yell or yawn and stop sleeping Optional mouse commands Screen area and UI mode (exploration/aiming) determine mouse click effects. Here we give an overview of effects of each button over the game map area. The list includes not only left and right buttons, but also the optional middle mouse button (MMB) and the mouse wheel, which is also used over menus to move selection. For mice without RMB, one can use Control key with LMB and for mice without MMB, one can use C-RMB or C-S-LMB. keys command (exploration/aiming) LMB go to pointer for 25 steps/fling at enemy S-LMB run to pointer collectively for 25 steps/fling at enemy RMB or C-LMB start aiming at enemy under pointer/cycle detail level S-RMB modify terrain at pointer MMB or C-RMB snap crosshair to floor under pointer/cycle detail level WHEEL-UP swerve the aiming line WHEEL-DN unswerve the aiming line Note that mouse is optional. Keyboard suffices, occasionally requiring a lookup for an obscure command key in help screens. Mouse button effects per screen area, in exploration and in aiming modes Exploration LMB (left mouse button) RMB (right mouse button) message line show history display help pointman tile grab item(s) remove item(s) party on map pick new pointman on screen select party member on screen the map area go to pointer for 25 steps set crosshair to enemy level number move aiming one level up move aiming one level down level caption open dashboard clear msgs and open main menu percent seen explore nearest unknown spot autoexplore 25 times crosshair info cycle crosshair among enemies cycle crosshair among items party roster pick new pointman on screen select party member on screen Calm value yell or yawn and stop sleeping yell or yawn and stop sleeping HP gauge rest (wait 25 times) heed (lurk 0.1 turns 100 times) HP value wait a turn, bracing for impact lurk 0.1 of a turn pointman info auto-fling and keep choice clear chosen item and crosshair Aiming Mode LMB (left mouse button) RMB (right mouse button) the map area fling at enemy under pointer snap crosshair to enemy level caption accept target cancel aiming percent seen aim at nearest upstairs aim at nearest downstairs All item-related commands keys command I manage the shared inventory stash O manage the equipment outfit of the pointman g or , grab item(s) r remove item(s) f fling in-range projectile C-f auto-fling and keep choice t trigger consumable item C-t trigger item and keep choice i stash item into shared inventory o equip item into outfit of the pointman G manage items on the ground T manage our total team belongings Note how lower case item commands (stash item, equip item) place items into a particular item store, while upper case item commands (manage Inventory, manage Outfit) open management menu for a store. Once a store menu is opened, you can switch stores with `<` and `>`, so the multiple commands only determine the starting item store. Each store is accessible from the dashboard as well. Terrain exploration and modification commands keys command TAB cycle among all party members S-TAB cycle backwards among all party members C-TAB cycle among party members on the level C-S-TAB cycle backwards among party members on the level m modify door by closing it M modify any admissible terrain = select (or deselect) party member _ deselect (or select) all on the level ; go to crosshair for 25 steps : run to crosshair collectively for 25 steps [ explore nearest unknown spot ] autoexplore 25 times R rest (wait 25 times) C-R heed (lurk 0.1 turns 100 times) 0, 1 ... 9 pick a particular actor as the new pointman All aiming commands keys command ESC clear messages/open main menu/finish aiming RET or INS open dashboard/accept target SPACE clear messages/show history/cycle detail level * cycle crosshair among enemies / cycle crosshair among items + swerve the aiming line - unswerve the aiming line \ cycle aiming modes C-? set crosshair to nearest unknown spot C-/ set crosshair to nearest item C-{ aim at nearest upstairs C-} aim at nearest downstairs < move aiming one level up > move aiming one level down BACKSPACE clear chosen item and crosshair Assorted commands keys command % yell or yawn and stop sleeping @ describe organs of the pointman # show skill summary of the pointman ~ display relevant lore C-g start new game C-x save and exit to desktop C-q quit game and start autoplay C-c exit to desktop without saving ? display help F1 display help immediately F12 show history v voice last action again V voice recorded macro again ' start recording commands C-S save game backup C-P print screen For more playing instructions see file PLAYING.md. LambdaHack-0.11.0.0/GameDefinition/Main.hs0000644000000000000000000000506507346545000016177 0ustar0000000000000000-- | The main source code file of LambdaHack the game. -- Module "TieKnot" is separated to make it usable in tests. module Main ( main ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent.Async import qualified Control.Exception as Ex import qualified GHC.IO.Encoding as SIO import qualified Options.Applicative as OA import qualified System.IO as SIO #ifndef USE_JSFILE import qualified GHC.IO.Handle import System.FilePath import Game.LambdaHack.Common.File (tryCreateDir) import Game.LambdaHack.Common.Misc #endif import Game.LambdaHack.Server (serverOptionsPI) import TieKnot -- | Parse commandline options, tie the engine, content and clients knot, -- run the game and handle exit. main :: IO () main = do -- Correct unset or some other too primitive encodings. let enc = SIO.localeEncoding when (show enc `elem` ["ASCII", "ISO-8859-1", "ISO-8859-2"]) $ SIO.setLocaleEncoding SIO.utf8 -- This test is faulty with JS, because it reports the browser console -- is not a terminal, but then we can't open files to contain the logs. -- Also it bloats the outcome JS file, so disabled. #ifndef USE_JSFILE -- Special case hack, when the game is started not on a console. -- Without this, any attempt to output on stdout crashes a Windows exe -- (at least on Windows Vista) launched from the desktop or start menu. -- This is very crude and results in the inability to, e.g., process -- the output of @--help@ through a unix pipe. However, this should be -- effective on all Windows version, without the need to test all. isTerminal <- SIO.hIsTerminalDevice SIO.stdout unless isTerminal $ do dataDir <- appDataDir tryCreateDir dataDir fstdout <- SIO.openFile (dataDir "stdout.txt") SIO.WriteMode fstderr <- SIO.openFile (dataDir "stderr.txt") SIO.WriteMode GHC.IO.Handle.hDuplicateTo fstdout SIO.stdout GHC.IO.Handle.hDuplicateTo fstderr SIO.stderr #else -- Work around display of one character per line. SIO.hSetBuffering SIO.stderr SIO.LineBuffering #endif -- Fail here, not inside server code, so that savefiles are not removed, -- because they are not the source of the failure. !serverOptions <- OA.execParser serverOptionsPI resOrEx :: Either Ex.SomeException () <- Ex.try $ tieKnot serverOptions let unwrapEx e = case Ex.fromException e of Just (ExceptionInLinkedThread _ ex) -> unwrapEx ex _ -> e case resOrEx of Right () -> return () Left ex -> Ex.throwIO $ unwrapEx ex -- we are in the main thread, so now really exit LambdaHack-0.11.0.0/GameDefinition/PLAYING.md0000644000000000000000000006777007346545000016417 0ustar0000000000000000Playing LambdaHack ================== The following backstory blurb is a copy of the sample game intro screen: LambdaHack is a small dungeon crawler illustrating the roguelike game engine of the same name. Playing the game involves exploring spooky dungeons, alone or in a party of fearless explorers, avoiding and setting up ambushes, hiding in shadows from the gaze of unspeakable horrors, discovering secret passages and gorgeous magical treasure and making creative use of it all. The madness-inspiring abominations that multiply in the depths perform the same feats, due to their aberrant, abstract hyper-intelligence. They look out for any sign of weakness or indecision, ready to tirelessly chase the elusive heroes by sight, sound and smell. What to expect -------------- LambdaHack is a turn-based game. You issue a command. Then you watch its results unfold on the screen, without you being able to intervene. Then the dust settles and you have as much time as you want to inspect the battlefield and think about your next move. Once the few basic command keys and on-screen symbols are learned, mastery and enjoyment of the game is the matter of tactical skill and literary imagination. To be honest, a lot of imagination is required for this modest sample game, but it has its own distinct quirky mood and is playable and winnable. The game differs from classic roguelikes in a few ways: * player manually controls each member of his squad, though often the best tactics is to scout with only one character (a classic roguelike feel) and let others guard important areas * the game is turn-based, but with visibly high granularity --- projectiles fly gradually over time with varying speeds and can be sidestepped or shot down; less so explosions that are swarms of projectile particles (turn-based just the same) * time passes and factions pursue their goals on a few levels simultaneously, while other floors are frozen (but all are persistent) * the same laws of simulated world apply to all factions and all actors, whether player-controlled or AI-controlled; e.g., the same field of view calculation, skill checks, equipment limitations, rules for item and terrain use * combat mechanics is deterministic; randomness comes only from AI decisions and procedurally generated world * there's (almost) no HP regeneration; attrition ensures all past (silly) decisions matter; HP of every actor starts at around half max * each character has 10 uniform equipment slots, which fill quickly given that most melee weapons have cooldowns * each faction has a single shared inventory of unlimited size, which has a physical location on the map and so can be ransacked If the game window is too large for your screen or the game doesn't start or you experience other technical issues, please consult [README.md](https://github.com/LambdaHack/LambdaHack/blob/master/README.md) or describe your problem on [Discord](https://discord.gg/87Ghnws) or [Matrix](https://matrix.to/#/!HnbpAHMjOGHlYtrASl:mozilla.org) or the issue tracker. Contributions of all kinds are welcome. Please offer feedback to mikolaj.konarski@funktory.com or, preferably, on any of the public forums. Starting your adventure ----------------------- Commands for starting a new game, saving and exiting the current game, configuring convenience settings and toggling AI control of the party are listed in the main menu, brought up by the Esc key. Game difficulty level, from the new game setup menu, determines how hard the survival in the game is. Each of the several named optional challenges make the game additionally much harder, but usually simpler, as well. Not that in-game hints don't take challenges into account so kindly ignore, e.g., advice to use ranged combat more often, if the chosen challenge bans ranged combat use altogether. Of the convenience settings, the `suspect terrain` choice is of particular interest, because it determines not only screen display of the floor map, but also whether suspect tiles are considered for mouse go-to, auto-explore and for the `C-?` command that marks the nearest unexplored position. Game scenarios, as ordered by their number, lead the player along an optional story arc. The first two adventures double as tutorials that offer rudimentary preparation for the main game, the long crawl. They gradually introduce exploration, stealth and melee combat, helping the player develop his repertoire of squad formations and move patterns, suitable for different tactical contexts. When the player loses, a defeat message for the scenario appears with hints about strategies known to work in the given tactical context. Alternatively, the player may postpone reading these messages and instead try to puzzle out the tactics himself --- this is not so hard, as there are not yet so many moving parts to figure out in the first two adventures. In the third scenario, the main 'crawl' game mode, the player starts employing ranged combat, stealth, light sources, item and terrain alteration. As soon as the player learns to navigate the initial levels of crawl, but still dies a lot, it makes sense to return to the remaining short adventures. They bring forth many extra game features and tactics and prevent the player from missing half the fun by trying to play the crawl just like a normal roguelike with spare heroes. The extra scenarios continue the plotline from the initial tutorial adventures in the form of flashbacks and eventually lead up to the events that start the main crawl adventure. The training they provide has narrow focus, drilling a particular skill set, even as exotic as opportunity fire management, a frantic race to the exit and big asymmetric melee battles. The challenge the scenarios offer may be, accordingly, quite extreme, particularly at higher difficulty settings and when striving for high scores. The main adventure, the long crawl, is the only one that takes place in a multi-floor setting, requiring lots of time to beat. The focus is on resource management and survival, including terrain transformation using tools, spotting environment clues and guessing and countering opponents' strategies. The player has a choice of exploring a single level at a time or portions of many floors along a single staircase. On some levels he may explore and loot with a single scout, eluding most opponents. On others he may be forced to change pace and perform a complete exterminatory sweep involving his whole party. On yet others, his best course of action may be to defend a key location until the first wave of attackers is broken. The large game arena calls for strategic thinking, including resource management and area denial. Thus, the crawl scenario is the most replayable adventure, but even the small ones can be refreshed by striving to beat a high score and by ramping up the difficulty settings. Exploring the world ------------------- The map of any particular adventure consists of one or many levels and a level consists of a number of tiles with a particular terrain kind on each. The game world is persistent, i.e., every time the player visits a level during a single game, its layout is the same (unless modified by other actors). Letters and digits on the game screen are likely to represent actors. On the other hand, terrain is depicted with non-letter and non-digit characters and with zero `0`. Blocky solid symbols are likely to be non-passable and/or not translucent terrain. White, cyan and green terrain is usually inert, red is burning or trapped, blue activable or trapped, magenta activable or searchable. Items lying on the ground are represented with non-letter and non-digit characters, just as terrain, though rarely with blocky symbols. In case of doubt, one of the aiming commands (`/` and `KP_/`, that is, `/` on the keypad) cycles through all visible and remembered items on the level and another (`*` and `KP_*`, all with default keybindings) through all foes. Also, pointing at a map position with MMB (middle mouse button) displays a short description of its contents. Pointing with RMB enters aiming mode, in which pointing again or pressing Space key or MMB decreases detail level of the description. If a foe or interesting terrain is being pointed at, tilde key `~` shows the relevant lore details. The basic terrain kinds are as follows. terrain type on-screen symbol bush, transparent obstacle % trap, ice obstacle ^ wall (horizontal and vertical) - and | bedrock blank tree, rock, man-made column 0 rubble & stairs, exit up < stairs, exit down > closed door + open door (horizontal and vertical) | and - corridor # ground . water, other fluid ~ smoke, fog, open fire ; workshop, curtain, foliage : The four groups above, from top to bottom, block movement but not view, block both, block neither, block view but not movement. Additionally, each tile, regardless if open and if translucent, may be permanently lit with ambient light or not. Actors are marked with lower and upper case letters and with characters `@` and `1` through `9` (but never `0`). Player-controlled heroes are always bright white and at game start they are selected (e.g., to run together) so they have a green highlight around their symbol. If player manages to take control of animals or other actors, they retain their letter and color, but gain a highlight as well. So, for example, the following map shows a room with a closed door, full of actors, connected by a corridor with a room with an open door, a pillar, a staircase down and rubble that obscures one of the corners. The lowest row of the larger room is full of items. ------ ------ |@19.| |....&& |r...+#######-...0.>&&| |Ra..| |[?!,)$"=| ------ ---------- Leading your heroes ------------------- The heroes are displayed on the map with bright white color (red if they are seriously wounded) and symbols `@` and `1` through `9` (never `0`). The currently chosen party pointman is highlighted on the map with yellow. The easiest way to control your team is to run a short distance with your pointman using Shift-direction or LMB, switch the pointman with the Tab key, repeat. In open terrain, if you keep consistent distance between teammates, this resembles the leap-frog infantry tactics, in which the immobile team members cover the movement of the others. For best effects, try to end each sprint behind cover or concealment (note that a thin pillar is neither, but a single shadowed spot may be enough to hide in the dark). Pointman hero's attributes are displayed at the bottom-most status line which, in its most complex form, looks as follows. *@12 2m/s Calm: 20/60 HP: 33/50 Pointman: Haskell Alvin 6d1+5% 4d1 The line starts with the list of party members, with the current pointman highlighted in yellow. Most commands involve only the pointman, including movement with keyboard or keypad or LMB (left mouse button). If more heroes are selected (highlighted in green), they run together whenever `:` or S-LMB (LMB while holding down Shift) over map area is pressed, though that's usually not a precise enough method of controlling a team. Any sleeping hero is highlighted in blue and can be woken up by yelling with `%` key, which also taunts and unnerves nearby enemies. Next on the bottom-most status line is the pointman's current and maximum Calm (morale, composure, focus, attentiveness), then his current and maximum HP (hit points, health). The colon after "Calm" turning into a dot signifies that the pointman is in a position without ambient illumination, making stealthy conduct easier. A brace sign instead of a colon after "HP" means the pointman is braced for combat (see chapter [Moving and acting](#Moving-and-acting)). In the second half of the bottom-most status line, the pointman's name is shown. Then come damage dice of the pointman's melee weapons and the pointman's appendages, ordered by their power. The dice of the first recharged weapon, the one that is going to be used now, is adorned with percentage damage bonus collected from the whole equipment of the pointman. If the dice are displayed with upper-case `D` instead of lower-case `d`, the weapon has additional effects apart of the usual direct damage. The nature of the effects can be appraised via the equipment outfit menu. Only the most common piercing direct damage, denoted by the damage dice, is affected by the percentage damage bonus. The other direct damage kinds, such wounding and burning, are represented by extra added integers and are not scaled by bonuses from melee skill nor maluses from the opponent's armor. Weapon damage and other item properties are displayed using the dice notation `xdy`, which denotes `x` rolls of `y`-sided dice. A variant written `xdLy` is additionally scaled by the level depth in proportion to the maximal level depth (at the first level the result is always one; it grows up to the full rolled value at the last level). Section [Battling monsters](#Battling-monsters) below describes combat resolution in detail, including the role of the percentage bonuses. The upper status line describes the currently visited level in relation to the party. 5 Lofty hall [33% seen] dire basilisk [__**] First comes the depth of the current level and its name. Then the percentage of its explorable tiles already seen by the heroes. Then the common focus of the whole party, coming from the aiming crosshair marked on the map with a red box and manipulated with mouse or movement keys in aiming mode. In this example, the crosshair points at a dire basilisk monster with its hit points drawn as a half-full bar. Instead of a monster, the aiming crosshair status area may describe a position on the map, a recently spotted item on the floor or an item in inventory selected for further action or, if none are available, a summary of the team composition. For example, this form 5 Lofty hall [33% seen] spot (71,12) p15 l10 indicates that the party is aiming at an exact spot on the map. At the end of this example status line comes the length of the shortest path from the pointman's position to the spot in crosshair and the straight-line distance between the two points, one that a flung projectile would travel if there were no obstacles. Moving and acting ----------------- This chapter is a copy of the few initial pages of in-game help. The in-game help is automatically generated based on a game's keybinding content definitions and on overrides in the player's config file. The remaining in-game help screens, not shown here, list all game commands grouped by categories in detail. Walk throughout a level with mouse or numeric keypad (right diagram below) or the Vi editor keys (middle) or the left-hand movement keys (left). Run until disturbed with Shift or Control. Go-to a position with LMB (left mouse button). In aiming mode, the same keys (and mouse) move the aiming crosshair. q w e y k u 7 8 9 \|/ \|/ \|/ a-s-d h-.-l 4-5-6 /|\ /|\ /|\ z x c b j n 1 2 3 Press `KP_5` (`5` on keypad) to wait, bracing for impact, which reduces any damage taken and prevents displacement by foes. Press `S-KP_5` or `C-KP_5` (the same key with Shift or Control) to lurk 0.1 of a turn, without bracing. Displace enemies by running into them with Shift/Control or S-LMB. Search, open, descend and melee by bumping into walls, doors, stairs and enemies. The best, and not on cooldown, melee weapon is automatically chosen for attack from your equipment and from among your body parts. The following few commands, joined with the movement and running keys, let you accomplish almost anything in the game, though not necessarily with the fewest keystrokes. You can also play the game exclusively with a mouse, or both mouse and keyboard (e.g., mouse for go-to and terrain inspection and keyboard for everything else). Lastly, you can select a command with arrows or mouse directly from the help screen or the dashboard and execute it on the spot. keys command I manage the shared inventory stash g or , grab item(s) ESC clear messages/open main menu/finish aiming RET or INS open dashboard/accept target SPACE clear messages/show history/cycle detail level TAB cycle among all party members * cycle crosshair among enemies / cycle crosshair among items M modify any admissible terrain % yell or yawn and stop sleeping Screen area and UI mode (exploration/aiming) determine mouse click effects. Here we give an overview of effects of each button over the game map area. The list includes not only left and right buttons, but also the optional middle mouse button (MMB) and the mouse wheel, which is also used over menus to move selection. For mice without RMB, one can use Control key with LMB and for mice without MMB, one can use C-RMB or C-S-LMB. keys command (exploration/aiming) LMB go to pointer for 25 steps/fling at enemy S-LMB run to pointer collectively for 25 steps/fling at enemy RMB or C-LMB start aiming at enemy under pointer/cycle detail level S-RMB modify terrain at pointer MMB or C-RMB snap crosshair to floor under pointer/cycle detail level WHEEL-UP swerve the aiming line WHEEL-DN unswerve the aiming line Note that mouse is optional. Keyboard suffices, occasionally requiring a lookup for an obscure command key in help screens. Battling monsters ----------------- The life of heroes is full of danger. Monstrosities, natural and out of this world, roam the dark corridors and crawl from damp holes day and night. While heroes pay attention to all other party members and take care to move one at a time, monsters don't care about each other and crowd and stampede all at once, sometimes brutally colliding by accident. Monsters are depicted on the map with letters. Upper case letters are unique monsters, often guardians of special floors, resources and keys to other areas. Lower case letters are the rabble. If there are humans not from our team, they are marked with `@` and `1` through `9` in other colours than white. When a hero walks and bumps into a monster or a monster attacks the hero, melee combat occurs. Hero *running* into and displacing a monster (with the `Shift` key and, in case of keypad movement, alternatively a `Control` key), does not involve inflicting a damage, but only causes an exchange of places. This gives the opponent a free blow, but can improve the tactical situation or aid escape. In some circumstances actors are immune to the displacing, e.g., when both parties form a continuous front-line. In melee combat, the best recharged equipped weapon (including fighting organs that are not on cooldown) is taken into account for determining the damage and any extra effects of the blow. To calculate the damage dealt, the outcome of the weapon's direct piercing damage dice roll (but not any additional direct damage summands such as wounding or burning) is multiplied by a percentage bonus. The total bonus is calculated by taking the damage bonus (summed from the equipped items and organs and conditions of the attacker, capped at 200%) minus the melee armor modifier of the defender (capped at 200%, as well). However, at least 5% of damage always gets through, even if the bonus is nominally below -95%, so excessively strong armor acts only as a buffer against high melee skill of opponents. The current pointman's melee bonus, armor modifier and other detailed stats can be viewed in the skill menu, accessible via the `#` command, which summarizes all the stats conferred by organs and conditions listed in the organ menu, invoked by `@`. In ranged combat, the projectile is assumed to be attacking the defender in melee, using itself as the weapon, with the usual dice and damage bonus. 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. In-game detailed item descriptions contain melee and ranged damage estimates. They do not take into account enemy armor nor damage from effects and, if bonuses are not known, guesses are based on averages for the item kind 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. 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 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. Nevertheless, sometimes before flinging you want to examine the level map tile by tile by moving the crosshair or to assign persistent personal targets to party members. The latter is essential in the rare cases when your henchmen (non-pointman characters) can move autonomously or fire opportunistically (via innate skills or rare equipment). Also, if your non-pointman character is adjacent to more than one enemy, setting his target makes him melee a particular foe. You can enter the aiming mode with the `*` and `KP_*` keys that select enemies or the `/` and `KP_/` keys that cycle among items on the level. You can move crosshair with direction keys and assign a personal target to the pointman with the `RET` key (Return, Enter). The details about the shared crosshair position are displayed in a status line close to the bottom of the screen, as explained in chapter [Leading your heroes](#Leading-your-heroes) above. You cycle aiming mode from foe to spot and to vector with the ``\`` key, which is useful, e.g., when a monster vanishes but you still want to fling at its last known position. 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 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). 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. Lead the new hopeful explorers with wisdom and fortitude! FAQ --- - Q: Why do I summon hostile animals all the time, why do I defect to the enemy faction every level, why am I constantly sabotaging my own adventure, what is going on? A: Whenever anything bad happens, notice it and use it as a learning experience. Especially if it happens often or periodically. Check carefully the messages overlaid on the map and in history log, look at your outfit, organs, stats. Observe coincidences. Build conjectures. Deduce. Prevent. Adjust. Win. Whomever told you bumping is enough, lied. - Q: Why is my hero immobile? A: Perhaps he's just sleeping (blue box indicates that)? If so, you can wake him up with the `%` command. If he's not asleep, his movement skill may be temporarily drained. Switch to another hero or perform some other productive action different from walking or wait with `KP_5` or rest with `R`. - Q: Is autoexplore safe? A: Not at all. It doesn't try to guess which hazardous terrain you want to avoid and which to barge through, so be prepared to abort exploration if open fire or slippery ground comes into view. Unless you have HP to spare. Oh the other hand, running is very safe and go-to is rather safe. - Q: Why does the percentage of explored tiles turn from 100% to 99%? A: Apparently enemies transformed a tile from unexplorable terrain kind to explorable. The new tile has never been seen by the player, so the percentage is no longer at 100%. - Q: Why when a single hero gets ambushed and is fighting at close quarters, his distant teammates don't jointly come to his rescue. A: The teammates wait for him to come back into the formation instead so that they may assume a front line and then melee their foes together. The immobile heroes are assumed to be pinned to their positions by fear and shock, but also by their imperative to hold formation, so as to defend an important position or avoid running piecemeal into a trap or into friendly fire or avoid breaking concealment and revealing their position or leaving a vantage point from which they can observe and relay enemy movement. For untrained teams, simultaneous synchronized squad movement is not feasible. It would be practical if all squaddies had cameras, with a few drones overhead for best effect, and if a team of off-site coordinators analyzed the situation and micromanaged them all. This is not the case here. - Q: Why is the noise I'm hearing "indistinct"? A: That's because it's out of direct hearing range of each teammate on the level, but ponderous enough to be perceived by all as vibrations and echos from afar. Any other noise adjective indicates that the noise is heard by at least one teammate and how far it is from the pointman (who may or may not hear it directly, as signalled by his Calm drop). - Q: Why are there two 'weakened' conditions in the organ menu of my hero? A: Each team has a different recipe for their weakness brew. Consequently, multiple affliction by the concoction from a single team prolongs a single malady, but affliction by concoctions from many teams causes concurrent ailments, with compounded effects, but independent and short durations. The benefit of the mechanism is that it's possible to tell the perpetrator team of any ailment. The exceptions are the conditions that activate each turn, e.g., healing (regeneration, various resistances that effectively cure each turn) or wounding (poison). These are similar regardless of the team and so the condition is always only prolonged. - Q: Why is a harpoon in my shared inventory stash charging for hundreds of turns? A: This is an artifact of time running independently on each level. Any ideas on how to improve this game mechanics are welcome. A workaround is to drop and then pick up the item on the level you want to use it. When picked up, it gets recharged after, randomly, from one to two times the normal cooldown period of the item and then recharges normally while it's used on this level. - Q: Why the bottom line displays a weapon with a timeout to the right of a weapon without timeout? Doesn't it mean the former is never used? A: Yes, it's never used and, quite possibly, it's your party inventory management mistake and if not, at least a very special situation and the display turns your attention to it. Shuffle the equipment among your team if you want the weapon to get used. LambdaHack-0.11.0.0/GameDefinition/config.ui.default0000644000000000000000000001506707346545000020211 0ustar0000000000000000; This is a copy of the default UI settings config file ; that is embedded in the game binary. A user config file can override ; these options. Option names are case-sensitive and only ';' for comments ; is permitted. ; ; The game looks for the config file at the same path where saved games ; directory is located. E.g. on Linux the file is at ; ~/.LambdaHack/config.ui.ini ; and on Windows it can be at ; C:\Documents And Settings\user\Application Data\LambdaHack\config.ui.ini ; or at ; C:\Users\\AppData\Roaming\LambdaHack\config.ui.ini ; or elsewhere. [additional_commands] ; Angband compatibility (accept target) Cmd_2 = ("KP_Insert", ([CmdAim], "", ByAimMode AimModeCmd {exploration = Dashboard, aiming = Accept})) ; Custom key rebinding example: ; ; Cmd_3 = ("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 ; "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 ; in-game alongside other keys related to handling items. ; If there is no need to have the new binding displayed ; 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 [hero_names] HeroName_0 = ("Haskell Alvin", "he") HeroName_1 = ("Alonzo Barkley", "he") HeroName_2 = ("Inés Galenti", "she") HeroName_3 = ("Ernst Abraham", "he") HeroName_4 = ("Samuel Saunders", "he") HeroName_5 = ("Roger Robin", "he") HeroName_6 = ("Christopher Flatt", "he") HeroName_7 = ("Simon Wise", "he") HeroName_8 = ("Daniel Homer", "he") HeroName_9 = ("Oleg Cracker", "he") [ui] ; Disable these to free up some keys, if you want to rebind commands ; and/or to avoid moving due to accidental key presses. movementViKeys_hjklyubn = True movementLeftHandKeys_axwdqezc = True ; recommended: "binary" or "dejavuBold" chosenFontset = "dejavuBold" ; 1.0 means don't scale; 1.5, 2.0, 3.0 give good results, <0.7 very bad ; for scales < 1 try fontsets with bolder auxiliary fonts and/or HintingLight allFontsScale = 1.0 ; NotFullscreen (default), BigBorderlessWindow (preferred), ModeChange. ; For 1920×1080 fullscreen, set allFontsScale = 1.5 above. fullscreenMode = NotFullscreen ; HP percent at which warning is emitted. hpWarningPercent = 20 ; Wrap messages after this column (measured in small font, if available). ; In LambdaHack, not enough height to fit longer messages if wrapped earlier. msgWrapColumn = 80 ; New historyMax takes effect after removal of savefiles. ; Looks best if is divisble by screenful of message (rheight - 4 = 20). historyMax = 5000 ; Frames per second throttled at this value. maxFps = 24 ; Animations when actors act are not displayed. noAnim = False ; Hardwired commandline arguments to process. overrideCmdline = "" [fonts] ; the auxiliary fonts (the format is: kind name size hinting): binaryRegular = FontProportional "BinarySansProLH-Regular.ttf.woff" 16 HintingHeavy binaryBold = FontProportional "BinarySansProLH-Semibold.ttf.woff" 16 HintingHeavy binaryMono = FontMonospace "BinaryCodeProLH-Bold.ttf.woff" 14 HintingHeavy dejavuRegular = FontProportional "DejaVuLGCSans.ttf.woff" 15 HintingHeavy dejavuBold = FontProportional "DejaVuLGCSans-Bold.ttf.woff" 13 HintingHeavy dejavuMono = FontMonospace "Hack-Bold.ttf.woff" 13 HintingHeavy ; the map fonts (the format is: kind name size hinting cellSizeAdd): ; with allFontsScale < 1, try HintingLight for fuzzy, but less distorted shapes: 16x16xwScalable = FontMapScalable "16x16xw.woff" 16 HintingHeavy 0 ; the map fonts (the format is: kind name cellSizeAdd): 16x16xwBitmap = FontMapBitmap "16x16xw.bdf" 0 8x8xbBitmap = FontMapBitmap "8x8xb.fnt" 2 8x8xBitmap = FontMapBitmap "8x8x.fnt" 2 [fontsets] ; This is a temporary hack around bitmap font breakage caused by ; https://gitlab.freedesktop.org/freetype/freetype/-/issues/1076 ; In a couple of years this can be removed and the small bitmap fonts will work ; fine again, too. dejavuBold = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwScalable", fontPropRegular = "dejavuBold", fontPropBold = "dejavuBold", fontMono = "dejavuMono"} ; best proportional: binary = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "binaryRegular", fontPropBold = "binaryBold", fontMono = "binaryMono"} dejavuBold_original = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "dejavuBold", fontPropBold = "dejavuBold", fontMono = "dejavuMono"} ; decent proportional: binaryBold = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "binaryBold", fontPropBold = "binaryBold", fontMono = "binaryMono"} dejavu = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "dejavuRegular", fontPropBold = "dejavuBold", fontMono = "dejavuMono"} ; monospace, for people that don't like proportional or many fonts: binaryMono = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "binaryMono", fontPropBold = "binaryMono", fontMono = "binaryMono"} dejavuMono = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "dejavuMono", fontPropBold = "dejavuMono", fontMono = "dejavuMono"} ; square: 16x16xw = FontSet {fontMapScalable = "16x16xwScalable", fontMapBitmap = "16x16xwBitmap", fontPropRegular = "", fontPropBold = "", fontMono = ""} 8x8xb = FontSet {fontMapScalable = "", fontMapBitmap = "8x8xbBitmap", fontPropRegular = "", fontPropBold = "", fontMono = ""} 8x8x = FontSet {fontMapScalable = "", fontMapBitmap = "8x8xBitmap", fontPropRegular = "", fontPropBold = "", fontMono = ""} [message_colors] ; Prefixes of message class constructor names paired with colors. ; The first prefix that matches, wins. ; ; E.g., uncomment to make all messages white: ; Msg = White [version] ; If an outdated config file mentions fonts that are not bundled with the game ; any more, the game crashes. To prevent that, one of the first three components ; of the game version should be bumped whenever fonts are changed. ; Configs from old versions are rejected, preventing the crash. version = 0.11.0 LambdaHack-0.11.0.0/GameDefinition/fonts/0000755000000000000000000000000007346545000016102 5ustar0000000000000000LambdaHack-0.11.0.0/GameDefinition/fonts/16x16x.fnt0000644000000000000000000002225507346545000017576 0ustar0000000000000000­$Mikolaj Konarski and others 2019; released under GNU GPL-2 `` X0ÿÿ ž$~~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ > ^ ~ ž ¾ Þ þ  > ^ ~ ž ¾ Þ þ  > ^ ~ ž ¾ Þ þ  > ^ ~ ž ¾ Þ þ  > ^ ~ ž ¾ Þ þ >^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ>^~ž¾Þþ > ^ ~ ž ¾ Þ þ !>!^!~!ž!¾!Þ!þ!">"^"~"ž"¾"Þ"þ"#>#^#~#ž#¾#Þ#þ#$>$^$  €€ÀÀÀÀ€€€€À€0000 ?? ?? 00üü0000üü00111€ð˜€€€ð˜ŒŒ˜ð€6"6 0`À€8lDl888À`0`ÀÆl88|΀ÀÀ€ðÀ€€Àð€ÀàààààÀ€0pàøøàp0€€€€øø€€€€€ÀÀ€øø€ÀÀ€ 80 0`À€0001000ð Œ ð?ÀÀÀÀÀÀÀÀÀÀþ800?ð 8àü8ð ø  ø00000 ü ?000?8üð ð 00?000ðð ð?þ8pàÀ€  00à00ð ð008ð ü ð€ÀÀ€€ÀÀ€€ÀÀ€€ÀÀ€888à€€à8øøøøÀppÀ ð 8à€€À€8133318ð üøð ~ÀÀ` 0ø ?ð ø  ø0000000ø  øà0 0àüðü~üð0000000ð ÿ ð|| ü ??ü€€€€€€€€€üp? ð||>0`ÀÀ`0 ~þx|0`À€À`0>| >0`À€€€€€à?`?þ8pàÀ€þðð08 €À`0 à`````````à 0€À`0 þþ€Ààp00ø ü çxsø ü0````0ø ü0000< ü ç0```0ø øü?üøÀ00pç ü øx~ø ÀÀÀÀÀÀÀÀÿp ü øx~0`Àà0?þü~111111qpŒŒŒŒŒŒs~ð 0````0ø  øsxø ü0000ì ü y ü€88püø üøüx< ç|  0`ÀÀx ÌÌx0|| 0ÀÀ0 xp? ü ø?`8?þ8à€þð€€€€€€ð€€€€€€€€€€€€€€€ÀpÀ€€€90 œøð?? ??øüÀÀÀÀüø€€€øø€€€€€€€€€€€øø€€€øø€€€000`Àø ø  øpÀÀp0000000üÆÀÀÀøÀÀÀÆü?`?`Àü8pàÀ€ü€ÀÀ€88p`À€üø üÀpÀ3aaaa3|Ɔü€€Æ|?`8?`À€þ8à€þ| ``>0`À€€€€à€À€€€€ÀÀÀÀ€1111€€øŒ€€€€Œø€€?7?ðø ÀÀ€øø  € P ØØ P €08 8pàøø€øø€€ðøàðèø8ðà``3666663ðÌl lÌðð00üü8Ž8à8Ž?ü 7447544ðÌl,ÌŒÌlðàà à00à€€€øø€€€øøð8à€øðððÀ€  8 ç>>>ü0000000000€€À€€€€€€€øððø88à8Ž8àxppÀff~xppÀ|<`~||ppÀff~ €À€€à8 ð ~€ÀÀ` 0ø ? ~À€À` 0ø ? ~À`À` 0ø ? ~`ÀÀ` 0ø ? ~00À` 0ø ? ~À€À` 0ø ? 00yüÆÀÀÀøÀÀÀÆü0000000ø  øÀ€€ÀüðüÀ€üðüÀ`üðü``üðü??€ü€€€€€€€€ü??À€ü€€€€€€€€ü??À`ü€€€€€€€€ü??``ü€€€€€€€€üà0 Œ 0àx~`À? ŒÌl<  0000 €à0 0à 0000 À€à0 0à 0000 À`à0 0à 0000 `Àà0 0à 0000 ``à0 0à  0pàÀÀàp0 001367ì0hÌŒ 0à| €À ð| À€ ð| À` ð| 00 ð| À€>0`À€€€€ààð88ðàøü8pàp8|øp00€€ø ü çp00À€ø ü çp00€À`ø ü çp00`Àø ü çp00``ø ü çp00€@€ø ü ç>c1aa>|Ɔü€€Æ|0````0ø üÀ€0```0€€ø øü0```0À€ø øü0```0€À`ø øü0```0``ø øü€€ÀÀÀÀÀÀÿ`À€ÀÀÀÀÀÀÿ€À`ÀÀÀÀÀÀÿ00ÀÀÀÀÀÀÿ000`€À`0ø ðs~°àð 0````0€€ø  ø0````0À€ø  ø0````0€À`ø  ø0````0`Àø  ø0````0``ø  ø€€øø€€0038/ú<æ† øx€€< çxÀ€< çx€À`< çx``< çxpÀ€? ü øàøøàxp``? ü øAngband 16x16xLambdaHack-0.11.0.0/GameDefinition/fonts/16x16xw.bdf0000644000000000000000000017643407346545000017742 0ustar0000000000000000STARTFONT 2.1 FONT -Angband-16x16x-Medium-R-Normal--16-120-100-100-C-80-ISO10646-1 SIZE 12 100 100 FONTBOUNDINGBOX 16 16 0 -3 STARTPROPERTIES 20 FAMILY_NAME "16x16x" FOUNDRY "Angband" WEIGHT_NAME "Medium" SLANT "R" SETWIDTH_NAME "Normal" FONT_VERSION "1.0" COPYRIGHT "Mikolaj Konarski, Leon Marrick and others 2019; released under GNU GPL-2" CAP_HEIGHT 13 PIXEL_SIZE 16 POINT_SIZE 120 X_HEIGHT 8 RESOLUTION_X 100 RESOLUTION_Y 100 SPACING "C" AVERAGE_WIDTH 80 WEIGHT 10 QUAD_WIDTH 16 DEFAULT_CHAR 32 FONT_DESCENT 3 FONT_ASCENT 13 ENDPROPERTIES CHARS 385 STARTCHAR U+0007 ENCODING 7 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0000 0180 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+0020 ENCODING 32 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+0021 ENCODING 33 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 03C0 03C0 03C0 03C0 0180 0180 0180 0000 0180 03C0 0180 0000 0000 ENDCHAR STARTCHAR U+0022 ENCODING 34 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0630 0630 0630 0630 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+0023 ENCODING 35 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0C30 0C30 3FFC 3FFC 0C30 0C30 0C30 0C30 3FFC 3FFC 0C30 0C30 0000 0000 ENDCHAR STARTCHAR U+0024 ENCODING 36 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0180 0FF0 1998 3180 3180 1980 0FF0 0198 018C 018C 3198 1FF0 0180 0000 0000 ENDCHAR STARTCHAR U+0025 ENCODING 37 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 1C18 3618 2230 3660 1CC0 0180 0338 066C 0C44 186C 1838 0000 0000 0000 ENDCHAR STARTCHAR U+0026 ENCODING 38 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07C0 0E60 1C30 0E60 07C0 0FC6 1C6C 3838 3838 1C7C 0FCE 0000 0000 0000 ENDCHAR STARTCHAR U+0027 ENCODING 39 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0180 03C0 03C0 0180 0300 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+0028 ENCODING 40 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 00F0 01C0 0380 0700 0700 0700 0700 0700 0380 01C0 00F0 0000 0000 0000 ENDCHAR STARTCHAR U+0029 ENCODING 41 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0F00 0380 01C0 00E0 00E0 00E0 00E0 00E0 01C0 0380 0F00 0000 0000 0000 ENDCHAR STARTCHAR U+002A ENCODING 42 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0630 0770 03E0 0FF8 0FF8 03E0 0770 0630 0000 0000 0000 0000 ENDCHAR STARTCHAR U+002B ENCODING 43 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0180 0180 0180 0180 1FF8 1FF8 0180 0180 0180 0180 0000 0000 0000 ENDCHAR STARTCHAR U+002C ENCODING 44 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0180 03C0 03C0 0180 0700 0000 ENDCHAR STARTCHAR U+002D ENCODING 45 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 1FF8 1FF8 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+002E ENCODING 46 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0180 03C0 03C0 0180 0000 0000 ENDCHAR STARTCHAR U+002F ENCODING 47 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 000C 001C 0030 0060 00C0 0180 0300 0600 0C00 3800 3000 0000 0000 0000 ENDCHAR STARTCHAR U+0030 ENCODING 48 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1818 300C 300C 300C 318C 300C 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+0031 ENCODING 49 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 00C0 01C0 0FC0 00C0 00C0 00C0 00C0 00C0 00C0 00C0 3FFE 0000 0000 0000 ENDCHAR STARTCHAR U+0032 ENCODING 50 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 3818 000C 000C 0038 01E0 0700 1C00 3000 3006 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0033 ENCODING 51 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07F0 1C18 000C 000C 0018 03F8 000C 0006 0006 380C 0FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0034 ENCODING 52 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 300C 300C 300C 300C 300C 1FFC 000C 000C 000C 000C 000C 0000 0000 0000 ENDCHAR STARTCHAR U+0035 ENCODING 53 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFC 3000 3000 3000 3FF0 0018 000C 000C 000C 3818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+0036 ENCODING 54 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07F0 0C18 1800 3000 3000 3FF0 3018 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+0037 ENCODING 55 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFE 000E 001C 0038 0070 00E0 01C0 0380 0700 0E00 1C00 0000 0000 0000 ENDCHAR STARTCHAR U+0038 ENCODING 56 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07E0 0C30 1818 1818 0C30 0FF0 1818 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+0039 ENCODING 57 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1818 300C 300C 180C 0FFC 000C 000C 000C 3818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+003A ENCODING 58 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0180 03C0 03C0 0180 0000 0000 0180 03C0 03C0 0180 0000 0000 ENDCHAR STARTCHAR U+003B ENCODING 59 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0180 03C0 03C0 0180 0000 0000 0180 03C0 03C0 0180 0700 0000 ENDCHAR STARTCHAR U+003C ENCODING 60 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0038 00E0 0380 0E00 3800 3800 0E00 0380 00E0 0038 0000 0000 0000 ENDCHAR STARTCHAR U+003D ENCODING 61 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 1FF8 0000 0000 1FF8 1FF8 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+003E ENCODING 62 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 1C00 0700 01C0 0070 001C 001C 0070 01C0 0700 1C00 0000 0000 0000 ENDCHAR STARTCHAR U+003F ENCODING 63 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07F0 0C18 180C 000C 000C 0038 00E0 0180 0000 0180 03C0 0180 0000 0000 ENDCHAR STARTCHAR U+0040 ENCODING 64 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1C18 380C 31FC 331C 331C 331C 31F8 3800 1C18 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+0041 ENCODING 65 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FC0 01C0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+0042 ENCODING 66 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FF0 1818 180C 180C 1818 1FF8 180C 1806 1806 180C 7FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0043 ENCODING 67 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF8 180C 3006 3000 3000 3000 3000 3000 3006 180C 0FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0044 ENCODING 68 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FE0 1830 1818 180C 180C 180C 180C 180C 1818 1830 7FE0 0000 0000 0000 ENDCHAR STARTCHAR U+0045 ENCODING 69 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FFC 1806 1800 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0046 ENCODING 70 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FFC 1806 1800 1800 1800 1FF0 1800 1800 1800 1800 7E00 0000 0000 0000 ENDCHAR STARTCHAR U+0047 ENCODING 71 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1818 300C 3000 3000 3000 30FF 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+0048 ENCODING 72 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C1F 180C 180C 180C 180C 1FFC 180C 180C 180C 180C 7C1F 0000 0000 0000 ENDCHAR STARTCHAR U+0049 ENCODING 73 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+004A ENCODING 74 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 003F 000C 000C 000C 000C 000C 000C 000C 000C 7018 1FF0 0000 0000 0000 ENDCHAR STARTCHAR U+004B ENCODING 75 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C3E 1818 1830 1860 18C0 1FC0 1860 1830 1818 180C 7C1F 0000 0000 0000 ENDCHAR STARTCHAR U+004C ENCODING 76 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7E00 1800 1800 1800 1800 1800 1800 1800 1800 1803 7FFE 0000 0000 0000 ENDCHAR STARTCHAR U+004D ENCODING 77 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 780F 1C1C 1E3C 1B6C 19CC 188C 180C 180C 180C 180C 7C1F 0000 0000 0000 ENDCHAR STARTCHAR U+004E ENCODING 78 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 783F 180C 1C0C 1E0C 1B0C 198C 18CC 186C 183C 181C 7E0C 0000 0000 0000 ENDCHAR STARTCHAR U+004F ENCODING 79 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07E0 0C30 1818 300C 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+0050 ENCODING 80 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FF8 180C 1806 1806 180C 1FF8 1800 1800 1800 1800 7E00 0000 0000 0000 ENDCHAR STARTCHAR U+0051 ENCODING 81 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07E0 0C30 1818 300C 300C 300C 300C 300C 19D8 0CF0 07E0 0038 0000 0000 ENDCHAR STARTCHAR U+0052 ENCODING 82 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FF0 1818 180C 180C 1818 1FF0 18C0 1860 1830 1818 7C3F 0000 0000 0000 ENDCHAR STARTCHAR U+0053 ENCODING 83 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF8 180C 3000 3000 1800 0FF8 000C 0006 0006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0054 ENCODING 84 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFC 6186 0180 0180 0180 0180 0180 0180 0180 0180 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+0055 ENCODING 85 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+0056 ENCODING 86 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C1F 180C 180C 0C18 0C18 0630 0630 0360 0360 01C0 01C0 0000 0000 0000 ENDCHAR STARTCHAR U+0057 ENCODING 87 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C1F 180C 180C 180C 180C 188C 19CC 1B6C 1E3C 1C1C 180C 0000 0000 0000 ENDCHAR STARTCHAR U+0058 ENCODING 88 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C3E 1818 0C30 0660 03C0 0180 03C0 0660 0C30 1818 7C3E 0000 0000 0000 ENDCHAR STARTCHAR U+0059 ENCODING 89 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C3E 1818 0C30 0660 03C0 0180 0180 0180 0180 0180 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+005A ENCODING 90 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFE 601C 0038 0070 00E0 01C0 0380 0700 0E00 1C03 3FFE 0000 0000 0000 ENDCHAR STARTCHAR U+005B ENCODING 91 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07F0 0600 0600 0600 0600 0600 0600 0600 0600 0600 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+005C ENCODING 92 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3000 3800 0C00 0600 0300 0180 00C0 0060 0030 001C 000C 0000 0000 0000 ENDCHAR STARTCHAR U+005D ENCODING 93 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FE0 0060 0060 0060 0060 0060 0060 0060 0060 0060 0FE0 0000 0000 0000 ENDCHAR STARTCHAR U+005E ENCODING 94 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 03C0 0660 0C30 1818 300C 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+005F ENCODING 95 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 7FFE 7FFE 0000 0000 ENDCHAR STARTCHAR U+0060 ENCODING 96 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0700 0380 01C0 00E0 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+0061 ENCODING 97 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0062 ENCODING 98 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7800 1800 1800 1800 1FF8 180C 1806 1806 1806 1C06 73FC 0000 0000 0000 ENDCHAR STARTCHAR U+0063 ENCODING 99 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 300C 6000 6000 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0064 ENCODING 100 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 003C 000C 000C 000C 0FFC 180C 300C 300C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0065 ENCODING 101 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0066 ENCODING 102 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 01FC 0606 0600 7FF8 0600 0600 0600 0600 0600 3FC0 0000 0000 0000 ENDCHAR STARTCHAR U+0067 ENCODING 103 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0FE7 181C 300C 300C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+0068 ENCODING 104 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7800 1800 1800 1800 1FF8 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+0069 ENCODING 105 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 00C0 00C0 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+006A ENCODING 106 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 000C 000C 0000 0000 03FC 000C 000C 000C 000C 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+006B ENCODING 107 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7800 1800 1800 181E 1830 1860 18C0 1FE0 1830 1818 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+006C ENCODING 108 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 FE00 0600 0600 0600 0600 0600 0600 0600 0607 01FC 0000 0000 0000 ENDCHAR STARTCHAR U+006D ENCODING 109 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 7E70 318C 318C 318C 318C 318C 318C 718F 0000 0000 0000 ENDCHAR STARTCHAR U+006E ENCODING 110 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 73F0 1C18 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+006F ENCODING 111 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0070 ENCODING 112 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 73F8 1C0C 1806 1806 1806 1806 1FFC 1800 1800 1800 7800 ENDCHAR STARTCHAR U+0071 ENCODING 113 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0FEC 181C 300C 300C 300C 300C 1FFC 000C 000C 000C 000F ENDCHAR STARTCHAR U+0072 ENCODING 114 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 79FC 0E07 0C00 0C00 0C00 0C00 0C00 7F80 0000 0000 0000 ENDCHAR STARTCHAR U+0073 ENCODING 115 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0FFC 3807 3800 0FF8 000C 0006 7006 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0074 ENCODING 116 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0600 0600 0600 7FF8 0600 0600 0600 0600 0607 01FC 0000 0000 0000 ENDCHAR STARTCHAR U+0075 ENCODING 117 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0076 ENCODING 118 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 7C1F 180C 180C 0C18 0630 0360 01C0 01C0 0000 0000 0000 ENDCHAR STARTCHAR U+0077 ENCODING 119 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 780F 180C 180C 180C 19CC 19CC 0F78 0630 0000 0000 0000 ENDCHAR STARTCHAR U+0078 ENCODING 120 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 7C1F 180C 0630 01C0 01C0 0630 180C 7C1F 0000 0000 0000 ENDCHAR STARTCHAR U+0079 ENCODING 121 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 783F 180C 180C 180C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+007A ENCODING 122 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 3FFE 600E 0038 00E0 0380 0E00 3803 3FFE 0000 0000 0000 ENDCHAR STARTCHAR U+007B ENCODING 123 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 00F0 0180 0180 0180 0300 0E00 0300 0180 0180 0180 00F0 0000 0000 0000 ENDCHAR STARTCHAR U+007C ENCODING 124 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0180 0180 0180 0180 0180 0180 0180 0180 0180 0180 0000 0000 ENDCHAR STARTCHAR U+007D ENCODING 125 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0F00 0180 0180 0180 00C0 0070 00C0 0180 0180 0180 0F00 0000 0000 0000 ENDCHAR STARTCHAR U+007E ENCODING 126 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0F0C 1F9C 39F8 30F0 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00A0 ENCODING 160 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00A1 ENCODING 161 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 03C0 0180 0000 0180 0180 0180 03C0 03C0 03C0 03C0 0180 0000 0000 ENDCHAR STARTCHAR U+00A2 ENCODING 162 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0FF8 198C 3180 3180 3180 3180 198C 0FF8 0180 0180 0000 0000 ENDCHAR STARTCHAR U+00A3 ENCODING 163 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 01F0 03F8 071C 060C 1FC0 1FC0 0600 1E00 3F00 3780 3FF8 1CF8 0000 0000 0000 ENDCHAR STARTCHAR U+00A4 ENCODING 164 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0180 05A0 0A50 05A0 1BD8 1BD8 05A0 0A50 05A0 0180 0000 0000 0000 ENDCHAR STARTCHAR U+00A5 ENCODING 165 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 300C 381C 1C38 0E70 07E0 1FF8 1FF8 0180 1FF8 1FF8 0180 0180 0000 0000 0000 ENDCHAR STARTCHAR U+00A6 ENCODING 166 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0300 0300 0300 0300 0300 0000 0000 0300 0300 0300 0300 0300 0000 0000 ENDCHAR STARTCHAR U+00A7 ENCODING 167 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 07F0 0FF8 1C08 1800 1FE0 17F0 1018 1808 0FE8 07F8 0018 1038 1FF0 0FE0 0000 ENDCHAR STARTCHAR U+00A8 ENCODING 168 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0660 0660 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00A9 ENCODING 169 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1818 33CC 366C 360C 360C 360C 366C 33CC 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+00AA ENCODING 170 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1830 1830 0FFC 0000 1FFC 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00AB ENCODING 171 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 038E 0E38 38E0 0E38 038E 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00AC ENCODING 172 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 3FFC 000C 000C 000C 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00AE ENCODING 174 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1818 37CC 346C 342C 37CC 358C 34CC 346C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+00AF ENCODING 175 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03E0 03E0 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00B0 ENCODING 176 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 07E0 0C30 0C30 07E0 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00B1 ENCODING 177 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0180 0180 0180 1FF8 1FF8 0180 0180 0180 0000 1FF8 1FF8 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00B2 ENCODING 178 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 07F0 1C18 0038 00E0 0380 0E00 1FF8 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00B3 ENCODING 179 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 07F0 1C18 0018 01F0 0018 1C18 07F0 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00B4 ENCODING 180 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00B5 ENCODING 181 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0C0C 0C0C 0C0C 0C0C 0C0C 0C0C 0C1C 0FE7 0C00 0C00 3800 ENDCHAR STARTCHAR U+00B6 ENCODING 182 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 1FFC 3E30 3E30 3E30 1E30 0630 0630 0630 0630 0630 0630 0000 0000 0000 ENDCHAR STARTCHAR U+00B7 ENCODING 183 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0180 0180 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00B8 ENCODING 184 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 00C0 0060 01C0 ENDCHAR STARTCHAR U+00B9 ENCODING 185 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0180 0780 0180 0180 0180 0180 1FF8 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00BA ENCODING 186 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1818 1818 0FF0 0000 1FF8 0000 0000 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00BB ENCODING 187 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 38E0 0E38 038E 0E38 38E0 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00BC ENCODING 188 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 1800 7800 1800 181C 1870 01C0 0700 1C66 7066 007E 0006 0006 0000 0000 ENDCHAR STARTCHAR U+00BD ENCODING 189 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 1800 7800 1800 181C 1870 01C0 0700 1C7C 7006 003C 0060 007E 0000 0000 ENDCHAR STARTCHAR U+00BE ENCODING 190 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C00 0600 1E00 061C 7C70 01C0 0700 1C66 7066 007E 0006 0006 0000 0000 ENDCHAR STARTCHAR U+00BF ENCODING 191 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 03C0 0180 0000 0180 00E0 0038 000C 000C 180C 0C18 07F0 0000 0000 ENDCHAR STARTCHAR U+00C0 ENCODING 192 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 00C0 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+00C1 ENCODING 193 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+00C2 ENCODING 194 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+00C3 ENCODING 195 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 05C0 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+00C4 ENCODING 196 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0630 0630 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+00C5 ENCODING 197 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03C0 0180 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+00C6 ENCODING 198 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FFC 03C6 06C0 04C0 0CC0 0FF8 18C0 18C0 30C0 30C6 79FC 0000 0000 0000 ENDCHAR STARTCHAR U+00C7 ENCODING 199 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF8 180C 3006 3000 3000 3000 3000 3000 3006 180C 0FF8 00C0 0060 01C0 ENDCHAR STARTCHAR U+00C8 ENCODING 200 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 00C0 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00C9 ENCODING 201 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00CA ENCODING 202 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00CB ENCODING 203 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0660 0660 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00CC ENCODING 204 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0300 0180 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00CD ENCODING 205 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00CE ENCODING 206 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00CF ENCODING 207 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0660 0660 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00D0 ENCODING 208 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FE0 1830 1818 180C 180C 7F8C 180C 180C 1818 1830 7FE0 0000 0000 0000 ENDCHAR STARTCHAR U+00D1 ENCODING 209 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 05C0 0000 783F 1C0C 1E0C 1B0C 198C 18CC 186C 183C 181C 7E0C 0000 0000 0000 ENDCHAR STARTCHAR U+00D2 ENCODING 210 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0300 0180 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+00D3 ENCODING 211 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+00D4 ENCODING 212 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+00D5 ENCODING 213 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 05C0 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+00D6 ENCODING 214 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0660 0660 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+00D7 ENCODING 215 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0C30 0E70 07E0 03C0 03C0 07E0 0E70 0C30 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00D8 ENCODING 216 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07EC 0C18 1830 3068 30CC 318C 330C 360C 1C18 1830 37E0 0000 0000 0000 ENDCHAR STARTCHAR U+00D9 ENCODING 217 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 00C0 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+00DA ENCODING 218 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+00DB ENCODING 219 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+00DC ENCODING 220 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0630 0630 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+00DD ENCODING 221 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 7C3E 1818 0C30 0660 03C0 0180 0180 0180 0180 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+00DE ENCODING 222 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 1800 1800 1FE0 1FF0 1838 1818 1818 1838 1FF0 1FE0 1800 1800 0000 0000 0000 ENDCHAR STARTCHAR U+00DF ENCODING 223 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0FF8 1FFC 181C 181C 1838 1870 18E0 1870 1838 181C 187C 19F8 0000 0000 0000 ENDCHAR STARTCHAR U+00E0 ENCODING 224 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0300 0180 0080 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00E1 ENCODING 225 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0100 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00E2 ENCODING 226 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00E3 ENCODING 227 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0360 05C0 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00E4 ENCODING 228 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0660 0660 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00E5 ENCODING 229 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 0140 0080 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00E6 ENCODING 230 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 3E7C 63C6 0186 1FFC 3180 6180 61C6 3E7C 0000 0000 0000 ENDCHAR STARTCHAR U+00E7 ENCODING 231 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 300C 6000 6000 6000 6000 3007 1FFC 00C0 0060 01C0 ENDCHAR STARTCHAR U+00E8 ENCODING 232 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0300 0180 0080 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00E9 ENCODING 233 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0100 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00EA ENCODING 234 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00EB ENCODING 235 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0660 0660 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+00EC ENCODING 236 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0300 0180 0080 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+00ED ENCODING 237 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0060 00C0 0080 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+00EE ENCODING 238 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+00EF ENCODING 239 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0330 0330 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+00F0 ENCODING 240 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 0180 06C0 0060 0030 0FF8 181C 300C 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+00F1 ENCODING 241 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 01B0 02E0 0000 73F0 1C18 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+00F2 ENCODING 242 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0300 0180 0080 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+00F3 ENCODING 243 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0100 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+00F4 ENCODING 244 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+00F5 ENCODING 245 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0360 05C0 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+00F6 ENCODING 246 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0660 0660 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+00F7 ENCODING 247 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0180 0180 0000 1FF8 1FF8 0000 0180 0180 0000 0000 0000 0000 ENDCHAR STARTCHAR U+00F8 ENCODING 248 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0FFA 180E 303C 30E6 3386 1E06 380C 2FF8 0000 0000 0000 ENDCHAR STARTCHAR U+00F9 ENCODING 249 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0300 0180 0080 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00FA ENCODING 250 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0100 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00FB ENCODING 251 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00FC ENCODING 252 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0660 0660 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+00FD ENCODING 253 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0100 0000 783F 180C 180C 180C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+00FE ENCODING 254 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 1800 1800 1800 1FE0 1FF8 1818 1818 1FF8 1FE0 1800 1800 1800 0000 ENDCHAR STARTCHAR U+00FF ENCODING 255 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0660 0660 0000 783F 180C 180C 180C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+0100 ENCODING 256 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03E0 03E0 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+0101 ENCODING 257 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 03E0 03E0 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0102 ENCODING 258 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0220 01C0 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+0103 ENCODING 259 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0220 01C0 0000 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0104 ENCODING 260 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FC0 01C0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 000C 0018 000E ENDCHAR STARTCHAR U+0105 ENCODING 261 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 000C 0018 000E ENDCHAR STARTCHAR U+0106 ENCODING 262 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 0FF8 180C 3006 3000 3000 3000 3000 3006 180C 0FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0107 ENCODING 263 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0100 0000 1FF8 300C 6000 6000 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0108 ENCODING 264 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 0FF8 180C 3006 3000 3000 3000 3000 3006 180C 0FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0109 ENCODING 265 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 1FF8 300C 6000 6000 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+010A ENCODING 266 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 0180 0000 0FF8 180C 3006 3000 3000 3000 3000 3006 180C 0FF8 0000 0000 0000 ENDCHAR STARTCHAR U+010B ENCODING 267 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0000 1FF8 300C 6000 6000 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+010C ENCODING 268 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 0FF8 180C 3006 3000 3000 3000 3000 3006 180C 0FF8 0000 0000 0000 ENDCHAR STARTCHAR U+010D ENCODING 269 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 1FF8 300C 6000 6000 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+010E ENCODING 270 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 06C0 0380 0000 7FE0 1830 1818 180C 180C 180C 180C 1818 1830 7FE0 0000 0000 0000 ENDCHAR STARTCHAR U+010F ENCODING 271 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0001 007B 001A 0018 0018 1FF8 3018 6018 6018 6018 6038 3FCE 0000 0000 0000 ENDCHAR STARTCHAR U+0110 ENCODING 272 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FE0 1830 1818 180C 180C 7F8C 180C 180C 1818 1830 7FE0 0000 0000 0000 ENDCHAR STARTCHAR U+0111 ENCODING 273 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 003C 000C 00FF 000C 0FFC 180C 300C 300C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0112 ENCODING 274 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03E0 03E0 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0113 ENCODING 275 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 03E0 03E0 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0114 ENCODING 276 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0220 01C0 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0115 ENCODING 277 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0220 01C0 0000 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0116 ENCODING 278 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 0180 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0117 ENCODING 279 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0118 ENCODING 280 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FFC 1806 1800 1800 1800 1FF0 1800 1800 1800 1806 7FFC 00C0 0180 00E0 ENDCHAR STARTCHAR U+0119 ENCODING 281 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 00C0 0180 00E0 ENDCHAR STARTCHAR U+011A ENCODING 282 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+011B ENCODING 283 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+011C ENCODING 284 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 0FF0 1818 300C 3000 3000 30FF 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+011D ENCODING 285 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 0FE7 181C 300C 300C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+011E ENCODING 286 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0220 01C0 0000 0FF0 1818 300C 3000 3000 30FF 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+011F ENCODING 287 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0220 01C0 0000 0000 0FE7 181C 300C 300C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+0120 ENCODING 288 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 0180 0000 0FF0 1818 300C 3000 3000 30FF 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+0121 ENCODING 289 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0180 0180 0000 0000 0FE7 181C 300C 300C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+0122 ENCODING 290 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF0 1818 300C 3000 3000 3000 30FF 300C 300C 1818 0FF0 0180 00C0 0380 ENDCHAR STARTCHAR U+0123 ENCODING 291 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0180 0000 0FE7 181C 300C 300C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+0124 ENCODING 292 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 7C1F 180C 180C 180C 1FFC 180C 180C 180C 180C 7C1F 0000 0000 0000 ENDCHAR STARTCHAR U+0125 ENCODING 293 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0700 0D80 0000 7800 1800 1800 1FF8 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+0126 ENCODING 294 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C1F 180C 3FFE 180C 180C 1FFC 180C 180C 180C 180C 7C1F 0000 0000 0000 ENDCHAR STARTCHAR U+0127 ENCODING 295 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7800 1800 7F80 1800 1FF8 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+0128 ENCODING 296 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 05C0 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0129 ENCODING 297 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0360 05C0 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+012A ENCODING 298 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 07C0 07C0 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+012B ENCODING 299 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 03E0 03E0 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+012C ENCODING 300 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0440 0380 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+012D ENCODING 301 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0220 01C0 0000 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+012E ENCODING 302 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0180 0300 01C0 ENDCHAR STARTCHAR U+012F ENCODING 303 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 00C0 00C0 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 00C0 0180 00E0 ENDCHAR STARTCHAR U+0130 ENCODING 304 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 0180 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0131 ENCODING 305 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+0134 ENCODING 308 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 001C 0036 0000 003F 000C 000C 000C 000C 000C 000C 000C 7018 1FF0 0000 0000 0000 ENDCHAR STARTCHAR U+0135 ENCODING 309 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0008 001C 0036 0000 0000 03FC 000C 000C 000C 000C 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+0136 ENCODING 310 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C3E 1818 1830 1860 18C0 1FC0 1860 1830 1818 180C 7C1F 0180 0180 0700 ENDCHAR STARTCHAR U+0137 ENCODING 311 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7800 1800 1800 181E 1830 1860 18C0 1FE0 1830 1818 7E3F 0180 0180 0700 ENDCHAR STARTCHAR U+0138 ENCODING 312 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 781E 1830 1860 18C0 1FE0 1830 1818 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+0139 ENCODING 313 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0600 0C00 0000 7E00 1800 1800 1800 1800 1800 1800 1800 1803 7FFE 0000 0000 0000 ENDCHAR STARTCHAR U+013A ENCODING 314 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 0300 0000 FE00 0600 0600 0600 0600 0600 0600 0600 0607 01FC 0000 0000 0000 ENDCHAR STARTCHAR U+013B ENCODING 315 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7E00 1800 1800 1800 1800 1800 1800 1800 1800 1803 7FFE 00C0 0060 01C0 ENDCHAR STARTCHAR U+013C ENCODING 316 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 FE00 0600 0600 0600 0600 0600 0600 0600 0607 01FC 00C0 0060 01C0 ENDCHAR STARTCHAR U+013D ENCODING 317 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0020 0060 7E40 1800 1800 1800 1800 1800 1800 1800 1800 1803 7FFE 0000 0000 0000 ENDCHAR STARTCHAR U+013E ENCODING 318 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0020 0060 FE40 0600 0600 0600 0600 0600 0600 0600 0607 01FC 0000 0000 0000 ENDCHAR STARTCHAR U+013F ENCODING 319 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7E00 1800 1800 1800 18C0 18C0 1800 1800 1800 1803 7FFE 0000 0000 0000 ENDCHAR STARTCHAR U+0140 ENCODING 320 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 FE00 0600 0600 0630 0630 0600 0600 0600 0607 01FC 0000 0000 0000 ENDCHAR STARTCHAR U+0141 ENCODING 321 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7E00 1800 1800 18C0 1B80 1E00 3800 7800 1800 1803 7FFE 0000 0000 0000 ENDCHAR STARTCHAR U+0142 ENCODING 322 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 FE00 0600 0600 06C0 0780 0E00 1E00 0600 0607 01FC 0000 0000 0000 ENDCHAR STARTCHAR U+0143 ENCODING 323 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 783F 1C0C 1E0C 1B0C 198C 18CC 186C 183C 181C 7E0C 0000 0000 0000 ENDCHAR STARTCHAR U+0144 ENCODING 324 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0060 00C0 0080 0000 73F0 1C18 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+0145 ENCODING 325 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 783F 180C 1C0C 1E0C 1B0C 198C 18CC 186C 183C 181C 7E0C 0018 000C 0038 ENDCHAR STARTCHAR U+0146 ENCODING 326 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 73F0 1C18 180C 180C 180C 180C 180C 7E0F 0018 000C 0038 ENDCHAR STARTCHAR U+0147 ENCODING 327 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 783F 1C0C 1E0C 1B0C 198C 18CC 186C 183C 181C 7E0C 0000 0000 0000 ENDCHAR STARTCHAR U+0148 ENCODING 328 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 73F0 1C18 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+0149 ENCODING 329 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 3000 7800 3000 6000 0000 73F0 1C18 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+014A ENCODING 330 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 783F 180C 1C0C 1E0C 1B0C 198C 18CC 186C 183C 181C 7E0C 0018 0070 0000 ENDCHAR STARTCHAR U+014B ENCODING 331 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 73F0 1C18 180C 180C 180C 180C 180C 7E0F 0018 0070 0000 ENDCHAR STARTCHAR U+014C ENCODING 332 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03E0 03E0 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+014D ENCODING 333 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 03E0 03E0 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+014E ENCODING 334 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0220 01C0 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+014F ENCODING 335 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0220 01C0 0000 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0150 ENCODING 336 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 06C0 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+0151 ENCODING 337 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0330 0660 0440 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0152 ENCODING 338 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FFC 18C6 30C0 30C0 30C0 30F8 30C0 30C0 30C0 18C6 0FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0153 ENCODING 339 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1E7C 33C6 6186 61FC 6180 6180 33C6 1E7C 0000 0000 0000 ENDCHAR STARTCHAR U+0154 ENCODING 340 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 7FF0 1818 180C 180C 1FF8 18C0 1860 1830 1818 7C3F 0000 0000 0000 ENDCHAR STARTCHAR U+0155 ENCODING 341 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0030 0060 0040 0000 79FC 0E07 0C00 0C00 0C00 0C00 0C00 7F80 0000 0000 0000 ENDCHAR STARTCHAR U+0156 ENCODING 342 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FF0 1818 180C 180C 1818 1FF0 18C0 1860 1830 1818 7C3F 0180 0180 0700 ENDCHAR STARTCHAR U+0157 ENCODING 343 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 79FC 0E07 0C00 0C00 0C00 0C00 0C00 7F80 0C00 0600 1C00 ENDCHAR STARTCHAR U+0158 ENCODING 344 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 7FF0 1818 180C 180C 1FF8 18C0 1860 1830 1818 7C3F 0000 0000 0000 ENDCHAR STARTCHAR U+0159 ENCODING 345 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 01B0 00E0 0040 0000 79FC 0E07 0C00 0C00 0C00 0C00 0C00 7F80 0000 0000 0000 ENDCHAR STARTCHAR U+015A ENCODING 346 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 0FF8 180C 3000 3000 1FF8 000C 0006 0006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+015B ENCODING 347 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0060 00C0 0080 0000 0FFC 3807 3800 0FF8 000C 0006 7006 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+015C ENCODING 348 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 0FF8 180C 3000 3000 1FF8 000C 0006 0006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+015D ENCODING 349 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0040 00E0 01B0 0000 0FFC 3807 3800 0FF8 000C 0006 7006 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+015E ENCODING 350 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF8 180C 3000 3000 1800 0FF8 000C 0006 0006 300C 1FF8 0180 00C0 0380 ENDCHAR STARTCHAR U+015F ENCODING 351 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0FFC 3807 3800 0FF8 000C 0006 7006 1FFC 00C0 0060 01C0 ENDCHAR STARTCHAR U+0160 ENCODING 352 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 0FF8 180C 3000 3000 1FF8 000C 0006 0006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0161 ENCODING 353 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 0FFC 3807 3800 0FF8 000C 0006 7006 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0162 ENCODING 354 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFC 6186 0180 0180 0180 0180 0180 0180 0180 0180 07E0 0180 00C0 0380 ENDCHAR STARTCHAR U+0163 ENCODING 355 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0600 0600 0600 7FF8 0600 0600 0600 0600 0607 01FC 0060 0030 00E0 ENDCHAR STARTCHAR U+0164 ENCODING 356 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 3FFC 6186 0180 0180 0180 0180 0180 0180 0180 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+0165 ENCODING 357 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0020 0060 0640 0600 0600 7FF8 0600 0600 0600 0600 0607 01FC 0000 0000 0000 ENDCHAR STARTCHAR U+0166 ENCODING 358 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFC 6186 0180 0180 0FF0 0180 0180 0180 0180 0180 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+0167 ENCODING 359 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0600 0600 0600 7FF8 0600 0600 1FE0 0600 0607 01FC 0000 0000 0000 ENDCHAR STARTCHAR U+0168 ENCODING 360 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01B0 02E0 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+0169 ENCODING 361 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0360 05C0 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+016A ENCODING 362 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03E0 03E0 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+016B ENCODING 363 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 03E0 03E0 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+016C ENCODING 364 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0220 01C0 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+016D ENCODING 365 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0220 01C0 0000 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+016E ENCODING 366 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03C0 0180 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+016F ENCODING 367 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0100 0280 0100 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0170 ENCODING 368 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01B0 0360 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+0171 ENCODING 369 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0330 0660 0440 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0172 ENCODING 370 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 00C0 0180 00E0 ENDCHAR STARTCHAR U+0173 ENCODING 371 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 000C 0018 000E ENDCHAR STARTCHAR U+0174 ENCODING 372 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 7C1F 180C 180C 180C 188C 19CC 1B6C 1E3C 1C1C 180C 0000 0000 0000 ENDCHAR STARTCHAR U+0175 ENCODING 373 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 780F 180C 180C 180C 19CC 19CC 0F78 0630 0000 0000 0000 ENDCHAR STARTCHAR U+0176 ENCODING 374 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0360 0000 7C3E 1818 0C30 0660 03C0 0180 0180 0180 0180 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+0177 ENCODING 375 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0080 01C0 0360 0000 783F 180C 180C 180C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+0178 ENCODING 376 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0660 0660 0000 7C3E 1818 0C30 0660 03C0 0180 0180 0180 0180 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+0179 ENCODING 377 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 3FFC 6038 0070 00E0 01C0 0380 0700 0E00 1C06 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+017A ENCODING 378 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0100 0000 3FFE 600E 0038 00E0 0380 0E00 3803 3FFE 0000 0000 0000 ENDCHAR STARTCHAR U+017B ENCODING 379 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 0180 0000 3FFC 6038 0070 00E0 01C0 0380 0700 0E00 1C06 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+017C ENCODING 380 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0000 3FFE 600E 0038 00E0 0380 0E00 3803 3FFE 0000 0000 0000 ENDCHAR STARTCHAR U+017D ENCODING 381 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 3FFC 6038 0070 00E0 01C0 0380 0700 0E00 1C06 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+017E ENCODING 382 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 3FFE 600E 0038 00E0 0380 0E00 3803 3FFE 0000 0000 0000 ENDCHAR STARTCHAR U+0180 ENCODING 384 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7800 1800 7F80 1800 1FF8 180C 1806 1806 1806 1C06 73FC 0000 0000 0000 ENDCHAR STARTCHAR U+0181 ENCODING 385 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FF0 4C18 4C0C 6C0C 0C18 0FF8 0C0C 0C06 0C06 0C0C 3FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0187 ENCODING 391 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 000E 0013 0010 0FF8 180C 3006 3000 3000 3000 3000 3006 180C 0FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0188 ENCODING 392 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 000E 0013 0010 1FF8 300C 6000 6000 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0189 ENCODING 393 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 7FE0 1830 1818 180C 180C 7F8C 180C 180C 1818 1830 7FE0 0000 0000 0000 ENDCHAR STARTCHAR U+018A ENCODING 394 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FF0 4C18 4C0C 6C06 0C06 0C06 0C06 0C06 0C0C 0C18 3FF0 0000 0000 0000 ENDCHAR STARTCHAR U+018E ENCODING 398 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFE 6018 0018 0018 0018 0FF8 0018 0018 0018 6018 3FFE 0000 0000 0000 ENDCHAR STARTCHAR U+0193 ENCODING 403 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 001C 0026 0020 0FF0 1818 300C 3000 3000 30FF 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+01C3 ENCODING 451 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 03C0 03C0 03C0 03C0 0180 0180 0180 0000 0180 03C0 0180 0000 0000 ENDCHAR STARTCHAR U+01CD ENCODING 461 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+01CE ENCODING 462 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+01CF ENCODING 463 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+01D0 ENCODING 464 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+01D1 ENCODING 465 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+01D2 ENCODING 466 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+01D3 ENCODING 467 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+01D4 ENCODING 468 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+01DD ENCODING 477 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0FFC 1806 1803 0FFF 0003 0003 7006 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+01E2 ENCODING 482 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01F0 01F0 0000 1FFC 06C6 04C0 0CC0 0FF8 18C0 18C0 30C0 30C6 79FC 0000 0000 0000 ENDCHAR STARTCHAR U+01E3 ENCODING 483 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 03E0 03E0 0000 3E7C 63C6 0186 1FFC 3180 6180 61C6 3E7C 0000 0000 0000 ENDCHAR STARTCHAR U+01E6 ENCODING 486 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 0FF0 1818 300C 3000 3000 30FF 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+01E7 ENCODING 487 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0360 01C0 0080 0000 0FE7 181C 300C 300C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+01EA ENCODING 490 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 07E0 0C30 1818 300C 300C 300C 300C 300C 1818 0C30 07E0 0180 0300 01C0 ENDCHAR STARTCHAR U+01EB ENCODING 491 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0180 0300 01C0 ENDCHAR STARTCHAR U+01EC ENCODING 492 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03E0 03E0 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0180 0300 01C0 ENDCHAR STARTCHAR U+01ED ENCODING 493 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 03E0 03E0 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0180 0300 01C0 ENDCHAR STARTCHAR U+01F0 ENCODING 496 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0036 001C 0008 0000 0000 03FC 000C 000C 000C 000C 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+01F4 ENCODING 500 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 0FF0 1818 300C 3000 3000 30FF 300C 300C 1818 0FF0 0000 0000 0000 ENDCHAR STARTCHAR U+01F5 ENCODING 501 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0060 00C0 0080 0000 0FE7 181C 300C 300C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+01F8 ENCODING 504 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0220 01C0 0000 783F 1C0C 1E0C 1B0C 198C 18CC 186C 183C 181C 7E0C 0000 0000 0000 ENDCHAR STARTCHAR U+01F9 ENCODING 505 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0220 01C0 0000 0000 73F0 1C18 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+01FC ENCODING 508 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0060 00C0 0000 1FFC 06C6 04C0 0CC0 0FF8 18C0 18C0 30C0 30C6 79FC 0000 0000 0000 ENDCHAR STARTCHAR U+01FD ENCODING 509 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00C0 0180 0100 0000 3E7C 63C6 0186 1FFC 3180 6180 61C6 3E7C 0000 0000 0000 ENDCHAR STARTCHAR U+01FE ENCODING 510 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 00C0 0180 0000 07D8 0C30 1868 30CC 318C 330C 360C 1C18 1830 37E0 0000 0000 0000 ENDCHAR STARTCHAR U+01FF ENCODING 511 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0060 00C0 0080 0000 0FFA 180E 303C 30E6 3386 1E06 380C 2FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0202 ENCODING 514 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0380 0440 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+0203 ENCODING 515 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 01C0 0220 0000 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0206 ENCODING 518 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0220 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 0000 0000 0000 ENDCHAR STARTCHAR U+0207 ENCODING 519 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 01C0 0220 0000 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 0000 0000 0000 ENDCHAR STARTCHAR U+020A ENCODING 522 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0380 0440 0000 3FFC 0180 0180 0180 0180 0180 0180 0180 0180 3FFC 0000 0000 0000 ENDCHAR STARTCHAR U+020B ENCODING 523 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 01C0 0220 0000 0000 0000 1FC0 00C0 00C0 00C0 00C0 00C0 7FFF 0000 0000 0000 ENDCHAR STARTCHAR U+020E ENCODING 526 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0220 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+020F ENCODING 527 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 01C0 0220 0000 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0212 ENCODING 530 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0220 0000 7FF0 1818 180C 180C 1FF8 18C0 1860 1830 1818 7C3F 0000 0000 0000 ENDCHAR STARTCHAR U+0213 ENCODING 531 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 00E0 0110 0000 0000 79FC 0E07 0C00 0C00 0C00 0C00 0C00 7F80 0000 0000 0000 ENDCHAR STARTCHAR U+0216 ENCODING 534 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 01C0 0220 0000 7C1F 180C 180C 180C 180C 180C 180C 180C 0C18 07F0 0000 0000 0000 ENDCHAR STARTCHAR U+0217 ENCODING 535 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 01C0 0220 0000 0000 783C 180C 180C 180C 180C 180C 181C 0FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0218 ENCODING 536 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0FF8 180C 3000 3000 1800 0FF8 000C 0006 0006 300C 1FF8 0000 0180 0300 ENDCHAR STARTCHAR U+0219 ENCODING 537 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0FFC 3807 3800 0FF8 000C 0006 7006 1FFC 0000 00C0 0180 ENDCHAR STARTCHAR U+021A ENCODING 538 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 3FFC 6186 0180 0180 0180 0180 0180 0180 0180 0180 07E0 0000 0180 0300 ENDCHAR STARTCHAR U+021B ENCODING 539 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0600 0600 0600 7FF8 0600 0600 0600 0600 0607 01FC 0000 0060 00C0 ENDCHAR STARTCHAR U+021E ENCODING 542 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0360 01C0 0000 7C1F 180C 180C 180C 1FFC 180C 180C 180C 180C 7C1F 0000 0000 0000 ENDCHAR STARTCHAR U+021F ENCODING 543 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0D80 0700 0200 7800 1800 1800 1FF8 180C 180C 180C 180C 180C 7E0F 0000 0000 0000 ENDCHAR STARTCHAR U+0226 ENCODING 550 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 0180 0000 0FC0 0360 0220 0630 0410 0FF8 0C18 180C 180C 7E3F 0000 0000 0000 ENDCHAR STARTCHAR U+0227 ENCODING 551 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0000 1FF8 700C 000C 0FFC 180C 300C 301C 1FE7 0000 0000 0000 ENDCHAR STARTCHAR U+0228 ENCODING 552 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 7FFC 1806 1800 1800 1FF0 1800 1800 1800 1806 7FFC 00C0 0060 01C0 ENDCHAR STARTCHAR U+0229 ENCODING 553 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 1FF8 300C 600C 7FF8 6000 6000 3007 1FFC 00C0 0060 01C0 ENDCHAR STARTCHAR U+022E ENCODING 558 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0180 0180 0000 07E0 0C30 1818 300C 300C 300C 300C 1818 0C30 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+022F ENCODING 559 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0000 1FF8 300C 6006 6006 6006 6006 300C 1FF8 0000 0000 0000 ENDCHAR STARTCHAR U+0232 ENCODING 562 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 03C0 03C0 0000 7C3E 1818 0C30 0660 03C0 0180 0180 0180 0180 07E0 0000 0000 0000 ENDCHAR STARTCHAR U+0233 ENCODING 563 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 03E0 03E0 0000 783F 180C 180C 180C 180C 0FFC 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+0237 ENCODING 567 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 03FC 000C 000C 000C 000C 000C 700C 1FF8 0000 0000 ENDCHAR STARTCHAR U+2020 ENCODING 8224 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0180 1FF8 1FF8 0180 0180 0180 0180 0180 0180 0180 0180 0000 ENDCHAR STARTCHAR U+2021 ENCODING 8225 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0180 0180 0180 1FF8 1FF8 0180 0180 0180 1FF8 1FF8 0180 0180 0180 0000 ENDCHAR STARTCHAR U+2022 ENCODING 8226 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0180 03C0 03C0 0180 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+2039 ENCODING 8249 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0070 01C0 0700 01C0 0070 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+203A ENCODING 8250 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0700 01C0 0070 01C0 0700 0000 0000 0000 0000 0000 0000 ENDCHAR STARTCHAR U+20AC ENCODING 8364 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 03F8 07FC 0E04 3FC0 3FC0 0C00 0C00 3FC0 3FC0 0E04 07FC 03F8 0000 0000 0000 ENDCHAR STARTCHAR U+22C5 ENCODING 8901 SWIDTH 1000 0 DWIDTH 16 0 BBX 16 16 0 -3 BITMAP 0000 0000 0000 0000 0000 0000 0000 0000 0180 0000 0000 0000 0000 0000 0000 0000 ENDCHAR ENDFONT LambdaHack-0.11.0.0/GameDefinition/fonts/16x16xw.woff0000644000000000000000000012734007346545000020140 0ustar0000000000000000wOFF®à×`DSIG®˜FFTM®Ä†ÈGDEF® $&'ªOS/2øJ`‰‹Ecmapü2Nž™—åcvt DÜLfpgm0±e´/§gasp®ÿÿglyf `¢ŘÚó¨ðhead€56™%,hhea¸ $ YhmtxDµ”ˆ¸locaT  Íy_žmaxpØ ©íname¬ð‡à|˜post®x ÿ;eprepä__ýÿP¦xœc`d``âÓ~Žïâùm¾2ȳ9En¤vnÓûBþ]asbr9˜@¢KÀ Cxœc`d``ùw•Íáÿ?6'  `pG~„\ xœc`as`üÂÀÊÀÀ"À’ÀÀÀðB330ø3Îc@ ü@Ê Æ÷vÊ ²+eùw•U„Q&ǬÀâ¤Ô Uxœ•RÛ„ |XŒCW†PñíP‡5XÀ-äÀ@ð¼›$,yl0Ë‹hiËF"»™“#Û&»³[åö¢ŒøË{µåYr@‰™…å» Ôå`‚%žX£Y‹*ǺŠÎçÀÑåŽûˆ˜K±‡ÕgMÂbHû£·)p*^£S~U¾­®_kÒû­¯ægo:®_s?ÝýÑ%þ¢Ì?¯8öš”×·Wk\™ˆ<çgž‰7hXº›xœ­’kLPÇÏ)E!Š\¢^!r)]èâNîw’¤’¤!„6÷"·a¹Ímîׄ…¹4}3ߘ06›Í†­íq¼ï;£ÏÎöüŸsyžÿÎùÿà+:!‘$»çÚSzÚ<ša4Á oЍ()’b)•2)—:y!¯ä|”ÏòE¾ËOQãa¼¯ñ3&È„˜pibLœ#Ô‘è¨}¦jù¼qpŠJ‰•MRâæynyÞÊùdy¾Éi0ÆxÓÒø›@lÂL„‰¶<G‚‹Gô½ÖéS}¬´F«µJ·êz-Ð|]ªyºHs5G³5K35]Ó4US4Y'ëx«£ê3êǼyùºÖõÞÿ0ìMÊшñ·ŽÆ=7› W§§[é¦4Ã_šÓ‚–øÑŠÖø@ÚH;ÚÓŽY:LˆU´ ¡t¥Ý £= §½éC_"ˆ¤QDC,ý@ñ$È@1˜! µg#Ib”õ{ cÇx&0‘ILf S™ÆtfÌLR˜E*³IcédÉ\²˜G¶½ÿvJØÉ.Ê9ÂIÎr†s\à<¹ÌU®PÁ5®SÉ nRÅmnq‡»Tó€û<¤V YÉrY,YÃi–³Ä²®"ÏâŽZ,pê´Õi·” /QC19äÿÙ_F¡Åùlf‡Å_þQüž'o¥ßlb±Ó„"ÖºO·²›-ì¡”½ìc?eäÝ?ÀqNpŒ¯Îªîêu®ô Tuž)xœ]Q»N[AÝ ÄØ 9Ú³™ï…6H ®.ÂÈvc9BÚ\äb\ÀP Qƒökh(S¤Mƒ $>Oˆ”™5‰¢4;;³sΙ3KÊ‘ªwi½ç©sHánƒf›~'¤ÚE€ÒõFFÚÁ#-63zåº}¿Áf4åN@yÏ[ÊCFÓN í¹2?ƒá>ÿË<ƒ–fšžZg!=„À|3nið5£YwýA_±:\ †ÓTÜõÇTÊÿ–æ\m¶63šwp!"?˜hj­@ÓŸ:¤z>Žb rùl¬ &¦¬?ÉDpa2]ÕT-3¾vpŸì,:ؤJsà°Už‚‡ã£ …ô-‰2KC„ƒØ*1BÄ$‡BN9w²?)P>’„1o’Òθa­qä50¨ÍÓ¾ÌfSÛ[‡0~GðÝ/Æ’>²¡6F„ØŽX `‘QU¾¡Æs/‹¹Ôþ3%`yúí_'­;6/emcŒ‚žß6ßùeÅݪ\çE¡»wU5T锿C/gßãO…á ±àÍç}£@í ‹ šÁÞÞÿÑZuÄUÞ Ùo5³±¸ÿ…°K°PX±ŽY±F+X!°YK°RX!°€Y°+\X° E°+D° E²+°+D° E°+D° Eºÿ+±Fv+DY°+ LdÈÈÈ>jÚ®Üè.ž ’Ò 8r¢8vªè d  j 2 Ð , š , N Þà¸NÖ`Æ À0jÂŽÚŒH†Vº4Úæ–Hx¤Ôê"¦  Š þ!~!ê"~"Þ#$#r$"$\$Æ%B%Ä&8&¢&ö'z'Ì(,(Ú)T**†++l+ˆ+ò,N,N,ž-0-Ú.þ/®/Ö0¤0Ì1š1î2ˆ2°3’3ª3ò4<4 55B5¦5ì66<6|6Ò7j88Ä9r:&;;ø<Ú=Ò>°?œ@^A"A¶BJBäCnCÖD>D²EE¼F¸GfHHÈI’J6J²KœLDLìMšN:OOnPPÒQŒRTS&SÖT²UpVVÐW~Xyyèz²{d| |Þ}œ~>~ÒdÚ€^€æ0xò‚d‚ʃDƒƒÈ„H„Â…¶†ˆ‡:‡¦ˆˆ–‰‰|‰äŠBŠŠ‹‹tŒZêŽ~:æ‘À’Z’ò“ˆ”D”ü•Ì– —4—è˜Ä™Fš6šº›¤œ8¾žŒŸR 6 ö¡Ì¢”£ £ª¤<¤Â¥<¥¦¦d§ §ž¨ ¨Ä©Xªª°«l¬ ¬Ò­b®:®ö¯Ð°‚±L²²à³´.µµè¶€·8·æ¸~¹$¹Æº.ºô»L¼6½½z½þ¾¸¿~À4ÀØÁXÂ$ÂöÃÎĤÅ^ÆÆìÇÀÈ>ÉÉÐʾËjÌLÍ>ÎJÏJÐ2ÐðÑŒÒ@Ò²Ó"ÓØÔŽÕvÖÖ²×BØØÐÙXÙÚÚ„Û*ÛþÜœÝ<ÝöÞŠßßÜàbà ààá6ápáÂââ²âÌxœì½{Œ+ivVÅG7Ù|UñÙ$›M²É~²»Ù]¼}ïôÎÌîÌììkvô´”Y10ðe úËZ'#¬Á®…À±¥YÛyXVþ±í 6U$W ‘bÄÂ^Þ‰àÚÑ… «•`C;‹$6¢Ù$ÐÞÛ›ó;çûŠUl²o÷Þ]åŸÜg±Èî®ïœóï<Ç0#o›oE?g¾oDºá}ÏŒ¾gº±¾g˜=/Ï‹›=ãäÔ<”"oOÍ·†C#yã;¿ýFìu#eT £}6 7¥N©sµùÚîÐïÈÃépö¿"oà\~úò]óÐ0"æûÑV̹ô³Wcd˜FÏ &fÌHÄznÜ1ÝDß5á9ܨåÅè‘Vé‘’òHöÀnÓ_óý©y8¶Ì·.?¿FĘ~çWâ?KÏW7šÆ/£Õ}ßÔ«+TÏŒ½çÙŽC?p­7~ɨ%{nÉÁ¢s›ôvœÞNfúé­¾[{äEVÇ«ÒÏMàkjU|MÍ ¯‰9nÕòÊôTk úH‰>’¶è#¥2>R*ÐG²Ž×æ§½wFTaJµ…Z¶P¬Mïµíö¿Ì·ø¿éå§q}ùéaèŽ| ja}Ÿ!ºõŒñ¼™6F­p{àE£ï¹wœQ . ¼SzY¥—§xžœMϳGŒ=°Zÿ¬ïv`µ{×™l5ìLÏÛŠ½7Y9Æ¥[x+ôÞï<öž»by‡ô^j?–¢ËR…?æ ¼},C”z|r×fÕ¡ã&Zl/}@Ô<¦§Ø·göF1«ï©vãïòµ;téîZÞQpýœnwˆ‚åMºy_Þ¦W/B v~´’ÍŸŸ{{v~\Üènw+熷ҴónãÜ=¶G«‰MzŸé\"ržuΘÚg vô>ëØøŸ©^ÕÛgÕRÇÄéÓ‡fïò]ªyøŽy8|ø€þ¡_—ï¾sùé‹áƒCó[†ÃøÀ[¾P“<ëkyzãñ×é¥aqcJÿ|†÷IßxÞø˜ñ—Ì¿gŒ> ^¥ÞkÑ÷Fiºö2$oVý­Úýð`òbÌHÅzžý2Ý{ÑòŽcï¢àhcà=OŸmÞ%à Šm¯z‡(½F¼ÚvÜ5Ësˆ+ÇuˆætoÍr Ðsµ3ཕâ½åÕéÛ¶éçO ­7?4›½üQbÄÐ7Øwܱ¼RŒÔÁñ.tˆ“çÎä$n¤é;Ð7·ˆ9ƒ{ôñ½³âŒû=+ÑóÒô"Ý÷"kôH}º¶,7‹DìŒ%hI=kr&ßã¬ïæÞ>}¨ˆOT¸e\lÓÅÇñAï³ç®;Þkôµ[?â8“ËÖð©~”~ ‰ûšå}’>÷CŽ÷azµ÷šô/½J?ìÃÖääk~ ïý8mEï0c罜a)ºj³ ´Ïl—Ðo•àï( Œˆ MàkïîO‡æÉÄWY .†Ã‡úb:¼?¤?þ€©^¿ !ƒØ=yõá}‘¨©-Rküÿã¯ë]\\Ї H¶þdëÔxÑø„ùÓÆèrµ?ðîïv~é9ÄÐmˆ 4‚÷1¾øIÅ«Îh ú¶4ðRôgœZKd{ne0ɈŒØÎÄ0s¢ Úö/=ϯÚï%ú¾-úª¸ã¶,¯K¯ž{ óµ¾{B s8u'ÎBüü‰ ¿‰Ã7Ü‹•Ã(–><¾$w?æ¸w,ïœ4ÄÙˆ¨óe¥ °)¶åû‘’uë–·I¢Öï9Ìo÷`åáî8Ï™i ÷Ží®ž{÷Îíü—íõêá‘ÅšãÎ1}¢×?Á'Û=<÷ž7íü$«66énËvkçÞÚKô©¾òa|*e»ýr¦x-úÅ×)"þ5‰D£sö¼ •ƪ¾ùe¾¯™-ºfúä‹t©ù-§§/þç|íƒ h#úE:ÇŒ¼y›Îf×È?iŒâPÉ#†§œQ<†ƒ ¾šìL¨ ƒ"CrbºVß<¢=ìeˆŠ g”‰à“™Qt0{9bŠ­í!€ZÂÛ¦å7ÛÄÞÈV笶ì"]^»E”¨Ÿ»†ínœ“uH,O®1kE:[Rý•W‚ä‚Ó¡Ú\r¡oáž¾Àž‚±J´øã?`¸k}ü!"¬ âÎÈ4°3𤔦·¹ÇKÒâbÎ(¹†÷’ØskI\® ¼ŒÚE0`Ûd°tl’°·Øè›êÿaÎüóßÿÎsÑoÐSä ÒóQèù5ÙÓ´ScQ|ÓØ /×ÇŽæÍl>£xÕ¥ÙÌM¯aÛ§õ“b#{iÐ,“13 ±r3DÂxx·ÉÊ– ¥¤6¸ÐÆ|+^Œ¼A'RAü‡˜øÚoh—ÌCzx²c ÑOðh-)ãuÙ3 éH$¾)ƒ^ˆò<ôSš|VÂØ5Œ+‡üƒ¾SšiÜiPÓ^¹2 uÃÎ+¿Ñ1þ¹1² Ê´óù¢H«[wX{ ºÉMºYs&­¶'«º¾uÙÌ*Òbó¢ù¼2]7·hy-âN¸ŽåUé2E—|ˆŽfQÙ¼UŠyÚ±¸…­’©ú‡ á•-zA†TdÑ¥Œ,>‚5Ž$×LR,^¼A‰®&ÄŒ¶ýÃuÔâ=ö]l>Èà¢[l|ìœtr™½w¦8`ΰ^¹¸ˆüæýûС̦Õ׈VÍvCèpJUFQK«¡,ŸH´.Zê(ÅÇQ*Cœ¤p!ÎÓ¿1âŽùOKV?Km¨iä Ú=‡ÃwH­E~“Œ¯ÇëÃÀÏÞ7¾ª~v‡ØQ;²Ñ÷¼=ž‘·}o²3 ð‰è²µÂ—ġɿ*_nÒåÚ_Òé5©Ôø²¯²À¾ÙT/ÊtØ1²ͼ5âRU —4 U¨ÀŒv·«L`¯VàL4_€1’¶¿K$³9¶LHÝo4ˆ‹Û-c·vhGLðl 4em´Cæ‰smvè<óhJüºüê”y8¥‹iôs‡âñLbŽí~‡i÷?£=>âˆvÐ.O´3öxåÛâUÆr÷*™Þ$5vbìãL꛸¿Ã+UøÓÕW¤Oç„N±G^"K¦YÿÞF˹ÖÀÛ¤µ²bÓ›Z‘¹Û–ÛÁVp¼ôÙsì©£Ò[¯BäÍ()´øí. ¸¼×l‰‹ØÖÙBZù;#"EÞþä'Ÿ|ÁüNF¢Qä·A’×^>þŒÐæç_Ó´úÒÏO• ðûHÆMØ`ñB  ë¶Y`#a"˜À± ^œðk´„è]ç®i»)qW8âAžìÙôa´õðá㟋¼a¾™6ßÿö„D=ø³ëÆ?5F5ͧ”–q£Æ”¯ø|Ê­ÕRħóÉÍÑÃ(;…T¯áÀäÃÍSLo²Á”ÕÂ’›Wž³vîÆHiѽ‚3LÐ\(» Û+”ÙZ!±us¶]äóJ…ÏÁ ÆõI©<þ¹Øèq%ÚVš:åðµHöâò”… ½ñ×Ý6zÆŸ£M¬¼K+ç‹­|ƒqmk3Ñsö*G…@’Q:LÒqã{C9gk\èm‚8!NÁòöAœCæÝ¯ßm)UÝ…««¨³É¦²»e±RNï+f þÌ.¤Ѥ#z«»EâºJæ¨Wߥ Ÿ(n4ØÆën ë×sw‹­›B>Y" z)ÒñlÊj--’ÛQ†l`Çû:"ÎÆùoÂÀó‰©Ôö«S¶ßñëâÚö¤¸¼Ò™Ÿ4Ät¯h¾JT„t³Á1ç ­hx•2±¾pîV‰Ùd°¹±ü8gíîÑI”µÙúÝ^p8Oãeõdpš/ô¹ýÓ‹€O¤nß¿oðyü'ôÌoÒ37Iîу6Xè)ù¢E¼î:²'Îôæ¸G7Ém=Üß Ž#âž8“Õ¼ríÎ$ÖSÏõÝM$ðö•{뵈ᛖÇubC¾ÎA‡8½{ª‚ˆV‘¾¢)¡ÏäéF¶F7:–»­ŽxÁÞ™¶^ŠŒYo»-½´µARA*¤¢e*F WûùI¤T6wp8Úãh¡xŒã’+σ¬ (bŠÅ$Úìñ±ð*³É„»LF“(5µñØnB”è´{™ü0ÄÚÿƒk,æQÑ’Tò£ÝyðlégrlKËF.AÎÉzÈ(’-#ÆBfõ(ytƉ5#‹¨äd5αwRìÕ+vuôª= XÚ‘7f~AD|zîDï¯ed«(”CÐrd[xÇ.øþ̸-àÙ7àÕŒâù<›Ñjq݌ң[ƒÉšC³UérüÌžÑ73Êhk§v]_ÔX5 àP_ÅÄ&;Ý="½ÛÀÅr÷Iö7`„™î Uø{šºä¨`[±#d‘eâ á•”ËH“²ç<½:¥·ì ïá”lÚîþ¹wˆPR6_€ó[$Ûk¼º²³+W[Lí¬í5zôùÃü8•³ØãêÛ£4{Î^­0ó—Ýu{­â{ÇónâŠÏL›‚3e—Ù²‘êÀÝFÊZeÈ^3ÂoX„"‚›ï›‡´¿Ä“Æ;â—ùÑÇá›ü ¶¬¼Ä¾Æ©Ù7FGг-²>`ún‚ä[“‰éo8£bDÅaùªgwå¨HlH×È"Ù¥ÝbÅñ‘I+¦Ýt¾ÛäÞí@C÷HCX^&Å Ë®ƒ\á)~â…EïFâF7Ö›DåC=ǰL'òd]î­[“ÈMõé…·š"ÏA$“=dA¥z8ÜG›üèÍýhm1m³pšèÜf§Â[K)SÙðNæïn³ì²XqH;''§fÇ·€$ †ÍöcÀR׃è¯üÃ×Á¬×Ÿ¼ÏÛDìæé“/òâÿ£ŸãíÈûk:;F`›•]"khÛøJ RV!"]uÝ!öl6Œ¸è1X®“dš_&¡Dw$á(Ñ&y,eAfºl!óìUê‡P¼lž¶GSY?iì¢)˵•CâVIÒý]D(ÚÊ/Qáf²â¶—¿™w³ç^,IwWIôm}­eÂôöœ—MÔˆý öŸÿˆ=ëWU؈ˆÆöãŸàí?&£óKÁxMÇ£èT"¹å‹[ ãBK<º­ÆÖ!É•ÐÎÒÊ(ÏÑéQ ʈV×DHTâLnÔÑq·<6å̕ę+ø‘ê:,K‡³“0.³[a‹• \5¯Á‰ˆÍ&lÇ>(‰¢‡hûGˆtÀ/ I«vûÃ!" ÓÈo!7ÛûÉG..Ľ‘½îÇßHN‘4-Gc:ôÖcïÍdiS‡éf]­{”â´O‹å§H;+R OQ¢Œ™ø{c;SMˆï_ê#móÒݪåÇsL–6»°~5¼2Y`nŠÒËfˆ:« ?@”}Zm·ý|$%úMQtO¾ © Ú^,$J^ ÍÿsxA22T2R1þv€"v`/¥¯,‘½"”[†· ‡·ç»lk­3%lq;Â6háF;HÛòò&çJ" R AוrE¸>[šÓ`~Ó’È“}øøë‘ßzòáh‹­–ói. àØ^KÁøKײlÅ ($—.qH²ÿ½þGøfeñÃÂ> =µ²…W^áØúÿjŒºzvg{ôV›qR«wWiuØÎ qíÔÖÜY¶5›,~£&'‘š$Ž£¶%YnãÍr-Ùc¯°Ã{¶‹=Ûæ=ËÙ%Ã+uéE{{çEÁÍ­ùhøÜNnûæ²ÞÍ[¼›¿È.ÜåÏD·¦h‘-M;ZY„çL?ö’Ø»+°ç;²KUœÞœÂÍ ÀýÑj*G·&ëI–€õ¨êˆíUYf•Ù^­ Vè<¨y.•J]’‘‘]ö½¬,‘‡¥£\L2A* ® G[9°²b[‰„ù-¨+ó[ä¤XyA.±¢åþMäNY¹0ÍgŠæƒ Ü ,ï–kíI}œžc|ïl^UßU~‚Ý–ðV¬Á)’"œ5ìÌ>,42ÖÙ˜‡¡>Úfûq»å´½K伇háq‡ˆØ†¡>Ú⺠/Eç…›<‰¢ï;]õRµµÆÔR³‡Ã“äTÛÍ$®¢¿”ÄjŸ*ÓjÙÕ|ÉÑÕ2Ùõ²r¶¤Uæ5,ÆÈU†dØMŠîÍH)…—!Ýë!IgÌ­SŽ de´lÃÏÕ7þ­’—ºÈKµqä댋 0t-DÅ`ÇݳX™rølOø‚šž$'ôG‡øpSž‡ý2ZÒ‘dñxUGý$ £ÈQpaG–w s3–—•ñv;K.ñŽXò;}”ñàªÍ%<ˆÀ‘êU9¯S­$áõ6Yomæ“pWØ‘koÛ(j0æ ‡R d¡´“v|ù·‚Ï+®šŸgeEzø§Px¥N2ùÚŠ³Å¸ÌDˆÆäÀ}+L_Zz”Ä ¶7£'¨7Ù,8ʰlÙ| ó=ÓåK‹U ‚ÑŠ#)ìÌc&¸”ÔzLð|-:ã"½ Á{âãp,(3Iˆ.›ñò aší1¯ùC*²5;ªoà{Õ«$ u\n”éÛ¶¯Oßsg¦Z‚6aÁ¦+X´W‰¯­7Qsg¡ßÒÓþ»9~¨¯•‡„_ ùãu}0~qˆ/ÔIº£,vÝ‚j¸è±6ׂȦÈÆ•0ªGT®‡/vÄØSr\±§/ç$–èâ#®:ä4{yÀÄÞqÜ}öW«¸wY¤‰b[ìA¹¸KÞfŠ‹¥ìŒm©M`1íí÷äб{d“éëmÿ96Óƒc ½#„ä% ¿oW× Eö>pwìQKçn5ïÆàœ†¬ã¶¦+ ¼© å@h!öºOF¡íC­ßÌ·øM<`ƒš$Þ7¬ulgfÖÿa‰ZS׈¥YEmŠº–ï#äœnøi1h‘^µE+ª ¡’Vý‚Dq3•@CÑ’s¶l?Õí°é 8ž#Þæ4å˜ mqñ8™BË6uãMãNâu¬¿NrÕÐÉĆN&(_š„‡6µ·wHJ` dЧøâŒ3H£{Ã㲪0ÌcC…Lv÷êkD©]äd‹Žd–¢8dâ;Çœa<ã Ë&‰%5ØÞ‡ÔÄÍ­ÓÙ犬¬å‘XæéU_ÛÀ§ór¤šülÒ(¹Ö€tumwóÜ=Í{ûôªgÓ•õ‹g žl$Êâyφφ²:d`ÚäˆÈÛ3 ;oÀ¶_sÅ|Ѝ*4ÓYÆéå»*⨠¬¹‚z¾ÂnÖ¡ë_£oÆ”V–<ѨR߯ÉÇ ïRÐû=VòzlK$….»{|Ùåd/³”¨z0'Áröms¹Æh›%Ë(²”àmÖ«Þ>‘ˆÜjì®x+YGòÄ!] în‰®Þ°˜oRêÂ:>EzL6¼½®ýÁM„^[Cí9[(:mÍÃ'_€NF"=†~Ͷ‡²åÿc¿ÿJzØ®æú™äX: výÌÝNçñ]ÒÈ“µæª—á­°f!1Éeùn%!R:c84_wTš¨’ØPõ2z ”©ZBëÒQà•·ˆÎ+ÊÌäüz4M"½ÆñD€Sç\ òåb©ÕÞÛg¡_›™©ôÖ¬…@E®‚9=?¬™úv<kì q ]ŽµéŒžú2äçFp9©Šªr²1‰†LÒ†r’о3\08Ë‹å—FY\eeI,Sœ—àŠ8+UBQa6Ž•ñÑž¾ríJÃ!éɇ—}úäW§—]H°è2ä«þSU °ªk¤~BŠ]2üÈ£({«|ÔyqzÖZÜï©Y\ÌËGi„ë.`³kº¦ƒ/¨ŠJ(ׂVÍBœÜu·Lv ìæb ÇÂj™˜\³3níbµ6«1°W|1è°!*z YØ3xò ­ð´Ò?¾¸ˆ6}~¶‚¶vÏøCc´…¥¦TÙ,l^g¡Õƒ[“ ˜WE|dÙ¼ìƒæb©e³‘ "ζœNtt•2 3™ÝS¹ç^Š¿‡X”ØÏyß~Î[b\rr’‚bovî 5òåÕœÕØDkäGñ›…¾ „;hV½,ß¡ßL,í¸”wû°á’3’PC×bÀjÞò­f¡ö¾Šî³‡¹:Ògt°ï“_*™T-^K$nœ±[䣬‰g²ÖŸdäŠ,“š)©pи¦i¬[ÀŠâ @ÏØ™™•¬ÄNU–.´‘5MY µ?B”|¨ˆú„”úÉ9™HП4ÿš:Ÿ—SðüÅO@ UîÞsFŸ„±±<ÃFsWUñÉÇérÜ[îGqçe¡ó©3*E¤›¬ˆsµ³‹ïˆ[:ÔstO{$öu[>À±½}‡ƒÌ`*ú´^ï»y®ã­r_Ï(Ïô˃!EÇ—ØKì¨üÑ×¥ÆÀûˆèþªå}P,ñs9Ï?ˆïqþ<³Øý å¾„¦’V÷…­ì^œ-´WT-Ö÷¤y?@ŒªWÉ O¦3 ˆÿGl)±yéâÞÉ)íŽ3{tÜwØØ\ÓÍ¿JogéñPuÃçí•ümg¶sìšÑ uøp¨Û}x?M‡Á8… TðË ýÁë~ãÇ=ãÏT¾¤ÍqTÙo¶Úo¹‚¯®ö„£?(sJŠù¥ ë@ò¸«&wP¢ÓGÍ{´c‡wNÏ­ŠoJzjœll%È€‹Õ×SCdJÙY³¤å—|VT%‹Ì¦l2æHw›8RGàgÞ™GÞZrN…]e„UX€ä8ñ´ Ù?ißé°ñ–ª0lJ„…Ïm‡oc“ãAˆ`·õ)îáwøGØmÀlVÒèÐ =‰D7[mÉ-å­\Öó“•Õ˜ª ¬5¥šÒ°Ç…âê¼\3ïöÏõi¾°¤nIaNû@mRþMõG :ýåµ/€ò"ÐÛ³JÔ…÷«£ïéÎ]ˆEÜÐ]G\Ë/eªÚÓ|?ò6‚·äŸ"t®Õ/œ…" ºJâlþ÷¢ZýèçVëÏzvfÏmFgî?w„E{Y ÆžÕsKìÙ6ßBà™T3QéÛ~nóýÀsz <¹î1<¶ýn°ðæ&Ù#ßô¡ôYDûhÈ ôYÜ#rã?ûçÿÙp ÞD[´Fô€½HkŒ(/Þ¤M‡VR¸ °‚ñ¶¦Š_M®†µ°šDéaZúhÚ¾ò’¶(ö×P¢£ :/¹ò ™KU£m|A,ZtÕX襦©7;E¸€›Ñ1Ø ›¥&nNJRb[²Ðe41 þ¤ŽÍoõ]ë‘Wã`óÈâƒØ#6!*¾`è¢Ì²f¡‹/)–°7³¦…¸«ùQl-%ž&§³}óמ5êéô¿-Åà!rÓ˯>àªãhûñ×Q­÷ªx±eá¼>©ˆ/½v%~òËêdV)†@rß³9b 3 ÉÕ:Îɪ€gu<)JP%gc´dƒ“³A†E\Œ¿à⸛n&?J¨IµHo¦S0üëóÙW;PªRº§ 9jÇõÁ¿ö€ÄJ>{ƒIh“zMëBâyÝøE)±±Liy›” ë´*¬¾ÌMè“Õ$ß E“ØqÚ{ý8D©ýTöõæ*=©‹ùr5NØÁÄçl¥ŸcFxe;I›TO}.Y7K[µ•ý >~¹L(óhúíѬ N§$©òGuã¿ ’â–nTAɳ$‰dð'ñj>I|Œ É ¯û ÌGaz,á¶UØW« ›ÄL4x{ÑlL©Ä€W/«/Næ‹-ÒÖΫxñáÐXmEt/„…\­kö.”%iþ×Bû¶iüåÑå4úY¤¢]VÜ+ò‰7©¥q[|²ºÆ—ºš¢Å+Έ‹Î ëÜÂâÄ QEq¶(™/¼Ìœ%¬¬¥,›;;ǼõV±eÍ0gÙ ŸñµÀ—ÂÝáãŠÏÜ‹èçˆ×8­Zú†÷é­ Ãðí%²¨+ÆnŒŠ*wh§V´ qz5™àôRQÉ´©`Öûn™-[w/kßUÅ:Uf•»•8¿H½£vÀkcq%Ö’„/œµ,ç$Îô'¢‚ÿÉ#?ñä æÑ㯙‡‘O¡ þbøàâ2C\ý3Çýί¨½¹mü–ÔÞ‚S¢¤õc”3%•؉›¬lÖrÄDÀ>¬bf¼¾ƒÄ¡ÝÆ¥Û`óÂÉ”K<òÒ5GŠ®;ŽXklŽÖ6twª¯Q4Ù”Ðj‹6p±M•D%¡ç ëUfü&R«hDã\¯Š,úqåÉk«‡æQ´%¥Õ‘ß~€Î¦Çå ][}_'Ûõ6Àf„òhão)maâx)Û™éëɪÐÐÁJ‹•ëãÚY¢Éê cRY:i­E*Ùf @!U<½ü™'¯N•¦Ã&òâån8š42Æâõ«*®+rýgŽGýžº5>.¡ÉVùáW“xxߨYÔVKÄqc-òÛ‘7íwHÚ.ˆ¾±×j™ã<ư¿¢d.­e.©dŽ[nè¡VWÒ­U Åöáñ5åmönÙUª¢Æ¼;ˆ“‹"=òΪ=PáâUv„v,8Ì“Lo€ÖÜÅF¢±+¢±ËÔÝÝuwƒ¢±ky]T¨·¤/ÈÍ;ãJ½KJ&ÄGŸŸ®%;ª$vëPµg£Æ¾_ÃM¢7ÜìÔ¥3|•Á 2tôsÛ˜/[¾áß^*†¾a‰ãŒ¹ùyU,AâË4ËŸ I\øÞ€Š |Ž|ƒxÿ’1ZSqaXÒä2’àù°BY4j œ;JsýÚAã8-:ÞNNmUÜ!´‘ߊ¼AÛùÉ+SúéœÿF›½K:®bü%ù¹Â@kf>¸øæVÉqtWv}%ÈŒŠåý,*GU¹ÄÒݬ¦ DËQ"¹Â}µ–êÎCÃùh5‘’¸£›ÆãÏf4PL?–KÚË—ðÎå¨TíŸÈÛO^Å_#dS}AÉ©-ú™œàš ±ëÐJPá Õkê× ®V!hˆz–\ÞGÊ[R8­MÊ*LÊ•Uî–4p&¯½n†j]B VKÖf7™>@ó­šËcuJì þ/w¤u£®ñ×–uOÏÛ$d^hccqËô’>éöíû¤d|9[U•ß*ùò´ieŽAR¦Zå ”¹©ò²æ/^\ .ßU4¨¿dŒb`vBhÿ$„`s £[ï#ñûdÍQ1œü+:#“¹j"œElOœ¬¢Ç‘sbRçÑ$ Ø› âµ—.q½áaTgÏݺ¸O'§•0“‘\m›úì.£/öñÏ]þ X-ˆõƒègµU}ùió[æ/ùý—ﲌÛdu~Öð…IŸLŸ›/ÉE ¤Ô‡á¨…3‹Œ¨Ñ:˜.‡l¢à8ó'-ìÌ|D[WÙu±®¼¼%CÉV]Ó>ÏÔ†—tlÁš¸ÿ„÷ñÏ]Ȫ÷W/`G¨EÍömÉø«S¸Þ²9Ú²yCYÑzË–yË’òxËJز…`à¾^AoÈ@ù›p£-õBt^¾k~žAf?úðül«Þg³çl_Rñ«¨n£'j˜›™ wsºuÇ5Ø”¸ðÏW·Ëó'%kÅþN¿š†u{RI ¿¼ª6aä™:)Þ%2v=I¤3›M쨘-ꦞŸdseì*%†‹.©“„ÜlÝ:ÇåuÑÏqv?úMZÿ㯳ûmäü§¯ý¼nž™†ì>:¯ à+T¢f9û˜’“Ë“ •# _1¸GQG²5"EÞ¯òÊ'uP[jjóºd©àƒ|ÌÎ52Cˆק·û]¾ËF´6µ6Ù³ÝPÈ1siO[“ö4nz†û“A¾¤y®APƒ8¹ºaM0r¿’©~š¾y:BÝ»b—N çÅøŠ8ëVqßø—3¤âCéE`dÉ]–¶©J @,µÝÜAn³üMú1F î# Ðç úŒ_>ª¥…\õ†seà‘ŠO£«€Ï–ÍnÈ tÑÀ!Ã}±2+¢bA\Ͻ ÜÅt†+Ý¢ýåln«£ÚÅû‡R6hÚî–ïDv|õh+i?¥­,è{ôÁ)7Þ³òšb—BºX³¥§Z¹éd¶ü3ÕÛ‚ä.c|ç2^Ž~“i:0îÏ2>l|Ìø¤ñCÆ_¢ò“æO$L£z³ƒr˜}t†<÷qlƒ—ö÷`Vü0ÝÝ"“p‡ËCH3Ž*xm ÐØ£Ò ¯ã£'&Œ¨ ß{åGé΀ïü{ 1ë½ú†ôÇQáƒ?ˆ/Dò?™±ñ…‹ÇègÜýËt—L~öàœóï÷ÝG£í¨6Q°±ô£¶wè+Œ8G Oˆú‡w蛟X“=©Ä¢} Î– ÷ÑÚù‹ØÛmD?ÎOúR{+Ñ›ÉgúÞé™Ëâå«Îøe¼í~d0ùa¾åýž\PFÙOü§!c ¢È7`þþÈO X•Ü!-ã¯ÌcŸê¿%a²ÄÊMþÒg*òQþ[üoϽ֟1ˆÍ?ýæï¾ùÓÂtu{“ ùŸß¡_Sÿå›oš¿8ÿq¹ü7úw~úÍK‹îÑ?æ?ô/qÛ˜ÕZÑo1Lýš ®C·û—TìäXï4äãéÔæmó>UU{hȨÊN4}t3À–ì0Ã`ô¶9tÝFкX#C|‹[3·h_z{ÇÂXDyµ£ˆ7B—Ƀ#ìĶdÈ>•¹‘ßÍ‹uq¸¯j% (qÇ™,¾.Šã—Ϥ9çlV;Íé¶’´¬ëz­©.Íà¿‚1¯ÿC½P•ƒ7¶dÕùÕbdÕØSh¼‘†Ä¹Ï(†¿E9a JL`‘Da•¼ï$e&ø—ñ>ž‹‰_»äU”%¾Ã™Q¤ÀÞ\˜ÍxæŠ[V%·¶î?E¹ŸÅ`T\8ˆÖ´ˆ•~ØC\ç㉦ á°çBU`±K'-0)tV (ÚÎ ÇkÙ#TØ_N¤,{÷/Vò/­Å’ëîvïÐÁ#{’/l6·ƒð}†·†0Ðá×®r]ŠwŠî¤^(ðptgv%šÌ‰|gº3W÷*Ìj·ƒ±Z›Ï;‰)‘¶¥ÓJJ:¸=ä‹:>ÿª®ŒŸ!¼ûÙÄ/ª×SÆbyUEñ"óúHªnPÒ(ü¸~Ò"æûÄðØë …)ú³ª×`Ú·À›£ÍX£0ºCúÚL’žszV'ÕjžA†Ýää˜_Ÿ(ûãÎSHÑ'Õã~PÈ@_º¦úK#ígáH;úvìs·—muPcɇ訋ÂKØÆãÕln…×pmÜ?…¶²<ú.PÛâú)l÷’’©Ö÷‹ó¿>ô{Ä Å^£ „.Õ²h“âøa`ÐÌ›(g;J,?®ɪð " èõñS_’å4¤e'kqÖ‹ûD¹«8É·§:³1F³N³½fgU”^AérÑÇ@Š=ÚæaìõoOMõx}(0l£ xl/²ûÿs »Æÿ¬bíU >×4«vÄNm;®ÍØ~6á¹(©´[Ö ÝJÊ&ªÅÏ £{Å!SÜ¢ã7¯°“úìÉhøgĆ»Œá¯Á•jθÝéJOhŠ{Bݲð³!F¬•WÙ^/—Qeþ^w'Xb=ªÒÕií÷|¥ËwSjöbq•œ‰Sò•Þ0âÆ‰ž‡¢ç„Ä^a R̆"¾«~Ä«cOc.¾?ðg1â+d«}&öƒÄƒŠÑ1ÎŒçÍ–Ç:íÁ"÷ˆëE"àÈHmÁ †cð¤Pû»ÑañÆ~¾n%ÞÇèUÂdŽsË݇–ÆpÖg#nÜÒuôLŽÐn^—¶Æ"g…&[¢²ï2Fa˜tîÀøŒOßõA‰}…Ãf5Yd÷ä‹îõéÐØ÷UUQ®öý‹ó¶Ó@†m}¶¹½cT§ÍÖ‹Ÿ=|Øîü¸©-Ã<üH¶<¿Ûþ mñ.oñ?$èÍaäÇhÓ¿©Ë*ß>ùï‡æß^þ˯Dþïô—ðo,òÿõ›ì0Óíߥ #¿1¼üÞ*Ìj_·—t=å”k±Byûì-5×&eäà·*X¶£VÕ,†¨®½Ô}ãÄ*îò°vUµ‡§ îÉ[µV3 i")8#æ¬J”Á[>3Y&\þ*CŒÅ6d»!ÊÏ£ ØïˆIŠ^zwfÓø²¡yœXˈše9†—U *;ãÀ3œ›Æ2G+ûSÆ´âá?&ëP—žñ.]U×vÜ s†¾À ÿ»ÐŠ‹J¯c‚5}ã2B §ˆ¯Xû~Ÿ _•Œ•û\(Ö\p‘Å©Ì×¢€ÐH‚T׳pŽ«Q)ó4ª_Q·ÙàÐ⺔ç7”5áCþïÑ«®ãCû76ÉŽÅ%ÇÓ²Gy˜·çn‡|d©|v÷lIŸì*àçÙÔ¡™‰«BYÊé°µÙ…$©qC¡•ã¡Bì×·pˆ±Rô©ôtŽf§ÆŸ£>CeÃ3䞌0 'Ç>ò{ÇœÂ%FO 2)ÇøÆ®Ôþo;“•b_%'‰<_wÍâ- ¯óú¢„$±ã° :ês©¿OÛ? 4î¾TWö8]ËÐ!Š·}Š÷ó_Å·:¸ÝµuÕÃSi-m"~z»´ˆæœžÂVFrf1ñ×€¹AúïýÿÀµ#Bÿ™Ün;£ U‡Ûißë]‘a Ìɰ$|ªJ—:ÜÛÔÔQ³Îµ nÎBˆuOdW¹ú'ôêé̔ܮÙù__Y-–¶8IåΓÞLxO”ðö5jy °h>¾Ž´ª°çL&ª-gèr!«º$¬ßÈñ.ÓW[ìßNîÿm0 zÜ KUMp1Ôc„^F,äŠ3êÌZºÉi=Hpæ[b×™$ q„N’$º)›/IµNrY¾ÌE¥%y“«RÄbŠJí êÕ€ËÊ•Ý=ÇÙl[Û–Èr9A¶%° `ò­èFÈ¢4BFIžÝ Ž„4Î9Dtë›x Cº;2QP¨°ÏUE€êô[“u E ÷Åïó`$Iê¼*&¡[«]ÉT(Äà¡Â6]y…ìÅmòŸ3?8ÐÑÑu“XL*!åûÑ÷Æ'û‡ †C™´N]h Œ!yžNáµ{&¹¾´5yN¾z@Ò|ÎõsTõvü¢¹ˆÀðãèn‹ NÕÐŒb#¨íríסgœÍ òǽgywpŒËá¾!<U¸¥¡R%S#_Ù 'Å4Dì%ñç|c®|€> ,áQ9:gT ÷P× 76Û0 ž;å‚bK&ËyY›'”2h™¥Õk|oV=¤øU™+ëˆ2ÓV;»œæÄÃpjÞ'^ j¿9}òÁ¦5ßšôŽðb>xÚ¦ÎrÆ\ðÖ¼ÿä#S™Õ4ÏÏWnÂÏFò'~íž•„Ÿsü<ø>òsB<l•=ô÷5“Ÿ³Þñ=m·7åéî }¼wžž§ôçi<¾C.ç)º4éO~mO1;öø&<=†Wë ø´ÙuƧ½ãïÙI뎱?ÇYzí>çsöyùw%uCÎ"Yô8OeþÞþ~ÀP Û»ûÐqÏÄNK‹Ë©6^éíí»,„œ2Ç• òñù;KøxþµyW‚u…ëYx14icúXH˸84Iÿšäf<<7^6?qžƒ‡‚ïM&ÚøÞî9f'œvœIë 9ùAŸ“/Ëwz8ùÊ"N*Öí9ÞK¾Àu;æd×™<'Ñ–ã>gñ™GèÉ}Éo`b€©ÇŽûüÍ™úaFò ¦Ï>ðý‡x÷¢0Ù¨‡0[_þÀÒíynKým¶§Þ¢íÝ0ùá~Ó>$ æþð)Û•7,}Ìß³±¿ ǸgîÞ„ãšã1Ì„ž´z OÕ¾ÏaG¾òH ÈVaö”2îôViÏGàåöçY¿ÞÄô2©Õ²$‹¼Ú€ìpôL‡ò¶|7ò8üçø{®ù{@›õPÚNò¶{oޝNïٷ댈¬.ãâCåsš‡ÍÅü›>d¶™¿ôpjÎëÛSãÜ|ù¦ú–Ù¶kñïâžcœÎq^»w}îËWß!î}àVšö9}Œ:þ1ÚWñþÑa_@©=Þ¸g70ŒÜâêñù¼VÁ§0¹FUC¤£kâÁ0ò¹'R>EÏf`›kjþuSFÒ—xd½ÌÀö'_™п8¹»ÛgWe{7ÁÐ:Ì,ÉvrŸh¸]iålë ÚaŸ·yò‡¶8Dñw÷)cÀŸÂ çÓ`­x‡ˆTï‡Ç xí}wxƒ±Ø-ºílÉ‚Ëwï›ÃËß7§è9^0Ž€ÞÙïS{t\ÅØB´-ˆº ÍñV{ P&¦r“r‘¡pž±ª-ž¬J157~un ̯Ðth±!5ªa°~„…ê˜ÜD刻y‚î÷êp«×Ï—Cø›3_ë šÿ“WĽF›*‘ÕNU@g,¦ÓcªšUÀ%m܈Nß j‚Ë$µ@¨5m,¢S s[<½=G§*jH×ÓIû/ è$.Ë<”£2O§Ð„¢SKa“5XwŽ;VB0ZŸN­z€Z»·‘*ÙÄu„ÆlÎÓkŸYWT²»ÎM‡ æÓƒ2zùÜËìÐÿ/œ NÔóØ#õ^ÿeºqo~ÌTÉÏxœ-X°(®³tˆA(¢ã'A¤¨T€J†\l éuêzŒ³¤®w'"e»ãƒÎŽø¼uàU£!nqѯÉP~<Ô ÷”Ò]ä¢÷46¼ÿ%%»‡s¥{="åî¹»m»;7‚ʈ7üB>¯µn€”Q…³é¸z:õ‡äL$ ïß2j9°3"ŸRI?9oJÛƒˆ£ŽwÛè±í>mrº­½ä¶oL[ ò»H¢Â™»%mÛôÅÛ7 mðX^D[³'ôBâêÃ:LÛcã½%´ÝƒÞ:`¨³Ûvƽm´užJáþS¤WõtA}ïJ c Oæh|ˆ9ÚîåíÂ?îÞ–Î7’Ý]°P~}a)••±¦ó³gÒy èÜæ²€ñi{@çà¹í:O¡ö²2ÿq«L¶ðÄQ­3á&™ÞAŒˆ.÷oSí?8÷¶"ûñ¹·¿ÃÁ¾[‹ù€C7cüò^È‹ûÃwîÏBñKÙáklÄ߃<éûÆW—p¥«œÓvŒ{?¯çÂÁ 46)j´…sîr)á{W ÿý‘sm-.$«˜‹ *&$z«‡±QôsFÇøoP“HkPmÆnEšŒ«e z•›´&+³þÎ.÷!%ü™Ìi)±Zã¦1X–ÆSsëV`Üuv Íõ&>ðêį̀ì&7†µÎ½ÄŠoúmû5h~›˜_k®A,Ä®{8œ2x_p‘„_@+Å­³s‰{ÊŒ™ÏA F (aãùÍjršŒOF;ö‡¸tÈfX¾È14àSKÎqÑUæ6 bzÜn)÷Ýß©xXðgLa6zqà”-ÎÀÔ/qÝqL¸¡€;WœMïžìºß¥WËDÖ€ÛÞk(ÏâܬZ߾ܑI5iGàœZ@¥˜DI½—‘C ªÚ½štõâéêîstµ· °‡S ö qf¿¥‹Yâ‹jIîϪ‹ y`ò›9H'¶Í^¹_Œ95¬Q½ ÍΪ]PŸÉâÌlÙïucÇø“%Ó08¨×$Kc§‰ ^C ™fc 3ÑÝ›ÈØP(¶¤ÛŠzúØ „§ôØŒf—£Än}ËO¢á5·8Vsý0`\ôÊ\ ó-1Þ¢Ÿ“èqä75e¥jYŒ·›ÑsGu»; XÅ Y£õ]ÓsÓìI[jKÍNkß–ž›\­ƒ©JÛ7¡gc‡ûƯ§çv ~º€žb¯-¢ç4[™Ñsݽ éÉçÕÞ@M { ”M¡jó*Un.¥êÄÚžcO§koFWÔæïc2{t^g›‰gú²ªËX@ÚC]‹±Œ¸(ÂätëFËØ†5°ºªnÐZën9KvþÎÓi:Þˆ×Ⱥ+ËÜ£…“rÆ] ±,·û,ÉEgž¼»š¼DÈn‚½‡ïŽŒ÷fÙ+$œÒAÿþCs!ýt ißïwÍVxRÅ]>·h‹Žî&¸3UÚédÛaÇbýh•‘ä¹Ç•ˆÄh0­Àœ‹Ùˆ Mk7™Zj$HÎÂÙ[–Ô0 ¾¡æÜ~”Åsf`”"ªwI-?΂ã,¼£»œ+¿ÝX‹yͱhÂEP1šÎ[CýE*ÃMŒež%PÁ4]bŒ£¨FÍdo´*®ø“íÞŸÑVÛÝ‹ª$g Š/Q€L:Õr`÷Á4µÉ¬Wi“ð¬,ÃPºÛ];g|–`ß¶ ówÐ8ñlÍ¡¹]¦cîTM’cXî1a»Ë% ø¯d"ÿ[ÑÛɪƾÕg!ÜëK/™ØçÊç©q±„nr]qƹ<íÉ™é­o º'vÍmr›ÒfY63ÏŠìÔÂG± iXßíëÁ\î&¡ºwÓhü%ó,ÕsÎÀeô E¶)*®ÑìFó#}ÁB!U7=Cþ öQø°$6)8$z•yó«ûÅ¢T—_½¯˜¯ð}<®j]G4Äl‡¾¹2Ýa› xI ú*deäûMÆ>èdý‚™'Kf>X–Œ¸dQ ç\¾òSÇÛÙÿu×â1žG}¸¯1hZ­åu[ñö‘Ê¥ïÂCJóìÎUÛÑ´U·ƒK¸»7gqªxñ‰/L.‚X0ObÊÅ©‰·çxp ë9ìóÜS’k¬óèï ú·àAWÅf:xã þžÆcÙ ÿ$Dü§ý˜³Ð7!z ε|t‡ v Íð:ñ”߬–]Ñ|`®.”{x2ƒ×-,ö¼:Ž{ ì>;ñïÜ‚ø;fo²Ïô;û0ÕpI‡6EŸoIJd?ij7Ž0¾¶¾Á%&« \áÇ …_uø†Ö² ÀƒÏ`n-á†è)3Ð[@>4øÁsÒ—ñ#Šâàqg±Ýwå:ŸgdÆÝ2c¼ÕBíŽÈ~`[ìrf„s*iΓØp/Èèû³so÷ŽTš÷ö˜)ýåLñ:gªºü†Ì¹’âXÌžûÁ ÇuÒ‘²XˆG;dÝý›+\ê(¶Üî3såðögXˆƒ#Žæ±sî쪃øÙö„_ð„Ø>[¦–ØrŽ…tÒ)í‚ÎbúîTÙèè çÁ6YÓNç4ÁÝzQ$ïsããäP wÆÎa?áãNlõU¤y<àÛ¢¿æ_¶5:RIV„ÔbÏ8Rê88Rj]Q:'Ù’ŒŸ-ïu0ÇU ¡ç¹‡µ$0tŠ”’—YŸ êôØ®Êð)>æò­‹‡P™aãÉH*eeý/Nq_`û7CÌÎõi½ #{͇þóg£õÑõ´ÖÇh[å»'=ŸÜ"ÏùiÄ=¼R(°”¸#mIù‹H{䛸s´=5¾5'Ç|ŽÆ¸ÕìÔâF©¶$0€yølDv®'2Ê‹Ÿì"&˵” 9>â=1)$Ì‘¼·Ëm§Ï–{^u>ÂèÌÞ;C9p¢ ¸W辬ž6ì‡]ã‡<•î×x7$øwMâ£Ö*Ïö I,È}sá´?í‚û¶ãñkÄĹ?ÂÄ\Cì{KHgŽôÚãø¾ T¼br,£¿vDPp»Ú ñ@cmgžÂ‰­ŽÛ©¢ÚsÀKp¤¶N[:œ¶Tê-ý®ªn¿›zÛ…ûá™ oråA¨ðv9[|Å ñ>â¿X™§ù‹Ì‰ïW½í÷OÞ¯¸ŠsU%·‹uŒ8ŠQÆþ-G?Ǹô)ãØ`Ôb…œãqÃɘ zÆP[Ëå1d$øHšl"Ã<Œ~óɯ¾óÎ; Âö® Îê[¿ÆskwÐÖU­ÅžÆ?ê@O)pû®ÀXf¸W@špdçœÉis=I¶ï)ÇX'í>¿js#ë¤sį:³©@qnÌæt¬(î¢jg='0LÕŸ.–šß¥?IËW¥û¼•6Ж\çÚsl!¯q„"Ô^ el¾¥ê9†¿xåö\ÇÎW©4—N]W¥ZXRŸÊ>~ô¸Å~øø3÷‡¦§ª—5l×p6†&6R‡A_²dtK™›YåÊɘ²0lµb¡ø)£$»KFI‡"su¼Y¨"|ÝRh°[ ›ÄÎRÃ&%€Ý8÷V@ïæN ÐGN#ºõóe(猜¹q”æ¡_W1?˜’‡‰q¤×f7ÍÑ«2rº‚ Z×nB¯Îzil»I²ÚöI¶€PÝ¡h‹«$—h;%§Ï[ºbž<ÓYh4DŸ]ãwÊ<Û]®m„‘Ðä@œ»ÖŸJ¨½%„ª‘M"ÃxÆ;™ *¾D›yÜámÙ¹…”ÛP®tm<¶Å8¼+öS¦šn_ g\¬Y½ÂÒƒ³ØFɨÑÉõß]¡„ÊÝp™ýiã\[ H5.G‹³ ˆVn Y6’¥­Èâå¡®6ë•÷éd Öz†É1Uë«RèÆÜHޝïÑtáäHv6£‚/Ëu›’˜»v¤äîµ#%u|»$h»)®yƒi“¨ÒxÚ´IFáÖ®:NU,AéoÄ+³(u˜ph¨™?åè7x–À…á®õ½40åÓ>¶}®/õ˜9]©Ç ¤P}™vÀ‹g çxé5ó½hÎ4g ó]5ÛªÒr:Å´â'_œÆ\ÁY 3î}…¬âø›tòüö.ƒ¹( Ýä)¿×ó¶»”·ãšEq.qˆþ\çt®n+®Ž‰«›ÈWßvˆhÀ[ÀKµqRÙa°ùå›à€õTcÒ:\ˆÝv⣠äkŽÅ$ºú‰«œ¨ËŽ%ÚS¸ŽîŠÅ•cÇŽŽ’j±.¼µnŒ±ØÈ(β[Ô´ 0A3û Õ×v˜Í²L¶©» ^õ¤,Zˆu#Sá¶ôú:#%yön€ø;(Ëɸº óÒ˜Ù\æZ±~~>ÀhûsÔBòmð4‡$Í_ ¸?[â²¶È÷·¿|š›pM$½ƒ~ {)Jî%™ Ol+œ$ø\ ž²ìYy€ñC9^Íë2LÒ$õ˜D§UC$åœ(ZZER/N½T2Ú“ XUÜ1î´BU|ÊD€žA€ºè7YU ø–] eýêrÏ•“U[÷V‰°‡’6¶¯Ç­²7¥ò6¿v!^Uìuh ˆµÙ»x¼.Gü[DEàZþÿX¯ßc¬×©:ÿ7Œ.]êȲ¨Ü¢*×ÏD¥…6@Ë™T›õ5™ˆèÎ$¾Å¯”(v!ŠÛ,Š -Šõ«Q™?é­¤È͇˜æœQ±Å‰Ã|8j£;ý-vú["1ÜÕ†/{-2;GÕÚ¦T°xÀ†1¼nSËÞÖœì¡zZ}¸"„8¼Ú—Ÿ6IüèÏ4$ƒL.Ò°÷/¦O>¢E0ŒÿÞ0þË0’9›LÕØm§lÞï= WŽS¡¿õ-½dß®\bêW`Ü%q©|GW½0ÛÆx®æÙÛv®ï t»ìÎu»Ô‘lm1Pͳu1Y–v1uw¼ÌaAý~X&ºzÆÄn½›à4cUújo'#{·˜ ¨y!³fÒô™°ü@ñÙÙ…Ú¡ótnâ\çõfø¤¼ Ž%_‘¬`DÁô ß ÷»õ=ß·y ö¬­W'Om½Ò`'X:רv:ß°Ùâ2S@®õ¾'ímaú.Ñ™—ôlÎH=§ÃþÑ_¬ Ø~WÕÙͧ.,œÞñ΂9ÓЬ+_ýú_´ 8ר´ï…Ĭ^Ó|JZ®È¸kZ7.ˆ*ÈeµØ{þ4täÉš}ÖlízS%oÊÅZܾDÃÿjœgH%áëîÜTN0,ªépßI•=‰ªÊÖpù=$¨:/A f¾Ê±ÓÀqèuê §g™©5¾ƒy'v—ˆxÊh!žvšxä¥kä/or$Bà:‘±‰8£µ ¿Òr“gú©=3¾{tàcƒ¢´h<à§Âð#)JlòñÀ5F ¿MÄ;=C¶\Wbv~œ/¬Wy'm.(FÔ9sî¹Tåˆ~Ûå¼5U ˆðûo?@»Ùãò—{1€¹†ZEµƒ”/Ž2v-y´Ùºgn-ÙCw"râŽïõîG ×{è¹ë÷n ”Ѳ“¾¼ú®wÒùÜNâþμn²‹¼Þ@•÷o¹ŸB>Çu*àu,ÛRÁ@xO¯séžò}ùUxϸ“îÞb'ʳöƒûêêæ¹7·y¸µÍ9wlT×IÏø÷`…ýÀë6QÐã^¼Þð$¸Ž09eá>:Ò8ªßí¾9~ÊÙÎkÝr›ôç¶É÷øx™ NY´ dXgç‰8Ÿa¹ï!B´PîŸE¾Ý^À ;x1ל‘>šéØ> ¯e··PÎ[±ŸAŽ•wzôš=øð‹Åö-P“çê²×Ü4FC„Èù•- "ñ-Ý{ÉΪ­èÝB†æªº™ÀÝÃ>JDÇǨbÙ!®oœB†É™p7}û©¨ªu¦Í=š@¬;LL†XÅÉ{àŒN¹æíô0ÙS]²Ä).M9££)´àÕV-iãÌ ¸g !8ÜÅ3éŸÉeéœÝâvÎNWr\ÇRƒQÐÍ­;gª­ÓhuæE¼²\ÆÜÝ"Ãu^ØõÔÝp+ÔÔ]F¦–º0,Ä–ùãý%2Ïšå@†<³½tzKñÊ ƒ:ígnìípÝ=¶ádÄzßÃ]¡ë·†V¹Ä6BQ׃ vš¯ð{*’S‚Û˜¢wÍt…õ¤¬âºL³Ä …U úƒZ¸ÊžqµžDzñ*@Èâi qŽ­ø{#‹Ú- \È;£ŠÅŠb’O”Æì$8f!ìØ3pÎè(À…)Љ|æ·¦ÓË7ƒé:-v‘é<b’$‘‘#—IycÊqU?Çú_+ •E’Š5E&¨'ÎáÕ‰UeŠX3-¢H(²–£¯ª1EjX\-Aª…óÒ5Kæ«¢9“·Y±LoFEÄölŒ3?*&Â4@:¬N-›Ÿ§ß—ŸödùÍ‹ûS(|-á¹¹Mã ái²M577áºq#Ó”Ñ!9î&ŸžÛê»éG˜bÀ­,–‘3ö³tõxàqž'Í’©É]¦<Û¹vî•0q¶Šæ:D„J G‡1#B3hCA¡™º\ Ù¾X3ÈNY‚Ÿ&ª#%Õi›\ÂBÂköÝÔ#ÕuŒì9qÔÊεc‚(6ˆ’VQQŵûÜ„`›Õ`Rx ›5‡á¥7|l†ÜІBöíQhÚhRÍ'HP.lãGÃras‘AìÊ”ëüMæ cxÇ•áÁàÓü ·š Ã<È#Ëø¡ø}Ö^Ÿ}|7çÁÛ8ÁnSý>nÛt~ætÝøå0ÖõLðzn¤¥¨wÑ<Í6x×äD`r"0ˆ—–®ª=¶°S&þ2.gÉ¥å,IF_ÏËZ"­tExPs(^¤ú\öן>—‹T8ƒ5ãï.ÀŠÄH”ZD ˜tô ä^7HVÖ›mâõ$‡ ¢PÙLɦx^B¦‰_Eß 9àV†zÖf(‘5²ÏÂõJUão)ýŸhÆú µUò†ûÌâR® ¶„éÖúîú#/B‡¦BxeØ+ΤØä¢‹¼¸Ç¥–Œx•¸×º$öÂå†WA¡D¦<Ϻpz&À8Í`Æ #ƒuªkÖçµNzMåŽÝµfY;æQLØ÷²´”bì‘k!¦ÕZ­òhÉÃÅdpoµN'ZšO©ô˜”H\E)­,ް¬/ðR» ê+[H¿´@ú•³ÑÒyX}…—i‹ê3!Y+¯–Œˆþé æî›‚Áxh’ý¬<Ú´öh“ÊÁŠHâwS&‚‘á’ô~äIÊêJºá\$ï„Ø#²KUeñ(Æ‚[!Þ4’1¢" 2³}.¤‹Y“’è9’ …ÀÛÜàÕªê†r}!Cˆò:3< $—%Þ2yC-$$ÒÉ#Ó3O€1ƒhïª HÉtOÇnòDfZy@¡áºLdÎ9a}ÿ¿ÖÞkWí*×9}Ñeêøœ3c¯õÿÿú/ßÿ}o3uÀzÆfå€"Ká`´ÎÃHõ¤÷û UÌËÍ›á<;$,¿“ƒÇìì Ôû™x)†—Wï¡òŠcúɵó¶5|ëw©;Ók7¾S·†­“S"zA²µv¨`Ïýj¥›6µKÍO.±sMí®lÍ..JJ¨Ñ&µ(H pg>¥Û(âÁ4|à·i䟻ÎôµÄ=Ÿ›[ÆðÐà¹/Ë_€îjxnp½»¥à:­Ï· Æ'{o5oÜŒúºãÕ½Ó 6vú*­ß¡°eˆý^J A,±¢.t_¿Ðý4Ñù¡US ÀÇܾI¶lâׂ¬.]6Zršß:Ýï°N’º…û«7Åý,P¥§}q1±39¡tŽ­á!QÂÔzbÕ‚o*çq+Ë:Ú#ËjÒ žº´¦·ß×ZkTZÓu™ßãR p0(„—s°wøíûÈG*ǼLa@¹è楆ü¹Râçú[Æ0®Þn¯°È¡°€óº±8½Ú úIƦÏðR7ýPIæ{JßS?TRVÜyPYG’1ʳòLj¬ÓáoUE8YqŸ–Ìv<*'Õ]údé[æÑý‹÷…ѳä¯Ü!E­ý0P;¿~¼ª¿)}aCåÔÀªéa ©u¡V¹R!Fw8^OnÁâ×ñcÕB ͉k[#þ/jÿoJ]F¹6iquµE}¼­-ª{7 Tã÷´›$E:†É öãýk}™Tú'^JîµÛ¹Æ0ö…¨¿/9×˜Ó ”xQªú³â6²ÈÇ‘&ö”VèeíXæÛ-Óí–%ñÙ¦)ó´“PîœïÑžþ†=ÌÓBI¾(þ¬eSí)ÓOg¸‰,åcÛü€l•î+¼ ¼+÷ ‘ú”oþÝ oª»vWÓ6“˜Ñ¼'šv­¨î¦ºcλlQÄ7LQÜ„lZZN€{ã"Ú úëã~*ÌÛ¿PŒî´újùâ¬(ƒ Y§Œ‚ñkš¯nàÇZA¦Z?:Õ}øNK"§Óž*î2mª²w”ϤÅÏ¥JçžT fѪÿ82êõPmzþ껫ù=üDfÀ]ïMÎ!eŸeCT†ß›ðQ=˜Ziz,¼G› ]XÚd"ͬ-¹Ç¯E¿‘÷?õ¯Ãü©âÍ »~•⾕º ¸Ÿ5Òó¨ïE™'¿ÎKB$6«Îwò\ñi'úGÚi¾üeñ-ù½ä¦ñëÚIVìa‚üiJ3Üăª»QãD¤E‘Uú‰RH@dß(ÓWDðgŠô1ƒ E¥kŒ÷KEéZYRœ)C.Uü6sܶ%›±œ‡¤µ^@‰‡ƒ@ÄÑaH Òsÿ*úÞ‚©º–q™ˆ[¹½ÿtð@wƒ´•”t¿£×rVu7XU£ b/AÅöX®(;QŠÆ{òÖh”¨E¼ÍÏDúO±lIœ[Bë2äyE.*ééÙ’¿q5*…Æä ÊÁ¿Iù#.}r+~uŸ÷T[Ñ7Ï?~ªïjqíȸ2Ÿf(À½\´T½«#c̺Gé‹E¯Š®–%4k ¤l}l ³ÛfŒkTío:á0¨ÆŠ.¯Ù)šÇ"zb:h:×äq]λtýÝ#Ù¹èê~×e ö‰M¯ -ÈÇ\µŸÛIÉ£J 4rõÔœª5\‹²6•‘ÏAPwag_•x§~>­»ÕˆJVCn=!šµÞÏ÷nÌkÖ($¦ÝU®ø5ÑM™å< d«s©––Û=ŒGÍívŒ Ìãnv>ÒîpdÑG°¥Nw•çô”眦‰ žc:OèB®ØM®èN®n¤›\é×rÅí~ž8yó”“ǾlÝ2f4ÅoíÜŠ¢ŽùÃìa£CpᎿ¶PÓîVñ›»˜ïµÄßyKüåþ¹ôínÓ†ô#Þ¡3EBt•Iÿ@nöòw¸ó@_ÃÛ)àåx^Ä•ÞÝJ½nÕCô5÷Œÿ4á;â¾jà¡Ò=¡%ŒºØÈ¨P.I6ªX‡\£á»Æ>ݵjèФ.íèw°CØq€;òü¬å)Ìäá•41ø šFÝ’,ÀO³øí*fàïÙÑÜ‚ŽÕÍV¤Sd±ÛªŽè@†8Fà˜=/ÑýâÇÞÑêJ¶â³<Ñ¥oÒ¯ïÆSóƒ©wãæTœÜÓÓQ”]°æKß~ïGoòõxû^=Ì dô'òõx¯ǶW^zû,²áBg\äíxgòí¸‘»+”oÇéøÇçS”‹¼“+áOÈ6 óŒè“± 'Ɔ¼#‹qcÌ}Pì…tÌ~Œã‘—¥¯¿,”ó…îì·åQ-Ÿ° i5ŸÀjÆôã ên'4Ý;EÑSçÞJrC¦ÙPÀ”ÇÎ{¼U¶A¢ñzlÀ©ìPTKLç´úhÀ¹+Û6†F´V×[|›wÚ€pn–¤ÝÏz½«[’¬mÞI›*[ÏqŽ>}Æèª÷%ìýþ•»[žPùO®dÙ6HGŒÄ/„5'žñþ¬gü$ .0ÇVk'ÛDlÚA_Ó¢{ˆX‡M±TÄ:L;ÇÞ«Íæ>‡§>}ÿ\†§¾nû}‚~#Žè7(óùá>V÷‰öpokqÉ#›nS\ÚF\Ú³=ÒÉzÁ©}|¢® ¯µ MÍ:)<öpós²À›­^ìð{À†Þk-ïL¼ÕmãÇÆ°dpe^AÝ™òŒ¦Œ7Uaÿì¿âÇ›m:y‘Šjp.Ë%µ°²²í²gÛå4/‹m1†¨B‹ô: ’>»é½ÎEõ:ãQŽƒBUœc!G] ¯}åY†?Ì|~¯2ŽM=ÉRo„¹v°güof¨Âtt"UKì<ëLä@L§³3Gf† ¶`u˜B^|n4vJ"°ˆwð1E˜e°ËF€a]H†À6ŒªØ1K³°<Àâ(N@geê{ÙžŽNÇøÑ9RRo蜈MâOS#J#š˜”…â³ ×ËQg¥ÛVšÀ¡gZFÀ§G½öaLßæœok'ó5ˆfÒ!Ì8ßÓ‰ó R#,pÆšºíC\ÎîböºÀ4a™Õ¥ûüXËÅ‚:h=ã¿ÎÑàZˆ§‚LöÏÛq¿¶ìYEŤqëÇ=CÙOçÑí¼ofœw_î^ò1N›ý‘{oÓæ#Ö¾€ö™ÓP\Òµ3ç_¦Ùž"Ð]L C°¾/ƒv¸¸ šî ¯©~Ø zí„´ònlX´x÷3ðµÏغƕiÍQ_tl¾‘ž”BÖe¡Àí4ú`Îù,¹^ôŠ Æxàq«#ù=_¹ä¯œ¥Iñú-3/êÆ|3Jû]k1¥F¯+Ö&å¿– u gƒOiö±,gfK7R*.%¯%q{¶‘‰¯bMB¤ )Ô§óE•x˧SE‰<—äN„[NËV$s~*Åu¡@ ^ç‘&ÔiÂ4uR¶-?1@†°YcÞÄ4ÖR’Ük‚=QçïgûÄ$ÜÙ÷ï^þÄ<Á¦ûÀÇhv_þ„”‘ä©iT‰~"òܦqlœ¿kP ç±MÈt/³ÁÝ9•æ*Šlwï̶ÇÅ|MÖÐjí @+Š3«ÔÅ—{é¡›"_Žë5‰hr#«¶GƒÐ’ö‹#Ü^&—h¥)/Áöˆ^b“»ë:S*Z :7P åá¯ÇÖVÊ[UèÆ6­Ì8¾¾!7‘òÛL|8ž$>È#T&JÉ/ÅŠeL3ˆM iD.Žôeµß°Ê[óÙ`éÛæwÅqKâŸú€§·æwÍÁ¨5<-Ko×ôÊlNjrt€a)Ëø1ªmBRK”o3 ê*JÊ•4Q%DìñÞ¶qÄ|{¸´PöñL1cé7Œ³³‹ú££×î4‰#ßàÕxËDⲯVp“(ÑZ•çèÅbg,I#*àÅÃ³É¸Ž†ldQiá.®H…ãpˆMæ®êÑá{Ï–~ëþåï`·îÈT"o•—ú^0xó‡}ãÌxË|×öp ¶0ÔnÏŽ±¾zpùiæ¼eðùŒy,ˆY:,å‰ Ã7Ñ/Çç;g’¿ötÏ‚¶Y àÔöì>p+OÈG÷|‡«íKj7E¥‰ËÄ2hÚÈjÜì1+1^ʼÁ4o÷hÊÜF„x^— š¸Ä=R $8—hAÏ0–Èá𥺿Qs¡ÈOzöð‚ЧâJŽÙŠ6D u7Úåÿ–|‹Ý¨ µñhÆI|Á­W©|uwá>›½}rŸnŸ»q/*‘”dLWšt¦¾îNt5ìN/>(eÍ'ECt5 j©yT€­&˜ÿ™k¼ÿ!yú³âtz¤÷ÒÞíHÊÃ<s¥ƒ)W:XàAHcá`. Äçƒm©µE¶FÂA‡¨\_ï¹95qáîäãúiVp&ÿ ô÷æÒ¬hïÍL¿7öÞì«÷æj‘÷FR•¾Æ{sxo$›ŸÛ;dw™—…éD~‹¿;ÂbÞË£8„¼=ûìúûsnšÚûó8{ì›y.&œfÞØ}å2äåQ¼²_áå C+Íz|ƒ ¥©÷GQ*z˜%Ÿ·»iü†A'”gU:³aDÁŒ×làá!ë“-äbt¦¢¤'Dúʪ§à%á¢@sjÔ uãYÔýçLÎf@tx˜!b‚Ljx'b5E€¦~DTtel¹«1q~QÔÒñjX¥òµ&Ì™ãê¸=±'Åa‰ÃÁö Tl¿¿ùËÔá!(¿.D}}Ó¿0ZÆ?7†uœKšLø1uŠŒ,Í—åæ›³Ñª4k¤³ƒõS”À´l²~½×Ù‰/Cƒt'üJ;)͸×â‰3S1æUy@Xï®^«Z”žx“z NÕrÊâ82eI¾’ÅX ÌÜš‚ ÔÒ·Í?äå)ùÚÝ@Fˆ"x`¾L ´WÎüiûZPŸ°R²t¯ÙºO"ìªZâeL#¦ÖËÔ4eÿ•iœ^Θ<›w²¼´ÉÛ†›ow-ÙǽÇÇ )HÛ¨Wd_|…bõJLØv€"›à…Û¥ ’T)[0\޶‰ÇbÅÛc.ìËÍ’,¯æÄÞÔZ÷äq~O˜¨Ô|§y¯Ó‡±ø–8lyß ä„7ÁËjÇÔ¬’ y¾:m±­Ç-Fú¨eRrã[ãp«Ú„ÑÅðr9q$îNVYÕ)Û£N£%T&Hpò=4E‘›ïЗ¥q6BÃâf¤ ŒXš3Š&¦ Úâg5=ó1•4œçŠJpÚZww<è¼ ±VöélìõØø?3ìõ8˜V9'lx9acÚv{‹Ù®J©–:z|W|ö„ ŸmøøÚÝ{,!<~å„°ýH>(’í¹Ùà@ÇX]7Œ#l‰OØõ–œã ºÚ–fÞêV…aŽ?x,Âϰn• ’BnuùKõ UxʹOÙáøÐÓ¿V=ÚI¬Ää¾_l|;âØ{^¶Ø€ú`úyw¼±Ö [™Ÿ#†aCòD ’6Ã45ÌÖúŠl "[eE¶mÍÎÍ%¾q–°fÛ¶§äÙ(ÛÞc»Þ#»ÞÑîÉ}Xß®_{<.#_Þá<ÆzlzdVjl«¸È²ÄÙ÷îޜݞeC3I7é{OŽíÇÜn|þïî"uB"ëœ<Àܤiük‰›Õ2àíƒFêlö$…–äˆo-×L^‹tꜜ7Øjdµ ÍjW6m¿iPÔšñê©iPŒir!Q§4=Ò ªP·t vµËrË*b@~¼1e ŒISMv6Vß(…«L5€ilñöŽ×è–¾Œ±]ãÚ¬4Îp®ú€J9Göèòô,FØV˜V©OÖÓ«´l4×ê} ÿ ñAëÑ¢P‡Á†·E\±cô†‘lA…àϘÅ-j÷ˆÅ-"ÎfÇ êÀ`YŠ­`ƒ2Ýô›:âÎ!³ žŠy¦ I2Éõ±ÁéYFª4ÒºÈÖ‡eZ!.ƒÇ˦ñ`l…—®ÛÖ(ž  ªÜd.Â9“šâƒ9Ÿ±{­H).kDÿÂ~oI=ê&üÀÓ )¼™:…åò—‹×L2!'ëÁÙÆ©ñ¿ ‚ºaÄ ÚœUÖ¬nd+â¤I5œÛùè—nªjÛÞþ”ð§ÓxÝävͽFæ^Cý^ŽÔ¤Žx„'-{¼Ê÷³'Å'ÔPF ‹1ÛÑ0E´®h*¬T íe¢áÅmÚœÑ]µŠ o8þÇÝî\K!Çk§+.Þk;á€-/l¨ã÷ŸE}˜ŽTä™yüLxo?ðÚˆô‚‡üÓgÏ–¾)ȹË(§L¬ç¥úÒ]ãÈð†!÷ª,ãå\Oû CÓD¸Ù‰ø+ì""˜ÇU¦ÞI„`¼Ç÷W°‰ÉCÄuÀ)§@Œ?Xöüd•ƒú±s´Vmyòj¬ñÀ‹A(4¯Ä‰ãÀY0Zªá5IâwÓ„6äÉblÍDkœËŠLñ-jp2ÚðjüÅ-%­Ì¨\99£™à6ƒãk^ùÇ>2E êÇ|þ7à+«œ¿0¾›™CÑ¥{Þν0CçbG: ï—(—’¸µÿ&ùp‹ ÍðYÂ…ƒXK?;ÄùÐæ™ß~ƒ _‡ “],ŽEbq3ã•T1¡¨¢IϽb˜xŒš¥1j’+„H % |tЄϔ1¹íaj>Ú¥/t8«„­Û|äQuäŠD7ÍÜ“n j/‘•%"òh’G¸¹¨$±2ÜTˆ² sRzÏkÌÄu¯†H±†ý<¯¹ô\ÄѾLâ|‰~÷#ß-Ôn ùʼnql‚îÆ:퓘¬ÚÀ~þp±˜?L9ƒÛ8ì³;ÛÒN¥ ôpL_—Ñqâ*Oç(_¿UP?züìùìAÀm~W’€ÖEZu«ÿÑ[í–nAÜ‚m'fLS·íâröd§Šø$öˆ-e1¹…5ÞÌä#ÌgÂz¿Gè8¹E1â«Åéc ôz1úØ šTú›Ë}f–;¥{”ä(;&eB"î)Ñ~N‘9E*!rÚÎ!5nò|ƒ´RÜ÷è I•ª#îd¼Q,WÖqa=ËM%UñÞ'mWÖb&= –„[êßé Ü dš—ó÷Âì‰ïŸ>>ÿòF8‚ëÃÓ_ËP‰ßô“'Æ}ÐOžŸD_Ñ;ÞÒ½ãBœê¡í^³õ__àì®Ù;†׺/\û¾ðt†/\gFÅÒÉ)tËÆä*Ë´õ«_Ðà?]ÄÚµ>8i/í¿­ÙyGq̽y»>ÐíZák„énïz'K´¤ EìvÀ`w­Q2E‹N:3Z©‘¡:Yk´ÝÌÁf¶U:Ñp#§8Ã@¥ÖŠo›_7AÛ|Çw-ìñë×ï {|»€Žm­W²Õ¿ Ûê¡°ÕС¡'ö›c÷AŸ¬77i¯é´óaáFü¦¨ãNúîS^ª}zƒ?öT>i߬n¬‘ÌfÜJ¦m‹¥ãÞû8åÌ•E€Ú}"®ßzjefÅùë÷ÅßÅÓë¯dóS`ŒùÖ?Иëd#󯱻áùÂ5R$ñHv¯ÎÛý(íô_D öúæäfÂIÀ0*üdt,´³ÕŸz:§¤]pÊïÁðì¿:ó½H!c”9+Ÿz"}j¸QÄ.ºð¨a*IB‘ÝSÿ®`SìUÜ)¥î^AÍóPGS*³LMùé mÏäfY‘[õÙ:håª!åä³Lo+U4Ø?-Œu”8ZU‰£Uik¬J²hĩ׀,Z•á‚yÕHÆ®WÁ&~‡*v*[âðZÖ¨TfôßfC‚lZUñ»¹ÍkâÔÈ vi ÞAá4ZúGƒóRriM¨~áÙ@ö|5†*ß#á rã`¿‹naQr´¤?ÌÒ6ôËQº¹ Âý`è GÔP‚yW«RI3sVkS2gQe|&'nÔ.Ñ Žü·íaž¸õòK 5§Ìy›»¼]Ç[sÈ`0!ˆc‡4CGÌÎ 9òŒW%x+·IgéÔèS¼Ê(ÛI\;;7¹-,¸¸*ìziÙ„]G,gÑÇò¤z5€+θŸ3V­†Jú„°”÷¢´:äEPbNÊÁæ=ƒcÐúyñCˆ—Ò¨éi!÷Œÿ,500C:Tdem{´W¥ŽÑ‡@,Ös¢ŒE^’Üëö(HÅH`–É&ÏC iâ#ØÃš„+ˆʈÌhƱä Éˆ‚iŒ-”"7žW+»±rŸ(ãpKÛ’¨#fêÇDpHÕhg°æ‚À <è”R ÊŪ'qX©’ƒC{ê‡hö<ÿp \0Ð?C–ŒÆ[½kXúäùç”»ü1~\Ï6Ð÷—š|2Ö÷ͤ1ãï$Ír¶y.Õ½‘2߃šÒg/~p¯sþ²'E´{ó)JÜ?3ïåƵüâëâIÑ0%£i\b«v]jè”) /QG°.ý®$ðĘOœ÷'TG"eI[ë( ÓÔs§Ó_ÌÕé‹r·©IàTú #Çiû,ý 0ÄŸ¦§#mðòøÏÜ¥uXÈ2ìÈmô‘Ý©ç- +–º$ésUi1PóX=+g$tÀí´èâÜýž,xÜUzdâ u¤>ÙMmÇÂÇkpµ(¾fqŒhë:³xf€ÜÞäçùç\ì<›*åX–®…˜UyÁV«ƒº¸çµuqE²SZH?:)‹VťЛ$q—(†¸Ù5ëQIÜÀæ&‰û{:pšoàÛ»˜£L 1»³)’ö˜"i>óp6Òãh'œ)ˆv×1ÅG“À÷ްâ¶õJ ÷°…äI0qð­ž ~Ÿ°ÙHCäPu¼]EµN[ßuõ‹9†YLùKú9{\Žá H¶Æ‰8}LÊÍæ,N2ç>X·¯0'TârtZnIŽ0²q$¶”LÁ r+(eÿÔ¡³¬Ãë" x ÑÚê¶òvÊ2›rI» ÜúgÃüúsÎXH¢å—°[û1ªd«î´¾™uAUÖuâNëËpɹ) ËÈT(IÜkQÒ’cB´¬=Ž'6z)*¯]ëÀæ’ÉÑt:´£À…1‰gP<êQN:œðÞYì!çgv´}æÍØÃ X@¾Ø ŠºF ü6Ì»h¢—:pTNî1YA± ÎðDNÅ&IûÆp[eqöË+‘ö6üöP¬^«T_Ïô'\ h&¾ž¥ógNyªÁ†¦Ñ¹ »ÚàÙ­Èù±"D!Ч?V!õcQ†í.- û¢^ïb_dçÆp@A ­—Ò@M:Qt+ȱÐи™ÄºFôK·)®D ãå#üŽ’Ð¼ ÈÙƒþD’þÆêMŸRQ¢×€*£j~îÇw?–p-ë{â‚‹å i):r8ñ ê_ÖÄ.Y´Z¤ò˜^`J]ưçXœwlíäôWÅúõt&»½³‹_Ô2®}&n§i7‹¥V› G¢@s#QÉ"!Í^uOf‘Âö©ÁÂÔõ&ÕýÜOyù“ÄÛ ^ØÄØ`½yD¿òa=¯¡{:$Ô¶ùò—ï%ÿ‡ä¼4k²C,Š%þ€.KÁžÍ²&›2èÖ$hƦLF¡d(âñ¸\Ù^•³¤ª-?(”¨Î1‡¬²‡“}À%’¤†ƒÒ¡N}ÒzUïs.Òõ<1ä¡ …ò+«5—'¹?šE¤)­6¹Qmš&ã¹³ÖШ·è¯h ~n2·/3UˆpŠÒ ±þöS/QTy6Ç;¡rE‰ý«áQýWVè¿=À<üfÇp_ºò¹«(‘)+‡kó;°R+˶!XS¶ §¯@mÊjâ#òK¦4oè&±c]£.¶¯ù¾dãÔ½ˆ#DÖ¼âxt±¿#ÞÛ¾­Q¼zâÚ¸YgÞNxÛVÌK Ýýê~¸G}qMHE¢àlÉJÂåZ@Ðçhñ ‚YO`gFí,ƒxeëx½Ÿçï˜ýq¢~vPú^et9§G¯KÄQõ!ñƒaχPú}Eš—a(\ZÑdNZ0º-KŠ=t‘ ,\ÊB?¹aæ¢ë.‘XC3ª`s¾Hw`a¤½•vJŠ^Öb¦k$ôj~(z¬”è$׋–áÝÔ®–ÊW²¼±]CJ_o\ÓQ¥ôÝ£…Rú©M=Á÷ê)ñ°øû^e%ÙÄ)ò96D–ùww<ë' íE,tæ¦:sóÿã3÷Ê'ñ’«3ÿŽ_Hé•“~樠¸g;oH“ò¯Ò£§îåbG&—´ò¥èêÝ‚k8{Ýk@(¤9mHdO™Æ¥œO\ÊV‰ \Ž»i_¿Òõ,wçiú̽• …Ÿp˜Öú xÅ„æÇW„{*?zO= ÚYÔMþ¼Ÿ7á>úBáN¢44éKRqˆðýÔJ5ãÉ>©^ø5~áÑ…T¼(Y{hIblU)k¯®$­5/ˆ'JMÉ5Jh‡Ñ55âYkјԙ祕hZ-‚‹L+J75˜´+#ÁãÔ (YZ+RÎuÞ­vŽW7‰ ®ñâÂŒŸç—>y¸} ýàVX'âxô/#Э9棶Ÿ‘†ð¿n&ÚŸ“‰0‚l’ûwñT”Øõü3Èÿ»@öéÓ/ž{ªÄfNÚ)i€g¥œ ¨;˜oöêÍ7¼ox„(îDQu¢Õu¯—fž½Bš©ê†4 {¬AÌj<…üëÍó‰D…ÝÜÓ=éÓzÊ¢‰æ4;ð¬Óü®Çž\z4ÁAýÙ÷þ|êÏN)Î@ŸoØöøœo"cŸËºäP‰¦]G<Û=%?«ËÒîÉW"åÝ¥?´é¿#T‘–IäÑ׌Ý\ûê³çoD}6À\1S}Vc°XT~6 ?ÛUø£€j]íRtƒB´ªÎzÒ§³¤fÕ>zÙžæXY@øtJfv¡Œ)V^Wlvò 'dfõž-2«iÉy8½ÐaˆàÍ¢ÝB;Ð-¼~#ÝBwÿØ–Jçö¬ŽaA*£ãq:鑾êT 8'˜\A²Å¹¼v+xzíq^©¨­Ím&>»“ü óº‰ò5òõÏÐO|j|`žNõ Ij,>GùÍDñÝÅû‹oOõßë/~mþ¢»rbÓ¸—o‹0ø„}‹TuÏÅ·Þ³½Æã;^ãñëSG7¶&Üê ˜ž¸åXÉŒ²ÞínŠ7¬ïèÞ<¥å¾×é?êXvá>$/+ÝBÞç±N¤|Ö¨)îXÍŸÿžß0ÿª¡'Ρ„aBkûX²AbŸз7"Yqî`êxBëóâÃ5> ¦¹¤GF˜Ã7`ï`ÕäoO|_XÌÁ -ñwÏ¥j ’ ¯þWmg·Ë1#ù‹tïQv´-¾wg¥Ïò„tmqïÚ`Ëwãï‹nâO¼Ý'`Ð×mÆk_-3[)0ùX=9“bˤ{Ð#%DaÅ=^BþÌkåIoþòó€Ð.IÓpoꟼõ øïü÷ç®3‘ í\Ûó¶jßù†ø­¤k·5…²¶?yÍY¾ß3è„¡Ú¿Ù@¾)ž·¿|K;ÂÉooé)°Q„dþÅ@)`yŠ C‘R‡Hy¨hHC¥]ü½‹»FßxÏü&Ϲ€ ß“É(­×Ø£>­eéi¤©¹û^”[¢îÆ i÷í¨¬R´ Â™Ñ. ¾Æ§õâZŠ(Hå4{ô«&ƒ[ÚGô«¶„½OU•a#!WØ¡Wd±Ã(°-Ú—£u£#,Ä9oI‡wΉbâ-ñ7ßá¿Ù£…¹®ÍØÔFló*’q’ÿd²ç~€Ô(ßµ"! úäVOŧ·ÞŸZMÜvWCï4“+‰ØaZn¾ŒfS[丫±D¡ãí÷¨3>iý€9dg˜B0Jp ñk·„@þ¡ò÷÷±ŽfW¥º·)†¸ï[ÁËäÙHSé€yØ¿¾ñļ^û×4ÁG§»ý•ãÆ™±?ý¿=ˆÄþ=áÿŽ îtOcÿ$ØoÏ&ų €÷ýƶä¥"!ˆeñµeЖ:7i`&Ä-¨ );õ7Ê r%íd(H òö2w=Ž.‰‡3|r6ø à?Å]Àw=ú÷@ü¸ÃûÙÈ?ÎÆPhNãþzØæžÀýQ!ÙÑÄ5zr¿æ«¡ýN^ í§¸SÉ1ÇûÒÿ&a~§˜ßqK.’.©(JPÔ5¨Ÿv3!~Þ-„Âû¸oäü(oúm ëf1¾Ô%ʧFíR‡›{"WÅç7©4Ì‹©4W‰p¡åµ ¯êÑjÈy’$]RЕᒥ—%ªãm>Õí4ÍcP¼ÝÀm1°¹uðDl%LõR.˜ š'x¿œÇ>ÈÒ'/>ˆ4¨%"ώ޼<Ô3ÛX‰T‘¾˜ÜpoKy$¬¢B¦g/‘5Þ´òRÇ ¢{\NæåšÝx5N¹y¥¦èz–™ïÊc·R\¡I¶°C’ZÓÔªA‚¾FøCÊREêAÂï‚M'wwŸ9®ÖÄ;2^‰'Ò¥T+Â`!…Ð/õÒDµ`à áÄérôyÁ[¶¹‹|D PïY³T%‚"a¸¿“[¥¬³ããNKÆ÷ƒ¸S"íõЦ£BªÄ”úÀnh¸Ó2$Y$¬k4Kò@Òö<ê(äé8+·òÄß)&%ª‡á§›º.š„^j?»9ý0`S„8ýígôñ¦Eãoñ¦Dw¾.Œ&¹ÄÔùE.3W¢FR¡LKâMQû/(øðópºacôóÅâp·Iìé&°§ã ÿ|<rÈÍĦ3…,Õ¯VC”ú?D“jJI“8Ò#ã¿ÏÀ40£nó(Æ~4Gzüˆˆû,·˜eìM`§]äÕð¤‹éPkfŠ!õ_Õ™Úà:WÄÎ>ë…c¡Fÿ<ñkCF§ÃÓ$TTKaB`¢^p êt>Y\§“^+vtÝ{£ã5t:ßzÎñ _Ì ï.HqåWÓTvË—}¥Í©º·iê¼J¹1¤£7hMœ þ8Çóq1K±óuÄ•/÷|•Ήw|¦Äò4ï\Îhÿ"¨ÑÙ”¼¶ß-ê¾ÚÊœÛ I,³Hç¸Å'Ú¢5E%3Aª^Cµ••ZÚ¢i9¶àµptÅeíĦ…9ý3 ˆ.{á[Ç@ïÿcz?Ð~íV÷¥,W¸ZÁÁâ<ß’?8–_‰ç»DBï_;{¼Kì¶;ÌÙù –;gÂH«Ý‹PBÚ‰÷v7Íí=Ëôæ#ž·çp{Ìl&·wGƒ8ÃêÜbIÆÝár´T!˜G˜¬ÒW²W§ÌbJHì° ]ãÂ<4† ÓìéúõhÈuD¬„‘/]〴Ov•(Èx§} týâuúˆ}@Ôï`¶¨ß%‹ú<"ê7º¨'e¥^gid´àû¯ ãwe†èø‰@zÌŒl}UýNhüUýÎMOã»=[ÒJeÄ~÷ KÊ E¿0¦²kÉK[7‰xežŽß^eý¨ÙًѤø«I’u'$Éä&^KÍ÷Ö ÚtRƒl¿Eɉ³kìï)‘)–ÂWU$ó{¶&õàSU¼{ÿéò’‡ãÉ5cÔ’¢Íù ³Žl1r®„VR-Tv¬®‹Håý¡‘(Œ*$™S—K¾BˆZHJåÓœ`Q¢â«aZå«HUó´±½’¦!UÊyrI! RÚÙLˆGÉ¡~0OÅÇŸ'mî Òú‡¾n”Së! må‰Ö5B:c™GtÆÖŠªÉuÉÀ@¸þB<šH”[©Á“Á#³Å¢.i?š,cR,êÐÄ‚ô§SjQÏ?7'ò97ù]™•f9+µ0ŸŸ³µT\3ª¬Åˆ* L«üx/û L<:FuZ¢”¦TYëÒœ?d–²ÅTàVNúd4»LãùÆDšZžà,NÚ4GÆôX[_2ôí¤¾÷&ûsb2kz_óâ?Â#^çû— zj&DÜ¡G5@e.Î\$‡ÁQ¼v¦Y‘íÿ{y¦)žÅ',›5%°¡”gÞÏ&™6BYbb˯¦iË|•©;±çµÇp„žØ’¦ÙÖ*@O¨á&¬€¤ÐºJá‰È3!•²zk“CV’nb]­ŠŠÊ]‹£P’f‡[Øö$Ýä¹Z²Iç©¡ØYù@D¤ºùK/¾&ÂÐë|dt ,éµôMa ò,Rõ@ïßw>˜†ïßSÁº `Êaè®þ‰×¯ïóß9öuÙÕ§ÁÚª¢Ü¦v½Zä¯F/Ñ¥mýyýy@ÃŽ¦ûó´±ÓäMTBp\»é#B>ûôýïާdÍïÏ›ŸšóóâN¹)¯õ8úò»Æ¿êË·å¥|ÕFüÞ«¯Ý£¦Õšîû¦{µƒŽA²‚£\µ^¯Ñ.© g4ØÍ®$7œî­cŸFéŠw¨ãE`9µfÎßí¨ßFï š^£j³ÎoÓ8¿¹í÷Õ·e_ÝIöœˆli.ÜSoI1…A—¨ªõo†Û±·ë„¾ ë±[¬–¢wÖݬ¹r=»ÃTÊœê°ûjÄ v…5ôÜq—±ûJ9©Í5?B€ÈÕ½i•zUêlZ-­ÁÞ±Ç{I|-”ñj¼Þ`ß³=üçC–¹Á.—©›%ÁjôÐWìa‹Þ±V•êX2F½á®>[ÜYÛ´Ù uJ üvûÑüvû„ê³j¾O´Û²ÏÜxtÛ=Ì1uÝ'ú´ãG3z‡Üjv_vg±ýþ=Â݉á›nº²{‡6[É·Ãû¬„Éö9ùœ>™qNM¹¤<£©úg~>¯¿d¿:£S †×ЩÏ+yÓOÁd ³¦ŸÊ½¦7Ç—n÷œíÏœCà‹%ìÆR`Ok.…±©Ë$*Ȇ„éqY$B‘¹oJ­€ÓkgÍâþuE”ŒÃ­êÉW!J×ÃÌïƒÁò£Aà$Ïž°Ú¿@óÉÎШ,±=ΧÿoÌ¥ÿ”Õá«¿µÜ2ø=s¯ÈéϧÆãÏKÈÓþr׍Š—~d¤ŒŸß pVz¼±ñÓÈ­—ýÙ¢:”ML-:è{ Þ6ƒøß6ÿäjySÜCÂøEƒ*rîÛ¡î&ÙÐ0ñ?dÄQp&iÏ~‰W3DÁ¹WãÉa|MÜÝ”Ùÿû¦’¥µDÅxh~|ºEä$÷¤°ûüsÖòþ÷³Æ1\Åÿ¾×²æ$ÍèaëÇ$l›úf2øf6Eü™»´¢ñŒ­Œßoµ‡Ê62œm¸9ú¶:܈–ßšÿí½üŽüéÛ:„¿Qú?âô2Ñô‹|!î)!îIiúFIÓ×?$'Ò{Õs"‚1"C÷™ïÇœñâ|~jFqVY㯠ѹO÷xªÜ£%ƒèôœxm~:˜4~J¯gÅ¥6Ò÷§3í¾Gõ]z =ú™Ëoù¯G‘¯ý;Éä ¤†„­UE—ìÅÊìÄCÑJðþ€Ao€¨Žé 9Î,²\ ì°)"ºÓKi¯U…¡HÜ¡4lGf¿¬´ µWÍ’³€|,ž@‡]©öµ³•¥’’«¿‚a$VoDçó:ÒYôýS_º¯fç ]¦æé…uÃÖLYÅ×8{ðZx÷ªŸGvÿ#óc‘°ýTdC^«éD{®aR[Ù]V†œQñíÿÍi´xœ}ÏJÃ@Æ¿Mk­‹ ^ÄÃDPº¥I©`{ê©bÿP„ÒsÚ,mlI`Û‚OàY|ŸÂg|ôæl²­T¥!Éþvæ›ovÀ^ >×x²,PÀ—eûâÔrçâÁr‡âÕò2âÃrGÎJŸÇ®sÃU"»Ç»ÇÄÁ°À Þ,;8yËÜ ÏrgâÙòrâÝrâÓr§†BLcw ´˜#f9ÇC9Ö†J¢Äj“Ñq–x!à5ÆV™*‚‡2\\¡Î¬9:ãÏçŒJ´Ë¤Æh MtÑOÖw‘\‰N8gþµâÈ×óiX¤¶Š#êøZ‡£)ùQ@ñb¢ôœ¼²{U'­fÊŸ«€–Q 45»}jöÚ’ÜhŒáú..qoÿhDã¡ñr/ïùÅFRbÀš3JŽ •ÈA(éþµ­ñ·­­ÝR­‡*ûV’iÓ{Z¦F›Ç"Ó“£^UV¤wûP?ð™±Äá–Ê¿}ä¶ù­—Üz+¾¬„”xœc`fƒÿ )@Š‘ %² ÿÿxœc`d``àb19&FFÆf ÉabFÓÕ¤'Øe‰¶Ø¾TQLambdaHack-0.11.0.0/GameDefinition/fonts/8x8x.fnt0000644000000000000000000000621307346545000017434 0ustar0000000000000000‹ Leon Marrick, 2005. Freeware. Share and enjoy!``,0ÿÿ † ~~†Ž–ž¦®¶¾ÆÎÖÞæîöþ&.6>FNV^fnv~†Ž–ž¦®¶¾ÆÎÖÞæîöþ&.6>FNV^fnv~†Ž–ž¦®¶¾ÆÎÖÞæîöþ&.6>FNV^fnv~†Ž–ž¦®¶¾ÆÎÖÞæîöþ&.6>FNV^fnv~†Ž–ž¦®¶¾ÆÎÖÞæîöþ    & . 6 > F N V ^ f n v ~ † Ž – ž ¦ ® ¶ ¾ Æ Î Ö Þ æ î ö þ     & . 6 > F N V ^ f n v ~ † Ž – ž ¦ ® ¶ ¾ Æ Î Ö Þ æ î ö þ     & . 6 > F N V ^ f n v ~ † Ž – ž ¦ ® ¶ ¾ Æ Î Ö Þ æ î ö þ     & . 6 > F N V ^ f n v ÿÿÿÿÿÿÿÿÛÿ·ý¿öß–ýU¯U»Uë€$H@ €@@<<8|||8 vªþ|ªT(~Õ«Õÿ«Õÿ~ÿZÛl(8(P 8lTl*"\zf(@@$$$((þ(þ((|Ð||D¤H$JD0Hh0JL6  l8Ö8l|< @@|‚‚’‚‚|0|B< <<  8D8Dšªœ@<$BB~BB|BB|BB|F:@@\bBB|<@@@<:FBB><@@\bBBB0< $@@DX`XD08ì’’’’\bBBBBB>\" >@<|< BBBF:DD((‚’’ªDD((DBBF:<| |`   2LUªUªUªUª"x x" 88V (T8T(D(|| $8X400ÿ  80@@08 (Tº|ºT( ¢ljŒ’l|º|l’ @D8F:<>F:<>F:(<>F:$<>F:<>F:~ ˜n<@@@<FNV^fnv~†Ž–ž¦®¶¾ÆÎÖÞæîöþ&.6>FNV^fnv~†Ž–ž¦®¶¾ÆÎÖÞæîöþ&.6>FNV^fnv~†Ž–ž¦®¶¾ÆÎÖÞæîöþ&.6>FNV^fnv~†Ž–ž¦®¶¾ÆÎÖÞæîöþ    & . 6 > F N V ^ f n v ~ † Ž – ž ¦ ® ¶ ¾ Æ Î Ö Þ æ î ö þ     & . 6 > F N V ^ f n v ~ † Ž – ž ¦ ® ¶ ¾ Æ Î Ö Þ æ î ö þ     & . 6 > F N V ^ f n v ~ † Ž – ž ¦ ® ¶ ¾ Æ Î Ö Þ æ î ö þ     & . 6 > F N V ^ f n v ÿÿÿÿÿÿÿÿÛÿ·ý¿öß–ýU¯U»Uë€$H@ €@@<<8|||8 vªþ|ªT(~Õ«Õÿ«Õÿ~ÿZÛl(8(P 8lTl*"\zf(@@<<$$$llþlþll|Ð||f¬Ø6jÌ8l8vÌÌv000  l8Ö8l~< 0`À€|ÆÆÖÆÆ|8<f< 0`0 <<0  0F>``|fff|>```>>fff>ff><``lvfff8<  L8``flxlf8<ÆîþÖÆlvfffff>\2000>p<|<fffn6ÆÆl8ÆÖþîÆÌx0xÌfff><~ 0~ p `0000`2ZLUªUªUªUª8dø`ød820ü0x¶@$Z<XØþØØÞ|ÀÀÀ|~`x`~~`x`~~`x`~$~`x`~<<<<<<$<<øÌFöFÌø(æöÞÎÆ<``|f|``$ff><8X8xbLambdaHack-0.11.0.0/GameDefinition/fonts/BinaryCodeProLH-Bold.ttf.woff0000644000000000000000000021224407346545000023370 0ustar0000000000000000wOFF¤\4®GDEFðgT¼Å¿7GPOSXý„-Ÿ:GSUBXKîO%eÒOS/2ëàZ`—Ùâcmapì<å23Mï1cvt t""ÿ  fpgm$üsYœ7gaspèÿÿglyf€Ú ïúÔ×Õheadæ¸66FÝ‚hheaëÀ$3yhmtxæðÐ ZÂ̼plocaÛ¬ Z^öàmaxpÛŒ ìXname˜6lSJi¢postÐ ÿ¸3prep S]õÙÉxÚ¬Y`ÚX¶}OÓ1Q „…cld ÷Þ q&ΤO/iSþNþÆ™Þ[þNï}¶÷šd{ï»þ³½÷Þ{ ù÷IBàÿõ/3!z¹”{ι÷Ýû®„(Ô€5OÝ WšGÇ]>ŽLiÿ dB–¾mªÁH FD÷m;‰(xÇèhì>ލôIÄjÿ2Ú#ë*Xí'á?vt:Y)&Š1A¯§’Sˆ ìmwØ,M&Æìûë§ßüé'bmÅS==û»ºö—o¡î>uõã#M¦ `âP‰hk& p *D HAÕï GKúrƒÿ“È ×Ù†SlðX…dt’CÊ)Îe¥XÎí&W–2¹l—(ð–|ú†´u ÅÒ”ŒMˆ#Û³Ûs]Có/ëîÑlb_ÚÝ&YÆ25”™HóøÖ„m.Ÿ»œàÉþ#U Ýh­õ8À›€Í¡¡ôÊ(u‘v*ÛÕGI7ÏŠ¢±d7àÌóz|AïþÍKWö]^äóQ¡ô옊žÅEsréhiñºåv)´órs ù¶íiÑüÕúšÁGvêž@VÀ¢¨è9C;!&eúa;%co¿º”j[<4÷ˆý)¯7Õ/z¨»Åù—/-­,¶^ãî\(æÓ.¢ÍÀà &4»ŠoZŽ*«Eš…Hë4½Œ*²0æ$N€¿wà7—ø»ßa‘º{å¦#­¿—‚_ŸœïÃÿg¿µÞ.Åo)ÿO¯œ‚O¶A숽€fÿ—±? yƒÔ½Íÿ"²BVrèõxkÿþÅöôâþ¡¡E. åZùÙ9gÄ´ûá}rZ”®ÛZ›Fã¡Ãå/6wêPÇ#GerC»Î ¬ì~@H«Yré  ¸Vå\á$€&Àÿ÷¿õþûßJÝýœºÛË¿%!»Ý*û¬øÔºurË^t #¯ØUŧ‘ø"~²aNŸS<‚?òÈ#+ø••ò¾ø^b€'W5ë­ä‡­ëx2È¿oOfMûXžˆ%^þØá/lêâ¶tK™Óø>oa§Ùä‹·OH#8v¤\êØ2@*Óp,É;nó†t¥µ\#Ìô«Õì«èZÕ^VØ!9%Ú)Ð,7ò ýÁç?òåçî.ÿ ›Ê§Ê×`îü·Â÷/!@¡CÉ ízZÍq’å—ß#P Kt,Àïáwhç†ØèR «æ†8ØÕwN"—ÂV¼º’=ó¼”ÏCLIÊç–b1!Hs\á±7ãxl…ѳÉLHišÖë!º?9÷ÜŸœºúSþÒ9‹¾7>ùä}‹ç”üŸ‚ßR+ŽMþ/P7¨¨Í€ÚV‹ÐÉK¢˜%yGÇè#ÜÐÃwlbtö{” á=ÿÑu]0áÑ{s+…Ÿ ,B&f :8aWg7PN}þ¨T ßÚ: Öá˜Ò¹"z¨¸­°wTG÷*W©§G‚—¹íœë–JG·$“[Ž––®;§íº}—]tüÙ˜¦X'èc…®9¼!…,šBØý6­G¸ÏìWi¨Ó?ÉîmíèZš>Ü[„å:‹ÝK9ß·pé)8b§ç¨qÐÆ„€Iuë%n½58Äà 8Þ¢€ÐŒ˜$XÁ;’qxa¥—WˆZjCKBN²“E±* žu«MUÛTµ~ݱ0Ða[¾TÝS(ìw/6 6¿ÑçHï»üJ¢Û•——¿ïkùW«¨zÛ‹~ÁoÐh0W:íhjƒ¬Þ²!M­š¦Ö´rŽ‚V;±©¨¨Ì¯QYÏ’–¦óØOº¶¶tÝÿâÑÙ‰kzŠÂ•ž+LìäËÿøÅ/ðž½ò %y­ÓÕ=á#`|ëd!+ã±!ŸZ‹|€#,ÛbðØHfб¼¬&9Äbítµ_©òó¼rà‰¡²[Z–;:Ú¬>‘_òK"/ô/g{¥¦##-©bÇüàtd`—9¾0(†¼¾ÑnŒCÉîhëx6Ø–ØéõfGij4ŸÌÔ šwƒæ,’*œhB^§óÒÀ’3EYRéßÿ0åû0e_Y9õ[ÒA'ÒÕßdÈO0ëèÔ ÿѲ¬ÊŠd¡ˆÊmZ^èqûá—Ýß–N§î½Ê|Ûõøå=ž¾"~¦|ðúÛH\lÀaüYPqCY£×²FŸVꕉp¡%§ÛÍK¹\ºÃ?úÄœ·26uö¡@]ZÍ]˜ƒ?¸ãÔÕÄ'>—ÀgxƒU]¯Ul³¶²¯©™œZ3=P3ê;kPå%¾ŠOPg–ݾõQƘa†¶]qƒÝÀ`L5[xJÔ)š6èE@ÿbb6•šM¼óñ¹Tj.Ž·CÑoš?EØ4›1`ã6õ³@+*2L2V]ÁÔzoÕ,^°ØÁ©`ã¤5yÒ‚5ï{Û‹ý&1s¦ÂSo{ß ãV•1{Ì#x÷á¥æf‰?R~Oùø->)”|·Üv­bä+¸)“ªÁm#›‚Û J™ÆZù ´÷ý—ž²mŒ=hzì¥oâ7?)NÄbâ“å…o›À£ Ÿˆ{×Ííbh¨Öˆ/Ä–Y»s²8 [‡¹M¸³üKü©òçqv>~d[yr>× {ˆÇÿ„Lˆ£‹+ž\ä‡]ˆ]çtéBH=]ÂGÁ›E=!C$VIüN’J„\Jm„÷ƒZ—ŒÈÕ‘W»d^®³0´·¯oï ¶Çfgšœ;E4O"»¬y4_“ìëhnAFUaç*¼@sN½ªšs°òË+ ¬šªêŸEJŠe%%ɲ$É*m£ôl>-ÎÄ/ÃÞØê{.WÜ/fÌÑá=½½{†£~ßìkN KþèaÎ ¢ŒM‘ñå5å#hqÚŸDÎ*­´¦?­%“ôï:çI=ÖÎC4lIÄS… §ɉ™îåbPG•ßhšéô†;“—ôµM_4ÉñéÝ;,D¶J®–†\¦Ñ±ÍߌiO)Éí“ å¹k±k!ÑÊN=2Jvc ¾]k°.uüãÕæ_›Yd8w|馛|¹¥Br:ÔÔ™ïH½îuøè~½%9•ÙMûœ‰”õªòMð〱 2‰CËgtÀ*F1ÖœAUp  2h#=ÁîRŽkÚž$eÈ!ŸGYÇÄýºi¡{iöþP<˜òÁæÛHíÛUþnδû|åw Ó§Ñ)/‘c'ËÍÜϼóÙ÷~æéÁiùòO|ªüõOÏUÏ—!¢hµZè‰}]·ð¦½öŽŽ¥RÌÔq;HÉ)i¥ñ]»v?Ó>`w´ðaW$"šîÆ·]}ê4ßì4° ûôF–ÿèÐ{&‚Ž µÅ·~’±HÔ*i¹DV¦ªLÚ,¬”÷-ê©ÆW¼X­ï¸W•äôÊÿ¢µ½EæX%æü™‘‡…r=hd¬™d(Õ`Í&sÝw›mV—éª&«ËˆÛLÆ[#é-Óåp‰w{<å·àRŠ\*9 ]èÂú µH×§¬ÌÄ`&†*o…™]ãâÔ¸œ¿Ð,C›7-Í)ù‹;j¯M_µJÇg½‰—ý˜x;·'ãÛ;+WIùOë•ë5¤u—Ü­'dLnˆÓµë©§Mšg‚ÔÕK‡z3CY))/aµžAk ãÒº¯ÈªMÌ‘˜ÀsÎjÃ…éà\kûT—:ub$ǰ[J»>N½:ã¢CЪÏæ_£•vMtŽvàHº£ÊQk†l]Jµ»T½á¯—ÇOÒPìH/ó²«Ç{. ŸÕ:$¥uH§ÆÖ^íÙVKÑCjP«ü„„Pµ¨7/ÂiÐJ«ûvxO$¿Sé^òMB\¬tM©×¤Áÿèqxƒ–V`ü0ç"^‹ÇaqW±üÍŠ(Þt¦=ñˆÇ”8õ_œœx7–Ç1CФEJ¬›H±H¤Dš¤$ªZ¶š‹l¹ÉŽã–Þë6w)’-ïzý¹¦I»×û]î¬í-íÊöžžì®®ÅÉçíë;¾KxAs¼ñw©ã13ÀxþoÈæt•ÖØªÔa(sÎÌt¢ÌFý BÞû!ÚbÚâ¬Ô2Bú¼&}Q¢!}Y¢_Gú*Á\`ûŽé§5òò¯xÏOÑ ?“èþ«D¿ôߥ·nÀ·/¼•¨¹÷é=›i½H5ÙKЧ¹WzMÂ!¤`T€èñO&쵘[ì„^ÖDœ¸›Q®¡ÝŪüagUî Óa?ÿ†Û£Æ\͸®¢fñ¿ª|äìž=Ÿ^|cO‹rë.¥:µkñCt§}šîµ?ö-!/|Ékyo²£@“Hî DG–põxÄ-ä–kÚsöò¾o!¿ç¢WõÂO«'!(!ó"b1"úß¡næn#óiòû4(6‰ ± ¸=ÿ‡ñÑÑ3[—×µw¬Šîlß4ÒÛÕÙ7r÷Ý]tNÓý÷Ãåq[}osb´R_96œMyî¥óœ–ÞÑ×ãuµ´¶×ä†Õ4´tÔó YòÞeÊ;rv¦F¡\Öø\J5*ºI¤“í:ˆtZ¢ãHç$z+Ò“@AciËD4gf¶ m0—c-Ÿòá">CùeàväçßJé_@ ä]v Þΰ:G<´FûŠç8šõÉ'%u8žÔ6T@γ=j¨ªl^ã:ñû²mÝ¡Á;WŸþãøÊ›{ŒÞ†€ÍVÛT}æöH4aÛíÊlí‹þéßm}hU½d6õu¬K{u¹W½€µ |,t :)ÑA¤ÓG:'Ñ1¤§%šDzT¢—è­HOº™°›Ž -ô[‰’´w©Ñýç#BG„ráG3?²œ,á“FÜ%f³¿‚%¼Â˜–ô;Ñ¢áŸSCVO rñŸ”ùÅ7/^TêË!Õo7;Ùˆ@‡wcØgRÅæ£ FNsXéh»«†vd}ŸÚ/õ,õYMA:¤{éó@GXZ‰¾ˆi—ëCú²ôܯ#}(ä ë9Fë¹4‡¿Ã{ÎÃ= .‚c:,ÐA¤ÓÝ€ô¸DoEzRP;S-!—Þ6ŽtNzBéQ¤rÚ#AB 7êÒ$[Pó…Y³ c;ÂI1´£4½§ ÚQu>nlQvsc{2?wT4ÄPòbƒ[cެzJôQýyñT•CQœõmáÚ¸Õg²˜Ü5Nu¸D1•Sé ƒ?UWe*»O§÷º=af ÐRAm,A_6OÇžæ4gfûË@eÀŸ³ñÏåné=EüM¿@TÖã«[@ãâ&hï{-x<ŬGT°´ J#¾ëióór¶"ffú¯CÌž¬l¡jÀšÌ/m;éÒ;(½`Éÿí¦CŸ®Š¾úú@ùõyåá²Ý»ví]¬aú/åÍ›Yy@í¹ç¥tR¢ƒH§%:ŽôˆDÇž–héQ‰ÞŠô¤æsçhÖrЬ…±=£§¬Ù/ú±_\‚>R%îgÚ`ݘyL¥ýaTR´E òNY&5huûÌÿÄen³‰(í «Œè = ´+Ì+ß)ö„¢<{°J€š‘òwFáMŒë’‚ÇôGÇ_á®á¢»f(¿L”ÜÛ„0Ni“öU´~ž×¡fãl}ˆÎçªÂÔ•mJø‘/¦ÞT"‹£Ol[®¬Y|ûÖm“³‡žË—­¯(·ŸY|ŽrPÙ%*±õçé­HOrš33EP¹þг°òg” zP¢ãHç4éNsv6ž"'…´·"=)Òš1-æ ø0rno-´q;ö¥tR¢ƒH§%:ŽôˆDÇž–héQ‰îGú¸D7 =^ò6œMS…_Œ¤AÅ&w¼Wî&BÂ>»K|>‡ÂA¯ZèU¡_hÕÉ»µrÇ “÷ERƒ§ÛÒ³­;×Þ¸¡!L» —‘zM!u£]†«.è-7voß3°abKKËÖ‰õGÿ$jo÷Š®CÙ|׃Lòöà]¼ÌfV:(3Ö)Úbpì]}éóšôE‰>†ôe‰~é«‚š±ö¯˜M åf]?©%Mdôš4ØÐ/pß5ž†‡¦¡ tÅ>”»ƒY:že:X·¹š ³¿-PS^vŸÙñ‘x“–Ó·$^猚›ímÁš¦·K3yùÅüösØúKéA‰îAzV¢ãHüFXÙhi=þí}ž™ˆ8s¯¨Cê—‰—D¨M?IŠ—„´UbR!VcUŠpUF)g”UÀ••4Š+½£®‚âW.¸á¾pê&A*p7$nŒgóž;ë•¡£3¹,« 'ÔJÜH«d¼¿cðÇÇja˲ö Cpùwª:ïoÈô>éeW–Öµm7'îfUÓ2ÚvS㽃¬F”-®¨íCw/~ãÞ»ë[àåM ¾š¯ZR§XSq‘0Ùrmm7 -Ïñ9)š3Éú 'õÂùVXX1©ÏعT•Úî›W5$ÖÜÖ£Dþtÿþ'¿n³yj¬Öš††‡ÆÇ¬œ¡Ó¤½ÊSYhOÏÆT•˜Ë¬‚²8yY‚téœDoEz’Óœ©×λ85#å­Ì­ ¸. Æc¥ø×ᢻf(¿L9hV!—.žK'A:ˆtZ¢ãHç$úu¤o jgZX¤Ÿ+Ó¢óúÂnEzR<ÁŒiyIô´'âêÑ! pígUï×j ¸`!~Ñ,è)ú Q"¸û$©¥¿Z¤}Á oîqP½Á6½˜øÕí'Óë=tÇÒìXnÞZÞT³ó‘šžëûv~̵ie_urÄ \‰B¶­!ÿ‘Å›•ϧǗEnÛ´/výŠØ@ ¼=ØÏËV‰QhÎδ·H?§Iç5#žø0rô $]¯l¾BK¢­Œ´0• ·S?=vkÒ:6¿Á™¥é9˜½dÚÁÑÓ—]»ãL|ô裊ØðÄ¡COŒ.¾©øYÎA -°ûÝR:)ÑA¤ÓGzD¢cHOK4‰ô¨D7 =®™‡G‘Öêö#}¼$8³¡4Jç8ìVËέ¶×.x-Ø6-0cä~ŒWšËÄ®AåõæªX«ªwÜùHsk²õÔT°¾Ê~ŠX݉×VLG÷Ébé¯õTú¼³pQ°Lç°L?”Jú_H¿%ÑóH¬™ö»òü(¶UL½SŸe6ú¨n€„Hÿµ«Åq¿Ù½@ÿO¯ %Êñe:ô7 FYF>RéwYÀÃXÑS¬(W Ö·+B}‹LpÙ óâ_M}BäûQh'¯c[-¥%ºéY‰Ž#=‚v¢%*ÛÉJùj°Ÿ[p8ìSÃ=øÆR:)ÑA¤ÓCz¨ G2>·”NJté´DÇÊϽéINq|óJ©÷GË :-ÇUØ7?Žjy:ŽtN¢·"=)ÑA¤ÓœæÌLÝ ”ç­œæñá">Cùe¢æ×Öi.ÃäúkØy’¶ò21N…’\ *H’Åêºcö˜(H5µbH¡÷ùÒ$mÙð¡A)’ôÞ{•ÝþpÇ“Ž-uý) ]¢ýQ; çÒ¥Á€ÛÐw±\‘JÚw=ÅÆâ¦W\ )Å-µ‹xšÕìÈ.íØDãièÒÅ-Uìõ›;|/Þ™i¤/¯`©:G-#ëJæ|Z¶‹‡°]$™7tà(EÐ^|Α dì(‚ckDA’ìÜ9T¯Sî0ôg£®¡ÈÎ~²TÚÝU 8õ¯é5õ6«sû®ó»n 5¶\7±aÃäu­Y#݉¦®h%µÁ Z‚fu×µ®%¢€ÐGå‚\W’RÉCqÓ'¸dåT8j 3Uù”»\K+CF«Ùê¶š–6ù²®GOGš›F³µÛ,KLSº2{ËÆþwÇêi°Ú²h¼nýC;æÿŽZMn ¬ÉéHÉ^CËSÌÊDc—.jåJ ¤*Õ5î4Ù}Tmò=êH¬dûÌ_.7ܧ/‹%ÔÉ÷?Þ¸º#Dß#D]Ñ•vZÞõ”ôQtM¼¦õhA‡ØEt‚Öô\n[¥™lKœ@bGîr†C¡††Ž#wA„¦Âþ˜H„B‹¯`ý@’Zç:è_ØþY\?™» ¾ûIžÎ/£z.-:Q êÏ‘NIô<ÒÃ]@zJ¢çÎHôÒcRŒHq¢ô~ˆ&é$C…Y–‡™×CtZ5òåˆ2ºH‚Zþ)ˆ;­ ¤?ò$ôˆAh­×?r5ñƵ…–lðxf{Ö%^¯ƒþ{ Ñ&Ê }M¾¦Êƒk¢æöpS_Ö_]å÷WUû v| -6K-¦RP[©1EúÈG®Ø…1jê;šd}ì ˜à[‹Y¹;I ô¹ÜÛ«§iê)­ 4»6E+WÌvQm»-c¡y•gSC6_m¥…Zm©–=»ÒaW¥ÇT[Õÿý¼QϧҞx­ÇÔˆkÙÒžñy«L†v§²º`Sn;3‹RÛáXŽ­Ð‹6=ƒms‹®és@ã,­D_À´Ëti¤/I­{é+‚š¥/,F¿°Ò¼|ïy Ÿ$ÓŸJt?Ò‰#ý7é­ð­Ç o%:b£÷lkYNû -1ôomÄ&Æ·¶d^j³åU—NüÕ½]ñA+­pOD¨hš&B/!ÍJºé/Aš®OÄ{Á—(·*7-Æ(ëÕYÛŠÒ.Þ`ȦiÊövhtNr‰=Ö»úè#nÒ×–Xßëvû–×Å{â.½úsS[]}<XJ¯¥kF¦Š ³¢³ÁÿNuƒñ ê³¬ï»ÞÞZë°Ýè ²þ8¶âºu»3iÿ nOC¤1üá g»ìÆè¾å«µ~Éa¥öY•fQv´ ¾+åƒl÷¾Ý Øí³ ö©‘N¨ús¤S} é¬D/!=‘`«¡Jè:@GÔH>þÿ‹<‹S£êš¤4t´«uiD¡¹~oš[3 -[¤±×eŒÆŸ%†R¾PÛ`]4íÑŽ?`Öû–•¹BN' ¥N'”¨áˆêÁ€´€Ò|õûÜîïÚmƒ+× )_U7ü ³jƒº¨ý¥‰öQŒñÊ/·#‡:ú%Ñ!› m7‘ƒWĆj·mó°m—/à>5´f¯è/üôÿ~jÈ4d¡E׈•d\ï/ â‹¶óíl£Ì'¬¨,eA ½k=½ñÿð‘|<_<‘èM["+vötïZ x×%Ö®\9Röþ+Óçûb¸`¨ŸÚ"‹-3Ä}. Ñ Hi{E:%Ñ·ÎJté)‰žC:#Ñ HYx¬ab —’²#_Nf~'1jíp£¯À¼'§Ðûø“|¿»u)\Ålö†þ‚’ðC±B$Å!þIf ²8Ê®ŽK-µÕ>W¨ºâ©'í¬Q­ðD_¬6`s»­v§úSD{\B{œV23{å­Ü­œ×•SŸ‘lzéa¤ëtHŸZËÒJôLÛ¡K#}Ij#H_Á¼aË`¾‚¥4‡÷ã=¯³{r?gÚoØ{‹@y¦s·“^Ê ü8òÓŒCkf‘‘Èá+>Œ\þÊ#9{ÿlîB ½§ˆ£éˆÂ´M”?M%kÚ_±h?‹>#%z¡£°-àL¼ oaÍ«S´B0éˆôå//ÑËa˜|jºb1_WÌÌöÝ|,æßé’‹y EPÕ$Ñ ¤6õçH§ÒÙÒÃH=êÒSyªüF=‡tFzî[Hg9Í”³ö«xûÅ'<©óbÚ3"­YŠ(±´P·-P·ìùªzï9q,Prå"­™äjãUO×PÀþaâTª¿­lW&>²hÑ«Â6ß’l³–¶žÕÐJêÎ{85#E…ðá">Cùå| ¬rQãÌ%㵞¹„0ĦþZ•-„¿ê¾ògOÿO¦×(¿©|ã‹ __u0¿OÒ=Ióã«zz‘våj–sDì5±hYL=?È©)/÷ ”¸îà°{v¡·è×ᢻf(¿,EäJâ÷­WÝ—4ájTöÚítÇöK˜Ä¥82wKú48÷@ú®ˆÏÝ’Ú1úÌG5´s÷ŸJQº4¯ ÍÓIž×óæDy”1cŠóï©J—§êÿ =ÁiÎÌ"}òg¨¼‡‚-Iy†•hЪ¾…tV¢ç‘æ4ggQ}Hç…´—ži͘–ç͈½§Ìg(§k°¹èSfTæûÔ“>8nÖ`}ÔÔˆ_ÆÖk¦ôšãy•8“£ÒZÓktÉ"°2½føoЮ^<¿Ë°¤¼1Ó‘èêݪ\\^ÛrÏ®Å(áT‹Ï³ø%2–Ü©K'Z³[F7M0 @ô/Ø»‘Û›hщUŽtJ¢ç‘–èÒS=‡tFz[ÒǤ´o!%Ü @dpi+Qï5÷Á"Eê¡ÞiÄSòªêJõêòåK,Îêò–}ÑÄÈŽtjçHÂÛÚÕ…hÐ1äZÞ&✩—C<î%e ;:óñÎéµ½ÍvG»ÇS½¤Œ-u¬d±ÏƒcÂo§%„rã,­äE{œA+­Ð5"}¨“¥•è ˜6¡K#}I²þÒW5c]ƒïRšõÞsLzÒýH_ç5sf¦¢j.Qû?Pí‡õ/MÇYBøêj8.ìê1èé@½Ñto…} -Ú¤Œ^UÞÒL–:²Á¾uŽðýE‘鬌¸.°¿ŒRz@¢^¤g T=ôðo„½™•ŵÊߤ–® >ÚŽä“Z'×h({*…ŠG‡Ê*{(«q¤ âÊIïh€« iS©¡ìi¿Â÷r5iWö¸Y€°§~$ØÒÕZböíkíJØå¬áSžp&s{&cø´Ûâtu´fÃ×uÀ12Ùdgh\*ú÷«Î/n\ò}VÂÀ`éɼýi)-bÌL{…r_ÞJÕÌ(ÕĨ©%¸zbàWÐ;°Xîjb Ô%ók[|ç<)´×íôÿuô¾N®'•ZNòKW·: Ë[bI뇾@À ªbéš@z(ïu¹£±n¶¨õ†©ŽÎ®L`e°nÔåZöLu½a¯ê»‡6ÃuëhSÜ›o±‰T*!–´ÚR•tM+ž\SæZÛÛ[s9w=y3·WŠPº9ggÑøHççH>­úÒÙT^BzèÍ93>Ç ÍAÏ\='xL?“’]š.ºk†òË”C$>ä²gÖ‚ª?G:%ѷΨnÒ7_égÍZtÞXxî%¤'Ä̘=¢À:mú]ÒUØß¨c«ŽXÏ~\ž ËTÛ^ô‚l­”z­ô~cé9zÖ¦x#CÑkZ )Óƒrèd¢?’r'X,i¤1âvV;k¼±Gj:·v-]ÕêvºÀ«›WXœŽ[­n—³Æã‹´†ƒÍ M_´7³èS~׺¡'Hö…êRñ°Óîñøë3u‘Þf‘Î r‘5ÚQɲÃzmçiž tå™@Wœ´¨ærü¬ðÓ[…Ÿ~šàvvp¨ïQ-:?Ä©)¯Ù!ÚF.â3”_ÆwÞ…üTîßÈIéÜ…0¹«Xý„vAQôÎVú‹ŸGhõ“´P¡‡1Üç¡^¢+ÊR£ái ò‰ b½¦=}ˆŸÉðì³ùSÒì †ŸŒ¶ÁÁ ÛÖ–‹£Øa ðUÀù ð &ùö¾(PõçH§$zéa‰. =%ÑsHg$zé1)F¤ uê*>&=á-¤³ïó³'´ß¹H ŽäÃc´÷×"šûkåÄŽªWùÄ./žfê.ïe^¹«V|:EQ¸Èó›há x`Ew*¿sFH7Û0[k):¿ÂjÏxÝnØ*Cua¹ YîÃH¿)Ñ»þH3íw5³S;8…¯Ä _ OíÅÔg˜ÞOW‹¢½‹}ÄžÆIçJ¨*‰ˆ2n÷ÿ@¬Õ>Gÿ-=ó£XƒU|HØòÙL¦û*lÙDéY ŠÑ Ð4iϳCŽÅ,=D”ñ~ð5žÀ¶_JHÔ‹ôLªç‘þµ°)³PÙ¦ »[/œHZ¬$å)ô.KéDª?G:%Ѥ§€ÂYð„ ®I‹N¨ús¤S]@zJ¢—žÔ©®‚ÒeDéž|ïe|ŸSŸ‘žqéa #¸:ÐÆóVƒT} é¬D/!=!ÑŸ#âFù‹@yÞ̨t–ù åÿGÛ[¸q]ûÃwF̬‘F¬ãJZI«]-3“ywm‡É”¬S'±ƒŽrRØ23'å6¯q!eüþ.¿×ç2³S,÷^]Ý•VZGyF+ÍŒË~çwþ…Ø4!öW@€÷g·h›ºmñ Iì…×4Äð4`kŠs„°Š§tU¿¦†Mg˜ÄŒÜ,ÎíŽúæaN›^9Ë„&üDŽÌú‡s} ßå-ô›¼ µ¿oož­‡+9é™);¸½†(7:ÒU~ç­åë§b'f¦¹ËÞ]v'ÝúôJÙ@Üû‚¹÷$Í™„ŒÏˆ­ ¼%‡ŸÈŠ-s%æ(¹c€Pq5Òf"m“qåÓqAâL×X(5•s˳‘`'’Àì‡>Ñ‘JšSO‹nrÖá®qry8DÊeCÃÃU^.¦æû~p}#ÒŠ6\š™Ç¡WZ¬êYO¨¬ˆËS8VƒªGÉçê|?H»»ÕÕñJƒÒnÅþï«›12øŒ¥¶†cŒ¥ÊèV£xûqªL¨=Г‹ýÂ&ß’"»‚ø‚u«ñÛ4pë!5í»3Ö|ºŒ-ù½_4énÒ*sæºõJ*¹ÔD8 AèAØïÐÁ1M‚ƒmøz³’*Õ¬;Ðô4 ƒÁ-¥uÀî‚b5À°¶ÂÅLug7&C!øzõ­Qåâ6 Ñ‘<¹Ë\¬èÙR\âèu Ò!‰C1æ*¦èDÁË^ª=¢Ë¿FñföfÐË«ÚàSDì¸fجz¬;h`Ñõ’ˆÍ"(€d•q EŽò.B“²¾ÝÍ·¼cõ§J½E%׫}¶1i^ˆ…ø9¼ý“êm×ÄLë€|ÀbÉ!¹šÉS¼Ú7¶>ƒRÁ¾]”Ƹ>;pᶦÛeªàI‡§Z ìW5Åmªï<¼¶•ª% 9¾f7Î4Œdå'^{íýð…†ò‹[Çï_&>qõà«Æ ì'üÇ·¶í¡’œ‡¯4ò¬(k¬V*`&ŽÜ ñìê¸G©ç±‘cp“ñ^Ô'"XL&BÐiÁÞ-8«Ç1®Ê Vê#·1Î. ¬òÄú“XÅ[®r… ÁQ9̤½„]Ñn¾=ìØD3#­nÔ‹$ß‹¶¹¡¸ 2× FÁ<,Ì¢Ì Ñ(0p£Ý_ =wc©wÍ;ð£œ¶ãªØèŸHwÛ®µ|>ê‹™Á¼oöýÝ{â–PÑt'S£½Þ®˜Í’®úÈw±‡¡–YðÈ6 ìT Ñ5Óœ5ŸãÕ}ÉŒ[jC;Ö²<­v!:qct·JR]´‰c—6×züë‰õFL¬Ø6$¤g 't:ë²A:m“M®‘óÚ@å÷¶¨Éœ°»²ï³ÅŠî~›·É´ô‹Ep&3V¼˜œ©A8Ä~ ÷ÎÃ3¥¹3Z¯h-Ýš-tc¸ü|¥Ñ´ 0$Ú9~Ú9ZÚ9JÒ9Mx®F“à®çÒÓ—Ð3wó"ÖJÛ=^¢ºlw(ÅüËá® 3õtã_wœ—H±Œ¼ô„yœö fxòÁ5$ ˜ÝR‘EÖ‚2¶Z}®;R®âq¢ÜÎŒ¹¨Etå£ ‡úÈãÜ£þžªÔ½1ÖáƒøžUå)Vìë[)eöFâS×2g×Ò©Êǃ![PͰ^[µ¬ïà°PU=>…pg§n=OI™?ñ0b¶gFSÃ0rÝÄÓ3ÉA‡ÑÝŒ++RT^ nø¹Xh¸v$M³Íäðn_®¢o䆇äaØÔ2´ ¥ì·í5®x?åŠoÆo5·ZL¤ÕŠtµ5jª/ºè [+´LG)+#tï)»Å,³¬+APVGü†léàx$6¾ZLv+¿¯R« ¬ƒaÃ# _Õ‹ YÇÒ\ŸŽ…Ò…Ò'@Üû|8cÊDèÁv¯艞$¥8áfÄ1¬HŸ¢&ñÓ}ÚIžV`¸P\ä¬õnÐp£É ¯¾‘Ìð¢Š&îr$¹Lh f˜;¦‡3§ÐJÝZ¿ÙàsÛä–uf;¼Qæ•êˆ:зÜq§Ÿ7i×x[w!ÀwEdâ1…^n ¸Œ:¡ku(x¤SåQâî$ã›»›<­F× dt=éZ–ƒ¾{(Û¨Žv¨aŒóͼI¹œ¥±fCo~ßP¨·Nöæ‹7%òÞ‰P)sÝðRWgßh|ÒѾYm w‡syXÛã æýz·sÍlâ¹Ä5Þáæ­kA7ŽÛ`¦d8‹@cº¨•>·4Xµ—Ùfˆ]~¾”Ôr•´4O¥ª;hÞ•–ú*œsÕ K!>|U9Õ¬ê'Ÿ„võ™3u–õÛǤ/Zÿý̱q͘Cô >{¿ îmÚ÷[IÅœûTJÂh'«J…>ãO.`'¹Ù(*ÑÛ ‚Ÿrø“‹àC«çH‘Œ=#šu…ÆÈŽYÚÈ)®Ž]ŸÈ“ž®,+Ùý舄ÀÝp¨ËÈØP‡Í«áåwæj[êýPuÊ~Þá¼ÊlH ýœ30¸·PZ„ºƒàîN;%’Ãâ PÉ]u=3jå?dÔ õ÷ UgkŸ4>ØžûÛè­†þ!‘šú>rÒ>²ÓØ]µ¿ü´¿‚É¢ý½ráó‡v¡Lc´±›aÍzŠ%±Dl›À ìBÏ\|®Ï¢n´A{(Æ™NCá‹6çµfCº¾gúsïuG™qÜ_‡§oˆdA›a„Àpk÷´ZrZ6OŸ¦uÊÉR"Êõ†©Þ,pZ„æ@"={mÞ_L… CZï6;2an¢p®þÈ{³Ú3pp¨ÿêaAi´i÷(ô*©1X Îè—¯þw çrst;ax>vsIUáSÔNM¼?…ÐiD–çñ? „—¶‡ÆmÈœÝ"š>®’}1éÉÝ §ûÐ+–TF•XeR-=þ>Í Ø=e¶šÍ£™ûîÂÉ/„mÌë~æí„NïÏ*7Ú"¿ihaÕi3!ßŠÑæÄíƒ#R‚#"€4X” ­=:zr®6KI^§;Ȩ3² #qeš‡³™†C—h}š\¸ÚÌľ#…´9jqb†>wt)\Šºû 1“Ã1§ Gö¹ü~|©×ö&WbœQ{‹FŒÅ‚Í-Z#[Iî];ðð]<ü×¥eT÷óÀ¯n3’bÜÚ°a‚˜|’ÍÀ)õ¬*pz&®iÐwcî–»–»j0â§_}ÀEƒj<ÏL_Úhµ™¯Qp³/¯ÙPm*–¢!J¶¥:8´0‰…:ƒ ›eHš|M èzw@\Ü€†np¬aHôO]6ÉÛ+_"þlÔšp{ãÞWךºyaîup@„8¸tìÔ×ÓÒ½ÝÜ^êë¾ÉÓ;à’òتƒ„[^27¶Ü¥ŽoIµ©-ºáÏØNПz¬…_5„v c±Ý›µyð]s±E½[Ùÿ¨ÞíüÆó©W¼bQ®¯Wþuþ>ÙÉêðï{ê~_W†ã¢Wç]‚‰V(Æ›”Ê“’RyúIDÄÌ“’âƒôF¡í­edîŸ{ßÿûì+ïÛxš¹ï«ßcœ_¿ûÒ‚…®ÇZÏÚTÕváéˆT%DµX‡iFË;ÎU½Rp³úR´'dìIlåüÀõ gnÿž—ª-:—ƒ1\.Ñeºëxå-3;ëÏGÌÏÈä&½Ê©5¡ñ Hm`~þI’Ú²_±FRc‰$/‰…{æè¶püÒE%A¿…ZËnøë°Þ:ÂHSª·»Ñ}Š,dªùi€­V+]õý,äFüQÕÄ* ˜Àg‘‘&·N\+ Ù{C‰®|Üb5d^û=8Ï|e|0n öïøbڿ¹‹¹d‡D6þÕÊiöÓßšû‚ÄŸ wv»aûîD8_!ÞFd÷,­y|çJåô:ü6gdž¹ˆ*IµíØÄiQÁ«–"³Å@C ]ëa%zŸ˜ná ö`€_ó±cÆïlþ ¥¤*zZLŠY­<…ŠI1ó•§’è àVNàV àÁf»» ÝØ<hòï<&|#KRM*ÀWGie!')­¯‡À¨‹67õ|qûÖ’wæ}°¹É(nn¬³§®¹gŸ~ÕUžÄ6­­žÁÂåQ\, žhOý¡-Û[uúhk4Úé<=¨ô4k„Ï\ô™‹¶ÛG«ŸÃç뉼š*…5C«ä=|@ËëÔfM]óÑ<È$PÇ`Çè„îgd’×Ù×PDŒ¹Øç©{M` íÌå‹$:í×o;èšß¾_ôZ¦L#ª =°TÝ1t‚«)Á GÛÛ*¶°Ï‰¨µ™PPz¾sÇ̆WpÅx}xàt“ú‚Ì?ТcÅ( QÿMx4ï®EÚã¶ÜÜÚÿOÎ-ÛRÕ¥TÏÒn)d!ÑD]º‰°¥eœd3²¡Ž°·a7)¿“Ng*ß½ùfÌÒHðÜ<ˆ€ÛÛ”stÑÚܵšHð…Óµµ€#‘ÙÚYÇÑ­¡ÑûäkvïS"GL.R4PðÍ?ÄúB¡¾…˜#áÿó›î»ï=ÿæIkM¹EåËi974l¯B>'‰ìøì5{ŽË$ërFÏJ†f£]>8©ýò8ÛÏžCLy Nlë› ‘Ÿf:½Æu‚—…Ž"HíT#©Ç‘ÚIŒã š×!$Qm>C¡ ÊWs`PFfõ¤—Y^— …RLý=›ë„î¹Ø©Ú‡™3~ñä|¯üD\E2¿‚kÄØœ½Ó¢¢IýÉ,ÆÆòoøô54ž¾Ôµb`~Õ\ÿ­¢ß¬Ç°5iØOSi®ÈÛ@ýBzZ JO9ÃÔÇW_‰ì›bl`¹ÔJå)9ãÿ]>T.cY`u¬Çìo®F×^À#Á%5äDyiÚóõÝÛPÔûvª/¨»öt˜cV·ÑcÖ‡ƒ>õçRqJ)õŒ3Ý/.ä*Ø•=æftTá“Jn‘«Q¥¿*ÆxÁÞÆèû6jªšò] cE]§†˜(&Œ6¡¾âdSÉ¿:Ç÷dÏbκudŠÎ«Ã7ÿ Â<3ßÞœ ?GXè[”†º:…n²Á¶3õu F<õYŽ{¯+,¹ÜF‡7rÙ¨ƒaò«Ãa•â$ è3ú@9ñsøsøÜ(À‘×ÁÜÙÖÉ¡¡±²ßA¾k² Z×9GkÜpp:Z X­„©çâS]0 }æ‡ÇwEÂó0aÐêOuO˜>ùÖ·2’€§ ¥óãSJײf-üFÙMÀùê:a)W®–H©%ÑGèÈê%10Ò¼Ä2Xís\¬Q¼Š~SÀÊ!QG¢õ€Áv†QÝHo£G¤I*-9 lX<­ €” ¤©“0OélòmÕy„Íyùƒ=“Ï_ò±\*Ù]*£’×EŸ¹y®ò£Û+Ý„µ8´3ì×(Ú£™è¤Ù›CàuMx¿†6a}öo²Θ9h9Í©¹RªÌÍ~cÜoLL~Ѥ½^¡Š%¡fºËgTݤPÆ’5º¨“* ÆÆG5‡(TEU¢ œ*4â%_‹@J£¨ø¼nwc£AàW½ªNäŽJ¿C…†—Óø {ndÃpfÙ©FG$ni[ÖÎ%)±,ä]y¡¾–¦ ­DD]"0Ig-‚ù½æRšÌs­ki•vk}Jþ[µ> 4»ÄкÖgÁÕaå}‹Õ­t?°òŽÚ6 ãÄÿ5¸=p×/8ÚÚ.§B×£{4ð¹‚z&ĸ‰F‡ãhÂ%c†ï6ø.&f­‚è4Î4Æóó(%F†V©ÐXý°ãœaË+]ÜÂäÒÒÆ+7víºC)òNÝÊ»®î)Dî¾emnTúzøÏtå_GCq(æ ËÀÖ„Áb«°™ß‰ê¸&5Õ4•d»ÈDöœ§ñ\-Áj%¡‰†×‰z2‰òê€/åÚýc—OmóYŸëÇ»])ßÀª:©nÊ{\¢õÁéŸò›&ò¥éàu‘kO92S’¼ d¸Á(hÇm!œbb:)ˆAEÎ:Ê~KýCõP&vè¾l²#Ë»UJƒKSïÀAõ'Må_ºWËEÑa‰È–›+Œ¯/m©ÂškB$¶òo0ˆ‚²k3¦Í𠂉¹gK ÖŠ¾V„•¹ØTƒµ•¦E]mø·8|ža¯þ])|À‘š¬äf–´’è üÔ$U½|›%VÔFÖŠ~kVæ"-ÃÊê9¸³:Àt{•Üiµ&!1h%ãM«É€k®ö‰6ÃB²º–¸¾iŒæGSÖ€Áa6DSjc]mSV‘šÈØQiÓˆ+YqÕJ›¢ø!_(¾€Z±´.,ö¨7Dä,¿úÂë Z§^¬w:_ûùW1ûnúC¡~á–Ê» 6ƒs7î‚¿h«­×,µ4¶« NƒøòóÍKº±4²–Z¥n„$r#ÀšhÌ/˜V.1r§ÕA Ü3îLŸOCʰ'¾JûvG8ƒ`´L« i‚ä:_Ãÿ¨xé*6WNNìÞw]&¡f)I ÂQÈ›°"«µÙˆaOÉÜ0¥Pÿé¯ç™‡þÌÅÌαD¦Û’J¹¥êÝ.§SçÕߪíL£&ÌW¾Ã2ìèL±àÎG8[éœÝÀ{[3eÖ Û ;æœÕ´Ú³ØhÄü8¢i£6OvÌzç´–…ÞiÔ"Ymâ#Ã]ió°kï΄»võxàéõPú›¡]®às5ù;‘¸u«*…n¤êT@7 ÛGkhŽpÝ烗)Üd6Ûѽ JÆ÷2Ô€OÑ#\Üõ‹\ ©ê6Ø]%ô‚cìBæ}=9õæÕyõÃBºù xêã:›Ädçý^Æ•v” _,à*eêÈ]{Õâ`§¯º3¸¸ž•®3à–Þ[zøÃöEßèØøX弓ßïΖÒ)‘tñ™ÊéDWpø`¹çàHãV6¾9ÿ¬ê–ùŽÅ² žRsP«@ÜðBk˜Sgš)¸sh\œÆÁ1ŽÃÒç·T6ß4&kyÕa’Õ fzôª^×È”é®Wù¢‘IW†La{0´;z¯éÊò¡TˆìW;;§’ŸG*‘ÚÎÏëÕ‚†³¹£ÅP°'j=ìäͧ+Øá¶3Švp¦=wï®MO50Z7Ï<膧õá´>ïAæ>ݬt÷ õù~ǵP£åÓ§è6&Ô÷O¾Ù`nsGµ<´Vìæ§œœ‹ã̹@²?b™6Þõ”)6–ãýZ¢=œ1:H³0îùJ,¨¼EªvfÇã_C=e å½äé¥K(Ciß°:|²¥ÁóS6R4!¶wiâDb"3|+31vÅÆl“£s¹•Ѩ=5aJîT,jëÈF]k៟ÏG»ò®JÊ0Š¸Û© í0{âʼneH‰h‡Q!gø—È`Ê]ÍÍ NÚÆb]ƒ†èp–a]ù®Á(üm¾^sE³¶h,å®|…ôƒ“ý*ð‚ÉFŸYK0g:ÒrâõŒåbÂapäŠT¾…Gi‹^BZç·^=³¶8ÓÛ™œä'Cë ™½Ãa“/n{ðÜ_G¥«1ÔU«‘.¾s>ïìLÇÓE{~ ÷UœÔOÙZN •SG÷wIK9›{²ê•ý—s3]¶¸ÏÞ“]¼-…MvöÎ,¾÷ÕPLc<ÝéÌÏwBÄæ*êÏØj“Ã0’fÁÙZk{L+û˜H)GRbÙÄjq9ñÙKˆ%%&˜d†hPœ¡ŠïC®5â=6üò-🗣?O3å1ÉcROß9]ù"ñÔÇà\30Èñ6XÀ —¾Ï Ù]¨¿>åHoöчkŒþ¤"Q±.uŒI׆C¡áµbqm$Y+r°wáÈs\Ük2yãœ;s×:¹òðêÊÃû‰}¯¬>¼’<Á•ö®–l¶Òêèøþ÷ÁˆÛ{Ý43†.O¡ŠrÀ–·d7hš!'°Ïe4ÿÞD˜oµÀŒ1&Hk5sÕ—çJÃÎ%ˆpxÁ.L$h‚w©»¼½C¡WØ ÊGºÄý㯮¾éƒåOÈ¥Gâ`FŸ(œêþ-{¶rî¥÷vÿ–ÐÇ_¾\òRæï½Æàðšr§«Œyôî}uwKï>\w÷Ÿèݳ-ï~°îî¿Ó»¯®»û$½‹˜ˆ™*Ç Ôºx0ÚÂ÷ÚÌ j¦ý¬¦ê­‚z²ëj]bCkÁTô×£A¤j.êµh¬*ÊUÀ+ `=?ÁÖ¤°}7¿ ¶†ŠJPï¢Ú(jDAùîXRÃ[D¹0j(Z!¿þv Ý87гÓçÎ¶Š¶Á½G[cî©ÑDtµ 𚺞¼ˆ÷Ai(w’ÃjhÕtlO‰°o³Ê–-ªâíB8DNQ,¸#QM:y)jZÙ›T6µs¡JÛv-$PŸŠ\y®ò7Þ€,špñž_9Rf——‹<åI “_¯Í&×Bn¿Ýj7iu¶Z}y\y›¥ÕQ³øZ„+ß‚×v\!ÝFýøö/Â[ø ‡˜'+pmœºt–V|’Èo±ç4Wˆœ¨‰U‡öuSÎd æjf¥çãúxåÁ¿/ij±†Wïes—¾µ÷Ô·2G:;djÕ–q{¤›UYðµc³z/¾V Ÿ¬ÖÉc_ƒsr0ÙF¥·ú½”ÇÒ t/Òͬcnv+ÏÒ›™DÏATZð`OµÄ -)hxXÞsÏB­„ãÂ={Ò÷®_sõ‘#W_³þÆûï§mq”¸M#ðz _«ñêàõ$®wškk EØA#ɰÃén‘î‹þÒætœV¬³igQá ¦£p]þ[9O«™âßÖnVÅ×:Ü¿äsªÀdãIºMÿ²¸We™+ƒ½*¡PUu¶!+¾‚ð5Ä|¯rŒ™©ÜÅ|îÔ)Æxòd官AÆÊ¢ÇýBÆÈÚì1‰HjóWD`þvã#¨#Xý¥‹ðWPéš n±ÿJ?ÖÂÎB9Û¨Ep ÜFŒ[,¦ö ¤UÐÕP Ãû8™ÏÒÿõ¯0Ÿ­üÇÛßÅ÷^üÃÞS_øö©ZU7,e³V¾æÀ5þ¯ß†#ɰw bLÓOaÅxáúÊkZlEÙq?YˆŸUGô1¦$VCÆ&œ„R@Ö*튅ùrg"ÑYù&ú{ð†“'o8Žþ0“ýsŠÊyåbÿ¢’éPÌU®Þ©|øŽ*wìP>x⎇•;a›Œ°Mc¸MvÜãx= {ÜÒf½#xfÐL)ŠÎ$±0ÑÖTuZšë—XI°˜9¨¦ð'ØŠ¿\¿þðâ©Å‡×¯ÿ‘|ýÈמ=¢ez+_ÐyökGÖå?¢þJ¸¿dÀbÕ6‰4”ýœ¯‹ÂkhŽ’©£ŠÍoá¡D§[¾OT¯·qϵzŸÃ=X.ÊB‡Oí/¼A®uEލK+7ðÆb}GÏxtäØb2¹xl$:ÞÓ¡Ÿ¸Ù™/ö‡b9—+7 õóΛÑ,áSä#@5»·X<šqh„­0b¢|¨HøÅëeôJIôc–’~1²Ú¶/B{RX‰Â&ޱiŽiT&XeüÝG?ô‘7xßê;¶?y3U.gö”J{*gÐâ|ýë™3†c —ñôœ™±³­ª>Z¤^Äéé†w#¦n7’Ò8³²¶7PìeüÓýɳgŸ­ŒCÁàaxê$âT¥‰i_€4t&óVD炔̦>À1 ¼S¡ÞúÒéý<°ÿXŽÿú/¸{Úí=£%²Ì¶³vhÏ}Û¸fÜ#_Ý»—Û»·2%©•5'r´®ò '0Þ¿?‚íœ8lãlF *_£ÍóüNÍ®«S0qÆ]œMÕÞõ¼Q¡0òz½Ý¨Tíÿ Ní¹gqÇÝ;b1ø—úår‹== KÇB„p¿#= TðÏ1ØlEÊÓÐôòlû=Û¸_™Õ—¿8¢0*Är³2zÿË+Añ.0zct,¼±²pör«ŠxdtC £ ÖZ­ä&  ¶eXÈ ´Ò“ êáðÒ9!ûU}v°ts»¢n š´,y¢®Âlf‡»¢ž¥Ï© *qÀë ŠUõŸ`Oˆ3wì¸y&›¹y`ǸQy‹År𪮠–‹Eï͵ÕbÈWˆ3f:ÿgÙÔƒÉFÚê†'³æûh¸æù’À‹¹ºš˜›HŠ‹Ài2×_ÁA螎$&í"ýË”V“ÆaþHmÜÝÙ§å:ô.ô¢êÒœÁc×Å!+-L-¾z8ïÚ¿jmÙi7KÉ”ÁäÒWغ+Ó ùàÄp•ÓcžM᨜j ­^4ÃRBIJW]-þ"a-~JR;(º(MÝ(2ÂP?jˆPÎo R¿“TŠ8›øÂrWt¶ŒÇ\Ù E(ŽBs+]îŠÇ¬FcA¡ËŠœ]fƒ‰äAkr$›äÅRwª,:í®Py½Ä¸²¹p—ôy_,ž™|b2`Gzì› æw½ÒL¡@*}ýL¡Uóô}ÄUâpÑ-ÂkŽ;r–ÿˆ1Œ¯8{•1´âh‡ìåjè4ÏÊ%Ö9ÝA>uú‰ÇK^~rïuù…7¯ô9zr&}] qb/ctƒÙ6ª¬ Œh3ÉÝTÂO9,-KÐÌéͽ±ØÒYšï…î1ÑìÞ¡0Ÿèæ¦×È¥Ç3Ýw•|¥›b¼‹ty;­p­êî — îˆÚÓ³£È…µ:!Úá²%}f›ñ°‰Ó©5—ßbë( Dò»úü'Š}É^‡v¤„4P—)j3SÚ uÄ.’)íþ™Ò‘çÍ”Î-¡Æ\é©îƒ#¡Þ|:…r¥ó7% Þ‰@)s­?ˆrå1Ït¨»tƒÚêe Ðjêv ~ƒÛu€39¸¸è&‘•çÝ&çþšq»`ìÑ q…I0·mÎõôZœv s×G¨¾«ŽFÎ>ÝÓ3×Wì†äLOÏLÒï°cÌKMQâVíëëµZC=4ïk¨òüHa"n|dÄ"6–ÔÅÌ^~Lí*̤ŸYï\žr¤…ü a$'/ÿ ;…ÏÞírU©¢Aêw@=”0d逊æª"+ˆOטyÜô™ Ǧˆï Ÿ–êD@ž]Hó‰Ä•Á½—y‡{T¨$fë£bùË%b±s`r)¥ŽLÄL™B‘W…n•¨¥–ñ’ Ήa[¿°Ÿ+G¤ÈBp~v܉jN¶ `Vãém²Ù¨ÛÂÂ' Í Â=K<þŸªV¬¬rS„Máf èyž}óߊTY/ç1xíÙäî}ó5b•ŠùG]8(—ŸPª®9\‘1sÕ:Õe\!% ^´-”æØµZ~¾áTÕüÒ½. è‘’R¦><hª3ÀWm@]B™%uj”è~Ÿ+m}Z”fD&5ªƒöLI#2÷LA #–|‘ýXåç…˜/Æt&®í€Q@ö°D–ìˆ'úò5EÔVD‹’ƈ۞6².a>µò%Tƒ:hJÔpÿD-ôº_e‰‡]rqžÄ*_Ç@lìÁ;oIyýû®º*²ë©]C'^tr JÐwkF„†Á±š^ôƒÞ¶vk(Rx h- §sœìrq¥d¶<-–è'¥·Ê-÷sâO!Ûº¿†+ZˆY¿Ë[ñ.›î‚'uÞÝ‘·qZ»Ãca¾÷x¶GçÛø0ïLñ®\Ðb v…’=~íú&¥N­’)‰Á´·””bÓh’Ïðæ¿¾žŽ|v+;.cu|ÈÊ{-:r ì,ÈÁÝ'ˆsI(º´Y—ÚÞ§ú<þS&¹ƒ¢ÑÑ}™Ú{¶£# _´jÎOzþT$Ç…ÿû”¤×¿o3%éî»7S’ÞWÍ´fGà(hÀbëÊiôø%46)j`ÁTZ †&WbM@TD¾‘Iž}õçž}Ù ñÊkŸúÚËîZÍìbŠÞ|½òtåïŸL°¿GaGÁÁ6,Õ&õ„˜@x#³Ã^µáüUtMÏê´áPŸÒi#5Wxì7Ó±üµg>kË’ˆFnt[OÆ¢ói³ñÈá¿“ õ/§Oºcpú©¸ÎrXÍ¥-foòôÜÕú Ñ;¡lV8χ`w‚É6´ÖìÒCØô#dŸNGG­œÄL8›æÈâÃÇhMYÁ{iÑY¾¸°_mqN‡‚œ-ÉŠÄ¢SðW‹…—êlÆÓ¾èžE·AlÈ©zÀ3<4àèì;Ü›X3¸Ü>«Õçvò‘ñwg6çÎÿ¬‹ XÃk8 JmñEy‘Äqj³R5 ÞÖH—2ß\·8CùØÒ`4½p}WiÕ3ìJ;ú—LNª/ s*䊙mÒß½«¯¼6à¼+6ÝDÙw[d*›/éáW³£¦Ø¯ˆƒn°³]Lqeiæ[gUF‰Šã#;c¢nglv‰ Ú^³íL‚ˆãéÕ‰„£¸£™é ¤b®\È 9ÞÂ0LGÙ¯N8bÂM¡B<®-uX¼µ£|`äO¹]Kºæ,b±§£W@XæûæÄ¢ù"|4<]¹©#éÒh¹pÁ‹Nª±Õ ³ésˆ¡†Æ€®ÙhÅÕÛbµŠ4V#ÑÄäÅOëyLf7òµ@£1WÐõ…sì¤ „fŸ5¤–EoÔKê^î´³²Ç$âª_7¿xÖåëžO ]7¸“e¸¤W«èOš&}é;îÎѰ¿ƒS嬫Ǝ¥0´àŽwº³kg–>þ©Ë—k»6+ewâX!ö•ŠúØ;ˆýÃ÷:Ñ]ìÁwG«w1ÖÛùøîxõ®µV•܉ïNVïþ‘Þe/лOà»XGÄ߯~Ë€O:|wGõîлìz÷ |ïø»{è¡Y†¯ü±Õ÷+ÿðšÁjÜÔŒ÷h:¢êr. ÿ¼Ž lÀDТ&zâÖ’Âüô™˜ 5\|‹U+b¾údpÜéñoo·~Ï9UdW,tãÈ•íW¾˜tÀ1‹àêW0·w¦‹L':-†ÅQUGC²¬‚uê¹hkP*L¢3›í„/}x8㊢Jx£1gÇPˆ•°×Î"-}t¾òǧ”ñþ9Zþn¹?¡LîL¢œy¸{š 6¥V#Õ´{"æA†áݳوÊå,!UäÚÞyg—šîØò@$1}}÷G"û}sƒStãÔ«Œz\(-ö#í±o<]4]ç´Îc‘ ”ž/?SRž©&H‰úü00Å:μ¾ògö?þM,þ0ûÖõ7_5ó’e¬‡L³1\kW†™Ôk-CS*L¦®[ÅZ_¢Pˆ:o,•Ÿ“ê•}w”ÿkô¶»NïI*?nÈîKú¼¶¼÷ä1{šsöO½vöPÏ+ŸüÂMÙkàªùÄå0ó$ëgâ0Ì0Š"x=͘·°ÐüÃ÷; L¿cdž`ŸbØö*’A‡»œ)'“ýδÁ¥áãØ½ïGkîëðÏ1&F¿)ÆßÌ¡ˆÖ×¹ôAÓ½ï¯üýÆ¿°)æ=pÅÖÖõl7s__ÿÓåóæË_ƒ×ŸÇ(€ÀíÏ0o8öÈ,ÚF] IÁ N­¢]~í+‘Âf–äF}Xm‹÷…‰09Þo·E¢Ã2úÕï°]Ìã";Kªô5]^¿_¿_„3WãëS U«nf“Ìçñó?àë›àõ³øú·ôù·ðõïñõ:¼þ&¾þ ¾~)›c¾*Z€×Ürý;|}„íeþë£Ôême‹ÌïE>xýõ–½z?ÛÃüP4¯¿ÀV¼|¼Ð;˜}A1¸OÒøµž’U;‰âÇ,´fG˜$SÌg*ß_É­ŒDBÃûrÊ¥žžyT7%' ]ón=–N›°lðš`ÙàùæÇ²«án?e·ØÕ„Æhƒ m©¯]O”M†fP”pmãð_*B‚"à&¸sèz·©] “ɤ”©U*)³w^ý`åg«*—ù©ø•sVEŽürO|lt*}éâê…sçXéÐ Aæîd"½Ù†рݑ%/iÄpnµMÓœùtºFRÏÀ÷4Í£¯Žˆº.–e"ž5A[KH¥‰4|ŸG(bÄi.y >‰áœ°p¯0Çá¬JØ+õ\nÈÕ‰` "æV^fÑJU«‚W[#>»"d»ÃRؽaNíPX%*©Ö"ão}x(xGpˆY¹ÏiaÙGX‰’Oø^wùòë| ^)7X‹ó¾Ê;cå/èÛ–‚g~öP|MìBâº"öPK*`V9Bt”ÏÔJ¼ü: mª¼WÇ3K}¹u\´LpèÞÂ\h9UîØ=ÜŸ+&¢ƒþéä­}ÇïŠE3ƒ‘ñƒêxözÁ—ŠÅ‚‡ÃBÈï䯎Ä÷/vL™ÄÚùžìlŽ¯Ã¯™Àd[¸iC=l™°j8n©F«`CAsEœ£È¥A6g´Q×+^]}cÏþ¶ûÞΗ2#—Žÿ¶ûTá‰Ê3G)†úÔY¬O=&$ b ö8®j­âLIÔ \»•HkOSÅÌZ€¸®"G.-çÔèF÷Œ!ëpj*›\'á¼Æžo¼é-eöìåWúS•XrFÄØ„•wTçÉåÿ‚óõ« 6ZfW˜'XºN ´²6WÇ•wÁ€p”ZÑ8ï×ó2\i0aº«^§`V6v«LZ½(ªÅÒYVáíËgVö­d¶pôäØÒ¡~{3ƲÃÚ„s¬ΦhD(âkÄß¿m+f0€ƒÏÇ0žn¼ <ìZ©R…õa-µþT$ÇöJñ rø°—J?êŒhy…Ùœ2(~ü†™‚¸sô_sPˤ÷‰$nó‹Ï¼á Ÿ©È§wbÜ"”iÎ, PÁqÚÛ*; )÷Ws¡¶a›q¥`¢ká•Àß‹‰ V4¤tŒÉ¼éO’5¡_~0“f:!Éù÷ß~ûáãè3yÒÈpŠ¡þþ!Eå÷ÆíSݽ~Û]ª={Twݶ~·j_ ¼â<‡öÃþUƒ™mÖ1ípIkŠ;±‘„9^äèÜØxrcZs„Â]dõø8A¿œ$ØTK›h*1]“bº* qªÏPËúÊY6‰S¶&}Ý+þ—–ú\aý+¹Xd/­ ½á #±ˆÑ]:¥pcÄBÈ7µÿÊÍ•ÒæÖª^âfÓÿ¹OÏß1š4½Ž£V»HëÁê6¿G­<É €$øPAêÆÀuÏôñé{zÔë5èH°€'Èx=°ÙFoâv(m„¶¢ù$»‹h¤¥R›ô(Ax×YÆãH-÷…òRÚË¥:®1²úSá23yÄs{<· þfq)@¹´¸Ùr[猅ž3ŠKDòèi·Q—¸ùk+±vÌ¿«Æ—:‡WŠÜý‰ÜNØV|®bûÚnhkOvÓ=Ù&Y{ÔÇJŽ[jd»7õmêÓ£x÷†¦Š5£ÌýÀ®Ó:Ô2wn,òÒ]§ö >A7“çh; þöH1ªe’tÁp`¾Í3™¢à'=—Méz¾?>õ6—Ê;­-½7=Â|æm&Á¸ûÆ— AµMEÖÒÞë~ô£-.8§°U…WÓÕÕÄoZ`÷P ì¤Ýź;¶Ë^]µËä8'éòÎêSm«qÒ¥ØZjN6¨×8¨ìôo|l㪚ÂyüÖr´¨ªû4žS Ej·Š”ýyý`á§j°Tza3hÊRã•:ìLBÔÔÂatæÄ‰ï}/˜_„FVò1_©»Ôïd~yûÁƒ·0ÅF3= Ëmá\<ØÿÇ_»ûÅü¹é¾ä~¬§¸ÄD±Ô2ÿjïÉÅ/=ÉŽnù¯äÝêÄî{——ïÞï¸giùÞÝ 6>5==U™Û¹¼cÀî€= %·ƒÑVg^“ÔÚM[Ÿ8ZzÎYª*Ê™jÉ t‰¢o|é óZN'V›´AëüïÖ9$zÇËï„IÐç«Ù€¯.K¡ +\•l-Ô)ê2PlÏKˆsæ·äÛA+e”ºÌ÷ª*eÏ~ ò·—.žŽ=Û"GXse$f¿aHzÄ$GXF'V±ê‡„™{ff˜×Vþóe/cìc&rêÓ•ßbÏ\M×Áž/€w=ŸÎ'iIÉ)9_#¦²’é#KWëdzYÆf®ŒVÖ5Љ» ]M…Ỏ¾}8¥¸Ú•„…»ækÚrÍqÐ|aþ5Œým·ç›,hΫùÞ®ì£ûéꀽs#Ø;—‡j½èF}äšö0 r’yà&@K¥ôu’BÄɨ'á7¦V7jÛo'“ý!w vÇß1Qû˜<ÚÞyÌøäOcïlñÖN5ò”´nqSQ>¤ÜqµŒ_õP;Òõ$F&ÊBÆS ž©›ðoúŸzÃerj§/jSÙ”6‰B¢ä4‰—½øX°—_HÁé(>ÍrS•÷oñõ|^šÇPÇq«S}ÏYª´/Šª·æØUO¢$=ì#@j êÕ”ªöª,5ùâÚX%›D3ÕÀS©¼¸öÍ ‰–¥11™Œñew:vÜ6Øw­o‘+„î¸muJè±-w¨Î}RÙ¹|t|ñÞ=©œwÅ» s™ëŽzù½C–¿iÊ;fÈ5Ò w·ÊŠnâ£c½ eÃYÈ6~#%-fqÞº“Òc~‡\C~\•èañ€é£oÒºub½[7ôºÞö²Wœ‡»ù­o M„á7V^2}þ<’Ð{z'”Ð Ž?ßy*¹Õ š?˜Pª$þ¤&²bµx⢢ÞR`¢«ÒIVe=õ_CCð²ü»â¸Ö¦«mê‘7~æ£ïìWÙàlQu¿é£/Ú æ>çvçø3Ì<Ó{’Ëy<9îdåÜ7áÒd@¶l¶Œ«Ûè7ª'iß HÔq@B#-‚dŠ3÷XâžÒ¾~ßK‹=ŽNõ“MŽA4éÒ6F<”íXºè u¾0ê#&mÂÝ·ÁÅ}¥Õ~ÿK»ú\!={öÏH}CO?=’‡úöZXz8Ò†uêG«:õfdÝìæÝË—àÝeüÿ›Vþ0µòã±-;°m ¿Ouù4Õåψ©_[˜ú: “zûÙƒM?M5þ3òÍ»wÒ»¯’×°1b¨N-Ãk7û#àIp]»/Ê I¼ü„9Ÿ«cQóQ˜H˜ÒóÆª¦Á™ÍḈlKyëC7“;q_jùøX*ksšû…üxÚ&ágbáñNз»3KEø.®KýþW}É®¨RªVݧ3HdZwW‡Ûã*í*•öNLô¿sù6w .ˆÎ0aöÄÝm1hS—Õc&Þ;3¡Ýc2éÜ ÚT‹D5]ÃPû°ã‰™’/œLwù{æ7,ÆBli2N¬15žlÐÛ­«¡ó¯è×3iÇÙ*?pÙ­.@˜ÇƤ™¡EÖC ”ü;sq½rÍãåìü¯¡ØÄ¾F­¤™ÿR²µ ÖÃT‘ÖÓ ²˜í2RÎ’ÁEÕðøfpü÷‹îG~¨ñ(tZ­”cnŸ×ÞSù£ÌL9å¥~ö ÄÊ©rúàg?ð8ƒ±IÀ3Ø‹Ï6bÄ4%ÁcZÇ6ã¡:F„Œ'åc¬ÜšÑI•hŽˆïZõ³D½8Æ "T+‰Á'Á¦¸•hÙÔ¬¢\©è€dì­¹M'QJŒrƒ*9l ¸¹–ᤠQà]Mn¹õ¡ÁØ±Ø £*óz¨X‹ÙhÏMÏ>{SO”C¥ZÏ—+ÿòMøêRƒ$6½÷NÔN »bÔÛÙõv"Û¨ÑÛG½™áê£Þ\ï‘r)¶$Œõ%Sw]ÎôÇ'¢…Ü¡ÞÂÞA!˜+æ#cÔîØšË†K«¦­6³ã€Ïcð‚žn­XSÈ¡º«¥Gf¼½Íz)4îÝT/ÅØ÷Æ',‰|ÃÍH6ù<˜Ô‚êþ?!—ˆ¥:½VúsñÑÐjŠë±~§¢GŸTN§Cù4±dìÏà.ûC¼Ë>‚sïfQ¼Ž€­) .{Þ(8GZ®! ®#ÉBbÀÑÙfnˆ‚s²ºi5»1ŸÔùB ‡ã4Z£Kïzÿ[_4Ï\üäŠ'Ê)IŒÞžÊs_þ2ªI ç‹g 6F®0_óa4D#k¨hQ(Ðà·T:ipjmRµÌì’(õræEG¥¹fI-•À°·/:õò銞¹»x´¸c”(û/‡±~kÿÃñ¶ñ†*ýäiª™Cç<—êH˜!zÎ|o,Ù ÉäÅ÷œI¤¬XåPK*^%f%1sñ}î‚ÃQp¿¯¢¿×ÜU.Y|SÞ7¿Ù;íµ”Ê]æ{S«)TmÎÞÄò‘,oÛØxsñ£­è?ýi4+q%¼GF¨“@rm¢–¼GÌWÑÏ ªœFµ/O_‰a=d˜îÌnˆ—&Dµ)[¢¥/•%¾Tê/5üâCÌ?ÒÄ­ãßôÀzÆ=¨>#Èa6#+”aˆ=‡yûÀ7¶¿”³Í¡ {›‘dÏ[ȧjìËFNÿùjr#-3¡eFcÔÓ˜X BgM'ü/¤à{7)õYÆO‚ðSou%‘²¤5ó q†QU©š—ÅYDÙÍ"É”aÝbý‹Ä´ ÏÛ£N» “œ’œ'0Ù£—Y0H^kÒÉM*Îëù.ïå]&“ñé{§U¦GïBÕéõª‰µ)0´V‚y\èú^&`ý«\5<192:;àÚx¯Ddé˜í:s&•}¿¢Ç5èÀ5÷šñ ²ÿ!žAxpòŽÉIƒá‹üìëÌÅxÙÿ)žA¸oìö±ûz´·è(ØOËÄÙ¶×;C¢{"htOò¼x gx˜q„’³%Ÿ¯4› yææ}ÿ‰Þ……¾¾……^вVõ6µdñß­ÀCµeDÓâUCQ ÅFÔ’ôäøáwž‰u!Ís¿ßµk>üÖ·~Ò4ÑêàÉ.¹·ì Ô¬,äS^¬×Þ¥¦¶{Þ}B 3yžÁéͪJ$¸c;>¡&frZŠÀÜj¬A¡ 5GŸ—À]U5B„bŠ$&(ËÕ!zA-¯4(Í‚C§¶ V.bÛÚøÄ'b~"“ï°ùÊ»ªŒãÖâþ‰îÙ”™ç+_cc£’‰ñéÄ~G2>ÛÄ-/݃1æØúÆÞ–ïT9}¬ûøp-³¶ë&Rdƒ²nï3¿ dƒ”Y)ß0K/ÜÜSZñõóÝþÒ¬öëßR/öGl±›Õ>¸jÐÂñ8÷šõ‰Ì5Ō͸7ˆ²ºðŠwbm-Y«tÍ<ƒ9…gH¥ir—µ±èÝ'ðÝØå2ùî—iUjöò?‘ün?­½ßŽ%-rº>#ô<”õc\¯à'¡ÙÎh:ƒ#XÝùRÈî(¤j‹L¥”0ux8ëtf‡Ãå$ ¥z˜±H¦¦$U3Äb‘nŒÌÝý'ûG§O¡ý‘ø§Ÿ¹ˆãØÚÚ¥‘ 2ê"©×äÕPõjs­ Hɧˆ†Àéùt‘‘ŒÃ)ê_6ÿm¹ßçóË‘öOEÿô§9½n))Æ{ÂDf2¶Lñìý»êì5S+V”¦Vì]PÓµ‘m rUÛ# ¦¡î¡ÁºÎHë~ S¿ Ý£AžÞxÃÆ«ô÷Hˆv¡‡x1Îï8ìy¸îÚ­!Eõ 'Aßµ.Àóé>¸ñ†7„’sHzÇü¼‡yïñÝ»ï6D`ïâµÁcþbÓN3ϵˆÖËÚˆÖkhŽÖs­£õ?øðÆ4¬³mV…Å::£õ6©Va¶Ê¡kñÁYTÞhbÏ.©ä ÖãÊÿw û}C‹X½ìÅêÃŒÚrBñsñ•Ÿ#ÿ"ÒÄwÓ«o½nà´iBF285½)V+vÕx7X^fÿÏ×¼æs£’߬ß6ý9ì„sõØçÙ¦8½ì#Nï J£9NO‹ŒUÏ4‰Ó $NƒÿÍðÐ8=-­‘¯¹…vˆ8.ôÏAì|èù}ˆ}Ú|y ·÷"¾æàg±qö &Á¡-y3²™wÕEæ [iBÀP™O È|8Ô¦o¶ý§’ørâJÎÅ«çõx‡Ä빂{±wJý.쯃í]BœeMqyÙÿr\ž:Tí qyGýè79ôð0ÿmîEóMN½{à~ùË~½—ÁA$õ¾–a«MñxY›ñxžúÊùtSQ!Œ—Öeä4Æã7')jÔ“/eL9¯Ê%DmV#£ñZ«Š;ûð9h´}rÅá䲇YÖèýOè’¬ó&4Çâeÿ·±xxÔ/î³èPÅö-ôªao+ ©)/k?o ’°-_"©§th$„x³$9]p§ço,õðŽ‹ ³}áAGkHÐàÝŠøÀÎN”2r­„Ù‘Ó´fóÊß‹ÑéíVS“´®¦ÆmáœýÍW^± 1©ÄJ“fáñÏ=ýAæ?~ ÆÂ¿ªÈ>à…¿Ø…5¥ýqþæºóÛ# iXŸš6ÕCÜ@'8n­Ãý;fÃïÙ ž}Û‘Ç6F¥Ì;©+ÓOc,NŽæ ”³9Ú/k?Ú/ý_Šö‡ëÚQ]îŸJZ—V¬²©2¯ûÀ[ß”S95b¥E™xÝÛö]—ü’%a±Ä­K û—KÜbI˜.߃÷îØ®)Ø®æX¿ì5ÖŸßë¯-!æFKЛœ)º7bƒC —ÒÝï‹-éÉüãGÂbÑ÷ñ’Bþc¬›~š•?ÖMq$ÇR¾_Õ7A-RîÅß ÁÆZ©³ú-¬ÁêáÝE¬mÙµTñ·¼ÕoQûÕ¿õ=°½y’꽨憭ÜhÕæMúÕŽŸ¤ÚñƒrzW¤¡w_)‡?­CFÙS”µµ‰ç³I u$º<ÕPÓxqäQkk‘0HšO«u k«• ¦—{{¼°FžAø¸L¡3ÙM‰›;wCêÖž¾x7¯íÊwñå_ÃVßÎ>¥„>޽à¡-ÞUËZO»+*/ ÙLËüVÍåQ‚w‰B%`ôUä-Çœr2‘wÃo 3aŠÚލO 9·ˆƒÿâê?U- +R¢pŠ”2K™+ðÐÉàä×ËMr¹^ó€ÆŒT.eÄ*u2S½û‚wòJz—ù”-é7›ýIb¿5û’6½N§× SQ³ì­'óëÐu·ÜÛ¼xå®Ìa|»â]w膰Øî²“ÛŒÝíOÄ¢fst žèšöù ¿?‘ð}wnv%ÆLáÜç§ëmƒ¥IÑGЏ‚S´ÆI‰F9JðY> ú´€PCÄŠí¤³µH72:Ijs•²ìÖÈA#œµðäCÓTd†7jä?Å™¹¿ËÍåW~´ÆÊÁ©e«àl½^£Sñb]›‹#cÁ½ &gÐÈ™l ¯Wè絺ŒÚyóñ€.4q#iLj'—==¼w*Q>œó*NèÓ.?óŸþ¨9åáœáÊÎ=0ÌÁêÇ.¼ÿ<´M­-U›µ¶ÌÀM<ÿ­Z[ûa`_±ön‹ºFwÔV{Gõ³àKßqjqñ®åhtù®ÅÅS;â·YsË}}K³9³Ô×·œ³¾4*Qô¤Ò’‹= ´ §‘k«u«êª™‰ˆCEyþᾃ¶ÿù÷?îÍÅ‚ÆC/=Á]‘¨´Õ¥çNä0bû!àÃ[f¢ªOiu׎X#ðUñ›Uûò›UûúDµ>Dѱœ¥{äØB¢ õ;æí™bo0»Ôã îH9úí¯ÞQºf׌? óóö]ãÊO¥"ËËË‘ä`0û‘ ʘlb hÝ2jä€Møq“‘£ú&Üç<|ß7±ô¢àˆw>Ñ9lLLuíŸÍ&B¾©ô-}{ T^‚UÕ™èš3XüœæÆtÈtóûBq.9ËŽZÅúÙ¾ÄhÚX›J±'1fig£FÖzÆÌjRë qu¼ÕUå™2 ˆ©Ÿß‡ŠÚVK×198kqÙYùÿÅG*ß}‡ˆë†×\ÇÜSyüØkù-îy·£à„¡Á ¥ ÙhF÷6²9°¾ô;f¤Î›€ŽÌ¤ÿë{€5A T] •q,¯‡‡¼“ÉÂè`No·ñúAs¦òŸ*kÈ–˜/ ]T ­ñÁl¦#+’«1fðÔ ¹ÌÖ1•Ë/•Ü5Î?È‚ÝmìÏ€8æ³Ç[æ¸á¶ 4š’jí Æ¾qÿÀóÉ-bz3$-5{u~äzיּìuå£<ë_ÄÆóî`ÿRÆ$8ô1gÙ%åÜu`æÄ޽ìˆ&={ŒKz¦Ë€Ëfçº\GÔö°#wúC9t¦ï9Ð kÙNä&¡•›Ð^ …q6ÏŠ¹øH¶ò,óãòTÌ(ž?sçzÿ©}÷ßý’5D„1ì!ð7w72>m³3øwL$ÞŽN¬ á&)¥«µõK„æÐ‘FÁÚ§„M·™†nó¤7U—-Q¢âÅ=}>“/näaYûtÇlÁ•Ù^£·wŸ¶#•2cúdÑ )…ÂV »Ò!·ZìÏÁšž6{r0œ³‰ÕžHÎH;˜{âÙ`2Ñ•ï‡#V·Ý¢1g=±tñŸ‡=¡£s‹V~¥ý@Ýxn’R…üœz­ú9)ÏT”VÇÓS3Õ÷”ˆ ¿MuI„XÇo8¨ÞÕërž€ O"~äzß\m† ËBlϰTfm"žš¹†ÙåòáÔ©N£Ê›Rn<Åf»|A4Å2ó]®ÚìCR? œÞ~Šóx[Q”ã!«)&Ñ9`pÇy>î6 ¨‡³ö¸×hôÆíÙaÀT¹«ÀÑ|È! ÅoV˜ mnOŒ}¶P4`áÚ+ðçãŸÏìó:,£ÑFK¬#æoá„» )ãöÙôgô±™ï|q¬„°ðè(m i4Sª&sV½lNŒT 4jšó"y}u êg-ÑÎ œ&¨€ßäõ9,#±Fô+ ±€´  :Ä>ÊÈçÛ}¡W(gƒò‡åA”ùõµ×þ–(Ã*`Í,rÙÙóàM%—e.cÈÈGô fB’‹–æ‚ÈdE[<ÕܱzM¤©YV§‰æ (0µtòü蘭?Ë5ÄÓEl]rú4äTOr¨(<…¢"X…÷Ä u],Wˆ·´¯B±Ì ׬<CÚ„¬‰ý@ÚH!mH‰5Lx"OrôIŽ>yqÖ>Íü'‹S˜c%–3lRÈ»ÇþZ~BÄ| b‡7‹·^‡k“`ì¬ý/ÀÞBëÙBëÙOÒäI+}Ò*áÆÊšÐ¬BÆÌ¿ç:jŸDË™o³*TÈu á;!U†aa9zæÛûö18 ÞZ³È[yx‹]/}´œ |šÀÓS¶bÈü ݉~P¶bÈàv¹‚9AÐ9£®‚Ñ…“`2Øž´Û“íAžlK¶mI›-Ù¶&mx<ŸýýM„ù/ŒÌ9º_„|ã ð…?Ôʽ؇·yûÄJ˜,“p­pe{"‘ž¬«޶¦Ó­ø‚|?Éœ†¾ne8RTie£ßøI”> EK?İ]ÂlBßCŸ·”6™KNËè7aØd»M–Ž=ùä&´´ôäÙW;Êx =»`̉§Ös“=vš õ—¾$ƒ'ÓPN …7U¤eU-CýºN‘)}ïñµ›dèìÒ²ÇR‹)t=ßñR-HŸ¥ÚÛEcNÎË?Éár¦ÐV(³\«&4‹~HJË-òŒ4)¦”bª¹‹â%:Xº¼ïsŸkB÷x×N“ZOB­o¥0±Uy³äë0CñÇžIÔ/“•¾D0 |FÂG­:WÞ‹A?ŰkÏ¢³á«+>ö¼s`àóÇv8ÎÑs¥/¡~n ª]ºÊX õþ­wLz»\[®R[SÑX%/1¯Dß)]ŽrŸkÂM1sð‰iÈó0³½ˆî‚¼Æ,Ñ ŒS½Ô£J-£ö;¼Ñj ^_‡ùݨPzQk4y½Mk%$-óŒÅ‘ãŒê'nº RÚggX×ì‹ôR8òŽï´£×ï=å²ú*ý üíËrxOïI¹Ò/äå¯J>ôú 6|rüža‡Y{;|ë¨9mF­ÝÂäv¢£ìðŠ0»eÖÎþiö‡Œ‘@¥¦eе¦‡ÃŒ3ÑÍI|g{¤aÏÞ]™Ì®½{"ÛßùyGWŸ3b­«³Fœ}]ðåS³ËÐYÌñ’y€¶²üöêžÕ_<«m禵 ¸¹‹–Ydð¥c|Y€X‘Ä\s“µLœÑÑ(t @ÖcÔÕ)ž5s<]ÀH’‚=HG÷lšædœÖ—‚M ÑéMËbÞ¨U-O i¹dá±›;&œqQ´é¼­B:Þ¡¢bÂ1Ñ1¦¨wòѱTKN-SçZÒKc¼³^1íø+Áƒ”xu¿ƒ˜“Rz¼þˆ“µÆ5½#¹+‹„‚y2óWœ¶–eø˜ØóŠVµ Ã*S[EolÔGŽë‹áúÈ¡>7(cKÓÊÔX”@Ù1áHˆQ¡ªV6QŒ;':bSy«g¿ík[ÐûÉ®‡™m”C#`â®ôù;¹mÜŠ¿úå¬íeCgB.QvôË‹ßç~ñí£å,‡œÑõ¥ÏŸ‹†ËY³U9+éx˜Ë›#ù_|ñûè™Ò›Å¿d`kc Œ¾|.Cè2Ý~c©ìaN÷G# ©ð=Åœ€y%¥øL÷I·wkÇ´w ·œÔ}ƺä§ú?žìÿTò0®õÚÙÇÐ,û<À¦ó%"Ë]™@8Ïÿ©`Væ`‚‹¼h~O}×]ê³ðØåU¥~ôQõYªcïWåÊ3ƒ˜–й·:5†ûèœ_bÝÅåiyòdu„Ë-J… ä‡@ AA ´\ -CCc€½ÁÙϰFöq˜?{˜.iÆÆ0Ù!æ#}¹â Ýö 0Ý8Föì$–„¸zNæIEž2ÜÛ)K%Xпex°–¥žîõTÊ‚,Å]©\»×-¶-i“½aýÊÖ_¿Ðì,D;;¢‰˜FkÏ]äËuºC¾ÆOx*–ÍÙÌf—ÓêÎDúcÔÑ¿2f›±¸D“ÓéKu§RݶúˆhÛÐ}6¿Ó^o@`3X‰môÇX=±É2“ãÊðè1IЭâßrF3tPäkXÝ ,Ùª†xÓuÿån[ÝÒµ2o-µ”~É=†õjφþû6ÂmøôSnà ¤v‘=Í;X‘KÎ u’¹cQ›m¾ÃУbAÊåµ¹,_á%*ë)÷úUíOS%¤<„»!ôA8p„,xÙš/;oÁk>ñ+cj[Ðg5ê-úψè-F…PÌ#o¢+ˇèS«v±§³™Á¶¢Çj«ÓOyÜæ`£7?ÒÙ›[)#Í ^¬µWa-ÁçäùQŒ¤hÖ’8!YÆZ#`,S> JO·U0˜¤©ÔtBøÄh@5¢´íCë^ºŠN>**ÊVhqUÕ}„ÁÕ>n'©­*í,´s$içhüˆƒVù£ö…—þÙ 3,Ó ² Æ>hÈ%‹Ù…­ñ‚'Òª[ê+§Ø£°š¡žRñܵà;¹ˆ~~n®;‹­ãum©H»{"zV{Êö€#šŽØQs´©)*65ÍÞ=3ÕŸ%šp{GÓ B¢CŒµ§ƒ¯©ô§B‹àâ¨ÅÌìkCZ 3ó€–¤?Rµ¨ë¾ãÖ„îˆ@/„`1_mé>X‘™©˜×Š«¢±|hUÞ;nU4\èþ±O|0 åÝ q½F¨‚õ‹B×Áß ‹¥à hæŠ+Jï74Pþw jÉø›R.åÙ[ß.,×I„`ÇW ™Ë(+ ¿ø½ÃRƒ53)ÿ$>/|v¬t'®!D¯Ïâ Íá Þ˜+­’x{d=>ænFŸ-Ý9Váå‘õø˜»y m*MBiæÙ¿°Kثɾøfåß¿/N­2G©|#(äµÔ(<ïúHþÚ‹ ÷ν«%[¶-K¥–mk©„ Ë[¼Þ–å 4ìˆt¥®lW@Ì[ŠA#~³SŸŠêôêsÇÆÎY™J­ J®œ’\|\c™²ì„k|Z?µQhÔíÐñ‹_Üpʆûà_DöSN•Ó…húWÇ*¹ÿÀç„ å~t6ú#[‚”Ó”CòÉÕ¢ ±­èUÓ¥D}Žª?L8ð~nG§»;Ö>Ðè)Z¢aŸÎl ‚6Xk.ØíÆgî>eÆ™öZØ«7‹œtBœc[ÿ1RŸòcõ)ÞhFÜ>‹Ù0Ä…þ¨”™mž —5h뽓-õ&­¡^Ýi²é¿Åé×ÔE\Ö°#â.ÀÕwn=ð€˜Ê×Â~b²OÚ£XEö{îÜ´tYφ\®#÷]÷ƒkÖôvP = ­ëcúÙâ îk•f­B˜PÆvÑiÊž#Â(èÆŒ «Éa7¬ú„þ{—ÉT6Mc›¥‚8`R©Üw­ r\.-D=FLKî8ŸŒ¸˜3p¼~h ò @¬l¨M%)ú)c=7YDN0Wà©¢yhŒe+_Aˆë\¦H@V/ÚÒ³&5sÄe”»´ëGi—`êÐhÓ½!›²h7¼f‡ @ùOhË/†õLäxpË ^åõs ®tâx‡ÕY}üuHåòj6Мoò¬nöµµ÷ŠxÞ¸Ÿön/ôïÂâ=Ü@' H•TZ£š ÓG^ WzøÂÙCÒ`Ì‘ÊäÑ>7œDq†<‚ÎjT[‚ F³É 3Ú”FÁ+hõ‚6ñ´2ƒËÃûmº†¸Ã¥‘) €“ßnxUkÑÖë4zõõºz“RaÕèÐÖ÷B½>õR3"mëêÕÔ-«Bbd­¤­-Œ‡*>[¨š*U‹ž7¥ð–ª ¥f>u6µÂ¥ýkLç6Eü2:…Ûl-|¼ã©:•Óì÷~Íjn×hR~g&ƒÛžì û;2Nâ?ôt-º—Ñ-ºO'’Þ\KKn<ÇÖ6X>~ 9›Ý^lގ׎߆ï/–¾WU¾«ý¤#+ùvrY _6·g³ÍÆ—ã3¿‚õùÐ2Ñ,Д‘Ï; â ‡p©CnW$âr‡0ßš=_ÖCYľ:Zžžq?-©§j°ÁªœÿfD‘Ì.ŠÎ`¸ç’fmÐíu;‚Ù–å Yé$Ðs칬’»r*BÊ7Q]Ä›hÊíò9òÎÍ4å>øêq’r M¹ ÞùI¹µê'I>·Ñ”»!å)òÎí4å?á«/w>ESî—r¾ƒ¦Ü )Ÿ')Ÿ¦)ßg^Cײ?”;iÊ·!åb’rMùJÃú ëìÝMSÞbžDg‘”{hʵh¬ÁÏ,÷$Ñ» îÿ¹ÿ±¡KŸ™ 2«œ7,ÔUãAà m[ÖÁVK¶MxjW—¡vÎ’MYsyW›ày"P%¡Ál®më©2¯Ght†ƒñþ±*Vhá„€±«ÑŠöµÅr] Þª7ô[lÃ˾²ØÆ Úf¹Ól4àZÜ µxeÌPâÀ\¨Åœø®¬^d‘Æ«L:n§'wµdfÁb;1¨Ä’óZêÌyÏÆXõÄÖ”ºmÑ\Þlö8u´/Dß=³Ñh™YïKÙ¦â+—hˆõ‘‡Êï®ÝLtu!0 Æjú>LH¼.Òuzs}á@Þ”ÕÙub&²øô1G7Ú'¤ºb pc³»5ªl<–5¨º­Œ«‹Šo®,ŒŸXpvŠÀÁKp¨è‰_°]k<=:SžåxŠ«jKeYQ7mb¤í^ HrºÂFnû”Š5e¹z‡VçÒ !/œµšˆËl14œQ‚§0¥­?¤QÇÒÙ8@{ÚŽ°ÏA¦¯k4Á‡|åþi•À÷ÑÓþe„ºç#”òOs戨Ô}|»2±M´pìª|Xˆò÷€èŒ«Ëh¿ìމ])»=Þê×[•o¦ÓlBu.ŸÏ…Ûùóèú€›dlD¯ƒË௹äBý ž²NV,+ƬӼöæË<1†[DwúaÐܾo:ð·e2+²h”£reN7ñ7ÌëBÉß#mZpÂpEÛ ŠWÒc]fÚ½«}úð·ï›ò¡–Æ»ÉQ@oî0z‡;_úŠ×U¯ÿ ´ÉCx;·kÀù޶Fó£\“œqÞöqäüŸ=‘ˆÇ-ŠýDéitVÀå …\®î——1A×@®°îX pHÌ žÊr`—sŽá#%Áð4Wö3wÖâ1»µÇ?3„Ú›i¹ÑÕ «¾àO'J¯xŠ1;úóˆ¹êõgÀžÊÆ“AËù„ ¨äïÜÁžzÚy—íŸHa¤”þzÁ_¿ 5õàð¸æF)64Ü Êb•ˆ‘€) ^ðd¾äQ—PgÑ…0Ú7Ú…ÐDéa·U._.'Ô"‹° ~rnejÚs¾¨“4°•6°ŽÈuaEIcø©P¶'Ìkt¥f§Tœ% 'qõ|C°˜Ûa¬žF›Õ5IX¼n§50ÒñÒW<^ïQòSés§…‰Jpž¸ß ¹š~·RÉñùpsŽvñ¨J±Z.&d$ñ1&ºÀû”27 !U²$ª×mbš«Ùhñp„qŒ¯€ó.yÛ‘õZD¿eù@¢ß‘á“6‡5åµÄüü$Úç仌&½Ö`T+` ÀgZ]õ:½V£¯W+ ‚ÏšîÄ0Þˆû ´Y•ÁlBµt—j±QÝ5ÕYÓWöñ0}.’u-™‡ÉHÞmÔ/Ñ­>tÈô:M–Úûtþ}Ëd'4qJØ­QjpÙ×@Ùϳ_ƒÑÕµ.Cupi‰US+ÕB¢Ve¥‚έÿÔٌɎ•*ÖšGû†:^yÎãÒY~Tz:mó;ûÐ>?†²t O[^“©æËçûèøõÁY•NŪ´ªì–3qÓÞlÏ99ûÍ¥§qn EFö9ȫ榤ÐcŠÏKâ–ŠôòtgP)ù¢4SïÉô— ŠÀb)ÏžÜæÑsæÈЍ™3x6ŽŸí®gë]Ãðõ[·^¯Òç/œž¾Ãq­•ÉTi±«¨;”_eAÛ@-h³e<ç›)™3`rÞ¾û¼J“Š“ëŽ=;÷í ¨ÍjV¥WºÑG>ݤwéᯠÇÜzøk"kæoèû‡‹`W›Á¦¹h_0;‹ó­ç^®ç5&¹Vnw(åïȨ4JV¡QD·œ¡q†Ò“qË'ëQ÷•BÚ&d„+KOû²^¼œ¾ Ï:P¦šæÍŽò9]‹#3L°Š{˜°j—°&ü|„Z%k¦¤91QÏ4´ZÌ;vÄå:§Ô*c[N6ó- °¯£ãê괿Χà°+Ò~ŒB€ÿXéO¾”=•ï6hô«Õ£×`\_¸þÿ¸eP¶ŽÎè&ÊÕ¤¹v\à‘Ì÷™Ž@nÈ–3&¾D® xÈ“®@.h 8zL6·Û_ ½‚9pè=ßžÖϤÉüáÎÌ?? íQÝÀiHa€‡#´„T¸UÍWuºê³¯€+Ž–æqä¼sÈ-¡Ë“‚7¶€Ém‡-ifU+ÖõœZØóÀßLæ>ÆÅGô&[¢=îLØÌoéËÝý?Ö÷ŸcÄÆdÛÄ® { Ý9qðµTD´–qB @÷ÁºËe½¥ˆÁ‹U3óÞቀ±~ùðÚl¡©q-ܘŒ“ÃÓB“£˜>9ÔØ‚«kd2Ýã+Z'G¦íöé8!à/ “#ë´ZÛX>•Êã«ì¯ü:Vþ3à(À唸ð8Äd­JH‰´‰§õrhúáfâÔFmœÚ Áa®”Ì!È$'ëóƒù »»;»2Š$z–ö$Ö,ÝÜ´Ì;ìŠ-IôŒö$VMmnØqúV·ßÆó.‡+Þ™îY¥½äbeÂ7e·§M6›+Ñ™êZ¯ÿäuò¶à…-Ò³—D«™ãmjÇqB¼,¬RH‚í8í,e[„Fj+²<Ãyè ç'"o,íaŠ‹Dj{91ÝJ*æçûšz¹ýS*M }ª±smpÜ’tg‹*ùC×!dw¡à:ËxA§_%·D-™¥yw\\Îóé¬)dZ§Ô©BMNÙy ¢~ûÏo©ÔGÁWKõ‘LX@}ˆ­ jˆÊ@VMåZP'òÐfvHqIµä©žŠš´dÙÞ"¾j­náë͇zèÊ{nêл 2 ñ×ßsåÃ?|ý@q…/ó÷úàï²ÒOàöz⇴T¡%˜½5ö@³L†ºr‰²}ü¨s˜Bè…PKB ?™5MT$! Pé&‹HˆMEæ1h9Þ¯°ð‹0ÏÂîù¹µƒÚ®*öØoÜÛžš|¹ý¥÷zíÿúƒa"¶Uëi]ÕÒ/`Ö["Ûwv42n¶µ7(´ ¥N1ÒÒîr,Me–Ù4ûKWCß A/,Ÿp JÂo'ém.º-ÀQó‘¸Aˆ+’P²‚ÄW|ÓиF–úȄ̔ÔJCK4R¯KüoÃÁPhÎß{}à¬zBì²öyœ|ÈÐæî \ªÇð~}wéÀFE´gM!¿ª+¦:i½Ì-LÛ¸Òïdfíz»[¾‘X︓C½µ~0«„$Yv˜6UÂz­Ô\SŤ/ÞzÅ-S6"èa+CtDôÜa£Îbw[\f½Â¸zÔ&Wy‡›êœN›Ê¥qA¾­<ýBA!¤7©•{”,Òú ‰ëNV$Šc6N®”"W&‚ÅÝ_NµúŽÊ–cßú¥crÌÁù¶¹ªúb'dp_œÛí„TˆÉi t Žb“ø9Të(^9¦èÛIO•í0`Aôz5}3JÎ*˜#J‹`††+–…Î@Þ)‘Çh9ÚzJ±wJ´O-k0 9ÃíX8ÂêG×\´WÛ¨©«7jsºbtEÖ3M„×äµ9†×hY•¶h9ô0÷ý“šÑzakÉxºÿ¦³®zвC9ÃmSî±ÄÇÁVhi·ÜaYMË·(gŒÝUE9˜3¬K¯XØÅÑ­‹¬DQTÔn¬B=„¼4^õÔ–~¢oÆè)—8¥«¤[@Å!¨¡­tÐâ_E™¼¦;UûÇœJ>ÝÇÍ£±è©{e Æ¾Ž³ @fM=©~…Φ["Ó骭\)ÑÚu#ޏ”VÎ8‰ãsÉɤ #B¿ï¸3<è-á¯xC%y©‡º³Äd¨ŠÎZHUCªÏ%p±ôT‘Hµ Nýè(^ÂpÙ©Už(µÅå¢ó›âÏ‹åBN4çÄ ¯ºÈ•è®"¹°¾¤Y  iñßÉ´ÁÒ/àßSËw-¾(ï0m25ÈYÝ9ç”6÷Î;EcAñ ø×ÞØþÎ;ï p#üÜ`áè(`‰|ù™- ö"·#—<¨ °Má—úŠ@éž@ †B ]ð¦yÎ_ož ¨Žž y#9l%q;pòÄÄç“ã]qU_¯*ѵ<‘<\WÀVÒ š«Ñy+å÷A¹.Ñ3ÿ€õE¿ŠI›0ìrÒÖj€GdXpæF³ˆ©`ÜÂuÄœ-±ùD·³"˜ÿ–ÚZJ¥•pḆ¶³Þ2Ko±´å ´å­:èÚÐIí!»á‹ýhˆ·5POAÎV¸Ü@k›ÉÍáÿ"yŠâJ³j—LÁºd¥ŸûFû}‡ AëkÄpÀݵÄs«Ýií‘ÕÉ,z÷Ê+K\¾- lSã÷¿Ç¿]¥×P ôÚx8ZGìsœðyé9riŒš4Ý'¥Ô¿lN. G (é¦ HOºÚ›…LeÕnõÂsòœ˜¯˜èV’åü|Æ\ +8¤[ïiìò…²¼Lqë­‡†¶ªeB«LšMÞÛ¿ÝsÆæ™xoxñ>ÃHCC¿¢tJØj?©Þ²O¯MD²ÉïŒ02©O(Yž¹²†ÓZ¬W«%χ¡£åC4)óp!§}; ¡œN†*ÖÓt#}ÏaB è(©ôýêþoÅ싲z ¼¨ãs…&d<Àˆ‡ÚSZ,-íÕ¦Ú‡Ääy®ÎÓºîztC Ó}>Z›—§óò‹ôžl°M©SÊUò6ƒ¯ÆH0xP¥S¤äªS"e‹¸×±XD˜- ,âjj àBGðQËDe:¯«ÌÔ7/™ÆuÒ¡sRkM Ùi/§Häטð£H#œ[ñ·¯ÈmŽº1«Ö;0Ò°,}Õ;;e›ªaâÔÁ%û¦òu;NVF½Ë­öSOÞ±GwÎÙ¥ï:CÊmO(n˜éŠÌÞó4ÇñÖå’¼uAk;±i€ËEëUÇ\S:Ó©)å–SŠ^™)é~/™ì åß…î©q +Êü*­¿ çÐàžSdí#¼à¹aä)ƒ)èsçÁU]ëw«ûÏÐ{—~£R‡£=C|cS^»m•P¿DáæaòsÝ>‡c\ÌnœôÚGôvͨƪó„Û7X<–:†¥<Òë„c=¹†[¯õè>á¿e4úh9äé¬î‚«š­‚4 QþÕ¸ÿ ,å_ßl„­…“òÓ=!lq">æ¨XÁ ÿxu­ÜeBww›ÝªÈNž62xÚT“jý:¹è×Yä¥éºèÒPJu29ÕË0¬F·“Ùr\½gzhŽRy]Å5µ’ PR¥:B Û:*ø)Ç ¤eñH¥\Lx7«0,0ÿ®>Ãö½‰‹uÅaôÄ­†e£ê¡Â—î½÷…3û^Û™#Á®‘î Ëë/OÅï±"¡I矀Ñu•…B/¤«Dg )K[D¡.=“T”ÑÓØD©#I#…<°¢äÊñÀ¥’‹Ú øƒýÁíðwýÕÁÇáÏÛ’t î ªmßÚ_øºŠþîÆo]°¤{U®8>tÊõ—ÏÜ¿sI âU j)0¹ãq´JœíoÊòùî<Á±Y@zjð†¯²¦¯Bøú~÷Ðã»wŸ^ì.ÂõÍËŠÝCE†•JWA+v}„3<ð¶x jº 0M)5€bDЛ_ÝŽùjéQÖ„ž\¢--e`¢-t5w~¤vµ0jÉ^sTÖ»ˆò 2:‘ÊqHé*]ÓÃ] Ê‹\Õ§…0k(ùª=´þ=߸º'"ö®Îv¼ûn±©µµ©xx‰<×/G"ŸÈ6 $ùÆÑÃhe!SÈ0ä,ÃÚ¡.FfÝ¢mW»^Kž¤Ê3C=+u ü>”犺9رO[‰Ÿ½héRtKéýÿùŸ«¯>i¥=wÞÁs—OŸÀ$.vÝ_e[@²$àkfôÙÜÕ¹»G]ÃfnÌ~{kzÐoX÷sf±{ÌCì¬ÈÝ8{1êg"ÜÏ>Õ}#HÚ•ÄSÕÌ$:h79‹;Ȭ«@óB5æÈhB3NhÆ åtce—nv($ŽÉCùª2 ÑMYˆ!z\y º ´³[cÃX èx±¸dñ'w†‡ Åas0ã4:F£Ãá6£oDJkjÒQN®XF$^:ÔÞÞ××îi˜»<¢èpˆ¢çÉä·6”&ætzEÑÞNs ÁS“Zn†`σ¸iö´r 0¸n½°òt°æ)ÖO*{Òðפýn©5|øððÕW“ßý#W_3r ü]=ǰ8¨Å0Ôb”œ¼vÛ,IŽê¨/·’.'ÄMj¿¬¢wóÌ"†j” ¦ÓÁª«ô¨Ç&¾J7Wb¯@ÕjmÈAÊsàä÷lDÆ0g0 øýIMœÛY‘ؾƒ5 •øwÖF¼º{ÑŸÙƒ0o´Ñ݈ÔIŽd*‹fV’Wœe(ðû’M/^ÎËÑŸK ‹tµ÷ýmï_v¿Œø¹Ù e¼M˘³ñyW|MÑ2°ŠŸ‚:5RT•&§\,’—ó›‘ ¸Ø‹s¿ìýmô¡‰Í’Þ0¹ÀWZ’:OµóO ¤Ê¥n £Ý@Iý³” MëètïçqÙ¼RÉ t¯Rzù3ÿøgvì¸õð9‡¦²@šYÇØbph©F|5JHÕ8”Žru©n>ÜÕI0„A!/Š€†½ 0”8xø ]¦ `¨Va3HÆjHH¾À zÁ0Sft(‚¯ü—ï¸bǃî¸â2ÖV!Ë‚%W5©µhõ%NˆËljÎlΙËj«n;붃é”,•F&tU錋.¿ü"B'¢P“=Œ™éƒ^£¢[©åuhŸ‰òœ|iU¡§|§ŠJ6q¨ÁÕ ç?ýÒÙO¿rÍàÚë‡"~h=‰”~9=gûÅÛÏ!?„›|— ¡‡a„‹TWnκ‚‘ÀÂPÊ¡¥{UØ.OÕ.{e3ç7-y‹¨_ã›Ê8ÁñßÙ\¶h<6ÖW ‰ Pæb=0ŽuxÃï'(ÕŠWc¥h¶¼péNô2ÐÇÒësïÖXi <+é ¤ðdÞ5°ý ô«Fe¸¢ËZ¬¨aIJ_JBʧ"í`ª0ÇQ7×&ºÔÒýžÆŠ<¯r šª,U9í¬>G¦ó$>Òñl~"*zÒž&Ñ:=œédÁ¥90¼épd:K_Bz£hu8¼(ˆ$B…¤=˜îޤ] V°aG2šÏ¸s±üD³û2§7ìñ;Q.ë‹¥G2ihÛÑaôeö³ôÌíÜ!­9¥¸¨Â¥•A+ˆv­ßÞ!çv±lç`Ëþýgtæ§{#rv·¬êÃñ›Ò~ôÀQÏè(ºr 畈N»€ Ô {i?÷Ø\ŽðÕùPŸÓ¸_0F)fa-ŒD“÷i æK«@ä®ÿàTbü`ïl-×ÄèIN*)'9R3Ùß/šsrÞ8âøìE\¦Pjó—£Û¼KK?¶Ä- ‘½ûƒó _R=„« _™¹û›á¾Hú™~ÆÙ«ˆ–Èè‚ùXêgÔÜ8Ý7ƒ',Ý/£1åK±Ä•¥Sâ‚\NÈ9¢‹¬Vvü¥Ç^º]ü¯èU2­Cg¸Ò s±W;ƒ½ê¶ÛŽ½Ý´·¡½½a/n^øù Ò0Ãó%-‹öûŠ?‹:Î^(¸ú<Ùû#Z%v(½ JWÂX¦ 3ó9 €¢\}bÉ„ËÎF®óvìŽéØÓY9•Ž\θ¾„™f ³ì¸r´Nˆ”iL–ú}Zì8¼)Y×0ÈÁŽJu¸¡×Ÿ{ VG¢Ùãm0f´¼Öá÷;ŒNmÌѼ›Ê‚>uœS·¨Ù Çm£] ÁZß®®ËÆ£}]ϗަ£c¯|ȹ\–Ì8Ù#LˆI3kHÔ÷‘<3E$`ò£ønÎú»G’º'çt]„Ü«.Ua‰¯ì- S¹JkÓYÁÕØYmk+¤|îŒ)ïŸÚLðF‹[k #@W¦ýÑ®™6÷ôòÌäØÒ(ß”tÕ¯ugѱKâk >o®¥jÙÉÞÀø˜Æšs'‹Ï.þ*~ŽÖ’´|¼ö€4ïðPÁJê’ƒºøÁ‹ušªPÀC”lÙ2šHŒniÁai³LÎ9cù ÁHìÔ#Ȧ¨s DbƒN®Í¯;g`àœuyªd¯+Ôr··#×>„¦¯kë.¯´M*ð°›ý-Û̪1¨ZAl(%Œ.Kï"ëÀèäÒm7‘F†4ã—žq)ñÅø3 ŠFø–¯²§¬‚PV¶ kTŠ(‡”¢ñ÷—ŽÓhÛKµôî’K·â< üZ~µÞª\ÄŽUÄ"âQ~µü™—<^ú“츥+¤Ò娈Í„â¥?ÑRMJïbº¹žÑ³FôuLâ äŠr¥( / Û¥äÐ!Å·é}ØlN¹Kw¬ì/F~¾†ß‡áˆ…!02¯]!½¤_qL·SÉ•.T~Þd è}ô.É=\DügFÁO‘ 8_üu‰è2oc ¬Þ'¹›eÿíä*/Ê MtLUÛaŸûsDtÑV„õeU[Ž!wéP4Š.,½5F[´eP\Ù_…W#ËÛ*ŠWÉxU½؃¡)ÀuÓVaÑ¿§ŒWçùt¹¹ÇJo¢‹¢ÑÒEÈÃ̵;…¶;µ­£ªê°.•úAuc¤7P¨q¨\N{ay9.'òŸ²7yu·ª§ÌeêX€«rŸ™ý#ã`·¢×é³:Éú¬ãfAIÜÌìÖ'—­+]·ìIQhݲ’ìõö={Ú_/Īðó¬žÍBN9€XM!–ÑÝ-RÓJ1ôAÀ‡2sP&?=/ÿæ'ÆÖ!W¹ôÒÛëÆ¼‚u9‚òYhO\\ØÿͼÝ»÷Ì…™B-Ì4ëe[Oèc ¿¶;î^›ošÆáôt¨gM.·¦'T 1™!¶] y‰5çyÔµçy¨&Å Ì©áÜ8 Š[*aň5*4$.ÙX,n\"VÂ¥’—¾Ì6¶žM/â‰`=¿ =_j&þ˜lí!û2Ñ R™TD¹êšÀoôçÑoJ;Poé++ bGË«s”C‰.ðe£–ô%a®ŠæXô•$Ïat__é:0sxç ô¹®Òê%­ˆhؘ¶›Ý,Ù½¦í¦|å ²´ YJ¿Âïö0'³;0Øj! ¾e¨„žwdïœ,Co”¼2 ;ýîÃü%à/J÷½¾Ÿ~5ô{\^?³›’:jqˆË{¶ŒÇ*7ý2l 7ú>äǰU9¨˜îåNŠ)¥˜ŠÆª)” y÷Ýr9íÞ]ÆÎÔ²pBO ïÀGè ™¬äeXé«õ¤€?a÷¿N¿zä÷ðNjf!5³-X{ÐzU\!°õØ7€l7Fp`©úRÅÿ¶l¤œbBŠ©h¬ÊÉÍ|ìÝrö}Ÿ`B%œù÷µ›NÆn=v;m74ûä0ú‘j7xìvv«T;¸Äel’$ä¼f C‡ÖñìlR‚‘d™Y¦¸¾/î[_¬„¡ö„;–¥á2ô~I!‹ô®+ÖõF*áRl’ŒPBéEf5ëgû kc²FÅžeaHs5+ÖÖ¦éÞp¸wº©†:1<¡Jh+{³Ñpu¨g]±°®'a¡á=Ý#ŠÝi;öì&ö¤í“/å°'ý`#iw¤|&†}köÏв÷HT¨†ÙåèÔ ©]SJTÈ=Êö{Í–P»§çØ£­¬ƒ¡ù~K1&ÆËŒ-ðS©[àa»*Á‹¼TÆ);ªÔP…yhzì~ŸjöX¡JQ­c£ò Woz}°Á"sO6”6¢Ùüt tö(zÁÙìž6GÌ={–^ÔßQÜײõ +Žý ½=ùÒ¹ÐÅ%žqST8öä(ôÖßÍþ öÏcx2ò tä×Ah(óÀi® vd¬ç”Ÿ¨÷G’ŽõÑ›Ì98µ17ݸyí˜Ç_64îè<¿kúá[/mËžsËç÷Ý­·Ùû4à[`°­v:»RYu/‚«ÖŒÔÿ/o!K$bZËhQÜÇG½Ü ,>Ÿ¬Î`3³n×Ô R\1ÇWïœÙ¯”³2µNoÔÙ2Ù¼'¾ÜaâNk2ìQ—nÚ|2ÔõÏÍ.ö5b‡›HΨìÉ 1ó<Ú³ŒRÜ[a V'(!a³Eô˜…XÑguÛ][WVÝÛZú]û±¿m9¹*a0Tz@ÃÀÚl ûÆÉD˜²Î…)sÅ! ežè±­ùrZà¡fÎèµáâs£¼ŒÍùŠ1ÁÖ{†vÃ5v²Ã9GS“#7œ-ì

‚«KH±V{†é¢v¦ŠD”,iˆ)´L¥P¿$¡áùMÁæ„[­à‡"­Ã‚Øè²Ý2§·^Žë‚+!¯÷:eî ›Îj‹ ñ µ;Ñl‘Û’ý¹Èx_¨4ª52>.ôΔ×dò¦œú€çeµ1XõGrýI›¼sÁÆgØ bá2ÅVüá(rˆ —¾?(­E;dhgé&™„Å ¯žÊªQj"èóJ¾œCµè+°XOB¯Ø²ÌÙúÙšÓûËZÄ^aBÄÞÔØÌ•+ÌŒ¥zÅJÕ•³ÿǸØõìÙÔƒrHVÂ:rÅq,Ré6q4Ép2)QB®¢tC! a+„c®‚pÂé#'ž Äã¸LTÎû°.1>/Î[r4DIp)œJ-ßßßw*OíËMá-–© Ýv·Û×å4ÜÙ»\•Ñ´ˆb‹&£Z~±^¯T¯×¹RË÷õöîƒhØè\•ÇnÓ«›òàÙ÷·Ÿ/R}=ÝÛböÁ?sKïëé?†QÀ°Ì²ÃìV†ìÒÉÚS|&k<Œa_­„:¨QÿV ì†pé"8ãª$d¹ª¸#’N"ÂFÌñ0‰’];GãñÑ]4¼ÎîtÚáú* [Æõ}["ÑÁ¸KUW§‚ëfã£Û;;·Ã—4DшÇ©¾^ìj7{ý~¯¹½k¶N­ÖhÔêºJˆÇÙ·½–½0S\T£³ÆÅðѲŸi£dý?LËkI @™00>Þuå–Ä»¦L2ÓTW|IÎíRÙÞuýÙÁ¬­Øß_´A¤]o¶±Úkö(Œ€KaðÐZmLôýLŠ¡vV¨]È™ 팙„MÌ '˜dyÕ$í’Q³ÉTçºý¥TfvÊjÂ0{ ðœœí¥ÂQ¶iµ•·?voŽÞpéþÿ0· ù ‘ð®®Â{¬=j.¶œ²sÇ~°žMteüZÃöåË·´þt×õÎa=ÉÚxØk³ò¶lÂìR©u:µ <Òm¹qû–K‡tËFbKœÅ\[6g 58³K–­^sÁDäÞcJ7åS&¯YšÏ™ínïý60Ö¬—+ujÑ®QªÕj•‚‘;ÇèÙíìLœi tctξ&¥é2ö $”-HcÏ0ƒÄ¦<\™r¼åH9솰;Sî÷Ô% ýí” 1°ÒßÊÑl?tw¡ŒÓòV•0ï%WXN5˜M] jÅ‹†4š†.“¹!^zödËryémùòÍ^¶Þd2²^6àóùõÂRÇTw~Ÿ/ ×ú›SÙV­>á/´©ÕVÞî®W×êÅq‹ Q·ü ½¶5›jök7å;:ò/²·;Àö‹=R ÷³A?»ô³0“‡~6tvõ\?£ò"ÁÙJ]ÓÒCžuÒ-Üg˜Njk‡ý4…ppÙññ#åí[‚º‚Ô+AìJ(nUo¬vÖûaHTΣ!¯öòᦳκIpð½Á«76LuÎÞ»÷ì@çŠÆu ýñxƒsãï}§õ¬ã4pï¼;£A0ðçZ¹’?‰ª¶À?U4rˆ_¹²ÏÓµ©wÿöíû{7uyî‰b¢Ù,vÄ ñÖgÌ f¥Ø_ôõ¥RS¯g8Æ9û,ÛÁ­aê?ôÊ<µ+RVþYrî‰K”µGËšŠEz:SaQŽÖ–Ï Àïƒ@mž¡W.Gã}Ñhú¤éé“Ò Å{;f‰ Ð dœŽô ¡H$4àcõ¾kf¶n¹Æwì÷¾+Íæ{5G “ 8"ä¿0“ÏÒi›ÅÙomi$AÉš´M²=a£žx³Ð¾hÁÜÉ‘½­lUÙ²RÒ›ð|¶¶óØÅ_ÞzÅ©‘&w¸}sóúþ°»i$/Ú­Ñ¢ß_ÞHñû‹Qkéª=Û‡áXÖx*·¶'îY›Kw%TÃÛ÷àuλ³ï³g±W2.&{|ª"]²YÍ/–€B‹”MÐ G•N ¤³¼ uÒð&¾=^ûÔðd^¾y³Ú´|xÂåÝcpë'‡'ÌêÍ›åzÕòWM`0Ñ™ò›Íþ”Ó™ö›>±¢^½w¯Ú09¼\ï©ßáqN O!E®UM »=;ð÷å=­?³zö&FÜI¾ÖƒtMV´(Ù9“„H¸È5¢$üîþûϾÿþNY øùÁ?uh-/=Vúܹ×Ï<8³^ÒÒÇ¥êþîR©B—Jabž|õ¬WKÛ‘o¤_Ö?‚v Lé¿K7oyûÀþýfg+š¬BÆ“}n굘Ëýäžú†ûiè×Xp4Æõ0ÆžU’äÊL-Òæ åõZ6•ÊF³Vkv”uŒýþà8²ˆÅ@ (òðÕã9RŽÆj~ZõBŠòø2 ‡Î¡ÙpÖ¬ƒå˹YÉXêøXêèùç,u|cyÙRGSÐtt›l·¿>[D<ÉZ‰|×Á,Ò ¨Ü EçD¿òÞCúCWBõ‡~ücÔþãc,Þ‹Úióeð^GaünåÍiæMôiô­ùVĨdQ#øP拎òú€„.óE_R‚ôæn•§>òf¬¹¸s"Ñì×)äKê Ðªç³ €ó ´ú9¤•_ƒûŸûóÉý™pÿû ¸¿ˆÞ÷Àý_áþ&Ò ^GÑÙìÛ äàf.ÇÍzoöÂÃÝ¥O›ìrôû(|wim9 áÀù‚öåÙÌæ…ùBw~Ý;í…‡4ßSØô.û|w1Ég/Û†ÀÜf+÷¶¬‘ýî¯ ÷²Íè1N ÷דû‡`ÄY9ܲÜëÙN€û«ÈýgèãŒî#÷ßœu£‡îµ0ý¡ˆÆn€IÃúdx‘ Ì.8qtì¾¾µ7Iȯ€ØÅ$v*Äv“Qš˜'K+ëFØè±% ƒ3Ô”IÉ Øšu7Óá „=.ƒp7„k!\ _­ _m¤©'+SŽ8ÄO…øùGæ›l=ÎR)\•쯊ÿ3éÂ\¼ºT”0ÚlF³ÍvžÑn7í¶çêíöz“½ô mZot8àP: ¿a² è:òªÃVÚRóÎKós¹f®¬²%ï¬DÊajR°X„êk¯Oá™P ÷Ú,À Y-6ÁBìÞ “Vx`…$×N›¿jÁü  öã¨ñ©­·° ·o&·naD×q,öÞÄ3c°Øx~ïq66söÅÞcÆ®ö³‰ïÛŽÂ{t<.Zndè–­lÃÐÓ›ñ{ß‚r¿ºx¹ÊžGÆØG®Úˆßû›Fr5ï™´fKð6m@sýtKIù,ÕaéºÕ\Þ‰ >Ú¼çÆ=ÍçËØ†¦K.išÁÃ…}}ŸëûÈ9|½{úÎéî2öQqß>qŠAÂXÉ^±‘æ`$9”sRC(§Ú(Ì?.ÔÀ%äö[ØksÜ ÛÝJânDÙ‹ÍâpËÇF ,B@v÷Ù'Ñï¹)FÊ” >ZUUeò‹”IiüéΨϡÕ[¸²|Uœ}Òd5YÊ¥ýåpt ƒ*Ôp!U… ¦›Q”]¾~=öNÎþ7ú—Yäð±Îþ÷† ‹PUú Ü_F¨7¥²x®’ö²‹´v:ZI,¥çP2ÑçÑ”< ÿäSˆu±í¶D‹_ïËd|zK†ó½òý!œÖÕ‘|•$_jhòÕ|«¥](¹z{kóùl¬`ÛWŒ*âÉd\1ºû-a¿¼Áò…¹Q;u‹ä6:¡\®Œq&Ø/ ôé½Á Wß7€=¨Cn¯q}O³}q :ÅÒ#8êc‰÷²_¼Gá à'4ã:ÓˆÑÌ;Ù‡æ­)Åb.¶3‹år±X¾ûûeô1në‡~mlŒÂÅ~9 §ÓáHša+3(=s‚O+hßœóÑk’ÝÕJɾL¥‚p}‘†l[:€O›Ò•RÎ`¿„ÞæÆh)¾¿§¬bì‹Ç¿—âì—â^œäWB†à }‚³Ó6A´ D- ©ŽÖzÀ@Z³#”TÃÜ,Drls«ÌPoâ¹idö‚vØ®'¹>®âÎb 'ÎÕj­ÊÕ¸~©ò|¢U¦ÕëÜ0Û%9²Ìè×op“§›ñ.Ö{¨¢¹íhM/ç5hüǰKå…tª(_ÊÚR½I_2éƒ‹Ž¦PSSˆŒ¦‹£~<î÷G¶Â‘¶À5ÒÒÒµôT‘bÁ©¢ù&R{é?“Ýn‚‹mîHÄ;:≎¢ÓÊ;¼Õ‰ÛûYwpüƒe人rø"“3”ñ…BÙ€[¡h+O»6\åÝ07N{.+õ)=ó¥8BfA€ë/ck6ÂÅæƒO%?¼û4ëåJy° ó¡y¼;|ò†ž¡­اw5] ƒ*<²4#QÝ#:Q_y4,kßÀÚùÍ{ؘÑfš.aȬö˜Õ¦>rGñ”¶£{šýÊ)Ñ©©è)Ä—9B§pIw^âÍ•Ê÷—°6xlÚüÆR/›GŸãt¹ÖŸÝ³yÍÉp±ùÏ$Ÿ Â_´°O£Ïÿ˜{✠[O?gÃÉìÓW7Ý€‹Ìy˜7^§ãÕ@)¾D”¾›Åy¦³~@¦ŠDÕäÁæIÿ.ÏÙ,CèüÓØZ#¥ó°’$¹jh®J’kµ±´q•H=û4ÐwLéì€mDgs<@h^Ћ¥\ÞQ® ÉB$ÕÖnõ‡du*{$Â66¦ÒKŒYÃ"÷y=8Ç—`ô}ŒÒ¨ù܇ÏBGkýý¬vå"‚d„êdÈaf›Y½=ìpÂ~>šæxS½A†×O癜 b©©LU™£åúR7Î’¢ fʺ ©ôÀìl…’  ™á¿WšEw̾Hl‹ÕØõ 9óÌ©Òlï`ïKðäYxû’ê·©Ýk3=òõìÔ™g–f_‚·IÞ0ã]Í~Þq,Ðú0TÖÔ´ õìy–íÄŽP=‚^gd ‹û8ª¸ÂÑxÂuн³§¡=Ò¦SF·&SßàŽ¶ˆ<ß–>@ûg_fts:I—TYÎMq&ÉöO/]:òùB¥Û’É´pÏ; —GgèRãjosPÉ ¢#X ß]9µò§ƒ;v â¯K³ðõgÉH¢gš¤},µHQØCìÊÒÊÒìÎÒ·/Ò“¯•–©9r'µÍJ_IÚ þüÛ àktÒÒ­ƒƒxm<«A‡gŸc•xmüÁ+xõ=kG¿œ}RCÊkx}=‹Ðc³G åzHÁö=È šà—¯ÊÚ‹bX˜çSä[š¶¤'àZÞ±ý·mœÙ¨7qe4œkÀýä蓟bÿ@r¢-5OãÍ÷n2 ™l Ù±Í$®3™ G¦9ÏbjŒ¾àaXžÜqI]dN)éM7®‚m< ¨…7ðŠCê€ ý·çNe€WmîEɯè“W;‰Èg{êmNÍ ·ßKb¥×ÒýVC£-æ·+…ŒÃΫå=F3”ñ_PÆýÿh­ ýF(cÚís5fUÐÒöVCÆØUBÖf1kä#õ<”ñ^éhûìsµç­aáöžb—¢ô‡W^Áõ}h6„>7û8¥&¬DMl%äBŠ¥§=|xpH¬f}è-°K©Ä«þË€fmè{³ß€”+ å(IÑ@ÊÓr;¤|Ë•f=èÝÙoBÊÅò^óÌ.AÏÍÞ )—BÊŸ°Æ.s?«GKž¬áê)宯Z7*!”SJn–Nn.X6ÞiÏ%­°Täšs‘ˆºpô~X,6™—ôâÀ´¤×û@™SÊDùNiÜTK[q—ù&J@œJ¥"¦HS'‚»8àî™Eä•òŠq£ò ÏÁõÐÒS–~Ј݆—jøò+d~7WiÙ³d–¦Zù|°ÿì)§üå©§ #X„˜ýõì“Ðkºè™.~ž¿Äòž ƒñ…­ÏSÛ <µØ£Ï”gBñ¢‘6­­J%a¥“¢_]ÎpŠj™:êhoF+ÌfKý€Ñb2ÿ©îÓ§Ó16Q—ñzmV«Íëeð|ÄÐì,ŒHË .0Ã*«Žçÿ²¡««¡§‡ü²…dsK²XL¶4'‹5ÒGÈ÷³QôéÙ f¡Å°+“j$cXùœÂB†J¹3lîlØÅòõ2…R¯qÿ°¿- ±*-:v\®ÀûÿÙÙ>.:{WÍÁFáŽpýò† ø­-ìû'N PØé P>P¥£T€M®Âñ¤9¼ÿr ²ýƒßÙiسwW&³kïv¤¯«££«Ï±ÖÕY# ”yˆmC?àN£k¶ ´j{ú’íïZ½‰€Þ¯:ƒA'\•ðP38œo :¡Ã¬„‡C—5á²/‚õâ¹÷þEeÙà2¿Ë†øÅ]gØ/IåÒð|ñÚ\ö¶ýŽëÄô{AÙ:H©”]O˶.(÷£bÕý½ö`ÐW[dDzsiœò;±w§Ÿû›ñúÊÍ¿çWo]çg6]öïÇùuçÍl\ç¶\‹ËÞÇÁg±œëí‰qåUãüÁïl~ÿÆáíÁImpÏyÇáð¿Çå*7ÎI½ÓLÛñ¤4ôÁˆrÃ{ŽCZÂćác1ùáÂÆYb ¹ì:½IÞ-oŒ@ܦw©!ZÛ^×èëõ&^ÙÜ ¡¨,¶ÌÕÚvâ ê€á¶=¼ÔvÈBðàh´–/,Y"õœGÿ…¥/ì=¥ÿEéÚÞó\:­ûNRº—ÉŸ°ßbÍxBÖÕHqXËÑõöBl|}&»U¶5;S‹‘ûg’==É +oý[áZˆ§fÚ6È6´-BÝnÝ&‹Ø²É'ÙÛÃňí°0@ÅIšËªy²t<‘Ï“¥ãiò›™õg¬Ïl“=Þ<>ư£G™¼0ù^âwá?˜ï«mÓL·ÍÈnMnmYýÖ­âÛ¸ªò5Ãìý^± ÞÏî[Q÷@Ó…ovþë‚W¬‚÷Æ“³'Uà]Ö¶šâ7Î5B¾ÓP•¯zñ|éñf±W…“ùëØJ5®ªÓ×;#úɳ7N«£k”iôúz®“‹m~«–¡mã> 0ðÿ ó×f•\£79#ºÉž>Zoe†Óè€v6åOŠ¸Åˆ4«§ªtÛ‚Ò•Ré6jYE½ÄP;”„”0²7]AK;ï±óAÕ§HG梀 ©©¦æ ºz]DÙR Å´ý7þýJXëä}kC1Àeãå5$ œ™»¦”GT-DuB²§ÂÁ¡ƒÇ‘«©4ÕR+'Æ]”*’ðO1ŠEgîõ«ʆ/L~¢J¤û#ÿÃè Ìà¢û-帞ö’ù»%åAŒ6¤J‡ÃiÕr¢uhã Ù5Á&y@ª¶'seË¿­fMg¬^_S³³{>ño©ÙÌé3çÕì–+¥š„RüLâ53Ãe‡Ë±H çýE*;¦Ö›Ü¢~EŸÙW#ê??Çj19XÎ5%l^+ÕÿUF‡m7ž þ¸x¸¬5x¨kJvkõf¦n“7^š½”8tp)¿ÕËkp{\xzž[Âñ)©ªÓ¯ÆTƒ¯Äªò`§6Ô1ÿªÝa[lø¡¤Ø¶°Còp4–±–p³™ˆQEcœD­jˆ²í&_TÐZ|>‹VˆúL‡ µh-@ÆDƒe®ý ÜK€Å ZI¡ö0úlz n¦–£xÍõåÖö©Áê¢@.U *"ñx‚¥†ÅIÇW:rZ«×kÕæ:®Å„C¢yŒã;Ù&ô ×Ë(‰.Ð-ju¢šZÍÃÞw›&ϘlZ%cýi¯IVPaµ}UAfò¦Ù&ÿÚµþŽ gÈb4;f£%ä4`o±@E¿ Á³ O%— Éz6›WçW›W_°ºyFÆŽŒBý±X‚Ñ2-ZßÖ¨µx<mc®Ù{æNÁrˆšý7ÝI»Õx”®öc ×´ãíƒí;G–îTÿö#} ûÝO7ÜÖŽ÷¼OíÝ=Á°åاÓ0f¦‰)Ë,tÒù"ÉÕΓ´u U~²ªfP– UöèvïÉ^&»,»‡ýJesîüK’'”¼J>úÚkÜ7HÉ`Ö © I§h—y"áƒkÿæÌ!Ù¡Ìf®!Z6Ï=}[vj*» ãôt6…^æì«™ Jg×0àÔ¾\ÆŠ–/žßÒ\•dU€r~eMù±IeÄÇëÛÙ.Î`;Sv’5êõ\ûQá{PÒN(ék´$qñ’¤• BSmiP³7«J»xà€¿¦´¥ßŽº’º²J 3EZZyV÷Ô”j#*Tt§#6|„J‘çv_æË`y¬¢ñìC°%¦‹8ëõÏS*B ²9y$l°Õ×±mœF­R±Rö£¬»3¯H˜©ÙW^3ó)0FÍ-Z=ï‹('Çø`âG“³Ø®VN_o0f )gØnàë -ïëÿÚò¾1pÀW[nŠª¶à)µ+”wb¹©©ŠÆ{èܸ>*çû!ßV‹ˆ^®Ö×c6µw ûýù à=V¥Rk¸¶ÿŸ³ë‡mÛzÂ<ÒŽ¬Ÿ_ n‡ Ü Íw"‚ Ew_Õ%S'k‰2u,BîÈ–‰K× —Léž©îg¡–x,PÁ¢{wïø==RQG¡½Ü÷ÝŸwwïh2ùòÞƒ¯"º~UñkštÎÙrg8ŽßWy©TÁReëN™‡ƒÃË´__²TTÕ§4If¡”¾9¶ÊëÓíó—"U°TÙ•b.&2±$:d»žˆ]ºGùÚÛ¶ô=+-Åqgéûž| {hýöKý»ü¯rý)wÁÜ¥q?‚0³/‡vYýo.ᯮ?ç8|oüw£fï“l9ÅGl»œlØÿê¿9Dø)Œ3–›1²md×’a]Z–rŒã;Ñ·Ÿe¿ƒ'Ý ê¸è¯Óznÿ«Üþ©Ö‚µ–Ðú âÿ©:1¸–uš÷þbÝUýÌÐýHžd‚ù…‚?îà’[ø7YºŸâŸ/.ês»þ¼¸`†¨`=åõÐÒÕò££NëNkxO²·Dê3½â×õyNgÕ"K(‚tÉr»x^LK¶X¼KŽ­VW3³¬HÏØ¢9˜‹.s[V™µ£H¯˜ce&–~|H“¼>§3æ›æ^Ú3ÇÊLÊìeSÚ7ÙÌ[xZ'±‡Ì\Ö—3šÌ%˜¹†³D²ÆUÒ+“Ùª8§»“œŽª„Y¤Ò…I#mÙ"9^¼ÓÈÌ ¾ç{"Y-0SÚ\eÞ„'ä@$•´¾t½&ˬ°÷1 Ó¤>¡£¼Êã©Ê" VXë%™wªG«ñ[‚—*<4C7é<ÅÚG‘Ç3rˆlmÛ=ðv3RToä3#%+4ãgp¹m3úhßòµÂ×Aê°4—ÒV—×x¼£CoÚhfø†þB³!á®àl¦ ¼Å;Ë›¹A‹{t$L“_‘E·ûa„y uë¢\Ÿ8ôH³1Ä£öBet1;ÖçŠ/F¢^úð¡ÇÞîAhwae°™Ï© Ë13—[6ã~+½¤¿4É}eÇœ9¥Y>jcqwZc¹=^.f6WëËM| ì-–+^pvGÜng-¿ëîLE5až´pknÃ{°±MV{VTÕJ–ã²ìª/>ß?n‹KvŒ>Xî‚åc~ Vr¥7’ÅZÙ4=¤Í¹ÐE0ßΪr™]ÊÕ2Ësé¹w8v{¸G2r\ÂfiLåÈœœùey›w öqê®I4µ±.Qó”ÕòY}+縵O'”~¦}Ô>dï]5õ#1!ÝFqzƒ|kgb·9zv;CÚ\kk,+rcóŠ\_7ªÇ®5­ðÄÑÜoÛ)ÄkN ü M_Õß½Òý·â Æ—¸õY‚?+ph쪙¡ºþ‰&ñ¥2´N®p2CÈ‚eñ«üŒS0¦4Œ®Ít$–°áL¹¿yöžÛÙ¼ßý/ï¦Ýÿë)Üâ^üž&YVÈ2Ö=®OižÌþÓc2Z?ËÜÑš\8` úÐËN$Âî~À;^:®÷“ÜíCÇÊ0Y¹ï©öûJpŸõçYü´E±l‡réP±áç²3„ xº‰ZƒêW"YMFG4¹ºNòúY½ï÷ð¥oèÎD?¶«p't™åX×ÏáËNðÖžmø"í[|)â§ê‹ô[ÙWƇ/Ý'ûÂRšgõ ³|È ¶å4¡ìê9£§c[5„o:°¿$ÀÛ>äSå –AáXì;O¢qTaÕÞO¯£¢hýŒQ豂¸’ˈ˜nó‘x¡XR8¯ŠÅیθz¦lAý!áx¸µÞ˜&ðÂÐaÑÑÀ9„ú‘‹IÔœ\Ì¿fŦš/Þ «r7 ‘«.E¯óc¨ ¤Tõy‘Ñpñ–Î2š°WÏýY™DÄxv?vP%6 8q°BÙ~!£ÒdºxßVœõ1T2B<s ‹,+\ÆS;`”Ç7O޾ƒ'¿ô®¡ŽŽ«_©G˜‘ß}·3ÑòUƒÝ» “]6™ß´„E²=4+kçr±!:ôlÏ{&ÒñrE¨l2fYá½csæ/Ù.g“u×ÝàoRy]ÿèz@»ìÀV Üim{È4R‡¢­Ë¤;ôqîCv_CÐ%¬ç¼ñB£Èu,a›óøa`]?°nϘÿoV*¹öµTGöD"Ð+ŠØ`«Û?hpμª–ÈÖÞÄ:°{²GÇÌSG†]Tgu¾õíVP›n‡"ÕYXyò%1ð}ÆbàÏÆðFʧKFÊ:Ï¡²‹­Ò}êI1kA®xƒðPŸ©7 r`'X_ &¼ÅzG¿ Äú_?A£PëVRðnýæj/œOìýV=û}ÇfìÏ À&“4ˆH&S’j Ú$ž®"ñw°>¢þ-[öÖúÏÚ^Ê6i:ói·ñÛ» p·,åiúhÅ@Ó÷¥ÌÌÜ?33oefühÌÌÌÌÌ[¹cæÍ~“ì;Çy¶3æ|Í¥§ó/t‰/~’-Ù_>G4‚óÄ;wʈY"‡,â$H%{{í·`ìС]¦ôÓ°jÌÂýz-‰÷ŸÒÅ8ÊÞ›” Ço( œG)fÖJ§•a–¡C™›¥°´%½ö[èð}¹v%Šæ2?§d¢µ%ôK í]㨱 ¤¸JùûsdÔ³wÚÆÍ,[:‰½R æ\e%ÕX¼òôåÌhó”ÿ[z†¢˜ŠbL4£9fm lóï¹± cŒÆ°zŒ ÖæýðÞfÂ;Ñ|×x6C.ȳ±žº Ó´WŒIE"¶ŠDEƒmÛÆ¨}yï1nÆ{ðžÍÆ@óHµz~Qa¼™£ÂÖúxÄ{Óæó^ ¬aŒO|9M’öf%ÉÏÛ;rlŠR1 ï(4ÞlG¡/Ñ w¾z‚Xj©èŒK£ö:eÉX’O¬š…e£ã.¯°ºÆQþ,ŸV®Ô®|nQåq»Ùr¸ qÍV>º˜ü¡¿ó (£üê 2rÕø!Ô[APvŲTdèÐúEçv¦¦ymÿrdŒ£®¹&‹yÚS)5×8ßFÁé>(ÑPÿÂFa?”S+|Ã¥úãz%–ø •?ì±QZO?Ôc?¤Êoµ¬ …jèj˜ƒÂû=Qx.êDoÔ‰¹¨Þ¨9(ØîJ•l·@³Á³!3Ú3Za°ƒçJ¡©QV§v5ïQ®]J¨2ˆAgè ÃÍüª‡Â‰p.¬„­p Ü÷Àð ì„Oà c›c{Lã`³ð@<ÏÄ¥¸/Àkð|_ÀwðüšÃ­øUgkvþµ­z–èâ[ñ=úÿ-Þ#Õ­dœ×§,`þs|6ZTåö¨«ƒ=ի׈ۣ®¾ûGòø´yŒñhSù¸G›YÀçÅÓ~ú¦1F´Ón/ì§Ì_†Á»›—•5çö¨«ƒýòì ;þH¾Ÿ61m¬ß>RÀçÅÓ~ú¸ÑK Õ^*ì§Ì_†Á 8K<·G]žgoxíä+ðióãÑùí#|^< €Ö“ýžž$­§éI }|)òŸŒvz!Ø•àíóP>"î²'ÉÏ¡pLúxPjùÁL{‘|6Ž‘(‘Y߆>„ñBáåÝä…7åZœ‹«B0›œ„ØøÝs&!y'j)2äù{‘MÓ‡OrëfêE-èâ`›ÅÄÁsz®³C!_+ÊK~[QÒ’Yx ~×Þe[\Z¥V¡•~)3J­jjT8 Œ¢U6¢ÇÂ/8®*ì=ŠiU³ŽèÑL«ÚiD³hIgZUú"z‚ã8Ó™Võ°ˆžÄ´ªæDôd¦U#¢§0­ª=•iU‡èiL«ª/DOgZÕ(!:`®9ˆAâ ¨ ø[[˜W«¾bQíša{ Šª‘L ïþø¶mŸ ÈôoŸHt–Â;¦ˆmfUÛš™¢LÛAˆ@_°ÎàµpgYÞWÞ±‚0ÍNãŽ/Efm®0„,½ÌH£V’Фø¡‘˜+‹<û©ÏÖ, $Î^» À[q›±è ý||Û8ÿÍsŒV8çÕùî³h]2b9š’ãÝ`ý0³Zeq´ö,éÝØ«ŒÓ­!¦qàD|ßh2Qtyu‚ˆÀýJ)}¶-]Héu…˜­_(fÓÁubÐ {i†ƒüg"–!Š2ýYÑe¢ÑßULS&è?ê?ÂO¬ýÓc¬ø*j óxÚ$ÁÀó:ÀÙ[dIó­ÝÚôñ·mÛ¶mÛ¶mÛ¶mÛ¶­;Û…lËlŸìYììK^GNG;ÇjÇ;'v¶unv¹]]Ë]ßÝ Ý£Ý‡=nOϯݛÖ;߇|5}gýiüýügý7üÉÍ7‚i‚Ý‚/BõC+Ã(Ü ¼&ü8R2²8ò•KÉeáòs¥¸ª\®5×È}âÝ|¾?˜ÇÏä—ðëù}üIþ Ÿ%Ø…ÚB3¡£ðPx+ÚÄ D]üOL/æ‹‹•ÄZâzq—xT¼ ÞŸ‰%—ÄI­¥îÒ i¬4CZ,­“vJ'¤ËÒ=é¥ôEöÊ¢Lå¿äÔòRyƒ¼[>._–ï+¹•bJE¥ŽÒ\é¤ôUF(“•À 8€ R€, ( ªƒF è F‚à(¸nƒçàtCÈ`J˜„e`uض…=à`8΄KAJ…²¢¨4ª†š¢ÓèzˆÞ ïÑ`DgDc°Š-œ gÃEqE\7Çp_<OÆóð*¢‹¤"YIAR–L"sÉ ²™ì#'ÉrŸ¼"_UŸZ_m¥vU¨£Õéê"õ3õPªÔ¢©ivZˆ–¥5hcÚŽö¤Cèx:‹.£ézœ^¢wé ú%É—T7i^Ì«›{£¹4NÚ©¥Ð2kù´’Zí‘öV Ñ§ë‹ôµúý°~I¿«¿Ð?Ç=q%¾=~(~6Q)Q/Ñ2Ñ%Ñ?1*1ÕÐŒÿŒ Fnc›qÐ8c\7oM›4˜£Íéæ"s­¹ÃÇ*'uŽv~uutrWq¯vovïs¿uÿðøÌ4`0s•yÀ¼f§±zv»ƒ=Àžf/s¹UÜGî_™¯ÇÏâó„éBHØ›dKºé™àYáyïîÕzWˆuEJ\+nˆ§ÅËâ=ñ™øVü!UªK¤VR™ôCn-ä„RE•CÊY_ã»â¯ê¯ëáŸàŸá÷ûãáñ5p4ð38>˜|j „î…»…†Ç„w‡EšGÚGzE†F&D¶EkEEÛF{DG=ÑóÑë±¶±Ù±¼x×øÒD›Ø¡ÖR'«óqelÃnìÁœŒ³q^ˆ×àíx?>…¯&ÏH©ž²9µEª&uEZí4GÚÑôAélúû "#7c[Æ¡Œs™í2͙řåÁCÀÜ@ÐÚ¶m·—Ú¶mÛ¶1XÇÉ—µ1“Ú¶mÛçºÿ{oQG4MA;Q@gÑ5ôýÅ¥pCÜ wÇýñp</Â[°ˆGðU’—T ­É2‡l'{É ò…f§ éX:Ÿn¢&=HŸÛ²ÛjÚ:ÙÆÛŽÙ+Û[ÙûاÛ7Ø÷ØŸ;ê:‚ŽûŽWŽïNäÊëÚäzãîížà^æFnÓ}ÊýÖýËSÜSÇÓÖsAè#œK‹†T]Ú í”œ’*¤¤´W:&—nH¥WÒg¹¶Ü_Þ ßVr(=]9®6P7ª!õ’VXk®ÍÕ.hôAzZlô0BÆYã-d‡ÐºAa,†5°(ˆ`B8„“fMs…yÈ[Ì;Çõ>öåöõðMñÙ|¯üeücüÌÿ>Ð*°" Ü – Ž ŠÁ½¡Ü¡¾¡5¡·áæa1ü72&ò Ú>jÆZÄvÇ»Çc‰Ž –lŸŒ¦š¥ÌÔÏô¼t2ý6Ó'ãɤ3û2Ç2WÙfcÀRì(»Êž²¯ì/Ä+ñF¼ïÇÇñy|§V!«’ÕÁlm´NX­;VAð@7ôn¶mÛ¶mÛ¶mÛ¶íÕîW¤žmÛ~ï!–kŽ­Æ,<%^ ŸŒŸÃiâ‰ÌDIâqøLþGf'ó“HüBå¥S­©ÎÔ* žÓ¥èvôu¦C1ó…ÍÊÖ`;°ÃÙùì6Vf?pù¸NÜtîqh‰_¥Òr Èyäž²/ÿˆtŠ|®Œº±ññäq*‘916¡*M• ©ˆŠ¢@å‹ZHí¨NR—«—TF¨ºê¨ÔWê'õ—–IË¥ÒJi•´ZZ#­•ÖIë¥ÏÒ×é‡tB×õ[ú;#³QÅgœ4XÃ2|㦙Ҭ`v7›ûMÖ|e´ÚXS-Ìzr€Z ˜„à+,[ÁÉpL@Â(ªú¡¹h7BWÑ=ôÜÎ`W³‡Ú[ì„ýÅIãvÊ8UzNs§ƒ³Â9å(Î=·¬ÛÑíé®t÷¸ª—Ìkâ-ò$ï—_Çßçß ò#,¸¦ k„CÂc῞àlh h¶m»Ù¶mÛ¶mÛ¶mÛn>Í»[ÞmÙõ¿g%„™Â%á½XTì,î?H¤’('– ɵåòqÙUr*#•ƒŠ¥–ÙHíªŽPg«'UKK¡•ÕFhÇ4d%@=ÐŒ‹Àp<.øÓÁ°2l ûÃ)p-< ŸÂw( *Œê .h,Z†Ž¡gˆá$¸0®†;ãÉx3¾€eü‘d$ÅI2„,!É=âê ô^‘7åƒùœ?—ð5| ßÃð3ü ¿ÃŸp‰cîò·üc+L¦ G)£¦Ñòh}t0ºÁèÛO½tj®;+yì_<õ èÛfÕÃÛf¼Íÿ0þ:($ xÚ­”t$ËGïÚ6'Îz&v2Ivg'“µmÛ¶mÛæ³mÛ¶ëÝóN/’=ëí{~]Ÿªª¿éê鸄\ ÊI±‚­L"©WÃT¶SM´Rh®˜˜™~tf ý©¤Ž4ÍÄ©`žÖl&0Ú\í8Š7QFõÄœ‘ï=å.'Ax5Hôd¤"pÍíX¥Dì#bÒ$F©NdŽ‚½#Îߎ0‘íáEü5šÚQp’ BHµ¡¸Ib‡‘<)|CýÛQyÐB1d( NÝ1(TmˆVëŠïÿ 9Ä#*ŽJŒÈ „›HKŽêÊì£à‰Š(nCrÒm W? ƒÔÑèÍ›˜Ï›d$(kGó(jCýIáùˆµ#u~32Æp/MhK)~_R®vRAGëÄ­*"‘Ú¨v¶Œ¤ÅŒGñvFJnxÂ\F¸žð&Écg¥˜¢ð,…ÑsŽ!›””zÞ»ÑÕì4U…ÏA“jC´§_(;œ½‚J’èM¥_ôi¥ÁlÈ­NዌêEïH›)#Å"© ŽrH2‚Jèi6N1õúãÄP³iªô&²¾NJ¨—¡Zõfdx“µáo<ÀE<Ëžæ:®’‹ùƒþ6¶ªý|Nv±Ûüû±ÙAoVÐ)$eZxÙN’QåĆ}èMf,cì>Ç91éÄ êü ŠÉHMxÒš!L#é¬úºJWóŸºÑ‘ÆË£ßeõdŒû¯Ç +ÜQ†Iµ,àMÖøþIÐ"3]+Ÿttfš­¹³’äÊXçÎ¥ÌhÒÕùÍú³eBxš<#3]'F+YÄù6ï®nt ®HÌx>IÒF‹ñ‹§BZˆKLš=¥Ã¨t½&k?µvPçã/¨¸@Ö±™ë™oä"JIq‘ú;¿åL¤V[˜ k>Æ[ø2ä,;8¾‹¥|¬v‡°ž?ó×Ý‚o £H²T¦“°ºS«’¬À:_RFö ÙžÏ#Å{ù ï—£øŸò~3ÿ=Z\ë¾Åù¾ú¿·êG\Çù<ÂÝÇÌ}ž«oð Þg—µîçF¾g7 훹ÎüWøo¢ÀžßÏW­û _´>$¾&³†)ÌÖÅ`;\Îàè­.e ¾wýR ú1¦úF:’2•’êo¤Äÿ@e‘*•”´òÔ|‹ûØËE$©å‹<Æ»É&&=hb25Œp× #écLXI=ÓÙDä‡gÈ8³ŠBÒ”Épeª4JBú„»™DE¸[ž ,<^Q/Ó/œn O†í~Jx(¸¹Ü‡'‹Ûø#_äG²Œwë½YîàkZïvÜÅNíò3~ní?9‡)Æ¿Î̟dzÖ~ÀšMÖ|eåûø8ï°ö*g¾‰«ä­Îþ‡ó¿ÕË÷ø<>·k¿ƒýl5¶M>Gt…†r??$[$ŽÊ;? ÿ9šaüÌB«¸ƒÖ*îW«¢øQì#0ÕD}j‰ã£u•6Ǿ:L x².‚Ž}Ð#ºÚØžETð\åLv£õ4ja áþ×kc~ xÚc`d``¾ñï=SăÿÌ@TÀ•ʯxÚc`fŠ`ÚÃÀÊÀÀÔÅÁÀÀà ¡ãŒ5¢ÜìœÌ,ÌLL, Lß™,˜ ÀÑÅÉŸAAáÿ¦wÿÙ˜o0 &00ÎÉ1~`š¤˜_/XxÚ„•p,K†OwŸIª®ííí¼Ù¼wmÛ¶mÛ¶m#º¶mÛV²“º¶9ûN&[×ø«þsþú{–ß4² §%#@¬{Ñ]¦‘Ô2»Ãî1Ö¶ŒJX „ÚÐzC6fØÌÖ°}ì3Ù]ö½f^ž’çàEx]Þ‚·äíywÞ‡÷çCùd>Ïâ xã‹ù¾žoå;øn¾—Ÿàgù-)b‰„B‰Ô"­È$²Š|¢˜h ‰n¢·è/F‹±b¢KűWœgÅEqY\×ÅMq16À&Ø §à Åå¸7ãv܇gñ*šxŸâKGG)G9GEGeGMGÇ\Gc»ô—ñeRé’idYJV‘Md ÙA“3ä|ùÞ¹×yÚyÛùÔi)OI¥+·Ê¨²¨l*·* ŠªRª“ꥨQj¼šª‚U˜Z¬–ªj•Z§6ªmj§Ks%t%u)—îr»Ò»ê»F¹f¹–¸Îë  ÝO«'Ðë)t§þŸžFÏ¡—ÔéÍx@¼—±À6VÛŒÆ!ã„qÚ¸jÜqÇw'vçutv7t7q7w· LX9°iàÝôÉÒá!áë?rËëõ€‚Œ Ah } Èf5™±ÕÄê$ó°;ì{ÅÞñd6«Š¼>±jÍ;ðž¼ÈGð©|&ŸËƒ‰Õ"bµŽoæÛù.buˆŸáWx¸W$Ä*=±Ê% Ú¬ºÚ¬†«ñb’ËÄZ±OœùŽ`<̈±3NÄé„ËpnÂm¸Oãôà|‚/ˆU bUXUÿÂÊOÆ’I¤’ÿÈÌ2¿¬,kÊæ²­ì/Ç« çNçIçMçcçg›U¥ˆU•™XåRy‰U ÕQuUýÔ@5Îfª«åÄj­Ú ¶øX%ùª"±šH¬Î+n³ŠO¬’ëò «†zS›U*Œ c™±ÎØA¬Ž«+Ä*±Êý…U3wk«;ÄJ _ó‘E³â¼{ÉÛ€dõ¶‡hU„ïä`Å¢ÙÛÛ‡f?o_oÊ=½½í³ÞÞæ,X e!ä©àe6Š`C(5eM¬Ö{ë)€µ–<ÁOs"y´5šdMŒj¬1V˜ÕÕZÑNäÁää¶wRÜI~'üVž´ð"djdÓô˜Q9Ônâc˜þfN3;¥,ffšÙÌ´fÚÿšÊLà©ã©éyïyO©y¼§¡GÑNæI q€æ½ˆ§#Q ‹¨AÍíg·f­[ÝÉ-n6¼•ëVAÊÉo%»ùüæ#j<7sÞ¸ãÙõ1«^¬dhÿ¼‹â7[À‘Ïï°G@KÄ2±‚ö qLœ§ÄqN„Û'{¾o$^ˆW¿È$ñD|@Ž)POè¶×—Œ)ØsÃ/„ù± DzXëb}l‰­±-vÄ®Øâp `{´mŸ¢zœŒSq:†á&<§0ïà#ü ÅÐvh´#Ú í ´SÚš÷À–•µG4ɶ|gQWÙÛãÛá_ί‘}¯Ú/º±¢lg[Úß~cÏO~கdÝ>°³/¥€ß‰Ùÿ!ýÄYñ@¤úŸcz‚ºû÷×¶mÛ¶mÄ©mÛ¶}®Í°¶mg3scëÍSIuPG…è_T5P€ ©°Çw÷*®±ê¤ uö$jã=Ù9¹,º(P]=™P1ÝÔ5]Ñu+Üi„ö¨Š§PuòÁžÖk¢&yOϨnªë#4A{UUô1N¶Ê¡<* "*¡*š¢º"Æ8ŒÇLŸXƒØ„Ïð9¾pâ®â®ã&>á/.¤QtÆc|&b&fgAbae-ÖfÖãq³;{°'{±·ÇsWs ×ñOð*3ª0!j0)Z3Ú0'Ú17Ú2Ú3:³BX ¡,Ž0–D8K ˆE0À2` eÄ Ë„Á–#XcYY“Y3ÙÓØSØÿò4f±f³9[a[cÛb>Û`);a ;b%°–!XÍ vÃ?<‰/Ù_s8¾b_áDDp¢9‘œ‚cœ‰ãœ…Sœ‹œ \‚ó\Œ+\‰\‹[\ÛÜ€;܈»Ü„ûüï¹ßò ¾Ãό¯ŒÁ/Œæ"fÕ- ÇnŽÄ³¸ÅÕ™SÙß°îñ3<àxÈ/ñˆ_á1¿FsfÁË‚Íìþ–«ˆjLŒf̌Ü€Óœ‡‹\Ês¼Æó¼À‹¼Ä˼«¼Á›¼ÅÛ|Á×|÷|gixÏø’¯ø^‘:®3:wsˆ{92nç0ﮋŠÖ Õ%íãâ®Nö©ÿ¿œëó}¡/ñe¾ÂWÇ-]ëë}£oõm¾Ýwø.ßãû|¿õ ~Ô7ùi?fƒl°U´þVÉ*[«n5­»Uµ`dÁÖÃ:Ykkcm­µ·ÖÑB,ÜB-̺Ù>û×ö v@´ƒ2;$Ùa¹Q<‹P|‹T‹RB‹V"‹Qb‹U;ª¤vLÉì¸’Û ;©Ji§”ÊN+µQ;«´vNéì¼ÒÛe°‹Êh—”IûuP‡¥ÓIBKfCæEGæC'æG?VÂPËŠQ¬Š‘¬‚ѬŽ1¬9l¹l‰…l‡ElÅì€eìŒåì‚ìŠu Åz†aÃq#Å©ˆá4Är:ŽrÎp>ÎrNr.q.s9ÞpÞr7Þq>p¾çaüÆXüΣøƒÇX+P“ÉP‹ÉQ›)P‡)Q—©P©QŸiЀiÑéЈéјЄѥХуeГeÑ‹åЛåчЗ10ƒ±…°•±ƒ°ƒ±ƒC°“C±‹Ã°‡£°—£±c°Ÿcq€ãð„ßà)7ã9·â·á%·ãwà5wâ#÷ãÁŒÀOŒ´a6Ô†ÛD›b£m¬·16Î&Ø$›l#m”PmÕT-ÕQwµUi•QyUTY•S…*Lõâ-Ô{õPµS{õToõÒxEh¦f)V¯tGtC/ôTÏ”E#õ«þÒßúVßé{ý õ“~Ö/úM¿ëýé½ö"^ÖËyQ/æÅ½„gò\žÇáòlžÆS{R§~ê¯èY<«¨¡Z{5¯®Fjì5¼¦š¨©×óZ^[ÍÔÜëx]µPK¯ï ½«WôQjå ¼µ·ñ.ÞÜÿc©`(Û(Šö¶Û¶mÛ¶mÛ¶çÛÛ¶mÛæ·Íœ¾deŸ_76ñ ¿ì‹Ã÷•&s!‹¸ü4«¸"4›¸ 4»¸+4‡¸/iNqéi.q…inäa·¢yÅu¡ùÄýGó‹ËM ˆ«@ ŠËE ‰ë@ £ ;-‹rì<´¼ø˜´‚xO+ŠE+‰+H+‹O« -,Ú‰kCÛ‹ëD;ˆkK;ŠG;‰kF;‹ïE» 'Ò£—Ø´7Æ"+ƉkGÇ‹ëH'ˆkO'ŠO@‹ÿš.W€.wƒ.w•.w™®w®×™®×®?„®7Š®·•®WŽ®ŸˆnW‘n7†n?”nŸœn—nÅ6cÜdº]|oºC|jºS\ºK\uº[ü(ºGÜ$ºWÜ6ºO\yºÙ_ÓCâ ÑÃ8Ž8”ÇÉ >NMpgÑç‚Î8‹˜€KÁ\þ•`®â&ã–øˆÞvãîá î7ð øƒ¿ñH¢wéc¤¥$:I«‰iDKt–6‘è*mŠÖìs´ Ú³¯ÓŽè¥ón{KôŒö•è9í‡áÜ ÂHö×t4Æé|Ù ˜¨sd'cºÎ‹…÷ÙÿÑñâÓ >“è/ÑSº Ù/è"‰¾¢K°‰±<ˆ‡•X…øØ,n/Ý*n?Ý&îÝ%î4Ý-î,Ý#¾#݇ýìNô ±»Ð#â»Ñ“8ÏîC/ã »;½-~½#~$½+~8½'~4½/~0} ~}(¾'},î}"î }*î8}&î }.î"Œø±4?Zñ“©?•zñÓi ñ3iLñ³i,ñoÒØâߦqÄ¿KãŠÿ€Æÿ/þšHüç4±ø÷iñóiRñ i2ñ‹ijdf/¥Ù“½Žæ¿™æ¿•æA^övš_üNZEØ»hqñûhT`崙ŧUÄO¢UÅ¡ÕÄ/¡ÕÅŸ¢5ĥģ]ħÃ1BÏ™+v(vŽì!t¦ØÒt–Øt6æ²ËÐ7ÅV o‹­Hß[™¾+¶'}KØ™é2¬a¡kÅ–§ëÄV¢ë±‰ÝnÛ‹nÁvzTlz,Ü›MéY±]è9±Ýéy\`w¥Åö —q…Ý–ÞÛžÞÛ›ÞÛ‡ÞÛ—>Û>Ûßoˆ½œ&F!ötZ8<÷—hW±Ûi7±ÿÑ¡bwÐaâ .v'!.¢ïŠÝEßgéûbwÓÄ9ú±Ø=ôqž~*v/ýL\ ú¹Ø}tž¸˜t±Øýt‰¸Xt©Øt™¸Øt­Øƒt¸8t£ØCt“¸¸t³ØÃt‹¸xt«Ø#t›¸øt§Ø£t—¸t¯ØctŸ¸„ô°ž-{œ—ˆ^—˜^{’^—„^{ŠÞ—”Þ{šÞ—ŒÞ{†Þ—œ>{–>—‚¾{޾—’¾{ž~!.ýVìú¸ÔôG±éOâÒ“¡š¸í´–¸´6ê°wÑâ.ÑFâÐÆâѦâŽÐ–âŽÒVhÍ>Aÿ'™@Æ`À( ß÷çlÛŒ3?k¶mÛ¶mÛ¶moÙÖ¬4¤Ý:ÕÓÉ>=ð÷w'þ¾ìîÅß×Üð«*óÔÖýQÚÍuqs#*¹y ®n^De7ÔÍͨâF¹E ÝrÐD·<þ*åV‚¦¹•MÜ*ÐT·*¢±[ šîVG$¸5 nMD¢[ ZáÖF´që@+ݺˆ¶n=h­[ÑÉm­s":» õncD7Úä&"º»IÐf7ÑÃM¶¸©ˆžn´ËMGôw3 Ýn&b€›íq›!ºí¡+nÄ·+tËí†Xèv‡î¸=‹ÜžÐ]·b±ÛºçöA,qûBOÝ~ˆÕnè™;±Æ =r‡ V¸#¡×î(Ä&w4ôƃØìΆ޹sgݹÐ{wâœ;úà.@œwBŸÜEˆ‹îjè»»qÍ] ýt×!îºë¡_îÄ=w#ôÛÝ„¸ïnF4u· ¬Ìnè¹»Jr÷!r»û¡d÷"{Jq!òº‡¡T÷"Ÿ{Js!ò»Ç¡t÷¢€{ÊpO! º§¡L÷ ¢{ÊrÏ! »ç¡fîD÷"Ôܽ„(ê^†Z¸WÅܫР÷IvSA„AøŸ‹4çN¼‡»»»»k”T*Ƀ Ó½‘b@ôµTص!ÑÚª£°w]ÑÝjþϤF`ƒ«Q¥7©1Øãj\éKjB´¹šTú“š®¦v|F4»šUø¾æDÿªy…ïk¶°ZT˜Ïì,µ¬0«Ø\jUank°¿ÔºÂ 7`‹©M…ynÁ.SÛ ³Ý¦væ¼{Mí+Ìù@t™:T˜ù‘h4u¬0ÿÑkêTaÎD»©s…½¸§.ž»RcǯθQcßoθSãïÎ{PãÎ~RãŸÞ󢯾*¼óMtŸzW8ãC¿ñSáç_jìò·Âïþ¨±×¿ Ïý©u’ž®þI´I°( ÃÕïÞ±¶mÛöÚ¶mÛf°¶mÛ¶mÛ;ÿËéäˆeWØ“Þ'az¿„½è¡J8œ>+a?úœ„èó¢ïJØŸ¾'á@ú¾„ƒé·øŽ}(ý^Â>ô_ ûÒÿ$ð8•1Mü[:_¬.뢋°˜ÝA—ˆuÓ¥b[ÑebkÐåXÁ®¢+Åêh²XMA*»†¦‰ÕÓ d²7£Yb½4O¬˜–Ë”Sh…Ø×´ ܯøZ+¾Œ6¢‰¿Û"¶5mÛž¶¡½&íûœö‹mCĶ£ƒbÛÒ]Ä¢»ŠÝM÷À¾ìÃè8ˆ} =XìzŽeGOÄIì3¨ðú^(ö:½Xì3z‰Øô2\Î~“^)ö6½ZìzØ+ô:±é¸‰ý.½·²ß§wˆ}BïÄ]ìé½bÜ3§ˆçÒEâyt©X2]&¶ÍÛŸf‰íGóÄÓi™x>ûNb5ö?tu¬Çþž®ØÐcÅþ£§ ¾†, gàLv = ç²Kèâuô¼¬×»Y ñ ÄWPϤSÅÓè<ñº@<›.OÐ%b_ÐâFWЧÒ4ñE4]|!-_@‹ÅWÒzñ,: >—n">›n*>…n-Ù¼¶ÍâÒùâ±)àpmGšN«'ǯƒà|öô\Èn¤W!^Çúé5b«Ñ›_ë¶Ò[p+¿?HoC|­ÛNïÀüþ0½K¼—Þƒ{Ùô>pÿi£ô<ÈïwÓ‡ð0¿?N·Ð'ÅèSâЧÅè3âûÒçÅëéb“ôMñ~ú™‡•7ñ²¤KÄפMhf¯F[Ä×§âëÒ.ñõè&â“tsñqº¥øÝJ|‚n‡íÙ|&—¿H|˜®&ÞL·_ƒ~†øzœû¦²åb;Ð,ä²wçáû\Äýê¥ôÜÌæ¶ñw›ûÿþ—)§&’~¾·±ù®ð/WFÖZ|T°0b¯ÃÇK#öÚ‰øj÷j„Äqf 'ñß<VÃtÎJ…ù‹°E<ˆ$þ[ý—ü7üN:˜·ýªÈÛ ` †BxvKŽ—'=J|žàô:$¸Dè&§ØœjH~ òšŽÇf|R„é6·ã4× ? u0”°´07q¬@lxÚ]ŽfÄ@†g›kÛ¼AYe×JqöÁ&T mŠh[îhú%PôY¦ÀÈ‹U;—K;3ߌoÿà›y¿”úĤ~?„«oÈ!{z\%PÞ˜ª¬že8ñ–VºÌ›š³¢¾ ºÝ©ÍëˆÅTe±%\†.öò>DË%êC»E¼ÏbçYLB1¼Í©ZÿÈÑ©o g×m¼‹<Íe@m­©xl#A[D¹:;$•úÞ_ΙϽ¬–Ò\ì-]äR3 Ñ~r–"MÿçãPp Ê$˜ŒYQ%5´ÓjpVÎJN òwî›.V’Ôâê$êkäxÚ$ÈÃ0À{2 £Pýþä׊çi ¡dLec).§æðÃ7-áqëšÿ®é£ª9èï\k3%\¼w0$El`”ÞÀ¹±,s– ÿ3 æ > ~  È xÚ,ƱD@Àù·{Ї@¤Oì)E/Ùb6TäO4Ѝ3ù°hùŸIÏ—7_m±çÿθóƒ5ºçk|¬‘Šb(z–‘t¯ 3'M˜™j33Û«Ï1Û]F£ÿî×…‘ì²ÍŽؘüíŠ.HP&FRüE—: qÑ7ð@‰¸l ~‰"§>°LFÜp~Ê–ÕÆbSÅ(M¬“¢Ü ëÙ¬úŒèS¶n×hÒnU–d¯&†°À—Ó¦ª8ɰhÞ¥ %ù]•W² Tòî¤ìûÌ=—¢‚ó]õÛìÙg^xíüM4“8ñÝçPÿæÿ8¥Â«}k$­ž}ª¿eòî¿6 “ä_¹u=^K˜Û1ã¬a®ŒÙäìë@¾H”¼~U¤|;ö0áÀ>äÀÞ¶f7š¿Ð]¦Wþ„*^LxÚc`fƒÿ[Œ€#,ÓëÿÿxÚÊŒžaÃñ¶ï³o¶mÛ¶moá‚1šmÛ¶mÛF0³ïb›ë?ùEdƒ§:†B¨¢#;¢™#Z:¢›#†8b¸#F8b‚8b#ö;âˆ#Î8â²#®:âµ#>9â«#‘Œ›êABŒŒ1f33³ÍÆl¶0 Û², ±"+‚¬Ìʶ&kÚNìd»±›í˾qÈQeÇqœÌÉv&gÚÙœm×r-ÄÍÜ r÷Ù£û‡T†ÿ¼"k«jbDA4Ä@LÄÒóqI%²Ä“’2HFÊÙ*;d¯ƒrZ.Ê%¹"×ä†Ü”[r[žÊsy'ï z3€6= d0g†d¨¿ä‘_òˆk‹jLÄÖ³‰J·©÷9¥Îgäœ\P÷krÝu¾'÷å<Ô/䥼ýœÂ—þšÃÃßuÇ^ï¾›éxš榹ùšëû]Z[Ü~øÂÒ9ácµ¶FÖhk>¢Y ­ÍHmí°v ŸµÛÚüÖuë x ò„b°¾¸y,ت9e2ˆ\ÊÔ²)R¡°"5J+Ò ª"-º)Òaˆ"=f)2`‘"#V*2aµ"3*²àŒ"+.+²á";Þá=rˆ-6rI‰€Üú‹ÅCI&ÉWòK~ä“ÂRù¥¤”D©/õQPZJK’öÒ…¥£tD)#QT¦È“Ù2Åõߎ²[®â¹ÎhÈÐ ƒ±ŒÂèÏØŒÉŒË¸˜ÂøŒ©Lʤ˜ÆäLŽéLÅT˜ÁtL‡™ÌÁ˜Å<̃ÙÌÇ|˜Ã,€¹,Æb˜Ç,ùü?ÿ,É’XÈ®ìŠEìžXÌþìåÌÁXÁ•œÀ XÅIœ‚Õ\ÆåXÇ Üˆ ÜÁØÄ½Ü‹Í<ÀØÂC<­<ÅËØÃk¼cFpÁjeµÂ%\¶ËvÙk6yå°œ´ëü<}¨+a•1åªÜft]ÿ F7C™ÕLg\ÓYÿqÏÙÝå­¦£éiŸ4Íbn6™Òì¶O:0‹M|}©!#ÒWo~~%ô§ª«ÑÍqíbbv7‘9øó½žÎf7Ó«[OÙÄgnäNç¿,ËÊf±{«&ëëîPÝ_¬³¦ÎM¶fGvÔÚ]Ù—ƒ]ŒäxNuÒp¶¾tQwÖ.är®åf7ñNå~ÅQžv²;gT¯;ßÄ»|Ì—|o,ão‚» kŽ«§Âéû-Ú-W㛌&©ImŸÔš]ßp÷MUSØi­­,iÊ›ªŸÆª •CÍM[ÓÙùnÓ_g£õ¦Âíèt·‹sÍbw¾R¹ÞlÕÞÔÑqû$DRq¢j%W?Ö^Б.Q®Ûé·6b;s™‰¬™µmÛ¶mÛöî³mÛ¶­û×y6OŸþºR÷vg\ÿßœlB6"ÓÌneõ6ó$«È}¥ØTP–8U\ÇžñdYH^ÅÎLc•åÍjž¯VÓ/ LÝ–zõµdžãàúMÈÖd žNæ3eZYDv%KÉöd,éuˆwÉ-t\d<Ù€ÌPþð+Œ*ß%K(Se/9I‰ O*þgÑ_RîjLfœ´§tMƒý.«5©™Ðz½€ÚÚ[¾@‚ªÙþ]zÆ"ñH@"’Œ¸áE3\ÄåøJ¤™L—™r—Ü/ÊÃò‚qQúb0Fb<¦bv¹Ë±˱V×ÍØYîr]÷ãsÅi\”h\Å…Ûq/ÖÊ“š¯âm|ˆÏå.—rå¶j¸TÕ -wV¢Å%©Ú)†ÏS:d‰Cœ"—ÐiI>KÞK%uù.ùÙž¬KƱsuù%ù0ýk¨3É ä]äcdù yŒ“µ€™ZÈL-b¦3SK˜¦¥LÓzr»)F¦’¦é|SWÓt¹ Õ4]Á]ͼ\ü\ˤ\ÇD\ÏÄÚ?À'ù›ͽâ–,ã+¥@ê™{!MdõwªÌ•öÔ7êpÖŽê¾)]MwÓQz+/ʃº÷k³P†ke¯Œ5­Íl™l¦~wÂL™/Keã'nÔ= u:;¿¦×]¶k÷BÝû¾<¨5}örXNÊy­ùjßQ¹R®WïV¹[ë§ÍiçJTOzYÇûÚsQçÕò©|­çYSÉÔÐ]Á¦‰4ñ&Yk^“cŠt40Íô±uÔûšÁ:Fšñ|”³ù(ê\nÖšÍf§ÙoŽržÖ³¯vª|þ·;+DÒ¿ySÙÎ!ÎR—’õé,¥N%Û’1d=ÒM^÷G9Šº u&u3²%IŽtˆÕìñ§3Ì£Íê=tâÉ:³ZB'ñ›ÆÊ=dz¬^OŽPGI=œlAÇE¾ùM´²"ýdýîd"j/u’C\G'‹ô’ô_`Oûo&)GQg³ê!“É@þÇ&¬¶¦Ó”Ú—ºˆtÑéäh¨ŠX¦íª†:œ‘ÎÎÿ‘ÊD|ú«3Báâû˜ 5à‹„ïN)çìwÕ­D?"ÍlˆY¯z›¼«Ü¥üEâ Ú3qNÛ3ºò>•g¨F¸fJRàA²‘‡nØ«Iâ#夢$H3,ê#0Æs¢0ô»ÑÑS× ä©¯ƒµhËÁúëÙ(#¾9¤|›ú8™Až$W‘=Éh2QYN~þKÜDò—øû;§]³×Úëgo²Ï!÷L œní˜fgÚÙv®oÚEv©]nWÚ5hH–N7×ßÓ¿¿RÿÕ¾¿²þîüýÇÈ¿ÿŸõד×q;I‹*ÔŽDÄêHÔáF Põ/ÉçÉMäåä{dùyû·(’Z,xÚ­”%MEOUå{k{ì϶msmÛ¶mÛ¶ZÛ¶íÀofT¼éX{pâvö½•Ý]À™™g–!ŸùýïD•oZ¯ÏW®W±:Ö(Û Õ€ÿý2(M˜ß~ý4(_aÈHò¹¶d"'ù‰ ²!œ¦rQ€Xrbüí‡â~ûî·^ŒŒ-d!7qwˆ¬ä¡ ñ‘ë0ÙÈK!H,_¾fÖ?+Ôª]“½ÂJõÊ–çðƒ°Fíò58ïyÕó¯žÿV*k5¬YÏØ!òd Y!3YÉFv_±€ÅâƒñÄ33¹Iã]JPNüÛT3W]vy:ôlè÷P¯Ð¸ÐÖð³áoÃ×#™ŒÂ&¿Òðß fÉŽH i-£e,Fu3i)£dÌ)Ÿ±ÒEºÝTýViìvlP‹"3¯ò6ò¹Þý™?)A9ªøg-EYÆ1ÑÒ_Ê`&#d…¬‘õ²Y¶ÊvÙ)»e¯ì—ƒrXŽÊq$ý;›Ì„È©Ý+0‰ýª³’›‚Ä‘";±ÚCçDûìU¶ÓœÕN»•}nrôÎýÞyÔ;£¯W1Ýdz¼Ì›¼/ë½£²…¬ñ©uÊÖ²Ég7+;ÈVå`_YA˜ìä%ŠÒx^Îù´RÓg}Z©éó>­¼-qÑ'”š¸àJM\ò ¥& !ýš]¤—_½Á÷á"¹y–×ù¯ù•RT¡-èD†1‹%¬d3{9ŽAø+󼪨†h¯:¦× 8÷9Ù|ujà<ÜϨdô+xõRPûÚ+‹¾£YïGJ3¿¾Z¯_PÔ úö^mHW6JÕ ¹ý˜ÉL¶ÈiOɤñ /òÿ3ël¾`õVÁùŒa(ÑŒ%+–hà ŠE1ýý4æ#šêoqš³Z¯×²IlâÓ¸ÀßXÇ?ø{m‚ý˜#¶ƒÝʃÑÿ F7$Hsi%m¥½t”ÎÒMºKOé-}e€ ’!2\FÊ\™/+d­l-²MvÈ.Ù#û䀒#rLNø1î·oæÈ¼{îà¼ã{²» n¢›ä&»)nª›æ¦»n¦›ãæºYnö Î$urƒÝ7Ô sÃÝ7Òr£Ý87Þqc±XëžWØ!ëMgc¥q%UùZðeb@ ¹°}Ž™€Aµ\féùÀýõ n«Ï2Ü9wì8,y‰Áx V®Ê5,F½£!Òc.{ÃÿkÞ`HÆ 6Ö¯³à­ù™Tó½Ykcmœs—Jp>ÁYL#§\½ãìèÌ8!ÿþèÖoœãÿ¼‡¢.LambdaHack-0.11.0.0/GameDefinition/fonts/BinarySansProLH-Regular.ttf.woff0000644000000000000000000022417007346545000024144 0ustar0000000000000000wOFF(x ˜‰GDEFÒhJ‰‹¶GPOSÔ€?MÀªÞ¥ÚóGSUBЦ*fWjâOS/2¶@X`_pÕ6cmap¶˜‡9HƒÙ­glyf0ŸR0næ=ªhead¬L66“hhea¶ $ ¬ |hmtx¬„ ™äܳ¸kloca   « tFÏ“”maxp „ Q÷nameÑ 1PP“hiìhòö+ˆ’¾ø’¡z´w“¯Ñ=õ[$ÚÚˆ!Ì?ý ^ƒŸƒé0¬Àd±üÁ `iI‹…5Éåj•⇴r9J§êî94Ó²‡ÞU­o]Î/ww\¶5Ôà›ÇÎH{þã’Ýé‰É:“Ïå¢ ?¨(ÄhIpg­sŒ/9ã¼sOÍvíØ¾}G>yå9ç^—8zbâP ;Ñ{°³DØÇ¨5§fá7‡öáßúãäÁ'“Ï&¿KJ+_•E VÒ⺿þ‚Uï&ù·%•VPiø¿TJ"l€£AèC‡÷ô9_®sUE}kwmͳK”ǼaÌKB}5sü>“ªŠ«ü7Œc†î›å <à? ¿¹,òd³ü ø$ÿª€2ûùgfVc×Áj™¸V -ï™é_ÐQð^aZ“5¤h`üù½‚auØ d-ijÌRÇ´÷ GyÓŠ°„Þû¨™xâHh:RËÊbÕsß®xx+0¸¥þâÇPö°ç1)oÏÀž‰1,FµÀãö¤ðu"î_õâW¥F£å‚ÁRPå†XRÆÊ¬,´"=’Ÿ 4ÅD‰ŠšÈ÷·oë>~‚(¼ MѺº¨ ­ÝcÎqÇ™üµ(s¦cÜÉŸ&qní<‚Å"ˆÝ"ÆÎå¾R²4Aý³7u#“2sc‚¨g½7Ïñ—c¸XáõPa¦_TcÑ.V_ZNªµõ¤H&HgöO¥öÏ4{Æ–-Ë{†©ìYùü郃§çóge»¢{§÷9î¸}Ò{Gg;™3¬^àF9ØoÞwwýW¤uKjóê¡ÌðÐr|Ò¼4•È9ùPª½#Ä0pÅwHÿŽ%|…˜³ßÿÍÉVì_=¬Ê5iL¥åe¾E:bûdAüÄÞ_•$ýþ¢,¤Ӆ‘ÔTýz,¤Ì=ÛÔܼ)¶÷xïà`ï8R]}ŸdíÃõü×h¤­­Ý?u r:ù+„®©‡þýô5`Ø<{f±Z]øÿòœF«­ÅTTÞ±Ëá6¯ò·§ô^ÃÊÚV{`EsË:“«¶ÛŠ2¾ª–Vshåw…êê›Ý&›®´¾´>êñõ78Ù°ÞÈ9 ¶ê{¹3êõ/õ™}¤ …3—¥?z }ø8Þ™LNÝ.0†nqà µTÁd"ÛY¿¡Xls$kK8œöÁÈf*8‘GñûõZ,£}èþ€üDâÀœÎz@’-ð‰ì¹çF6к ¢BG½ }{^ÓúÆÆõMhèt-ìû ö1‹÷-Œ0?5.:f8^LÅj43BU…EƒGmZ]TJdyáJˆv`,  õüÙþÍ3wàc­)‡#eåw¦„ÿ ˜BÅ•2N+A…8™’µJX¤âþ›–PÚr¢TM Ü|ÿòU¥5åDYUé ”A­—kôúÍåüýü7Tqµµ\Õ šž–Nk@à‹"«y4—-7ÐÍ(²ËãÑIwš;êê:Ìwò[x ‚~”Nïb TˆQ7 eüãè"þr´‡×&}S_&±‡£ÑO0ë„óQ°NÈ/6i[KM³‚«p–—áÐaµí°Õw馵M#ñB™i@˶±é•6/•Š.¥ AÖ  kmã9þÕ&½=n0Zdtm, ¦ñü L)Í +K¼U“"šäXѰÂXFvSÊ(+Œfd¦~ÛŠµÍ«:Zú[â- c„bô>ü‰‡†õ–÷evG:6äÖDyô»ázÐTó¯§/ªX²©¥}sÄ“¨²«Ta¾‚Â4û‡NCÕz}½©ŒÓS×l†b­cÜ«—óÏ {¼µ®Ž¿ƒO€ÙoÆoÃ-¤cò‚;'àíôÓVì&ñíû3o÷Åû ðf'rH˜ÈVRNËž]~Ñí¹ãsø¼‘„ïËü: êU6S-Ø2[ú+]öLY1AÊK*¨&?¾aêe9B‘" ÿ ²Ïˆ(Zn¦Æ‹´‘s×4TÖØã/QXúœÝ§+Ë8ÜÁú¬ƒu{vnVpt™Í`¨æ1g²}s©‚-ΜÔ%?B VóM½ù¡‡ÍÛÚ66·¿-m½½m‘Þ^©Ç[¶§¶·$6d²7f30ÑŸúâŠþÔγ&¸U«V.ð§@”¨œfâ ¾[°gS’i}¿9¬³±-½Ú|é%ˆœõ§è#½)"0À<‚Àóf‚ªÒ²E>B‡úîú3-ƒççôìµËt6ÑLz½kª‘ œ$åy Ñ ò ]>Ÿäê”Í ®(V•bUè³e®`qR&óFø'„Ý:Èð!°ÛyXJë©ŠŠ {@QòðÐ Â«Š‡o§=ñåD»¼©ÁŒ>ç1¤Lÿ͸}Àµ‘œ Ÿ™1]€ B! !’‰Þ;ÖÙØ¸r½r½¤^qz¯ŽÓ{O|ö³±ý~æ¹`_ñùõv%ÍJ¸FìDé×Ú73;û±cË÷Ò¿F;eµ³»ß|ãwã•"ëšf×Þ…Ïi÷k3{´R<•.õy¼NÅùø=C™O «ªö¬\Oüˆ „òr÷à„²ø>ÉGÁH>~Pò0ø¢ä6ðÃ’O‚/H~øÃ’ÿ«î&·äI(Fw5ÆÞùÁSRù9ðS’¿޲{nFòOAù|É÷c{`<l<³´ë|¯ô^,ùQð ÉçÁË¥ú'¡þcýÔÕGÆÉ.äFuåjÏr«———^vRì=þ9Ô×ê(…ÀÉfFñFÝ•%¶µ–ÒHæNÜœ ‡†ÊW#)쥺´Ûe·äGŠÌø4°‚V3;S|F÷AÝcu¾‹b6¸&ÔÛ¨uxÜUÅZ}ÿ)Gq´Ú'«]Q‹¨0uq\‡ |„­½U!Ô‚W#ž2¨,oÃßʤ÷;ìÎ|:ǯ~¤ûEÀƒ\Öv[[æ ±QæGãWSR»šH­º ü0s”Þ¤H­½ÙP- @LæíÂJyì ëÖGoØÙÞ96Ýw×äû®Ù<µ>yÃ=›¯¾jË=ß»ì÷d×äLdª$§pýÈàÖ~{¼-¼òס‘Þ®¯w+}¬5<–¥ÄikÞšãØ…¸«³,ÆžÆaî­ª“E¤¨çpoN#UÊõàûð¤ÁGÁHž?#yü¸p­âåsEùVƒÇÁë®ÆXt <…£Ð/£§Á’?‹Ü=þB&Ì£ýû òmäFˆÆ‘àDhO–<íéÉ¢ÅÒpñL×m÷Üû1Ïú"¿¯­oìKïšßó½/‘{Vžüìîÿhh2î¬ì´m½­ÿÃw¾Hô/ñ‘4‰Ù¹eó}’‚< ~Fò0ø¢ä6ðÃ’O‚/H?®»cï›à)Üh(?~J:ÎøÁU'Dy¯î&$|=/ž›1çåSP>_:þ>pbl'Œ[€›hoâ18Õ‡_a³ ÌCC°ó‚øë·B}QWæ—zÔTÑäÖg?6÷ šÅáŽÚÅ $jX’jð^º†Dco¬úa½‚[­M5lº„ËEž¨M¯ú¾}/ä}Ä(Š^Ñ6‹­Òÿõ”Eqäµ×2gñýc¦[¹Ká)~–óÄYvÂuà¿Æ|ñk¼Êà£à$Ÿ_<~ÜÉbÂàKRù$øÉmà‡%Ÿ?È\H*uÈ…Zþÿ¨³™,?§_:/)ík©p­-Í+-°;JKEd:hI˜íÆÈ4™«Z"=kr{•\§¥ªÖ¢ÿêc|üÖˆñA«_Ô]±Ø6xJ¸OuJþ,úˆ(o“ü<+û ’Ï‘](•"dnKÐÛª!Tîž&5+ψx9^îÊíÏ8¤°¹ªòg‰_ó_D6o‰Â&ƒïÓ\ý+u«2 ~@ø“ÔÛ” ðƒÂÿHÝ¡„Á…g¨(6ðÃÂ_ ^¥L‚/hަè_'•8øq©=Ið3¢|?ýëIeü”ð¤C'È.ðs¾NàC¿¥#Yq0/EãµüéP_suÙŒ»Þ0EÔT±GK¢F\¾Ú9ÁüœäósÒR©Ð#¹§¿þ­ÜÏíÊ<ƾÂïR,¶ËÇr­øÕ\wµ>ÞÆBq IžŸ•<-¼‰­GðsX$ÎáGµòÌÉÏÁÃ7©KZ½Ôw/a^¯ä)ÍU•¹b çð£ð¹h—ô½4sDÔ·¿a±dæ‰@±”¾^Ri»¯óßmÏw®ÁΧ:ãÖm×[3Ú QWÂoñør5BæZqmWZõ+Ÿ¾8ñ£¼o¨7ì 7yì–²2— ¯y‰ —%‚ÞÈD[¡»¶¢¸´ª+óí°0ÐÊœ)ô.§Î×køè‹¹òV„ÀãàÇuWcl}<…o‡9 I¹ùQ‰8ÎeY}äIð3’O€Ô]u²Uð%Cn'‘""ÓlÁo» vmŒ´džËºøå®'{êWn»Ä²ªjµñ³~LœõõÚ!Å)ÞƒG»:WŸŠ|-e"y÷—÷<ø5üÝÌΟǮ̑Ȼï»ïÀ·¿õ  !¦ïÜùIÖ«„ú­Ç‡Š´u=à=$x‡”·_‹ÌDBÁ|ÓÀÚ¢Îå¬$;jóë½%®BGiÐëZùÌ¥V)U•µCôwú›Í÷H>~Jò ðƒxLxŒõœ^âIÇÇ×^½(ºúF}A·óŒ·'Eƒ¡Þ¦FÝ϶w6éžõ0ôæž²”@ÿ¯úøL¹¹_ÝÆ6y,øŠ-ŸùÊČҌt÷ð±0‹1 < ~Fò8øqÝU'[C_Âqá1ÉSFWlÂéý‡Gú|Vú^Z8_Såí­íµ|ü€äIð3’ïÑÝTK]áîdk¶PþYLÇ+‹/1‡ãÄÁÃñcRù+0š¥¿¥»È·ÿâ5á„ö¤[æÕ×t´_ >Üq…+âèòÅ;Ý›’¹Ÿ,lY¿cæSïž¹Ýö©¹+š76øb.GÔÕÖP˜øº'rÕ ™;ð÷ßùñ™±¯V^7Öº)¢ªÚ 0…J1 kƒ×]u²cðg±)«/Aù˜ä)ðYÉÓÌéTÏYÚÂôMÛÿö?lm7ÓpIæqm‰ZUµj~Þ«Ä5;ˆPß'ù(øÉ'ÀJ_”Ü~XòIðÉïXòÛÀ•< ~FòÏènrH¾n“|Üy‰òUà1¶âõ¦¤òs৘#Âý‡J*DµúŒ.&tœ5a€4G‚Ñ’Á‚¢¡ÌóRê¹­.·®ÁUÍæïÎö•Ù2 TUÔÇÎëÄyB(‹ï‘|ü”äàÁc¬?Ü¡ÿS÷ñLˆzi澨Òô7„z£ŽC7¯ØVÛd‡+K.êç>\™ßíÓ5m¢-ÊágÄ"Z„xKynï±Uôø“Y}Ÿä£à$ƒ/j®ª岋·È?gõ}’‚< ¾(yü¸îµÒ<…þj(?~J:ÎøA͵œ ÞÎj鎠yüŒäqðã’‚Ð]±œ ð¶ Ÿ•<Í)¿ÌAE¨ f· “ý<Óç~gÌ÷Ãl¾ûcڟȳª¢Ç*ya•«™I•mi5ó®‰Û{hªÕ»nݲifó­d×¾–kõ•³Z¦Fô*¸Š|KĽ´ÐZ¥G¿÷@ʈv¿ÛüÞ~Œñs™»ÉMë/Ÿ»îšèÆšPÝ-øÒèÎÂé/Ý_–ßû{?ýѲ¶:^:ð£Û¿¾ÇÖ^"ã´¸³iW‹–‘B×lY¦Èêãþй©¸¢ÂYÝTÚ»3½×¶ÜÔÕ¸¡0?oª ùs×ÝÝlktdJÈ®éú¶þÝ÷¿ïk#|Lè¸áÙ‚f-›Vxŵ¹cæUuE¥–¢:ëL{aÿq~Π’ÛÒHÿùöÉmZž~Œ¶µŠ!nÈ+=8·yýpËWØw÷Dß\ïÍ[q¹ÿšžŽ¡3áK4néÛüÿïÅór˜¿y} !öVŒÏi1u–³_%^C=Àb0â×ù±"ä+뤕?¡ä~—söP7qÿœ1¦£=krTz“ãó÷¿I÷Ež5¤Ñö:¸ÿ¯Íê{N–Á÷ü(y|^*?~BòÝà‡$? ~Dò¿€?$µsJw“ ʧ¡üi(cYRà)ôª¡¼ ~ÒpüW”pU¸™•7øŠ¡ü¯•hOžÔN86¶Æ3ÀÆóâv)ð½øžìE’Oƒ›%÷‚—IõŸ…úëçï.Aük:^(w@¶ðçÍš`&½Ä@žþHCgÔÁrËÂfœùBns‰¥°ª´ù1‘vö‹¦Hý 9Q¼(ý¬´×a·®Éo**Ç%†&ÚÈÏiuCÄÏ’­‰——½¡o5tµØóÌe¹YãÌõfkm¬µ°²4Ü mT(äâ\¹GIy½úâ+üíÿ·|L;Pö\=‹>ýBž˜–³ÃÓØËï@¢…OT4ïó_¥Í LÐæ^Ó/®ÞÛ~5!™3¹›{½v§g*Ö³±æú©ÚiOþ‚ßyuôo/u·µŒ³¿-¥®D-ow{$Ò¾s®È¾&¹½¼œ¦˜Å}³Ã>oEÌa-îhzŸOΨªÖz¥ˆøqƒ˜¾îd7ø!æHAùô—÷&ϵs#ßÛç ò…V…‡¾ëy¶àÀ4ñl¨¿êÚ®«Gº7t¸¼žÞ7.Ã9ZÊ`zkM`çëîè¾zýÈŽZ÷¯ürwîkÈM$~TÃ[s "Íg%OãzîaÕÉ2 ©;¹?ƒ „Ó D¥|/._u² ¾_ò4øiÉÀ o¦£6I^C~äâþ qŠD÷cè¼ð˜ä)\)|Vò4¸Sògp;+¼^È€ôÿ=9fú:–§%B†z]ÊôpqLU{»]®Ng Ï?âÀÝë ¸sü–N—}湕vš36\ëù¥ÃšŠ¬»¿<ÐeÒÇá?øxºÅý&eó½F'Ëàû%OƒŸ–|ü„ä»ÁI~üˆä àÇÀc,£<%•WÁOJþ8ø¼Á_TJÀUᥬ¼ÁW å¥ÔènÊ“ÆÍŽí„q ðqƒöòœÃ ë ¢Ìë&oÖ UéÕFäªÎ5t†¬•Ž‹3V».s)ŸÐ3ª–ºJ't·9ï‚VßD­>™¿DY - ¡X–(™Ô ˜¿åüÙ†®¦Ú “h{ì5öÍÙc37kÕ<4Ê7TQllTEyÂ\a*èpÛ#œ„^+FØGýÚ^~‰ÿÍóp•i9x¯I¡už!ª¼›z­xüJðEÝÕËÅOaÌ\FO ·ªNƒ›rŸyVÚ&•>ÏJk6ø<Ó×v‰\_>ÿnœ&µ—E“ãCQ_—ÿ›tÊ¥ÓíõžM]}“á^¿¾›4is­JûWŠ ó÷§,ó—åÏàƒîeª>KÕB–A÷sýªAò8è<×4Õb2z‚ë«T1Ù zˆë3T­ä,èCmiÐÓLQ;Bè?‰ z’ë°Óò—‘—kê~¦.5ˆRø1‘1$òPÑñ9}oÒÆMø\Æ®å9Óú—!ÏyÿNí,8ñ9~|">fFšÇ$OÏJž¦ùÒøäK›õ|éMÊ]þý…ŸÛN›¡b|:ó›ó·ì¿N¤*_¢GòóöìÛwÅ*~·ñkwñ”SèiV¯ð%ô½èWOi®¾JÛ¯(Âý¹‡wFV?E…´U†o¥™#¢¾AkÒß^fk…yyf9»ÊñîÞ+kîï½l5­w%:MñNGæ3ÆoÑ›ÛèñD=9SèùjÏûæ÷’€¸—<ˆîdü˜îjŒå‰ƒ§ð‚+”§^'Õjt’?-ùãàóº³+¿¾„+ åÀAù”‡{?÷YÉÓÂû"ÿDN ÎVæôÓ§ážñ7Þþ hIVßkt² ¾_òÇÁç%Ÿ?!ùnðC’¿þˆäiðÓà1–/ž’Ê«à' þ†R® ·°ò_‘Æ¡FwSžäpllŒO€O–v…zŽHÇKÂñÖ2G ªWŸÇ¯…ïƒó¼Ý>8vÌ ` ì ¸±¯ïÆî†AßtÀúÞ0þ~XÚ0Ð<“ØqÿŸ~üF€µ‚ׯÞôñgWÏ:RXë”ZüªC”0´Bz9q’ ^N`Ùʧ½›àüÎë»{®ïj¿ÊæØÑºãzB2àp»1»’ážkª»ßÝ35ÕÓLâ²7û-“¢Á}]íƒ;犭±u ¥-UŽö{‡oÚ4377³é¦çÇ/Û(ÚHŠÈøÊbð‚8´Iy»߈4†KúÖ{Z̲§Ÿðä‡ÕÎÒ`U îÉD²ï±PU­ü.·KŒ^Êæ?5:QÁOJþ8ø2Ýã±·:‚!_¢ÝW㯉6&†7Þ~M"Ùà 8}í ß–Âh{UyÄåmpzb µÑ†f—?¶>™)"Ê•w×Öûâ¡{·Æ×íÛbÜV„Ê»Ñ.ØŒV樦{Ñúµ½h}…úV4U»e^£= ‹|&;Bº+]àÇЗÑG¹;ÙîðgØHeñ%ð˜ä)ðY|¹ÁÓÂ^o+ø V/"°«'‡Åͬgy—ØÜc]÷ÄÔ…û{R4ïüÅ{|TUÛãÉÍÒ½ùBßkt² ¾_òÇÁç%Ÿ?!ùnðC’Ÿ?"ù_À2ø/ÈàHåÓà§¥~Õènª–< n•|¼æå+ÁclÏÔ›Âå†ö¨àüYæ$L¾‰ŠP­1S…¯QdÛs…ÿ«ÛÍ7õYM™7åýWøŸÝ¹ ÑâšB§·±>îÎø/Ú¥ªZ}ü>ñˆ8¯ÅeñŸ¨à'%|±þ€§0[ÂÔƒøu|ùå¨î]”^ýꌱ(î:¶›1eýô…è]ú¶ª4̶CæñúˆÜɲný†!êdû×BP§å¢*³ÄgÙ‹}ÌVl.Ï5ޝ¥©Äá‚Ú£õk,×>e W%ô ,3‰á忆ˆ¸úÿÀG‰ïnã«Q1ÚsY}¯ÑÉ2ø~ÉçÁOç{ÎøÕÛ¢½˜Õ÷,ƒï—|ü„ä àÇŒŽÏ§ð'Q”WÁOJÇy|^8ßׯÛÙ*Úy~ÕIü´ä àÇ$_߯»cûæÀSèYá³’§™#‚ÚèÕû[rPH~2µˆìpýH ~§3)Ÿµ_ד¸¢³qƒ ׎x‡&üýþ¡¡Àصݾî;··Ä¯9iÅIkaÇÍãÃ7´‡ÂíQ×ìHl2xû¶m¾fÊÓêÞt×—¯Ùpï0Vöõýµ°ÿÝ"E´³ì¯ÝDÜë꯾n¯õ·×*_~dÆîãûkkl›ŒûkÙ}=Àw»¥ì =Rã5l8¦7óU¾¢²5¶Â:+ßj¼IßyÜó«|S/[îÄWi{ŽõMÈ_™Ú†0ß¡ý|ŽeŠ^¸rê†#n‚ƒÇÊòÔ6¶ß›¶íA²€´xIªã+F˜Ý÷ñýø¾–Ú–u-õkú;šBCözçöžØÆè†þź¡h0âµFÜ‘º‘ØÐ6÷·´·{2ÌÞîñüø~‘› õ¾ÉßW¶Qà§Ñ«âÚ|ÿœ¯ñσÒÊ%/òuavQÏaNÆ N¥•þ/ýù~Ëæ0îÿ .îø_h¯íúŽ=ãvë¼ ˆ\`Dþµ¯OdÉþ¥ $„Á‚t qww"w™É¸_w¿wÝw¯ûº»»»Ëì»k™uwöUMCÏäÎÓï‹Q:§ªëœ:V¿}‡Öòð-y£•=$ yÌãaëé¶œzcÉ$:·Ȩ#›œ[—j#£úÁÜú×”›2ŒRˆ¢ ÎRÔ4kSpR%@­ ’_¡Lòrn— ·GÖ§·tqõ5ûÆN|³œÄ+RŸÅÝà¹$‘æòõ”Ö†&ƒÉÀ >| z\¶v6§mçu öÏë0só û€T¥õ8 ¢ B—&õ)‘^ã!†ëGzq&zFKÝ}ªvU²t`ijÛ¨ÿ+MßOˆ }%Ú¹2³®FˆËD ú¿G…po׻и¬cNß|ع6Ê{Ex|¹&äsq{4kœú2ñ¾Ã·›'c±å!^—}:‘ZóëM;P(+éòdv"‰-/7ž²k16ÅÀè3åLµ¹J­ŒÐ‹ë£î—×|þ¥ṉ̃LöhÊ[PQ2ŸÖY¶ÚÇœæ°Ó<]* ßpOЪ4ý.£Â'Ux¢Ñy÷¨‚¼@º=Pë|¾y*Zñ¬8%=oyre¼G AQ4=ã‹O¾}e¡gDÈëQ ¾ ®É‡’ryLñ`ããçF^ΆƒÏóS²ÞG8ülÙ'úqj¨½-³ÒC‘J#ãö6R†\Ó;(ênò??ž¹OciAf|ö_¿µºùÜ4¿HÙ¸(;•'¾ƒø`ç¦Xg€®ÕAdò <ÔßcP=hÇ]åY>?Cð¼ö‹_¸¼ƒ`âûr޶ó¤¸2ÃxØ\&üo]¯ Ž•ë¼Æûx§Öãáø¡w¾ ŽŽ‹4rÿèÚ¤Lsø¦ÔY˜mHKiºJQûÇ5Ñ 6lÂPGTeÿ!F¬=Ëq—ʱê‚CT_6D­»q ’e‹ÆÑ9@Ú¯þŠmsÀátŒx˜££9BÃe«ï&3†!ùoÖNpãMæLI–¬ùR[‘ÈV2Rênü³(ºùDŸJ8¶,“•j¸’!+ÙÚDuDTˆ7B*nLÃL™X &´Ú‚|,1——"ý.»àæâ^TJ±T ”¦pãˆ0´ÅusYĈ±æÙ¹Ö¿.ŠnžÔðµ›oïî÷§û $¼5ßá-9Lu«ï½±x®¹ÁyÖ‡!7Íg€‡Üï(téŒlþ>´ÈÚ(»gÆå¬ŒèÔ!½%Žù²‰L1jLéGL5™±‡’½¼¾¸ÑÓª~é€Él°¦m”Å-•{u*D]©¿Qgí½l½…Çφ·ûb`Ö<¢O#Åt2ë…ƒ}D«×W®ªËFYs%‘Jç•K=V3$h5õù*mÔN&úx½‰ #K¶pG~©Ë8¶¶Å&E±ØaÂG…ã©âéœwÆc£lÙ³ÙT*nSGƒak ¢#O­Ü˜ÖÄœÆ8¤ æ§³ Ñ›ÌGöVk0𠸇÷CÆã ÑÞ3÷Ó£gí ?è°>~ã§– ÒÔ£PO< wÀPGä#ÌÖZl¥û¾”lÎèÈ ™ÏBƒXb;lMê­?$¢‘áӛƸNEyîÒ3¡Ø°l,±ª6ßqnòTrD5“íâg _Ö‹¿]?Ìã¯5rçL`v‹æœ§¥|i‘ÿßb+­ã ŠWàF ½Ð.64ŠÒÇ ÿ+t?TêÃF)s"©¨¬2?•_[(«]ýÇß­^ÅÏÖ¡ö2)RõÎÊòA‰øÚø·•£hŒ¢ë‘—ÁBž’±5‚íÅD‘ˆä¯Â ³ñ݈#o'M èf[ùÙ3~ó|ƾÐh#FøˆFÈ›™3û3±E`¼âWŒ`oÒ¾ŽÐPä¶# ná[À; ²þå·¥çÔìK« òÞñ8þý¬˜$–áÒFCj›+·àüè7±„ùÑ×Ñö;êݶ¿ý¾/AÆ’·,y6å¦IÏOc“~©IëR&™XjiøŸ` ÿ3l½"X¤~d³c´_öbl³»à ¸Ð<ì— µ€*t\ssÿ¨× `3ŸoMPAÍæS¥înMæ¯ý½îxi×ßZË·ƒ -ÌJ?L?óÂèÉUá®ûk„ßÃE¸ýSt§ë'Mü«ü*Ô‹è¹:™~Ó4)T[­_ª’Ñã³[LÅOSad’‚?÷'H<È3NZq¿Á/¿ùÙ‰º¼œÚÅjó‰`\/6Ž" Ï«Ó9V0‚Eœ–‘¢n…ˆ”1ð^u6îWœ‘¬ÍN¤/Œv›¡ßü«¶oÞñd÷Áƒ„­R½p {P¨••yp`‘&5·• Ù¸`;cu=Òãÿïëø3‹‘àä#lÇ_¦e–&8Ävý_D¯SzÕ| \¸i4Ø4*«Í?ݧéãqùÕù§ÄÚ>Á‡Ëê÷ò؈{Ýüņ ôËãZ׆ù‹°7%”̱Wʶh.µ"ßã˜Õ“Š˜!7gÎXH¯Ü¦žÇ$“‡DmXªÝ«Gç\Š¡°Â·˜sƒò4€­vÆ uÛ®˜lëÒÛ…jùå|HÕj¤ª>«œªÙ¬uJníSI5#am0¿4ïv‡)KÈ ‘·‚'½:RœŠ\V«K$LˆI÷dp|%·:–M¦e‡5ŽsˆµMweÜ6 aäô'¥R‡ɽTj/™«Õrð{”Dѳõù3±Ø™ùúÙhæÀÊÊÞÞÊÊ×§Ó´…F¼[ä&öÞm‘2?þ64-rIk3ºÜ‰üRºGÀðìc¿müPë‘õ Œöç××E±³õ¹31^wúyéÇR~^ÀÛ|ÁúæÖ¤ç‡ô^ˆ-B'½“§z@mW® ƧeÌ•óãëTõÌøÁ»ó‹U×d¯oÌd TU™-ìß«ìz#¢³çBfÜ3·ŽŸ¸Þïõús¡ÒáÃî°¼:—܉¥Ƨ¦š³ê'nÄZS†cÓPOP­q hoZ¡7ÓZ=×ÀUÍo˜˜¸¡ÈóÌxàW’çöìVyc×O\#lÿ:ö©ø™ÙÙÓ±Œ³Ï9A&œsñ¥ÝLìôìì™8’ç‹ðy ¡H:Ð…½èÕ*$½2„-‡Ô¡r\$ãò|Ii¨–]áÖ§!ßÿ'Ÿ&T#­OSà4¤­m®ì ÖÄL/N·?„X™7ûüTáqW¸ÅÖÅ´œÞÁ6™dT-+¬í¬¤×¯†wìmÄ(‚Ôïö¦ÏkëäÐMÞûZÖceϬL ýª!S ÄvúJ^:Ûø.‡‹õÍWÁ…ýÐuÛgX¤TXQ{S°æ ̺6«áT*\ýúm¹Àu:Î|´á´ÚÍÕo ÿ‚ÃÅ( tôØZ/È”¡üAZ)¿â÷º'PsnÎ>65J¥úX½M“F ú4T™7{lvËG~öµ&^ …ñcÍ ^0P®hžÂmñÁBŒç_°úܿژµ-O;§4ePï²k=j²à´NÇ49Rª6ŠÂy~iâ~B=á ¡È´Æ§õŠ TiÓ(´²^±!ì‚x‚½ýª5¯¯‹›âõ4#·@£$jöt©»£g¬ÉìE‘ mÆ‚$ŽT–»¡ÿÒåw\|ÜÙhïÌûåqô-ïì Ý#Om_ ´Ï¶?„ÃwÄ›qbFy ´•¹ ÓÝy§Á¦QK†\Ê©äø¤ðᇠ@ìð‰Ý¢åñÆŸÒü‡›øŠ(ýïã+Žˆdîó0÷z¾™{5;½EA/¶FÿD¤±WªÄã6ÈöMùõ¬ï7>ÈÎõ5â¼tã/—%øT}{e48ãlÑZïRA¤Íim&)„uÛï·øt)§+©óYÞ¿­vEé½Hå°–›wÇœSn÷”sl×Êsµ‡+‘½4ù㦤ó;Xg NæLW×ìÑzÚa‹˜­q·XA¦½Û¢C§ºñËZ)º“ªÎÜL÷¡ó÷N±Q#Ùˆý¢7m½maMØôâÖP-$"+Fc…lüŠF|/ÙÆŠdõÐþkøŽƒ¥žþ^w¿°°}ïü˜ âFôLÌNö t#,É2ìÿ_Æ"ÄG6.¤4LæÆ<ÙøYû¯¯ÐçR#š)¢½X‚NÆ?!q½h@ ë0òOÎï¶` »§êàós?æÌ6”@¤/#˜ÇÆŸh,ç'À…ý°œ_ûº¥<ÄžDhй¥×> rÏцFFBÚç4>ò4†„ëôWÈ΢u¼ˆôwd:ß– 5þÃE6Ý;›/ÒÈ‘··u:ìí­(vÇÑ_}¦‚u :¿&´«%›wªNíîR€ó_  +>w½›DjÌÏöÜóJ-Pp€ëröÆ}a’÷AT¸çWA*:FÃt®&Ì¿€ö^>7¾¦·ªÊÑ8YÌÊ óòÑßùf×ýñó^mpØlÊ;¿å¢À¹L{-Œ]gê¤Æ‚BgOØ'`ºÖ¡Ý¦ìk£šP~y~~)Ô†GÄÃBlÙÕ­¶š¨·7.àŽxO*ËË•À)ïÈ(_ïíE]óôÅMЙ»ô•N?Ð6ÕÿÂsûÍâáÈZŠèšøÐ….¤•,| s«"áô•baùð þ¿€SoÕÎìº#+Í­N‰¡£Õ¶’QßðíÕw¿†nžÜÒéæý­Oè^džñd‰£Pß…h}¤ž€l.¤+âÚö`ûÀi'Ò†?ˆ=5³=]…Bµ!c[¬:'5f¸=Ùm) ™³Ëc‡EåЯ°’üud|È(koLçÖ¦1’ýh_ X¸"†"fá®¡àøŸ]_iŸ¢µô?ÿ©·ôª„Rk}ßm…+à¥Â/?Û^&x!ÔŠg9٠ퟗ.Îdޢʦ_-F×ÂŽI/󬥢›ÚoÙçS‘…‚¾ñ êmþx¬ÕÛ>˜ý ɇ½“ŽÈf¬°¨³*sžTqvŒê ‘Ô¼= [!šZó4ÞÇ!°žºöÕQAô, šÿëS—ñxÜ]ïÙŸQ‚æ“Õ÷þì–`×—qŒ»ÞŸmÙ¨`ÊF“Ätï¦4–vêoLýí3õ?Â>žDÀ1F¿çâ3Íš·Œ¸]ó†)ýR2Qœé&‰\óD ¬ÀUw´›Q;‡@íD ר™/­±Ã»¾rs©àmGµ¦¢SÉÆ‡‹KȾCƒhÈÝÁ-_÷݇䋶¸˜—ôl¯0Wfb¿Bü\|¼Hè: 4ÖŒ]™7À ]²âAí‹RHÖ*pQ¼A>Ú7Ћ΃ /½Be:ùjAWšË 8Ú×§°®U91±ˆž ×'ºÀ?9Ìk\E¦ë”ív⿘ö7UGû÷™öw³Úßô¿ŸÕþ3¦ýG¬ö_0íŸdµ¿”i;jç€Öy–+`ãÝeÙ‡êzSgiWBŽl;y˜©æ"š±dl-w3:Æ/ÞÀêÈòÿú|K3ö .`ŒZñtg-aU‡÷¨_£Ò+õ¤xn¦ËAŸïÆXãR•1¨!„ îÜÉœÓ¹á ­Œ0¥» / Ž.~ ÿÓqóŸ¾ù¨yëñbtÅ eÎÿ‹cÞÐ$•™ \ŠŒ|oû{ XÞ+ò >bã⓾2°ó› –'¦ÐÕFˆÀ¯Õm„Hô#;r™;wh4/)´ñ¯ŒvõÌOïM Ri|ì ÈVøÆ$LYÈp’Ưá.þ‡Å˲¾¿¶ó•#è"©Zr#2ïÄÆàWãQ÷ÓÇô‹$­ ãÊq1Šéaìd*ô·¼³q?i<+—A~•1F=+°ÏÖs"ì­Ûh Í{¡À×çßEìãßKÓa>0jF´üs =UÊ…ä ®AæøÉÀkŸüü—À†¨ÄWþGãoå®þ¼¬‚_«hܵ_¯½ëP´ÞÔzJôcêÀé ÷M¿,.Ý]©Ü½dXF¿–MÖ­³g·¬›gÏNM–ÓWÍ΢šùÙÙ«Ò¹£K‹{{‹KG#>t±)ã{f0m%Öæiøã&Œ¸­À”›á5¿Ÿ´u‘Dmâ–Ö×–+«õÞ›ùÓµw=ñÈîÒÒî#OÜuíŸ0eó¶â¸Þ(œÅ ºÖa¤Šƒ¡Î˜Bæ¼S"‹·ÌÏßR"ƒÚ;©º“‰EW5Räi¦äÖ^T«½h-wdZÃ+&Ï=T>Íž.z®#‰žd¤'…ûRŠS÷*%šÛ_ÃVnÊâVN«õna·r~‹[kíVg´‹soó]pÅÜßµäg÷¸Š[×on·â'‹?ûæ]¯ÇïÚvÓï¢ãª$Έº/­½è#°~jÕ"XDÃaAOÚÏë‡Û県ÕÄš!ÍD"êåö 7<­Q\ØâT)*-ºõÚ¹kR±â¹ô ow«i^Ç¿[²¬«ÁÈ}!3·9™þw•Â}U…­fÉe xìóÀÍ+ܪá¢èŠ~\#+—OÛ— ž¹€Ï=6^ôºV̼"X)TÃ"J“°Fv 9ûNâÑü‘¤y2ªkyƒÕìšÇ`M^“úu%«vez=i}$²º¸ø¦×ZK]ÿ®‚¯ÃBUF¡V%%}]8E(Àþ).aˆMu]:ØÇsuωIï¸[¼äåw{e¶;îŸ\ûäÁÚKÚ±ïÚš·Ìÿ‚cõvIJxV/®]vÅvã±ÖcNųysÊlsTkË× ¥ÖFm¦™Œ½l l‹ÂG'`öÚ^ðL¯¬M&¦ƒò`ÞâÈR9¹T7dÌŠÔf¹t,AgÅßGÈT± ?Ú©q™ÙHÛûã?WÎ%¬&‹Þë˜6êaD9Œ÷ie£ƒ×—R£Ò訉¦ Á(,ýH1F¶›M+ ÄŒÅaŒp»PçŒ'ˆÏ!iåS^[Š#€„~ÖÇÅáÅ&¬ÖáœJ>èvyë~Ïô¦K½´¢žkkÆKfå¼ú}jýèPÿ¤GèÞ˜Ž\Õ’D>o8põ5þhžì;}òr³ò» ¾*!]f-ádÕÎ*[à:µ[ÒÖ:ÁŸp͵`9õΙ׉ÂT’’¶Í¿BÛæÚ+ÙæHUþƱcëǯ“a]!(êÂ7€XÛÞuö±ç=ï±³wEÁ@&CgÙ™uFK¦)¢oõÛæß(õjDñ0¸mçË‹TWš/@R>öï¿”'”¡ø…e³ü)~T€T¢‚^Ö5® Îçò¨ÍÃÁÕ/“KÀ_Ô‰¨"¤ÌOšàHÇôÒ”þþüU³â)ËçµU²[7¿é yN¶¢ø¤ò38ö!‰‘R,¸Ó’‡—¾Ù5²iÅ2‰]?>§ßÒ=¼înð{Y®Ø#(v ¾Ø/Åã¬ÃÍH¡¢#êH H» ÿB £V·¹õ)#œé4h…Ú!«O¨Š­[¸Üw4¾›É’q0 §E6ˆæÅéu¹¤O!Ia)‡?PÕ*܉¡uÜ–p87Wï¤êƤWÌsæ{椋•{þ±. ¼èþìÚ7·o½p{3J>^Wp´µ‚iùmI³Žñ³§e‰Þ|˜e} .kGÞ¿°,Ž¨Ù ²ÚGÀ-Ç=ãT\+UH¡ÔZ'<Ö’c,Õ\â ¥Xh«f<Óao°°fñ:OGGƒ#w‚GsWHÓ×{«Êéçÿ[·+õ&&àWýLfËål¦\†e/³Íóo³gãŃsó{{ósÑèåpñÁ§EÒëÊ9„Ÿ<ŠÈÐÒÛ ìýwÌâM’Þ9Éà™øÎ/àHÅ"±A”N –W-ÿÇÿÐùí ¿hÝX)I<\"ÍÃëx Jì_à9:fØooè’Á a*¶ß;[õS$rsh2QN³ïoœ?¾ë*t•=šý5*é]Y½É¢¤÷C9ª¥a75 §%뜨ÖLuÇwé½(ZËV§/²ŽDm±É QìôÌüÉ`™Çã)w Ç¢ÅÍÔXK«u|‹“ >‹w :t|º9Qmq†ˆã©¨=M‹\%ê·À¹è%×÷tgPÖëšÏÁÙiéí -ØC´ÂfNìü}qæ¬gùDWÖ,H³Žs¢í-û‚ê.eDo=å̆–7"W奷ÞvÓÍìcfOþÿÌ>¾“kø×Þpåì#®êÍâU*ÆZ„ c䨠WÀ%×ç>þ‘úËx7-½ùK³Oò>* &küëoèŽAA\'!×7@®Ûw¶˜ƒíIÁ³‚׃¹‹-ÊÐÜ1?ÁŸáLײ`n NBöÜUE‰£Å;šS¤NÆ— …ƒÇBÛiòxúî—¼rŒsÅÏeê8Y·Ä4 ~Å<Î$ò—ó‹^ó¤Õîµ(ìjo•ÇSÛäV7i¤œ³“ÞÙ^ž¸*ºétéeQCüæle^7–ЄGGÚԘvq.{&©‹Ý[=ws¾PÈsðN> ^91²õ;ØÅ(œd^¥ÖÚœ áĘ'¯´ËìÚĤ9eJxuïRrÜMüë“S'¢FExH±5ÊšDRP‰nÛÃu4<âÐV¢™†tÚ6›à2Yé°Öð¡K8,ÓYʱZtÍxOÏ®ÆvbÑŠ6哌Å›'Ý \Z­Ë'-g¿t"mžŒžñGªéU_i<¥|v(žÈçù/„׃ú¯SÞ)ä×ën]-êÜÅŽ)Ã~ýí?«òöëcðçTþw¬s.ø *¿ëœ î)_‹ùià/*_…ùIà/)_†õçBý/+oòWtWO_—àÓ—'ž¾.Í8óÑÓ³ùz’ð”á<ãNáQÂ]Âc†WGO×½‰uÞfø.ô› ÷ÐoПN™O§žõ[¾žÖGГ„§ çà0n„G w éOÑÌǧhÏŽæëiÝy=IxÊpõ%Ü%ÃŒ³ œùèiáÙ|=IxÊpãCx”p—ð˜áÕÑ„Çug8ž]³ø-í÷`­ðJj­Ðüèúòíé+‡Íêõ¨R®|Ûœz=¯Ö¥èä•mæä·‚ø@ù Š–_ìÑ£_½Y{Ín¬Vp¥ªÚÿ¯U•®[Z¬Õå»bU¾³™m¬[µß7X·ê;þ‹gY¢i“\xgÞÁ¾þ²;üμ6¬wÃF`»öðVåKUnTwpýÓÛì¯Ø9·ÓÀ¾mÛÖ­(VãâÅJT)i0²K°äà þ¯µ÷Œ£:Æ÷½½¢.®ézÙë½ßI'Ýt꽺Èr÷‚)6ØÓ‡^ !ôŽ!@½—F: „_ ¤žàÕÞ»½ÓI–ÿ÷}¶v÷vvÞ̼6¯ÏŒ ËbUa-_8Q¬–ÁvŸÅqù ~ëÛÃÚdþ‡ícG–@¼e€û3üSëBÚ²Õð$Yû·;íJˆ»Ó™€^«ª¼z[lÏ­“ü=VÔÕ÷¥›8Ôú¾ôµªÐLàÀoŸÎdº¿7û¥µº¯±´_Ü}å°¥½°n ó2ðˆ ~3ÛÑ ó8¼›Á ÚDäEÏ0ßD #OF¥ÒqÕx=‡žùxæZ@ hïa>&_£Iøœ«·&U3×~LhýЖ¥sÅõ€‡f?BßaÈL ´œIµ=eBß©~¾š`ïÅQô0+ì¿ Vzÿ5ö”Ùçhˆ°Þý!º–i(_ïV€Žú&YVXî%+Þ¡Ñ›{ K»@ëuœ@«ñ,­Ç Ë¢§2A!o ¯C B4÷Rȼ8 Óq"‰C€lÈ(„ ‡r …8È9¹†Blä‹8 @\ÇAìdnGOâï–Y-<Rí~V8êyšŽ% Ñ=¬p´B>ü }dº‰©…|˜g¶©Rm™¨ìè3GÑw•FmŒ á ókI¨¿B¨ …P´J$Õ´2¬3DM}í•5úŽTãµÂ8ÄÆ´ÆJó{39—ãúÈ }çùêçS)xøVÃ4,C ¨M52m¾²9Ó|Y¤Â8ªª¸(„C@¼šÀTžÿ”Â;s‘ðöd&U™×4n‹ô ”E}Á(‰Jcµƒ„§üZ”4©––ø¯1ôEæó'ÁþÇçáKüÛhørþºÿß¡G™·ažBÉpÞZ¾?¢dΞGÐË›Œš†šìÁVìÒÏí•ò&³M’ζàQØr%—rœ¬±F.á¸,âúªºäBF“Éú99ãA#²8Ǥ­%×"±™›ã(5Íf`،Š÷¢ ~ºtt¿¥JØ}ªæó=~M£kù§öwoË|aÛ¹§NL.™ØEÈÛF:6®ÉgóÙÔq{Z‹¾àÉ;¤|´³ødàSIw¸(9'9ÃÅ2®KáÏ ÍüYdÛú*ñ@eÿÀà ²…7bnª3Xëò*Ôýäw#‘ï>ižԊå©Õ{’â°Ä¿~IÇæmÝÇ.[°ëUØ3¦𠴔;ËO`«©ÕdáEZ:rÅÉà?+y¥«É¡¨ªª—*ª|]¦Þ £{Ì]>8äTWU¥pª»^ž¹oëè–ûÐÌWbzÑûîFJþw÷ExD¤}…¿%ø×ÈÅ jý ã©eO·œS¾™©zN}K›sÓ3“ý‰%C}ÉÀ°ÑÏmiÙ ¦²[k|±”—Ë·e[sÍÉXܬOqÞå#±™¨z¢3»,Xܱš„Tüù(=Ó&#»©¥ìô¸T$êùÓ 4AŸ°{üþè¶Èat#ì®]Ý>ÌŸTÌÛ$±¡ÏÒùYñch ÔÛ!}› ԣųºÅôœžÜ=©õõMõu55µMZùø/W=¿_©‰5J†Ùj+w#;M* ÈÌþ“”NB±¬“„? ɲ6jÐÖÉ*j¥Fƒ¼É^:'Ô¸T¾T¥H4ŒYUÿÐÆW¶‘ݸ?°~‹óD;ƒK{Ÿîò¾{iÉ… wô ø:Š/b˜…;v!b’ß%Ÿî%3:lT®&‰ÏJ$‡Tˆ‚LÚÕÖ½»|_DØ5õ#Í`X<ÈŠ4z+úÿ‹ƒ‘õØoÛzŠp„T[8¿o/ìY£åeÑý¾Ó—ÜtÓ%®Kn¾ùdÓŽ›"wìXÑ?~ðŒ3öï?㌃ík—.Y¹rÉÒµ7ã{Ž-;nŸ/ä+ÔUØbM-Ä-‚kÿ¨RŠU|ç$2LõU>ù$qdN¶Ù £œ€àÃßAuž’ðã ŽóuÇýñ{ȱ­{gûyÛwoš˜|p-îëáÿæoß¼¶3ÛuÃÒRñ"•XQ*ÿ õ&"Q˜<¹wGÛù;6ÌL,Y²d9z~ ÿßã”R¦™–î‰4q“@qqzÓ“göno;oÇÞK—,Ù6ŸP0C9‘B9ÑÍ?yüA-²·ñ¯ùî€e$:½yÃ*ï€!`ÞØ6qvWÏþÛQ¦&âsÎC‡\h1$Íááëvî<2„ ÓNâMÖ^€‡”Q­Åƒ3Ÿ6˜ž|úiô•±3óù3ÇÆövvî›Z·n .ˆÀûù7jæ¸ò,rZà¬+–Ô ô¤ˆpZT@±ã/c‹‡åY¹šE×_Ôº¾%½)ÿíçvìõG!%öLµwq Ê–ÿ5ÖžÍòw£Qÿƒb;Ážå}u±† ;±×Óò¯‡t<|¹þ …í§ÔÝo¹GÈ×d¶êúuSØ21dˆ«&A~ IYasl¼F"ÊK5-‘þ¼DÄ?Eœ?'{gúû¦ÝÅV$q°WÍùLdpQÇ0ºOµ„¥Þ Ÿ<·¯ÿ¼©ô*C@Ûé^sÊöµÁ~SÀ¹ŽòrèÈÎ× ‡ÍI½õ‡9¹¸?Bò²èšj2³ðlÆ"»Û à‘~¯¦R‚¶f§ ,Œçx}]*¾DoÅm/ã…<4MKõWo¨5)T¾ftï()ÞÃå ](è¸oúè2PëC¤À u†=ê}¡,Û9ÆðfŸ«;È1¹‚2zˆÿ)Ú7Õqj„”@Ô(PG1þÎeDu¡¬ºå9®TrÅ’ðm’ÏÚ6ÓÖÝ}ýúµ¹kv<ÿ|–äv±õh'>aÙ„N3"ž¤ð#‚oÜA §­ÛðÛ…¶•t²—hªó„˜rRd|a¹'Ÿ¨(iäSždX*} þŸOÓN´v÷ø$ÉScË:ßéÑÿáå3›W¶·ve/tw—ú¼öc…£Aä>1½uëôòmÛ.D/Ԍް}ûµÃÃ×nß~ÃhÏ¥øâ\p黺Šç~"ÀƒH;göЊ‡âÙo{ùr¨¡©±S™Šé•âÚ ¥¦údãM-'77ŸÜò^Gå0ÙíÇf‹'«Þa[…;0&'v| =•äxÕÑ¿ÒóUÀù=õz[UòyKa?·äVÑŸ7=ŽøhÆ ×#a"síÏÿî%æµÛ+?yêsv2 -ØçêjRK»´¯É2b«")u~þç_&*íŸÕç|òɆ7úÌ~'¦¥uM#Ó\ ‹ˆ+ =øŠE;Š…Ø/Ú]Úñ×€ -ïFò°@‰É—^>AO¼´Ÿ²8!g/Ò× •îØíØ[yì'Qhr’ÿNIZÐ)Ðì€:)èòNªËYj‹æÉB}^®Ð=•ÖñsÏŸh}Þái[ݾ’(t×:šücG¶n926ÇtæÜéãg_n·FáãNŠáâ{Ér%Œ·˜h^†d¿xqz²Á$É,õ“Ë^úŠúG<ºòq[7Ø®°=Îïà!õƒzš æ¹T8¡A_ô?ú•|åÓ€Muƒ¨VY³tùמ^¹¦V×@ N¬Jôj»[å7üª»Éá‡4Q£1ªyˆ¤XøuS~Ú?çbÝÊ¿»öV^SÞµh/ì` c9ÐúV4õË•ùôdSÊÙ¿µy|rÔžP@ò"‰{ÕÈsÏgÄ"þ›Ç·Ž…Ñ%ÕÔ× šúÖ²=†~¹o ð À—ÐðCe½‚w€§§pÁã5– )37ú2”ZŠG„–‚je6ð›¯|ƒóÇ3ì³ SjQ4¥åæX|[ þÀ…ðÔG­„ÑÓ–'N{ ?e¼¥ÓÉDaf¢N¤T,îˆ[»ýLïè®.·C%“GM‰±×0ПêÍg¹k™/ÙÞ¼¶æ–«Î:2᪒TW ÔÖ‰+äöî3Vl>mÃøÀ2G4㜹uhw†a‹¶¡õi*œnŸ§¶8¶¨ÛdÅãOŒ®÷:Ü­[§v¯l‹E›×miKç2'¡š;\ñjQe"Þ›Gz‡Åbç¯ñ:mî…ö¡­—£ $Ìó+Iy”ÏN¢YøZÉhÌ]g. »³¾§}XÙÓƒ¼ÊÔÉëÔªJ- }"⨾cz½G–rÃ?â£E_äT6c¦©\ ŒîDf<À!Y<š#4&Pæ I±-QsNŠ.'­½ÚÙ`nKÄõ52G›Æ?90åÓ¶:äUu•2³ÌqõŠ=Ë–í™]ãjBxXâMo{å•mi/è¹&ךٟÁ?ÁÎ1ºåÿ¿mߟG¦½œµÇéɵåSé–fŸ§ÍÀqñÐhØëð¶$–×踸^r¹àt–ÕaS)XÛåZêEÕÍq»Òþ½’U?B¿`yGÆ‘y/zöw|T,Õqu.Ñ ^w®ˆi3*$çµä§¾UÍÿÒ•RÛö}Zª¿‰ *ù%ü³ÐR–Z ¡q& :>¾!¤pú´²šz‘T,7ËÝ7NŸ³}ðÊ6¯¯^2Œ‘ʳz–$C°îôOô,Ȭ,·¡Vš¥€U˜¤VÞ ®’jÍÒªÆJ丶¢¡BYY1ˆØPlä+#¼Ýž¿(³ìZt4U¦Siü -hW#‡ëVW++EÕŠªëXU©ª§3+ˆ]#xC¼¯kµXZuïóÚŸ«Ò±Íð‹_ÚŒšLZEí‚vV…‘¶²`Câ>ñ»‰‰ß¡^|‘×þøÇEo´|º™b¸A¸ÒÙgRG.˜ø%ú€Ä…_Yò­EÃxæ< Ów#¥¡…Û#„F©O-ûŸÛÑ‘Û - ¦•yQÁ÷ov¾¿ibÚ@ÿgåýxOl¹‰AK©øÊT*ÔƒaÃÄÉÑɰ!¨ÂcXÔ-™ŽNFÔ5¾IéP(<ÊP{²Gsñ´Â­Vyß¼±¯^Imß(êz~¸_¦ >ä ½7"i½ëˆr>©VÁG^GâP±|˜Æ!"¬‘LÒ³r¥Ùú_)E¯ð‡'~2ÑSyô(1}ŠnãWÅŸü4ú*à“¹”ø‚Fr Íhæ™ m*›râY‘ddÅ$BbÑ{¼V(M7–ò·ô|üþÑûÇ>p.¦’½¸'šIÁW9N¯8v zôh'(²y€¹yiÜh»J(ÌMKQ òâýüØÒ­[—Žõímm냜þZïèXÇò€‡)èv<á«@:ÿ|\x1u]Ú¹dëÖ%é½éL-G¦%"-ØnÀ´|>+PZŒÎøÄ BcÅ8Ð/#À²®Gö¼ )oß Œ›¬ê´­gr¢ÝW9´ý‰¶“ZÓΟ­±à ÝêUkW5ÉC.¹{é䮿ße³$žÐÖ~8ÌÍšP›˜Ÿ6’ŸØ·µµ­M¥Ö¶¥W77¯Î¶vuµ¦{{Ñ/÷¿]Ó¶{dlwÛB7ÐÇJOqÎ$Îy”Y”&ÕR´j:´,Yš¸ÿ+ƒƒ-ííÝßyÕÓ¦|"žÕñZþn‡Ûëçßô½r×”.÷\ ïÙ‚§ÞY'ÔòW þ¹ÅóŸ‹s&åõ”š AN¹©tb5—²è.kÚ>6ÆZÃKW†Wð· #ÑO*DYV’úâ™KÖ»9;ÿxõ]gõéš«îëÅàÎWÐõç²uØÁþBاôjGIÐ;Ÿ={BG«í›Ò¹­ÄÁ…K0w/ê¶·¨-–Ñw[Ó¼krÉ®fêÚbÝÊ•ktŠˆÁ9Zô{Gu›Ÿ¦IÃPŸ¼ÈßrÀ´‚¼Sß5Ðï6šfЂàG©­ÛÂ6¤Xt…¦Ä.”`¡IQ[+À¾²äH­ºê´¾º )RP¹R´y $¿2NK7º½çâî«„2NjÚ›À¯‘Ñ–4‰4öR=ùÁD–¾òÈ™²IÒ(P‚?þ1©0WÁ33ÄmIÉëá4~SÚ‡Šm1[Wj‹ï&mqÉwÓÛ€ÿÇBÍüµ¨)Ñ;ÂJ¼“”'ý«è+¿™¸¡ª¤'µG‹ºGwcÄÅiŒ”Üšÿðu×M<ùdA õBíïEßÌïÙ“?¿¤„ýg’›ô˜`™ï¯Ô¦Àq³ªÒtÄ_«ððU"¯ )’‘eL\S‡A·œÙú8®ÏvUo§HÜO&$ø;…Vô2|e¡ŒÂ|´¢¿“å+¡E>ú'~eaL‹®¦¶jªJ¶jHC©ïûñ¡OúpÇ÷Ú1ÜZÐß/BŸF„çô÷çï<âÓu[*Ž~¾.dAÿJG’z嘤ýH–ž´tF¥Îÿ½ÉÇÙ‰*î3{”G}ô–ÏêT -›RfÁüâÝ>ô¨vs?µhç¯ëEº€Bù›å³‹3hš¸ÿè ú–ÐñÑ.B3çñms©Ã†~m3£Ý6AJ¡óV8I>I5 ˘hN|êœÃï å ¹M­àO!Ô¥q+B_—st$v¯–FNW•´õ¶.ùÈfWòÃyœÎ,iõ+KZýáyžúþS‚?Kàå;¨=ܘnÎù6Wéf~:>ÏüSaò.3¼ëQ Ó$å-XK‚÷Ut<Ñ÷°¦xêÊü.-&e¿‰}À¬\«•+4šÓèS«=8‰_×+Uz½J©/>ʼn ¦|ÿ qQ2ƂݪÒD)&¥•ˆï¬[r:]‡˜*Å‘åˆ%¥½¹H×ö䠪џžZúJô¯Š^þžä<[·…9ˆ£GçÛ¼…ÔlåBê8ij Íá]MS«ž£|àó¤VN©Ó):Ý6¹^/‡çþ <¨W”Rè‹Ïc_'ìIâó¨m=µ¶«:Ç%M+0ÚÛ¾‰´Ü#¥Ø ‰ hÄû„.ü\|HS.œ-ÿ´pæ½lëÌ³Ø ÐÓ…9,\fáäÚô©«Ü£+m\GŸ=ë´õ…Àɢ˯¯—)¦îI»+gów;ëkbk;Öœ¢õÈ×õúóö™¾`>©Ó†¦HPQ)’TJjjE•j™Óš:&Ó[Ò¥3ùÄ*gP/~œW¸rGFÅá‘9[×§OÙ g®ž@r}>=²jÙ)ƒ9sÒî¶·ÕøÖ;ãb®·9·B)ªð µ´ÍÄzû³]ƒíž¼-?yãZºL‚/§‹pV‡ 'ñ„¤zÊß[2ãëâôA£7 Okœ†áàÐrGÎ¥‹ZBAOŸÉa]]3ÑHZk«”FµÎ­nL©,C)GÜVWÙdÕ˜¼F]‹ÑIö¬ý{Ö‰®Þ¾EçšNÜ=x¶w٭뉷Œ ¤c¾6S>îH9ûH41Èã#Ík~¶FµöÁlk—Qª¦”ÁdlIN(DµSƒ¹U‘¢7]RæI߉vÅí(cJZ™öï]Åwü1}w ^i'±RØ•w8qI+üPJÑ‹&3KœÑ– q«dҺꊣ`αÀ íùªF„âOŽ‚¨5„#¼O²1Á'm~ºÏ’wüëVêeï%ÊF£òè¢vkhq'&%yÊü:KÐÃs3–ø•Eg,ÿÿ³Kt¿ä¸9Kš?¿îÂêæ¢óy+C ‡OÛP[ìsÝ1sÁjü óùëÅé}BoN¡½²} ÿÇ#t`±ô„CtFĤaÅN|r2Íô0…š@( Ö?XP j5õÈ “ úy¤ÐÿXQQY!•ÉÖ­3ÔŠÄ"dsUVJ°æp‹BxÜÓí)ý9ƒA'\K?©Á¨]Ö„ ü)æ^gní²Ð-á0Æ9YÓ±÷Ñe–^gvݲðÍá02;z£±‡£'íu$š#ÑT*i~¬¥¥ Wð¬tµ=ÔeDEáŒXM;žÎ[2èQ\>HÌ8I$¿M¯ˆx»»¼‹ÝëXéP;—ú']þ¶Nw{»{ ᴌنUùgkMp<îk‡“œÁ›p6nœáÒò®ê€Õ³å´=1_»·Å“OEci[²Ãk]~aß…©Ý9àƒÒü"äA5mOËZ³òý÷šíÛ׬޾}uª³3•ìêªyà–[ï¹çÖ[ȸøâ}û.¾øÄz„aÐ7ñ¦¦è‘ Ô))<#WžæëÐæÎ«º‰ uñ—»è\;Ü^¾:‚½˜Ö¥ûÚ]«/ìimsuê‚®mË7v9¬MižŽ®¼âôh²Ëgöû'3û.Ál`«èi é9ΓM<’8ázÌÃpª¥ß¹|°sÄšŽºòŸ}:5±-km^]“äF6îh6gÍ K aÓÇ8ÿÔPK?èÓ‰ŽÔ¸xË >oCìÈþa¾^jQZâúÀ!Õhë®AÛùV$›A÷kz5*_ÓO:’èºc<ÏBh3Ã,æsJÉIçŠúïO²¸ ƒ©ôh_Æ4ø”(÷O™:`HLÇÛÖÕ$,I½8ßÑ'—ëQ´÷Éš:ï²®®µj¿¸¼ éã^l6Hr¢5¬šÌI-];³±Am@ž2ú‡ýÁ\:mê3³Æ ×ÄúØTMç7|áÆ§!¦Ö'öo=ýì¡l¦Ïnwׯƒ¦W°þù2Ät%9ŸAL±×±Òû%®±ÿúrs¿W):ó©{–õFûÎ?týJÐCnr¶ d5AØæb=*Ù)„+õØyë„ÒZW%¹¤‘K„Æ£ku`Ô-bŽ®”™¹–¸/íB#{t¸Æ?ñ´‡DšÞH¸ß½¦ß’Ö‹|m^ðÙ€6r\°=tD8þåLس7jº|1Z’]4ï U·¸¿ÀKf™¾z3:¤ (’ÆÀ $g —n1÷Z 9ÝãœÑ,Ið§>–l9ý¬ÁyÉÙyh#$6áÜ#¡Ç™S™êù5w¿`‘»¦dŠ›A…ó€KmÞxNA¦b ¼ÂªtU¨jš8›Æbï¾/$ϹÚª¤F½Î äÖÍÎ~ô †S8ƒ!!ÏÒYrø …¯ý¾x:D†¯hé[ˆðtä3ôÞÄ7Ⱦøžj<‰kÊ싯ZUf_œ†ïeÞD«Ɇ^5[~#®n®Á!üc«Ð^þ¢UØgÓë8N§§~wûð}ûäIG†]PüëXu¹D·H›¼~s8diQ‡ÌSÉÁsЧs«•JèK'fWʤõÚ‚KúÙTs‡§$0ášm%³¥”Ÿà­ ¢­ªcç Jy ~4¸8hÅý’&ߢ|MK¼BŒ^(²ö¦ãÀ·ør!œ*E1£èAæ8##&ç }¥ëØe¸=x÷Ê•wCÁw´¾;D“$gà^èÓ4£û™'ðcL#„³Çj§ Ö™ÙdLmwÖxd*¥Ùˆî_ѺÌèÔ4û:¢2­Fc„ðYàû,ä² ¥­¼°-Q;5žF%„V- ô „öæcZk úZÉ^\m ¢%T²%D2€e´(;DK)|i>»“y–9sìÙífÎÌiŠ9]ŒÈö„ ›£$õÞ$U¨ÔwÄ’ʬTÉ<5N;zи¬Õ4‚h²XÞÛ¬¡V{;˜Ÿ GÉ·¤3žTKÕß8tÈvúé_Î|ÙxãM€‘Œ÷Šð_­œ†ï¶Cú›n4~9Ò t&`8DÔê Üo¦ñ€è\ _Eá«(àõHǰ@ú7î~–Ëà¿DßDM¸EéF—þ²¹¹Çÿ,ƒão—à⯕ÁŇKðŠ8‚V«bç?¿…šŠ_Ыån ©ÀKDë…Z,h‡ƒÂ ­z¢™KQ%úÑOåþøóy¿¿³óÒ|"‘'»bÎ@º¦`ó1ù)½»#®ŒÍ–q¹ÉÝmí¶`ð G»Ës8rnW»#”p»ä 8æJT‡öSûÕ¿ý-á¦b.@:tÀ¥ðEf¡KB*$þþ­·^€0Ïûï!i+…Ðt=©=Ijo\z9{ù•‰ùÿb 2û ’ƒÒ$&+NŠa:ëïbïâ?ylæ9øŸ²>âUÔÊœ†Üè¥$‹Š¡;Í"ÿÉiˆŠ O#³¥JF2élÄÈÉÿÄ}ォÑœ™)Hu*HµÒ"˜Ùë.2§"1Æü©T«@ª‹(-Ù䮻 hàЂ–å:E¤’‚Tõ©ìö¸:*5ü‘˜= „âß!1üÈeäpœq)2ð?ANì½w5‰êÃÍ0ÔƒÅùHAšÖ iZ–k Tq~¬³3íê:ŸÄ¼&æÉUØóø £É¥8ˆ‹Â/œr CgV¢ßÎ>Nàj€× ŸH&éLZc‡è +û…ŠSè·x'–(~”ž¢PãèìÓLmùjÝ\¦]{©—sŸ;4t®‹ó^ò»þ¤3ÒÚv& ç GQ%þ9 ¥žwæè‡™îîL´%•jyèäwÎ?ÿ'5­~g÷îwVCÇì(ó !̼“QÓ±–ææ–„|HÀn:éçŸÿƒ uÜ„døRÕdŒת¯íÙs„ ÃÁ‚åÿMÌ÷ Ò£†Ë´gÏ׎1üä‚ahxË-kÆ©ðRr]êí Un©ä¢Q®²%í׺ôbÅ¢ë]Ú#©¡ä²ˆ!„b(¤.K¥‚ÞP`2ŽVŠ*c¡èd ä-J†ê¾ÂjFÉsiCà§VJ8“Ö­ëÌf¸¹µþ9!ŽØ„bÀ˜Q6D”¨žŠbˆQºßd Å/Ñ¢¨ Uµòÿ¸…ÝðÉ äë̬„¹ÿ‘~…qëLËØß~ÒD¾ÔA¸ÉB81„CMü?ZQU! . )-‡, ÝÒ‚8ô:‡‘ßwøPiA·£Í06®,ªÕ“Q5ÑþRçaWóÚú •‘Êõëš]ÝèvÃ*w@³mkSÀ½Ê°„H˜}]aI_M.“µˆ$ÌZ>#¾ì2q¹â4ü¸å¸{±<„B$Iˆd)˜šÞhØRxJBKzHBè”~‘ºéŸ}F[G “Ÿ?ˆJÜ(‚,J:´lh |û0¾ÜÑ´¸\~W¶;ëÊœV+דsMs~—× ¿ÜCÁƒ[°º®lOÖÕ–³[½[o°ŒîL Ò£ð4x9‹Ïlt$½éá7= .'ç7íIOlتN(tv­p=™@2G$wC*|—ú+¨+XweçÖñÑÙSG{wwd·wùFñ»ÇÌ(X30²ÒÍ“örù W"z~î×2>E½ÒÜðQ E" ‡¾ Ëàû>NáãèÂ">ÞKà>/âã‰2üQÀ'ë€þ¹70nÒú;Ɉб,ëW*Ëû•÷W‰»%Õ5V³2ï‹…¸éÄØ:s0dÖ{–ÿžÈ݅Ͱ¿ÕäË´ûÜþåþŽD<ç+u.…¾åµÔo¹»Ô·ŒGÕjz(Bsaÿòý›d•¸G Ž¢LÊdl ²Î¾/,ï¢|¼eüÛr^w ¬“I4Üdøaºkj2Ðcøáà±DS@š|OøF5ØU{¾†P ø„;ßh}ý—ÝÃ;*è˜7 dá+„#º/ áƒÇ†!•©öœ¡º±zN:Å*~ÇŽ×|>A‚ªlª¯ vI—lÓŽüZ¿_P©p§ñA*¢Ui¯¹=äÏ%r!È%|b_Ï òãE­åq:‚äÏÓèðµûü¥Köö\ºôü¾U©Ö¶¥!«ÆîÎdúÔr—Ö¼Pæìç‘ùÄ»Š>Uè_FkRymñ\6¦÷+lš\ 0 †Ôf­K®̵(v­Å?ÕÖóEó¤0ÉÏ–Û)ì¶-÷xú©òúg–,Éôeüáˆï3’v Úšjëh?õN‡›³0 eÑåøU¦jŸ¬àÈö‡J'´¶…éex¡òý ¶$ VÚaB!:óäμǓw¹ò¦AsP) 8·ÖÛ¨Ö£ldyKzY$²,ݲ+?Îyœ Ù™þáá^«É˜1.dŒ#}ý“¡–ØfFD¹O¹!Ÿ<ŸÁO(„‚[à㹞ÊuDÁœÅÜ nLçì Åc§ßžÐ*ø_¬¹“¶n'³F|!“c†io¾ÌÑu.9Y Ô-ŒT2·÷Ž.á ÁÂ=Yª;èÊu·®Îï92¹ášQ{º‡s7KXuÎëÕÅz¼&g-[ÝâÒ Zœmg.k?kmóàyþ1‹fÙ^•_k¨7*ì)£ãËã·ì9ùö=Ù‰kÖwœÜiuÚÎþ÷D—W­u½¾+¼";|Vwü¤ÃKV_Э–'¢¨^gú²º1>d¸!fV:s|¸°K/šäâôùé¥äèÅAËáÃ2ÍÈ”|rº)­ÞßÔª[Ú85ÓÔÚ´_cÞ/?ðjö²ì×á<^}õUTqé¥pèbõX*zøÈRtìß5äEâB‡QŽâ¼$@H¨,…¼,@<€3JC½R†“§Wˆ Ý4Ôk$¡ 8¯Ïá¼Þ ÎäMb„9ž1ü7€<Í08O 8 my¦Aïä€<+@ÚQšy‰] o)ä y«(ól 1¿*¤ÆìIÙæ#æm’Ä5;‚rç%BBe)äeâœQê•2œ<…¼*@ü馡^ UÀy}Gàõ†q– oƒãìV4Æ<\è¹Ìíîûòþý©®ùbüСøŠ÷7ÀsGËF­‰Ç©OÂ[¾; í& ÜÒ¹\†y Yij œò…¼E9c†ŠCèÊO_z,?>ž'—#r8Âášm'­Û¶mÝIÛ¢#ÃÃCCÃÃ$d®î#t!™E(­ ²øqóHäõü>:“·ðSà¿&UÆV¬rq̡λÒ=övÏöñ>‡ÏÒÄY=¹Œ½«×›d•< ” ×S⋜ûS‘û׃jocÄdäš#I‡Yå’9µWdÀa²FÆk¼0B6Ù-VÎí2ê­:%gà’k¨®"ìLd‰ì¸Á€®~}²ZâËŸ‹D‘¶÷ùŸÞÞ÷óícë–cü>A¾<`[ïI’ _”î›±>µW²r¾§æLoßRUgnˆŒd Û]Æ:õ“©Õ’ÊÎlëõ*Իѕ¥9æ…ÍÑqÎ¥_è_×kõËtuE°=â2¹vµ· û[ÓYkºfóÌŽ­µ¾ªºÉeÝÝ~Žó)õ¡PWÊÞÊn¸å…˜gp’ô ê°ô<‘6â|É‚1¥¨sÇÆDÒÖºóÔN†a©tËt<¼À“Ó¼•9gÙ*ŠZXÓ!¿m è5–žH¼×©°8Gl*…C§±GjÌ}@—ðÖ´ûì©‘Ìm ¦RAk N$ sÁö?Èr­ÑÔd´¾ìÔ7éåÕæ°Öò;yìÇåE©ñ&¯ñ®K)·Ñå°5Ñ”jÉYÓ¶îñu5e)eõ“”êN:ÒIÚÍÃPï¾üìÒõ^¿ßë·[­ö‘¶/LMžÓ¦j9БßßJ$„0@˜ãf—–‹#膮°ªu¾ã@‹ªíœÉ©/@ïì¿à´ú²…kCËa—® ­R¥Ó©”Z"“eöbæ À•î"³õá£U¢¯nl„-MCl½ÚmŒyk•*¥ÆaóuÑV†áѾQheªh+Sŀ͢Wˆ U¡ÅyI€PY yY€xg”†z¥ 'O!¯ ?@ºi¨×HU 8¯Ïá¼Þ ÎäMÒÎ<Á¼„ß¡PÀÈò–ñ"Ì\„Öä;ijWPÈwˆ‡ÜøX"z¯àDÃôýOt>@øåÝZÜ;–t”« ¥à½:§%á/¹zÒrÚ¤B­ér¶&ÂR‘"RëôµÑyQo¼¹ÒìQÕxêu]¹캰 ,ë4Â+¼§ØÕÔK6íIsÒygJš›T±×T…Hʰê°>›wfƒ¶°Óãð)Üè RaAoh‘k—÷'Gܱ\:–öÜÚPÏ ;+ÄF ôOÜ×},¿ÔÛëæÂõÆz½ÊŒ{µÖ:­,Š.M ¸TµîšÚöx¬½¡Ú]×Dä¶Õ*·èžÐ··Àä¾\l?Óúl&"eåþ:…M¡p6Zc^tpf"Úç”ËRgð^N¦)·Éê3‘4KÒ—ÈÞĘŽ©¤\irØQFßòõzÜ]®hs“èǸ٧ö5¨Ô©s勉èíAtÐ5˜ˆ ¸ƒf¹<˜ª¯v6(_Rztaà"‡þÕ;즎®0ÉÊãÉX²‘=ŠYÕµRyµJµ-ôè!m‹tT°Èlà/…‰Ã¤NÆÞNSh”rPÉ ¹s;Š”¹ÙŸQ;ìA@}{^iîN½ñ¼Ãü€ž€)°ËÉl¼0§YV£cccSS#\´ƒ }AÝ S«e jÏ~^¾!…•A# Î_åeTÔ ªêZIM•º!ã­èýG$ cÖh@ëù[m¸¡HWˆÑ Ĩ¦ O´°™DEt^÷)û»V¦H<ø7o9ÿ©«;Æ ip¹B”ï)ƒL:ÝnÈTIY‰¨RÚ€úèKü)uÕYQ1õf!§-„—à#O½ÀnÅ\ZF$¬"lîTA·)Ì4q6u¶‰3HâøU¦Ö&[!eÕMé+äp æOXÊ*“4ƒìŽ @¨<-QHeù§¬w=êîv“¿.§Ëíá¢Å/É5ˆºbí±x;©N €ÐÐmdŒ%ÉJçje²L!1Z÷í!/;\ªÿ½Š…Ö}{fÓ7ŠÕ_ˆ7ó ßP¤^*Ý$!¥¬#¡©©‘È*•>‰u*'IaÖäxƒ„•Cœ¥Ö aËZÚº¾—\nko2ª‚:½Ñ¯Wù é—›-jÖ«Wû 9tPÛèiPjåjuUe“MçH¨ê\µõš•²ºBe3zSÀÊó+ÖϨ ¥(^òq_òõvKÜŠTÄ–-‹T6VÖÖrè`6rwÜ^DîΈDAQéå€ìYü£¨”¶¸ êúT0«¶ÃiÉ IÔ$:H3Û«Òü†ʯ4… H P XaìÌž½'ÛbbM-™=$µZ³æ!þ)"3¼ýp•e¸BÎ J]ºyyÔŸ*ØQŠ”ÞÖ¥ƒ^%VøGff!ÿÔ+VÜ!ðõ-ÐJ:2RuNŶõ].“HgϯßÜéåD7:|ïõÑhÇ-·u%“]4¼¥ ¼šÄÜ™,ZñR—lH$íêÆÚêZ­­Rz^.[» 륱Ìò$–DÄ6½åîõFšê{ù£‘vR” “LH‹x’ˆ*•w_õÞ]GÑaë#܇Q3h9#t—°±TG’ÑRÛY‹$9…ê–hZƒñOpÐßälT«ã殱ɬ=(ºšßP·¥º¡Æ›¬«vÖ+–¯‡ƒÎæ9-»…Ñ—ijBý8u›j±jêêÕÆóôîêT_µ_ì4•k_–©`þ‰’¿ZFKÆ ómDH­…ˆ¨JC)'G[þý§eÓuR¶¢^ÚÒv*çôj›²MZ¯£®NÇÖÔס;‘ó.•]©´«îâò ŸÓ&¼Þ„–ó{äõÖ€ÚÝP//¦­ÝHËN|‘ù3åEáö±Œ¯Cni¾x+:ü„¹/ží²©¹Æ&çj³ü€¡óÿFyüaÃZ¬®óŽ“y‰Ä\ãü@&©V‡Õ^«òisý®v[ÄÅJÂ#i›Kîj䢾Û¼uuaG­O®^Úés9Œü׳½ßv]!«o ‡ÓôDÑ8Wç‡\qÑýÀ îjÂ/©–&:¥Nºc£LQöøs6[ΟgÚ¾œM.Ëù:C™Lxg¨»;îêÚhç;Ãí.›­ ~wZÓjx±Û5}­étkµñs%úþ8û˜Ð"keÇ/‹‘í¦,\o9Ùê4RIº(æ‹§Ç þ¦·?A›w´µµÕxQƒ5æ"NG ë z‰SCÜk@;Ñ?yÝ9çàŸœ{.É8dçKtœ­4"^˜ŸÂæp‹‘®E"šlÒlœH§ýa.ѬT˜ŒHÃïÌÏFukRîáaçÔz/çÑhóQGÿ?Ömõ Cœ%z² ¸ý` „§×¼rKT&¹Þºí¶Û¾tËÔp½Yƺ‘ñÛ.Èu±| µµä9.o9Ê¿¯×AzªatõÝ23öO?…j'§Ÿè åá`HvgolXåWD¬#K&‡í U@;Æß؉œü5]h¸fäê¯ Øí:˜I»Âb5 ŠcgÂi×6ûKô2¾D°ø)\©gEw×–¶ŽpìË¥ ú§—ösÍ*»el$Ö•Ú64´5Å©crÕª¡áUzyÜ`Ý€à˜êé\îýò÷u÷g›é> jõñ€@Q0p)ÜY"±”¥¯]2è«ÕF'£µ¢o÷ØJØaZï{­=÷\tº‚ß ÷ÇAîÓo»ÿòDÅ)FÄtÒ=þ‡éèÈNµñgXr. ÁíW½S=¨u·HXU³RlÒ¸ôžÖ(FÎþq´aÇêe‰!Fµæ¢côªLiþ2•¢=ÒÒŽöÄ‚âcïÇbŒhAª™>5Ýh)˜¬ŠYÇ–N "µ'?ÔyѶ‚’°u–Köžu‰ªþž.þã¾¢Še™:ÐNïCÊ:AÛ‡‹v}D 7ŠÊVK%ÕMlâRÙvÛc¯q Ñ¿Š¾7G:‡q9ÿRº”÷v£Ëj¦B=‰€?ÎÙBz}À샥\4iu¹"6½ßA[KeŸh®+ÑÏñïh}?Q*”W¤oG¦}Ïp`XE9±|ÉH¯?dš Q÷ؼZ=Dý²¼ÛZÇ.ýä^ˆu/äæó›Š‚ïïdiE“,ç ¹ê$qdAôö MNõ ³›Ý­Û—t¡ÄÙO´üò“|GW.ž7æýß ‰büªróoÒTý}èË€¾¥°žÁs³V§3‹ÕR©ðØ3îŸnE¢.1;󕯫vvÞ¦z¬s¹ð¬Á‡=½±Je­¼Q[7¹#þ®/m\â[kõ¶§¼ëŒ Kx 7Ÿ”©/³S(›Ïæ­ÑÑ£¯¨.ïß«º¬ŸðÄÙlöسçú†¬9ß ¥MxB.”hV05ŠIBá·ìFŽÎ2è(ú._>ˆûâqFDçA¿}¢:"+ÕЂº<çô̺æ–umnsÏÐPù†Q$ão5áÜP¦}Âňzóíù$¿d®U‹PÀ£’©+Y©&1%äÎCcüìw¾sóدðÞzø·ÏމR š]»¬ÿKí®1REw£-)þÚbòf«Š_±D¬by'3Õ¬ B; 3{ŒS¬B»é~—ЫE Ñ!Àpˆ58›väþ ÿ‘†|qö·BÈãtS ãß 0z(ÆKèqŠaŒO(ÆË³³ƹ£%[@£b¼Œ¾A1Ü1Åx‰AÆ©ÆK%ÿZ”Æ«e4*¥ñª@ƒ+a¼\J§ó0<€a¥¯”0 ¿,Éñ—r PŒ×Ðý%.˜b¼ZÎ…b<ùqy]ˆ Áøû¢\Þ0ÉbqŒ—`¼z #pù˜b¼>û¡€Ñ&`n&PâNg\¬´~ÿ‚ ¾8XÀê»·£0ndÌÚrºbˆ}öw/ºè»äyýõ×÷õúD¾Þ_e2 C–qA$H ô ÖKÃô]?À»NÈÛúù¹+……þzàœ‡3O=•yøœBn¶"=zßDg  ¯V˜ i=Òw}_{Xù_½à‚¯Ò¸·ý FÔ…õyKimÞ"mÜø¥ ªÁ¶Á ‡Wü†ÿçPûì‘ìz#¼l¡7ÑUŒ^¬\ê[q±W1j ºœJ•]Ôu˜ ¿²€®!Uc0[Bú´©•s-Ðã!·[InÃýšb G›énn˜È´ÈÐ%ü5è(Ûü,3ÿ{’¶VHÏk6!1>øú"n¡qŒÇ˜…T*}ñ•éôL¼c ›ØôßöµñøÚöÿþihbbèO嵋Ò&í’µmÝ„^à÷Ë….æJüÀx˜99ºiC(ÿt„Yl+í$ÅŽŽ¸´T©Rá–ÜJ?¬äµÃ¤¼{e·;jq«s‘!Ÿˆ_áîÔ˜òW,nŒ¤ZÞA›gàÊŽfEØêéõ®0‡š‚}þ‘hó’àDÂá=ž”¨ÝÉ%cÉV²Ëª¹ ?­ª¶4Ó´Ødì]–fn bmæ¸f«Åã±À…0µ8ÒÝæf—£ÅppÁÆú§QýÓ<û´âZÈCøŠ¶ÂW‡¸“ÖÉNátJBÝ#„r‚’Ùe­W¡g®]þ"œ5û_¢çJ'â_Û-[4ÿEh"[—÷Ž«¬ÝÖ¾Þ¼tÍ@{&â0×îΟQXšî8¹ÙcËû¸¥é®;¯Ø¿53sW^¿ûþ5tŽlvбÊrûLÀäxwý…“x'™ª$UÞ¦Ú†ZUkKVZ%•U»55²eëMÒ b'†’ûÏ;'Õ¾±U„³OÂëÁ©öM­Àë7ü)èàÕŒ9Ù|(2ëµJ°‰?…}vŽ„=䌰¿eêË夋’;M •ÒÆ*fH]”½ó“ÕžîÙ[q„‘pbVêL–Ù^Ašk{äìWˆD¬„}¤í§«ÑQs …¿»ÁÜø_Ÿlg\ô%ZKFÇeÈ@ìÝw–ï–™D fÙž©w^B0oqer{sóö$Ú†;.ÌçI­&Ë>R»¨» çiŒäŒC4Ĥ@×!žˆQÍB/œ¥TÇå„E)gá¦úãûè1þ¡ï¿ƒN?3ºPW¢WÄKÔ¡–ÏÂ}Šž6°P£½ËÑŸ§øÇÏÄÿ8V…›ZmÚ‹qˆ~ͼB[ÑM´ýz‚ß T”Pþ®‚ù›02‘ÅJ{3œÅC¼Ê¹UTåíõMfZúǯ>ɺäü~‹FëŽÛÞF«¦³Æ3ц†öœ<20ñ•Ër±Ôþ/õGÜ}üÃ]-:YLí¢Ö{HâjÐ.±…;4J‹UI^Q^Qàe‰Àiqáü½DÑå}ç®M&ׂQø/¬M{N$fÝ»r§{bá«øo£Ç$Už™Ü)V\›Ù×ݽo&FžHŒEI*ÅD[´µMK«©òEâ~QäëzÊø/8¥¤å£8R“S¾jäåÿ„džWУ|ÿ+õ¨R„*Ot>ÁP¿'ÿ‹Ûq=Å—I(ФN’+H°ª‚OhУ¯xPÿgÏ+|?à;þˆ@ßIŽÊ:“H‰²ÞWøôW¼¨¡žPðÿ-¤Ž’d¨•T#åÿüVçAþ*Õ7øW<üŸ cÁUèYÚæ#¹”MŠ¥v„~Ê¿V•³èl¢· 8ÿ'ò"ÎVò¯¬›ÑÐ3´õw"±Ó®fòá9¼*s% þ|É Ä&‰Á:åcO‚hÆ’è>Füë²2+N}ñ¯#„#|vA( |ÈL–ÙÕ{XQ® ÅÃük¨‘M… È9†aKù!¥í¿'J5©b¸„œùè£o¡{øeè!s~&ú2$&@£”Šd!—89™q‚k¹W?u$þx»[òJ.  äZI g2ùF%°Ãù÷ÑGwÇ~Œîæ—£»ë‰$÷œ@š›QÈK§`ÿæéOE?ƒ<=#qNBCãyz.%±*rŒ±ãíèû$4MimÇÜ;wN1âxùvô³Ù\ú^~NÑn/ ùÑRþÞæÐ-·0”Úpî¤=Rzy?B?ÿh }Ȳ¼‚ÊV¨(â8å–8|R@ÝŒ¦©®îZà^:¯ˆÕ¼}Än–¼è•0ÊÏ+ÊÄZˆ¿¯€9„rT>%`ï*­’ÅËÝ-ÄÍþ/|ÏÏã—;v7^^âÇ2Ìz¬ÀaZæ¹B¾Ÿð”†P;Ñw~›'å7ß±1ÞØöù†ÃÐõ ­Gßæ#¢Ö¹ö ­­Ús[#Ñhl¶wÅ¢£¤lE™PÇÆi~v)OW:wu–þbË“Éå±èt29µgašÌNï+Úv íjkÛ54¼«-šÎå–‡BËs¹éP ×îó’E6¯¯ƒÔîŸý~ß\(ÉêRIîoÃÃñcocå±?TáӣǮnób:à ø¬Õ1²‚-»ƒÎª©ÇÎ:,—( •ú.oÊÞ¦Yw]2˜¼ôT;ÝÕMC‡rŽ–Ù=¿y} ÒûìïÏ?»Ä†žúüoÂG©M€®3‚+w§ZÄíºl81|ÅiÅÄ厶CgüþÛC‘¾—þx)˜ex?Jç.çfÚeQ#¦gJÎïH×ò «ÍÕPX¤I­NÓPÖpâ\kh¤Éi7Å-R1ÖÔÊU¡ÍÎ.um“[œlä·$û¢Ùßâ1ü FFf¼ðRâö2H’Œ¦7¹Z>…UÒ•¶Ó·T`p–AûJlÅ—òg›¸ðŽxé±’vôÑ‘Ö#ÇJè[À5ÑõVX׉§¼1 ˆ¦wgÔ“má~¥H9ÊL¨3»kÖ^³wKbÝIñ|>~Һ˽×0˜ä=¥TÍ$ ¶\’$¡” »@4Àå…Pª˜H„mû›ƒ=¡*GU¨7ÀßœîŸh‘YÕ"‹¹Ê™w»óÎ*“U¤¶ÊÒã}­öe‹²Û%õ­œ\듺nÊw÷TV‹ŒÉÌž2›SvY(iUWötç§`'²£è…lz¬ROGQ¤„¨ñ?Cv²× "´¿JÄ”âñˆWgé\¼³‚Ôs"¿«êv'ûu¹]S¢%§ät})w—zNÄÅÝþ±•g-[vÖÊ1w‡#Æž8m§µØË$™.fˆ™úìÚÌžÀül3„ŠÇ8+ìmžÈ7uÞÀÀyS…{϶¶¶m=ôZµ*µV{ÙªÖh6tŒUXë²>_®Ö*?X[[{_mmsàüÉBÐÉóü™}};3…;bl…ǶâóÉ®¶Z§Íæ¬mëú{½ða$ŒIã4.ì(s2q¦:>¶ìüM·¥ßRˆV!œ0 à_Ègj‰ÍÌÚLþï•••UUp+>§Ó+ñÍø ªÏ¸€xa=AakêÏ»w.éX*É—v,ÙéÎOôVs‘h~mϺéþþôÆu=kóÑHŽ‘Ì¾ ¥ã”9è„“fF˜™’EB»\Š_!¡HZ•Ž×QÖPÔ øLp¥­á’µÓhùŽóMèºæ¼½‡ëZº¢SQ¶õºÉ±–ô¾]§ìKoéZ¹NS¿{é²Ýõšu{*«Aïÿ†§U¯ÑÖ66Öj5úVXm©¬nh¨®´¨\°jýhUWgdÈÓ’pGR£‰@®gléÀ×uÿÃç &S—oPŸÒ+ô&˽eCuEµHT]QÕ¨2ÔVÕVUWWÔfü„©Ã3øZÆÎø˜¦·T^’…=Ýš,mß§·BŠX ¨ iP˜GRÏ{C¾-æ|A[&XY£õ„‚²K«ªƒ[З»pKf@¿'˜ÖcYc£é‘Ål²ÕãFøof³ÉZ§Î¥C-5ºl"]Y©RMu•2¹sDÞT]™Ndu5-¡tN½º¹¥¥ùuÌ™LnO&ÛK¿˜ Ø]éÄ»iÝ70nZ÷ǘeÌêÕþ²Œ-•õO‹ž´ OÐÀâ¤PCÆõ\²ôÜÞ¡ƒcÛ³çîÜynv{çÚ5›–/ß´ú¿fÜÐØ(CfÄ™MÜ=óÞäõªz•ºNÅÿÛåàÜècìæΞÃfÎL§ÏœÙp¸Çß|úŠ}›6í[qzóƒžÄÄÐÐDÂÓ-ƒ…Æ¿ôë_Õµ•IemuKg_Ðöî»¶`_'=ð-ìe—1 ‚5,D…X(¡!”Úa 4ï0~O¹\壣Ë.ÿWŸÂ§5èÂÖz_‰À¿n–Ø®_ºfÍÒëmÇþc;W¡¸·Ú­ãB!Nç®F¶+ÒiFÄdf¿‡µøA§?[ËËæ˜A+Ï©ß: ]¬m™Ü¶ŽŽm¹Ì¶Îð ß? ûýÃõ7vlÜhÉŽ8Ù”$ÓÚš‘¤XçðC­›r›ÒéM`x¸Ù7‰ø|#1èò¿›ÉœrJfïóæ¹JW{»«’Ë{!µZAZ=~^öÿ—¬È×µ=“ÙÞU¸‡‰Xáðp 0Üp饗^úqfsGÇæ¶6rÏ4ûG£Ñ?LšFGýüÝçd/½4{ü«7Â[?/õ<þ“çõ÷Ÿ79u°¿ÿàT'‰w'½;ßy§åwÜ''î±Ö­ÝÝ[[ ÷Æ_'~  «“•ød|ˆr§;!>#öÄj‚]ÍrÉ_wmËf·uî4Òašo…ŸÕ,n­¨lÛRŒþ–¶yÑGáðÒ4Ëò¿LU0ÊVãx¿àŽcœA&Ê$OPvJWN­$'©hq<ª¶À»˜ü.¼Cú$WÄÁm{j:ŸN¥‡†Òpérlðmø‡o“ïØOIQÏ!^ ¿Q€üos‡âË⥿ÔP[Û¹œ ëßäÛäñmðý6ÿ~ìtl:t¨à;ÿˆZÞ¢³¥,Œ Yg4™¤‹ êwî¼sðŽ;\¢L{{F”ÍÁÜË$šäòG»Ï?|è &Âpsâ/3bjÓMȈ—ÿ°ÿ‡üÚ´f\4¾mBQþMþH+R]vöÙ—Ñ™ï_Á¼ø÷±æ¼-ôl_%s3þÌÄL±U©ÈåòCf9[Å ¦FMø…=‹™òÛÚöщEE»¯m¤ ÎnT‡9³M­àšl™t0§b€Ž èØ€Î ÷F^ÎŽ¶ùÚÖF¯Éî ·¡ÃCÊ\0±5q µÍÌ…Õ!âÔ?,ñ.AGûQ˜à ÆË4]ÕÝc]×v_z)Z~É%ÀMƒ– XŒ¾¯‚o…/³72Ï1O¢kÉL *؉‹Ÿô0B:7HUµ=ämÝr}}ÒS)öUÔG³dÅï"܆Nbk‰7Áõ·r…Ü'@öàºÿ ” €ü Ï ÆÌ ôÞ%â—š?‚w ^Žð•Ônõ—(†·1ÿdkã¾ð>û¿ðþ}¿{'¥À ¶6‡÷_7/î ~?@·â¯–ÙÌ> \®.p)y‡O£ûñOò r+ö ýøOyZ€<ñùþ@ž ?Æý¨? ÇÈ£³F´žaò®à­ÿ›³nt*…¼Gp˜:¯¬GÓsV&ý‹nfœYÉlfö0_ ±ž×Ú–ý.ƒËO°û9à';gº­V©¬mPÈýô©TTÞ=uJe]½RÑ <§ëŠº¼žÓ­^i+<äЦ¨¨W(êëT oáu­¢¡Q¡hlPlWÈH'G¦Ø¡€ž¼ÑÁ_.L.Gç(äð¯AÁoÀrôuEá—‚ßXÀs Ê²AÉÿ @Õ6G´ÎNf(ÏÄmPËk‹~÷½_‹Þ‡Û®OÞE¾À3ËØ“ŠßRßr~ ?~jê ùv'0–%ßXøÖ”ŸrâÀ‹ ¯‘o÷ÍC@SøæõÜÅmž»’äÛ=@ó о¥Ì_sâÇÝSäÛ/æj )ðkjö,Áê×È·“qŠù2©;4™ÁäÎävMîâNá”ùâ‹Í»Ö6üs5Û]ŽuŠmÓ²M¶"ü„v×.í ÁšÀíÌP#ÔÀgÑõß^Ãd”©Ô8‰ƒ1ÃlhT©ÈoÜ˾>ŽMÁÓoµ µü"ó0ÛǨ¡Fˆ èÝ&c£\˦pÒO»*à'~Q©WZÌ¢–v¥AÙ"ÙYx9s¾’¦L¾]€ªðòæfòeþ%s!«)~¹Õà_¶´ú/„ýM-ï1áý†„< §™¡&C]“ÍïÅó6&Ž„Ãq [ò8íï²W8ÚÛö.?„ÜŸe®dû™Z²lLÛÝ-‰H‚^oÝøÙ¡Xs±!Âo=ne¾‚ß.ñ‹‚Eëp*oÆØŽ8P'6çq«¯Ûémkó:»}np;Âv-.Ùn`û0çñØP6æð³ŽV+‰pÖV„{÷3IÐ9Õ4\©Ê6™LMpá~ƒ¦ÉhlÒˆl/ãטf6q®ÅÒ~ÍØD6|4Cì˜+AKÒÓÖ'ë¯ ïß/ßÇ¥ÒP~1bö:Ä,ðé”6R :ÝcpŠÇ[õùŸÄ fS ù,í [øÌ•Ó„¸s .©Qé%qÀHÒßbFØM‹„h2OTvôæñ·R…ÜɆ°Ûáõ€´Wb³ô<•Vv‚SæÑt:J®bÊyR^O*åIB«^o±èõV tÔÆì)ŸN)ÒÖ!—ÆlÖÀ…Ÿy<á°Ç²éµ:½^§Õ¥›¡öƒ¯†BûÎ =¶(¬”“ §”î„‹äÚõøiæ-vÛ|Ì´oe*éiÆO+˜/« Q2 Q~_Ä‹&ÕRQ'§s»pôÉ.óÅ€õ>¶1&ü/FD°¢R©FÔŠê°íÍ_.è›ç@ß •Óðe³Ã¶ ?ÚfF» °ÞÁJ†ktl†AÒ-ÁÊ—_¦_žb8v˼/O= ñHAŒ×.ŒñI'¥—n† §î4< „‹Ä8 1Þ¸0Æ'oiž9msj%~úó%\sh‚ÛX;#c E]@UUåÍ]¢‹…EºÕÒ³a–ëJª \¡˜L-QõMpGÝDòhÔßد€¶Ð,Ôå´{û+‚ñ@ ! JzU&“ .ªCìѨtȃhÂEuW t׉YÁ¾ˆZJu¬dSŒµä㉼ÇX×bö5በ4™‚ Õ`Ï€ë™ |ùî…5Pb%5†Ÿ©ˆƉ#¡^Ç1&5 BomGÞF£®Ú^§¨×ªq¬‹ËÉ-uަ°C¦€„jig¡–Ê…3å´7köç:Å& NTD|ÁNŠÂz)„ø.Ž0QüQ!žó|C£¬¹ÚV/¯×j"±èCG#aÕÅǯ¢N}ÿ?Tß}V 6Ò^b¤sö)¸;#ÿî ö>M[gø>Xø.ÆS‘;ÿý4 Ðð¤GzRL- Ñзâ¶UAÒOþ'z„¹Ÿ'h“¹-Ĩb²BÚ^U»=â®ë?êÆFGS:!åg™$p#»#$$Jj¥ƒ$F2™íèȺ,×,NqlCœNÏ!"Á-³~fMIB*ãšžHÏ÷ƒ[¶ ߥðý–Âw*¡RÓóLÏ¿·ƒ[Jß!|µð0 8@¤‡¤üðFïÃRbÍ3ƒ¤}u62ß„¡Ë0Ÿü ;fÌ•³7ä €ü WÎ2ÌŽÙò4@~Ôn52Ã…˜JçYXZ‘ ظÆöØÔ{.±Z¡³©Íê%mÞ#8ˬƿ%aäó¬8ÛcKF œÝгœÕç¹Ej¥\Á2ENdc§¨´ÄË §Ð œƒrS)T’ù ³ Øý_…¼gÖÍœ$¬% çåʇIµžtPÓjp˜åõM5ʆÀü!W»B“4h›*+üµ@á®Y;ôèщ(Ô{3A}³ÆiVÕiªÕ ¡Ÿ®•:ª*Bµr¢-adÃì²Ð¹ã$-þcªG/‚xí}°X¦‰#ª½¯~ïòË׬!9zò¬™ùòìÆY[ïä¤Y sãìcù@~A!r€<7ò3€LÌZ˜fÈWò.ékÍ®`.˜½ Ð×:†Äÿvv5Í^Md"{ øßâÐ]--$Å:˜Ç˜§P/iû’‹ö2W[Ân‡ z–)Ö¶„]ÅßAçÒk­LôÃÓg'¡ö*Ô£~¡tÅU$ç¤ñ‘:ui*^Ç!&Ê•¦‰©¾¼·ƒ~ý›€÷oíÊ|÷ìrô­ÙKJ¥¨›Ýsw,F¾´`z ¿EòÍkÁøñ¦“ß;çœ÷ÁêíûÛ¶½2?äi!6† Ôãè%v”Óÿ@š\²ÿ=Ç¢~DÈÜô5¢ÒP[@Ú# dÐsû¿øÅýP"*p F…/¥ñ™Ûl8©ó$ÃFÑù»ÃÐ"[gf¬½âöÓH)òCï\öÌãì¶®ì]i]):c𛂱×ô´¶K¼Ì¾š„Ù|þ°ºŒÏ!ni¸ÈG¦•„9ø¼»ºŒÏYƵ¶"óJ' sðù"k0†¹0Ikùåbö9ç ›¼¾NäY8£‡~‚,Ë–Y;{•ðOäŒà°¤º‚½ÿóp(ÊÙiöX”2Öí.³Çª ?‰äK—j;ú5š&•Øk¢8p8 âp‹çÅ›:ú^gZݹڴFtªÂ)ÍÄ„&;•HR÷ ÈÅKޱÚ2Ó;cY):ÕØu-¥! mS‡„P–Ç¢¼Wä†3i±Câòx\‡8-0îKZ«Tf³ªÊš¤©þ¤úW?ZQÜÑ!ˆ½^ŸGå¡úší*‹EUaoÊÊwYÃ|jó¬î”òîôJ¹Úä¨HgZ„²#åÄ0—"Çaìrº¸b)ú{øS¨•òé´ªF­ÅÔš…RUljë2ìÁ6ÎÁÍ¥› ¨Õ—¥´0(¦˜™ÜÖ˜V iÕWV3¡¿²|}lñÐŲ’ÛJËŒ6=KtÝݺ%šŽúÙ!¡×¹KBÂ|ÁQãa;™3ØdÝâ…Ù€'çãÁìÁ½ÆC.2ƒ0jÝê'xt–€mgdŒr‘±„!“!‘&`±ú5¢¶äMçø÷.l?ލ 3ß ;pŽ€t¼ƒ’°Ï†G·ÿBÿYåýÿu[§þŸI4äÜë?N"ó^÷ÿ_‰V[ÏòÏ“Èt*Õ'§€D—³ä—ý¸¹”Å+}¸ÍÄEÁ@0 ²¦¶A»Eœ´‹ð§ÍM…Æë…[Äü%P.*±#"<ªe^-óU;NöEÙvwBµ À?xtŠW8/ä<:·[WáÉ]ZG ꦤu„´ƒ´2-–öåõå¸\«Pè9·4“Ï,Ì‹Sl"9©ìvºs¹òÄL·0fó¹ÌËŸ•Õr­ÅQ U³<—V/¨Ÿììn<È\Ê-ÖH.è ¬¡v¸4šS¯°Ö žñ }(äÚÓÖø¶\W|!”·á×aÜÿä硼Q ¶ãïù¹N'.øuÊ ­9É¿ó`ñåsô^’‹ô^V¶çÝýÇ÷^^¬{UU|ù©!]$5ì LªŽO P-oÿÿH é"©‘²žï­>>5¬;ý/”§ÆFùzÖ ÀBüYË-íǪ̀õp^[“¬‰õ`cîç›BÜŽãõ<~µÈ™ÐhÈ#~F1fÅç\Ú‹!iYK»Tm?.én o;>å.þtK)E5¦ãîÊ`ðJáw™¼yFJ-•æ¦N”RÖUFh£Œ«.L›—Ö†† köe>^öG?/ÛFëF\_¨|Kˆ›Z¯Ö­ß^”¿a.òlv€¦Ž0ƒ˜ñEæ#íº‹¸Ó’¡yS’6¯lU$.m±?/¹›%~´UB4yÑXÚ~Ävj} šŽ•f'#® fk¤¹6èpEÊg(él"ÛLS£”åó^ž]¦}¢}¦]žâTZæbý©§ê/¾ª4…FgŠŸƒ™â;OLŷͰW´×°ÍWœ8{n»qfƸ}Ÿ¡@…Î.ÅAfËÚ:6€çf ˆ ÉhÃæá6ш¡iw¦ß‰ãÛ ã¨8¦ÐV^¬¶tAæÂR°—(¬ŽxûR¨z­žö;"@!%Û$Ìm¦6Ô…†r® °ÎÞ|–Èað(µœ¡Ñ ²»–3Êè/ÛÈ…Ô•”Jé€ÃÝ.Aν%9Ëç·©¶<­²I?"Ê ›#·eú8îª0)ƒ¢(öûÍ~] ‰*ÐØ 4®81õUM†QÛ°9|„D–Òˆ‹4Hdƶ43$m…ÇàÕX4¶Ö }]D7kqÜ0?¶Ldv˜}æó½ÌDÉŠ¤EعIÿ”6rZ¬¥s2aσ¬TU,ðVv2ˆ¾Xè§ïôOž¾½±áôÉþÎI§s²“¼ó±¹÷x:ùúZÐZú|¡DKK‚¼ñx{d:¿tùò¥ùé̦ ºöR/ïX6=½¬c9ì©æg˜íD/õ¶ÚBétÈÖ:ØË· öß ¾¿Øçðµ…q‹Yâd¡gMñaþNþŸ(…V Jþa4RƒWô»³î=ØÌäì'ì𥌘©bTô4R"»EÎZäeS “x'’ñ:ösô÷ßç«ø/»Ìé¥ô.ÂWòÿ#â¿‘EuH·ø¯º2pÍ5+¯ò“YP¿ºDžf)œ%`ÅeÔ‘jÁ;=þÁW½²åäÑ€åíÈbÿŸgÊ©cF9û:[ ½»J:+&ãè9Nf'æø‡ÔÑ÷Ÿj@{EhoÃSü{ï݈ù¿Šø¿âãh³mæ¯sò×ÁOq€$¢~.ÄÏá+!$µ_B ñ<º0‚tüo"%û;8s0²ŸR›C¶â qt¬ î ðÿx:€@xÚc`d``³dèaàaHg`ò 3ÂOxÚ$Áà8°åÊ7϶mÛ¶mÛ¶mÛ¶mÛ¶Äøé£ŒÑ˘n¬1öïMfþe¦3K˜ÕÍæó¾…­bV'k›uÜzms» ÝÎm/´o9ÚÉî´væ:û‡Î×rwS¹uÜ~î*÷¶÷‡—ÞkëMò¶x|ågõûÝýÁþx¶¿Üßìï÷Oû×ýÇþûÀ xðw?HdŠ•ƒúAë {0<8’0U˜-,ö G†SÃ…áÚpgx4¼Þ _ ð+ø•@=Ð tƒÀ80 ,›À>p \À;èBsÃ6° gÂ¥p#Ü O«ð!|‹TUDuQKÔ D‹Ð:´ C—Ð=ô ›ãßp,œ —Æ pÜÁð¼oÁð|‡X$1IOr“â¤2©OZ“îd0Of“åd3ÙON“ëä1yO=Êéß4>MM³Ó´<­M›Óît0ÝEÑwQ¦(_T*ª5ŠÚE;¢#Ñ…èNô"ú ûƒÅa)XV€•a5XÖõaÛÙavžÝfÏÙgx ž„gàyx ^…7àmx>„Oàsø ¾…àgø ñ‡ˆ#Rˆ,¢€(#FŠ©b¡X+vŠ£â¢¸+^JC"ù«Œ)“ÊŒ2¯,)«Ê†²­œ,·Ë—*®Ê¯š¨j€£f¨%jƒÚ£N¨+êz£mMõ:ŽN¡³èºŒ®¡›èº£gè%zƒÞ£Ï|%À†€Ý¶V®˜}Û¶mÛ¶mÛ¶mÛ¶m[‰â(”ûÊkå;ÈòR hÚ^`˜æU`8þÁÄPŒ`.X V‚õ`+Ø ‚ãà,¸ n‚‡PT5F­Qw4G³Ñr´ íC/Ðgô'Âi1ÇΊ àÒ¸®‡[á®x ‹gâMx/>‰¯â‡øþC ’ä!%HÒ€´!=È2Ì!+Èr€œ!7Èòü¡ iZ*h@sÐ"´­C[Ðt@Ó˪©6S;©ýÔQê4u‘úRýÊb±¤ 2ƒ¥gùXIV•5dmYO6”MdsÙJöŽ'ç˜[<ÏÇKñj¼Çgñe|ßÇOñküljø"µ`ÂÙD!QN “Ä<±Jl‡Ä™W–”UeCÙVö”CåD9W®”[åAyVÞ”OåGùWK¤¥Ó¤ÖD›ª×kïõ4:×Óë¹õâz%½žÞJ_¬ß7¸Ñؘoœ5S™•ÍÁæóŒÃ*f µŽØ±lÓîfï°_8ºSÌéêluö:'»ÎO7ž+ÝЭîvv׸»Ü·žð{S½ãÞ;Ÿøý©þÙ yP:hŒ vOáV —„¢8‘•‰ºGk£›Ñ‚àV¨0 hC³mwñãêC¶kȶmÛ¶mÛC¶m<Û~ç$†Ñǘi0Þ›ÌÆæ^3ÒÒVok¡uÞúieÚåì®ö2{“½Ïþ"j‹b„˜"ˆ5b‡8".ˆ;â¯H•Õd=)dÙK’£ä$y\ž—×å{ù]þWyTUFUQžj¬Z«¹j©:¥þ©X•¡ó麴®¦j­ë%úÎrš: »N¸[Ú•îPw•{ßýç9Þ@o›÷ØËö+ùíýþ1ÿoP;èœ â‚,(Å¡Ô M ô€Á0fÁRØ;à\€;ðÞÃ/ƒ8ÈÄüXË` ´±5vþ8'à\[ðžÁx_áWü‹±˜I¨U¢z$( Ô™zÑ G³hm¢½t’®ÐzBèÅPçãÂ\Ž«±Á·ä®Ü—GòT^Èky'å‹|—ŸòGþ˱œ™K<ˆÄ›mÛ¶­ò:Û¶mÛ¶mÛ¶m[o[ $ƒLJA5`h Œ‚i°ÖÁ.8—༂oàa˜Óa.,†•°6ÂvØ ‡á$œ‡«pÂsx Ÿá'ü‡ARQ6*Då¨&)5§NÔ—FÐdšG+i í§St•ÐkúF^Ɖ8çâb\‰ëq#nǽxOây¼Š·ñ!>Ç·øâ$ñ$•d“BRNj‰I+é&ƒdœÌ’e²IöÉ)¹&äü?‰ÒdšIói)­ª¤Mµ£öÕ‘:UêZÝ©Gõ¢ÞÕ—úU=5Ô%ti]NWÔUtu]C·ØÝt–ÜjZCë`ýl¸Mµy¶ÜŽØY»f÷í…}´_æm!G<‹ D¬mÛ¶mŸ“còRÛ¶mÛ¶mwXÛ¶F5¾vãQ*”åAEQ9Ty¨'Ž–£ãè!úƒ+ãžx ^Š×â­ø!~IŠ’d2™ON7ä ùNþÓ¤4-ÍJóÒft0M—Ð#ô ýÊ2°š,`3Ùyö•ý`1¼2¯Ïûóá|<ŸÎçó[¡/˜¼¦¨©kÀô7 Ìsßü°Å-ØÉv¶]loÚ§.ƒËá ¸®‚ëêf»Ån½Û펻î§‹ Ju/X< 3† Â6á¼poxÒ@U¨ -A@  ƒI0–Á&Øà|вG-¢þÑðh|´;zܺtëÞ­w¶þÜ: xUúÞµmÛ¶mÛa¨ýõµå®mÛ¶mÛ¶mÛ<çC—?R<)™T]ª/ •ÆJg¤/ryšüFI­RÚ(”õÊ#5§ZI«žR 8(Ê‚Š &ˆ£Åè6z¾ãŒ¸®‡[áΘáax:^·àø ¾Ÿàø/ILR“,¤‘ˆAz‘Qd>YOŽ’«äùMSÓ´ m@[Ó.´+OWÒ]ô´–P««µÑæhƒ´ÚNí¾^Mï¬3Ý×ûê#õ©úE–‚ecEXeÖˆ!Ö eSØb¶)&YŒ³Íˆk”0'ÍXf-s­yÇ|j¾3¿C,H i +äƒâPjB#h ]@]¡ ‡ 0ÁjØ{á"Ü…ð~Y9­2VGk‚µÜºÆ“óüÜæKø>~ƒ¿)E&‘[eE5Q_´ Ñ[ cÅT1W\¶ãÙ%mjOµwØ7ì¯Nn§ŠÓÑá¬p®¸ñÜ<®âpºÛÝ»î_¯ WÅãÞtïª÷Ó¯àGþ.ÿc-èŒö?Š¡Ž ÷EÿG¥#mþuOvkÛ¶mÛ¶m[‹øi³±kÛ¶mÛ¶íöþoæÈ: \pÜïÀ7𦇹aX¶ƒýá(A.†[àQx >‡?PJ” •FuPG4MCa4@Ñô'ÂÙqÜÂ"^€OàËø1þÄdd 0™ÙÌ*fs”ùÏVe;³ãY‹ÝÌîf³Ø¯\R®G¸ùÜvî÷ŒOÈçâ«òíù‰¼Ãoæ¯ò_„ÌBu¡³a¹°_8/ܾŠIÅvjõ§ÞÓ§ý´Ýs–êô’d2›-4ÃŒR•Ù í¬ÑRtoÒg`·†¯”ejÐð¨:sz ùÂŽD{©ÐôÑò¶1-¤¸›z£ÎÑgÖšþ*‰ÖP®PÉè%•š"•EËT V‚þ“Z­IZm?¢ß¤¬’ñ%Í H¸²¨+¿u¯×€hºúPgóè=u0o¨Oô¤:9;ºUøãÿ’x˜Ú¤03”ðq ±kŽesŽRVÜAE.Æ‘©iò*Rë•–°žMÈñz‡2YcÅQk» Þ…Eð;¬À¿¹ó£ãa2dE…Ô {"{£ÐÖz]i5½íoë÷NŠ“I§ƒ}õ/Pz{؆ ûxdØÛi¹\ÿs{ÿáLü#Îß0MÏZSÜ™K‡;äHÖtæ{žFšyá.ø†ö_óv±?øÜðq£ÿh­ªþݦe @Üô߯Ý!©{¤9f¬ÊLQ3 Ã]ó×è’&/'uo5£OÐ6ÜeZ­<ôkxÀjø ði8ÔB$Sw_Sdé§­×J@©o¹çŠìºh_wGBÀ JOl4ÎÝ¥Þôg(Í7NS7ÜÙÑvž~P쉶ÑÝãé0ã)ßY w×ÿq””þ;Âó¦ú4ÅùÒÂ;´!L–ªý[6(¬÷MŠæ«›ÙGIì<¿çÙ_nOøºþ-ã¨R¡¯WM¹WÙï¨mjo³OrL9ú+yξœ=séÇ”i²);À.X_vi7ŸA~nIO%õjmM¿{kûC´Œôζ!šbs>3ÿå~ŸM0»(¼Ïîö5R?æGái 1·Ò×vö7Pn²¦…½éÖØÇ€ñ±– Ù‚ye³¿º¥âb}µ>†/Á­Š]_ñæt:u (s­ ôˆê)[£Z£ñ¦£F›‰JÄ#`‚f:z0ê¾]ˆG:ÜDˆñH}E Ô›LýΨ»+Ï£¢Öh ´Ö¿k=Uáò̉ØÃ ŸŠÝ;àïÞ·TˆÕ8s3íŒU+3Í.6¯ÛÅÑ&ÀÛk^#]‡íÊ¿‡]N\«55ú޲Ýéó)â{ í d-ÛitÔÊÇuèâ×ôµŠîäõ¦Þfêer5Åï‡×8¿k´_Ÿ¯‰÷=c×è.Êk2m•¹¾¢§IÓ¶o·{ˆv\=‡+ï+oO²ëÌXÛÀb?Wm£y‘tJ÷·?{ˆ¡ç$Û ÌwŸó:Ò«HcÛ Äb,Žg‘>J7³?;¢£l#4¯>f+H/%Òç)ãàû~÷í/¢Ýwi—ØÆGÒÞÚ›ƒÎÄ;œuäב/7ÒÐFx3ãV´±ß§v¡[“øTêžH¹ãУБŒÍ‘«8Þ®‹kñ½MÞMô0òçiF‹¶¶Z‰Îi¢ÓìÏŽ¸^%-:Pïw|g“wz zZCpkõamÓÖýÏÂþűAóÐoá]X ‹`Uà+X o:;ð­ÛWiaß:ÂÞû{„=ûgùË1% úø+p¦‡ûó÷\ÅylPW­SL€$Œ7 -Ñ»n-’è«'á>X3a8ކS»À¡™â¼Am.ÐBì±Sú„ýÙáÏs­f¬¿R?w7Æã¸ UhÎÁ稙án<»é½8 Û}Çji>S™¿ÿµjâhë`ì·4 Î@k}ùÌØµ±‘z¹¾<ÜsQCh‹oqw»;Ôý>‰•âkÉ{ˆ>jÂxèË÷¿>7–®’¯ß†òû„±Fú5ò/”o×ߟZkòߎªa|ôæ~šÎá»ùœ0Gð}{Û)WÍ;íßRÞ‡îî­7w«#ïZŽ© ïûR´»Ú™9heøý[€…ï]îoÿ6~©2÷»×ùM¤Qf º ¶d ®§ìB_¾ùŸQ¦ëËa©Mtyh‹78üî^Ñf;³ZþûÕƒñø¾J±ÇÓn‚:_¸÷ÿÄæ£0ö¾äïJþ(M÷íR'z¾e|m[&ùÌS–zkå¿ãSßѦ:Ì|=ðû¨…äÞIbÜ-€E¼Èóï–xãæcwR™{³˜Gft«]»þ»mPø&ÆF'Ç›k²³}¬]œˆI|ãÚ…zËU S!úAWè å!¯òÃY+Ki|¿vˆ²µ•çVåB9þiQ½¦¡#`öl”tg•ÀÐèôtí¥‚8S-?SA›K|>íúŠ*”`ô‚ÖQ…]ŽfC·T:è*´(j$–3„t¹’ ;@d{ÝAí´BÛ¡¼^§‘Ñ Œ G‰©`<§h;îà­bê›[´äFw«?”§Ú[¯óÐyh3¿ùIžUÁwKos¤bèÌYñsÆu Á6ì %P4)´Xß_7U^w«²þv½:öÜþJxÉ™¨Äž‰šèeu§N•nR¡nå›<‰}wHåW:Z†Ý_Ýõ“ºD‡Ð×Dή³«Ðñ­ž´××µ%]Fz޲Ã^¯‚jè½aL4Ø~ï4ø;@»h¹]ŽÖ:Ê S(³I°³•P¥­m] ¾½µ¨× ¸£ÆÅ0@ÙqZ­‚” ־0¤y ¶'K —nQ+ïs{$Pôq§itp å/vwbݶðç-Õ‡÷´ ¾ÉÄ»›¾‡™¦Dá¿ Þð Üc½ÌuªcN‰è2õä¼–š}Õ !/è>0v‚ƒÿ¾ý Ý&™mµŒm`“@Œý =ͶöG´†ðNnæ0»IÑ Ä×ÚZ6±É³ÃWð+ü€¯$´=Úãk„ßLK ']áâÃL@göQ{è%~ûyæÛ+LB'Ã#p˜C‡r_õT+}­~úÚþèÒá;ãH8 …káø€ͦ*•ìl8 n„z¸¾„Ýa?˜SaÌm¢»Á°+ì/ËÎÒ΃sáj¸Ô·ÛÝ~Úþ<0*`Ø£IÛ¯Â]ðvhë>¨º€kšñr¨{\ ÷uÜ7…>OsÜ-0˜Ûï?¡C;'¶ùMbñv¨CœT¨·Œô̵ äµ’«£aóšù‚í9fñMéA¡~*}ìÒ){OvÐÿ0×½`›ÐÖÜP~`KñOª„\ØzpÖzœ]i7è’4;èI’鯄¬Äÿá%xÚc`d``aøWÀÀÀ1ãŸÕ?+Ž|  `³{²;xÚ-…µÂï >¶  ÚÒ #0 ’ôÙ.+¤Š'Oî0Ê—è[¯À¡±ÜYÈtÔ1sSõ'šF›õnsaÅ$ÏìŽ|'H ±†À+h ÐxÚTÑc˜&I àª$k›JWo÷w\Û¶mÛ¶mÛögÛ¶mÛWÏxâü~•Rä]×ȼKµwŸÎøQÍø©”(¥ŸÔJROªZª‰j­†ªIj©Z©Wë­z—Þ§é3:Q?¤ßÑéÏôWúýTƒFІÃ8˜Ë`%¬‡½pŽÀ)8 çá¤Àp7<Oà ð.|Œ%° Þ„·`]¬-°öÂÁ8 Çà\Š+q+nÇÝx¯`">€Ïâ ø ¾†oà[ø~@¥¨¢q´€öÑ1:G×(…n£»èAzÞ è úŽ~òB¯×Õëáõöúzƒ½aÞqï´w#â2\‰}¾™›qîÇãxÏâ |ˆOò¦‚ñLÓÃô1ƒÌ3ÌŒ0+LªyؼdÞ2ß™ŸÍ¿RHŠ ‹•HjK=i M¥•´—.2GVÊ*Ù)gä¼\’+r]â%YÒäN¹Ç/à—ó+ùâ[?òkú#ý-þÿ²ÿ’ÿ™UmA[Ê–µlUkìöfÛÈv¶c섃ÒAåÀ¢àæðLx=¼3|8|4|:|.|#ü4*UˆšGm£ÑѸhb4-V.Ö76>öYÍÊ5ÚÍ.U¿w)íZ¿¿èßÿþûÏ‹ª­šª6j˜š¬–©ÓÎr§Þë,Oëý ~F¨?Õ_êŸõïPÙY¶Þ0ÆÃX«aì‡ÃpÎ8Ë‹·Á]p/< ÏÃëð>*,…•1p–5el›c9?Ãr½³Ü‰{ð^Å$|ŸÏg©¨4Õ¦±4—vÓA:MW)žÒéNºŸž£—éCú”¾¥3,;9Ë^Îr`Ž%pA.Ê™¹×å–Ü—òDžÊ+y§³Ÿa –eœeË9–£íø2,«¡³<^ “ûåÎòugYÚY6ZeXNˆ¦fY~ê,mͦ9–ÿN@“3aîMz’ß¶mãlÛ¶mÛ¶mÛ¶ms•|(œÍ¹·²©3žªFŒ™Al,ù6 ×‹V@¸{ÈóëР§y>B4VÑ£õH=ÆÛ3P÷Ô= F{e×ÃÇX‘¬ÖeÆ"i'd0†ë;ú¶¾ O9)Z~w™ø Ñ¢»3äNtÝO÷Öót;=/ î儱@Â|Ñhш„l YÒà9DÿÇ}œ“Îi§¦SË©‰Çˆÿ9þ'îÏòŒwâÞv¿“~OâÖuÿw+K«à–¯ä–tKHus¹i§Ó:’Òý^Ú$Ñ §—›ÕÉ„³:éeÛÄ?t>w>Œ^¢E:ïÈ>Ë1B›áš ;…*‡jIÏú<ô‘äÁJÁ÷ƒŸš{¤Ù‘ÆG~).v¶DÒ"#; @ ^t0,ã5ÀÜaîßeîßcž5Ï›ÍËæUàK|Mü܇ɘÂo)˜îþÞTÌÆ|¬!­Þý}Mð¬#jÄœðØ…ÝØ‡ƒ8†“8…󸀋¸Œ«¸…Û¸‡û pè€'Ê`˜Q&òªzI}¡¾R)T*•ET%ë+•ÄûXƒ¬}v2»]Çnf²ÇA°gÙÛÄw@ÜéöøÈ1¯—ü\wÿø Ñ¡û[AÏOA$þÞc1‰ñ÷œ°ƒöYÉÛÀK±]Òý¼‰g`»^˜…9YA}Ìt,Ä–lÅÎêæbyæd{æfõ¿úW%aNbkvd•‚MÕ |†Ïñ¾ÄWøÚH˶ìÀv*̯¨ÔÊ`'vSc8’aÞc˜`Å€(ŠÝó0Û¶mÛ¶mÛ¶mÛ¶mÛû˜m›1¶¡/ðß[àú¢>·[åVû馇íZùen¥»ïù¸>žïª*»r(§r)·ò©¼ªª¡©±šj¨†i¸FâµT«´Zk´Vë|&íV ‚¬»úFV…¨D#:1ˆM2‘—|ä§ 5©EmêDúÑŸ dÏÌx6³…­lç"Á\Vb*/±T–TªFfU'‹j’M5ȪZdW}ò¨…ÔœÂjAu¤¬šP@ÝMaõ0EÔÓU/SLý©¤!ÔÐêhõ4šúš@c¡ ¡¹§I4ÕdšÑRÓh¥´ÑtZk5‡ZDW-£§–о„âŽÖ3X« Ñ^&ê³t’Ù:Îe¡Î±HYªó,Ö Öê«À&Ýa›î±C÷Ù©ìÒCvë='õ™súÎ%ýÐ_n·7YEzu!¤÷ê§}LÒK«q5–ÚÄP=b³WOا§ì×3PAI§Þ¦¸Ž0CÝL!-¦›òGH«=LÐ%–é&ëÃ}Uâ9xÁK…×¼åïùÄg¾ðÕDåŒ1ÎXÒ„2¡M΄7LDÉD6¹øh0Þ„0aÿ/ýé^¸÷î‹ûæÇùE~©_ì—ûù~‰Ÿê~¹Wîƒûê~û•~_ï7úÍ~«ßîwû½~¿?èû£þ„?åÏøsþ‚¿ä}öwü=ÿÀ?òýÿÉö_lWÛÝ4±LSÓÌ47­L3Ñ´0­Í3ÒŒ2£Í$3Ôô3ýÍ3Ð 2ƒÍ3ÆŒ7cÍ83ÁraíaÎqáíQÁsíqÉžp‘íIÅžrQíiÍžqÑíYÞs1íyË^p±íEÇ^²—]\Ï^qñíU—À^s íu—ÈÞp‰íM—ÄÞrIím—̸äî¡{잸î¥{íÞ¹î“*“AUȨÚäPrª.¹TÜêLyu¥¢ú˜H  ²QMƒ©®)4×TZh&m5‹všM{ͧ“ÐY é¢åôÒ zk%}´‹q:ÊL`®N1O§™¯3,Ðe–ë +t%ºÎÝb½n³A8­œÑ'Îê çõ“+nc ÀHU6ª ñTˆø*L!¡Š’HÅH¬â$Q ’ª$ÉTŠä*M •!¥ZRT­(¦ÖWJ¨-%ÕŽRjOiu ŒÆÑPãi¤Í Ó†k+#´‘ÚÎ(í`´v2Fû™¬LÑA¦êÓt˜ézÎA½à^qD¯9ª7Ó[Žë'ô• úÅUýæšþðázHØ Z3Ù¶í]¶mÛº@¶ÖÙ¶mÛ®/Ûý¶ùnñDE,qÄ“@"I¬ ó¬eëÙÀFÒH5›ÙÙÂVv°“]ì6‡9Íc^ó™ßd{9ÀAq˜#µ°E,n KZÊÒ–áÇ9ÅiÎp–sœ·²U¬n kZ‹K\æ×¹ÁMØÜ¶¶mmg{;ð˜'<ç/yÅkDc³šËܵ˜U­f}ØÒVv¶‹ÝíaO{ÙÛ>¼åïùÀG>ñ™/öw€ƒâP‡9Ü|ã;?øÉ/~󇿎vŒãàD'9Ù)ü'ˆ`B%Œp"H&Å®v²©öe›YÅjÞ8ÃéÎt±Ëh?YÈ‘lc;ûØÏWç:ß…–µœ¬hy+q‹œà¤ó\à"ÙØ&6µ¶ulh3òˆ;Üå–K\êlÇ:Êqvt*W¸ÊSžñÏ9f0‰ ‹ÿ«mÛ¶mÛ¶â¤æª6¶Ý¾»õÍ̹ÖÝw´¡MíêI§šÑ¬´¤9ÍkQwº×¾O zÖ»Ît®½éU†þˆÆ—@¼‰$Œpµê— )"…TÒH'ƒL²È&—<ò)`Šif˜euæ˜gEzf”R*¤ƒv©âÙ§TŸúÒ·~èg@&™uÌ>²ÈŠ ³l²ãÀ‚U9±a—K‡89ä‰- áâš9çҫާ֧ΫÁ«Ñ㟥z€a- £(|ÚÞ3¶­gÛ¶mÛ¶mÛ¶mÛ¶mÛš•ì¿É—µã^áøÅôítpÎýcBøW‚‚ô? ŠÑ(L£QÅMü¯4ºi ŠÒ˜ˆÅnCcKÐÆŸÆ• /'A%_‚<4hB¤bg¢©‘†¦ŸŒ¦Ÿ„¦Ÿœf 0Í(ÁTš ¥F ÚѲt¡å$hOË‹OM+HЂV?”VBuüÎ@k¢5þC :жt¦í$èHÛ‹OK'š&IPˆNÐ)âCtªè4ña:]‚®t†=éLñãé, ÆÐÙ\¤s$¨@çŠOOçIP‰Î—`<] ~](> ]$>]Œ%Î3éRñÃè2 7§Ë%èEWHP“®?•®’`]-Á%ºF‚Št-Ö;ç§$(B7b+’a›I‹í&v˜"؉ݨ€=¦ öb?ºà€éƒòô™‚Ã8Ž™8!>!=iVáΘ³æΙ—8/¡è Í q•ý½&¡_éu Ŧ7$”ÞÄmvzGBè]Üg— $T†>”ÐYúHBÍécW”~láqÅè§&1>W‚~n’â qÅé—& ¾W’~m’áqåè·&5¾W•~o2âqÕè&~Wþl2ãq è¯&~W™þnÒãqÍèŸÎDÿלþ-áÌôq-è¿ÎBÿ×’F‘pVU\;ÍDtqi S1Åu§±LiÄדÆ1eW\/Ï”C|qýiS Åõ¦‰Le$6Ÿ"‰¹ˆ¤âÒd¦ ’‹DS˜ªHij •¸¡4µ©‰4â†Ó´¦6Ò‰FÓ›ZÈ n,Íh “¸Ñ4³©,â&Ò¬¦ ²‰›J³›È!n2Íiš!—iƒÜæä1ç‘WÜ<šÏtB~qóiÓÅ- …Lg·†1PTÜVZÌŒDqq;h 3 %Åm£¥Ìh”·‡–1PVÜ^ZÎLDyqh3Åí£•Ì$TwœV1sPUÜQZÍÌBuq§i ³5ŧµÌRÔw‘Ö1ËQWÜ%ZϬ@}q—i³ Å]¡Ì*4÷ˆ61;ÐTÜ3ÚÌìEsq¯h s-ަ­Ä} ­Íq´1!´5'ÑÎ8´7'ÐÁÌDG½Å•§ýÅ5¦ÌÇ,ÞÓ!âÚÒâЉâÖÒIf ˆ»CšMØŒ-Î…“Ó}âÒÓýæwׇ4åqHÜBzØtÁqWéQ³ÇÄ]£ÇÍœwž4kqJÜ zÚ¬Ãq7éY³çL~\W€^1qq]\+zCÂÙèmq›é3OÅ5¥Ï$œ‘¾7‰¾2MñZ\:úÆü†·âòÓw&Þ‹[M?˜þÎ…wþ*¡©ô7q'èïšKSHèÍ$¡Ïhf ]¢YĤ&t‹’ÐcZEØ·iQóŠIè-Ž’ì´ʲŸÒò¨Â~I«šïQ 5tœáš&ŒZ&@mAÔÇÇh€¦øÍÐ? ÚãwtDWü‰Œÿ0Ã#MJŒ’Ð:Ö„0ãá1Á|ŽI˜Œ¤˜j`:f !Jp“.’à ],ÁmºD‚Gt…ÏéJ ^ÒUâûÒ5Xë\Pœ®Ç&ç|?º{ÙÃéAb¢'ÅO¤§ÄO¡§ÅO¢gÄO£gÅ£çÄO¦çÅ¡%xK/Ip—^–à1½"Á zU‚wÎEœø4$~6 ‹ŸG#âÐ@ü"êÅ/¡‰_F?¿Š~"~ ýTü:ú™ø ôsñ›èâ·Ð/Åo§_‹ßI¿¿›~+~/ýNü~ú½øƒôüÃ>L£ :û !þ")þ2…Øì«4®øë4>³oФâïÒTHǾG3‹L³ û)- Å¢%Å¿ Ä¿¢•Ä¿¦MÑ ÐÚ4G{Ó]Mt3ÍÐ]•iÓ=Ñ[Ϥ¯„kÐþ®IH¸6(át&±sÓ)˜Å.CgK¸:#áZt.°»Ó…Ha»Ý,á.t v±ÛÒÝîM÷H¸Ý‹}ì>t¿„ûÓƒ8ÄîJI¸=aj㔩ƒ3¦.Ιz¸dê;8|Í^H¿Aö@šØghe‰l¤UÌçh,‘M´‰ùM%²™63_b D¶ÐAæ+ –ÈV:Ä|áÙFG˜o0R"Ûé(ó-FKdc¾ÃD‰ì¤“Ì÷˜,‘]tŠù³%²›Î1?b¾DöÐæ',”È^ºÈüŒÅÙG—˜_°\"ûé ó+VKä]c~ÃFlÒõ6›ßqÀüƒ9L™?qT"Gè1óŽKä(=aþÆI‰£§Ì?¸,‘ãôŠù×%r‚Þ0ÿá¦DNÒ[& îKä}`¢â±DNÓ'&šsšp|šE‚Ë4‡WhNäb_£¹%xMóIðžà-(Á=ZX‚´¸ÿ'É@Eσ(ŽÏ¬mijv\ÛNkÛf®µmÛ{¹æ³­ëûlÛ/¾9}«O']üjþÕœ©¼º\ãÿ£òzlµ®òsl5·òkó›+û`«MÚqiå–ðάm—Un ïÏÚqmåŽð¬à.U;Ãû°v{Uí ïÆÚ îXµ;ò¤Ê}Vî ï“âñÊýà}RûýªÀçWŒxªò¤ßr(Ü«ê0¤ß~8âéÊ#àSG"ž©< Þ?u4âÝÊcà½NÇ"Þ«<Þñt<â£ÊàýM'">®< Þåt2â“ÊSà½N§#¾¨<Þåt&âËʳà½Ng#¾ª<Þñt®Ñ ëit¢z é·SáÙÿ¡‘=™FBTöt„`ö/¾Ïu…؜ϧqŸ½˜&Bbî„£I’‰¦Bjî,§i‘½šfBfîÓ,ÈÉŽNs!7w6Ò<(ÈÞJ ¡0wÓ"(ÎNNK¡4wöÒ2(Ï>H+¡2wÒÒ*¨ÉÎHk‹wŒÖï$­/Þ)ÚP¼Ó´󨼴 Z² ÒVhÍs´ :²/ÑNèÌþJ» +÷h7tgG =ГJ{¡7;)탾ìô´ú³óÓAÌ.I‡`$»,…‰üY7é$Læ¼*‚™ìštrç]„Åœ7¤K°’Ý”®ÂF›°™ýnWšnW‚nW†îï'Ý)®2݅ݜϥ{°—=•îïÝ/®= ®'=(®7=$n(=,n="®:=*® =&®=!.ˆžWŸžo[o!=ƒ³ì•ôœxéyqýéqéEqƒé%\fÇ¥Wp•€ÞŸÞ—ˆÞÂmž7½ƒ»ìÍô¾¸ô¸Aô¡¸!ôž²sÐgxÎNM_ˆËN_ŠËC_·§w†¾Á[öqúN¼£ô=>pý(®/ý"®1ý*®ý·¸&ô¿âŠÓÿ‰kMÿxþýô¾³/Ðâ½¢?Åõ£¿Äñ¾Ÿ(¼„ËD#"2;3 ¿1 þK;Dü4ÑØMhtñ[Òâ¡1ÅïEc!6»#~Wüz4â³kÑâ×¥‰˜=‚&¿5M!~EšQÂe£™ÄâÐ,ÈÊ.F³‹• ¹ÁÛͯOó‰?–æ"-€‚ìÞ´˜X ZRüq´”øhiñÇÓN⯠ÅM»a{ (þ:ØÛéb±pt©XtºL,<]•ìtµX$ºV,2]'æè±º[ØQè6lgÿ‹î ¥»±‡D÷‹ñöNlb•i¨XCü‚4¦øói"ñÑ$â/¤)ÄkL3ˆU¥•ÄRÐʨÂNK«¢;­‹ì”t´Xz: SÙÕè4Lg7¢30›]‹®kB/á*»})áø{$ +G=±’Ô‰•§áÄ ÛQÅ*Ñ`±Š4D,#.“ÆËB㈕¥ ĊЄb…ij±B4­X)šS¬-%–Ÿ6ËK‹e£ÍМÍc’zbc©“piXAKÂ¥¡ñÀßÇŸL!1wJÓäHÁ´ô/6o‹ä‹°˜;Íéð>é·¥ëÄïA·`+wZÓmØÎy{º;9oKwa7çé±Îtö³ÛÓ8ÈÎôsÞ‘ÁQλÒcb]è ±Vô´X7zFl =+ÖŒž[A/Š5¥·ÄïNo‹u¥/Äçû”Aæ¼;.6˜æA^öšOl4-ˆ"ìá´‘XÚT¬/m.Ö‡¶ëGÛ -›Ï·)CÅzÒ*b-i3±AôxŸ÷ù8ÉKüI4 ’³gÒ<ÈËæ1+ã‰ý ë%`.Ý.|[[U?„ýØ.^n÷œÔÁçœçþõ[Âeüœ1 Ö<ÚíØ‹yqõ_6„—ZC°‡ñòjŒ0ì¡I–_xÚb```gˆb`f`dá²1„ÍÈ äAØL | ¯ l ™ÃðÊfARÃʘe³1è1ž`X è‚®£ Â0?Ç€îëðÕxÜÝú•‰'“ý•pydpÆõ5abdl$Üú½ÛîXÊæ’ðÎ…Z#9U OœYÈ*çeœ•»¥pÕFãÜŽ¡¡¾ÔÙo´Aùyjèš›BïPã@x+©%•NAóPv¦^š9•D\òNÖª,¤¸là®Cgf*EëÌYQùZ% Ï=.ØûZ'åÝÔÀÈȆ]wÜw×+»ãþF)ný÷¿üòÊ]ìÚ“Ì4öT’ä³dÇBgd꣤R;” sÈ‹rÓ ãr706¶nש™cI.èÉ'I˜X7°aÝFÁú+ÿ”?÷øõ_ŸÐ[.xÚc`fƒÿ猰/xÚÌ \]Äñƒûö3ê¸fXÛ6‚Ú¶mµmÛ¶mÛ¶Ív'ûËdnΑ¿ù‹'“þü³±å AÊJyz¢Ri+íèˆJObÒ‹¨ "AFÈHúT¢²˜ÙLT¶Ê6ÜCT®•k$ÈS¢òš¨þ¦¿ãú‡ýSÿ¤ÿ¥ Ä4‘&bÉ ™X²kv–|𥬖c© °šVc©¯õym  XškszmCï­½y®Ãé#uNÐ ¸Háb],¦«t}®‘ »u7ý Âkzoé-öÛz›~G‰écË%j¹-·˜å³üXÀ ²³bô’Vš^ÆÊH° V+Y%–*V…^ͪa «µ­6Ö·ú¼6±&Ü6·æØÒZ²´µ¶ØÞÚcGëˆÝ­;ö´žØÇúà€ClWÃm8Žv¾à-/xko]½«ïîݱ¯÷Å>Gù(^ÇúXïp’OÂ)>gø œësq/Ä¥¾—ûJ\çëpƒoÀ]¾ú1<ëgñ¼ŸÇK~ ¯úU¼á7ð–߯»~ïû}|ìñ…¿À×þ?øüä_ð§ÿ ‡0iH†©Cj̲`¶ ›…f‡ÀUÄÇFc%D ¢ô…Ñ^ñhtE7ðVô ¿Ç~ýûC,öW,=e,%=U,ƒ¸˜ü¿Ôø¥ry_Gïñ#÷¿×ø¥ÆoôÛÈ”HxÚl޳âÆ_×63/2¦Öl®Ù¶{„´ä9ŒÙ®ÈøÛ¶ícŸv¶óãü…Ë×ßvæè~Ò»ŽîØÇÜý[Žd)FÚ¨¥`ÄÚ'&l}òa˜±÷æmlÁsߎ£mwÛ[Žîcõvß}llµý§í*¨í›c3 ¶š e¡å°¥ÙuØu×mô}NŒ];r7âDžÅÊFôÙµ´umØiÉ 7¼øð Hˆ0¢Äˆ3œŒd£ÃXÆ1ž Ld ’¤H3™)LeÓ™ÁLf1›9,c9+ØÈ!s”ó\â2·yÄžòšïm8Êd‘Uv%•RZË´\+µUuQ—uE×t_ôJôQ?õK¿õGÿ”§|¨PE*V‰JU¦rU¨RUªVjU§z5¨QMjeˆÀ# ‚ ¼[ÕÛ¶³mÛ¶mÛ¶mÛ¶mÛ¶/ý=c ÿ³þ‡VØÂöp€#œà ¸Â îð€'¼à øÂþ@ ‚Œ„" áˆ@$¢Ä"ñH@"’Œ¤" éÈ„ÌÈ‚¬È†ìÈ|Èâ(’höèˆÎè‰Þè‡AlͶœÀÉœÊiœÁYœÇÅ\Æå\͵\ÇõÜÂm<Àƒ<ÄÓ<Ã˼ʼÍ{|À‡|̧|ÉwüÈOüÆüÉ_ü/Nâ,>â+‘’I²c–Y^£ˆõ†Rz‚ ÖWJæ›Ì‘Ö?29;=\´I/ ƒ—6 “uˆ‘3øÁxf\Lü@’n™Á\,`2±r° û`›Éd¸ùä÷¡ŠÈ.”BݪIÕ¯ƒFh‡ztQõª?“0CΨúuªYÕWÜîØ¶Ù_ÇvÆ3=±m[_Mlm³óbÛ6*F½}×CðÌìïþgÕ:ë xÓÇÆØ$m 9¯”­BÌyeµ²>X ÌvkƒeÁ×À¢`2ÈàP@ŸC,¨Ž•ÝʬüP£9 ™e•2É è?q(™‘i6#Àš` °%Ø£®@ÏÃ*3±â—X+œ¹2ZµNßRæv2tÉ’ðãá\bd wBç“‘̆ÙÞ‡‚×Áwÿ ÀÑ»\&#¿z1ØɃ¢÷0x½ }Ü~äFpßWÎijˇ|FdÎC¿ }ÉmÐgÀ³à>:{ÀóJà %Lw€›ÀŸÁ!H†€©xµ³`žà~‡œú6tô0§ô]h/fˆsƒ‹ÁY¯¬ºáTÁù ¿óuÒpŸlW'ú2xÉ¡G²-˜F`?·r07è:;ôCèС'¡`4Hè½^÷Á‚>€×-QõŸï˜鮤ÌH>%ÛJ KSº4¹ïé$e&vï§bªÅµÇà¹pÍC1„û"ß’Œ’`WkOõ’¨• rT…j…”£Ô8¤%1·"Rf#Àp0Ô!í÷À!èÐÙ•Oò¿HBÖn·;ˆìn{˜Äž´(Â^²W)“½eoQ6MÄé嬗‰JQ4uÔ*A½´JÒ­R4]«4-Ð*C_i•¥ýZåè‚VyçîM8\wY‰ pªÌ©œJU¸+w¥ªÜ»Q5½—O¥êÎýœjðZ^Kµø=~jë}ý{ªÃùÕåc|Œò9)G)Î=”:÷PZìÜah©t‘.´L^“×h¹ô”ž´BFÉ(Z©¿š·h•Õ´ÚjÑòQ×ÿéÅDÁó´VnqSnÍùy~•f'÷à~<„Gñ!žÆsx¾”·DòR^ÍoÉFþ€?“¶üI’RIæI‚)!çe-ÿ¤Î-ÞÀ[M ï–1¦14]ù8Ÿ5ëù²¹Î¾gö 9sIFñÉÉcÚK‚[RLר¤N }µRå¤ÎØÜ4–¥­tV¦©“$ä—i¬#¨£'™´Ë ã×]œ”…æ7Y.kåùH¾0+å;ùE6ÊvÙ+‡å¤Öy¹jÆéZŒ˜PãÕÊlr˜“dÆ™B¦„)gª˜Z¦®ÑÒ´7]Í˦›éc™Ú?ÅÌ2~³Ø¬7+õzÏœ6Ÿ˜¯tõßÌf³Óì7GÕ¹h®›;æ1 žW¶‹ƒ…Òxp x~=0|LAïDè2 €“á?€Î}:\gx°´êµi7œ àQð·`1§/(;ÁñclC0¬¶+ƒ‹ bþÌgtNÐ gt°°òk8ùÀ®`z/a‡Ùá„ €{À1ö,ö™™lðßg‚¯c†4$ß ^T®‡ÓÎUŒZÎW9>g3a†ð£ÀÀ`KÌ0 ?ôGà@ôf:X\Y~cðÛàì_5D2#èCþ<ôËÐgÁmàîàk˜m¦2c ÉeÐðYÌÿ1òß‚ÇÀŸ‘ þáõDrzûÀ©Þf`<ü!Ðí o‚)X+Qõ“¿¸•>JP²½®V¹Á€ÒåŒRõì¨*„ü.¥¥hÊJù©è“£ù4Yd}”ÇYwÉ8bñÊ|ÕiÚ˼¯*ßPýÌ=‰êPK¥Øãö„¶Ù¡b•\ºr¼³õÂß\çøø–ˆÉ, RO÷ DŽ‹•¤ã‹QªD5¨¥Rsz‘&Q[êLT¥Q/m‡Ñ<£î mó "å\‡4:¬¦íÀ ÈÜ{ÃÉ}<À·Ð®„3:QéæÇÏ¿š²5ÿBB¡vý‘ÈþlÓ)·Ýh·Qœ½foR>{ÛÞ¡Âö¡;”Šº#Ü1TÑçÎGMˆ)ܾmß·ØíGösû¥ýÚþboSk"~Y¯®hÿ¶þ¿¶ãBßÿcoÿÆkò·÷ÍD²Ú!?¤p|Nãp•ÒÊGE”Nâ èwhb¡¯‚›ÀO)ŒB)„2ëÈ*¤c*Q-b|žË¡¥è>h)‰ÐRÕ4´Ô±ZüÇÇóÝÄÜß«2_ÞŠ›ãTäüè|?.ÙýdÁû+%Wäü'KûŽ›¥žö²§DÈѰ3?Â9ßS"riä'‘;=Y=…<%<=ÝœŠ÷ôñú=<=×½äÍè]í­æméõ{Wk¥y§Eç‹êçݯå¦V?SŸ(Í£ÎzÞ{QqÞ{Zm/ëcuÔ£¨8­~ÎìZù¢Š(kµÔöù¨W£zhëÔ­ QW£Ã£ói• ¦BÙpФ§GøpoºÎ¸17á¦ÜŒ›s nÉ­¹ ·åvÜž;pGîÁ=¹÷æ>Ü—ûqÀyæ<“gñlžÃsÙÏoð|^À y/æÃ|Ž/á|èޟ΂pêãà OÌç9OqÊKФJ#i&­¤´—ÒI:Ësò²¼"=d€ŒÑ2I¦Ë"Y,Kd©,“å²BVÊOò³ü"¿Êo’.äw&³Éb²šlÆgr™Ü&ÉkbÌ«f¶™ƒSŸuæCóùÁüh~2éf³Ùbö™ý戹j®™€žêx]™q²ãsewtrq•s•wUpupurtÍ´Ö䳬Ÿ\܆rs[Jàß³Wá‘\GÇëu¿^ˆ?ጘ™—™™)(æåc˜™“eÇÌÌÌÌl‘Ù|ŽdŒ5ÒìTþß,ž²·œ|øuõ¼®Æzõ¦u’!_2•â7UÅ‘ÙYfjQÇ~=¤Ä4J”³V¢Ä83øöÆÕ L•í“$ýT’5$©ìÏÒyø:Þ@ÚÑN ’;Dî0FÀçŒ"ˆ1Œ#„kŸyCLÚÑNté€S†rTè€ýŠvÛJýÔV£Y¶­Ø‰]ø>cÿLÎ~ÐnoŸ~*G$Aû%I#Ož®É#YÈF Ê0M&Èt¼FîëxmhG:1È5Î÷ñ0>ø‘€D$!)HEÒ‘Ld!9ÈEòQ€B¡%(EÊ1MŒ™Ž˜‰Y˜9˜‹y˜XˆEXŒ%XŠeXŽX‰UX5X‹uX ؈MØŒã'ø)~†Ÿãø%~…_ã7ø-~‡ßãø#þ„?cöãâãŽâŽãi™`žÁ³xÏãCÞ½›c½À'øƒÃ8Bb?ˆ$”Êg*q)–a9V`%Va5xwfV¿mA+vbvë ý·øåÇr3Á³=`fõH:³#GÃ2…8M<™Ž:"³ÄÊAòŸÅsx/àE¼„—ñ ÎßOÆøœ{Ž"ˆ1Œ#„k3qÐøàG‘„d¤ iHG2‘…lä yÈG Q„b” e(ÇV ›«p5û×àZ\‡ëqnÄMxZ<ó žÅsxoˆ5mhG:Ñ…uЙ #N&òØ/Ï)딣Ó›Žùv`!a±†m¥l5êUm#še‚mA+vbvkÈþŠÊþFß¶ê'ž§aoŸ¼C:âÖ‘ s5Ó†vt ]x‡c’Ô€ƒlä LûœrThŸÝ¨oÙMØŠÔ¢Yƒ¶­Ø‰]øEä«ÒOöB‰¢«Î®ÐïŸ&gAŒa!œ@XßçÊ}ôðG’&Tîiª Z½TkL*4@oúèÍ(* rO‹òf.,âµ×øàG‘„d¤ iHG2‘…lä yÈG Q„b” e(G«Ž™ ¹çE¸—àR\†Ëqž½ežÅsxo0Þ†vt ]xGǨV/Õsb¥TŒ˜ƒRñQµ€S¾U{ÒnÂV|Cré ­"Vk«E=kD3UnA+vbvë§Tômû+‰¢¢‹é¡ ·_r¥é‹¯ÿÿÿúvTüg*`,¿"ÿB¬¤`…÷á°ÊÄsGé™;Yß:s46Ò‘dü¯n<ç쇹w ½<éÌ<Þì» [QƒZ½GŒ”JŽL4•²ÄT£F*M­¬1u2×ÔË6Ó •­–bSƒZÔ¡ Rìý•sMd›à|KoqþÀ3ôRgT/u}Ìuaá!Nƒn<|âwýÚí¦H¼›Š4™í’_ŒMØ‚ïé-ö¨~$–ì‰nІÜT¤ ߎ‘(7©`ÄMgÔ=wT8‹ì³gM– xšg]Î>ÉÛœçqžGfW+f#çÜ£øÌh,£“|*7G&¹qÜ=>òR$×Mïd7ŠØ-âDzµ‡%3’#ä9Â5îíÂÂCä ’ÂÑ®°ˆë/—ù×ÏÜëgõG~ ñkˆ_CbœŸK¼ëH-Û£ú2ÛÃú™L²_ÓKí×ñ ½“3n²UÄj½ÉÛ‡ýzg$û?l,“…lI±_Ç7¤ÂV«ˆÕ’âíÃ~© /FGxúw¤Øn’‰¼ÏDƦk?Û™‘퉎Ü;L/µHŒÓ*ÎvÉqvww‹ßÙK<ʱ %Þ¹ˆñ+‰Wo&ÞB¼_<çrž ÷ö_”™Ì¼,fö-75¨EêÑ€VÜ(¹2Á=ªíî?´ß½ï0·z´ÛZíŽÌëE¦5ÌåZbêÑ ‹Ä9ó6Ö™ÁgÊlgq¶L'úçbE¢oˆ)ý/{Ö kG'¨Û nÜ×hŸ Oо`ÍðƵí‹cÛÖÜÉæØö|™ýv¾Ýß:Àܘdß°b`‚>ñ'= Ñ"ïà®"!ñ'3tÓ'`Å úÆ >Ä>äŒ@J^qµÊ©øÎ ŽTH[¹zg¶õ]ÿúÒ†«Õ»ß¤ªq1Eg¿æg•/ÃŒ|Æøó c¦`rÊùýšBvÑù¦˜‰îŠf¾¨ž?ü€UÀ9LôU}Bt¢ Œv¼ë_@ä[†ªÆ ÿ–¡~+…>Yˆ]£¢‡n&Sÿ[¸oèýiž\þ'ËŒ¿öOK3_Û[\ç}ùû¼RÜ…â¢|5*0ˆ&¨Œª§Y×Õ{ùæšH+Ì,þ¦¬Ûlà.Ÿ¡Nt^飚ÒÒ^›͹q·®uß}íÊ|b†2\QÝ®éšè>bòÍsXLˆúwcfæG›WÝJb~¸w™®½?V}»”%5µú6ƒV,!&<+oŠ`À`sHW-_UŽI먺¹‹Iq´‰M³–q1a¨¬˜Ç¤8dÙ`mM‚ \Uó_Û¶qôl[—kÛ¶mÛÞ wccƒµmÛ6ê!_ÒéÔü]=át'@Ô±˜ð[•l{ë<9<µõ…qÛëb2JEÑTt ßÏÉ»1ïVµÛ&òŠø-¿E䚸N3PèÚ™(ÖÉ:ežT ´O=*íÕ€*û Ïž£È·oÁÞ)TÛî1…r÷9¥îu*¢s¢s ÷¼µy·ºs Ä*h‘C¡C‰ ¨4c¨6c¨5kPgºPoºÑ`zÑhúÑdÖ¢Ù ¢Å¬C«!ÚLfÇLf—™…n“A™B¯é„·¬ ¸2Ž!ð‰cÄÄ0j∙âf6& c”éà 33Íf™”¿ÚaÜ´»OcØÍ´ãXœêülÓ+MÀƒxÔÊSxkð2Þôß¾ã¦?áWßî7üëÛýÇë˜Ç<´³€˜fË VÒ¿%V± ݬæ(ÈcÈ2Î2Lr3bãfqœi+ƧYî…©ms›qîÃ}ÐÉ}¹¿OàVäaÎàŽGòd+§òzûÜÀÛßÁû‘à|À§žð´óC|³ùG’Oð ôðI>ƒ>>ËW0ÀWùR|‡ïøûwù®Þã{îø>ß÷>ä‡Ö?âGÎ?æÇ ?á'îõ)?µò?sþ9¿°ó—üÒµÞñ÷ßðëÞqü‘?:þÄŸ|úÿ°óŸüÛŠç„Ýý_þëÚÿdDqEŠó•9ò†ˆuÿÇ[ñž’*W¹ó U SÞq^«ÄÔ¢çmjslW»k;Ô„¼5â/{ÔƒÙêW¿†5Œ(†”J¸6©”Oçk1fi©ÖXY«µ˜ÒmÀmÖf,ӘưZšÀMkÚzVY¬RN9+»h¬Ô®Ú­ÚM»¡M{h,ÕžÚ+´·öƤÒAÖ=7‹õ:L‡aXÇé8,× :#:I'aƒNթާé4Œê áüL‰!­sœŸ¯ó]{‰.q~¹.w¼J7ÛùÝêûxÒÁþžsqþšÞµþƒþÁúб!, «ׄ +»‡ƒœu¼2\‰yž„¸…Þ…¹‹Ã áÔ†[Â-( ·‡Û±9ÜîrüŸrК+Œbhº’ß¶mÛ3/PÛzµÚ¶mÛv;¬Ýfæb¯“ó-Þå\ŽR®à dq%W"•«¸ c\Ã5Häz®wº‰›ÜÝÂ-Èåvn÷d'wº»‹»0½܋iÜÇ}Þ|€œæao;Ê£ðÙ<Îã(æ)žB§yÚÛÎò¬Íó<ïô/ùÄ˼ì¯ñšë¼ŽaÞá]§÷xßÛð&ð!B|ÄÇÈà>AŸò)&òŸ¡€Ïùy|ÁWN_ó5Òø†oÍïøÎÎ{¾7‡2‡F&#Œ ŒQF=‰1†QÆ·ó‘‘ÀOülþÂ/6¿ò›ù;¿#…?øÃü“?‘Ã_üíî_þõäÿa¢ÆÉß QD‰$!M J@†’”ìgŠRP¦T¥šÓ”†l¥+Ý­ e H™Êô<[Ù(PŽr§\åz’§)¯ô<ø8÷:Oƒc—øñѾ;1}Rúd;ÞÚæv™3›ûÏùZ¿×Ϧþͼ¶ûÔ5Ï+«Yõ^¥_½ £ÊëL›ÇnªÅªD)UÍ©¢²žda}oUN["æ´Ygêº (YÎÇÛ½)*c·„´6‡;E«ÁÎwmVµÉ ‹mÚ¾­{[ØàóêíBÚŠØZV½Zû>—jP·é'º^#Z=fUyþî>Xß/ا;t¦ò±ÒÎÕµ$X»i+ïæýµÃÇŠ Éíf•! #Úá¯'Ô‹™Ž½›¸ñ† QÃRýúZÐbm¦¼ÊI×s´Zu·¯ #™W„ïs&~ÿÐ3t3ÉJø‚w‘‡”ªZ_Ë/[Û‚ÜZÉtµO—u=þ¤ûé§…¤-Öð¯oh§nüK7êÏ*“ð)¯úhëžëmQûëªÝý¹ 2_é¼=ëUzX¡ùÞ˜þ•ñqVm­–ÜÇ®JsyEK£+{úŽÊëÆq"´µruu¾mwî­ÚàcbÖCëÔ—s˜V߇ ŸÐÑe–1 D)ä[|Ç™ü=ÕL·Ç ÝJôÏù–{º³Þª‚>¦œ~­üoß×·u‰®ÕÙúºO ©èåÞëãF­µVm¬oSJ_3­ ísfÑ;ú¥.Ð-‘äš÷Ï__ˆóhDƒÁ_°)x‡h{êÒI¦YuÚnTå$>ÉÑè/+:[e®EO×ÚylË©iVr!ƒÚ•-¥{vçÍÏ_™ÜEã¿"2â<¸Ü§\x‹·&¨WIeì/ÿ&½*ôî–¾²Äq5ú+N£´s'%£­6ÅäÛô`Nå³ý^Û’ÎRHæWöšÕ- ñƒ¢¡ïK¡÷–¸·0¤ºBÎ…¤ãÂéP¸Wñ»ë@JÃŒ©r$)mvºûØcG…ïzfôÉ]Ô -4ª©…¡®*µð^-ªkŸ#¨ÛOìn ‚ƒÊ“Ü£A§/J:+©\—ÚG¦º¯>AFl+¢A*5ê¤ÍgÕ÷ýöÀ|3ì´2½¿™æðvÐ;¬eб©ÅP²y¶¿e¬GÃ>ÕôRÙÇ’e’Ušê”§¾UÆc¡*êJå´]ÃÚÚ*Hmä,”‰ú‹ñ¥Ñ8XëžNFÓƒÍQ }ÿØ\ÚD=}d¥Ii íØ ­|†4¿­ajP׿tßýô@\Ù´tvyAı€úƒž¾‡¶j¥×2š¯æªûn‰£= úºnV^k›ËÎWk;íb%ËwÔµ*µ(ןëìàrÐ<ëÅž%4sBîOnòÑß'4hÔþ–-4™ÐaivÀ ¶Ò{UJ¸ãÀ°Rޱ-4¬Bº]£{´ }>ý«®”ßÓ:] ’¯µº\·ë2ýB—è®hÕ„½¶M7ŽSÛ_Á¿ó)¤Ž„º] ›Yc­Ñïy~‹¿šÞýs(yÝ\Ë ·«ÊÌY ݵþLLÀ ÑT'µXWÎgÛf=øfÍŸSó§t¢ÿ©ÈÚüùý_™óÞ×ßZ§÷»þÁº½ßõb›ëÏñ¯¶G¥Ö¦ÖÚÞ:`á5= ¯éáþD[ÎŽð~Ñ{í¤Ôý©õÞ»Á{5—¥ŠÎì ιö"¼Ž/qí®Ë^æ}‰óíµÑy7{#ÞÂ7»ï|‹;È{ß'ðýî(w”}ÀïN°º“Ýrû°/ã7ìïálr†öZ`éYàct-òÑï×µ^,íNã)–Ð>.ây·¿¼ÔçšÉÓyp#C†Ù\Ëø‹ÀoìÒHòwöCŠ—†ÌО>ζ¾h¯µj¾ÑµØn=ØcÖÇÝ>[lK}\Èó.9ÿ~{möi2,ä›c³ý•2óq¾ŸïŠ<ã7 )üš¿œŽöu'½©„PÂ(ôqE¥Ï‰ñ  qO€ÇõÁãúàqODz<®o,‰£¼¦-Bö“ n½P³~ƒ—ÁÈ„Ž=.Öƒ…Ùaa}c)üë ø×“`^O€yõæ±­^8T/ª?¨ˆ=::Ó¸LÄbúa1½©·§Þnad ÝŠ!'@(@(–@(–@(RŠã ó ‹ Š„âÅñŠ„b „â4űŠŠ„â8ÅŠ%Šý 'B(N`¬¥kK °'Ã)Á):á'Â)2©[S·Ú“¡K ÇA+fC+ŽVd¡]ЊeЊ#¡=Њ,´b>´" ­X­È¦6§6Û*˜Å2˜E[³X³X³ÈÂ,:`Y˜EÌ" ³˜ ³ÈÂ,Ž€Y,KíJí²£ YÈÅ2gÎlüâñð‹£áóáY—vi; }’­£Y(†ƒb,ƒb,†bd]§ë´•°Œ#a=°Œ,,£ –‘…e,ƒe,†e,„et Ž„ĕe,sý®ßV@4²ehè´3à¸Æ<¸Fm¶ .>Ë玳ÇÁ8²0Ž#aí0އÁ8ÚÝ3Ü3ì±îYîYž¤<Ç=×qÏsϳƒÝóÝó-í^à^àÓ/t/´44¤r*4ä$hȩГ !§@CNÁïï>â>b§»¹ù¿ŸpŸ°Ó¡òË¡ò‡ºÏºÏzfÿe÷eÿë_q_ñ¿þU÷UŸþšûºOË}ËçÿŽûŽÿû=÷=ÿ÷gîgv˜û¹û¹méñU¿ç?=ãk~×ÿÁ0—^k[ð=sf ¿±øIÖg/Å;QQJ?3ºTöã{i gðï™Ú©² Øsœë÷d£jñ¬ƒüØuu𦠢ÖÄÞßÑ@V³×ÿÚÆ’swµRœêÖ¬·wT!ÆP#ë£ûÆzå£Þ£œ¶B)ªULZ€lÕ€lžîaå™Z§¡àÉ©Jz«á›ÃsMÃ>ÂL˜ËÛUT9y}A÷CU¨¶R£‘ÉÜ3¹¸ûÁCV „ašö^Ç•œ5÷®º;¤jq-gZ†ðå5­Õh멲ªÑª=~­¨¡‰/9ãÎÕÝr1ºåϪêf 4ôëû4@­n :†œq%WUEûѨµ´v¨2í%Ær•V'ú•ªb€ëš³´*º…ïTnêg5å÷æ\QøÛ‚L½ç!‘OgH”ŒVGWo ãàAΩÑÉõа³³•&d–ˆ¡;vßÖya$”¸öá4[aL3I£¯'²—¨öäÆîQ‰Ûc–ÙÊœ–2øžÏOíX_pEGMïù9³V'èÂøÝ1å¨_ë:]¥|òù’ve­…ýÛ#ÚcçÅQë˦FwÇYŸª¨êã–ø7šöG`ï5ž!Ï4„謃qJ&N£6ÙLpΚ&ªÑ:ÆÊ}ÓwºMÕ‡òî•c5iÆUÞÃÝR—Ëobmvü²õY] µ±sÑ­ú‡w²ç¥è¯³ƒ·û¯Ñ§u~ üßWI9ýYg“¾W×›Ù¶4ä¾Üö9°ÚÞûÙ¿Ê…UÙŠë©ÚÌ9ôo›È±ŒnÕv$›l±f톬øX§¨Æ®xÃÞ¶5f@ë“æÉŽ E%‰¨„¿nߣr*)¯ª÷G|µF g&cæP¤áÓ aBz ô#n5çem¾¾5µÐÍÍ„ë‚,<™ݲezu ¡®M⟎cg¦[~ŸÖ’RRî›ñÖ âEÚ¶·3ÎT%ÐÚóû=ÆsN@Þ¯›Ä _Õëp•άô¢Àüðá4hÄa­Sžº Þ†tÞ×Õzhò o4ìÞ_îÆ‘¡¤±]n¨µ,šèì°7óJï_ÈEþ*c×G§ÅŽü¯üKi¿—cƒ±1Ññ=sç• }ç¤Ð’‰íóÿ0q0²`½{¿µÁ¢FÔàÙÆŽþ™ÚmôlÛ¶Õ±í jÛnƒ"fy¢߹Óó[w{‘ó’Ë®¨ÆøÕù ?«z/õ*µòÔùn¯ó&õÐͤ¼ÍÛ”ö.ïV†§Ñz¿÷kÈ}LÃ>éSºÃgü¼îò˜Ç4ÅoÆ=š“c™®Ç¦Ø¤·âLœÕÛX¦wñKïá‘ÞãE}¯Ä+ú#ôFècŒÐ'øŸOãµxMŸa{>Çê|_ÅWú oó5æ¬È·øï°ßW§ªSú;ñ#ÎágY iŽ¼Ø‹ÕäQª›þ¨ÍyçÕá²Ëê£?jà.Y?á'”¦6j¡6Êð|þÍQƒŸõ³êðR/UŠò¨Þ˽\ôG]^é•jõ*¯R;7íô:¯S×{½z©“ú½ÑÕC£4À­¹õ uÒ uRóMo¤Qª÷!R»û°:}ÄGÕCµ4Hµ4èS>¥Ÿñu5ø†ŸWT²~Ó)åý§zh—êýwÔ¨=j£]ÑÃꉑÿÈ R3 R3ÕG«“¦i0–ÆRõƲÿ¸µ®µ­…h5 Åj(ÖA±rÕ«\ ärU ÈÕ@® r¹*Y¿S-¦(÷R´s)ª¹¥\Š¢,Ý©]@Q6‡¢lD§R”-¦(›EQ¶ˆ¢l EÙlHÏ€ô4HÏ„ôtH?F]6Æ£0ÎÁxŒó0.xµWëaH½ÖkU†÷Dxá½ÞÂ{’7{³‚zBc:öó©Ô*µÉÞ骰†„5Œ÷ïQ…b-ñ~P™MŒ²‰›(úˆh¢ú¨`‰û„*ì#aø´O«Báö…ÛT ·ÙnS)Üó¯þUÓÙÊ([)°•btD§&FWtéèŽnMŒžÖ´pIÜ÷h~Ü“TaI KeIE–”°¤"\+b…•±Rà¤Klk"fú¾8çÔÅ;GM\ˆ ê üt\Ž«êÂR7Åõ¸¡®9¶«)¢ Ã™Îø´ÜJÚÝÝmìÙîîöˆ¾á°`þ=Ô¹‘/…UGzI/‹#Öbá»ì ÖGˆuŸWÇqkÕÛzG½Îè=½/®¶>Ô‡âŠdëc}"Žggô™>Gµ#T{¥/õ•8¶=À¶Oô­¾' nGœG¾#介|÷y U¿ê7qä{¡¿ô—¸þÑ?âøwDÞ:ø÷þ½Ä¿ ÿÞÅ¿þ="CüÛðïcüÛðï€þÇ¿þ&½Yü;àß=üÛðïþmøwÿ6üûÿ6ü»€ü{ÿ6ü;àßøwÿNáßÇø·ñ^ìã߯«‘Å¿ ÿÎáßÿNâ߆OñïïËÿ6ü;þðï$þÝÿcø÷ÿÞÁ¿þ=ÿ ÿ¼SYä{|ívâø÷ÿ¼b†y0ï æ]Ƽ+˜÷9Ì»†y×1ïæÝļ[˜wó®`ÞcÌ{‚y1ï æ]ż«˜wó.aÞ%Ì»„yÏ1ï3˜÷ó>‡y×0ïs˜w ó.aÞ%Ì»„y_À¹›8wKb雲+’ù“»*59G'ÎOÿعζ¢(|°ÖVaeë-5°€u°:ìOÀÆ, ´+ƒB3‰ ,Þ–§Þ^ˆ%+ßÎýÜ×{ò$ËÅÎñÞ}¯¡Žsß»¹9ßw>IÞŒ‘:#gr‚žsõ!n%Ukóí@=1œª±š‘–»þû(§Á¬`”¼ŸÔj'‰ºŽr Qw1Â5.Ñsˆ°ºë›âYÈa.×Öeùo»(­:^‹Õ;Ãõœë8µZíßeÎ…÷<öqËûF¾q5¼ï¸ J!z¾"÷ø©föcõ.ºòC‡½ ÓˆDïGäë+öÃJqÎZÙ! Öbv(OÒ×8\v²6«{5VrŠè~@_öPê¼NXOôyí¾ñ¿a-÷Fç·ƒ?g/'Ÿ]µ|E©ÏŠÇWÌåÌõOG˜ýCÍæžˆS‹1´¶ú‰Ukµ»ZgN‘s§>ó+&ü‹Õ„~ð<”øö¸9§È]•S}¨ÑœÚÞë©èŠ1,*˜`;bÖÈ‚šk!¡æbAWôDŽ5§}LÉ»à'úwfçücn#ôº4½ÉWrO#Š$>ú’û…;=O1p»ö­#½¬W—em£NG(8š‘5ĵÂê¯OÈP›qüM: ø£÷sSûHÒm‡lc´™¶dý?¿XÉ¥.€º~²LÉŒÆsµù-kªå2oõý¸ìª?äXQÁ$ÆþÕÔ j‘RÅàN}.fAG˜çm0§:ÒZòÔÍDZ„iZGªö~#·£Þˆnøíت wóÊUèyìŸÓ_•weGv¬‹¹'¿9µ3ëë_;¦ªÉÑÊ®çWk¬QGò<ÈÂ3 í…¼Õãµ×»!#[¥Nð¹s€‘ÿŠÂè}omþ¸®µ¶5S¿Q^mÛjÛFP»qR;nØÆ¬ æÍ©íþ±ö~÷ÝÕ™ïÌñ?¾¤òW/þø­þË3$óÑ_þ ñýÿG£ß‡’Ê÷L÷Õ¿~9­¯\˾»ZËt*¥JʤSÿWH·ôéã㲃ÔÊr”ôË1úIÖÿËòœþ_1 ¦QVñJfÕ%9&{š 3-û™Yãä ºw'šKÌ%r>È“ÍËæ9żaÞÓp4žnÞ7ïËØÏ4šå,ó©ùTÎ5Ÿ›Ïå<Ø@η‰MäÜŠÚ^Û+Ùa;,Ûi;-—(Ç*É¥v;»“\©Ôj7¹Îî«,êFhÓÍp¦[ I·*7:YnƒÝiϰgÈ]ö,{–Ü û¹ÞsŸ½Í>%÷Ûg”ô< ¡y¾ìPe0/C_^©ÐpÒmÅêÿV)ÓÿíœìP*õ*Uú¿“ÿ-Ò­ÿú“‘ôêÿ„Îc ÇVq’—T R’vÚŽ!)§¤ÜIÊ))wÓjl5šuªYo,-fM„Ño£ofcC6¶‹íч0úJ¶·FÂè»aôŒ¾ FߣO`ô1Œ>dÃ{`ô!Œ>fÛ{`ô›Âè+`ôu0ú ý¦0ú}£oƒÑ÷Ãè} £O`ô1Œ>„Ñ7óU£FÂè#}?Œ>„Ñ÷ÃèïÙ; à6’, ¿µœH#Ù’Y *Ì^&ù˜™™™ù®à˜™™™™™a™™½ÁrlmbûÌvmß›¯ÆS³%»–iº2ꙌZêÌû'êþ^¿£oáɲ:_‚cpÌ-pÌŽù88æ68æcZcZ8fÇÜ ÇLÁ1·Â1SpÌupÌupÌpÌ"³Ç,Â1›á˜†cæá˜ƒcnƒc>޹ ŽY„cá˜E8æCX»ûP»×î•GÙ~{P÷v@mí!­Ù!­Ûa­ÚQÝÙq%¿“vRÏÏØ=3kg¥‡çl*jÓPQâ®Þ—8ß'çûDÖW%èø¸X‘γËÏ–íroFƒ5ÖŒì“åÞÆù:š`woÇÆw¥í¶Ÿï&ÓÇ-Úþ5ìãùLã„XÃWT§°ˆh |õ»'"×M¢iDÓ,Èô1#Œºˆšx-#ÆÚ<ÛÆg¬ïƒN«W½›p#ú:xsæ¢q «À6ð­>þFÆT¾#lËo¡UE)]Ùþk%õ/vßv_ÑúV¥Œ_màêº~×g’)ê:÷ }߀۫¯ßu?¿‘úÒ6i­†ý-]ÿXêü(º7’$”—¾&'9° !ÊHèùÁ´ß¨ÉY7rk¯ƒ€]ïœòòˆÇ20ë»s3=7ÛYŠÕvþë®ÁNVð~Ÿ ×áDb½–È{‡\m…{éV‹­Îó¼ðWëÑoÜø±“_é.™·Rµo®Õ(ZJãÙ ÷ËÛÑÖ¢Ù¿î!¨0ÑxIÚ¤¤ÏùÓàbGÚ_)ñÿi[ñ|«Jì·Ìé0Ž$øÓ…2²A€ÒÒ¤ŸYª ögñV€îá»3ÃÚX)·£ÌWÞ•>ß]<Ÿ±.é}gÕÜ8ZÁûÿÿsž×¾ÿ:ì}²ßN6ñϸñÑ|ö8$)_(®ÑZ²fö|ƒ›ƒ‰ÆžP§ÞÔì7þ7 'ËoJ?è,`ðÿøA3ÿW‰+½¬¼§ADºß¹]Ö©ŸØˆ¶ûW|ÂúܵøW|Ö½s.Ž+ÞAW¸ß¸±'/½Å®XäÞžé¯v?ª“«ó!ñkû¢d)sߺX'×`w\Û!=ë:FoÍ ´Ô.=¨ ^?ÿEvüYïÓuTƒñïÒÔ練Ö‹°M\c·_\Ñк¬Rœ­ECþ[QÞIWsÄdðÛÔÑÕ¼!.ö¢uè~éÚj¸nuL?s‚qçNÉÄ÷Âïí€~/kì÷±Ñç3ƒÓDuËRüù†ÙØzN@¬¦ßZˆ3ü;7à­çŽÖGãMÔžwcõ~cÒáÚÑzV„ÿý—M’u“®^ÿ/c¬{d®ÌÆöÕä÷nÉÛfYçö&­ø÷onhx#ãœÍsõMˆìúð•<¨ýÏÄã°ÃžOH’)t~fú$ÖÕ¬ÿyÇâw~Y=5…[–KG¬·)bé á—´ãǸñs0'2~#½äýï¿SòŒ-Û?=OÆ ìI#µ@Ž“´ÑÞ—â|‰m²NyQ»hÒ&#?1¡ÉÉ/M‹)ʯÍNÓ#¿3Ç›ãåæ‘æ‘ò'óhó ù³r£Ê•æãÊŠ¦”]¢ôé¥?÷0{”þÜGyO¿¹Ÿ2žšy 9¬å!fXùÎCͨ–‡›-Êuc¤k±ó„ d̕崚')¹i7Ï :ƒnó¬ ¬4ÏUr³Æ¼ ¢5æEJk¶™+›9ʼ"8%xy¥Ýc÷˜7Ûýv¿y‹í·æ­¶f™wØQ;nÞÍt›÷Ù;kÞoo°7˜FóÚæCÑ\¶ùˆòyþ튒ÑHV‹…+6§w91„sÒ,-@‹”VJQÚ´t@‰½+«½hº¡Ž›¡Ž[¹+2WæaŒ)cZ6jY&›´„ðƼ^«LÒX4¶CËÆUÆ ¤q¤q¤q ¤q÷»Èýä-ò ÈSiä]òQ±ðÆ&ù„|Jûõi-9ùŒ–¬|V¾¨õ/kÉÉWä{(ü£Õ>þ¡ýþ§–¢üKK«ü[Δ(å (åj¥”Ú§AR†:œÄûݱÜL¦Ë¢i2M²Ò,3˵ž1±&4¡äŒ­·˜±0Ì¢!0–WÀò2X^A-‡d¦ ™HfÚ<_Iæ2óbób ÍKÌKôú—ª†j£oÒë߬–ZPKýŒ4›ÏšÏiýóÊ9Ûáœeç*%œ×ÖþaN•5æ4¥hçhç&hç6²^ yÓ§œ3M¾ËÚ™†p¦ œµý~©DÖ¯uµ~iSë–„3Œ4 uÕ€T`›Û`›òZæ!œUEJVÁ9C8g%Ò†´Á9+Ê9[ôÊBPÐ3Å ¨gZƒV)DšÑ}gÐ¥ûî [Ï—ƒ²^³"ÐØØÏ ä³ ó\ó,›ƒ­²ò¹!R””UQ§H*¨UýôÞàzå=ƒ{Jó¬À%ôÂýÛáþ!ªèE!Ü¿÷¯ÂýCr ý Ú€þWñq¹§™4“rO<:ðQKµ”ÐI…Tƒt–ÐI€*:)áPÅ €*þ¡j£,'áPÅ+ Š6Êhã8¼:PH…”PH iÆ7`¾!¾y|¶â° ߀,: ÑIoðà!r+ÁïAŽåcȱ¼“˜ø;áέÞúÙV¸ó½àÎkáΛàÎáθóF¸sîÜ w^ w^w^ w^w^w^w^wÞwÞwÞw^wÞ wNÃïw^ w¾Üy-ÜyÜyÜyܹ gAô¹Óî³´ÞŸ< vÁ ;ía{Xº Ñ» ÑÞs¡›çÂ.;m§¥‹§C'O‡îøé ›d¡Ò¨ôFže¨ôv¨t1ýïô¿eKz_zŸ¡ÎJ{¹X‘ÜÕù«F_#Büã/»ËÝe¬Ür{Yá3îÆø z-1“5/ûŽŽÛÏеS’ÑqDÅýUvÝAçÁy¿røÑ˜|ßH/ŽV‰ÑÏ^oŸºÝ÷pÈM3—2È,3·î`ãwˆ87qßX#Ôþ Eiǰ5"…Þ¦Û­g¹]Ë£µ¥|ôG}'¡ø>£®ÈÏOÙºq=ÓY<³O·ùæE1¿"Z#ÆóÀ‚ka>—Ô¾dEºV¶h)‹ÄùŸÊôp‹û¶öð ÚÒîÝÿÍEíë5gií÷ÍøøGîë·Ž•2Î?à.ð˜ÊH#+Ö§ë¯×3ßKÿö»Ëã~zf.Æ=¹¦ Š´Ï|~k,9æ“û‰ª>N܃šãˆuTûµÐî­v·¯»ƒk‹Õ«É ÝZÃÙ>"»™I€qæx„+xňWNûZ;+a Þç9¢A'ó@zÄÙ>/f,s3r].=n£7ã×ç.ôæ¢úÙªó¾3’”ãñlÔx4ýÎÌ]=2j_ks™£8Ž?;Å6Gè«yT’ú­°ÑŸ;˜U^¢>%¿!ã]cBûã©öCäÑ>¢ëÿF2X6÷MÏdü§YtU27—iÅ‹pM£Ó9à8¡ ÷Ñr|òÝÇe§îÕ›$*r|û_Ëeî,µ‚7‰nd3û©^q*ô°þZ½77^Y뮦‡?•ödfôblq™Ÿ3Кüí©z†c£‡þÑhßý<9þȈ¶ŽŠÄQP“û†îyÖ7 YŒ ’ÁðÞ¼èîTüî倚žñÇ…=×±qtF£Yf¾{-ÚÏ;ê¡e}I™ˆ­Çi dÂXI1Z.êˆq»téHïÑÒ͸nµŽëž&ktäö<Ùü?ìâŠaˆ¢èHú–¹ˆÀ6ÌIgÁÂÐotž-Z ,ï ´¡:ØX'l¢‹.6M{ØL/½l­>¶ÑO?Û&ÝmgÞºÌ3<ùO>G>‡,3d)d™!Ëœ *dÙ"ËY6ȲF– ²Ìe,KdY ËYFêŽÔ¨;Pw@yä-e¹ëžÞÔIE'N<×à¹Ú¼ë›çWú#Óžø/ë®™«)Ã8oö<Ïëîî=wwwOðç#QRa%VÒR¥‡>g.œ7ÖGïÌ5óÿ-íÉÑžJÚSéÚjÚSK{êiÏ}Úó€ö,О۾:çiOíÉhÏ=Îó‚óΓqžÀy2Î8Oç©áÕ!ÜR Œ „ó „ „c „Qð•aT Ì+FíÁ¼ö`A{°¤=˜×ҌڃMÚƒQ{° =µµ£öàˆö`^{ð¥ö`ÔÌñî¨>V|¥:U_ªFÕÁ’ê`T|¦:˜WlUŒªƒ«N¨Ž©FÕÁ‚ê`TÌ«¶ª©6©N¨n©æõ£Þ`^o°¤7¸­7Ø¢4¸­4˜WŒJƒîÙE÷ì‚{v1Ì„™ä#÷ìj˜ sÉ'®Z¿«Öçª]sÕú\µk®ÚEWí²«vÅU»ìª]qÕ.¹j—x©º`r“‹6Ñ«,ô*ù¼A>o‘É{.Ù5—¬Lža’w9ä½ßŒö•¡xÚ´C´l;EçIíkÛ§ølÛ¾(ó÷Û¶mÛ¶mÛ¶mëŒô~»:s̬`á¥Þ¡Ï¹ó»³´.Ýu»u _¹Ýòµì°n‹¶â0àï¿Q ŠN†±€jÌìiŸH&=Ûgl°oñ¨¦‹áLe¡3†|F0E΄¨%ÌH¦³g…:"ŒbK˜LɬìBŸîYÎóÙtΤÏVsÒYŸ}zgå}ŽéÍôøœ—Y’ñ¹!Ó=Ëç¡lz‰Ï+î.¥ž(£™I·3–bŒa=®IÄK¯KÓD‚q̦Ï%JhfãIºD)- dsH9SF+ƒ˜È\ÒÎ”ÓÆ`&1Œ3´3„ÉÌ'ëî­¤ƒ¡L!G~éÒþmŒ-—mµu¿i/Wl·ÅR)×­^¹…Zn½tpbÀ¹»¦nþ?®ï˜­vìßά* !ÅRD1%” @ˆzi ÖÍk¡’jj© Uó¨ X¸r7Ë‚éQOS9­âêjò5Ô¬Û¯ñ¨poø®h{tb´;º[ô”èÑ·bk­Û/vGì™xo|Ÿø9ñ§3ñbLd>õ½\¯Òkô:½AoÒ[ô6½CïÒ{ô>}PÖGõq}RßÕ÷õ{ýEÿ²XcÅZ[lKm¹­´Õ¶ÖÖÛFÛbÛm§õmãn÷ÌOÞ.¡?ÄH±TJ½´Š/ *ce²Ì”ùÒ+yÙTVÈV²“ì%ÉQr’œ!çÉ%r•Ü ·É=òà¾à~ã1B±G‰”Bé”E¹T@ÅTF•TCõÔD­ÔAÝÔGƒ4Bã4E³´@Ë´F›´CûtD§tA×tGôL/ôN_ôG!Žá8Nâ4Îâ<.â2®ä®ç&nåîæ>äç)žå^æ5ÞäÞç#>å ¾æ;~äg~åOþå(å)\( àŸkwO¶mÛ¶mÛ¶mÛ¶mÛ¶íº¶¿y@bCHŒ”H¬È‚(޲®0*£&ê£)Z£#º£/c$Æc*fc!Vb=¶b7â8Îâ2nâ>žâ5>â;þÂgL‚ ™œi™•yY”eY•µÙ˜-Ùž]Ù›9œc9™39ŸK¹š›¹“ûy”§y™7yŸOùšùéÍ@†+¦ „J®´Ê¬œÊ¯¢*­Šª©†j©Žê®¾¬‘¯©š­…Z®µÚ¬Ú¯£:­‹º®»z¬—z¯¯ú-Où+ÔEwq]|—Ô¥v]v—×v%-ºå±bVÛÌê[Kn¬­¥´Öß²Ú (Emˆ‰ãl’•·)6Ë"¦ˆ€ÂïÝ •2A‚q3 FÚ*% ªB$9p‰¨"˜1„„Ã~Àxsìq@…cÎɸàŽ÷QƒžhòB‹mڼǶË7GüÐã’?S®qŒÂI§hYvš¶3Vø03£ç‚U~­¹Ä¿Ë®˜¸jÝ!×ÜqØ]÷õ0÷ÄS'<óÚ²7Þ:kî£ó>ûjÕ7».úé—õ‡ÜõP„ÂF脎ÍÐ}7â牛i)-¹E€…º@µÝŠ0Ì•Uwww·1Ô;‰;N¤nxq²‰Â D¯ä†bAzRÉÃÞ ÞÊÏB¾ÿÈζ+ÇcÀ ì±Â/ìÿ¢écgTù&“ལ·ð*­%xcïè-hͬßHÀÊ<‘WóT0Ã[SdÆW<˜·IŒ3œ±Ç klQqDÛÉ)iì½…WiÅ*·3Ï0Ã[\pÆ óNɰGÅþkÞ%1̰À+¬qÆÕ¼›ñ÷È‚Š«y¯œqÁí&óÝ˨û$`‹jÞ/cœ±À[T\Í$Æ€Øc…5^ጊvÓ&?{GoáUZ±Jƒ½ãˆªù û|XÎØâ‚6ŸÉMXWó ˜álß›Òòî€ ¶¨æ£2c‹\Pq5“fX`ÖxWh뛽TÞà½Ì*Š£yCÆ8bj>ŽOHŒ3ìͧ$Æ€öآ∅ù´œ±Å Wó+Æ|‹?àÞÄg$ÂS̱Ãwxn'6}섊ߤ’x#ïàͽJw’x#ïàͽ6ëÑ[6ÊdÏJ‹«ùœì1Á'lÌçå[T\qo¾ &˜â„–¸Ã´Ý™œ’FÞÁ›{•–¬ò"ó¼$v¨¸âÿµ“žI„ ¦˜c‰;œpÛÏËÜÒßœâUYPq5_“€×¸l²/S*Þ­æë’`ƒj¾! F8aŽ 6¨¸šoJ„ ¦˜c‡%^ℊvg&wI#/Ÿs—ÔGïxWr7ßãOn„æ¸=ñû|G\pB>ç&L)y·šïJ‚)Nö½) ïö¸`ƒj¾'רàT\±3ß—L1ÇKÜá9^¢íÂì¤ô&^ÿÜgÕMnàRŒpÀ·1à‡a‚)væÇa‚)vؠ‹ù‰$8aƒ)*Úéø]:újóø3^ÿmžü.½Y㟸ßV<:Û<ùÿå¤Ká/gÿ:Û¶mÛx¶mÛ¶mÛkÛöîÙ¶í¾Ž ¾hW‡éæã,í:S‚MD⼃ü>àIîç à)žã^àeÎô¬ó\Zºv>mèÈt§—1€¡M ôF3ž[™Èdîb*Ó¸×óÐûYÌc…kO°†µ<ÉV¶ñ4;ØÍ³ìuíEÃKÄ‘ä"¤¸ÖŽLrhO>Et¦œ*¯ÖµÞ4ÐHÞä3úñ…kcøŠoËüébÿ­€Y:]—²@—ëjÖëZÝÀÏdwèÝÁ.Ý­Ø­Gõ õœ^&Ö³Úd5—‹­öêNºz«7 j„FR¬ÑG©&h•š¢ÙTi®òŠk oj¥Öð¶Ökïk“6ó¡¶kk¯öò¹ê_(VY|¥åð‡òTÈŸ*SÿªA¯ë$Ï‚Ï ¼Zü^´äIZÑšNt¦ ý™Àtf0“¥¬d«YÇz6²=ìc?±äRL eTSC¯ð*¯ñ:Ÿó?ð—¢ÑeºSwéA=¤‡õ¬Z©µÚ¨­z¨úªŸi°†j”Æk¢&iŽÖj£¶h›vkö鰎討)ZÙ*R©êõ†Þ$Ì›+¼#ôä×4„,‘PGÏêЇ£zJ3 Q]¢ATr$àpèiÜ@¯œ÷ç!Fx½‹½÷šì_"pX;\B€YÕƒ½bïØGö§Ú×öçØ/ö Fåª&a\Ëuê!®¶ývÐÙ‘ÿ¿ kñ–hÉ–jé–iÙ–kùVd%VaUVm5Vk ÖhïÙö•}o?ØOö³ýVp¸UL@<øˆ^ÒqN(²ý^>-”Ã¥^~(ôvmÜ â¯êœpQX—ÃBù)« س(ìÙìVæñ­Lß^A€tôC>JQ ƒ "V±2ÌäøxPEÄp\€ÞÕ¬¨fE +tÔŠZVèȾÉê¨gÔM¯©Þû‹)+O£;âx\‹Ûñ*>D‡Sjš:s 9ÕÜlî5o›OÍ÷æOÓc˜UH¦äK¥ ’12MšdGÙ»Oª*ÇË™r±\+·Ëƒ°HD&rM¬*ÓÆØ{X[´šîW5rõ(®8œ2ˆò2|žFæ] ŸgR`ÝÁH‹23}f¦Ï|·W¿,¿ª‘«ó{­v€«Œºjj£.#ÃsŒò:}žFæõæÔåW5ru® ¤›6î¶ÚÒ ¯ÛWhdÅQßGzåõø<Ìû(ÊKëõö(ßÒ£¹ôh.#šËˆæ2£¹Ìh.+šËŠæR£·Žhµ#šëŒæ:£¹®h®+šëŽæº£¹žh®'škÞëÛuá›l¢o-0¨¥š‰HFšÿÚ/A)*P„Á…Fì£p>®Ä#x¿c¾I2}M­™`ÍÎf³¯9ÃÜhž5™³H&#úZH¾Ùf61ª“Øv’½Ø];T¿¦NOêô´#ñùÄ%Ic’NMú>yŠ1 ›aKìˆ=q ŽÄ‰8jßq'Ä“xoâ}|Нñ#~Gz0‹°ÂÀ&U~†µ'ž’Ÿ¿‘_ÃTÈò'c•üÂX-¿1ÖÈŒµa}l ë5²^#ë5²^#ë5²^#ë5jý{’\‰=$×ëø¨Ü®ã3r›Ž/ȵžFÇoÂÌØ¹¢è7CûVˆ"ßQÜ› Šz‹ŽµûRî"þÝÄ¿“˜wó^¢ÝO´{ˆs_ˆƒBŒÃ44`[ìŠ}q(ŽÅ©8—âZÜŠ{ñ(žÅ«xâs|‹Ÿñ':0󰫤ÕëÙâõl÷zvz=»½žm^ϯg—׳ÕëÙÂÈzêÙÉÈzêÙæõì`d=õl¦ž‘ïÃäû ù>@¾ÍÔðQ²~œ¬Ÿ¤†ûcÔð j˜¶þç™j1EÖ¤jì¬Û_^†Øsåöx–=^噽Â3;PÇ×äiv}Š]Ÿg¿Ùï9vz^ú§õ‘ß$òëÄy8oç]â¼EœwB~e¾æ!iýO& ÃLÂ&òÑ>$ÚgÜç§Üç!ÜçûÄÿˆÈù“P‘ÿÁ3¼¦{Ìò˜ã0Ï;`¦wÀlÞ3¼¦3²ž˜Ã¨õŒ¬§f3²žè¡>×ñùVdzäK*ðø‚|{èƒoÈú;²þ>øšª~ÏsÉÃLÀ&hÂöØûãpÓq>.W nƸ_Ux/âu¼‹ñ%¾Ç¯¶ž{~È60>jë@.vs‹Ý†qm9Ù-ANvk“m9Ù-@Nv+Ïi!,"ƒU<Õ<Ãd3ŸîX QäÍÈl ™-#³d¶˜Ì–ÒwËÿ¾ï,Â.Ö°ËZ"¯ ‘­ Ñl,D³âXGßåhÝLA¶ÆÎØãhœŒ³q1®Äõê“»ñ W§¼ ~¶;xuvôêlïÕÙΫ³‡Wgg¯Î®^ݽ:;yuvñêìªc¹ï$ˆb&@1à¾3©È¶¡"6…ÒÈ!#TÄ&“IjÈĦS‘¬õZ¨÷ë°%¶Ç®Êë@Žc•Ù™8ßöe¯~ìUž„-àI\~5Ù>ìžÅîEì^¬Èým‰Ž¹¶TÇ|›Íär'ùÜIî$‡;ÉûgSÁ¾•ì[Î.eìRM´Z¢U§†g#j2s@˜ùo±Ûˆ]±Ëh²EvW݆DÛ€}G’ݲKvãÈn÷3ŒûAvƒ¹«¡d7œìþSßìã}³¯÷ÍÞÞ7{yßâ}³¿÷ÍÞ7{ßìç}s€÷ÍAÞ7ÉvÙN ·ñä¶)¹íIßL!Ÿiä³ ùL&Ÿ©ä³ñ?øþsˆ&?…hòkˆ ¿‡ògˆ ¿„ò[ˆ ü„V"´¡Dè&B:ˆÐõfa:faæa&fa.RâêãÕF´(Ò'%ú  å(DªUµ‘ø¥øÞdáMez(VÉ52ÏÙ«í¯æ Wíš#Ü#\óÿÿÿÄ|(öè?½uù4ö9ïZÄ–ÆVÇÖ&°A,H’‚” -Ȳ‚¾Aÿ 7(Š‚’  ÖßoTq'à»cYÁ¿:ŠPjCe ï‡8oÀðž¨G™ZÓh>Š_3£ÂU“?Ç©³ÀÌ2«ÅI•ä˜90HPç.G¨sΔ³äì¸ìTX7ÁMt¡K¬mµvy<>¶çiÿW‘D=›Wëº ܆n#78\±×ÛÛìýöÛl»ì\çâ*2Ó[­ÇõÎá› 'äB­¿Œ}ôG|/ÑQTûãj ‘↺an¸áFºQn´ãƺqn< RìöOÛfÛm‡agÛùv¡]äâµhBºûÖ}ç¾w?¸ÝOîg÷‹ûÕýæþtÍîw÷G\f¹f¾ï>pºÜÇî÷©ûÌ}î¾p_»oÜ—î+ÄMuM€;Í®ªÅïû&~]œÝ• ÂÖ‘o ýé†ìt/¿ñßúD›Åeˆîá –ËE:,O3?ÎC…¼E¬÷Ý€Gmà¢U2òÆÙ2eꩱ”ɱÙú ~Ò9[¡'›û¿Æã=µ¯i–Í¥NêC—‡žŠn.yžqÙ£rA O2oÕtúêSªO…>ôŒÁn€¤ÄVòkb,Æa?ì‹p1ÇhF‹É69f²™¢÷‡»›³Í9æóbxO«þY×±Á¨¿{~zváB äªeÖ/‰×’LambdaHack-0.11.0.0/GameDefinition/fonts/BinarySansProLH-Semibold.ttf.woff0000644000000000000000000022427407346545000024306 0ustar0000000000000000wOFF(¼ €¼‰GDEFÒÐJ‰‹¶GPOSÔè?+À~ºˆÂ GSUB¦*fWjâOS/2¶ U``;Ö cmap¶ø‡9HƒÙ­glyf0ŸŒ/¨îB—ùhead¬|66Œ“Yhhea¶€ $ © zhmtx¬´ ÌäEV™loca Ø ¤ t½ã Zmaxp ¼ QönameÑ€9fRíj_postÒ¼ ÿÑ2xÚ„VXY7qAÒd’cÂM Rbd$”¢-õÛ-íV×j«üëîîîîîîîº ÃgJXEf曼wï9÷žón0|†Á0Üåa #•´’äQ” E<¿×Bà/N¾xÐòBC>?¯¸p…yñ£§'PŸgÜëó0_38ˆñ°t6†Óa$†Ñ Ú­V*¡ˆànB’G»}^E’ŠÙ‡ôó ל¾HËDt4 45¥‡"‰¶è~´1Ò`Oðåû´4w—£® ÅTÑ~·C˜kæ܆ŸŽé1LPBQ^Ϲ4"Š"K„BB¥†ø~õ¤íHžn25i”'éJU.ÔÓòöSFGNKºJbZ}ÍºØøÁcd)†s Z€SdñnÒ¶í5'_~éñÎå##ËøÑç]pÁ%Cë·nÁ»} ;eì> a!h‚„kÅL}þ92âG¯¼påÝ+³+ß•’œ•Š4šd¾˜š‚U·¬d¾Í2ôCÓß1Ìô’^Z!¢Õ=“Éôäâð2–eu÷ðªå:WÁÖ-cYš´9VT|ÀøøÁyG-cÞ±TgPà­³xü“pMoFüÍ›™?ð£™ßZlfÞͬƇՆiJl~•>‡^¿¸£n²ÙéØ\Ñ'Ÿð4ÕEÐõÌÂÀª,›»vÉ3Ñh%͹‰ˆôfÞG»ûb×5þuDÍ`Ìrm¼+»½ {Y¼°åŠ—W²Ÿ¦ â©ð)T‹§VkhŸÏ¯d9yàñHž4â„"5¹Ÿl„/QH÷›Šù|ÏpûZŸ/BÎ{M«5`BA ^aJ´ÏfÞDÖ³m óò,òäÑÌå¡(–qÆ- sEúàX ùé¾³øTKÂæÝàCF¼žé~æ} 纼º\0_É»ØXÕeÛR‹vÅb»uïŒÅvv'úûð/OŸ6<|rgçÉÃç¥×ìÛ¾}ll'–Õ±™«°*LJB’$à9#ågb¡–õ‘ôÿâ‘ühª7[Rý5jß¿Á‰a<@–Â'³ÈÊÿ[ ­ÏZNø·HQñª#Œ[U½´Úš§.ôϾ|ç |b'óÇê„ÌU%àw‹eÙÓ¤ã@`Ú?³˜u¤’çW¦ßj ÇZW÷…ÂáPß—gu?‰m]ôW2hÐﯩš~¢Ìl)P,¼*S7Êf³ã1™Z£É¤D BÛArÐá««­ê5lµ+‚õÃTЦì>ƒCßS«–»í%¥öR›Ui˯Zèðv¹ªJÛu†2RkVË­ÚdÄÛëe1耙˜‰XV¤×þùNôõxÅʕӜÎ:@ͰB•­<ë)謻P,±>°9Z_V_²¡a_yÞ4Î1œ"Óa´™9~dë³™? ‘=Ó!/Ǽ9hq®¯(*Üö4èó6ÿªššU~á\½û,ó÷åF˜›'îÜV#Îò%*YûD»L-á D5ë<µ]"ò…rqâßìYW[»ÎƒâÌÍôpæ œÐc‹WWÇmÌ¥b1â 'Ûi%ÖdSùÙ£Á–Í%Ê»íªÝ™†‚Z:øêÛv·çia"É(‰Ǩ]F£K} ssÓ©:Úh¤u§B~‡Rü‘ ²w^dâÞ‡ç ø…Æüð}‡Î¼Ø¦¨°õbfåw€© vþœ=©½ÈB¢ Õ2¢k˜‡Q ‚>XeŒ+YÇÙAW¢_¿•= Y³ø9‰lœ¨Ér°±Çv>Úº7˜:âTê¶¶Z*Ê×Ô Iù–˜ØèÒ‡&JÞL..¤|:U»Ž_üå+¦–hµkźR­Iù¢ )~?ôÊÈjÄFŠ`¨".W®AÙÙ‹jÃAžlp3ß+X]¿4élqû=^­[ôà÷ߜԗ±¾ksÓò®T,éû@­ÀXîÀçà£ûÓüÉ:cÖìêÐhSëúGX_k­®‹Gk 4á$»ä ÉÔÆ€©(nî‰ÇzÔŠ6“­R%DÔJÌœ­R&¬ÍKçÔgÖ~?Œ×/õ•× 6 Iøú¨´Ø¥Ñ:µö–:ùá[“ ڎ립zj¹®ø}"èHa8‡üU@®Æ >·DÖîœGs‡Ò-¶¬­ õ;„Ìã’¶º€·,üþ‹ngE¤:76Ö·ZôÖ’¸ù| u,=”gfž’õoNaÏ막AK¬Nï,,2¨o>ƒÓ™p‘85ý>:ºG¸„­n._AØÓy®mì´â$£ˆnîÎè&«Íì2@–ª× 2O¢ÒÐ8s963ƒ`&??ŒSh9<‰·a»1x?óü }ÿÎÜûLN\9g§®ŸµDDtþu;/¹õÛðû™ÄÛ3o¿°è ± Ã~õÜlŸ;‚ z·7z6J"a¾ZžhÆ›§oS+Z,ËfáI¡ú.‹†ÎTg?ÑÞ{t™„gŽÙ}Í 2áho¶Rß&ö‚¦Bf‡£œrÏ’n`.ÏÞf«9T¹9r«a-í{ˇ¦ZÌöyÕ˪dbü÷äUF[ZF‘––‘€·¡Á ÿY}6¦’C=±x+sŒóeÎòWb&¨Û:vnÁ¼ÑÊcrüe«ê—ú-Aˆ5&íÓ¹îïòè¨#&º6ƒÈS§!ͬ595¡).ƒ{ýxÎHÐU/×Ch“@.Í©ÉÄ“¾¾×D÷›Ò‘œ‘ŒFçtiæ\”©ñVȲ`^EÔ\…õq›R'+’† Mõ8=Á*¯º†yöA}O€ýT!wÎfÆlîQ¢1âPçéV¹µÔF5–“M¶å)w—‘¦ëKKh;Õ\¶BN•„tEf­ÉTGùmÍé]ˆ2©õÅ…y¥µ•Á^Ȩ‚ŒƒøŒÈèÕKzáŒç¾†«8Ï ùË<°U_D(½òŸôO;mÉ'‹¤i‰"„ ÂGh SýIñŠìáôz§(§~Û·6ùšAäaÞhp›IÔɨ¢”C¬»Ðǃ›¹¹Ãæ–+¶$d„”/%¤ûL\‚¦¦Kã/fTlµ@:/¢/0Ëü]¹ûç¦àé;7Õ‰å¾(OÔ°¥Qœ/â‹d¢ºñOpþ@.v£/f,­UU­–™Ù;C|h 9!ˇb^FS™YkËI"ÒÌeÉ?㸃}2 VJ«·wÆÁõr­œ/SËÜ› *Tª bà×ï‡ÔUQ¥ÂXî3~®~ºÜžûýóÊ ލLùY˜rÈ%÷mIËÔR¾D)‰N\gê}L(èåáv« }ôµ9J’á’¯§gº¸Ø$\.„ØÜ,V²_šxÐÚžDC}Ù„:–™ÿsnPmyÀgfAt$Ѝ€…ЊB´`z±…1ÁàØ(¸¦÷ÄqŠíë½_úõšêœ×{ö…šbÃõÞO9ò½‡_H'ù¾T´ßÌhöÏVê+)?FÓvwv÷?ÿ=¾iZô=VÙMTDøê÷0¶¡X~PònðÃ’€•¼|Br/øqÉûÁOJ¾üqÉÿ«»i•äƒP~ZwMeïÜàa©ü.ð)É?õ£Ø‘ü(Ÿ(ù£àÄØ˜O›Ïýú-üÎò.ž.ùiðlÉÏ”Úï‡öOÛG„þÝÍß=ÝÈË×ý:þ~O³,òa2>à°‡¾E]ÍȃðÇ> QýNkº-Ù•Õ¹×ñWåûÕäå€{?NnpÙ¬É ]éV|‹ð¶Y|$ËØzƒÍØž÷œÈ VýÝM%6‡h¯!2±2s¦¦²|SÍT³D{gÎÎö6 Xhë™(!^ŽQ¸3 ±8܃§#o=õ¶DöÅÙ*mGí¸A„.H æ²ªÈÝÓÊ4-Z¿žú£×ñ¢e÷‚gŽâ“Å 6TüÁ¢C ìqêÊ?w]ÕRß´v}ÇîàõC=íí›.nè½øñþ%r[AOSù:s\Ú@OW¨ ­º¬L67Õ×Ü3·YÓ¢1)%Hû£ðþŒa!æZˆ ð\ÇݯÙXd‰z÷9|!wqRvƒÄCï?,ù ø´äAð1á<òÄËÇ‹òM‚鮩,RÆuÂC’/€Û$ŸÃùÜé?É=.&Ì#ýÿÅh܉ńđwêñ˜Ä˜gï)¾êÆ[k¯Ûéq×4v®ûɧþüà]dk$îæOÜrÝÐêâšÌ öáµ½»Úñƒìÿ™¦E£d|Mbu.D(†”¼ü°äƒàÓ’7ƒOHî?.y?øIɃàcºk*‹ò‡q¹¡ü.ð)©žð£ËN²•OènBÂyyðøˆ¡|²r”O”êœû óæcó&ú[ŒxKsDú*X…܆ ¥C5áïû{®È=’™l-u:³ÓsÙÚÇVžÎ ±@¸ÊrÄúmÍ”¡Ï»·PO[pŒë \—[êpe³Å.“7Ðe9·û?aì©|Œè?~%"›un#¥ÿ¤ç2Î_Xˆ<¿~é­li·¨á ?Ê â(¯‚k´Ÿ‰âl¼ÂàÝà‡%ï?)y| ÜÆb»à³RùAðiɽàÇ%?Êî·*å¨Þ?zœÅþ,ÿÕöîeWšeM³½(Íš˜ž”ëÌLæ2{f®1ÂL¶ÛËrêÖ%™6+qN[vA–~÷ðùKóׇ–½|BwMe÷ ð°pU³I>‡nåe?ËÊ#Âï=?¡gD² ”U×à¶bCÈÛ9@j—~%âÞø©¡øáH‘1ü-îjèù‘]·D‹%áBƒŒºöõ*¥ü°ð¿P_¯ €~–zƒÒ >!\£^ xÁ •z™Ò~Rôg Bè_J|LêÏ ø´(߆:«ìŸÔTôrx˜9ÂÈ­¹‹¥Y!Æå¡³h¸”¿ãïñ;ì{ú ×oig{)®×®8"ØËH"?"«BëqŒMíÁ}ñ"e?¡=a; „ð™LçÌÅp¬C¼‡©âZ'\•< ’|AxÛWàG0MÁ;™‹ý†ÿ€ŸÆ7hOEÛ¥~øìòŠ+yX8f®x…ûè¼þ.ú%ýn9"Úÿ±‹3oðµ›™ð}<ÏçB>2Óý™&uóÀåêËÑÂ#¾7¶Óy-W¶Õ'‚qæ[¿ÖÛSWZ]Wj·Z,¸2ò,­ [:*KêÏo6ç»3Ó 7DD¥0ÐǸ>Œqu¾ëÂç.]¬“· Ó]SÙ. xïë¿EÙTdõlˆéK>>-ùøQÝ5ÛµŸÅ åƒàcP^•ʇõòZHòá|‡ÏƒEôs(¦”¼ü°äàG%oŸÜ ~\ò~ð“’ßþ¤äƒàÓà*ÛKåwOIþ ÝM(¶ÇG$¿Ê'Jþ(8W óæcó£_ÿ„ߥ2G *ÑþE¾@N"EDå|؆ß#âw}±®‰ü)Æ6Vq#y¨qéÖ˜Ûnšm‡Ÿ¯£âxGïÇ´%üÍË[oyùiÈÈÁº›vÜ58tÏÅø¡ÈÛá06GB……—îÞ½îîë®»=Èvâß_½oßlT:Ú/*¥( ¹ø}ÈWÏãk |@Ê{Œd­öx“L¦Xš^1H²¡4ÑS˜Q˜âµTÛk[—¾{—QÓDØù=!Æ»Åò‡%ß>%ùøQ¼V¸ÊFƃâùÆÍwM=¨jù-zÅ€Œ˜Š1ž{‘¿»RÍ„×moö—8­ÑÛU™˜zÛòôæ˜aäÛ¾|A–¥AúÄMÆYà{­|µË«H=Ò½™ÏB–˜*ƒ‚OKÓ]³±}PðYÜ.\• åUÉÃà!ɘ#{Ëq(U<ë*b‹yûVœü"ÛfÆv‰Ì‹ÝfM‹î6ó#Ÿ#®× Šå%ï?,ùøQÉ›Á'$÷‚—¼ü¤ä{Á—ü&ð'%Ÿ–üǺ›œ’ÿ^»±šõ”]àÿ¡=å³Éû(Ï@‚õÀ¥CkW—mZÕZrKoçõ]Wb³:Ò8¯ØÕ_ÜX9Òµùcê¿ÅsòGþ–õC„Øû/~=kцH;qu‹µˆ³ò±óã;|ë;|ÑH$÷]RlÙM½ûmÆØMôé’û“Ò[_½¸¿"­š<óGQ‘ŠœÜ÷cKL?`t2~Èà3$ ~D*? >.ù½àÇ$Ÿ?!ùÛàIýÜ®»©gÊ/BùSP^e™NàaôŽ¡3øärý$Sq€kÂM¬¼Á— ýyK©‚þ$Hý €cpÕ0Ÿ>6Ÿ1úµ ~g†ßÉž&ùàY’×gHíÏAû'ŒíóÌ0ÏV)Cõ°N°í=O¬$1é­rÅð×+Û«rY~Xùʬ1_±+#=7ÅžUÿ„Hûwµ¿¤Ý\ïTW¦¥5æåXZÓ³p¤’‰ò#ZVCVí€ç]³Øbw3RÙa/²krŒÔ¶L·9ÓA»š—lϬ¯Õ»ZåwVySSâÎÉvK³gdÑõs»«ðóé,ímjB1sílË«ŽòRYöM4¿LU}>>€†hÍÄûÚ=´»…¬»—jz-®þê¶Me ‰ü-©·vU£¸è‚ª¦ù)®v{B!Û׫‹¿ªª(]ÍÓóÕñžW—–V·\Ýcq'µwfZÖå:«‹ÊJ¶;¾œœÔ ¯ÛñúÖqEœUT¢âR±2ЕNwr/ø1æHA„z’Èõó¼¶p7Д¿ä-7)ëhÊ_óŽgMcm£=y³»xÞß3CŽÂO^¶žf¿¦¹·a4' ÿzc|HÓDná Q‘+šAÆâÜCÔŸ_Àª¸_ÛX® õ|îO‹ç+žC¨¨àp6øcdüä‹à§ þ 2>*¼ŒöóJA**à~šÔ鎟gå…¢°p•ú“àal’|Ü&ùÓ8ŽÞ®Ÿe1R/z·<Æå¬ úú•ÀrwÖ•®vÆÝtQ¢âX“ÖPW“WÕÕ„7­Oýôuk¯n¢ÙŒ³K­5ÏŽ†Q[f_[×züëÐIŒÿ_|W‰;L2B1ü€ÑÉ<ø!ÉÁOI> >.ù½àÇ$Ÿ?!ù ø(¸Ê22ÁÃRÿÍà“R=að#ËNR¸&±ò_2ÔóšR¥»)Aj7ŽÁUüùø¼AyΠg––"?¬äla‰•b*­"ÛôšÊö’¼ÄÌ”s“N[jôUüYž}J×pµ1‹(+’P‹[W‰œ(ÑlTèSª‰ “{”«SöÊŽò‚¢s»ä¨÷”zò-æÜdGVýw¡WYé+:•eMLªqHý‚9¾æ8E̱—ú_ðëÈ„_ãÿϳiù;½[¼ÃœEÕÔyާr3õBñÎà7øµàºk*˨ã"}Í’|A¸]³Iþ4ú¼(/ûYVDâ™ËÙ(oEÎnÈ:UغÛ{S\ÁúšàME%«üct­¥ íWÅåÛ"o¼ÙRSù;q²U–ïù¡gòw÷GówÙZƒÝ?uíßÔ«È<ø!á ÞDÂàG„Ÿ¡^B&ÁÇ…¿N=ƒÜ ~Løu™?!µ»~Jô³!4«˜Á'…7kj4ÿy¸‡ÅÌh>ô CY«ç#£Çðóú7EÁ=øùHv4Wóqžãü¢~dHZ*Þ¾=ã¨7‘Òý4NàíÖh6–7 ågÑô~JŽºöº¦b›²‰;ײú—ÿŽÒµ.6ün9Û!¤íÒ32¿ ³„„,9QÛåÛ»ckñv\µœ°=} å¥}«Ë#©Ûb åU(OÜC’/ïBˆ6?çö‹ÌA;'¤ú.†úR˜#i³„tžé_ðÞ¹þ lg0³åêÎΫ[ZÙ?[ë»;ë»:÷µâŸ¶ò¤ÿ›ZZnbIÿ#/ nÙU‹ß©eíóvØû>þ&ïèîãkŠ¿Ž¼È¡ý÷O^yÅ:¹åòÖ¶+Z†í®MÕm¡ …DþlZ_¿ê<»Z¸±ä¼m®ü kZZjü--8åO5UeÍ¢Çõ5þ†æ«{2ó“[‚™–5¹® nûHﺑ‘u½#ÿ¨ooeýdÇÕC~òá÷ñ#ÍÞ’$S(%cí¯c|+GKKK-ùÉŖʼæžHEìo'Ä™åáw¾;Ä ZQ,ß/¹|Òè$ ~½%\e#c+<×Ñ+à#î'T¶—fÃÀËû*Ë=úÓœ*æ€>9y;³Í0òÎKè ¢^Ç+Œ³À¿ áëU©¼^!„¿Æ×“rñ\ð÷\ÍÆ¾ÔŸEŸåÞIË—,‚Ÿåù—d|Tx“¦Jõ„ÑgÁ?Fîî‹?FËþ®½¦…¤ß-D=úUˆþ>³ìdüš3ø"ø)£+t7¹Ámì (ÿ4úWLŸeõÏ€B=ªT>ÌÊG¿"Ýäw( ï'V*uµ­¤®VßR„s„…@ò‰ývÙw©ÝŸçöxÝm¯ðZn©»b]ëp¥£Èá© x¶¤æÚ6dgzóœE.w ¸ ÒWîr—»".üjûEåÁom±7'·Àé]]Õ¹ÅøÝ™9©•>È“><ËqÒÏd î_ž…R|eâÓ3M_ÉDˆŠ*ÅSKBº+à£è^ôUî6öU øÓlÖbø,¸*y<„¯7øBÔµwx»kÀÇY»ˆÀ×it?‚ÍÂU6"ð0fï;˜º˜ñóH•£½+)mr>RÙQ—BG€û\âÝ›Çs-kÕJyé úí#Ú&;¨LoST‹ëþm>KüË6~ð‹ÙÞÓ̃’||\xªÖ ®ß€¨ÅôF'óà‡$Ÿ—||¼œ¾%<Æû‘ Ú5ƒOJõ„Áçßµñ~ÖŠ»äKËNÁOI>>*ù<ø!ÝÿŸµÿl¬¸ú‡aͽ²ä"Y½—«^­.K²$w¹÷º»Þµ×Û[½À’Ýz'@¨¡&´P7B[R) 'ÏŸBÈŸ4Hò¤‘Æj¿sGãkÉk¾÷},¯š9sæÌÌ™3sfΜ¶±÷æζˇŸ,ÁÇâ0Ný¬ž¡ÞãY—E}P(–ÎŽc¤=†';m·*PmrKºq]ÔÕ+GÚ¤!–élˆ$Ó‡FBék£ñÍ?1 9£(±½«cSÌîk³©sÍ-=Óû¯ßæé2Žî½~vðHúsôÑÅû³do•§ û,«ÞŸ] uíÖõ[áú¬%¥£—®ÏÒ_øfŸÒˆïϪ•ýE÷gñ¼ÂþSɼ¾xw˜)ºP e¢Aµƒø>ñ]â…Å«Åúö“å‚ Ö ŠÒ…[ŋ׌¿Uðˆâ{ØÏÁ˜oÐ>U3GõÐ!ªìTòâ»ÝÀãëØVaðZÓÂî-››7â»í¼PdE/ë3!›#«‰1ë²±5±ÁôS欇q™íLÀÞk]cø"Ù῞]ë£=˜¾x §C”¯a&·£Ÿ#!=Wàï±§ï,ñiâ]H¬S{Kv¢ €›Xœê,ÞË*XÜ8ý—JÖÆXaüÇœÌ-Ð+߀5”Šg.h¸â‹ÕBníD‘Qý¶-ëaª ¤r—}‰ò¥LmF—cþêýMc£hÿV¤Í2Î\rx®Þ×HzTvTY´´Y¶4=HGRÖ‘1€Ý-,]sAÔŠÐÊŽ—S»nXWWÒ‚²Ú*M}¿”Ö¸ØÛÝCæBÏÁUk´W&¦&ùÂv÷Ð¥¸ \\‡WÀ—èà<=E¦žðS#<ØG[ÀþS¨´Cª(Ö” ±ÒAŠß¸6R%˜m›T›/Ü=|°inh £?ö†\LF%«Ø™[ -½ W2þ˜Hˆ[‰a$6ÌuE6¤Â¦‰àDÂ5Å”}>ε++"±0ªk?AÝÞ±+mïO%Æêù•“CÙ™Æ<#³)’ÌL§¢ Áþüs6hn+æï CkÛûb}™Uî±à£‚óC_¦ÖôB0ж½Í""q+ÒýÃMzÃØÓž3^zm:Ø4TXíâúµ}½kurKr;”HÃúw 7¢«F¡ï¿ýXc…¢‚_®¨l<÷¶vT(ËùByeç ´e.TDаêÂüsùëV%TªFõÃ…˜!jäª'³sé}ì2Zš‹¯îS•ÝHÛf[Š‚!S—‹…2“ÿw‰ÿäHÓn5³ãéSb›³Œž,+GLQL 챡^Å<”ø«Jnþ|ZÔ9,×UËUåÕ*Á8¶vΔ•MQ´Óqê½Ò <ŠÛ÷Wò Dâ:›JWÙtü}k{4›ìæLS¡ -ɹdzÇ]o ½Á”H«ê0wdj´õíے郰‹€Âñt–GêiÄ#×URÏP¬HÈŒyÅjnˆÙ3nø‚íÃWÝU³¬šzC 5»g:I ۵ΥJ‚öê‚Îû>ÜâÐ?ã­Tg`‡­p©ÊÆŒ0Rù_[ÖÊP\Ø7&4vÛd ;ŸlšÏúRåoGE©6EªM"îV«  ƒûe*“jlá!¶ Š+¾B vk±ç ÷³HY¹.ŒËý{ûŸ²ŠR+«%BMMëHçÏ-¹C©ÎL ÏŽ³£E¶ë­ÊzMuÌ%,k¬¨d+n·û'›ç&¤â^•ꪳÛ÷·'.¨íW€Çb]Yù’àFl–næ%þþ]O»ÎðX`M³KÔ3¾éXÇÆd@—Ð9¬c"s³7ÚTůI})}C§¸Ú¨kO›­bI»Aeª¡Ìë¹2¹5:ÖN\…—©±"cV‡.© &6vĦÝ]Hãj] :ž¦ø:‘ÊÐ.[ÍéñF±ZÜÙ OùŒÉ~USÔÛl†VuB™y(SÎsqÒò2ìÅEíg5wh O„NO:9ŸFf<:ÓkJôõˆÂÛÖÉèR~\X"ÔÐÊL9ŒMã•Î4µ~(;þ`=# ¥ø<YÓË Ñô‹.¶-^*ÏÏ–xœø¨+^lÅJÝ™Šeú (-‚ܳN'“3%­n³5­ ¦f#öF­¡-&5TË…µ"•üœ Z¿ŠA­É×5šA¹$ÕУ26íîî= tFÊÕé./›¤ùpm[²qJ©u'⡎Ž—8Ò‰çYK<¸?#g]±áSÒùòõ¥lXH¯b>>ý6¥ ¸®ù¬¶ö}Í©^]Lî±Ä†ºš¤Œhæûºu¢Æ}ã°ùgQæÄŠÑ‘þIQåüà+«'D§í8fˆšgåγÈJf¡îV¸WÙ¶ÈO²]ëcEE¾fµte$Rß"}3—°5GÑs¿6EÍæ¨é×ùl¤yОÈõ©e¾–V¯LWŒô(SÊÅ)]+¢}C/Y޵‡NܵÑì]\,hðÔÇÏm¥;RÂ3#Ù&tql#Î&ÙŒ½¤ÿ)¡]þ[ÄRg©»W±ÔW*Ûì{‰Í^Z41ÞË&±ñ(”¶œçaœ½Ò¿±>Kú7…UüÝÀÚåTެ¾îÆvy>Z@šøÎñbL·,ÐßÄ}}3¨af&ÿ“ÞbT%HY{FT%V‰¡W* W í  _ûd¿=]‰>Ï®û¨.n×–í;Bhǽr–·21˜‹ ¡÷ Aþ3£®ƒûÎ9Ÿ@w¼‹v… ?× Ý›ø)ôé Þ¾,Kï#Þxgn¢³Ç1÷¸„ä !Û× W5/íj »˜€*¨TµÚ°€l­gÌÞÄÔªÍúZÉdþ-êùSiiE3P,¬ƒJ«üN¬Y» -X5æ“Þ õ‹S:»[0k8DµA^šÇœq:•¨÷+lÐòÅ›[x¬eÒ“ÇíÞtÔ/Sˆ_þ3:v¯wr³'9Ërt2 ï–3­‰pB L¾?“ýƒB^ïÜ&½ùJ%‰1—ð+à”Þ7ßµƒGß™JÚ-¶ôOÄ{ó3ËÊ;dµúbì’ÑÐåÇA®¸A)4±øKŠC]YC<\J†”ÂJ;|®\ uÀÜáÆtý%ž¸sÎâdIžY [Ü"8F—¯t\•Ææb–GïÂ7ö)-5òòúJ”‹ÕµPÄK×­tqÜ®â^x‰ŒNÀ­Ä‹gï•"zAÑn¥Y$+¯+WK7 ‹£{i³…òvªº4Î×åÁo¡¶Ô0·Þ·­¸Òç:$UkÖ”®ô¥êŠ:¡¼wOÔVºÖÿ×W¶þ~Ž>$±A$6(>FbƒÊºÎ¾ ZYÃç vž}HÅþ}ø[y#ã[o} /û,iö­·=@¢gÝ îóÙ”þZF¢Õ4¨"É6«ÛhrÙDk{}Ûý*h´ªÆáv;jªFE•»¯wí?Ãt Ý ã'¾Qô úûgñÍÚÙX8õéíÍÍÛÓö3“hkK$Z[§#JìÛŸHìÝŸ˜œšžš¼h|œW°×¨ãØ ·•Îß £e!ücÂEÅÄ\ÜOdjÞ×Ó¸¦ÁŒÔo~ûÛ¿Ì¿c ÊDºêj(6hM³“ùø¾¿|l÷øçF…ekËø(Î/Ov™Ø2½Pæ×@Î2n½•¦H¥`0“åô n;Äxwx½cèðÀ®+Ú'Ç‚£Uüê‹Á¹Ó«µx·nëšvyD û£›ýƒÇº·žß‰4® ¥¶lÉ4´vŽí†·¶ü¸êN¬3%ø´(hŠÅئB\ˆB+¸víDÌÈ·sC×y½½çu 2##™iøltŸ×Ó{ì¤}rÞwPrïððÞäCÓÓ…ßS”ÝëÊC;úAo‰Ià’ç(#“W)ê J»¹¬îUn¦ùÞr祓ÐbnàøÿŸÜ:•^ÌB×AÙºBŽ`-ÌQ)^\÷õ¦€€c‰ LÅmsrºíâ@h‘­SáF5Ö¬w“[Á ¯t2ãââI«µPDÉÜV…cämáó!FÞ7‹Êè€Í8!hŽÒyǺõ¦ X"QK 1ÛS.˜òãéüÐÒXã|ˆ>ürí1E×Ó±‰pbÒß“ðƒþÄ£ 37£u­©œñÄw˜S~÷IãïYš  ùDÍÅñš5öP„ŒE{HñIt2œ˜òõì8‘ð…B¾›-è[›ºM É&¯w3 sÿ‡/ñ°. aÿj#‘`„Óf%Á‹£âA[Z&>ÍhùÉq«ËnhPÃ’¹áð°>ªŠè,LÒajvoœ‘5åZ=XòjËɆ‘ðu”AbTÉ4²Js6€Ù-QêZ}ˆ‰û²SÕÚTÐŧÇp à3gqØÑ•ÎDg,fδ4te”fXãÔUØ´Kö$€©­\€ÿ}ê'ì4GÖm£@Ï3së¶U !‹¸UÊ!ë¹Õ CpZl3šÀš‡§À±oœv—¦ o?©Z+qv›ÞrË̯ƫÆÊÙhˆSe·à8Š!´<¯rbép.W±ø- ¤X­­”JC'7¢û±»5?9.ä «ðª†½»„W«*\k“lEGŸh}œ¿«ÔÁ÷ [Ëœ^‘ìwTUfQ—sK/N—Ú#¬V'ªMѽ5ÔN¾`¶ÅãvÓ Sº`l‹(³1Ö²AKÏô™'²©ññTvÂÜ7Ck7´Ä6fH$È¿àh¾l)ÓD˜d‰¦÷‚ÝÍXV$ªU::§Â›E»ÏBêü‡íécó§Ê7ힺ¬Ÿ·,*¤$T´§(º÷үVáU{ò”¥Çjí±œ"Q!Ù8&œ‹[ç_ú—òȇö#kÊ£—|þPbBò…µ™Íµ€Š„ ôÑ/ÍÞ¸Óü~^ú‰¥Ãjíd>Á¿;,Ÿ?€Õ–š-­DÅ ‹þ®aj÷2¡´ÜîIN,ŒVɪøõ•9J=º¡¬l-Uæ³Äóš?˜:s§ù$3xBVŠÀ|ÏýçAÉj¾H!Šœ{ïmhðl}Ì`ˆéÏÎ?vðÄ@_<9•¤µ#&.öã’Z2Ô¤Éÿ‡‚ü%Ñ·¦“žn›Ä…(õ^¶­ùÀóÀ3¬‰W[­ÅÊ+¹ æ˜:ëihNEØ¥Êo£m÷&ǼMó' Ý óÒ*åg¨a<Æ.RxhɆ+ÞGÅ#¡È²t’l“Â_k–©ÕAU$µqrbc2¢jÐh%`Á)mc~߸¨¬3-XgÞ¾µkû¼`·iÁn«fí¶Â]‡=Nüq™O‹ÙL6^þÃúìÃÉ­ñ5![DƒC:«›ËÕ.¹ÇãNFÐCÙ }Tµz’+Œ~‡ßîßèL¬';¯JíYmg÷\ì–kþgËs¿— ÂkãDNRÓ  Ç"lßñQÜÙ!Sè;,‡«†£ Ãã{2jVͽ?|ÍКΜZDZa88¢‡sÒnʸüçŠL‰n×KXû½n…¨êܤƒcÔrsŽ«ÃÉÃçZBÔ.˜ ä¼ðJqjqþô)ˆhß>ÊTÈÊ¥bW×*SEQ|Û2z\PybõYŸn¿ƾb•Û›`æ\2ÞÓteÕqf¦#9—puùPGp4LXÕ–ˆg¢96ÖfÌÿŠWˆ[¤¶BX}…ø¾nWjcªm9®J;ÃÉÞ1¤6¶Åš'<K‡ÚšFGƒù'yÖJ-6‚$9´ ƒ¦™¿½çÀLrÎÊŒR„ÏRÚ+²Û2óñûgpŒI¯Ì6f)¢+¨çv¯šÌVöºêGæü5ðñ‡…÷€Êƒð“?„÷K€«Ø!ú1ä,cíám i¡yixƒûì~½ÝèñáçM4|ãùG>𚯗1{¡ä(Ì]¿†ZUñ4ä¦=—×¾t{/sbEß¡šÙ……Ù¹……9}HÿéØOÞåÒÑ7žúæ7¿õ§¾1:¼eö`4zpvËáЋN|g48¡T °~>†ïD-·c—ŸW×ÔÖ Më‚õŠj­Toùœ]ÓúëäµÁWçÐ)JÖ»±²|R p:O}D޲M‰¸£läî‡2mT#öb3Ô.Æa¯fSncqÅâÔæÑ~æ :¼v0[Ë#9T‡C™±\þÞR?%¬ÆÜ…˜ò'X;ˆÆÜ|•H&¶šdþ3« ótêpŽê(æi¹ìVg±7ÀqT+KÑóÈÒ{&LÉ;'¬füŠÆV+“ÉMŠšå/ìÑ·>R.x™¥WNJ^?9'<âg[ˆììSôo÷7>'!~ÝNýŠÃ.ÁÁáß,ÁÈáO”ààðŸ—àæðï•àwqøãäî¹Ã“—Ä¿+ö¸]åköÊÕ Vsññ­¦zÖ²«ªÉ‰-¢× ¾œrN÷„Ä`ù¿´ÀÚø¯³ IaßÒi¡uŠü ¤-˜Å¬@ßY *…FaÑ,ØšZz:šF/J~¼qL\§ŒY)*__í‰F[.Ž·ŒSÚè· ÿf™¡\ÐL¤\#æCŒó8á‚&“V³Cq&…Ð$sþô5ôü/¾ñm”éDüçæßùÝüb #œ[¶'ÿ­Æµh8ý;ê[ÔW€¶µF­‹­ÃmCØKón¥õÓ—öö^:íXsIoï%kü‘‘ùù‘Èðü||{kúàèÈþTjÿÈèÁôôÌ@ÿôtÿÀÌש'Ou/ä}([…uy#‰é)á)pÉd3-D¶ÕŒ-mìÂ$‡ÞxáÀY¿:rù]·Îö÷ÏÞz×åG~Åʉ•‰—ÇÃ/Ù±ï&â£JŽý³:8R2k*dÞeÀ˜;bâüœÑ¯Zø$’vtGÂ=ŽLd_Õ5—\sÅààk’s]*þ†qß‘Éì\<>—<âgÛ„ežºf¥î ' Ô²²ý í_QÞ"úŒ–¢È‰Ña‚Çð..¤‚þr§šì/¥x# vj{Œà¤mqîqé~ŠsÏæHªÂ«TÖFžoùùŠªäØÁÖF,f[Ÿ;š66zztýÁjÉåâòZ‘Mh,§Ã°Ñj¤À°3ºÛ#¢ Î9˜ §æS—žãT)øô¤ ÂÕ0=gN˜'S¾ÁÑþ~–“ÎÓý”¯V«ŸV‚y‹;.hç¬F¤èôoè¯MøÜÙ¡ÄTÀ=a£ÊÐ1_s<®”Õ¶Àú/¡hcçqêêö]{S¸_Ã¯ÜØÕ±Î§w¤&Qy*áo³É=žÿH÷—¿ìT³=‰:Ý~OvíÙÎ>ÊÓSx­ª¡Ù õg(bíöÃêXCߪԹ³þrþq9ã2D.¹î’ˆÆzèŠîÑãë3{Î:·Û;ïâ!yâÏPW.š/¬\¸3þÇ4ªMon2Ç;­1GÖÒ&˜$ãÌyF&ƒ»cšøz«Q7”ö´Úb›D±í=þ‘¬µÙ×ãnëîj å‚bY¤Ýão—I¦¤Ry6Á¨š§›;v¤°&ž¾»êî¢}˜åÏaÓEf+>ðøé‰˜Ûj¯‰xÆw¬Éš®5Ö»lsMq]Úèó‰˜Ö†P#ì}7ý)FÕc3éâ4ZG7™mU·Y¥{Nw ¯SÏã(ú¸a HS1‹õ¾ÖЬ pW@‡×)”δ4ììXP Ðú²†5]îLkw³öI“Á&‘)ª+ý뇂۷­·Q453cX·c›'—®® G•Cl}3ж¿"ï!AÛr>$(yÉ„vÓ4¥·ÃÆZ°½‚fúÃ=ÍǸ31ýðdÔZ]c]C#xŽÔÄRŸXê†O·Ô±÷C4{ðàìÜs:ŸFãÓáÏsQÃ’qXؾå@ p`Ëö…À¯'&ˆ¿Uáyâ·$ã”mŒçÚ‡Ÿ–HjêjkÌðÐ"ÚÖtá‘€\"˜”ÃKŸþ˜ªÅ§M»®*ëÅ ˜Émr™ÂfÃ}×`j¬C4=3 è=*Š¿Y¨‹Ú}¢êôx¢ëÑ??Ó IuéíÙ,땃ÏL,Eãqöêá{Yr¤ä–Ù—éçvpY¢FŠ›œÝ !C—ÓÿìLPô•uU bÉîúóŠFH!JDµÕNÑ@‡dr‡oæõyá¦W§¦ë-#½I‘JNSìnÅkƒQ†[ÈÀÍ+Í Ðåò¶¦-™íëÂL‡ŽâSóžl(êkÖF£¯Lˆ_xêjÌM;@÷³Çu‡zÏ2HgD+>Gc*’’I=BV,ªEY‰Ò[Ó­;RHâhS`3 •ÏÜJŠ{‡Æ÷Ä6–ñ_¾6ݵ=¾a°ÃÓåéì!'ݾǮ<ÈŽtAT%·H¼m‘[rhö6Š’Jà]¼:I„Ê`±œ»¬Q%çó§@)”Þâ-Ü_F ÂVo´NºO*; Ú5c°ƒS¥ÌÌΆ`4|V«ºéhæs—cïc?x÷þã}üyþ»ùoi <†Û»€Û¥Yl‘%q`y("ÅÁVebíž ¿l_P¦k.×[+Û/•^öÁNQvOÛÁ‚d‘úäÐÌÌÄ|pCFØÓtÙõ÷²•á!ì~¸õÇZ*}¨øX±Ïøµ®52«b·Íi³}‚²£eÁl×ÛwZÝc£¡1¿zHô…ýž 묞ÔÁT¶§µ×v8ÂÆ¡ô`wÓÖ”³ñ }.nkmmÃgÚºÐÝÀ…¥H÷Ðgl}+õÏ(5j‹¹-jÊîÈøsªF‰UkÖGÌž\ÿm£Á#U›ªDæØpr`wÒ¨è×´yã&Q­)hW±ã¤j­†Û” ^€Èyµc“… €Ì±…>#GÁÔÎöÀ|Oh<‰tOf¶¥šãiK[íºy»Ï´A@QŽ”È·eü£î­ÎþDhPů˜ÏµÎæRq‡X:QëÏØüî»ÚæƒMmZr¦ïÿ¬ãÅÜØmXÔ VËâ’¹‰7$4s+Vü¾5õå5¢]Õ‚jŸQnÕÔªýF­GeœGŽ´i"kwï; °z¥J‘O×èU$wŒÍïX ®€t@æí$¶4?ùÈ÷!7•“ìm!jÞgx™ðz[Wl,5jOçÆ8¶Bpú½$½ãx–Çø~’žÄÇ!8‰RÐí8ýÙ„ŽãX7`üs„N~ géü»Ç}§?ŸÐÙË}ÒiWîßH¹˜Æ?Gðÿ!Æ×ñ^,m·’}ŽoÇ|oåúU!^â㣟\Öß* ^C$Îí·°1®¡<ãº$¯•ó8éç,ëÙsJÇÚÇ´¨>ô…½›1\Äíèàšlç(Þ‰9›-mÀo'xý²‘v=ÁË—´/\°¬æÑÎ{ôÁ•q¢%¡%ñëT7[szj(Ì €Ÿ‹ëx-Áws-¿ã_$øìbOáKpío(à´`1=_Œñ/œ¿,ý«¤¿©$ýo¡Ü{0~3)÷KËð[ŠqÞöÿ°õ$øM\¹aÌÿW };)w¿·ÇéŸÆô øc\úïbüA‚?²,ý×VIÿ—žðÏ_ÀøÃß¾ „ÃÉÜOÿê ½ŒÔë;\¹iÌÿË$}€+ׇñ“7qéßÄôBð—¹ôo`ü5‚¿À¥ã¯üY.ý‹ÿ/‚?Íñ¿ã?%øÜ2ü¿‹qb“©8›ì5°É*W°Õ^|)½Ãï[ÃKèl«àÌ*¸}ÜR‚ws¸cÜZŒS¿àøüf ~‡?^‚¿Íá®b³¾Æ;½"þj nãðûVÁ\™ŽÀ¶ ά‚ÛWÁ-«ØÖ¯!÷Šø«%¸Ãï[pe:Û*8³ n_·”àÝîX·ãÔÛŸ<Œ×*Îvwîÿ®ekŒWÉã×·q8ÇpÁKâ^¿DG`#øK€Wá Á?ÜS„Û þKÀÍE¸…à\Fws¸cÜJð_ÎÆ=$8÷ðï€ã8‰œà¯Žã-â-®´æ(0¾êZèô¯0ÿ\zuÉÚLÅ­Í^C^å k¶W_JoãðûVÁäð:Û*8³ n_·ïºaþÏ%ü;x•^Ëá¯έ qúÏ‘þ–Çé—¯E_|)½Ãï[ÃKèl«àÌ*¸}ÜR‚ws¸cÜZŒSos|>Z‚ÿ‚ÿY‚ßÅá—Зqô_uMðF.½ºd ¯âÖð¯Á¾r…µý«€/¥·qø}«àrx mœY·¯‚[Jðnw¬‚[‹qVþ„ÏG î8íg}›Ü¼ö1r-á¬|èŽR{ãµ'ö‡Û8ü¾UpÎ(¥#°­‚3«àöUpK ÞÍáŽUpk1ò!87¿gNûY7_|ÌûüŽå3ÌÍ›•^Kp2or¸Ãï[çæÍR:Û*8³ n_·”àÝîX·ãX>Ã%ó¦äûº\¿úmY±|fJÇÆk NƇÛ8ü¾Upn|•ÒØVÁ™Upû*¸¥ïæpÇ*¸µÇò™Y_øNT úö&ºWó&–ž—Žœ5g¸ÑiƬÉYúæ†[4»u]ÑÅøŒZÞ['hëÉíÙÝ‹þF}j-ãi8~°ÛÊ·Ú­B¨:þDï¨,ï¸|[ïþÛ‡+ó÷7 ~ºóâ{ ¿¾QXã=ûo7Oöü×'W6ÝxÃCËÍø“muÿéïWpîTÀï/Œ”ãýüÍÆæ!–çatïê1öÝs‰$$KI\è$Þs7|›€>ò}$f¿ ±7g¿lÏÝù?­_Qt>]Kö©Üé?¡çyaíF-{LÑÔ«ÝŠž¯=‰w‚·RˆQ$cg>$ǼüFìçNÿI@ˆW¼bÕ-zÅÎKü½¶Øa>Ÿø„Y§¸ªû–dÁý ´¾GEÑ.ÌËk,/+"O²‘E8í>¨A#dY0ß>t‹pÙ4 ·a„!ȹ€|#v‚ìäqŒXr>‚ˆbÝx¤.G¬YGeÐ ÔÛEQ†7ÔN°R(J,G‚¤Z ¬FÖv íð1ľ»Öô5Ð%ñb†J©Þîªê¶4¢r¥J®¡À³âµÔï/ïV’ âSì3%,=ƒÕ.»^ŠNT^KC#dÓ(”J|OëwÐî»—ÚÝߢCÏŸ¬… Šå9à»^ýÜ( ±AT«02ÕYCàþø¬ŽåJ-Sh|ÔÇ4hËX2˲•jž¨ØlE¼±DfÜÙæŽjÆ(¯;›Õ¢á`8¤5J¸yÒPĉoNb1…°ˆ“íll9'•KœxVáˆ,qÒ¦›/ãÄD8ù-º›÷ !㙆¹$ö qßKìݪ0ªk«j©h™Í¬dÿYCGʬ*-ZKygs–j‹EârS‡Yît Å¢D&=c--Yµ¼’š¬\`Ô•ÍæÊ-:iÝ% ÌF“J¬©.ÑËϱ’w½œø]/9¹*;ó8âúZþåmŽ%³«c×úŽvü¸×üW,ÃíÓ®uáxÔ·üDìâkñìßT/.Irº:JªÀÑ/dŒ½çÂDìb¦† ° 3ÿ™Äo«¢¶Wm¸^X]S©iVÔèMâêzq%ÚòÔÿtuýÏSvŠöwwù¯º¬,'ðÎ7Íηœºyù‰Ybu#=»‡N(Ob'÷¶ql>® F"í+[4#†ÿÓüçR³´²R*Ñúµ-C{‡šuýµDRY)a$M'>ãÄÞ‰›Ðôen—Ðßy;ªÌ|{§_¸‹¯r_–¿Yòï°?P¾}vü\%qJ‚‘Ö.vê"iëáPÖ1ˆ÷vu§;›šü®vc“cGjëA¯'ÒÞr–È´šb¡PÔ.iÕV×ÚÁ@_=8´3ÍÓ>rÚŸ8–b3ùPŒ=‘+fÏ` é‘ý•eüÖž:ˆáóÿ;º턃¹ÚèBèšüõ¤ ©.Є6ïé–} ó„úHf‡UD9²°¶×Üã7ÖK+ë$EÇ_xúLAR;i•ûÒüW R‚ýê©pª,¾ÿŠ1|HW ¬¡ÒrC­BX].ÖiÅr¦Í-\­4ÊÍÀûNŠ–µwÍ>7Ïâ½åú­û<¼UÎsg¥KÏsß§p_â-;Ì¥·ãôЧ–ŸôÅñÐþŠß—Õ!£C žœ‚ëU¢B*˪ÞýÜl˜˜ZxU;.¶Ò|‡}øÑ5×|têŸm佦ç ×TN¹Y §ÜpYñ„ðÌUwß}UðÊ{î¹2Ù:1;;Ñ:>;Û¶¥ÿØ‘sÀǦFûûG¾NÝ{júŒ“ÁЖ0.ïl<”ˆ (âÿË„Te¾káO }UO=žlΑîúHŸü2ü:3Ï$&!³B²Å èvEزÿ¼ƒ­mË¢øe›Ï;~¼+Q<§ö‘~IûɉjñÒÿGœØwâ)~ 1ÅrF†´¶¬2|mUh-ÊDóM7T}ò+œõ©§æA3” ¯»>²ôj!9C»1Ìû?F™‰ŽýíÙ³rëFÚs_ì§\óŸ8ÆÛº×ºÖG›x4î ¿Ã’ò”˜ï3© ¶ C]ge›Ïêìhï¹¶=°@]õ‰}¼PÊŸdPk'ôV¦6²0_ ´q (M–Áõbï4R·àò©/h¨ñŸšw¸³ºnßàÚ5#Ž]Ƽ#3r¬»÷ü»Qƒ(àëÑ[9°_¯î3{oÜ·ïK]Èèiì‡aïyŠÅh2ì¥æO[Œ,<÷ºvôH[ۑѱó::Îëé†ùÿÚšO´òÅš…”$BnÑ›"Ì¢ù/¦/ÑÓ®Ù“…µÅî¶w__³¦µ£ãý÷FÊ#÷tg÷˜4׺×=ñX,šLæIÆ1~ËPÀ[ÏãþƧ¶7â¾ÏÊñ (—)y/’;¬ŠãÉ–êŠ^—Xª$µb‰MÝžÕ¸$ ÷£¼©ô›§Ð¨HÀ_S&óÛ›Ëèü±¯ßÎ/sÏôv¬q/ΆÀ‰¾Ž¼†hų"Ñ,<ͪѲˆ€ï;LŽ^Ø?pÉhzÖQ5ÙF6ÏŽ»Û4YÇ–ü›Ð–]_Ú·ïÆÞ ©OeØMjÕ÷øl[.¾ õ—”·Â=Žågáy*H'þ¾Šå’ÕÑô ÈÉÌs©¥—º/«®Y}m%Ýx¹Þîï€^ÝÛõÔ¹"µXlFÇ“Ý×&ŠÕ8éè”kàúPæÛØG ø4¯Þ()VlåFο: cç顽©öîk’lïCBóïáa„°v¸¨b˧¨­e2î‘¢çÙV…Ô;Ï9˜jVø¥¤Y7í{æ™LœmçÅÙbŠ}“‰¾…´m;OÀÍž‡¹Ùó~4Œq<[Ðmþ2«Š9MœÂº‘ÕvDÉ1B$=±ðï…oVrzxßS Ì}+õÝ¢hâ3,7ldÝz Ù¹·9½+×kïèè ü ½A}7‚éËvO;¦ƒÉ°÷ûssÅ7ˆ>kt™á¢±ó»»Ïë_·®`ݺcèÑÀÍ»wßÐ×wÃîÝ7Ì]Øøðþ…£¯¬_¿xC¨Ê`9¦C’Å«í%÷çéïýèsp3˜_#‡•-™¦ZQV[.WU\MùD|>ÿ}c¤·L°ƒâ[y ¹õ[:An÷ÀœËÞÀÒ×o®d/a=”ÿßSoàHµäŸWɽ‡Ë^ÖbßÀeïk‘§¾Á¸-¤çà ´ ì¯aiú̦$õÍ5UŸÜý™ ÊÂüõÌJàÊvº Û•4>‰ íŸÝºüåÁªÆÿd_>õ”äÕÇþ£Iáþº€¥£[’ÎÊâ (Å©_¯`*¾’iHæïŸA ¸·#X-HV&úžzzE‹s È‚ua‚>)ñ†ªS// þÂBþÂ&Ö$0Ó€)hï6¬½i<^pK,×àK*T†/ºd$½+ððÃ0«ÁG°ÈûoÜ¹ãæ¡€¹K¥‰oëÚ}Ô¤ï\æ=2ŠüÍöb)¹)]¶,LDx æOÿàhG®–_§¯é8òƒ· g¿õgtÛ½ˆiÑa¹7?ÿg¶ÕPƒ–‚qI «†ôE' ×Vúà‰‡/è¬RÔòEòÊÖ£?ÿãæÂªkç/íüðÒ)DŒ_Cp%Æ?ôu8ß’ÓÝ@Ò0N^¿¦HÈ[Z[¹¹Ùá8™°¦Sÿvòbßpéš…†÷¸YÄJf6m¾ŸÃŸ'8~ý$¯Å³Ûo‘X¡ø2P´°ãP×IW6'‘nßÏÈÞvW®$Œ‘¾ N”M5ÄãA]VWÏ,Å&B©M¢¯~éàu£ÎJ¨rkM]„ÈîÙ8ÑÑÙat:]‘Íݽû2 6.°˜§&6æ²Kiô¢Nãâ3uý(6t:=‰Èt㆞—Ãß?ô‚£èCeÜo‰‰ø•±P £C½Z­Ï?Åèt¦å± Ùh“9ÔÁ gòxP{z”2·<Õ²½ °fðÎ,Fm®¢ÄU›ÕVº›¬5Õ2I…>ôt_ßÓ³àÒñÍ^º]hè>™/ûŠ'YÀAôúïFÄJ§èø¥JMÁØèÃŒ°â:c­V\Q!©•z[ÔžömnM‹WZ+©¨¨ÓÕ¯;¶¥k[nëé~FF︓Ÿ}vcÒ-ØM˘þÓ¯Âÿ \#ðñÜ`ŸŠê2òßçeئb†&ÅG ML‹zFË/q¯þ6‚¸× ?^,#[HÅsˆ1wpÄ+s… Xhõ±ñÖcûÆÑGÏnt»ë@RkO>‚Zå°ù>w[HŠÛˆ`}.©:¥HZ&*ôe•â 4x舰¶\öëD;|½7õæ¥èªÔ9©{ µ§ÒP{†‹oXT{Rý‚fÕSêðò¾Êz!¿¢®"wÅV~ͯTTu æà“O—ÑèÃï© “P}//{HÕ‘Ó˜; O>iè·N‡ê!–wy•¬«e…¨ªPù…ç‚XBßýn^ú ‹¯¾á^éä-æûò݊Ρ¯ú6úˆ­J~÷ÂÎãZz_ÿ­Ç4Ôðñ=BƒÄrýÆ-è›ÑU30¾¸×<ÐG#D^ÎP/ÃM.3ÏÏKþ9#Æ{é;v…Œ.Ž7hâ:®˜}F€–Ôc["ãOBï§åQÍèúÈD0য7ÔÕYĶhéQ¿Þ\/fN\Ñ]-Å!rÄ5¹»îê©‘âg ¹+¢ª‰jªØ`9ì­¥ªºü_kÁ÷ˆß‘xó$þQ*5^¾g‚Nä/^x|¡GôÐCl TV€}OCúd: é%ìÚ¶d²‹Æ ¥Ð¸ÜbœÞu(~¾Á/»ür¯›O¿“—’>ò]¾g’;xÍÞk’ràÒR,gt ¤]mÏ„ú²ùºªSí8ëCÍ€ê*!@l®& PÓ­´gÂjŽ'î@²–äÚHdm²%17—€†¾ßØ‘Hv×tuBûbŽ?‡å#áFæ2eø Ó`JÐ=;ëF]‡ÐGKt>Ä!(ÜC_'´V¤”[è,PéÌÎÎf‹I@n9ôÕ¯¢¿ÿç¶Ï=á7z¥1C¶'—2ø¥!õ@°yG&½ýâÓ"ƒ¡U¬›”ˆ[Ô¦ø¾É±}‰w!`&_ €h×Eû$8ɧ­žr »v¡pvS"±)Û4ŸLηÂZÜ裧·þH”Þ?sÙƒ$ÖàчE»$¤Œ’mðu ±\ÃL:2¦†Häµû×_ôˆ)—llÕåeˆ•Ñlqä?ù½ñdñ«Ø”,yÝZ€2…—|OÙwaÁl+ÞÇfkÆ-­…çÔ]rF$«T‚:¥Ï¬ èöï/Ów5LÍDæò×÷!Ý+åeãt™ÿðîq“NŸÿ/xõwFç¹âêëîm§~G^U‡Ú^KÛÁŽy—œOú+‘xòßÂ-úé;&¸E‘¼ugškäÙ‹{ÝŠÛ%µÐ'‰Q(ª ö`ŠíÕ©ûwª««»Ð„{nÎ]˜j¬Î›†H7GWµ~¡¹£gîíì¸{ß®Ôpó@Qy0!XÇÌÇ€~8 ôF ~0Iš/¼ƒêŽâ,@ãXx¶Åmò9S¡â¡ÅÙ™¶s³ó}ìì\üŠý2kóõ'úñÇÛ—v:~Í¿uá©C7ÖpÚSúТF²S{kÅFiÄcì MðÕË/?ôøãÙ‚6ÈÆçæâèÑ™ùù™œFžH ]J@Iy$Ö %Fÿ ;Š•w$¾ý5ˆ/¯ä—×UåÙ”\]#/«®«+7 NY»m¶në¶¡AÙ&¼!ñíÅ™õfê¦Â¬ÈÞ°„™õ]ɆŠ×ÐGå?.ØJ6ZŽjSÉEµa§Oeÿ_þ öéSÂɧ°‘ i+`Îù+ú¸t/â3’ÔÚ+ªN?“9Y˜V7*YÎЂ`U’={Hhÿì¶åÃe 5‘ÿd_>ô­oÝùŸLÌÂ|GÙ°TôKRYÙDßä½+™ƒ…Ÿa’¾wèÓ< ÙX…¸ïàw¬li‚$=“4ÐîÞ³gÎÕœñ†^…¹:/Å&ár…›Ô£¬œéI ”èÂUø3¸»ug^ûMØhñ&ºsq]HÖÀŒ²r.¼³jPvÙ™F.î1èðÈ€Xù¿"±÷—m0œøæávZ+sI¥.ÙÚþeƒÜ#“yä ¬´ˆ—¥æÊ²¯d°Ýo¹^ô­b£­@w™éƽ¦ý<Ì f¬Añ*kÄ_¸ ãøœÎ¶´ZÇéÞ&éªqê×8˜ki¶ÆéÞ#é*Yœ{KG€þÊãqë§»9 ý8ÑÐÜkÙôoK^Ë.Ñ’œ&¿‹Óä—¼Èçð€—zûlïÄëiø°—D…ÒcGþèxIt¨‚“ÿ©§–G‰ZŒ.<â²I<%ø{¯* Ÿ¢:³Š‚­’¢ø ¡¢³Û•z½~ÎQ ¥ÁpÛõ=F¥fµŠYü}ª©à¯Ã;<P9OãZ)–fT®*‹þtÕ¿§àåØÃÕ]Î?G'çÆ[ªë‰ ûôÑŠÒ⺶ºŠRUmο0Q·°ÿ€gL..ȆDÐÙØ±¬H”sø[ÁÊ _I=ùYdÕ¡2™TJ£q¿ÒdRÂÏÍ T ÈÈlfeE~Ÿz†Xëìlz^Cë`Ü—ÈŠ«±A –ob®0Wo㪃ºÊçæRœ¿X#¼®@<7Žìó/ÌY~%±¸x äŒ$ŽeÂEö$çSgÇšÚ½¡k“…éôÅæÛƒqS­Bž6¶uk-MVo«µq›(²±uÝy¬v4“jënu¥‚jU¸Ñ +|A…pku -(¯®«QÕú–‘xû¶ÔÒýW`N÷­tc¿À„ýÌ×à€Eä‡ðüðÞqh*šô;Ú]Éíí¶Áþõ“‰ŒÏllù6þn`¡ÕÒ›jšòË=]ÑôLhcS4‘žî ø|SkÞ…£ éA>7ÁœàXŠ•=ó%+aqì©wgæ\Í&GÒlUÇÕ=Û`k²¨ 6ÓfŠXæEcMÞ8#ªÕiz…d@ah²Ç¬Õ2­TeÔª‡õv>ÅÂ/0”ì2•VuKà‡Í1u³7ÜÞÒœ Å›bÁl£×1hœM¦±¾äæ“~¦SiÌ„ƒMk“n_Àݯ7ZûS±)¿z´³y]pñ]¼6÷—¾8̨âqšïZ8ÿ¦+ñßvòí(ÅFÄv„m…7hÉ?`•ÿl\Tòø¬¢¾¼¶ªê¡GÙe¯ÎÞÈ>HÓ÷4”FÞCñ'Â¥µÃ:}Js“Ò>ýí®hêP´¡Ë¿Â+&r 0!&’FÿÊ™,²ÃI ¯ú_Ú¤ž_iwŠÿÿhwÝßQjJ’Ý JVÝvðÚ}RlWI+Ê%z±éÞóÏí¤žZnop-µL*²Z¥gþŸ­Å¡€]«.Æy|öÞÜï½Z0Éëäõú=K™UDbŒ)ð[4ÙêЧðÃF0š‰*à°Ð%·WI*ùå /¨9w—ÇÓå.|-£ÁjC”ˆ¢¦Å*Š—?¢I1é ÃŽ£ €êÔyº@ÓdN¯v„´ö®p„]¶DÂ]ö.Ÿ×çóÁÇ÷ãèëZâ^꿚u ðÙÓS 9^qÑO Ø_@–·!¼¢ýCËLÐÓÖßæQjõv_ÿú€£×¨÷Ì6$¶œ=Á¶~g¶3ëÌ4ØrF{J¼KÞòGB¡ž˜×2©ŸÕµ~ý˜Qmn¨òš[÷ï z{<}wk4Ð1ycƒs{s×Eɬ{øÎÏ%82اÆChã¡Cçœ7°ÑÀ z¿Fã=t×]÷Þ{×]­ón›žÞìvožžÞæe%Ñ /RGx¢ÅwªØB—ê¾êPC»aòÈôåî uý©ÿ»MÍÀÇ6uš:SïγO|¾3´¦Ô÷úìÚÝÍ»ZÔ Å=]Cžå&œÚtCdûdòÀB ÅŸ'/Û¼4Ëôª‰¬îäýß¾m渮Ûj6 åÖu6x¬Mº&çLbí¾D8ÞÛ´Y¶õhA©A=Þè¶{Œš.‹gr0Ò%å×fÞŨԿº×±ç¼÷ëÁY‘€ýë*C|‘ºf/šÍ»þ}ºLÛ§U5¨Y;„.=uðý:ä6®´ë&5®p©ûÿ©oÕåü‘¶LXïÒÅ%hü5R»26ËlE˜^37ÕI,(1ÿ¥ÊJ×d{ûŽYîRÞ)H§l5ÏëÍ€$ðèOçþæÄ˜6SÓ9Úm{4ìÓ¦ÕešAËð–ØQËe;ν%çÒç¤Ú†ÝëmiJÄã×°Í=;Ÿ™ ³%óx`wÁ/°§VaÔBÈÂ;ö¾Lþ§è¡ÆœSÂ?ëøãó¹ŽóŽ]¿Ž¼Ýó&ðªn£Ë£–6 ÷0/î³4±mp…JÏÅÛ½–Pp26¾9jô´Dg*MƒÇ47ò½ G»[kkyûÃÉA%_ †{]ëûü½r¾ª/è÷¡ª(l…{—!ÿzÐmö˜Äò”Å×Xx+÷ß’¾F) VY¬»ÞJŒêX1:[ÏcŒô>‘[ÇŠqÏ +ÆX£Å=bõÌÎö-—î!ãh°úùÞux?¨hô^N¢v‹¸pÝßœ€´’Âk·v»BÀåÉ©‚Tæ©×ë½>£Ë;üfiÖ1™«·2&{¶±{7Ì}P¾ka'w-ð›»“Q‚÷b|4ýEP¢ß[âð-šc- ~õhá 5Ðûº}µä uüZ\|ÿþ¢ä8?ïCôB~{LWAåßȯÓI’ÿø~4œd?Uå3›½^³Ù·¨ÿ¾ñ T<+p`KÓË:} ­(æè˹ӣwÛÀAßb^Ó8°Éàõ¨ù!£Û9t·Ç Æ­U¤lÑa[*NÙ–&w“¾³å‘÷š ÚòâHtÅ5x¯Jp«ù[ §[ï"åzHžY,¢¤\JÁUñúѼÁ]˜2ö~ÈW¸MÁ?›¯@<±ÿÐBð=º¾·ñGqËŒâh-ˆGçð^§çÕ“^-(ì„­KìÎÏi.¡W¦ÖØ­èœdô‚–ˆ"åéKILz£µ ÑxïÂ*IBî%@_+îlt.È]©¨ì:»}r»ûSUfÉ–Š\œè+À?Ž9g9(C,ý'ŸÀøÄ"~zŽ÷.ïfŠEx§~^@Ðc¼›!å4N9½X7àíuÂÛ"Gä% ¡T®øÖçœvÕ®©VVz….à­å‚HÊfÒ™«*Rý‡°ïíô{Z>f€ß@ñ­‹/öïÛ—ºaüÛ-·BŠ(¤øh1ü_!›÷íÛç»$e½õÛ ãÀÐ@×C Ç_ÏGq=¢€ßŠñŒÏ°8Ptñ^C*dçÑ@vT\|m|¼€¿»úL7É\èŠ7úú 8õqN½ÌáeÇ‹ð²K8¼|wG0W•ïþûSˆYü½Y\£8%%PþFÌy†÷gÐ×nbñònÄ#ºŒÝ_ˆ-³Üî1ÇM¦¸ÙÌþw9i³%Í…OìàmGitw!"dìSl¾{<G§ÇÛáptxm~¿Íê÷owvz<ÎÂg:êpF"NG¸1ó.FVtáb=̈¾8ŸÇ¯3, ºp!|#6áç§êó‹;îX@úüûÉûY9 !w}•I1‡\x+}ëÅ4Òä?‚ÔéÊZt¾/U‡SXCvÙ·éoçÿrÿè:óOЉ;’‘· jw¦$•ÉÊŒ|¤ÎÿjJÜJßüTÁ¨CÏbJ$E,f¯ÂbwÜÑöÑùѱW[€« 1-*Eß}+j¼u ÒÐtþÌUpu¡…SØcÌÝßÔèý=¨Ò=qËU9pe+peµF¬`ª0ù€Î¶Ûè[ó?dkøgà+Eø"iì!jÌ?:¡fýlU¯žGáSðÎC~t5Þ[Â2-n5R^Ïåâ±®®óP}þ¢Îh´“ýakÄã=‰:Q–m¥°‹:Ÿ¼è¢Âžê9è7§O°¸pzç¢NÖ+!Êÿ‰ÅaÕº€Ç |ž^`q* VØyF¨§xp%•=}’W]ìÃ'„3×û¼îc¹Ü1·×{ýoÚch¬!ÖØ¹zᤚz—Í¥(¹Qô“tGGº;‰Äo~÷‚ ÞÝlœ}s÷®·f±uÕÏûÉSr+ëNß 9gßÚµûÍY#Î y¦OoEêûlT°k/ø™þÁÎ7Ñk³§,YVöÓ[y‘ئùïÜùƒ›²Ô;ÙO¾Â¦0î∛̾ A"¼röû ¿É¤*,Á ¥"•‰É-J>»*â+-ò›’Ñþ€Ö‡rȧ ôGYÆëjˆ„*ø¡HÃËË,r†l¤\²kÁYhÜ^)O!»œQñ cä«yl‰‰›V(&1XÂÊ@²Ð'6 ꇸOHB´èWƒ¿¾“ÞüÉ-ìwS§pòø;XÍNõÿ‹þÍ'J rõ’\tHòɯErQE¹@ÊÈGã¼ýýЛŸÍ7ŸúôbžÕ%~t]Jýß+Äú=R°ó€Ð~M8±E²»ª¹j·tk2܋ި6z›Tûö©š¼m8.mÃéÇÐ7!o˵0Âz b°cù¼ðæ›…³ìÇ ¥‡Üw|œz¿8‡ŒµrØ1.›à¼\~LrW\}K‡ûWá•Êo¢7Áþ2ñZKW>>×¹õ|hž˜=JТ¥ùîÑ9c»*nf,ö¦¶&{6Ðh²8ÒiÇŒ»ÅjudsYÇø`d\SÆtfƒ=Óž±'z¶´J ›ÆFÞñûû$‘j‡NçÐëí wvôpÕdptö¸»iئè‘jµr¹Qcô¤}á6l«þ…ß2¨)Ä{¥—¼øè ÿÕy ­õ¬vÏ õî)32æ{‡×»òmìÌ­Š† Um||c>\Œ˜;õ·>ˆñA$Zô Ð õ àÃF×.¦§îÁø ÁÅ‹é©#EéÙôÀ± è߸žçbí;ëY¶¼Å6¦¬ØÆüZMù¡¨ÞªŠz™Fm‹mS<12&碙yr¶å|Ú™aÓä÷9ºþÖpCÄ™šÄÒüú[6·AGB K µ²½ùRTMÅly•ض <¿„ç^ü?sm/~æ7<Ï}*¿±«¼]nˆ´õŽzÞOgs=Ú ƒÏߤß|o§QüiQn½ˆmbpÈ“|ê-<žñ†3ÇÛ»‘©px*Ï©†'£llø©1û,ž¨É¬°ÔI”(šN$§C¡édb:äïè‡7àÆ“?Ò3öºlv“¾ºŽi4Ú úî+Ó!G€êð2ndkWè8€7®hé£ \éØ e^Ñq‰] ›TêUZÓ!}Tbƒ“Œ’z™Ä­7g–3³¡!X'ÓÕVÃÃv.«#*‡ªêë«DuåR‘ÓjM‰ÎäS |Þ |Jx^àsÊÌ« BõZ·Š¯t­ë®àg’lé¯d§ÝNc¨ÎY¥ü>ƒTSãt¡¥¥ŠÆ"½e}ƒ¨: ‡êj| ìÍh…‡ $°3fY™“xéú+îg0ÝÞ3:Ò¥S8mÊåÅ¢xo_ßpjcÀÃããÒ¾†KÓC 9?µçœÛ.ìøÂ:.äát¿…tþÅÕ±5):à5+À¯üBʲç àóAVZù¿ñÞDV,-R2 ÏbäU\2…=âè‹P²¢do¹džy²}b¢½}r²ÝàtàG´kãÜÎswµµvug³Ý]­ÀÛ0ˆÐEì®ç ºÂæ•w"kL ­ËŸÃÖÀ9(O©"gúÞa 4 ¯7äVqHs¥{;C—:ç&›Ná·:[ÓáuQÖé„£AvëÒ«µ@lE§OaÞüv`@³D5öíî£Zj© é²îH¿Igô:ZD.‹;¢×$#B«ª÷ÍA“·¶²Áj‰è¡49 uÃ~.¦À¦)õ{³Œï þ*ÿƒ·ÿdïÀdÊûïü9„·H­[ñÅ^v¨Öž÷©b·Ngsè%úZGý/n)תÂ=#,?jSo†k4Úg{º*[ÓÉ^òÊ@?Wëå³eQ)¤ÁzFgXs·Æ( „<•ɨ—X¤´#ÑØÌ4‰v­Ýt–¼¦±ªf°7›0ôL½<îm‹ÛÒ +ã | Z§)üð.Üö9—¯j°<‰t¾€”ÎnÙé¯åžVñàtw*˜§ü„C²=P꿱mažíì?ŸÈzµ*So$ÞkQÃn£^.Õ;+4Ö Ù͈š½L´Ž/vヷ†_²ø›ÿ¢5ÉU*‰J{¤Щj5C<5=œœ¸(™¤9? T³$#£ÒdÔI­"#[ÏØ‘¢º±²¶TFö&†•+§ìé¯Á8{û̽£›Ý>Ÿ;˘ÍÌXöóŸÏjSçµ¶ž—b9„<Ï’<%{G››ÙôÍ.¿Œ¤Öâ¼Çuú°Ï½}¹hÎg)•Fc¹A¥6Õ*CaèÞ=Vºä*q…$ŒÑ¡¸Rf6+ôæä¿È¦O‡å•Rïq…{ðÌÂûMP·™¥Ï,åQW±º” &@r8Í b€\]ùAlfçúqQšŒ¼@ ý8׋ B®œæ%‚8!M7Nó2A¬˜‹œ$H–÷uÞ›Ô/±Ö#iy#¯Ä…x¼›Ñ>@~B#ï Þ=y«×Ù(B½ÚôýÂ+°è0‡HôþÙU?÷½ò’óa±¢E2yÌ:a;Ã…¾èšÍH¥Wz]§ý¼Ör¾$@Í™:;æŠ6V¨’ê˜X}åsL½*kjev¶”ºžbí1\Š–Þ'°“gäadýTî—ðË[Æhiƒ*ÝfÏFuΓYlD‡e23zm!R¯XÓt¶E£Ì×V‹ ¢¸VüîP_uQùdû´·Ûm W; 0ã÷Zz#Ž¡ÃSÃÑ^§¼:VUÕèó5ÖVÆj”,×: ÚÏq½âß±)ãÞL·3kV»Äü±örJâáuu†Z­^;êvHı³ò~“XÙÆ\[# ùvaÛè§±ì g¬„ÌâƒåöñÏøº=®Ng(¥¡©“eA«›Ñª’û.Ñ0z;:ìì‹‚Ø×a©–VÙò`½ôeµK^ŒŽñþ1«±w ¿²[‹LL³'ÑÓ‘&‹F,W9Î\6ÐúpOUEÇó_Fü ݲõaËhˆƒÜf¥Ád¾1¾$bŠûÇêu&)$~íPVjÈ%_zÎl‘HÞlÀÕô$»¿Nö)‹†°oÌ©Ðér²väŸDçh¤R û»øïÃm«!g Û²xå½ôÌ«¤ˆ¨Îª:Q­¬fÌ/ ÔþZF7—i´h"ÿp,G 8¢¤Flo¸ Îãšì]wàskG¼l-ò¿¾ê’g.Š ræ=9jp;áòIëóYƪ„AU¹ŽúÐÁüEµ"*ÍçÊ¡j¡}l9ä=Þ0&1˜˜±¶ JA‡;'O˜Û{ù'Ý#¾õŠŽáüŸ‹XuöÞ±­ ŸÅÊêžïÉ? iëà¯!­¬(-ÉÁØAu 7Yëh)ô)¿žž·ÔSõ Kãî îfòO^½fÍÕ¤Üf %Z1;!#Tè”oŸ[kÒñ•Úé¹-k_gDǼ- %î½3‰$q~ê†üJ²ËŠ,VrÅ@ èQIkk*U¡àüñH¹HÈT =ÃûåiÄ4—éåb”¹QáU+}ÊóÇ3@U\éˆ4"1| $SÜxá¿t+:æûïVÿO[Y¿ Ì üb¿ž±95Y4sÅ©üî`“–¢_á;ív«N•0tŽ:ÛÝzÿJ:Û—³VKDŒ[KÖ „û\Y‡Uïçqºu†§#º™Üéž©dS­RCµ¸Z,£KµíD.$5•%Û‹u. ë—?ƒÜ~z_ÍÚý¥±„æBUäÜÉ^x4ïœ}cAA€ÖC{¢b07»ñ¹\ø­` ) 2Ö‰Æ*µXq|¬TºNR_×”j÷' mMq½¤.ÓÔîK&üÛ\ɤ˕H¬mÊ(MæFy¶©C¯ïhÊÊͦFE†ý+š…3™p(Í®F¼§¿ˆ~HýŽgø–V‰«:»„ìƒàðó·øv[´­?à³§ÛÓö“é c“*n±{ñß»Ž ‰\Ž.8ˆ®s[,¾ŒgxÎk˪ÔN›ÆnbüÍþ±s×\C½zé¥ ü2÷»Ô%Ð"VnÅ{¦™UX™dørhÈŸ„Á¾&‘Ò¤aqè×׫ը:?ÑŠê»1K.gnšhðZ“ ycƒ¹µÃînÒñ瑬³¬—œKü#uß/•‰Jú0«@ÙŸÞ~ûíǾ´­Q×òk´ÕÙ7äš¶DrÝhj1›[L7æß€?¯áÑøžð_ñÙM¨É§ß+µ²•À¨·{së¾\|XÙ$ñéÛ»{ÚLIZ5ž¿¶ùó—åи¨ï‹Û¶Þ0à·E”Ê£{ö~^¯s•)k;? ¿¦Óo£7a¿^Íjëê7¸ðCŠ±î½™ŽC9§*æpåúÛ AiÀ<>Ú–LîÜ“0)»™‘ŽöQu]Á¸úú©>°”>8BkAWi–Å$cÇÇV_Dí¨‘UÉ*MJSWBÓP§ÔÉÓúö±Ü«'Ë;ÅMEBƒþ*ÁXùð©—Ã# Ôx/ä—8}5Œ‰?à@æÓå'”(èü ¶þÃm‡{cª&IÐØ38ØSÞK›ƒ›}ð#ê¾nGAvjùÑ=‡ŽèuÞ@l==%üä~þ$/ó'@ °&w®ÔK;àRÛ…Ð §Ãê&¦Ñ‘Ô«5¨&?Õ =ñ¡$Ru*'bÖ®“šð{- ocÀÜÚéð@Gœ{‹´Ö¥<ô苵<)‰Á%Q„ìì]VÚÎ(Ä4ŽÍÃ~ ìÙÜÅ—ãÏ/w£{óu]è>êHþÍžF¦S Ç?Nçަ—( p¤ª#ø®·({i¸X(B–t×—2›’©Mi›6™Í&µ_ïG÷PGìSwîÿWÚ{À·Q¤ÿÃ3³*î±,K²%˲%«YÕ’mIV/¶\Ó\S;Nu§'HR r\hwð»8rG½Ð{ë ¸¹~ÇѽzŸY­ÙN®|þ¶¶Í<ó}ÊÌ>;3;3ký v$‚‰À]m¢n:ò寰+Ì4"CeõHù=C%3œÄ‹æ¯­Éè7 ùšÑ®}šbAQÕÚñOAâ•W_·±WÁþÈ=÷Ô)öA\Ó*ìD™?¼¢štߺçœ-£æ³þ˜¡30ìÞotv›u6¹`t^‘y áŇT§Ð6Z*q¨–¿ºxóÈPóÜzEùàÁ©šäUóÊ+v–•6Zšp_"$šúS"ˆ¨Y«iÎk·l)è<ÂOˆDêïÎ÷³¥°#»LKz’ÃöŸa}2’ ÿ2e'lÔïÖƒßý5øÝj¤E†SÒ©_’d½.¶tn%.\ÐÐ_—7Ô$»»[eu½§»®Ë; ›.Z¡Þ³aÓ®’‚k—³ïwðn––3ðNŸeÈŠ2ëõfòä¼%Í–<\±l¶)Ф5ÕÕº*}5 ½½+µMÁ&­ÓlŽjÜÚQö¡Ly,Z6ßwYÌÎê“JÖÙ†zÌ!—ÕÚ¤ÕÕkÔ}^) ˆ Øáïä·Ôs× YÏìÚoÛ½ÐÖ[+³iºæö´EëcÚÅ‹[yÕaP…¢Tßë÷Ûœc̯î­ÛøÑ÷poqí…ÌL"îE]fæ7Õ‘1z¤m‰D{{"÷_ªÖE&ã Ûqà–ûçðÁ°?ØâXšF­#¶‡@vvíg>öMš“bXýÀ/M÷q(@ÐI÷TQl£1Lb1X½ÍÜÕ€™Qñ²ï<¬Z¿üfÕ©å øc¹|ê·L]Ä"–Uh*wøÞöù¬ |#Ö¥Þë|r«¥OJà&FsrV”LçòÂÝøô]¿T]¾~ƒêðúIþHŒýýS?]åëpùaëâ‘,f*J#z($àînÇÛïbÿ€OãG¨ÒíÄËÝ!vúîóÝ!’ìÂðwˆ[vÁµ¡•p‡´*_8ìS=4›Ø+æšÈ®ºžP¬ÏÈþ ÷<- ì²(±6ªç‘J(>÷V•jJáöàUÿzé¥Óó¦È®•·®dÿ0Ù&ìòÔ ”p†[I ‰±ß‹W¶³ÇŠè’µ(•ÊÄ‘PŽÎqM{ù¤6¦ûñQ(Ç;¹1,Ïáe(ß ƒ°2ÎôeVåbò¹”ϧ~ǧå(^Â÷d)$3(æq/à—9 5P”s/¦Xžb7q*‹Q8£ƒ£x?ÃQh¢’£xažâbžâ­,Fñ91^ÎÁPãeC“¥x1k§mÓ( @â(^ÊRÜÄSü3+‡h†½Å+ø‘ Þ/Oç/œÏ¦¼.¯òºPŠ¢sryRð\”çÒ(ÞšAñòy(ª³º¼šúOã)~p>Iy9^?«‹ (ø˜,%ñ÷Óeü3Ë­rF)Ýv6¹±½cø=â¢=àžœ÷d?ú ÷à›è< I@Ë'º¤þ1hÁþ:.4üˆ®z¸£˜îSÎ&Û ¾å§GëÊÄb™Bßccø‰'zrÛÉ“+âN3>Ö)MFc“P¦}êàÁ§ðÛÒT+N‚P"ž²8WÚ§±O>|š¿~dÕ‘-±å«þþ™)s¸`š4~‚K¶¢Å(0´¬8²Š}7›ŽÏÛ9Ósxò9Ì~ŒŸ¸î–þS§úo¹aÔŒå˜~»1Ÿûˆ4Ý7Ò|ËØ-+<ñ—³;ò9™ƒÔÈŒ¹6˜QG‡cd»'Üg»>õP»š¹4÷I8¼Ì.4„¶ëʹþ*~$ظÀéêcGu~¹2 Õ[øz{£·¡3¦kÝôËmÕŽÛâU¡º†hC¿':âÚ`Õìõ:­»Åèð óóñ1ì%'ÿs?ìú>ûÓ ÒjC&sH« ™M!­ß¢ÕY,:­Íü¶Œ"óm™ã¸o¸ƒz*ˆÇ÷A¼A˜àîÅ?ç¤Ò=ǧ3B¥k‡Yº]†¿yÇÌþÆÛÒëLs«»F"Oï%;C¿Q¿2z¼z¶À‘Îô¼œôLHE“”2¨•IØUüòmüûêãÏâíØ…]í+ì_Ø¿póZû Õ¤C*ºî l}¤d'ûþNòñT¹jjå©‚'f-­µ ~‹^ãž—¸çå_ñ«ì.@™åíqòF¶ÇFÒ˜-Wg‡(Ë$g›EçB—­Ûë]pÇE¶WvªÊ庭ÚVf«î)²öGpÂ5ÎþžÞ¯]ÙÙØzQÒ¤kI°¯¶¸ªJÛUVÄáúù'¹4oœ9¶²\V«•¥?TEÙ¹]­«‰¶'Òo!D‰¯ï¸dÌ㻤£sßrÏÔÓ!cÞI\`nløû:~T(Ôo E6™ó —^ÔÖvÑÒFz$B"xXT ´7Üþv¼Xøˆ¸P`u7Ù ‹¾'äòào$FòèsBˆ›°‚ÎÓU` ûg,m9‰¯`wœ,Å\96z qã,þ@¢¤”£—ˆØÅF ‹âBJõJ|ø„KÙ?ûN°½ððøF:ÙÕèÁ2j9ÉîÀWœlÁÒRŠa?ÌDÇÚ¬ò(0v³ÿŠùUb©ï>Ì^pÂΣ5HAÊðÓ”^²3!ˆ„ßdψ¼Wø!–+°ü#!Þˈ Ù3”Ç<$Ç_¤ÓxŒ ½^À# ¬ÈÉX^4(bØK„ý òŒßçxèA†; ³ì¥‚Ø3q,/y –ÇÙ3 ÙK(M¨™(! ÇÜ$†dŠ­L†ìCa3/\"ÄdóAÌ=åù¼q ºaãs䃞ƒn†|;Ÿ)¯^™J&ïM&“µàIçNJû”`‡J'qµÿ-ù\:Þgcø8;J7ÄdóŠG€üj‚ÜâøëaÃ!à96õ#^ÊžæÜŽ´³e yè“ùuÚ¸œL'º(¹¼ææûïCNÎ pj iÈ$þ)MU+(3 17ÅJ™<™`¯ Ÿ:^‡G'"¬à箋/vý<} –J½Hʈ“ã®3B7 HhÎ¥ÞµËå,¼h"rŠb~{"’^‰eÑCjIÓãÈŒ30Zñ{¿›†òE‹VÎÂ(‚–“ âæt˜ñìÅÖÄD08‘àö˃ëbññ`p<[äÚ¿ ‰‘Ä fvÊY+XÛ·D£[Ú;6G£›;Üóíöùnn¿0º¹-¹)Ý”lÛ8{›šzé}ºüO’|’žG ¾s~l?ƶ÷ZJ”Ä{v ¾VR¿`'ð\öÁ¥pRÃC(‡RÈ•p‰§V"æhãøëöF¼”½k)¾ÓÇ®»ðåY…¢$@Æ(O}>ÁVhû—âÒ(–àRöoœ½Ö€½æŸ[9#8³†Á¿g•„äÄçe(ôµ %b¯ÁEyJ?®¬(ZO,$qvvaT@c×ãŸü‘9ƒH6>wv¡^Ÿ& ÒÙŒiÊñË.Eœd½À9@ÑôÜìA\ÿäL/þ=ðJ-‡"/Cc”Ö¦ÉÈAöcžôj\KñlÀ]ܳ³ ‰”Uâß ÖŸaþÈš/K‘;ËP"äÉ¢ G9Ž…œ| Þš«­„!#S·óÚâÔï >9_bêv2Âdø1¨ - re^—Î÷óΪàKb3þÓ'ÉÉPh2Ù¶!ÚÐæì¶Xº ]K× 5£ 4™h] ­oML†"¶yn÷<[z\šÐ"¢#\wüçRî™}ÿlŽÅ6'Z7Åb›Zz]®Þ†ô^K×9Öê|:oQhSWׯPhcWצPÈÞçóõÚa$²¯Ïî­õÓSmþZ°v'Ìzø)¹=]’Ù’ÜÙJ:ýS¿&SŸMþ©[ZÝÄÖ¢ôŒ‘Ñ#Izõ+½{+¢à¾­YBÄÂ,^ JØztžrnâ@[gÛ¡MöàYÔu=—Å]Þï=Ó‹?òû+.=Ë#AêOäBr÷ÕÄ\""þ¦¦ÓÕcŠTêÖíïèXÿ´¿¼÷Ú¤;xYtçožk­bŸÒ«R¿Ç_‘‡øš-o>xœV“ô´1?°ª”Í1K§£¼V•¨*[ÝÊÒúXHŠ‹ÊdeÎ¥~SWei…Ye_XÆ^¶dØ"õUê÷dfy–ŸºœíÉ ðÀ5†¶ò*SÔØj)׊b‚•S¦ˆ1çâ¶•ì»øº GüùÍìK–9LõZ/½üD0õÕ’eø£»w!Î6“>ò,RPog¦5µèpxl,Ù®¹ºdYwCh "¼­häÈëšGW7ÅãM«G›×]pÚá QszÝ5LVl­¥eë…Šl)ä‡Ü¯t¶Û œöv'{CK§1¢—ê”]mþ@8<_«(uRCØØá׷˲¤Kl]:oÞR«Øu|aÜÒä Ôg™'‘ð”9=jAa¾&äŒ/4¸\ª'=‡ †*¦zâ&ìÆ2Pý®àú"w{4Ñ"¡cK%-‰ÍáÿýB[‰“® mDnD³ue¦ŸÍž‹A)î5¾˜êßÔ8[{l]¸'™Ü³°ww2¹ûî ¥²B¡R=›Î[Zç™ÕK–¨ÍEó•eeâ‚‚c°‰akJî^°’p{\§¯¢ýfUúÌñ¥h°´­­4ý¢\ZŸ_X˜Ÿ_@ÃÜq ¹‰æü–Î.œyç x)’ æÇLëû£}R´?Ò¿Þ›¿ )±‡\®èHëè o2é]1Ú:u¹BH”ú”‡ƒP¤È€\Èæ¡¥Ùõ (vÚDü»»´q¨}²“Þ8ÖõŠfˆ¦´Æì€Ž†ì÷Oܹ£Ä?éßÓzãeÛ®øÛšÝu“ß°²wžÏ·mýÄvïªø¢áŠ9ë{{×Ï©\:™_€UJË S³R^QTZZT!W6›2M^AIIAžFVÛÿ¡Ñ5s; ;“MómÞ&wCÓüfG¸m^çŽÚ>¶ìÍ›ÁÚ­jV•+«k¾­’Î)C:¤Ê¢¼øƒ?ú•1réP=ò¢¶Lñð_é¼SOv¼½8;§ †‚÷3œÚŠiWغþptXjÈ/VÕ»œ’¯6„ôÃÑÃëýsE좹#ÕDRVV†Õ¸¶F£=©N_Uc-\•("þ…*ÜìÏϗ˪5%ù©qž´¢0ßßV)Füň'ð¼JtjµŽD=žhö å¥~ ùz!äk1Wò›Prv g4£RfïóÜ ÍFý;ÅÄ9túô¹ÐÃß}{Ž:t¼ç@|]p׆ÉÝÁuñe#kÖŒ|RCJËÊ$¸ëj4ºoM»*›£˜£€û•Õjtâ?b§ÑZÛ~Íš+¯¸âÊ5×´û<Ûm[»vž{ê›{{zz›ë;I­Z]Ëiž=û †X ÅÃÕÑí2üùÏWwb.õib†P)Òpë#BCÀÍk s+àqcÖ ÆÍŸv™LöeýÃv“‰ýc}y}E³C§u6·œ¨·Z,mZR ½añØØâ´SŸhH¥ß.÷éìv¯k5µ´4!ò¦^#5äcÞßúÿ³§erz‡ Íº×‚­É áØÆxbC,¼!im5™Z­Vºnž˜X<1a‹ Ô“n‘ßãmusÿ‚ãÑøx 0Ž»aug‡ÉÔá„•FØç–ÐÎß%ä`}«©Ðä÷îµìÔÒÖ‘gyiÿ£¬’Y¡-+ŸnjOïmtQ›®arÅ‹¯¸âÃðúxb"šHÀJÄÝõG‡ÙL÷õìÍÛû¯¹¦Ÿ¾Ï"=é%ÿ[îŠiÜvu<ÐÕu`0J»Y¢‘Ï åk¯u¿öš¾ë@ÿÀþ®®ýýºZ}«bñ•>ßÊxl•OñNû;ðCLê/“ ä0ÇŽ_øÜ¥t½‚Ñy~Ÿ«¸ªl·µ™Ímï$n»+2_ŒCëñ‰px"žXê6w:í0lÖáè4cYâæ¼ùt¥“£Àˆ22J.Esø±4F°ƒ¹‘ç<%'{ëJ¹ÕŠ= lB˜"_ ×Bzž¾ûøF[ZF}-#-ð£+—ÀV4ôÒêÀê/kS¯(Æ °"8ÇuȾ=´Û·¬Ù3ìó {š—ùº:|¾ºá¦†ÖÖ¶Vþ2? 9l=p€ÖdšSŸà÷¸õ°hß'í<Æèöx¸—ŠŸÞyçÊ;ïl´47·|Í͸÷±'Ø+ölݲwï–­Á ;ù: ÐõÕ$€­Ø&ÀüöØÛì0Žôu :ûðZÜÀ¾ÉÞ¼àËK·o¿4Ýsý8ôl¿FDÂªâæØ}¾C^AbáŸÑ ùBŠ!äò#h߉0¢ã-ä nTâ¹ælÉÖ†¥Yb•%ìѾ€ï—ÛjU5²2G¥>rFeÉÌèÆfÀ9ïèÆƒÎ`Ô—[%fUÁÄû’²¨3×W:Êd5ªZ›|<=¾¡·ðŸÒ-x7tZ€ùÄŠâÜ{/Žß{/7R5ÎS!)ÄOB\:&uz½‰ï§½ Œˆœ±v[Óú1VUKŪC•)ˆ‹zçË54üùsš"Å`£$ˆw2º’?¿ô}òòÙï¯7â'Èï!äd6$!ÿ€gù2°õò _ˆ½×"Ò‡kÈítýht„£(ÜRÆÿ·®S?"AôîúÛ[9öhKê4\ÿ6vî/±o Qü9³võAàr<Í…ÙDüø!ò>„|Ÿù:qâ«É—ò$rœ4ã¿2yšy ôYL^€Gù“)Þ€yÿVþwSF|)ò¥AEÈ”ú'Qãϸ‡:äàVíEËÐ8ÚŽ.AW#4í ›sž.Í9gþ·ðœà\êóq]W¢P”Hr;}ô–)äùüu°T¡(-UÈ%üqˆÆCxLMü¡´¢‚RÐ㜠¹•¿•IËe ˆlR.-‡?©|ƒBZ.…Ÿ‚}G^.-‡Ÿï•¥#eìêlØ÷äR©L&•ÊÙ5é°z¼'afßšA }+  ­b ™¯Þ׿˜x‚o{?›$ ÕÌ–L\ËëîÈÛ{.§q7+²1shqºŽíuÄúfù»4îVÀü`òqõþ'$Ørÿ\w `L>®Åú‚›<Ðry{07&ÏO7ß°ƒXç¼[NãúI#ú½ƒ ]ºoR·Ë±qçFÇEÒh9tȲŠRÁ8NtœIäRMºG÷º'ä”vãFí¥ê$aôÜrà3ãí-wç·V4U%ZQñz¸Ó2)Ó…}$,WËc‚Ön8è4ÂD7ÅŠ“ Ó̼³X¢é¯‚;TæÕœR3WnàÎub8%?VJUja"G¯(§X[IºšÜNõ§jWc%éëá,³‰ü]Çè31×áòã¹s©àÓx zÍSÂõƒˆ¦&~t'yŸ>]$ÓkGž¤Ž´ í«MØJ´Iâ·$Í…Z¯W[hNZ åJðǘT )§µZÛ{Äq±Mo°Â¡‡œîÖ˜Í5…Êo  »Á¶|*wöýNÿ\2LªM& &=$°4^ߨX_JÓ,NßbY3‰JH YKVµV[W‘Ú$9­êkêëkàé%”ŸSérîR 0©1I$ÓØ¥<†ZZÓ>‹¶¾^Ò“gõÕ´1Ii èN§çA3çiɯQét*•V{ 6œ“hRY—»Òp½‡ ü{¤ Š Ôj›Îg¯®ò?@šÑbfµ+3mލ¡P^e°ˆûçöf—°ÜébšnâR=„†˜M¨df*H&S,Å :u>òPƒ°´T"%!b7Ö˜å ÷AâD‘/ÓrKÎ3¼%iñ‡Ãþrµº6âl±Xü~‹¥Å¡®¬¬®®¬TÒòº˜Ùùï‘|±˜nr•JyÌmµºéfS)J¥B¡¤«Á¿`é'=Ã×ÝÜ‘¡ù¡ðàÒ¬þ®þ^Øhþž¿c¶N§ –÷ýË{ÈcUß°\[Ú«DÀ«ü#Cçö(ÄêR.rl$‘C–Õ«-‡€ê·¤¹ ‹”Ê õ2á<\Nê~¹%ísžŸ³ #B΄bÀã i7Õ[¤9Éo“ƨŠHå+¯p1 '³eZÌ#=zÔÆ[gj¼u|Áàšµó‡Hó½úïªa£ƒÉÐ…35Þ²µgù¶-=ËÉc×X¾Q¥ì#~t‚ Ðó,û¨óu˜°ÈãtzDabìôUÕÕUÁ–öz¿_O½Ä¥:u•NW¥Ö!®¬Ÿ†²þ ”ÐÊ™#¸§7/˜çµX=yAq_íÅó"z‹E^d¤Àœ÷jïõ ´J¯ÿÁ×—µ]›×ÛF}˜Íj±“æ"s´1ÍE:¯—ó^÷êâï—œÊå"pZíœ ³ô6òXQ £#PTk6Ó¼|ʃ‹ÉGz·r“·s–@%~«£<˜W')/S«H¤Û—š*îz‰BQ¡¢©¿ÒÎOߣÒis¿ îž¹ýb‹¡JNšENW ÓE*ä¥ !Mõ "?#âxB›:g‰^àYSëæÈ¤jµÔo%A“Òü*äªC,•Êxðö§ź/Å áÔKH|víÕädl F¶?Oãï„ø…éx1ߎMNN=\ze˜¼Å­jEÓCêûIp xâ>Å÷W¼–úÉt_’¯3æ‹αuâûÜâ:­¤ªÈ$±)­-Šy‚M!êMñˆ*¦¨U<þ޶¶}MžeZ`Þ#cÇšÊ æj )Z•““tuk¬õ—ññ@:žø{Òñ 'èQÛúZëm½gã!}!ii¥Ö€_š. tûCé€Çkƒô)úhÊ€’©j³}õ{Y’²¡»S÷BÈãò'9˜"è¢Ôëò$„¼ h7¥h(õ2*4ÎB¼F ‚Ö˜¢Ó7ú‘KP.­9ÄÓAùßCühf‚M/1 KGËè MEüy@k¸™òòσŸ,Îä‚[œeScµ'«êŒ”“L.‘‹–Eók5Àëÿ)åm)#Ú)ËÏ~)ÃÛR*¶Gµ%sUµjIIe²¬ù§Î†*‡¾PÞ ”ËòÄ¡)W+LÕ¢#)2¤´!áªê®¨S——( ÔeÞ_»ìõq©Ô¥TÈ Äñõ”оq¦Nsž*wNQŸˆýÇ«¯BÌe€½;õ@¦\ÓBíþ"¸ï××^ÛÛKó³?Uƒ¾“z›@k òê=kÜ™: !߇_p!År B^ƒCHgJƒJ=!÷󩶦†ÐÕ©»!Üî”aöÃÔ®HÝMe¢£Ø‰óžtÍŽ@oãnZ3ôœ«–¹NçµÕËuÂn¦±¡Îc¯—KÊI7ã~j—á’P´ê’`ž‡;¨»ƒ@;9Í9qS3Í7¿A£1À]Ä:°¦¢Rƒí Lnáì¡{|/{ŸÃ¶;¸/øs0ǵˆ/‚øG“¡ ëßîþâô¾}Ÿ<éœ =©Rt*õ RCü´^sqú¡PxÊݨ4å ò̕\^*dòçw*Õjeçü¼&]¦¼\S§k¢þ‡èÀ¿üŠÞ—LΈ[w½;û#:8XàG/hÞñµu"¢¹×|®à®yÿ ×o!€M-$Mè>FÆÇPM¯u)€£"çz—L­–Á¶Éc°ò礧JNßHÈ«*ŠÞ-{(sAsôí”­Mq³p)JÓ´òëj´b ïòÄE•ÿ_ ÑXT™_V‚qT@¿T#ôyó(‡©Ü(u÷™vyŒ† ÌzS ðK©£Ù²äe®»¿µ•‹!Õø%ò½wqæiÆ56DìPÍê?\|ñV×D¾v$?r=©ö†Âá×`ƒÕЀ¤}¿ÄLŸ/€ËÍ Ç_ñqtýò×u·ØÓîÊ_žïjÇǯ=räZDØOH#´#T@Sx¶-eûGGGk·NF“FëÒ¥ÖÙj¿˜–*9´¾®avMO#†4#Ž%«—8†—Ö/…vØÈˆ6‰6,¡i6‘FLfòñäðù¦}a4ÃG¿ËJÓl>ÿ˜ÉG‘Ãç*à K†Oý"7ÏôqCumÎ"ÙÚJ†s¸Ê¤‘—½B“¾ÊTSZR"ð Ì ËÀ€5ÔmêrQC#4»ep ÕÌ#ÀA5“ÃY?#eTm®‘T‰üÄb®6ñg õ¢EÚh¯BQ/nhJï{+H5S1#7 Œ,µ ŽZ ®ÄJÒ¨T‡FÛÚÒúž}oŸbĶhõ"Û°à\“f–\žNq!ð8ÄYH–+n=.ÃDZıÎdÒÁÁÇ3îkїȪ«e%ú@›þG˜'ÿZFÒÖ. Íð‡$/TŸßR(×hä…?Ÿ{g ÉsЦÕB²Ùvi~©Tm(lT;ùr#´æ”H$L”±™«ëÊøô!sÝté¹xÙ\º¤°T^m(J†C|‰*0 JJ$¥¤‘˜µfýYËymNŽ­ù¶@Æfvº[lä­µ¶zRoY®b^:wêŒènضˆ·N/lIí"DÒífŒ¯ùCþÒ”Ðsð=ÃQ'í=Ølßèù~O§ƒ~„‡M7¹i_¨}KèÒýÌ0È"Ý>€$´Æ6ØNajàæþ½ÓÛloÞ‡ß{ð*W;«50Pæ9L&§8(îñîâZuVk´Ölwíýodñ$sdƒ4íõÏŲ¯ù”eȹ+8MÓ^•eÈr”iYô³d9·qEk˜ Ðl±Ô ‚LM¤€÷&!¡ÕÈ{“ÀFü5^//¨r×\Í{þ@ùŽ“§ÑMÌ“ {í,ÙÏá[Ú;DQ½Åj†CGþ¹ÌÓa{AE]]E=|U…Â"nhVTP/“µ÷ÐQ=ÛÞÓnŽ–Ì—È5¦‚ÎxlºýwZ™ÒI‰1v³Ý͉ç@ÕLm¦3˜–'+¹[°°=ÊÍ™Õõ‚b¸zÖë铌ÄÐmÌØ¹žüžOþ5t,…FsÁqçQ%ÿ]C«QÐSEÍecþßÍ\Päe𬾃ùñƒ<Á#o<æ»^›A~–QÀ ë;e.þ‹ÚŠçµ•e­†¹³k+o”½[”¹ø/¬!>‡5Ìà4Êf[\ÈïþkˆÏa ŸýzŸ|¶5ì;?É\œ•Ù‡¤é7šÒs”pñyzÑZµ-LÝi8ô™3ûÛ<÷‡3œ_¬PÔ ^EÝÈè”9ž•Àv.ÝèÓó\V a•a–ÑîîŸm³ÛÎ;Ûf\£Ÿe²ã~ÿñÙDbÎfÙ¨óYɹÔ4&3-½i¦eÞÓwuéÇ®ÉÈ<[ö×ÿ[îUŽ5‚5ŽU·ñ<žäy0‰5Ú¹sµk.ÉÅçôà{÷1˲ýR€æMçèuÔT~͹½¥iZÇcM}ÙdØ-öf÷>îf~IW¾âQ)¨g6hTwWÃIƒ+Ú’íƒl´Ø•»âžâƒ¥1·’ë3dæsÖÈÚ"·‡+°Ñ¼O°Ï¼1é3‹Òë]ŸÛ[¶˜<}ÞOž%²Ú¸[°Û¸:’é"{jÜ44dßËaÂFQ’ÄŽs=N@±“³=ÔyDª¼ú¾¡¡Îrql…Ž4 +íZ—‡i%*¹®Ø÷†ô]@„Û΋`®òè{9„qÃØL„âw¥€°‘¸ÑõL'*B53z1ÎUùÕXÂáN‘^©+UÖU•*Å!¡ÍÈŸ ìÄ-4Öê ØMÊJÊó.•ÉŒBú04ñÏÂ~õ𬬹=,œ¯¼¤°¼¢KÔÑ«÷<u]"¥Ô*h…æµÎ¡*u)ÆZÀ¸çü CØÑ§÷>×ÍÄ  ÏÐ0r[çÔ8_Wiu†ÃÖsjlÊS—7®Ó֙1r¦æ2@¶ …ÙukÓË-OŸÂ£«å‹ˆ!áÇ:H²ƒô2ÓïøÙ=ô¼–‹ùѼÁkñKkwÎK™ÌCmôšõr׃fÓP—Õá°êëvB~a‰ÃjÐùS¬=P÷àHÛ¢%Kµ„ÇCøxdmh8±dxxIb8´6ÂŽ†ÆÃC¾ ~Ëïw¨N§QíØÁ:ƒ>g½¨r‚nÜìégÈÑt Eˆa¢ ®åf†’‹Øï±ŸávÜó؇pgižz|öÃéñÈŸ3ï’ë‘ 97# ˰¾Öc¬•æt)t’=¸ˆýxê þñ+¯°æ‹ƒ—\Ü·Û‹ÉÍì£öÔЧgçö_

m[ïÿ%3jƒÆVã„ñÙ Ìøf.³ŠÙØœlî6YÐ*kõ°¶YÇ­§¶g±;Ú£ìö :Ù–ÎTg»sÓyà¼q‘›Â­åöq—¸W=è¥ñšzƒ¼%Þ~ï#Hª€  F€)`Xv€#ฃ÷ЀÆ)`XV€u` Øn‚oP”eAP4M@sÐ ´@gÐ t½Fß±ƒ³ã¸<®›ãθ?Ž'ãùx5ÞŽãóø6~„?ùiüº~K¿Ÿ?ÌŸäÏóWùÛüCþ9ÿ–ÿd#…H9R‹4#Èx2›,'›É~rš\'÷È+ò*inZÖ¡-h:€Ž S躆î¡'è=ÆX<–Šec…X9V‹5cX?6ŒMbóØ*¶bçØ-ö½e?è Y)È” ªÍ‚NÁ¢`]p3Œ¦ ³……Âra­pn¸2Ü φ7ÃánrÊãò”<+/ÈËòš¼)ŸÃWð-ü?Ãoðûü§ÀB‹d"“È'J‰j¢‘h'z‰Abœ˜%–‰MbŸø(-Éd<™Jf“d?9LN’óä*¹M’çä-ùP¾•?#é(Y”)Ê•ŠªE}¢YÑéè»J¢Jªªª©ê¨úª¡j¢š«Vª­ê :«nªêú¡‘V:©Î¨óê’ºªn¨;ê¾z¨ž¨çêµúg¬fl[ìPì7AðXôl3¬Zû<Û¶mÛ¶mÛ¶mÛ¶mÛ|ï,‹Æ1ÍR²¬¬ +Ëj²¦¬#ëËF²©ì,»Éž²ì/Å“qâiyN^”WäuyKÞ•äùmŠÌ"·(.*‹ú¢µè.‹ñb‡8".ˆ;â…ø"#ʸ’KOf9dYAÖ‘-d9FÎKä¹Gž‘7 "Äd€ºÿ¸qÜänUw¾{ÚKãUñÆzÛ½—~<üzþ4{7àAó`fp6øæk‡=ÂMá×ÈŒºF;¢#Ñ…èNô"úÿCbH¹Á‚âP êBSè}a$L…°6Ãn8—á<ƒOø/&Ä´˜-Œ° –ÅšØ;b_‰Sq!®Á-xÏà |‚ðŦĔŽr’I¤²T“šRGêK#i"Í¥•´•ÒYºAéÝ_‚àB0  B!„B!ä¾÷/„B!„ŽB!„BÈ !„B!„°aÂb7óB†¡ hC†09¬` 8à <ð!ÀÆ1…Y,`+XÇvq€cœá7¸Ç^ñ/üà—b”¤ å©DJ5jR‡ú4¢)-hM;:Ò…îô¤7…å§9ÇEF®rƒÛÜã!OxÎ+ÞòÏ|c}$"qIIV R–ŠÔ¥%]ÈXf²”ìå$WyÈK>òÕ˜&5£y-©jM›ÚѾŽtª ]ëNzÑ»>õ­¡E-aiËYÑЪְ¶õlh›Ûʶv°³ÝÌ3ßqq—rYWp¿îÏý@cÃ`@gÛ¶m›_Ú “Ù¶mfÛ;ζmÛ¶qú÷Þ–Ÿ ”‚ÊÐ(è}ȧ°¶À^8çá<„Wðþ D(5Ê‚ò¢šH h2ڈΡAÑ uÐ9˜Ì –çƒëa–0 …ã á•ðNø$|~ ÿ„ 8%.Ûà.x ^Ž·ãø7)@’d¹A4-F mG»Ñ~tÝCŸ³, +ÀʰƬ'[ÎβìGÔ š툞ÅIãqƒ¸s¼<>¿Š?Åÿxržžçæ…yy^›7æ„;Þ¯ãwù‘YäJÌ{ÅIñ^f– [ËÎr¨+§ÊEr•ü#TNUCaÕGÍR»ÔM\×Ñ]t=DoÓ‡ôýG'˜”&£ Í 3ÊL3óÌ*³Û<2¯lf[ÔÖ¶ãíIûÓqÝ@7Ûír|6ŸÏ—ò•|=ßÌGÞúξ¿î'ûÙ~¿?îÏûÿÁ´ä0À³mÛ¶mÛµw“6]7É·­‡³mÛ¶mÛ¶f¾2 ™QÌ$FdÒ™]lav(›Ìaϰ7Øì ®W‹9·„;ÅçæŒÿ$Ú ^![8$æ+‹@\&îïŠ/ůR©4PŠ’¶IǤKÒ'¹œ\Kn&·—{˲ì—ãåly¶¼\Þ,ï—OË7ä'òù«RFé® VXÅTb”e™²M9­ÜQ>©¥Ôjj#µÚ]¤úÔtuŽºV}©5ÐÚi½µ‘š¢Åh3µýzu½‹>@£ó:ÐCúfý“‘Û(iÔ0šý Æ1’iÆG×0×l×Cwq·ä^â¾ ª€$°ìÇÁp<¯Áð„¥`eX6…í`w8Ž„“ ôÃp˜3á4¸ nƒá)xþ0‹™­LÃŒ4W™ÍŸVO+de[+­ýÖ#ëµõÅú‹ ¢R¨2ªƒš¢v¨;€F¢IHBnäE‹ÑeôÏniKv¬½ÈÞo¿òäóÔôŒðØžYžžgÞ:Þþ^Íæ]à=à}íýçkåã|³|û|ý½ýñþ=þçzñäÀá`Þ`«à”`jðTðO¨c(&´1ôЩìôuX'ÓYîlsö9ÇœëÎcç .€+âF¸3‚l✉çáõø¾ˆáÏ$?©Bš’ždÑH™I–-ä¹OþÐÊ´-LÝ4ƒN§Ëé6z•> k&„ÙaaaiaGþwOvCQkÛ¶mÛ¶mÛˆO®ãÔ¶mÛ¶mÛÝÿ™þMJ+•”ZIc¥©RDZ m–KäÆroy²Ìåeòù†üII®”PZ*£CY¯œS>©©Ô:jKµŸ:MÅê,uƒz@½®¾Pÿki´ÂZe­‰¦iǵûtr£Á Á¢Á^Á«¡š¡A¡—ávᛑL‘‘‘5‘×Ñ’Q]=½½}É Ô…V0¦@0Ø0–ÀØ{à(œƒëð^Â'ø­'ÐSêçô;ú+ýJ„2 |¨"j‚F!-DëÐ6´ÝFßp\×Ã]°Œá½ø&þGò“*¤%éCF“ dÙHŽ‘Ï4'­F;Ò Tеt+=J¯ÒÇô?ËÊŠ²ª¬%ëÊú±álcl;Ìðø¼/Ëëòf¼=ïÁòQ|:_Àwò“ü–H(r‹Â¢´¨,ˆ   Å~qBÜ_ŒTF£ž1ÞXhì2.¿Ì¼fSsŒ)Ìuæ!óšùËÊaÕ±úYó¬+vB»‚Ý׿önû…“ÑéäuÎ7[Óìbw­{Î}ïðyC¼¹Þ\?¹_Øïè‡ý•þžXOøWbg¥ÌM‰º‘wð_<õèÛf­NÛf¡Jþ8þpnèxÚ½˜steÉ׆ߪ“îdÚŒn؈š·õâFºÛ¶mÛ¶mÛ¶mÛ™I¦YßsjÕÉdå—u×°ÿxÖ[Æ>»vÕ½ö­¦§$;Ñ-²khUÛ¨´ÝMiUãÐÝÉŸEº~RAÜÆtPþV±'¡—R?+è*èUØ‘*¤®ÕºQ™†ØRõ7n¾-R•ÍÒx;E-æÒS5Æ,P•©WÊN ,[µ&ǽbû‘¤–hµØ4Œöm[|û½¨{Lùf[ ³ãÕl~Q?ûùÏ4Àü¬~æ>ô{ù‰šÙWÀn¥"o·`»îx[vç•DªŒmüY2CÝää{e$|Ï.Tx½OE¦È-à;W£?Â+ð%8h£|&J¹ê¡ÊM#}‚O$àcíÄÄjû¹v» ã'¾“p2ùLàWÿ˜KTVGëùüäž ~¼JðíŒÜ¤Õ½ï÷Ĉp&þ õLçY+×®Äg.þ<öÈÑ*±ë°ßó4͇.Éw\(E?útµ?)Êðo·xþ^ÿ7èí5YQ?pîmC’8Ò»šÆØ©Ý˜bÍ_CêF'q«…^Y£Éæüd«}^€¡>Ê4–…RhIb_Wˆƒå¦Âë$@]:À,ïÚÍy>FvbÓ™‰ˆ—q,ý ,Ñcy¥F¡£¢:UgìOÌ6{yò¡)¤—p¿š›(Ï€ÝöJű¾gˆ'[ËßÿáŽùAù]ñwM&¸‡zÂVk‚¿ËÒá{T-÷Ù{ZH—Æ>ïý+›46ôkè«&[§߯…~¯£?V”ø¶™¯ÑvIÒÙšíÓ÷à›wªÆ<€/Ž¥mƒû¡³-cûý¤7ñÙZÆlfÍ»*í}lg÷µí¯ls qÿ0lL™]“ò+½ŸÍ°÷¹÷­*æ}»”ik®¶OÃ[gŸRßyQcsÍ´V³!Ïgl¯ï9†ÙíXÛxöPò‡Ý·§›ïmøµF Yñ\Ñ6ìéVúÔâ ·ÈÂs/mïE¯R‘~×D[L Û^©h78@){zÊÛÅÛ$þ\60öHÞGþŽÝZe6…+ýÝI 4Eá$wÞk5á^9‘ô,(Ã.Ü>ö~ÁÚxa§¥ìÝØjcÉîí~¶íîg³/p÷Ú_ÈŸB:nïH73_‹f±ËÜãÍs|Û5Ÿª·ùšfRjÁWêìü©[ÿ&‹oåÚrúmŽ&jºMóMÞÇ/úh’ÿ>ó8ã¯iŠ}CãÌ]´«× [€2—yžõ2v<®I¹ý8ôóО²7|z7ÏnìÚ`.ûPÔÛµGY®-Q{öƒÎ7ÔÕ®ÚØï7ðs4™viÚ'Z‰VRßN};õ7‘¿òD·q_ǘk];´a¯oàg;…q'Ñ/¨ù61¼_ÀEÅôïOý"ÆÅ¶Ñ]äï`¼[Ð)†S¨ÇîÑlêZ¡c„;3JÓ¶œ²~î[ëhóù§É?ˆÞnÌÚb–ãx´›íïÚ²Ê]{VéÝL}Ì­Z5«‰²&êžCŸî¢‡»¯c¢W5.kÊj)ûý}} =€6àý⯾m&¼OýIð)Pƒ?|‰–¢oóð| m¡^€7_x¿Ê~ëÁ×þøìŸæ¯Ú;ü%˜ã/Ѱ…ù®âœg«¿ÉÕ¨‡U`ibïÚÐýŒn_Åç}4p7\ _„úu`_Ø ¬›v׉®Úó“ÏYËum‰ê÷µÀŸçÖðöwè‰ÇÆhMeñÍ*ì9” kÀáÄ bcˆ™!.B#iâ"q'²ßk|}üÜRõÑ!Œu,é7TÕ¢­ÐHúRtmîæòÜI\ua,Þâq<ö¿_ø}áæùö1Ça=Ìåçß–²x-5n’èÀ°ö£É¿Cýå(ãúð±xëÛMõ¬9Â^ÃïB› ïæsÃÁÏþ=[Ãýß›ûòJÿßÇ̱„yEe¶Ž{±x÷Ñ.ÇIº¶ëïßðÞ%~Çw¢å·Vü»×6R7\S£¾ÜÏŤ7eÍOÐv®oŸk;(¢¼äíÊÞ,óS§jg«6~›ú÷Èïʉ²ý»Àúu°?×R¤Wd\îwÆèg®¡ÿ£Ê"ÖþªÊüúö§¾Õ¿üÃüH¿lÖW©©Œ×â÷9í%ÿŽOÞÑ~Ï쪓÷“÷£ø.åžÄÆ£4é%±ãú{+} ¸«¹³ØG o„yï¶tx“öï•M5׳Û‡rl‚OL±‡¨ÈZUÁ ù0$èØ@5ŒgmB¢ÑãZÍÔiUϪ‚q”¯h¾‚\- ͤ—„æFɧ4¦™gÐgÐfÕp®k¢2ÕôzO59g©†ù|ÞÏ5KU#`d›Yî[´ úvÉç ¿ y ó«ú%Øò3µ<Œ…u ʼ®Eim4×ë¬ësì~ï먊カ¯¯„­^ÔªPežÕp÷ÇxAÏEÏE»•Ûo5!ÁÜÝÏP¹Phoˆ¶åŒÖµ›J`e(‡R¨Z“ FCQ‚©F/޾I{=¢(ûÛý6¦ýÎJ™Ðý8Ó”B‹Í*Ô;Ì›y¼nQ…nwŸ˜ÍHß}–¢ÝÞ@?ßÇÀH V‡úS>Ö¬†Ì× 3]Æã«c™•ÉÏP9XeñÆÁdl>rIךz÷9é¥a2äPÖÛ÷#é&èM¾&”/…΄l(Œ§<­õÜš Ñr*Ézš¸Ê[<ª…*Gÿ‡ÎЈD¡4ÚR!_ê5¤=iú“ÏÚÒ—•0Lj@:èí±f`Zв@R^kTÊ÷Ú@Uþ.ý*ܧcðíí°Ù§zZ‰Ïåá¿Éœ§z»1â­ÆžRæ^bT{ßFùP•A·„VXvúßz·] fصÔih…º@4I~\`°]Ë}ŽNƒ©ÑH͉±{*ËPŠº”gÔÀpꆠ>¤l$, S e?Àï6×]ÛGw©úãG¡@_)Ïó°;¾Ü[ÅèpâU…zëK Ð—î ‘ïŒàØ Î†‹à9¸n掭’ܦ°=œçÂ9ð*¬Áh†]a§.º>lëÂ:p7ÜòkÁM#àp8Žôã–ºÏÂØ/FA V }“±…+à¹0Ö piиÎéÆÝa GÀ™p•ÐÀpn˜óаÇõs€½-ú ‹6¼‰-žƒUàIMÐ@¿%;9Ùýˆ¶SŸlµKÐm»ØooØ>©ùÝz°o¢ÛÀÖ]ò›À°J’ÒAƒýÃ^×Y°!ìʶÒRô•&BÌaœµ¡°ŽµTnÃ@’ö z”dgâû­Jý?#H¼ÒxÚc`d``aøWÀÀÀ1ãŸÅ? Ž<  `³{G6xÚ%Å­AqÐûû XAø'Ø€£ ªäÁ’ КÇ9ï–«rg òþŸã|\lcC®–U]™}•Àî°?aLSûÞhñâñÎ'†úZ txÚTÑc˜&I àª$k›JWo÷w\Û¶mÛ¶mÛögÛ¶mÛWÏxâü~•Rä]×ȼKµwŸÎøQÍø©”(¥ŸÔJROªZª‰j­†ªIj©Z©Wë­z—Þ§é3:Q?¤ßÑéÏôWúýTƒFІÃ8˜Ë`%¬‡½pŽÀ)8 çá¤Àp7<Oà ð.|Œ%° Þ„·`]¬-°öÂÁ8 Çà\Š+q+nÇÝx¯`">€Ïâ ø ¾†oà[ø~@¥¨¢q´€öÑ1:G×(…n£»èAzÞ è úŽ~òB¯×Õëáõöúzƒ½aÞqï´w#â2\‰}¾™›qîÇãxÏâ |ˆOò¦‚ñLÓÃô1ƒÌ3ÌŒ0+LªyؼdÞ2ß™ŸÍ¿RHŠ ‹•HjK=i M¥•´—.2GVÊ*Ù)gä¼\’+r]â%YÒäN¹Ç/à—ó+ùâ[?òkú#ý-þÿ²ÿ’ÿ™UmA[Ê–µlUkìöfÛÈv¶c섃ÒAåÀ¢àæðLx=¼3|8|4|:|.|#ü4*UˆšGm£ÑѸhb4-V.Ö76>öYÍÊ5ÚÍ.U¿w)íZ¿¿èßÿþûÏ‹ª­šª6j˜š¬–©ÓÎr§Þë,Oëý ~F¨?Õ_êŸõïPÙY¶Þ0ÆÃX«aì‡ÃpÎ8Ë‹·Á]p/< ÏÃëð>*,…•1p–5el›c9?Ãr½³Ü‰{ð^Å$|ŸÏg©¨4Õ¦±4—vÓA:MW)žÒéNºŸž£—éCú”¾¥3,;9Ë^Îr`Ž%pA.Ê™¹×å–Ü—òDžÊ+y§³Ÿa –eœeË9–£íø2,«¡³<^ “ûåÎòugYÚY6ZeXNˆ¦fY~ê,mͦ9–ÿN@“3aîMz’ß¶mãlÛ¶mÛ¶mÛ¶ms•|(œÍ¹·²©3žªFŒ™Al,ù6 ×‹V@¸{ÈóëР§y>B4VÑ£õH=ÆÛ3P÷Ô= F{e×ÃÇX‘¬ÖeÆ"i'd0†ë;ú¶¾ O9)Z~w™ø Ñ¢»3äNtÝO÷Öót;=/ î儱@Â|Ñhш„l YÒà9DÿÇ}œ“Îi§¦SË©‰Çˆÿ9þ'îÏòŒwâÞv¿“~OâÖuÿw+K«à–¯ä–tKHus¹i§Ó:’Òý^Ú$Ñ §—›ÕÉ„³:éeÛÄ?t>w>Œ^¢E:ïÈ>Ë1B›áš ;…*‡jIÏú<ô‘äÁJÁ÷ƒŸš{¤Ù‘ÆG~).v¶DÒ"#; @ ^t0,ã5ÀÜaîßeîßcž5Ï›ÍËæUàK|Mü܇ɘÂo)˜îþÞTÌÆ|¬!­Þý}Mð¬#jÄœðØ…ÝØ‡ƒ8†“8…󸀋¸Œ«¸…Û¸‡û pè€'Ê`˜Q&òªzI}¡¾R)T*•ET%ë+•ÄûXƒ¬}v2»]Çnf²ÇA°gÙÛÄw@ÜéöøÈ1¯—ü\wÿø Ñ¡û[AÏOA$þÞc1‰ñ÷œ°ƒöYÉÛÀK±]Òý¼‰g`»^˜…9YA}Ìt,Ä–lÅÎêæbyæd{æfõ¿úW%aNbkvd•‚MÕ |†Ïñ¾ÄWøÚH˶ìÀv*̯¨ÔÊ`'vSc8’aÞc˜`Å€(ŠÝó0Û¶mÛ¶mÛ¶mÛ¶mÛû˜m›1¶¡/ðß[àú¢>·[åVû馇íZùen¥»ïù¸>žïª*»r(§r)·ò©¼ªª¡©±šj¨†i¸FâµT«´Zk´Vë|&íV ‚¬»úFV…¨D#:1ˆM2‘—|ä§ 5©EmêDúÑŸ dÏÌx6³…­lç"Á\Vb*/±T–TªFfU'‹j’M5ȪZdW}ò¨…ÔœÂjAu¤¬šP@ÝMaõ0EÔÓU/SLý©¤!ÔÐêhõ4šúš@c¡ ¡¹§I4ÕdšÑRÓh¥´ÑtZk5‡ZDW-£§–о„âŽÖ3X« Ñ^&ê³t’Ù:Îe¡Î±HYªó,Ö Öê«À&Ýa›î±C÷Ù©ìÒCvë='õ™súÎ%ýÐ_n·7YEzu!¤÷ê§}LÒK«q5–ÚÄP=b³WOا§ì×3PAI§Þ¦¸Ž0CÝL!-¦›òGH«=LÐ%–é&ëÃ}Uâ9xÁK…×¼åïùÄg¾ðÕDåŒ1ÎXÒ„2¡M΄7LDÉD6¹øh0Þ„0aÿ/ýé^¸÷î‹ûæÇùE~©_ì—ûù~‰Ÿê~¹Wîƒûê~û•~_ï7úÍ~«ßîwû½~¿?èû£þ„?åÏøsþ‚¿ä}öwü=ÿÀ?òýÿÉö_lWÛÝ4±LSÓÌ47­L3Ñ´0­Í3ÒŒ2£Í$3Ôô3ýÍ3Ð 2ƒÍ3ÆŒ7cÍ83ÁraíaÎqáíQÁsíqÉžp‘íIÅžrQíiÍžqÑíYÞs1íyË^p±íEÇ^²—]\Ï^qñíU—À^s íu—ÈÞp‰íM—ÄÞrIím—̸äî¡{잸î¥{íÞ¹î“*“AUȨÚäPrª.¹TÜêLyu¥¢ú˜H  ²QMƒ©®)4×TZh&m5‹všM{ͧ“ÐY é¢åôÒ zk%}´‹q:ÊL`®N1O§™¯3,Ðe–ë +t%ºÎÝb½n³A8­œÑ'Îê çõ“+nc ÀHU6ª ñTˆø*L!¡Š’HÅH¬â$Q ’ª$ÉTŠä*M •!¥ZRT­(¦ÖWJ¨-%ÕŽRjOiu ŒÆÑPãi¤Í Ó†k+#´‘ÚÎ(í`´v2Fû™¬LÑA¦êÓt˜ézÎA½à^qD¯9ª7Ó[Žë'ô• úÅUýæšþðázHØ Z3Ù¶í]¶mÛº@¶ÖÙ¶mÛ®/Ûý¶ùnñDE,qÄ“@"I¬ ó¬eëÙÀFÒH5›ÙÙÂVv°“]ì6‡9Íc^ó™ßd{9ÀAq˜#µ°E,n KZÊÒ–áÇ9ÅiÎp–sœ·²U¬n kZ‹K\æ×¹ÁMØÜ¶¶mmg{;ð˜'<ç/yÅkDc³šËܵ˜U­f}ØÒVv¶‹ÝíaO{ÙÛ>¼åïùÀG>ñ™/öw€ƒâP‡9Ü|ã;?øÉ/~󇿎vŒãàD'9Ù)ü'ˆ`B%Œp"H&Å®v²©öe›YÅjÞ8ÃéÎt±Ëh?YÈ‘lc;ûØÏWç:ß…–µœ¬hy+q‹œà¤ó\à"ÙØ&6µ¶ulh3òˆ;Üå–K\êlÇ:Êqvt*W¸ÊSžñÏ9f0‰ ‹ÿ«mÛ¶mÛ¶â¤æª6¶Ý¾»õÍ̹ÖÝw´¡MíêI§šÑ¬´¤9ÍkQwº×¾O zÖ»Ît®½éU†þˆÆ—@¼‰$Œpµê— )"…TÒH'ƒL²È&—<ò)`Šif˜euæ˜gEzf”R*¤ƒv©âÙ§TŸúÒ·~èg@&™uÌ>²ÈŠ ³l²ãÀ‚U9±a—K‡89ä‰- áâš9çҫާ֧ΫÁ«Ñ㟥z€a- £(|ÚÞ3¶­gÛ¶mÛ¶mÛ¶mÛ¶mÛš•ì¿É—µã^áøÅôítpÎýcBøW‚‚ô? ŠÑ(L£QÅMü¯4ºi ŠÒ˜ˆÅnCcKÐÆŸÆ• /'A%_‚<4hB¤bg¢©‘†¦ŸŒ¦Ÿ„¦Ÿœf 0Í(ÁTš ¥F ÚѲt¡å$hOË‹OM+HЂV?”VBuüÎ@k¢5þC :жt¦í$èHÛ‹OK'š&IPˆNÐ)âCtªè4ña:]‚®t†=éLñãé, ÆÐÙ\¤s$¨@çŠOOçIP‰Î—`<] ~](> ]$>]Œ%Î3éRñÃè2 7§Ë%èEWHP“®?•®’`]-Á%ºF‚Št-Ö;ç§$(B7b+’a›I‹í&v˜"؉ݨ€=¦ öb?ºà€éƒòô™‚Ã8Ž™8!>!=iVáΘ³æΙ—8/¡è Í q•ý½&¡_éu Ŧ7$”ÞÄmvzGBè]Üg— $T†>”ÐYúHBÍécW”~láqÅè§&1>W‚~n’â qÅé—& ¾W’~m’áqåè·&5¾W•~o2âqÕè&~Wþl2ãq è¯&~W™þnÒãqÍèŸÎDÿלþ-áÌôq-è¿ÎBÿ×’F‘pVU\;ÍDtqi S1Åu§±LiÄדÆ1eW\/Ï”C|qýiS Åõ¦‰Le$6Ÿ"‰¹ˆ¤âÒd¦ ’‹DS˜ªHij •¸¡4µ©‰4â†Ó´¦6Ò‰FÓ›ZÈ n,Íh “¸Ñ4³©,â&Ò¬¦ ²‰›J³›È!n2Íiš!—iƒÜæä1ç‘WÜ<šÏtB~qóiÓÅ- …Lg·†1PTÜVZÌŒDqq;h 3 %Åm£¥Ìh”·‡–1PVÜ^ZÎLDyqh3Åí£•Ì$TwœV1sPUÜQZÍÌBuq§i ³5ŧµÌRÔw‘Ö1ËQWÜ%ZϬ@}q—i³ Å]¡Ì*4÷ˆ61;ÐTÜ3ÚÌìEsq¯h s-ަ­Ä} ­Íq´1!´5'ÑÎ8´7'ÐÁÌDG½Å•§ýÅ5¦ÌÇ,ÞÓ!âÚÒâЉâÖÒIf ˆ»CšMØŒ-Î…“Ó}âÒÓýæwׇ4åqHÜBzØtÁqWéQ³ÇÄ]£ÇÍœwž4kqJÜ zÚ¬Ãq7éY³çL~\W€^1qq]\+zCÂÙèmq›é3OÅ5¥Ï$œ‘¾7‰¾2MñZ\:úÆü†·âòÓw&Þ‹[M?˜þÎ…wþ*¡©ô7q'èïšKSHèÍ$¡Ïhf ]¢YĤ&t‹’ÐcZEØ·iQóŠIè-Ž’ì´ʲŸÒò¨Â~I«šïQ 5tœáš&ŒZ&@mAÔÇÇh€¦øÍÐ? ÚãwtDWü‰Œÿ0Ã#MJŒ’Ð:Ö„0ãá1Á|ŽI˜Œ¤˜j`:f !Jp“.’à ],ÁmºD‚Gt…ÏéJ ^ÒUâûÒ5Xë\Pœ®Ç&ç|?º{ÙÃéAb¢'ÅO¤§ÄO¡§ÅO¢gÄO£gÅ£çÄO¦çÅ¡%xK/Ip—^–à1½"Á zU‚wÎEœø4$~6 ‹ŸG#âÐ@ü"êÅ/¡‰_F?¿Š~"~ ýTü:ú™ø ôsñ›èâ·Ð/Åo§_‹ßI¿¿›~+~/ýNü~ú½øƒôüÃ>L£ :û !þ")þ2…Øì«4®øë4>³oФâïÒTHǾG3‹L³ û)- Å¢%Å¿ Ä¿¢•Ä¿¦MÑ ÐÚ4G{Ó]Mt3ÍÐ]•iÓ=Ñ[Ϥ¯„kÐþ®IH¸6(át&±sÓ)˜Å.CgK¸:#áZt.°»Ó…Ha»Ý,á.t v±ÛÒÝîM÷H¸Ý‹}ì>t¿„ûÓƒ8ÄîJI¸=aj㔩ƒ3¦.Ιz¸dê;8|Í^H¿Aö@šØghe‰l¤UÌçh,‘M´‰ùM%²™63_b D¶ÐAæ+ –ÈV:Ä|áÙFG˜o0R"Ûé(ó-FKdc¾ÃD‰ì¤“Ì÷˜,‘]tŠù³%²›Î1?b¾DöÐæ',”È^ºÈüŒÅÙG—˜_°\"ûé ó+VKä]c~ÃFlÒõ6›ßqÀüƒ9L™?qT"Gè1óŽKä(=aþÆI‰£§Ì?¸,‘ãôŠù×%r‚Þ0ÿá¦DNÒ[& îKä}`¢â±DNÓ'&šsšp|šE‚Ë4‡WhNäb_£¹%xMóIðžà-(Á=ZX‚´¸ÿ'É@Eσ(ŽÏ¬mijv\ÛNkÛf®µmÛ{¹æ³­ëûlÛ/¾9}«O']üjþÕœ©¼º\ãÿ£òzlµ®òsl5·òkó›+û`«MÚqiå–ðάm—Un ïÏÚqmåŽð¬à.U;Ãû°v{Uí ïÆÚ îXµ;ò¤Ê}Vî ï“âñÊýà}RûýªÀçWŒxªò¤ßr(Ü«ê0¤ß~8âéÊ#àSG"ž©< Þ?u4âÝÊcà½NÇ"Þ«<Þñt<â£ÊàýM'">®< Þåt2â“ÊSà½N§#¾¨<Þåt&âËʳà½Ng#¾ª<Þñt®Ñ ëit¢z é·SáÙÿ¡‘=™FBTöt„`ö/¾Ïu…؜ϧqŸ½˜&Bbî„£I’‰¦Bjî,§i‘½šfBfîÓ,ÈÉŽNs!7w6Ò<(ÈÞJ ¡0wÓ"(ÎNNK¡4wöÒ2(Ï>H+¡2wÒÒ*¨ÉÎHk‹wŒÖï$­/Þ)ÚP¼Ó´󨼴 Z² ÒVhÍs´ :²/ÑNèÌþJ» +÷h7tgG =ГJ{¡7;)탾ìô´ú³óÓAÌ.I‡`$»,…‰üY7é$Læ¼*‚™ìštrç]„Åœ7¤K°’Ý”®ÂF›°™ýnWšnW‚nW†îï'Ý)®2݅ݜϥ{°—=•îïÝ/®= ®'=(®7=$n(=,n="®:=*® =&®=!.ˆžWŸžo[o!=ƒ³ì•ôœxéyqýéqéEqƒé%\fÇ¥Wp•€ÞŸÞ—ˆÞÂmž7½ƒ»ìÍô¾¸ô¸Aô¡¸!ôž²sÐgxÎNM_ˆËN_ŠËC_·§w†¾Á[öqúN¼£ô=>pý(®/ý"®1ý*®ý·¸&ô¿âŠÓÿ‰kMÿxþýô¾³/Ðâ½¢?Åõ£¿Äñ¾Ÿ(¼„ËD#"2;3 ¿1 þK;Dü4ÑØMhtñ[Òâ¡1ÅïEc!6»#~Wüz4â³kÑâ×¥‰˜=‚&¿5M!~EšQÂe£™ÄâÐ,ÈÊ.F³‹• ¹ÁÛͯOó‰?–æ"-€‚ìÞ´˜X ZRüq´”øhiñÇÓN⯠ÅM»a{ (þ:ØÛéb±pt©XtºL,<]•ìtµX$ºV,2]'æè±º[ØQè6lgÿ‹î ¥»±‡D÷‹ñöNlb•i¨XCü‚4¦øói"ñÑ$â/¤)ÄkL3ˆU¥•ÄRÐʨÂNK«¢;­‹ì”t´Xz: SÙÕè4Lg7¢30›]‹®kB/á*»})áø{$ +G=±’Ô‰•§áÄ ÛQÅ*Ñ`±Š4D,#.“ÆËB㈕¥ ĊЄb…ij±B4­X)šS¬-%–Ÿ6ËK‹e£ÍМÍc’zbc©“piXAKÂ¥¡ñÀßÇŸL!1wJÓäHÁ´ô/6o‹ä‹°˜;Íéð>é·¥ëÄïA·`+wZÓmØÎy{º;9oKwa7çé±Îtö³ÛÓ8ÈÎôsÞ‘ÁQλÒcb]è ±Vô´X7zFl =+ÖŒž[A/Š5¥·ÄïNo‹u¥/Äçû”Aæ¼;.6˜æA^öšOl4-ˆ"ìá´‘XÚT¬/m.Ö‡¶ëGÛ -›Ï·)CÅzÒ*b-i3±AôxŸ÷ù8ÉKüI4 ’³gÒ<ÈËæ1+ã‰ý ë%`.Ý.|[[U?„ýØ.^n÷œÔÁçœçþõ[Âeüœ1 Ö<ÚíØ‹yqõ_6„—ZC°‡ñòjŒ0ì¡I–_xÚeEVA „ wÖ°ëîÜÝaõ»»ÿs#.Â)¸ ú½žÁ_^¦kR•Jº%µé^Mjhî?JnÐ<.nT¯^\Ì7ÓÐéáæš…jnÕdÓd4­IM‘Fc_K 5…”U@að…ê*ªNsí)£ lA9ûõ[.3¨¸D}YDÕÆ8 Ï L§ÒpC•£Îa‹dAÏmeÖè‡4UcútAµ¬>aÓß:ø[²+(c·<¥’å<Ô®­³=ʵ”BvZŸ”íœÁg’˜× 7ßÄóôcÒ—¯ë:öÏÏí;¡s åU8 ¹ŒOE“̺¶·,*nïg~MQ+¡„Am4%8rŠ˜ƒO㛤•R#¦QÌ‘óš#ñúõ*¿wç=þïÿÅÖ^¬xÚc`fƒÿ猰/xÚÌ \]Äñƒûö3ê¸fXÛ6‚Ú¶mµmÛ¶mÛ¶Ív'ûËdnΑ¿ù‹'“þü³±å AÊJyz¢Ri+íèˆJObÒ‹¨ "AFÈHúT¢²˜ÙLT¶Ê6ÜCT®•k$ÈS¢òš¨þ¦¿ãú‡ýSÿ¤ÿ¥ Ä4‘&bÉ ™X²kv–|𥬖c© °šVc©¯õym  XškszmCï­½y®Ãé#uNÐ ¸Háb],¦«t}®‘ »u7ý Âkzoé-öÛz›~G‰écË%j¹-·˜å³üXÀ ²³bô’Vš^ÆÊH° V+Y%–*V…^ͪa «µ­6Ö·ú¼6±&Ü6·æØÒZ²´µ¶ØÞÚcGëˆÝ­;ö´žØÇúà€ClWÃm8Žv¾à-/xko]½«ïîݱ¯÷Å>Gù(^ÇúXïp’OÂ)>gø œësq/Ä¥¾—ûJ\çëpƒoÀ]¾ú1<ëgñ¼ŸÇK~ ¯úU¼á7ð–߯»~ïû}|ìñ…¿À×þ?øüä_ð§ÿ ‡0iH†©Cj̲`¶ ›…f‡ÀUÄÇFc%D ¢ô…Ñ^ñhtE7ðVô ¿Ç~ýûC,öW,=e,%=U,ƒ¸˜ü¿Ôø¥ry_Gïñ#÷¿×ø¥ÆoôÛÈ”HxÚlQ˜¶ÙKÓ¾Ÿ~ŒmÛ¼wmÛ6ÇXÛ¶mÛ¶mÛöØ]óɱ‚öx¶ìò«®‹ÌmöèïDåýÛuàÿ[ vceÌÎB"™ Dÿ2'Bˆý9w„1ó;¶ë÷wþÝvmÕß5ÿ£íèêÀÖÿnˆóÃ_õ‘³)Ì„ˆ¯E‘.¯~Á’ v]ðò—ã߆ývG>ù¸tù‚¯þr7ì»QWí:± ‡x$ IHF R‘†td YÈFr‘‡| E(F JQ†rT U¨F jQ‡z4 MhÆ*X«asô ýØÇâ8œ…;pîÆƒxÓuü a‰HLÊ¥B*eYUV—­¥[Ž‘ãäx9U.—ëåyL—·åyWÞ“ä ùR¾’¯åùV¾“ïåùQ~’!–•1— ™”)™–™%(TC 3Â(cœÇù\À…Œc<˜È$&3…©Lc:3˜É,f3‡¹Ìc> XÈ"³„¥,c9+XÉ*V³†µ¬c=ØÈ&6³…­lc;ç\+r%v°‡}àÜ‹ûò@íÐ.=^OÒSôT=]ÏÔsõ"½T/Ó«ô½V¯Ó›õV½_Ðõ)}Z_ÒWôu}KßÕ÷õýH?Ñ/ô[ýAÔÓqÐY›o ,ÕҬȭ9‚KñåuvË»®#"Ë—v¤]+ßÛ‘2¢ßÚÍ2e7“žÂ ÷tÿÉ´³™iwr†WòM{4ÈôQ¾£TËuI]>ˆèò¬6èμ8H¨gòe{]¯vÏvùÏnËre;’kr}nÊ­¹#»~ݱ?åÑv§¿~¢=ê(¶tžDx®žDœébç¼Þq+ïv–=‹!>îüÏzÿ2ßt=Å:d[1ß秺*¿Ö þ¨C¾;$û¹5i¼¦j¶ºÞZmÖÿ;\·cHWµ)׿¶Uê†z£n®7ºmícw±³öꮺ·¨‡ë±z²ÕûoœïüWû¹ÛmJïÕ‡õI}^ßÖWõmLJú¹g?dé:a°çžhéî,×=Uz_o­¶¨-m+Úê¶®ÝlÛ–¶½uZ¿ínûÚÁv¤ï«§ÚÙv¡]n×úøN»ßy:È·ƒˆ½nïºÏ/í{ñd®œŽH°ÐSŸã½ +]²(ïs*uc'“;ͨmÛʳmÛ¶mÛ¶Û¶mmë¦}çÔ{éu×ú¿÷)†µRÃB¤¾~jv¦WÐchzmO[ÐBšN_ 5‚xDGZn¦óÍ:ªf{gê'f1s2Í âº€•3‚šÃ| =‰fÑ2z½$ˆ̯«Zþ˜;dþHŸ±œÇÝ3%f6½˜Dc½#mÂÊNæ,æ=!#ÆJsæ]´š•VÜ¥6+Ã謋Šåñ\a}‚΢ÏpÌ÷ôÚ*ˆùÌÒË8r;]ÇÞr:…• ú–Âü€]'ΊÒ|z'ÍàÈœ»n§+5½æ›s…>9‡.£‹è½Aɤ'þå+ËÏ0Ý”Bæ8scæG™1d¾Šy?s>×i@ëÒoèÉúµÙæ±Ò›6£_ÒM[Ócƒx®Ô¹©É|îj½ \HO¦Y<Ïî ¤Ðëè)hÐ냘Íü&Ífås.óJææuüŒ•¨þýNDZ¤±™ƒ¨)~¾‰ä³Íˆ,þç‘ȃD&£¥eõ«}9k ¼#ë ¼ù$¦9Zê?·žV«¦hŽè޾‰õq4NLì ‘s3ŸfÑ$šÄbZNÓ™A)2ÿñ4ÍC¿\…÷sý<À/ô+¡¾ÊoBªßâ·#×Ç| PÛ“ m‘…󭵯ÍÖÚà^kmñ‚µvxÇZ{ ·ÖK­uÄ&kÂýŒÎ’")è*¥1ºÉñr<ºËÅr1zÈõr=zÊsòz…½åKù}åGùýìæƒþvs¯Â)—r#´#Ž ·$Þ ·$Þ×õD|¨éEøÈîÌkð±Þ¤7á}XƧ ±„>óöŸ{køQÿ®ñøRËÉn²=3åLYèÆÊùr©\­ÇËr»Ü«1yXs¤BÖË“ò¼¼­e.C^Õöò¶Ëг5ª÷+\ }T_”­R%ŸË·.I~Ö:z³ vGËHï^–©nµ,”Ùö,çZ[e·Ö‘ýzªB#Z¥išckÕq›µTë\[±¥Þ¬]µ½;Úìm• -Óã­vªíz¡^nÿ¿Öýè~´ÊîA;Å\}Ô Ô§õE}]ßÕõKý^Õ¡:Z'êtkm±®t×Û^ÝRÝ®1kº$—á®wy®†+tõ]S×ÚutÝ]_w´;ÑîÎu»+­ÿVw·{Ð=î^vÏÚó¦{ß}ê¾¶ýºán¬›ìfºùn©[íÖºÍigŸWÁ«ô… 8z4½ƒžBO¦=i})ˆƒÌIô;V÷°üu‹Y9@wÒEñö©ˆo²ü g]AO¢Y´Œ^@{Ó7ƒx„d…J>+oÒrV2˜Ÿ‰·6§°RŸ³¾“§:ÌJ+»™S˜ct!}*¬ yÌÕñæ–K9²6+Ãè;ô.~¼Ç[Yßl.eýkÖoåI¶sîOôKúׯA 8~&ë¹ôWzç¾ÎÞ/˜¥±·ëkãÌcX¿”NŠ_o–så¥5ƒØÏ|'s5]AWÇÏdïëæÎÍæÈÏ™¯Šo2ÓCÆŽŸD7ÑEôÞ dÒSø±qüÇì}Œ•cØ{mÊú£ÌW1ÇéyܱÄò?þ~Í1£(5Åo¦Ÿ›@„ÆÌ„0ËÒ¿Î*DSŽ_`zdáh„ÿ8[6Ãslu²B…hTß´|£l6?—˜ù¦å¹Ð§›ê+|%à_œÜ¥ QöÂÍüjƒìÝSU§yZªevÂGLØü–h®è2Sq9žÆÙ¸wZº7Ûû~¼ŽG­ú¢½ß…È:À|-ˆç˜O¦eôfz!}%ˆ8½•<æƒt7ÑaAÉ¥ƒYù˜¹ÄŒÈß?þžæ™2Š$ÿ…ø ~ jûé~Šý¿ }µß‹fþP$ -"©‘Bt‰Gâ$Rüwþ'ÿ³ÿÅÿê‡øa~„Ÿè«q& WÚs1ßÿ%é÷yØwäÏöÿ?'ÿóÜè§T‘ÂïÓb>m­5Ds3ô­¢_]3šÀÊ":ÉHŸØ0X;‚0 ßsž13[ÑjÛ¶mÛ6¢Ú~¶Â†k·AmÛvÿÅóÍ—ìY{ô…I ¨,%¦†ÓÂ×Áçn⦵ÄM°›­ç¸9ª››Ë½åƒ‘v¿Ù)WÉ<ÈKGì·‰Ž³jä-€W "kÂÛùDÇD¶= JPBÞªFa#:åU9:N5ŠN‰>}]UTuT#5@ͳ±´Z¢cÔEu_}ÕÁº¬ÎÒô0£³„9z§UÃ,Ów…˜"dárîz¼Ößõ_ã×…ï’eYÖȺ`ã–Ùgj˜z2 “œbf™’6k„‹æ³¥­B#»e•oSαBbƒœ^ï|ßGôFôE?ôÇ Ä ÁP ÃpŒÀHŒÂÌÅ<ÌÇ,Ä",Æ,Å2,ÇvìÀNìÂnìÁ~ÀAB b‡»x7Žïù™çv4 Ë;Ʀz?“oeš²{±7ûs0‡sGr4Çp<§p*çp ×p=7rcÇx&0‘ILf óOòOó ÏòœôƒÊˆi(T^lC• ªAÕ¤W4MlÎnÇâdkp4蘘ƒ3Òç¹ôŸ»ãˆ8>³š3³ÅÌp`ffff‘©oÍÌŒ•þÂI•6­™í£pî|Êùp¥—8ºT)~Ë8oæ½ý6ÍÏWÏ—imü±‰ð—¦ŸIÿýOM¶É1.ã6CÌ83ÞL0›M­9`.ðy{ÁqÑq]½^eè ªToÄ&•¡zéµ*]¯S=Ø3Œ=3ôFlby3¶¨]«zXsÒAÛksY"Ô€ iRÔy•-I•Ëò( «gÌŸã¼ðÁÚ9öŽà%¢ˆ±=Ž’HÁF:¥I¿°öÂ?JØrÁ „Í ùʬ•ÍzÔKÔ4 Û±Gä‘9Í1gqN¾rœá9.ñô?óôm¿?y¾D©“U„bÔÀ…aʨáxƱÏñ^øàGíÒÖå[ ’¨Œtd YÈFr‘‡| E(F JQ†rT U¨F œpÁaÊ¡‡cFbFc ÆbÆc&b&c ¦b¦cfbfcæbæcbc7ö`/öa?à á0Žà(Žá8Nà$Ná4Îà,Îá<.à".á2®à*nÓ‹ïà.îá>B¼{˜}ß¡­hC;’HÁV+ÈDœÊXC™OÅ4LÇ ÌÄ,ÌïNÏ ™4b;v`§´™|5ìQ½é éøgLDéY_ѳ"ªDl5„^2LD Çi¦ºXê<ÇßÅ=ÜÇ<Ä#<Æt=ž¢*‚—ˆ"Æ=ãH ‰lt S¾¢'Fô`¤#™ÈB6r‹<䣅(B1JPŠ2”£•¨B5jà„ n,[¿w¤M¿‹÷ð>>À‡ø㶈¾ƒ»¸‡ûx¡,í…~DH"V7i¶ Q&m–SÄrQöÝð`Û†c¼ØÖLÄ$L›5ë±Yb¦õʘ4b;v`§$Í!"{78æ¦üèpˆMΈ:.H³ã¢4w+v·qÄtQ÷ý™GÈ€D:y>ëŒS²aöu‘;È€œGI¤`£Ij i0$?ëÁHG2‘…lä yÈG Q„b” e(G*Q…jÔÀ Ü ›jz öÂ?âKö…8&!Q«?ŠQ—4Ynx¤É,”‡f–b6¢^"¦ØŽ8 ?Óò)ÆT™¹I½IÿW–þ¦ë,Ì1q$D 6:Ð)ßpå¥ÍdøZ"÷©"ã­0Ѳ•‡åaª·Î]G±|žý›)hXHƒÁ jÐ`¤#™ÈB6r‹<䣅(B1JPŠ2”£•¨B5jà„ n42~nrÏWð*^Ãëxoâ-ÜV½É†=ô]ÜÃ}¼`»>ø@_r½×M0&ú£Hˆó8UoËź´QûÄ,ÂR¬Qùf-ZÇ|=ó lÛˆÍD¬õD¹ØŽØ)íD4d©DÔÊ8Ϊ|U÷ëÿ÷­oâ*ýïhÖ¨ƒj™²~e¯,`ì6‚0üÛ»/œËÌÌL¢2·Ë(.s+.33·‚233s8ÇèSÜL?YÖÕR˜éfõíxfggÇïí l|æ.¸ÛÆ«gÚË3ó¡ÂþèZ-ŸH"ö4¦vä-: M]2ÜÙûþH8r·÷hêꜢ£‚,äTòÊulPRÕ¬¶ r‡¡¤­2²7ˆçšðbû,¼‰¦Ú‹át{Ñ…ö¥sà!6ÙUB•ª]µÕº!rn( ÓNn â·‚#áh¸Ü>ó÷Ú(y¢û¹!6Ó …aâ·ÃÓË ¡€Ç­‡×¥½b—ÍLí Ô—j~u_Õá…è{Y{B.|ÿ èѯ¡_Gd³Â‰ùšØŸ¹þEë©7}¹Ý·gƒ< %8 ^Q…z¸{m¸{Ä"÷Œ£·š­Ö{«ûz¿ 9z9.@JÚOa×ÝøpNÞU;…»¡w×v Ü / x¹lÕèTɦß4­abã­~´V‹l4ºÞ†ã‰àG›jµÖ ¿k êÿU‡Ù[êeoÙ‡Öa„…ÆÏ]uàÿp ü§"ÆÄ5®î©k½ÝõÚ_Z/¹úJVâ‡vá]ñÏüŠÊkSlbֲ̫óNÑ ›¾œóý¹rŒ€ÑB§|c­Õ†Ãöqòýù۾ך }µ\Åš=:”kŸ'¶>Ø[Bb+ñÇú-‹ö¤X3#²Zæv¨eŒ§Ã´Ñ6&¶Ç,Ï/×âÆ1¦.ý{pá{é¯7– Û[ÚÕžE?kwÇ,ûUø7kPJ8#Ñö ó7É5:ó 6ûÀGƘ_7Øp{U‹/e ±Rbmó{[Úðôõü÷1šaª1TFmÛ2Ÿÿ“…N÷0Œ‡N›©¥ûƒœKÔWó;‰‹kJâ—¦Þùœ…µüêM‹5j5”eüV@6*&ý`'#ö¡^ÖÉ€X­ ײŠOñéæ$ýñîþÿ÷lÛ¾[}¶­Ã2¶mÛ¶UŠŠIÙH)¶õñbÛIÇ)#û«ší÷¦gU|óþó¯ýõ]??üêCøÓàÿ>f¨g ·ó•þqÞyzðħŒ>aôLr2œ Sá¡>_¼ rkäNuÞ»Åü„Ÿ tM)\§(ríL”ÈSÛ(÷ôù }Pe¯FTÛo{Ž¡À¾ öN¢Æþ“(t*ÜçP”¹×y¨ //€ÜóVÔEîtçZˆÕ B3Ž"3ŽR ÊŒ£ÆŒ£Î¬A½éFƒéA£éC“@³Y‹3„V³m†h7Ù¿¦0»Í,×õXé5)ô™.ôcÀÊ +c6QŒ˜FMc&†¨‰#ff#n0öHš~Ì0s0Ó b–Iz×&¦Ã}rÇ^¦'âLçç›N\k<€G¬<ç±/âutã |Ž|aúð%¾Á¾Å/¯ ±ŽFÐÁB"Ír–C¬¢¿%V³=¬áÈ(£È1Æ8²Lp3¢ç8fq‚+Æ«9î‡Ô“š1ÀÐÅy°Wá!VåQÎá1ŽÇòt+gòfûÜ»ßÃûçý¼ß«žé´óƒ|³ù0=ËÇøzù8ŸB?ŸæKäË|I¾Å·¼ÿm¾m‡­ÜêŽÛ¸ÍïawXßÉÎwqÈiN»×;|ÇÊ»|×ù{|ßÎð×ú~‡÷Ì­ûއãüÂñK~éÕïù½àOV~æÏîþ qí¯²¢ˆ˜B…Ž*ÀùÖ‡uÿã­”© U¨Ây¥*Ñ¥*U;¯S#¢jU«óvµ;v¨ÃµêD\]êöÎ^õb¶4`‡ W£Š"©¸â®M(éÕùZŒYZª5VÖj-RÚ  X¢ÍÚŒe×8VkR“Ø¢´ÒÖsÊa•òÊ[ÙC{`¥öÔžhÓ^Ú íÚGû`©öÕ¾X¡ýµ?¦t˜³îIY¬×Q: #:I'a¹NÑ)Õi: t¦ÎtÉß®ø1ç—_×ÿÊA7 ‚Û|SÛ¶m÷ Ê ¶ûðµÛß{œìÍÆ¹x2?dz*â%^Ô¯ñêä-Þ´ïñnç#>”ŸñeþŽo›?ñkþ‹?Çü›‘Pu$#ån&2N²‘Õ&yø"5¨”|òUN!E>‹)V+%”˜K)Ue”¹UN¹© ÂyUª§šjÕRC“ZjUAuæzêí7Ðh¿‰&­ÐL³òi¥Õ­v:´A'vºé¶ßC¯¹~µ2È –bH%Œ0bgܯ ,Ú\cMõl°©¶ØÖ";ì˜9ñyÆ™ö8ç\ \pa¾äÒ|Åf¹çA9nθmdãÿÆ{af—éÊÌÜúð13 =fff:ff<ærÓ>*¤0­StaŠ“sý|nGÍÍ÷VßìXùÙIS\Íj6òjµø×î÷[e.÷…ÏsöÎȂ……‹r_W-J¤ncíí³À¢¾B—µ^<ÊW:ëD[ðtbõW‹>}åY©çÑ*ÄœlÄ×¾UŠ9÷Ž ˆÿ"bŽ™ÎÓØ0H9 j¿KûpÚlþ”˜0:Ì ¶™å˱.³åܺª Ø’Qíg™ƒ×'Ï a³¾ìlé$§€>)=«\À.ÜÍhÇ««Ê}+3;íÁ³y¤ô2Zຠs_H=ÚÇF°_kô0•‡}4Sy_îOR 9ÆRF]}Ž9Þ—e}B<ßµëÍN(Õùj"lŸg©û›Þ­T$c¦2C|cx¥Ó‰ìDÌ6¬ƒ86£“ºœêŠŠÞ”¿ôîì7+©}x q†Zt(’ŸBëþŠv|="øw¢7‡Åٔˮ²‡…Ó¯eô¯bwµ–¯¾Ž˜Ç(‹˜UdŽ5l„p2u¤`ÏA7·Ø•Èñí£yëÓ6âÉ9¥–×.r¸Ÿ=û˜—ˆÓŽn=fiÇrž§Š`¶qCìƒ:ÛøÿŽw5P–ñ«|˵`›Á}Ø„VlÀí:tL§{Xû“ø—x;ZbÜçä’½ÓÂ5¬ù ˆsîÂjìqÒ:lÓÇŸ=sõ&ÚqÇ\M–0nƳ?-vâ8•³oXˆeê—6kå,–H¥ÏTÂhg<“ ‚Ì×ÞDìb´™çX<âìïA0Äë~D=•«¡Iµ³¹ªÅý2‹g!¬ý „65™bчæÇPLÜЀxêÀAX†Ñô¤¤ÌëâO›ÄrÎJD8K‹³÷g²Þm쟘Íá}Z1¨ÔKK:uÈoF¯¥¯ç¦;^Z7B,mq#»¶ ˆ.ç=é„[ÚaB\wÐ[žóœn3ní¬ªºÊ¥óoÎ >‘½Ì¥öˆ"â¾}ݶI{c*„Ü„…V£§?÷¤WÞÐêZm¬æÜ<ŒÕÞ,À3ˆ`/ëÊó½q$¯cúöٮы0nÇV„ð¯ôÜóJ¡J\a;¢QÏ|½ä¶B¸c‰ 9oêÏøa·ÍË¡O.S‡0ë,”˜;ôé+Ý.5ál…öÑ<æ÷Mf•÷lŽyîË1/<É•™›Ë›±«dêÏà_ØŒýØG± Gq‚³µÓ\Áeu ýå(Âø=Ól7%[­\a½ R]ìÑG‹)fÞyqdìí°ùæ4NצÏÍ •ö¬+ËúœëÜAmVô·j·Éç¿Iïn­Á*­¾>!UZw}Zj´îúºV2WûÖÈxß:ß:™ uÔ™HÕt*UÓiú«¶ L׺èAYè;¬õÏÅZùìÑÚé›JäJiÕñST?£JUµ|NÕªù‚óÍ›|…já×Ô5N¾®&j%ð{T¤fª™òc5OÍ—Ÿ¨Ej…üL†èÜ>c¾O®Ð¾Æ©1’žFí£YûÍŽFrW%ü•ó‚áÚ7ó÷}hTÇ*â¯õäFÂ4„¾Yê¤Bn«”¹3 >mSÑ©1MSŸ\AŒröZ‹B5hïMô¥2‚}½RFñê(ñ볟kY—’r«PèÈ´„iˆ‰W+åúð‰hß ïgË2=á3…©ðiú svè²ìgL•ÎäÐq£œƒ¹màWã¯Æ]O7’ ? ÅŠÙ$$>Š9$ÃH(ü$CH(æ“PÌãXóq¬ùIa’SÌ"§¨"§˜ONQáÛíÛ-ï&­ð“VÌ!­('­˜IZ ­¨&­XFZ1´b8iE€´¢´"@Z±Œ´"àëöuË*2‹eddCÉ,–‘Y4‘YÈ,*É,dd2‹:2‹™ÅT2‹e¾·}oËt’‹ÉÅ2%J¤–üâ:ò‹ä äU¨ e:íI€c()F€C‘b,#Åh&Ũ*U%+É2¦‘e 'ËeeÈ2–‘e4“e4‘eTÒ:M#Ë('ËX¦F«Ñ²‚D#@¢±ŒDc¨š &ÈUääõä´fËÈÅ‹Õ\5W®%ãqL#ã(%ã(!ã(UPkÔ‡Ô‡4Iùˆú¨LTS“ êãêãR¨>¡>¡ÃŸTŸ”BÒRÒŤ! HC“†, YD²ˆ ¾Lý\ý\–ª_ª_êó¯Õ¯e)©ürRùIêêšÙÿMýM?ýïêïúé7¨tøFu“ߪnÕñoW·ëóêN}~P=(“ÕCê!™BÚ2âŠôžÿÂ+nÔ»þ'¹Œ‚Æ;E‰4Ýì—Œ’ÏRŸˆÁ’I2ZÊq«Ã$òþ­Cˆ#,гÔB«Þ9È6l¹È™„2W ,Ór°°Þ{e•¢W¾Ê½þ­©9çÕWÑÂT¬ó]›ÉsÄè=È©åÁ/•6‚ˆTØ<Ÿ–sæH "hA‹Ôc/wQ>‹µ8ft‘ l†#TKÂTî©;ãq¥Ø¼×Ka!ÌtBø3’«ìNèw–¨ýœíg¯ëF!³¹Û¾@{¯½ Ro®=¦hå7-Ǩåeò§ÂF›µuŒˆWJdAƒŸóV® \Wèa[^‚­hIé×»ÑÂRrm Â^9‡ K.ao‰Â[ˆ]𜄨‰lÁ‹ùŒ?< |ë÷‰B {ø×a_ˆü÷ç»"sö¦@yÈÇžàX’’Ôîê1î6¾s«ÒpggVKHõÐK#ŽNGŸH²M{t"Îd\D!c*g˜öz{‰QZõÌÜ£âaÃ8GÓÞ(‘Ôã¬Lv—¦ÞÆL Í»°ßωxA— ¨=ç!1£·¾Ž-åÿ¾&IÛ(¬_¬ÕáãžóŽ“÷Uº;Dîð‡”=g<ò1»3}D‘ÅowËbvîsæØJd™]߳Ʀ•¾¤Ð…nÖA^½'a¡—¹¶°%¥ß™z„Ýùùÿ˜8Y0ŠÞ½ßÚÆxjuûlûŸ©ÝF϶mcÕ±í jÛnƒ"fy¢߹Óó[7{¾K.¹¬*?âGTã'ü¬j½ÂkÕÌsæ;½Ñ[ÕE7“òNïTÚ{½Ož?Ÿó!RÞG|RŸñYÝàó~^7yÌcšè7ãMŠ ±R#±5¶ê­8ô6–é]üÒ{x¤÷ãÅxQÄ+ñŠ>Ä}„ú#ô þçÓx-^Óg؞ϱ:_ÄWñ•¾ÂÛ|‡ù+ò->ä;,Ç÷•©Ê”~ÀNüˆsøYÖ<š£:/ð5x¡ª“þ¨Å‹½Xm.¹¤ú£:î’õ~Bij£&j£ OäOÑÕùY?«6¯ð ¥(j½Ê«TOÔá5^£f¯õZµrÓvoôFuy“7©›:©×[¼E]4J}Ü:Ç­sÔI9ê¤Æ«.^O£Të£>ªVó1µû¸O¨‹j)Gµ”óYŸU—Ï{DuõójƒJÖoú#¥ü±ÿTíR­ÿŽ*µFu´ª=Ú¢ ®(Æ-ÊQ3娙j#‰ÇÕNÓ”‹±Bݱ2V*×.¸VC´¢•P4+¡XÅjÈUB® rU ÈU@. W¹ Èä*dýNµ˜¢ÜKÑÎ¥¨æR”r)вt'At.EÙ Š²iDQ¶€¢lEÙ|в‰eÓ!=Òs =Ò“!ýuÙt/„ñ"ßãÅ0^âu^§‡!½Ô¼A%xƒ÷Rxσ÷£ðïmÞ¦‡ žÐ˜Î‚ýl*µ„Jm‚÷xʬ!a ÷z¿÷«L±–ø«Ä&²‰%lb©û¸Æù„Oè–‘ø”O«Ì>öñ€ÏùœÊnQ¸M¢p›Ná6‰Âí1ÿê_5™­,d+KØÊÒh‹v‹ŽèÐÑ]QдpIÜ·hvÜãUfI KZÈ’–²¤„%-¥‘Kbu¬Ö£±&Öèœô2¶53}{œ‹êà£*.Çeu°¶»c †Ô¥nˆ‘ý—C»8´2šw×ü™çîîV% åÁw—%ºÃ¡äô/#gJ²ê.®Æµ’ĺ#ÖÞå±>I¬—ý:É­#nÅí’ôz îÆ½’ û ă’${?Åã’<{ žÆÓ’T»£Ú{ñ"^–dÛ+lût¼‰7%%aÀíHÊ ’”ŠaÚÝÑî™xïJÊÉA|ŠO%™÷€Ÿ/ÉwG¾É÷²¿0âK|-I¾wâgü,¿ãwIþÝÉÛÿîãß»ü»ñï#ü»òï5i\åߟâßWþÝø÷þ]ùw¿ôòïÊ¿—øwãß=þÝøw?ÿnüûÿnü{„Wþ}Œ7þ]ù÷qþ}–Ÿçß§øwó_ãßͯ1È¿ÿâß•Ÿãßoòï5ÿË*ÿnü»Ÿ7þ]ù÷9þ½Ä¿{ü{÷ñïÊ¿·øwãßÕ?5H¾wÈ÷É#+GVJòïþ]ýby¯1ï æ=μ'˜÷%æ=ż§™÷óžeÞsÌ{–yO0ïuæ½Á¼×™÷óždÞ“Ì{žy1ï1æ=Ƽ·™÷æ}ȼ/1ï)æ}‰yO1ï1æ=Ƽǘ÷Î=˹çJ¯ÿÆ_vÎæµ‰ ã¢Ð‚(E(xU°'=VðXÃd!!!›ÝÈó% "7vWžË-y"BŽO>IíA:Uù0ìÑÆàÓD ¤®XÒ)¡›®N 8—ͨÔirúï¼À«–v¨œ0Äa0ži½NÁºìð‰GŸ>ûXÙÂÈ_=¬÷e&üêúexœ|*B¦C•½É˜6›a9 ’:ÖÏEt*Ó&Ÿ’ˆðMMÄelûާ^ˆÐ¦A rŸM Ú^½ïâýú¼òŒ¡õ\Ù2CvÈ*M(ïý‡šdµÝ‹aìL«Ï|6>£Åžç3e}>’ðίù&2š4eƒDóÒ_ÈHD(hhBw×ûÍ_“Ë–¤‚«¿¬+è¦:ž§XšŒrFŒ08É3×Òsgqºó}¦‡+­ý–T«çÿ¦Xr¾NF‹–¬“Š›t0S‡ŸÃwVº«šë: :‡~p kìrˆ%£MŸ1ÚEÀ=<ã[mÞ%Òn¸-—?EÁ†”Ã!mr:2'xOÌßÕabw e%Å-ñ] ç¿È)(È9bL®}µf>e›îź%ÏÊübpdgw„Žc,ýªȺ¢²¸ƒ@ÓV¦¬–•°6””»U9Çxý¦§Ü|óg¡¾ ü£âCQƒ.ÂzbCNâñ…TÜÓŸ€›£!òJ RŽÈÃEŠÓ;$Çw02ާרgD¶q0®Ç„Ó\ì•ø®¿zfÃòö¨U¹ 2þSÜ•‡“yOû.×±ǰ° kò@i3€+õ›(çÀK‰‚—éëÅFõÐߢ å›<=úþz§tHéß›˜; Y(ŒÞ¾kïèÇz÷óÖ¶âždÿíØ¶mÛf!vÊa)*¦•;…˜ÛÉ«.<.¾Ûwuæ;~q qüöå=ÿÏËkÆóÕôO¿† áû;4úõ¯(é·ÿƒGÜÿù×þäzö¹U¥æH©”I½Çi‘vÿû~ÙZ*eg9Tòr¸?Sòˆ?ÓòŒ?3¦ÆÔÊ,^ÉyÓeúd3`†eO3j¬ìK÷îs±¹XNÄy’yѼ$'›×Ìkr*ŽÆÓÌ»æ]9;ãæ}ó¡œi>6Ë9æS󩜫9ÍÉyk,çãV¼@Û´M.Ôní–‹tX‡åbϱ¹D·Ômå O­v”kuÏ¢n€6Ýgº’t‹çF'É­ð¡;ôt=]îÔ3õL¹ ös7¼ç^½U“ûô Ozž‚Ð<[t€g0/B_^¢Éä9)þN•´ù“åsl ¥þ4H™?Mœ”´ø“£?J›?1ÇFŽi±²¹4È’H–¶c@Ê ¤ÜDÊ ¤ÜB«1mj}Ö >ëU’2«}â1‰çH<$ñ˜ÄI<&ñfœ™)œ™1¹Çäž#÷ˆÜ3>÷÷ýŸ|àÓÏ~Dú9ÒÏ‘~HúÒH?"ýF(b=ÏÅ^K4šh"e~[ID±¢ht[?¡2¸b \±®Xí§µ—ÿó½uo1ºŸ\5“«cr•L®âW ñ+‡øU1Å*¦XÄ+™bS¬òS|XÊô?KÃ,«™e³,e–Ê,EåMf9ÂÇh´ŽÑhÝ„FëÖ1~޼)½Önz­“ôZ{høöÒnÝŒvkíÖ9nÀ0nYËÔGᳬ² >Y‡OÖâ“uød->Y‡Ovœfl>Ù ú±Sød§ñÉÎЕÅ'»À}áј£1» ÏÌ¿½Ù1²›Òží¦=;I{¶‡l/Ú>:´s€Ñ·ÀèC}FŸ‡ÑÇ0úF°á­0úF±í­0ú50ú}Œ¾F¿F¿FÂè30ú<Œ>†ÑG0úFÁè}=5ZaôŒ>„ÑçaôŒ>£`ôu|dYᘠÇ\ ÇT8æ"s5sé8fñ8f1Sᘫà˜EpÌUpÌ"8f³޹ Ž™‚c¦à˜)8f-sC8f s޹޹Ç\ ÇLÁ1SpÌs#º»ÉÞY@7Ž$a¸ZiÙaΠ‡™ÙsÌÌÌÌøx™™™™™™™™y7Þ›€²—Éfà¶®ô=GOx³Yõ¹¥‘%wTÛÝ_©Ê7ûfù†oñ­¶nómòMßî×Z}½_oõßaõNßië ¾ËÈo·ï¶ý›ýfÛ³Åo‘yô³¨¨O@E‰»úiâ|/'Î÷2žÎHP{€x‘º{š~)³ä“ÌzæxfdµþOÞÝ%©Ýâå£¥ïØøÃ»¼ûóÝdúx[Ïÿ0ëv¢øå´#Ï,šãÄš¹°œÂ"ˆ5hùÄxmµõ›š7%†X‡­ŸÐŽX”²}„c¶oœMÆg6ZÌÓióª·÷¯·×æ·2¾ZÙ1ªuñ®ž?Ô˜ÊïƒeÔÛõT‘î¡émú3=Òêûªe˜ÖÿšUµèCúŒ>4 ßõ°‡GõD£n«õy{ÝK/¢¾º¢Øœ¶½ý÷ñó!’î1aðÏäDÃ"BT)õ1?˜¡xÄxí~»îrqjB4ßíÌÓÏ-=qü(OGÙ‚râöÌw‡ýOad?í°ÚÃØ‰eèå .¶hV|èÀý,Æ›ˆpýFéU‘l[÷EóQuzs, dÜÏÿŸúTŸ'U³ÑLàfí¨m5x÷yП^·Ï£Ù—KRÊíïs\¬ÐþÊ#ÿŸ’<¹ó­*g==|Š*ö—8^’ýªi½n¯]¼‹~¿o¼Å˜ÅC`àó/ñÙ‰;X\+ï mýp]¿÷÷‡^1¨w=0ðw´æÞÚoöèûçéøùc¯«ãWæŒqc‘ò‡Ý!v¥ø3L}#å®ï‡lTDÏpox+üSð„z4~Õwæ—T¬uczâOÇ©±mm)Ì·È_j¬ÓâòZ„¾ÿÍ0‚ ©Ë;g‡^OX6,"z€îÒÇ•(“/êÕš#úäyo±¦šëmÑzé¹:?›˺NFQòŸÚ¶û[*£ˆËse±,‰ö/”âKßß'µRcX<êÙü¾ït,Óµý{èê!œ±sð1aã1¶µ5®µº§ðûg0T²06nñß?w†>¬9½˜ ïÓÍúÞÅ¢uØzðê*ÏTs|çnIŠogáÑú´ÞfAˆ·£Øõé›<ÕéñVý|Xyè)ËóqüÉi{½Ò´Ö{–÷i¶k+=ÁÆ~aF¿ìè¬ÿ#Ú/õº™¶õj?¾Ì]½seµ/_оÁ.ãuLîm¥pÜ;õ•¾ñF¬¿›£ßD$dÍê:ÚT/\­'/@ôk¬³ÿÈÀQäí\Áß«›w¼þOAžcq¸™¬åê6Ç@!v –ÐUã&žå€9‘®¡{ÉGŸ’v ½¸ýÓòhXÄþ5V d±$$Á}lÌçK¬&NküP~.SåßVæsÚOæq:J¾$ÇÈUF°®‘‡Œb>"]²—K¸‘r®Kº”\äÊ]¥\âæ¸yr¹[â–ÈUîëîërµû¦û›\cÜh?yÊ`¬h£Q¢G>=kôçcî%£?Ÿ2ÞÓâ>cŒ'ç>ïÖYù’ë0¾óe×iå«n³ã:s_Jï¾²÷½`d0Ò}ßXN•û‘›÷ó .hp¿š‚1î×Fnƻ߅´ÆýÁhÍL÷Gc3óÝß‚•ÁÜßýKþ%·Ù¿ì¶õ-¾Ímçs~­ÛÑwú.·K8Óív÷›ý·‡Í¿æö çµÝÞá\¶ÛW9‚¿]¥Œ´ðméáŠÃˆŸ0‚Â))“r •”*J¥T[©…:{WÆÅòZN:Nƒ:Îà®T¹²ÆXcLÈ+Ãeª•$¼±ÔŽ5¦iLCk MƱÆ4¤q2¤q*¤q:¤q&÷»’ûȶ²£ä©t²³Ý{o&ÊÁÖ®C¬¤äP+£ä09ÊêÇXIɱrºr†ÙG¥ÙÇÖTÊÍVªä¹Kj¡”£¡”ãäii³6µËz™ QÖË)Ëidº¬tÃÜ0ㆻVivæÍÎ’’rV¬^îÊÅÃ0+0±¼ ,o$–Wa–÷})…d–@2ÓÌ„û­‘ÌáîÎ"£º?¹?Ùñ6MšnmÇoc–Za–z¨”¹ÃÜáV?Â8g œ³ Â9Öç v¶Ým2ÞÝn´3 íœ íœ íœIÖË hg©ÑÎ$A¾Ë$´3á,pV˜í·H:´~«›õKµY‡T@8“¡¬n4ls&l3M^ËRgÚTQ"cáœI8g:Ô†TÃ9ÓÆ9ËíÈŠ ÂöT•¶§*¨’ŠP3¶® êmÝ4Øþ¦ ÉŽŒ‘ÈgòÙó ó¬¦3däsr¨(i2E­4RŸ 2võUatåàãÁÇ%€y¦až%¡Þ¤!Ô›Ô™ÞZ¬ÞjÜ©îToÚËY}ѧˆSÄ©âÔqj€85†j”z(ÓtSã2)¶ÜøRR¹Öï`ý)X MV£ÀÚK¡ºI¨.@u“Q] ªó¨n¬$¬±'ÖŸ„õB‡«Ða-ÚËÀú“°þ L¢ÀL,wl) lDe(°6¡À X?¾#âðøK âŸBÕ°þ*š„ŠXÿd”S‚r<¬$¬1”ß¡ ™ å¯!G,¬_’°þQä…]NV¡“ :©…øg þˆâŸA ó þ£ÐC)Ä¿ ”¡r4Pñ2ÿZˆÿ*r/C «àþ5pÿ$ªX…*’pÿF¸îŸ$GñBèÒ¡ è—»n×-Ç O€$j©@-è$ƒB2A"HÈRtÒ„?@4âÁ € þIÓF“,Ç+ ƒW@m4ŲÀÖ¢RÒˆBæ£2|FãÄ7 ߀øÌÄ7`:I¢“UÁ—‚/ÉRžÿ9–‘cy1ñçÀ«bÏÏVÁ?wžwž wžwNçÀÓpç*¸ó8¸óx¸ó8¸óx¸óX¸óX¸óD¸ól¸ól¸ól¸óp¸ó4¸sîü ¸ó¸ó'àÎàγáγáγáÎÃàÎ#è ê¡Ïu>ëÿcõ–¨/˜ ƒ®óëü:©‡DÏ…D×Åú…ú…¹~“ß$õôuô ùÞÁ•NC¥§Ð_4A¥gA¥+·$n‘é‰l"+K Ô£$HýU¼Hê™ÒsL£ÿ!þñ1ú„ÞÏ“;ky^çíÒ5ü}†˜É-ú¢iG^oÔ¢Sï‘1z‡Ì~ß0ε}âþ¶Ä£1Å}#£ì‡åŒ}Úµ[;ß-|…~Ñušc Lj‚øŠâÜäcÝd{žnaŽ¢¦gŒ@œÐw{yûã,×Xù&OIÚ?ù¦¼ß—¾ó¡æk->ƼºlOcCHŸ~Ï|Þ ù×;ôØ<¿_›‹R¤#ó¯Wê32èú’Ïþ4N×#­…¿Ó‡­‡ºGï·ã6„g·c°Ú=zB~ûPÝQ÷§¬TÛ´Eï‰1•õY1½ðÓ¼®aÞNû^Ð'¸·îÑ+¢ó{òœ ggîÓö<‘ßÎ_™àv׉{Ëou†ÄÈ g~Ûû£ïpäÜ'bóÌf§1ñ<ü½™áŽàK Z9g·Ú‰®ùŽå¢y Ûbo63–¹ÊCo„SèKÕ4ÙM'¼&\wôû®ëó¯íÌ‘uóî‡cß›Ý=G`ر];£ÌQlGWÆN±Íõ´Õ¶b³‹Ô߯‹6¿­gJoÓ³¿ŽbQ±õ°¬š¸£YbëÛy°kî›í)ìËì¨h.*ÏvtžX„kjlÝV8›eXfe~ôÙ»d’­»óh~ûßJ³>`V°C>Þ•z¼ajï?‡™¶°þ×@¿‰X?G ˆLJ¢…©‚'þ÷QÍö×ÃÐÂøÖ=ÅhŸýqkÕ•{ia^¹Ý‚žôp[ÓÛ™Ãß¹8$‡áá±è˜÷è)}ž½lÅŽÍÆ·cù³ñ}z}±9f>{.\÷×;ó­%<_ÒD®ÅVyÕy)a´\i#ÆYRo#½oJãºq6®û‰Œ·‘Ûod#´¹~k¿µÌ÷ÛúmeßÞo/ m$¶¿,òúe¹?Ø,+ü¡þPYi£ ý$#Ld>#àzŽ‚€ë9®çþ_Ö=èjs…ž3³÷çß¶m~øm›ÇF½ï£º„QWqã4:1Ú¸½ŠNžº=ŽGÉ{Þ•¬56Ëh³ 6Ëh³¬:‚†ÍrͲi³lÙ,›6Ë–Í2Ú,k6˺Ͳf³¬Û,+Ž»â¸ Ç]8îÂY o”»c#¼Þ*ÿ~»<“†3)œIáL û\Ë>×,};Ú“£=´§‚öhOí©ø¯¶†ö4ОÚ³íÙ†ö´'¢=[pž}8OÀy"Îpžˆóœ§ŠóÔqžæÿ8Ïbœg‰«¿çÉqžqž ÎSÁy œ§‚óÔpžÎÓÂy¶â<Ûpžîâ2œ'Çy*8O çéÃyã<çÙ‚óFx³á O@x"ž*³á©#<Íÿž%ž¡¥O@xöá9}xNDr"’ÓDr’1œˆáD 'à6ûp›ˆÛ,Ámš¸MÀmb³± ˆM± ˆÍ>Ä& 6« XMÀjc5U¬fVSÇjšXÍ‚°š¥XMÄjêXÍ&¬æ)V³«‰ ŽIÇ}ÇcîÆ$'êwã!wãwãwã wãwã,wãwc"ŸÌ§³!–Ô`iIÍeslŽCÚšœŽÓœŽ)NÇ3NÇ4§c†Óñwã9k£Ÿ¯qޝ1ÂטåkœãkœgjœcjÜåhœcgœfgÌp1α0Î1/&˜“eIóÝò?Ê?-~žUþü&ÿ>ÔÖ*-¬²9vÆ¡²³ùc6ÀјähLä?å?ÿQS|ÿ’ÿš ð5Nó5fç³\Œs\ŒÓ,Œsü‹sZˆc*ˆ;¼¥o¯ âmýÃ:‡]ÃÝ:‡Çu¯+Ó6ìªÞÖ3¼©dØU/¼¢[¸K·ð baW«p·Já)}Â“ÞÆ·WŸð¶2aWpXðºá1ïáÛ«F8æ=|/óÅ|ö²ánÂQ¤@¸]°­@xGð–aR < @˜Û „I{°­=ØÑìi¶µ¯i&íÁ£ÚƒI{°£=˜´¯j&íÁÚƒmíÁýÚƒI{°-'0®:¸Suð€ê`Rܯ:˜T{ªƒIupê`[uð„ê`R|UuðŽêà-ÕÁ¤:ØQLªƒmÕÁªƒ×TªÞQUlë &½Á¶Þ`OopLoð¸Òà˜Ò`[i0) Þ1ÏV˜gËͳáAx½bž­ £ì5Sí²©vÑT»dª]4Õ.™j+LµU¦ÚjSm•©¶ÚT[iª­ÄK×¢£ëqÑ#ˆè,t ò¹ùÜ€Ln1É.™d‘ÉŘäfrËoz ‘xÚ´C´l;EçIíkÛ§ølÛ¾(ó÷Û¶mÛ¶mÛ¶mëŒô~»:s̬`á¥Þ¡Ï¹ó»³´.Ýu»u _¹Ýòµì°n‹¶â0àï¿Q ŠN†±€jÌìiŸH&=Ûgl°oñ¨¦‹áLe¡3†|F0E΄¨%ÌH¦³g…:"ŒbK˜LɬìBŸîYÎóÙtΤÏVsÒYŸ}zgå}ŽéÍôøœ—Y’ñ¹!Ó=Ëç¡lz‰Ï+î.¥ž(£™I·3–bŒa=®IÄK¯KÓD‚q̦Ï%JhfãIºD)- dsH9SF+ƒ˜È\ÒÎ”ÓÆ`&1Œ3´3„ÉÌ'ëî­¤ƒ¡L!G~éÒþmŒ-—mµu¿i/Wl·ÅR)×­^¹…Zn½tpbÀ¹»¦nþ?®ï˜­vìßά* !ÅRD1%” @ˆzi ÖÍk¡’jj© Uó¨ X¸r7Ë‚éQOS9­âêjò5Ô¬Û¯ñ¨poø®h{tb´;º[ô”èÑ·bk­Û/vGì™xo|Ÿø9ñ§3ñbLd>õ½\¯Òkô:½AoÒ[ô6½CïÒ{ô>}PÖGõq}RßÕ÷õ{ýEÿ²XcÅZ[lKm¹­´Õ¶ÖÖÛFÛbÛm§õmãn÷ÌOÞ.¡?ÄH±TJ½´Š/ *ce²Ì”ùÒ+yÙTVÈV²“ì%ÉQr’œ!çÉ%r•Ü ·É=òà¾à~ã1B±G‰”Bé”E¹T@ÅTF•TCõÔD­ÔAÝÔGƒ4Bã4E³´@Ë´F›´CûtD§tA×tGôL/ôN_ôG!Žá8Nâ4Îâ<.â2®ä®ç&nåîæ>äç)žå^æ5ÞäÞç#>å ¾æ;~äg~åOþå(å)\( àŸkwO¶mÛ¶mÛ¶mÛ¶mÛ¶íº¶¿y@bCHŒ”H¬È‚(޲®0*£&ê£)Z£#º£/c$Æc*fc!Vb=¶b7â8Îâ2nâ>žâ5>â;þÂgL‚ ™œi™•yY”eY•µÙ˜-Ùž]Ù›9œc9™39ŸK¹š›¹“ûy”§y™7yŸOùšùéÍ@†+¦ „J®´Ê¬œÊ¯¢*­Šª©†j©Žê®¾¬‘¯©š­…Z®µÚ¬Ú¯£:­‹º®»z¬—z¯¯ú-Où+ÔEwq]|—Ô¥v]v—×v%-ºå±bVÛÌê[Kn¬­¥´Öß²Ú (Emˆ‰ãl’•·)6Ë"¦ˆ€ÂïÝ •2A‚q3 FÚ*% ªB$9p‰¨"˜1„„Ã~Àxsìq@…cÎɸàŽ÷QƒžhòB‹mڼǶË7GüÐã’?S®qŒÂI§hYvš¶3Vø03£ç‚U~­¹Ä¿Ë®˜¸jÝ!×ÜqØ]÷õ0÷ÄS'<óÚ²7Þ:kî£ó>ûjÕ7».úé—õ‡ÜõP„ÂF脎ÍÐ}7â牛i)-¹E€…º@µÝŠ0Ì•Uwww·1Ô;‰;N¤nxq²‰Â D¯ä†bAzRÉÃÞ ÞÊÏB¾ÿÈζ+ÇcÀ ì±Â/ìÿ¢écgTù&“ལ·ð*­%xcïè-hͬßHÀÊ<‘WóT0Ã[SdÆW<˜·IŒ3œ±Ç klQqDÛÉ)iì½…WiÅ*·3Ï0Ã[\pÆ óNɰGÅþkÞ%1̰À+¬qÆÕ¼›ñ÷È‚Š«y¯œqÁí&óÝ˨û$`‹jÞ/cœ±À[T\Í$Æ€Øc…5^ጊvÓ&?{GoáUZ±Jƒ½ãˆªù û|XÎØâ‚6ŸÉMXWó ˜álß›Òòî€ ¶¨æ£2c‹\Pq5“fX`ÖxWh뛽TÞà½Ì*Š£yCÆ8bj>ŽOHŒ3ìͧ$Æ€öآ∅ù´œ±Å Wó+Æ|‹?àÞÄg$ÂS̱Ãwxn'6}섊ߤ’x#ïàͽJw’x#ïàͽ6ëÑ[6ÊdÏJ‹«ùœì1Á'lÌçå[T\qo¾ &˜â„–¸Ã´Ý™œ’FÞÁ›{•–¬ò"ó¼$v¨¸âÿµ“žI„ ¦˜c‰;œpÛÏËÜÒßœâUYPq5_“€×¸l²/S*Þ­æë’`ƒj¾! F8aŽ 6¨¸šoJ„ ¦˜c‡%^ℊvg&wI#/Ÿs—ÔGïxWr7ßãOn„æ¸=ñû|G\pB>ç&L)y·šïJ‚)Nö½) ïö¸`ƒj¾'רàT\±3ß—L1ÇKÜá9^¢íÂì¤ô&^ÿÜgÕMnàRŒpÀ·1à‡a‚)væÇa‚)vؠ‹ù‰$8aƒ)*Úéø]:újóø3^ÿmžü.½Y㟸ßV<:Û<ùÿå¤Ká/gÿ:Û¶mÛx¶mÛ¶mÛkÛöîÙ¶í¾Ž ¾hW‡éæã,í:S‚MD⼃ü>àIîç à)žã^àeÎô¬ó\Zºv>mèÈt§—1€¡M ôF3ž[™Èdîb*Ó¸×óÐûYÌc…kO°†µ<ÉV¶ñ4;ØÍ³ìuíEÃKÄ‘ä"¤¸ÖŽLrhO>Et¦œ*¯ÖµÞ4ÐHÞä3úñ…kcøŠoËüébÿ­€Y:]—²@—ëjÖëZÝÀÏdwèÝÁ.Ý­Ø­Gõ õœ^&Ö³Úd5—‹­öêNºz«7 j„FR¬ÑG©&h•š¢ÙTi®òŠk oj¥Öð¶Ökïk“6ó¡¶kk¯öò¹ê_(VY|¥åð‡òTÈŸ*SÿªA¯ë$Ï‚Ï ¼Zü^´äIZÑšNt¦ ý™Àtf0“¥¬d«YÇz6²=ìc?±äRL eTSC¯ð*¯ñ:Ÿó?ð—¢ÑeºSwéA=¤‡õ¬Z©µÚ¨­z¨úªŸi°†j”Æk¢&iŽÖj£¶h›vkö鰎討)ZÙ*R©êõ†Þ$Ì›+¼#ôä×4„,‘PGÏêЇ£zJ3 Q]¢ATr$àpèiÜ@¯œ÷ç!Fx½‹½÷šì_"pX;\B€YÕƒ½bïØGö§Ú×öçØ/ö Fåª&a\Ëuê!®¶ývÐÙ‘ÿ¿ kñ–hÉ–jé–iÙ–kùVd%VaUVm5Vk ÖhïÙö•}o?ØOö³ýVp¸UL@<øˆ^ÒqN(²ý^>-”Ã¥^~(ôvmÜ â¯êœpQX—ÃBù)« س(ìÙìVæñ­Lß^A€tôC>JQ ƒ "V±2ÌäøxPEÄp\€ÞÕ¬¨fE +tÔŠZVèȾÉê¨gÔM¯©Þû‹)+O£;âx\‹Ûñ*>D‡Sjš:s 9ÕÜlî5o›OÍ÷æOÓc˜UH¦äK¥ ’12MšdGÙ»Oª*ÇË™r±\+·Ëƒ°HD&rM¬*ÓÆØ{X[´šîW5rõ(®8œ2ˆò2|žFæ] ŸgR`ÝÁH‹23}f¦Ï|·W¿,¿ª‘«ó{­v€«Œºjj£.#ÃsŒò:}žFæõæÔåW5ru® ¤›6î¶ÚÒ ¯ÛWhdÅQßGzåõø<Ìû(ÊKëõö(ßÒ£¹ôh.#šËˆæ2£¹Ìh.+šËŠæR£·Žhµ#šëŒæ:£¹®h®+šëŽæº£¹žh®'škÞëÛuá›l¢o-0¨¥š‰HFšÿÚ/A)*P„Á…Fì£p>®Ä#x¿c¾I2}M­™`ÍÎf³¯9ÃÜhž5™³H&#úZH¾Ùf61ª“Øv’½Ø];T¿¦NOêô´#ñùÄ%Ic’NMú>yŠ1 ›aKìˆ=q ŽÄ‰8jßq'Ä“xoâ}|Нñ#~Gz0‹°ÂÀ&U~†µ'ž’Ÿ¿‘_ÃTÈò'c•üÂX-¿1ÖÈŒµa}l ë5²^#ë5²^#ë5²^#ë5jý{’\‰=$×ëø¨Ü®ã3r›Ž/ȵžFÇoÂÌØ¹¢è7CûVˆ"ßQÜ› Šz‹ŽµûRî"þÝÄ¿“˜wó^¢ÝO´{ˆs_ˆƒBŒÃ44`[ìŠ}q(ŽÅ©8—âZÜŠ{ñ(žÅ«xâs|‹Ÿñ':0󰫤ÕëÙâõl÷zvz=»½žm^ϯg—׳ÕëÙÂÈzêÙÉÈzêÙæõì`d=õl¦ž‘ïÃäû ù>@¾ÍÔðQ²~œ¬Ÿ¤†ûcÔð j˜¶þç™j1EÖ¤jì¬Û_^†Øsåöx–=^噽Â3;PÇ×äiv}Š]Ÿg¿Ùï9vz^ú§õ‘ß$òëÄy8oç]â¼EœwB~e¾æ!iýO& ÃLÂ&òÑ>$ÚgÜç§Üç!ÜçûÄÿˆÈù“P‘ÿÁ3¼¦{Ìò˜ã0Ï;`¦wÀlÞ3¼¦3²ž˜Ã¨õŒ¬§f3²žè¡>×ñùVdzäK*ðø‚|{èƒoÈú;²þ>øšª~ÏsÉÃLÀ&hÂöØûãpÓq>.W nƸ_Ux/âu¼‹ñ%¾Ç¯¶ž{~È60>jë@.vs‹Ý†qm9Ù-ANvk“m9Ù-@Nv+Ïi!,"ƒU<Õ<Ãd3ŸîX QäÍÈl ™-#³d¶˜Ì–ÒwËÿ¾ï,Â.Ö°ËZ"¯ ‘­ Ñl,D³âXGßåhÝLA¶ÆÎØãhœŒ³q1®Äõê“»ñ W§¼ ~¶;xuvôêlïÕÙΫ³‡Wgg¯Î®^ݽ:;yuvñêìªc¹ï$ˆb&@1à¾3©È¶¡"6…ÒÈ!#TÄ&“IjÈĦS‘¬õZ¨÷ë°%¶Ç®Êë@Žc•Ù™8ßöe¯~ìUž„-àI\~5Ù>ìžÅîEì^¬Èým‰Ž¹¶TÇ|›Íär'ùÜIî$‡;ÉûgSÁ¾•ì[Î.eìRM´Z¢U§†g#j2s@˜ùo±Ûˆ]±Ëh²EvW݆DÛ€}G’ݲKvãÈn÷3ŒûAvƒ¹«¡d7œìþSßìã}³¯÷ÍÞÞ7{yßâ}³¿÷ÍÞ7{ßìç}s€÷ÍAÞ7ÉvÙN ·ñä¶)¹íIßL!Ÿiä³ ùL&Ÿ©ä³ñ?øþsˆ&?…hòkˆ ¿‡ògˆ ¿„ò[ˆ ü„V"´¡Dè&B:ˆÐõfa:faæa&fa.RâêãÕF´(Ò'%ú  å(DªUµ‘ø¥øÞdáMez(VÉ52ÏÙ«í¯æ Wíš#Ü#\óÿÿÿÄ|(öè?½uù4ö9ïZÄ–ÆVÇÖ&°A,H’‚” -Ȳ‚¾Aÿ 7(Š‚’  ÖßoTq'à»cYÁ¿:ŠPjCe ï‡8oÀðž¨G™ZÓh>Š_3£ÂU“?Ç©³ÀÌ2«ÅI•ä˜90HPç.G¨sΔ³äì¸ìTX7ÁMt¡K¬mµvy<>¶çiÿW‘D=›Wëº ܆n#78\±×ÛÛìýöÛl»ì\çâ*2Ó[­ÇõÎá› 'äB­¿Œ}ôG|/ÑQTûãj ‘↺an¸áFºQn´ãƺqn< RìöOÛfÛm‡agÛùv¡]äâµhBºûÖ}ç¾w?¸ÝOîg÷‹ûÕýæþtÍîw÷G\f¹f¾ï>pºÜÇî÷©ûÌ}î¾p_»oÜ—î+ÄMuM€;Í®ªÅïû&~]œÝ• ÂÖ‘o ýé†ìt/¿ñßúD›Åeˆîá –ËE:,O3?ÎC…¼E¬÷Ý€Gmà¢U2òÆÙ2eꩱ”ɱÙú ~Ò9[¡'›û¿Æã=µ¯i–Í¥NêC—‡žŠn.yžqÙ£rA O2oÕtúêSªO…>ôŒÁn€¤ÄVòkb,Æa?ì‹p1ÇhF‹É69f²™¢÷‡»›³Í9æóbxO«þY×±Á¨¿{~zváB äªeÖ/‰×’LambdaHack-0.11.0.0/GameDefinition/fonts/DejaVuLGCSans-Bold.ttf.woff0000644000000000000000000070162407346545000023002 0ustar0000000000000000wOFFƒ”9È^¸GDEFe4éGSUB t¹1fpgm]D€¬[kðgaspe( glyf€÷RO '{}head=Ü66 ÕhheaUä $ ˘hmtx>ÐD, Ózïloca˜!CD0-@e˜maxpx L-named ò-ŠC poste ÿZprep]ħ|a¢çxÚ¬ü|TUŸsïëÓ{f&ÓR 1¤B(2"½«ô)¡¢"%¸öÑE0 J]Dd1†ˆÊÒÔèZpÑt]ÀF@×6Hß½o|úï¹óÞ¼–ûî=ýž2@0@ƒ*Dâ6ú˜ô(T)•‘H Ùui(()ɇÜS ysÐuÄ1Œà´B]/Äizµx¸é-}Gf;,jß¡zØ™‡_õ…Ñã–dê°²o¯Ëq={ ƒ7àâïËçYûὟOâ†ÉïNaŸ¨þáù_7ô6èÇ#†èŸ`ŽØ. ¥ÎÝ,–ÚÖ½°ã=õ›“JJPo›+ê_Imþñêko[iw…žEoÔ_Ö¿ÇîÝ{Ü"t¿tR~KüLàc¨Í„B˜/ š¿ß””L‡gÍížMYÕæÙ€ô¬yU’³:9°¦½??;¨RÅ£ªŠ-_‰¨ÙŠ-½½ýX}›ö¹_ؼ HØOœïrʸb?—×ÿ–‘²]<Ë·Q)˜"±)t`3Ì,ð²i§¦d0p`*ÆZûZ='ö4°OŸ{ïúôÓ]»>û¬)ý8ýÛ«v>¼‹o} êÓgà 2máíó.œwûÂ-ÿÙ³çèÑ=»4~"YŽìÙóùç{öÙ²èöy‹Ï»}Xx餴‰M>ò°C|”-&›ªh6EU®Ëi—+dçÙ¡ feÚ!=/SAA0å¨nÂO†rÖ[ckCÏÖX«5³-¤aˆÑ»)/-än›f ùU_[‹Ãœool¨?t¨ÁþÎ9ƒøÆJ ryçÔ/_Ÿ³³æ0®;|% PYϲÖ/8‘J©U2ÆÅï“ÀFm‚ªÚ4«IÍ4åy!B#‚_ i!Sr^:äj¹¦ôŒnZ7S\î¯õ7õÏíŸ7G“aòhm´iX›±íÆæ,˜P0¦“©r©<]›nšœ±´à:Õ¤eúM̶¦SQr^(/\‰“~¦¾™}ÛŒÀd¬iLæTö/³Lžq8NÅë±8&c™VLÃZ±AÚ…^Ÿ£¦(¤VL ²˜FpUQ±ˆÅ¶òI˜®œ8ýÛ¿è¿ëÿX¶¡mý“1oÏ¿eÈÀaùOßÐ}ë7Î/ 'ôë{mŸ³S?5Sß;»WôýëW3ŠftÚX éßçµëÜ!e„þIî½çmÈÊäÂk aç‰kø8T *Ø ×`H8¬i–m€P£ÿDÉ .ka“¼Ñ*£ €É!Ø­L4Ö8¹°µ²6ƒµwòÙzÜN*蟭úëòjÌZº´RÿéWd¤†oü|Vï|ü¸Þ 6é?‘ßŒŽ“ãØ$m´ZdÍ!µ2¹QÐÐÒ««ƒ³¨=ÉŒy7‘7-e˜U½ü¯«ôŸÎá;ÇãÛgÖ»=ªßø+ d‘Abë€^ñ6v‹ISe hf…Šs˜­kœŠY£ªä H Ø“j‘œLeÔc›ÏGõìÃi -(§3ˆ™9X,: 邤ïÄ~ô[õÚ8ÈØmÅÂß^õš¾G¾VµýïU;q¤¾e' Š˜ %n™>¯âfQÀI4ÉÌßÜØ¥ž‘ùù.웟 yö6&Üôc¢5ýr®é¢‰BÓù†¦óÄÜ@Ì€0[?NJð^Á7Ó`‹D$à’¼hë Õõ1Nûõðd¼W?¨?Š€PŽ»ÉIr(8_%/ÂVA`àæS6þ'VNæ7=FNèÇácã=¯ÂÂ_‘ “Üijs]Ê:_ p©;i0ÈÊõ*aDEin}‰ÌyXˆ©ÄúEÓÿŽ‹‡Ÿ &^:)æ6‹ÕÔ¸KÝf£Û—ã<œËõÙ€ð €(°kÐ&î¡Û²]¬’a»ª$I I"š`êó-8ßpȘ´èH9Xû‡êq ÎÁ¡P£o÷A©ÏÅ"ý@Ø œ`“K‚GãYDÁ‡Þ¤( ébXŸ°™¶9×ðIòˆ`U¼ Z©ß~¬eèÈ×^ÚßqÓœ \â•´¼]-ù =Ï ¾¶Þ¶>JÁê“|>_R ¤øR’Š ÈW”Ôú‰½|½’lã`òq£¡4(e¦> £W¨dyòâ[$Ö·WUÅèOïþ‹~7š1kÉûdÖC¿ê¾¤Çô¥`Ÿìœ†Oîþäe@xøÒIá›aÏÏ6—ºR{Ù²MŠ®Œ¼œ¼ÍU“ºFªÎòºün°û½v/¸#Q5œeo¾m¢Ñ}ûï}Œ °Àñ(Æ,Ô1J-é@Mòvi•Šf ’"˜ÝLÚ1nm(0PrŠ[™Îf„ÍZ[åö˜£ó/kä'}4nŽã¶Ã‡õ§šfÕMËèöÆ›õïôÑŽýá9Æ„½?]ã)‚ZØ&ÛW:V¸·ÉµòTYªÃ4ÐçÕ"`3‚éIû¹f¦øšM?ÅÁyÑÃA7´†ž´#tOÓ×b²óôWôÏ+_x÷Ñ[[¿þ±[vÏë§¿1[ôÎÿ¤ŸË/ÀÜ^½®¸ã¡ì PɆ—*ž¤Aq<â‚m^i[²y¥e…}[Êšäêt³ðÓtWÀ¥s~=e?WoŒ®‘3í§ £­ Lh;™´Ài‚‰ ¼äôöW6e Þ1aËmûíÝW9~ýý7ý¸þV`Ï©o’¯îmþ#ê 9Ùûvççëçü¨ŸÀ‡q:ÞŽÏG9açH%è÷‰~B‰à§”¤‹ šT(EíÇ–öù†«®Z™"Ž‹›d3híEÅq®˜c{X˜Ò´K˜d6勇\„]@a1ƒÓa© ½ãm’Ì™Û@Ú†šÆàáê¼$j¦i[Û€'C 3iÀ–KËc\Àd‘Á |2Ø1¸béÜPH‹%Ø4ÁÑ4f:¸ø%ïÍš?ÖÌÛo×=ôÐ6 <úPõ3L~ÁÀ÷Ù3?M;zâÄÑc'‘5wÌ™SQ1§¼¢2kkåžwÞÞ[¹5«íž'¾8yò‹'öà°Q¥¥£FM(3ØÔ¬ õI ÔËþm¬ÄÂ6F‰Ûìk¼ÕéÉérÀ僔€Å@}}cã©Ä<ê[˜¹8A…ÅV†tfõ$ÐÍÉC7æùáØ-7oÛïè±zôiý,AÓ0®?¢ïš¾+§”•Ma[ ÝÙ Ýhúü˜¢ß¡Wëë£"äܽ÷þå¾ûþrï½@à4$”k¸ä¸•lÂID"€‹-á·rAšPrF;Óõ§ù&”ê‹õÍL‰¸G?ntÂW–éq—²E2´ë ÆâÒ¥=2ˆ¼‘á¯YÍ6÷÷Sµ¬Ãu+X ÍxºŠG…‚‘¸]äÖˆH_‘êX‡ ÈÜÔ1td³5TâÁ"6v¡õt‘Ðõ:}ç<Ø€ÀÌÒ£ÍS´Àód§ô¼ ˆ(€b˜0¬ŸC †î÷íˆHX7l£GÏ;{íP$‘…W°N–ˆ2ïâòP\EÈÇbô@ߦ^Ðóôü ؘr"ŒîCƒ>ñÌP@$‚ä·Ácaû“¶æõ®ªð£QIT1à•xm aL1ÖåÌÈO˜÷†Z3ÄÄY¦×¸㫽«ÕS§{ZÑ#2“·õ§»Ïˆ—•¿iwÙ‚wÇG4>Þ£mø[þÍ•;µÛ®×à»c§¶mþ¹oâ3£J®ïŸó†±ÊP_#—‹ëÀaV¼“ÙFÛç XhKò«Aúš­0O}ÍïÉ{ÍöjqÚkžWo\6¨°ý€€”dn€¶ÎpV M_g›¶Yí»âfÓa ýv½Ë;Œ…P3.ÕŸ³3 ]àh^Çð}¶âVäÜÑlý&Ö°™i|ʆÙìó œi|’š’–™0¨;¤1PðÕ/cŸTÚmúèQ3fŽ9W‡ïµí³ÿ¼8êþðÇ‹ïÔy‚~róüC£ŸúÛÌ)“‘._Ò8jæbýHõëúkK—>ðÐ’%8¸îKœ³°ÿ@}—þY& .||ÙÝ ªªôѽÿþô¾·©¿ë½g'¿Ô÷îûºvž¨¿÷Ê ýâä‰SÇÙpëÔ{/ƾ{˜å»xÑC/®Ÿxz‰þ?ýФL&qev¤Çã’D9$I¢R4‘Q?&!"Û”¨R5A éšÛ¡J"šªÈ_¨e6Wm„â¸úf_§—„”V )-óqÔê ·ƒ–&i¹Úp­L«„J¬T*ÕùÚ#ÚZmkY;¡ÙJ²1§+mÕ¨¹¯ÐKì­ôQGÒQÂpq„4NÊÄ©R©y>,Ä»… q¡2_}Xx@|@yX]%¬W(«ÕÊëêð6¾M>ßT>TÀ§ø)9"V>Ws ‹ÆÐhTèÕ´y‚¾˜dá$K_Ü´W@»þ£xøB6I'7…Ýâà p 8 CâéNxݶWz=P¥¾ž$La³MM 2õ†Í^]öúiÌÞȸˆ[ƒ|­r…“X+É‹«bOÉM’"à8l6Ÿ®–ƒ„u•…Ž„B¡Î1ãÇ9üßùó+þKú,zH?¦ÚtéŽÅè+£Ë‡ x“^ß4oâ¤[oÕÚ›Uÿ9$Þ}pö*@(cb,~è˜í*x©…ÙzÛ©}»º†V\9f²v¶@l±âõzûÛ‰1çÕå' ŽKgìP -‹aΜı3¾^¬?ªÀ:¬XüõŒ™ÍûWCÿæ}4óæâޏ§`®ïX¬з‡þÛ·ßè¿õè „ÃT*1`êƒvqìW«p¿W!^ Ä{;ðª4±°`B§ryµCüÈàåhfRdàñ%V›ƒéúHÃï¿múWâPxçô²²éwé5¬ÍjoûþÄßbê­ó§è¿=ÿ‚þë”ù·òa_±a˜ /îöK{É~¨Rök(ª ™@Íëë[P—·Ónb™ka„ߢžxÛ-Øšn'S›V‘MЇ™2ÿžm[õãͯø€½B…ì¸ãò+D4ú׌þy÷ͽ›†˜æš½_Û·^ÙÒsÓƒÍx½`÷þñtÀÛ‚ÚÀëÉ—‘›úóÍ…ëÊ‘²Ãì]̈ç*±ÞX«±–@³éÚ dx–„< Wã™Ëâ…'ô÷ÿf {7þe:,þuÑ׿ÿx¼ËÔ¬3ddy¯^§ãsá½{é—~8«ë6;¦ I€BüÅ€öõqÙ¯ì J“ D/ÛÐÿ]Îwaàˆ{¸gÈ&Ú$›lS†XJä×Z´k`÷ŽÜ[Úô.‡PÓ?I ãÜŒØ IWc­{Ý«ðá/aëâþ5¶¡ýkìCǰ%@¼ã¨ÄBwÚŸ8D¤F÷»y—\^ªÒËŒnLpCÜe" ïß„*³¨Hè•Ø¸ÿ Ëú†æe“AÁ¼oKœQŽÑ7W ów|¬8¨Ë] Ù›v³G¦>ð—ÄÀ¥ÛŒ%ï=̺ ¥ð†*‰(H Ÿ„)A6Ƈ)k¦•ãý¢W²›»ÐÎòÚ_CGŠãär:•ËJiš\j®¤wÉ•ò\óýÂÒVÙ—I Hg¥;é¯ %£Ä‘Ê¥””‰³•¹ä.&R+•GÄ*åEÅ5Î6ÆTŒaÖÙ¸¯)ŸúõpÓvÞ'H¬©kã92 ©®C‰à‹›8¥ è¥@%&^8”Oµ˜u¼'ƒW.já™Ë3ü+^H;ÈŠL: Qø%ª¦bMSÓ5™PP,L¢ªP‚¢&å Z>0²oLˆÚ@ê[œ—ÕÒ²t°K‡2•Ø›!9¢DT–Mº‘>d0 ÐFQò(m)—˵ä å õR#×h V/ú©ÝÚÓi',¢ƒ1N{+ÕQêë4œJ§(3ÔRëýÊ#êÓJÒ8`ÐSëùT~ß3Ñ4¿ÓŸÒ·œ×·èËÅÃ_ÑÈ…l¡GãQš~qw+.QaQ<$3( é ÉR:ŸûŠ(¡Lò9?!< oCbÒ%­¹‚1U™ûWÛk˜üRGìý¤á8JbšŠÓ¤RÓ.Ü)Yý$ _O å¾lîqy-—‘é²É@;rD£ÞN"8[¿¹é+cÀ 04.…¤²f÷PAÜ%mwÂvógu’šc+¦9žìkÜCqø}.»?×ßÍ/â8l/ÜsHrÇØÓ²š7߬yùÍ7_ÆiX­3JŸŠ«„#zcý…3 ( OŸ¬¯ÐWê“q ÎÀ™¸Èem­ òã^óvÙ´ªT—EFÅž#˜Æ?nƒ.ëÃeT d³6T®À4vYÏì&•Â|¶V8©óÀÌá¹wß=—Qþ÷gšš.{õ ³'OžÅa¡6`aƒ LŒ,Þí`ß.¯jk²UC‹ËŸ£»r„ìäÅæ(¹,‹_§˜!ä7ÛC™n¾ÅRf.µÜi¾“ =‹leeå™4Ã4Ýr—YaS/ÌͽÖ,½Í°\Ö—ée¸ §2°¬øì}ÌÑŸÔOÖ¼¹·Ž5ˆÕ8‹ƒ‹öÉFý©±z$0Àþx)×fý †~N†ãÁIö‡¼>æóù¼é~¯æ‚íª´Ý\åÓ¼®$jOöK X˜(÷ÙUÙk¢¡„„b³ô•4³%‡2‹´bÌ–@@R2çÉH0)è‚Á@rOoOOOïpÏpïðÏoiØvÙPò“˜ãòRÛçŠQ¦¦È‰Ó§/Ø W’˜‰®eO ^ÿX/ÛY|ÛxÚmôÔ²‘ú=ú/ML¿óéS{sœ•÷è#qÞÜ›a9Ó£9 k™ð|¼-¤ î$‡f G¾%xÞJÚî¶§¯qT· k¦HP† ßê–ý)mìÇX$¤ËØ_‹ÝtÚ~ú²¹—:7 KB%á’HI´_¸_¤_t¤6.46<>2>::6#¹1¢D•ÞÐûÐ>BO±Ÿ4GÒeŠ£EM`LÃ{ñ1|ïmúL/bʱVÄ-è  ,çêgÇûI!¦E6b¦µ& ]íÀnê@(Ñ:¨¥„„(1ƒ™Oœ®‰„J\k*f“¦*’Ë6É`á¨S‡rôé_câ;óæ@‹6-øceúGÓ>@5›¸[´‹v-]‹²Ö‰iÖNb7±PËc-n´ñd¼x«öyY«a- ésUM’ÙK’¨Oð‹Å­Ls&iC3…6"[šs­˜9P ˆyRžœ§¨íÙü»YûÒ^B±§ÚÏÄ×v£Éh:L&Þ,Ý,߬ŒV‡™Æ™Ë¡ËÉ:M˜&OQ¦©3µ9¦éæróôNå.õÓ]æ‡äû•‡Í¯’]t§ðšø²²×Ü¿-ìcìºâmHpÛÎÑ×é=ØÒú¼Þƒ!ê-¡+ߘ^,¿ø$ dHÄ0Œ÷Bœ„B„«m˜Ô¢ÿÒT¦Ù•,KŒ) Bº*Q4ù #Ù„²hna3G“ã ¯Õ«o±{ZeàåZü\…§‘ µQI5Bíj.ÍP£jZ¨N CÔet©ºÖ¨•H‚¤¨Z¸·Ô²-ˆ…4)]ÉfH-Š”NÚ˘!•[^'¯¯(»´ƒ‡Xˇ6I/×72·ÝlvdÅ¿boì…O‘ Mê:!ÈQ=‚_^jOgE¼ÍÕüH‰ 'eÅàG‰ÈB'Ñ dµ…€-(6(r³rþ_ãÐtFYy$N†RÆ­š¢àÁMǶ´;Ñu÷l?‚6LùäÇç½3lÞ·ó1-øÛ€~=>9;ëÁ¦{6—û`ÃÛ¯%Ü®:’C?mÌϨÅÉÐ1”Ö›?²ÁzOµíýàÆ¤ü´»3ždV-»aSœO¬#O³Ï«. sÿT«P5âV;°gr¢¦0oÁ‚y,•fa¿ºŠƒ¨é¿¬¨ë§¯Æ²o¶¬[·å…µk_ ‡'ŽÓ_Õ›X{uÜÄ l8h ªˆ¡ÊÅñÀC‚õ€å¶Þ!¬÷1 äîpw¾Úsvþœýg6ª ÁÊ im©”V>4¡¨ïòÁ+Ÿ~åЧãCÿ>‚¹¶âpÌù¢ÐU?V÷ҳϾT¯D°=¬Gx”{ØÐì HùÖÃGVÇzñ#¥Úú>n¤n,$ènêÌ!Åi… ¸m°Ÿº).ÂØäCaÞmÚt›y¦@¿ÚŠƒú/¨œ¿c‡Üí ÐÝdäï &žHYë;®ñ=;¾%¨ZÔ¨ÜdÚxþƒêå¸ÖŒŸ%­u¾o®Nzˆâ±@b±uN6ÆWß*¤ÆwŽ»CÝB|žX¯†YÄ݉Zã?,k»íìâJfJ~¤¿„ý1ìª?qgé´¿ØIaÙ’%7öÐòò±}èÄNú›ËËWÌIèeZÅ@è‚›âÉvÍÊz ×ÂF+‹— ²*Z,=m&·ä¿lÕš¸RéÒÀ2?ê ö+àQ¥#...•—*„ š /„ [—yxxð€V½3  èÿÒW×ÖøTrŸ)î1è4®£¥ÈÄÍK«z/Ñ*,`8mýãÉ6 žÞCþµÎcõ}ËŽÔõÎÏá#ša‹5î‰v—:gµH>”SÇFhž¯F¢Û–¶å,ÑÏ3"ÌHﯯ–Ü/ã3LŽnÀOôÀVÈ¿  vKn‡ŒX*ΗŠdŒîx¿7ÂV¹ŽÉ”t˜Ï zœª CT’“lG£¥Ò÷Ã;’¿ºfÅ"õq[œ}"AKÈž‘ðÅ2– q–mìÂíŒæ|Ö.]Nñ4N¦AYóqƵE2ó2‡dÎÍ\šùDæK™ò84¨Þåø&;6 jô÷…]=þ1÷÷õjÄ^ƒÊʉ^¿yê\v:­û‹Sç×ÒÍÓfŸ;Ù4œô±$emÓç¤Ï®™/<ÛtD(Ý4¡tn £°É]Ë(;þ˜QNýŸ3Šçÿ„Qžy2Á(Æ >1¤v¦°ÐpÞåÄ]Òz'¬7¿ÏwÝmhwOçkœw;‹]Ýü½ù²»uîCk¿yãÎÅ‹ï¬X´¨cØ“­¾Oè_è¯coºðÅõë_ä‚þ®ÞÀÚ»Øݬuä «Ç ÅÖ9º¢ØÞ·VãtGˆ)µ¸¡ÞZŒcP§®Õmé Z©W«q'‡H+ `O®eúmCS¤mj¥ÿi17 šUÛX}¸!žM‰±5›'I YþÁ/lÕ!Ã8‰33¥•ÚMŒí* ¥5È\­Ô/Í1²‹éØÛÙX\Ö½ Q\MïlQ³MÙ—µoo&…ÿ%AF´ÎÁL¯ÃªJ2Ðý¦§¬ï«;dM²€bwr†pq†PŒlµCeð8PsÌûZ òцÜq¹¬àÀê¹c±³mšëõ¼ü·¦F¡ôµ9S¨ȳN„‰ìµ™p,·˜‰ÕÔ! ‹’¬¨¢ uˆDÂé ša5¹x%­ukÓß¿âG»%x3s¤ IØÆÈ)n8uµ'ígnG9ÿ(…˜'›¸¿p¼ªªšj2™MÕ&¦ÌKÀšd»Ni§¶ÓÚ™Ú™ÛY²¢%Jgµ³ÖÙÔÉÜÉÒ_í§õ3õ3÷1¼ª»”]ê.m—i—y—%Ý*Ye«bU­šÅTlé–5!KW9Úïµ o¹Ø’,øæ}:¡lRÿ[»¡k¯þ›~¡üìâ™_ÍŸ>£ïìn?ì?ß8ésfÕý˜—WX”ÝΤ¦®{qG]j*ÚÛ·ïT’—kQÂþV»- AºI|\0;°ŠŠ®wàe=hŠI%ª»Ó:ÔÝ*ÜÄ<|ç4Œ«a¤Ô_1Rêx)„ðQ’ºA7Ôf·9†!tˆ§””ÒÄbíK£ýÈš_‚ª1Npa2 †Ù4Kèí±„–yJ/è…LS ¥Ê]Òƒø}P|HZ +q]%¬WK[èN|¦]ñF° }èÅ©zoýN¡´ñ•.®„>rˆMÞŒ¹ÆGØ>á#lÏ}„í¹°ýŸúOü¡Púÿ{ðks‘"1Oëm8'h³´JÍ€žÈšÆ (~¬}¤1ø‰?h>/qS·àí’uãTÝš×”i$[Ì’Ú*Ùjš)9‹ Û“Îb'©“\¬t4w³ö&†KgŽ˜GÑáÂpeŒ:Ô4Ê<ÁZFJ…‰b©T*—*“µRS™+ÌçJså¹Ê|m®‰a@ºe’<`zÔ¼Ìú”òŒù ëfòÝ,¼ þMyAÝlÚjføX†‰ù¬§ï ïI‡Éú™ð¹xZùNýÚô½yŒ« òÆLëƒcwïÁl¶Õè öìÖ0¼5R¡I ×QÒ¨7¯ÆðgÂñ^æ&líJ¼â2lñ xB“#e4]í3tòûÊr¦þÐÇáÿu~Ô¦ÚiDÒ\µvQo¤Ô›éXu"¥Î£w«÷Ð¥´J]¦> OÐ'èÓÒu­ºŽ¾¤ÖPÞö©¼í§ûéAõ úýˆžPO¨_Ò/éêê¯ð#ýUº¤PbÌ Õ\$YHV\ZÄÂ.d+iC´ÐYi¯åYzþB¥‡Vny–’eÂÃÒ2e©ö4¬ «…UÒje¥ö¼TCö ûNW…ƒÊ~í3øˆœþ#P>Ò¾/É·ÒÊ—Úïð‹ÔÉð]r¤©ÖðŸú,\xú[\Ⱦ·è÷7þ®ßOº’T½4hz'êÏ‚Ÿ-ßœ m6ì¿Ñc2³…Zyùµ"È6Îm¶t«Fì"hk•Ï)ì°Û¬&UáòHÀžÀÙUè2øÍO‰÷øü)˜¢T Lmº"®¸{s ™O`ë)CdÕµY‡šEV’€*EF`²¨xÑ#z$œ&¦Ii2g±Ž´ƒÔA.±–ØzBOìGû±FOi*™"=@Xú•ôå!ëÓd%[R¬²¬²n![éVáë ¶Wp7}M¨Qk´}¦×-¯[ß%X>°¾gû”œ&%—Åš‹¢nèÇ2ÌA`ò>«÷žgÎs‹±E“B~»Øá+ÎtêI8Z…^ ê*<>VÃۚΔåº^T8 \åh=Pð§ŽÖ£Èpy:)“;äûäå²ԦؑÁŽÚ»h7e0l®‰{`û)L+Ï)/Ó= £a…ѯr’6(þqÐÊ[A?nª%ƒ½dPÓBé…¦Õ—à)DÐk…ZÉ i0ž•Ú<&)] Ø=!“QPëM_A=~é©wmLg±`UÓ¼.↨ߡÙÄt–šy€©wêOäfr}j¨V#,Æé¢%×,Oô0õÙ[òòÙQ‡ÎÍ #nʾ¼…Bí‘Ïßõx×®Ïz÷ó#=«†žS>jXÕì¯jÞØ¿²âÄü•¾ñÒW#ßüìãþàãk6/ˆ‚Âo¤Ppì4|žD`ɬ ǪQ’ôÍI)ô(pÕlKã‘ô ×lS‚&ß&ˆQ Áz?›mê—¶zÇÆôd@ñØ0Õ)@ âqØÁ$Ó-hìrÀ0 é]=_>]Ö ;V4&i”l]žµ—Oòʬ³¡×cÃF——öX¯Ö3Ÿ=bÙæ5ý?»ùñ_½ôƇ+矨X¹ÿžŠ‹µâQz3„ 5Îh"h—ð|Ðü¼+ ¶°Q‘æ¬Á¾ß;z¸Ý’™Áš±z,â6µÏË xtƎҙˬŠl{füÈmgÖ²3›Ìφ¿@o>?¤g'‰R©ëÀ[.ö‚ýQò`s¶sJÜAŒ:Zy‡F‰"•´Ç˜OøãRZ‡+ærp£#…H ù”õÐ7èã›ÞnÃÉFšívýŸÂÃb5H #âí’ÜÉn“Ìüv&Çû!ËzwÄ´>~ß]íMs›ä¨Ç,Ûå°h÷`šý«±`”˜pH'Òº4Ô7;@/'•¡Q-çã†t6²55÷çes*IG{HwºÂHö8p~ÐoÙ ¶QO—Üò×NÚý‚þ Z‘lÀÞËéèë–ÏšýT[Z¢Ë+hšŽ¹úÑpT?…Ñ6ØÉyLMe úІa1€ðø#!yeÅH†w2A+l󯱛·¹+XT“°¹<ŽdžIÍíe#O†/s Œ °±'¬ÿ‡Q/RTXdïÀÝ·G{=5ðŽÛ°‚Á5pqåÉ7ßÔÑ5öq—þßÌŒÅ÷nýAßÅqØë‡­hû-úAWá ð¶QPOb~ fÚÂL„˜ôÐP ]¦4zƒ†~@L‚ -xØ™ “â¶¼d·+ôIN’§…ÍN“JQ ӹؽ(iI,ÖIy,)¡9&¹¥í», ¸`0ˆ4qË TCxøŒ›y(g&ê‹3‹}‰%`±ON”ðúäLÃ/nå#ÆÚƒ«&'‡Ã¯:˜7«k‡Y%«†’Cძ¶›Ñ­Ó¬Íc–V޳t šÙ•ü™ÝºÍj÷áêƒáP0™ÏéÐmzîÁUÿbç;+ÇŽ­¬;n W£ÜÕ%†$¨‹w·‡N×¥Nþe·™-fkÈb1§Û,&~õInõW§Én©"ªY´QsآɋËMœ-S¿‘ÄÞÐÈw†œL^±ÝØ…ßr8ÿ,A l¡×`º>#ÿ‰ëôR\÷f÷áûIP/Ä›NïÞ úüF¬n®"0#ˆ(QŒÙ1œ2µ¿cH]WBòž<)V7iä—Fà{ `½Ô•þÆø¡ºÁÌxÇ@ÜùB§ö&Gz¶D!þÖ'|›¿Æ7ueÒ‹ÛwŠÉŽ® mëî:¬íŠÇÚÄyI4{YÃ!Æ-ä“Hšç£à ö·ío3‘\r™ñ=ÍKg&ù¤dìyD¦¨yɽW­‹l›…E¢Z¼bÒs%ýìXþ½ö¶Šý:<앲y;KnT—æú`]ùmýo8ýÞÐ:}u×#âΓ™™ù¹76‰£þ¾¸bm^Öm}Ÿ|>:›•]ЦW?«2bÇ_¦­.,,ïÿĆPÓýÉ nœØÝ7¸ý3€ÀÛbgÚYÊ7 =–§·üO€Ñ) .é.ÿª´Z FµËˆ *…»XZâ‚fdç‰ëÍqAW¡'5moŸ9ƒž9#åÿòË/@1 ??äC_èocuhà,Ú~ð |]p6ãkÇ™ëå2÷u#-0©ã”èH÷´ý8S3wC ·lÎéçôúkq`DŸ<­ 2™fJ;e1@ΰ‘YÐ’P}uUƒ˜]R^r[å’ÛãóÚÝpïõŸèú'=—ž[ðÑþ7üã ÿe<¸ åÙ³gÍš=[ÿ½ºZÿ=qŒ²PöYzêÒÛç-IK=ìõ}ÿæ[ßy^eÿpðîG >Œ-¸ûàˆ‘ÃWÏaÿyauµ~qöì9ìÅêÕ(±c ðVÐï)S°B¬ÛÓ2£M¥vÍÈk¨Ô‚¼ÁŠ2ØQ ŠàT¤Eâ¾Ì5m!5 m!J¦²cž-Už—Ô–Á²ÞH!m®IhÓSö³çNýò/ƒê[òâ¡nB7±3s³wVºYºY;û:'uöw t vKî²rc0vU5µ¨´¯ÞšÓø ~rÿ´îFݎ柋ÿä¦ÿ÷áQÃFl<ö…þ_þ‡t}—Ô·¿¾°Óí#¼Cõxå€>̯ºÏݱsjj]r2!= 3„LÀE;Tbþ.зY‰YTA“˜),­Œ »°,ëË“L¤Ž¢Çb%¾È£úQL×]ô€~Œ -—ùÓñã?ám»p^óaE˜eTeäújÂó$êDì‡æ`½‘ë›êàÕMg.~G«šþŠ÷üß«l.³†¢ÐÀÄxÇœ¶Ù™syIçbgàë¢³æ¯ ²ÏÜÙö:š–—“Âþì4g,œïŸZ.K›äÙyŠ4²pRï f1ÂvŽËš±ÙçÃY‡—AzÿO˜ƒ¦zZÿÎhLe× {ÿ„sø.©MM4x`vÍöé¥Y½Cå§gft ™cþŸ0XøG¬¢9XÔ¾}Á#w.>×áÒéøáA7èkh,þSB&éFÒÎF0¦ç+®@þÐC´]It0šÂfý£DNåo“;š‘Ü»@-Lˆ:˜ d®}ô |m«#£èÈ`Ø(zk©}»ú—3Z´D è3yRß>“§ôî~w^n~~NÞ¢UûÞÇNîÓgâ¤Þ}&o(ÌÍ»«Ç rò ÷UU½¤Uý¾ Úœğeú<ü¬¢ùy«…ÌÀiVfi’ÝÐ-1@v̯ªæGvx¥¢Ÿ}µy‚þ\ƒþNlÀ‰¿ç®ÆÙd µ¥ããâNHŽóºq›Ó$&Yá5ŒÖ«i¦ÁpÜæ\ãOÔ?äA£„¼þPKÙ8§ûÆz#TxÅ*y;¼Å÷F¥ðááõW‹ )\ƒ·7MÀ’®íûv¾þñ¡#6ö®®÷ÚcË*ÔŠ†´)¸>'wW0xOyŸÃa4©¢N£|DÜMMV-x\šå5L©•"É»Ù8]˜ «+uMKEøCö‡¨;¢f±ñÖjUÎ†ÌÆÌµþ¿.kük|ËÒóÚ‘“!cËÞy獵zt_Úe´™·÷:qô !íöºáéÏ¿8ªß´°>T27UÌ¿í¶ùäÑ&Wù‚ÏÛÿ–Áƒsót' Üs©³0Y< Ȉ;ª$bÑ$ãx*ç¢[hõ†\ç?ŒÁV䌬f$“±DÙ»gï/Pÿ^&É¿ÿNùÖîý>&)¡'ä2W~D‚÷ʯHЇy­,Jz9ù\P7kœš\*Ø©êfÒÿ€AÕ¹‡p—* nnÖ¨ˆµPK.¶PK&Ý©xae^Y{,¸%Öé†ìœëgäNc±TÛmíÚnî ®;×ʰ£ Å 2â5‹«MÊýÒîGV¢xªÞ¨v>o®†Ñï*tÈEÅ…ŽBO¬êàW§ÿ{ ©’–µýºü¦ÑÏ… ¬8_›Œ4þ’4°¢fÁ‹$ÀŽ$n…5Éf—jñWPW…šlÀ½EŸ6œ7Ä[«ÿ¡Üh‰ì~ø×_>ýô×_ß¡Åïáš¡cÆ ½eÌñÇ/öî=vlïÞ/¾K?^2þ¢Eóç/o0õ^uY½›E¶VfPPh²•É·F&ÖœW«w•kõb쑬óº Óõ£z Ïè¦á ™zÕ.}×î-—.ÁäK'å-âöLy½pwÓdö}] `—`ÁwÐ|ÿ´q?q=n>cϵܟö§ÿ?Ï ¼BYŸ#D„R£By`¼ms…2åÉð«NfEÊŸGw¤$Š”½F²M%Þ¶Ò=Ĺ!4¯„ ÏÚÏŸs6—)·”$_I¾JIk)]Nç•-ßàôfÇ;¶geÊ{¦ÜùÁØó—eʉzsR–Ó’É ÇæöÒ£»þ+SÞ;陑ºÈy '³¹ äòLg¤Øeû °â®ÿ£ëÙþ¥˜„ ½lÓì]ò/ߟwùþìþB°×I*gÇÅ[3%‚ã,ÛøÓ.ç òâ#!6$V£¹˜Kri®›œÊ çFã`2˜' ŽLÀ d LHžšžY‹kÉZºVX›¼6´6¼6²÷‘}tŸÀ™t_x_Ä(oâŽÖ?©ñæ7È3g6*½Á8 Ü5mêÔi ŒJï™Û· µú>½Qßùÿ_ïMf£½ø‹<›-×@ëb‘V£”Ñf^³<ô‡×ÿÉõ/¯º>;qÝãS±‚Éq[aЇËñxÊàBF%×ö`Èñ2ödZ-äOú×_ / . Ï V†‡'„£ÁÜ0ma3“åˆYbpLOÖ‚ ˆÔ¤ò¾, Šd6Â(Íy*‘UÌT±XE±N„µ[t–½·kÖ …ì|Ȭ1vÆm¬ 9—vJ+¥¨à‚lø6~}(°d_çÒTh4`ÉrÓ¶áÝžaÙX“Q}]öumÝ>{4œj<"eùì!)"å¤Ûs|Ù×Éeû?)ék­'ì‰òôývvóò]#¼Ÿ 9>ðköœ¶ÿ?æþ>Êâ‰Çwžv%¹\¿K»$—ÒH„r-ô:„ªô*½(¨ 6ZˆB‘&*"bCTTTTPQ±"–r›ÿì>Ï].1~>¿ö}½þ—äööÙm³³³3ïq¦yR½™Í[; <ùÞÖM:4ïêâ,‹-õIÒd²s|Ô„ØñÞÉ)“Ó£"#ÝÉiž&Mò=­šôt{º$to2Ô1Ò9 ¶ÌS–Pê-K–>Ù0Þ>Ù1Õ9:¶"a¬wJúÃVÃã^CNH.¡Ú4‹îïr3¾m!Þ ®\5®Õ¥ Ã[®pèŒÙÒ+weÿƒg""è÷=z07ÌÊ¿*^ùz¯æËºµ¶âÜL˜Ý¾Ý-;ÏÍ~°Mkt¾ôÍlA¿ws3óËìÙëÕÙ¯Ž­­d®­}ú¼1³¶Vu“ãs%_›i0·‘ô+¥siÃÇ¥ —ŒÒF#ß_æß'5D:É;œOEÞçÜ9ù,²Ü9=¥H#›µª£²Êº¤7ñI“íh§FÌiDvˆn8q¢õjdÄb: ‹ëÁiWˆ­¹Á„,F]ì,âŠûJÒÁS\8Ó°Fp5kà2ü$qñýÄ…/m?iFÜ€)‰”YB–˜%õná$í€Âq‡t h~ 98á—9ªŸ¯`4B;޹tôåN¾ PŠ®Š5þ¾÷ ÇéˆÛÆŽŸF€{,LT¸j'ûZ¶HO4éä¯B¤ ÷öȘ펌íâ.Ç–ÌD·7RLO1EÄ)ÆÑ)-ÒG+£âLÖ&bÉæŽÛ8ǯ°”+¢xÛ)âԼϬޘ!¯‰¸IdfNŸœ9Ûs©}r:¤vÈQ¸´‹YõÍb%íÉy_Q«öù–ø8¢{7Ÿ\ŠO;Ÿz1É´=ÿíæMmçÛ¿Q(nÿ02Æy.«é–:‹Q!Í#ò-ÆTbŠ+°˜Œ&[na†cÀõ§\ÉÎoA‚±g¿ªi‹ƒzökÚö7 ÕªêŠ{··µ·wðõ²õ²÷ñ´´òM³M³O÷-ó­·­·?l{Ø~ŸïIÛ“ö“¶“vWN»¬öEí|íû·ëÛ~L»òö«Ú-oÿ@»ûÚïn·£ýávÛsçÍ¢Nà êÕG—³†ºO 6ò¾;ʆùå–[üWUçÿÈùŸÞôQ×^ço´«9±m=ó¯¨]z„þÙmüg÷œù¢Î­GØË|)kN=BXO¯>½úÐ3ô ßÅ”qSƽ¼GóóacF§3O³ÎªDFž Œ¥lä³£X›M5š>§ñtáÎzéÙjº•ÍLgÙŒØ çZïs3töè//ÝœëâòÌæô”ô´ÔL×'ʼnæ$±:Æ€>S1I»š˜ÓRE[x‚7IŒLOIŒ—â”ð"›RÉF§ukW¿ÜKᬊn‡¯:0wkþ3-n_Ç™öŽ`ÚÈ: ““¤ SãÏ&š’Ø—_á…̦ñù‰šöi:]š.OWDC?Ý5Ý­žð &/S–é÷ɵ̽,rYÔ²hf:Ã7Öûh”øT“ò–|M“ÓRÒ¼î·,n)˜Úr³æ.6íš;9haÞô-z'Ÿ¢÷¤@î (Ÿ ’èËÂ711w,|hϾêæÃᡵ´ö¡€Ýù7`0E|2Åèssf­(WÇ^¶q~ԇϬ*òkpf¥ñ!ï«ÍÐMŸÓH:Ÿ"äBpÛ çÛ®ö}ëà÷Uäç{†ìD<º×åÈä 9âbX"âÜ.Cp¸üº‹N ÜD!¸}V‚FoúÇ ·Ë°H»DÂ-ªŠ›CåÖ0-XVà€%£VJxré›ìM#™žî¡oHƒé+®3oÈÚréEMÜ{Ö×>Þ­ÊzŽ&¦„h.왪SÅêès©[22š¹ã’,(ñ©Òž'ÉÒÄ­Ä[Š’J”"w¡&îYÊ{Ü&8 ð…Ú&0qÏ׆ä QÄËó¶B¯;)6ötvõtövk2ÌYæ)õLŸD¦8+<ã½ãÓ]yž6Mºxz4è™>É8Þs[º±ÊP´MÕøoâ›8ØÓ3sþ®[|Ÿ¬¹‹u:a§ÎÐ'éùàm당KÃÔc½š!vŽÙWݤcÌ=qqûª3:yü_õ~móghÖì‘ñûž[ÊæZóæÛFâ˜r3i>+ÊTæÃf?íE#gi˜O'ÌÐ0O§‹œãÄ%X8Ç fÊ–>‘gÒµ$xuNqE^ÌP­ø*B~Š#àwÁ].¸óÅý9øTÜÕ¸ßÅõø6¤ðõÿÀïBwÊ¿§QÇ‹ ÑÙA¢'2¢ƒÚ¯>¨kœ‘PhW˜ %W£ êPr5-fˆ™²KÁ"29À'š ŒÈ ŠÑìIrQðÉ"uúQ¬ÝnI¶€Z‹v艋6,ÓÏ ñz ‘X±ºéd11¤èvìIòqíŸM§3¸ ̰ ï>‰­mèÌÒ¡Š§ñtÛÆ¥ìdM®Ç³¡VÑ\>”µ¡|/4¿% %ý 0ËIŠKeJ–'¹ÂËïÜ ÿ—ÿÈŸ§80ÿ1é35£ô\ ¡§¤ŽaFxãùS´üaÎ+ëšËIçsï 9æócz7^þr¥Aù_6R¾Úø¡åÓ|½‡éïË£H|ƒüñ˜þ¢Ôæç'þ!0{¿ çâRB¸N§5¾öG&¨™Ä ž©;fJ=)(n¶ï£;2“Ýxüä’£ÏpŸû¾Èåîå‘Òõ¼­‹Àó¶¸I[_œ" »„Õy:œìEuánÉem&¸,$Òr™Ynž¿Èjx{5—ñoÇÄë ®þB«G“ ›Š´·nе·nû,¿ÿüóÏŸ¹µ‡!îIƒÏœ9Á ÌöìÇÕ”Ä3ïF— ˜?‰¾à9çÐ\Ý6g”Éä°$h¤œ¯ó¾Œdpÿ NŽœ¤ _šWðþgÇ; ¦ÐúKßœ™2å E'ÿ8 „—t·aW*.æ]Ý”o˜)1Æd]F@ûú¼ÒG²ªv_/¿ð QF~< ¨^ùŠ®] …ZmE‡®âdÌY©®b,K,‹çäe5ÃçÕU[³žD¨9̘ã)uý†ÒÌÁ¤ð#.ÈTaÛÄÆK`b-¦žQ,õ4Kí¡ÍÇúÚé/Åå0KEG°´€Ó®T®:í~ö¿vUgiNF…œÏøÉþÆÂµ¦øD«–ëÙœ¤bé[T¤§pŠçè¡æˆ²¨98ÜâïêJк#C[.úàráÙ—Ôµ¯öüÙ §fƒQä µ‚´Åh¦uËæWÍÒ¥/xz !kl¶‘Ü€]<ò¯îÖ9ÿÿàn-•ÓñuîÖAF;;ïŠøÌÃÎk®ªGf¾¤œøH±%pÞ>Ѳ®çY{aÖ$Ÿa9™Á³J#4¶£¹ øä d;ø¬Æ®e@¼ !mµzŒÄ‡ÀÍ÷iyî§„Lµ›ÅLÞͽóO}Öò‰¢ÔN·ñgµ8B$°t‰Ö>iU r£"°ŽÝvòž©DÞ‚`Ad¾°’¯WÐg|ŽÍ‡ÇÈ1‹ZÔîÆü×¹ùú#˜ŒaÆpô 3Š’Èüt˜ùªÎ%î*È@Z˜xX ×Éà2ÕW€Åþh¡Q8 IhR 9)KxRÆA+p«ªF]ZÊ«°‚n ³ÙÝÕC uŠåϽH¦ ½"„O>õ2Ò†»«Ï!–úš‡é1z% ¡ ò8e»X C8‘À(ƒIGJÃù%M‹ÀÙ‰­Ñ†DIQ‡-&æ9=Wš#¯’D†HZޝ9Ò \.-71oPN¹ŽÓ]ð*L\1ýgºeÉô늃† ®Ü]óª°¢¬rWpWãÓG΀y6…ݧÒ·¡„7g‰Sû¢Îùm)6!mërc::ÂóyôOïºgoi®Êhm,½"xP_Cžàù'¢óò©ÐG0ã7аø[¾¤‘!]â0¹>£bt·ÍåãøJ®Á­Ï®;-q˜ñ¹ÞÁ¢hF”bÑ)H«Õ’Vßýßf•/­ÞÌa[—eÝÿðazÌ™þíý_þx?ýpÁÝt÷R!ò» ÿúœ?U~ÿË5óéYú'¥˜ûü'“,_Œn»òq´ãÏ﹘#Ñ©]8nŠ‘±¦KŠêöÏñR¹ÀÊcý8tJ’WEhp/ÑiTåä3'Ù6mê›ô›Ç*JÅ™œÜb†ÏÇA† ›vfü8Ü'¶p¢KÀ÷D'9ìƒÊ48€™óç.¤´£UßÔ²Á )» F¡ÀieG«€Ðiˆ¨ÿ‡¸š¦‰«/ò®/WùÓŠ«j}|ã“Ë9§ÏâOxô$‰=ÁÎ[}ô>‚ÛçY:J¸ ûQ´:,'dæ”ý»˜³ñ²%ðãa¦³±©%˜Ÿê_”?$z߉n»Ç`:m>Æ ©­ºÓv8 ’%ÜEŒ6©ytFÓ¦1«¸ÍqGÁÿLÁpN½ooï(/Ã9MR¯rÅ»íD¥¨j*²¾€xH˜4Tš¼é^aÊ §Å4>†ãaYÒá/¾ºö•t¸îÆ[€;`L„U‚¦îf( ~6Ç—“/º,á:rIo9o¾¾½Eµ=½Zÿ¡¸=%þ\”}KK…¸³ ÄdÖe› E±…-Y›jÐr7è\ÆÍÛØe?è ?;2ó:äõÁר¼éøZ–§/N„µtPÿ¬HšÝ›ÖRMAí‡í;`}–Ö’¥~Эÿû+UðH¼ó­~ý_].(º9V˜PµÐóƒ*ç~ƒJKˆýËõ£¯´ @Nh§Q¢ò*é9Ϋr¼JŠáU¼J!¯jAÚj¹ù~ÆÐBö35?/ó*ÌgÑ7Ú6tóªœ#­ÈrsÀÑz´´ä¥pP…ZJI]n. I`¥Aî ”ìîJêrãñ«97}h©)Ú¯¨¹étØ€é¥ÜújÁ˜²yîPºe§ ÏKÃI]éF,}2ë,÷$OÏãé1t:Ã-@ÊðÔ<•C†êfbéyÚ‘ïnÏ&aKÕl"u©Æ"™•U˜s §ã¹€xê@G¹=¯[ ×…‰fÜ– VZéDW8ieŒàæ¼çk𭂨ç°+î,»æ®¥â0i¸£þI—á›c¢èTCuð7Hðtð‚‡´ñyô.Ù-EW{ 戩Ú¡z‰˜Ü1Î(OiîµXûÿ¸;)óȼÌ*΋ÙÛáV¡ÒГá@Õá)°w|<U<äðá®Â½µ¤Wï˜XdNC.ör/CWÞÑòsŠÜ³Ç|Bˆãh ~Nñ¡ SfŒÅ#…èù™…üÊÇ¡Œ@)S÷A¡VJìÑM÷€L3óúº^š¦+{3Ú2Κ‚(7ÎÌy=3÷…PÉÏ(\yÆy(µüDÓµÓ÷ÙB%t&ùu¦ &´’\®ý•8}ÖåÑËSË£ËSûF÷ME÷‹T}b¾bâ6NL°ZÎýYµj³¸ØŠøR@´Îiƒ$lƒ–곘~\*ç$ðtA½Åâ଺Yl‰*a1–ÙŠëü%«ªóçÀ êQó:‡~¯]¨óç¥küù6ê÷þ½¦åÏ“¼«P!ÚÈEüe|šãds¸^=r8Êz¬¦S̽§CXÍ0B‚¹§sл°°~nM ‚-¯ŸŸ×_¨±·an ?g{ÃT¶WJB×,x°…m7î±Z:F‹Iê"ÈVæÃZÃV”*€5‚š×É¡ƒŠ ¶WJââ4ô´b<ˆ@lAÁdôžJòz’cCPã(‚‰^ÕÍD¬¶çQës¾š S< N;µ<õ¿# õÿŠ,X‰bt£Ïþ¾ ô>S±vöáZl‘fd¨¯)-’­<—Œ.Ò”EIcÑEšbt‘Äx‹Î•%6çbÿùóÅÉ9«Eiai‘Ù¢o‹ÿe„õ¯‡…õA1R‹82¯Ñˆ#}š&ØkÉØ‘?ü÷Ð#ÒÛ±Ö¶™½ö ìÇBh²ü]L™+î&¤âcéà‹«¨¸íô1¶õ1^»x8« ¶ÊþÇ0ýèGIöþwðGz©ì‡TÌä# œî²È:ÐkM´q»œ(”‹3æ·i“wûÔ¿â„ÐŽBLõTƒÒ<{ݬæt=½)V…;ü¯ÎüóÉs±Ü–Øú°õ¦‡MvÇóww²ÂmXž:pX,n¡¸ÇСsß¾ÿ½=wèP$}¿`[ôu»¶mÛ}Ý®}{ñÊîîößÜýhf‹1;Y]go¾‡¢¯´t`)=SZ/L¦/:ê<*ÌO‹UѦóá,§ [¢c,ÄD¢q¢‡”fÁÎdÙ¹°‚ÈÁ噄WA_`.¥%ôžzA_ä­è(ZI·ÑQ­ ¯{gÐó-è;wW)QîÑ#‰ˆ¾˜ê sÙN›Ã¢ ÅÔ¨ÄøbZ’„.4.«3¶¹“(I]ӂέÿœF´x43yTrý5nìTo®•;oÿk¨š¬ÌÄ1[ —ßh4d´qÈÝôÊ+ ‰[üô"¿A£!àÙÛÔã.Š%*8¨W%ÿ<’ŠÊ(GÐÄe‰ë²qPl„Š]ÐVhð¹{MåÀ¢ÿ«ÆCï,Å£Þ?oRÇQæ' AD£ã"ÿ{'Ái“<_?OÎyŒÁ“ò_bð„ò¡ú_ãñIãqyêøÑò0ÍŒ,CþY]n1Ühr‰.Ÿùqù”çì§ôÏ…­·<Ôƒñ÷¬Ìà…µ8 ›Â'¨tyÚƒ‰ú]Ò”š£K¦~ñÅ”¥oƒHσ ’nfÀ‹Àìþ!h«6•KEU[/DØ5´U‘l‡]¢K²š_l +ŒÇ@®"“úŸ «nkè$¹ÞÕRëÚÝ’ô $ÕÇ]y·þ…^¥mät<õE“&è¼d‹IqÄ[¤"W|‘¥'I)26åAÿØ&aãG:Ÿ)Æ‘^­Õ‘‰Õ¦sM™o ¾˜¶ýp‡ ávysò¸†![B§è0¦•7! yUÉnWÍíàÛûÝwëöB7ˆÇWÝ-}ù¦Ý'Dæ7-Z=sæê{Ç´x ẍ¤¤kÀŒ¯ÖôUúã…꜄>Ûé‰baš«Õ ‡>xdÓÆÍar½hL(.šdƒ‹„é„pî{†[Z6:[/(§u§ÄÓhU7â ¾¤«·>vû·b¼¤Y,x$A¾qd‰@&j±Jœ8 Ý}&C=Œ(p7×93´ácê3ŸÍ}ÚiÕKa:é@XU|Ìë–xbaUñÐèèêÃ6ÖKBfB‡‰yU¤k~²Æ+°ïÀË? ò¥÷o_òõâ?—,ëFX 'x,“G/CD›ÂÈÖ£ÝFl÷ðÒÅ]!9纙}¾uÎS:«×Ê•½Bƒ¸â» ƒš;­É‰ÙN ê‹A½Ò´ ^g-×ÕÎŒw²]-Çâ;kÒs)Á½5%­±Ø_8?x°ôÐ`o£G:0qüfu/ý·€`Æ­ÃÔí´AT0È´¯2X¾DL¸!-Aî¤#)2.Å03 à¿ÿàCRóëC´?ìO\ŠnÎÌ"Ñ"Þ±>Yw æL2[¦×sTKñx‹A1’<$9¸Ýƒ?"º¼Øl$É쉈 :Ùr½­úCo¹ÎÞË%´R-@½r‰^|í…±YBAJœœ)5ÏÁZØ{`­KNyÏÌnÔ~ÔlPÏ6 †ŒLÍÌËËL9Dî%쮉í»ç¡ªÎ;7­ÛÝ}û¾%ƒÕk&=Á ™Cä5‚vì®AÐDδ<”Ua3PF÷6x¶8çõïÙºié{óún­üÏ]ço›oµiˆüåå‡ZzF¾wê¬ÒnÇFt\6õúð#ó¨¬WœÊYožÏ&€üÆšbQêÀÕx€³ëgµÓÆuî3kSÝcÕ,‹´í6eï`앲•wÒ+à·ªÐíZ8«l8-1šƒ(ƒË¨o¼Žf©9jç„FQ Qï¼+^.{áÿ¯B[홟G †÷Ð&Á«îÀ…m$åq®-§ë¡Ì`|5rÕšaöÆD5‘VÓ{Õ f,Ä|6bÄÈ•—fÆÙâBP™íX0¢BðÃ礂­þ*å!ê±q}…—ë.ê¤bU¯ö‰p‰†²XŸlÇ>ÙÅúÄ„}b gBþvÏ †Á[n’ ^/¼‡ÑÊgžÁ¡Q× µß"-ÃâÍ„äB%C¸k#zÑ3)×*R¡[= ?ÓÊÓÇÄšwÆ£eäø±+Fß¼ ~ÊÀ˜k¯é°+ØýZŠ¢žU˜w³¢s8êFW€µ?UOž-UÜÝyäˆÇfŒ‡‰ˆ{PѪ@€‰ ý'¢¨{aQÙ@„L>>1¿`þ”Ÿ¤øšN¦ˆˆèùùy¥¥ÛÅm5Žçç X%VÝÌØ=*+k†º§ððhVrŸ/=$<šÞŠkš‡ëa{I0LZX„‰ï7Ê –ƹ7=ü÷iöz!Óì×cœ¦¥Å8‘¢˜Yä&k\rrzùþ””`µr9›œŽÜ,ˆœùüÒIûöÄÅ•¼Bo|PQñ}] ‘©#&+Žãª.Þúͺ<è)ŽéPèZŸ§EÇ솈ó¢xB8Wëamç 'Ør¬'9›ßòȸ†&5IÜßkH†6¿éywÖ¼ZdÌ7öR‡¦ÓJ®â[[‘Ï® ÆÙSˆN1…)¶ú±ö&"]>«Á¸wžä}ö"]¨BŽŠß²ð{âÖ¬lúSyy _jê¤ìCO÷ï¸ãG¥…¾T;‰‚rcÔv×.c”K&w<ö£‘眵¹Û®H®beúìõðÈ=*98¢‘äÚr–ôÅ&W¯Øä5Ò í±£ÏÁzÓ¢;m­tÑî(ÍâÇgv#£•Ó‘–dK¤~}‚ŠlQ±ßeæFc+`äG@ Ú*ªs1»Ùˆlgœ¶‰¾)<9sÙ™ƒÏ,›)qåMÄ!~ïÊ1|ü¡Ú“ò»H6>%x·‘Çhq‚+pÞdl4ÍŠDË#ýû (ÝõÝ0a†×‡o¢ægçïy-+Ææåa%´›tH*g5Öœûou‹ïªo0yÊÔ™sf£–‹W\†°¹ˆçü|ö{ËË’p’æi~×ÁÉÅúßË»&Ð'¤ÿðOHí3ðÉEWmÙ +pHæ³ïœ0êwaã¶é f¯Âé5{å§8 |î5ð¬Î‰Õå4œËÁê’ö‹ý¨SùÏm;»õÁÊŽÏž3{æÔÉS&lí׿tÀ®ï±¦+Á&b³çØì)Ð$?&–U…!µ€ô¨ýBÊ•á=ZwìÔ*K ­¬€U–‡† (=Q±mõ;¿<ݯ_IïÞ}úÛZ—}$"+8óÐCg:żÿ›ïx×fM‚¦N׬¢ò®%³BBäD7!r¨ÙŸÓ"m UW«Ae“^<ß{XÍQø¸u«¥ƒ\M?VŒNOi6cÙ=tøyè½{fQÇ7צeZx|òþ¯Ü¢LZËÌÉa9»Û‡åBOn!mÄôîÜË€ðIË_ü¸Ï_0 VÀ(ZÍÞtÿo<\øáòþñ¶/ÿ€¥Ÿ#DÈáwRÂ>Í€*Üɬ5 ²›ïˆ)DK7~Ï, }H:w"25]>ÎÓ»¨ùi…®Lµ>—£xzs-½X—¡Úœ‡¦7´EçvÂZ9úŸÔru‡Î ”£?«æIçåè´t¾£s#ÖF€š®Ê„ƒ0Dpȧzǯ!Gøs !Â_!úWÅeÜ[Fê-#a¦?B¼‘×Ô.!$4S##HA/NA ÷Gmè¯Ê(ø€þ}£0ø|ÞÉa$BíiÖŸT{N„˜©×ˆà…èûk5Šåû¥$hÑÓSOp[µéä"soI%Ušç[{á·`þ˜^ÆÓ$‘ôóFò_­—¿,˜ÿ×ÉÿÙ¿”C˯Å6´)BTÝ òÖ$ª­™„%à­Ù­µæ8s æ¾^!s÷Ååõ–û.h‹é®IµÕÛ@¡M!w­t=Q¬,]pÕú1]l´ÛòÏlš€v&Tu xÁ0vØ8ÊcL*‰q`g@yÿñÇLy¿ñ®µ —,^´øë%‹QoŸÛ²Éƨº«éÅÅï\PÕ÷±±ô®II+–3ݽ^g™Òã6èý=´†ã¥…$œ /pÐç€7âD@`ÃÓ‚]9PVIRC²H9O8>}qÛ¶,Òà(‹Fx'þS£Ú&­mÚG‹H8tè°ýéÓâ| 9ا/‹9X3¨ht¯1ÃwVß¼µ£šÇ&ÒsȈ¢ <:atL iÄ쪼!ÄL¼‡øwqÍ1ÏßLË?€å¯ý)~yz‰š.NÃt8ÅtuÜé­/'+è —¦:ÑcSc5¨™µM½uù•hÍ€ZÁô}<ËjŒ=ª&V˜>Ÿ§Áíî”2m@û«†V#ÿ£yÆ|Î,CáO/L ¤¡«縜Ãþ“ãòôap\Kå¸ZתN‰0¯¡k^ÀÕŽÌ!÷¢F{7,Æý#?ÔWã´•pöæi¹saEÛvwÝݶÈýÄŽíÌé VˆUBk èÞ§oß>Ï1xVÒ½j´Kò¯ÁæU®¨)“¿–V÷Ê”Û+‹é<¸{Äè—>/WS.:´»gtõLÿ5ݲçdd/¾w͹ 3Ž.îÞíóz>|ŸÉ/ÃÜPo‡PÒƆ°þÙ¬c”‹°;쎠ùù^LO%ªÜMK‚+ ÏæÖ_ù¼üÇÔŽ„»x:ÇbRóËé<ÿžÎöyþgù0ÍI©£fj“𚜠óÙ€NI'B¨Ìÿµì©ÁüÅÁü§yþ ×¦§jå‹0¯¡G'/ÿ“üc‚ù¿–¾áåk~ªÁü§ÕüµO"ʼü¦jù⣬üÚ¿1ÝÄWð)ñ#¾Â~áåãØoóò›ªåã:µžÝiq`H~¾Mòò³4úgü†Û#§ÿ¥ücÔü¼ÆóòÕüÅÁü§yþ ýyÐBüšô1/~¨…8/PHþ1j~Þ?GxùªEyq0ÿi-?ßÓxùù¡ô7ÄÀò?É?FÍÏéoÍË׬rƒùOkù¤nfWνtPʨ³B§nfWÎÓ›Ô³w3Å OïWÏRÜÍ-Å1]<°g´?·pÈ!„›%F@8`€i…Ê“ÍhÑ^¿\·V\Èx+kn|bðÔ,ïú%²ä¾{*gö,•ÛÊwéû~ó ½JN¿¾éÛ¾ÿù‡ygn(ìL~káĬ™çƒÙÑ"¯÷€)¾M¢ÒQÞ>E½›Çæ÷I³UÙÝjðÌÉÙ”ÅOûHC¥El|U'ç1jR“§ãPʹެ suYµøîŠN z¶lu潞“ó+›0ºÔ[W;[&¦)º4¦,Õ•/»Ñ¢`ÆBLϦ).ÿøÙ̺qö¬¶íÚµ^)™<2×'õzlX×ä¸ÝŽ–ðæÅƒkTƒÈ9C†¢„ðõ¨ÜL p£GüGOã_1Ã[u©Ü1hD÷èÛ­`f„­sR³Gw 7“,Á©^#ÿÄô”„ró$í,rV庂TíìîLEÉ*ô‰öÕƒT½s þ«€¨Ê'ð¼zlļ“ÝMw>½o䨦qeÃûN×ÅP)¤´jÛ%+µMÖü)ÙYZ(ùå ’Æ¤4Np À_rL&&Ökh›UÜeí¦>½z~J¯<´±Wï-kï΂1©9ÙÇŽ91uN›6S§°Ë"é̤>Ó—– lÒ¤<”¼&{—Mï3)³×¬¬ò!½¨a^°E¸0“_I¯ y°!Q^MÑä•g*ŽÝŠ>kLI^3@ôÖü!}Só™ØŠþv[GÔÏûN|’¾«)PËâÚ•+K*)Ë¿”…ÞRÃpÊÂ\@0W`Ó<þ#ùî™Kêñ_¤üæ±Ë‡À~E»ˆ®î@ݯxú}uûƒ¾pøžl©s>ã§„cšÜÿ!6’ÿ²'$–…ï'<]¤ŸòkôW4tnÓè)Tâäå –œ&b°ü¾¶TøI£çEhp-Ðð*¾F •»$\4 uQ¸+‚jÑ]!næ| .„ ˆÁuŸX÷Èf„ÚB/-™<(·kÈÒžÚZŽžÕyWõà‰êê“Äkj-Êf~Å&¼Z¸R¸=ÿ_<#웤T$I¶•Lî—Ó[zôªÒj' úÆü%§ß)@4£S†7-Ú÷äàá*5ó¡ÝEÿtŸü’¶•F y™¤4¦vœxµCzæö¨\d4©Zk!@vèÅÑÖ@=¹¯Ð€ÉÃr;IíKžà„:SMax‰ô³V‰t€_&U•·ìBÏšÊHk?ÔVw›¤Æ,gñ$ Ú4ªË?&®W˜/{8»OЬ’y”ò†–%Á(åÁåÁèäœñ©Œn"1ŒÖç÷ Aûu*ÄÄ\kC#÷$n Êþ…ã!L_ì Ù·ç ¹í…îCê à‡th%ëøÿ7¡|_3|ï2Úü.Ò%ÿ¾òæãøæåÛÑŠ6t Yhgý`ioDB@¥>W¥>A#ßÅ?‚5Ék­£™ŸdsõÛ%çÔÏâ-}*ùíXSè¸PªÙ±¶¢~«Äùÿ¤ ÿà_ìF(Ñ(ß…'Üê6áþ Ͻ”xb%,ê>¯˜¤KKÊK+¯ÜÌ8© Mô¦Iň#}¡HS@ ¼qÌœI‡ÃRºÄÿù•n¥ÐƒÁHó¯åaô#˜9›¿õ—ðê•÷ü> BíóXK2Öâebˆ; R󰸚]Å©iÜŽ$`?š vP/šÍ×Ô2Ì]2{Héš™¯eeúÛ:cç1K{n‰ÿqØë°þCÞºÞ3¾ ï\ˆu¥®™ýÚo¤¿]JüêÎÝé­Þ1Â~a˜Íð;#-¿Öî”ÎK/"ÇÎ`´4JKˆ’ßšdÌŒWÊ÷íƒÌÁ &NÝœC³M–ÒM¥%=}¥ƒÊѯ?åvÛ¦Ò#G¾ÏÛà¬Úf6ZgŒßYè¢f‡mPT³°·˜ï„Íp; üp$Z¿f´l"é>‡vZW±dpéÂDŒ`&\gñ͸WÐn‹[n`ök @³`ã÷<ÜzK¨½³ö˜üö»™Øø¿ùJöh ’ä)óèxéåŸ)Ì¡V¹I™þu›6ÉéóèššWáY?ÅtÁ I=fÓ›Xànê^’qþÉ-€U)&$Uzp«æÀ™œ ªn†‹ C]g礦ð`׫}nXïß~|Û×ôÏvaSfÌĨ×ÉIÓë…½ŽŠÎËp»ÕÀ×CK•‡»þ°àè[‘"XZFGcìHWD]ìáœÀrbW¯Ç²E©iZ`Íãg­ÕyuC ÀfZ›¡GX¹OYyœMÒO³…IåËš_ì9“ÔÛîå¤II|p®0Oo顮 P›eóö¼;·#ìÏ–­[·Ð/½t,œÑ®Ýí3¤SàHÉŒðöÚÖ1ÒÓÁ$/[úýË–úoŠž‚üצ`Ç&iv° ëUËMtzmràjuåBüôöXÒjú¼¯@§Ê­[§ìÇò_blƒÀ‹ó ¼DD®èꉓ¤“ù¾.æ¦<—¸>6Áf7<Ë<Äš¦¢‡XlóK wEÙè!–àõˆM47·F|Ę›,·²ænbM-M3›ömŠŒßg‘0Ã;Ã:ö&ÆÐÍ›@O)žªóö?uJ~ÿVo(£{……à¢ß ¸"Ù(Ç‘’†ì+¼® x‘†u¥t¹ìt pû{A’ r[¦ÉFÿùY1´WºlŤQûµl¶zBbù€áûo/È|Äßm É蟟è«ÐÂÑBnÅW‘ªÏ}ìÉ?âÒ+Á 6hã|ö?ÞÛüØ0wݽ¬'¤wÊQ5…«üºh Ï#[D´£WHøiÃ<€Š`7"—´-Dt4jü{–ƒ+XÎ2)…;"qsfìô9û:ƒvÀA{KŸîÞp8qyêd4µ°µ„Ý`$—޾P|„úù’d‹`d´m7¸® èÏÇŽ]‘¢lc:aªˆ%’¹~ãb:Ë,è˜ißunk$ 姨ò¨QË£˜üÄgV Å׈ •tá‚*M]¸ Â&ÑÈ3t$«`籊^&K+t“å¹C+ õp”C‘òBl®Å²'ŸyæÉýÏ<#XÆüPIb×n`Ýz} ܶhA_n£r“þò+¥¿þBcW.„CP áàí+¾¼y"h ÍUùJ—ˆš4ê ALMk ¶i8ëKo2}«,V¬'¤ÁÄ’ŠÇêIjþQ¤vÀÄP‘l÷­£k†ÿ@ÆôÂ×yÐíŽ …HVdp1ƒÎÆŒ^•0°ŠÖZ 7ŸÙmt›ÉpƒrÀ²>ƈ¸f|ÇåqÝÐX6NHðx¢c¢c=11Ñ)!a ñq´þL`è8ñ1±1žè¸˜øè„ü¨üè.Q]¢Ë¢Ê¢û&Ž]žhjúÇáþÍø¹^IÐúïZq ­ÿÄë?ÿBHBUÐþo0Ìföµ¨rc0$’tFÈ«ÁDÐxÓÅVˆ½”Eo¯=‹ã<.Öf[¤ÓnØŽsW€Š.G–Øš¦‹™.çÔygh8v¾}vÚxQ&‚z äf+¤ÎëFV"—G°L9Éí€GÀ³ à±E3~F¾¹Œžùe¦ðÙ™Ñ*@õ0À@¹›±´?ñÙwùhâaû¼=; XÄPËÑË‹‚Š'¯?{Þ§Ÿ0»iâ6~EßþTFæJåŒRÚãŒÿ··ñôŒìgÎ,'Wfš°$Ù•$¹ b¼hr´ÃP^¦pKjÀ±ØjSMa=…DŇ‘wÝo+Û#.XÎÇ¿»=ÉzÎt$iK*3<µÔpçã?X @Þµ¥é9ЭÜ^¶ž;•𒊲͢ u;ºâÀGôú©Š¥=ûÛ9È©þ#ThhÚk„eUŒxc§MŠõüpE”qœÖV¾ÑjÕ\áˆÈZ}ˆÕZˆ(­/ÄëÿðAÌóþ7ˆK˜ø/à–»oíh×2àÉ„…©„…xrÇá^÷š·êm¯öß]¸ÿÝ*þÕ·¾¼ž‹y2–2{ã’ã91Éްy>î®ä˜pïö¸âã˜zPîZõ  `¦èü7ª/½ 1{*Jþ•ôbõ| * +v¬›ùl»Ï“‹Žjñ\døù°‹æjý–ȨB[‘N"ÿÕg;Äe;ßÂ7üÁ÷ †L&<Ðw=YêÛ\Ê\Šû<Ø]jO_÷@_4Ë} Ú<••Ãõ‹³9 q/<}fC¼.ÉnK21Ïæ¨Bt<ÃíÙ‚1ÞT‘Òç çá¢d·†U§x™/t,úB§©.Ï—1ÕFçôeéÜ68?WrZRÓœi-ƒrŠ pí°mGó{έx¼tO7å5˜Öw”án}ºM›V ÀÆeæý¼¨ïAù;z„Y0¨sj÷*Õ:¾f]TëžÚ¾F4«ób_šnÒm7|œR=v¥dAËS<–íøj€±À‚ÂnVÃk›³ÿ_–UI…ïÈ›—püsi ƒoÜó~¼zï¿,0—;Q#¼“/Œ¨Æ‘ê‘-íBOU£ÂŒè$[¥:/8Gÿ3ÀyêÁyú/án>KoÿW¶ ™L(¦Vµ£´À\VK=#,Ôܦx5eÝàG¶ƒ}lÏôÂ2ü·dÞü™ëªL5Ïà xÇÒ±ãÄò¶B.·oøazƒ~W¹-*2 ½K—^™Ö¦°CûZ™ùrWt$t ‚ÉÜÙiÒCalˆ¯“àܸFn·0Ö½¼=üœmK,Óƒ1îÎs1T#f±ó¼êöi B!$•»»pÀ*&9Cô£O£ý/úÌà€VB§Z[¿A¿C²_ö2Ô*¤u!Æs ­v‘r@exC­*ïQÇÒÀ6ÏÝœªu;7¨·ÒA†°Âò‰;Z`„ã±GYåÛ¦‹àJì½næüyKXGBÖQ®Ë¯¢‰wFù3}: ±¿‡‡u@õ<4ëWNo<üðö²yð|“ØÞDFhdÔ6wÌJaßÁVG§{ 6Ój¿ånIÝøL²µ‹:‰h^ˆYßÀëüò¸ŽL=þ ·.Eÿû[ÃG ÚuäÀ^s:uj¿bÞâ¹9­[Œ*ÜÛvØÐ!ƒ¾3]ĆÇÝ7ãɱ5É.GZz›¶i)ŸÇ%6kšœ”óSdxx·®›Gû:¨)iHP‡ÿ»ÕïN4Aùßä 6dÐ7s»ÆœPûóÙÞE@_Iüß´ftŸìÓ♌j¹wÚÏ$$¥Bµë%7ÎM»Åõ¼…¢£>ekÇ0è)ºvþƒjÞSYK^¯G ŒëdïˆòkÛª„¦UÛÔþ‹â”POCêÝŠÈO*üô¢ _’/°ƒËTp‚ †ê±eþ<~nQ9,gTnˆàš»Åek,Éåó¤)…ÙLÛ\Ç”| .S¸}»%Ù-Ïù¸‹ÞíÑ)¸ûÎYveTGL.ˆ™_}¥2.ú;‘ƒÜ‹ïÁ9å9ÖG"rp$…Žºëå‰!¾?âc̨ˆþą́(ÀÝ4«¢£Ãî¾>}ñf}tjÌpètót>vçr5ôWú%7'Ê«Ü-*hy´ivO,x%?,…Ø9¡NÄjߤѢþiUiáN0&î­³mâdp+¨Sܤé‹ê5Vúë†{5k&^¿jùm4!a¸4Š$jN™šw;rL‘:'4´Í”†Oœ>c>ýíá‡*Ù5åìࡇ‡>:tÈþíÍRR<@¯ ±Ýr¢£6o+Ø7mLO¿ž5dðáC‡9lÏMln.í+ý‡ß'xÈ2Ÿ)ÞcvF™‹ b‘3*ý´k._ïžd)<û´CDƒÈý³}FGµíc Lè:âDe¦•f´'*Šûa‰Œò½¥íÑXÇÃSžŽ‰ ÷D±4ÉÒöHxLtŒ.ÿˆŒ‰å`o× ‰ƒ`¤UÔzÇ€Wx *:&2*&æÈˆ1/¾þú‹(cME×®Á¢¼ÔMš7mÚ¼I¸þ×^Ûž|r[¯_× 54.±&Ö^äM g AJ°¶TËC®,3P ìNÉSsý…@itÊ tnËÜ–Í ÛⳂD/Aº¿æìânƒ »õh=€ÙA{È –…þγH üÇ÷ ±}­Än6Y»²ÂÑêžmµb …2'ë¢ò˜å1jHa§CÆ$WM©yTŸ0Qˆ°ºº¢?8Ó'ô{ú¶3oÀQaÿŒaôÇkæÇ$¥îß*4ùû{a0úÐì•$…`Õ ucö@U”n ÅUÝ™+Júå·d¤íP@Q͹yíèæx/>U|ÓlV(HÓv¿’áZEò$ Ü´1bÄS¼®/iñeÇ…9¬òa+ ²1¢ˆÀhñ èfÄ'4˜¶+FñŠt„ì2ê 2˜"Â%b±p¨?öV±o,ÿkk¹u†u¹•A¨€Á“5¬dC¯óµâ —êIš¹P"|Ì×OŠÏ —A»†û­æ¼Êõa‚LŽÈÛõ(²±åa¯“/óöBEIEä=o^BØi\b$ ¶ƒHRœØ‘H‚IÕÜúðCQ~í‡~îþáÿ<-ïyî-GÀË$ŠvË6îÃÍ;™jî —Ù½½°õZmví(,œ„îä’Þ‘ÐZ¿¶G7Á¿F¸©sÅâ‡î ö¤ô9w.äåòžànw¸cé˜îÛQ©ÂA¨8wŽV¾I_‡Á“‹ò[ Y8ù5õ¾0˜ ê&õ,帋Åiü^õ¡Î|`bjÊ„Çè7oNÅïo’;3[ü0oá|vuS†à#©øHãî+¡¢¨Z8ñm:rgÙ’²F,ÿŸ‡Yþ7i%_ z²ÜÞ™þáð;¬¨ÙÁ+PŽÛ/$Í~ñÿWÝ⫘[ü‹çzÅOhâÿ,zÅßøWسuNñBªÿYæ_ú°vkwŠgdÐÒÿ{dXÿ?$ãï?„Þ{Ñþ˜´¨n¥ãÕ{}äb.Ò²þkz—t\%FÀkÑ-*Et LT)ã$ixƳ…{T¢ü ÝT¢øµ¼pBƒ]ŽÇCAb„ÛÁ⨱"€ã´õ G€Á¬«T¡TUs7ÖX±scïr/rãúq B´¼š H¶ã¨º^¨‰J°í~ •ãG.`H%»5ÎŒÔd¤3‰ø3†Ð!îÎäÔfºs£…C†N±b\`ÌÞ͉OtÚõÜÍûb\³Ìù ¾ðOæËfùY•#H·{ÚStƒºg¨ ¢IÒ!c2jÁÂ8ÖýW9*R‚Ϩk -E‡øøHóWÕhɰèyú ò©¹Â=·v`û¹DTíL¸žˆ9§·÷…9­Eb‰ËRd(Œ«C3°8/Y­áÑÑÕJx59§l‰ãê&ÿaØcºñ]ubË€ÿ "Ø€K‹à§Is¤ŸÛ>Ù o›SÚ/¤Ia™èLß>KZD_£×y40Í—¾– Ì))ÉH§ÜÖqàÀŽü8IÑã410ùŠ4ï}1àô‚o¬<–“#®œþäþ²²'§w{ÓQ°š.†Ã5ÕF«Í€Ã*=õ6›xçÞ}û–‰w~üÎÇ´öcH¥<@!r˜´…ظWsYf³Ê’–he²NZIÀÀh<èc@ /¦eC”÷£O¼î‘^ã¶4‡'û`t̆  œ)”NýüåI0hÕŽu_L:ÓÝhÓÚÕU›ïZ¶ÎÍÂÞ"Š3h1¤ˆX;myM¼ŽÓ‰ÜÔõ¾…Órff‹Ì»{ö`[]—!C—ØœÎtÈv…yTníxvZN.r (Åùÿ¤¨Á‰ÏýýCHQ¸†» UqõвóýQÅÙýg™PR¡n]7¤’ß4Ì[øÙΩu”q;Ò ,=®,Ç‚×ôæªÇ±b&+±lOMÅ –3A^Vééóê,Š¡£¢£ÜV%Uôö‡\ç¬G¢¦0›‚ËÌTë[˜Z•úªUWáóð‘h)ÛhmNͳ×Óãß½‘5÷Ú›—„PEþ¡G©…‹ R ×äkë¾ê7Ê¥Q•µx‹íLvxT©jbr* 3&Û"{5Òàé\»º¥òÓ)ÓßC«7 5yâc…½ïÖ܇Wg·`=M‡°›Éøôs<¾Fb=HMÞÕ9u&—ì"…‚?, à ëƒþÓõiÓZ·žÝ!''÷qºñþ”Œæ=…y Ý»%ÄÑ_®Ð Ryž;¦wëêÈÊMÏ€««'þUâuMÀ#™ïŠXRg4™„Tñ}úðJ÷¦ôlž‘r?Ly<7'§ÃìÖ­§M»N­PßÓ_âºuOH¸ïé .zftfV„·k·é;zöʹ‘E13Š&YpýV˜·âÿ# Ô)Ý,h­Ä§jóY³²H¹í_­,ŽJcRy·;_í®Y¸M# ¾#гøÝg_„¨HᘉI2X,J S‹kJ] 1Ø’bÙnÚåP¶‹;¸OR÷;<`{T”8Ô .‡ÿÂz¿Ãzˆ+¹3Rn^ÈäãÒ£R§s0¾}øpE…øûöÛò„ öá[æá?h0CÿõŸCùúÝYš‚3 ©¯ja÷ª[fPcfå“m?Ø>¨@iºý¡7§NePÒ"Ä{¨©eaþ›¢ þ.ÁÚ\kZÙ„±gq—Meˆ¤{ à Å{öMZú|›7&ȨH¤,§Ç©Ží|ë‹‹UãÀ‚[x8¸Äq·Êsùfªª®~¿ ët8ƒ”€Ü‹x(?Œó»=ø¡$€†Ó”ZBÏ„àTØÉ\_|xqè·?vèà B¹Áñ¿‘4Í7 j@nÈ_$BeÛ…zÐ Nº0Ñ(D/ ˆÁ{üR£æ(gKb 7Vä÷5Ê!Þñ¨ç6›]:“1\ßÕ-™À)&°vÉØ&'Ø ‚|!ÜévK±Ÿ˜/ضHú#¦L~hFI_ÒgÙ¹‹½‚‘Ýœ&ÀÐ8Zû?`xGêQŸ™…_éBÙÉÎѾDMÁÅÞ8»vŽñó—C½I©e-T_^'•×i(ò”èLEbáÿ†êHK5i8ò\ûÌyߢÚH³â|cê "ºQ3¹ç®ÿ’{—®ž>}uio!ù?#î¢{<|mÓ•¶/ÓkÝ·LY\Y¹xÊ–îórÛ+›®ÑËê±S&œ`¡0rl+n3'5A›‚*'‰A¬¸Ï…üÓBPä°’å(¬A!ïû"ù3‘(²(Öª³\<Š×’0⬛ ’ÌÜ(Jô°˜6dö‹GÛ!JÝ…¼^{ˈɾÅfÁ,Äã+Sè ô … Âvá)áíõ6¾>Ã×OüU‹¯(,Y4 Fâ”ìR¬MR¥d1š|©¥ˆ×$KÆäbwèNÊõ ä5âZX£¬•7I›Ä­°•lT*å=pL|’qN7fFé•£h%ªñ¿§[á9¸KTjnrnÏÑݹΈ40ðâ®4Òyèî'éûøÞ i°æä X#J55B_’Êk¨(0 ®‰fÛ©ZKô—!È6¬ð=\Ø•ða×Q#ÙÍw“¦n£ð—_/{¾§zÙ…¬ÆBº"zÆóbT8ºÕ0çÇ`>mMÏKåôqâ_Àu\ªIþvÌíR-ìx´‚à~”æÕŒì }̆ãJ¾ãL’ýK†v•×—&ØdÜ\æ\@K;~C5[ú™«Ž=¤¶Á-¹mr’3[çêΰE.7/x§.lñã'¢ºlåkàÿˆÛé—=°zT’Ë+ÈHI _"*ñ:¬Â¿=9«¯TwGÕ»ßÿó¤­`ó`ç|ŽUçüÙsfN2yÂV Ëâ3XäßÏøØ¸¹Z•æ0.£H¢S¸ aê?Ñ2Ìø‡.÷„‚¤d»Í›#\ovâ!“—69¢««#gÉ Þ{MìUî8"t¼(Áìg–t4([K‚=âƒMQ½PŸ2CJºI‚ ²ÀN€…L“r;w£äM®wY”¯IRV/XF|T†ó}쑾}z÷.é×ïéŸß]½­â­>ŸUÒµ¼hâF@Ó$« Þ»ãÃoîO- ŠÇɤ9½¸CÝŠuL› ÞF+œÁ»?7€3>÷'ÀÏ*–Á{€`ôƒ ˜Á?Ë×1­Ð¨h´|1WøœZBʇ¦õʧe¼ü$BRêY+Œï5lŠö4è¡z¶Y­vý@ë&qt½FuU®/Š«V[n½l”=QàîbÈáUø2-5?q3¦¦¦­B ºƒ‡Îœ9ÄáãpuÂwyiø//¿$5UHUÑè@úá:Hà€ñ@ë´x6Ǥr‹"¾LÈ?î™\êÚÐi©Æh8à^ï¦ÜnÓ.ì[SVZ:fí¨‚V­sæ.ž·¢}§¢Ž%ôýyé©­æÌùªÙÓëP>bï‘þã )¹i³Ä¸ÏSÒÚ¶IOûúÀ“3î‹‹êཹk·mUþª¶±+ Bäu(We4¼²@®"íÿëîÂlQ&L} "QEÿ¸Ä¯^b oÚø& év“±{'¿É¸¢õÒ'¼—.0kMº› ǹBØÁ63{@­«¤ñÍ’Kº:Io¿ªåÝÓjö¤%'ÑoQê“59ë|k²Þxdì<ÈÇ{=}-b¹\Z¢E$PÎO^H+qË…$ÿa8 OcÁ%b:^@Ë&ÚÞÓ·~|Pætqåådø¦æ)ÜB^âa Љ_¼Åð[¨ú. !*zÅzî âc¡þ‚2QÓ¤£¶ízˆÃÎσó¼7oZdo¨³BÈrNn”ÎuªÚ„™Ùþ“+ýòãJ PJ+~¸!ozWeM• ¥AuÜ€W LÞF¸ TR B£Ð+ÈIæ±’+ƒÐ+¡ÜJóu¹ÂuÒi¤é?Ì"¹–ðGC§Ù õì"a$Lßfi5Ù1ôH¨}$rëYG ï ƒi óU#!&’§üÇp¡›a '¨Sãäüox}R¡ó¨Ç·„’ßéÿ]sÒ("aß§ÿ„·§%)$íÿ¥M|2çØmàbæBÿèo>õÒR¡±nÓx“;iŠïœ$ý'A|C¤áúth¤yi)’œéϲ‰¹ûèuzã­Bȯù²uWI÷ýÏ Ý˜ãA7o<¥°@ éBáÖã»–JM p</SïyC•ìÄäi~;ží—3MÀØž¨ˆ2Š)j~cÀwÂ~z¼ýpaAÀÙ‚ªÖ6O(‹K`ª€Ü¼Œa†ôž}÷Ù|HsóŸayñ­c5w‹$F,jœ[ñ;ûfœØz§qG´!N¢­Ú5Ø/xÒ@9¤oñ¨VÂ3ô)<«2šz¼ýßœ?ù`8¨Ý#”s(D"¢·Ül ;-‡´!]‚ÝŠb¢ÚŠ!#fÚgç0iκ\¹\È ÀÎ¥ª}ÇÝ}Êvïѽúõx'<tÏD½ø†0¢kN²†Kº3»Ó«ø£'‘î=]åÏÇÇn3[âѺ,2=}Æv}š¾ÐºõÈn)¾­¯Õ5dš˜Á_¼-Ëb‘Žðä"vk›ì˜¼¹¡Jü刯€VD\Ïú2E«9$6Ý­G+¹šDq°~î­ù"6éQ,¯Ù¡FN¤Á+w iêÖ‚ô¤&à¿ÍYÜú„’I“!9iW>56Aóðzîº6ѸÊ韓R½:”Ùš7ò«¾: VV‰ø:ç“^ú<ÖØ\qhÌAüNññ„HëåÎŽPöÜ`ÿŤ0HC?´ølÁÂ.üôÄ­… nž ý ̦}á -…ýþõðí''ÔÿÌë8$¼„uèlD¿Ù¬{ŸÜ lÜ@ïR‚3´=4Õêoµ=ðæ_Ï—±· *I­ü¬ÕðŽÍâ%ªÚ´np™”¡¼TÌõÕ_QgÝ•6Þ»]«ÃÍ+,VÊG}€î¯ÙQw´³¸‚WÒ˜F¿Ã´CòX=ÂÑq똪MgÖXB•ØAÁÍQÑڛߚ4Çש¶ÄßH’1¢…§[“&×®µlSX¼Ø×QЦ+Û_o¶»²²Ò7Ä ô`AttQ§‚S-¸eðÔìÆª(.^utJÇȨ–­Ûtm¬&aü’NE«¢£{–äG±ú„Ú‹°`}&ޝ˜ ó˜È1íZ—.«f¥wŒŠ¼vM6û·,-ê´‡ïRiVÉq¡w ¼õÈq «â=qqƒ³²®]kÖ¦u_„(–Æ™9;g8ýL˜=¡}l,ˆ¼ õX™8Bo=P¨í{iÓââX1™_ÝlVßpSD”ØÕ$+X xýë'´o °R`·8 K‰¯WŠ];„\ž$bH×̬kY4ùèª.]›µjÃéÃ’Y-yKITôê¢ÎK†µãd $Û{+0/S±;Y  {ÈÕÕ×°çêÓÛDEGGMugybÝ#¾ìMHHïž’\Üu3={M<°Ùbëoµ-¶YÒ·ΉÖÛÓš´Ú´‰0›i!Táº73m‹ý_ºln°–X°:àsVí¹z Ÿ–k/î¾FßmÞ®íØ;ºµ‰‰6+X xÂßRÓ¦«-Ö¦V"ÍS5£ˆBÔÏâ2üœÊ>[t¢^äé…{˜+˜®„^Ÿ[óÏ<5¡Ò a”ò1‘Ùî`OóSÿ“»•÷ÓÓàÛnˆ a*b¿]íÉ$‚õÞ‚TœPùóJ ('ÍIKŃcå€Òë¥BžCr ˉãÊv§ l!„©Ïäãã©Òõ™Jþ<>™‰xP]µö€œÈ¥ç¼öß­IÚÞÝ*9¥Ãì\Àì‰s¶®)Ûqá5©•/#-?Åù¸Ý1¯ÇÂýò¬ôûúÅÎÿPù\3Oãâ¹}mAÇq﬜Ó27;g\ËY¾dH)¸GjEÇËš×oÅÂóŒFûã‘‘>ô¤Ïð€0)Qø]Áø¿ ÷Âig „Á÷¸Wt¡.ùÜïOì~tßï ²9弿'üN]ìrX>wä÷}î~©<,-{+<“M%»0ô·=»Ûû›R™;Óð}ƒl"Ÿvð˜šMZŒU‡bêÂlk¤âBž-ß&ÉÂ+äe$(¨=¯jÒsZgŒc¢!Ÿ$ºÚÓø˜_¾VÖ‚VCpj&¡×¬ùë|õÕÃHÄÚ«W–,’W$xY'Ê,¹íõÿQ²rÑ`M-/¸–H7`Á—_Òµ˜ëñآ؈¨l9©Øüï 8_–‹…삘LgónC:v*<«’„5C7œU*x9#qr?¨~i€$€…}–n`_ÂVMí0Øw†Twª¡kÄÂÚNƒ‚üÉÊ¢áÁÚ,³\WÖ^’A=âNíÛ:2ð["XüŸÜ Df£åí^hú&T¾ù&­¾‚ÅþXº+ól<ÏÁ¾Ebn©B*tÂRBhI½”e,…D÷Âeq/v/n}…—iû5G…ª£G!‚þŠy0ÏZn›•dÅ´3˜ç£Gé¯A›û›pMœLÂXž\îY›„‡dè{ôêÕ£W…»ð'þgá}![¸Kíø<¯¢ÆxŸ}uPöˆ}•bbÂGáR˜ŽDèVèa‰°0Ïßl‚÷'ÏgéÚÌnz,|Š0ìÀŸµ§ùÁá¥˜ŽŸÕtmêø¬fb³Î¬'ñ¯‹×ëGØsÕϹüjåà†0´B¶A9PUu@).îÑ£ø¾­[ À|¹P¸KÉF²›>#üB$XI$‹JÔuN«Pâ„HDÒÔÀ|°ÑJöD _JWE.;ÉM:$"D'xpŽEúŒæH ܆¬ Ä:ÿU-DòEâK[eÚ ·çÒ®jB Kzå”°wåãóæ=ÎÞ2e ù½z嫟'etº(ä–$Þg2†é"±‚Q2Z–‹gÝÊ.;.žÇ>÷b÷hÎ`ÎPTáåõ9©…Ùi½Ò F6™½(¿S÷è´l³¥*,lxYÞmHçn9F®´&qäuŸÃmµˆ“ˆ+GE:LFƒì8±$bˆ ±èýr¼,Dï^¥_nŒçCΣ;p~Sà §­üíæïúÏ;G50Ò¥ìU¼$ÃÃÃMááæpK¸5Ünw„;ÍhhŽ4G™£ qÑ$¢…h1:<ÚcޱÄXcl1öGŒ3ÞïŽŒŠŽŽKw†ñÈàüœÃcHðþŽ>}Ð ‘w¼ HJfTrê=F$woÒΕbNNgw’KS)¾=Ú>—a¶øZã_‘D /"#zŸóZT:$¢õËå,YGéúèˆz ŽÁˆó×XóFYñWXõÑ_»„æßП¨¬‰ó.G…«RÛw­)õ| M¨Å¿8ûd9?>ø¥Tð̮ӟչ…~ý¼öôÓ¯ èÿÚ±cÒAƒž?YVvòy¤<öŠñ²E53{®ˆ³/÷F1×®™Ãöû~ª]@- jº/!Á+é@OK÷zöÙA{Q¨išÖ¤ij¶*jr1UäB*Š¥×±Wé,¬Þ’ã¸!ž3x†Æ^b ½$2ïϪ:è‘;·+ŠOÈjŸ~°]»Ò{ºö,éÛ¹[·èpãv§omß®ªÚ×¥XªÙaŠ0{ðbSv4oV>.7ÇhplñD˜-ÖøS„N²¶h>|\¹^49P†o#›Y?©{‘AÝê’Ð+ùÁ‡þCý`ùOõŽ_Å+sü8}›žzå•ÿ“RA¹X(¯#2qú pVÄq—År–1§LdNÕØ*,£ÅÂ<鉛sBžÑ‘4¼—:@¦” ñR<(pV’ ‘¬Š gEä°2._¡•’ËËã'ĪвAQQ¾°ÆŸnø°0ÿÿ“ŽÒ HÀ"Y@‘ÎÂÿ: %’‚E5ìrV"VA tÃõ³Y)ÌÅ9OXvÂ÷ éÂÍ9'p;<ê¿.ö­ý·Ã;ˆð÷ ¶K—PLxÈ–ŠÚrßÅÉrL?!–¦#r–¢ëÇÅá/©CôÞ,²7îó¹ŒÛ!3^[ÊÕ¹!LBãx Í…3 >mCXEËÜÉÉ)È,00^²ÃÁ?'zéÃAŒÊÉT¦¶ ‹L WtQÑ-”Û ÆÚÚšáÒ z¿RÁ :oQF’¼t£¶’§Ì¹õK©¹Sæ³áN5OM•S[ª´Æ”*-ÏKòàÚÖŠƒ ,Z"£„¯„YtºY5„§—=µC•b|jK“ÀßSú™ª¶çñ>=Ù#J¢ ¶Ãƒá=™˜ÈcPd1uˆ¿'S°K,ìu¹°¶ƒ’N¬‘xaaH¦¢¼`P ƒ)àÒ»&¼‡€ÐÓ!»Ü–‚‡%š¥vL<¨ï„ìU÷¹ÜÙ q½t#Û“e±G»R­f[Ò—mäÉiÉ¨Æ 7aõW¥«µ1º6pZ{_Æê?â½ôpÔ ßÖ`Fâvé:®ì H|tøIeí '0óÒõÚ(e%M8ANr´Éµð!¡&‹i©rv.”‹Iö$ ³ÎìúëП5žï@ëÌXÊV$âÓº®t>c7bR ÀIj ®ÇPRyŒ *Ðê€ì *þ‹âàÚYÒJ¬ÕêSDÀa@¸‹è£þ-=ñéiq04żŸKgè¯Ê‹DÏd1™SäÿœÉ+ `¡òâô/Øp{ƒ|Î$W®3ÙšG]ˆÔßÀ\·oÐÿÁ”7Ò rL›QZMª€Ø=\¢fÓÕŽÞ_[¤N׿.³éê?W[‰)|ºþu[úuÐ>µÿá½ÅPžq'ðï>p`ß>z`_%õ"»1‡_Í!«2&æØ‡YL•û* Ð<¹)×Mcò²ÈØDÒú€qR9tÒÿ>~])¶$Òçõ=/4ã”܀㠭¬/ˆ»ê 8HÀ0$ ƒÑE ª_Qù~C‹SsrRÇ®];vONJjÎÆ»ÆŽY‹múž:jµkUÑ9— Æ5ß?sòê8ùÌ3'1ÃW˜Á¬e@³Þ¤mÏÏœ¤oœ<ù ÖiÄ:¿ÃF‡iÝlx%6º’wþÁ‹ƒEÅQÓk¾2蘓¬’Gè‘äg¦nHs:‚þ*°3 ‹ýù²ªheÃðf®P3§xu¶î¬Úîý|9 +ó÷¤½è¡Ú™ü\ëÔ¥N{Õv¨í¤.õ¿mDð;±-?`ÿ…'fçr«&V:Þ9.:87…ueJîƒk_Y‹ÿk“sYæ&½‹u*öª u×’ÚŽê'¢bÝ—qæùO@çOO¿@?Ày¦Ðéô ÕPeg~{k¾R·a _ÞHÖðÚ¼ êò.§s`¼œŒ©m]{?j¬°ß­¯ž '¦Ó»‡y˜a’²¹ÖG¢ €øq¡$×åÎ)hYÀåLVfù¦´k?ùéaÂ{ï>ÀŸ]œ¾à‹*.~!«Í]pU—î6›•q œók×Y9ú;•¤iÒn¾ÃU©w?Ÿz8Öõ½óÿî£Ç±-Ck}*óùë&Lq`J»ÿÊ|¨Cc>l¡ÞÚÒÚêþò·Âöº;°¦¬ì/ÔCýĂԺ”Ú¸ãôTSn&¢Ø󌺙ÎÙ`!«õ ‚“»P”Ñ¾í ¿{á7}ê¾ù»—"¨ßܬiྕ3CšvÛȑҋãñ±ŠÚcµo+ñ/ÿþp¦ü×Ìøc6ÿ³8!vá<5á—ê¡*O=gùŸeK<%77ŵvÌØ»6æ¦$çòi…­Ü@tê].ã]5/á<¡Q0…À»qê¦öÚV¸ŒEu.y­€³é5𥝙ðççtÿk¯ÕŸOšX“RÓ˜Œ¨ýª™€ªãäöí¦mŽS©W_3°_]P÷?§ü§ôT­¬œälD—VVÀpæÜ:?]qçŠÀ9{å+VÞ±bÅ+W܉Ýð)òýå'â Ý)IÈ8ê ÚNÍ¥ÀM_J2†?´VßÚ§7DdćGàß…íã%Aþ0ÍÓ¿ÿíq1afs /º4öp{lŒ=IÙðKm¸À‘¦ ªàAlTÈ™‚{êUÈѦÓNi†j" O ìVE8£nð(qܧ{×’F&ªå‹Ñ5¦bûŽÑ2ú@ínbàž(›1ÓúÀêÁs´ì™Í»'bǽ‚p¥;6QuMó¾r”þvT˜#{,€f8¶ñ4‡^­}VÛéµ "ô*ÓlðwΦ¡›P øW“³¾Y“ß5¾û.øÞ}—s:©²àäûásdÎQ±:¿>?J/6Ų" ÝØÕ f|xN Gƒ <¶ŽDx, œ“¨¡ÍºÑÑö„ô{„¼üA))ññE÷LÒËGŠÝÁ“R“ä÷„µÍÁS<¹cG“Ñ6ñ1IÐÇŵV0Á×1ÌHD¬þ.¬þž^{ƒ§¯ ‚Z*·)$–=ϱøÚ±“8ÉàD CµÕ}¼qϰ{Š:að·ÚSÔʪÚYMIxÔ NA€\ÿ?7¨ÿç—…"ß1D¶ Oź&rÚóQñ˲”,¾Ðyú;‚"´ïÂ|ó9oc¾ Ì—&6DÍ9‡ó wþM ×¾YοyŸq+ÿ¦yð›Ûù73§^knO5}OËú‰?ÑJ°hOLãß¼„õÿÄŸh;TMWéϬý=Ÿßµ¥Š%f–S;™:ÜÇ~8ø»øç¤º8ì³ÈâõüD$ Ùå3È’" 2a—8‡ã¸•üÓŠ°^:Mp3–W<˜‡ˆ²[)y@ţȂÜX^3’ÍIôeÆëôÝ…bq0 ÊÄAd"L*Ä hæ~»0W¼K¸SÜ*l_‘ÎÉJ—d7ŒðEE+™J™R¡ÌQîV¶+/àëm|}¦„3ÓwðâKIûûK¡ ¼áÿ]èF?e ”bihëH*o-mÈ?{ãþ¹I½˜Öa³·/<ÊÐÜ\àŒ›;3¡§æ,{££Däé˜SáÊùUqhžÐ¨Ö·Ýâaè<2Òn0G›£°¬Hi4ndËWºXѸuíD¨¥ }ôßÍjýûéµ1ÚòiZ$ˆnÆùQ`h²“ ¬DVD>°¢:°GAZ¯èÁÇ<ÈŠŒÛ §G”Dð‘$ñ¿Ž«TŒ—qC¤2y2Qœ$UÈWi®|—t§¼UÚ$¿,¼A>>"ê¸2¸/¨€9p7l‡ðõ6¾>W¯«´›Î¥sè6¶Biþ-œ»73ÔÆéZóƵh0’*¾)® i ŸÇ:ÍÂg@6I/²põˆ;ˆcFN‹§t§¥õúÓa> žuïÏ2˜E3Ñ« ¢Æ»!GxÕßz’¸µfWÑ+âpz¥f&mö¨X÷¡4‰‡¬½l> ÚìDúÆøÂ‰lÔ…é…0¨©¡þù¬a²{Ÿ(Fñ´n=’ÍÉÅoùŽI<’$Ëž°0£‘ Ëu…ý?fÁÍ0‰Z´]ðV1Â+·>Ë4Y2ûGÃD¯@GøÖ@Kz…NUù‹ˆ‹€Wä8û|¥¢TpL+IÕg%¸r}šA Å–ÖÄÔÜk#1®k¬ÔÜ•‘Î|µÎˆí+ŒhÛ!+ðƒ·¯Wqý,ªp/£C”å7ö#ÙZš ­Šfô™{ ã ­n®+<Ö"iôh„^^‘™8z´7‹.»úàùó^…W8þ«t™$Ùãl®ìg·Ç »÷ÁAqöšGf=nˆ|dVÂîÝø7ý–~÷ÈLïNlt6JàlÍh6~ŽŒ"EˆÖåpÙ£L.®¸ähËåšËÇ–… Ÿ.ŽÀáû(ût»8‚ñcDØéH°‡Ëë‘K"¨ÌeÌêíD¢Eež_Á RIVáñgéjXž1 –ôoËéšÕ0¶Ò¿õÝ,¶®yusßß`$@¡Ãé9‹iÛˆ¬*'U©=Š ðGc@Þ†±xÑÐ+&2:àæe2¸£¢-,ôi].w†% N$ñjùecŒ#Žš$S¬@T‡v™Í > ÇNgƘ”lez™­\ÿ€TÏ“,DTÿÈÅ´­<*ÿ9º{;βVNÏS5§A¢5§kžò8ék´†C>w(&D8ÁbƈÍyˆËâ ˜˜|<½•š.ýÈÒÉØÚ¯Y()¾™ßÏ‚Iˆ(x0ÁXP!† †2Mt_Lå‘glfžïà.ÿú»g}Ú«Wø›³ût÷Ððûn^½zŸx~Pû³ŸØN{obm“ 3¤¯¢n@Ô†ù@HX1øèðúôÒçÓ¿_ð3Ó„xlí@©œØÈiŸ$äµ&1Ân¹ˆˆQgU¿HbŠ€é’qDþTÒ±%ó[[gÄ ñ ¡g¿¡ƒ}‘Ú>)€¤þÒéuÄ£×ëT†¬çoö÷_»…0T?F £ÄQÒ(ìîQº®P¬ïLzˆ=„}´$­ÅÖB i*fè;!D‹ñ‰„ FÑìˆc0èó  ú"×Îd°ƒ Ý®úÖêýÅoJów‚ &Ð|ZI?JŒËO鼫ó–"ÑBûŠä;!ì~lñüÐ#d:ã¤+É$Å¡RŸ=&Åo‰N)2–X$“+¾ÈRØ”On‹ IË¡ céÕʹ¦Ñ*TGîjšXmÚÒT L^Ã=±qRëÑZÛ„R½‰ª;Ÿ&ýðˆEyD_䉼&"Èk‚Òòœä„d_ò²d‰‰îÐ ,S°l¦$8•g²Î%¦.98À~`½ úò O?Y€À¸AVOÂø¾òWg0vôÚ ÂW@Ÿ “©ÁÇXL”Údt·$‘ˆì`4vÚ³¹#£,h–rñØ 1S4‹âˆëçÏãÔ‘—È—î“ð-.¿… j‹Í(ÎBæi‰VC+$Y.C|%•ÉwœÍnÕZ]ŒUž•Ê‘OWt¬Ia>ó€ †S_J$äøŒqEF_¤Åd³xU>‰¤ÄT›œÕÒ9Óë§Þ€˜…âUby"¯´-É¥³³FJà(t\³héÂÔ”—î¿#€ý-”"ølQÏ.Í›ÖÑk*î·ÿ¨ø D1@p  d0-Ó ·[áÚˆ\U϶̶—ñd‰ãAì9§ùtħúKNúU&@í„e ŒDíŽØºÕÔˆ–ò6lÝúø#÷¬{Joz䎭}`¯5橇·¬7èŸ ôR N;²|3´hÜoŒÑ†"s B€ÆXjC³Ù¥TÛHuø¥¨#¶-1œª(^Y‡bcé'9É¥Å_A‘» §ç-Y2oÞâÅBåE ¯Ýk =ΑEûª«÷±w̪uÈ]U+ŒÚ¤kƒ í—h”L¸TqzÕœg&‘KpÄ©ÿÄ´Å©ŠN,Ÿ3 ¦Q†tœ–…L!æ+ÅæO€§7/Oç'ŽêÙÆ.KÄ@Š 6“•ƒx^<<ÀB ç)ϸpKö¤"}à4ž3*&3¦C [-5ÞÁ›Ïjçäà~'îwµìö¹]›RJwÒË—BEò9Û Ëïî²ë¨í#ï¤géÞ¾£Ï ×üÝ£Öøˆ@Vã„>/M&näxC|³‹DF{¼R‘GtìEa œOÕ£\Ó]Ë\Ú©z”gºg™‡ª­jœŸÉù‰™œ‹q~¢ÿT10Q—;_&^6t9lÆËH+²æ^Nf¡ Š“]S%yóºÝ4‚VшÝkö¸Þ•ùwÊbŽ4yùFú1µú˜H샟ظ]žXòÓ8X _Ð?”wé‚Ñ¿ÞyÒ§þØ»1ä’/Ü*FÉ-è‹;–16ãÌn«îCvÑŠ_×%Á}$VÉ`þ4–àfsß¡[’Íä‘b<6 ˜làÌ‹Øv„ =ûy”àpXÌG­6û%ÆHfÁ|4B0›læ§v§ÍfÕɬ{žÆ½Õü#ÅhP'$>àžfc­Ns ˆb¸U6‡ÛÓ v!âÞ(±ƒ„ìq Þƒe2Ûs1¸Ñ û.b²=œÐ5ÆãÇNõ´½Mï€'ñöÈšñöª³½…«°¬æ':¿OþŸoNW‰f¸§îë_"@îÀ)ðÕßá”L¢Þn2Ø"L&‹ƒý2ÛtÛ([@¡"NG‰ªBÅ$ÚLGÄú‰ÎhøÔÌî"W!ç=–à´8‰“‰\Ÿ¢½ªgA½ÝÇJÍ8Úf¨¸Æ÷“`ò_÷ÿ¦Ž³.8üÿùiœXÎÁН)U<æG™Ï“ƦS (¬šbmEn¤ÑP$qr±æ"?þwfŸ•&“IP,aÕQ§r$†¯'5ºI&g+™œ­²dµØ½Š}†E¯Pm½6¶Í‰·'Òã´ø,ä®\7[† yöº•{–Óã Ó«B)HG¦B_xàÏÖÐê:½TKüû˜ö åÄ•lv^;FÕc˜ƒz 5ìàéƒ -öWµ$K‚¹5Êc!¹yï¨é°§W•˜´Œi™¹¿—è/Ó2bŒ#!^ðg‹Q4šÖBÿBvÛåÿLðúÛÕÜÀÞŸ+¬Àó·HfÔ~'U¢Xn QY2 -ÌàF–D4/ žß =¼ Þ¸žvûüÙùÅB_höèHT‹m»wšAg5$Á–ÜXÚ‘~/”®™],V6¿mé29¾ý··éÀ?¾? ËaÎéïèÊ7é+Òë#Ö?þ"IHÄ5N#!_çq÷TÒ¸‚Ã%è81vÙæb~Þâ.]Úð^Õô|ò…§éð3„¯]n0,¿‹^D}âú*ô˜Ùº«¸uyÿ3G¶%uýÈT4úÀ}zñÃst]òÊû@? VT`J{cAq3bEf\1 Ü’Ý–’Ïb‰âÁøÌÓíšÒeM3ŽÏYÐz/lì46Ìn{pZÓ©eRe·7Ö?êß' \pü…Ò–Ñàµ9³è*˜ÛÊ’½áþ.›z¥Û©#ÃÕZãák>À¼~±õ²ô5‹Ø*Žçš^ ‘ &CunÔóª°+\…JªR¬ê™Wü¸}YYb,­zÉwƒÀ÷¸»žXâ56±¬Lë¼EÇcU·e MÓ4x ~áN0<µ$ ¶ÓÑQKž¢µô?;‚( ɸ­êXGæêŠÓï~Šc4°MÙ› ^„ÓNqZrs¬^ö£¥ò¨¶lå$ò5Õ¶Bú¨æë„uÓ bÚ:&nUÞëèç€{+Ùöy×6ú2¼J[Cá6æÄB‡Ë+¤±LííMe.j:Á›¨Æåk™ªF÷ƹèÐؼùn›§|VÃ¥æáŸRúôæ‡ÆQÿ¡¶Ï%4ŸÐzXû±°)V1=4ăHŒ»Ú­ßêª;Ý~¹Õw_úà©ôé}·t^»|*$· 7Z¶÷…Û¤3DÿîɉÃ~lKRAË´K*!¡—Jöº!áòŠN ^+¹èü¯×?ÿ&L`÷Kçï©¥t+žŽ„Í×½Åî“ÞzK½_zk˜˜ÇaQM$®aøQ­6^™ŠÆòâüö:´Ÿ?·=þHçï·{÷.HïÎ]»Æ‰‹–ÑSe–"äÖ¬ Cᱤò*!½ ÿù/U!f “§ÊïskáìKÀ Os4LЦœ×*øeÀ¼y3î]2pİÒÇ‹ÝQQ‰…ôù¼öÁéðˆÕßþ°üvI:fσÃJ¯zâJbS^€ñ€ö(@¯h2l)Î)ï¿×§¡74«h<ØfÀ€¡MC«ûéž}ûÖC/˜/–·üðŽN=¡^mKY ƒåµkÖ7øÍB!<ì\™¸T…„.°6Ðy€»( \ Žu¢¨óªÂ¬1©CS›²ù £F¾Ð±Kçn–øÄ–CÒÒ;K¯M©0Úc~ŒmÝþ®=7ü£Þª¨0?™MmÚ ‹‰$gÈïó(¬"ˆ »ˆÀþ,°>lÅa—‹æš¤&¬¥ÏN¹£u™?㉄øÄ; ë”EÒú0´õÏ^’™FkІ…:¸:µ â¾XúãØÄ;Ñqð+ ¼¢ô½B/œó?ÙÐì’XU3NX(VúïÂܳpŸÂIìU–˜tæMÂôLlWTᲬ-;0•¡0ŠþòéX  ̈(ðòoý¥‰>zÿ³GÆÒ«^é-ÿ_ðöä8p¼Mß¢ïø¯ÐÎô±ëBSؽ´oλþW„¼?7¬c>'Íé}ROiCKÑy! ¼Ç8n·‰IŒ«Å¬®%ЬÝ„¿¾Êzø¶…ÐùÆçË·¶¿a±¢Ó\Õ jì? Ðòû‰CHÇF÷š |êöÔÆ®mKˆÌF-€ÔÒŽçWEDîpðj¨AÈÜ&^Ûœ¾ÙñHËùJøŠÕ ²2Þ~ˆÖÜûЂ®3À×qBË‚Ö&¢ÞpÑÔñSÑîtbGè0¯hA»âOOÐO6n»õsž‘=9rä¡gá¡ó¶}Y1}aû3ô›)·Mºíñ_~Ù÷îm³ÁúÌvèX|çzý%úÓßî¾{LŽy—3·+¿€TZJ医§¤¦ ²„d{'R,ŒRÑkÃE!0:9•RÿÏ<±¸ÏM^ í&¯›%Aê´u“¡ÝÒV Ÿ½üØ}k¦ïsUNøœÖEŸžþŠK-ˆ$$ÉÿKùì£ÕôëÅfgﶤÒÿuœqK[ ¡ ÚÊ37÷ïß¿dõï+è²Ë'm¡þmUU;ð–éÛeKýiÅ2ü)\ÿk…°v^÷¶—>‚0øâ'W®ŒÇ(‹Öj°„ÂZBb$ÃÄ ŠÌi­ € í²Q¼$±Â… ž8„ïóý‘rÔ )å…‚‰[Y¦A·³ïõnÓìÓÇW®m‘Ôñí’ICºI€ Â1»[þƒ± Ù•›b•—€:pvC¬Ë„^‹W€¢èWÈ¶Ó B;pP²dÁP4a½£ÍðÐrÝöUØø“ôÕS°©÷,(ë»8¾ó ÓÎÜwÿ/ï=Oäž½eÒi¯V=ÿ·j ãïüãË_xXä päÞ];NžÝÁî G²ûÜ ‘ mÞºä¯Î·ßûØ¥`ë7°_›übg‹ îŸ:õ…‡ÖÓïˆ@¦bU_âüxŒTošœSb}*w äׇÁUÈØ§°ú=á¿§rõövÄàP§e´y`HRôÕ«1‰åw6MMO‰qôöþ¹Þ6ínnƒ¤¿ÿΆ³0&K’dho”•Þñ©ôiè™îé­ÈFzF’¥,”ªÛfÓ+À xÆ"Ý”©ô¥ä†Ò—4Ï+Ðç ¡}…–¿ßî/»9˜ßðt´º¢’s÷ÅDmÙã{<'9!1:ÁmíèyÌͳãnM~ý‘GZÃRDIüE/I¹®ø_ŒwåJ’þLJÁÎ~¼õë¯3igÿ³ êÆCƒwðUÃÀ Þ¿ µö rg1ð[Ë•So¿µì¡‡¢'éögžnšºï8 yãžWÒç?¸~Í ³&Ô,ëÜ©SçeË;wéR,^èÜ*óþ'~£Ÿí~´ª r àðŽ«žˆ7ãM£ÊjîB`&dË¢t ·‰Á‰Ëú¿CœF›`Íœ;zÚ³ÿà>zf÷®ÔÄÊÇ!vÛ’¹}9õþ #¾œ­†¡o߾ÿf䤕֬©GÖ¸²Ú"Ë2«ÀJÜÒЩš¹ÈméßÇé²ãôoxV "~ØÊ[QÒ7+áQ˜K™¤—ÇÊÀ³IÚ³IüYi哯Ðm¯< ³a<>yߌ›ÈÆ0C{rzí/\ºfpNÿ*Wç„ÈÕV%‰‰øÂÚÆåiãP—H»™ ¼q9:˜F/·€Û³í\ÁZ̬.&ÚºU¡9¨-Eh{ÂH¢bqa|Âý@1À#šªt°À¿–& ¯~½hÑ× eÁ ¦mÇ÷µ¥;ßþAÎ~‚žybøêÌQöãÖ <”•Mqàçž„„š Á(~¢¾óOP™O N‹º[ž`ĵ§¬¢HF®ZD/<±gϵrñH»oñÔ{Kz‡ Óü„U­›;»·a‹ÚV)´ÀˆtvÁo´y;øfÂe˜µÔë2ambm+ÕÚ qïRòä@MA´E¾E<*÷ž=w]U˜ÿaZXï’{§.öÙG.^ Y¬~zaѪЙ\Þ¥³°‚™¾^›Ñ/رvÿ»X{= ¨GxÓ¹’›wÅ©@WÈùUrЄ¶u@¹€R%æÊµ ßC)”Áwô!ºç7º‡>(¿ÏîÌnfHi?xêÖ |xž£âÞB›œ1¼pÏZ ûwî„ý´rÐìò­ÞŠ‘þz Aß%鬩Þu$ÀHáCT¦­q6Ÿþ §§6C`>½‹VNœ(Î׿Áï!óƒ^{qÆŒ…``~ÐØä6È÷cXÿÇz’ Äó %Lí<åÂì·®_kö…)“¿^s?Æôž%_÷‡¼îAÿÍ· ïÜò Z]@¼-ºíÂ;­Bº3U\§Þ ¹¼›µ:-hùŒéhÚ:6h ÊŸÉ\°‡ÔO¿,ÐÒ¹=!­PÐÄŽc˜Zx¨zU @†…Ø<3®§ŒÙ²™‡¼nßN§p'¼eôZ2[ãéŠñ÷Óïî¾’:uZ¹Joò¯‘C />îµ %S‘’+Òs0O3Z…@;I$éè3¸]¤¹U COÑš³95gÑ\Øé:MNE`Œv½¨·Úeñ€Þ~ ¢J¿%JþyÖò²ªÏcœÀ–µ.l†•ûªâ+ZÏ]úðíþ¤÷Ð8 sÿüàí/³ ÕÂà_¯§oL‡j¨€ñP^ië¿^ÀMDÊšó Ý’Fl4;âçg°o²¡hŒ$DÿŽiž£Üs42‹ [“mLÓ™ËÂ;KÊ„…3¦ß¾´Ò~rÆÈ8)­Ð*œ¾ ÊÈÃFÐõô¦td¸ÿª´‚¤LH:B´ÿÕÏ~¾ñeúÛYM\Å«AVÀWL †&À>(c-KÎøû%Xa…V½†7CÙõEVÓHi¦ÿU¡µZº°Ð¿F³?ÅùÂåoŽøåml®HâT Ëþ1EÄ*œ4oú£ÿ˜š©ëTÝTœèOr±+rk˜°Àä†cûxúgò n8Vˆé7xú1žþ… s¹ÿ‚ü‡j2{­ö|Ù°B+XÁn¥™}±tøfFCûj"8¦G‘4¼6}NÊòœi=y.,Br¹ W¸ÍnðP‡œ0; ‰3aô·ª†ãæ·i“wûÔ¿Bšä¤SýÀÕ 4Ï1,«9Qù}Ös'ÿ|cò܆öÝ!6¦2qã=ÓiXOÉ¢‚4°°aX» CBJñgeÝê­XD_PåO9H–/\p…YLÑ…ð!NËåóhtà O‡­7ÛM£e"ªýùÖ>;åöÀç¦Ö…Â!ØAOú¯Ll#uy-fc™X)næ_~´ËÂZˆ¾ÕÛ_é ¸†vÛØmx’GéÐf2*†°Åe•f4zQ‹’lJ1Eu’ G³Ks˜ ³ïV£¹ð¦ßä1FÂŒzAç1bœ(C„Á C;éGfĬ·ü(c° òŒ¤#Þ•a?õ>»M6Ùåp"‡ldƒˆæ”–îmê^¹+7`Î Ã¿Ò N‹wÒ 9°ßù?ÆÒÁtÀaºì0 .Êü[„‰ü½·f…0÷%Ú¿äg"Òf„…Ì,ñû3ˆs¿Ý°ÉxÈ´_IØ(v¿ý`R•²¥ š¢:ˆ%Ê•jq¡]X|‚!®‰¥æ«³Ìb‰s²@€–ßx|JËÖYjìÑè“z¯cÆšŸÌW¹"þ¶ùaºŸV=øýÔ©ïoݵëñŸ=ýÔ½w¯½÷. m$t…^w‹)i/nüô›Ôdh‚°c+&ý5lDÙÈìDˆv7yáôªÇՉǗ’´óyÄ|ƒÞ Ï—D)EuäE ’^Ì&úlY¬ÇéºÇIg‘~dï!‰jDzòôAÿUÚn♸DjºÃ‰4²Ò¿[ñÍ9_ÑC|hÁ’ba&Öu',EoI$ë ’œ4:¢÷ ¥Š,z$Á#ë  _ÿ˜4öº¡ç÷Cá¨ÿ)aŽw‡Wø¥æ]5k…ÅçhoŒ©Ëß!M&’ðŒÁ¥]2 ’ „ôŒñ´¼N=Âg>gñ*ÃI+¤4º ù;$ÅBO!¢±ó_¦òµ‰oBHCûô!Ò‘X_„„¶å‚"ºˆäÒ‰z>&l@ØzÖ–0öÿ2ÿk¬A¬Ò[½å÷Ú´7”àÚ‘P9/Öi!íŽX”p+3ÞÁ!Ð¥(§Í¦õ2·náw¢h¦ÆîBI[»ááa2Fóâ!½ôØç)JPâ(à/îJ/\@÷O^çÕëüßÀЊࡿæßI]Ìf©°°Gµ\×­ån&äf]s{”árͤy,7¾ÈmÔ9‡‹0J,ñë#ÃÄ®¨Èz¶ê¨`±F€Ì¢`´ð:%&1È(&³?Rà·m{öl“tì§¿-¬€é0šnGã®eø~ÆÀtáïMwF#ÖlÚ´&¿/âNqÖCqôý_?Å>VˆÄ—-®ãtÜ«”_øÌNöÙÐ%'Lì>—À'8Î(2çüom³ÔÍÛÊ9²üÖÍênv‹^Q73Æ~Ùi‡çy~²KxL®K±X.cI985ÃO §"tëq×`lËJ ð»ÿSoŠ#a*ÞæûoàýñúBaÁÍ u²R8¹)>»QPÖýiY@[WV"†k7«œ^v± !°|þ+þÜ +ØoHb{¯J¶ÚÆNrsŸ;̬¬I8-B„ˬná…»#ç<¯;"ôeHÁÎøó­^Û‘ÔzØ[U‚èâ¸ä Ç TD’Ž21ì d»8e£LÜIªX¤­Ø{Ñ /U|ßÅm•û5"Ü$Ãç2pX×[ˆÞa¶KÍÃŒv—Uä“A ­Ì¯pB%‰š€&Kå±äjNÍ .JÔ ¯¢ø€œêÕ¹“ U¸ˆÊ?cåÄ΄ØÀÀéZäþ´U\öõª5©æÂQZBïùóXƒÒ/$¡L yÓé(5µ AÊÿƒ…2ƒ¦|_œó€Tå9`Üâuœ6Ÿ²­·Æ5×E6 ˆxj½¼mmUô·ˆá²y¾v³ExrUãz&ÔÄ"xÄ:ªãÇc_`úTñ˜v&.ºuòdÀêäÉ[þ=ØêÇ`dèu» à6>Ð.d‡ì²’æ“Ù•º|ÆéA‡ò e½ÝÀ¸äEζžqzŒ.ÐtÌrƒ¡¸¡~·W-Næ’BI OŸ^8‰¶†9%_€Ò„ÉtÙ*zT"†Â²Á·*…Û3>ÜøùW73„<ÿšáe®C("D3rj¯ BÁ¨) ‘ž^ä.I,²™DOa %güY$ÓQmØ•‘R_í±9ÎEoÉ@¹ ukæ­Vs#ËЧ0M°ÌôÆl„ÆÙ¡vPÓtæ6S6̃rZ÷-¡·ï+žcxð dx‚~ÊŒ „‰˨ÛïÔéìòëYMÒ$´ÖH4gõÙ8,4ša*¤,¥ˆˆÁÍrp ‡‹&W¸)Ö¢ »©£/ ­µtÕ¶#Öêî»<°öÀàå :Õ¦Ì+<ßÞçkÿÄÓ}úöíó4­àïÏú÷{Û÷7üo Ç%4çušŠw–íoŠË×oë?;6æþÉ;·‡5àbs5!‚d"–úb3ªËö‹ý¨S*¿R³„d6Ep3|(B•šÍ&tÙŠ°˜¤°ˆP©™·D:b0)öˆê° ÔloÖ¼:y’PMè}jV¹øÚñZR3£–ˆ3j@ ©~˜t±f‡ÿÄÐkBŒPOïW3ç0ò)R™u—¸±Í¾ Ìlúw™ùÈÿc™|†h}Xx˜É¤—FàVŽÆçÌfÄÿw²4Ü¢Í`-,‚ |¯¥Íðó½tçPZ9”î¶Â«,ö·öO€sƒèz˜=ˆæ ãpÙ$K‹p^µõ¥¾ž¾6bˆôZ.Ÿ=«Í*—á¼þ¢²=ìœ7¡šxÝç#/FownñªÞ 5‹EU^æüO¬ÒÂäYÕµáM‚„ÝN¹þòíÇ>¬ùö“?éÏôÊGG?¿ãUôóG„ã¾fº€˜µaó+¹í®>yÀÈ>9LÒçN×Á§zÑ Á.‘t¢‰èLÊ¿8[¢Eµ¾¥$‰Z€Ó: Yú‰VÒÍtöÓ$©ÕY¥þýçt¿PpñVâPBæ˜9ÂòVí%ͼ/·—œ”qª7.!ù$!ó)"šE£Þ¬Ã)rÌ)Á¨G„ Îû‘è À´ª2îÖñ´ÕÇwÀ">¦o—Óû‘Ô™Hj’ŠÒ‰­”#PM F“.̤€% Aç‚|\P\5­y(=N|VÖtY$yîxæ™[hh›B»/§5Z/\áÊÂ>GŠQ'ˆzoV ×™$#v‡ »ã2/`ZµöÛ„–z£Al‰n>BK´4 õ„T»¦r“DÖÀªšx˜n;ùÃ)Ö8ÿ! ‡h»ýw}Q:Cµ ‘#:êLrÝx\¾ž£NDÞ Áà7–¡3чYÑ̶íÖøˆ¦5bì¬U£IýÕº#‚‚N2é,JýÒ'RYÕìÊz[^&Ê+ UÜX‡ë¨äuD#?×U“#2`% û¬Qh6@½oУ™ÀdºNâ|T\…åEñˆÈÌF~(·AšIËtË82ÉÄ ¥–æE¶äähÜÓÂÅ¢øÂ,FyN2/ _Õ‘M•jwô…ØRmJ¾`N<ù©3vKØ®,ºþ«ßr¾Â¬–¯snäd¶¸+èeË›p/Óã û€W62®|mî¸paÇ)íÚµƒŠÉ÷¿ FÔ÷Þ?™Vâßô0âßB~;s‰ó™­t¨é!ÇDa9‹Þ·é1¨hSr ?tèô}õðx÷Öubé^áÔªþ¢¯–“ˆ‘dY ÿU—®Ã¨øëk¨`¿ˆ8ÛTšÕnÐ/Ç‚¹„¯…#æp=îÉÄ„%+&d—‚¥Q ?xäÑŽ=ºå7ãð7n«¼†ºžH:À!.‹8I Ȳ L™ã²(IâK}úöé‹¢].|ýÆ—¿ÿyõs.?|.®ºB=NÚ­²Z;Ø’F~‘VsÛÊÏÑ¢Ñ?-•s±Þë³ÊÕ¦°#8Q‰É&‡›Â,šDÏ] ˜pÃyV¬ú@½X‘žÖ÷é§û¤§û‹¡â/o_´èö/ýoÃÈñq7âÅŽtЀÒcÇú÷'@îÂ…Y¡FáB$x«*ÙC8|lÚ@ßÚ°÷óƧ{é;÷¬Ø±m“‰& ›{nÉíéëþèëí!eì¦j(LÝT,”Iòí}qè Ydm‚‰0‰ ws³0˜ÎjÑjÒs «‚µZ GL[™_ýv‘c%àΙXî?„úP±9ÛŽM› ±.fö¦Ž‘ ¿dÉü¹‹#»ëpúÞ{æ†ûNÓöu¦øHnìõÛù ºH/&Ë›Ðç«¡,_íPª-Gþ«,ï3'¡u<2"Ñh ÞççòíF„ïú [±c—AÊÀ¥þ"xiåÒ›íæsûšçà³ì}“·W!ƒFˆ‡€à¦Ü†ókü–¦#yˆè‚éÙÁô‰äeÎÒ‘9ULÀÓ‚ˆ:9ç²a2LMU¡çÁ9“~*àgjyäôëÿ¡wý´xÁˆÐS§*;«]¡½@DUÃËôhÒöw>xné“[_Wi²B:'é^õr'ÑðyIù Ä EŠ5¼P½hÉá-‘Õ`=o»èÐW.JÕøÐQm>gP/ZPùÛ j#¯Štt?$A÷¾ƒD‚Op·‚N!èwÈ[þåþE†·nK?iš½˜ösßœ¢‡x²{»€™<²~n¯jŒ”H­_«9C/ÒòØXˆ%’¨ QÑO!V@¢ë’$ë\¦/’¬¼c½\ó»ªIÒö«»ä´Ò5øŽ ‹ù^õ;ît † ^§ž‰…gö<}äqq3(¢§Ð— »:)ékt›äŸ™Þ4»f%Ç ò¹âEeÜZ&ne•ò]ƒœW¯¨éºOžñLbº—Y;>†¹K¾žÌ®°çuöÛ„…[ǬîܾQÐê§Àå_Ã4OßW^hU@ßèÞyuÉmÌ<—G\v"³iªâ‡Õ “ÉwŠ*ÜA2ÄSp/îÈ.ÝTZÒÓW:¨l}gßq-ØzÉŠ_·õPïM¥Í…'ÿSÒrPTT6ý$ÞbÞ¶ð%ZŒë§×‰ùZ`­¬AÙD`lºÏ¹æ2‚ÁnîÂÐiÂÝçô.úÛ!ºåýÞ% pÞúþû‚Áÿ'{c£JPÞ‹Eä¢Ì•ÊEˆ¼‹°‡2—¾|á~ú×oÓ—„{!M©Ü ãh{ã!á#ÐÓ¿T ñ-~æªOïNŒñ­Pb Øÿ7ë}áH©G–°Pío¡µFç?úW¹ªÀJ^ pN¦þÕ:2AGïäüç&èêÌæö`;x)Hx(ÆÍx[~á«t:½®»g“ Ð)¦‚n™Ò~$}w`›)tóù™Õ´»|˜öØ1ó¼ŸüEÍßÝÑ+ž¦Â ÝîømÔgå ú ¹³Þ‚-ðxÔÎ4)kAz!¤p«—WÊC·‹séÖ#è—~jw¿éô©Š›Þw=Õ3£ãçí›–ˆ«è9ÿtýê•&´õŸMëUE¯WÌvÒ³ÐÆ™9PC$¶Fà†Ëö 0PhM!ÍS"‘ôŸiˆžô­EõZš$Šð”ÿÕÀ ðVÑõÚ;õ|’¶K<ÃÛ˜ÀÛ(7VwR nEf-íCgñúú6û€µ÷žÀˆý³½Õû÷ˆ(“nèV^X+œÄ]C…5@¡C³ôÃTU•ŒÖôh°‰Ê¦5a4Û@'æ6ƒ„ŽZ’tcïÖZ>Úol~§Ï·X¼jÜÈÅÞÓ~™»»¸d½D‡â³Ý.Y†Æ@[ú×ó§6¬ÛväžÒäĦc²bÓ -,åPM‚–pìÍEÓ"u4Y[¾›f”ż’Wï+Ñ"wÈ)¦¿„ãG@tºÖÑ%ß‹KÕ/”3þ“räß]˜Íz?:bÂIéiZR°mÈ~rr4›IåÕk`šÚB¦&½}Òž´e }&6ÉNZŸ4aÆÀ¾ÍºDfgG½?f}Q—É]»¦´Îίl¹Wiç©OaçGV]¥53;ÆcÒµ°Œ†Ÿü íw ìnÞݯÓ{ôdv91ªO)$§Œ‹1|.LkYøSt8¬´S‚Œßšÿ„*'Ćë{‘fyÁ †ÀÄÉeÔ®®_Ô&Ê<4‘ž¦óér|w… àz–ŸÝÓ­Ë€—ÆäKubYÍ»ôäãv|SìRó<¼ûZ»V-ÓRìÿ«†Qu5TÕÕ0.Pƒx;¯I0¬é¹[]ÕZÉ…ª5„¼ ºÙ6ùgÆ­™ó¢ ëÓ¶ða¬û}ÚþçÆùœ³šâ¢ÈÃ{ÓÐr­“ž³nSûIž%- ”ÑÔáäeØ­{úî82&¿Mt„ÚF$¼ûéLË/œn¤ŠSÁ-”®ÉÙÌBIÒqjyÉÌíJ8%—‚éz®Ø(§Å†0¤ÚL’ÙÕ(0±2Ad¦¡6æò&ìÆlgwZøì3h¡ÁrÜ|6r>gÏ·Kï­ÙAoз¯]»ì`Û¾–Îs§Íœ•æÄC3ÓÜòÚ¤¨ß¸=o;½¦aéÙi©^Ü%*Ž.¾§Ÿ\¡·@ú’!å È´æÓut[²¸ëƒÑÑÖÛé¡ÅÝ,·ò÷õúD{'$^ð»¦1X Z¬oÉ]„<¤%áUêbžŠÁ¨»÷¿Ñ>áþÍŸ}¾ùþû7þÙæ¯Þ#½‡wä¯C.BªûÕ¨NË©£ZŒ¥©ûÙãO:acñÌ™Å7u™9³‹ðjc” \rÌÁÎNä—(LfÈ ZØ%!]Ì>>ïå Jup.H,{¤.L­"%õOL “Þ@Ï*ºuhr‚··7!y(ÝúæùóoÀ¤¡‰‰3úú’‡=ztù¡aI q¥q IÕãçƒC“åŸ#ÌÞ“§èazèÄÉÄü—ˆ!øû@ïS'½fzlâü]Z0Ï˾ñÎ[pé£óÕ«eƒ´ˆ¡®¡ôi†Ò¨ÓšU°(ò&iI¼­œ¯a[ ä-hÇ|¸rPs›[›lÑëò™CW»y­ÃÍ)˜’b¾ÑZnصë^úF¡‘'%89ŸžÛ°{÷}—È&Î/-{¤ºl Á`³†‡[mÃÀ²êGÊJ#Ôïß]õÝõª»ÂÙ'³CÖÝUuý»ª»µ/qÅÌ©ýÒøºî6KjÄ œSiR”ž»Kn]10µfyí5ƒ lÇï«ßëµ9ì{e0AÄsõ˜ ëjIµê¹æ?u^¬òŸ–4«¾ÖAbúo„¨Þ'Ü‚0J;ˆhÓ (ßò;FñÜG—gÒù¯ ñeP¢½~’ü¹°÷ÔïhB,ýëÓ×è_!@ ±„¤°’yÁÿŒëþ‚&t+½TõȾú!a»c`Ñá°÷Í €³*4äë†þ¥Ì»ö¤ÔTü†‡>@”á4þæ¨Ãü-uiÁ%öžüë¤Ô¿F‰Ãv-½°d÷’ ‹Ÿ^wlÉ…%ü[ÙÒGµý”¹ ‚¢ã¯†N#^þ+0Óøœb¯¤š‚!Ü銉nãr†‡…ññÂÈF“åŠ(·Ç¡~vxbââcø§hWHb|œšH æ°CF¬I!N Ü{ìq|¨þ+ŸÁ²ç¾äaö¦¬~‡t†¦*]9‚mVžWmáw€íAx€Þ¿¿zÍí’OùÔ0y^šJo<‚MFä)¸}Íjï”NƒÒ‹‘h¢.K)NŒ=‚î¹ï|vß ] ZÒn×éÕ•ïm‘®ÁÄEs`§{øL]øÛDÿç,ú€hÑMåÀ¢Iؽ…¤+@p'ƒìýпåÿé)!ÓûCýo­ ýdB?ý5$ô“2&ôÓß{ÿý»šK¡ŸèZȱ»öÇ}Úïšg$HŸ‡$„þ&ú¿ôþ—„YúæDƳbŸy°×š¨ž1yŽ ¼N†hœVâ–Á­“ÓR@—&Ø¡Àm׉`MB v°æÊÎÿYúËî“ýé¯`Æ_b›Ó·CÙô«›ÞÓ·Ó½w@,´>½pú{¤rþëfäR×~?óQS:ò_á‰ò·<Ë­ü—œÐ°$ñHhM̸/-æé¦ 2Ô™¤©—'J‘‡éFMr•!ÉÒŒ>xˆp86* Xl/Ʊיwí¸‡jͱn¬XW¦™xHƒO¨Ö Òà3Ï?¦æ@Àăp­¥t·üˆVE.ê±êÔÊ<ü­·® §uøðÖ«+çÎ]Ž*­Ã¨!Z…*gT=¯bNÔO îsìHŸÁb90]”ߡЛpS3]y„›ìÇî1¥Z#/%ƒë=X»äRâ_Òí3Þ¿¤`îdˆ]÷¥Ë„eàÙ²ÊçMOØÆM¥£¢Ç.¥mJühåºlé÷'æ]@ÆáRZî«ÔjÞ|.¬Å®èÕCX0 µEÇ«»ª¯L»ÙMSîÜøBçÒ¹C‡ö`Ç£C‡r/?›nÆFXÝmìô“)¥gJ ;–øŒ‚Ì£YìêGwûoîvê×, ×2Ð+ã3I ³®%ÅÜ~î²zÏu>+p¥B ‰ÙBŽß­Þ+¹R}^T¸™¬˜ˆåϳ÷~aÎflmüßÕ°êÛ`WÇp;Â:íD9wŒS´ˆ.;v½ÒaÔš_a.$tV‘$vXæÏ9ñÅžcÚA):¡”¢[Ä~4lW]»B;À‹Wà 7³ªßÅÊØ¹W Øü_lþ…vÜ¢¿1ƒfg¬Úr¿Òˆ½?³@æzÁbé8‡XQ+ó@ ¼1ÖÅg×d 6ßœŠÿÞ<Ò¾¢¤¤â^ZÉì1„-óæyôê|ö#?ÓR9+4޾Àˆ# µ2Èlþ¿™íBÕ«tyzㆻ´è 35ßÅZ®q³‹†µ8Y'ÿ«AÀ=´2¼q“Úù Ó€ÖÙ5 YC§3aKY@6p0޽\¼G§KÉ}AK=°ä6°üXÚæP{UxôMÖÐP‹Uÿ>­}Ün•kk¹¥/'´U¡×÷0§Oxè~ ü?Ä$ׯ‹)À *ªÎ]• E› {ЄöîM‹TŠ–B§½ÐÛÑÚèq»„Ó°^°«…a¥n Šîhi*YÓž˜ü5 4²F»ü0·,ðý KA¥+K£‹Å"1øò8ÈgØö®['¿?à }2ľW:ÍVKW;´²ðU¿,Ö[PÑÀàá‘GǬ7©%ÄèAzÿMLÕ­¤ K…’ð%ôû‡½ñÿ´ï€‹êØÞsïP)+,K‡e]ú»¥ƒô¦  ØˆPAQT 4æ%M3¶Ä$Ögªþ}©¦÷¢é=Eã3½˜ö”¿3çÞeWÔW~+*—;gΜ™9mÎ9c?9$â˜çIHGê^¤±Å€|Î—ïø òÿ ÀÀ•¤Y 'þa÷®Õo°Ý+y„0Å–Êñ“Nw£´‚ÔEt’«ˆÒ^y ÚPÿÝì<Ù8ÆÜ Èy¦!­·­2Ãs¶t†&SÇÊ(®éìVb(8x~6=´¥No .9ËXæìñ˜ÄöÜo¦$§ÿÈöˆ \•ñQèô¨«³>)¤Zb~š>Ä5äêÌO&Ù—±?ŠÔHºŒòq‘ç°úת~Á3Ñ:„ž(Ú¬9ÊÔ”PtVy šPp8ùÑðåGF 6ëÈÀg¢Î7TÀ_Gå`›DLPóøeZFKiLHæøy7×U/k)NÐù§5ä†V$å\›šx]víãÍõŸ­ÜÏμ9¯ùáû3gK±µÜÔPݪ¸föw÷N‰ òÐ Î2oõ–¦‚›ú4¦¤Ôw-Y>jÌwÜ’¨ñ dëuqY™‰ Þ}÷‚þ%*vlĘžýó¦?ÚWé{³yjëÌÔô®‰™™M©EKgŽÅÎC+ùQåbh5nAkŽà2†t·€ã¤PAéò[Ñ‘(˜¤TŽa¢m6¯vë‰ ™ Á±•Ea妢”`Û¬{f¶ÜÝ•ÝÞc¾&ï×ÑF%dDeÎ˜ŠŠÖäöEc~tß4¥&4&`¸oh”6:]0÷}º½I¼¨PÙV,m´xy—Oh=yGw~fûm–<_áíááûë0íÍ0¿èà (ÿaõ[>.*(. µÅúNk|„µÈ÷'I|º‰ï’ÌZ6—™$ž çÊ)zÏñèûËÎ×®£½ºŠy«¦1­´O¥l*»Ã¾¦ØáÎ=ÊOªD%íO Óáá:´7‘A»¬4<8‚wîrÒ&XF´B( •y°ñ\ÃMøëÂB66(@@òÉ8ã•R+mœA©¾²¿vü8[~ì˜ý5Ø õP)÷>ÿüï¿?ÿ¼8Íþ½àëìQ åfòÉ< Aר/àÅFHx`4ÓÂÜç7n|>Wy3ǘPJ 2oKålQ—þ€*ëUvlÛÉö°NAÍ<á6(b¸ƒyJ´Æá¼ð ƒ“ö(ª)ýµj˜ÌçÖp„°”m;ÙVÝÀ¿µîc°äë³Èb;$^w »5'#;•ö¯¤és;tev+.!‹ÓxþUÇ4r2t*5G‡°[çt2hå!cÒå)åLÂ1­<yýhÙi,7Œ *€! ÊIå ïiZJË1ö¿ýZX)ó;èp²ÿžîÂÍs¯½)ø‰ÕHŒ®¥Óî+‚¦h#å&Zq[1ähüXhU«Fx‡†D>?(šs}E»%nd€—'ìw¦/Ði½ñr©¤GS²ºpö W„7¡xGuyÓP|yЉ‰qØ×p‰œ ¹|=êNðm{Hë sš9¥›ç Az„Ðß/EêvAÞdÔ•2$…hab}Iv$rˆ2ÏŒ”P&-Z>£—bÊè$Ú Yÿ;èâŠT$`rý Ñà;äXUÄañu|eýSp,þη¡ÚÞ>"^¸}ñ²3K—,]výßn¼“õÅï ‰²TxMxÅ?³åÑ-£ÄÍçî:zôm–w÷òFc žó¿ó$n¾ëêÆéu¹ÂüªU«ªøm3È¥õÊ%HÏHŽEë N9®0EDÓíC~)M f‡CD€d<í±}ݼ–-gËÙ»ìˆÿ‚¾«§öLk_å-ì? :ö"û£µ··°ˆ}oI†€÷>=b)ž<¾uÙÂ9œl-QÃ*38ªº"Y¸ªy eĹƒcemZ\B›òjÔxd:âfó|N˜#,ßI/ »Z÷? òp°lj0”ºÒÐ ®ôqb†¨I¹—†ÏHËËË.*^š¿UÏÉ$ìçdB#O {Ò‚Ú×ɤºB¾2%bR.x¿úÊÿ»ã¹æS¼GØ$l¼I½!›­j“\Ò+õ?¨O`OO¿Šò$U¼ºò$U””'•€;4˜:¼²òD]ùCT'©õlGn!ä4tçÌ$÷Œ¤„Ki®¢ âa@hO©¾Tê°gè-í±Ÿ9WШ4qVLZ›Y:{ˆö¿„‡N JD qè ÊåuaéFV0ʼnj•¾¬üÁ¡RƶîêœÆb_Åz†]Ç ØWÀžbߟӂ"¢g?´~(‘›2út—¤äÛ´.úé uŠº3ñÎ^¢¬+ɸ Ñæª±Ëäsªì¾áà¥Ü`’tw ȤYpÕÛa¥D?§æ~¡š“N’Ç 1 ‚SÌU‡V¢–SƒXÆ,㬕dÙÅ}”£.—b8—+À C¥Øù8žª,É2å&Y–Ù®$Æè#Z.“bˆö–cC„ 4•ÿ’Èæö$¢¬¥1ƒ‘ ¹ƒD7Mæ`á G% ‰xÊ¢/Ĩ ‡QI:)†¹Pƒv— L4Šr9 ¢ ê˜“.¨ aéR˜:‘ñ’ð¹*çåbšÅ|׃¬>jq'îzÿC¾˜Ë£‰ËSu)–» “1.P\®€ÉG“:›·P|~ñOlñ’ä‰5ø‚"¥ñ"$-Þô¢ á‡Ô¨½#DÝFÏÚ.ÏåÚ<Ïm¸g’ÖÛ3L;lr²2EK¾$GK>E8â—[q°çÆn Ü3¤ºE¿ò³ê…}YL˜~¦P ¦¯#( aJ¶KÒÞj¨q6 iËCÍ…q gþt ö22`ôÈK.Tòç Ì™Åço¹¼O½s·ö÷ [|_!¿/ßÓÄ'¶'6òWh'6àÞ GBWmí ZZ454<()Tä:Â1¹*Mî{¼5Â~–Ï«Ü:ù?…8û'|[i6–8ß“ñ}fc)=§H™º›i|ÏÓs ¨Æ÷‘º•|t'è)e5ËÔ &ê=FÏÉ0‘©w'Qïc—÷ :¯<1ä}ÂßW\áýÓ—¿Oaw»¾/©’2üJzŸ6ƒ¤¿^R øs‡‡ªD¸»B6©ö䱕”ùAïì×”çîTßÉKÚ:wÀ’‚ŽNWG†+Íû³ÒÕMjoXè‘§=.mW¸Ÿ0ÅÆO;ÐIø$âàÆŒ9GäÌ4\]Ýx½âîë¾³ÅÏuú¼Y¡Òäð(ëèã ×4ÄmÀàt†¿ìëðŠ›P¿‰K·)é.f£Êì窿N®d9²V0ö\Áp,¦Úâz„'¡Ç•P§õè~Ê?Ú:ßû¿Ûã,Â…!Fäh(ó>Ò½ø»ÓZhq5%kY‰Ÿä rBVÆ©ëø|ɦb7Ù°‡Í•0Çÿ=%ÎfšÛÙ€ývB¼” p$­HP I´à¥Û0Œ6XÄž‘Ñ{úÂHÙ5dùŽÿSØqäXù1–Ê _8r õÞl²”øLf¸ãt<ŠÂèôÈ-œïï_l5*ya-” +1bökžÍ Ý –æÉÉ ¯ý®06aùwÜ(¡½"+{vGV6¬D-üSöÙŽøðúY3?¢¡etÏÊË éž4’•Ob´níÂ0ûì%ö涸ô¾ìlaŽvjRbýØ|˜Èᎋ“[4—ô rIRgž+®´„·¤Ñ)ŒŠ_­ *¬(÷ô”ÙDŒqiæ±µ1i0½ðŸKÉÈœÍL ÃcZ’›fÁpà ¤®{ã­ =`Oð½ëöÚï[@N Wø¹ìšP™L…TýF\âà•;vHó͞ʃö]|ŠÇ>NSü–m|Š…I÷'ÒâAºåŠÄ¶‘'TT^B軸3Tìcsºª†Îî±c²þ÷#陵Ϋ&¹ž)–ÒOA üÉ—j÷Ðâ5j‰îÎ>ä’J>‡Ù7 cÉnX¥4PEžðŒúE>Lv$4û*-®™ÍŸ¢¶ÒíšÞ|èäß%Fm+å$g*<4,üäÜÈÅ84Aíÿø¿Y¤UI]±«%õžDuò®½8Ñ "Ð\ ƒØî“ôØ'«û>aKˆK:‹ýÉF}©DW4´T8I‘%÷×ée… jëG]‹wÙ÷S4ÉRHLðföC…öY6µóõÐ/o}nç–õØÏK¡%b¿„çF;+?ú¨ÃåpÞiˆj¹êãkXõF¾37ÂÁ5¹†ŸÃb†ÁÊö'w?GÁ#Ïí®º2ideR7-ÕAo0•P%W q|p€è¬²œW QþeŸ/´Ù·».|UCæ³Þ´K®2þÄi^¿÷³c¼zˆ°8ñC…¨ÈEÎó-LaæjKª#c1-uˆÏ•RkD'>ÄþsåTÆÜé×]æ{ýûu½ç«¤œG¥ä€•sƒ.ñÁn›ók“Ó ïæH:ÑúH²¢)´Ÿ¤"Ý×ãÏ’]šDÂâžøsýŒ»LÂó«xʇK0yåôéÐLšÁãq0¡ž©«ùb;>`gÆÌ<%‡•0æ ÕæšulóE­òI9?8‡ª/œa6•À]À¦›„¦£)ep#ûqÛ‡Ö©ƒ'æÐúÙ&eeJ1»°ÍÚì8ÁÇhƒ£›$ÎàÞ‹˜Æ89Ã8Pvp‹«)»²ûkW?Xva'›åÂN®rÂìÊ[.;evá2,cèQ³œäèrvn¢Ø úü§ôÃ’'ðߢ“sðêãåD—ctƒÜûÕ‡ê, påót’­— ”{åˆE—±ªþ ãY>õ—Ìg—þ¯¹?“Ú8› Ì'ZîT>Œ£’ ̬*©0Œkh&ƒ†²+DUÛZÎÚÊQ×zþÆž½Ð{­{9LÂFø¥ŽDŠ6Š­ÚéáÇ ïC[<žýï(ã˜޲w2 kà€>ïó?O+D‡Û 7§*rÐŒ W ÌDp”LQê)½”H1JYÜéQò?ŽZƒ! ¹§ç«–MŸ•T½½˜ýÉ^‹=·­çÜDq±÷ÿÀ½ôÚ¯n~±µ‡ãÜpK÷«ãÏÇõLk[á-„uÍk{íMÁÁ!U5U5ȵ€¯%yöŒÙ3^Ù÷ÊÁãk×rÞr°²¼¨êÖÎ…ÈUZåjòWxq2F¥Xã¤cfúŸ¦ÖòzAÞR¤¥V¸sÇŽ¾yózz{{fþë«Ó;™™)V¯>q~ä‹ÑðùoÀžõìÙQX)ˆ=ê…ÍÎúÞTÿ‚2ä™ò´²ÅERyG£TJ½|Ù?¦(a¤ñÿž}æërP/Z6±ºªªzâòÕ°ÇÄ%öÙû¡àïgD!"ª±âµeË_«lÄh a›eØÞ\Fù¨”r¶ÒűWé¨3àƒî¯ãïÛÖÌ]½Z©Cã—8ªø)-N ]r|‹#˜ްT¸0pÚ±.\ïÔUa⸶DªO=Ä’rFnS©IáÂq.¨À»Wþ«›pI„W #iÉk¤Òöf[¥o^Ç„UB¤Ðb>-ÖKÇã\MŒÎâ˜ð=´ÿv¶b]IZ«õÚɸŸa¿²Sá€Ãm[åÓÕëIýEº|!ŒòâÐô*Úчÿ ÌÚ:—ý ÿxäš¹ï!/yeæÖ0”Ѱ¦pëFÒ1I|ÇéA¥ŸßrõdzSý×µ&øÊ¥À^Wþ¬Ü¤Ððq(¬ÑáX8—x oñçس˜PP°×íŸòú[‚žø¾·÷û¥‚¯ý0; EBså^‘‡pB¹òe *át‡nn‡†Š§ò OŠ}„½úH O™ ƒt!ÒÃòÊo«Ý´gϦúÍyõ7€…ªH¼Û´ç èâkªQÊûqëùðŠÆà¨Nàž s|–JÐ1FWf(Ö4 äŸ z྘ü ›CC¸/®0Ä~ºúõ%à ññ÷\{g훽¼ÜCB¶)°9¤"©g™FSVcY¸T£^A…ãIv|ÖÿUÜÞ¹¾x ŒCÇcUwtÉŒŒGG+’¸rá#1/×[ÒœÏ)äŠ(Çy°Êy}F7‰c¸Ô³hY»gÏ.7kö*ˆØ‰7±ã7;nÆôµ¨Œ= ]Y£§µçç{ “\.Iâ"8óˆ17ÖÇÆÒeHÛÇ54šc`Ô]­9¹£GÍyOº4‰.P"3LH’/e’†uÉ@.]سŒÝ %R½ !‰Áà$ãÕ1°e3®¥X!‰ZJ6ä–ð.o 9ÕÐ…ú´ÔÒQ° WY;®ÊP*]B!œÁS87hå²íðÕésçN/fÛàÚæªúú*å´g=ø[)®aŸW—T"8¹Çp §³8Ý{zšSÁÄŽ µ¹ ú•ššftK™öGU$ÄEÝÊîÜ‹EÛr»33çÌù°Ú•°¼BÃKËÂÃ……”°o!§%Éâi(){oEù…{]úæz­ª¶6GÔ‰UêÒ„½óÃ@áê°¹Ù…öû9s23»sSRR÷Âì[£â*àÛðð²ÒðPð:[.Ü[^qïÜÒƒ§%©rØ·%!a¸C©úíàF¿òËÁ¸Y¢;™¡J¼9SÝ”©u(øÿ´÷³?‘ÓL[4cÆ"á^û´_¾ÿþlý°ÂW‡³@Þv°|ÓHûˆì(âD{}W3ø$®í 2F?¸Åþ‰rÚýX¶ÁÚÏ‘~ÅxC6gµu¼ø6n‚õ ú åRb’* ¡ñ® ýü!®½rà‰)Pë¾ýÖ¾”wæíW*w6îМß$¹¿ …x8¯õw”* {¶ÿCjÐ!: FÓ冧ÿxjà%1´'Í–¬° _<•f³ Ø”çDCôø¶F›32&w½ð&Ê5ãöThßeå—öÈ¥”ˆ?7^f7sTL?7Ó-¸xZ¹›;fœ8lé\GTñã,,Ìë]µ¦ïÎíoŠïýÜ”^Ÿie?³ãKÿZ¼ìϳñ1±q¿Šóܼýë*gîz !ÂF¾¤  ‚ÜØT9PâàØÀK®è •—ér*¯*“œ•¸x¬6£]É›)ßu4£‚QAÔŒo,l™6èõÐ`\ß]¸èr=¼ ÕÛ#Z·u<Ͷ>Õ± qiY˜2ÌMh²ïs–²_Å÷Ë¢q?ý0nÑ/ØU_£e}”ZØ.žP4R0h¸¥Šß¢p€­Éú—rI¿±@X!Z7‹E‡!ª¸R*lWb;I)Sµ&5Æp„äû~üM\Åf@[ûí3Øë“¾UåE_6ìܾó¹/¿Œa÷ËÌ0tgD# gâŠK U½ÖH –ü4*$44dÔ ·”EEEF•ß]Õ “&fÐùrcSžr»Þ/>Nïw€ÝœžôÄûvÔ\Ÿ5Z,²Yd7(êù{ìÙ]Š'p8g£ )Δ8d˜Ùxguõö;Ùc øøýŠ‚pƒ!¼ âÃ1 4› ùË–®Ú™–ŸY^>yjyyf~š´‚¢|-6 RcºêÁõméÇGô±OGƒe6˜¡:1p_ÞÎ_ÏÖ‰/«~¢8xér_óÈ 0 âËÍ-×´÷7¼ÃèÜ”††Êšªêê[kª++nŽ6„Þ‹…ÏM¾> ¼þæYÌJ|Èìã[ZzÚÒR¥Ò}çF½äÁHI,TXbÑ9hýõW¶õœê{rq] £GzfÒºà¯ãkç8ò¨[=Ñ/<è.§v‹ÎMöBrT¥)ZuÂî±½ªzgtT™IéEKƒ7|CÔï`£á7 »‰÷þ$³¬°6› 5UÔŠSýœDnûT™ØÊõªt ÅNy ‚ÛÇŸ‚õë϶~vaæÃs‡…TQa» („ xAôpöcLuËÆ&Ùö—.}°àTñ…3¨Qýr"ô¦XÄ[)>§J7œS:Ür-œe’Ÿ0ˆ›_j9wñâ¹Ü-vjÅÊ•+Nõ3¶r¥ªî¶Õ}·ÝÖ·ú¶íÝ Ø¬îî3°iA7¬Áuò Âôॳòªö”h»„ƒ;‘Sj§ë¢¥÷*¿0ïí´ŒŒ´·‘u/:Ð2Ëjkë=jn×_vuÝ=+Ù6»ìÕâ«Õ<«>;¥ÏHƒ!'Û€_Ù¹ØÞ‚úÖì’$‹^Ÿf»4/77oéXkš^k±Ægg˜T‚.Ü` Ò ³‚‚ Spw¨)Þ\K1úP5Ä)û¡çŸ?tï¹s7”D›LÑ%ª·°ó·°ã¾oÃB»ÂBe ¯Óò1¸ò•$³~ÈuïTr|+[ÏA÷S ›SPXÍìó‘ÆÑÊÎÀ€ÌQþŸÜÉ^‹Ò3®«›0,ÒXöû9z½ˆ‰Î$&Jž÷ §³×e¯á>çõ•iQ¬ï×|ùÊtz²Så# ÜÑÑ ¦è_gÌ`­3®ÅU8¼=>!!¾~gÃ6466ðBÙÑÈwÛäœüÈ=³ÑßÜ?þùçqvvÿßÙY\÷ Óø7.µ¥öÕHNì?]Š-ãäEæ¸Í0mÎKmVÙÈ#//. IÍ;}:!3c\瘢ñ ­©ŸXš“k+½­ì¹ýâc±±Ñ:¿s¶¾’mœ˜™Ñ<Çjõ²Ä{xlöö¾Á¥ÊuÚ’Ü“€éC.'Sq0ÈÁt2^ðjq¤18$5/.þԩČ̱sŠÆ4Œ|M\ì„2ަY?W?^üM§DZIõ¶øùyxnñvs+,”q¹qÑÒ±‚-ÕA€hoBBàN鵯°œw–Q¾jß¾Ue™o¬¬ÜXÉo_YßœøD<ÈR§š2åBä@Uy4 }+•ÖȈ&'K¶#:Õy§¿òˆ%#ÿ½„B*¡b]Δ¢1ñÉ+ªö¾.x»''7•–ce²±µ5óêës‹K—–¸d .:*;¿v84 RNxÐÛ[‡‚1þà™øÐg¤i[§GhhFVppzú'*2Æ‚ìNb:H¦å k‘@„"+怜ŸIwn¡ON—UV–•š¢£MU‰óËúêêëëúÊæ'ö÷‹!H,KÒÁ$ lJ·NšÄŽOšdMoÛœÈs¢ýçj¶\>)Ι$kÅY€Jç\ð^ãplˆ×ØqKNžP©‹_ÏÏšÕ>ó±½ÓgœŠÎ*ˆ1?!âùDsâ54nßÒÔ4aÂ8¤˜!<Ç¥CE8¦C§]çËÇ”.Ïr§à寧ÒÙÙ»ïyº7LüÀ9MÔÓ£ÇÑÌ(€w#ìR…’ˆ¸¶ë•}j²ŽtN]`È€Á½Èh¼í6ôÔdFÛÒ¢²Š#ÃÊ«ËÇšâbcÊ,i£2Gµ{|ƒÙ$þèiL¯¯EUh—»»Ùâ£íõõ‹×ë†ð]ï­q³$·äšÍCð!©âzñmÚÕHòòÜþ[|{#¯„ßW|ø4À.œ†´·¢·¼ñlÄsøQD‡“!Z…å‰]å+ÇcþðÊòŒŒÄS³;Þ.ÈÉÎÍBôæZS«×U£‚Rl2‰ê4ëÄfðŸ810 c²ýí»jkÝÝ}¶‡zyÅÅM‚ƒ“êâö2û FåE–©Eztòuöó/•—}ýo;‰€ÛÅnµ(«;Q²ºÓÙ"¨Ù>èf?±úÕ"‹>ÃN°‡ðÙy©‘òK¤z¬ËÅ„(}ÒlŽ@üAÚ¨ü±Ìdý„¸Ñ£‹ç垈OÉ+†Sfk¾ÑxêTLZZ~Y—r侮ЂÂål”hÜܼ ²¦L`oCI{V’¥‘½%tOJ ÊQÑ8EJ;Kë\,8»•8„˜2¦håö|‹R5Ìø%fsêüâŹ¹Ê@¶~YaájkŒ óò><Ù2žúJ·88öB’’À1Y&§’„œ~­0›LæŠ2nþ—=ÖÚÖÖúØãG*7X,‡,úkCc#{³±ñaH{X^·œ‚aHvG¦ùµáX±HÖŒçfwtÌ~.WŒÙ’d›jµÙ¬S““•Zw7 ²›»v7º2““MNöð æˆòÞÜ×#Æh•¦K¸ E¤¿;ú8¢+~4‘£9qÛÃÛ [üˆZúJ+5UÚZ6 4U—W˜Ì¼a[+6œæhG+î!IÀ·vN‚L(™€œ§BM¿ —n#¾]Š\ø}Ø×LäÓp„87rViå {U/Дf+±ÂŒJÀsgÏúøŽZð⚌Œ®.Õ 7°?â½¼7¤$÷À° å±¥¨¼8†>ˆŸ¯Æ×å¶[ ißvž·áž~úâóÉ“©óÇ,ÎË7ÂòÜ”ªªÀÊoŸáÃ,–°¡Xæ«…@ïóqø”}‰‹2•`òyD¨’1)!˜-Éll¥E&ÒÑÂv /¶f‡†lö¾ hyøúæ–¾þzbJjvç| mÜsž ³ÓãȪ}eW(×è† !Z¨¢—›w â“]7\î7Q”öô²'5Þž…¸ÝÀÊžäÛ­ ¬öõ“li92ƒØˆäRËš sxä‘SìÛ¿T^¬Øymˆ>ïXÂ+€‚¢\eÞdK v£õp¹! 4怀Ì1þ¦è²Ú°°róÜ6¶ ±t%ÙmEYJDlÞ”QÍ55·¬oVN6&“,ÎMH²D›üj-þ O»JÔÞ][-Œ,M¾Öš˜©Ô'•ΛQ^6›=†£ðE'á(F"zzŸT\161š¤Ç®m}¤Ÿ}uò)?ƒlkÜ´eݯð°}ýO?Áîdwm_¬™ ' ¾‘&+W{£¥í4dlVTòq~œ\{>­zcöí·5u´·ýãP{{›Á<*÷NdDÆÆÄ–”ÆÄNÌ QM3¥[}}r+ÐûÝØØ´uáÄà‘ñþab»<Â’›S’SRöÕ«EÄ£DzW¥“D=Ð¥°*O$ݯ“ú¶y«ú¸ð¢…§Nhc¯ž`¯¶ŸåÔBD½RWéJëv³ï"ú)tÛKW@’¥xMæ—ày§¸èË´+x†¼» WyQAz*ðáÒn‹NLN êáᾟØoÓq'EÏì¾.iøªŸ`Ä ^V¼ª.;'Y™ù1+ý"ÁËkuýÌ€áñc‰ž^}&s'ú†:xØ6R£Á°bû ƒ~ž>aÕHÐ-?é¼ø³˜"tÔ¨<è µ7ÒoùIÇE­x؃O´ò_|2‰ÞÉŸ(é¡OáëÊyÚ‘‰‹­¨µÃ ~s|uM¥¿ÿâIõûV•ÏKê‡ëʪªÊJ«ª„ R/-¸×Wï¸õ k:{\¹;jêÁ)S&Lœ‚½Öa{…ë±W_> '®s(0N=’/’ÐPKf¡CŠÉvú„?qÝ5gHB,0Ž KJ:”t©ð‡ól¨¾,+—‰‰IÉ’¼·%Y̨¾¦Kº@FZšr`W°§ |OÏàݤ @×hUÔ‰›¤†Þ‘‹³.ÏJXŒcv©3YmуÃÇ‘âd€qÖ³‰I‰ÉSmÖð¼Y‡ñ5OQ‚Ð;&…°°¥‹¡žá±p¿„JÓ˜„éâª@X¢ýb›¦† ²²’Èr´C6¤ ‚AËHO…È0~F¯®GüÂËkÇ•¢~¥R¶•"H¿š:uOˆ€þºïô/,:Vüƒß«Ê,/7MyQÝTIû+KK붡εmeE¹Rt[\Pš6/?¯4íJ†ÃPÍÜi;X]-Ùp¨2™ÆïFû 3a´-¹ }÷¦±h;„EgE¥Ù¢31¤õ¶ÛŒFñG­ÙœÛ’lqÓx¯÷1\§ðóíÕúXÌîî^k<5êÚútc¤¬ØÜª%ÅF’\½1ò-D$n]féóüóý]]yù7¨Å>o¯èø ,NoˆoMN–ôb¡™µE¿K»MB:B%6ó‹Vv™ðR[aÍ”ßÁå®ô/Ø0h»ð0‡xÙv‘3Âåv‹èêÈq1\αZî(¾ÄjY×;qâÕ,–úGi {ÅÔrI9Cý= j5”gLš™Û0DY/•‰ŠY.*f½9_ËÁ+À,É `ص鲿]à¢STx!XÎΈ<:#gr¿üܧý»*çŒlùZ’Ç?²døÂ&“¸GiMhõ$¯^=M›ÏÅv¾¶­¯¯Ý;Ÿ#’ïÝò'L¡GǪؾ~ø??yký€ãÎFD\ÏÝÂ\„˜ßàÍ]¤Y’‹ÃNx¶¬°0+?,Ì_Ÿm~(+«®n·1RéãO35¦°¤¤¬À¡×1Ý•µõžÜ1EÊ-è ôô ñ÷ðP«|â§ÍHMÒu|Ë/17&ÄÓË[„÷)µ‰ “fLss“±}Iççj:W Iµ'–%ùø‘P²äå#îóò޶·p®`­îpJ6nõ‡]bõk\ä[ôPû„|‰Íø É®žYâªâe€@¦uyb Q™Ï´Ø³J^ ?NÓpÞá,Pf,çnÉ¡&ÿ¼µk‘[xòÛ?”è5àÛÍOd¿£o`äpO=îìNТë -´(f†»È@+ÿq¥Ê‹x¹l:ÚÌ—ÓÆÄ%¯ZüÂãb×ÅÄ…ë-P·%ÃóéƒttšGo Êyq ùY5ÉøU“•ŸdÞ1ÇP‘5á–G›šnjÎ)‰â{­··dûHœÊõH¨£ß::Ëzª¾Ží¯CÅn,ÈÈ(€ëØk ðk­$E3© Ní‰Ä£V–ƒkÎOØ™’5­e ™§Ô!åVKd  Ö(¿0”g7mhj"ñÉ+ÖMÕ„ž\›•—dn/õ=€q8!UÜØÿè#ýà÷§°°¡[§ŒC< Ë„6^òÎþ7XÀ¶«¼Ö±}ÖA“ì=|ßö“ßFj;7ŒÉÙt»Ú]=næUE"›=euñÈá®ú¼…©Ù"÷±ú*B$€üN QcÔ™¨Œ¤Øø6õÓÜkUšê™¨G?=¶—íæP…ÀI˜èä'j ¼¿¾„¾{U5PðÙ¾A „õrZqç~:w$©° ÇŽwèe²Òú¯ojûWkZMZÊìÙGKo+)*,ÌoiÅóÓEâ¾O7¬;§ò™;ªvi~ž¯nO¨—wVzfhqÕ Âïêt&7ù-§ >N>‹Ã…ìK!Ò\T)ˆFSQ~l„òÖ»ø‰á]›lhPz•äeÏÈ**JË™‹zØ”žžáþžE (TþxñIu«BÅ»Aׂa PÝs/¹Iµÿ0»qÈ+©zƒ_9¸Gµÿ?¾Î>Šëkÿ÷ÎÌÎ즶 ›uš”R48´Š»Šk*HðºK*ê-©ÀŸ Õ”º{û¯}J“º@UÞ@=HfÞ3Ï=#;YÞŸ ßyî¹ç>WæÎ†½s½YúÒKÑ2¿Ð ±JŠê4Kík£ `å7’¤ñÇ«Uˆý;zÑœ®'„HP”G~¡u`§#ªß}÷OéþjÔËóêêìk·oO—ë$ÌG‘_ÿ|÷Ýj)Ü_úíÛ]¹{ë^’,ÔjbyBªwüã´­\÷e¬.7^öâð÷:ãQ¹Šx>ø#N]l†5*§@œŽ_ŸÑKe/WoŒ]‡ø[÷…ôÄg˜)—ÇŽ§rЗ³¾B<Ö'=øÃ¾žO ÷À¿‹UÈ•Ä˃ÓÂÌÒdX‘pÕyÙ›=¡®‹õ‰àÓ(ûíë(ú3æ4ðœÍH³§Ê>FfzÊþp•½ñ¸§/gý¶ˆ¾zâý¹J/Î_ˆxH_}8ïß°:ÅÑ¿DOAM<}ˆ¯¤>) >1“{ ý³¬DU¿ZÉ=Înûß evÇCe|õnç/¥¶gÛUFÆëb$Õ\ÀîÏvÏ‰ß >ˆ[°„õ‰o… é—@ïòwÀ=}Yøà öI„Ê•á‹âÇXlÂÕA~û?à±ú5Æj!8ýªW¨ÞÖ÷ ·ûïußgu#~ÝO ~û;þŸ þÐùˆ[ømÀq*æÂÐ&sá_M 芯'žÃ£é{ð¡éçOˆ³dÙËßñøñŸE|ŒÄ'—œ!|ý(â|8Ç·æ|¾‹± ⮞¸Q®ôÇÛK´/Uï9 阤ÁLÏ>Ž|¶0–NƨùûSðVBhŸ[%ÉB"ìíÔÞ?üÙ¹”çòRôË)¾Ÿç[ßPœhïú<¹’gçRösšJÏ~žÏ~þ>›¬§íyš¾œõâ£4ýHŒV7N5ú¥x•=RãþÒg‚ïïê­ë(OÒ¡ÔÇÈ÷)ýRäÛšÊUª4œ™²wÒŒ]ãåÅõTˆßQ¿ª§]®­Wy9óÀG²>åê¹K»F<±·ÀqœvJûÂL©¸3ƒ8|µz²¯ûàk{Õâ[ÌRÊg…|ñGCÏãºA®ŠèSX%;±ž}²¼Urü`=ñ-˜Õ[Íà£Âó†òt©Eµœ!WyóFÅ76#þh!Bó†õ˜=þ¼A|ž7¾¾ÜÕ£Ÿ'#N¯ŸY_!¾O‹Ÿbý5rÂÞ‚@Oœô”çhöùø‰sV0®;"ÿDpœN¡5žøæ9ö÷¾þ _ÿ—x9¤/JB®Û_gˆ¿Gl é(=xÌþ7ƒ~·¨Ê¨ß =ßÃy<œ€ñÐ z:®ÆƒXÆ6¹çÿ*W¥ë¹¿¾I×›‚Ç}‘7õ]¬Ÿýxÿ.+‚=Bz5~ô]è/žga½šGâ]Ì£‡½ÙŒÚ[6Ñ!ZåÏzÁ£a~6ìrŽ^á\Êær­·v7²ïŽ÷,röÓ Ÿ ô4 ÛÅöP­¤KZhô´ÌáÑ9…õ¡øpµ\‰zršÔSŽ^xÚ½Ckëž,I/_”£j3“eâ†ÊMºßÜnäÆm©wëˆ=äf¶ä¶ÄˆW{‡]Åα@ÿFFWð£„Ðïç;æjð‰JOñÏáž{=Äzê‰ûc÷‚ îéËY_!î'ž¾úâ"=媩­]yþ½°·ê”«&>‘ù±àî:ñ&¢; qº!Îâ¦1 | x‰·»`ýc²Eš~ ôÄųB†ô³Yÿm¬›Òφž¸±œõÔWob^’Ž<ý>6JŽOZÔïqíR·]7±^”¨ëÆÑɽn¬—Óýòg¨òÄë„ð˵Tå0 žtÞÂq`Õâÿ€±E¨±%z«»Þ?À7{Qžw‹h×$¯ÍB^¿[RD_O¾µD»¶wƒO ¾¯l-£ú{ ¯̳äEÞ÷LwlNæ7c^.¬ úï¢_!ëBú¬§vigÉ^¾¾ôà²:”F­yÚKå|=fll úw²ÒÛ#ã·¨öšç³žîWǨöÆ^ éq¦¯g½1S{ú~fñŠpôgzÞ·u”½Òõ|2Õ•¾žæYú7â÷ N¾âøw@?|éÕ>i›ñÚÛ;ø¾‡9—fÑ5«‰NõŸUx—d|)«ÁAÜ»”_H_Îú i²ž9ô4GO Ô¸§òÞéÅ`oÉ+±Ž"}¥Üì{ÌmU\l ÚŠ7A%ÿ#Þ?ó^öåï(7S¢9Žx± é£Ïì"›_`æºßZÔúÚ•kËìêê?—9ïŒèóìûgù{I%½•CŒzöñßeöîÅ7á4©p4õ“ ~ôåF«®®ªâ` Ú?k¯_›­âd¯U/5o‹ý)Šù4ònÝÕOÌÕ™/Õ›«ƒÆã$)×ù;šüoÜõ=§ŒÞîá¹ç X³Â¾TËÓ/þpõü®Y'Íž×?'uJn÷Xñ)§NëÙvðâÊ5cn®ž5åó…}Ï^r–6³YÑ€’vçÜž*è´é‰—ë—ºßLìwa]ùl¤ûIñþŠW'Æ»P=kQJåüC _u¹ñRУ¬/ãøfòßY¬Žu7÷{Ñ“ÿ:ïÚ«3¨ÿâÅ~×lôÕÿ9?Úk|õ«¾ºq®{–5ÜW7:»B±Wûê†á~ÞV _Ýàü 5|ÁjuûÒ<ðÅ,%¾‘}¹ ÔΕ~ô} ƒè­üèûœíG2¨÷‡Ô¾z¿ó©ý¤¯Þï«Ì ÔøêÎçPGÛ¹7ûT_½×Ù5Ú‰ƒ0ÑBê·7Bítõor;Ïlòô´wOg}—<­oWã‡T²žÏ’Äxá]fH¯îrFu}/þ|â{Ý3B*}ì|ŒÒ¹»/“¼¦öm3Þ–+¹Üåæ~”ÃHÙöj®ç?.‡‘–ÔÒµi;ì5\ËÅiJݘðÔË0ÒñH³Ul´yµ¯n¨÷cc¤AÍ#µ(GáÄõ£f©r¼ ¼\í å¨~:|ñRóeðYŽMü¯Ç´SUéQÍÐg|ÞÆŸ½Ä¹&Äc¥àý| ¹‡_œOÉ+îÓÐós ÷ð›È?¤‡ó¹Gª}}zjŒâŽ¢úÿÄ–PüFèÁI¿Æ×¿êëÅf_!zk s+µ¯oñÑ_c˜ èáõüA»n üáþz\å¹â×ðsÙ(ÜÓfϤøUü\†O¿äìˆþ‡ƒèû5ÕûþGõìTÏûÈ3£zw†¹:̰j!]=µ·Ælî·××S{«ŒmÜ^ŽìסïK‘"ûuèkÀCûiο˜òñõÄMÎÿóÐþþEÊ¿Šý,Dþeà•¤ÿŽýüù,‰è8ˆ¾_S½ŸOTÏùDõìgߨ~.a?k„tõägü„?žüùŽý¬áøÁ>r™›¿±S^ÙGÒk&E¿yvVPS”p?ïÅëö7Ú˜E|ŠâæqAôxo´êçhtnm°Ke½j-e³2¢ïåÇ¿›f‘TgrXgâ|Vþ:0Ÿ•#còríþ½cµûGÕîqÏüÛTðî!¼9.Ÿ ñ»‡4YZYiWÚ¶]YYi¦–-[öÑþ¶F­ûÿ(¹ÎO±6&½;Sà¦aî„hÂM«è‖zj+.Þ™x®X—‘'Äóy^˜“YÏ›2pÓj~‰¿…@Ž'ªÜ¸ÈÀ­"û]pCˆø0wë'jùˆÕåñÑÌOÏ}ÆåºÖRÅå0ðÄǪb~%xÞC.§ÚÍ剗 {gžàøf-qÒ‹ÇQsgnp¤2âÆ1÷ORxšG÷Ã3Eã}m@só™Å!šXìÑV!š×³‹½JµêmÄ÷ÊGûFÁ퉽ÍWv°“7ùNvƒmô‰';0qrâÚs'k™ÿ8‰ÔràÀ0•¿‰(G>âÓ/õy\lÊÈsÅ;yâ 8Ç3¿-î¡?rÈ ¾™ù‡ìëãçQ ĵu¡QÄï†Ó”GrEŽšeYˆ—ú<.ÏÉÈs嬌ä~t5} õÑÛ¦º}‰˜wsö¥­¦s—GVÓõŠkYés9.­EÐ/ŒÌeŠÄWüÕ4ã®§P\‘ÆM½¯ÈtO/äS¨Š3Þ« ¥ 8tý°Bþ¬ÕÁê[è,„î^âŸA×?ãŒ/ﺙÑqϺM‡.ó˜(”çºÙªõ”/všæp!ˆ¦–¤Ó Í‘騌ttF:&B±¿4Çf¤ãB4Ë.Q»Ks<æÒ>ÙB¤ó iܰKâÃPãDÅõÞàñÑà%Ì‹Áu»ÄZ >IqmŽǪŸÌü.ð8Õû²<ƒë½Ý‹oÖ‚OaÚã Ï3U«îL§gE)ê<;#='#]‘.ŒRä¶(#]P´£™•q»»ùíÞ¾„yϧÄ@Ô¸Tqã!p‹øðeÌôüŽo_Î<Ïï·ßÀW0áÕkU#Ë•Üo)/¾õø*æÞ}%j5×»\OɲßД£Þÿw¾ñ ü¾ƒðû›räùÀAøƒçvaí5âqô½ïÃrðMìÿßÿ×PïÃìÛM¾ÿ€W1¿Û÷.øfö­‹ïÿzðG˜/ñêKäù(×»•ãSðÇÂqxuä­ºÚiO¸)‹2q£H_!®ÆùP‡)kõ¾aŠÈ=½Èæ±Å nþRó{¼§Õäî |>ùÉÙÛq›ø^õô)¶ªûÃ7NWâ“°®¬G±Z.ï¯Gò\ã í:ê7ÓÜ¿ßk—h;÷»ù!¤w]ÄÕ)ý1z-I¾Nh¼¡¿ê–§ÝÈÇ|‡;W.§+[ÕGí:w„CÄÃqwèA¸Cc„:%àmÀSej‡á<ãrk«Ç­•Qâûˆ›zk5SP¾'tðËÿþÌd6:c §ìµ1Û©‘¨_„‘Øø³Öhp•ÇEÞH4z«çi‡¬B_GܱÔnì¹hwö5;¥ž§Ó•ßøÊOÊ©!²Š‚=œšÌsycˆ§À‘¡¸3àÖVp8%n8œ:=ø'<Ø_Nàg'§,våáìd‘U™öøf£X|g{(gír¥ÏGƒ«¼ñœŸªvøÚŒ³ï a톳'øŸ&ÌÆþ·«ø2mÿ;ƒfco·åâ,Þ¥ ¹øÛÄoDËR:£^»Å(úEî/6½;³°›‹rèõÙaλ¼žÐÃ!y®¯·Ú†õŽMOwÇòÓÝs¡íÔ™O¢Å­„`gVò>}[H÷¦¦r&õt§zn‹íqŸxÖ–º\.2êõhïŠ&«}w£(·uvWíº[äÍv½9øªò ¤LÚ5»ÉöÙÕ™«~5Z¡É¶É-W/¶w²šx‰*) *I !4P+MdYÙ¦D{E{’ßÖ‡*A³¼_儵óå>ü´ÁÞno”3„æ:e ‚Sé•l:x%¨öNr02RG¸!li>~5²æ×îxþML'¤èêôÖßÄñú'çÅšY'*²´‡ê“b²$1É¢¼júì8Ðg%µ£óß»’õh|«\~Õµ,‘ÃðÖgìd+ªãÊúýŸê)vÇn&ú·JRìCã;7³¨‚mGê°T¶¥OJP-Ù“Ž8D¤¨ª‡þß9;§§ªì@ ¹TT®˜+§ßýì>õö•òâzù¥! åM:ÞùFËHίf‹C§‹Éš]TE ÍŒÿeì;šººÇßy#‘‘A#!„°GBödˆ ˆ Óˆ¸[÷¨{|ŽjµîZ»¬£UÛúi[õ×â¶Új÷n@.ÿ{_ýúýþ¿<Î{ûî½ïÞóî8ãÞs¢M¼¹•ÃñV»ûá8•Ñ`œ›•5Om4 ófÍÅkbèŽsõ£I='kà<ƒÁDñ¯He3Ø#MPÿe|!•©E™¹’&’ƪZÚIìçK£M±ÇÈ#{Sê|z_O4>*C¢ KlQ(MŠÈ¥ööÐ@SŽ´G³}Ioï"µŠHÀH³¢¬¸Ç$»Žÿ™5<º¡´‚Y„›ä´»~EZ÷}'l±¿JNi¨Ê@%SÙÔÌ”~'½>*Ê)2Ò M¡¶öõÙ¼Q±Õ?C°5ºïƸHñ½ÿ–CV¿(µÒÑÁÕ‘qpïç¨vÐ%:f9¸›•:ǰ·Ûp;ÿéÛ/äxü þÚ½œ|ýŸô$”|õì¡ÅB7îgÃýž·½i³F‚4ý×p²Îî¹]<Ç2óò2ää 8Þ~õãׯ=ý€dææá€öãà…;Ìî/Dǯ^=~üÚµãòr33sr骖†Æ––Ɔ–½×Ož¼uëä‰tÌ̆ÆÖÖÆ†™{oœÔ_…CUn¤¾4#ø÷>wñ·ªZÿ©î+sóP74µ·£¥íô(R÷‹Ðtõ*Zz ®/ÏË]™c_u´Ð®fö!öÈ@ oÞ„\ØžGÙP@²cáG®ô âÑ0ð ÈÐ]6-éî&O™ñ„úVðtTOÇðáì ®³†üõœHüŸÏ•XJ†w¢» #ÙÚ2­ä3­æUÃLûLmá:>œnËôòà\¨~¦Òh9 p.Ð>´ÿí‹<:;i Žó/j½„ÕQÊ÷ES,Hpdñ›Bš¡š9!gô±üT îÎàFz ºZ$auܹóÔÇ™¼39Ô“ G1=™pÐŒ»½ {2‘,Âà}øIÀÝfuhÙq´gE<ÑâlìpÓQë©Vê}|uí>K/#Wz8çc½ò4„+ÛA/Ã)–Q; •¢»'óOhø ×áŸøâ+¤”¼­V1\n Àc•Ðê)OOÜLðÎ@¤6ˆ4tYXR8äJÑz§’Šæó£•W’’ŒÖCûÍK*ŒÊèoaèÏ¿4ˆ üèôøÍÃc“²Ã,·  m!´—ËR'+•#Œ¤ û=¤ÜëC¦6Î]€íºëŒ2|Ü{‚ú¡(ùÑÏð‡w욟?tGÿŒþ$‘ @pOXCî&,¶N‚ÏrÁ½_PÊ@ñ??Â|N>dà|Rœl-ò¥ÇÀM2&ËÜ©AÓqk1²ÝÕâ©h-ó.Ò ?žU<æ ìuÓ‘WXZÈ•%Wk®‘†¹Y4ÌúS²Á~îBVAQ¬€yK,ä[ÅG8GìÕ—ÞÉÛ¦wì¥v1C'•µ?…jÜ5æ¡Kh L(ú™’»»­ ^Xƒ¥Fžß"|±Ìõ€™/G‰x»ŽÎ Ôb\pº00s"l˜Rò!Ì|ôÑãÑ£DpâO/BŸÄ åj—,© ý ’—ê—,©[ô󪮾ÿHÖ±w§± ü˜áFØp~TaºŸ’Q¥{"ÓN¾7nÁj /Âä²ªÇ ‡Íx1Æ)¿![¸eû7+\tïÛGþýÒù·×ìøzåÆ/äRüÏÜk̇ùýã #HÌÊß ‰Ù<~¤x@(vÌÛ\mþªìÞÃþ ¡s!Ô¡ès¥ È•LÎə…VsG¹´ÞŒÈÜa—MÐç t5‹Î2|}¯'K”DÎÌ|rîZÉß×ñYÓdµYšù™Ê`h'T$KN˜ânµD¡V\›2´Kð’0²·|l°9ÊHsg?$gËVr&ÀìC³‰3/N€fw4¡ÙGÕQ” kïÍHIQZûšð á/b<]ø|•`×o–åtÃotÃê®e}ÞhYFF`RÃXnZïw¶ó3v…T±˜VÔJÀ²‚T×.™]uÝÿ[b+ÂÐãÐäÒõ:ú#7^ÆDàiÀ™òæ½ÍØÛ–>o™NKÙ V©Ô9ÙjÕÏãK½5nüøÒ7Ž•ŽgS¶ÎNIÙº-ÿþõYm]]ý•ËuõõuV³N0HÀ"öšuÒðæmy[FËx“[˜pî.¾sæÝE0=†ŸÔF8ýë¯â§‹}ƒš9/t’X:6ïä7'ŒÃÅ–àDþ6? |‡þg a¼Q„tªWš|ö”Œ.¯x#/'''W)0`n¢[§êÇÝý×lCn΢EÙ9Ø0ë>ìë@šз¯.Y~cÉ• ëu2Óû‚^{mBRŠ¿ÛHƒ’|ñ/)™œ¿Ž‹6DÕÄÇ÷ *‰Yv`ìØ¯Ÿkh<ŸŸÀÆúk³úc—³´þðÒ&¾}¦j¥×º–òßþ÷gS¦::âz`d q½|½ ‘Y@¿Ž6 CQw·Ùb¢wc\çââzó5”г/0OÆô|Ò²ôo2[#ÚoöÓøûkú¥fdü‚½ +Úûʰ¢àðÁcÂ#GŽ8úúÈ‘%k}}bÍ >>Ø<…/+±÷OKLJËPDF¨Ž1›Í59!Šhc¾Õ¾~Í{z¥ÒTæ©ä-ú§â²zâ²z“‰Kbs+fô–§£Ñ6CoÄ|Ϭ˜*}u|¼SpD¤yyaÑ{VL¦ÆÇÿ”„‘”ŒÏôáÍ F"6¸;0{æ ;o_¸2uªƒ#¼â¯]=pàj-1é¶ãó7¡—€±ujº 2ÑÛëÐ[ÅM@·byhIæ}æ›A1Ö†·ˆÑuÝ`3P4~’ŠŸ|@šº­Qö<çã|0¿e‘)Ò|# )ŠâÍ×k1b‘Hß¼;6ÿƾ›˜Os‹[g>nµh~[ÛS„Vâ„w4ßÍ­–‹tÄ6 ù€•ƒƒ‚ãäØÚð8š[€“´µÍ_4«õñÌÖÅD^ˆk™ÌùPJ;ƒTáðF¢˜äoY¶K¸wfPÐ×_GÇÅgÌLIµ³?…­¶Ù /£mx–Ièþ‰` °UÐm6+š-Ÿ0ý0ñÑlgÔ…Ä¡l!#p*1Òh ‰ÁqÂI*á[È8G‡촅äáÿ®—m!8•³Ë’‹ãxóqvÛBRqˆ'òŠ-dNu„¾„Cöô„€–9ûpÈÞÞ8»qœ òª-¤¶»¾a*pÈ>[H%¹Ë‡ì·³j&QÙ§v!ãùd2…„pB[ˆ—'ŽØ¥_þTÝÙZe’‰õUºÎ«$„cÿŠCù8;þŠÃÞæßÅÙçCB„zß%²¾‹s²…¤wÿFJBèåCïa´8N_BÙb/ôã®òÆl ]DÀêÝÍ"ŽrÜù^6Sk¬€qB²ï‚ùJÐwè8d€¢ä@~þ–¢Í¯½¶¹hK>ïuuß„íÁÁ }ñôÙðᡃêÍCUt¿ÂI±x¨ˆ$ëß¹PC ß÷ÔZ³÷åÄ_D: Fy«n8’„·ÙD,pA€ŽÞœ–¨ÞaÖ‡~òÉiH‘í›MŽN¾;"eî qè÷²‘ãËJ¯O‘JÅÛ †$L®7¦"%›gk<ávÊ¡âÊÙ¾ó…. ]³³O_t1Ô»«œVùe¡‹æHâdêŒùß­¨Þ"cÞu p7yÀiV(EY e'Þ„,Í[8²šaÅa+QáãËËnLAÛQ9l†±Sn”•Ÿ›xænmT DƯŸàêïAyASfãèÆ¿ß ÛIaGŒ]ÌJGO¿ûv~œŠþø•Ò‘4‡îH$ÛÖR@m$,.&oDÑ{=‹È5FHb]­î‡Fm»a;Òêp¸»}~Ä>S+nYßâ i¬ê­1êæÞÛ D=öa,7£ $£Þž±&wjL4GÄ,hÏÎu÷Ïœ9Ãt£i3•Jô•. )®áÌÖ3Ÿ£ÇÐçs|ƒ_¶¯;MxÌjSÓHD=FÒù2÷–„d¢ fëœï ˜YS3áûŸªTCªÌ1„þéWXPÕ4¼(âøb}ÔûÐZ£É+u ´ïü²wÌÆÂÂ-–ìØ]c#"cŒ¥ÐxÈé—û–":Ó3hªÙXà=d¾å&Ųƒ›ÄÓ‡¾<Œ9ÇWSƒ <Ã#”Á wƒŽ™;.·ëMl(­Ú“[ñ Y½“XÌŸKOÿúë3g¾>o¢ð-¹!{¾þ¿çD÷§³¼ol*ßÎ;6ogMÞk[* ‚÷û¬ÅÍ]MÞÌ;Ï6“½b©ÌXÞçoO +"›WmöÔ_ïÂŒ¬“MÕ5Y“’}Ïrȯb#ºPq·½±Þ»·®úªõ¥7g7^SQš=.$§ÐSÔQ;ÕÏ}ñùk6ïÛh'Òæ)•ë'ˆ‹ŒpvðÙ¹[£ZXØféàÚg•l„$tFé£B¿aa®1$ÜÉQ³}ÿ믋8ÁkŸÃ-Šæ]ë§ñî³3p}m>ÛD Çð ©Ï’û€/ü­úÿµ®ôÝæêêæÌ@ošëK&ájå€$+VjM‰+«©ë©÷îªa “<¢²¢Íé:KCcÝËE®ýëkN…‰ÛæLPxöVôè¿×ì˜X@ È ìð®”/åGàa,Œ˜“èpÃÑ©åj3BÜp0j†#§À_„ÖxG½ZŠMPo‚u&häÏ“MÅ3¼Ö½óo”þñ‚ (ýâqn[u úÅ <¼:gÔTC¶WŠ—e3ù—ùnêê¯ùßê©«»ö1EV¯Fip ¦NEOmwøG|EaUì="á}ÎÙüõCùŒšÓZÿÃC<þùt„€?ÆÒAôƒ¶ VY^Cíè6jâ’ÃlÚtíáïlë #$Âp´çmì5<É$¡%ÜbÞêÓ•¯ˆ’u„b)è¾":p¿a45¤Ä+¼Ü©p&Xð$ÐÁWôÄ5*üãáuÈ} ³Aíªôc´ê •ÈAïêØ7Ä7H,è« Òb[h ª¯ëmõçô]çΉcc­"ü‡Eÿr"Ô<§ð°ëœ:‡Åþw~Ñ[u‘Z~ Öi̾U™ñé¯AvB5ïæTŽo¬Ö9‡1—êPÚ˜}£x<àßñâ9¹P‚ºKc¿]Úöz­,Bš<:8µëÏ*}£`ž7¸Gg]|½¼U®gMFï¼A-SϺùû;ûŠÎΘ1h™H(Š[i·yÆš÷ôŒÏü¸½ÂhÃRÌ™h6]Æ#½9™KO±Ü¥Õ–%,1tŽå}~ÎIsšŽs"Ò Ù6`9«_[#dÆ÷ðDÜc4ëTÌc:¬¹ 6•×WÃZ6·4ZÂðáÍ›(Ž:̎ϸßpæÔª‰šM2—ð¾ìü X94És,”Æ3™ñMÝ_ |Àæk¯7îàÏyçF Äí|8à8`çêN˜wm§6Þ<({ÂÀäA!©‘ yù’ º€ú¹OÈPCŒ§Wbú³ë}º¹ª=åÊÏåɱ Ó.|²cÃèÂ!^žiþc“ÒsÆd'…GLʤ¾.ƒ7-4d`ÚÜ€^S¸4DŽM­UÊL†é–$_laÛ]ä£AæúòË¥£cÍJMlñ–ã¦ê£•²dX˜S ïŸÓœ²Ã[Ç„ÄÞʤÔÌiª””Ä ãÍ©_:wVÿƈh…·c™§›VêZ˜š6vLvSÓõQmºÒ,–'¦Vg·V”Œ|;eMçEW—¾MyᾉñDÉ "6èyÄØs{V^ÊhE;G{Ê~¾òÊ®i§)B=/ä?²\Á„#«Vÿ9#ËØ¿iûÌ¡)¡zDŒÌÛ?H!…æƒKúDåk;ôµûû_>}oâ¤û)á5EiMUù ™8/3$66²ßzŠ%C8· ·7Õ— æ z«y—mØŽ=fe8ëÌ©² Ç< >÷Ì`£VÍû0`¢•TÆrhlb 3iÂe%¾mõõm¾%ËгÑÖIpj9.¶òfLš™;™/ –äçWBñ¬Â•Ò@iåÂ<1c*ßp&ŠÆ$åHÎ_0òçY{ fþH#ÄâYiÆ~Se6÷I:id¤ŠÜpÛ U-óÊ«ÿ¾áø‰õ¿.+iª¬.ª­õ.HFu¥iSêÓËé‚Ë–ì R°x…ÏB=V_Úí·r+÷2?Mö €ÎS p61_hò—£g¨sy¡Åð¶h^à»§’òá}EIðá®!葨|H0ÆðT¡ŽƒÎNŸ@Ë'àtÞ°vH ÚÝ‚"ÈyÈZŒ4@¶aç˜úäYºÐ²ÿìÚ³Ä/³k×®=K˜CbeâEÜjLÄe/O¬4»¿Y$`5ØÒ(1KNj‰ùbÒ½zÁ“É×ÝúØ9´°K*X8k&8­X¾ZŽ>[½.þ¦ 2• õ™¿¸ê‡–%À¶€ ¨.v‹ürè¥gÜ=ò rsлFSóôÇgb‚Œº32¯üœ3$hµ¿Ýur:£ÒäÅ è3ÎA ¥TD@@"„Ðù‹Ü´½GÔûq8‘Í:+‰+cãv^>ˆîƒ÷ö;;ãÌUcJÙoDÒÆG::Äìå*D?€Øµá̂ڋ Îz‘ÁWç#ìÚ,ô´Šìê7£ëÏÔÝ;l2”ÓóÒ67¶c;+Ñ,Î Àeé\!‘PŒ5‚„Þ LW}|ÚN0w)yÌÎ éù}`j¹FgÆy-Ó¶>lÍfé'Ÿü-\䘿ìJ$‘ˆ$3é´pñÝHe½òæÝµ6èÄ^7`‰QÉb;“¸xêBoW\ ÝÔ?þØ9æù”üÏ®PD>j+»µd‘µ\ðõj­ŠÅÚÁKû,nZÜsRíTü‰€…ÙVïCÆèŸŽ6}ªLÂ<}C"ÛXh7ü :rÒMú䑊N÷y©jù‘[÷ -º¬´úèð_®ë0Ýw à1ºÂT^WWW5,ÿ…ô_ÑàòkúBæBxxhؘl§fäÜß5g÷œ­kÁ¥6xÊÞëxW`´˜ÑO)ê©_LÕ¤¡ï½àÐc…µŽ¸¾xÞèO ìhÝ0%o”ËT=ä¾›Z/wרxRßþïÕtô@¥÷+·‚)hPÄ;›á…ã¯x¼Š~ÑeE^_ûôËÿCU£{±OsÒ==ûs_¹á‰Ó€w…“з•ÎþÿÔ—¢y£›£x‚IM”T< -â‘în'"‘mÂáFÕ|9÷%KÞiüyRÍ7Ø<Êl-¤õ›®ÛRé±cRnΪ”~É’“ÓWaŠr‡MÐÍtŠÄ‡Ñç¨ý°“ÚG1cÆ!7åÏ\áM ÔñÚ½y%ØË3\jãÛ”ÄÛ”ƒèöÛ…ÁßímJuco§*)ÅRL20a¨À0ÃB ë0¼‚áM b¸ŠÁyt*G}oa Gãœq®Ž8W¾:Ù®òËÖpåe¬Äîqœò <)&ämÊÃ-2ªÇú¶\dT»ã3?îc ñ#±mΦçùzxøÂœð°°ðȰpzWç­ÿ™·ÿ<¶IìY¼uë0¥˜)OÖW„nûº[ÎÈÔè–¨/€«×ôÍàî[¦ÙºÐ<ËOÃ:Bµü·ŽAZóÜR<òñ™Ôì ÎÖä=thvÖСYP÷ƒ¯‘n›]?ðQ<&ý C"0vͯojlhhlªÿî *xü<¡¾ˆ¿â"zàâ­Bl).¡mnèm]à+:W)àèä¤Ì(„dô¾xõðm»,Wþ·’ÃÚ½*ý’_Y¡È]Ö0 ^ïZ5P–ušsøç¡j7¶Ã‡5ó°.<Ÿ‡®rÍì}7!óE¡?IäÀãæùÍ=”މ£Yx™ðlî /âÄ܉ØlzNÙÓ£}áF/3 '½Dj7{¶AfÏ3x€_OZ{Ç$K¥Ã=ý—+wåhÂý êp™'•áAZµN­y(Õ戌ïPV®­~kX¾&Ø}PØç®5eEùCBBµWhjºÚC!ÕÔŒž®ô 6«cnß}"„»—n9 åçÛTZm¤:TêëˆI¥6'½éë+ç- öMÒ†èZ×¹¼cRòvAš}é`JPt¬‡¿.ÓÍKç[¸\éd’àØßÄDM¼Fëe¿|JND(”s7š¹gT 1ŸÍ£ÊžÇµ§¶f+JäÁ€“&-o&x/ú±©~‰ªUØèÕÍøËS4±õÉe`srn”žàÂÆà’BVȘ{BzšµÖ¾˜8 ”LßÒ6wzkëù%µ+®®˜1;ôÔ¨‰Sgy×'™22À1?ßxá­G‘/¼h,áÞŽ‹ŠŠ»7>®sÖW~9Ó Ó&Ÿ=¦xé¹%ÍÖŒ?1òуò) r0•gñDÜ@0¡ýÇቄàCϱ½ýÇÜ+ì:&9`¹#ç£<dòó4*S†¯‡T3ÔËßE¤‡…lm=è&oÎöS(\Ý’7½´)yæòM“ކѪã‹ûæÈƒB$.ô™Ok›×-äÄ‘Ñþ*¹"?ÕÓ×7pÄ„å¯OŸ1£²4Ý76¾ ØYóU[³Pz¤>õ£}Àya…1vNÙK‹ó 6l £ÚP†æ¡'P ž+ú"ÙVЦĨÆÀLÜøÕT¯V5­ê…ç]KóŠ[bý†Â¨uëõ¹‚má¼ ôG–ج7›.-À¿KMof¡Mo»8L÷7‰ ‰õÉIIV9èÛq%»wY:ví.·Óf.§³A¤`¿YÛ¶Q´Õ€3?n8ó @Ûëõћ߲”£að}y:êc‰žn.¦w¼Áew„pý:NR,Õ§K°1K„4§(2-c– p&Ñ&µ;oDRÚˆ§pʶÀ–0›Ɔ¤¤†¤§YøU¾oCè0bùòmôù0•àïÛ‡®î£gYæ2ëj?­¯ÿ”>ÔEÑûÎ/¿“ß9‡¥,Eü-}í,:pîXJÎS,()Jð5÷ æÄ”ŽXë5?X;¸[Y91±Òh#*’hBà2±»”&«!pØD¯ƒÓ]½úÑQô'8¯TX8(õËØéì°¢+Í-Ÿ’›b¤‚HÞœ8ñ&z†.£gäŽÝ¹aé² èßôõŽ´é);ìХ͗†ºÔ<ãÒ°âß&ßD›6 Î›“'ßnÃ&ܤhj2Šçú1©H"ÞYžÝ¢?Ü“ìÄ‹ î«Üø6ËØÉé¬&©þ­;s¤gm™q°,W, (›T˜äç»iî?|õì¸ä»áßÍ›ähòP2¯Í}µ°ò–“9ùûo^Û‰! þ³F½V›jFÛÑ“['/OL[N]ÿjûeW×{«Æ%†N£X¾íàÛŽ %£|­³Ž–×c°Dظ<",êâD¶É¢ñôCïìa”»P¢É”ˆà÷þQ“&EÙÚ—å‡-–z–€þL ±TʃF¼~tD¡Ç¶´Ã/\‘ã÷é(J jÆÆYà³”ÖÊTø•*!C˜ 7Þ-ߨ ­zèƒV  Ø•0Ö^ë:Ñ)--è½–V/={<˜þ¢EGÀ 6ÀDi ªB«ºÐI´=hˆîüë!ó<`ÙvÀ“e:¿•y¤Çð$Ü­þ¸Õ$hÐ÷FG÷L*ÏÍ:8‚èé4µÙÝÞY­=ýG¢0¯˜_U›{÷ëÑAW?üxFŽ1#4Ý[íè¬⣊##çŒ+Óy̱^¾ å·/mÛ9¸ab먬ÜDµ2@×_TZ5(7TϽl‰«qiC®F*êÀnè໇ÒÂcÍIžÞƒLœ»fý»ô#…"Ø®VǪ³eáž)cæL٦Ǔ,}·tŒáÁÜ#<5IêE1#´¡ÄY,ž@&r¿ã d8Ñi÷(0{+)ïi©½ÊO ·ŸšíZ¾ r³°B”Gœ5ûE¦Ä+GQR>bÄûË**b°Óm}LLTQ>G§¤­'î·ÖŸåãvúääÿY©i­qQþJ•ÿJC×û’…mýRSÝ•±Ã½OlZ8nuHøÉO§N™2õˆOMQnî°¥ôªuÃrs‹F– Í­›2uËÖ)S&hTÅÃ\¢^Ö2³$ÊËIJúÕ²iùèLRüÈá5ó½ƒ(ŽØ '\'?òú0 ]âú×ìr‰¾¯ ¾%½D#·µT-îCÌÞÒ‚ÅÛÅì?ÏXŸô Š>—´zº¢?ªkJ”eôWÈåŠq>ùƒüº£'%îA¿‚¨1%*Ãcþ,T‹fVY°]ä«×/¢WY^¤é&ÚËò¾Ü'rÕ‘¤—Q4ñ ¸ËVE}ô6¥Æ¬°5fV].cÀ÷΄]Æ÷~8L‚ï¹Û˜ãýW´=óÍø†Ô<C†áª14cX„a#†W1¼C°ƒáçÑÇqÿu¡$X-Eá—à)1ø`æØrà™c7·ã¸„Nü=_î~~.Ça4Nf U\Æ@Xj[(f¨EVžU‚¿ÔªBƒÜLüÌë„j€ƒç*ëê*aXË›? mt6p bëë`º hÛ¬6´ "xµoYÙDË‚D¢àµüÐ6víZ4ÕîÙ«ˆ$ Û“[&$u'ž÷µüØ' ÐýœM ªYƒ?®Ü¬5s¼RÌ:xóÔÑÑ10mïå²ñߢëç3ÇÇ£/¦M‡ëªû ÕQðÝk ÝØ±\ „Ó+š,*²Ï"#¬^‘ѧk_ì<¹JWN=o Õoñ÷µÐ·ûR4pÌ2Á0î&¥£¢{$/›üÈægØèW)›‰om²¶W£iè§CFéÃ|C2Æ,\’Ü–9ª&*4\eH®,™ßjn8ý¨mÎÃ3“M:˽»P-•«<¤}ݧ/“/Qz¨¼å}œ%)ùã\=GËyÈýòKôºuù˜£…_®-âÙ ™ùCÏ6±³0Á)Kz×÷ô!”ëP ù¯ó.뀖¢{h)3Çq˜i;¼çÐ'‡ñÒš®ÌB\só’`nÂ}))55%‰ÆÃ$P Äg(GG-.”Э+vùÅ™¦–¹;i 8GNR#âªÇj'çQµ«;ÞYòàóŸ>ÿÉíH,çw‡Í_x#Ý9¯aµmø·ÐÍr?Üäf‚F†ß"¨±yPeçL›vߢœEêÀ“äýû–SðF7ʦÓV \x”–w+¸v¡”_Oš‰„(A@‡Ûª„þïĦ9p¼-Ð/|w¢Ñ:ð¹Ã}5÷ÈG]3Ñ«ì«Ï¾†±–·û3ŽÌ–7)š7)~ðyŠ®ÇCëÖõbÂ;Ë8M×M&±ã0Ðõ»mÎß©›>Ÿ½Aö0¢ vˆÀLV‚`±d#yàÿø¬¬À‰BË/D5;àŒªhçFô @\™ÔËÑ„Ÿ÷ ß|ýÉœº)_T’vmÊœ[y"oÜA_ßA÷ÚÖ¡÷ë–å8€ÙìLQd³³X@6;“ÿySÛŸ‰PÏ[„Žs£¹Ï©<^Å«·››a7a¸«l¥ëåVìžÛM¿67–Bì©‘;@›5xÉþÉCœtëÌn~89·yÕÐÍ_qñtåHA±T$S'Df;7Œ\lÐâÎ_Q\’¢Qûú˜ gï—1 6ØM¤L[<"%½$ÅɼdªÇ˜¤É{Ê[Ýœ¦}†>Ýw%+cÛ\‰R£ÏÒ«Ï¥§%)ôƒå¥s†‡‡Š\d" È–G:˜}_I–/{tÍ"õ¹¨zUÒçýR«¼\Ð&úñìö„‡ÄwdHq×"›§bë÷„cç9@@-à÷t#Ág‚p*cp4‘¼ÿEeH{qg;4jsK'úEËp½~J’HDR{´2‡è§»÷e§UËî¨+mVé žY)‘]þÓš †F*[J«8'µ¦¿^´ðÄͧg®ÉLZ±¶ :J)Û®\hœ;8)7nòdA¸Y£N‹k^;¢Øß¹dtó† Þ^JµZ¬Wz½ïíiLZöâ:_–™U,ö‰Ò†+ÕŠÀ¬©Í[#;®Ð)æ…‹2G; X¡Ã‡ ™26Àò–ÔQ–mlZ—fô!ûuçvßh0A™B0⯵¶\¾ièìêη2›çh«lS—xæÆ’ Ñ+b×’ôz3D@ˆÿ@i¸A!ûN S¨(!\-ÖeÔU® Mʬ‰WÄÆj|¿ø´¥ZªÛ½TY€®[ª¸óéÈ-)"^Î =eYƒ„®êÍì…æ…Ijs¨ÞGœ’‘[ÔÜnêÃÇ´ ƒ'ßOKøJvì& ?sž¢©Öîo1åFyŒÔDe[‹«ò'ü ޲bwµŸ»QjPë¢>Vö½n¨É­³üu4mþðßPyê$T^è(‘0+¥c6lÛ¾aLúƒ\CAAÂØéF¼©‡@ÿC‡Ð»G$Y Ôå† Žd%þØãl:¿°‹“.ÀOÀ°\”ÀÔˆ–SjÜà„4Sèê*?Yg€ ´SŸ4¾üJºû úé—J¨‡Ó3öû%ÓÙ±…¯£o'û ølFÓû@CË 0}Üat‚j – s¿E ˜m+ÁIæÏ5X»ä ïµØn_€ž§®mã‰N¥ç (¯Y¡yE!ýõ&‡›X¥U;´jtØÄêõ,{!£bfù®ÕREtð€WÈ[èýwæÌKZúâ&ŒV'xqóØùî2]ìà‚À?…Ê×Ç[äã—9ríÌ¥{²·wìè}›ëŠÇƼ$†Õ1þ;¼½L­Ë?n®ÅuÊÆ#ßW‚âÃ8™Ù” ½Â_ ž&Õÿ‹0‹øÆæÌj‘]uíA¨v0,4»ÄùQêœP,[ ˆ~v…º!è«Hý°’µ«ÛÞå—(ÕÈ]}“&M7©Õ¨5â'…n.,s¦…‚HïT³"äè.0å&Ÿãnq¢Å¥žiãcÞ²aÈ}Â[ëÖæ ÍH>0Þ_­– Lvõ®ˆ¨ªžwÒi*à•3l(‰¬Î†…Â/íö•Ž™X=¼Ymõ®âd/vþN/·l¤+ah@ºÇÍí¾å˜, ÉR^-nF¼¶\úJLö|V¯˜ØYr1yf/x'‘xÅÔ2]ø %†f؇yl‰Âd¦~x}[at„*Ôîãì* 0$¨Ôj¿ñ¥qA¥ÑÓ†Þ<»rDa~~ø¨m{?øi\¤—Wx¯‡§Ð!ÒS–$ŠÅž*:Y¡Óõ«j®M ñòdÅnǧÌ×ç@|4·¬Ü„|N¯{)|xh\˜¯?çÛ'<Ú7fró:z“ŸºÈ/£AøFM^݈MËÛom„°Û÷Öí ôð w–)1îf´ýs\j²BiRùõKnÒNžü í ì'è*>VQ4ßý-ÓʌⷥS@¼4¯òó½i^Ñ(¦Wg¥“•1nÖoþðëëheÃLôëÿ4MWZòý[Ñc|‚>dmn¹2ëÚ G ˆÅ4¯Pà V«¸#FÎ4øÏ$•ƒÙzc‚2ádéC½%¬:)í@YbQ´ç\ÄxãÒM¿ãPÚáÛ4|yƒ˜¡Q¿Ò¨¼ÙCõôg5É8nÍÄФ˜äÑÞ¥¦ÊÂe›·>´tà$Q¹s†âTüEL\àA£ Vå¾»”ì¸Õ¨ýHÁ’À &|)þ7€o.>4.eì÷B¼wŠ¥~û«·»Êý8IŠåÒÞtšÃÊ`_/úêð‘ì]©:‘ïn‘@çªÙí*`TYŽLý=7M¢Š)&Rõîw˜ù?ÁE‘öêb^âñ·÷«y¯õÕrf¾Ì)ІÄ>òjGçRq0jJW¿"’øÃôÞ„ i¨"ÐW «}¹ñ/ê¤Î³-únJ=8aL©‹Nâ³ÞUȼR»)U‘à˺òÞÎ l#”ʬ¯Ç Áó¯” ÙólByÿ"‘Ââ6!C&òU2© \ú³ ¼gZß [•É´×¶L"YÏ¥¤h²,Šx­ã]dSžÉ°²`èqN¢nð ÚÚAƒë`ÞKíúõðŒ+\[‹'wNinfŒÍ8ŸÝç…S¸óÖjànîೞ•ƒ¤Xàì7åküXA9ªBkÐZT ¶ &Ž‚üg]ÀþôÕº~ü uÑ¿?/ó¹@ä@èþ‘3§Ž½væ Å’¶}®°cq™%”Œì¾u$¸Ÿ•„jðó×ÎÇP„ž|ó=Æ×·Àñú5pêø5&wÃ$‡Iݨ \^èÜÛ³GÝ…#3èѧðox ÿ§¶h^ffw͆,(‡£d“€–b¨ Üy|qAúP.VÕ§u² ÀßF#2€FÔ3_¹Û)¼ ˜ì5Ä— [æâ]ègyõèQfÌJ‘›N,r“D™ŒY¥)ñ&¯¾nÇP‰¹(Èoúš»lóåÃÁ9ÃB þ/Ð_u­dê,޼¼8®ït¹“S€¦ ${Ü,ZÒ¥`¾KÔ·|¸…¢»£"nÛJ ù#‹ n Íá6«’ˆ%š¡Ej€‹O÷®ÙÑÒµKýà, E“·/¿ž¦fljŸRÄ<=Ôõm‡Á.^ñ±%6 Z‰Î£–¯hìÏ…Ž¿bM³<#ãh-Z·4ñBD6ÓbÓ]u¼ÌµOïx™bx 8ĦsµSÙ&¶ûXçœcl?¬øºñœâ K²G“\ð+=Ð4Á î}žW¥iµ#í®vä™'G0ÂuºË≛Ô7–ÛÑF®,ól1#´ÈÑ/LFÖõC×»‚Ϧ“Ë´Rf(ãÞFG›óüŽ0JBšñ-ÙÏßêHŒÅµDóEʽL»a CéhÖZôksë“™ß<ûíNBeЖ½\YFƒ6£õh|ŒŒ2~ýhW7ôBV;ú¬™·Xc›@Y3j³ŒbµìQÐàçÅøy«õ9XÒÚy‹Þŵw„€†=Ê«¦)î!‘pµI4î!‰àc sh©å'~ îŠ%£˜` Ž.щŠ÷SÏ/y”ÙóA n´²ÂÚ á<çJ¬¢àX[·Œ/ôp«sr[W¼ö:‚jè­OŸZóEÛI¤{¸ó!}ÿœ—òãxq©„kŠlª˜—2´äÕóË›¢"É4±3Œü7ìr»ïȼ·ëÝ)–ïK¿ÄHCéˆrM«ÿ‹_dd@V ˜]ÀF.Ø4ðÄä‡VÒ«x‡¯=õ;ä¡xCNk§ÀNt•¨JÎM¹TÞO¤ß½ûRSþ`8u+ú»Î¿Ù*òùÛƒk³¦Ó]£hf]üì§¥£vOÚSÌ®Ýá0«ã¶S죟ÎÇÓS Õ·)Û‚û½”—þ¥ùFm+3X{>O⨬e¦²ÉÔ¥†«*ïMÁÎybxdѳ˜ÌòO*PWaÃÌÿ¥xqþü6tyq™›^?p˜=Â¥obŽHä­7“cËÄLzÑ[Ù6²­«Qé!5͈Txl®=œ>Ä·­~Ozn,Åb{ö.#K6ñÕâ+Ô@U,Ú Ûl?úvá¾%¢Ûè)P¦wn§t eÐí|⾔š˜4³DÃ$jFÀ†€™p0‰Ï©{tR‰æÔ± Úå°«==ûäyRÅè’ë³v·ØÂטÏ;;Í è†nG ×xàÇù@Ð_NÀ‚1FH6¸&Q4ˆî½ë*ºäüåÃì¹ÐæbýöäËæ`ý¹âfñÚƒÜwÂþvöF˜i¾÷ªºkz&ì~·m3]Ç{ïÿ^½®ª®ª®&W(Ï«E-fn‘®<>Jß7®¢Ï¨§hþgÁû¾í0‘ð*ßb×øž{ÒÏßpH=é0ÓÿéV æÃôG¢é‰û>–î%»0ò}/a½äóÁÇùí&Ÿ[¬t#óéøÞ£Iÿá,8ôqé@gOß—î)À;çÿ/ÿ(ÝqHÊ7f‘ÿ Éÿ†¸t/¤ú$ý_f¡Xâ‹O\¶ôâÓ¯=}ÝÇÒ²§£|}-]Gùbwl‚°Ú aóÃËd «µâõÓ2Þ+â1]í·Òž³ÑØ7¢ÃBœÀ '¤ð|A¯ýÊŠwµÊx¯ˆÇô„…VzÒCK÷Štû¯ëÓH‡¿×)®2v2ú¡EŽ«Ì '”A9‡ ±Ç»ö =†e*âº<ú)1Îg†­5‰.v+ãé…VØ!Ÿ´>l‹È‚pVK ì%5 *A{¼~ÄaX¦:HZa¥;î•éÒÂvE?Ü©n>-VØ¡ó|-&ý_¬xgÊ1øë€ÞN—¸F¤;K¢é2?ä“8ñ|­ì À/q¸8ŽV‰cŽà§1BìñÎR/ääÊt»½êä'ýêg!|Iôsü<ýÒ臧yúU|Xò3À¶Ò#ð!4®±˜æzå+þxxÌê{)¥éæ ¹Z„®|¸§Óÿ:{†nÄÕÆS‡[1|íÕ÷ÃÛ”»Þõµ‰b9 q“\r1y ·ôð‰)'LR¥“…ü:®_/JÃ4œ9û¿™-¹ ¨Εp^g3œ7ÁÙ ç^8ûá|ÎcpŽÀyN±<.þ '»N`š÷²À·|œà6YŸä€)€Ï#N~‡ËR³,3âTk3?8¼Ö/®†wó]]LY¥°ï–ÊÆU+|¾«š‚›ÛÁÊ@‡ñEìèѺ7ñ5øØÉ£»[ÐÂÍjƼòÒ¯6­ÛSZ>/£O[Q~Û­å>9oØq£19(åMúš Ù@Ú ‰ƒå0áæ;ÒM€Š'ÎqÇ»õcºx¿éN™V÷K{ß®ÐëT?‹=냑›ƒM‚¶±r ÷¹Ê` ]}&Vm•›â]#hª.‹uÆXG¥ù-œzϺ¦¯rêftQvDó•ßz[ù Í^ãÑËbïHÆ÷Æ&7͘ŽËï4ýã/ngðµe¼È]‘½ù3/o?51qjûËŸ1¾´@¡‡âßÊ6&/¹2üKVÛ¯ýÝñ=.½ƒ<«W©ŸWûòsêÄ•væÌÉÒjg5«v–°§zÝòjÓœ”èÚ îÂM%áÍÆ“i'Kª“œN’àHp µCƒ==ô´Šê$…1ª¨Š:H•ÑO¥À\yVa Î´Š´Š¡„A''šR!?^µfú©¹•QÝψºoz×È=¬¢¡‘{¥}`£%Ú8¢µ£’ –“%êY½³ŠUñhÕÙѪÚ4@KãѪ­ h©:¼|+&%Ë‚¨;ï™îQ{.¹ŒÛ Kt;®6Ö&ŒëÔ…’ÔþK³¨¹g”ÞÀAîR÷u÷AÀ²ḭ̂îY„k‡k]¢Û]¤5üÿXW˧ææVÊ! ¢ƒ{Eït7n‡iÜTòÛc—“¥’´4À…Ð’Vj›«ÍQïªwè×-'Õ©¦ºR MpªÉê`rêä.;Yšv²À§¤¦’”ä”äA`öÏmщ.—31)1 P»lѺÑ kàhÍ®*ù·D—îPXJ*ªËUN‘±%4”8Œù•>¼l·„7 Jϼ1”KFî¡ûÑ(áŸÈdäc'7/ʯó¢}^ýgº/'ñ¦K¿Lmp58ÀGS«]ÕŽ„ëâ ôÏK7Þ@êìŠõ…Ù ãÒ@ª4¼ ÁK(ú‹y/öÞm³Æ½pOvß=~ £¸õVÜ r“^íyÍÇŸ¸êÁK,Ýí¬Øx_Yñhøä¨úrûhøä£Ê ãý¸ün¸í)4¹~­MfÑ|í‘3tÆæ/tà~¾Óž·eú¨}” ¢óâ²{qlÍû»e–^0[µNˆp‘L^s*Q'aó‡_ÞE[…ðctI7¡5=c!ãXϘ½‚B6ÂÙÉz"ò2¤o#»;e,BÀLðY@žÐU¶ãZêÀFÓÖj8|ìÿªâ¥ æ•,+^µâÒX$J©,s´%Á²ê`äu‡”´TÅ27aÒóÈ*¼QãŒ`¡HÇ- -C™K­'$aõûu1XrE ù³e6–Ì!ï·ÃgS±F2q/„BÒŒuB¡ÐêJ LšO C,íæ6ß0±žìD¡~"ê»YùtF"¶™²®Î^¬ûù5K–¾ôgpðºXЩör®\-ÊYztð­sX¸ã,íŽ=Ò J ±«)ÛQ6tp¡;¿À5ï¢y.—––›Wè]VR°taNºîä‘ÞÜù Î îMpz7-Ê)LÒœ®äÄ…‰É.§–𗳍Hœ®ûD™±Ô¤ŒWR’+)yY~azFÆÜÜ‚â4'ÆxSµó¦Z÷jjÌw/)‚$´‡Ë™¶¤(oNF†¦ßg#×ûiл‰¸±‘)ý)Ó­ÈÒPº>ª2þdy ]}åGU4Å2ºñïO—Ro´‚j-5~# üÃ3“Ú1íU±—&T ^¨*2)°×²_x§Û(;`üìð¡Í‘ü# Ú¿N9tØøÙ|úóÛßa×-Êó‹2BéB´b=ç-`&UbÔŠCF—tä/(/J@³h®1iÒ£•"ÝϾ ¡w©Cj¸͜Њµ³HD£rÒµâÈW¤úwjg§×JIÊ‹( È®1É|QatˆÞ)ó+ÚÙp…Í2ô•™ ú2õïœÊ­{é+ì‚Ìù»ú÷p³ÔñGÂQµMšÂÉl ýA4—½qaΈ¨æÒ~}Ÿ¶›¯sλ-MÑæÍh²èûBÙÙÁŠš+½¢¦"˜-!-/½¾u÷mtÎm»[¯/]Î>l®¹¼ºÌ““ã)«¾¼FBÝÛÝs]ëÊ’’•­×õtã ÈÿžŸ+HŠõÉX9R| (öaŒœ½–ôXL„‘?’‹øË•N’lï½+pE}¶Žòª]£¶Ë.ºâݹëã™|½A{Cì9%K±AOÇ"ë#󙚄v…D{Ë|¢mM>Ýl5ôfL±î÷+×÷c¬…ž&×éyZ/_ÎkLn"{`ö‹mÃ`öa2vQEnvE0PÉÑ+Ö–ÃuvNÐì_¼|ùÅ7´íè2þÔµ£M„ÔôUkkš›î« äçää–C ©¹fí*Ñg½qk÷Þ¯vo½qƒ¿¤Ä¿B_Ý !Àx/`TLŒ€Ãì&‹J_ꤨ‚ú#³Rƒ9ÙÙ¹åk¯8zI} X‘«¦?:;ÑYæ æòÜœœü@å}¨Þ$ÌÅ»aºÕ#Ì2ˆ˜k/dƒMñ<¢/Ý¡ýnz–›â-¡ì-ÑÉÁLæð {Kp°FnDOH9mfÝiå4g O¨½‚¹05LTt–õ†©Bu&3WåZÊ€±F_ØÔ0÷›XTÞ_†(Î@ŸîÁßXP14¢œ Ä ŸÂÚ~ýù4o¾É^œæHØBˆqR6BÝÔ¬CÊ-ÆB å6ñY9ÍAƒ´‡ò”*íóü¡À+æ„eý´æ]k„nhþ 3å]164$zÀ¥ýjó% Ì„U}ʆ8õoïZãgCCðWÚZ ä\ÜrKX=KÛü®5žDð‰ Æ"kÄÆ±€®'…9¼$ žXb ^{×m á/Frƒý ¤‚Áî'„ ƒÁ¾†\“òm3å˜B˜|ŠÈ" @4<_Y½Ø¹OÅãp{”/¬7‰J¯‰%u¬¦UFG3ýSä鹑§kké…_µÜÖ„†Â #-· ôЕµµD‘ý•,$´µ×“Ë»,I>ØÈÈg6dù^?Ó×Ö½Gm‰û5^bÛ†—Fom- ÔÖöûÏKyçáRÔÚMý ÔìZz¡«©»Óã™é$ÊJÐËâù èeüŒ~»ÙHaõsY=àp¨>PLp·3N€ *m‡Ïq–ó «Ñ¯«CÓ~uÎôZÀ< á5ý:!Úbp¿y+›>èURï¥p°hÝ@-pZ«Îi¼›€±¢Æé u¨¥~) ~Ùð£õÂRjzÍ55‚Ùg9µ; Ü̼sS!ﺻž>£ûÞÆê©EwÝôÀÝêÂdj$KH‡"“Ú "¼ Jʈ< ½æzàQsXÐ`!4 ²мEšF€„³RSwm­òÝÚÚ“‚š¤pÐ9eæ„îR_Bñ‘=‹·nùp•O§4m‹dcëÕÍÞÚ~|³rgø‰mìÍÛÙÒöÑHö6¥ ,þ}èz=­Žz/Íç펮üê®ðÑ.ö涤ãT${«Ò¨L†neožêˆœéƈu›‘žx´§µOú…%Žð,åƒ+ŸúV|vÚª4vE²wDÎuœbon EüÄ£»}!uã*I‹Ë‚«,vY\î?([#Ù§:@ƒ>6gMåÃÜIý”ÿzôªéOu«eaw'ÚªSù`úçÝꓬ´{ “Û¸s ;êqb³}ô8^Ðü^º]}²{úçÊhõΰ[-ëžþz^4‚~Ðû>“(L‡WÖ4¥=ÿ-ÃøÖ›²ó» Ý0Úc.Œ•]LŒ“Â”Ý;ˆ² -Ì~??ìc"x@k„©Š§„"‚p}eŒ®”=ý´ÍWÈ·Øð°{àM©ÅF²ig3ÀÊ8!-²9+² âàŽ_l P(ÛAå ™y”ÌLr*¿ñ‹‘{@wøW*g«KoÉÅü>A¦l›„6!Ú¸Ðõ%ŠU ’IÔêpj²T>1óèŒö;Nô ê6S‚+Ä –¡$s¯4E6‚`|;‰²Ìx”’©a0OÜXXl7™¶wŒ™+¾lÑŠïoÇõ°§âúÔ„‘¹¤>BtNìpc‘f:ùº-ú˱1åíð¾È8+À eÝØXÛ»‘NVÀ `‘#Œâ.Þ_ÐLr¯Ã²5'åË-‹cs«[ÇÆ€ÇÛ}¬ 2Ž ý Þ ¥¼ÜQ…tõqÙéSÈŒŒGÆi!5Pų́ö°» wòAïB!õÆæ¦7Æ)CT ‹-…WÖ@¨„ÌXEZ F÷âþIµ±ÑÝÆ^º»o“òöØîõÌ¡â…Í0.ÁvÑCð:¨X¤ëÖì½ã¬ûxÛØýÕñp×qãw‚¯º5Ü»e‹Ò36™c|Žîa‘-Q$KG,ÓÀ…©òvdΖ-l¢/ž« h:.V´±*DéŽÂt]½e(Ü5düóûƒQ÷-2ÓõùÌ·Jéž(Ã>À®îîïŸÞ ÄqwwäËê)6FSÜlƒ1ÎSnY€ý7´5èqýŽÊÛܰã‘jö(€?´`llÇŽóqíN!ÚØÇ-ã551e*\2 ,ÛÀDÂ:´À.x7ÉÖÇ·léC½h-âéS(Óæµ¢Ð:Xy¼ÔÜGXýÁl…Èþz«;mN}«¾ðM×.Z”›™B¿’™»èÓÆÞäŒÜE‹0.ƒU ›¬9W®)ÏËÍÍ+_s圬ÐܹÑàܹX]~'=#gN|| Ã8–™™“‘á̘g¾ÛÌg¾[ð™kcæ@ôQ2?þiJ>R9ÜYÚúI¹JÀþ0ONÒK&ͦBüÇvur²o¶yIôpî$Öô =¢/•!£Ø6'iƒ9IZ3ûœ¤ü’œ¼1`¨°’¾¢gäÄ ’‡QÌÚ墩#‡Aaôœl÷—„èÙ½8÷­Á€';6 çG5‡xÑܮ顎Ðô4ü1~£«Ô7^g`*u‡_0·ëþ‹+½"F{ix+ðzÇägZÂâ*Ÿ Õy}}o[æx[„9Þ~ÆY$Ö*ØL¿V©&L¬ªvhXÉx²èç¡Æ ßÕ:A‹û7ߦÃtØøö>>Íç%MÀ‹5iþþžéc@1 ”ýý¡!Úa¬1ðã_I’$*"¯„4 ¤a¤f':<ÜÏXRÚQP³ÖÞ^á;p\¤¨—Á—h#ç ä<ýjÛÆÑ“êK4m æ5ñQä(kËn2·÷[p‡ bîË!XçJ8/ƒ³Λàì†s/œýp>çÐö&QÙÿx½êî.<‘¼AF@.Å oDñÀ[A¼o£x®gŒ÷Ë#1&1j4AÔšM4_¯¨1êæ@D’ÕMÙl<¢0]ü_U÷À@Ьùý¦§«»ªÞû¼÷ª«kº_½ú·3¸}‹›æˆ‰¸Ñaäñ% I_¯c<ª¥HËèhÂè“bR‡E[jÎÛì¥ÍD°ð ³|ÚsÆ®]¡»ú4f«ÜjE-X0a‚QµÜ »qŸ],Ÿ]Ûµ‹Þ™ç¿øbÌæ&fÜb?+ìç[35Ù<æ‹ó_ÀL1 ñ'9@É"Ü{pÂ{ ‰?¤Õf¸êS!$ߊ€}Ú’N›'@ø ’ÿ¾åÍýãLôõ”qam\G¼“¶°q“.q¶v­Býú¬^Š“£² BXÙsN_Æ>Þ9oÈ”.îÚÙ¹^ßöݦƽk?Ô­EËæ}Úu;¸v‡±þ,¿â_.äsfÔ¡|ÓfÎ@´¶‰E-Þ ­ÅD²z|è`Ä*×ï‡&L‰M²¹ŽÆÊn@¨j ¸Ð{ÿ(ö²F±8jY6jÔ'£ ¡¡õˆÒf#FTÒf7Øy¾A4‘°£ýÑ0OÁè¡$D„ÓÖ‰ñ¢2j˱€ŒŸîí ¨ YDrÕ† Ò0W¾Óí£US“3¿`+/^ú4Žû0·sãñgaÎ¥‹ê,zyÍŠ؃Ծ}¥qª%´É²V€[Ÿ4€´XeÐà½é Þg«Ùö caqA·®Ça6d]({SšÛcãú­»vm¥ÿ²Ïð뢌&”àŸ²E9‰) Ì%|®IîR©"Kˆ¢Â/ñ`á%%aÃÄeA¿`Çè: -¨­t²Œ‹:È'KÛU#F°° KÓ/ ‹:3GýRÙ¢~ ¥eí^È‹åvÃËJŸ¾b ¥^âmÞô©Y»‰Š3Ôóð™¬1¹§?“@õÙ=?»c·ü¨ÌÅ+¨CöÂÝ´Ä^ž7"FÃÊ# ý¸}{|é[Ú.1»CÇÇDB.o^6ØÐæA¤)É$h›ðcØüÅuä‡×”ñiLû“Hûxá%Œ›g+Òõq_÷ .kùÜ›pŒ{?Ü7Å=.ÖÊŠVÔ&õ[Ðüˆ_Ò7c0‹õDdnþOÏr˜Oé¿´Á͆Ä6΂[êÅYtŸêÚð¼õÆÛÿFaiT «Ë¥íØÕT׆¢bDµqŸò˜­,e+QÚ‡ÐV¨â™­„ÉkøÙ¨Å©mfK Ñ`Fx‘â‘”ÊrÖnß¾†Áw¬ û!¦‡4×þBV ßÍž:ÁüÂÔʽ ìcñˆæ@*7{“5ï 0¦‡ý¼À[±|ÖD^3u>ûi–»>Z³Ü9BäÞò$Jþ#̉ ¼ü'Ç9éyRÙV,óËVr•“<6DRmUe;)u‘¦2¸ “ XCÞ^¬-Ô/þ_R°½êV¶W9ùèC“Çß "³ÈgÉnT(uQ¦‘N%‘»=¤wÙ/A:él–±ûãï M}€Dš°lÑ‹¸“ÞIuÜdª”i)5z¸)’Dd¸ð…úOœ:¥¯¨bÇ•úy ˆ¤z® õâ§„JÁJ7c]×&®Ýš¸·qU†Á0]ü6ˆr…Jûf:IÝ,½]ú=¦ŽPN²ßÕùj {x@’i.7õP®ÓGDÂÅôé;d DF¾baêþ^éšBK}TL(Ž(×Y‹·n©\®ÓÜáæJ‰úº•P7õ¡ê¢PÈ(† öZ1=RR"ïjQû%¬;ð}Ø6åËÚÞESí¦ï=y¸j×p^˜VÌ0£H°€Qqm6n’ú;¯:ا¤¤J˜O 3Á]囋í™O1ó’b()/çï*h±˜yÃ4}Ë·Ëm -«µ l T»Akg¶<žŒh~CY>W$1`·j óúW,ÕK'³³a^ Ì›ÍVˆ¯ª1£†BÃbüLÀo'ÒH‹œÐ{h hÊÊ¥MRÙ.:+ 0%x%;Dû4îÚN*ÛêìéëÞ>XÚ¢eËÒ³ï\3»Èš¼°t:â»[^®(_o⇑{8vˆÑD<Î? p}h«Yñ£FÅl5†™jA´4\Ý@³ßM€Klcª[2Lc¶õ«Y |ï ákh›1bâ/þ¾…½:÷ø¯8ð¤ßܾ“ À^–N·‰$žÃÃÞâbCø#îÑšÁÒ¥ë˜ãNâ;(-( ¡¢GÓ mJR>y tÌ º «èAèÖbv?’‹Á#žv¶›¤ÑÛ²>‘Ší;¶g!ùÛÈo @"þE,¸Ý..†ç5HÆïªÀ2‘ù8@ÎfÜ|ÅD0ܵÁÕn˜–qsÓ!˸÷àåÄ@BïÜÜq«-Dp' D0V¤jU¤¼õþjC´€FPla·‹_-î¿¿»ÇÁ?ä¡ö惤ùj­¦Y=qèþMº}}Ð,‹D¢t—w‹÷þZ,:´kgX]Ø«o½%ï..ì¥U ê¡2°à˜·Þ‚±ìc–-ï¶—J†²áNÍXPÿ9Y,N B”c(Èî´l³aUB8»C›9 ê7´Ùù޽{'^V¿©F oîŽP¢˜ôÒ~‘" ¿ð„UÚ´¿©/°"öÃü©4›6S¿¹œØ»wÇó<ÅÞ\´};|ÀÂå ÕI‡ß] §ar8î9̺Æ}r§E¯÷ˆö¹é“ÐÞ˜Yåhÿ¶Ñ)mÖùÅUÆT$ËË9ucªÁÅ+ÔÕÈ{YW#_¥\;ïÖ¶â|˜8/ò•1'²!¢ÊDT±¤³XQ¬‹+Fõ’xL(@òùóÎ3Üø±Ø ̼¬¿ŸäpšÁ0†p ’&Ç£¿K£…݃ÌUñuéÑV_Þ¹†¼Ôeõƒƒ¼¼CܽÙ/ ê›#‚‚Ý¥îñ©=öè¾°Q-#ûfGÿ(€Þ±kýↀ¿Ä õÞŸ@b>„UÉgw¥ õŒw½´V¶ÆMƒÝ=Ø2Øì]ߜѪUZˆ;‘I êo)êaÉ"ãÉs„XåçîôÏ Ú`ùƒ~ü+4krR˜,RX¥¶”Ϫ*‡yþ÷Zó{¾ÕÕïо[C‡BÙ×AÌ3è‘C©Kªi²ŒýðLš} ÿ é¸ ;Ñ~lJ¥ÖÙH¸Î¬áΚWH8¶Ð>¨ù¹d!YBÖ’W„îÿ²®Ÿ®{ãÓ¬ŠÇV´G4ŠS 'sÑÜ¿nާÛ'èO ïC˸ÙTø ÷ì¶Ã|FÅaX·ÙÏn¹g°¤ñOÚIÙ1n[Üú9™f8›_ïKÐØí„sgÍöæÂWò’Ñã¯ý@­ÖÄô?` î0ÔÃÃ§Š¦j7kœÞ¡½µõÊEÃØå6së4níY»«û䎋ES|C³Os_j–Þ/¹m¨"ƒ\E”¯\½¬íÛÊnî„ýmed\“ffƒl¯S¥Œ´ÍÓ'¢U‘EzEŠ'I*Ÿ©îÍ[RM2ùcü9ô?ö®ÜÇœ¯h®HšJ“VN¸$V‹æz8¾8=¬yí*ÒzÖª5°}\‹@ÏIýî´l?°YDÕ6fruM_Œ³ths“¢$„xz°%ar<®Bˆ®å¡  ƒgŸ®^ušJ=‡y¹V5ñ Y KŠÑqy÷¹oÛ“Â|\ÙOU[˜‡$‡-NO=›ú{ Ñz¡µ={P¨Ú,Ú6¿u|HøõlÌÄÖØÆº¢Ž¢¢ðVÓ— FE¡H(ìµÕŒa5k SV£¦)£P^«¢ H1¢Å|Ô™Òží’Ó7ûƒº†&t ªë²sak›”žÍ[š««kh|⦢[4ðžÂòB¥VáMZR³oóúÐѼG1ú6€Ø½#~˜Ø!ØËíÛ*ú8)BÛ&÷Kˆ¿×{ò"[r *ìǪ «%É¡íÎn¤ÔܼARr25QS‹°é±qm"ÂÀo,ŠOdxÞ·ç‘]J¥¾âzá-¾§ß³møªëAù>¶Í¹?ã({#¯!JÌ%o@¡^b¯ÈÁ=Ûþ”ºâXpÁcAC<4P<¼B]J´¿eD"×еv¬|°2H·EÂû±Éåüg+ð¯²ëÒûö>òA¶}ÀÒíÅœbù#Ayõ7mËø^Ú­ªÚ œ£(ËwÅLB„>¾…Ï’¯Õ£ü'ä*×¥¾bù<³'T.b0Fµ…Ê€•ÑÊõ ‡GNÜàéb¬½}xúŒñ‡GMÙ⦸xn>`ŸÔ÷~jç8ƒ${¥ÝOé/SLö¨JßP1ÅÅæX§Ý1‹"4ìÚØ}†o÷tQܶLux|ÆôáÛk]<7Lyx‚Ô·DƒDåøÎ)÷Óz%rNqS‘þ ð0´”óHªP±ð»1ˆÏ9Ñ–4wD މäó‰Ä’TRè e]FË€¥§‡÷üdöâÏ»¦®­z‚Öß1bðà¾ýr’÷göùhÞÊió–8.ä’‘Êuù x¡éc‰4át|“Z‚xT5éÚJ€Gé#’º (ZÉØïé#Úw@=è¬ ‡+ê8 WX8RyŸšªP®K0G]¹œ¬­rþ ES¤tŠS$jHèÙ' “ÝÇùâD7ƒçn” ¹kŸó ^Û†Ø/0nGŒã…;»몙MN6ùK·ûÅØ7 <`ÊÌ‹³ú'hö8yæEz¾NþhÜPv 3 ª+¬k…þÅS®B4íuR8O88·F”óäZo<þ­íLÌÅ??ÞrʲÄc³%¼ý¸QCbÏ~<åêÒ¶¼_Ä™@JŒ4ªl¡áàÄû»jl<]ü*›hmÑBu>r°ÖN]õfjà­”%ß% ?³«#&NˆÛˆÆË1w6Ê»Fc…d&¥ÅõaÓç'!='ÐYö_Îì[^N`÷ÖÁ³û KËÒx·ø·|ˆ}Bc“âï÷îlÞq—]ÿéÝëm<@ð¯ cEwÐy…Á.¦ù¿UlÆšÅýÇ]®Ùò“:Óð?c)9µ´nH¿± Èê gÔã3è‡öó«N÷@æa˜‚İPo0H¾>Ðgb‰Á+°V`‘²IŽFí±!±$YZ6Šm ^õ=j´’‰I~§¾Ç;¦ wb›¾ÓÊ”@ZÔNðú:áT$Tøuq>˜.(ñ:ÁN;¶ˆ§wº¦¿Þ%Õò\/¹ÚF-O®±»f‡ÅiY¿ E ¥­èʼnJ}‡[ªöN½Šñ‰=Œäµ¨¯ßð‹Cškðâ{ÚõÕ’“_~qxóÏÓ-5`P¿3F”=ïkábŸÈ®w†`WÐTåâ‰!ñOoɦ¿ÞŒý“ËMJ®¡qS¿NÚWÜå¨ËoW Nû«¨Ÿž÷Tiž=VÔ$¡ù¿»z7W1h•ÃÚ ÝàøæÔ®øéc‚ÊNNŒ úÐÀUŒ Ø­»wYÒ]ÞÏå‘ï°í¾¯;†Z£d%ÐjÆ/Åb••¨i+d;`,4€PÄv³À ƒÙ›ì&ûí„q’yÏý½÷Üù0nµé[÷_ýêWI" ÄÂU~â®-ÈÉ9 {.q¡ê«Ëã¡“Ÿµ4$:<¢arÙCÏ1]40:Àƒ=„Ãr†øCÊʳY/póˆˆÙbx‚KáÙáà#YúÆ)Lˆi³E±ÅŽj×5,@J‘º¶UkÓ2" ÇÄu ³rè0R‡ˆÐŸ†0! y:µ«¸±Ž~?µà§òrí.®ÜãÏ+t¢—è£So<ÿqþ;:Î+ÏëãdÕó¿+ïˆù'ôÑm"Ïg"Ÿ¸ÐÌÿóóõü]4Æ)_V1ÿ¹büHn,FæØ¯0‚Åñ4–8®•P–` ±'¯s„w‰cüƒW1†âž->Õ-˜?OáÓç-ü=‡˜ÿ}ãÆì÷°°}¼Ùq…²c>>\ʨodB\ë–Im#bblñEEmÛF`³”5†ÅOq98Y 2T®æêˆœ%†­^Ñמ.í::³[×ÑY]:Ì‹Àiu-"tZ÷ÙIézQÁ]¹dôÆŒÌ.]Gï¶…GÌéÔqn‹ÛgPï$‘t;¼!˜z;± ÄUæ±íF93Rø+g·шÎ{ùþËôElû’ýcÝû¸‚ _Á$Ø*H@ÈG@úîù%‚}@wª™Ðƒ}2 °ÕçDœHÜ\òüwL="((›ÙQè¡f:êVÄÄŸo+&«bª ñ.Œ™6²ØÍéì:+˜¦Š„~ìÀ×ìÀ?þý¶0¾&¨¹UÁ#Y$Xœ´ò"4ÿv1ô²ßÖ bóè[öCĨKTWÐð'MI'J"¨¶0£ˆ¶-‰åí¸©ñMSTÜ9C£D¶QdÁŒß ÁfÖ]þxf¥rÆÌšU'6´ãÀ­q­­aaVÜèRö«Ù2¤Wã&}›dNž8|ÀØ9¡‚š*´Sû:¾™™ÏÅÆvHÖ,þÌ¥u`ÀĽØÃèqÑø™ˆËøù òò=dPÖØpKãÐ`¨ßÓǯnûê¤Õ­°]w!i ' µÅFhkÇrý˜ ~Òfù•ªÄðsEê”"|&ß :51´wp›†ÏKkÕ^RËù§Û^e9Sá3õä zÈþÕì³m.Bï¬m//Êó\Ñ×w¾.Ê;÷Ü,åã‘ö«:žÚD5£›”ÿoú^QÙ~þípŸ÷"7–äËßÕËŠ›‘¼ãöƒL\¨»Pe$÷Eû§ý¢7¾ƒ·òC-0C(×9Uû"i ߣg=såÊ=H-‚”?ç¯õeEq!ï(è”a™pâH]#~ŠáÀ£8qkviII‰tä>Ë-b9÷® ¹üòP¥@þÕÑ¥Y8 'P¢š°ü|ˆÎÏwÂö6ζ’u~•-ɧ^ùÒ® ”ä³¢¶I¸4VÚ²âç›E>AÌÖú©mÿvЗ}_wUÿIÏþí x9°!²¡ù»VÖÁï×WÒ÷ÿ+&Ø'2±{jð«±p’ckù1㥫67Æ• í¹ äÆ ö³RlÙy9Ú^îHÉ)ÌÆl¥´O1íSZ™¬NÇ$`¤í¶ÊÑeç¥Ø6ÚNN)Uß/Vß/…/áËŠ$RÈa¿I© 1Óƒ¹'Cp´öˆ4†ïmÜǦ/þhĽdÀ>l–h¡·©víÂN¢bÇŒJîô‡3Ÿv‘¤ö¸±ß¼ýÌ>7d­ùýó«w–~qv£*åý×_yï nH^y1^\ÛÄ…ŠcY«É_Â[‡\d>ž¯Ã¢Bb$Æ.ÁhˆxçlöáSÌþ}O¡9ÛÃÎì9;ûÐPnJu|7Üß'Á¦4VØdøý©aš6Þ‹²©iаéûSBÅ#‚ߑ٠Lù ;Åßhì¨bÌŠ¤˜‘—4”ÿ÷`ÖB‚wŽxÀVí &uÓ²ãô?Óú^ÜW ~öé¥KØ”²kýÅ¿!ÅZ:Eœñ¬˜;ª E`À®"&4³ ˆŠÆD_NnkÛµùZ]C÷»: V2×0ê« ¤SÕC¯eÓ) S¢ ½jÓok×jµåþ!XbcóÇ-kÍýÕ/lº—ÃÙàùñE+Þ\owmÀnd¦˜VB¼‰5*”ël  ;`›jß Í„ r"[’ˆ;OÉ‚â”T­kò&ŠYÔ¹XÁ¾M…E°ƒ–!šñ*Œ@4,JLä–½„yÜj/BÎRÙ™y[û i@vËa‚Xb_(-ƒEì |z¤äyZTÞ?•³B˜³¼x,®NÒŸXL]pÂt@`?ÁÚÂ"i™}![‚ذåðÆó%GXå 4d‹/g+¡@ZAð?‚`CÒøOû>'ìO}Z]žM5öFr`Uéž TZâ©`ʲªêâIXjÐLywü¹ÕN¥Ÿ F ƒ@SÞRCQ ü‚áCùæÆÉ‡?î«à öé¶K»ÊØEÈ„s—˜GZ¿n]ä¹SÖ²ó‡'±ûÓ Øy€×YýðhHóâÄWéôöwëy¸›«3C²Þâ}±·¿HüPP¼V|à[3èÀ™Ak–ÊN»÷93¨=e-Dž î3®Ct9yþ‰·¿¡Í‹^U×wú f@.ÕÊ/òâ«$ù»;}އ¾º_®m??啞)“0qñ»eëׯ•çn[ð·½ ×oûêÚÆõ‚ÊóTn§b>‚¾ªL/®ÅŠ—ì¥è1ýz¾2Å~^ž³~ãµ›×öê¼jÁ¶B”ûBy¼ò‹òPp^Ÿb²IŽÁõ!XHWæ.@‚Á0íæ4èËÆí¡mÕ“oyë„Àº½Ö¢°uãzÈbÛ Õß Ù›´-õÜ6yj^‚“ 9Õ8Ÿ!x‘¾>¿yºko,±µÉÍ5N¬9±÷à•=êçty%'y,²b“6®gÛ!«zÂ0õsõ· V„ +?"‰ÙŽ(E5- )È?—Õªª¾Ä5våŠâWÈi¡²…@$ÍIñ›+%¨¦qåHY)ºRë“R„Ö¯^½*w\x¹â{L¬+^R~1˜H*0:Ælˆ—NÂ&]ÿñ µ¥ûUŸ>—ÙÏ08ôÀ— ›´—&¨ó=?¢àGípüJ¶×ÓÜôßB('殜98í×k¶iUáHûÓQžž ß«Cèݾ—:NÀåÜi¥³ÕdÄÀŽÂúPÖì4” Ù+Âǽu¸4P>ó*‚¸:³2Ö ¬"·n[KùË©ðŠúÞ¡ IlMÇìÂ>LÆfG^¹rvQ×+³4SévkQÁÇ,ÞŸú;Ëâd1‹fE¹‰-É™‘ÚµÂxš=B—–]Ÿçt {."ÔÉݳѓ>cô 5¹~‚õT× b73ºÖì m’OwËœ“Ù­¼ÜÁ Ý úãc„TAay" ³ÍÌãdðÇi5á ½Ö®]¾vmÍhÌÃã…±|@¸W>Ø(ûEö/eò"µiQÕBÀ§Zà¦\Ç|,w ¬_ý¶Hôºf¼ßô¯öüŸ.WT¨ÒŸ“d“ŠªW¨ ¸sW¨VGcñø„Ëó XøÄÜ”ç9Q…:*U-%fãvÓp !9Žs\+!¤.±qW4dÔÝýœã¿ñûeH ¥TtòCTœfŸHÇ?ï—Ö9¹S§äÎiý*“ì$ø–dd¶Â¿ÌŒV\‘†hÃÎcç <48¸sòžÌ‘{’;‹ôÈLž.‘™={íªìÙ™£1=;{ÕÚÙÙ™»õŠø©&HÌ_DB+=«!Ò³W‡Ÿ;÷ÿêÁèoä?Op @̳"§?Þ¸±ÎñyfØëNµ…0·P˜ f%òø ÀÑVQ*78VwHå™[aàO=:w Nî”Ö73#­'~Ð)Y;¸š1'{ G12%¥Ìl…‡0jí²NºÚyï¨Ì´´äŽÁÁXO;è˜ÌºeDpñG¡P«çdgˆ£Q(?R믧ÉëœL#¡<1šO×_1Ý#ü¿ÌÏf©3[H·=£=ˆ\ÚMÌféMÈSÞ>µ…¿r} ©ñEÕÝg¯eͯ´Ö>£ÄNfò&ÍøBGP1­h3ÔBÿ‚”òmö\Ùƒ•ìA"x°û‰à¾ÜKžQJÏ5k¦oawÁ´eúš5Ó6‰ÝÝtó/UF©wù'-V}ùß=£ôcL{žø ýïÅM¡ïH'üÕ^|gùiƒ¿ÒA¼1‘$Ç'<Çdû˜Ô—»OüΑrj¡‰äÈcdŽKäÇDß®âÛóÈ,V;ò×ówy?r¼4R¼Œ ˜cÈñÀ2HÇ-2ÇI‘¥”ŷ‹yæN x‹“>ømÁéFmõÆ»ª~ZqfS¥;e)ʼÒr@å­u _‰öËÒeN¢j/ƒ2IŽ[xŽ;âSgLrÑ…Ì‘s@€>ä‚‚¹rÁ\DEgĆY”qÖ"F[‚Ž>•ÚØ+ Ò€sçhUlNfH©É  Oׯ§ÐouŹR¾6+„áGGC= RÖ±\ñÅr×A Ë­W¡ÀaU3ÖñÂo?I“âþâS»g°Ï¤ÄÝà°£ªp25¼¡¼¥Ïˆ}?O©Wz;OyKÝ–§n«RÆFÒ.‡Úr¤ðÙvHÙ|÷a3Ú ûpÈ¡á9’-Ç`ã Á,ØÌ•®7Ãy¥·•zÊ[¥Cé˜<:¦*sÀ"œ¹” ÎT~SgæÉóòÊZW)N)ÈÝ —p“q3"GÞÀa€í™È™Ï þ yt]©'’,k—§‘”öVšZÔï« -Ê}šENNÚ«~/Jby©m^›¡¼Åf8ÁtÇÖrÈ€0¶9ÇaÒ1\ò²fð2‘d‘¨™Ô! h›]x;d¨\?së;>ÞVËxë×òàeûÇÈ#;û»‘ì 'ôÏqè°^Xè{pÇ“â‰ðfå‚2ƒ4G6²cƵ˜©Åá…79B‡ULË–V¼ýú¨/­Ÿ²¥yR«¶­ºŸd'Xl‡¤“Ýñ0©ù–)ë_0êõ·éÒøI㎮HO™üf«–a¡qуßi<(:.4"¬e«7'§¤¯8:nBù¡\ס„8æz‹)™Zdp™?<žì|pï_1!\jÉÊ«A$ØÎ²Ø‰¿…¢VT„bÔÏPåý«F¯ñßÄÒ.ö#þ¿JkÓÚÕKf8E¯ÙxYõ¨ó7õ?ÅÂis–}Sþž»hô¯i?¡+˜t¹#:³©^U<:3iÌøOö"Á«Ý‘~wfIÉ<ˆ6®.kkcC•9÷14:燒ÿs?>¥roˆÃqwËÈÈæ8î^?¢ur–˜^{âóµ˜Q‡R(¯5ÒÞ 6µw\\jérå{dën úPÏ&(J‡è‘§Rgöî—êÎ ÁÕüËáé8 Ȱw|ljŸ6ÿHÉCç}êO0 lH±ùòçQ&ͽQ¸yiQ ´É"ÜkžGiÝðˆyÖül]§áþÄÄöI‹ëJ=Š%!"Üv’ÝþÌç$5.¶wD&ƒÊ†(Ë+½]€Ç,ä7$Òïwô§ºZ}¸¸DMݤ9ÔC½¿¸¸˜mUïSL ‹ ‘¾«z‡„ì„4tÜ!)¹JˆrÐ`ÍÇ1[Á²Îs¹Yºœ_j0i3ºWU¯æ˜'ŽÕœçг^Í1cÜ`*z;¨‘c¹¨§O£àQØÌ„4áº@cnoð¶hóôDÐ…^Ý>ÔFxñ”•Pw¯vßn Q(;ojÔ›Ýÿ¸{Oê®> Þúm;/'¢”«È厛·\d¿)••È>ÁÁ~óWü.eaÙ†O•“zÔ)ü€ôžcßã“ñÏÙ÷ô ¡Fù¾Ôbì^cazNmÚ–¶‹òÝK-Ê÷"M&‚…½I£$C­Ú&Eòñ*¸ˆ\FÐàJ|<JdH”3_ù@kðdÓØVö*›žþöOÅòËe©ì#öô…®rNÙxdCÉR²T^,/æ-WÄ  S\)\:’{„Q`”0J—âA.0†éòrGÀœZö@D‹ÜOnËéòh£¯Òﲺ»em'OKÑ ®GKÎ<ëŠA¬ø—DÈOWí&,FÍô$ØØ—|{ží„ vz/äBî^v2ð8a/Ka){•Ùì‚})»@ÿV ñxJ(æesXê^Án§1Láa!\ˆ;7±· 7WÉ[> ‹þe¿f¿&áT{ö<]ÂnÑh;Ðþê»Ê9õ]Ü7€:êeq‡x ‰Œ!T'Qñ‘Úa½)Œ8-º”×€whìùãðÿGZ 4 2_=%…‰úcÒRö3XEû—¶á•!Îà F¬L´J†¸WY¶lûñåy QÞÂájååý ÕgZ=Ý7ÿOýòåà'øä‹Ø“ÄPOÞ-^c[|L>& ‘,t4ÌùyÁü_`6 Í.eÊ»ÙCöwènl‰ý†\µž× •¨4¾Ï„ÈÑp“­þeþ‚Ÿa‘l¿Á–€tdgQ®O˯b“‹D©Úñ›”>U¸-8ÌyŸ(âU‹žïõ¤|iJûøØô:uëÖIoO'«[p¥úFc}\ƒ‰¯]<ÿ³ÖÑÑ­?›¿x-?‘ž¶4Øj ^š–>‚N†N¢æRA¤@Ôj,°õ¢Ôõ D…OEÝnâa…ü…@ØZ  æÇ¡¯@+=1ßKËOV·ÐÉÎÀù±³`üØ8?v :Uî$[_¹(Í~5QŠýhB7¥‹P Í‘Tø ¤¨m~¤g ÌN¹=ÓõꤷIXú\zD‹¡RÏK¨ðÄ<®ÂŸ¢ CB–öE ý–…ÐIââÒëÔ‰H®ƒº•NBá\°nˆ/?\‹5¸lŸ.X¼–Lï»4$D©nåÈË2¸Éå:rMÍA-iÞß´¶úæGzV Àë”+äÁcù•‘à œý\ò-•Èé$u«r~茜;#§“ÀB”6Ê7ÂaP_WÈ ”èÍŸ|i÷g<’‡ö°4.hléQ°&siÁâ¬5t礓}Ž´Òdé9Qͤ;Ï®ÍlYl[ÃŒ•ÔLÑ*[+×íFÈEq%þÁiÍâ? #ÓËo°/ †o˜ Ü .Â=V îÝ-(‚Rfàº7ß-íž$b§tV C4_P²M¹nONñ‘6; ìéKv„}P _ÃW¬?»P½¦îýiŪb¨.¶ÑꕲVŬU)tߎµ$î3êö¥î3êëä}ê´s»h?'µá®§*±Ÿ‘âxÊÎø¹§¸£:%«s©@'Õà¢×FìÚh»¸=U“üžÇp¥!W88{‹¡¯ZuÕ¿4 ,±/ªêLÝJJÔ8tßb9E,×8ÅÉ/¹Iàö5s’ŠÃìz¥w3[¤ÑGzöž÷!¥Rï]Qö9¹ü9»`;s¦"sª]µŽ³LU…ÁJN"TwS‚“½ .Ï!o¢È †Û°ï—’às‡‡æ1$·c#ÐQ >-c?wÇ9¬•§õ¯NRøa… ÅÝè|.%Ù÷³~š—[âðú‘ÛAë€ÎTª™ ¿4b+Ñ߈iþF“Q¿ÙÄBBŃ 'ŸP«Qö÷&ÂÕ',Jö?R úû(¾gÿÖ9výºå_]þ”ý_'úû•eÝ<›÷ñðºÕÑÎNÁ]軯ñ¡] ÙƒXIëXöÓð.¬pÐVxüo[ÖÝdg ÖAÖp|×¶EIÓ®›{µ,î´™mLoäëÅG*ÎOäqŒ jC4Ÿ6a&îd‘Þ!Ö˜+×ä·g W¾\z5ý4gϱ ¬ôìÊÕ3ŽÃÞ=½¡—/ÛÇÞ7 ʯƒ˜s±5æDßð’›ÔÓƒ ñÈéwhᥧ QáЦCnCCöpPËTËjâOê;ž£8¡BÄo à“ ùÈ7‹®°ÿ»DÝÕU½è)ûåwñ{±Ùú°]ba†Õ#YqK*ÚWÄÖŽߢ}ÒKj8Á’jbãíÌBwËñ—t6Õ9ÀÃKj‚ñâ%\ì*xÁ‰"d '¸>ÙרO?â[ù‚ßßin¹Ãu€½Ûd<¼¨~üõññl"í²XUü8îF«‹ØÏëWí«J ¼«RÑ þHE¼Þ÷³OpÐØ„ì{…ßo}DˆÕy2oXŒ“·[ÅÜé_•‘R.¦VF è}Þvô$õ?w¾« ø¥3âèOž‰«Ö½<…«ýM­¨™§s§Ã’ùu‹‡ý­ª~…5£¨æU(ºô[¢Ëq'žÄG4ûoÍ÷¤˜ZÔÛ(_Ìé‚úN~~>k%IùCØ18 >†ìÁìå´Ò‡¿«ÿLkZ ‰ê çN¢ ¡U¨ÖÖ© ÿ~“Ñ[á+Hµ ²Gþc(±_9•?áu;ä^4´‚ì!RU÷\(f‡XãÁ¤¼½âv§`„zÊrdd:/Cþ¼üŒØŽ3GÊuðŒ­ÚâqX¹DOŠb®4ä-ž«7ü:Eò"–ù¼9( ä±Ì¿UfýwMžÏ…µ8¨Ë>^Tvp¶À†ÒR‰ñ&/(˜ÓØ*Á‘7y;…Éô‘ÖäÙN6‡m­¡ÉC ì'Ü̹9Å ‡0çXáêÀË—é>G´ð;ê½Íƒ}“ˆNÉÖ*mûé‘ÞMØ þ<Úû’ÒOønx¥zÓ~JàslêO~žR–[CtÈ,( #œñäx‘nÖ7CZ>å[é ežü#[UбP[RùUy‹J̨Š0¬ÅoÑFÇ+7¼w‹·YŠ׸øéÞÄtÜáÕÙÙÙY;_¿‘Ãcw¾¯ ²gÒĘìþƒßž4Gõã²³ç¬<|ãµ×Æ`U‡¿íµÆLœøÊÄþÙQ'¾4±âmä¾Ò.Íw¢o0ÓêØCãè<ö)tÿ¶:°¾%”,'D‰–O™?˜µñÇ~V ~/§ét°=–°Ml ÝA«yêè»°ž?Ÿ%k¥ÏÅŠtœ¾þžd~¤/ìòH>öH[óå‘cÑ>ZOµé«Bš×>r,ÄSX©¼Ö–ÿ¬4•20pê÷„ØC½ŒÞBè GŽ<±"'Lä´×JHó‘KÃòzÁɯktkqõóïvà/âúyéßðF§NË—ãÖǶÇÛÖuñòN¸¥& í¶ÜÖvíßm-)¿ËÛ;e û}m·4Ü=XM@Lílhð%Šc<žC'³±ßÂÇŸaþö}$òµUE¢ðÂ;Àþeð}tÓо°^Àœý .ûÍôø³Âjĸœ>R·@]T÷o…†ö„ÒËåÝ£?~ç«ÿñ—E—óòŠé@8¦î¥“òòÄšÔÒÞŠbü âô-ŸîÁâê^N .LŠ^ )Yù2~ŸÐê‹øný§šÎ©Á%i¯(¦SãÅ85©“FÍ—¦‹¨m”ëÒ:^¼ïÇ“)’ÖÝe‹èñªôg¹›Ò\ó*0…(Ô$[}ÀG’­Ôò1L)Ë–±Íló2vþäIj¡A'OªßÃŽåËyÖÒ¥l³z“zÐ6ÇŽ©÷Õó!¹ƒ„>3ø0ÒŒD“$Òƒ D{aè×0ðî½ ÍÀ,Þà†&B0¾_ Äüx)*8L¼á¯ó,8¢Ö¼ñPýf™âˆ>¸áüy¸p¿ÎŸßp²Ï¼€_xÈÿ²ºvÉÊê‚%»féâØÔ^ñ,&¥‘5×tÐ=p°ËQeý½– 'hVÓmEò"ÙÑ)loYÒX)ÎÑrÄÛ‘86‡1ºZñaýéA,±Kx9µ=ït5cŠP5 Þ“Iq˜ÂQ@ŠSgE"YNó½´|›(°VÅ"ïªi 9Ç8N,<S€êéáÐBÑ’Áè …xÿT<~ißöIíkûdùSÀZ¿gEQw¸8¡_Rï^ÝCýÔ¬„F¥.Ȱó¥vã RWP4Ý*C]»`ÓojÿIãŠÒò¬(õkí I·'42P"‘ð‘Ëy›ãµ«+ˆ^ ¬–¨&Ôl‘°¤M´ÏZÚgKbÏ©Éô] ½“4Gd¨ïo‚‘¿ò…$wRAˆ˜´·œ+~ï Ç$‰vT“íªúw¥¯-o³øUÚõ^Ͻ·ÁWt´ÁL_~zÛ\¡ ð׉¦v÷Inö‡]çÐtu¯|ÈÞòY¨t„…Ò¯ñ"N_ÃO|í핌²TšNÓY¨Úòé·ê^,0€&ІyIÄEüd3‰—«¥’x»‚ËŽGXàQîxI½ærfß\g,TÎáLð¢›ƒÂøŠU”üI0 'ñ¤ I##ÈD’M–‘ d;y‡&Ÿ‘ ä;òù7)#˜ 4ƒè½aŒ…™°ÖÀËð&¼Çá,üþ ¿‚ºð†iöö ¶Ey·ö÷öµY"1‡`ömA‰Ð:o¨Ni«SZq.ó”óÏ’VžÀËÿsJÿé°ÿþ§ë͈&Õ‚ù—Íô¤J|&o¾ÁÐßçüpcKw—»uû= ;HÖîVü|oíÒà ݵC–¬íoh;øX?|ÊÖ[ˬøyÔ±¦Ü‚?'ò”¼‚ÿA=îë+~ðïaÓ«uk…<~Ûj•e«UùæFÙ-œuQÿî»w—ëw·7èfµv ÕJˆßÏþùð\þ~ÿÕÏu+»iµ¾hífU»:×ÕC„¸©Ú‹ÎЈ]¡ ;bWx ÜŽ<h(¹÷åÎáÝàœÒï Ÿw,´tÞ Kn’[.ï å ùe©z÷¸GÝOÓ‘×KåɆ1Š(âv#ü5âU¼Fo‹_¸ÐÙt¶ºZ]=ƒþLVë„ÆÂÍØX„Ÿ3 нiLd/ÚoKCá K³¿ÅÒ”ö™¬8“ åß´TåÏv•_£çä‘^©§xÜs€ P†ËÃã~â8½ü–a‡È*Ž31¦`y.wõXK×ä<&Šþ‡¿ž£ZE¬¼,¥œ@ñ-xÿ.ëƒ_úðKå?¤–ö–°0èËbòyNƹ컊·ÒŽ’/ë«@þƒ~"í7EmÕÇ©¯ª Äm5˜Ã5å²P1áÊñÙ“PüIl«Ú˜yÃ{ÒTA_ûÛ¨©Õšâ¸ÎÔ:Ü1JµK‹“9ÄâÅ}¨D½áMvc×±=@\¤ÚïH3àG{UýM³oÔ*›8Ü3ʼ˜/Ìg\ñp6Ïb¾Hs… i$C‘d¼¹hã¥r²íÌ.ãŠ;öwY?Vø•v„¯¥%wªA‰ò’¨Ä]¼ÍtØ¢ƒ‡€ì9¶‹™i Œeªz™Ø×c=:Lµ v²ö;"BZ¬!q]] ‹ÅŒT‚*Á»Îl+'—6.âh±æê§ÿ7à6ú;©!rH„«v×éìÆ§Ç/T'ÜP‰qÅï‡Ø ×UÉ‹ˆKDµ»¤@ðà ÿ­Tè¸<*^b=cõ™ê%Ô%¸«(–â?œ¥uìöüy7ÀÄþ5¦™E½cœõ`ÝZök„õ ¤»—¥uƹbTêW±4I8x;Mð¤§b›e±ÛSân'&þ4êe5¦'7³c³}ïï ÚË€!Eg¢Ô®ìÁ+  óE0s1]”cá/ï¥u–fc .»›¿p¾qî£7Þ°F°_×®zl(D¸/ÀHš8ÔSï»>Ϫw0OC ÆÜóÖ›ÎDŠì_`º1o> ·)‚×Úu­8ŒŸ«pñÄlgoš‹‡Þ² ³ Þ”¤oÇMa·³šÑ“–„WÀ~¤ö;5«3»rûWûÒcà½9QhÍWÕt-&Ç{‹‰¿t&Ô›¿0ŸÝ…º¨kpßcü±ï%t͈ñº¤ ¡>#®+Îs,¼ŽCEÞKX]õE7­âÝ?.ÃN‡ˆE”o³7Òo«—¦ÖÖY¦ýÊÞ[+Å©øBÌHè!†oäSšë­¾P³á›Ç·¤Z’çã[†út )K€GD®Òû6"ͪô½Ä·beîà æoQ¯¶^|šÏúc6%:zè°üaC££‰½«K NÉêÕ%ìç’'õÐÒ9ç*"Áö;jáwy9ʱÍ`‘Ö{ÜòÅú0©ìw¥/àkéuZspfǺ¨¾U×MEKGÅ _cVÒ+–À@KÏ–À»™G>•‘‘yôHfFÙÅKWÜ¿sqRÒÎ×Ûãßkÿ˜:mÚô+—§MŸ>M]G÷´ÞÕÊþwBˇà|¨áxWôGM•xî¸e¶ð5”ø³Å'2Ú+4ÈàE}º™õ¸±‡_Í:Òmc_[¡Œc0ûvþí¹soÏ—>ÉÆ>TKÙ‡Ã2vËçF.™p²oê’!e‡öBÒÞ}ìsd9k©DãÆ(n4bÒ0L¥Ee)Ex{ùÿÈûÀ(Ž,íªê0à¨A#Æd„²Òh$ 8 ŒÙ3>ŒÖ9ƒ±l¼hWÄ g¼ ³¿I+öøŒ… ^œv°—;Òjw933]ú¿ª®izZ ôç 3|=Õý^½÷êÕ«×5%¦Ëº…‰½c7ÔË l@'ÿôõ¯b{MSm¯¹ƒdªmm3Q&䣯ëS"}=è™_Ñû-Jož?ù‚Ò{Íj²ãÝi÷õ¤“|ûeš“uaVÙ¸Â Ž§X}ïå÷Ìóô×…ô™äþO"²<þßmI)H‰()(äå@5PÌê€`5°Ø¼ìD¯Äçx¹IZ¡*m#…#„t\ˆ¶tò€r ¨fu@ °Øl^ö“BHßF†Øwɸ‚† Uê&ªòH†,ç¡<–œ/ËcQ.&#d¹å(CL÷±–*!²p$±#E à ¾Ä^}¿Ú¯Ž\þ«ëæÜ[uíÕ×ÞÒ´lÚ¤‰*_Ý#Õ{ߜރ ´ž5›®¿epÀ_G¦—…#Ïž7¢_ÖûýRëþq!Ñ“¢q0¼ðMr<†Ô` ý»Ga2²Æ`|f¿ÙMgLYÎã TÈÄgÎi²ø™4¬‰ž‰Šô ËÚß~;ŽÿÙÏø!>±ÄkøG,b½öæï~wØ“Êiurgd“°,gÿ;NæÃÔP“òÂVòPÈÊj ˜ Ô-Àj`#°xØy姺#!X”Gzn™¤TÍ4ºïbCüý9C,P{Ç­®=èrn°M| záýÏ^»jãÍ‹–WL¹úê)áÍîAl}ûš^OþrõSƒ‡~2&÷âʱ÷Ëõ ËREïNŽ„!¤B–‡ Qs’ȿۨ¶Àäl ¸˜ ÌîîVë€g—÷Û¾”-Y$ ض ”Õ@ 0¨Z€ÕÀF`;ð °@DÉ:‘î#*CÝ2ä ` ,ŸnŠ‘a:…¢òg¦¯ÒýׂrO¦¥âUVFÝ8»hæm&[Pµvï¡í¯¦ZÇ%Ëz’òUUWW Ð7÷å÷›YAÓ7/dûñÃÒ3ž“A>Çøü («}OMœ÷9ð&ˆ RÏ;ð*Ť;÷õ*u;[ ©„pÚ$@C´G3õ7µ~|1›ÎãE=èŽÍÞ#†gH%YÈ;¨Ç8ƒºÎå¢ÿƒu£€m y@9P Ô³: X l¶¯ûYø,’sȬÿ§E.úcˆŒ³»éØò·ä„.ÄuÙ\ÅgÕnÊ('¡ ‡¢<å Tr¹å\”/êJT©ç 'Kì'¦î`¾ÄüWÊ ÈF=íF­S§‘>×oúkÏ?÷ÚŒ~C¾³÷ÕwoÛ.,÷$ÿ\6ª¢¢¼¼¢bÔ¾…S~ÒÜü“) }½ë¯ýÅŠ¿¸¶þ¹aÃÅ^ÖÃ<÷¸’®÷¸Ju[¯Dy„JÈ#þ]˜œ ”SÀÀ=À*`ð,ð2ð`Û‚ÂwCÐ ‚îJ5B££€m y@9P Ô³: X l¶¯ûn—2 Õ¢PN!²Õ¢û£ǫ̈ì÷ßô ÊT™ˆÈžL˜ü×ÏϨ5µc×î=¸í#<þ×K–Q=rþùÜúzd±}ûòΛQAC›—¯Þ=üÿ.•~:îÒKÇ¿ôÒqžd›MŠI%ý­wj ³ 5Œ”Ëò0”ÃjE!üï6Òì›ô ¤øò& SÄD[2 y€xW5Àl hVíÀ+À~@H.—_¥[b$æ(‰(äå@5PÌê€`5°Ø¼ìþ/¥…ÅRM„™ˆÃDšëÙÌʪàáÇZà9Ú…9yù99ùy9/Ý¿ìîûï¿{Ùý=YÏYEd·å[s¶Òÿ¶bþü«,xø}ôÇ?~ü1¡GÙýC­/KÄÐúq6§µU Ì“,— B€Nãeçq¶ùšA’`ç­á¤wó±&Bé^N´^Š„Zt/­R4Œ†ª•B•z<Ô…´Zši·ýò%kPcžª!$:ow Û°ˆçÃÀT§ŒÔ`¬xa޵J|âH4?ƧºÕ9 iWèZWw7Nm) §…]ýÏ‹ÅûyÕ<¼ˆîi ¨Ýo§–»‰DG3 ™}œÚª¡ÞÆâe,¤kùußsqÔ[ÅÑÚ$ŽDG£ ÑŽO³½MN)K† &CHAÏÂö†õ'FßÖØ*‡ažbð»æñ ±£"€<" ^Ä3•Áࡈ\žŽˆˆPb½Ë^G½9­ÑÏ—‡^Ž>lsh½Ü" v¨6P‰pƒ# ÃÀò5Ûa4Á†±9F0¶²UÒIt¿MócŸ£wÑP·€ˆŽ  `P$ƒ#Bn9P 9¸¡%À × õ¹ÑOí!îý ÐÓ¢+ìê §:’=@«’ê;t¿Cw¨îƒ/áÕJô8’ZíÊåOæúÜ`ë­ž¥Äý@´Y°êpŒéåÅ8R‘ôHµcIŸëeõ%Üœð##O¦ÝŸ¬]y]™êUïOR?OOsx½úýýŸ»£Úᓽ)Â=¢Kc½\þd.Ù1 6Ùs½•00fJFÑ?«ÝÇhyW^Ù îªXáÌ£Âe£B¨ËHDWí*‚T»=Ìõ‚Þ<1l=¾Tú½ÍIƱ3ÍNúq;ŽZÑå×ܺVñZÆÜómûi a`4ý²Ž~iϰÁãž];’ÙãôËØzp93jÂ0igÛÌ]ŠÓ={Öæiÿÿ}ìJíÊ8D¤v­—Ãè™!'óv¥;‘A_´y5˜Gí/X1Ÿ~\ëÛÚÊ7+ébN´ih½£[¼ébè¶¿9 8SçUîqå̯íi¼ˆVÇÇ ‰òÆá°Š§ž@¢6Ÿß¡öLZGVÊ•TC9?͆`·Â"*n—¢£Ÿ¡Ï ]M1C"ò­Rƒ=‹ÖvÂh¥dEÓ`ˆÊ–™òÐæ¡mlŽ) !L1 wØH¹Û¬mOçw°Ba9äB¼=m¤;]“,å5qdÃèNã¥ø8"üÁæ¸9Ã’[z˜þÜžNÛÉîP2•’O¥cðÆW¬aoh˜;B0ß`¼ýÒE7³mÎ-o5“–¹’ _h/DǨéô4‡Q ?³½!Lój8`´­P¬Å,|¼Fë ¡±•ˆ #(d;shÇlÊj. »eˆ ¡Š_S5œI²r z]Ÿ=®5§†ûÏuœãÔK8HFŒí#t°‘7®ÄqObÕU³Ý…€W1~ŸSÃüé5•GÔÑœö€VžÓTMåjHk”ÿ0i•Á„êJ¡ ŽBé³3«D?.mDƒ `×Íî¨4'ÆÁx»Ý¯¯Â\s4hÒ‡²†ûÖ‡ZñÒ@éDhPJªŒ¢zMÕ–uUV‘õDÀÑihüïšÿØüN?Ã*úá¿DøkŽ|·}ª–£EÕtôˆÚJ—ŠÔävbÑéh'&Wé’l°ãX¥^z×m½ãeÝ!ÂÀO‚*¯©„ânÛ&ü,“ ìJH&ÒÛÒïF_wå %}Ô8hئM´Žß[s5Ú³ðqJ[ëã8™Rñ«]lú þ«kø}tñ†VÁßX\võmÝé+¬L‹˜53tqÍÕkZ7°9«Ý¿w-/Ù¡ŒlÉÛ7Ò?ä÷nh]su ­csøÁ^ßÙëyÀÈ÷Í—«y4Lkô·Úb#x wͱõÉTšIÃF>ˆm‚ê¢ E™½Å¿0þQðX_Z_µ±ÙÖ?µ±sYJ›õè$NôV}3Ž$6Eµi–ky‘Õ‹!!ío÷¬!²v}Wl,H³N,IRÔÁyC¢’p(kOZxÔÛcURQ*©åJÃ/áOÕJJ»wQÒ¥ÌYæ4¡L­sªÅL¥NÕsé³ëùTÖ—_SÞ`ò;ÔT×b¨û¾ŽE1á”ê»Rc%!H~qó RÈ–K.IÔnVø–¤øÎØLpÝ@³¢s¬³†kNFèŒå»$2'‰$—2‰®L"i+ãs$Öî")‡(’v4‰d$#˜Dò¿ÿ4A2Ç'‘zÿ· í&ÉL-IÆm.’q·›¤¯w‘ôçÜ$í ‹¤}•DJu“rÜ$Ví"±kÝ$Úä"ÑnyF‘ÄÐ|Ŧtvj©hÖ(_3zé-‡ñŒ.Œ°›äèó9¿Üϫ˔¿5!÷Œ¨OUÄ2rð~¦öð\KÝÏÝ­íßÏÙÃ&‹ãçB—¢ùÀA¼ø«‡ðú~‡(ÓRy,“Lj¸NËäQ^'¹¢!fŽoÞir¹„L$“Ér¹š\On&3É\r+¹üˆü„4‘¥ä^òSr?YAþ‰ü’ü–<æ*t­¥‡Âi™Y.@<~|Q.ò|–‰Æ»P„kiþ÷é»wïÞ#_±›öì¡ËÈóÝ’ªð?ý2ÿ¦^_Ú¯¿ãýÝ~}©>éaàüÄ5àî*9Ãî;OFH¹½ï‘ŽdfêÓ)S?.Èoa~tEZ 8+SœZªíøQùé”ùvË×v¹É=ÖÓ’dŸ=øCÖ´C5²h5ŒވÞ/Ž[/‚Î¥Ô;Œó£{“Aúwßóth¹(à÷€þož È ÏemmtèSOÑŒ¶¶ø£O8/šÞ¦^ ªxœk›WL7Ü'ëhžàÃÛxáS¼°O:eÛº/«*ü ®^.ß™ò¯ïù¬!v÷ ±hi4LÓh™@E…`DvG]³šµP,ö¾³.6~­>0~x-kâõk;dÑ+;XSœ7àzü05F7ÅÆÓ&ýùØq-ÝjŽÓ¥§³IÐJƒ62ñ¡P&ƒ¥µÃjŽÁŽbZ(úJ´C‹fM1^ßaÛQ S—Æ­fª…ⱨ¾³)vÁc0êÈc0¶á±W餷|^&¤“¡¸ß#ENŽ¾Ø ¡kÈd:™#ï§ò¡é™~|²$ZÒ™ uÁƒ°ÍñPÂvgF0Œ3™-Šë#uÅÅtrccä!Îë¹8gGå¹ÕØÐÀŸihhäÏ466j- Euܪo¬ç[p¡yÚÐPßI~\R_¬ß×XTÌ·‚U ìYQÚYQÒÈžk(.ÆE!¬dE¤‘)ùqq´Å ‡‹êK¬ ¶H˜QÂ;Q1þzQcCÌÀ¹% €)R=Z_Ôd[±µ‡gëVl­KsI}û¢±xu‘c…m=ßZRÒH7Ë…%ÞT¤B¹–ôÕÛô>"×–‰\«}ßW‹ÇuÂ\2¨“kµ@üh­öý}Úyñ;§”ñ¨¹Oû…‘¥~‚hx”}K˜CrV$ Áp3rÞFÁDÏb)Ö—µ`YßÔ6NÖgßDÍÇ? Ì¡x²>;×úª–}sRÚTÉIÕï•%4¤?[@—ëÚ¶8Dµ 5ËM4%>…ÿ]ûÀfu[d"¶Cˆm‹zSSü¬R݈\$ÄPoíi}õëø7‚g¹þjt·à“õÕ8œÈà^ˆ qWþu‘ …¶–?ÉuÁs[ÿDðy¬ðSSÞ ã_zm€Öžô=± Ü ¯`3°«ÎrÁè1dsÖÅ[[k]‡þÝE§Ð)ñ'%£”`† °Á‘  úÏbwÀ—„äV¤†d+¾6†vuejoŠŽ{šÝÄnŽ> ˜ô¶X©þj­×™vp]ïãõ¥#ƒþý‡øÁ¤­±~ÈÖÕz½™Jý°ƒ´”“Ø!Ÿ÷ÿ™¾'z•`±Õ_·£Ç °Á  Ük†#‚­ÿX°ðÍlß,Ýþt¬¯CÕ>™úÏ„^Ê^M´˜ÙÑùµhMüKwxÈp‡ù¶HüŠ1ª8­ûØb¶ØºOÕÐÜ5ä6 gÒJºîãÖO½uå£ñ̸.2òNó§Fžü/‚y„ˆÇG†Õ×9$ſܚ’%žóFR²MO1ÞÅ›–4"/ijäÓé¤Ç×­œNâ[_¿îq^Iï}é¥Ý/Ñ{y>^ú;›ðЄ YÏ⨕Z¬ý†ÚÇ1Ö©-ë¸í¶Žx=ŽÂ ŽØ#üûñ¡ÐxÚ G~+]5=žÎçàÈžSÈÝ6Ü)zZñ¦*^o·Ç| ºÁOëŒÁùâèù6b ®– ¥Þo˜]¿‰ú{3|{êʶ“^ËÌ\¹ŠÜLû¦—‘šîÃqhº£žžõÝÓAwwÐ=¼¢ƒî°v0ƒjL +Æ-+Î-}pì(-]J =@¥K£¿Ö7ÐüU«ø{±iü½U«\Î30ìζ¿°«¯ìø‹µÂwµ²á@,ÚZyVž0S«fÓ»Ö§Ný,YŸ½Ë°~àÔg“¬¼ÄºoíÉêÞú7±}¢¾êÆåì}6Ý[½{ó ¬ß¸ªOgc~Ò]p°ç>hút-dôqî 9þ.7E­¥…¿d×Z¿ Ò;gó--Ë»Ü$ÿåœÉm¼µ–·¶!ݲø®ÉçXIÚÏój7JÕ(aD°«öEË[ ÏNí°âOV¬hiñj¿îœÉЉD¯L˜|»¹{å,Eè 1©ŸÂ’ÔP׹̤–öf¿.ÔÓe0¥$h­õêÖþÙÒÑZ¶^‰¦Ç²àzåé• ‹,è£LùÝ«\:G¶´h°B9Ý£†>¶ïœÉVÀí¿öŽÂ2*â%±’®?$âÃË$Ÿ+î|·Ç2Çý¡Ú“h÷šÊrÈ:ñ·:ã3ýI滛~Ylk­12M¿"6Ú(¾Jh‚ÓYY3FÞ+)BÆA#CŸØeoY¼¾VŸ5Y[×õ±‡ Cq:2ôY™‚Ò5S¹V,\æö&¿'Ä\b샢³0Ï ù¤˜ŒÂÒp5!4ôû2süF0Í—“ôeâaFñêz¾ †mвԧî‹ÎÃêÖ Ý>É4'™¬lÔ÷câV|Œ¶wùfÍÙ;/þäM™ÿýk"bš|³©/¢5>_o^¢ÝS;æ{ë­âX% kûL¾Ö4éu¿«ñ±ãU(ÓŸùË Ïß\?öšgfà_˜“ÍÄ?þÉ]uZ¥¥ç¯~§ª ‡ÍêŒýÊ®Êl‰¦ªžº“twÒˆ3žh¬^Û¶½;ø¶mÛ¶mÛÖÁ·Íƒ5æüzIÖÊð¡nUݺõÆ3yúUø®”ëãb6V“8ÒIìü­ÜþÐZ;JöÍJiC\•üâqkãY“ÛÅS®[Ý÷™ k¹7·ºòû‡/-<ÿð÷³¦jX^+SÝt>ØZ¾ÿ²¹—gzW /vü6+{p_Éxu8 k8ÛƒIÍ9ù{ÆBäÃä"¬§lOö‡•ƒ¡0Éâ¶ÖSÆÙ«)§Öù݉uzÞ~ áÓPîaNí;÷Ê›‰W=û¾ÍógPõdö 7ÔÏÎ*xZ‹Ã¿qøå“í÷åÿÁ>¬Øv«gå¿PÞ\õñ°o:}³Œ—Ô«Jå6÷íâÛÜWåV©XÄÿòwOÈT3ü5õT}?>…L%3‘90s^æ™™d~Öà"x.|~®ãŸÂGáÛðx MЉ\f’®*=ÛZMÒÙÖFš””Ž4[*Žn¹Æ;Iy¼ù&íUn¤Ü$ÒJ'%n €°GŒCÍ®¥Ü‘ÙÕìwØ1«fS“‹¼{vçˆ:ó;óDݹÝ"ÎÌ"¿¾qëe/ùñ_ò²­3Çqý˜­ÍÍ­cVšð‹—^ô÷SËAý¨‡½ãmýëÛÞñ°ã«~rêß¾igb_ßßwB²'”óq" ‚t‚SSS¶?[Ëçׯ(Ð2 Éñ°¹âû+Mô #©ƒãÇÖ<¯o§8}»çõí"äÖP»ÍFDŽOeÏ[nïPÔwxmPWª€VèdQô*ZW<4³*½û§BÃur%ä]R M­›v 7Ò}wP׺à €4èW©úhä ”8"¹.J ~ŘŠZ¢ë’0e¿¦uÍ:€›vjÆÔ,#Ⱥ¼~ËÀZ@Î… —9ps tZÓz¬œ.—ãôP´to]ÿâV{–×.uÇ©ä²=Ëô®ÿý°Ä™χ#<¥a¥ëKÅÒ¹ž¨-úþbMx.Ù^ø¸]lÓ,‚+lhe°ÐpœF²¡…{Ã-©É¦v XwÝz›š˜êS 6;h –[amnzÒ-íœÒõhHÉs™’ÐÁÿ™{ø(‹­qxÎSÓ6Ù¾éÉöô²›ÝMï4B„š ½wÒ»Jï‚4ûµŠ¢H±]¯r-ØÛÕ Ùá›y6Á€^Þ÷zïûÿ}úL9gÎÌœ™9s ¹ ûDºá¬G6T€êÐ 4=€6 }è ô*z}0å`D!„X°Ð18‡q’AÐi#@›ÎD°ˆdLæá€²'ÿöî¸;Àu¯<þ8‰ùß` {d©mš~Sµ)–ÄÌh¿!˲ñ¡FÞß?w}.²hÙ/ab°¯Ì$ƒ`ÙÞ.îÅ¡Äíãoô'îÍ]Ül÷S]¾‡® žïÑÕí®ïšBÙ%ºÆt-„‰ìÿhIfðX1>µºqršÿtÖoÖÌEZ¹²¼§mꪤš¾0,Â/@ˆPŠqþl„Ú ÒÃýÄp•W‚N¼)~þb¸Zìðfu~,ãÈÇnæ·´‘*/ÜN>"•b<‰Uy1¢'+1ž¤Õˆ7îðéçO’zIßzßô»£Ø·Ã»”*~&eÛYʹۅP4ÿÖ/Âþ¬´Ñª=¨¼A5ŸéùĦ‡>ƒpðiv%“æ~?ä~úᇙTÄtI¦êšÐÅz z_Ðß•EáR³6À üàrxwŽ\~øSxáî†íGû%Yï($=æjÑ\*³ è:œì‘4ÆÛ=¾i°`×GîTyÌj_þÆiòsÏwÿô)NµöeRtºf”fóWQ­«Msgnt»ÀbU8¿æ»å†¸ŸƒÏz›qògîoRàï7÷Bi1µÖâ~‡¼ƒ³ #­<>Ô½Ñ^PÀ7¥!V*aÉH„îÔÈÂË‘GK$ÇðŠW²LSÂ\&i^); mDóJÛ+7î‡ÅL,>ŽŸÄñ“ÇÙ¾#«*ï³5öm$’KäÓr8Aõ¬ %zV6¾€?§oˆAàQÈ?|?}´jäÈªäÆ¤¤FR¡é¤BñÒ’ ¡ß4^™»*¿’ÖF´FÒ¸@‚H5’ªp9 •“@EѨù£k† )ëÖvsK·¶¶neC†pº mÓÉ9³O6ö5Å‘øèäé=OΙuªI©šÏBó ’jÈš¢Ì“²^¡mž3ûÔ©Ùsš{xÝxÜí>¾q ׳·Ÿ:\³"`ùÉ_¡/ÞþëÑÛœ• ÉCè¶›ÓÜE¡ ¨i% ô Dº+§Ç¼ë>k×@­(Õ~ÍöÊÎ qFœK0þ ΢R6š5çdcÏé“¡;|Ý'êÑxröœ“MÚ ±Üƒ —‘ ûý‚w@Ÿ_Ž/ P6sò$ÆM½Ø1ëß¼y|ÃýÞ=š=aÍZÅÄ é·ÜÜÇüKÈWzâ°K;¸(‘рäÈnÓŽY°âìÙËϾü´J¹Ÿ¿zŸ‡ÄwûÕÿ&¶å••ËÏž]¾ò• ¹j#®Æo@2[¼Ý[ˆ۶¨Þùd©Š© ¢;qGïA-J«sY$’B ¢û¤}û¾Û·—=öÃÞ½“'íkÚ‚o:o¡[§NÏb˜÷C‰ãq@4p¶¯/ó.ù˜ü1’í¾V7| pâÀ,–YÔ­ž8<ÌöõCþh)BÞÃIÇÖIªóP%ê¢Ñh*Z€D[É z‚nqp<§¬f‹ËiÕ“9SäkŠÙ;¥Î ýg]ÃvQÓÆwQjdíâVtq3ZB„±ÃoN-„ƒ?XÀ• \8ˆ·ÓäÔê:>2ÿætB6è:?êêæ~ñfš À2í5®ææ–ÚñQ½ñÐû­ ÄV¬üþ³öüü»|µkïõ/öí¹ÎŒ™Û·Ïܹ}úÎ}(2P§×ë#;í Á‚J•àïï¥RÁ¡Q­Š‰$ûªýc´Z­èÍôŠÓK«%¦ûQ²2ÕÚÀh/‘™«%¦F«D Öø'¨T^2j¬€x•Z”<"ÿŽbóÎýÛ}–wŸìå5¹{ðRŸíûwnÄÊ7”§µà±®CxKñº?ØsýúžÝ_}µ«Ï¼y}ZæÎýPk0h ÷Ø@½>Pg00=- ²Q¥&õt¡JÑ:ûËèï§Ä2Ôü…Ö\ã%F\úKuH-¢UõT8¯QŠ2ÿD…Z”$0r±âÕ*Za$'ô¼GK“B2£ø;ègZˆÖ ÝèYô.Bà0j:„ Ÿøøß´‰:¹ŽNÁÜeƒ?mKW¶Ô®]Ýi{B%¤ížº³ArvÄÓÖ7wqCç·¢ íêVýŽxÍ”M¾1km.ŽtÇšwiEAÇÓ/“Ùª1r¼ j]f§Õâb¿Jxõ/üÖÎy¥â»ÿþ鮟} èñÙ³ŸýdBddçþAéEp§R 2Y<ôV¬Æ“6÷—Å»ÛE­F-Šôù[¦„ÐŒ&ÚŸî´aÔ¢Mâ™z!F#yˆ‰³Uñ²/%%B/˜A<þ¢Š¶®Ø°àoWþyöÊßà}Å [ñâY…²_e “™ü_•ÎÄ‹Z 'ÕµfeAqs™ÏWøelJrb#~ùKŸ²f(ÎÊj­eŸÏ]ž›œ÷=›¿ÏK&žÒÏvìüôÓ;>;4ûñY³µ–û„d1E2™ŸBtß’ Å’=QSûކ˜š¸(/Q¦“@¡Ý&jŸF+ÁHM<žÐl\/‡7ª_­Nã}ò™ã=Rzi9üz°R«}yä¨Ìè­hLœSY¥é²ˆ¸û+ê ÙèÔ…=ï~tÕ øê‘Soü¨•¯¯êèãã#Ï2ùx‰raê•2Î×[!1‰ lݼ1Z[ÊAN°R?ÛBs›î+,N­3%T D -vèòé¾*o`õ,°Û˜u졳Þýz9÷c‰û þ*žxó8+p°;Ú[ØG :(­˜ÞDÞR:šr±›™¥=YšA#3CÍ¿IR>Ã’¤…0K@@oI Ëø‹(!`;ìÑ’]bˆ¬Ù©ÓJ.ÐÒ†“˜MVÉ•¾Ðéµ8Éϳѱ“®”õ2[ˆczsª¬ée(Z4Åh,•ó*C}õi¹zã°Š7¢W¶‹:_9ÔhÈI5ø…TÎSŠÅFãäŬ¤´ž«9rLŒyæ8üå ›Õ/$?_£IKWo‰ö3 -‡/C,Ã×ü¢CÌõã’Ò4ê‚ü?«`ò„ ³¹ ( ew¬B‘SÙu¢Q Ö»Öes™Hß&Ó©À›Ø=®Tël.¥Y+ªEB´Ë[XAç°ð{F©Ëލ¹BT ¬bÀ µ<³R†oˆìÜÂÞø1÷Ƹ|»—ìþÞ2Äœåcú Ì*­ªJ àÇHn?¶g).W&;ìݧ.*+¿™;èr¦{³q:k£_%|¨öZ[+Õ–ºh[ Þõ`%{BjÊÏËþJüÁ¯ŽJÒ24\·CõÔIízéyÌé®ŽÍ RªœÜÂÄ ˆ0[ðqÀ· ³Ã‚ÔZ¨aÜll6 cã¼ä±0!&û³ìØÙ´ôc‚ÃãBl¡>YqÁ6_†Ø!ö`­"";îÎJ©RhIô‰y›Gý²^§×ê:v)vmývèÁ옮ðbんócÝ,5ZuPhV!¾ÇH©áAIpN•20,+Ö]Íã²#Ú`{H\–ãëo &v¨-$.<˜ñ#;ª¶ØlÄ ÇÐEa*©”ž®˜Í*ª.Á@FÚ®õÉ2+$Ë.Z¬ˆ¤–Þ`•H|{w¬àÕ-IrêÚ¯ÖÖ2·dx[m-¤{P[{X•][Ú Ow€Z×üµ¡ssÍÍl\d>Ågù÷¬ÆlOcÊ»U”wë^^JÌ0hþÔP‘_q19ñíÇ«â*øs„B„\<ÕàbU8I}VEŠd9ˆ’¥‘N¼4:‡ÇRP &S=ñiÄEPmVe5¦”t¦¬[EEéÝÅ+‡zŠ/d²ãÛƒ±eÉ‘0À©e­µµnF­55øï¿h•9Z U9wZ×XÈ 6^Ç1n vûE}^L e6(Ž¢·»4„Í‘ V»ÇÒ˨—,QOHШD–’% JÔ#Q±D£ÇbŸ«­ÍQ®­ý«7d’Zm•1¸¶¶ýªÖ‰·E&—E•S‚ÊÓ³q´.n–ƒaRí^d¯Qû WN­ï»•W”–w'f·r&Ínnÿ2œ­ŠÏaІ´, iS‘ÁË¡@âÖ?IC<ïA¥¶Û²À¥ñX b9’eUвy«™ªåD…šîE‰ÄeqPFø먧ôÒŠŠneLzŠÉSz6Sè)}¨R*̱ÚÚ,5öoü!¸27ÔÖ²Vúë“Ê¢ °xq~dûE ÓNf-‰ðÒ™kÔ>À•Q …I/òWPw4 -D+Éø~@O¢×ÑÛdõ!ú}Š®£oÐèèÂÀ€Â@fˆ†xHBéÙÖ‘.lw:œ„/Ò2‹NêXÊ6Ññ_ÐÄí €Îå0“!–¸h’pX]‡ÓìµÎN> :’'á¼hö:»‹|G>!Ä+꜄M£¬Y¤(ðZQ°’±šÒbTv“]ç$YXŒt}hŒ­‡Žì‘0žäI‹§õ²:@Kk‹Kb2,’„––ÎkogyÒmÌ—ƒÚvË*Ò_‡z‘„é¨M~< Õj$Á~ƒ`Iæ:’ƒÅ(šEô©UúÜåpuüØ=¼*}$<gòÊ)Îéýköõâ–Š<Á^ôtÅÁGK/ØãüS+zW\ÃoŽ(‹9«>)s~ßp#ˆCð¦b1ÂW&SÉ"U‹õžù‚>X/‹ŠJ>š5Ú‚ UÃýi²ô¾U&xãUÁÜ/¿€ùZ€ÀÀÁ‘Å¢:4åŸí…JBKÎ÷ƒ^ bùÛ-S‹¢’ñÏñ—®Æ zShù¸ÔgüÄøPuà °A²@¼N ¦`|)赦DÇŽTí Téþç±HðWo6¨ž`”ŸØ4~ÙBD‹{4š|¤æ¯<7¢®hÌྂ#!;+mšÃ/ŽˆV”)ã,Ù&ôimm ê9º >V&¨\+¦ÊBòGÎzöuåÓþaxÒ„ñÕ£Smª¸å…{ýú†§ÆÊ‚ûæ›ÊÏlQƪ} ɇkšbßó}Æ›ò’‡ùÍ=}z¬ß\•sx/Ÿn–ï!£U_Ž2ùòʉ²¾Aµy&™øÍ/mh¼ã¾$/Ûv¼àeÿg §YwÓöî[=±¯yasNNs÷ž²8>ŽOöI °ñÉÉ^‰B“!¸È¿,;oóON““§ò¼*-)µ­*Ì> ¹°-. º-GÉ¿–,¸ºÙ„d}²h ´ 6ÎÆÛ}lbòmÎ늴‹ìa€ðfÊ yƒ•.A\ˆrÎVê%ÄjçòWÜŸ2Í=·?{_•÷ÐöG²„©öàÛ~"Íw€?eYe÷ÅÜg÷¾ÐŸíýÒaGjàâŒ+$D[ùZux®noa+ %¡LTŒzÑ Zœb‰ƒ-'o1F² ÓÚ#UÎ,*hôr ]ó¸TaôZ³“¸ÍdH&‡Äu@B²é€ÀÉ.‚™à5ÿåù‹‘x¾Ñ 5§™ðÝ{ðÅ/g€î±¿BýõÞ€ñøbèÞe’R½l —áZpOˆPªÞ™-,n€Cì[Óq·šÔ Âè±:M]jå¯àe^ÂGÜ«aîáÇB¡˜yûà«GŸÇ/|¼×í€Þq7K{èˆ0¼ªÕ­…—Cä{2?¼Š[ñà‘Œ[ŒèŽß¨6'”8#j̾[j(:„°®tÜ­JEÕh¨ôx$FÑÂZˆauR51ÄpiÁÃÙvp¼Z“´·ãÒ ºp ï³Švb(è›7ôö•UâAbu$•Ò,0ô¼PЉÑ"p©…U¢É$›W/÷*Ú›­×ô¹Q„£šŠYyÛd¿ö¹‘»l;›!kbs¿i³VŸ^›ST˜ž›Û'ç}|k{ŸäÁÜÐI‘½5úÔœb±ød]¡õænò˜ç˜8Fê£)©ƒž ¿Ü Î?F­«òçǼc¶0‹« -övéñõª‘CÂÍ‚û±ZÁ´ŸéU”X^ÐÜÇ1I‰ÖƒëÇÔò' ®>´Ë§`¾å³õ¤Ïꨚòˆ-£?o/ ZûÈ4°üG·áo[7Xk e¦å³éÐÀ[íÜ?ø ˆ%˜ BaÒ#.MdI¹­E›È̳í#³Ï1ô8z =^F¯¡ ¨.6]E¬.+]^PôSù[£ÅÅjÉ`¯¡8wXÈÈKãŽDX5V‡ÉÚ ÓR¬;Ùßu$6…Ã)v„hÔ4ÄB¾£‰UôSi"ê+)ÚI¾u8Ø-.ƒ?ðtWÝÚÅ%Cç´ Ä0ëœ.jw@¦ËcXÉß"FZôhÆî Âú‡Eµ´rV5N)Toa{*²|&û$¶tkÓý5³¬x+èðÜ–ˆhhË×¾õ 6°—›— ,¤1ÁãÊðç3+¥a?Çà —\-ƒex5Ëß_9ô=fïkÛÁÜÂ?ò0º´ÛFÃr‡õéÇyæLYYž²×ó]#›ËZ}8æ’ Î"ã‡'(õÊðp…A OŒ‹ é·~¬%¦eFµ"9{¶²¬CØ ,&×–’™ì–…ÄmŠ‹±Œ¨NVT×ɳGYnV=,3Å&~YV0ÿÜ»Zt½`µNCw%Y W ô­ëžÊEÕ#]¾†]P™?7=;Rá­n?oÈ,5²»ƒÍÞíñ©Á©Ñᮤ@ác\ƒS5ÊšcK #UÞA–ØìÒd_µI—ÊY8´jQ™RP_b ŠÚ©ï—î'êã¢*ëŠ4¾±aóýn¦×t͘é—¨ 0øºâ¢7©å¡)Ì`e¤"¯P™™›dIÓ´æbg•¨FU®.÷šÑK‰ï¼!‹Ò‚ÆúZ’rà 9­1‹T%³Šy•¯îµÌ·d˰qAi‹ÚNŸž¯²ìjœáŒU«±jµVÖêÃhÃBó.3bè0ÍWIË{“ô2•otJK:œHWE£B­ÓÎÒhZ;;™™<™ñ;㕉~ì¹s,¿pljÃOîlÅ»[÷ž|sG¢rRyókO™¦,ußZU6tI팠²œíc[ ÎÚ»: qÜ—‘í6Ç­qi€nî€ÓNÉÎ*ŠÓˆöΦAt²Ÿ}aÝôç—²§w]Ý¿·³¯qÁÐ;P¼Û3믎Îx!3> Ë¿¹h¢yÀ¤ýüå§6_>»xÍÜý÷õbqò216„Õ·½4vœ0þüCú3ÓLxP‘kâoóä¾H‰´(éRyDK$S¥Ö+ •VN—7Ì#«b?œ'¸à„ô¬Ï9\¶a2ìg^À;y–iy}?^gîǼÍ\•¶¡Iø8ÿ> ~†ç9¼Ã=à¨À´¹×yí„“xýèŸÿ }8œ¸ÛR¢p…Rè,àyñSª+½ÚØõžO±t kt©7»ÎçÅ•«ÎâøþOU]»â%8—e‰ÉÌŒ‰ÊpeÔ¤§×d°ï„ "æ×àåϽ± ´°OÅg>öì¶èôôhšŒþ¢Ó3­°x`»u«£Êï(-¾Ç’[©É,‹Ôæeñsù‹]â™Oâ“x¿ß§§áR¼”þñÇüvùRú{Åß#ýñ{§¿³|äƒv AXÄ¿ˆüP&ÊG¥¨Š ¨ŒFЋ¡:I*DR9À[\v'á¤]„/u8u-Ý!än×ÒoÊ "S*Z¬„Y%4Ç ¢ÄíR7‰%¨™¸-Vž8$þ–D•ÓEI>ÌOI‰ù}“ó Ý3SSG‡ ¯ h­™ö|U÷“‹ºVÔ MBÕ÷×+w_|²ªêÄâý=ÚBà¦Þä·|ÌŠ;5p)~ÔKûj…Žù%P¥ª‡{¸Ú§-gŽ{i_«Qàqz³ß²V<3ÌúøÀe¬e^bbßyIII©ª:5­ºE^=8œ¢Õ# _ɧ«ªŽÌ­nöí1$Œ†êFöðm®žüÄ©|@¦­Üï£z©Jƒ³-ÁúÍq…|fÀåá1Š¥ ?Y ‡L]¤ôÅß…WîòV?_Nûë­aâ!á>‚u Ò£x”Õy;Têæg§ßµwu“•Ed‡žŽHQ™9=2ÒkjÒ3z0ù5ãjk‰ãæ§Œ?ŒƒñŒŸû'¼/uÿð<™OáüüÎûÊd?Ép'mæ±aÖåxÄò¨PÖѣDŽšždX[{ÆSÍJRúŸ?G’ÀSR6Ïâ\·9Õ~â„=µ4κ|YTì2ÊŸFv(‰´Zþ$dcC£–ÚåÖ°±fÛIwûIû¿ Ùþ’XëòåÖ¸R BÄÝ•™’DŠ:$Š:¤‹ tÅÜŠÕе+,R%ÆÕüû­’^[3Aª·TüØ•oêlZãXäs{øýýýº^ÿÕvžKT”? ;¶Jª&-\¸»éΦî’/7Ë^(M:1ÈüTñoî&B£(âÝ¿d1!RjÓœ ¢i‘É ¼sV‘-vZ¶d³·:/ß}L.ßîÍm>ÞF/ßU ø3rù.ÇÍÅf²8››Ø‘’i(™š7FQ0'ob˜¤Aü2Ò¡ØÎM+   ´XWÐK¸tWYÃ)伨¡€IüXŒì Xòaxܱ#ɉ=@3}3~laŸµQp?+4lû¢& ïÚ±¿Œ8ôž„o º=~°ò£j9ëëŸÍ¾ˆÕø¾Ï¹Ú ?õéã YÀö¬S¸jþЧFúH ,&ÐÍlR QPЋ²Ò&³‰TÎå0èQ©‘ÖÎ>ŠÇão‡ÂÒcG“ªñW36A­vAuœÐðÐ絡иsL>ô,h »}ÿŒGÿ­»œó¡õ¯áÁÏÙÚu?7÷õÆÏãvZŸGo%qéü{(¹ã0SÚ5.Hø|£Ãníõä´þ˜pîÕdƒ¦=ùÔ\Spyî“^>skE±¶qŽ×“sÍ-Yâ·}æ\ï:Zo|‹çßE" CY"I%"¼…ŠÔRV¿C1;Umá&_Çg>û Š®·ÿuTÃ{P»oáqãïŽãï@~‚+¸v Ÿ¾råó­ðTàãØ°e†ÿ:Ê+No…„¥OøxáéŽÒHIrR¢Y+•FpÓES¾Z¯CÑgŸá3×Yý>|›ñ!Rþ¤"OÀ”k× „–†ø8TÀ‡¤4ðšFÅ!§á_hiÛo}B‹!¥ý›KM Cœô.²¾T€–Ñ ×q|ëñAè çFÃP¼l)|³oÍÓø÷É̼oJ§îÛ?ÔüŠ`#9û ä;¤—­ZgÊ¡¤*)Ô'5wÜ~J†”n×›}}t÷î©®îéYIFßX‘˜MÖ ç7oD®‡HHÞ¸¿?‚¥Ý]iUUi®ç•IW6n¼R‘¬€¥¸|=óöÊvÒßwüzº ©“•Ô)!³GÛ3a…è•.È­vIÃT‡_áé=]¢é±Ø×9²kÊ:X nX¹’a ­jȃ°jýƒªÖ °nÊ®GÏœ9³~‰vʺËuË—nܸty³¾­»š9·lɦMK–¹]êªaë ·'®7÷öºÉº%ë_!ÄçP6’1è GA×ÅF‚¦Cœ‹Ž”탟`#$ Œ¼™áÄû™)^^ín¯¡xN©Ük»¹åÍÊüf…û×//þ幨4'¸zg¦êCðqžtÍH„ôTz™Ñ%¶?R¥%œŸRÚ’NÆh@ ¶¼ovÚð}v¬‚qs½xÎkŒ]½½Ï‚á©söâ™^,ÏlŸ÷ȼ3í7“£¢’Yþ ñl‡"2ãìEŒ$˜6ž€ä…$±f¹Ô9 ³šÍØ5¦óž/"…ucŒÑa –]»úüy°¼ñúÚ¹ ý9=uÏ\.!v¾úö›øÊšè¤@ÍìSN?QZ_É6¿ß•>^Çgå?<¿2B½ö­ ø*X/¼µ6&Ü_5RG>žŸEªEz€0Œà@E»0bà"鯂‚ ï¦HÞá̧N!XåÞýÐÄü =ìÞ}ïs{á]û÷nø‚üãÚµ)k×'ÞüX—¸¡©iCã̉ÁÁˆCŸ¡\¡ˆEÀA&”ŠÐo‚Üq`uh9kÇJKN%OiϦñà:ÅÔE¸?øü§çÍké;÷ůà5ÝL¼ßÜ×W¯=ßÖíî}çÏrÁ|ƒ4±Ñí¾èÛõ3óø¼§çÏû1Q±Óêôæ{V¿AP4¤²‚99ïéyä™ÄDÇN­Ó[Fì^óÆkV ©¬D „¼Èhÿ¥½uŠÆê´ªÎ§Éê‚ꙓj‡:F IK rZ˜nëòÂÂÖ”—熅ñ×ãßÅr JæÍÕº"vΛ¹.!È`±-Qb?oV`•¾¦¬Á¤;żL`bxø þÊK³&¾±ç½sÓ'~Ž¿Åmø™¾ÏE?2HÚó*Û2®¾ÒÞŸÝyÃ>¹Ü>ŽË{ú™îGú<Ù¾Êýæ ¨yn%žÊ/¡ádcƒ^O1RINÈôu[Ü_$£Óåao¥ùþéF }œ~ØB˜ç~”Іý?í¯—«€%Wx6´Í= ,vŸžÛ¶aƒLQ?yÊþýS&×+dPÑ­Lás‚»‰Ö‘AG˜¬×¿Â•ý7«x4¤woîÕW¹Þ¯Æyi~£¢tŒš=ÊM˜T6ÕÞ)¸tÏC€öQ¤~r„¨ÚMѬÞàò¦ˆQð×Ý95‚,4z€½ø/# ‹ys,b®@»;G ¢¤˜ç6¸Ñíà Õ´H©E)I½IÝŽ‚kt¸œ* CÇF1…±:lZÕº$íeZ¬¶vx ‘™¼qÅòÍSâf,Þ»iÊP[nºéaaÛ²y«‰Jˆž=ûÌyÄ[|h¥}÷ÁÓC<0ÿáËÜà`’œÕàÐ77|'ôÿ}¶WÓºv˃/nU‘Š4"D³EÒ[3*鲇ËÙiªDFH«x–ˆðüt7výà—%jHu:ÅFOÙ·ÿÁ†ÑAŽƒ=]©AIxï€ÂqqþÉ{Þ§sŒžN> qÁ)$«ã‹­µ|yIÉòå&ü‚B7T”¯_ZRº _WAf]¶¬´tÙ2#~Ñ¡++Ö/+)]¯õGþ:Ás(B 2 "i3tHeW‚ ±zQ¯+b9ÿï#ñõ]`ÁW *¿¾´‘?D€n~=¬€¯„}7þ"‚ÙÈâÃøZÀuœÐÅÏâCP ¹,~Ÿ;ˆÏâGXȃÄ£³ ­¤ðIíu=iŠ1øµ´:=ú¢Ò§ 1óZçi¶Î<‚]Œã6Ÿ¤¡£]8í·÷Ò¢­3`«1Ú˶¬®«)#9ö\ Ú²µ«»W–¥À9ÌÏí§c,E#î{©­ºÂ)“1tĹ,Â\ѽí¥ûFZ£Ý—‚âuñäOľ̲±¡Ñ=ÖV÷Xåïå嵪GõÚÑ¡±,ë~_€D<fʴΆxqXcÊQŠœ³¸gÏb''*sLLİG Á©•áÙÜô Ø ò¯ÓÅ#/ô1ASEÚdªAmhZ‰v¡3ÒÄHq@Dkèâ Ë>Ço>¶«ÏÈR„:ûÆè coã[jŠ8WGhG`G˜KEzÛí@©µÀÎÚîH-Jþ.´²Å/Ìê åÒ¾M©U†9"BaLÂS±L¸#$ÌÎXN'„9CÂa¾ ͱaŽÐpG˜¬õáö¬é^{йÜ~«ò3QTªÕFµÒ¨Q›”¢78žx…JeRªŒj•I%zÁ¡±ýx^eViŒ*¥I­Vz‰xÓÀ$•\¡ÑTJ³J¥ò€è¶Ý¾¾$‰Æ RU*%Dz7™–P{(ùËÕbyºO¨-”üÅÜj6%<)4%88%ÔØ~޹–B? ©—»ËÖKœ$ /Nv_`ñ_ÿVžõvÒÎPÃø³œ‘”@ëLJ÷¯‘Ì?ÖÇh1‘:[hˆ*„¹êΞqû°ù>>Ò÷jšÖÇ‹i7®³…ü© ¾ ƒx¨¿…ùKü%†ôÈŠâ¥ÍwÏ ŠF¨Ði¤çb艗ÑJz ‰b€‹„¿&qV‡‚¬ŽX/Ðÿ±ÍÍcoÎKåÖÆ‚÷~fýÍoزšöwkº±ê2pÏà/mÞ¼}å:÷©µ+vìX±)Z¿rûæ7].öúÜ––¹î ãfÍ—Âè÷ˆƒ“ñ»Pêú"·Æ’€†äü0=*fèàqJA ÷Y&H´¨œ”¬$ªÒ±zDH³“$æÕˆH/ßLS4@tlUhx°B_]¯‘‡†UÆÅĘÓ}½""‚Ûs!üœpðàöÌøÊºÊøœ™Å¸<Ì‘î w††¥ÀGZ¦CáoÉ)­æª»•Çøû(ìѱå11å±ÑÖ0•·Ly7Ušm P82Ó,‡Çèš6bùÃÅw‹­ŠŽ®ŠÍuâƒ|Pº#4Ô‘î !¸éApóÁM8ŠAvÔ@‡A‹K+"•ÄWX)èæ°’ðåTú…tÇ ¼…´ÃêÔ¢ÑdB´ ZŠ*‚,—Hñ©E:µÎÌ€0$R$.³7nŠ‚ëñíN¬önLMØæ·$¢Ñ?©Tãgisn7÷b‹4^œ –ú„F_1VÆfeÄ©ƒ†ô7>6´Æ‰ë?v¸+³j*b•Üçý&ÍŸj•šþ0$äŽe—+µá ÕùöÈ)‘†™!“ð—9 Æäð<Ú˜®Kföô³ +4ôoýáËŠÀÒ2Ÿ•Ü¡µøµ))Àº`Èæ–¨0´$‘·^nò—‘ŒÐs5jBˆ6°ýv «îÖ04Tv’RMu”‡¢~#-¥tÃÄ*eEB˜gÒ¹Ò¥¦ v9õ“‡ÈCåò°ÿ09Ótój­ï®U–¨pML}eHXp€.®*:Š’"($¼¼!Zn±¬Þí×Ìåýâ£Ún7>qÏg޾þîÛÓâti~ÒøZ XHK°ëà—"÷ ~îó£Hƒ­—¹C¼˜ü#VjæRþ}BÌý$)'©ðø_µ´ÅJ]¬:4T '¤“O•Óe×R‡„ÝDÊ›ÐñÚá¡ÒFô »ùÕ¶ 1Úˆ¬è_›åoº}?˜­¹@ ãÏ ú¥ÀnÉŠh ½RòÝ>¿á¼ÌfU½5F®͵ÛöÖû¹Â„á³#ƒÏ8£¯143”ÿ|qÚ¼Ý:árjúÛýH9ÞxD@Ý9üù«Eé_Öø £Z^ªÂÇ6¿æÇz_,XÓÇÖ¼I æ?+|˜Äz³‹î«Œ;ÙSþÊÍó¡ýØV-âЕ[X ØFþ( Å¡R„îºuköxU^þÎX•Ä1q‘&i”´ ¢ ÖŸ3…Ä2d2¯‰²°Iü~È“­b^daÛ“¸ýÐÛã ÝA}q{Ÿ>â5øèé§ö²sËö@”Aâ¹|ZÎæÖ9“Ïîgoœ¶ôë~_¯ñ >ó±ª%O]jêþÂôG6¹ß|0Ìæ£8<ùhÿ~G 8Ò¯ÿÑäè¢Ê½êë7ì­,ЦW'nýƒ9Õ¡3È®£²é )é^µ+˜¶Rñ…$ãÅPîm}k#Ç¿ãuý#ž›1'Ïå.z’s£Hò0)¹Æ3û­õ¢ â$êi§S0½zz…ún žu­9裴.êúõæß­ÎœâÅ«úE&×—Äß¼´1~ï\].w Lº•È#y‡#¼NÊ,ËS3…Yªèm/ãfޱÏ:ß9jmèá~]Að•ý±½«ùwÎq7¾Þ³“›¡eÓ9ß}7ß[”êµYœbà ¸·ƒž¼õV>PÚ #%ð„ŸsQÀ©~#c¤ƒz:ÂãÃ9|ó å#»³í»¼~à0þj`¯^lÿwSùŽfçbÿ f­»ñ ÓÑéò\îoolÙB²?qëkà¯JbHEkwÚµ*†·*T “Âi5œ‰çÙý~¢scSa»ì‘‰üU¡=çkßÂ={±ü¥›ñÖVH>è-]QFLÉMÖ™›¹CrÁH²³›Æ£ÑT1\eRcø«µöÄ£ÒgÅ®ä càšµi‘LGÖH@óHí·Ú ¤}Ô(eÐ׃€rAÒm7Ê%Irç:;kµÄÏÚ¥ëV£Bº AMæÈ$*I¥å[ä?áa!C›”¨ka¾î«KLÔáq‰qñ…ñ_—/^{ÓgÊŸ7¯Í6´­mƵ7}§Lñ%¾¶¡mÃfÜ7nœsÜ8öx|B\|A$$pͦ¥½¦…ÚÌ/3†# qÕ›¾“'û¾ ‘¤C‡Î¸tftaìC xÌJxùu\³:H4¢äD¨µ¡‰h)Zv£Gén”-Hw×°zÚ\†ò¢4¯1¢ÑduƑרD°¼‰:•$ Z•ÎsÓÉÂ:t*»-‡N‚YJH(‘ *™ÃZ¤þNeÆu¤ÐhÉ×:‡¨l·ÍEL%¹4LX©È‹¯)’‚ ªD ™»ô 6 –¤‡Uðâá¼²æŒ<á~ÝhWø9‹ßæ¶)â@yN{®%ô~¹ÚþÝÃÙ•¡uqìfåZ¾ZgZåwܹe¾ÓÄ$µ\s«ÔÞãíâƒ#ÄйÑÁúx±IIãô<£ÁGÃ&ó`±ÇD¼‹_`‘Ù¦ùÜï:਽Âú›|êcÐ mA‘ Bï>(Ù¤>kWˆt!HV„¼ER¡ëRYEA§uÑçö­3éš«KC+ëR‘y´„XX«‹˜ˆ¬UEb˜wÝÖ¯-öueµB¿î¸O~ž“Û«4á[Ããïáø‘*Ø™§éb(JF…¨§´×ªwdSEž£W‘x<çVôùÏÍö:÷÷¹¯%›Ê2‡ô®4s“%ɉÛçÏ"á­>õÉÛ4Ñ©ÁQö@Ë}AêÖ–ÂxÙHüÄ 'Q( U£V„ <ÇåVG8xÄþbÁAw£è?À×èÛrh«}Dqé Gßæ†î£Æ––”u‹î×P>2yö¦ÚöaKÌÓLmfó åˆp¥Íok", Qê4 @Ö»<¥ þG¸âoãŠA2d@‰ƒdí,í"F Ù,qü'$sòÓOÕG'OÞŠçà•|¶ß‚ÅxBËòåýð¼ÿa„I£A/OgÑô\÷nú dñ€Îœ¿à™äÀ%Û÷¬³µWôqÔôéSc­.Ʋ?‹‰7[Fƒ8®uàÿ€1ÊÜÔ´\Y®ý/µ¿7i}*£’?Ð)‡bQi:v¦% uþÿOˆb«°Ð•žŸ¿1«ØÐ³™û¤Ž`ÙøùK÷­²T¶ïú³½äɼ ¹¹òÀ°Ö¿ŸCŒÛ¿eÜ®u+ûµ»ÿKD# ”€ò$¢£JÿŸPÇv†Áß­Æx¶üYjxÂæ>Í»§þGб·¡S#+JGÈõçzåOBré?‚@ø½™ü#HŽ‚ id«¹ÍãiQOX<Öb‰Ÿ5Y kJ®ƒsºx¥NbÜŒ,ÝÙ¤·Á.1ij‰E#ÁüYŠ‹éú²Va«ïëÏ¿d” “üþ9d­6™mÜÍôˆšîÿhàÙöɘÑÏr,ÏŸõ⪃^c®0°XΞ ŒxßÛ—çñ‡ãù:þ‘ºé.­áÞ0 ˜onÀ'7©€ MÄ øºÎ:8Ý+Òu£CðG²|RÍ‘³¢}ÓX¼kl°7Þ© æ#ÞÇÏJJšƒÄÛ7ëTÈŒl(“tçf4! r{aà¹ìOùS¹Ù©Ó„hQ£wº(So1R¥*‹QÚy ap]„¿5ð:ATG€…]g‘Šþ¸(óë0 FÂ@óBÐx»(–üØÚqŠÅÕŸ×BæÓ‹,öÉ!éè3­í®Ô»þàÍ­Í×…ÙZU2föÆ ‚Eûß|½ºÜwI²Ü»b üóa6+4e cµü‚ྞ7,¸¿ŠW~CÀÝ¿¬€{w8 åû¹«ÙáçYmëõ]Ü"­þë’€ÊÉÓ#>dñO‹·Vf¦LMÖ~žQôkÜ÷øï7Ù¯š„YGY¹Ð2h¨Õ¯)šòº4zž•Hú@>¡Ÿ~h6WIŒ—#å¼6ƒ:‰GÚ4õ™F 4æ²$‚‘’˜¤3È ÝÙ¬YO/œQcaÉ¢Ýê!. +EÑJb"ÝÈp²`ý×Cð¼pßÏZ`á~÷?aÀÖýIÐöw¢IçáÇñÔ¿63MîLï1’(‹ª\RÙ¿‰ýD914ŽÙ±H3bà–ö+ ~e«ØÝ‚ߦ¡Šq P¢<¤Û'àÅ+?÷° @p¹þ±‹¸×½aÜö÷`ß—un%}¥ôÞõ",ý¨ï~F ˆc_3²)ØêWÜ€ñLù;Á‘xÏÇzÝðîý›Œ¯k‚}A©]–Œ¿jQ ø}o¡þ›¥<þ’—ŒøÛx‘E —ÄYPÉ@‹UøfQÒÈ£@Õ³Z¨Ž8§=Dª˜Õj4ÿóÏ3§Vöï¹ýQ¸xâàoÛ{ö_yŠap,;õÌÒ™»í­3gm{`+Ì\zfãÎü=fzyõyŽe,¿ÃËÜó¯2¯?ƒ }Ñ+๞S¶_ß2¥çs^/ö¿ gˆdÄð7Ì’(«U]$i.R¹èƒ$¬ÞÀ¦PÍôüÒse† P³i#Ì •ŸœÃ_ûÁS~Ë$ñøùZlô«‡_ïŸ:íãé¹úĨ ¦vÃîõ1E üÕå;˜OWqƒ<&j™Wï@8ÄšæßhË™þ·éÓÆd&\Í,¨ÝP[[±Ï†X´!¡ÿºô†nDÇY™É;4·ÝVÓï¬1ߪÔ!±1¡áq…ÆH.÷þÜ÷¸Y7g1_Hw|âÎç_¿aßÅÍE\—òè{½®{”ø?ÔNKåßU3âê×~gýàðÔÙòí~•æï¨t *GÝÿDµÿ8àü#pÀù'€„ÁØÿž wAÞ‚ZÑvóÿƒ&v¯ù3äÀEü¹>ñ±ÿ>݈¿ÃÞD4Íüïãïÿ/xÅçî=8ü`ÿ|ÏñÆëš`ZÖ#ôï"[ñÿˆè9Ó!À½öBÿ¯»G×Ñüž.XýGÍÐð¿LÍ¢?¼ùng#R Ì‘Î"B$aÛí •&P+‰KéHaLFÇp§ùþ@V¥ìº'aЫ/âÍxÞüâY„£C&ô‚ÌÃÇð {ˆÈ÷Qvµ!S™i0fFè3ø«Ïá x ÞúÒKÐ  í9·ú~ñðaÈ8trÀϽžŸ©×gægEF"9ÿª™:] âüwëéÎÎÍ4vøÿ­:¿™ÕáFL—Úß]s[W‘ÚßÕŽI ÅGêIñ€³öu·K‰DìÅXïYé7:ƒpïRÛwu¸2sÿ‡<ô[ƒç:-z…Ô ÑEì~þʨW^AL—Ñe¦ÑL?÷Ãì$j¾Bþu^½g+;®þ‹ž«û~üŽð*~üö÷ þ ü8 —¤ þ-iÛ…6‰“3+«…O£• -9ÉÒ[Á™D…‘wÃð¾|ýÆêÖrÿ ÷•›Ÿãk˜9ÞyÈñi,3 júC1L‡#8jâ|·¬ì×*÷Kn!GèwÙÂλ²âwÞ C <Ç¿K¹Eo­.«KçÒ‰Þà±µ*oÐVò¬;ýÒ ‹cO¿ø8TBåã/=·0®ÈˆßÆÇƒœÓø‡cá‡,fô8óò™¸ÅqO¼ò>Ýϼr&~qÜýx8ð£Çðw§!÷øqüìi#½të+aÿ  Kú»%U#R©ì”ÁD±±¼˜DG[–"*"ØÙ¥Ûyôj§G0Ëjf­4H`åÖRKTITt‰Õ\õ‰—Ùʲ¢¸¿Ú —õ {O¡Û¼‘ãñ"˜”]X4=/ZIQVH0ŒÄ«öWư=!%¼§Ÿ¢aóXî’µÔJþ¢¨yã`aúõRÈó |Z®Ð+9Jd.ζ{ù¦-œ^X8}á4ƒ¿·m¶;ÑÇÜ‹es§áyrE¯d#!$Dð—P"żt,O€òzE˜¹|AR#@…Èj3,ô¶tßEd?myHSPŸ-ÙŒ[ÂmÒdœÿÓùƒ,Ím[ 'ªœƒœ-}‚›CÊAëþ$’YÈ,Ö3Oá732Çw;ÉæÆ€ë¾7Õ+_‘ üa²"ß+uo©o˜©‚=Ùm|f$1óÖºÓ™—Ö"M¿õûiiQŒôZ‚;µ³u*gcI ™5k¼¤pMaU)¬ “õN…k¼ƒ„fèïì脺BéŸ~!Fö$ˆ[!X(fB_ÄŸ È0q,Y2ÁžùKee%žÀÉÞ)JÝRÑšìË1? "^JF†/WòšÀÀ­.&P§áv†ÇúÞ\“–›dÈ« J Ô&åL̃SæQ -ö„Þý{$:Œv“W±€gËa¼jñJžë¤´Êrí‰ã˳"³Œ÷‰ï›”˜˜àKp1™PícVDdGÈì ýhÂ@ ˆžéÀzÐúƒŠb€‘f€Kg»KhÁ_á¯@ã~Ml5»úRjôó f’µô=П¹»’Õïà“‡’T¯E¦]vØÛgÏnOùšÍl¡¢B2Ù_b‚¢Ú_.ÌÎ))™QÌ:ÛÏÏ()ÉÉ.dÓ¡‰äÒl~j3b$°‡IµÒƒ·w°-´#‰kU‚ÊÈêäÇUÌ[osÚΖ±3f|T£¯Z±ãåIç¾n™Y‡‰*FüIÃÌþ:oëîªiËÒ3ª•Í)À-„îiîÝpGÓÓˆˆ&…)\†’'_­±k.5'Jªÿ,&6\öξÝѵ™OmMI¶&[JcrbÓaÃLJp´ÕÁÄ$°<èõò/+…¼Ìªž½¶îÞÖØ«*{a`o›­÷À¦¤$¿žµ|¶iýŸ혷’™Ÿe›Ecì–ƒÛêë·ÜKgµço} ãßD9¨„®‘-:µ§H¦ºC}Šd€`H„N-*’™Ò¡vE2s€Þ§?Ž>*Otk°ÃzÅäΨȭKÔï2Ë×pܹy—>±.·bFnApì½£A†×` ŒÒ4«©iÿ&F>/>w‹-3¢®¹ÈXh,nª‹È´oÎM˜æÿ„ <á6/!w³-+¢®©˜D5{¢ãç†ÉÎà°¯•ò>žæ×„T·¾Ìü’ОéP(Ò# ŠEIÈÒ j Q5}Âu(Jþ=‰|—ד¬¿Æ©–D:ÆKšÑC¤¬F’¢d=·K-¢AÒ²HOʰ0E$üx_LpÓƒ÷™­“*äI™SmaÀf0Ù‰¶ô„ö‘Á1÷õŒ¶šFõS'É+ª2ÀVœ™ÄE J·%Š`pUT¸•,£Òh¬µE«Q1,@J4ËVk¡ªº÷pq)Q)à« òÆ•¦÷ÂõÕP¥©½£ø‡ ä̦P1!;É’ªîŸ­¯™Q¤Yñ`¹×ô~›J|æ ^è ãcIÊ–Óg÷^ *šQ°Ð«üÁžK|J6 ìZ8$½Òá©F|#U#c%5‰‘”ööŽb¹Y¡iG` ZSæˆòn¿Ô¬ ȆzG9Ê”ŠÈMBÊRo+iL^ºìþ6 @Á¤P1ªA‹Ñ:´ íF¡'Ðó躄G3¥4TIƒ±h¡“¦–ÊÒš©ÈqYI•¶€GîYú@Ò¼æi!I¬NÕá¦<ï’Zuçíá¥ÃáVcçéöáNÛÉWǧN*[Gëàô|Csö”!}AÒP‘lZ'Þ¡¨™]§2©ÕdñEÍ¿u/™iwÚµnð‡ÿ­ƒ‚‚])3K*&-%edb²Í–œdKd˜{’=9Ù–À%!ÅÓ©ÁÁC¹ƒabNöþ1ã"ýŒ© a±KïññA0¤gäæe¦|á«w.^è4úFŽÃÄ„$$&ÛBêêš3³ &½žÇk½y½ÁhȾ¶uHPHªsZqwÁ;ÍnOƒ4Þ;3Ûh0^ÊÎôæ# ¦Æºúà„Ä{BBpëP™4ž+\“êª#mH]CÇ¥5öíÛ˜Æq±± uCÒöX£¬VKTt·ðÐÐààÐÐðÔô´Ôðаàà0âNK¿ÆGÚ ú†ø8ž÷fŽ=ûì1&“aRÕ! ÙƒÆÃ„ÖìøµKÈ:2 µuÀ‘lÑ¥ŽÏi8~`Nb°:•a*·÷llì¹=2&F¦TkÔ…¹¹…ÄR†Å¦ò|\|CÝ 4Gß­–è(‹Õu©0~¥N Ê+Ô¨Õ 1úm½›šzoÓ#Q"J áy|‘…¡h4Í@K)Ó'µ$ý |ŠECEdf& ‘”dJtô$Š~IÜžS, %¬pÐÑ e¡D ¥F­ËA]©ªyB¸cú1wY¼e{Þꊳ{³vsb~?:ÎË¥¿ Ú,–+~/)&ÖÎú'ØlVüKðÈÞƒæÔ¾º¸dë.ØiNàë÷>ÌàÞ‘šÆ`uZj`¤Á²|[FqKPdd¯ü[ ì†H]jš:eè“mCž:üéÖAO¡ö¡¦^5ô|¨WãC̼{Œƒ–í0úõÊV¨ª›PV>fVNÃˆŠžLO׾Ѳðaûç7ìûúÆ40ÌÔ‰Œ01HÑ”üÀO?=Ü$ f' ÌÄ© ÓÝnüþÀxcY¥Ÿ+?QcX´Èꤗ_U¹!nàûƒ Ï?ÕÖöÔùg† éQÿðá­½zm=üp=bÑר–ȵo“X_i},á¯U èÍ­ˆ»y.wêf·\ÉÉoïì®ÛëÁµ¿òû H:+½Ù"¸øi$Mwóf7n"uñÛvá³;ñgû÷»ì‚ÔˆëÐÖ|‘¤•! 2“ôv’ÚjD:š mÆh`ª»F0ö!‘5üó—7¢s·ñiîiêÊ” 3È®2“ ‰¿xÃáíÍïß|³»Â›Ûú|¡{3s_†[F"¥!ƒt7µçKî'þ RyDJAmç4r¤M‰À]ð׸—7Á\±½‡?óÀD¼Z.òW4¢ûã’Zÿw½FdzÊJÊ%ˆ§zpÄFþ]éæ]2Ê¢Ï €Ez.ða]UáÜV‡C•dH÷Ìå.‹ŠD“ —¤À‡Ž’Z9ï”[9ô"šr¾Ý8}ú毠fo§¶œ…9°ó³Ïvæ¤-^’v¹× ˜2¸4-çáë×ÎK_Š/?ŽQKštŠ‹ïÔ¥Ó©O‡}ªÛQ¬…Ã[•íkâ}ð¯àí³K~J®)·âêÔP2f >M€ÛŽ›¼’ø¿F6¡8”*©(¢êpèÐŒ\P£H«4†ßÖ‹ãьÂtEWÏ H$®HªœO`Ø_Æ9ã\rp½Ȩ̂Šsö7-A¤’áÕQÊs` UŸ³oÊ ¹¿„}ý‹?~óð”qPt}|‚{Ù#×w~šÆå:#â‚σ]tºW;Xe´í7=;“:5í°ðÎÚÖhÆ®×ðe߬äW tL<>{eçGˆ#8×ë¯ÂUq‹)/¿©WŸt{ Nà þ–çŸ]ÒgNwgÍÈÊLˆ  Ò%ÆääLÉÉLˆ "KÂ(í×›Jk››z75gŘÍÖìÒÚ>½ûöÊ˳˜ý ]< û.yÛÒþ†Ÿº¤½d’•™“—™f‹ "¾˜œôâKŽA zïà¿•(õ§ 5Ûmˆî€^Úí â[ÒFHWDXE+ý©xÑJ·qt]¡¦<Ò]Ha¾Ií<öqtiŸc QE&£ÑT”ÛŸ *üõ:ü5¨R“ i°±ØšAðR×gé³p>íã:œŽ—8²c"‚ÓÓ îðqøùÁ¶æÅôôqÿ°¨¢†ú-õ E¹QjŸÇ×}÷ݺǽ5æÚ2 ¾®$*¼tèâa¯~LþÌâæW‰ûíÀÐØ„ÌŒü¼ŒÌ„ÄÀ@//ßãC Ì"ä‘BÉC:ÇSPŒhu÷$E˜B”6RîB„ñ.D±»(2ãs=()l¨0¸ê7_r*ÅÍÁá0 ûµ -=Ñxl¶£äÿþl݃>ê¨\ RÙh¤>Š êÓx?¸‚ ‡þø¡a7«;¡Ž ¤> jɇä ráî…¨ ò_ø—]ƒI‚Ûêò«ù6ü1ôüœ÷×Õß»®õëÞï¬êÙ{U•yˆ"è^¤>8oÃÛî‰@ÚÒ çS¸—QÊ +ô;ùN¨ ÁûÃôž ]Èý×jê‹J F£¡¤¨¾¦«çxAZFsG™Îâ®îe£%¿[ EAM·|‹QòQôPߨÀ æ.( žÛ]*þ?Tû»gñˆ7,ãNqeȹ‹Œïjç{Ö[c4……aa&cWÏš•YÙö” àà {vVW{œ\b0˜¢£LÆðPyñ…MQÑ&CD¨|Wph’33§[qN¦3)4Øã+î&ùC`_Ij‚ÊÐ 4!謌Áø»qK÷¯(œØ¶ßw{×ïIÜØñ‰µ3´#æ‚& 88€>tk —N) þhq…—wViyzA‚-$88Ä–œK}Þ^$teVn²'0¡ ½¼4Ë{–´QIáW„DêIZAÊs›Æ? À_èKLÆaÌ)®Ì0D(ƒ=³„ã8.88<Á–K1“kK&Ùr I&aÝŠIXB8 › ‹±PÔZƒ#dFg4FÒ ÈÀð`šc$âQ4 ý¸)ˆEz”‡Z¤“j»ÂxOÒHÛ½‡Nî?Jͬ[Ç„Õ;ÑÛ…¼!dïùn$wñ¹#þT2nJûGLcWLw¡Mk0¥MŠsŠCŠó»Z`ÝŸMHîË„ý\™„úétï¤Õ?F‘Uÿ? ߦ{öé{Dz¿ˆ~:çï°#ù2¿(ø‘?ŒwêüDæô¿nþò/†uD„g9(rîFUJ¶!0,ì÷ˆ£>GVxÄÌ{Œ&÷hÄ ·àSÁŸ«A¼´àt˜E̼‚7ó“Ú_{ÿ}¼ưçÚx9syñ Ê'*¡ïœIxÞ¤9ˆ£¨gùuH#Jw Ïár5:„ÚFÏzÆêÐk:®÷³c[&Ô˜ªÒ_I¯2ÕLhÙ¿Ú9µ]\/&·W\]ÿ© 8þ×”ÍÑÉÛÍYYæíÉÑÍRþzü ¤_ï÷tsyPá…AåÍO÷»~‡ªTͪJ5þ+©ÇÿX5ª{*³@z“|ãH¾Áu2ëžÅG¤huÞVÃÊv;A™ó¨”G>ÿŸ™1~/ɼzÏ¢càaÆéÂoظÝ$÷ÈM—Nµ”uä픩¼™. —7s„Éf²±ždñ½”yƒT·ãìCL†ûE÷ø§Fßo/Œ‚2|Áí #hèÏâÒÉDk—¬E+ ¦ºµOÅ×`žš)îÛ+ ÷РEÜ?Ià@¼µ} ¾6 9Õí>ì)‘©qOÍ4Ä¢I·„:ÞŒ¼‘Lº­ÑSmrÄ`IY ×¹‡í F¡¿sÈÿ·a;T@Å÷ñDà.]n9…h9”MÃyðÌ4|ÊØ÷Ü»™F÷¸wÞ‘¼ˆå·â¸çùG‘HowšøûÒÜΛ™Wæ½™Ỡٕ»¡‘‘å\»h••°jÑ’Žëæâûf]×±ysÇu³àŒ¹$†l–ÌÌ#}ê©ú±bÁ8¡éq0±ý0&·,ÁmWUˆzä¼cªQs dû·T„9»Br¼å˜Zá§é¥fÅ*«6Z9¦l§yây/ZˆX¢f²Þx ÅÌßóÁHÊX#»ÿ ;1#Ñöýø|è±®È{ÐMÅÿ buFCT Ú¹¿ø+’Æ:^û3( CË—G«¨û—‡C~Qô†Âú\n_(Œ_þ47W6·uº}šŽN7^:sbMTêJ¯ˆSÿ[š5ýÄ*¢?N—óÿkM‹>‘&C£z>:zµ­þßÐéò9ÛÎÑwI•¶ i*«”Ïñõ²Ѐn.”ψ¥ÞŸxæSúN?C'þ-üÍCï™oþÀ* o®Zjx·î}ç¸üô©=‹Nš¼œz’x°Þ?4v ÷ÙøãÀr|ÿ€ùø£ß}åþW_½ÿ+¿#byá ì:FEKÑ%h+kyrwQʳ`++Ç”h>f´f¼ËÏzÕ±ž&x'õDx*Ÿ ý“_lþz¨†¦‚_O…jS©T@Óe—àó'Ã5ÉtÏ™KÓ¢WVhÚF .ï¬=iÈ­¸0– CÙå$šITŸ?¶ÿiöj©xÖËGMõy4ÁÃ0.Þ-)~IU5AàX@£¬‡\š ÙŒ<·æUuIÜ És;á ?aí‚]—õ˜Ï+8Ý¢_K…3©HL× äÏÒŒàæì>¯¢(S~·/É„Óá˜ÏÏ;#¾H(KFt—Ä ÞÀùóbÕÓv‰ƒáœ‚G’]<œƒe.I’IBÔwŒ<Ñ-ÉnM‡ ‰²‘':Œˆ³ÓlßA~´ÉX’›Ç´Ö‰ •cØ3͉@à“Lf ]`Mý[²ñŸqÙ£qM%ÆMãíT—TðÏ2ÉNþ$Of,Ju~ÝùsßÀ_+ž<¢d§,ZÌ8Á™ žµtJ6^å ‘,O ” M€ãçì>Ö¿1÷üºÿpB6°QD·G‘=n1ã²bÄaÝÑÞÓvÕõˆ¢Ýëñ'£á€æQ×Ñd‚Ž€¢y4%àH-Ž&’Ñ„¦‹‚%ý¯]{êvÖ{ÅI¸½ ý»Î6-A#æ3·y.ntªxY[–ÄcµÜ[šû”º‰*;bTΊt¯a³ô¸ž3®ŸÁ8x\iÒ·^¦î’VÏþþÜ¼áæ°*8\² $cÁïR\QºgÁ,¯K ½‹Vøˆ½vé™=édm(åó .Yn!Ý‘º_pPlÖÿ­ÍÅÇ5·Ã#°œÀû$]«q»yˆö€¤«À–òÜŠì5ó\ 'zdUm³6½:6o8©ºY٥ǓÁLˆt'ïDÑ@M0âsûSI·òú윛sÒY¿Û)zõX,‘ˆÇšE·Sðúbº`D»À.Ú}Фº%—ÃÎ’Î#{eI”¼)v:’çñº=.adA”‰0.É ¢¿±·Úþ„–ËíF¢†zhÒy~}£å¸™j©cqŒÿVÏc¹“NõÍsЩ|ïº|êÜ“÷Tò¡N™Ë”2OFÜÛñIÔ›·j†µ ÔÂÉ'Ò½4cÜÈÓµ|¯i)ñ§çtøç.›R_Ÿ®J(²ì© æjêjU‚Ìæv.†å¿Yv]‡=‚)À{÷–ýß¿xgŽ•2£m¬Ég«5ÁK‡šj§Ì;]Ç.O“?Þár~6 ¹YÓ$ÅAþIÓ>פ¿œõ:=NV‚j&:}nÁÒS– GdK†þþB­¨Y>û5üÅ[dÒøCkOIG3j”$? 1‚g¬Óã‚/AôµÙËkŠo½Æ°‡×á`™R„zÍ.ˆËÇȽ)TY™Id²Ç㉦¹lXvt§·//HQ×Ï~æJ ÎùK·§»>=—­ ÇDG %Ò­‰zâ*ž˜¿hcìg³;’`³ ÎnBT×1 í˜¢Å«9»è‘H¿•%H2LHóšÕ~ ÈSt™{gcí"ïõx(èW<.—èUb¡xÌ‚¼(Ù}þð™r‹vM‰DÜ"Ÿpy0‹‡bŠWtYP\ü¼JQ§nQî×µj²"Æo"yÏÈÌatú<Ú…TÞîNµ‚¡Y½àdž§ÊÌ4ŠÈF¶Õ±-l 2`ݺ}T 1›ÊÍ&oJ¡)žw*ªoý—®»ôⳆ›šÖ¬¸tä†k¶XP¨î¼ŽŽŽÖÏ­Ûqã·í¼fó†Nº`ûŽÛﺡfjJC¶ußyp.ëI%…šèÔ^ìM´feÏt_U³_§ëY™â•`2­á®¯G"|ˆï”ÂhcY_0ä ÈŠ3ŸoZ5|ÑÅ×í¸xã9+[F¡׎Lí¹õük¯¿ãÎ[/¾ »««}Óúí×ß{+6tvuÉ“5æjQû'‡àf»ÚÞ9ubMtËæ3cÞ`ÄQßWjs%j&´û3vÍÝoÇÖóJ®¦úÖTÆÅË…ÎÉ-ˆ-[ÉB¨ ¡sÐõ–3;›.›KWFoFŠaÅ¼í¤ %¦Né™W*Mñ–Ï~Š÷ì³ ÔØ%kRíMÕ×®­­OõåÛÚ›¯ƒÊïÅ»D±)–RüÇž@Ðëu’?¯7¨:J&mZµòÃëµö±–-C†ÕáâÁ9nw1’h®oHż€ûª¢¨ Øêp`g‘Óç nµ±ÞXª¡¾9~‘Ó¥é`4 èšËiA‘¨ QÚ¨]¿péÅ«Î\s¬É‡Ç׀ʆG€FP;‰Ù‹êRZÛ²y½¦è¼ž×’f0F÷ª9¦!’²’Êrɶ[R‡wí‚Hë¾Ë6ß²hø¶å·‚½¯1kìÃÿ¼uùmËnÙ|Ù¾Vü›]»†'R¯T1¯?/ð ^=ç@@ª“Hl7¬„ˆÓõüëˆAÛЛl£í_æ)M Tmˆä€8Tü ¹ÌÎÄŠæ@‰M Fô?`ÿ‘ŰŸ³›ž×Z¯=_Çóh|7î³=€ûXnÔ#Ë5áùð0]üyˆ^ uø%‡Ýp*~Ç^ü6µ¿%ÉÊyijUoç,—*·ÅÍW¹øK[øÈo‘ ÈfÛæšŽ ×П­=Wæà.¯—šu—1ž-ŒµÎR2G³Qsø «f+Ž[Mׂ5.4¿¥II²{”ò–nN¥)ZéAA룒¸¦–›¿‰L$# …”Ëx{3–`JÁÜë^¾“ZjpàÛi¯Ü¢e(Ê¨Ž¨ =˜òÈÒÐôŒ[TX†g\ °©`u[Xíš²­ÑP|›q¤›€ ËÔµt6ic€O2Þ‚ßQ+ÕϨ’(Ù¹° ¬¸nÅ6•±ye–Îmnáj"Š‹ch{S•`ohÍ ÝWpС-9–)Žâ8w¨Jtû¤&‡â9¿ öKŠv°àºˆ¥dg›d‰wÚ(îû¼›žŸq;=v›Ëî¡Þmkâ¢ËÓ<@îŠÓ“Ý©P0ƸëŸ\‘Ö¤Þº¨çšNÖÉ×TEºòî%ø¥Pµ7µ8U»%üeÇ„Éñ”ř9KÚ©Ç;¦®ž ³n§pVö‚/7»]NÖmäñê¾å.»b3$- 2tÔËs.o&&»r­©ÞúÕ41(e$–ñ8òëܼ®Ow;yÆO3Â[bN“÷çÝßíll›8µ³uñ`¡¦Jy|ºZ[ßÔ)¸$áéf£¬æ&µ;dž—%'{6²£Zô {.³±¨Í@Ì=š×£)^Ë«“°O-‰pŒbZj­µ­%UΖ¡Ü”’ãË€×°2mÈ7%›•ñ¬_½ò«‡ŒBÍY&mg8ªK ƒÞ¿Ø9Þí<úù=®=ÂVpP—ï'§Ín“àß)aU|¾R`‹G6RŒä.^ xÜv†uð¢è%Ñ!:8ÃóõWR=N¾`¢Ýåáûûú@¡-äë¢X†ú×­»„[wÁ¥rqkqí~j 8Ôâ_¡ƒwQ=G+ÄÝÅn,~ÌR魯ÛìÂÂ#J‚Èëq Ñó7±ø 5™¨«v³;m=æë©b(kz²g „F\UÎK¾šÙ|™Ö,§ét›±~‘…¸¡§8"A‹§ }ô€åË2t5ºEçÌRÌý6šcøµ|^{‘g8úÈýgœqÆ7¾AÏ£8úõoÀ õ¨?Yd顿â"jïÑu6ÚÎɪÌÙiÛXôàê㤟zÛçä{E\m¿¹]_Ár~á«6¼ºÖÄá‡B_úñ³|÷tWÉ’[ôx wÐXôÒaP÷Ÿ¢±Œº¼Z³në> Ù(•å²4¤iÈê@Ò8óLz0â0îÊøTþ¦(P0Š2i‡ÓÍsö¿x …ˆÞEqÌÑõ»„=»„HÔjÏþâVY¤z¨IiÃ’fSa‹ºi;Ž4Q"ÍDä,cwˆ¼Ñ€dÊElï,>Ã8 KuùBm…ý};Ã{\EŸ°ëVa×ËÀAmÙ_\[ÜúiW.Ò®®#ÍðãKšáù"i‚ÓÃÚ†' yÁa@NpS®É¤Ù" -†ËXÑv:J–’^iKƒ–n€òÌ·<í¶´)F&w/&ÍŒ2+¦ÒLÚ–½¡2Tš:¯ø#ÊBD<˲v»S$€,2Ԥ○oà×÷èQaø.¾cÜ0Ñ`û5SWÒñ_æ‡ì;!Ã;mw\ ‰}?Oð?øä}ææM4Ùª¥^¾™ÓfßtÃXeé±;»¬På®\ñøíŸ¼G’‰Ýï ÏνɉټW@ùüCoÇf¤ly'üñï.­rMÝ!È¡_X¼óÇ>æM'JZPÿ þàÀ!Õ¥û½.5â‘·½ð+|C¡&4aNT¬à!qg’°äòD¸uÿ sEˆµ3ï ¹¥ŠPªHå|¬4+£F_Ó_ªSy+ =¾^Æ!¥ ÂAÕàc¢èùàÝ[n‘"jš}±TÄ.¦‚z`É%X,iŠì¦TÙNEÙwõ³—®òêo œºT+œ»ûÿr1ïD¼À¸DA«ò¼ðãûd_S@¢ÈPÑ';ƒ¡Oá#רLTpE’R°ž|>-úÍ—þœB¦XVô7ž}쯻6:Oe^&Æ×QÒ|ò²üIÇÇzlÑ>o+à¼öÂ~Ø‹¿ ßÇ“‹àÉð}jñA8hÄÒ`^X*8§Tž*^b€§TpGPÁÍ– Å·ñ†âÛTn¦)Ũ$¢Ê84r¬|k²­B6ê·ï¿ˆÚTÜy¸¸“Út˜Úü>”bãQ †•U¤¾;ñ&¸©’5•„PÁ`BÌY%‰ÇøÒ/ÂÍxÃa¼n>l›vøè>záa¸Qà"åבòn4¤Õ NY)™5®:g k±Mܽ¿„wîÞ½ê`Óîƒoâ— îM¨³=¸ï,®ØM9wÃ&¨Ûý±1ó{³¸‚º÷MDC¡щí—=™ýD|+þø`-ø‚ÿ©øO¤ì÷ÀA#~Ðv¾ÇÛ 6°eNñwF„ ‘Ä‹5ø*Ìgt˜²Ç*)åËîÙ²;«5®ù"„Çy)9š] e?˜o5ò{ •v_6¼tÉàäºTssªfê´å‹Ö^–ÎöOY1om˺y+¦ ]¶vÑòiSkŒu“û–-.8²Í*@Ý“IN^9{½mËúÙËúú³©K‡—.ëŸ\›nnN×N|tøÒT¶¿oÙìõG¶­Ÿ½rò@&sٸȎžüä÷Ì¿˜¿KH%z 0Š™ÚVâ´wB\¡ã:‰)NàòÆY{\žIêå”¶$½ôWðÔ¯~…'™Ä;¿úU˜ñÕ¯~uÝ«Ÿ¿nÝù¯áK_]‡ÿhF™¿â-øròÙÛ`+lÆ¿6À†(DŸ~òI|ù÷V}¢ßûl{Z@U+™Ãæ”’c{ôÎki9?êIX¶²VÁ Ä`VV‹U0OWͨ+´´êfÄž¸´ï¢áFò7|QߥOÄN–A‡N^òÉKŒŒ¦&#ã’'Ç2ÛMÛVs ˜d9k“uyøbí;4>yó9o#ÃØTØXÍf•ªDR+ÉYÔ5o%R¢’¡NSþ%ŠWñy õóbQY’ähl^}üXÂí _â=’æQk"qc-§ªjYUÜ&Óº I•v¸Ç£†Už2Q=‡©Q=õ¥S’}ùä½| ²Ÿ*4Ÿ4œ3‰þoGJ^Ç8™‡Ùøh£}¶2œÒìæV/>…óÓêù‰Qº"œô9Iò¡«T_Jôøøª*•x::Û ©ŒW%°«ÕŠ:µ¶3.¸d>ÛÜÜÞ™ËgR>Mó¥2ù\g;ÙÙÇË.¡º97Q ÆŸÏjfªÛ”³ÈhYÞïHroc>+2™áöÎÆæL<èõ*Íê›3¼ÌªUÍthJ][¢¢T,t\©¦ÆÞOåõEÖ²§¨y•ªšýÿ÷¤ÿA¥Í޵¡eIñQ]oç“=ÓjÂ'òCÇsœéxëŸðÉ ëq4NŒTÙDN¾"—Êë"@ ‰^у²êäy§êTý¡ˆ/ ˜P•ߎè~Uä!#‰ŠÓéUÔ€?öT8íÕïÅHÅÅó.Å«Â!¿_uØIA)ìûÂVA§"Jø ‡(…õH¸D\•ƒzÄRF ê6¥0Š;U_xL&%à [TÑ߯ÃH"¬‚¿ÅØÎÈJ6Ä Î¡€_%D(%à ¤: H Í‹ªß”£Šw:yU €ê,ñÒ™.™Ô“Ô%`1¡¨î¯²hºŒÒhVËFBþ 9H¤÷e¯£¤Ñ@A‘ƒ™Jªbi¿. ‘T¶¬RÙe¨T1 @I¥ª?`ˆ¡•VeZB!íu/áL X¼d£.~ÅU†ˆJG9©”ü ( ˦%Aq™Æ„üaK‰v¾B(è4r“\ÁüÝx€U)XJƒ”ÒPgôæyÚí;*VHî¸ãÆë>×ÚÑѹaó5;¨÷FW2J‹VÝ\¼uÇ÷^¿}ý¦ö.DUðé­èÇ¥f[ê £]#þ)’ÀêÕËN̤Ӊ™}§Ÿ¾fÍé§÷ÍL¤Ó™Á¡Ó–á§”3“Xºäœ5ËÎÈf2Ù™ —­9gÉÒ¡tæSêÐHNœØÆ|ˆæ‹DÒ`@I3 Ðq0üèo¤\åÂ_Ka'v¦àçO¿ { þ þVü2ýß¶Âqa+>³øÇ矧üô{Åoã[ŠÂ:| 5Ö!ª¼0ç0ì.Ó¦?m»ÂÒ‡ŠÿÏ<ÿ ÿù4½xŸ~àu%î?@/8®‡5#{ ó]ëzB;›†,a è„—Q] ß8{ îyè>ü>÷¾‡pUÿÐ}·ÃÅì5x6œq ¬€×Àx6½ÑèF-ůµ˜ýHnÁ X‹‡?ÁÃè“Oàô ã7lã蛢 ‡ƒ"&†®…z&Âî{ð¹æ?üøÜ{àËø¼{l^Æ5ïâwß!á]\¿x´wßTàVB0D2F€ÓómY†èLÒ%ÐÀ± KýfdäðÈ™7ŒÀ‹‡áÅÜ`þ;Lè'~öóŸÿìg ¼€?¤‚Püˆ/à¿â_¡ø*€ÿúBż—0)Qµv2Vy âý<Þû†q_?|'¼Ä.>ønŽ‘:\|œêÇwÀK³çŒ;£4i¬hZÖœ6KØ0t<ÍPhH+qÆX®‘hERÒ´JÚ ŒR4cû~ëbj*~ÿn» ÿúâ‹ñH^\ü¤!Š×\Ñ‹/¾¶}®Å—|o¹ûn¼å+ø¸ö+°Í¶í2ü+¢úãÊÃíø­j¡ù+š3Ñî©@»ûB™ˆÿ–LÄ›ÎãÎfˆä b/Ý@åŒmÅŸoØPü9<…'Q9¢‡ï=mL¦Ÿ.>IM{Ú˜N?]ž4ŸX­K7Â#øü »@³6nijÈÕEàCðs;AƳ6©2‰ÿ¿lÜD0ÞGvœºè¥{á’ë^v=¾vï^|íõøXw=\BHIû*’öî#E+Èé9‹ EÒ"j½Üý°ê,Â{á;ûñ^ˆ?èÇwÂ{aÑ!XÕnÂâA+os)ïA8ÍÊ»p³…_Ñžs XÑiÐ!­3ÄiÄpià@áŒm4tV,0Ù4iêé6£cÑmtddïú½Ð¹?w4àUÄØ{ð0t-Ä?xpý^#Xxu܆§‘«`>¸:\oÓ <Í@XcÒØ·~Ax?KrñÙ p+¡ö=¼¦´ç Bkdßú¡ƒÐ&HDô¯M DSwŸÈ b>xüÉ®ÆÇòJnãS™k-ɱîã=&$#[9ư~;- ½`€ÙŒE½2×zdDéìô’Äõñ'e§t·xµþˆÊˆ§õÌ`Y'¿uØec\k®6ifôœ&2j¤_ó¶tOÉN¹@ ú=%/ÊL‡=¬‹L9žÂ¶acs oåã)L[ç „íô-®Ï_Ü7ËòÌÌê»ä +êìÔK³IÎ×’¬óúú›Sœ¢’šw¥šû}Þºd‹Kf¯èµû¬ûÊK*°¯tÓ´ Ù{¯‡®zU…ë<ýê.Öï¤U¶X=þ)#¦Üg´h-cÐ.ø_²'A¯4g-œÄšëǯ>•%§´³«ÒÌ®ÿq+»*ì:¡çŒ_ð2 U3¡ÿoíèabŽØßxiƒñNi;¥Ùã,ndY™ÖS‹nн•01nٲɲi¼JÒmÔßsMû×Ï_&‡jj.XvZm{ã¹ ó˜W˜xncnNÓÜeÔÔ„äe§÷M2Âm|Ü*\·nÆârá‹g¥ó3róÒµaåôÓÎûvz'ò8[ÖÄ´Öüªûâ¡¶¶=ËVžÛ2{v˹kÝ4©-úÚÙ¹V-60¸~ 5ïRcÁP gFHm9§íl 'òßÖ483ÖY8Ȇî%ÚSˆöD´}Ëx½;Œv—ч¬£Œ¿áZKãoWFë7ZþøœSh%›œ‹Ïö¹…±<Ÿû×t@«±sÉhŠÀ¥LÍMÕ·¹}c·ïì`$79ö÷/ºµR ÉNE“œ}¤F ü3¨k"íG3[Ó„.Òš:Sn1…lƒ¤ëžVA´òŒ’å6"|¦VH·hÛŠœšŠE‚>QD_0K©¹6º¸Š# ‘º³ôD,•,çûâpŸWìÍ7N±ÑVB8ke;(üìHÜW¦“LÅúYu„ˆ)™rX‰Ù°U„¶MiÌ÷êˆ/ÏXêÑéèe°A΂/Â7à-Êi®òx×f¡­¼ñkl#éc"¬ ƒæðN?a³†ñâ˜eÂñ]a<þø.qÌ áøn8®f­ÌOiÌLî-§kÞÊ'ü¡4t]Ï´ºŽ»œÜVZø²hÒ,W [\¬ÞTÒ”…fu'°úcYœZ©VÊVæb;åOÂr'p;}Ò;ȉnOT'KýNr𢋷³6kç].—CúÅþ4¥†Z¼ª± zÉýXMÑŠýaJŽ¥ä–ˆœú!KýJâ<|‰€‹Ðâ¤#¿HK’ä‰Å«ªëkëªS+£™l]m}uU<¤J’;K¤J$Ý ÕÕ4§Ý%¤†êcªÒ5õu5Uñ˜‡M6×”5xZõìt²œà§‹w06Ææ`ÅGEçþ¸¨ñNàyMk Ç|MTà]7ÿ¸à­Lö ÊÙª´ÝÉðÎJ"¬ow:D… €qð.§ ºœÖ¹SRÎQD-à‹…[4çÁÉkb¼x½¬œ­ÞÊd¯ÿŽSü–Èð¶1"’C°3NXÒ#`h¯¤RØé ¸œD † zµvžÐÓþn@ó’ø*]k—ÔæõV":i†¢nózCkã‰ÑD—Ë´EŒØÎ«^"jw  z‰ý†}$ÅÅ{Õ@ ›èÄ«ò] P£B4LN±ÄÒÕuµµ5Õ±¸Çã‘Rù±eí|ÆM’â¡úªútsUÆãI$ÓU Í©ªXD• ³×UŸ++X±ê꺺Úê”<†•ðxpÈ)<*CXªs¸\‚@(Ë[úWþh8è'-™wªbœ1Õ¯U¤jDû.ñqÒc$<ÁÁð8­H;yÖ.X4Ë GxT䋪“W5¯?ŽúªÙnœ¤Ýh©š@¸ÉÛyÆé¬$Á8áåS¶ƒSFŸ³;—aVÆjv£Ïý$­„~Ò»AÕë„•´ÑéžOË‘Ê䈜~Þìun¾DÂÅ ¤×![yY&ŒºÑ2´¡ÿ¹³©Foʧ¬æè´å%‚é$fè»4ßZ[ëÕ4ommk¾ØpÚÔ™…ŽX4éê˜,tŠ5 ;îÄWÞ/‡­÷ÃUwÞ WÝ[ñå÷ã+mŠ×WÕ'óƒÖ|C•ÏkA]Ý&tM4QhŸ9¸`ÖŒ¡®ŽhÔ‚î³8ÿT‹Hv“ø]•Lï"LWžëÙÑ—Ðèkèô4zÁpÑý7×sƒÔlYXI—G…À”§yJÚ¸$¯ã&~5ÿæ|w™Œ¿W”§{8…¯.øêŠ)¼ W3„ÇÿMþþŽû-Îðô[£³?ü“R òo•g€GÆ¢Ðc1˜yÙ|-nÖzILk^Ó%ëÚ¦sR›uÕ¹,§[W-Ùš•¬+Á8rµ0©ß~¦ûšg»Ÿ1®#×t?sx„@ÆõdÔ»7lX;sÓÚM·ÿj;xׯùÛßÎ\»¿{ïn'‰3×n¸a{9¿SÊïöR>âH?@¬Ô!Œâ(mî¶kB­æ™xiã&šK“HRb$&[O·‘ §éÆñam ©Qš’Ð $8®! ŽnüHá·ŒÈGd ¿…?êIHá$÷í3Bñ£‡Hä¶½$ö}ûFö1/ã“COn[{ÛìY{þƃø'æÞ¶vÏd˜ó`ö¹UÏ­"ßµ«Î:k g¯:°êù"@W#Û8j Kói©¤yJš—Òšg¤’æë¡d;’H®¤&7×ÁÅݧ“©óððKº—Äÿ"#tðôîÅF)'þˆ¤-ë^ra^^;÷áíø§·ƒv_ËÚֹ߰çöíÏ%Ñ{A»¿@âkW“к¿{oëÚüܯo¿mI%Ñ{ñ»·CÄ[ÕïÍ[ÕµAå­ˆÓOø^5÷*yuó¼öD˜Cv‰W¤4nlËš‰ ã^Ò¦K™¬…Z~ß„ÔVqcþ r­…EúàIi(´æHR¨>ךoʶ4’XX¶nY”Ñ>°‚9£¸Qˆ ž›3ðîæ³áâB³]e¤ÁÝøL’6ö,©1|k´GCå¶LHWR]ûŠ"–\÷£¨gRyIr†„ }$,"a ‘p »Iø* ‘p€„_ ,ŸÄ ß“ÈßI  àBòcD~©&¡ƒ„A–p —p= {Hx„ÇIxž„×H–ïGé7шFS8ÚóÒoÆuÏ~ä!A|„œûIÜ?¾`å'^hlR<(CÒèK5IŸ29e½Ã&k$;ñ8Ü!ò¹é1¼ÿá‡ñþǨ7ýdÿ(îý)lü¾Ÿü= ‚{ñ3~êü;c+ „¾ ýßú~üè\ ž‚Þ°éEÀ7ÄOãIDÕ—~rûó߉ڌZ0åZð™qžH(UD£ÕB$“C`Æ9Oc“ñÀ‘nìÃÉKqæÈÇ/JÃÌKÃ×Íg>~q¾5!Ä>¶1bËŒœ£ŒœcŒ˜2#~”?Ê(mìöiK#æªë/¨Ìô˜*9,N¶2'û(';áä´8ÙËœØQNì('NŠëf…¬J •êsõÇCÇÔÇ1^qŽQ.Ž1.¶2û(û(&.µÕæjæ±1µ ´½Ÿa¯fþ ÑÖ# „.i>îi2q/̇ùøaø%,€yøëEÍ‚IŠy¥þ\†H.ÞG½ ñ>KXéh«ô7X•0@`<¡ò½¬úOCQ@}«6[)+.Ùlø¡rKò¾^‘÷õââRú:]EÍb^& Uˆ´tÎP-*«â’’§óÔ,¼ò+WÍû ]@/~šà<Ÿ‰ÅD™x@4I‘À%.ÑI%9÷èd± ? ½ˆ2ØÚ:MteM¤ÕÜX€î+îz¡,Ì8™à½ceûFe¤ m¶Q&¹“JjÅ 3vI• ¼Ž­\Pªô|2™­g†f£ùïõ±®ã?’âæoG*omÓâ\R:‰È06õV4ôZ;‹½Aâ0Vã=ïQ=¾XoÏ`}›?ˆòöµíÈï€7L×öL™3kàœ©ýõÍ^ŸÏŸËL퀾/jØ8¨&¬Š»Ç£¨ ªW”YG0^ß>aêTXqåúýßÿÓÁæ/;òƹ VMì˦ý†|¡{â̦|[mØ-Ô¢â^Rõ»>y‹ñšoµŽ«zEäSŸß¿ôžKjê2ŠzÝ#W_óõëT%“nZ~Å––ÖºzÍëÕêëZ[*æp•X›iljÙ2¼õ§?ݺæÒ–¦ÆLJ«:ÚçõÕ4´&tZj|^ êž`BDÒYDÒ5DÒ´õ"£ôqûãü÷6êSä¾ d‰[O¦/™´JþÒ™\úú¦´Qüý'­õð³÷Îßj‰n ¥›r…B®)mÑRFõ¶Í»?wª*ºícû˜*R%”þL¢—^W< ¶µN›tÛ¤©…Öp(n-L%À´Ö¶`è¼bß¡7>2½oj¡%bäµôM™5kJ_‹Q2ÒR˜Ú7ýÑõ"€ÅŸ`æ%ææÃ”,}üŠ×ñk¸2óÒûnzdh”t¨ÐÒ7yÖ¬É}-…Ð(é¡G7|H½s^ùL(ÐÞÒ7q֌ɥ²…©DÊ>ƒÏ@ºÕƒîBùáG–­tøŽÎr££9†À–NSÍÞæ‰•3¥²•~Cs ÜR–Ó±²Jƒ@úßø°¯ñ*µC뵆3méªÇ?ÚsÎ…ž|¡?÷ã'³fîLÏê?Lšß[UH©1¿¸yùEk›§W×̚є÷t/šQS¯ÕhÑöI5êM+F&ÙÎn¬ë­ŽËî`g¢ãÆ¡ÙSæöœñúE3WÕl[zÇžû1¬ÞñoS÷ægå[Ó™°®†êýÓ/\6=Ý1?ë™|Ú¬ R<í•SZ{¨ª/ÞzÙÆ-öXÊ*¶mˆC³ Y+å³vns^$•"µO¶š» Gí•·tØLÂhÝRÔÒ„¡GŽ…_öÖOÎî™ ÿÂÛ/j,Te©é+‡ýSj«ì_«ï¿³vpM]í¿ç†Ä†aT…!PA¡@#I˜‚ ¸¨(¸‘å@[÷‚VÁŪ8¨3Ý»»§têûº·}»÷BsúÿÝAÞDxþãç«Ï½9¹÷yžóœsŸsîõDàVzY)S1Jxc´y¡zY»WÎI#-æÖ¦´ÔÔÄM•‡Ì–Ò¢iCñ—={æBz9=¥¦êÐþ£¯˜wÕ™ÒÒ#Sô´sgË‘gZv®_™šbwª¡8Õð»I~‘„޽ÙºpæììqÜñN+*µ˜UnJLMM3ml5ÓË‘)©+×ïlyæHËN"Ó§D¦§™êv™_9ºÿPUMJ:CX?ú³S“LÎ8s‡5ʇ„à­wîqNM¦uë«èÓ õ;eò«TO£hü•oÉ»çÏ‘·¿³û¡§eÀî¼>Ðt0Ëú‘ÜÆ™ ôéªä¤u2ùwT{îü¥Kß^¹x‰|LÞ„mÈN!Oâ×Ü YˆÓ)z𲄒 Êà;¤8×”;ùºPîVÉ)òŠ %”²b1¡` õ”.·? t9ÕZKØämë4»jPn«ý5ì^ë4ò6»ÃZâpÀ (˜àpÀêÉkøƒ† Ü4û>ÀkøÍ²oS§Ñ(8i,Þ½#Ç´“dÒóèU¢˜÷CýóˆBú'¢h õü$%ª8"Ç_(Šô꼜^æ~ÔÍÉ8ÿú¬ÔeƒpX¬Î- NbpíØ'óãfñü þApztüm.©ÕéêŠÍw¤nΛ\V>)oó«[vœù±±¢nQýOg¶×MÜü×®ZßÚ–¿6OÄ9ZqŽlœ#çŒl;?¶GðÛN†™üÇžT^1,osjÇßÆÇÆÔétµ%ã'üÝñ*ŽßRë듞¾ '¨Ë0œù©^3²®¢ñÇ3\6õÏEY¶Y?ãÍd ã"`NCFÒ$ûpÛŸ²6oüëù)ˆDbíÊHø¬Ë•(xØ £¥®ñ““; ˆÁè3ØX*›fÒvúÍl's-¯¹Ç®™ÙKçé¯×¿ºÀ~„o²Èó$îM¬µÿ}غã{>òÝpë7ª†0ãŽ0*¨*ªªºAÕ`ì“ "m*cˆ8•E•y•=ÿ—Êz"¼&|—ž¯Ä-öÐÝk{õÄ˸NjoˆMÉ ‹ [Ùè‚nÆÐJLÙš}•§ÜœLO¼û¹ƒ]±Ì;G˜8è|¯hWœÍ. ãÅ¥ÑrwÍ&ì´`åˆÓüøyÓ „ñ35ÐÈF°Ô€f`m œ.…æñØðN„èÞ;rÁ;/@öfüyÙ²doüÔ„¡D¤=ìèF¬—   ©¼ü‰¸ò²žt÷,}âÓc†¦)SŒÏȼobåêY…“ 7̛Ғ}þç…>n³»ù8iå ŸûDª#†½‚ÉîXåðK¾Cê릮–Iæ3;_f|¹(ºžöã½ ÙÎÛn¢G,(Øåw¹àwDüä)úÉ“÷“ïÿˆ"i ‚aÐCè\·~þ9Í$/¡›6ÑW»‡ ú¡ßÏíÛ÷)c(põ> £ð hÀ0õdÀ• ììBh@P 0€|`KA hfÐÚÁ àRCýECýyCÃl!g†ð²²Š âedöËá¯TÕŽ|-¦ÇFuíMãÑã&O¿qñLóý|ó¯×tä k^w'ýê“– O Õý'ÈÿéæOÝRð¹}UÃô(2ÂÂø‰þòƒ’®b»wå}(úŽ÷™'Ó ŸAP 0€|`KA hfÐÚÁ àÂýðœ,Ä‚4æ€%ÀÀAð"xÂÔ„ß05Å:  5ÐÈF°Ô€f`m œ\ÓVÛ9ò¿\)Ö©’¯S…­NQsâ´Y·ÚC½u­¬ ʈ ¦ -[xmELé¡k¬‹å?q±©õ܇mg=’X†×ÎÍaœ>¦²{]¾eHL4VG¿rîﶦO(Iü\Ÿ°/}<š€'Ú‚P4DÞk_€¤¢Ç ÛL>¡;H©¾þU7]I\fœ" ëå’z»¶×-–”ÿÿ±¤`B@,Hy`XL /‚wÁq Ä„ßKJ¦º@P 0€|`KA hfÐÚÁ ÀÅRà±)zºòÝc|$wìèÂy ¨@>0‚¥ 43híàà¦sc¸ÖO¬Z?¾jÕÝcrs//Gõ¯RÛêþ]×t„ïWBÐuë!tèâuúm¼ÎúMÇûǾ˜ä3烣¯|0Û'pÙ=<:zxTtt÷~8^£‰92½vûöÚ·š?nùÚµËÇÍtë ²mXDÄ0îCÿÂ÷O0Lf3l˜xµ޶‹)ltÁ¿Ô@ ÁRPš´vp1…“…€XòÀ°˜@8^ï‚ã@ˆ)¿Ñà×¶¸àõS1@Ђh€ä#X j@30ƒ6ÐN—BǬҮYs)Æp^ë^ßQŒ­jQÙq®D¼Ìx ºê¹‡ëï.§ó9sì‘„2‰láœhãÔœJ ïåžÈîž®%·_>×iiÞøD‰Ÿã­þÒÛƒ­œ0‚¥ 43híààkG `ùÀ–‚Ð Ì  ´ƒ•#L¶º‰ä% üt) ŸòKí–ä‘tÕj«‡þØ—œØòxëŽ-’0•*L­R©«êêª*ëêº×DÛólCس!TéYÖ°½´t{CûÒW|õêÇíný§Aö˜L‡( åïN)èS:<,L0ÿÛ¯rdjîJÇA¿Òqjð7ú¡©²´œ òÒJ¹ÇnËz€ÜB/ÿVr‹5w3)hjX±J› ]µ¢¡‰î·Û’žÐÄ–—¯7µ4šÖ——Çj„­Æ~ëÚJIfûÈkgª¥‡õ+WÕ7>óxcýª•úDaëñgø-±´cß&" µížo‹Ñû·Ö²·ªÙ{¬·ì٭ξË?9¥8wÌøiÓM³”•ßékð,.± Økª«§¶î˜¨ÕÆOÕj¾1ûT¡……; ÉÉ“†”—;åõ×G9Li_>’ØßŽñ']S…y ¢è5Û]e3pUT$'-[z>+'wBêÞû›rr32*Úú`BºeÉYYc'ç¦[æ/¤ºzκÔ';»âééÓñht‹*T«]8O_5xôÑVë>P'|o]'¹ýúï&nî§EòKƒ%)ìËP)”\ŒqTÃß²ÅÉÍt%ûØeT…©-®øédf˸¬Ñ÷åm|ä‘I‰I)uõÆÌjzÊY&++=úÆq…Ï>µ»Ç¶•+¶!$ûürE}v6Â,|Wûñðÿ«ˆ½»T!òÁÜæ7#ôxIbâš’ÒÄ„„ÄÝ íÞ ã#ÓÓóž/2ž|á“Ò2Ó†‹ºtÊ€[ÐC™ëÖ4df®Y—‘I:·ûù—”œ\Zöò‘³{öaÄïÐaoÂô7ª‚˜ä~D $7u*04û0i²¾y| †Ouía§@RÕ›aR;ÃÂaX6SÒŸ°ÀbTó/0Tò3k¼©2%—'xØ -o ôÜ¢d|®¦à/úMQŇdÖ‡E‹“’;hEG¥¦ÂX´dI‘±bø€cãnþß:½^_\¢×RÔέ;x°nn­Â7'gÑË‹vEAQQAaQù´¿KËd1óúô¢xeг=¦Ä7*ù`aÓ_Ùß5co"”‰m*"}tÞ s‹N>ÿii©ÉtQ— §|›+-IÄ'cÀ×vW®IŸJ .)åšÔnú½j>ìì̺£å­ÍÈÌÌX‹¦'©ï Þˤ0Óû¼HuõOÜŽ¢¼§dÑ#‘hù ˜8î p“ßø»Ãó&äå…‡ÿH¢Æä採ïͥћê×®§ó‰t û`ù²?ÿãß]µ| ï­/Ð éÑÝÝz}®¤.x{HŸy#ks×mH~N9ºW°¼÷HaïbŸ¦µÒ‹ä®å½¨Oüª«Gn×\_ UöO@þr‡ÜÓÅ7®¨àÕW6Áî{«¤ØÓà{gy™%íÑéÓÆOÈ*NIö¿+[½{Oû‡õËÞ´V¾W^>dRr²agaa¨j_Œ¯B«¯ÕNŒØÛ:µºZê×?SF ÍdP!] P4ƒ×óÞ¢A¨ÝÂ.½æ$3.,.9’ž;¦  ++yÙº„„·>T™‘‘›ÓtÿÞÔ ¹9Yç—.KJ¦Q½X(ññ­žU;~œ‹³b ÖÿšQ0)rxU¼vÞB­6TÕ‚ÿ@7}úÓÙÙ]ÿ.f,†¸áü“G·þíÏÜwPï ¦]uc}[ñ ¡ò^tuzŽËÚ¬··pY› RÿœÀ¿~PÈÉÄÃ6ÓÜUò>£È/)—iåÝ—5z\KæÉŸ*C"H®R¯ÞÚ­ÎzŽÝ¶bå6OÕ>Åñ7Ž––ÉdÎ$¸º¿)‚+Ÿ^1Ä&¼VDÈžäX1ÛÞÁÎ!}ôR¹bG½&1‘<‘²Bwqƒ©¬ô“N‹žÏKOt?a¯v…Žúö:á\”™Ù ¤AÞÞ‡Íô ×óœ=òrYipȨ“%%þ~ä±þÕ’Œ â{MbzrœÕÓ¢ÃäYk¯‰Lbõµ3NsFYûw‘uáçe²ÿëdáá^•+ÁAÉ{W˜&Tʰ#Înö¨d$¿™FÊÕd|"+*;ȦŽä¤ÅÈ;hˇÃú“n°êô%ÅÈ(tÿFFá»[H#rr|mÉÅ$…H*Øšþ…“?„‚ {ù`•0C†M[þÀeeŽÃôÑ>Rô º‹&Sié§ÏŸ,2>—7:=BŒ"’ÄçB>NÏôbë“ÞηÍDA"wsQTZœzª˜‹¢¹ý¿ðÛWªB|ü‹·¡«á)…V£t‡éÄË6#Õ{M:5ÏŽŠš5{Ö¬¨("¥¯ŽŸ0áÿhûÀ(Šìï~UÝ=39g2¹ ä˜Ì$# †„îCµ@ œ®Ü*" +ÂQ¬ˆ.Ë".¬²ˆ²*þÿx,›³"®¨ú_¦Œæ°`•ÇR?§ìtQQ%§kymL# ”Ml×eùo¨òßwŸ4—É™âz¡Džàoá…²Ð\¹=€Ÿ˜U¤ÔÊMOo¤Ê™¶X[lOOíÕ-))Ɔ{¯úïïÜ̓›1žnéý¯:bq3)©[¯Ôt;lp’møª$WÁŽÈg*vÆÝW“sÓSñxnò«ŽôTì+œêt`çg;[ìl(~‹„ÚÚzṼ—Ù2e,Ôñ(É%Ÿƒáªœ’ÓÞ#NBi·£”/”»ðÂdòJs3;ºbE}e%ýš-‚^åó2„—¦v€V!N° ÉX–”&S¤@#Å­bìOlÑØ0òóëØ»kÖ°w×ñI•‹êË ùWâïÕŒu‹ÉÄ£œów¡_ˆ¿WÎ)ç ˜@„/„£hz°@4¯Ái#4‹ôöÙ̪¤-›ÈjHdß›¬ŠUéÅ9°‹K³ÓÁʸ 8 'ÄAø¢íy¬'P¦d Û©ÉuÊrey(â3y§#ø&_™+ð¾Ø²¤$·»° '-ãô¿Ó³ ÐíNJBZ/.,Ès9“Žñ Krºò UÈ<–*dð]³bà9"*䋀ƶoÅÓ’S ƒW :ƒÕÁ¸‡=I-ÌQ)‹§QM $òŠâ8ÂoðßN·n×’µ Dm²Ô–!Ûõ2^kJ9E½ £•J9ùK |ÅÒ÷Â6_)ßÚ—›X:K‡ª&Ø[šØ~ ÉW)Òå)â­*ó±d±s¸Ë|.°µ¸h!äÐÖÍþXÉßœªÒ“ëÈšýëü‡ß†wMaõ¬¤>Ò£–¹¼e¥BÝ^lûœ®4'’S©›É™g# ù#"!ü~å^ÒRŸÏÁÖ»Ø~P "r‰¸E}…Epy]^î$¤…¼<7FiœMf-„¯f³ôÕª£¿Ù^O6mgéõèéËÛ9Îv× JÁ‚nò.N ¢ÑsÈBå7³Ir_å7d‡·¨HÛÉ&œ„Å©xèåJa¶<ÁâÔ–j¤Ü^t×iq©Ü e3Ž%Yâ?Uòï%´;ôÿ€ÈmÍê;F´h;‚ú\e¾ÍÐZX€õ?:FÌî;K…ݨlûXz[|Ap!dÔqòñ£€Jï )·-ºï®¥7ϘңG‚o×öúu%µ|@eºpØ )=ŠŠ §O^´ðþ»o¿ùÆÚb¾WuÃv8Ç· 6a5 ÄkÈ›¡žYB®/=¥ÔçŽ\Ê(?%˜O ¥^wZÄBÎï¨Üq(bYñéò<ô‡·Þú»ë5å rc%Îø±)Í¿±|ÖH=²zõHÅ­ùãHëY~•ªBí­ßKñ›qìÊÌ/Ôypˆ;_°Ôí8ôÖ‡e¿Sž5°î90tÐ3ë ,X9,bµ°§2Ø$vq—ÁLt|g %  ýâ¯ÅH.‹Ëéu¥‰lëŸþ´•mQv“ÚzeO}=~‘½?™=ûÈcÇÕCçúzDÙÝög~¹±óò»<ÎÐkñÚÀ&—xó Cîª+°¼ÐÖ‡m„=¬¶‚}‰ä2è,vz ­iïAÚµœ6ž4àu 6ÜYõÖ<0L´Æ+4!4/áÖ¥¥ëŸ8dødo:;‰ïýŽözº°ZO×EÝŠ ssN§#öäÄ⤆] H´ç ·,.° Ë·S(ÊLHLÄ£{pŠÓ$‡Û]PÜ­¸Ga.›äÊVÇ]Èd'Ó¼S† ôŒzÿq†ð>ü+RàLB-·«ý«ë®Ð5ƒï¥Êco˳'° ]q$vDÙƒs gôìYP”s§“Zˆšé ûìyÛÈê9GæLñ:ñ`F¾çºâ®씤¤”ìÜî…Å×yò3Âé2ç]Û1ž:àaáöÃj??ádà³â¦ºg 6Å›ÆNº²9 F{RnaT(p»øþ5×°& µç³T¸CØW Z!•ÿŽËÏö<™vìá õ:àww2¸ÂÜ÷Ii±è;(üünµOKU¸ƒ‹ÿÆDG†:dÜDçúW³Ã¸Ù5=3‘þ!$£)$%®ø’m‚wgþbSÔî,‘[žçñvÆ0'<лÏ%$&uÎÎàå|^æ ´#‡±å³"l†l¹’ò·a°!qêf€‘Ĉù[ò‚žð³/°½y@VN²×[ˆ]à+KwÂF¶hgŸ…%¥Þò”Ì´LQÎKr:“òfúý3Õ-²:¿¢ŸÓƒ›©9sGŽœëÎwfvrUä{ »—”v/ô~»dÉ·¸]ZÒßt섉r }^˜£v°N„ˆy}úAxŒ9­"óbüà^‹{A~¯ÀJh¦c^©Có+p‰ð„CX,—Èr³h³Ø-Y–i¥T–¬V »`Å#’8E¢²U¶*O[âdº„â?”³X¬d¶U¶ITš"J|:[¬VIFõ Umb³LIJ@|‰RK°‰±³b%”–%*âÉz(†J"•ã°?Òr2.(.‰œ9d•pS¤’ŒT$ÔmZ¡„ÀVEî!âˆÊ¬¢ôìÌÖÈ1@‡*dc±CåaúR÷±fh¸Í?n[cªOÑÀ-h B3à,KÅÁ¼ÜúfÁË·@°ùº¥pÔ0 12[Ó¹§òÉâ²*Gˆîª| ìày/§6n£ÏßÖpxö€±ÝýÕ‡Ùóãc1¬2sÓ&yßâ=Mm’ÚiáGø‹eñp‰m!ï³Íp‹ÒC^Öz¶õobwÚS±úë¨ á>÷LU{mÆj•çñÿFÆ®ÛÉ.}ìÐI©‘žþñ]¹dÅŠß]ÑAŸéœÌ.äó;W(5ЉýCÕØûç+P Cð!ªˆr’ a€Óû2ÁÎ>ƒ,ÖLVÓ þÅþÅ(¶‹ý›¼¯‰ÙT1r’}Κ! %ídµ1Öë mmNÊ‘“ìùqO ‚Ý÷é"xõÅ&õøVõøV~q'vÅ?'¤r\‹×áv¨‹Wêâú[rô×ãvlÞQ]«q¿&G¥sJÿ ã”\zßæÖFñPuuëH1y³5ùtÜrTríG™O[ˆ ’|i=ÿÜÔ‹Ö=¸üÎÓJ‹{–̘rׂǴ½Ü˜w~¼b‰s•WÝ0bbÍð‘•9YÙ®òÊêa“k†€c{§öâjÓîÔ@&÷ÖöÆLzGóã•ÌŒQåUYao¬Ú>·~*y„<¡\…®¤€×çÒfXËQ û³UAŽæËëós¤N@©‰:Kdªnà6ë§Hü{?.£ç¬™M¬±i欞q¯¿®íC²ºÝmôÿíÍ ã‡ Í³Âœz˜c‰Ï:d<$³mõìik,ß™ õrÐa–Ïš9{öÌYË k>ðNmçÎÃmŒu»‡ Ÿ•v.'küÐ!nå$;ž¬L¾íVKùȳ…c©ø Á&¬:Dl²cÿàƒ†’þ¶þ¤¿­˜ÛÄé=ûÇÉ‚,í–cûŽòò{Cqÿ8«Í&X-VËnÔ¶Hv¥U¶Wö£„¿Œì¡ÃªÇ× /RbµÙ+핇¬»mªRã!ºåùÖdg¤Y°=J³ÌÆ|³U÷Ò±¤òQ¾Ó|l#¿ÕF¶É!¶‚d;†Œ±ÝHnÔØÚd‚$ï–þÿ²ÕjIÖ„ØdžŒï^²U#]ž™ ÝÙº‰Â™—-16’(ØíÈ—SŽKÆ&Þs£eLÌ‹<½§Ð?ÑBĘD¬61^ÜoçÜÓÊJì %ÈŸG„„ø„øÝfÀã¤b¶Ø¸Ø8¤qXV/ñ˜HŒCØW;÷õplŒl¡$!‘û«úœ°Mê}>»Õ-y7b…³†QPãÌÕzl#<Ì#ñý¡ÓÎb¾QóÔ¤C ýxĶâûëÆk§íxëÇß­­&¾ãÃŒßQ® ª¶Ï«¦!Û7ÕÕß­µ~<^#0þûëˆÛäñšqTH(1’¢é[Ò¤ ‰Ã|¿½ºÚò柩Xr ˆˆaJðH·â8‰-„;Æùý³<jx¦67“"50ÐÛ¿+쓪òÇÁPcŠç³¨Ú %;N§ $dUTÝEw""Z¦_Q]m]ûCzÈ—¯篑N¼…$ˆF"„”âòy!)¤ !—µ™ÍÍ–¯‚!&Å•Øñ·è´·BaŽð',Ø‚úÒŸ –ÛâU?ð‹Yw ØÆ´¿YÐ9èëgcä¶YÍ¡âë#mmÐM<@ŽÈýí;ãÛüÙJŽˆ…Ú²-xe¼šLd‰ð9BRù¾­ÑËêK׫iG×é(A׈¤d%ßæ:üHX'\ÂÙ†K%J êt”@k! ÜŽV‚ºˆjˆ]’®áLµàö¶ŽÜÛ—„ã]§£·Êuu °ÄÈߎºˆjˆ]×!¦<Ñ}0’àèÆÑ%õù7’àXÆ’š5˜†t ×ÐÎ;¾ÝCW¢ÏSTh$5ÈÄH%¢Û5Ĉ.É׺úÃH‚£cKê3cc^&0 5øšÇ^+1Ê9—ˆž7c #ÉpûbŒe$æeÓP£}ý2Æ4– [5ƒiF#œyq5ÁÛ´ šl19"üß cµ/ ç=ºN4‰pûe¤‹Ñíc\URß~éKÂFÝÃXRchŒe,¶fÓX#Ü~EÇŠ&Λ1†‘dÇöË8^ƼLašÐà‘4Æ4– [5ƒiF#œyq5ÅÛ¼ 3š¼]ÑZ4£8£Dôz`ˆa$nߌ±Œ$üLašÐGÔÓX’[5‡iF#\LÄÕoó6Ìh†ÛOcì°¤Q½0i¬¥}5oS¼ÍÛ0¯É3bΆ ”0oÃXÓ¸þ›Ï‹I¿®Ý¦yº†#h÷Ÿ|»#¦¾DW¢êD“hÿü|Õ¸E·kˆ]׺úÃH‚£cKê3ec^&0 5øºãó³Áýgô¼`IvlãeÌ˦¡FûúeŒi,¶jÓŒF8ó&âjŠ·y?M׺ûO£8´+F’ï?ãhÌË,fG £úi„i,É­šÃ4£®&âj‚·yÆšQÚOCì°¤Q½0i¬apÿio¼MÛ0Ô4nm˜Ô@ ó6Œ5ë¿ù¼˜ôëÚm^+‚ ð¾m„Ñë•!†‘¤¾ý6Æâ’ƼL`šÐGÖÓX’[5‡iF#\LÄÕoó6ÌhêÛocl.i\/L`kDi¿MÄÛoó6ÌkòŒ˜³aF%ÌÛ0Ö4®ÿæóbÞ¯k·iAÿýóôSÚM˜ÑïÏMäÁoó6Ìkò̘³aFÙ˜·a¬i|~˜Ï‹i¿®Ý¦y„(÷ÿf®#&êyÆšF×óù2ï×µÛ¼vžYó6¯U%¯Ý¦yãë“ù¼^»ß? óHÚ;¯r¿«¼ó*ñWVWÓ·ýç‚ï¼ROÔw^å±W}畾]]í¯ln¦ž«¿ó ‰‚ í‘Î ]÷“`Ï“]DÄ>"Ëw-çÝNDé¬âß0Î?O,_Þú ñ…å­¡Oa‡Qã v8 ÷5ÙµAñGàlGŸjýËrñ…Ö_,Ëýó°‡ þ(L—Þ•ßrÔ®Â-týL¼/­C/µC—zå äáwÑúˆ¼3¬î7ó.%<È SF]¡»ù&Z(Ë)çG4AãºB_•i`räŽ}IÔO”©Ï|˜NUPü{‚ôÆÞ‰Ò3¤ù IFMiÀô«=TÈ7\®<õ£ôJ¹RWWLVX °ƒ’ENjßáâ”,Àù‘|éL –Tu¢•G2ŸrÅÕ~ΗRr ~ÌÓE±¹-Š}j™TöhMÍ£eìä»ÔUUÕ-Øý#X¿<\) Þ!îy®BêóÊö¯FS¶bR]kícµ+6}òå“KŸ¹ã‰ Ÿ<ÁÄ®öx·ûò)³<5Í‘l©¸JøÜ/á©_d·‹ó(spFtÒ®§b=”[ôô˜xå9( S`'5Z`…„Ñ7€ãÂ+}¤ŠçZkw –*Kß>þÉ…'îxfé“_~²iEíc➺I‚(œVþ%•ç©#vØÔwáyà€N(x›œöWJä¢â%§/¶0‰H™²M<ÎÊŸ™Âž‚¦Fø—@…ÓmŠåAÉ‚[2ŸÐR³ÇF8ˆ¼í¢rK€J1-ßÑgØgøyŠ$6’Ä+èb{'o_¼û4¨ÊžRÞ†,å)Fù¶Q ¨ø‹Wš#m ‡ÓÆõÄ7`å?ü§ý§©—zÉ*ö%éíR£ì“ê•}¤&BOâzW¢ØfUüéìYay¥ ¤+ï Ö è@X%B\ÜŽ‚s¤´¢@½ÊV2WÕpô¿?ŸiŠWQâ d®²•+µôáâÇð“%@¸"*QhË,ŽÅû#rP¤zV§Ö ÑÅ—µã_Êàë&›ÀΚ›XKk&?8å2_ù_#P2T”ðîˆOŒoÚ‰Ÿ»ïÖ¾Â`:@ ” lŒå˜ôwÜÊ*´ÑS Wí9˜cÛOOŽn,ÅO¸¡! ‘• [î^5çæy½zÍ¿ùW+!e`gÁÝ+Ùï¡&ÖÕ :tX]ÛË–°½Á=ÚCù=ß«ÏÜ[Z7o¼oÕÜù½p ÜÙ¸ùW«çÞZÚ‹mxlý¾½Çÿ¼oïúÇ6àöÞ}>¾wßúÇ"<è"ôæꯑ½ÌF¸&È&£|פ|GbÄÏÌ»òJ ;$îgóÊÉç‹ ¼6¸F¯öÁpûn¶–ìa±õ»ávVw-î±ÿeu*{l,Qàì՚Ŋâ»ÉV{¾ÍÂYµYH“ö-Ão‹Íb=®örÍdEâ,Ù*¸Õñjœè\ —1:M1wÚ èº/‹œ[ Â{Ûà•¿­îôiRåKtf§Jt‡Š0ØðõÓ5ecÊM­˜Ê*•º#dc„ñÂŽÆ[:ÀŠäÛ#œ:‹vˆa_Ú¡½íÒÏfÿ€‚DÒ|dñóÏ}Êt‡`ÿ7çôြ©q©,O!ò. dûŠuFÝì5¨í®;DZÿØòšr£Þ`ã‹-¯“åºc ¨â£”çqóí8´» i—1Üà›(B{û$§V¹ñ½µßˆœÜ«dkuUU6þU]©llß¾9ûžXGfNÞÚ%+#¥8×ûïªìx ;¥÷$rˆê‰µÕÕUý°¼úäCs+úŽVàŽc Sá׎Œ”™={Ž÷Ä<Þ†[…Áêd™¯=¢¾Ò4‹—ŸzbÐòA( œ·ií¢àLž—’Ô­bíìΟáv¬œ—¤s0±ðºÚAUÞ¼ýé©Ý{'$½mÐýj©³ï@ž)«š–ü‡Ä¤"HZûÕü©7ÕLR•/‰Ì¯óìÝ»g@åôÉÓ}•¾‚nɲH¿Š–O*xµËuâJ_ÕùaK¤‡ðä°¿gvãlzNÜ‚Úñ°rDH^eÇ•ïa)€ƒ:ã/û¯ûø7Â"Èôåý¬óа¾j¥–bóGÔÑ„½ù–\K;–j¨ÕŒpÂDéÕî ©køàŠÂ£óI£‹Šzu«ø‚ ƒó=Ά?™{7!UþL_Žu‡sô´²ònE£NH…tåõžšÁÓ'Ÿ4醡¤?;®ºÓC*ScÛW‹­yª¬=!Ò!ýTÛñ°·á¡kÉVΨnþ¨Iã»!iÈ …»—ïøÆºá'í{WöY° {ï›­ÿ]¿ ÆðøÃ’ã¹—ÜŠ|Iîk±ëNÎy~£gØ€"“E«û\×{ô†úvh(#„ñÂ$t""ÈŸ³NIÎ<}ÑrÁ÷®xvZÊs5 O81½ŠŠF'9óG÷(ºÎ%C“õ©('XR'TÃBïе-H×e œÎÑEÝÊ˦áùú|´Ìå·Ïú °¹ª‡O¯=>iÆÀá¹iì5Ì,h†½üâàs¸|W?cúP¼ûÃ{륰¨Ý™ÝÙÆö‡z¸f¿}xîhº¥UGçCÿütþFÞ`©ðO–*@Û[ÈêdÕ_ ]«ìü^PÚ®% ´ Ê»Ë°'ÇMþ»TG©¸iûRv *–n÷ç隢ܽ½ ŠÝ¹)Î|»¸«¯“3 ÀžÔ™ä(½uDɳۗBÅbŽ@@WÂ-¦„Zsç†fžJÓfÌq^½4^ ƒtî–yëú«%qÛŒÚý3o~ñ¦…ÇHÖ„m3&>7O+³ÉZÙ­q/Ñ¢•Íõ¯¤«8g¨¹öa…Qο õÒ—®¢‡8¡º[4¬‹¯"1l ”7Ô0—ª¿ü¤Yx¼|Z4#·<"°‘Û Õº©aP‰W‹cÿ‘ùw †thÃЯÑ~£ð¸ø[6u¡Jò×!õ±Õ<¶É·wS¿+þ6äýX5–)wª¡îC“‚uãg!­ÕÓ¤•µZMø‰”C@DÊ›ð_£µš ¤ðĹR€/Ý¡LrHj­pä…ÏõUþ•Úv0LdZS[ÒÔ¤¼ºq㥓ìÉ?߀›Ot„ŒŒÍŽš"©6ð8«¡‘ÙJÍb+kšü —OÀÍ'/m”ï¼|‚=‰êyø0ƺBȲ…üðyXápfCà<ÃZ횎Ôð©–8›æ¨Ö!”þ´sÆ8©dõœúôz0ãæYP-¿†,@s?:…P9… qRȺPÐåT:{WÈóW ã_LzN éæ9õÕ :£yúŠ…L žHÛ‚õDÎ(‘ÎÉ#*x`œ=‡f3Œ3áê§F~UÐmöR¹·‰–餠Ÿ¡Õ)tøÀ zu.ˆ¬a†­ vÔ55‘!Px?Lü´ò%F±[ä™)ñÊcâÌüÝ¥“—ü)¸‚å4ÉB˜~ •! bÿ´sów'/lù®`B îâ=Aú¯[ñ.æífÎeàHQ[·0œ¿Rüœõä·‡µ»Žör{E'ªØ×;¼Óàßâç-¨©Ü#a„v¿!Pb‚Φvû¶Kêýþ޶qìùŽ(aÑ‘@Œ4H½ r¦ílnÓBnô4:’tNQññ3Jóÿ//Ãè‹0–ŸÔz}£ŒTδÏ9"œaÇÏœQ*°mb.²ƒª'SÑ“ã#PL¤öi?ÂöÆ*[Õ¯ÖÂdÚ9¹:.2§;ùèb¸ùŠõ ñÁÄ èÓbuú;¦ Îd˜Ê†5﹉3¶%X¥˜ÇÞôâÍ3÷×ÎØÆï~ÖßúËo@]¯¦úz™±ïÑ—ÇÂM*W ë¯z`ÎtÀ««›nQ‚ÎFµrO4–†ÖPã—'t„´@ô“‚НDB4Zyk%¾xeRSƒ!1&ecRíâtu^‘µV莕­?nå­µ¼'PxpiÎV{7T6dõ€¼âÌt‘‚˜¸¬Þ“&¿;y¢¯KİÑbzfq‘2œKýsÑ'fy‡÷»é|yéTœ¹»djqåù¹ýy¬ÛçjûSƒ¹K ¼®çñÖ\”DpyePÏ\`«Ø¸V²Uþ_Ñÿ‚•ƒššÄNm5ãY;+Ö4Þ ÷·Çñ!K§CRT$J`%ý/ÿ¯Ø*X ÷²5ƒš$ d²ûïm\ÃÖB6ÀøŽÂ¬*J¬ÊQœê-5Å[êVgHÕ ÷ñÛjübÕW £–}ÜÄ^\ö±diÏ3ºæÛÎB;æaøŽPÄŸŽ ½I­hI‚ ñˆê£nŠØÇ¬;œ‚|¶Þ¿ˆî€™üú¸°©‰>Ò–çiõܹË`RtÔNâD…™t‡[ùpŠu!"Ñç–5Îe[‘¨àÉSïÞ7ª1B– DÐs¤¸ÂÏ1ê’f‘ζ§Êº YJíò†ö2|0BùŒìÁmq°žykmÓýM Ë&iëöF#j\Áèi½+ôL4›í|S–êŒR4ÚŠF_RŸa‡þ?>’G¦Ñà9öއ®ü;¯},!ÞÌ­4NãŸÙAçäšl£ÏœYÿ®TWïR,ª®ñI3œ7ú¸ÙGäG:$ìŸòÜyNŸ§ŸÅpvL»¡<¬ÏÊOt¢}2x[t»tVü{ðþ(p—^|ä<ÛþöÿéKx vóüY÷,©ƒQm ýâ–wZÿ€ò4j³d®ú%ZCσDð·½']ëY{[Ë•Æ_³p9¼6"Íg Rúï$o(õÔrBéǶ£Ô ])ýv’£°Äÿã ¥?Ì'o ÌQ„ùSÆÇ?®<‹X0ryc§ÒûˆÜs‚e[”~'¨SCôh÷µt cá—䳟Cš¯Y]•fOÀƒÈÑJ?D!oÀ|¥ÿ ÿ¸8\^ðr—ÒlFBWM…ôAöš<€~ªYµ"ŒO˜!ÅI­lüª<9ø*ÏçÅ+‡%­ÌE¦*çˆNò©®X™þþûJÚûgñ°³§Í|Õýý÷o¼TÈ~)ô —€#@oAî“­i¯PzKéÁ™µ¸\Û_¥û¤Sa9é>Í&0ø6,@D\-·ä›PFÅúp!"v¨é£ æL¬çJ©¥ˆ|ÌKq8zHIÍýK(öÅá€C8à‘áÖ¹]hƒ!ÀPÙ¸k<Í|%NJ°³š†¸ÄaÐÔ²¶yibc—ñÇ-_šø©:8­¿FÜvlc/>ºï?›Y¾6z5‰‘çI}PC{7Ø…*/ˆŸr)üd÷VêÃÕY~ó?ùjB;ývÐ ¿aNñ¥|ÈåÄmþ> ®jΠîp%4ÕO3¥h u\´õá£\îksl#·ÄÙÁdG„WÛ¨|JÊSÍ8ò0¯Ê-“$_ËÿñË9?þ} Ù~%Aè Hí:I¢%(îÆ¥–Èë¤Ê–q\¸ƒ ÃÅåYÄds…Älu’åÊ:ÿ%OüÿžMVÊZj÷7û›ÛK;ùû”Ûɲ 0IVÖŠÍQ¥ù”ÅÉò–_…ÈŠ(â.›¸]¼¢‚ÂLâBîIê ­!EýõÎ-tº =oÀqxä2'®ù¸¸ßø í•òòð‚kùÔç·/Á?ölÃ?‹£–ÜþOüf»–|Çø*õÂÒ%ðrC:~ÜïYŠÒþ K–ò¿–cK–^–Á{K—\@ð´—ߓ݂ÈC@\:‘Nd¯¶þ¾LnS÷' $BĦMð ¹ö‚“Þ N¥Ø¿7¤ðˆÿt ‰cÙ!ÅÍmCå¹’_ÔúOø¬t¸8¬d9O£²n ùŠ|¥¤C{n%íì ÿ: þÀÆûw±ñD`>Uy³¦rI'•ÛöŠ|Zz˜KI<‘ï±qð< ϳqÃ4 %3ÁºF¾ŽJµƒ'Gì æä-&P"â")­LQüLé/î‚͛ه­µìÃÍ›ÕÓr‡¼[ÅCe£±‚Ï5éO–Gú÷ú÷ÂP1G "6íýYÉ!&øÒÑ—M^:Æšü²FòÒþ;”x^›o©Îþ°¬n‚£ô¬úþ‘]­gŸ?(bÿb~þy‚}‘4$ò±œ,8IUÁ©`o> sÿˆlëar›ªõ[v“Äž„›9FÛ'6*|¾J“Õ ñ#[äLpó·VRjbÚoû–Ïã¬:'¦îèÛ÷fé”2ÈšœSn];b8a{Ù'¤Å’šÝwo Q¦èa„Nºpð1]·RHá¿à+æ¸á8BVB(yj ûœ±•ÀªS‹ª®¿{ñ©¯Ñõ¯/®¼Oi¡ @dþ1¾Þõ F3?¢aseÑ­‚Gx²§¬ŒšîqÉ‚˜b¡©Ùéñö›%ÕIóìþÆ’·ËË?xg H+ÇQ÷q8þdgŒ-þ¨ýÍlNì$à¯6:raÑו½ÒÒ± ŒÞŽøÑ ­ZF9H–§¢>ùõ¥1¨`¾ëˆ ³EÇK+MvƒýAöÇ ”­CW6Æšf¤ìÔxß7¡„th-Ýpî¨#Gý:ÜgâÑG•ÛôŸ¥ì¿~ôA-ý³ä¤ŠCz î1óðžaÖˆ*UÀZºLwí"¢}ÁÐòHù äu¦…\¨ ¢yì+X`#tB½ÃBLY4lؼñ0þÞ3>qžöfýM=jmß®®Â,­Ì—ðЪVqùRÉ P³ûø $ˆÏϧž?`­ž·˜‡ˆ—ÝŒ·LãI ‚%©<8¬…^³â+¤ØˆlÕ —˜Ìß°°*ÃëÕÃeܶ{÷·³ú÷ƒ¸t,]W*ÔÇ»s§¥Ú%“¶¾O•ªãÆÝÄht“€6PÏ ¬ÐWíƒ ÔŽ“ûh[0 ÒYE8 UƒPmÔg-µ R“k̦ÕÑQU9ªžBÛ¸ùŠ6Ú٠ɰVk90îG\Ú[à2“s,×H#ÐØøòûDÂÁN8Î,†Í¶QÊç5ù>NÎÛ-ÛœOÕó¿9ã8üßäãâ¡[Ê ¥×«‘ÏjÛeÀ¥ÎïX+í„Ò+W[¦Ý‚À\–LÕ'ÿ¦)Bý]*!Þ¨ä°éâ,Y$:1B’má²dñSöÂP\ØçÞ2²Qk 'X—ÞGüb÷î/6ïÞ­<¾™[ñê”ñ°m€­ïäÍ@ Ç2(˜™l®A„øÐlŽš µŠ3' ó”,»¹žÅ“­Ú¦¤âú'¸Â]èà·øÞ‡ÐéSäΟd{^¦zÞyC.+1nMÔ¤™ÿâ¯÷ñÏî±n=aªö#:´a*낲•Ã`É­H]ó“¶…í+ˆ}@Ä–"|„ë’”DœÎh¡Ëp†Úçîr°BO\ƒ,½lË®][ˆÚ«7ÕóD,¶Ñ:a›w”ñ–ÖðÑcˆ-K˜ÕTR%~H`åíe‹ ²®ŽÝCªŒÆû8›B2øJ·þ›ûÿX50"À‡Ðê+½œ¹, ßæä´Ú¶¶>m¯C#Ï·êC¡f‘2gðѶ¿Ÿh›ë[øŸØáú\ #€£@Êè—õH|h h«¨yyʰ¼êù¼CJ¼CìôNê>íbã ¯2¨©$¹"ô @Å9 @þQ"LÛöáøý™8/Abæ~¨˜ùv'p!O«¶`Gµ2s"‘â”MýÐÍEŽÄø«ÓÕe«äÍG ðæ d²\†öšMvîëÃÂÇ‚ékwi`Ó!¥Š½ÝoǼð6¡x»jt@W¶÷Ȧ¼Õ¬tùuUvªS=´nÚ–¦ö -Ùô@.x4â]âR"tPü£Ñ­ÐúaÉÃQÌÑfpU•$ù’ÒËUÍצëèQ¬Ë”^Ø)#ëw ële°äÇ»O«Eµ…Y-„´¯~pÍj²C"ÆPQ?zzâ ô\7c½Å ³aYìtUPO^Œ—ÑZ¯ü>+Òo±'þÑfÂÏøÔô=¦ô~<½qÇ*ÿ–|ßä#Ü{®qVqtÌÅ-\¨ÃÖµ¹jÕ*Ußo_¬eòz(sò".cºt›èåë[ªù/Ö¥ÍVzFâ½7òöî|+*˜*ÉR—ü›jõ´Yµ¥ð“_T¬pUæï³Š!-!Â|cå7ÛŽjÓft­84a ¡1îÓöãWì­e'ÜŸ8&¶iCo™g5 :£±áT‡>î‰ÅßX³iÓODÌjbq¦n–€¬‹ëŸœéüò0N¢¯Ò+oõ/Ï.Ÿ½œŸŸ.ß Ú˼Ìl¤$iý¨[!_W¯‹iz4 4`3=É}þ×á5õºöä̤Z ÑGM$5qp0BNi,* A Ž‘øóa‰8fÄèZý]Ó«Ò1lF)ʉԻg½Ûu̺ß/lõÎ’Ž­Z×_5OÆ5j1oÞ<(>aÒôî3ÿœ<‚Á3õ3ü6®zË6µ–´Eeg>K©”ÖsRª–õõ t@ã–ovh•Ð2È5`ÐökíÂFû¨#t{í.q@ç5U;{X"·ëÜÊ¿ÕqïùünÖü óÈÂC(YûôÊvp_R7ÎÁ·•²JêÆ@ëgå‡L~²ÍÊå…Ï Â1±u! BtYÓÊñUàMT¼GòøcIOò¥V=ê5ªƒp ÀG0¡r|Çj ™¦U–Cà‘ åŽè³¼kø•™W@?W$ŸÜ®P„ß9m'Q¶U®)—!™U]»å[ÙP¼ƒ›M>7{ fË5ÙsqßØÚ„ux‡ªlV£oháˆÕ•l1ç 0´…Сò v™7€Ùsqßh€=§»Ðßîñ™C*_–$@>‚èÜ*@}Ë· ŠHDÆÇó;ÕëÑ ¤'IÇÆ'g¶íÔËQ£Þàø<¯\¹E|®Ëû8Z®iаuB•Ęš}ÒÚU[d0Á‘S9­>´˜`€{,Ke%•ÊE™¬<„O^ “(o”$vYée1±òb23:ÿf±ãjm.¬jHD¤˜ÇD’JLl&3óµp½vQØkŒa3ÎE¹mï=˹rþ¼•¹³{ã½Yã;´õZµî°~ü¨ÁƒdZ0xð¨ñŸ·mÕêNhÛ!eV™zcëÑÿå¿yeNÇ\|zDVªY¹R$øʳ®srnžÏ[¸Ðë•Ë)½Dl­Pwv‡”ñË…¤éïHR5·„N1Qý‡ç´;dèŒÇ3.Žk“3¼TL'ˆŽ­>¿ÆûË'uꜘ°b®c~x…X¥Wúžm«oNžÛ¢qã-fµ˜;ùæêm{ÒûÞ3)_jÞÎÃjõlÛ¤I{sÎ÷£Ñ0”8.ïçâ'/5Ý.'Iæ”^t=‡_—©“‰ÓLA/†[OBá-Lwd´ BM’-X²Zø§=„ùúxEGy9b˜iàHÇ´¥ñ}\ŠKp ,0FkömYYÛ¶feÉ©¬[nEÖ%§Ã  oÁ œÞ÷ï¡”{÷A!˜Ýóošɼz‰ANÀ3›”€çmU™†%܈Ëú,þ¯â5ÜÁ!PÉçí1&Œ7q"·ŒõÍüyP•ƒêøË7pðÖ&üzÓ&h°Ib\ÐÜhê)1ý½LÿÛŒñNÌŒ”K°4*kŸõĉWñŸ¸Âºkp‚•ljÌÎÚ9—âCþ?ã”QC› ûb„}ˆTY`/–¼ÇCõfckqÝ*Xa( ôJ,2ÌÅÄ cË%Ï¥|Ò Ã&ŸüaºkQ<èU™#º·í?‹¥‰åʼnµñ1OÃlÝ*¥;¶Öà–ͽÉWLJÅnc‰:‹ä'PÑOšêßñ6_‹ÙsL¿i‰?`޳4æ X“}‹û“`µ²Jѵ|zŒX¿Æ'XZwmrµ: o™c”^F«!Q´Uk(5 éêôÝ›/9‡çKµGöb}œíå÷ÏåhùÔo±Àp sX¯²¸à{ ¤Ô sÄ"XжÆz€Þ!ÈzÐ ö„–ˆiøS„–†¬Ç,¯•²œš¨ÒÓ#Hv !«x:.Ö*p² D؈må‚8.¸ÌÙø}ÂÆt…Ý»sh¶—›MF§>×qÏ+îÇ2`‹9v¦>V拌¶>‘4Ò§Öú<È|œP(Á›qvÔf•Ce>ZAS¹÷Ðpùþ{_óy‘v†E ,a6Ô/™fÖÉdÃ¥H¡~ãàg)ˆ®3 ºÞƸ^3°¤ðúæÛêCáªÉ±ær°äëÃõÄ#®£â“½ÍqÜåŸmhD¼PÞ€¼„O Æc]È—W↠ئÂz þØã‡ØÏ¥¸’ÂÑ3-URV)°**zK(4…ZÈ?«~åÊ÷§~úé4¤ÀçØgâ ì Ø < ÑôwÓ¥£ ó"¡¿:§Š]"NEHãL¤ ú¼²˜*xϽ·äOü‹ª’²®ÔDj-MæóU;¹ù¼ò-éS½/ã;ìÊÒ+eï(~”¦"ðO¥"È¢s£"lÙþLƒÉo018@eûÂ…ÚÞ… Y‚þW.Tf/Zäôß>pàöû¶´}\…LQÐÚñÂWøVß噙˕™x$)Àb)™„‡÷Vg°23s%‹Û µ’JZ,IP3³†6jÑ"¿+V¨ÍèèÚ̸7cÆ=–3óîŒwgŠOŒ×ôÿ1²PAî’½yCvâ´M'ú–)üÝæi‰ßmÚœÝmÚ¦ìþ¥Ê”)Õ?{Ó´nøý î–£•­…”.ÔµÀ΋ð}ñ1±¸\ân1µZY±o’û/ùˆ¼µSâë˜MÂCAŠ™†ÔæàÝqcnáümÖ,mØpÊTÃO¡ÍP¶D7LP7“Ž–ÑgX¾úòÆfWJ ¾Bà°Ü”p0È'‡Ð'wÄó—‰ÏÅå8Hé•so츛7ƽ—8c׌Dl/OÕ=Œê`Ýz R ¹±Ú.¯•Ž¥6ÞåŸb38V’¤,sY_ýa_.-“KΦuÉ`Ž-ÄJøtñS‚¹`–bUc1˜E©1lÔ„‰…³b_ŸÄ´€j=8“.Ÿ„àEÉ-ó[wï |î—oCœ1_ûã^"éÍfjÐËäc¡§$饞y½¢W^º1é°$ɵÔ÷õ= ç½70í›or3•3™¹£Õ÷sGÿ{5yEfÞúÿ®Æf:¯üw5ˆÉDËëªÙx5òöêÕ2 »×7$wr­o¾Á„LÿUMPðßÕÿ]Í àuø«/«Ñt ꨼†ˆ!KÏdcIÄßÏ,t×,îF÷3a/¯€ÙÿYE[òŸUœý2‹¢a3H‡½Ú,–cÙ ¶_ýEl,òsÑz ¡]ŸuŸøÑÄîê/WÚÓÆ²YÃÚŠºK¨îuQ74"Ndûñ‡#F]¶„ê²YÚXªË¤l,¡º*¯M½kà‰/vÞ´­ ¾yS½ŽîÞÅoþ­¦•Ng[eG´ïÎ൶›7é^}ªx€M|t Â_ÙbO÷-{LÅ :ƒÂ!ß¿Õw(æa7½ö£ÒKo$*o™Z ¤= ­ ]Õ»ëzÜCO‚òõ¡`‹¿Ê›5 ¾ìŠOgË}µKò™‹×ZAµs,?^]ê$_PREuKAõÏ{ëÕϲøb‡Q'èF•<¡’w£§ÀÏèrúT‡t<+ŒnªvLwÿ°¹™.#üª»‚H(¼t³0ÝúŠQ´Hó\n¡LfWÖy#w/7­2|²p.ïz;ë¸:hÈI¼óYrKVl7Û_¸œËI¿ßÔ:Ím®7Ì¿¾\'_(H×dã‹,k¡óÑdÙáåð!?´ií&ujµq#.^¾|ù’å+–-^Õ®mûŸìƒjÚ}×8Ò![÷µ 0ïêèQAeë@Hóðp‡£orŒã/èá<¬ðÈÚ@ljzG]E;˜Õ‚ñwD‚ ‘¾ˆûóAxœ„dù4öXÓyBçÖ7á¾w†Ž1øbòÒ¶í:´ÿ¤ž<·m;+Ÿ#•^Y„’»¿mÄ€¹WGöò¶CyGuŽ‘#æî¶sÜçŸ/¡½UÜ5H'1~VÓ¿‹Õ5M%YzûÊ•Tú bIêkNcÕ)–ö 'Fã§X–Š æN¹ÖW}òä3»vë^{c­žqÕkD™06=¾aƒú£ßìØ£I·Ä¤Dø‡“˜ûõ)¶öÓ÷èo±ããÈšuë·l]'özýƃ’éÕÛuñ'»—÷[‹è“où)‡»kTöåR'¿¾ð%¶ÅkJ/òk¡ÈLdõáNþ†{=T½ØA±v³J>«7Ð=¼²ø)³–/×o¬<œËQ±zë¡_k@÷] 9šÙ;ä¹yæ~MŸàÒ¼>•`EIu¸‡·ÐjÍîÚ: _úîQ¡¥]ŸË]à‘„¸–l­¾–S†Š5_RóëbåÇ÷• Ö|ÇÓ½5ºÈÊkÙÌ]GP…ðÜXçÍkÁ2b=xxOÁBÐàQâåb¦·}Û“îq¶åßt¿Fö¤„䍮E‚Î0o/bZ‘%ý5b­äq 6ûN›k°±ï -˜'¡%ýVZÚ« •9ñ D8¬]Ÿ"+{YL"*;ƒiûÍ!` Ð$f¯á_°laÖ¬(ÔÃ`ã~T¼†3V6ûhØ«\ûŒåÚ÷ÆJy9Æ‘3@IÕ×RþšØrE5l˜6ÑÅ¥©…\ÅWâåK˜0Xb“CØIùVöI¥ Ö™¦íuÉתØõ³ÆC:fqÒ¶h_ë Éûä¿’Óœéxˆg€ruˆQŽ+¤;®Læ× Q°b%Ƹ;LI«N`nboÔ¾ú ýB^Zœœ É|É«³aí#k§ÖEèþÿ¬øë;›—²òy« „Ißr[Btã_…4Š~* /é˜fl­'Ià*£o‹äVä”$6ªS®ˆÚXÿ@h—ZeøYúŽF¶èFRÿ*Qøß×pRÄ·c%jŒNŒ›° Š z`íþèèñ’´ LÕ;ïáC8gy«$»ÈîZ«Ûþ2¬ÑÞ6™æ$r!Úm¡¾râëH›>Ê:s øøã?h:`mW©—|âÐóèVÀ‚UœPLp‹„ú¿b=¡"ˆÕT]ø›%ì]©¿l²ù‹w»W+m*dãå¬÷-†Ð2­½x_Ò [Ÿˆ./ÅüÉ¢M®U](DùkÉ®öî ð#9-ß´G姇woï_ ;ëÔÿôwð/¼ Ï‹ÍlÞü^ÍÍ 8@#C#ò’Ôäžük­¢8øH!Côš‰Gøy‰1†’èìу,ŽCzr²¾-`ßž>p@=ßø†´ów¾=­ŒE­±Áäãj‰R­½lÉ}’OÒ7¨Y+lÅ·§Ïœ1ùÌÍBà[ÔÚZ¾=­|›¥;M‹%‰#v1MRé ƒt<‚oÒwî~myWî©õ0vÛ6t³>ãàOñݶ%Hìä-ššæÈnQ½ö„% „P`A/U¤B¤9ÂÛ?rD=ZæÈi)| ¦Æù>“‘1 Éý…ËþúËäƒ?£Óé)?¶ÜFŠG㈯$RQÿóeã®Úˆp™õŠ‹yÊø¡o›1-“7ÃÀìuÁ ­ÏÆ%¸ôäú  ƒ×„[{7ôe?ø6ì½W«èÐ{ë¶Þ êÃSŸQ¿;ñÁ†S§6€7(Pü³SøÛ”nï¿ßmжhQÒ”)Iú˜f¯¤ÆÅŸ:ǤãA]ûZ\‚míÄ2—.”œ¶–F“«ŸL/yáR™IŸ@ȸø0 œ.?Ë{Vˆ7&¾‚’ºbÜ×9§ÓV¬H;óõ¸ùRǨæÍ£:b»î5;v¬)É®%³pÈUXĺÜä&ýZm…~ËÉðvv6 -lfgã¸ÀR`Œ²rVÁp{îœkÀ=w®À‘þ[áÑö5#oºXJvIz9šŠ1X (~Î/ﱃÆèÉn ¹WL6ö*~M <>^ žF%9õU§ÝŒºÛnÆŒ|©ÐÈj3BžÌ<ŒøRƒ¬ˆò£ØB…~Ú|»­Îùáï¼3\ÛÈÒ  TÃÓxú Ó–/W¾ÆžC íî=m":§MEÂNj±/[–‹“AT-Ò㢈>²[b¡HäQ·SSow­…äæcNC1|zzLFóÜ\\&Û€YÖC½¼«9Ê…u}’p·–‹»“úà¯9y}‚$™ AⲺÐ2=—™˜c×› °"°1…èÀ´ÒSj׃RA5¦ŽlÕhÒ¸»m ]P{^=ωÒr­"šT©XÍoOÜÀî“>ñÌ!c's(ÑÆGxM: A…ÛIÚãbõ iVªaa¨¢ª">çC\VæÍÙ«Nñ·S¡‡-4¡0&xTÀŠ9ÚmYVc6ëÔaõêõ>&Ô’q2wb“ •tI÷3¤;´ÀíÃÌþ”ñÀß¡FÄÉÉ[³¾Î ž„ó0™‹: òâ¿+r˭ɸà |Ô2S¹D²ü0_å¼7` â>8qQb¥gêôì{¦aH°U/ùs³óaŠØÀ÷/6£×ÉòMÖ|û˜ÓøŠ³½9 \»—<~äøñ#G¿¶o"4c&h’Ø÷³¼ Ôï9òƒõøÍz‹õ…¤˜2ÅRÌCY bpðÖÃäó\t!DØ%r&& íÛÿm-Qu[]¹òê8íÞ]Í%Ê&ŸWD¹pãÖ—"mˆ2†ã²µ$͹¹…ä¹@”µ´ Ó´œÄļQ ÐÿH‡ÐktN æ¹²QETÒEˆsôjUE''"judä?"Zÿ?×IÓ,mý«J RuIR’ˆ•‡¸ñ!OIÂ4-Q±+Û!Tx(:1/Zjʺ뀆N¾óâ©~Ët“D7€³€ÄÖJ¢ê°„»<B£¼Ô‹ SÈžµ`åvœ£‡ýºàæy®s9tgBìšÛßåM0§Ýœ1¿ÕÛU³”TÝMKíúÒp$<Ûž 7L DÕ«y2~;|Hp…S²*>d¯¯wì¨[µs$ÀKUMÅÖñD@·$‰µRzy™¤@á†Ý66D®cxõк‚ñÅÿÀ–‘‘!”…§l†`Mh½bù`Rd`’ÉB±œêiù%»ž{ ¹¬¼Z¾&+I‘`³Ê6™Méö€µDÎ!y#l¥fdÉ·^1˜+ÍQ$…É ER#OîG÷pl}@Oœg [ÃKíe’÷ l¹h8 ƒ›BØŠøC_râ í´êî;qûÜj±E±Í>AUºŽœ  ·ø ïœòöI<]Ë;ø‡4h às-༖9½j•{cÇŸ`]ÀâŒ8ÄGú$ÈÛ®­O±©·…•…¡y˜ƒïh7Ćž¤Hy­øV§"9%#1[I5Æ'«pª VayðS½hƒ¯z¼âÉb±ßå$GÖÛ†KéE†ówIοK•ÏIÎüŘV~J(¥gåÁ§12¨ø“ÏSµÉœ5üü­q .ŽÔ^Íß}г/§Çôª?2aÊb°Žÿ¤>¶uPÿ>RïLèããØ:%§ö€ò÷†%$À™Ô/ƒnE”ð„A°ªzþ}kTˆ. “ûo|6µCz „,åHQ¯œ!«éNCJ c­&|DÞú!=#6Ö0¤„ ™óÃÃëWŒÛ;l/a‹Ÿ@võäôš¶{ª;¼qR×UaY¸ 3}Û·GF÷­ë°v‰RµG ¯Íäqë>4›çÖ©ØNkÒ'qmBGûäˆÚ ò >Ù]<[-½¶–ñf° ücËÖA\TH›vmæIW’L# |—`ZTú2“g†pÎtE™Öc#ê£+…AØu¦-¨ÿ½+Ú´nÖ­åÚ=øÄ<­|«¾Þ¦>ý‡6 "Píð$ ÛÂ7Ø ¿ì‡ËVÚ³\‘¨áËÓ§c ,‹»/UK'ÕxxúIBã¬:E»Vj×aUý$šPÿ* % ±½ éü<þJÒ˜•Ä®Un×wž`­šh/`qå[ _É—zœüCbsî«*.&xá.°BˆTC`I˜LŠþu¨¬ü)˜¬\zëÕTq©ßCýû® ÛÍ£78Uì »÷ÆB§Vïc³IŒÇµ}·Zz ­Û0Xîï; ›BÓÈÉu‰ÆÒR¨Xð‹ö‰Ì`•°(ÍíÍ…EõîeuD‡°–>+:øÏúKh:ïe\·c|L°¾ u×2‡v‚}GÝļú†Å´¸Õz^bÏ—ÑÝP¬r`äzOÕ»Qq#f¼Ì=@ýh e诿|¿jRÕÙÍk½§Ã°˜èDHn¼c¢W…rrU?ß. ãæ§¯Ð)¬Ãfïy»¿¬JÀuXåÓüªE#å¨yáýUl!a…äÔ®JqEcä°Qõñ°¯uþ„?ÌÅŸ âãzk;ëC%OÏð÷(9ßC·ú”°&g>{û28Îw×­¤7ÇÜa&¾ucô Á֭͆Íì û”èõÄÙ÷²RFFF;*V)în2ÏKogñj³iGFh(XbbjÖˆ¬êáf2¿—Þö ¶ŸnßDŽ&9éFr"\Ö—ÿx•ŸÄB°±éÆ·ZWöΗúö0¶*²ñ´kyº´µvÕ77ul+1¾Ç¤ŒT¯‹|Ú ¢iD´˜‰é8Ÿ/ùÓ¥Ó!ùÏ.]þÄe»v¥ßêõAƒ Ñ@\ņ 2x°$Kqì}µ±$™$‡ºsCõµë`,qO|ZDf!Fv¾Å/æøJu´-ÆÞйõíÒdqëÖk[œQ9fz»Ý§Níî0%:fbÇ/O8ǵhݺEóV­”R5êÇ7¬Ñ$!<|y``åjU«nØ-U"«UÞjë•Ñ‹þ×7fÍGøˆ/ÿ$œTVhL<ÜSö³–²ÚHõƒ`;ÑÒ—•¡ÍRÌ^ m¨F,Èîè™Þ@òsxk6Ðʦ² ¼…PˆaÎ"÷ײĉ^ÓK~ÎïÓTA6ë÷£õû›Xÿ™%Z©ñ¯Â…8‚«Ýì eаXÙÔÍû¨Æ{JÚ{$§¸Ò‡0Ú)®\0®ÄwÄ•+¡Ä]§¸r‘_y…ŽfVë@m·ØÓeŠ´{Ùhe]yOÓakKx‚´S\¹`\‰%HwÄ•ù•W8b@²{MÔÖ‰­aù¹À7ŠðíDÏ\#Hßi÷²Ñî>ºò<Ôa;ûñ:i§¸r_)Ê[¤è3µåêûy‘™j|>ü"h*Œï5©p»— HûèÊ{R¦;/RßΆt]€ø»gŠˆú‘]bš+¶,¶`«Ã›ŸÕ¡xMšYm~’·2EËÕ~­í[ž=†‘Pà$°Ú9Ví]Ã}[þ’ò_-P÷,‰$F-þL-~Hj\š·j’y,¨Õn¡Æ-vžk׿CÛ¬xËÎeÇÎB|ÞϰX‰[йÉ:3¶*šðXk\ ÉÞÚ‡lpu¸Ðq´­ÏÃ9Ñ­–7í…5%&?oZM`B˜«Æ&àDÇFÄÒÜFE™оò¼ßòž=€í¸v„U¬0‚Æ(˜ óÁì˜ü.› ÝpsW\‰îä—i:®´¯Ó¨ïtHÁφvhŒ»9H²ð?‘:HÙ—Hˆ gu€fõÜlj±/.œãäã¥*Þ¶p;YÛÙûwÛ³?¯‡®s7Á߯±a¬8†h©ÛoÃàÎʳà½Sk”ž€¿ÌÇO¡®\×¶FIfêØšÑÚãÙZ¶­,‹6bÀ@|žôÖIh‹·>dl¾RÎzBÐ_ïÌ@°É>Ñ~Öƒ¨_9k¨;ïiÏÀIuŽç°²̰Àu¸—Ð|0ã»"Ož¼À`ŽB"ÚáÓ&³B[ãxÛQ´äã>ñX9&ÔÄ}BIb—@Êû9Z‡?;bZâXó™\ w»Ã øMyr)¥ þ5MÌç ž¸qyÕ.'@Ä{ìã]ká3ÏÚëÁ*&ùg<‰áO£ý«C|wð_%ø¬Br¼4 ÇdÓÇÔ»eÄR=¤ 6Qþ<®ÁlãEùÍYµ¯üt¢Ñ¤]eoÔ\wp·Œ‡áW&Éé!·?wí!³|Bý`¾‡ÁB˜Š|ê6…¤x5±œ{¥…©0ë"¤‹MyðµXÎ!¹ÏÅ!=.dC¼vW{ÓY%íb§5æÃ¾€â•°+þŠ‚fx!—†%®ïŽÄó·Éí"žó¶Á& !)à°~EïRŽ]ø+΀7·ÏÆ_,€ò¸pÿØøñ›á);„£‰¹ šI÷Vi•ÙOÞC ∃9°•UZ´H;':ù†%‚ŒQ"ÎfÊ!bc¬6“bÈ*ÚUhVâÂ!òï·ÞÁ0úô»ý¾Å2ðôû‹Öý‚y[êBp[­8<àM¼nÀH7}%ù0lİPý|-v«kwØ•tíöš:‡®h# 'c¸F) 7Pbíp xÈJ§.à öÁ»]oÎÑõÉ Ä*»Á*+õ.u†`UãŠÂl¾aæ0Î/o3àyƒ6| ¸{ÿzHÁ1˜ ¥ÀK.¦U݆¥ÍÌŒ}ŠCð˜ådÃ&lÛ7fÅ_p”€‡Á"`OÃÁV›—h‰)$r«_²< Ü"lgx7£~¾ÊjÑýñÙîÉÑйã3¸«½`Ý•6y­ñYö”UîÉÅçSÏ?"ÉêF‹´%DEXcrd˜­&¯8òFsGŽ·lV¼ÌV?'MñæöÖ®ö§ Œíú|Ó†Á‹¢Á ÑOÃ38Wc€þl»Õôà§øý|Ìf7*®ý®®fS¼bÚ&<ñ–†«g f+LíÃÏ %1ó. ^4ºz‹ÀÁÕÚŸk77*ºè)óÍÚU¥tнyZ¼Àúž8µ°e¿E°4í68¡~~ù¼~ØxÜý<^¶ G&x»0ÁÕ]&˜“d㇗l|†Ïz}‘~™$E±:\‚OQh²ÇÎ/ö®—?`¸Ï']ÅÐaUbã·U\Ù`éMZY<KiùðèzÊÄ/áÜ;¯üþM&®=s;œÃÓ÷>©—´fdÖݺ··‰[°–Í‹™ÂJ§Ç¦¿~Ùט=¥SÇˆŽ モ—Z¡†*Y‰í=íyYèÅ^ß>l°‚KÁêù‹!~B7€¼ù:á pÂ&νï°ŒƒèÁ\ÎAÿiÓójü¬,h¦k·YqcìþŒ8h0}jADhˆ-”ïˆ{×FYøØ£#lþÑp$ùÓÊc±k‡Ê­Ò”O}íÐ5².´^€q-‡B»²÷•φœ?§£/¾yp嬦ÐvåJƒÈÕ<‹1› ã6kl´B#øù襗ŽA<_'AÝeÕhðþñ€ñ/€æ´Ü¨¦¼ƒÅ‚AãaQÔbOÃ|šíf± e ¦ž¯ Þö8• ›¹ýô+JÏT<Ø *ã6lŽÙpråE\ëÇvû.çÓ¶cê¶—Oä¬^3öÜñgàÖ¸÷\[·ýÚžqC媹&^žoLÜÓ]£Ÿ1¦r£iå_¡â‹‡a[iìö§¿‚!ötã;ç'ܾ‰ßõ3ý#öÎûð:Xüòò{ ¾ÙÀÜ+õOü`J¿)o÷&†¹B"Â%Iu r˜8G¸5Ô!L³¯‰þ,löâ¿?^í»½•µê±£AÏY]yÕø`]Yx®ÄäecÞ¹urs¹1¸¥6{Œ»gôé7bRÛáÛ±†‡ÁÊ”‚¹œ¹ÑÅ¢âÂÅd™>d¾çe7Ù`Ó‘ìGñ«#‘O™+‘?`>B’À¬Û…/°±99y‚s¡.Ìb µ¯ð<@©'ðÑ¡CÆÜd²i]Ù#ÌËÅO˜79ÒH$-CÈìÝ×r!áèwK4†°R8 w)¥)”Ø 6bÜÉÒ¾ZwèÒ3€Õ–ëp¦$>ÁE½'ãSÄ\±Æ* Âaál‹‡8S¨bµÑ…82DÔMS¨›þ‚¦‡N}µ§ÿ°w­}úQ©É·«ÆÀîÎp‰ÇQÍþiçN,åŸ?½ƒ}{v“™ørë:IæZ¤ªŠNy [.@¥pöÐVZ¿Zù7Z ?¹Éå¬ã?`¿¬Ë?g?rE:uŠY˜çÔ©SÙìû»åŠˆÐñSgÙØrž¦ŽO¸‹ƒzà£#øLW]s1¢WÐÝþ´-&¬Oxt;¬´`ç–—L M]üȶ+^¾&ãß¡ãÐP{0¤­¶|G¶ÞKûà>JKûŽ(KÀ_þ§ß¨¯jß{†`ç?wÉñ·ð·øä9Sì„v[·nÙIHÄ*æ-¦œÂ’(fÙD¼UÃâ¼ 3˜TØ(L!bí²Í›<ܲ¼Û6ó_2r„Â&Ï&ïNý”M ÖìIøUù¨Ð5Œå´†Ø¦ì\Uù\«õ8hËÁ- aã–fÚñëåPaéYÅÞÈ­ÉÊiƒßƓė·1ÎÔÞt€Ö»5u¾ðwLyEÇzû‘815L¦Žˆð¤>QÅàNØEÐ2Ëj±‡;ÈÜÖŽpá¨Q}WUkêç ðÅœâ`æ¾Ø£}Âßj!,Ú„ÛÕ•S>y¡Ãt¼ ¥ü"¡j¬\Ìð"ÔbPxéQÍb\ϺËSkÖ¯íÑÇמ¾9ÄR§ä¨îøÑ»‡ðÇ䥙ú9r ôîÚ¸Óæm9€ ”½ø„B†ÍÇä/É®¼è®• l†3離M’£±V±AͶl.„FýMþ1n]¾–/½Ø€eâ’l4)vÅ“8…÷š¦†7𠌟^ ’lqqüËùÑß¡(”úÑ9¡äóßÈp,Ö‘ƒÁ¹ü“ý#Itˆ±¿†yftq+MçØà5$±±Ð³ò%6ÿ5­§á^çÔä˜YÑ9&šý*†€/Þù} þï Ì î¸èÔ 92¹òC%7±Ì‚åh¾Múw äA%µ¹%÷”LŸê ý^CÄPüºÏ›×úo„Ű+LÈS`!y« ?ÝÌ{ “õ± ÀBò^Þ±e–Íœ9¼" ƒ5Ir•û16}xf|)·™ŸŽWoÃJì^¡«€ õOØ|ꉃù¬ßkat޹'þˆ¬1‹þ(+À©úGPŸB5¼ƒOE·°?_ÐŒ{Ã/æ±ÿ k¨PtOpÜ?”—ò>Ú]_}8W/½ñÏ%I)'ºÊGfÚ„ëÊj¡iù>™Ã ^¢“ü†á¾±˜5÷ÅÆÉ{©w4OÌÇžÐwáŸxh=„Oaavu5 ¡k”xÕ³ž±±E[¥þÐjþG³œ9% 3畦åe/¹Ò´hó.¡½þÏ òóMå;‚ vxÉäg`‹51å· UxÁY£DÂqœ ÆÏųsßÄY‡á«¬þƒ÷ÁnΧ²ø…ºPââY¨ºãCܨÀüRS%Y‡j˜Ê°öï°å´—–L)ý/æS«ñh(:foÖ–+jKÿgd˜àIÐ?cdöÖ1qQûø?pruHWW‡@è¿ã±“‹ÕS9ùŸ¡ kñ¿ÁvÒ^ÈNþ+pe¥a2§ø²„AÀ%…þ ²c„4 ©( ÄÂ]X9T¦‰l„·¿á·¡¬­°”†Ú¥`ý¼„ŜŚÔ[kÛÔ¢Q«VßBKnYWaSØxfÃyUÃÖÔq#Ò ²ÏÀb puy,,ÿ ½ŒÄGŠÏÛÿŠƒ!•¹{þEGÄnHd¥—¨¨ÿ3ª½DߌIø7¤Ìá.ÁÌ+þ/h¹ú¥««_ ô?Qa[Á§Ÿ²Ýÿ_ Gßý7d!–å ‰å‚—³\¢éCþ׈1ü@ã dâû<îJ©Ýÿ›‡ àÊ’r € üWb:²°õEéÅA¤¡0ùˆ]ıã¾6ý„G°d䙈«<’¤ pzj_öеÞ>ÎÇ/ŒÉ<ö2Xœ-½joWþÊâWôŸS~èT 8Ŧ9&úN¨ç-º{øy–/é“VÚäak\6´C9‹³öýûQOjggg;kgSÎJkÉÒÁklö±GÄ'¾fÛŽRAUkÿE5îGYîGEE‚o¡8“h›Q*…bwä9GZÁ²ää à%~Fá)\†1º};]4ùÜ ,M,Üݹ‹ø‘/9W˽øHü¢ÄxxŸÚ€p—i~Ù£ž·)À]ñ<ŠCZ wïÆ¾Ö¥\ØêV†YÏÛÏyy®a9ÅÔéŸb;,opü©žåÁïQ"ë¦õaà7['Ó¿á½ëºA'@&Z ‘"!:.Á ü¨ù‘5âGø~´uk6aW ì^\£Vy…°þRg¸@ÚWªWϪJnÅdH{ÃÍÃÒØË³ƒŸ°Ó@×âí¦”(¾Æ]ùXúÄê¾Ãƒ \ï[tLuTN…0b©ì(!ò{ËÚ€w87³M>/î«åã Pa·z„J©ŠÔ©^€g@EH‹_¥4)ÈÝ£”OZY“GXãðU q‘ºYßReC×TÉ #A¨èQ$!lGÉ2¶ªµuYÂð_ÒZT x@qQ¡hQ¦ŒÎS¹\(‰œß¯H‡àrQùÀ+ 9³ËKoÖó/iõ¨hñL“Êu~iA&ÐÆ6{‡ …©»ïä´y• YêE"^þsèKé`"쟅\Ðñßbnt •¨_^/é¼›^+캢6%RȯŸ&~ ¤Oø'€ë¾V=ÀWÜg¿…LÖÞ ÉÉ0ç•-Ë3žëÒuûö  3¯ X—λ[‰ÃÎ¥é WWqø¹U£ÒðÂæêRGî·ôýd¦OáãÏeA!r…Ž‘‹ÓÛz4j]2¥OºudÆTYÉŠå׊ªÁcçªE…Û{×…€®zÜܸ·§½5|EÎ…¬ûÇ7Oœõ”1¥gôLê™8³å«dÔ +^=ÒÎcêJ9*úûw/ׂÔ‘#,1¥JQH]I?O· µM'ó3õgI2çID¬ÁŠ94Æ?à0yÐä¿L#›5ɬ[EÒêŸånFEG”ªø††‡ý2ô™çÃL#—V©›Ù¤™Z ÉƒŒ¼©žÈí'"'C]Ê”ùIž•Â:9šÊß(#†V7p´©.„–’‹…E–ŸÑ¼™§)ˆ3y°[F#e ‚ƒ£]9õFÚÔjÆ^6ò—ÞH¥ÊF#²…¿¸í2¤_6>-tW\©øbÂ\'EJà©'¤SF+ ò¾ÎûZy*Óå¿’˜™ŠÛ ðûÿHPG0-D„"ÝMú{¼¢AvÕ&‡ò•)Û WObàox‡%j)‹Ç±ÎjbŒš”¨Ürngù¤à IG-dÿ?­y‘+ „††h=°Ü^ç˜Ä®<ˆ³š´C!¾*‚jÕóÈü‡%"b2ðëqàŸÜ{ÂL¼‚´¾°îEBíÚ /2ùg&ìêP­sçjþ-"{÷ŽÔÑIÉ<=t©=,kÎÅs캶ÄÄM) 8U? ³”‰™4¯fˆ6_;åš± ¡’ÂÑ5Õù°üݲ"-5i†—¯3› ¡ÉCê!Öj‰È„¡¼9nPrò ÜJÿú/¾~oÖ~ŒÈ`¿]¹¢žï=º?>ÿ|>ë?º7Ç*VyÇÁqË[¼ ’ ?ƺL’”Ÿ—J¸¸c¼5P,‰ˆr%JÉhKéヴí·mcŠñx¶xÜã•Ç‹>Œ‰…’?·§æª³ƒ\䭺Ȋ÷Ç™qzIçû†ω`µøû)&¼wnéG–ÌÔƒY=8[ºóÇ:‡z[÷ìôV{”jœ7¨~pÉòÌÀ%$Àè¯Gä/h—÷™"•°lÆl<õ…—ž‚Q¶‰¼Åz: P«g¥Áˆ~Ía‹ç!))°.ƒr֥䦤%–?“Y/åéÏiç;¹äXíz6‡ÈC1¾ž>Í« Çs°ŽçóQ;¥N¼š”û^û†U‚KUNþYÉNѳz¤Pò¤BgTe®@¨"ö“æ `…_32xCrø·Î‘òÅoœïs®¦Pî6L«ç¼Çkµô6T¡Hâ…DI„èä ¢ƒw$Ñ’ûI Õø–LÏBejáÎ ‘ƒMÖó[ÁHÜ®þ=›`ÀD¸ ! ~Ãs¹¶ ‚ž¥Äç~¢žçoõÈ­¨÷1ÿ“Wv5ÉL pkb÷ &6ˮޑ7ŽkXáœi-R¡^j*~“:±tÀçŸÈ-Ŷ,8¥)Îg.ЉV¾ˆŽÉ[¥ø•ÚŒ§ñÌæRŠœwÇ/à ˆ„j›Kåo©Ó˜´„®2áTR¼•_Œd‘…«ªE3±ŠØr‡[îÊÏÉ&jÙòëÅGjÕ*ýñª³ëŽÕŒÛ‰Pón£×=ÿ¡hy·÷ßÌ6¢Ëg«?IIJŒ.çèÐqIÛš­™]®Ú[Mßu˜5Jê“Ú¡t¹ÚÎ q~¨{bÿ¤éÝìåê<ÿ„‚Ííú {uŽ8ÈYÜ•µÔZž J1u*Z1-ri!ªÔ/ÔÔÅ™DõqØÌÊwìb8&þ«ÄþzÒh¬.H° úÅfLªÌ>«[yR†`ÝÈ´)ÃI€¿Âm¸¿š7d¼ÑùŸ¥T(]ºB ¸Ëfg§=Ê弫_}¼v—¢g\€X¨#ðuYuúe7£Á&OÇÈ| ÎjS™;aÇÚØEè¡]_9@F\æy¤ÝЇÍ"7ð¡ ÔÝÕŠø6í O¡‚æ_DéôÇRq5v‘ÌÝDÓFó"“´+”™pŒ"ëVPQ©áP†ÚÑ9~›<–c9œŒ“ÁwÚ"¸i0yÆŠVS˜vŸ½#w×0om:Ù†ûÔ ,'-–E3Ä[ïh®]>°Á£ùÚQLLÉ@¿ ¸KìÁþt®Öf³1(ƒÆ_¡'BQ%%UlcW’b„.ó,?~¾ªõ5 zjK·¥Ôµ*×3’pù\F¿ŠMC¡¾šà¿ÞzPhRkãIVhß42…™Ïu°ûÕnuIK72ÌæÖ–.ó?¨nBOÏ÷š^‚KÖ25þlX™ÐìNh63ùPÿ–’‚„„‰‘Š0 ‚_[‘C‡Ü¤ÞËLîP²dÀ–2ìÌà›ïmœ<ÜŽ³2ØŒ=Œugò:öÇ­_6Ykòyyö0‘Ìç5 ë:ׄFÐD_*+±"‰y°ëè‰Uá2¿Ì ïÏG¡òJlÒ—o¦$%¥´Ü–¾èLvy#t‚ª]6uÔƒKMŸ^JÞo·:çÚì“Ë’÷ŠU€¸²eCä7|J¾sýÆø€×Mà È…Áò©?×N0ÉÌŸ5ܲä[ˆtH-8;zGsXG<¨”‘á<œÁØžî¹õs&²&ÆiØfI}_ ¬È½"÷íOj˜9ÐȈ͓žzK%9Ýj!õµyì`²2:ê'&•Êù4LÃ3ø%´€pƒ:N„n#A gæ.eo ›d4édr£Èjàð‚š˜…Okà„À¾ìñÌçãäÚ8óó·Ò$ÆÏá(ÕÅ¡B,@û€|ùíZ©@ú‚‡o¸¤Rt«¼eQÍð.NÀÄ­[;wÉT–}Þñò¾SW¤Ôˆû !Å&_Ié–Ù¡18ÔæÊx‘Ê$FªMVÏ5ŠñÌ"‰ƒ#Dª Áq㸈‰„…“¬6|æå^¬Aµé|Ä%¸qØ€·’Å‘8~hŸèçå°¿¯ß¢>ƒ÷Yìç3®jÄàÁlVo«¥TÄàsò¸§•Ÿˆ³Xlëø‰¸oú$BÃ\q`ÎyP;VMÜëäèÒU&cŸÉU¹—‘pnE<ò£Ñ¯<Zc^— Ë¥ÓþâŸxý®¼Ñu¼‘£óá±g/Ñxûfƒ©þî&Ïbfç·ìm×1Ç1/Óu¥°¨ššKÚKXlõ«}*Wáj"É|‚êÂÉ蚦YEÊ]PÅ„ƒ€†VùWç|yxJÞjî«äü•˜ t“|øŽ„·Î|«ÍxZÍ8>dðV®v‹y“3öÒÉß!>¹…h [jtÜ&ŸË{x@m’—qý÷Q‰‡Lµ©Y&²~*ðñÔ±¡6\sËHáT³ÑÇ´¦ìl¦6@]ÁÉÈíG'ýÆÀÚSm#+§ÏÚØM›·kH#Õö晞ɮGd´ŠÞÔ2yëÉKi(ÅœQÛ:ÖP§åM:”8ê÷ëJ‚¤ðr›¥¤Š5‚Mz“H$l h‡¼Z|Í¡T‘AÔèν|;ƒÍ,ÔoríÂØò•>YÊÓGïìDã–ÞÎùEû‹-k’qèuœ£y°¿.…n¹$RIý‰ôý$‘„ÃkÞáäHJA*w‘¿6Ô•“ЏôU¬_­\ƒUcÆögBϰ²“&•]Þn@ÿ¶mûh·íA“&)©…S×:ËÉ%ýʤ`|J™¦~~ò?¿jÎÛ~A)•Äm8vR›6Ę% «…ÊsMáÙ,c½½"äÞ6§‘õ£·áœ5Ä• ¹·áŠ,Œ¸¼Žqסy-Ãä–;\-é\Ùš@£ÜÌMRY±ã»ª€¤ƒkÆËÑDõŽ´š¼kôrø„µØƒ†OWN½]¿WE¦!Ä?_=\§'åì(ü}ZiwKxÍ÷ªVêÞxåd{1¯ðÆ£ìZåøº j–.mqé×û’ ÞR³fz1qžÅ*õìuЦïQ”äu•ñ¢\a`üÃ#b"¼­FÒXW†AZ-ÂÝŒºQƒÃ€¥@*Nk1j?v9¿vÆ 5ÜÙbWh Ò(±L€Òw¥h£Þk•òñ÷ì­h.ân©”^.jÕ_çŒÕV0dzª.ØâŸƒ‹s34’[o¡‘P©¨­Qzi:Éfç­:É6¦h§Š“éÜÀHŒS¨¬#DŒiºkòkc×ñÐyè¦=…Kàþi8§ðg¨ósEÒ/™§è3=ÏŠY:„zG‹?ýM,ô]lÊGY¿f9#²îÒöBfæ~Ÿ¹^Â…û3q¦$Ѧþæ$ï-s¢Äøöœš(Ú.¡»„ì/](@ [·AKÄ~p¼Àk ®Ärø;þ¾z£.‡þXC@ŸËáµÀsfR‹&¾nWÉ¿”eÚH¡%\`¶ÍãQ5Üù­œ•Žå›&õ\~úôˆx-ŽÃ[JÛ¶Õ‘+¥Z1Oµ«s5HLœ"õàlà¤ûÒÿüFòÃÚöøƒâ¡mg­ò.ó× MqÎO!Žæç»Þd1I!®72¨ï‰r¨+=0]•ÃDÙðQÙî*+7D9¼ |B”# žÏÏ—+hÿ©¸_^”ß%yÿ—­*/ Gq¿b¼¢\éx•]e³ŽO•WÚ¯*ʆK€Ê‘¯ÐWM¿çûQð$q?Z”å;•cDÙÈMe‡^ŸîåX*ægœ(óÕäBQ®.ÊÜÎD¹/óU›zM”kŠû‰TÖé­Uâì°H†Ù@”7vR:rù–›´ç%î77äù7ãù†ü]bÇDûú¡Öå7]öAùA´§ëÇì ®÷[È£(·y~Û¢ú#ëúÑ)ÿ’ú“¨ß¾>M¢r½>=ïõ;<FÔ×õ£bþ%å–(ëúñqþ}Õ]”uýx—ÚËVSY×j喝¯ëÇ4ºB”»íß2­åDQ®Eú¤ö§þ¹*ʳ¨<@à3S”åÿ¦Üüë'ÊÃò©éJ_*OåTjÿ¼  A_‚Aß»¢\…àðIÄZ©ÑWU}(‘e¸4Ù \ÁšØð‰¾·î¾Ÿ`um À®ÈðéÓÃåjÓÃ#éW ~ƒ—ñ~õ ¡VmÔ¶]£FíÚ~¦Ì©ºwáî U™ šo¨’7ž®@3œƒCa!Œ‚Ñ<›§Ó¢¼ðòÎs÷–˜ô1¡öØð¸ o9جªÃ.\æl$yÜc¡uÐ!o—áFlšÒ”ûÏ™´–žÖ3ûÚùÓ`lEøYÈ[!‡˜aüå ü·ÏÄ›ú× ö„øf}cÙúþ*ÌeÛ7˜ý‚UÎ;ÄäiãS`öòÑïwÝŸð6>îßw;·ó`_üë÷q«&lÃo‹Qhû¸O¶ÏZ/É®ÌÓ’M çã.XTÅ*öS®Eí¨è‹©à๯¬„…låKÖ˜påþ1¸÷þö³ýÌ…Ñ#cð T¬Ô°ûÁóÁ¿\fðôW_…—êW½‹2_4nƒ?\½Ó|ÒгTSЉ½a®í>±ÞGX±ñú­Ú'Ú~k£Ñ™Ó$YÏh‘ Õ$YùÔMwƒ‰¬SbnËö¢üν^˜ ›ÏŸÇDm­ò\«*Çr­³] ¶…/7QK«HlµäçZ,šÅü›(Q,J0wÍsêxšûØ‚Åâ¼sWޯݱñm“HÜ©å<ý |>-[öyFgxT7rèÐHv7Inš¢¡|{<è´µk¤ÌÞ•ßbnÚyÿTm‹oDg¨Ü9œ¤6À‡ª¿n61IKCóèï,ÄXÕGȆàF±6]ßz xäfãA¿û:|#¹vã<‹«œ2¨j£‰A¶¡šÎQ[áð@|‚qx f±Ëðy½4šƒwb:v™WªTùaлo•-ˆ9e›é+1žß^e†[ݘûŠ|ÁûÑtònÊxí,ƳŠZ[cëMlÐ }"ÑãEòâzKu©ŸOèû…÷Fm" ¹d¨¦ÄW ¤¯vÎb.áqàRS»±x‡­3gA)°B (5{æ’太W1\.OZ©a£JÎ_û…ÆÄ„²FŽÐ~ýB·ÇÕoX=îÝê5Ô£?<Ú¨Ò¡•äÀòÓö9üuÚÆòöÏ¿zóæÕùûÑ mm>Ú&Ÿ%,pÅPÃ, T*§š´§&“ó›j2iMeX'Ÿ¶¸·½»zÕ…z‘Õ—Õ¢…¶‹í(»N¹Muõ ‘ëS/â¨ù b .ÎÆ}¼¡&õ"ºéÅ©0?Þ€ã`ñZ8vÕ;Ò`½èDü˜:‘ý¬)²ñÍÏwD‹Ð*ßÑ7î¨ì%ʆq¡²·( UeQ6tŒÊ¾¢,4E”ýDÙ]*ûëÏS{¿‹û%EÙ*¸&pª§¨_J¤Ž;&%³©µŒDwÑúkeÕ*sæä-sï =Ù!g˜òW^qI.TSøã¨^„È–m=æÞuγƒò¥9 ¼îù'§ý}vH«#)Où Lˆð$¼˜MXM“x…jÎi±–ò+YÊj¡_%›• ð²¼XâÂ…ù—ò²X,¥ýÃìtKÔÑÖ¿Z-º­FbÄY^ëoüí²¸Z%·m¯ƒy˜Pô*QÂèo§_t‘~庹ð„ºK`ɦ%¼Å » ‡Y-ÖÒþMÚ%ÿ¿ò¾°‰#ëfw¥•›,Y–änɲ$c75÷†)c‚ÓL0%Œ1)†@Hu8Lè åÒŽrÇ%!õ ¹|)„KHŽ$$wáH¿$¤7.!¹KÃÒòŸ]ieY+Ùã Âúîo,m÷ÞoÞû½7³ã± I*…óG¿¶K}ÚžjÀhP&â´žo¼h‘˜WÑF•·ZæäÐÅ„ìá㵞£Ç%¢ @†è´Wt5'út0Ì €~8A@4eö«î;Øy'5# Qú©…ˆC­õ|æw»ÞHÂã?oåÕDžpãYÁªÅ£+×—ÞFá^ï&bŠp§sŸâdÅu‚+ÁJpMèT'^!ôe~ èB¤æOp\M„Wõì´Êj0 ,7€›AØ@ÀÞIrm~[uþ}VËnÜVsÿc vcì· ðåÏOƒhk?Ǻ_Ø»ÂMÌ+ã9Y9=kÙÞ¬T&ÓÆkÐ~¨d»3ï„›\×8÷\Š×ǾÊÉì‘i_áaI„ø¼ôâu"*z‹×“ø.ŒLËÀ`XT{¡õuJÎ ]ø¨g˜ZN@ókÁz° l»ˆºr…a0 .À·SˆIE¤íXRÑ –"R±Ü~öaD6™“zg¸ H@û>¤DÒ¯·æ pFФ¾Ú§®ôÃ"’Óu¿‹Ì2oWÍ÷nGðäü?3dùÅ +Ó§† Ý UæÏxcñ´tLUZXe@¢/Qi£U l=‰­^àëN°Ê±¬Æ¯t„^¥ø¼J¼šÅR-ñª÷ÍXG€J0ÅCh»À-ÂráFáéÂN/4X@ÓúUêû‚ý±¿Þ+v½€ŒñÆA{‚Ç»“õñú`NŒXvA-"Wѹ —øJz¨ cbªîN©À‹ðFFEa¦/O,» AD-ãoÄ/å¿Mñs“a·3^ÆzÛGœ­ÉÛþÉЧ/&B14t8 ÁüÒP"'zÀA§ÀÉaØHa$GáŒ3Œœ)´Ø‹dTåðq8Œ¬„ŽÃ7 B|ÓxMh!+ó{üFü\ ÇEŠ$àl³ã¡³"ó“pòØ¡ŠCìˆ@„pf“Ob2|qDhÌ<1ôVš_u1l9@RyO¦•B›G²‰oèaH@ñ5`Rúdø§¥BL„²Š“aH`ñ±=HRûZø§µø˜ ÷dga›ã#/fФø‹ðM‹Ã’"’åðÇé'aB·äIõ/ÿýiµÎa–l‡ßÛ€CvÈ'å \ÚÀµà6¿´ÜŠØ ðøP|þyÐp±ïÍ×]k1F`Ôú@•ÒOúèƒ<~·`›kg€³ý²xø}¿‰œÆ\xû"s ¬ë›À( "Ù} ˜ ëÀvð'Œü>l <ñvsc•†QÊ“¼ xFD¡ ÌÀ!¦‚0œˆ]WNE¬7,äBÁpœ_qpá•"ÂpŠðaLÂÅ5^~ºô1 ÁÊšÆp6Ä•Žá„°pyEÐCê…Ä» µà×ôÎö+¹ #†¡ä"è2Hba—áÔ€ñÜ» ¿{øŸ]Ô‰—ð˜Öð—™D‡õSÿ—ÖîÒ>`|Ë5ÞðŒOªQ\–‰±¸“Hâ Çc¦„p»°T#XÈôW‹Žáá³!XlôBi©7'°3Vj„ ™þz܈eÄðÙaXWôWéù—ƒø¼YX¬.dzqÔÏm›àqìÐÛI<ó3ß…L/„õT¡gåȦáº^è¯2²Ó:æîCË.dÎ¦Ç fá½4h¢¿ÀíáB¦<Ê0Ê „h ÿ•@/¾ÎýyØ21,dê‡3˜és/³jB´à¨´K8ÑÍõ`eN"Cp˜‹¥ £„tX¸)F4 RÉÑ}mÑA åä¹ ©{,\sF|I=d%Í¿„Ç ."ªô~vãÃ]ÿk-LJ;áq pSüšþðš4|[9LzÓ X°`Ó™ë"@‚jü¿´.J(Æ)Bˆœh'Yâ™´ìA9Ô‹¡a'ä°„ï ̃>¹Èd@®z/¶„ÑÞ÷0/²ê… ÒE£1<Ò‚¯¯^ƒ(!Í;õ_¸¾È°Ë„À7zÓᚥª‹“€3ÌŽ_ôebèe¡Ó1ŒÌ럄- ^5ƒ£Ãc+œD1ô¶‹ 0‡®Ì+ÁF°Wü£_¡ã !H)CÄ-ðŸb Il﨓Î'Ã?íví0MF1\@¸&¨¡=HÒúZø§­ø ÷dda›àâ#Ï›Š_¦tX'ó…?$Ã=Ï‹>“ð"aWŽ‘Fÿ2ü‰ô0òñSÿŸ,ß9l)÷°¥ƒ‚UJýq_$ P/€[`ÜC!îùkTB?¼†þñ°Yˆ§/s'®Å|PZ,_äú;æâ.ÄÒgûä„—)ňáaY¬Tí¼0ü ð&Š—)1ºfà]9FÀˆ¸VySÆ`«”Ь7„?¶þ;VøÄ/P„/…É£p‘Ò%aˆMªñ¿u1ÏPV:†Âò`ƒþ\ùxœ? ÓÀ?k©+ï,…IªÑ< …§_üIóîàEK@\‹µ´$´Öÿ°w8Äò`d1Òð*™ s' Oýÿ» 'f]ÅZ¨®‚µ+qs}àºÊ°wn…Qzê,Q@ ’€je¼E_dSZ F}úbÐg /ìʹðñU«¨gV­ê­¡ž9{¶·æ,Ì¥ÆJXõsϪUôêU«ÎþÜsö,½úìOÿ áÏçÄ{í €”5€ÅІx­Án3 ÂòŸà7&õ¤ÌzÚ@½YË"’Ÿë¨M+»½,­¶ã¹äXæÊðk1aŒ>y'Ô­­c«[K&¬f®7?¹ëÄÒ¥'îú¤Ñh‚ÝT³ ÕÌ!fë|êwå×òz1íF½Úfw›ÕdÈÒ¶Jh)B€RL)ànxÀu0£ ÎX<Íz͸‰¿)¹õæp1®4Ϊ+;Ó‹a᥵“ †5))k;62Ÿ$›)F²4¨ŽG™Ì¶4$”I˜m胟¨¸’w¶\väá’ëÆX¯µlÕòŽæK/·pnVÛäúÛþ4 Wb×­qËš®]§Ž_œ4iÌø†ŽªòÒŠ8íUʸ• F·@c¥Ù# B£Ò¢6¨ 6½ WÀ-ÌËЦ¹§§g3Ž¿eæ)®O–=ñÄjÙ&‹hºRÍ…Îñ» ˆ‚2œÛn¸ÀL€ ³:%5%e2TëCå××–TVŽ^Þݽܺ؎+ÞÙ¹'"(jbÃÍ=ÆÇÅ]‘,i{ -^-0S5hÑ ZªŽges TšM*5 |´‘î¯tî+®!Çΰ ŸÓÙ÷û ‹Š o¬«‹4•”ÖÝ:uêŒé·ÞûPGÇch36dw[m³gYm6ë¬Ù6+œ¿(>2jBÃ…II ×Þ0©ñýS/¬X)‹˜ß!B%Z½ SÒ³Äfÿz8ޱ`´Àù/â õkj˜ ÚE4pä h5¹$Ù®ó0 o?Oêìcht®-û„ÍÖ>§Ån³1g¦æåï‡iM“óò§Ì*¶MkÞ½gZ³Í>2?oÚä‚‚ü{ ò±M:;7gBCsnήœ9q…Eãv•–._PWê¸du úYÝXRüB•Éh̬áÞ¿Ìyw_FêúBÛ«)U§ §WlÄÃ<ç‚›êFEÊKFo;0}Æ­÷NŸ±”Cù‰m¨/L³Y­Ø éý`Q|Tä„ wæ''ݾö¦‰Ë—»!¯\Üâî-V«D•`¼(—<Âràȇ\PâTc‡JÚæ°(1µ@~µ¥Èb)bßVß|ðàͿߴ‰éúͦ[°}ùñææC÷7£Ÿû5?øþ?N½ÿþ©¼ï‚‹b¬E“T^È\P,ðbX©W0L"*”Ÿ{ ÐÊ7‹l š*ä—ÂÆ )¥ ÅÂfàsHo›°h£ }¸ed)H ”zÛJDƒÈÁ·Ö!2+üdà[KÔ$…ärÝú) Â@™BŽ'Ða0ZGzÛÁ±QUz¨Š€*¨ª „-ð5æ–­Ì- ŠÔ·À¿3~M‘2c™o™±PÉ| ÿ*Éë­ð1›äƒ2PÉVl%v›Ôl2óŽýÖ!_ãõ9´Ô¯Å!›ÓJÆ´%Þ¦¨ºoBáµð­k 'ÜW¥¸-Ñö§úç‹PgCïs$ŒµÓš¹YÕÝþÕWíݪ͙ÖÛÆDCÉó½ ôw8¼”jª‘pH+¾€Á‰,¦ øü–z¡¡E_ %àb\eàód¼ëÅ«EH¢qek Iqø6/W žT¢É8† ¸Wr|ÞÎ+aX*J âI=¾Vð‰>®~Äó^_µb´%Hð” H°Åä~RŒm`ž$!H20„›xˆ‚ø|„×&š»$B—â“|â'0˜ŠŸ×ðʪÓp=¯ø¤_AÁ!làçG‚^(Z ‚ä OdAB…/¤ ψ3 OI¾lÁ39ñó5^â‰`ú¯”y€$ohnÒ0å!ö3sA±Eg<‚¬[NA¦ˆ/]à’òI U «Wz iDÿõ¤JUF‰ ƒ'“ ™§ ô2HCÉ(ažœ?|„y¼•M.ç3†ÿÃLôk=Õɼz„yåÈh9`–g11S?ø€Yä4a¢ ÁÁóíôýÒN€Jô: äÞrØWÑ®ÿ0™M®ï‰¯Ð·X×wÜ·ÿÑ€àOå’c qŸI)Ðç2T.¬€O3£˜ç`košÿ5cáÓ°pô;×9†‚?½CˆÀ÷ºÐý¾ßõ=³‰ÙÈWÀ.Bî¼]ñ¦†9F|å¾ ìr_¶÷6þný.¬xa$Æ@×fö¿:'K¿[hû©v`5÷Àg`9¯3t£½Ã.´u3¦Ú_ß«CôÛÈ}܇솭Ì^öðÿ[0k „ô;¦škè~÷PùÝ£@ìÁ®.ù‘¿°DÞ÷QשžcŦžñ·J¬_c‰_ú[ÈÀèÏÐÍ€J XÀ àÏ«½H/Íp°‘€Ë¢3Ñ7Úªõná±{öH‹*I‡JKWR(çFÞ©r¹·"Â@³® 1PÖ=™Ñ¹|ŒÑJiv©5;Tid\‘Ö“§³GÊ ZBi)û.‡(Ç“ ¶›Ñ{‰.Ç,â³Ú–ÚÚ–.åÈe|û¢UæÆ(G2Ë5:Fw†{ß×ј—A(ÖE—]qU³}”£ ¦~þüISºGÅukaöl¨íŽÕ=eÒüùõ5ŽQöæ«®(‹^§ "’óŠooÈyiëÆûçÜ=áæÛgÂ=”ë $(Ø«abt:÷'£½]g(¸gæí7O¸{Îý·¾”Co[´hۢ¸Êå îË?Ý_\v³ÙnžÃ½ŸÎ,™3½–J{Ó–vmo»Ã’e¸yÆä%K÷·ÔV™¦LM–“¤¾a{×Ò†ô^)óBåô9%RBºîÈŸ7m¸òÐñŽãû®·$Õ`zúÕ‡–mذìPõééÐP—d¹~Ú}èÊ ›þ|dî„óí#¥Òâ¹ZhÈ[À`pAÈZךÚÌšÝC…QM¡othi³¢"ƒQ+åv§AŽTsäÙìÐh+) ¬èE…9B¸ b¡”6eê(5D[HšÃDš›FiíÆ¾ûAš»ƒÕ.gÈÈ“šóÐu*¡ƒ…K¡Õ¤Ct3”iÑM+IiÖåt”ÆB.,rÿ8 yþA©Îì.«‹*+,è•7R®ÔjlD½¥¢²½z§Xž/©Ê&wtºNÖV)%%Ö‡7.}cm‰DYU›'I‹‰„Mf]±1_c.̳¦´œœ8¥Î P©ªfV0?1G˜Ÿ*fV©T ƒN—““f"¬y…ÑvM¾±Xgn‚‘ñ1i’8Rê¾7ü×Á쪒çϽ£.QDêËrlÉUUdméˆ\ƒ¥§&ÕR”¨5*k²ûáþýެ¥Q›XdI­é±rG”ÖFV©ɶœ2}¤¢D“Ñ¡ÑÕÚ×[o¯Õi"æbHë<æ‰yÊtÇï–^O¼ÖÙ݉þ'ר•驉q§NÂî-Äá‘ï<‘°Ã5ñäÉí+3딉ʲ˜º² ‰ÊºÌ•1±ªÅ¶éÍñQÑ:eadvƒÉ•dPÉ3”‰‰7_¶lÙe77&&*3ä*CR’×ÑY¨ÔEGÅ›'N·-VÅ6zîIÜù䎄'Þéý6;÷’÷ê#dÉ1¹±%¥K›¬smESâÌ ‰fuSjrccrj“Úœ˜`Ž›R”a›kmZZQ›“,‹¨ï’Ü사«jë Æå™’“Myã êj¯ÊbNk4™j³ È@ÅùvY*Ïyº,Pj@#òuYdr/-›ò¡ÔJ3h‹ Ô›I³8§3Xd#Й$•Ð7IôæØ,”°î@ðBàFØeËItžQ+Ѹß4:R£C½¡‹#·i¯‚¨Ëhí¬ÿCz)tqy)ûFÐR½Îl"Q0™Ñ×{ZFÏ.:µtöè–ŽmãëF×oƒW^óì5•³Ö1Μɺ¬1vµ-¿fÌe™’œÑqø¸dá˜Ú|›Ú>&K7yutRA–‚ŠŽÉ­†Ûã"#5Trö¿ïèºëw]wü;;Y¢ŽŠŒƒµ‘Ì3r-LKb>\´ÿ0üêðþĖI0MËÓA •–|jÓ±M=S+\#<>*>ñÕÀâšÏ?¿qÊÕWOá@þ¹È9ü‚ä' ¢Ò£…™àz j»ÉYa´¬XFNBUù=Àˆ§Ýë<°K?äC‚F¡‰d™Š»YM÷‘V™¸ƒÐ ¨;ëuè ´›;Ô,%Ë‘©G ¡™†ý §‘\¼Nfx”_ô|è}š×(1§,!ÍÞž•ýsŽU óËk9‘q‰é ö4my áM»`²Ñ„޹~DBþï3Ï2ÇNß™Ÿ0býHöØd÷=ÞvIÓB¢Xåõ¸ ð×ðïy¤&§Ç#—c²§'–è˜/Q  ôïP“V’˜n7ÓUi)©¼ùâUIè¨õ#å±½óôé»^PÄŒ\ŽKŽÒݲ§CöO>9kõÈ™{€nØûÙ )éf^åT­sº¾e ܱ¥EØÿN[™ÿu«mÍ tþ– Ðé*AÐn…!ž£ɧ |ñ^z ‰·) uñå£ò•îaE­ÆÛ $¿NZéš“’ìɽ^ Á©Ü‹3r’’í¹›N¬6–‚Œ\" Q‹ØŸÑ¬–˜(÷Á.u-4O|á…‰ÌÛ{™·Ù®DÁ–2¿Ú¹qCç«Ó¦½Ú¹ac§ë‡¡Zu+’è8½Ä‚t0[¦sM;P;)Ë‚X.câ¥Ê‡,Å¥¥’¾žjä:0÷Z3õ:–k”žã¿±Û<[µv½NËòD¯ˆÔ«R²¶Âí[Ì©W‹s9+b¾å~_þ)ó&óÖgòÓäO¯vá;:C¤ÁàDŠë#^U3’wŸâÕôôàx8aÔCï¿÷ÐCï½_›kÚ ·˜sÇ•>l“û4rZ^R"?íÓñW6M!ZÖ9²®»ÅÉü‘×!œíl!ìã'_yåäñö¿ýÐÙ™Âwº@ÎýÍ‘ „ëÚ+G+1sjÕ ÔʽC”•}šS'aþDæ5¢J ’BÛ'º^@:D(èÐ4ð£µ0{-|ò~^WHZþ:óT©á™Î×—»*‘®¶TчHÍÎ>ãúûçÌk^ÅäN‰·[Ä‚d`EH ¸¤ÞÁ+h$äú‘Š!–Z½Å+žã»|M Ï€£Ç­‹…î.%˪„r‘Cty¤ËáêüEû÷]ôóç0 L8Z¡œVj@bXâØ,¨“§B»Ð )ÍÕ¢”‰jóç€ÌV–õ+/«ÉÉ0ÌŽ˜ wÀ”ž5Ž×‚W¢˜Ô’nÊLVÄè!$Ä2V¸Ø‰—˜OaÊ‹¯ï]¸€zZ›Ú>*3+A+S•H¤yxv†lʈ¤Ö  QïóâÛ™¼ ¼Rª†jhâ[Ù™ùØ–Ö‚|.Kžî^¢P-…”›šPZŒ„ÕZÕ³Û-žÕØèxî3Ú4ªù©IrORb9,:@ì©„‘¦ªº”HcpÕWb`dŠnD•)Vî!ÁòD&nPéÏ•xTn¬ª©üÛ‘ºubÔé å©cÇ¦Ê u:X—¨n¨;ò·Êš*£œÉX-®ƒýô (-€…c¦<¡7¨9ƒ³oÐ ¥}ä7úÅIRïáü}ùÑÔÎ{Î܃þwN:­«Q°•Ú¡Ipóö„¾¤è¹½“nÙ²}MÚë‡åÙýY–”xút¢ÙIO‘d›ýèÕzôRyš¬fXêvx°÷<Øñ’§q ã¼~P{‘›zÐë§5D 66¹0ÊÕq¸Fòa/) ȣΕbRxEO±‰óû^æÿöM{è°W³ìïÁtÊl"žÛ«ÌO{zàê!yóddjGðT_ânfߤ›¾ß7ŒÔþ)œ€«<·}â4>X/AÚÙã&$áýdÞöË@4P€xŽÚA-—_pÕ:dqktÄ@³Q;4xH/©I¶£¤ÂYAbL„3zßCo™/Å@†ÉÆŒFÒüñìòï:;¿[Î<ØD·âƒÈv'"~ ñbß.&° ä“Z1ÑDØcf8D“;KÈE%Îub ph ܵ>)ºÓrúNç°…Ú¬—D@$3øu=Ù´ÅÌ-÷É“î#m"^œ ås]³í2Ô§Ì÷èÐ;­#¢Æ“a+Ö­V$z>[³¢ÙÍ ŠÕéd {IæÆA €Ÿ±}ÏL_’š1.ÈÒÏ[’•1~VÆÜ%ièwFVÉÜ~~AãÎÚƒV\H”¼JÔ´–Kcƒ¢MXpyõ’m5,/g\5Û.q>:¨ù•&³²qåæyy36¯l¬„†A,)õ‰Ô`ç%ƒ3a‚%¨´=)©œ@Dfw%iªÎBD†ˆÎp6 ³ŒuC”Ÿx0ƒýrF‹­ûçÖ#*Næ±Äž~¢˜B–ÎÌ)T4Hð åÛø¡rròG_ˆ1^óK_ˆ¿ Ù « Å rÈÃF( œÄz+²ÈáÅ¿G²Ãi¯"é‡:± §Ç#µP‘¦i% @n/°Ÿ9U W¿>õ…qØî”Œ\57øÐ‰»ð'1r"B?QpJŠÜ5@øW^V×ß}tpäawõïUó ûD¡ûqÕ8Î66nL“RyfoÐlD4±u_í¯æSgPrAPY2Ù\ï%6Ý 4âH•ËXlAPñ¸\ó6!2ŸÌ^Å… ¶J74³”Ź)÷Hä뫞8NËÒ¦¦Éˆ²(hOI–;«åå ráRÝŸÛù€•.%Õ;q•#YÇ–-™9WédŒ"t!Y„*—ï÷žA%N×w¢Ñ¢)Üì*7fŒÐýE â )ÿ’~ #Râ0ûuDÖÀuán?†þ‚ÂOv'äwÁ ÄÎè~Àóã`“‚»Ißh-ÔgS»ºÆO[²|RÕŒ£U¼æzcx­ÚêÒŒui˶×åäè3ê2ô99uÅ[:SëŒiuvaI⊠m‚>=IcpíÖ :qeäåK¦ïúA“”®OÐfé™éºEúŒôL(×gä$8't‰ÙPž¡ÏL§’š:VÔOZ¾Üù%§ðÏ8åCo“»~ó,ËÁtδͬ.¿µ‚®vb¤˜çÐ×êÌ1Ž™p{vóüSëîö£´ûôúA猄¿cnE·™T¿¢£)QgР]põ`ä&UV¤ìûøSZÂZýÈ“‰©wÔ»è¨ç"âý€å)±6çåþ ñ Þßq½ëóåÜS"A®òq_Ì Ÿ/UC@¹iSzºw ËÅaÁŸ3#1óåÀ›†æúhbb2ã"%Ñæô²8aþ,NÄñ®¢Ø(*:#^-ÍŒWÀ£Òj|•ˆŸ§!sž†µñ'j’xg´I@4P 0#j¨òyg«I÷ó’¸÷OQy~vÚËœÐDW¿tö‹ : ͇¥Ñ@ų4~ºð£>™»ëÔ®]§˜£¢†žf±çî"ˆà…)=Wƒ¯,¢{E "Ë`±œùbBû„ íÎÿT° Ñ›‰g¯0Þ3ˆ„¾£A îa·ðÏŸÑ ¯8¿jDȕʘvG…òÜfm90DÃùr™Ã§»j‘–‚“[dç·I-1:$^MØò ™PZÙÁ{ ƒmT+H9DÅýW4Ulù—íÙ±PS!â*:3»õ~–yäADLâXÇ‘ hLŸ;‰ýW ¹_n‡H£ëìAœ‡‘¨'³*rÕOž¼¢©‰ù…Õq=§LçqNµ®}ãš6,®({ÒÄ—Pú@YÅâ ÍãzL7¥^C˜KRù’Å—š“×w2½«×'›/-±D.5–š‰kRo2Á†«aDGrÁ­ÅÊr%!‹Ž¶Ä*‹¶¾ïZwz«%.Ö’ #Ðöâ[ ’ɬ)+P#¦ w¤íoâÏ"8ßàŒÁ¬z¹rv!ÔB󔥑<eÌÏžŽ\:…y›ù²pvåËŸg¦w–œ…q)Y†eÿ¾"×XR^ÕÛ¦kì­*O©6æ^ñïe†¬曳%陟¿k2êg–]Öœ¡ª[»®Ø^ <¼íôé­‡£äëbWÕ©2š/+›`v™gsë2û?­×qø°QßíÆ Ä‘¤[“ßßÖÒ¾vス[»¶Î]°qm{Ëm<™ü Oÿ˜{ÄOÉYÏùÅà½=©mýöÙ 6n\À¾fo_ßæa…“< .Ѻ¹²8›¦A:B¤W}ãA3¸Ìm¨#\ºÀjp½Ïì %KÎÕZ–|ÔÞ—Ò‚þ™Z­Úö¢ï~G˜YBŽÎîGÙ3=úT¡Ã¡ÅfÖZl´Y©F×6ZMJÑÅ”h‹º¯*Q]—f’Jë¬uîŸÙwÝËÌ¿w×ÄÙîïh‡TjL¯ ¿œGUÕAE]Ýz+YhMtþRW—œh-$ûê·2¿ÿÎÍZDE0#×s?D1ó&TT-_²¦µuÍ’åUÌY8Âuܽþ#‚Zœ•ËDÃë¶Ý×_Énï»(v‡±çŸ^@e@à ™×h˜å§ó†ûUÚՙ̻7eP Ü.8ú º¡XŇne õjÒ¢ÖÛTc€O^o4D®ù#\óóÏÌšŸ¿a´ÂOÎõœªˆ "('ݱ˜ùOËbÝâ÷ûÜ œBÈ:уà|ÏÎá¦[°ÒHÞL>âf²GRNS¹‚ÇÝŸèA?Ÿññ¨&0ó¶¿ý"TîfŽÁòÝÌ·ð ñÌéÓÏ0ç"@)âù&šÍ_SAØ ~î‡Á“à/σn—ÓQºLî©G´ö{r‘›“çóôb giŒGÑÿP£Éìy¬Ñh̓ìÓâú~ß {i5jNDÅ ©AbC Ögº[*W-¸leåÞ+»vß&Iµ3ÿbîfΔÈ{W»æÐp¢„yT&‡V sé4ÖêëÆÞ´}ëu¦hµµð¯™óÕ£4ãk–Tu”~„l\ùq±­½jQeƒz”ºÉàAȧ'ŽèǺ‰tÝ<=³:ƒ£ÞÄ>òHVÛuÕ“ZéºÉ9³Fž‰& ÛÔ÷‰!é(:òV µvyLä¼y£ˆZˆ~F ªãÀp<ŽWÁ)ðÐ º¾‡^Ù”O‘­¡ŽË©(á(I´r@[üB€ùØbR8dtÑO,4’²¿¿³‹9Þuû²S$êÈH"á’õ¹JKnAŠLÁkÿaøåc¿oCÀ:€¥¥äZyšœÇY€ÎoóTšfg¾aZžAìÚ»Êy­ÑQi%k£JeqÑ:*Á$Gp»tɇís\[Ê& ‚[¼”ZLµK©YÖr{f®5NžÛuô-×ÑwŽve')2‰O4µEñëY´½Äô -73 ¡ÍŠÐ6&¾²ÚJ^›•èÚlsnbîzK¹qzƒ?àêZ³Ê-ës ˆÓ»1תL%ÇÓuR üYS•Ö\£½Ü’½x>Â9—\a#Ð×eƒ¹ànp< žÌq»Ò`º‹éÓ8îyŽÜÑ÷y¼{‹Ùî~²Ü÷ò½ÚåW^]Ue;úÕÓEãJW8¯?Z0ªr]РÏU,s¹2WçeÅ£Œ™óeE#ËF-Ø”—í~Ê<ˆŸ[œ1ºaâèH¢ñ¢ú»=cæN/¥×Öçèbb¤ùúØ‘©Z}tâ¨ÚlÓÜkÈxubtiÒ~KŒ7ƒÛÁý-` ù“ÿн: ?}Þ½à“±ˆ›†¾xç©Yí­@Ž,™WÉNÎ/$ªóÙ ûF–²³÷+ò«‰BvF¥pa€â‹ò¼?ÜYP[Q8~ü‘¿UÔf›¬¸²¶@‘vÞ„®>ÿ|ÿ˜'àõ¨ ¨9lS?Âî;» ©•:yݧRjãÆáùÙ{EÆ«RÍ[˜DŒ˜}"a§hvR[Q ;©m‹)÷$:±E)ç.s vqg³ËI~GÄÎ/Ò´ròd¥{ô@×2êæ-i ²Z H ’”âÔ‰B€Þ  ž4¸Ðxü½Ç¤mÌz@8Ÿ—€óe’9þœRC滑LGÁ8 Sœrž:ÞW~¦¨ZÔ#z9Ÿÿúk¸ñë¯%?¢÷ÝÎ7r(> ÷ÍŽ±'+Šž:ãi@?  Ð¿êb¥£xfuL,ˆ…é æƒ|(õÐÀX`2óñ1ÉÌÏ:÷£‹¶H~ð\uT¼J«A@F$)ž„)@´0¥Z¥ÈŒ‡*)KNº1Mvcd¦âÔ™SgŽ*ãJâJÐ×p´¡(N[¢D[¼ÍÉäš“ 2ùæ´D€ADQ±QŠ(eT\”*">J­NˆMŒMŠMŽM‰MM‹M0$$˜D$‘ɱɊder\²*9>Y¬IÖ&'¤'¦'¥'§§¤§¦§¥§'r¤rœ˜çxž$“¦BTÐâÆï8§ÌøYPWç ÄÅ$=2E—µ‘X:Ý8Þ\¦6+Œfö‹iŒ$Ý@PIåERI¼¤½­°'-/‘¢Ï7i®AæÞÎâ€D ‘Hd•º^yáó1«(CWíAX®*£«#( !hŠÒè¢n½ëS'Í]œ4qkÝXеo:ÅZ8ŸŠ@6VÌ´ÁÕ­<ò׿>ÂÞhß¾~7šPÀ hR½7:V4ÀÑmˆX*V‚nD¤Sét+"ŸÊ—DϤEë°®6îfPß@ÙïÏAë!t¿KÐýbÀ”êhŠQrú&¼ È9ô¸ïyÂ{O9wO9ó÷Lå„£ceœxtºŒΗåGäÇÈç@ gW‡…[³ÛùÐVukCC«z«âÐŽ‡$?V”—WÜÔÓ ëj `nᬖýgðAÁn@)Ü÷?ÔnP¯Ö»®†qÌ7¨÷¹Éò³§óêªcEÒ28‰¬" RBËP?5çg®¨ÿ²½ŒD/æ=®§=ìíoì¸8º˜ ]¬Õs1À"BÏ¢ÂÀ¢‚¸­ìËGQvm¨„: +ð휼0ëõW^ð7fŒäÉë ÔVkŒQ{"ßQ$ø‹2>*–,¤°¨ÿÑýö| ‰î·gžMàZð>uNñiA-jM¶íùµÚ{ï93ûÎqËÅ3³Ì-q5ڃ΅]ü9ž¶­â®Æ‹öŒ’.\.[ŽöœFÇ®@{Þ“f«Ý{¨Oûíù@:Ù»Ç&Ûä³]ØÁݧ’Jâö|„ö\â³§íAõÙÃiíiD{¢}÷ ÍÞÍíéàöô*ÌÄãÔD9ÚÃáiu—ìv´“h×úÛà6@€°©‡Û˜ËÙ—»Ä i<Â®í°”ˆŠh…ó̱ ɧLxH9yÖŒÿÒóG‹g‚GÁ¤oÑùg>)¨Ž¸$¦5æò˜c(88^SÕì,Õ†6ÙšWÞ’Æ÷>ÑÓòõ¿><b–ÏR­ ø…”¸ë(HCowQ­Ç@ZTF=©‡3î{ô>æk¸ÿm¦…ü¹¼|Uï^@0ï2cˆÝè`ˆÜ£@•”ªô¤”ÊÖ5ÚæT^j8òèKÆ-pÿë¹Ì/¯|£­l›cbžW”L{˜<Õ{öÉØÞGOsUËs¨Is9Û‘ž8)€ )Û4´ý 3‘ˆó6•&N’sYŵ»$¯sëÂ#õ±Bôèµ–Œ{é%ç7Ç©GXè¼9A¹@x?’p–Õ¶Q­·I"œûÏž={þ¼óCò½óÉR-ºô½ ã/.í¦Çr®ƒ6˜ ž—´{UyùÉr{vú9 xŠi¦W¢–È•™’CŽt»ëC¦ .#ÒáðF&‰9O'@ P ·‘yifÍÍ4Kk©Vÿ ¨¡z—¹¹ŸÙÛà4Ø1Õg‰É®©V×!bªó:túÿEóå% N+x@í!xÚTЈ @ѶmÛ¶mÛ¶mÛ¶m†³mÛ¶ÝŸÛ©Çjš ÁêAðN¢=„*¬Î:¡j3„ ¦‚j§Y: aËh„áËéDh j,Dj‘c©¾¶è'Dɯ¡5D› ÑcÄL å+”A쨦“'¿¾@Ü /·ž@ü^ú ö@ÂUè $N¥E¤<$ýÉ3é3¤¡r~Ê‚¢5*&¤N­¦ª^CÚ.µ¬KßW§!C1ÈJ…!S-…Ìét ²4×-È–U!{7Èáïre†ÜÏ oÈC× 0YS ,Òb(Ø E„¡Ha¹kѵP,-¯ %Ú딌­Pjº>Aéκ e&@ÙÚZåªk³î@ùêÚ Êé°Ü­bp%×}}‡J‘•Yueß*ήÚGS´FtC¿¡ZT3̈́공YÇu_ö«‘TujUTK ÔB]×?¨}ê\ѨB THµ4ê‡S2åS5µÓDmÑE}ƒi¡áÑ}F1”KuäÞMÂ)½Š«Æi4-¬Úšªmrf•RUS5R µSgõT¡yLPh^EÕ]ãµAGtGîÔ2‚²ª¬šª¯¦j­ê¦>B«°Ê¤Nú­çj·îB›ÐJ mÊ«£†k®v²f—VQÀ°çœ}.Ý*¥„tI‰Ò J‰t‰tçE•–n”¸tw *(bw·~Ïÿ¯Þúñ÷ì=3kÍš5GÁOç¤*ÏbÄÕ{Ë0FŸ@ÊÑ•‘$‡Ð·4ïcïúe (5iK?ä f¶ã¬=Sˆ6Œa!›C0 „gg°’=\æórQ†zt`3XÎ.ó9ÿ†0¨8±>„Áñ SØÏ·! )@%š2šØÍ%> ahjî£Ïb¶rŠø-„aÑ£† aDòp0„‘¹˜¨¬¨ÏÑ“ãsõðù˜ž!<_gc¬ýÃóã}>A¿˜˜û?)+ÏãÜL.Æ$ô¦)ê{êýÔD OËH1ÔÖt9œ‘±Ì¬ŠÞ:«ñîìrìaÎxþ annÊ¢_ÍkŠ}NÎAGÔìüLŒCm½ ß â D-,lÊrRç‹y)ÁÃ<Á@¦³‚½\á‹Å(D5:1‘UC?~) wQ…æôây^d 'y‹¯CXœ hL7ìá’i,g—QKy(GCº0Œ¥ìä£ß¿œœçÎë+9¸Ÿzt`3XÉ>®"¯ÆÉGÓQÌgG¹‰{aY: SÖôaKØÁYn£o.ÏFIÔñŠº´g0>_é®Z¥VWÇiÎvp–ÛxM6JR‡'Ä V²«Xÿk-ñüÚçÑÖÉãúk㸋6ÖA<›*2ýc³Ÿm©Ê—x«;{[u´½Ö³Ãç;«"×»òâþ|½Öø†qv×@ÏÝc]{뢇íSÓ)ù9†:Ûo-ò±5pÐy<Ô…‹!Ö_ŽÄp¯MâŽe¢33ù(„ãé…½>ÑûpR8Uy<çôL9V³ gålc6âìŸ[ˆµœÏÊ$üì‚^z1 ƒq&.ÇŸ/—âZW&£6¯Êß›…yŠ…øûµ¨‹ëO⬼•—‘¸SÞ„šx';/¡çßHO=z¢¶o>†Þô®û÷½âØ[öíýâXÛÎõ‡hÏ!ÔøGýûí&¤à¹b Öy§ 3¹À-ôšO’ÈIžaróéŒc ç1ÿgÍØÀ ®c®Ï³r?Ò,c/ç0Ï1ŠÑwö—I4æF_=Âר‚5~“‡n¨o3SŸeØ›ïî¢=¯qµò}‚ÜËBÖ²—sÜÂÝôCŒì4§ ø€Bø1â. ϳãÿ§õx”§™Á΄ðszjÒ‘±¬ã(æüå.ªÑ„ùä2·Ñ{~-È`¶ðöï·RÔ¢]Ìd–±—·°¿áúsgó.Œb¯²ÃÜÄ~þY˜F<˼þÊÌ\Öâžÿ;)@MeG÷ªÑ„ŽŒ@<ÿFä¤ÍèÈ<Žp çü¿´äåAšÓ‹ç™Ã«üb!…©@Sz1Ž—ØÁYnãÙXœl|È÷Á_ÉÂnñMˆEû8ËÍK4§Ïð|ˆ%å£$ÕC,Ufî&%ÄRÏe-‡xã¤I¢ •C,m®K—±ô™ù6Ä2œ ±Œ½ð\¦5!–9bCˆe¤ËZßB,[j–†Xöê\ ±½1~Î~!–˸¹„XžŒ\àþ ±¼Ù)E]Œ•/-³þ Å\²ózˆ,Äþ ±»z3k¼»)£ø.Äîib…R1k.Ü!VdEˆ̓Ÿ+ű»· …Ø}UÈß!V¼'C™ÌÂ+ñ,ã˜Çrä¿d.&òâ/•‘bÔb+§Bìþ® d<ÉXCéôôç4ò{ˆ•ÉBZs5P6/±.ÄÊ-±òí9‹Ï* ã"ïó-òQ±­éÆ ~±J›Øb•³Ò—ã!V¥ã0vÕÜØŸò²ž½!VÍ>TÏKsÎó^ˆÕ°ÎšÏóuˆÕJ ¾Ó1›;˜«ö4äq^ºšË2~ ±‡›ñZˆÕÉÂc ࣫[õPo2¯`Ÿëç¡¢ö dá¦â½Ð’§Ç®kØšn±F0^ãb<:j’šî¨é¦åÙbÍ|Þ<ðYˆµhÄFR8b-3ÓŠÞØÃVõïÖæj³”Þå‡k›–îÈq»,¬ÃgF”Â-±Ç;2”Ùˆí‰6ˆùÉÚŒå òÕ¾/¡†;´a—ù4Ä:–a(GC¬SFž`-ö§s9že+_„X—Â,Ä»êݶq¹ížƒú´c j¶Ggj»g†cî^å˜Í{˜ë©qübO‹«w!ÊÓˆ®¨ã>÷3ïö-HÖòyˆõ«ËRü¹Yj3dÎñ1?…Ø3ù©IS:0„Y¬f?×økŽÂT§5}XÇYnâ¹gãä£ éÂ汎üƒý˜†*tcó1Æ ‡IF= ®G2êxHœ›¡]ø#Ć9»ÃÓ3‘_BlDW"?#Ë2çxTêКåèe£[°%Ğ˾ ±1˜ÏJv žÏK?¼3¶ög\]†³{=¾$jgBCG OÌAÞD™TÆLb«Ù…šžœ®Ø“)o 7SK2{5íQz±=mzOnò%öeF:ú0ÅF]Ì,M-†b/gU!™CÈíìNÌæ5Ü's2r?Ý™øçæâJˆÍËÅÖâÜ$߇?ϯȴ{¡j|AAæ`?Êÿ‹9YÉIÜ‹Fb/_ªÅJäsq†â.ÉÎôŒ¥ØŒóõòHôÃWÑžþŒ%™U8ç¯æ¥µ8ˆ>¾, ‹¹bË«ãì­(ÏW6'ñ­jÇÜ?«³Q›Q8çkòÑ’gq·¾6 ë[Û’18gë¬}úâ¶5E©I[ú1‰—ÙÅYn£F¶e¡ õèÀf±šý¼‡ç¶g£$ux’AÌ`%û¸ŠºÛ'hL7F1Ÿ å&?†ØÎt¦:­éà V²«sWœ|T 1ÝÅ|6p”›óõt¦:­éÖ°ƒ³ÜFLod£$ux’AÌ`%û¸ŠùwòPކtaóØÆEî`O÷äà~êÑ!Ìb5û¸Š1÷Ši¯˜öŠi¯˜öŠi¯˜ö.ažÛ+ž}âÙ'ž}âÙ'ž}âÙ'ž}žÛ'ž}âÙ'ž}âIOŠxRÄ“"žñ¤ˆ'E<)û¸Š¹÷‹g¿xö‹g¿xöwbsxƒ¼…^p &êðà4¤ #˜Ç:òî¦C òQ†taóxƒ\ó‡ãä£ éÂ汎üƒžw$ ÅyˆGy†I¼ÌÎr98š’ÔáI1ƒ•ìã*rp,N>*ИnŒb>8ÊMìÁñt¦:­éÖ°ƒ³ÜÆü'²Q’:<É f°’}\Åü'ãä¡ éÂ汎ë!v*7e©OG†2›5àMÜO§#òS‘&tg4 ÙÈÞF>‘—Š4£'cXÈfNp }úlFŠQ‹vôg*+ØËôÁs1òS™fôd4/°žC¼»ò|ZŠPƒ6ôe"¯°›K|Æ!v!7ei@gF’ÌzŽpwÀÅ´¢­èÍx³“s|Œ})'¥©OG†2›5à:Öy95wS•ôf"KÙÈ®ð¾s\ÉJ!ªÑˆ® d:‹ÙÊ ÞÅš¯¦¦ iDWF’ÌzŽpϾ™–»©Lº2œÙ¬b/W°–k1òRžFte$ɬç70îõ´¢­èÍx³3|„øÞÊJ jÓŽ¾Ld1Û9ÃG÷í´ä¥­èÊH沆ÞÄz߉‘•T£}™ÈRvrŽqÞHOjІ¾Œf!›9Á-ÔïÍŒ¤2Íèɲ™\Ç~¿›šÜ܇ß}ŒLe»9Åxþ½ÔäæÁÿú2šõ8'·’¨@cº±1½ŸZè%¤¦/+ñ=òÃÉø¾ñQÄ]4b{þÿ}œ9Äîä¥*-À,Vsã~Rš!¼†Þüi2†]øîñY ?]Hæ(7ÑC?Ï@m†° ÿýúE*ªÑ‚›¸?¿|‚¾BÝ}Uˆ>œ@Þ¿~€µ˜ï›Âÿ ƳgàÛ†\ÁÿKøÎ9ú¾­éÇ|ÔÝ©éÌ@&²µìå·°®cd§UiD{ú3–dVñ:§¸ÁWèI?e¦©Ïcøžþ³ÿ>ü%7a~}wÍo¾—ýn¼?ê°yþSßü+/îÆ¿ÝÛÿè;ÿÁþþW”ã!Z2)Äcqžæ _†x<-åy‚œäxbYˆ'å¤tˆ§ªÍíO=‹—Ù‚gÓF!ž.?}ð÷ô÷s9Ä3ô ñŒ`œLmX♋ò^ˆg™âYc, ñl¯…xöU!žÃúrÎ ñ\yy=Äsgçjˆç)F/Þãk¬Í¿ÃÇóà~j†xþÖ!^  Æ)˜OCü.Ïß&ÄïiÁò/Ô€Ã!^¸.Ëù<Ä‹´åVˆÏ·!^ì.ºbþ{ R‹ÎÌ ñûJp=Ä‹bwˆ—ÈÃXKɈ"4¡/3ÙÈ9¾ ñRé)Eú2“œãc~ ñûÓSŠº´¡/39Æ»§tD*R—6tg03ÙÈ1ÞÄóeÒS‘&´g0/°ŠóeG„x¹ßB¼B1*SŸUø¼âÀ¯t—B¼ò˜¯bϪîÇ^,ã­¯“ºŒä¯;{PïÔxý§™ÅvÞB­Ä{¦crÚ«;³pÖžŠhÅVNñYˆ?ý]ˆ÷éâýÔJgïg|ÀXVp kx65wóÓYâÓÒçjP ¦r'Ä7bGˆ)ƒÚZ û3L]—›…B|dNf"£äytfî£O1•õèÏfÖ1溰uû¼ÚÛqŒëÇrÔâøZ,A­M¨Â¬abM&¡Nz¿OÎCÔ×”Êl ñ©%y'ħMCçyFÚ2ghfy†ø¬&ì ñÙÅè‹1æäc2Æ™[ýw^fbíÉ]üÍOC:³€}ˆ÷ï,x<ÄC,/ªíEâzIX,Þ%úèÒ´¬ ñ—Û0¹%=å˜ÈZÞñWÛ11/K¢!+Q·Ë{ñCˆ¯xŽ ¨ã•Yi„¿jÎÇêÒ´f:GðîšæŒfúökµXÍ5ÔÃÚÀù9wmÙÈGüâç+2ƒÃˆïB1Z"ŸSóîöK¾O\vÏ_u6®‰ë-õñŽÞrÓZßs>ÐÛ>Ò³î¤æjð(ƒX±?ÉN &£}š†¶ e{¸É_!þYA¤ ãXÁQäèóˆ"Ô¥3ØŽ³õEÊÒilã-Äúe Ú2šu\C^¾*LsF±’Ó¸¾¾‡†ôãE£~¿IOº2=8Gß ãÙµ~—ŸÖLã zÄ÷Åxœ™B¯ø!#UèÆt¶á{Ë©¨FfsßM~ÊI#±”‹¸c~.Ç@Và»É/Ùx„!ìÂúÍAk!ß¿U¤;ó9„ýþ=ÅéÅX–°‹K˜ÿÔ¡6Ý™Æ&®r÷ÿŸù¨LKú2…ìçmä⯌§.ýü[ÿø73—¹ýø/3…¨H;ÌŽ£Æþû'D!7óC‹è¢ø«!ŠŠ²>DI½™ÈfNóSˆRe¤*m™ÌrNóqˆR§§(ÍèÅ\Öq…¯B”&'ey‚Á¼Ì|À!J[„º eoˆÒ5äû¥o¢ Mù7DwóCˆ2=ÂËü¢Ì-y=DY’(I+¬?ë]<Ë1¾ Q¶ü4`{ø;DÙsR†útÄZr¤¡<1ÏæŒQ’VìÇ»¹j2š”宆XòTd×ñLÞ"4ã)’ÙÈÕå;¢ü÷óQˆŠŠõ¾I§xzs.D%:„¨ä½ˆ·T[~ Që*÷YˆÊ`Aˆ*¤Âº+î QåGYÊ¥U±/U{ñWˆ˜¢jmBT=/ƲÕÎÉœ=´=Duì[ýÊl Ñ#•‘»é± †ÈwÔ¨ñÔ5)ÌÎ5íÅ^ã$Ÿ†¨Y*îå|Þ\Œ-6…¨¥çZUQë&Ü Q›þ!j[ u×.F_äâÑÈÛcOr8D—æxˆž(Š5?¹(Dí»ðfˆ: æDˆ:v Q§ÒlQg5ÖU¾»y®»5ö8¢žÞ{Êþ?ý4Ÿ‡¨·:î3,D}Ó0*Dýò£æûÇ8¢gòÑ(Dþ}1z¶%sQ·›²5:¨>7B4Ø”«Û|¬¾>QsŸÑ—êä{qý°8D?}¢Ÿ}ö‹ºýÕ»¿©µ?üþçÊýÑ?rùoJˆþ+Ê~‰Ðžý¼ÍO!ËL¦ðUHÄ ‰¨5‡DâRH$5ælH¤ú"$RŸ‰4UYÍ­H›Ÿºôcø$$Òe¢=9é«q'$2˜7Sq<“%{Hdý<$²u ‰;C"×´È“_B"_ÞÈo ‰‚]Bâ®—Câî¡!qñ ýEJózHÅzóOHÜ{($î[Å ‡D‰Lü%/âùR%iÇx¶‡ÄýmÍ*ä¤tð*ø&$Ê<Ä|R°Î²¥ı(—•¬ÇøåÛp"$*ôâ`HTLàóJ1…DåZ!Q¥Ö\õè–‹M!Q=#ýY|ÖÈBU:3œ¹È[ÍÊØZæ~Ðøµåû!¹ø†±+$êd¦1“±ouó11ÀBÖr„7_½Ô” í@2¹Àw!Q?/¥¨OgƳ”|¤¥õyŠdv"–©©Fz2–¤p‹ÿB¢a êÒ“©lçMìg£ûx‚Á,d7…DãˆÒ<Æt6rã5)O{fsˆ?B¢izŠQ›®Œæ¡¦›E£y|ÍóR‘v gÇ¿¹iD_rˆB¢e~š1–½¼‰šn•›‡éÎlvcþÖé©Lg’IÁømîæ F³{Ú6F):2—38ÃíŠñÉCþ­ÊHf²†C|Ä_!ñXAjÓ›‰¬áÖÿxvjñ‹ÙÉuœ§' Q—þ,â zÆ“9©Í@–rûѾmÊ"ŽðqHtÈHUz3“×QO³Rƒþ,ãÖÛ©-ÏFÔOçÌ4b0kÐϺd¥.cØýïšÊ<ÆxVqñt+A†²Œ+˜¯{i:³˜ô«™y˜¾¼‚ýè™–ŠôdÞïQ‹áìåÝx*‰²te&p¾ž.A&³ùï]„Ž,Bü}"*ó+PO}3Ҁ餠¾ú•e Q?ýË2õðL&ÚaÌrþl ô«-YŠÜ ÊBCžg-ï…Äଔg8zÿ9!1´GH kÆP~ ‰á“BbDò9RŒê£Ë£¯ŽI5?¯oŽÍ€}×}y| šÐ‰ìG-O¨Æ¬{bjš¡OòÞ䲌átHLñûÔrÌÅ9š6ë™n=3²Ò÷ÖÌRLç(övÖÃLA½ÏNO[ä`N.ÜasK NçÕa—C"¹óq̯Á¶xAM.H…>±pKH¼8{±(N#æ"/`‹8„çg¥=˜ÍQ~‰%%éÂBN£–æ§}YÌ)~ ‰—‹puôJqú1ŸìÅ«ùh˫܉eµçrõ±¢* qnVf¢%ó9…{fU{¶ ¯«bŒ5©éË5äñµB$ck;1›e¨«u}ÙëKSŸžLá5̱¡uhM7ÔÇÆ¬lâ8joSKIJ9‹ç–:ŒãpHl¨K{ógHlëÏl6‡ÄŽÊ¨ãZ á×ùgfWêÒ‡d^ÇܯG”¤Cy7¸ü¼‘Ÿ æÎàœï¾‡NÌÆß“Šþ˜kï<–‡Ä¾Z<Åp¼“"ÏûKòVH¨ˆ³q0‹pŽÃþO2+ÐsT¢7ÉœDMÇÒ°:$Ž—c:ÎÔ k8™†xîT[†Äé‚LcÎÛ™üôã9VñeHœÍGI^äjHœ+…üŸ‰>z¡îî‹I,À>]Òã.—f,rs¥Ïð2b¹Z˜Á*N`OÞL¢õyŒÞŒf6[9›|‚¿–—RÔ¢]™ÈBÖ²—s¸s®WÆüoeÆù}; C0Î;ƒQ—7Š¢>nVç¥x7-ý‘›÷"¾[9ïÂÞ|a8+¶Àþ}Tûr;N[¬ûã²Ø»;i釞ûISÔÔ§úÀg¥™€óy{v…Äyƒ»ñËz¸#¾*Μï«¢Ö¿©„~ñíCÈåwµPßwÄû= íþÈý…ÐÿlÀÔÂ_™À;!ñw"Ö*ãþ·ëù4$þkÆ«Ü IafHŠed\HŠGŒå£­ I‰Rô %$%ꇤT•Ù’R×ãtHJÓŒ¥!)mN…¤t-™Åç!)}ur;ü’2$ÈAQ*Q¶ô`“XÀjvqœkÜÁûä (•¨G[z0„I,`5»8Î5îàýL rP”JÔ£-=Â$°š]çwð~æ9(J%êÑ– a XÍ.Žs;x?K‚¥õhK†0‰¬fǹƼŸ5AŠR‰z´¥C˜ÄV³‹ã\ãÞÏ– E©D=ÚÒƒ!Lb«ñ~öÆL I9Ò±‚ÿ«zŠ+ ‚(z‚êØ¶mÛ¶mÛ¶mÛ¶mÛ¶m;3ý±ëÖ­5ŠŽËbdtÇÜ5÷Ì}C1墹d.›+檹fn‚óúXÝ_ûŠë3â­A ê¹ë(á$”h#k”í%ʱ嚇òÜDùK¡½QÁ·¨ð*Tt*þ•ªƒÊ†G冡 …P¥ü¨Š×UóŒjnDu– zMPƒ–¨ÑzÔ4 jþµúƒÚ¥rUÜhÔ^n?ê°uü„:·E]ã¹é¨[%Ô½<êáu½<÷ÞQßͨ_Ô¿9àyŠƒ_EC/ ¸_hTV4zÛ÷º‰^7%2šZMÛ†ftB³|ÏÏb~D´ð0Zœ-ùŽ–=G+¡Õsкhc)´ÙsÚm¯ŠvÞD{üßþ‘èÏ:ÉAG–¡£ бºèxqtÂó9ièT.tº :“Ç}@g£s^{¾>ºÕ½F¢K³ÐåèÊDt5ºÖ]ïŠn4D7#º'èÖtÛ¿Ý â<Ó»}н0î$ºï™?HfEb¹»è±çÿ$œó¾§eÜQôÌ÷<^,A/½ÿÕ@ôÚ{ßtGoýÿ»Oèý_ôÁÏôqú´}‹¾¤s-Ýiô5‰î>¢oUÝ:ôÝsøýþ‡þ%E…0\ô&l;|:¦Ù˜µÅB,ÃBnÂBÂB7ÇÂtÂÂVÄÂuÇ—po±‹°ˆYÝW,Òs,ò|,Ju,jY,ZB,z,FHw‹¹‹5‹]‹S‹Û‹× ‹¿K° K8KÔKì½IJaÉŠaÉ3`) c)£`©`©}fš .Kw Ëà³3Ã2-Á2wŲÄq°ì±œ‡±<±|Ù°~žB«±"¿°bÛ±^[²¤ëæü½T¬t7Î}ÇÊ4wï°²™Ý·ÎÝÃÊ5sSœï/Ëeqå\ 7ÈÍqÛÜ÷ «ÌÅs9\%ׯ s Ü.wÅùžŠ!\"7ØÍtǰJÑ\:WÌusãÝBç³+Gtô6jð^¸‡øà£_<õÓÂ,ÓÂ,÷rü®é exÚí›tUÅÂý÷½gN¹—¢;ÅO‹O:‚ìJ±ðbˆ¡¾ˆŠô{¡ý-|Oìòâ0vÌb‰½÷Þ{{ö^€ûí}ν Ä×[Öú­=½ÏÜ™Éó0Ƥ©6¦wz7àLwý½ïq|z*ît–a%©6ÐþÏ;YŒwÁDêãé†MÞ%‹É餂,$'ÇöÉX§ Þ&§* ‹©Æü˜çýY¯êÜW1Æ«¦ÎŒðÐ^‹ºô÷"?ßëBw†ó¿¦_-aùÝ—#õ:ÐïNœëNgZ'ÓiO¢¯WŒöîƒù݇Q¡º¨ÌÔ3™ÿ£îŽA™» 9³ZÊòG™s<ãÉ\‹\zµÈ×¹‡Dæ 5rwOŽâ)œó9ãßÉz>‰ô[äö@¿û¹Іæff™ÒJ!ýaÊHUÛölŸåa;m‹¨ÍÆMÉÝR-Ì,¶g1*ŽÚ^nò#µ¡Û©OÊ RãÝ7±œéœªþ¡Ý£ûñŒ?ñ×û¡2æ(µ½ÚhCËõ…í ûa"uH?æ½½í‡_pC Ô]Ô…„}ñ.u rj÷ ŒFYØãûàé¸/Þ¦¶SûÛ~H¢v¡R_¢¾PŸIUWå—TÕù׫£êsÕ_ª¶Qù6¥Ï _¯Ö2\mþ#ÿ¶Õ!XÊ6nËz†mMÝ‘º3u3j5gÕý’ñN=?_ãTóDc5¤YÆê©óLd7Ó¨íòÕéoó/Fy#—TÿeíQeV¿ªm“š)Ř` 휃š±.°vÍKÍz•sVó&©/곆ªæ»æœÆ˜ú×Î{ͽ¤Æó{ ïÄü4­=Ôƒ©]Õ϶¯ÕÇ~À0MqvØ×§Dýé=ˆ£Âõmf~¾s|þÌp­*ƒT çœÀz·BÖöCú dM]þ­^m~¾mKoƱýFÄóê—Ÿ¦rÄkY7¶MåeÆæoÛåj´¶íã­ÀmLç¦ÓÃ;å•ÿÐëf¶~^GœG†¸m1€öéZŸ´Ÿ”Æ»7Ó/G–æOF]£¨ËŽFE°%êüg£q•É¡.3 uÁ$äìaxãÉØ1ÐÐ>ÒÏ?å—ÿ†e\¤ö&9ûû)Ü:†-~G…ŸÆ±\k&˜ Lðæ`‚û&„é*Œâ» ·º¸[¶É 3Œsi.j¤¦ŠngiíÇrï¶ð7xq ‹™Vý¯&l_m9Gy#p¼„œp»0­ÏÓ»‹nÄm‡±Bmá’¯ã<~ß<‹œoÂgÞo‹Ý ÔØßôhmÁlw>Ë^EÚb(™®yUHØ?›#§úºÇ±LóTþõÕî‹Ìã äT6³*27“¾¨ðæÚ·óžˆAw´÷ÞaؾŒ³ ùûßÑ›?À;(€³DjMþ©ô\L°8û£e¶›§Çb‘)Æ"'‹'É©d}úc|r: ƒT£ÔÆÌÜÿ½H½[û êpk—YnéZ 6=º5'm!˜˜^„‰ ·’ö™Ô7©¥Ôj²&ý@}Ë9mSkXÞÆØËˆ­T–8ý$7HÝþ¨SZ¤X[¬;‚:‡\Fî$ «è~ õ~j#êëän…£ý ꤭ÂÅaŸ"¯EäϦû5•ù¬ïF.¡ùpº}@Þ¢9+% ÉÒ‚ü¶&£‰GJ•_\®#•¿°ù&ÊL~HQw¾|u ãߢ0Ô²8oh~…ЏœÇP«ìª#õ&ê=Ô6qØÍ©Õ ¡ÛŽ@×ÿÿEæ¼Új29=†nb]‹Ø|”{>ÛçÞ.¨óV¢NcK„ë٥ȱ†kÀrÊ´ß×âüÿÔáê?oºi_êÝ•4Ø"ÿ¢9-ÿ´ß6ÿ‚ÿlþ~¿}þN§5:ýx v-Òºh÷™éï©]"?{p›â~ù…æ— ãhÍÕ:%w¼‡0•挿ܹNü^k’Öæ»Ð=WÈ-\˦¢ ÍCåÏy4Ôî1n‘»‚æÍÑWûl…“¿ü¸>–Ú5ÐÛs½Š•¦×ŸëDtõÊ1Çû2L£}œW™TnªÛ “Üýq´û-~ã¿Tªõ‹þÝ/0Ñ®õv_¢::_p/±'¸ûâj¿S\ƒ]‚Õ¬ïõ¨ò–E¿…þ8Ô1þoÜWp˜ÛýÁçG¥gBÿkØN½Ì-衦ßI˜œÌyóÓuPä÷¡ÛfaøÓý{in‡=”WÈM(RþJK¿KÚ»“vêÿc”ûKþ5©*òRþê÷p,Ýʲ¾‰¢@my3r,Çæ ]²¯ .øö§=|æMÚYw&rœ\Œ-‚±…õ§fݳi5Ó#ƒ¡( .b>ÑM·0ß‚qÙ=€~ ¬­ÖmñÝb§ì›)NÙ·¿Œåšîµ;›· ûmU|éH=½MǾÆS9rêõÆ€úAsEugû[íLm´Á®ÞelÛ¥LÛã êzä²YÏvl‡Ë9ÿ_Æd[Nõ•Ý;YÕ˜Q¿ï ¤hì¨ÿ UëAÐûÁ㵪qnË–1ËÕG¶î6­àO,«ú¸¾=±Ý‹é¼Q¨ÉvÑžXû»[­wÏȱ¯ñ§¹¢z'Õ–Ñö‹æŒÆ­íÛNVÙŸg›—QžÙgûÿƒòàKÜïWàJoîwÏÄ•™OÐ=ØÅÚ›-X®›YŽœ_§"ëC ³”Ö[Ímͯìþœ‰ÉÁ\æû ó¿³o•_ëš=ëÙqmÇðU˜lûÛ¶µölÞ5Q[šWPöýÑÈ‘"S…:å©öw¾D¹{ˆöe< ŸÏ-¶S~?XZèœ+wr¸™Â¾h‡bŽ¥-çm,÷ÏÇ|÷ho÷‹á¹ó7˜¦=ŸÜ4×Û#ZKœw౟á<‹¦f8“#L;”˜˜‘zMäoqÞ¥{wÌpúõˆÙa¶Ä`ç:ú½N&Á3ÛbóßÉù#Ƈ<ñn­4Á¶14§ÖPßa¸ÁÔ%ä]"³T(|óØïú(œIÓ|)Æ9°•3=Ïp¨S‚í•V¸]’Cµa4çU~ííÝï‘5-‘ ólóËf˜Ö}qþKh¾‡”ÄJœ©ÔKÉUäk–ë ¸l'““èA”–)¥Ž$ÅQ=L õK²+Ëti\Îè©wjr$Æk÷ü9?¼-ì<µwIMÞmJíÝÔÖþXMÜ%Õ$t/{¶nèÝÒ_|ÇÄþÖܵjןMir´¿3õÝK©­T.{ÎÔSólú¢Ô/Fö¦vüý­ÔŽ·?W9ÎæqÜC Ç“íç_«öŽlSšì/{?¶)µýQŸúW0½°…ÎãÇ»ó!0Ü'ùþñpü`˜Oའ_¿3Âû|ïpÁCŒw7ü œàƒáŸÍ<.CMÁdŸ÷sNf)Ó˜À´N¯³JDþ>r:Í%Ô/ÉZòµó Ã|À²õ`÷ƒÏ4÷=ó â«í7„¿ó‰ s7Læ–wË» Fg™áOfœcYÎ YÆJøÚ3n oæs ã|Ëú9ÌçRæó,ãöB@³oÛݶ£mÖíçlymþ6Ý¿´ÿÒ~ùËë½é²ûN~½ÎÚ2ëþ…,ŒÕbƒåžÈ8:—Wç×ël®u›a÷&§*ÛÔ#ؾ7Q;‘@ãIÿ“ ýÌ·ùõ:Ï3ìÁd˜â$Ç€½'´vÍ1tȯ×€w#Ý-Ù;¦ µO0‡ayg\KåÎôº‹Šë‡€XM_E¨©RÀ]8Yš/ð6ø§ÿ­ †`€ A˜¶1Ô‚;´s¥n‹íבsb3I¿ióÖûŒUŒ¬• ¦ÛúÊ,µvÒ”æEl¶áËuK¶Ÿ ׃öîÔý¤õ#'b»âެ§½¬¹‡ÍCcIïeL9v³÷¥œ—#ÝsÁ9w¾ð€ç ÍœG÷yppWGïpÓªŒÞडýs7ꢵØ-Å€˜ý]ݑֆgz— š?2H-3ÀhòŽÔ;9Õ­ó:ZûÐ:[Öæ´òàì)èDshÌÿ8‘.DZRåQs¥°Bú+yNšàYò4÷S÷‘»åöòBȪˆ`$ÎXióß%ØTøDaŸ$ÓAóm àòBüƒ°¢s˜yãÄy¿/-$Ž4‹¡Lëïd<ñ#6ìj æ÷ÂMã4WjMœN‚8ÞI¨4¯–ÔD¤NÝÒhÎ2ÿ5 à ÐÎ’ÙçÎÓ¤9á^¬Ï›ÇQB¦p­Ÿm×$‹ó^ª•Ý«YõªÒyU¨´ª²˜Áðüq¸Vfçv¬$s­š×S¯sÞî’é‰ËD£ƒb= U4§¼µ¸@gBíÁíYÈ{͆~í pÊÏI“Öçî!=Õ–I­ÙªÊ¢5ÏÿV)_¯=<®¹«òaýïAUC0ç?ŒHogOÝÇöë#ê·S­™Øø–_ퟄaþ¹I´ݨU²KEaø†»7ÅM˜¯¶X7r” §rÊnË[¨^ûü‡Ζ{°DnîdëW_kÖømî¸k7GFhÜbŒ¨ÇþkÃY~À$çËHÉé¦0=Ëw+Ë{çO8Íà’ÌË¢óí†Ú¡0Lð%&%ý“åøYºÕô'îKrãºyME­$5d™#Ìù´¹·À‰ÂÁóYsœ˜ùgeSX@¿[ÈÍÂôÇ%q:‘\ÌÊ£À^k^ŹÔëb=1v¯!•n9ù5ñöF©ÝE­Ô]!³Ò£6Œ«‚ÃPaÝÒ/KÍûXÁv¡}±¿sêPr;ÍßR ùæaqýäþ.ÝZSÇÇeØ›ü1ö[I¿~ÔûÉ4ÿŽÜD&7à>@îdOºßJ-£¾EíO¥¿µã9çäÜÈÑ­Šö:ÚoLó 伊kݵÈym1;nÃË6ŽmgKê·Þ—¸`C4¼}UN±äš›˜!aŸK¨©Ô;¨q#ðQL¥»ý’ÁŠàfþîa?ôÒ>(ÕDïÇ©§P3Ò˜uäHº !Yïv‚ñ¹>8çÖ0¯ÓêaR‚ ~»藘77Œæ‰§ïø>F1i|H¨ÞLjÆFßfá7 ø ÞãÙeklîñ̾Ã;—½­s–c¾ÓŠß—Ò½êx6¿+oZ .DÖ[È8EßËÐ×›‚]Ýù–aqø¼ÃÍêHõÊ4A'÷‘ð]—Þ™ê{ªËÝÿENfæ?A“ ³Kô±iŠô«4KÑÚïÃuj6ŠÂïˆEŦ¾—6Í1û¯ñ½ôßë»è¿Æ72oì»÷\àïñ}‡þ·§ï&ô-‡¾©Ð7ñ}{Çø[ù+6ñ­ü„†~SŸ}÷ý;àÍù÷ èôw¤ÏKÿað®r£”áâL¤¹o¦ä'÷ÌPŒ&øƒôNýelgÍŸN†Go†)%}ë¡X7>þFOZMjɼ˜•tŸI]DÙ½ZŽ ’Ê-u6 õZØ÷3Ô4©Ño÷î×ê»É`uúûÅÿ64,xÚc`d``Ïý'ËÀÀ/ñ½èO8ßK  䄈•¼3™3™×f àÿRýÿ!PfEd ÿÿþšmã`Ÿß×xÚ<З$;€áL§ï]ÛÈ|©IO¥+Õ§wþÁÚ¶mÛ¶m;YÛ¶mÛÆXÙ ã7ÇBÈk66Ûgvv“ÏL…¤6No¯÷B!Â|fAò|öüðüõÄàÚ¸>n‚›ávx0§áxíᤠa$Hš“ndNF’‰ä$¹š…€@E¨ ¡)4‡–0öÁ9¸ Oá'DB2í@ûÒYtÝOÐãô6}HŸÑ×V6«ªUݪeM´¦²|¬³˜ŸØ@6Œd“Ù¶ƒÝdoÙ{öÉÇ|]íö!û¬ý†{y6Îyu^“×ãx+¾€ïä'øþŠõk'‹“Ã)ëLpî;_á%E)Ñ]¬GD¼[Üõ¹ ƒ£ƒEƒ eeYSÖ•e/9S.”KUaÊUeUuU_5VÕx5KÍU[”þY,ÒŽ,}>áÿd¦“´6†­ñ|KµB¸.nl¬Zà>xž`¬fáõ‘ÂÄ&¤%œnµœœ E€ (P¨uÒ­ZÃ8à<‡?MíFÐ9t =d¬nÑô©±BVŽT«:ÖTk+ÊJ2sYÿL«åìFºU˜¯M8²WÙ§ŒUÿŸç䎱ªm¬šð¶| ßÍÏ«ü—ó¿“Ý)ãŒwn«xDNcÕMŒ;Dœ‹\pëÚ†¦Z…« ²†¬#Èvrš\ —¨¬ª„ Sª’ª¥©jŒšªæ¨5*ö'ŠD‘~c…’©ŽÓZ¿Ö§ô }DÔ;õ½UoÑ›õ ½L/Ò õ=ÿAp I…tÝ}ûº/îî®wîà‰8=“pw)xÃÝaþy÷Ýè ?Ç(F0Œv´¢Mh@ ‹hD"¡A0‚øÁÞð‚ÜáGØÃñ?ñoñ Ïä\ÎäTD´Xr(Û²)ë²*R+ÕR)eR 1â)â¦/õ”þ«ÿè¯ú!kf¶ØÆ&Ÿð!ðoó¯ð(p·q W°“‚ZWójZ©U¤’U¼z«^¨'Ö®mɶhº™¦ùÃxk¼4žOéš,:¤}*&ƒò)›Ò)•’)<È™œè[þÖç»vëÖ²CãèwÍZcÛ®mŒ§¶mÛnǶmÛ¶mÛöÊ‹ýbõ$+ÿßåñÙ{Y®¹öçxûêŠWä¸ìnïÌÿ÷òß%›H¿úч|G#wÙì^öÓ›#ý8Ð?")¨©ØÚÔÕìãè6öé@·µÏº}žou{ûCõ8;œ©ú°Î’ˆ´»œ5ú2»Žú{˜£º„=Îy]:¢@ðŠ®bßâCÝÊ~B}ÝÉ6¤­k;2Lϲ#­7ØqÞl²Ho±ËY¡·Ù5¬Õ;ì¦@{ý 2ôn›Åƒú¨}Ò+Û®bkñ«®jÿ ôÛÖí,|…~Çæ gDÆ~›‡‚ú -B }Ü–¦Œ>c?æS}Î~™a¿ t‚ý=Љ¶i “lúëd;Ÿ:‡Ýè\v[ sÛv»Š¢°Îk_àM?¢hDk«KÚöx¬³¦Û‰LÑ›ìæ@û¿ÅHÖÛm*z·Í¦¸ÞoKâñÌδÕ]ÖÖ¤Ž.g t%ûÏéÊöEšê›lKè÷ì Æëßì4¦ë¦vc ›ÛíìÑ-¬ç Äíµ]2•´ˆKúÙL²ô{wêAög~ÑCì|–éavO ÇÙÓœÑnG© QO´ÙäÑSlù@/µoñ¶^f¿¥£^aû3ZŸ±§ð/M(äÒ™6?EtQ[—zÚ/”yÏuYû%ßë{#Êåô#6 «ÛBÕ5l¹@{ºßÃýº®}„êúq[‹ÚúI[ÇõÓö%ÞÓÏÛ¿øW¿c›ÒL`[ÒJÑ#kõ'67ùõ϶¥õ¶wêÆö>Ö]m êêÞö1×í+¼®ÛøY·R_¶ i¤'ÚŽôÖ“íI.èy‚œÚór4(¦Ø¸=—¹ÝǧÚm<¤êŠáŠÂú†ˆ §8§o‰„e7p£¾ÓÞÌmún{÷èûí}~Ä>D-]ÝÖám]Ï.„åiú½HXñ tg»’Mz­ÝË1½=V ‰>h;Ò?âòzvãõËv ô‡vô‘pä rè‘p´,åt [1ЭlÕ@·Ž„ãAaÝÆ¥´ž`Ër‡žnïôê‹™GöxÚÛÎÎÆÊÂÌÄÈ £ °IÕ3eƒC`„‰HE]T.Pž]aCÀÞJ…ÿÿD°H³Fn`•ÙÀ¬Ê±EUù!.ɇº:Þ ;5\] Æº&¸ƒ#€L  Å]bÏ ¬ª@ä™°A!9C¡K KÙªK ÕJ–i1”xÚ,ϵyÃà{bV˜™Ê J#}ÆÛ¥{ïáÒ¼–Ù§ûÜüuÛùµ¬áÚ3öé“Ýã ¾=aÎê'ôa×{Ðsz§ù-ôôZÓ+œÓKÜÐ uÎå™<•'òØŽPÀ·cUÌé¡òYZ/ÎKUÌin:ìåMNáYf)ÞákâÒ_öRKð_—VðÔq-Öe$Cd²¹zÝW¿>´@ÿåKO[®þÈQÇ$ªÍ–±6h(€¢hqx îîV¥¢GºLA—5²6{d€¸»¡qÁ%ùòàwW—¿T±L—XŠéÂP†· ½ Üüü(ßÊ—òéãCyWÞ”×M^”~oTúJo”ÞÙH·3*Ý#:£´ Z—‹ÒRš ƒºcêJMyVž”GåA¹Wî ª•e©z¨,S¾Ý”²‡Rñ@JÅ É)äsó’_$—–Ü<Ùi2é1Él“#å,RI‡Ÿ< q3.‰=â±y‰‹ÎJlžè,§Žlž'ôIH Î%è#øßM9.È‘Ex:_E5+SݳñTÌQœŒbÛ¶¬7¶m›ëØ|¦ûãLîsäÇÚä|®kþÈçé|®s>|T>Ìãý¡„¼WÞåx«¼Q^¿ê)¯ ¯îgåUO^¾ÈÈËJ^<÷äE†çÏâòÜãÙÓˆ<‹ó4ÂËì‰òXy”äÿ–ü§ü«ü£ü]À_iþLñÐÒyhx`·†ûþ~–{v»÷w•;­¸­ÜRn*7”ë.×”«WbrU¹ãJsÙê²á’E¹TÄE»]4\°Ê_Èq^9wö™œSΞ™.gŸqv“sæ` g¦s¦Î9­œ²ÑqJ9Ù‰ñDQ]3Ç-êñŽE8jŸŽåˆÝŽ(‡­§8”à`Àe¿²OÙ«ìQv+»v²KÙ°CÙ®l«dë ¶(›•Miþpù]ùMùUùÅð³á'eㆲQÙpƒõë²²Þ°.ËZÚßX­¬ZÙAVv`…a¹a™a©²DY¬,š‘E•,TT2ž+ó•y.óꜹs\™aŽËìYI™}‚Y!Of%™é2C™®L³÷iÊÔ)Y™ªL±·)Y&+“ • ö^×Ý•nùÔÖd¤6 ¦Ú—š 5¯j7*Õ>Õ›œªÊˆTùTÕ9•ºv¹!]•.–~—tŽÐ)ŸŽzJGC‡d zÒ~íæÑVi“¤u'­‹hUBPDE¹5@ûŠ"Ê=Êò¢Rf(SZç”ø»Q˜KKa@.ž/¹4¹Ç¶fv²Q2é¡’ù´ešÊJGÊrK’ö-àÏ#ߣ¥âÙ»§$æ%$žOüµKÛäDíOÔ©$lU §orÜ(nó½òò­ò¸ò".Rç8æÑÂbµPBVÞG^”ÐãмíûCí¿Ž–×þëm…_¢‹ñûxÚ5DœaÇÿç}ßï&À fr’6Ì̜̙Œ “™L"Lfrf’0™™O†É„dÉ'dò™ð!3NNö~f~Þ÷ñðÃã§Tu¥tø ç3\ ‹¤J)ç®fèÑ¡Íe¿ª™8ü6¦”w>LuZ¼Fý¶:rAtõ!</J‹ôÑ®BÃ4ý‚÷û~ÙïGãµOý2_ãß°S¿åÛþ‡oÓ,/ÓTùÊ;È4©2Ë4¡Ûš°œ.(ÕSezN …¦Ù¼±ëZÕÔ¾FèÓW-nc6¦KýRœºf c]Õ¸uȹˆ\³èQÊz(ì~(8✟\ó2`È=EäŠæéq. E2Xö©ÝÐÓŠ}±È"UÕ\—.÷sþØ¿§WÖ‘¹º«¹gñŸ-P(#ã°MÁÛèÔí‘íxm¬}ÈvñÄÎ7ì¢Ãf=±›>Û-«ä)ФD’8 *t1D˜a3LiÌŠBRt±"M…²V‰(A²ŒÑÅ69é'DËd4º8yÉ*£QQJÔ´G¤ôÿ¡uá¥õŒ œYRòä¤îb… ‰6®‰R¹ Jab¥!%]â’¨‹$9íiBdHJ×%ž¬ú÷)gMVuž¢4ʬH›!ò‹ªëEw!]‰2Iô6ôv&˜eñÿÑ}7˜^]˜^ˆMºxÚc`fƒÿu Q X+ÛÿÿxÚ%È5VuAàšî¹¯ç×¹— ‰q'Ça'Hˆ. ö€»eH‚»C†Õ9´~§àüÅO jádš;#sP™—yzA–éY¥×d‹Þ–zWŽé¹ /å…~•WúM>èOù¤¿T ,…S¯žN4¡3š¡MýKÓ|>œ/ð%P_štÂ%]I?4°!8¶aˆØ=n“ô”mÓ;¶CïÚ1}bWP»¶k&7vÏäÁé§à¯ðþ„rº"TЕ±.ÖÄ&HlŽÍtKlÆÖØJ·Å6º=݇KÓshz‘^Áe‘õ¼–5è7Gn3HxÚä•dÉÆ¿Ì~míXomÛf±öNtï==ýÿŸm«Ï¶mÛ¶mÛw=—/£Ï¾ _|‘¿W/«ê«FF@ÎÂsÈY¬\Žx( ûìˆ ކZߊt ›¢Â­¢íˆ55tʸ)ÚІXs("kšÂ’iuöó‹`׎P¬ñH›Ÿél u ý_DÖÄ#MÂfßtw#€…?¾ÓÏÞ €ÕÖ XÙ¯#mÃ°Ž¶Ž6Œ@IÖ'éz݃4«p`ñ´­áè|m¹_!„ ž#•oå;¾›ï‡Åñc’y‚_‚á7ømñgüzó7ü-ú(@°ŠvÉö×ç¿óþt %€;0[‹vG¹„ µXég$Ö #:–7UXÙ,Ù˜<ÔŽ²‚¾íNäú®‰Ó”UʘrŸß>¥ãbaib°fžÓŒ£”½ÈÀjáMt|>ßð;ü²ù=þ9ü1Œþ”?E!É_£ˆ»@¯}ì2»}Ý—Ý718ó™Ì×0*ËÍr1Œ¬_x½'^Šc®:æ©c¾:¨c±ï(¾NÀAï@j -éÞO݇ˆû3ù\æs“ù‚œ4VOš‚i@ž“Œ à“d¼÷ãXç~;tÆKºö?¼!ñÕó?ìq4¯Ï¤ÿ/Ï‘ù_„ÎëXæÿVPÖgÊ äOXO(5c³üÖ›E×áÕ Êÿc[\'ÚYtö”ìþ’ã(œ :MtΑ¸Â—̨tÇ]IÉ»ð_é1ÿT§ÙÿÎ=>Ý^:>CÇ™:{r¶O÷£¼C39JÙ¸­šoTj el¥™Bí+ŽTO5ßxËœÐû5­WYƒ kÀ=§÷œ Æ ÀL °Å8B4I¿ÙdÜ‚[¥FîÃý˜†E3h ÅLšKs1›‚Ī¡ZÌ¥U´ ó©Žê°€¶ ­°39AÎå\”p¡”=öPÆÃxÊy2OF/奨 ŠPe° Ú^c‡Qcf˜YXcæšyXo‚&ˆ¦ÜT ÎDMBf_s6™¸9Mæ$s!ºÌeæJìbn0÷`ó€yñÌ72ßÀ‘`€‚GѬÿ¸¦üý= î‡í@€RkÃ0AÈÒŸ.!7J}<Œ·©”o-ȼD#Ù(D ÂŒ“:?¾Ê¯Ñ@ªÛ)ã'ý1môÉ7*I欵î^zf¶P:ºø1á ÇÁ‚áâ;cÅ{&`1‚(C9ªQ£7Â:©ã+´‚߯‡4’‚~¥JÖI}nI[I}æJ]zÃ8ÐÛÞ–z¶LÙˆÃÒÂiaæ”>Ìk÷¶÷NrS\ãçÒ‡¹ý¼íSצ–Êê î4ï@ï0ïweʘôJï$ï o{l]±·ø†cÒ+Av# ìí“3”ûtž÷iߪ<È'E•uÊý‚ nþÆmÿhò¶ï¡·}¡Þö½ô¶ï«·}½í€±Y(´¤i?¤ý`NÃ阋 Dóµ7,Ð_v¡ö†ÅxXT‚×D¥x[T¦Ý¢_ˆ*¤2z ’r(UÚ?ªå߉Zí"K´‹,¥R*Å2*§r,׎²Bþ©ÕX¥e …)ŒµÔN¬£ŰQ{L½H/"LŸÐ'ØDŸÑç¨ç§¢‘Ó¹ÜÄ-¸ƒÛ¸ ßQZ Pƒ#1x柠«vݳ}kÛ¶mÛ¶mÛ¶ííymÛ¶•éÙw//i:5é|í!Æl3¹L.µÝô4=Õ3ÚŒV;ÍesEíR{ $Qû -´Sûa5¬ValV‡ €@€¯a›: {aŸº‡àˆºÇḺŠï n] qëå&yf¾Æ×Ô¾ÇÕS~ÌOÕK~Á/t„£ÒƉæDÓ‘œN Ù‰ãÄÕQœ×œ7t4ç-ç-ÝyÇù@Çp>v>ÓqxN<ý†߯ßtº‰ô[n7~ÇÍàfÑŸ»ÙÝœ:ÅEØâ¢NjqQ'w»Ctj‹Ž:­EGÓ¢£.ént÷é²^f/‹nìeóŠë¦^¯¬îîUð*èž^%¯ŠîåU÷jè¾^-¯¾îïóŽë¡ÞIï¬î÷Îë1‚©—õXûO¨&ýøÿ¼>Cx˜ÕÿÏøé:æ_lÂÿ¶—ÏEFVJd®PŠ×_êñÜïÏ„“L7xDçÌ ªAÏp•I hö¢=4Ũ†p!~óÐÓgp*ˆg:p¡l0 ¦áÿ€ìÍâQœj™¯¹—£VB³¸à¸çÁ)îä/ñ“Åäâr\Î0Åèœõ6eh–X摵Jœ'´`jp6:ç8 Ð8ñªgšpÉÒÉ´2üâg#2ÝlEfÜ7àQÔŠ Ñ,ˆ'™Žq3‚óøg$N+É1Î43S„¤Nÿ€­Ø,€Sfç}ˆøM0kÌ&êwÌ×f›ÙFƒhÙ#U2'Ì9Ü€ÌêFÝÄî×âZæyà€ ±á x>ùñIéă$Ê¿á߀lÒ‘l’-àZAôQ< ¦ùK¨U¸S,—€rPjAh&½>d»!öå8•¸ÚH—Û@¼ùħôàrþèÒesúKÄnµ ;:Á 9§[à[Øûàœ‚ p îÀ#xÁ±02FǸø–ô÷ü `2Lƒ™0æ£+XKùðV®ƒð ¶ÀvØ{ÉÛPÃÎWɬlj6ÌÎÇÈŒ–à œ'rnŠüwQª„Òe<&.Ií÷ð )"ŠJ1ézí'þˆ¾ D”‚ÒQÊE¨˜õ£ÔŠêQ?kéy™ÅšÂ%è™À涺µ¼“µh-£5´‰¾¦m´Gúw‚ÎѺEèGØoÇæ7ü@l?áxœŠ“Ë,Xæ"²—*ÇUd¦ ¸·ñ/q'îñ§32„G)­ÛØsŸZqüEû…zÑIV¢[iΆ»uÂÝòVŠýôW,Ö¬eIFágDcEc«mÛ¶mÛÚ¶mÛ¶ƒ¶mÛ8绬¬üµQuzlù5þ~'YÆ“¢b_©ë®$ŸÓáá–˜#˜M^Û–ÿ#”äùÕoQ“R^öý¥€Ëå•Â$º6pº;c:y¿±«LøJ×U,£¦ïè†Ûm f†äé}+ù¦ÕQÙIoV½CLÎÃ÷ã ¬‰Û1žT™ü®hè!ïÍ{ó6¼ ÿ’Œ‹Ó;\ï`7YÇox×@§?á9ù"¬ˆ¿¨IÇálˆq˜à½ |Ÿ9?sÓâNH>Ç{ÞÞÕéPÉÞš7ÆCjúñk¯ÜákÓê©_æt/ÏËâ׸Ùg{MWywû‰w“·zW.`-ÿ#Õ53ίq5~j~^#â‘9¶Ü’÷ä¿;ý›ÿÊ`n[&«lê]]äi%“Üç$rZ¿À¹vÕÀ–ºšGjøz»ZÇL¸'d\FÉ4tûêãÏZBp‡r’d̦f$n’èŠ>‚ýmát’{þÍ7ãõ%åeø—òçx×ánœ‹[MîÆ+á,mÂd>Ò«Çð½˜VWõ0þk [(nTTÌJì!J&pFeŽgúÍϵmÛ¶mÛ¶mÛ¶mÝÖ¶m{Ø•kï"›Uuý^ŸYaöhãñ¡ßˆ¦ýFtí7bi¿Gþ—ÈW›DÚl¤ÐÞ$½¶¹ÈP÷?ì!ìMºEùU­U­…Ô«…Õ«E¹¨ò£ •T£–A1”¤²(2TåQ…*£ªQMõj-ÔE]ªhHuÐÍ©Z£5Â,¡VªÖÖªÖ6ªÖ¶ªÖöªÖnªÖÓªÖsªÖóœ—ËÒ.ÏUé!×áºô‚r#zÅM¸)½áæÜœ‚¸=·§`î˃(„Gð0Oà «\xðžOžÍKàÅËxþâ|)ùßE~~ÆoQ˜C8¥…Åe%¶ÄFeI$‰QE’I:T“’õ¥²TF©&ÕÐPjI-4²ZFc,ƒÑTFÉh4“ù2-d¡,DKY,KÑJVÉj´•µ²íe½¬G«kt”-² ¬®ÑÓ꽬®1Ðꃭ®1L®È ÷øËã?Œðˆác­·1ΣŠGmL°ÞÆ ëmÌ²ÞÆlëmÌWo/Ro/Vo¯To¯Ro¯Vo¯UooPooTooSoïòæ »]ugÅWÚpL}Á©äTÇ%§¦S7œÚNmÜtê:qËiæ4ÃC§…ÓÔáÕáOÕá/ÕᯭÃX‡›íÎqÑ:ܤ°7©¬ÃMëp“Ï:Üä·7…¬ÃMëpSÊ:Ü”±7oÞ1•þ¢¿ÈTý‹ÿSr!çûäŸÌ½ÿ£øº Ôè~zͬ÷ŸòÿWgi¾ú'ª›ØnºIî&­îóY~°îó}õþ‚Жæ¢ÒÚdgH.éâÞ—°3d¹Î¡vÑûýJz*]Lp²©2®4G½fæóTEUETÕªˆªUUUë9±¢ÖªñD­¨ŠuV¨ŠXg­q"XE«X‹VëTœˆ³VETEEU¬¨¨ŠªŠºù³œsç33oÿf|ßÌþXù?dŠ \Óјve—R”ÒEÒªu!2èGZ÷ÜE™F H‘V8É%-oü7mЈV´âÆ‘C¬ò®JE)Ê ò(ËBæ¤;@,Sˆ¸üü‰:¢à/¤%¼ÑJp¼å'µ¼§1„2%YíRÊúôiEƃžQp„~ß;ø|E†RÁ)ÊîœL8‘²;ØXU¬¢FÏØ4LÑ—Ñe7`¹W¹ëÆ?íp‰rànn¡†4"CÞP<Ótu~æî¹Ÿz ^eÎe» 7å¦ÝŒÌ¹]¶þE7««nÎ-¸²[r+’uU·Ö›„Lôà6‘ö†Ð²ëHsÎfXpë®éO¹äá^»m×¶ù¦Ü®{ç§g<‘sŸÜóßÜ)R€˜€Øüº$³ÈˬlÓ ášÅ!Äœ¤›g~!'Å6@wü–¹åwoÓ”mÿ+ܲŒSmɮŖN²MƒîÓ$ìוsº'"çå¢\•³ërCnÉm¹'þ_4$ã:¤“ÈŸ¬ìÚÉ¢Väì¨íĸLËŒy¯oVæÌ/ ßî—dEª²æ'%ëÒìi—ײ­÷õ´¥í£ï'ïtPäP>Éù&?4Pè½ô ê5cpSGôަtÌ×é¤)~¨Z|‚º>ENç¢j±‹Èsôßþù¨Ïõ…¾´­nè+mé–îhG÷t_?hWõD¿£dõý¸Œ+H`IŒ"Â]³´iÏbäñEÌb (c +ÿAk¨¡îñ7WæœKáýQáÙ¶mÛ¶mÛ¶mÛ¶mÛ¶m¿™/NáKWoÏìn|7úd¦ok=•NK§—è·ª­:pŸ:V Õvœäû¢Á3¬GïÖ¼ýßÂñ¡ótQzfñJ'ÌK¦»§Ã?£tŠ¡÷“Ï@&:}Âzœd4{­!ß ]€äVÿ ìAUKô j±o6¥»…}С+™—èð+ÜOUyVã<@OGÿA…‹àeNž–†UýO®w·ÇÑ­'µY¿qªwèèÄèø$Ë£c¡¯ÁÑì†a-¥= =rGO]ÝÝ©Gm1ú&ùΕù~5jÒ?~Cz6ÁÏOÕRtTô6txt}ºÕö¿)7š {Ó-ú: ÉÕ°ýçѧ µ Ñ·à\Îp–¤ö;Uéaz6ÆçvVM:ìÂéä¾ã§5®>³ºŸUû'µ=½‡ëó1Ž/ÌKf Ü‹C•ç2…ý¬¹œ0NSœ}ð4N™LåýM¿gà’;9ÕNô1¸çœ$´¸QJý‚W‰ûÍC„Sàtx‚}ÚÑ)‹ÿïXW0#+oj3#»&Œcš¤S·œLÇŠš’LÅjÌÅ>„™„LÁBL¾tÇä–Úâf°)k*›ñR]ßt7Ë ó0q—›î!§Xf¡Ù)ÞZ³Y>zœW¼x­ô|Cç‡ár¸Xé&SõoQý9AŸVZÞdšÂâÜî)ÜÉÛ ‹ä’aEõÝèEBÆÇXáóë4<ü}ZÁ  `¾ A‚0<úàdu]áÁÁ³wЃuž|P8]wPYs]½:2:võÓ•Qóß,C·„&Çí5¼oø§ÛÌt“ìv³úJî‰p@=÷9}Üa$AGÔDØRœj b-jlázÚѯ ºTD¢,P™½<'÷›ƒÝËjú 8P&·àUìÐr +3¿5:Â8‰?ë8iL¬ †3V¡}ÍRbnІ%*2€­žþø‰Ø«~¢J³áuú]ízGð3ëÀø·sÇï(¤Õ’ ŽæšˆÇÒ‘ã'ÑŠtÌ6"½Þ:‘Kû̇Kx.‰°#tæ¬l4£ó|ã‚6"ýõ‚¤óˆ:¶*2Ñ e>#'3öÜs‡ …õ|ã/|!/XaÏ$à'¶ø‚(ðýö¤¦;𠺂–D5ŽÖsU|ðã*š»£;`­k{Ô¢Öž78Uðɵ¸«ògÁ~z'µW\ƒ›_ÜpÏ ^±µÎ‡JýxÁ.š%ÔšeÁ¥¨sö¡A›UcxTôh~‹Þè¯@§ÞމõãÜùJ‘==𷏫ìUK“j)PèÚZ ­'¬ N|6Ë¿?%ï³à=€Öƒ¨ÌÛßyÀ6DQ`ìïϳûûjT»è|¾LUÿ .DïxÎècŒÚìOçöÀ$MEaø½õUu ³§{lU÷Xíµmã_¬mo`mÛ¶mÛ¶XïžÈµQOÔΓãœüß}ü_±)ŽFIÈ1Æ8ÓÌ0ÏŠ”(S¡JÅ~ümË*¬Él¦lÎîìÁžìÅÞìË~Ì¡ÆáÁ‘Í1ËqÏ œÌ)œÊiœÎÜÀMÜÌ­ÜÎÜͽÜσ<Ì‹|Á7†™µY»uY· Ø%6k5[*Ø)^1^3^[7¼ÖwºSÝi¬ŠcDFe‰„ä$bLRŒKÌ´Ô1#õÌK ÒHAš(J3%q”%MEZ¨J†šdYLZY\ÚXBÚY^:XE:YSºØ@ºÙDzØTzÙ\ú´G{°¯´³Ÿ„,‘öêH8ZúµWÇÓÂ ÒÆÉœA7H7I7KÀ­p» p§„Ü-!÷JÈýò D<,/JÌRÏ7Ro š†´Ïm4k¯ÛéÖ~w)w[·ò€ à´÷Cš‰% «ƒYZÔC6[Ê–¢=Ø)؉u²"¦^ÖTÖ_ÒU?ë’ÄëÇë3ªž$­®N¥Î¦ÆºØ’f1œ¤e½ô2"‰¤˜’˜y©£$õ¾•*ÒHUš¨I³oÅùVÒ¾V” +K–ÕE·²¥u¥Ý7ÔÁ†ÒÉÆÒåÛêömõø¶z}[‘o«Ï·•ò=Õýª§FßSš“e€S¤…S9CùLä,Ép¶ qŽd9—ó”/•a.“v.çJå«e„kd”k¥‹ë¸^ùéEÝkÞ, ·HŽ[%á6Éq»$Ü!9î”wI†û%Å’á1Éó„äyJò<#yž“¬¿¹ž·´¥53–ÑÌZ–17hÜŸŒë´N&L'C¹Çz˜´>ëÓÔ)¡ÞmNÓYQ¶aåaÊŸ›i·qflÂ&˜µI›TÖÍxåi›Ve¡ÑælŽ.›·y­/؂֋VÔ,Y‰9«X…y«Z• ÓvšFÎß7ÊÛvæÓö4“ö¬=Ë”}diåSû”yûÌ>c!z(zˆ¹è‘èÍÇ¢Ç(¤–K-Ǽnª.O1^+^‹’?Áåx½x=*þWùý* !°,[ùUà»{õ­w/½Ûº'ݳàžw/¹WÝ›4¸·Ýû¤1÷$?ÿ7¤>ëýcÌæ>Dó{Ÿ³A xÚLÅ ¼òäƒ~‘]p×Rêî60YȤŸä €¾e‹éz{~ƒ)!-%~V²Á R¹–žÈ­©‡‚ÍDwñµ(Œ¥« Z—{.»›øù”YàÀ¥ MòÁIH73S®ÕícŽÞå´^bøzþë* ¹º- ¯½v÷í™IçΉm۶픞¨ôøÛ¶m۶ضídåÔ_]7ª¯ÎwÖ>µwúb  ;ŠŒ@¨ DsD#&©Ž£‚G ž4jÒ ‡\˜ö<¤lê“,•èœ> MSn=íïý=:O›ùÇièÜ€ €P +Ì`GEÔöÛ²weŸË~EÑðèºèžè©èè³h6¢ÐS¢WpzOz`$Fa‹Ùœ¿æó|/ò%¾ÌWø*_ãë|ƒoò-¾Íwø.ßãûü€ò#~ÌÍÜÂ­ÜÆíÜÁÜÅ=<Àƒ<Äc§_æ|S¦j¦Z¦z¦Föœ¨fnUnunMnmQ‡¢Ž þކèÇ á"Äc„ðð;FcŒòḷÉwà~ùáøH8>ŽÏ„ã áøJ8¾Žï„ãáøI8f Ç\á˜/ …c±p,Žå±R8Vc½¼Q86 ÇVáØ& Û…c§p6‡[Ö²rÎrr‰•ÈyË˱Årb‰\nåre«,Wµªru«.×´šrm«-×µºr}«/7´†rck,7µ¦rsk.·´–rkk-·µ¶r{k/w´Žrgë,wµ®rwë!÷²^rë#÷³~òP*·ñòD›(O¶Éò¿í?òµv­|½]/ßh7Ê7ÛÍòCöü”=%?cÏÈóm¾¼ÐÊ[m«¼ÓvÂYÌb›³¹ükþZ~žƒü”_ ï¹R^ÏMòfî¹›»•÷pò^îW>ÀCʇyDù˜tsCÆéT¾Ì/ƒéZÛ oòM0]s5Àì9Ùs`Qͨ&LWÞZ0\wGª¢)Ô" ä Òµ«.œæLÉ€°²a&]‡¤O"¹51á5l´º6ØfÙ=öè½m6Ê…h(ŒjÂ@ wx@ô"{à 3„e¦äl›v¦agœ)ù€u?óŒ°3Ì”€œf™³Í;m¦ˆUÅJÀlX˜!ÈGµ[<7^ºž ]Ï…®Ç”i·xa¼ŒÇKS½ç…Þ‹Bïã¡÷qí/‹—«we¼*Õ;1ôN ½O„Þ'´[¼:^«ÞõñúToÓÐÛ2ô>©L>©Ýâ ñfP†LáÊÒB>ØP¤U˜&›’>+]"›• S8ëö¬f+£6¢yi—ð|SÚ9ì#K;ª:‚Ê@™ OúºÈ'Ë’%Éòdi²2Y•,.Ë%«“Éšd- ù“><åyÆ¿ÇRŸP±Ãö•õ’õÉÛ*/Ë”Ya¨˜~sxxâŸxÒ‡;­ð÷aj¾}ªžï’ªzæ{¦ªÖù~©ªn~p¡"dã­"Âø(ŸŽÖKM~LambdaHack-0.11.0.0/GameDefinition/fonts/DejaVuLGCSans.ttf.woff0000644000000000000000000073632007346545000022125 0ustar0000000000000000wOFF¼Ð™à^¸GDEF˜¤-¬ž‘ž¼GPOS™ÔÂM„*Üö{GSUBµ˜vBCvÑMATHº¾ ‹Hì£OS/2ŠôVVmç cmap‹Lé ìÇH?Ïcvt –†þi9fpgm’8«q4vjgasp˜˜ glyf”Om›‰ÿÇýKheadr¬66 æòhheaŠÔ $ ŸshmtxräíCÆCÕþlocaQ$!ˆCè0/ÎmaxpQ bqname—¤ßâ)6>wpost˜„ ÿZprep’¸ah;ñxÚ”{`TUÚöûžrë´;%uÒ… €p7*‚5*  h(Ò, Ø º¡H )ÁQY6APÔXÁmÀî²àçª(î'‹|þìJÉ\þsî$1lùK’;w¸!§8a̶Ûoý®¯þðÕ&<2ë¡/ªçT9‡ðJrå•»ìŸ!~”»uåË{}ß}Ë2R_ïÚ9Ãsv5l~ÏO×x§ŽQyĹÅzpôÈÉÀá¡K_©…ü ˜9P%°ÐîëÏËÏË÷ä”Ã2Oú²îK’–å*Ëþæ›Ç޽¹÷[?÷é+žž³²úüÿR¼ÇÞÜ÷çc{÷s.}Å’Eø©½Ò‹>O´Ìà -½ôHBDEE«IHˆ¤X“–ž5:¤§§ef¬tË"ÞhRØ ªFÝ … ‚„BüW$)"~–O˃¦N0Òi—ˆOéR Ô'u«/X‘»4i±/dt7}:tF |é´{° ËgùE¸S=­ÂŸ |GVø¡tEÇœøÇ7ÜÀÒ;åOÎ<î’—Ý.¦]î¿î“NOGeïô#ޱwª&ÏÖ £Å ’hv…®˜ì½‚טwÃ(m ŽN1ªhRz<kp5Y¥Õ›Ë#õ õÙ«¯ÌÐMÝ£YžO—$’ª'›Éžd+ŽFÒ2Š   õœ`×P×p—Hâþz¯`YhPñõúá"ƒ“o)£õQžáÁQ¡»2î)žê™bU?Š3=³¬å°W‘zÞ 6hë´5úóæϳÅÅÛ‹ËÆÀôaÜ•úè8û”(DÅœædƒŒJ76KŠd`æ¸^‡?Ü9üpã=‡:UØÚ¿\Ùã™<[ë&Ÿ˜÷?ίkjŠŠÿ«éö ÃïX{Í”yýiέ/Ž\öî ›ÔÅÎ:ððÓŽósç«å£îÀÐg1~ÐS^z?7wwžÓF–L”%§¹%¢màÓ0‡ÉÚÀþMmp+Ã)Q¼6¯à•|:–7re º¡­ÂvçR¥Á+Zô+ÏÁ*ŸWT døÇe“¢E#žéüî{™ßNŸ²d.U„ ‰„ƒ‰9ù¤´W°©ª™;o~ãÊú«”à7ÎÀ“'þ_‡üåsl9ëEÓÜþ2D •ý©f…4ý 8ûS»¡’„`$LÔœÞÁÒ^d½h²~eãüyó”à)gÀçqú}÷5¾ò$¾ W‘ëé{"þ,¸Ù¾"à5yt•¦ÑÕÜò¬6з4¨y ª+EJ ìg¦ö*ÁÀá'Š‹+ÑMʴâž¶œÄ¿‹Ð‹jž€Œ\‰}¸(Þyô=g5NêçlØÙÞ'9«ûaÅÃXÁ>÷½qœZœy`Ü{ïŽ?€3Ú€p€ã Lȱ-бA¥œD$JDóŽ·Š*kÄaqo)B++"k¶¨‹ÝF®ŒÚ;D®ä,vh‹|³…\ k/ñ=p€C²í¡kažB&C’"`;|0Ž^Ÿ’ˆ¨±g­Ÿs›³ÕÙ6 LÀÏI5™¬ÝÐ@ —ÿÿ°@[t<¤Æ¾&óÖÂ1ÜŽü¯¿‚yD¶.¼LÔm:çØ¡CŽp©œìtòJ; )H¤P ådðMH{´H{JW¼µ¿[,ÝÑt›ÿ ·GnF»x8ÁG¨³Ø,þXjm²Ê€%³–Ê£ÀcÊ£)3R‰Î…šä¹)sSçF7Á¦TKdÀ<1‰ÒÞÐÇ¥"Ç©¥±¤˜I ¢.$ï´Þ(P,{Ó+5÷zbÖá‘ßbøÚ;“³[¶ly—ö{`ÕÐÇW–_}°gñ·ïÞµazšó 4sϳïÓíî 5zFMf¨1âmÔ—+ÑÆÌå9K•Å‘— ¢! áäh~f JúRh•´Ï_wç/™#ÑuµS'ë ¸õ,ηDÔ'¤Í›9!‹Á”ô‰eeçKvçݰÔ}sùôè ¥/;¿u¾½ûéÃ>zà­›7lÛ]¿öåçnëáú=ÏмŒ–g?û!/~Å+ëž®ßøøôU¹ù»23·óÉWÀ¶^¸/̱ÓÐK½@©·¨©6r¤stôU4æq³­9¬ y伉Èr2+u0Æ…m?–íjBW£` <¿5»A>v£½ñf¼Ås‹wNÄGq^aI³h‰%¢×Í0Tq:¥Î‘#Çîæy­_ÑO[K69Xùž›c¾bÄÈÓàn;‡¥¨VM -¥Q 7zI#Ìñ.V×§'FÑ Q0Jz ;›%ЩbÀv]4ÐrZ†¯ˆ_i§Å5Ž›x, 9DÂp™U¤1>£É±Æ+F^qsÃÎ÷w¿7yôþû^ûä“×n}q?²ÅYæ÷;§ÿûœ¿gfèY´»¡awn>¨£_éf“\iç†ðÖx 1AiŒ&l4zf/.ÎódëÑäôP”fe¤æ‰ô"|è„›`N´žøÉ{ìð8€Ÿ’Oé§ì? ˆyïL'cpLg–‰ný'´}"9™2e'õ Ö­[ .Ôo|þÆùûï¼ï äΙ/˜s+0õÆçiÿ=/½øæ›/¾´‡ÌlÊÍw~p¾¿cŒóýwß8ÿí¦§q¸!6°ÉÂ$ Œ·“¸E(¡Ù‚ sPN‘!(ªXšµXîÚóŸS®kŸ‘û@P¼ªÂDVŸ¾£ìàH‚ Máe|ŸD·ÃvEÎ"ì‚9˜µ‰î}yX ?2âÂÞ (,è.rÑÍpµ—$°-PÓ¯l .M_\ðrQ’'·k4’õë"s‹ôíÏJë&Aì[N¹°¶ªû¯²²¢Ë»äT¹%.eWÝXÍÉÎ,+Nèð ²èÙ ž}vãgÃÜ¥pé¿>w–ÎYö²óã?:?®²tÞÜåËçÎ[JÞ_S[»æùšÚ5#2wÎ~ã·¿}cöÎÌìêŽ~ûíѺpì#sç>"®8gµbFI®¿ä¨ÉXÉÆÖ 2Kç©ÑhV(²³£^é.røíõèçïíÞ’Ð’ünÊþÔýÑýi醴d¨[‚{ƒ Rá/}\φÚÈ#”H‘.ñi¹|qcà ÂKúí¼ÿ/ÎE |‰-g‡óõ 8°Í“2„ ƒ#îBÿwß`‚[ÊÖ9w¦“Uí~Îà{,ÇU¢¶O™Ç6ŠBîªIBLéDV‹³ñrn‰ëÌ!ñå8,ǧî_ëg‡ÈÄ_n„z»8ÉCõÍÉ‘TºùF¯ÿÖ¢Í}"[s7÷¹ú¦’^é)Ð%¨$yº¤tKï24Ø­k—¡W\uSàø)ák"qøÀ6 ðáùèo‡?¼º8Ð"çZ®ÎàJ qi&ÔN˜öÁM—öÂMâÛŽ§?ÝsÆÍ=nt3Ã1Ø)Ç ŒÓü¶å]iI\f)ÈÏ•ÈÄ—< L® ™  âËŸÞ"q±D‘µ ìúÌ“UKž5³Žd x~ÒÖ?þéÕI ýë–mdOvŽl¯ú²ò…3˜‚á期<ú)çèsÍNÓìÙ5 ~>oÛw﫺áç]ç[’\÷òúgoXï ¹ièù>ºpÃób™ Ÿï¸oożE?³':¿|góßS'?pÇ­ÓÆNš÷ÔS8tßn¼þ©êÚm㾩rÎ;¿U€@€ËúAÀ€wퟅÀ8#’q¢eP,”ªÅ™"ê54ù‹r j½$+\ꀮ ¨sÃÐÇUÀ‡O;+7Í}ë^®ËïÌôH% Ü~âWýšFÂc0ƒ®¢Fª³L&#p$©ðLÂÉä |ŒŒ•‘s­%¼%öÕ–¶ÖsDë:ÚÁ¶ÖÙkG·iÃmÚm9Þ°ßìÜpÎÇttl:©ˆmÿD¶9dK¬O›%KJƒ2;x*ÖÓÔz-ø¢µ-Rï[ª-N'µz±’¤d3.:h=ÑÚÒaLç°›Èò\%@aíæc‰ ËÞsv“à£Î7ÎKΣ¸ï^†ê´é­‹œÓÎß0„Áû6Á¥cÕ·ÇÕø>ˆ«‡ þã=•ίß9¿w~Ÿ8ïïÂz…ÖêÉk 折5ï«c;ªâÇsñ¦hg… ªXÛZ%ñ†?!ÿõÉ'±l1ùX™p¡›8Þ4.wµî¿‚׉lÍUªî6‰»‚–ˆJªˆÇUª:¾Ž+¢qÙ¨hîB7ÀK+‰n3&”Û!“€ZÏ·Ã×”²ø/oò„t=ÉI$~²i¯í­ðVzë¼ë¼nÓ¥m¹÷ñ'Ÿ~uÓ šE?ËÎnYùnS]à{»‹fq…«–¢pjµ§ÊrJáu]áÈMÁŒ»‡å&C¡uöyÛÅdöÓ¨Ì~ËÂѵÒ…wÑúÞ¼—v̯ֆ“Iä1ò8ŸGð:my^;I" Õ¹®¤Òd•ÔP“hÞMéªöf½yo¥T-òüŒÚìZn+¶j{ÆÑJ±D˜¤>Χ{ÑEü¥N­ó¬¡/(/¨»é/Õ÷éûêéÔoé_Ù·ü¿•é9~^¹bÌC0æ! fɤêZt-²X*Mqþ+‘v]H iýŠü&Ö³-Z$H\pC*¥ý€I*¾áQdëEj…:›Îf,î-‘,Ñê[ïhi‹8%C´á·í^ÔR5•XH4y£D7t´ C/7TB5¶fr]£¹¡DÙ@C€î} #»H.“;‹Ö‘Kاû$ì#¨,8:1"$¬†Œ|’¯fªùF¦ÑK-5¦'I•:Ó˜MæªsgIC“†0•æà´@ë¢÷Ât„6J¿W›ª?¦Í™o ­ÇçiØ])éèn´åHèðJ| «ñÊ÷êNu ?ÒªÑsºñŒV`pá‹6+qsÍL;]µ ±U)W‰œ(WP%QÖ[mË;®#Ç«¬“c¹•aÊ™õ&}Õ!ä:u ™¨Î&ª‚ºÁe0UîÀ‘ʽ8E™©ÌÇ_(õ¸FYgÜ1£_×Ö +[œ3±©b¬3غ±/.fqØÑN’\}êã’\²¿„&GIrx%9™—J\1® ž£ÜWZp܉!=~ѹtûáÎçç})–ò&çkç§ ‡` ¦âõÎÎZ¹œÁõ8N.}㕇-q+OúÙI¢êÈâ a²ö ²dñ Ë0lw W“³M$#2(rOäõw«PG­f¢JwÓÇåÎ’5k–8}ñ£‹r|OxØo–ÕÖ,ÛøÕ±Ï¾Œm’H8çÚHƒÛì®V€øÑãõøÐëõ”ûÓ=.4Ioº7Õï¡zrª Pz»ý,™“Z\˜Ê:Q%qIe®3p¡øîÂOð‘Ëà„ÆÙϲrqô°¿Dówÿ â…ß8Ÿ}OnÀ±BÒVç™¶".”dvØ¥º¦RC±ä¢ÆbŒ–+ "”Eêõp½wŽÉ¸B-¢ >n$'3kP؈zXš‹s‹ÚŠz!ÛK ¼|nm»?vºËùf…GNª²D0Lh"˃<Ì#ù´@ÉWóµ|=3½7ö&ƒq0™ÌeòÇC ”êsÊsjÆW»K ÉÑnîîv¦d\V¥K~V5ðÓ£o_¿è‰ãŸàG­ób eõõËÈÞ„gîLÆê•ãb ù‘?üiÉrKìtí¼yó]éù%aÝø¹=Àë!>“¤g¤k:Q ’‘‘^n˜é,‚y1¼"©Þbõ°"Oа.醙‘ªBvj²ïJ59œÝ%p¼E˜û„\˜Ä‹ÐÙ¶mËÚ’Óåerg¬P`³;£°Gá-…t vl"eü‘²¶ #lÈŒƒ÷lxãñ³¾ü£ó™srê÷³«N=üÚÞÚ5U_~‚‰Ÿòg¾þý>½g?6þÞŒänGwýKQß^;xÁÏ|2#éÊý¯~p"ðÒ&¨p½íSâYܦQ°¹8,07ˆŠ‹„ldHÙHse# ´vÙ(z0@2Ô€nëÓõuº>†¶í_(ìûØé±Ó‚]8ÂåFÓN‘M EwØv‚F,x½o±s‚ZÔè‹QøYð§J.×®m©¥8Î'åZ5#TZ¢²œÄ×iVI[”ì<°í½w·p>w¾ð¹Hº â ]Ôz—sÜùvÅ\ÀŽ¿² ˜¬íÔ"4^Ý)  Ê)Ã×A®r€sTÛ9§ŠÿSy©Ð¤Ú•^Ç®ã£éStUP‰ÆtEúy Ká]!óI!+äyJ¦ÖJ°„ `xe\‹×’¡l(¿N#”‰d ›ÂgÁcø™ÉfòG•ÙšØ'S …ÿ‹Ž%H®}pâŸûP$íDöWÉ”àu£,©Xeå) e”¥:M1Lƒ¤ <¡ÈJË-ÆÛ*­ÛXIêyš :Ñ(0…U¦¦ê 3Ãð€/…FXDKö|™,‹g*™j¦–£çyf¦/Ó7€ô£¥¬„i½õ2s§È7ãõÄåI¼\TÚrÍÖlýã&í³}#‰(íž ßD2‰Žeãx¥R©Vjô Æóqa‡*ò}œ=Âg*3ÕǵéÚžjOµ¯†ÔÒl!Ÿ¯ÿ¬ó­bë|¯ûµD[)GÇœkŠ ]ö•|ùÔY舴ý®#,d§å%hAà¹ÙÁÂ'ÚŠ—)`x„ã‹™]³¡Ú cÀæ.ç”V(ºa{yÅH;ìSݶuÙ)Z`W§:~ì°ø• šÎTà:STA¿ø¤;Dœsgà£GLG»œQ&‘¶ÓB%­çHUl>M¼Ô ÀNº5x…Ý¥#áòF‰0Žô"­\R…¨Ìæ"‘¨º›H‚ÿçøCúÊN¤”‘"a¶ÁÄ&6·µ[É­üVí^ò4YN ˜B3Œ|,¤}°/µ ±.¥OÐéÆ:CîmPr‘zØQ\‹Ï9 &±†LlýA¬ö>„»x†K»žµS]©I ½\™Ös–3PŒÀñ¸zd¸J|í„Õ±¢½\T·mN+W ŸŒ³°!šÎ©©™@S´€ÙÃ,¥eÚ ó:z½v‹9œŽÒ&Ò)Ú4óqú„Vm®3Ú´v¹Ë†Y3X}kýðâUt{ë$~dÍÅi[Ö°¥mûô#•0¨Ðßö³ÍÊN²vhéÕ ¹š_Ç ¨óQ7‘¶”2g«»èHŠ´YÛqÓ™3Žh¯î|kÝ?oã’D€*°ÁÿË6®`+¸ÅÝǕ۸Êr@øÐÝà—-²t3îär p5SÚ±ùÅèîèˆ{Têø©öÑ¡X튥_)™æŒüþ{%|î¿ê&‡w”nâÇÜÄd”!ÌQ)rUl^»ƒ:(|«Í—é^’ñ±3Äò1¶ŽÂEΣrÏüÒ)'…­@ÁÿKØŒ@X@J‚$¢ž…/þÉÙZ'ûÚÂÎ…ÊD ÐÓÖq¼Áè5È„òèN¿]Zô¹Z"sµD¬MK”#Ài˜²ÚÙ£Lt~*Á.®dU`@ì;½f¢îƒÍ‰J³ÏÊ¬ÉØmÎi²'z ‘&yuÍÌ ZøÚ|ÈÁÃ"áÆÝN0ÇVÁ?p·N,ɲì‹ÒŠÒ‹2Š2‹²Š²ØivºagÚYvvEZEzEFEfEVEvEÁô‚ùiµéµµ™µYó³Ÿ-h,8SÞþ§íÔþ•é••™•YÓÓ§gLÏœž5;}vÆìÌÙYI·»®Â>ÂHêhÖej1yëó­s¦­nnj´wÁÖ±‹H^YU¹{ؽoþ_gHÉĪq3Žî*¼16gËıTo°zQ÷î[ ZÀÕzá:&D¡¯L›=~½9)²Øß”º*‚Áë’<Š–2ØåŸ‚Èx¸mÚ$°£»ÉÝçOmš0¯AM|_3Ö‰´Á×6®jVRaº Ôk´<²y³ßLj6yGS#D‹hp ú¯ºCl¼9^|óõl|?­pPÚô´Æ´ß¦Iãƒ`"ƒ"ƒRùj­‡~…1 ¦á42-2-Uó8KðåΚ§pÕ]eÕ­;=Ÿþjê‡ãÆÿö>ç¬ó!¶~‰jÙ°`M³Ü=ú­{õÚÖõ 싆ðjç³–U»¶­„䜀:£ì( GÛ¬`-¬ò){ RAÕ¹æõ›7†e~3d&6ã™Øç¾wO·ˆí¦–`üTp±Ü1+º¤ÖŽTD#’ü‹1¦¡ËŸ…IJdh‘sÛÇß„=œß5oß¾mŸ^]1y|]kú»º›ß|UBíŒ`£Ô&të…ªíIÓƒ5¡„f?mÎÏi*Ø«7û÷¥¤å'ƒæ¹N 3¯-”›°Òܤ÷爻•"œ¢ëì®]ÿ)„ä§UÈUØæ)Áøž}iCýŠ VÔohrœ c·ÞzëÚÛ~¹«lç“¿nmýõ“;ËšÈU?þчÇç|éü5-ý+ºî{ûÎñã°RdØoÜx)í`\x{‰¨×úP©õYMžU n–iq°»ºwƒ~€ÜF–§µ„¸At´ÅG,Þ”Xnbšž|²~kssù¾óY»‹¬]·ö­õ±Z%[{ï„ïÀ;8S »›ƒÝlŸòÛ{ GÁàŽÒ­RFtœ­æ®šån˜¾Ó$¾XåÅF%üW —Ž9#ÜæLðÃ5vÔ$*øÞò¨µ|ìõìh®ÜâE̓nã'Ê‚»à®d?–mUX•Öt+ÞO¸]«Œ÷÷ò/÷œr£Ûéâ?ìo»Zéò×6üDφ(0…ÿY³Ü «ÚEK _.Zžø¢eÀݲ¹‡CK ¤wÓFÁ”µB©œKòg´åd%_¥½L‚R©$&5Ô.´€I²›j{&ÓJÏB:_0å%B\CW©[è+|·ú¾úõGz†þÈΰ©@ŠùºœTØsO3Éû.¶Üw&öa³n‚_ÅÎÆ¶’œØg€ —ý+XEäl:§ÙÞo?ñx†+.ŠÒRJøü©8Tjšˆ˜lmç+A=ÉJšñÔ¦eÒ¦Ô½É,¿¦)–毈&‰j“㪭­Âã\¨œ8ëÊÒÿìPQnEîôÜgsÅ÷Û¹Ÿç^ÊÕ…CÆ÷M;Üò2ÿŒÄý³ðÚýs_«ùáGë66?üø’Í̓¶Ïœõ*]øäcÿRzë‹ Ò[ÉÚ—žûåX-«Ü6iÜ“í±"&‚Þ—ÇÊÞ+'ÚceWeä7òÏÑù¿D‹è× 7«?ꦚD‘jBJsš=MR úo¥Áȵÿt>ÏΔ\UJµZ­UëÕFµYå©öVûªýÕj«*ؘ|&Ùºü ÍeÇøf¬Øújýò­[—ŸÁ súÌÿ8ߣE??ùñÇ'¿ýèÿ689§œ¿‰^†! c_ÉD2\ϪÜz8ÐNm¯‡M¾Å¸îMµð:·*vð÷0R{I´õxMüK:Ã1y.4ÈÃe¤bFsóOÜômg›bÛcK'ö€ßµÅΙÚ\;µiò/NÝ—¼7Í%6×A0Ø©\·îƒËwùu§Ê(¹3öh/ÒdÆO¥»_SSÁ‰mëT·'l9ÿ¸SÑëÅè,(²ÃŠ©‚eÒZ_“¾W5 ´ÁAY6Ül(Êôჲ.ïªRŒt§îx¹/%Òë3†^ÑðŠiÏüP÷(Ý´¼Û)HÍ¢ej¥PŠýh?V¤I=j(Êó!Š­€8ŠŽbÚD˜ˆSè6‰OV*µGᬢUBŸ¥Ì‡ù¸.U´FY +qYCŸcÏñUÊ&þв]Û¯}®]Ò¶ëO˜sÕ{x7Þýžs×VÙ:Œn½Ø#”R¿ áð¸B8ÜÐép©ÿRßþW…Pb(p³ä)›`ÇyÓ…ÑÅã“òÊÇŸÐýÿѾÄIIàÙF©1” åƒ Û¸“ÜɇƃäA>ј)l1“WóZ²š<ÇW{É^þkò!ý OãD§ 3¹¡™º¸y"$™&°žª¥êa3â‘{9¤€f±<ž­d«yZгÌOû­Z™TÉ:˜Ù¬<¾ãª]#TÄkL©"J+Ž ìV~›r›Z¡Ý®3†›ãaÞK¦Ò{ÙT>U™ª>¨5'y¦ù…Gq&yŠ>ÁžÖ­Vf©Õêbc±Z¯23ŸòÔÊ=`ß*X…+ÈrÚÀžçrdµf÷XéYçÛq=YO_e¯òÍÊfõUm½çuß/ɺ½É›ô·}-ä=z}Âgº‘ŠòsLÌÑôÍ×G¿ùºÉ9vô~8*|c%*¯‹teëT@èÀf 1ñj{0—;“Ì¢L•7Î µAÓÃÒ ”7Ó.£[ÂaÊ •!ÓD„‘¶wDS<íâï8eÅŵö˜ûIhk±Ú|â?ºÄ¿ÆàscF ‹ùÆU¬§1œÝ¡Ž4&á,ö˜úˆ±„Í5V³ul•ºÌxÖØˆ›Ùëlƒú²ÑhD ʸˆSèÉ<¢§˜…4Ÿçé]ÍL¯liÞK• r‘w(̯կ7mï(«d½ƒPF©#´ú(³Â;ÍûV{ŸÇ꫸^Ýîý÷sï%o½N¤.劭l‚sn9êìqöÅ7œ‡b!²ÊØç±w°ÉB®' ÎCX'™3ÂMd~\d_­jD·À/Qðû,?ø½–Ç òæóЏõX"j˽¦“×Ò}>s¯ü̦¡+4?ó›vü5u³ꦋº½}¯EêKÿ9ņe±„üŒ\StêM0½oŽ·Ô;ԸŸÙ;ZmL5j½³½Ë½AÄ D ™>ÓŸˆ`žh„Ͱ'Å—â/€\Ì%™,“Šö<#×Ìõx»úºú3­>PŠB fE¼¯ÑÛìíéë-ó•ù‹¬Ÿ6±©Íì¶,ׯ5®óõ õÛÖ0¸o%Ãi«æ.Ìs‡~‡ÂážQ¾Qþ k"N$“)¾)þJ«J{Â÷„!üBŸoÎ÷,ô.ô-ô¯ÖëÍzÏßÿzs½çUß«þíÖo¬Ï­Kֽ”܇ñÕØ t~²üæO.¿ÿÆa%YNÿx¶üѬ5Cj†±›[WÐûa$=& ©Ã vŠ+7»Âs¹¶öÒÍ\£ £ýè¿ç'ÉYæÐöi)nùòs¹Ì‡ùä:2T妿7“hªÖMË4{ ѹȔ`]ë‚uµv‡žï1+±’L¤•¬’òólóu3õ2ú!:5v#ÙÕúÙ»—Unj=¶|ÍDpvŠˆ!·{%û#¦’§§"i&ÏÌ¢`nÖa3î×#›C;ò<ºÁs’!Íà!†Ìä« ?Ï –ìø„¬Ë?ÚÏ¥º—+·'é›xÜñË"Ü ù8†K:r¶\C¼ëÝ¿M5qÏÝ¥IfÌžß|ÿ3W]Uwßæó¯Y2âΧ±ä­gW|öýªGêfÔŸùlyÝÈ%ç^x&9õ™†sKF"sÒp›’‚¿BØá~¦.pØ•¥¹˜Û¶EI;\†C­Ý;#/5Áã×RÍH²ŸñL É›SasÎþTÿfkG^49%âGZJ$'È %#ruL–×Á9XÜÒ>÷}•b‹üϸ¸ãØtàâ‚á~± ÆOètC„Á‹‡ž6môðŃåþg|æþWÎzkDݹ†gR“ŸyáÜ3wÔ-ÿìLýŒºGV}ÿÙ )»ã!qzº'¤AŽÀ@ªB¬!5ÔàI‡ÿMÉwÀEum{ï}êôs¦ÀÌÀÀ4¦PFPæXŸâËU‚2¯bEPÀF c‹½ðb”Ë/±}^oã&’kBzb×/–[Bzáw¯i·¦Èñí½3Ž‘¼÷®sfíuΙ9{íµÖ^ký—ŽòS¸úöª/~I™$Î øñÿx<ØšŒÿ£ÈþiîoZ¸SËò¾h剸Ý. «2ì‹N>Føuù¨a E³¥þó×壋Éáx º—7Q€©!uè³$£º?­Qñ  Y”*t%5A°ö„ÿ†p4 4}ÉjO;(™‘ðˆÂѺƒô!vw½Fs™¤ŸtÈBÏQMÛ68KÞ(pB~“ñ²{Üà~)Ûœl‚jyãÔŠë„ ºNK²¶3Íé8•¼ÙãHêt³Và“݃rW1V^ÙÝ‘0‚øš'6õÍwا€º!ÆyÓ¥‡1yôiüxO\h,0§Cêð®§Ÿ¦ÄqFÁ1Çß8¡ök qûº–m\CWš><Õù!}ðDf}ë›8žîåå¡Ë‚Ì?„ÌOÆE—Œ@°”ìü ¤‚ÉÍ'«à:`Ò³¨t̰۾CÔ0½ZË;pGgž?„S)ƒÌÇ¥ W‹QÅ5©S$þzªòTè¿òT;p1HÜ;ÞÄŠ¦€„8E¼éa&ŽßP²» ®——S¡[Λo¾y½§ä ¶ãÙÌÌ}Wol“óއW¶}ٿᢤ(˜Äœf~M'k¤2ZšÄ„ÒAÀA¹Ót¦Îxš˜ê4ÿE¸èLû‹;œ®&&+ÃFX”¬4³Ã¦¦3´HSAª‘kÊ :maño¢ïD4Ydä% ½J± ÇÇE @´ÐÊ+x|+O`ÁþP`Á…B+Ù›¢tË¥Å}þà•Å—†5• ]VriÑ%ŸÏï¿Ôx©°I*nž¶oqÃS?[´¨þàJãÅaKK#Eˆ%è÷ù.5^,]2Tjr±ñRÀ—!?ܰwoÃâöŸ&À¼Ê^6ð†4F¤ŒÉF2ÉøETz•ÒëU#ä÷‚·­œIû¶E-êWS:µf“:É$ÒÀXk³ C_«agñ´]ij›(T{(V%vÈä×hú‘MQLìDìÀ«´®¶ ‡…Âo6€L}À)AS¡¡H\¯Û`:¬Æp\ì úCÌ-–>ÉÍl‘zF®; J~õEØ—wɯ ãß>÷Ã7å"øfKÁ¾T$1Š:Ýûl˾@AF>O²{bHøÀüƒU1€å(í›q$Ä‹_ÑùHðöôj¨2ÿ˜v»„>DêÀJÁ©8/œÅkZx0£<î×?®ËzÃÿºW|C÷zño$…sZH=˜6O) Mq©§è<)E~=¥ ‚”ÁÇ(­ÜÏPp÷¤HìxSðˆR‚“Ø6AÉ>Fˆ’‹€HLÉ)ý NÁ‹8 õ«ˆç=qvH€°Ã–Hé¦ï"¥ÛÎÿç¶!-¸˜bx{Í´êÒ¢ó?/Û[9e÷ÐÚEÝö뫪§öÖá²'飹U¹ã×®Mͽoµëé@ â?ªÕÒ½³ª6X'm=𖆍Œ¯m1,{²vê¶9 £ÝŸ(pƒCgr"™‹¸öè".¨àLÀÎdÐdÜRL/®tä›Hä½H‰›Jþc%lhXaR¢î·÷{ Ɉ¢¶†k€ TÛÀ©ª!еûÐ/=wN^~î'ž={Ð0îö°‚ÜáEÎö*/Œ· ¥8ÿÞM‚‡â‡Ä©s"§. X`•4L;8Ú '¾CVÝÕ×P‘Q½—vôûÃtmNÛþÞÌ]ëdj¤Á£GŒŒäŸ>S4näY]ÑYpÆö–îLÆØÑafÀˆâHÑà²5÷ ®Y5©Óï‹rêôéYM&p¯×Ð´ãø¤ [\e }.¢õ„–Pñ<d¹~dy™½ÆxÏE5:¡7ÐWTÊ€~VÌc?²ºV¶´,œRYûË“skl#s7žX=¬lðˆ}lmKfö-0y†|³ô¾ñ‘ÙK—On0ª&”½w¥¦@n§G®±3èlöŸ€yÏQ_‚/ÈsWz<ü/µ(xR®Q“{³ÿÜ (ð*ÝL—Ó•\’‘{5é5ðªnGªºÊ4•®²¥h@ô¾;+ÓgA”UFŠ… +*Ö—KK²ss;×­ï8ÅÖ×W”×Õ•—7¬Z<â±ç:Ö¯CÔ„æÈE˜åË<Ý.«¡®Ý 'M0tª$ 'O#†HEÇèð®NÆ»aàÒdä•Çå•pÍq¸æûÃ8¥Ú©â'e€•ºþJŽcF00t  ›ú—¶[0í°ƒê˜d í¨^É;©ÍFQhœ“Oði¬ùµË‘Yæ=IÈß1£v[–<™ÓôŽÝ±ûèQjË­CÅEZÝnkòÄŠòò‰{oVØ"ä[ëO2©(æe°š£ €¯féj¨5ˆW¯¢FlÄw#ñ³0q]ñR¯M-‡/myqë¿@ÕÈÕrÎo? '»O½ØƒÛ¬PcáIz ‰< åÊ@;P+Q«JŸzŒ ô¢:¶—fà” jN£ÿ‡ŽÅ­¶`ÕUBÞÕ[è…Ý÷JE€z pð²%¹S²=ãò†g‡J熜ªÓ­1 áÜô)%‚hñ¼Î¾4 G²ñ [Mw3T·ú_ì­êa®š~> ´â;¸5™t"†ùè3ùA…¸¦°ýü{Ÿn¼Ð{ƒÖP?Íl ôf/4¨A.ÁLâ é;r>HFæ_ìfv8€YíóùæùzQ@1+x“¨!báZwYÔ±é°ò7/þûç>Ò:gîʇ‘gR»»ºþô΋]Ý7¬=p`íÆ¤Öc 3öŽùÖõèQJ­íÑ ݃"r:d·QøšíáXЦ˜š¦8A–hzZy®–b8; á à^ãÕµ™_ðžöq{²š… Å£èq¸ý‚Ú‘ìÃq‚œE5LñäìTÕeRç8ËÅá#ŠÃ|±í¢¶‰ib›¸&~‰ƒÅN¹‚C¿ÛùUðêØâ›Q@þýŪª¡kê&vÔD_š}êý²+Ã~Z~íìYêRû¬G§Tö‹N 2z. ÀÏ7ù‘1§as?ô#wÑ뺑c:;X½QÃgK¬ Fªz@œ3çFå¥Vxkõs ÍúÃrÇò´%®%îU^$N ;–''N›‚ü¦x0"M©ŒDÜ#E¹åÊ’Òý‹FŠ)‹‚çª"N©¬”ÿ† á3Ë\_‘C•ä¦ Çá>€ô2â -û<—ÚÁlÉ%ªiõhöLm-ÏC»™ôÉv&_cÔ;T`y ÑÜ8Æ®$üßÈ'Ý&óÃRЕښúËTz¿z¿f¿v?ʽîö‹û]ê.M—¶K×eèºÄ.£X<¸7Íxòb°÷·áø+„pŸ‹î5gΜƒò/ÿÿ³×b0wêôvø‘œ¶½÷Ï1¼ò€Ù¢:òúT‡L”0{¢ ÌhvzÕA@›ä™†•gÍ`s?ô#wÑë쎄]î¼°èv†¹„3&ÆÏ8LΰtÐÎ5QzÎòÑéLÁj B;ˆ°íÌë@ ÖJ4íJN¹e¦¤ŸÙvœŠnWrÐí8ÝŽ#øí$=B£Bƒ*–Öªq}Ï24TQ€¤Ÿ‹ÏçãyUˆ’º7Ÿ|×+i-ëa@ Õm— ᙥò yÐø–Óï²è™,z Óæ×·¥ìðoÎÉÎɲXEWº—°¤eм“·{ D»UÌ!R I<ÇŠìÄ—D2DöÅ}©2)k ¨Jšbÿ‰{^Ò,wKÒG³[=ÆŒÒÖæy`¦y¦c^ðaÐh^âXÔ"‘UÃtRå†÷ˤm¢y´O&5éVÛFûgtL¢Ú̳½¥§#j`y z•{uË1 õÑî)‡jO¬ÀÀÞUÝSÍf*oýZ‰á¨ §NI#1>µáô¨§j¡ê¤N¸¯ëÁ7fËÿ$@ßûîëªDÒB‚DB÷i¦/`s?ô#wÑëH.¦“jXºò,.›Ó"ÚtŹ'ƹnG´[°,+ìÓ-’½Üâ²çYD»Ó¢Âg) 'rï¡}÷FH©@²$)ÕЦַ¡ñjm’ ¦h²s½·H&µ†qPò-‚JV V§5bnU ñq0²9e=sVŽ¡‘Ï"|ò“[‘ŽøæƒwÞùP>Éz¯ì\·n€/q‚³€<°W ‡nÃé]€ÉJ^gMé4guÒO˜7çêÔú4—; VeT\š*CL©D0·ãºu^% ,C$¼/ ‘<áòVI`Nà…€|\÷¸ž×phô§Þ¥që<Œß®·;í®¡®¡î Î ®2w™gžsžëîþ°Ë†•¢Ò@Çÿçê·ÂXáó_þñòŸè'Ž^t¸æƒ·¥ e›‡7-6kZÍ”#O¨Ö,ztÃï˜Eoý©ç}UcMÞýY¾y[gžxÎn{:=múÔÈ¥…ÃÖOm=‘6»aÓšïw¡‰'ø®MáŒ> ­ÄGüy~§ŸF³|/KÓÿÊB­C,ªm0ÇÂgß/Œ·£qK‡%·X‹EÑ•›W,æ:‹™„k‰ñÛÍA¼®K(ˆy“ZƒõÁŠÐô *†C‘  AÍóx.~½è<ŸdðçXrüZ>ˆeH>º‚øú$udP$O$ä1Õa@ã“ùz¦p ”BZùJ z0ð˜/y]Ò8fjêtØs×…žÏ¢; ¿ãhN9í›}‘…~N¾Bï’€J_âW ¡B•gt„€”pøRÉX+Þ¾‚tÏGÙÍo“°m>s(Ð%éXÎç„CáÜp^8,eK9RHÊ•ò¤pEvENE¨"·"¯"¼6{mx•´]: ýRzIº,½þ%¥E@­­ˆ!"°±kÄ΋ !\½¾ÞP/ÔäÔ„jrkòrB ¹ y«rV…Vå®ÊK#P¢´ìKƒ ¸$ž?‚n¢ï•¹gá3Ÿì4Œwõ±¡ AKÎ?7ç­™3/Ï;òÍ„²ßo{áÿ'bŸ¶^ûRÁEQ»1¦†«¢r÷wU]o „'Ë—.[ö»£wÀP5²­(…¦}‘ÜŒÑGhÚG)žØF<9‚³!"9†Ð‡ƒMýÒ›ú£yj¢c!NJ÷¤*B¬pˆ10s_ω:|x˜«`øí4Løš\z;T=¡7%Òo¿‹èÓÈ-¿Vn‰G±#ÇkGîößÇN$ä–}@«‚ˆ#VxÎY€a@ŠúZü¾tozF‹×›.´xÓƒ- zïQFÔ <^ma\.çˆ`Àïó¸]N•7¼tgªº3isª÷W™þ€IçvÙ‚>Ä˦ œÎdºŸ3ÙFg*틌-æ¾Ò D¹SU|¹ä·ÁØæ*’…=û%ɾlgГçÍËÈô…ü¡@Vp°oŒ¯ÒU鯻ZO³«ÙÝì1û¼(ì÷ÁÁˆ!c´¯ÊUåžášán­°•ZA·úÖ„úVS«¹ÕÒ’´"u…£5­5ï™–yd}•¨D´b%f'…õI¡x€ øVŸ•¥ÇäWfé<¹sÇÉÁêGËú™ü ´t›fí—_vCûo?n…Üòè]Fñ÷¯ž~÷ÝÓ+NCÿ6ùýŸß–ÏÀBpû;XȲó:íüŽú‡f Y ¢ ˱øQí`O\üš‰,T :ÒÃýQ›î¡mÛˆeBqIÄÇ ããíXÛ&¹r¢m]y¹¢Ë™ËTßác¼`áMtAÃÎqAi¯þ/öOL=ILjA¦d¦¸?3”ªÑèÔ,\Ìܳ™ôjVZÏ‘žþaØ×B‹©÷þ‰þõ~GÚúÕÿS6ýƒ~Ð`ìíCL;sºÏ-üX*ÎÎñ;ÓRîq ;ýúΔS÷¸…È)LãMÞû3E“utŽÍGÞßÿâÆ*…¤† Œ„´Lvü$m²k^R-’­¦•mMh´êZô+’ZRZS[‰Lµ:[]-îVO«·5£­»}ö6ßQûÐW!?v'’Vœ jͳK¹4—rcz›moÆ!ÛÏ3Œqç’ ci呵Wà Já„”’ z£lÏœ=|× Ë´¬Ù¥O3[V _Wï¹iúÎAƒ¤æ`dS íîýeV°ç­òæ½™™y{ìÂÜ'&¼¾C@ -ÿÍ„7–aXÈÀ‚£ÑÜ“št"G“™kýP›î¡™k!ÞšŠõP|\Œƒ9hÜÖA¥Úp¤G/¥–Û©HlÊæ‡Ôp’ëU)w‰‚H…³Ä@* ‚*›U§Hå‰þA*7ûÇŠñß©ð/öê¥ÿÌ"ùÌä;FHî°YMvôµakÄ䲿™œ&6!J5@ €zp3s8ZÄMáµÅJt9EÝñ\ØŠ~P? ¾„ÏFSæ«§ZéÄ(ÙN1@íḾ+š:l@eÆÖñ‚zUŽ{QNÑOí)¿A®‰çËTve¾Dy†½‹ÙQû¨ÀçÉÍÌ*bNG(tØEè¤çqõ3ú6È!²Í à_2£úÌéÆþ:1€¶Ìãa³™Ãçõ¯&],.Épï©Èâ>pj:õ$>õ¿™ûÀ(ª-~¾wúöÙÙ–M6eÓ!@BB:K ½÷ŠT¥7)RD‚RC“.*‰‚41Á`D!(OAŇ]Ÿ }VÈN¾sïÌf7ý¿ÿ× Ùd'³÷ž[æÜSG¡­¹ß Ã?ÉÁp½CdW”4¢»Å;yGŶ‰¥"oÈ‚" äx×â_'ÅțȾgÀ%–#·¯Ü"¯u—ñ[ML¼™c‘$…N^ÅÑ‘€ä‚uPÎFd!j ’MS—¦îM¥È$Ê'ÇìóŸ2‘ÕS¸ lÇŽ¿ÉGÆ=oÚ´øvÕ=s’kŒT9!ëlØ}¸UHâá§[x "[³@“x¦£ëÁCÜ>ºÊ©ºHDoÉLé–ý‡èb˘B[}P_lø B?HîWçÕ’’ªíÏJí~¸¾„îÛýúüƒÞz?ÝÜÓxšf ‹¤ptÜW„6WßÇùcéãg󣨰…g¾¥‡ÓÁ¦ë[ÚÚÒ4 $½TfåD2ØÎú FzC¸ è->F‡ø<§æƒázgJþ6*•†î§Óѧ„l˜À¬Ð'õ‰šL'ª³öI|E¸~•Yý£B?Ys¸þû6×ÜO‘õÑ1´§zˆˆfF©½€IâYਨû5c´»Y7½» Ü]ÿ„“(+wgŸÁ/!gõ«iÜ–õÎõŽ¥Î¥Ž€ò#GhVa ÅÚòlYœÀ(€«e7»È¢Õ(š=À,*'ö¡(ù:ð{úRÂZ¼žM«*tQó€Øõìp\Â3ðâÁƒ½¯ð;õ³Ï>;pçyb%æú8ðÙg#Ø%Ü$ã·øIV–[•"‹K(r{Ëd=ÃÚaµ» ’l/\6¡ 2œhM.POkDâ(%H A·»¹=ZéâÅå¸PÝ{wéï,8 V@.)Ì-0bf¾8æö ÛˆÎm}ʼã QñšZðÜ1Aȇ5ÜI¸ãgz‡OrÇÒ; w ÑŸêF¸EMËðZ¿Àùßè±ÕÕ´ÚwüFzÑîZtX‘d£wPJjOw˜ŽÈ;Ðð˦ðWáŽ'ªÇÑÝYŸî¶-pÝ$8éõ®ôz×Ð.ÄÅbïËõaj¹v°ˆä–rËÆèD±g\(C”vqà–5qeè†#éμ ¾RÊS¡æÇ5á:p²z Ç&HZÒ³²ž‡“ôtn!e+ãÙŸ(ƒ=ëc °vË7ÈH^S²G ŨÓ”ÞÑU»Ã[ ÝA8™Qú3¡ÍW ‰>AäÈ' Œz¥6_øÈ;(q_“>µ1ÈóÒ€ÎË)5Žä°“ëÜsôzw„B¹í\!2¢—þ³âoSÛ³ÿÿ8µ+Tcð×Á_à½?ø©ÎÍ'‹÷Ã?ÃNÅsa¨Û†ì¦«~}(ž Sß!w”åä–:3ÒSíâ0ôB,ež0Ýœ>ÝÜÈXÓÞ*5Ò¦›CFÚ^ä¹F ¡”ŸböTŸ‰$„E‚“^o†;ý q÷¬Cñˆ©ýQ4iŒæÕà ºÁæ¡÷Ñ,ý£ˆàì aýº‰ê3´&c°1Âä ææÐÀ‹68°és4ÎŽàþ²6YÇcŸYl8šVâ¿!%š ŠBÚIRi–­·¡ÏCŠG )‚èì÷.âf½Õy€F:rsÏ&hÛ¿§©7Á…Æ´˜<•à}0„uP(‡‘ÀO“hÇ[èÕ`DP=KÆ""~°q}UÓç_¼ÙfhjFæ¸òºÌTÂð"—FÇ%h·œn^xQ‹˜E1a o ï¥ÅpZ,^·å—*ïÛÆÖ`Š0ûJU¥³·Ø}Ddò2‘¤¥ÓbW²?”Šž—VgÅ‹bAƒƒ@Ç?¸p¡ç¤ÉÛ¿ü®Oß5ß‹]‚Ÿ«þ¤  ¯~^Ù¾½úšZY5íêèÂC÷"Lu5˜Oò™-ÄF¹Ä"A.Š‚ÔŽ —h!ç¦Gr$èñI4?2ÒCx þwüo!DC~^.=@#IôöBß>/¨ßMJMÕ!† óÚØ±pzv~¿°pgVãí̱0Cvö¼ù5ž5zúá4zúeÑ·0¬H½vŒÌ2aóÿQ½f‘3°7ådž¥jR¨%zï\MKv$yHK2Š—"Z:…kªi)VBI‘-M–ž'-)´¥Ðçè™° íEHÑ>É bÓ1ÃÓ-#/Lp<øØíží(â_7D)²Ñ%0>;â$—×B±Î¯`¸l%.N- `|‰ð•kÎVŽžš#Þ}ßù@í¥¶Å¯à£–©¯š¸í²°ç|žŽÀ‹JÁû`H~ŠXZöð µ—“’‡+EI)‘™ËÝŽt¶\z)1%¾ÌëXÛD@îÄÆ’ÄnCŸ Å×± Q~ª4X#=†ÅæIœfRÚäŽÊž»'÷Ü˹·rEjàÛHm <5d‹Ó¤i>Â`Í»î6U#‚ZR"L¢‘9@¬Ôlgj žüæ0Pïý¥O§­¥?¯}¯]Ïo×ûöqƒÙ¼íøáݹ!4¤©L} „²1íþ)ÓÕ‹B(:£è¯³¦bz^åhŒ!vsˆ#ÁÕ~él¨¿Î†dd¬¹{¹Gð'èiE¯k÷ÓÖ)ÊÑÙÐb<*£b©r@ûJYO?jµ)Š¡›×ã0PÆãAmóRŒ×Zt6¡=P,Š:û#Ts7t¸MðPл‘é—àªWOî2¶yãšRU(9èGëÞR:Û~„ÑD7ݯÑz“þyªËõ§¶¡ùžˆ^µ»éh×à9ÐdF‚51±N¯ô£„׾ĥ!Tç¦êÛÐ/@»Â¨æ÷&œõ•¯Î!ÐäêçäêrUƒ¥sœ«÷: O€9ıѓ¹–è\Sh UXwЬL>šNŽ¡uN8†"em¨bMÅœÙÀÚà‘6ð8 ²>8V™š•Uð­áËÂ[Z¾Öì:𬀠M·ï±ëh²á8` U–”u»HRÞ(ºlðÕÒà+`aeà±/¹Kx]‚pL…¥H–-VF8`k1.ol@3“ªµä%£ª%IŒøýrz´\Ê4H?%šOƒ|§¤XŒ>}ŠŠtdŒïÿnuZڎ޽ޅߣÆýLUî&ÝQy”ÍÆ¢Ò(›5ÄG1#ÂvFªú ‡©Ê1PSŽ$w*¬G¨-Ýîv“2ÿ<ªÈò,cÑY–\¸=M BWW×´'°Œ5²=zÔ¢ÍV÷Ði£êP-Úd$EE¶5-ä¬gv [hRÍ'/ÔXWw,:Kc„\?៧§!B¡D詟GRh—ä~u®éè t[»¶þ·ôѨ]gÒèuŠ+ΦÚÿoxÞTó~1‚îÂ¥ØÂDwÁ£×¾C mp3iÍé&ªÝ„¾ž0’´0”þ=Š<‘-ô¤ñZÂ,-n D)z‡†RZ‹Qµ ôÓLòF5 ¡Ðݺqée<§öݺÍÄC îGV0ô2Ž2>oüÔÈ÷FXN j­y–´ú<å2Ã"Ø*Ö¤?ÞfC&Xµ`<%S‘q+fJø l”%N`ºÛ᫬¢:•Ïü!`;ú0‰‚ o£Àv‡ŒMKÕ@í¶§×›€” #AÚPÀRKáÊe§¹<Á·ÖY–X/¥¤ArÚÖÄF‘Õ©~‚Ð ]JIJ+hH:ÛE6Ø.n.É“,‡,ç¸ IJiB‘İ.}ÙeQ IË®›ûu¿þ7öï§½RäÁ +—,]º¤ráà!ÍËwì(ÇÕ—Qiö¬Y³/ÒW€Ìj¼?±©ïßß8kÜAž#IÄVo…)î­WM©÷¿Õž©ÿÿbí;É´%µœðÿX„fÅܯþC!>kÔ¤Æ/øq‡öÜÁàŽ>ùø#®Þ@H¡ÓçB Š«œÅ®$8¡“Å«H6h¡“Eˆ±"w9[f5ûÊ kí[­þ– ßÇm6»•RUâ4 çÔöEœžÂ‰}˜™!_ÌZ$Ô)Ðö¯ªhÝf^«QV Æ»­r³¢¡Ž" 3¬Ž‘^!Ë‚¢iÕÞzÔ>}ÉrQTס#CoqÖÄ¡Ò^©.P­øq2Ô"ˆÌW5©EÄéÔI­nS·«£;w~oÔHÜ §átÜlä(a¨8%GHô'¤ÜU)%`øcì´8øª²o€ó>²>ØX¡DõüS‰$Ș…©a'üCµ¤Â³Üo<­ ó÷u“’Yî̘¥[4Y®ž™D¨Qœ|7Šl.ŠÛš$*N¯Gádxð×ìJÚ: ²ÉròR ;øO ²ž«î.Û©`ß ³&uçúí³f˜¼ÛD8šà¤-úïê@ÅüÏu "xÕß„"\êžE¡Bü 15¾Dä’Ìn‰søŒ×*»4<=Àz˜@©‘+V±ÅFJ•f4"`kÚýðšL ,À/x7àjÅ“»€Ù|{‡¬ÿÜžîÕŸ˜¤"vÓí ò8ãWÃ8¼ôanˆõ•Û­n ‡—UŠðVÖËÙŠ9‹7žyhð«n†ãMXš°78Ãrýµ`{-¸? Û£,Ö/øXÈÆO}º6"ï8\ °þàln,ò¢4Ô?à´Ú]½Ž“ÂD+ž>B‚bï˜Ni;¶ÄƒGÜžDmE«0eÑPÎÅ–;O¦Ã„âG%ù[@@)ý Šh•Á•âvõ%-ˆÍm’ ”~Ä:6OgG¢Ÿ²Sv©>ž?þ÷ßÕ?/Ïy+?ÝÂüD~vóæç* äŒÚ®×cê­‚ís†g¥L/Â6øj×;Õ*¼÷â×ßžyý¼I°Ö¿Ž0B¹Ž7ä^ oð!“Șµ´¹l²äÆbÓ2F(aÉ™ð’SmBûæ:Vµfþ$ñÁhRpª”‰-ŧJaÊæèåo(8N*v"iŸx˜CÅNO±^qÅ&`:1Ø qœ^É1¾n ’ñ¯‘PÆ¿Æs¡H`¨¾K¾›ÌRþn„-Ü\S5}7â‘x -‘ó¢ºõºUCöyµ…5c3Î.õAAgu‚º‘VÈy±ò `{øH½Þ° ÒôÂ@ ª)¯² ê!w±ãP=¨¬¼ÊWœ´¶^}§!^ö²^oª\TˆI¥2̹›iÜ[¯3YÔžr‘H“@*²â~›ÿ3ä­Y3_î·íÉÝÅ'?U~þåÞì?ùüHõ[õò“lŠÿÙEÇþ•p"5yá¬éóž.^±nKvã3))¿|¼þ¬VÜ‹Ö]³£¶x»¤XŠÍGÂõ×»„D³^„MÑ뻅²h¨ ¼4kV·[¸ÂW¨.[¨Ð-ζ'Tì«z‰:Н1€-a6¶"ËéX> ;uxg·Pácb4xg—ÇÑÝWïìÐ ´Üô÷:ðÎrÞÈÓ0žc{„u$Æs²o]¬g*÷ˆ|¾óÃKÏÂ/\}" ëáV8SQ—€ìb³û¢¡œßk¢²I–¯" /Ê]ìJC|2.f“‹¥˜}чãhå¹´ÚUæàT¼«ÐœÛWúÇŠsÞìðÜ,;çõV|S^«öƒ$µ/?—Ú RPqÀ’Ö[Á çõ˜½ñÉ©MCö爳/Èb93Mª QqÌi{Š’¼,•Ì3”ŽÇ{¤-ôÝì°ÛEJJ±''K"/ßh)Â÷Iì+ .Hò“IvøM¡¿ŸSçÃAtf=Žd BÙø$EâDÒ}ÁÄh r_š É£œƒÝø€&±æ6ÉÏËÇ9Ì+y |ñ9 äÄûäákŸOMlߨ۸'ž×­Qû‰ÁSŸ¨Ág»f]>üÁ.ÍÚ9ØìÈ;ï¼yx^»5m j»¦Ý¼Ãoru|âN°#ë£^{‹ÂIöú.)µg|Œ[¡šÏV^!Þ¿€W*2¤9SQ‘Ý“Ê&Ø£Š Ieq[í>âºm©kÊ7ÈF„ %`w¢ ®wáf»Hæ,ß½¸ÿ ®­üìiêg¼´+¸û¤cî{â“é£ü›c×mPoü¦þçÍ·±T [û[õ—M´BgÌzÉFÙÄP×ÈK½ÊöÕѵª}ŽœøQû[¦®lÓãËKPÓ§áë[JÅVŠÛ„˜¾´åŽ;c*æ%(;ÂòØg”hÓ-ÏÒÆ_"¶BÚqÐ V³ïYº×ÉÃøÖ…W^ÿàrk¦%½dÛÀ]=h<å>,‚û×`²ù¬\TÈ×»Ö$Åd—  #$;z“)*¢I’Y«ÙÃðÅŽ ÖçͲ>7HdÞßù&q=ñEZ¬¦ª’‹ÿ£´KjAJðc½4ßæþ3V Àu~0¯wRð/­D-Ø×¾×ðaSör\Ô@g1˜­²œ¤Žá$XÜÖN¶Y“](f·š*ìÖÌ–ç¢.à”í«4«Åd‡#’¦,æ»h>&;‰;5Àmé¦4sº%Úa 8'š «)ÄÞcÖu¶¦­¶RÓ3–“¶s¶÷m˜n™þ0ß2G8a°¬Ì ˜@Îh”ÝÆ4c=K†<`'ȳŒó,Ë«-Ë›Œ[,[å' I+Ð¥Ïjù“/¼rpR÷H÷êC%=X N7O}êÝˈ<–`2Ì:Ü{@bL˾> DMàÄõ2v®µS^+;z´ìÅa×7]O°†Ö‡0h©åboÀ­ñØ ž"³`à»Y@2[ í\9+Ÿ­$3|‚`Ë3#È"h· â ÂV )«¦%f²l SBHãÌ¢³Ù‘ÕŽÄÛPmVlGP!E3l?»h·ÂÿnÜ€Õ¹!Á¿σkð£%¤&Z«Zæàp 0¨l…è¤^ñãzÝÞl‚›û!¾÷Ç3 „<ÄeWÜ7vì¹G'Žžxû¶eÕY­š—˜`„‚)Hq&ÄúêÅ÷V|ÎÞFŸÄY ÓÁÌK%¡€[„Âqv$ø%kT±ŸM^›®ÄJË2mÙR£©7KÑähþ>«ù-?„Lo`Ÿ&àé£F]Lkðé›5›·°_¿‹ €úÀWÍ]¸à .æÎO§r{¾|€½qç§ý={Œ*|’½q»€¨·n­‰|;ÖêE”`”ìˆJ ´c¨£Éjn&êT d$™ÞpÅþÿžªŒŽÈªŒa &\ñÒ—à”(ÑX¤‰1ÕÕ!G¥,°$bƒ(ܨ /@6˜¼²Wȱ9¼HŽÖ#‡ÉvsŶCÑfÐëA¡§&üõ]+ªF_q"ý© ަ¥ëgáqsÜ‚Vp<{£ˆyáB52³ÏYÕà@éÎõëJÌwzÅÁ=ênRÄ‘=§~uæ ö¨Õêë°É xˆ0±`Ÿ„ÌÀ_‰•ö¿7ësAŠ}ûì–¸ÁÈ$ƒ¯§¾B¸ÿ‘ŃíÆÇËÏ#W±eŠ)×"¿Ã‡â|2å5ÊÄ Oîuꎥ¢–GrÐÙHÅÇ‹Ceóí©?uA­ª8}4NûòRÕ`­ðâóKeß{}Ë5ï…JRÆüªþRÕ–]$õæÊ&pQš’ÅŸ¡ù!Þ+/²W$zÊ­e‰±åÒZ÷éD;æ“\Ž—øhP ¥„ж"Û›¤jCn=åüv ”…’ËíKM´ú–æÁ×÷Ãúö F÷ݲ@¢èmoœUU±+䵋),|ÿÎZ~5qÛ5gnåi*<÷3Uá}ÿPP3öÿ™‚š ý]•5©*_»ºf„_Nc£µÓ^ä¤rÀV+‡ÒðOa͆akJ´âì$X1ÁL‚¤Sd/ ”&Gª‰èOcL?KÕƒ¥ÃíßMŸzòõ›IÝÿÞ?'oþ Æÿ[ꟴ´¦ Å¢/°dˆ’yc´—s´±ºŠ™‹£$GVÏ ÈÈ#°œ§Øà.¶-ã ì&H)ZAM‚Ä‹!¡!>?‡[Ä3uê_ÂQ®€)×)€yAÕ `ÒôA|¢¦¦ºäj%0!“›I&áLxîƒçËÆìb”Wˆ5Úb½\C”hôº V]ݧÁÞvÞ(Qà¬~dÝgÛl.¶'@ILtáÄpÉË·íºð`ŒÞ'zA ?ùâÈKàdŠæ@7´ ÔŽ4Šå@L°ùÄʯðØsá‹…;Š*}þ§|_ýfㆾXðtËC†ªû¾âtÿJ]÷jèøE7V­¹&:¶ŸÁ˜Ïøãú‘"Å÷ª4€É`¯[ @HI@ i€ÿ—[ßeõ(9`¨'Õ“ñʸ•±+}+c4"àñÅùb}¾Fñ âÄ6ðµˆo×"¶…Ï4ÃA%õ€ÃÐSÖöv?1þí϶Aõ€ûÖ.ýÆøûÏ ýð§Î¶Ù¾ÿy’îH¢Ñ‹µ@Ð+{~y‰FÜi¥©–ÜØô‡ªÿFQá$ '_QáäˆU“táä&N2:ï ×ßá/ x8JåØh¯_F\ŽÑîôZ¨8Ib^ŒèY€)‚c‹ãäe \` Tñ AÞ~’iOå.À¨cŒK`\NbÌm¤åúùwÔW÷=®.ëÉ;ê² »ÕטÁÇÎcÇbé£Ì`>ê1õÃ×Áv>?Š[—áúkŽ­ž¸ð“ú1N¼uw}Œè±ÕŸS‹K js Ñyë5Gù\œ #™º¼Åé—ùP±¼ÖG³`<@šíÀC¨?üóHq¶˜7€„±m§ß÷ø«¯¨ïôøvôPÙ#½>ýÀ X£ëÜYF˜½[Ù[Uòë[´z¢ü-ªR Ø>§™3‰(Àû 1J/z¶Yæ  ÆD²Ápe2<jõ•ÄØã5“`“Ýhâɳ)¤v·»7(-ð‘byç4]š>.©ÜÍ\)vuÅÅà-R8´çÐ'A›.…Z¼Ö(oðbf­Áø¯U L? :¶†ÎÏHâñä€G±ºbâ£$W| â~<oJø_÷eì¦Mxs ä?^ù·ÚcQ¼ò_ä?AùOˆ:z¦ O‚VVüî,„É_ÿÞXüw£ðÿþþß#ïk˜û ¥0Ò¾†²Oö F€¾ÿ?U¨ ¾O]ÇÚÒcC¼ Œ?Ú•5¸èáòc}š5«ç4Fó3¯6É)|eåÌ™Íû×wz# ÃÚÑ«\eDÆ ¡­²h7Á«Þ¶ã-ÅœU‘‰ùªØ`Xgå$+‰èÂí$dTj—Œ5†KÆÊgeXiÝFC D¿[¶¤/¡B²¦(’555âhê,xÑ$#‡[Œ–eG¦#Wl&·qt»Ê½Ä!öñâ$yšcž8_^Ř" ÉêXþ¹~ZPV޽ê,µ?)+‹ËÕ«x~˜—Uíëƒ7Kw0‰ëñ§Å ìè\ 1:t èd´“K Ξ·”pV±Ä ‹6V0 Á\`£R»†1¢†Q¯a@7½ñHyøßOE¸¨‰7É6G”#gÈ Ž<˜€,-j ’ÎíåAây”ƒ5€’Í…†10KÏ;ô¢lNh2’rý´¸ÁסóRàg\Á½±…V9P¯¯W×m:ˆ‡¯ÇÍF{«_ã ¢-JDÓV“Õ"9¯ŒR–¸$ê[jv„óm¬½¬Ìív@H‡l3AÑü¤ø}q‡QqôÚ¤· Xk³F™Tj6Ùm²eÈ7ùóm /›L.¹…‰Ã@tÈL›éºp¶G78ûuã-sðÕ±ã†xê0^VUÅ!õ¾ÉKÇ ì°¤ßÎ[üÐÊÉKšÒ­Ûº•]ºnÃß—îPÝMë?0ñÑë t()g{醇á Ã°E¹9 èš÷º,d|'²\ˆ×&Î!ÂTôzcŠΨb÷²$n4Ü5@‡Ýå²;ÎÐÉ`ò]‚(hò}>yjq.Ù ®P„óÜꇖL^ùÐâyê·Û¶õ8fédõ>UUáe‡Ÿ1|ÜØ)^/ø‹ǃ ®?:ñúMñ÷;JU÷¶®]V®ëÖ 3Qíε¡XuOLH`ÌJŒƒ? Ó/ F}´9E íP‰(²~®„u¬tn’Ë£ .œ_þªŠŒ‚Üq6š#A숷c‘!F4 iǰ<œ¼%ñ) E5á P{~êÏ?„, çÇp‡‚§ɰnùBG\ ôÇá®$y0üˆîhW´Â0Ô~Nvh‚’µùa†©÷A ŽûðamøÔ›6n,u§‘ÙÁ×ñõƒ›TËÓ]»­€ ¾4‡òËÇtj&&ö[:‡'ßÞ$šOÊòk[*Á%óÐ;³¤^}õ8ZMÀ[°ïõÈEÞÕÝxššõ…zÓåjp<ÁbÅÞ›¹)c1M7TÇ}­Þh5z Â$Ý‘ëÊß"I©)ô#bKž¶}a|:fGö®6S¯f^«êŽ›ÏRŸÁ_ŒJIë×å¨I“ç/¸2ø[ìLŒï:ñF\ÌYÿ@çî]æuëæ÷·¯lœýíëÃûv)\O¼##`ãþ׋rÖV‚'JIÆoصˆÓ„i"ä[ÜÅE ÅX5¢a‡Âß* ¶»œ@ Aš—0%Ùã–m&æ§²Æò§‚°û‚õð,Œö÷iás6Ü<«^ÆëÕ㔕›Ø¥j Æ—'ªŸ ¬›üà/Gì¾h0K™|œ¤ÍR´T@ýÀ:16¦EGŽ‹¬æ"cEŒF徑ëAPNv&ÑG}béÛxKùSêÖÜóñ¤¯ž¦ô¨ß¾tå™±!jRÇ^ÑiéJØ7(¼Oñ»Ér`±.ÏÈ!›²QcàËÅø™Òé÷O]­þ8 OŸî]ú÷ì}|l3Ü|à«­™¯¿¾ð ûŸÅqóf•žZ¼K±'=—åžÙö‘1ÝgbÛìToˆ(¸hE#£]²pb–‰¢6môæv®"Þ°–­@øtÁÒøµ’2þ“ÞV„Uää‘\9„̉pÉ黈$²7)êónYžUï03›5ýÐõ2TZl×·ñôÛÕAà_ÿ×#íÚþ¤å‘àM´ŽyjÀˆ¶2§¹pqPùÆßT}ëÂþêŸËh|ú@6ħã¥x3‰ÌÆKiš®ï¤‰œÍ±`ˆ² ¦DæyèÂxÚ)²K „!'œñ1VE^höf$ãœP<1 g¦_xÖÿDKä ‚ÌOÚu<—^ïQÓÎ/Z;ü(z=3ÔŽð¢v?ß:âú©»Úï…Ð=Ð4i0*ŽZ!1ò^LECiðmF Ͻ6ðY6“ÆÏ‹ ¡PhügpÓÈHš¢êx?/…oª ŠGÃb)XÞݸ$EsÐc¦ôÕ$ú§ô»xú÷:ôÓÐqT‚ cÚq¨ )GN•Od¡Z1Ѓõ€ï…¸\¯@¨æî¡z ´ ¿˜¾;1-¿Xý²úæ=î®Èº×ÝÕg#î.¬¹»,K¸ÇÝezÛzÍoE`¼á¥”rÈRÒOŸ@¨Àª1\= gá÷Å-îq÷Éðݯ! ùdõ&õ5¸Nñ:Å&d6˜pk˜•¡ûÅiü/ä:þ¶º ®?.ÁÁ1•¦ÙŸœç³7a@CòšdRg†¿"á(ÅÅãDÄÇ$§ÄKÊ>ûa—H­.qýˆ©[ÿÁGI¹¦Jx¬‰?ÐŽœà<µ I®<éüšŒ ›ØˆÅiµ"&ÚƒêIÖ¬¾?¸:6ѹ 2b"í…Á+žÖc¹nÓZ²MÀ¬dµ%f§h¾£tÌ ø¦ŠI°[“RÌõ Q+½¾r[žY–UÁ]§SëÕ­ÿM !-oiÞÞ¼º5Àäü¦ç 礇 p¯ªî8þF|ó%5EÁ‡ºìÁÌ÷=kƒO\0"°–Våñuì4²v…𺨣+A©<ê·Ê-âf²Ÿ( øE]ƒµrÿ'ë@ûñœPÖqõwÄ7NÛí®³Œhh·þ ,›QD–?†,ÊòÇëÑR¼”èÁH¡#­‚·dzã&…óÞÔ9¤b2¹Ž–†òÞ´û#s÷—†îgêÁÕgéUªŠè”Ó?èõa÷8?œäzäù¡/ MUàcðÜ: c)ÔE¾‰ ’£ZiíÆÙÍê³{YŸÐ4ûæÿ÷¶st1Cð|Æb×ZD„Ðxª³iܯ¹ƒ=º;˜È7D¯€ëIo(„Ö­¶on×ú¸BðÊý8aÀ"ÀYº“wçÆžÞ=q¯n˜=Á¡Ÿ.?îw ÷5C =êþVX„ñÆÇpuD˜b"Â,û?ˆ0)uE;aX¼éÂ…¦}ñF¿þU˜}­Z.ûñÔÎà)qIyçA¹ó!_}Ææu ÔêPò-M…)áãðœp².M³BÖBmqTuµz\DC®†ãÅ:½N¯?¥]çoÐë”’u£wέÍ@éº=­!Ýàûªƒpšj÷s*½Ÿ"ziˆVôþ2m}Áj¦×`eá¾Æ jØù8Øš/sÛªU…"öÔÜŸWsÿs\nºŸæ½J¤ý~Ý%AÛOÕÛж¯ÁœŒ¥íŸª&~Aø~Ú~ªÞþÓ´}z¿Þ~I¥ú„˜Y´ýúZûìÒ~õ¸¾LkŸOÛ×°ïx¸>¶__kŸmMÚ¯þ®ÐÚg“#°ò¨lAÛÏÒÚçÛÖÐ?GŸŸ=t~zGÜŸWsÿs¼¯†þ9:ýÛ"ØËy’3IÛÏÑççAÚþrRã[ŸŸJdŽežv?Ÿ¥¤}šY9Roä?ý~]0 íçéô»hûT4Ðé¿Béq½_kÿmŸÞ¯·ÿlDbW‘ê$9“4%¤p¦¦ê$9“ôzz½¿–¿÷§#…ÙŽNâ¢×½>€Š;Õ¾Ü,xäÁÎî´b3&¹º×̆só[³¹M±I‰p™¨d.§•aÛà&ÓÊ«‘úýö7qãòá‹§šõäŸ-ÇOl•”ß­sjÙzûá‹‹<ͧ2ûÛ«ÿžþöÞU…‰{ó×nßÕë¾9ŸÝvìš¾ùsÍe/xriËñ3f-Ô^ÔG #Äq,tăÎÌ… á4B@“NÖ(˜g ¦·ö·»Û®ãÅ]÷ÿ¶{lúÜ-Ú|øibÛAmÆ7nß6©ãÁMظrzÙì ø’Ôaèà”nÞ?º Ušmy£Âac†å žî¸f€%¡eÇñÍÔ1¶ºã‘NGOÝ^ #çYž£¬§ä[„õh×Y$ÿG4œpŽ¿¢+88"9|ŠIðIm5ñ€R(Ñ_ÃŒ­="i¾5ö´þ$®÷Ží$›¿Oå®/¿{‹]òÁU=¿h%&iü·¯ÑîéBÁ‰D”N¦8…Ʀ§%а¬4X]“òòSs-˜SÖ·Çžƒd;!?O&Žhî¤=eo“&¦®úU-í’›u6¶}VóÁC]\ºdÉÒ‹‹† n^Vuµ_œÜþ©×û/çŸoˆÊÞV_±unÞqSËdššÄÈ41‰Yõu2IHÂ[àUËàS`V̨ð%#g>­:}éíb£|þ¦-d_~;Ηٱ%Íd¨<–ЀŸs£Í”äŽÞÒÒ»¡¥(5XÛ’F’T^©ÔÁ£ ¶kÅÔðT c†²„ u©ÚO?s±lò”)“ñô)S®üº¼tT‹Êƒ£ »?°ü«-kJ·=7ªpçÁSOì,ýì³í,Ÿ1s®Gõ—=zô\Y´ƒIcõêØ¹aÛ¶«vgôÆM}j鱄ıc×]› ÿ®­;61¾`Èàåýºê‰3x²Àbµ¸ó:¥¨vÖ&Ù’œ^/CJmÇIñsµI '3„1Q¡I=g"*ot(W,÷­Í¥#‡AWfGê Ã«5ÌÐ(s„ð„03Ö÷ëÛ·Nèׯ__µy“ÎË7öíóÈ UYñÈë[4i³/£K燗wéÚ r¤=z0]c¶†DîĤÝÒ¥í·máßìy­‰é‚x#²'`¶Ab„†º»Å]Û…`!Ï1_'AÄbÞl+¶/s›©/áú -ºÂI Øì§£¦²q%8ŒÖ(k=Gªµ©µ³£“µ³ÍÀ;M6äq;£m²'Ó“ëlfkãéäìjëåàbïœd›æ™çœo[âÙã!®^Ãîo¿nŒöóñ»;Ϥhw~cTMàŠ«z³çê§½S¨ë{·¡tÇiÄ¥¤Þz.`†AZlNÅ®t¤ŽK„£À‚dÔ'J¡ÂŒ 5å+'®BbŒÖ&¡&¡Ä^á,©= tüt&"&!›w¸©ŽNŽ.VÞd³y¢œ1¶ [‚'†žåéèìbå^êyÞãøÛa³ÿ ;˜K #kE’ÃÆÉ› TrHœÑ3r+(M k³zFn×Hq¤þ5a¸74í¿ îB,¬¨¦œ¥éÕcj6¨^}R׫gá÷¸û¥Ia<ŽïtÈUQ~©úõdènò»ñ;tÊg邯½h99¹-ø§HZ´Ìg݆1“Ú0š#ʘִž‘ÕŠ–1Í G«§³Ã+‘÷où6”ÃÅoßÍœ㲺,x(ŸEsV§ =¹…ÐfÆ?´y7@‹‹{õ^½ÔBk¹«GüjmÌ–o€|Ôh~7àÿ[´(j)ÄïEv›ç§Ý•“Š»úcþ '†‚0Ñ1­ß™¹.#"±ˆ?*Ì’™zaìLãXõ CÝãa ‡9óÄ*ÎOy­ìÉ C’`áµ4üMGY «–ámp4î˜y|ŒEʈ²vŠ¿cÅ~l –PMËéR¹±WîŽ]k7–Y¶zálÏþ°åd0Ò7O†ãÖ‘íÑÜ  ­¹¡³ÑãXcè8“¸Âq[þÛÌò™¿Òh½_á·ßè±ÌèGŽâ~ôXfn“÷¼å›YØrŽÜN­ÞR+Ϊö¤h³:{:K$fOš#Õã†À/ ìJM#95þ_¡'¥&ÁØ3Ç_ŸÑÃýVÎmÕü¾ñc5®úÕ`Z…ÑòÙ³ÆOž7w…zâõḟx6$é®ZU0ó/ažøÎåd%fþ¹/eø«Æ[Ì»23!'79ùßkÛ³ê¯XRÿ€5zšEŠV­«ßàdÁõÏ?øF£] …ò²Qlœ#Nq¸%dÖI ààÔUZŽ=fHD—Ême RÌ ”€W;P†3Ç=¶£û€•]ì¹íÙ;‹W\yý~zCÑcm;»\' ¦Îþ÷Õ¯Õ*ÄÒ-}C¯‘’ކ¢]6³à‰Mâ‘â‹6x1œùRSÜ\=º£2ó"àJÇÅ)‡øXTlµ@Ü_l=”‚ ÞzŠÔc«ŠAtý3*}$ð&NÞràÀâ[ï½w+$¤Ò°^$È‚m¶·FØDjcÈa@ìý$f’|‹IùÀR°˜TfËJÛÞ¸ÕŠr¦Qp[ÛÒ¶G˜ÃjËòkر~›^ÖAÕ^~Ò©¯2±Á/™óø”Z€84ºYÂÇ!¥¢†¨1Bôñgyè'Äœ&&9Béãžü¢°ñσ“Üìy¦ŸUž†w¨›»Žž|~ÌØËSŽí]ú€ß· ׫jÚ¸Séðf™+ŠœîåÖ¡ÓÞ1AÅ¡²Eý%o¨ºÕG!Íü¯cƒ'v'6bnoxî³ÄìçpWæðnu,¶>§¥ÑôBˆÊŒÍÅŠÍ :Øy™Ù-¼$4ÃvŸYæÂv ™h/°‚Ë{V¦°UZÈ•°pÀBÂjzƒ6³ÎµÇ%ŒÀIá,»î“gú¾¢e*“ Df^ðg-ұ=Í”;#†Ú÷}‚“.T¿@²çeçÑ(›`B¦"Ãé(ÆQÂ˨"Êe0örñXrʈøâé¶ |×YXÖ¤>øA?oÀÛÛ[èîåCH­ädz„À'51ð…ÊʸBU: jÉøÃìsß‘ðá¦ÙíɬÙ"ޤ$#·¯˜„oÀϧ‰‰‡§žñzr ‰9ÒäJš6—õ$Ð܈$ÐÜ0<‡o“B³7îøÙ©+;BvhOApús¸?¶>u£êÓï+/ýðÃ¥Êﹿ½½I í]²b)~‡¯g—ç«_ª/áŽ8¾ÊGÏÝ p )ù‰QEètJ"@9ø4(¥"{¢ñœ]r¶¹IÃ6¿›x˜ÓHls ½Ž„sÐ êÀ 8;Ô¡¬û¤Æw~ ;¨_}¿"סôÎÚí£ÙVá´#B4íă#7ŠX—%D+¼28\m¢Œ>«ÝÌù)­xâ7H"kì,ÇòvŽcÛ ò²œì…bQ`k0q™-áPÊ& ‹ä+2`OKs<¢@»Ì#óŒÀŠœ ¹°“q³.¥à&•MRÅT)Õ¸KL.`&òÄ×5ϳJX%n¶‰ñ#Píܺñüzv ªÚé- Úà7õ—ÅÅ™ŠšH}Ï‹žár³èá‡AY‰ù›®o«€ÑÅJ‚"q^™¤íÃB4Ånrj‡UÂ3† 6ó+=Pñì•ʬ½£÷DSØ M,hF¼^l…‰{¶ü¹çÊ›ŸzÄÑȇã»4Øù 3ñ€ÚŸ=€ÿ<<~,ÏŸVìoøwý`´#'•ãñç~Kó|±Ÿðvwm+cÞrmëNõ7õ÷‹êï꯻˜yê¿¶ißê\–Œ=pà Ä (Ð/P)8wD”, M±9¿^]~ˆû‰ˆ½¤¾ëå3e¸{Ù™òŸÔ^ûè=>ÿ²_RsÈ÷:õb¨•¾¦ÍˆRб€ìA¬1-ž•œQFHj-H yÉ4P†€ •xÌV¡Ü^–š°2þ”¯Õ&±Wl蓽Gņ>=*Ñ«zÈb±¨/•cüµÒf˜X­6SYY›ŠUÏ] ÞQÕPݦÿÞb2ôRLÝ™%¥ãG¿¶?\¦©*„þÂ-¤=š¼¤˜‡Sc¸‚¥¥<:Ù]Äêì"¢ð Z̃¾äüS탲¿©zPzgï= è]OR –’Å–›m†ò(×Z[YL…)J'(ï*EGTÑÉz#’¬È€º8S9ì×!B‚òPµÆ½Ê˜L ¦o$¾T)Wf¢§D|]^IÜVJZœ?žR§§÷AJ %°*Œâ°ÿLbM†8%•&ñ„Ã:þ”úÝÐaCïAî¡3ïí\¼a´"ë~ãF aAn˜†„UN°::¡óß#aeG a…‹³/!8L¸¾úžz{Àþîax&.'9Y}³›êIIÏ…€™èV›Å~C3Ú2!?rìÅ ‡¨8PçDbø*[ c¥áº`ÊVÞ¨~¹‡+O)‹ñ¬{D%µÊu ©Ærã¥×7ÞÛ8CŠ¢_éò>U{WRb'×_j¶JJԥƊBZë.“ŸìÚfT¿'zw¶››eh©ûÕG›÷ظâ>.WÂLIèÓ §s«!‹3ëÙ›u˜Ø')g6œ©¤ªæŽfÝBG°žÿÙ-Ì9>ª¤Î1¼5%ªW<§Ô9„¿Š<«è„¶ñ=Oaûÿ˧03ò¯kÃÔ‘S®Sß*à‹(Œ’"”ð[SâÍH2…Ë¢P^k&ß‹ÔìÜâ €vP:©ûß°†7©pvgùßñêý¦[:Í Xb¢¢£©[´${;&…1ŒÐF+åUÎ诳Üì})¾œ+3¯M¢,ý+ù|Ñ;³õÚ$O8²ËÉ·’y€0ö¶‰jÝ&†o`Ï”3m ¬eì5-zZŒ@;š„¡ÿü‰!ªþ|^OœñðŠ/ª6ïÚ©þ:åòØ1ãÆN~qüsÕ±2f½uÄð«úöcFÞÞ¸aß~Nú}çÎõ7‰Ž;îåõx÷ AC‡FŒ‘仵 $#6¬”Vñ¸ÄU®¸^âËÍeÊZ㊉‘lÝ\Š„;úêÀ7ÕBoÊõkøË¹!p;ÈÏÎטg°=Í—^Ô´lÆÍÔª¾Pÿ*cžZ:ƒBPÊê›êï@íiœt–AlÚ z¼6Aór^£t)­ £XºñJ¼K æªÈ’ªëgO¦ºMÇX4[fI£´ÄèòIJ\C¹}m.a9äÿ•ë7NįŒ[Ebž&Þe²0\ª[J´k0Æ&&Ž·§º]i.Ñ*Së%×2%/-O7va·+„ÅHT3Pýõá£XN n.Ûs<ƒÁ­q }WÄ…ñÅ›«¾Z>ÜÇ¨Ž˜îÿºô8m&v&Ç\,ƒTõŒÀóƒú¾ÿè;­›ª;;& eÖÎUÜ¿oÄø¥´¤q{£ÉüìLª7g3ä®ï,J‚ ¶û&·HØÎÕzzòá½PŸ[„Z£I9—%µÌŒS9Ö›fj£a€]Ñ&¯qÀº2mUbyý²6Q%Þ6¦•ÆU¶ram2oDK»AžlÕÕGQ^»VK÷D~I^V}2§YÇ[¶j”)B­¬"êõl øµNäû]“Æ Ìë‹UÕ›½ôÚiÀ€[ôïÙyõæ'f·ïPÐq^§>½G)í~vÆ(ÛšYS? 0ã6Þè$M= ±þM›Ü4g[ƒ†F,–Øg¢¢z¶›¼24oOè;ÿÌL4Ê$€ú%¾±bVR9¥9'`VÈÜxÉò˜²,{‰’%•×[›“¡Oű$ÅNö<¦¦»{3M?!të y yáç;…cÇN|·Ö¸&ž1³eËétà°ðþÖ­ìõ©3/]ž6»ÖPööìõ؈@Û(:V ƒp×n„5•dœ ñ!]שëºN³t]†Ç’t]çßëºDÑÕÕï鮿Qt#•Ü:*®~:•QÉ% Ôó.ž²Òá.·±å©Ie± Kêo5”ÛNGÇÖo˜‘êÕ2•„Žk2ɹL‚.a’#Â.ïg/ÍÞ›]7äRfÂCí0Ëðén©ÖDZž‡X>ùÂË%“º‡ã,Õ/Ôïh”åéWjEXj‡…Y:”[I"ÒY”É'ÙW*.àaåIR¹õ´×EN1^±ÇwLÖG¦*rD)KSö¦hµ½þq@M”?”ö*-Úò7ƒ`â™VxÕ¥—îMý_¸¾Ž«Ù xh,A¢E| .acÊ%e¥}^&­cdoÂE¼@" ¯C°oãÚhY)°ü~{8S·[뤳%Oà¶÷}°PGÍœxnĨWÇi­×¢Ýؾ6x+Œ™™Ÿ_Úº5Øl0›…i4ŽîÜ$ô`À˜ÂD)b‚bQ’ul³³/ Ÿ“JС͜åÆÓØÁ(É5ÐfÑg¤SzÂaWÉF:nW@øœÀv†¡W^@˜er øÓqº­ð‹aú‹!Y|=XVÖŒƒÛ½ªŽšœÚb ük‘:Yõ*n×>ÞçKoÞ<Ýç‹Çß2ó«®í·tì²åË—]Úï(›|ûñ˜¨†»Œ;v\—Ž £bȈa)þïF¼öÿw#®wMÙ©AG2⎠:©3¯áz0â´ø¦MãÓ|ñÌ¿ðºà÷cºÏo½|Ù²å­çwø«âaÄþ@vAAvÀOFŒÑaxbÓ7 ÄêZV,Yík¸Â|Úi1H¦žŒD9“ö¨ÈÄ%x6T!ÄõŽ‹§¡â¨w”D·aêaROëPYYû£s^{ŸbžŽÞ³çåÌ¿~gŽ{ a=”7íC…½hM/ŸÙ¥”ØôÂ^6Ev$¿»F“jYE€RBd+ŒÅxDdu¯:H)P¢D«îEÁQzÜ]ÜKCÙªä7­j %ËÈP)„¬Xl)Š/Ktø¼%î­ŠXÄ)‚Hs{=>YR C¤QÈÐNêP—D© sjù."ó(O’**Ýÿ¬¡óáo(™)qÌ1Ü*’Ö·~B ± 1q”¥Û†9Êú²ù¨ áUjŠZøº î¾þh˜o†ž¦Ã%;cœÉ)1ÉɹÉù1“ bú'YCÓTE“`Ž6GÛ¸¡¬cŽ&ÒC;"-ÙžD#TØö“–ÐG½¬~;rì¸Éýß|àeÈKöÌa|88¹¬Œ½_ošqÿÄG?þEq@êq6d%¯èÖõiüß*¸S»$Ì¢ú?b,7¥B&²)Î8€¹ì­H€"'I–)ü£ C.ÚŽ„xÉâ)Šç’Ö¦Å(RE’kƒÈYé½Ó§§¯Oß _¯¤š^nü'ºƒ5h9Mœ­ÅtC1 ìáöc —?ÿrùÌ9ëúõ+Ÿ9ï±§ËË[ L€å>|°UËß¿Ž2fÿΑ˜ä—W÷3 îì>r_NvëÖóÓÇ3úÿ4ž¨{Ž'ö4ŒèÆÿ;†dÿeHýÊ ­=¦… öŽ (Vd·9$±·OŠî]/EJ+hL“7ª¨Ñ…$KDY‹,1b¬3% õcüž¢ú Ù²ôŠÔ€”¬õ0ð²{gOÏ^Ÿ½¾^Éþ4»:àl4ÍÉY5‹ L”ý„bé\= 0TE Ã8Ÿ.LÀÇ`´åpÀ@v5Œ¨¨µo'9 ˜Ý4 ӢЀTßà¦ÀpcPŸ€¡²ÏbíetINzˆRåâWxqæ:+0­ýE0’°“` ûBÊÇ‰ÂØwb™ÈC6â<ÓÍÒÜzäk HyÝCv=¶¸Ù‡kY¾+•y[@%ƒhVrõ4ƒ|œïGx¦…È!eæ StIÌiSùôëd­FÃGÀ†¬ãöÂʪ}#p_SS ˆz²ðýÈrd€õìL4O é8BTø3€è©H°+$ ¦(q=E†Œ@eE¸øÿ{Mµ“Žc À‰jÒY®°êA¶èÎ^öþªÍˆÕ"ie/ ò¢õWbË”çÚh#§Øû˜Xˆð A%†Êªƒ€„c€ˆ…eó;ÝjØÕtfƦb…™­o,>\ypË-¬¨75€>öÓ×–,y­jï·›7ËV è©Pç#ZBB2LœËÙ%`‰çA`$G”b¥ ËŠz}ö¸Qaš›ÅÚ¯RaŒKŒ°®À*ªZ$¶D¬V‘žåË™7j•EKÓ®<ôÙ¡ë+¸ƒWWð¦yÕçêKÏ<ø îX?åÖ|øw+…+Ü¡~ü„ºyçãë€DåñuËïŸòáµû@ ”¾Î·Ë‚εì€%µ£¨x2+NjÖpx gAÆ"k+ŠKZë)Kù#R9¬A£"O´Ì$0d£U—Q‚çAh1³ÄT¶sá”Ô.—Õ©º¤N)T9ÜŽIÄÍþ`¿YÙõþÌæ³O¾5l¶áÆpøÜÚ5,ë…'Ͼ‰pA= .½ÿXÞô'ç<¸çG¶¦ÃÃü/Á©ìD*m¡ê¯™õ0ÕV” Çí’…ÓjnhÖ#2ñ‹ð\Y2oPç1…¤€2×pWüÁLõ¯”¦¸åàÏ”—~Ëx:éâËLÏ;{CÆÿ­“­ÿS'{è¼ýûç_¿“N ¼z.ŒÄ á‰f{/ éÇÙü 8q[L <§=‘ãòÈ5]êÖòÚ=‡e›ZÃìNaÄÂc?ôÎWß§Üh0õ­ÚQNÀ-˜Dd7±àH5Tˆ§£ !©@!¶‚lj¥¯ì‰ÞZÃвvµY‡íJݧåº3•=¡Ø/¾\-Q7ªÆþh‡^ÈÇ¢©È|:ylQT™½"Ú¤@§rA4íTWöÏÒd­ß˜=1á~íÝÿ®gºOúN}áÉZDwÓ1s³C”¬@ˆ}(±¡”€È˜‘Ä‹²|íä,4"Ñ\dª€#‚£²;•5Á½! ܹ耦º0__9p€iKX=Õ "›F’Ykº²Jo ‘îÝ´f¦s3NoÐjšPª:n¥Z« 4¹G€‹å\%g‰¥ÂÄñk7 ŸÛʽ gw%³æ8¼L@#nümœ@œ'àøßââêÄ 8þ>NÀŸ];FÀ™5+ûð™ý]ö))Ç+>zzð·7æ-™³Œ±g€<Ëø¥ÍÕ4®p÷S“Öâ]êó{ö(¬3!.ƒ(ØX³Ï­ð ©Éƒº{Ä› ù¯ÓU*£#ó«lÛG¾Hº*ñ €÷ÝŠÅ8LÎŽ•§ÊKú,ìºÿÌáìYY“§¿ÿ‘º¨ües–Ìe1­ùR¯¾=b¶1fí¤§vãêó…=z¤Q–ÙóT[ho13VS¿ô4ÉÀˆÆ~iéií¼%ѧMié\˜šÄ“¶^& Nð_T‘#Ùh-’O;³Â©ã ó5¬"ÌñV }³åõN6ÂmVýHV¢þgvÀm°0vÖ-y,uð?Í>‘…*G‘}«‰²‡ ¿#@a?s"üóI‰z©¯y/¾¢äö}Aý¶ê#?s|Ë.šê·?1-Pøá»Üa:öÛOæ´ÍŠ¢~•ù… Î €ËªH<60™-Ék™SVI%¦ ±Tà:­Ÿ@˜%¯Acž#f¬>Fª:†0+ÕWqëuL!…« î]§¾®cþyâçÕð1‘W8¥"Ói§IÒ5kSGÔ¤„ÀÒë*]9Ó‰J‰/Ú}Ò…Ï^xRG­Š¡û5¥µÃx…üqÄFoÁ¾"‰\‰Ñéo3+L3' =£ˆÝ1^—a©é ׳³á\eW@'Ix'A·>ꦺ·™¹ E˜.cš—:¥"³{‚ç-ZwŒ™ˆÿT%ò ¶­çÀùñÔ ßsŠÚã¿àˆE+"-±ÑŠÁ™¡Xœ†…yål¥…iµà’8¹"ÁS^ 3àýÌþ™üSìOq—í—åOc? Ø2 ó1ƒ3Ÿ0"u“ü=0Û»¬¸¦ÚmZ¾Ÿ[qcôõ«Ï?W¿Z2úG|jüª#ë×Y5ãlEç¶ý ^ÇÉð£óëí>X³çõ×÷¬ù Ýub¯Aˆª{¨æ¤8½¼l– d «@üÖ*níå\T¹€áØ¥XéZ…é0j}>¥DsÂ7üñ Ÿ>wYjú„œõGºv™:qð39‰Œí満ËÍ\æmoœÍœÝýí}cã¶HYÞª"s( ‰4Îβ ox†šç0±uJ~…xŽÁ<‹D 9IÃŽCh“ÝŽ¸hŽ¢Ù„´>ëÞø˜9ðE‹¸œœ’ý\!èLÏUõGX« ô˜ñO›d0šÌ˜a9^0!Ñ‚º´÷ÖíˆB^äÅD» -’ýà ¹,k— E•Ô `äÖ,½"²ÑhbðÈb`ÀòØF7Àh0òÞ`0b;2#à8ŒÙŒ@Tá%Qì¢x/ÈÏðOíW+Xmä½¼—ihln,à 3ÙŸïÏ …’˜£ùÑÌ$ã\€…šËLgVñPõŸyÂxœ?μƿɽÊÄò¼Xc4›L©Ä{97ÍĘcLNƒKJS„$>ó³) €¢sš)Ùè7$I͸<6ÏÜLʲð¹6À¶cxHÀÜÁÔÁØÁ°¬ÅÂ@¾7Û‡é‹û¢Þæ~¦þƆ±â8á>~2é†Ìd<M56L¦Yçˆs„ùÅÜ|v1³/Á Ð4ßü i‰i¡q®a±TįbÖY·Š[…Íü&n'û³ oCÛÍÌ-ÒëÓâÓÂþ÷,û,S‚KгæÒóÖø£Üiö%¦ÌôŠõ,†«d/0‚´åO2aòçÄà!·ì?__ûÏ×e8åÚÏ¿\c'Wm!ß `Of·D¿°£‚ÁŠðûÍ "@·[=²^”-f;uÕðÔ°~’l'N4cÍv"ÛífZiD”¡lE•≠kûóFïÎ]IcýF™~|O}’{÷|ëV;’“ŠFÞ9Fµ|ŒÆ)S`ÇJà42o„Ý"!9 ¯PÄý㪠W„ºA,’eìØ·S[ê òêÈO‘1Ç_Ã)¸Ñ5uÄE®ÀÙ׫2¾¥å¸r¯P±±4¨Ø–aÁonog(1ž¶) °%6c/Na1â°ì¬ù*Gd¾Ê:D¦Hb:ZhU-€ÌÚ°˜vÞ¤Ø\QlŒ’¡$¸òØfJ–‹Âb`V[{e;D墰˜l!WÈQ–¸–fL#4,¬Ñ$×ZBk÷I™pêóCù-ÿ~†=$½10·fôF#?:°»9ŧ˜½0:3ÆžD^Íà<ƒóDîºVgÅâ…Ùêùå lL‰'Ìü` §ñè^mI²]á7¯~oòÚ’`øÞ oB ß›•¤ ¿½†î•¤ }ŒwIÒÒ¤ç“<ú°sB¡mn´id=&1m¢@'áùÄþm W4OÅ­û>4‰ÝΪ[Èdàê¦ ¦S²¿Sþ fƒ 3[å´lÑ&¦ê¼ ƒ°†ÈC¡KƘx—’(ÄÅ[‡²ÆÔ¯™úÝš©?Î[2õÇUø‘üÏvþ„èøÿ ;ØùÀΟ›vþ°ó'ü_ÛùápeZRpÑÚ6~Š=z˜dAãë]ô. ÿ¦ƒˆAÀ„Ü’Lðã0øfx·çÞØ¦nX‰´,Qȧž £e\ÿ÷Ýæá#è©÷MZ6®Ÿ´qÛÓ[ ‹Ï=Œw¿P q¼âŽ–%œLIU:k^\pv-‚rÆ8I,8Ëiœ0D(’WÚÌ&g¹i+ò%s7EÀ˜B d]!âH¼sðÅvÄ‚íØ³W ¶#Wõ³6Ø1Ã¥;Ö")æÈa%$‡çHQ´6(Â[cÁ÷R—(P{ÿz¦ ªxTý‘RÕ³Wˆ*æW&åï¨R»UÄÙðdJ´¤((¼£×©…E6I–"nkrtQŒ»(þt2µ—Ê´V‰£îÌPÓvN-HÚÁtŠtøË#ã›7Ò8éª?®žÊjÁ_Þù¬ÖRFTÚ± ’ÜŠ0t©§ÊÁè Š¢/8TMGFÌ ‰ YÚLHÄ=ÕΤ .ÃXMÂÍñ|Zõñ)BÃ|×(þ5Êlç÷3Õ2õkÈð*Ãq4ŽÁ7ÔaênZòs?¥cCµ¦nåsÕ‰k°6†0C ÄÙ”=L‘öl(ÌXbÈr”'ÖŠK7"cÀèqQ;,-›ZƒÂtœ×± CûÙOl^ݹgÿ[ß8vl¡ú;½u«ú¾úÆMU}tÑô–-gÎ`¯7j(hØ`[NÓÁC›6=íò¥™S÷îÞ¥Þ5óõ“‡îßÕ60â±^=µ þ"l‚4Ô)¦ÄØ}Q%®­éˆOÁ%lJ¹d÷­Š§Á>é ’bšp‰ì“ì2=؇Z<ïó¡‹ÿ}ôOþ¤ÆZxÁ߯Ùl~ª”«_ë¡@úò,£Ës™$ÜQ=º ‡E ŠÐ»ÁÁsnÅÆÅŠ˜—G«1ß7ìtÍæoвœæ*ÐQƒÐLÀê¯0Eƒ´À{¤Z-‚2í$+ôZYYÙ §•ünD~M*›ƒ—ŒÊ€ªKß]¼³wÍüN…_ZÓ®mVµ§vçvÒ¸/„AN¢rO·³<øï˜¾Áâ[L³rx ·ª±Œ™éü˜IÒâOØŸáC<Š XÐ+ààE–à  1U’X‚¸ê X§Ê˜%Uoœ˜³I¥ûSPô$€¸P@™mmÒ锸"ßÖÿŸØ )Jd°4ñ`鵋ûDÆE×ŠóŽ ¤Æ™¡èhfVDœ·¶˜å5IÁÑaÞoЕüë£昌ºL©p€ÄGK Èš쑬ÔäöE'!¹’ÔívÒÓ$9¦ÄOΔx8SâCœH@ÿÓÁ⪉(ùÛæ›o.|ýÕ½™+ÔØy å=æT91VJ"g ZJûÙã ‰(I'2y&®$ÙŸDŸ$¶$6éÞ¤;ÙòïN XÊøã›{D­cÈ»r/¦¯î ´ë@ghdF*A¢ŠfJ¡1ØOr›î‚pÂ83’‰I(c'ù¶jkôîZØM¿0;˜Û€L™‘jwò6Œ@mêQõ+ÒmÛ]"É—MHMI8~ëöJ úG‰Š‹ ŸZVFŠH8+mÝn1 öRUN$…ã(Ùjò½…±÷"H³p7ˆ¯Ó9µ¢Ô¼î¬â"(+dDþ$MD¾ÇH1MjOó‡Ìöàv"Ñ-ÿý%Æê±zT€ÞUgœ8è<\0¸Ö¼ªºÍ A'ü~š ÖµGw{IÞ0F+`#øÝä'µžÑ#j[›=uÌÓ¢Põ‘¶”ÔYLÀFÓ­ÏÝCê7ž\Ô]ý+xöjŸ>áÒ>ii 4éÒ£¾n„Ž«±T7غzã}­{÷Ò+þ0Ä8ÈK0»©(“Ò˜EÐÙépF€ %…IbìÚH€¶ùƒúœ»gjÓ§/±ø¦¦ž¦”09Œº ¨&õ|aØÀíéé` Öãœ!nWáý¸˜ŒX8ÿ³Õ5C´«ý¤Êñ˜°›P¢k&ŠóÚ}<Áƨ!qa1ª?ÍcÇJ½ÞÆø¾n»úwa¶ ê”^¶E»~£\ñ?<3Wß_ºFÝ®S¿ Ø®ÀÃîv͘©ŽŸixö÷¹õâ”ìîl¯¶5£ø+J4¥uh4ìú‹OhÃÁ(Ò˧ÁÔæ’¡„g2"†Á¯±Ëˆ)Å~øMWû„i¡ Ø ¾W~äÈ AÄÌœ–Š2/ê`>Ìp ˆëÀþ«*“ýT+@'~¬ê£Y ù¶T.ApÒýO;ñ5xþiçõœ·WþãvC µV~ý‘=°XxÖ$°9~­ZxY¶ž EcJ˜ ¾âæ(x‚†˜Å“:®böˆ@Ìj2cNå:q]Ì‚Åb³Eñ±– @if¡%_HÁ[{KG~?ÄRH, B¡XhcÏ?o{Åö©-š(Úa¾ì°Ãhì·ŒÙ!85þÊ^Ò™j^õìw|<2#/™ÂLþ³ÄâÄF¾É›½aýê ëg—¯Ú¶}Uyäf€êÁß‘ïàc TùVµß“>Ð!Ú‡ˆl(¼å`7` ±ÉôH»ÒR+•>J$™`²d$_ˆ~ú’å û*â;=¾.8eö ýƼz‡›©ý¤¾ù®ÜzX/é-¤§Mâ ŒÑÐfhU¥1k£ E¦­îЍÓÑ2¶ Ȳ)4ª+Â+!‡âØHL«æ†'q®åϽõÓ·ÌYj£ ¶SÏ@|ðƒÝЂø¹‚“–¡´Àñí Q¢€–B£$$„D_QÜiwE¢¬åéþ3‚n:J«K×{bvÁÉÝü=Ⱥóøæñ;ËIÅ„9ÌNæ¬@‚¶³¢ A,’‰ø¨ZfVV‚‰¸Ïà£.°5>¶Ü3( v3CþÞü÷Tå­[°š9;»gÏ‘÷Ï™SoF×spÞÌ2dÊpu7;A}¶iÓ•]Æ æT Û_ŠE,”s1ùHÇ•5=Çj=Ç’žEÁk·1䎖W*¡ï!ÿ "bÂ̽ÛeÝE ³bþ”Aygj˜êïð!t¨± ;J ˆ–ˆ¥ÿ¡g«I"µV‚¿$èHfGγ›äÌlß.*jölÞ\>?/opLÌ@vær}° Çx»I”hц‘]d­)“ÄBÀî¼`ˆt3iΜä9ýͱ ÷¼Ñ0cŠz’™5ºY>ÃBã™ÇðW¼ 9! &;´M‰~ÙY«hȬ¤så RODÅÞԴδÝÙ³µv™*Ò.î|¬¦]|ˆ™íÆ ´“±VˆóB>Òòõë„r Ša£$ƒ3³%íÑGØ”ÉÔê,«]|ƒ™êöh˜7øþù&­c%Àl­ãm´d+ÒYt†VÉd·Á9$.B„ÃO´/hßaõ¯³§´JII¿pªßŸ¨~°¸[Z½ziݳßÙz¶Ö°!3KýÃål³ïÙܸ¸jä‰ÊlEÊîä1; Ù!Çßò6ÞV‡}a” “s•N:ÂÝ q´¼§qf£ž#:dEÇ$¸Ô/~|}!3fzbÒ §³[&3Iuãï Š87ï\p•ÒÞ³‚NÞË"+ãÙôúaö;ü†»kཬŠ? ï{Ðø©³ÜMüðâ‘¢Õw#e— ®œ¸}iX§ b3rñÖ¾Ë=îÓÀsdS²?á"`HVÔø9³M¾~¢¹—™Qyý$e1AdGÃIÌ!QÚjtýt8¢¬t®¦Û RX9Áƒ³z÷êÕ›ýiáà@›Á {÷úOïÞu;‘Lœ 6ùuPM9d’9ÙÅÍLíÄj‚Œvr%Ô ‘‰ñÆ¥©ý¸ˆö0‹ôÅþÔ»÷zõ^8¸M`°þ|­äC gqG[Q,”õ¾~½R¯ž‹Õ øàª¾]ï¥æe‡í‡Î´ó“qR› ç6ÎÊÉÉ=<¨C‡‚)K¹aúùÝ’fsT!$º .˜8*ÐÞ®nªEFÀч¢l(JDl,ôu½R>«ÂÂÈmNIHØl˜s—Ù¿dJ§ÊÍÉÉjœÝ8{Fëdœ’÷ 7Lm‹°·ŒšX0ØhpF™Íänùõc—Íü&AB8ÅeÁ®lÂß«ÛñÕÍ¿ôÛš'ÖüVç6>·!Îå™ß`wN€¿çÓB·¡×¸Ela<ÜÖêÇÌD°€ÀX^àxaɲOÈŒÌ3#še! 1Õ ‡ÓÄ£H|Jð u qŽƒº­Nn¦M ãµ¾jõÐýËÏÁ¡XÄaÒCË+Çd„ÿOí̹¨¹Ë4縀/–>Ðø¤6nQhh—7õ.ófÉ Jxp´¦åÿ0œ41r8«~û­nË,:A†1(ÍÿSË€@’A÷M<ÿ·ßÔUpÏûì7x¤  8«XX29G–ÉW[o˜°-[À_{hšQ’®½¾e ‹¶lQ¯`›7U3§ØÈDîÉ!F*W`’á[>ûlËgÌx…ÿäFü)“ÊÌ!3MÜ=LªÚ¿Ž?%:Ï×c› ‘0¯­¦[œˆŽ[¡f$ˆ¬°qI!òÞÐv.lZ+Ý´VdÕ7-\·õ§Á4î€ÜÚzXGŽ–šŸCe‚ósM£ ,´Ì5îÞ°a·Ð¹[·®Ý×­_0ÞÁw`Æ 2Ù3``ÞF—8 ó+Sª(Fº@úàhâô>ðK·þG¬ÝîýG˜¼£²R+ƒnrŸ³Š( i ØÙ×\¿^³ý…šç •(èP/a<× jh£¦M2ÛÚÔ60xÁžY³öoþ×6ƒÁE8÷î%ׯÝÕ9 è‡È=ÙLV£ø_ë¯b’K³ƒ$M"aaëBP ÷ý9wWÆeófäç%´ŽOí·`Vn«ÁñÙlƒFdM‚†Nò±PI Å@BH2RD«‰ÃŠh3qʯÈq™~ôÙ°‰®îy°¨s}4¹Þ®'vmšaÖ¬æˆ?™é<úèRûàKçOiìB’޶¹¬¸ª™ÛoIM†d³ñÃR)µÞñCóŽ.}‹6ùü¡_ƒn[:LYˆÚìs‚CÇ%NdY^”@#€ðd KçžÙµ¹§D©“à+Ä—Ù$¾™ªc-+ýt5ápăpu5þ*™HȆ›Rcw8ÃÿvÜPúþ~hý' QÉâÄ /œ1üÄÉ“ÜÍÑ£_:]Xxš`—ÍÁÏ2A œîBƒTA<ù”¯cØFÄTÇðÇ~Q{áöêW^\­V¬^µŒ½þ׉+^ÛŸ}¦>zx[«¡ëÌNª›°Èr”a3a£El&-¾ZÇl­Ú®ËëùL” ½Qù¡.Âg>œ3q„ÈÁLnœÛ¤Ù ŒŒøø鋳›ô챪YëVmZ·ê;8Á7-ýþ¬¬NËÍšñ–Ñn“ äçd§K‘¢êgô”“c4:¦ÄÙlÙ ë{œÉU¯~—ÁY ’FëçmtÞXLK2¬³›W_º´+wV¿ôØï¨/ÿþ;îŒ3>ý´º:$IɳŒÊ¸ï#£øÁˆG.ðJX„$Žˆ» àI~4È«m™NÜåU·F|FDi¨6\œÉeb(Œ\Âñq’ÀHÐÍ–@‰z+9´üÊ*vç*ܤNSÞ€éÞŸ®ûaf"ü¿›ŽéÜt µ{1¿XàJðÿBn¤@Su§•(+.4 ÊAíï¹Lñª`ù*îò탫H…¸à ¶Mõûpè<gñtr:s_°Â\%JK",Õ ö€=Ä>TѸ©:Y5ƒOî€Oæ.£Îe»V¯G~@Ø´*ØŽ Ñcæç&Fò“€ ¹ v+ÝÅÊ»ÏÄø3ÑV°sü ›¹'‡©ÕJ÷qdâ+å3v9œÖ ¬¦îõx¾Ó¨'¶úü¿#¦ê ·P-¢<‡#Uó1ˆ\ϱ0OÅ4x]›Ïp”Fq:â” >@%å¿r7« ãáÊ.2å°öç`.Ú‡×^×*Ê$ªX„×þV-Äà)M·èR.fÁè+¸já ̸õ,ÎgjªÚa•pPí€+€À[ÜjY8(§1ÕMB+P%%ts÷YÎ ,ùÿë³,~øÚ.ýÉ —WªæðÚwâ@R!Ü®fÿáÐŽDfõ‚&ÅfRö„ñ‡ÄÌ–À±Ù¤)&ÁýNXʼn»ùz5Ò¨Ï~WÝO'èÉ–Á¢vº$GŽúË{–q¦ ÌÜzîuu¹ðаœ”ë&€Àêò_¦Oÿ…?£V/dW«unÊ… èÌw%×ÿ<Þ¤®f"\ý ·Q&p~ÎvAQWÁX?U‹ª"cwÔ?ª®Î~\Ý›^™M®@ɪSmYý6Õ€’®<»wïüùê ó÷©pÇY¸ãºv¯) pÇ|¸%{ßü}0ÚüJÔFœŽx蟜BI]ÔÝøûU·«ˆÖ¬Þb§ …ÜgÈq •³£Ìp$1]D=ØùzöuòÀðFî5dÏ<[sVƒµ—–£g¨¤ª·j‹²ìµ] x©ª¨«_Fä Á8µRy…×DËk 8Ót¢á?™j:—ÐÔ*hŠ00ª¹‰ fÕªUð—0¢Ëh)Œ ¥¹"Y!oØÒͺ»k3Ü>n¿¤Ý^[h Î#"+¤íôé»TW ߇û÷««Õ¡Õ³¨> ´¦ú˜«®ª««ÓªûÁ†™xGý«=°i˜õ L.ÑâRÕ' ¸^cA'‚ þ{ H–—F&:-ïÁECaÖá[ý>5Lx^êÐÅdöM“aÖÖU÷×#iŸÃÂìÂcVIØÐIêõ5­äŠv°»‡®0O+„GA;ª÷ÖâQ˜Í„¶4U¨ÎÆkÜR{W{ªwuít!íß­VX=^=ùúë¸+Ü1VmV½­z0A|Å0¶In‘0rÜxaÄÄ ÚröGö9Ö‰÷Ùæš'·Íµ]Ÿ­®é‹£vË–b¼EýÜžSÕí«×’²µUÜ_©(­†ŸÕ£Œ4F¬ë¢„{ÁG[Ñ®¼£þ_~ho\=XcxÉÕ<¸2€X²ÿ±©y:c#G=´Ñv=êÿò‘£V!“Øm˜ÇBG½‹œês2P…ÂWªoÂáßM[‘;kñêC¬™`ݲ\xª®æj. Méè×m]¯¡3ܦrz=Ö¥çÆ~¹ãºš]³G¬f͇[äç9T‹6‡[æ¹ãyc~+Xêùøl5HÑ”³:?x Ð{ |ûGøC5ý}°nÿÈ+ø,ùˉÕû«+¸ ºóÓ°çΛ…ìo£Föä*{ÁÇ”êSÕOè·œÀ{Ç ø!ÐÙsð$,ÿ6ƒ^³ˆ¯ ô)à5°ZÝþ|.ÄfÚEp™ªŸÕaÕQÕOQ¢g€Vý¼»V«Ãp××_côÃ>þ²ú}ÄRV .Lœ›äìÖ¸ë»õáîz÷ÝÚ»”‡ÇÍát% #)5 ^”üì Úvé\Ë„ñò\“¾Ko÷Uÿ£oR‡apÜÕ¯…C”e‰iPH„ÔÓðˆª´¸pqè??=â ªÀ½^ ¼‹b¢¾sjŽÐTJʽÉêÒ¬µ½ß·©¨oƒYɯù?ïËòõîÃLeb|F³ÍšQû-ÙH\s*E¦iR¤&Mª/““Ž»Edsí¤£l`H½é•ÙôÊ=äÔÐé* ½ã¾ zŒø®,²1U2½ã u º¿ú\q’+0¹W¡• ¡ YVãþ«[Ô÷·0ùXb€ƒuŠW³Õϫ˴uÂzpõsºBä;{þ–ùp› –óÊ–èÉjOÂäìuA#-[(ORg# ­9¡µ÷ÉñÊSÏu˜òÔ+dÅA'a ]A|Jƒ¾ÀÀPì'•˜êÀý¸ƒÕí˜brGj> qþvS§Xœµ¯ñÕJ VcrJߢ®]¬–Ø M–šwˆ…¾WCß•°à~ReV7òÄâ¤&š¦ÃÄifRÀAΖªfÞxsrúâ “[e5ó$ÚÒ3ú'¶øvË–w·lá'ú­¦ì†Â}så˜8‹9§ž0nžÜh Ó~Ku5ôxz\/'ÊÑ‹2 ¯=C÷|zõ®êב…Ì@$¤ÁTøÁä"ªÓW¨ÞKž~É üÍŠ‡ž^Mâ‰K—È7LnLî¥êH¢³˜›ïq¥©ÙãÈ ª“ÞþÐ ÂV¸ÏÕU¢€d¸M‡£.‚J¦® ÔoР~`fÏÞ½{ÎÖàú m32ðBÜ«÷ `-a½\!H–’"™@îzYÙókØ]Ðû×ÕoÃŽ¼ê²?¢#:Ê"¦âZÊÊÍÓ<—ÁùZ'´CaÍBÜzè=ed´ 4¨õ¹ñªo 2´¸=¸!X¢õªR=­ú$"y.‘;»5NMòó²Ý$¼Ðšäe+yMàšÓ4äöÊeò{åþÑú…E‹N¶j}^Õ¼¼^¹¹ê™ÜÜù¹´Yt|qÛÀâã‹Ô·ózäÁí0¾éj »¾úxgU½…¹"La׋Wµ+Ro„áÊpeŠfFvù«¦‹ ~&NÙLþ¤¾ ïO™lÍŸž#RL}Á¼/¦¡$Iõž0•º{[ïëÕ ù¾Ü~?7cú›ÃÚ4J¯ŒnÖ‰ ]ºæ›crV'º‡é›ÝèJ7TNºg“‘qóÔtvl,*Ó•”_5O˜¥í õ Ý+zÚÄÇjm§•Ù’/s…ð¾=i‡ü]ðñW ? Dy‹ïwA]yQ] EŽ«~eÆÜ¾Êgwk÷s÷Eܯ½Åî,+‡uîÖ=Ã0…ö€€Ç Ž¦ð‰‰ø³ö¶}y*Üò‹j%7«ñÔY»™a9Ä„oNa“xÜÝYµâ_BeŒi‘‘gkÊ?Hc /RÑIu ³±ú}ðȴº¸ÉlÜí,6š­Iê@t£¯¿¶šaŠC0Üð8C…é¶œ†æá^Õ+d¹EÜ×Ñãp=tÿîjŸá’ùÈ{Råo"6(”QÇcÕ8` †°¢è®€ÿņvqAgðl_ñ]%MD$î"z¸9Q@|^Ú#’Ø~”d“˜Y¡‚,¤n7­~„ÌQ %ùßLg|˜-çQ’¿ûæè÷ͦžûzè{ú—ÿ"$ÄëYJÿ’¾¢ù!f±þ—ô/ õÏüºXÿËÚkN:¹þ9´µA¿>•~¢).Õê‹Ã_xý/é'Zæ…Š /ß…ë1ÕH@¶» … Í„ª Ò½yƒ¾O ímZöŠG^pŒãeHd8æjBÈaÞi¹ý-v_°µôöUÄ„šE qC]HTÌ-ÃŬ$°"xÌÈçBn ²óËØGvÛY–ãE‚[ /’|ìj~ÖŽ©Pdz¹wÊ™æ_3Íxë¡xMF¨z4%8•`8ôOß· ˆoAß×C(œBûý"ïãŠÙbt(ÑT½61IÆ6¯;r€L€ÏB:º¶¿ô¤|ÂâHÊ…]F„CÙeà^ ˆâdë0>øóz°5s}ÙrÌ^¿¾léÒe×IîHEÉ©8}º‚^ʬ€ÀÁJ›ÈtJÅ& ‡ <’8ÎLíÁCK-Óáiduø#œÍœ6ƒd¾ª ô{F)k-½³¤úÓ—‹Öº¬µœì±šýú¥§qÍ~]CykAÀÞÎ(‹ËWÌZ`VDJÆZŸæÜkSc²ó½ë· ¿îª¨Øõëîq× Yoœ›5È?Ͻ?«d÷ʽ؆mûŠÜ{öxVîS¡ÀѾ•ž]¤h̓ԌÎ4οa8±P-Ú#[Šc—&;Þås3V_ŒMò‰q`Š$ôÒ*ýÈ›}˜øüyiõ¼²“cð‚€ÌÀI/x˜×1~¿¢Œ¿º|꣺ü*_Vñ>Æ+¼[‰'âíêõ?ÖZÙŸ«lÖµðàÐ+Û*ßí|Hê $½Ãœ 'c*ŽÛÊ*®bÆÉŠÞt%æD+V¯+ZN£0À´Š>$7(²mV ,t^"Ñé…+~˜XZw¤R‘'xðèØ±+V|ø¡úÇØqÇ‚q›Ý]:¿ú nŒ3_{µS§}ÇŽýôÓnmØ ~üÓ…§Ÿ}ìØšââ2ÜúܹsgÕ‹¯oÞü̧€3Xì Ô<Íf’ŠÝÀõ’NXA‚ŠÝ¡ ¤ÝÁÍ w4¥w˜‘ÜAØo¸¯„ñJ*Ûòúè©50ì4v,º7 ³ë°¯ôd@oР+B:Þé6ÚFwz’DWÐëÅ VŸœì`ÚvL ܨõÈ=ˆ,ÀÞ ˜»Ø@öˆ³–»˜„4ŒmäL9^f×ÉxÄ °+v[^¯Ìk*÷¯µU¸‹HeQÊÀCÄœ±vd·ÂMvÙ?‘=~å–7nÜèÆ`Ê7­ pÑxµe©Ì8º8º(]|]c{Zº£ž¸‹µ›Ý0"`onm¦4·7mjoci‰ZciDÀ”aÉÀõQ=k=;|ÎâµEYc,18yíÄòqñÌð/EdΡÊã®1«¥é¨]&™?lÏ ­lÕ£[÷Wv>0öÀ¥tCR½„ÁSGö¯go4²ÁºfÎYòØ8Ÿ1|óãÔGO^Ÿ4å*þ§÷ý»q½¬ýëž6¨Þ¶]ØSÝÓù*,Ê*Á¬4¥ÓI?qP«ÄäF>‰ÕH‰é+Í0ºœ7¸ÊÝörËZæt,A»RÕìÊ n#û`Ò8ø™•Úàw>7ÁÞ$) OËq)>öŽRã® ™v'¨YxÊ÷ꎪ]`ñ›êõ渡PÅ(w=‡T´»þ­¾‰0J …)[ ʬ(‘¨À+W4ÔU‘1¿"œæDxz AŸ(Ø.‰ A¨²Z0,"ìP` àˆ (Ø¡åéc-TOú <£¶(S?j0ªqÁ9à¦ÝÙqwˆðZÊvxh·ž9‡¿E¢çPÿ€Sàӹ̎¾tÅØGá”(?ä5Ò3I-1â7ÎÒ,<˜%Oa|ˆ)N)·®m$“I¢hfÍ‚;¶/ð¢US†%w9yWÒ¦UÕseHÀñ×TH*{cüOA,]žóøêÕ·~ztõêÜn;qùÇóÕ¨ù ,ÞôܰÑx{ÁœáY)Ó‹ÔÔ?vîÚµ;*Ÿ $÷z Ëjzl—•gÎúöåó…L¡r (D¤DóJ«‘ÊrbJ¢rÒV¦¯J*o°6§‰×”攸x)«c/ Õ¯¹¢—° ûîõhÁ{'„†Ê¤Ò’54Ãól÷Ò1£{÷éT;!tÄ€†öÐûæ²e2ÃõϨ¨gb-–Z ¡Åô¬Ïj6 2Aa8SԾ쟩¸Ø:`2Z•xÎå„U¨”‰*ˆá–û€CÀMdCC½aT®¬‹öEm#1vð}6t(H¡I®[çÇ ¬LÒIñ  +zõïÚûÉŽõâ›÷c¦8µÇ,ÁÝzÍŸÕ{P·î|ƒ¤”Üõ™|)/W7«iê¤ò5·¹äøú¦è›Ìi“§€•ÏéÖàxjÊ-Al×^¨<®#ƒÞ]z:¼]^“™rç×¹veiš%÷TèN¦G¿0ªb,s˜ˆ‹í‰|9‘9;"ƒâîƒíJ\G ¶ ›Ú ›:[<¯Ä6öx»ÍÎŽ¸!k ‚››ÇíìHó+Ú ;mñYL+·„ù{4ø°­ãÀâ-ýº|y7mïpàV·Vÿ[?¯Z½zó†zuÉÎ]â·°oÜî©Ủ+頻k®\½zÏÁsß»‚pYPL@´ H²XC8̼¥‚/’`ÚÀ?Y‡ML¾È´}ûb®0˜±¡ø%Æ\a.@s¯ Ä=B¡µ» 3` ìL¹m’E¢)³%qKvHfDN“xÇÚ9Æn0p¢ÌÝù†–-Eø!K€ bÇ9$fìü¯¼Œ¿üLí„Á@7fõË\¡š‰ÿ¥ 0í–©m"ÐP, ,ò6lâÁÈ«õ,œ †§t еc$pv^ ýÛÉ;ÄP€񴇮 þ†Þ9ôKº¦¹„G³€áÏ€ÈÿÐüC×Ì)…s¾àï»ýÛ²ê,—ýÆC»¶ºšÂÓMtn4‡JU]¤(Z”¿]@„ƒºqQ4œ3á ¤¢¸…µ»ìϱ¢N®!pZxòâÇž…$‰¥å4÷Å{ÕÒ7°þ¸îå›|ªc£ú«Ž.„Ôßþ‹ž[6=wܼ‡‡,šÙóìó_ÍÖjÓl¹@Àu:—ëKP¢ šxšLKÄÀ¢?ÎiĸH&ÐÏDøkö÷µ]NI¶7¡(Q v'ïJv(ÌGµžï ¸‡úíõˬ+¸†‘ƒo0ø®Ç¿€ NÆÜ¹¾ð¨üŠ¡Ô­V¨?ÃÜê8²´>ÛêkÜ6˜[Ê„'Ȉ‰uHÈ•hhÅ„Y,²®u±Oªpå6[C#ÆàZQxZ&[W®ñcóm Ĥ®)ó‡«c+Öq…eÁÞÛðø‹ùá#ÌΪÁìAÄPÎs€pÆïñº HS\L¹ÊÇÅwT±EŠRdŠ|~ eÊBº,a¸paD|dU5g«­-NÖÔLc3BµÔØmã‡&…륳ï|óq¨Â†lÙ¾´ZY2 K‰’jè,+¬–Ð]y¥ vœ=¦<ÖQŽÖ¦ ¥\(K!KJ‰"Þ„{㨻ÆÌAõ¿Zb~Mµçî]³À±¡¤üšJjÝk¯®. ¼O¹NLž)ÁfT}8%¦#OƒÙràp.’Å+÷¬·¡rkE°×fìÛèqûöÔY/¿õÍ·o¾¸`êÆ²«?j:v=±ŒÆØ"î®fI k“ÌFE†'žÏÉf*‚³¥œ'ên6lý;„ˆ,šËüGgŠêpΠªÔ˜¢RVüršðÍÒc\Gƒâ@’ „ D(­hhã`GNeñUðåèÔB ?£îŸ¦ñá¾ÙùS¶Lja“âà¡/hXéNõø}c(S. t¼<÷¥ñKç=UY+ÉCNDTÅ—›âÊê³iE÷Z×éúò Š+L^`\öˆÒDôx‚­¬/C¾+$rÙe¶UÛQ£ ,È[»dõêe_<¿rÕªñO·k>p`éìÅ Á¾G|zyú°]+S¯©?ïÛ¦NŶ“¥‰Ûø€ïIOÏY/_˜5y7BµPÀ‘?yÐa°_ÉØÂú›5D‘´±–Ñê¨×&ÃæqÐ?ºlR‰ÍPå,q1§£¼#ߌûHŠn¯²›í«©È‹ÖÆœöÕ,aË,>5¢RÛé;š!ÃtüfÊçt]ôâÃíœzxîõ½)£çý—Y|èÅîí¬ìÕªÖvýŸg–çWM›VKŒŠÀº™F᪚œŠâðu”—I2‹ Ã+Ó1†–BÔ «iøˆÁ¤pVC‘‡*íÖ,ÈÞđø óÑžC^š°9Š ´!–aRÕÑO¼±î“‹¥?Y÷Æêhõ–¿^,Ó÷P_½¥öb¢ƒÿÁGo‘w?ã õªn&],ÈD¶!QÝ(¬ ² ÆâX¤D%0š¢½˜\íq/½š¾—–ªÙÃ55Ñ Vê@bÑÓrÎìØo`@„gÀf7æ‚1l´ºq%–ƒp"º¹ÁÎU_‚š´‚ù-رè,{€`iD1yÅÊû¢ëuLõ«ô9 Ò‰(“wåX›h<‚>¡>*KO„’téÀ-·¦‡ÊÒlg‹cGP âl²¶yD”Ä”ƒ€a“ÉÏóÄa…¨2e2àk“z_ìð ®\0"ßü½ÅCªuÍ4fþÊw|ÄŒ¸3äÓæ½¯…±Ó¹V÷oúäÔýêùg¾[>êêÕƒlƒ—¯bü…úõÉ-Nõû¥Gv6üUút1Ùˆâ‰6lB’¿£K”l®hÉÛ1SIöCUK²K¹"ek*2Ź×F'h²ë»§æÊJxb%îJ…‚üx\²’ °‡·+î7ÿÁ®Ø¡V>üÞåç?úMýJMR«8‚3Ÿb§ÿþPéê¡¥CóKÛNÂy{?QÇUU©÷á“ø]¼²Ó¸ÂhB|þS”@äÖDãä|6ÑÉ[UÍ®Ÿ=Ñb˜äQ²‹œ“·ÛâQ±¥ØeßÀgþ 5žoT^¡ÚjÀ0-~ ‘'ž&=V/ B|"$ e{8‡’’WSç“+ØÛ·þÅú}÷ö˜—?mnßy ŽOU¿ŸÝÛíÞÞYýïcOç2>õ¶tN2”-YêL¼°½!!0¬^f;Ýk°¯–×à>Ü›nN: ÝÒtÏM{5YêèìÈ…â`{µ“e´"pÀ*'$+rÇÚ&Ë€—P„eéîø"ƒy­k«ét:§†Kj·´ßÍùsìýÄJbí°ãˆé…©:ÿÈêÕœ§¬åùgôèñÀ3^œÑ}ƺ Nuìöêp(oW§éÜß±s;Þu{Þ„YÞœ3¡ªŒíúø»ÌYð¡¼G)Çù.šŸåÀÝÏ¢ÁÑOìðþŽFŇ¼¢U±sŠ«#%ûX–ÉÍš]×Ë%¦l‚XnµƒÈž@ÏÄl)ט‰Xtº©³%}ÖæšyhàÀf¥/OMûç[XY=™ýtÖluêìYß¾y~öœ*H0c“MìܹS+@±s!Âh h_ÏÂá 6v£âVRÍIœ/€…h ½¹X‘Ào•‚Š—¥È!Ñësv·<ž¤;©êa[ þ·pá°Þ\0$¬p©¯>^\þ(žïã3æmS/©/,ÙÖ¿¶H~ñ}|îùãÑRM‰àúPLÅ& SÅ*‰Ñ :Ò¦$³.Cèô”{m Iø  Eøkèä)¶Ðâÿ;õaB¥¦=àCê°ãoœ›ÖBºCüÎóoߥ7辡$ê±H#Gmu@ÈP(¼ PDI@é„i·¤` Nw‘ÃXdpX<=¡(>¹(ætz­®^‰£³nR®‹º> Å‘`z7W–@rnNr½z æ7ï†Ûª¯¬s¨YNN£Æ€ž<(З[¾÷ñqƒrÀgµµî3M}k]°ÍƧfMîñ²½Û(R¬¥ºŠ»K2,°Å|ô‘|X¥Hr­™¼©¬ùû´˜vÿ§Ï(ÇŽ ðœþ;W¤ÃÖ¿?étAß“ø#u³^§Û]V ”mÄŒÀ³¢€8 æ‚0¾“íØ‘ߊ¡º¾æ=”Y;1/ ,Ú9ÄV^xé+I|p€¡åV©ß”}LÅÞ²/ØéUë™7ØÁÐÛL@…)äoÑü}Zõ(m‡r~‚Z Õ‚€i¶ÁP€’éSziâe¼_ûñýß?Ç\ß¾£—˜“/]Vdñ̓k™.Ý&½ñŽú¶z#øº:X݈»ã ø_‹3{4¹|ÃÏ8ÿX÷æ Óyêzn 7¹Éô%…“ÈQ¦}'#™úS9gÍV Ê™ø1*Þà ùì¹Ù qµ¿šÔ#x™9²Ä¿âÀ‡oª½ÕOÕÕx$)ë1Kí&ò°.­ 1Ò×Ò‚e סœ 2—=Ã× ÑçSb½rëðÚ ’ˆlr¦òˆ¡åò4Ø RëŒf±ËZ[R»Ö¶¦ùÅp¾ÀH_Äw'ð‚˜Fl7©ˆ&ÃÂI£í3¡ýJåVUD¾U‡ŸPoÌ];úÑ/˜Ða\ZÚ¸û6¾÷àÂq-FüÕ¿¨}ç.‹gvît¾ `öʶÚõ¿=ªé8æ3õ§‹ÿyzÕ¸úžG¢²ú,õ´yèÐÈ‘GOàmoaëž%ê+{^T/My`ÒÄ7~þù‰“˜‡Ó^܃Û,ß©þ@DyuåSRÜ+û½nÅ“ÖÑ© /c“dÈ Š$tL½ç”$¶Bf$ÑŸjsK©H¸Èµ©¡9Kfį²TÒg*ÂÍ&Ûñî”éÃú¬9±Eý‘i®ë[1ÆWÔ †Á5=à_+~ݽ°KŠ|¿)£ ]ë|gþuqm>»q>¯V£ôzKê“íZë²8©ØEL9Ô^æêŸ éâ^ªõE9¯#/…¹q¢ð‘©cΜ3öÚµ'–V£ûOmÅé|WeE¹zó¯g·mÛŽMÌ?—1ëWÚ\ýwÂyXºŠ¦g×Z8»|„&+L®“¢:ÆFÒTä[k©ˆ÷:×ZOÇÖ¢Éå¤Çh­òcÔsÀ¬ÙÑ{æÈ~¸{ÿþeõlýG¬Þ½$È­c»|zÙìY³/2¡²e_‚úuðÏ4 ò-µÿ< )€hÐh—/­#ë÷%p©ºâqý,¬ðQ‹Y¦klò£2û;°,.ó«ªJz‚R·jxéWnD1ªg²ÝqÚÑ—[¶/=ósy—Ñ+_Û¶ªAÞC³.]œ5ãÂ…ž“&q ¿®+÷[º©c®ùwö L\òê®-+—=ô‰šªÓŽ¢vᎂn¶’*<¤®!¦ˆ!_º;d;IÑáQ)i”áë#ôæ´Tú¥.Õüyùm°ä±øüØ”XÙj¶EE'%Äy;µè’ŒM÷/™¦þñØØn¹mûô/µ¼o›a»zuk_0g|§z³Yõâbãµî}ßÈw×7­¿*Ÿí°ÍmòG'Äø\>;¸¾¼ÎÆQÙ¯^9²sU£¤¶›º7J9dÉÚ^O“ŒTÅÄèÎMÚµhÑ*³MbÆž{_L/ع&þwÂsyΟ€%?dç× ga>w»,ý¼ZΜ:~ÿýcœ™u«ÁÀ'qvìÛ¿_ýCÚy×ûöÆËøIüó¿Wì|ì¾óÚìÿúú ?x¹G^8NŒb°7'ÃÞL 1ôuz¥3Å‘™¥ÀeÜð¾ ÑO˜vÏ ÞÍöÞqÎÊ”.kn~‹>¯fn( žSÚŽÄC^Ÿ²xZ¿nsd/ë»óÛ×ì<§~vj=bhÂIž” ÍE(Åo`é™lÃ!ì‡~séD°æ×õÓ ü3'^ª^wD'¶ñº;úâ}3»5ÈÄ 64Ië636.ÁßÁÕ2Õ­¾‹g blÇN·Ïtêm.ÅÔ‰L+üRlt"g|ÉÄóíÓ ÔexI·¤ö{ãOÞ}ÉhL;š, UmñòÿÌšýêéù³févàa@Q¢»UdŠ@]{ ¥)DI°'S¬Mj§†æínq’îšÊùÆ4çqþUõ© ›Ñ jö£›LÊå'ÇŒL¢[†@bªƒK™šPìÌÙG3«èq°¤’%~¿##ƒÝð(¢°Ðû?äRÉ9" ú=¨Ú`—&RÈ:¸+ûaJÊØÉ/Ž¥0O‡)£Æ¿¸´G×^§¨¿RÇ…ù¾©éiìa‹1zìð’õ%ÃÇF{ŸQÜ;w2é;wº%3Ž‚Ç+CëÍ‚¹•EZv.W?i8¥ãlZúÔû°™4­þ:eb¯®=–’®ƒÃ˜§HדǦ°‡Ÿ4KÐAðCè@yÆ[Ó¯±VØÞîšà¤t&^®™‰4ö—\bO­¤›R\"ÁŒFÁǵ6úgÑŠUR›¹QdX‰EÏK&>“VóñÐz*ĉ+’ÌHäËŒl7 wUZ96ÝJj¡Ù9؈1é"v1‡¥€‹ÁÖ-y ©¸›‹›³¥®#-]` Ewq1Ýâ†x1^‚žS—\T—œå¯ª~ü)ÄSÆ«^üÍíÏa8K`¸ÇIµˆ0X D‰3JÈj½nm £0°¨Â`>-dÞˆ†ÂØ%£A‹6^””„IþqˆÃïS²Ǩá˜rõÆvõ‡r`' ñ#P2æ˜Zˆ÷Þ&–ÚsD­ÇÈc¿ëq ê-Aµêp¤ÄÂÁCšã¯Õ˜òûØõOÃzæ…oÖŽñ8¼Y:}z)þ5âˆ\f\PkÙSÙµ4ño“yŒEc×Ö# þ§ÀýÉýr ä/Ï©¹ÞS»nÁª¹ ÏAݸµÝ‘MG¶µ;º”kwtj·è:‚“ºq±&u}d阉G£ ‚Â#ÛÁ…Ií´Ÿí ƒXõ7MXíGpî£Km%ê’I¤©‰-Ž8ìG˜\-; å@ïÁð§@Sí*ùÂÕ·Õñüs°›cˆÑ„7úbœV·Ïa(CbŒ¸^Eá'ùbîˆl-öÈn{±c™L|ÑZÄOVŠ¬× £Àªdµ©S‘VÚ㟫üª{ë•Ó-RW]ÛÌšàM¼ïÔ'Õ_~-Ýò:Nܳ;ØŒÏ(-U ¾»©OÔd:OÓ |†; …CŽ¢èN´bBv§Wð’šçW4k‰ÇQ¬òš÷™[‹ ¼h>ì)f×ziDRèå¨bírÔãèˆIŠ#Àxšô9Á¯cíƒÅŸÀP†¦øø´+…vT]€Wâ^Øvù=Iÿ¼é&åÜ7±„.xo=Þƒ'â xÏãWO«_þTú“ú•æYáöS^Ñ¡º1Ó–Zw¬õGHÊ„Õp"õxeä‹1aŸQ ¨lÄ.ITkL±ù—ù„bÞ©U*“«h$³Ìð¹yŠr*ârIþ”ŠËiç æMuõbvþ÷w°¬Þº£vÇb·ëÉÏøŒ²à…থ™øif°ÜñÑÇŸ|ô4¶¯) ~Œ2 ±È"øó¹à•óÚ$äócðñœú¥Á´šháÁÅ&ñ_UŒ8‘Güv½8|ëtaJlÎ\yá³à‚NΊ>Û·‡bƒº´“ô|òñGøEBNÙ´ÑMë€ò[À{YŒ¦V‹SÛ³”“±ØaV,Ŷe¼9³¥¶_³0&Mª½Q9ç©OÞŠØ¡ìŒïn¢<Þœz¤÷Jº5i[3üló9ôú³ú3ÿ5½ž„wZ||‡¸™Ô‡qRw°½Â· áõ†—|¢©ØÀúÎ.‡û¢‰+t&±F7*³êa}Ú€fæê»À|rAõ•òWIVÎí õ•º™ˆ eN ¹P#ØFä•q&Ÿ‹ÓƒÊÎÒº{¦b×2'[‰/vÖÄÂÓÙñgç9ÈSLW (à<»žT+p‡'wÝ”qO²N©UÁoõ%á>­ÈñWÿß—·qŸú ê‘x“ñYÅ^ÙÃú,ÎbOEþ¢ N—›-¶,SÜ6䑯 ¢&ý¡7/_ñb˜B= ¿ÇlVc¦{Ô£+SG³óKKï|RZÊ-»¢ZÔ:d´™›0©8úöUÕE¬¥ø‡º™0¤t„¤¹tn ˜$ÆbÀ>3rjÁgµÊ½öv`mpHž‘lFÖ,ËÖO·h$;©ÆÓ2^‚2ø‚@ã–ÜD€C”— ֲÂ%+mX1oÇœ‚Y$wiþBøÁÈ<„ ä–ÚւϨ×Næ`öû LéoÁO™ï©U½ ªªã=¦CðÓߨPÁ8æ‹àñªù,V¿#P ØSUÍ®bzã49·ƒ¿#o0Åñ^¶¾Ûkî_J9^›4=(ÜÑÅ1fyŸí°£Øœ²/õpb1Ú`^[OþŠ2aòMŸBŽ$éÆa“ÓjÀèAB~Ñ3´™.¿ã¦?¼áIu×ÍêW ¶—,ª¸\~¦òÊñÝO¾±rø¥7ø[ÿ:¶èYüÙõÿù®ïòŒõÅ+ŠŸž7}Ö¨çÒåˆhuÆYˆÒ“$ZµVb Âó L¾ÈúPžè¸ÿµÎ¸Ô”í,ub'IãÙ¥’(pƒ‹‹6p] ƒ¸Á†û¸I† pй­Î8Yºý0Ÿsàne¶¤îó; GÞ‰ç>¯a%h¬Ä.ãø N¿¤‚QAbÆ#`Ç™;îˆLä^B #dÝ“èU¬ñ'ÊW†„·#"\%¼MíÈNä`#’}xãÿzj8ð.fƒ¯0£®o«?]Ð÷᪵½ƒ09ÉøæTM íœXOÀ2=Ũ`)™ŠÈÃB †0óé©•¤=´ý¯µÄο ÆŒSšy3p*ÝΠǔúgðÞÉ@áܪºÉ:‡þŽD9HÅaÇÂ$ùDN¢®•z’ÖÙßâB0†Fº^{µn†O]Õ nÚ ÂÚ9ÚQë»`Qô˜Î“ídË2¡"²B™?6;fa·Û‘Ín¶Ú-síÌŸÚP¾ð,3ë\ ½îôh3¯‘Oi¨ú¯áYxε²SG”ÆØëOæ¦TmfïG,z¦‰ð^:[ì~Ùè¶"—/Öâf½v9!ÂmNE *ö8]ËâE¾8ÖŸy:ši² ¦ðž’]Y>ÉDtJŽü»’X˵ÿüçÚÅk_}uí¢zF-Áƒp«‹¸ >«ž¹ÈíwVž;wÑé¼xî\%¤¹{p6ÀWCÏš5õ*\¨W=ÇîÎî¡§æ×ú©i¶Šœ›5‡æ–”óHpdš$8\€ûƒ…!|jÓç[Üyœœš¥¡C8;4ûÖè3Aë‘}@22кD¶A6‰iJ!ÍêßÜsL¯ qൔóåÚdHÓ(ÕÙ›ÝRŒ a}>dÚvhFŒ‚‘Yf3ësB&%bVô™ÏÑ.àÄäèÄ – QTÑõl:¦˜]†”bþ£É¯Aá|˜’AÞ"@(›.„7ç yiTž öÂßÔNÌøgHÈëÕ“Šûz÷9«FÂëÐ>øq‡öú^Á©ì+TÿªÏ¯%Û‘ ÎÁ[™àYry×Λ}&*â„("pf‘$ä„èòs½ÿ'{öª:v!Di4¸ƒQžó¤„—¼Y½…¶ UA ¹ïײÏzØT,$ê0S쮫‚ˆ¦.G¨ ¢ © ·?A¡¦ p*Qg¤¦¤M{#,ÝtW}ÁƹoªV£÷.cî…Wª @3ùí .8} vàØÓW‡ðŽíê6u41E¿Wý¥”´ÚP*IJA¸sCšTì[›æ/N=”¼,)Ýìâ¼ ²?Å—ÌAú­h gÅ…KuŸ½žUŠ•p 1¹²š4Ï?t}ù²e˯__ºlÙÒªYÖ'ôPš]zâ„U½|áß½NÔÃàqÔ¥J=*â½K—pTp !“úUqoPÉÕPÇŠsò>;ò,`ôÁ¤–¡X^fp³‡Z0ZË-}Ñe7ºíj† î`LUpçp*TK®GY;ÆOoÄÍÕ76«?âmãªæ0ÃpÀw0—K{¸ˆQ¹3y,ºäƒ·?»qn«a;.ù§!µ—µ£Ûp:¼_"ä£XTXýÎŒŽi ÛÇçW ëë™4|ßî.±o­Ÿ\.Ä•|¨LX[Ä¡fÍnÈ7éD“à R ˜Õœ8É)ÄCÛl8Ï“(Ô”+fC´†zóÖÏêï¢À73--ÈËiÜðöCÝÏ;Äåïm~Ž„?Ç-Ô‚6Ôó8 û¾žWoïÜò½ ¶otb»¡ÁœíMçíüæ-L|þ;ĆJÁÑlˆE·Xw"¯øAR üú“Ds¾L¨Ä˜`(Q*pIBfH”³û5­¦¤~ŠVb?TÄÏÄÂC+V<¤ÞþeâŠÕøä¹+÷O^²ö›÷Õf §®5tèHü–z¡_×®½6­dƒ¶¨m3·Ð+†]¦ü'E†“0-F Op%¸"Bk8źƒOr…›ªrI_ó±´²›j ’E)‰²¹YÉì(q{t­álHk(1WØ]Vä.qÔÑD>v~n„ÒðãVÏU?–>†ùmÓ¦ªÙ›6±Íתã®ç¥x—\©ÊQûâõk|XÆ\BwK=ÂàUø ¨ŠÔ¤Ö*þ¤/`F›¦ë m^Äß½¯NÅ•Pfð¥_R‹ÎáJuêûø;ˆÔþ…±ª}‚½˜¼·ÔMø·‚˜£øùà/ÉdéÀ.M^%µ¾¬°®c=Hn8ÚNî„•ñ«|åIeõ ¾¹žÓªÂ¯UGÌÝú ®ŠùuÙbµÞga]iHý{¥!‡î?æõEªêÍÞ%C;•ŽØZ²eõæ'f­™×ißè1¥ÝÏ~9RÉ6Þè¤Ï÷lJÊô¡ƒ§lRûLTÔ®ÇF,C8„?  î¸¨¶PÁð¦¢/HH”„#>`á¨Øø•®/ŒaC¤Ò-‚ ³Ä܃/¿ºñ"Cke?\µò\–°ËBh‚„ÿ‹Døg¶¡Õ• ü‹F…3h4•û¸îþ5R7©‘¿„ î-üWü?/ü‡6Ç©ñ µ?þòmuÖ¹õÐ+؇É}“èôÌz4¹ªÀ(‰&)RòÑT"‰üójQè~²iæ·2uáà‘Û}Åç^„ɺ4Míýú;Ú`ù^ÔZÝ#àØ+ ‚9d¹.°ðM˜ÿ¿²UGXÍOa nÛ`S¹z±L½PcT˜›@Æ‘`s­ª·>éË褧œ††#Ùa–‰“øðìƒd¤ïŽë´¶a 79UÎ (/> -«ùø­;{.Xuw–P¨]i)+…*óˆ“Ä‚{+-Ü2­Y2gU9\aÈŸAíƒP]ÏtÒRO‘盧’€$(CL9í ¬•—ybï%yGtZrÕv/¨í(Ãe/y]KÎ…‡†"u… TBKG9k3·c=Åo¼RZ£hR¦±ICi •ǹʼ[›4fË“mkM§›è¡þ_iéN"ˆØiÅF»^²Q!¦z]%”„)Æ©f"enX8fæÌ1 7àÏš=Üï™ï¾{¦ßÃÍÊÿâÏB70™?œ?È8ž¿Ó’i6²'ÎÅøjÕ}$Y•‘ÝÕ3$¸W½Øsdð<{ n Þ8x†Ý‚²2ì!ÚbÁÉã¤["¨)%‚„€Y0t+€ˆE„ò¦´”’{i)ܲªfd[lÚÄ’‰%ç ´ºzA4¹§EÀΔXlbr`e’$Q )+ »ÁA v~ìü–#¶¤.}e,eÕ¯¡r&‘‚B ºúÒY-Ù>~xY“܉¸^þ`ÔÀ2ìS›Ó¢þýÎ^mï ¡¦Û* ‰5J&Âù"m• ÁT ©äž ?*¿Xޝ•C¯;{µQió$f@úS­IèTéŒ6YTƒ© LÉßk0Â6­ è+!áÐƒŽ¢€WxIqt­ˆj-ð<¥ꉯ2E­D«˜r·X"¼d*—Ë@i¡æº›² ߿ݼ‡Ò’O“afaÍý®)-‰i3vïÚô®#ø&“mÞs~Ǿ]Oc7Tÿ­VŸÞtuà³mšôéÀç àqú™ÔìíL ÓA0|w Ð„â@ˆ¶%@r™Â*Qfo,d˜Å‡t=Ÿ«ÜV#”»ÖÆxKâ|1ÖVÂb2ÎSrB2uZÕR¸®ºhœáøëVHŠþËp¹¬Œý4$ «¯¿Œ}5’s£?ƒon 4ÎFˆK¤Kæ†mƒ“—ìH ë#ôpJä ƒ«„%ÚÈ•ÐFZ`;ð0ƒ^ñ¦mb¼`é¼?ÿœ·tàì\Ã'Ë¿VO²±ýFâÑëÖá¢W¯Û¿ïÎ^|MM I–q9¼Iöj;”¯¹*kWí+DøÆ=Œ6êdL:r2&“Ñvo'c\“1<†‘NF›LšÐŒIö#,8•#Iº“1‰8éUò)­#­qÒ” €øåžR,ìHÌ¢‚\6ôé¦Ï¼ÝY∗p‰ÈpÑEF ·5ɳz1YR§›T›ÔÄ~*ëÙv ’J‡ÿôÈ‘r&6øe»csO~¨–|°à·E‹vuJYþÇERŠ¿Q½\!­g¢8„TçAÑhyÀ ¾+‹ÓgsX‘è3K–(äccH‘‰Êë'lR/‰†UR%Ò°,¼Ó [Û™mvÄz‹£,Ëœ†µ_›Á¹¦»ß Öåµ[mÖ(;ØÇ£íPïXä©i@ÿ1äXÀ‡5è’P‘Dp°ŸDÃï°K°Ÿ]saÓ¸åWOÏ/UûðŠÚûÀ슫K ÿÄ­ƒ3C‚Ï0C|ywecæû #{ù»ên·oÁ&Ø-âÂì10·N„Ùhƒ“X-àÄ1Ÿ¬óØRÖFy,‹Å‘#ôrv‹ŒŠ™hEmuGÈØ9Ì ŠÅؽˆÁöhotž˜î[çc†Ã  Œà#hvif;ª#þÅHêKSº­>3°ùÀ²~M¾¾ºëýêKìIu ã V¦Ü©Þ8?»W´:ïî6û-H#Æ4Ñ£ˆ Å‘°NCääENªîY€ùüIÝžÇíÚä˜|áÂv™Uv3.b"çxTmNeÒµ­)€ÙóÝ£Ó{†Ñ³ùê7œ\g1ñêFP– ˜‡¿»÷Hã£ô ÜFÝ¢%íp³Ä¨B˜&æQ /rt~oÂY/~Üõ ?Ñ­ 0—£&µÃE#ôiÝ,™‡WnÖ¬mot8•Tx½íÒÙ cúw˜Ý¢ªåâ®Oæ¶™ƒ¡øfÛ—ÿ¥ ô<ÙwŽzî©Oö.Û3kïä8›Ç“Ô.:)ÍdKÌúô˜™êOõ€_~gÂ*iùL³‰SžI‹i0zã‚>#÷|:ßû4“š÷ØÍ®'ZðÃêߢҔ㠞™v!jиgÔ`Ǹá…0ÔaêlúP¯È¡Òs(« #Õ€7I‘Öìœ:êȱê÷Û0ÉЀa²oÎ}`‡ƒÞÞcüi Ž5;¸Å˜ÆïfOj³ú,n0tjÏ–3ùc&ÈOÁ¹#;ßPöÐx2ÎLWFŸè¤lä0œõí´N½qßÝ#JËFÇ’ÓÖ$zRúnÅæÂ!Ý›fHÛðÞ>õýùÏÄ›èÇz²ƒÄi29›ítA˜²í×`ßú`hzä3¦Ì ç€R_háµæÞ„”Øv-Õ¯n_¸­?ƒG.¾°øÛžÛ-ËiÊ;EÖÛt_Ш^Àyàˆý¡ÊÁ̼Ò;G†.²ÁdvYˆû?wq9ÜÅ‹z/†º`? uØu¤ ~òíÍZuº#mL.Bú(I˜¥¢…5T»¯’Ç—´iÓÁíí’Ø©¿ºìëàïák¸é¬Ãü-É.iÿq•"Yó‡FF9tIm¤îÛvúô6<ª¡I»¯È¦X˜øTԻΨ­8±î˜[ãÐ)¾{Ì.gí1³3îëÙià€ÎOjÜ1Ã[Ôêù«WÏÆ|#Ñ`‰qÓ‹b#Ì̃‹êˆ‹ g¿øÌãMF»Ói7šFÍ.«˜>мƒ2ÆM«wÞünÿ*£Ñf6’÷ÆU;úvÿêš÷ð|Ì…@€Ûâl9Õ:!9ó [á§Èy„,L,IÛ`ìdøû`íï|•º“ü/®>£1 ‘sà¹{Åýdýo=†ÿLN…뮂yAÏOM£ù©ºŠ|1–,·——I~jÕ•™1m†„ºî­û,‡ÓöEC|F%Ý{öêÙûøXøwü··¶ì¹•]»ì @й]3f^*Ÿ9#x—8¨ê§Ø ì3”IŠi¢¾i½ {a܇“>÷áâËáuù><‰±^ü¹B¯¿@¾Ç½€ð¸OªÛ h2š_OaM¬“ê§¿„ö¯Û¥ß¦–«9F“Ãéjåu:LFí_ _uõ«ü„oŒ¢Ý¤À¯>í]ø¢+ÊëÓß!ùzÈ ô ¨ýޤˆjæ †¹žý#†mZµ¦ë¼Å8™¯÷Df7ÏÊ´úíá3Ç¡ZÊSÂhZ !Ñ®U#SŸúåçGVr¯cÓæÝ»Øˆ›"«šÁM+ù™¿¤þ6ݵá¿.p×B½c”W.Å•œ– €](ÂÏÏÄq6NPGý¤^ÿñ6ãî g㕞á3Dó;Húã}u=[ ß—€8£ê†¢ ÐVd£Zoøÿ·þE=_«@Û´ÖÛ¿×z+Œÿ§¿Š/Özû‡¹Ö[nuí¦ÚꕘÔ>ú/ëôŸ·q L½:îlÓAÒŸƒƒg˜×¥†ˆG&dGxü’P=²Ca;Hͽ4»´ü="Ÿ– ïjˆ]€™Áã¤\GŽ L=l’cÊ–§ /¹3þàã·,À‡«_Þî^ü Z2 û„ÂçHÂüŸ¯C©.àFAÑ®¾ï‘—¡[à†á8–a˃x|Dý¹xÁÔazK™¨+–÷½B^˜O#›¨®¦HXUâÀãÞCHÖ£q‰i®7œpý \'Žú½Õ¤b³Ÿ¥ý¿Óªß½¶)l:xðÂA|ý°¥eùÝȈâP3âbAŠÝ'˜}¾(wt| ÅW勿KÌÑB‰ ’½B H*à•&eQ"©pêb—µn¿”øÃ–µkü2fœ={ø¤žx¢lî>²~#uk£ æSõ€¾ê‘¾}‰=³µúú:=šïsÎHD_àBueSðÈÁ×sD€'rÖNÞøÐû >¿ygé²/ÌxÇcª:³Ó“Ù¤9-ì„" 3fþ{cÃÍVn ¾“y⡈ÔL™§À¾£Ç}‚”Óî¨Ù0FFÆÖ_;õÑ^¡²8fÓ´âA÷“ü²òòæC/$ùfæmÏŒxjÖ¹‹€G0‹ä98Þ(«1@[ ÆÒqYZ®-”þ ’B—1&+ü–|˜“h<Í¨Ì ÙSßbþ(%98[M¾}5Å“rq±/àƒ.Ð>Hýó …Šlàóð&(rZˆ›«9U¤6n'èÿ'˜b xØ\là ät² F³ÁÇpتm‚J-£Š¨õN—ö{’µ%¹LRðcfs)³µTu”ï‡Ö›Bë( & CÒ6¥ˆ6i%~#6—@§%|Åߵ߆¶ïå½j!þµ¿¾Kýw©j¦ñ®ã¹JšŸùPJ”‹5#ÁÇ’´#È/®_«¿Ð‹ù›°TšP$¡@(ð–‡ÆçPt{&|àV«÷·)ÁD’êrÏ´pÖ³\aÝ>î$6~A™ÿïÂľU»ã˜º¡bD˜Tç´9ÍoFr)K­ Î!däêÛäê…š8ð”Wèð„”–Z øÇ½ç:ÖJI&…Ǧy;h¶+m#<žÈ¸¦y—5­Ùô=¥F7…S-Ú@>ð šAæ\$t‡_«IwˆÒq÷¿‰/êé)x”õŒˆÛÒ[ns´–r)5…÷áv¡8«û )9£?ž‹ ´X+õܕє ¿Nš¡?Ãi„?¿uáàÁÚÉS~¶^}<2aƒ»o½ŽA½ApêMÑ'Sú¢MzSwF-^,8,æF€ñ±‹5€pŒòœQ‘ùQV(²J³¶³ìEu“Räÿ—‘iþˆñú³™ÇjVmU77…FÑׇI[ÎÖ¢O2ý}ÿ/ОD}¦iäü©Kê„ϱohû\àè>Ï D›‹™C‚w('M¬ojÁ>D£ý³áKV ‰–ÀdŽõ¶c<ÂQ£Wÿ"Ùo½EÓY¨Í^Ëm#¼Ž8A n)@‰Æh eJ &›½ÀÌIb‰‘·€6Y÷¶ÞÈÖû£Amu{z­ŒTÿ‚%eÄí/U5£xRµgGRè®hˆŠ9£66³ÄC¤ØÔD†eÕ‡E«¡1É´zP„GCwx8/ BÂôÓÈ-”p&:ƒ…7ÂPLdV:Žšö¯ßÕ>áÅáA”Vu @/ͦ…fš‚”gó9¢Y’HQœåu½R˘Ž-6bXH3ÅNü TŽˆÔ»÷¡¤%q3ï>•hj ÷H¥¢FrÀ}o“œ1¬„$$˜P‹*S¼%A„#Ez tï£IäV‘™`ÏßëlbߨjNìݵ§©tÔáþgÔ–È¥4bs›´ær²ã@Àµ2b®A0oĤ‘É©Ln¥5†‹¬ËÇ¿·†5‚€_+G®pÍîï›í¨ßaô²~=.hèŠn3¹OêÄ­ïËi4¿UjŸƒ2Úy°Týµbì°çþØ5ëÙéM[.|yYߥGÛôËrš<ÉY­SwÎñ+bz»þ‡¯.=;¥Ý£Ï”ÝúÂ%Íû8VÚ5Úåˆ>èQ$gRLj“ÙïàØƒ{±|q±õ÷ø—-þä™1Þš¯||CÇÌñc‡´OÏè4¬pTê˜gŸª›—æs Pb^r4iÍDŽ…NÀÐy¿öw¹H^1<.Ã…[ft[÷¯•#Ö0»ðÑÁi}Ém2cÒ{ÏîÑnzï†ÃG%uˆ‹iX¯~”=¥aÓ”üåíã ºtŠ/º²¡;;0±a´ÉíOW¢øLúÃîÌ>!ÌÜ6xÌÞ™­Mö” ó—Fì™Õ¶ÙÄCžøy¤Ãȶ]F{ö5PbCÿíï—¥Ž76£Û¤vñ©'µÿƒƒÔ9:êÁ|Ü¢œÐ¬È꽬Þ|´Ž^ň ¼Â}G] ªÀÀ>Î䓼d&‘w~O# Fàk¤ŸY©“kœ„¦f3¼õÖÅàMî§àÍR®H66 W…ü=ÌÒh¶Í™$š½~½Šlt Í,–H¨äÞ­>_VF¢W!ˆu;aMìÉÒÒ» dáPL+’д€ÌIŒA`‘ÄräÛ _évÄÕý$ZTÜDjˆt;âìO~‡¨ì!Wªº4×ÊÈW’ú^ÑO£`çhMw)+G“Pá ¹ ñ#‘”ÐÑ2üÎAY0Cy…T·çzÒœ>¢•– ÕÙsé'@}d?Uÿ¼H¤éà­kx7~âZð–ð,õ1<«*™w0ã«~) 4bÀ. Z#Ô·D-À‚zûŒú¶ë4€aùØàÒS§Jët@z6» !m†é1L á.N?çT‡«C>b\j¦–œ¨&Ðedac™²àU&#˜CaÑ¿¦¡ö÷jFš"å)õÔ5|\y ×Ãõ®©Sèºá2µ3Ó•q«3ðºà§ÚŽ;Qjr²/–HÒ%`ó™F>ÿªê+9”g²ÁÉc^&r¨Ø Eâ’M:ÈüúFyŸšw×· á¿Û‡²Åí(ÕÀsí| #IÐÞ‡ BRÒ Ø®ÞBæ è¬ä:›Ï …c³ª™¾3Ùóð«¾;Iª"Ìô" ÙüØÍ ç´øÀê}ºxµ0D±Ø~Èa^†´Ç‰&-†v~bZDê"w,u0Zö¢¶,çá`Áùê5ÈaGâŽaUÊhñèÒX¥ç‚|wd—F±DqšC A戨qÒdQ … G’¢‡ðPZ ;2™..c€päïß#ÁÈ/GdjFÑ⟡¡´Àtµf“˜å-´)ëß¿xñýõãÁÍÔó ód0çÕwÂÕ´5ºõ²L˜4ü×^àÏï•(aÌ×Ïâ—!Lßpö¬NYÖ]¤’O0#Ÿ“õ¬)=ƒÔ¶L´Bˆ>=ùýµ(ð¼/>ANöà‰«z×8¦×…9Áï {"ªXßö¤dƒ-Ù+å¼Qò‰¢2äëTšpÊ|n’ˆoŽ‹’ŠQò!s±3½˜Ûà\›Aµèz zÊìsøIùŒˆˆ¡f2Rü©Ôh‚%n©ÀÍqû^Âw¦c+kª¦O{xÅFuÿ¹ céÜ¥ÝÚá{(jÌrµóÒÑìØøÌ»ïþ[]¡þª¾]8ꃂääK¯ãx”O»·ÞÛ¶óÊ¢ÁÈlplŽ3M j„&ìR]±©’£Þ¥˜P&ŒìÆõ€ ñÓ³–ðG²ÞÉâií!}œÑŽTgÜÊØU^Á”Zâô–'"çKõ!X8qm&ëWǦ9ñˆl™à‘ÁˆeÆâ$f" GìÄ<Ͳ‹õПð<Ô€]áÁ뇀|Ó¬kôì¨ú‹‡WT3ŸðâäqcÇL;õ>õ¿;7³=™¾ýVí1ÜÊp}‡>WÐx«?õÜò}6Þúò¸±ÑÑM7pºwnÆË¶:hÐn²¦j]SÁ¡Šõyï±$D{H®µ*Ìšš™þùžëòÀ¼cjpohaØèl¿rךÄñê“xlÂÓgpÿš•!k¢¾H×DBÈ_¶ž³¡p7w84oüôõêúôë 73›µùX÷ws&sx˜½pó=u(‡µ" £P+%ô:͸:ƒwx>Hø¤&Áó¹º3?çx(„)Y)««.Ð*쌺òw°™V `“ z4Ìá{ÊÒ¡Ös˜‘NÌwKÓ Ö¨Ušò3¯¶(ýtr=Q‹¿„òÀoÌÒsùuœ>7„nâÂI|sLX1FÝp¥ývþ[P®Ï¸¢F©¥Á¦Ï4:ˆS˜\‚kRª„¶)Ò0H[)䳎pƒ4”—›U¦~²ðkHÄš;R­<Þ²·o§UñίT7ªUïÄnYò~æŠ>©-(]V‚ äæŒMEPU9—¦ÎUµ¤G¡Uæ˜béAXÆeRÝÔ”02Px)¸ ­õí/éLdF…âÍnoÒ&Ÿê=Ú|»Hâª,!ÉS`Y‰×J§]×{5ÚìS"A%®ÓNùˆNÃkÄ^E‡h°¡uq2QÁ*M¼³V[­LÕÏeº á†È,RýRNf?%§Í:%™*Û÷ú,U´hÿdæ‰ÆÒ¡aÞ5)€7N2\§ÏŽ­0Ϥ¶)nTeÓäh€>¤úi±Ã^#Ô‘è‘É~!\|«”‰°þA—ãN‹KR° f™ä¹#;5ê™I%N%$(—‹”RW ÔÔÎG*+ÛŽ§‡eRŒOèSÿ-ÔÐ6-Ö3öì$}Oˆ'sŽ3t¬»ô]i)Ùµ||âÐmð5TIìŸÚJd`¡¦‘Mñéæ?FóÿŽa¦rúþ׳µ¬ÀZñö=PMkq#a=èmÜÕ!°O"„”QHrˆ¤–Ü44 B³BiåB¼œÊlëõÜy†VUMD’î µEÑãžÔÑã*ðä>š«™ú9tÍÿ¥­ù,TçÞ¬ø6ôÞŽ«Õ3üHÖ{Žìƒ[×ip•¢bèãš@Çõj8ÁT›Õ/Ȩ>C(„‰¦ÏÚL:k'éuªˆÂ¬¥¢i#Éœ½q7m›"¨Õº[k{$ªs÷¸ûù»ï&å˜]wS±]o»’Þ}Š^§ŠADÛ…¼½Ì€’F ‚]f$>ŸÄev’ š ʬ¬ !P–F±ÌÞz‹¿ ÕÉ‚¥9(¤A¨)âÃ$“\·\œ$À÷hê5G‚6VZõ%QBè(t¼¬¯Ô‹xN(ñî†O-…%:ŠNê@êô‚ØâµÆhÎ?‡I€îþ£Æ«ÓeEa/¡î°“Á“é´£ßN1„ýXÛAˆ'uêûŠúݤîaOáÍoNÝ9úqª©ÃÐ ­@‡Ák¥>C”³š×Pޤ%ì<¼q”ïùgZê8§ô}Uýnr·°Wqm˜–V ·Ô×Ú¾“Ô¾»évÁªdÆó+Bìo0 #r“ìÚ¨Þ%™bïè‘«B N×`‚ìZÒ땺½ê—ÐmnëvËH†ïÒö­™WÕfïÏš=ûA‚¿Ó}hF#<©Q}æä½PD¶7„NŸ#ã…ó²=EJÊ"õ²¹T_=KŒÙË{=µ17ÓÓX¥b.Ž>Ôб6½"vYcùÑddAÈu€‡ †Š"ÀLP9Uÿ ùUû û=˜^£–è6Ðo¶æL9õæ²Ut†ÞjÓdú¤m.LøÁµñ™·ÚwP¿ï=êÅ{Ôc}…y—­¾@fíÎuÃzå fOpègKæÏo¦Y±…YÔ‰’n[vAªTÏj‰‘âxÅݱ±\1k¢¡¤1.‰n¨¬M­€ÁT‚ìŒË1šP¨ûß ¦î¶É›‡7lÜ~ÄÈUÏ¿ÎzYë–“'¶l]6nâÛï‹g˜FõGoÞì&;ÕßïÏj<ºðéR² ÁÑ{z÷êÕwn«–ݘ5~ áݰ.,¨×ù¤bl³F¾‚ÉÓy“¡©\¥ÁÊ4(ÇÏ iÅÙÊ>Ça[±gCÃìµM寀 k ŽÍhŒÎ´€u=”M¯$Òˆº»Ã*>â©Ã'‹{õêÓw[¿—gÎwßÿ¬\Ù©ÓÃúö‰\£,Ÿ[ѵÛÓ-ZNŸÕªurê‰Yž8騢ýñcƬ_Zkšö ã{VªÎä\Rn/¿‘ÒÀ¤Àð”$Á™'_× Vôá9W:VÙÊ=eÈ-䥕g¯m'Ãèê"¬Ü=LQCUùça²Žž ÄJûAíMQVfÌlÙrú²@ÇÖ­&é«w~úÜt|Óœm 5²XàJÏ^´òvîÕyp‡ŽáœsŸ^‘°%‹b,Y$‹76Í—ÁxSœB}º|74ØIÅ/KȹÏuØ^ìÝVœ~²];²t hý‹G1f½õ«õÖ[øè#·@£7ªŸ#klÿaX“z(Ó®ø±’!(õÌÑõµ¹ACNíÑ+½«\å±eà¡4£æ4×] Ýbú?¬ûæKee·µï¸=x—â‚Á÷žú¨.Ý&ã<,àPE~R< Ü5F—¹}~_"͆¹¦O¼»Ø¿,‘/–×úŠcŒ‰reËpì=žéÜZ6 ¾yx¿¿?ij¥‚¿Z— -þ‚Q×nAL°Ýoê¾,‰€,cpJÈ%Au’ZºJ*ü\‰mmLI´Á_‹Â lܬÂ[µÆð'17èÑȹYWÛB$2·nHµ ë÷5Â{hÙܼ€-Žëj½RTA¬îƒÖ赸J¢}QB±Á“IžJðKß“ÜV8d1àVE’ª™ îAgeȆ ãh¯†g)Aps,'C ®Óg”}/笾¶B´\lSŠ-„µ>b=¥ 2 Cÿa„ -Û.û³ müêq W/Úª>qöÕWÎáûpÚç>ü7“òÁ øÔgKšùãÞ­d._z÷ñ^ î îÝ|ßÒë¹n¡NMGAQœu$Ô\Ñ©‘Ë£b¹`€ÕQ‘Ô84G(t^’0a'¥oÆÆ-ØtùÔËgO:tÌé/ÊÞúZ­*~œÉÄÖ]û¦–.Z4S]4snýò¹ß<½›¸ˆí™ Æ™‰M1QäóœÑÔe¤I›a»»P±g™¼Ö N='ý·Ü9ÌmšL otâ@â3ª¶Ü½Ç5OÌÄïT Έ (@’Ëýøz¨ÿym4 ”y*Hÿ¿ÒÒÇd#ñOD̯Åx‰c‰=<<° cíüñ¢@ÄBªÒ*œ!/Ђ:ÓÝ IÞÎ5ŠÔ+·DsŸèô Ø«=xôŒ±XŠÍ§½N ÕÕ+gÓ§. igÕ^?n¯—¾ìÑfe~>ÅyÍzòwfœªøº¿ÆÑÐ^ ¦”:bJéÝ8$Ñ)Õí¾„tO¡ îÕ½3¢{Š3õRàL=ùBAãÑR)ÚTìâ§ñåÁÐ÷êάzûØÏC˜Sý2ø4-!„RK¥dÍQF«}P× t-äúÜ, ã×jšAÕŸºÔBâ1::d`jZkL±wªPz-ä ýTÍ ̉Beaœ¦i Á×4§(~˜R«ƒ+°êl¬‡ÚRÛ™„¢ª쟺åh9$:*¼I}lÇŽÇpñõ{m‹±€'jãâ3ƒïl,‚Ûã’€Û/ÿUŸ‰"cÒ‚m…ƒ*#ŽS™O®Õ,Frj‚ »„MÚx.F_BKÅ—7? aµýcqŠšñŒzý™gp²>¦}gï­‡ÕŽÞ?e+^äð–o¾Q'£FŠ0*Š…å á<º"tC¬ÈšŒ¿ªì‘û6ècÎ[0¿WµGÈÜ|„lª¾¹Q}„ðÏÒà—¥ù‘+Uo $bÑlàçó`xñ(•¦Y¸üáš&Ž“B¸.¤4‘¨Eò§„@_Äyàø¸ôâäócÆ^†:(»¶q€òò†ùÂÜ7pÂX,cžZµ£Üì¦Â0#‡¾|¾I“ÃõìÜöºš¦ÁĨŸÝzâðîG)> Ìø“Øtde빡ÜBx¯Y«:À=Í_…÷Méû ¹¿ F(" ÑIœž|?ö$¥‰d j®Š4frÑÖ1«>ùæãKw‡Âê.àçÿÍ?ºõ õ‰7—î EÖ‘H;\v="´Ñ¡µ™#jmbQk“É]ÛvîÿÙôx÷%z Ü‹¸W/0ï»MÝ—é‘p$0÷Ö84+TÙÍêKsx|±¢˜Oj¨$ÂÙÊ´´N±[,ù)§%AjTz—ÃàÒ÷ ¹ª9÷ÕñûDBPß~E³"Ù”h†q§QZ¥zÎèD)^L’œ^‡VsS"%·r­þ:´R†õ·¤Hœ¼ZAYÔ‰œ§f]éMÒaV"¢Ùü ªÚe³/ÉËúbø¦ñ$/‘zn¼J%寲R,ŸöÅÄ 4h/ÁGÃóˆm¨*ø÷Ó«Ç ÖÛpß½&Vé¾"Fä&'›¥%*“by?‰æKaÀèójWJì±%‚Ÿ†ýÅÛO IàŽÐÃüä™ÿ4³:6ßÝÓFë»ç¤ÒøÀpè¦ !(¨^©;‹› Ö@ħ’èqÍh,Âôƈ_Y™ÖŸB¡üCt—P¨>#…Xðàìg¹%gÆ«òø3øÀmõ ^ú*v¨?ÂÔ‰áCç‡ÑHBpw$od1ë·›ð5vrðwü_ÕÂx‚™ê˳ñŽo1ÇnVßÛ¬®ÚDZc/?bRr  &@LW„R¬XLáHÕÄÖP’üšÆ¨ûÔánr§d×è4±˜»^YñÅŠlOså‹ÇÁ¿Ë“ŸY´¨G—מ4iÉCwTµ_ÿ­…'ûß7jÔÐÇ'O©ú÷ë»zûðV<éÓçÚ¦Á¯O I<ǯ¼ Œ³UËyóçyðta¡²¤ c£F¥iNGV.ÒÎ…ãŽCÂý'êÁÀZëÐlFdGI00ÈK¯‡I<µ 7ÑrPœ!T[È6£`lÌ|̨ªzûúuüÉ_~¹Qþ¾l»h¹Ýþ‰ñF1»6¨•^ާ°¶`ήC0îP 7AGáEXº ZEb<ë±k/Âáóq΂ǤB} 6Åùóï_=¿ àÔ>à´“¬·CrÖ#ôq; á]z³0–ZílÙ‚¼FÛŒ+ µ…Ìæ=¤yµRÏAY£íSGÄþ9Åœ o›ší÷/‡ñãÑÐw¨h|¸&qî½TÚPMFyezË–3gœí^:ftï>æu,èÐ~ö›WwîÙÀÖtÚ@y”a£ÚF<Ö«gTÔ3±K£F‚† ¶å4<´iSÅ‘ò¬Ïj=yèþÿž"bßd‘Hƒí“ˆÂðx P¦Úý»ï€fTý%³Pó̶Âq8$SË„ò{Nh”±u÷©NëZ•·lÕ£àæÁ`‘úŸïènœô@ý“¨ LV­èMº‚LN:]Ûyª|u÷«Ìꪗ§v-ãªãFOÞªN)Å='?¬5Ã÷"ͰCôýÅH_ t£IèK`Ž#ï8¹©ñ³!oàSã'ÔÀ/߉`#ä6!¯ÜߨS)ÜÝΨ»f¤Ò7nÊdê y›n©7±rëglWBšÍØï‰lš’¶k½aOR#¬µ ‚5w湌Œ/Õñ_fd”&'ë•°›'Í÷ñ7Þ8Þ{WÏOâúê{êíûarÏ÷à† °¤ á:±´".¥I@°Hb,02„û'­UÇâ>P€±>©ô®ÃéÂs{Uƒ…­þŒsjá­É šš.$¤ÒŸø?˜™U~M|­|ñãû™ãÁ€àðoPLzì‡FÝù öwrj§øsu u¢Ù'ÑcˆíÊìÙ³çåÁG¸ÂçÆ½ÕÐâ¶ßœ“››ÓÜ’›““½«ÄlJ [935-nxÉ–º ³i¬ԴÉO½u!-mÖ¬Áæ’ÑÄȨ¨‚ü訨èü‚(èjÙ74¬ ²O”˜<Ôví¸§Æ•_%3«9kyFFVvö̸!¹¹Õ Öí=hªOk2e,L©\«öƉ‰e† uþÏ&³9rЈ‰C‡’o‰cK6&<–œnnÁA99ÁAÓ똵""úµz›ÿYš`Œ¯0S«R³‚õZÁzd¹ÒóêˆÙHföÂÚ™ƒ¶¥§§H 5U"ýP…ÎÓV _mc憶¶ “ïefeeÒ¼)Ù\U ïÏ©2'× Ó¬$ ¼ƒ<ÿŸTêA®w~é&ÏÉHOOÏÎÊ}uÒäºÚ½O×BM vu¾N˜™œìÑ‹µ¼üÙ]ÕUcKf—âã*ìj¤i¢Çä·»Áã ñ÷pöEô½Ê®]IÈo÷—ßÞglU~»'õr ž^6±ÕI8›ðXãеϱgw½ñÂìÒ©“*è™uÐr’@wHÞáöaÅ»í—/ÿÕÞdŒúóÈð°Q#—Ä™Méi ÑùÑÑQƒGOª-‰7™ê‡™M1)&“qû¨háa¯ÐÐÄ¢èhEq¯óìÕ·_hDß¾ãÄ]]|'§†‰©‘4¡E (4Š·í+ {øi#³éÿ¾ŸQ`Õ'Á“_Áô)o2×¥úÌLØ^26:J8ç>*±(& R¾ÕB݉Lþñ .®>< F#SzeÄ*„x•š%ütc®Õqèú™Œ$/1±®6¾jë¿”‡Œ Ʀ””ÐääÂUEÅÅE« ““C[„ÙwÓÒÓÓèLj}…EÎN†©}Ý\M“úûWTààŠ ÿþI4îb|=”Hèåâ× Sc¯¤ÄÂݘ'V„âZ”é(»ùy†ô'ô`j*|_¸’‡õüåúžÞ~Ž™Íɉ–Qù•Ó[[#ÇÌ굨¤qA˨¼ª‚ªîÁ ªË«Q÷æ‰)Ió‡g¯[®>ŽÀ‹™¨nÐ @· ›¢ 7?oŸM±+ùíîc+^ „çoA¿@ªŸ“‡¯€"SÆ5Èjt>Þn Aí•än™\H46í{&;®°°nVkkdrJÞ¢ŒtÑO]¼Ðl®0—áU••\µäþý³sùìþ#(ä ±†9Srö|ûèôì2©Ï² íÌ_Ô”)!7Ù«›Ëo¬ˆàÓ˜ˆØP;{V]í¬Yµ;ŽÝñ¼ÍíRÛ¼ym«àq®òcÏï:~|×óÇf´‚ÐŶåÔqDZ§ÈÈ8£ÑüR]}mݾ¡&ðó ñƹà×2æáWñG犊;*+]\|êÁ™ed~–™áæÖ‡Ö"\gí¤Ïk €oH¢VY)5›Ï˜“ynGy™p½1Üí¼ç•—?ÏÕû£pôÖ\¸É裹ڸóz¹²rÇr„õô¨Ð €’|(Ù›P»Uè¿­­v+ nÐW ò‹zR˜T¯¯5v+F!d7¾!½|@.->Åç@»Þ†É׊;ÜèGUsZ"" ëÌI--QIÉy‹– ÛðõÉÊ賩OîÜ4!¥¿ÿZ&é’%^£’pÏgLH_u<~yjBBjê4u.yVfz~¹4-$xĸø¸iÓ¦dÍJNfSwL]¢¸wvЭ'5ÉØ¨Ô´’9 [CCGW™[BCó+Mq%%Œ™%ø †˜x±º®:*Úœšk¿™«h& óï?*?s¬!ó臄/¹us‰úÓɃ&5éÜ]ººô†L“•÷׺Ô@7Ú°ùð‚øô2%J½/rՂݰ'ÆsÚ1Vÿøxʈeo¨7>Ûöù@ý¯P‡Ñßw&ŸUÀ§&¶ïq«ßZrtðñ ˆGþÄ]i´5â?σ&ÿi9Ý?5 z©xT]Š?ëÜôõ×øÚÈ }üY€Ì3ëÛ0¶‚OØ4`™{|móNemß÷ʪ¼ª²rõ/eååA±“pï ꕨà$³&nJZñ ©iPvb?ߌüÝÛÊ+*Êw5Õ†úD=¡þä9(ó^FÖª™sÍ)²¨­0‚³ä®:’꾞Ö]šuBCƒ‚¹B¾Öœg"q‰LfNŠ ª[V6¶­yù¸²²Ìé‘afsüñÜXuÅPÓ²– cE^ÑKÏUÿ÷¿ÆWìÚW’f¨q‹Îš9/s(n{LÆÜªevŠB03Ý5Áö×›N Æ_÷þ:ìÕ01 ú…Að»úuûþ¦^Äç°2²‹µÃCÕ\õ§•D´XÐb„ÐCaš—¢ìC¨s ­/²ÊK!Í™cyDÞ&Í^ŠäΑpà\gˆ#‹,žä†Ðˆ'G|Ñ8^A„q¾&«.-À6Xo:I”¨·ç~öDx}XóÊ}ûV6†µ`uFMõÌ™Õ53ˆ’àó†ú÷7“’Õ“$¸mɼU«æ5µµAm#-ž‚3Yµùðúà¶ ¤_Šðür/ˆK öAP¿’Lòè ¢wû“ù†x[½=;£byâØÈ–¼ñzʉ¥èèøÃÒ)§N½ôì¶—^ÚöìKdD?~žŒüö»Õ“'©ÕNµC½8iò!rùæÍË—nÞuøù8}xŸ>l®Ñnºð6±ë}ì;ˆî|à»…ÂWç£öövá\Þ;ò¢zÞÈø".EÍ@-…BSú¢`:)´”O¶;w@˜æ;ôÇ}q—qóé'Ÿ¤÷ï|²²Á÷ñ6×–•Ÿ,y$unqÊ+}úéÒ¼Îß‘túkúÁÎc±-ø ½“gÕ[þûì‰ø½c -- 8}MYKK´c«ZFbed-|õ»¶"’èIûâë#âf†É©S»ŽÑ;µÌÛ|dŸ_ÖvÝðyû:æô'ï°€œ…íDr’u;‘”ŒL‰Úv"‘üROšµÝ„ú÷.û=ÛÚ;€î%ÂíƒBëvΤ-’¤Û„yééóàÆôa{Ͻó’Éhœ+mfÆg™°ÒÖûøœØ±³ÝÙ…5!Ÿ W˜c¢þ‘É÷Uµ4Žçý¨ÓbHH0® ‹«Á%™pNÜ)t`Ý;/%&Æ'ÄÂ7­ŸÈ¦%ç0þ‹‘`Àø„ˆÐ~}ųνWttQâ(Pg wÀ8¾´óµ]:L m®h§Ïî4¦÷íóî»­sæ$%-…ñ~½‡&CÒÄâè±q[î㣸Fò`'¼øCAÚšLó!¤ÑÙ}ú45µÌ!»]!…è¼k1}¶ªXß—x 7ÍÎ Û ·çoîK˜v$ï¢uC’Øò[;áÖ—ÚV$ è¼Û'gÇíÈÖU4†ÁG+òì %'u‹†:öà"4̺lðaÜtñ£?F7µL›;ag î§~VÓÙIfÒ}Îb(´œ™êç{Uü<ôë“30º©AòPçŽïï_>kÞ‚°VÐ]¹{ä®q43s¦z²Q®²°ª#zi3iîÜTžbzn@¨¿;m,È]r{iÆöµµwq o/¹'tük\™¿Ŭ…P¤š«Ë'‹()ž`Ld£Ì_ --䛉l” ç*Lë`—ý!¸[- ´bŸ·?ÓùPØy«^Û“±ýb"ȶn@<}¬Û¬ð¤D~Ænµø¼tÜuíJK-i–Úдö^ÃN0Ѥš‰°GLNS`6‹;† Ï]{±Y°‹û"-¦ªÂd®aÆè(øíË`‡p¥þß‘°çÿ3Z¿/añ¯É¤G¦j¿¿Ãc‡B(¬Íiò˜ƒ®;¶Ý¦¾xÚ[ÏÁsó§g´D ‚b&[µÝ&Y>199;·î~až@ µv ãµÝîɘ¾vÛR̦œYßÉÆeD7Eo&ζ‘…:—A•æ…¼·a‹íxñcÞa³ä°zúYUðÏËñ/ªŒ}ûëÁOµ3_ks2½©†ÐÀO6ì W v<7¾¼|ü÷g̱ Þ*ÇÆ« \Ã!b6‡Ka÷·‹EÊÏ———g¦ÔT'=<&+k­ c«ÑÎÖ£6­éËùœ´f9÷ó4ó«žZÜŽ‹và;æŽ(+1w‡Z·cNNyyΜ3DÊa‡CàuëÒ+55{  ŒìõxMÍZøÔîQ­î5·1+FMÁ×z Îúwf͇ ==?t±;O`[œb±¯ÞiÄvê*–ßê!*?3¹|pt@@í %‰‰c ŸLIwË~ftTfFzZIE@à¬ÈÇ&Œ±*³IìœklŸØP_/§>QƒÇ”΂¢õ]ðÌ 1ƒ¡r'ŸÒÑqq¾Ä| Fóƒî$Ôi=gœ‹1N2.žÓ˜Þ§/á#’lŵxÌTc]³:󨀤{fF÷Ì{f]Pîº}æâ‡]ôDm|»Z7ÓՑ‰¢ «ÏÒý¾69Óp$¶žz¬›Ó‚§µ´¶ò¥ÿSÝÅFz<_´¥í¯WI„®1±,XÛ@þ•„k޼ÛÅ0L]ö}#±ßðA‘IÉUÕIɃ£3°×¶‘¡ÁU•ê½Êª à<ðöK"†fäWÂn/©²$9=dIMrFzÁj¸Y´deIjÚP¶#_OžÖVœ`½æcË[…׌Õš;üñµÃs[G‘WáÕjó&.Á›ÔeUÅÚÚFÞ,“ ×0 ëM¿P{´iŸŒ ®¬z V€¿~YUÉI‘ CãÄ \/ºüÑ…°?Æ òØäœ€>Ž›wL/)a#¹¾äæ­%Øyéû÷f`%Áïs€åÌ.pcz­$j~½ó#üº LšVR‡? l˜< `ìÛ2•†íŠb¾b©é6®ÚºåJE¨¸ŽUìî˜ÒóNI!PÞbÔçù„DÐs!2^J&O©*zNøúÍø/îî³£AóAA@Z H?°~fI€3’@ps\å@l (y]Ós 5N˜Ð>,##Ç´|øÂôŒ¡C§ÏÁP]¸ðJ _Ú°1Ksrá¼ñlÿþƒç ެÅ}Jê˜—Ü ŽË 9!Û*èW Ï¨‡ô¶nKèc2Nm”c©7Œ•œ£àY´Ó%î4á¾0qðÊ1åcnÏYY1¢¯†KÂu <ÄÌG¸£Z|hyIn@’v*)ɨ¶w&~7ˆ·Ýër\0úùv¿á ñâ…÷¶u!xÃM &µ÷åõÉnJHg1ßyÿà¹Õ ÇßãÎt°‚%êQMÛ¾ßðúëôƒ úþ{¼ðû.\x0@ lïÎO^}Ã÷Ûè‡Üðý÷›&?Q½¥éðèB¹&žCØKFÁZzÕ[„·¡2¼”áÙzš’J1$v8FÓ¤ÂðSp¯Û‹J ðßzãtÀs4¾ºŽfí÷ §<ƒBþ"¾Äð3€¿(ûžÃñ]œÿžÚ¡ñç|Žoœ'ÍTf>¯Hx>Ôû„-i¦ÜüW´—væpí¥v®ý¦}˜~ãg•²Žj/© εY/µSë€~¶ñÕõb•ìÃðÀmü÷9ÿnþûœ· 僖aLË…ÕDÓ²®¡?h\_.ûfÙ¾”Íp.Û–PÆ© ¤„S)†3õd/JXàŒåØ#zdŸ~`ÇtKqÚò7‹ÈtÉ7€Þ[š_CI”8M9|Ž·NùóøÇÐI®û|ŸáÏs»´Ú'ñ–C Ñ;ÖÞ¥¹px9šç ÍRùÖ¶„(E¬—çãhc+‹ùIëeá.ëå‘zJ– J6ˆi±'à`ëc>Ò€¢ÒɺÑÖ14L)¤Ò%Ü R"µ@s :Ì€ÑÖ ÓÆDÒø|LácƲÏäC­‘Öô"ô}–¯˜|¨—¼å ßSm¡òuþB&NÔ*ðÑ¿0Õ'ÅžOnI.\ŸÏpñY†#u>ÙÆøƒµ>²h¯<¾ ø4ª? ˜þ G X¹FçÞR[l¯HVzþ?Ÿ¨žãí©Í`ñßlGë7gïÖp¡Šáº=g2}¢¹ç¸©Ûs7Ó¿€ã[­Å¿ »ëÅÆ—ɦùV5“Ïø]G1ˆãxÖ[BÃǸR\O0Sy ð“Â`ÜÊÊí„YÈËY:Õudœ–’Ç23ÈÒZÝÏiZÅpö”f%õ=’ÇêÙd `VÃù>\¯µhÇÏáºüÛª©>à– ç|„äî›™õ†X­ª `~±P{ 7ÞfãksFZ€[ºð}˜g¼Ïô)´ÚCࣺ$é|u2@\ÂðÏ^hKÐÄ=/KÐDg£ž“É×1ùEz/y?¨LN¬mÖ€|>k 6þ{”íŠåöüQïeà^ÄñV{_³§ v³É÷¡|†?Ëð"[Ú&6ªã˜|·A`@q´˜éw-'Ô:»Lg¿=ËÆöàlš’ªY ÔA± yõPÇ™™¶Ru^øí7,bìÓÓíÙà«O[Dþ(ƒæ7ñY¶‡‡x†³×·°ñ0V['t6÷ kp‹#›÷Öˬ·Æê½[ªùDq5ó‰Fkï yœïÇøãôÞ-å½»ŒÕk´ö®§õ®¸Ÿ'¶ZÆfÑ«6¾–تÐCúX(ecÁÈûüS†—ªïQé€ÓpË =uÙÞÚS¬µ‰€ó—¾sí#˜ö¥ÆàéÓ~=HÒùêzá›a3¾Mþl«‡£õ/â§êù7ëàC"­ËèÌD[l¥Êá}HïÜCŽº)1Ýbð^2²éÆÛòbýcfø1бB\Âð% /ãüù”º˜y´ñ çϧ|ÀË8¾™óóø7Å»(‰ñóm|†·2üÌÈH§*è©w‡ØiýW¡<0„cm}nAÛñZì7º°åï ?Þ°\VßcºÛ.ƒ:“X»ÑWx„õ¶x9 ðrfßݳ[]Ç¢zΆ¾:Ç“µ­c7Ñ›ãu¶˜•&¨õUé{¼ÔËáAfV7”…‘xEþ+=ðg¿¯EJ~!Kø=NïÂ70¢Öî|Ã1ßìÈç«ÑH{>¬:/³žJÖÚ%ýªñÕuôÀ+8žÇp–Ù‹ú™ü†³$]|ÆObò+íø… 'E‹ÉÐÇ8]ç¯Ô’ö5 k¢{¾Ö5±¹3Ô–*KŸÀ¤W1¼¤0ÿpH|‡ñ‡Ù¸`~¡šõíÚ~Œ }ÀÚ:Œ÷á?4¶ºŽ>žAÙGÿ=þ&t‰óßÓùlfúóõRÊÖ9‘yÚþñZû-1Ð~ÞЬÛwζµ”áUŽÚ¬I/¥‰Lz6× ÿì92Çü9?ÀeÔ‡¥r…k`ÚE ýd<ú'áçN?“w«½ýØÛÇÚ§„‡ÿ5_½C|¦Ïü¸¹ðìáƒ"§©-ÏÍ ÏbÇri~T“ßfýÔçÅ@\»=é>²œI(wZ„á›7n^7¢lç ÀÜͳyûºô JÒnkè%°SÏé‚1Aô ¦ïÐèÛ債“p®cˆ ‹Âìc™ õ™áŸ¤LÙòòäฤ?ª–œ™mm3sÔ¯¾þÑì×'íˆQ=eJõˆÈ;âwU}2§¿ÒØñ¤°uõ7Ô úðíeõ1911é¥k^ü¨þOÃG̯0#C²§ål>k]ùi’a$Ò}÷Ôo!d·³_§íìbx¶‡y}ÌúæÝßë O—ð>£x©ï4”óeÃ{qöqùCËïYv¨ ½Göûå²/¸lÃû–mêF]­³/éìßÉŸìK–íÝd·êìó6Ù öy­ÛE™ ìU|ž·Ù…Ͷ¶“^¥K¿`']é,[ÕC\zwö‡]ØZžîÆÞ ³/Ú±MÀ¾öèÊ®ÔÙر݆Ðôv> ìó¼=ïÙµ3ð8nÔÏÔl`kF8Ç÷ð½6]3–>‚ãlg èxáüSW¶êédùžžá›l|™¤L/™–ÓùI0wÓs=ög$¤>lÄ„ÛÆcÁÇן WÏ_ØØï?´É¾ì÷½Ñý½SµÎ¾¤³ÿã‹k£ jÐe·êìóÿÕeóñõg]nG6¾Öw·#{—6³ !\vbÇ×"Ížà)I?fÏÁšÝ„fÏ\ÀEy)ß7SÜa¾#}†öçÿEÐv`ïkàyÀÏ”­£ILÓp)B׫•õo1×·Ù¶ã¡úpþ)‡Dµ´‹yÿ.³ñYÄiý¥ïyþÀûëMf¥âÙïÛö:¼¿ÞP7vaWëìK:ûmîºËnÕÙç§ê²yÙØÜ.¬¿¶u· ï¯fÖ_ýµU˜%æyÒ(±(‚­XX©ùPËU(ŽZš ‘#ûÕß`gucƒå¥[^gƒå§[^ç³™Õƒô$ˆ²wÛK‡˜vô\º­½ó¡½WÅ/uéÐj‡(¢`Žoe^Æñ%€ó¸–3×ò!nÕ#[=jÖµ?jùQ®×,I™ g#ÃÛAÎ Í’K¨ös»°_ý vVW6³d£M º¯».Ì’Ý¥sµÙÉ[Ö3[6:ئl6ƒÙr®Í6zdØ6JqhC^ªG†ñxIFYAeƒ…?ç+ø|„ãHq?÷¢Ç‘\ú\:´ÊAú*.â¢óYk{ˆSÙ¸Y IçIŒdHÑ¥]Àö¤ÿàBÅGñ1ùÛ/Gišxåaã.œÐ_qh4ioLäÿLÆ÷OÝ=µ?û}žyæ™e¿tH"ý^Æ&éçâ=y¢!\È¥†‹º£Šq” mਢ8Áî ÷EkzÄïöÙpe¶^«ËüžP¿\}if†Qò¯–Ô®Õo.1¿£ÎEÃ5í-Kٹ˸ÿ$ÅÀ{k8:¦éo‡xp.Wð#´X/#ídº†ÅSÜÅ2ÖZBº«á )îg)´e¤cÚŽÒ¬ŸÛªSlhƒŽ:ö„ú†YÑÿØ¡ÎE;´w‰Uç¨-BˆD4[ªm¨Ç#`Í6«5•$fHr¤«5•ÑüÈ;]¬y”ãÁݬyŸ©³Y“ŸüšÂì§é…^î WŒ  oÐq'´²4šÛ 壳'¼7:fÕÙ:î‚v÷ˆû¡'tëÝgÖ‹ ]íú?r¸›]?g¶ˆêÁ®_ñ#'íªÌḱ«]•½üÈ”®£T>ÃÜ‚ºŒSùc~d3²©Ï#Dæ0›ðùæË𭎸b´¸2רøi¨pÞŽ†Ÿü’îkIµ¶œ|j‡;kYþøCmHcø'´^ÐÔŠ»húX.Úp®¿A¿H’ÅlÓ½7ˆ‰é6ÊÅ|fÁrªkoØŽ8ö†p‰ã‰]{Cø‘¹]{Cf6Á›»ö†Él×{C­˜ÕK4¿±°T1α¡ :ê4µ'Ô·´ ÊçÄ“¨'¼7ÚªéËt«Ñb³)Ãå2 _æ`QÈ'[Ĭ`$ó- G*ù‘•Ü¢\’â¡áØìhQ8RÌ,p°è³ í`V{…f´PÅèoCtÔ)¢'Ô7¨'ÔyBOhïrUfë¨KrO¨_ë|ÍF`2žd|Ö[X“…>Ü*|d8 à¸ÙÖv>ëlÌ$“‡1óôIk6þq]8ÔKíðw•=⾸®GÜ¿Ø#Þo°á ©wÁ“{Äý¸|j¥LÏÔÂ.óWžÇ¬—Ú}þ:AˆJ]]ç¯S+?òroš£á¸¥›7]Äê:OЗÉî:•b~¤Mï‹î$†j±Ge!õ´¶G”ê(ceö¸^E ‹6œñ²˜gÜÄ<µÍ“F¨ÿa¼—Žñ²QOž!b‰Ž2V=êiÔGÄé(cMè±ï#ðÎxõüž,Þ”ó©4Ág”#ZÐ mtLhahQhqW”Ž;ylh‰jPË´(SÇæÒ/è rÄKpI-£1&àã5œŒ`¸¬ãe¸xo†—k8žÎpð †Wpü•¯År%¯÷K+_‹;å*ÎOpŒ äi|,É]ðéÝqVïŒßÀgþþØo೺ãLÏٿϱἽ÷™ž¼]{tü3†ÏåøóVûÓ¸ ðy.ì¶Ú_ùŠáM?`•£Ìax3ÇC¬öTö2¼…ã¥zgz¶ò~œ¢÷×Ç ŸÏq~ð¦ç^­]¸² ¾¯;ÎêÝÿøßÀ_ø üÅî8Óó¥ßÀ_¶á¼½˜ž¯p;×èø<†äx»ÕþÔþ*·ÛA«ýZ~ˆã¯ëöÏaøan·Uºý1üÇOé|O¦çQŽëüb†ãøf;ÿ;ÊêÉs‹uTúÁŠ¢ÆEŽÞâxW¿ .âÜnžEȰ¡¼¶$}Ý1ÇÆTþÆÆåf[÷¯p;ÖܪãñV5¢sx˜vJA:Äj ËfÖR‚t9ÌÃö·y&é:/¯{&¼Vþ?ÒïÕï§ ¿ÛJ¾) òÀ$2éÉþ!ä1èù„×xÁõ¾.nädww!.¡KaÏnGÈêOÒ7˜h‹#®`Ÿ[Û„Eò@›Ûò9Ñ–ÇV¦üª¶ùÂs˜ßÂ‘Æ àg¯Ë¤a¬c¢xÇX¾N|1èaÅ;-q ÿði ц¶0„ mðJ%žâ\!Ö¡-ˆÚ– Ïs°áWPb³a Ól9´|eBè÷·ÑÂ8òšvÄâ¡Ùª¾G[)F[XÃlU¡9†Kq5†ê(؃£J˜¿=* 9¶;xXØZÊObœí ‡ú_³Ó Iß¼tDô°q;ûéçšÌÅ|ܪãñçÎb¥Õ¢²¶=MÆw,ú/8²•Y4X?E¡mŠMè CP}æd¤ò8·9‡›Ïüo€—°vûk<ñYG%¢¬.KÏÙîKŒ8…ˆ=.UÂD·…ŽIú6¦é|†+Q:ŸÙˆ¾ašo‡9´ù„Y›ƒ¬sVñæ!~®óXG¢Lœý£ô9ÝŠðeŽÍÜ,>JX›[º9O¢ˆœç{7™cLç)AV4ç©Ô+]œ'Ñœ§ ~Ôy ™"sž&8·ë“äŒ×~³ví7Òï·íù0 a4ß!+Èj$ Ï7Ñn"b$ÒÎøq4‘ßâßù)Yý"¨Ï©^²ÍdEï‹'É"(jx“¼ŠîCÙØ‹P²ƒ—‹~},Ú¢1…ÝÿSH3éñ0)Ñ^%ù'NDø¾ê‘è D'ôØVTŸäp½¡–éZ¬Ýû@ úXžÉpøÄÙ ÷˜p_…³çê“éŠöm² ¢¾X6Ü츰w焘¼n»úb[‰pÿ(ÎP߇‚'ÿÍZP؃hAÜAÁzA£/¤tÅ"-(]N}gÐ-^ø*Rõ‚«­¯iÁHaZAD ªGÔ÷p¦C…ûz‡&üxIÛ‹\îsˆÐ¦³ÁEdzËh³Ðææì"÷UQF¹A£ U‹A½öª ¤Žçm“ìZˆwÙµó(ûì.ÛYØ##*[é+c¦¾k·áæC]6o¾­-\6˜‚5éˆÖ0B "ÌFªƒìÕnLªÂjp”ÝAe‡ÙÉ&LªÚÀ>D;¨Í­¤Ù® lGf3¥<ª}‚S,ÄKP†*ãã$žv!зçÝ„J —;W*ÐRx—(ªÜûîšÖPOž Ï„¶ãFuk»º7J×Õš¿¾ Öî´ëÒ3½äÓÈàtÚED^ÙFÎûx JOw¨@ªt§U€àúÁª¹`€ÿá?4׾<—ý%^%ýn×~ÜQkð‹»HÀNü"¨Ñù/„±ÿYx\ ‚¦y½å|Ip“khkhó'šõ¬ZØxtùÒÃG–-;B~YzøðÒeGްâmÂuq"4eüIà6òH}Z÷ ÷‘JÝÊIè7‘I=V“ØqIàŠ\¿®ªˆPžøã)¨ß›0&¥¾¢“áZ¥¿.Â\ ±¼{Cè·°ýúõÎ2ZV…2´´âÃK»¢LO'(â"±¯¢œû:¹1QTX,µ¤ »v¼zýú/G5 £›ä1|\˜ÁRyçæÕ«Â ÄÉ —éKt‚"e |t,37Ô#,<,Ü#"4";=í6äé›=Ýg„üt¢Û¦¡á^Cûl-•ãžæìîÑKp÷w÷Oë5ÚÝ7)ÊÝ¿Wk¡ãÞƒkÜ3<4<ü¾á•üðŸötÂw©ðʆ ‡ÎÎW¡¤¯èwe0Îo÷)Íow.­Îow¥nôÃ~ôb‰Ô À­aø,€¼KžIÉ•¿™jÉî÷ #ñ¶ãÙó³Ã¾+Ÿ2¥|üäÉã÷œ9û»}gÎþܲçÜÙ½{OŸÃöuTí (›<©¼|òá?¾·|òdÊ'[·­Yõ쳫V?»âæÙ³7nœ=wƒÜ½qöíÞ8wææÏßJO=»jͶmkÚ¶wîµ_a-œiG¸7±Žè‚Q JBÃÐpTŒ`YïÒ<×ZêF´aðk4Ù‹‘(¥‡öºõëÒÞÎE]€ŸþŠSÏUÏŸïP´™<éØd¡ðÿh¾ºž7¯+Ò£ATS·£Aâ¼OºN,M<4èè'ê€ZÙ1p\¡»¥E& 8! ~`ë}Ô “ÃÛΞ{—¨~þ¿ Œ=hiCŒFf† _ˆ’H¾ÀD¢?"#äl"~Ž^Vˆ€¥9 }åI [\SØÔaf8ño‘Î_gì‹M¤Y½ŠcÔ(1ä]õ;õ»wñ¨î}¨n;«ntf¸ô…,ˆ_PÕpT‚ú1FÙ’@ë“p£€æ*t±±«WÄ¿ BoZ3~_W£ Æ«bˆZW¢Ö½‹Ý°Øè$J_JW Þò í RH’ÞH¤?Å4ø[¡‹þ3 E´Ÿ±°‹œˆ@Ü ;ÒÓÀù³¤Èì!~[&DÜ +½°¯>ÌÛ“½J26¼X5±îýi§îŒªªˆp’å?ùäØcVÞ*urÉ9—’|~oÁº²²XS_ßÔDõUÊVKbLuñ•I4p’–Š>²«2)õ[^QýåK–Ç‘; ;)z¸ô2\¸vá$´¡ø°”øLÅUpp£‚Du7Si†¼„PÈà·^†BjQ0Ž?üôKuèÝ;jêƒÏðyõéÓ—CFßËgL£ÒeQ¾¤ÌùMé.¢+¸{é"$… !Ýh¨`¾‹/~ù©: Þ‘•þÙ|A™3þòéÓ§ÍX¾ ¤‚A9_gÙÉÎÚ㿟ORÿNúê«ÉЋ¡j(^¯Î§w[X>T>WC_@ìй„þéOXz‚þÀèiŒB=´Ø·HYç!¡þ6Âê:x¡Æï¡·•é<0ÁÄC@B6Ùˆ¶ŠX ,Ò»w¯‹¿DböuÔì“ýü-TЮ~C*dþŽXèµÜxú#µ|ôˆÔu†lV‚7w†ä9²½Yâ#àÃÏöGðAµÈyäÖæŸno&· uyЙ~쮳À·è8 „ÝÒF ”§áÚøòpÛÈ Ä»´mE¶‹µPmÒ[\E ^óƒy“é’íêÄÞG1Ü•ïR/<`+Ú[Nž’â)Y¢¯&ªô¦O+Ðð$ú”]ÿýõ¤“z÷9ìÔª~pY¬5Vþ:H<æáA£ Ë0ñ é:{q{_ŠÂo_!Gþ†Å/ÿöÔ;›¤ë_¼sê–gÇ 9àçÉ»AÇç…A¢˜žJÖò÷EÇÏ'ËŽ].zu¾†,[z¸=Âs˜¶ FaüЗ=²Œàó˜Ñ H%ÂÂc^Š6JÁ,^$ÜŒÀôõ…ÐZ‹8]½ á&8µã‹O¡¸6—”9àD›ÙÉ„˜Ðo„”F/mè40áxÙjÉr˜ƒ0Ðk«Ô…à§XwxXþ!Žb;ª«Ên!^ËѤ%ä°¦ãåù¬´Ä Êî=Ÿ=½íÁÉÅïþÆ57·Á_íéåäæ{㪿 ñ'Šr‡‰„ÈécÆ(ž*AJ/`ûÀì_nP¨®?ÆÉè²´TrgÙÔ<ÙÝà™F@F ö˜~ŠphßÏó~¦©Ì™÷Ë>øÆÕ½¿Ì#‘Mø¹éç}¿4ýŠ'©{èÑŸ›ÀtpBxS|“Gzìý ˜¿Ãá—Ë8úòåËdËåËêøyîòem5’B»­pð—°Æn¥qFR¡øìd'7\&—;ò* ’¹—/ —/# 7  ðm½°·VÜ›µ‰ ?\¦_Ñ—o^&km:¾ƒŸn ‰ PòŒdžËTËXvã*|Á¦¡#BE"š/Èꔿ¶iêÍãXÌéS®^/^½Úù;(E£VUר?C7Rs’s¹xW-÷zX%¡«ô„¸Öª.^º¯³ìÚGvýਲ਼ºFß  1èŸÎ+à-Ð[ÁÚ‘®vöRÉÐ`{{cS<»•/²N¨ÛUáÝ.”“Mä!øQwúR'ÄްL±öq*qú8’9ÑûÚàÐ!iGYÙ‘£eðuôHÙñÛW¯Ý¾}íêmDhz ôP˜.™²³„Eäo߇Å–,*pv$à¬}gSoq Ï2+L["2ÈnòtÐëZfo$¸*·$ìü£°µvsAµ²“èÖ \ õwž,|è ûD¯”8L#õüv²臧Ë»°¾àá³à¥ððž‚ì ØÁÌRW<Çâ<*…Âá°pVpÅà<ˆ‡ä!{(N®nî)@Pœ\Ü܃éÛ³Q¾û8Tí>­A/¡7ÐÀZ\댃k:’ ¾¥~‚M½_~â‰-ê$©A­%ý;ïÍ>sf(ÓtLÈ_á‘UO_¬½Öè)¬ª‹úèß]ÝÄ í©Ë³SÒÓ‡7®ZÕ˜8Ý,Ý©¿â,Šc V?‘Ÿçå5Ï¿—{ñ_0r Ôñ- Û=É¿·5!œâPµžÑŸ¬0ÂWE9ý..vܸ¸XqptT~AYtÔö¨Z¯ø„QSZ†’ïå54 ÞC_‹ S’/f„‡……f±O°ñ8P÷>¨€†PokMØ!ÛÛR×Ò›gœã]±Ì×–›ã’š2|ËÁòŠ­¯”W<Ƭz¥¢l?ÒÓɱi>®.ùù»býû=·¦mLac£fRÜ4½F3|Mb"ØwÙ ‘îclɳWñ$cq½º†º×K©—ëék™êq2- \ÚE£5 ['DtüC4Ö#G&‘—è\çãÓzœq^ª{¢Ê7 o‘ ‹*za¼ß ú‡vÞRZRR:µ.66vH½LÚ:TU]¹zåŠ_V,_å¦A¹'¡óg`1Ú—!fö:¬iSëc‡ ‰­ŸZ9xp¤LÖ,‡B´è L–/牪/°|q™Š/Ë8íãØÙWKììK;˲§¡ÇS!}z §É…žÃy^Þ¢e¢Ë¡¥çnN¦5–[þ‡<J!7é^žÓsBäC!B—E©äuÊAM™¥~ÏfŽdç;ZJÙÏ‘à\eÈŽÔò-«ëŽÌ‚R×çEŽTçã¼Ä‘q€ÜgÈË6 …vòG@^±"8YhÇ;9¨s>Ž3 ¯r¤Ñ2o9Ä‘i€<ÉÃYX÷Ó¼¬âO Èu†¨vÈ)")¶l®BÍÂJ^èVÊbã Â4à̵C>§ˆ$rDÏKöÛ8âV—d/‡"Ê+bñ$ÇÕŽ³…<r¸åY–ÿÂÿÒöQÛãð[vÙ¥mc—¾»ìÂÒaY‘*"MAì€Ò±#ØP ¢¢F}X£‰5±‘Dן¦˜ÄøKQS-i&yy)–4ó^ò’Èîð¹»wAãïûw–Ù½wê™33gΜ™9'âx ñWq7øÂR2*Ñ¥ªUÆ[b £LpÌç£NÖaü ÅSN|®öÕ+W^­}n$}Ñ~•VÛïÒCì±èlr2²ÐÈbNîÎ˽‹sîææÑÒvü|;žÕN1pÑã{^”B™¨^MžšØÁåuw"—åXÂ_arÐ Q€ÜÈUÑÔfúÐŒà]±á¡C²ðkŠEW+j>n ðWìòš¶k¨R~òä>äæ³ûëÍ¥KÇÐIÃðkæ¡Ü÷nÊÕ×Ó"zwTV]Íϧ3'*‚×|§ðyMCÓß?§aÓÎÙƒ?þisYÉön÷¡UÝ÷ { ˆ_pIÉNU¦¼ýH—N[Z.®YmZÄ#χ‰©)YQóI+¾„w¡)(¹í“ªŠk‹îþ¹¤ò‰qO6z‡iš±}ýcbS,@“‡ö(Pw^ÞõªJü&þ²»9˜©Ø3¹ŒåzdÞgžhVCW¸ ¼›\ Ä š6Ñ\¦Ü>‡.±[ïß¿ôZÆNìÁù=øùáÄš!1ϳR×B] |jA®™8YrÃ!5¤Ít´MûÃ+¯€=ÍÛé;F\›¾mZƒ[QúŸïéAÙ]]]Uˆû·6èLDÄ®kùðÊ[vËqÁDU+g@aÐ̧Q&Šyò@rE_éA³¹$§…³Œ²ÒÖÅã“CW÷”Nn9~Þaþì<Ó²1¯¨ªæomjB|FX~#ñ?_õ[PCAGîÚ±¿žhJϼÖdòQš >Œ^)¨ÑÒqAK)8N“hf4`D¯ä_D²kß]<øžóV^^RÂìñnÝÊ$Ó¯öOÛ¶¶‘âà±m+˜i÷]bW¼ïkÉ»¼]ñ’슋E.Ó;ç ^wœp÷Ô©¯¿As å ?8ø&^#ZóóêÍ›W£ÏEsÿðóyôÜ‹'úŽ>†¦à]y ÏT¿8iñªªýiû»®^¨>š_¸wüŠëSÏíºsyüß§>óöNÁ`9¾òÖ̹ÄdùÜ1kQ¾Ðeß·®Ø`6—QêmÜÞ±÷ÙààcááãÆeè½"ŸX·ûŒÁ€º?¤«5tKäˆÀd¸³†2¨ ÆÀÄ Gy©ª8ñjÿ'*g C[ñ–í۷Я°Ÿ¼R¸¾å&ØYG”måÒ¡m“j«ëj:6Ly&ë™ÿxwêé‘ÅGÊ×­«Ý–yñàï—×DA•~ZƒD«ï,F˯µ¯ã®ßøxã Fõ¦e§ýþj¤p£øëÛb²êÒ:;L4QŸ¡4A·¾z ïÈmcâçÇÌ (€'šôðt&¸bHü•8Z÷5ùz'>¥"hñÞ¯ð§¯ßÄÿèf_=x7¡ÍA½ºƒ¿ ZhIÞXïæ® ü_Ws—­i'®«Ë>~±ÍÍØæ|‚?@{œ˜þ…—tMª¡:Êœ¥#P¼ñf•€K¯¢ÅTÝcÅ'Îr³Ûûð}xAcٳ܇ ¾oâÍh½«÷O¢HdDM¸çÞU®˜?Wô*û+ñçõs¸é°Ëv‰GYCtŠ«!²Aâ«¿ýþÇÔÛƒÐ~ê»=vsІI }„!L!“2ž¹[”.L)–»É=ý¨°H°ê}íb‚íâEÚÁ?ÙàÐð)d=t1á›_Áæ·s“ãó4Øè@d_£GùFÒ/_~ñ“À/~àã\ü,†…r§ÝãÇ©fx4x5z7ÉšäMŠ&å Õ,m“®IßÒdh46„Î0͈îw»uKº¥ÝîÝÝžÝ^ÝÞݲny·¢[ù¸¶[×­ïé6t»C»ÃºMÝáÝݑ݃º£öD'ÀЖÈÜd›N2ÞglðLŸiZiU=[¬ëy®xîŸÝ(Æ`IL"\¾dÊe»&ºç’Žj±G#D£óƒñcºK{¦Tv®úG|9üªe=B,»ÃQòµç¸Úë3Êßz±Í7Æ ‹ñm{ñRù ˜ÑPbÚ4±:C¬¦ !’GÄ÷Å©SÑ‹xK×È€x²÷ÊËtó¿íí¨—ÒÖ?íU/ï`ÆÑ+ìëèEöåóíZú={<ýÑx¨‚\ÈEã´Ñç£'×ø}ÀñRP³Ø$ð'ögûOß–àËß•àK—o¡¤ÛEÌF´·§ÿ²¿é㵚>±ŸE/ìoüdíFŠ£ÞÇ)’§¡?©a´ÕR‹¨•„Lƒ¾z2“:W>N-„Êò(˜‡J´Õô©½ù¢Š&ú@Ù  5oâNOº ;¼¸¸ñTÚi–yÂàE÷õb:Ò~ãÿ‡7>ùäFÌ]ÿú³Ïÿyèþy:tˆ0´²)8Ÿ¾Ë"•Cq+ôv›âÓe«èo9ªoW=DH‡ÿqD`ÈvD¢Ô–G»jçS¿‹³‹ùôn$Xc0‘c¶¬Zvü¸#›]ï¼óPu–’ƾØþzÐYJ/‹‹GN–Q#ÿˬÌÏ„Þ7ZTXW—Xpipòè‘Å`øŒàILd«Ú?kûŽì© !óÞygnjsmq#ë®0<;HíÍÚžfe>ƒzŒ w6²ç ݼnä…{æ4è‰ßBOôÞ%œX“üï?|„Y˜Ñu‰=b\П“ÅΪY¸°¦zÑ"ü%êøôS¼ìnÇ€d#b vï–ÍÝûnßž?®_ûãkd&ØÜŠ6¥c‹x ìã Âøÿ‡–›üÒè’ºæ—^mªÞ»ßÞóß‚‰FÒ2ûoóf¬hF÷l›r$%¯±}zÛÓÀƒ¶mX¨Ä©ÿðg`¿–‰™W) ÿ›?3C‰ÃôGÃð Ó8¡½ë¥šàÅÉ 72p[ÂÁ¡:EˆÂþ…FX[WïZjÖ{„hÉ~ȵöÐ"Õ€4»ZÌ+tú#<%{D][º*, T¥ KÔ‡û«ýÃýõQjKPbŠ6ÔTÝ—j ôÄÊsÅsç¦g'E™ÌÞ wµÄ”=it®1tmE)OBK$ŠP½oJâ¶tļe[£swµñö—>ذ³`š*"(Ò_¦ÏI]t¿|°c)`NÕW™*,CZ—tú½½OM>–•”aˆòVè£ÜM£s{S}ôL€¦0O›$3£d‘AŠˆØ”U­# ¡"¢± ÿÉ­ç>!r*Î⬵°.2„ðl‡˜ó Úù… Pa¼T”æÑâÀS²…`šÇÂDÎ/ Õ@:ëŸÛ1Ö:<ÖÝÓ÷ò¨³9·YýrÉx¨û¤¸¸y W—íá6(XªO-ÈŸªÕ6îÿñ_{R«ðq•ïl­ÕZR–.:ùxeCLÞºÖy“'%áû£RB 3 _T•¦¶l]˜U·qÕÖ¥bäOO‰)ș쯶¤f$L¸ux]óU+>p•åu¢0Îýo¥£ëëÁÁÑYׇ½~©÷o—ð‡h ­F[p£ý.näæÞÿ7çI\½}-Ýrý™=”BDÍ4»F¤"›Ð8ì ädƒJd„#XlóÊs+%Ò™UÛ>ѵžE±gå~Ú²í) ý4.÷ÄæûŒ«|ë}«Heß]9{ßf#9’V> ãDq¨†cHwƒ|• GþJ8MÖž‹F½pIRÏ­×vD¯<Û‘ñÇötmç¾üÀë7‘êî’šµø··¾Mä£(ÈGÙåÙu#!ûÔ¾¯˜0¡¤8†ŒÌµÉøP÷7Ë…î¯r¦þο xúk]i™¿!@ëæî J1:C¯]¹$‘J«öõW‡—­ßþ¼~)÷u”Ú†qç…Ž4KNŠw€Al‰ P¥w®:ÏÈýƒbâ3Ôl–ĤU¨Ã‹–—„›<)šš 3_.ª'$4ð„=P$hǹRˆ@<ȸŸ¹¡©9eÕ&}k}^FIÉõ’ó¥þ•¼bõàÊÆÙÛ–/n_ºô­õM]7º·—4•LXW3bÙœ-³ë÷mXß:k[íK•ÿúbT —<8>vHÏZbÈ" jäþÅ›µæú nYú& f‚5S’c()ÿBCË)¥€ÄŸ!º±|lÚ´ Ù’(߯ÖÖ¤©¥]¸wLŽÅìi.ð³¸s}VÝóò²½"´QAžîâˆÄ”1#˪Ôa–lÆ;U5ÆùúMõÁ^«¼¾il5hó×¹eg®ò}­ToôMÉFË®j/Z”ai𗟧ZB£Rï~¹¹5*'޹ý8Ÿö!à;¬?©\¢ƒ4’ LÒô R˜@i¤P&cŒÅEL2buZîÈÜÝ`ß±¡Dªmžžß¶)òñÙS‚ýf× ·æO5§¬oܺ®xüÎtjóèçŠ+ðJü;šŒÔ[ÆO´ßßLÑ”îÔð‹7“KY±D vÅD”,L;Uº×Ξ=GŸ°k{W`ê¸Òvbȹ³ ­ˆÝuß<|j‹ðÉzP`n¿‡_ª©?âÔ`Ôû5zNμEÑäÚ;G„ ašC‘A‚ô„>À—ÓÙÕ?ÑwmÛÐúýz\j«¯çéÁÜ^-ûÕýAìW½ZžÃ¡Ä¯ñGùµÀÅÒê[vd[·ZwfSV*ö”—wT¬•Õ¤@Û´RÖèÏ­1iÖhkÐVÃ;2Ù vEæ Nj¥!‚;D0È­î «&ŪÑåNÏ·jdÖ«Ád(˜‘z¢Î>±‚ÊN·Y5†k{í kPnµU•[cÝ2ÌŠª ÙŽßaVj|©Uå|\fUÉNú‡é¬ ”ÑNñyøÆ!kœìÃb+›f¥.Z™ò@»U–&ƒ'Ïd›Õ+ÍêyB^â4«wŠÕ›@#³zZ¥PÆP0=ÕêžFÞU7  ÝçÖ¨k±ÎvÃfõ'Þjk¬5ʪ‚Cdäâ ·¤ôù0•.*VðiÃ!˜1¥¤ˆ¿ÆÏ*)u=º*Z$öôòü9Íಲ²~œ©¬þNœ©gšGãLÓ3ìd!âœÅ[Y«2¡Æ”zœy‹­Þ²"ùVhENn—zО'8‘›••Y¹4w©ãxˬ"x-K4'˜I·fÃ'ć<©D/YXØÅ$÷R’·"ýøž¶â{ö™{د7M›¶‰¸ ) )Äm@?õÌb_íÕ2WÚ·÷ÌêÍb¿²%µo§{å•Lj+ž6­œ=§­bQE‰¾å.Q¼aËh2ÐÉ BG1ä[rE¾e&Nغ ÕóÐA›ÈýS\‚§÷ð:âQBþßÇjF"$F­/Ú¯ž¬›5«ŽÜœ±ž>m=¹‘ýå3ü.~Ÿ7tŸ3Sðjü'îÅkP+âäÅEøxéè’Òúu+W¯ÁäÒR#Nå†1ÿ¡â¨4ŠâéüƒT4Ù2à0 R¸f'O¦ïÏG Íkµ“žùlÕÊ܂ݭǦŒTH"æ.®¦+|w¯^¶rîÌ/cno[¡‹¨cÞè8_4kʶ—G–<óéG‡Ó…o˜þú’ütÜŠ¿X²§ñoéÙÈݶwùOOÛ~~£­9&Å&UÃð‹(UwÊzÔ‚ý~"­´È‚ ƒ´·“ÃBQ—êQŒ¯FãkÏ¢7ßwÈ9p#Sš]19ÊÁtØgÒôºþ³«×?µ¯åÊíO¨#&tïåÍÇ‘”ÚQOEðDË$9„ä›Uë p˜7þKC$wœCa³‘ô×Ïå2ü_|¤¢!¨åû $khÀ÷fu»véè/ Ú]»´øóŸ„¢T‹âIx/¶á¿¹ÃÙ¨zg²ã‚BÏØw?Êš{ÏÐugÈɡ쾟D¢èeUBkCxÅÿ_žÃGå”å;£è“3‘Ë6‘ø/|<“­M滛16`rùŽw'hãr»Ï5ZRýý þê¹ZæŸ;ulŠ^kEš„–ÌAQÓ_F!A©igÖ­J(mœödù˜6ÿ¨ ­A®5¤d¤ŽÐæöÛ Ä5Ó.<56@åÝ«FßI^]».ç)s–%ÒßWí¯5ÉüÇÓ½—‰ì°äûÅIÌÞ½Ö7ª`sËžAYÚÄÒï4Nûâ6­6<ÁwÞtí(r  f¸îª€h`æÂý×¹ú¯Zã<É(`\_3SýÃ@ìĈƢA°ŸÎcÏ‘Š£Ç¨ÃGENn™6mÃÆ©Ó¢c²²6efeeN [³Æ&ï˜:mÚԦˮ¾8'Ö¸lhöÒ!ñÆ]ô¦HÛÓ>On**,ôÕfM ú{÷†I‡ ~óÝ…ÍÍ ­‹'€5ºÇé]ÛK'NœpiÂÄÉÍ wïIƒ®v¢Wü‰‰mK*ãÓÛV¶/6‰;b2Þž7lúÔÖ:i,Å‘"Jà5Óuü²˜—g’Cľ¯RÏo6\Ç×9GÆÙo‰ ’í™3¼%ºhJ×›øóÜŒwp>Ù.VáFú0¢lÍñöíh8T;‡*/‹´¼ÄK9c߯ßÒ£üV-ÃÓû·ˆß¯ :†˜†Z(c|D4wMpWò‚-uø,¥†œD^gO"ô€g=ø)á™» r‹»PÕûàè ~øÁC¸pàÊÀÍ× n¸'ÀwÜÛà>çYA„à@¦LyQJØ&¥ 0(Ð\x{S/ê𖽺óÏ4{¸Î' ˆ4^cî4 ù¼ôéæ?‡/ñ²Õâcçoãwа;ï]» „³öªdû"b}vÖgwq'ž…XlCÛ)6þC¹q¢7)Žß“‰á©œ2Ì ì—O›ÉŸq°„Z8Ç)F‘;â;'RÐÖÞÊœlš1„RO‡5åV¢·–U¹ã¢Ð™Áše+——Xhÿ¬«‹ìì£?üíûÖa›5 däþMÇ6È”³`§ÝÂÞÑ9+kmkÐ4ŠF \èç>¦Lü9£G¥øë‘ 1ÙHc ŒÜ@7~¢ih^ìðÁsflÚ4?oR}ìð–ÌÁsê[’8ãý³ûœ}lÚ븿ÃuŸÈØp»D1aè¨IJŸêHߨ„¨H©DQ”ž?Ù›ÃÏ¢qö·Ð6ü1 Ç C®Š»Å±ÕdJ–xŒsO€#¿Nç¸;Ê2§ð¼x>ýº «ùËDÿ±¯¦^þaßÅV·ÛtÌ?ª§âß§â§ÂÉï¦BÕMÌVQ.t[JWJõH$–IY…—„½íIS+|„«e$µîžuÞµÊ:¹ìÚ7¶oÈåHÇ‘d¸©I€k¦áþ8€¹ÁyÔˆ-<ötÏÜ×-—çͽ<ïmîz…(ûyô þÅâ@:»¢o*ÁSÖÂM]$Òk ‚®€”(‘ôB“~?Bˆom|àúôCû›ßîµ7¡hüÔU¦ˆî„vµm¹ìûƒÐ^ûª(ú7f 9ðKÔcàÐã2À‡Þbßµ½5‹ó¶½ÅŒºÿ:Ü$ÿÒÉ¢×ßÿ7áÖ R<’åW”F­ã‡ IKþùŒPôX ëCL¥=è©×ÎÄ÷ÐïX‚~·_dŒØ„>Á¦g¿<´ñÆ/^;\÷ԕέ‡Ð”­ð[á¿æÚcøêÔÎBrÅŸ¢ø+þ"rÅŸ¼÷} ï—d"4—ZùâwÙí¢¢žõÓmç‹.~~K R˜òT_ÆèœÒ±ÉŹfO!ý­jiëLCühßöÇý'}wà¯#jœZUÕzŸ²ÏO}¼3%H{®£ÈÝS©ò“‡Í…s†;%olîŠ+˜6?veîØÜÈ) ø6š¼¶az\鄘qE GhcJ™q²šÊöcK?mîL]¿ê*½²£ È=‹ò„-ZµaBç‚nSáòžœ^˜8gÌŠæÜÈ_û… {8íWznÞ„(__ÑïRˆêè»ÍY€ëK'5N F¤2D¦-HàÔÂ1“³6‚`þ¢‘|ñ="o'Šuw—ËÔ36LOmŽòug¼½½sç¿á_^`ˆÑ+—̰~f!ôÑ”WqWGEL\嫜ÙÐ0/p‘6Ü[­Éñ2¾è˜’ýoDèÆÜF­A]`XxnÁ˜ŠÎOñ9ëWaËSÖŽŸÐ1¹kó󼼕¢©¥}·De\4åÏó¬½Î’ÌËtFÚœ¤0²ÂGâcV%êa¥$´Ú4¬âqT¸sfQSÕßNáÅx×o£iç_FÓ.õ*•Ì&UnÕÎî};«r¾)J7.­º%¯üzÜŠ†;†_´* ”J4§$qÌè$ˆMs5›C³|ÈyúCÄ!*QGÌ”,’¥˜E±!©{¤Lõ]¾}:®o*öŽGcî¡5ï|0kÎ×/¤ ¡hgXHeF£!sôò²Œ'fíÊ)²£$üý›Ófü£hî3Wð¢@ÉÜ=þàÜ‚h`r‰x¬àô!øè™5Uie'{»f4 ¯êœ}’nHU×öËÚq‰Ñ !Ï@ÿÐASk²jJbË«–<Ó¶hÈìšò +š3?Ì ]t%³Ÿ|±+N§çþ¥|j„*ÝÏ~3§nçªö—ª’²ÃµþDÿì¶Œº)çrè߯ÜqéÚ‚(­V›{zÑa7ˈµ$ŽB£„õÙHЪÀl‡èMv+]r©í¡ßæ8¼é–-¢©Y…ÚbàVžD[D ͈M4ÏÉU/~øÃHfZ¼‘ /LT+J¢x¤ôP$úH­Ž 4‡Ç˜"Më»S, …;ÖÚf©Q¥Nµ¤fä&ÆT–L)ʉ ò5X`I5S˜?=?-]ãíZÕÚ5C|pΜD÷qKÛwï<™m RGÝÔbwÖÏ?È?’ ²›o¶¯ˆUGEeéSsjR,{ÇÑ(mÖžÂ1è*òFߡԣ‹r±÷/žª•»©3[tÓ7JÓR—2S3‡›ªâûøW|ÂåÿŠè;MAA&™A¢ö b}ÕäHÀô¾[L!Á+U |ÐüÖ“‚!Ršß|RÐG'Žš½qBÉìv^þü8ôû‚¥¿·  ±ÈþñSoà>øBˆBDÃ>w†Xa=ê’LH,Ò"µÃF3O, äÇ|¨4烖 aÌE´¾nS‚­wWFö³õé†ÿv1¶Ö¼áÉ_À“n@~ à÷ 8ý>n®‹ï96ávgBÜQƒ3+‚bë’_Ú¸ëGýq’‚<ãøQP¨¯`L•`ÏBCÁä®¶AB¸ÑLO2KÆð>Œ9‰¿¢LET³çGE¯‘«BzUûú†¬–«ŒÌ*•*4˾<Õœ¤fe*?Hm }£l{Æf`ª€5 ­RÈB:%iIð”‰XßøIyá:•.XËN¤Ðw–ê8ìŽÈ+ȧƒ‘Âa7Öš 0=¿Èm4•4+=âBi:]ê­Vˆ¼êQxv.Z_˜§ŠöÐÄȸ.‹!2ü»œ|³å£ä@LŽ¿Ãß2wágšÆ“p7Þ‹'¡ƒ¨±h3çc;bÐY„.©Þð Ô†b‰ÉH`­$„éîÜɉ#<2áºM@˜Qb·þÑz%V¡—pÎfåóhfµ-z¬ç”o`ntýöŸMøu4fpuw6¡jXgà3Ëíw7<Œ6ÛÚ¿ûŽiǧPƒcÐR¼‘‹¯Æ¾{ÜbÁÛ±©jvLjrb !Š>€M‡Ÿ^NÖc@²žXO'« N½.Ê_âÕ©Q¥|UæÖž¿`•=Òç0>™^žP?ï ë}Æ®ïì,OÊ2é =ì7-ö޲K‘]ªµ£¯m,c9ÿH[=V”áã ‘!Rx>WζQnü¥g §òDC3à€€ñÕ‰M¤ZŽ>:6~xÇð†¤Ü ÿuÔ·>oôoèôºÆ}—‡å²A¯áÉø0nAÝ(ÙP$=±»vT¹==‰¼pPÃóv;Eó"À8~…"qˆÿBI;ÈAÐÇ ±G ‰õõ÷»øHd;9·#½û·#‘°yõRï¦K¬7Ú€´ƒ©y×Ï$šòËDc¹×(Žœò•Ðz í£—ð«" 2£CŒÌ.Â6ú¾í·ÃæG~YÍŸo0b»Ê”1‘¶¶Ã¢[õä§®ŽËø,§õ¦a WM’‰Ž^Kt5#áE„f¡BûOÍȪDëq3>°ÿÙ4Ƕ)ÿˆšu„Ÿé½bìx< Ã<-Î3V£DÖÝ ¡âÏZå•1iž§©! Q²Eôþ0ÛÙ(féù«~ÌÑKöf?7¨‡B¼Úó#ÎDl ˜ˆZAÇ~z‹ø)›9b«¢A=ÖýA=Ü 9tÛñBJÏ"Qºõ ®ÅS>¡e—é©ö'/úM{ Å›{¢B^ã“’Ò{%Èq5“¦"âèÑ øÐ3‰JŽá7ú—Æ,A',ÐŒÀ!¸ÄnG!hyoÝoÏýsëpSï¥^úø;~ïUMUpן_òeÝBrZŽ»KÔ¡ìú¡¤e[z¿–)÷ ’©ã/ÌV åÐÏU’RZÊ@$fJHŽ>#½ã"&í`Vˆ·^LÆ8nýËÊ[ÚH]òñìÁ‹˜|R_\[»äòÞʸ¼dÔ(ÓBTŒñïHbOxÙ×ÿࢭv°å ?½`ïˆù£ÒuL§.½Ãöã‘z-yl+9yåÄÇÕÓ•õ÷ŸåÆáz[2ÓÑѽ·pÁt¢*ip¶[Ù66ü!P•€x·ç®ûÙo î§Åêàýa†}GòªZ——OÖÌj˜= ÿº´ÜÛüü)Û7Sse {—ŠFҔ⣅Ãè¸4ËÆéAjfˆO`‡½ÐG«oŽT«w7Ê)BÊû7×mÍ-)¯œ°pÌ|9ÅÓ°ô ‘J¸Ž‰Yv¥A©WêÑÛgcÎ^ë<ÙiÄ'?EÅ"ÕÞf§’Æô©ñg:Êß™d]üÝbÎ`᫦ÔûˆÏfê¥Î÷ æü„øæMUüŒ¸Ø ;¨ø‹ˆú)ëýWXEïûÏóŽËÿpúûlï†ÕäFM*´ôY~Ö‹#\‹ú¨° D ¿ùI.ÓüUveé?rðfSó_XPïØº¦³ò”ÐÙ—ðÅÙ±ÇOÜ›3cej¤ñ×öU* ³h·²ËóV!zÒn/›ˆü¨ý«ºÌ¥iþËh<ßTš8íÝÑûK·\ÈÌ¢1ˬåçc–„“¸¹çR"Z*»xñÚD Y–›xˆYC½DÆÜ5aÔǬµ½M/Çbô'[Ý»o+jÚŠÈAP"®& CÄ-Œ™(d ZyE O³ëíõÎð?Ô.=ö—pÕJÂ!^·øUW>¥Ï.ĆûªÎ|*]áóNÒWþ5½ø€Þ>ï‘áµ®ðù ÏÂÅûNÊ_öˆòŸr†OÂÅNÒ/zDúƒ.ø„ðù Ÿ*„Cþ op…?ýÈp(_D»Ê;ÂR´•—êD ß,Ùoàš}Ñåorø“p&\΂îôK‰øü-»h§`Z•¤ç8Áß-Êåorø“pÑB¸äÕ¿„›á$ž[´ Í8RÐÍ~¤<)»è/Å G€àï–íò7'Ç]xpHÁøò„àðnÂ9+Úoœáß_£(b^Þ …wúN¿¹9ªÍüÙ2—¿ÉáOÂéᢡÒMDøyãV"j¢«Þ/óñ&:Ò³9‚¿8Úåorø“pî´ {þnr„“xb£ ŽÂp”¡úMÁòp” ïÜ5>^™ŽI‚¿8Çåo"þN8.?„¯"Ážs‹mƒ÷á-çÇ÷\çˉT’ôvÉRrCϹ̳J޽À9ºãIdF'Ñãx:p p žŽ‡÷î_‘iñWøò¯—~Å—ñWH‹ÌÜu¼·†ÍD¨͸ÿ9R¢¦w»ßÁ×ð‡ø6Å}‚’Ãle z_à×"d‹HÛE^”†ö‚g-ÅñÏZ #{Yÿ×ö¯ 7…#7 ä¦rÓ@nÈM¹i 7 ä¦Ü4›rÓ@nÈM¹i 7 俀/ô&àšvñ¥ø@Ê„g¥s?L ÏRJÁ?KÉ~˜€qxà×sŒk‡Lïl´µÝùˆº÷K_~q÷Ö­Ýø6ê× ²S¶O^xïý×é½öÛ+6<¶}!4 ŹŸ >œŠ'w³àA.PƒR?°s ¹óÄ­Òìì‰f§*F®Ú W? <„Å×û; ½À«í„#ÈÕo~Íïû墸XlW%lV½M"t&Wçú’ö]ñ؆ôÞ“B:õúûï]HzÃÌÄr¿‘®OóÊÞá@ÂN:¶Üœµ Šx­tÂ;PŸz«£Â÷ Q·¦¾íá±pÀ1u®Öu ¿YYe—¡s,ktS':+s·kÖ19fm™HÜÍRO]e²ƒ¡Û¯8MKDЮ]»’%É¢³$qtœ„­ˆÏ’ÒœQ"n·HJÉÈæWÊÙ•¸,7‰„r»‰wCj1'³¥¹‰diY M#†eØÝˆF.o« ´–Q§ÚM¦ÒN»í–ð‰~8Íì†øä©LiFNeWõh]ËöõÞô;-ôøròÒs§…BtFpÞÜMí@¨\@ÅSTb=J’IgB%†–}4´ì@hÑÃв.hY€%"±‰¿ÖÉD—£µ-·{î¶p7ñ–»½iìÅ».Õ È-q!— „«Š®r W"¢ʼnvsÿo‘‹|S«˜¨/j¹Ó›Î¾Ø=^/vûþì^'àëAìR¢]¤„.ùƒ]΀œêϘ5å©=·[Äè±–»ìÅÞ4¹õNäzSŸžK%´7%“\4OË»JZ%%%Uëí-¦Y©7…Ü$¬'»ÛSFê¤I¹’ »’À{y{S^ž^ž»!³ÿ\ow©TâîáîPKx‹Äb7ý‘ áÁ–ù»»–´—7©._e¯ÝPäƒ-tÚ}7dÌ?‰vC^†„É·ž00˜—î´ u¤Ýl9®r§/âÛÒÙ”?“õùâÿ¯îñÔè“C¿ô.‘–ˆ¡zgI³Än#èÿ¿uFûh=Ø ú„ A¬ A®Q½ñýÅ5o·à¾ÛcÒ6ìn ^H:ó2é>0‘$qÝì)^úd¢ƒÄ0« ]&…Ï0•-¶GÙ™GmZ˜J|æ¡øöÈ„ôf®Ú…=•i¿† ù·£ÆøCŸpÝ"ÍÏ<Ê„ Šn"‚- Ùe¯rEIز-h“zâ{ŠRýlö[ÛvØûH =ƒ®‡Wô$ŽG"Òuøƒ&Ç÷{\TÊ‘¤wõ6’¯™ž‰i$t8ûnxÅSéøÔ®>ÇçLòŠ¢®Šö2%N•5D’‘ÁoÚ»v·Ä®]áC±0 ­À¡ ±ƒÍQq¹>!i©Å…¥“òÆXÒBöøô Ä?ÖhðÑf&LÌ›TZXœºï(ô«ÞG©ôÉ•6$D§ µ¤gÐk”J•ÑWm6ét!Cöá3tTæQPªʵN(-¡f=iOÇÇ¢i ðÁȱcžéÌlŸĉ ™Zƒ16Ê<8"ø§=!i–1ÓB.â‹~݇ŽòÍ¿@i2gÄŒ*¥R£7Ä •f %P¦‚ªCelc%´m¢09"8žÜC5м_Ûƒ !ºï‘Ö âÝMVCšBÔCqrî&ÉIÃÅG˜¸³†Œ TË“XT n­cÈÜÃ{[îÚéoî¶ Ú{ðõ؃EB/ “ Jç˜Yîánè(Aôׂ{z<ñ'Þ=R( ¦¹Cåƒ×,|äމ% *¼œƒüß#€Òߨa8ï½GÆôLyQ”ƒ`" Ô ô’¾ ÁP”mW»(&ï‰/ðhP3/GÍôäðé]Õ²«¡(ƒO@ßX-ð…¼…z¹ !J]jfà¶÷Þ%ë×ÓCG÷Æ2ÛÓŽ†"d›$ …öq7" f8Ò.q9HzKËmj÷ô~'óXSBRHDð¥‡Ìq’uØžFí$}?&/LzzøYêlÁ ¶“?î`Npq+WƒÂI ‡H‘ N™‘”;(zP´iH¼Î–/´2ú}¢öU*`”+}ÕÌ͵OtDSzÚ̸÷æj|©T!÷ˆQ)¤R(}!iJ(=¡ªQ8¿@>‰.âì¤Tæ$'uEû  È@ê!ƒ¢˜.aÆÐø!¦hµZ3(6!)ƒ¹IÊó×øk½!‹ˆÃÕ·˜¤ä´ô䔈MÑ.4øRÊÀ oä#€&vÁ@0åÒG"&Ë8ƒYÀ»¹Ã ”&ËÌCzÔ Ôƒ vµosÍ^9ÀõÐøËɽ5N8P§§9ÞnÞta•¢]˜tTà!À]8@R^a øK¥hƒ³Ék€Ñ ®¹qÛѾhÔÐÌІ:ßè:¸važ¯Š ó7o’~‘BpŸDz†ã-}“[ ðœ¨LtôÈ€îA]ž©B„yÖY)WíðERÙÒ8£Á(õ ð•J9™.$Ôg4éôÞœ”÷4éLÑR¡ºR/wO©6ÜT® ñf¥RO÷@ð‘pÞ!Úð°rS¸|8…Â(õðzxFBå …*$,ÒÓƒø˜¼.LˆDÒ•+P$‚w‰,2ÌáïïûS¤b¯ñ©\|·ÞÌžrE²_c¯ÙÒ…‚¿G?æ29æ/f>q@o¡£ U4Ú'šÂ•ò§œE ãRç–†¤Æ4ââüc¢'—76 ª¡±|rtLOvvÚ„ÂóÅc²² zú¬0{$,Ÿ3«jB|LLü„ªYs–WÈN5éõ†Ì¬±£+€QÀ'ÉÙqz '€l›Þ•5¦ø|á„´ììž ¥’)Œ>›P1zlV¦A¯7¥f¨xˆ'pM}A%‰Š„c¼J˜9×Rå ®éþÕ‹n^]v­F 휾ý¢®Î©ÿŒçÑ"'MLš€ŸžI3Ìadîëé!XCzêŸYB=üìYlwC%dö¤KÉü‰­ôø$™DKW©å"?n¯ç†'¬$ÛГE*g(-°qeãL[ÝÞ0§´,.PÊÊ=º¢\h¬ìå£GgOžÕc(Ÿ3gù’9sËÆ&Aʉesç,!ø8÷P˹¯pZþP2‘ÞµøkMØÓ-ÐŒO霆öÕÓŒ#ðÄ– %g‡@%\¯×±ƒZHƒ`&BĤ±`æ”CåÈBÀଆÐH5È`¡£yRD8E‹(1aÓNgq%]ÿÿñö&Uëøùfιp»pATE¸b" nˆ;n¸‚ ¸ç¾ae –)îk¦¤df¼ûLÉzФ>-ó‘¶XfîYZ¦eJffpÏðÿfæÞÃÍ÷êÿÿý•sÏœ33ß6sfùæ›oþE5^fº9ÚÜê‰ú ýbæL‰%Ú]»æT­‘æY<`wWÉ9N#•Ði&È!ÝmNÕ4“ŠørLš©2:5‡Gj.tr®’¦]*Ÿ89g—³ª9‡k&EqêШ–%h.õ™ÔôѽÎDr®B÷Úy5Õ.—„¨Ç]³”T‚SRš‰˜¹fêÝþÚh'é$ IýÒ± Èa‰Mý€MšÛ9 ®a¶SVD1UDHÕe†¬-Oóc‹ÆÐ͹s8^à{EYÉÉÕÄ©].æßiEQI­Ý›%TfR_ÆÏ¡+Ë5%hi–P§…}i1{LC¹6ØùØœwÐ.cÌtîôc¦²/¥Bži2Ï‹i@ÄÄhiRù`èley‡Cj™Ky{îþÊÂÏRʤ÷œ¬#Lº¢˜¢å¬¬†2UvÁFpòí_·‹»z—rõüyüuÕyHÍ'5š¡ä”Eè§ TÊ\ÚÎsçðW–gVï©$5Hž -g£60”RæÁtD™"æöó_ÙE+þG™âÒFž?Ïù[.5¥:ÖÈ(µWQŠ]Pj›E9R!ægLQˆ1aT…ÅDÙ?ÈÆ¥iF.\]e$—/ý]eÒ$¸_ý͹Y=Ùé$Ù{&‘¶øCÈO“&}ñEúÉÞIΞ4 Ãé E$Ç}WÅ_©«!88tækXñ1ô)@¨ye§|x<€?ÉQAÛÞr'MbUÙ$hð±¶» tß`XŽ0W¯/&á?…ÈïÆ`Qi¼ˆ8[.ÌœÝpr˜L@¸>ìâý6ÎyjåªÌ9þ'©m°èÂÃY¬¨Wmô’„5êÞÒ\̨‘¥&‘ÚéÍé“Ü”$ÕÇY'CÉ/é£xÄONŸ¨æ—"'Ér¶˜\Z¤æO¬¨=ä.Ü`+\ ™4©t€éb£ô#ÎAºA|º›*„çö”•NÖAòË䉈UŽ ÛDN@ÃÉAŒÄ˜$hŠŸS¸fÍà^Òkè†I“OO¢/꣌:³ÓI'hÒ·†8\¤)v¢­¢E»ìí´…RC4’HòËc3«ùˆmbu»ÕÓÎzŠ/,~‡TõriH¶:ÐÑH Š^,ÝCޤg§;åkhª\ù±¦áÅ ™ßµ¥!êåìÒ=ô¢¹£‘:Äñü²XÜ¿“ZòX1ñߘ\úGŠÿ¦˜ÆqïòÓR]ôåΤ¹3` ÖÏi´5?÷Ùˆ)Æ}þƒF¡–{€™ü¯b˜µ§ÌjuŸÔÍuŸÇ”Þ!ç8ÈDµŸ )D‰T– ÖçÏìI°ƒ¬žà7øŒ=/µjéý4¦‚ƒè ö€=X Õõ…à»Ëš(’‘íD“‡ DfÖ/Ò/ÜœóAŽHK+݃XÖ‘:ú }µÏQ¨›L¼•Z¾wEÐÞ!9~êŸCz:¶dLϲ ¨wa«ß/]SN?½]Z9¨«­¤ 5^b:Šž¿ˆ¤ßbæ’¯Ån 71twJ6ÊÎL#÷¹Hôª­äÍ7›cJ0­ý¸¹Â|͘´™5SB™û¼­âì­Œw•eÆ Î˜Ä!Z>¶9ŠM|7£ÏÕ^Tc50Ì7X»þIùJàT¶ÞÎÖÃT¹ˆ 8g§¼¹­~bguà†„¹­b~;‡d,J0þÕ«AnÊ`ªÙa™¡×ogóËW=\F¸`£-7.k»qfpìÆ-¼É‡&ìÞØð \?Âyír΃ îÃ*H€ ç¯A ÿfE)’xT†¯ŸJ v“ ÉY‘¶rùÙ7%óˆt*JˆwõÇQB± ‘Û‹Ìšl‹àèźÀµ,…û%Ùb8GØâ,Þ€až…FžÈPÊ÷­FIIBé~Ìqs.-)afÁBÖ…uQ(Ç#óÈösqd²åsæul*Q}õŽ•œ4`TuÇîG\&Ùòâzˆ +àÆwkC`Õ/åê'Î.%¯-E'ÿ!?8ðx¶2Jš* ¼„¶¶ÓÄ>¤©î.O¸SY|l„W^qx%â5¯yx-Ák^»ðÚ× ¼¾ÂK:•ÅÀox‘$¥PñqšÅúX ÅÞ¦h5jµÈÝ4üÀ/qȱ5px ¸Cå~õµ¯laWB'èZhóµ[^†ƒ ŒÒ:L{uÐ3ûX&{õÈèÀМ/º ~2!õÈh¯Ò[ô ~¯®_»uëÚXÚÍ1°~­þòi‚BÊJ°xÇhÇÄ)Díbk(¨¦RІ×F{óƒp[ÿݬÄú¦ÂT2ÒIY Í3™§) ‘ç-òAÙÍ’i7ýî7¤kÉõNéqüc*áPlrDvÃÇ!ÓkãõÃgÔè’_´c˜”½(Ξ(NnëìsÑË‚Óåe­ÉPsL¯Ë6ƒz£ÈöH=ÊÖ-ïë©þFf÷J_Þ¯l2„1EÛ¡oÿìô$~üç^ö½©§i¯ðïÔRQäôò«fÇŠæWX&g¹Xm´Ê%]?õü¦žÂv$þúÞü޶sLÚ¦1Íš½-èøÑq%nÈ¢zõr;ä¯Ï^8qEžÜÎÔ\VOÛ^…݉k­T']Ž{¥¯·Tƒ‚\pôë±./~]P¶ªß«”š(1~AÈSà§IÆùˆ©¤MÙyö4ÆPÈânNêW5Ù¶¸YW¡pëƒââ«úG½¸jm««*‰Unðà,|±¢bPŽsPcöš«Þƒ<Šè{õ(K±–V¡¡.±²Ü÷²Ü'ˆU/¬üj̱ùãŠ./p6kzÔL›IƒM=ÙeH>¨ß"P¬­!VNÇ48np–›XÙ÷(VpäÂu+øD²‡(V']´ƒ+‹ÏeêcÄ ¸£;ô™`k#GbphJÖ£@›°¯P¸qPÈÅêȦólqs®°8uƒ_î¢\P¬°¶õ@@Áòý‰^îô @ÇWJËV­åvZòoz͈xsØ|m&~f%0¶†‰œS¿RözÐ  MP<,Ü©º4ràFµVVÞ!ã•£ž-Ö½îjÇî7-ücqeô+ó9 özhÌ¥D´ÃÑÊêÜž›s—<(fóͽþXlZøp?‚¨ : ˆ*JR¬/N¸‰f&йª—¦RPL&ªxn,ÿÜ-xN£åS~`y«ØVh¢– ðšMgÐyê\m†öŒý¤Uá™A2è m†éYÏ9UV“Õø2ÓTÛkPùÞÞHõGGg’«÷¢wK¾&gõžÚ±cz˜®ÝHƒÈu^$¤¹v‰œCùyïWrˆ ŠŠd ¸Kº ‘†º…`é)Ú%M•gð•ŸÿÄõ:tOž~†4Ï#Íõ3zSÀb;,f 혵<ïY‘wh¡¢)&ÑjéÍ„w/ç½êiù¾ÚièŬšR]$®&¼˜Q&˜€âe>-Þ§[µv;ï³6WÑ=œIV~üšiPj‡Rÿ¸¬Œ/D}b ”Ãe]¶N6Ù,f¥#d+ €†DïÕT¡~²EjȬͿ˜mˆC %o³ÁC`w싟<|A²~_ÿ;ÜeÞxÃ?æ w_À»Az„¾Àg(‹ô/ðÍ0Níô¸!ѶT<µûœVÛ´?Mbkyyz˜MZ¢¥Š—'Uªx€Gìx ÁÛqoÿö­8› xö¨£ÏÖ÷taÇÖ†PL ÇüQHg7OHÑ; ³ºyžO#rÿ¢2–ïǹޛ_ãïQÊ—•ïÕKô{¥ŠðýÞ&’¸üÊh~Î%ãG]3 ëŒ6û„‘o¤f,K ‹ˆhG›öíѶ~ë®zÎyixrò Õ=#bš6nÌŸ{Çn˜“± ¦ƒ)¾ïøÝãÈ 1!ƒÓÃcÇMÞ›7k\`Pû°îø´¸˜•(”“¥}Šdc× {<<¡ØÜˆ¤Íåa’¾ÁIê5›“4x•$Éý9‚EH"#Û‘Ÿ6Î~>[!éꌧ€¾p –A @9….ÉøÆ(Å&¾í•Ò,;ýò”ú¼£ï{‡³"ž;uSÏ’ñôà=Ç©“'qðó›B°ÙàL€Óe¬øØÒù´äû´jÈ}ZâK*TTÅG1cÛÁ¿1±0Æùa\Mç›V­m¼…§5gÏ­IBƒPž‘5Õ1¡½:eŸ^‡1÷XŸ™“šÂ0ö‘ºBs4!Õë'Ô;¹|ÉìdH®“ÆúÀÙëßÌ?N#ü|çZ‘ÒŸËJ´ËÚçŠ?ÈÃù×[×ùõ"Õž‚jI›§ ²¦ª-CH6[‚›ŸVÓìg¹²›#[= —ÎÒŸ%dUízËëÕ†/Yvw“¹¼Èlu—Ô_ _!uÚªfh ÂРÀ>A!w¾8òÎ Ï×Öÿû‹_õi@n±Xñ³‹¶)ù¬I05U¨8Ú?òssMMÖç'S³^t+ÆTQ" “œSˆÖ‰8“ð8íìnÌÈß)‚ OgHøXôv~†€KØ¿rõ¾$‘žuääOÿÁñðö4…€i(4˜%â²äæB¬¤ÆümŠ¬Ê³Š/"öÃË×Ù~*®~YRçÇÛR «xyaØ„ïTÑ®JÊ-.Ê5¼ªàUCpPÅÉÙU3BÞ"dp%83sÎ$wHlîÈ„âoR_¸¡vw4IÓôª±ÓÓK 鹎WÈï§)Š¢žQwpy w#žø ÷'ÂÄØ×슺£4™îv ®˜„/<>ÀŸ†°Í%êÇ`º»4Ù-o ùì˧6Ç…b–ß~)þm÷†7vmØM\à =ŸDÞ?sæ¾þQÅüηà>Ä xìÞ°ë »+þå72$èñ|$RÏ—#?¡ÐéK÷›ª Er'@/|fÄm¦Õwß ÏÃpÝcÇËÏûnðnßãsyP½öæ‹ WÔÈÚ[’ï ”•q`Ú%SuK#O3oŠ=ÍĦ(ò½‡b¼åïe¼Ú‹×KƒˆæJ?^dûç¢ÅeëCeCé¤Ëê²>²º¬Z©ÕiŠ$êß«oHIjõÔ.Ó¦%ήQáe¤žÓ¦š›Ý±Y÷¶f‡kvlßÛ†Luëɹa_ÕêýܺœéC¬ìˆñZ×ê9}kÎìD«¾ÕÒ2~Âä!>¬-|åÝ¢×Ä ñ!Šê’€ÒW ŒPRá%Èú?ñôg¼a”Á&ur^™_x¾Æ@u–õX‰,ƒçk±¬#R ´ZM.  ‚¹‘ÿEJ!ìƒò‡Vµ]"[¿° CXPÈš:e¨h®¢LUf¢"çyå¥'HîoJòÉ’Œy þ†tÿ†¸Ÿ(µŠ¿–~ɽ´6O(†¿Q&O( ZH¼­è£¶0ÕP¤̥å¤ÏÝò—Ÿ 8@GŠV2XÔZyÔ%mèÝ¡»‡exÌüäø#‡âGõkÖÌ#7¾& cšÆ¶L?5qª·|§>@^Þ©37'0)þðáø¤ÀÙ}ê8&¼ñÒ3«‘nºÔÒ"qÖÖ©‰Þ0š¿’͈Z+‘‡â-œ©»ŠÒÖ6”âF¦ÐH«QShŒ”í\åoL4ƒp@Š27×ì8Àó¥ø§9‰Õ‡÷™ÿfh^¼Û7KÂ]%'ß@/!½ ãCn¼ÒÅ7κ4ž$ñr©Ùwús˜»kÌ,Ns…&¢w.5ب/ØðQ:¸±-@ða•|ð#‡Í¡¾ÿSƒ#šÕì8É<(~Ÿà#u¹ÏÎøÞïÿ/\@D—V>ÈGVìàxrWÔ¯Wc0ósGÿœ ló(Ÿ¨ÛÔ³ØÚ¿V¶½Èï¤ïfÏ× TLo\)`5¦À"ÅkÊ'0GýJÀ8kDÌ öb…¼2O¨Ì㎱bJ9;T'‹_±˜<îˆ>炲]»¬N6\äðó#ñâk§šÇ™3pæLéi:Ì‘¯2zŠm´#¨ó2‹¥!Pêl¨¸3áwµ9>›ñÞB0=¢¨y֒鉕ÇKBøÄÎh—há}J ÏÇ´Æyl†CÉíÒ¤wÆN[ç…^·&ÛŸÖWÑøSâ?h›wúuï RbêØg0jƒ½+·;@ŒS©]æ³B/LøGbòÖ8][?mì;“RvãOf¯uü‰¶Ò»£‰­C÷~ï îƒAªbáï€Ö&ú¥Øê"O#³ðë'¿xé½£"¹Ãa_Ñ8ШÛ㦑‰ý`RΨ´UÓÒßï›_ñÍqÒbΜ§ã;ßÚK|Ÿ£¹Òý‘ó¦ EÙ¾%Æ=8Ô¡$´! Æñ“ÕÇ¿U»ôlé´ñãç ³»䜞 _Áe`Á$“5a!WÌiE!­4ÂEÌ —2âƒÂœÞe¥%1iõn¿nQªJLÑ}ó ‰`°×ÄwÇ`¹x˜ªçðr™¼/eÚºªªÉ²%9Ñ.ˆ|‰|Z¨Žb•J,ã¸1Ô’©Ýd©&-ؽ5o\÷ÄLÀ‡1 þ¹• j µ)<ÇV4è=ÃÍØ‹q\,Ý87iàª?-á±Õx–UÈûq5¨tœÐLç•Ç\©ÁÒº®VOn^­¿ñð˜8/Í,âÈôÇ6ÌpÙ–¯%»*¡ÎöZѰB”)m¥ü ñ—‡[òíân4_ÿüUïöM%I±}z­!ƒ‹ZWî\Ö¾©¤.¶oü:ù]¼Ÿë³r'[­÷$Ç÷½v¢iéàÉ6ÇBr5A/¾”ša|¬ñ|„õ5\iÿ¤ký› 'wæêJ'ÂŒ–£Ï£U¾xLµ¾cÔ.@j?Ô&<‘Ú¿ódþspîÉl=éÝ#Í–ìècˆ{/_D'¨Mç—ììkÖpïê½EO_Þh éc6a'¿oïïͺ÷gEv;[.áç•sˆ±©Ó¢¦-\ÕØüðGó ±©Zxɾe90êB#Îv°oÀ#Øëìkö {&’3»ömôÞµïåwðªAvî{Y´†(UõS¥.¯[6¹˜äZ¬kèZnߤó0||t3¬Á¤âû¥#»6o ÂVö zÝÈÄáŸX•=€5 °]‹®][ðh–^U#†btbdÝêàÅÆñèæÔÀf]»Ì‹n5²MXX›‘aíóFuëÖ4öS›vë6*¯}˜Œh7²{×f²s””‹ŽøIÄÀ'R wEÐôOðé×ùHT¢,cJ?8h1ñ~™¿WˆñÞªZñ÷ÎçA¯i_Ëø2†ùŒxµ•~ØÅ¯æñDÆ·ù?ƒsú™ˆ¿ÏîÈ»²Ì‚KJ ½¢}‚)ÂèÇ<ç}…Çk/*KÞÙc$±C: ÞèÃø,Û_lû‡”†‹J_r£mÛ{amWÂüÏ×`™aËê)iÕ³G«V=z´)¶ÛcÚuê—Wgt;E•M~b@ïíò ÌÿË3gÛDà˜®s3¸t7uÁUáÉ‹ñiikÓÆÅw˜Ú¤IÞ3Ïî|“îÈ+¶«ÝR⺳ £®€ ´R"•J7H~Ø ×ã35y."?̆ú=zv¯Y*PCÃs~Åk“¬ RÔ1!S§v‰‰éö3û¬_—FñÁ!½BÇÌœ2rÀè®Ñ­#ãOtéâÛ¬ç¨Q=›Å~=©cÇI’æC‘!!a,Æ„ÕíÖ‘Ùt‹·útªa™1&uΤ]bÚOº¨Ÿî<çÈÜà ë Z?È·tËÚÄ/y‚*2%év„ù‰ÈrémJŠ2jDdûî #"£º%Œ˜Õa$«cǨôéì_ß¿oÒvâú$ã™õÄ6¤M«ŽF!]tNz›¹‘€B FéÅ€ái5Üu°+÷×Î7Í–Ñvܸ”Ñu›7¯[§KDèˆV-Ç´5=¤y3_•RYÏ!£¨_›ÎM›´¬Õ<28(¹nݱýuoÚÔ? šW`1ØÁQ¬zE±âÙ= q·8%Ÿ©JŽ©êgÕ½r|L_Ÿêè¡Bån¦´¸JpÇwrá0úã6ŽèÛ}›îp‰Œ‘1È9BЪ‚²p´dñg%Œ-ÜÃÎÂþô—èè0þ,’Èþ3þŒ½ØèAû8…[]5)Äà 7°U©Š˜£ñc¾Ìÿ!ÂòÓ‹ ‘8טI}»~Ô.ÆÒŸbÑV´rïÆT†_t£ÞÖÊ|e4d‹fÁ/ú¿¦‘ƒŽ[‹^È®À¢éÌBúÐö›ƒ7ØOáÿoÂ_Eu®E{çæ?ëˉ®ZÝÃËRÝËD —œb±\öÆ?Izeâa@^tgÂÑUo5O1áG=8¶É¡*…T~# ™4,4­‹¦P4ú¹éMsŽ'x%ĬW±`9µiÃÍnß¿k½¸yÒ¬þl¶x8/õçá =‰„{Ê¡#–u£ä!û‰ÝÙ3ì0ÝA*“ÛÒ”£˜-‰Qr@là4]L%Ц~H‡ÉLBÛ¾svkü}cŽWNçÿ‚1< »,¹¨ˆÃàQlPZ˜?V¯Ê=’fÍS5ì8A£JOªæ ©Þl›õ!ñØN¼>-V†#K¨¼mP#JÑ7};yS½?Õl×~«`Dùì5™|p%7TŒå䪿ãxNêVùÝ*Oé •‡M’gŸ Þ¶W¯¶ÃƒŸbGËù5½;wö®)~M>=â^ßvíê7×·íèÿvl»þÍÕkÛ^ëQÒ¦C‡kðé·¢£;|ËÚ\렱렦ñ¦#™ÃCb ‡Ù6«¿;:µ¨8ÔÖh(úîGàyu{ÀÞƒ} ßðÒ"Vú]X †õ+ÃôTÒ¾éöWyß^/Hxÿ©1ûfÚþUmó¾½UØö ‘³N- [±׆SL[8 .\»6Wú*é¤?óÊKë™7 xkéT¨Ïž)bq WOíÿ‘Ž…ˆwwA˜{câþå=^Áa–劙¬„i ×På¥ù ÔÂm‘Íúëï“%î:¾G˜ì[XR…ì8µ!LèËN¾»‹}ÃÖV€ÙéLTˆØ„Šta5DBm’PÊ Õ‘®cËЙ>[ ‡ÓõS”ÎÛÍÙ²YpK¿4ž,×Ãûì¤ýŸ¢Ï²C×ö+Ûñ?‰ùO³ÏWìÛÆ~¶ÞñT¶P@T‚r'šÕZðüŠÕc¡ÇP–7Gê‡IÆ ;3à¢>u*UÇúí$/÷éä¸j¿=ßòßÞÌ?ã:²¼¥öÃì¼ Ó,è<¥wrl{²–B~Úe¡eô2q-#Î$¹r>¡21o‚ë{.~¬¶’&Wü²Ëòò€r}Wh pÇ5„W@]ÕsU¸t¼³±¬¾¯_\´ªš:õl„h'ãc,?…íI‚Òšë9lnÈ*¦Áx¤!––€­M5 S«Iü¡M§è'¨ÇÜ7ºƒ­ž ßéßN1»„1ÑTÄ7`{Z7§Bf¨÷ºýkhßù½Áù“q“̾vH%ºWí|4½=m¨¬ Hp¢$°ðÍ5¥ÿÇêm2óá?wDQ^]l¡ªœÑç&·Šx”´&îLl‘˜2qDßAÑl,ÜJÖ’Å™;où”þ{‰ý½ß«ÕT 1ŠÁz5Ø/¯zÖj„o‚Õ×°nÝÏË/€ñõÌ^âÓeýâ…îí 'ŸèìOÚ¹ê 5І¾ç^9*Â]›«|áÊ]ê!á‘Ðò¬ð ¼°©ršMÕ.›Æ ¼@¾o½9¡¢©³ÍÞC­¾ã²Âº/ôe—Çè…äý»ºÁ ¬g¸§ß $±z¾`s  –yúSÙnhòÎèÁ0ôMûIXÚˆ}9=!ûnÕ¾-‹Þ°±ù ù¬}ÏC­µù[áë”!(Ñ8%`ö‡È–`|Т±s5~6þ±“£{6C¯]ëvêóÈÛcw¥ÁývCÒLÿG3²XO³+…õ!uê“ÑЭM3òzË&-åyXÿgí›7lÏëý‚}ÇÙÈF°g¡}ˆûÞÃÂ}ز$`š ~Š-<´Kl–þ+ñ„ulºc5 «Õ±lÑØdöü]¢<·ë9¢TÌj W4?Â[ÈÀôŽÕl:¬#žú¯êØ»Œç`Ê]ÈJ+?7Ì6TѸ`ǧVÄ<Áñ2M„Ýl ËÁ9ÒÅqެÝó"¹Ê¾OJbߟƒôv=™•†)6$ÄÛžÝ4Ññ2›t±c!ßá—òk/îY˺hCϱÌçv½À–žƒ€¤$m €VOiü_Àr²Ÿ¬“­ÌÊÛÕê!YûkÄ@9ËO$¦t\EAü-Êe1ÎÔê)~¢ j„LÛÀŸFðó©êïG°¢Šá ¹SÌ.ÁÐÜ6§ð«åòûu–õSLyùµ…s!ÈS»bÞ ãz³9s7Þ}g.û2ãç»ÐpóìM½Úú‡rdì{ä;ɬ&2¦A¨U´dV<Ç¿àÃz¢l¬#{]s"c—¿Ïÿ”Å6rdt @¦oí +2h†ÈØ×»à÷ÞŽ(+¯k7ÕÑŠ•[6TøˆJ˜ÌËcþ‚´›Ž÷O²ÛÙÙE4Öqté‘ä1KiìëP³èÄÊ囎.ÍNSFdx0‡”â|¸¦Lc—ŽI>²Ôq”ÆáôöÉ×7IÎ^ztÓò•'ŠßLüqYí¦ö™8¢Y`‰½?–º,ùƒ­NêèŽAG«‡³ì³`K;‰úЧgØg (NçG‚,Úf°5›ô›Øf˜( ˆÂÑ$"±*Ž­ÁV—m{m£[Oêㆈ­ƒœ“ú²#‚šˆÚ DŽ‹l ÌØDÖlÜôÇ~t":"&GÜ+~-EÛ,¹›ÁÊâ½§Þ*õWo9Ž}”½|¹”ñ²#£¹Œ7m‚fË—»‰Yη¸ƒô~DØHõ’K|ô‘eRâù#ÎMèˉeV¬gçO1ºvÒÔTœ7r-çôsvRPšCêÝ;’‡ŽÆ.éÃnÁàN³80;uIhÅÖ©á›6mú9¼`Ó&Å‚›»‘›8/ˆûÛeBÛRNÎ]ãü¦Pizžü²5ºKâ+º' O]2xÆ0ÎÛï1äÇõ¤â4LíĆÀ-xŸ³:¦«¯ç¦»‘§ ;éØ$8ŸŒ…sK©Â“OÇ)1=¶ÈqÖ‚t™™Þ~Òߘ™N»=¶¸FDã“ÜË쉘Wv•0±ÐGÊÑÀS©0Ý,g{.û›Ý­ïž©ìÕ9ѳG<ÙüÃè¹CæD—•¹ £í„Ij¨áea«†ïßD¬õ‡ü¸k…àp®tÇKjÙíãìv2ôQ¤ðí?Óþ‰ 8õ ˹ö1Vš=,¨3Aý >Q Ÿ¾4T(RuRš§+aêr»ã#ûŸä¾|™ç7kd~ãä íf† –—εÓH»¨¤‰øT‘&).<~î¸4‚WŽ/ó‘ .ÀàW\¦—J~³»¨§Ì#}⥳;Šìq婘*̹ä‚¶³»Q¨òº üC ÁÉr7ÛT¤o½Jn×xÿÒÀp-Š ¸¹èÔÊ7ƒpRüå—gÿÑYþW V ‚=±Ko£ôQ”?[ëƒ:"aÚ´ÜiÓûÇÎkÚ¢ÅÁ%/½[PüÙê<=¡ÿÔ©Ûf,jÝ¢ù¬./x÷¥%…ÿý‚ñVšŠó§MOÍAnƒ"ƒ=µ˜†®mÙçãÙçm¡+;ÜZއ–¿ÿE[ïÍv€½ƒ Ëß;ü boõï—%_¦¢¼¯ø›©C2õL~Á?ÿ"?“—]Jîÿ¿kÂ"ÿ^ÆûÈ¿ÚtµãLþõ&Ë߮Р"·Š×ÛÆÁ¸uc0>¦u*yŸÜ/ï –•«–­x(âð?éydÎÙq”½I‚ »‰QN nùêâIÌžÂå½'ØœùµŽl#LYÀ6±M ` Û¸&ÃdÇJ£B^cs¦©W¦¨›AÖdåÆ>3‘œ"7õÔggê³µ-%ßk…9wxî%+3ÉÊ - ä{g úœLQN é§ï3ˆ1’TÄþÄ E†H(³Œï\{=6±d/WbâÆ&OîDϳŸ zÐу½Š¹$˜Š驱o5 Ür ŠzdDÝ.s–Œ7r»a£FÞJBj†@Ë/™Ùàªl/fý 3J)?ð<¡ ¤ ƒf³Éd}³Ï†ŽÌ—c%—ÃõázýòO”"1@X…'j,ÔZ' PœWªy³$µõ·&“úc³o±‰ÈRú“õ´ŸÂ™R£Ÿ‰€ßY§½úÝwåeý/§LBå‚È x¤Ò>žCdd¬ 7!Rš#AáŸ#J ¦ˆ¨š8&>ÒAH#ÉH УÖÖ#zcǽAm­°¥ÿì„þsúc ìÖrø›÷v ë²*aÀÌþýUY€~~;hi†Å‚Ø'ª 5›w˜«ZLyyИTÓÍÌËcçQ^MKû>›TËäóÖ;ŠB£µ­ÎÞ!K&˜ ~þ&éwû‡ ç¡äVí¶XNWÂ0Þ¹ó²Hí]ú›±ùÒn×nËí—•³ðÁ„UîêäYþ[¾³S»m/9blï|L>‰Jæ3pa¶\.À<¯¨Ï+U ÇzòL">|h$VÞĦê‰o܉³ù€¯£é^# ã§«Ñë!ûÉÇwggâÒM?Ô9fõ§Ü@.OÒÂQ¬ê]ÇWxÞy3ú%îÏv|eÇßÒ,ÊJØ|Ó&í˜øxäáøô¹zJ_¨/~ ƒÃo±Ï´z×¥íÕo  ÷ÒÉŽ”6PÿSòý£¼5êóXZ›®’P/¤"hûëÊëú´þOÎÿOiv„û….ê7¥í>Q²”,5SÍT¨¡fª³bF€…’ÅŠ¡:XØ=` Ÿ‘¢K$öÐ8Ìás€,VT¨Í=NÜvzœÀÒ¥3ghÔ@kÕS­Í=R•ÛÔýáfJGtw+:êlâ~ÄÔ–£.o¼¨ðxçéìåøP\}€ÿ²>ð.[ž'ád:[Žá>07áÆÿt°þ¦~ô€EÛÅ m)1N´sï›Cµ.Цp·ŒŠ?wy¡Ào(®d:.le´\ë¤eêù$¡$“ô› Éz7Hf;È …D~›Ÿ'ÏëX—͘Ù!€èsØI¸ã>V’`®°ºHåìÖp‘=‚$z¬€¸¦¦|Ã[ˆ?AÏ¿ß!öC ÙÍH»se3èsLý:}ßµ+¥IWHBŤšHé ’DS?}YÑIV_¿ÂYýÊ{]žlNýdSjz7Ny>jF$¥") Ô¢ñS¬>¦Ð`’zæÆô„)çX»ñ…ãÕl ³ÁE˜‰ëXWhpÅl˜+Ì‚J Š09c{Æ`%È ÁŽ+lÌ„‹ÌÆÖp vj^ŠUlÕ ®$¤Ÿ"ÙÉã8Ϫ÷¶DF “ -—b.éS¢7Nˆ"õÉ™#†ï[D6éSØ/‡.„äe}²T“´jã§ÚG%ØG ËÊ|×Ñíð¨Uÿ}Ønhþ/ $â8 9ΚhéLh§e_¢c0o‹ÙtlZ´oøˆL}2Ù•иq”>…l”ªp|õ2Ô(<|øû…Vµ¿›™5l„=!ªýSÑþ_êÄí‡ÿÍî³_F¾@Ñ]Óú)ÞÜÐ"í+v–‘bÌ…‹‚HyË„$40c$ý.aŽÁ@½ÏɶNµ–2ÕžY’ÍÈïòg‰@µKîÙå’[À!H¢ݬS¥å‡aXÁoÝìÛ Kâ©]¥å]rú´­“Æu—¦l£Ÿyz+Mr5,Þõ…‘]“_ãn5ötM‹ãÔOÊ[—¿‚qþ¼l¤l{µBÄv‰O8€WoíIHË Ü·fOEÄÆÖlÎpe;£νڜ¹Sû—8mÃ#VFß²Úõÿ‰Žr#rçOé`#+Z…9©¸÷£“Šâ8msEû"Þ¾?À_UÌÅ}„f.Ò[¿áHÜŒMýaÛâ úÁKÛ.ë×h¨ºüô¶µÌËT5–^Ú®X¿Kj³ F9î’ãí™ c?} ÜP«Ù[šØ!ÜH Øà„Kz \èx—ÝçppÇBÌpµ¨˜ÔŠÕøfÛI½õèö€ýÂv w!Â}CþÂH iaÿ VIºü\Áªi­W}¦kð\®þíylÛ¢KúɧŽÛó¶‚Ð5¬l#Ç£˜¦Ï´Ã’«»`$œ‹eŽ(m÷ {ÏήÆ~KNÃèOÏYÄx1Z%ÆHol€Ä$vBƒÍÛÔv–¼qkÛ-ø·Þ~1í嘈 AI3‹ ô˜ 3sÔ"v‹´‹Õÿsp»ÄŽ <öïÈ¢éÑ3 ™96—•ÑMe]ÔIZØ»÷ž£DÞÉ,‹™ßËöãÞºÛÀÌVâ@›ˆ×¼÷‹x|Ïãµq2¾¬‹Ù*!(ÞÆT„ SÈ™Bñ.O¡€2¬ì¶9OÛ#L$å&Æ >SƒÜª¢{S_Ö kôM†´£¤@ï±>;»wgõ~iusl¯åÙëÉA=þ(ËIŽ'³s7¬Î7<}º€æ¿èæW”·¼5}T ›X|$^•ÈÓw½a¨yÔ\_ÙYëzöÓrOuHn.zkeúá¬æg­9Œ%E‹›ßÎÚª%DNt%±Gì±QoLéWÜ\µJ¯åò‰ ߊ. Nn¥*/WðgäæìGÞ†C,«<Ë9ªÇ“ƒ²Ê—VUïÉ*¯÷ G!-9ž†þoU¾ ‘°},Dâ ñ‰”«èIwûq}eGºëÁo¹g)úªCOº@y•g¹l…p¤›$‰“£¬¸b•‡ªE}  gIè”8[Á¶JÞjy•×k­ZEnò!üÕ²Xç£^Ü 7°Î'ÀªÊ•û‰¾€ÃÿͰ6’ׇ?õ L«T®Þæx׆Ȟä|—~Ï+ø£.x³WHs'S}í}¹[¾AÎT¿4MÍ))Ôâød±Æví};ã[8{•]V—©u+c0æ’ý7šŸŠ¾;+Š&—Nêƒs‡L,\=7uÞ4vüJÁÊy©s§C4MËÜq«Gjêó¯ßê®úÏœ—:oeáv|º\…è³ñ·ÞÈLMéqëõ̤sôQÒÒx Â5oð#;ôÓOZ‘gØÐëýhÁö¿2[§(j–z\Q¹ú1ŒkölAø»ÂCÇ~²“ía{H. Ñ/ëcìp°—ø‰Jô"?©I*”ÅQa@›³;éÍgbÅê¬Xžž×ÁN„ðä¶0GŠñS,Îs\ïaHœuVV&“ÒK#ËiÑV^UG#×{5ÀÊß+—Eü9W<¢èPö!yËÔOÔc*J|Íò74um#Å/x¦OŸçžã—+`ê7¯ÿŒ„E%LOXôÇ"×ÿU€ïô"ÃL¾Š&Fã¨Äȇ¼â“ïê?¦bì0vºŒ±Ù˜L/(6ù>üÚ´~u¥Ì\åá—_PÌî™þ1uu¥Ì¼ #ÓÙ°âvoµi½BÈý²“•d•»ËäGÃ’ûkÖ‘¡P¨ç‘5k¸Ò.­,…¾`$ã&}˜”’ñ$ “ëyÜ3Y5g2„dï›"Cõ™kÖÀ=u‡÷è 2™„Æ“qh´„Ö“q—QõQù™¬h®åu<t.{ÙÙƒ`L@T?cæK8ÖXU›øPÕF¬!+`,Z¶Œe°Œeì—‚b&úCÈ^¶ 2! -±ŸÇ§û¤îÛo³ªú÷o+&åwÌž&+ÒÕZé Ä)ý•áò ôÆÐ\;Ù¼åÆîèl.ïrSrSð–ŒìÎ=˨մŠON|p€?RÑ',œxú4_]8Èv{FA¨¡ñÁ¬ÅdÚí™,Î:Ⱥ¥tï–’Ò}×®î)ÔÓ){™BKÌO‹OU†uÀ%jl鹜¿VÞ°—V=r2!-6²iÝZ5Lž–‰ÞLÑ4…Hè ýa$L€ÙËáexöÂ{ð8ßA18ˆ¯Çmü¼}ƒƒ†{·õ÷ö jƒ|D›¯¦Ð°#´í€½¨[ØæÖÜÓ<áý_ k‚Ëÿÿ ê6ÿ„Cÿ/é‚Ü̼HåS0ÿ ³þY&~Ç"¯¾Á0ÄãÚ•+¿^YßËãZÏž¿öjëeÿ«¶Þ!!½mÐK>²8y¿"opÐùø„¬²•Úðïa×ÇÅ^þïBžwùÿ Ÿ‹î@Ûrþá¿MBl¶ òÇ6›ªÚlÚÙ+¥ß_¹¢\Á{¯^j@/GÝž6[ÏF2õüýëþÓ¿‹¿÷ï’}m³ml`ëiÓãË߻ģh (Š×»Û_Ñšƒ¹íÁ!/å!ðz÷áPÇêeþdž•¼)\Y²„Tã­£þëéUÎ&˜zQ¯ ²9ä(¥¾<ÃKõdâÚTg¯90„4j“Äú9³·?ïnÑÂ"¨Yä&¹©×jÔ¾nßž5Ä¿Y³ œ›[’óÜ0ÓqƒŽ‚·Ø ÇN6HëœÊn§²Qü—”èçQŰKQÈ4uŒÅDûK…,QÝ©>‹ÏƒÅ3že ñ£Äó̲ÔïÔøÜMÙV‡µÐ~SN3±ã'é$^ýÍayÛØyxhgøãì?ÔJg–Ç|‰©_®¹§ŒÃ½VbGRÈ5rHÝbœè…]#þSŸ™.h§…´[èžð—ø ãO³ zc¶š²ð {Ø YN‰Õ’ÓkñQŒî  æu‚úàp‹Õ×Ë)ñ†ÙäîÊeŽºãGú2\#­€ê¿²%úÇÏ2ÌÆ±å+ì8¸ÿàó²³yóE€Ë@?/˜ ¼p˜±þTîé5¹æe?:Þ`C™/ÝáiDÛüX‰Žp %æcõñöC“ÒÌ‘»«0—“a%>°€Tgº~š:&a>²óqtªœH´¢ ‚Ï••H óC 4˜%+?È]s:÷Ôú NË'ú%æÃÞƒ­pþAG¤LU4.>MEa\¬;DO&;P›Q?¸A»©TK\—ø /&ç /…ÿyßÅ‘ìÝÝ3³ ŽZ´BD[YÈDe‹ a9Êá$#ÃÉøÎ"œuOÎ ál2NÿL0ÂA>pÀ`ƒÏøçœqºûôt/ø¸çØÝi}ÿîžfgAÒûÒK»üg{¦ª«ª««kzZÌ É$ƒe9eB²Lä›Á–”ç5ÀÕÀBàv`%°Ø¼| ¨7¢ðÀÄÎ×°3ôG!(ª€Z`P´k€ÍÀ.àp8Q¼/1ä¼£)##ýdù8”³H¶,g%í$ƒÄûš’Hz ‰%i¬~ZÎËÊÌ0q†@ÿf‹Å¡ôÀr^Á_áûxÅÛô¦Ý{éMoE® ÷ÒAøÞ»ƒïܺ•ïÜA¢ô±ÏìUìk¾—ŸA_¦¯Ñº×^ã÷½Î;øô>:h=+'»ìNÖ4tõÈØ?Þãsöé´"ÞÙ…ð¨(þ‡u6lƒ/¿ ŒaUPQÈÊ€* ˜Ô-À`3° 8$9ø<&r  ×EG&§Èò`”GA‰(Jê¦CÅó¹J²J‡›’zÒŬš>gO¹„ÒËGN0æÖyÓï÷øþQå¡¶tù=‹/ú.£âò!rFoÉ:uUãè9çôÙè €³èöø¨$Ų\ù6„”ò€2  ¨æõ@ °Ø ìÑéƒð[‰NG÷;¥d˜ )ˆJ: y@PÔs€z Xlv€ƒ€ÒS¿xžä¨°‚†1Ý”dÈrÊätY®@¹€Œ”唡\1]‡¥ïÉÌ-–ÏÓGð ¥Å(ø£°'!hì:q᧯Ž_2irãY æ]3ó.<%}Êä¢úk¦.÷ØÞÙÝÆáJcñoî~(uà¦Qƒª/‚mÈïÈGƒð›ón]œA2e9ÃI`™øÍíæ¤-ÞRÏ(H/îQÒx™ÿ@Oì÷öÝwïàgvÛ?Õüì<{çÝ|à?7”Ñ âûâT§/NE9‡ÊrÎØ~yž ws#€’‡BPTµÀ hÖ›]Àà ÐåIÞ=øÎ™ð‚š¹tžH ˆ?SøÇ­®=š ¬fãø FũϼóÑîñ·Mšú»³f/\8»`Vm·@ûß³…÷ÌŠÕ­á~­#N¿¼fz8Û70sÉxz¿?ƸÁPìLrŠÿð&ç%À9@ p5°¸X lv¯Ê~”-hjP¶ ”U@-0¨Z€5Àf`p8 0eâ®ÏÎ år2L–ËQ.pÆ`A7!eé¿i!ˆ’å} 'Âð"PîQ6yl”)€/›>òwÍVïEÖ=ÿʺ]{¾~½xn·ÁõèŒÉWΘqåäôá‡2û.Ÿxü¸g—ß¿Íþ§Ïèñɾt“AþŠ‘2© _ ,ßãÈ÷üý߉&H†Ôðÿ·RLš{†OïÉ2]¥†îÓͤ=»¸†máË÷Ò‹y~ú±ÇÄò í r;Ñ?¸Ç¹ƒ;¾åüÿƒu#€²…<  ¨j9@=Ь6»€ÀA@„D6~óåd2{äÿ­„1ýqª”9ï­,:¢Á\úŽâwü]¦“Ñn:Õ¢<å3tr&ÊE(Ÿ ±=O,Ù#¨û×õ ç1ù¥šY§G9æé˦N½ ௙sÿÇÃï˜cž¼p÷Ö­»^qñE\rÉ]ÜmtÿãĪª‰ŸW5±Š.¼tþ’%ó/]H[¦,]µjé”[öŽ«¬W^Yé;­%žÖÊSy¹7ÚQéäã‘ÿá"&ç%À9@ p5°¸X lv¯Ê~âk0t—;3%44(«PÈÊ€* ˜Ô-À`3° 8ºœ)¥‘w:?Ì™A C¹ØYv(î.5ª™Qi‰ˆ4{ ådoÎ%Îû.Ÿ>§³ãU?*Ng/ÿËëÅל0y²8Õuq}çCY)˪OûôòûŸ³ÿù3þs2}nÆäÉ3| 6M.§{Më42V–OC¹„Œ’å’ÿÐWß02()(äe@P Ìê` °ØJ š”ò€2  ¨æõ@ °Ø ì.m8D‰òpïœ\ßÊuÖ·z¶&—\¨Ö\±&G=™/׳*Û“p{ë‘Ö<ÊË süÙg¯8眊?,Yò‡[–,é6Ôx9+Ïm9Í.YľZõøúU«Ö?¾êï;Þ{·£ãÝ÷:í<Äæš1ú ÄÀÛÂæa6·µU \+ Xv„0Ä7ÊúqÂvëå{Öo–ïÿÜL(}£—CB-ŠG384ƒŒ…ªP¥ðuéŒ:šiW*@¿üÈaÔ¸V×€D÷ë­¡ Ó  ¼&»õ`¤áÆŠæÚ+SŽl‰㯃ñÉ^u.DCö8H¬kú'þ¯Và =ûâÍâû’nhú(jÁDïW×r›è‚˜hæõhf][7ÔßX|¬ézþÛ_¹Øš­bk?)¶ÄD£¯G£]!¾fû›K– Lîßû`{Ëþ€«okt¥Ëp­Ãò0\Ë7E‰ò‰€zc0ÏÔ"Àà¡´D®OWD±P‚?ßD½¹­‘¯5ƒW„Ë/G–+£—WÔfƒ¼"Àà¡´4 +¸Xq¢)è 6œÍµR£+Z%=G)zغ.úWô.ê¢#è—Z´mƒbÐ"Ôà–Õ’ƒZÂlØ: yMä_ÔwÉ!—Œ˜)‘eª:BíŽ$@¿ ããê»ôK×twƒ!¨}à´=ޤ€V G¹Bñ\õFØzë§H*qE›«©‘>^Œ#'’&BªŠ%ó?kP»YûŒÚÈ£jÅk‡×ý¦zÔ‡âÔ_k¦øy]ý!Ÿþ¿z£Ú哽)Â=¢Kcý\¡x.Ù1šMö\oG3%£èÀ‰`U}Œ–'òʉp‡P‡Î<2*¼6ªq!„zŒôI %jw"íNdö«¼ðæ‘aë÷¥Òïo¾H2®)*˜‡Uµ¢Ë]®k]Í)š½)‚SI¬9o±j»ò<œ’Z£{Rñz­ôh‡LÍç·RµÆ<Ì3»³Re#CÝ[©<$rhwVz´{¬tÒTÐÉ1°@¦‘áL†;—P}h„Ã?ª|Ƨ’;ÎB%ÂËtdŠÏ‘¶Dë„d’¢_Û<Áä²ñÌ®ì“ßb‡5ö](¾c¼…Îø?ÞI,ŽÙÎuåÇèJSëãWãÁKåPê*uùkh¯újhËußûtGÕqŒ*Ž«UÕhWKféЕJxfwÒÝ JÅ1kœ(k¸]«“ª3Ñ㯴ÅW-S ïµ.傚D¯y´‰i¿§I•…Úq5G³ÊPË“¹&ñDÍ1Ý­¿~w+MÝôªÇáÝWIty·Ñ&¾¾*Ý…[bŸe,n¾MÄý,…ÀXúm=ýVͰ óή=©ö úmt#¸Ü5a˜‚àvÒÀ^Íé™=×—ǶD/3.‹A$LÚcôÒŒþr´;Ô\v‹Ñý.‚N×p'ÉÊ-|ž>5® ·†ûOvãÖÓ’£|„"ââÆ“н“XgŠj(w!à˜ЭᎠ>_zÍSª·†ðÛÉÚs†ª©]-iö_äkL¨®ª±à*”þÀVÏÎì"ó°´ ²0€=s4ÕQ)nŒƒqžêW5Öb¸ åCQÃ{êC­Ø2a r"48*ÊÀp{MÕVuUV‘õDÀÑIhüï™ÿ(~·Ÿá 'úá?þ†+ßkŸ®¥µ85]=¢6tyÒx²·ƒ½±èv´ŽI·Ãu`z…x v« ×Þu­w½lº"D„HªÊk:¡xÛÃv ?Ëä»´d"½-ýnõõ6P/o¨ð(êã¼@#$^õ îe­ŒöÜø¥­M ±ûtÄk~ç5}ÿäZ¾„Þ¼©Uð75 —€]_­ë¾rÜÈ Ó›k'¯kÝÄæB®qSìÞMܳ¼ d§cQ½˜"Ýü ¿cSëºÉµ´žÍå›{Cdoäaë´àur5ÒZó½¶èHî]ûóÆx*ͤ…Öi ¶ ª‡EòŽÎãÁck×ÆæØ«ÚØÉ,©Í^ÍÃ&‰³ÕÜŠ-‰^ê´i¶gy‘5ˆ!!íßã[Cd{Ì½Ñ fY’¤¨ƒýF]I8”í‰[x4÷DÇKEÉPä,WZ!('xꬤìñ/Jz”¹Ëœ‚&”©™^Ì„:O=>U/¨²¾ºL¹OxƒÉk¨Ïb¨÷¼ŽE1á”ê9cE ’n~AêjéÁ#©³’öÆ%ÛJp\``Lg…»†¨FèTð½šDæÆ‘ÄR¦&Ñq$cEìM‚G¼$íA2Å‘¬C‘°&Y©q¤`ê¯ÿ¢IÊ8R¯Ê_vkí%‰L­HÖM’u›—dnô̼$ãKÉø.Ž”ì%åzI¬ÊCb¿ñ’h³‡D—yId»&ahPŽ¥hü_ ïÛ·o¿üDgìßO—j¨}AÕø·~Mÿê|¾UŸoð§£>ß:¿ôOÀWêÇ€ÓQrœê;_Šÿ;ŽœAˆUXœ™ è_]¦!l !4(pÜð]@÷Ʀ²·Ü'|Ñ–ÈÂ\?d ëž©kSèR®¸ÒÈåÿó™gømmü«#¿Rê|ëôÈëñ Å÷<ª!§Aôÿp_@Nx.lk£Yx fF[[líSµ9PxÜc[ÝOÔ \"ë_<ÅG´ñaÏðam|DÜ.ÛÙ}UøW2¸zy|¯*“~d|'»‰ÅH¡…4…– £è µXv{;½5j/6Ò£Ñsw}´r½9(ö§õ¬™7¬o—=F/kgÍ1Þˆã±?Qkls´’6›/E;bFš½8FoioïÎ $AK(MUÈăR,­íöâ(ìèˆé‘õÒsP4ö'Öå íÊŽ:˜zKÌ^LôX´ƒš»›£g>£þüŒm|¬ÝF&é úTù¤,œo‡“|7GŸã†Ð2Ì"såù”ÐÔÒÜÌ6AY#nσbuÀBÅ^죪:Gö¬ÔBìÉlQÐP\_ÐÔT@«›šŠ༩¡‰‹}vHîÛM|{ccßÞÔÔd´4æ×s»¡©oÄlänccC'ù}QC¹¤)¿€?nT°¢´­xYQ{¡± …°¢eÅMôÔ¢ßDZÍÂü†"»jó…E¼coæ75æÃ ìÛʘ"¤ÑC ùÍÊŠç±ÙÑ ­x®,‹‹òÙßš Öä»V(ëùsEEMô”¦|P`¹°¢Ò›óPF®%}Í6³Èµ¥"׿ö5b1“0E<ÌÈ͵F8v¨Îøu‰Ñ/¶PpJk÷€|êþÅÀÀµìGÂ\’»"i †™Èy›=%ÙßÖUdý€±ÕÊp³>û!t¸ƒ0Mñg}v²ý]ûa RZä¤úæCe M7WG¯§KÍû•-.Ñy;µË š»”ct(V¯EÄv:b›Á¢Þ4 îÉtND’"îã~ÖÜo?ûAð,5߈ì“| ¾‡Ü â>PÇ¿’¢ÁÖó§¹)x–° ±|>+B4 O†±oý6À@kOæþèœ /aW³«#K£ÏH]ÌY‚ J[oÿý»—^J/=-•„@º+6¸Á'Á¼+:6x$Ä·"9]¶â{++ѕɽ):îY6ƒÍŒÜ'˜Ì¶h‰ùFß™vpÓìã÷¥+ƒ^L/ŽmLÆ:ûJ¶¡ÎïÍd‚,l$Åùèÿ¯Íý‘+‹½Ö|Ó^ FŸ`ƒ,L¹ß W[û½`á[Ù¾U2zýéZ!(~‡ö–"h¦y—0Ã'CÛkˆ3ߋ־õ†‡ wpa Ä-ÀˆQ‡Ó^Ânf7ÛKœF\ã¡ýxZN?¢ñ¡öþºôžÇóè' 2nä;­<ùŸóÉJ*•4J Ч%‘$K¼U'+I<ã6H’²ÍL²M<½yQÒñ¢æ&>‹^ôĆOЋø¶'6nx‚—Ó;^}uß«ô^ŸW¿aç?pþùØ;°5Jl¶gÚ´=Œcí4nm¿é¦öX¶Â <Óø×ÀÙ¤Œñ!&ß¿Õ+` ”¯0}3µ,Ÿà'T†ÿÜ;H#¼Y<· Ûù ükqìØ23;úyôs8dz›HútѤ¿tÝcÙ1 ïEê:i ÅÊÅY5™ &¹˜•b2v.òšxöcP;}DQÁUcÎ.;÷¡a£Gœ>êܲóKb9§3ôªüê²sþváÚ s¦î™fœŸ”t}8l?†ýÜi{¦ÆvÊý{Çæç¤Í2jÜù Òr†1nÂèQCÌ\ïÁü±ågWŽù°oßëdS«ÖUåN~c²q¾Ü·söIÀã[1Í–óPRçÕähhH44”JCÂý!8?5$š‚+PJº1Þ×ǧö?Ÿþ-[¶Ð•Æ6løÐŸÏ? 5uÀøxòï‘AwŸ|rvªùsjöÉ×=kñ¬99òn0h fEñ$s|ÒÉÙëc ϲ bú¾T¬™¤ “J’rED°¶¦®¬ltY]6e{fÒ“&=óÌ$üðԮ㈭æ¿V¦§WÒ^ØòèÊY……³ø\lÙÓÇŒ0Jnì4ax<>èÓ¯ÿ ¤_Ò¾)FþQÉEp&¬s_4(L„ooœ3{Çö÷ßÛ¾cöQzï}Qb}ÞÛ±}>Ûw¼ç–Ó*Ô;“úˆgØ:ÏõÌȲ/Üø½æ}à½÷øŠ÷Þç+ØXÝPz]Žñßòï¿?]µ£k±4#h 'Š…ß¶ ?úÅš_NÛc3ŸÜ¾ rU™!¿`ó#§S¶ù³rÚ,œÏw-Úpb"AôÝ퉒ùb!™6{$Oú)ÉiRpšë !:]Ȧ+Ùü7ÚZýwÇðJ¢’P¢’cIg«¥JwP1hŒF š 3¸C¹bÀâ#‡âáE€>¦?*&"†H¨ Õø#BãcÅÓn’³ÃCÖÚÀy D¨~²aF@^?ÒÖHë–†¦­|—ùgilmmlز%¶00öð>b@ öFÕÕÛ;²<ãÑ?V…4ZÕõ@äSÕ+DŒ:¾3©kírÜ>¯×_>ö²°PªÞ{• ±™:¶ò¨cÀóêŽÈzkCäzц'6l ±°Ž¢¯¡PÞ´wÚ´½„©f°°–Xè.ôÓU[²ž Moåóm ‘oãÄ ór!޾=„ò¤HJ˜H$Ã÷½!P•"ì ²gÆ…¢3}¼0^µ'ð*t-€Ÿ6XyÒXŒÁËÅJQXòKa``\º æI›…Eñ¼MPí2’…SˆŽàÂl1DD±°LÃ!Ç`mˆ´ºñ TÇéhÃTCT*‡Â>1êˆÞ¸@÷þÝ7,¬¢r¢o¶Ž™^ºòOãEHWÿ„YRÓeZHß3n3Óî)qáÔ÷*eäÐë—¾¾´eæ¬;ÇlmjØ‚À"W-]zÕÌ––™ˆþŒÿRZªó*ÓçÅ”›=d_¥.ÿ­±]0F®`Ú³ŽÊpõ®.¹è‘U1—Ïð­O¹Š©Rne3í‡íal†ýˆ®Ù'*ºŽ ÂE'BŠÔ{úÄöXó 55öG.Öè0i¨áëôE›ÿJÀB=„áÌÄ«»„«ÀPo†+—D¶9éu+F<Ö"Eö¤xµI¢x­¶Yi¶fZöwt;Ý×N÷ó1í|l»ý<³¨ÁLÀ²£Ü¶cÜ6O‰âÑ[n¡–¦Ö-·D67ÑÓV®äG'ñW®ôxͨ9QÒ—Ëø ¥ÂiuÂÝÂsX0µóììhiñëÿíIÕЊ\íQ}›Ùz–$4¦3i…-ÉéG™q´´°wSù›Âz+Œ)Jµ×ûµ/Ú&ZÌ>¢—¡ùÑlø‚^Ö½zåw‘Ì‚”iïûÔK—C稖£v(×û•£áÐÈ>:©ÚŽÀ8ÿaÿh,¥"nôZDšù€ˆ?“z¸¾¾ÆRÃõæuGÙÞu¥uäïõÖ׿Óþ•)óÂèsuÖ¨ó’èX«$ò¡.§^ݲFÝ!(RÆ—V†yAÂ:Û­±†:svµ±4fš×G ÔåÔ2ÌÙ™‚’˜±<«s{“-„YAÑ  ÿ‹²k sdm©ªOÝ“4‚Ž3gÂñìd5mûœÁoÛ¶m[7¿mÛ¶ys¼“ü§º“õîódùôVÕ[ou.ÚÕØd¬ÛʇgÏŠÅøè´¯k-_f3ºÕÈê?§A¶G!z7[ÿõâ0Ækü³Æ ¥…€;<^8ÐDèaç)užÂÎÖ»vlt7vÐ×gpÓ÷¯J|öŒÙÝ\ûؾî9p‰R½(ñ¸JëÞ»Ôô̇ûÚvÇN,FÖò'ª÷V¥àú×í¢Þ®ÿöv­#>ò‡?skö]»®ýÈZ±÷u:ð×ûûSC;»br;cÅ#–,Øúÿ÷ëWá8SΓY^÷eø¹ÙácàÚåmÐZÜÙð£]·¿xD¦µü¬êZò»¶Ÿ€ïÁa—Zt §~›”n¶Â^³µÜ ²JZ´-•I|gs›qÜ;ðå•ZîpU‚p÷2ÚÍäŠpCcgÍÅ×¥¹Ðv!Ü Í°v! Vã¶V]êhÅŸ\·pÓ\h‡Ø!¡ò$ܦ†w{B¶éáÙ¦G–?3zt …£ GeuØ ©é´s¢øÇVà¨í*;h‡Ü8N^î!yÁI!‡eJÆìÍç哟håÛõc'¦ZD½MMín"µêˆß|üúà]_yÒö™=O¸îÚçîé¼8SxÙÓçñ’sÓ¹]ñB¾Ñö¾½?½èÚGfRçžÛ}Ï„×G5.„g6ÍYžã Ø@œ¨q‘?·°ÑìOÀ5VÎUÕžœ0 Iu¬eÅ5SÓÚ—õ…âx–̇î)s„P½‹²‚Ñ BB˜AðDÐR¹!6AQ¹¼d±Ok„e'yxÙrÙët ìôhž&õ¾æå89M41#pb†hrº—ÄFùÿ«/{É÷¿ÿ’—­^vÊ™ë«++«ëg¶+ð‹ç‘ºþïå¼Òé{ÇÛþú×·½ãaçÜࢿŸÑ]Ûâº[Æ„?B™€â.Žu<¯3Æ‘)ÈЈ?]L$GÉÓ2"ËÁJÛuÛt,J¥¥öÎ]tœh2)­;N´.R\9ЇM¤Ér©<ï8óer-JG¯õJJ•<Ð ­¼ÖyÀ •îýÉ/ÛvÙ'[BÂ&¥Á¯h]ñÙ€ìHûÝ^Ië’‡}Ò [0¦à¢‘}ëG`dÛ(5¸ycò.h‰¶MÂäÜ¢ÖÅÐì`¯hL1d#6ßÒ_- nCÃ8Øqº“Õz4 .çð éÿâÐúmòJ–UòÈæõ¦wÏïåû#6ZHÜ´ð”r(]$Kg;¢8çºsEáØÖÂŵdŘJlºVý²e•}4*t-ìýCSÚ‚dɶKI°ÂÐÄTŸê‡Ø@Cà‡Üü>´é>é`y‡´Prl¦$ô½¼½`TÅÖ8>çÖÝ$›í»éÙ’ÝM/»ÙÝô„„FH£$ ”ÐAzïˆ4鈅ÞA+DAì½`OŸ>Èÿ™»I ê{¿Ïï½ïÙ;s§Ÿ3çÎ93sæŒ )Ñ"„„}"x'¡<Ô BÑBt/Ú‰E/ ·Ð—蟀HkÜJÀN‡béŠk——äé¾¢|ìÕÒ¹“Öi4Dúã òÊ#€ý}ü>Àû‡2þŸYl=€)JÓ9õƒfÒí)9qAÖql¾ÑKÎG%¦—ºj8“)Õ[þK¤R TÄ( L±·‹yñ[ƒ‰k?ÛŦKzèšáÙ.]ý¾ú®94]r€¥kL×J“·wnyœ°IbRFuÿ™Á3ƒæ7³X *5Å9trXxªÝ £¢ƒ”B´FL f£u2ÈŠ RŠQZYrE^Ó£ƒ‚Å(ØþšÛ‘XÁ‘Ä>æ·¼&­ ·‘Ä‚I#&‘X­ŒýE‰I$¯^¼qÛp¦ `’U&¥•ß º­Ú7¢ºÔ*~!ÛQËË•Ð7hÉ­_„=ü¹Î;õA+í¦Ïã§™ñ8Åa§Ø{˜Lß‹øßK0?Èd ¦K6m׌^V,˜Áü»"ŠV˜¿ îÆakVãß—¨ÇU„?‡g!NâžÐ~ı_“‘Z²VN–¼Á«µyÎ$ÁÃÉdä¾9ð]ƒ{?ô½§íÆÜÈ¿Éxb‚|K|?}Ž3™t£1<ŸY †„ü5JÛêÔß^šYMg!jÏ·|Ï‚pß3ðE?NûÂ÷½2>»¹JKøkµ–pßó8üMœkÃTINøàF[@w¾1±R ù«H!™ÙE°šâ7šbâ¾Ý2Jî%,Äeø>ËÎAëãg õ¥a9“ Ëã'ðAüÄqvàØªÊqÎþûý"’´ï³0ò™gðæ³øK< î‚£Pxø0~òhÕØ±UiýSSû“Í! z€¿„uGýfŸÅÖÕH7I-’†DÚ¨—¬«h¨Š’( Œ^Ç]7bDYÏÖ›÷õlmíY6bg 54žX¸àDÿÞãgà£Ø„Θӧÿ‰…óO6j´3áièEäùÉ5bÍQæÏY¯64-\pòä‚…M½å“6÷ùŽo¾ƒëÓ„ÛNž®I­\}âWˆ·ÿz´’s$Ý‚KFN­ÝKm$‚ À*º”­¢›ŽísBG×@ƒ"µ~݈¶ÊŽqV\@0þ.˜©Õ4žœ¿ðDÿ>sf@/øzÍß»ÿ‰ O4B'qkÚA/£?ú_¥Ô4sâÆ}Ù;6¿y󸦉òÞMþ°&ƒšÞÛ?ç–û˜Jwvé/E8²Z ‘­02Kï>wîîÕç^|R«Ù„/\{ _€”·ÕAð¶ù¥{VŸ;·úž—²UÚ͸_„48ÎHx£Ÿ„-ïçÏI&æÓ¤J(¦Òíˆz\ÄGy­‰ôª´=ëµK>$…D1®éûöý°o/{ìo{÷Θ¾¯íI;¾uòQ| Ý:yj>üA<¢ ™·IbòÇô—\ß§uCIZ€GOÌg™»zÖ¿€A(­DH>š|ØVä&ÝY‰ú¡aè4­B[ÐÂVŸ§—‚‹ã9=]:²I”IºÐL™)å³< N§—…»hH(˜©uÿ\péÙ.¦Õ:¾3ÚëŽ.~u?c $BHCÚ©òB£¤WDA08’)OË. H½Ò»×ã1Ûó‘oÉ#}@íÅ@£€$´Z¤¬ä XÊÜÆý"g@3BñÈœ "ËÍû,EA¹x¤( · [qÏ_ü³Ï—?¬9ðÍ®½×¿Ú·ç:sÇ¢-0pѦ£Ùl 1u¸—­698X¦UªyتuÄ W*DŠƒBtÁñƒ†•Å1}ÕÄ1ôDôy˜Ì­D!$NÆ2ó ä©6QõÁÉZ­L©ˆàÕJ-s”áÄ£ã‚á¼F©åø7Õ[wîß +5 ÚÐ&#ËM=Bt š•°yÿÎÍxjù¦òÌfü16ÀuˆjÎ$¯¾÷ö\¿¾g÷7ßì°xñ€æE‹Þ7&b±ø&…˜Í!F‹…écOV(E­.IàûF›¬ òXR°B€Á²¸D½ Zr¹Aÿ\°E!#®LŒ#,µ^Ðä2Å7¢ä˜eÄ5ÄáuQœ¢ÖqJ¤%žp²Ž¬%`¤Bk N˜ ]·Jê¤Ä¡hš–¡uh7zý€¸­z:ŒåR:ˆ7¾‹q+®ýãb~çB0¥o%˜Ûü¡B]„Vžúó@ò¶ÇSʱuñCGZQ0tõkÿ@ø¶ÎoƒPIh#Ê;ù¶…èü~¼4ž¦ãýëm16‡ÞÊñ‚H_m4MÉ~“¦<ÿ¿íì˲ |c÷gŸïÚñÅç€[°à±O$›Løo™B™¤Õ EôÓŒ'iµb°"É×&ô†8Q$-Šcz*ÓÇÓ‡ïí8(Æ‘x¦^ˆ×K/ä‰ó´I ¥L£MR*d0—¼‹ZJbÃÒ®þóÜÕ–â}aåÙxyÀŒnŠ_eº‡¤†0…¿þªè6=ß•QêðDGC‰RY–ð ~Ǥz°¿øu@^™R %ÑÑ;ûlÁê‚´n?âa°õÇniä¥ô‹;?ÿ|çŽ/-xlþüÇ@'ç{Tr˜bO‚R-únIð)‰ä i~øèÓð¦ž<õ‰±2Q—iôde±ûô ©ôÄSu·C•Ÿ+(¤~xÁÔã>¯·þ{àeìãõÞ×ã3Ac*²‡ÙwqØ 5€zŰÏï $AE&¡±Äæ’¾Ô.6n›°  tÁ„m…eZÞk¡º!$Láßì¸{ϨÍ5]ó’?"Š7ª…q܇ü›â Aà_j½)ã~E,:I2MmÏd¹=›H>Ä VÁÊ$€›Ì$\Ò•´gøc‚p£RÏ}xgE>•'¨¦(†½jܾ±§àʲu0YT¾‘!ŠíuÜ/tËÂ%øLv 0bÏE»';=ýwõ†Ý^//£ô"XÁêC—únþ[ac3n…Mx‡Gr°¼½Â¥x$l$Ñx8KcÇ4ÂØŠ€0šÙ=|Ò‘Ú\à²ý[5€Ü?òaœÀÕã×!¹¾Ìà×Ù>†/ò8Pï+ë1‡E¶~Á ðOøYD2iÿgqªÄzÃU²œŸ‡æRnF'†¹Þé±uxŒˆâÿeûwà Õ$d1 Ld“˜ÉDIo¿pôI^"Y`g"ˆ™RŒïÀÿjA€‹V&†^”YYï'e1¬çy’ ì b ëy‘>¿¾(·2™[ø ¸º"HTÄú'+˜lïþz¦!Ä(ý]6¦ŸÇÅ&g„zUÌüg_˜€ähAî½ü5I…(¹PªD3ÐQÂnÊCÅí“dAä´.£hD"!Ó}5ºcN†‘>†M÷ÒWÑéÕØ¤eBêné{°³T2pÛù=ãueGt€¼áÚdV½iH¨NEˆ¶Roˆì¢¢¥rüˆosb¡K¦˜ØOá†ø³ñ†æ–VeÚî„ÏLÜ&~RŸÐ˜ÄpüC™â°|@]Reåc †ýCÅÂÐ,9›htôª„÷uƒ2GK¥Î^£ÈÁ»dòÅ=ÙG¥nÿ‚@´ 9éN¶$bä»èp&͘³ÁÛj¦^è2Óþ¡¶¯:!7"T£õpËRB£#aÇß*Ê‹ Õ †ñ± y$ŒM”©¢`j|Þy áNEÄ„E%†;#rÜÁ qÃ]aut^âíÒ¦ÓšèåùN¿Eg³Ñl0vx]R  Œ®ÎЃyñ05!J-cÃÒ¢ò|,5]hDn¾ÇH­Q¡©wr­&$27ÁWÍYó¢Õ†0Wxbnì #n„3<1*Œ bÎð„<Ä GÐeai”\i©ý‹hNµZt.³h¶è©c%N6HŽK´;Ú1I³Å!a¼QôQ䱂¬gª?à1¶]ãzÕ2·øþÚZÈ’¿W[{X›W[1`@w€:Ÿ2øêB+çãššØDS!Åhù¬ÞæÊ\[QÞ³Wy)yFBë¦-ù)‰§˜ÉOj;F@¸!®‚™)^žš„H'Ë0¤¹j‡:]rÜĉÉÑKwz£Ûï¨)-¼éÕÄGPmÓeõ1éYLYÏŠŠÒßW¯鯾ˆÉKj{&•¥™`ˆÇÀ:jk}ŒZjjðYùVM>•ÂTΤΧ,dSoàºí²¹^N`e¶(Ž¢7¤Ž0ÓU›ÓNØ‚Ëc5‡¼˜i z­ÈR¢dD‰z$Ê!Žhõ;ì3µµùšÃµµÈ!‡´j›‚Áµ½¸¶k¾ß”V[N)ª›™M¤ñ±Œ’š÷<û)uråÔù±gyEiy/ò\›é²µ}ÅV%å3Å#Z F´jÉ eÄH ýp럤žõC Õ¹œ„çéýŽš8nµä8Ô´ìVÞa£f>Q­£«f"ñÙÝT@$on¿sÔ_wiEEÏ2&+=Æ_{Sä¯}¤Fª>˜cµµ¹:ë{rü¡¶! ÔÖ²£›SËâ¨xy¡©í2‰i#|N¢º,æSêàʨƒ"¥%¢‡ù«¨šV õh':†GÏ +è]ô ú}¾GG¿ ›è° îÀñ Nð@ä!&ÿJ6aý¢ËãöyJO*@èÒäI—@Å-ʽ@ünÊŒ^·Œ¶ÄG³DƒÛAT<6èŒ.’€I™Db£Å]^’Ž$!äU4zˆxGE:“(ð´ Z¬#]¢ âºÓµ®—ÑCʱ[é4Ô&XÚ£4[³ó¤`ÚšÓá‘Êé«$›P¹†„“´¤¼ž‡‘'ŸMïuS×¥÷:Dúk·; ’0#uÉ'¡=ˆh‚M$I v«hRR‡”Üëö¶ÿØ=¶6k˜*<˜#Ë/ Ëï÷kÞõ’æŠn‚«øÉŠƒ—^r%gTô«ø¿6¦X,áæÔœ%¡A£­ ŽÀ[JÄè@…B«0i7ë7ï¬9̬ˆz|8‚©ƒ‰ÉLF¡*.žlƒ »-@HÈpS‰¨‹Hÿg›EœžÜœÿã°ó¡Ì°`—}Vqlþ9éʵˆˆ‰(ŸœñT˜dPéB9Ü©dC!xƒbÂð•ÐW™Fv’Iº+tRh¥É÷Ï;Äb!X[²Õ¢}œUj>qêƒò„èfß624tI¯ Õ…ñWŸSW|Çð‚;9/7s¶Û’$Ž‰Ó UhíyS§hii í3¡{R‚B1T³^ÌP„N˜'ô0þêçƒ#ñô©Sª'd8µ‰«‹ö ŒÊH Q„ ,Œ)?}Ÿ&AhI;RÓ˜ðú°ø³_S8[·ö è½q+ËTcÍB^ÐÓž/1Aûõx[h ¯™¦ZÛ-F!>à Ê™ä—*snÇK_ ~* ¨ÛHûឆ~«i·Ú–5åç7õêÓ]‘È'òi)J'Ÿ–&Kr™lÁKþåºxgpZ†˜–6‹çµ™©­U‘¶¨!ME­‰Ê¸Ö| ÿJšàíéÒÌi¢3Ä)89'ï pŠiBÛUiýÛ/t6lTš¶òÊx‘´A_ ¡º¸Â»'¦Ïö ÌŽ«’l{è¡1á+˜±ºÀ!mf 毮ªìE.þ0¯_Ø¥Ál¿»3B–a¼áá†jÓ+ÕQAHè\|W£”ŠrP êK?h b/$©ZÞn5± 3¸LZO.U"z ˆÎ¼Z„H0»‰ò7ñÛÈM¾ × )¦{ÌÙø¹g/›ðb|£jN1Q»÷àË_Ïã#@ýõ~€ñørÄÞU—R3dÎäË«ð‡Í¸Dk´¯‹ÌÒf7@´%áõ9¸gMFhQ\²X©+uðWñª‡¯à#¾{aÑáG" ˜ÅàkG‰õ—Cÿ »•ÐïCîfi?‰×öec{6ó*0½£Âk¹»×ÉNv†Â˜^øbµ-¹‡'ºÆfç{fD ¡]Áì*RJ×T¡1hB@ç,”¯I…$¬¤N…'Õ‘Ñ‘‡(-À/·KÌt•µ}M‰*ãPÑ‘Þ<~éÚE5½UG4Q»„ vÿIŠÑØ‚Á¿3J³ŠtJK*æ2ŠJ!&†™Qœ_!6æ™õ}&‹KÊBqlc «jÏijόÝìÜٹӚ͞__í},+%¼ØbŠŽ,óæ?X\wo *æFNNñú„“ K àD;äæRnÆØã&‘Qƒî¨Å>ì)(d &îf¹ùÀšØÝ‚‚‚y­¦ù9[Hh0£°r †ˆBÞkÆ×«î6Ù-Éç{©”‚ï‘Z!f?Ó·8¥<¶ÛàC+ªãCŒŽ’îƒûÝ‘Ë?ØýZzúØñÝâ[ÛNÜ+Š%¡åÑ÷õ鶺­$$týCS™!wAð„ÖãŽú¢MŽšÈy@̲{‘ ½ÕÆýƒ¿„DÒ9V”€òQ‹¡íèôN¢'+;‡^Co¢·Ñè3ô-ú ¬QìÑ¥<£Eê¯Ã?á=Hö<€Hv/Kzˆ2 ­ž&qÛ%ñ‰äãH¤CïpÇ8:ô:wyØ®ÁF3úûQü-)­‚‘´41)”Ô,JL‹PG ’t„0ˆ2ž%xºÕàèòÉCGj¥ÌL¤<ÞèñR_û#(3“6(Ü‚ž°m©Ô!šºyE˜›´¬ž’ð-ÍòuV½ÇKš`&éØ>\`Ph@/QVè%›Uc¬yŠW̪’m ÁËš£•ÐЮà-߆YØÃ\Arb.xeØä2ü%ÇÌOo8 '&¤3л ßËò+G^€f*ÃohÝáÃçaBiÏŒžå›³>SVV†(^•å¦m2†dÊÄxæuA<œ?D&OÕ˜5J¥Ú¢Ç''†Ú8Éß<·Z–·À ¹Ž†Üi¨.M]]§Ês•ä­9éNø¬,©ÔîWÍÆ~ °ŒÎhŒ‹§+«, Ð7ÖõÃ?ÇÇq&'Ðå3!¼14áæä™Ôr]|jÛdKN©•Ý(ãåò¶¤Œ°Œ¸(OZˆBÐëã<Ã3õW4%”™´ò0G|^Yrp :’ç°y¥â.û·A'*#&½{}±)ÜæØi”$šc+ëŠõÊÀ¨xsä’ †›'›£Ÿ8—Ë 1ò!LJBÜðFJ‘Í ×˜ÔæB‹Ú”SjÏÔ·ä[æ÷ÐŽ¯¼·\6w°Z¹8?0hλ2C‡5)ªí©êÑ–ü–ø»´=æß%ÊV÷]˜¿X©6‡ ͼK¡h:pAx6=hW“à‰‚`ˆÔã0zƒ#€0Dy„¦] ÄP&ÁWñ׌Úr¢´$͵+š2”Ž&0jº”äVÓU6µÁÅÎ`fÌ`d“f=?-ˆ}ùevÑŽGï;&ìlÁ»[ö Çö~”{ÿ(Ã]=±ïøÇaýaþ|²o þŃƒ=|øQøÉòºAL¶þ–‘êYdB‰QóA·Ëoc‰×›Õ„®Y+ëp%ã‚V·Öå÷P›iLÒË'Ì™#‡u¸åîep–.Å£aÃÔ= ÿÔÿ |ANŽkøðí}áaß—+î[‚¿ƒ'–n]ôÝÞvN÷hiéÑŽ„­b‘I2Ûæô¨ìÔRŠ´©‘j¯KVVmUKwü³±—&Ï#ëËaê…Ë—/\zû]<¿óýnüœÄãÞöø®Þ²{Ú8î®^{¿ú-.ı à`Ä_ÁGð¨ó}„ Eûu›ÉŸH "O³¤ïM4Á½Nòt¹á­Â.žß1‚€ßø}žËžßÁ¿ÅoÖ …M|ÛBžgçò›ÈË:a+Š[Âÿߤ—Z-˜šöúÕÉ©ADB+¡Fi…$†à›D1§á„ €—÷öDà5ß-†¥™y¬wio·£ûØÂƒoòW×zðË÷ŸÂk¯ôp £Ã!ïé¯`ÿwý°ë;÷´zölöÜ\q’D´ˆP›õBCФvukúsè麱CšaÐ)§SϺbÔ„O®M÷ÇéZ²4¿”eÐ<Ò‚˜*ô 7Kv(LÒhL؈–+Ýfh×Óé®”ÊÆ\/H6Û õæŸþïù™aí†}ñBqË+ßu…)ûjôzµ„c¢&œ¼šrùDoA¬yø)ÆþäWêày‚ÅF4“„Ze»Œc´ê~ ÈeP«X‰éÑÂeý’ñê$-g‚ AïŸÖ‘ôôD·š¾k%Ó8«4‡LK¦‹´<‚U#~æjaV9Ï•ñ*LŸ±úÎ9zþúúé½2¦G)âƒä‰BöÀêÖñÕ[ÄL!.xCrÍä-Ÿòú9WògL{å»UŸ wr%‘ý\±0ÁÒí‚1åìäêÞõ/)Ÿ¾`Û GîÐîˆÏNЏ×ϰٌ廴 ,W21;gråÚOY³ vZ*¿Ú­qøæŽÍôò¦Wž1[S껵¶läŠÚ¹¡eùÛ'µÜ5êÊ(Mt¬^›ð_ŒªŒ™Ú-¸˜öÿ%d“"bWúՎÈI6ç _Û ×_…lÇêÕŽÄR BÄý2ª‚&ieµ+eµ+hYè‹­‡GWX¤FL®ù#,0éOz%«¶fªÔn©ýø‘ÛßØÑ ´Å ( sþãÆ¾ÿÕ#·—N¯Ñ‚T%U“™IN8ÞôåÑŽio5)Ζ¦>:Ìv¦ä·ŽÓ ¿(âÝ'³Ù¿ä2áR¡Å³MiKåùkɺ‘€‰€ Í÷å±·:N;~LN;†ì-h:ÞJO;V ø rÚ1ßÇ%ä°8›Öž“iè1+?ñu÷…ݶ †yOÌßBF”б’’ãŽñzÔôì3]úÖsj/ê)`’Äv+;V€jž|ìHZJoÐÏÙŠY6`},œÃ' ÷U‰wí¸Ÿ ßyè<ߢz}‚àž«Ul`pû<Öáq_†pµ›~0@¹À÷Ï\¸jþª¿Ef¡èŠ;i„(fHs8Ò<Ò8¯Ûj¡+U‘¤Ed ÷0ž‚¿ +MK®ÆßÌÝU´EØ Õ‰BÃ_ÖF@ÿ÷ÃŒC/À2€¥Øëèyü3žðQ/@Ûßš/CØÚ ?7 ”ãgqmÏ÷R¹,þBàtó ÃÊÞ«‰äou»{ÇÄÍIaKÒš‘ü¢ÀÓõ|¡ j燳dñ˜»¯¾ àå—™{Ç.YKÂV7ÖO±O®o\½$ÿbŸRßxrἓõ\CXÌ¢3OÌ|‰ /ŸrI1lögÅ„…Ú=! XØ¿Vkû/ =±ÈÖ,õ€6bÄëWGÛoñIüÛ(YoWA…v=P:h·IO-Û™ïv~ñÅ·®_÷ÛÕßµû–þá8þTòŽOwî¸zuÇÎOÛ¾ÝöÝ÷PcË}s5ø×ÙT t6ÈÕ¶>á“„'ýµÚº\GÚ¡J»\`謕ñîÃG¾ÿ"µâA)Uý膎JY-©‡ xŸT ²ÙTÙt6þ…VºýÖ'„c?‡ä(Š‚ÚŽn»V õ!Ò@¶@Õ¢š¶ù¨½¾›{ú„~Ÿ:FâU+á»»ñ– ì†OñcW¯Bé§m?î„\xêå²дëî†)xçÚg¾DµñoIs@îö!/¬íêµ¢Kí’Ê*fîß7kÆ À#foDO“bG‘béšP–Õ@M˜…F¯“Êï@§ÚÊĵé‘GÆozäû•Wm?nË&nÕÆãÇ7â9c1>Ýÿ³ÌâïJgíÛ?— ýŠà$% ´ÛTÄÉ Ÿ¥KÝBÚLZîî¼U‡ÔîÒ—}uB¯^Þ^Y¹©›ÆŽÞ\‘’G&¶n'˜6‚ Ò6oÆñ‡°²—7³ª*Óû¬&µâêæÍW+ÒÔ°—odÞØD…RúÛäKÚH— €#mr6åBñ¾&BÝ[tЉ»ƒÂ.ðjWû¿©.Ñt 8ìÇÙ5sÜk6ÝsÃ@kÕˆ5°vãš5Ã*Gµ‡l˜¹ëáÓ§Oo\a˜¹á­ºÕ+7o^¹ºŽÙØÚKǼ¼jÅ–-+Vù¼ºªQ Ÿ?®÷ƆÆß ÄçKB|$]x r?[ bŒúv 6:2R¡~‚Í<Ôt3Ûƒ÷33e²6Ÿl ”,,UɶÛzóW_«lÆÿhRûÉÊdü‹‹p̰ê9*hÁÇòÁÆ dV!ÿÙg«4-0i D*ÔH‹7¢ÕOHÍ6à÷-°Ô-°c-L^$ã9Ùb˜tïöKë- ÷♾,ÿ¼eþüÅ->Ýv3-66åO“—ù ,g 7Ú‹I#o L†’Û?“GEÊò—ÞÂt·¦ßmÇÇXÝÎ(0Øõ÷^¸ö‹¯®_TÑ0ø¡S³ö,Ò“Öâko¼†¯®‹K Ñ/80óÔã¥õ•lÓEü¶”xŸ[xüðüÒ­2*nýë—ð5p\z}}|T°v dŒ9|¼0—4‹|Â(‚ ­ô’àLtBM†þïö䃇0\"U3š½û¡‘ùúÙ½ûÞç“á]û÷nš<å+òǵÒ×õï¿1%äæÇÆ”MëÜú …‚ Ä¡/PPÌ' G";¥Áßôæ,‘pd'¼}N¦¢*ºô+§) Œz{ŒÕÒå&h*#h*EU¨µ¢Ùè´ –ø.ÅTµ¬¥Ë[8ØosÿöÆv}³²¡n"#E2f7ÙN|K@çmmlój ‘wJ½.Öy[n)PJø‡ÐÊæ HOd„'‚Ëü>½V鎎pG2Égº3QîðHwÛÝ~*9Òå‰ lhHˆtGD¹#-¶í`cÅëO2oµÝªüB5:U§±êu1Qže§^­ÕÆh´V6F+ÊàФA<¯µiõV­&F§ÓÈD¼ehªV¥Ö,ZM«Õ+!®uw` É¢·huV­VñìM¦9ÂAþ X`žጠIŸŽMJH Kð„´½Ì|šN†×«|e kI ^0§ù.1)øƒÊsßHݹêâ™`– °’h›ImàûÕÄü“a¬öÒf; ц3—@×À3¾¶0 @J¯£ydLÛÛñpÕ[(„äOg dÄCý-Ì_ᯠHdF”$­ÿûo¬´êEµQ/]ÏC·ß¬òÅ ð’âIœÃ­&Ó0Vú<©©iÒÍy“¨^ß$ïg6ÞüŽ-«i{»¦'«+ß\þÊÖ­ÛïÙà;¹þî;îÞÄo¼gûÖ7½^öú¢ææE¾©“çÏŸœÎ˜ß{ˆƒñS {Qw2ªD:çä’ZŠßÛI˜~CBô¿^ð{¥ 0Ë@@æ"Ò…8Z%+‰ªŒ¬Òì I9m’æÄÄÄ%TED…© IUñqUIzUXDdeb<@¼-+Pmbk+%<8õàÁí9I•u•Iùï3Ëqy¤;Ë剈L‡uîÌ·:Øž_ZÍU÷,ÐD:âÊããËâ‘Zyp|yOUšgWªÝ9Y–ÃS`TÍ3›üáG’z&TÅÅU%xðA>4ËáÎr‡‡#—áß¡¼P:¥2! puh"­$‰ä‚dº]ÛÃãõ+‘/3›êžHëA¬‘Ì=,¹àÑJê~ŒÑ'YÊ)‚‰LÙpºé$¥3ÅJfÊàzR›GËûg$ß/O\]Ť–’éS«g»mP¬—q‚XwÕZ˜È•…Žl½3rdM ^ÄŽv` pÑŠJržûR¨ONØ:",ŸiŸ‘õ Ä“ØÕCTrõ?ŸŽŠ;Ó¤åÃÇ…OÇ_ç'[Ó¢”x‚5]eLcö ²çÙÂÍ•rpKbâóOW„”–ÜÃŠŠšŽ¿™ãIÖ ³! !¨WYYœÖ¬U+©ä*ž›d¡LAÈ¿5"DéÁÕIÚßq ôN5­4”×N‚íè'Àß(QC<„T¥¢HóTÖ0oæ°ŒÌá^ϰAª¥*B¥ŠTGª˜Æ›×jw­µÇFéãë+Ã#ÔÆÄª¸XByêÐð¨ò†8C”Ý~ïî &® øçù ~îÞcŸ™04#c脫•Êp•’”qã4dÌÙªôÊH{yMíFBwÚHG]e||e#RG¨ncmM¹=R¥«Âï<˜•¹xÎÄ‹͘8Éþ„Á¶¢hùÿOl–"ØkîRÕXrQÚí’ËüÀŒa§oÒü¥F[X4­ /7.’ù‘‰ŒËÍí6­¨0W£Á_ýŸ±ë¶/½tA 2Ïè^8«»ÙÃ2 c4wŸUØ}†9HDln^ªÿ‹<œo¿÷÷?嬾8æ-_Ü‹÷¯ªÂWzõB\'k¢¶mô>ho°öÿÉ ¸+¾ùÌËmß´ý+¶Ä-8°-eüxÞú/8b:ùO:* uÿUÎf°we<ÌÁÿ!se-¡`“x Ãü5‚Ƕ¡~‚8¨ @|H€p¡|Iìd´ÝTÛ®YPí…ª(jõv2Œ–b”´JœÄK¾u–K![!IägXƒ-*«^iá&Dbë¡pJ@ÿ›:úŽð¾Lj ›¤kÍØÏÉôÅ Â Û¤ˆ€R1Î¥Ã4¡2‰ûÜèÍ4$Gõî§H |Ð?ê‡O”‰{Ö¶VGÔ~ödxØØÖyD˜ÕV>ó–**Í’Z€ãB"•n SÐÿÝDZ5:‡òdï_¢!û2ôþÅ1t'-ù’¿,­Ô´šNS;«^e´º=j‡µÓòÊvÓÀ«ß Žíî‚-ÍñµoÕ³ö”úy§+ä§!»Ë_Þƒ¿Á×3ë«c÷ 2¼„?Û l„H¶ï’K=“Ož‡…»XÄ S„Iü%â×âЈš‹Æå¤5«D$Zýòé@#¸@ÒÞðëT[ý;ßÿöäA1áëK3íïáÜÁ÷àô{ö¼Ã2y­gzdá_„Òag¶ñ—ð·Õ!%à~£ªZñ¾k…Ì·œx}v¾[õΧ¾'˜Óm´œ»Ùê.}x´1З äu‚µï+=鯅œc709¾O¢•ØXÂda–ð@þ²/Lôe9u~ƒ‚ÙàáÊÜÇD!@Ÿ"$ô Åxýº*F\t7©]â³°Ãz«Ã£v[™ŽÝ>O£ÛÕÀ7^¦`§ç°euÚï/ÅöøÛ0¶æ5¡žúóSÃCÿQè’7T±u‘9o‘Š>IϘò`NÄË…9¯¶  ‘ã}ca>T²øësEY_ÕE°=ŠÇõ>תE," 3ä7‘å BÚùàïúø­ï­ô•¶ÌK{ß+€@Ø£¤Êîrú]©Öewonã\˜×ð¨%Õ°f¡†øŒú‘†{‡²ÀÒ<î°Î§š6è¤];B{W·€óŸ¯ÎW(ÿæGسb){©Jõ¶¹•uؔÅù ö¥Ë0¢ç‹xüÂ%§Ýò·KÖ퉊>r¢oN0ö¯A€N$O#H6¶#Ù%1a ÅÐNILìùÉñ kms祹¦×|ß ¾ {_¬ã/= mûè1Öö¼ñ‰o sôշߘ„ K÷“Î7èÁNz‚Ý¿ûÎù.Œ'>©^á —1ï/G¬ÔÍ¥ü»„˜¡jIJøõ´ÝA}¬.ôt­P Ò®–¨:¨GÂn ]$ýv?ý“>¢‡~Ø­ç[Cï0DçÆýÚ¤zÝÒóÇálÍ%JÿxzDè/Ý]öÜè–ˆ«=~Ø4zªüï¹â²Üª×ïPD[mÿû÷Öù<ÂÔÑ La§=qÕ‚‘Á¿{¡$sñn£ðVFÖCƒH=r¼G" ^þò|qÖ×5A„£öó#µøØÖW‚XùåîëØÁÞmúÒ%O&'ï§²rö®q•‰'ú¨^ºy!bÛb@ºz ÛH%mÕW"tÛ¹èT2øÙüÚ΃Ò|×ä]+IUœ)F*]‚(è äêÂ8˜©Š5Ì+¢"rGªrtS¬e^‘ÛÓ”»¡Ÿÿ%byIÜûä)|¯ÃGOÙË.*oØÉP)»*ñ™ù[[Î8·{²«ð˾ôíÆçOã 7©Z!“½ÔØë윇¶ø^[SxGðšC8*íèàAG† 92hðÑ´¸âʽ›êë7í­,Ž£ÇSnýƒ9ÙnQÊe¤‡çéÐ)Ùîu©™ÖRñlªõr÷†¹¥?±s"»áÛ‘Ä-‰^Âp—ýÙ¹ñ${¤”]ïgÑV‡ß4­šx‰!Núé©™¾}dá‡~˜ÉEå~Úúaæß.õ#Öžü’åk™ÒêJzćݼ²9iï"cwL¿•Â#eÓ}JÞ(–ëo™Ú&5´ó•ñ1ÇØ§=ou4ôö½»¡{ØÕý ýªù7_æn|»5~'7×Àfqûn¾sW†l«8ÓÂuçÞ@ zâÖ?ÚøiÑÔÀ¡ÏK§Ö¯¬&7}Q‰ $Œæðͯ4_íŶí’ýÃø›¡}û²ü›ÜMÍ›ú˃³™õ¾þcŽÎQp]¼ï>Rü£·n°þZû ¹.¢ë§ex‡Z«ŽQSYN'ñìþ5Ÿè"}ÇØ Øë§ñׄ¶üoY| ÷éËòWn^ÆÛZ M´N&"GL*)Q´þmíÊVR¤Ë¦¶ Šábyü0“Î@$Ã_ãqÊCÌÍž‡Ïä–û6ã9÷H·BˆM'eÅ¢D×{©”¥öÒ‡T¦ô È êöª½´:5sg2?e§Í„|2?äãù ˜y ˆ &ü!˜h¥¯å0«Ä¼Â{'׿Åq ^‰w§[Îj!fe¬èчo,…Jß|ç ˜ì?+ÅW“6•Ò=ÝT2tPUÛöÓ2.2zwøœäGZ ®vŸ^G iªS0¬v ùѼ̉½Ü±êŒÀ–Æ”YûèžEldVÒʤ¬Hª¶^›afZZC½ÕǸ]bXukvš·Dzy¸=cΨ}ôa)U‰7-ÛÚ׆ᅳ¡–çxÙÁ¯LNú2}››yÙ†”‘x´Ì÷X®Ð? ‚ùƒ=–fˆø2Ï«ãy(!„WÌO À/¨Ê“%¼MÉR<¯àñ¥å`¼g8þY¿ÓUv×B.¯Ä×G„eo_0ú­^ >Ÿ7ëcùpH ‹#Åeó¼W0²Â0 ±IM“Í<£ÇG#gð`Q°ÇD¼‹_jW8gÜ/‹r t²ÈÁ1õ è‡îC&ánBï„2üŒR-Ò9#™<òvɳWë;½Èæ%läóÔ;¼z—_B`½ZÂ3À@H†uxý³È:´$–yóÀã/Ï;€Ã}O´×å}’Û"­”*Ø€#Ê3“ùõïyýõ²BÅ´——ýÝÖöù¾Auù¡×sZ,题ï~eÙŽŒ¸[ÂGahz¬²WiùÚ˜ÑøVÌAÖjJ„Ìáƒï è¹q}I W­M/…A½ð€Ânî~X»&ßm= òÑø¡*ØÙÍÞääFe¨™R;áyÔ>Uû•1äÅ¿FwÂÿs$° Î÷cÁ«¡i1e9)#úULH·Åž¼ÂðŸ åõõi÷ëã2Âb]!öq¡º–Þö¢$ÅXüøG±¨j@£I¢›çn¿B7ÝhvÏïQö_À×?°ùÐ6ט’ÒaîM ½ÆO*íQÖ3nPCùØ´[jÛFýXcždjsš†kÆDiœ®$g#rˆ¯!JE¿òô2òãŽïăȎ²¥…)ÖÅÒOʪ'ë0îÿIøüsÝÑ3¶á…øžÿl ¾ËñÔæÕ«áÅÿE‰DTŽù?.½@÷£O?”lþ ¨`NÃsøy&-dÅö=œ­%Ü5Ô8ªK°â?ÁÌkÍ@œÜ2ôŽ`åÑš‚ŒÌE(®ý/Ò‡Y$ê&é"µ+kõí‹âk×ÊùoÍRoQ‘7«°psn‰¥O+Șrçª)KVî[k¯lÛõŸ|UOt›ZP0µ7r‹±“7OÞµáîÃAm¾ÿ"Q (¥£žQUkþoPÏv†Á?Ü‹ñlÿO¨åq§ïæ›õCËvB‚’è"‰÷?ò¥ÿ²+ÿ1D‚$oÎãB*†(•¢šNSψf"b²v«HÞÙ-A£7¶‹‘/¯1J‚£••Ö]n¸$!Q'‰ˆV¶C>L÷²ñ°4Ë\Ö"lSá}ƒù¬Šá#ãÓžÁ?‡¯7¤±ýw3ƒ•±s‚9×6ã3áiŽåùs2®:lè§ÌU–›£Øs!ѯð{ò<~r _Ç?T7ÇËàoõ¼[CÂø¦|b‹¸ˆÜ€¯ódJÈ2NÇ*òñ U\$4™¢çÇf²xפ09Þiœ¶#ÞÇÏOM]ˆÄÎŒZdCN”ƒÊP“4P¥ÇHð›u ò±Êæ1ê BôDÄ%ûItRa·RK(Z»ÕßµDþ&¤@äk oD"»™¸Œ©”— ßn«`%+ø®wÖÆ_Ãk_p¯¯+`éÞnhþqѽìè ¬!š•ýx—!°B 0W3cNôû,þiyʶʜôYi†/³»‡þšø#þì&ûM£0ÿ(«š‡t5ÆqƒdH£[o)( ú„FQ ~­d ÃÚqUŲI ‡¼H ¸~"#H$»gö°R“lEy€ › ›µ™éi>+#êí,Y:pø‰KÏŠVQouXµH—S<ì&Øøí¼xç%<ð‹fX¶ß÷O²m*´~FL(=øžõAÓYî¬Þc‰¥°Êµaá•QI[ØO4Ó"™wéÇ ½¯í*èÖ°l»[ºi¨b|ôÐ…2î„;ñrH„Ÿ{; ¸ÜøÈeÜ÷«~0yû;°ïë:ŸO¸ÚúízV~X‹w?c‹‡$²¯XÙtì*iÀxžêÍ0Þó±Ù¸äý~bÓðu}X hÔ«Òð7Í:¿+ê¿[Éã¯ù‰iˆïÄ«ˆŒ(u“¾Jª=I&¸À¬ Jfx@o%X“öËé¢Y]ñ¸¨h¨—Äeº„oûC<Ëœ¼gpŸíÃåß¡>ÚÞgð=''°S ^9¶Þ¹mÞüûïÜ 3W?2ñåüÇ?½Êœ–cû0ô÷ìyæÕ§ðß#Î(OU‹2ŽŸ¼öï‡Í“yN&VŸRžü;"ÒÃßF[ªì$¤BmXI&¬´^zÁk¶°étûGZ:—DU#¨‡Ù²懀6HÅáoƒàLЪ¥©<þNµ[ƒêá׉³f<§ÀœÊÔnÚ½1¾8™¿¶zóùZn˜€ïˆ]%ë‡Ø˜%7Zóç|4gö9É×rº×nª­­ØçD,Ú€0ˆ•ì”Ðþpó°dÏÚ/d¾×ê¢Ãâ#¢ôZ⋨£ßþåïpóoÎg¾’bnKî+ä_½áÚÅ-B\—Ú’PrwÔ÷‡‹ŽÿÍ€SRå·×ÅŒù“¶µMìhþ“¶1÷ýI`[PGƒùÛ\Dw€ÿr“ÿ€çO Ï_†ÿYàà ³ð;˜Q3jù«PÛþ»Õ·î¯ýgÄùøŸ&üuJÿ€µ;ÐT4óÿoÿÿ£¿ü§Àÿ!ŽñÏÿvL‘u"ÞH¸v €&£ènjþÅ-ê¶®þÿĹ˜?Þ·þÏBÿ/?æ«ÿaŸÀ½†þ†ÿanýIàÍ·;:‰a¡´÷.i˜8Û¯6gTšÐiˆOãNgb¬Žá:tèmvvÃ0ìüóx+ž·>†áØÃ}!çð1|vQm?ÊÞkÉ1›r,Öœhs6í¼ ÏÄÛ^xZ`)´>ãÓÂÏ> Ù‡Bþø™W sÌæœÂ\“ q¿k˜ç¯4-¦Ã— éž¿ÚN_^A޵ýý/µù]Sn»1]Zöû–;»ªÿ¡uLV4­Þd&Õÿ ÎÚ6tÖbBìmÕ8þmE1¿)öY„_kÛ®v_NÁÿ£üÖaà?Œ^"­Bt꺟¿z#ö¥—Ó%BôGÙh43È÷ ;>_"ÿ:,°•íD¿„? ?~[x ?Þ™~韄§á’î…À¿Np¥FñQ-<ŽÚLê8iVÕ¨’FŽöˆ®¶2bzÎ|¨:]RšîÞstÅSN®¥ ¸ÂízöÀ¾‡ªÊG ?€Ÿ›_]2ˆy@½#ˆÐEÄ–(ß :ëAè_Ö;ÿ¼d~çï‹C£ÏðoÓc•r^‡×è5Šrð»­Œ ‚ƒüëÁÇN½ðxòò„SÏ?•PùØ %.K,¶â7ð±G ÿþÛ±Gð߃\f.ô>ýâéÄ剿ô8>½N¿t:iyâD<Üøácø‡SPpü8~ú¨ƒ^¸õ0Œ)é”â÷ªŒZ½U²ÙfÕà¨ñ=–Sé ì7Eu;>ZéÈ#=7ë×,sØX X•£ÔÛ#6®‡ÃVû‰Ìæ`YQ܇Ï7¨}"ÁÕGè¹xì|L Í+*žÓ­pvâÜð0‹ï:pqÃöô¨>Aꆭ“¸+ŽRù‹¥ÏfP_µª”Ï.ú¦ÅŠÌå.Y°eö²9EEs–ͶË |)–´¾,[0?ÚM¥î›fE „„hþ J¡˜—tPÁ@·È¨­9o H¨z™ƒæƒC"Cº‰ý åá¡îËc„œ„ܶŽá…?ý­p˜½©õ>È-ç ÿ¾aMáå`ð}bb–1ËÍÌüZvΔž'Ø‚x p½öfÈ Õi`Áï§© e{K#c*Ø=§ädC*³x½/‹ya=âÑœ[ß°o’þQJ–®Cð{ƒzéÞ{z,±ºgÓû äñ’b€Ú¡U;ÔÔpžóòxw MCºÄ¯«Ú¡ž uŠ@rYP¸•}t'tƒîŒ2âyü1`Y2‚ÏWVV⩜âMŽíæžÐ–Y$“˜ïo ø“J^o0…zng\¼¹®0² ÕRP–’’7­œ´oI)²»’û îⶤ9,ø¸–¼@S´Ëïá‹£B…pÆãL™²\•kÊƳ1’Ñàäåa’ÁÒßIS~¥kQ`ÐZY³šüX«–yý±û=ÎsåVuÂÜò’Ôm5æLGJÕŽ§Æ}Û<¯¼œ ¿a^3gܱ»ªwföª,kÆ±Ò KwüAlöÍö톷pý1ýq¤@¡@î?èŽë³éôYÇ‘QdÙcXíuu|úí_>ó¹³1ÕÙèLÒxš°¦á8‡›‰Oà±²ï‹_/P Ýrªúôݶûþþ}«2¹³Cû9ý†6¦¦â¤ûž²±e㲟÷nÑ-¾Ò˜¯`«hM¸ïàýõõ÷¼/²Ègo½'Œâ_Cù¨ùrFC‡=é©k·g#=• XR ÃªôLo·„#=ó™Šþ8`±;'쨾 aÝ æVÔ¥˜wÙTë8nʶ˜RWP1· {X¿^‡100üIãüÆÆùüküiEÔ⤂ûœ9ÑuMÅÖ"kIc]tŽkkAòâÈàÇáñàÈÅÉ[¹Ñu%$º¸É´(RqŸÅ>¼^*sôZ^#"PÝúV°ñï"FZ"Ý”ˆÒe£TJÓH/ @ö.…ÿ¹K{»µKo§µK¾‹Ç†|jé’òÉÚ%Uñ“/«—”BYÿ^Ê›-’±LºµÑ ŽÖ«Mð÷qñakÆÙý§W¨Rsf9!Û>DŸ“âÌŠm›?®Oœ#fü ]ªª¢ZI±$'Ûö–,g @„·¢Â뮬èÏ2Z½Þ¡×Ù D\gÒãX¶ÚUÕõ8¶(‚KM‡@c¨ Wl:g‰ÅõÕP¥¯å±üƒ–h‰Øœš—jÏÐ Î3×ÇÏ-ÖŽ­XS.›3H­Z4kð2oXK£¢—=5O5Êœ78n©¶xn÷e²ò5}VäÎÒé‡Îbü˂ •n£’úSñÒ®xÉd?y,ËÍw ;‚‚tú2w¬¼íJ“!b(!u—iÔê Búʹƒt3/x)Q4áN^ҭͤ[ cè4:Þ@ï OÐ÷IÆFÛd:v‹¤³:vèêŸ*´_Wî qÚ1~åoš²=-ùß~‰Vòwª ¼WÚ&6RÇÕ1*Ñ 7‰Y;fk]Îiw¸¤^žøÛUxŒ~ÍrÚ&”ª=Nj ‘R‘œôÆJ.)^ o·ÎnÐÆètdâGŸõê1Ïe“dò–MÁð±¼¹E.k0ÊléózT0L&±ƒ›’æt¦¥:S&Ù•êJKs&³¡ÄÐKÉd”óË´ü¼©–Xn²)Èš± –{ÍAÑS…q;Á’•]Ð-'˲}œ0=:ÐìY¾Ìc 4MâØ Œ-<9%Í•’^ßP×”“g‰1›y¼^Λ-VKÞèõ-òƒÜî™]ÒKgº\™ÉËsò¬땼9o²Äô¯«KNIw%'‡5°nmŒÞ.N£½æÎQ—m×;¸ÌþöÏäÁGV݈Ì=ŽX‡Ã×3*"",,""*#+3#*"2,,’ø3³ ¨³;sX}¶C ¼œ9öôÓǘ,&Cžœ7l LmÉK ×yÙ¬#CZZ†É漺°¤ü–iS†æ§„é2˜òí}ú÷ï³Ý¯ ÔQ¢¨  ˆ8šÈ„ b•ZÒŠa™îÛìq±v‡#öJQ7šN—Ú­H¯Ó%Ç›ïïרØï~3%¶é*é$ÓRÓÑR´ŽŠ—RÿÒŸÀ§ÛõT]Øã·5èMñØÒ%£¨Õ€Ÿ~hzòæß[ÓSò‹òJùɈ ¥\V?eÇM}©&j@D¸«ÙºL-ó€½àð&ºä¬Ë–’ŠßK”yõð¨sY6¬ø­Ôøœìt:ð/¦`¹©ßp†9¹¯.1ͱ v:â’ùú½3¼ŸIŽÌ Óef„˜,NP:³KšCM¦¾…ùÎî pYLÆŒL]úÈ'ZG<1rô“-Ã΀¥öƾ4ôy oÿ˜Å;ö(­ÃVí¡œðje TÕM-+¿#<´ZØŠ>LeO¹zßEÔ¨ýKFÛ”{`\7EÃÌšÆÓBÕiwþðÓiŠ0všÀL›Å0 Š‚(ëðw‡&YË*ƒ¼…)zË]wY@—ÚÝTUnIúîpë² gZ[Ï\xjĈÞõÞÖ·ï¶ÃÖ#}‹j‰ÎÿýÒ\/Pš™K${-úqw'ÞœXÀ¼Ù³@òò÷ã—v‚×íÇõà݉_úc$ŸƒŠ¼ül’'‘»y³'7úøûwás;ñû÷û삌ˆk·~™äU =²Ñ7’ÛaEFZzÆja=Úß €ì"kùç!™œ 7úZùLßl]ez¨ÑêÒÚ{ ‡^Ã_¾á–Ëùý[oöR˹mÏù¶2ã²} ) ,äÔùo~å~’LXHж sqz„¹Ó­1)À] ÖûV7Â"±­w0sç4|¯Jä¯êEßÇ=jƒ{øêõ"ÓGÑ£LÝñÔØ‘ØŸLñ¢PuU½oÓ´­K À¿‚<`—ê*ôøT³ WgŒwÜOà¶ãFY*ÿé䔈2PÎ$¨è޼vÐ!“ÄoÀœßÔ‘ßØ Éö+ ‘øLÔ Ã"°¿LÁ§¿˜œìU÷•f ~›÷O‰Y:f?ôƒóãÛí!Á ji=ÞR>±ˆ„}ûK0~íýÈôÉP|}J²oÕC×w~ÂÆÜA &I ».Ñã»çK|æ1kmÃî§–“Ô’á¤é¦“Ø©xgmKx>4aï+ø­ÀÜ´1W¡ôŽ$|îêÎG:q‘ìá8 eÒ«Žc$mé¾ÒÛmU9Ú;·«m'[—….]É®…IS¦°}ð±¿o™Ûgèo­î¤»qöü-Ÿ‡Qx%x5÷æ½þn½kELÍÈß~ýúönY«Ø—”´Kð×_à'Šº˜½š+õsQ™p›¡¨Ú»÷àŸBE¥ÝÚËo˜KxD¬G"µOhG’®á%€ºªo‡˜›qtÙõmÐv?½Z;­VÝf¨‹ëþ)­úê—R³ÆAcg“žýþ÷öº@Aìu½&ü„$K+þ£iTôh—:¤=¤Ubºn¢6òc"œRDºtlÕ"e‘D.¶Äe)²ØmÖ9Cõï?0›¼Øc5öåê3ñÒ²àÂÓ+,ÅY¾’ܹ¹9ýÃBC)ñsgæçô %3ÎXí7Ç”Ö6olXo³9òJkØw¡Ýt`äò‘P4pÅSØ™ù>3rE[ÉÞ”›3?'Ó™”Ú7?kqNfZ<ÁôE¼ƒÿNÔ \ÉÊŸÇÙÙüöK›ÄŽåª…FþÛ%XüÁ¢ƒþ´¼è «DFÊ$;N´Ðý'Z%1ߥOðû8“µrÀ±äØâ=Š ú'倻 ÚŒ´†¢â%%Ž´h~Ý€•OÃ…Ìëp^Þל––œ’•¹ü7/wgPÜß´œÀ˜õ1[|`Hóâ‚X]Àc~øaÃcr½­{ñæ!ûzÄF•Ž\>êüÇ¥O-o:Oüo„D$$O+ì6-™Lÿ%·BɘOz;]êí„@M!7» ÃjéÄýO‘¢¥Õ˜Û ¾ ì. hNRÿ xQCÍáUþ´ ÿÁÑ0†Œúµ{fV …0!Ͻü7/ÿ&þbÚ]l…è@qw›ÕJýbâ×Ë×l€ÐC£a0~`ÔÍj² á‡-!"„ú)lÔ$•ðw¶6«šü§0IËÜ6úÚ¥mú_RAwxåà°é( H„tz—cØ¿ðÝ õÿºiõÞíhÙ¹Õ2抂…Šºƒ£ñýøþ…ÄÀ¯ƒáY¶ôÌ]ªÔˆ¬?çk§À@§ÓÙ?/Ÿ(x<…Öd¢ðšÍâ¾'ô©ÎŒÊŠŒŒŠÊY}&LìK‘€xTý…AÜLÄ¢H”jÐXI•ÝŠþÕ@–ήCÁm#÷¿ÉÄ*7l`"k¬änTtDdŒµÆe 'K°åYÝ“n·3­ ÷Þß¼¾è¿’š›Ùö!Ó_© ‹.‰‹- P)‰™„(²^¡Ú•ìœ×³dž3-($rÀï±AüîÆèyq‰=¢~CbI4õÿ9rƒ^‡Ï…`®ñÒ\ÅmõF°ñjÞÆOo{åÝwñj¸ƒ}¹ÍƒW3o-Vùx% \8/ž¾1p5ð,¿é¥m×ö¥·×mÕý—D3fÖ?¾;Üf}û¡zvRóÔš˜ª¬—²ªbj¦6ïÀßìœ5¸.±/SÐ7±nð,Pÿ }HS\Úv[n®m{Z\ÓôŽŸ†¬ëƒžl*-ºq£(´¼éÉA×»˜R‘þv‰ÍäŸZXÿ žÁ;’õÔã¿ÆÚg¾YÌR5èGÊM$å†!Ô!çùåV“èð˜:¦-lÏG©\›þЗ_âÓs§ì%…Wï¹ë(ýrÜ#"3Œš¼›”nÚ" xÔÊX{ÙZz*W+gÔN—ÞbUKn§Kí•3G˜<&›ï\¶ìÎÆþËÈ¿©…ÇÙ˜lßó¾¯ðOí’bÐ^eø‚Îb£iOÕ¤#UhÑÑEX@ˆœå;Ö6  wá¹ ŸéëQ}3AO‚îâþI‡âmm3ñ§³ñ׳|¾Ãþ™ß,ÐÏF,š~K)Ôñ6$G èÍÔ<y°¤.0½Dv“ƒU¨Ãoà|òÿ ØPñãA< ¸+W€[M!Z e³q7xj6>eì;¾ÝLßä7ß”^«o%rÏò#‘šŒÑ$[Œ]2UëRÛŒ mjBU*±OÙ=eÊ­¥‚­o† ø~%­ߣS}×.€uD{_8´îÒëëã× Å#ð{@¾Åõ•f©ýÝ~©†×c¤”L§t‚COú¼TBÔ}ë nÀáÒìóC&ßèS_6TþqUlšò$ŸÊR‰Æì%ÛÎâW·g(mÚ[e H& äÆJ7€¹Hßëu’Ñí\ðÝF&Qh¢×ZD^­#1f°ÔtÄ2›?p©ºp 9ãߘoóÙm¾G.ðHœ4a„Ïñ³øÅ/%äA&yï †ÖÖ†Ak-Ó§›˜ÀÖ†@KC¿Ìe½ñöªeÿsßG‘,Ú5³3³9ÍÌÎæœ”v%­´Š–lÙ–œsÂÙHgœIs`cÀ`Ãó6îóÀîÎg ïøðÀçË<px™lÃeÌ6¿fvWZËá~þwz§ª»««ºª{¶Ót·]|qÛ S`ñt„ˆ®($÷Cm;:µ¯wºX0Lhv(¶†äö@QpÝ5¢žºè´lTŸ†éþù,áVVHN·ž–+úÂi(;_ËXeÖJ™ã-`Ò^¶î&³ ¢¨8= “°6«ým¨^I­±ÍþO؉Ûj=B?£'w?†&h(üW&|¸=äg¼¡ö#…÷Ð;à ·ýâÌ 0àó/Zôö3. ø=V«Ë@Üç6ÛÜþ}ýÐÜBÙÜ¥=ð;ÈD²@=¬æìš¨Ô•R3ÿ[Š3ñì*b7L—Ÿ ÃÿkE‹=›&ý%=Yºë–ÿo(€lÅ.ÜÅ3¨ÊïÌÈ’znzÅNܬ:×.[±Q7óéÈe‹G÷.X¬àÉàŽÜ@ÿÐxÞÕüÒþÙpÕ‚1]ófêYÄ<ëØvÚæÜ+é߃EôÁï÷Å_Ëx£X.¸Ž_ÃIdYM®C±RjSàÏ>ÞRÇp4XóÎBEhcÅ8…zº`ñ¨f%Q¦(wâJ£A€ä âB´"Z’ýñ?î¯fƒ¥>Çã^Yqš- ±@uÌ0Ùü„Õ•`YÁbvM™ÕÐèñÛD³¥)7β8͆ ËE«Ö΀›ÞŸºÜQ¸ðõ/-²ä¶ËŽ3mŽZ‡$É^·ƒPïTüf9¨Sl5’âýØLpÝy½E_¥„3“Íê‘ã Af)–ó úŒ(Šc¢’äö“=pƒÑtýñÑA% X\Þµ3ÂUz…ÜvGHqZìvžWްËég¾Þ›#";­'‚V'z[ êDø±ºo™ ¨Ë?åfg¢ƒÍQàTË$ŠèÙšù€Êdš2 ïS2Ø>FÊhDzÇ »d!“´;ìx%Ë ÌÚÚµÓߤÿ´zß4‡ßïJG¦D¢~oP Ëæ¬ð%Ýî¤ïÂùÍÍ‘´ËïzýQŒ¢‚ŽiûVÃú7§¯­ýâ÷g$ ­kHt†ð¦BN!Xóe÷ܽµ]V«>é‰uze»ÅbUäQ±¨G Þ´=í5D-vÙ‹‹½FÉŠÕ¢Á1ORoµvÕîÛ½í,éªEžüW~n*™Z*ò9!¢Ž³DŠSƒ¥I+êMÕŸ¶³ð5£ùÄ¢6@ý#\1$S\ªêš=$Í«<€Q"„Ó°Óâa‘¹Ç±|êûôÒˆÁì´„¼ãÆÅÂ-F³hy ógMq™-Ë‚úšùËb5þx½Åìt6a Ñ`ça–T/šú úO·9± ûWÍ%%Æ´¹}a¤ÓÞd7Ã7 ô‹©‹ª oÿ‚ã —ÁÀse€ù…ÞbxcŸ1þªäÒd4åuÚíöP<šMœ†ÎÄMógZ!óOjNXL3çß”è4¸•lª:¶âÓÌM4GëÒñ˜O´‡ ôU§íˆNWĽ†1§¡ºÓ¢®ôV»+©Óa·b€†É. cZOC…Ó¢.´t¼Þjt[•ˆßçíf³Õ%†ý‘p‹­½ÛA?ñ¯Ù9{Gƒ6«1j¶‹Ÿ?ñ‡E—Õ\Ä"ŠÅc”fÁ¥s²¿ª©T“ hWxÊÇbî$YF.%׃ã%áʱéØÐPŠœdš›œCÕØ)KL±¥™Á€âêN *Õår/¯ SŠƒàH8T»¡r ½V#‘5Ë¢ä^¾+/¿p`Ù’K®Ü¼kçVž\{Ѧu—®Ù}ó]öì¼xÃÅë.]wÓnú½]Õcâa¿nÍ·™ÎÛãQC¾:4¦ÛîtE›SNûDwºÑ£°u¼“1оXXdYÜÓå²Ë– Ñolw‚ÐÂ}þF§hÊåú>¹a÷û_[ÚT„w_ÿ\×k±çx7Ø/_×ÙÑѺi=ýøþ;èÉË7´wt8{æÔg'‡ì¶Ÿ€[õRkû˜‘Õ¡­/ »|AC]oÚßbŽVhÅ¿5ÙVi ø×…]sC]s>ïØV4Ö…ýK?G… +möÃŒ¶„ËÇ/±‚ÑÆñ°Ëð`TE) Û j*+Ço×ñ®p|Acä2“YVšCÁfE6›48RaF.šêºÏú—­¨´àÀ0k!RÙ’ÈfRËâ’:BÄæ–TNÉ'6§ää˜æÔV°¤5.Ó\Ì)ÆSB¬å¶øÈ½{!Ø|hËÅ·Í8°èÐ÷Ö‡Ãõ½ôow,:00ç¶‹·j¦¿Þ»w`$ó³4÷Ëï[Œ ¶½®vÌ‹Xï1„öÁRšÌßÿ%áÈò_¯û»¶)P”T©" ÿC ÍbrèY63~Ñkú† û8rjôÚXdKŸ…Çé –ÞK{uÓ^^(Q t&<Æ>çeWA-}Í WEá }‡ÙMßv8Å‹pܧN/…ƒ-\ QtÞÕNý†èW¼ë¶r'°çñ .ìú#!ÐTjiå”Òßdù)¡.zÌWþßU¼"ÃJêö:*¤>)þ³ª¿Pl«i—Ö[©\~b4úr ! C˶Š_ a5b‰FŒâá­H‚±K²©ÎhÇ®¦Jâ5‹M@9©ÁµhIL²Ï<•PMÖ:†Ñy«‚RTñÅíÎzÇ„‰I›Õ)pFÎÌ„|U- Á€Ñ€[x—3$ê}Næz6Óq`Œq®¼ÇP㨛䕸tqÉ Kë$Nçròlöâ&¡:(šŽÕ7¤-úL#È6È+î¼õoÍbgFlþ´Õæv4tŒQÐÀ!0[ÖÀ™™uÄÚ[:pM:ÆÌñk¾ncg&m&»^gÖ›63µ4¡En »mA¬3î÷…9³wý³K²}dmȾ³7y> vx—Íô5Áà¸âóâ5[=7Š»o<ìŒ4™—œvAs¸mÌú`þÅÍË Sëno´™M¼)ÔêÍÑ彋ÌzQ§Ž#4ÍJ²!—Q0»’a§9Û\ï® 3 #}ޤƒçì†Ü›QQ&ÚLFÎÃr–Ѻè´×Ó½ßi¯ÏuêÇ´7ÏŸ©â£Ä$] ÕÔ5´[Ì»Á +n£SÊŽj58F§Ãį$zRCÁõ¸kOÚÈ$²l!W“ÕãüE—fS(üPƒ€À‰ÚÒ­ü`»µ´£ œ¶‡]3ŸPC¡HfÕ3Ñ,xo)ÿ ª÷Fº\xx-LΩ0—o±A±aˆ('´$î“lvvCvM} ¹oB¤fâú·kxëX¿gÑ–‘££ñ¦®ýu‰ å»?ªMçù_]Ðõ·µ_> ê9˜w„æ}œJÙÚ4{¨ÉóùÅ3»çÙ⳯érG¥‹'tOL/Å|7Ö4z&6fî©ïúò_gZ˜¿¡>T£Ðû p’M…Ba;ýž•³ë;ú/³3ì;¿8ºõ‚¬×Òt¨±Õxóú@pûÓ?½}MâƒÄú€ . Z?h^?jDꂸ–Ùï¾îÁYLµÅå´§ ÕWSg1™ùÄ’‹Vo›v_µzÒÒWvn=±på¿OôŽÌ%6ÓÚ¼¯?'9Lp?ÄQc‘xH˜¤I#ifé,ž,ð1IPÖ/ƒn°1bÔ‹í½ š på Ø¯+®ùT¯Ác…5¸1Å%ôf¦C æóã]Ÿè£÷˯ï·ìÝoÙæª#¸Ûéücaº™Â‹ðÍ<_8µ‘á¶Â:KÈn1Z/Z­‡Õ`µë8©îj¦Ëdl©÷9ý§Ê¸|‹ßÝÁ˜˜¿ß±×rÇ^¸ÒYØ^Xu„Ù ©ðÇV£™éúòp>bs0üÆÂ<ó¡EætLزX±Ò[í&‹=ø'káE¦õÒ ûø=º.â ’ãnuJžç %Á…—¬R®9%È Û¢é§ ¢ê#BÐÉ‘x“ú×£<Úúç⋊ ÅàÎøªœËɯ9áÔC‹/þÖ·Ø̼ÂÃ_>þ-Ï<í‰xvBcasðË5‚^pJNü-/Oc¾ˆy˜wܲAà·E¤Ö[[¥È6^0Lüù† ?ß«ŽÓGé£Ç¡Ö£ø<ô¥ îû:Ó16·­¶XøÚ€Ç§xÎSÔÁ'µ~5*ªÓvG†TˆI ),¤@¯-š*‚`é¸)̳z•æ3 B^Ä% ^£ ÿÄ5>Ÿ*ŒùËõ{-û÷Z>æ`¶2û¶;­L„j)­ðE5íîX­"Ï9¬F‹=ädÌÐf4^ä Óáö·äÇ)Ÿê9£¿à¶ì½Ã²÷õc``¶)¬*lÿ ‹ÙØzª/62<ªµÖ Ýb²[±$Y±|aŠ'cù6÷`ñ# ™[x«nZ¾¬U¹päDÊ GʳœXì2ÄqA¯S› çÜ`{ph‰KèpQï®F½ Fa.*<ÏY– Åj2š­z“A§•cFnÇ«7ý~EÇ=ÁØà;ô®Êf^„:¾ðÙœŽc–®£ŸÌôëízžç†@8$BÜ?“~²úv]°îw¹éÖUnŹŸYG€ì‚Ç0“Uhrj&PDkÁL¨Y¶H¦Üfßg’(åÇøÆF^ø#an~)࿯£Úá´ÚðS™—ý”aé{ªŒ‘€ÃfÕãK ,ÅÊ0È‘cÜ\ΨíõY|\±ô$!ê¸Q¹e/§¤´Ãbω®ëîÝF+}ã,ÑLSo ³gäf ^×8³ÁZEë _ÆyÛ®ý¸ªÞMW™>ɽ̙IpˆÅàæ¾D¨à‘«`²|éÕ^“R¸jKõ-JÛ¸Þ¥õ¬Wår÷·&èY—ÓW¾3ÉkÞýäΆp-`ÉmÜܨÒ:Xí°(ìàû{h+mÙß×~e>¤-»Ut·Šî¦­øƒb~‹tó·OKÁ¤û'¸„~ŸÞŒßï#t3´À%ømAÚa>]J¿ôŸ}õ)w÷é)fS)g‘g]êÝCÍ7ž<¶Å)2ƒMÄr?ﺿ±ŒfóÚ]¯¼4ýs0¬syÍ^ió×}'<)®Ë™8ß~{eÚàí6Å`qúÃÆü¼=?rsoÙíÞœÏ!û”_ÓÏŽÌŠÇe–‚v§÷À+ïÑ]ùjÿˆi!k5t¢wÇŒV¯7à0ÛƒßqäGª½¾$„×s’匠+g¤r ª<\ÅhíÊ™|Ë€ž1uËYŸ¤÷ÃV«ý³n»Í”ŒÕŽõÖ¸Oñ^pÅïç9dÆé°1’Óˆ §ûÚ—®ìw)v—·LŠ£Æ²zßþnæ> ºÚ3[-rÚþ&ýÞpº¼Û}n§Éç_ò<=u½7²˜ƒ1‡¯Žµ¸Ýrè__û70ð{Â)«§ÑûÒQÐÿÒLX²3=†{][fÓV¶ ~C`„š¢Îéòtá¤ÁwiOáaÚßeæW‘ã*|œ=‡èìrÄiåð|áŠbxž@wÜtQ&Zx‡n(¼ÃDáVöÉ2ÄÄ3HÃRåšc-²1¿ùôÓ̦ž“…=̦“ÌÅŸBNª2¬Ì"ó ØC7Á-•¬™ì îX8¸ Ëñe_…[醓tÜzR7öä—‡ØÙ'áV€ã¯Áø6âS¥UTN)G,¥ÞA%—‘ÑÜ·¾F÷ìÛ·jaÓ¾ãoÑ× ö-¨Õ=²î),ÙǘöÁ&¨Ý÷…Ú“{«°„¹ÿ-ÂB—z|ÂÙí—:—ý¬ôú>¸a¸ƒ›¾_ø›Š2úûTä¸ ×­UaºCe[§~«Œ=ÇYƒ»Â| )¯ûSO‹(.ŒìWJE„âzÂ@y©`s‹#U\2ZS~Îç”HKi˜ƒµmxj|Om¼Ñã‰WûÔª-“úF/™á›®ÛšÏÏX2zü„-«ž;¦Z‹PÛÓ{x áÔŽR澉ã{–NõÍÒmÅÓWööM¼ràp_OMI5=㟸rb_ïBŒpjFXÚ3nÒ–aˆž<ûÕ︿sDí:ˆ„ö’ kz#¬1ADd# B¢ „œº]šãb ¤±%ÆÎžï=:Js·?ôLz衇Öüü±µkÖ¬ý½òçkè4û#ÝJ¯ÂïVØÛaûý•ºw2„ ôìÀ³ÏÒ«žëBÏ=;ž%@fá ÒRV¿ÜÓ.=tQÅ¥Hé¡«*¸TzòŽ€ò™w¥þyŽMOªÍ·nnÍ×N ½²÷²zü \Ö{åÑð¹Xÿ¹c>{…ÐР\ñìPýúƒ6úµ];`åôU¦Ã™j«h“Ã'Å*—c I•Éi©c¬s ˜©©³ŒÝøc”ᙺáÓáp†Â3ê23ÂQ›}ÍhwÈv©:‰F#átza:VÁXB±È$e ¶è™¤ÉÈh¤%F¬–ì2óó&ûú¹%zý<ÉþC‰™Xp–•ô•ó<굂ŒUõ<êÍ@ +îyÔ‹9Šžç6qØtmÜjwÓéêd¸=³©>OVW'^½T•³Ä0)Ä,H­’G“#m4™Œ’èSÉTæ¥pŸ›ÌNÌ'æÅ«%RÄBŠ']LóRJ£Q”0è÷”ÓpúPz·Ïé2”5êÍ‹‹ÊLòºK¥¿tZìVÌì JfU¥¢ŠyË*•<^U UZ!”f ‹«âBÎÁe(«4à̓ª´ÄyH¥øAUÌ+¢e‹hÖŒíõ{E%êB sññ¶„û³ÚÔµ~£³Yrj mŠŠ‘›µPνs÷àœÄ]wݼ{Í¥ëݹ›ù¸8opÿÝô¤6Ô¹îrzòîûAZ¿©µƒ0é·R^¿]ª™¥‡_±âEÎÖ/_8wÂø‰}“{,X±bÁ‚ÞÉ}ÇO˜»î>§\ÉÄ„qO}mœǥ’ÉÔ¸ÉO¬øÚSã&$’ÿ@Þ,nzŽ#<ÊøSz¤pÆÑ#ŒÆ1îóÎó0¤÷ ØÁ}N$E9÷\<~Y1¢D”ôîøB½âv+õõmm•ÈÖ癋¨ùyj¢¦ç©™¹ˆý­Û[רÚÑ3ª£µ±Îë.b£z4ŒI–ts½]Ó [ai¨´ö>ý!+žÇB{Î#ìùmïü _V¦Z|Z›WSxE†Ä«Pâü¥ÐüÅ’/ð‚æ¥·Ð,l¾6k?4{Ë05êÆÓ_¦¿<|’‡áí£'h8è xÌžÊvVA”œ ¤œÆóA™} áØ^(Hrë@r¯­ÉalŸ¿˜î‚ì¥Õ~è.vüeøÅ‹ Îó½ûM¼Þ„þ7 ?Èž!ѱ7! hN@ °©DD0âTü 6îƒÍïÕ·¾Ï,ÅÌÃ-Ãó¿ü=1±p ž™ø½cÌ×iß1væ5«‘¸ùÜwŠ ¸1ñTRÈAT™š_À+Âï¤]>@Ÿ¤«x”v1u>p'ÜUHí¤SañNXKvÂb:•ݨ֣¦?Ò_4iÉÙD£°Š|EÈW_Á]ä+ê‰%|}éøu@´›@€cë@dŽÂHØw]­ýàéÑ«ïƒÛéE÷é¼N«?¢}ˆî#Z o|òG‚Œ¸ôc‚œš”\KŠC¥a‡ ®DO1¿Ù¼ùÓÍ›é~šÙ ¯ž„W7ÓŒös=Øg~òÊ+?ù X^¡Ÿ3¾ÂïÀú ý#ýü°~Ïxé_©è†"r¢Ú2ú2'ˆÈìkðLí ½}p´p ³‰ŽE`ŽöѱÌ&æˆêÿ,lD@õ#3 ·hÇH|ÈMâNjhg À„®Û;ð`¦·púiíÔ©´ú G™^z7¼Æ,<´všê;€{÷ѻ൩Óâ.b(÷ä*ŠV±£™B6Ë%84+²œ:"²¢CL°&àÄ:`9ÝŸéÛ—3cè›ôWp` þ\~9]±Ë ÏABtš׽°ã›p=½â›të½÷ҭߤWÀõß„ºë¶`?•&ÜEßÞÌŒÅtßcyð¾ Â{ïà `ù7ØC>©‡¨O7U,Š>“Uß(üdž …ÿ€çé(&‹ºxîµ—ûBáYfì j?÷…roöª¿ž¤'àOÔ :eãF:ïfÄOÀ“ÜIj'²Iõu"ügú)<¹qR|:,Ùa*cç„+nT+Úôúƒéõ7ÒÛ`Íp&©zªð:x£V$§“+&XL²”(&{ Øú ÿÌ¡Oлûèg—\B?ë£wŸ aÎ èï²x¤vq9ì˜[ »äâ"}E™Î¢bQaA„Âá8'$@QPßã`S"¤€K%°¸'ZԺŶ°ÁÍׄöÙôe¸+CûqP®ögèțß{dýA5˜› ítyб¡?ûéñÙÐöÈz‚øX•`…–Æ¡õ‡àQú†Ò•¸S{Ž®ÈÀôeLÓÚ|hý#Іi#Šþ/¨) jêγNh‹æ†ožYÑâ¤OqÒ¯*ÂÊóþ¼íÌ H•ûCÿžH®îX®bêtm T„•* Ȧ&–G&ì!·qã¨ÔèÎ&—Ü”8ëÜ®If“qû€YÇ™W\k2š'u͵rR°Ov5uŽN^gõyìåÉ}@±®}ýŽ3§3l7š†Ñ]còôìmæ¯_Þ;¥8T2¥÷ŠmfÞªðc®ŸŠ î¦X­ËÝ×oD óÜoìs»jcMn!–¿­[ï1𶫯¨ ¾ÚƲYß½m¹ä’D¡ý4òk;x‰%Ì ¥<äÛ„œûõÆÿ¯­¸~ø^ðÿÚ¶Ó†Ï9Å$Y…'ý¿µ9ûÑæN´ù*µ… Få ÑÁu¨šZUs­­[+ å•Rhb4o‹uhkV3kѪšQKk³‹“ñðí{§G²ÙМ}ópš<Õë¤)ûkúZëWÌÏxxF~äêúì´†oL‘ž09˜uÏ´X}}9jíyêȹå¸NQãæ&eoŸ"=iïg^2J~ŸSX4®vií¸0AÿKËþ…?hš:µé•9·Œjñûÿeål>w U;n±QòùK‘'ùùd®2v˵ØÁÿ²bßt ‰±‰nPWV²Ÿ<¡žÚ ¥îCiq’X\ç‘;KA/—òØ`1^Ä‹t¹¢ ¢g† %άxjà™ ‘œ+åþ¦¬FM¸†`5µ2/v¾¿5l˜ÕL´>ìl^N8íó{¢ÊkÖJ®Öž™Ùî Î9¨óYóPésœ}= Ó{Z]Òºjɹ°w¾Ý™/ov:sË#I•r¾dÆ Ñ*)Ѳ•„kkÊ„ÉÈò½c̨`À`lîì8‹Üe4‡jz=qxk\ÜÓ[2»d 'tt6 à(ö?û'4Nã­©ñŽ™Þ8>m ³nW›j«ä“õ+n¬2J³¶.\ºZ5åÊ¥ ·Î’Œ©›Vf“|x\m[ªv|SذÉBºÜ„Úd[ݸðpº¦Õ•tÁ *ݸf¤ }Ý©ƒh4å–#J\ÖyÒí³'L˜Ýžöè丑ݩht„!ëá{¹Çˆ‡\~žÇiêlo@Ÿù:ô™oCŸåÕè ¬œûÀ­#ºrM9¼ºF ›»­Íù–úºªÚt]¢5»ÀW©K×VÕÕ7^‘mMÔ8.QFêÒ™T2®ÌͶÇJQ:º²Uz_V£øt&¯¿>¿e|ï–|½ß«Á½ãUXl¶+ŠT“ZÙÕÞŸIzdN‰fú;F¬LÕH²ìI6LI$ç–àL{×E±¨¬“}q.Å©µö ÂI•¶›ðƒÚ]H.#»ÉCäßÈ„$þÇÔ4¤õÿ0j„–ü°­;Ô¿QóIn_õä•O¹’ …æ@9½dAc#">l¶ T!Øù\ ­tEcÑ`‘zßUëhúºÎ†ö\¢KðivÜZ4†ðÌ;ùÖzÁjñø<áà¸`ï¬[ÐŽ…{²AßJ·0À­{ƒÍVoÖ ±PÑbˆ¬cZc1÷ ­ß½ÒÌö„ÿB¥þÓ Õbƒ÷¢<§ÍÇX¡4°ŽiUʪI¯’¨±Ð[]K×]ö”‘!SÅ,–ÒäÐ/¶²gKy®VY^Ð$UxAÃ1]­¬—Ô rhÑÊ:*i(1üªõ$TöL!Wí$ÔóA*ª ›{*f`Xä{özŒì†=¦g~ë0½F=¯Óñz£Ûlpü–á—üM.Ií–\M~)ÎzþòËÁ!Ï Óð;žÙ|…`w•ˆÍn³ÀúA"ê°‡#骺šÙñŠi»äÌšºªtÄ/9©–ÉјÍnw†ýsa‡}ÆĮ̂ŒžZW[ewDcÓJ‘£“áɾÇÄ «Åd9€ÁÀ[Ÿ¶šŽD¬² FcuS Ü([EøÈf¼è"‹Kõ“eÕÓeá.•'rFE%6Tj“žc2a¼`Ðq¢Ùd±*Þ´Ç!~M´Êá@SµÑ²lntŠ#FX\ªŸ,«ž.‹áÇ&ëÒ%œÑ ŒHípa’pùYwÕ`]²1° í5›ðÎø\r«QvÉ^O§Ç‹wc¿"‡#l—Ëeµ ’˜ôÀp¹üŒ?èc6êŒ×%µ%—ËëîôzñnpK‘ˆË%y½n/Þ™.E¶Z1ªÙl¶ªÆÓC41{NuU8b·G⹡ÉÓ\2„>þ:|¦ÎO†£±D:ÓO‡ƒI´]mUÕ¨áÑSA»}TUÕ¼Yñ¨y)Ɔ©ßdyÚ‚EIÕ‰Ól¶XÜÞXT¨” ||ÊIÖ³YÓ§¬ya¹–eT§Ùºd)gÒkê4 ­Ýeà{Œ¼Þc±˜ÍÎ’Õ-O[ÏD¬˜TÇå$Íè&4ºŒi¹4?m.:'NàLŠJjÐlŽ–gß=Ƕ$4®V½Éƒ‘ðc0*½Z~œ9,ù&–ü\@L”«Ãç`u°¹JÄf«Ñ NxHŽL!‹ ùÛY§XcÅsO/(lqäÒÚÀ%Çþ󕹿íÓœ7Ì39߆EmÆgžc.â®»éÕÒ«`ûƒpÍÝwÃ5ÂvzÕƒôjèr§3«:;VeÒn—wtªðÎP4ߺoÖ”}m¡? °ö|“z-Ñ{*™ÝƒÌˆ0ØÑ“o»È¿'É äuxèÿ»Þˆ ¥w'ÄD¹1Ä7ØšÀey-0¬kRýÿ¨WrÆøqØ6Ø!¡qzí 2Ž^[Ñ)ŸÃ¶Óú(ðÌÿ£îÉŸi_‘3¼ðv©B\† ÷ö`åÔH€&„£8Ä8ŠHwhÎÉŠ£xoQGKñ®)A)ÞåXsÊQ¼#…CÀ{‘’ùõÉ;w¾ÔùÒfõ¾ygç‹'7#¦ÞÏÀ|´kêɛVmºó½›Àµ~Õäßüfòªõô£]ï݉ž“WmØuÓ`8ý°®›ÊáDÀz@x7æ!@"$AªË9êî3¬@ æà\ªŽmÁ› +êJ-æ(=ºýpÐb`è¤8}[þŠÃßômú×N0 Gœþˆ:¤ºÂ_EàÀA„þrèÐæCÜëôG¸ãÃU~xÿªô[ÐCî˜~`Õþ‡Fo˜öHêåþ—ûñZÕá…ýèVöë?†r-Ññõ%K5Ÿp”5o²æÑsH󜣬ù:(Û=ñŽ9¹yà$˜Ïë\€fÈtràæ :§G@ ¤=¾ sžËDÿŠ~ ;/8¸×WMì&ú“;A~ iUóôÇví¿ó¦Ç¦#x?ÈwÐW^µc@ó~úÑýÍ«rÓ¿éÀ~ôEð~úÑGXdú/r'µ—M+E‚rÖ3™Ô1>‡öÊ‚¢¥¼C¥ÜLkIiž™ag0)ŽòÃàÞŠ÷7ŸÏ—É6ç3¢ã³Ïb&ߜ͠—¿.ÛœkÈú¼ºÏdôÌ/\³0qäÏt^_VŽ‘TÂÕY•Î.5äò óèq:•šð ·^M<§[›Ïy˜l7Ýl3ù7e³x³™¼K¾™~ ˜|Güñ™Pn–]˜˜·£L}óMHQ”"©ÏW¢¥h´u~Ÿ/H dš`&÷3m‡c›¶—U†Œ% µ½cJÇ ¨KżRxX1ÿ\a Ö5W‚­øû¨°—‚+ŒÇžœ·uÞ¼­ð/ôªÙ°½`¡Wφk˜{ŠØw4ì™6›¢†µ)›Ë€N±µéuo§;1&0clpT¸¡!<*8vF`L ŸÕ›­U™Î+d`Çlº•™×̦۠Žn ;P†mÈ2g¥¤ÒÙùHÿ]BžUùê«<3ÉÊJå+//`β€ó,~¤¬ÙŠ€øYüt;ŽîØq´ð5¨šMÆÜ¯Ý FíÆNÔnzQNeÖeR²xn àÙÐÐK§c- e@7ù /v¢ÊkG¡µ˜ü$íÛèÏfCUa±vcÇŸAuê©3¼˜ þ;d"†ò":b&níèÛn2‘Ì%K´ µ³~Í ­Ê7å;€“9µ‚ö*¤@-žÅÞA­¹!Ô²­Í- yUÙ`c¬ú²{Éj &5Äœ¨Öˆ¥~ÚH—Â}ÆdÐÿ÷?µ¦p]ª"\uÀ'Ðïô#ø«ªO½ŽŒï£±t(P ÉûèwŸnyp†_ ´Æ ¾Â¾¸V wy[«.r c>®úÓéN¼ûƒÌä@u*€d½Hæ?u§†Ó‹)ú1E?²îEÖ>dýb0¾—Rã,Uâep¯1(Ì֪ǗýªÜK—¡Aå®!¤4èÏB©NkOHT¬.N@åÛ¹ VLìˆÀµ7[€«XqÌAùŤV€Õ`v~M6ÆM¿Ì45ö„‚MFÝïzÌ“¢™L|‚ iØågš‚¡žš£“~0…‹eç¨$ô§±HÂøaM'„ ñL&:ÉìwéQ%‰÷02ø‘$Ã}žMB4 7 9Ý’5¨©é¢™y™8?™þÎÍ £ƒ¡fƒ®£Ñ0!–Ek~Ž©T’I^po2G’¨n (NƒF"E @/‡Ÿ­HC²WÿãÞ'#8J [s”„÷#$då#.ôN¢Ë£ëE7Ý t—¡Û‰nº‡ÐFw Ýè,‹Fqäwü£"b’_ CÄG<T¡kC7Ý辆î t7¢ÛîtÏ û>º_ ³,:Bo‘£„%vM8Ö~”(ĨÁŠý±£³¾‚.[Ä={J°a5<úJ}ƒh'‘0q”ÎÌ>¦"Îxq{WSÆàè3p øñ{Ëazä±Çè‘ÃÌ[}‰vÿ6~׃Ÿ`ãi7}ÑÃ|H«¾Tþ§ ï‰'è3_Ngàyè>›Ž1Ì1zëqú…ª¾ò«»ø'¹×P|iQsÁ æÂHˆQB‹šERÊÁ@€ öúu©‹¢¾‘‘sD¸S_¼êà^ø¢v&—ùâÕ™¨2"„’obÄ22•™†qƒŒŒ%FÆ£„úÞGsÄ¡2â®ù¢y!£A¦§eÉPä¤ä¤/qÒ#'S‘“~_âÄ—8 Žˆ¢f¨”‰ åü\ûÅ„Óòc®8C‰‹aˆ‹n‹¾ÄE_âÂE-C¹¹–;<¤¶ Hvð«SüµÜ–Ž\°¡Ãt±øØ²Xd Öƒ0fÒÇà]˜3è㹈£vg>Ä0”b>„ÙôBÑ¢?–£û-^BÔ…ß:ÈKSÌ¿RI§+E:}t0ŒÇ°Ç+Â/Ì+‡‘ÇÙ4£®^â‰ó®å¥f³ªŠÉ Š!âslŽñӽ߼fÆlÖA7U¶lFø÷}ê( ØKiaJGƒŽS+OÄÁÆÄØfX§&…½Š½ôèþê+Uö îu{ÒœGí æ< Dè,Øû4ÿÍ¿Eõ' t}ÕÃ×ñ?ÕN3&§¢”¦&NkqDÔצã¹Hs‹b޳¬çW•ʼòâ¢È.Îøˆ¢Ýaí’ݽ¤k|]Kýñ;Ðý r»aÖÍǧuž6å‰1}u™†lrL<ü]xÅd–¥ÖP°U’Í&ƒÞnOeª­NÞà‹Ôµ^3–|zä»ïoübË©7Wgdo*áñfr›FNh© ¸¡‰™S8ˆy»ç«·9v.d’­È <´NÞyÎ}RØù÷]Q]›LÝðäµ;¿!•L4,Ú¶µ©¹¶®¦¦®¶¹iäN¦­5ÉeM[¶ÿä'ÛW\Ù´,—Ó_öºÜÕ™Õ#:WgªÝ. î¡Â(×”kÊ!êÔ@bØØò°fs·Ð¿j²Õa“?™H§ÉlfM]Cå¦9rVy™Ç^ºævMJI’‰¥ùüÒDA9޹Ø1ã~úò¹D²Ž¹µ4ŠNÿPpvþtÉ‹--ÍcG5&ßÜÖÖœƒàØæ––é’C'>ßøäÄ]ù¦ ßhnºqÊ”›š~°)¿kâÓë?'ó¾¢ÜkÜÚ‹T<ËÛfþt¬Qqr¯}úù¦''“ôç›n˜2冦¼¿˜ä„§7|Î|¸‹.}1ßÚÔ;rʤž1ù¦öÖ|~Lη¾H†l#Õø¾ù=Ä‚…Dpª8â‰Jíñ☔,EÔ¶‹=¢¾Ä™,Ç­DÓšMƒ-#ÒUn{RŠÊþ³1àÎ]bÍ„õrfñœ­uô‡û¿v‰=—ï¦/ÿèè1àVLŸl_þû±fv§«½q)ì±^¼è²U«ª§LjÈÙ;çLª®“«åPë¨jé–%›GéVÖ×vWEœ6_{´íæ SGOïZüËËÚ'÷Wï˜×þ),ßýÏ£ëæ¦äšÉ€"ùë</Y81Ñ63îê™;e„#u;ãr«?ÝiÞ²¸ÕsÌçEÝb Ó‹[® Ú‚ßk­z©yÄ|¡bÍÚ«W%KåŠjlDWʾˆQ‹ÊPU)ððnw]oj× ô¦ËêóɄޙ¸tÀ3º&­ÿ§‹¤x*¸|r·-1îBó-=ºU5­¡™µ?os/¿ÐlšÖó×e»¬ñÜÊ@{k“ŽFèØž¿4wÚ [_M°aâ¡q{¦u2N§Óêè]ˆ%g¥<¢T?­fT*@¿q 3ètø:§?va/ø'ú[.˧Hs9üÅmÌ -Ïq٣ܿkgï˜ÿFÛwÀEq­}Ï™¶»€À²K¤, ¬‚€( ¡EJD@L@±D1– ‚L½ÒÔµbÁÄKšÉÍ‹7¦ZbM_Ìõóš›XÙ9~Ï™YÖ%ˆñúû^õ/ÏÌ<󜧜Ιsâc^®ÙÓqdvÉ”ÝCáOvúŒéóñOIqõµ{v}§csãªÄ¤À¸h|o“þíƒúM+—ÅÇ™%å I…š->§!;Á¾ ÀѺùÓg¤gù»§”Ì>Ò±§æå˜øøÄU«·wàŸã◭ܤ?ø¶~â£ã“W5nîxçè®=µõqI¢]ñol¯¤¬Hôàd^G4ζ­ª^Y‹»Zš›Zyåƒñ˜Ÿÿ…>þî[ôÑ5³-5Ò„º& Nø¥]QFkSs îª]YÍ+¯áÈo¿Ã£ÿõ3úú ûCÓyšNÞ$Q ö"¾„i„‘¦à´úx„.mø40¢‹è>°y"Œi#›Ä¨Åjn‰¹@n ŽÊè&ô‘0ÅL`ð­3Ao¦ è&¡¬À‰À8¶À±X-jx­†‹€oйÀE¢†¿þYCú#¬b“QFQœ£ )‘£ŒÉ¿>_AÎ3¯7_Ÿ‰œ¹O ¬eXÅÜX‘.)á?`|eæuöyé^¡!ýgñkœ x½@,œdKNý„ ¿~ ưpw%sZn~@ÆT¾ÛQÖÕ8«ãîéø59“çÌÍÏYóîÚ¦Ëÿn-o\Ð|ãòúÆÜ5w67 viÐßY“ il‡4Òi‡ SJDðƒ”üà›?wÎäœ5ñڬƨ¨†2Hí]«opܰä6®¿|£yAcyë¿/7‘åÞ0ÞiÇ;N” ô­)FìâX‹ý_•©ÿ‹`Ð;,`í&NáìÕZ¾‰ôú{GdÐsŠ.½°ß¿vžþ•tõ{;ÿÐõ çÀ6?¡ºOÒ¾bÒÞÆ¤½Å¤Õ$ÍE#i÷^™ø»ué—ɬ ŽñÒD£þÊW¢ƒþ±3jÝÔ5cÒ‹£5ø^½ [öZ?å®&–Ž^ÖêîõêhŸô±C'ŽõümüІò=}4 £6Þš5(G†p@ÜHC8 †")€<@)`1 °Ð8 88 C¸¤›`´=@´])Ù´†Ò‰´h'ÊM¤€¶Ú â8Bù‡¨?ûÉûONòCöAšn{7d<}38¬¿ÏðÒ¶GÕÍZ–Aë„ûùãwoõÔœ‰S]÷:©÷¶5wY WÌ£ÆS¿½M…Ào$ F8ür ´õ%Ø%Ñcjê@òÀ@ °PØèœœlσD×KâlŠ+å$rÏ*«-€Ý€7Τ٠nhâ£ÿ=Dÿ;›ü^iG ý!Bû´ÿCüÀó½ˆ3ž´½Ùq<Æï CVÍì(HO/ x¾û×÷o;~d[z•‚{qQàÔ§SgöÏšùÓ¦åOž6öÅ7/üáõuM£,3š´z'UŸ¼ªË‹Òhò‘U£·… ‡f’ãès|àT&Lì§J…á S˜F±}òœ*ˆ Œ8gJœmIà65 ¸[šTøÿ“H.€Ä|a€D@àY@` °ð&àcÀY€” €¸ æÔ@±{I †")€<@)`1 °Ð8 88TH²OQbÝ:´³êmjL ‘7î$é@ DRy€RÀb@=` ppp@ò}(©î]ñtã9졹5˜%ÒÁ¶Ê£œéec»(.¤²F¨·¶‡d×ÃEO?]D üðî¾}ïVذ¥onÞüÖLÖ¦ÍJËÊJŸ™Ù?×–%gf¤¤dd¤¬nm]]°’AÙåuuåÙˆY‰>ŠŽŠŠŽíS§ø€WÔ)Ô!J©QR÷’/ˆ¤ò¥€Å€zÀ@à0àà<|)ÖPº/ y¸T— §|a€D@àY@` °ð&àcÀY€”‹€¸   Íû@æ¥M¬³=LÅÁ— i_ µpßùauI°)$$™52VêŒÙ_ýƒcø²ûê{¤ Ù:¡Êª€©SgEBÝ2y2Ô/ýûTiÈâÂ/¡uM"MXoïU]uËdÅöéáyÖ§HOKiêi ¦ÜEz°X®ýÌÊ5<¸"Ä0@ (,Ô6:‡'ç¢c‘÷’ †")€<@)`1 °Ð8 88[WcDlĈ˜æ†ÀÐKïÊHïô¡E"ò:Ò ߺðØÞ-L}âØ˜„„˜±‰x^ÝÊ꺺ê•uýÞQEÿÓ·^'G^;Z[wìlmÝùÅO?}ñåOäèÅýþU> ìõ¢¢Im)}L.}Zî!}g.~YŽ`ú~ñEv²óhqó>¹ªföÜÙð¯fi¢…W>tÛŽäBÆTÐÖ²tù’%Ë—¶´á]&š;6w.¾­oÅ·çÎ ‹ WHѪG ¸êYÆ4F¬Ýs¹Žk‰^¶Ÿ>¸Ÿ^¾,:†\!ÿ½‘?\õ1)ŒÊx´IЇ“ZMÝôPS7ÝÑÿ½‘x=R°(@QÃÒ£jge¼2ejgâ‘Ëá)ê¯<‘ÅUuuEè©€ÜÊ1E•¡¡;uClJ©Éwü†Íy\ÀÐrㆌݷP©ûÖ;0ü³?¨ÿÞåå+ÿnÜÖ‰ñÛ&µmmªyi]åòêbÛ ²^M:rf>Žz‹ëË»Ž»¹êuC+çÏ\T[”_0ÍÕm­ã= ÅuŒâQnaÍÜ2„ÒQO=Ê12¤aB`‘ŒVÜÕê ¬ÿ— 6ßO:;·ÃGÅ~ObëÿªKÃÍUdô¯gþ󸇣ðeGvê£jxS¼Ÿ¤`ï¤_ÀÎÚ$m÷ÕÒôÙãšsVïØ±ú©qObõô:|ÑÊêâÑÎ:Þ9ÌûïË–þÝÕÉ}ÜÀzC–ûÈ"ßÛÂÅ*{i…øwÐJ[ è'ˆ7>[VUU6ûÅOG-ÜÖaØsèË o|~ ú!j)~æ‰rûKÍÕU--UÕÍèÞzW·sÒ.{û†#W4Á Q›)<¨Ñ‘)=Qµ^ÀP8½µ ÇÏ>‰‰Sz6²ZTû(C83C´âùÓEaµƒ_oñ7b}.Vt¤uU™5­O`1þvAMÍ•ÚükIùIT|²¼dáÊÓ¸ütMDy)Û––=Q¬ý—œ[²tVÙ’ç†çwïn|®ÁÙeË‚}GŽì[°9””–” /· ÿeA ~¸5êítØAæZe~äÔäˆ=RhÌyãó ‡¾¸€ÿø!j†’1JH굃Öû¿¾Ÿzr?þ‰¯àN´]sõŠææÕÍLóãeWÊÆ)qÔTÉ/æ{·xôÙ»Eæ)3ÕºNPÚ‚ùP;„£¥,4œ4 ʣﻇù|¿ÉA¡£F…J.2»r}Úß/gbNŽŸß¿QpfFF&þl[YÓÒ¼b%ž‡8ÉK‘cÇFšü$^™<µr•¾uÕJ£§àªU/^}²ä…Û7oݺùûò%U[·€£NmÝ*VþÛ Ï1Þë}LݧæÖƒ{[›î‚«½Å+Š6¹ËBÜp‡LV’éSoåÀ™v§»ðíøä¾d€#׺:V;z}„áyÎcàp™§?„ “Žt}Ø,*%³T»LïüðkuJøå#‰S§tŒ›U•> X@ßÎ ¤ Çÿ|ã˜_“²éÈPÝÎÐÐÊ¢1•¹ø½¢º:Îõñô…ó6D}Ck6s`ÝY¾tþ×o'm/(h‹}¡zy店jš¶¶MÚ?që¸ï~_‰ƒ°€ìRWܰg°óZ7×iùEµ‹fίªÓ»ºï*×3Q×XÐ0øÒ‰«HÜ;”Lhz \èq šƒmh—íx¯“ÎŒ`©'™U@=žG]©p¢É@ÓŒ2å#³€kÜŠ§HW$gCr¶>í«ÝÐKAW°ý@uP”Ðîæú÷¥Ëþ®Óíì|öƒ£ÐAÞuÛÌZJ#(óÙ qq˜ ³{ ³41÷ˆF†±ªDûâ–Fý€¿øù¾<”ÓhÕ±-riv(ü~z NNøþŸÂÇ¿yûØÅ´„ çÜ\Ñ«çtèë‹ÅžÌÏ wt–ŽÆ%íè5aÀv?¦®ç2ûl‚ðxM–‚r‡ê9AòŸ´R'~³ãñ`ÆÈƒ‡ëp³ ‰G´ßyå5µWjjËkN£—O¯\­5ÖŸ,þW­4}rIÙ¬¥Ð'˜š_S“ü3´Á…ÐÓõ—%œ¤,áM2-dZjUMÍ®ÎüüÇGädMܲ¨Õ…/]øòõœÆ)' §Œ]N|y[ö;9¡LäŒQäƒó©ñ¿†|ðÜãµ›¬Yƶ—zæYÛ‹4‡vÐi@ö¦9ŒãÁn˜Q\ ÿЇßíè@±=ëÐñí+^?Ô2Pqæ&iËà¿*±Í‡Ì,6eGšŸõÞÕ” j!µ‘Ï`gRréM(€ÏÀNø}42§¯E1t5vBón yâÝó¸rö*Åþp䂼™Û×A¿q:QHG/ч:Ò“DÙ'Aö(‹¸PŽQ ö…ž–‹§…+tdð`4§âTQ¬ ˆm’ÄêÀ†ÍÒ:èü ýZ"ã pô¡ÓIB:‹ä|c *«4 ˱œJ³>ÚŽ è2¡ ïjèwQ´aùr’L\È»²”Ÿ¸ö[†ÈbãZŸ±Hã@Œ¦C4Ò i£ GäÀ»ª•xž­“ÃH;;•ZyÄPåìï¥Ryù;3UG”j8Τƒ“-Z«¢7^Ø|ã~¿²²;2’¹ Ð:"!ûe?Àõ¾Á¹V”-¥¡vŽ<*0f’Ùocü6ž›ñ@òþÕø³šüÙjršÅˆ¶¬â—±ËÅØÒâîâ+³=°VìrÌb)ÑYЦ~¢Þ‡¤ã­F*͸Ña†c-8š+»×DW#ü:Ž£q´9»$XC¸ñ˜x!«—˜mŒìˆúéþ>ƒ³¢`fèX¼Câ¡W B…ÉãÓˆÉ0iý ñ^¨x„oZè¥ÕúùZ1ùÄ{x<ïë§ÕzNßMôÖ¨ì>&üØN¥ñž(JÛ!º‘H“–oõ “>´HZ?YAB×îÿÆžçT’[”$30ÏCçÑ\‹rè\4C˜+Ì5Ë5r1lKÂÆ²kÑ1¿Öð<³q‹¢-tE‹%ßM,ù6Pÿ†’¼’íÅN )DÐÿsý‚:Q."6^G\‡zÁ E_Gm¨í:Þû_Q?JˆåÄç³]R&[ Âê~¤ × bdÆy0)«‹Ï >-‚óåjºfïjÃ[¡Ï@š€»qp·rP`¨F‚ì­ê6•ÆQ…üè3-ô…sÅ …>’@ü^a)}¯Û‡ðu´ ïR4²§(>˜m“Oit19š¬¼4A¸VLOŸƒ~)ÆNÕ¢¡›·tÓM[°S7Xúæ"g ¶ú³(‚d`&Y¶«Ç4æ’$dް¹˜V6ÓÓ‰8¶M”´…n‚½á±•(¬|‘*æ‹A˜•XLI)­ÌUÉ4¢nȾ…)‘Á 2Øp&ø?ó™áD áÝ…RA?Hkú˜b”zOÒ±¿ÐpXjTÓLh0ãlkØi&tJ-3]»åP„"ïŸã>bˆqôÝ-ÅlëMQ:ŠÔÅåÏž»|qyé3ù¹Ïä¹àIø È]ˆ¦á;Œõ6b,2à42J§qdñÆ·ßÞˆÛ„=pJ„ÐÞÝ ?èÎ;ÇŠ‹ÝÉÈêFÎÝÝ eÏýfؤzb¢†:™N”ܤaK$Æîjð•¸? ¯Cí8w4þ”sa>è’ù®Ý—Øu?Ônµs‰ÚdG:£Õ¾PIcaÈJb}$mÙ..yEÖHœn€›$U®¼ñ•Ø„ä*§Ý»µ>éiCp®×¤@ÿ {Nåå‰Úa™¦ 9,4Y6ÏW–ÆìÞmmã6µ{yÚÉ5AþAyžx’&’ÑÊðQÇIIq;ÅÞÃTpÔNqÇË ÑÞb4@C‡@Z¬ém€và3ÞÚjý¯_aÛ„!îB»·§{ ½} »§7ë>öS°@ŸX‡n¢«g›±Bß ÊÓ¸Û+\ìÝ=sƒ†¦ºÀ7ýª3މ{ó},z<ˆc®÷Šd!¾½¢{Kº“â!R-O¹y¨u:?ø042dZ‡çî5'8DaïêèÊòÞðgšÁ0 ~ÐÕ>£Çª¼ìT*gÇ{Vë£r¬ í£Ë ÉÕý6þoºÜ`ÒåÙrø`f?5]ìrÚ >û£ô՜'>…¿p@“§p›Oo—Átª£˜{÷e“ûÉS>˜ço° ™­LÆó<ãÂð<'—Ëð9ÜàØ|Žáå¼\ÐË­­xfÌhTΛL&§‹å¼‚c¸|–#—ÈY&—s’xâì ž~ÉΤs \Ö Örº%'³á9†…œüý Þ >"X¸PMÿÂ7Ü`ž±8$ç€dŽ·‘qð«žq –8‰ú C•ÑI²‡;É=ÜI:¦¯“UÆs¡ê$GÉIx'ïõÒ$£— $'U4StRJ*Ò÷:©ErRµä¤£&õúHΡµÄI6nãX£—dò‹ ú—n<ðÒúõj2w6˜ùIoÊŒ~b‰HR·S»¡œ>C©¤]µEWhÅ .î-v#-T™0P°FŒrê >£.ûåÔ²€€m>II>Ûì“Ê’²…“¥ªp_ÃYæç9iƒ¥Í1¸0¾¾á*ºzÂÄÆŽ9³wéÂÂt»Ftîjš(œ‰ªM.nx‹iöVCqrmùL¿FÁûPƒA _¤ób|(qe¾”±WSàHŒ¡( ÇÉ4B³Yæ™ÑÃëBÎ|ƒ»dVhHV‚¿f ŸígnfN¨q¡“Û ã/%Û#ÄOñ%ñ*˜|÷ÒùP¡#!|2’OÉh¨‚&¨²Ð+îúæLHÝðÑÏ0,šhœ^‚_k¤‘}ò¥FæÐö“Q?!Ó`¹ï϶7òXË\çŠ|Y¥àÄÉwÑMl‰¡ßqýnA3…~aÏÅžSìpf„ 740ES]0²¿R²”öƒõÖØkÉ7RwãÓtÕB×uw7Ÿ_\|ww1¼qÞ4½¼*Òm„MèOº»‹xCä÷Î7Åðÿþ½„·£80A'ZûrîÄWà?ºšYk˜g Ãï]ÀT"1‡…Ð%øJ'YÓ \†yÌZfíýûDó9o%m?JÚ€ž?V‚·™câýâýä>HLQüdîå@¤Êàt$¥ü'B‡˜zú«Ê‚ʯ*gά$ýwIð¯,ÔÌŒS=ÝlWIIŒÿ‰pDŒŸ`.Tѧ+ èk†M§ØQ=%%lWO÷)f† †‡þ$ƒPe| OEÖAáæL™/ÙZH2,|Àã6è‹b²ÒßM͉ŒÕhczi£ì—J\™+*¯Ê½‚òž.®¶ªìùIùùÙ ÉÕL¤©«ÕhtcbwæefÇ/2#ÆþZ‰+ÐêÊ«SrŸÏË4)ïùyK^œ']  X <-6·ýö0©êh´ÀÇÜ.óMµØ9ÏÌ,«ª}®üé¼ H'/w^ï•—ü*h‘++‘9jb#sRßMÏŠ‰Ñjb{i4"x@ÀÈlÐíÅ% Û¤ é*wÊUIÿ_ÇFdÆŽÑi4Ú±1Ù™y…;Œ¤*ºÿ£ü΋ò¦"¨ñ`ƒ=Ò…k¤#f<$e¦D#É.Ü«7.âA[OÃrès.€ü¼¨ò*Þf©1}Úu|íú´é#4–ï¼cáI®‘š\{Z PÓðÃÙ‚qã}h'Ú¯pô?®ÁY8ËÂ\J ô|"4?³bú´ââiÓ+2ó;'g-’.eM¾Z™£Ó_…fàMQáãÇé„ñ&4cL¡uâÓ0]1uÒ’Áµ‚ZqˆVð°béôé“Á1Š:FD)ØÂ14§@Ïéy Êöô5eDÄIÛ“A1Vr…‚’Ëä2=¼-ãl ‘rÞ62ÆŠ¡iDZ=¢‘év—mZv.õ:C˶‘¶‘‡äz…øÒµCŒø •§ EŽ0ƒìZŒê+¯*þvkñÕJzb>¹è¼Z)MXªMÚR|¯¶è Š"ºHÒVÁÓˆâx=÷¿«­”KB!'XªI0n-Vü Ô•"#©[lT׆ºð¦ÌBAÛÀçÌ /QyPaSdQ$›`1AÆŽ bld4kaC!¹‚ÄêÙÝ#NÛž íabÊzõ ={´öä6Ä¢°´²´µ-Ìn“¦žç à‡?ÙúÎ,chkb¯h³µ’ìkó!K=)^²Ì¢¦ —üaŒ¼êj%ª'ž¸]eŠ!ÄEÄ;@˜ål0ˆÎÆ`KZ3ÃŒ ›&C?1öúVÎQÂ>ol –¤9¾°± I6/.L††µ¾Ø˜lIÃ*&†™yޏrðæ¶Á¡IjÄÉmëðnGL,ë3ïö:væøÅÒE [»LŒcKFÖ—õŒ‰Á’@t6SÒ`ÈÆbJÖx1Ùæøee'aæÁ´¿˜ñbðâÂäÐÀH²1Ù’†ULN 3ó\qåàÍgƒWÇ}DcÄ%ìë€Á4Ç76–!ÉæÅÉ¥aD”É–D«˜œfpÅ•ƒ7Ÿ ^Msüdc’ìºàÁdkØŒ¯<ñæàÍoƒ_3ÂaƒS%øm°5ÙõÏŸ~¿:g“Aºôþ·ÛcZϘud¯c'a½>fÜlí²1ì%ñ3²>í1XˆÎÆ`KZ2ÅŽ “&S?Ûß?³úO›¼±1’íÇGv¼Ø¼80™Öúbc2$™÷Ï\ã»þxâÊÁ›ßÆñiâgdÿÉŠ3c\a`ØH2úOf¼806õÉÄdK¢ULN ³¸âÊÁ›ßKÓfüdc’ìºàÂdi°ûOF¼9xóÛ`h²Ç_¶ N ”à·ÁÖd×?^øýê¼M~ì?ñ n³°Q¾®X IËøÍÆBI6/.L Œ,“-iXåÃäÑ0+'®¼ùmphZÇo66J²ë‚ÓFƒ=~sÅ›ƒ7¿ ~MÌŸ  ”à·ÁÖd×?^øýê¼M^Kÿ}œy:žq“ÓFƒÝŸsåƒ7¿ ~MÌ Ÿ  dÃoƒ_ÓZiüyá÷«ó6ùlúŽ¿#³i‡Ò‚¤ÝoÎFqâ1kª˜…“Fèkºôˆ˜¤ß¹rLSkùpSë_ÒªéˆÒv„¢¼(æÜ“д“> uÁ¦©b&ZÕúW“|¸µO“˜Kï§>ýGX¬d:žRqºIFh¦†G›ª@£šºàѧ6H¹4xÑOœ75/ñ…E5—̪œ=¸ªz>܆SDŒýY—Ô”Ó‰#OO.ëxVÉâì¬éSç^ Â¥s§ÎÊF]Wºv¤ LÏÊ–ûScÒÆt<¡%Ê‘b‹#ôESå}nhÏŽp2Åg®ª1yㄸm~uØÇl&Ž”M&O×™~ kmšÊ‚ìYQ~ØLX“Fž­Dˆ_þØ‘(x„laúˆ®Ù'öNèáí™ìè"d¦Çg ìãiñ ½aFEc왓qèC§ïF§5äµìˆwœäg´ìÌ6ŒNt ³ââ3·gÈ=z½œáNð>–ãùcgÞëôíùaX®ö ì¾>ïÂ88<Îdí± I^í iÖÇCä‹ë*órË~Xí[rÖYK|äÈ‘.]VR²ìÒŽ@ןï™ã{¿¼wÓ(ç%÷üB~*)•þ8wÒ²ÖÜ¥“Ïx`„ÅÃI#ºví“Ý«»Ø3¹·"ÄdõÍÎèO=¬hŒAçú·½­‡Oyûä ÉIõ‚$4 ©ÎL§¾;ðs¸:‰†æÌTUØ,»*U~;P$¾NÖ׉ÞzÑÛ^G·ˆšU˜ Í lV5µÖ¡ B5TŠä·ƒ?×SÒÍm¯:3•Q‚‚ l¤xqB|!x›¥L)3м>+QʼnG‹•—‘Í⛫dš*$5RfK0U …t„¨)…°IT ×NpR©€&¼|ó'beЯ«&½¢5|š†O‡F%q‰\ýIÐ/VRø‰ÁÆÃð…&·GáKbÉ‘ˆ(4² O‰LÃw’6¯ ß>1¶&4@>ÙÕ@v4]âA1+Ø„o¿ð­F ôÔPÌ®,k¾|ÐÐð$}íÙ£é`@TŠÈ%Î7•VAÒWHÃeSìzLï-›óåÞéOÙ½rÉ^J^^rÛüËçÓ·-ámÒ–AÕ²ecÇŒ»lQÉ|¢{â7d•ôöð¢ùµä‡-ȵó‹†ã$n؉tïK—=¤¾óÆCê²¥+è¶úÐï¨-[ÁŸÏª t†¹b¤—€ÙÈϱ¸H\¬mÖŠK•Qün,®Öª¨¬Šÿ’G)n ÿœnÈtC§<š»`wɇ] H>É«ƒ]$¿S®}ƒu$ä/@Œ:Ø »ÚÚ„+Èi1Ž8O¦Ûé˜'ž¾n§ àž˜A¿n§<žz('=鈣•)(¸¾FÍQB ä…¦9K4qRànïð2WoòôèîÚpÏâÄ5žâ2W7èJ¾JÓö×^ÿ¨xŸ*Æn»åÊåñ×?Jº¨ÁÓp;ÂHF„‘Âö†œcÉäðÄ’/ =^·ˆãÀE~‰…^¤%Å0 »Õ_`„a¸¹á•tǽhÿ@³“‘¤è6 5&Š/Cg!Œè=`4yâ$ÓZò"Œ×÷å¿®4øõ“Ÿ Î Y»Ó=qµfI [ ä¶Ð·L© 0ÅጴiT‘nB ›ê âî-¿¬ßÄäÞI%¥nÇÏ(Ÿ;ïŒ+ô#’O4®")‘ýo9. txkÒÝäÅ7Ü­|ÞÚuµU P¼õÖ+—Êà€7{üŽK×£"gQ®ySZÒ§à“ãóñ™òXîV¾ù؃!á#TGäTÍ÷BÁóã¦Ï=kŽ×µñnj©Û3­dÁùó ¢‡Ê ’d Î“sÖüT*þ¸©ë.­ò’MÑÓº—,¸o~eþ Ò½Ûn3$2”>‚èà+ášD‘ÕBwM†àojòxaÙ–èÚ|÷µ”Ű1î8F6uqm_y îëIð;˜/æÂ#Ütͬ“?Ï“¶‡áN”ͺMš–H‹Ø•Zâ‚ é¾hýœþ'%¦ÇºâÎ5wnUm¼I&¹_Á¼[ÖPp­¨ÀkÜtõRdp [ÅÕdÿ¼³È‹!Rð¤gõ‹t:rF'[§šIÊ‚ÚDB‘ÜpIBÎßÖى˛šÈ•˜£žYCkÞ¼ëöËÞ?£›cóÝF) /ݾ»eí¤r=gâ#$ø-æü’^+÷-ß$ÎÀzAzï%|KöJ#[´ —"Bt‚0Xg•LÉàÎ’Q(ØÉÛ† njzoÍíV,ßÜ®¦‘š9<"6’+Wì[¾rÏ ’*h( ´%~Š' i8Ú£j {‚D{ÚªÝÃ=föâ¡y=ε}ù"}¿ËÝ"o<¸z­H Ü¨½A‘|ô𢫖aÎÜâå$¾#É´½Hó6‡È2®,ÚÏÒÖ‡ËcDOßàF]TÝ!îÜ;VÌšírÍ&ß㸺¦z“–çf“7`øìç`@OzEiëqéD<!ØÑUGwFË;ºàñ6"¾¥ËãýùUiQ*…^B‘0o—‹A™OðHIí?;:<™Œ/<ë-n-û«ç®Šq8<÷ÍœüPõ…;fÏ]·nfUÃ…’]Ú¹ #¤—1n¯,.K¢ã” gâ¦";N©€µþ‰¥E²,:Š'œî¯¤›¢ì(/Í$ß“ŸüP£Â¼Õðý¿œi=ó/½ÌçK#Y©’~誗‹VCChÝR}Am½:¥‡îš ¿ý7Ý5•t+ÅFô`óÄõú–*ÍÐ =môëDÀE±#³´@h‹Íè‘tbÜ µí ØQ mÓ¦—ä¢C½µ^Jϲd’Ò9«]Ó·N‘£‡´ìŒü;ªpsêœÓVÕ8§Ñ3ƒ#jL¹aHÅY]éÛŸy´@§\?C{òWÜ zôóøèJ1ð Åàv˜?øxøcuôê`°/-2¨›¡—…æàôB>Sphëd&!{d¢æ2 7ݸ©_;¤+è7Z»"p~ëo©µrTÈõïNW[g¾¿ßOöªdŸt­Ú‘… ¼t¨ö†ŒÑÍæHH4EjØÀ"ŸªjÀe˜#ªŠh›¨…½‡Ší„‚y ž$jÁ¥õéCñ}ü칫<‘—Y\²v‘¯Y†¨b(rÑ˪⠌ž#¦x<¹TUÖ™™ç¥ ¡ßl`äÓ†€™?£ºÒèÆRûiB¿cÙO…Z¾rè"¤àuįE ¸³:>yí̪íÑ7ò!¾èÿOÙp':”FÙp‡›Ž9ãL-22ÄCƨ,“=™Öó¬UÑ1ƒCëK/,<2ˆþM;ةƒŒÈ“û°GšUÕ ÝÙ«2"®éÚ°3u ÔÂOí,¸ÁçÃH²· ÝF¯‚œF²/Xvi/=ÕØ‘‘Ù¶í"„ÖÐBð3Ã+ТÞÔÜH-ôÕ›ÄÒaAçj‰j°—Ùך¨·BO’ÕVÅ ¨ˆzÍz#Ôúö]ó$Õ ®…y0Go‰ÚÚ4ÅÃJ_½c!?ÑÎägìX @½c!_Ðã¿hˇmOÒ ø*ÍPš2ŠÃÅñaÈåäEE”÷©j+ ë<jüä'q˜ª¶íj¸ZByy90ÂŒ :ú=õ¦«Ÿ¬PÉJ‰èƒÙË”à.ºåÔ‰èG2†ãéA?e¬ÄÏÀÝaBVPÄiÅHfÌ£ùH¢æ «}Ï2Ia/õ¬ŸÐŸê®=<ÂNó9]…%PX€m¸JjË… U3×ÅÅÆ¬™;{Ç…ÕMžyŸ'6fÕÜjÿÅbþî™J²\<¦ÒÞ ”™. ÕÜã³êYF¥W§¢ŸÛ=7Â]¼ÿW‹]:þ7I ÞLÐwB$hóaúnæ: ãpD… DUÉUUZªÆ;dGQiåŽ3';DY¦›R3e䙄ŒðeÀàd† KL8Ÿ¶~»éVoJJH × ´äœ/-b¡Zz\ë†nhdÄëGgMí)“s!&6µ`Ò”ÝSª {ÅA 9Oî™:4+8\“šªŠWÉ©KK§7 ËŸ–—ŸŸ7-·¸aZÙèA©oUmhØCk|’ a…¢yA‘!IÈð¥^“ëH:´À"r]àéX´P•#_ΘA¾<u7Ö_ ‹£pâ±¹Oóy}‚’›$ƒOÈHÇ‹I7®!×Á"h!é UUv$‹¯®¿‘ÜzzΘä †q‚léƒMNíšk+;¸LïŠ)~{’¶(Ù×vÍv4qÞB‡ 7E¯ÀÃF¾0T^^J^‰è®àb‘-iÒ6˜‘*˜KÈ—Yÿýû \>¹¾ þÇÑ`›ž‘É`†´-0l"1-øAñ ¹½ª~2Yrðïÿfi¸LLÀÍêx!¾µ@èÁ“¬Ø—<ξ)¾}Ûíýa"9y–xJ°àöÛ¥¬|§Öª[kÓÏm—+Ë£¼8.£¦?¶FIK´‡Ñ&£ÜÄÖi!µx‡p¢P$”Q›ío'ñn‡¾7›%`Æ#ÔXÅ8ÎÊ_={Ó ÝÃFÏr»®Å¯®o¼À­~Q7žÃb#‘îTÀ}´ÃF;&‹’ctl™ôCTDÿUÿ"BßYÿZϋΠ¿{Ö" þ¨´Pï „‘¾5óÖ¹›XkÎ ý¸of¿²Ôÿ¿âODž:ã5G<î´»¶6ý{ÿÅ=€Ãûfwï wU,œÇ9Š•ÄŽXˆŠŠzÖØ»ÁØ v¬±E-ŠÄØEcï¢&vc‹±—5Æ…Ûá{3{w€šä÷ý{oïfwöõéoÞ`‹µÉÑ)Šf=\ìŸñ–»h¿Šüް±ÕxÍ¿ÈÆ‚áÞ‡ Ì ~ôüú^ëÈV„@¶²«Ø«ðtÇŽüSò i› á1}ØŸÞìJpýpÅt€ÜJévH1ÑíäÖaò“Ô<…ßë#”…‡Ä“[‡ù§4nN(F}4D»‚Þ¨u%D¡é)äÖ1–üt˜Ü¢Û“Ôœ¦«À•Wä';xFŒ!¯ËÚÁhÙŸQŽv%Ñz¹æG})$· ^1Vª1b„›Š9ðO/bOÄÇH”_X¬Rצ“Þvò#!Z˜ŒŽm£WÁ€@3zvaSƼ3ÄB%°#ب¤Ü&“úÜÈp› 6¼ùôö3nþš0¾¡o&L78àºXãkT!IÅó^Ûߤ€Ž‚ 'i\ì‡Ü`>èèx*wœ£Ãò埕“äËÎCbŒx‹¬ÃÐÃ)•JÒ³\½òеÂ}§.¡.U…àTÜ/PR—`æZ"çp" i iD®(x.ŠN±CØ ½¨¨ß¯*.*| Xlo=»H 90¯úÆ)¦ÃV)0Nukr•…é–YÌeî2í#ä1”mU¥´§#´,*2”Τ5Ÿòàº$TÓZ.†¯ðxÙü l¥“,[Þ$ñ<:ä¥ËÅ0ŠôSZ3J?ÓðµÆˆÉlÇÄy1DXF)ÍV•ÅžVQϤ÷Y4iö"3«ÈÀ±yqlÚAúˆÇâù¼IOGØqÝÏ„ãH öäKšËr ¯D¤7£0öhüsÛÈѹ§lš€w¿"Ë?–>È(êŠä$L{²D–¹2Ë5Hã?MŽÉmÍ2Qod5]?ȦÁlFg6WY4aÊ4ÛŸb ±¤í••Œ #”tQg{i{ù~nè]Ååd¨#3ñTÒ¥—ÿ˜À•`v2,÷Ë‚ȈÈnt•–K¿ÙIá™ QRÜÉžÌ=ØØ …¿PN0 „P¡šͺ;Ò€Wöü˜ìßøçLYøs篚Ë÷Âtí™§9R³AŸâ7]3è>Þcï‡CÁÎ3g`{NŽ-pþ“³bßCròÐñ Î ôP!0?GsNc$&b “Ä$º'ïWq'é«Ì³y ¤PWÌ„b’™`¿ƒfËt¾0ݶElAŠÓrÎæ7Ôt—m‚Ì‹a'RqW5p!Ø•iƒÈcòXñ…Hº>'?Ãçtí¡˜ßÑÏlkèg‚!D󗪯üY$WùüÝškòT–KfŠ8G[Ã&±=l¢­©o(¾˜q\&jBAÞ: HæD‘Hø‘•<ª(6ªÄJë¡âÂ…ôJ^2½²p!¾Û,¥æ[M3&*WQ/ÕdçÈÙ<5ñ¶L[&4”âÌÂEżçd½TÒ¶UlŽr PóËÇésÛdúŒœ“œo›+’zË—ùTsã¼3;ÁÏ©hOײ¹jëtÿÉ—©6##ƒŽÎ$•ñ[B*j<¹«÷Œ³¿l÷”ƒ‘[¶dïÂÙ[¶½Ê%þ©®œÄ—ó¯Ú)0q °ÃÊGæÞƒâ„ð.-;T‚÷œNåЗòð _’îí= Œ*\pÇÏ[¾¬Tp5¶ž<_¢U3QÀ¢šÒ„3<Ñé„2¬8…«Þ\•@%ÚÇËqŠ>ÊáœÌ;nÜØ<šsà@¹dÉL íåËݸ5p ²^ü)#ãüÜ)“çžË@À§hw9û ‰ØRú’îØ¯ý\…À²Þ²›ÑOò«[Ú­´§dÖÙž…ÛŽ² àzôˆÕá.±A®.ZÑM£%u'‰Ÿzs+¹@¿YÒHBQ*³À5p×8©êëg—žyÔ`º×,&zýÖêäßÙ!¶-¿56 Cì§_øƒ,È ¨•¼/ð$Þ¢dÌ`&Ab°&HääQ¤!4$=å¡ÒPy¸º&]»H³H[®ƒÐÊ‚Á$V”Ý7ÈŒüHnÐÛEg~’VçìµqÓGÜì7¹n³{gá$¶ ÊT:wÁ‚¹d¯wÆhÚÆ,ì¢üqxaæk()÷½òóÌ=¤…ò|Ê„ “ ÛžUÒ2i/Ú@ˆÆ]"Qj:˜¾T ¢‹ŒlXÙ,‘ZðæŽÎv§))4»îÞô))=RûÕRò*,/V"á«Ô Ýö·{4b~§õ5~ˆ¬þ=Ý[§ëâÝʱ]-IdŒ2.³{ç˜/›OJJÛwð@ñâ ªTÉ ´­ÅéÁ•>ñðÝCGT¹¶½½ ^ÅËZÒÐ`ç*âI)Biîj°ˆÌÉ‹‹‡ÁD“AÏl†W;˜ypåù̦ƒ÷í»Üª„[÷Ì+S¦ÈËéabQÎΦîý§6_Ò²¸Ž^!ƒH™ÙP'?è€F’/ë4R$÷ÆÛCÈ¥5žìÌtLQõæ!ÅíÇ1xáO“dÍ5jAÖîÝŸ¶m7ôP×®ÙÙdùŠåû×(S4žÊònááÃGT7»A¯ÐOØÁ¿üÜW"m;z´Ù³µôÍ‚(¹ö™íöÓ3gûíì™§Òïs+=š86B þm«Œ ÷èÐÊà_ú|wM9äÞ“·áȉëÕ˜OA(| c ôì½fõ'÷£cNÓ1GåË6ñmî<úêuæÂÃÒ.› ¹w‚¼ rO„Æ«33„Å`4XÈ^ðPnÀ¤ʉ éä›ôË'È#[kù2›Ø*$wÁ_ãGÄ`i @p¸m@àlâ‘°Í9…’@³è«µÏD#ç–6bù¥PF=G†ãð¸ÑÛAò6¡;=Eé1z˜Òig &Ô¸‡­GLÒÔôÄÄÌVU*†”–îä•“u¹/åËyÒsü5ŠMIißoÂȨ–!ÌóÓéý¤ÏâÎËR»\Ê/ˆÂÀü{ÒPÔ©M=@î’åÌÖ\€ŸDíã8Ò&*Rôî8¥Cí·=Üúœ–£/ÄO²èµ,<±:eRz[‹tîÔqJJ é ÕéIå=5àú“G§R›5ªÞ¡Sø§MÚ#]PеÊ>ôR?¢E©J|ØINÊ)5oå<1Bçççç?É× ÏÅZ2H+ëN‘ebOùW>¨æ–0.ž¡ÒDºÖWbÉAÛ^Ì:…ꤓòK~¸³[âàJ)'uŸ²À›¥*„/*ã_¤ãöªüD¿@´w©ñà3Ön‹‰ÚøÅÃ}æ>5eÔ¼†¾uË7ï‘s»ZÉðàácÓž únï”%iwsÀç¯^×å5Ç¢£Æëj©bò«Ú«ÏÎkÚz ï×$SÜ¢ú¡nF­oÕs ¦î7ªœoèÁÇïñ@'³*З÷MÁf¦Y@ y]¤m}æ·6£½Ø|à—¬AÀÖ!qÐP[ú º‡Cÿ³.¶ ™ØH”èt¶_TõlÚOÌ’: >¼ã%oÕI· ~†r`g0°EõZõGøó@@@ÛÎõȾݸ]rÕ`æØéÓÇBkX> $gmŸ¹”0ÕÛ[£úáñS“’ªFúyÅD@Iš»6…Fv¬Ö6ºœÃ­M /äîf mÍÍbM’ŒV,ó6 ÛÇͰþe¶Æs¶²ŸžÀ§ýÕ§ütUì|ëwË“f+AìeúŠ$Ã> °°ãû¡çKšœ=f6¹Î^ƧýÕ§`d½kÒŸ&¿ØM_Í&M"ÌCÓÛŒÆ ¼zŒf©ª€ªi¾Òå°.í2@öžÖh4.ÁU“Û5¾ÝãPÇÎÛZô™`©Ñ®]uJé¼´ˆ/¿ÈªIISãW®^co½†ýÚVëÙ6i|·%(AXÊfÊ/uèÁŒ­ù÷ĽR¦{ò4ÖLä8ëÌ@O¯Ãt?)U§!xçf×”+W"„:±^aÕ*—|}ð蜊e5BTéÒ†‘ØHŸ9ãSãÒݳKg°—\‘x Îëðgá7Ÿ…§ã°*Sywà?¹?½…UR:cxÁáU–=«Î,øÓ%·û§íûuNýbÄi‡ûµ:¤{½FýßnŸ´ñM»½ÔÚ4®Ã4ÛÎíß°ÐWÓ—×­_lý¯§¿:~÷Ù¢Ïÿ¼»“X¢Øî¼ûç狞ÝEÞåßÓ~#ŒBL¬Á(”s!%KH®e<$bÒ¹kPþÎ|6hËxl\gK”/Äö>ŒåËK¦½Ûw/ºn©2®µ>Óè zù%½Ôššv|î¹:MFxYmVÏ‘Mêž›+£÷èo´åÝ»°<¡ ÝKóª·/]¼}uо…F¨˜l¡¢‚Ä}Ì”…Šòm®°:‚ ™&¥áý[üþ²^Ô~„öš+ë ¬·Ä/>h¯¬‡F#K²«´1Í€l/@XN=W’.¢Çèqš# &Ô‚žr<ͦèCš ¡ø# s+‘ô1ík¡3tµ´oœîÉyX©»1¬à8‰_ŽÖ¤Lå9)d쬬y 6fAÏËÙÙç\é»?pc#xÀ¯ïÄÛONœ|üæ>¢‡á6¦®oé¨NuzÑ\DaàWpg«q¼‘6bž·™ê”Ý7H éJ§ôË6Z›Y-ô§Ï‰Y¾œ[‰­P’ÊçØèö†Ÿ?w(ÿžFBªË ¡Ãfžvˆìè툂‘·“±Ú ‘Ø`&ëtUú6;.ô^ä[ûhæÏ—tÎí¬qýï¶`­ÜRù›ñOÊ{Œ™ÎÇ06º½{Z—Á8zA2ºÑ..È•‡PJЂ<üØ*³*8F ¦t".HgMê¬i¡TÈÌ̱áú'¡þ|BÈü̺# ‚A©Ì¼1¬{NÀoÒ¸K´ÝQ¿‚ÇUMÍ#)ÁûqðQúÈÀ»ƒqsû.rkeõ¼y»•Ðybwúå°2Ä{^HâEØpiží8ëÅÓ°I¬9ƒZo²sÍÁE¯n•¶DÚá2€œÜ„[5Û0bs¨÷.™a«ß+Í̽,WÊVrè[_G >Ïé*üz%`ïk5ñ?XêËŸ)«!›¦%w\\æËupn×<›E<ÛŸ¶<ü“Ú¹ryÃwÛí/î±K’Rá˜L'ô×¶¼«Â4§,¤ d“ `”€:PW쩬ÞMOgÓœÝRªâAž3Ð5ˆ\³µÌÏç}ökXÉ Ãá.–BÆË¦½‹£ Zò²9ªäO$–_Ó7Z~$'okÍøñà›1Øþ‹‚2Kª]¼k.^üC‰?÷ÓOçÈv¼Ò“´ýà!ˆ )¥ÊÒ ÈÑ‹.\Tb.H_-®‚Äh‘ëIiö%ì²8Åò³eÖ©U¿Â^DÚóâŸ#÷î;|ïÞaÄՄÒÛ *É¡”Ã#ÚïÑã“'ž,CzžÑßé G1U¥âÖ¥T2S˜l4ƒìR¸ëŽv§”ÆžHýó'â7—NSr£  6kÒ6šå?œ'q¡vz« ÑB]¡Û”.LÄFÔμ÷-¨ýÉûwà?¤…÷Ò†p6sR Ø•ÜíS« Ê«•ZÀ®¬KŠŠb=8†PŠ›FªF?ÍmÃÖ­[í8”ÕÏTy:µí ŸJ©óhf£Vè«ËS—…EÏ—u¨Ÿx–‘/Ò^=pò Ï‘q‚ûòé0N¥íª:÷%V`”á{}‘2½2OÇìQ¤JKC“©or¿-?£ eÿœ*alpC¢ñåïñåmˆÔG¨[ÌÏ¥´ÆP\(í.ù²yÅ3—ž±Þ„YPì;¯ã´%«Æ¼~ö;^Æp¾¡Æ.P졾@¡þrŠKsõpýµW3©îOžÓî0 Rª{åà²ÎX¢ö V«ÆS§!A‚PP=4%3 —8µ„á Š¤Œœ¬®ÝXÙÁÖÝUà@ºœœž&'ç®ÿ÷lR`ºÍúßÙÈËteÑgƒ½éôÌDzE³lØP©ÙðßÇ2Ë„í5Áè ô·t ÿ•³ðßÙ8 ÿ³ða6V! ²qpæ‘ùÔ!ZtL'è'Hz¡§ZþÔbžXzæ?³(‹þ3‹Íš^” 3/„Ð(»Tv'ËÈ,ù>_e{MÕ};Öø;~îçóâåû·êMPÉÆ õxÞE˜÷Wž×„[ÓÔ€)_Õx?ïó¹ñdæ%•DÌK„#dÔü"ææ–€™µ¦à#cÇŽ-ѽ„Xq–XŸÎ9yòýœ`âáepÛ+ÔìŻ—”[ϲí…þ'O¢Á õÄú:mÉÉö¡úT4õ³èo¨cý‹bT?o­·Áã'ˆó¤;!®,]EÊ”tÝ”­q[M‚WñUtƃ®}img‡0GJý ²'½•ËâåNI—‰³5n‹aÔb(µˆ>^Þ¬í^šÃi1ŽwÆÐùhI¤Îè¡C×L'W•{âYJ/Tj—HÀ –YH㤴ÈL’™÷©s–r-¬h |¾UWhÒÒˆ œaÊÁéʺ“ûát›g"VÇ\%é¤Ôà3ÆI¬ÒàöB“×¼£‹5±³"³ìØÛ†:¾zGÃjX™Ã¶¾x¬4Æš(¬ú\ÞjÄkØNE^í²?wìË7²q`$&,ö¹+ÄMˆØ‘vë5®kb«E(ÃÁãÆï=fôÔƒ]­)Ön‚qÊrµI9½„zW¯Ô§ç´›_Œ4TóókÚtöä&q‹à©-Ž7.Ñ4^Ž—³Þ˜¹¤â. …Q4 ‘lU€ñN=Gwìjíp²ï~¤ ý·›`“Ò›¯¬š2e·Ø nr"n½ò0˜T G&6[*kpå`Å’Š‹ôðzvv6öÓøP¼%ZO¢=Ä“Ò(¾+×¾»œÿíki^öÇþ“)½ø‰ÏØébb <ŸÙ¥sËVÆÕjX¿Þ¥ó§6nžøUçV‰Ú%$¶|Né4È HJLÛÓ³g© ¡ž^u÷íÛ°Á¢f‰“pöµtéêûÍ—fR{Áâh)•FçãrTy—i¥=Rê|zq¾òrÒ'N°IÖõDV.\ŸÛ‡c^¬’pµÉ*^L|ÕÕHâÀ98üz™d±q™8XåÕ‘«#4ö‘K*~äŽÐ$! [®`>®Œbù¹ÑC0³cÊË9p{oÀææóÁÀ ¼}x..k²_¨‰[Ùh®_+uL§äÌð(2¤SÒ#aä þ¼ý'ÄbÄÅóq^°ìí;èåö¬_x¨=û­‹Ág>:&»ü{®¡X5o!€É΀ýJÇØMU•_Á`Ř:`/Ô‚š0‚¦ÓãôØÞ–U”NQ¦ÓÆT²äË´ì¦//Ðöt9]AÛ_H¤ U4¿7<¹€ÓìCàꬸp|ˆ¿$ùˆD|¢ ‹²`!|©Î̦G=RglÝr¦°¡âÀi´…}L,ÍÏÚ¸ç¤4ÇØáë:­L)"¸}û”l‡Ð²c 0¡¹°ùt¢8o xY•gáb™ÔÈ^&šööÆÞtšÞFŽ33³ãRØ”7èàÅ—èœBoÒ+Ñ“Ž—D9”D aþ¸Ayë9UHâ¼Æ«¶n)j=/±îü.;»Þ–¡(÷ªnÏì_˜W[¾°ÅŠýkHZÞʬî]IK;ãêR’v;g·g×ÀJ•ZÊT.O¨e©æ¸ W/('{Ê5^†.ç•“îäVRÓ4çe(¤HùÑGþ“~À؋Ƈ%â%_qÚ¾r–Y}Q†h$[ ÄBðÂ*/äAåƒÿ™|TVÈhèAÛÎ}àä¨ÇèzA™Þ+’1µÞ^3\¶µFõeND;[ÜrYEoü87¬úw¬â{DâÐöîÇ—YºôüEꩲf¿_¨ûž…¯i× å¬SÜ~t4I»”³ê«Ú¦Æ9˜© NÅ©ÌBŸ¦ÍâFý¼1¾b¥pƒ“ãŸàô®ÁÇëÖÎ 4O ·k2V:ÈôH“8ÃÁB8²û¯Ê+ 3".lŸCÿA—žÍçš×'>ÁRоV¹þˆF‡‹½óªŸübIãÉŸ:ù¶O³ÆMœË‡c°‰ø‹5¢ ¨î'|QÛƒ÷ôX_7 v—ŽS輡®i‹¥èÅ‚IùòîLúŒ­jÛî±5niP¦l†FZ£ñ´ƒa-“¦Éæðn(OáËÜäó—¸È½}»Æ3k²›-t+Yl‘[:ºF2’»#5¬©ãaý¤S96æ¨B¾C”ŽÍ”Š™$W]”#]£Öž1Ê~šM–‰#¹‰m9y€™>C²–!¼NîìÞ9y9ë×3 ë×Ë—]ck›)µbøG ÐW”{äéî8Ð6ûÎü¯ñ¤w×äm’Z¯aû¯Ð¾q‡Å¦ ß‹‰dÖW&Z kD|Í#:JbápgŠÁ¨FC±yØûϸÑÅ6ïåMÄ–b£Çù¬ÚK|hãq½=Åí†^cl ݆ƒìqÃä—vÐ{t?N…zPfÇ…5k&_¹2™zæ~µfÍW*ò,) ©(+Û»SH…Æ>©T0?éò*Aœ fñäìË©ý½Žt=õåé£Ý÷êŸþÇü¤ºz2X_7i£2Á%>iÁ¼¤fRÚ²Ñg3^g|ý5^ÎŽ^¶lpâ„q‰C©ycÁCz¢c,+”…*ª|ж”H ¦t‚Äo½ˆÁ(ª&S•}…Î}UMU¸|èI| ZN1£à7Š'²víÈ‚f4¢vLNÈ‘£°vMцаò17¯u½xO¯ ­4:òµ[u©“Ì%Ê%ËRòv{Ci«lÿ1ïåT/¯©ð¶öå<…òe®¬âmG3霚[æëïï»ÌÙȲÔ׎iºBÎV.|"Òá0d.ðºò*°ÏyªƒUu§ÓÕ￳¢ZUù‰¹X­S®ŽfPHÉÏ£IÒ¾Ä[N`á¬áˆdÏê(îÍŒ)æ½(>ø–Þüö[´í;°»æ–´3XœIÛR“ÒÝ`……ÑÞh éY¦µ3Ý«¼¤{;[陌Y³Â*By1ò Ú¦1\ÂŽ>wã½g¶ÅBkïa×í¾cp™ HdNhJXM(åS¿Eƒ†c:¯2ÚýÈäË÷nܺ«¬'ɾåjGøÎ÷iÒôËyñµ³ +M’#{Źb£¶n-:{ðNÄØfÑdŽÛ×sg%ÆØÞm©P9¢¦¿„„‘Ñä\¿‹eðÓéGNÓ{~¾þŸD„؉™Ü¶–Yç”È*å<[ú¡óåk|´i*¼ô+ª`Ž—h™–)X ¬Žmœù¹ÅÃ]-ji9¼ÁÕÆ{»¼.ùþ2PnÆ—]†ˆàXâ%ÄFg%7õeŸÏÝ‘”("?)Ïܪ)¢Ø(‡‰»´´Š(´qUÍ-£Nç ù§Gm©™}à@6þTf¢E¬_Oo®·v†úDõ;[3ó9«¡ÐÜYCÍê¼ìÐepa³y·4n›pr;5/ÐÉä¡aJB¦VBÒE,fŸ 4Fò?Ò…ÖÃìM2ç`ˆ„;Dà’©[LÆi$DAÐèlM7c¡XŠõó1ÕÝ?ë v7‹/D©Cì´Ú–Z±Ú‚h!ò´ç*/œÇH—B7äñ˜˜ ý¶(DǺÁDaœ$ÈD’h„¦› í>áð:›dŽ-+·”Sår†¼RÖt`úg"Ìˤ2iŸºöЈ{8“Íh’˜€¤ÊÙ£0yZ¢U§á‚i“0ÑÀTs}¢›AJËìÜÜÚ#ª5<°˜ÄÐÇEŠ›ÖÈ%.6ºR0jȲ39dò¾7voÇ[ö®“\6Ä×îtdáQœ.}©ô>jÆ\fëšÌ[Ìîéø7¯Zà/™>¢Ûyó²‹hVyÁ–/ÓVR*S,Â’°UC~ÉFfî ¤‘L,p$÷len†÷ÊÀN–>àíÛҺ“x ¥WÒUÇg†$T0:Îmÿ¶éôÅ´)¢t¼Rž·_ïi ÿ|ýe˜³NóY"œ™{·ôsÙrÐ7nxµSêâ¯è5X0†vúiøb3*RB³Õ!cABó2ëˆQÝÍ{ï¬uRæa¯J£}ÜAæ].Õ,Hu2’ÚQšÛ9«U«å­wl¯±uÔ£ë•ëi¢µý8jkìÆ»•žUZÏ_°¶„!¼õÂùkÉtz—>)Sv[åŠû´ïÚåëϘºW¯ÔìÒ5“n ïZ»öé61ýnžðÝå“~7O–)&"©{>‡ÁÝNàcö‘ñKË犣ÍÁ^§ìN«#IëJÊï$š¾¡Å~ÎYRéTïçð_ýâØ˜ÍÊ ø»1m},c>éÙož ]Z®ñ,€nQ¡›ôHºÝ¹µ#<7Ò†ý’NÚ¿ùƒò½~®í×t™G“¡O]zñexטÎÝÒ‘þÖN¸7@3Ÿ°Ö¢ÊvÒE½Ù Gc„ŽPý(ÒK_Òuô(ûmð_W>ïAªPl[KfõRnÀ}ü½n(ç½ie¤òÀuêÃçx²ºj©l("µ+®  ³°r‚ÑÏͶkœaŽ1k«¥Z2šp 齟&@Ô™€ #Ú.®ÖµFØÐØû×t²s5/Øñíõ…ÄU¤Æ![â å>g&½—Å' û‰såœat“rO*åÃÔ‘ŸGå¸y”΋«]¬°ËøT,c>% ?¯cm>&±DÉ™{^m«ò)Pï¨_½«Œ$1Î^WΤ٧° xиrM*/û¶i(ç¾æžI†*¥ÅíúÓû#2º,S¹Ž!7uï*Ëj¹•uXn«"Ø·)Ú1Ù[Ä™N6#³EꆯK å+ÑÅm¢úÐ[ôQï7ºÎÊÝJЭYX·"ÎtÝÊéwŒ\Ýé~ sú϶^¾J*÷¡ãDI8Wùq¢|0®àljb“¿âôæëÛMÚe~(ì1Ò+M+·†¿nw¤O÷ãÇÏè¶1¶0‚©NPü¯?vß.ÕÝ’ÛÄè6¯k×€ø©ËvšLû##Û¶jÚÒì’ѵ³PÜ„åYeÈÿ -b…êÙ , ¹‰/dóåkB'[vð [,ÈaëaбWµ†;Vóå‚JÌEmx Y&•“åëð|ômïÿp|cÞz<~Ó»÷>¶W/ü-ÿúå—_¦( `D>OI= ´™!Ýç»ø‚…j‹ÐÊ^F.»c`0¿Ú}â¸ðz.6©?ôh“:Ô…è•ß\Æ®7ºNÝ õº-²„NL^”•µ(yRÅjsÛÍúV‘öôíÓ±C©d¢µs۲ὔ)ÓÙ×·ví:1ß/Yº©NLLÕñãîŽ? ¿ºzªY$¥bÃwƒOY G›=Ï' x‰µûˆžÏa±•Ì•ˆœÊ„lÖ»i(’CÁ®ñZ`à@fob¬:¤ÈÁ^ÍÈ“«ÎÚ‰êÒ·Ù>)ïðÕ_C{Ìû,ʽ³ïÑãôÄYNÉ:æ…[‰yd3olÂaéYÀW3õ;a‘DtG/a—âv)X¯g …‹Ò9¶œ8S\Föa^ é ‹×•&Ä%D\6š°§ýHOr_]lDBð9¹¯£x/¸…tNYÔ†,Æ<©A^Éóª¤òj‰ì ³Yë‹¿bžv‚@ÞÉó®˜òx©e•Îå…EËÆ÷(/\G!Ê[¾x MPå‡ñù) / =ƒwf"%ûø+ö;ý–ûüÎUûT6{ÃïüÌî剡‰r¿…r­'P\5&‹Û(‹Š@¾n‡sï, /Uìjĵ߹b¿ÓqÝçw®²;EecÇõªƒrz)‰KÏâ¯õmÖ"4ßæ¸ C¾n‡|ï,€½*vþÖLĵ߹ÂYÅõΪE|qeØVD÷9ªÂ$ßæ¨ ¾nGuï,Òõy^z÷£Žt1aêb4tä–VrÁE”ÐÆª G²BqE]ù/),mMùfaç û6Ê`¡V½!®ïª]9¢ºî&žm€ÿv–3U °”f ¼³Ó³¸¿ÁEçëx*jÝ%¡˜ýéæg6'7m¼¹ºLèÒ=`³¼ÅÓ6`slBr¬›%¬jåèˆJ!Á1öüÙØ„äm¦€ª•íɃ˜ŒŽp$³¡ì`ïDGðø½Xž§\iGz,bÂÿïïªb sÀày²uB—l2¶kSSg•.OŽ ]îÅý}}< ºb.Eè"ˆD+tö¤ˆIƒÎßמ”,ßb.îÅ/ȘCœ94ˆFøO-ÒdÐùxúûªÙyŽÍ®˜ånë¿Ó½8c±ºN}´K+‰B1F£=o5a—TÂ7 ¢BñªªôcvÈž~ưH{BðÒ•®VYMìr5ƒ‹¾L`(Obö0V5Šø{$–ö²ÿÛ3ͫժ #AÊuü ³ò 𑛢™ÅœQ곫ìnµæ¦[í÷zaæïز÷?³ü-õ×ÿ ,Uh ÜVñûÿ²ÖŠ´ýïd¯ÿŸ½ú“de½RŸ]e?nŒö{0ójöƒìý¯,ÿGÖ¬ñÜZãÿ/k«ñÿŸ¥òÏÿW³ ñöš5þÿÂ5küÿµkÖÿ¯f¡_þ])J³B{ª3è# Qý!è-Þ‚Eƒ¨G­“ ‚¨û€ä›ʯÄ<\éõÁt,LË~§ÁçÕ Ä ïñ³¤B‹öÒ!HÙŶQyzk-’8è)z ¶۪Ã<ψɓÈO×ÍžJ¾†Å\èøDÚ¾k­¬ cà·Ú«[µÞÚgùŸ ìxfk¶!#âa(xŒDQ¸;Ö„IQƒ ¾@wÑÕ Í÷)(ω¶c3ûw®6ÅëC kh—N´¬îÜFÙFꌀ+ÍS:/9 Vzhzâ †R4ß#J¾ØkŽŠ.IFqÜr J Ãs±ßà#ùà’s v/" D×1t ìÞ°¢¸FƒiDpÂw4Ö_‚)ÕéÛÏ)ýKšDÐ`€Rp¤¹’àx_x»mƒ’¿@¹P¶4 Þiƺ«+ ¥[|?»‚Êm—:Àlº| ^¦4ô@èÏäDú¨Rñ‹¨3J¸pa,¯÷1‰L:¨RoZÚëÁP“Þÿó³ý `+ì‚“´<ªô<ÝÐB|žæ¿0•NwZÊ6.nÖ»  BÑVTSá‹íb„IbKwxK*)ŸÍµU<ƒ(Âög¶¿Il”Šæ=~° ~ýŽL¿¤>À‡þJ×CóÁgC6´·ëð×’å!ˆÝi®_ƒÄ,I[žo½µ| k”z$ûá‰6EΓ+ç{v?èµéIºAíÃJ¾¢tÛ“`NñbÐXú¢àööN]]•N‡5E/Mnªd“Ö gîÿ#Ì¢U̱cT Ó¬ä”WeéÇQ›‰h 4èu¦ò¢Þ~3 Ýö bú'ýPÕb·3ôopñyuÕ@«Ðøñ׃ÿ† Dš ãéheM5KiCšûnÀIWB•}¤éKŒäáuå‰ÌÚ$Ñ{1ä¢É™6ƒ‰¨„HâÒãkþ¢76\÷`+É£ÓhÔƒlX£éxÚ~F·ÂHKHYå®S…+yÉ$FU}2+-Ì#'µhôöÒi½Ñ@€–¡¥+ýȵwáæ÷BþIðµææÂqZþ„@z“n¨?bÖû~Áîý}éßÖ®t¹JµËoHu$/!¢ZB˜€Ð? Ù¨ ¦¶¢3iØWÚ¥—‡å#1ÈÐaðZ; v=`…å};þ ›nŸµ’/dÝ»ñ¡ôm(lý"ƒ”z.„.¦Ké©u#êý /Óà·­?ü°•–뱇ZaËɺÐèmë@ ”}ã®ï‘ô“'>ÅLtØÚ”öôˆ½‰à:f»0d /Ê{%î‰åF£5±´8½w6=5Á¼†S¸·ükzgql…=p ®Ãéº@eÓ]JÈ&xîwþÝÀ>0ŠvETãUœf%¢òçæT^«·17¢ ì Æ2€rÒSëÇA$TÚN½¨ Z‘’¬ 3à Ù-šÒ úÔ§¹Ô‹ 0~åɪà5“‘“L‚Cá&=ÿrÜ çŠÐL¦'~…ŽôÕƒ]ôïß ÎUzöÿ2bün8E£Ýü ý ~‚ ô'e<™!XV«ÑoéB¥ð1¾|©Ü.h<¸ìÔiÑÔl’{’ÐcCÂ*'Œºxž(Ã<›7_ "ípÒ~ò᪙†ÁímÛè!¨[ï`èκ›Wz½pÖ­­#ûÙQDjñ˜¬:s€ÞÂ¥%K^:s¤eŽ4K‘Ô—÷3‡Kå*¤*Ù° í ‡óÔ8š-¤Zá#®Ð/ÉVòãæNߎ·‹ëŠ+ˆ“ŽÒaêõ&®ç@£W ÍÍWs‡®zShý Ò‡Ôó£‹Á¼HŽíÕæ/ÁmÚÐpS,;ŠvÁ‡] É hJèUº :¢·/ ‡è× Áòá°Ú–·èϯ_ `·‹‚˜Ÿ&Ã…H‚Qžž‚ÇÌ`;s›³‰¡'ãÀ…hC© i-}ž×—ƒ&M·AÐâkGј›æ´â|©© œøA—*6˜r8úñ¡!{SSô™`„ºç¶…O·Å@߬[ ‡•õ[7é.hNa+õ4ÿt$4V©Ð5? ,* B´£WbðÀÉKÃ+\Ž oð¶÷MdÛˆ%4Ž_›:â!òýƒ+T¡lTž€mÃ>ƒµØ+Ðí-]ÊCiÐþ¦ï@K:ÿFçCê«H ƒèAˆ…™t}_ÚŒ~'y ¸ÓºkƒýØ+Öo„2vŽv€]1Öb[4Ø£¢‘ëêGe—g\¯Í ‘w¶¸féz8FËÀàŠê¨o»#°;déKóCèupBjª¿]øTzÞý"öêÛþô¦ò|iÉ©¶AâŒÁ'è ©ëi5ê p/}ë¦mè\¥£/AS8 ǛЌAtt ")žïHmHµ½fÍí+¤Ûµ IÄÉX‹Føz¯(ÖËUe-±õel] Z2[·ö覥pÞ|L¯Û½ Æ€§\r.®Ñßé)­Ý‘*ô[ºyºØFI/áý}.‹ý†¼Ѭäœyk%î1…&ÃvóëK–ê {»Õ¬4‡¼…ú€zœ º:Îlü>K𬢩¼ÔþfÝå£Û°‚XëLc€ÈŽwhXÖÑ|²ü+ÛóW—ëÏúKîEwϤ‘eŽA(D¡CØOk¡‘£¥wŠNgÔnéY ‹„müéUéP‚¶³áÕŽo¦bë>ƈ‰4™ƒ±ý:)ç‰YZ¤ìm¼V½+¾°÷½µfQòÑHhÐÿÇl0GËÑì$?-uãéâ‹fôÛ9ÐØðj1øœš;ø ÙÆõ6ÄÅŽ_61®iÙâ™ip·ßÍãWŽùâ`HX°Ʊå?«nXÚ4nkôQg­¥¶‘Œ®™î½Í[xöeÄ/Þõöö­gmqè’G—,yôhí;liõÙÂbÚ„HtŽ·M ;g/§™K;7Ÿ2¿këG ¾:š­ ŸìlUDÑÜÔ£x9ÖcÌÛG£Å¾*I¡¥w@Oذ¡,OX·®?½ `Ù ðÝÁ²ðPj–·î_É– _î‚íã–Ó k?ë1zlJÒÚœ›H'>¬qå®D8ŒS;×bx´Ià?ñÌOËl’‚¡2BEl“nì¿V—ЧpîÓU^¡¼èócäí9b,‚U°’¦æmø $èëH-åíHWÑ·-ö?|ÎÚqïˆss· m8;âzW°Í:YÒcåèCÍûÀ !¯î§A{eÇ!ñ8²ÓÒx¸tñ½žK¡Ôšï`„Ï3˜í¥X>^°cøT³](ŠAÇô(jL’Þâ‰N,^D–T­€¥OöƒäÛ \‹¿|¹¤½ øÀðá¬Ysêweá‡þ†?v‡Ž]·Ì>V;„2Ô îÜÜÈTÉ:µúc¨¨Í&~½:x1q"=Aï@„+ë¡Ô ?Òë¢++yPè_pî͘1ƒüBðÁ¥×bD¾â^Þõ´œcïT¨ 7èè¯öÓœlµ¤k£_7¡2À¼® zµ'_>ñGD²ž0kv°ÖÁaI ëÕ%/ͨqëè/¹ÞC}Ç.¡UÁ“^Ÿ¾Ì´ €ïi¦À‡â,¡×èçôÀDåÖ”å´ÏÊ×â^zX¡ß¯…7 @.üü3УP,_вšwš)L,¼þ‘´¢Ænö Œ6àÔ„ Ù`%Syˆ2‹z€ˆâÅæÿ©Ï€APe#‘J6öŸÝŽÞ#Ö¥~‡Ÿ:Ð.VlHö¶Øß"]ª(Úš¯§•7mÚÔ°'änj¢|ókÖ¹s¾‡px—[“„(½ûѳ(+ Ñ4…0~h–·Àzäz"ŠÑÌ«å$ù˜‚Ìm0ÙPQA‘舣º5 ¨F2MÃFiâçIe¡¿D»xÆ4ìx º‘JòbtÔÂ!_|¾Œ.‡šA+}¿êÜ,Ð,²öE9ÿ¨äzn4×Ї»7ð¤•i€§'üÖ}–›kJ4ÝM³¾;øËú¶â¢>=ª>Oá@õfC·T9+cêô/ÀÿR^Ih-G”êð ]àæÆ¸ÁÊ0KSìm?ÃQ¥c`› ÀH¤çIR2ÉEÚžvÞlzõè?ý4eçÛhuØîýbnÑ[ßqçdu>k[ª ÅG“øÓ§éÈÓ§É£`WÏÁ¶1@—TèZAð@‚Kñžñ‡pñaG+f½[)Áø©*nÈ| Ù9Ÿ˜½S:3Té€>ÊDðo”~Gš|À úÖôOòÑúÈG4i‘à‚Þ}ñB.ýÛïƒ×úÒ·#çÛ¾}“ê‘‚ “ÑÂÏÑü¾bP¶†ùefùä~@þHzÊ-\˜Wíchq³Ž¡(ñ~ÌĽԙ+=ãÔϤCh²1ËïõÝ;'£³ªA?˜ŽÖ“MS`Ý{ô1x‹ ¯ïÞ„É´¿¾•Ãçrÿ'ø¤/>¼ø|+}›ÛíãD6Šë \þMÛ«÷w©ŠXü!*mSEîÊç†é#àãú(‚KléTÊþq9Ôò®ñ?bñ êFïœìbÓkz6ÕÅZ ¦-ñàâïæ¾¡s6}[!S‰æŒÕ§¯éIÅ2ƒöeQ` &” ~°¾¨õ¢ŠŠ¥»îPºš=ï%}ë„:¡–úTÑ!ª¢ 8åóàýT!ñ²/?Di”æ²÷`3„2L‘ aiu Ë#?úûy¨ ­{t úyúÒï´ér&Ù<¹˜’èyz ¼!FÓo/\€¶t<=KoB XˆÛ½{L\¢kuŽ5ð¿ðÚ+ªí,U+«ÿÄþ3«·$¿6ÄŽ?äÀùÿ„ ›G¡Šó¿i‰.¨F¹8¬ÿ%Xàß1Œ™Õ¡v˜G;Ñšÿ ­ÃÒþ 7î4¼ÿ$ Àc°ð?æUe`ÑQó™/Ñ$â8CiØçr~çv–GûÑF~[}@~L¦¤¹Ív[Öº¯g›–³[Ô[ a¬òŒb«+÷îÑ'à#öØ9ƒ…òMø?Ђ¨¢µ7Â!ð"'CTíþ~Ö<ç¶þ $•mŒ}ñ¶²Êüÿ˜Žß¦ößÄÈUC{×üŸéq*ª*,âÓ€µþÛÕä³ÃÍ[ëãåµ…Bµÿ":­ |ÿ•G‹Ðè@Æyî’+ΓiM†ÿ–@ò_Ó·oŸþ üW ÎÓô¿þ‚o_ü+•\*êü½\JUÄK`Y×òAñU ät4œ]m\Z>þåLõ0EÝ_q‹NøÊ[¸×¯Œ±jLÌýððûᯟ鞅ã$±@™8ì”'¼ˆ -2 ¼UµÛ¦tpó ›P~d+“¦]õ" SãiWrbiC0“¥C¶Ö­¹ ¡y¬wHÃ.:o_O.ýk\Œþ®åLñÁ…ã|q#ð-°Á¨'SŠ}å¥5îu/UVåèuÌì Å…¥_”ηҪ°³áda£C-…é·—µx'¯©FÝAœ^èLˆÛ×|¶²rjpð’–-Xü¾PO³U~)´Eô…ãë°»^,0íû‰ªBÁ¶]þýôYXn-'¶Jj’ÐphÝZժͯ{d]Ú¨ŸSS:÷éÞµkçñ †‡n‰Ýß}ÀŽÆÍä”ö«ftéX„)aQõDE঎JzÏŠÁmbq9¬d¥ES—µo¯÷À]¥üC+·jbªè×tüºæIÅÊ”›Ò©¤¦¤Çá¶ù'•¡ZXlml4’ hû±Ý‰úòÆ1´<¢µ>ƒc·5—zô=|¨hV’˜aÏÉ6ºÞ†cÏÒÚšKK†Ý«‡JEM „,0SUpžöáˆÙ+e”nQ9°[dCy•4pÀÉöu«·Ñ4&ÑÅ<ÜÃM‰kR²DÇT‚\±)ÎSD1T k4’ €|ð»ÃYx~–¶!5iÊ"eìâᤆ/½GªËsÿÄO%ø†v’/Ÿ°ý-hx8Šæ|ç‡'ï›g##=<%“ô(+\õ–( ~³;€¬Ve—àS¤û£Üƒ”¾ÌS&CÆÓZK­§WšCLçÎÕ9wN¾|p˃é“×É%MK†kIºrS$åÒS"O_ÿæƒú £Î$á1ªÊ·³è~®…A ³PÀÀé`øEˆÒë‚¶ÂBÚ“¶T˜ ]ÿZ¹ò äøíÛòeúKGÓ…ŒëO¤éŸp\Ÿ/€4¸L œñϸÝÕ]œ<üœý¡9ô‚4B)Åö‘·yøèJX­Ê}«2ت¾.=ã;tøîQÇ«,ß)ɤô'­”ïi?¨’ƒ‘Ïø[î „ý…éüð6߈֬W­Ñ±ûÓ~¼„äz´OŒ›ì-[ÇPÛ¤¦LùaΘnI´µcï'êØöik]ºÔ† ¥¤ûÅæÕŠ * ¬|Ïçbè«n÷Ìë*5ñ.õ=ÝCø¾Ò]Èþ¢à¶Fâ §ýH22¼Þju0Ÿû'þ.!ü…´;iË•äÉ^õÔ2ÛÇ÷õ:ÉÈÀAhRFÝôdœ½GÇpcË+«[\ǧ²«κð°*GM‚RÏŠvÆT1²€'§ CWØV‹sÛÓÖûfÍXÛoüèN–]•áCæAU¸&Ô×q“SÞWìµÜÙV¹?Ï- G±6éÏ-^ÇÈG”&Õãָ׳oùíÔˆvÔàÏüÜ«ôBNn™y ’;Û~òF%•XfñÎåë¨Yw5X,ܨ#Ôs ÞhÂZ‘i ,½Û61jîÝ¿o˜²oßÞF}@×§}Ù§W`ÀÒ¥äS¹¥KËåÁ€ —éJúÍ¥‹/°À/äÎ —®†‡çõ–ʘw*Ëv˜¥È¼ïËî$]w*¯Q>éù7¥òAÏÛ@Ž·=:m”Žù0{áÖ|Oõ¦`ŽÄÎo¢ €–Öô¬þÖ[–ÙoórÑýGzlUßùýrNÑ_©æÝ†¾˜Ö?gxÃq9´=ÍYR¡Tðnšß<¬?û}oû€¡SŸô ôÿ&?ï>jÁ\er@©ª?Á $EH•7Û+²‚RˆdÐIÛAy§òœPž9 p¶<³nAAÿÉO ,ÁWgQÂ~€Ö'˜z²î;á±'¤ãÉ`Ì› ƶ@/œ±0æÐ,¥;Í’ÚÓ‡ã»@g1Öv.OåÄV[äË‹›ë´=6WE´©²‹ÊO ˜®x5.ç'©Ýp ®[•ôŒÚ<é ¿ÙV8—MüU-38þš‡Á¾¥£Pÿ$ݛɿèÁ6È©W+JGñSLÆØ›•WâÜ,‡öÁ’¤q”gW…‡ DL9n[CGC6 ƒ‰]h<6é4:Ê ;í+?’pe¢í[Ž¿ÒP96ä9‹ó’*D”© «à~ŒÛ—¨—02iøI8“#ÈaÛ+¬1Oð³ç2•$åk5‚ž4Çb)£m öÌÎ&2è bHÕ‚ÂAÞ$ˆd#;ÄVÖ³S¼¥ª k7M·ÒÍu÷&±R熲RÃoWG³-E7Ÿ‹ªÚ±K­š´G½ô/Ș‹“};¤+m‰¿2ÎGª1‹#5„…›ž()¿KÍ7º ŠGéÇ …óÑ¢ ØGtç÷¡¬)£¶µ]Ï[zf¥À;!Îèˆó¡9 "äóð!t«äÑݤ+×ðv·µž ïüJ­mÚÌ»d§kKùi< °XO†¬_;ÀöE@탛Š×s³äj{õ·|Åøñ¢:6IiH˜‰í„cââíƒ3\£½ù0ëíôˆš­í  á†‚&qU|ƒ¾â’×kÕºqõ­˜öEàlk` Ô’à5M&k9_wO[ƒÎà/~VÊ<öéà A“°RYFþ1¬\"œ gñ4§[ bª(Ãܽk0™tC&+ãP":•ônÖtm)q'éøîYAˆÚnyÓÁ-jˆ“j'_«±E‰ãǯX7¤g/Ad]jéK‰AðåSäFÃíå8™ Õæ½‡ÁÈOu—¾T¦CÓN׎ô=ò9}MO@ÛÓq8Ö‹€xfC\,‘h¯CC+@upÔ£×ÿ†_ÐôÏYpqÿ#½:Äï¡DÜø$6²‚w bxÅeTXˆ¾G•S¼=àMókÖ€‰X‚·à0"y¯¼É-´úœ¼R\§úÕ§[kDϵZm¥Ä‡VëÞ„$Abak¤—¼°˜„*Bmµ}õŽòp„a(ÊÆ¨ç‡3‰žª*ð¸TM0æ1€×o¶>< C¡7¨+ôì œ‘C…ª…­T̵ÕΙ! bÿܯƲ  ‚ଷ…òX âpf¼âdn¼ÊáBf+²Írc.­’ܯÖ[=Œ ‰ü 6:‚¼‘3Œ’àêÒhÆg‘¶ÃS~ƒMŽ»tƒf}þùØt‹T¢Xñžû•ù$öPûš°3w»oµG}M!çx@DÄùPH$âd­½9aA÷´¬_7eåîÝM¡Ý‚ñžâ"âûä4 ˜•&‘âø’ _µX`ÞŸÑšìAÀ˜Ö08°~йQHí×·ªõ 4C‹V­;gg{Kyçv$¸‡5~m «¶XÊÊ›q%5U»ÿ‰@¸ÂòǺ6{ ™8CiLÎ/TúÒ-Ö)öXo”L*NÞÌxgé)§Žõ´˜–ÙÐÏ Fçe ®vH#ó”¿v¶n-â{Áü•ÃÜvHa¹íRS¯H£óâ–u´ £Æe U$TbEöØïkÂùƒSj.Ð\¡Ÿ{T }íŸðKžø•1CÚ«ª$÷Ôoì$+nì0§Â6(NÕ‡¾† …~;L )ÿДÌÜ”¼þ›a˜­›Þ3$Éy>T1~*—É“T­y ÞlRþƒ>Ìc¢©±ÐÆ[FXð¿5"xÀ€`¸n 0 DJ{ÿ¤‚¼«ÅЉ{г}.(:–Ž [Ûæ–7ƆTµ°™ÕÂ…"˜ñvIm™x0½s~5Lp€n-Ü4‘iŽZØöód2¦P-¼^Ù¤qYïl˜`¯„yÃ4^é/7Î͆ 1ÇY ”´¢õ’ ÕIlDQ´^*p‹µJŒ£*Âb2Õ®ŽÖª{Þ½çªvlárc[ãÂ-&&|)ùŠ`‰”s„Ä@ÚÊN~Š‚Ù3Éß=‚þ [U3´bxQ;‰EýP¾ˆ#Ü0v3ðJ¿ÈùYQE&lÄ‚¸?OÉK¿½‚K7 ±¶ö§fs™ÄJU&Z2j4ºøÙʽÆÏ‰˜=Œ…؈KéÔ@Ë–o&ò0â;wŸÚú‡YÊë,µbÓZ×Õù|:¸%:ؽbt½þ 6]½cbó•u•¿J_·# ÜÕÛ&×7¹W]ž±æ ÙÌOÄty¹çËÄÂIj¼ zçÁFG j8WQœ)l–ÃMÓïX¼éÞ‰£Ä|[ôwÍåšeU§6·bˆé¡VHà=òr})ÕQDyT²bõ©½—g¯²x|px§Q±Þ”ßh/¢ÀŠ($|ÆJ(é£d%T\dë©–P²F c%”lk¥-ˆðýîøYh;®¦@t„ŽVšB/ÙkK°ÇƒMÍ+þ*Á"ñc6úþGÅÀ(þñrÊ+[¼Ðwðð4øÀJoМ?^Ñ*@”Ÿâ‡Ÿ¼•ìT?°eäÒüãpÙø•’HWA;úq] èzÞíídò ³¡Ê`ˆú"ãËÙуqÍ!„W¾[ëÐô!òÛ:õ¡ž-玚7/¹U%Ñ–wc±8ÃjûJì•WýDšµ^[—Œ‚ëÂÁ•pèź.‘ÈtŠ2l™f›*þ¡ 'SóÞ©J†,+jÁ*²€ ö³%u¦hLó‰ž6ñ´3«èx.oæÏÍÎüÓy:È™6ñt°óýéüýçûµøó <ý• 7,­û‰¥Ù\ÌãÏ+9ó÷æéÊNøj:Ô‘Ö”ãé*Îüž®êÄãs¾?“?¯¦>§oíÏÃï'ðçžž‚mÇ*þ<‚¥g€`:’?ߊð+ðtOÈ3š§q´¨]ÇÓÕyº¶“ÿž á=âéšüy >Wù©ÅÓl°Ê¤ÊÓ{0Qø0]§íã L×w¤¥vüygz Þ§Q*ä-Þˆ¥y½ßŽ?oÌÓ΃?„&<}H`$OÇqzoÐ${º©3ÿ2žŽwä'5XZwÊñ\[†Ão^Ÿ§[lÄÓ-Õç<Ð'K·z¾ÖŽ÷Å8žþŒ¥ çéhDD^ŽéDžÞ„ð“ùó$‡üQ©˜nÃÒ¼úˆäédžF©K#yº­šŸ&Ùó·ãiÌ%Þàéö<a:ÂÏÀY"-ŸrõãQ2uìÃæ4±)aã¹ðh`ó~oÀ¾¡nô-ùùÌ«%-""ÍB÷à¤âK:X)AÃLò§Í4.ÁåÙaîî¹×ÜÝ¥¿3hešÁŽÝB„l/òIbG´alœåH3‰Êéqy7!Ž.†ö)€ÜÙl†¯±µ1soÆ—— ¡:V£ ör‚YËWD@7F‰ýÒ‚ŒŽð)k×ó@€ ©åãûÔï"¶k0þ<}âŽ?¡\X«4—„ä=ËkœÒwÑ!+œ];j¤Ô𕬷îù‰Þ’jô|}¦KÖÂ9´B>érüÖ/@ú—¡Ô#1BEV.åòÁ‘|ä­çÃøÍö‹ŒâDi´¬±•"yx[?¨ƒ­1©ôÃ%ãÑÌn-¢êž¹T+¼fü²kþ¿ÅgŽµŠž;3´Mk™¸ÞÅëëÈ7« p‹qé{~ë¨Lc¹c·^ÍM|Ügf YHK•™œ%ˆL¹R6­FЫNç|žÇ&äÑš¾=-ž¤—h+ø^„yß}G(·™”¥C6Î]ìõ Œ-Ìåó²¯ò¢y#³½ïÝ«^ÄöÝÂÀ¨l¯×;&Pb/%Ή=™ti§îÒ±/œøÍ‰k ÀáÄQ½IŸæ]óûÌÄìáÃ}£!ö;Ⱦz?Cµ“ÖyÙX†™y#ý‰B l>¯oL`fïÙy3òFù½Ýo'ÙîÒÙ~!™D¦‘(™±^5^Ø%åGì ß«·1 ¼Fn%«ä…Î ?A~D•w¸›ƒ¤0ÿ ³Õx´Ðç ÈDFƒ—‘uä.²‰l#„»:퉃‘¤£ç&¯Ù@ü0I×÷\[ü`ísŒ¨ª¾ÿlÊŽöÙX…«;)Ï×÷úÊ∯fŸo”­©~{úâËYÝà¡ØZÎ{ߣ¥ç õJêûýÞØ¡êÑs¤0¾íØlúlõì8 5úófœÖýGúï,’¯.’TX¶=÷ª^CãHv…˜"Z•rÕªdtéPÿ«ËÖ©+s•YäX8ðâ˜îêcúd&nëI£‘ñÂ…–dç_ñÅ7¥ŸÊÑŠìÖ;Ê’€û‰£éЉxõÆÇI¼¤ÃÊÔF>Jn!†Ñ“_ê¹1µ_/¯üsãL‹är'òëÉf²ì–{ûåàL’ Ô}¹œ,!kÈ>òNß[#±†Âßüt£Úf‚ ‹Ó&¸TÁ7‡HÁLÄ÷ÌDÑn¿aa¢Zœ2{fVr§ÃL3ì€ìITã“RjBÄêØ–ßùüŸ,NI‡÷ÓJé$¿'oÓ,‹5SÀe±Æ¨/›x?9‚‡˜g²>ösÞÔrag˜ÐrK†y —L" ˆŸ{ëÅè‡ÿã3L•ç×Ým½3L“Ø}ùô÷êìR»{ÇÇâ·Ÿ ö>gfõü¦³ öü·Ï=Ý×wÛÛOìȨë·ƒ8Է醘ö-'*©îóìÓ0»Œ“³çº–j—vª—Ö„qr‹ Þ"úmAaµ‚ØVáóÎw5Ã]ªáz=¤š‚—\ÎÅ'î«9颥p¡E©=w«žƒ¦zÿ´M‘p‘”œÝÄÉzÔñAçh”{lälÂf+€'ÆNQtT \†8@ý¤…Î>Q ÔCv”©'õ6ãÑB  ñgˆKŸ½ÉKÖ?LG+Ôsu¬¼–ö9øû0ÊŽöÙTOYáÞÂÓ eœ›1±c¨9¶;9ïuþ –$ÝüR=îÅ+’sç‘.· ©ÝRƒKûkdcå28ø´ëA>ÈaIÒÍ/52l‘….‰Ïågãý‘œ+ØjU Ì/t?q4í1Áo|œÄ+:,L`~)?zò+=Œ©ôy¢ü(w?"Àºç—rìíøÝÌìÑ¡òӄᙺÜù¥†5ÃJ\cšU'…òÜ5ÖlR«×}…Úî;‚ÓHíß…ãx+‰îƒÌ@ÿ”?JäÛà}Z'ÃõÓ8 Q\t¼ i~©S” $$ôÛ’gŒ®0U‡N£às|›CBDM'*Þ:‹¼F àÚòz¢‘Ü’ÁæHfë8'€Ï7›ÚæϨ¥†zëú,5õü{Zæ·™„Ëú©Ò|PÓ[WhÀèÛ5Zy5â“~nb™ø ¤âI둒0ÈôŒ]Gl„ô;²†¦!J]¦Öœ tj}InD1ýŽ sEÏ(ÎïÅófجƒL凞+?'ŽmBžD˜7$fúA¦éÈž_>+g9µÎ¼Ð4&¹;,¹ëh©…S/f– j]” ¼C’ ™¦<>“UAºÐdŽFH#¾Þÿ¢iÊc0È4 º ¦¿ýåôö‚{ªFÆÀQ×ÍðA·ØÀdN[8-¦€³¼ö,H‡á SD5•Tû´AŸ-ÚO£Üþ~:c¯ãÀpæŒxK]ZKóÛÖ˜à2à.=È›Zîú~ÌœZîày^æÎ1Å÷ôÍO©5‡­Þ&ŸzÀ‚ÁSiéH8?HsQdÞ§€œ!€“,ë}:h+9Ä4Ô3r؉ݔòfô§Á%\Q [°ÚëOÓE$YÔb¨Š4ãù*Öº‰"ínÞ©ÿÁù"¦Ý—‘¸7jòÎ,õN¸O˜==ècbÜmòbL=ðI°>"-5èš™–û4k °ÂEù¹Ëät†ô#Z_²¾lÅCÊúb 2« \ò4)b<¦ÔÔ‡ù¬IëÏó”ó$¡ùkBF¿g¾6‘ŸúŒï4Mr›&Á”Ò$!®G"@=€÷â^ýŒZš„7>ôŸ¶Ì žÓÊݶùƒÊ´Œ3¾(ö"r¸‹mçÝ:MÈSŠ(ˆÖÈ,Bꛚoã€ù Є"L©ÀÝe‹C s`:`n\› "M2O)ï7X[ø Ÿø…u1Å#R*M;’bRFj¡#Hw¬jsÁòH8ÿ1¾°=ß^FƒãòÝAÇO'ÑÏèï¶N>»æÞš³'·~w´‡ÖÄ ’¬²5l¤C^¾Ÿž»~ªò…©ëíyÊÚ±¡Æ?=йbEçj ¥w:šÕÊq¥É6¼ûk¿øq$Y±›­êHy‰„Ç \îH--+e@]H/èAúdìhAmYÚ^5sö†ª='•ÏWb½é®ûØMçTÒ’&_ ~b̘õ­Û”?Ï®ñìlš›ÃN;.r6sªœyx!°/’\Åzj»ÿºEßøtÕšéáÛ¦´µ¯lm¾á†y o;û££‡ ]|rqN.HÓD2W¢nWnNÜ· †Êqcý¹¬ðÙAwß°I|‹M䌖2öOËœøŸÇJJKKnŸ:5slUõÔ=W]Õ2gÏŸjmý;Œ†ìÁpäÚk‘Høšk#a:INfVìûJFš5{ýºKsê·Üšáé¯I ?—i>ÒÓWmË”G7Ó™J:¡ÅÝÿ¶ý‰¸’L½„©ååæ’D %Drù¹½Œe„š¿^é¬K4{6—÷|%Y~ýu呈ò¯«ŠÆ?JϾâò¢ñW^Sij>x¨©9R~ñø¢¦ËKŠ‹Ç?^<Òk/º°aVóEÞ}áõÙ%¥3Þ\]½°¾¸$;»ºâ²ŽI쟎ƪÊÔ … '%þ¿ŒÜ(ùžµÌÂáÒC[‹”¿4 `"®hÄÓ"çâ;¦NÉ N¨š¶ûÉ9-{ž˜Ó²"òΖÅl-4EÂa4Hº~»$'+³¡áþñ£GÝ»þŽÙ+Wö@œ~léu= áºp€¨–Ô my¤×Ù8ÆÓDQJ„¦œúÜ‘Š22 öì,-++ÿÑq×Ñ£w=¼}»ró†í[Ð{ùÓÍÍÇO4³Noþäo^9õ›ßœzå7ÝÇm/ó¸î¯ƒõ}‰Èö^æc™î-ÄØì~fݤ‰»Jλj:z•Ç~u`ú´¹Wµ›ëE0;Œö¤ƒñ…@nû¦gÍ—W`3¡dQÕçŸ~æsU‹‹Ç//_½úk*–¡ÑøÏÀˆ?þèù?7=~ìXan1úÈh&ɬ8)î…M¯qÜÙÞxÉô2[uÚÖåð9>ðàÁC6·¬½1³iÞܦ¦¹óè~œÇÑŸþâ—?ýé/{æ¼å•ôvú‘ƒ•ÝoÂy ²&ÂÈe>Zæ¡Aê b«É:ºüQeýÒJû:4Ô”Ctž²J™L÷)mº¼ä’|r1)NÌq>]Í=TÛñ0J¢sDEíUÛjýk”ï)ßZã¯ÛÚ<¡"fK-$èn­qà⚃‡”›èÞ‡×\|`Æ$G'FH¨VOGØŒT|p"á FD¨^5£¼P/é-^@¨¬AûÍW8çôâë :Ñf㕃±'xá€p %T//!×ãýT ·ñ¢¼hi#ÃÂK½`Àùƒjf5i!KŒL• Œ½ èTŠ‹5\³Éuø` )D„ðB¼Pà©!3ðÛ^%ð#€ X—ñ"A](x?¡BÀø¤X/ @Pí°«øÆ£ÄÖ¾8ø9s©½W”°M-#”Áž·~œ0PŠqî‚táé5”˜ü$©4¾ T‹d“³™áçB‹=´˜cdÀ³ô^e©²XÙœšùÛÞ‹½{ßærebȾjL4Íî5Ãp„ˆåðª±3 ±Â¤âéÐ ,5‡0¸HsÉÅ¡Íú­·øU{ê4kÄ)64ê¡Åh"­_DÍeΚ7‚dú%ÌU?C|/!!ÆC ̃´7éê²RÒ¢Y çB£…¨-ô,lVB-™Í8}å;Žg¬ÐaQ’ª:y©„;‚+ÕCQè ž{´ªæ£È&0òKh¤”ÀŒJhžCj6¡h#°ÛFv‘¨ÖÚ&qI¡}%pÄVG¤„LÈñ bi]³Å“`£„úó©ßCý4ß0@y;éKÊ–]Ê…Uê-ôE¥8Ég‰2Cù¯2ƒú”ÿÒ/SŸ} °B—¶ OjHm¼cë,¸Ø#RêFÃþ>—í5ÚžãvË `ÈfSÕÌ£‘‘{½uÇJVÓŸ­.i8VçÝ;2òT´ªû™Ô´{V×wítÈ”áÂþ;—ÿãËïôï( ï>„:¿ß5Ëý†—ªN]%ä‚´¢4&²xG‘üVuz¡€Óä!ÆÉ“!¼£âa$ë;$ÖO|[õë:q¯pdé(‚ ã=Çòv5 d…x„H=>*x¢(ÿWã5Y$Z@à‚ÚM #’¼˜I“[pžHX焇ÄõˆMöì’x,Ä >€xƒœ ®Qƒ%MøWTôàd,„ÐÁë#° ÅÃÄÊe ¨ðNÜi¹# °¾ 3ŒŸx½¦z<›Ì9CŸSˆ¼m“e\É'àö7¹Z$[\ñUˆõ(E¼w|éÐ H?9'ѯ°ç;í!ößÿW÷ðMÙÿó¶heÙ–%Ë’ÜÕ,ɸ۲$÷†)Á‚1¶Á ÓS!‡}ô„r¡¤@¸„P.G€ $¡¥@H/Gz¸é`iüŸ]i…lÙ`üϯv½»ÓÞ{óWfgÖZÅ‚Ê̪ÀÜ}0Y‹¤8`'‹ÿY³öãƒÕ|pYƒŸ‡gðNÔ3MøõÃøµÃ‡Áv~Å3ñgxä¿ÿ ;À˜HßhÚÙÞÈí‘4¡`„T dÐ#¥pÞIÉ¡æP!î_ðr¼ÂýõræþQ¸û… A”XTŽë)É(àW “…ð"î‹Au[|ç:ÃàE( Žþ辂ø­Û‚Q8Bþõ‚ç‚Ûãþ ¯ÀËùì0š)¹ë.Rã\ŠSÿñTÍžjÛ[ëP±:°bת?X{^:4¡í Úk‹¹Ž@(3ÒPig¹C3yÚWl —tfÆ¿v ¿ÍüM`;ôb¨Æò À‘ÎMàE@ƒ‹’üˆKúvhCÕ© ew-tW;û›Xqˆ|wQ²0¨Žñl3G:÷JX'b©?:ö ’¢¤#\âE!JF6t7z!”íÈÒDHŒNÞQt¹³§Zß1Û"É*¢*-WĘ›h§"bo9‡ãUñ@yõd%eE£•p|z<­µ:Uñtx–Ö§ó9åÇrfNŸå@b<–x»é@Îi4©N4XÔWeãÊÊÆ5+SC•i°O¸Ñ*SB•©x†F¯Ñè/ ç­Ó†¦ÅQŠ¥!ù³fW9ú:3Joª©6bqßðÅZHÚ‘ÚÅá}VSsSi†³¯£jö¬ü¥ *(&-çÁŠäS«—ï¿eð’ÇÀ#ŒûP ´ip¨þÉ+Oêq¨Ú Ü—xd̃Ko¿gùêSÉtΚɓ×L^\^©œ#Ü|ä¹qV«Ã:^8_HÈ?ªò$mºŠ©Ík+=è´%š–ŒÞ0uû¸²bˈ‘1rš–ÇŒa).·}jÃðÑKL‰6烃*Ö6O­ÐµIð+E£ÆçJ(ÉÒÃϯXvûîÓNl½Ë]nnoÉîéË–Mß]²W§Sy´í®­$y÷íËV<x)îàöÆ TÉ2‚´a.4¥ Uh#z¹CøÞµñ]må»Ýë “9…« àÔrV'I³V"$ǃàT γթÑ1NbXÉ™G…5!¸ Â@ÂYô G:5&OhNÀDWV¬ds³Ÿ^ÞÐïž²\VY\ÎÆ‡FÈ ÒªÏ1§k!™iÙ”%>99\©7)Tªâ1…øw|ÿ^8¦X¥R˜ôÊðääx •–âФ›sôÖJE„Ƴá´ÄÓ6|»3©8÷å+§CÕ¹ ™!?ÙãTËÊòú¤˜l-¥q¶¬(­YYšèÜÛ·;K•fmT–-®´ÅfJé“W&+V9cìÉù™"Wj¬ÒèË­Ç[ezMÐS(=¿0Q©s>4õ.êlÓâ&ò?¦T­ÔÅE…Ÿ?Ë=O¨©¼¹Î=äܹµsÊ•QƒóFihyþà(eyÂÜÐ0Õû¨!Öˆà½2S–Tá$|'F›Tr£2*jè’ºéÓë– ŠRå*St"á×Y‘$ËTêC‚#¬CFÙ§¨Â†zÛ¤6ýs]ä ´}Ÿ”róÇ7IcBSÂrƒò¦VfO°³F„[#£¬êʸ˜¡Ccâ*ÕÖ¨Hkøˆ,£}BvåÔ¼ Ü°”ÐiÐMßœ’”‘8»¬±¿CmO/í_74!ƒMlmÿ²t»ÚÑ?Q?|AHtF¢‚ M)µá2™†‰Iúycóæ‡š7þœêƒeáP&ÃGäZˆÆŸNÞ~þs`ûdüi4Ä«ÃðQÙ•£JN¢cµ–°‚Õîñ Ÿ5Œw¯ÎWX´¬.DÅ)§É¦‡é›¢§Ï9ö>¾€ÿýþ¿æNž¯˜.ƒ5%))%5µ±±°c~aáü4TÄ›Rd¤L´d$Èøl³2®ïÄ”Œ© ¦àX†ÖDÆÊCèü'7§ÕÍÚúDÞqµúxÞ[cn/­Kk^œ\–Ò¯dAѯçÝwß¼¯-È­H)+¼cunã¼Òµ‘sVäiþ:14tâ_5Ay+æD®-ט»úް2ùðQiEiø÷U?¼‚ÉåðQò2Ì›]é.X0‰qÈŒ2…5kSЃh':Ô•a¤xð`@^SÙxðà_Nh‚2ƒÑ®SDyQÃ¥GÑð( ×3¤Zqx2ðgŽh¬4t  Á¨å?ãÀcM0µ_ùDZÌüd¨„…‰A:ª²¹²²ù~S¡ÉQD¦˜ãÉA… q{¤ÎœÂ/¤»>o\üzõ³ù;ðw÷ügýû#ããtU·.0K;sÀâ[«tqñ#ïxÁ9±A׸Fk\:† ËËÙ5}¾°nú®œ¼0nÌRcl¨tMc\”¢aâ;‘úPub2§ ³0Ò;Û¬°H‹n€–KNT‡ê)ONå”L•œüÇ•j}‚Ž?¨á9fÕº=9`ª'‡{ÇÈí£+F¾s;þäô¦ƒK×§dÜ–¹¬aF?»½ßŒ†e™·e¤¬_zp“m¿£u³d{KŠclñà†E3›ö8+*œ{šf.j\<Ö‘Ò²]²¹õ÷5V™oû[ººeѨêêQ‹ZÔé³ÅçU†q(ÈÏÙÓï ³Q.*Be¶1’$=cÎvˆ—¬Ÿ»Fû]ƒAôˆ¨¥‰Vš”³Ãibí& µxx~^ee^þpª¬²yÄrášÞÙõméìRBó!^ïváuÔþÑÛ[·WP!ckÝúºXº=8©’ÔäýýaG7ëäF7bŒ7@㿃“ÀŒ‡[ñ‡I+k¬ïÀ¿VFí(£¾Ââ¤îdaʶa`ð»æºd^—[úЊã+Z Gºûx/þÝ-ûÔ®Í.¥YþòËËGÌ›7Bü½ûüO ÊáÂ?8‚”^)ŒAw!„v–ð<3Zž-³À¡ªþ½Àˆà<ßy ‡xAG‚…æ=Ï\d1XœžœÙÄ2 ™H2œ z’‰$ Y­º€tuÂ4®Ø^qÁ=T”Éh¯à¤÷¢íEQ¢ÔøüÈxG….*<8é,¾Â ÿq6Y¥«pÄk ÆQ¾° bÌ’/µµOdú¦OðKøø…Mé‘}ZSù¼1[Öß\YëX0/¼ aog o» .FATŽÅ¡‹ÊÕã‹„¼ šøÜ(ÃbÖE©âcãÄî‹PE“\­©ò°W7]¸°ùEhj+É$] Ë‚ž ÈŽÁ§Ð[fé§£‹aØöÕuCÒ•¢È™2×(øU°nÕ¸Àñwí°ÿÃ#¶EsHùU㤋AW„ºV=`Ï™EøÓtгwêaoE×½Ûcþ¸þòQ©°YQ«ñ öÿ[ÉŽÒX€è˜ GLÛ¾?‰q&åžcrtŒ#Åd×È ·},A¡( ™…@YK¼?³UÍZ$öép™;À:ä•W†à÷Åïóî_{[ÆúzÓòeM¯ßrËëMË–7¹½Ñ^]M8:ÁMBaH‡’foB£„mÓNB'ãä½ Þ—±ˆ\¥ïârÒÓ~ƒ– `á² zÞ#Ö(½ù‰ã°{Ÿj½–÷ˆ{EÅÍŽM\ kWYãf›³žs»žËÂß‹ "ÿ¿‹ßûJ~^çÕîÃâ@Ç¡T<Ü ÷P±îÏDQ}ŽÙ‰bzñƒû>õÉÇO=õñ'pv@Še%¬²¦ ÌÍ:pÀïJä‚<7W~!ÉoàÏ­A[zøðÒÅã\øï¢ a¬kå4üöÛ‡r¼ñkSS¬8¸ D#¹ð76SQ®0È$t--kĪ+›•W%§¤ÎAú|–*ÍçCܯÈÐ4ðÙ}tüs(«½ìäó3ÞÆ‡:r —šÞžá."²Z *Á¤Me Ö7,ÂP ²¢," Ài÷~‡( TÆ×¶]«÷DáÀqhÁÙkñèc´xdQëR|ÙPq _ô ª¼.¸Ké…ªëÌÚÏ7¨ÚÛAŸ;‰s8üÚÖ*‹Ä´aÃôÈn#ƒ<ÊJ8ñU- ™˜úÎ> ^ÇóÒ:·®49c¼®â`Ķ,rží¶sYŰq:KBŒ"Ô@uaËxæúdLc¡¨SøKˆ=ùö£µ“˜ÀÖuW;ú&$Fj¥ª\V’Ö³~>d$\kH„h𻆞÷3ƒD8E¡ßhGSßwæ×ô¸§µ‚Õ€Fy>Q¨–„‰Ä¦fµ6âÇøç6ïר¸!ƒ•<ïÖªuûxtTdí )™¥¸>V¡&÷M…¦PÅêû[dPôµ# ¢pøu¹¿(Ä£rsqiчË+ÔQå ×gÊã ˆ“gêõP¥®(?üFQi±YŽ“®-÷ÎrEŒŸ”LÈ&x¦¢CoR Ο Á$áüø7w²“´Áëó_F6=~éqò¿é–@¥5;À V3ë4‘¿=òjPtìÑaµ«VÕòǰG;aylGGçGG]¸å×í´w’¤ýÔr¨²¼$ºëXæAØÙvvN;å%c×]×í/zE ùqïöRCåö›‚æq"E³Ð³ €ª ºæö&€Y--Ô Þ=ÿ÷o€hÎëû$Ëÿ¾žLñ ú蕇{*Ì/[Z`Á ióÒÕÎîC}ÖCæÕE!vC‡»„ö°œ‚ù^‹[E®‰_/Œg‰´S[<ŽMù.ÔG¿… мQ*â a¶Î ³ƒ{nh|ØwôưäÓÚ@kÒ›k¶Â v µ>—žœëZÚ›ؽ6,‚ö~ÐòòÖ Ø"tw7J‚À ´Qü®'¶XÅÏ}çÆ{ÉY¨“@>Á}ëu‡ ó%þ‰dÝ”Ý'8²ÿñþ±!F{Ž~"Ê{Â?ŽT,ÐÑãø*ñ=×™üŠÏôÕg¬I‰† r55‰Æ ñä·11wB½ ñDíÝθÐ$xeÕœVc»E[à„Ëë7¯)º »K×ÜìzöºÝ0Ó‚ІÎ]9:-môʹC‹Àtž”øYj#ê#h‰¡Ý{4F•4FGPÄ‘y¸ˆ’YJ‰#C…]UQÆÒäŸÚ×…C;ÄX ’ž«þ{DÝ;ó=b?pâé÷d3ÐKÇã{ÉTŠô1åOüúäôoþêàôŠÏXÿ^1Ý+ €yy‡EßQh(ºÕ£ih>º-CkдmC"4¾ýÇàeŒØôM«MJ‹Ui²l[MvoáV)ᔚNm"*‹d!·$‹ÿìpG9œ$H3â®kÄÕ­- Öhí¤m«š¬6$gÖ,áÈ59“kfCó [f «¸;¯"£X”\[¨(U{y¼¹<~úªœòädƒ±ÜhHN.ÏYÕWnŽ/wNIÌ2j# ºhÉý°ÞdÒCÐíÃf4Ü2¨ù/&M´Î©5Rº~²Á¨K¹Á˜éfÒG%ÜhHÐ1Ñ•ÓæÜ4lÆ ×EAà_ §Ø'›ÿòä§õÁMcJ ¶ p7R€ÁÇÈmIBçX›”†_>´tK'—v{¤Á©1é]2ØŽï'Í »iδÊ(½IC’`Ác [¡J”IвÄGÞgH=·%Õàæ‚Et~°wŠ-Ùý”—ç7ô êׇ~7Wõä*?õ…GûÝßÊýœ6¥wx—¢‚øsftÏãå.$À¿…¬õÑ„†&„ËØ«.?<0~î$¨ÍYaÁLˆ1B-KˆPÀÑž‡Õ]‰¤÷ë4˜îÁ:övàBž3ÄúV´±(©P!ªAÈLí|þÉ«Õ$ÛENÜ;‡¨®´?uÚiiª¹C8ûÍŸº ÍÏKãJôÒÄåÂ7hõé” ç7l8öêÕÓ­|Ù tþøÀ^˜ƒ/¼žE÷±Ø /׳帛Áƒ7ºž¹.cÝZoÁ×0¿‡þoƒÂf“ðáŸþ~oƒ|ìü½rÇ HÝò'¾Jótke/_ qÈÔ^ µóøôÎZèÐT´!dñ°¬÷þ6®Y³“õIžVJ™Í¿¼GzÐhãA­ å@Ï_ÑTñÓ¿üÈ9ÄñUôV>Œ~ÞóHãWÀ‡óŠ#Ìä¾§ùWÂ/BäH½ŸIÐ0¬z8/"÷MÇϩ¬Äðò£î„é:!ˆÖ½u`Uä²)…ù{£.‚do~á”e‘U[,÷Æ-¤¬yf‰¼¡OÎmÖ˜Ö&ܶ 5Æz[NŸ¹Äœg¥ÆÝkŠy4-&ãþe’’†„ØÂ”Y«?q/½°Úf‹”RäyÎý1tâˆ9„ˆäL¤ýü¤\ïçŸ.› Z°Ž˜*û÷S Å—÷]MßÇ3Çþ:A×”û„Ç&š¦ÿ<+Å\[PÜV¯ÚV\[bN™õótSb,þî‡Ü&]Â×Z̆1ùuUFUùü°¥9Ž ¹ìÀš V–g4- ›_®2VÕåébv¾`™Ç ßeösþ´~ZÇéçú?7wã8R×ÖôOŒk¼où¤ «›WO˜´ü¾ÆqdˆÎä_D÷?àø)…Þs}sýÑ]ߺvì¤åË'ñÇØµ­õ^¯p˜×ƒ‹ê亹…>¡,×2Ÿø¡*t1yõd ÌDÍhºËou†’wÎÕZÞù6©}‡ÒFþYZ;­ÚIRÉ}§VÞ!'¥;¸ì ^yªHv°Ù­Z›³*Õ¤°sjZB*S’'ꫳ%åñ‰¤<»Üó3vȆ'p͆ŒõÜ“‰Ä¬+ï"~¥ª*Eyyk6™åú£¼<&*;“¾:?±s5þ üœ’8™ ©­Â•ƒßEñŒ†EÕÕ‹fã û„' Þ b¦$¦à¸sÍ®ŽŽòhßZ*Ç}žN¿0‰1‚éî„…<ãBúÓ°]¥]€?¼×ÈLò¨àYÜ4‚b•hb„/S 0¨i›Ú`W™Ì]\ù´Ñ úš²ß`ÑåËxÑåï°6ðÊÕ*ˆŠÜ —“›6ÿ2n „ŒëôûÊÝ‚@èò^¿Gv²°Ü‚÷ôNÞ³v7ýÁˆÒñËK­Ÿ0kÖ„Y·óçÛqòõ§ÿ CâýŠÑ‰ây´ïÚ3±žáWëWÂoõ“ÌûÙbtcµw3´;à%~UB°×6ò/ò¼sµþ K)lä­$”0h z³CÅÒ”ÇÝ¥µ†€­ß0 v¾;ÛÚýíY.ïúŒéš{(ˆ8­ù´íügöݘÃOöpì&%`»û -äç+ѵ@$ž¸öý“ |‡‚‡ñ÷ð PG.\8‚ÛBA(øùŽ_ãP%ZB»ÐôOô/„wã£SX€ ÄtŒ>AØõH€ÿÎEïš<ßîÅ.÷:r=ØÒHNâ¦F‹Õ»­Ñœünq“€ C‡;à«VrŠL^HL¬¬¡/-W4RÝÜ¢Goo~ø6οÅ[ð¥|½þÑîñ añ³R9dkð‰pMýcüFÆ­“£ø5 ¤É¥øHºÚy{ Žl3Uªž¢©UÅ4õlq<Ú§¸MSTÇЙê̲Lu ÿ–í¤ïç%$Fg%'ôÑ·VÛæáêââlÊ•Æï‚t™ËôQåî]»úÎÁ#´Ú2x5¡FÝW3¨´¡xZ^ægdcãÜÏsìÅ“‹*Ô}Õ•Ù]l„|qH¿kmx,•sU£JŒü;êü–GºÄ¡/1«æÊ‡'ßگ߲XøøŠ«W˜æ‚9Ùý,sß$kÙĉA} ˆª ˆ.£h<ÚžCÇÑëè<ú!ðC‚þê¦W>ä“é«.DP'DˆÌ5È .I1dó@¹>¶Ä¦ƒ€1/Ä$pÃèâ^ xhD'ý´©Ÿh~ð—¤XV-“Q‘7·¦(m)±R…¬íàâsÛê °Î`)¤±)ÙŠ´e•®<@k꽕Ä;ðw<@ ŒÔ†Gç»î0;‹²o•Ö3µixˆž‰´È Ünkø´q¼{U¢!p‹0S˜F skv#!%;\™Ò|ô=÷ÑŽ6'E+¨/4eY­<ÚNy/E´¥$$´e´õ(*ɦïHLéjm’5%*¥ÕV`UÑpåÕ‰¶Ö”¨}]â´ÔaNÉVÆÑƒ¸rIð—§ÌN1; lISjDŽŸ™:Ž÷þj, M@[ÐNô,:†Þ¼ž’â!KݧNüpï>rçÕ äž'V‡gg¹ÿòkkµ™·Ï+.¶ýÏ‹Yóæ¸îüQqðÙ¤¾E㹌 Cbߩ۳É\MµKsúš†¤K³RóûNÚ–äÙeÞž›¢ êW1¤ŸŒúߪïé?aT~¿È`ƒö¦d}h¨$Ý–§5„Dõ-K²LXHG¨£B‚i;’tødÀ ´=ˆö\ësæÿòÿàù:Џ|ÞóÁ's–gAIשe;®õê…ÿÎMÿÔç²¼Ô´"~q~&U’Î/Øß‘šÇ¯Þ/L/¡2ùýEÈùoÙïë3Ê 3 :üFaYÒÀ1ŠÛË21&•¾qxРÌB$ó€æMâQ"r¢r4í¾Ár©ü xÕÿî/F€oY¾wï ä4döÿ…FÐߟZ•;Ûh?€Ý³&²`þoü|¾{Ubò@‡ý€ßçFä(YQ6j þÒt¡?"H¼v¬¼0A€!QK|ØðY)ÑH‘“?¤TþK!)ѪÒÓþ ŒP\?J(Ñ^ÑWqâ~«#2€±Á\µE´Óä´?ªó_ˆ|ߨ¢yµÄŽÒ;z “%³»‚IXÂâR(¬Ç>±^¨ÐªŽØàb¹e£DxHEÛ#õ$¬úÞ¤ÈjË+:o嘴Ô1+ç -„;‰‚ÐS‡F£}DåBGúߪfH6Îbä̽VhÃç“ñ:ÒÌrfzÚÿfM3€¬nª&+›ðš”ä@õªS'Ö€Vü&,2[äÑŸ;þw)Õ¤I ~=@ˆ^WQ…DOž'ú±>8éQ S-Z%Ìsžwöl»Çx9ùp- º˜\£…Õs`Q,1¡ƒxðÃiEnÈJðw¬Óáj„’«è“”Ý3DÏ¿:izinÑØûÆÚ¸0ý{:LÉýîéÜ´Æý¶R¯‰èÞˆäà«‹kÝGp½Øµÿ.|ò+ŒñC ”†GÇ+’ ²ð Y ½ê*®˜ÖåÏÊ‹ù7/)4m'¦&mRd\±üÙóy®<åoœØ¹Ó&žyô„_áóä뱯p|›ØëÉ‹‡·®Ü[ãCÈÈMSE|øy¡Ùh6ZD¼Ð´!øo4´oñŠ nÍ/n°‚–KúÑñK;¬TY»öÜYH\ï·|‚úà¿]‹˜8¸yÎ௿Þþ~î"4rxRºÞÂˆ•ëvñºßL©]x/®ÞË2ÏŽ³®ÂÓˆc„·öb ;Ãñ‹Ú²rùEm«,)çHÁ8J*)ª9Í|iás’?Ra5y#*ç®ô¼=GÐþfzUŽh$+¡Í Åù3JmzuÎFI8nE”ë]VÞÞ‡}_ðšÿ^Ê ×%”j[ËHç]]Ê$… .ee ÁOÉG~J9rìè‘• giûÑ¥3g xCGŽü”VÈAR<9P‰˜CIrDuYœÔ9²‹B%Êj¨¦ªÙjÉL˜IÍdgJ¤0lj“™ø¶Ó§ñ‚Ó§Ù÷OŸ>À=›ùÿÀþF8¹»DIv!º QW iFq\q”°òË™ŒÁ#Fs ö$Œ1’–Cø–¡e1Þ–nÉsrïyD/¡Ô”Ì”ƒêOUQ0™ Ca C:HGé ÏSH›°ó_ld׸ž#2Ä^DqhN‰A££U! Zª 9ƨŽÅH¯Ä+ qšªH®*(^qþÒùKÊ\òÃÓ[@n²Âµ¹JrdˆÔÆ ÔÆ£øNÔ’{Ïs/µÏ*‚uÑÔx° ŸñŒ“Ty£lÂç¤Ý.²."$!ñÎÉU …¹Zc˜>ñNjòHc>›kg¥Jêæ*bâCCl}$µóDÒ!DÒËIÿµD M!š¦Êü¹’N’ hP8$ °C_¸Ý G`Úr"sšÈœ&2§™«L´ûÕã¿âeR"õ7˜ïÚ3ÙËH‚æ•ÈÄRW8†’pŠóÇ=‚=~Užœ@‡¸N4‘{Ïs?˜w•¿DÅC „áAÇð0He‚ÁÊdåLîm‡ÞZþøãËÙËø·ýû;P¶´DF3€®p4 >ÊŽgy(#Ã%ˆP2þêÜ›X9®g?D4ðH}v°rø|±ÁbqÇdßn[ÂÜyEûNuõ–ŒÌ=ÌmK 0„(S‚QQ¾ÁO²RtHÂR •£þ! Wi5‹4þÅG¼-) V„–„ÞZ:3”å›U*É?»íØÁíÛ>û,S¥Ûqߎ(ü®¢ÒHåráCÔz{¶à‹èŠ6=Á“Ÿù¿8n¯Ô˜G3‹,á¸êãטê¶G7Ï,®…­4CÛÞH&ÑßHm +š^¤ œâqæÜ"¤L$)ÙžÔîI¡Æ )·yÊ(ö’¼7+8OY’²–Ôö¥ôV’òóUë«Í?åIfUÐŽ_O{¾±ŒÊ\I##(ØÁ…q~,'í|+”ÙE¨­Ûñ•©")<w’¡¬¯Ì(O˜CR_ÔÞ2ÚØª¶c6RšGRvzËE¢€ "mùHA4BJþåp >Ïø—­T®$‚ 9÷q‡_:N”ÈÁO)†ß:úHB”˜Lô,ú›¤…K¿ôEFIèUHÿ*@¡É5¿Pç_+‚õ“D´-Þ8qËÑwÚÛÑ<‡ f),èG„µ¹}BˆBÃÈiSíQNbAT•áÕ¥û—&àýÀÐZúAC’©v ®2J‘!–h áÏ ±&§°;]e ÷°ŸZú¦É>0k0þ裈̆Œô¸‹0ô“>µu&¼íÍ#Ã2þR›‘¹‘Ùݶ‚Œh´·ð\j2¡ËJím#”Ixú½Gð_a½çh’ŠýÐûQDÁ(¹D#£ÖýAÌz2ôIh+“ÇÉBç³.e)\äD4À¥¬_ˆPÈ€çŒØ”¦¿Q·:íÞvòûöζ»wîdîE{¨õl]Ìwllk;]¼sg{»ëÝÖNl!aSnFc#Ùö!!¯$”DKX†“IƒIË%AÅÁ3‚W¯ fÆ”›{ž?Îð®Õs$Èø,g²>*8“œÉjòlLmVÖ¾,á¨e/œ¨%?' “D麯r„ &%”6á¤k;înÄ?§6€â$~`)ói›ŽhJóé•·wâ*S¤lS-–5ñemBYæžçÝ_\zžŠùö þd½Äu'Síº›¾×•»Q(ù±û<·_jâñ 2(…KÞÆ{ÚÞÄ{¥¦_ŸJýí­NÙ<¹”C%“#ÜçeÃ~{+(õ×§ ÉVO²{j r²ÁPÉÏWž¥Ÿs%³k.¿ÈP®>RþÞ%k;x‹™Î¨ ¿»7²%Ò5<Ü ²4ž©#Z°¸VáE µ+ä‹wž‰=ysmȸ௧z¯%ß\:ôԫꃗ/ßé¹øŠT'(]£°(Þã¥x–"äy<œ<·*Þ“(IŠÕ“¢ðùí##9P±ïok‹Ôˆ‹ÝiïÌ›‡0•ð ^~XÜÖlòT$ƒ–Äá„æ93§L¯'±bü–- ·©ƒçú=TßW¿å¡9«*®Úìbü«®ª¸³ _Õã& á9ÃcûÞøxËüXœ+>üž2N~è÷-Œq=¤-«`C­-Ê=—z0²´áêmˆ…€† áèV4)€ƒ*€=® ö ݱKt‡S ½Ìwf™ŠU“ äAìÙÈá¼ nÛ›nÓû?U¹¿èR*/Q±F÷øg¯L˜áz˜ü…3 ^GRá’«7{MS¼b[Cqõ ŒšånðʱPn&²›Žf¡9è.Ôr]é‘ûž€ÅÐCéIé%l©85ŸC|ÜvuCc=ÅÜ_ô¬ دH]—;ö7FWUTS×eGô¼WzÔE¥Ýv“Gm\$ÝdCD©ÇS &râ…eõW!¼(Õ&»Bœ¡eš"GÔkï³ã/õø—!ø·úÅ3V?óI+[¶pͦ…›"=Ï8†pÓÇ8ú‰’–Ôá_ ¤nIÒcÓ*®\¼xd÷iB;Ó®-›¿íÃÍ µðãÅ#{NyuÊYB£)QBW;4ÛJñŸœP‰”óköH¢OíùB$™šîíJ÷®®jv葺<wMÝuõ—Ì·ðýé´©^ú½è}o r¼m½ ú½7’HrUÌ¿êèÑ|é¦?´¯®îÛ £ï&½$pã¯càÈ7G}Üœ¸ G…þܤÀgõ°:ନ‡Ì0™CŠtUëesë];+^Žú¦¾÷XϘy~|yáæù±óꋸOÆòûð1Ý3í—Ýçƒn&öJ‚Pðæ ØR& êšíš?eB hþíCÙ°.33\Û £WÒ+A×ö‹` ? J,á+îóÒÅ$³!У(b’HüÍlǧñà_¨9Ô9jÎ/x—âÍI¾„° ߆sþˆ‘¼5з‹ŠlÞŠlBE`‡¿¸—»ÓÜ˧ñ!(…:É¿þˆÁ9ø6x ^•|ùG1~B4ªt“aér‡œñ¦éy¸Qêˆpߘp{6?¡Œô ÿH©à$Þ4V‹RÁg`ÎÂ^\ ÿÀ¯¼´uûVòÿ¥Wà8½m¹vS6øüäÉÚI“jOžÄqî×p¬xÇÄºÛØ1#+·>†~ý)^¸mëˆ#*·=K/| KÛZ cNŸ8ùóøs|ñ§N½zæÌ«'ü4ûãÏ'_õc!ÊóÅÞÏ* jƒÒD܃R²ìÆ™8@·Ò›]uß¹êèÍÜÌ^òr ÑU÷#_Ãôæ?‡-U¿Ý kôŠ­%ÔJjåN÷ljå÷l÷ìÔJ÷ìÞó‡ò5íäkÚÁ×´“¯Ýã~ÉLjn™¿uÕMóóu±+i7xT†hlÕ¯^y¼fHÅwÂj“WUhGÖa ¾=ÝóàÿÓ{¤¾äõ1±Fîâ]é¼NðkÈê×PW4¨ÃËá^%n‚‡üTÌÁ÷(á~øÆyÕuãÏ£· æˆòµ˜‚ìb›Â[â^h¯mz|ºÞCIÄ{SÍ¥K]1±{-íN=†×ËyGÇÕ§¹¤‘ÛB»ÕÌW^ðiv¯ˆ¾!g‹ä¼Û6râ]¬@¢m„H­@µV”oÈ難 Õ/Þ2ŸôÏ=W‡jï½cͬÅEü³É:žÔ6Šò­É|üKý}É?¸0+D‚ß5ÞñÁÖ¦ V{ñȤAþr¥‘Á£]i>Rù3ü39x ¥ê! ¿£¤TXüå;ûy§kRhü‡…ÿúSü}}ot‡P!t$U}éÐÓ¯ðji«Áë~}†¿éÐlЧYÒIþ}¡è,"rr(, Vä¥èÄæý"_y)ÒåVQLFÑY£†dü¶û}¾ß,%Ã^¹âü±Gñ[sRB–ˆ²ÕßC>ñá¢ÉÞ®¤Oá®—ÈXXñ$žú‚Í£‰'vp ~ħÓq¸0ðh?¹… l‘ !MÏï‚¢E )Dðw-Mj/AÐ=†Ï9õe øJE™zѯá{Îê^[ûÀ÷4/øá ü>ÙŽüåNäÆ##² ä @VG¥AÛ”2 ð.¼¨½Ú¹jXK#‚¿¯¯{Æó@ãϸïOo~ÈäÃ9_•m/ãú_ŸÙ{’úÚþ³>Æà,˜A Oˆ–åvcÍ_ñYœÿ€Ñ®ídaúøÔ'¿ûLÁvVš‚KÙQ2¯Ã.|W¤-ÚÅcîÚ_xU&mz̆pšû''VŠêñCÍ_!Îáþøñ~Ì?Ë34è#¤AÑÝÎòðóqÔ.³ÂLÏÔFï5ÕÞö¤ä/L/ßéÕ:²ùŸ}†(ô7WÏæµ+·ÔÝUŽ2U•»¾ùüóÿéCpPùT+h™xÚTÓct˜[…á}jã¶±2®Â:vjÛ¶mÛ¶mÛ¶mÛïßÛµž*ßÑžýÿW}œ”Ì)ÓW)‹½”m®HÙ+a–”##p¯¥œ¨ƒûR®)wœ”'o¤¼«¤|ý¥ü¹pK*°¬)ŠMø"ý×J*%Þ/Yì”,[JV}q[². ηñC7<‘ìrƒ³ì«â½äÐ?%Ç#’ÓuÉùœäîèÊ[Üká¯ä±Oòìîâ兞؃§’wˆäÓ?$ßqRo°gщR±8/wÆ ©Ä–JÁÉožäŠ›Rw \"9³ƒI¥žIexW¹@pÐRXv|Â;ÙÁ‘ì5[*Ͻ*d–*FK•ŒTÙœUå(ø³ªÝ|€ùRl8¸s\|—âSÑw¤„ ì–Gb%N⥔ô¬I.„P´Ät)åš”ºGpÔ*Í eŒ¾Iï†%Øj•!ø£žT«×C7ŒÃ|’jäÁ©æVÜÀo©–ª¢?ø®N7ŒÃ2ìénfØ!ièJÛz!ih‰‘Ø ÞÖ€ÿo¸Çqäߨ*‚Ÿ7.ˆ²hˆ±X‹£¸ f I~ø# Ð3±'ñNj*ìÂ%|–šY" hо˜Šmà ͳÂaHG7ŒÃ2ì³Ó¼¡e4Za Öáȯ•%Š£:bîI­3Á•P]¤6Ñ›qìß6|‰ ÚõÀLìÂ%¼’ÚÛ¢jYë0 [@; Î(…D°og CyÔDGLÆn\çt)€Ä >†`%öãªÔÕÀ ¥n¡hŠáØ æ»{fx uÐc°{@=2ÁÐ}0 «p ¼­gam¯ü’zçù÷‰uíÛOêf©NÌ’Xƒ· œ¾4Gœ†‹Òî8”ÙFÞÃý1 ì;‚º¤ÿFQóÑyqJ3+¤±±h„^Ò8Wð³ñd2Á·¤‰ôã$ê=9 fKSqPšZÏ¥i68$M§ßfpÏ™d4ë¾4›¬çœæÒ{óKó b ´Àó¤…±ñSZÔÃ1Ûqœ±ØÀ~¨Šúèù8ÎY’.ˆD#ôÀ,Ã>\ý±4 ìQQ¨v†y «eÜay/,Á.P¯Ö(‰*¨‡.ƒE8Žû N+ Âá`ݪæèéX㸾]]^GÚb(æb+Îâ)þJk¬P•Q]1K±WñVZ›NAš¢/¦b-ŽJëRðôÆúl6¤KÉuSEi3çlñDÒу0p ÷ðMÚZžC:Z¼·E‚ì·çõÙqFÚYç¤]µA»mA÷xùÛK¾ûrã$nJû£ÀÚœ}Ð w¤CÓ¤ÃÌØ‘ª˜ŠÏÒ1K¬–ŽWÄéyŸä-§œpZ:MîgR¥³íÑã¥sœw¾+Èöýw‘}/Ñ_—ݤ+']õÂéZ%¤c¹t= 7¤dt“=o¹ƒnS§;ÌÛ]gϽêÒý¬Øfãè—‡¶)=²@;ðÖÇÌÙfýéppÿgôÔó,˜,½È zâ¥7îJ¯Zº½uÃé]!齑>pîG?ͧ<>w“¾ÆlÏ×ÞÒ·rˆù~/€hÁæíg'é—÷ÿMæ è„Q˜ƒ;x'ã–Š&膉X-ã¨†fè†eà™QHDŒÇrìÇy…gqÔB' ÂlüñÊ{¢R1Ÿe¼³Â¸ˆ‡ [ŸT4B'\Â#Ðã¾YÐc1W¦H´‘)j°çðA¦X8"ñ`¯âeJ$€™)¹NÆ}ü]@O4™úaÉq‡àpÆL™ KµÂI™Òü¼La™²N`¿råQí1³±¼'”¼Â˜‹pg”½af2’7Fù€¼Êó÷ àŒÓ2}@ŸTʃÆ2•{ÊTIÂc™ªíd¢™Ÿ˜Üèæ)vžL\<îÉÄw¹%$à¥Lâ|¬Ç~P»¤p¬ÄNœÄM™ä2)Ø€¸òLÍꞀÐËéÙa/„àŒLÆ?ÖëúaŠ* ðÙ!¥lDBéîîNTZ@º»»»»»”¶é.i°»»»îóÜþáQáÛyÏ[ç³=—ù³Öê>ú1Ž¿C¢u:jq…÷B¢8ž4×mKàsítd YÈm>å×hŸŸª2°™ÿB¢CkÔ¬cMÚ †N£B¢sy®¡O»èë®5‰ïtËÈ?!ÑýXHô(Joþ ‰ž­¿^e©ËlV³“¯qŽÞ¡®} O}džÛï~ÌAÿ¼\‰­é3°5Ô‡1Ìa »BbpÞ‰!=ÇÔch’71KÃôÕðØ#î¥5ËËH±Œ’ÇÑ÷0/$Ƥ䇫ßÇc›ØÚŒoDgôÔ„ì艞5i2›9É[xÆdqNÉ…sLMIEžb Î?=â(¯¡gÌ ‰™ù‘“YwÐÙ|³`¦çtA.ææ§*ÍXÌËE/žCNç›õ•Qƒ…÷2ïZÔ7$Çè%Wù<$–Ú°µ]Ö–˜…å…„~Xѳ°òQVc¦VÕâ‰X]$$ÖäÃÏ×¶¦'YÉ/!±®›ø6$ÖwåíØ™fœ‰-ÙÄ×è—Mù©Ï0vs„ËÈÕæLšà,‹°}û\O®‡Äó½Å,V±ï~¡!êûâÖx©¿…ÄËyiÁlôË+zúPvŠ2ŒËøîáyìç+Ìö‘’Ôäq:1õ:šg=Ö‹¼ÇdÎx"-µ˜Â;!q²)À®ò-Þy*7¥YÀôúé¬@¹‡ÚàCÄu¶íÃ>Þ ‰sééçó1、cù'$.NæË¸¤O_ÍÍÄpyKH\)é¸ÚŠÑì ‰kÐG×Ûs ϸQ†eÜ ‰›9èÅG!q+5ÝY·ó² ¹x-Ù)NMšÑÜäÓx½3ƒq–7*1„7CâÍrø½ö­ú˜ß·³Ðï{§(YÄ~‰w›2™c¸/Þ‚<¿ÿ»±Ÿ>¨z}8=ýQN¦c>®ÆÎ`v>©†:|𠳨É%Ìêg™©C;†2—­æ~'ý<Ù(E#:3šÅìä$o`¶¿HEÊð(Íbvr’·Ð_>@ БáÌçiŽòžùU*rPŽ&tg<ËÙÃYÞÜ|‘|T£9}™ÊVs gú&Ù(ãte,Ky–Ó¼ƒÝ÷mzòP…æôe2«ÙÇ5>ÃÞþî^ŠP‡'Ìl6ó 70›ßG<@ Б‘,d;çù ýúý¡íÊ\¶ò ×pÏü˜àJÐ€ŽŒd!Os”[8ÿO)xR4¢3£YÌNNòöÕÏw‹JC}ÿºŸbÔã)†3Ÿ­æêûw‚(F=žb,KÙÉIÞ@¬ÿ¤áaÊÑ„îŒg9{8Ë{ˆõßôä¢OЛɬæùÿoöß ­éÏ nòeH†@fŠSŸ c[8Äm¾ ÉDjrR‘ÇéÁ8–°ƒcÜæûL¦%7•iÁf°¹Ì§ü’Ñ}¥>ÃvsŽù;$ã{(LmÚ2„9lá·ñîiÉIeZ0€Ylày.ñ!†dÊ»(HMÚ2„y<Ãn"O©÷Q˜š´b³ØÂn#O©S’²4¦ãXÆ^.ð!Δæ>ŠÓ `ËØÍnó)>Ÿ69)ICÚ0€i¬a?çx—ŸCòŽ´ä¤"Ó‹I¬b?ø¹Hw©I1‹M¼Ìuä"}’,”¤!Å"vp‚7ù1$3¤%'yœ^Lbû¹Àûü’Ó‘“ò4¦ cXÂ.q“q¶LÈL6ò×ø"$ïLð …¨Csú2õ¼À>㿼+#ù¨FKú3“•à"ñGHÞ‘<”áQº2š…lç8oñUHÞ“‚l¡ ÍéËTÖòWxϾ7 S‚µÿœãõ¼/Iaš3–¥<‹¾º?-üf.Îlކä÷±Š7ó,Yióœþ_˪ŽÙÚ3šÅà òž=X’9î ͹Œžx¨*k8‹š>Ñlå0·ø.$s¶a>Gø5ÍU˜š´eïb?ä®ÍPÖ¢Oó䦓Ñ[yï¢Ñÿ…u|’ù¾ÉüMQÓ)8’«1€Yìåó,”›e<ÃË\â]ÄX8É=ä¥< iK?&°ˆ-<Ï9ÞäkìÓ"™ÈIiêÒŠ^ŒÁn+šd_H«ÆXœ¹xwôk‰WC²¤z—ªÍ6ä³Ì ìì²B²œy(_’ó!Y¡-gB²b7ôF¥Ø'UíŒj؃úV{ ÷CÍl¡z²–>­=»§N!ä¯n)ôV=犯‹õÈ{#ߤzçѦØÅMä¿i9L¬{ÿiéŽ<7«ÁD¾Æ~mž½Ó¢8Õñœ–rØ*5r×:5ú·Ín^ ɶYè’í2¢þí °ýýTÞ É±;:ÞE ìÈN™y”©¨Eç2ØÃ]&¡ºæ@m»¥$/uéÇÛ[ïõqþ¾Ç¹†Zõkˆ¿ïŸõà=Íà õܙŜÄ2äaúb¿}*$‡¥  «ø$$‡c‡ù=$G”ä§Y”ñü’£Ä6ÚŒ±_Çfe '±sÇ£+«ßøTè¥ ©©m<ãnÜî>ÙaßìLA>Й¹lÅgv¥&?3Ó³]Ø’»ÓÐ ¹ÙS½¿7öå>³±¿<öÄ98ØwÂsepg=߉Q¬âeÞÄz!+ÃPÏóÑ‹ü’/Ù™/?€ï¼’ûóPL'ìÙíÑ?G Ð{àh FòJH«‰~>ž¤rw¢î “Ùѧ&b¿ž¾û÷̃´`.zñlaôá¹ÚØ]ç³Ó »þBz†á~¸ø¾énÆb¼Z›¶¼„z\®HoÖs¹½â™WÍãµ’¸“®ûà†™ºÙ#$oåGmo¢ §1߯U -ÙÌì€×¢)óÓéè†ßÿÞ|1¿UˆVLáúêívxÆ;÷Ð’ìD?¿{?½XÎ ôû{Ù»çý´Tb ÎüAº0søaNZã÷ÅîÆï·§eòûIu†ã}ŸÞI?Ìág÷шÉèÿÏó¢Ï¾h„òåýtÄ÷¾Ê‰ø¾ÎÁôÕ7Íp¦oËÒŠ¬Â™¿+Êäûûˆ!èéj¡Ç~|˜é8ßOOpgû¹-Ó9Œ;ô—û)F¶r÷Ú¯Ô¥/KxµüM?ýž‡ºtg[Û?2Ó03>‰Yù¯?o…(4¦=ýÇ<Ž„(‘†G˜ÂZn†(™Žü<ÅxVðoòsˆ¢{¨Ïnñ]ˆâÜŒa/C”"+õø–C”²¾Ÿ*„(uߥGº¿B”afˆ2- Ñ]¿‡èžþ!º¿wˆ¸¢¬•hFf±×C”-õ˜ÌKü¢ì•™Áv.òuˆrd 1ˆ%俆è¡Ì”£%ÃYÁ> ÑÃwR™Î,à‡(gZJÐŽìáíåJMqZ0‰ÝÜæïåÎO3ư Èaž,T£K9„¸óf§)“yŽOC”/3M˜Æ!~ Qþ¼´fGù)Dî§½XÆaÄ]0#éË ŽóKˆ å cÙÆ•NR’.lE¾Š¤¢.£x†ÏCT4 õ™†œË@w–sñÏIuÚ²Š\Æ÷KDä -Ìrñ~ˆJ¦$3¥hLO¦°ž—¹Å÷!*uy©A[†³ˆ]¨géé!*[+Dåbî#?µhNwf²™Ó|Š>,Ÿ‰4b(gy/DÊ!¯¯‡¨R+Þ Q•µ¼À¨wÕ|ˆ¥jofð7ñ³j÷SVLd'ÑCÕSƒtf.Os•/CTãnŠÐ’álç›ÕƒÏÔ*¢Ú…y7Du–ógˆê>†ÏÕ‹iwÕ¿›ò<…÷7ÈHG"ï ÓQž®lÇ;¥$eiÌ2Ž¢Žd ,ýÙ‡4Av¡§¢7_…¨I/¾QÓÌå o…è±ä 1óBÿ>±2Dmó©ö,Åüv¨…þìèÝz°sQôp·'BÔ³=p¦^ã,}œ£ïÌ_?}ÐÿÄ4`BˆÞËë!´.Dƒ;„hH® ÷zxdú0Dãê¡·Ç?±Mȉ¹™X:D“Ä:ÙlL©…zOÎÓœå³MKE>êÑ?ŸþWˆfèã™bšõdˆf Ñœ”ÈïÜÆœ Ѽ ìÑüOò]ˆ¬ä¿-¢Ei0׋õÆ’´8ÃÒFøÜ²Fx÷òk!Záy+õòê !ZcæÖê£uzw½woó¦J¼¢Ír±¥úg«nKO3ÌóÓzù™tøìv³¶£>Óù(D;k³™_B´«Îõ¬Zí–—½SC´oDˆö¯ Ññô³çÌÙ Côbfô÷K¾÷Š?š¢ÃÓBtä!æ…è¨y9¦–ÇÍÿ 58eþO—Gœi‡^:›¤5hÍ@Ìõ¹4´B7³F‡è¢ú^j…Ù}ÕY/‹íªsÞ(¢›ß†èV1úò}ˆn;çkâ{= o¨Ï›fê­N!z»jˆÞÉÌln„è݆!zOŽßW›<ûÃ̬ä‡}ô([Bôñ=Œäƒ}R³ñéù}&¾/âï¿¢¯õÏ·zðGóð“Úÿò0féWõÿ#SˆþT³¿ôÕßBôÿëgÿ•d)†8TfLˆuBœ¬È‹|â(½X…ÏÅÃCœ¢ß…8å3!N?Äi¶‡8}*º‡8ÃýŒ qÆ4¬ q¦¼â;ÛóZˆïªÇ þ ñÝx)Ä÷Œâpˆï½‡Î\ñ}sCœ¹\ˆ³ ñƒ-CœmZˆ³â‡Û„8ïîçû$ÄËðGˆ  qá!.zwˆ‹ùwñoC\²QˆK-q™$½¹ⲕØârÙ©H –ñžUþN¾ q…©!®X=!®”+Ä•k†¸JA–†¸j’J e-gðÎj÷S,F\Õs1=Ä5’ì qͯB\k`ˆkÏ qÝl, qýwBܰgˆ½âG¼ïѽ!n|6ÄMÄÚtCˆ{!ÄOñòÜÌ3›{W‹~!n™9i5 Ä­³#'mÚ†øÉüâ¶/ñEˆÛÝKuú°&ÄíóÓ”œñSY©Í`6q"Ä2ÑŽià÷w¬Â<ÄÙ)CðýÎÙ™â.˜ˆØ»æ`Gˆ»à¹wŸâCܳtˆ{µã³÷®ËÞqŸ»¨Eæ±#!î{7úª_~>qÿB<@Î:Ï v¬æ.Mvðsˆ‡TåX€ë|Œžš‰‚Ô¦#XÄ..ãÃÒR˜ô`k8Â똃áY©K&±…Ë|âY¨Í0f°‰C¼‹<ŽÌBeº1MœB]Ge¢"]XÆ^®ãL£³S^,á âs•éÇ .ðwˆÇ–¥>Ã:^äMü|\^ªÓ‰)ìæ*Î7>7-À""þ ‚4cÏpÏ›X”VÌ⿆xR&JÒŒqlàÎ39/õÄ.#?S Ò–e¼È‡!žšŽªô`—B<-%ÅéÄ|z "Cxžs|ˆøfd§­~ÏÄÆÚžuÞ‘“ã!ÞY³¼«…øY}±ûnÜg{îevóÞtcëp7îË@^*ÓŒ>LA-ö»”f æí ûå¹Gq_<ŸÏ!=r÷b~&ãL/5dþûå씥1óñÎW²Òwô¡V|âÃãC|$PŸ9¸6b-vÔ±ÎÈÍq{ý„8Núó©ŒAN§¢1K°›Ï”¤ “ØÎ þ ñÙ¼4e*¯àÙçòð$óy½}þJÒyFœó<ß…øb)Z3‰#¸Ï/=HÄýj:ÔýòÆ_iÌÌÉÕ´Ôc*/á¬×&s-Ä×SÓ3v#cqWߌ(ÀÓüâ[}XËÌÆíi˜§×šÓ—Ùìàî½×[ÒáLgyˆßp÷¾ùåiŠÞ~«rúvD'^Áì¼S‘1â&_‡øÝ\Ç.yϾzúìƒÖ,Änü°6ÍèÂVs‚/BüQ&ÊЖÉìàúÿã»)Kkưš³üâO²QŸ6¬A~>MK9Fâ>ù¬"¶Ï'± ±‘óû¥~ûª3CüuV:aÖ¿©Àù[£\AŸ~——¾¬à0jöý¬ñaÿ8¹ù© Büs#ì³_ª²œÿBükKŽóvóoO°…ƒ¼âßË1“•ˆóè?Ó"þ¿²0±þÝÿþ§ î·Û± µùo‹8R„$•hÆz.òEH‘HO š3”é¬d'‡ù€ÿÑf=kbÅ`þÉÚ¶÷Ú¶m{m£n×¶Ú¶mÛ¶m÷«3ó\&'™ûÿ(ko„ ”a2–bà6<·ð¬CDÈìuYç-²n1²ÊzÆâ¸¬W/Ü$ëÝçàYŸxNÖ7Èú•á*YÿÑX‡7ef‚ucpL6¨—ʧàÙX‰¿dCËp¿lX¶à_ÙðÅøH6¢ wÊF‚XGeà1Ùèƒ /c À9c»`ž’ Åü!?ägB?œ†¯dàOYÀ2ü* \„ïdA«ñ›,x•,¤‹‡ea9²ðÑxGÑ&‹¬Å™8 òø?nñEoÇϲ˜X,Ä/²Ø(œƒûÀ;Äà=YüÙ¸N–0“ñˆ,q0¦È’Úã„,¹?˜“2Ä™Z‹Çð·,­Çð±,úÉ胣²Ì8<+ˆOeÙÙ¸œS‚ 7¹aHCÚ°+°'p5îÆÓx_ƒsóz`†2´a!V`Nàjܧñ6¾ëó{`†2´a!V`Nàjܧñ6¾ë z`†2´a!¨í¸PV‹•`]ñàJvÊJ‡"É(F3æá,lÅQ\;@]– ýT^%«(•UnUKVÃ~µ!øDVG]4Œ’5Ò§Íôm+oÜ&P{“©õ)ÌÆÏà¬YÔÈlö˜C=Ìãþó—-üA¶„x–QcËy»S³d§ÑgP×gÇ j`õtÙš7dëè‡ ûd›˜·•^Ûa²]Ôòú}ù:ØAvx³ìo}²RvþJ\†wepþ…ôÇEäðâ|Ù%ôâeËð¥ìòKdW·+™w5½Èg»¾¸ç ôåÔòÍKd·Én§fï$λÊewÓÇ÷ºì¾d0ï!æ=:[öØaÙ]eOR×Osγä‚ÿ¡öò Ù+»d¯q¿7RdoÑ«ïГï_%ûˆø?!OŸÍ}q¹ìkrò-c?ðþ¿tÅ,Ù¯£A^~ûIöûõ²?è?ÉÏ_-²¿yßΓý»[®\y»aøCÞþFy‡¹òŽr‹Ã7rIÞécyçíò.wÊ»>.ïv³¼ûLyDyÏþào½êå½;à9yŸ)ò¾#äý ëäý·È,’ŒÂmòAÓäƒûã<ù!àœ¡ʇý&Îú/ËGž”a|l¾|\•|ühù„¾ò‰=ð¾<`-î–ŽÂxS”‡ËåÁí1Kº@Q-:SsP‹8$„É»áKyÒ·òäûå)ä ÍñŽ¥V>m¤|:{ÏxC>“\Í"ï³—ÈçTÊç/$_¼A¾t«|9ñœú¯üŒrùYÄy6sÏ9ûä+_9?ÉWqæêX¼+_Ó×É×öF)ÈéºN˜ñ4>•¯o‡aˆCæbNàV<òºÁ0 I¨ÁBlÂù¸¼óÆ$Tá,<Ïå›Ú¡˰_Ë7a:¶âz°ÿ–dTƒy[©ÙmgËwŒï¶s¿|×Tìgíîˆ2ùžÞ †½Ïãwù¾i öö×É8®–Ûå‡Z击a—üH¨û£yà…bËy^n~;œDN3H•1þ£äõíytÅø§0~mX‹Q §±íoP5ë—V_¨2a?t بû'×õØ‘ÚZ¢¾ÈD}a2½\Ô¨Ý#\J-‰û!öÁËlÿ©"_ªý]?d£v¡vU_8\¿¨Ï¤ª«òËVÕ]ù7¥£êsÕ_ª¶1õ›Vg†oZ ÓÝ*Zö§Þ€ ¶ójÖs.ÛÚ§~NûWÔÕ´W±ÆR먵æq¦? µš#§š'«F”'ab½Fê#±?I½„év†§~T[fkp9Þpfõ«Ú6[Ó7 ,Ý¥šƒš‰^îìš—šM©æ¬æM¶j¼¨Ïš«šïšscê_7ï5÷²5™ß»ÛÑ8­=TPaïC™ëkõq0Æ…‰Çûó Ö½Ü|Èuñ¶ùœèõ¡½éðm®ó4W86¾Q?`6Û=WåἚf¢±®-ÃÞ8ƒéŽã|ù9ÓÍg>[cv²– cÛ[Í»ª ŸÇí‚í]ûËñÓ© vAoûÃÁº.â˜võìûÈ æ^ ¢½Të3í'Ù·¡?ë«ñãJÆÖÞ‰¨kq êr‹Q—SÀz?Æy݃~S¸n”¡.¤{p*jÜÑÀ±öŽÍí#̓ìù¦õFs>{>Äã8ú {¼©n¬Ç@õÌ2»xi=ʉËÙùp]ÒÚ]ŽìùÊ:Naϳ,uUv9ØO/gŽóìºfokp—9]’yþ©]É6¡u…ãj%Õõ]Vy\~M©ïþë¸6x5þä«m” ;"úŽylÆñœÃyŸËðÓT.åe梯?}9¾sYîÍ”·Ë×­'ÁIÜ€\Ös Æÿšùµ0o¨ŽÉ3˜ßá÷ µ7¿n×ãBÓW™#qUðk\eû¢TéÚ›’°9 ÷Nj7R“£þ3Tvê{Ô—’r¨NÛHi¯v$i´å•´Õþ±9:v¥34úí'%Ã9ëÌhÔkPgDÆ–Ð:kÛrL#UíÖ©öûZC’ñ³Ð^‹–Ú—Úo£÷‚o£/üŽÑÚàÒèóàöhEpg´”sݺ³€ö'n-Òº˜xŸ©g–=<ösç…‘ŸÂ˜¹±]k.i»ÎR=+?»íŒR­IZg´Æ˜ûq›ÜìNxÖ<Éù³&ö7Çál·ÇT83 ÏhþkŸm*ÐÝœ³ågæ »Ö@3 Ú“Ê^¬4µqmè ¦ãjÕQîŠ#•›êýÄó%ãŽÃ€à©h…4‰s¤íCuÏœƒ0Zyè|ä?Á0ÓÉ3A=†Y‹áaÊ”fð3”©>Á1Lû#ü’{“>f)Šƒ½è®!ãݳ˜îÈWî ¤µYù%ë÷ Ú¯ËO{"µ¹m‡Kh?Æí“¤îŒtg»¯ Yî|þ©¿H q~ýÓ´+Œ}4î;óòÔo®ß…âó™ó;¥—¾­ì¸Za…â1|–ƒšÕïÌӨߕæ*Œ°ê÷ƒé~!K_Ï´žeøQnçÃðCÒ1—ϵkô¼5Ÿ°¯f± ~ͺ^ƒ³Uÿ°:ë¹¥ñ-Ì|ê.èäP;ÄãBêÚÎ0}`!†R;Øat¯ÁØðh>«F1Üü–nʧ˜ý£=ø[8Oy ´VþòÓs)9{~I–†‡ gxÓÈg˜ùIy˜—òwýì‡]í)Ø5¼–õÚ€Ú0‡ó¤#Úætaý}Ô&ãèìt>ÛbÐP?¨ÿU÷ ݃ZÈò· ¦`?Û µ|žÖ…†új?¶CÀñ8úû3œÆ‰úÊíœj̨ßÂÜcŸ«±£þËT­áÕØÚÎÅ–N5Î]Yíá*çd;γq +“Æ¢å9Í¥îÈ>h£þ¥[{âŒ=`y¦f·‹Û»µØiS{F}1ÍÕ;[]]¿hÎhܺþqíä”ýy“™Årm†›Â_rœ¼Îð_â!;˜ñgâ¡ô-88|jo>ŶÐ^½ ËÓškA¬ÎRZo5·5¿rŽCÿð Ž÷}˜Î‡GâšpÎSùµgtg=7Ò“þ0ôT‡=Yï¬ÿùÉüx -Ô–¦ ‡©ï¹žÕ½üµÔ~¸\íï·ÌÚ+áin«íÔN  û¤4¡­Î¹tiƒqœ»mŒÐXÏxkx½ Ú9GÝ~Q{Äv¨`ÚÓåf®`<ÅãTWrÍû°~9.ñWÖýü÷¸—{—¤.ÑS~o>Sÿ—˜céÿbÂ7 s%ºø¹4_J¶Žü*®=뱿¿'z ÿ0t±­©ç¢\f¿(Æ[‰þÞ;8Zn©Á©ý˜Ñ^¬éCÿ¯Ž*—c<Ï/Ä`ÿ9 V8¦±·ŸÜgníoÃ0ýçüBii?ª°?Ó#\ÍyÕM{{[„6샛XæéòMÊw2vM->ŸÒó¶C·TÄô·E7Oæw¢"ÙS+p¼ï£Tå~7ÆYÕ¥”i¹ô:{=¢TÓ’wfÇÒm5Ñ*§÷1òý‹‘/ÕÞ_q€†ã³8J¸yšÜ <žhÓ컢kóïôÜ×ÝÅïŒðx¢5±Fk©?£Ýsî¬3’»¤ú¥»#ÊÒø¾¨꨾×+úNv…OôOzîêÌ•­î¬Ý”nê ëžNÝ™º íïÎÖÍ¿[ú+ï˜Øßš»NÝú³)Í^'Ýs¦©{)µËeÜ9ScLýMÏ0ŸYÌçyæsã¿Ì¸'SïqínÔŽ§Çç8[ ³\4Fç4W^—¿K÷¯íÇ¿¶_þÚzoºìä žo¯Í©ß“êXQ-¬+oAF¹‡2Ïåá°¨Ags=7v™¡8l×È*²(K«É»úMB¿OøoD :Ï3ìÅä6ÅÉîžÐÙ5ÇDð<á×ÀЕd¡´±ö ¯fXÞ„§Ryg 3½î’ú!Mœz 5U¤ß)iMóí^G£úmM0¬î®ŠÓ1`ÚF¿º;4§ºú@{#OµÞ#¨·PÏ4w6ò§rå>‰ÒÍyœšƒVÞwheöE«L³W~[ÜCjƒxÞ_‰[Ìá„e GS„‡¨’ù`yªhÃ(ýæ)ÂSÐÝ.Ç-Â_Õ±ža%ò7Ç]_fÿvܤºËÎßñ1UXg{£Í%Bw8Á©ôŸ¡2‹ÔËøI~; Ð>‘z8)!'sȱ¤ŸiQŽÔ7BÎðB¹â‘Qî7ÏŸ_{á¢d?;†iÉ\@Mî!MÍÊC{]šµO ŠtF&§«ÈeÚïP÷ÌåY6=u|Öw:ãdº•Vdkú¿E=t$'’–ïk©“åOÎ"¹ä7@ÃÏIa´¯ï+szoüFhO™nÀó6/Z@{Az tnÒY.¼χï£[/xÚsu%Çq¯ÞÁÞÊ|žGG¿w´šûŠÝƒAx)}4ÖïÈôlïÂ+zÞ§á¨à ô ŠðJx´#X¿V)?=>j`Ø}¬Ö–¯p‚ΙšwÂ|Œí½™¬Ÿ»“ëŽì}¸ÇŒÆ(÷›˜½’ýzZ\§ðT´L…–¹£h@7îåÂá(ÌÙœó¬üû÷´ÖYÒµvÎÑ‹“ß .bøµ "ÍcäÆñr+:¤ :(¼ûÍDã*çkŒPZRÝ«éUé³|‚šs©‰Ôr©³“Ý3‘»ÂȶI¸Ér“:ÿl÷ ¿ó³”¤ö#Ç‘‡3tyâÿKÒ‡ü‰ %sÉÝßÇõ­+·pöÄx¢¢ByJew4UÞÄ>)©ó“"«þ‰2/ÒHû„˧ý$jWiGHIØ‘"Ã>Üåå4ËœïòÐXÒû2&k…»/ýwÚã¢u‚æÇ„}‹3Í|_ä€` Ü%l1vÈ|‡Fï³ØíSײ<Öz©Žd Ç^¡u̲ ’ÌHíÄ9ý˜`¼µ©*”P[JíÒT:³?ËÜåér”g›¬Ëámƒû¥åZ”¨c‚ÔÞ’þD²Ò"Õ1iàMò†ÜþJ–d’>· ¿UÊ#qœù­,o‚׳xZî™*2ûãGiCó?7¶f?!óÆiª>i`„ ù42‚i}l—â‡ÆžnŒÖÅà:a=\®½õÏI:?BîvNíóǘԅ²;ZœŠ[…Ì+²ìóÓðÇj—ÁÀÌyšmÎòwºX¿‘a\ëovk’Ã?Õ!Ù«½ì4¨ð¾ *PìTå1;!`úßÈì?‹*ò{§fÿÔcœ—ûå‚»D‹¾‰^‡ šSAÆêl¤=¸; [bG…¡_žª~ˆ7FÚ”{Àv< uf§*‹Ö¼ðÔ(_ž£®ÇÚ­£+T®C㛃™ÕÇxÛ›9©WØ~¢i;Õ™‰‹ïøéþY0Ì¿4ÙíA7j…ìR‘¾ùîÍGq³ÌSÎ ráTNÙ]y35Ø'ªÿëðαGìfK_SqœYã·9ØsbœÝœ£qŸ‰1¢ ûO çX¡þ—±’aÒMaòËןå}þ{üdâÂgç©ómcí&ý%†fû»ôMwý‰]#7®¥AÔbRI® W síû¹ï„‹„=¥wÄE9ßàºÜn¢ßãd–0GáŽ$ñäá„{•G†}šy·P§&zQâ^©¸ö8ò-*mTòN«ÒœäÏxRû˜ÌJÚ<®N@¥#vó–KÍ|v,‹¡}Bx@ªy–æo©†ÔÓ|bR?¹¿G·]©ç&eèDLüªèw$µŽ¬¤ùRM~KjĽ§ÜÉoè>›:ºšzµÚÙÉ"ÿ%G ˆÁÚ„b{û%‡ÏôY˜SÂ~h¯}Pjs>ÛÞ¤^JÍ¡^’°Žè9ÝŸäÏB{”©q¦Ž¥V žÏ+h¿žtd¸¢L–º­Ì‰vËð¯ W2ÍéÔýI5i—è@¢s@[êZªOUZŸ‘2ô‡¤Ž&›‘÷'Hÿ$Ÿ~‰y@BëÄý Ä,u´Q™Èø$#×¥çHü¦$ÜŸ08Áå+·jr¹<±Ÿ*\:ì“22‹ÊøLéU‘À»’ Ó’ Õ‘ÜCz+?ªÚêbrÍ7Åx»šg“¥$´'Çáô{œ“ÜÝßÜý‹ÆÇߘrÒ¥M±:ƒÄ-UŸfÿ˜Ô61¸DÐ\ôÁä$œë;g.#cÉ„„rîY“>zù_£Š@Çp*ôŽÅ ÷^³û^@¿ûkqšÞ«×»Yz—Dié›9µGö= ÝoqßëèÛݱ¸÷#ÓPÃÒY†'yz sƒ§Q›¾¿ {àìô5ÜÓ´CÏœsQ—n™ñžÌ"–s:Js—£,½7JÃ4 Žâ=ñH¸wÝ»ˆ.ŒÞ•ÿKñ¾D‰ð,žwøÛ`”·£¸Ÿ/®}Àåxùî=mü EN„–á \sõ#ju1._d»ÎЇcu¿o°¿rM%¶°¥¨óóPk¦ Ÿ*‚óØöëê§þæýÑzû6÷œmHò. Y»;åi¡ßUìʸ¼úöÔ÷pŸ¾µÔ7­j7}K¢wQí1ló¶0LǰÆL 6­È 0~÷Øõ3Á hœ¹›åÝ]ô>¯¹ÃY¦Î[úÝ(g<:¦fâ¹Ô{ãmÁÞÎ8ÅËÃq²g»K›åÎøÙîúƒãhl&f ó‡»PÅÙm{Ú×”ÚsŽ¥V“ǹ·û2‰3‹aúÄnv1µ’m».qß7 /fã û+ ¥Y~W˜Wã8f´1i~®f2óÞ‰ñ'‘2Œ †£Ø´l&{}OÐãǘ“QÐ(œ7ö ´¶¹1á“1ö"´VaHü}ÓÐÈ_úרծEK¶A]0ïÛ§Çß!û{¡Äß3Xºr®¶Å ºË½ÆVòN{¦ÚWÐ2½ƒa8Ç>5Ø㕸U¿ïIM=†ø³YžJÚ×p\¾‰wùÛåBÛ.6ç쉹« #x†ÖÜR¼|û!¦še( »¢ÈBO¡í?pSßOû×aÜßâûéÜwÒ¸†œM.'w‘ãÈ­¤”IF3­³šûíyîxå?àªÿ Ò­ÿÅÒ¼ÓÛ(q{NzÐ\˜sÄ÷î\§O–fqƒôŸO“eÌsæÍæ#;ü¡ Š”6A%ÍN¾Ý+£VS7£¾™ð² ÛÔwIoÛ€ù\k¶ô>æw•©›3¸%ÁÙÏ'£³Õ™®pÈ=u uŒŒuï™èjÚŸÑšÊ=î#ú¾0˜žç¾kI߈å"¼yv5&§×br‹¯09ÛìÈé}ƒ—§;ڱņ8Ìÿ£‰pæqÿÈåçxÚc`d``Ïý'ËÀÀ÷ïûµ?¹|‘@ ð ›j53™3™×f àÿRýÿ!PfEd@ ÿÿþšmã`ÿßÿxÚ<Ðwd1Àñt2k›éÍk¦/ópfû Ö¶mÛ¶mÛ¹kÛ¶mÛ¨2þçøGñêMõöéSç]A©MÓÛë=CH«?³—Œñ|õüòü÷DѺ´!mF[Ðt(KgÐYt}–PV˜ `-Y6Œd£Ùdvš] Žƒ"À 2Ô†¦ÐZBk à܇çðÂ!‘wâýù¾ŠäÇøI~—?æ/ø[#‡QݨiÔ1&ÓEQBÂ/\1XŒ£ÅT±Hì·Å{ñQ|ñ _ws•yÄnäß%þÐp3¼|䟬‰")!)I+r²Îó#U‹Ðú´©ÖjEûÑt’ÖšC72Š2“…±ÖlhºÖJv!˜âÀ¡ÔK×j ãà\‚GðþA$'¼ÄçñuüˆÖºÃñçZ‹¹RµêÓy¢¸(-|Â3µVŠ[éZ!¾v¡Ä\cžÑZA2«Ì--­UWk5“íå2¹W^ÔZŸä+«•Ó*gM´ni­X›Ø¹µV{´½ËŽqˆNC·;¼ûòj•ªz¬úWé'ý¨ïô­¾Ñ'úHïé]½£·ñ¿ñ?ð¯ñ/ð 7p;±›±ë±k°«°˱ K±‹°ó131Ó01}ÑÝÐIò%OrEÄŠ/™’,‰/±rS®Êe¹(çä„l‘…²@æÙd'ÛIv´mË–™}öØåÎä NãdNâþÉ_ø¿â|g˜xn‚Í/óœ2»ÍvÓÓt1üT/Ê‹tç¹3Üiî8§§ÓÕéìt¤:ò)“Òé49tœÓ~ÚK»i- Ù4‹ÆO™4¢dHÅòAçúÍï=¬çÆF?›Ÿ–tkB§†ëÖ‹¢àyæXmÛ¶m›ß¶mÛ¶mÛ¶mÛVe’Töº6Òÿ߃¸‹j$Ð#"ñ ÍapŒAœÊù€aIÁ—z¸ýšoõuû#?‘È/ÉüF.R(Ay²©DuþMq Mî¦=¹‡Þ)÷Û"n\.y¢ý/÷êeöžÐ[ìSüª÷Úß)äòÿ°å© ï´•}·­è{lÝ@ßk;3]ßgç°PqGpY{.î þ‘ú©½‰ÛuN{ïëüö#>×MíWü¨[ÛŸÝÙþè.ö÷@wµRUw³5£§ÚqÌѧìc?…ôgí~Î~E]ý¼mèÂÖý¼÷ºˆ½‰›#2ŽÙÛ¸[Ÿ°÷ñ>gå1}Ù–¦¬¾j+AfتN°õhÛ:Évf˜N¶ËX®o²»}‹Ýè[í@»_÷Ýýúv›ƒ‚úΈû#â.úaÛ ÏuÖ<;ƒÙz§Ýh×û`ÉzŸM%C²Ù<¨Ù‡ù4";Ó~èÇí—|£Ÿ°?úûègmNÚèWl†ëbv$Ót=;—yºÝèvv‡u{ë5xèºöÞ~8•´ˆ¿ µ™déáö-ÞÖ#mmêèÑvkõX{8ÐSí%.k÷ã‘ QϰÙܦgÛ'½Æ¢°^k«ÒC¯·Ã˜¤/Û‹¸Þ¿¦G<Ü¢3íܧï·ßòv†ÇrP^?n+R]¿ñÄMþÌÞèÏí=ܯ¿°OÚkÐï=>ÔßÚÏø\ÿh¿âký³ýŽõ¯6ÅôŸ¶MuÛ†¶º„í@G]*¢:ÿÕeì­Ü©kÛ{xT7°Oñ¶ne?àSÝÇ~Á·zýõ›‡üz”­Am=Î6¤™žd[ÐRϰ=¤gÙ \×K#Ž7k¯Ë‰ I;òLð€>mÂ}û›×ãÜÜ@?q9HÕχÿTÜ«_‹¸~‘«ú­HXû/ë÷í«¼¡?´oñžþÔ~è¯ì'|¥¿¶ßPXÿd{°Z—Š„õ8¬ûE‰§h­'Ù ‹øûv$Óôãv&Ëõ«v%ÛµÛ9ýnÒ?G™ÇyBç´O:·}>Ðy"á\p¯ÎkïçQÝÐ>Î[º…}7Ð}nîò©xÚÛÎÎÆÊÂÌÄÈ £ °IÕ3eƒC`„‰HE]T.Pž]aCÀÞJ…ÿÿD°H³Fn`•ÙÀ¬Ê±EUù!.ɇº:Þ þºº@MuMpŠG™@лŠžXUÈ3aƒBr†B—@—²U—@ª•.ª/2`xÚ l]aÅ“ýÞ7ïÎFXÓ‹Ì[¬.Î o¼ÌîjÛ¶9×mÛÕ¼uFqÞnüÇaÛˆÐÞA10>üuó'›ß¿ÄOñÇïß²ùêCüJÓ/¾dó9›O|àƒx¿Œw«x+Þ¸éë 0}Ùô:½ôt/6=t/¦K¼¯Ü¼œÆ‹lž‹gSxÄ“N‹‡Îûà Ü_oq=÷îÎ5÷ÄݹÜ·Å-qSÜÈæúµ溸¶€«n®ˆ‹Q“ÍÅy\˜ÁyqNœgÄiqJœ'ÄqÑ):&ÓícÚE[k§i­-ÓÚIk¨«¥ÙÇ´xVxiYájö¡IËæ¨8"‹Câ Í‹ÆÓhÓP?Å4øP?…:Çtݵ¢FT‹ª)TŠŠrËT¸)·(³)u^J³)ÅEãM±(OaÁlShS?ÉÌ&ycÉ9ÙLŽÈž@–ÊÊ&3Ã2™þdX¤–ÚiÒDjŠÇ¤v’êJIö1)RV¸’}H‰ ‹L¢HXD¼3~%q±ãLÜ4bÇã,bl¢¦¢}ˆšL¤ˆŸl"DødÂD¨+¼ÁAA&X±ß&pÃtèÃ>±Wì±Ø=ž]cÙ)v °}€ml`‹Ø,6‰ÿ«!‹Ä( gæKÅzšñŽ»»»»»»û²dÉ6WÁnyðjUþ¤’gñŸ²§mÅà ÷®¸·âF\‹+q9ÄÅgNű8‡yvøÆAûA¡íw±'vòî;i¶C1Û.`+ÅæRÒ6ņǺX[ÙšX±"–ÝdY,-Æl)Éb©o‹1|æÅÜ+³¯Ìˆép«M¿1õ›Ée&ĸMØXŠÑ‘¨&ömdâO”aŸ!1(úS6ðF_ÌúSôõzÖ£×£§ŒnŸ®NϺD§GG»g>ím­¹Ö£5—–.š›j­ù†¦Æ„5ÕÒ˜ ¡¾Ö&©¯¥®Ö³º(µ5¢ZTE©t:+TÜPþF™“PvC©O‰s°D¿Q4E¡+ EÁ ùΩ|¸£ ´H‰¤H¸…„ˆ;­ñ)b/Doø"üH`¾ˆ¸íH€'òb䊷–#²SdÝ醙b"ë"®·Š‘!B?B7_¿…šÿ‡Èø§Ù•þ xÚ%da€¿÷¿w†™ @ 31DU  ¨bˆ¢§ÎÔœ *¦˜Ã¨‰S5s*8U¨ªšsà3Ô0Sf‚ `ö·õyðñÿÏ÷d‚ÒsFN&S ÀŽ7G)8—Rö]Ë») jrÞQjnÈmoá[àø+=fdÒ‘¦t¡aÏmf]+ì—U¬[l•õ-–¶~ ^S?½pùÊ# ¹"æD¯µ­§¶a÷¹ÒJs~ú-”¤LH(hJÄÐ%®KÊeP1öD”Tr(5 Nd9ÕÜ&‡2猒%{ÚsCжPsɄД1±ÌåÿÜïfdÀ6°¢­`~Ë‚! =&a6«RSà/!çò;üÀµ¾Ôý.#[µÏ¶Izwí“ú¿Ç7o¼±þ- ¸]ëKεõÛ$\à‹€™ëú¢§~vÃTòLFºOJ¬P5¶ì)™‡Æ[" Ò5^‘ð…cZz@ê’»Þp=X’’ÙßœÊ{·¤Ò 3°?ÄЄƒÿ¬çƒ>xÚ•ŽÃBP†¿1^ ×Ùek›m×jl{Þ¾?suôã>îpápÀ1ϹƒAU9NüŽ£çÜõ®ï~—{èv\=ç^zÖ)R¢M…4IRÔ0Fˆ2ª8ÇŒö¼²ˆÆš05ªºâ„É3±KAø)e«ä´³W­êcWŒ‹ÓÐ2ø×¥W× ÂbæÉˆSZÿ LªCÇ eñ®¨crJ)—Ú#Ò”W”™T zKÂD¤›ÎÄ/’B³/:l³®xþ¨Ve¢v–«G÷ª8ÅGÅ9¦˜gù=û™û̼ 2DÑxÚc`fƒÿu Q X+ÛÿÿxÚ=Ð5B%A…á[çvOÕhá²Hð ‡ !º$Ãw€;<)îîΉhýÚ»~1"òƒ«˜Z‹Á×QL‹b3ô,èE,ÑËX§7¡£Ø¢·±Oà’¾Â}kú7ô-^é7¼Ñï Qå$F èPCú‹ZÚ©£¿ê7ú{!&È rEƒ¼°VLX6‹†-¶MŒm·íÛa»ènÛK÷Ù :b#tÔnÑÛöPÔÙ#ž9¶gÍ•Û/œ¿p>]Ñ~£s`t#ß3Ä(çwôÇÕë=âør+B92Ž÷•8†<ó°y Ì“æY|ó‚yÙe^3ï‘oÖ›MT4_™ÍT5Ûͪ{â 5=ßó©…!üÙä{äéLeIëLM©­3u¨ ÉÞÿmpäöHÎuéï°Àá|()Îgãp S˜ÅvUìí*r8Çrò÷8Óå$ÇŽw"Σ\^ywå.Ê••+(wT^ª\Ó±Éë9©l•5³ã ýŸÆ8¾_®ÅPÎÜd–ùÀ|@‘ùÈ|J±ùÜ|N%ó¥ù’Êf‹ÙFSæAÚ3ž¡F¬$6ˆšÁª`õ ®,¸™¦Ïô1¦€Œ)oʳ“©bªÐßT3Õ(1 MC²¦iG©f†1À«âUa`¬N¬ƒbccÓl;Û®Œµ=lO&ØŒÍ0Éfm)“í®v7¦ÚãíIL·gØ‹˜m/µ×±«½ÁÞÂþöN{‡Øeö)ްÏÙ—8µàÖ‚[9 ÒUãliýWð? ûä{Ý4$ÊçL7«ö–ó /²Iò¥‰ûçÆa pW"*Sƒz4¦%í銘l¤cóA°Ðñ™ÑYjjf7å¾­A¼\pŒ¾g‘ãGxÁ±˜W¡õ¨O¹½ÍiAKÚÒž.ô¦/ýÈPB–A f#ÅxŽçNä$ÎæznàFnân–«®_d=ØÈ&>àC>áÓè[H&Ò³Sód§â½e9IN–ÇäqyBž4NÍUœŠ:õ‹;0^)^Ùi5cÏ´gÙ³í9öj{}Ä>jŸ°OÚ§ì‹N}Ä‘\%` ·æÍâîdE–$'ño%6³²Úé:‰ “é¼é†á’¼á’pI"Ëúd´Ÿœô ›óF|ƒD6qa°{°oºarRÞˆ…«¸‰‚ÄæàÔ¨?]'Ý0ÝüÇõtÛtçà¥ð¹äÊD6ù~js"«>õ¯$^J¼Á[áVºw?Ô½ïJÄ/Ä4‹XšGß/âØ>zn§ç¾Zm¢—*k'úcxE¿ù³’ü?x1Œ§ª •PªÇñœDwuªÞêÏ}¸ž›é«®UÂr‡¬þÇ¥ê]yÑa0kù!Ñ?Í6;Œ“¤$/ù’Ï)–b&ª¿Mr:hÂu¹©ÒGú0M½nº ÌPÇ›)Ãd³Õ÷æ¨ïÍ•y2y²»ìÎ|õÀ‘zXìôó$»Èò{Ï$ØÛ”3ý9ØdÍL0³Í\^7óÍ|ÞR'|Ûëçõ㯿ןw½R¯”÷¼£¼£xß;Ç;‡•Þo#«ü˜g_Û¯Í:¿µßšõ~;¿üýüýÙèßãßËþýþr>òòŸâ ÿ9ÿ%vø¯ùo‹‰œVÂÈi%/R±G:– ñ’xV*H•à‹àk©l ¶I ä¤v(a\ê„ùa¾4 ÃBiV+Jã°rXUš†é0--ÂaiÖJ»DõDuéd›ÙfÒÙ¶°-¥‹mmûH7ÛÏî$ƒ#?—a‘ŸËˆÈÏe”=Åž*ãìéö"™à\ý™ù¹ìm—Ú—dÿT¿Ôh95µ8µX®Hí’ÚE®L]™ºI®r›ÜB]§‹¿¿Ó£û1¾ÛwîöJÎ]".Ë96eez.“Û‚·~‰xƒxƒØ¬Xå ¢ÃþÿÏ$@Ćá¶ï}ß'I~9×'¿Ì™93¿É™É$3gNfNæÌœ™3É$“L2ÉäL2É$“L2É$I&™dr&™s&™ü&3™ùÍdïWFï½{¿ïñ¼Ïÿ¢ˆw „OkT_2ðl` 4"!­rݘ%ý?…D¦›;N¡ý •L¯égëÔäÐ "¯ÿŸ¥/]’1Ã’qzfTª­ÇMª2f&NÔ¦T=a“68ûÊÌè»F¹ä\#næt¦ÐMbÉ,èý”Ä̪Y·-ðͦÖÛfÇìP‰*Ti(#4eSÖΞ:?„ûæÈDæ˜k)GD5ä‘§®º(®þ}Jè,²”¤€RÔbÛ¨ÒÔN9EV™§N$N³V*Ø´%ešÆiRÙ#õÑ|BH#T´ÁIg–¦•ó´HËšìmQI5"ä]rnÄá£è*UÝ•|‰)©ŸëiŸB$èý:Á®â*‰±8–¸–륞Ë/òe¾Ê×øßäÛ|ç†|Ÿêf<æ'HñS~ÆÏù¯ðK~%¿æ7ü–ßñ{í­ðþÂùÖó+ãïü“ó_9/ÕR'uˆ«¯Fá;ÏúµH"¹ ûtEÝ4»TtŽ[ÈJFîÊ=y {r 4áné•~”a™’Q“ =dF9§\RÝUYÇlʶìHÙ½CžÊ‘D6c„ x¨A>´r™¹Z= ‰)´  i´#‹òèDA7 }Š aD]cÓ˜Å<±Œ5l` %ë¡¢ØUžÅ>ªsÿX0èÜb ^=Û¶mÛ¶mÛ¶mÛ¶mÛ¶_mç6™ô`Μ/›MÚßkíT¯?û‡¢ãŠ©ÒŸ)ZOIR*š­›xÑ\2µ/™„|%»ªUë5¾¶$é7‡ýIºk’L¢gúO…ëȽUnÿ“.Iý‚» ¼Žrg1žUŸB}aöN†õÙ›V¥2>Êcø%˜.…}Xx[è {kó”K*¿‚_ÁGâ#ñT¸¤™…sWÐó/Þÿgu¼¹þϰúšþŸÉ œü½‰ñ™p-ìBe8OMI:„°º˜þ ñÔÔÜf5Àˆ+}“Gú$r]¯Ÿ?‘x/ò3plÉÞ@VGPy‘ü†ûS&AäuH:Ãp»^â+Øu™{Zø<| ¾Ö­%Óñ}ÍàþÅèü¿…?ƒ£5Ý`ÉHít8Œ¯âÄœ®“ $è<˜n)È÷ÛÜ$õHñ\ ¡çn¸™»á$¸梾7ÞÄÚ(¹\ÑÞGÒ–¢Fÿ7ŠpúCò òŠ0ìHÍr¸Lÿ¯¨ dW:jr“Lƒ¯Hšã?©KRŽä%< ÇÀtÎ ³¨þz2dÇuÆL†n0Íñ3R1ÇÉÀ'#sœLÌq²2ÇÉî¤vÒ9˜àäe‚S˜ÉP)¦6єذX1н٭ÛLÝQm»ÃÚ¶mÛ¶mÛ¶mÛ¶m#ÙÚÿOæî¾hŒó’‰ 5ù]Äjá 4ƒfRFZäFfµpÚàFVµp6:èF§à›”Sÿæs«‚ó¨‚óªó©ó#5RSAdD&*¤þ-‚ÈIEÕ¿ÅQŨJ $•Gi”¥Š(òTE\ÕPª¡&jQuÔAª‰&já9˜CÍð /¿(¸µ*¸³*xƒ*øŒ*ø¼*øgãrοU¹½âZ\‹Þp®Co¹×£wÜ€Ð{nÁ-èwåÞô‘ûr_å<Áx$FpÇãЧð\„áù¼Qx?_A:¾ÉŸ¡˜°GUçèX¨- %êJRI‹ú’Q2¢…ä“|h)%¥$ZIy)ÖRY*£W6ÚJéƒö2X† ƒÌè$‹e :Ë2Y†®²BV ›¬–Íèîô°¼Á1JNÊyŒ—ËrÓTâÓƒ”uŸ¡ú^ ú^¤ú^¬ú^®ú^¥ú^­úÞ¨úÞ¤úÞ¬úÞªúÞ¡úÞ©úÞ§ú>äô‡ÕÚ÷li[lE[OlM[¯m}[ïmCÛTâ½Ä ¼ÄM/qÌKÜD°KíRÛ.·«M»Á3 ½ÇM&ïqSÔ{Üó7ÅÃ.»ÔT »'ìS%ìþ°LUÊL„ _Ù¥ÂÏÑuûËšžûC¢¹ÄÑ艴.)\Ò}yÊâ’+w ¸”ÐïÀÆ| iKò¥ÿÒƒ>ôá†úNÙÄûú~€ÛÁ‡ŽZ3kâë»;®~¾úKëö\7Õ?‘a=‘\qxy¿VEDzNœJ£VÔ‰8ëTœŠSiÅ9ëTDE0‚m¡Ä!‚b‹38‹³8ƒ³ˆÅ bU¬SqjĉU1"VU«ßÛ­jÕç7ß›÷~ïý¾÷½Ù5»vòH$íñ…¶µm ©H£hoZÞkÑ5â›Cf•7Ü3I‚®¦Á'AË é¥¦tC7šjꎴ¼×Hk–±¬”ƒR“˜ëtfƒkŒtƒ[M% ºœhÅÝq‚¾WxEÊ\Âj~•QèIܘÆn\Û.g÷ªŠî!*MyUº¯,‹uʘ‡p_»gt&‰e$q߸o1ó{íHÙÎ%òºµM …F¿—ŒÏ¥¼æÜ¨õ£Fœ Z芮Hã¼ÏûT°sw«—©%¯éŸ7ëƒÁ>=´ãáŽ]ä*îÊU¥‰¨Ñ ͺØÕ\ }î)¢éwÇ%ï†k¹ Ë à>ºžéÄ%îJêtã®}ußˉd_ Œ¤¦]§í€'‘?Ey®Ò —hz· ö’wäžÏñ-KT@í-Ü-k¤‘w×UMq ¨¨mZ(‘‡Ѷ¶yçݰ9Úµ¼…#Ú8“: #ÞÀÝ;:^Ò+‰èµåéæè-ðºô¨Á‹pñ[: .±häàú9ü^³ › _é7ú@¿K·4²,ÝôñýÉwó ð=þÌòÒ“iþо”aiyæi~¤1Ö}ÌO´­úmŽ·¡)?åçò ¯I>ös×yµ[È.>”ïpEs˜ý×ÞG«ù·Þ2G}T¸Ê1Çšã×ýÙY†O¹‰¹-d_pòŸ¸âk¾å®ذd„$ ½wå¾LȤLË#y,Odc‹òTÖ幬ÈxC¶dG^ȾŠÒ6ûÑÖ®æ¤ §S´Sÿdk /"©@GUb©Y(u9•¦´äÂH¹’k¹•:$= ÔtÈï\Çt\KZÖî¯øk>ÔÕ‚Îé‚.W5D,{ÆhQ7mY·uW÷ôÀÏó>kE«X±¦)P×Sýî¾Ó÷ÖÐŽôÒ+õmÿ3²¬ Û]»³ž´)ËÛŒÍúgÊl (ز­ZhEÛ´mÛµ=øvdÇöÒ^Ùk{c'À[ĿѰ3;ÿ.Gøfû|ÿ΂øßuàAï=ð<øs¼s}çþ"ÊÀs‰(:«ÿÙ¶mÛ¶mÛ¶mÛ¶mÛ¶m™S}8½½dSÎæ½ØV~ËZÖ}}km׆Õáa¥• ½ ½.ƒSáp·—ÎèŽðªv´Î::÷éº]‘¨¸¾t†ÁÐýá jÿU:/wV'<ò^ÀLÄDç‚p> ÃºônŒ.—â¬B·¤Ïzœœt+¦tùëáÄg5­’² {ë|b…'{ +ÃÐEÐíÑ‹a_œùð-Î;¥õ‘ÚÅ©ŽnœÓö¥Éj‡î@L1œ8ÃÐeá}8 6 ËXŸ.­ð}è8t 7ÂxzîÎOV?ád&~ÎnzÕÅÿ„n„ŸN…‘ñ“x? ã‘;E}·þ,tLtvÕ‰˜Rè"ð NgtLXÙd˜=öQZÐ{¨އÍ`C"ÿÂè8ßáköðNRZÏ mÎ9ªÎ¹xGáU¥}—ØÔ0³÷Ð01O–¼d†¡uÞe}:¬§QöOØÓ/J'ÝÂ:ïªvKãGŧ›9ÙgzâJ«/ƒÎ»ô©¯´û‘1ÉÚ y—t®eµ‘uá"¸f"²gqg/{ØC«ã¥§ÂQV¿*Ý+¬FÇ™ÁªÇ~ZSí&~eœnÆá7{ ê ÙÈ»F½#è dtu;òLFó“Ï“¥„µáZVsQûŒ‰a"¦j$z/¥ÒJ¢£“ùý¬Pï–ÿï/òì÷¬l¤î7tKjÌʼnŠ3§:z:µ›¢Ëyï 'zqun&¦4~vщjé‚ ×¹ÿ-~nŸk ]›˜‰pŽMäg¥')Îx§"ú11ݽo:7+íËp3;© »9 ~¡K\¿Ÿp¥ÿ`E©Ëà5ø’P’úß*æä毨””òRUjJ-™.ÛåƒU4è[;nìÂæ ºû£S\É+…e¨Œöϯ- ¥§¬—æÒV:w½ù¬?ëMݹ²XVÊAãn•Ýæ£å^ç}À~¥ýΆ{õ½¿³‰ÕÖJ7–R~ò&šj+/‘µÑÕ‰L.ä©p8oG†s§3Œ¦¾—¿°¡­JgŠÿR4­1Rü¦ àÎ aÜ·¡0í —E:thà¤IÓ¤€I'œ4|0<ÐlRqh°q`q¨ÁPG ”Ì¿ùìܫﲶi“«²}•Ÿ_ÛùâØï=½ì’ÜýG@‡îÕ'—Ûxt2!s¥¢aÃÝ¿JeôÖK3¼N7õt13ƒqŒüüAb¼Ì])QA ­yޏEãä ZZúæýø“¹Â¬Š†Üí[¼Þ ìžoÉCÌ4æ'ÿZ´“jä³¶ÈÈÁÌ¢câ¶0ÿçQhŠfä}ÍØ~c{žâ/jz†´£®å7Ž/j¨Ó±%{Ó¬&';w/²Ãh /u_«¸GÐPý;ÊÖx<:Ü!‹Æ ƒN–­N>•m3~ýf´(Ø…ÝŸM=ãÜë'ü”«ïŪ¨O»$gøûbâ7ëR”a4ׯÆÉ«AK­ÜÃpçÙÖdN{=e ¤ÎâkSWÓæï¼òHs|&Lð!³ÚÉñ~-{«mëdyº¯+µ×´ü7h ±¿:ìŠI|Ó$µ:xÈ¥AÖ¯tuãOJ3´rÁ{‡Õ¥ +#—E—Z¡0þ\tÔ~ë ü Ê¡¯[ȸcMî´êÔn·­ »°õ-µílãjg…|Ýö~­â{û‡ûÿ—ý3ô»ö Ûý寭Ae÷üFyŽ y$;Ñxéë-ÏbÙûHs7»óçcEuÞÂõ¾¡ŠøUbs<0(LèKå¯ë)¾ÒñÛǧ–"ârlëóq…ô›rogïln_ɇþw¸Öïž-º+·6ÉîÜ5wÕ~ ã×Ï$‹f‚ÿ¾á GQLÝǯèYþ,;çzÿÓ~ü˜†3ùß‚å­@:SlßÎÎOSz[šaFG<¼i§ÇãØÆo˜ÎÑ=~¹â¯vë½RŽJfÁÜ¡·¶ÙÂø ¸¿Ô’ÐŽÛq´‡ë˜‰«h(–¹~¶ú/Å ÷{1†W?îwÌü~Žs‡’³yF¡Xç7žÁ’ò§"æO??[ÈŸ.•j‘üÂo± ¡F-#ÿ|éÁ/1~v%[næ@¾/,Ô(ôìç7 :¿ñœaÞÑöå»Yí‹Ã%ü\~^;Âoþï3ó@¾Ï\~Jn‚[Æ÷˜Ëãû¯Éÿ½÷µÛéù¿/ý>0éû´D3-š¶—:ö(Tñ½ÑQÞA¢GÉ•èèëáWúªBû;= P!ce+¾¡m5|ë¡o¯¯øQNyŠÎÓùýÓÆ=Àȶ aÿ×¼iŒNm{¦Ý×¶pãäÆÙhͧàÚ¶mÛ¶m+Xï~k½ür*_*ºûßᜠ*©¢†Zêh ‘&Zñ Hˆ0¢ÄhC[ÚÑ“^ôgƒÆ(F3†±Œã‡üŒŸó ~ɯø’¯˜À$&3…©Lc&³˜Íæ2ù¬`%«XÍ6²‰Íla+ÛØË~pˆ#ã ç¸Àmîñ[þh˜×2-Ër-ÏŠ­Ô*­ÙbÖÁÓ]¿Äê|ë,rÇP1Ê%Ž ù‚J‰§J\Ôˆ›ZñP'^$FI¤I’h•dü’B@‚â#$©„%ˆ¤• b’IÉ¢­dÓNrè)¹ô’<úK>ƒ¤€ÁRÈ0)b”3ZJ#¥Œ•2ÆI9?” ~&•èS¥Š_J5¿’*¾”¾’Z&ˆ‹IL#™™Ì!¹’Í<©c¾Ô³‚Õä²AØ(lb…ì•2öK¤‘CÒÈiâ˜ÔrFj9'..H3·%—{âá·’Ä%É0hѬ¼h^™ø4³,Š4·\å<Ë£Uó+&U3,U­´Jüše3é³ÙÖÁ:£¹v' Ù&èÑ?Z iÆß’¦9/"ÑY¬içÇ"1|G|A¹ÄS!.*ÅM•x¨/µ’ð³OD[®Ü*IøÅK@’ Š—¤‡ˆøˆJ*1I£¤ÓV2h'™t•,ºK6=ä¿÷ 7}ȧ¯0@ (Ú )b°3Tr&% —RFH#¥œQRÁh©dŒT1Vª'5üPjù¿"ž/¥Ž¯¤mIh3ð1W2Ð âe4°P²XÄRåå’Í id¥ä¢+UÞ Ml”"6I3›¥˜-lSÞ)-ì’rv³Gy¯T³_ª9 ­?‡¤•Ãâ爴rTü7Ç%À rRBœ’ §%ÌqsVœç%ÂE‰rYb\•×%ÆM‰ýcSïH{Ò†û’Ï òHÚò^ÚñYÚñ[ñòGñšPdɦm°K¡½9æ(û̧œj©t°4KSÖ®ã5í:%–c9´3m¼:Úxuò-Ÿ+´BUm¿j‰•Pj¢\feÊåVNG«° ¦«À±*«¢Ìª­šNVgut¶kPm²&UÝ >k±ÊÍo~u‚T Yˆ.¶0]-bºYÔ¢t7]¦ë"Ó¾´/ ™þ‡F[m«éiklòZ[«|Ãnb·ì½ì¶Ý&dïìA{oïémí£:Ÿì}ì³}¦¯ë¼ë<]\\èçºèºHýûú]]—]—Õ¿âºBWwwº¹»º»ÒýoW=À3Ä3„»íA»j�G%ñ@g¾ uþ|CÏF=‹õÌQ7Þ¹æÜçŽó—óÈyF¢óÂyƒs®‘ƒýãMcÿÈqß‘ãÿ‘Íy‡ê_æï xÚ Š7ADçZjà…PQb`–Ëaó’t e+à«à7»ÍûÓåzƒ0Ò 2nb"'È6©Ž]T}ÇŸ¡sš‡Ø-øŽ´ÊŒ‰¨ -!+ݪÙâ·1»èRÝÀ ‹ÜÛAuü×U@’,Aþ3sºgvn®·Î¶mÛ6BÏÆ)ôlÛ¶mëlÛ¶mß¿/&ú_ôWVdöª¦6ïý( íÓ«sy¸A;ÒÀÙ³P’ ð•°ªŽ°Ç >åѰ—woï¾Þƒz ʳÏÙióÉ| bo’XÂë¯> Uc®=ìÖ›¯Eãa#¯†–ÞíLþIx«·!?JÛ{ÁGÁoÁ´0ö _ ?  Ç„³Â}O Š‚xÊc€¥¬=-н°Ws´º^©?êOú³þ¢¿êoú»þ¡ê_ú·þ£ÿê:ZÇèX§ãu‚NÔ]º[÷è^ݧûõ€ÔÃz\OèI=k0µ'l»íLMKO”î K&7&7%7'·¤¤Bq*¢ ˆ¡ 1t#†Äü÷$è>ÌCˆá!¼A¿…éqÄ0&Ãb˜F 3ˆa1Ì!†yİ€ÃbXF +ˆa1¬!†uİ6a½ƒvÃbØKûˆá1"†#Äp\ &tR’tZÒtF2t$íÄÑ¥ ]X ÓE¥(]\ŠÓ%¥$]ZJÓe¥,]^ÊÓ¥"]Y*ÓU¥*]]ªÓ5¥&][jÓu¥.]_êÓ ¥!ÝXÓM¥)Ý\ZЭ¤ÝFÚÐí¤ÝUºÒý¥?=PÒƒe0}§ÜE?/ÏÓ/ÊËô«ò*ý‰|B#ßÐßÉwô2YF¯ôÙC0ÍшV×êô•z%ý£N„êd†„ÎÔ ô6ÝIïÒƒP=¤‡˜ëaæ#zŒù¸žd>¥§™Ï &&H˜š2?aO@xâ¶Ó;m'„'¯4¸/¸– KBxþ¶@ýéR(Šª„( |NÄkëìNvN˜ðOàgâ5g.Ø é4J¢/ÁØ!e¥³Œ’÷dÔZË"$ "ýœ…'û9÷0%¡–’.1£Ë̤ ºH†]j†È%gr ú‘4¿ô ‘K̤¡:L—›!rÑL>(Š’yLÀ"ˆŸQ¨~ÎU¢%Ñrh¶ëßõƒïú‚Ùô ®­ˆV@£UÑšXï¾÷ßû¥ïý’«Dk£uìÝmŒõô½C}ïW¾÷+®mж°w[´-Ö[Õ÷Öô½_3«~ÍU¢íÑ.(ñ·södñAÆ[âæE‹MŠïå~I‹¤‘Èî5õkÀÙÂ(ЍžÛÂß’}r›ûµgn3(«¦Pæ&Pî ¡³÷}YdÜ×îK÷ûÊ}ï~p_¸ùîG÷­ûÉ}A&{ßû»þÿ›~ŒŽ‹½¡<òc¿u3´œûÅý*G}5ÛÍq3Ý,òÇÿ˜¿÷óî{ÿIËþ|š©«ºgšÄª–™–±ªv¦]¬*›éœ­A´ÇCXóœè×À9-%R_xÚ…Òp?9ð·I¶›¤?áÙ¶mƒ³9<ÛöýmÛ¶mÛ¶ñ6ýÖí4[|6y#à±+q/®Ì ™õ)`žâié[°”ÊïÑ‹â‡Pg‰r€zu—ÇC–ÿË,¥Z½XÞß.·ºØó}eÙ§§âS ƹÈ`~T=}õŽfªÃÕùêju»zX}©~UõUkÕ] VãÕlµ\mVûµ×Gê õýúMý£þW7×u=ZO׋õz½Û™C͉æts±¹ÞÜm7Ï›·Í‡æÓ›ÏL³Ôl4{cŸŸ Ç_ΊÂÕ|4žÄÓ0ÉíÉíHì»Ö%.b÷¿k€ÃÝR·Ggf§àxDÈàD\‹7’·í«öuû¦ýØ~jÛÚö¶#Œ¬Rû¢t­î=ÀÇ8•ú‘7ÜŸ@…œ+ËÙ²\RC.S–+.Ëù{«R.rÏ3— ¹™ËVËÅþÐ{›¹ÃBîoæ ú¶R]!|ûY…ožJ¿µüC±•Š*ô|d =QCχWï™»/=SÒsZo×$w¦ù{ƒ×ÒÓÅëètÊ%ÉàÂL¿³dܶ0‹7TñÀm¥à^-ËYæg¥yÚѳŞž#.¦çŠ3ô1ÝXwƒŠ*´^›¶ÖSüKUl}#mU[©’[új<7ÓØ7 ~n"~3^—iê;Ìú–Rÿ)ÝJÜ–n&nO7wä·-|§ƒÑÑèLambdaHack-0.11.0.0/GameDefinition/fonts/Hack-Bold.ttf.woff0000644000000000000000000040606407346545000021317 0ustar0000000000000000wOFF 4M`ÅGSUB XÙf=®#eOS/2ø@[`,õÙcmapøœ ¹ …ë$cvt  d” Ï;!rfpgmXK v6·œ6gasp PglyfXá ùL£E£2headñ`66 Ù¯hheaø $U°hmtxñ˜….’eÍ`locaâ„Üh £¯maxpâd ÿœname ø?¬5'K‹post 8 ÿ'[prep¤ÀÜ`‰xÚŒVUtãX­z Ù‘A²$;‰Q‘c%íÐÆm;ÍžÄ݃Í䡯afæ™t/ 333þ óמeæßýŽvŸä`§É|ä«÷êÞºUõ€‚À^”‚ÐàöZ CeJFê/¶µº–@¸š R¼:€2p&óÍ@éÖº„ŒmcK“õM³( ½l pZ}tß«­YSS£Ñ¨Õ¼GLSƒJ²ˆ–n;²°Ð¢â‰”âWXþËËŸ|üò‡¸ oq·Þ=w­û;òËÑ›å…î·=Ôø¾ˆgº?%'‘Çî6ISd¡}ÍÂÃqŽEl!A a“‚x@Ú Œm­ËÈù6>IròO¸"¢ 9ºË –V.!ä:rÙL:•lokÕÕP"—(ØRŒ"v”ËsK¥AÓ4ìÒ´ß|šõLæÅLæþ§Û·«Ò¡™·3“¯3gˆ2•{ D`Çka”È„$Y?Q€¯–QF%¶ÙZçHé6:!H ºlä'}rŸk 5¢¡P(ŠøIWJ»ŸryÏ$¦>ùèÚÝ%»±[VÙqVåÚ`“ Ü´ÛfïnZÓÝÖKλñúY»gßÝ9cs–{:ûŠÿ ’ kz%F(Cñ1¢ Åáú‹=^UPB%âÛ¢†06ñTª¥'!þuÀ§ B³Z[*…ʤ2†.Ê$ Ë’˜ ãðÁJÕ”ìŽBynÕö9XhK’,™F)eó²Ëº¶}î%§ËM2x‹Gí;ЇÜïFov£ê½ñu+·Ÿçqã¿ú¾Ø×±Ûì2=a†Æ§ š=ÐKj ûL"‰ròCFçÐð­ £5E[,ííM§zû{û;íTOºGÓÚç•Ö"œ‚µR­TJƒ‰xÜ4D¨ò¸úƒÕJy®S°;üèqR|¸×PZú:í­KÎ[žïkQ̼r21Ãe§àt—ªÇÔœ÷Goæ;œþÖjjÉÖN{`y~Ë’ôPb ðýÙ>µ¯œB¥"6w*é>ÍùñŽô jU˜—¼6[$¯0"sÝ€©„t‹¨RÈ-/…Ð è_’¶Öƒ(ËÛd‘Â9À(°U³ïðaÞò†&XäÓš?4¿Z&´»½»3¤¤‹8%KÂ{ÇKž %Cˆ!K"Å‚«¡)9£G†Òm©zwg>RŒ-”Œ¨Éú()§ïîwU禭e côÉY}ã˜êª´• q#ä0E dR Xœœr€QBíȈÍ䤯.îv¯ptXT›hÐM-‰¥MD„ °Õë˜|Ãx7TUUW…‰›Uñh[Ú´'µQ³)n¦¶û'dîXûÒý÷ÊoÉÖ#oå¿rt?ïÓÄõíîíd%¹xìZ p)»žÿ4È@—N9Á pFø& ´‹oQܾCsY=ætf»r]éd,£gzs%^Ô-‘Oq9î}3Ë÷¥oÌ‚mkºoRï×¥ø·.¹t¤¾ó™U+ELp³ø·3û­æð~¸cÞÐy7|ȳ±ñîÎc«'žðÒoÞ§?ï6 ®-ÆZÝÇÆÖ œáv÷P@Øñ¿°>þ_ÈCµ67„saB‰…@éâ7Š3ê‹C€3a)BªÝˆeÈcž{m ç4µZñ‚OhS.1š.ªT5Qgä÷W>ðÀƒX\uÄ÷–+ w>x¬x<¸ìpûâ¡Ë–¥?ùÏÇ ?ù$Ë»oÎéhÑ+®þáÇ+®´Ô~Ç}3‹ñ£­ø³¤ >®EUd4¯Î:‘p*J'àY¤pxã  T0àœ4$$äL²49ý̱wœ?ŽaüÈÑ:»‡î{µ5biœå2­ñ°Âºx—¦Ê"é^ÓÔ÷GBÚŽ)÷ï=º/!½iõÝ û«fÃ?•áÑšN€Žœ9‘xR‰M\n„‘«#Ö7I¢ÇÉH陓s:/`LÀ.Cô¥Ú=v?ÖRŲm%ÛÔˆ\ µ˜˜‰"–ÕµöK.<íå?yù/îÏî[°‡Þ?Év¸§ûår œ^3"°Ea‚СG(E ’ù“73áŽ)%Ç!§zvINÄ'¡ä¤)ˆ.­!Ì«vÒIQfâ’(3œÅÐo“ ºà”'÷\¿YωJ3ˆ›ÅD·™\ܾj¸ô“B1‘(šsòCvÎ $Ô±Ñ™Š±ÿÌ”‚ˤ䔪kúŒ6‰·ð{aL$ ©nµ3®¿4KÇq¯õɃ @¯b £i“p–ñ+PB ›v)¹Y:á×tZãºʆ³Í£k`—£+ͦ‚`«윦ZƒB#²*›ýU6[uí¾‹qÎK7Ür‡û.|Ç-£ÒÂì›Yñúî ܉à…¸óp÷Íë]âÒëÝ77°sù¯@ 5{ª3$d¼‹ÀŒXÈ ›NŽûý;—ˆÇLU–¼Î]ÑTÇkÖ7âB<ýÁ#yè!÷î‡xþó·Ž ¿pÔÿ¹{À¶ªëqøžû–$OY’å%Û²,Ë{ØÎÙ;! !ÛÎÎ6YPI²Ã¦ÂNùÊ ¤ìYh)«-PšMé„BSJ 8öËwÏ}WÏO²ì¸?ú­?ŽC,ß÷îwœ}”wœ­ÿñu}ËCã· ‚¼~]÷q»á¼)ÄGÃu*€\ÄG‚,ä⤠•.¡9S¹PŸžêsúB~g*J:®@C3.,uϾøýò—ô¦Ïéô¾`éO~®o&’Am4Q‰“d‘5Gð¼@y%Cì|\È‘¯ª@C‘Ôº¬;_¢{âaŠ™+-Ó›–åÊ*òómŸYæ2¶½â¯•™ Jå€"ô‰ØäúOaèWPuÅú;_(]÷àtm4nì•7ëGõoô¯õ'nÇ1PAýM¡“Iö`šÂÈìMw§d§fY *Aæ8 "÷É 2C ª‰|Ù•„‹¨2mH´@C¸~ûö<·qïÆÃ¸öÿé“ÉÍ­´þèÓ£Ön­þÍYq@Íá Ö…«…Ág`êEÏêñGí™Éhõ¹;jÒœ§/³Ì ä“:'±K!ì;$ Ï*DÍ’¸Û–.´¥«'Ù£¬=E‘±vaì‰÷ E³ +·4…äAž#ÆìcÕŸ#¦鉈ݧç–^¬~ÄÍ>¦ÉÇz3 ³ò8!ò†§Âùñ.?”8% ̹6²çYýþâÇôîžÈ&t®aÒz)‘’A¡E†n«YRäå1:®Ñqƒ…‚œ,—Óa#¥Pª =—6ôJÔ!Sé`Bw#Ê ~.˜Ó·õßÝ6iæÁƒj;{ΤáÓ§-þá̙Ӌ:Ït—:ÓÓ~??kEýZßOÛ!ç%ØñÊ«™YÕú§—_p¡þíÞá£dEI´ï›žY‚.¡"ÚP4x7ÂÉ~P•°«Í k’¸&*$à€:€(”¬dD´iv;èT¥(ÊFä°Üž²¼É4§Wô.NÐøý‡“à1êqÂT’êÊP07Ç=Ì3,%9Á®*Ä.ãE]¥Nh.MuH>i JGúîkg—ÖÛ®p×ÔLžR]í¾*evŶìøÄß<³$ØK{µ~t~Û˜v¼ç ½ptÀÿíúc†½^¨ÄOþNGÑ$Ù 6%T-$U¦ˆRÀ½JÖš®hªM[­ñ¶Ù…V,®@!~ êä`yD壅tÓÏ3ƒ?7W¤Û4¼þ2½î´äDÍoCÜaÑÃ…Ê•nè\î´ºÚoàL!ùÜ¡ÿž ¿g€ù§Ÿÿ¢þÈ;ŠG{G´n¸ÎmpeM­þã;t¹[ïq:9¹¯CZë.¾³H.™>Ó P7Hr&¼ûÛÆ6ÜÆæ ¼Ac¯q<ÒÒR“ð0Ë‚,nÃî…é[ý¯¬‡_ôõýýMËbI eäÇádÈR:hr)P%²qClBùÕˆªhj¬l²F•ˆQQw{üýjÆêl”wJŠ™QÒ—éezvYR™qÃ;âêÙ‚bª Y#àd££ù­?1Šp‡î=p{A«Ë•{˜‘."|{[õºôSÝzwBBBÆ_r9ûkœã¹2äçYy9´ME|%h„`„ôô«ðFèy•6w•Ñ›zVqU€Èo²wºˆ—´†‡(‡JI#R"$›–`[Ž*¾i`w»Ý^7[8¦éØÉVë~ ì.gߨîw9ûaßÅóÙ‹.¹Dÿ ÎOoÒ¯’ϼûwì¿»Oþ¡n=r„9Ì "vxÛá¹+´³¬ä†ý}i‡þ>”t_²ôš«»ëP>1ÞÊM¤)äRa¿ò‘ {lBÞE³dŒÜXˆ4fã Kgcñ¾¹q”@°Š†·JàûVDPOèz;.2¨]¥Dôö¨ ´b4%HRŒ>D}H! Xõ7xà7¬IÔLŸñèhÜPŠÂNŠ`'” ; Ä-Ee+-#êB¢ª†-"Fý F«=ý ø•ß]å‰âl”«¸§ÈJƒh•§ëÙ¸œFZ\±ÏƒÍ°È ¼j ,'a#~Ùà5œd{rsßghþº/šˆàÿö¥+!î×;³-…T.%„€L >eûj?Q»…û¼wEÍ—ót×Ý–é€Ö7ðƒ2¬;R€B*H4¢þì6*îy¼p%ÈèÆè~Õ­C¹‡kTZÚÏ`@›;!l†<©»0ät:ZQ¨5uqÔÚ€ø˜f3<¢ôèà däwz¡Û˜žnqV›4Φ,#çNß2îµ´Êâ뵃°Á¬šäUMXHV' (GpÏïJó±~Æ2gJY)»Û‹nEAgaáóE‡/—© O/"WEÑ—‚.Þ(.%®?kÒä¡î {âå´c>ó4&ºÎüâËÛ£VþŒ5»kBI©[Ћ»;=»@KHi­S£u§>‘N0nzIm¸J   èlŒh¢g#nÆDñ‚—; бá4< ¼TT[Úë—¯-[PÓ¶¢s­~#½zsËs¾¤¤Ìz˜]1Ò[yÎNú%ÈÝýï[†¶ fHˆüS~÷æ‡}v›Be40-$C v¥¥¥Êhäõ;Áìæôo¼ Sõø ³àØÃ¥‹GèԞǺ·Ñƒ÷ê*—Y«Å«SHy¸$ÞËM3î¼áq"¼á­¡ðÙw2ø 1S¢  ä8!òN6«B2Ã醶 @Ž Û|R‚WˆÇ!D·u/QÞ=Y… ÑxRF$b' ½%ËbPᦠIöùȉä9ƉÔgÐ@o˜;÷ S…gÈ "½Kÿ2€Ó†éß´[áã˜ÙQ Ä­Á.¼h cÀÈ'ð¤AÖ¯Ø2ãÀ¥øF½Ú|c0\M­IDbú˜”Þ·ÂzƒøV΀S0“HžIš*Q 6XúpÃãæÜÐBAöÖ 8WnÜ6k£~>;sÃ-OÓw{¾yñfýïzß9³Ùç±)Iq8èШ$áÛ%~´]`,/ÙX»iiÎ4¸‹ÁpJɘ í< KôŸÔÙ"ÚD¯ê*“÷tÒ+NÎ"@¶™Ö—Üp¶FiŸ/5v‚Œmƒapdý}R÷äKò0öÊa'_"Ôx_WIdãã *•)½ØÛ»:$0¤¬‹++jqEèïY¼Úì @!-^¦ésØc—¯±8£´™ÊªP³O¶SÅ R̰ødàá„Qq{ÙÑN°è!ý>;8äi, Ûô#˜ÐCfÄ‹°Ë ¿ð"Dc W &‘Òp™8ÐÉÃfEjYŠK<Š©pµXèæ4Æy³FŸ-/W¾$ÙdΑT €¬urHm~Æv"„›†Ö! ò"¼08ÄŽ`< ²@—`>B¥ð¾žßGž#”8ƒº,¸lÆü¥Û·³CÑá+zxÏÑüÖŽW×ï•^?¿óæÝÝõÊ»'v_¦¨Ü}¹þ¯Læ'udYöxk#µ™ÁV~3tÊrïj`Y í /ú¶ÅÁÖ òäaCAå%®â ÃSR¥ÅUÕT[âª4“fXUQp¨-=Áˆª0êd™Òô@*ÆU]oåW$¦*)#¤$(ReÓ¨”¸JUQ 1£ª¢Îf§ØÂ -i¤€Kt’$Ì) PjÞÆnúá]î‚;Iƒ45r#;SUæqæ—±Ä0E‡3"åLõ¦ÓU—¼0bïÞ1¯ ÒÅ]~éâÍõì¿ÍØß¾å‹^†pþ«+¾ü²¾KßóJÛr°ïØ©²eÇŽ-ú?ô[vÉ‘ Ð餈ÈÇ™º|ƒõJF¨@yYfêóõµe-å-Fifé õùA1: ü÷‘2Zëÿö¶AôÿÀíîÄv7”ïªÿÖn§Df$|Cu‘RN6°½NÜë)܈,Q…JÊÜ ŒBãF¼†Íª*?þ7‘£Æ1”‹Èk¬#™¡…A?—bÜì‚K÷¤¿a/Î÷[=:{Àr=àÇòg¹*Y¸€}]ú6u5 Íê8ùLì¥![V‘F\$@®«ƒ¢Y–=dÇ=Ôn‹Ú–‰"ACñ:´.yÜv›/ÛðR’l.» • GB†0ŽBIJÎÿ†˜+ÞâWÎÿ—n’I;#[·å>Å%6\&é;ô]H°nÕ"nÊÉÃ`‚[ ‰¿Ê™>$gzSóœy v9EI‰,pŸ›y`è@p=ýíŽÁ€|ªëÔ ò+ª—ä“2ÒJÂáaÎJ™“±, ‰æ0÷n*ñù㥉ăº¶‚§&•×…ˆW1Ì=ÉIî´$O²ÇY”¯r#\¾µígNC¨÷ÂõP¿ï’Köé?_ÏTÚƒûÏ9gÿåÝõë¾ùfÝúžw¤Ð’;–è %w #P É$•¸‰"ƒ²R™PI¦+™El0‹ Îtf:CþTÛm<¸$ŒE™Çß ¡õÐpé%ûÆ~x“˜¿¥õÀëÖ~ýåÚ! úctÄ’íç2GíÔõ;åw”(™¡×·ÂF/bÈ›Š#:â ×3¼†Lä-®A(šˆY¤T?ĪW{5yßKgìÝ;öU¾/Ø3ôûõÏ-·†Wåm/ÃùpÁ«+ðö­~`оNìsqP²—6¦Í…0c/¨ 9á §(‚o"Ì—ST˜ò…˜+ÌëJË×<ó#Ñ÷é^ðKFv?BϽp?ä_0bx°¨põ¤ªÜ”P&$ëO?æ)3d#ï#;2rêv\¦¼»¢î¾u³¶W–79rS†×öüv”ÏžÙóˆ4¶:}âÐõ•Uúj"s[Zñ“G@V#z[aïkY‡+y0WœDÊPäÔ6Gòã»ïðÁ½3Z²2ós3ýY~ga&FE›¶·|ÉC!±Ú…±Wÿ#S&Ð뮪^C Õ«Î½Œ[è^;ö?¸†·Y(Ó¶~í" YPÍ&…ä°ƒ›ì@Ѩ;+,rv‘¼’ØlÂÍ€R¬5½8ÞxNc>?øWc.‹/'àÏ)ô"‘x M¶Ô> ¡Þ‰XþÞ˜RÂ8 ­ôj¾øÝÄGü(5i|ùÛúYþÖà™tú„s²<¾tà Ÿ‡¾·‹™÷ òí†]±_»s¸q»„g³£¦eÓ|¨_¥ )@@šÈh-DŠh—QaAnN‚ÔC½ÕedÍÁotä5Ö‰8¼JÅíÊÏG:vèææs–m[¿qÕúaC‹=­…×t^š¶{Ïñ /t<ÿòÚ¦æR÷´‰;Ì:ëâ‹}‰ÉÙõP3«¤¤¶f¬PrSùÜÊùÞyNyþ»}eeÓF»KsªGu¬;üᆄXñý\Nn §d"gkTRT ’5 cÇ@k–”$ÅËO³˜Xâ¢Øo3ÒÓ0ü‡IöåJ¹%=­€‘/¢½1Ù·¡¾°ólÝL²a¾¢0³Ïúâ—¯ìÜ~æ¤köß;äO^«ýÅÅk7qŠýøýKÜ×ß>¸jfÎ\|öøe¾¦¦­×-}ïÏ õß;{j˜Ójìû‡~÷‘I)ÜâCÉ[FŽZ1¨JI•ÕDdYP«€PCíºÇŽ(˱Æ+‘ƒ2à@3›MaÄ9¿Ÿ±ƒxß\#­¼´¨ÐåÔ†Ú†Št6q¸ÄýÏVè q4ìà õŒõƒX©‘˜;cÁV¶º¹bqêø>œÉ|ðc@‘Ç–RI)Q)•Ò¨(Â#Ÿ³¾[^Æ%„¬#Œd9„‡/3b”ö„q£Î( •T©¨‹ŒØrÝBÿ‚2œuñÒ›½ q²ßRrµAR”EâLOÏ®qÇK…Ë-<™q¡ë6Nàb4Ö„SÒA¢^‰ûC„jT΢N<­lqé`>?¢ËÊâx#1³Ð¤œdgz\šBŠ¡˜‡6rƒóÈñ§ÛÞú^9ˆýýC~ÄÏ#DÞÆn&0'Äò˜B¬Ó‰NG3WÞÕ÷Ê>}¯þüK º~²ÂòŠÌšRT Ç'®ä8¡(Œoâ%ÖO‹F2Z¨ïVãò×ÐtËÊøß/ë q$£ØŒËú2 Šü@"ŸƒaĆ9…á 7¤×4C©Y€(ÍÉ7dh<ø×"z{Ï6éËžÉôÅ“Ÿ~%}òWÝÊßIˆ¼O„„_έ’ ŠL… 7Ïc%&ˆªxcVµ®Îø#Oû. ÇøçÞpqˆ `†‹ï´"‚Fë®çú`#Âů '£P€i T3·7ßšnÝk÷Á ¼¨ˆ6!Åzú·a$9"mKêEì„YŽ& 3nîz>nXýj?{)P)QpŠkßfÁ®•¯ZR”ãŽ;Ý‹Pç‚)yèâ7v£½­|«2ÒñÖY±3Óñ´âׯ$Ú=ò÷H i$ûÈõä4†¤€†€0f[?Ùsaùˆª€º2’_ÖÉÃTI›¬QèQäí;÷¥§EL±™e¥@jªJË ¬°T’]#%PÂÓÒQONëꘄÏc"¿Êÿ0‹¬Ÿ™·U5TËÚ6?„?<øþY×>¹âõKë×¼´xÕ«¾7_¢jºWÿÈ•&·>l[°²l˘KçÎ÷½úÔÖëgäÕ žÞÒÐU\}쇡s‡5¶û} Ûóó‡§Mz}ùÍSÝ®I³gÌ#’A-u‘Èp2™2b“3Y¼ É-@5¤—åUãôÂa¥b#*°vM²¡ÆÀ½$LÒ4Ú¦Ú©0=T4׸x"’·Î`®ˆÑ#&‡'7Ôåù°N<\Žå=Mß•QôÕ_' ¼rb6qzr«v25bBб‚dE¿ú÷›ÐˆÑqÐ¥_ýJæ;:†¼ì¯×Õv,]Ú¾¾tumÉÖ‘—?ÔÐØBPFo›’bc·K6:dvéÜÌÎá-÷„Ϩ¨P÷é—výbùò#íû«ÖŒ feµv9ýv[âˆÚÆ#·ætl[’æ)Mv‡’J²Ròò³L)v»_ŸsÖíîÒÒeS+*Ln´2–Ì‚!‚ùŒèy ¨#A¶#7,Øì&7jˆBvű ¯Æ5“ƒ>Ír5—Ô9) ìvfuH¤ÂvÁ/¢†A<ˆIÒâéDÓð‘Å®#”= Q²2ú->®ÅFbŸè)tµÇÃ{|êäq³ÆÏÖ æd¥»Ç&µÛ4E–H$$ó3„…¢¢©ªNU῵JVÝvX½Âû?÷{¯RëùN‹EqÞtÍÕW_sÓkÁ\JTr™F5VBÊ}Ù ¨Y +Ã@²É³c?Ôlrdá”E´-•Ybö;† ¨mŒ<”¹;T«¾ñˆC˜”Ã9£FÚ´ ãFN5mHc‹êr¦hgØÎ`÷s"/8\˜ü/± NüÊ¢úwÛΗê—?ÞõËïÆYð§•K»3H;ùZlérPµ2ÆŒ©bK[>°léJ±/V&Ò!õFï­dÜ m¶$jÝ’µ>%ö–x4)²#«Ù Ïk‘ ôÛ%³föóysf¶ÏjŸ0®®¦¸¨ ?cFæ «Œ2Œý_b{´ˆ¾ê»íÎ1QÅ |§5ð_¿bWÆ\±í]±+ã]±íƒ»b]hxþ/±çUÈ‚ìW&üå†UN f|øŸ1䪨[¶fR¿òŽáÕ¯ìûÄ@BOv¸œ1’ ăÊH§’<þÿÚ¹Êmü/|·uê鮃߉(Þë›äƒL¼ŸBæ“ËÂÞ± µœš@©©Ÿ‡á*Å<æO"Œª•Šš&ÂVlQåó‚‘¡6Q>/Î#¸e¼@æÌž>µš5Èó¥»5…L)±-LþCê›…ö¬E¯äDá¿Xgµy°Œ¸®‹óÝj0`cE·(XqŽIßAs¤²,…7qpÌ`l‰ÜpZ6q‘BÒFÖ‘Ÿˆ-Õ ª6u¾¸á,Xn¸ZCaV&BŒ,¸²ß[.Ü8ÐCÜXá’ÅíEA,[·ryûºÅëfÏ o¨«ª¶µ±ìBOa‚ãÌ K´”09¥©¡¨VìÃÿÖñHWoŸ¨ªÌØ6|e°°jƬ­Ø‚¢¾‘O­^ø6||f`õˆðÊÀŒŠ`!ëw1sÖ-fÏŠ¦¢Ôô®Uÿûm ’v²fþ·067ƒf[jòßò#ÁäÑHb‚–¸’ç–£­Í$šD±`ºY¹µØí“R !¥T$SÃùd¸ªZý8q8V‹w$ƒx?¢›yt?Áè~ñ®A¿ ÜÔû¬ŒOÙe²Ê„h€'Q•)Y²8TdÕŠÅë—¬?û¬3F4ÖWWµ‡Ú ò}Ù.g¢CSH‚©B Er@⥞cÂÈMÑ‘HáÈêQå9i®Ë³3½ãkCþÂ’áI©,Q=À’Iêjõ+¿Û-ÌðW°ú±­¶q«þâ¤_™Ý®ØòóZlû&Lìzö».Çd¶ï–Ï#Íd4™Æ\P‹ÓS)•Rh9¨Jc%ª<1ÉN!vàSj”5`'ƒ-/Ò¦%öž±cZZ€Lž8fÚØi-£[F‡‡×Õ„‚þüìL “`×H34§p›ŽGd”7L:œAüð@Ê{ü–R¢q1pÚƒyžx@p­=óœ}öòeoL¾9ü½i×^Ü6bDS¢<4!Ò‡ÕzÇÖ´ädµÔÏŸP´jʤÒâ‘a*ef ž•©_>"/wÂCaß‚…÷ÿxÁüº§j[ÿ=5/?<ÌïßÖüê½;¯œ(IMN*.øªÜ“Àòé3J‹é[Þ *«ª*/ðêjYÙœóo3™Pr£ã6ÕM’IÙmȦáaÆÂìÈ ÓˆýÌG(]cæÙvòìBÒjÄxæù=ÚƒÌ Foj f1ËJÍrØTÔé“!ÙfИ—“á…"RiЈGçîrú‡7úôo¡pÍóO¿ÿžþ§:¶mÚè{é^ÕÝóÀ“,ËçoìëÕÏÝqÛÏÜvÛ3Èu 9§|>ñ`ì¹ (¸ìT¢î¨Þ(1½QÒRqSš‘l–¦Âim ^Ñÿ­… ?r×î»®½õWÿÏ×û <ͳõ¯6?~è¾#ú?gc½aÄF¦¼³GF…ÃØòÄ-O x ɾMG:£šŽ(2»³Ò’äB¥°¿~#Zto UšÍ­]7ê]û÷_{-À‚Y3gîùᤎà”)“¯ž99B`Ê]¨ývýòÖžÛnõxÊÜUå î»wÁ<_bi¾þÏòRþ8õ2¢’™é1Rˆ=F‚¼ÇHœf QÍ@lËò°F ![ÈÚdxÀl´xŸ"ÿøÞ€˜\öÉ' —·ÄE”a²õÆæÃÈ#’)BwŠ$A`ÀæƒlþÑ)š´6—çù6†°Ñü#3=Øöô[+ÊÅSJkêÓóñ›üfg/¥¤+¢Èàe¹}jVeZüÖžÔïGQP°?ÈMdk8ëŸæ§SEöóÕk˜"ü¦›€íûèU¬ETž<ÓOwž hcÅK ’ n’³!ÇsÇÁ½Kÿ-4ÿžY{ï½ú曯½o×õʘˆŠCÀ…´¤‚áväþCŠ:áØsÚ@$qФ8g q„ˆãMê{ö1,õÑ®)½g u¦¦aü6; CjÈrÂçϼ÷ëgž^ …ú·¾…nÜÔ¶°cƒš‘k0äI:óÛ¿ÝûÒ <þž¸íŽç%K0òGÍ#>Ò€ñéwÄMèD7Ðñ¢.ë*³Ê³þȼ\ •å¹ y iN&4ûÀ§™cê %¤„‚!¿7èU¼<çš0pE.b~ëÒïòóŸ×°7N¯;¨¿ÖÙ ¸JFêŸÝ»´¼LÿÇ—Í3³|9ªšä¯t5”u óå mõÍw —œ˜ylÛ–u?;gçy[>¹äÂóoÖ?×_ý¢¦´â¢i-e¥ÃÒÒ”äñ㮟3—HÈNå!ÆÎTâ&ùÂ9„¾J ’¨ÈPñƒ¤š ðž!2p¶iš`›PNl¶†âŽB抡¶s³Ó=XiÇ“Ÿžå(1¹•18R§ «€­Ë¬lþì™_}ðô’Ë[8Ÿ«®¡Í÷Ït[ûºuímk×ú 2 ªç,á o[¦V뻺G½óΣø-–ô*¦Š•“‚I`‡, 6¤€øA³™ñ¢H˜¯€;t ¦D üÐÐKÐ6,¨PÖïHaÆáŽ%òp±Òáåyå™–Šô.§DNH")?éü fEÞjÕÆ ÅßžéX³¶î†¥‹ÇkÔß„Z(Ü¿ŸÑ¨rÒ„„ECšš†,j[ÛØè»ù&FžœÇO¯¬ZÏœsÎ3'}÷¶×ÖœX0ÿ®»æ/8z'û‹P2ƒQ©“ ‘.’…W³“¡”†JçD B%…#ØËe±yEêr–;‹eç²¼e›Šê¤MH õi . -Œ$ÎTÔ?Öù½¯Ÿ¼ùwçõØ¡ö§oü¼|óšµç&ø  ²…„ë®ì9´MïÚ¡ÿsýÙ9ÛÎÝD$ßv.:øI)™žœLªP‹Ü€é‹²¢ÉØ-Ðz°ÚŒƒµ €‰¥¥¡ k¿„†#lp’ꈭaTטž¦yüéQý©\bkR'îÛÿî¤ë¯ºêÛÛÿ¾KaHL™rÓÒå•+šW¬iÜp>´4oX9th‚!ULš|d€ëöÛ•ïëúámq†/¹¼@ÿg)Gé϶´¶A°S½’—3Ê'g…g xáUÉI•¬’…lDQmÊ ¢iâF¶;ÏʲiYùYù¹9†³bi!&j$D’´ `Ô:¯«%N?Í_‹J†kök!bÜË8ôäœúÜ 9¨[ñâÚתé9kÖnIT¯äÆP^H‡ú¯õ1fµŒÜÚÑirkC¦T 2h¤ÊcÈäsd,!ùd–ÇG&´iÁŠ ‹¦ÌÎLG?J¡­0 }¦Ç6š$Ü$pù`8·{C~1(ÞÉ]ÃéÑ@†“›ÃŽjÐÔ5*ƒŠ$lé¨ÙÕ¨þ.kPßV7NÓaX&êóP~óÙø£ç†C†c¢qxãðÖæòÒº%2¬n‰ä܃¤"¬²zþ=JÆúþ£Ý€Í%Aföt˜Û]гÔ!öúr›EâŠÞö¦‘¹Ø¬o>(žÑy\ø æþ¬ /öiËMÊâ(õ»‘h—Âfõ¿,²Ø@; wìV÷ýßtî¹î}Á=c¹ypë žÑÜ=‘N|dOØáføy?‘'Xb7Yfº⣉B‰í7Ÿ‰;ãU¼^Eöú¼¾ìL ÇI°®fÈŒ…‰<ôæŒE£ÿ<5ÛîL'.’GvMz(`´¾p ˆMVFš9 Øþ©\„Ä>o8Š`@Dt.vôv-Iè§k‰µ•Pܾ$=ÏD5BÝ€¤ÓÚKœŽC‰“T‘!äaG)hjÈZ$F³:Qœo+Œ3ÑadÄ Q¡•fï–èô÷T¸R­ÏcñÀÚsÕÕ®4 ÕCª‡4Ôúós²Òª\UÉI›"'8“£ ( òRZõƒ³À µµ³fÖÔ:®RgÏ]3˜3Õ5eæ¾1Á¢`ј«¦Léz{0[ E<}ñŠ. »TâÓ¨  2¦èFTþbl3OØ·Ù §Ó,sº)ºyÐ*j’Çy‚÷A,))©*©  +ü¢A½ØWj¼¬5`nÀ ±ïäÎÈ>+]ȱT)ŸSZÞ»õŒMï›Î™ãˆ*[¾h•dÝšËùŽr !rHu7‰FÚ^­±TUïµN§Ë_ oðóD–:Oàð{0£÷~ý¤Nxà†ü1\sôCú1içñ©—Ü6òxíˆ%Ç»w¡ªÏ‘Ç3ÓK)Ž($i^P%;P(À‚ÊÑ ¥€k2DöøfY$'íˆÄô"L!Geÿñ,Ìõa _qnqf‹.r¨2a*ƒÝXÙNžÒÀSIy«cÅn¬öÈqÏ>rçC{¶ÍþÉÖ;–”—¶ÝÖùæ<ؾÔõùçtΟ߹YŸÓIóÎ9ÇÅw¼¼lÉë®9|Õɯôþxrÿþÿxÿþ'‰d†i¤™L„ñ‚4uàÐ Aµg‚¢¢—IƒÚ¢?¤©ëÓåphìàDƒ©íÈI÷Ým¿JÚªGŸ¼çÈù[fþüù߯|ê‚Å×#µÿɈ½yóÂ…7ªûPOûv\dãA9Àà _awÞ‘o8òØ7=f’ÓG$NÎÇÄÎËæÓY³ñHp´ góÕèË-ò^§*ˆ(C¯[m˜í ìÆÕQâ\‡¸³ûŘd­DLt (+b?Dõgº á€4¤Û_œ«ÿigÛ#‹f>8c×öòk½r|å“ß[†DÄs+BDŸþæ·ë²s ©²¼ó‚3'1*ÊTüê„AÅ#7ÞhR‘+-%¤|%¨XȨè™­=INTÄÕ¨ÍÅh7Õ+bhâd4Žžó¡(bð †5‚¸v9¨'Pò—•&%ò0†¢`s ²–Ñ%I%ñËðJƒæ†è=såéØÑ™¾u Š%¢/U•ê&.ÂìMÌQ4–œ$ c–,:d\H{î@ö8ýf½öÃÈ»qí­wêŽ;{¡þ!ó²Ý½hù{oö”Ðû·¬½ò{¼Ac¢˜* ë±9@¤ÒÉ‚ÆÂN*hl3û\´€“ç>[špy8ùNëü¬`<úgù7vÀpýÃQçÌÑ?”ÿ}÷¹çÞ­¿¸x‰¾!ÙÚqÙN %GO}%¯âë0“,ã—éŒéhÃÕ¦_‚§Å!üL‡ÄHÖ̤Ì(¿„“³ÝéF}²»Îa&k^ÊÍýknî…o?ñ9M›¹øSù¹ÀtcýO¹'SŽ}G÷¬Ü½ñ8h¹¤`C"ˆu °d¡lò’œhÓT…¤@Š"Ì¥b*/Î%•¿½`>›¦¤x駉k×ôð·ïÒt\Ðd×£]˜‡o) …ë"ÕÀU‘ñdvØe—âêõ5BÌ„ôqn „KؼŸPÎË•ßOâL¾ŠßVédh¸Ù(saiÑÉ1ĵEš‘:kÓ<.Þ»)Ý–Ž©ŸV›W4pT·è9½6¬hX$K3·U…H§Qö¥ÐZ[b™†ÓÁÕ¡…ŽH¸Q_‚7ߊ?È,1QTˆ½·2B™¡Á&‰J±X)c“»1eT”IéábßÎܬ^{:N/ì—Ó®8œ¾[? Ä øšæpò^ËýA\FsnóŠüÜìÜB‹ká,yºë¡¨™E &aSØbØЏž«¢@:pOµ"‹ZŒÃc~;BTŸ¼Uñì=Ö¦K±v„®§­­ºuQ?( w2{ÈX°6Sf]»v\–¦Œ$ÔÖK’5ý E"£ãÃpTl#áÏCŠ`+‰^ÁÛŸ"®Ø¤©¯@=ϲԣ…å®ã±ëM_Ç(²‹$JriØá*g€D#¶³R¼ããlÖ6‡ÊrL[±lêòqŸÁîKL’X™XYQ* ²}ޏÄþ޹ ^ÉŠhˆ”¼,kåŠ×æ™D(‹\]?-)‰*_!­Ž·ÿܼJü”ðD"S%6 R3ÃåÌ>ð†Ü¾0Á¸ŠQ.+ÁNɾòÜò¬ ÌaK°ÛTÔ„ ÁK5 x@ 1\sâ‘hª3–©?Ë;£¶)±þŽþrï-Þ5kãniÖñ“¯w}@€‹Pò ^-^ŸBªÃñ'lK²¾¯ã3­Ñ"hÙ©oäélæ$«t²&F:aÑRª"“$Hµ:¹lâ~û ¤lǾ‘V?ùÊ=èžþF«­%qbhVtýKÁÀ51²Ij2»4l²SqZeët¢*-Ô¿ó8ÎzæÒOµµ¬BÎu¿>ɧ¾h}dê{9RN,LCL¾Å¹(ªÑ_Ý‹¦ÖMFXÉaNú$ö+³õNªæè—¢¹MÿRžÆ&vò–e2‘/êµ¹E³;˜oÙ_È¿…©­É0µi(Ã/ô×`ȬYøÇGz³ #ÛϦN=ž‘5¢«Qc+e¬@­1\Ã1DǺJ˜XÏe¸T,˜ÝÀ—IQt›Ö‹:F— ¸jC­£¿5“îFÀl ˆ'Iƒ>qÐ ÉÖÿÎüùúß—Áfàœ¡•úæÒ’%µ±ãÞ—î;®»Þ9¨ÿE¿ŒŸ@f"ïaÆÞ"2íq¥2¯«æb'ˆG£È %„UQ¨(’œIðgü%°_tF~‡åGÜ…ì+Àò#h¬0"ly”‚ÓpF DQÿœqÔ—¹vÔ¸‰g\ESÓ VF£ÆåùÜíª­KsŸÛÏ5ùÌ–Ë–.Ó_b²Í[ç²:Ž•;zjè[[«ËŠ‹wõÔDt7’0úÉÄ Ç ¼rDyIGðûªusŸ°(vÞ8ŠêšÝ¨±gµ1ªýPD±ÛºîjTì¨Ð!oá.ªùGTP¨€ÄZL„»—ðôéè-«œ•·ßa U#²UÀ¨B4ÞXå~Á• ýkDÿ›0g.ÓBoÑ/}ñ©ý­»b§Ü}<`©QFÚªDÌ>Kë6ÓŒÌB¤@Š?:1¿ÃìsÔ)Æàz0 NÃõ âãAna©ÒoŠ…Ý°N=z½š3= ‰ÅöœX¶›ÒÛ‰½b‡ˆÚ”¸QO /¸¸ÅÇ ‹q£Ëí1ò[;‡›ÅÇ$@µ4Žëè§q\‡ééŒ×8®p ÆqµûÆU…–ÍP ¨ü‚õ»ˆî4€Œ@=«û}s]T‘ N¾8R v5n‰5™ Á’ 1†vLrˆÞq†Ô]Ñß84Ò¢ÓòPgÝÊ8V‹‘Æâ?.±fˆÇâEK¸ÈðaCkkP"Î'Ht>â°#¥ÅI†T?m‚@Ïd´ ¾nõšwpQW_?®`јž"Lq[T)$ÛÚ¾®3º}3 $ÎvfÛ5´‡ˆöu´Á°±¥yÜ4*\’®ºý¯ìüìŽÛÿtþοܾòÜsW®:÷\ßU`{ì1°_}…ÞõØú¿¯þâ™;î|ú™;îx†HêP^5/DV¡üÝa*6ò}àSÑc°Jš7ûK¥‡¼¡”$¬¸ÓN®È@… #ÂŒ}Cl¢Š\wáÓ#ÆŽÝù×ÿ€ôËF>wÞš¡C‡æÍªrfû‡:T·ëg›öÛ‹íjÿ™þåí“f}±`á½w/¬.¯)[x÷½ XUI)%›ÍJ¤Y±}ßÖXÎÃú¾Åĺ¾eeb×·ÌÒ¬Ò´T-Ö]ß,–A`nf|ÃŽÓ!íµƒA_6ÐF¡ÒJv Y(¦‘›Å~9p#·ø£˜/½¬4ÒÇ­´µ¬•õq+É,d·Á&*'ú“ˆcC:xz"ý¿¶!\ƒÛ"å¸ö¿±(±3\_“¯"ù¤œl5ýr"©D•ÈDUi›&SJ;ñÚ2ÿ+ÝÚjM gX¢Èd%ŽÄŒD¼ uŠ‹üååXùÔ]TTgÅÒ±ïZ:eÞ£Íì½fõ Ȥ?œ¤Ù;ÿzû¿îdè_ÿÑÈ„ì¹ÖóíšíÛ׬޾]¾Š•C…Òy5Gôo¯¾´#G Îï”yõî‘Gï½ûÉ'ï¾÷¨¹ÐV¬/ Öð¾MÑŒh¹MT4Ḭ́Ûò|Ì8áL±yí^³!ZÀ½- p6Ãß>øænZñgýă^úŸîÜùé8ra;ÆTE ¾Zÿöbñ… ´X< M€K _X Zšy‘ ©þ¦Ç­ÈÌדçÉKJ@[¥¥QÚ@h‰*©qPâ1R`cöºs‘"ÒH•šL;¥ÍßéíOçରGU¨-ŠŒæÏþFöµX=¹8„É´Õ•¡ÆâF–|^ä):½ TDSÙx¦Ñq4šUë£ ¥ßþhgÍC,2ÂÞolíâ¾VTs•Ç’6+š´ÑÃ`IÛÙ—´EA4¥²Ü䯢ÆÜS8{©ýVðTì?bá–ÎÈX“ëÛƒ¹2O]&?"ÿ’ä‘rÒJF†G8S“%ìãW.I4(¶ñK2>ÏÒÆ¯3ª_eEEk%»s²B kÎÔÔÄ›øç]¼>~"$ƒÿÖk„i0bðN~™wÍ»|ìó¥3þ ×]v03àð^6òÙís¯w#S3uÍÐaòo:Z˜Ù¨¦ßðåÒWö¿©¦Ms$8 ¼íxºdnÑìNý+†oyå#Ò;5 æß—æKJdÚûÂêÝœ… ™~}iNb‘Iö©ËåÇÔï#!H)#“Ã’BjJ’$ÓÒ’I‘3Š]ü2A×·^§ÑG¯Ýè£W”––„¬-ô›‘âB0ˆÀiàB:ýÒЕGß5±÷ІýBvêÚ˯Ÿ´f‡,[µ]Qõ»‡©ßgê?Îíê9 Ý[Þ}Lÿæêyˆé“wÔgœxê._‹O_Ú`nšj"ÍdyVDZ• G”Ö$”DÞ/Á̾â-Š6éX-¨ßgÂ¥¦dy.þ`ŒVlm))æA“Z'±Ú4Í%Íþ<¶™œÌ´e#!% e[Šˆ²Ã õ\å1µ¡£6ñàv—Dߨ¡þüm[õ·YÞóäÔää’|–öì Mq¾[˜¼|÷ûòGŽó÷K//È @W yzfUuú”‰^Ì€ÖgËÛ-1ûo9šŽ‚å¨iÁšC¡¨+"HÂcfúõ27œ’šb¤ù¥ˆ6~^‘ßÌèæô[Eº@ó-|ã™÷Þ{ê••,•ñ"É™5^Ò_å Î?}ªç(Ýe&9 øU7É$Åèggðƒ—ÃÐ@egägg§— °œ‚¿¢A^€sŸCˆ{À ˆ» Y¾Õªg;ë‡Ô5ý`~ÕÌ¢üQYn[cãÚ6ž‡…Ñ"‹kjnžúá¾ýŒŸ:Ñ“åW³ ¬;iXh²fÀ· âo7îp€D¤^âÔøan/?ÄŠæ(ñkÅÊJ£ù!†Äá‡3­ÐˆÕ€ÈÈÃ(¿dá‡äíxî©_¿÷ K=Mÿí"ãòÀS0<Í/úößt—™p ä„ÈS¸-o\8A£”H4²šœ ¼Èj"ÂíáK(* ЧÂa„ö/‘‚ngþ6_ÙÃÛÎ[½ø¶\Ь“Ûl[Ó¬ÿuÎ9ë²&¶ôt ìo©^ÍM¸‹Â ¸ý©¨* vQw’0䯝¿Ú•Ø+j3½åW•¾`J‹›éC÷‘­ç®YtpÉÅE.ææºZ¿ô胋W!Ä£†­]oøZnÅ]NáÍíT‘RDEVΫ)WQL‹cü«a”0»õ ܢª°ãÆÝÌ<×ßè ýµhÈ}*~Ò­WË;…wQîgÇp?ºóS^œu`o vAñ>{1‹4ÐØZÖ ¬0å²k¤ŒÁ[€N8Ôm6¬2{óuÆôæËHw¦¨2)€á»ÁèMÞvªÐÉÄfÛ)£éT*]üÌŸ^ùÉ›K¶ž©?û÷Oæ<°`Îg-[»|SÛÁë}cl:·qdfÚxî=’›Š4Õ¶ÔTœ‘å-:ô}ývB ¹ZZŠ)šÙ È9½öú¶Äãd³TOÈc1“,Ñ¥T)íÓ¡}Ƹ<`¦”çQ„[¸ ö¿õëů-ÝåòÎæw|öë§ŽÜõÄâ¥Ë7%Ì:p£‘õðhjù^NAJZ~jZáÁK=2üYÝR]5"[rÝw•~—•4Fê(@U ¨¬:x¼8mèxÝYKf0ÈKMÖmÖtˆ‘Ù o_D ,üÐbfë9\ÚÛ;ÞÚ¸dÙªù«ë|Þü…óÏ<4eʧï.xÑYw/]TS·bc¬ƒ×oâAµˆf ÀðôùrÕ>_m¹;­èÌ™Ýý˜ÏŠ4ÖåäÔW„³¶?¼úÙgs/Á¾´fÍáŒÑ ÈcÊ×Ê4J%/M×n®síæ¬)è¬ìd¸¦ª(èËNKåÕÍÓXÀ?ƒ$›t¼¨pÓ¬«¼ùñK.¤T,*¯[×tZÒV¼÷Ù#Ò²ª\ñ 0¤ÝT³Ùrœƒ 3÷Ù8‰‹H)6œó‚D3úm8×9è†s}Ε–‡²3y´"(â~âAm!Þnnù@¨çNÞjîô;Èè4·“m¢T7™@Î1"‡kr ¨ P\0”ÀLl÷2œøªYc &)‘Döló7â¬Å_+¼Xû Ä6œÃF&ÀÕÀW‘iŒ+* åàH“yîGÞG„ ]íè×£Z›ÇZߺêŒSÏŸ]Ñ”:9PèÍ®[rò€¼âWëšë·ný:YÎvW+S“ ÁÄ‚‘E…³ê¦]RS]M€l8Õ-×ðôŸºpµÃNéxÐ_oŠ–—ûsó0T7¸E €ˆ:ýžH Dë=OOѹ„ú®I3ˣfڎCín}òñžO'ŽA~¤Ýº•1¦Ø®鋪èât#FøÝ­wCðû³BPÿÆ7Ê÷þôƒžYÇ/¿€î9yðÞ—5€S׉øÄùs$ ¨Œàe*›àù¢û¯uˆv[íëQLáÕdVÃaB=Æ›:S²OñE» Ð….À×TáLœ´£l ¦…3Ê92ù&©ëÙÊú¼vÈš·«õõùEãÎç˜-¬«³à¦’|²K ‘1Y$OVñƒ¦˜çjnLÁÑcÐFã¸ÕȦYmŒ²E\ƒn›†¡µ¬¤i’–oËr €«éüG¿Ø?Þ?¾’À×pþÕÇ nV‚¬"7+Á¦šÜô;¨ÝâÄxÑ×-AÔªŽÃ¸™»Þ`uò¡è8ìg&ž~à %ĪæfÔdÖ ÚoØ/Ý¢½…óã/ÖMØÿßߢzqÈÎÁÙÿ‹ýÄÆPû)óÉçád†Œ¤Qí²Ù„¨„È*YeL‡3ñ‚vÊQ\‰¨¯šã4EŠ3Æ9ÈwyÑïw€ ä,d¿–´=‘Áñ‡ÍÅ’Tã ²!öÅS”Ó Èò‘£:/¦÷ >ò£éÇ(™wÍFýyŽœ8çl*v¥À‡îGîu,ïþǽ VÂÝHß-k/ÛI÷<%©ÔÅ=bãkG#Yd«±z2Ä̰n@_¯ÒºùìTì'¿õ÷Ñ»ÉqÃ{ì6Tœì޶eÙ³ú8*£0åÈ[ü“·÷¢ÈQV× ßäçìØÒaèF6…ònv›Â-6…;zSˆ†ýýl ó÷m ”ˆ’˜hÆk9˜.JW„¸gòÑ\”?<ÝõT4ð&f ÜÏåvRD~,.Ý$ J¤ö²4<²ÍO%˧Í[¶!Êšµ³¡üùÅ ü5‰¸C¢†s5Q­¹ïXÑÎÖÄ—ƒñÝÈ`CK±ÅTk4Ä.hg¤³ÙÈšPΈY:òqÖu{Oò¿oÁƓ؀²kU¿»‚ËüUä&ƒ> Ú>à°™ô)Ž0gÇóN4œL¤ÂMT>ÐP¬ÆËÇ'FTús}Fët¬há«Ê­ŠÎwH:M¾Ä¡Rwü”}w ¥âf>Ä;=€L'DÞ%·Ç¶©ìˆmSérùÁ?Jþ~Jô[õåöî¤5'FšT²W$bØ~¤Ie$b‡!qúT²vø‡wüûjïŽÏ·]òÕëú§—^¢Ê^|/mèy]šwò Ûód¤!?ºS1á+ÒÐbN–(z>/ÀpT³Ï`l÷Ië”’Ÿ‹®=]ÑsãA{C¿ÓcïÉ–p“µ÷¤¥£$‘%U’Õ pU˜M%­%…Œ©Yá0Åʱ€ < Š$@Éý$'…ÇGúIEÖdE» Z$Äóë»6•àÆk* ³Rlã^ûïsÑËÅPÞ4±Ùo‘Ûû6ŠìˆÓ(;p„ßÕ÷Àì'Ÿ‚Ùú%p‹~ÿoé÷Óñ4C¦öü¥ç(ìÐ/!@Æèsä;åvÌ™ ç94Jx—V®çJ‹ GPaš§ßïe”N›Àl–dìËÏ0¯¹2WŸ õ@þõzUîÙ»Û¾üiBÙ¢Yò,tõý“¡3¿?‹Pc"NC/6YNI¤Ô$!.B3ZYÄÜ٤^¿¥cp¡š“kÚe¸LîßV:Æã­p¦[ ¨X¸í¬/¹àzFrªszÅÐ0v·ÈhTâEŽâÆHM¢’L­µç„ùB’ÔEDUש“išÁQ±%$– »5pRl‹ÍÛZ¦4e[aj½gë—BÛ:#sâ¤×Idl22:|F ŸÊlwDGXâÊâÒ¶ˆhÚ:cg`ZZ?+j_8ØqH gYwÇ·ý4zŸt¥ HZ‰¸‘ÑIŽÓ0©*Í3kc)zß*úö~éÙ‡š@î$Dnf«ÚNŠÂ…PÜ1fâP?Yn=ŠƒwÒ©=?cö—Gé”îÕ¤ý±§ý¡Æ+9ZI¤6\e—)ZYúv^äìAÔðˆíº(&1s6Þ²N†¸œü{ìŒë°ç"¦_ñž‹ÅÖ¥qz#vX{#ÆöE´NqhèDͽ'OÄÎŽ ÒC†…[œ‰_kza¢‰á  8DM‡ž7­`DØÉg"€H@à°ô®r‚jRO>šôP6vSð•Ü4O40´| â0lb‚f~&*²MÁ3bÓl‹ /ʲ‡‡Þä¡7 ¢^`yÈý=®.:!F=f³ig‰„RÍ63 @êëjkÊJÕ…ÕX§Ä™b×ÐI˜ˆ´2 ç£Ñ<Ò´ ÉHL „D܈Ñ‘GØàjʼpÊä…óŸ¹-Óí]7côøðг'Ozú‘]µC‡{JÇg6®X·,7ïŸô¡žéôÉÑ£¶ï52+»0!!)ÕYðt¦3ùðzxAKVö!Ÿ·~Zn}suNŽþÕýD‚4Fû?3Ú'“|Rˆ!Q[ªŠÒr²¢Èg‰% +SRSð®.ðg¤§ä§æÛT^ª™cf Vëi"B…Òù¾8ÿ¾——.¾ï¾ÅK/¹obiiiÙ„’!ie¥®ý-úß>ó̦ 6=óôú ¶~݈…Yáuë‡åô¿ÝO(ñ"?¤¼K4â eáb;`f›2¡ Ó„ÚIx”ÖÁd›Íæ°9P½Um¸?ý\¸’$x Võú ô-óôè//¿Ly·« 6éWÑetiÏm„’›Ø,›xss&šÊP§ŠÙÔ¨–‹/|wÔI“““XUzO²§k&ÙxðˆßY—/{ܲß/Êöœ7aÁ¼õÐ),óu½^½uëÕ;”wõnýÝß~ýõo¿Y²cÇ}"‘ÈùlöK”÷ù칸9PøÞ4ÀÀ¼^êâvE|½éÉI¬é~®7×§8ßfC³P>Ë¡LOçÀ@WäçÃqøJÿð¡CúIý·zߨ¶xýË/_¾yë¥/+ïë—ê¿ÉJ{åá÷^®™¶`áLýžžùm[ÏíÐÇ 3(ç0)$3œÞKBį.Ù¼)ü#êúˆêÉ*B'µ†©årK[à¦I‘DÐZÄ0O&nR®Ä=éL¢ªœµ“l•°±@JJŠ;ņx¸òí6<± µ©Î@ÅP•ÒŠ—e”IÓ÷ê¹à}[×sAž]#O iŒû!))ºÊÇ'>E×vÝ6åĔۺ¤­´­ç_4ßÑý}šyD¿–éù3r>3Lfgv™iÛæ*EÊ*ŵ<ÒÒ«¹×Ý‚þCl"ÊÊu óf3Ø‘cAü£@ãµdtÔò#„–þîö?̽æâËv8°aûŽÎ;µòÕEí¦+nyPÿÌ7Òj+7-í¸ä³gœ¹`$æùß|tß}Y9/~ÿ÷È6qhØIn8[£T¨:üÔæÊ5bÿ²üÛ@†ƒ0Œ~‘Î~ò%yXW™<ì$º¡øë´–ˆãJ¦hU´ã ¦®T{ëT.À ƒ’ñËì j·EwPU̹éªë§\Q|ëÅW¥î9× ‰Z{ñî;v_¼0.@(E–†C<Ð̪H"–lm ˆ«Ø¡b߈Y¡…ïš<ët¸k¬Sé`S=ÇO‚ü°Ï(…MLQgÁ]g¡Bð&LÕKà/úQýÀ<ÙzPV|¬{=x¯®j¼Zåá\Ûrÿxà4qH›“ч£zWŸI9>ÑóÉbó.`(¥8—H’8Py’‘l Tätzóñ>p¡–3œ6›Øã¦TcÊD–cùYV%i…þþ{m¸í`_Þ±áñ£p˜Ñî‡þ¡_¥÷LÚD€ÌfÓǦ3¬C•$|ï‰^`RÌŠ¦¼… kÛºR@rfCû#ÃýÀguFÄMô*6ÁážNzÅÉYÈ>BäñÈ ÄFt–XNùkÀ³¡Ÿ#z„`„Wqæ¼J›»ÊèM=«5òbØæOˆNm¶$‡¶~,z\‰ÙIÙš‚ \µ™í{øß›²š-²¯Á-b׿ֻ-¹&>ذ¯KÙÁ·‹}mï›WbAVCc¿V­²Xì­ºOz.‚ò„ߪw n8ˆ ·|§<ð]Ž-X1{W¢ËY˜¯rôÎ^ñæN¸>"h­×?0%-”²zÞ‘BBÐr£f%£f!ý%…ü$*QTS¤æHS$óìt§Ù5R… R5ºké£ÚÛh,ÂE÷àí·„²³§N]1.Ÿ…cœÉþ;8e|`LÙØ)S|¯¾Û`Ç«¯¦¥êGK«’)î9Ù}á.jUH?Z˜–F€Ì#DÞ!1(jBt1I ‘kí]}¯ìÓ™ä"ÿ¨®Ÿ¬ @ŠÄ™‘ŠÖ »jZƒjMkP„úz>gV¬¦þÒôÐèáYMz&ÔÀГP£ ¿kËk Û›Ç^#í¹Ž®=9ï–ë 5¦áÇr9#<Ü“ÆM1¹@hë¨5mu§­7ÝíJàh†X˜èfë ýt\#Gu×/b •H.ƒôQFñ‘ ¤Á\_º+ÁFl"Ú…AÈWpV&eÒt°ÈHwkj¨ó¤oš'àT6öu"rþúý·6o~ó²Sß³³óÍïŸêœW8wºñ÷E‰BúÁÛ!ýH⊉é=xPÿìÑÄ‹äï\~öÁM©o]9çàÆT$›ö8Í4 ö½+Ñ,hãfÁÞ?ïè³ò©àdØÏÇô·ôOô?0íêßôc=Çzü=øBÇõ´SV0ê‰WaáÁ%„¼2yKóˆëtº ðìCz‹P‘tÓö/ ͽçš÷Ž@`h{ãå‹  Hõ)3~òuêuííþï_wû7ä"&3ÿœcç…*3pã€%‹!uÄrú#‡a›á¾îqŸ~ŒÝÿUx^pÃéTìnå=ƒB2¥‘GYÔG®sF¾üSO<¾Nþ\yïd¥üûþEWLa¾ÆLAyŽhT›¡H!Te ¢vÈ:ËW@úu÷¹üÝ#O00«å·Ø÷Û'«9¨ûôgÅ]0ÖPL2ˆBˆ²Á Á dÀ0x‡(D¹¨ïo0è9šBâ þ¬ß)îŒ;õƒD]eú³B]žÍU/ªË*(„‚ÑÌ%ê„F5Å›’^À^_¤!¯±Øs!Ÿ$~pŠS°²þ¼¹àüVÿàNý]ý9)ù™ èEY˸ø×¯v•±ýi,ܸhݺ6BÉÝ €ó{Ouq ®$‘[.Š>Õ ý©üT£Aß ¡õ T§þOý§¨?ߺyëMìTïyúØŸ}ø·%;v¶ëc å[úŽ8º»8/ÖÝ!ÐgfÈè¢þ\sî§`:Î]¢ÈïDO.‘b6ù9ºÉ†cÂ!2l]Ö‡öþõ`'zãÆÃ[·ÞØØ+¹ý­tñŒ‹KMf¿cHŒä2YŸ`…‘z¥!l¡T^¤²ªðµ›¶³ÙP‚#—lj ÞÔýŽB{Z†7/×ëÏð;50š‚+ùH+)üN+rOÀÈèÏžJò†È×ótÓÆÃ[sýzq²‹I-³™ÔRŒR î÷öMUŸ)µð } )†b%FÛ …øV¯«5“D€.ª{ã¿øeçÇ+;·Ÿ9éšý÷Žù“×ßjqñÚM¥îiwþøýKßj¨™9sñÙã—ùšš¶^·ô½?/Ô7jìì©ábwiNõØ÷ýî#ä\r6Ù‡×ì@ÁÁÎE΢È(sËŠ$/·È\ŠbÑWÍru o¼Ö+Íš)*¶ÊÌ¥¼žJ?a'Ó‹0Êß4¢éºE+öNpС=/;&^ºôúkGÞ䃫`;ûº2ä×Öžµæåz÷â×^¢-("@žgÀ.fÀzØT•ÌIF È*×"@؈ÊU°ª–§$Ôå£ôKý¨T×qðáÛ¹7ÛRŠ{~OsKôܶÿ’‡ †ë/ú aü%pÈð½K`üx}³~)ûÚ$Êb#7)žÞÂ$ƒ¼ØösØZkdKmMÉxkăÁÈÂà BšAkEAÐïf} LQ~Ûž"+„ÑÖ—®ç-ðYwê‰]³Ä‰ÎšsèĈӢ6fýó˜m#^“!¦ Ã(E=«_Ûvö «6¯Ño¤W÷U´Àì9 ¶\@¿¹Û¡ÿûK§Ð¸x@u“bœRî!–ˆB˜_˜(˜8ð=Ôh3$‡ŒôŒbôãh0*;75ŠÛì+Rh(*Ð4‘ºtë³Ò˺2!dö̼œ¡çT³ŠÊôé‹KÆL(pÙ½éú‡Ê¨wÞéÑí# Ü.›”ÍZëUX×ó2|³½!˜(ùRRò—×k%*[Ê¿`K9ƒ”["v2Uæ…,Œ#[óHµQN¬l#A«0Î8¼ªÁ60Ɔ‹Ía|€Èïoî3J/¶™ÈÎJK5£Ö3 Ã.øQ$ ‰o¸„>„4RHOgWŠ™/›*ÕϺ¶~­sSéBŽæe7w¼Ô^]Oå?é=…«sÝêI4 Éöx|‹V¼=[ØIúäÊ÷Žlx¹MýGú÷õ°k­²iøË¿oúŽSž?ÿA¶2æuŒ­ÕèÿÈP[U $.à (ý‘±ÍŸ,-7"„k,ÚäRÆ]gFF>Jƒ¢PW43‚ëMÑ\7Êw5\ºôÌ(¶>Æ8ïJ-«Ê[<9/gØ9ÈùÇßú «èoD³ÖXÕ¾<·G3Àßy™/7!òlc¤‘lô*ZJ+„Iì2£Ô¸ÃÐë&1·‹ù–³ÝÙLrŠë ÌZäž¾ÂPrW„`”þL_‰H^öÎÁƒïü2J&2mN’Eüdx¸U‰(²¤XÎ{ÈáÈdav}ƒV.ýY´rކôbH9v$"KòaŸ4Ú¢üPd5_“hعàñ(ûWî ú14wõš¿ôdN·hmVݤç¥` EJìÖ'„.ªº·(£HØÿñ2gb8ÜûâÎßÁˆQZÕt]sëÖjSvKwžPÊ4+Þ;dEmv^a¢”•mÜÛ—Ñq»gå­ÚóÍÿM2>5\–Z+ÚËÈ-áD&žáIKÑ4+&ãæŠi+¨*]gvåÚl‘ídþ!kš;êt¯™+ dsk¯“9"{í½ 1öÞx>Ó(§œžϪÖßžQ8R·°‹ø(#r ~N%(ªŠ’b 2DÝÊ‹Œ[¹ °Ì[¹É¸–ó„5®e¼—ÑúŸ\º"X¨;ûZóÀ6snvNB°|Þ¼wójÓ=pÏU5™§ï' )ÉÕ×èÞ“k×¢{ qws™!sSv +qa ±¦Ä`9‰ÝÌ™”Ð{?ñ èºÈyÜ5ûÛß ml_.fû#„F, €ÊލD„Su1nÞµÈvH£qøÙIª©ì›±‘}‰|¤HÝ®Æ<4‚LÖÍ>ghd£@À²QВ̶® ]LÊuúhd· ]Ù²[ÐºÌ ’ÄõWIGϸl2´íÆÄ>êbSWYVâ… ðF ët7okƒ¹‘ý¯F¥7ˆ(z>Qdz‡£‹d ˜^’G 0ØÁωp<ä!! Ve~>ªÛùùÿ©ºm::ç­2ê˜x+ìfÛR;8f“ÇÓXdX €`TØ`l¹ôεzvn¶Æe˜Ôµ†¯˜z†›dc}Œ$ R"¥x°+2(Ë#í.š'iVuûÓ³“0;=;5Ûzb^·MؘüFnH¯Í†«ÂcJÿ°ñüQû']vعÛT_¶(UŸ›°Ú—ÀÎ6å¥÷Ï‹¶›únæ7Ýöýð¡CD6„,'u¤-¼ ªVŠJ'Fì.š¡²ÙmŠÝ"Õ&Ífô}n‘YÍ8#/²¦º¢®²® çd”g–G5}È3‚QÃúa½j¾½} ä%ãÈ—aù–©SèÏÍæ×5ð'êÖ‘Ð¥xÑæçùr²2»úãFÝŒ²®‰{ƒD!ÕSÛï="9£ádExi5ØìU ÙšS³S³@V7;2V#ª¢ñ~Þö6¶E¸ÁÓäk¤JneyqËwõe ÏnE7i tÈMVö0ª'–¿§ òqœúƒü*»«‚djx"ïž‘¢Ê’¬¢|²h,“Ѹ""Ö _W›fúÙ³1Ç}튌u¼ùŽ,(,jðóÄ#³´Z Pé†\ŠGL2¥+?Òÿ½†ßݹrŸG‘Öž“Wz Îa!È_êÚ¯Ù\}æêý:gÃ;×v,™0!5/yöòdù7° ä¯ô jæ£ÖßrÙC‹ƒÑ‘<bFò –÷Çãñkf‘føxtÏÓ?´DñHbš¡|ñøÉ%çea“H¬Íu•ù~ˆ c5N³­éM“¢‡ ð8Š`¸Æüù¹¾Ø”Ç€W‰À-&ßæQ‘Qc­ëe“]qô t¤ŽÜÃ_@ºÙR@ºEŽ ŒF¹¿Áƒz%–L Šäò²Âº`QÂwÐgË151Ũ°‰¦×·· "ÞF²áu7£Zº'11rÏ%ºYþ¯d‰ ’*)»L{ZlhPµ£ä¬É“Î*µ›ö´Xp¥ÃK0oΖ cšÃs¼ð^3¸^.ŽD΢$Ð øe'x•l½ìû+Íqø¤eôóÔà' ¢¬²¢°Ðz¯––—„ú.Œ”Á-ŒX‚Æ®‹©;ðÚèCl‰<̈=C»Š¼N+›=š-5›wJ6ˆ_:øûH¾\ŒŠ¥|üg=؇ÉËXJ*º”¹«2«}éõç`˵Òúæm»æö^oÛk"Þ¶X*÷ç|£$‡ù¿Üh8ÉO‘ Ú¦®‰@©Oâfè›LÆ/ˆsâ-±›¢ëâØx%c~ÁÞbríãþ\ ­,õY¼>áU‚‹yÑçgßa¿Æ¼:й$–5è«b‘öüÕÅ%B4cºží—X:!ƒœÿ¸3ÙBl³B¿Ô@\,dI\Z ð ¤Dg°üüî‹é[< –7‚a[âó{¨þ ØôoºÞîƒä|Ì&ÐÎúÂ9¢4„QO¸€¨D¶©ò*kzmWy—»:Ö9²–Y‰<`Âò–§FJ8K›ÏŠÊÁ€«R¼˜08ÃÂÙ‘±ñaê|+O»ïŠ-ö¸þ¬&ŽqІp1¡´70¥Ž˜™!K)|(AŒC»³õ)FPŽØ4¤ˆT‘GqÝ#i‚H4PˆÃ®8Vj((m×Ù¢¤­’È|ÆJë¾ úÕØÆ½8ĈÖªŠ«Š°a~ª÷ƒÔâ”~¢† 3*#ºŸ¢a­O@QlÂA Öõ‰ª K… ÈA ŠrÚŽ‘»ž› ù…XKK1%EˆY]$Ö¬ƒgéí âÏøÁ7í³E¯®üÕ;¶o8p`×e_3÷íoø~ÿþá³³îÛ÷è›þ<ý« Μq¶Þ}AÇÒM•µúÑ* ß)¿Ã O'Eè·wã~ÆÏéED–Íài ñ%Ø o~.š{½Å5ˆ¦hj ¦€?ÕÀß'‚új¯&ï{錽{Ç¾Ê ö_°fè÷ëŸ[ä<¯ËÛ^†óá‚WW.àUú¿ÕLÑ7Љ}ã©Í(®D ÎŒT.  RëyÙ‚:Q¶7ÓϳÏýF2<íØõ¯f|XÿüþÛõ¿³À£ô•žVy|W™”Ú}‚PÒÆ˜ºGþÃx<éHÁœK7PÉ@EÎ¥å‘s‰Ãò#!msJ+4‘Û²FEP@aß!K*Æi" œ9´ÈÈpëø¡ãkª²3ÑúBZ Åpó†ŒZÛþ:ô¶‰4ÉF³">jz¢– ÿËÏk/¢¾‰÷sŠaÃ?îè6òB°¿^WÛ±tiûúÒÕµ%[G^þPCc  CªÚ”›þ‘Ë.ÙèÙ¥s3;G„·Ü>£¢Â·|ù‘öýÕ kÆ ³²Z»œ~»-qDmã‘[s:¶-Ió”&»CI%Y)ù ùYŽ {b±Ûýúœ³nw—–.›Z¡‡g!™ÕI¤Œ4a[/P(™Ò‰éã£jà·NRù*T êT”©«)oªh Üi¤ Ê4[¤Ã“Qc’…F0r௪j ûN¦ZüÞOßc-8ÀiÓ¨ýŒ=kg¼ý@ ׳ÞUåÖ?1#«xæÛ…O´vçðagŽ&í¾‰ó™…~æ³?¬+ÊqPÕñ+ÍN3”ŒÔôÙkVÄkÚq!òz¹$“lRˆù8Zü¼Åú¨¼E_Nj ë{Pè+ÌÊHÉNÍ.y‹µék…ÀÐþHíù€ÇÌ^¼ ~õõŸgL[µæÓ¯zNÂóãgyB!Ϭñ·ÞwÁ–-Ü'·_vKÛÊ\ßþ5·ÜMŸLLL¯­ÏNN~]÷;¶ƒˆ@%KìE§0ÖG§0wèƒ*åé2ö<Ö}¡üÉZu¦0ⵤʉúxù‹—”Á“ÿ6Þ¨®Ð/½_ …)ÙÊ-ýf/Ö÷“½Xß'{ÐéŠ`gÑ©ïpCKÏg:­ÿžW„H%é}³룳ütg:Ï^ôðìÅ&´â÷Í_=9=#1YšvÞ±ž9Çèóú×yÇÊÜS'ѺÏÓ5ÈÕßÒûõ±×-vÚóyièL´ÓâmâÄ´ã‰F'K,NæVD̼F\. ®LW¦×ƒvï b3µWlRàñ§ùùµ!= è}úÇú{sêKp÷ØêV¼¸öµjzΚµ[}P^H‡ú¯õmÓ»ZFèÿœEvζD2`äµ–Ó‰L Ovƒ"{€*ÔRÀ‘M^Á¨'Ž9ÞÒ<ŠÀëUd¯ÏëËÎÄ„ù»Q–Êa-ú…ú ^aBÿ ¥–€_ð·r‘§”Ô ‰¹4µdNŒÖˆíDÕì\3ãq+šTLͬ¬Lèe5e5U¨›1ͬ4³tð&æþ°„ÍVݬ«l•1Q:Àð’Lˆü²ÜÞOVf}¼¬LI؇%W¯}8± ”R“íšå¬5`H«ˆÆköÏ@^„uütT5 /–©cá+GnØ1}ÃÕS:*ïÚ7dõèSwÞÔøä–¯¿i9«´qÅHýõô¬â¿ù ‹¦–~ÌÛXu^ýÇ©ù\?eÅФ”©['Ýz3LzgX´÷Éͬï?7ó&Àrîªûž1= ôuVÖæõžBשy§ÏͬÿßæfÞvX½Âû?÷{¯R÷ì‚äß7]sõÕ×ÜtC€Ö˜›Yßצ‹›µwVcsê-Öé°°‡u*Ñ[n“›Y››9pl¹]ßÓ_X ˜›Y?ˆÜÌ¢¯lŸI9>Ñó ±y'1”¼ÖÜÌú¨ÜÌÈÍÄK¢Ñâêå+ácÌÔB›*Kg2Âý+©²¦âÊm'rgÏl¹ºî>yPÚûÊ’~6jh“HÎxzÉå-¼½ÊÚµmíëÖ±n0mËôŸëa±?oÏ]‚]UŽÞyçQü&@®cüpÊç“üH~§ó;s£ò;[cò;3½ü·ü¾ùMtr§JŸÕ»öï¿öZ€³fÎÜóÃIÁ)S&_=srþ„À”»|·€4ðÜv«ÇS®µ¹ª|Á}÷.˜çK,Í×ÿY^J€Ü-8ÓYß'§³ÁB­ÿ\ºŒ¡û¡´÷ÊŒf}¯>õfFgcoF§7Ö2µ/nÑ¥ŸF×[2k>å>¡³Þ45þ¯:­ÇúޏFÎ÷8u¶¦³…»[uƒ4€§oÀ—cIènŒ$t*<¡YYçÌ¥<ŸÓ)ò9E:gN,yþ‚ ž_rbþoÙ‰±kÖŒ=1žU£÷Ù¯xôÈ©+àf}e§Ž<ªŸºÊ®w*MiwÞÿiÆßUx«ï“ÊùŸWx»9K^LRñå"s9nõ^$àæÇ2°%luu\= ß<»ûÕMP6ÿ’(Ð?‚ƒ#ßuúæ·Â_NÞÎc•/¿ ·ÇdmÖŸ>k3 ý©ûi㤼•]bÝçá¹¢k‘¤M¹Ý’´Yÿ$m¯Âo^ßqƒë;Æ$mÖÿI›ÿî>“¿{¶ ¦ êÿ°·ã挗´YßoÒfý ’6pJÇ«d8ôÿũ%ý÷¨ "aÓ&‘ô¸9›æAžœœœžìÁœÍ ÏÙäuMûæl®›³÷¤Â!ýÃúIýúÎg®[w&ü¥ô†Ýn:yÐgÒKA:vì0BÉlösäö8 ›õ§Kج5tuÓùþÕ?ÿù+½lX½zÃy캜wø¡‡8&4pJjÙl7Èíý%kÖ"Y3vV¨8IswòyÁZœw‚´óxôÄÉc?ÌÑL&™±‰šõ±‰š¬ØUfj¦™¨iAÅBK^_<|êפuh…îyÇ?¡¶v‚?;‚:ޱDz[*+›³8D…Ò|R†Ùšñ²5ëOŸ­Y?P¶f¿¤Ø_VPr‰lM«€©ƒË ˜’š¥!´ÑÙ kžØ´é‰5'·&UÔ2É­\j1sš"œ>oò-[·þ`Š)ÈYQ-¨ºñÛ±"xŸ\Íú¾˜@ éˆØ4ÉoKÀL÷ørYT5£&&81HM‡0u¿°ôe³¶ÝæÕðëúÏî½èÖƒ …úG¾ÙÔÚ²ãÂcÙ ‰³x¿ÉÏõW %GO}Å–Ú.^q¥9ÜH@ç# 3r9—.ÔŒ£m%YH8¬¾bØGl†}ÄÉm"NwÀÅ;¡àÙ…‚d~œ›û×ÜÜ ß^0ÿj+)^ú©R– YÌ*ò§Ü®Ÿ®]ÓG÷¬Ü3¤é8‘ pDÑÖÎZ#?I¤iÖsÎBß "œ ýz¹¾ŒPfhо½X8S,›b ´ÑUC1%÷ës¤uœ€™˜^€"¸Q±.;ŸC;0|Œ;|é N¹žùŸ²ˆæ÷éÔ#¿Ð_¶Ð.yýEÒ¬ã'_ç9N蜭ºI–6s€Q™˜hª¢­”¹ÜmÞÝêBá›ÃVüî¶Û|f‚Oâ¬ã° íÄéÿåÿüàˆ>)ÿÆ®8jÂìùú‡ìÒÇX+ýƒÅKôú>˜¸µãÒÝúÇ„’elÁ•÷7)#w^ŸtK<æ¢ò2EÕtKÂ%ç³Ø1ƒÃ%ýŒÃÛGeffc ¬?ÏS–^–”häfÊÆÁ:øn¢!v7àiÚØèÍ¥B?å}0#Í`Ec')tå[©+NœèÈåº%­ßrtZz½»4#=µ>+½ÆåIÉõ6ú@êÒóÙ1{æï¾rh{O=û½kÏeesYµ3”S[¨ºdíØ¥(z˜Þq1cZS½@4˜ ÆÐPåÀ|_ƒZ¤×¥À¹ætñ¼ÌÆ4§à”3Љµ†I£#î½ 0aÈÆEú‡NæÞrÞªƒóê/üÃ^Íò“MÛÊ/ºXÿ¸—¥‹–ÏÿèÕ‹>{fg¡$€õUw$å’P {úæ]"åÒÍÓW¤É=y’Áµ__#×<~œ]U{ŽwGÉ63D:ɋ֖áMMňKo^F^jz*ºDËDÄç´X&\Ç](@°ÃŸ®øìŠË#Ɖ3W¯Y³*ñ£±s«íú™öê9¾Û@âãà¸*b¥ ×oºûîMúÜ1k׌A CûÉ´¬Í´„B2*qáîÝèHUúMú‡ð³cðÔ±ßÓ LìEñ=Ã’h¹2’hYß'Ñ’ó"Ø›6ÁI/ ÛïÈ‘üeÓ’3ÇOÚ®ˆs~g<ööK,{bß÷–ërû;ú¿õ÷¼‡vh¦:¼ŠÖ2t±˜•*ºžEæœL Ô’Mé2TÓíêµM£ªò·a‹ÖMëôƒ3õ¢£MÔzë07ëä&¶†b.e¨Z%(j3ÈJH²wP¹”­ÿi.ekß\JÖN½¿\Ê¢P`p–n¹é¯)ùwEÒiŒÞ·÷¦´p»HªÌW7  ¶úïÑhÛVeý ²*¯pªiSmÝ«ÌM¨K®îÒH…©¹ÆÏªÄ¥Oyú’2 w†¢ ~6UZÏÆLÝ›t§à:ƒ˜W"Ú(W‚]’ mÀ¼ÊÖAæU¶Š¼J^ºT„ Ï«4ÔÏx™•E½r$ýìê›o¾ö¾]›÷÷¦VþU×… ¹WºâÈý‡erâ¨3µ²jqÔH“›ÑŒ&4õ¸¹•õqs+¹E}Ñ“ú‡†U é']ÅëGrÊ53Kë%Ñ7³’ffeÐ0Ù8ýð¾¾êÙI]¯/Ñ?Êf©çô&ËËH«äG]cTZ¥‚iøÆëy?6Åk¯õNÂþýQױᑉ†{mMŒ·ô®be UÌþ«ÍM8‘XÅ;ÐØ,(íZýc´8õZ ô}º3blÆÛOp‘Ii¾­I”(N ;}¬…Võˆð4qΞ%y7ŠM ³û·v\¶³g'ØC=Äf’ˆ‹·kŠ\÷ÎT4åÞ{PéVÝ,ˆŒ¾@n´Ì#ãÃc¬éŽñëÍT“Á&'ÆsFy¥tµ1bºìy~âƒúnÉDq" âe%Ö÷Éü+ô§Å¿Âðx2ï°'Å9¤|IQ÷˜8¨‚‘«ì°¸FûdÖÿÇ™‡ÞÄätó¼zìœþ9±N¾ÎçÛÍp޼˜d¢ã'NÎ!›6b( yü|Ÿý ìt1[Ÿ¥£ºbÌ €¿ãO?/…²€ìК:¹¡Sÿø·–Kó†¶Îa~Ù¦„ÐÞÉŒo’ÅÍX2‹<-"£òAÕò@QG‚lÇÈ(Ë6»UYV @4¶¬DØß*ó^ä¡r»-‘ŠéºÓ>ñ™â±H°t88~jtS'›5~Ö°ÖPëù&ŽMÛ»T“c—ê)ž*zyÿå;W™VüÏ¿[”•,˜ft0œAÚÉ“‚måŒKeŒKSÛ,XØV ÕÔ,¡š½HFâ·Ù’"<«x82L<“aXɬ™†>oÎÌöYíÆÕÕ`¶^ƌ̃Í(øo10:ý`Õwb`lwÄïÄF3èérIü ìÈMõñÃ2Zml—D³²Âè}æ™ñ1á+ ¸Ui9êÑIe>ÿ–ˆ—‡œÊ æþ,–‚ær&æ&åön­¤˜­%õ~´Õ²YÖ áâa ˜JV0ò\ÂDy7É8¿µµo~k†×“™ž“ßZÀÓ?‹¢Ò?Cµ,I…®ýRÿýn¹aõ‹k_ý@ ó¯þ*S¯S|P ÛmoI³ô_éÕ?Õ_øä³Ïˆl(¶a9©|^kkü¼ÖꪊšÊšÿE^k¿ˆEgµþ¶_,cWv¿HKBÊÈd>kýwÏg¯4ÑšJqI¡#ÚB×se¿¢‡Dnf¡LM‚‡£0™µlöZÐlÃ'#9D'³¶ÆIfmj¨©*+Ážý£2G :¯gÐ!P0ÂÊØ‹NÃàûµ^//•ÛIn>g"UDÁ}nú.:Ó¦iš¨QSb´ÌI°«2É‚,nÔD5G̬ f«L]¿~ãÅç/½Zÿ\Œ­sôsoø^í8ý }üD(Y·fÙ›¿ŽýQoV½ã¦®¹ì£?Gb›”?ÄÆ6™}!¸à?¸|Õë­¡M¬'æk–È&ɘH,w^ÌK$¬Æ¦ššA]ÿÅ4Óž4+hÑ‹Ú ¥lXÀÄ*‘:Ü–ñÒA[£ÓAKŠÅZUQ\WRçÏCcù ·å<ÑwîïãDóÄž7ý÷üÿʺ§Äð¡’4‘yá³LÈlNȬ®*L©¯«jªn*®,®,/¬¼è4ŽÑÜùÝ@Dzé4øKä' ÿQfkí%á¶°Ù‹A³EÊÈÿñIZQÂ!žÛ`þ$íã‹äH–YÑ4Ö?6õàB¡Rÿ(w«¿¬´Ôâ0keúâMÊbûÙG‘P]E’dã “$Œ„-{Sbc€ ÄUŒi"KÒzô Vúïç4væès«?ðE;è ŒŠIf*b_à­iˆ¢‘s_à,mœu4³›s߸ªó‘=jž i8<,NJárÍ€ S z†X:«Wƒ¤Ï16D †Z(zQT¸’R•H¸-6xé/Ì Éíq“ëã% ƒ`‚aÝGs ÜnX#^DÍ5sk0) ¿¿úè?#¿¯º2TS\ó¿Èïë7Ø$zµÝÜ_èIÌE/ÈõLîóÈ熰'…¨2`’_k?I~ v‚™ädßÜ׿õ™I~žÞ$?ªÒƒúI½ †èý¶{ȼåã×ì?£sËæÕ7ÞxîE»ï™~MÇ…¦ßÇÉò’·÷Þb÷¤½_=ª¦fÄÇí#‡ÍÌ-=Vè&”ÌcöÕV¯ºÝˆQu$¢u |³ùÔøano+´QˆÄ§r·†5„•ÒèV1$N+ëP€ñHAL†¥ÎÔ4TÉ›Ð>om-ím¹bñÓüꙵ×Tù@Óû _mga¬–ÌÅ6YõËÚ}ûoKë Çfþg Œ,Á "•NMEµŒdCdc­t*Ü”FúÏ’ÐJGë‡O诰rÌ=ÏÒ‘_Ÿ, wõ,$@Vàã¿»þ´ï^qFÑ!úÑú3,‚[?.þE÷fj„¹•× tav†…Û©´Üê½ÌÒYlÝMTÑJMMu¥²ø1gsìC´K%΀+$&ebé{Te­]7÷Ê£ÓS¶c.ÞZ^n.ØØx§ýÞ;`0ðÀàwɾêÒ-‹>ûÛá•ÕßtÝØ1?gÙœ™••¹Ù"ðwlºjï¸ÒŒ âiLrUÖQú€Ð -,Ëò,‰™eµTeႌ'–3éï½'–bd™ÇFÊÆ%9¡¾ˆVÔß|guû ¿Û5ŽßN “Ô˜EÕäýcMOi@üÌ&ŽjEˆO‚UgÔ‘Å%K«­FK±Ö“¡,ý4À{J²ŽWˆÉ:zì [à\a°UØö“ÜT­ßAÀd²Q£p4Š7ë°×;Môêd{©ð4š‚fÝn·'&I¯ÒÚ¨ÍÄÈ•Üòûüt—$l$ñÆëÀ™hK˜ZÛÐmkÚ»2Sê5:ý½÷ð-#ß×$%ƾ’ÑÔqM¯Xù¥þä_ÙóÉN9©¬h'v=‡ÁÉ`„E= 8DÖ9!?j±ºVG\¥éóœX©ý$ŸƒêÄ1©zgŽ{ë’o¾ýüÃý³3Ïx—«„Š­Ð2¤Åç‚r˜;FÔÃ[˜ä–¤}åùñNæû‘ö룬‘2.ZÆ}Ì¢?&¦›H ×m”‰­ðé£h9‘ŒÄ—ŠWOa}ŒËCôo2¶•Žò¼¯!"¾^îïÆïZ…W˜; b„Ï…g«÷ oÙ2,ÜFNó<vÿD­Gve#¨<¿YÅSeÜÅB0ø(6Iq–Ýà!Cð8Õd@F…)›¶Ä`üÝÂÈÇ_A4hþ»¿4 ¢âp Øhx€4"á:|=tÑ‘ÍY^U`l\fë£àý1B,k?º ?Ü•?~Lø;ú`ᕯ "¿¢a¾§ÏÚS ¹ãÇ€@Ü6Šu‘JB…ˆc¨™3bâHÙAÃE°¸&€¨×–ä:„Ì`V©èIÂÅþ#*;ˆšCñá"ø¿£‘t<àÿÚ.†°›<}«4"á9á3hBìv2š/å»ð" ¦ ÚÌ Á0ƒh†öùr$Òü ¾ íøÍ§m8#¤5÷ò£¢‡Ì,¹Åê•ÕC.Y»®¢ÞŽbõ¼)×VUOúbM¬Ý…ÿ¬ªªiIÙ<,#^h®‡îiÓ?Ú³/¿µ4r 4mJûG[¼iú•"›5Œ;WLl‘,N…Å YW„Ȃʜ©k Â(gªŠ|D– ÿc´WŒäŽÒŽEwnŒâM©Îž 8’N UJÉ7¥Bozº3ἜøïÄÓ2!úñ¦ظYÜ“ ´ErJKÖ‚ŠG¼ mV¯~Zx±9 š0Fƒ)”Ð|L´)똟4¥M”Ûk€6§yIÜI€RüIî šõÒb¡L…\àÒ©$2H­"€2·Œ¼iâ±BÄùr$Õ;×ÉÍè¾yÉÆ+l€£ç¸iÓæÍ¹©8.nIEYÈþ‡ëþX´|Ï9ûö3¢·ÂÈ?^Zµâé‹ÿÑ»hI7EdþD¶ÒDb±dŸp©ciÙhºú‘¿¡~…tSU Ï?i¬ ¹ÌÛ%6¥Ð•2ëøX³X³Åë’Rfê4Tk¯‘fL}†iL’èâL'Œ[E:Ogé<ïHEeáêu€ÿþ¯…oú‡-(.¶×Ö6<» bwïk„yä°}Cè¶ CWuÕò9Ã[ Å±}4Ó,Ų=3,ƒ@•û)2 r+€J‘S j1ø³ú& ß ¯,_¸`•Xxد€ÔÆE£gQ„ØàX%añbœA¢†¯R* _~¼vd-q%B~ëÉ÷Øí¤§Í•¸z;ØMWß%Š(s°J>×Äëô!´Ñ¸Å'KOù-ºêÔ9À¡/¼Ar…¤M Ë'ŸˆšI_rŽ?Ù±U`Kddln–Ù$ä]b ßµ þ<²žØÑü¢&KZZÆ QSöÞô¯ùÔw@Ñ ‹“†Bè‚ÎNŒÃÀ(‘véˆá¨Àf1²œ ,*ßyrÊóK×\>U*ÙfBcÊÑ$…ÁôPFÈ‘£—&LõcÓ¥‡‹L×(ðHvF‹A®qH dÞ/òL8£Ï¯ø‚âµ×K<˜ çŒî¤  1)tò{yž™B{‚NÍ¢z è%Tæ4/]C+HÌ6nš¨Cþ¡kh\zõÝ_î»:þ‹aîÓ=­·¦GÛTÑqü§©›»÷=>éº)P#Ì‚ìÁ¢ìá”(»Îb Ü–ðžðw„e>îk”ŠÑäÐ$ð—9>›Áˆ£ 0…8à¹Í4¿ùÙ8ãÊî|~*xäç{b‰ÒF­Šù=º.Ðݧ[¬¸ûÒ33f{Ƽ¹4Ÿ•1˜›œÎö{çßЛxüª™¿oKJÞ2œ=Óíò¯\üíþ56OmoÍéò½3³²züm¡ìé©ÍUSS×N¾öÎâ‚)|ú&{ZʹVëG _UÝÜúÍõ× o+ZºÀ™åNnkVx#…ì)¼‘žì†/šÈ õ™¢Ñ.Jö讲çqŸ#ò¡¥¡H{|„†e«¨S³q"*‚Ž@Bÿ¨‡„éÑLT®J£›Ø‚"ž„@ ¾/õ€×i‘ l”‹rÉ4® .£dò7ˆ› 30˜¤8¯çþqrgÜÌtáEh¾¶r[cÃNß´ü¼ÍÍp›på ­/ª4¯°vãùÓÞ¾(õ¢Ž>ªLINþgt&®œ3j}4pwtiÜ‚]^_…¾)~?”’…ENaú(uÊF0òa–O9G"u‰Â¿¿ÈìÝ·ÿ+»"Ðef¾'lL—Ìíå’G^C–8ŸBs0,õ{RÑä6†YO+,1T°YQ`™>¤RùÄ)X2 TÃOÜ·"iɧ|È“MÊ€ŽKK˲½O2»ò‚4'œw†Çïz¼ªºRW¯,ž`‹[;ZÜú^ºêoMí0|m½mJcÔ웜ÙY¾F'|/Õ%Æ}ú’™®$o¢ÓÑUØ0Tþö†Æ'mµ“Z?¿òÞÑ‹L1Zš½NeˆzQøûfVË2\SEè–î=™iÞÊkvÇÄü鋿æÊ1rð52Ðë0OLA9Z(^ ²­ÄwH\<ž/8Ž´KøaDMÅ““wŠâ‰›òJ bšA”`@€ÅGÛ€è—ჷÑïhàb8ôùÂÒ†sŒ¨Z 7ƒ7¾Í^1;ÉåU­Y±M8"A{Áy¥™Ùƒ¹ƒ 6FèÛjï_|íµŠÀqýï„wÏ®uʼØ*á(!öjr¦¬”ò Ñ.ŒéøNE–‰LK¬Ïl¢£BÉ¥0h³[D= ¢7¸™ßM›+Ìë*jë]~ÅkÕUAùýÕ·Fa* 7_½nÑÎ×ÛÚ;»¦´½¾sdaÔ€{.y|*jBk%.©xÌ«È!ƈÇh# €8ÃJQ@ãz$™ÖÊ8}snóiÛÏé«+ƒù©)É~»ˆuj‘w4‡ÃU¤iVÊÄÂòœ6®…þˆœ­ÀôdcýôÌ+Béi-g±Z§V”ýnðà³ÕÙSS¶N£r ¾No×b¬q³'5Ì·;âÔQ19öà²ì”l€æ:—uF°n #Ó-.o«©ycYÉPzj΂i³ß^ðüS.ËNà ×´@ÇÔÒ]y¹¡‚ÔÔùÉæŒ˜8flÉîy%ZãxÍ€R½—1DNpÄ5º †°Ž$¬PÀÜ¥è&º¢ ì‹ Î¢œ•:Y£¬‚dJ@‹ì†è1^$ þ²Ð? Œ´S¥LŒª(j”8ë+ŒȘ‰ë™¨ø99Še”çŠ$þ^ŽÐx…™“¤¼K–'Èû 7Þ ª=X8Vt¹åh¢rŽSÀ ãq81“™‘ì‹Ð’™7@ƒŒÅ4å–Øpµ¨ðÏDÐså‰üô)f‘ ¯¹á“-ð;'%·­eø¢üøÂ„ÆÁ`¯=!Æl©Ï œ7wË}e¥ÅÂK¡¾¶ôÂ’lGIlé®’â>»}Kù}öu/_Ø:#.^ß4lŽÏ(·EZt Ù.WOcíŒìެ܎D§/TX°wjMµÉ•ÚRXŸ`°ª-A·{Asóœ®/­?©í´º…Df"¥¼á/¡Qºð"\aBõß©~j Fµ‰î«8šZk³²õ+$Ÿã|àÉ$¿ Pyi~CAƒÛEìj±"Ùσ<íÈ>=Û,dÆ\ŽV)'œ,;#c‹ñ_rÎ8NÀ[bï,½ñ°Ã´©ôO×¾4ÜÜV<9>Þ\VYùÄ…*³¿~Q ÚdÒG›Œ•¶qŒ³Ãß[˜à6gzê=« Ë–Ù8÷’dëÔ윾‚ž?Ås(!.£¥z¡Ír¹Ê-û8­‘ŠæL˜¨-w€*‹Ä8UU$¼")ŠFnTBÞÉ]â©ù’Æ/k€ð*¢‰Q!ò³q,h­_¢ýâáÊÉ6Xi8ƒU¯UÙÓ¼ãñçÄ2èa„ W•rèì¼³ ššWpÇ®“wÿ­­íowŸll7™5Z³©­ùdãT“Icîšw>?³sòëû.|­uÊTH¼Á·÷Ý'||ꈟÜù&eu Lº0hu|iÒ%ÅÆyt¦ÏÌ:w\¬[§Sy4~°Ê²&:y×|Û‚¬'ÿ@\»tÏaÎq¡ aTI¾— ÜŒ’Á,i§µ‘ $¦˜¤ÅmÃß°Ò79ò7XþfìO¼fÌnâB*`@E­ît*j+i£üQº «ª¬ÐĆŠ"¢…ËMA*Ì`ÒXZŒ]gìÊVÚªgX†gÇuB§ëBÆ"È‘›l¡•jc|I•UrS ÈN”vƒ„šÝ²¯‚ŒÕT¼ì!í™wdÉÒÅ‹<ÝkÕà ðú³sƒsr›âm‘*UAÁ5sg^q¬wåÁÇnÙJ¶o½ïºëfÍÎIË.sÖ§^7{ÔOöd±Lséa˜¢™ñØÍ ÐIˆJÂ`äVZ †¡S=]+€b܄ϥaƒ†E„$izø) 8ÂÀÛE@:ûlá |œÑ- i)Iy3ªxí¹âÖ2bÉá'©§« ¾ŸZÅœã­bc*çÔpC²müĆ"Z¯{¦U²"ur~,ĪE¬n#‘Ÿœž‡g…GÀ‹/]QÕ4}uúI¼È7²{hŒz>›ûïhöͳûÆ #ò¡=c‡€§Ç‚“AžÇâ¹ù&ª Ex–áKFînVœ 9,7Ĥa,'4¦Ò™Ù”`5ùÌ>©žñ4àø«€QXös°øã±ã§ƒÄ»bxÓOìoΟ·¿™Å×ÝàÅÕ£Áç#Ÿñ&²Ä£óŽÃ”&Ôs ¦tŒOV¨(îSÎs’Ò@Z¯Ó5¢Ù£DÔHe›0j4„QãDÄ82C†æÑ ¼GÓ$dÉ “QÚA†É#30ð¢`½Y j†eÔìf°5fúÇ»ŸɸU ýLóð<Â]ÈŒ©)Y)©F¯×k0{ô:UL9–% r@¥´C¢¢“{Ê`É3„\Ñz'A¸MK÷6L:PZÒX;)jJ0¸¥j÷fîÜ?§®{/9Ôç7&ö&›â]>²åÓ2‹jkþsËÞ-wäR.ßÙNUÁŽÜ@Åž-'ŽbóÆ+Éæ²ä?žrì÷ò´šN2õ(oÊsžÎ”gO]Þ’íÉr];UØjN1ÐÉ$k’@Ù/Yê$6çœsiy~°´ôÁÏ€gpÐb-X¼°{ÕÌgVN7ÅåÚçNéxyhå?['Í”òNÜѺ¯jwsrú—_퇮n=;£ú=ÙÜT17l±c€‘dЦû¢€–›•qU§ Äýý)ð$â†Á‰.hº˜ó”¼Ùâ†+;÷˜Á$v¹Üà}˜C$ÔF†º• 5€¶ÉHˆ2IN1%ºMüÀˆ$ä’&ÀóÐË€< ÉG&q $S¤6 #"Óq¶IÚXt*âΘcˆÍ0ëÕ¥,»[Œ¦Ø‰<e±3ÊäùeÛrÛg)æ¬ò˜|›»ÌA« ³Z…WDìîŠd0™rsR ‚‰OŽ´«£ŒÓoÍšFPL$Ãõxú•Åev+‚­ÌNçAÐB]š6ÿµj×î+žþß«s?‚)Ý@meÕ_·–v%Ølímq©ƒ+ƒ¶v{Wë5 9úŠóaú¾ó®<>õ¦…ëNjíú2?;ëìúÍæëÿxÓõ32r ›É§)À¨ØßÄèùÍ åS肌¿%q2pN°Áñ‰-§$N!o„–QòvâZJÛðö¾C@ËRJ¤ÎLg‚~º€‰øg!ÿ–_AÇè ²IAò°,T…JŒ€Þ .q%2¤Ê…Æ¥”N MÝŠEN%º¦rhLåïlfy…aðoEÕñáVXó È9Ù€²«rªR’ã,DxÕ ,È’lÕŠÙQÂCA—l$’ò–Zuå"¤Q0òóÂö[øjóåÕ33Ïn[¿Èh€«(&:?M£ÂÎz?›è™µÝçI^‹.p{g–æV˜L1–¶ŽÖÖK¶}Ý#Ã÷ß?çÎÉxW»§ÞÙÖiË5§Á’mùÁ»¶¡ö`÷è÷7gÔY­ _æx}³2ºjö®OêY—ß᪠󊔜žÂ+:y(¼"ÀDr*7Ï*ü„œÆ¸%…èiÅŸ²‰Âì \âÈJOÏ'pPÇ}†|¨Ý.mn¬XƘµààÁC3 6ÙNw#fŒåsb0Ãb:lÑ,(ó…jÑVϱ‘)ghG%)¹qØ}$ÙÈ_”\äNŠ‹:Ñôs2m’Œúù²Iß¶ÓÐX: $(•šhÎg7_Qëó<ÓÞC¶Ö8KZŒ>A—Õ˜ä.±žóÜjÂz^‚gÌJJr.¬r»C^ƒÝ¾õßÛ¸ìøohõßGÇp,¶ÆÅW%Û®ü÷úÑC7÷,ür÷–kÝ^wP=ðüÌ]«õj†b²® çË„ÖI¼w ˆ‚7ùËá1Þ;Q±®(Ù|ÄSîjæÆÎ›´às[ÐÕ Ÿ# ¹"­JJw%bÏSÙ-qÚï¡¡›Î–ÙlüÊhE lðøÏP.@%LgþAݵå‚"¦S£Û䤄Q®-e0ÕÇóS’Á{øáqZšÍ—³ ôÄbe!ŒÒB0òB0c ˆD*z° :Cœº K=`'´8ýBè4“6ìË΂┘lgCõ/¨Â[A0<~=à<Ô&ÊŠa¯7ŒÇoEY Çé æxÇ`÷4öFŠoÃ<¶îT™yªÁQ6Áe•óƒ6“­ÚjLKOˆöE¹*MZ]sMÕ‹gÞQ{NaaÁï–¦UZ¬ð¸>gV\c²¯ÑfOŽ7‘`ÿ숡«dvÀÓ•œßž`ª æ59ôîǤkaÒ´‹ ÊEÃOͼº­åÚ¾ä:‡+;Úvÿgp÷‚ìÞ|[’Ãl ¥:+lž(9I~Z1!v9ÔMyušI€ÁЧšQ…<;NÞez$+$õÈW.JK8¡%ÌÆ4Cœ7,ççfÍ/¨tvVƒ;²ÀÝ<¼þê–DknàÑÑŸ1&Æ[—ÝÝ.¥ÔkpÍ#Ë'í­XßsüO³—7„ÊûüÉW/Ý “š4Ÿ1hDjô2©Ñ&5± ©‘d?–S«a`³ò‘1ã|j4FVb FTÌOàm©âö}á¯c¼í÷ö1¢²z)enO\{ó¬~h.ÄaJ (jeL ]Sê0§#jT)›s¶š²9œÂXŸ/L9ë0EûÁMðB„¶ˆZƒóeiº˜ùQiMùÀa&G*æË¼ë†ðg¥š£¢¾\´ý•eØ÷˜‚ìU£ïìxõ_2ð§XÛ*DmÕï#oEéèþ– 6«ÆMR&üT¤Áj¬QS5 ê!芨äTv§ê ¥Jß«Ù?×%”uæÖ§XÆÖ‰ð° 6¿×–žNM¹IU3ÊY{-å5è¥bŽRöPÈ *þ²ûÅŸ††@eï~nþ%¹‹— =4ï‚"|ùè€ð:|ÝS ©˜{ç— ³ïÝòX{Õ‡Μ+¼q¼o¥è™gußpóÝÊÁP_Y+Á #¢b‚BÇij¨þ_[†A©dCo&íD~”GÓFä%ë8–cdôR"žxõöУ¨kÔe¢^¾jdá¨<(¹ª(öÒòb )º×8ž‚ÿ»~ãäûåwuZÏ…¹ ÿõü ·>6uêc·~ÙRßÐüe‘)MF‹eR)£« üµ –D«ð–ÜŒYW|H¨Ónûø†›„a,³´4eäMIfÆÚêÊ…^w&e’å¾A$”×üŒìhca¢:J—(¨º)£M8sá&§ÈK¶qB;¦í6ž¾ÝŒ_%aRzL©Õ`˜9ú‰È „5]+ -Çú,gÏG.Ê”ªxÉO”(&f¡ZÊ–ÆY¢"hŒ «¡þÃb©U å-•”«²!ÔCÉOy,Ï„ѤÀÿL1ë–.^ÕÞ7_ ÀW¦ÃKG„Ÿ½T9½(ÉY•}öy;7÷­s¾zÝ´…zh}íþ·Fi.kZô‘×£8”Œf6²¢¨'EjZÇôpu*ÎbµbÁ YtÁÌöS®5£ZM§#>ÙšAuš<„“‰ñŠú)Ñ·Š*¯ÌôX ¯|*Ö/0ÃÂy©E– ½£¯û¡Ã atñ@ÿÀ¢r{b¢½[àÖþ_Š‘3n:oŽÞ½sÞÊzáä¿F…o^¾î–kîïIßÔÙ¹.y2Âh²0D TäE94D¬ vQ¶ºP²ØÍJέÑD 9íÖ£¿/ÇŸc·é£X†íªÈ$ÀiÐ{Ç{ÒA[Tôüè]Nƒäv¬—fäã¥s>ú!vÄú£ìñ‰örgmšÑ®¹Ù {à÷Ylýhóæ¾Ë.ëÛ$ Áëð/ƒ&!.§Ñï%žx±Ú¤ø¬­Ù©AHiX™]ýò5‚÷•«Vœ—™yÞŠU;F6a}€u¢4 µ†šmÀ‚>#Fbšb8Q³ËJ‘ò5b‰8ßZ±˜k=×RW‹P{[í´ºiä65É>w–E«‰OEø!ü»¿¥ÓÓp:ÉÇÚÃÝ:ýÒèsKu• §zÀ1”½WŠ6 Wcñ}I Ž&—·É^ÉߨñäÚ«Ú­V, MJk¾ê‰ù ¹gþ4wS[BŒ°³::AW–X_Wœi-p8þZéqÛ mÐÍuEf ƒ– “Ù¡VƒÖ\½Ê*PUïoï¸k2|çôÖ,ýb«pòu€Ð¢²Œ™ÉpÐnŠLÊÎï¿(¿ Ö™`ˆ°V§g„ð¢Ê²?wÄ'Ø<±–sºDf}ˆfóPªY› âÙSh/# ¸XC4•ÑY,”ª;£Ó[TTx,ÇT3§Çk…ï„oš»ïŠ6q»ehé@ÿ¢îÛËAÏ¢ÛŸ}ööEÂÐÑû»×·÷&¤šÌi†• gÍÇ[†û»ú§Î¬ê ­¸qžÓ;pdžÑÝ‚ˆIÄ¡¦¢RZE 53l¡cFC™E¥æB¿Rs¡&œÐª^rzôº­q„bE¡R(•`Ûç§gÏ(éGóÃnwáIC®ðj6ëñá'!Vç²|«;kéµó§WT¤ÖÄÇ%Ùš f.w<“¬Oúæ¿#?L]ü¸0ôÒKù•‰!ëTßëÚÎ鳦w¾`޳ÔæYm“ÚK‚ó¢sì%í×?ðü_]\Z{¬YXsóQÈŒQZvXO­ÖUeIAåJéYÐoT^«•SïÇþ±¦ãÐp*ê½Mõ/Î,WIÉ™hIŒÐÒPXˆ¿°Tï:ˆÅʶâIG«Pt©¦­ÂŠˆhlädÇœ)sÃEÿø[ËW ? ûÏ.ª/-ùxñr{bé’E¥¥¡RÐ{-H½p,¸ý†ÝÓS?xü$~`¾¾«±½~ú#ï^u ycF×´é—˜ÒF«hÁ¡Û§×®Iõgx3Ó7,±­¯i8¯A>w+Ø8”…*B¥)ʑÿxähâ»Í£§VUoˆGNQé…ONù2YQQwâ—ÂwkzúM$£Þ´¢/¿°qúÎׄVÅ/Ì^²û…0ôÊ£ÁÖ¬IîQñÑÞ”õ«2³çe-[óúö@£5ëmahÎYÿm›ŠÿöqH¤Œ:LëÔa°‡M~T;ÍnTU˜‚®ž¥;R†Ê²©^—25ôSƒ='.'+©~du˜JV¢)ܤxd˜£À§n6z"›öx&¬øèfE¤yL¬0ù §åx†8zíƒË–ÏîMª?¸XºÕÃrC7Ê£6BÎÕß~õ図zr¢mMœÎž&§Ál‰š™šÖ\Þ<Ï:ÿ’-GΙa-غ¯Yæx†ùh”€ÒЇ"€žH-YîÆbÒ[Æ Ò5ãjŸ×ŠŒ¦•z)–¾_Ñ¢©°i0\`]î8¡yÈ;Ö’E”SíWzœÒŽzˆÅ;ìTh·§9ÒâbMbšZðGBZÈ0¦¸ÌÏ“ÃT(«b0ao®AÑZŠœÊ*O\Q]õ販ƒ™uö ÏÂ{Øøƒðë¥ö޼‚v{ÙÐîì‡ËJó}ÅoÝŸW0o” kG§;‰^†¬ëÜv»»îÀ—!,hî[dDv´”¬1Ç(˜Ú¥8êÔQ^[æ½T§¢êD%âô-¨vÉl õØÍvZî‘c«Ôâ” ì"â!+82P†LÊ ÇóGOìÆºÑÿ$'—Îïü øÑ¯ùÏ¢µð-¾–½¨:^N*= ÿýüèU7@åÁ…#Æðn€êð´¢"I‘ 8Ñž@ñM7k1=˜¼ñŸZcM Pö<šVèT|ëm¶½ZŒn†šõ}¼YF]ÿèoxêѺ™~ÿäÚÇžÖ›ŒUùÂgK‹çTDÇbü0Æ“ó"L¦†ÂõŸ O!‡Æíñ&~Ó]¼ª$¸¶lÕ³=ÿ»;Ö.,…Kì&˜ÖóUö‚üàP!™ŒjPZ|(–î˄мEdíë$?ý±}qÑ$ôÛ'6ßF”˜\bôž!ZKžª°CÐC@æ. oAñ‡JnÖ`RIâ̓«ÖxÝ_[b6ôß´tþîεۄ[Áû† nêN¾ûiLº5!謘ظt{Ï{º;¯¬*ºô“Gšðî_Ú«îØûÞ2G·œmCùhy(2'SH äF0Lƒtæ”K&sb1†aÄ!j°ï›R½4m¥ K§}š6â´‰ö%™¨(8Ó E Á3.“bôPlnž°*•y\Ä‚;?WÂŒù^Â%.|‹—ô&Õ’@\{v®JËq6‚ÕÅWb"‹'×}ô»ÝW‡þ|«†à–þ!«-8À4²¢_)¥G˜á­5É)ó’;w¥ûÓ?µÒZº_pâ a¦Ð|"ª¤DÌ(tdF ¿Õ•¨²¨ 7'3Í™h!ü,=pLLa;XŽE2&¸Ó†pŠó"¦‘|pŒ2G!Bz¬åX˔Ğֲ[OÜã½ô¿=ƒžä¥U°Bc0T o”ÕÅ48."Ñ2™ÿ‘:[m¬7ÖÐ4½åC5ÓÖ‚gÓ܃›ƒ¯'3ì·{œBö®ÒÕ%¥+‹Š–ä-ÌKfn¼3.E†K½‘Ú`ƒUhºøÒ™ÝXN¬ÿ^T{8΂1(ÈÉ€d g 7ˆE™ÃßÑÝŽ¤QIr¡(¡S(rӘȮĞ!<0ú=øÏZ4¸²¸ÜQf1Dž½»~š?àhÒΚ)8’y{ÄyÍWíííJ6¦úlÖŽk&ovõfšô^>ë¶ÝŸ ?"ŒbˆÌ÷0WbP"EF¶8šU’Ša`*"\šŒ4‚1Ñ”¨Õˆ† ”2Q2F™y2l£ÛÈS9(¼$¼PS»õëèG½µªJ8&¼ùóWEîa%̸ùË·¦BpŸûöé_ý^¸A¸è%Ì?1’#ÄÀg¹ß<3Jc±,d Ãl5¢i¡ŽJÀ|6pX«ÁÀ0M9 jËtÀ#ªËî§&WU¯T*™sÒ(œSmuEYaAfz²Ïå´Æ¢Q#4jéLÆ«-ãä®0¨rûefC1Š[!ùЄ±1ƒñy?ž´X,‰—Çkv,XУuéZNw–UëË´'GDEÆé—óî$a¨ãžK“[ë “[>|à_a>¸¡(„oÒ½Y[ûÆýÍóRR+í¹¡ºÊöoÅ·YbLA;¬Hͨ>k¯¹Øeä<磡ÈtÀŒ,¹cùXÛËÖ5‡U=`LÓc£Wvû鯊¦ÀÜ¿×bŽŠ h]/³³äêá'_b^Ê6ú+cssãB¾†Â„xYƒHyú(d%X‡±€ˆÕDoäÓ`5*¯Ÿþ2Åjúh@ķɪ·ŠÙN¢ J%“pÆe5†È;ÞWc™ð­ xŠ—µ~œ@”Sk×-^ºj-ø!‚¹ô™º4Òâ²l_½|åu7\{ݵ×_5fK<ŽR©-- '¥ÑM´ù˔ǩû©IÑlR´ þ ×À1.†3x-”E”·P´4¡Šªâc^[¼i^ó`®‚&Йh-“3Æ:Ã3g!±<½öôÂk§Ÿ›Õ“ñÅ’ÞM¾{Èf!¤¿oF¶œ¯KŒ2•Ä6Ü·éé£Ƙ„:ê?T _ r*Ðe(CÍÒ L†Vd©kVÊ—Ëü²z “9¨ÿíÁ³ù×t!­ +¼}b—Ó·ž!:f’ü¢4?F/jfê NÒ€‡sÉHÈÊ•ï¢ï¥7á¬B™Œ’³ÀÁÈT\ÑF~ÞUËçnÔh5’v 〈ÉfáCˆU5¦·§%ß7wfÉ»©æšÓSQPQV½³³D8 wîÚµª¯¥ÛR¥~-.?×ô«Ò»â\‘j ŸP±r¶ÕmÔóllÞÜÉU=.×ܲÕu¥Ko{”–ã†ÄõïD/…¢ê@¥nŽo§•õ›$ëCaù@-jPXž[8¶LMõ«RÏ&ÈJ\+E5 lFÎXW5Yhµªïô·wéŠ4dKÔ°ýŒ½ÂĪä€ÊJ ò9i©É_Ùâ£"Q'tê4ã]­Eö‰BsØ|^0¦ö½“߈i€jæ2ü۬Ǘþó륉qZm„Æcƒ˜¸D­V¥Ž[½fÑÒøäŒH±ôžLr{’xÀíS•WÝ ÇJ»3ÿZVUSÒXP^ÙX³GZóõƒËß¿ |ygÅyÓ[77O]Ý´ê³þçN^4ùÅ¬Ü «ù¹¬²òø—S‹ "2>NÈtG–ßRµÀžØWÞ²Üa_^Û4×Ùögáa‘Ì|.Z×Ì’»OèÍFƒê1 í3¬1-i±>=KŒiÁ\1è‹æ'4xü<d d„tî%åÛÖXàÎ>wò.áÐåw o«K¶Þ´xñªØÔžw¯ÿD _þ÷÷Ç{29áÕ)¯è‚£î[v\sÓ5W^p³ˆžúÄÁy¨bÜ€“\ÎDnRã¢.xXt›Ü!§ó¦éÿÅZÿ5 UŒ+êâ3¶ãrx­FÍ#x(¶å$£~Îì2xõ*ªbŠÂDäTëÎÚpÉÖ;îX± „+…W™÷øaù}o¾qßò4HÍZýðªÅ‰ë‡á±þ`~.u Uÿ8ë‡Äµ·õ'¹ûo_±cC¿‚€y=*AO…¢rA­ŠN] Ÿ ªÈPÔjıD¡&[4(jâ{‘(RÖG*5¯êŸØq|sjУµFh´$'EgjžeP ”he²àUtNÊá•RüïËLz8•\žÃÍDzÆ÷´0óš%Ù]ŽòPùà¿öT'¦pœÎ“fóýoû61ŸÿÕWœµlÀU;Î9g‡0ôŸ`óÆxkþ¢üîܬãêNÿòfš²’e.8òÔx~ÁìÎÉË…ëïü°:õ’•ùÂLÄ A„؃|4²¢$” êæ;cè²ñ:PiAµPa¡ZDŸ2vv¤S9, ´ZÔqŠ…5€t*Ðu’¾è·t¥û”C¯‘V@(ä™î2¡#õñDj–Q÷Ÿ¹ï)=(ëì÷¸m6±öPª'Õ–d£•(n2J™®¥’qÑ-¢^–ùÔX”ià¼øÒ5ÎJo”‹<à)BÂ×_ÿã_ ïõÉòOøô±ÇâÿÇC°øð‘–Gï¥w¾ù•–ow W³ÌÍ_qíȳ×_|ðJQ‘¼LTzPzLN b`]RåÏ`Å¥Ò£‘+Ð¥Ôª1Õ„ò@}ÄB¢pR/º;)W<¾­¤3¥ µ V‹ÀjŸP*Õ5ñ˜éßíô©·¥Ï›‘êÍñåÝn7‰'Òë4 Wø´Ê¼OT8Ì’R63é]I~3Ó ˆÊG3 ‡Ò®å,ÄæY£õg ½Ð}m{òúVèn ¥h…)\u“ÝdætÆïŒÃØílþõûW88÷]¸jzfª°àCafŒ^Äý%e°{c©_U@ÓE ƒì²´9Ô8=-Öl䯯‘sÊ( ÏcÓG¡Èᤳë'Ž`ÄP&q–ÔsÐ?æE…c&%(±s cý5nZ“5m]yfnbC>ÿïè§îê+Üž™¼±h³1ùß5uÂWÂà±ÿìøÓÒòwVEiNµ%ÇWäj¸ý–CÏùCk¦ÎÚ¸Ý=ÕuiÛµ ÿ~\d,Ùål-ò£«BQ1€Á!F$´è¤OXn˜î'å… S£Þ‘ü&Ì ZU%3Ê™’6”ŸØp|ºï¢wŒ+AâÈiñ!YË>Áªæ•x^=FEô–ïâY< ߃oí앬ÎÄE-`Ÿ²ØéŠ iyÆŠÀ‡A|áYV=úXCíòŃy¾ìy›*ªvV éFã¶¾o?zaÔFèÅV6„’P&šBDÌ(Ž!ÑÏa<$2í¬2¿Xå Þ>î­.(59‘°<( ’¸°µŠº.‰î€¦0»#eUe ÆÉþö×Ö®?\+~ÿ½¿®¾÷¯{ûrвª áùç¶-[·þ¬³V C7^¿`o~þßl»o™×;xxËeït— Õ„²³–-»âc2nX±~Ãðš¢}ËÉ2¶ƒMC~ʇøã,,u­ÖjCSbY„D¤–bó2ÄÏŠS”sTõ/{²ã§Ã¤£”ãÜðfƒ!hÏló¿Ò´´¯ÄžˆÕðÌuŸåß74Àú’{§ßPýXQ•É_SµqÈ=sSÅ9­ðˆO8tŸðø¼ ÕüeÃÊË;¦uZO¡ÕBðÛLo@Fä@KÆÜFD¡OªCësñ€X4Ì‹a²ŠÇH¸Bì´1·µ…½FfÍu©â¯H¢ª€ËìM2›ƒ$pÍDc¨¥yRõìÌþÕ/>ðOž²´ ¸¬Ú—1Õ›Žß¾ä2¨~|-.YšÛ±óÒ«ftÞX³pa ­¸FÜÙ»¸/Q$-I‚jCUˆŒ‹aÃA; ï7ܬÅ—`9jq8¢£9Ž@J2±:٣튄HŽðMc‰vh4„wB~¸`8oI 쇾ß@þýübú³È¿þ>ñ·!†gÕ,e‹ŽåYÃGÚ}¶ÃÎGÈ?aÃÈ?nÚz0lÙ´i‹ð…ðå–M_4føŒ 3WWç¥.´æå¬E€Š‚úQ¤Eî“òƒHB‰‹š1 Äô †  MzV'&8Tù¥’MBÓÛÀØ¡id°ÌuÿÝɉ#Œ±µ)¿(ï²Eím3ÚÓ/8«©Öc‡¾‰ì{ z ­Ä– ,ç†Į́°v¤¸ .T\‡Ã®‚Ë¥ˆê$'õÎÔQÆO%n¼ˆ c)N$]:Í× §Z¢³ §ÀͶ”’¤å‹ó{‹†2«ykë\ó«®¹f°¼´´x¥¿;;£/oÑüù³¾Ø¶ÃÒ²çÚŽáS;umFˆÉ¶DFXí… —„ì.`ÓÓ&OÍÌÔê´‰& .úþþC¡¥Ö„ùón~ù=„Å‚a«EPˆ¤ÙC"°èZÁs3€Xz‚bÙ¶E§ÓEê"iV3½JOÓÍ€œÉÌ`|xôN\ù7Þ(¼'œUà`V,Žà Ü:z—œä«Ž<*VóuæâUX\Þp5ß~%wÀ¢ ËëM¢¥HL1dyÓ!ý”åUjBä-(ÕS2 #lN›—“»0'crfÇÌßµM¹úBw[rJ§›I}\›n‰É5/¾¿º²AxÅ®V«ã£A› 3W׬ßrŽF«u˜*BØ?a#Y†©¯©ºå'ÂW£í'³ó¸Ï‘i"ÌhÀ ÇÇ0M RCa –›‘×(3Ýôi ~9ˆàV+³Âbð8á—¤Òn'C'£ÀÅraRREìØþH¨ª¤nÊHi«kjii®«ÑåÔ;§ÿ±t¨¬|ã‡-¶@ 0/·>.`‹³6'Îè ÎûŸ„Œþ¿FG;„o2,±Â´GTÌæ-Ÿ­ªö¼Ï]ÞÚíÛëê<®úmµ \tŽ\6„§YQY@âùž- Ƹ…HŽŒNÎE$ ðèEÌ/¼È¾ì ?Ž m%p0™œ6ÊFUhíŸ9Tª²jE×ð >átÝÔã×-7ÇD {9U¹Ui)IÙîl£ÝPLòõ%õ¹RRÒú²×$­q¯OYŸº>mMö®¤]îóRÎK=/mWvLJjöŸ¹5ÜXÒƒ øF9„ÊTñLøœR-«Qþ8æ/‹6ÜUºÅã®Û²¢¸dýúçŸ?k}qñ² õu$@lz 77wFW_ е°´¬,arþÂ’²2qÉ,\ó|Ëꜹ>GçY÷,%ÿîÝÐÞ‘ì›7wßókV37æåwÎÈÍí›Ö™Ÿ7ÚTQ¶d9Çåe‹…’FËN¾Â¶rï / Â|jUÁ,Ô*Š@‘)¥bP¿èŠ9ñ0QN0â´Í†'63üº»™ÝÝ,¿în ¿înö_s·_¼Ñ é_È"ºÝgú‚þ -^,§é¯rE.›ƒ²¸.Ó'$H³‡Uò¼ð]r½·µqRsm}ˆ´äLÜûÄ?‡Ï:ë“Ý–Öµ6´¾œȵYsõq$¬_¥6Ø ’œÁ¼`àšÁmgoùøûÝçõN_ ¼y×ëÂ[³ô0uõ[k:k½Î:z, å'«Ø9bõ?cHÁ„Ð"Ùˆ e¥i™íoŒ|þwô‡A„Q|îå7 -Š¢êm‰îGƒpS0õôÛ1Py¨SaÓ:Æ ªü`€†Î©ÀBü^óÑÿŒ®S³nï‰7˜¿ãÄ´KSF[/zTk…IÏ,áë±§å¡®?ó jBÍág:h]»ól4¨x®Iy:b8DN#–§›¼JÅw"‚ÔgSíò$ƒÝŸE¤Ö%¤ý™G5ë‡w"¿à¶*Š¡i N8œc­ËøÝ•&KZSùÙ“S\ÅS¶gfeÞt$!†‰ˆ…{Gû&Îmtwl±oç-Y†T½Ãé½Qkät‰NgGrã?.‰b4†‹‡LÂ;ìüH%f~NE­hgh[2 \еº*Ðh›@­É•:Ĉž€0J£€Ådâ‘€#é0êG:¤Õè´ýHƒˆ!PÝ(‰›&*®¢iß¶ô4›U£TW*/)*,ÈÍNkMo%ÜˆÏæ3D«ã5ñƒT Òë¤h8yyèMÆeÁ¤qB@×2ÉŸ¯È·Š>W\TÚFV´ÃBáO0¦ wÔ“%H¬÷gG»\ÑÙ~O|\ŽÛÈnÙ òêЂÙs§-[ÖÜq]öÞ† ¶ßxÿ?—¬gRà.ø³Ð&´^P×pùåu>gÝÁËœ¾·L&{bJŠÃž2ú²¸ê#_¤L°U•ÕìÏÉê1Çp¯ZpÀ58÷Öèr˰–‹‡úÃpæÂj,t&Y¬àx°œj¸i(¸)k+'ÛÀ²Ü4±ž“–4gÛôѩɞ$êe@ÌP¹ú\ ƒº‰‡â×­Yþ~aiÂ`÷Ë  —áÍŠ¨ - -:}ŒfpÐAÖ €¬A9Yƒ$àx°œš®€ú•B®¿¸ ÎÄâ¼ܬŒôTŸ'±ÊYeˆõ¤ˆ«PŽs1™ª¢¡‡Íò+EŸ›oÓ]wÅäŽ%¿kÞSûÇC¹s*=z‰¿´FO¤¥ÇjvÞ–TS{c¬eGWדTþòz±§ §x²H½"zêƒÍÅð#Á uvehA5«Þ1†£´̳,ßIÿòlrÙV«Õš`M ,²Y‚ûiñ”+è7á ̈xGX kŸ¼ÿÇŸ`ž»®‡Ï´÷n¨†&áðØ<.@¨Õ¢tŽ8IÀr®x̰Y€ÊrA}Y1£¦ÈFEAĤåµ;(¦e(¦UB¡ BŒùïÔa5Ï«;é_5ߣ^Í·ÖÕªk¨k¨­©®ª U”—–¤øHœš‰’LjÖŽÒ™Äù‚H6ó²©ì|pnV²ó(÷“µä&_SsÅä¶úÜʸ¬y]Ë–5¥¥b“[ÊöT;ë¼îgËŽìî.+)ž3WÐÿdSxNk:HüùôzgQiÍþøø £7ŠÄ:PGÀ¶%–,¼Ø]ìYWzø´‹Ì‰Øe³ ,n”…ÊÐ@¨/ *Q â&¬áÙŸ12%U¿x¤QóšþP³¬zù£”šmËÉözôÑEÁ천²´O–7ËíÖ»)ðDžÁPÞƒ¼Æ~|ÉëpL#ý4²Þ¼îŽ{¯¹éÂ}7]ÙÙ{ûu¥µµ¥äURÓa‰I®©ë˜úSŒ³}ùï³3w÷¯[ßw^vìâi+·bmqNNaQvN±°¡®$13:©¶¸¸qÈ.®ÑÈü¨”bÚ:7XŽ:D'‚XD{1Fà*˜ujUÓaOñz‰5ÒiÕºþhÐò¼vù£å{#×òmMåe)É€Ú'7v5uÕT•5”7r’KSJmbåæHŠ£R%{¿q¿}y¡þLˆitè×­xÊ™pÔÿyÜ2”Úh ¡V´#´Õ˜‹¢.¡vºô‰dé³¼déÃ…å%¹û7.}UeA¾Ë ¨©¡²µªµ´8?T"Žqy®<‹™Å¥·í'KŸ¤ó€($âÿ²äi›v6•/ZÖÛ7ŒÓ„õqA÷¥GžÚ~åz{.1Ç7×ÔMJƒ.:¨R_}ö¶ËûR[奿‘–,v*B‹B Y vvŒ´À³V=VSïŒh"þÍ2¡¥6wžcù~-pH­âÔ3¨F5üQ1½`TL[FºË¡Ë¤eù½Î4WZ¬I—‘HÖ5B«‰ÞÜÿ\Ox—pó³¿j©¸/Z=šû[@;‡yDuÃOT‹›ÃªÅ-²j1". Âè ŠddÅ~¥ !Cnw»ênä…{BÑÑ€ J, Œèó¥¡ãfEÁH`eáðÅm.Fþ\ϨŸë©ÿ¹ž1?×Óôs=c®gÜÏõ´þ\OÛÏõtü\ÏÄ3÷iüc©ªzü£Îô~šøì?K¿O;ŽðÌᾓ÷2fî]Ä!?šG5ê‡)žCÐ$Xq=é–@—FÛ,§ kD +‚îX ±²*>>–÷«ü1Ž€CH»#v‡eGÜŽxVVžƒÁe`ÜF Ë2|Ð_ãTèr\¼ öšÝÕ¾ Kn®ŽguØÀ[õÑM99Ék#¬C› Gÿ:úz‚%·cJ®ŽcÕ75ʹM0T“4W$ü,Z`;Í×ÃñÅ$%!”äOò{=äf® ž#°‡‚@(nñKŠÊ‚éJq¿!Éà°FhùH–ËÉiŠÖ[yŽÐ±¼.7×R1T#¼Óʽ˩I ¶(%6ÁÜ‘ “Q¯f9]\KÂèë£ áo8„°X¿òîˆ(ÝÚ|g*Í¢Ö0b°¥ÐB—±¨9Jô¡š­â0Ã,iŽn@GÙ tši’~KÐø£ÓôUZ‡|<Ðø€36—R÷GBBBbB¢Ãn³Æ+®gò?½Ag§I]â+ÀèËì&/9rnÆ ä× ÏÛôÄô¼ÙûÏž·ç>ò]Ó3‚ÐóFÏ3=oದgš`õ1ðƒ5ÂùôuLx혰V »q ­é€î>ù2 uH¯`Û^¬ØZµj¤=#Z?&˜Æði¼D<O×ÖžGÞNÈC½wòY6‘W¡zÔ‹†t©€ %cDýS³iAE"RøÇÅÔ²ÌôÒÔË)cikÀÒHfþXC1®·±ar[Coco}]v¦Ó!’Sñ¨IÇíÔz³ôåÎZ•æ6–ÕÜLQ™\šò”‚ <¼•h(̘©³ë,­Š <«²èãMœËàHJÈJŒriýs²ãTsæõ,ß4©¸Èn÷J ¦ÑÜMÑ\ÌëË«{²âJzkP zmŒ*¢×wÔx›ÓgèQ z½52Q§f-j}B”I#ž€²ŽÎK2çŠí ñÖœœšº¶öZgK~—Ù¯NJ»Pàz›³,± °¬ZkÐZ OSb‘ìÁˆuÕ‰*Ñ ²¡ý!]V¦+*ñ–ÌV°¤´ãÿŒ%å†KÆwϬ®š7wæòîåMU3ªgT†r³YZÊ-0 KûȉQýf—÷ç/ÃÖßu†ïw˜¢lvS²£±ª´ùìÛ.KK¶:cT¬„oñ¢3_’N·ÉÒ—=ýRo¾L»µ8”a/íð×µ§ÆÏ„üåG.Ÿ²·>»¬(=¡–ÏÚpðç/ e§ÛžÓnFwž|ùœ7! ªE‹CÚ `™H¡^êGÅAø¸PìDeæð ODŒXmóøfá4~&>ŽÚÌÓÓâjãkÕ*d õ 8Óš3ò÷Azbè»<êì>14¼…^6Hœö•§Y½³ÿö~’¡,wz•ѺDCÐ?3Ù¥µêêΞÈ™“Ÿ;‡}òt«q"ñȵ“+“²»B©þ*ëLÚ Öø|3’“cÒÓvyŒ¹ᛄ”è¨h„ÑÌmÌ¢˜Bf2BÓ)*•Ý—\@^:IºÐßÜÑÑ£8m”ÖU"wÀ(‚N˜ Rl¨´ïJ„ µ3¥l2£B45¤Mƒ€ë%9Pàt¤XÒMÉñ ºÈù³ŒÂ÷8,öF}\~~|mFF}|œýĪœ©žÞ ޵dLmÏ´ÑŸ’¨¼÷œ­õÜo5:Óðþ³Ø'«Hs§h›0#ºÈ²×‚IR?j4°M@k:ñé‘¿Qª-ØÙN~.nmªØ5‘vÂ"LÖ°SÖ<`f’²Óô&f—ôŸ}áD}Åé<‚0Ù·gy»(D‹!ÐË€ƒ[|Á;ïÀÇï¼ÃÛ¿Gjô½8`¨`]ì#þ/Avš†Ê]#o±w BýÿKE`}DPx AáfdGùhrHk¥ÁÛaü«ˆqÃ?Ç΋ Dv>Ñáó:òó|”?SL°”/ÒÏœ‰ÞkJí*yü¹'>¬š3='=°ð-èܾ̈è§Á©ì»j}fD,দ=]Ù%)Yp‡·&½*IÃÿxü´˜â&‘Pbݯƒ¼É¼î‚“ 6Î|²é/äÖ2˜Š½CÃk³Œ²6j>¼ ¢ÓaN¯™Å‹oþtï_`Ê-Ð s…KÛä©×ÕÖí¾îp³ð5s=o’îLM¥,5•²4­Ö\_,†‰uãuÖ4 lâË7ÃÈ×äŸÀò¦#£ÐEn™¾¥ƒ‡¸¹JUÑÅ̸[r´4}Qt”Eo#;¯:B~¾S¦ú"Õ÷¡rÔ…É)Ô=”òs4W”mìC¸˜.-±¡°èÌtn¥€¦ìdƒNË<8Ç1§ébè_¨"Ù__WÑêÊÊô—'—ÇÄP"î—xIû%«pƒ(†=q:ú?÷ÿÀÀ!YûìWSm¥ Ûÿßoüš­3þƭßžyïFÿB÷nÎéø¹øÓ}ùÿú­tËÉv7¡…Q4ªZ,ÄÐß;Æ#AKXœ"W1¸éÿ[°zßË«ßúçƒGæŽ.~øì;_‹¹‹WýðbÐ'ÿJøÁ$dA(PÈÏLOós’8î8E¹8¬Š\~zU¤(9)èóÆeÄgHüÿKü_è–$Gbõ@ 5·à§YܳEª›1èD:!|,’ö¥Ó­ÜŸŽ§ #¤¤Bøf9ÖÉäa4!v=“lÈêBÕ:`p¼ÓHÇ8À 2ÎìG ôˆPï>v¶ ¨—²X$ÔíLðÛ}r9 ØÔ2k%GGäKÚú€¤Ò7ó<ˆz7—¨wcÚ f–¹jæ–·]ÐÚzñ¤àP8–/¨;§¹vŸPêp€¼^Š·F%8‚Uï•m Q%UK„tä}Üs„ d<Ë|Hu" ¢¹!­CX–+Ù„ãDA˜6 §ô(—3Ùï º‚ñž žZ)Ëè§'å3ÐyRÏ`1i»ëLçGDfdOŸSõáÏ=Þÿ—λ³Kg¶†„o7ŸîEDÅiü¥U™]=;šnz`9ÉŠ]unæ¤Ò-ç-:xzy99ÀE+GÈ6Žšc ä!rAcbˆ‚ˆÃùú`MÞþÄûn¼ñr@'F…ï¿ùAø~Ü"Ît§+ÞI27bi%éhkëØûä¼—Ÿ3²yÓ÷çŒÂö·!@s„»˜OxÊDÁPž[<¼®8 W…+4WñF_ÌŠþ¶ø¸ØèÈ-Í ¥’­r¶tÅ$$*KfÑExÌ—˜ò%Óù¿®N’¹|Aõ¬Yäw¶Ÿ¸lÃ"·;'wÓ&âÖì^´A¸«©ñÍcuƒ{ÂÃE%—{³±ñHjúžnª®6škªozfozêÃÐûÂ]ìr2ò êÆ©–&Yì—¹¥òaL2ÒqôÊàé8Ø2nìn÷ûëy<9 [²rÜÆ¦ìõ—ýØ>•K˜?kÖž‚àÔ®ï.Û›–~á37UךŒÕÕ7½pAºÅt¤±éc‹‡Ó¸‡‹‹/;öFµý&~ç5~R¡¤_fâA®ü øa¡ Vò ŽüÈ݇}CîsˆŸ/Ý'Ì71Œ ô>(| -žG^ßÀB-~d´b*÷ý‘ï÷*<8ÐvC§\§p«Aña—y8-aààéÑwǘ7¥·ä†0G@{#¥7eÚ$v ´”WKõÑg3Å”›”«ã`À;Ná!iG‰”XGá;Ê7¾KK:¦…;"Ò‘¿„à•Ž"ƒˆ]”;üŽë:">ÑFÆ[ÈÇ íˆ!ü4„Èw&+=1 (aÅd¯*Ów4­KÔê[Þñnh¾+ í¾^òÎYÿùàƒw†Þ\¿êÕ¯¿{üÍã_!€+È#Ÿùñâ#”@‰ëáUpÅr_çÜêw¯%‰Ï'%¯n¾þŠÜôÝׇ>\ÿÁ›+>ZþÞV¿½’~Ì1Fÿ‰ Ï™ˆF07È·úyMˆ™&2iˆ¼o‹3Ä‹`É9•øOÀ,e×ÑìÀž~{ÉŠåK‡!úÒ™×ÖÔTîØuß·—€æš«!béð°¿É’‘‹…c/¼4î‰æq¡_<ÓèA襡-Þ`‘ 9““ç²Ôe]¯âY÷—÷ïܪÙuߥ=¼tùƒÂ7W–¿ø¤ì]ò¢oxx©ðÍÕÂ?ߤkßòf17iÏ´`Z¤5‹ˆ—ÌLotaƒÞˆg¬_ÿ5^~yý_aÙ=°Î.ö ç ÔN?Dn8Ÿóüì %š@ƒI\1VÁÐóσæÓ5kÖC¹]?ô  7üE¸ê~yr3³‡ë”Î$CïÒ4QpŒ1ˆê ¸¡V¥šñ£x:j5ÿö{àû„ ^‚UÂãî^<9¸Ì_¾‡\®á84 ±ð±pWýö›€xü¹CO¹Ë³DMÃp/HøB¾KX»5~ÏÂõp±0 nñyLÃȽ#÷õÿ×Ïö—°\èýp½ÐCnp¾xƒ{Àß…÷5·†:ÿE…!;3-*ƒEÿpÚãO °~0í±7>6Mlü)i¼QjÌÒÆAƒ?h1Ƥé‡>ô;ë±iœ(ÿÄ ÷2÷r³¥šñ,‘?LÉßGHt]F³EJ> ÍjЉÐjT´ª¬ÿôå‡=¥:øf)ͱœÁ+n×Ú­¹“¦§÷µän]{yNum|e[[e|muŽpoá¾ÇÎ8{Ñ®ïØÜêšÑñÖÞMqqg_ðVÇ Å? fŽq(™ k$b0t”cD›ãÄ“KSp n’Ïåse:©ào!G”†*U™4V:TB©Ìoœd«ÖYo›Ô˜ÓÖ ƒS‡³²–u/ÜzÓÓ®]ÿ¹p[\ìÖ}Ç»fº¶^´ëµs¶ œÿÊ®ý4»øÿôŸŠüÔŒmK Ç4È)”ÂÓÄ1 Ðpk*Eè£h¼7;¦­Ä1¢ì'f-Ç”µÄ¢àÇÑ·A33¯¬Ööοâ—̪m÷2¬+¹Ìq,uÿmWê"ºfVtf0Z`\™‰IYÂ]*Ö{;DÁ²îø×P¼3»rAÓtkRok‡ðÃG7¼sõÇq Ë©˜ãÉ _ãÓ‘ªPL€Á,Ö‰Cˆ¢¾ùJÐÝbÜ’îÉ’ðn¬\"” _òSP@y`•Df8ó*ºŽ€I1©EÚóÖðüª¦šn8ïwÃW‡Ž9ÊüI,?oú¬%ñ/¾c«-[6ˆ†ç zëŠPñÈàyqˆg'/XPIê|óî§Â>áÛ?x9*Éœ$Äñ_| Ò"3R2ë´g‚ë‘ y‚ª(F̽ÊüëáË¿Ìè¼ ­kØÒK*Ÿmi¾N>ô#x¯¸œßJ=d<ÿ™o6múæ™ógº±1–•*—±ª`9“Ÿ—‰ý'ï=½¯ðEø¾| ¹¯ðÚ oÿx8yü}©Éæ[¾!|p½°€T°¾Û.|–í»™¿Á·Û…O\#|³zté¤]”ãKÛÉëu°m‡ïíÃË΃ˆíB`¤PЉ Ò‡Øð]ˆAqgRó)*>|×·ˆ¢í™?þ|Ê;‚Ÿ?ºßEëÔï!Ϩþù.FÒesx¤I|zžb¾Áÿ|ƒë Æ2òþXxdq݈ßúùq\׎ž‡ßúÿd„àÁ_¿ÈßÑëh>ʺOv’§›ù`æ8Î}TøLزÎûü7>GÚü¨´eh[¯—´ Vø˜´Oƒ ¤¥°v»°at™pœ-‚eˆ“ôžcŽÄqa cD€ú^ÐCç{ñ==~ /¦¿ç@´ð•°¶HF˜sØ"Þ$ÞÛ²Ý[1‰Ü¨rW£1ÀuxüØ<á)¬~üèá)ö2Ø,l¾‚hé/ôoºñé‡Ëñ%|MÇ+ýEw2ç0-ô¶Ôú.{ê„…¼Jã¼ó‰cóÃcûÕ©3…î„âycø-}7Žâ'Æ÷],gxºÔÄAûž‘3ñJ ñ ™ý¿åÍúík)Ž_ü}Í;5ô÷iÖòvÓtfƒEy ߺž;*ìF=ÍÎaù_°Ò^âk½Ø{”ˆN4àŠÜäù&:Dlæ²ÿ‹±|#Ewo”oE_Éw;ÝMéëW̆¬É9ûGe2'ãPÿ¢Ô覯¿Ó~Ýô×ÿq"F·òÒÝjJø~ò=1¤ÊôÙh•tÒi2*âÏelj†jµZ§ÖQ§1—€"õ{‰nRð„ ~´¬DV^÷óëÖñ¹?<‹0%w¾å”;S‚OïVxL¸³¢ w…xáü‡ÑN¸þ2>wÝ÷oJ7h'7ÞÂÇœŽÝ]|*» í`½ðp_âm̾‘á‘áßtóÙï#ýžͱŸŒÙ÷¹?€+ü³&žQ™ÅδH¬ô0)éUD½Ï¹üÝÍ_°'lNß²¶Xg ·y×Ã~uðÑ“-Î’ŸR¶µ0TIzi4QN›Í÷ÛG ¸lc£®fïc÷¨B(Õ‹½KõŠ0Â,Rrõ÷Ó²,Â˲ÑkZ¨«¾ŠG)¢R…ƒ©ž‰†Kc¬rÓ)sÔçy¦!075=¨¹‰ÏKʵFª7Oí¼¨ð¬˜G™`æìKê¯ì꿾híÔÔ³ØûâìŽîz§Ý©÷D×—fõ_1óªªíMn0{þeÚS³iÝk²ú&‘ºÕÑyHùªcÅ¥š¨_¤§Ó;Î8$È–bFÍ´Höøé‚òn=‹´x!&šõÓF“µ„«ž³Ò±à€VÙþ©£3Y˜ñŽSÞ¿-™4„/¤¿ø~ÅŠ1Z§ø‹Ìg­È|r„×cåõX¯õ’ï³ AxLx¿uá…Âk×"¹bïT•"2¢XôûCfP³JU ;Û¯5â95êBIñ7›š$81§ÄéÛÒfCÍb¥í¯»%ÍÆl22Å’¸¨¹>°Zű uÐɲl€BL€ç9¦ ù ƒ]r{ôžä«.OÞ}»p #÷Ê,aõLáüâ»ù¼]Û֯߶«÷Ç4X.ìÅ ðüÑ«FŽ-‚;€<(å‡r#×n‰1p%9C×XÀtÆC}hW¢–x«ÝǺť'1 RâvÑ­ÑP ~'qÓ?²¶vï'çžWW}î'–•¸ƒv{E\~«"¡<¿¸ ¬êÂegEŲøØ«¯ùqÛÓ“®ÒC¢pœÓk-QÂ7±n­E=Ú‚õºøTá›Äh'¼Í>5#}}ÛžÙÖgTëèZ¨ÜÔ^aÈ‹ñƒjU±Ð?Ò¢0ÔBI“f“V@>ÆZI‰ëKòì{²“Sÿr Xï7^`ÊÎæu¶=©ºµ5`ýtú Óïü»:F£ß S K?Ê(16äÍñj£bW>èÊ~æ~ ]¨›& /¾ž‹à„Æz£FâªFáŸË œóDÁÈÚT óGWÝÙŸîïCÝîd{ØZhµ„ã-b)b·5XÌdú< ÍUQC– Õa0bhñ <IoÄ“˜àŠœŠŒ4³‘ú…Åj¾Jí$¢“=ÀˆJmÌÕ¢òKõIÈ:HIÂõMï¿-*BÅ©T}ûö›SRÓSS[³nßr ÆÐq1‘·o¿-Öe6¦ÄO™yLmÊÉÍÉiiÌ äæ@Îm6WœÉkµ¦ >ËmÛo‹ ~×·m½-ÍŸ™œí×§ÉwQÑ<§J»Üüú朜ìì–†übЋlh© ¢*z2­æCUÐeÓRõHa‹X1²æf®œ!ŽjnlÖ8ʸ” ,ƒâü–°Œ?t$:Ƹc'ÂX0frÀ(eaÏ¢þ¹s:Ÿ˜3§¯ï¢7 qRÒ¤ W}RÛô',(((»8ÞH® ɜβ=žL½;IŸíf,±éÀ—¹œýsz~wóüùŸ}ÆEòƨ“(6QÇ Ï$ö/¼½kg~I.'¼È­W»¢2E>ŸaôÏœ5&¥´,9ƪB úTâzù8‡<(åŸÐ@, 5n²)Täà UæpÀð $[Þ¿?ýŸsîîX›¾ï§MMÕõõ-EÓÎ<Üß!lñ_iÇsq MmõõUMöíÛ|@­Æ‘ŽD£ŽV?ôÃöm11áÞVÇcwÌ©}HmÐpQV«–QÔl©®ñxZ…{=11P„ÐÅn¥xŠ+mÀ± Àp,fÛÄ£’‚z*Q¼‘ԀР¬Iö>Éi1Ó=ר¨YžWѰ¤|e‚bÅXÌ©Å,E¨¤ô$ Å—_–,+NIõ¥öæåë'@¼?¢2§ÿ<(µ¤|)³ù†8`…·zty …®8ÛǶ¨ÈÒÒmxþkàíÞ“=çu DÅ𑥡ÍG^ vî ÌG€È?¶›ˆÖj¦§ƒÀr y 5•aÂ.Vjõ€ºe,þC#ÅPó1ý/ì­·ÞÊÜ5úGœ5úÂ]w ×AÒt¶[xC :ȓӅ7å2;•ú‘Ñè×(à¹h`yüxC§À›Š˜"""Zœ‰šd/ÞãIIÈw狯‚1Çž®ôßàúÛÈ?þ•ÿÞxUéÎò%sxhî_¯Ø@ûÒ£W?*ü·@øß·byÿ’ð5{œ|—}–,M.*AíØ%Ý å= ÃmÅ0M€¡‘ÆqK—ði.Eœz)rìÒiïE0†ô 6¤"Ô\±Y:ÄDé˜~²búóE/ñ³5jÌóKhlsø}TÍÍPäe€ÈHÔ+BJ°yÍÏÜV ¿þ^tV…§Ü+òÌ÷Š2`ÒÎøþÆÿwL1ÔðówÔôl½RºŸäÛ¹À€|³èq˜;£¬4/¨uRi{Y{qa $¯ÄïM’!ÐB¹>2BÅ¢\È‘QÏ/y¿ÜQØM}è¨C%` ÿEÈT<žÄ<½ çú0€X”[fÜÊPÜÀ|š›O€Mò–¶ oêÐïÁܾ¸P~Õµ¡ó¦u¹RZ¦û–Ü’“ŸŸ3‹)˜NR…/ê\áu%LM0°,“•:ãExmæ5óFäŽ.¼­Ê…Gmå—s¥eê4q­ç,}ì^¦hpÁ‚¥BïèÝk)jǵ×Y’ô‚·Ùaƒ"9 Z‹;O<¹E¡‚H`™(1úƒÌËç•eåäu*@H={ a˜’= Â]„¥X-™1’˜?é”\k?yòÉåÃôœÕ⨎wÖ8—æäÂdáN8>bZ?qrÓF«Sø<Ö¥Öi«jöÞy\¸KAÚGx¸UK‘9PJA¡)Ž8L«¢¨U)n¬Q3T„xHµpÔ•z¡4)±ö-"i縖ÄD“)1)1Éå4YLD Чˆ&BgKsQ=]ÀLsEscŽ’äÇ+ya0á·Ø*¬ó÷<ð–ðá­×ôîžuÀå7O пSfÍkæl?œˆ¸üá¤Eÿ§+¸Z+ü‹m†láyN]4¹½D=²¶ ›XuãÂ¥mZK?ƽŒ|¨=ÔF¢ÅV`xD㎛O­Z*š¤©4–Ïs”詽„âoÍlþšŸ—bN£ÇãKÒ’]ÊK–>º-f™Û ^‡ƒ’¾¸hMÍæúû<»buÙÐàðYë‡ † ±G¸AĤ£Ïõ©©w×]À]èËøpñ“wüñÉÜ ¼òê ¯ '_E€j J]D€(x9‹0Øu˜ÁàÂB8LâXÂkZé°Ø ;þ"×4Cêk§JnZ8y) 3.¯¢ƒ’3ÄŽOoŠÂ ä’Ò‰V²Ð|úá”­ºù´b‘M7Ì’JÆ(.ŽYoy|ûƒÂ§—ã½ñª›o¼{fJîÿ²7öF˜­P]1g¡-I?µuÊ„™_|Ý-þ¹Gøß‡ïï(IÄOß<¿›a…7|×\ö ¡òÔ ÿ, –n?ñi¸…s  tž„%£c³Æ(Œ0å+h2ú þFñ¶vÒB*;¨’í¤G÷r ÆxI³šÇ@Õr†4Aôm'yt®TšQ×ÍŒôX¯‘¤0˜<"„)Í@#¤•¨è_úMPJ›£âéu•¾1™ó+=ž÷ßïÚѼ̱v¨ÐåZ¿õ}ÈÌ­ðjuѯ• B.Ä.ÎïìºrôvÎ/]+ïÐ}úÞhn°¦¬ì°»†kG_=‚ò_àŽŠ‘±n4=ÔIãV1f:åpÕ.}”š¡¤(W£!&ZÃèzä0V§3!Áévº“\g e5ɬL¾ÈÎÉ‹FFH C^àw›]ùnKýw\t U’Bûþë…KàÙIfX5w~ô½Gá°ÐD_ä­ô¯{üC© ¿ý722T‡. é2AÅgÅcL)IJ£Åw1§Æý䈒Kç¢ÔýXÙ,úˆ0 eäœY?ÛšÉíÓ«U@=±¢œÖ;.¯«¨ËÍÎHs%šÄHj•AYD8S±ÅÍP¬¬.ÉÅ„:èÂ$êgs£§,QgÑÁ{}úƒ3¿ô4Üpe۔̷³`CùzbÙ¹d×ËÏß²lYj{îŠì=#BuIIõL»VÍð,†•ÅMæ”ÑrGñàC1›·­É ¬KÍŠR;n¸êÞ;ÆS­ABµfÍìï›.lE¼'"”øQÊD9D ´,'G"hòRÖJ£âƨ×2bÌs¼).ÆbЩ€EZH]Éh D¶$'{§xbbKž^ã™yá.˜"Ðè(² DWIr½Û n£S#ÅÄPšH‚q¹ˆ`kÐj ô\±ð§W>˜XæþAÔ³¿‡÷ýîD*dB Ì~ßïØc?F!@¹±·‹"JV(ÀÑÀ#ÜÄp€x@} P…–eà[è~;ª(à}.h|ÇÞÙ³Á A¥€ áæ……áü·xõè[Ø5ºûÀh!n½bù õ9RÄv*ÛR«”ž½Ây¥Å•#Œ‚¨•?˜O^²tD`fò2¸àåéûÈÉþC(šŸ¾—üÏAOñ¤©·¼ n". E“Úo!¯ÑLJ4B}XÌX_ƒf¢¥h ú—¤¶·–Æ"X‡@ Ë€ÕÎHÈe›fª·ö¢Jº8CêžLî­NM iYÐR*9Ü¥‰TEpb F§pÕy§mãšÑŽŒJ…zǺÍ¥ ÎêÎδbxpÍК…ó»—ÎZ:­³¥)«&»ÆëŽ‹UqTŽ&ØÇ’$ãBX©rL¬ Q D—b¹‘ˆ¡üäm07NØä§2Uµq|Ú&¹c”Ì+þp™HonPfqnhho¨KJ`X<]ËëmKíÍöæ¢ç2R’brœ ³9–(“ÊÉ«’gxœ¹‘ŸP<¯›×q&_œʦƔ$'RÌ}09¹À™a.»Ü©ŠŽ·xušëȽFÞe¶o]8kÞäŽdÒÔ¼¾¸¼òú)‰IÎý߮ŗ›ª×d-Š5FšÕ^Stç¿Ê‚®ÌÉœ}WÝ%Q#OÎH²å›,D¨~þê¿Ë‹2Ö×îúW^^Vå軸§Ö5?ÔšžV‘–4+³tr$,V„Q<9ý÷¨ÉBÅ(*K±c–1è9zRÆe—¢@ýH¥R;‰4d9ßâ³ú=„A‘W,˜ÆË; Öì–—™òˆI—äeÝÆpÁüâðòŠàÙ¡Ê>S >»ëá¶}ÓÓ:½¡ Q—¤›Ý}åuWÞ˜ho¾c³5>3É“òäÐüYÕeÁ‚\kÁ>R)Êå.ÏŒŒšùüÙ¿¿÷®æýsz×_|•¹¿`}Ñ”y‹«/C,*Eˆ}žœRò¢T”‹^•R£ ÃDòXÔÍÄD©Ы1Š´ÐÑH£Öô#N!žZí€VQMæÿRkzWÌÌŸÀ,Q:ÿŸI¤Ìäô4Ÿ/11';-7=×Gt<)ɉÞD¯'‰P+Ae&×O™ºø4 "–RQNù‘p›¹¿ËlÉ¥ªË|}ù—x»vvÂü.€®yŽYµï\:D CÛµ‘Åû×Ã&–¶¯¯]lÒ 70«ú'÷CuAaÓîþääþ š‹òG_`÷ëµ9Í÷$'O¾ÛbŒž„0J?ÙÍ^ʯC.”†Bµb‘¤hÌá4 fØ„y†U2gÒ QRÍ=… WÀÌ`4˜ fC¼d@‘„ •°$-˜ÎB )%gÜ,Z’ &:w\Ù·ûö}{Ùõ=ÎwÏ"wrÑjh[ùòüë»+ ½±¦CÐ@˜îšC±ûÎcÁï+ªàÄÌE@åó£ÍuåüùÜeIS.~Øõ·Ú7u>Cˆ½WD÷4Aq[¨%š¢ÇmjŸžnªV‘†lª¸:E*›-.Îæ°‘-‹³ÆYOŸ+%$/â˜B¶ŒîQ²ÞùUFœ]·9íÖㄊ?¿%mSí"áµ~aÝ"áu¼.9ù/i¤ÁÂÇ.} wŽþü¡Ùt„+™³Ï?_¸åÒK Dû ä° y¡^ °\,0l “a´š<б›8Ð@cXS© КñuBhñ1¢ÒÍͦê\"þ:¬q¦ª«Vñ4ÓœÈ(:=ù’¦X¾KÎRJ£)Y¢·’B¤¨»”äHQRÚ+œþ»õSßô{¡ˆIÓ¾1÷Ñ9Ïýã]`aã!Åù÷­-Ãù#o2kW¾ðÑÊWíoý'æÑ¤XÕ‚ªG-®›êªÁ9ûê£Ô21|ÆŒ Ë£zÓŒy7 Ïe#Œ2‰ôo ,BÙ™n'Ñ϶DC#¬# Q‰L[Þ¬h4Iìr’˧giL&!O=I7a(TÏãl˜Ra¢¿õ}ðôÖ³žüxøz{QÂ’î)¿kh¸ªsöâ„"ûõÁ@çÌÞ[žÅ¦j½¸#÷{ÓTÕ³Zµ#_øgk­jäJUm‘õìj-+|Ì­ŠJ*zdÝæ¿c©#îL„ص¢§“ùCžpF,V4wH™5¨±8ìó¤äŽÿÏdí'Þ ÂNÖ.ì^!œ• ì,#¿G4¯bÏ"w÷¢Ë9’9É­F-k±!œ4HQœQWŒ/ÞÓ4P‹cSœÜø×Ü’"¿(¯Ç r‹f—¤‹½ÇE ¢+èQZP´_¼(€ ¾LíοùÅŠk´‰²¦ÃúHkþú‘éÌ]o;" ¢ë~ËžôP6D ÂMÓ§|ïtü†ÇRí%=VØôØœƒ1Ú²¹£ @j„Ø¿“‰¥Z--Uê‡5ú}j ­è³FÙôl‹<&ù'æÎ¦éNýD-$¹"Ñ$ȢƎ à@—pämá (‡$pC¹ðÅÛ‘.8 ,^Ã]þÖÞ‚§~ùÀå 43ótô#»ž³þа¯åi™µŸ^‚Ï>ñgyös1$U`×3#ȚЂÐ\Zê%1#¨劯 `–hP§Ä§ªñTk¤âXÕ4¹ö*å Hà5°*¶Íë¡¢‹ ³2NÍöæ”ÖFÏdï¼íàXÝ[nSC“«1mB‹æo«wþvH,\ðU'5Ö66ºñµöX5ŸaÈp¿WFì Á¨‚ÒÊ ‘ÑBÏøk¥ ôš5Aí,ÿ¨Ø­G×°l'2Pß=ÉL§‘yþ¨ñsK %Î":Ìq~¯7È!Çká8•×àÆzáP ´w·@óíÐÒ"†&òévøîÞ=wüqÏa˜.ÜrxÏïØs¯p T5ìfyQžè\”Âs˜AÀ2ÐÄbcbU;L’É‘fÞd·Éç¥~ƒ¢'YQ©â‡_Zá|^åVR³XürJPvk÷AÛýÓ*Kkì ú4½]ó_Ù}ëÔ«@mšÍHÇÌÀ·«˜ë}ž¾9–˜d¨}jäœk“Mµ™P›f»üÄ +Êcïa_Dñ(€*Pe¨¼¢¼¬”Úÿˆ[GODåë~Ĩ ¨++ºBÙ¬€RüÖ€-‰â!ž†w{édNåÝy¥¼{8G–‚€ýÔµ h¡®*<°ååÖiÝSRçå/^³æ•ζÙ]]­GÏnÛ_XP,sV&Öµµ^RT\T,ÜûÖ½÷­»üÜ»þž{óz[§[µéèäÔJKúôζc›Vëjí}”°žu¶\XêL‰r”_ÜÚqaA°ì¥{ï[?éyϺ{î¸ ”U ³æÉ¬UÀ©ÕP¦‡GÌkê¤uЉë [‡qË–7~M"þkÂP÷Ñ€"÷oZNö6à„·ý¶%àmÀ ?Þ&Œ"MG¥ìzö#IKG ¥gŠ…gdz]¯š«)3§ÍŠh[ GÄk~!"ž“ÕÓ? ?r7ë:ñ}1{Né§ï|†T…ºÈ¹éP4™Ci¨b:l¹²äxN•èÃiÐ".F6ƒ•" ɇM­;Õw“Žoüg ù1 Κ=ló‰Cp«øYO~“Ÿ-Ÿ MÒ;ðŽa¢˜-¿FñLþÂl.F€ Ärß ÔÝ|§KtUUò§c¬h)ä`3I˜ 7T†/Ëêq[ý<Ì]Ø÷þžµSßoyª¾þ}Ä šÁ^ÌÞŽ¬(¢e‹ÔaŒòóR£€#@À0M€¬õRf # h•ÈÕ­¤Y%¬!Ì ÜO®0ÓXªs1@*4F¤§¥¥ùÓý!žAQh ;'I¾IT¼UÁ¸ƒEñ'—«øçdhmrÅÕ¿{s芼µk×ú*®n>²jð`îZ¼2ÁjjÛ•Ugã¢ÕPpéÒ%6.R-h#tåϯÍ;8øöÕ¿«L&ò ½uuóÔo-3'Fg+Í*8§¡1eüãÒ%—5WFÆG Zug£Äü„˜¿†˜_”n”OÃô1ˆí“×?;OÊîóé¡åNáLA®;‘Äž±Ü¤0o©Ü3|_åõä'ïгè„ØjÂBéPJÈ7®T·"çÒˆÁ‰,kÚÑä× xxt}1׎Ì9?€]_ý˜†ðÉb_áö"ñȲòR%¢Ç’2•00ŒÎ¸€þçÙ·½ À%£¯A ·÷Çåì]#ÿ™ªA2ÿá #Ј!˜ÎØFÞ£“>°ýGaΧÂ\%¯{ MO,SjÏ üd‘ö¥`"quð;£veupÌ /|#Üõ‰pgMÞbŸ`õÈŽ2Qšj*IJˆ$«M5ˆ Ž®_Ëk‘Ú‹oXÔ˨1­.š•èTXU‘]á÷:23­q1z­ÙÁ®©…?œ7Ã2V¦QÒ~ñ~ê^"3^"¥pó¹SQˆ†æsguOËXY;§®>US?ÿ´i3gž7cWåàƒ™™)A/»Äë3\xk±Ïërsr’Rc’ßZµòî{—ì¶x0'0<|Ï_V­œ·ä™§òò·øSŠJæ¥µÚ ‹‹çöƒýÕM¦L¨AØÍžÃÇ 4_RÔ8ËÇ‹|î¢Ûô¥Úbê<›$5¡ÚhÌÞxšF"¶Êj{N³Q«¦<•’ü"œ·ÚË‹gŒ1RÖ˜°†Q%óO’~‹=Í•p÷Y]~oy¡ÛM½°NWœ…¬-* \»ra1ñB÷Vd$&ꨦ5̵ЩpQ:µÝboÎèvç˜+SK}ÞDƒÏ`qGsZ¨€Pdó ÷IX´jñšK(9äöÄÅeDÇ¢Gw9S ç¾ÙY¶µ¬mÈ;‰ºŸ ±ø=Õ.dG«éÄàWiíâ”:ó 3$‡gÅ$bTXK&×ã7õ‰ ¤ÃÀl—hÃfˆùþLFê‰IAŒŸ Ž˜¸|be²6tÀ¹p]si^‰–Ã\\VI~`µEÁ¼…ع­y)–ÄL(‚Ê =ܳ'ÙþQáÒ•bÐþ{bÏQõ‘y->dÀʼ,0Cç%ã÷!*µ/cè´ÜtÚÒefóO®+=:ãmc—1™Öa³×“,êæóÆöž²Uî$™oÆ o6¸à=yðK'Ìê$ÂÇèØa ûؼ„§„‡/ôüXŠFˆµ²×#Ê“ò,”çË&9j€–ñW¿üÍŒÃF·1IÄÞÔýO1œ$QKêágß}÷Ùs¦M›sÏ„<0 Ÿ OÞŒo\½yóêÑY¦ˆb#0L=òcßµ?’@˜÷~ømXM §ä$œ?jÇïÈXmÙ'ÐúÍ /ü— µð|yÅ ¹Ì&g.±it1Œ î²í/Ÿ{®ð2{ý‰fñÈetü°˜]O€Cå’""VŒå^&…vÊ’ I4f™ø½Š W1¸™Ý#ϰ®ÏñeŸ~0q€˜¼&„ðl©¨!†v¥cTwRÏ=RG…ËÙëG.cŸ ©³ªp»ÿøK¡¹U”Ã?ÒEY‹.f§³+‘ŽìˆF&5A Eó°–Öjºê¡éá°øK¸Š  nî ‡o†Š HÖ®µü}(ùQ:Z2¦S <‡›"t5$_/±yIZ5f4 2p* ö9:K‘×ãg#j£iÓ Döh\ópŠ@Δd§ßé÷¹r¨{H²Žèd-A9H ´ˆfLÊnS^Od-F£¶bqôúEÏ‚”ù{µéXškýU`.½JMYAW†uºöš«vˆ,áƒj3WÿØ Ï<sö=1yõûBïûÂx_ÍÇ~ÄæÂu"ƒ8åü<¨õäwìv.Q^(G«ÁLƒP= 'i—x‚ * `AGOY ªT¥«‹ê)•¬šazö_•w ѤóeĆÿÄðÑèàÈê»>{ýÈÙt7aš„çè9y>ÑŬGñ(‰j“€Á,ƒ›"#ÈÄ[Oùf†+ ¹"#ê&‹gLó"e9Ž,žôtFZ6Æ(Wkµ|×S–ÓP·íswÍÿKÃû£ÿƱ£OEÅG΃;Oúœ=ññçH%û@&Ño¤MBÝh †{3~ÐpXÙm¹Ü)Ýmp:Ш9M¿A«gh¡¼h>Š÷|ö¬¶Ö’b2}3 ¾³†fuu´v·u×TO*™”›\”Rd·Ë[§A&0ÅL(·¢ä:u7èfXÄó3¾»KJîvJG¯ÒQìçRÌßþ$E¦Gé"_G†wêYeý>_z{êâ€×Û”p‹ïS¬ÉâcÀëÚ§¦¦L]³~êÔäôøzõÔŽö©kKûÈ—þE£Ý¹>oS Ðàñà+¾hÂZååzôfOT3=Ã×±¾bÀ;5Ãï[Tº±=-åz¿/Ûäöäåû|Â5ð»aÖè?;ºÖUÌK™:uêúâíkÙuS§føRŠÖMMκ4H_t–=ÛëÁ}–<*VÜòYìÂÉèP‡xBÝby¸N€ĉJLeN<^’ %5£fhÆÍL3߬jV7kºQ7tãn¦›ïVu«»5zå`ŒÓ¥Á×Âd¸ó3¸Chÿ G|Nÿ~.LV¸)Ž5Fi¡äñÏF¹…r=`Õ€JqšméãoN,ò€>8øú>úD.ëð§0O¸æSáš>ÊQZØ"Ž‚jžÄÔÆòvŠlåàx¶r¡ø e+Ç'§ÿ0×DBïIÄ}þ}ÿâ"ñ Ž]‡Œ¨Q¬9!efF ‹pZB+ñãÔDˆé¤>”³Åf“DY!F/æ+2‚‘ÅQ™ä‘¹QÕB¡}¯o$D>9tàŠA\|þ¹ïíÞ÷ÎöãWnÜtàø)#)—F#…Zå\Bgw¦tz3û\%©üd•¥L¦¼›HæÅƒW}Ÿûƶoì} –Ø´ñÊãÛßÙ·û½sF&º“Õ#JA¡:—ݤe𢀇F Ô>BI ‡Å„iô/C$&&OŸìLäu'¦8S¨OXd„šGphÂkAÏ&Y1‘š$s“÷’lîSêÞ}5뢩Ü×½//cw·>5>Îo¢¶¯ùÜÐàS šrJškÞx`ãŸß^0¸tá“V{BCqm]læSs—>óÔÑMíkæÍF€rHåõËø#HKTóÊß³’°HÅ‘¨°ÖGªeSûMdk)Omyj•ßkÂòÐÞ̹ ¶ÿ;OÈ{¿xrÃèœçù>Ù¤““"º^÷ˆ¯X䱌h+—µ@äÁ®Ÿ^ž €ÅyŠãÂÀ%‰ó?.zˆ~ `’À%Îö¾+ÃÙêÏ'Œ¶]­m\vk†EÌ©ƒuN¼úÓ±j  ËœþDíÐOaP<ë ¾»—À!ßéE"¼lâ6yû†OD1T|æsfûgÂKâÍ_’Ö‡‚Uô ÐÑlÃÐö°Bžû…üÏ(âO›Žöµu›$ÏØV‡H³Î²H k¾Jö"Ùå1˜œ’’1WöQ ziK,’;™Ûµ¾šzFFÞe´ õS"¨\ô¿††‹ÎÚ°_ˆ–TÔ“žh¨ÿé?ýéÙ„ëûû -O&ÈXG”xÑX’©0(•)v6™ìǪ24éýØéû–béƒQJ–ù”ùµ>b˜¿Ž®‡G„2|p$Kx‘écR.8:êyÿEÄ ‹Ñ[ÅÞ ¥°3VH¡õ¢k4ùÃ`Zå‹ O:s‰¯3Ü»Æÿ¦t?ú¨ô¦H™‚± ü€òNù;ޱŠBõh*šƒѶÐ;`&;5‘ÑrKc¶¦³š0K­-1Yzáú/Û…o ñ`{ˆx°ùЀ¨¦ûÀp Í®Cˆa×Aê 1áã•Zq›G= 7ž¾ÈL¸“ì6ÂLèÔ<òO%:²!C¾¹œDOáR"n ’9]q@¢*î-Â1ᨀð_3%Y8Ó©r¶nÈÏO°ZÛ«ªfZÔZÖ&O¹Æ—À ?—ü½o¿É£_±bð wå=…ÁÒÒ¢ùßß÷wÄ N^ÇVóybÖ+î¦qm’…4i–Ej1£“ÏKé)Ä !¤þ[@=jÑB«TÎͤi…wü–^ ÿ·‡9ÿ#xŽß>±×;ˆq—  ·Z;MS¡Œó¤Œ&îVêEI¦É)$áD¶Q¹ÁÀ n惚‹€òEÂL8fý«4£ó­ãº+/’ ‹½$J/óìν6 T¥<me‡Øí(Í–ì˶°=œ&f ¹~j5Gˆë—Çõ ¦soX̧ü«Š?Õt~11ï?kÓ>øZˆº Ç¤eâG>Ô&{wÃ×[Ï_³öÜ ?«©y{Ž!Ãô¦)Õ4ïÍS”Ä”޶„Œf`970,n21×HV»µ*Ìhd¼17<Œsp).»ãZoCÏRIdàLKõ¦ûÒ§'CThp¤Eòo%;óÿ î;£ª²†ï¹÷•i™dZ&=™L¡“Fgˆ@BW@ÞmˆŠ,ˆ`—Ų.‹uYu×®(®ŸòÙ×-–u]ËŠ½DÙµ,*d®ÿ=÷•y™$ ßîß“yyóæžrÛ¹§ÚÂVuT—Ëæi׳ 0C&cKû”·]ÏÛáÄ_ê~½b}ógê¿ø"”¿þÖJ§ñ²S¤ˆÆ÷ï8ãŒàæà&ÔG?›zp߃2ÙòåO+ƒ„š˜™Z_àèÎB2Íè΂ª×œÈP¼ï"MbGÎH繕"eÜ)Ci‘Ò)Qj(Q.¯ZOéß;Ôñ1ËË­^p)÷ÃW¢O…@yΕâzÑ ~½«ÿ^S»øï—¬]{Ù–/ƌż»„ˆ¼»Š‘HÏi‡±“x¤o§âªr°£“* ÑTPCÓÀæ[‚&Þ²9}¯;e¸HÇuÿ÷}µW¾›ïPh0C¡È(–I/tcØt§ÐÀs†S©!½`{Z«¡*ûS-•¡PkØŠŸä"™s g‡1ÔæåŠ‚Enõ¥Zgé 9ßY £Ëæ];ëØŸÏMžÙwÁÂÅ[’K'Å Ö̺vþüëf7ö]tÉe ß²$Ù²ti2¹˜P㼬¯%qÒ´Dq(jT\Šƒ!ÚcæÅE4óÝ]—¨¦Û¾ÓÒ®j˜¡[ÝVv Ót ÖbÛ—¸ˆëü÷Mbå)éÝùKà–Vtóm\•zu~Ú ñèú-—ØD>~}ÑÇ[¬R¡–WWÓÕ rL'®7Îzé þNÕÀiÛ®}}bé¼E³N9¥õ˜›lMnÙ¼óÑߟ¸ž‡J%Ôk{Qá¨!#ÆlØ~$ïºøš¥×•ùO^ô-Ü¡N`f—,“ƒóÈ9‰@^@5t³Rt…™QEPç«è˜zM£ª¶Ìí¢”É[ˉLÀ…C…ûA]ßÔÃÀ…;sssóró\ôîÍñÊ:¨“Á q–&™<Æ×ÁºO>ùÑÇœD–T^»¾àdëüî4Uê4ü€¼˜þM+¶§»LrŠ5ÝeY·FI7Y[n¾ò’'…¤‰LH´ô PFû•X)£Øœ˜À€âäAÿv·âb2éHqúð÷ YŠšŠ›ÒÇw™x¤Éèà€¡›Ò -TKõüÙW6NË(Ê]½!} ?ëö6Ÿ_YuE(4v©V~åòr̲ïÿŽ—ÝÝ%ì‡×±‡±èQ9‰,O,S›Ã;¸wI@Ñ€53†${ujÚ§Ü*•zg{4ŠK†ß•¥øTI~C=ºX&Ç5'† ©ŸÔ0©_ŸšªX):Y†ƒè׀ޖÙ6+œÔ¦ñÓ?¹¦zö¨éYÑÜðiÇÛWà«5¼ÙwÎ`yu:duó$Ÿ_ÖûÌœÉÇ!kŒ+äL¬¯ vY÷º{Ž0rµÐ Œ´µÃƒ`e†§˜®(§õ¤p\ßgê&Úº @WÝ¡ð9ž 1¦ÒN H¤ÚBFf¨€:‹.É{ÖUØú þuÏz ¢dhMú£Á“¸©Ëà’E: ¼š‡!—*+ >õ®©è_Ù¯+·|?’[±ž9×qeOZ•ÃhW¨á™ |m™$').ÇE iïT5ƒ‰Òü°ÏìÀièÃÀ.´@lA·µ ‚e7"Ëþ}¯ØL¸Î¿Ùj‡CÏÓÏR¹¬"“#Ûù\›tUt]£r«òŠìH¬¼UGŽNLe^ꑹšeÝC]Q—¹EìâM׿e¹|˜ooR¯šâ"ìÒþ}DÜæ £êíÝêï¹[%ò=}6ÙÀœ†MzGžzÍ›ù&YË3{ó¤ý¼¶K3Ó]'E¼’ÄY‰c*@!QP•Ê\J›@À¹(À ´¨®ÒéõÑнjÒÄ–—懃¾ê¬jƒLoF¿„~dí1x× eáK±+,‚ø˜Ã#K릋Q7 A†ŠÌ@™$À°T©LE)"8tÓiO5Czù "AZ#zªÓºµµõ¢qKO-æUM“®šRª–bjש;í>2íéÁNÝ>ðäñP:dyÝe[Ç^!ü¹ßêçy¥"´æœO³]™;P|¤Ry‚¯+2m,@:èG 8rP½©›1¹å Ùãe"N.ŸT’Q‰áe…ѰϫêXײ 7Âh²ˆènF7KJÅp@2ò†«†R°¤6À£á~TU¥k–¥;ß yªÖѽ+^‚é ýÕ¢¾ËŽÙÙ\?g\VÕóW<<îþ±MÇ?}Æ|š“:áÚÕ·Tª *~/Ù?¯@ ÂÊ£ùcÁÖÑŸÔUТÛÛù÷Ò¼µË ÄèO%æÛ|®ª¤:a’ÙýAi±îÝ šªcJ$Õ‹+ø"Ÿ3&Ã<%ÔTW˜Áé.È:B€åxÕd;^é‡é’TkøNh¹–Çé™ ï ÝÁ¹ÞN]´¨Û  ±g~ñþçWQ"Я" ˆ>M]¯$ÏIÌ*ΦŠ÷»1›È„ò¢B¦ ŠEi S4†§ES•éðD·ÑnÙÙÕU•q<èzћ˦ vЋ³Y—Ѱ3êU¾þÔÿVÐæò`ïŒi«]{…E¯«B;áÉÙ…Y£'µþlâXÄGÍ£%©MÓOD½/\oªdS`³ŸP’ùû§~&>ìÉlnöpÏ+æônöáeu[pžÙ°µ »›K»Û®Óñ|Å$F*1ž/C ³E±®ñ|R‹%÷ŽJsÓH‹hî#‰hvDßaE5~K:¦/ShóP!´É¨>JfÜ g7 a¨ª‹R~e”NÀÝ<ʦ¬Å::­G§YiÁ =›²¼š‚)Ó Ù²²A‰ªŒ( ’鑊êr•ÿŽ{ø18š?’ª†äsÏÁx~ëÃ{øôõîÐõáþÐy%%¶°@èŸ8¹PbsXÝT`.–yÄpÝíú/·[[à3„øâ¢AôïS‹ Å)¤€ÜÊœ,ÔGcÉE™™@ì³èI«Ë¨<(—3Å®%?¬ËEŵ™œáØ¿)cî5¯8¾bi©'¬þßÑP[ÕpRC”ò]4;¾|eyüo0‹ìè[|bcÀÌ¿ùÇŽ¯™ÁÞì}qÿÜâ^S›ŠÚë/ª‰ƒò³Q )‘/wäÈêöÆ «üzÕСícOë-²ä>Iq)žø;S¬ ЉØ2µ¦³œ¦ û¦AnA‡£Éá¨ÀÑѶFL"¦Þ.϶!4üDÑð“Q:_³ó;Iëˆóø‹uï¶„y¢Ò‡_½›ÃoÄyð^aGÔ ³NC 1lObÍŠ¥dd…I'Êpº†³Ïm-—m×çñX,O‰ò‹íÂHHa^¸4RÒ}Oè;ïu%.6ît&‰ÛŠŒÿ‡ˆ ¸µ%ü%‹6úÕ¢íÌÿG;ÎI/k5q%G¤mI)MÒ†“ú4#p)`.*Uiš¡JÓ@ª…ÜõBH–±g,CòL¥¤&íCA“>í|XÈ1Ž|™º¤t—ÕIä‘5ãïSJJî))i£Ÿ¦¢üõô5}ª4AÆÁˆ¶ó¬PŇÄ_ü¿A–“ È ÿf÷IZý‰a‹ì?KY:ˆ8DŠ0ŠS3QièÂ,“¶N¼°Íáp¸Hü $,rö ¥ºÿÇÐ=ÍHC .*Ežct&?H~!çJì",Ý9b~Ì«‡ú­õu4<|PýÐöö¬aeEƒª¾øbÀù5F@#í|“ÓqL Õ9 B¡$-Ydî9éDNÏ.Ã6-¡™Ž]–» ]>ó¢)Ó/9fÊe3¦_6½N„¼7Ž—ÛkðЉ›§}Ñ€I3ýü‰w5`À¸1Å¿jwl7É"² áSp¦ºUj”ͯDssÆ&b¥Z hn6ýzVô¼…¸ÑüIs³tB«ª<0ñèÏ|ô¼öæ_ßžoᬖÿüçßüÜMo8Ú¢vŒóó“ÆD›gÕYb°Ùt\X.8¬©3Ì 0U“NbÑE¯ƒp±`,n¥y·|ºwÝšç^³€j×¥~ µüµÅ=À´Ãí(Ù,U%=ÃD ÌPK¯Âýž[³nï§·ðD( jS¿vÕÏ2úëã ¹IS.’´`K6/ï&Â=”¨Ä@5T©Ú€Ÿå‡à¨¥Ëa4?´œÇòÙ¼¾ìqÄÿø8ÿÃãŸcîÒ‹"ÂL⛣TI<½eÌé-ÿ™Ú¾âëËÞp#“N«ó{=/Ú“{°œÜýÉ rB¢,Æ€áú=h`YÔ®¨'fº ÂÜ &§r2×s“O^óœŠã£ËS"YPe¼¤ˆbbú&#]iSÆòà húDâË«ârÉè72qô˜òâÊC0³…/O”ËÆSÍMàD2 uë$6ñD1í£#’’³Ï/üa"ϧd,'¿€ù;ëÛú?_˜áb eç¹\_±;«gJ~dÉä$ÉËz:A°g²1‚mÙÅ­,aÍÑklvmÉ­1lÉh"`4hw£Eú¾=´O6[DIˆ#tI!)!1âyXd.*Ìb¢-Á»&Ülì¡£3:‚¸£…ªõêoýv*.À[è‰K÷áŸÐÂÛÀîÔµË^øû£íVðí·7|=ÈÜoÎÞ¾¥ý¿á¨q·Ö3î2÷ës¶_ÖþߣÞ( ÓÖp ¢Ý_˜…Pµè>Õî¨èÂJc³Ö~'ãsà›ªÔGtÎûÎø˜_3~Í}U´ uë|;¬ŸÐ~úž ¿þÙ9×}ó8¬Ç/ß~úc|#þþú¿d}è™"ªgPõ„@?Õ*‚CB¬M#iS€¨ (ëNç½xÜK§ð×y×¾÷Ð þ¿ Ž’w›¯Œ¾èsø |gy~¯£Ù h6¨"¨Šæt !úL(cø]ü½9ï»ìäWç}öÀ‹ü±Ó1éaÿr˜ ‹/xè¼ú^Ã*ÆLæ©»¤”vœè‡ä`Æ ¬NŸDpÌjî"XíuM¼‘Þ•¤2Pór(ëù‚ÔFº®áÿà÷Â4Èæ§¦VÑ[ᬫàÛzðTRÿ–Ñ:ž·w¬¥SÖ·¯NýОªt…—‹ð ÒˆP BÐÈv,dÃ4~/ÿ\C·¤6òp½5µŠŸ Bd|+„`zê`%?PÏÝ(¬n_ŸºWŽæA&¸*Œä#•‘\PXE„be[I1!Œl& e–é+¿RFÇ© Lñy…»p^(à­òU…´ÎÌ8ܾЕE‡Ù1»p-5©‡}âˆ$©J·$­@’J’¦:IÊÕœü>òÎôI£/Ý6öÑÛîà‡èí/ñ7ì¢-cãü#k²ãtðøÔÔ%ü†Ûæ½²ø¿ÅtúEê5vU¨»;ÈY2®ò±Æc™^\pr¥z o³¡úÀ;‹éX@±„,`Áb ó_ÐÚñ©G*9#«*©ç؉9#Ô’DÈ”ŨL'Å’ù@ZŒÔ wP#™¬XqwVlöê2üœ˜…ï¶'¤H¥^XPP^XÞã±07t´É ’3\vŒü0X)‚…Œ>Õì@ò#ùzê3:sNÝŸNFõ©Ì>õôÀÇÞ¥}Þ:ÎNDÅ€÷ŒôSœIªÂ‚ª×H±œg$BU *ªÌR–“Mi²XRU€–Í T‘**]Êâv¤`e·OàAÑ~0¶Î/’¹d‚²@Àck+ŒÐ:+MYŒYú °RmÝÞoÛ#&¿\ÐwëQ÷×<Ãßÿ |•ÚÝ—ž‘êß §Š‹b7µí¿!Þü‹?_ñÁæßŒ]‰ù¶ÎØÿ”ZôÔ–‘rV"èVS$µj²Ì¦¶Ì¤sS³hðŠ r«º¤+½A‘㪦\Û'&éö:54²£MÎî5È–]\Íê¾âD5ÿõÔ thmõØûÇöí•+P,4)o8ù={ág`_k÷½¯°×ø'_úþÅ‚tæºî¼3'ºyžÔ2™9£Œ¬Lx e¶Æ4Á%ÎI jr+2èXŸ0@TĪ¥X/Ò8(Æ»Éú‹doäÁëó';ól?‰åïŒh„^ó›“Æì?ÃL²ýì ×½;pÌþ×ÄÄÈñbâeEr*Æl‚Š­î±f­­0è‰g|ÞU©äx…±Š˜™ŠNRƒ¯Œ!¸Ðt&ûê¤ívº±ã{ͬŸ—¦çÝë6œ½]$ ¹réC“û4[‘$JæˆêuªB"$†EÌ€ › ê)¢I©]ù0𠤏 7Ù|M%ˆ¸ŒÜm´AëÂÕ¿Q^RÍ™Àš®|âãûëÎY½zCÝþ•sa0~²¤¤xøöÑÇS3wÜž½k;½#Õ¼³ŽD53ñ?7a alîš,]"鱪ÄÊÒ…ÖÒˆz{F5ïuRçÞQ¾eËùâO˜Î÷HïÊ™â”z×O¸0”„/°F!ßÕžÚØžÙLOY‹Ò͉e/H÷‰Ñtcû¡;äz󹦹D7ÊtUœ*]r+ÛXEœ˜I in;O´ïjïÚnÏ pÒíÚhb»6ªø¢'Âí'ìBÂG¢\-³±å'rI©ð$î 7¯‘ÊÌÏS?ÃàV%ÿÐÇÎo£¤hDZfær“Åkäçtãg‡îP•C+ù;2òò7¡’”€  ?5*4̺dªƒœ v6þÌ,ü®ÿa~´Øý¤ìûü[zjÒOL¼ÿbñAû?U€¢ðÓ /À§m<úK.Èïü?Щ,øi: “òÿ4bé~Q:‘ÚL¦``«s&˜ô€B5e…Û Û-ÉvI²Çe„>¶Ž?jʘ)ÇÖ774ÿ'ªK4I ¦•v0?þI¬™|êÏ[ÖO8½uRr}kßQ¥¥}úñÓ8uþÏWO<39uÂê‰ëÇÝ4¢oŸÒÒ‘}û'ºƒwå¤7(†Êh2µýš…‘·©NÔW«Ž#~8GP~–Š0jÆ b‡ePÈàLu€á_˜õVEFè±j1ULí†ÿ£˜…¿\Ýoö… š+J+ïŠÇÊûÞ—ƒYoó=où‚{Eõ¨ùG‰Oîª,Mv¹Ø_{`›*è'\kù¿Àãòe÷Ïçœu`XókNØ4zHè Õ§æ÷Ïö¹À•U:Øåf눒1›†áÒ9ŸÐ g‘w3ÀppuW¶Äó?ž[¸›üÄEó zVê|ýÄ9vÜÏÎßúÿÄzòé½îçÿ£=biUnV•¦×¼â†¬º¬³äÅ„äå‘53fˆ7†žˆ &)Š©U•Þ)Š®èA,w•r@¥ØI)§º’ííøšÒõd”G]y–X€ÁV©˜ ¤¼(¨"RŠ@ ÙÏû5͉ŠÝŠ:6è:ÜJÓ0ßëÂÜ%4y™YN‚ àŠTq5ª€MŽÒC6à~¼þ£Ú>úP{¶áßþáÇm‹›¬ÿðÃÿ8Õ4ÕâfcC¥CùUΜ¸˜èÝNWƒ‚gWI »¯ù 6¿ã–/T%u`ê›n0¢´)ÍlÀqQºã6ÿ‹/¨g?õìp¶ A  ›èòm;ɪÒ*2¬*¹û÷úŒP;±=#šÐÒh 3µÛ¦ªB¦ý"õ2í'³Ûú }ë¹g»~Ce¿vœÊ€ô`…©—•ëd‰~ŽŸJ˜Ÿý¸œø%ò„ƒhxŒå@LìDM1€÷xL-˜ÿLçEì|Øøgþ*ô~‰oì8;³YWÚé•QŠsÎl6(›“¶\Á¨NáðÁB [ÞÈÎï8›oü3ôáyIª.^ãm‰’ޤͰ¤Vâ’ªâ’j;‡¹Aaº‚ë›CçUÛóséLÊ.3“ra~Úš Q^>¯w~ïܰL…¨G*<ú‰ì{#©¡ÃIŠJ°îÒá2Ïåw≿;x9œÌ¯Úôê† ¯n«c¯˜9óŠcñªvðàZ8¹>‚¾%½§‰ÏÅSo ®­üVáK Âã„Ê‚ðËÌкefjÇÞ"È®éù©Œ´Í o•PÕĪÊd©†Ø )1ª1ôáöÓx,ÐMFÝ9p²A¿ NÞú=ÒûýV¼FÚðÝ –_õ[“8‹X:YRgPŠkuêa(l+u ³÷–)'èÐ’™¿±K‘”@†½U‰9l®¶¿Xj¨xã÷´óûþMÈi¿±LûlÚFËnL;¥®n‡©_‹¿ uìNÔM¡ã±€Œ°z*obI7®Üþo·i³,Í*l5Í¢´åP5,‡8T†FÃdŽF ËaTZ¥ Ò&ºcé6ØÊ/HÍQbüÄÔºnæ_ò»a Xïõt <õÃêŽBªU‚§¾3¿}|+ÿ¢+H´Rf‚,A`€´Mhè67‡_[é6¡¾ À~7ÿn¦Skø‰4±š4§î]ß± B­ãÝÓ¹»ž¨L”0}b$œL*H5& „Eÿ«$)Bî…½¸° OD­ ¢r ðE|¨J.ÑQ´XUH´´¹¬ZWBj8‹E/›¼óØå¿;¶æÉ÷ äóuïŽOn±ãÞêgó»ö^óä›üõ õ¡N/,˜Ó¨‹?6>žÀI´f¿kÈæÉü·×Ï}pÁÝü·\ü_×óƒüqh~$õ:LQ؈ÇI.µé¯0ý¯ÇM?îmÁÏ÷·Ðø€Ô諾ü€«wù¾y5íu§vJö¾%°¬Âd1s)+ÈD wD¨S—ù¼TÓ‚&+c™ç6ð&EG^‰©¦©Áë¡vi⮈öäÄ2‹ß 4AHë&š°õæÔŸ \-<áPðfij]¨tÓŽx¢ÙD•zã0†[dKPX`˜iÛélÚ Xù;Ó&©F¤Í8c’üExs]ê·†¾š?¸ õ¼òWg.G–!»ŽZÞþqϤ1ˆ;2võÁL Åv'Ú¥zÛB€ÿN®u1ò 1èe2˜²"3L¡í‰ädìÿ¸¿ÖC}T3ª3ÕLS¥±|WêiO-›§Î$S›î}ÿ=ÑÌ<3eî‚ ÌL<ó—;Í-lÙ{2ýLw2¢œlOÄ¢’±b“E¹iŸ,H–ÿ_àD¿n8þ?’QÖ+&¤>4v#>sã‚îxñæ_ûêE}øG67p°|40Y_i~ziè”DŒÑʲŸ¢€é|R…Xdt…Í2Ïp+ÍÊ,(…9ƒz…To~'¿“Ãäêêw’r;¨š÷ü‚tF¡]½uvØäA`ŒIV0À?ù t©Jt­@º"]S]U¹H—í|rxºººÕöHVWç“Ôþžèbd¼¨\džÙàp-Q•ö[|‚Éó©¦ GΔg†hÖõp=«¤DTý½×ñ^,º’þ‡’Ìwp+ÿŠÝ¢…‰ŠzYáÁ*(p‚]îba@ƒ[¡ã+~@ ?þýÐ_|±wú‹„žo©N öã¸äÑØWÐÁ¨3ÿîï æ&BæbGÒàD LçùŠ+Zøû;ÁÉoa¯‹o¤‘TÑJƒþ_‡+Sµ¥‹|’ê`Ÿè'?©1òI{±<£™³PüïUâ¸þ@4"¥Åœ*q{X ûdáöûÒré™óøÛü#x„î¦a8ï¼+øó|•ú¿é3 õêFFwÆPx-¾®’> ]êhÌ~ „÷ˆó·õ ¬Å¶yXp¡ a€ÑO¶MÁ¤Á'Ϝ柳BÄ£þ\†b[•‡„Ê+§€Eé'“î½a)ÄûÍX{åÑ<Üëi„> O~ö¿3õ>oåWòo#@>ûôÇr,71†«ŽåPÁ1úLü3/mrôýÛJ–-¤áIXMWœÇÏM}žJòÁ2ú Âù± p=ãàÙ§‚gŸþdžE‘g¡Æ&yæ¡ú§G_¹vF?ˆ/½áÞI<|´ŠÇ¤Å0C0mTjTª_Ôæd*©3HɱñÆÍåŠÈ;§Ò㨛Àg„#þ'ó·¿½ïÿЈB|eC9VÛt~_äPúV|!o§ˆ~á·÷ò·»ç8yà\%V‘t› Û¤Oó¸`ÃÛÚˆØJ€>þ'ÇÈÓ‡ç‚D–'°¾ÞÊäBÔäÂÛO‘ç]S\NÞJ}¦<)˜àË\K¹p¬\’Z¬ß”:ȼõ¯·!Ä?3¹Œ¤.8yüŸ±ãOúN0ž¸NèëIý`Њh¤i9$tÚô]LjŠaîÂPLj@+…’£º: ³ÈÆ„r_3Ü·–ßl1âÝß@Á²Ú¡Cz× â=¸áv¬GQøèSô.1[ÿ±mdiÙˆñ¯^˜‡$ò° q=ñ¡ÎËê]2Ÿ*O âȬø©×£NêŽì·o|e“Ç@>Cä{}Öw7Ÿt|ŒÿSòíI*Ã5'/³Ÿ;uqº;y(àéëœE€ä¯ÂÙ¼Ïh‚ ’÷‚ RÁDÒßÈð!ôö¿ïW\ ‡rà»ËÀ›\Wž0y¯uò^Ú6V8k;î2fà‰¼Çâï’ØÉ‹ºº€î§þ º>ƒ‡×,ƒ‚ß¼;bÄ/~ÛÂ?zòÂWÇ,-¶ tw꘧å›qû†Ôt¤‡…ë 3Çj<\f KTi! å@¾XPðÝü‹·Ä°Ò|·ë{ÞnÔA¦Z+þïO,øÈDL¬3Ó–ÌDbZ;U ª€ ËÌ)’/rŠ”¢"Ê(¿åê”{KfDbšÖ9ý:4 SÁ„MGŸ>£&D×¥.AëÁZa3*l°bÒùÓ¦lœ0Ý4\‹&4¢Ïàqe®v†´˜¸1M•Ê[Ô 'ÝŒB I-›\E1æJanUÁð5|Ã}p<ÿ9¿Ž¾”ê+éT¥òЛ‡^Q–ÓïR®ÓR„€Öý ÕõÔüªõ¸£Â`±Q€‚N¥dèÒ(Ñ,·¨>Ï-në“å)Ð5$ŽÝ”}dâpÃÓ黇úô/xà þ½UFÐ,%÷ð©i€Fh§¦P n:CWŸÇň[ÀÄ»Ô=/ËKÝÔ=Ép‹‹„Ðý6tû1°®ž>ýo[ÅÏÔ©ò-‡|aU;Þ_9Oód†SË8n•X9¡Õ¶ãJãv‰²¼®lw¶Êˆº2ÂÅ0&^•æ;îVteêšGóSÃqü×üÖûÑ„Éû³/Ø?:¡ÇY?6¨ã/³7øUÿ¹’Ê3ÌÚu[É.ùt Iw Su™0Ï™9t]?!‡î‘óæ!_îÿ•¬ÀìGf¦/Ô]Ñ#u©‰?=+pº¨zE"ÆÀY­Õ@„®Å¥*$4èÅÊi@Úópk9”®´j¨ÿÂPOç¤æbùtº3u+‘N¨¬û{ú¢úBz€)0Á,¬¡(FÝ—VÛpÈ‘ç×þ@ ðwõ¸l.Tó“ùÏ!Á?½Lý ß §ìÍNM]O€pBØËÊ$âÆͺtÞ×Ìbé¢]i™ ‡äÊ­WCøˆÃw›Ú âr(oÛ´Õ‹k!&µmêx‘Ù)<7›Ø¹¢µÉ ¿ Øß˜¿À ´åXB], ¸U5áá0ˆ7ÀN:|ìX»'õÔÅâj-_*®äõ¬²•ýR@®ITºtŠ"R“tiÉl6`˜0QÏVÝ ßô-R`Ô¦}›ÚøÛ—KQõW?¼«4²5¢Ý%‰lè’"FEÓIÙú*k{Å╲ªSLµ4nQJf˜:y¶ 3,ÅÊ:h±Fê1Z!èÚaP(Þv¤žÚ×®uvºÈS»í¯PST ±¦¨ ©Mô÷Ë!Þ¶‰Õw¼ˆÔ ÛxOéËÎ0Hc€TP RYb“†<[E2I³þ4iÝÕ÷FÒa£ÅrQgõí{,zŽ…æu§앨’)ÆÙŒÆgÚ±&lš¹¢ºTÔ6§‰ƒÏR¤Yù¹ Gä$[%”Nm{Hbwœ’Æ|b.‡$¡äüÌû‚  $Äî…) A‡{Ö”Ðslîp”Mú'úठIºÆ¨‚9¼¥+YÑ4YÆÐ¬n²ˆ£…ÖTK}hiT¼pŒl´y®QÒnçÍ!Ñ\k„˜“Ú'öĖ㥠T7”øQž’#0„K’‘rG¢‡b`ŒÅpKH:FczñÙãòÑô‚⣋‹ÅÏúMb/¦¹)¶vŸB[äÌ¡Lç1œ¨“BP,µ’5¤™ —\ý”üW½0…·Û\ýçêûÄ yš†Šå€r²)¡0A,ën…& Ý‚påÃÂLjA  MFþ „k¯4|‚„€ {ɱAÊþtÂE™‹Á–ÉS°’Tgâ˜BÁSÃ3kGb$' KÇx$Ü.Ø`^ºŽæ¿ÙËßµ©>xpßÁƒ6Í6lõbŠÝ,P õ<éµå0¥£™¥Í‘Œ“åjþ[˜¾ʬžåûö“×È%üßIøQ’HŒðë¨ØØÐª 2ª Ù¥Éã5PˆGu@Èà !nRz‚DºoûÃm6x OâµÁ4R¼ÃÁ•|tV4{D%y ©Fäƒ&îŸ3J‰[§ªFT_  cÚ¤h®%‡¥4n!fG†4È—ÙGÛ{ìÞsÏ¿þæè#ÙKˆZ&F>ší§€ÉÔ³€&s@o9Âù ?Opªóùqq2ËÀêIɬ½oXœzCü!;ÎÙuλO u7‰¢â:ÐT÷b\$„á’Q± 0UæºÀMÙNyd’P°ËiSÙdwš-<üyà­V§ý÷à;ñDÿøÛ'v·¿úãtÇ}i³évRŠF‚l?®€™ú•”„(v_i±ïŠ‹05NFߌnj¼ò„Ê®´°”Ø= :ÿ_6ªv¯‡“•‹Ä–UGo½'€ùÑp¨…ET¥l‚Ø„û8n+Ömy‡9îÌžm4PÈp¡03û©ªÌ#¬.Ð@¥ê³±2YaªfJ”nžKÄì¢ L<ú>ƒ™ª=á^½ +“3ÊŒ¾¹R o4ƒÍeþC,ÜdJíšî§º‘¤ ŽÛ¨'^žö-d)ÁÊÁ}J×÷ Žkö]î‰Vx ² Gd—ë´bDmߦ!sª¦ìaîµ{â½CÙ……´ ÏUÕ¯º(äÊs7L¯?6Ü?¨¹Šccteð˜HÞ€>Ç¡Œ1A°öÁÚr«ÉÚ:š Pþd’‡u 8ïX,T* -F²ÌºøLž/I³Pª`^#ÒÃsF C@SpOÏ ƒHŸ‚ÞåÈB0xÓV;8V|l5þ§+‚V22{cÊÔeÃWæ«ùBGÈÊ–_:Ìçñ]î=.Ôo}iŸú|=à ùQV0òC ÊI‡q6NY¢PŸ.3ki@“YŽ£32â809Üi åº)µš^­ïÝ+@ÉuA¦úHx”4 ½ÆV5(à’‹å6‘ªY/ì¦Zë,ŒGz A·Š#ÁšI°Š³A%ŠŽ{5¦|´êuÖØOi ÓAvÿ¬¦£û° ˆ<îÃ<ëLR©¿…j°Cy‘“à4ƒ• )6rµ';›P\D™]P)ÓŹW Z‘žåbç5"'ÌH1½™­Yï+‰øÜ‘ÜâÑ'Tõë5«üøq©oì5 â-òÅ‚¹ûTõ;ið9³Êë6µe¢Uòƒ”Ñ¢B)Vaî Û–ö7'èn^È™o¥n¬‹”ëFŒZ}”N}3îøòY½úU0º87âöEJ¨Ï>QוÏ:gðIýªìË Æ|EÞˆu¶¦6fE¤7–¡ áB¬ÉMºU&á¸d”¡FR# ­ÐUŠŸÎÂwB(¸‡O))uz—ôzUˆè•|ë@‡V(Í‘õ…8™sU²õƒ©N}Bu¿^3+O8ªåÝ6µQZÔ'â)ð—F²t“½I‘ó»u¿UMgÏÔà8x£wiqØñ•x’ôˆ|åù ²’<ª¨…”(L-†'“¥èWè5ÌjE»•vµâââÞŵ…á^1LQ ¸/ñ.·Ce4Žy,„†z+p=ûE_|ðnËÙ3ªöêW}ÂèiNýÀè>VÏŠˆÒ-žHŸ"œªûfT4ºiU¿êþS.::iö¼‘ô”ø"Yîpqiodȇ” ê/H–°©di B²¦hBQâB¥·ì¿ôÄU©ò;ß>޾úàµKø=!˜²äZzŸÊ~]ÉC°óÐßO¼l?²[9XÝL¼Â·6 *R榨¾„ƒY(‡Ø º¥Êô›yv—ÉhÆšŽæ±TÍØ±¼ÍXlÅÜX¹í²ÏŸ‡Ö¡Ú-Èæ~E¥F!;?œ ™Cö¾ˆo§Ë,¹ìó3÷)—Ø»VÇ,$Y,~ƒÕeÄ‹JG@$É^¥-ݪŸÅV/a”Õ¡×én$ .Þ—ºIÛuh¡Q $”–tJÁ㦄 8¥ *Ĥ©_\ •ÛMغ9y7lJmß”ÌÙ¤\Ò1+­ûz”¥AYM¼¨Çt  ¢LÙSá@HvKS½e íâ"Á•ô<Ù–\·ÖíëØeî» ù=lÞ¿É›„ìò¶}·Û¼›SçJÞ ‚c••,RŒ‚²0‚2\Z(îéT\² „±MÄ,±ALÉ‹‡kL¢¡Þ>ùDâõv`.6¨mSɰ>gîyøÜá-¯5:®ï1±X‰¤’žÕ¶)–3se¿sÏ~ÓüùcšKËË'Eñ#BEè!Œ²T”1,Z Ú«ÖäWš|”Á¦€n RB#<ü¾mSQ¼â¬³Ê›_k~îÃ{Îì3, >Úï]RÜgÌüù7 ?÷Ü~+gæÄL]ñ ¡~¿AÁ`–,aÌÆ”ÏxH–i¥,›P>¡ŠBO1µ ËìY†­BÏïæ£Ù‚AË܉¥•ñƒ”“] qK#¯ÌÜÂçÛJyJjB7*K‰J²E¦Æ T,à¬ú¥Tœ N¥„©Te«­Ø®SµÌx‚nèé‘¡Qt¤=‰§±¬Q–t(+àFÍ¥+:­q Ú‡e§ÒFÜ¢@±ÌO(©)I QPÕf¨ðr¢(R2UçË )ˆ(>¡ÙÐÓ#fQ‡°Dl ”*Ç;üöQi‡zLœùù6Æ—Ú7Ùö¨ ¬ãZ›DYô튌ÅJøŠuª°Ån6,$T…®vöãÕLŽš(2>… Ý|Œ•Ï(À¢Â`é ½1-ˆjˆMQ+j½Î]P‰dpXháä¡ÓNybâ7ÞÙ¯ï€Úê“Æ3g`/Ÿw€d{h@ߢi““OTOo½þý¿þúš¾§h^][3hÎÀSU#³’²3Æž²iä’'''gdRY™MV)ƒ4• `«MÝæ©qRi|ªlÈüئ²¨ ƒ‡NTVÉÀ:Óø©›ôÖÙáûM†ˆA7žþDrò´¢¾ýÃë^_¯sÎwRu퀾ýî¼ñƉOTÏHNÞ»xä¦SÆJ£i2kdUâÔsÕÔ®nqZßk~ý×÷¯oŽb<)Åu _†I‡’d œaf4¤ZÓüvè~Ú&mo–øGov¶£©ÝYòÆQÚ0ã)“ ÞÃÒ|ß™G% žFY¦­SzÙ†i¹oíS&Ù6#N}Ýú>’ùý@Ú¶m¶2Ú‡îÏd†¶M2“‰‡±M fX Лq|ØÉ ASAZ§o˜6­ÒH Nó·;Œ½ð;i^Û,E<:±£ ¥@»röK³ñCV·ZÇ*e;ÑŒƒº*G‡¡LGpL'X/=”Gdk¬˜ä¥‡Ì}&l;©í’Ü f©5¥(ÌLØÒc¬ <«Z]0†—Ý3OØM²øXmxhÙ™&Ý„‹£ƒ›(3< BA„kˆˆ]át>´F@µmÔÜ :^K›:éHÛåàIËv݃¡ÕpÑèÙК6¯:L®ZÚ¾ú™etuWF×®l$4'ÛAsÁü®ËÃNýŸfˆÓT¿ÕÁú©E;¿Úé‚a°!­éD«6â1#³% y0Ló Úd°!€¿LÜy§Á„M–ãÊwJ r½°I`Ay”Q˜_À¢h÷†r"KpÞšiõºÌß Ñû•’X‹b=Š“Ù“=G¢‚”"jÄ¢ÎÓ2¬»»ÐÌ0¬›]]­K³úæóŒN¾ëŽ´Ó‡ÝÃwÞé$M¼ £¦Žˆ5ÎQw}ØùeU¤hª¶ûؘcwÞ™ TÉú¸ÁúNÚB?.¬™Äm/HÔÖ(â;Guµ.ºóÑ1怳Æ'—ÓcŒ±<&åas“ÉŽÈ/xF9†+ “x€Ð, h™ð« c¦˜•ª‡Sì‹eÀ/L䢂äü.Ì~H¬eAÙÅЇjÛ- †Ð…3S·L ‘UR†û,‡ÎŸ™ºiÍÅívuêS§òKeùTŒ1åÿc dh`uÚgÉ&Ö"FYžL.cÏœ_ŠN>FØ N>˜÷ƒÖG ùE°Ã,§HáötΞ4ÙŸ®–ÎO«hdBê–™tá„Ôg{: úÄ@4DP0å ÖaçbçÅV1“†:É#’-Ï(r†ÌÀ”$|DäýYb­'öÉtÂ'©œ"è•ù˜"¡@àôŠGįÁ(v§i†·WSe‚1uu¸LEШ ÛvX¦ÛT‰‘rrr"9‘H4Œ‰‘2ý¨ ©Z0Gòn´– ¾hL’L¦®I&M™3/ <)þêð­Š ƒšBêð± ü(«¦€X¿¢bÄÚ ˜µ[Ç ØX!_Ò%bÖDH¡ã#ÌÁŒËW”éÁ 9ãÒ)S6ß™2OŃ÷¤ÜÜ܂܂pÆÃX>Ñ6`9ªí ›l.5sQãAþÜT¸˜¹°q¢=™Ïµ9uð%˯X¾cèbäÔ‚QÌF4ŠJ²01ÏÃ(’BtêÕt-7›¢ “(£~êpbr¦m™×ía::4éN‡¦ââ¼z*îhä­¶|ÆÌÄò9Hïëød6;µcël–—šl®%Ëg‹Ÿ¶LÐYÀ¿ÌXìU€NGÀ÷¸Çœ.cRnÂw¹Í¥&³¼Ù[Ù©³;>¡÷™ÜÅW.„ë¦ß¥Q¥ žQF„¸žÕ½ ^Ȩ|TäNÃZbÕi#ü]­{Ÿœ¸jÌÚ'÷®=Ï6vLýã§îÛðÆ:tC¸§>¢ŠîN^F“!µ{A„lî鼺e×8oíÞ'׎Y5ñɽ­wÙö ¹[’qzªÉ“ˆ@·YÅÐrJ§&óT†Ÿ#»Cï@AuTñ»R¼$áÏ<#ÀÙ³ÅË [šöíÛG˜½J°Èr‡ß¸=¸lmXFºÞ0Ž0[ ‹Æ£uøŠ8½Êá]x_ïògÎÙsÖžLŠÑÝ"ÃÕ –c ß Áæøžw°˜uŸ-‚é7H®y&3IN»€gÒ̈n°ZDTU9Éá°5²²Z˜0ÄjeÐß}ﲎdÊ$A$ÙožÄö¢ä(—âµíJYRzΠ¦’ÍXî×rÁÖu=KÞ+²^i~oÑ<Ä1%_]µpSƒÛ?‚[áöOøF(šÿèî¹ô[¸ŠŸœrÏadçΑËܥʋìu jp¢ÁT`4 ÐZ§$µ´ÅnY¦Å.kåÌšx7­–ΦlpÊCÀÕü$p ŒÀ©°ñc>›Ï†Û™·{^}Ç<¥QÖbØFòÑ&…>­l“ê Ñ«$ã3ÎHïRië]e¬„dˆQtïG°ñ~ì£ówÏÝ WÑoSn~2 `$E0‹*8°5Í/ÎY ~¡*8-¶«2âEe° ¡ã©Cz¾êQÕ€²D‚ØIÂmóCëîy<â ßC"ä”rª YÔ9¼tt9Dñ’ÀfDu“U.PÝêf¸ÝÊ|Á㑙ϑÍ]Iú½^oÄA™(³9#Gˆ`PeCÜ%ñȾçŒqòBÇ~ºûU)/C¼ƒƒ…Çø's–Ò»w§–.%Ì|–@ÿœ| UMô«ÐÜdµKüÿÏö€ªSé§¼ÌÛ‰€ù$mÃaE" ü~¯×ñ›É;¼YÞ,IŒÏ›ï$‹Å zâÄ4?º>yuw'jØA YJ‡¤žíLP€ä‘»ŒúfSäÔ4£5;Dˆ3›}/Óu›¢Ö{ŠÇ«2'›»}:Q›ñ ‹l&.͵™t}x¶`APôd0/˜‡¡uÞ€W¦Ô‘qm…ú3 û2&»5àà,¿vîËtþ«ó¯íÔ©ìškD—®¢Û®é8±rH”LJL¨0![²Ã£QaM:Ñm7Ùìì,,!4 zs¼9–>éŽÝ„XFÅÊBŸ0ìõéÓOÍO?kÔÒ¹sÅЃ°\© ßBDßJ}Èæs v­KÖ[Dý$LJÉ _P«&jLý¦‹Ž¨ó»¨NGê¡GÏ*Er[hg‘U>pg¹7gCV–6ŸÈø³B£Ëuùªgó‘¾™xØ/‘Í]¾ƒÝZ‰dgGJ#² Nf‡³QH—«XŽáŠ'1m™éD€1|DWÈ21ôâMǵP‡IæjŽàÖ'Å]3ŸR@¥PÄ6U˱Ɇàè³FbêYsiµÏ³àórŸh{E¬“ó^IÝ’^+Q)‰º9?®6!%úªèþ¿Inål TH—— ®Îøƒž8é‰!“¸1sDnÈ4Ü`©œ!;p µb̽¶Ó!ð£U>0E:´Ð  $‹‘M e– RÒbd™,ÜD°n“8 ˆ¦â§X8%¿‘Ôð‰ÚâßUY6ÌžÙ6Þ?y²¿µmj¯õG•Ç]WÙ‚`¿å-ÍËê÷ Z1.6¸xx_GÈ_ÙªžKòÉ ‰Š`@ô ú9)‘0¥,Õ˜yQJ@•å`Ì’>Eb Û¥{,±EaÉ<;R ·ºŠðéGeGð±^ŸìI¿*%üL©SMÚEYŠêÒ_£úЩ1•JSçÎm†ÿôª”ÚZw§ a¹’i¾Xz—6vhþü>þ<]ˆ´!¥6 7.…6ÜùÆ0¿ 3‚‚ÄO¹ A ô"XšLá‡_Ž“ÈlÛM|$ÝA½*E¼ýPÓáskJWÜÃ6D¿²: (€›åh] ¨`^“`“AÏ#È6þ•2îðƒ‰l•R+­«aÁª¨ƒmObÇ'y-ÜÅ—ÃvÌå@ŽþÊ9ìCâ"ÓÔ$Œ¬YERZe(RÓ ™ ™\˜( èf©LÍüu©éTË,ÄÙØT6ý’|c¯ß§ ˜™û×ü×ȲM™§œM4á¤1ô ª„&…(@vjéÕtþ£#þü('62ð§vJ|¥œ£ˆ†VÙq¦Ûc·© *`ýS•ôͰo¢·ÇʟçÏK žÜ™Æ •X ÝÁ¿¤sG<šºfÒyŽLí( ƒ|?¸Yù{—¸ÍFÁ‡9ÕŠª¨´9õ9 o7¼Îkø·ôª¹_=±pá2‚¡V¹XA*È †~.¢6sŠéã· r1kÖ†ñCr~Ægh©¦l³ñYZO€ÂäÙ2q~4íÖIT™ˆ¥¹½)f¼‹”®æû (äFeNyÐhªª°¨è²ÔÂÊ¢¢¢ÒrzÓe⽪ªúøu=àå¯BmaQUuÑ–Âü^½ò‹.+ªÁ'ù«Yìp$ŽÈn{Xt[åOè6œml\ÊO¿’CýåÁPhöY?hS~¥„Zk lfÇë¬#ÿéi·ä÷ñûˆBá å—ŠfÓ¢´s“šB™l/°aL‘`«7Ârû±ŽHb× ™/6¿}a<¸ãƒ¾}Ÿ3çÚ 5Ðç>¾›? ïðW¡ñ"Š`ÏÁ/qÉ!aÄÁ9°Ž„C®‡Ü¨è5'ãøûP¼ù6§üµµKfÌË+p¸sñóæ¿A³ø ¸Óx€ä6v“ò’`E4Fý€åL¢Çšuü„7vA ÞÅßg¿€Áü¹úÖ]FÞaï*˜]6ö¦âjgœúÄóØNîǼœ³ïÊô©P TÃe4· v¯K}·ÓHoiKa=Ÿ’w”qØ.e £¸nâw#øM€›1¦¦ÚŸ—ß™4÷ùÔgm<™IÄQ!k¬4CÆÑî‚â]b-}‡~•òóç:áJä X¢:à†¢ ~b—µÑÈó©v—þ¼M^'‡÷býZ|µ,Qlœ){ÞŸ¤Ðt 4!`ú4íâ첩þqì=ÓA‡ë`7bA¿iƒGÚGÒ¸X[î=йgö¼ïH!®A¢{pºñ±µ]8ãvíJsÖPWÀPËXdoÐ\ô-9 Y¼ c#Èfx 'XMÉBX£œªäf¸<¨ˆ³÷,EÁ,+X ×­TFá>þ[%pèì˜ÔÏ~OŸéü·lúûßJ΂ÊL¥B¶I%çeF Ø0rÊ­FíáO°7;*Y ?ù ú$øü Hü]û¯¢RöŸáÈkô¡á\Øè€bÂ¥_§¡\#ဩ ±ó”}?e)•2 ]ÆÛ!‚Iáiö‚‡xH¢¾J N~ÊÞˆk–ªèô½Ô1ô®“pq> >„ë>Z²ä#BÉ28Q9I‰,&rÂï ¨J6ªË„¬’-¶ÃŠ YEQAYNTBÔDUœŠ¶ßò´ÌÒÓC(Ά,%“D Å©ø¢ ¸ïCýÿ¢aXº e8Êw}µK"Ü_9‰}øÿÂO „uø^ï‚p#Ù­ü’u/ ’ÓaTŒ¹ÂA7CiQL´ã>i¬—ý¬—u‹5šk4€‡bMu¡ˆ@œé1ñCk³S©?ÐKZ~‘úþZ{æâÔ[èÌÅ0®ðrDÿª­|7|jῈüUYÍî°ðWh–‹2ÜH…‰ÀÄÙŽ¨ýû\!‰&×™ïß#,ªŒÆÄZ^ÿ¼•½Þ¹|ôÏøœ9©×Ï£5sàx¨?+zÿà,þô9´íåm„’ äMåheª§ÐÊgr?@)QÙ„.h0Uú¯‹â¬ÈA ƒjÙñ¨{6vz,?µ^˜.òƒñmÓ!ÎÿÈá˜~-ì_Ç·]Ë·Q­ã—®#è½I9[)¸4‘í£T10lO]:Y.I%×9ü¨˜\EÕí$J™ÑÙ'MÔXž3 îIÌ-Óøš9ð3þÏ£y¿ÚÈO¿…Ÿ?ƒÖü„óUu0Yx(ŸA|$L®HDÐnðàY6ÌhzTG3iË`2Ò6À¦­‡G~̈AÚ‚‚¶1äñp\íè2´ïÓÝ©›*W¯¦ «*±×’ü©gÌ ‹àº>¿ýVtœèLxDtÜž=Øqµ$¥Ü¦ ´HÔ(ÁCÀM‘N“ÄÈ 1ú$12»¯ •ØM­>í´*zBÕѰ¦Šo9Nç/¤®ž5‹žkšÔg"ß"þÃhZÿøãë +Ér)"gáRJ™[ÎÌ<§ægª&ìâÄf’çqÔ5Æϧ#==Í8(sÐ'A‘jWܪŽEÌ¢[tŠÝc ûdÁǰ†oáѲsç·èäÝÍÉÖ ã'>ò»)c ‡ç'HZ³qè¹UUÓ›“»—.Úý¿˜{ð&ެgf‹äU±z±eu¹H¸Û’eƒ-lc¦0˜®¸ÐcB'ÎÁùBH‚é×ûA®÷~©Ôë…|×kz½;ãå³+YÖÙ ˜ÿJöóZ«Ùћצ¿yoúŒ©( -%Ýʘ©»3É » ùP ¡RtwñÁhxßF/C¼(¾ðGñ#„ÔEâGÈ+òÞû‘¼ƒ‹Å–â…idŠùø2 Å×$¦uµ±HrDD¦*%_+ˆÁ„‘tÅh¬{Kjû\žÙÉ;÷æÀu„ÑÚþþµ×žÄP\ ý#ý5çöµ¶îÃ÷¬¥kQ›ÑW`m㌴»i’v7%Ûb½Næ‘c<²eª“Öt^xå2˜Ìú1=ÞF+S¦àýxÑÐ#Ä$׿/=x±äâƒ<óUàÈp‘øQqŸ—§i£á~„ARgARGP€î)»s“@Ë ÀÀj$ÌTx@D>ÈÐ?¢’ú ¾|ÉIðÔm:¦B¡^4“Ò±æÓ/ ú„÷G£Žéþ¹Q¼¨´¹¹ô-“kZmMä½½¼«P¯âß*)9¹mÖÄGgî|výÇ–dtÉ&§©Èf,s{H„þ[ Þ[AVCÐÔÙÀ kelŠüÊ·°NðU¹k:ÝÊy9K‹Üóªº>™÷¤¶È=tœ¼Z±(³É•ËBZ§¶|QL\ß¶;¬rÚýÎÊýMúºùÎZGÍû·wVµN\Vª-Føúqô$W=N‡‹Ì°­ÿjjÆîÜ“îpQ†z«Ž8™þÁ=ìžÑŽ8SpÇïM‘I·–1gº†¿ÍàM1µV;Ž¯ê´­ˆìFi~±—¯7 §¼hS<ñº±]f'q´†»ÅÏÒ30 >ŸÝÇŽ][5¾ïŠ»®^½ºî¸>ÿüó‹à^(—Ò‘“Š:Õ;o(¾&ÔŠÚÐltº-FËÐ*Ô…Ö uhÚ‚Þˆ½*( ¯Ê" 5¯ˆë´D­Ï&pÝb"«™mcì)ly¹Äî~{ÝÄéó—ßéŠæQ ýxU)«®$åá²òx"Æ#K•JÉ †:K‹I(ÔÓV[C` ³.J"‘u‘[úîÞ¼iã†õëÖÞuçšÞžî®;â«W­\±|ÙÒÎ%‹;-¼}AûüÛæÍ3{Ö̰É4µµeJsSãäØ-ŠvbJ´Òíçsåˆgß8ûÜsøøùóxàÂ…¡Ã—.áð|ìüùÁk7N¿pþ¼‡{Õ¥KÏŠâΗӯWèõÎiHHèÖ](yP¡bTŽªQMB“qIÛÙhV(1oÄRøy¯ÁÃè bZtæ¸ÏDX»|Ž4`ó3–<œë°äÆ ŒCƒU‚C×ãìlÉw»bm[~!C•Ÿ§­ÈM:µj"ëh–« !‚3—^4ËëëkkÃኊ’’`0?ßëu:ë'׃zÕNª GàbÕ d%å% fÁâ (Z~Q>¨š7àeszœ ncj[´ Ñûô©Ûc†[O]¬{è ½3<•/|qþމÐJà  sl®‘_¸Š>^ÅA ñÖG®Â¥h¡ÿŧá~æy¸Ä§á~6™†0⯒}–ݬ²;„¤àS›hä Y±•Iºq"ÕU†HÂ+¤Ál"t;¸§åô̇NŸ~hæé––³9uê‘Y'æ‡Bs°ùSŸÁúy¡PhÂ<ñÍÏ}N|sÞ„"#J ÑE]É!9$¯B #—Ḛ̈Û0òyl!{H.Ÿ‡ò `ÝV_Up¥­7YáÕ˜8µnXÿ›šš ë7¬_ÿ›hdÃúŒ8~ ¶sý+—OŽÅöˆƒÒ"×_¸þûö02CE˜Óv6@Í’Gã.›%K.˜.x¹“bxIaÆhµPâ,ÅÖb Όͣ‰KÒV0ìKv™žtFºf˜wì86ïĬ÷Ÿ8þ?q¦aïÄ­{÷n¸·!z_SßûößÝtßÚPYÖ}ýëX×QZÜ!¾òõ¯‹¯t,Þ“ïmþÍ¡C¿iöæ{óýøã¿nÌ÷¦ÉÅO¶ Ñ¤aä÷Ùm²H¸Ì"IÇ{lYDwÕß³cÇ=õ»¢ŰÝçkþÅþý¿höùÒ°óJØ1€#9ÈØÀÎë¹ìï°ÿ±±qÛ¶mëöí[·mˈÛÞÚrñá‡/¶´¦!æ‘Õy4f6Pg›Ç;n7Vg¬£ŠL19üþ7·m£fDŽtcûVŠ`ºLß©®¹œ6¿ÝŸBî‚õÈZ˜Y°×Jwo®ßuÉüI#HãšëŸdG55CF©Æ'‡ŽîÓQaZOÍ8rôè‘§Zz;Öò“Xß £¢`’AÌ7áv –+•´ „kœ8Ô6s`à0»ca‡øÖ§>»ûÞŽŽô¦‰îÝ#†%,Cvñ˜p2ÛXVÆŽ•ØFG©FJqŒÓ˵øFü­ð±y÷M0¿èó5þä ÄË4œï¨ŒNxѼ±¼}fMl½‘¾(é 3§h:ëb5EÂbz°IŽžwGнƒÀ )o ‹ ‹!‚F½@FSy£.@G·¡ô'cÔÓ§1qnÞ²íÍ7Ë— ýVÝ{è$³´óÖÌüm=€íP•.åÐÕ-î*%|NÞõJIATF5¥ÌF¨ÙÁ"ÉÍ'bØxZÛD¦ßMT±ðÈ6_K¨}(¼µŽI¶8\»oûÖúÝц‡ºUĹQœoùeçÒŒTíæ¶ú>ºÿ—S|¾²îôm|qwX3JH@QaÁHŠà6žÖOpåÙm7¡DžFê½ô: £’ÚNxƤfq ½‡NMŽl‹½ñFçÒ‹—ÿÒ*Þ–YÅNoùHW™7p@ü“ø§a&¼óå4JŠP)U·×;ª[0HÕ-X, °"{Ñͨ.ñèS5Sc ýص¤þÄ¡^õÐo¯èÛ¹ äDœ–_-_œYéJ»?ÜG|êHÓH2"Ww†5âlħu4PP M¡NϦLŽM¬««6¯Û•›£¥>]25óN‡Íc÷ÜÔJO;!½…Z{äÃZ·8žR¬Ð˽ºYÎQƒ˜±éÎ+Ïnÿn HÕ‚!/Ï$>aÌ7*uJøƒìêºö÷•+3³‚a~İ×JXÖ&`ž *³tZ%VT\wíðOePÝñ…/ìà·=ñÄ6¤fPÒ#;ò“ªa²ÒJ½ÝX4,ƒÑ”ɵÑêŠòâ¢BŸËi7ëµYœ‘Á-±IŒÕ“a1aȪ}GC}sc}kC«ÏÃÑ>„Í”1TRºÕMwÃ#À ,{qfx€-²7SœxçIäå¼ðµ²žñ•6¸ «jk«Ä»ƒ]‘¹Áénògw[áœHWpðó:Ý µšY£VŸÐé®í€¤½îéAüíÂVÿÞòåñbâ§…ml—Ù yÚkÛÝX'O²>àn¯³&··O–²HÙè7q¼X ¬µe“g»Òß0§Õ–RÔDk ┘%Û¥À„ÿW%*/£jT-k*o øl¥öRY™²2+SÆAl$é‘r'mKo4Vî£+â«W®\_±¼kIoï’®›D¦:ÊΚ蹵kÏEkj*?Ó×÷™ÊĦuý h.õ§8bã»TX!OlW&ÊåI,VÒv_EowE?_øïá¬Ysxê½UÝ‘òÞCGºA£˜ë/ˆ(Ql.èÔ½òЉ;U:µ{]F½ë´ñIzaÊTÒ"÷Ð-„Ê‚˜$¦èPž$ –ç³€>!2ú„ºWÊÓçå›o;ÅÛ7a_%Å4©/€iN ÜÇî[OÛ Úbð…ƒ/ŒÄꑟ4ø|Rs€ˆ,bBЉu²hl €Øetyº§ QWˆi{ú^bÄv¥ÞRQ¤½`jŠ˜ò£ËÎÖ ZK¶EoôA¶S©ã$_ß~êƒV½˜nÊÁå³L?vWäówlÛösùÍmÄ$ž>[\x/|ÐQ)0“šîðÄ$5Yzz¤!´ Rc.˜:!‰PKÏÊéRSÂê}0D–ì6‡Ë9ó›_ÿæÎ͘˜ð²sPÄ9ñ fúï½·á[û’«BÀ" µ«bðpD„ÛQj9ƒÆ#רxY°%9çªL©`nÕ@,Oº¦™-^<³é®èæêªMÑž{ΈŽÌ–Q{žžäv¹Üußßt.DGˆIšÈ Ès0XÚëJuÉÁ Ao¤¤a ­Ò*MQp—:Ž·mëÛÔ,?»p{kó…Çè´0M•€†Zjމ¼P%¡%cÆÌŒë}3OM9ø~b¢ÓéíesÅ·¾œfWh:±å’Ád”c Cóœ Ò„³oçÛ8,‰ïó×Ñ¢Jh:¨tþÂJ€®&™ZÉ=~H¼¸}W,OP•xìBs«$«'@úÒîód%Y5t‚˜ÈÊ¡“C'ù?꾺ãhxÛ‚‚$¨B‰$W¹ª¸éOð'¢-CÒÝ–Q‰išÔ£\Ò+Ó½¸¦çÉþZŠ^Š\¿¦çú>¹¥÷Þ{/"pùfvînoy8QTåGðPnwg§ïÞÝÌn¤VŠ¥ïK9À¬éú5ù„÷:ïõü­A ±QŸyÕÒ…–nÐR傦O{«"ÍžªÄ)¿‘Èqî7Ã$­ ™Zî­öV›fÉ9®CyØsí:Dl´@L“n €œÞê( HZ˲¦²äšD2š¬–PÿV>ÊG½[mX‚g½[™bËÿ±Gý‹º!|ZÓRnjm!êäã3ÒɸNFðÍ\qåÕêp‰Ÿ| ~K¯^ŒÎË1½R}ž„bÅ…çy“«±Ì«õ7z%u…{cLÌ7ò?ò?yÍüÍÞ½7ò7‹~µlò›“_T'ÈSjÏÕþÕˆýN=-O‡–]åŽXä¤ÿ´^gÊ[¼Ýµ1yz·ü7­Rè*’5ƧTJowQЀ–å‹2vùÕ/ü/9Î7y…Î×þc§|¯º‰B!NŒ>¾nÚP¾V¬«?¨nš\³odå¡«Ÿ<ë€hB(ËrÅœ†¤.¬-6äC€<‡Ê €bF?{6?¹·KËÍ6ý‰\þï£Hò¸ü^­‡qÑUÿú±ìd[ 1€3%0ðq»Ñ%ž©q»ìÜ19PŰú±~x}!=¼nµá?ÁnanÄìloû«›å  Sѵ]œP_Ì_µƒñüª˸Bö=Á²JRÿ’ؽ] ïØó5„*†Åƒ6!ñGè†<Àvžø¼Ý"õ¾#íbùöúJÙ©Þ¹g7¢y—eoñ»ŒmûìŒ9ÁÓ»p´’[–ç'o'})¦,Úk”æÝk–#Ó¿ˆüQïÆ£´ c`}ŠAý¶2NP Ñü'\cÜð^pd~4bU؉ú;õàC÷u)Å.%~"[¬³.·Ï‘9ú“ëG1¤Od»¦o2%;5u¾}Pï7Sïy{³@&b$‚¢BR«rWÂöƒvHÈ~b=(¼á»xÐÆÍì,ÃÍÅq3­ n {Ú¸‘zªAjñ—þ@j²soþT"ÃÕß‘ÛÄi ff±x…™Æâ€ a^–}˜º[I%£PÊ2¶ÆU,“›J¹G/6PG54¿U‰Ï|׎;ä·€„ÈK´dÙ[“ÒRSÅæp£ v!¨‚),$·,/PܨC´XÈ—Qâx¼Ê»¿ð-‡º‘ù²Ÿ°ýȰ5Ôó\ư Øä° ðÍöØ’¼ÆÐmˆEN_ؕپ\·z±ùô²Žz録^ëÇôÕÍS‰kºÙ=â+"Ý$=Ë™ ¥áèݬþ…äÑmòg@Ê.m¶Ž öQç —¤J£S©0=¤rIr½Æxb5®´¼²9ýGk$‘÷Cy;ÛAdš¬í°(Ì  oòù­ÉW¡ôäóèz‘©'*ˆÈÚ/Vrc°t,Öœ,Äk–ó‚k« O3n{™P›H“À‚AŒòƒ€Pþ*A/Ñ2ûáú´¾I1aÊ¥Ô©ÅèØ”\×bŸâAžGBft‚¤‰Ð?ßå‹;ËÆ‰‹çF} '¥³9aªÐiÊõ”Aq­¦knÛI.GÓXívݰ³rOP(5òÅÉ »’Pì’Ü•"¶««PÿäZõ)¦6J«ä—Ó lVh\ÐèGibƒ¼j4®Ì×fóó‡˜ͦâÒaÊ5JLÊQðHÓ4¶ÑB{%‘ÿ¬¯Ãè?÷„3`{ 0K?+G*ìÙW±‚(f `Üa uXl  I@Gt"àOjKÁõÔNÖ6I„Èè@€9&ÓrŒmÚ…ˆ½),$·,/ ‰Ý@¬˜ñ8—ïÐtùû¼­¾ùÒ$°æ»kÛ‚ ÅÞ ®nº9᪡ӔãiÐUº|˜®y#ÆÒ; )ž©Ý¦ñ¬1õñ=»w8ßþ¼âğP´—Ÿåg”˜0åJéÄqÕn©¦øbÏ$y}}ðIœl’èq d‹ÃJ„K­ì5ħ2J=‡÷½}*嘙}NuEk`Á5]O"q¬Œ8±YV_‰Ó¹`´Ðš‰¸û¼“zH¼¸7/àŽÃ¥ ì’b(ÄŠðM¨ SCï¸R\ u¥XC „W\2RÏ*ù{µ»6Üô>ç3Ô‹=N“â:..w\_»SÆl’*Á8mÌgzHå’1£ÆÕÓÆ :l£BgVè´ÒãS¸×PRDº¹"Àš{$H "úÊ—c¶‹¸UŒêN t'ƒÁÜtÊÊòtz,p¬Ø¨ ÍnLžA,Kuà·›ž°j‚ÎÀGʽÚ¤\Í¿¶)‘–ÍÙnK…-­‰ª²÷¥=F¥#Z$B³LEœW¸º¢TLnžº~Ià¼"ðü5þÅêtÍ ÿ(3séX YŸ¹|βM„à|ŸÉB0Çñ·ËÀýQ™xÖ¹tŽ9füßK£8Júåû†íÀÍàz𮩅q ©âÂoŒ”‹ð¶ÄDƒÍ÷y«s’¨ñb¹7Ì…fnWdY™:Ï1ŽáuMxÁÓßì;X…3»CêÌ\€¨¿ƒ²l÷ï]Ð5 Àš} Ž÷·$×»9 ‹qÒÂÍì‹æðj£ðG´¤ö4‡‡|ç¯Òl¯Ý†· ¨ªöì a33Hdif_¦O·è¦il£h¨™·¬ð§-Áì˺B‹íÝc®Ð¤šHrbѱ7sW:$ ñ“ç·¯<™äÔ¾ò´ozjý·{1‚—×k±ËÎPê¡Ï̳ú‹rRƒÏ,å_âR¬ùXÊ÷’ñ £S¡°wåňpJ 4¬"RÚΉøÁ¦l·V^c¬F‰}« 49°ÜF´}–h[Lƒ8׃8Os–¡+ëÌ•,“Ë…ÉÕF+aµÂ¾@+/eJfÔÀ^ê©L"Éø2n3z_ÅL«ýû+þPjžÍô9ƒyf 3‚spoðÇŸ(˜ûû¦Ã,+°~Ý¡ I¿†L~]4$}ˆBÒ»ˆ®]æqO³üÜæBKgJ±,oÒÁa=À×bQ¿CîCn%¬žÒ‘k_Á¯ð>ÅÅ<>è}êG?ªÿôG?:~váÏúϼOÿHtÁéŸý(öôìrÂpA°Ñ¸Ûˆ5K5 Xcji¹QÍU ÷«óÙ”Š° ?CV½\ίý$ä—|}í'¬9ÄÉÑ{/™|©cýl©ç ËsšyS ÏÎiÊÍåsr¼µmNëP'oË·· Ìïêùó ²=ß~MÊUµ%íH 4T2ÈÙ«è—¦è\]C0> OBy7xØ"’¾äÛ¯ô»ÂOa¦ÓB½ANG)’‹TZÕ8aMò ¦ûÄà8ßqç}æ3üîËžÙ:ù™ñquÁÖèçàòo—m}¦>LÕÔMpfþžøð=÷Üó‘Ï|æ3¾l|ü²Ýxйë­dµÏ»OkæþUr §¨«¡9<ÅÒ™Tz¨•gX즜ÃÌNQ˜¶´8ÏÀG•)‹Ê´lnN§•Š.ý×ÜÚ [c¥³iÐ4_Ñr¾¢áºÑΔÃ<ñû¾} þÉŸ ûºëàÿ[ÃÃßMàÅ;ˆ‹çf[3sR-Ns† ¤)í*¥ùDÆèÒmjkC1?Áì§Á€Q´òAßæëFøºa~ö0ï5Ö5å`ó ‰3ÏMäÏ+¶žž=-³:µÊY™‘í°è£jn‘ÍCÿ¯cMþ¬Ü™­gdZ-œß¥:Áo ½dñÿï./xñ¼e ǵt‰ê)z†Î9öì£{—­-½4S<õä—«ãŽ/7T]Q9¥ï¤sOX—9žñã°÷-Qöb 8?~ òóxÙñ(²Ý›6mØpÅ—\rÑEýýÕêºuk×–ËkÖœ~úÊ•'Ÿ]×ÒˆvÓ+6Aæä†—o€ÜÉ+®¾²'/¹üÈŸ¼èâ‹ ƒ²ÿÂ~È¡¬¾¬ Y”ë*¸XçÚsÖB&eù¥eÈ¥\óâ5MyúY§C>åÊÓV‚Ÿ9yÅÉàif²bf‚J­÷U*OélÖá48W€£4å(ìcÛ*Zû›õstt|^#ãÃÃ{«˜-ý¡ù†‘¯ß2À¬ƒ?–6ºïïð×κØBÐýw—s‹Ì/t¶ç`Û½´ÓŒë™‘ª/lŒçlª)E6ï:JFuòXÆð”Vɰ&Ú{øÃXûÒn¼îèPª{q÷ây ç-ìèêèÊ·á¶ë &OùÜÜ9Á¹ w´ÍóÏáQ*H\\ ŽÒÑ¡GY/?ÐWÿçˆ?zÏ¿›ÿǶm¿ªgņ»½—ð•wߪí¿*oï« ó·‰Ëësø£ü$ïl¨VûŒøóõíœïz÷Ì­·Z6Âì°&r;üaØ]Ú°alëØÖáÑáÑ¡k‡®Ý°iævØÙ•Xž?ìzÙÑ¥”ÿ¶ Þœ£S…£Æ3ô}P ßRæ ”g—Yõ©½s±´xZð¦Åø½âú%ü нü ÅU~ÑúžŠ÷új¥ò«³+½âþJoÅ»~ñ‰JµZÿæÙKÊÇ—ôöõWª•ú·ª•*+z¯ªt—ä®îJoýÕ¾ŠšŠÏôVzRíëó.[Z)Éï—*Kë¿\Té]Ê_½´·²HëÈ©=}K.˜_ìí-ί.ìë˜ðß[í…ÿêùð~/¹ »û‚%½UèÎÀ þú+ /X²ä‚…•Þµ•¾*t gz+ðßÛÛ×W…«UèûÃÞgßdï–wMøôMœ(Ë»`~,L“ý |ÒÄ'êO!´q±ºþÔa¹.ºÀ›k®‹C‡bcR‡.뽯±³š#)<{c°szÄ»=z»"ÉÍ~Eå,Å%âýÚN¿;î“WëIêíۉx.‚š°HìЧ̓FÜ®T˜À(ì‡O¹¼ÄþbWoS0±–ÄIÅ£ý,fLqÍK¬¡¹©¿~¶ãÅš¤#|-†¼=Ö;W~Ï;72µŒ¡Ó†î— ºq†Òœ—ÕRƒ•©¨ý|Õ©QkI·4ä2w1G¯þUDP|˜?è­«M~¯þ¯É)ÿfË`¯Á´›’Šqe«¡±2«çj@Qˆ§B<;šš”jjij¡‘<§ÑìŠð°G¨3âØñˆ$¿o( èZX(JI(JDÑlkêÍE[ÐâX”®£8†?Xëiˆã&G”±P. “Pt¸ÁQ N¸€d´"â˜â3Á&Ö¿QF~¿8¸z Š\ßaiQcjVÈR¹'âJdTÒЃ\£æLþA~´öµ`òãò¸ÚW7Àü$è‚qÒº6®?Ë.WEùÌ~:éÏÊñÚ6Éêãò™Ú¶q±­>θ¹÷¡‚ÚB»È}3Ñ wD¢íªŠâ•âåtS̪äÞ#˜ÞnŽû7Í0JÚª±èÁµÃŽm5@Cÿزþ±F¹†ŒbÃøkëv(,Ê×<\ßò< þ‘R:ÒÉIÂ|ç>òó½uíŒÖnµ¡AP5˜ÓAÕ’4®ôo!‚ÄFPã÷Oi°â^G~Î>Þ]ló¹Í¹O0@¼…?@®¹2Rp¡É—š·ãµ[ä±AÓ±ŽI«…Þë\q ç}‹õŠø¢ _R®”Ø‘Kå4Ü?Ëp eL¾Ä´(¯øýgTÏ;ýôó^2íáêgT«gœvÞþÊ"gà‡@÷VΗ«Ø•*F†¿¼ïŠk§¼ËÛêm55âZÆß‡ó%Snë…Xæ+Æ'jO,þ¾xkLƒ¸ìeÕWp1öƒ˜9\+ÃJ8ϲ*:hVÐaj´–›ÑÌV`VPÕÛe†ò€`S>öKIØ"IzfÈ^XqæÑDrµ·ÆbžÕ¶esDƒžCóÉóš0hª{Ÿ00 ª{køcF3óO(sÛ;¿á÷:#»\Q= j=]½<7Ofî&©ÝAñC9YäjúNè©)€—–‹Š‰é4AOi5íÖž"3\½ ò茅XÉïZ ‰`N¹‹rdïaÏ9?ìyÄûÔxBÓ‡2îúãOà:òY²5bXÝ!;iÒŽ!JÝ>±=ª;@0O¹óðœl™¬ÑË•ļÃCOüÓpÛîÈLàftÄá"wø%t0)º¼ë!x0ï¿Àß‚*þ¬˜‚­¯§»X©L‹Vë #ìô_*_¿ó^ýÏ$»^¦)zãp:H`ÁÁCM_+;º¼ÔØšÓú ãz'TⵉÊë‡6æXó_ÿÅWÇÿ7É%©Œ$gÜE®ÄÍë‚DjĪ@¬£ Ö¶ §`_Š3ŽÁÐç ùi©ëñ¡÷1Éµì ¶qTqq£ÀÛ¼«î þ™™ÊôaGÏè¥ æ°Ñ ‚(F$‰+g’¸PL“ß@ÏtPrÃÙ핼‡(9 Àl XB7ö9 °\$¬_²ç-V¦8± Ðõ–Ê…ÞC”Yæƒ=`l X]aû!N½ËER!ì¦û‘= ðo×~Hù~Øgû!î3gú$Bõð~¸ÒGÔ©{nòƒ÷Ó‡²KxµóoñríG&Q´®>‰ÎY“h—³íž5žÐž›‚áŽ)¦z¦h$7ËdÅ…ä¶å…”‚D…‚¾()ÇŠ©+!¦¼Ô* Øb·Ò²Ð}[4eR‡”Rò†0•hT§öÖº<ÏŽVæ…ÄBý± ˜‹n•Íà{ÄeoÆZ`'âç§YÎVáçlá·;ï&eµÒÏÙÒ³'„gøsSÄOèÍ’4Ûœf ƒ½:5Ì£Lka¿k÷÷+Dýâl³%évsZ[1Ú•ÐiTN3¹JçaÚR¦YN®DX¢sïÇèQQ&',DßET¸ˆšÓ6¢‰íÊ ¢º ã¤UÖSÐIš<?û½‡ü8W33;òiÕþä9éã7Ûž‹ Æs°›Ù%ñ\\âˆ*ø 3Ç?â&n‹Ü6r¼bŠÉ &™ iÌ4Œ˜: Ó˜sÑøçÝzƃ] ׸KÂ?!.?ÆXæ´¹ÄZ.ž¡eŸÅ.p €o–‘ ’þA]¦‚ĤþŽåzb!ù—y44)U¤‹ìC!&‹Q9‰ö©™*þ…b]ò„M SÁÕHbã„ôe“KA—a0lŒ–¸ÿ;Wr q<Æ”P 8 hí½q|¸Bn™XVèñÀ\”¦RrfþvøÖgAS ‹Fþm³@ËÕDÉ å•j?š-ê±_ºƒqô_ol…‰'Ü«as–°ÌQ¼Š½ØÑtPÊ=´äQ¬NòÂG¹† ѨL—†H‰ÊÏ‘ª!)v•8)IP )I±ëL¿†S®áNt•3ë–ÊÅQ6Üãü6gš)lNÀysç„–ån}]íI¦ò'ÄqNÒòÛËeÍR¬IÕƒù1]¤¬g™~F[n.çváL— Æ @'.²ÍuÇÜõ…±b¢ ¨S˜N¹DÅ.s Ü75AN(„¦‘”Ê. •§Å\Ri¢úÓ´Ô¾…ý[¸n€CQˆbC]‹™L…•îß%±I›Vy)‘E•,ˆL)v%ÔVl}Œ¸’žTšûVÈ´M6òC-ìnKfŠ9€ËVç¸Ììâd™%ÁºŒÌPQh¦*^U& .,;¢ÆÓB3˜ƒSuñ~Ò,-` DŽ+œAÅ…p¯²Åf×ЛR­°/ÐÊE«”éJ›ƒJÂm@H ¨‹N'úoœÕžØW6ÛÏþÄ |¶/NÀ{¶Ï¡’¼ôÿÑY¾bÄÌîyTÒ–> +¨‚æ,ÈÌìóa Ðï¼¹âZ|ס#L˜°‘éPÔÄZº½݇÷uÝ çÈ…aîõO{Ÿ¨>@ðC2=8È&+Þ"„eÖÉÍÎ8h¡­XÒO”WÂëÀ·%ˆiåãüÕÞ;îÝηŒ>|óŽ=µ7D–°ÍÎ8$ ½X¤>J ¥§vvê>îõÞÉ_µåë¡=;nzh ôa´Í&=Ú¥äûÑn°:MBex´ ûEУÝSW(Ú]·Tl篺wûúlñÞ);÷Èmµñ-Ýd–®":ͲæÑg^ÁÅ'úw€BRuÐûŽ9¹Ëò¬Ò»Fu”F¸.G÷òW‹a`"³ï|²þç˜ÔíL²3áñ8»ùú­™Oq™`Cê"ëŠò’ºiç䊩ÛÿrÙÁ‚*êÅ;Õ3S‚ÜȸRRúBoáþ‹¤#¿Erä.BÅÉs®…†'°ŒÄv[[[N«>_¡ÿKð/æÖ?´S´ˆ9;ë›wÖÿPÿãNñúÒä×ÕÑ“Ë'OTGÇ Ezå>,ð*^WŸðx`ÖuWgßèi·qñÊé—œå€Ô{}ý­‘5gi'õ}ŠªœÚ3ì£~¢æExq“z•~ÓFplÄôÝ“*Á¿GD;¿ØœuPYú¯€)Ó¤bbJ4"u?¸b:øB“ûMº÷lÜ/]#€Âlïǽ°qŽi…ȦØbSÎ-ø©Ššö@ÿIÂr>l`*„¼(n“V£hzºöX"–%t¢°R2Ê\К”^_ ê—Íjó:BI©‰H§ˆºv(`YÁíHFHnD²ŒZõ@?LH !¬ Ö÷sdEøñMô„øÈJ@5Õ•“FN*-ÎÎt¥;ìñq#ŒLݸ~³ßRZê åA˜.‘Y!IO RÀŠUèc%‹¢ \±ú'Zí/YŠJÙõžRB¸‹>õGqÎ3n[udèúW9Šj>gn°pî!ψÔRO͸­‹>¬jÒèñ\‘9ÉŸ1Ö;={zö ùyÍmÁ`sQ¥#tT’?ÿ ö<¿VuÅ Ÿt’IéƤU«_š}WùJclR~ÖöššÖÆú¯o¿ÿüäMÆ£Ïq,-;&Ö„—l¼zy(äM -_>"Í‹0òu}"¾#Ch< ª˜™w†D=pÕè`†( ¥€$F®~.ˆ'»-#RûpchÔ*Ê¡å¼þ{äbnkt3žBSœ¯5’·i9†Qf:~Øø’â¼Ü”$«U@/ÏA™ßÏÞEè`î)å€R­†S È‹:ÜNŽ@# ûYk‹“>Øys†wÌM)ÆÄÅK~ï¯òVWnÜ:13ËdNåv£¤/=ž±‡È˺AÙ“”D—)É¢³ÅÅ7+ÝžAž i[6¿=~ãUÍ9Õé•#65Ê’ÑޅƦ¦mùžáíÄ•, š\±É)Á}ó—ÔŸ¯Nöšbc¿¢,« :¼›S1d\åÜŒ)PUW…˜%½‹â‘;”Žø9_¢OøÎãÅ’2ùüëFB$Û×¹…w–-ݼåÏX÷ÂÉçðÓÈÒ%Ç~³âšÚd净Z'ž’Æ#‹£¹ËsF…¦6,ÍkÆŠ<DE¨A€ „öH)4U •ªÑóÒÆ:^ ¨¶&àÔ6§eú”I5‹kìKéå›DËh”Ò,”¡_-rÆdþPgN„v=•ãÔ¹ãPËÇ©Ä×:°ó:]ü:ŸxvÖÏÛ°pySöGn^~sþœâÅë×/)Ÿ“]8Ò–—›0:7»­|áÆ`sž¢¿:#ÃjößÜXМ7xZVýî úoLÍ伿1¿x³,/{à£ÜÌ+(˜YLš3¸)/µ4¹x]^ASvûà6.Z<½ Òž›ëSÔ^°pãÆÅór³«ó¶˜mæb?­I6»´)»ù¯-Þjöí©Ÿ2wVCØͼûù;'­ÏÎ)Ô‰&Ýa§ÉT\ÜÚ,NqÖÅJz$v“ÉF£Y”Pó9¡Z&‚¬cE-%ËfÂN¡D¤×‰úöÐ!YÒ±âuœPFN('ÔœÙ^ E f/ž³˜VÙoij¬®**ðŒöŽŽ‹EnpÇö"§ÔE ÅéĨM$öÃÁ)%°Õ[Ôû^•¤¯Ýk”zóÚñ¼ð‰Æ‚<ŠÑ†åýêµÝ®Á¶ŒÚQ£š š *ÞIÙ¾b³->#¾8PÕÔpÏù­#BKq¦Ãm0¦ÄN8rElJ\LF‚ó¿—§¥¦¦•‡Öh\¾pæ…Ëš›óú#Ôn×Äü†Y³ŽUö3"ųýW ¦çÎj ¯Uþ Á9(gje¶-UÑ%Œ”‡¶¦äÄLpZlÖìl»í„ÑÃ]ÃD¡#©¶+ {AÀu6‰˜¦f3J¤¯¼,«…—±RªYgš^A#a¤¤ ¶†ª`Çl¤6['F~WeWFY‰ÇªÒÅдÍÔc‡s'fÕi˜8{…ïÁýÉÉÁYc\®?>:vBÎøÌ)#CCª9lsÚ­ú«¯õU´´W¹]óŠöƤdÉw®y…Že/ùG¼]_1(»!d›zxº²w”§pØð »Äé¦3iñ¦´ø,[îäÁixmΘ9S7æ>iߤÌ|Јóx[¶Æªœc¯džp›ØFE›‚l¬ê¤%Î (jÂì a½–yZÄóju2„È'¡Žº5ѳìfÞS‡ £ŒY ‚W0{wí‚»º?Ìiþã kW&nÝJFâÊO> Ÿ„ã]©ER„á)鯎]ΧUÞ%J·³".^(Û ‹TPeÓ<*a4Êpü³ÿˆÊg4ÂsǬÔÚ´¿etEÅ,GbRR^ÞWTVÏ),S=g–ÐP4¨õ––ÍII³***«*†åå%'š3¦º°hÎØ1³ rá°x€"LFni‚ÀÆœK ×Xøl¢ wöàì·pükòyùkéÝð;KÞA€R¥¤ÕÓ›‘dý YxŠùüxžTw‘ŘOÉq¨uä©.$¬ø¶uvüWT§VTÊïIØ(wš Ýëõ\ÁÏÓ×õIÜèãψ<'úY=ÏSŸyò#xW}*Éùˆ¬Cų]årPG‹©ÛÛæF­Š7x‰’ÉžWå6²Þe"EÄÝÅJ3ºø]ý;Oüf‰‚6•üë/çtœÏ’œ<¯ã<$CŠö¼Îãô®µòïP"ªçwÅšc1‚„$`„©«¯Óv!¼”›öZÁ;@"q¡¾ß7wxw\“Â~…Ê„’HáPÅî ON½Âœ;ÄZT0jñŒæ”f*^[Bf^@çä[ 6ÝŒk¦P +¤ò\ŽR»ç‚H¤S@`“¨jÑL,c$¢‰€’l¬x™NF©ªôÌ…|LYÞn÷ssLµƒKJ‚6™ä8G•Å J³³½Å?*g¸Ígñv¶K)O—¶V·ã¶”ô“9TNßQxœ”´ñrÛ€9> xÃÎÿzâµß>Áð~æ ídºÔDÞ‘m¡4‹tNÇ«Ã;dÛs”ÔùÒ8„dã@oíÈOuäËÆ“ô©ÕRùT®BFTÈn½DùD½N‘D…µ¤(qdÕ!ƒ²Þÿ³#ò.Ç#;vËG¤´·îÙsëÆ¿RqË…Må_¹ð<Â!!ìVùØÍ—Œåò´3…Ó¿ú nþê+ÚÏyÖÓuâò”’‚âPʤ§9uªRÌ)TÑÅA\”ôÔ„'cYY•ššÐ ï¿iܸMã'nª«Û4±tüøÒÒqãÄ3ã6¿yœú÷®qì»Ò²: Pe˜ErÛOšàe¬N9ýý8÷Srî(ãXC¶÷Ç€˜'ö6JK|ÙÒÀLÿ€·ÓùÄo_{¢ ýGnûä.jÊųáïä‡D*ªÀÂå'OÊ“Qt8©ôñ6vçÀ‡£ÍìL=yQ;‚çXgÏS tê'âÆêµÒߎç?}ïèüÓ/ý¼3XåoEJüá¼·`DL¬g#Y @ÄXlD¢ˆ[eÀ"ž ‰4ã åw1NŠck=üéˆfyΫ;¢ø^ÑŸ?¯q~ôÌÊãÏõŠfQß"œjaO·Å#õˆH&¼Ìjꬖüìž#ònÇÃ8¤´[°îiçg…mä*ñW£•àßûR3ù§ø+ò¥sœø’/"Êæ÷:{ª—‡Lhe±› –h•0û%^ Ç­YrVºòÚóçÉ÷Ðo‡=Ùº‘ÐÈÝjú†Î,³¥§Ã 3³‡‚ÄM΂åZ¼F| @wþüµª®$ ä¶I ÁžÉ÷G4غðc䉮À%õžä¦`×{§ÒMáL9k7ÝÛyšX»D¤¨ã¾|l‘Ãë- –°éSâÄÉ“á=Ï?~—ŽXGûº¾§¯Ë‡ò8D<.ÕºçŸ~úùΧŸšBµYIU– ùyO)ý.Ö˜£4/>>GöâœçåÇŸgðtþBfŠ»‘M罘^`ó×Dvëa¼¤GÙÏä›ó'²À_iî{¯ŒÐT~22ƒYì–©’ˆÿ2±>:U•Ç“(tL˜†?Yº+h±VN7¯aÅuQÙÄ@¤®QlfkïËl6³­éj*;—ÕA"Mß°aº¯¨ÈG §OßpC‘Ï_DÇ|cxyœÓ –u]{ÌÄÒäó,ùqxöÛ½¥S #“»óÆ')­É8J*ú°”à·“qôac{æàƒXú£ éå÷2GxËÍùÁý4ÎyúyÊ\œ#öÐGýºKì‘D_¸í1§{xU㱪þ§9ÇjŒFÆG»Äš¦›qNøÝN:öp iêjA‡‘QcѾՉg2–赺",*qËÌMÙÃë ·fMïñ·×¾¶pÛºuN#èø3iÿ=5ÂÞF cAÌlAƒN#+sÆ`ý&ƒöðŒ·ƒwʬi8}aݺm Ï0#„Úßïteþm$õ1Bþ¢Ú ÏQ¾]AùÖˆ°Axšs3MsîP$o” ÉiÔÇþ`,=cƾ`ÐGY›q6ûjúÆ‚¾Œ Æá?':’Ð5d`$¼üøK¿eZ£ó²”°sËqÿ‚Ñ ‹ÌÈê%¢ÅÁ¼éKfTÑ´sÙµdiòAo^fÉŠë§ÍœJAz‘‚´¡âáˆKúò©á ™bd©·Œb‘¥ é1Õo—í¸né®"»uhpü¸ydé´Æù«F<É\f1K*¼ |¾Ë? 4sKêùçÿJFÑÍ£Üæï#TL/í»ÞMG!@¿){ Ém湉;gàUÏ‘çþJnzöYú(D]ù]!”†¨ÈKe6@  Z Œ§ˆÀ±'¨Ü)C“$cÉ"÷ú»H³ôJIJ°Ñ¸[Oƒ4¥‡º~f@9h0¢”¿–2Ô² ¹óßOæK ì¶Ž›ìeŒcØ-¯Í«\4lq¼X’¶]·QÔ‰Ûu‹õÝCIUUÌú%)]ñ]· Èú•úX¿‹™ñÛÓÇ€\8© ·Xóઉ|Ú5üÿ¯/Eì=%qÐþCÿw‹òžHÿÄÑmQV$òMWJOí°Û,šOIñ©L±«u~Feu¨“ÇR‡zôÄá9ÎÚí$)Ñ­yÓå9Nö¼`²€Í€+îÿ•;Á$ !ò·Ÿæ“¿DX½µë,"æ×å#Ø#]ñ_ËÉú“”mð‡®ÃBJD•¼·2#ØaÀLX&®fæ¡Ð¨™Ýíê² q†Ý«WP"$JÑs†þðx‰©àRˆl˜¶Ìh–Ƶoß3~ïä’95îå3oÚ½þ”ìI\ûa¥Žô¸²uŸ¯ 0ýNoñš£çß‚Üy¯y‰aN „×ü4̪·VwÝÜuF|§§Ú¿ô[ÑtsÇ×X<µ¶£6Êêz±ëÙѧQ–˜u¨ã½ ô÷Òúµû4ôõñZzÛ}ªŠ,áúÒÎf6ÓŠä–"¿¯è¦¹ÔýžòÜ’>RWUóK7¹˜qÌìKPË õ½ÐGêíÌÌwwΠ27|ÓÞgŸ%7!ܹ¶ósr‚³·¢ ‰§ÄÔD¹ÛGeyçZ<,ü2ìýô¯á—ùôÓ^Ò|®*ÍyÉÆlc4i.K˜±òRŽ/†0ƨ4çWñ ý\lf¾í7Õa±y¼Š1IE¬ºÌ¢’½þ ðÛõUV:“Ù ‡ [¤ ôñ‽T1N\p÷•æÌP“{åÈHA/PF€²@™£ÌÁóiwîÙ´éÆM›o¤7o’òèKäAXO]Ѐüo g¿ÏµÆ%øG<’æNruó•QÅÚÒEЮ­Ì±I¦òŒQ·&}3鶺Á—KÄ ·í†–;\yîâ”I“¯5™R¬^QÌÊÌM4‰ÍqBâ÷H@JêÒÃEoÅa˜íîzÛ÷'©ì ŸI]˜‘Ï"3¸P"}ŒÂJ,²_^?Ø‘Ýk+éÖþáBYiºÀèw»Y¹p…aÚ‘Ãëçûì×ó »M¯¾AÞ!¿õU“ôñè9•™6Œã³Ç´“áóeŒA߈×–9ì,’…K>XrŒúë&pLØ&ÔÉ6$Eð% ÌToß,æî¬AÁì†c¯¾7›¼&ÞëÉfòŸÿ]O4O¡±ÀWß“d0‘¯ å&øH| ý¿hà¯Ѐ¿ ÅßQÐâ.'½ú•^<Þè…¿–Õ+UŠâ5,œx׸M&lý€Ë»Å üÑ͈cŒ¿£Ñ.KÇÕ• UûæaKÄRgvÈJBƒùrc°8˜˜¨é;žH¸ÂáóGÏ4¾†ÊUÙp¬ÅQ³ž-]Wèv—·OŸš^^_5ýÀ´Û½% ò—úB¿ˆ±è§îž¸ù ù¢?@Œ(øš%j4ÅÝÎwc”âöûüÝ€ôröžÅx¶›nì¢Û§˜^U?$mêôör·»p]é[–6OÜ=Uo‰ùEAÈWê"s`ƒð)~M¶ 4`Š Ï?/[Xtr¥&áÇú!·;Ö ]ÓðkpãOy(ܨ>”,¸i¹Ó ²ÂÏ-]—ŽA©ý‘YÔ°‚Þtx7á[xHÝ„án„„Rq&Â=á+ ýÍaktÐjuÃÝp÷[ð{2WœIn ˘s7þ»Ö™ƒuÕ'™¬v/¿‘Ý„ÞÏŠ†,™ásQKt$¥ ÏÈUÈ|Y³ŸWR™oeÜ1»Ÿ[tª#„çlh”w‹Š&O),4î–š¥´qu{Fûü>_Õîqt.¢sâYáÌÃýVÕá9×'ÚKÉR!ˆrFœgËÿG£{¬9±SŒ^rök»´éÏFYýÇþì$GŽ=;c°×e7º;ÓsbR†'7RJƒl{‰ÃáOͲÓ]CÁ²æ<[R4¾ìçz/»a:+èT,äq™ ï&3ú|Ì ‹ÓÙýžSXB‹—‹ìv›7õjÓÓ¥§ÿÿ•*ÒŸMïþAJøköÔˆªúQdú¡Ÿç@8=ý½ž^ ×ij¸ó2œk¥œûZÏZ/†,ÊîÃþW«PðU?«Pÿ©')­Іî7¦†Ôÿs(Ça3Ø>¡nÔ6â±¹¦m§‹ì~äû°düËeÆÇÅÆe¦îò¤é¬–˜ôÁ&¿-.+Áz…5¶Œ,Í˯w%§—9óòLžd»Û(ˆBŒþ dÀÌŠ‡:ñŒ°UIAÖmÔ°Û¥¥Ø2ìF=²‚UŽ‚3jÃ&Ÿ*å©í”™¿f²7¿À;iõðv_Fþ¤)«J©0É*.¥3.¾@<3Ù;ÿŠP»wR®/c^håä)wü¥¥löÑý9ñk#¥¯ÊåÈ7 —ßãJK¡!Iƒ"±mrJÔüÖ`²{»w¹9Ýó½$HaõÿGJõX¬;iÀꢀ;#kxl¼ÑnòÚ¬;ƒERJ¢;7Íé¬Ð™ïugÆÆ¥åèõ’Îå¢Û1¶¦—}q›0ÀöÎ t=WúM=›¸™£üt3GG­‹×¥Œ’ú«1,û¤•Z nÐýúÁ‡~ýé’> Wág;Ÿ£ÍÜ‰Çøf!Ê eêA¤Hô/·±}µðr :Π3ðh;ßunþgÎßSôòmäôt²r*9ßÙy³ôî…XNvã9xvøgyÊþ”ú^O1ü´§hBâ6é=ƒì( ‘@ÂP#ƒVWƒWÝà‹­`Ot$˜bÓRÒé6K¬ÝdÏté ,Uµ‹º±Œ‘E·[³k½fkäÍ:8ߎ&ä}â„3ËZ¯ZòÊ+;¯^µýé=²ü5Ùòêã~Q¸uÂô“Ƀái­«®]Hª Ê&Š ‰ h !Œëq[å2ðrôÿ áPx+9-½K:;ò Ãôζ½ˆ¦ˆdÙèé!’B¹¾{“‘!™÷ù˜õ‡¯¿ˆ¯ëla½’“ôw-éD€Vоwоõ¬o 7Nq& ÃÇ=× ul˜H©SÆóì°?+ð½áÕÂ×á:üRÇ߀øM§—÷»ƒö[Íaæûl‘(€8ƒWÞ[ÿ°î€,‡9Mv3 „_Ãårðð<$ C×gâkÒM\¦°$ËZaÓöHaS &ßú<(¬t»©‡÷Ü.dæ¡{=˜GÇK™d7À0ð‚*Èäò6y]8tËcÇî:ðä“;ŸÂ#¤†9ÜmÛOî&Ãl˜ýˆ!ÿùøý/¿€&–ÚÿB¢/²õ‹ñ2ßï5¥V„.M•#EÞÖ’÷ «sˆÂïA$A޽•´××y¯®Pš^'aQ›ß|ÂpޱZ,ñŒ´Kàe-³{%à-O²à3ò ¹g Çàx|ø©ÎÕø¾‡ˆŒ {JʈfÚÒìÖ-Vç¡EeDpóN#“|H>b]È_ë(G€h/×Ñ^bPfÈgP° °~Æœx½ §Èáä[ ã%C”Z¿‘í„ 0ó‰Ça¹çËû…q9ÞM»>^wu°]Ú«µ¹¢g„ŒûŽž‚=óŒÙŸÕ””÷H^%-¤Sz·ãeqírXÇËHèfF)?y±_1¦ðBçˆsð!qŸcïø4zƒþ.¢Ãh:Ýæ¿R:G{OAãj¥2C  µÝš5ñdºEÛë{­f°Ý“›¢-¦XÍ`dŽp0P=eŽWµ˜9ÞA+à;Ð ÉéÉw¤óêbúïêe%ôoì€õôÿv:Ó7Ñÿk@¿v%ÄCÜʵkW’ÿs+×FÍS…±ß-•"4&cÿøÌä¬À‚§£C+e‡\)cM)G‹¸£%Þh±š3\2u.‘ Y¯¹Ĝۡâ(po,!§á¾=«VíY+½ûþwß½>%f­];‹°PÙ,„Ä5|zä‡a¤0› ÓKÆFhÚWtYù@@å&¼fÁ­d©˜{É&À@„Oé"5™$ñ‰8!qu=z¤"ë5‚žž“°ï’­b¡²[|0!¹P"íâé äBÅ!}ŠI§ˆÀÏ"úøA1'Çu ûÄmaö©ù¸Ë™„ ö*²˜c¢˜KùNoKY)}a µdl§}cð´bÏ®aî-Wl~,sá¿æÖŒô[mI®¬§×S™pR^sÚl&ÐÅ1©'߃kËó³¶lð¤¤T·“ã¿ 4¹’5 ùQ„rƒz’Ù€u æb¢-ÙlNæs‚%y˜/e%ü°”=³XbÒ9¤m^w¸™ÝÅ6`çá´¬ž{d^û‘¶Õؾºí‘yói[þrµ7+óJ‹ñ6W?'À b•©ùÑ;iНf2²:¾é‘;m6U‘NáÕ×UCV^iÑë-é BÅsÜ1±P Bâ Î"ÎP*;úcB%'¸{~N‘(„x0C!i‚§ÈÈ'äïäø<þøàoawØg‰…y‘‰›è¼L9-XàXÂ@ÃP‹†!ýÁ¥$.ag)é+ÛcÊ?cEÅEÊí3kGC $Y(ñJÕcgí¥¨ñ†A7´ƒë×0Û ^`ëüJŸnÚfß¼Dþ>gkÎ QlûÊa32+#j(rqÄD=‹zij8)Óœ˜Åá+ ú#N7æÍlÓuÐN¡ìmÏ3ȧž][RZ=rÄó×®=9rgiÉÚ³$kbf`ðàö¹å噿!ù$ðÎâíÛNþ¬³©þªÖ¦§È«/lÛ¾øˆ™°råçV]sÍÊ·¹&8wù ]1•ÄÝ;#2¡!¡²XÌ­YÀ2y½2Ylã,¨ÁcŠ1i•¸¼¯¯Çtü~`©¯É<ÁO{[ªä~fcqâP›ºáåDŠDMDÙº)J=«Z“Iôõ‘ÖH~Ü8ºn!5G`ÐF£Tp•hkƒe°a²– l Ж@1ZÒ ƒ€dIçŠÀž¯cC^žzÉF—p‘ `|´‰ÆŒ²Ò09Î," Ài*€éˆ)ƒ%’€ø†õ)µ2`¬Ìˆ 1ƒ6`°‰‚HacÔí§Y?ÐAt:ø.Ú¬bÐu&D™VBW=ÓÔxN`*OâC?o‘€E·…î(à™ìéM˜'yeב Ú£¯R°ìÔc“ñN³Ó¨W·brW‚x í½Œ,Ø.¡Ãü¾€"ÇáÏß’Y]² 41ÂÞN‹Qkº&±7ý1Žh1fðK‘ü¶[ÔÎð&3ç฾¬" «ãù×%Õ‘\M¶‡«¤’p×f&Æ­˜ç*õ:0–¡F¥²@å’eN2ÕÞ”Z4NNð›égNkÐO•“… 6‹Ý†U±Ö³+‰J´Ÿqèÿröše>ýÙ=ŸoX¾‚Š´+Ç/ok[^ÛHŒÿ˜q/ü Š|¼õ§ßÿ´3öÅÃoþù©Cç&1Y¦úÊPî£x#i´¼€$šË ”gP¸§Ôê@õ3^_¯œ[ö±v‘$Sk/Öx@]ÒNfRz=n­RœJË'FGc¨yy±¨`ôÌê! û{M°ªèxá…“ÑSMµ|4\ F?éƒ~—S”Dˆ„ÊËB$Ù+eHF¼*’صTônà»Ôý¡d-Õú^-TG±â`X\æqGcCw l\Þ…"2á’S 7Î:þ8°É€J»>wŠëå¡79†N$;°ˆ¡&…¿€¶²´ÊÌÂi­åQ10Bõ(‚SGÔ% èf|6»œÈSñ«‚ÐØo“äK5™A›\²ƒf†z3 ôÔ¤D»•Æ;àdŽ›ï‘UtÒ·lž–QD—¹¶Öeú]$šPZ&úï½RŽÃ‰%¦Ô-Þ_>xðŠ×7_ýHA$¿˜’6jÉI·.X9$=Õ×\kš"[¤´!{ŠatÝØn~Ðu¢8¦rQø…;<ƒÇý¼-4hhÓÑik³J<é9L’q©ìF6äg$=ˆ’Ñ 5Œ¥IlçÇÜ¢ê·;àÀ|f\—a¶Y­ÙÅ’ce‹Ò±¬¨k‡²?@¹$3™£”$”–úÙö£N¿ýóSí1£F®ZðÊïfÞ"€·ðæ§Þ¼«fš+¹¡úV±ùŸ¬}ágгžÜÿö9ƒ”WHÌÂкfiæ’usä…æí;ZXA½: ÷íâLdaaˆøX¤úãA+€Fh³Pg±¹}Ìäu |ŸŽ·„ÙyVîCÇŸ'äÄyLÙ—¾ž·úè_BÞÇçÆ¸cÌ_ºƒ¬¢þÑ}¤a4‹êO†¡\TŽF„†'ˆ,d$b‹\³pœ´PåÍs%Õ½ÍÏTÌ+Ï/OO7¡\ÈU˜wëñcÍäŒÄdQY{÷eNwJxDmä^Iñ‚{¿Ø0ftjaÌ­þÙ2ËØ³ ËæYëªBã] ÞüÚ¬oÀºá³û¬Y³`þš5i»AwâÆSõžœ+nßqw €”ö`)Ýo‰KΔ#XÈ?É—Ï‘ÿ¹åì3=ðì³<ô t{×¢]¼PMhŒ 0Ä€%… D Iª‰dŠŒ›²ä°ý TÇÂÑ©´<¼ÕlÔ£ø ]ô)ý,ìªCb¼ÎßDN[ãûH¹ƒÉç?tž:·zÁÞ+V^=ÿþkoØüàÄ[n<šððýƒ²þ¸õ.½Ýòž'edaáά69=ûo6è^Êe² ¥0Î̼ãˆOÜPñèôDHj¥@7Ku¶ sB÷$ ×~l±”±4ÞH ŒÊ^|ataEÅk䇭º%g"Y¢ð=Üuì±[^¯ðû+·ý’a$,:s†ÜHf÷bäïÙw”EË(<ùâu”MëŽùÆUb%ð=L65ro˜×,rAξÒv­Ñ›Ÿ¶ÐÀ¬‡…–@e ¦«”“\f³[ˆ9¶©™Î'9͵! 'à‡W-Þµ!¼AœyøªYdzÇ$ «hDq©@f”Œ¼hx¨B­\t3ÚMÎÔ‹Ë™âMõZ’-É»^aˤzÎ˽m Y¶æa —¬X‹¨£çNÇx>åÙ _ÜsÏÆVŽz¡MW<£ªjF±îÃêE:R©+š‘ø2´Ÿ?m/¿Lîüî×äýöùK…Ý›þ°fÍ6…§^ÿûëé0u}/¼Cg»‰Eßæ—4ji!€Ë{@±F"KÈ&‰dz;yKÜfÎgÂÖÿ®}þU3eˆ®òÁ}8æLXª©<ƒEH<*Îì'ôØx©ÐãQðÁL˜Mšà7ä9òû9äoâL²Ö‘–p%Ž™I¦ ¾À&ÞÈ–FXL8X¿‘8sC·Wnu{YßÐ# À-n8—•<®eÙ'~?î%ï}ÔEÞsW×T|yè(-¾;¼Êga! z‚¬éñ®“’µ»µ4’ÁÍ‚&¬¸ó¡èë÷êL‰Õ~“Ý´ýŽáä 8³óµU¯veŽÚ–Wqëmçÿ×obáÏ;)çÒ‘ä±éñ KÀ”ƒ ’Ü.BÏ Ó&—ÒŠ¥™ú9f›Ùœ^ÌŒRmzò$&ýÔ%4v&‚ÅĨDE.õ´Ùvntai(aþ¶]kkŸüÍÁCsÖ|',"_ÏÿôÍž±kì݇ØÄófA°ä7í^xàYõÞµM«óΜٰòªyò‡âukëjR”¸Ÿí»çш ¯ãÇ sCÙ-ÖÚ;Pù£ã…}y¼¯DžGÍæ ŸSîf<‘»{@9~”={ÈÇ“öDKWS×iñ¤X‡\h½º—Ñ 2¤’qMJ÷ª[šÕË©ÌÚ­ïñ$k±‚µ¸ª‹^Æ ãèËœöµJ$Äj”ì 1î8¯K\àÒñai)4¸Á«žWW\‹Ê¨$VdáBNeÚ°²89K>9$î9“Xk×¹ýe…½ÎZ‘—?ïíÜ*\wêTøßcÇš\ <+Ë£;(¯¬æÈNe'¹cŒá&C¹†ý&͆0Ç'&ħšS­>“Æ[Ü ÙñT”p¢¨¬åwtzòÏ3þ|rÞŽÁä}øOkié¢Ö™‹ãLÈÿBž¢^ú[3ZÂÏàMÓ§zàégbˆ?Laʦ¶d  ZAÀ6 j" Z»YWlU…mjF Eñ£ø6Q¸f’taÖ.,‘¾ð†–?àéNÙúО;Kfçíy”±©/«BÄÄa«^Ü^(ËÊ%ß\}üñE®pHñ{Þ{a4ž²çõt›‘C©4Š1Œ¹¤häëfR«Åcõ ª îaRž>ÐÛ}62åUKO:øÙÆ•Ÿ>Œ3ÿ4sòÆÏï«n+»È>,i„¨Ê²‡;w©¾݃]{Ï6î›:mk•Éc´nzóív,¢Æ³EMz0£ªÀq±=ÁñÏí\.‚™&Âäqá%ò8•ÃL ‹g΄ÏÖŽ`r¸!q+}F{—ñ"D„üÌîgX-š˜O‚$þŒh1϶ à€[©xÖý÷=¯×·,Ƹ´¼¥µËþ3÷ÈúÐplMåÚÒ¤rþÁgÉ—F#(ìP® °jZrœIÑ2•1«#'ÉT·›)Âv-“Y5m(ÓRÕÙóÄBÀðÞo¬8Ú`ô›³o>ü³…‡¯zååÚŒ+®°˜â}iIn°œ:?OüGÐÓ°áË6Øþõ¹Uß¿D:É?SI~Ø¿ïÈþ¬Š´t«êLáÛAäbJíÇä)¢8ûÚÜjúò l´éë`­™œeÔ`Ó+ ¸áO'ÜjOX9ûÙ‰#Ï@ÙzÂâ±Ño&®ZøÄÄ‘‘÷`:ì«I§†ñOìƒ7; ‹3óÍôãMG÷’Rh4…l…ÌÊT¨Ù  ‘ÆŠXâ6›ÍÊ-ñ 38ñ<ÌßI_ï‘%¡þ0+<„c°-ü?ä¿' :7Í…Xþ*üi4 @t®ÜAŸ•Ç⦹ê^b0T r4ˆüÄ‹¬ I@ƒ²=î$¶§ÉBÅ_äEÄ_f6Š «IžŠ¬AKP Ös5ÉÜ3~övT.óæ—Y-Á~‹)&ÆU-t Õ.Œµø çÇÛÊrýW´¨(-ÏWøÌ]ßÌYôìóKg=’̲ŒÆÉ)ÌZúü³‹ÚÏ“Ïfæ<ù$Â̪ ò2$ «¦¯-†ˆ<æ’ÄãnLK¢9ŽÉpÑ"Q®Ð€°(Uo;GÛǺÍy ä h›GPí)9*ü¨*4RuÝ‘ªÇµè }¾¢ˆ­Åz±.)‘E|^ZIô'ù£c ÆKÄ„Þö ®@7˜Rf¯ÀÊ£Ñq†*e6¸ ÏFõ¡I|S)`vV|j­Q4‘4! jÝ ¤ÓQøõz¹5d¹^®KKeaˬ@ ¥f§e;lTÕ'ubœd2ÇÇjø”¢ w_|ž Q‡ñ(™Ö=e6ÙþòÅ„UiÇiŸÆLª>´—¡›øJ„øŽüiR·rp­—*„»ú“‚œ't•…‚ žÆ …hl¨Ê€õ}D™iŒæœlG “ }»è0z± y¿¿Ñôa¿\t\¢:®n>*AM¡ú^|#ûò‘"Œ436š‘òó4V*I+éÍJ¦þXi@ƒí—«ÈæþÍ9l€ã´ ÆúΊœ,´¬1‹}K’æköŠòÛm’˜’dsÚ±FÑ*Y£Yî&±:€Ûú1Œå4´ÿöÅ­c¬Švy ²ˆÉ¤BÀ"ÔD*°hÔzX´MæÀêô˜C+‰y~_ øÒ]Nb‘TdŽ7hð öÔý‘//¡û׀徠6åmôRC›é)X¦ÃMáo¤îÐfZd`ª½•-¯á™‚‚™Tˆ9Ýý6â’ƒ©ß&!îôEÉÞN’ûmœ<°Æ3hãvʃ¡VI¤›S“q&Ñ+y{x ~ZHT#¾ò'DFåm”Lÿù©ñQ“7#å †Ðd ˆ‚$Q²” ŠœX‘zñ(ÏqÈJF8Š‚[u€ñ Æ¥þ WzZŠÃ+æH9æx}dN 4B¦üËÅÉäÍt°]2X†Õà höñ8>ÝT®sE‘c±•q·äêa›y9 ­‰["n'©µF7£å"XÌÊËÌ•>Ð~"W“¥¬o‹IŒð?bå“OĵòuÈ€L(µ…f‹Ä€€L S< ëÓ,X’Í JrMO^_ mHA:½¢›‹ôH–ôr;’DQj¤ps3G'ÄÇù3ØêQj2ÍM•Ÿm6Ûâ,#}Ь””ñR 8x~½ &ú^–¥T;þ_öñûÿýÒÇá5:Ñëëøº~gpÝðK–¶¶-Ã9dmb™wß o ¯cgÎíYáñ·¾Êjr¡óßûìIµ£ªÆåÀ©[ïRt?Û¸y?P<ìN”ŒÒÑ´04/ØP1JEXÀét¼Et¼¹:^7€R£¥„oG˜Ž s/;b—³´¸° ?oP¶?Ã9Ä5„D³øˆ‡c."{η²¸¾Ârí\ñE%)Æûïœ8eá½µ»F?züÉc“á™p›ó’øx5gP‚~ëÞQ£HpÜÐÐðÛ#.ƒ›4 7^@#ÑöÐÖD: ¢° `Æ`ÐW€ÎXšƒ]6ÈŠ¾gaCà‰aÚF‘ž!M F½a.2"bÔµ#vh²‘Îa¹5–Þ)OÈÊtŰ!åƒËJ‚ù¹™#³F¦$Ù­ñ¦X£"!/xM,Óo4j¼%ü í—¨èá $ÒePcöñ³ÐœW5.]Z;åþ‚ÝÕ;·<ðì ׆—\YYw¥¦\Q>lÔÞÂü{âï5s¸L‹¯úî…K Rìú¦{NÅ"3ÊE'B©1³ð¿ˆÌ>Š@—"ÐÆÐ ­°Õ þ"Åš¨_ÿcÜq”ÍxJù‰v·E­Ê{ÝžWK]·]ùÍ¡t«ÅhTí.gZ’Ã’kÍ5ÆcÍ츟I-nA3O‹b6 Â弎¬†Õo.^üì…)—DêW‡à_í~ FB 9þ÷¥PIÅÓÇâ-šxò£4/4·G@%š°,%";A¯HQÂI¢Â©'C.ËC o7‚NuôEÇ'«NœˆË(È,`»þ©ˆòÇû™ˆŠéOD©+Lu¢tg'Q77Ùm}Õ×ÇŽ¾þVáì¡…m%óWàÐö›ïøÅýÛ½ÞÊm?’Ug_ÿ˧ï›l·'¤®Y5v÷;ïܺLVàfa{µÑ§ '*C#ЖÐF+`É$`Àil:f9±Nɽ®Œz¹g5IB$m**l*ªr‹MIêõÛM`eC#}1p~0ÈÊ»]l:Q>"XHÑâs•¹Ëvïä2RâØ„¼HÂ䬻¹/cÜ~ò-2ár˜Ëê‡G^¸ ™†ûZ\­±­:ö0Õ„)¾TÆA²(ßÐÃ=L°×3Á>ƒ¢T¤‡iíŒ3cJ_ÎXL¼½É/ %õøFòÐ4ÓÄ[W†‹ÈÕîµP>*"æ„®ŠV¼z%d$ÙÒ¿Þå,$®tf3ž–[‘,Ï—ë衘¸œ,n ×íLI¢Æ‘/ÞwqžVIY&{£î”Pý0ôžà¬¡Á¹ÁÓòÈm7Ýþ‹û{ìwûn¼qû5oœebÊÙ±·Ü¸ëÀý峿Á‰ýöS2@‚Ø,<«iÜË<ž¡àvò\ “ÉÑ1N—Ë9&PçvÇ2’ ½Þ`¡Ø OÀãd¿³ªzÿþ±U~WÕ]û«]þm¶4gVVzZÂè¼8SxHù9çy¡.Ê¡FŽ’µëYøvƒXg4v‹P%¦¯ÅºE¤dë%€âç˜òJA#C¡îY®å(ŽÌgQä«Côi݈Ô{–*1—ž¥8·ßù'6÷'‚18Å&áUJ8r£1¡QzÀ`ǃ$PÈÔÏv„q7 E±4–¢×™–èNr[ͦ ž:Þe…^ׯðŒÜ&6]l®¤RR¢¤’YÐY‘EBN!Œ7°Íÿêé&uÚâ×´‰ªXøÌ ™Òá8® ¹PªŒ“0;¤h‚¢ØU=T;Á(;ÓçÉKM¶»\F=²Mný2Zd!ÊÕ·ò÷ݹ‰çßô™{¼eσw×·¹èèÑCéoŨ)K横)“Åæ-ËQwsûšµm; 4®Ø„ C —!×UU8óâ<£‡ ©BŒt8Óá$ tz¢€ÎWŠv˜ª=·ØÜ‹±€R“é‰é–øƒNF À¨wÁSúbsÿ¢£ ´Kø³èE2`ºã±zã‚k Ø%É.¨/ð.ùìqôå+°aÇØqöƒÎÂMÚÚKíôôbÖ‹ÏG;€\Ìþ†ÁFï~—äÀoÁN»cﮣ7“Õ[Ȫ³‘7Ð]dЏGœ‰rPa(/e¹‚¸†‰à…ü|ƒêÇ-g¾Ùb¹Î–™i³ùTõÀ7h«§Íì=Ë×½êB¡V©"`ußÎ,[EΤÌLp›bÆø˜ø|wÀ(³'»M¾²²1C²r’0ùPœnZV‘š^^¶îü~”Ç)KÆÓ4é¹Ýl’5.+Óá($„». $ž–v#ÉÈòH€‘¶Z`²¤ñCy¢(Ê¢Ìλr‡Ùí€Où·°‰ü‹œ•v_XþPÒùÆåN‹ú?-. ãRfTŒy»Sͪìå©–D…?ÞXO_Ü¢ˆ2ýçÛ´å’´³½£…:  KE û þ(ˆõ°“2TÛY¾ëx,È8²ûÐɱÀïèÍåóøúâüîB2-5KÔt^*ºåeû¢±ž¸^›Õypð*=›ÕñAH%¿êî»Y]J¾oís¬r æÑaÉÈ…^Ye@À¢0.–%èõü×nHä)!EÄ›|5wYmdÏìr6HŽ ·N.ÝŽ!Ë'Û×óÂ:ý7½|o¼p¦7Ãïí.wju[½½.Á_ïcûÀÌL«`{¼›M=8õÄPòá¯Áþ%{ª!iç«m·kü}AÕ¸ê‘åbБïïÿ¡¡f>ú|¢¹š<3&DÂDp§Eí£H3Ë iN´LEš"ÍiBZrß1.èãŠËޱw: o g„ü\?pÒ3ÄH0« Þ|?ñüúÈ…ÊJ€ë~êЖ_î¾óÎ=®—ÌCÞ½íÂí »¶ÞpVí­j ëæ'~jÂh%ÚÎè³PBdhvÐI.6:Ic‰¬^,¡CЬSæ^Š52ù@Öž±HNú´Ö—`•÷ÎX&Ælór”²2QLãɈF©Ël³pyÍ–à/ÆyóÈÙÈ×cÆ@Òöß>pÝ‹äñ{ï.Lw&þÞŒðΡ}Ç>þ |w¢i6ùÕýD¼@:ôúxHûž­&±šeäG¥èQ­’€Ò(–ãAÔë@P©&¥û‚Üû‚VÉ''’øA:hD•µ$­`k*|ÜóYÈïZÌÎFäG·–å…µütÑ;h¬Òb¿Ïê÷eXÔbZJ Á¬é´Š2l.¶¨‡ö˨†ãú ¢ípÚÇj)(J'>Ö°yHEãk/~ÜÞÒzí¬;rèÐ,_yâTO˜?lèŒääåÓ'Ë;X¦ŠÖClNþŠë×Þƒ@8Rõ˜©“vw|sü¿‹Kð_æ<úfcÖäÚñ­wœ@X=}È×B¥³Ì­½þ$Ò ’®­qÌ«U˜HíO"i­8»õnzùÞ8w±–ÞÜ%Doƒ?¤ñÒ3ätϘóäôVF˜©,a§óÑÐP9™ ×ô ¢¸€…Ä5zÊ*0^‹—á3¦ö‚C ›U“‹8l×°ëö…@·7iªÜÆ!úþÙ~`F™Î¯·s=Âh1•§8ÂSѾ‰ÁEYTŠAúb^F["+ßó¤ýé™6S‡ÒÓË÷Æk([½UUV%eÀ¬º@œñx¯6dÿàç×Ï6wѤßÎ]±ˆìÇ{"d¸¡¢"Æ”T õ­+×ã¯Aì4³"2tþNæó×!ÔôÛB[ c‹ÅBNa^™Q¯8tNGD’ îÍ>,Qç¶E$ø7ä•/6zÍä=<~B}ƒdfIGÎ|ßvfÎàaÊ™Ž7~#Ÿ›]@IÑÆOG¸ÐÍêuŽä‘0jgç:æ1)Ègùr1‚zW¤mÏpÿãv—íŠá݆PzZr¢ÐòpêÙ굟V• Ü ŽÐ!^x&îÑ}·‰ßå?xÐ7kÙ“O~TR «çνÊÐÛ·mZ»öû¿»nÆ7„£ºåë×/¾ïœD–#Œî£ƒöQ%ƒœèwÚé5½€™¢ÕÒ¾áy|8 9¬3uÀ€¬×.ÖT§›¯¶çcëY±Ó¯Þv±Æ!Ÿ¾7®úoÇ4PBÎt¶`g³šãè b<!g iýà2G£Ì->4§iÛð²!×ÔŒYzã«ÅÅ3ÈG;&OJpŒþ Ó¹­9ÜÀ/Žø>ŠV,EkŒ†Öä¾XXÐ…—ÅBï&þ<_Ÿá@SZ<¬©•‹”z3w¢fsuý/“aó__ÓúVËìÒ’@Á¦+q÷[³v_5}МÀƒO·ƒ¥‹Sîß§yw’‡Ü>@¥éÎáÎŒ4‹ÿþm«ÏŒS’âºÐ|Z1ÏéJlþÃ|U ‰Gx* 'ÊAù¬2˜^Ä";G)#Q/‹lEA‡ôº¹tà óú` 4ŸÚ±ddgf˜5C2è5­¬"© ©¯wê'lÝñJ™Eçþüûž ?Â>wsYy½·ÒãªmÜävy<äÝÁÀ`ö %d üJz·óM¡øBŽ`Ì.¯]¾ø÷­ Þ0º¢ìÖÖŸjÙ¶­…þv¾Ë6¯K‰oÒÙé€JBEˆQ$êžKH%¡]æëk\;0ÓX¤‰~ÒSœ'õlT…ÓL$Œ¯ ;AG²P”ii(þ§Ð Ác#JK*áv<Š|BNólÏ&-¿}­øAF’gâæ™[].2èŸ_±¼Só&óÄéXÞe8…,-{:Óçd%RwÚ°¨ÇHÁhe%h€ÍA( ×ËóeG@N{OãèÍ¡$@†×ÍW»XñYdçè L¸²E’Y)9ìô×ëÉÇêî ˆœÖ€#;—Ô×Ö ÍuÕìÄ §_“èŠw`"µgƒ0<à…á$(§/Ø\H[)Sö—m.ô(±±Á~‹ë„áø©s5Âh45¬ñ#Fy¨6TéÊØa0@:‘•©6HXÔêA§[ÈÌ]¹Å<è––ŠPvVÀïJOÍKË£}$gxÜ>O %0ØÙüé]úŠ×W`åÔj£à«ØýOÚg¹œÞ‚‘Z-¬‘ƒ†ç¸¼’ñõêé™ ƒ¬ ¤Š} Å$ØÄI²å¦½1±®\žÔ;ٙ㤲¾¯ú0=~\áPq5˜ßëÜ_U°š|‰“4íV)Y„°£Q2²FÛCQ¤Ó)3§™Ñ(ψ¶ š´}ïîïzx—mÌÊ POÎëéKñØQ¼û?œèŸèx_gKO‹’¼'kÐ$*hWp’£¡¨%4-tJ>èuX¥»Lé F¤èŒÊ\Î1* ƒ/r­`HÒ·"½~¹¾®¤¡’¡%C‡ ..Ê¡£õº»ùÀt9>ð›ãUÑj¶»-›ÛlÒx+¯iæ‘W~ñø¢ÞKdÉ “³=ƒî-韧Ó|L} Oùø@ñ¥‡ P>äÕ«É’ÉééäŸééÚëÖŽû¶X|uâÌŽû®¹j6Cå„Ä æÈ‡8sñcŽÆŸ~̱´KORÒ:Y÷XY0JãÄÕÈÆ˜ÕòÍóÍAºmXQ«ýÌ&¬x4‡‡ûBxê·/y_gZ5L{ñîÖû¯j™tåÌ ß|–,ûbÚÐoeË´›:7½ðÑ,aÔÕU£VxÜ™¶„»ÞBH<ÅóŒ'¡µêVF£€p„ÙRø'Aû¤í°M—¸AL€°·ê±Ü…<¢6ŸyËž¨ˆ]éN›²BkÄØÐ¦±ørÏ]îs;¢ãW ÛôXÀÒ™ÃFÓ"᫺‰ùyäƒH†óÓ‡#á«kç©¢Ûù– ¾ïØâ[Ê7 ¶E¯ìϱ¹^­HÖ“Ú;Í’©§¯˜YéeµÂ²™tå±YQLØN?;¾ý¶ªøªí™Câ<¿àWödGuÞ¨âÊôoåï:÷üûæÔ oL®tŽZ ON‘G®Ü̯qæeŒž5‘#ùVø'Š.¶G¤ÂԽo[,n“ÙÊ»Ä|jI˜!‚ ã2dz3‰ŠLï”Ìf11ås«¬vjyƒPãaØøäqòíá'Á”pDð êüõñõ¤éOŸ›áí#,\M±´Œ‘€\(ŸXÈuc…ÙüX"`±]˜¢V€ï®BJ=RÔ¢úa<[3ÎÏËô§¥:\‰.‹9Æ ‰(ìÜÆô”PùoÖø“ÎN3Wܯ產SEQ˜mOÜ=¤°íî»Û ‡Ü}œ­/-‚kçÇ6 * … ó÷o Ë_‚[ö–.YRº·…\ûRçîãÊŒÊáKɵpã¢-K]5*7?¿°rf¹ A×W$Qœ®œBÉO‚‚Á 6qIÔÿª#z‰œ_‘_F½.½(_éll;ÙŒ_ì7  4²D‹´·D÷fÿѱG9u~Œø„ÈI¡Kë³zì~ÙÙ(ü²¡óa¡>Nqcg}gÃÿaPÊÿYxš¬‹b vxÚœÑpžk†á{ƒÚVlÖ¶Ö¶mµmÛ¶mÛ¶m?cÑÉÌõÚw³à¿ÿž‚ý‡ àNùÀ9=$“p$ª‰§@’ãÌA6Bò 9 )&AÊ@©rAjOH“Ò†B:w9 é‹C?é K@¦S¥€|‡lùe$¸¤—¶àª÷nÀÝ<å,x¯´àLÆË+ði"³À·ˆŒ‘‡àWNË'ð_­D}„ ù\B†Aè=È‘rö€\½!·zÌy%ßf(à%— `e9 …†@áP¤&U®b… x*(¡kÉPZñe\¤‘„²uå/”ëåàBQ¨X *åÓPyTÉ-w!l/„+GDBi ‘©ä(DÅA´¿ —ëPu4TÓùê.²j$”ËPs=ÔÒLj+OÝ0¨ ]¡±'4Q\3ÕÐhåm×V¹Úmö…åt8¿C§ÛÐy;tY]×A·+нôPÏ=½¡WZèÝút€¾áЯôÏ 1:{ âJB|8éyHº†e’0¼¦l¹EµŽœ£²ÉÝÆŒ‚±³`\ŸMúÊg˜!'`biY “²É˜ÜZÞÔy0µLK*Û`F#y3/Ã,Õ1û;Ìy sÀ¼©0_=,Ø‹ÒÁâ´ò–Ž…e7aùLXQ VÊ&X¥Á­ö•+°¦7¬UÏërÂúI°a(l¼›Âä)l[ZÁVg¹ÛÎÃŽ ¢wEÃî ù{Ý`_IØßZ4ëÉeÌ(Êw(JÖÀጲŽäƒ£È ¹ Ç*Š^„§àdŒ<‚SMàt")%³àŒƒÄÈ 8 çJœ/*£à‚‹ “‡p±®œ€KÒ.gZr®ô–]pÕKNõVr®ç“¢šnä•Õp3TæÁ-Ñõöh¸wóÈ}¸'WàþùÔÿÃsðh.<Î)êãI ¹O›À³òž×€dŒÜ—ár^5“ðZõ¼é!OàíxWT”ÿýtøP >‡O©á³§<…/:ÿU1߆À÷œðcüü ¿ÔûïÑð§ üm&0Þc¶sh!G0§Ì9‹œÄ8KÙ‡%,)±D‹±Ä³±$iå:–Ô_¶cɼ%V¾bÉ[ÊQ,EVY(:—r%–*LÆÈ',õ,Mn'¯°´Åe”¼ÀÒ5–»Xú0QM±Œå7–i9–9ZÞbYb°¬9d+–­4æÒsM# 0·pQ¬»¬ÁÑžö{bTïAíáPìpUQGwì.v"³Ô•ØÉQ²S>a§JI¼,Íï´ærÆSúÉYì¬îÏÝÆ.L}¿ø»´»¬óWœ¥:v5¥hÞ×J¸Œ’ØuG)"s±&š÷ÍT#ÊË]šÉ2yÝÎ-]e“ÜÇî¤íýn}ì^ÑýýôÒ^æÉ%ì³62[´×‡ÎRPÚÈ9Œ=ò>¢ïSH%"Ú× “ ò{šEêÊZìYbi" åö¼Œ,Á^Ì•gØK_™#·°W‹°×‰d€¬ùÿÞly‹½[Ž½ßŽ}Ø}Z}þ×P=ÆëÄP?MjÛ¶mÛîåÖ¶mÛ¶mÛ¶ÛumÛ|>ü{õîLæ$ù-³ü‘}ý”¼å+ýýÖ_a¾×S˜ÌÏÏ ó+H¦d²0d6Q桸, , ?^¡§,âsY¤C²È}dQúÊ¢–ÃRY´í²èå0\ã¤,f!<Åz&‹ý]gŸ,nU ’Å+(‹_sðY– æàŒ,aY¢ô²ÄueIòÈ’ ]±¯eÉò`2®Ê’‡CuYŠ`PgÊîØ"KE}izb‡,]¼”¥ËPÔ’q¸;Ó Yæòà Y à¾,kkY¶²ìd9R!»e9£¢FÉrqNnêÌÃïóR_>~—?/šà ¬@Fl“ª +œ5eEŠÉŠ”Kƒ…²âíqEV¢•¬d Y©øGVº³¬L2œ••%Ïrd]¾¥¬gWù"{ºIöŒù|þSö2Zƒ÷½ÊƒÁ Ö×Ñ€¹àÞ7™0 ÌËÛ:Ø*{—Á9ïÛƒYùP{d³`îÈ>eÆÙ縠·_ ƒÚ¿¦ýÿVSö#†ìW[y˜@•[¹×ÅyØ)òp9pU>1È#¤ÀVyÄòH!òÈ©ñM5©ËÓm§À5y†IòŒéä™"¡–<3ïÈ‹åYSb¢<[|¬—gOîÌÑXž3óä¹¢¿å.%ϳOž÷”<Ù\)/Ä; –ËŽÆØ&/ž3ä%¢-¾ÊK.’—*ƒGòÒcñ·¼Lx„b6ƒ>•‚,è5òráQdPÞQ1GðV^! ¡ †à~É+–AW¬ÆePG¥Ü‹?å•S£6Ê«¤D/\—W Æ-yµÒ §Õ«€{jTÂ"oÍXŸòZ]ðP^».¸»NiìÔ‘¶Èƒ˜­`w†ÔÄ y(ýQ7åuGÉëåÁyýrØ%o=ñLÞ°!˜ÅFÁx-oÜ/åM˜ã¦)A]ÍȺy~<•·àç–AòV‘qGÞú€¼ ÷µí&o—÷åí©¥ÃyǼè úØ)±[Þ9È»dÆ Mט2è–]À,t†RÞУ%˜™žÅÐdл¾¼ïîKžýÈs5d–Uû:¸4fʇÄDy 9­ 2–mAîÃãè判h‡Íàs# ‚\F õ1d2º(za=îÊÇdG/ÍØ`Pã¸2ŸípB>!¦ÝœØßä“òb¡|rlošRÌÓÔü脵à³Ó&âŒ|z8dF 0ß3⡽|f´¹Î*€. «ÙÑìÙœhèj˜Þ0/)‚1gåó…|‚­x"_PÔ¹° ØÙEÉÐK@Í‹³á£| ?/­Žø _VÌÄò–XŒò`ŸVÃT0ƒ«òb*¨guYð¹5%@žk“‚Ù]ÌÏúü`Ï7 –o rÛTœ³™œ·Ì‘oe¶…Ê·SëΨ8&ßE¯v'Åò=mA¦{9_t´Æiùþ<à=úÈ6Ä;ù!r9œÌþ‘Ø)?Z ûåÇØ¹ãr:1L~2 8÷ÿÝifü ówŽ]9ìÞææ"÷\Ž+¿ÂùW™­kdv~ý™ ÷äU9ü=—åÿ0ãÿ–Åðþÿ&€Ùú?xË Þpó‘œÿ'ûtò»}å÷ð\~ÿüûÿ|Dÿžt–?e_žÝ”?gÆ^°G/‹Ë_%—¿Î%S Ûåo§þ½ë.|ÿ¡&þ•$ïO5pMþy.à»üK2Tý `oHÅl_<õÖ€Öâ¤ú5üÙ°ÑxÚÌÖ¬$YÆñºcký°/=¶mÛ¬ííQÍÎØ¶m«³¶mÛ¶íÝÞ’¯7w+wÌ—üò{ªÏ-$UyYž÷{ü‘I!ñ0 ËÁ³òà ¸1‡EÈŒø ˜¼/’xìŽ C•‡,J$þ¼šb?vk=/`–C8`¨CVeP-x©ÈáÐ ã,½OP>¬ÇZ #аt†K$3¤lFÔ!Ž<¸Ía‡2®L:Œk¤?²YY 5çuÈiÕåHµž Æ÷Yž7Eï/Äf`TÀN‡Ê Žðj ™Ãnã¡u1 ·È;(‰í–¼˜† –a’‚îX„ˆr7–!Œâr%RÑsѱ!ìÕoJÂS= “q¬÷8ñ™My1"èÝÖµÆÔK^GIÅ$Ä‘×úŽôFH""Éýra‚D,×#Ír5nAIÍEàiÿnX³Ï&)ú^D,ù°-×H tGQeòÜ‹fÒ50»Ñ-pMñ:*#¹Q q¬Àý÷©$ú£ˆD±ï 6á”S½µ¤Ø×ÌÑo¯A³Ñ["(ÉýFb£D- Že >CÖ! Q T!Dp ZèxH¢h‡ÑªË¨?›ÐE´n¨ù•••­1T{UEËä>Ê!ª+c42PÕu0+Tߤ~u­ð<[’Å~Ä&Í-=“]ëí:þ:êxLs}Ñ Ô»KëFˆ¢4Ê«î¯ùTÍE%¸î¯5}Öª•…pYò˜²2k]KIO³PÝ/Сùv:Ö|WÍtr 6c¦a V``Fa *ÈÅÊʤtA {×B!dèFS?wb*^¦ÿ9jQßNÎWf"o"?Fq\‡OéÁÄt-½à±6ððˆö¢NÜÍz?*h~âø 6÷^|BFþ”œ³2›¬ß˜A‰¦€÷¢<˜Çå ÖàWjLf*ù– oS—KÒ1˜ôíÚÒ\gªÞò)Pý¸êRäßdgÀó“›QõBeÜEýtþSR®ÁU¸{ñ²!<ŒDLÀJõš¡„êÊ,¨¬Ì‰+Peq9<”Gƒ4çË$4EŠøhÌýÝOöGCø²Gÿ›ú’-Ás1“·€>T+cú£=|Y…Ž{JáËõ¨¤ÚÖ3°Ìš|’f׺ç˰ýÐ_ùt“ªsÀü¢wär-ëûtŽÆh ðOR¥K'ñ£»œð9NcOg_ÂâÔ—q©ÐØâ¤!,Ùðè'þ¬ë8=µ‚ß4zŸYøffÒ·S=¡ÿ0Iš·H¾$ô]ySù±î%&¥#&Þ—þÁ^4Æ­7ƒµ’ò¨>žºh­Z³VºûªáV ·a³•w !*[J¢,jˆæÍo:‡s-yàʰÔ$kLÓZ©½\Ü÷û+³*-:®”¬V®FWŒ·²ÌÔö=Ÿ¸7…ú¸úʵ’í4¼yÙÄI÷ÿ˜¾yà¸ÖJ€9^߉ãç½ê˜êCÔÃ`Óóc­{³¿^&Csxr¤I%ka+þm`…Ž¢8ü;Ù5dÛ5ä†lÛ³mÛ¶mÙc ÙMÏ6þøÞÞÝÙw±{Bò A)']Ê}¡&Œ#ÖÝKä…ËaÔ$Ö]𧢯O¼¬£þJÇiübèuð» zLƒ=@Ü›À¬/nÏ8§à¥Û55½ånul†g÷‚ÆnßxX vzÅóküH)»5ÿ’jwöée½ ”°æyXc›oÇ‘ÏVY‰6šÛ_½·ÆègÖÏJQ_Lµ‘•z¤+š¯­*¡ZHd„ãƒsš¢1ª„]Uƒ4&¸íŸõ—úƒá^d°ŽêJj£õêGÕAÆŸletKÃÕRG‰•Ñ!&Ð*:«—º«£•¡â!>BEGúÒ×RåT=HE×…–VAéVßjZ5«b%©òt,ȶÏtÖÖNÐBz·Ò7S×4Mg$­Å+®Â*Æ–+¢ÍÜõ_¸~6µ >XE&M bõŠn…­ÔÉÞxÚc`d``Ïý'ËÀÀrñ—éfÖ @Àº„áxÚc`a¹È´‡•Õ˜å ÃLÍt†!IHssp23ËR¶÷ ;~3@Ap3ƒÿÿl"ÿDg±ç2>V``œÿ:K+Ê2D‰xÚ|%I@³«²×˜9.ÿUÕŸžë¼¶mÛ¶mÛ¯mÛöŽn{þÚ6þet¬õ"^f!ø€‘œü‰¦foL÷@+B—”PÒC0ü }´¾Ú-B‹Ö–kW57ËÊ6°ì;Á’xj®¸“çåÕx}>„à£ø$Áðå|ßÌð£üfA'æÀ‚XbGœ‰1¸ 7án܇ñ¹î©g׫èÕõºúP}D†¼/<^9;¶8:ž‹4¡âo‘CäÅEIQW´ÝE1F,kÄù‹TÒ[–—uåp9[ΗKåz¹Aî’Iòº¼§~T™”PN¨r«bª®j®ú«‘j’šª¢Õµ^íU'Ôuu׉δN§t.5˜‘ÖÈe”5Z}IÆ\Ãíݬbv6'™áf„¹ÌÜ`n5wš{ÍDÓ2ošO}‡ùNó½éûÒü‚ýZú§÷ÏÐ;(SÐoAFPi­zãv»ìŽB „SÇpê¸L»¢½a™¨ã&¶—fç9ð4ÔÑËî8ˆ£ŽcødÅòÔq ?H³¢æÄBØ;à8ŒÄ…¸·ã><„Çt=‹JkèõõáÔñ¸ÇKG?ÇfÇÇ3ÂC‹0ñ¯È% ½íØIô#D¸X"Ö½íXNV¤Žãä¹@.£ŽåniÉ›ò¾úUe¡Ž¦úCåWÅU=ÕZ P£Õd5M­¤Ž;ÕowLE…s¦7Ò¹rFk£Ÿ1Ù·;6£ŽÓ¨c¤¹ÂÜH÷د˜}‡úN¥Ž/ìŽ-üuÿ_ÞvTÔ1ÜZnwÔÜ´—4ÿÀpx GΘýÃz±ê¬'kÈjÒ¹5kE³$+A³ÔëǬ$ÀëͯnÒö†oòª?ô]îU×׿£1÷ƒ¸:’,íZ „«¥«†½³¸*^^y ظ|.^ª”üøÒYk½ä#ó»Â\ìÿÜ`“Œ`cÝ%¯‘.Ò²NY'hï°6X«“C¬YÜtKOþD¦¸pà¿Ë´¯$UMȘ‘8'qob,@bLBøí VBRü–ĶqpþAÜ©¸=q;â&ÆU"®Îé2§‹œÎa¬4bøNÐìzÈ:Ú $æ±|¾]r!¹”¯‡÷ðíÍýoߎ’§0‚M°¶À~8§á ŒÄhŒÅø >ášoìó+ÝÝO·èaz½íjï_*‘-ÈoïCôõ4÷§H—"@ŠL)¼RüE»Ylízð)j½µ¹™|2|BÊýðRnNyøíé(|€³Ò¬2+Çöp®·gUXGÖ‰â)Xq6æÊ1 3 oßv—™1g/¶m;³m•qR·©mÛ¶³¶më7<˜ “oâMž¹aΚ‹&Sþ”½<Ës¼Œ5.¼Ê[¼ƒ;xáOa$‘B*™äñ ßó¿ò?r7Æü.íM4ñ?¥”Ñ•ž f#Í4¦3“ù,`[ØÆvjh¢#å§¹`~‘ßͯæ¨Y`ÒÌúG‡ÙhÊòŸ4È?\Q7sÛTÉ¿Ò"í·0‘&JªÕCþ6±Rg"¸ªî&Èø™`“ªOð4ÂS<Ë8bƒö|È»¼Çû¼B(Q„A‘æq|Á§|ÆçüN2t¢(¦#E”PI?zч*&0–qŒ§KYÅrV°‰•æ$ØË.v³‡3låt6kiÏ“ò<åê«~¼D^ KºcEœèÏâ@_œ„-½Õ_x¡¼ÉÞ`¸jo3 7¦ò™Äd\™ÂŒÑMÔ` Á“š†7³ðe.>ÌÖ$MÆy²`Ä"Í „%IJ–u¬'†5$²™4vA5éì$‹ZÐHr¨'›:MÑT h!Ÿf¾ã0_s@C5Œo9ÄWìçòÇùƒ³üÉ?\ä?.ó/—˜C4«ù’}¼Î0âÙH. |L«ÔK­l’ÍR#Ò&­²Kš¥P:I‰K©”I¹TH¥©—zËU¹%·ÕFöÉ~9ðع#wÕIÕE­Ô^­ÕNõäÈ= Pß„u¶Ódk]ß¼ò7gº¶¢aÙ’UkÖ-X´aØ ÓQ‰jÔ¢­hGO`ÀQc&Œ›4eμY+6Í8pèȱ3ç®\»qëÞ“g¯^¼y÷íÇ—¤œO.åHø—’–ñëOÖƒ;>l+ØQŒ¦S»Jö”í÷ ‚‡ƒ( ÿýq²m»{ NímÛž*š¡÷Ì+´ È¢bK^½)±¬ÔŠ2«Ê­©°®Ò†*Õ¶ÔØVkG]›êíi°ï݇Fšjv¤Å±V'Úœjw¦Ã¹Nº\êv¥Çµ^7úÜêwgÀ½A†<ödijQ/ùœ1¿1_1?1¹•Û¹žk¹š;9žcyôO<(yP­û¹¿okÛζe×8Û£üi˜ëU²µûW¶m{ÏÑAÒíÕ>íÑáhK´Uã4G5I“5WLj(ÒTÍÓ|-Ð@-Ô"ÍÒb-¡˜JµA)£œ ­Ô*µê”ÚtAS\—”PR)Õ¹0F÷t_C5,Ô…úÐCShÖHÒhÑ ®pŽ \¢­œç"—Iä6wxÃ[Þñž»ÜtšÓág:ËÙîärW¸ÒÅÎu¾ ]ê2—8Ï.r•«]ãZ×¹Þ nä%¯ÜÙMnv ¿øí^îMÜ}Hq•k\ç·Ü×ýÜŸ³T!½£Z¯C?½×+ý£†|j©£ž4(Ðõ˜&²h¦…L}¢£ê§~ëgô™.t¥nt§=éEoúG_úÑŸ Ôs1Xô_o™ÆtýÐGf0“YÌ& :°ŠlrXÍÖ²NÏØ¤§lg;õ’]úª/>•ø˜Û ‚+(`?{Ô¶mL\œv»ä£¢ª?u ÍøjiëÄ_WO?rC#cS³(Ì-,­¬ml#µ³wptrv‰ÌÕÍÝÃÓË;’’ x0Bxøa³yFX+Ûv]n‹þ͵íŒ55tìlôõt ÍÔÔµ¬mL-­Lœ-$…„EDÅÄäåde¤¥•UT%]\ÝÜ=<½¼}|ýþÁãY¨Ð{¿Î›yrmk…0E¶m?y¡þf[ç8qêØ™CG^}Fjdzói‘Iðî;Ò#Û‡ŸÈˆœH‰°i˾zL›2«[Ÿ~5dؼ‹–¬Z³nò•È‹ü(ˆ\s.\ºríÆoü‹ÿzݺsïÁ˜q&£$ £Ø£'Ï^¢Hz š5iÔ¦U‹*¥*”©V®RZÛvìÚsžt&]I{Òñ4!3ÖxÚ¬V×zÛÆ^°©÷vŽá2ðšŠB,`¹«€ ã®ú}ØT@¤}Ÿ\å‚§0ý.–€T/i¶¤Ù²3ÿ?³K¢P€â0 4ÀëßÄàîk¬ìâStô’Ã Õø÷~Ñ/Z-ydZ —õŽ0„y !úè`AAð,ÍÞY0†ü `%-,VõÞ¡%-3 wvHµ¥MÀ%^-i iî·qTÝà"ÛÉ“N ªIbÀÁ0" °mWÏyõ<2#­µIÕâ ßB±¢xÍÎíÍ×x—Ww_Ç¿‹{üVGZ·c†­µD±~ÐÚÁ¢Ê\ªÆ„¥ìï„X–V¤GÈÉ5r°¤$!vZ>ò€-ŒÑÌkæ¿X‰‚k}H ¡éb¹J´ì†ÑŽïéPjKní‡d3‰Œ^~Ë û|»# 9·ÚJO éÅX8úˆF‹ªÀrÍÁ>\ê0a)‰#àp+ÒìÕ³RûU§oXøW³Ž»5 Îvo0?Ű%é wA"chw&wÁÄ­c–¨Ÿ2®ç)†®Ç%(ê² a•ê iTM¸¢—2^’ü’^ëa2‹½§ÃCÏ>¬¥ec&éd”r†®õy¬àYVù%º>ÁÅœhØTËåz1û“àõ ¹”>6fj¥qð)QÍ+  ðã%Ÿ)wnÍÁç7¹¢á·ÈýuIÌVÁ…& É×åe’4eSÆ%hÐ3ñÜ0f¦‰ä%E•ṕªô“¹¥ƒÂÃßþ¸`-¡3—Ϻ›Ÿ‡%éõ¼#~T¶và PóÇÂ|ñ–öø¡í÷!‘Y„lЧ†Ÿ«ž»ü“¨àGm‰E?nï„´‰MZGüЉ%PúyÙ 6KÊÐ œ$²,\–DrI–ˆ$–iæÊN¥U5+‚þîäOéI.íàJ my¾Ë…\#šV{&œÙ²ÉI¹‹k=[&gÅAèš´LÒô”tèq+°R¥ÝËÓßò&^6ðÝnIžúõS•ø½vEü¥â<ä^‹7”—Ylà„î˜{:„5í¦®1m;¸yƺgn]{]ĶÂ%ûº„žÂe;¡ÚxÆÔ•®ÔP]Šð3È<Ÿó9ó1J/‡Î*a \¹Ô=¿No}ØPÈ?éæ5ÅŒ‰Ÿ²5I¯Õ©y±t·Î@¥bÉî±Ò Ý²mÉ./ré,M…b&¿öÁ7|ÊŧtË?»Bÿ’Ž3¦§ð­_)|Aâ5³ÝÐø³Çz@Ž(0üÝ#ÅL.Û¶½±¹±y™ÕÓ÷ì+~úYÿÉ=ü¹CŽt~P8ìŠAÐ%„`ÔÃŒ B0NÓ-˜ “4`ŠL»Rí¢Q¨šu¥Ðþ6' Uót!Z S-Ò©–èTËœÙ)X„`•3Áš ë4=‚ °I¶h@FïÕ!Ú…ªÎSíŠBÕžÞ íÓ©èT‡tª#yŒÒ'ðX§|‹ðÄØ*<åAשM¦3W}sn¤¹Ð&ôÍ¥,nLw½b²×FVÜɳ²nwF‚{i›Òý˜,4’?ÉŸe¥^Œ¯F‚7i›ÓýÞ™,ÿ0’É¿d¥¾?F‚_WªI¢òOÛŽº|u&׎eËßÒõÿ™ÖxÚ=ȃnEA…áu´g×¶ ˆêLtÔ¶Ã*®í>ËÔFܧ*.׊þOøî¼Ö´«!¥=33Ë×î#Ö&|ÍU„jÙñæ¼6fåu¥¿žþ„ʹ®ðà ï@3í“™Þ ûgÇo™‹ÜË’êE>Slõ“ö{¨ÌËkRÑø­5Kò§L6uxÚc 0ª2ª2|`øÀ’Àz•%á_8{î?YÖOÿùÕÿCø`•ïÞÕAT…U}øÿ¤ÂcÔfÔf¸Âp…E‹õí¿<ˆÍÆf#Ì8aI€éc=òÿ›:„] u`Û"`¶ù@u>Ã* <ÍàÂÐÃÐþš%$ƾ‡%DÜÎNYxÚ……j<0Ãçwø_&x9dîînøÜÝí\ßqøð=ÌÂG™ ¥!¡i>bì"Này€¸”q‚âŽòäy˜¿zÏ#Ú@³çQþ'™!M†*9®¹äŠŽFNiÂÑM'] Š­ÊU”ç”s©yS)ÎÈQ•ž’*èoZ:/Ý¡ûK®N؉ýŸ–§@^7§ücîiűDJþvË¿Óq¬¼dåM“×ÍQžÉ¹¨¿§Ü*/-ÿÙ»®ß÷<Œ{q»ÿ0/3ñºaµò\K§pôÐîçaWØ¥ÓË…°›>úÙ§Íæ¹³™Ô©|Ž‚Î…´6Fš+¥¥ünJtÑ΀m¨Íæí7–öÑi|ëÐ«Š°ËóE{é÷jVxÇ1R¦/>è{aòeƶ—ÛXcŠvù*ºIó–… Æ6 †[–ðEÆ3€Ë_ÔxÚc`fƒÿ* Q@Š‘ #‚ÿÿxÚ<Œn@†¿™ÝÔ¶mÛVPÛFP#nXž¢èmz¡j6yÁÌoˆcgtbjf…¤‹Ã‡+òð??B :;?SBÎÒül eû h9üÙÝá1yvË.®/¨±Û(е‹]ÁG%” ¦Ä D!–[å Q?‰"vxä¢H"‹"ªh¢Kqº¡íQ%ö[´ß~sT…ýVíµß¤Ýö¼ö G )äèa£Œ°16|TyÐP¢HÒaB²2¬ÛÚHТ´A§ ´Kh†Îc'RLambdaHack-0.11.0.0/GameDefinition/game-src/Client/UI/Content/0000755000000000000000000000000007346545000021614 5ustar0000000000000000LambdaHack-0.11.0.0/GameDefinition/game-src/Client/UI/Content/Input.hs0000644000000000000000000002610407346545000023252 0ustar0000000000000000-- | The default game key-command mapping to be used for UI. Can be overridden -- via macros in the config file. module Client.UI.Content.Input ( standardKeysAndMouse #ifdef EXPOSE_INTERNAL -- * Internal operations , applyTs #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Client.UI.Content.Input import Game.LambdaHack.Client.UI.HumanCmd import Game.LambdaHack.Definition.Defs -- | Description of default key-command bindings. -- -- In addition to these commands, mouse and keys have a standard meaning -- when navigating various menus. standardKeysAndMouse :: InputContentRaw standardKeysAndMouse = InputContentRaw $ map evalKeyDef $ -- All commands are defined here, except some movement and leader picking -- commands. All commands are shown on help screens except debug commands -- and macros with empty descriptions. -- The order below determines the order on the help screens. -- Remember to put commands that show information (e.g., enter aiming -- mode) first. -- Minimal command set, in the desired presentation order. -- A lot of these are not necessary, but may be familiar to new players. -- Also a few non-minimal item commands to keep proper order. [ ("I", ( [CmdMinimal, CmdItem, CmdDashboard] , "manage the shared inventory stash" , ChooseItemMenu (MStore CStash) )) , ("O", ( [CmdItem, CmdDashboard] , "manage the equipment outfit of the pointman" , ChooseItemMenu (MStore CEqp) )) , ("g", addCmdCategory CmdMinimal $ grabItems "grab item(s)") , ("Escape", ( [CmdMinimal, CmdAim] , "clear messages/open main menu/finish aiming" , ByAimMode AimModeCmd { exploration = ExecuteIfClear MainMenuAutoOff , aiming = Cancel } )) , ("C-Escape", ([], "", MainMenuAutoOn)) -- required by frontends; not shown , ("Return", ( [CmdMinimal, CmdAim] , "open dashboard/accept target" , ByAimMode AimModeCmd { exploration = Dashboard , aiming = Accept } )) , ("space", ( [CmdMinimal, CmdAim] , "clear messages/show history/cycle detail level" , ByAimMode AimModeCmd { exploration = ExecuteIfClear AllHistory , aiming = DetailCycle } )) , ("Tab", memberCycle Forward [CmdMinimal, CmdMove]) -- listed here to keep proper order of the minimal cheat sheet , ("BackTab", memberCycle Backward [CmdMove]) , ("A-Tab", memberCycleLevel Forward []) , ("A-BackTab", memberCycleLevel Backward []) , ("C-Tab", memberCycleLevel Forward [CmdMove]) , ("C-BackTab", memberCycleLevel Backward [CmdMove]) , ("*", ( [CmdMinimal, CmdAim] , "cycle crosshair among enemies" , AimEnemy )) , ("/", ([CmdMinimal, CmdAim], "cycle crosshair among items", AimItem)) , ("m", ([CmdMove], "modify door by closing it", CloseDir)) , ("M", ([CmdMinimal, CmdMove], "modify any admissible terrain", AlterDir)) , ("%", ([CmdMinimal, CmdMeta], "yell or yawn and stop sleeping", Yell)) -- Item menu, first part of item use commands , ("comma", grabItems "") -- only show extra key, not extra entry , ("r", dropItems "remove item(s)") , ("f", addCmdCategory CmdItemMenu $ projectA flingTs) , ("C-f", addCmdCategory CmdItemMenu $ replaceDesc "auto-fling and keep choice" $ projectI flingTs) , ("t", addCmdCategory CmdItemMenu $ applyI applyTs) , ("C-t", addCmdCategory CmdItemMenu $ replaceDesc "trigger item and keep choice" $ applyIK applyTs) , ("i", replaceDesc "stash item into shared inventory" $ moveItemTriple [CGround, CEqp] CStash "item" False) , ("o", replaceDesc "equip item into outfit of the pointman" $ moveItemTriple [CGround, CStash] CEqp "item" False) -- Remaining @ChooseItemMenu@ instances , ("G", ( [CmdItem, CmdDashboard] , "manage items on the ground" , ChooseItemMenu (MStore CGround) )) , ("T", ( [CmdItem, CmdDashboard] , "manage our total team belongings" , ChooseItemMenu MOwned )) , ("@", ( [CmdMeta, CmdDashboard] , "describe organs of the pointman" , ChooseItemMenu (MLore SBody) )) , ("#", ( [CmdMeta, CmdDashboard] , "show skill summary of the pointman" , ChooseItemMenu MSkills )) , ("~", ( [CmdMeta] , "display relevant lore" , ChooseItemMenu (MLore SItem) )) -- Dashboard, in addition to commands marked above , ("safeD0", ([CmdInternal, CmdDashboard], "", Cancel)) -- blank line ] ++ zipWith (\k slore -> ("safeD" ++ show (k :: Int) , ( [CmdInternal, CmdDashboard] , "display" <+> ppSLore slore <+> "lore" , ChooseItemMenu (MLore slore) ))) [1..] [minBound..SEmbed] ++ [ ("safeD96", ( [CmdInternal, CmdDashboard] , "display place lore" , ChooseItemMenu MPlaces) ) , ("safeD97", ( [CmdInternal, CmdDashboard] , "display faction lore" , ChooseItemMenu MFactions) ) , ("safeD98", ( [CmdInternal, CmdDashboard] , "display adventure lore" , ChooseItemMenu MModes) ) , ("safeD99", ([CmdInternal, CmdDashboard], "", Cancel)) -- blank line -- Terrain exploration and modification , ("=", ( [CmdMove], "select (or deselect) party member", SelectActor) ) , ("_", ([CmdMove], "deselect (or select) all on the level", SelectNone)) , ("semicolon", ( [CmdMove] , "go to crosshair for 25 steps" , Macro ["C-semicolon", "C-quotedbl", "C-v"] )) , ("colon", ( [CmdMove] , "run to crosshair collectively for 25 steps" , Macro ["C-colon", "C-quotedbl", "C-v"] )) , ("[", ( [CmdMove] , "explore nearest unknown spot" , autoexploreCmd )) , ("]", ( [CmdMove] , "autoexplore 25 times" , autoexplore25Cmd )) , ("R", ([CmdMove], "rest (wait 25 times)", Macro ["KP_Begin", "C-v"])) , ("C-R", ( [CmdMove], "heed (lurk 0.1 turns 100 times)" , Macro ["C-KP_Begin", "A-v"] )) -- Aiming , ("+", ([CmdAim], "swerve the aiming line", EpsIncr Forward)) , ("-", ([CmdAim], "unswerve the aiming line", EpsIncr Backward)) , ("\\", ([CmdAim], "cycle aiming modes", AimFloor)) , ("C-?", ( [CmdAim] , "set crosshair to nearest unknown spot" , XhairUnknown )) , ("C-/", ( [CmdAim] , "set crosshair to nearest item" , XhairItem )) , ("C-{", ( [CmdAim] , "aim at nearest upstairs" , XhairStair True )) , ("C-}", ( [CmdAim] , "aim at nearest downstairs" , XhairStair False )) , ("<", ([CmdAim], "move aiming one level up" , AimAscend 1)) , ("C-<", ([], "move aiming 10 levels up", AimAscend 10)) , (">", ([CmdAim], "move aiming one level down", AimAscend (-1))) -- 'lower' would be misleading in some games, just as 'deeper' , ("C->", ([], "move aiming 10 levels down", AimAscend (-10))) , ("BackSpace" , ( [CmdAim] , "clear chosen item and crosshair" , ComposeUnlessError ClearTargetIfItemClear ItemClear)) -- Assorted (first few cloned from main menu) , ("C-g", ([CmdMeta], "start new game", GameRestart)) , ("C-x", ([CmdMeta], "save and exit to desktop", GameExit)) , ("C-q", ([CmdMeta], "quit game and start autoplay", GameQuit)) , ("C-c", ([CmdMeta], "exit to desktop without saving", GameDrop)) , ("?", ([CmdMeta], "display help", Hint)) , ("F1", ([CmdMeta, CmdDashboard], "display help immediately", Help)) , ("F12", ([CmdMeta, CmdDashboard], "show history", AllHistory)) , ("v", repeatLastTriple 1 [CmdMeta]) , ("C-v", repeatLastTriple 25 []) , ("A-v", repeatLastTriple 100 []) , ("V", repeatTriple 1 [CmdMeta]) , ("C-V", repeatTriple 25 []) , ("A-V", repeatTriple 100 []) , ("'", ([CmdMeta], "start recording commands", Record)) , ("C-S", ([CmdMeta], "save game backup", GameSave)) , ("C-P", ([CmdMeta], "print screen", PrintScreen)) -- Mouse , ( "LeftButtonRelease" , mouseLMB goToCmd "go to pointer for 25 steps/fling at enemy" ) , ( "S-LeftButtonRelease" , mouseLMB runToAllCmd "run to pointer collectively for 25 steps/fling at enemy" ) , ("RightButtonRelease", mouseRMB) , ("C-LeftButtonRelease", replaceDesc "" mouseRMB) -- Mac convention , ( "S-RightButtonRelease" , ([CmdMouse], "modify terrain at pointer", AlterWithPointer) ) , ("MiddleButtonRelease", mouseMMB) , ("C-RightButtonRelease", replaceDesc "" mouseMMB) , ( "C-S-LeftButtonRelease", let (_, _, cmd) = mouseMMB in ([], "", cmd) ) , ("A-MiddleButtonRelease", mouseMMBMute) , ("WheelNorth", ([CmdMouse], "swerve the aiming line", Macro ["+"])) , ("WheelSouth", ([CmdMouse], "unswerve the aiming line", Macro ["-"])) -- Debug and others not to display in help screens , ("Escape", ([CmdMeta], "", AutomateBack)) , ("Escape", ([CmdMeta], "", MainMenu)) , ("C-semicolon", ( [] , "move one step towards the crosshair" , MoveOnceToXhair )) , ("C-colon", ( [] , "run collectively one step towards the crosshair" , RunOnceToXhair )) , ("C-quotedbl", ( [] , "continue towards the crosshair" , ContinueToXhair )) , ("C-comma", ([], "run once ahead", RunOnceAhead)) , ("safe1", ( [CmdInternal] , "go to pointer for 25 steps" , goToCmd )) , ("safe2", ( [CmdInternal] , "run to pointer collectively" , runToAllCmd )) , ("safe3", ( [CmdInternal] , "pick new pointman on screen" , PickLeaderWithPointer )) , ("safe4", ( [CmdInternal] , "select party member on screen" , SelectWithPointer )) , ("safe5", ( [CmdInternal] , "set crosshair to enemy" , AimPointerEnemy )) , ("safe6", ( [CmdInternal] , "fling at enemy under pointer" , aimFlingCmd )) , ("safe7", ( [CmdInternal, CmdDashboard] , "open main menu" , MainMenuAutoOff )) , ("safe8", ( [CmdInternal] , "clear msgs and open main menu" , ExecuteIfClear MainMenuAutoOff )) , ("safe9", ( [CmdInternal] , "cancel aiming" , Cancel )) , ("safe10", ( [CmdInternal] , "accept target" , Accept )) , ("safe11", ( [CmdInternal] , "wait a turn, bracing for impact" , Wait )) , ("safe12", ( [CmdInternal] , "lurk 0.1 of a turn" , Wait10 )) , ("safe13", ( [CmdInternal] , "snap crosshair to enemy" , XhairPointerEnemy )) , ("safe14", ( [CmdInternal] , "open dashboard" , Dashboard )) ] ++ map defaultHeroSelect [0..9] applyTs :: [TriggerItem] applyTs = [TriggerItem { tiverb = "trigger" , tiobject = "consumable item" , tisymbols = "!,?/" }] LambdaHack-0.11.0.0/GameDefinition/game-src/Client/UI/Content/Screen.hs0000644000000000000000000000365007346545000023373 0ustar0000000000000000{-# LANGUAGE TemplateHaskell #-} -- | The default screen layout and features definition. module Client.UI.Content.Screen ( standardLayoutAndFeatures ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Data.FileEmbed (embedDir) import Language.Haskell.TH.Syntax import System.IO import Game.LambdaHack.Client.UI.Content.Screen -- | Description of default screen layout and features. standardLayoutAndFeatures :: ScreenContent standardLayoutAndFeatures = ScreenContent { rwidth = 80 , rheight = 24 , rwebAddress = "http://lambdahack.github.io" , rintroScreen = $(do let path = "GameDefinition/PLAYING.md" qAddDependentFile path x <- qRunIO $ do handle <- openFile path ReadMode hSetEncoding handle utf8 hGetContents handle let paragraphs :: [String] -> [String] -> [[String]] paragraphs [] rows = [reverse rows] paragraphs (l@"" : ls) rows = case (rows, ls) of (('=':'=' : _) : _, _) -> -- A title. No new paragraph. paragraphs ls (l : rows) (('-':'-' : _) : _, _) -> -- A title. No new paragraph. paragraphs ls (l : rows) ((' ':' ':' ':' ' : _) : _, (' ':' ':' ':' ' : _) : _) -> -- At least four spaces before and after; probably a code block. paragraphs ls (l : rows) _ -> reverse rows : paragraphs ls [] paragraphs (l : ls) rows = paragraphs ls (l : rows) intro = case paragraphs (lines x) [] of _titleAndBlurb : par1 : par2 : rest -> (par1 ++ [""] ++ par2, rest) _ -> error "not enough paragraphs in intro screen text" lift intro) , rapplyVerbMap = EM.fromList [('!', "quaff"), (',', "eat"), ('?', "read")] , rFontFiles = #ifdef USE_BROWSER [] #else $(embedDir "GameDefinition/fonts") #endif } LambdaHack-0.11.0.0/GameDefinition/game-src/Implementation/0000755000000000000000000000000007346545000021434 5ustar0000000000000000LambdaHack-0.11.0.0/GameDefinition/game-src/Implementation/MonadClientImplementation.hs0000644000000000000000000001272007346545000027075 0ustar0000000000000000{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | The implementation of our custom game client monads. Just as any other -- component of the library, this implementation can be substituted. module Implementation.MonadClientImplementation ( executorCli #ifdef EXPOSE_INTERNAL -- * Internal operations , CliState(..), CliImplementation(..) #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import qualified Control.Monad.IO.Class as IO import Control.Monad.Trans.State.Strict hiding (State) import Game.LambdaHack.Atomic (MonadStateWrite (..)) import Game.LambdaHack.Client import qualified Game.LambdaHack.Client.BfsM as BfsM import Game.LambdaHack.Client.HandleAtomicM import Game.LambdaHack.Client.HandleResponseM import Game.LambdaHack.Client.LoopM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.MonadStateRead import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import Game.LambdaHack.Server (ChanServer (..)) data CliState = CliState { cliState :: State -- ^ current global state , cliClient :: StateClient -- ^ current client state , cliSession :: Maybe SessionUI -- ^ UI state, empty for AI clients , cliDict :: ChanServer -- ^ this client connection information , cliToSave :: Save.ChanSave (StateClient, Maybe SessionUI) -- ^ connection to the save thread } -- | Client state transformation monad. newtype CliImplementation a = CliImplementation { runCliImplementation :: StateT CliState IO a } deriving (Monad, Functor, Applicative) instance MonadStateRead CliImplementation where {-# INLINE getsState #-} getsState f = CliImplementation $ gets $ f . cliState instance MonadStateWrite CliImplementation where {-# INLINE modifyState #-} modifyState f = CliImplementation $ state $ \cliS -> let !newCliS = cliS {cliState = f $ cliState cliS} in ((), newCliS) {-# INLINE putState #-} putState newCliState = CliImplementation $ state $ \cliS -> let !newCliS = cliS {cliState = newCliState} in ((), newCliS) instance MonadClientRead CliImplementation where {-# INLINE getsClient #-} getsClient f = CliImplementation $ gets $ f . cliClient liftIO = CliImplementation . IO.liftIO instance MonadClient CliImplementation where {-# INLINE modifyClient #-} modifyClient f = CliImplementation $ state $ \cliS -> let !newCliS = cliS {cliClient = f $ cliClient cliS} in ((), newCliS) instance MonadClientSetup CliImplementation where saveClient = CliImplementation $ do toSave <- gets cliToSave cli <- gets cliClient msess <- gets cliSession IO.liftIO $ Save.saveToChan toSave (cli, msess) instance MonadClientUI CliImplementation where {-# INLINE getsSession #-} getsSession f = CliImplementation $ gets $ f . fromJust . cliSession {-# INLINE modifySession #-} modifySession f = CliImplementation $ state $ \cliS -> let !newCliSession = f $ fromJust $ cliSession cliS !newCliS = cliS {cliSession = Just newCliSession} in ((), newCliS) updateClientLeader aid = do s <- getState modifyClient $ updateLeader aid s getCacheBfs = BfsM.getCacheBfs getCachePath = BfsM.getCachePath instance MonadClientReadResponse CliImplementation where receiveResponse = CliImplementation $ do ChanServer{responseS} <- gets cliDict IO.liftIO $ takeMVar responseS instance MonadClientWriteRequest CliImplementation where sendRequestAI scmd = CliImplementation $ do ChanServer{requestAIS} <- gets cliDict IO.liftIO $ putMVar requestAIS scmd sendRequestUI scmd = CliImplementation $ do ChanServer{requestUIS} <- gets cliDict IO.liftIO $ putMVar (fromJust requestUIS) scmd clientHasUI = CliImplementation $ do mSession <- gets cliSession return $! isJust mSession instance MonadClientAtomic CliImplementation where {-# INLINE execUpdAtomic #-} execUpdAtomic _ = return () -- handleUpdAtomic, until needed, save resources -- Don't catch anything; assume exceptions impossible. {-# INLINE execPutState #-} execPutState = putState -- | Run the main client loop, with the given arguments and empty -- initial states, in the @IO@ monad. executorCli :: CCUI -> UIOptions -> ClientOptions -> Bool -> COps -> FactionId -> ChanServer -> IO () executorCli ccui sUIOptions clientOptions startsNewGame cops@COps{corule} fid cliDict = let cliSession | isJust (requestUIS cliDict) = Just $ emptySessionUI sUIOptions | otherwise = Nothing stateToFileName (cli, _) = ssavePrefixCli (soptions cli) <> Save.saveNameCli corule (sside cli) totalState cliToSave = CliState { cliState = updateCOpsAndCachedData (const cops) emptyState -- state is empty, so the cached data is left empty and untouched , cliClient = emptyStateClient fid , cliDict , cliToSave , cliSession } m = loopCli ccui sUIOptions clientOptions startsNewGame exe = evalStateT (runCliImplementation m) . totalState in Save.wrapInSaves cops stateToFileName exe LambdaHack-0.11.0.0/GameDefinition/game-src/Implementation/MonadServerImplementation.hs0000644000000000000000000002017307346545000027126 0ustar0000000000000000{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | The implementation of our custom game server monads. Just as any other -- component of the library, this implementation can be substituted. module Implementation.MonadServerImplementation ( executorSer #ifdef EXPOSE_INTERNAL -- * Internal operations , SerState(..), SerImplementation(..) #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import Control.Concurrent.Async import qualified Control.Exception as Ex import qualified Control.Monad.IO.Class as IO import Control.Monad.Trans.State.Strict hiding (State) import qualified Data.EnumMap.Strict as EM import qualified Data.Text.IO as T import Options.Applicative (defaultPrefs, execParserPure, handleParseResult) import System.Exit (ExitCode) import System.IO (hFlush, stdout) import Game.LambdaHack.Atomic import Game.LambdaHack.Client import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.MonadStateRead import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Thread import Game.LambdaHack.Server import Game.LambdaHack.Server.BroadcastAtomic import Game.LambdaHack.Server.HandleAtomicM import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.ProtocolM import Game.LambdaHack.Server.State import Implementation.MonadClientImplementation (executorCli) data SerState = SerState { serState :: State -- ^ current global state , serServer :: StateServer -- ^ current server state , serDict :: ConnServerDict -- ^ client-server connection information , serToSave :: Save.ChanSave (State, StateServer) -- ^ connection to the save thread } -- | Server state transformation monad. newtype SerImplementation a = SerImplementation {runSerImplementation :: StateT SerState IO a} deriving (Monad, Functor, Applicative) instance MonadStateRead SerImplementation where {-# INLINE getsState #-} getsState f = SerImplementation $ gets $ f . serState instance MonadStateWrite SerImplementation where {-# INLINE modifyState #-} modifyState f = SerImplementation $ state $ \serS -> let !newSerS = serS {serState = f $ serState serS} in ((), newSerS) {-# INLINE putState #-} putState newSerState = SerImplementation $ state $ \serS -> let !newSerS = serS {serState = newSerState} in ((), newSerS) instance MonadServer SerImplementation where {-# INLINE getsServer #-} getsServer f = SerImplementation $ gets $ f . serServer {-# INLINE modifyServer #-} modifyServer f = SerImplementation $ state $ \serS -> let !newSerS = serS {serServer = f $ serServer serS} in ((), newSerS) chanSaveServer = SerImplementation $ gets serToSave liftIO = SerImplementation . IO.liftIO instance MonadServerComm SerImplementation where {-# INLINE getsDict #-} getsDict f = SerImplementation $ gets $ f . serDict {-# INLINE putDict #-} putDict newSerDict = SerImplementation $ state $ \serS -> let !newSerS = serS {serDict = newSerDict} in ((), newSerS) liftIO = SerImplementation . IO.liftIO instance MonadServerAtomic SerImplementation where execUpdAtomic cmd = do oldState <- getState (ps, atomicBroken, executedOnServer) <- handleCmdAtomicServer cmd when executedOnServer $ cmdAtomicSemSer oldState cmd handleAndBroadcast ps atomicBroken (UpdAtomic cmd) execUpdAtomicSer cmd = SerImplementation $ StateT $ \cliS -> do cliSNewOrE <- Ex.try $ execStateT (runSerImplementation $ handleUpdAtomic cmd) cliS case cliSNewOrE of Left AtomicFail{} -> return (False, cliS) Right !cliSNew -> -- We know @cliSNew@ differs only in @serState@. return (True, cliSNew) execUpdAtomicFid fid cmd = SerImplementation $ StateT $ \cliS -> do -- Don't catch anything; assume exceptions impossible. let sFid = sclientStates (serServer cliS) EM.! fid cliSNew <- execStateT (runSerImplementation $ handleUpdAtomic cmd) cliS {serState = sFid} -- We know @cliSNew@ differs only in @serState@. let serServerNew = (serServer cliS) {sclientStates = EM.insert fid (serState cliSNew) $ sclientStates $ serServer cliS} !newCliS = cliS {serServer = serServerNew} return ((), newCliS) execUpdAtomicFidCatch fid cmd = SerImplementation $ StateT $ \cliS -> do let sFid = sclientStates (serServer cliS) EM.! fid cliSNewOrE <- Ex.try $ execStateT (runSerImplementation $ handleUpdAtomic cmd) cliS {serState = sFid} case cliSNewOrE of Left AtomicFail{} -> return (False, cliS) Right cliSNew -> do -- We know @cliSNew@ differs only in @serState@. let serServerNew = (serServer cliS) {sclientStates = EM.insert fid (serState cliSNew) $ sclientStates $ serServer cliS} !newCliS = cliS {serServer = serServerNew} return (True, newCliS) execSfxAtomic sfx = do ps <- posSfxAtomic sfx handleAndBroadcast ps [] (SfxAtomic sfx) execSendPer = sendPer -- Don't inline this, to keep GHC hard work inside the library -- for easy access of code analysis tools. -- | Run the main server loop, with the given arguments and empty -- initial states, in the @IO@ monad. executorSer :: COps -> CCUI -> ServerOptions -> UIOptions -> IO () executorSer cops@COps{corule} ccui soptionsNxtCmdline sUIOptions = do soptionsNxtRaw <- case uOverrideCmdline sUIOptions of [] -> return soptionsNxtCmdline args -> handleParseResult $ execParserPure defaultPrefs serverOptionsPI args -- Options for the clients modified with the configuration file. let clientOptions = applyUIOptions cops sUIOptions $ sclientOptions soptionsNxtRaw soptionsNxt = soptionsNxtRaw {sclientOptions = clientOptions} -- Partially applied main loop of the clients. executorClient startsNewGame = executorCli ccui sUIOptions clientOptions startsNewGame cops -- Wire together game content, the main loop of game clients -- and the game server loop. let stateToFileName (_, ser) = ssavePrefixSer (soptions ser) <> Save.saveNameSer corule totalState serToSave = SerState { serState = updateCOpsAndCachedData (const cops) emptyState -- state is empty, so the cached data is left empty and untouched , serServer = emptyStateServer , serDict = EM.empty , serToSave } m = loopSer soptionsNxt executorClient exe = evalStateT (runSerImplementation m) . totalState exeWithSaves = Save.wrapInSaves cops stateToFileName exe unwrapEx e = case Ex.fromException e of Just (ExceptionInLinkedThread _ ex) -> unwrapEx ex _ -> e -- Wait for clients to exit even in case of server crash -- (or server and client crash), which gives them time to save -- and report their own inconsistencies, if any. Ex.handle (\ex -> case Ex.fromException (unwrapEx ex) :: Maybe ExitCode of Just{} -> -- User-forced shutdown, not crash, so the intention is -- to keep old saves and also clients may be not ready to save. Ex.throwIO ex _ -> do Ex.uninterruptibleMask_ $ threadDelay 1000000 -- let clients report their errors and save moveAside <- Save.bkpAllSaves corule clientOptions when moveAside $ T.hPutStrLn stdout "The game crashed, so savefiles are moved aside." hFlush stdout Ex.throwIO ex -- crash eventually, which kills clients ) exeWithSaves -- T.hPutStrLn stdout "Server exiting, waiting for clients." -- hFlush stdout waitForChildren childrenServer -- no crash, wait for clients indefinitely -- T.hPutStrLn stdout "Server exiting now." -- hFlush stdout LambdaHack-0.11.0.0/GameDefinition/game-src/0000755000000000000000000000000007346545000016447 5ustar0000000000000000LambdaHack-0.11.0.0/GameDefinition/game-src/TieKnot.hs0000644000000000000000000001654507346545000020373 0ustar0000000000000000-- | Here the knot of engine code pieces, frontend and the game-specific -- content definitions is tied, resulting in an executable game. module TieKnot ( tieKnotForAsync, tieKnot ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import Control.Concurrent.Async import qualified Control.Exception as Ex import qualified Data.Primitive.PrimArray as PA import GHC.Compact import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Client import qualified Game.LambdaHack.Client.UI.Content.Input as IC import qualified Game.LambdaHack.Client.UI.Content.Screen as SC import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Point (speedupHackXSize) import qualified Game.LambdaHack.Common.Tile as Tile import qualified Game.LambdaHack.Content.CaveKind as CK import qualified Game.LambdaHack.Content.FactionKind as FK import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.ModeKind as MK import qualified Game.LambdaHack.Content.PlaceKind as PK import qualified Game.LambdaHack.Content.RuleKind as RK import qualified Game.LambdaHack.Content.TileKind as TK import Game.LambdaHack.Server import qualified Client.UI.Content.Input as Content.Input import qualified Client.UI.Content.Screen as Content.Screen import qualified Content.CaveKind import qualified Content.FactionKind import qualified Content.ItemKind import qualified Content.ModeKind import qualified Content.PlaceKind import qualified Content.RuleKind import qualified Content.TileKind import Implementation.MonadServerImplementation (executorSer) -- | Tie the LambdaHack engine client, server and frontend code -- with the game-specific content definitions, and run the game. -- -- The custom monad types to be used are determined by the @executorSer@ -- call, which in turn calls @executorCli@. If other functions are used -- in their place- the types are different and so the whole pattern -- of computation differs. Which of the frontends is run inside the UI client -- depends on the flags supplied when compiling the engine library. -- Similarly for the choice of native vs JS builds. tieKnotForAsync :: ServerOptions -> IO () tieKnotForAsync options@ServerOptions{ sallClear , sboostRandomItem , sdungeonRng } = do -- Set the X size of the dungeon from content ASAP, before it's used. speedupHackXSizeThawed <- PA.unsafeThawPrimArray speedupHackXSize PA.writePrimArray speedupHackXSizeThawed 0 $ RK.rWidthMax Content.RuleKind.standardRules void $ PA.unsafeFreezePrimArray speedupHackXSizeThawed -- This setup ensures the boosting option doesn't affect generating initial -- RNG for dungeon, etc., and also, that setting dungeon RNG on commandline -- equal to what was generated last time, ensures the same item boost. initialGen <- maybe SM.newSMGen return sdungeonRng let soptionsNxt = options {sdungeonRng = Just initialGen} corule = RK.makeData Content.RuleKind.standardRules boostedItems = IK.boostItemKindList initialGen Content.ItemKind.items itemContent = if sboostRandomItem then boostedItems ++ Content.ItemKind.otherItemContent else Content.ItemKind.content coitem = IK.makeData (RK.ritemSymbols corule) itemContent Content.ItemKind.groupNamesSingleton Content.ItemKind.groupNames cotile = TK.makeData Content.TileKind.content Content.TileKind.groupNamesSingleton Content.TileKind.groupNames cofact = FK.makeData Content.FactionKind.content Content.FactionKind.groupNamesSingleton Content.FactionKind.groupNames -- Common content operations, created from content definitions. -- Evaluated fully to discover errors ASAP and to free memory. -- Fail here, not inside server code, so that savefiles are not removed, -- because they are not the source of the failure. copsRaw = COps { cocave = CK.makeData corule Content.CaveKind.content Content.CaveKind.groupNamesSingleton Content.CaveKind.groupNames , cofact , coitem , comode = MK.makeData cofact Content.ModeKind.content Content.ModeKind.groupNamesSingleton Content.ModeKind.groupNames , coplace = PK.makeData cotile Content.PlaceKind.content Content.PlaceKind.groupNamesSingleton Content.PlaceKind.groupNames , corule , cotile , coItemSpeedup = speedupItem coitem , coTileSpeedup = Tile.speedupTile sallClear cotile } -- Evaluating for compact regions catches all kinds of errors in content ASAP, -- even in unused items. -- -- Not using @compactWithSharing@, because it helps with residency, -- but nothing else and costs a bit at startup. #ifdef USE_JSFILE let cops = copsRaw -- until GHCJS implements GHC.Compact #else cops <- getCompact <$> compact copsRaw #endif -- Parse UI client configuration file. -- It is reparsed at each start of the game executable. -- Fail here, not inside client code, so that savefiles are not removed, -- because they are not the source of the failure. sUIOptions <- mkUIOptions corule (sclientOptions soptionsNxt) -- Client content operations containing default keypresses -- and command descriptions. let !ccui = CCUI { coinput = IC.makeData (Just sUIOptions) Content.Input.standardKeysAndMouse , coscreen = SC.makeData corule Content.Screen.standardLayoutAndFeatures } -- Wire together game content, the main loops of game clients -- and the game server loop. executorSer cops ccui soptionsNxt sUIOptions -- | Runs tieKnotForAsync in an async and applies the main thread workaround. tieKnot :: ServerOptions -> IO () tieKnot serverOptions = do #ifdef USE_JSFILE -- Hard to tweak the config file when in the browser, so hardwire. let serverOptionsJS = serverOptions {sdumpInitRngs = True} a <- async $ tieKnotForAsync serverOptionsJS wait a #else let fillWorkaround = -- Set up void workaround if nothing specific required. void $ tryPutMVar workaroundOnMainThreadMVar $ return () -- Avoid the bound thread that would slow down the communication. a <- async $ tieKnotForAsync serverOptions `Ex.finally` fillWorkaround -- Exit on an exception without waiting for frontend to spawn. link a -- Run a (possibly void) workaround. It's needed for OSes/frontends -- that need to perform some actions on the main thread -- (not just any bound thread), e.g., newer OS X drawing with SDL2. join (takeMVar workaroundOnMainThreadMVar) -- Wait in case frontend workaround not run on the main thread -- and so we'd exit too early and end the game. wait a -- Consume the void workaround if it was spurious to make @tieKnot@ reentrant. void $ tryTakeMVar workaroundOnMainThreadMVar #endif LambdaHack-0.11.0.0/LICENSE0000644000000000000000000000271407346545000013100 0ustar0000000000000000BSD 3-Clause License All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. LambdaHack-0.11.0.0/LambdaHack.cabal0000644000000000000000000004505507346545000015033 0ustar0000000000000000cabal-version: 2.4 name: LambdaHack -- The package version. See the Haskell package versioning policy (PVP) -- for standards guiding when and how versions should be incremented. -- http://www.haskell.org/haskellwiki/Package_versioning_policy -- PVP summary:+-+------- breaking API changes -- | | +----- minor or non-breaking API additions -- | | | +--- code changes with no API change version: 0.11.0.0 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 tactical squad combat. It's packaged together with a sample dungeon crawler in a quirky fantasy setting. The sample game can be tried out in the browser at . . Please see the changelog file for recent improvements and the issue tracker for short-term plans. Long term goals include multiplayer tactical squad combat, in-game content creation, auto-balancing and persistent content modification based on player behaviour. Contributions are welcome. . Other games known to use the LambdaHack library: . * Allure of the Stars, a near-future Sci-Fi game, . Note: All modules in this library are kept visible, to let games override and reuse them. OTOH, to reflect that some modules are implementation details relative to others, the source code adheres to the following convention. If a module has the same name as a directory, the module is the exclusive interface to the directory. No references to the modules in the directory are allowed except from the interface module. This policy is only binding when developing the library --- library users are free to access any modules, since the library authors are in no position to guess their particular needs. . This is a workaround .cabal file, flattened to eliminate internal libraries until generating haddocks for them is fixed. The original .cabal file is in .cabal.bkp file. homepage: https://lambdahack.github.io 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 -- 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. extra-source-files: GameDefinition/config.ui.default, GameDefinition/PLAYING.md, GameDefinition/fonts/16x16xw.woff, GameDefinition/fonts/16x16xw.bdf, GameDefinition/fonts/16x16x.fnt, GameDefinition/fonts/8x8xb.fnt, GameDefinition/fonts/8x8x.fnt, GameDefinition/fonts/BinarySansProLH-Regular.ttf.woff, GameDefinition/fonts/BinarySansProLH-Semibold.ttf.woff, GameDefinition/fonts/BinaryCodeProLH-Bold.ttf.woff, GameDefinition/fonts/DejaVuLGCSans.ttf.woff, GameDefinition/fonts/DejaVuLGCSans-Bold.ttf.woff, GameDefinition/fonts/Hack-Bold.ttf.woff extra-doc-files: GameDefinition/InGameHelp.txt, README.md, CHANGELOG.md, LICENSE, COPYLEFT, CREDITS, cabal.project, Makefile author: Andres Loeh, Mikolaj Konarski and others maintainer: Mikolaj Konarski category: Game Engine, Game build-type: Simple source-repository head type: git location: git://github.com/LambdaHack/LambdaHack.git flag with_expensive_assertions description: turn on expensive assertions of well-tested code 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 manual: True flag supportNodeJS description: compile so that the JS blob works in terminal with NodeJS default: True manual: True flag jsaddle description: switch to the JSaddle frontend (may be bit-rotted) default: False manual: True common options default-language: Haskell2010 default-extensions: MonoLocalBinds, ScopedTypeVariables, OverloadedStrings, BangPatterns, RecordWildCards, NamedFieldPuns, MultiWayIf, LambdaCase, DefaultSignatures, InstanceSigs, PatternSynonyms, StrictData, CPP, TypeApplications other-extensions: TemplateHaskell, MultiParamTypeClasses, RankNTypes, TypeFamilies, FlexibleContexts, FlexibleInstances, DeriveFunctor, FunctionalDependencies, GeneralizedNewtypeDeriving, TupleSections, DeriveFoldable, DeriveTraversable, ExistentialQuantification, GADTs, StandaloneDeriving, DataKinds, KindSignatures, DeriveGeneric, DeriveLift ghc-options: -Wall -Wcompat -Worphans -Wincomplete-uni-patterns -Wincomplete-record-updates -Wimplicit-prelude -Wmissing-home-modules -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -Wunused-packages -Winvalid-haddock -- 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 if flag(with_expensive_assertions) cpp-options: -DWITH_EXPENSIVE_ASSERTIONS if flag(release) cpp-options: -DEXPOSE_INTERNAL ghcjs-options: -DUSE_JSFILE if !flag(supportNodeJS) ghcjs-options: -DREMOVE_TELETYPE common exe-options ghc-options: -rtsopts -- (Ignored by GHCJS) Minimize median lag at the cost of occasional bigger -- GC lag, which fortunately sometimes fits into idle time between turns): -- (Ignored by GHCJS) Avoid frequent GCs. Only idle-GC during a break in -- gameplay (5s), not between slow keystrokes. ghc-options: "-with-rtsopts=-A99m -I5" -- Haskell GC in GHCJS every 10s. ghcjs-options: -DGHCJS_GC_INTERVAL=10000 -- This is the largest GHCJS_BUSY_YIELD value that does not cause dropped frames -- on my machine with default --maxFps. ghcjs-options: -DGHCJS_BUSY_YIELD=50 ghcjs-options: -dedupe if !flag(supportNodeJS) ghcjs-options: -DGHCJS_BROWSER library import: options hs-source-dirs: definition-src, engine-src, GameDefinition/game-src, GameDefinition exposed-modules: Game.LambdaHack.Core.Dice Game.LambdaHack.Core.Frequency Game.LambdaHack.Core.Prelude Game.LambdaHack.Core.Random Game.LambdaHack.Definition.Ability Game.LambdaHack.Definition.Color Game.LambdaHack.Definition.ContentData Game.LambdaHack.Definition.Defs Game.LambdaHack.Definition.DefsInternal Game.LambdaHack.Definition.Flavour Game.LambdaHack.Content.CaveKind Game.LambdaHack.Content.FactionKind Game.LambdaHack.Content.ItemKind Game.LambdaHack.Content.ModeKind Game.LambdaHack.Content.PlaceKind Game.LambdaHack.Content.RuleKind Game.LambdaHack.Content.TileKind Game.LambdaHack.Atomic Game.LambdaHack.Atomic.CmdAtomic Game.LambdaHack.Atomic.HandleAtomicWrite Game.LambdaHack.Atomic.MonadStateWrite Game.LambdaHack.Atomic.PosAtomicRead Game.LambdaHack.Client Game.LambdaHack.Client.AI Game.LambdaHack.Client.AI.ConditionM Game.LambdaHack.Client.AI.PickActionM Game.LambdaHack.Client.AI.PickActorM Game.LambdaHack.Client.AI.PickTargetM Game.LambdaHack.Client.AI.Strategy Game.LambdaHack.Client.Bfs Game.LambdaHack.Client.BfsM Game.LambdaHack.Client.CommonM Game.LambdaHack.Client.HandleAtomicM Game.LambdaHack.Client.HandleResponseM Game.LambdaHack.Client.LoopM Game.LambdaHack.Client.MonadClient Game.LambdaHack.Client.Preferences Game.LambdaHack.Client.Request Game.LambdaHack.Client.Response Game.LambdaHack.Client.State Game.LambdaHack.Client.UI Game.LambdaHack.Client.UI.ActorUI Game.LambdaHack.Client.UI.Animation Game.LambdaHack.Client.UI.Content.Input Game.LambdaHack.Client.UI.Content.Screen Game.LambdaHack.Client.UI.ContentClientUI Game.LambdaHack.Client.UI.DrawM Game.LambdaHack.Client.UI.EffectDescription Game.LambdaHack.Client.UI.Frame Game.LambdaHack.Client.UI.FrameM Game.LambdaHack.Client.UI.Frontend Game.LambdaHack.Client.UI.Frontend.Common Game.LambdaHack.Client.UI.Frontend.Teletype Game.LambdaHack.Client.UI.HandleHelperM Game.LambdaHack.Client.UI.HandleHumanGlobalM Game.LambdaHack.Client.UI.HandleHumanLocalM Game.LambdaHack.Client.UI.HandleHumanM Game.LambdaHack.Client.UI.HumanCmd Game.LambdaHack.Client.UI.InventoryM Game.LambdaHack.Client.UI.ItemDescription Game.LambdaHack.Client.UI.Key Game.LambdaHack.Client.UI.KeyBindings Game.LambdaHack.Client.UI.MonadClientUI Game.LambdaHack.Client.UI.Msg Game.LambdaHack.Client.UI.MsgM Game.LambdaHack.Client.UI.Overlay Game.LambdaHack.Client.UI.PointUI Game.LambdaHack.Client.UI.RunM Game.LambdaHack.Client.UI.SessionUI Game.LambdaHack.Client.UI.Slideshow Game.LambdaHack.Client.UI.SlideshowM Game.LambdaHack.Client.UI.UIOptions Game.LambdaHack.Client.UI.UIOptionsParse Game.LambdaHack.Client.UI.Watch Game.LambdaHack.Client.UI.Watch.WatchCommonM Game.LambdaHack.Client.UI.Watch.WatchQuitM Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM Game.LambdaHack.Common.Analytics Game.LambdaHack.Common.Area Game.LambdaHack.Common.Actor Game.LambdaHack.Common.ActorState Game.LambdaHack.Common.ClientOptions Game.LambdaHack.Common.Faction Game.LambdaHack.Common.File Game.LambdaHack.Common.HighScore Game.LambdaHack.Common.Item Game.LambdaHack.Common.ItemAspect Game.LambdaHack.Common.Kind Game.LambdaHack.Common.Level Game.LambdaHack.Common.Misc Game.LambdaHack.Common.MonadStateRead Game.LambdaHack.Common.Perception Game.LambdaHack.Common.PointArray Game.LambdaHack.Common.Point Game.LambdaHack.Common.ReqFailure Game.LambdaHack.Common.RingBuffer Game.LambdaHack.Common.Save Game.LambdaHack.Common.State Game.LambdaHack.Common.Thread Game.LambdaHack.Common.Tile Game.LambdaHack.Common.Time Game.LambdaHack.Common.Types Game.LambdaHack.Common.Vector Game.LambdaHack.Server Game.LambdaHack.Server.BroadcastAtomic Game.LambdaHack.Server.Commandline Game.LambdaHack.Server.CommonM Game.LambdaHack.Server.DebugM Game.LambdaHack.Server.DungeonGen Game.LambdaHack.Server.DungeonGen.AreaRnd Game.LambdaHack.Server.DungeonGen.Cave Game.LambdaHack.Server.DungeonGen.Place Game.LambdaHack.Server.Fov Game.LambdaHack.Server.FovDigital Game.LambdaHack.Server.HandleAtomicM Game.LambdaHack.Server.HandleEffectM Game.LambdaHack.Server.HandleRequestM Game.LambdaHack.Server.ItemRev Game.LambdaHack.Server.ItemM Game.LambdaHack.Server.LoopM Game.LambdaHack.Server.MonadServer Game.LambdaHack.Server.PeriodicM Game.LambdaHack.Server.ProtocolM Game.LambdaHack.Server.ServerOptions Game.LambdaHack.Server.StartM Game.LambdaHack.Server.State exposed-modules: Content.CaveKind Content.FactionKind Content.ItemKind Content.ItemKindEmbed Content.ItemKindActor Content.ItemKindOrgan Content.ItemKindBlast Content.ItemKindTemporary Content.ModeKind Content.PlaceKind Content.RuleKind Content.TileKind TieKnot Client.UI.Content.Input Client.UI.Content.Screen Implementation.MonadClientImplementation Implementation.MonadServerImplementation other-modules: Paths_LambdaHack autogen-modules: Paths_LambdaHack build-depends: assert-failure >= 0.1.2 && < 0.2, async >= 2.2.1, base >= 4.10 && < 99, base-compat >= 0.10.0, binary >= 0.8, bytestring >= 0.9.2 , containers >= 0.5.3.0, deepseq >= 1.3, directory >= 1.1.0.1, enummapset >= 0.5.2.2, file-embed >= 0.0.11, filepath >= 1.2.0.1, hashable >= 1.1.2.5, hsini >= 0.2, witch >= 0.3, keys >= 3, miniutter >= 0.5.0.0, open-browser >= 0.2, optparse-applicative >= 0.13, pretty-show >= 1.6, primitive >= 0.6.1.0, QuickCheck, splitmix >= 0.0.3, stm >= 2.4, time >= 1.4, text >= 0.11.2.3, transformers >= 0.4, unordered-containers >= 0.2.3, vector >= 0.11, vector-binary-instances >= 0.2.3.1, template-haskell >= 2.6, th-lift-instances, ghc-compact if impl(ghcjs) || flag(jsaddle) { exposed-modules: Game.LambdaHack.Client.UI.Frontend.Dom build-depends: ghcjs-dom >= 0.9.1.1 cpp-options: -DUSE_BROWSER } else { exposed-modules: Game.LambdaHack.Client.UI.Frontend.Sdl Game.LambdaHack.Client.UI.Frontend.ANSI build-depends: sdl2 >= 2, sdl2-ttf >= 2, ansi-terminal >= 0.10 } if impl(ghcjs) { other-modules: Game.LambdaHack.Common.JSFile build-depends: ghcjs-base } else { other-modules: Game.LambdaHack.Common.HSFile build-depends: zlib >= 0.5.3.1 } executable LambdaHack import: options, exe-options main-is: GameDefinition/Main.hs build-depends: ,LambdaHack ,async ,base ,filepath ,optparse-applicative test-suite test import: options, exe-options type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Spec.hs other-modules: ActorStateUnitTests CommonMUnitTests HandleHelperMUnitTests HandleHumanLocalMUnitTests InventoryMUnitTests ItemDescriptionUnitTests ItemKindUnitTests ItemRevUnitTests LevelUnitTests MonadClientUIUnitTests ReqFailureUnitTests SessionUIMock SessionUIUnitTests UnitTestHelpers build-depends: ,LambdaHack ,base ,containers ,enummapset ,optparse-applicative ,splitmix ,tasty >= 1.0 ,tasty-quickcheck ,tasty-hunit ,text ,transformers ,vector LambdaHack-0.11.0.0/Makefile0000644000000000000000000006626007346545000013541 0ustar0000000000000000play: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix play --dumpInitRngs shot: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix play --dumpInitRngs --printEachScreen expose-lore: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode crawl --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000 dig-lore: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode dig --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000 see-caves: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode see --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000 short-caves: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix know --newGame 5 --dumpInitRngs --gameMode short --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples --benchmark --noAnim --maxFps 1000 configure-debug: cabal configure --enable-profiling --profiling-detail=all-functions -fwith_expensive_assertions --disable-optimization configure-prof: cabal configure --enable-profiling --profiling-detail=exported-functions ghcjs-build: cabal build --ghcjs . chrome-log: google-chrome --enable-logging --v=1 file:///home/mikolaj/r/lambdahack.github.io/index.html & chrome-prof: 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 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 # 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 # specially and add their extra delay. create-gif : find ~/.LambdaHack/screenshots/ -name 'prtscn*.bmp' -print0 | xargs -0 -r mogrify -format gif ../gifsicle/src/gifsicle -O3 --careful -d2 --colors 255 --no-extensions --no-conserve-memory -l ~/.LambdaHack/screenshots/prtscn*.gif -o ~/.LambdaHack/screenshots/screenshot.gif frontendRaid: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --benchMessages --gameMode raid --exposeActors frontendBrawl: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode brawl --benchMessages frontendShootout: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode shootout frontendHunt: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode hunt frontendFlight: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 3 --dumpInitRngs --automateAll --gameMode flight frontendZoo: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 2 --dumpInitRngs --automateAll --gameMode zoo --exposeActors frontendAmbush: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode ambush frontendCrawl: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors frontendCrawlEmpty: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawlEmpty --maxFps 100000 --benchmark frontendCrawlSurvival: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode crawlSurvival --maxFps 100000 --benchmark frontendSafari: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode safari --exposeActors frontendSafariSurvival: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 5 --dumpInitRngs --automateAll --gameMode safariSurvival --exposeActors --maxFps 100000 --benchmark frontendBattle: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 4 --dumpInitRngs --automateAll --gameMode battle --exposeActors frontendBattleDefense: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 6 --dumpInitRngs --automateAll --gameMode battleDefense --exposeActors --maxFps 100000 --benchmark frontendBattleSurvival: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 6 --dumpInitRngs --automateAll --gameMode battleSurvival --exposeActors --maxFps 100000 --benchmark frontendDefense: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode defense --exposeItems --exposeActors frontendDefenseEmpty: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 9 --dumpInitRngs --automateAll --gameMode defenseEmpty --maxFps 100000 --benchmark fastCrawl: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors --showItemSamples --noAnim --maxFps 100000 --benchmark slowCrawl: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --savePrefix test --newGame 1 --dumpInitRngs --automateAll --gameMode crawl --exposeItems --exposeActors --showItemSamples # different benchmarks use different arguments RNGOPTS=--setDungeonRng "SMGen 123 123" --setMainRng "SMGen 123 125" RNGOPTS1=--setDungeonRng "SMGen 127 123" --setMainRng "SMGen 127 125" RNGOPTS2=--setDungeonRng "SMGen 129 123" --setMainRng "SMGen 129 125" benchMemoryAnim: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --maxFps 100000 --benchmark --benchMessages --stopAfterFrames 33000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS2) --frontendLazy +RTS -s -A1M -RTS benchBattle: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1500 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1) benchAnimBattle: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 3 --maxFps 100000 --frontendLazy --benchmark --benchMessages --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1) benchFrontendBattle: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --benchmark --benchMessages --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS1) benchCrawl: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS) benchFrontendCrawl: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --benchmark --benchMessages --stopAfterFrames 7000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS) benchDig: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1 --automateAll --keepAutomated --gameMode dig $(RNGOPTS) benchNull: benchBattle benchAnimBattle benchCrawl bench: benchBattle benchAnimBattle benchFrontendBattle benchCrawl benchFrontendCrawl nativeBenchCrawl: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS) nativeBenchBattle: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS) nativeBench: nativeBenchBattle nativeBenchCrawl nodeBenchCrawl: node $$(cabal list-bin exe:LambdaHack).jsexe/all.js --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS) nodeBenchBattle: node $$(cabal list-bin exe:LambdaHack).jsexe/all.js --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS) nodeBench: nodeBenchBattle nodeBenchCrawl nodeMinifiedBench: node ../lambdahack.github.io/lambdahack.all.js --dbgMsgSer --logPriority 4 --newGame 3 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 1000 --automateAll --keepAutomated --gameMode battle $(RNGOPTS) node ../lambdahack.github.io/lambdahack.all.js --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterFrames 2000 --automateAll --keepAutomated --gameMode crawl $(RNGOPTS) test: test-short test-medium benchNull test-gha: test testCrawl-medium testCrawl-stopAfterGameOver testDefense-medium test-sniff test-short: test-short-new test-short-load test-medium: testRaid-medium testBrawl-medium testShootout-medium testHunt-medium testFlight-medium testZoo-medium testAmbush-medium testCrawlEmpty-medium testCrawl-medium-know testSafari-medium testSafariSurvival-medium testBattle-medium testBattleDefense-medium testBattleSurvival-medium testDig-medium testDefenseEmpty-medium testMany-teletype test-sniff: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterFrames 1 --dumpInitRngs --automateAll --keepAutomated --gameMode raid --sniff > /tmp/teletypetest.log 2>&1 testMany-teletype: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 9 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 50 --dumpInitRngs --automateAll --keepAutomated 2> /tmp/teletypetest.log testMany-sdlInit: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 0 --boostRandomItem --newGame 9 --maxFps 100000 --benchmark --benchMessages --stopAfterSeconds 50 --dumpInitRngs --automateAll --keepAutomated 2> /tmp/teletypetest.log testRaid-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode raid 2> /tmp/teletypetest.log testBrawl-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode brawl 2> /tmp/teletypetest.log testShootout-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode shootout 2> /tmp/teletypetest.log testHunt-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode hunt 2> /tmp/teletypetest.log testFlight-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 3 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode flight 2> /tmp/teletypetest.log testZoo-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 2 --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode zoo 2> /tmp/teletypetest.log testAmbush-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode ambush 2> /tmp/teletypetest.log testCrawl-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 600 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --assertExplored 5 2> /tmp/teletypetest.log testCrawl-stopAfterGameOver: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 9 --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --stopAfterGameOver 2> /tmp/teletypetest.log testCrawlEmpty-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode crawlEmpty 2> /tmp/teletypetest.log testCrawl-medium-know: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix know --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --knowItems --exposePlaces --exposeItems --exposeActors --showItemSamples 2> /tmp/teletypetest.log testSafari-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 100 --dumpInitRngs --automateAll --keepAutomated --gameMode safari 2> /tmp/teletypetest.log testSafariSurvival-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival 2> /tmp/teletypetest.log testBattle-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 3 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 20 --dumpInitRngs --automateAll --keepAutomated --gameMode battle 2> /tmp/teletypetest.log testBattleDefense-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 7 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense 2> /tmp/teletypetest.log testBattleSurvival-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 7 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival 2> /tmp/teletypetest.log testDefense-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 9 --noAnim --maxFps 100000 --frontendLazy --benchmark --stopAfterSeconds 600 --dumpInitRngs --automateAll --keepAutomated --gameMode defense 2> /tmp/teletypetest.log testDefenseEmpty-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 9 --noAnim --maxFps 100000 --frontendTeletype --benchmark --benchMessages --stopAfterSeconds 40 --dumpInitRngs --automateAll --keepAutomated --gameMode defenseEmpty 2> /tmp/teletypetest.log testDig-medium: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendTeletype --benchmark --stopAfterFrames 100 --dumpInitRngs --automateAll --keepAutomated --gameMode dig 2> /tmp/teletypetest.log testCrawl-appveyor: ./LambdaHack --dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterGameOver --stopAfterSeconds 300 --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --assertExplored 5 testDefense-appveyor: ./LambdaHack --dbgMsgSer --logPriority 4 --newGame 9 --noAnim --maxFps 100000 --frontendNull --benchmark --benchMessages --stopAfterSeconds 600 --dumpInitRngs --automateAll --keepAutomated --gameMode defense test-short-new: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix raid --dumpInitRngs --automateAll --keepAutomated --gameMode raid --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix brawl --dumpInitRngs --automateAll --keepAutomated --gameMode brawl --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix shootout --dumpInitRngs --automateAll --keepAutomated --gameMode shootout --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix hunt --dumpInitRngs --automateAll --keepAutomated --gameMode hunt --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix flight --dumpInitRngs --automateAll --keepAutomated --gameMode flight --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix zoo --dumpInitRngs --automateAll --keepAutomated --gameMode zoo --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix ambush --dumpInitRngs --automateAll --keepAutomated --gameMode ambush --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix crawl --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix safari --dumpInitRngs --automateAll --keepAutomated --gameMode safari --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix safariSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival --showItemSamples --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battle --showItemSamples --dumpInitRngs --automateAll --keepAutomated --gameMode battle --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battleDefense --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --newGame 5 --savePrefix battleSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival --frontendTeletype --stopAfterSeconds 2 2> /tmp/teletypetest.log # $(RNGOPTS) is needed for determinism relative to seed # generated before game save test-short-load: $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix raid --dumpInitRngs --automateAll --keepAutomated --gameMode raid --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix brawl --dumpInitRngs --automateAll --keepAutomated --gameMode brawl --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix shootout --dumpInitRngs --automateAll --keepAutomated --gameMode shootout --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix hunt --dumpInitRngs --automateAll --keepAutomated --gameMode hunt --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix flight --dumpInitRngs --automateAll --keepAutomated --gameMode flight --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix zoo --dumpInitRngs --automateAll --keepAutomated --gameMode zoo --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix ambush --dumpInitRngs --automateAll --keepAutomated --gameMode ambush --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix crawl --dumpInitRngs --automateAll --keepAutomated --gameMode crawl --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix safari --dumpInitRngs --automateAll --keepAutomated --gameMode safari --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix safariSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode safariSurvival --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battle --dumpInitRngs --automateAll --keepAutomated --gameMode battle --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battleDefense --dumpInitRngs --automateAll --keepAutomated --gameMode battleDefense --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log $$(cabal list-bin exe:LambdaHack) --dbgMsgSer --logPriority 4 --boostRandomItem --savePrefix battleSurvival --dumpInitRngs --automateAll --keepAutomated --gameMode battleSurvival --frontendTeletype --stopAfterSeconds 2 $(RNGOPTS) 2> /tmp/teletypetest.log build-binary-v1: cabal v1-install --force-reinstalls --disable-library-profiling --disable-profiling --disable-documentation --enable-optimization --only-dependencies cabal v1-configure --disable-library-profiling --disable-profiling --enable-optimization cabal v1-build exe:LambdaHack copy-binary-v1: cabal v1-copy --destdir=LambdaHackTheGameInstall copy-binary: cp $$(cabal list-bin exe:LambdaHack) LambdaHackTheGame configure-binary-v2: cabal configure --disable-tests --disable-library-profiling --disable-profiling --enable-optimization build-binary-v2: cabal build --only-dependencies . cabal build exe:LambdaHack copy-directory: mkdir -p LambdaHackTheGame/GameDefinition cp GameDefinition/InGameHelp.txt LambdaHackTheGame/GameDefinition cp GameDefinition/PLAYING.md LambdaHackTheGame/GameDefinition cp README.md LambdaHackTheGame cp CHANGELOG.md LambdaHackTheGame cp LICENSE LambdaHackTheGame cp COPYLEFT LambdaHackTheGame cp CREDITS LambdaHackTheGame build-binary-common: build-binary-v1 copy-directory copy-binary-v1 build-binary-windows: configure-binary-v2 build-binary-v2 copy-directory build-directory: configure-binary-v2 build-binary-v2 copy-directory copy-binary build-binary-ubuntu: build-directory LambdaHackTheGame/LambdaHack --version > /dev/null; \ LH_VERSION=$$(cat ~/.LambdaHack/stdout.txt); \ tar -czf LambdaHack_$${LH_VERSION}_ubuntu-16.04-amd64.tar.gz LambdaHackTheGame build-binary-macosx: build-directory LambdaHackTheGame/LambdaHack --version > /dev/null; \ LH_VERSION=$$(cat ~/.LambdaHack/stdout.txt); \ OS_VERSION=$$(sw_vers -productVersion); \ tar -czf LambdaHack_$${LH_VERSION}_macosx-$${OS_VERSION}-amd64.tar.gz LambdaHackTheGame LambdaHack-0.11.0.0/README.md0000644000000000000000000004231707346545000013355 0ustar0000000000000000LambdaHack ========== [![Hackage](https://img.shields.io/hackage/v/LambdaHack.svg)](https://hackage.haskell.org/package/LambdaHack) [![Join the chat at Discord](https://img.shields.io/discord/688792755564052486.svg?label=chat%20on%20Discord&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/87Ghnws) [![Join the chat at Matrix](https://img.shields.io/matrix/lambdahack:mozilla.org.svg?label=chat%20on%20Matrix&logo=matrix&server_fqdn=mozilla.modular.im)](https://matrix.to/#/#lambdahack:mozilla.org) LambdaHack is a Haskell[1] game engine library for ASCII roguelike[2] games of arbitrary theme, size and complexity, with optional tactical squad combat. It's packaged together with a sample dungeon crawler in a quirky fantasy setting. The sample game can be tried out in the browser at http://lambdahack.github.io. As an example of the engine's capabilities, here is a showcase of shooting down explosive projectiles. A couple were shot down close enough to enemies to harm them. Others exploded closer to our party members and took out of the air the projectiles that would otherwise harm them. Actual in-game footage. ![gameplay screenshot](https://raw.githubusercontent.com/LambdaHack/media/master/screenshot/allureofthestars.com.shooting.down.explosives.gif) This was a semi-automatic stealthy speedrun of the escape scenario of the sample game, native binary, SDL2 frontend, single tiny bitmap font. The enemy gang has a huge numerical and equipment superiority. Our team loots the area on auto-pilot until the first foe is spotted. Then they scout out enemy positions. Then hero 1 draws enemies and unfortunately enemy fire as well, which is when he valiantly shoots down explosives to avoid the worst damage. Then heroine 2 sneaks behind enemy lines to reach the remaining treasure. That accomplished, the captain signals retreat and leaves for the next area (the zoo). Using the engine ---------------- To use the engine, you need to specify the content to be procedurally generated. You declare what the game world is made of (entities, their relations, physics and lore) and the engine builds the world and runs it. The library lets you compile a ready-to-play game binary, using either the supplied or a custom-made main loop. A couple of frontends are available (SDL2 is the default for desktop and there is a JavaScript browser frontend) and many other generic engine components are easily overridden, but the fundamental source of flexibility lies in the strict and enforced with types separation of engine code from the read-only content and of clients (human and AI-controlled) from the server. Please see the changelog file for recent improvements and the issue tracker for short-term plans. Long term goals include multiplayer tactical squad combat, in-game content creation, auto-balancing and persistent content modification based on player behaviour. Contributions are welcome. Please offer feedback to mikolaj.konarski@funktory.com or, preferably, on any of the public forums. Games from different repos known to use the LambdaHack library: * Allure of the Stars[6], a near-future Sci-Fi game Note: the engine and the LambdaHack sample game are bundled together in a single Hackage[3] package released under the permissive `BSD3` license. You are welcome to create your own games by forking and modifying the single package, but please consider eventually splitting your changes into a separate content-heavy package that depends on the upstream engine library. This will help us exchange ideas and share improvements to the common codebase. Alternatively, you can already start the development in separation by cloning and rewriting Allure of the Stars[10] and mix and merge with the sample LambdaHack game rules at will. Note that the LambdaHack sample game derives from the Hack/Nethack visual and narrative tradition[9], while Allure of the Stars uses the more free-form Moria/Angband style (it also uses the AGPL license, and BSD3 + AGPL = AGPL, so make sure you want to liberate your code and content to such an extent). Installation of the sample game from binary archives ---------------------------------------------------- The game runs rather slowly in the browser (fastest on Chrome) and you are limited to the square font for all purposes, though it's scalable. Also, savefiles are prone to corruption on the browser, e.g., when it's closed while the game is still saving progress (which takes a long time). Hence, after trying out the game, you may prefer to use a native binary for your architecture, if it exists. Pre-compiled game binaries are available through the release page[11] (and Linux dev versions from GitHub Actions[18] and Windows from AppVeyor[19]). To use a pre-compiled binary archive, unpack it and run the executable in the unpacked directory or use program shortcuts from the installer, if available. On Linux, make sure you have the SDL2 libraries installed on your system (e.g., libsdl2-2.0-0 and libsdl2-ttf-2.0-0 on Ubuntu). For Windows (XP no longer supported), the SDL2 and all other needed libraries are included in the game's binary archive. Screen and keyboard configuration --------------------------------- The game UI can be configured via a config file. The default config settings, the same that are built into the binary, are on github at [GameDefinition/config.ui.default](https://github.com/LambdaHack/LambdaHack/blob/master/GameDefinition/config.ui.default). When the game is run for the first time, or whenever the config file is deleted, the file is written to the default user data location, which is `~/.LambdaHack/` on Linux, `C:\Users\\AppData\Roaming\LambdaHack\` (or `C:\Documents And Settings\user\Application Data\LambdaHack\` or something else altogether) on Windows and `Inspect/Application/Local Storage` under RMB menu when run inside the Chrome browser. If the user config file is outdated or corrupted, it's automatically moved away together with old savefiles. At the next game start, the new default config file appears at its place. Screen fonts and, consequently, window size can be changed by editing the config file in the user data folder. The default bitmap font `16x16xw.bdf` used for the game map covers most national characters in the Latin alphabet (e.g. to give custom names to player characters) and results in a game window of exactly 720p HD dimensions. The `8x8xb.fnt` bitmap font results in a tiny window and covers latin-1 characters only. The config file parameter `allFontsScale` permits further window size adjustments, automatically switching to the scalable version of the large game map font (`16x16xw.woff`). Config file option `chosenFontset` governs not only the main game map font, but also the shape of the rectangular fonts, if any, in which longer texts are overlaid over the map. For high resolution displays and/or if fullscreen mode is requested in the configuration file, `allFontsScale` needs to be set. E.g., scale 3 works for 4K displays. Otherwise, the letters may be too small or, in fullscreen or on retina displays in OS X, the screen may be automatically scaled as a whole, not each letter separately, softening letter edges of the square fonts that should rather be pixel-perfect and crisp. If you don't have a numeric keypad, you can use the left-hand movement key setup (axwdqezc) or Vi editor keys (aka roguelike keys) or mouse. If numeric keypad doesn't work, toggling the Num Lock key sometimes helps. If running with the Shift key and keypad keys doesn't work, try the Control key instead. The game is fully playable with mouse only, as well as with keyboard only, but the most efficient combination may be mouse for menus, go-to, inspecting the map, aiming at distant positions and keyboard for everything else. If you run the ANSI terminal frontend (`--frontendANSI` on commandline), then numeric keypad (especially keypad `*`, `/` and `5`) may not work correctly, depending on the terminal emulator you use. Toggling the Num Lock key may help or make issues worse. As a work around these issues, numbers are used for movement in the ANSI frontend, which sadly prevents the number keys from selecting heroes. The commands that require pressing Control and Shift together won't work either, but fortunately they are not crucial to gameplay. Some effort went into making the ANSI frontend usable with screen readers, but without feedback it's hard to say how accessible that setup is. This doesn't work on Windows, due to extra code that would be required. As a side effect of screen reader support, there is no aiming line nor path in ANSI frontend and some of map position highlighting is performed using the terminal cursor. Screen readers may also work better with animations turned off, using `--noAnim` or the corresponding config file or main game menu options. Compilation of the library and sample game from source ------------------------------------------------------ To compile with the standard frontend based on SDL2, you need the SDL2 libraries for your OS. On Linux, remember to install the -dev versions as well, e.g., libsdl2-dev and libsdl2-ttf-dev on Ubuntu Linux 16.04. Compilation to JavaScript for the browser is more complicated and requires the ghcjs[15] compiler and optionally the Google Closure Compiler[16]. The latest official version of the LambdaHack library can be downloaded, compiled for SDL2 and installed automatically using the 'cabal' tool, 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 cabal update cabal run LambdaHack For a newer snapshot, clone the source code from github[5] and run `cabal run LambdaHack` from the main directory. Alternatively, if you'd like to develop in this codebase, the following speeds up the turn-around a lot cp cabal.project.local.development cabal.project.local and then you can compile (and recompile) with cabal build and run the game with make play The SDL2 frontend binary also contains the ANSI terminal frontend (`--frontendANSI` on commandline) intended for screen readers and a simplified black and white line terminal frontend (`--frontendTeletype`) suitable for teletype terminals or a keyboard and a printer (but it's going to use a lot of paper, unless you disable animations with `--noAnim`). The teletype frontend is used in CI and for some tests and benchmarks defined in Makefile. The terminal frontends leave you on your own regarding font choice and color setup and you won't have the colorful squares outlining special positions that exist in the SDL2 frontend, but only crude cursor highlights. The terminal frontends should run on Windows, but Windows disables console for GUI applications, so they don't. Testing and debugging --------------------- Unit tests and integration tests can be run and displayed with cabal test --test-show-details=direct and doctests with cabal install doctest --overwrite-policy=always && cabal build cabal repl --build-depends=QuickCheck --with-ghc=doctest definition cabal repl --build-depends=QuickCheck --build-depends=template-haskell --with-ghc=doctest lib:LambdaHack The [Makefile](https://github.com/LambdaHack/LambdaHack/blob/master/Makefile) contains many sample automated playtest commands. Numerous tests that use the screensaver game modes (AI vs. AI) and the teletype frontend are gathered in `make test-locally`. Some of these are run by CI on each push to github. Test commands with prefix `frontend` start AI vs. AI games with the standard SDL2 frontend to view them on. Run `LambdaHack --help` to see a brief description of all debug options. Of these, the `--sniff` option is very useful (though verbose and initially cryptic), for displaying the traffic between clients and the server. Some options in the config file may prove useful for debugging too, though they mostly overlap with commandline options (and will be totally merged at some point). Coding style ------------ Stylish Haskell is used for slight auto-formatting at buffer save; see [.stylish-haskell.yaml](https://github.com/LambdaHack/LambdaHack/blob/master/.stylish-haskell.yaml). As defined in the file, indentation is 2 spaces wide and screen is 80-columns wide. Spaces are used, not tabs. Spurious whitespace avoided. Spaces around arithmetic operators encouraged. Generally, relax and try to stick to the style apparent in a file you are editing. Put big formatting changes in separate commits. CI checks the code with `hlint .` using the very liberal configuration file at [.hlint.yaml](https://github.com/LambdaHack/LambdaHack/blob/master/.hlint.yaml). If hlint is still too naggy, feel free to add more exceptions. Haddocks are provided for all module headers and for all functions and types from major modules, in particular for the modules that are interfaces for a whole directory of modules. Apart of that, only very important functions and types are distinguished by having a haddock. If minor ones have comments, they should not be haddocks and they are permitted to describe implementation details and be out of date. Prefer assertions instead of comments, unless too verbose. The 'pointman' from game manual and UI is called 'leader' in the source code and there are a few more mismatches, though the source code naming and the UI naming should each be consistent in separation. If the UI names stick, perhaps source code will be renamed at some point. This codebase is an experiment in extensive use of states without lens. So far, it works, doesn't result in much larger files or lots of repetition and has the added benefits that newcomers don't need to learn any optics library. Record punning, etc., definitely help. First steps reading the codebase -------------------------------- A good start may be https://github.com/LambdaHack/LambdaHack/blob/master/GameDefinition/game-src/Client/UI/Content/Input.hs That's where keyboard keys are assigned commands, help texts and categories (including special categories indicating that a group of keys also forms an in-game menu). This file is specific to a particular game (hence `GameDefinition` in the path) and the engine dynamically creates in-game help screens based on this file and on player config file that can partially overwrite it. The commands assigned to keys are interpreted by the UI client (each faction in the game uses a client and the player's client additionally has UI capabilities) in the following module: https://github.com/LambdaHack/LambdaHack/blob/master/engine-src/Game/LambdaHack/Client/UI/HandleHumanM.hs By this point you've seen one of the six major command sets (`HumanCmd`, the others being `Effect`, `UpdAtomic`, `Request`, `Response`, `FrontReq`) and one of around ten distinct interpreters for the commands (mostly in `Handle*` modules). You've also seen a bit of the UI client code, but not the AI client nor the server (game arbiter) code. The wiki[17] contains not entirely outdated further reading about the client-server architecture. At this point, before trying to grasp anything more and drown in abstraction, you are welcome to pick up a few `good first issue`-labeled tickets and get some hands-on experience with the codebase. For further study, note that most of the commands are interpreted in monads. Server and clients share some of the customized monadic API, but their monads are implemented differently (in `*Implementation` modules). All these monads are state monads (managing different aspects of game state), therefore the semantics of a command is a state transformer with extra side effects (e.g., frontend drawing). The "main loop" is the following: the UI client receives keystrokes and interprets the commands they correspond to. As soon as one of the commands is not just local UI manipulation, but a request to change the main game state, such a request is packaged and sent to the server (e.g., a request to move a hero to the north). The server responds "not possible, there is a wall" or reacts by sending to clients (to all UI and AI clients that can see the event) a series of game state-changing responses. AI clients, likewise, send to the server requests, generated based on the perceived game state changes and the AI goals of each AI faction. Further information ------------------- For more information, visit the wiki[4] and see [PLAYING.md](https://github.com/LambdaHack/LambdaHack/blob/master/GameDefinition/PLAYING.md), [CREDITS](https://github.com/LambdaHack/LambdaHack/blob/master/CREDITS) and [COPYLEFT](https://github.com/LambdaHack/LambdaHack/blob/master/COPYLEFT). Have fun! [1]: https://www.haskell.org/ [2]: http://roguebasin.roguelikedevelopment.org/index.php?title=Berlin_Interpretation [3]: https://hackage.haskell.org/package/LambdaHack [4]: https://github.com/LambdaHack/LambdaHack/wiki [5]: https://github.com/LambdaHack/LambdaHack [6]: http://allureofthestars.com [9]: https://github.com/LambdaHack/LambdaHack/wiki/Sample-dungeon-crawler [10]: https://github.com/AllureOfTheStars/Allure [11]: https://github.com/LambdaHack/LambdaHack/releases [15]: https://github.com/ghcjs/ghcjs [16]: https://www.npmjs.com/package/google-closure-compiler [17]: https://github.com/LambdaHack/LambdaHack/wiki/Client-server-architecture [18]: https://github.com/LambdaHack/LambdaHack/actions [19]: https://ci.appveyor.com/project/Mikolaj/lambdahack LambdaHack-0.11.0.0/Setup.hs0000644000000000000000000000005707346545000013525 0ustar0000000000000000import Distribution.Simple main = defaultMain LambdaHack-0.11.0.0/cabal.project0000644000000000000000000000013707346545000014522 0ustar0000000000000000packages: ./ package sdl2 flags: +no-linear package splitmix flags: +optimised-mixer LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Content/0000755000000000000000000000000007346545000021236 5ustar0000000000000000LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Content/CaveKind.hs0000644000000000000000000001675407346545000023273 0ustar0000000000000000-- | The type of cave kinds. Every level in the game is an instantiated -- cave kind. module Game.LambdaHack.Content.CaveKind ( pattern DEFAULT_RANDOM , CaveKind(..), InitSleep(..), makeData #ifdef EXPOSE_INTERNAL -- * Internal operations , validateSingle, validateAll, mandatoryGroups #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Text as T import Game.LambdaHack.Content.ItemKind (ItemKind) import Game.LambdaHack.Content.PlaceKind (PlaceKind) import qualified Game.LambdaHack.Content.RuleKind as RK import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal -- | Parameters for the generation of dungeon levels. -- Warning: for efficiency, avoid embedded items in any of the common tiles. data CaveKind = CaveKind { cname :: Text -- ^ short description , cfreq :: Freqs CaveKind -- ^ frequency within groups , cXminSize :: X -- ^ minimal X size of the whole cave , cYminSize :: Y -- ^ minimal Y size of the whole cave , ccellSize :: Dice.DiceXY -- ^ size of a map cell holding a place , cminPlaceSize :: Dice.DiceXY -- ^ minimal size of places; for merging , cmaxPlaceSize :: Dice.DiceXY -- ^ maximal size of places; for growing , cdarkOdds :: Dice.Dice -- ^ the odds a place is dark -- (level-scaled dice roll > 50) , cnightOdds :: Dice.Dice -- ^ the odds the cave is dark -- (level-scaled dice roll > 50) , cauxConnects :: Rational -- ^ a proportion of extra connections , cmaxVoid :: Rational -- ^ at most this proportion of rooms may be void , cdoorChance :: Chance -- ^ the chance of a door in an opening , copenChance :: Chance -- ^ if there's a door, is it open? , chidden :: Int -- ^ if not open, hidden one in n times , cactorCoeff :: Int -- ^ the lower, the more monsters spawn , cactorFreq :: Freqs ItemKind -- ^ actor groups to consider , citemNum :: Dice.Dice -- ^ number of initial items in the cave , citemFreq :: Freqs ItemKind -- ^ item groups to consider; -- note that the groups are flattened; e.g., if an item is moved -- to another included group with the same weight, the outcome -- doesn't change , cplaceFreq :: Freqs PlaceKind -- ^ place groups to consider , cpassable :: Bool -- ^ are passable default tiles permitted , clabyrinth :: Bool -- ^ waste of time for AI to explore , cdefTile :: GroupName TileKind -- ^ the default cave tile , cdarkCorTile :: GroupName TileKind -- ^ the dark cave corridor tile , clitCorTile :: GroupName TileKind -- ^ the lit cave corridor tile , cwallTile :: GroupName TileKind -- ^ the tile used for @FWall@ fence , ccornerTile :: GroupName TileKind -- ^ tile used for the fence corners , cfenceTileN :: GroupName TileKind -- ^ the outer fence N wall , cfenceTileE :: GroupName TileKind -- ^ the outer fence E wall , cfenceTileS :: GroupName TileKind -- ^ the outer fence S wall , cfenceTileW :: GroupName TileKind -- ^ the outer fence W wall , cfenceApart :: Bool -- ^ are places touching fence banned , cminStairDist :: Int -- ^ minimal distance between stairs , cmaxStairsNum :: Dice.Dice -- ^ maximum number of stairs , cescapeFreq :: Freqs PlaceKind -- ^ escape groups, if any , cstairFreq :: Freqs PlaceKind -- ^ place groups for created stairs , cstairAllowed :: Freqs PlaceKind -- ^ extra groups for inherited , cskip :: [Int] -- ^ which faction starting positions to skip , cinitSleep :: InitSleep -- ^ whether actors spawn sleeping , cdesc :: Text -- ^ full cave description } deriving Show -- No Eq and Ord to make extending logically sound data InitSleep = InitSleepAlways | InitSleepPermitted | InitSleepBanned deriving (Show, Eq) -- | Catch caves with not enough space for all the places. Check the size -- of the cave descriptions to make sure they fit on screen. Etc. validateSingle :: RK.RuleContent -> CaveKind -> [Text] validateSingle corule CaveKind{..} = let (minCellSizeX, minCellSizeY) = Dice.infDiceXY ccellSize (maxCellSizeX, maxCellSizeY) = Dice.supDiceXY ccellSize (minMinSizeX, minMinSizeY) = Dice.infDiceXY cminPlaceSize (maxMinSizeX, maxMinSizeY) = Dice.supDiceXY cminPlaceSize (minMaxSizeX, minMaxSizeY) = Dice.infDiceXY cmaxPlaceSize in [ "cname longer than 25" | T.length cname > 25 ] ++ [ "cXminSize > RK.rWidthMax" | cXminSize > RK.rWidthMax corule ] ++ [ "cYminSize > RK.rHeightMax" | cYminSize > RK.rHeightMax corule ] ++ [ "cXminSize < 8" | cXminSize < 8 ] ++ [ "cYminSize < 8" | cYminSize < 8 ] -- see @focusArea@ ++ [ "cXminSize - 2 < maxCellSizeX" | cXminSize - 2 < maxCellSizeX ] ++ [ "cYminSize - 2 < maxCellSizeY" | cYminSize - 2 < maxCellSizeY ] ++ [ "minCellSizeX < 2" | minCellSizeX < 2 ] ++ [ "minCellSizeY < 2" | minCellSizeY < 2 ] ++ [ "minCellSizeX < 4 and stairs" | minCellSizeX < 4 && not (null cstairFreq) ] ++ [ "minCellSizeY < 4 and stairs" | minCellSizeY < 4 && not (null cstairFreq) ] -- The following four are heuristics, so not too restrictive: ++ [ "minCellSizeX < 6 && non-trivial stairs" | minCellSizeX < 6 && not (length cstairFreq <= 1 && null cescapeFreq) ] ++ [ "minCellSizeY < 4 && non-trivial stairs" | minCellSizeY < 4 && not (length cstairFreq <= 1 && null cescapeFreq) ] ++ [ "minMinSizeX < 5 && non-trivial stairs" | minMinSizeX < 5 && not (length cstairFreq <= 1 && null cescapeFreq) ] ++ [ "minMinSizeY < 3 && non-trivial stairs" | minMinSizeY < 3 && not (length cstairFreq <= 1 && null cescapeFreq) ] ++ [ "minMinSizeX < 1" | minMinSizeX < 1 ] ++ [ "minMinSizeY < 1" | minMinSizeY < 1 ] ++ [ "minMaxSizeX < maxMinSizeX" | minMaxSizeX < maxMinSizeX ] ++ [ "minMaxSizeY < maxMinSizeY" | minMaxSizeY < maxMinSizeY ] ++ [ "chidden < 0" | chidden < 0 ] ++ [ "cactorCoeff < 0" | cactorCoeff < 0 ] ++ [ "citemNum < 0" | Dice.infDice citemNum < 0 ] ++ [ "cmaxStairsNum < 0" | Dice.infDice cmaxStairsNum < 0 ] ++ [ "stairs suggested, but not defined" | Dice.supDice cmaxStairsNum > 0 && null cstairFreq ] -- | Validate all cave kinds. -- Note that names don't have to be unique: we can have several variants -- of a cave with a given name. validateAll :: [CaveKind] -> ContentData CaveKind -> [Text] validateAll _ _ = [] -- so far, always valid -- * Mandatory item groups mandatoryGroups :: [GroupName CaveKind] mandatoryGroups = [DEFAULT_RANDOM] pattern DEFAULT_RANDOM :: GroupName CaveKind pattern DEFAULT_RANDOM = GroupName "default random" makeData :: RK.RuleContent -> [CaveKind] -> [GroupName CaveKind] -> [GroupName CaveKind] -> ContentData CaveKind makeData corule content groupNamesSingleton groupNames = makeContentData "CaveKind" cname cfreq (validateSingle corule) validateAll content groupNamesSingleton (mandatoryGroups ++ groupNames) LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Content/FactionKind.hs0000644000000000000000000002013107346545000023760 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | The type of kinds of factions present in a game, both human -- and computer-controlled. module Game.LambdaHack.Content.FactionKind ( FactionKind(..), makeData , HiCondPoly, HiSummand, HiPolynomial, HiIndeterminant(..) , TeamContinuity(..), Outcome(..) , teamExplorer, hiHeroLong, hiHeroMedium, hiHeroShort, hiDweller , victoryOutcomes, deafeatOutcomes , nameOutcomePast, nameOutcomeVerb, endMessageOutcome #ifdef EXPOSE_INTERNAL -- * Internal operations , validateSingle, validateAll #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.Text as T import GHC.Generics (Generic) import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs -- | Properties of a particular faction. data FactionKind = FactionKind { fname :: Text -- ^ name of the faction , ffreq :: Freqs FactionKind -- ^ frequency within groups , fteam :: TeamContinuity -- ^ the team the faction identifies with -- across games and modes , fgroups :: Freqs ItemKind -- ^ names of actor groups that may naturally fall under faction's -- control, e.g., upon spawning; make sure all groups that may -- ever continuousely generate actors, e.g., through spawning -- or summoning, are mentioned in at least one faction kind; -- groups of initial faction actors don't need to be included , fskillsOther :: Ability.Skills -- ^ fixed skill modifiers to the non-leader -- actors; also summed with skills implied -- by @fdoctrine@ (which is not fixed) , fcanEscape :: Bool -- ^ the faction can escape the dungeon , fneverEmpty :: Bool -- ^ the faction declared killed if no actors , fhiCondPoly :: HiCondPoly -- ^ score formula (conditional polynomial) , fhasGender :: Bool -- ^ whether actors have gender , finitDoctrine :: Ability.Doctrine -- ^ initial faction's non-leaders doctrine , fspawnsFast :: Bool -- ^ spawns fast enough that switching pointman to another level -- to optimize spawning is a winning tactics, which would spoil -- the fun, so switching is disabled in UI and AI clients , fhasPointman :: Bool -- ^ whether the faction can have a pointman , fhasUI :: Bool -- ^ does the faction have a UI client -- (for control or passive observation) , finitUnderAI :: Bool -- ^ is the faction initially under AI control , fenemyTeams :: [TeamContinuity] -- ^ teams starting at war with the faction , falliedTeams :: [TeamContinuity] -- ^ teams starting allied with the faction } deriving (Show, Eq, Generic) instance Binary FactionKind -- | Team continuity index. Starting with 1. See the comment for `FactionId`. newtype TeamContinuity = TeamContinuity Int deriving (Show, Eq, Ord, Enum, Generic) instance Binary TeamContinuity -- | Conditional polynomial representing score calculation for this faction. type HiCondPoly = [HiSummand] type HiSummand = (HiPolynomial, [Outcome]) type HiPolynomial = [(HiIndeterminant, Double)] data HiIndeterminant = HiConst | HiLoot | HiSprint | HiBlitz | HiSurvival | HiKill | HiLoss deriving (Show, Eq, Generic) instance Binary HiIndeterminant -- | Outcome of a game. data Outcome = Escape -- ^ the faction escaped the dungeon alive | Conquer -- ^ the faction won by eliminating all rivals | Defeated -- ^ the faction lost the game in another way | Killed -- ^ the faction was eliminated | Restart -- ^ game is restarted; the quitter quit | Camping -- ^ game is supended deriving (Show, Eq, Ord, Enum, Bounded, Generic) instance Binary Outcome teamExplorer :: TeamContinuity teamExplorer = TeamContinuity 1 hiHeroLong, hiHeroMedium, hiHeroShort, hiDweller :: HiCondPoly hiHeroShort = [ ( [(HiLoot, 100)] , [minBound..maxBound] ) , ( [(HiConst, 100)] , victoryOutcomes ) , ( [(HiSprint, -500)] -- speed matters, but only if fast enough , victoryOutcomes ) , ( [(HiSurvival, 10)] -- few points for surviving long , deafeatOutcomes ) ] hiHeroMedium = [ ( [(HiLoot, 200)] -- usually no loot, but if so, no harm , [minBound..maxBound] ) , ( [(HiConst, 200), (HiLoss, -10)] -- normally, always positive , victoryOutcomes ) , ( [(HiSprint, -500)] -- speed matters, but only if fast enough , victoryOutcomes ) , ( [(HiBlitz, -100)] -- speed matters always , victoryOutcomes ) , ( [(HiSurvival, 10)] -- few points for surviving long , deafeatOutcomes ) ] -- Heroes in long crawls rejoice in loot. hiHeroLong = [ ( [(HiLoot, 10000)] -- multiplied by fraction of collected , [minBound..maxBound] ) , ( [(HiConst, 15)] -- a token bonus in case all loot lost, but victory , victoryOutcomes ) , ( [(HiSprint, -20000)] -- speedrun bonus, if below this number of turns , victoryOutcomes ) , ( [(HiBlitz, -100)] -- speed matters always , victoryOutcomes ) , ( [(HiSurvival, 10)] -- few points for surviving long , deafeatOutcomes ) ] -- Spawners get no points from loot, but try to kill -- all opponents fast or at least hold up for long. hiDweller = [ ( [(HiConst, 1000)] -- no loot, so big win reward , victoryOutcomes ) , ( [(HiConst, 1000), (HiLoss, -10)] , victoryOutcomes ) , ( [(HiSprint, -1000)] -- speedrun bonus, if below , victoryOutcomes ) , ( [(HiBlitz, -100)] -- speed matters , victoryOutcomes ) , ( [(HiSurvival, 100)] , deafeatOutcomes ) ] victoryOutcomes :: [Outcome] victoryOutcomes = [Escape, Conquer] deafeatOutcomes :: [Outcome] deafeatOutcomes = [Defeated, Killed, Restart] nameOutcomePast :: Outcome -> Text nameOutcomePast = \case Escape -> "emerged victorious" Conquer -> "vanquished all opposition" Defeated -> "got decisively defeated" Killed -> "got eliminated" Restart -> "resigned prematurely" Camping -> "set camp" nameOutcomeVerb :: Outcome -> Text nameOutcomeVerb = \case Escape -> "emerge victorious" Conquer -> "vanquish all opposition" Defeated -> "be decisively defeated" Killed -> "be eliminated" Restart -> "resign prematurely" Camping -> "set camp" endMessageOutcome :: Outcome -> Text endMessageOutcome = \case Escape -> "Can it be done more efficiently, though?" Conquer -> "Can it be done in a better style, though?" Defeated -> "Let's hope your new overlords let you live." Killed -> "Let's hope a rescue party arrives in time!" Restart -> "This time for real." Camping -> "See you soon, stronger and braver!" validateSingle :: FactionKind -> [Text] validateSingle FactionKind{..} = [ "fname longer than 50" | T.length fname > 50 ] ++ [ "fskillsOther not negative:" <+> fname | any ((>= 0) . snd) $ Ability.skillsToList fskillsOther ] ++ let checkLoveHate l team = [ "love-hate relationship for" <+> tshow team | team `elem` l ] in concatMap (checkLoveHate fenemyTeams) falliedTeams ++ let checkDipl field l team = [ "self-diplomacy in" <+> field | length (elemIndices team l) > 1 ] in concatMap (checkDipl "fenemyTeams" fenemyTeams) fenemyTeams ++ concatMap (checkDipl "falliedTeams" falliedTeams) falliedTeams -- | Validate game faction kinds together. validateAll :: [FactionKind] -> ContentData FactionKind -> [Text] validateAll _ _ = [] -- so far, always valid makeData :: [FactionKind] -> [GroupName FactionKind] -> [GroupName FactionKind] -> ContentData FactionKind makeData = makeContentData "FactionKind" fname ffreq validateSingle validateAll LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Content/ItemKind.hs0000644000000000000000000010154407346545000023303 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | The type of kinds of weapons, treasure, organs, blasts, etc. module Game.LambdaHack.Content.ItemKind ( pattern CONDITION, pattern COMMON_ITEM, pattern S_BONUS_HP, pattern S_BRACED, pattern S_ASLEEP, pattern S_IMPRESSED, pattern S_CURRENCY, pattern MOBILE , pattern CRAWL_ITEM, pattern TREASURE, pattern ANY_SCROLL, pattern ANY_GLASS, pattern ANY_POTION, pattern ANY_FLASK, pattern EXPLOSIVE, pattern ANY_JEWELRY, pattern S_SINGLE_SPARK, pattern S_SPARK, pattern S_FRAGRANCE , pattern HORROR, pattern VALUABLE, pattern UNREPORTED_INVENTORY, pattern AQUATIC , ItemKind(..), makeData , Aspect(..), Effect(..), Condition(..), DetectKind(..) , TimerDice, ThrowMod(..) , ItemSymbolsUsedInEngine(..), emptyItemSymbolsUsedInEngine , boostItemKindList, forApplyEffect, forDamageEffect, isDamagingKind , strengthOnCombine, strengthOnSmash, getDropOrgans , getMandatoryPresentAsFromKind, isEffEscape, isEffEscapeOrAscend , timeoutAspect, orEffect, onSmashEffect, onCombineEffect, alwaysDudEffect , damageUsefulness, verbMsgNoLonger, verbMsgLess, toVelocity, toLinger , timerNone, isTimerNone, foldTimer, toOrganBad, toOrganGood, toOrganNoTimer , validateSingle , mandatoryGroups, mandatoryGroupsSingleton #ifdef EXPOSE_INTERNAL -- * Internal operations , boostItemKind, onSmashOrCombineEffect , validateAll, validateDups, validateDamage #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import Data.Hashable (Hashable) import qualified Data.Text as T import GHC.Generics (Generic) import qualified System.Random.SplitMix32 as SM import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random (nextRandom) import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal import Game.LambdaHack.Definition.Flavour -- * Mandatory item groups mandatoryGroupsSingleton :: [GroupName ItemKind] mandatoryGroupsSingleton = [S_BONUS_HP, S_BRACED, S_ASLEEP, S_IMPRESSED, S_CURRENCY] pattern S_BONUS_HP, S_BRACED, S_ASLEEP, S_IMPRESSED, S_CURRENCY :: GroupName ItemKind mandatoryGroups :: [GroupName ItemKind] mandatoryGroups = [CONDITION, COMMON_ITEM, MOBILE] pattern CONDITION, COMMON_ITEM, MOBILE :: GroupName ItemKind -- From Preferences.hs pattern CONDITION = GroupName "condition" pattern COMMON_ITEM = GroupName "common item" -- Assorted pattern S_BONUS_HP = GroupName "bonus HP" pattern S_BRACED = GroupName "braced" pattern S_ASLEEP = GroupName "asleep" pattern S_IMPRESSED = GroupName "impressed" pattern S_CURRENCY = GroupName "currency" pattern MOBILE = GroupName "mobile" -- * Optional item groups pattern S_SINGLE_SPARK, S_SPARK, S_FRAGRANCE, CRAWL_ITEM, TREASURE, ANY_SCROLL, ANY_GLASS, ANY_POTION, ANY_FLASK, EXPLOSIVE, ANY_JEWELRY, VALUABLE, UNREPORTED_INVENTORY, AQUATIC, HORROR :: GroupName ItemKind -- Used in Preferences.hs pattern S_SINGLE_SPARK = GroupName "single spark" pattern S_SPARK = GroupName "spark" pattern S_FRAGRANCE = GroupName "fragrance" pattern CRAWL_ITEM = GroupName "curious item" -- to be used only in long scenarios, such as multi-level dungeon crawl; -- may be a powerful or a mundate item, unlike @TREASURE@ item pattern TREASURE = GroupName "treasure" -- particularly powerful items, but may appear in any scenario pattern ANY_SCROLL = GroupName "scroll" pattern ANY_GLASS = GroupName "glass" pattern ANY_POTION = GroupName "potion" pattern ANY_FLASK = GroupName "flask" pattern EXPLOSIVE = GroupName "explosive" pattern ANY_JEWELRY = GroupName "jewelry" -- * Used elsewhere pattern VALUABLE = GroupName "valuable" pattern UNREPORTED_INVENTORY = GroupName "unreported inventory" pattern AQUATIC = GroupName "aquatic" pattern HORROR = GroupName "horror" -- | Item properties that are fixed for a given kind of items. -- Of these, aspects and effects are jointly called item powers. -- Note that this type is mutually recursive with 'Effect' and `Aspect`. data ItemKind = ItemKind { isymbol :: ContentSymbol ItemKind -- ^ map symbol , iname :: Text -- ^ generic name; is pluralized if needed , ifreq :: Freqs ItemKind -- ^ frequency within groups , iflavour :: [Flavour] -- ^ possible flavours , icount :: Dice.Dice -- ^ created in that quantity , irarity :: Rarity -- ^ rarity on given depths , iverbHit :: Text -- ^ the verb for hitting , iweight :: Int -- ^ weight in grams , idamage :: Dice.Dice -- ^ basic kinetic damage , iaspects :: [Aspect] -- ^ affect the actor continuously , ieffects :: [Effect] -- ^ cause the effects when triggered , ikit :: [(GroupName ItemKind, CStore)] -- ^ accompanying organs and equipment , idesc :: Text -- ^ description } deriving Show -- No Eq and Ord to make extending logically sound -- | Aspects of items. Aspect @AddSkill@ is additive (starting at 0) -- for all items wielded by an actor and it affects the actor. -- The others affect only the item in question, not the actor carrying it, -- and so are not additive in any sense. data Aspect = Timeout Dice.Dice -- ^ specifies the cooldown before an item may be -- applied again; if a copy of an item is applied -- manually (not via periodic activation), -- all effects on a single copy of the item are -- disabled until the copy recharges for the given -- time expressed in game turns; all copies -- recharge concurrently | AddSkill Ability.Skill Dice.Dice -- ^ bonus to a skill; in content, avoid boosting -- skills such as SkApply via permanent equipment, -- to avoid micromanagement through swapping items -- among party members before each skill use | SetFlag Ability.Flag -- ^ item feature | ELabel Text -- ^ extra label of the item; it's not pluralized | ToThrow ThrowMod -- ^ parameters modifying a throw | PresentAs (GroupName ItemKind) -- ^ until identified, presents as this unique kind | EqpSlot Ability.EqpSlot -- ^ AI and UI flag that leaks item intended use | Odds Dice.Dice [Aspect] [Aspect] -- ^ if level-scaled dice roll > 50, -- pick the former aspects, otherwise the latter deriving (Show, Eq) -- | Effects of items. Can be invoked by the item wielder to affect -- another actor or the wielder himself. -- -- Various effects of an item kind are all groupped in one list, -- at the cost of conditionals, sequences, etc., to ensure brevity -- and simplicity of content definitions. Most effects fire regardless -- of activation kind (the only exceptions are @OnSmash@ and @OnCombine@ -- effects) so the deviations, handled via the conditionals, are rare -- and the definitions remain simple. Whether an item can be activated -- in any particular way, OTOH, is specified via simple flags elsewhere, -- again, by default, assuming that most activations are possible for all. data Effect = Burn Dice.Dice -- ^ burn with this damage | Explode (GroupName ItemKind) -- ^ explode producing this group of blasts | RefillHP Int -- ^ modify HP of the actor by this amount | RefillCalm Int -- ^ modify Calm of the actor by this amount | Dominate -- ^ change actor's allegiance | Impress -- ^ make actor susceptible to domination | PutToSleep -- ^ put actor to sleep, also calming him | Yell -- ^ make the actor yell/yawn, waking him and others up | Summon (GroupName ItemKind) Dice.Dice -- ^ summon the given number of actors of this group | Ascend Bool -- ^ ascend to another level of the dungeon | Escape -- ^ escape from the dungeon | Paralyze Dice.Dice -- ^ paralyze for this many game clips | ParalyzeInWater Dice.Dice -- ^ paralyze for this many game clips due to water | InsertMove Dice.Dice -- ^ give actor this many extra tenths of actor move | Teleport Dice.Dice -- ^ teleport actor across rougly this distance | CreateItem (Maybe Int) CStore (GroupName ItemKind) TimerDice -- ^ create an item of the group and insert into the store with the given -- random timer; it cardinality not specified, roll it | DestroyItem Int Int CStore (GroupName ItemKind) -- ^ destroy some items of the group from the store; see below about Ints | ConsumeItems [(Int, GroupName ItemKind)] [(Int, GroupName ItemKind)] -- ^ @ConsumeItems toUse toDestroy@ uses items matching @toUse@ -- (destroys non-durable, without invoking OnSmash effects; -- applies normal effects of durable, without destroying them; -- the same behaviour as when transforming terrain using items) -- and destroys items matching @toDestroy@, invoking no effects, -- regardless of durability; -- the items are taken from @CGround@ (but not from @CEqp@), -- preferring non-durable (since durable can harm when used -- and may be more vauable when destroyed); if not all required items -- are present, no item are destroyed; if an item belongs to many groups -- in the sum of @toUse@ and @toDestroy@, it counts for all -- (otherwise, some orders of destroying would succeed, -- while others would not); even if item durable, as many copies -- are needed as specified, not just one applied many times; -- items are first destroyed and then, if any copies left, applied | DropItem Int Int CStore (GroupName ItemKind) -- ^ make the actor drop items of the given group from the given store; -- the first integer says how many item kinds to drop, the second, -- how many copies of each kind to drop; -- for non-organs, beware of not dropping all kinds, or cluttering -- store with rubbish becomes beneficial | Recharge Int Dice.Dice -- ^ reduce the cooldown period of this number of discharged items -- in the victim's equipment and organs by this dice of game clips; -- if the result is negative, set to 0, instantly recharging the item; -- starts with weapons with highest raw damage in equipment, -- then among organs, then non-weapons in equipment and among organs; -- beware of exploiting for healing periodic items | Discharge Int Dice.Dice -- ^ increase the cooldown period of this number of fully recharged items -- in the victim's equipment and organs by this dice of game clips; -- starts with weapons with highest raw damage in equipment, -- then among organs, then non-weapons in equipment and among organs; -- beware of exploiting for hunger inducing and similar organs | PolyItem -- ^ get a suitable (i.e., numerous enough) non-unique common item stack -- on the floor and polymorph it to a stack of random common items, -- with current depth coefficient | RerollItem -- ^ get a suitable (i.e., with any random aspects) single item -- (even unique) on the floor and change the random bonuses -- of the items randomly, with maximal depth coefficient | DupItem -- ^ exactly duplicate a single non-unique, non-valuable item on the floor | Identify -- ^ find a suitable (i.e., not identified) item, starting from -- the floor, and identify it | Detect DetectKind Int -- ^ detect something on the map in the given radius | SendFlying ThrowMod -- ^ send an actor flying (push or pull, depending) | PushActor ThrowMod -- ^ push an actor | PullActor ThrowMod -- ^ pull an actor | ApplyPerfume -- ^ remove all smell on the level | AtMostOneOf [Effect] -- ^ try to trigger a single random effect of the list | OneOf [Effect] -- ^ trigger, with equal probability, -- one of the effects that don't end with @UseDud@ | OnSmash Effect -- ^ trigger the effect when item smashed (not when applied nor meleed) | OnCombine Effect -- ^ trigger the effect only when the actor explicitly desires -- to combine items or otherwise subtly tinker with an -- item or a tile, e.g., craft items from other items in a workshop; -- in particular, don't trigger the effects when entering a tile; -- trigger exclusively the effects when activating walkable terrain | OnUser Effect -- ^ apply the effect to the user, not the victim | NopEffect -- ^ nothing happens, @UseDud@, no description | AndEffect Effect Effect -- ^ only fire second effect if first activated | OrEffect Effect Effect -- ^ only fire second effect if first not activated | SeqEffect [Effect] -- ^ fire all effects in order; always suceed | When Condition Effect -- ^ if condition not met, fail without a message; -- better avoided, since AI can't value it well | Unless Condition Effect -- ^ if condition met, fail without a message; -- better avoided, since AI can't value it well | IfThenElse Condition Effect Effect -- ^ conditional effect; -- better avoided, since AI can't value it well | VerbNoLonger Text Text -- ^ a sentence with the actor causing the effect as subject, the given -- texts as the verb and the ending of the sentence (that may be -- ignored when the message is cited, e.g., as heard by someone) -- that is emitted when an activation causes an item to expire; -- no spam is emitted if a projectile; the ending is appended -- without a space in-between | VerbMsg Text Text -- ^ as @VerbNoLonger@ but that is emitted whenever the item is activated; | VerbMsgFail Text Text -- ^ as @VerbMsg@, but a failed effect (returns @UseId@) deriving (Show, Eq) data Condition = HpLeq Int | HpGeq Int | CalmLeq Int | CalmGeq Int | TriggeredBy Ability.ActivationFlag deriving (Show, Eq) data DetectKind = DetectAll | DetectActor | DetectLoot | DetectExit | DetectHidden | DetectEmbed | DetectStash deriving (Show, Eq) -- | Specification of how to randomly roll a timer at item creation -- to obtain a fixed timer for the item's lifetime. data TimerDice = TimerNone | TimerGameTurn Dice.Dice | TimerActorTurn Dice.Dice deriving Eq instance Show TimerDice where show TimerNone = "0" show (TimerGameTurn nDm) = show nDm ++ " " ++ if nDm == 1 then "turn" else "turns" show (TimerActorTurn nDm) = show nDm ++ " " ++ if nDm == 1 then "move" else "moves" -- | Parameters modifying a throw of a projectile or flight of pushed actor. -- Not additive and don't start at 0. data ThrowMod = ThrowMod { throwVelocity :: Int -- ^ fly with this percentage of base throw speed , throwLinger :: Int -- ^ fly for this percentage of 2 turns , throwHP :: Int -- ^ start flight with this many HP } deriving (Show, Eq, Ord, Generic) instance Binary ThrowMod instance Hashable ThrowMod data ItemSymbolsUsedInEngine = ItemSymbolsUsedInEngine { rsymbolProjectile :: ContentSymbol ItemKind , rsymbolLight :: ContentSymbol ItemKind , rsymbolTool :: ContentSymbol ItemKind , rsymbolSpecial :: ContentSymbol ItemKind , rsymbolGold :: ContentSymbol ItemKind , rsymbolNecklace :: ContentSymbol ItemKind , rsymbolRing :: ContentSymbol ItemKind , rsymbolPotion :: ContentSymbol ItemKind , rsymbolFlask :: ContentSymbol ItemKind , rsymbolScroll :: ContentSymbol ItemKind , rsymbolTorsoArmor :: ContentSymbol ItemKind , rsymbolMiscArmor :: ContentSymbol ItemKind , rsymbolClothes :: ContentSymbol ItemKind , rsymbolShield :: ContentSymbol ItemKind , rsymbolPolearm :: ContentSymbol ItemKind , rsymbolEdged :: ContentSymbol ItemKind , rsymbolHafted :: ContentSymbol ItemKind , rsymbolWand :: ContentSymbol ItemKind , rsymbolFood :: ContentSymbol ItemKind } emptyItemSymbolsUsedInEngine :: ItemSymbolsUsedInEngine emptyItemSymbolsUsedInEngine = ItemSymbolsUsedInEngine { rsymbolProjectile = toContentSymbol '0' , rsymbolLight = toContentSymbol '0' , rsymbolTool = toContentSymbol '0' , rsymbolSpecial = toContentSymbol '0' , rsymbolGold = toContentSymbol '0' , rsymbolNecklace = toContentSymbol '0' , rsymbolRing = toContentSymbol '0' , rsymbolPotion = toContentSymbol '0' , rsymbolFlask = toContentSymbol '0' , rsymbolScroll = toContentSymbol '0' , rsymbolTorsoArmor = toContentSymbol '0' , rsymbolMiscArmor = toContentSymbol '0' , rsymbolClothes = toContentSymbol '0' , rsymbolShield = toContentSymbol '0' , rsymbolPolearm = toContentSymbol '0' , rsymbolEdged = toContentSymbol '0' , rsymbolHafted = toContentSymbol '0' , rsymbolWand = toContentSymbol '0' , rsymbolFood = toContentSymbol '0' } boostItemKindList :: SM.SMGen -> [ItemKind] -> [ItemKind] boostItemKindList _ [] = [] boostItemKindList initialGen l = let (r, _) = nextRandom (length l - 1) initialGen in case splitAt r l of (pre, i : post) -> pre ++ boostItemKind i : post _ -> error $ "" `showFailure` l boostItemKind :: ItemKind -> ItemKind boostItemKind i = let mainlineLabel (label, _) = label `elem` [COMMON_ITEM, CRAWL_ITEM, TREASURE] in if any mainlineLabel (ifreq i) then i { ifreq = (COMMON_ITEM, 10000) : filter (not . mainlineLabel) (ifreq i) , iaspects = delete (SetFlag Ability.Unique) $ iaspects i } else i -- | Whether the effect has a chance of exhibiting any potentially -- noticeable behaviour, except when the item is destroyed or combined. -- We assume at least one of @OneOf@ effects must be noticeable. forApplyEffect :: Effect -> Bool forApplyEffect eff = case eff of OnSmash{} -> False OnCombine{} -> False OnUser eff1 -> forApplyEffect eff1 NopEffect -> False AndEffect eff1 eff2 -> forApplyEffect eff1 || forApplyEffect eff2 OrEffect eff1 eff2 -> forApplyEffect eff1 || forApplyEffect eff2 SeqEffect effs -> any forApplyEffect effs When _ eff1 -> forApplyEffect eff1 Unless _ eff1 -> forApplyEffect eff1 IfThenElse _ eff1 eff2 -> forApplyEffect eff1 || forApplyEffect eff2 VerbNoLonger{} -> False VerbMsg{} -> False VerbMsgFail{} -> False ParalyzeInWater{} -> False -- barely noticeable, spams when resisted _ -> True -- | Whether a non-nested effect always applies raw damage. forDamageEffect :: Effect -> Bool forDamageEffect eff = case eff of Burn{} -> True RefillHP n | n < 0 -> True _ -> False -- | Whether an item is damaging. Such items may trigger embedded items -- and may collide with bursting items mid-air. isDamagingKind :: ItemKind -> Bool isDamagingKind itemKind = Dice.infDice (idamage itemKind) > 0 || any forDamageEffect (ieffects itemKind) isEffEscape :: Effect -> Bool isEffEscape Escape{} = True isEffEscape (AtMostOneOf l) = any isEffEscape l isEffEscape (OneOf l) = any isEffEscape l isEffEscape (OnCombine eff) = isEffEscape eff isEffEscape (OnUser eff) = isEffEscape eff isEffEscape (AndEffect eff1 eff2) = isEffEscape eff1 || isEffEscape eff2 isEffEscape (OrEffect eff1 eff2) = isEffEscape eff1 || isEffEscape eff2 isEffEscape (SeqEffect effs) = any isEffEscape effs isEffEscape (When _ eff) = isEffEscape eff isEffEscape (Unless _ eff) = isEffEscape eff isEffEscape (IfThenElse _ eff1 eff2) = isEffEscape eff1 || isEffEscape eff2 isEffEscape _ = False isEffEscapeOrAscend :: Effect -> Bool isEffEscapeOrAscend Ascend{} = True isEffEscapeOrAscend Escape{} = True isEffEscapeOrAscend (AtMostOneOf l) = any isEffEscapeOrAscend l isEffEscapeOrAscend (OneOf l) = any isEffEscapeOrAscend l isEffEscapeOrAscend (OnCombine eff) = isEffEscapeOrAscend eff isEffEscapeOrAscend (OnUser eff) = isEffEscapeOrAscend eff isEffEscapeOrAscend (AndEffect eff1 eff2) = isEffEscapeOrAscend eff1 || isEffEscapeOrAscend eff2 isEffEscapeOrAscend (OrEffect eff1 eff2) = isEffEscapeOrAscend eff1 || isEffEscapeOrAscend eff2 isEffEscapeOrAscend (SeqEffect effs) = any isEffEscapeOrAscend effs isEffEscapeOrAscend (When _ eff) = isEffEscapeOrAscend eff isEffEscapeOrAscend (Unless _ eff) = isEffEscapeOrAscend eff isEffEscapeOrAscend (IfThenElse _ eff1 eff2) = isEffEscapeOrAscend eff1 || isEffEscapeOrAscend eff2 isEffEscapeOrAscend _ = False timeoutAspect :: Aspect -> Bool timeoutAspect Timeout{} = True timeoutAspect _ = False orEffect :: Effect -> Bool orEffect OrEffect{} = True orEffect _ = False onSmashEffect :: Effect -> Bool onSmashEffect OnSmash{} = True onSmashEffect _ = False onCombineEffect :: Effect -> Bool onCombineEffect OnCombine{} = True onCombineEffect _ = False onSmashOrCombineEffect :: Effect -> Bool onSmashOrCombineEffect OnSmash{} = True onSmashOrCombineEffect OnCombine{} = True onSmashOrCombineEffect _ = False alwaysDudEffect :: Effect -> Bool alwaysDudEffect OnSmash{} = True alwaysDudEffect OnCombine{} = True alwaysDudEffect NopEffect = True alwaysDudEffect _ = False strengthOnSmash :: ItemKind -> [Effect] strengthOnSmash = let f (OnSmash eff) = [eff] f _ = [] in concatMap f . ieffects strengthOnCombine :: ItemKind -> [Effect] strengthOnCombine = let f (OnCombine eff) = [eff] f _ = [] in concatMap f . ieffects getDropOrgans :: ItemKind -> [GroupName ItemKind] getDropOrgans = let f (DestroyItem _ _ COrgan grp) = [grp] f (DropItem _ _ COrgan grp) = [grp] f Impress = [S_IMPRESSED] f (AtMostOneOf l) = concatMap f l -- even remote possibility accepted f (OneOf l) = concatMap f l -- even remote possibility accepted f (OnUser eff) = f eff -- no OnCombine, because checked for potions, etc. f (AndEffect eff1 eff2) = f eff1 ++ f eff2 -- not certain, but accepted f (OrEffect eff1 eff2) = f eff1 ++ f eff2 -- not certain, but accepted f (SeqEffect effs) = concatMap f effs f (When _ eff) = f eff f (Unless _ eff) = f eff f (IfThenElse _ eff1 eff2) = f eff1 ++ f eff2 f _ = [] in concatMap f . ieffects -- Anything under @Odds@ is ignored, because it's not mandatory. getMandatoryPresentAsFromKind :: ItemKind -> Maybe (GroupName ItemKind) getMandatoryPresentAsFromKind itemKind = let f (PresentAs grp) = [grp] f _ = [] in listToMaybe $ concatMap f (iaspects itemKind) damageUsefulness :: ItemKind -> Double damageUsefulness itemKind = let v = min 1000 (10 * Dice.meanDice (idamage itemKind)) in assert (v >= 0) v verbMsgNoLonger :: Text -> Effect verbMsgNoLonger name = VerbNoLonger ("be no longer" <+> name) "." verbMsgLess :: Text -> Effect verbMsgLess name = VerbMsg ("appear less" <+> name) "." toVelocity :: Int -> Aspect toVelocity n = ToThrow $ ThrowMod n 100 1 toLinger :: Int -> Aspect toLinger n = ToThrow $ ThrowMod 100 n 1 timerNone :: TimerDice timerNone = TimerNone isTimerNone :: TimerDice -> Bool isTimerNone tim = tim == TimerNone foldTimer :: a -> (Dice.Dice -> a) -> (Dice.Dice -> a) -> TimerDice -> a foldTimer a fgame factor tim = case tim of TimerNone -> a TimerGameTurn nDm -> fgame nDm TimerActorTurn nDm -> factor nDm toOrganBad :: GroupName ItemKind -> Dice.Dice -> Effect toOrganBad grp nDm = CreateItem Nothing COrgan grp (TimerGameTurn nDm) toOrganGood :: GroupName ItemKind -> Dice.Dice -> Effect toOrganGood grp nDm = CreateItem Nothing COrgan grp (TimerActorTurn nDm) toOrganNoTimer :: GroupName ItemKind -> Effect toOrganNoTimer grp = CreateItem Nothing COrgan grp TimerNone -- | Catch invalid item kind definitions. validateSingle :: ItemSymbolsUsedInEngine -> ItemKind -> [Text] validateSingle itemSymbols ik@ItemKind{..} = ["iname longer than 23" | T.length iname > 23] ++ ["icount < 0" | Dice.infDice icount < 0] ++ validateRarity irarity ++ validateDamage idamage -- Reject duplicate Timeout, because it's not additive. ++ (let ts = filter timeoutAspect iaspects in ["more than one Timeout specification" | length ts > 1]) ++ [ "Conflicting Fragile and Durable" | SetFlag Ability.Fragile `elem` iaspects && SetFlag Ability.Durable `elem` iaspects ] ++ (let f :: Aspect -> Bool f EqpSlot{} = True f _ = False ts = filter f iaspects equipable = SetFlag Ability.Equipable `elem` iaspects meleeable = SetFlag Ability.Meleeable `elem` iaspects likelyTemplate = case ifreq of [(grp, 1)] -> "unknown" `T.isSuffixOf` fromGroupName grp _ -> False likelyException = isymbol `elem` [ rsymbolFood itemSymbols , rsymbolNecklace itemSymbols , rsymbolWand itemSymbols ] || likelyTemplate in [ "EqpSlot specified but not Equipable nor Meleeable" | length ts == 1 && not equipable && not meleeable ] ++ [ "EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template" | not likelyException && null ts && (equipable || meleeable) ] ++ [ "More than one EqpSlot specified" | length ts > 1 ] ) ++ [ "Redundant Equipable or Meleeable" | SetFlag Ability.Equipable `elem` iaspects && SetFlag Ability.Meleeable `elem` iaspects ] ++ [ "Conflicting Durable and Blast" | SetFlag Ability.Durable `elem` iaspects && SetFlag Ability.Blast `elem` iaspects ] ++ [ "Conflicting Durable and Condition" | SetFlag Ability.Durable `elem` iaspects && SetFlag Ability.Condition `elem` iaspects ] ++ [ "Conflicting Blast and Condition" | SetFlag Ability.Blast `elem` iaspects && SetFlag Ability.Condition `elem` iaspects ] ++ (let f :: Aspect -> Bool f ELabel{} = True f _ = False ts = filter f iaspects in ["more than one ELabel specification" | length ts > 1]) ++ (let f :: Aspect -> Bool f ToThrow{} = True f _ = False ts = filter f iaspects in ["more than one ToThrow specification" | length ts > 1]) ++ (let f :: Aspect -> Bool f PresentAs{} = True f _ = False ts = filter f iaspects in ["more than one PresentAs specification" | length ts > 1]) ++ concatMap (validateDups ik . SetFlag) [minBound .. maxBound] ++ (let f :: Effect -> Bool f VerbNoLonger{} = True f _ = False in validateOnlyOne ieffects "VerbNoLonger" f) -- may be duped if nested ++ (let f :: Effect -> Bool f VerbMsg{} = True f _ = False in validateOnlyOne ieffects "VerbMsg" f) -- may be duplicated if nested ++ (let f :: Effect -> Bool f VerbMsgFail{} = True f _ = False in validateOnlyOne ieffects "VerbMsgFail" f) -- may be duped if nested ++ validateNotNested ieffects "OnSmash or OnCombine" onSmashOrCombineEffect -- but duplicates permitted ++ let nonPositiveBurn :: Effect -> Bool nonPositiveBurn (Burn d) = Dice.infDice d <= 0 nonPositiveBurn _ = False containingNonPositiveBurn = filter (checkSubEffectProp nonPositiveBurn) ieffects in [ "effects with non-positive Burn:" <+> tshow containingNonPositiveBurn | not $ null containingNonPositiveBurn ] ++ let emptyOneOf :: Effect -> Bool emptyOneOf (AtMostOneOf []) = True emptyOneOf (OneOf []) = True emptyOneOf _ = False containingEmptyOneOf = filter (checkSubEffectProp emptyOneOf) ieffects in [ "effects with empty AtMostOneOf or OneOf:" <+> tshow containingEmptyOneOf | not $ null containingEmptyOneOf ] ++ (let nonPositiveEffect :: Effect -> Bool nonPositiveEffect (CreateItem (Just n) _ _ _) | n <= 0 = True nonPositiveEffect (DestroyItem n k _ _) | n <= 0 || k <= 0 = True nonPositiveEffect (ConsumeItems tools raw) | any ((<= 0) . fst) (tools ++ raw) = True nonPositiveEffect (DropItem n k _ _) | n <= 0 || k <= 0 = True nonPositiveEffect (Detect _ n) | n <= 0 = True nonPositiveEffect _ = False containingNonPositiveEffect = filter (checkSubEffectProp nonPositiveEffect) ieffects in [ "effects with forbidden non-positive parameters:" <+> tshow containingNonPositiveEffect | not $ null containingNonPositiveEffect ]) ++ (let nonPositiveEffect :: Effect -> Bool nonPositiveEffect (Summon _ d) | Dice.infDice d <= 0 = True nonPositiveEffect (Paralyze d) | Dice.infDice d <= 0 = True nonPositiveEffect (ParalyzeInWater d) | Dice.infDice d <= 0 = True nonPositiveEffect (InsertMove d) | Dice.infDice d <= 0 = True nonPositiveEffect (Teleport d) | Dice.infDice d <= 0 = True nonPositiveEffect (CreateItem _ _ _ (TimerGameTurn d)) | Dice.infDice d <= 0 = True nonPositiveEffect (CreateItem _ _ _ (TimerActorTurn d)) | Dice.infDice d <= 0 = True nonPositiveEffect (Recharge n d) | n <= 0 || Dice.infDice d <= 0 = True nonPositiveEffect (Discharge n d) | n <= 0 || Dice.infDice d <= 0 = True nonPositiveEffect _ = False containingNonPositiveEffect = filter (checkSubEffectProp nonPositiveEffect) ieffects in [ "effects with forbidden potentially non-positive or negative number or dice:" <+> tshow containingNonPositiveEffect | not $ null containingNonPositiveEffect ]) -- We only check there are no duplicates at top level. If it may be nested, -- it may presumably be duplicated inside the nesting as well. validateOnlyOne :: [Effect] -> Text -> (Effect -> Bool) -> [Text] validateOnlyOne effs t f = let ts = filter f effs in ["more than one" <+> t <+> "specification" | length ts > 1] -- We check it's not nested one nor more levels. validateNotNested :: [Effect] -> Text -> (Effect -> Bool) -> [Text] validateNotNested effs t f = let g (AtMostOneOf l) = any h l g (OneOf l) = any h l g (OnSmash effect) = h effect g (OnCombine effect) = h effect g (OnUser effect) = h effect g (AndEffect eff1 eff2) = h eff1 || h eff2 g (OrEffect eff1 eff2) = h eff1 || h eff2 g (SeqEffect effs2) = any h effs2 g (When _ effect) = h effect g (Unless _ effect) = h effect g (IfThenElse _ eff1 eff2) = h eff1 || h eff2 g _ = False h effect = f effect || g effect ts = filter g effs in [ "effect" <+> t <+> "should be specified at top level, not nested" | not (null ts) ] checkSubEffectProp :: (Effect -> Bool) -> Effect -> Bool checkSubEffectProp f eff = let g (AtMostOneOf l) = any h l g (OneOf l) = any h l g (OnSmash effect) = h effect g (OnCombine effect) = h effect g (OnUser effect) = h effect g (AndEffect eff1 eff2) = h eff1 || h eff2 g (OrEffect eff1 eff2) = h eff1 || h eff2 g (SeqEffect effs) = any h effs g (When _ effect) = h effect g (Unless _ effect) = h effect g (IfThenElse _ eff1 eff2) = h eff1 || h eff2 g _ = False h effect = f effect || g effect in h eff validateDups :: ItemKind -> Aspect -> [Text] validateDups ItemKind{..} feat = let ts = filter (== feat) iaspects in ["more than one" <+> tshow feat <+> "specification" | length ts > 1] validateDamage :: Dice.Dice -> [Text] validateDamage dice = [ "potentially negative dice:" <+> tshow dice | Dice.infDice dice < 0] -- | Validate all item kinds. validateAll :: [ItemKind] -> ContentData ItemKind -> [Text] validateAll content coitem = let f :: Aspect -> Bool f PresentAs{} = True f _ = False wrongPresentAsGroups = [ cgroup | k <- content , let (cgroup, notSingleton) = case find f (iaspects k) of Just (PresentAs grp) | not $ oisSingletonGroup coitem grp -> (grp, True) _ -> (undefined, False) , notSingleton ] in [ "PresentAs groups not singletons:" <+> tshow wrongPresentAsGroups | not $ null wrongPresentAsGroups ] makeData :: ItemSymbolsUsedInEngine -> [ItemKind] -> [GroupName ItemKind] -> [GroupName ItemKind] -> ContentData ItemKind makeData itemSymbols content groupNamesSingleton groupNames = let allGroupNamesTooLong = filter ((> 23) . T.length . fromGroupName) $ groupNamesSingleton ++ groupNames in assert (null allGroupNamesTooLong `blame` "ItemKind: some item group names too long" `swith` allGroupNamesTooLong) $ makeContentData "ItemKind" iname ifreq (validateSingle itemSymbols) validateAll content (mandatoryGroupsSingleton ++ groupNamesSingleton) (mandatoryGroups ++ groupNames) LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Content/ModeKind.hs0000644000000000000000000001314607346545000023271 0ustar0000000000000000-- | The type of game modes. module Game.LambdaHack.Content.ModeKind ( pattern CAMPAIGN_SCENARIO, pattern INSERT_COIN , ModeKind(..), makeData , Caves, Roster , mandatoryGroups #ifdef EXPOSE_INTERNAL -- * Internal operations , validateSingle, validateAll, validateSingleRoster #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Text as T import Game.LambdaHack.Content.CaveKind (CaveKind) import Game.LambdaHack.Content.FactionKind (FactionKind (..), Outcome (..)) import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal -- | Game mode specification. data ModeKind = ModeKind { mname :: Text -- ^ short description , mfreq :: Freqs ModeKind -- ^ frequency within groups , mtutorial :: Bool -- ^ whether to show tutorial messages, etc. , mattract :: Bool -- ^ whether this is an attract mode , mroster :: Roster -- ^ factions taking part in the game , mcaves :: Caves -- ^ arena of the game , mendMsg :: [(Outcome, Text)] -- ^ messages displayed at each particular game ends; if message empty, -- the screen is skipped , mrules :: Text -- ^ rules note , mdesc :: Text -- ^ description , mreason :: Text -- ^ why/when the mode should be played , mhint :: Text -- ^ hints in case player faces difficulties } deriving Show -- | Requested cave groups for particular level intervals. type Caves = [([Int], [GroupName CaveKind])] -- | The specification of factions and of levels, numbers and groups -- of their initial members. type Roster = [( GroupName FactionKind , [(Int, Dice.Dice, GroupName ItemKind)] )] -- | Catch invalid game mode kind definitions. validateSingle :: ContentData FactionKind -> ModeKind -> [Text] validateSingle cofact ModeKind{..} = [ "mname longer than 22" | T.length mname > 22 ] ++ let f cave@(ns, l) = [ "not enough or too many levels for required cave groups:" <+> tshow cave | length ns /= length l ] in concatMap f mcaves ++ validateSingleRoster cofact mcaves mroster -- | Checks, in particular, that there is at least one faction with fneverEmpty -- or the game would get stuck as soon as the dungeon is devoid of actors. validateSingleRoster :: ContentData FactionKind -> Caves -> Roster -> [Text] validateSingleRoster cofact caves roster = let emptyGroups = filter (not . oexistsGroup cofact) $ map fst roster in [ "the following faction kind groups have no representative with non-zero frequency:" <+> T.intercalate ", " (map displayGroupName emptyGroups) | not $ null emptyGroups ] ++ let fkKeepsAlive acc _ _ fk = acc && fneverEmpty fk -- all of group elements have to keep level alive, hence conjunction fkGroupKeepsAlive (fkGroup, _) = ofoldlGroup' cofact fkGroup fkKeepsAlive True in [ "potentially no faction keeps the dungeon alive" | not $ any fkGroupKeepsAlive roster ] ++ let fkHasUIor acc _ _ fk = acc || fhasUI fk -- single group element having UI already incurs the risk -- of duplication, hence disjunction fkGroupHasUIor (fkGroup, _) = ofoldlGroup' cofact fkGroup fkHasUIor False in [ "potentially more than one UI client" | length (filter fkGroupHasUIor roster) > 1 ] ++ let fkHasUIand acc _ _ fk = acc && fhasUI fk -- single group element missing UI already incurs the risk -- of no UI in the whole game, hence disjunction fkGroupHasUIand (fkGroup, _) = ofoldlGroup' cofact fkGroup fkHasUIand True in [ "potentially less than one UI client" | not (any fkGroupHasUIand roster) ] ++ let fkTokens acc _ _ fk = fteam fk : acc fkGroupTokens (fkGroup, _) = ofoldlGroup' cofact fkGroup fkTokens [] tokens = concatMap (nub . sort . fkGroupTokens) roster nubTokens = nub . sort $ tokens in [ "potentially duplicate team continuity token" | length tokens /= length nubTokens ] ++ let keys = concatMap fst caves -- permitted to be empty, for tests minD = minimum keys maxD = maximum keys f (_, l) = concatMap g l g i3@(ln, _, _) = [ "initial actor levels not among caves:" <+> tshow i3 | ln `notElem` keys ] in concatMap f roster ++ [ "player is confused by both positive and negative level numbers" | not (null keys) && signum minD /= signum maxD ] ++ [ "player is confused by level numer zero" | 0 `elem` keys ] -- | Validate game mode kinds together. validateAll :: [ModeKind] -> ContentData ModeKind -> [Text] validateAll _ _ = [] -- so far, always valid -- * Mandatory item groups mandatoryGroups :: [GroupName ModeKind] mandatoryGroups = [CAMPAIGN_SCENARIO, INSERT_COIN] pattern CAMPAIGN_SCENARIO, INSERT_COIN :: GroupName ModeKind pattern CAMPAIGN_SCENARIO = GroupName "campaign scenario" pattern INSERT_COIN = GroupName "insert coin" makeData :: ContentData FactionKind -> [ModeKind] -> [GroupName ModeKind] -> [GroupName ModeKind] -> ContentData ModeKind makeData cofact content groupNamesSingleton groupNames = makeContentData "ModeKind" mname mfreq (validateSingle cofact) validateAll content groupNamesSingleton (mandatoryGroups ++ groupNames) LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Content/PlaceKind.hs0000644000000000000000000001247607346545000023436 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | The type of place kinds. Every room in the game is an instantiated -- place kind. module Game.LambdaHack.Content.PlaceKind ( PlaceKind(..), makeData , Cover(..), Fence(..) , PlaceEntry(..), deadEndId, overridePlaceKind, override2PlaceKind #ifdef EXPOSE_INTERNAL -- * Internal operations , validateSingle, validateAll #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import GHC.Generics (Generic) import Game.LambdaHack.Content.TileKind (TileKind) import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal -- | Parameters for the generation of small areas within a dungeon level. data PlaceKind = PlaceKind { pname :: Text -- ^ short description, singular or plural , pfreq :: Freqs PlaceKind -- ^ frequency within groups , prarity :: Rarity -- ^ rarity on given depths , pcover :: Cover -- ^ how to fill whole place using the corner , pfence :: Fence -- ^ whether to fence place with solid border , ptopLeft :: [Text] -- ^ plan of the top-left corner of the place , plegendDark :: EM.EnumMap Char (GroupName TileKind) -- ^ dark legend , plegendLit :: EM.EnumMap Char (GroupName TileKind) -- ^ lit legend } deriving Show -- No Eq and Ord to make extending logically sound -- | A method of filling the whole area (except for CVerbatim and CMirror, -- which are just placed in the middle of the area) by transforming -- a given corner. data Cover = CAlternate -- ^ reflect every other corner, overlapping 1 row and column | CStretch -- ^ fill symmetrically 4 corners and stretch their borders | CReflect -- ^ tile separately and symmetrically quarters of the place | CVerbatim -- ^ just build the given interior, without filling the area | CMirror -- ^ build the given interior in one of 4 mirrored variants deriving (Show, Eq) -- | The choice of a fence type for the place. data Fence = FWall -- ^ put a solid wall fence around the place | FFloor -- ^ leave an empty space, like the room's floor | FGround -- ^ leave an empty space, like the cave's ground | FNone -- ^ skip the fence and fill all with the place proper deriving (Show, Eq) -- | Places are rooms and other dungeon features, their names can be seen -- on a level map by aiming at a position that is an entry to the place -- (an individual entrance point, an approach area around the place -- or a phantom entry not on the map, but only used for statistics -- to witness the place exists). Entries are proxies for initial places -- created on the level (which may be otherwise eradicated by burrowing -- the walls, etc.) and so used for dungeon statistics. -- The statistics are presented in the @Dashboard/displace place lore@ menu. data PlaceEntry = PEntry (ContentId PlaceKind) | PAround (ContentId PlaceKind) | PExists (ContentId PlaceKind) deriving (Show, Eq, Generic) instance Binary PlaceEntry deadEndId :: ContentId PlaceKind {-# INLINE deadEndId #-} deadEndId = toContentId 0 overridePlaceKind :: [(Char, GroupName TileKind)] -> PlaceKind -> PlaceKind overridePlaceKind l pk = pk { plegendDark = EM.fromList l `EM.union` plegendDark pk , plegendLit = EM.fromList l `EM.union` plegendLit pk } override2PlaceKind :: [(Char, GroupName TileKind)] -> [(Char, GroupName TileKind)] -> PlaceKind -> PlaceKind override2PlaceKind lDark lLit pk = pk { plegendDark = EM.fromList lDark `EM.union` plegendDark pk , plegendLit = EM.fromList lLit `EM.union` plegendLit pk } -- | Catch invalid place kind definitions. In particular, verify that -- the top-left corner map is rectangular and not empty. validateSingle :: ContentData TileKind -> PlaceKind -> [Text] validateSingle cotile PlaceKind{..} = let dxcorner = case ptopLeft of [] -> 0 l : _ -> T.length l inLegend :: Text -> EM.EnumMap Char (GroupName TileKind) -> Char -> [Text] inLegend _ _ 'X' = [] -- special placeholder symbol; TODO: unhardwire inLegend legendName m c = case EM.lookup c m of Nothing -> [tshow c <+> "tile code not found in" <+> legendName] Just grp -> [ tshow c <+> "tile code has group" <+> displayGroupName grp <+> "with null frequency in tile content" | not $ oexistsGroup cotile grp ] inLegendAll legendName m = concatMap (inLegend legendName m) (concatMap T.unpack ptopLeft) in [ "top-left corner empty" | dxcorner == 0 ] ++ [ "top-left corner not rectangular" | any ((/= dxcorner) . T.length) ptopLeft ] ++ inLegendAll "plegendDark" plegendDark ++ inLegendAll "plegendLit" plegendLit ++ validateRarity prarity -- | Validate all place kinds. validateAll :: [PlaceKind] -> ContentData PlaceKind -> [Text] validateAll _ _ = [] -- so far, always valid makeData :: ContentData TileKind -> [PlaceKind] -> [GroupName PlaceKind] -> [GroupName PlaceKind] -> ContentData PlaceKind makeData cotile = makeContentData "PlaceKind" pname pfreq (validateSingle cotile) validateAll LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Content/RuleKind.hs0000644000000000000000000000516107346545000023312 0ustar0000000000000000-- | The type of game rules and assorted game data. module Game.LambdaHack.Content.RuleKind ( RuleContent(..), emptyRuleContent, makeData #ifdef EXPOSE_INTERNAL -- * Internal operations , emptyRuleContentRaw, validateSingle #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Ini as Ini import qualified Data.Ini.Types as Ini import Data.Version import Game.LambdaHack.Content.ItemKind (ItemSymbolsUsedInEngine, emptyItemSymbolsUsedInEngine) import Game.LambdaHack.Definition.Defs -- | The type of game rules and assorted game data. data RuleContent = RuleContent { rtitle :: String -- ^ title of the game (not lib) , rWidthMax :: X -- ^ maximum level width , rHeightMax :: Y -- ^ maximum level height , rexeVersion :: Version -- ^ version of the game , rcfgUIName :: FilePath -- ^ name of the UI config file , rcfgUIDefault :: (Text, Ini.Config) -- ^ the default UI settings config file , rwriteSaveClips :: Int -- ^ game saved that often (not on browser) , rleadLevelClips :: Int -- ^ server switches leader level that often , rscoresFileName :: FilePath -- ^ name of the scores file , rnearby :: Int -- ^ what is a close distance between actors , rstairWordCarried :: [Text] -- ^ words that can't be dropped from stair -- name as it goes through levels , ritemSymbols :: ItemSymbolsUsedInEngine -- ^ item symbols treated specially in engine } emptyRuleContentRaw :: RuleContent emptyRuleContentRaw = RuleContent { rtitle = "" , rWidthMax = 5 , rHeightMax = 2 , rexeVersion = makeVersion [] , rcfgUIName = "" , rcfgUIDefault = ("", Ini.emptyConfig) , rwriteSaveClips = 0 , rleadLevelClips = 0 , rscoresFileName = "" , rnearby = 0 , rstairWordCarried = [] , ritemSymbols = emptyItemSymbolsUsedInEngine } emptyRuleContent :: RuleContent emptyRuleContent = assert (null $ validateSingle emptyRuleContentRaw) emptyRuleContentRaw -- | Catch invalid rule kind definitions. validateSingle :: RuleContent -> [Text] validateSingle RuleContent{..} = [ "rWidthMax < 5" | rWidthMax < 5 ] -- indented (4 prop spaces) text ++ [ "rHeightMax < 2" | rHeightMax < 2 ] -- or 4 tiles of sentinel wall makeData :: RuleContent -> RuleContent makeData rc = let singleOffenders = validateSingle rc in assert (null singleOffenders `blame` "Rule Content not valid" `swith` singleOffenders) rc LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Content/TileKind.hs0000644000000000000000000002451107346545000023300 0ustar0000000000000000-- | The type of tile kinds. Every terrain tile in the game is -- an instantiated tile kind. module Game.LambdaHack.Content.TileKind ( pattern S_UNKNOWN_SPACE, pattern S_UNKNOWN_OUTER_FENCE, pattern S_BASIC_OUTER_FENCE, pattern AQUATIC , TileKind(..), ProjectileTriggers(..), Feature(..) , makeData , isUknownSpace, unknownId , isSuspectKind, isOpenableKind, isClosableKind , talterForStairs, floorSymbol , mandatoryGroups, mandatoryGroupsSingleton #ifdef EXPOSE_INTERNAL -- * Internal operations , validateSingle, validateAll, validateDups #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Word (Word8) import Game.LambdaHack.Content.ItemKind (ItemKind) import Game.LambdaHack.Definition.Color import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal -- | The type of kinds of terrain tiles. See @Tile.hs@ for explanation -- of the absence of a corresponding type @Tile@ that would hold -- particular concrete tiles in the dungeon. -- Note that tile names (and any other content names) should not be plural -- (that would lead to "a stairs"), so "road with cobblestones" is fine, -- but "granite cobblestones" is wrong. -- -- Tile kind for unknown space has the minimal @ContentId@ index. -- The @talter@ for unknown space is @1@ and no other tile kind has that value. data TileKind = TileKind { tsymbol :: Char -- ^ map symbol , tname :: Text -- ^ short description , tfreq :: Freqs TileKind -- ^ frequency within groups , tcolor :: Color -- ^ map color , tcolor2 :: Color -- ^ map color when not in FOV , talter :: Word8 -- ^ minimal skill needed to activate embeds -- and, in case of big actors not standing on -- the tile, to alter the tile in any way , tfeature :: [Feature] -- ^ properties; order matters } deriving Show -- No Eq and Ord to make extending logically sound -- | All possible terrain tile features. data Feature = Embed (GroupName ItemKind) -- ^ initially an item of this group is embedded; -- we assume the item has effects and is supposed to be triggered | OpenTo (GroupName TileKind) -- ^ goes from a closed to closed or open tile when altered | CloseTo (GroupName TileKind) -- ^ goes from an open to open or closed tile when altered | ChangeTo (GroupName TileKind) -- ^ alters tile, but does not change walkability | OpenWith ProjectileTriggers [(Int, GroupName ItemKind)] (GroupName TileKind) -- ^ alters tile, as before, using up all listed items from the ground -- and equipment; the list never empty; for simplicity, such tiles -- are never taken into account when pathfinding | CloseWith ProjectileTriggers [(Int, GroupName ItemKind)] (GroupName TileKind) | ChangeWith ProjectileTriggers [(Int, GroupName ItemKind)] (GroupName TileKind) | HideAs (GroupName TileKind) -- ^ when hidden, looks as the unique tile of the group | BuildAs (GroupName TileKind) -- ^ when generating, may be transformed to the unique tile of the group | RevealAs (GroupName TileKind) -- ^ when generating in opening, can be revealed to belong to the group | ObscureAs (GroupName TileKind) -- ^ when generating in solid wall, can be revealed to belong to the group | Walkable -- ^ actors can walk through | Clear -- ^ actors can see through | Dark -- ^ is not lit with an ambient light | OftenItem -- ^ initial items often generated there | VeryOftenItem -- ^ initial items very often generated there | OftenActor -- ^ initial actors often generated there; -- counterpart of @VeryOftenItem@ for dark places | NoItem -- ^ no items ever generated there | NoActor -- ^ no actors ever generated there | ConsideredByAI -- ^ even if otherwise uninteresting, taken into -- account for triggering by AI | Trail -- ^ used for visible trails throughout the level | Spice -- ^ in place normal legend and in override, -- don't roll a tile kind only once per place, -- but roll for each position; one non-spicy -- (according to frequencies of non-spicy) and -- at most one spicy (according to their frequencies) -- is rolled per place and then, once for each -- position, one of the two is semi-randomly chosen -- (according to their individual frequencies only) deriving (Show, Eq) -- | Marks whether projectiles are permitted to trigger the tile transformation -- action. data ProjectileTriggers = ProjYes | ProjNo deriving (Show, Eq) -- | Validate a single tile kind. validateSingle :: TileKind -> [Text] validateSingle t@TileKind{..} = [ "suspect tile is walkable" | Walkable `elem` tfeature && isSuspectKind t ] ++ [ "openable tile is open" | Walkable `elem` tfeature && isOpenableKind t ] ++ [ "closable tile is closed" | Walkable `notElem` tfeature && isClosableKind t ] ++ [ "walkable tile is considered for activating by AI" | Walkable `elem` tfeature && ConsideredByAI `elem` tfeature ] ++ [ "trail tile not walkable" | Walkable `notElem` tfeature && Trail `elem` tfeature ] ++ [ "OftenItem and NoItem on a tile" | OftenItem `elem` tfeature && NoItem `elem` tfeature ] ++ [ "OftenActor and NoActor on a tile" | OftenItem `elem` tfeature && NoItem `elem` tfeature ] ++ (let f :: Feature -> Bool f OpenTo{} = True f CloseTo{} = True f ChangeTo{} = True f _ = False ts = filter f tfeature in [ "more than one OpenTo, CloseTo and ChangeTo specification" | length ts > 1 ]) ++ (let f :: Feature -> Bool f HideAs{} = True f _ = False ts = filter f tfeature in ["more than one HideAs specification" | length ts > 1]) ++ (let f :: Feature -> Bool f BuildAs{} = True f _ = False ts = filter f tfeature in ["more than one BuildAs specification" | length ts > 1]) ++ concatMap (validateDups t) [ Walkable, Clear, Dark, OftenItem, VeryOftenItem, OftenActor , NoItem, NoActor, ConsideredByAI, Trail, Spice ] validateDups :: TileKind -> Feature -> [Text] validateDups TileKind{..} feat = let ts = filter (== feat) tfeature in ["more than one" <+> tshow feat <+> "specification" | length ts > 1] -- | Validate all tile kinds. -- -- We don't check it any more, but if tiles look the same on the map -- (symbol and color), their substantial features should be the same, too, -- unless there is a good reason they shouldn't. Otherwise the player has -- to inspect manually all the tiles with this look to see if any is special. -- This tends to be tedious. Note that tiles may freely differ wrt text blurb, -- dungeon generation rules, AI preferences, etc., whithout causing the tedium. validateAll :: [TileKind] -> ContentData TileKind -> [Text] validateAll content cotile = let f :: Feature -> Bool f HideAs{} = True f BuildAs{} = True f _ = False wrongGrooup k grp = not (oisSingletonGroup cotile grp) || isJust (grp `lookup` tfreq k) wrongFooAsGroups = [ cgroup | k <- content , let (cgroup, notSingleton) = case find f (tfeature k) of Just (HideAs grp) | wrongGrooup k grp -> (grp, True) Just (BuildAs grp) | wrongGrooup k grp -> (grp, True) _ -> (undefined, False) , notSingleton ] in [ "HideAs or BuildAs groups not singletons or point to themselves:" <+> tshow wrongFooAsGroups | not $ null wrongFooAsGroups ] ++ [ "unknown tile (the first) should be the unknown one" | talter (head content) /= 1 || tname (head content) /= "unknown space" ] ++ [ "no tile other than the unknown (the first) should require skill 1" | any (\tk -> talter tk == 1) (tail content) ] -- * Mandatory item groups mandatoryGroupsSingleton :: [GroupName TileKind] mandatoryGroupsSingleton = [S_UNKNOWN_SPACE, S_UNKNOWN_OUTER_FENCE, S_BASIC_OUTER_FENCE] pattern S_UNKNOWN_SPACE, S_UNKNOWN_OUTER_FENCE, S_BASIC_OUTER_FENCE :: GroupName TileKind mandatoryGroups :: [GroupName TileKind] mandatoryGroups = [] pattern S_UNKNOWN_SPACE = GroupName "unknown space" pattern S_UNKNOWN_OUTER_FENCE = GroupName "unknown outer fence" pattern S_BASIC_OUTER_FENCE = GroupName "basic outer fence" -- * Optional item groups pattern AQUATIC :: GroupName TileKind pattern AQUATIC = GroupName "aquatic" isUknownSpace :: ContentId TileKind -> Bool {-# INLINE isUknownSpace #-} isUknownSpace tt = toContentId 0 == tt unknownId :: ContentId TileKind {-# INLINE unknownId #-} unknownId = toContentId 0 isSuspectKind :: TileKind -> Bool isSuspectKind t = let getTo RevealAs{} = True getTo ObscureAs{} = True getTo _ = False in any getTo $ tfeature t isOpenableKind :: TileKind -> Bool isOpenableKind t = let getTo OpenTo{} = True getTo _ = False in any getTo $ tfeature t isClosableKind :: TileKind -> Bool isClosableKind t = let getTo CloseTo{} = True getTo _ = False in any getTo $ tfeature t talterForStairs :: Word8 talterForStairs = 3 floorSymbol :: Char floorSymbol = '·' -- '\x00B7' -- Alter skill schema: -- 0 can be altered by everybody (escape) -- 1 unknown only -- 2 openable and suspect -- 3 stairs -- 4 closable -- 5 changeable (e.g., caches) -- 10 weak obstructions -- 50 considerable obstructions -- 100 walls -- maxBound impenetrable walls, etc., can never be altered makeData :: [TileKind] -> [GroupName TileKind] -> [GroupName TileKind] -> ContentData TileKind makeData content groupNamesAtMostOne groupNames = makeContentData "TileKind" tname tfreq validateSingle validateAll content (mandatoryGroupsSingleton ++ groupNamesAtMostOne) (mandatoryGroups ++ groupNames) LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Core/0000755000000000000000000000000007346545000020514 5ustar0000000000000000LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Core/Dice.hs0000644000000000000000000002277607346545000021732 0ustar0000000000000000{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Representation of dice scaled with current level depth. module Game.LambdaHack.Core.Dice ( -- * Frequency distribution for casting dice scaled with level depth Dice, AbsDepth(..), castDice, d, dL, z, zL, intToDice, minDice, maxDice , infsupDice, supDice, infDice, meanDice, reduceDice -- * Dice for rolling a pair of integer parameters representing coordinates. , DiceXY(..), supDiceXY, infDiceXY ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary -- | Multiple dice rolls, some scaled with current level depth, in which case -- the sum of all rolls is scaled in proportion to current depth -- divided by maximal dungeon depth. -- -- The simple dice should have positive number of rolls and number of sides. -- -- The @Num@ instance doesn't have @abs@ nor @signum@ defined, -- because the functions for computing infimum, supremum and mean dice -- results would be too costly. data Dice = DiceI Int | DiceD Int Int | DiceDL Int Int | DiceZ Int Int | DiceZL Int Int | DicePlus Dice Dice | DiceTimes Dice Dice | DiceNegate Dice | DiceMin Dice Dice | DiceMax Dice Dice deriving Eq instance Show Dice where show = stripOuterParens . showDiceWithParens stripOuterParens :: String -> String stripOuterParens s@('(' : rest) = case uncons $ reverse rest of Just (')', middle) -> reverse middle _ -> s stripOuterParens s = s showDiceWithParens :: Dice -> String showDiceWithParens = sh where sh dice1 = case dice1 of DiceI k -> show k DiceD n k -> show n ++ "d" ++ show k DiceDL n k -> show n ++ "dL" ++ show k DiceZ n k -> show n ++ "z" ++ show k DiceZL n k -> show n ++ "zL" ++ show k DicePlus d1 (DiceNegate d2) -> wrapInParens $ sh d1 ++ "-" ++ sh d2 DicePlus (DiceNegate d1) d2 -> wrapInParens $ "-" ++ sh d1 ++ "+" ++ sh d2 DicePlus d1 (DicePlus d2 d3) -> sh $ DicePlus (DicePlus d1 d2) d3 DicePlus (DicePlus d1 d2) d3 -> wrapInParens $ stripOuterParens (sh $ DicePlus d1 d2) ++ "+" ++ sh d3 DicePlus d1 d2 -> wrapInParens $ sh d1 ++ "+" ++ sh d2 DiceTimes d1 d2 -> wrapInParens $ sh d1 ++ "*" ++ sh d2 DiceNegate d1 -> wrapInParens $ "-" ++ sh d1 DiceMin d1 d2 -> wrapInParens $ "min" ++ sh d1 ++ sh d2 DiceMax d1 d2 -> wrapInParens $ "max" ++ sh d1 ++ sh d2 wrapInParens :: String -> String wrapInParens "" = "" wrapInParens t = "(" <> t <> ")" instance Num Dice where d1 + d2 = DicePlus d1 d2 d1 * d2 = DiceTimes d1 d2 d1 - d2 = d1 + DiceNegate d2 negate = DiceNegate abs = undefined -- very costly to compute mean exactly signum = undefined -- very costly to compute mean exactly fromInteger n = DiceI (fromInteger n) -- | Absolute depth in the dungeon. When used for the maximum depth -- of the whole dungeon, this can be different than dungeon size, -- e.g., when the dungeon is branched, and it can even be different -- than the length of the longest branch, if levels at some depths are missing. newtype AbsDepth = AbsDepth Int deriving (Show, Eq, Ord, Binary) -- | Cast dice scaled with current level depth. When scaling, we round up, -- so that the value of @1 `dL` 1@ is @1@ even at the lowest level, -- but so is the value of @1 `dL` depth@. -- -- The implementation calls RNG as many times as there are dice rolls, -- which is costly, so content should prefer to cast fewer dice -- and then multiply them by a constant. If rounded results are not desired -- (often they are, to limit the number of distinct item varieties -- in inventory), another dice may be added to the result. -- -- A different possible implementation, with dice represented as @Frequency@, -- makes only one RNG call per dice, but due to lists lengths proportional -- to the maximal value of the dice, it's is intractable for 1000d1000 -- and problematic already for 100d100. castDice :: forall m. Monad m => ((Int, Int) -> m Int) -> AbsDepth -> AbsDepth -> Dice -> m Int {-# INLINE castDice #-} castDice randomR (AbsDepth lvlDepth) (AbsDepth maxDepth) dice = do let !_A = assert (lvlDepth >= 0 && lvlDepth <= maxDepth `blame` "invalid depth for dice rolls" `swith` (lvlDepth, maxDepth)) () castNK n start k = if start == k then return $! n * k else do let f !acc 0 = return acc f acc count = do r <- randomR (start, k) f (acc + r) (count - 1) f 0 n scaleL k = (k * max 1 lvlDepth) `divUp` max 1 maxDepth castD :: Dice -> m Int castD dice1 = case dice1 of DiceI k -> return k DiceD n k -> castNK n 1 k DiceDL n k -> scaleL <$> castNK n 1 k DiceZ n k -> castNK n 0 (k - 1) DiceZL n k -> scaleL <$> castNK n 0 (k - 1) DicePlus d1 d2 -> do k1 <- castD d1 k2 <- castD d2 return $! k1 + k2 DiceTimes d1 d2 -> do k1 <- castD d1 k2 <- castD d2 return $! k1 * k2 DiceNegate d1 -> do k <- castD d1 return $! negate k DiceMin d1 d2 -> do k1 <- castD d1 k2 <- castD d2 return $! min k1 k2 DiceMax d1 d2 -> do k1 <- castD d1 k2 <- castD d2 return $! max k1 k2 castD dice -- | A die, rolled the given number of times. E.g., @1 `d` 2@ rolls 2-sided -- die one time. d :: Int -> Int -> Dice d n k = assert (n > 0 && k > 0 `blame` "die must be positive" `swith` (n, k)) $ DiceD n k -- | A die rolled the given number of times, -- with the result scaled with dungeon level depth. dL :: Int -> Int -> Dice dL n k = assert (n > 0 && k > 0 `blame` "die must be positive" `swith` (n, k)) $ DiceDL n k -- | A die, starting from zero, ending at one less than second argument, -- rolled the given number of times. E.g., @1 `z` 1@ always rolls zero. z :: Int -> Int -> Dice z n k = assert (n > 0 && k > 0 `blame` "die must be positive" `swith` (n, k)) $ DiceZ n k -- | A die, starting from zero, ending at one less than second argument, -- rolled the given number of times, -- with the result scaled with dungeon level depth. zL :: Int -> Int -> Dice zL n k = assert (n > 0 && k > 0 `blame` "die must be positive" `swith` (n, k)) $ DiceZL n k intToDice :: Int -> Dice intToDice = DiceI minDice :: Dice -> Dice -> Dice minDice = DiceMin maxDice :: Dice -> Dice -> Dice maxDice = DiceMax -- | Minimal and maximal possible value of the dice. -- -- @divUp@ in the implementation corresponds to @ceiling@, -- applied to results of @meanDice@ elsewhere in the code, -- and prevents treating 1d1-power effects (on shallow levels) as null effects. infsupDice :: Dice -> (Int, Int) infsupDice dice1 = case dice1 of DiceI k -> (k, k) DiceD n k -> (n, n * k) DiceDL n k -> (1, n * k) -- bottom and top level considered DiceZ n k -> (0, n * (k - 1)) DiceZL n k -> (0, n * (k - 1)) -- bottom and top level considered DicePlus d1 d2 -> let (infD1, supD1) = infsupDice d1 (infD2, supD2) = infsupDice d2 in (infD1 + infD2, supD1 + supD2) DiceTimes (DiceI k) d2 -> let (infD2, supD2) = infsupDice d2 in if k >= 0 then (k * infD2, k * supD2) else (k * supD2, k * infD2) DiceTimes d1 (DiceI k) -> let (infD1, supD1) = infsupDice d1 in if k >= 0 then (infD1 * k, supD1 * k) else (supD1 * k, infD1 * k) -- Multiplication other than the two cases above is unlikely, but here it is. DiceTimes d1 d2 -> let (infD1, supD1) = infsupDice d1 (infD2, supD2) = infsupDice d2 options = [infD1 * infD2, infD1 * supD2, supD1 * supD2, supD1 * infD2] in (minimum options, maximum options) DiceNegate d1 -> let (infD1, supD1) = infsupDice d1 in (negate supD1, negate infD1) DiceMin d1 d2 -> let (infD1, supD1) = infsupDice d1 (infD2, supD2) = infsupDice d2 in (min infD1 infD2, min supD1 supD2) DiceMax d1 d2 -> let (infD1, supD1) = infsupDice d1 (infD2, supD2) = infsupDice d2 in (max infD1 infD2, max supD1 supD2) -- | Maximal value of dice. The scaled part taken assuming median level. supDice :: Dice -> Int supDice = snd . infsupDice -- | Minimal value of dice. The scaled part taken assuming median level. infDice :: Dice -> Int infDice = fst . infsupDice -- | Mean value of dice. The scaled part taken assuming median level, -- but not taking into account rounding up, and so too low, especially -- for dice small compared to depth. To fix this, depth would need -- to be taken as argument. meanDice :: Dice -> Double meanDice dice1 = case dice1 of DiceI k -> intToDouble k DiceD n k -> intToDouble (n * (k + 1)) / 2 DiceDL n k -> intToDouble (n * (k + 1)) / 4 DiceZ n k -> intToDouble (n * k) / 2 DiceZL n k -> intToDouble (n * k) / 4 DicePlus d1 d2 -> meanDice d1 + meanDice d2 DiceTimes d1 d2 -> meanDice d1 * meanDice d2 -- I hope this is that simple DiceNegate d1 -> negate $ meanDice d1 DiceMin d1 d2 -> min (meanDice d1) (meanDice d2) -- crude approximation, only exact if the distributions disjoint DiceMax d1 d2 -> max (meanDice d1) (meanDice d2) -- crude approximation reduceDice :: Dice -> Maybe Int reduceDice d1 = let (infD1, supD1) = infsupDice d1 in if infD1 == supD1 then Just infD1 else Nothing -- | Dice for rolling a pair of integer parameters pertaining to, -- respectively, the X and Y cartesian 2D coordinates. data DiceXY = DiceXY Dice Dice deriving Show -- | Maximal value of DiceXY. supDiceXY :: DiceXY -> (Int, Int) supDiceXY (DiceXY x y) = (supDice x, supDice y) -- | Minimal value of DiceXY. infDiceXY :: DiceXY -> (Int, Int) infDiceXY (DiceXY x y) = (infDice x, infDice y) LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Core/Frequency.hs0000644000000000000000000000721107346545000023012 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, DeriveTraversable, TupleSections #-} -- | A list of entities with relative frequencies of appearance. module Game.LambdaHack.Core.Frequency ( -- * The @Frequency@ type Frequency -- * Construction , uniformFreq, toFreq, maxBoundInt32 -- * Transformation , scaleFreq -- * Consumption , nullFreq, runFrequency, nameFrequency ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Applicative import Data.Int (Int32) import GHC.Generics (Generic) maxBoundInt32 :: Int maxBoundInt32 = toIntegralCrash (maxBound :: Int32) -- | The frequency distribution type. Not normalized (operations may -- or may not group the same elements and sum their frequencies). However, -- elements with less than zero frequency are removed upon construction. -- -- The @Eq@ instance compares raw representations, not relative, -- normalized frequencies, so operations don't need to preserve -- the expected equalities. data Frequency a = Frequency { runFrequency :: [(Int, a)] -- ^ give acces to raw frequency values , nameFrequency :: Text -- ^ short description for debug, etc. } deriving (Show, Eq, Ord, Foldable, Traversable, Generic) instance Monad Frequency where Frequency xs name >>= f = Frequency [ #ifdef WITH_EXPENSIVE_ASSERTIONS assert (toInteger p * toInteger q <= toInteger maxBoundInt32 `blame` (name, map fst xs)) #endif (p * q, y) | (p, x) <- xs , (q, y) <- runFrequency (f x) ] ("bind (" <> name <> ")") instance Functor Frequency where fmap f (Frequency xs name) = Frequency (map (second f) xs) name instance Applicative Frequency where {-# INLINE pure #-} pure x = Frequency [(1, x)] "pure" Frequency fs fname <*> Frequency ys yname = Frequency [ #ifdef WITH_EXPENSIVE_ASSERTIONS assert (toInteger p * toInteger q <= toInteger maxBoundInt32 `blame` (fname, map fst fs, yname, map fst ys)) #endif (p * q, f y) | (p, f) <- fs , (q, y) <- ys ] ("(" <> fname <> ") <*> (" <> yname <> ")") instance MonadPlus Frequency where mplus (Frequency xs xname) (Frequency ys yname) = let name = case (xs, ys) of ([], []) -> "[]" ([], _) -> yname (_, []) -> xname _ -> "(" <> xname <> ") ++ (" <> yname <> ")" in Frequency (xs ++ ys) name mzero = Frequency [] "[]" instance Alternative Frequency where (<|>) = mplus empty = mzero -- | Uniform discrete frequency distribution. uniformFreq :: Text -> [a] -> Frequency a uniformFreq name l = Frequency (map (1,) l) name -- | Takes a name and a list of frequencies and items -- into the frequency distribution. toFreq :: Text -> [(Int, a)] -> Frequency a toFreq name l = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (all (\(p, _) -> toInteger p <= toInteger maxBoundInt32) l `blame` (name, map fst l)) $ #endif Frequency (filter ((> 0 ) . fst) l) name -- | Scale frequency distribution, multiplying it -- by a positive integer constant. scaleFreq :: Show a => Int -> Frequency a -> Frequency a scaleFreq n (Frequency xs name) = assert (n > 0 `blame` "non-positive frequency scale" `swith` (name, n, xs)) $ let multN p = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (toInteger p * toInteger n <= toInteger maxBoundInt32 `blame` (n, Frequency xs name)) $ #endif p * n in Frequency (map (first multN) xs) name -- | Test if the frequency distribution is empty. nullFreq :: Frequency a -> Bool nullFreq (Frequency fs _) = null fs LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Core/Prelude.hs0000644000000000000000000001523607346545000022457 0ustar0000000000000000{-# LANGUAGE TypeFamilies #-} {-# OPTIONS_GHC -Wno-orphans #-} -- | Custom Prelude, compatible across many GHC versions. module Game.LambdaHack.Core.Prelude ( module Prelude.Compat , module Control.Monad.Compat , module Data.List.Compat , module Data.Maybe , module Data.Semigroup.Compat , module Control.Exception.Assert.Sugar , Text, (<+>), tshow, divUp, sum, (<$$>), partitionM, length, null, comparing , into, fromIntegralWrap, toIntegralCrash, intToDouble, int64ToDouble , mapM_, forM_, vectorUnboxedUnsafeIndex, unsafeShiftL, unsafeShiftR , (***), (&&&), first, second ) where import Prelude () import Prelude.Compat hiding ( appendFile , foldl , foldl1 , fromIntegral , length , mapM_ , null , readFile , sum , (<>) ) import Control.Applicative import Control.Arrow (first, second, (&&&), (***)) import Control.DeepSeq import Control.Exception.Assert.Sugar (allB, assert, blame, showFailure, swith) import Control.Monad.Compat hiding (forM_, mapM_) import qualified Control.Monad.Compat import Data.Binary import qualified Data.Bits as Bits import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Fixed as Fixed import qualified Data.HashMap.Strict as HM import Data.Hashable import Data.Int (Int64) import Data.Key import Data.List.Compat hiding (foldl, foldl1, length, null, sum) import qualified Data.List.Compat as List import Data.Maybe import Data.Ord (comparing) import Data.Semigroup.Compat (Semigroup ((<>))) import Data.Text (Text) import qualified Data.Text as T (pack) import qualified Data.Time as Time import qualified Data.Vector.Unboxed as U import NLP.Miniutter.English ((<+>)) import qualified NLP.Miniutter.English as MU import qualified Prelude.Compat import Witch (into) -- | Show and pack the result. tshow :: Show a => a -> Text tshow x = T.pack $ show x infixl 7 `divUp` -- | Integer division, rounding up. divUp :: Integral a => a -> a -> a {-# INLINE divUp #-} divUp n k = (n + k - 1) `div` k sum :: Num a => [a] -> a sum = foldl' (+) 0 infixl 4 <$$> (<$$>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b) h <$$> m = fmap h <$> m partitionM :: Applicative m => (a -> m Bool) -> [a] -> m ([a], [a]) {-# INLINE partitionM #-} partitionM p = foldr (\a -> liftA2 (\b -> (if b then first else second) (a :)) (p a)) (pure ([], [])) -- | A version specialized to lists to avoid errors such as taking length -- of @Maybe [a]@ instead of @[a]@. -- Such errors are hard to detect, because the type of elements of the list -- is not constrained. length :: [a] -> Int length = List.length -- | A version specialized to lists to avoid errors such as taking null -- of @Maybe [a]@ instead of @[a]@. -- Such errors are hard to detect, because the type of elements of the list -- is not constrained. null :: [a] -> Bool null = List.null -- Data.Binary orphan instances instance (Enum k, Binary k, Binary e) => Binary (EM.EnumMap k e) where put m = put (EM.size m) >> mapM_ put (EM.toAscList m) get = EM.fromDistinctAscList <$> get instance (Enum k, Binary k) => Binary (ES.EnumSet k) where put m = put (ES.size m) >> mapM_ put (ES.toAscList m) get = ES.fromDistinctAscList <$> get instance Binary Time.NominalDiffTime where get = fmap realToFrac (get :: Get Fixed.Pico) put = (put :: Fixed.Pico -> Put) . realToFrac instance (Hashable k, Eq k, Binary k, Binary v) => Binary (HM.HashMap k v) where get = fmap HM.fromList get put = put . HM.toList -- Data.Key orphan instances type instance Key (EM.EnumMap k) = k instance Zip (EM.EnumMap k) where {-# INLINE zipWith #-} zipWith = EM.intersectionWith instance Enum k => ZipWithKey (EM.EnumMap k) where {-# INLINE zipWithKey #-} zipWithKey = EM.intersectionWithKey instance Enum k => Keyed (EM.EnumMap k) where {-# INLINE mapWithKey #-} mapWithKey = EM.mapWithKey instance Enum k => FoldableWithKey (EM.EnumMap k) where {-# INLINE foldrWithKey #-} foldrWithKey = EM.foldrWithKey instance Enum k => TraversableWithKey (EM.EnumMap k) where traverseWithKey f = fmap EM.fromDistinctAscList . traverse (\(k, v) -> (,) k <$> f k v) . EM.toAscList instance Enum k => Indexable (EM.EnumMap k) where {-# INLINE index #-} index = (EM.!) instance Enum k => Lookup (EM.EnumMap k) where {-# INLINE lookup #-} lookup = EM.lookup instance Enum k => Adjustable (EM.EnumMap k) where {-# INLINE adjust #-} adjust = EM.adjust -- Data.Hashable orphan instances instance (Enum k, Hashable k, Hashable e) => Hashable (EM.EnumMap k e) where hashWithSalt s x = hashWithSalt s (EM.toAscList x) instance (Enum k, Hashable k) => Hashable (ES.EnumSet k) where hashWithSalt s x = hashWithSalt s (ES.toAscList x) -- Control.DeepSeq orphan instances instance NFData MU.Part instance NFData MU.Person instance NFData MU.Polarity -- | Re-exported 'Prelude.fromIntegral', but please give it explicit type -- to make it obvious if wrapping, etc., may occur. Use `toIntegralCrash` -- instead, if possible, because it fails instead of wrapping, etc. -- In general, it may wrap or otherwise lose information. fromIntegralWrap :: (Integral a, Num b) => a -> b fromIntegralWrap = Prelude.Compat.fromIntegral -- | Re-exported 'Data.Bits.toIntegralSized', but please give it explicit type -- to make it obvious if wrapping, etc., may occur and to trigger optimization. -- In general, it may crash. toIntegralCrash :: (Integral a, Integral b, Bits.Bits a, Bits.Bits b) => a -> b {-# INLINE toIntegralCrash #-} toIntegralCrash = fromMaybe (error "toIntegralCrash") . Bits.toIntegralSized intToDouble :: Int -> Double intToDouble = Prelude.Compat.fromIntegral int64ToDouble :: Int64 -> Double int64ToDouble = Prelude.Compat.fromIntegral -- | This has a more specific type (unit result) than normally, to catch errors. mapM_ :: (Foldable t, Monad m) => (a -> m ()) -> t a -> m () mapM_ = Control.Monad.Compat.mapM_ -- | This has a more specific type (unit result) than normally, to catch errors. forM_ :: (Foldable t, Monad m) => t a -> (a -> m ()) -> m () forM_ = Control.Monad.Compat.forM_ vectorUnboxedUnsafeIndex :: U.Unbox a => U.Vector a -> Int -> a vectorUnboxedUnsafeIndex = #ifdef WITH_EXPENSIVE_ASSERTIONS (U.!) -- index checking is sometimes an expensive (kind of) assertion #else U.unsafeIndex #endif unsafeShiftL :: Bits.Bits a => a -> Int -> a unsafeShiftL = #ifdef WITH_EXPENSIVE_ASSERTIONS Bits.shiftL #else Bits.unsafeShiftL #endif unsafeShiftR :: Bits.Bits a => a -> Int -> a unsafeShiftR = #ifdef WITH_EXPENSIVE_ASSERTIONS Bits.shiftR #else Bits.unsafeShiftR #endif LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Core/Random.hs0000644000000000000000000001412307346545000022271 0ustar0000000000000000-- | Representation of probabilities and random computations. module Game.LambdaHack.Core.Random ( -- * The @Rng@ monad Rnd -- * Random operations , randomR, randomR0, nextRandom, randomWord32 , oneOf, shuffle, invalidInformationCode, shuffleExcept, frequency -- * Fractional chance , Chance, chance -- * Casting dice scaled with level , castDice, oddsDice, castDiceXY -- * Specialized monadic folds , foldrM, foldlM' #ifdef EXPOSE_INTERNAL -- * Internal operations , rollFreq #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.Trans.State.Strict as St import Data.Int (Int32) import Data.Ratio import qualified Data.Vector.Unboxed as U import Data.Word (Word16, Word32) import qualified System.Random.SplitMix32 as SM import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Frequency -- | The monad of computations with random generator state. type Rnd a = St.State SM.SMGen a -- | Get a random object within a (inclusive) range with a uniform distribution. randomR :: (Integral a) => (a, a) -> Rnd a {-# INLINE randomR #-} randomR (0, h) = randomR0 h randomR (l, h) | l > h = error "randomR: empty range" randomR (l, h) = St.state $ \g -> let (x, g') = nextRandom (h - l) g in (x + l, g') -- | Generate random 'Integral' in @[0, x]@ range. randomR0 :: (Integral a) => a -> Rnd a {-# INLINE randomR0 #-} randomR0 h = St.state $ nextRandom h -- | Generate a random integral value in @[0, x]@ range, where @x@ is within -- @Int32@. -- -- The limitation to @Int32@ values is needed to keep it working on signed -- types. In package @random@, a much more complex scheme is used -- to keep it working for arbitrary fixed number of bits. nextRandom :: forall a. (Integral a) => a -> SM.SMGen -> (a, SM.SMGen) {-# INLINE nextRandom #-} nextRandom 0 g = (0, g) nextRandom h g = assert (h > 0 && toInteger h <= (toInteger :: Int32 -> Integer) maxBound) $ let (w32, g') = SM.bitmaskWithRejection32' ((fromIntegralWrap :: a -> Word32) h) g -- `fromIntegralWrap` is fine here, because wrapping is OK. x = (fromIntegralWrap :: Word32 -> a) w32 in if x > h then error $ "nextRandom internal error" `showFailure` (toInteger x, toInteger h, w32) else (x, g') -- | Get a random 'Word32' using full range. randomWord32 :: Rnd Word32 {-# INLINE randomWord32 #-} randomWord32 = St.state SM.nextWord32 -- | Get any element of a list with equal probability. oneOf :: [a] -> Rnd a oneOf [] = error $ "oneOf []" `showFailure` () oneOf [x] = return x oneOf xs = do r <- randomR0 (length xs - 1) return $! xs !! r -- | Generates a random permutation. Naive, but good enough for small inputs. shuffle :: Eq a => [a] -> Rnd [a] shuffle [] = return [] shuffle l = do x <- oneOf l (x :) <$> shuffle (delete x l) -- | Code that means the information (e.g., flavour or hidden kind index) -- should be regenerated, because it could not be transferred from -- previous playthrough (it's random in each playthrough or there was -- no previous playthrough). invalidInformationCode :: Word16 invalidInformationCode = maxBound -- | Generates a random permutation, except for the existing mapping. shuffleExcept :: U.Vector Word16 -> Int -> [Word16] -> Rnd [Word16] shuffleExcept v len l0 = assert (len == length l0) $ shuffleE 0 (l0 \\ filter (/= invalidInformationCode) (U.toList v)) where shuffleE :: Int -> [Word16] -> Rnd [Word16] shuffleE i _ | i == len = return [] shuffleE i l = do let a0 = v U.! i if a0 == invalidInformationCode then do a <- oneOf l (a :) <$> shuffleE (succ i) (delete a l) else (a0 :) <$> shuffleE (succ i) l -- | Gen an element according to a frequency distribution. frequency :: Show a => Frequency a -> Rnd a {-# INLINE frequency #-} frequency = St.state . rollFreq -- | Randomly choose an item according to the distribution. rollFreq :: Show a => Frequency a -> SM.SMGen -> (a, SM.SMGen) rollFreq fr g = case runFrequency fr of [] -> error $ "choice from an empty frequency" `showFailure` nameFrequency fr [(n, x)] | n <= 0 -> error $ "singleton void frequency" `showFailure` (nameFrequency fr, n, x) [(_, x)] -> (x, g) -- speedup fs -> let sumf = foldl' (\ !acc (!n, _) -> acc + n) 0 fs (r, ng) = nextRandom (pred sumf) g frec :: Int -> [(Int, a)] -> a frec !m [] = error $ "impossible roll" `showFailure` (nameFrequency fr, fs, m) frec m ((n, x) : _) | m < n = x frec m ((n, _) : xs) = frec (m - n) xs in assert (sumf > 0 `blame` "frequency with nothing to pick" `swith` (nameFrequency fr, fs)) (frec r fs, ng) -- | Fractional chance. type Chance = Rational -- | Give @True@, with probability determined by the fraction. chance :: Chance -> Rnd Bool chance r = do let n = numerator r d = denominator r k <- randomR (1, d) return (k <= n) -- | Cast dice scaled with current level depth. castDice :: Dice.AbsDepth -> Dice.AbsDepth -> Dice.Dice -> Rnd Int castDice = Dice.castDice randomR -- | Cast dice scaled with current level depth and return @True@ -- if the results is greater than 50. oddsDice :: Dice.AbsDepth -> Dice.AbsDepth -> Dice.Dice -> Rnd Bool oddsDice ldepth totalDepth dice = do c <- castDice ldepth totalDepth dice return $! c > 50 -- | Cast dice, scaled with current level depth, for coordinates. castDiceXY :: Dice.AbsDepth -> Dice.AbsDepth -> Dice.DiceXY -> Rnd (Int, Int) castDiceXY ldepth totalDepth (Dice.DiceXY dx dy) = do x <- castDice ldepth totalDepth dx y <- castDice ldepth totalDepth dy return (x, y) foldrM :: Foldable t => (a -> b -> Rnd b) -> b -> t a -> Rnd b foldrM f z0 xs = let f' x (z, g) = St.runState (f x z) g in St.state $ \g -> foldr f' (z0, g) xs foldlM' :: Foldable t => (b -> a -> Rnd b) -> b -> t a -> Rnd b foldlM' f z0 xs = let f' (z, g) x = St.runState (f z x) g in St.state $ \g -> foldl' f' (z0, g) xs LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Definition/0000755000000000000000000000000007346545000021714 5ustar0000000000000000LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Definition/Ability.hs0000644000000000000000000002543207346545000023653 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Abilities of items, actors and factions. module Game.LambdaHack.Definition.Ability ( Skill(..), Skills, Flag(..), ActivationFlag(..), Flags(..) , Doctrine(..), EqpSlot(..) , getSk, addSk, checkFl, skillsToList , zeroSkills, addSkills, sumScaledSkills , nameDoctrine, describeDoctrine, doctrineSkills , blockOnly, meleeAdjacent, meleeAndRanged, ignoreItems #ifdef EXPOSE_INTERNAL -- * Internal operations , scaleSkills #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Hashable (Hashable) import GHC.Generics (Generic) -- | Actor and faction skills. They are a subset of actor aspects. -- See 'Game.LambdaHack.Client.UI.EffectDescription.skillDesc' -- for documentation. data Skill = -- Stats, that is skills affecting permitted actions. SkMove | SkMelee | SkDisplace | SkAlter | SkWait | SkMoveItem | SkProject | SkApply -- Assorted skills. | SkSwimming | SkFlying | SkHurtMelee | SkArmorMelee | SkArmorRanged | SkMaxHP | SkMaxCalm | SkSpeed | SkSight -- ^ FOV radius, where 1 means a single tile FOV area | SkSmell | SkShine | SkNocto | SkHearing | SkAggression | SkOdor | SkDeflectRanged -- ^ intended to reflect how many items granting complete -- invulnerability are among organs and equipment; -- this is not strength of deflection nor duration, etc. | SkDeflectMelee -- ^ see above deriving (Show, Eq, Enum, Bounded, Generic) -- | Strength of particular skills. This is cumulative from actor -- organs and equipment and so pertain to an actor as well as to items. -- -- This representation is sparse, so better than a record when there are more -- item kinds (with few skills) than actors (with many skills), -- especially if the number of skills grows as the engine is developed. -- It's also easier to code and maintain. -- -- The tree is by construction sparse, so the derived equality is semantical. newtype Skills = Skills {skills :: EM.EnumMap Skill Int} deriving (Show, Eq, Ord, Hashable, Binary) -- | Item flag aspects. data Flag = Fragile -- ^ as a projectile, break at target tile, even if no hit; -- also, at each periodic activation a copy is destroyed -- and all other copies require full cooldown (timeout) | Lobable -- ^ drop at target tile, even if no hit | Durable -- ^ don't break even when hitting or applying | Equipable -- ^ AI and UI flag: consider equipping (may or may not -- have 'EqpSlot', e.g., if the benefit is periodic) | Benign -- ^ AI and UI flag: the item is not meant to harm | Precious -- ^ AI and UI flag: don't risk identifying by use; -- also, can't throw or apply if not calm enough; -- also may be used for UI flavour or AI hints | Blast -- ^ the item is an explosion blast particle | Condition -- ^ item is a condition (buff or de-buff) of an actor -- and is displayed as such, not activated at death; -- this differs from belonging to the @CONDITION@ group, -- which doesn't guarantee any behaviour or display, -- but governs removal by items that drop @CONDITION@ | Unique -- ^ at most one copy can ever be generated | MetaGame -- ^ once identified, the item is known until savefile deleted | MinorEffects -- ^ override: the effects on this item are considered -- minor and so possibly not causing identification on use, -- and so this item will identify on pick-up | MinorAspects -- ^ override: don't show question marks by weapons in HUD -- even when unidentified item with this flag equipped | -- The flags below specify all conditions under which the item activates, -- charges permitting, in addition to universal conditions, which are -- hitting an actor as projectiles and being explicitly triggered -- by an actor (item destruction and combining only pertain -- to explicitly listed effects). Meleeable -- ^ meleeing with the item is permitted and so the item -- activates when meleed with | Periodic -- ^ at most one of any copies without cooldown (timeout) -- activates each turn; the cooldown required after -- activation is specified in @Timeout@ (or is zero); -- the initial cooldown can also be specified -- as @TimerDice@ in @CreateItem@ effect; uniquely, this -- activation never destroys a copy, unless item is fragile; -- all this happens only for items in equipment or organs; -- kinetic damage is not applied | UnderRanged -- ^ activates when non-projectile actor with this item -- as equipment or organ is under ranged attack; -- kinetic damage is not applied | UnderMelee -- ^ activates when non-projectile actor with this item -- as equipment or organ is under melee attack; -- kinetic damage is not applied deriving (Show, Eq, Enum, Bounded, Generic) -- | These flags correspond to the last cases of @Flag@ and addtionally -- to all the universal circumstances of item activation, -- under which every item activates (even if vacuusly). data ActivationFlag = ActivationMeleeable | ActivationPeriodic | ActivationUnderRanged | ActivationUnderMelee | -- | From here on, all items affected regardless of their `Flag` content. ActivationProjectile | ActivationTrigger | ActivationOnSmash | ActivationOnCombine | ActivationEmbed | ActivationConsume deriving (Show, Eq) newtype Flags = Flags {flags :: ES.EnumSet Flag} deriving (Show, Eq, Ord, Hashable, Binary) -- | Doctrine of non-leader actors. Apart of determining AI operation, -- each doctrine implies a skill modifier, that is added to the non-leader -- skills defined in @fskillsOther@ field of @FactionKind@. data Doctrine = TExplore -- ^ if enemy nearby, attack, if no items, etc., explore unknown | TFollow -- ^ always follow leader's target or his position if no target | TFollowNoItems -- ^ follow but don't do any item management nor use | TMeleeAndRanged -- ^ only melee and do ranged combat | TMeleeAdjacent -- ^ only melee (or wait) | TBlock -- ^ always only wait, even if enemy in melee range | TRoam -- ^ if enemy nearby, attack, if no items, etc., roam randomly | TPatrol -- ^ find an open and uncrowded area, patrol it according -- to sight radius and fallback temporarily to @TRoam@ -- when enemy is seen by the faction and is within -- the actor's sight radius deriving (Show, Eq, Enum, Bounded, Generic) instance Binary Doctrine instance Hashable Doctrine -- | AI and UI hints about the role of the item. data EqpSlot = EqpSlotMove | EqpSlotMelee | EqpSlotDisplace | EqpSlotAlter | EqpSlotWait | EqpSlotMoveItem | EqpSlotProject | EqpSlotApply | EqpSlotSwimming | EqpSlotFlying | EqpSlotHurtMelee | EqpSlotArmorMelee | EqpSlotArmorRanged | EqpSlotMaxHP | EqpSlotSpeed | EqpSlotSight | EqpSlotShine | EqpSlotMiscBonus | EqpSlotWeaponFast | EqpSlotWeaponBig deriving (Show, Eq, Ord, Enum, Bounded, Generic) instance Binary Skill where put = putWord8 . toEnum . fromEnum get = fmap (toEnum . fromEnum) getWord8 instance Binary Flag where put = putWord8 . toEnum . fromEnum get = fmap (toEnum . fromEnum) getWord8 instance Binary EqpSlot where put = putWord8 . toEnum . fromEnum get = fmap (toEnum . fromEnum) getWord8 instance Hashable Skill instance Hashable Flag instance Hashable EqpSlot getSk :: Skill -> Skills -> Int {-# INLINE getSk #-} getSk sk (Skills skills) = EM.findWithDefault 0 sk skills addSk :: Skill -> Int -> Skills -> Skills addSk sk n = addSkills (Skills $ EM.singleton sk n) checkFl :: Flag -> Flags -> Bool {-# INLINE checkFl #-} checkFl flag (Flags flags) = flag `ES.member` flags skillsToList :: Skills -> [(Skill, Int)] skillsToList (Skills sk) = EM.assocs sk zeroSkills :: Skills zeroSkills = Skills EM.empty -- This avoids costly compaction (required for Eq) even in case of adding -- empty skills, etc. This function is used a lot. addSkills :: Skills -> Skills -> Skills addSkills (Skills sk1) (Skills sk2) = let combine _ s1 s2 = case s1 + s2 of 0 -> Nothing s -> Just s in Skills $ EM.mergeWithKey combine id id sk1 sk2 scaleSkills :: (Skills, Int) -> Skills scaleSkills (_, 0) = zeroSkills scaleSkills (Skills sk, n) = Skills $ EM.map (n *) sk sumScaledSkills :: [(Skills, Int)] -> Skills sumScaledSkills = foldr (addSkills . scaleSkills) zeroSkills nameDoctrine :: Doctrine -> Text nameDoctrine TExplore = "explore" nameDoctrine TFollow = "follow freely" nameDoctrine TFollowNoItems = "follow only" nameDoctrine TMeleeAndRanged = "fight only" nameDoctrine TMeleeAdjacent = "melee only" nameDoctrine TBlock = "block only" nameDoctrine TRoam = "roam freely" nameDoctrine TPatrol = "patrol area" describeDoctrine :: Doctrine -> Text describeDoctrine TExplore = "investigate unknown positions, chase targets" describeDoctrine TFollow = "follow pointman's target or position, grab items" describeDoctrine TFollowNoItems = "follow pointman's target or position, ignore items" describeDoctrine TMeleeAndRanged = "engage in both melee and ranged combat, don't move" describeDoctrine TMeleeAdjacent = "engage exclusively in melee, don't move" describeDoctrine TBlock = "block and wait, don't move" describeDoctrine TRoam = "move freely, chase targets" describeDoctrine TPatrol = "find and patrol an area" doctrineSkills :: Doctrine -> Skills doctrineSkills TExplore = zeroSkills doctrineSkills TFollow = zeroSkills doctrineSkills TFollowNoItems = ignoreItems doctrineSkills TMeleeAndRanged = meleeAndRanged doctrineSkills TMeleeAdjacent = meleeAdjacent doctrineSkills TBlock = blockOnly doctrineSkills TRoam = zeroSkills doctrineSkills TPatrol = zeroSkills minusTen, blockOnly, meleeAdjacent, meleeAndRanged, ignoreItems :: Skills -- To make sure only a lot of weak items can override move-only-leader, etc. minusTen = Skills $ EM.fromDistinctAscList $ zip [SkMove .. SkApply] (repeat (-10)) blockOnly = Skills $ EM.delete SkWait $ skills minusTen meleeAdjacent = Skills $ EM.delete SkMelee $ skills blockOnly -- Melee and reaction fire. meleeAndRanged = Skills $ EM.delete SkProject $ skills meleeAdjacent ignoreItems = Skills $ EM.fromList $ zip [SkMoveItem, SkProject, SkApply] (repeat (-10)) LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Definition/Color.hs0000644000000000000000000002013307346545000023325 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Colours and text attributes. module Game.LambdaHack.Definition.Color ( -- * Colours Color(..) , defFG, isBright, darkCol, brightCol, stdCol, legalFgCol , cVeryBadEvent, cBadEvent, cRisk, cGraveRisk, cVeryGoodEvent, cGoodEvent , cVista, cSleep, cWakeUp, cGreed, cNeutralEvent, cRareNeutralEvent , cIdentification, cMeta, cBoring, cGameOver, cTutorialHint , colorToRGB -- * Complete text attributes , Highlight (..), Attr(..) , highlightToColor, defAttr -- * Characters with attributes , AttrChar(..), AttrCharW32(..) , attrCharToW32, attrCharFromW32 , fgFromW32, bgFromW32, charFromW32, attrFromW32 , spaceAttrW32, nbspAttrW32, trimmedLineAttrW32 , attrChar2ToW32, attrChar1ToW32 ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.DeepSeq import Data.Binary import Data.Bits ((.&.)) import qualified Data.Char as Char import GHC.Generics (Generic) -- | Colours supported by the major frontends. data Color = Black | Red | Green | Brown | Blue | Magenta | Cyan | White | AltWhite -- only use for frontend hacks | BrBlack | BrRed | BrGreen | BrYellow | BrBlue | BrMagenta | BrCyan | BrWhite deriving (Show, Read, Eq, Ord, Enum, Generic) instance Binary Color where put = putWord8 . toEnum . fromEnum get = fmap (toEnum . fromEnum) getWord8 instance NFData Color -- | The default colours, to optimize attribute setting. defFG :: Color defFG = White -- | A helper for the terminal frontends that display bright via bold. isBright :: Color -> Bool isBright c = c > BrBlack -- | Colour sets. Sorted. darkCol, brightCol, stdCol, legalFgCol :: [Color] darkCol = [Red .. Cyan] brightCol = [BrRed .. BrCyan] -- BrBlack is not really that bright stdCol = darkCol ++ brightCol legalFgCol = darkCol ++ [White, BrBlack] ++ brightCol ++ [BrWhite] -- See the discussion of colours and the table of colours at -- https://github.com/LambdaHack/LambdaHack/wiki/Display#colours -- Another mention of colours, concerning terrain, is in PLAYING.md manual. -- The manual and this code should follow the wiki. cVeryBadEvent, cBadEvent, cRisk, cGraveRisk, cVeryGoodEvent, cGoodEvent, cVista, cSleep, cWakeUp, cGreed, cNeutralEvent, cRareNeutralEvent, cIdentification, cMeta, cBoring, cGameOver, cTutorialHint :: Color cVeryBadEvent = Red cBadEvent = BrRed cRisk = Magenta cGraveRisk = BrMagenta cVeryGoodEvent = Green cGoodEvent = BrGreen cVista = BrGreen cSleep = Blue cWakeUp = BrBlue cGreed = BrBlue cNeutralEvent = Cyan cRareNeutralEvent = BrCyan cIdentification = Brown cMeta = BrYellow cBoring = White cGameOver = BrWhite cTutorialHint = BrMagenta -- | Translationg to heavily modified Linux console color RGB values. -- -- Warning: SDL frontend sadly duplicates this code. colorToRGB :: Color -> Text colorToRGB Black = "#000000" colorToRGB Red = "#D50505" colorToRGB Green = "#059D05" colorToRGB Brown = "#CA4A05" colorToRGB Blue = "#0556F4" colorToRGB Magenta = "#AF0EAF" colorToRGB Cyan = "#059696" colorToRGB White = "#B8BFCB" colorToRGB AltWhite = "#C4BEB1" colorToRGB BrBlack = "#6F5F5F" colorToRGB BrRed = "#FF5555" colorToRGB BrGreen = "#65F136" colorToRGB BrYellow = "#EBD642" colorToRGB BrBlue = "#4D98F4" colorToRGB BrMagenta = "#FF77FF" colorToRGB BrCyan = "#52F4E5" colorToRGB BrWhite = "#FFFFFF" -- -- | For reference, the original Linux console colors. -- -- Good old retro feel and more useful than xterm (e.g. brown). -- colorToRGB :: Color -> Text -- colorToRGB Black = "#000000" -- colorToRGB Red = "#AA0000" -- colorToRGB Green = "#00AA00" -- colorToRGB Brown = "#AA5500" -- colorToRGB Blue = "#0000AA" -- colorToRGB Magenta = "#AA00AA" -- colorToRGB Cyan = "#00AAAA" -- colorToRGB White = "#AAAAAA" -- colorToRGB AltWhite = "#AAAAAA" -- colorToRGB BrBlack = "#555555" -- colorToRGB BrRed = "#FF5555" -- colorToRGB BrGreen = "#55FF55" -- colorToRGB BrYellow = "#FFFF55" -- colorToRGB BrBlue = "#5555FF" -- colorToRGB BrMagenta = "#FF55FF" -- colorToRGB BrCyan = "#55FFFF" -- colorToRGB BrWhite = "#FFFFFF" -- | Additional map cell highlight, e.g., a colorful square around the cell -- or a colorful background. -- -- Warning: the highlight underscored by the terminal cursor is -- the maximal element of this type present on a screen, -- so don't add new highlights to the end. data Highlight = HighlightNone | HighlightBackground | HighlightGreen | HighlightBlue | HighlightBrown | HighlightCyan | HighlightGrey | HighlightWhite | HighlightMagenta | HighlightRed | HighlightYellow | HighlightYellowAim | HighlightRedAim | HighlightNoneCursor deriving (Show, Eq, Ord, Enum, Bounded) highlightToColor :: Highlight -> Color highlightToColor hi = case hi of HighlightNone -> Black -- should be transparent, but is OK in web frontend HighlightBackground -> BrBlack -- gets a special colour, but as a background HighlightGreen -> Green HighlightBlue -> Blue HighlightBrown -> Brown HighlightCyan -> Cyan HighlightGrey -> BrBlack HighlightWhite -> White -- bright, but no saturation, so doesn't obscure much HighlightMagenta -> BrMagenta -- very rare, so bright is fine HighlightRed -> Red HighlightYellow -> BrYellow -- obscures, but mostly used around bright white HighlightYellowAim -> BrYellow HighlightRedAim -> Red HighlightNoneCursor -> Black -- used in ANSI for cursor via @maxIndexByA@ -- | Text attributes: foreground color and highlight. data Attr = Attr { fg :: Color -- ^ foreground colour , bg :: Highlight -- ^ highlight } deriving (Show, Eq) -- | The default attribute, to optimize attribute setting. defAttr :: Attr defAttr = Attr defFG HighlightNone -- | Character to display, with its attribute. data AttrChar = AttrChar { acAttr :: Attr , acChar :: Char } deriving (Show, Eq) -- This implementation is faster than @Int@, because some vector updates -- can be done without going to and from @Int@. -- | Optimized representation of 'AttrChar'. newtype AttrCharW32 = AttrCharW32 {attrCharW32 :: Word32} deriving (Show, Eq, Ord, Enum, Binary) attrCharToW32 :: AttrChar -> AttrCharW32 attrCharToW32 AttrChar{acAttr=Attr{..}, acChar} = AttrCharW32 $ toEnum $ unsafeShiftL (fromEnum fg) 8 + fromEnum bg + unsafeShiftL (Char.ord acChar) 16 attrCharFromW32 :: AttrCharW32 -> AttrChar attrCharFromW32 !w = AttrChar (attrFromW32 w) (charFromW32 w) fgFromW32 :: AttrCharW32 -> Color {-# INLINE fgFromW32 #-} fgFromW32 w = toEnum $ unsafeShiftR (fromEnum $ attrCharW32 w) 8 .&. (2 ^ (8 :: Int) - 1) bgFromW32 :: AttrCharW32 -> Highlight {-# INLINE bgFromW32 #-} bgFromW32 w = toEnum $ fromEnum $ attrCharW32 w .&. (2 ^ (8 :: Int) - 1) charFromW32 :: AttrCharW32 -> Char {-# INLINE charFromW32 #-} charFromW32 w = Char.chr $ unsafeShiftR (fromEnum $ attrCharW32 w) 16 attrFromW32 :: AttrCharW32 -> Attr {-# INLINE attrFromW32 #-} attrFromW32 w = Attr (fgFromW32 w) (bgFromW32 w) spaceAttrW32 :: AttrCharW32 spaceAttrW32 = attrCharToW32 $ AttrChar defAttr ' ' nbspAttrW32 :: AttrCharW32 nbspAttrW32 = attrCharToW32 $ AttrChar defAttr '\x00a0' trimmedLineAttrW32 :: AttrCharW32 trimmedLineAttrW32 = attrChar2ToW32 BrBlack '$' attrChar2ToW32 :: Color -> Char -> AttrCharW32 {-# INLINE attrChar2ToW32 #-} attrChar2ToW32 fg = let fgNum = unsafeShiftL (fromEnum fg) 8 in \acChar -> AttrCharW32 $ toEnum $ fgNum + unsafeShiftL (Char.ord acChar) 16 -- -- These hacks save one allocation (?) (before fits-in-32bits check) compared -- to the above, but they fail in GHC 9.2.0 and possibly don't do anything -- for JS, which is the only real bottleneck, so disabled: -- --import GHC.Prim (int2Word#) -- case unsafeShiftL (fromEnum fg) 8 + unsafeShiftL (Char.ord acChar) 16 of -- I# i -> AttrCharW32 $ W32# (int2Word# i) attrChar1ToW32 :: Char -> AttrCharW32 {-# INLINE attrChar1ToW32 #-} attrChar1ToW32 = let fgNum = unsafeShiftL (fromEnum White) 8 in \acChar -> AttrCharW32 $ toEnum $ fgNum + unsafeShiftL (Char.ord acChar) 16 -- -- case fgNum + unsafeShiftL (Char.ord acChar) 16 of -- I# i -> AttrCharW32 $ W32# (int2Word# i) LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Definition/ContentData.hs0000644000000000000000000002226307346545000024461 0ustar0000000000000000-- | A game requires the engine provided by the library, perhaps customized, -- and game content, defined completely afresh for the particular game. -- The possible kinds of content are fixed in the library and all defined -- within the library source code directory. On the other hand, game content, -- is defined in the directory hosting the particular game definition. -- -- Content of a given kind is just a list of content items. -- After the list is verified and the data preprocessed, it's held -- in the @ContentData@ datatype. module Game.LambdaHack.Definition.ContentData ( ContentData , validateRarity, validFreqs , emptyContentData, makeContentData , okind, omemberGroup, oexistsGroup, oisSingletonGroup, ouniqGroup, opick , ofoldlWithKey', ofoldlGroup', omapVector, oimapVector, olength ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Function import qualified Data.Map.Strict as M import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Vector as V import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal -- | Verified and preprocessed content data of a particular kind. data ContentData c = ContentData { contentVector :: V.Vector c , groupFreq :: M.Map (GroupName c) [(Int, (ContentId c, c))] } maxContentId :: ContentId k maxContentId = toContentId maxBound validateRarity :: Rarity -> [Text] validateRarity rarity = -- @SortOn@ less efficient here, because function cheap. let sortedRarity = sortBy (comparing fst) rarity in [ "rarity not sorted" | sortedRarity /= rarity ] ++ [ "rarity depth thresholds not unique" | map head (groupBy ((==) `on` fst) sortedRarity) /= sortedRarity ] ++ [ "rarity depth not positive" | case sortedRarity of ((lowest, _) : _) -> lowest <= 0 _ -> False ] validFreqs :: Freqs a -> Bool validFreqs freqs = -- Greater or equal to 0 permitted, e.g., to cover embedded template UNKNOWN -- items not yet identified by the client, but triggerable nevertheless. all ((>= 0) . snd) freqs && let groups = sort $ map fst freqs tailOfGroups = if null groups then groups else tail groups in all (uncurry (/=)) $ zip groups tailOfGroups emptyContentData :: ContentData a emptyContentData = ContentData V.empty M.empty makeContentData :: Show c => String -> (c -> Text) -- ^ name of the content itme, used for validation -> (c -> Freqs c) -- ^ frequency in groups, for validation and preprocessing -> (c -> [Text]) -- ^ validate a content item and list all offences -> ([c] -> ContentData c -> [Text]) -- ^ validate the whole defined content of this type -- and list all offence -> [c] -- ^ all content of this type -> [GroupName c] -- ^ singleton group names for this content -> [GroupName c] -- ^ remaining group names for this content -> ContentData c {-# INLINE makeContentData #-} makeContentData contentName getName getFreq validateSingle validateAll content groupNamesSingleton groupNames = -- The @force@ is needed for @GHC.Compact@. let contentVector = V.force $ V.fromList content groupFreq = let tuples = [ (cgroup, (n, (i, k))) | (i, k) <- zip (map toContentId [0..]) content , (cgroup, n) <- getFreq k , n > 0 ] f !m (!cgroup, !nik) = M.insertWith (++) cgroup [nik] m in foldl' f M.empty tuples contentData = ContentData {..} singleOffenders = [ (offences, a) | a <- content , let offences = validateSingle a ++ ["empty name" | T.null (getName a)] , not (null offences) ] allOffences = validateAll content contentData freqsOffenders = filter (not . validFreqs . getFreq) content allGroupNamesEmpty = filter (T.null . fromGroupName) $ groupNamesSingleton ++ groupNames allGroupNamesTooLong = filter ((> 30) . T.length . fromGroupName) $ groupNamesSingleton ++ groupNames allGroupNamesSorted = sort $ groupNamesSingleton ++ groupNames allGroupNamesUnique = nub allGroupNamesSorted allGroupNamesNonUnique = allGroupNamesSorted \\ allGroupNamesUnique missingGroups = filter (not . omemberGroup contentData) (groupNamesSingleton ++ groupNames) groupsMoreThanOne = filter (not . oisSingletonGroup contentData) groupNamesSingleton groupsDeclaredSet = S.fromAscList allGroupNamesUnique groupsNotDeclared = filter (`S.notMember` groupsDeclaredSet) $ M.keys groupFreq in assert (null allGroupNamesEmpty `blame` contentName ++ ": some group names empty" `swith` allGroupNamesEmpty) $ assert (null allGroupNamesTooLong `blame` contentName ++ ": some group names too long" `swith` allGroupNamesTooLong) $ assert (null allGroupNamesNonUnique `blame` contentName ++ ": some group names duplicated" `swith` allGroupNamesNonUnique) $ assert (null missingGroups `blame` contentName ++ ": some group names pertain to no content" `swith` missingGroups) $ assert (null groupsMoreThanOne `blame` contentName ++ ": some group names refer to more than one content, while they shouldn't" `swith` groupsMoreThanOne) $ assert (null groupsNotDeclared `blame` contentName ++ ": some group names are not included in group name lists, neither singleton nor duplicable" `swith` groupsNotDeclared) $ assert (null freqsOffenders `blame` contentName ++ ": some Freqs values not valid" `swith` freqsOffenders) $ assert (null singleOffenders `blame` contentName ++ ": some content items not valid" `swith` singleOffenders) $ assert (null allOffences `blame` contentName ++ ": the content set is not valid" `swith` allOffences) $ assert (V.length contentVector <= contentIdIndex maxContentId `blame` contentName ++ ": the content has too many elements") contentData -- | Content element at given id. okind :: ContentData a -> ContentId a -> a {-# INLINE okind #-} okind ContentData{contentVector} !i = contentVector V.! contentIdIndex i omemberGroup :: ContentData a -> GroupName a -> Bool omemberGroup ContentData{groupFreq} cgroup = cgroup `M.member` groupFreq oexistsGroup :: ContentData a -> GroupName a -> Bool oexistsGroup ContentData{groupFreq} cgroup = case M.lookup cgroup groupFreq of Nothing -> False Just l -> all ((> 0) . fst) l oisSingletonGroup :: ContentData a -> GroupName a -> Bool oisSingletonGroup ContentData{groupFreq} cgroup = case M.lookup cgroup groupFreq of Just [_] -> True _ -> False -- | The id of the unique member of a singleton content group. ouniqGroup :: Show a => ContentData a -> GroupName a -> ContentId a ouniqGroup ContentData{groupFreq} !cgroup = let freq = let assFail = error $ "no unique group" `showFailure` (cgroup, groupFreq) in M.findWithDefault assFail cgroup groupFreq in case freq of [(n, (i, _))] | n > 0 -> i l -> error $ "not unique" `showFailure` (cgroup, l) -- | Pick a random id belonging to a group and satisfying a predicate. opick :: Show a => ContentData a -> GroupName a -> (a -> Bool) -> Rnd (Maybe (ContentId a)) opick ContentData{groupFreq} !cgroup !p = case M.lookup cgroup groupFreq of Just freqRaw -> let freq = toFreq "opick" $ filter (p . snd . snd) freqRaw in if nullFreq freq then return Nothing else Just . fst <$> frequency freq _ -> return Nothing -- | Fold strictly over all content @a@. ofoldlWithKey' :: ContentData a -> (b -> ContentId a -> a -> b) -> b -> b ofoldlWithKey' ContentData{contentVector} f z = V.ifoldl' (\ !a !i !c -> f a (toContentId $ toEnum i) c) z contentVector -- | Fold over the given group only. ofoldlGroup' :: ContentData a -> GroupName a -> (b -> Int -> ContentId a -> a -> b) -> b -> b ofoldlGroup' ContentData{groupFreq} cgroup f z = case M.lookup cgroup groupFreq of Just freq -> foldl' (\ !acc (!p, (!i, !a)) -> f acc p i a) z freq _ -> error $ "no group '" ++ show cgroup ++ "' among content that has groups " ++ show (M.keys groupFreq) `showFailure` () omapVector :: ContentData a -> (a -> b) -> V.Vector b omapVector d f = V.map f $ contentVector d oimapVector :: ContentData a -> (ContentId a -> a -> b) -> V.Vector b oimapVector d f = V.imap (\i a -> f (toContentId $ toEnum i) a) (contentVector d) -- | Size of content @a@. olength :: ContentData a -> Int olength ContentData{contentVector} = V.length contentVector LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Definition/Defs.hs0000644000000000000000000001634307346545000023140 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Basic types for content definitions. module Game.LambdaHack.Definition.Defs ( GroupName, displayGroupName , ContentId, contentIdIndex , ContentSymbol, displayContentSymbol , X, Y , Freqs, renameFreqs , Rarity, linearInterpolation , CStore(..), ppCStore, ppCStoreIn, verbCStore , SLore(..), ItemDialogMode(..), ppSLore, headingSLore , ppItemDialogMode, ppItemDialogModeIn, ppItemDialogModeFrom, loreFromMode , Direction(..) ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.DeepSeq import Data.Binary import GHC.Generics (Generic) import Game.LambdaHack.Definition.DefsInternal -- | X spacial dimension for points and vectors. type X = Int -- | Y xpacial dimension for points and vectors. type Y = Int -- | For each group that the kind belongs to, denoted by a @GroupName@ -- in the first component of a pair, the second component of a pair shows -- how common the kind is within the group. type Freqs c = [(GroupName c, Int)] renameFreqs :: (Text -> Text) -> Freqs c -> Freqs c renameFreqs f = map (first (GroupName . f . fromGroupName)) -- | Rarity on given depths. The first element of the pair is normally -- in (0, 10] interval and, e.g., if there are 20 levels, 0.5 represents -- the first level and 10 the last. Exceptionally, it may be larger than 10, -- meaning appearance in the dungeon is not possible under normal circumstances -- and the value remains constant above the interval bound. type Rarity = [(Double, Int)] -- We assume depths are greater or equal to one and the rarity @dataset@ -- is non-empty, sorted and the first elements of the pairs are positive. -- The convention for adding implicit outer intervals is that -- the value increases linearly, starting from 0 at 0. Similarly, -- if the last interval ends before 10, the value drops linearly, -- in a way that would reach 0 a step after 10, but staying constant -- from 10 onward. If the last interval ends after 10, the value stays constant -- after the interval's upper bound. -- -- Note that rarity [(1, 1)] means constant value 1 only thanks to @ceiling@. -- OTOH, [(1, 10)] is not equivalent to [(10/150, 10)] in a 150-deep dungeon, -- since its value at the first level is drastically lower. This only -- matters if content creators mix the two notations, so care must be taken -- in such cases. Otherwise, for any given level, all kinds scale consistently -- and the simpler notation just paintes the dungeon in larger strokes. linearInterpolation :: Int -> Int -> Rarity -> Int linearInterpolation !levelDepthInt !totalDepthInt !dataset = let levelDepth10 = intToDouble $ levelDepthInt * 10 totalDepth = intToDouble totalDepthInt findInterval :: (Double, Int) -> Rarity -> ((Double, Int), (Double, Int)) findInterval x1y1@(x1Last, y1Last) [] = -- we are past the last interval let stepLevel = 10 / totalDepth -- this is the distance representing one level, the same -- as the distance from 0 to the representation of level 1 yConstant = if x1Last >= 10 then y1Last else ceiling (intToDouble y1Last * stepLevel / (10 + stepLevel - x1Last)) -- this is the value of the interpolation formula at the end -- with y2 == 0, levelDepth10 == totalDepth * 10, -- and x2 == 10 + stepLevel in if levelDepthInt > totalDepthInt -- value stays constant then ((x1Last, yConstant), (x1Last + 1, yConstant)) -- this artificial interval is enough to emulate -- the value staying constant indefinitely else (x1y1, (10 + stepLevel, 0)) findInterval !x1y1 ((!x, !y) : rest) = if levelDepth10 <= x * totalDepth then (x1y1, (x, y)) else findInterval (x, y) rest ((x1, y1), (x2, y2)) = findInterval (0, 0) dataset in y1 + ceiling (intToDouble (y2 - y1) * (levelDepth10 - x1 * totalDepth) / ((x2 - x1) * totalDepth)) -- | Actor's item stores. data CStore = CGround | COrgan | CEqp | CStash deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic) instance Binary CStore instance NFData CStore ppCStore :: CStore -> (Text, Text) ppCStore CGround = ("on", "the ground") ppCStore COrgan = ("in", "body") ppCStore CEqp = ("in", "equipment outfit") ppCStore CStash = ("in", "shared inventory stash") ppCStoreIn :: CStore -> Text ppCStoreIn c = let (tIn, t) = ppCStore c in tIn <+> t verbCStore :: CStore -> Text verbCStore CGround = "remove" verbCStore COrgan = "implant" verbCStore CEqp = "equip" verbCStore CStash = "stash" -- | Item slot and lore categories. data SLore = SItem | SOrgan | STrunk | SCondition | SBlast | SEmbed | SBody -- contains the sum of @SOrgan@, @STrunk@ and @SCondition@ -- but only present in the current pointman's body deriving (Show, Read, Eq, Ord, Enum, Bounded, Generic) instance Binary SLore instance NFData SLore data ItemDialogMode = MStore CStore -- ^ a leader's store | MOwned -- ^ all party's items | MSkills -- ^ not items, but determined by leader's items | MLore SLore -- ^ not party's items, but all known generalized items | MPlaces -- ^ places; not items at all, but definitely a lore | MFactions -- ^ factions in this game, with some data from previous | MModes -- ^ scenarios; not items at all, but definitely a lore deriving (Show, Read, Eq, Ord, Generic) instance NFData ItemDialogMode instance Binary ItemDialogMode ppSLore :: SLore -> Text ppSLore SItem = "item" ppSLore SOrgan = "organ" ppSLore STrunk = "creature" ppSLore SCondition = "condition" ppSLore SBlast = "blast" ppSLore SEmbed = "terrain" ppSLore SBody = "body" headingSLore :: SLore -> Text headingSLore SItem = "miscellaneous item" headingSLore SOrgan = "vital anatomic organ" headingSLore STrunk = "autonomous entity" headingSLore SCondition = "momentary bodily condition" headingSLore SBlast = "explosion blast particle" headingSLore SEmbed = "landmark feature" headingSLore SBody = "body part" ppItemDialogMode :: ItemDialogMode -> (Text, Text) ppItemDialogMode (MStore cstore) = ppCStore cstore ppItemDialogMode MOwned = ("among", "our total team belongings") ppItemDialogMode MSkills = ("among", "skills") ppItemDialogMode (MLore SBody) = ("in", "body") ppItemDialogMode (MLore slore) = ("among", ppSLore slore <+> "lore") ppItemDialogMode MPlaces = ("among", "place lore") ppItemDialogMode MFactions = ("among", "faction lore") ppItemDialogMode MModes = ("among", "adventure lore") ppItemDialogModeIn :: ItemDialogMode -> Text ppItemDialogModeIn c = let (tIn, t) = ppItemDialogMode c in tIn <+> t ppItemDialogModeFrom :: ItemDialogMode -> Text ppItemDialogModeFrom c = let (_tIn, t) = ppItemDialogMode c in "from" <+> t loreFromMode :: ItemDialogMode -> SLore loreFromMode c = case c of MStore COrgan -> SOrgan MStore _ -> SItem MOwned -> SItem MSkills -> undefined -- artificial slots MLore slore -> slore MPlaces -> undefined -- artificial slots MFactions -> undefined -- artificial slots MModes -> undefined -- artificial slots data Direction = Forward | Backward deriving (Show, Read, Eq, Ord, Generic) instance NFData Direction instance Binary Direction LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Definition/DefsInternal.hs0000644000000000000000000000530507346545000024631 0ustar0000000000000000{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Very basic types for content definitions with their internals exposed. module Game.LambdaHack.Definition.DefsInternal ( GroupName(..), displayGroupName , ContentId, toContentId, fromContentId, contentIdIndex , ContentSymbol, toContentSymbol, displayContentSymbol ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.DeepSeq import Data.Binary import Data.Hashable -- If ever needed, we can use a symbol table here, since content -- is never serialized. But we'd need to cover the few cases -- (e.g., @litemFreq@) where @GroupName@ goes into savegame. newtype GroupName c = GroupName {fromGroupName :: Text} deriving (Show, Eq, Ord, Hashable, Binary, NFData) -- | This does not need to be 1-1, so should not be used in place of the -- 'Eq' instance, etc. displayGroupName :: GroupName c -> Text displayGroupName = fromGroupName -- | Content identifiers for the content type @c@. newtype ContentId c = ContentId Word16 deriving (Show, Eq, Ord, Enum, Hashable, Binary) toContentId :: Word16 -> ContentId c {-# INLINE toContentId #-} toContentId = ContentId fromContentId :: ContentId c -> Word16 {-# INLINE fromContentId #-} fromContentId (ContentId k) = k contentIdIndex :: ContentId c -> Int {-# INLINE contentIdIndex #-} contentIdIndex (ContentId k) = fromEnum k -- TODO: temporary, not to break compilation too soon: --{-- type ContentSymbol c = Char toContentSymbol :: Char -> ContentSymbol c toContentSymbol = id displayContentSymbol :: ContentSymbol c -> Char displayContentSymbol = id --} -- TODO: The intended definitions. Error they are going to cause will -- point out all the remaining item symbols hardwired in the engine -- and make any future accidental hardwiring harder. -- TODO2: extend to other content kinds than item kinds. {- -- | An abstract view on the symbol of a content item definition. -- Hiding the constructor prevents hardwiring symbols inside the engine -- by accident (this is still possible via conversion functions, -- if one insists, so the abstraction is leaky, but that's fine). newtype ContentSymbol c = ContentSymbol Char deriving (Show, Eq, Ord, Binary, NFData) -- TODO: Generic and most others are only needed for TriggerItem, so once the latter is removed, these instances can go. -- | This is a 1-1 inclusion. Don't use, if an equal named symbol already -- exists in rules content. toContentSymbol :: Char -> ContentSymbol c {-# INLINE toContentSymbol #-} toContentSymbol = ContentSymbol -- | This does not need to be 1-1, so should not be used in place of the -- 'Eq' instance, etc. displayContentSymbol :: ContentSymbol c -> Char {-# INLINE displayContentSymbol #-} displayContentSymbol (ContentSymbol c) = c --} LambdaHack-0.11.0.0/definition-src/Game/LambdaHack/Definition/Flavour.hs0000644000000000000000000001471607346545000023677 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | The appearance of in-game items, as communicated to the player. module Game.LambdaHack.Definition.Flavour ( -- * The @Flavour@ type Flavour , -- * Constructors zipPlain, zipFancy, zipLiquid, zipGlassPlain, zipGlassFancy, zipStory , dummyFlavour, stdFlavList , -- * Accessors flavourToColor, flavourToName -- * Assorted , colorToPlainName, colorToFancyName, colorToTeamName #ifdef EXPOSE_INTERNAL -- * Internal operations , FancyName, colorToLiquidName, colorToGlassPlainName, colorToGlassFancyName #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import Data.Bits ((.&.)) import GHC.Generics (Generic) import Game.LambdaHack.Definition.Color data FancyName = Plain | Fancy | Liquid | GlassPlain | GlassFancy | Story deriving (Show, Eq, Ord, Enum, Bounded, Generic) -- | The type of item flavours. data Flavour = Flavour { fancyName :: FancyName -- ^ how fancy should the colour description be , baseColor :: Color -- ^ the colour of the flavour } deriving (Show, Eq, Ord, Generic) instance Enum Flavour where fromEnum Flavour{..} = unsafeShiftL (fromEnum fancyName) 8 + fromEnum baseColor toEnum n = Flavour (toEnum $ unsafeShiftR n 8) (toEnum $ n .&. (2 ^ (8 :: Int) - 1)) instance Binary Flavour where put = put . (toIntegralCrash :: Int -> Word16) . fromEnum get = fmap (toEnum . (into :: Word16 -> Int)) get -- @Int doesn't suffice dummyFlavour :: Flavour dummyFlavour = Flavour Story Black stdFlavList :: [Flavour] stdFlavList = [Flavour fn bc | fn <- [minBound..maxBound], bc <- stdCol] -- | Turn a colour set into a flavour set. zipPlain, zipFancy, zipLiquid, zipGlassPlain, zipGlassFancy, zipStory :: [Color] -> [Flavour] zipPlain = map (Flavour Plain) zipFancy = map (Flavour Fancy) zipLiquid = map (Flavour Liquid) zipGlassPlain = map (Flavour GlassPlain) zipGlassFancy = map (Flavour GlassFancy) zipStory = map (Flavour Story) -- | Get the underlying base colour of a flavour. flavourToColor :: Flavour -> Color flavourToColor Flavour{baseColor} = baseColor -- | Construct the full name of a flavour. flavourToName :: Flavour -> Text flavourToName Flavour{fancyName=Plain, ..} = colorToPlainName baseColor flavourToName Flavour{fancyName=Fancy, ..} = colorToFancyName baseColor flavourToName Flavour{fancyName=Liquid, ..} = colorToLiquidName baseColor flavourToName Flavour{fancyName=GlassPlain, ..} = colorToGlassPlainName baseColor flavourToName Flavour{fancyName=GlassFancy, ..} = colorToGlassFancyName baseColor flavourToName Flavour{fancyName=Story, ..} = colorToStoryName baseColor -- | Human-readable names for item colors. The plain set. colorToPlainName :: Color -> Text colorToPlainName Black = "black" colorToPlainName Red = "red" colorToPlainName Green = "green" colorToPlainName Brown = "brown" colorToPlainName Blue = "blue" colorToPlainName Magenta = "purple" colorToPlainName Cyan = "cyan" colorToPlainName White = "ivory" colorToPlainName AltWhite = error "colorToPlainName: illegal color" colorToPlainName BrBlack = "gray" colorToPlainName BrRed = "coral" colorToPlainName BrGreen = "lime" colorToPlainName BrYellow = "yellow" colorToPlainName BrBlue = "azure" colorToPlainName BrMagenta = "pink" colorToPlainName BrCyan = "aquamarine" colorToPlainName BrWhite = "white" -- | Human-readable names for item colors. The fancy set. colorToFancyName :: Color -> Text colorToFancyName Black = "smoky-black" colorToFancyName Red = "apple-red" colorToFancyName Green = "forest-green" colorToFancyName Brown = "mahogany" colorToFancyName Blue = "royal-blue" colorToFancyName Magenta = "indigo" colorToFancyName Cyan = "teal" colorToFancyName White = "silver-gray" colorToFancyName AltWhite = error "colorToFancyName: illegal color" colorToFancyName BrBlack = "charcoal" colorToFancyName BrRed = "salmon" colorToFancyName BrGreen = "emerald" colorToFancyName BrYellow = "amber" colorToFancyName BrBlue = "sky-blue" colorToFancyName BrMagenta = "magenta" colorToFancyName BrCyan = "turquoise" colorToFancyName BrWhite = "ghost-white" -- | Human-readable names for item colors. The liquid set. colorToLiquidName :: Color -> Text colorToLiquidName Black = "tarry" colorToLiquidName Red = "bloody" colorToLiquidName Green = "moldy" colorToLiquidName Brown = "muddy" colorToLiquidName Blue = "oily" colorToLiquidName Magenta = "swirling" colorToLiquidName Cyan = "bubbling" colorToLiquidName White = "cloudy" colorToLiquidName AltWhite = error "colorToLiquidName: illegal color" colorToLiquidName BrBlack = "pitchy" colorToLiquidName BrRed = "red-speckled" colorToLiquidName BrGreen = "sappy" colorToLiquidName BrYellow = "golden" colorToLiquidName BrBlue = "blue-speckled" colorToLiquidName BrMagenta = "hazy" colorToLiquidName BrCyan = "misty" colorToLiquidName BrWhite = "shining" -- | Human-readable names for item colors. The plain glass set. colorToGlassPlainName :: Color -> Text colorToGlassPlainName color = colorToPlainName color <+> "glass" -- | Human-readable names for item colors. The fancy glass set. colorToGlassFancyName :: Color -> Text colorToGlassFancyName color = colorToFancyName color <+> "crystal" -- | Human-readable names for story item colors. colorToStoryName :: Color -> Text colorToStoryName Black = "unfathomable" colorToStoryName Red = "depressing" colorToStoryName Green = "confidence-boosting" colorToStoryName Brown = "mundane" colorToStoryName Blue = "fleeting" colorToStoryName Magenta = "complex" colorToStoryName Cyan = "wierd" colorToStoryName White = "obvious" colorToStoryName AltWhite = error "colorToStoryName: illegal color" colorToStoryName BrBlack = "inconclusive" colorToStoryName BrRed = "troubling" colorToStoryName BrGreen = "cherished" colorToStoryName BrYellow = "glaring" colorToStoryName BrBlue = "profound" colorToStoryName BrMagenta = "torturous" colorToStoryName BrCyan = "peculiar" colorToStoryName BrWhite = "explosive" -- | Simple names for team colors (bright colours preferred). colorToTeamName :: Color -> Text colorToTeamName BrBlack = "black" colorToTeamName BrRed = "red" colorToTeamName BrGreen = "green" colorToTeamName BrYellow = "yellow" colorToTeamName BrBlue = "blue" colorToTeamName BrMagenta = "pink" colorToTeamName BrCyan = "cyan" colorToTeamName BrWhite = "white" colorToTeamName c = colorToFancyName c LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/0000755000000000000000000000000007346545000016741 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Atomic.hs0000644000000000000000000000164307346545000020515 0ustar0000000000000000-- | Atomic game state transformations, their representation and semantics. -- -- See -- . module Game.LambdaHack.Atomic ( -- * Re-exported from "Game.LambdaHack.Atomic.CmdAtomic" CmdAtomic(..), UpdAtomic(..), HearMsg(..), SfxAtomic(..), SfxMsg(..) -- * Re-exported from "Game.LambdaHack.Atomic.HandleAtomicWrite" , handleUpdAtomic -- * Re-exported from "Game.LambdaHack.Atomic.PosAtomicRead" , PosAtomic(..), posUpdAtomic, posSfxAtomic, iidUpdAtomic, iidSfxAtomic , breakUpdAtomic, lidOfPos, seenAtomicCli, seenAtomicSer -- * Re-exported from "Game.LambdaHack.Atomic.MonadStateWrite" , MonadStateWrite(..), AtomicFail(..) ) where import Prelude () import Game.LambdaHack.Atomic.CmdAtomic import Game.LambdaHack.Atomic.HandleAtomicWrite import Game.LambdaHack.Atomic.MonadStateWrite import Game.LambdaHack.Atomic.PosAtomicRead LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Atomic/0000755000000000000000000000000007346545000020155 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Atomic/CmdAtomic.hs0000644000000000000000000003145607346545000022362 0ustar0000000000000000-- | A set of atomic commands shared by client and server. -- These are the largest building blocks that have no components -- that can be observed in isolation. -- -- We try to make atomic commands respect the laws of energy and mass -- conservation, unless they really can't, e.g., monster spawning. -- For example item removal from equipment, in isolation, is not an atomic -- command, but item dropped from equipment to the ground is. This makes -- it easier to undo the commands. In principle, the commands are the only -- way to affect the basic game state ('State'). -- -- See -- . module Game.LambdaHack.Atomic.CmdAtomic ( CmdAtomic(..), UpdAtomic(..), HearMsg(..), SfxAtomic(..), SfxMsg(..) , undoUpdAtomic, undoSfxAtomic, undoCmdAtomic ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumSet as ES import Data.Int (Int64) import qualified System.Random.SplitMix32 as SM -- Dependence on ClientOptions is an anomaly. Instead, probably the raw -- remaining commandline should be passed and parsed by the client to extract -- client and ui options from and singnal an error if anything was left. import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.PlaceKind as PK import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | Abstract syntax of atomic commands, that is, atomic game state -- transformations. data CmdAtomic = UpdAtomic UpdAtomic -- ^ atomic updates | SfxAtomic SfxAtomic -- ^ atomic special effects deriving Show -- TODO: needed in the future, if ever, to save undo information: -- -- deriving (Show, Eq, Generic) -- -- instance Binary CmdAtomic -- | Abstract syntax of atomic updates, that is, atomic commands -- that really change the 'State'. Most of them are an encoding of a game -- state diff, though they also carry some intentional hints -- that help clients determine whether and how to communicate it to players. data UpdAtomic = -- Create/destroy actors and items. UpdRegisterItems [(ItemId, Item)] | UpdCreateActor ActorId Actor [(ItemId, Item)] | UpdDestroyActor ActorId Actor [(ItemId, Item)] | UpdCreateItem Bool ItemId Item ItemQuant Container | UpdDestroyItem Bool ItemId Item ItemQuant Container | UpdSpotActor ActorId Actor | UpdLoseActor ActorId Actor | UpdSpotItem Bool ItemId ItemQuant Container | UpdLoseItem Bool ItemId ItemQuant Container | UpdSpotItemBag Bool Container ItemBag | UpdLoseItemBag Bool Container ItemBag -- Move actors and items. | UpdMoveActor ActorId Point Point | UpdWaitActor ActorId Watchfulness Watchfulness | UpdDisplaceActor ActorId ActorId | UpdMoveItem ItemId Int ActorId CStore CStore -- Change actor attributes. | UpdRefillHP ActorId Int64 | UpdRefillCalm ActorId Int64 | UpdTrajectory ActorId (Maybe ([Vector], Speed)) (Maybe ([Vector], Speed)) -- Change faction attributes. | UpdQuitFaction FactionId (Maybe Status) (Maybe Status) (Maybe (FactionAnalytics, GenerationAnalytics)) | UpdSpotStashFaction Bool FactionId LevelId Point | UpdLoseStashFaction Bool FactionId LevelId Point | UpdLeadFaction FactionId (Maybe ActorId) (Maybe ActorId) | UpdDiplFaction FactionId FactionId Diplomacy Diplomacy | UpdDoctrineFaction FactionId Ability.Doctrine Ability.Doctrine | UpdAutoFaction FactionId Bool | UpdRecordKill ActorId (ContentId ItemKind) Int -- Alter map. | UpdAlterTile LevelId Point (ContentId TileKind) (ContentId TileKind) | UpdAlterExplorable LevelId Int | UpdAlterGold Int | UpdSearchTile ActorId Point (ContentId TileKind) | UpdHideTile ActorId Point (ContentId TileKind) | UpdSpotTile LevelId [(Point, ContentId TileKind)] | UpdLoseTile LevelId [(Point, ContentId TileKind)] | UpdSpotEntry LevelId [(Point, PK.PlaceEntry)] | UpdLoseEntry LevelId [(Point, PK.PlaceEntry)] | UpdAlterSmell LevelId Point Time Time | UpdSpotSmell LevelId [(Point, Time)] | UpdLoseSmell LevelId [(Point, Time)] -- Assorted. | UpdTimeItem ItemId Container ItemTimers ItemTimers | UpdAgeGame (ES.EnumSet LevelId) | UpdUnAgeGame (ES.EnumSet LevelId) | UpdDiscover Container ItemId (ContentId ItemKind) IA.AspectRecord -- Here and below @Container@ is only used for presentation -- and when @CStash@ is not visible, but the item is, it won't -- break anything, because item identification is not registered globally. | UpdCover Container ItemId (ContentId ItemKind) IA.AspectRecord | UpdDiscoverKind Container ItemKindIx (ContentId ItemKind) | UpdCoverKind Container ItemKindIx (ContentId ItemKind) | UpdDiscoverAspect Container ItemId IA.AspectRecord | UpdCoverAspect Container ItemId IA.AspectRecord | UpdDiscoverServer ItemId IA.AspectRecord | UpdCoverServer ItemId IA.AspectRecord | UpdPerception LevelId Perception Perception | UpdRestart FactionId PerLid State Challenge ClientOptions SM.SMGen | UpdRestartServer State | UpdResume FactionId PerLid | UpdResumeServer State | UpdKillExit FactionId | UpdWriteSave | UpdHearFid FactionId (Maybe Int) HearMsg -- in @UpdAtomic@ to let AI analyze and count | UpdMuteMessages FactionId Bool deriving Show -- | Symbolic representation of text messages about heard noises, -- sent by server to clients and shown to players and used by AI. data HearMsg = HearUpd UpdAtomic | HearStrike (ContentId ItemKind) | HearSummon Bool (GroupName ItemKind) Dice.Dice | HearCollideTile | HearTaunt Text deriving Show -- | Abstract syntax of atomic special effects, that is, atomic commands -- that only display special effects and don't change 'State' nor client state. data SfxAtomic = SfxStrike ActorId ActorId ItemId | SfxRecoil ActorId ActorId ItemId | SfxSteal ActorId ActorId ItemId | SfxRelease ActorId ActorId ItemId | SfxProject ActorId ItemId | SfxReceive ActorId ItemId | SfxApply ActorId ItemId | SfxCheck ActorId ItemId | SfxTrigger ActorId LevelId Point (ContentId TileKind) | SfxShun ActorId LevelId Point (ContentId TileKind) | SfxEffect FactionId ActorId ItemId IK.Effect Int64 | SfxItemApplied Bool ItemId Container | SfxMsgFid FactionId SfxMsg | SfxRestart | SfxCollideTile ActorId Point | SfxTaunt Bool ActorId deriving Show -- | Symbolic representation of text messages sent by server to clients -- and shown to players. data SfxMsg = SfxUnexpected ReqFailure | SfxExpected Text ReqFailure | SfxExpectedEmbed ItemId LevelId ReqFailure | SfxFizzles ItemId Container | SfxNothingHappens ItemId Container | SfxNoItemsForTile [[(Int, GroupName ItemKind)]] | SfxVoidDetection IK.DetectKind | SfxUnimpressed ActorId | SfxSummonLackCalm ActorId | SfxSummonTooManyOwn ActorId | SfxSummonTooManyAll ActorId | SfxSummonFailure ActorId | SfxLevelNoMore | SfxLevelPushed | SfxBracedImmune ActorId | SfxEscapeImpossible | SfxStasisProtects | SfxWaterParalysisResisted | SfxTransImpossible | SfxIdentifyNothing | SfxPurposeNothing | SfxPurposeTooFew Int Int | SfxPurposeUnique | SfxPurposeNotCommon | SfxRerollNothing | SfxRerollNotRandom | SfxDupNothing | SfxDupUnique | SfxDupValuable | SfxColdFish | SfxReadyGoods | SfxTimerExtended ActorId ItemId CStore (Delta Time) -- This @CStore@ is only printed, so even @CStash@ is safe. | SfxCollideActor ActorId ActorId | SfxItemYield ItemId Int LevelId deriving Show undoUpdAtomic :: UpdAtomic -> Maybe UpdAtomic undoUpdAtomic cmd = case cmd of UpdRegisterItems{} -> Nothing -- harmless and never forgotten UpdCreateActor aid body ais -> Just $ UpdDestroyActor aid body ais UpdDestroyActor aid body ais -> Just $ UpdCreateActor aid body ais UpdCreateItem verbose iid item k c -> Just $ UpdDestroyItem verbose iid item k c UpdDestroyItem verbose iid item k c -> Just $ UpdCreateItem verbose iid item k c UpdSpotActor aid body -> Just $ UpdLoseActor aid body UpdLoseActor aid body -> Just $ UpdSpotActor aid body UpdSpotItem verbose iid k c -> Just $ UpdLoseItem verbose iid k c UpdLoseItem verbose iid k c -> Just $ UpdSpotItem verbose iid k c UpdSpotItemBag verbose c bag -> Just $ UpdLoseItemBag verbose c bag UpdLoseItemBag verbose c bag -> Just $ UpdSpotItemBag verbose c bag UpdMoveActor aid fromP toP -> Just $ UpdMoveActor aid toP fromP UpdWaitActor aid fromWS toWS -> Just $ UpdWaitActor aid toWS fromWS UpdDisplaceActor source target -> Just $ UpdDisplaceActor target source UpdMoveItem iid k aid store1 store2 -> Just $ UpdMoveItem iid k aid store2 store1 UpdRefillHP aid n -> Just $ UpdRefillHP aid (-n) UpdRefillCalm aid n -> Just $ UpdRefillCalm aid (-n) UpdTrajectory aid fromT toT -> Just $ UpdTrajectory aid toT fromT UpdQuitFaction fid fromSt toSt manalytics -> Just $ UpdQuitFaction fid toSt fromSt manalytics UpdSpotStashFaction verbose fid lid pos -> Just $ UpdLoseStashFaction verbose fid lid pos UpdLoseStashFaction verbose fid lid pos -> Just $ UpdSpotStashFaction verbose fid lid pos UpdLeadFaction fid source target -> Just $ UpdLeadFaction fid target source UpdDiplFaction fid1 fid2 fromDipl toDipl -> Just $ UpdDiplFaction fid1 fid2 toDipl fromDipl UpdDoctrineFaction fid toT fromT -> Just $ UpdDoctrineFaction fid fromT toT UpdAutoFaction fid st -> Just $ UpdAutoFaction fid (not st) UpdRecordKill aid ikind k -> Just $ UpdRecordKill aid ikind (-k) UpdAlterTile lid p fromTile toTile -> Just $ UpdAlterTile lid p toTile fromTile UpdAlterExplorable lid delta -> Just $ UpdAlterExplorable lid (-delta) UpdAlterGold delta -> Just $ UpdAlterGold (-delta) UpdSearchTile aid p toTile -> Just $ UpdHideTile aid p toTile UpdHideTile aid p toTile -> Just $ UpdSearchTile aid p toTile UpdSpotTile lid ts -> Just $ UpdLoseTile lid ts UpdLoseTile lid ts -> Just $ UpdSpotTile lid ts UpdSpotEntry lid ts -> Just $ UpdLoseEntry lid ts UpdLoseEntry lid ts -> Just $ UpdSpotEntry lid ts UpdAlterSmell lid p fromSm toSm -> Just $ UpdAlterSmell lid p toSm fromSm UpdSpotSmell lid sms -> Just $ UpdLoseSmell lid sms UpdLoseSmell lid sms -> Just $ UpdSpotSmell lid sms UpdTimeItem iid c fromIt toIt -> Just $ UpdTimeItem iid c toIt fromIt UpdAgeGame lids -> Just $ UpdUnAgeGame lids UpdUnAgeGame lids -> Just $ UpdAgeGame lids UpdDiscover c iid ik arItem -> Just $ UpdCover c iid ik arItem UpdCover c iid ik arItem -> Just $ UpdDiscover c iid ik arItem UpdDiscoverKind c ix ik -> Just $ UpdCoverKind c ix ik UpdCoverKind c ix ik -> Just $ UpdDiscoverKind c ix ik UpdDiscoverAspect c iid arItem -> Just $ UpdCoverAspect c iid arItem UpdCoverAspect c iid arItem -> Just $ UpdDiscoverAspect c iid arItem UpdDiscoverServer iid arItem -> Just $ UpdCoverServer iid arItem UpdCoverServer iid arItem -> Just $ UpdDiscoverServer iid arItem UpdPerception lid outPer inPer -> Just $ UpdPerception lid inPer outPer UpdRestart{} -> Just cmd -- here history ends; change direction UpdRestartServer{} -> Just cmd -- here history ends; change direction UpdResume{} -> Nothing UpdResumeServer{} -> Nothing UpdKillExit{} -> Nothing UpdWriteSave -> Nothing UpdHearFid{} -> Nothing UpdMuteMessages fid b -> Just $ UpdMuteMessages fid $ not b undoSfxAtomic :: SfxAtomic -> SfxAtomic undoSfxAtomic cmd = case cmd of SfxStrike source target iid -> SfxRecoil source target iid SfxRecoil source target iid -> SfxStrike source target iid SfxSteal source target iid -> SfxRelease source target iid SfxRelease source target iid -> SfxSteal source target iid SfxProject aid iid -> SfxReceive aid iid SfxReceive aid iid -> SfxProject aid iid SfxApply aid iid -> SfxCheck aid iid SfxCheck aid iid -> SfxApply aid iid SfxTrigger aid lid p tile -> SfxShun aid lid p tile SfxShun aid lid p tile -> SfxTrigger aid lid p tile SfxEffect{} -> cmd -- not ideal? SfxItemApplied{} -> cmd SfxMsgFid{} -> cmd SfxRestart -> cmd SfxCollideTile{} -> cmd SfxTaunt{} -> cmd undoCmdAtomic :: CmdAtomic -> Maybe CmdAtomic undoCmdAtomic (UpdAtomic cmd) = UpdAtomic <$> undoUpdAtomic cmd undoCmdAtomic (SfxAtomic sfx) = Just $ SfxAtomic $ undoSfxAtomic sfx LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Atomic/HandleAtomicWrite.hs0000644000000000000000000010156407346545000024063 0ustar0000000000000000{-# LANGUAGE FlexibleContexts #-} -- | Semantics of atomic commands shared by client and server. -- -- See -- . module Game.LambdaHack.Atomic.HandleAtomicWrite ( handleUpdAtomic #ifdef EXPOSE_INTERNAL -- * Internal operations , updRegisterItems, updCreateActor, updDestroyActor , updCreateItem, updDestroyItem, updSpotItemBag, updLoseItemBag , updMoveActor, updWaitActor, updDisplaceActor, updMoveItem , updRefillHP, updRefillCalm , updTrajectory, updQuitFaction, updSpotStashFaction, updLoseStashFaction , updLeadFaction, updDiplFaction, updDoctrineFaction, updAutoFaction , updRecordKill, updAlterTile, updAlterExplorable, updSearchTile , updSpotTile, updLoseTile, updAlterSmell, updSpotSmell, updLoseSmell , updTimeItem, updAgeGame, updUnAgeGame, ageLevel, updDiscover, updCover , updDiscoverKind, discoverKind, updCoverKind , updDiscoverAspect, discoverAspect, updCoverAspect , updDiscoverServer, updCoverServer , updRestart, updRestartServer, updResumeServer #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Int (Int64) import Game.LambdaHack.Atomic.CmdAtomic import Game.LambdaHack.Atomic.MonadStateWrite import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.PlaceKind as PK import Game.LambdaHack.Content.TileKind (TileKind, unknownId) import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | The game-state semantics of atomic game commands. -- There is no corresponding definition for special effects (@SfxAtomic@), -- because they don't modify 'State'. -- -- For each of the commands, we are guaranteed that the client, -- the command is addressed to, perceives all the positions the command -- affects (as computed by 'Game.LambdaHack.Atomic.PosAtomicRead.posUpdAtomic'). -- In the code for each semantic function we additonally verify -- the client is aware of any relevant items and/or actors and we throw -- the @AtomicFail@ exception if it's not. -- The server keeps copies of all clients' states and, before sending a command -- to a client, applies it to the client's state copy. -- If @AtomicFail@ is signalled, the command is ignored for that client. -- This enables simpler server code that addresses commands to all clients -- that can see it, even though not all are able to process it. handleUpdAtomic :: MonadStateWrite m => UpdAtomic -> m () handleUpdAtomic cmd = case cmd of UpdRegisterItems ais -> updRegisterItems ais UpdCreateActor aid body ais -> updCreateActor aid body ais UpdDestroyActor aid body ais -> updDestroyActor aid body ais UpdCreateItem _ iid item kit c -> updCreateItem iid item kit c UpdDestroyItem _ iid item kit c -> updDestroyItem iid item kit c UpdSpotActor aid body -> updSpotActor aid body UpdLoseActor aid body -> updLoseActor aid body UpdSpotItem _ iid kit c -> updSpotItem iid kit c UpdLoseItem _ iid kit c -> updLoseItem iid kit c UpdSpotItemBag _ c bag -> updSpotItemBag c bag UpdLoseItemBag _ c bag -> updLoseItemBag c bag UpdMoveActor aid fromP toP -> updMoveActor aid fromP toP UpdWaitActor aid fromWS toWS -> updWaitActor aid fromWS toWS UpdDisplaceActor source target -> updDisplaceActor source target UpdMoveItem iid k aid c1 c2 -> updMoveItem iid k aid c1 c2 UpdRefillHP aid n -> updRefillHP aid n UpdRefillCalm aid n -> updRefillCalm aid n UpdTrajectory aid fromT toT -> updTrajectory aid fromT toT UpdQuitFaction fid fromSt toSt _ -> updQuitFaction fid fromSt toSt UpdSpotStashFaction _ fid lid pos -> updSpotStashFaction fid lid pos UpdLoseStashFaction _ fid lid pos -> updLoseStashFaction fid lid pos UpdLeadFaction fid source target -> updLeadFaction fid source target UpdDiplFaction fid1 fid2 fromDipl toDipl -> updDiplFaction fid1 fid2 fromDipl toDipl UpdDoctrineFaction fid toT fromT -> updDoctrineFaction fid toT fromT UpdAutoFaction fid st -> updAutoFaction fid st UpdRecordKill aid ikind k -> updRecordKill aid ikind k UpdAlterTile lid p fromTile toTile -> updAlterTile lid p fromTile toTile UpdAlterExplorable lid delta -> updAlterExplorable lid delta UpdAlterGold delta -> updAlterGold delta UpdSearchTile aid p toTile -> updSearchTile aid p toTile UpdHideTile{} -> undefined UpdSpotTile lid ts -> updSpotTile lid ts UpdLoseTile lid ts -> updLoseTile lid ts UpdSpotEntry lid ts -> updSpotEntry lid ts UpdLoseEntry lid ts -> updLoseEntry lid ts UpdAlterSmell lid p fromSm toSm -> updAlterSmell lid p fromSm toSm UpdSpotSmell lid sms -> updSpotSmell lid sms UpdLoseSmell lid sms -> updLoseSmell lid sms UpdTimeItem iid c fromIt toIt -> updTimeItem iid c fromIt toIt UpdAgeGame lids -> updAgeGame lids UpdUnAgeGame lids -> updUnAgeGame lids UpdDiscover c iid ik arItem -> updDiscover c iid ik arItem UpdCover c iid ik arItem -> updCover c iid ik arItem UpdDiscoverKind c ix ik -> updDiscoverKind c ix ik UpdCoverKind c ix ik -> updCoverKind c ix ik UpdDiscoverAspect c iid arItem -> updDiscoverAspect c iid arItem UpdCoverAspect c iid arItem -> updCoverAspect c iid arItem UpdDiscoverServer iid arItem -> updDiscoverServer iid arItem UpdCoverServer iid arItem -> updCoverServer iid arItem UpdPerception _ outPer inPer -> assert (not (nullPer outPer && nullPer inPer)) (return ()) UpdRestart _ _ s _ _ _ -> updRestart s UpdRestartServer s -> updRestartServer s UpdResume{} -> return () UpdResumeServer s -> updResumeServer s UpdKillExit{} -> return () UpdWriteSave -> return () UpdHearFid{} -> return () UpdMuteMessages{} -> return () -- Actor's items may or may not be already present in @sitemD@, -- regardless if they are already present otherwise in the dungeon. -- We re-add them all to save time determining which really need it. -- If collision occurs, pick the item found on easier level. updRegisterItems :: MonadStateWrite m => [(ItemId, Item)] -> m () updRegisterItems ais = do let h item1 item2 = assert (itemsMatch item1 item2 `blame` "inconsistent added items" `swith` (item1, item2, ais)) item2 -- keep the first found level forM_ ais $ \(iid, item) -> do let f = case jkind item of IdentityObvious _ -> id IdentityCovered ix _ -> updateItemIxMap $ EM.insertWith ES.union ix (ES.singleton iid) modifyState $ f . updateItemD (EM.insertWith h iid item) -- Note: after this command, usually a new leader -- for the party should be elected (in case this actor is the only one alive). updCreateActor :: MonadStateWrite m => ActorId -> Actor -> [(ItemId, Item)] -> m () updCreateActor aid body ais = do updRegisterItems ais updSpotActor aid body -- If a leader dies, a new leader should be elected on the server -- before this command is executed (not checked). updDestroyActor :: MonadStateWrite m => ActorId -> Actor -> [(ItemId, Item)] -> m () updDestroyActor aid body ais = do -- Assert that actor's items belong to @sitemD@. Do not remove those -- that do not appear anywhere else, for simplicity and speed. itemD <- getsState sitemD let match (iid, item) = itemsMatch (itemD EM.! iid) item let !_A = assert (allB match ais `blame` "destroyed actor items not found" `swith` (aid, body, ais, itemD)) () updLoseActor aid body -- Create a few copies of an item that is already registered for the dungeon -- (in @sitemRev@ field of @StateServer@). -- -- Number of copies may be zero, when the item is only created as a sample -- to let the player know what can potentially be genereated in the dungeon. updCreateItem :: MonadStateWrite m => ItemId -> Item -> ItemQuant -> Container -> m () updCreateItem iid item kit c = do updRegisterItems [(iid, item)] updSpotItem iid kit c -- Destroy some copies (possibly not all) of an item. updDestroyItem :: MonadStateWrite m => ItemId -> Item -> ItemQuant -> Container -> m () updDestroyItem iid item kit@(k, _) c = assert (k > 0) $ do -- Do not remove the item from @sitemD@ nor from @sitemRev@ -- nor from @DiscoveryAspect@, @ItemIxMap@, etc. -- It's incredibly costly and not particularly noticeable for the player. -- Moreover, copies of the item may reappear in the future -- and then we save computation and the player remembers past discovery. -- However, assert the item is registered in @sitemD@. itemD <- getsState sitemD let !_A = assert ((case iid `EM.lookup` itemD of Nothing -> False Just item0 -> itemsMatch item0 item) `blame` "item already removed" `swith` (iid, item, itemD)) () updLoseItem iid kit c updSpotActor :: MonadStateWrite m => ActorId -> Actor -> m () updSpotActor aid body = do -- The exception is possible, e.g., when we teleport and so see our actor -- at the new location, but also the location is part of new perception, -- so @UpdSpotActor@ is sent. let f Nothing = Just body f (Just b) = assert (body == b `blame` (aid, body, b)) $ atomicFail $ "actor already added" `showFailure` (aid, body, b) modifyState $ updateActorD $ EM.alter f aid let g Nothing = Just [aid] g (Just l) = #ifdef WITH_EXPENSIVE_ASSERTIONS -- Not so much expensive, as doubly impossible. assert (aid `notElem` l `blame` "actor already added" `swith` (aid, body, l)) #endif (Just $ aid : l) let h Nothing = Just aid h (Just aid2) = error $ "an actor already present there" `showFailure` (aid, body, aid2) updateLevel (blid body) $ if bproj body then updateProjMap (EM.alter g (bpos body)) else updateBigMap (EM.alter h (bpos body)) actorMaxSk <- getsState $ maxSkillsFromActor body modifyState $ updateActorMaxSkills $ EM.insert aid actorMaxSk updLoseActor :: MonadStateWrite m => ActorId -> Actor -> m () updLoseActor aid body = do -- Remove actor from @sactorD@. let f Nothing = error $ "actor already removed" `showFailure` (aid, body) f (Just b) = assert (b == body `blame` "inconsistent destroyed actor body" `swith` (aid, body, b)) Nothing modifyState $ updateActorD $ EM.alter f aid let g Nothing = error $ "actor already removed" `showFailure` (aid, body) g (Just l) = #ifdef WITH_EXPENSIVE_ASSERTIONS -- Not so much expensive, as doubly impossible. assert (aid `elem` l `blame` "actor already removed" `swith` (aid, body, l)) #endif (let l2 = delete aid l in if null l2 then Nothing else Just l2) let h Nothing = error $ "actor already removed" `showFailure` (aid, body) h (Just _aid2) = #ifdef WITH_EXPENSIVE_ASSERTIONS -- Not so much expensive, as doubly impossible. assert (aid == _aid2 `blame` "actor already removed" `swith` (aid, body, _aid2)) #endif Nothing updateLevel (blid body) $ if bproj body then updateProjMap (EM.alter g (bpos body)) else updateBigMap (EM.alter h (bpos body)) modifyState $ updateActorMaxSkills $ EM.delete aid updSpotItem :: MonadStateWrite m => ItemId -> ItemQuant -> Container -> m () updSpotItem iid kit@(k, _) c = do item <- getsState $ getItemBody iid when (k > 0) $ do insertItemContainer iid kit c case c of CActor aid store -> when (store `elem` [CEqp, COrgan]) $ addItemToActorMaxSkills iid item k aid _ -> return () updLoseItem :: MonadStateWrite m => ItemId -> ItemQuant -> Container -> m () updLoseItem iid kit@(k, _) c = assert (k > 0) $ do item <- getsState $ getItemBody iid deleteItemContainer iid kit c case c of CActor aid store -> when (store `elem` [CEqp, COrgan]) $ addItemToActorMaxSkills iid item (-k) aid _ -> return () updSpotItemBag :: MonadStateWrite m => Container -> ItemBag -> m () updSpotItemBag c bag = -- The case of empty bag is for a hack to help identifying sample items. unless (EM.null bag) $ do insertBagContainer bag c case c of CActor aid store -> when (store `elem` [CEqp, COrgan]) $ do itemD <- getsState sitemD let ais = map (\iid -> (iid, itemD EM.! iid)) $ EM.keys bag forM_ ais $ \(iid, item) -> addItemToActorMaxSkills iid item (fst $ bag EM.! iid) aid _ -> return () updLoseItemBag :: MonadStateWrite m => Container -> ItemBag -> m () updLoseItemBag c bag = assert (EM.size bag > 0) $ do deleteBagContainer bag c -- Do not remove the items from @sitemD@ nor from @sitemRev@, -- It's incredibly costly and not noticeable for the player. -- However, assert the items are registered in @sitemD@. case c of CActor aid store -> when (store `elem` [CEqp, COrgan]) $ do itemD <- getsState sitemD let ais = map (\iid -> (iid, itemD EM.! iid)) $ EM.keys bag forM_ ais $ \(iid, item) -> addItemToActorMaxSkills iid item (- (fst $ bag EM.! iid)) aid _ -> return () updMoveActor :: MonadStateWrite m => ActorId -> Point -> Point -> m () updMoveActor aid fromP toP = assert (fromP /= toP) $ do body <- getsState $ getActorBody aid let !_A = assert (fromP == bpos body `blame` "unexpected moved actor position" `swith` (aid, fromP, toP, bpos body, body)) () newBody = body {bpos = toP, boldpos = Just fromP} updateActor aid $ const newBody moveActorMap aid body newBody updWaitActor :: MonadStateWrite m => ActorId -> Watchfulness -> Watchfulness -> m () updWaitActor aid fromWS toWS = assert (fromWS /= toWS) $ do body <- getsState $ getActorBody aid let !_A = assert (fromWS == bwatch body `blame` "unexpected actor wait state" `swith` (aid, fromWS, bwatch body, body)) () updateActor aid $ \b -> b {bwatch = toWS} updDisplaceActor :: MonadStateWrite m => ActorId -> ActorId -> m () updDisplaceActor source target = assert (source /= target) $ do sbody <- getsState $ getActorBody source tbody <- getsState $ getActorBody target let spos = bpos sbody tpos = bpos tbody snewBody = sbody {bpos = tpos, boldpos = Just spos} tnewBody = tbody {bpos = spos, boldpos = Just tpos} updateActor source $ const snewBody updateActor target $ const tnewBody swapActorMap source sbody target tbody updMoveItem :: MonadStateWrite m => ItemId -> Int -> ActorId -> CStore -> CStore -> m () updMoveItem iid k aid s1 s2 = assert (k > 0 && s1 /= s2) $ do b <- getsState $ getActorBody aid bag <- getsState $ getBodyStoreBag b s1 case iid `EM.lookup` bag of Nothing -> error $ "" `showFailure` (iid, k, aid, s1, s2) Just (_, it) -> do deleteItemActor iid (k, take k it) aid s1 insertItemActor iid (k, take k it) aid s2 case s1 of CEqp -> case s2 of COrgan -> return () _ -> do itemBase <- getsState $ getItemBody iid addItemToActorMaxSkills iid itemBase (-k) aid COrgan -> case s2 of CEqp -> return () _ -> do itemBase <- getsState $ getItemBody iid addItemToActorMaxSkills iid itemBase (-k) aid _ -> when (s2 `elem` [CEqp, COrgan]) $ do itemBase <- getsState $ getItemBody iid addItemToActorMaxSkills iid itemBase k aid updRefillHP :: MonadStateWrite m => ActorId -> Int64 -> m () updRefillHP aid nRaw = updateActor aid $ \b -> -- Make rescue easier by not going into negative HP the first time. let newRawHP = bhp b + nRaw newHP = if bhp b <= 0 then newRawHP else max 0 newRawHP n = newHP - bhp b in b { bhp = newHP , bhpDelta = let oldD = bhpDelta b in case compare n 0 of EQ -> ResDelta { resCurrentTurn = (0, 0) , resPreviousTurn = resCurrentTurn oldD } LT -> oldD {resCurrentTurn = ( fst (resCurrentTurn oldD) + n , snd (resCurrentTurn oldD) )} GT -> oldD {resCurrentTurn = ( fst (resCurrentTurn oldD) , snd (resCurrentTurn oldD) + n )} } updRefillCalm :: MonadStateWrite m => ActorId -> Int64 -> m () updRefillCalm aid n = updateActor aid $ \b -> b { bcalm = max 0 $ bcalm b + n , bcalmDelta = let oldD = bcalmDelta b in case compare n 0 of EQ -> ResDelta { resCurrentTurn = (0, 0) , resPreviousTurn = resCurrentTurn oldD } LT -> oldD {resCurrentTurn = ( fst (resCurrentTurn oldD) + n , snd (resCurrentTurn oldD) )} GT -> oldD {resCurrentTurn = ( fst (resCurrentTurn oldD) , snd (resCurrentTurn oldD) + n )} } updTrajectory :: MonadStateWrite m => ActorId -> Maybe ([Vector], Speed) -> Maybe ([Vector], Speed) -> m () updTrajectory aid fromT toT = assert (fromT /= toT) $ do body <- getsState $ getActorBody aid let !_A = assert (fromT == btrajectory body `blame` "unexpected actor trajectory" `swith` (aid, fromT, toT, body)) () updateActor aid $ \b -> b {btrajectory = toT} updQuitFaction :: MonadStateWrite m => FactionId -> Maybe Status -> Maybe Status -> m () updQuitFaction fid fromSt toSt = do let !_A = assert (fromSt /= toSt `blame` (fid, fromSt, toSt)) () fact <- getsState $ (EM.! fid) . sfactionD let !_A = assert (fromSt == gquit fact `blame` "unexpected actor quit status" `swith` (fid, fromSt, toSt, fact)) () let adj fa = fa {gquit = toSt} updateFaction fid adj updSpotStashFaction :: MonadStateWrite m => FactionId -> LevelId -> Point -> m () updSpotStashFaction fid lid pos = do let adj fa = fa {gstash = Just (lid, pos)} -- the stash may be outdated, but not empty and it's correct, -- because we know stash may be only one, so here it's added, -- the old one is removed, despite us not seeing its location; -- warning: in this form, this is not reversible, no undo, -- so we'd need to add the required @UpdLoseStashFaction@ -- elsehwere, similarly as @LoseTile@ is added when FOV -- reveals that tile is different than expected updateFaction fid adj updLoseStashFaction :: MonadStateWrite m => FactionId -> LevelId -> Point -> m () updLoseStashFaction fid lid pos = do let adj fa = assert (gstash fa == Just (lid, pos) `blame` "unexpected lack of gstash" `swith` (fid, lid, pos, fa)) $ fa {gstash = Nothing} updateFaction fid adj -- The previous leader is assumed to be alive. updLeadFaction :: MonadStateWrite m => FactionId -> Maybe ActorId -> Maybe ActorId -> m () updLeadFaction fid source target = assert (source /= target) $ do fact <- getsState $ (EM.! fid) . sfactionD let !_A = assert (fhasPointman (gkind fact)) () -- @PosNone@ ensures this mtb <- getsState $ \s -> flip getActorBody s <$> target let !_A = assert (maybe True (not . bproj) mtb `blame` (fid, source, target, mtb, fact)) () let !_A = assert (source == gleader fact `blame` "unexpected actor leader" `swith` (fid, source, target, mtb, fact)) () let adj fa = fa {_gleader = target} updateFaction fid adj updDiplFaction :: MonadStateWrite m => FactionId -> FactionId -> Diplomacy -> Diplomacy -> m () updDiplFaction fid1 fid2 fromDipl toDipl = assert (fid1 /= fid2 && fromDipl /= toDipl) $ do fact1 <- getsState $ (EM.! fid1) . sfactionD fact2 <- getsState $ (EM.! fid2) . sfactionD let !_A = assert (fromDipl == EM.findWithDefault Unknown fid2 (gdipl fact1) && fromDipl == EM.findWithDefault Unknown fid1 (gdipl fact2) `blame` "unexpected actor diplomacy status" `swith` (fid1, fid2, fromDipl, toDipl, fact1, fact2)) () let adj fid fact = fact {gdipl = EM.insert fid toDipl (gdipl fact)} updateFaction fid1 (adj fid2) updateFaction fid2 (adj fid1) updDoctrineFaction :: MonadStateWrite m => FactionId -> Ability.Doctrine -> Ability.Doctrine -> m () updDoctrineFaction fid toT fromT = do let adj fact = assert (gdoctrine fact == fromT) $ fact {gdoctrine = toT} updateFaction fid adj updAutoFaction :: MonadStateWrite m => FactionId -> Bool -> m () updAutoFaction fid st = updateFaction fid (\fact -> assert (gunderAI fact == not st) $ fact {gunderAI = st}) -- Record a given number (usually just 1, or -1 for undo) of actor kills -- for score calculation. updRecordKill :: MonadStateWrite m => ActorId -> ContentId ItemKind -> Int -> m () updRecordKill aid ikind k = do b <- getsState $ getActorBody aid let !_A = assert (not (bproj b) `blame` (aid, b)) let alterKind mn = let n = fromMaybe 0 mn + k in if n == 0 then Nothing else Just n adjFact fact = fact {gvictims = EM.alter alterKind ikind $ gvictims fact} updateFaction (bfid b) adjFact -- The death of a dominated actor counts as the dominating faction's loss -- for score purposes, so human nor AI can't treat such actor as disposable, -- which means domination will not be as cruel, as frustrating, -- as it could be and there is a higher chance of getting back alive -- the actor, the human player has grown attached to. -- Alter an attribute (actually, the only, the defining attribute) -- of a visible tile. This is similar to e.g., @UpdTrajectory@. -- -- Removing and creating embedded items when altering a tile -- is done separately via @UpdCreateItem@ and @UpdDestroyItem@. updAlterTile :: MonadStateWrite m => LevelId -> Point -> ContentId TileKind -> ContentId TileKind -> m () updAlterTile lid p fromTile toTile = assert (fromTile /= toTile) $ do COps{coTileSpeedup} <- getsState scops lvl <- getLevel lid let t = lvl `at` p if t /= fromTile then atomicFail "terrain to modify is different than assumed" else do let adj ts = ts PointArray.// [(p, toTile)] updateLevel lid $ updateTile adj case ( Tile.isExplorable coTileSpeedup fromTile , Tile.isExplorable coTileSpeedup toTile ) of (False, True) -> updateLevel lid $ \lvl2 -> lvl2 {lseen = lseen lvl2 + 1} (True, False) -> updateLevel lid $ \lvl2 -> lvl2 {lseen = lseen lvl2 - 1} _ -> return () updAlterExplorable :: MonadStateWrite m => LevelId -> Int -> m () updAlterExplorable lid delta = assert (delta /= 0) $ updateLevel lid $ \lvl -> lvl {lexpl = lexpl lvl + delta} updAlterGold :: MonadStateWrite m => Int -> m () updAlterGold delta = assert (delta /= 0) $ modifyState $ updateGold (+ delta) -- Showing to the client the embedded items, if any, is done elsewhere. updSearchTile :: MonadStateWrite m => ActorId -> Point -> ContentId TileKind -> m () updSearchTile aid p toTile = do COps{cotile} <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel $ blid b let t = lvl `at` p if t == toTile then atomicFail "tile already searched" else assert (Just t == Tile.hideAs cotile toTile) $ do updLoseTile (blid b) [(p, t)] updSpotTile (blid b) [(p, toTile)] -- not the hidden version this one time -- Notice previously invisible tiles. This is done in bulk, -- because it often involves dozens of tiles per move. -- We verify that the old tiles at the positions in question -- are indeed unknown. updSpotTile :: MonadStateWrite m => LevelId -> [(Point, ContentId TileKind)] -> m () updSpotTile lid ts = assert (not $ null ts) $ do COps{coTileSpeedup} <- getsState scops let unk tileMap (p, _) = tileMap PointArray.! p == unknownId adj tileMap = assert (allB (unk tileMap) ts) $ tileMap PointArray.// ts updateLevel lid $ updateTile adj let f (_, t1) = when (Tile.isExplorable coTileSpeedup t1) $ updateLevel lid $ \lvl -> lvl {lseen = lseen lvl + 1} mapM_ f ts -- Stop noticing previously visible tiles. It verifies -- the state of the tiles before wiping them out. updLoseTile :: MonadStateWrite m => LevelId -> [(Point, ContentId TileKind)] -> m () updLoseTile lid ts = assert (not $ null ts) $ do COps{coTileSpeedup} <- getsState scops let matches tileMap (p, ov) = tileMap PointArray.! p == ov tu = map (second (const unknownId)) ts adj tileMap = assert (allB (matches tileMap) ts) $ tileMap PointArray.// tu updateLevel lid $ updateTile adj let f (_, t1) = when (Tile.isExplorable coTileSpeedup t1) $ updateLevel lid $ \lvl -> lvl {lseen = lseen lvl - 1} mapM_ f ts updSpotEntry :: MonadStateWrite m => LevelId -> [(Point, PK.PlaceEntry)] -> m () updSpotEntry lid ts = assert (not $ null ts) $ do let alt en Nothing = Just en alt en (Just oldEn) = atomicFail $ "entry already added" `showFailure` (lid, ts, en, oldEn) f (p, en) = EM.alter (alt en) p upd m = foldr f m ts updateLevel lid $ updateEntry upd updLoseEntry :: MonadStateWrite m => LevelId -> [(Point, PK.PlaceEntry)] -> m () updLoseEntry lid ts = assert (not $ null ts) $ do let alt en Nothing = error $ "entry already removed" `showFailure` (lid, ts, en) alt en (Just oldEn) = assert (en == oldEn `blame` "unexpected lost entry" `swith` (lid, ts, en, oldEn)) Nothing f (p, en) = EM.alter (alt en) p upd m = foldr f m ts updateLevel lid $ updateEntry upd updAlterSmell :: MonadStateWrite m => LevelId -> Point -> Time -> Time -> m () updAlterSmell lid p fromSm' toSm' = do let fromSm = if fromSm' == timeZero then Nothing else Just fromSm' toSm = if toSm' == timeZero then Nothing else Just toSm' alt sm = assert (sm == fromSm `blame` "unexpected tile smell" `swith` (lid, p, fromSm, toSm, sm)) toSm updateLevel lid $ updateSmell $ EM.alter alt p updSpotSmell :: MonadStateWrite m => LevelId -> [(Point, Time)] -> m () updSpotSmell lid sms = assert (not $ null sms) $ do let alt sm Nothing = Just sm alt sm (Just oldSm) = error $ "smell already added" `showFailure` (lid, sms, sm, oldSm) f (p, sm) = EM.alter (alt sm) p upd m = foldr f m sms updateLevel lid $ updateSmell upd updLoseSmell :: MonadStateWrite m => LevelId -> [(Point, Time)] -> m () updLoseSmell lid sms = assert (not $ null sms) $ do let alt sm Nothing = error $ "smell already removed" `showFailure` (lid, sms, sm) alt sm (Just oldSm) = assert (sm == oldSm `blame` "unexpected lost smell" `swith` (lid, sms, sm, oldSm)) Nothing f (p, sm) = EM.alter (alt sm) p upd m = foldr f m sms updateLevel lid $ updateSmell upd updTimeItem :: MonadStateWrite m => ItemId -> Container -> ItemTimers -> ItemTimers -> m () updTimeItem iid c fromIt toIt = assert (fromIt /= toIt) $ do bag <- getsState $ getContainerBag c case iid `EM.lookup` bag of Just (k, it) -> do let !_A1 = assert (fromIt == it `blame` (k, it, iid, c, fromIt, toIt)) () !_A2 = assert (length toIt <= k `blame` (k, toIt, iid, c, fromIt)) () deleteItemContainer iid (k, fromIt) c insertItemContainer iid (k, toIt) c Nothing -> error $ "" `showFailure` (bag, iid, c, fromIt, toIt) updAgeGame :: MonadStateWrite m => ES.EnumSet LevelId -> m () updAgeGame lids = do modifyState $ updateTime $ flip timeShift (Delta timeClip) mapM_ (ageLevel (Delta timeClip)) $ ES.elems lids updUnAgeGame :: MonadStateWrite m => ES.EnumSet LevelId -> m () updUnAgeGame lids = do modifyState $ updateTime $ flip timeShift (timeDeltaReverse $ Delta timeClip) mapM_ (ageLevel (timeDeltaReverse $ Delta timeClip)) $ ES.elems lids ageLevel :: MonadStateWrite m => Delta Time -> LevelId -> m () ageLevel delta lid = updateLevel lid $ \lvl -> lvl {ltime = timeShift (ltime lvl) delta} updDiscover :: MonadStateWrite m => Container -> ItemId -> ContentId ItemKind -> IA.AspectRecord -> m () updDiscover _c iid ik arItem = do itemD <- getsState sitemD COps{coItemSpeedup} <- getsState scops let kmIsConst = IA.kmConst $ getKindMean ik coItemSpeedup discoKind <- getsState sdiscoKind let discoverAtMostAspect = do discoAspect <- getsState sdiscoAspect if kmIsConst || iid `EM.member` discoAspect then atomicFail "item already fully discovered" else discoverAspect iid arItem case EM.lookup iid itemD of Nothing -> atomicFail "discovered item unheard of" Just item -> case jkind item of IdentityObvious _ -> discoverAtMostAspect IdentityCovered ix _ik -> case EM.lookup ix discoKind of Just{} -> discoverAtMostAspect Nothing -> do discoverKind ix ik unless kmIsConst $ discoverAspect iid arItem resetActorMaxSkills updCover :: Container -> ItemId -> ContentId ItemKind -> IA.AspectRecord -> m () updCover _c _iid _ik _arItem = undefined updDiscoverKind :: MonadStateWrite m => Container -> ItemKindIx -> ContentId ItemKind -> m () updDiscoverKind _c ix kmKind = do discoKind <- getsState sdiscoKind if ix `EM.member` discoKind then atomicFail "item kind already discovered" else do discoverKind ix kmKind resetActorMaxSkills discoverKind :: MonadStateWrite m => ItemKindIx -> ContentId ItemKind -> m () discoverKind ix kindId = do let f Nothing = Just kindId f Just{} = error $ "already discovered" `showFailure` (ix, kindId) modifyState $ updateDiscoKind $ \discoKind1 -> EM.alter f ix discoKind1 updCoverKind :: Container -> ItemKindIx -> ContentId ItemKind -> m () updCoverKind _c _ix _ik = undefined updDiscoverAspect :: MonadStateWrite m => Container -> ItemId -> IA.AspectRecord -> m () updDiscoverAspect _c iid arItem = do COps{coItemSpeedup} <- getsState scops itemD <- getsState sitemD case EM.lookup iid itemD of Nothing -> atomicFail "discovered item unheard of" Just item -> do -- Here the kind information is exact, hence @getItemKindIdServer@. kindId <- getsState $ getItemKindIdServer item discoAspect <- getsState sdiscoAspect let kmIsConst = IA.kmConst $ getKindMean kindId coItemSpeedup if kmIsConst || iid `EM.member` discoAspect then atomicFail "item arItem already discovered" else do discoverAspect iid arItem resetActorMaxSkills discoverAspect :: MonadStateWrite m => ItemId -> IA.AspectRecord -> m () discoverAspect iid arItem = do let f Nothing = Just arItem f Just{} = error $ "already discovered" `showFailure` (iid, arItem) -- At this point we know the item is not @kmConst@. modifyState $ updateDiscoAspect $ \discoAspect1 -> EM.alter f iid discoAspect1 updCoverAspect :: Container -> ItemId -> IA.AspectRecord -> m () updCoverAspect _c _iid _arItem = undefined updDiscoverServer :: MonadStateWrite m => ItemId -> IA.AspectRecord -> m () updDiscoverServer iid arItem = modifyState $ updateDiscoAspect $ \discoAspect1 -> EM.insert iid arItem discoAspect1 updCoverServer :: MonadStateWrite m => ItemId -> IA.AspectRecord -> m () updCoverServer iid arItem = modifyState $ updateDiscoAspect $ \discoAspect1 -> assert (discoAspect1 EM.! iid == arItem) $ EM.delete iid discoAspect1 -- This is ever run only on clients. updRestart :: MonadStateWrite m => State -> m () updRestart = putState -- This is ever run only on the server. updRestartServer :: MonadStateWrite m => State -> m () updRestartServer = putState -- This is ever run only on the server. updResumeServer :: MonadStateWrite m => State -> m () updResumeServer = putState LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Atomic/MonadStateWrite.hs0000644000000000000000000003656307346545000023600 0ustar0000000000000000-- | The monad for writing to the main game state. module Game.LambdaHack.Atomic.MonadStateWrite ( MonadStateWrite(..), AtomicFail(..), atomicFail , updateLevel, updateActor, updateFaction , moveActorMap, swapActorMap , insertBagContainer, insertItemContainer, insertItemActor , deleteBagContainer, deleteItemContainer, deleteItemActor , itemsMatch, addItemToActorMaxSkills, resetActorMaxSkills #ifdef EXPOSE_INTERNAL -- * Internal operations , insertItemFloor, insertItemEmbed , insertItemOrgan, insertItemEqp, insertItemStash , deleteItemFloor, deleteItemEmbed , deleteItemOrgan, deleteItemEqp, deleteItemStash , rmFromBag #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Exception as Ex import qualified Data.EnumMap.Strict as EM import Data.Key (mapWithKeyM_) import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | The monad for writing to the main game state. Atomic updates (@UpdAtomic@) -- are given semantics in this monad. class MonadStateRead m => MonadStateWrite m where modifyState :: (State -> State) -> m () putState :: State -> m () -- | Exception signifying that atomic action failed because -- the information it carries is inconsistent with the client's state, -- (e.g., because the client knows too little to understand the command -- or already deduced the state change from earlier commands -- or is confused, amnesiac or sees illusory actors or tiles). -- Whenever we know the failure is logically impossible, -- we don't throw the @AtomicFail@ exception, but insert a normal assertion -- or @error@ call, which are never caught nor handled. newtype AtomicFail = AtomicFail String deriving Show instance Ex.Exception AtomicFail atomicFail :: String -> a atomicFail = Ex.throw . AtomicFail -- INLIning offers no speedup, increases alloc and binary size. -- EM.alter not necessary, because levels not removed, so little risk -- of adjusting at absent index. updateLevel :: MonadStateWrite m => LevelId -> (Level -> Level) -> m () updateLevel lid f = modifyState $ updateDungeon $ EM.adjust f lid -- INLIning doesn't help despite probably canceling the alt indirection. -- perhaps it's applied automatically. updateActor :: MonadStateWrite m => ActorId -> (Actor -> Actor) -> m () updateActor aid f = do let alt Nothing = error $ "no body to update" `showFailure` aid alt (Just b) = Just $ f b modifyState $ updateActorD $ EM.alter alt aid updateFaction :: MonadStateWrite m => FactionId -> (Faction -> Faction) -> m () updateFaction fid f = do let alt Nothing = error $ "no faction to update" `showFailure` fid alt (Just fact) = Just $ f fact modifyState $ updateFactionD $ EM.alter alt fid moveActorMap :: MonadStateWrite m => ActorId -> Actor -> Actor -> m () moveActorMap aid body newBody = do let rmBig Nothing = error $ "actor already removed" `showFailure` (aid, body) rmBig (Just _aid2) = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (aid == _aid2 `blame` "actor already removed" `swith` (aid, body, _aid2)) #endif Nothing addBig Nothing = Just aid addBig (Just aid2) = error $ "an actor already present there" `showFailure` (aid, body, aid2) updBig = EM.alter addBig (bpos newBody) . EM.alter rmBig (bpos body) let rmProj Nothing = error $ "actor already removed" `showFailure` (aid, body) rmProj (Just l) = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (aid `elem` l `blame` "actor already removed" `swith` (aid, body, l)) #endif (let l2 = delete aid l in if null l2 then Nothing else Just l2) addProj Nothing = Just [aid] addProj (Just l) = Just $ aid : l updProj = EM.alter addProj (bpos newBody) . EM.alter rmProj (bpos body) updateLevel (blid body) $ if bproj body then updateProjMap updProj else updateBigMap updBig swapActorMap :: MonadStateWrite m => ActorId -> Actor -> ActorId -> Actor -> m () swapActorMap source sbody target tbody = do let addBig aid1 aid2 Nothing = error $ "actor already removed" `showFailure` (aid1, aid2, source, sbody, target, tbody) addBig _aid1 aid2 (Just _aid) = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (_aid == _aid1 `blame` "wrong actor present" `swith` (_aid, _aid1, aid2, sbody, tbody)) #endif (Just aid2) updBig = EM.alter (addBig source target) (bpos sbody) . EM.alter (addBig target source) (bpos tbody) if not (bproj sbody) && not (bproj tbody) then updateLevel (blid sbody) $ updateBigMap updBig else do moveActorMap source sbody tbody moveActorMap target tbody sbody insertBagContainer :: MonadStateWrite m => ItemBag -> Container -> m () insertBagContainer bag c = case c of CFloor lid pos -> do let alt Nothing = Just bag alt (Just bag2) = atomicFail $ "floor bag not empty" `showFailure` (bag2, lid, pos, bag) updateLevel lid $ updateFloor $ EM.alter alt pos CEmbed lid pos -> do let alt Nothing = Just bag alt (Just bag2) = atomicFail $ "embed bag not empty" `showFailure` (bag2, lid, pos, bag) updateLevel lid $ updateEmbed $ EM.alter alt pos CActor aid store -> -- Very unlikely case, so we prefer brevity over performance. mapWithKeyM_ (\iid kit -> insertItemActor iid kit aid store) bag CTrunk{} -> return () insertItemContainer :: MonadStateWrite m => ItemId -> ItemQuant -> Container -> m () insertItemContainer iid kit c = case c of CFloor lid pos -> insertItemFloor iid kit lid pos CEmbed lid pos -> insertItemEmbed iid kit lid pos CActor aid store -> insertItemActor iid kit aid store CTrunk{} -> return () -- New @kit@ lands at the front of the list. insertItemFloor :: MonadStateWrite m => ItemId -> ItemQuant -> LevelId -> Point -> m () insertItemFloor iid kit lid pos = let bag = EM.singleton iid kit mergeBag = EM.insertWith (EM.unionWith mergeItemQuant) pos bag in updateLevel lid $ updateFloor mergeBag insertItemEmbed :: MonadStateWrite m => ItemId -> ItemQuant -> LevelId -> Point -> m () insertItemEmbed iid kit lid pos = let bag = EM.singleton iid kit mergeBag = EM.insertWith (EM.unionWith mergeItemQuant) pos bag in updateLevel lid $ updateEmbed mergeBag insertItemActor :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> CStore -> m () insertItemActor iid kit aid cstore = case cstore of CGround -> do b <- getsState $ getActorBody aid insertItemFloor iid kit (blid b) (bpos b) COrgan -> insertItemOrgan iid kit aid CEqp -> insertItemEqp iid kit aid CStash -> do b <- getsState $ getActorBody aid insertItemStash iid kit (bfid b) -- We assume @Meleeable@ and @Benign@ are never secret and so we don't need -- to fix the weapon counts when the item is identified later on. insertItemOrgan :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m () insertItemOrgan iid kit@(k, _) aid = do arItem <- getsState $ aspectRecordFromIid iid let bag = EM.singleton iid kit upd = EM.unionWith mergeItemQuant bag updateActor aid $ \b -> b { borgan = upd (borgan b) , bweapon = if IA.checkFlag Ability.Meleeable arItem then bweapon b + k else bweapon b , bweapBenign = if IA.checkFlag Ability.Meleeable arItem && IA.checkFlag Ability.Benign arItem then bweapBenign b + k else bweapBenign b } insertItemEqp :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m () insertItemEqp iid kit@(k, _) aid = do arItem <- getsState $ aspectRecordFromIid iid let bag = EM.singleton iid kit upd = EM.unionWith mergeItemQuant bag updateActor aid $ \b -> b { beqp = upd (beqp b) , bweapon = if IA.checkFlag Ability.Meleeable arItem then bweapon b + k else bweapon b , bweapBenign = if IA.checkFlag Ability.Meleeable arItem && IA.checkFlag Ability.Benign arItem then bweapBenign b + k else bweapBenign b } insertItemStash :: MonadStateWrite m => ItemId -> ItemQuant -> FactionId -> m () insertItemStash iid kit fid = do mstash <- getsState $ \s -> gstash $ sfactionD s EM.! fid case mstash of Just (lid, pos) -> insertItemFloor iid kit lid pos -- can't be inserted into outdated or unseen stash position, -- because such commands are visible only when the stash position is -- and so @gstash@ points at the correct one, thanks to @atomicRemember@ Nothing -> error $ "" `showFailure` (iid, kit, fid) deleteBagContainer :: MonadStateWrite m => ItemBag -> Container -> m () deleteBagContainer bag c = case c of CFloor lid pos -> do let alt Nothing = atomicFail $ "floor bag already empty" `showFailure` (lid, pos, bag) alt (Just bag2) = assert (bag == bag2) Nothing updateLevel lid $ updateFloor $ EM.alter alt pos CEmbed lid pos -> do let alt Nothing = atomicFail $ "embed bag already empty" `showFailure` (lid, pos, bag) alt (Just bag2) = assert (bag == bag2 `blame` (bag, bag2)) Nothing updateLevel lid $ updateEmbed $ EM.alter alt pos CActor aid store -> -- Very unlikely case, so we prefer brevity over performance. mapWithKeyM_ (\iid kit -> deleteItemActor iid kit aid store) bag CTrunk{} -> error $ "" `showFailure` c deleteItemContainer :: MonadStateWrite m => ItemId -> ItemQuant -> Container -> m () deleteItemContainer iid kit c = case c of CFloor lid pos -> deleteItemFloor iid kit lid pos CEmbed lid pos -> deleteItemEmbed iid kit lid pos CActor aid store -> deleteItemActor iid kit aid store CTrunk{} -> error $ "" `showFailure` c deleteItemFloor :: MonadStateWrite m => ItemId -> ItemQuant -> LevelId -> Point -> m () deleteItemFloor iid kit lid pos = let rmFromFloor (Just bag) = let nbag = rmFromBag kit iid bag in if EM.null nbag then Nothing else Just nbag rmFromFloor Nothing = error $ "item already removed" `showFailure` (iid, kit, lid, pos) in updateLevel lid $ updateFloor $ EM.alter rmFromFloor pos deleteItemEmbed :: MonadStateWrite m => ItemId -> ItemQuant -> LevelId -> Point -> m () deleteItemEmbed iid kit lid pos = let rmFromFloor (Just bag) = let nbag = rmFromBag kit iid bag in if EM.null nbag then Nothing else Just nbag rmFromFloor Nothing = error $ "item already removed" `showFailure` (iid, kit, lid, pos) in updateLevel lid $ updateEmbed $ EM.alter rmFromFloor pos deleteItemActor :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> CStore -> m () deleteItemActor iid kit aid cstore = case cstore of CGround -> do b <- getsState $ getActorBody aid deleteItemFloor iid kit (blid b) (bpos b) COrgan -> deleteItemOrgan iid kit aid CEqp -> deleteItemEqp iid kit aid CStash -> do b <- getsState $ getActorBody aid deleteItemStash iid kit (bfid b) deleteItemOrgan :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m () deleteItemOrgan iid kit@(k, _) aid = do arItem <- getsState $ aspectRecordFromIid iid updateActor aid $ \b -> b { borgan = rmFromBag kit iid (borgan b) , bweapon = if IA.checkFlag Ability.Meleeable arItem then bweapon b - k else bweapon b , bweapBenign = if IA.checkFlag Ability.Meleeable arItem && IA.checkFlag Ability.Benign arItem then bweapBenign b - k else bweapBenign b } deleteItemEqp :: MonadStateWrite m => ItemId -> ItemQuant -> ActorId -> m () deleteItemEqp iid kit@(k, _) aid = do arItem <- getsState $ aspectRecordFromIid iid updateActor aid $ \b -> b { beqp = rmFromBag kit iid (beqp b) , bweapon = if IA.checkFlag Ability.Meleeable arItem then bweapon b - k else bweapon b , bweapBenign = if IA.checkFlag Ability.Meleeable arItem && IA.checkFlag Ability.Benign arItem then bweapBenign b - k else bweapBenign b } deleteItemStash :: MonadStateWrite m => ItemId -> ItemQuant -> FactionId -> m () deleteItemStash iid kit fid = do mstash <- getsState $ \s -> gstash $ sfactionD s EM.! fid case mstash of Just (lid, pos) -> deleteItemFloor iid kit lid pos -- can't be deleted from an outdated or unseen stash position, -- because such commands are visible only when the stash position is -- and so @gstash@ points at the correct one, thanks to @atomicRemember@ Nothing -> error $ "" `showFailure` (iid, kit, fid) -- Removing the part of the kit from the back of the list, -- so that @DestroyItem kit (CreateItem kit x) == x@. rmFromBag :: ItemQuant -> ItemId -> ItemBag -> ItemBag rmFromBag kit@(k, rmIt) iid bag = let rfb Nothing = error $ "rm from empty slot" `showFailure` (k, iid, bag) rfb (Just (n, it)) = case compare n k of LT -> error $ "rm more than there is" `showFailure` (n, kit, iid, bag) EQ -> assert (rmIt == it `blame` (rmIt, it, n, kit, iid, bag)) Nothing GT -> assert (rmIt == take k it `blame` (rmIt, take k it, n, kit, iid, bag)) $ Just (n - k, take (n - k) it) in EM.alter rfb iid bag itemsMatch :: Item -> Item -> Bool itemsMatch item1 item2 = jkind item1 == jkind item2 -- Note that nothing else needs to be the same, since items are merged -- and clients have different views on dungeon items than the server. addItemToActorMaxSkills :: MonadStateWrite m => ItemId -> Item -> Int -> ActorId -> m () addItemToActorMaxSkills iid itemBase k aid = do arItem <- getsState $ aspectRecordFromItem iid itemBase let f actorMaxSk = Ability.sumScaledSkills [(actorMaxSk, 1), (IA.aSkills arItem, k)] modifyState $ updateActorMaxSkills $ EM.adjust f aid resetActorMaxSkills :: MonadStateWrite m => m () resetActorMaxSkills = do -- Each actor's equipment and organs would need to be inspected, -- the iid looked up, e.g., if it wasn't in old discoKind, but is in new, -- and then aspect record updated, so it's simpler and not much more -- expensive to generate new sactorMaxSkills. Optimize only after profiling. -- Also note this doesn't get invoked on the server, because it bails out -- earlier, upon noticing the item is already fully known. actorMaxSk <- getsState maxSkillsInDungeon modifyState $ updateActorMaxSkills $ const actorMaxSk LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Atomic/PosAtomicRead.hs0000644000000000000000000005131707346545000023212 0ustar0000000000000000-- | Representation and computation of visiblity of atomic commands -- by clients. -- -- See -- . module Game.LambdaHack.Atomic.PosAtomicRead ( PosAtomic(..), posUpdAtomic, posSfxAtomic, iidUpdAtomic, iidSfxAtomic , breakUpdAtomic, lidOfPos, seenAtomicCli, seenAtomicSer #ifdef EXPOSE_INTERNAL -- * Internal operations , pointsProjBody, posProjBody, singleAid, doubleAid , singleContainerStash, singleContainerActor #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Game.LambdaHack.Atomic.CmdAtomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import Game.LambdaHack.Definition.Defs -- All functions here that take an atomic action are executed -- in the state just before the action is executed. -- | The type representing visibility of atomic commands to factions, -- based on the position of the command, etc. Note that the server -- sees and smells all positions. Also note that hearing is not covered -- because it gives very restricted information, so hearing doesn't equal -- seeing (and we assume smelling actors get lots of data from smells). data PosAtomic = PosSight LevelId [Point] -- ^ whomever sees all the positions, notices | PosFidAndSight FactionId LevelId [Point] -- ^ observers and the faction notice | PosSmell LevelId [Point] -- ^ whomever smells all the positions, notices | PosSightLevels [(LevelId, Point)] -- ^ whomever sees all the positions, notices | PosFid FactionId -- ^ only the faction notices, server doesn't | PosFidAndSer FactionId -- ^ faction and server notices | PosSer -- ^ only the server notices | PosAll -- ^ everybody notices | PosNone -- ^ never broadcasted, but sent manually deriving (Show, Eq) -- | Produce the positions where the atomic update takes place or, more -- generally, the conditions under which the update can be noticed by -- a client. -- -- The goal of this mechanics is to ensure that atomic commands involving -- some positions visible by a client convey similar information as the client -- would get by directly observing the changes -- of the portion of server state limited to the visible positions. -- Consequently, when the visible commands are later applied -- to the client's state, the state stays consistent -- --- in sync with the server state and correctly limited by visiblity. -- There is some wiggle room both in what "in sync" and -- "visible" means and how they propagate through time. -- -- E.g., @UpdDisplaceActor@ in a black room between two enemy actors, -- with only one actor carrying a 0-radius light would not be -- distinguishable by looking at the state (or the screen) from @UpdMoveActor@ -- of the illuminated actor, hence such @UpdDisplaceActor@ should not be -- observable, but @UpdMoveActor@ in similar cotext would be -- (or the former should be perceived as the latter). -- However, to simplify, we assign as strict visibility -- requirements to @UpdMoveActor@ as to @UpdDisplaceActor@ and fall back -- to @UpdSpotActor@ (which provides minimal information that does not -- contradict state) if the visibility is lower. posUpdAtomic :: MonadStateRead m => UpdAtomic -> m PosAtomic posUpdAtomic cmd = case cmd of UpdRegisterItems{} -> return PosNone UpdCreateActor _ body _ -> return $! posProjBody body UpdDestroyActor _ body _ -> return $! posProjBody body UpdCreateItem _ _ _ _ c -> singleContainerStash c UpdDestroyItem _ _ _ _ c -> singleContainerStash c UpdSpotActor _ body -> return $! posProjBody body UpdLoseActor _ body -> return $! posProjBody body UpdSpotItem _ _ _ c -> singleContainerStash c UpdLoseItem _ _ _ c -> singleContainerStash c UpdSpotItemBag _ c _ -> singleContainerStash c UpdLoseItemBag _ c _ -> singleContainerStash c UpdMoveActor aid fromP toP -> do b <- getsState $ getActorBody aid -- Non-projectile actors are never totally isolated from environment; -- they hear, feel air movement, etc. return $! pointsProjBody b [fromP, toP] UpdWaitActor aid _ _ -> singleAid aid UpdDisplaceActor source target -> doubleAid source target UpdMoveItem _ _ aid cstore1 cstore2 -> do b <- getsState $ getActorBody aid mlidPos1 <- lidPosOfStash b cstore1 mlidPos2 <- lidPosOfStash b cstore2 let mlidPos = mlidPos1 `mplus` mlidPos2 return $! maybe (posProjBody b) (\lidPos -> PosSightLevels [lidPos, (blid b, bpos b)]) mlidPos UpdRefillHP aid _ -> singleAid aid UpdRefillCalm aid _ -> singleAid aid UpdTrajectory aid _ _ -> singleAid aid UpdQuitFaction{} -> return PosAll UpdSpotStashFaction _ fid lid pos -> return $! PosFidAndSight fid lid [pos] UpdLoseStashFaction _ fid lid pos -> return $! PosFidAndSight fid lid [pos] UpdLeadFaction fid _ _ -> return $! PosFidAndSer fid UpdDiplFaction{} -> return PosAll UpdDoctrineFaction{} -> return PosAll -- make faction lore fun UpdAutoFaction{} -> return PosAll UpdRecordKill aid _ _ -> singleAid aid UpdAlterTile lid p _ _ -> return $! PosSight lid [p] UpdAlterExplorable{} -> return PosAll -- Can't have @PosSight@, because we'd end up with many accessible -- unknown tiles, but the game reporting 'all seen'. UpdAlterGold{} -> return PosAll UpdSearchTile aid p _ -> do b <- getsState $ getActorBody aid return $! pointsProjBody b [bpos b, p] UpdHideTile aid p _ -> do b <- getsState $ getActorBody aid return $! pointsProjBody b [bpos b, p] UpdSpotTile lid ts -> do let ps = map fst ts return $! PosSight lid ps UpdLoseTile lid ts -> do let ps = map fst ts return $! PosSight lid ps UpdSpotEntry lid ts -> do let ps = map fst ts return $! PosSight lid ps UpdLoseEntry lid ts -> do let ps = map fst ts return $! PosSight lid ps UpdAlterSmell lid p _ _ -> return $! PosSmell lid [p] UpdSpotSmell lid sms -> do let ps = map fst sms return $! PosSmell lid ps UpdLoseSmell lid sms -> do let ps = map fst sms return $! PosSmell lid ps UpdTimeItem _ c _ _ -> singleContainerStash c UpdAgeGame _ -> return PosAll UpdUnAgeGame _ -> return PosAll UpdDiscover c _ _ _ -> singleContainerActor c -- This implies other factions applying items from their inventory, -- when we can't see the position of the stash, won't Id the item -- for us, even when notice item usage. Thrown items will Id, though, -- just as triggering items from the floor or embedded items. UpdCover c _ _ _ -> singleContainerActor c UpdDiscoverKind c _ _ -> singleContainerActor c UpdCoverKind c _ _ -> singleContainerActor c UpdDiscoverAspect c _ _ -> singleContainerActor c UpdCoverAspect c _ _ -> singleContainerActor c UpdDiscoverServer{} -> return PosSer UpdCoverServer{} -> return PosSer UpdPerception{} -> return PosNone UpdRestart fid _ _ _ _ _ -> return $! PosFid fid UpdRestartServer _ -> return PosSer UpdResume _ _ -> return PosNone UpdResumeServer _ -> return PosSer UpdKillExit fid -> return $! PosFid fid UpdWriteSave -> return PosAll UpdHearFid fid _ _ -> return $! PosFid fid UpdMuteMessages fid _ -> return $! PosFid fid -- | Produce the positions where the atomic special effect takes place. posSfxAtomic :: MonadStateRead m => SfxAtomic -> m PosAtomic posSfxAtomic cmd = case cmd of SfxStrike _ target _ -> singleAid target SfxRecoil _ target _ -> singleAid target SfxSteal _ target _ -> singleAid target SfxRelease _ target _ -> singleAid target SfxProject aid _ -> singleAid aid SfxReceive aid _ -> singleAid aid SfxApply aid _ -> singleAid aid SfxCheck aid _ -> singleAid aid SfxTrigger aid lid p _ -> do body <- getsState $ getActorBody aid return $! PosSightLevels [(lid, p), (blid body, bpos body)] -- @PosFidAndSightLevels@ would be better, but no big deal SfxShun aid lid p _ -> do body <- getsState $ getActorBody aid return $! PosSightLevels [(lid, p), (blid body, bpos body)] SfxEffect _ aid _ _ _ -> singleAid aid -- sometimes we don't see source, OK SfxItemApplied _ _ c -> singleContainerActor c SfxMsgFid fid _ -> return $! PosFid fid SfxRestart -> return PosAll SfxCollideTile aid _ -> singleAid aid SfxTaunt _ aid -> singleAid aid -- | All items introduced by the atomic command, to be used in it. iidUpdAtomic :: UpdAtomic -> [ItemId] iidUpdAtomic cmd = case cmd of UpdRegisterItems{} -> [] UpdCreateActor{} -> [] -- iids and items needed even on server UpdDestroyActor{} -> [] UpdCreateItem{} -> [] UpdDestroyItem{} -> [] UpdSpotActor _ body -> getCarriedIidsAndTrunk body UpdLoseActor{} -> [] -- already seen, so items known UpdSpotItem _ iid _ _ -> [iid] UpdLoseItem{} -> [] UpdSpotItemBag _ _ bag -> EM.keys bag UpdLoseItemBag{} -> [] UpdMoveActor{} -> [] UpdWaitActor{} -> [] UpdDisplaceActor{} -> [] UpdMoveItem{} -> [] UpdRefillHP{} -> [] UpdRefillCalm{} -> [] UpdTrajectory{} -> [] UpdQuitFaction{} -> [] UpdSpotStashFaction{} -> [] UpdLoseStashFaction{} -> [] UpdLeadFaction{} -> [] UpdDiplFaction{} -> [] UpdDoctrineFaction{} -> [] UpdAutoFaction{} -> [] UpdRecordKill{} -> [] UpdAlterTile{} -> [] UpdAlterExplorable{} -> [] UpdAlterGold{} -> [] UpdSearchTile{} -> [] UpdHideTile{} -> [] UpdSpotTile{} -> [] UpdLoseTile{} -> [] UpdSpotEntry{} -> [] UpdLoseEntry{} -> [] UpdAlterSmell{} -> [] UpdSpotSmell{} -> [] UpdLoseSmell{} -> [] UpdTimeItem iid _ _ _ -> [iid] UpdAgeGame{} -> [] UpdUnAgeGame{} -> [] UpdDiscover _ iid _ _ -> [iid] UpdCover _ iid _ _ -> [iid] UpdDiscoverKind{} -> [] UpdCoverKind{} -> [] UpdDiscoverAspect _ iid _ -> [iid] UpdCoverAspect _ iid _ -> [iid] UpdDiscoverServer{} -> [] -- never sent to clients UpdCoverServer{} -> [] UpdPerception{} -> [] UpdRestart{} -> [] UpdRestartServer{} -> [] UpdResume{} -> [] UpdResumeServer{} -> [] UpdKillExit{} -> [] UpdWriteSave -> [] UpdHearFid{} -> [] UpdMuteMessages{} -> [] -- | All items introduced by the atomic special effect, to be used in it. iidSfxAtomic :: SfxAtomic -> [ItemId] iidSfxAtomic cmd = case cmd of SfxStrike _ _ iid -> [iid] SfxRecoil _ _ iid -> [iid] SfxSteal _ _ iid -> [iid] SfxRelease _ _ iid -> [iid] SfxProject _ iid -> [iid] SfxReceive _ iid -> [iid] SfxApply _ iid -> [iid] SfxCheck _ iid -> [iid] SfxTrigger{} -> [] SfxShun{} -> [] SfxEffect{} -> [] SfxItemApplied _ iid _ -> [iid] SfxMsgFid{} -> [] SfxRestart{} -> [] SfxCollideTile{} -> [] SfxTaunt{} -> [] pointsProjBody :: Actor -> [Point] -> PosAtomic pointsProjBody body ps = if bproj body then PosSight (blid body) ps else PosFidAndSight (bfid body) (blid body) ps posProjBody :: Actor -> PosAtomic posProjBody body = pointsProjBody body [bpos body] singleAid :: MonadStateRead m => ActorId -> m PosAtomic singleAid aid = do body <- getsState $ getActorBody aid return $! posProjBody body doubleAid :: MonadStateRead m => ActorId -> ActorId -> m PosAtomic doubleAid source target = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target -- No @PosFidAndSight@ instead of @PosSight@, because both positions -- need to be seen to have the enemy actor in client's state. return $! assert (blid sb == blid tb) $ PosSight (blid sb) [bpos sb, bpos tb] singleContainerStash :: MonadStateRead m => Container -> m PosAtomic singleContainerStash (CFloor lid p) = return $! PosSight lid [p] singleContainerStash (CEmbed lid p) = return $! PosSight lid [p] singleContainerStash (CActor aid cstore) = do b <- getsState $ getActorBody aid mlidPos <- lidPosOfStash b cstore return $! maybe (posProjBody b) (\lidPos -> PosSightLevels [lidPos, (blid b, bpos b)]) -- the actor's position is needed so that a message -- about the actor is not sent to a client that doesn't -- know the actor; actor's faction is ignored, because -- for these operations actor doesn't vanish mlidPos singleContainerStash (CTrunk fid lid p) = return $! PosFidAndSight fid lid [p] singleContainerActor :: MonadStateRead m => Container -> m PosAtomic singleContainerActor (CFloor lid p) = return $! PosSight lid [p] singleContainerActor (CEmbed lid p) = return $! PosSight lid [p] singleContainerActor (CActor aid _) = do b <- getsState $ getActorBody aid return $! posProjBody b -- stash position is ignored, because for these operations, nothing -- is added to that position; the store name is only used for flavour text singleContainerActor (CTrunk fid lid p) = return $! PosFidAndSight fid lid [p] lidPosOfStash :: MonadStateRead m => Actor -> CStore -> m (Maybe (LevelId, Point)) lidPosOfStash b cstore = case cstore of CStash -> do mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just{} -> return mstash Nothing -> error $ "manipulating void stash" `showFailure` b _ -> return Nothing -- | Decompose an atomic action that is outside a client's visiblity. -- The decomposed actions give less information that the original command, -- but some of them may fall within the visibility range of the client. -- The original action may give more information than even the total sum -- of all actions it's broken into. E.g., @UpdMoveActor@ -- informs about the continued existence of the actor between -- moves vs popping out of existence and then back in. -- -- This is computed in server's @State@ from before performing the command. breakUpdAtomic :: MonadStateRead m => UpdAtomic -> m [UpdAtomic] breakUpdAtomic cmd = case cmd of UpdCreateItem verbose iid item kit (CActor aid CStash) -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [UpdCreateItem verbose iid item kit (CFloor lid pos)] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, item) UpdDestroyItem verbose iid item kit (CActor aid CStash) -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [UpdDestroyItem verbose iid item kit (CFloor lid pos)] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, item) UpdSpotItem verbose iid kit (CActor aid CStash) -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [UpdSpotItem verbose iid kit (CFloor lid pos)] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, iid) UpdLoseItem verbose iid kit (CActor aid CStash) -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [UpdLoseItem verbose iid kit (CFloor lid pos)] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, iid) UpdSpotItemBag verbose (CActor aid CStash) bag -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [UpdSpotItemBag verbose (CFloor lid pos) bag] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, bag) UpdLoseItemBag verbose (CActor aid CStash) bag -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [UpdLoseItemBag verbose (CFloor lid pos) bag] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, bag) UpdMoveItem iid k aid CStash store2 -> do b <- getsState $ getActorBody aid bag <- getsState $ getBodyStoreBag b CStash let (k1, it1) = bag EM.! iid kit = assert (k <= k1) (k, take k it1) mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [ UpdLoseItem True iid kit (CFloor lid pos) , UpdSpotItem True iid kit (CActor aid store2) ] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, iid) UpdMoveItem iid k aid store1 CStash -> do b <- getsState $ getActorBody aid bag <- getsState $ getBodyStoreBag b store1 let (k1, it1) = bag EM.! iid kit = assert (k <= k1) (k, take k it1) mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [ UpdLoseItem True iid kit (CActor aid store1) , UpdSpotItem True iid kit (CFloor lid pos) ] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, iid) UpdMoveActor aid fromP toP -> do -- We assume other factions don't see leaders and we know the actor's -- faction always sees the atomic command and no other commands -- may be inserted between the two below, so the leader doesn't -- need to be updated, even when aid is the leader. b <- getsState $ getActorBody aid return [ UpdLoseActor aid b , UpdSpotActor aid b {bpos = toP, boldpos = Just fromP} ] UpdDisplaceActor source target -> do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target -- The order ensures the invariant that no two big actors occupy the same -- position is maintained. The actions about leadership are required -- to keep faction data (identify of the leader) consistent with actor -- data (the actor that is the leader exists). Here, for speed -- and simplicity we violate the property that in a faction -- that has leaders, if any eligible actor is alive, -- the leader is set, because for a moment there may be no leader, -- even though other actors of the faction may exist. msleader <- getsState $ gleader . (EM.! bfid sb) . sfactionD mtleader <- getsState $ gleader . (EM.! bfid tb) . sfactionD return $ [ UpdLeadFaction (bfid sb) msleader Nothing | Just source == msleader ] ++ [ UpdLeadFaction (bfid tb) mtleader Nothing | Just target == mtleader ] ++ [ UpdLoseActor source sb , UpdLoseActor target tb , UpdSpotActor source sb { bpos = bpos tb , boldpos = Just $ bpos sb } , UpdSpotActor target tb { bpos = bpos sb , boldpos = Just $ bpos tb } ] ++ [ UpdLeadFaction (bfid sb) Nothing msleader | Just source == msleader ] ++ [ UpdLeadFaction (bfid tb) Nothing mtleader | Just target == mtleader ] UpdTimeItem iid (CActor aid CStash) fromIt toIt -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> return [UpdTimeItem iid (CFloor lid pos) fromIt toIt] Nothing -> error $ "manipulating void stash" `showFailure` (aid, b, iid) _ -> return [] -- | What is the main map level the @PosAtomic@ refers to, if any. lidOfPos :: PosAtomic -> Maybe LevelId lidOfPos posAtomic = case posAtomic of PosSight lid _ -> Just lid PosFidAndSight _ lid _ -> Just lid PosSmell lid _ -> Just lid PosSightLevels [] -> Nothing PosSightLevels ((lid, _) : _) -> Just lid PosFid{} -> Nothing PosFidAndSer{} -> Nothing PosSer -> Nothing PosAll -> Nothing PosNone -> Nothing -- | Given the client, its perception and an atomic command, determine -- if the client notices the command. seenAtomicCli :: Bool -> FactionId -> PerLid -> PosAtomic -> Bool seenAtomicCli knowEvents fid perLid posAtomic = let per = (perLid EM.!) in case posAtomic of PosSight lid ps -> all (`ES.member` totalVisible (per lid)) ps || knowEvents PosFidAndSight fid2 lid ps -> fid == fid2 || all (`ES.member` totalVisible (per lid)) ps || knowEvents PosSmell lid ps -> all (`ES.member` totalSmelled (per lid)) ps || knowEvents PosSightLevels l -> let visible (lid, pos) = pos `ES.member` totalVisible (per lid) in all visible l || knowEvents PosFid fid2 -> fid == fid2 PosFidAndSer fid2 -> fid == fid2 PosSer -> False PosAll -> True PosNone -> False -- | Determine whether the server would see a command that has -- the given visibilty conditions. seenAtomicSer :: PosAtomic -> Bool seenAtomicSer posAtomic = case posAtomic of PosFid _ -> False PosNone -> error $ "no position possible" `showFailure` posAtomic _ -> True LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client.hs0000644000000000000000000000162407346545000020516 0ustar0000000000000000-- | Semantics of responses that are sent from server to clients, -- in terms of client state transformations, -- and semantics of human commands and AI moves, in terms of requests -- to be sent from the client to the server. -- -- See -- . module Game.LambdaHack.Client ( -- * Re-exported from "Game.LambdaHack.Client.LoopM" loopCli -- * Re-exported from "Game.LambdaHack.Client.Request" , RequestAI, ReqAI(..), RequestUI, ReqUI(..), RequestTimed(..) -- * Re-exported from "Game.LambdaHack.Client.Response" , Response (..) -- * Re-exported from "Game.LambdaHack.Client.UI" , CCUI , UIOptions, applyUIOptions, uOverrideCmdline, mkUIOptions ) where import Prelude () import Game.LambdaHack.Client.LoopM import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.Response import Game.LambdaHack.Client.UI LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/0000755000000000000000000000000007346545000020157 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/AI.hs0000644000000000000000000000711407346545000021007 0ustar0000000000000000-- | Ways for the client to use AI to produce server requests, based on -- the client's view of the game state. module Game.LambdaHack.Client.AI ( queryAI #ifdef EXPOSE_INTERNAL -- * Internal operations , pickActorAndAction #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Game.LambdaHack.Client.AI.PickActionM import Game.LambdaHack.Client.AI.PickActorM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types -- | Handle the move of an actor under AI control (regardless if the whole -- faction is under human or computer control). queryAI :: MonadClient m => ActorId -> m RequestAI queryAI aid = do -- @sleader@ may be different from @gleader@ due to @restoreLeaderFromRun@, -- but only leaders may change faction leader, so we fix that beforehand: body <- getsState $ getActorBody aid foeAssocs <- getsState $ foeRegularAssocs (bfid body) (blid body) friendAssocs <- getsState $ friendRegularAssocs (bfid body) (blid body) mleader <- getsState $ gleader . (EM.! bfid body) . sfactionD mleaderCli <- getsClient sleader unless (Just aid == mleader || mleader == mleaderCli) $ -- @aid@ is not the leader, so he can't change leader later on, -- so we match the leaders here modifyClient $ \cli -> cli {_sleader = mleader} (aidToMove, treq, oldFlee) <- pickActorAndAction foeAssocs friendAssocs Nothing aid let tryAgain = do -- Leader waits; a waste; try once to pick a yet different leader -- or at least a non-waiting action. Undo state changes in @pickAction@: modifyClient $ \cli -> cli { _sleader = mleader , sfleeD = EM.alter (const oldFlee) aidToMove $ sfleeD cli } (a, t, _) <- pickActorAndAction foeAssocs friendAssocs (Just aidToMove) aid return (a, t) (aidToMove2, treq2) <- if mleader /= Just aid then return (aidToMove, treq) else case treq of ReqWait -> tryAgain ReqYell -> tryAgain _ -> return (aidToMove, treq) return ( ReqAITimed treq2 , if aidToMove2 /= aid then Just aidToMove2 else Nothing ) -- | Pick an actor to move and an action for him to perform, given an optional -- previous candidate actor and action and the server-proposed actor. pickActorAndAction :: MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> Maybe ActorId -> ActorId -> m (ActorId, RequestTimed, Maybe (Point, Time)) pickActorAndAction foeAssocs friendAssocs maid aid = do mleader <- getsClient sleader aidToMove <- if mleader == Just aid then pickActorToMove foeAssocs friendAssocs maid else do setTargetFromDoctrines foeAssocs friendAssocs aid return aid oldFlee <- getsClient $ EM.lookup aidToMove . sfleeD -- Trying harder (@retry@) whenever no better leader found and so at least -- a non-waiting action should be found. -- If a new leader found, there is hope (but we don't check) -- that he gets a non-waiting action without any desperate measures. let retry = Just aidToMove == maid treq <- pickAction foeAssocs friendAssocs aidToMove retry return (aidToMove, treq, oldFlee) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/AI/0000755000000000000000000000000007346545000020450 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/AI/ConditionM.hs0000644000000000000000000004606307346545000023060 0ustar0000000000000000-- | Assorted conditions used later on in AI logic. module Game.LambdaHack.Client.AI.ConditionM ( condAimEnemyTargetedM , condAimEnemyOrStashM , condAimEnemyOrRememberedM , condAimNonEnemyPresentM , condAimCrucialM , condTgtNonmovingEnemyM , condAdjTriggerableM , meleeThreatDistList , condBlocksFriendsM , condFloorWeaponM , condNoEqpWeaponM , condCanProjectM , condProjectListM , benAvailableItems , hinders , condDesirableFloorItemM , benGroundItems , desirableItem , condSupport , condAloneM , condShineWouldBetrayM , fleeList ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.RuleKind as RK import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- All conditions are (partially) lazy, because they are not always -- used in the strict monadic computations they are in. -- | Require that a target enemy is visible by the party. condAimEnemyTargetedM :: MonadClientRead m => ActorId -> m Bool condAimEnemyTargetedM aid = do btarget <- getsClient $ getTarget aid return $ case btarget of Just (TEnemy _) -> True _ -> False -- | Require that a target enemy or enemy stash is visible by the party. condAimEnemyOrStashM :: MonadClientRead m => ActorId -> m Bool condAimEnemyOrStashM aid = do btarget <- getsClient $ getTarget aid return $ case btarget of Just (TEnemy _) -> True Just (TPoint (TStash _) _ _) -> True -- speedup from: lid == blid b _ -> False -- | Require that a target enemy is remembered on the actor's level. condAimEnemyOrRememberedM :: MonadClientRead m => ActorId -> m Bool condAimEnemyOrRememberedM aid = do b <- getsState $ getActorBody aid btarget <- getsClient $ getTarget aid return $ case btarget of Just (TEnemy _) -> True Just (TPoint (TEnemyPos _) lid _) -> lid == blid b Just (TPoint (TStash _) lid _) -> lid == blid b _ -> False -- | Require that a target non-enemy is visible by the party. condAimNonEnemyPresentM :: MonadClientRead m => ActorId -> m Bool condAimNonEnemyPresentM aid = do btarget <- getsClient $ getTarget aid return $ case btarget of Just (TNonEnemy _) -> True _ -> False -- | Require that the target is crucial to success, e.g., an item, -- or that it's not too far away and so the changes to get it are high. condAimCrucialM :: MonadClientRead m => ActorId -> m Bool condAimCrucialM aid = do b <- getsState $ getActorBody aid mtgtMPath <- getsClient $ EM.lookup aid . stargetD return $ case mtgtMPath of Just TgtAndPath{tapTgt=TEnemy _} -> True Just TgtAndPath{tapTgt=TPoint tgoal lid _, tapPath=Just AndPath{pathLen}} -> lid == blid b && (pathLen < 10 -- close enough to get there first || tgoal `notElem` [TUnknown, TKnown]) Just TgtAndPath{tapTgt=TVector{}, tapPath=Just AndPath{pathLen}} -> pathLen < 7 -- can't say if the target important, but the constants -- from @take6@ and @traSlack7@ ensure target is -- already approached or close to level edge -- or not a random @traSlack7@ wandering _ -> False -- includes the case of target with no path -- | Check if the target is a nonmoving enemy. condTgtNonmovingEnemyM :: MonadClientRead m => ActorId -> m Bool condTgtNonmovingEnemyM aid = do btarget <- getsClient $ getTarget aid case btarget of Just (TEnemy enemy) -> do actorMaxSk <- getsState $ getActorMaxSkills enemy return $ Ability.getSk Ability.SkMove actorMaxSk <= 0 _ -> return False -- | Require the actor stands on or adjacent to a triggerable tile -- (e.g., stairs). condAdjTriggerableM :: MonadStateRead m => Ability.Skills -> ActorId -> m Bool condAdjTriggerableM actorSk aid = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel $ blid b let alterSkill = Ability.getSk Ability.SkAlter actorSk alterMinSkill p = Tile.alterMinSkill coTileSpeedup $ lvl `at` p underFeet p = p == bpos b -- if enter and alter, be more permissive -- Before items are applied (which AI attempts even if apply -- skills too low), tile must be alerable, hence both checks. hasTriggerable p = (underFeet p || alterSkill >= fromEnum (alterMinSkill p)) && p `EM.member` lembed lvl return $ any hasTriggerable $ bpos b : vicinityUnsafe (bpos b) -- | Produce the chess-distance-sorted list of non-low-HP, -- melee-cabable foes on the level. We don't consider path-distance, -- because we are interested in how soon the foe can close in to hit us, -- which can diverge greately from path distance for short distances, -- e.g., when terrain gets revealed. We don't consider non-moving actors, -- because they can't chase us and also because they can't be aggresive -- so to resolve the stalemate, the opposing AI has to be aggresive -- by ignoring them and closing in to melee distance. meleeThreatDistList :: [(ActorId, Actor)] -> ActorId -> State -> [(Int, (ActorId, Actor))] meleeThreatDistList foeAssocs aid s = let actorMaxSkills = sactorMaxSkills s b = getActorBody aid s strongActor (aid2, b2) = let actorMaxSk = actorMaxSkills EM.! aid2 nonmoving = Ability.getSk Ability.SkMove actorMaxSk <= 0 in not (hpTooLow b2 actorMaxSk || nonmoving) && actorCanMeleeToHarm actorMaxSkills aid2 b2 allThreats = filter strongActor foeAssocs addDist (aid2, b2) = (chessDist (bpos b) (bpos b2), (aid2, b2)) in sortBy (comparing fst) $ map addDist allThreats -- | Require the actor blocks the paths of any of his party members. condBlocksFriendsM :: MonadClientRead m => ActorId -> m Bool condBlocksFriendsM aid = do b <- getsState $ getActorBody aid targetD <- getsClient stargetD let blocked aid2 = aid2 /= aid && case EM.lookup aid2 targetD of Just TgtAndPath{tapPath=Just AndPath{pathList=q : _}} | q == bpos b -> True _ -> False any blocked <$> getsState (fidActorRegularIds (bfid b) (blid b)) -- | Require the actor stands over a weapon that would be auto-equipped, -- if only it was a desirable item (checked elsewhere). condFloorWeaponM :: MonadStateRead m => ActorId -> m Bool condFloorWeaponM aid = any (IA.checkFlag Ability.Meleeable . aspectRecordFull . snd) <$> getsState (fullAssocs aid [CGround]) -- | Check whether the actor has no weapon in equipment. condNoEqpWeaponM :: MonadStateRead m => ActorId -> m Bool condNoEqpWeaponM aid = not . any (IA.checkFlag Ability.Meleeable . aspectRecordFull . snd) <$> getsState (fullAssocs aid [CEqp]) -- | Require that the actor can project any items. condCanProjectM :: MonadClientRead m => Int -> ActorId -> m Bool condCanProjectM skill aid = do side <- getsClient sside curChal <- getsClient scurChal fact <- getsState $ (EM.! side) . sfactionD if skill < 1 || ckeeper curChal && fhasUI (gkind fact) then return False else -- shortcut -- Compared to conditions in @projectItem@, range and charge are ignored, -- because they may change by the time the position for the fling -- is reached. not . null <$> condProjectListM skill aid condProjectListM :: MonadClientRead m => Int -> ActorId -> m [(Double, CStore, ItemId, ItemFull, ItemQuant)] condProjectListM skill aid = do condShineWouldBetray <- condShineWouldBetrayM aid condAimEnemyOrRemembered <- condAimEnemyOrRememberedM aid discoBenefit <- getsClient sdiscoBenefit getsState $ projectList discoBenefit skill aid condShineWouldBetray condAimEnemyOrRemembered projectList :: DiscoveryBenefit -> Int -> ActorId -> Bool -> Bool -> State -> [(Double, CStore, ItemId, ItemFull, ItemQuant)] projectList discoBenefit skill aid condShineWouldBetray condAimEnemyOrRemembered s = let b = getActorBody aid s actorMaxSk = getActorMaxSkills aid s calmE = calmEnough b actorMaxSk heavilyDistressed = -- Actor hit by a projectile or similarly distressed. deltasSerious (bcalmDelta b) uneasy = condAimEnemyOrRemembered || not calmE || heavilyDistressed -- don't take recent fleeing into account when item can be lost coeff CGround = 2 -- pickup turn saved coeff COrgan = error $ "" `showFailure` benList coeff CEqp = 1000 -- must hinder currently (or be very potent); -- note: not larger, to avoid Int32 overflow coeff CStash = 1 -- This detects if the value of keeping the item in eqp is in fact < 0. hind = hinders condShineWouldBetray uneasy actorMaxSk goodMissile (Benefit{benInEqp, benFling}, cstore, iid, itemFull, kit) = let arItem = aspectRecordFull itemFull benR = coeff cstore * benFling in if benR < -1 -- ignore very weak projectiles && (not benInEqp -- can't wear, so OK to risk losing or breaking || not (IA.checkFlag Ability.Meleeable arItem) -- anything else expendable && hind itemFull) -- hinders now, so possibly often && permittedProjectAI skill calmE itemFull then Just (benR, cstore, iid, itemFull, kit) else Nothing stores = [CStash, CGround] ++ [CEqp | calmE] benList = benAvailableItems discoBenefit aid stores s in mapMaybe goodMissile benList -- | Produce the list of items from the given stores available to the actor -- and the items' values. benAvailableItems :: DiscoveryBenefit -> ActorId -> [CStore] -> State -> [(Benefit, CStore, ItemId, ItemFull, ItemQuant)] benAvailableItems discoBenefit aid cstores s = let b = getActorBody aid s mstash = gstash $ sfactionD s EM.! bfid b ben _ CGround | mstash == Just (blid b, bpos b) = [] ben bag cstore = [ (discoBenefit EM.! iid, cstore, iid, itemToFull iid s, kit) | (iid, kit) <- EM.assocs bag] benCStore cs = ben (getBodyStoreBag b cs s) cs in concatMap benCStore cstores hinders :: Bool -> Bool -> Ability.Skills -> ItemFull -> Bool hinders condShineWouldBetray uneasy actorMaxSk itemFull = let arItem = aspectRecordFull itemFull itemShine = 0 < IA.getSkill Ability.SkShine arItem -- @condAnyFoeAdj@ is not checked, because it's transient and also item -- management is unlikely to happen during melee, anyway itemShineBad = condShineWouldBetray && itemShine in -- In the presence of enemies (seen, remembered or unseen but distressing) -- actors want to hide in the dark. uneasy && itemShineBad -- even if it's a weapon, take it off -- Fast actors want to hit hard, because they hit much more often -- than receive hits. || gearSpeed actorMaxSk > speedWalk && not (IA.checkFlag Ability.Meleeable arItem) -- in case it's the only weapon && 0 > IA.getSkill Ability.SkHurtMelee arItem -- | Require that the actor stands over a desirable item. condDesirableFloorItemM :: MonadClientRead m => ActorId -> m Bool condDesirableFloorItemM aid = not . null <$> benGroundItems aid -- | Produce the list of items on the ground beneath the actor -- that are worth picking up. benGroundItems :: MonadClientRead m => ActorId -> m [(Benefit, CStore, ItemId, ItemFull, ItemQuant)] benGroundItems aid = do cops <- getsState scops b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD discoBenefit <- getsClient sdiscoBenefit let canEsc = fcanEscape (gkind fact) isDesirable (ben, _, _, itemFull, _) = desirableItem cops canEsc (benPickup ben) (aspectRecordFull itemFull) (itemKind itemFull) 99 -- fake, because no time is wasted walking to item filter isDesirable <$> getsState (benAvailableItems discoBenefit aid [CGround]) desirableItem :: COps -> Bool -> Double -> IA.AspectRecord -> IK.ItemKind -> Int -> Bool desirableItem COps{corule} canEsc benPickup arItem itemKind k = let loneProjectile = IK.isymbol itemKind == IK.rsymbolProjectile (RK.ritemSymbols corule) && k == 1 && Dice.infDice (IK.icount itemKind) > 1 -- never generated as lone; usually means weak useful = if canEsc then benPickup > 0 || IA.checkFlag Ability.Precious arItem else -- A hack to prevent monsters from picking up -- treasure meant for heroes. let preciousNotUseful = IA.isHumanTrinket itemKind in benPickup > 0 && not preciousNotUseful in useful && not loneProjectile condSupport :: MonadClientRead m => [(ActorId, Actor)] -> Int -> ActorId -> m Bool {-# INLINE condSupport #-} condSupport friendAssocs param aid = do mtgtMPath <- getsClient $ EM.lookup aid . stargetD getsState $ strongSupport friendAssocs param aid mtgtMPath strongSupport :: [(ActorId, Actor)] -> Int -> ActorId -> Maybe TgtAndPath -> State -> Bool strongSupport friendAssocs param aid mtgtMPath s = -- The smaller the area scanned for friends, the lower number required. let actorMaxSkills = sactorMaxSkills s actorMaxSk = actorMaxSkills EM.! aid n = min 2 param - Ability.getSk Ability.SkAggression actorMaxSk b = getActorBody aid s approaching b2 = case mtgtMPath of Just TgtAndPath{tapTgt=TEnemy{},tapPath=Just AndPath{pathGoal}} -> chessDist (bpos b2) pathGoal <= 1 + param -- will soon melee anyway _ -> False closeEnough b2 = let dist = chessDist (bpos b) (bpos b2) in dist > 0 && (dist <= max 2 param || approaching b2) closeAndStrong (aid2, b2) = closeEnough b2 && actorCanMeleeToHarm actorMaxSkills aid2 b2 closeAndStrongFriends = filter closeAndStrong friendAssocs in n <= 0 || not (null (drop (n - 1) closeAndStrongFriends)) -- optimized: length closeAndStrongFriends >= n -- The numbers reflect fleeing AI conditions for non-aggresive actors -- so that actors don't wait for support that is not possible due to not -- enough friends on the level, even counting sleeping ones. condAloneM :: MonadStateRead m => [(ActorId, Actor)] -> ActorId -> m Bool condAloneM friendAssocs aid = do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b let onStashLevel = case mstash of Nothing -> False Just (lid, _) -> lid == blid b return $! length friendAssocs <= if onStashLevel then 3 else 2 -- | Require that the actor stands in the dark and so would be betrayed -- by his own equipped light, condShineWouldBetrayM :: MonadStateRead m => ActorId -> m Bool condShineWouldBetrayM aid = do b <- getsState $ getActorBody aid aInAmbient <- getsState $ actorInAmbient b return $ not aInAmbient -- tile is dark, so actor could hide -- | Produce a list of acceptable adjacent points to flee to. fleeList :: MonadClientRead m => [(ActorId, Actor)] -> ActorId -> m ([(Int, Point)], [(Int, Point)]) fleeList foeAssocs aid = do COps{coTileSpeedup} <- getsState scops mtgtMPath <- getsClient $ EM.lookup aid . stargetD -- Prefer fleeing along the path to target, unless the target is a foe, -- in which case flee in the opposite direction. let etgtPath = case mtgtMPath of Just TgtAndPath{ tapPath=Just AndPath{pathList, pathGoal} , tapTgt } -> case tapTgt of TEnemy{} -> Left pathGoal TPoint TEnemyPos{} _ _ -> Left pathGoal -- this is too weak, because only one is recorded and sometimes -- many are needed to decide to flee next turn as well _ -> Right pathList _ -> Right [] b <- getsState $ getActorBody aid lvl <- getLevel $ blid b localTime <- getsState $ getLocalTime (blid b) fleeD <- getsClient sfleeD -- But if fled recently, prefer even more fleeing further this turn. let eOldFleeOrTgt = case EM.lookup aid fleeD of Just (fleeStart, time) | timeRecent5 localTime time -> Left fleeStart _ -> etgtPath myVic = vicinityUnsafe $ bpos b dist p | null foeAssocs = 100 | otherwise = minimum $ map (chessDist p . bpos . snd) foeAssocs dVic = map (dist &&& id) myVic -- Flee, if possible. Direct access required; not enough time to open. -- Can't be occupied. accWalkUnocc p = Tile.isWalkable coTileSpeedup (lvl `at` p) && not (occupiedBigLvl p lvl) && not (occupiedProjLvl p lvl) accWalkVic = filter (accWalkUnocc . snd) dVic gtVic = filter ((> dist (bpos b)) . fst) accWalkVic eqVicRaw = filter ((== dist (bpos b)) . fst) accWalkVic (eqVicOld, eqVic) = partition ((== boldpos b) . Just . snd) eqVicRaw accNonWalkUnocc p = not (Tile.isWalkable coTileSpeedup (lvl `at` p)) && Tile.isEasyOpen coTileSpeedup (lvl `at` p) && not (occupiedBigLvl p lvl) && not (occupiedProjLvl p lvl) accNonWalkVic = filter (accNonWalkUnocc . snd) dVic gtEqNonVic = filter ((>= dist (bpos b)) . fst) accNonWalkVic ltAllVic = filter ((< dist (bpos b)) . fst) dVic rewardPath mult (d, p) = case eOldFleeOrTgt of Right tgtPathList | p `elem` tgtPathList -> (100 * mult * d, p) Right tgtPathList | any (adjacent p) tgtPathList -> (10 * mult * d, p) Left pathGoal | bpos b /= pathGoal -> let venemy = towards (bpos b) pathGoal vflee = towards (bpos b) p sq = euclidDistSqVector venemy vflee skew = case compare sq 2 of GT -> 100 * sq EQ -> 10 * sq LT -> sq -- going towards enemy (but may escape adjacent foes) in (mult * skew * d, p) _ -> (mult * d, p) -- far from target path or even on target goal goodVic = map (rewardPath 10000) gtVic ++ map (rewardPath 100) eqVic badVic = map (rewardPath 1) $ gtEqNonVic ++ eqVicOld ++ ltAllVic return (goodVic, badVic) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/AI/PickActionM.hs0000644000000000000000000020136107346545000023150 0ustar0000000000000000-- | AI procedure for picking the best action for an actor. module Game.LambdaHack.Client.AI.PickActionM ( pickAction #ifdef EXPOSE_INTERNAL -- * Internal operations , actionStrategy, waitBlockNow, yellNow , pickup, equipItems, yieldUnneeded, unEquipItems , groupByEqpSlot, bestByEqpSlot, harmful, meleeBlocker, meleeAny , trigger, projectItem, ApplyItemGroup, applyItem, flee , displaceFoe, displaceBlocker, displaceTgt , chase, moveTowards, moveOrRunAid #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Either import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Function import Data.Ratio import Game.LambdaHack.Client.AI.ConditionM import Game.LambdaHack.Client.AI.Strategy import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.BfsM import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Ability import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal -- | Pick the most desirable AI ation for the actor. pickAction :: MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> ActorId -> Bool -> m RequestTimed pickAction foeAssocs friendAssocs aid retry = do side <- getsClient sside body <- getsState $ getActorBody aid let !_A = assert (bfid body == side `blame` "AI tries to move enemy actor" `swith` (aid, bfid body, side)) () let !_A = assert (not (bproj body) `blame` "AI gets to manually move its projectiles" `swith` (aid, bfid body, side)) () stratAction <- actionStrategy foeAssocs friendAssocs (blid body) aid retry let bestAction = bestVariant stratAction !_A = assert (not (nullFreq bestAction) -- equiv to nullStrategy `blame` "no AI action for actor" `swith` (stratAction, aid, body)) () -- Run the AI: chose an action from those given by the AI strategy. rndToAction $ frequency bestAction -- AI strategy based on actor's sight, smell, etc. -- Never empty. actionStrategy :: MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> LevelId -> ActorId -> Bool -> m (Strategy RequestTimed) actionStrategy foeAssocs friendAssocs lid aid retry = do condInMelee <- condInMeleeM lid randomAggressionThreshold <- rndToAction $ randomR0 10 actionStrategyRead condInMelee foeAssocs friendAssocs randomAggressionThreshold lid aid retry -- This is close to being in @MonadClientRead@, but not quite, due to -- random numbers used explicitly in a couple of places (e.g., weapon choice), -- which should be fixed and expressed via @Strategy@ instead, -- and due to recoding the fleeing preference, which should be -- factored out and done in the @actionStrategy@ wrapper. -- -- After the AI code is totally rewritten soon, this should be revisited. actionStrategyRead :: forall m. MonadClient m => Bool -> [(ActorId, Actor)] -> [(ActorId, Actor)] -> Int -> LevelId -> ActorId -> Bool -> m (Strategy RequestTimed) actionStrategyRead condInMelee foeAssocs friendAssocs randomAggressionThreshold lid aid retry = do COps{coTileSpeedup} <- getsState scops mleader <- getsClient sleader body <- getsState $ getActorBody aid let !_A = assert (blid body == lid) () lvl <- getLevel lid localTime <- getsState $ getLocalTime lid condAimEnemyTargeted <- condAimEnemyTargetedM aid condAimEnemyOrStash <- condAimEnemyOrStashM aid condAimEnemyOrRemembered <- condAimEnemyOrRememberedM aid condAimNonEnemyPresent <- condAimNonEnemyPresentM aid condAimCrucial <- condAimCrucialM aid actorMaxSkills <- getsState sactorMaxSkills condAnyFoeAdj <- getsState $ anyFoeAdj aid fact <- getsState $ (EM.! bfid body) . sfactionD condOurAdj <- getsState $ any (\(_, b) -> isFriend (bfid body) fact (bfid b)) . adjacentBigAssocs body oursExploring <- getsState $ oursExploringAssocs (bfid body) condAnyHarmfulFoeAdj <- getsState $ anyHarmfulFoeAdj actorMaxSkills aid threatDistL <- getsState $ meleeThreatDistList foeAssocs aid (fleeL, badVic) <- fleeList foeAssocs aid oldFleeD <- getsClient sfleeD condSupport1 <- condSupport friendAssocs 1 aid condSupport3 <- condSupport friendAssocs 3 aid condSolo <- condAloneM friendAssocs aid -- solo fighters aggresive actorSk <- currentSkillsClient aid condCanProject <- condCanProjectM (getSk SkProject actorSk) aid condAdjTriggerable <- condAdjTriggerableM actorSk aid condBlocksFriends <- condBlocksFriendsM aid condNoEqpWeapon <- condNoEqpWeaponM aid condEnoughGear <- condEnoughGearM aid condFloorWeapon <- condFloorWeaponM aid condDesirableFloorItem <- condDesirableFloorItemM aid condTgtNonmovingEnemy <- condTgtNonmovingEnemyM aid explored <- getsClient sexplored -- This doesn't treat actors guarding stash specially, so on such levels -- man sleeping actors may reside for a long time. Variety, OK. let awakeAndNotGuarding (_, b) = bpos b /= bpos body && bwatch b /= WSleep && Just (lid, bpos b) /= gstash fact anyFriendOnLevelAwake = any awakeAndNotGuarding friendAssocs actorMaxSk = actorMaxSkills EM.! aid recentlyFled = maybe False (\(_, time) -> timeRecent5 localTime time) (aid `EM.lookup` oldFleeD) prefersSleepWhenAwake = case bwatch body of WSleep -> Ability.getSk Ability.SkMoveItem actorMaxSk <= -10 _ -> prefersSleep actorMaxSk -- nm @WWake@ mayFallAsleep = not condAimEnemyOrRemembered && calmFull body actorMaxSk -- only when fully relaxed && mayContinueSleep && canSleep actorSk mayContinueSleep = not condAimEnemyOrStash && not (hpFull body actorSk) && not uneasy && not condAnyFoeAdj && (anyFriendOnLevelAwake -- friend guards the sleeper || prefersSleepWhenAwake) -- or he doesn't care dozes = case bwatch body of WWait n -> n > 0 _ -> False && mayFallAsleep && Just aid /= mleader -- best teammate for a task so stop dozing lidExplored = ES.member lid explored panicFleeL = fleeL ++ badVic condHpTooLow = hpTooLow body actorMaxSk heavilyDistressed = -- actor hit by a proj or similarly distressed deltasSerious (bcalmDelta body) heavilyDistressedThisTurn = -- if far from melee, almost sure hit by proj deltasSeriousThisTurn (bcalmDelta body) condNotCalmEnough = not (calmEnough body actorMaxSk) uneasy = heavilyDistressed || condNotCalmEnough || recentlyFled speed = gearSpeed actorMaxSk speed1_5 = speedScale (3%2) speed -- Max skills used, because we need to know if can melee as leader. condCanMelee = actorCanMelee actorMaxSkills aid body condMeleeBad = not ((condSolo || condSupport1) && condCanMelee) -- These are only melee threats. condThreat n = not $ null $ takeWhile ((<= n) . fst) threatDistL threatAdj = takeWhile ((== 1) . fst) threatDistL condManyThreatsAdj = length threatAdj >= 2 condFastThreatAdj = any (\(_, (aid2, _)) -> let ar2 = actorMaxSkills EM.! aid2 in gearSpeed ar2 > speed1_5) threatAdj condNonStealthyThreatAdj = any (\(_, (aid2, b2)) -> let ar2 = actorMaxSkills EM.! aid2 in Ability.getSk Ability.SkShine ar2 > 0 || isLit (bpos b2)) threatAdj actorShines = Ability.getSk Ability.SkShine actorMaxSk > 0 isLit pos = Tile.isLit coTileSpeedup (lvl `at` pos) -- solid tiles ignored, because not obvious if dark after removed canFleeIntoDark = not $ actorShines || all (isLit . snd) fleeL avoidAmbient = not condInMelee && uneasy && not actorShines mtgtMPath <- getsClient $ EM.lookup aid . stargetD let condGoalIsLit = case mtgtMPath of Just TgtAndPath{tapPath=Just AndPath{pathGoal}} -> isLit pathGoal _ -> False -- Fleeing makes sense, because either actor can't melee, -- or at least won't flee without scoring a hit and return next turn, -- due to threat no longer seen (due to blindness or dark). fleeingMakesSense = not condCanMelee || (Ability.getSk Ability.SkSight actorMaxSk > 2 || Ability.getSk Ability.SkNocto actorMaxSk > 2) && (Ability.getSk Ability.SkShine actorMaxSk > 2 || condNonStealthyThreatAdj || null threatAdj) abInSkill sk = getSk sk actorSk > 0 abInMaxSkill sk = getSk sk actorMaxSk > 0 runSkills = [SkMove, SkDisplace] -- not @SkAlter@, to ground sleepers stratToFreq :: Int -> m (Strategy RequestTimed) -> m (Frequency RequestTimed) stratToFreq scale mstrat = do st <- mstrat return $! if scale == 0 then mzero else scaleFreq scale $ bestVariant st -- Order matters within the list, because it's summed with .| after -- filtering. Also, the results of prefix, distant and suffix -- are summed with .| at the end. prefix, suffix:: [([Skill], m (Strategy RequestTimed), Bool)] prefix = [ ( [SkApply] , applyItem actorSk aid ApplyFirstAid , not condAnyHarmfulFoeAdj && condHpTooLow) , ( [SkAlter] , trigger aid ViaStairs -- explore next or flee via stairs, even if to wrong level; -- in the latter case, may return via different stairs later on , condAdjTriggerable && not condAimEnemyOrStash && ((condNotCalmEnough || condHpTooLow) -- flee && condMeleeBad && condAnyHarmfulFoeAdj || (lidExplored || condEnoughGear) -- explore && not condDesirableFloorItem) ) , ( [SkDisplace] , displaceFoe aid -- only swap with an enemy to expose him -- and only if a friend is blocked by us , condAnyFoeAdj && condBlocksFriends) -- later checks foe eligible , ( [SkMoveItem] , pickup aid True , condNoEqpWeapon -- we assume organ weapons usually inferior && condDesirableFloorItem && condFloorWeapon && not condHpTooLow && abInMaxSkill SkMelee ) , ( [SkAlter] , trigger aid ViaEscape , condAdjTriggerable && not condAimEnemyTargeted && not condDesirableFloorItem ) -- collect the last loot , ( runSkills , flee actorSk aid (not actorShines) fleeL , -- Flee either from melee, if our melee is bad and enemy close, -- or from missiles, if we can hide in the dark in one step. -- Note that we don't know how far ranged threats are or if, -- in fact, they hit from all sides or are hidden. Hence we can't -- do much except hide in darkness or take off light (elsewhere). -- We tend to flee even when over stash (on lit terrain at least), -- but only if we can't fling at enemy (e.g., not visible). -- This is OK, since otherwise we'd be killed for free -- and the stash would be taken just a little later on. -- Note: a part of this condition appears in @actorVulnerable@. not condFastThreatAdj && fleeingMakesSense && if | condAnyHarmfulFoeAdj -> -- Here we don't check @condInMelee@ because regardless -- of whether our team melees (including the fleeing ones), -- endangered actors should flee from very close foes. not condCanMelee || condManyThreatsAdj && not condSupport1 && not condSolo | case gstash fact of Nothing -> False Just (lid2, pos) -> lid2 == lid && chessDist pos (bpos body) <= 2 -> False | condInMelee -> False -- No fleeing when others melee and no critical threat -- (otherwise no target nor action would be possible). | heavilyDistressedThisTurn -- and no adj melee || (heavilyDistressed && not (condThreat 2)) -> not condCanMelee || canFleeIntoDark -- Almost surely hit by projectile. So, if can melee, -- don't escape except into the dark. -- Note that even when in dark now, the projectile hit -- might have been enabled by dynamic light -- or light from lying items, so fleeing still needed. -- If heroes stay in the light when under fire, -- they are pummeled by fast ranged foes and can neither -- flee (too slow) nor force them to flee nor kill them. -- Also AI monsters need predictable behaviour to avoid -- having to chase them forever. Ranged aggravating helps -- and melee-less ranged always fleeing when hit helps -- and makes them evading ambushers, perfect for swarms. | condThreat 2 -- melee enemies near || condThreat 5 && heavilyDistressed -> -- enemies not near but maintain fleeing hysteresis, -- but not if due to lack of support, which changes, -- hence @heavilyDistressed@ and not @recentlyFled@ not condCanMelee -- can't melee, flee || -- No support, not alone, either not aggressive -- or can safely project from afar instead. Flee. not condSupport3 && not condSolo -- Don't flee if can spend time productively, killing -- an enemy that blocks projecting or walking. && not condAnyFoeAdj -- Extra random aggressiveness if can't project -- and didn't flee recently and so undecided. -- This is hacky; the randomness is outside @Strategy@. && (condCanProject || recentlyFled -- still no support, keep fleeing || Ability.getSk Ability.SkAggression actorMaxSk < randomAggressionThreshold) | otherwise -> False ) -- melee threats too far , ( runSkills -- no blockers if can't move right now , meleeBlocker actorSk aid -- only melee blocker , abInSkill SkMelee && (condAnyFoeAdj -- if foes, don't displace, otherwise friends: || not (abInSkill SkDisplace) -- displace friends, if can && condAimEnemyOrStash) ) -- excited -- So that animals block each other until hero comes -- and then the stronger makes a show for him -- and kills the weaker. , ( [SkAlter] , trigger aid ViaNothing , not condInMelee -- don't incur overhead && condAdjTriggerable && not condAimEnemyTargeted ) -- targeting stash is OK, to unblock -- dungeons if party has only one key , ( [SkDisplace] -- prevents some looping movement , displaceBlocker aid retry -- fires up only when path blocked , retry || not condDesirableFloorItem ) , ( [SkMelee] , meleeAny aid , condAnyFoeAdj ) -- won't flee nor displace, so let it melee , ( runSkills , flee actorSk aid -- rattlesnakes and hornets flee and return when charging ((heavilyDistressedThisTurn && not condAnyHarmfulFoeAdj || (heavilyDistressed && not (condThreat 2))) -- prefer bad but dark spots if under fire && not actorShines) panicFleeL -- ultimate panic mode; open tiles, if needed , condAnyHarmfulFoeAdj ) ] -- Order doesn't matter, scaling does. -- These are flattened in @stratToFreq@ (taking only the best variant) -- and then summed, so if any of these can fire, it will. -- If none can, @suffix@ is tried. -- Only the best variant of @chase@ is taken, but it's almost always -- good, and if not, the @chase@ in @suffix@ may fix that. -- The scaling values for @stratToFreq@ need to be so low-resolution -- or we get 32bit @Freqency@ overflows, which would bite us in JS. -- -- Note that all monadic actions here are performed when the strategy -- is being chosen so, e.g., none of them can be @flee@ or the actor -- would be marked in state as fleeing even when the strategy is -- not chosen. distant :: [([Skill], m (Frequency RequestTimed), Bool)] distant = [ ( [SkMoveItem] , stratToFreq (if condInMelee then 20 else 20000) $ yieldUnneeded aid -- 20000 to unequip ASAP, unless is thrown , True ) , ( [SkMoveItem] , stratToFreq 10 $ equipItems aid -- doesn't take long, very useful if safe , not (condInMelee || condDesirableFloorItem || uneasy) ) , ( [SkProject] , stratToFreq (if condTgtNonmovingEnemy then 100 else 30) -- not too common, to leave missiles for pre-melee dance $ projectItem actorSk aid , condAimEnemyTargeted && not condInMelee && condCanProject ) , ( [SkApply] -- common, because animals have that , stratToFreq 10 $ applyItem actorSk aid ApplyAll -- use any potion or scroll , condAimEnemyTargeted || condThreat 9 ) -- can buff against enemies , ( runSkills , stratToFreq (if | condInMelee -> 4000 -- friends pummeled by target, go to help | not condAimEnemyOrStash -> 20 -- if enemy only remembered investigate anyway | not (isLit (bpos body)) -- would need to leave && not actorShines -- dark, most probably && condGoalIsLit -> 1 | otherwise -> 200) $ chase actorSk aid avoidAmbient retry , condCanMelee && Just (lid, bpos body) /= gstash fact && (if condInMelee then condAimEnemyOrStash else (condAimEnemyOrRemembered || condAimNonEnemyPresent) && (not (condThreat 2) || heavilyDistressed -- if under fire, do something! || speed >= speedAdd speedWalk speedWalk -- low risk of getting hit first || not condMeleeBad) -- this results in animals in corridor never attacking -- (unless distressed by, e.g., being hit by missiles), -- because they can't swarm opponent, which is logical, -- and in rooms they do attack, so not too boring; -- two aliens attack always, because more aggressive && not condDesirableFloorItem) ) ] suffix = [ ( [SkMoveItem] , pickup aid False -- e.g., to give to other party members , not condInMelee && condDesirableFloorItem && not dozes ) , ( [SkMoveItem] , unEquipItems aid -- late, because these items not bad , not condInMelee && not dozes ) , ( [SkWait] , waitBlockNow -- try to fall asleep, rarely , bwatch body `notElem` [WSleep, WWake] && mayFallAsleep && prefersSleep actorMaxSk && not condAimCrucial) , ( runSkills , chase actorSk aid avoidAmbient retry , not dozes && if condInMelee then condCanMelee && condAimEnemyOrStash else (not (condThreat 2) || not condMeleeBad) && (Just (lid, bpos body) /= gstash fact || heavilyDistressed -- guard strictly, until harmed || length oursExploring <= 1 || condOurAdj -- or if teammates adjacent || bcalm body < 10) ) -- break loop, avoid domination ] fallback = -- Wait until friends sidestep; ensures strategy never empty. -- Also, this is what non-leader heroes do, unless they melee. [ ( [SkWait] , case bwatch body of WSleep -> yellNow -- we know actor doesn't want to sleep, -- so celebrate wake up with a bang _ -> waitBlockNow -- block, etc. , True ) , ( runSkills -- if can't block, at least change something , chase actorSk aid avoidAmbient True , not condInMelee || condCanMelee && condAimEnemyTargeted ) , ( [SkDisplace] -- if can't brace, at least change something , displaceBlocker aid True , True ) , ( [] , yellNow -- desperate fallback , True ) ] -- Check current, not maximal skills, since this can be a leader as well -- as non-leader action. let checkAction :: ([Skill], m a, Bool) -> Bool checkAction (abts, _, cond) = (null abts || any abInSkill abts) && cond sumS :: [([Skill], m a, Bool)] -> [m a] sumS abAction = let as = filter checkAction abAction in map (\(_, m, _) -> m) as sumF :: [([Skill], m (Frequency RequestTimed), Bool)] -> m (Frequency RequestTimed) sumF abFreq = do let as = filter checkAction abFreq -- This is costly: the monadic side-effects are evaluated. -- If we roll an unevaluated one until we find one that is permitted, -- keeping track of those aready checked might outweigh the savings. -- Even worse, without evaluating per-item frequencies, -- applying an amazing item may be disregarded in favour of throwing -- a mediocre one. strats <- mapM (\(_, m, _) -> m) as return $! msum strats combineWeighted as = liftFrequency <$> sumF as sumPrefix = sumS prefix comDistant = combineWeighted distant sumSuffix = sumS suffix sumFallback = sumS fallback -- TODO: should be: sumPrefix .| comDistant .| sumSuffix .| sumFallback -- but then all side-effects have to be computed beforehand, -- breaking the state, e.g., marking actor as fleeing, always. sums = sumPrefix ++ [comDistant] ++ sumSuffix ++ sumFallback tryStrategies :: [m (Strategy RequestTimed)] -> m (Strategy RequestTimed) tryStrategies [] = return mzero tryStrategies (m : rest) = do str <- m if nullStrategy str then tryStrategies rest else return str -- don't perform the remaining monadic actions if bwatch body == WSleep && abInSkill SkWait && mayContinueSleep -- no check of @canSleep@, because sight lowered by sleeping then return $! returN "sleep" ReqWait else tryStrategies sums waitBlockNow :: MonadClientRead m => m (Strategy RequestTimed) waitBlockNow = return $! returN "wait" ReqWait yellNow :: MonadClientRead m => m (Strategy RequestTimed) yellNow = return $! returN "yell" ReqYell pickup :: MonadClientRead m => ActorId -> Bool -> m (Strategy RequestTimed) pickup aid onlyWeapon = do benItemL <- benGroundItems aid b <- getsState $ getActorBody aid -- This calmE is outdated when one of the items increases max Calm -- (e.g., in pickup, which handles many items at once), but this is OK, -- the server accepts item movement based on calm at the start, not end -- or in the middle. -- The calmE is inaccurate also if an item not IDed, but that's intended -- and the server will ignore and warn (and content may avoid that, -- e.g., making all rings identified) actorMaxSk <- getsState $ getActorMaxSkills aid let calmE = calmEnough b actorMaxSk isWeapon (_, _, _, itemFull, _) = IA.checkFlag Ability.Meleeable $ aspectRecordFull itemFull filterWeapon | onlyWeapon = filter isWeapon | otherwise = id prepareOne (oldN, l4) (Benefit{benInEqp}, _, iid, _, (itemK, _)) = let prep newN toCStore = (newN, (iid, itemK, CGround, toCStore) : l4) n = oldN + itemK in if | benInEqp && calmE && not (eqpOverfull b n) -> prep n CEqp | onlyWeapon -> (oldN, l4) | otherwise -> prep n CStash (_, prepared) = foldl' prepareOne (0, []) $ filterWeapon benItemL return $! if null prepared then reject else returN "pickup" $ ReqMoveItems prepared -- This only concerns items that can be equipped, that is with a slot -- and with @benInEqp@ (which implies @goesIntoEqp@). -- Such items are moved between any stores, as needed. In this case, -- from stash to eqp. equipItems :: MonadClientRead m => ActorId -> m (Strategy RequestTimed) equipItems aid = do body <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid let calmE = calmEnough body actorMaxSk fact <- getsState $ (EM.! bfid body) . sfactionD eqpAssocs <- getsState $ kitAssocs aid [CEqp] stashAssocs <- getsState $ kitAssocs aid [CStash] condShineWouldBetray <- condShineWouldBetrayM aid condAimEnemyOrRemembered <- condAimEnemyOrRememberedM aid discoBenefit <- getsClient sdiscoBenefit localTime <- getsState $ getLocalTime (blid body) fleeD <- getsClient sfleeD -- In general, AI always equips the best item in stash if it's better -- than the best in equipment. Additionally, if there is space left -- in equipment for a future good item, an item from stash may be -- equipped if it's not much worse than in equipment. -- If the item in question is the best item in stash. -- at least one copy must remain in stash. let improve :: (Int, [(ItemId, Int, CStore, CStore)]) -> ( [(Int, (ItemId, ItemFullKit))] , [(Int, (ItemId, ItemFullKit))] ) -> (Int, [(ItemId, Int, CStore, CStore)]) improve (oldN, l4) (bestStash, bestEqp) = let n = 1 + oldN in if eqpOverfull body n then (oldN, l4) else case (bestStash, bestEqp) of ((_, (iidStash, _)) : _, []) -> (n, (iidStash, 1, CStash, CEqp) : l4) ((vStash, (iidStash, _)) : _, (vEqp, _) : _) | vStash > vEqp -> (n, (iidStash, 1, CStash, CEqp) : l4) _ -> case (pluralCopiesOfBest bestStash, bestEqp) of ((vStash, (iidStash, _)) : _, (vEqp, _) : _) | not (eqpOverfull body (n + 1)) -- 9 items in equipment && vStash >= vEqp - 20 && vStash > 20 -> -- within 2 damage of the best and not too bad absolutely (n, (iidStash, 1, CStash, CEqp) : l4) _ -> (oldN, l4) getK (_, (itemK, _)) = itemK pluralCopiesOfBest bestStash@((_, (_, itemFullKit)) : rest) = if getK itemFullKit > 1 then bestStash else rest pluralCopiesOfBest [] = [] heavilyDistressed = -- Actor hit by a projectile or similarly distressed. deltasSerious (bcalmDelta body) recentlyFled = maybe False (\(_, time) -> timeRecent5 localTime time) (aid `EM.lookup` fleeD) uneasy = condAimEnemyOrRemembered || not calmE || heavilyDistressed || recentlyFled canEsc = fcanEscape (gkind fact) -- We filter out unneeded items. In particular, we ignore them in eqp -- when comparing to items we may want to equip, so that the unneeded -- but powerful items don't fool us. -- In any case, the unneeded items should be removed from equip -- in @yieldUnneeded@ earlier or soon after this check. -- In other stores we need to filter, for otherwise we'd have -- a loop of equip/yield. filterNeeded (_, (itemFull, _)) = not (hinders condShineWouldBetray uneasy actorMaxSk itemFull || not canEsc && IA.isHumanTrinket (itemKind itemFull)) -- don't equip items that block progress, e.g., blowtorch bestTwo = bestByEqpSlot discoBenefit (filter filterNeeded stashAssocs) (filter filterNeeded eqpAssocs) bEqpStash = foldl' improve (0, []) bestTwo (_, prepared) = bEqpStash return $! if not calmE || null prepared then reject else returN "equipItems" $ ReqMoveItems prepared yieldUnneeded :: MonadClientRead m => ActorId -> m (Strategy RequestTimed) yieldUnneeded aid = do body <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid let calmE = calmEnough body actorMaxSk eqpAssocs <- getsState $ kitAssocs aid [CEqp] condShineWouldBetray <- condShineWouldBetrayM aid condAimEnemyOrRemembered <- condAimEnemyOrRememberedM aid discoBenefit <- getsClient sdiscoBenefit localTime <- getsState $ getLocalTime (blid body) fleeD <- getsClient sfleeD -- Here and in @unEquipItems@ AI may hide from the human player, -- in shared stash, the Ring of Speed And Bleeding, -- which is a bit harsh, but fair. However any subsequent such -- rings will not be picked up at all, so the human player -- doesn't lose much fun. Additionally, if AI learns alchemy later on, -- they can repair the ring, wield it, drop at death and it's -- in play again. let heavilyDistressed = -- Actor hit by a projectile or similarly distressed. deltasSerious (bcalmDelta body) recentlyFled = maybe False (\(_, time) -> timeRecent5 localTime time) (aid `EM.lookup` fleeD) uneasy = condAimEnemyOrRemembered || not calmE || heavilyDistressed || recentlyFled yieldSingleUnneeded (iidEqp, (itemEqp, (itemK, _))) = [ (iidEqp, itemK, CEqp, CStash) | harmful discoBenefit iidEqp -- harmful not shared || hinders condShineWouldBetray uneasy actorMaxSk itemEqp ] yieldAllUnneeded = concatMap yieldSingleUnneeded eqpAssocs return $! if not calmE || null yieldAllUnneeded then reject else returN "yieldUnneeded" $ ReqMoveItems yieldAllUnneeded -- This only concerns items that @equipItems@ handles, that is -- with a slot and with @benInEqp@ (which implies @goesIntoEqp@). unEquipItems :: MonadClientRead m => ActorId -> m (Strategy RequestTimed) unEquipItems aid = do body <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid let calmE = calmEnough body actorMaxSk eqpAssocs <- getsState $ kitAssocs aid [CEqp] stashAssocs <- getsState $ kitAssocs aid [CStash] condShineWouldBetray <- condShineWouldBetrayM aid condAimEnemyOrRemembered <- condAimEnemyOrRememberedM aid discoBenefit <- getsClient sdiscoBenefit localTime <- getsState $ getLocalTime (blid body) fleeD <- getsClient sfleeD -- In general, AI unequips only if equipment is full and better stash item -- for another slot is likely to come or if the best (or second best) -- item in stash is worse than in equipment and at least one better -- item remains in equipment. let improve :: ( [(Int, (ItemId, ItemFullKit))] , [(Int, (ItemId, ItemFullKit))] ) -> [(ItemId, Int, CStore, CStore)] improve (bestStash, bestEqp) = case bestEqp of ((_, (iidEqp, itemEqp)) : _) | getK itemEqp > 1 && bestStash `worseThanEqp` bestEqp -> -- To share the best items with others, if they care -- and if a better or equal item is not already in stash. -- The effect is that after each party member has a copy, -- a single copy is permanently kept in stash, to quickly -- equip a new-joiner. [(iidEqp, 1, CEqp, CStash)] _ : bestEqp2@((_, (iidEqp, itemEqp)) : _) | getK itemEqp > 1 && bestStash `worseThanEqp` bestEqp2 -> -- To share the second best items with others, if they care -- and if a better or equal item is not already in stash. -- The effect is the same as with the rule above, but only as long -- as the best item is scarce. Then this rule doesn't fire and -- every second best item copy is eventually equipped by someone. [(iidEqp, getK itemEqp, CEqp, CStash)] _ -> case reverse bestEqp of bestEqpR@((vEqp, (iidEqp, itemEqp)) : _) | eqpOverfull body 1 -- 10 items in equipment && (bestStash `betterThanEqp` bestEqpR || getK itemEqp > 1 && vEqp < 20) -> -- To make place in eqp for an item better than any ours. -- Even a minor boost is removed only if stash has a better one. -- Also remove extra copies if the item weak, ih hopes -- of a prompt better pickup. [(iidEqp, 1, CEqp, CStash)] _ -> [] getK (_, (itemK, _)) = itemK worseThanEqp ((vStash, _) : _) ((vEqp, _) : _) = vStash < vEqp worseThanEqp [] _ = True worseThanEqp _ [] = error "unEquipItems: worseThanEqp: []" -- Not @>=@ or we could remove a useful item, without replacing it -- with a better or even equal one. We only remove it so if the item -- is weak and duplicated in equipment. betterThanEqp ((vStash, _) : _) ((vEqp, _) : _) = vStash > vEqp betterThanEqp [] _ = False betterThanEqp _ [] = error "unEquipItems: betterThanEqp: []" heavilyDistressed = -- Actor hit by a projectile or similarly distressed. deltasSerious (bcalmDelta body) recentlyFled = maybe False (\(_, time) -> timeRecent5 localTime time) (aid `EM.lookup` fleeD) uneasy = condAimEnemyOrRemembered || not calmE || heavilyDistressed || recentlyFled -- Here we don't need to filter out items that hinder (except in stash) -- because they are moved to stash and will be equipped by another actor -- at another time, where hindering will be completely different. -- If they hinder and we unequip them, all the better. -- We filter stash to consider only eligible items in @betterThanEqp@. filterNeeded (_, (itemFull, _)) = not $ hinders condShineWouldBetray uneasy actorMaxSk itemFull bestTwo = bestByEqpSlot discoBenefit (filter filterNeeded stashAssocs) eqpAssocs bEqpStash = concatMap improve bestTwo return $! if not calmE || null bEqpStash then reject else returN "unEquipItems" $ ReqMoveItems bEqpStash groupByEqpSlot :: [(ItemId, ItemFullKit)] -> EM.EnumMap EqpSlot [(ItemId, ItemFullKit)] groupByEqpSlot is = let f (iid, itemFullKit) = let arItem = aspectRecordFull $ fst itemFullKit in case IA.aEqpSlot arItem of Nothing -> Nothing Just es -> Just (es, [(iid, itemFullKit)]) withES = mapMaybe f is in EM.fromListWith (++) withES bestByEqpSlot :: DiscoveryBenefit -> [(ItemId, ItemFullKit)] -> [(ItemId, ItemFullKit)] -> [( [(Int, (ItemId, ItemFullKit))] , [(Int, (ItemId, ItemFullKit))] )] bestByEqpSlot discoBenefit eqpAssocs stashAssocs = let eqpMap = EM.map (\g -> (g, [])) $ groupByEqpSlot eqpAssocs stashMap = EM.map (\g -> ([], g)) $ groupByEqpSlot stashAssocs appendTwo (g1, g2) (h1, h2) = (g1 ++ h1, g2 ++ h2) eqpStashMap = EM.unionsWith appendTwo [eqpMap, stashMap] bestSingle = strongestSlot discoBenefit bestTwo eqpSlot (g1, g2) = (bestSingle eqpSlot g1, bestSingle eqpSlot g2) in EM.elems $ EM.mapWithKey bestTwo eqpStashMap harmful :: DiscoveryBenefit -> ItemId -> Bool harmful discoBenefit iid = -- Items that are known, perhaps recently discovered, and it's now revealed -- they should not be kept in equipment, should be unequipped -- (either they are harmful or they waste eqp space). not $ benInEqp $ discoBenefit EM.! iid -- If enemy (or even a friend) blocks the way, sometimes melee him -- even though normally you wouldn't. -- This is also a trick to make a foe use up its non-durable weapons, -- e.g., on cheap slow projectiles fired in its path. meleeBlocker :: MonadClient m => Ability.Skills -> ActorId -> m (Strategy RequestTimed) meleeBlocker actorSk aid = do b <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid fact <- getsState $ (EM.! bfid b) . sfactionD mtgtMPath <- getsClient $ EM.lookup aid . stargetD case mtgtMPath of Just TgtAndPath{ tapTgt=TEnemy{} , tapPath=Just AndPath{pathList=q : _, pathGoal} } | q == pathGoal -> return reject -- not a real blocker, but goal enemy, so defer deciding whether -- to melee him to the code that deals with goal enemies Just TgtAndPath{tapPath=Just AndPath{pathList=q : _, pathGoal}} -> do -- We prefer the goal position, so that we can kill the foe and enter it, -- but we accept any @q@ as well. lvl <- getLevel (blid b) let maim | adjacent (bpos b) pathGoal = Just pathGoal | adjacent (bpos b) q = Just q | otherwise = Nothing -- MeleeDistant lBlocker = case maim of Nothing -> [] Just aim -> posToAidsLvl aim lvl case lBlocker of aid2 : _ -> do body2 <- getsState $ getActorBody aid2 actorMaxSk2 <- getsState $ getActorMaxSkills aid2 -- No problem if there are many projectiles at the spot. We just -- attack the first one. if | bproj body2 -- displacing saves a move, so don't melee && getSk SkDisplace actorSk > 0 -> return reject | isFoe (bfid b) fact (bfid body2) -- at war with us, so hit, not displace || isFriend (bfid b) fact (bfid body2) -- don't start a war && getSk SkDisplace actorSk <= 0 -- can't displace && getSk SkMove actorSk > 0 -- blocked move && 3 * bhp body2 < bhp b -- only get rid of weak friends && gearSpeed actorMaxSk2 <= gearSpeed actorMaxSk -> do mel <- maybeToList <$> pickWeaponClient aid aid2 return $! liftFrequency $ uniformFreq "melee in the way" mel | otherwise -> return reject [] -> return reject _ -> return reject -- probably no path to the enemy, if any -- Everybody melees in a pinch, skills and weapons allowing, -- even though some prefer ranged attacks. However only potentially harmful -- enemies or those having loot or moving (can follow and spy) are meleed -- (or those that are in the way, see elsewhere). -- Projectiles are rather displaced or sidestepped, because it's cheaper -- and also the projectile may be explosive and so harm anyway -- and also if ignored it may hit enemies --- AI can't tell. meleeAny :: MonadClient m => ActorId -> m (Strategy RequestTimed) meleeAny aid = do b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD adjBigAssocs <- getsState $ adjacentBigAssocs b actorMaxSkills <- getsState sactorMaxSkills let foe b2 = isFoe (bfid b) fact (bfid b2) adjFoes = filter (uncurry $ actorWorthKilling actorMaxSkills) $ filter (foe . snd) adjBigAssocs btarget <- getsClient $ getTarget aid mtargets <- case btarget of Just (TEnemy aid2) -> do b2 <- getsState $ getActorBody aid2 return $! if adjacent (bpos b2) (bpos b) && actorWorthKilling actorMaxSkills aid2 b2 then Just [(aid2, b2)] else Nothing _ -> return Nothing let adjTargets = fromMaybe adjFoes mtargets mels <- mapM (pickWeaponClient aid . fst) adjTargets let freq = uniformFreq "melee adjacent" $ catMaybes mels return $! liftFrequency freq -- The level the actor is on is either explored or the actor already -- has a weapon equipped, so no need to explore further, he tries to find -- enemies on other levels, hence triggering terrain. -- We don't verify any embedded item is targeted by the actor, but at least -- the actor doesn't target a visible enemy at this point. -- TODO: In @actionStrategy@ we require minimal @SkAlter@ even for the case -- of triggerable tile underfoot. Let's say this quirk is a specialization -- of AI actors, because there are usually many, so not all need to trigger. trigger :: MonadClientRead m => ActorId -> FleeViaStairsOrEscape -> m (Strategy RequestTimed) trigger aid fleeVia = do b <- getsState $ getActorBody aid lvl <- getLevel (blid b) let f pos = case EM.lookup pos $ lembed lvl of Nothing -> Nothing Just bag -> Just (pos, bag) pbags = mapMaybe f $ bpos b : vicinityUnsafe (bpos b) efeat <- embedBenefit fleeVia aid pbags return $! liftFrequency $ toFreq "trigger" [ (ceiling benefit, ReqAlter pos) | (benefit, (pos, _)) <- efeat , let underFeet = pos == bpos b , underFeet || not (occupiedBigLvl pos lvl) && not (occupiedProjLvl pos lvl) -- AlterBlockActor && EM.notMember pos (lfloor lvl) ] -- AlterBlockItem projectItem :: MonadClientRead m => Ability.Skills -> ActorId -> m (Strategy RequestTimed) projectItem actorSk aid = do btarget <- getsClient $ getTarget aid b <- getsState $ getActorBody aid -- We query target, not path, because path is not needed for flinging. -- Even if unknown tiles exist between us and the target, we assume -- they are walkable and not just transparent and we happily try to shoot. mfpos <- getsState $ aidTgtToPos (Just aid) (blid b) btarget case (btarget, mfpos) of (_, Just fpos) | adjacent (bpos b) fpos -> return reject (Just (TEnemy aidE), Just fpos) -> do actorMaxSkills <- getsState sactorMaxSkills body <- getsState $ getActorBody aidE if actorWorthChasing actorMaxSkills aidE body then do cops <- getsState scops lvl <- getLevel (blid b) seps <- getsClient seps case makeLine False b fpos seps cops lvl of Just newEps -> do let skill = getSk SkProject actorSk -- ProjectAimOnself, ProjectBlockActor, ProjectBlockTerrain -- and no actors or obstacles along the path. benList <- condProjectListM skill aid localTime <- getsState $ getLocalTime (blid b) let fRanged (benR, cstore, iid, itemFull, kit) = -- If the item is discharged, neither the kinetic hit nor -- any effects activate, so no point projecting. -- This changes in time, so recharging is not included -- in @condProjectListM@, but checked here, just before fling. let recharged = hasCharge localTime kit arItem = aspectRecordFull itemFull trange = IA.totalRange arItem $ itemKind itemFull bestRange = chessDist (bpos b) fpos + 2 -- margin for fleeing rangeMult = -- penalize wasted or unsafely low range 10 + max 0 (10 - abs (trange - bestRange)) in if trange >= chessDist (bpos b) fpos && recharged then Just ( - ceiling (benR * intToDouble rangeMult / 10) , ReqProject fpos newEps iid cstore ) else Nothing benRanged = mapMaybe fRanged benList return $! liftFrequency $ toFreq "projectItem" benRanged _ -> return reject else return reject _ -> return reject data ApplyItemGroup = ApplyAll | ApplyFirstAid deriving Eq applyItem :: MonadClientRead m => Ability.Skills -> ActorId -> ApplyItemGroup -> m (Strategy RequestTimed) applyItem actorSk aid applyGroup = do COps{corule} <- getsState scops b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD condShineWouldBetray <- condShineWouldBetrayM aid condAimEnemyOrRemembered <- condAimEnemyOrRememberedM aid localTime <- getsState $ getLocalTime (blid b) let calmE = calmEnough b actorSk heavilyDistressed = -- Actor hit by a projectile or similarly distressed. deltasSerious (bcalmDelta b) uneasy = condAimEnemyOrRemembered || not calmE || heavilyDistressed -- don't take recent fleeing into account when item can be lost skill = getSk SkApply actorSk -- This detects if the value of keeping the item in eqp is in fact < 0. hind = hinders condShineWouldBetray uneasy actorSk canEsc = fcanEscape (gkind fact) permittedActor cstore itemFull kit = fromRight False $ permittedApply corule localTime skill calmE cstore itemFull kit disqualify :: Bool -> IK.Effect -> Bool -- These effects tweak items, which is only situationally beneficial -- and not really the best idea while in combat. disqualify _ IK.PolyItem = True disqualify _ IK.RerollItem = True disqualify _ IK.DupItem = True disqualify _ IK.Identify = True -- This is hard to use and would be wasted recharging stomach. disqualify _ IK.Recharge{} = True -- This is usually the main effect of item and it's useless without Calm. disqualify durable IK.Summon{} = durable && (bcalm b < xM 30 || not calmE) disqualify durable (IK.AtMostOneOf l) = any (disqualify durable) l disqualify durable (IK.OneOf l) = any (disqualify durable) l disqualify durable (IK.OnUser eff) = disqualify durable eff disqualify durable (IK.AndEffect eff1 eff2) = disqualify durable eff1 || disqualify durable eff2 disqualify durable (IK.OrEffect eff1 eff2) = disqualify durable eff1 || disqualify durable eff2 disqualify durable (IK.SeqEffect effs) = any (disqualify durable) effs disqualify durable (IK.When _ eff) = disqualify durable eff disqualify durable (IK.Unless _ eff) = disqualify durable eff disqualify durable (IK.IfThenElse _ eff1 eff2) = disqualify durable eff1 || disqualify durable eff2 disqualify _ _ = False q (Benefit{benInEqp}, cstore, _, itemFull@ItemFull{itemKind}, kit) = let arItem = aspectRecordFull itemFull durable = IA.checkFlag Durable arItem in (not benInEqp -- can't wear, so OK to break || durable -- can wear, but can't break, even better || not (IA.checkFlag Ability.Meleeable arItem) -- anything else expendable && hind itemFull) -- hinders now, so possibly often, so away! && permittedActor (Just cstore) itemFull kit && not (any (disqualify durable) $ IK.ieffects itemKind) && (canEsc || not (IA.isHumanTrinket itemKind)) -- A hack to prevent monsters from using up treasure -- meant for heroes. stores = [CStash, CGround, COrgan] ++ [CEqp | calmE] discoBenefit <- getsClient sdiscoBenefit benList <- getsState $ benAvailableItems discoBenefit aid stores getKind <- getsState $ flip getIidKind let (myBadGrps, myGoodGrps) = partitionEithers $ mapMaybe (\iid -> let itemKind = getKind iid in if maybe False (> 0) $ lookup IK.CONDITION $ IK.ifreq itemKind then Just $ if benInEqp (discoBenefit EM.! iid) then Right $ DefsInternal.GroupName $ IK.iname itemKind -- conveniently, @iname@ matches @ifreq@ else Left $ DefsInternal.GroupName $ IK.iname itemKind else Nothing) (EM.keys $ borgan b) fTool benAv@( Benefit{benApply}, cstore, iid , itemFull@ItemFull{itemKind}, _ ) = let dropsGrps = IK.getDropOrgans itemKind -- @Impress@ effect included dropsBadOrgans = not (null myBadGrps) && (IK.CONDITION `elem` dropsGrps || not (null (dropsGrps `intersect` myBadGrps))) dropsImpressed = IK.S_IMPRESSED `elem` myBadGrps && (IK.CONDITION `elem` dropsGrps || IK.S_IMPRESSED `elem` dropsGrps) dropsGoodOrgans = not (null myGoodGrps) && (IK.CONDITION `elem` dropsGrps || not (null (dropsGrps `intersect` myGoodGrps))) wastesDrop = not dropsBadOrgans && not (null dropsGrps) -- Don't include @Ascend@ nor @Teleport@, because maybe no foe near. -- Don't include @AtMostOneOf@ nor @OneOf@ because -- other effects may kill you. getHP (IK.RefillHP p) = max 0 p getHP (IK.OnUser eff) = getHP eff getHP (IK.AndEffect eff1 eff2) = getHP eff1 + getHP eff2 getHP (IK.OrEffect eff1 _) = getHP eff1 getHP (IK.SeqEffect effs) = sum $ map getHP effs getHP (IK.When _ eff) = getHP eff getHP (IK.Unless _ eff) = getHP eff getHP (IK.IfThenElse _ eff1 eff2) = getHP eff1 + getHP eff2 getHP _ = 0 healPower = sum $ map getHP $ IK.ieffects itemKind wastesHP = xM healPower > xM (Ability.getSk Ability.SkMaxHP actorSk) - bhp b durable = IA.checkFlag Durable $ aspectRecordFull itemFull situationalBenApply = if | dropsBadOrgans -> if dropsImpressed then benApply + 1000 -- crucial else benApply + 20 | wastesDrop || wastesHP -> benApply - 10 | otherwise -> benApply coeff CGround = 2 -- pickup turn saved coeff COrgan = if durable then 1 else 1000 -- if not durable, must hinder currently or be very potent coeff CEqp = if durable then 1 else 1000 coeff CStash = 1 benR = ceiling situationalBenApply * coeff cstore canApply = situationalBenApply > 0 && (dropsImpressed || not wastesHP) -- waste healing only if it drops impressed; -- otherwise apply anything beneficial at will && case applyGroup of ApplyFirstAid -> q benAv && (healPower > 0 || dropsImpressed) -- when low HP, Calm easy to deplete, so impressed crucial ApplyAll -> q benAv && not dropsGoodOrgans -- not an emergency, so don't sacrifice own good conditions in if canApply then Just (benR, ReqApply iid cstore) else Nothing benTool = mapMaybe fTool benList return $! liftFrequency $ toFreq "applyItem" benTool -- If low on health or alone, flee in panic, close to the path to target -- and as far from the attackers, as possible. Usually fleeing from -- foes will lead towards friends, but we don't insist on that. flee :: MonadClient m => Ability.Skills -> ActorId -> Bool -> [(Int, Point)] -> m (Strategy RequestTimed) flee actorSk aid avoidAmbient fleeL = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody aid localTime <- getsState $ getLocalTime (blid b) fleeD <- getsClient sfleeD let recentlyFled = maybe False (\(_, time) -> timeRecent5 localTime time) (aid `EM.lookup` fleeD) -- Regardless if fleeing accomplished, mark the need, but don't forget -- the location of initial danger, in case enemies not seen any more, -- if not too old. unless recentlyFled $ modifyClient $ \cli -> cli {sfleeD = EM.insert aid (bpos b, localTime) (sfleeD cli)} lvl <- getLevel $ blid b let isAmbient pos = Tile.isLit coTileSpeedup (lvl `at` pos) && Tile.isWalkable coTileSpeedup (lvl `at` pos) -- if solid, will be altered and perhaps darkened fleeAmbientAvoided = filter (not . isAmbient . snd) fleeL fleeAmbient = if avoidAmbient && not (null fleeAmbientAvoided) then fleeAmbientAvoided else fleeL let vVic = map (second (`vectorToFrom` bpos b)) fleeAmbient str = liftFrequency $ toFreq "flee" vVic mapStrategyM (moveOrRunAid actorSk aid) str -- The result of all these conditions is that AI displaces rarely, -- but it can't be helped as long as the enemy is smart enough to form fronts. displaceFoe :: MonadClientRead m => ActorId -> m (Strategy RequestTimed) displaceFoe aid = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel $ blid b fact <- getsState $ (EM.! bfid b) . sfactionD friends <- getsState $ friendRegularList (bfid b) (blid b) adjBigAssocs <- getsState $ adjacentBigAssocs b let foe (_, b2) = isFoe (bfid b) fact (bfid b2) adjFoes = filter foe adjBigAssocs walkable p = -- DisplaceAccess Tile.isWalkable coTileSpeedup (lvl `at` p) nFriends body = length $ filter (adjacent (bpos body) . bpos) friends nFrNew = nFriends b + 1 qualifyActor (aid2, b2) = do case posToAidsLvl (bpos b2) lvl of _ | not (walkable (bpos b2)) -- DisplaceAccess || boldpos b == Just (bpos b2) && boldpos b2 == Just (bpos b) -> -- avoid short loops return Nothing [_] -> do actorMaxSk <- getsState $ getActorMaxSkills aid2 dEnemy <- getsState $ dispEnemy aid aid2 actorMaxSk -- DisplaceDying, DisplaceBraced, DisplaceImmobile, -- DisplaceSupported let nFrOld = nFriends b2 return $! if dEnemy && nFrOld < nFrNew then Just ( (nFrNew - nFrOld) ^ (2 :: Int) , ReqDisplace aid2 ) else Nothing _ -> return Nothing -- DisplaceProjectiles foes <- mapM qualifyActor adjFoes return $! liftFrequency $ toFreq "displaceFoe" $ catMaybes foes displaceBlocker :: MonadClientRead m => ActorId -> Bool -> m (Strategy RequestTimed) displaceBlocker aid retry = do b <- getsState $ getActorBody aid mtgtMPath <- getsClient $ EM.lookup aid . stargetD case mtgtMPath of Just TgtAndPath{ tapTgt=TEnemy{} , tapPath=Just AndPath{pathList=q : _, pathGoal} } | q == pathGoal -- not a real blocker but goal; only try to displace -- if desperate (that is, already tried to melee it) && not retry -> return reject Just TgtAndPath{tapPath=Just AndPath{pathList=q : _}} | adjacent (bpos b) q -> -- not veered off target too much displaceTgt aid q retry _ -> return reject -- goal reached displaceTgt :: MonadClientRead m => ActorId -> Point -> Bool -> m (Strategy RequestTimed) displaceTgt source tpos retry = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody source actorMaxSkills <- getsState sactorMaxSkills let !_A = assert (adjacent (bpos b) tpos) () lvl <- getLevel $ blid b let walkable p = -- DisplaceAccess Tile.isWalkable coTileSpeedup (lvl `at` p) case posToAidsLvl tpos lvl of _ | not (walkable tpos) -> return reject -- DisplaceAccess [aid2] -> do b2 <- getsState $ getActorBody aid2 mleader <- getsClient sleader if | bwatch b2 `elem` [WSleep, WWake] -> return $! returN "displace sleeping" $ ReqDisplace aid2 | Just aid2 == mleader -> return reject | boldpos b == Just tpos && boldpos b2 == Just (bpos b) -> return reject -- avoid short loops | otherwise -> do tfact <- getsState $ (EM.! bfid b2) . sfactionD mtgtMPath <- getsClient $ EM.lookup aid2 . stargetD enemyTgt <- condAimEnemyOrRememberedM source enemyTgt2 <- condAimEnemyOrRememberedM aid2 case mtgtMPath of -- I can see targets of only own team, so no check of @bfid@. Just TgtAndPath{tapPath=Just AndPath{pathList=q : _}} | q == bpos b -> -- teammate wants to swap return $! returN "displace mutual" $ ReqDisplace aid2 Just _ -> return $! -- Teammate, possibly without path, for whatever reason. if retry -- me desperate || Just (blid b2, bpos b2) == gstash tfact -- guarding; lazy || getSk SkDisplace (actorMaxSkills EM.! aid2) <= 0 -- can't displace back || enemyTgt && not enemyTgt2 -- he doesn't have Enemy target and I have, so push him -- aside, because, for heroes, he will never be a leader, -- so he can't step aside himself then returN "displace teammate" $ ReqDisplace aid2 else reject _ -> do -- an enemy or ally or disoriented teammate actorMaxSk <- getsState $ getActorMaxSkills aid2 dEnemy <- getsState $ dispEnemy source aid2 actorMaxSk -- DisplaceDying, DisplaceBraced, DisplaceImmobile, -- DisplaceSupported return $! if bfid b == bfid b2 -- disoriented teammate; doesn't care || isFoe (bfid b2) tfact (bfid b) && dEnemy -- foe || retry -- ally, I need to be desperate, as above then returN "displace other" $ ReqDisplace aid2 else reject _ -> return reject -- DisplaceProjectiles and no blocker at all chase :: MonadClientRead m => Ability.Skills -> ActorId -> Bool -> Bool -> m (Strategy RequestTimed) chase actorSk aid avoidAmbient retry = do body <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid body) . sfactionD mtgtMPath <- getsClient $ EM.lookup aid . stargetD let -- With no leader, the goal is vague, so permit arbitrary detours. relaxed = not $ fhasPointman (gkind fact) strAmbient avoid = case mtgtMPath of Just TgtAndPath{tapPath=Just AndPath{pathList=q : _, ..}} -> if pathGoal == bpos body then return reject -- done; picking up items, etc. else moveTowards actorSk aid avoid q pathGoal (relaxed || retry) _ -> return reject -- goal reached or banned ambient lit tile strAvoided <- strAmbient avoidAmbient str <- if avoidAmbient && nullStrategy strAvoided then strAmbient False else return strAvoided mapStrategyM (moveOrRunAid actorSk aid) str moveTowards :: MonadClientRead m => Ability.Skills -> ActorId -> Bool -> Point -> Point -> Bool -> m (Strategy Vector) moveTowards actorSk aid avoidAmbient target goal relaxed = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel $ blid b let source = bpos b alterSkill = getSk SkAlter actorSk !_A = assert (adjacent source target `blame` (source, target, aid, b, goal)) () fact <- getsState $ (EM.! bfid b) . sfactionD salter <- getsClient salter noFriends <- getsState $ \s p -> all (isFoe (bfid b) fact . bfid . snd) (posToAidAssocs p (blid b) s) -- don't kill own projectiles let lalter = salter EM.! blid b isAmbient pos = Tile.isLit coTileSpeedup (lvl `at` pos) && Tile.isWalkable coTileSpeedup (lvl `at` pos) -- if solid, will be altered and perhaps darkened -- Only actors with SkAlter can search for hidden doors, etc. enterableHere p = alterSkill >= fromEnum (lalter PointArray.! p) permittedHere p | avoidAmbient = enterableHere p && not (isAmbient p) | otherwise = enterableHere p -- If target is the final goal, is not occupied and is lit, permit -- movement into lit position, regardless. if noFriends target && (target == goal && enterableHere target || permittedHere target) then return $! returN "moveTowards target" $ target `vectorToFrom` source else do -- This lets animals mill around, even when blocked, -- because they have nothing to lose (unless other animals melee). -- Blocked heroes instead don't become leaders and don't move -- until friends sidestep to let them reach their goal. let goesBack p = Just p == boldpos b nonincreasing p = chessDist source goal >= chessDist p goal isSensible | relaxed = \p -> noFriends p && permittedHere p | otherwise = \p -> nonincreasing p && not (goesBack p) && noFriends p && permittedHere p sensible = [ ((goesBack p, chessDist p goal), v) | v <- moves , let p = source `shift` v , isSensible p ] -- @SortOn@ less efficient here, because function cheap. sorted = sortBy (comparing fst) sensible groups = map (map snd) $ groupBy ((==) `on` fst) sorted freqs = map (liftFrequency . uniformFreq "moveTowards") groups return $! foldr (.|) reject freqs -- Actor moves or searches or alters or attacks. -- This function is very general, even though it's often used in contexts -- when only one or two of the many cases can possibly occur. moveOrRunAid :: MonadClientRead m => Ability.Skills -> ActorId -> Vector -> m (Maybe RequestTimed) moveOrRunAid actorSk source dir = do COps{coTileSpeedup} <- getsState scops sb <- getsState $ getActorBody source let lid = blid sb lvl <- getLevel lid let walkable = -- DisplaceAccess Tile.isWalkable coTileSpeedup (lvl `at` tpos) notLooping body p = -- avoid displace loops boldpos body /= Just p || actorWaits body spos = bpos sb -- source position tpos = spos `shift` dir -- target position t = lvl `at` tpos -- We start by checking actors at the target position, -- which gives a partial information (actors can be invisible), -- as opposed to accessibility (and items) which are always accurate -- (tiles can't be invisible). case posToAidsLvl tpos lvl of [target] | walkable && getSk SkDisplace actorSk > 0 && notLooping sb tpos -> do -- @target@ can be a foe, as well as a friend. tb <- getsState $ getActorBody target tfact <- getsState $ (EM.! bfid tb) . sfactionD actorMaxSk <- getsState $ getActorMaxSkills target dEnemy <- getsState $ dispEnemy source target actorMaxSk -- DisplaceDying, DisplaceBraced, DisplaceImmobile, DisplaceSupported if isFoe (bfid tb) tfact (bfid sb) && not dEnemy then return Nothing else return $ Just $ ReqDisplace target [] | walkable && getSk SkMove actorSk > 0 -> -- Movement requires full access. The potential invisible actor is hit. return $ Just $ ReqMove dir [] | not walkable && getSk SkAlter actorSk >= Tile.alterMinWalk coTileSpeedup t -- AlterUnwalked -- Only possible if items allowed inside unwalkable tiles: && EM.notMember tpos (lfloor lvl) -> -- AlterBlockItem -- Not walkable, but alter skill suffices, so search or alter the tile. -- We assume that unalterable unwalkable tiles are protected by high -- skill req. We don't alter walkable tiles (e.g., to close doors). return $ Just $ ReqAlter tpos _ -> return Nothing -- can't displace, move nor alter LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/AI/PickActorM.hs0000644000000000000000000005330007346545000023001 0ustar0000000000000000-- | Picking the AI actor to move and refreshing leader and non-leader targets. module Game.LambdaHack.Client.AI.PickActorM ( pickActorToMove, setTargetFromDoctrines ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Data.Ratio import Game.LambdaHack.Client.AI.ConditionM import Game.LambdaHack.Client.AI.PickTargetM import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.BfsM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.FactionKind (fskillsOther) import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability -- | Pick a new leader from among the actors on the current level. -- Refresh the target of the new leader, even if unchanged. pickActorToMove :: MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> Maybe ActorId -> m ActorId pickActorToMove foeAssocs friendAssocs maidToAvoid = do COps{coTileSpeedup} <- getsState scops actorMaxSkills <- getsState sactorMaxSkills mleader <- getsClient sleader let oldAid = fromMaybe (error $ "" `showFailure` maidToAvoid) mleader oldBody <- getsState $ getActorBody oldAid let side = bfid oldBody arena = blid oldBody lvl <- getLevel arena localTime <- getsState $ getLocalTime arena condInMelee <- condInMeleeM arena fact <- getsState $ (EM.! side) . sfactionD -- Find our actors on the current level only. ours <- getsState $ fidActorRegularAssocs side arena let pickOld = do void $ refreshTarget foeAssocs friendAssocs (oldAid, oldBody) return oldAid oursNotSleeping = filter (\(_, b) -> bwatch b /= WSleep) ours -- Faction discourages client leader change on level, because -- non-leader actors have the same skills as leader, so no point. -- Server is guaranteed to switch leader within a level occasionally, -- e.g., when the old leader dies, so this works fine. discouragedPointmanSwitchOnLevel = fskillsOther (gkind fact) == Ability.zeroSkills case oursNotSleeping of _ | -- Keep the leader: client is discouraged from leader switching, -- so it will only be changed if pointman waits (maidToAvoid) -- to avoid wasting his higher mobility. -- This is OK for monsters even if in melee, because both having -- a meleeing actor a leader (and higher DPS) and rescuing actor -- a leader (and so faster to get in melee range) is good. -- And we are guaranteed that only the two classes of actors are -- not waiting, with some exceptions (urgent unequip, flee via starts, -- melee-less trying to flee, first aid, etc.). discouragedPointmanSwitchOnLevel && isNothing maidToAvoid -> pickOld [] -> pickOld [(aidNotSleeping, bNotSleeping)] -> do -- Target of asleep actors won't change unless foe adjacent, -- which is caught without recourse to targeting. void $ refreshTarget foeAssocs friendAssocs (aidNotSleeping, bNotSleeping) return aidNotSleeping _ -> do -- At this point we almost forget who the old leader was -- and treat all party actors the same, eliminating candidates -- until we can't distinguish them any more, at which point we slightly -- prefer the old leader, if he is among the best candidates -- (to make the AI appear more human-like and easier to observe). let refresh aidBody = do mtgt <- refreshTarget foeAssocs friendAssocs aidBody return (aidBody, mtgt) oursTgtRaw <- mapM refresh oursNotSleeping oldFleeD <- getsClient sfleeD let recentlyFled aid = maybe False (\(_, time) -> timeRecent5 localTime time) (aid `EM.lookup` oldFleeD) goodGeneric (_, Nothing) = Nothing goodGeneric (_, Just TgtAndPath{tapPath=Nothing}) = Nothing -- this case means melee-less heroes adjacent to foes, etc. -- will never flee if melee is happening; but this is rare; -- this also ensures even if a lone actor melees and nobody -- can come to rescue, he will become and remain the leader, -- because otherwise an explorer would need to become a leader -- and fighter will be 1 clip slower for the whole fight, -- just for a few turns of exploration in return; -- -- also note that when the fighter then becomes a leader -- he may gain quite a lot of time via @swapTime@, -- and so be able to get a double blow on opponents -- or a safe blow and a withdraw (but only once); this is a mild -- exploit that encourages ambush camping (with a non-leader), -- but it's also a rather fun exploit and a straightforward -- consequence of the game mechanics, so it's OK for now goodGeneric ((aid, b), Just tgt) = case maidToAvoid of _ | aid == oldAid && actorWaits b -> Nothing -- Not the old leader that was stuck last turn -- because he is likely to be still stuck. Nothing -> Just ((aid, b), tgt) Just aidToAvoid -> if aid == aidToAvoid then Nothing -- not an attempted leader stuck this turn else Just ((aid, b), tgt) oursTgt = mapMaybe goodGeneric oursTgtRaw -- This should be kept in sync with @actionStrategy@, -- because it's a part of the condition for @flee@ in @PickActionM@. -- Comments are in the full copy. actorVulnerable ((aid, body), _) = do let actorMaxSk = actorMaxSkills EM.! aid condAnyHarmfulFoeAdj <- getsState $ anyHarmfulFoeAdj actorMaxSkills aid threatDistL <- getsState $ meleeThreatDistList foeAssocs aid (fleeL, _) <- fleeList foeAssocs aid condSupport1 <- condSupport friendAssocs 1 aid condSolo <- condAloneM friendAssocs aid let condCanFlee = not (null fleeL) heavilyDistressed = deltasSerious (bcalmDelta body) speed1_5 = speedScale (3%2) (gearSpeed actorMaxSk) condCanMelee = actorCanMelee actorMaxSkills aid body threatAdj = takeWhile ((== 1) . fst) threatDistL condManyThreatAdj = length threatAdj >= 2 condFastThreatAdj = any (\(_, (aid2, _)) -> let ar2 = actorMaxSkills EM.! aid2 in gearSpeed ar2 > speed1_5) threatAdj condNonStealthyThreatAdj = any (\(_, (aid2, b2)) -> let ar2 = actorMaxSkills EM.! aid2 in Ability.getSk Ability.SkShine ar2 > 0 || isLit (bpos b2)) threatAdj isLit pos = Tile.isLit coTileSpeedup (lvl `at` pos) fleeingMakesSense = not condCanMelee || (Ability.getSk Ability.SkSight actorMaxSk > 2 || Ability.getSk Ability.SkNocto actorMaxSk > 2) && (Ability.getSk Ability.SkShine actorMaxSk > 2 || condNonStealthyThreatAdj || null threatAdj) return $! not condFastThreatAdj && fleeingMakesSense && if | condAnyHarmfulFoeAdj -> not condCanMelee || condManyThreatAdj && not condSupport1 && not condSolo | condInMelee -> False | heavilyDistressed -> True -- Different from @PickActionM@: -- If under fire, do something quickly, always, -- because the actor clearly vulnerable, -- but don't make a leader only because threats close. | otherwise -> False && condCanFlee actorFled ((aid, _), _) = recentlyFled aid actorHearning (_, TgtAndPath{ tapTgt=TPoint TEnemyPos{} _ _ , tapPath=Nothing }) = return False actorHearning (_, TgtAndPath{ tapTgt=TPoint TEnemyPos{} _ _ , tapPath=Just AndPath{pathLen} }) | pathLen <= 2 = return False -- noise probably due to fleeing target actorHearning ((_aid, b), _) = do let closeFoes = filter ((<= 3) . chessDist (bpos b) . bpos . snd) foeAssocs actorHears = deltasHears (bcalmDelta b) return $! actorHears -- e.g., actor hears an enemy && null closeFoes -- the enemy not visible; a trap! -- AI has to be prudent and not lightly waste leader for meleeing. actorMeleeing ((aid, _), _) = getsState $ anyHarmfulFoeAdj actorMaxSkills aid (oursVulnerable, oursSafe) <- partitionM actorVulnerable oursTgt let (oursFled, oursNotFled) = partition actorFled oursSafe (oursMeleeingRaw, oursNotMeleeingRaw) <- partitionM actorMeleeing oursNotFled let actorMeleeingCanDisplace ( (aid, sb) , TgtAndPath{tapTgt=TEnemy target} ) = do tb <- getsState $ getActorBody target let actorMaxSk = actorMaxSkills EM.! target dEnemy <- getsState $ dispEnemy aid target actorMaxSk -- Some usual conditions ignored, because transient or rare. return $! checkAdjacent sb tb && dEnemy actorMeleeingCanDisplace _ = return False (oursMeleeingCanDisplace, oursMeleeing) <- partitionM actorMeleeingCanDisplace oursMeleeingRaw let adjStash ( (_, b) , TgtAndPath{tapTgt=TPoint TStash{} lid pos} ) = lid == arena && adjacent pos (bpos b) && isNothing (posToBigLvl pos lvl) adjStash _ = False (oursAdjStash, oursNotMeleeing) = partition adjStash oursNotMeleeingRaw (oursHearing, oursNotHearing) <- partitionM actorHearning oursNotMeleeing let actorRanged ((aid, body), _) = not $ actorCanMelee actorMaxSkills aid body targetTEnemy (_, TgtAndPath{tapTgt=TEnemy _}) = True targetTEnemy (_, TgtAndPath{tapTgt=TPoint TEnemyPos{} lid _}) = lid == arena targetTEnemy ((_, b), TgtAndPath{tapTgt=TPoint TStash{} lid pos}) = lid == arena && pos /= bpos b -- stashes as crucial as enemies. except when guarding them targetTEnemy _ = False actorNoSupport ((aid, _), _) = do threatDistL <- getsState $ meleeThreatDistList foeAssocs aid condSupport2 <- condSupport friendAssocs 2 aid let condThreat n = not $ null $ takeWhile ((<= n) . fst) threatDistL -- If foes far, friends may still come, so we let him move. -- The net effect is that lone heroes close to foes freeze -- until support comes. return $! condThreat 5 && not condSupport2 (oursRanged, oursNotRanged) = partition actorRanged oursNotHearing (oursTEnemyAll, oursOther) = partition targetTEnemy oursNotRanged notSwapReady ((_, b), TgtAndPath{tapTgt=TPoint TStash{} lid pos}) _ = lid == arena && pos == bpos b -- not ready to follow goal if already guarding the stash notSwapReady abt@((_, b), _) (ab2, Just t2@TgtAndPath{tapPath= Just AndPath{pathList=q : _}}) = let source = bpos b tenemy = targetTEnemy abt tenemy2 = targetTEnemy (ab2, t2) -- Copied from 'displaceTowards': in not (q == source -- friend wants to swap || tenemy && not tenemy2) notSwapReady _ _ = True -- These are not necessarily stuck (perhaps can go around), -- but their current path is blocked by friends. -- As soon as friends move, path is recalcuated and they may -- become unstuck. targetBlocked abt@((aid, _), TgtAndPath{tapPath}) = case tapPath of Just AndPath{pathList= q : _} -> any (\abt2@((aid2, body2), _) -> aid2 /= aid -- in case pushed on goal && bpos body2 == q && notSwapReady abt abt2) oursTgtRaw _ -> False (oursTEnemyBlocked, oursTEnemy) = partition targetBlocked oursTEnemyAll (oursNoSupportRaw, oursSupportRaw) <- if length oursTEnemy <= 2 then return ([], oursTEnemy) else partitionM actorNoSupport oursTEnemy let (oursNoSupport, oursSupport) = if length oursSupportRaw <= 1 -- make sure picks random enough then ([], oursTEnemy) else (oursNoSupportRaw, oursSupportRaw) (oursBlocked, oursPos) = partition targetBlocked $ oursRanged ++ oursOther guarding ((_, b), Just TgtAndPath{tapTgt=TPoint TStash{} lid pos}) = lid == arena && pos == bpos b guarding _ = False -- Don't try to include a stash guard in formation, even if attacking -- or being attacked. Attackers would be targetted anyway. oursNotSleepingNorGuarding = filter (not . guarding) oursTgtRaw -- Lower overhead is better. overheadOurs :: ((ActorId, Actor), TgtAndPath) -> Int overheadOurs (_, TgtAndPath{tapPath=Nothing}) = 100 overheadOurs ((_, b), TgtAndPath{tapTgt=TPoint TStash{} lid pos}) | lid == arena && pos == bpos b = 200 -- guarding, poor choice overheadOurs abt@( (aid, b) , TgtAndPath{tapPath=Just AndPath{pathLen=d, ..}} ) = -- Keep proper formation. Too dense and exploration takes -- too long; too sparse and actors fight alone. -- Note that right now, while we set targets separately for each -- hero, perhaps on opposite borders of the map, -- we can't help that sometimes heroes are separated. let maxSpread = 3 + length oursNotSleepingNorGuarding lDist p = [ chessDist (bpos b2) p | ((aid2, b2), _) <- oursNotSleepingNorGuarding , aid2 /= aid ] pDist p = let ld = lDist p in if null ld then 0 else minimum ld aidDist = pDist (bpos b) -- Negative, if the goal gets us closer to the party. diffDist = pDist pathGoal - aidDist -- If actor already at goal or equidistant, count it as closer. sign = if diffDist <= 0 then -1 else 1 formationValue = sign * (abs diffDist `max` maxSpread) * (aidDist `max` maxSpread) ^ (2 :: Int) targetsEnemy = targetTEnemy abt fightValue = if targetsEnemy then - fromEnum (bhp b `div` (10 * oneM)) else 0 isLit pos = Tile.isLit coTileSpeedup (lvl `at` pos) -- solid tiles ignored, because not obvious if dark -- after removed actorMaxSk = actorMaxSkills EM.! aid actorShines = Ability.getSk Ability.SkShine actorMaxSk > 0 stepsIntoLight = not actorShines && not (isLit $ bpos b) && case pathList of [] -> False q : _ -> isLit q -- shortest path is through light even though may -- sidestep through dark in @chase@ or @flee@ in formationValue `div` 3 + fightValue + (case d of 0 -> -400 -- do your thing ASAP and retarget 1 | not targetsEnemy -> -200 -- prevent others from trying to occupy the tile; -- TStash that obscures a foe correctly handled here _ -> if d < 8 then d `div` 4 else 2 + d `div` 10) + (if aid == oldAid then 0 else 10) + (if stepsIntoLight then 30 else 0) -- Overheads above @maxBoundInt32@ are unlikely (and unsuppored in JS) -- and also capping the value does not distort the choice too much. positiveOverhead abt = min maxBoundInt32 $ max 1 $ 200 - overheadOurs abt candidates = [ oursAdjStash , oursVulnerable , oursSupport , oursNoSupport , oursPos , oursFled -- if just fled, but not vulnerable, -- keep him passive and safe, out of action , oursMeleeingCanDisplace -- prefer melee actors displacing than blocked -- actors trying to walk around them , oursTEnemyBlocked -- prefer blocked actors trying to walk around -- even if that causes overhead for the meleeing , oursMeleeing , oursHearing , oursBlocked ] case filter (not . null) candidates of l : _ -> do let freq = toFreq "candidates for AI leader" $ map (positiveOverhead &&& id) l ((aid, b), _) <- rndToAction $ frequency freq s <- getState modifyClient $ updateLeader aid s -- When you become a leader, stop following old leader, but follow -- his target, if still valid, to avoid distraction. when (gdoctrine fact `elem` [Ability.TFollow, Ability.TFollowNoItems] && not condInMelee) $ void $ refreshTarget foeAssocs friendAssocs (aid, b) return aid _ -> return oldAid -- | Inspect the doctrines of the actor and set his target according to it. setTargetFromDoctrines :: MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> ActorId -> m () setTargetFromDoctrines foeAssocs friendAssocs oldAid = do mleader <- getsClient sleader let !_A = assert (mleader /= Just oldAid) () oldBody <- getsState $ getActorBody oldAid moldTgt <- getsClient $ EM.lookup oldAid . stargetD let side = bfid oldBody arena = blid oldBody fact <- getsState $ (EM.! side) . sfactionD let explore = void $ refreshTarget foeAssocs friendAssocs (oldAid, oldBody) setPath mtgt = case (mtgt, moldTgt) of (Nothing, _) -> return False ( Just TgtAndPath{tapTgt=leaderTapTgt}, Just TgtAndPath{tapTgt=oldTapTgt,tapPath=Just oldTapPath} ) | leaderTapTgt == oldTapTgt -- targets agree && bpos oldBody == pathSource oldTapPath -> do -- nominal path void $ refreshTarget foeAssocs friendAssocs (oldAid, oldBody) return True -- already on target (Just TgtAndPath{tapTgt=leaderTapTgt}, _) -> do tap <- createPath oldAid leaderTapTgt case tap of TgtAndPath{tapPath=Nothing} -> return False _ -> do modifyClient $ \cli -> cli {stargetD = EM.insert oldAid tap (stargetD cli)} return True follow = case mleader of -- If no leader at all (forced @TFollow@ doctrine on an actor -- from a leaderless faction), fall back to @TExplore@. Nothing -> explore _ | bwatch oldBody == WSleep -> -- We could check skills, but it would be more complex. explore Just leader -> do onLevel <- getsState $ memActor leader arena condInMelee <- condInMeleeM arena -- If leader not on this level or if we are meleeing, -- and so following is not important, fall back to @TExplore@. if not onLevel || condInMelee then explore else do -- Copy over the leader's target, if any, or follow his position. mtgt <- getsClient $ EM.lookup leader . stargetD tgtPathSet <- setPath mtgt unless tgtPathSet $ do let nonEnemyPath = Just TgtAndPath { tapTgt = TNonEnemy leader , tapPath = Nothing } nonEnemyPathSet <- setPath nonEnemyPath unless nonEnemyPathSet -- If no path even to the leader himself, explore. explore case gdoctrine fact of Ability.TExplore -> explore Ability.TFollow -> follow Ability.TFollowNoItems -> follow Ability.TMeleeAndRanged -> explore -- needs to find ranged targets Ability.TMeleeAdjacent -> explore -- probably not needed, but may change Ability.TBlock -> return () -- no point refreshing target Ability.TRoam -> explore -- @TRoam@ is checked again inside @explore@ Ability.TPatrol -> explore -- WIP LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/AI/PickTargetM.hs0000644000000000000000000007374407346545000023175 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | Let AI pick the best target for an actor. module Game.LambdaHack.Client.AI.PickTargetM ( refreshTarget #ifdef EXPOSE_INTERNAL -- * Internal operations , computeTarget #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Game.LambdaHack.Client.AI.ConditionM import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.BfsM import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.CaveKind as CK import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind (isUknownSpace) import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability -- | Verify and possibly change the target of an actor. This function both -- updates the target in the client state and returns the new target explicitly. refreshTarget :: MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> (ActorId, Actor) -> m (Maybe TgtAndPath) refreshTarget foeAssocs friendAssocs (aid, body) = do side <- getsClient sside let !_A = assert (bfid body == side `blame` "AI tries to move an enemy actor" `swith` (aid, body, side)) () let !_A = assert (not (bproj body) `blame` "AI gets to manually move its projectiles" `swith` (aid, body, side)) () mtarget <- computeTarget foeAssocs friendAssocs aid case mtarget of Nothing -> do -- Melee in progress and the actor can't contribute -- and would slow down others if he acted. -- Or he's just asleep. modifyClient $ \cli -> cli {stargetD = EM.delete aid (stargetD cli)} return Nothing Just tgtMPath -> do -- _debugoldTgt <- getsClient $ EM.lookup aid . stargetD modifyClient $ \cli -> cli {stargetD = EM.insert aid tgtMPath (stargetD cli)} return mtarget -- let _debug = T.unpack -- $ "\nHandleAI symbol:" <+> tshow (bsymbol body) -- <> ", aid:" <+> tshow aid -- <> ", pos:" <+> tshow (bpos body) -- <> "\nHandleAI oldTgt:" <+> tshow _debugoldTgt -- <> "\nHandleAI strTgt:" <+> tshow stratTarget -- <> "\nHandleAI target:" <+> tshow tgtMPath -- trace _debug $ return $ Just tgtMPath computeTarget :: forall m. MonadClient m => [(ActorId, Actor)] -> [(ActorId, Actor)] -> ActorId -> m (Maybe TgtAndPath) computeTarget foeAssocs friendAssocs aid = do cops@COps{cocave, corule=RuleContent{rWidthMax, rHeightMax, rnearby}, coTileSpeedup} <- getsState scops b <- getsState $ getActorBody aid mleader <- getsClient sleader salter <- getsClient salter -- We assume the actor eventually becomes a leader (or has the same -- set of skills as the leader, anyway) and set his target accordingly. actorMaxSkills <- getsState sactorMaxSkills condInMelee <- condInMeleeM $ blid b let lalter = salter EM.! blid b actorMaxSk = actorMaxSkills EM.! aid alterSkill = Ability.getSk Ability.SkAlter actorMaxSk lvl <- getLevel $ blid b localTime <- getsState $ getLocalTime (blid b) let stepAccesible :: [Point] -> Bool stepAccesible (q : _) = -- Effectively, only @alterMinWalk@ is checked, because real altering -- is not done via target path, but action after end of path. alterSkill >= fromEnum (lalter PointArray.! q) stepAccesible [] = False mtgtMPath <- getsClient $ EM.lookup aid . stargetD oldTgtUpdatedPath <- case mtgtMPath of Just TgtAndPath{tapTgt,tapPath=Nothing} -> -- This case is especially for TEnemyPos that would be lost otherwise. -- This is also triggered by @UpdLeadFaction@. Just <$> createPath aid tapTgt Just tap@TgtAndPath{tapTgt,tapPath=Just AndPath{..}} -> do mvalidPos <- getsState $ aidTgtToPos (Just aid) (blid b) (Just tapTgt) return $! if | isNothing mvalidPos -> Nothing -- wrong level | bpos b == pathGoal -> mtgtMPath -- goal reached; stay there picking up items -- or hiding in ambush or in panic | pathSource == bpos b -> -- no move -- If next step not accessible, something serious happened, -- so reconsider the target, not only path. if stepAccesible pathList then mtgtMPath else Nothing | otherwise -> case break (== bpos b) pathList of (crossed, _ : rest) -> -- step or many steps along path if null rest then Nothing -- path to the goal was partial, so tiles -- discovered or altered, so reconsider target else let newPath = AndPath{ pathSource = bpos b , pathList = rest , pathGoal , pathLen = pathLen - length crossed - 1 } in if stepAccesible rest then Just tap{tapPath=Just newPath} else Nothing (_, []) -> Nothing -- veered off the path, e.g., due to push -- by enemy or congestion, so serious, -- so reconsider target, not only path Nothing -> return Nothing -- no target assigned yet factionD <- getsState sfactionD seps <- getsClient seps let fact = factionD EM.! bfid b slackDoctrine = gdoctrine fact `elem` [ Ability.TMeleeAndRanged, Ability.TMeleeAdjacent , Ability.TBlock, Ability.TRoam, Ability.TPatrol ] canMove = Ability.getSk Ability.SkMove actorMaxSk > 0 canReach = canMove || Ability.getSk Ability.SkDisplace actorMaxSk > 0 -- Needed for now, because AI targets and shoots enemies -- based on the path to them, not LOS to them: || Ability.getSk Ability.SkProject actorMaxSk > 0 canAlter = Ability.getSk Ability.SkAlter actorMaxSk >= if slackDoctrine then 2 else 4 canMoveItem = Ability.getSk Ability.SkMoveItem actorMaxSk > 0 calmE = calmEnough b actorMaxSk heavilyDistressed = -- actor hit by a proj or similarly distressed deltasSerious (bcalmDelta b) -- Speedup compared to @currentSkillsClient@. actorMinSk <- getsState $ actorCurrentSkills Nothing aid condCanProject <- condCanProjectM (Ability.getSk Ability.SkProject actorMaxSk) aid fleeD <- getsClient sfleeD let condCanMelee = actorCanMelee actorMaxSkills aid b condHpTooLow = hpTooLow b actorMaxSk mfled = aid `EM.lookup` fleeD recentlyFled = maybe False (\(_, time) -> timeRecent5 localTime time) mfled recentlyFled20 = maybe False (\(_, time) -> timeRecent5 localTime time) mfled actorTurn = ticksPerMeter $ gearSpeed actorMaxSk let canEscape = fcanEscape (gkind fact) canSmell = Ability.getSk Ability.SkSmell actorMaxSk > 0 meleeNearby | canEscape = rnearby `div` 2 | otherwise = rnearby rangedNearby = 2 * meleeNearby -- We do target foes that already attack ours or have benign weapons. -- We assume benign weapons run out if they are the sole cause -- of targeting, to avoid stalemate. worthTargeting aidE body = let attacksFriends = any (adjacent (bpos body) . bpos . snd) friendAssocs && actorCanMeleeToHarm actorMaxSkills aidE body in attacksFriends || bweapBenign body > 0 || actorWorthChasing actorMaxSkills aidE body targetableMelee body = let attacksFriends = any (adjacent (bpos body) . bpos . snd) friendAssocs -- 3 is -- 1 from condSupport1 -- + 2 from foe being 2 away from friend before he closed in -- + 1 for as a margin for ambush, given than actors exploring -- can't physically keep adjacent all the time n | Ability.getSk Ability.SkAggression actorMaxSk >= 2 = rangedNearby -- boss never waits | condInMelee = if attacksFriends then 8 else 4 -- attack even if foe not in melee, to create another -- skirmish and perhaps overwhelm them in this one; -- also, this looks more natural; also sometimes the foe -- would attack our friend in a couple of turns anyway, -- but we may be too far from him at that time | otherwise = meleeNearby in canMove && condCanMelee && chessDist (bpos body) (bpos b) <= n -- Even when missiles run out, the non-moving foe will still be -- targeted, which is fine, since he is weakened by ranged, so should be -- meleed ASAP, even if without friends. targetableRanged body = (not condInMelee || Ability.getSk Ability.SkAggression actorMaxSk >= 2) -- boss fires at will && chessDist (bpos body) (bpos b) < rangedNearby && condCanProject && (canMove || targetableLine body) -- prevent the exploit of using cover against non-moving shooters -- causing them to ignore any other distant foes targetableLine body = isJust $ makeLine False b (bpos body) seps cops lvl targetableEnemy (aidE, body) = worthTargeting aidE body && (adjacent (bpos body) (bpos b) -- target regardless of anything, -- e.g., to flee if helpless || targetableMelee body || targetableRanged body) targetableFoes = filter targetableEnemy foeAssocs canMeleeEnemy (aidE, body) = actorCanMeleeToHarm actorMaxSkills aidE body nearbyFoes = if recentlyFled && not condInMelee then filter (not . canMeleeEnemy) targetableFoes else targetableFoes discoBenefit <- getsClient sdiscoBenefit getKind <- getsState $ flip getIidKind getArItem <- getsState $ flip aspectRecordFromIid cstashes <- if canMove && (calmE || null nearbyFoes) -- danger or risk of defecting && not heavilyDistressed && gunderAI fact -- humans target any stashes explicitly then closestStashes aid else return [] let desirableIid (iid, (k, _)) = let Benefit{benPickup} = discoBenefit EM.! iid in desirableItem cops canEscape benPickup (getArItem iid) (getKind iid) k desirableBagFloor bag = any desirableIid $ EM.assocs bag desirableFloor (_, (_, bag)) = desirableBagFloor bag focused = gearSpeed actorMaxSk < speedWalk || condHpTooLow couldMoveLastTurn = -- approximated; could have changed let actorSk = if mleader == Just aid then actorMaxSk else actorMinSk in Ability.getSk Ability.SkMove actorSk > 0 isStuck = actorWaits b && couldMoveLastTurn setPath :: Target -> m (Maybe TgtAndPath) setPath tgt = do let take6 tap@TgtAndPath{tapTgt=TEnemy{}} = tap -- @TEnemy@ needed for projecting, even by roaming actors; -- however, CStash not as binding, so excursions possible take6 TgtAndPath{tapPath=Just AndPath{..}} = -- Path followed for up to 6 moves regardless if the target valid -- and then target forgot and a new one picked. let path6 = take 6 pathList vOld = if bpos b /= pathGoal then towards (bpos b) pathGoal else Vector 0 0 tapTgt = TVector vOld tapPath = Just AndPath{pathList=path6, ..} in TgtAndPath{..} take6 tap = tap tgtpath <- createPath aid tgt return $ Just $ if slackDoctrine then take6 tgtpath else tgtpath pickNewTarget = pickNewTargetIgnore Nothing pickNewTargetIgnore :: Maybe ActorId -> m (Maybe TgtAndPath) pickNewTargetIgnore maidToIgnore = case cstashes of (_, (fid2, pos2)) : _ -> setPath $ TPoint (TStash fid2) (blid b) pos2 [] -> do let f aidToIgnore = filter ((/= aidToIgnore) . fst) nearbyFoes notIgnoredFoes = maybe nearbyFoes f maidToIgnore cfoes <- closestFoes notIgnoredFoes aid case cfoes of (_, (aid2, _)) : _ -> setPath $ TEnemy aid2 [] | condInMelee -> return Nothing -- don't slow down fighters -- this looks a bit strange, because teammates stop -- in their tracks all around the map (unless very close -- to the combatant), but the intuition is, not being able -- to help immediately, and not being too friendly -- to each other, they just wait and see and also shout -- to the teammate to flee and lure foes into ambush [] -> do mhideout <- if recentlyFled20 then closestHideout aid else return Nothing case (mhideout, mfled) of (Just (p, dist), Just (_, time)) | timeDeltaToFrom localTime time <= timeDeltaScale actorTurn (20 - dist) -> -- Only target if can reach the hideout 20 turns from fleeing -- start, given the actor speed as a leader. setPath $ TPoint THideout (blid b) p _ -> do citemsRaw <- if canMoveItem && canMove then closestItems aid else return [] let citems = toFreq "closestItems" $ filter desirableFloor citemsRaw if nullFreq citems then do -- Tracking enemies is more important than exploring, -- but smell is unreliable and may lead to allies, -- not foes, so avoid it. However, let's keep smell -- more imporant than getting to stairs, to let smelling -- monsters follow cues even on explored levels. smpos <- if canSmell then closestSmell aid else return [] case smpos of [] -> do ctriggersRaw <- closestTriggers ViaAnything aid let ctriggers = toFreq "ctriggers" ctriggersRaw if nullFreq ctriggers then do let oldpos = fromMaybe (bpos b) (boldpos b) vOld = bpos b `vectorToFrom` oldpos pNew = shiftBounded rWidthMax rHeightMax (bpos b) vOld if slackDoctrine && not isStuck && calmE && not focused && isUnit vOld && bpos b /= pNew -- both are needed, e.g., when just teleported -- or when the shift bounded by level borders then do let vFreq = toFreq "vFreq" $ (20, vOld) : map (1,) moves v <- rndToAction $ frequency vFreq -- Once the most pressing targets exhaused, -- wander around for 7 steps and only then, -- or if blocked or derailed, consider again -- the old and new targets. -- -- Together with depending on heroes or aliens -- to keep arean, sleepiness, inability to displace -- and chasing random smells, this makes it very hard -- to fully explore and change levels for, e.g., -- animals. Heroes idling on the level help a lot. let pathSource = bpos b traSlack7 = trajectoryToPathBounded rWidthMax rHeightMax pathSource (replicate 7 v) -- > 6 from take6 pathList = map head $ group traSlack7 pathGoal = last pathList pathLen = length pathList tapTgt = TVector v tapPath = Just AndPath{..} return $ Just TgtAndPath {..} else do upos <- if canMove then closestUnknown aid else return Nothing case upos of Nothing -> do -- If can't move (and so no BFS data), -- no info gained. Or if can't open doors. -- If stuck among ice pillars, we can't help it. when (canMove && canAlter) $ modifyClient $ \cli -> cli {sexplored = ES.insert (blid b) (sexplored cli)} ctriggersRaw2 <- closestTriggers ViaExit aid let ctriggers2 = toFreq "ctriggers2" ctriggersRaw2 if nullFreq ctriggers2 then do let toKill = actorWorthKilling actorMaxSkills worthyFoes = filter (uncurry toKill) foeAssocs afoes <- closestFoes worthyFoes aid case afoes of (_, (aid2, _)) : _ -> -- All stones turned, time to win or die. setPath $ TEnemy aid2 [] -> do furthest <- furthestKnown aid setPath $ TPoint TKnown (blid b) furthest else do (p, (p0, bag)) <- rndToAction $ frequency ctriggers2 setPath $ TPoint (TEmbed bag p0) (blid b) p Just p -> setPath $ TPoint TUnknown (blid b) p else do (p, (p0, bag)) <- rndToAction $ frequency ctriggers setPath $ TPoint (TEmbed bag p0) (blid b) p (_, (p, _)) : _ -> setPath $ TPoint TSmell (blid b) p else do (p, bag) <- rndToAction $ frequency citems setPath $ TPoint (TItem bag) (blid b) p tellOthersNothingHere = do let f TgtAndPath{tapTgt} = case tapTgt of TPoint _ lid p -> p /= bpos b || lid /= blid b _ -> True modifyClient $ \cli -> cli {stargetD = EM.filter f (stargetD cli)} pickNewTarget updateTgt :: TgtAndPath -> m (Maybe TgtAndPath) updateTgt TgtAndPath{tapPath=Nothing} = pickNewTarget updateTgt tap@TgtAndPath{tapPath=Just AndPath{..},tapTgt} = case tapTgt of TEnemy a -> do body <- getsState $ getActorBody a if (condInMelee -- fight close foes or nobody at all || bweapon body <= 0 -- not dangerous || not focused && not (null nearbyFoes)) -- prefers closer foes && a `notElem` map fst nearbyFoes -- old one not close enough || blid body /= blid b -- wrong level || actorDying body -- foe already dying || not (worthTargeting a body) || recentlyFled then -- forget enemy positions to prevent attacking them -- again soon after flight pickNewTarget else do -- If there are no unwalkable tiles on the path to enemy, -- he gets target @TEnemy@ and then, even if such tiles emerge, -- the target updated by his moves remains @TEnemy@. -- Conversely, he is stuck with @TBlock@ if initial target had -- unwalkable tiles, for as long as they remain. Harmless quirk. mpath <- getCachePath aid $ bpos body case mpath of Nothing -> pickNewTargetIgnore (Just a) -- enemy became unreachable Just AndPath{pathList=[]} -> pickNewTarget -- he is his own enemy Just AndPath{pathList= q : _} -> -- If in melee and path blocked by actors (even proj.) -- change target for this turn due to urgency. -- Because of @condInMelee@ new target will be stash -- or enemy if any other is left, or empty target. -- If not in melee, keep target and consider your options -- (wait until blocking actors move or displace or melee -- or sidestep). We don't want to wander away -- in search of loot, only to turn around next turn -- when the enemy is again considered. if not condInMelee || q == bpos body -- blocked by the enemy, great! || not (occupiedBigLvl q lvl) && not (occupiedProjLvl q lvl) then return $ Just tap{tapPath=mpath} else pickNewTargetIgnore (Just a) TPoint _ lid _ | lid /= blid b -> pickNewTarget -- wrong level TPoint tgoal lid pos -> case tgoal of TStash fid2 -> do oursExploring <- getsState $ oursExploringAssocs (bfid b) let oursExploringLid = filter (\(_, body) -> blid body == lid) oursExploring spawnFreqs = CK.cactorFreq $ okind cocave $ lkind lvl hasGroup grp = fromMaybe 0 (lookup grp spawnFreqs) > 0 lvlSpawnsUs = any (hasGroup . fst) $ filter ((> 0) . snd) $ fgroups (gkind fact) -- Even if made peace with the faction, loot stash one last time. if (calmE || null nearbyFoes) -- no risk or can't defend anyway && not heavilyDistressed -- not under heavy fire && gstash (factionD EM.! fid2) == Just (lid, pos) -- The condition below is more lenient than in @closestStashes@ -- to avoid wasting time on guard's movement. && (fid2 == bfid b && (pos == bpos b -- guarded by me, so keep guarding && (null nearbyFoes -- if no foes nearby || length oursExploringLid > 1) -- or buddies nearby || isNothing (posToBigLvl pos lvl)) -- or unguarded && (length oursExploring > 1 -- other actors able to explore || lvlSpawnsUs) -- or future spawned will be able || isFoe (bfid b) fact fid2) then return $ Just tap else pickNewTarget -- In this case, need to retarget, to focus on foes that melee ours -- and not, e.g., on remembered foes or items. _ | condInMelee || not (null cstashes) -> pickNewTarget TEnemyPos _ -- chase last position even if foe hides | bpos b == pos -> tellOthersNothingHere | recentlyFled -> pickNewTarget -- forget enemy positions to prevent attacking them again soon | not (couldMoveLastTurn || null nearbyFoes) -> pickNewTarget -- if only, possibly, shooting, forget hotspots, target foes; -- this results in only pointman humans chasing old foes -- in preference of new visible ones, but it's fine | otherwise -> do -- Here pick the closer enemy, the remembered or seen, to avoid -- loops when approaching new enemy obscures him behind obstacle -- but reveals the previously remembered one, etc. let remainingDist = chessDist (bpos b) pos if any (\(_, b3) -> chessDist (bpos b) (bpos b3) < remainingDist) nearbyFoes then pickNewTarget else return $ Just tap -- Don't stop fleeing into hideout after 5 turns even if foes appear. THideout -> -- Approach or stay in the hideout until 20 turns pass. if not recentlyFled20 then pickNewTarget else return $ Just tap -- Prefer close foes to anything else below. _ | not (null nearbyFoes) -> pickNewTarget -- Below we check the target could not be picked again in -- pickNewTarget (e.g., an item got picked up by our teammate) -- and only in this case it is invalidated. -- This ensures targets are eventually reached (unless a foe -- shows up) and not changed all the time mid-route -- to equally interesting, but perhaps a bit closer targets, -- most probably already targeted by other actors. TEmbed bag p -> assert (adjacent pos p) $ do -- First, stairs and embedded items from @closestTriggers@. -- We don't check skills, because they normally don't change -- or we can put some equipment back and recover them. -- We don't determine if the stairs or embed are interesting -- (this changes with time), but allow the actor -- to reach them and then retarget. The two things we check -- is whether the embedded bag is still there, or used up -- and whether we happen to be already adjacent to @p@, -- even though not necessarily at @pos@. bag2 <- getsState $ getEmbedBag lid p -- not @pos@ if | bag /= bag2 -> pickNewTarget -- others will notice soon enough | adjacent (bpos b) p -> -- regardless if at @pos@ or not setPath $ TPoint TKnown lid (bpos b) -- stay there one turn (high chance to become leader) -- to enable triggering; if trigger fails -- (e.g, changed skills), will retarget next turn (@TAny@) | otherwise -> return $ Just tap TItem bag -> do bag2 <- getsState $ getFloorBag lid pos if | bag /= bag2 -> pickNewTarget -- others will notice soon enough | bpos b == pos -> setPath $ TPoint TKnown lid (bpos b) -- stay there one turn (high chance to become leader) -- to enable pickup; if pickup fails, will retarget | otherwise -> return $ Just tap TSmell -> if not canSmell || let sml = EM.findWithDefault timeZero pos (lsmell lvl) in sml <= ltime lvl then pickNewTarget -- others will notice soon enough else return $ Just tap TBlock -> do -- e.g., door or first unknown tile of an area let t = lvl `at` pos if isStuck -- not a very important target, because blocked || alterSkill < fromEnum (lalter PointArray.! pos) -- tile was searched or altered or skill lowered || Tile.isWalkable coTileSpeedup t -- tile is no longer unwalkable, so was explored -- so time to recalculate target then pickNewTarget -- others will notice soon enough else return $ Just tap TUnknown -> let t = lvl `at` pos in if lexpl lvl <= lseen lvl || not (isUknownSpace t) then pickNewTarget -- others will notice soon enough else return $ Just tap TKnown -> if bpos b == pos || isStuck || alterSkill < fromEnum (lalter PointArray.! pos) -- tile was searched or altered or skill lowered then pickNewTarget -- others unconcerned else return $ Just tap _ | condInMelee || not (null nearbyFoes && null cstashes) -> pickNewTarget TNonEnemy _ | mleader == Just aid -> -- a leader, never follow pickNewTarget TNonEnemy a -> do body <- getsState $ getActorBody a if blid body /= blid b -- wrong level then pickNewTarget else do -- Update path. If impossible, pick another target. mpath <- getCachePath aid $ bpos body case mpath of Nothing -> pickNewTarget Just AndPath{pathList=[]} -> pickNewTarget _ -> return $ Just tap{tapPath=mpath} TVector{} -> if bpos b /= pathGoal then return $ Just tap else pickNewTarget if canReach then maybe pickNewTarget updateTgt oldTgtUpdatedPath else return Nothing LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/AI/Strategy.hs0000644000000000000000000000724107346545000022612 0ustar0000000000000000{-# LANGUAGE DeriveTraversable, TupleSections #-} -- | AI strategies to direct actors not controlled directly by human players. -- No operation in this module involves the @State@ type or any of our -- client/server monads types. module Game.LambdaHack.Client.AI.Strategy ( Strategy, nullStrategy, liftFrequency , (.|), reject, (.=>), only, bestVariant, returN, mapStrategyM ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Applicative import Game.LambdaHack.Core.Frequency -- | A strategy is a choice of (non-empty) frequency tables -- of possible actions. -- -- Currently, the way we use it, the list could have at most one element -- (we filter out void frequencies early and only ever access the first). -- except for the argument of @mapStrategyM@, which may even be process -- to the end of the list, if no earlier strategies can be transformed -- into non-null ones. newtype Strategy a = Strategy { runStrategy :: [Frequency a] } deriving (Show, Foldable, Traversable) instance Monad Strategy where m >>= f = normalizeStrategy $ Strategy [ toFreq name [ #ifdef WITH_EXPENSIVE_ASSERTIONS assert (toInteger p * toInteger q <= toInteger maxBoundInt32) #endif (p * q, b) | (p, a) <- runFrequency x , y <- runStrategy (f a) , (q, b) <- runFrequency y ] | x <- runStrategy m , let name = "Strategy_bind (" <> nameFrequency x <> ")"] instance Functor Strategy where fmap f (Strategy fs) = Strategy (map (fmap f) fs) instance Applicative Strategy where {-# INLINE pure #-} pure x = Strategy $ return $! uniformFreq "Strategy_pure" [x] (<*>) = ap instance MonadPlus Strategy where mzero = Strategy [] mplus (Strategy xs) (Strategy ys) = Strategy (xs ++ ys) instance Alternative Strategy where (<|>) = mplus empty = mzero normalizeStrategy :: Strategy a -> Strategy a normalizeStrategy (Strategy fs) = Strategy $ filter (not . nullFreq) fs nullStrategy :: Strategy a -> Bool nullStrategy strat = null $ runStrategy strat -- | Strategy where only the actions from the given single frequency table -- can be picked. liftFrequency :: Frequency a -> Strategy a liftFrequency f = normalizeStrategy $ Strategy $ return f infixr 2 .| -- | Strategy with the actions from both argument strategies, -- with original frequencies. (.|) :: Strategy a -> Strategy a -> Strategy a (.|) = mplus -- | Strategy with no actions at all. reject :: Strategy a reject = mzero infix 3 .=> -- | Conditionally accepted strategy. (.=>) :: Bool -> Strategy a -> Strategy a p .=> m | p = m | otherwise = mzero -- | Strategy with all actions not satisfying the predicate removed. -- The remaining actions keep their original relative frequency values. only :: (a -> Bool) -> Strategy a -> Strategy a only p s = normalizeStrategy $ do x <- s p x .=> return x -- | When better choices are towards the start of the list, -- this is the best frequency of the strategy. bestVariant :: Strategy a -> Frequency a bestVariant (Strategy []) = mzero bestVariant (Strategy (f : _)) = f -- | Like 'return', but pick a name of the single frequency. returN :: Text -> a -> Strategy a returN name x = Strategy $ return $! uniformFreq name [x] mapStrategyM :: Monad m => (a -> m (Maybe b)) -> Strategy a -> m (Strategy b) mapStrategyM f s = do let mapFreq freq = do let g (k, a) = do mb <- f a return $! (k,) <$> mb lbm <- mapM g $ runFrequency freq return $! toFreq "mapStrategyM" $ catMaybes lbm ls = runStrategy s lt <- mapM mapFreq ls return $! normalizeStrategy $ Strategy lt LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/Bfs.hs0000644000000000000000000003604407346545000021234 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, RankNTypes, TypeFamilies #-} -- | Breadth first search algorithm. module Game.LambdaHack.Client.Bfs ( BfsDistance, MoveLegal(..) , subtractBfsDistance, minKnownBfs, apartBfs, maxBfsDistance, fillBfs , AndPath(..), findPathBfs, accessBfs #ifdef EXPOSE_INTERNAL -- * Internal operations , succBfsDistance, predBfsDistance, abortedUnknownBfs, maskBfs, distanceBfs #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Monad.ST.Strict (ST, runST) import Data.Binary import Data.Bits (Bits, complement, (.&.), (.|.)) import qualified Data.EnumSet as ES import qualified Data.IntSet as IS import qualified Data.Primitive.PrimArray as PA import qualified Data.Vector.Unboxed as U import qualified Data.Vector.Unboxed.Mutable as VM import GHC.Exts (inline) import GHC.Generics (Generic) import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Vector import Game.LambdaHack.Definition.Defs -- @Word8@ is much faster, but in some very rare cases leads to AI loops, -- e.g., when a move through uknown terrain towards enemy stash -- goes beyond the @apartBfs@ range and makes AI abandon the stash target, -- only to pick it up after a step in the opposite direction. -- In normal LH maps, path length can get to around 200, -- in contrived mazes it could perhaps reach a few thousand. type DistanceWord = Word16 -- | Weighted distance between points along shortest paths. newtype BfsDistance = BfsDistance {bfsDistance :: DistanceWord} deriving (Show, Eq, Ord, Bits) instance PointArray.UnboxRepClass BfsDistance where type UnboxRep BfsDistance = DistanceWord toUnboxRepUnsafe = bfsDistance fromUnboxRep = BfsDistance -- | State of legality of moves between adjacent points. data MoveLegal = MoveBlocked | MoveToOpen | MoveToClosed | MoveToUnknown deriving Eq succBfsDistance :: BfsDistance -> BfsDistance succBfsDistance d = BfsDistance $ bfsDistance d + 1 predBfsDistance :: BfsDistance -> BfsDistance predBfsDistance d = BfsDistance $ bfsDistance d - 1 subtractBfsDistance :: BfsDistance -> BfsDistance -> Int subtractBfsDistance d1 d2 = fromEnum $ bfsDistance d1 - bfsDistance d2 -- | The minimal distance value assigned to paths that don't enter -- any unknown tiles. minKnownBfs :: BfsDistance minKnownBfs = BfsDistance $ 1 + maxBound `div` 2 -- | The distance value that denotes no legal path between points, -- either due to blocked tiles or pathfinding aborted at earlier tiles, -- e.g., due to unknown tiles. apartBfs :: BfsDistance apartBfs = predBfsDistance minKnownBfs -- | Maximum value of the type. maxBfsDistance :: BfsDistance maxBfsDistance = BfsDistance (maxBound :: DistanceWord) -- | The distance value that denotes that path search was aborted -- at this tile due to too large actual distance -- and that the tile was unknown. -- It is also a true distance value for this tile. abortedUnknownBfs :: BfsDistance abortedUnknownBfs = predBfsDistance apartBfs maskBfs :: BfsDistance -> BfsDistance {-# INLINE maskBfs #-} maskBfs distance = distance .&. complement minKnownBfs -- | Create and fill a BFS array for the given level. -- Unsafe array operations are OK here, because the intermediate -- values of the vector don't leak anywhere outside nor are kept unevaluated -- and so they can't be overwritten by the unsafe side-effect. -- -- When computing move cost, we assume doors openable at no cost, -- because other actors use them, too, so the cost is shared and the extra -- visiblity is valuable, too. We treat unknown tiles specially. -- Whether suspect tiles are considered openable depends on @smarkSuspect@. -- -- Instead of a BFS queue (list) we use the two tabs (arrays), for (JS) speed. fillBfs :: PointArray.Array Word8 -> Word8 -> Point -> (PA.PrimArray PointI, PA.PrimArray PointI) -> PointArray.Array BfsDistance fillBfs !lalter !alterSkill !source (!tabA, !tabB) = runST $ do let arr = PointArray.replicateA (PointArray.axsize lalter) (PointArray.aysize lalter) apartBfs vThawed <- U.unsafeThaw $ PointArray.avector arr tabAThawed <- PA.unsafeThawPrimArray tabA tabBThawed <- PA.unsafeThawPrimArray tabB fillBfsThawed lalter alterSkill (fromEnum source) (tabAThawed, tabBThawed) vThawed void $ PA.unsafeFreezePrimArray tabAThawed void $ PA.unsafeFreezePrimArray tabBThawed void $ U.unsafeFreeze vThawed return arr type QueueIx = Int type NextQueueIx = Int -- So very low-level that not even under EXPOSE_INTERNAL. fillBfsThawed :: forall s. PointArray.Array Word8 -> Word8 -> PointI -> (PA.MutablePrimArray s PointI, PA.MutablePrimArray s PointI) -> U.MVector s DistanceWord -> ST s () fillBfsThawed !lalter !alterSkill !sourceI (!tabAThawed, !tabBThawed) !vThawed = do let unsafeReadI :: PointI -> ST s BfsDistance {-# INLINE unsafeReadI #-} #ifdef WITH_EXPENSIVE_ASSERTIONS unsafeReadI p = BfsDistance <$> VM.read vThawed p -- index checking is sometimes an expensive (kind of) assertion #else unsafeReadI p = BfsDistance <$> VM.unsafeRead vThawed p #endif unsafeWriteI :: PointI -> BfsDistance -> ST s () {-# INLINE unsafeWriteI #-} #ifdef WITH_EXPENSIVE_ASSERTIONS unsafeWriteI p c = VM.write vThawed p (bfsDistance c) #else unsafeWriteI p c = VM.unsafeWrite vThawed p (bfsDistance c) #endif -- The two tabs (arrays) are used as a staged, optimized queue. -- The first tab is for writes, the second one for reads. -- They switch places in each recursive @bfs@ call. bfs :: PA.MutablePrimArray s PointI -> PA.MutablePrimArray s PointI -> BfsDistance -> QueueIx -> ST s () bfs !tabReadThawed !tabWriteThawed !distance !prevQueueIx = do let unsafeReadCurrent :: QueueIx -> ST s PointI {-# INLINE unsafeReadCurrent #-} unsafeReadCurrent = PA.readPrimArray tabReadThawed unsafeWriteNext :: QueueIx -> PointI -> ST s () {-# INLINE unsafeWriteNext #-} unsafeWriteNext = PA.writePrimArray tabWriteThawed -- The accumulator and the result represent the index into the next -- queue tab, incremented after each write. processQueue :: QueueIx -> NextQueueIx -> ST s NextQueueIx processQueue !currentQueueIx !acc1 = if currentQueueIx == -1 then return acc1 -- all queued positions inspected else do pos <- unsafeReadCurrent currentQueueIx let processMove :: (X, Y) -> NextQueueIx -> ST s NextQueueIx {-# INLINE processMove #-} processMove move acc2 = do let p = pos + inline fromEnum (uncurry Vector move) pDist <- unsafeReadI p if pDist /= apartBfs then return acc2 -- the position visited already else do let alter :: Word8 !alter = lalter `PointArray.accessI` p if | alterSkill < alter -> return acc2 | alter == 1 -> do let distCompl = maskBfs distance unsafeWriteI p distCompl return acc2 | otherwise -> do unsafeWriteI p distance unsafeWriteNext acc2 p return $! acc2 + 1 -- Innermost loop over @moves@ manually unrolled for (JS) speed: return acc1 >>= processMove (-1, -1) >>= processMove (0, -1) >>= processMove (1, -1) >>= processMove (1, 0) >>= processMove (1, 1) >>= processMove (0, 1) >>= processMove (-1, 1) >>= processMove (-1, 0) -- Recursive call to process next queue element: >>= processQueue (currentQueueIx - 1) acc3 <- processQueue (prevQueueIx - 1) 0 let distanceNew = succBfsDistance distance if acc3 == 0 || distanceNew == maxBfsDistance then return () -- no more close enough dungeon positions else bfs tabWriteThawed tabReadThawed distanceNew acc3 #ifdef WITH_EXPENSIVE_ASSERTIONS VM.write vThawed sourceI (bfsDistance minKnownBfs) #else VM.unsafeWrite vThawed sourceI (bfsDistance minKnownBfs) #endif PA.writePrimArray tabAThawed 0 sourceI bfs tabAThawed tabBThawed (succBfsDistance minKnownBfs) 1 data AndPath = AndPath { pathSource :: Point -- never included in @pathList@ , pathList :: [Point] , pathGoal :: Point -- needn't be @last pathList@ , pathLen :: Int -- needn't be @length pathList@ } deriving (Show, Generic) instance Binary AndPath -- | Find a path, without the source position, with the smallest length. -- The @eps@ coefficient determines which direction (of the closest -- directions available) that path should prefer, where 0 means north-west -- and 1 means north. The path tries hard to avoid actors and tries to avoid -- tiles that need altering and ambient light. Actors are avoided only close -- to the start of the path, because elsewhere they are likely to move -- before they are reached. Even projectiles are avoided, -- which sometimes has the effect of choosing a safer route -- (regardless if the projectiles are friendly fire or not). -- -- An unwelcome side effect of avoiding actors is that friends will sometimes -- avoid displacing and instead perform two separate moves, wasting 1 turn -- in total (only if they had opposed direction of their goals; unlikely). -- But in corridors they will still displace and elsewhere this scenario -- was quite rare already. findPathBfs :: ES.EnumSet Point -> PointArray.Array Word8 -> (PointI -> Bool) -> Point -> Point -> Int -> PointArray.Array BfsDistance -> Maybe AndPath {-# INLINE findPathBfs #-} findPathBfs lbig lalter fovLit pathSource pathGoal sepsRaw arr = let !pathGoalI = fromEnum pathGoal !pathSourceI = fromEnum pathSource eps = sepsRaw `mod` 4 (mc1, mc2) = splitAt eps movesCardinalI (md1, md2) = splitAt eps movesDiagonalI -- Prefer cardinal directions when closer to the target, so that -- the enemy can't easily disengage. prefMoves = mc2 ++ reverse mc1 ++ md2 ++ reverse md1 -- fuzz track :: PointI -> BfsDistance -> [Point] -> [Point] track !pos !oldDist !suffix | oldDist == minKnownBfs = assert (pos == pathSourceI) suffix track pos oldDist suffix | oldDist == succBfsDistance minKnownBfs = let !posP = toEnum pos in posP : suffix -- avoid calculating minP and dist for the last call track pos oldDist suffix = let !dist = predBfsDistance oldDist minChild :: PointI -> Bool -> Word8 -> [VectorI] -> PointI minChild !minP _ _ [] = minP minChild minP maxDark minAlter (mv : mvs) = let !p = pos + mv backtrackingMove = BfsDistance (arr `PointArray.accessI` p) /= dist in if backtrackingMove then minChild minP maxDark minAlter mvs else let free = p `IS.notMember` ES.enumSetToIntSet lbig alter | free = lalter `PointArray.accessI` p | otherwise = maxBound-1 -- occupied; disaster dark = not $ fovLit p -- Prefer paths without actors and through -- more easily opened tiles and, secondly, -- in the ambient dark (even if light carried, -- because it can be taken off at any moment). in if | alter == 0 && dark -> p -- speedup | alter < minAlter -> minChild p dark alter mvs | dark > maxDark && alter == minAlter -> minChild p dark alter mvs | otherwise -> minChild minP maxDark minAlter mvs -- @maxBound@ means not alterable, so some child will be lower !newPos = minChild pos{-dummy-} False maxBound prefMoves #ifdef WITH_EXPENSIVE_ASSERTIONS !_A = assert (newPos /= pos) () #endif !posP = toEnum pos in track newPos dist (posP : suffix) !goalDist = BfsDistance $ arr `PointArray.accessI` pathGoalI pathLen = fromEnum $ bfsDistance $ maskBfs goalDist pathList = track pathGoalI (goalDist .|. minKnownBfs) [] andPath = AndPath{..} in assert (BfsDistance (arr `PointArray.accessI` pathSourceI) == minKnownBfs) $ if goalDist /= apartBfs && pathLen < 2 * chessDist pathSource pathGoal then Just andPath else let f :: (Point, Int, Int, Int) -> Point -> BfsDistance -> (Point, Int, Int, Int) f acc@(pAcc, dAcc, chessAcc, sumAcc) p d = if d <= abortedUnknownBfs -- works in visible secrets mode only || d /= apartBfs && adjacent p pathGoal -- works for stairs then let dist = fromEnum $ bfsDistance $ maskBfs d chessNew = chessDist p pathGoal sumNew = dist + 2 * chessNew resNew = (p, dist, chessNew, sumNew) in case compare sumNew sumAcc of LT -> resNew EQ -> case compare chessNew chessAcc of LT -> resNew EQ -> case compare dist dAcc of LT -> resNew EQ | euclidDistSq p pathGoal < euclidDistSq pAcc pathGoal -> resNew _ -> acc _ -> acc _ -> acc else acc initAcc = (originPoint, maxBound, maxBound, maxBound) (pRes, dRes, _, sumRes) = PointArray.ifoldlA' f initAcc arr in if sumRes == maxBound || goalDist /= apartBfs && pathLen < sumRes then if goalDist /= apartBfs then Just andPath else Nothing else let pathList2 = track (fromEnum pRes) (BfsDistance (toEnum dRes) .|. minKnownBfs) [] in Just AndPath{pathList = pathList2, pathLen = sumRes, ..} -- | Access a BFS array and interpret the looked up distance value. accessBfs :: PointArray.Array BfsDistance -> Point -> Maybe Int accessBfs bfs p = if PointArray.axsize bfs == 0 then Nothing else distanceBfs $ bfs PointArray.! p distanceBfs :: BfsDistance -> Maybe Int {-# INLINE distanceBfs #-} distanceBfs dist = if dist == apartBfs then Nothing else Just $ fromEnum $ bfsDistance $ maskBfs dist LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/BfsM.hs0000644000000000000000000007133107346545000021347 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | Breadth first search and related algorithms using the client monad. module Game.LambdaHack.Client.BfsM ( invalidateBfsAid, invalidateBfsPathAid , invalidateBfsLid, invalidateBfsPathLid , invalidateBfsAll, invalidateBfsPathAll , createBfs, getCacheBfsAndPath, getCacheBfs , getCachePath, createPath, condBFS , furthestKnown, closestUnknown, closestSmell , FleeViaStairsOrEscape(..) , embedBenefit, closestTriggers, condEnoughGearM, closestItems, closestFoes , closestStashes, oursExploringAssocs, closestHideout #ifdef EXPOSE_INTERNAL , unexploredDepth, updatePathFromBfs #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Word import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.CaveKind as CK import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind (isUknownSpace) import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs invalidateBfsAid :: MonadClient m => ActorId -> m () invalidateBfsAid aid = modifyClient $ \cli -> cli {sbfsD = EM.adjust (const BfsInvalid) aid (sbfsD cli)} invalidateBfsPathAid :: MonadClient m => ActorId -> m () invalidateBfsPathAid aid = do let f BfsInvalid = BfsInvalid f (BfsAndPath bfsArr _) = BfsAndPath bfsArr EM.empty modifyClient $ \cli -> cli {sbfsD = EM.adjust f aid (sbfsD cli)} -- Even very distant actors affected, e.g., when a hidden door found in a wall. invalidateBfsLid :: MonadClient m => LevelId -> m () invalidateBfsLid lid = do lvl <- getLevel lid -- No need to filter, because foes won't be in our BFS map and looking up -- in our BFS map is faster than in all actors map. mapM_ invalidateBfsAid $ EM.elems $ lbig lvl -- We invalidate, but not when actors move, since they are likely to move -- out of the way in time. We only do, when they appear or disappear, -- because they may be immobile or too close to move away before we get there. -- We also don't consider far actors, since they are likely to disappear -- again or to be far from our path. If they close enough to be lit -- by our light, or one step further, that's worth taking seriously. invalidateBfsPathLid :: MonadClient m => Actor -> m () invalidateBfsPathLid body = do lvl <- getLevel $ blid body let close (p, _) = chessDist p (bpos body) <= 3 -- heuristic -- No need to filter more, because foes won't be in our BFS map and looking up -- in our BFS map is faster than in all actors map. mapM_ (invalidateBfsPathAid . snd) $ filter close $ EM.assocs $ lbig lvl invalidateBfsAll :: MonadClient m => m () invalidateBfsAll = modifyClient $ \cli -> cli {sbfsD = EM.map (const BfsInvalid) (sbfsD cli)} invalidateBfsPathAll :: MonadClient m => m () invalidateBfsPathAll = do let f BfsInvalid = BfsInvalid f (BfsAndPath bfsArr _) = BfsAndPath bfsArr EM.empty modifyClient $ \cli -> cli {sbfsD = EM.map f (sbfsD cli)} createBfs :: MonadClientRead m => Bool -> Word8 -> ActorId -> m (PointArray.Array BfsDistance) createBfs canMove alterSkill0 aid = if canMove then do b <- getsState $ getActorBody aid salter <- getsClient salter let source = bpos b lalter = salter EM.! blid b alterSkill = max 1 alterSkill0 -- We increase 0 skill to 1, to also path through unknown tiles. -- Since there are no other tiles that require skill 1, this is safe. stabs <- getsClient stabs return $! fillBfs lalter alterSkill source stabs else return PointArray.empty updatePathFromBfs :: MonadClient m => Bool -> BfsAndPath -> ActorId -> Point -> m (PointArray.Array BfsDistance, Maybe AndPath) updatePathFromBfs canMove bfsAndPathOld aid !target = do COps{coTileSpeedup} <- getsState scops let (oldBfsArr, oldBfsPath) = case bfsAndPathOld of (BfsAndPath bfsArr bfsPath) -> (bfsArr, bfsPath) BfsInvalid -> error $ "" `showFailure` (bfsAndPathOld, aid, target) let bfsArr = oldBfsArr if not canMove then return (bfsArr, Nothing) else do getActorB <- getsState $ flip getActorBody let b = getActorB aid fact <- getsState $ (EM.! bfid b) . sfactionD seps <- getsClient seps salter <- getsClient salter lvl <- getLevel (blid b) let !lalter = salter EM.! blid b fovLit p = Tile.isLit coTileSpeedup $ PointArray.fromUnboxRep $ ltile lvl `PointArray.accessI` p addFoeVicinity (p, aid2) = let b2 = getActorB aid2 in if isFoe (bfid b) fact (bfid b2) then p : vicinityUnsafe p else [p] bigAdj = ES.fromList $ concatMap addFoeVicinity $ EM.assocs $ EM.delete source $ lbig lvl -- don't sidestep oneself !source = bpos b !mpath = findPathBfs bigAdj lalter fovLit source target seps bfsArr !bfsPath = maybe oldBfsPath (\path -> EM.insert target path oldBfsPath) mpath bap = BfsAndPath bfsArr bfsPath modifyClient $ \cli -> cli {sbfsD = EM.insert aid bap $ sbfsD cli} return (bfsArr, mpath) -- | Get cached BFS array and path or, if not stored, generate and store first. getCacheBfsAndPath :: forall m. MonadClient m => ActorId -> Point -> m (PointArray.Array BfsDistance, Maybe AndPath) getCacheBfsAndPath aid target = do mbfs <- getsClient $ EM.lookup aid . sbfsD case mbfs of Just bap@(BfsAndPath bfsArr bfsPath) -> case EM.lookup target bfsPath of Nothing -> do (!canMove, _) <- condBFS aid updatePathFromBfs canMove bap aid target mpath@Just{} -> return (bfsArr, mpath) _ -> do (canMove, alterSkill) <- condBFS aid !bfsArr <- createBfs canMove alterSkill aid let bfsPath = EM.empty updatePathFromBfs canMove (BfsAndPath bfsArr bfsPath) aid target -- | Get cached BFS array or, if not stored, generate and store first. getCacheBfs :: MonadClient m => ActorId -> m (PointArray.Array BfsDistance) getCacheBfs aid = do mbfs <- getsClient $ EM.lookup aid . sbfsD case mbfs of Just (BfsAndPath bfsArr _) -> return bfsArr _ -> do (canMove, alterSkill) <- condBFS aid !bfsArr <- createBfs canMove alterSkill aid let bfsPath = EM.empty modifyClient $ \cli -> cli {sbfsD = EM.insert aid (BfsAndPath bfsArr bfsPath) (sbfsD cli)} return bfsArr -- | Get cached BFS path or, if not stored, generate and store first. getCachePath :: MonadClient m => ActorId -> Point -> m (Maybe AndPath) getCachePath aid target = do b <- getsState $ getActorBody aid let source = bpos b if source == target then return $ Just $ AndPath (bpos b) [] target 0 -- speedup else snd <$> getCacheBfsAndPath aid target createPath :: MonadClient m => ActorId -> Target -> m TgtAndPath createPath aid tapTgt = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel $ blid b let stopAtUnwalkable tapPath@(Just AndPath{..}) = let (walkable, rest) = -- Unknown tiles are not walkable, so path stops just before. -- which is good, because by the time actor reaches the tile, -- it is known and target is recalculated with new info, -- perhaps sidestepping the tile, e.g., if explosive. span (Tile.isWalkable coTileSpeedup . at lvl) pathList in case rest of _ | null walkable -> TgtAndPath{..} [] -> TgtAndPath{..} [g] | g == pathGoal -> TgtAndPath{..} -- the exception is when the tile is explicitly targeted newGoal : _ -> let newTgt = TPoint TBlock (blid b) newGoal newPath = AndPath{ pathSource = bpos b , pathList = walkable -- no @newGoal@ , pathGoal = newGoal , pathLen = length walkable + 1 } in TgtAndPath{tapTgt = newTgt, tapPath = Just newPath} stopAtUnwalkable Nothing = TgtAndPath{tapTgt, tapPath=Nothing} mpos <- getsState $ aidTgtToPos (Just aid) (blid b) (Just tapTgt) case mpos of Nothing -> return TgtAndPath{tapTgt, tapPath=Nothing} Just p -> do path <- getCachePath aid p return $! stopAtUnwalkable path condBFS :: MonadClientRead m => ActorId -> m (Bool, Word8) condBFS aid = do side <- getsClient sside -- We assume the actor eventually becomes a leader (or has the same -- set of skills as the leader, anyway). Otherwise we'd have -- to reset BFS after leader changes, but it would still lead to -- wasted movement if, e.g., non-leaders move but only leaders open doors -- and leader change is very rare. actorMaxSk <- getsState $ getActorMaxSkills aid let alterSkill = min (maxBound - 1) -- @maxBound :: Word8@ means unalterable (toEnum $ max 0 $ Ability.getSk Ability.SkAlter actorMaxSk) canMove = Ability.getSk Ability.SkMove actorMaxSk > 0 || Ability.getSk Ability.SkDisplace actorMaxSk > 0 || Ability.getSk Ability.SkProject actorMaxSk > 0 smarkSuspect <- getsClient smarkSuspect fact <- getsState $ (EM.! side) . sfactionD let -- Under UI, playing a hero party, we let AI set our target each -- turn for non-pointmen that can't move and can't alter, -- usually to TUnknown. This is rather useless, but correct. enterSuspect = smarkSuspect > 0 || gunderAI fact skill | enterSuspect = alterSkill -- dig and search as skill allows | otherwise = 0 -- only walkable tiles return (canMove, skill) -- keep it lazy -- | Furthest (wrt paths) known position. furthestKnown :: MonadClient m => ActorId -> m Point furthestKnown aid = do bfs <- getCacheBfs aid getMaxIndex <- rndToAction $ oneOf [ PointArray.maxIndexA , PointArray.maxLastIndexA ] let furthestPos = getMaxIndex bfs dist = bfs PointArray.! furthestPos return $! assert (dist > apartBfs `blame` (aid, furthestPos, dist)) furthestPos -- | Closest reachable unknown tile position, if any. -- -- Note: some of these tiles are behind suspect tiles and they are chosen -- in preference to more distant directly accessible unknown tiles. -- This is in principle OK, but in dungeons with few hidden doors -- AI is at a disadvantage (and with many hidden doors, it fares as well -- as a human that deduced the dungeon properties). Changing Bfs to accomodate -- all dungeon styles would be complex and would slow down the engine. -- -- If the level has inaccessible open areas (at least from the stairs AI used) -- the level will be nevertheless here finally labeled as explored, -- to enable transition to other levels. -- We should generally avoid such levels, because digging and/or trying -- to find other stairs leading to disconnected areas is not KISS -- so we don't do this in AI, so AI is at a disadvantage. -- -- If the closest unknown is more than 126 tiles away from the targeting -- actor, the level will marked as explored. We could complicate the code -- and not mark if the unknown is too far as opposed to inaccessible, -- but then if it is both too distant and inaccessible, AI would be -- permanently stuck on such levels. To cope with this, escapes need to be -- placed on open or small levels, or in dispersed enough that they don't -- appear in such potentially unexplored potions of caves. Other than that, -- this is rather harmless and hard to exploit, so let it be. -- The principled way to fix this would be to extend BFS to @Word16@, -- but then it takes too long to compute on maze levels, so we'd need -- to optimize hard for JS. closestUnknown :: MonadClient m => ActorId -> m (Maybe Point) closestUnknown aid = do body <- getsState $ getActorBody aid lvl <- getLevel $ blid body bfs <- getCacheBfs aid let closestPoss = PointArray.minIndexesA bfs dist = bfs PointArray.! head closestPoss !_A = assert (lexpl lvl >= lseen lvl) () return $! if lexpl lvl <= lseen lvl -- Some unknown may still be visible and even pathable, but we already -- know from global level info that they are inaccessible. || dist >= apartBfs -- Global level info may tell us that terrain was changed and so -- some new explorable tile appeared, but we don't care about those -- and we know we already explored all initially seen unknown tiles -- and it's enough for us (otherwise we'd need to hunt all around -- the map for tiles altered by enemies). then Nothing else let unknownAround pos = let vic = vicinityUnsafe pos countUnknown :: Int -> Point -> Int countUnknown c p = if isUknownSpace $ lvl `at` p then c + 1 else c in foldl' countUnknown 0 vic cmp = comparing unknownAround in Just $ maximumBy cmp closestPoss -- | Finds smells closest to the actor, except under the actor, -- because actors consume smell only moving over them, not standing. -- Of the closest, prefers the newest smell. closestSmell :: MonadClient m => ActorId -> m [(Int, (Point, Time))] closestSmell aid = do body <- getsState $ getActorBody aid Level{lsmell, ltime} <- getLevel $ blid body let smells = filter (\(p, sm) -> sm > ltime && p /= bpos body) (EM.assocs lsmell) case smells of [] -> return [] _ -> do bfs <- getCacheBfs aid let ts = mapMaybe (\x@(p, _) -> fmap (,x) (accessBfs bfs p)) smells return $! sortOn (fst &&& absoluteTimeNegate . snd . snd) ts data FleeViaStairsOrEscape = ViaStairs | ViaStairsUp | ViaStairsDown | ViaEscape | ViaExit -- can change whenever @sexplored@ changes | ViaNothing | ViaAnything deriving (Show, Eq) embedBenefit :: MonadClientRead m => FleeViaStairsOrEscape -> ActorId -> [(Point, ItemBag)] -> m [(Double, (Point, ItemBag))] embedBenefit fleeVia aid pbags = do COps{cocave, coTileSpeedup} <- getsState scops dungeon <- getsState sdungeon explored <- getsClient sexplored b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD lvl <- getLevel (blid b) oursExploring <- getsState $ oursExploringAssocs (bfid b) let oursExploringLid = filter (\(_, body) -> blid body == blid b) oursExploring spawnFreqs = CK.cactorFreq $ okind cocave $ lkind lvl hasGroup grp = fromMaybe 0 (lookup grp spawnFreqs) > 0 lvlSpawnsUs = any (hasGroup . fst) $ filter ((> 0) . snd) $ fgroups (gkind fact) actorSk <- if fleeVia `elem` [ViaAnything, ViaExit] -- targeting, possibly when not a leader then getsState $ getActorMaxSkills aid else currentSkillsClient aid let alterSkill = Ability.getSk Ability.SkAlter actorSk condOurAdj <- getsState $ any (\(_, b2) -> isFriend (bfid b) fact (bfid b2)) . adjacentBigAssocs b unexploredTrue <- unexploredDepth True (blid b) unexploredFalse <- unexploredDepth False (blid b) condEnoughGear <- condEnoughGearM aid discoBenefit <- getsClient sdiscoBenefit getKind <- getsState $ flip getIidKind let alterMinSkill p = Tile.alterMinSkill coTileSpeedup $ lvl `at` p lidExplored = ES.member (blid b) explored allExplored = ES.size explored == EM.size dungeon -- Ignoring the number of items, because only one of each @iid@ -- is triggered at the same time, others are left to be used later on. -- Taking the kind the item hides under into consideration, because -- it's a best guess only, for AI and UI. iidToEffs iid = IK.ieffects $ getKind iid feats bag = concatMap iidToEffs $ EM.keys bag -- For simplicity, we assume at most one exit at each position. -- AI uses exit regardless of traps or treasures at the spot. bens (_, bag) = case find IK.isEffEscapeOrAscend $ feats bag of Just IK.Escape{} -> -- Escape (or guard) only after exploring, for high score, etc. let escapeOrGuard = fcanEscape (gkind fact) || fleeVia == ViaExit -- target to guard after explored in if fleeVia `elem` [ViaAnything, ViaEscape, ViaExit] && escapeOrGuard && allExplored then 10 else 0 -- don't escape prematurely Just (IK.Ascend up) -> -- change levels sensibly, in teams let easier = up /= (fromEnum (blid b) > 0) unexpForth = if up then unexploredTrue else unexploredFalse unexpBack = if not up then unexploredTrue else unexploredFalse -- Forbid loops via peeking at unexplored and getting back. aiCond = if unexpForth then easier && condEnoughGear || (not unexpBack || easier) && lidExplored else not unexpBack && easier && allExplored && null (lescape lvl) -- Prefer one direction of stairs, to team up -- and prefer embed (may, e.g., create loot) over stairs. v = if aiCond then if easier then 10 else 1 else 0 guardingStash = case gstash fact of Nothing -> False Just (lid, p) -> lid == blid b && (length oursExploring > 1 || lvlSpawnsUs) && (length oursExploringLid <= 1 -- not @==@ in case guard temporarily nonmoving || p == bpos b && not condOurAdj) -- don't leave the post; let the others explore in case fleeVia of _ | guardingStash -> 0 ViaStairsUp | up -> 1 ViaStairsDown | not up -> 1 ViaStairs -> v ViaExit -> v ViaAnything -> v _ -> 0 -- don't ascend prematurely _ -> if fleeVia `elem` [ViaNothing, ViaAnything] then -- Actor uses the embedded item on himself, hence @benApply@. -- Let distance be the deciding factor and also prevent -- overflow on 32-bit machines. let sacrificeForExperiment = 101 -- single explosion acceptable sumBen = sum $ map (\iid -> benApply $ discoBenefit EM.! iid) (EM.keys bag) in min 1000 $ sacrificeForExperiment + sumBen else 0 underFeet p = p == bpos b -- if enter and alter, be more permissive -- Only actors with high enough @SkAlter@ can trigger terrain. -- Blocking actors and items not checked, because they can be moved -- before the actor gets to the location, or after. f (p, _) = underFeet p || alterSkill >= fromEnum (alterMinSkill p) || Tile.isSuspect coTileSpeedup (lvl `at` p) && alterSkill >= 2 benFeats = map (\pbag -> (bens pbag, pbag)) $ filter f pbags considered (benefitAndSacrifice, (p, _bag)) = benefitAndSacrifice > 0 -- For speed and to avoid greedy AI loops, only experiment with few. && Tile.consideredByAI coTileSpeedup (lvl `at` p) return $! filter considered benFeats -- | Closest (wrt paths) AI-triggerable tiles with embedded items. -- In AI, the level the actor is on is either explored or the actor already -- has a weapon equipped, so no need to explore further, he tries to find -- enemies on other levels, but before that, he triggers other tiles -- in hope of some loot or beneficial effect to enter next level with. closestTriggers :: MonadClient m => FleeViaStairsOrEscape -> ActorId -> m [(Int, (Point, (Point, ItemBag)))] closestTriggers fleeVia aid = do COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel (blid b) let pbags = EM.assocs $ lembed lvl efeat <- embedBenefit fleeVia aid pbags -- The advantage of targeting the tiles in vicinity of triggers is that -- triggers don't need to be pathable (and so AI doesn't bump into them -- by chance while walking elsewhere) and that many accesses to the tiles -- are more likely to be targeted by different AI actors (even starting -- from the same location), so there is less risk of clogging stairs and, -- OTOH, siege of stairs or escapes is more effective. bfs <- getCacheBfs aid let vicTrigger (cid, (p0, bag)) = map (\p -> (cid, (p, (p0, bag)))) (vicinityBounded rWidthMax rHeightMax p0) vicAll = concatMap vicTrigger efeat return $! let mix (benefit, ppbag) dist = let maxd = subtractBfsDistance maxBfsDistance apartBfs v = intToDouble $ maxd `div` (dist + 1) in (ceiling $ benefit * v, ppbag) in mapMaybe (\bpp@(_, (p, _)) -> mix bpp <$> accessBfs bfs p) vicAll -- | Check whether the actor has enough gear to go look for enemies. -- We assume weapons in equipment are better than any among organs -- or at least provide some essential diversity. -- Disabled if, due to doctrine, actors follow leader and so would -- repeatedly move towards and away from stairs at leader change, -- depending on current leader's gear. -- Number of items of a single kind is ignored, because variety is needed. condEnoughGearM :: MonadClientRead m => ActorId -> m Bool condEnoughGearM aid = do b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD let followDoctrine = gdoctrine fact `elem` [Ability.TFollow, Ability.TFollowNoItems] eqpAssocs <- getsState $ fullAssocs aid [CEqp] return $ not followDoctrine -- keep it lazy && (any (IA.checkFlag Ability.Meleeable . aspectRecordFull . snd) eqpAssocs || length eqpAssocs >= 3) unexploredDepth :: MonadClientRead m => Bool -> LevelId -> m Bool unexploredDepth !up !lidCurrent = do dungeon <- getsState sdungeon explored <- getsClient sexplored let allExplored = ES.size explored == EM.size dungeon unexploredD = let unex !lid = allExplored && not (null $ lescape $ dungeon EM.! lid) || ES.notMember lid explored || unexploredD lid in any unex . ascendInBranch dungeon up return $ unexploredD lidCurrent -- keep it lazy -- | Closest (wrt paths) items. closestItems :: MonadClient m => ActorId -> m [(Int, (Point, ItemBag))] closestItems aid = do body <- getsState $ getActorBody aid Level{lfloor, lbig} <- getLevel $ blid body factionD <- getsState sfactionD per <- getPerFid $ blid body let canSee p = ES.member p (totalVisible per) -- Don't consider items at any stash location that an actor stands over -- or can stand over, but it's out of our LOS. -- In case of the own stash, don't consider regardless of actors and LOS. -- Own stash items are already owned, enemy stash is already targetted -- and allied or neutral stashes with actors on top are unlikely -- to be vacated and cause AI to wonder around forever or look up, -- leave, return hopeful, find a guard, repeat. let stashes = map (second gstash) $ EM.assocs factionD stashToRemove :: (FactionId, Maybe (LevelId, Point)) -> [Point] stashToRemove (fid, Just (lid, pos)) | lid == blid body && (fid == bfid body || pos `EM.member` lbig || not (canSee pos)) = [pos] stashToRemove _ = [] stashesToRemove = ES.fromList $ concatMap stashToRemove stashes lfloorBarStashes = EM.withoutKeys lfloor stashesToRemove if EM.null lfloorBarStashes then return [] else do bfs <- getCacheBfs aid let mix pbag dist = let maxd = subtractBfsDistance maxBfsDistance apartBfs -- Beware of overflowing 32-bit integers. -- Here distance is the only factor influencing frequency. -- Whether item is desirable is checked later on. v = (maxd * 10) `div` (dist + 1) in (v, pbag) return $! mapMaybe (\(p, bag) -> mix (p, bag) <$> accessBfs bfs p) (EM.assocs lfloorBarStashes) -- | Closest (wrt paths) enemy actors. closestFoes :: MonadClient m => [(ActorId, Actor)] -> ActorId -> m [(Int, (ActorId, Actor))] closestFoes foes aid = case foes of [] -> return [] _ -> do bfs <- getCacheBfs aid let ds = mapMaybe (\x@(_, b) -> fmap (,x) (accessBfs bfs (bpos b))) foes return $! sortBy (comparing fst) ds -- | Closest (wrt paths) enemy or our unguarded stash locations. If it's ours, -- we want to guard it, it enemy, loot it. Neutral and friendly stashes -- not chased to avoid loops of bloodless takeovers. closestStashes :: MonadClient m => ActorId -> m [(Int, (FactionId, Point))] closestStashes aid = do COps{cocave} <- getsState scops factionD <- getsState sfactionD b <- getsState $ getActorBody aid lvl <- getLevel (blid b) oursExploring <- getsState $ oursExploringAssocs (bfid b) let fact = factionD EM.! bfid b spawnFreqs = CK.cactorFreq $ okind cocave $ lkind lvl hasGroup grp = fromMaybe 0 (lookup grp spawnFreqs) > 0 lvlSpawnsUs = any (hasGroup . fst) $ filter ((> 0) . snd) $ fgroups (gkind fact) qualifyStash (fid2, Faction{gstash}) = case gstash of Nothing -> Nothing Just (lid, pos) -> -- The condition below is more strict that in @updateTgt@ -- to avoid loops by changing target of actor displacing -- and walking over stash to @TStash@. if lid == blid b && (fid2 == bfid b && isNothing (posToBigLvl pos lvl) -- unguarded && (length oursExploring > 1 -- other actors able to explore || lvlSpawnsUs) -- or future spawned will be able || isFoe (bfid b) fact fid2) then Just (fid2, pos) else Nothing case mapMaybe qualifyStash $ EM.assocs factionD of [] -> return [] stashes -> do bfs <- getCacheBfs aid let ds = mapMaybe (\x@(_, pos) -> fmap (,x) (accessBfs bfs pos)) stashes return $! sortBy (comparing fst) ds oursExploringAssocs :: FactionId -> State -> [(ActorId, Actor)] oursExploringAssocs fid s = let f (!aid, !b) = bfid b == fid && not (bproj b) && bhp b > 0 -- dead can stay forever on a frozen level && (bwatch b `elem` [WSleep, WWake] -- if asleep, probably has walking skill normally; -- when left alone will wake up and guard or explore || let actorMaxSk = sactorMaxSkills s EM.! aid in Ability.getSk Ability.SkMove actorMaxSk > 0 || Ability.getSk Ability.SkMove actorMaxSk < -50) -- a hacky way to rule out tmp immobile in filter f $ EM.assocs $ sactorD s -- | Find the nearest walkable position in dark, if any. Deterministic, -- to let all friends gather up and defend in the same shelter. -- Ignore position underfoot. closestHideout :: MonadClient m => ActorId -> m (Maybe (Point, Int)) closestHideout aid = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel (blid b) bfs <- getCacheBfs aid let minHideout :: (Point, BfsDistance) -> Point -> BfsDistance -> (Point, BfsDistance) minHideout (pMin, distMin) p dist = if dist > minKnownBfs && dist < distMin && Tile.isHideout coTileSpeedup (lvl `at` p) then (p, dist) else (pMin, distMin) (p1, dist1) = PointArray.ifoldlA' minHideout (bpos b, maxBfsDistance) bfs return $! if p1 == bpos b -- possibly hideout underfoot; ignore then Nothing else Just (p1, subtractBfsDistance dist1 apartBfs) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/CommonM.hs0000644000000000000000000002116007346545000022060 0ustar0000000000000000-- | Common client monad operations. module Game.LambdaHack.Client.CommonM ( getPerFid, aidTgtToPos, makeLine , currentSkillsClient, pickWeaponClient , updateSalter, createSalter ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind (TileKind, isUknownSpace) import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | Get the current perception of a client. getPerFid :: MonadClientRead m => LevelId -> m Perception getPerFid lid = do fper <- getsClient sfper let assFail = error $ "no perception at given level" `showFailure` (lid, fper) return $! EM.findWithDefault assFail lid fper -- | Calculate the position of an actor's target. -- This matches @pathGoal@, but sometimes path is not defined. aidTgtToPos :: Maybe ActorId -> LevelId -> Maybe Target -> State -> Maybe Point aidTgtToPos _ _ Nothing _ = Nothing aidTgtToPos maid lidV (Just tgt) s = case tgt of TEnemy a -> let body = getActorBody a s in if blid body == lidV then Just (bpos body) else Nothing TNonEnemy a -> let body = getActorBody a s in if blid body == lidV then Just (bpos body) else Nothing TPoint _ lid p -> if lid == lidV then Just p else Nothing TVector v -> case maid of Nothing -> Nothing Just aid -> let COps{corule=RuleContent{rWidthMax, rHeightMax}} = scops s b = getActorBody aid s shifted = shiftBounded rWidthMax rHeightMax (bpos b) v in if shifted == bpos b && v /= Vector 0 0 then Nothing else Just shifted -- | Counts the number of steps until the projectile would hit a non-projectile -- actor or obstacle. Starts searching with the given eps and returns -- the first found eps for which the number reaches the distance between -- actor and target position, or Nothing if none can be found. -- Treats unknown tiles as walkable, but prefers known. makeLine :: Bool -> Actor -> Point -> Int -> COps -> Level -> Maybe Int makeLine onlyFirst body fpos epsOld cops lvl = let COps{coTileSpeedup} = cops dist = chessDist (bpos body) fpos calcScore :: Int -> Int calcScore eps = case bresenhamsLineAlgorithm eps (bpos body) fpos of Just bl -> let blDist = take (dist - 1) bl -- goal not checked; actor well aware noActor p = p == fpos || not (occupiedBigLvl p lvl) accessibleUnknown tpos = let tt = lvl `at` tpos in Tile.isWalkable coTileSpeedup tt || isUknownSpace tt accessU = all noActor blDist && all accessibleUnknown blDist accessFirst | not onlyFirst = False | otherwise = all noActor (take 1 blDist) && all accessibleUnknown (take 1 blDist) nUnknown = length $ filter (isUknownSpace . (lvl `at`)) blDist in if | accessU -> - nUnknown | accessFirst -> -10000 | otherwise -> minBound Nothing -> error $ "" `showFailure` (body, fpos, epsOld) tryLines :: Int -> (Maybe Int, Int) -> Maybe Int tryLines curEps (acc, _) | curEps == epsOld + dist = acc tryLines curEps (acc, bestScore) = let curScore = calcScore curEps newAcc = if curScore > bestScore then (Just curEps, curScore) else (acc, bestScore) in tryLines (curEps + 1) newAcc in if | dist <= 0 -> Nothing -- ProjectAimOnself | calcScore epsOld > minBound -> Just epsOld -- keep old | otherwise -> tryLines (epsOld + 1) (Nothing, minBound) -- find best -- @MonadStateRead@ would be enough, but the logic is sound only on client. currentSkillsClient :: MonadClientRead m => ActorId -> m Ability.Skills currentSkillsClient aid = do body <- getsState $ getActorBody aid side <- getsClient sside -- Newest Leader in sleader, not yet in sfactionD. mleader <- if bfid body == side then getsClient sleader else do fact <- getsState $ (EM.! bfid body) . sfactionD return $! gleader fact getsState $ actorCurrentSkills mleader aid -- keep it lazy -- Client has to choose the weapon based on its partial knowledge, -- because if server chose it, it would leak item discovery information. -- -- Note that currently the aspects of the target actor are not considered, -- because all weapons share the sum of all source actor aspects and only differ -- in damage (equally important for all targets) and effects (really hard -- to tell which is better for which target or even which is better -- for the same target, so it's random). If only individual weapon's +toHit -- was applied to the target, situation would be much more complex, -- which is precisely why we keep it as is and let the player make choices -- by equipping and unequipping weapons instead. Content should ensure -- that the rule of thumb (which AI uses) that more weapons is better -- should give good results almost always, at least at the start of the game, -- to limit micromanagement and to spare newbies. -- -- Note that situation is completely different with choosing projectiles -- against a particular foe, even before (potential) splash damage -- that hits multiple tagets comes into the equation. AI has to be very -- primitive and random here as well. pickWeaponClient :: MonadClient m => ActorId -> ActorId -> m (Maybe RequestTimed) pickWeaponClient source target = do eqpAssocs <- getsState $ kitAssocs source [CEqp] bodyAssocs <- getsState $ kitAssocs source [COrgan] actorSk <- currentSkillsClient source tb <- getsState $ getActorBody target let kitAssRaw = eqpAssocs ++ bodyAssocs kitAss = filter (IA.checkFlag Ability.Meleeable . aspectRecordFull . fst . snd) kitAssRaw benign itemFull = let arItem = aspectRecordFull itemFull in IA.checkFlag Ability.Benign arItem discoBenefit <- getsClient sdiscoBenefit strongest <- pickWeaponM False (Just discoBenefit) kitAss actorSk source case strongest of [] -> return Nothing (_, _, _, _, _, (itemFull, _)) : _ | benign itemFull && bproj tb -> return Nothing -- if strongest is benign, don't waste fun on a projectile iis@(ii1@(value1, hasEffect1, timeout1, _, _, (itemFull1, _)) : _) -> do let minIis = takeWhile (\(value, hasEffect, timeout, _, _, _) -> value == value1 && hasEffect == hasEffect1 && timeout == timeout1) iis -- Randomize only the no-timeout items. Others need to activate -- in the order shown in HUD and also not risk of only one always used. (_, _, _, _, iid, _) <- if timeout1 > 0 || itemSuspect itemFull1 then return ii1 else rndToAction $ oneOf minIis -- Prefer COrgan, to hint to the player to trash the equivalent CEqp item. let cstore = if isJust (lookup iid bodyAssocs) then COrgan else CEqp return $ Just $ ReqMelee target iid cstore updateSalter :: MonadClient m => LevelId -> [(Point, ContentId TileKind)] -> m () updateSalter lid pts = do COps{coTileSpeedup} <- getsState scops let pas = map (second $ toEnum . Tile.alterMinWalk coTileSpeedup) pts f = (PointArray.// pas) modifyClient $ \cli -> cli {salter = EM.adjust f lid $ salter cli} createSalter :: State -> AlterLid createSalter s = let COps{coTileSpeedup} = scops s f Level{ltile} = PointArray.mapA (toEnum . Tile.alterMinWalk coTileSpeedup) ltile in EM.map f $ sdungeon s LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/HandleAtomicM.hs0000644000000000000000000004747407346545000023200 0ustar0000000000000000-- | Handle atomic commands received by the client. module Game.LambdaHack.Client.HandleAtomicM ( MonadClientSetup(..) , cmdAtomicSemCli #ifdef EXPOSE_INTERNAL -- * Internal operations , updateInMeleeDueToActor, updateInMeleeDueToItem, updateInMeleeInDungeon , wipeBfsIfItemAffectsSkills, tileChangeAffectsBfs , createActor, destroyActor , addItemToDiscoBenefit, perception , discoverKind, discoverKindAndAspect, coverKind, coverAspectAndKind , discoverAspect, coverAspect , killExit #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Game.LambdaHack.Atomic import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.BfsM import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Preferences import Game.LambdaHack.Client.State import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.CaveKind as CK import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.TileKind (TileKind) import Game.LambdaHack.Definition.Defs -- | Client monad for saving a game. class MonadClient m => MonadClientSetup m where saveClient :: m () -- | Effect of atomic actions on client state. It is calculated -- with the global state from after the command is executed -- (except where the supplied @oldState@ is used). cmdAtomicSemCli :: MonadClientSetup m => State -> UpdAtomic -> m () {-# INLINE cmdAtomicSemCli #-} cmdAtomicSemCli oldState cmd = case cmd of UpdRegisterItems ais -> mapM_ (addItemToDiscoBenefit . fst) ais UpdCreateActor aid b ais -> createActor aid b ais UpdDestroyActor aid b _ -> destroyActor aid b True UpdCreateItem _ iid _ _ (CActor aid store) -> do wipeBfsIfItemAffectsSkills store aid addItemToDiscoBenefit iid updateInMeleeDueToItem aid store UpdCreateItem _ iid _ _ _ -> addItemToDiscoBenefit iid UpdDestroyItem _ _ _ _ (CActor aid store) -> do wipeBfsIfItemAffectsSkills store aid updateInMeleeDueToItem aid store UpdDestroyItem{} -> return () UpdSpotActor aid b -> do ais <- getsState $ getCarriedAssocsAndTrunk b createActor aid b ais UpdLoseActor aid b -> destroyActor aid b False UpdSpotItem _ iid _ (CActor aid store) -> do wipeBfsIfItemAffectsSkills store aid addItemToDiscoBenefit iid updateInMeleeDueToItem aid store UpdSpotItem _ iid _ _ -> addItemToDiscoBenefit iid UpdLoseItem _ _ _ (CActor aid store) -> do wipeBfsIfItemAffectsSkills store aid updateInMeleeDueToItem aid store UpdLoseItem{} -> return () UpdSpotItemBag _ (CActor aid store) bag -> do wipeBfsIfItemAffectsSkills store aid mapM_ addItemToDiscoBenefit $ EM.keys bag updateInMeleeDueToItem aid store UpdSpotItemBag _ _ bag -> mapM_ addItemToDiscoBenefit $ EM.keys bag UpdLoseItemBag _ (CActor aid store) _ -> do wipeBfsIfItemAffectsSkills store aid updateInMeleeDueToItem aid store UpdLoseItemBag{} -> return () UpdMoveActor aid _ _ -> do invalidateBfsAid aid -- other BFSes not invalidated, because distant actors may still move out -- of the way and close actors are considered when attempting to move -- and then BFS is invalidated, if needed. b <- getsState $ getActorBody aid updateInMeleeDueToActor b UpdWaitActor aid _fromW toW -> do -- So that we can later ignore such actors when updating targets -- and not risk they being pushed/displaced and targets getting illegal. when (toW == WSleep) $ modifyClient $ updateTarget aid (const Nothing) b <- getsState $ getActorBody aid updateInMeleeDueToActor b -- @bwatch@ checked in several places UpdDisplaceActor source target -> do invalidateBfsAid source invalidateBfsAid target -- other BFSes not invalidated, because distant actors may still move out -- of the way and close actors are considered when attempting to move -- and then BFS is invalidated, if needed. sb <- getsState $ getActorBody source -- At least one of these is not a projectile and both move, so update. insertInMeleeM (blid sb) UpdMoveItem _ _ aid s1 s2 -> do wipeBfsIfItemAffectsSkills s1 aid wipeBfsIfItemAffectsSkills s2 aid updateInMeleeDueToItem aid s1 updateInMeleeDueToItem aid s2 UpdRefillHP _ 0 -> return () UpdRefillHP aid delta -> do b <- getsState $ getActorBody aid unless (bproj b || signum (bhp b) -- new HP == signum (bhp b - delta)) $ -- old HP insertInMeleeM (blid b) -- @bhp@ checked in several places UpdRefillCalm{} -> return () UpdTrajectory{} -> return () UpdQuitFaction{} -> return () UpdSpotStashFaction{} -> return () UpdLoseStashFaction{} -> return () UpdLeadFaction fid source target -> do side <- getsClient sside when (side == fid) $ do mleader <- getsClient sleader let !_A = assert (mleader == source -- somebody changed the leader for us || mleader == target -- we changed the leader ourselves `blame` "unexpected leader" `swith` (cmd, mleader)) () modifyClient $ \cli -> cli {_sleader = target} UpdDiplFaction{} -> -- Depends on who is a foe as opposed to a neutral actor. updateInMeleeInDungeon UpdAutoFaction{} -> do -- Regaining control of faction cancels some --stopAfter*. -- This is really a UI client issue, but is in general client state -- to make it simpler to set this via commandline. modifyClient $ \cli -> cli {soptions = (soptions cli) { sstopAfterSeconds = Nothing , sstopAfterFrames = Nothing }} -- @condBFS@ depends on the setting we change here (e.g., smarkSuspect). invalidateBfsAll UpdRecordKill{} -> return () UpdDoctrineFaction{} -> do -- Clear all targets except the leader's. mleader <- getsClient sleader mtgt <- case mleader of Nothing -> return Nothing Just leader -> getsClient $ EM.lookup leader . stargetD modifyClient $ \cli -> let stargetD | Just tgt <- mtgt , Just leader <- mleader = EM.singleton leader tgt | otherwise = EM.empty in cli {stargetD} UpdAlterTile lid p fromTile toTile -> do updateSalter lid [(p, toTile)] cops <- getsState scops let lvl = (EM.! lid) . sdungeon $ oldState t = lvl `at` p let !_A = assert (t == fromTile) () when (tileChangeAffectsBfs cops fromTile toTile) $ invalidateBfsLid lid UpdAlterExplorable{} -> return () UpdAlterGold{} -> return () UpdSearchTile aid p toTile -> do COps{cotile} <- getsState scops b <- getsState $ getActorBody aid let lid = blid b updateSalter lid [(p, toTile)] cops <- getsState scops let lvl = (EM.! lid) . sdungeon $ oldState t = lvl `at` p let !_A = assert (Just t == Tile.hideAs cotile toTile) () -- The following check is needed even if we verity in content -- that searching doesn't change clarity and light of tiles, -- because it modifies skill needed to alter the tile and even -- walkability and changeability. when (tileChangeAffectsBfs cops t toTile) $ invalidateBfsLid lid UpdHideTile{} -> return () UpdSpotTile lid ts -> do updateSalter lid ts cops <- getsState scops let lvl = (EM.! lid) . sdungeon $ oldState affects (p, toTile) = let fromTile = lvl `at` p in tileChangeAffectsBfs cops fromTile toTile bs = map affects ts when (or bs) $ invalidateBfsLid lid UpdLoseTile lid ts -> do updateSalter lid ts invalidateBfsLid lid -- from known to unknown tiles UpdSpotEntry{} -> return () UpdLoseEntry{} -> return () UpdAlterSmell{} -> return () UpdSpotSmell{} -> return () UpdLoseSmell{} -> return () UpdTimeItem{} -> return () UpdAgeGame{} -> return () UpdUnAgeGame{} -> return () UpdDiscover _ iid _ _ -> do item <- getsState $ getItemBody iid case jkind item of IdentityObvious _ik -> discoverAspect iid IdentityCovered ix _ik -> if ix `EM.notMember` sdiscoKind oldState then discoverKindAndAspect ix else discoverAspect iid UpdCover _ iid _ _ -> do item <- getsState $ getItemBody iid newState <- getState case jkind item of IdentityObvious _ik -> coverAspect iid IdentityCovered ix _ik -> if ix `EM.member` sdiscoKind newState then coverAspectAndKind ix else coverAspect iid UpdDiscoverKind _c ix _ik -> discoverKind ix UpdCoverKind _c ix _ik -> coverKind ix UpdDiscoverAspect _c iid _arItem -> discoverAspect iid UpdCoverAspect _c iid _arItem -> coverAspect iid UpdDiscoverServer{} -> error "server command leaked to client" UpdCoverServer{} -> error "server command leaked to client" UpdPerception lid outPer inPer -> perception lid outPer inPer UpdRestart side sfper _ scurChal soptionsNew srandom -> do COps{cocave} <- getsState scops fact <- getsState $ (EM.! side) . sfactionD snxtChal <- getsClient snxtChal smarkSuspect <- getsClient smarkSuspect stabs <- getsClient stabs soptionsOld <- getsClient soptions let h lvl = CK.clabyrinth (okind cocave $ lkind lvl) && not (fhasGender $ gkind fact) -- Not to burrow through a labyrinth instead of leaving it for -- the human player and to prevent AI losing time there instead -- of congregating at exits. sexplored <- getsState $ EM.keysSet . EM.filter h . sdungeon let cli = emptyStateClient side putClient cli { sexplored -- , sundo = [UpdAtomic cmd] , sfper , srandom , scurChal , snxtChal , smarkSuspect , soptions = soptionsNew {snoAnim = -- persist @snoAnim@ between games snoAnim soptionsOld `mplus` snoAnim soptionsNew} , stabs } salter <- getsState createSalter modifyClient $ \cli1 -> cli1 {salter} updateInMeleeInDungeon UpdRestartServer{} -> return () UpdResume _side sfperNew -> do #ifdef WITH_EXPENSIVE_ASSERTIONS sfperOld <- getsClient sfper let !_A = assert (sfperNew == sfperOld `blame` (_side, sfperNew, sfperOld)) () #endif modifyClient $ \cli -> cli {sfper = sfperNew} -- just in case salter <- getsState createSalter -- because space saved by not storing it modifyClient $ \cli -> cli {salter} UpdResumeServer{} -> return () UpdKillExit _fid -> killExit UpdWriteSave -> saveClient UpdHearFid{} -> return () UpdMuteMessages{} -> return () updateInMeleeDueToActor :: MonadClient m => Actor -> m () updateInMeleeDueToActor b = unless (bproj b) $ insertInMeleeM (blid b) updateInMeleeDueToItem :: MonadClient m => ActorId -> CStore -> m () updateInMeleeDueToItem aid store = when (store `elem` [CEqp, COrgan]) $ do b <- getsState $ getActorBody aid updateInMeleeDueToActor b updateInMeleeInDungeon :: MonadClient m => m () updateInMeleeInDungeon = do dungeon <- getsState sdungeon mapM_ insertInMeleeM $ EM.keys dungeon -- For now, only checking the stores. wipeBfsIfItemAffectsSkills :: MonadClient m => CStore -> ActorId -> m () wipeBfsIfItemAffectsSkills store aid = when (store `elem` [CEqp, COrgan]) $ invalidateBfsAid aid tileChangeAffectsBfs :: COps -> ContentId TileKind -> ContentId TileKind -> Bool tileChangeAffectsBfs COps{coTileSpeedup} fromTile toTile = Tile.alterMinWalk coTileSpeedup fromTile /= Tile.alterMinWalk coTileSpeedup toTile createActor :: MonadClient m => ActorId -> Actor -> [(ItemId, Item)] -> m () createActor aid b ais = do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD let affect3 tap@TgtAndPath{..} = case tapTgt of TPoint (TEnemyPos a) _ _ | a == aid -> let tgt | isFoe side fact (bfid b) = TEnemy a -- still a foe | otherwise = TPoint TKnown (blid b) (bpos b) in TgtAndPath tgt Nothing _ -> tap modifyClient $ \cli -> cli {stargetD = EM.map affect3 (stargetD cli)} mapM_ (addItemToDiscoBenefit . fst) ais unless (bproj b) $ invalidateBfsPathLid b updateInMeleeDueToActor b destroyActor :: MonadClient m => ActorId -> Actor -> Bool -> m () destroyActor aid b destroy = do when destroy $ -- if vanishes for a moment only, keep target modifyClient $ \cli -> cli {stargetD = EM.delete aid $ stargetD cli} -- gc -- Here, among others, (local) flee time of an actor changing level is reset. modifyClient $ \cli -> cli { sbfsD = EM.delete aid $ sbfsD cli , sfleeD = EM.delete aid $ sfleeD cli } localTime <- getsState $ getLocalTime $ blid b fleeD <- getsClient sfleeD let recentlyFled aid3 = maybe False (\(_, time) -> timeRecent5 localTime time) (aid3 `EM.lookup` fleeD) dummyTarget = TPoint TKnown (blid b) (bpos b) affect aid3 tgt = case tgt of TEnemy a | a == aid -> if destroy || recentlyFled aid3 -- if fleeing, don't chase the enemy soon after; -- unfortunately, the enemy also won't be recorded -- in case he gets out of sight, in order to avoid -- him when fleeing again, but all enemies should be -- recorded in such a case, so not a big difference then -- If *really* nothing more interesting, the actor will -- go to last known location to perhaps find other foes. dummyTarget else -- If enemy only hides (or we stepped behind obstacle) find him. TPoint (TEnemyPos a) (blid b) (bpos b) TNonEnemy a | a == aid -> dummyTarget _ -> tgt affect3 aid3 TgtAndPath{..} = let newMPath = case tapPath of Just AndPath{pathGoal} | pathGoal /= bpos b -> Nothing _ -> tapPath -- foe slow enough, so old path good in TgtAndPath (affect aid3 tapTgt) newMPath modifyClient $ \cli -> cli {stargetD = EM.mapWithKey affect3 (stargetD cli)} unless (bproj b) $ invalidateBfsPathLid b updateInMeleeDueToActor b addItemToDiscoBenefit :: MonadClient m => ItemId -> m () addItemToDiscoBenefit iid = do cops <- getsState scops discoBenefit <- getsClient sdiscoBenefit case EM.lookup iid discoBenefit of Just{} -> return () -- already there, with real or provisional aspect record, -- but we haven't learned anything new about the item Nothing -> do side <- getsClient sside factionD <- getsState sfactionD itemFull <- getsState $ itemToFull iid let benefit = totalUsefulness cops side factionD itemFull modifyClient $ \cli -> cli {sdiscoBenefit = EM.insert iid benefit (sdiscoBenefit cli)} perception :: MonadClient m => LevelId -> Perception -> Perception -> m () perception lid outPer inPer = do -- Clients can't compute FOV on their own, because they don't know -- if unknown tiles are clear or not. Server would need to send -- info about properties of unknown tiles, which complicates -- and makes heavier the most bulky data set in the game: tile maps. -- Note we assume, but do not check that @outPer@ is contained -- in current perception and @inPer@ has no common part with it. -- It would make the already very costly operation even more expensive. {- perOld <- getPerFid lid -- Check if new perception is already set in @cmdAtomicFilterCli@ -- or if we are doing undo/redo, which does not involve filtering. -- The data structure is strict, so the cheap check can't be any simpler. let interAlready per = Just $ totalVisible per `ES.intersection` totalVisible perOld unset = maybe False ES.null (interAlready inPer) || maybe False (not . ES.null) (interAlready outPer) when unset $ do -} let adj Nothing = error $ "no perception to alter" `showFailure` lid adj (Just per) = Just $ addPer (diffPer per outPer) inPer f = EM.alter adj lid modifyClient $ \cli -> cli {sfper = f (sfper cli)} discoverKind :: MonadClient m => ItemKindIx -> m () discoverKind = discoverKindAndAspect discoverKindAndAspect :: MonadClient m => ItemKindIx -> m () discoverKindAndAspect ix = do cops <- getsState scops -- Wipe out BFS, because the player could potentially learn that his items -- affect his actors' skills relevant to BFS. invalidateBfsAll side <- getsClient sside factionD <- getsState sfactionD itemToF <- getsState $ flip itemToFull let benefit iid = totalUsefulness cops side factionD (itemToF iid) itemIxMap <- getsState $ (EM.! ix) . sitemIxMap -- Possibly overwrite earlier, provisional benefits. forM_ (ES.elems itemIxMap) $ \iid -> modifyClient $ \cli -> cli {sdiscoBenefit = EM.insert iid (benefit iid) (sdiscoBenefit cli)} coverKind :: ItemKindIx -> m () coverKind = coverAspectAndKind coverAspectAndKind :: ItemKindIx -> m () coverAspectAndKind _ix = undefined discoverAspect :: MonadClient m => ItemId -> m () discoverAspect iid = do cops <- getsState scops -- Wipe out BFS, because the player could potentially learn that his items -- affect his actors' skills relevant to BFS. invalidateBfsAll side <- getsClient sside factionD <- getsState sfactionD itemFull <- getsState $ itemToFull iid let benefit = totalUsefulness cops side factionD itemFull -- Possibly overwrite earlier, provisional benefits. modifyClient $ \cli -> cli {sdiscoBenefit = EM.insert iid benefit (sdiscoBenefit cli)} coverAspect :: ItemId -> m () coverAspect _iid = undefined killExit :: MonadClient m => m () killExit = do side <- getsClient sside debugPossiblyPrint $ "Client" <+> tshow side <+> "quitting." modifyClient $ \cli -> cli {squit = True} -- Verify that the not saved caches are equal to future reconstructed. -- Otherwise, save/restore would change game state. sactorMaxSkills2 <- getsState sactorMaxSkills salter <- getsClient salter sbfsD <- getsClient sbfsD alter <- getsState createSalter actorMaxSkills <- getsState maxSkillsInDungeon let f aid = do (canMove, alterSkill) <- condBFS aid bfsArr <- createBfs canMove alterSkill aid let bfsPath = EM.empty return (aid, BfsAndPath bfsArr bfsPath) actorD <- getsState sactorD lbfsD <- mapM f $ EM.keys actorD -- Some freshly generated bfses are not used for comparison, but at least -- we check they don't violate internal assertions themselves. Hence the bang. let bfsD = EM.fromDistinctAscList lbfsD g BfsInvalid !_ = True g _ BfsInvalid = False g (BfsAndPath bfsArr1 _) (BfsAndPath bfsArr2 _) = bfsArr1 == bfsArr2 subBfs = EM.isSubmapOfBy g let !_A1 = assert (salter == alter `blame` "wrong accumulated salter on side" `swith` (side, salter, alter)) () !_A2 = assert (sactorMaxSkills2 == actorMaxSkills `blame` "wrong accumulated sactorMaxSkills on side" `swith` (side, sactorMaxSkills2, actorMaxSkills)) () !_A3 = assert (sbfsD `subBfs` bfsD `blame` "wrong accumulated sbfsD on side" `swith` (side, sbfsD, bfsD)) () return () LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/HandleResponseM.hs0000644000000000000000000000605407346545000023547 0ustar0000000000000000{-# LANGUAGE FlexibleContexts #-} -- | Semantics of responses sent by the server to clients. module Game.LambdaHack.Client.HandleResponseM ( MonadClientAtomic(..), MonadClientWriteRequest(..) , handleResponse ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Atomic (UpdAtomic) import Game.LambdaHack.Client.AI import Game.LambdaHack.Client.HandleAtomicM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.Response import Game.LambdaHack.Client.UI import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State -- | Monad for executing atomic game state transformations on a client. class MonadClient m => MonadClientAtomic m where -- | Execute an atomic update that changes the client's 'State'. execUpdAtomic :: UpdAtomic -> m () -- | Put state that is intended to be the result of performing -- an atomic update by the server on its copy of the client's 'State'. execPutState :: State -> m () -- | Client monad in which one can send requests to the client. class MonadClient m => MonadClientWriteRequest m where sendRequestAI :: RequestAI -> m () sendRequestUI :: RequestUI -> m () clientHasUI :: m Bool -- | Handle server responses. -- -- Note that for clients communicating with the server over the net, -- @RespUpdAtomicNoState@ should be used, because executing a single command -- is cheaper than sending the whole state over the net. -- However, for the standalone exe mode, with clients in the same process -- as the server, a pointer to the state set with @execPutState@ is cheaper. handleResponse :: ( MonadClientSetup m , MonadClientUI m , MonadClientAtomic m , MonadClientWriteRequest m ) => Response -> m () handleResponse cmd = case cmd of RespUpdAtomic newState cmdA -> do oldState <- getState execPutState newState cmdAtomicSemCli oldState cmdA hasUI <- clientHasUI when hasUI $ watchRespUpdAtomicUI cmdA RespUpdAtomicNoState cmdA -> do oldState <- getState execUpdAtomic cmdA cmdAtomicSemCli oldState cmdA hasUI <- clientHasUI when hasUI $ watchRespUpdAtomicUI cmdA RespQueryAI aid -> do cmdC <- queryAI aid sendRequestAI cmdC RespSfxAtomic sfx -> watchRespSfxAtomicUI sfx RespQueryUIunderAI -> do req <- queryUIunderAI sendRequestUI req RespQueryUI -> do -- Stop displaying the prompt, if any. modifySession $ \sess -> sess {sreqDelay = ReqDelayNot} sreqPending <- getsSession sreqPending req <- case sreqPending of Nothing -> do -- Server sending @RespQueryUI@ means that it's sent everything -- and is now ready to receive a request ASAP, so no point polling -- and instead query the player repeatedly until request generated. let loop = do mreq <- queryUI maybe loop pure mreq loop Just reqPending -> do modifySession $ \sess -> sess {sreqPending = Nothing} return reqPending sendRequestUI req LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/LoopM.hs0000644000000000000000000002504307346545000021545 0ustar0000000000000000{-# LANGUAGE FlexibleContexts #-} -- | The main loop of the client, processing human and computer player -- moves turn by turn. module Game.LambdaHack.Client.LoopM ( MonadClientReadResponse(..) , loopCli #ifdef EXPOSE_INTERNAL -- * Internal operations , initAI, initUI, loopAI, longestDelay, loopUI #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Data.Time.Clock import Data.Time.Clock.POSIX import Game.LambdaHack.Atomic import Game.LambdaHack.Client.HandleAtomicM import Game.LambdaHack.Client.HandleResponseM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Response import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State -- | Client monad in which one can receive responses from the server. class MonadClient m => MonadClientReadResponse m where receiveResponse :: m Response initAI :: MonadClient m => m () initAI = do side <- getsClient sside debugPossiblyPrint $ "AI client" <+> tshow side <+> "initializing." initUI :: (MonadClient m, MonadClientUI m) => CCUI -> m () initUI sccui@CCUI{coscreen} = do side <- getsClient sside soptions <- getsClient soptions debugPossiblyPrint $ "UI client" <+> tshow side <+> "initializing." -- Start the frontend. schanF <- chanFrontend coscreen soptions modifySession $ \sess -> sess {schanF, sccui} -- | The main game loop for an AI or UI client. It receives responses from -- the server, changes internal client state accordingly, analyzes -- ensuing human or AI commands and sends resulting requests to the server. -- Depending on whether it's an AI or UI client, it sends AI or human player -- requests. -- -- The loop is started in client state that is empty except for -- the @sside@ and @seps@ fields, see 'emptyStateClient'. loopCli :: ( MonadClientSetup m , MonadClientUI m , MonadClientAtomic m , MonadClientReadResponse m , MonadClientWriteRequest m ) => CCUI -> UIOptions -> ClientOptions -> Bool -> m () loopCli ccui sUIOptions clientOptions startsNewGame = do modifyClient $ \cli -> cli {soptions = clientOptions} side <- getsClient sside hasUI <- clientHasUI if not hasUI then initAI else initUI ccui let cliendKindText = if not hasUI then "AI" else "UI" debugPossiblyPrint $ cliendKindText <+> "client" <+> tshow side <+> "starting 1/4." -- Warning: state and client state are invalid here, e.g., sdungeon -- and sper are empty. restored <- if startsNewGame && not hasUI then return False else do restoredG <- tryRestore case restoredG of Just (cli, msess)-> do -- Restore game. case msess of Just sess | hasUI -> do -- Preserve almost everything from the saved session. -- Renew the communication channel to the newly spawned frontend -- and get the possibly updated UI content and UI options. schanF <- getsSession schanF sccui <- getsSession sccui putSession $ sess {schanF, sccui, sUIOptions} _ -> return () if startsNewGame then -- Don't restore client state, due to new game starting right now, -- which means everything will be overwritten soon anyway -- via an @UpdRestart@ command (instead of @UpdResume@). return False else do -- We preserve the client state from savefile except for the single -- option that can be overwritten on commandline. let noAnim = fromMaybe False $ snoAnim $ soptions cli putClient cli {soptions = clientOptions {snoAnim = Just noAnim}} return True Nothing -> return False debugPossiblyPrint $ cliendKindText <+> "client" <+> tshow side <+> "starting 2/4." -- At this point @ClientState@ not overriten dumbly and @State@ valid. tabA <- createTabBFS tabB <- createTabBFS modifyClient $ \cli -> cli {stabs = (tabA, tabB)} cmd1 <- receiveResponse debugPossiblyPrint $ cliendKindText <+> "client" <+> tshow side <+> "starting 3/4." case (restored, startsNewGame, cmd1) of (True, False, RespUpdAtomic _ UpdResume{}) -> return () (True, True, RespUpdAtomic _ UpdRestart{}) -> when hasUI $ clientPrintUI "Ignoring an old savefile and starting a new game." (False, False, RespUpdAtomic _ UpdResume{}) -> error $ "Savefile of client " ++ show side ++ " not usable." `showFailure` () (False, True, RespUpdAtomic _ UpdRestart{}) -> return () (True, False, RespUpdAtomicNoState UpdResume{}) -> undefined (True, True, RespUpdAtomicNoState UpdRestart{}) -> when hasUI $ clientPrintUI "Ignoring an old savefile and starting a new game." (False, False, RespUpdAtomicNoState UpdResume{}) -> error $ "Savefile of client " ++ show side ++ " not usable." `showFailure` () (False, True, RespUpdAtomicNoState UpdRestart{}) -> return () _ -> error $ "unexpected command" `showFailure` (side, restored, cmd1) handleResponse cmd1 -- State and client state now valid. debugPossiblyPrint $ cliendKindText <+> "client" <+> tshow side <+> "starting 4/4." if hasUI then loopUI 0 else loopAI side2 <- getsClient sside debugPossiblyPrint $ cliendKindText <+> "client" <+> tshow side2 <+> "(initially" <+> tshow side <> ") stopped." loopAI :: ( MonadClientSetup m , MonadClientUI m , MonadClientAtomic m , MonadClientReadResponse m , MonadClientWriteRequest m ) => m () loopAI = do cmd <- receiveResponse handleResponse cmd quit <- getsClient squit unless quit loopAI -- | Alarm after this many seconds without server querying us for a command. longestDelay :: POSIXTime longestDelay = secondsToNominalDiffTime 1 -- really high to accomodate slow browsers -- | The argument is the time of last UI query from the server. -- After @longestDelay@ seconds past this date, the client considers itself -- ignored and displays a warning and, at a keypress, gives -- direct control to the player, no longer waiting for the server -- to prompt it to do so. loopUI :: ( MonadClientSetup m , MonadClientUI m , MonadClientAtomic m , MonadClientReadResponse m , MonadClientWriteRequest m ) => POSIXTime -> m () loopUI timeSinceLastQuery = do sreqPending <- getsSession sreqPending sreqDelay <- getsSession sreqDelay sregainControl <- getsSession sregainControl keyPressed <- anyKeyPressed let alarm = timeSinceLastQuery > longestDelay if | not alarm -- no alarm starting right now && -- no need to mark AI for control regain ASAP: (sreqDelay == ReqDelayNot -- no old alarm still in effect || sregainControl -- AI control already marked for regain || (not keyPressed -- player does not insist by keypress && sreqDelay /= ReqDelayHandled)) -> do -- or by hack timeBefore <- liftIO getPOSIXTime cmd <- receiveResponse timeAfter <- liftIO getPOSIXTime handleResponse cmd -- @squit@ can be changed only in @handleResponse@, so this is the only -- place where it needs to be checked. quit <- getsClient squit unless quit $ case cmd of RespQueryUI -> loopUI 0 RespQueryUIunderAI -> loopUI $ succ longestDelay -- permit fast AI control regain _ -> do when (isJust sreqPending) $ do msgAdd MsgActionAlert "Warning: server updated game state after current command was issued by the client but before it was received by the server." -- This measures only the server's delay. loopUI $ timeSinceLastQuery - timeBefore + timeAfter | not sregainControl && (keyPressed || sreqDelay == ReqDelayHandled || isJust sreqPending) -> do -- ignore alarm if to be handled by AI control regain code elsewhere -- Checking for special case for UI under AI control, because the default -- behaviour is in this case too alarming for the player, especially -- during the insert coin demo before game is started. side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD if gunderAI fact then -- Mark for immediate control regain from AI. modifySession $ \sess -> sess {sregainControl = True} else do -- should work fine even if UI faction has no leader ATM -- The keys mashed to make UI accessible are not considered a command. resetPressedKeys -- Stop displaying the prompt, if any, but keep UI simple. modifySession $ \sess -> sess {sreqDelay = ReqDelayHandled} let msg = if isNothing sreqPending then "Server delayed asking us for a command. Regardless, UI is made accessible. Press ESC twice to listen to server some more." else "Server delayed receiving a command from us. The command is cancelled. Issue a new one." msgAdd MsgActionAlert msg mreqNew <- queryUI msgAdd MsgPromptGeneric "Your client is listening to the server again." pushReportFrame -- TODO: once this is really used, verify that if a request -- overwritten, nothing breaks due to some things in our ClientState -- and SessionUI (but fortunately not in State nor ServerState) -- already set as if it was performed. modifySession $ \sess -> sess {sreqPending = mreqNew} -- Now relax completely. modifySession $ \sess -> sess {sreqDelay = ReqDelayNot} -- We may yet not know if server is ready, but perhaps server -- tried hard to contact us while we took control and now it sleeps -- for a bit, so let's give it the benefit of the doubt -- and a slight pause before we alarm the player again. loopUI 0 | otherwise -> do -- We know server is not ready. modifySession $ \sess -> sess {sreqDelay = ReqDelayAlarm} -- We take a slight pause during which we display encouragement -- to press a key and we receive game state changes. -- The pause is cut short by any keypress, so it does not -- make UI reaction any less snappy (animations do, but that's fine). loopUI 0 LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/MonadClient.hs0000644000000000000000000000704607346545000022717 0ustar0000000000000000-- | Basic client monad and related operations. module Game.LambdaHack.Client.MonadClient ( -- * Basic client monads MonadClientRead ( getsClient , liftIO -- exposed only to be implemented, not used ) , MonadClient(modifyClient) -- * Assorted primitives , getClient, putClient , debugPossiblyPrint, createTabBFS, dumpTextFile, rndToAction , condInMeleeM, insertInMeleeM ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Exception as Ex import Control.Monad.ST.Strict (stToIO) import qualified Control.Monad.Trans.State.Strict as St import qualified Data.EnumSet as ES import qualified Data.Primitive.PrimArray as PA import qualified Data.Text.IO as T import System.Directory import System.FilePath import System.IO (hFlush, stdout) import Game.LambdaHack.Client.State import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.File import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Core.Random -- | Monad for reading client state. class MonadStateRead m => MonadClientRead m where getsClient :: (StateClient -> a) -> m a -- We do not provide a MonadIO instance, so that outside -- nobody can subvert the action monads by invoking arbitrary IO. liftIO :: IO a -> m a -- | Monad for writing to client state. class MonadClientRead m => MonadClient m where modifyClient :: (StateClient -> StateClient) -> m () getClient :: MonadClientRead m => m StateClient getClient = getsClient id putClient :: MonadClient m => StateClient -> m () putClient s = modifyClient (const s) debugPossiblyPrint :: MonadClient m => Text -> m () debugPossiblyPrint t = do sdbgMsgCli <- getsClient $ sdbgMsgCli . soptions when sdbgMsgCli $ liftIO $ do T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough hFlush stdout createTabBFS :: MonadClient m => m (PA.PrimArray PointI) createTabBFS = do COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops liftIO $ stToIO $ do tabAMutable <- PA.newPrimArray (rWidthMax * rHeightMax) -- always enough PA.unsafeFreezePrimArray tabAMutable dumpTextFile :: MonadClientRead m => Text -> FilePath -> m FilePath dumpTextFile t filename = liftIO $ do dataDir <- appDataDir tryCreateDir dataDir let path = dataDir filename Ex.handle (\(_ :: Ex.IOException) -> return ()) $ removeFile path tryWriteFile path t return path -- | Invoke pseudo-random computation with the generator kept in the state. rndToAction :: MonadClient m => Rnd a -> m a rndToAction r = do gen1 <- getsClient srandom let (a, gen2) = St.runState r gen1 modifyClient $ \cli -> cli {srandom = gen2} return a condInMeleeM :: MonadClientRead m => LevelId -> m Bool condInMeleeM lid = do condInMelee <- getsClient scondInMelee return $! lid `ES.member` condInMelee insertInMeleeM :: MonadClient m => LevelId -> m () insertInMeleeM lid = do side <- getsClient sside actorMaxSkills <- getsState sactorMaxSkills inM <- getsState $ inMelee actorMaxSkills side lid modifyClient $ \cli -> -- cli {scondInMelee = ES.alterF (const inM) lid $ scondInMelee cli} cli {scondInMelee = if inM then ES.insert lid $ scondInMelee cli else ES.delete lid $ scondInMelee cli} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/Preferences.hs0000644000000000000000000010403107346545000022753 0ustar0000000000000000-- | Actor preferences for targets and actions, based on actor aspects. module Game.LambdaHack.Client.Preferences ( totalUsefulness #ifdef EXPOSE_INTERNAL -- * Internal operations , effectToBenefit , averageTurnValue, avgItemDelay, avgItemLife, durabilityMult , organBenefit, recBenefit, fakeItem , aspectToBenefit, capStat, aspectRecordToBenefit #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.Flavour -- | How much AI benefits from applying the effect. -- The first component is benefit when applied to self, the second -- is benefit (preferably negative) when applied to enemy (via melee). -- This represents benefit from using the effect every @avgItemDelay@ turns, -- so if the item is not durable, the value is adjusted down elsewhere. -- The benefit includes the drawback of having to use the actor's turn, -- except when there is battle and item is a weapon and so there is usually -- nothing better to do than to melee, or when the actor is stuck or idle -- or laying in wait or luring an enemy from a safe distance. -- So there is less than @averageTurnValue@ included in each benefit, -- so in case when turn is not spent, e.g, periodic activation or conditions, -- the difference in value is only slight. effectToBenefit :: COps -> FactionId -> FactionDict -> IK.Effect -> (Double, Double) effectToBenefit cops fid factionD eff = let delta x = (x, x) in case eff of IK.Burn d -> delta $ -(min 1000 $ 10 * Dice.meanDice d) IK.Explode IK.S_SINGLE_SPARK -> delta (-1) -- probing and flavour IK.Explode IK.S_SPARK -> delta (-9) -- small, to not affect weapon order IK.Explode IK.S_FRAGRANCE -> (1, -5) -- situational IK.Explode _ -> -- There is a risk the explosion is focused and harmful to self -- or not focused and beneficial to nearby foes, but not to self. -- It's too costly to analyze, so we assume applying an exploding -- item is a bad idea and it's better to project it at foes. -- Due to this assumption, healing explosives should be wrapped -- in @OnSmash@, or else they are counted as an incentive for throwing -- an item at foes, which in that case is counterproductive. delta (-50) -- not too low so that S_INK_SAC used by AI IK.RefillHP p -> delta $ if p > 0 then min 2000 (20 * intToDouble p) else max (-1000) (10 * intToDouble p) -- one HP healed is worth a bit more than one HP dealt to enemy, -- because if the actor survives, he may deal damage many times; -- however, AI is mostly for non-heroes that fight in suicidal crowds, -- so the two values are kept close enough to maintain berserk approach IK.RefillCalm p -> ( if p > 0 then min 100 (intToDouble p) -- this may cause ice to be attractive to AI, -- but it doesn't trigger it due to no @ConsideredByAI@ else if p >= -5 then max (-100) (intToDouble p) else max (-1500) (15 * intToDouble p) -- big Calm drains are incredibly dangerous, so don't be stupid -- and don't self-inflict them, particularly if you are an intelligent -- high-HP actor, which is likely if you collect and apply items , if p > 0 then min 100 (intToDouble p) else max (-500) (5 * intToDouble p) ) -- quite a powerful weapon, especially against high-HP foes IK.Dominate -> (0, -100) -- I obtained an actor with, say 10HP, -- worth 200, and enemy lost him, another 100; -- divided by 3, because impression needed first IK.Impress -> (0, -20) -- this causes heroes to waste a crucial resource -- but makes aliens more aggresive than defensive; -- also, smart use is hardcoded in AI action choice IK.PutToSleep -> (-10, -50) -- can affect friends, but more often enemies IK.Yell -> (-1, -2) -- usually uncontrollably wakes up enemies, so bad IK.Summon grp d -> -- contrived by not checking if enemies also control -- that group; safe for normal dungeon crawl content; -- not correct for symmetric scenarios, but let it be let ben = Dice.meanDice d * 200 -- the new actor can have, say, 10HP fact = factionD EM.! fid friendlyHasGrp fid2 = isFriend fid fact fid2 && fromMaybe 0 (lookup grp $ fgroups $ gkind $ factionD EM.! fid2) > 0 in -- Prefer applying summoning items to flinging them; the actor gets -- spawned further from foes, but it's more robust. if any friendlyHasGrp $ EM.keys factionD then (ben, -1) else (-ben * 3, 1) -- the foe may spawn during battle and gang up IK.Ascend{} -> (0, 0) -- only change levels sensibly, in teams, and don't remove enemy too far, -- he may be easy to kill and may have essential loot IK.Escape{} -> (-9999, 9999) -- even if can escape, loots first and then -- handles escape as a special case -- The following two are expensive, because they ofen activate -- while in melee, in which case each turn is worth x HP, where x -- is the average effective weapon damage in the game, which would -- be ~5. (Plus a huge risk factor for any non-spawner faction.) -- So, each turn in battle is worth ~100. And on average, in and out -- of battle, let's say each turn is worth ~10. IK.Paralyze d -> delta $ -20 * Dice.meanDice d -- clips IK.ParalyzeInWater d -> delta $ -10 * Dice.meanDice d -- clips; resistable IK.InsertMove d -> delta $ 10 * Dice.meanDice d -- turns IK.Teleport{} -> (-9, -1) -- for self, don't derail exploration -- for foes, fight with one less at a time IK.CreateItem _ COrgan IK.CONDITION _ -> (1, -1) -- varied, big bunch, but try to create it anyway IK.CreateItem _ COrgan grp timer -> -- assumed temporary let turnTimer = IK.foldTimer 1 Dice.meanDice Dice.meanDice timer -- copy count used instead of timer for organs with many copies (total, count) = organBenefit turnTimer grp cops fid factionD in delta $ total / intToDouble count -- the same when created in me and in foe -- average over all matching grps; simplified: rarities ignored IK.CreateItem _ _ IK.TREASURE _ -> (100, 0) -- assumed not temporary IK.CreateItem _ _ IK.COMMON_ITEM _ -> (70, 0) IK.CreateItem _ _ IK.CRAWL_ITEM _ -> (70, 0) IK.CreateItem _ _ IK.ANY_SCROLL _ -> (50, 0) IK.CreateItem _ _ IK.ANY_GLASS _ -> (75, 0) IK.CreateItem _ _ IK.ANY_POTION _ -> (100, 0) IK.CreateItem _ _ IK.ANY_FLASK _ -> (50, 0) IK.CreateItem _ _ IK.EXPLOSIVE _ -> (50, 0) IK.CreateItem _ _ IK.ANY_JEWELRY _ -> (100, 0) IK.CreateItem _ _ grp _ -> -- assumed not temporary and @grp@ tiny let (total, count) = recBenefit grp cops fid factionD in (total / intToDouble count, 0) IK.DestroyItem{} -> delta (-10) -- potentially harmful IK.ConsumeItems{} -> delta (-10) -- potentially harmful IK.DropItem _ _ COrgan IK.CONDITION -> (0, -1) -- negative value necessary to collect such items; -- smart use on self is hardcoded in AI action choice IK.DropItem ngroup kcopy COrgan grp -> -- assumed temporary -- Simplified: we assume actor has an average number of copies -- (and none have yet run out, e.g., prompt curing of poisoning) -- of a single kind of organ (and so @ngroup@ doesn't matter) -- of average benefit and that @kcopy@ is such that all copies -- are dropped. Separately we add bonuses for @ngroup@ and @kcopy@. -- Remaining time of the organ is arbitrarily assumed to be 20 turns. let turnTimer = 20 (total, count) = organBenefit turnTimer grp cops fid factionD boundBonus n = if n == maxBound then 10 else 0 in delta $ boundBonus ngroup + boundBonus kcopy - total / intToDouble count -- the same when dropped from me and foe IK.DropItem{} -> delta (-10) -- depends a lot on what is dropped IK.Recharge n d -> delta $ intToDouble n * Dice.meanDice d / 10 -- this high value to price weapons with @OnUser@ over fists IK.Discharge n d -> delta $ - intToDouble n * Dice.meanDice d / 10 IK.PolyItem -> (1, 0) -- may fizzle, so AI never uses (could loop) IK.RerollItem -> (1, 0) -- may fizzle, so AI never uses (could loop) IK.DupItem -> (1, 0) -- may fizzle, so AI never uses (could loop) IK.Identify -> (1, 0) -- may fizzle, so AI never uses (could loop) IK.Detect IK.DetectAll radius -> (intToDouble radius * 2, 0) IK.Detect IK.DetectLoot radius -> (intToDouble radius * 2, 0) IK.Detect IK.DetectExit radius -> (intToDouble radius / 2, 0) IK.Detect _ radius -> (intToDouble radius, 0) IK.SendFlying _ -> (0, -1) -- very context dependent, but lack of control IK.PullActor _ -> (0, -1) -- is deadly on some maps, leading to harm; IK.PushActor _ -> (0, -100) -- pushing others may crush them against wall -- and give us time to fling at them IK.ApplyPerfume -> delta 0 -- depends on smell sense of friends and foes IK.AtMostOneOf effs -> let bs = map (effectToBenefit cops fid factionD) effs f (self, foe) (accSelf, accFoe) = (self + accSelf, foe + accFoe) (effSelf, effFoe) = foldr f (0, 0) bs in (effSelf / intToDouble (length bs), effFoe / intToDouble (length bs)) IK.OneOf effs -> let bs = map (effectToBenefit cops fid factionD) effs f (self, foe) (accSelf, accFoe) = (self + accSelf, foe + accFoe) (effSelf, effFoe) = foldr f (0, 0) bs in (effSelf / intToDouble (length bs), effFoe / intToDouble (length bs)) IK.OnSmash _ -> delta 0 -- can be beneficial; we'd need to analyze explosions, range, etc. IK.OnCombine eff1 -> effectToBenefit cops fid factionD eff1 IK.OnUser eff1 -> let (effSelf, _) = effectToBenefit cops fid factionD eff1 in (effSelf, - effSelf) -- in both cases just applies the effect to itself, -- which is approximately equal to applying the opposite to foe; -- this may result in double-counting, but ensures that weapons -- that harm their wielders are properly discouted; -- in a way, this should be double-counted, because the effect -- not only hinders (or enhances) applying the item, -- but meleeing with it, too IK.NopEffect -> delta 0 IK.AndEffect eff1 _ -> effectToBenefit cops fid factionD eff1 -- for simplicity; so in content make sure to place initial animations -- among normal effects, not at the start of composite effect -- (animations should not fail, after all), and start composite -- effect with the main thing IK.OrEffect eff1 _ -> effectToBenefit cops fid factionD eff1 IK.SeqEffect effs -> effectToBenefits cops fid factionD effs IK.When _cond eff1 -> -- Assuming the condition met most of the time. Really, too hard for AI. effectToBenefit cops fid factionD eff1 IK.Unless _cond eff1 -> -- Assuming the condition *not* met most of the time. -- Really, too hard for AI. effectToBenefit cops fid factionD eff1 IK.IfThenElse _cond eff1 _eff2 -> -- Assuming the first is much more common. Really, too hard for AI. effectToBenefit cops fid factionD eff1 IK.VerbNoLonger{} -> delta 0 -- flavour only, no benefit IK.VerbMsg{} -> delta 0 -- flavour only, no benefit IK.VerbMsgFail{} -> delta 0 effectToBenefits :: COps -> FactionId -> FactionDict -> [IK.Effect] -> (Double, Double) effectToBenefits cops fid factionD effs = let effPairs = map (effectToBenefit cops fid factionD) effs f (self, foe) (accSelf, accFoe) = (self + accSelf, foe + accFoe) in foldr f (0, 0) effPairs -- See the comment for @Paralyze@. averageTurnValue :: Double averageTurnValue = 10 -- Average delay between desired item uses. Some items are best activated -- every turn, e.g., healing (but still, on average, the activation would be -- useless some of the time, namely when HP is at max, which is rare, -- or when some combat boost is already lasting, which is probably also rare). -- However, e.g., for detection, activating every few turns is enough. -- Also, sometimes actor has many activable items, so he doesn't want to use -- the less powerful ones as often as when they are alone. -- For weapons, it depends. Sometimes a weapon with disorienting effect -- should be used once every couple of turns and stronger raw damage -- weapons all the remaining time. In other cases a single weapon -- with a devastating effect would ideally be available each turn. -- We don't want to undervalue rarely used items with long timeouts -- and we think that most interesting gameplay comes from alternating -- item use, so we arbitrarily set the full value timeout to 3. avgItemDelay :: Double avgItemDelay = 3 -- The average time between consumable item being found -- (and enough skill obtained to use it) and the item -- not being worth using any more. We specifically ignore -- item not being used any more, because it is not durable and is consumed. -- However we do consider actor mortality (especially common for spawners) -- and item contending with many other very different but valuable items -- that all vie for the same turn needed to activate them (especially common -- for non-spawners). Another reason is item getting obsolete or duplicated, -- by finding a strictly better item or an identical item. -- The @avgItemLife@ constant only makes sense for items with non-periodic -- effects, because the effects' benefit is not cumulated -- by just placing them in equipment and they cost a turn to activate. -- We set the value to 30, assuming if the actor finds an item, then he is -- most likely at an unlooted level, so he will find more loot soon, -- or he is in a battle, so he will die soon (or win even more loot). avgItemLife :: Double avgItemLife = 30 -- The value of durable item is this many times higher than non-durable, -- because the item will on average be activated this many times -- before it stops being used. durabilityMult :: Double durabilityMult = avgItemLife / avgItemDelay -- We assume the organ is temporary (@Fragile@ and @Periodic@) -- and also that it doesn't provide any functionality, -- e.g., detection or raw damage. However, we take into account effects -- knowing in some temporary organs, e.g., poison or regeneration, -- they are triggered at each item copy destruction. They are applied to self, -- hence we take the self component of valuation. We multiply by the count -- of created/dropped organs, because for conditions it determines -- how many times the effect is applied, before the last copy expires. -- -- The temporary organs are not durable nor in infnite copies, so to give -- continous benefit, organ has to be recreated each @turnTimer@ turns. -- Creation takes a turn, so incurs @averageTurnValue@ cost. -- That's how the lack of durability impacts their value, not via -- @durabilityMult@, which however may be applied to organ creating item. -- So, on average, maintaining the organ costs @averageTurnValue/turnTimer@. -- So, if an item lasts @averageTurnValue@ and can be created at will, it's -- almost as valuable as permanent. This makes sense even if the item creating -- the organ is not durable, but the timer is huge. One may think the lack -- of durability should be offset by the timer, but remember that average -- item life @avgItemLife@ is rather low, so either a new item will be found -- soon and so the long timer doesn't matter or the actor will die -- or the gameplay context will change (e.g., out of battle) and so the effect -- will no longer be useful. -- -- When considering the effects, we just use their standard valuation, -- despite them not using up actor's turn to be applied each turn, -- because, similarly as for periodic items, we don't control when they -- are applied and we can't stop/restart them. -- -- We assume, only one of the timer and count mechanisms is present at once -- (@count@ or @turnTimer@ is 1). -- We assume no organ has effect that drops its group or creates its group; -- otherwise we'd loop. organBenefit :: Double -> GroupName ItemKind -> COps -> FactionId -> FactionDict -> (Double, Int) organBenefit turnTimer grp cops@COps{coitem} fid factionD = let f (!sacc, !pacc) !p _ !kind = let count = Dice.meanDice (IK.icount kind) paspect asp = intToDouble p * count * turnTimer -- the aspect stays for this many turns' * aspectToBenefit asp peffect eff = intToDouble p * count -- this many consecutive effects will be generated, if any * fst (effectToBenefit cops fid factionD eff) in ( sacc + (sum (map paspect $ IK.iaspects kind) + sum (map peffect $ IK.ieffects kind)) - averageTurnValue -- the cost of 1 turn spent acquiring the organ -- (or of inflexibility of periodic items) , pacc + p ) in ofoldlGroup' coitem grp f (0, 0) -- We assume no item has effect that drops its group or creates its group; -- otherwise we'd loop. recBenefit :: GroupName ItemKind -> COps -> FactionId -> FactionDict -> (Double, Int) recBenefit grp cops@COps{coitem, coItemSpeedup} fid factionD = let f (!sacc, !pacc) !p !kindId !kind = let km = getKindMean kindId coItemSpeedup recPickup = benPickup $ totalUsefulness cops fid factionD (fakeItem kindId kind km) in ( sacc + Dice.meanDice (IK.icount kind) * recPickup , pacc + p ) in ofoldlGroup' coitem grp f (0, 0) fakeItem :: ContentId IK.ItemKind -> IK.ItemKind -> IA.KindMean -> ItemFull fakeItem kindId kind km = let jkind = IdentityObvious kindId jfid = Nothing -- the default jflavour = dummyFlavour itemBase = Item{..} itemDisco = ItemDiscoMean km in ItemFull itemBase kindId kind itemDisco True -- The value of aspect bonus is supposed to be, roughly, the benefit -- of having that bonus on actor for one turn (as if equipping didn't cost -- any time). Comparing or adding this value later on to the benefit of one-time -- applying the item makes sense, especially if the item is durable, -- but even if not, as lont as I have many items relative to equipment slots. -- If I have scarcity of items, the value should be higher, because if I apply -- a non-durable item, it no longer benefits me, but if I wear it, -- it can benefit me next turn also. The time cost of equipping balances this -- to some extent, just as @durabilityMult@ and the equipment slot limit. -- -- Value of aspects and effects is linked by some deep economic principles -- which I'm unfortunately ignorant of. E.g., average weapon hits for 5HP, -- so it's worth 50 per turn, so that should also be the worth per turn -- of equpping a sword oil that doubles damage via @AddHurtMelee@. -- Which almost matches up, since 100% effective oil is worth 100. -- Perhaps oil is worth double (despite cap, etc.), because it's addictive -- and raw weapon damage is not; so oil stays and old weapons get trashed. -- However, using the weapon in combat costs 100 (the value of extra -- battle turn). However, one turn per turn is almost free, because something -- has to be done to move the time forward. If the oil required wasting a turn -- to affect next strike, then we'd have two turns per turn, so the cost -- would be real and 100% oil would not have any significant good or bad effect -- any more, but 200% oil (if not for the cap) would still be worth it. -- -- Anyway, that suggests that the current scaling of effect vs aspect values -- is reasonable. What is even more important is consistency among aspects -- so that, e.g., a shield or a torch is never equipped by AI, but oil lamp is. -- Valuation of effects, and more precisely, more the signs than absolute -- values, ensures that both shield and torch get auto-picked up so that -- the human player can nevertheless equip them in very special cases. aspectToBenefit :: IK.Aspect -> Double aspectToBenefit asp = case asp of IK.Timeout{} -> 0 IK.AddSkill Ability.SkMove p -> capStat (Dice.meanDice p) * 5 IK.AddSkill Ability.SkMelee p -> capStat (Dice.meanDice p) * 5 IK.AddSkill Ability.SkDisplace p -> capStat (Dice.meanDice p) IK.AddSkill Ability.SkAlter p -> capStat (Dice.meanDice p) IK.AddSkill Ability.SkWait p -> capStat (Dice.meanDice p) IK.AddSkill Ability.SkMoveItem p -> capStat (Dice.meanDice p) IK.AddSkill Ability.SkProject p -> capStat (Dice.meanDice p) * 2 IK.AddSkill Ability.SkApply p -> capStat (Dice.meanDice p) * 2 IK.AddSkill Ability.SkSwimming p -> Dice.meanDice p IK.AddSkill Ability.SkFlying p -> Dice.meanDice p IK.AddSkill Ability.SkHurtMelee p -> Dice.meanDice p -- offence favoured IK.AddSkill Ability.SkArmorMelee p -> Dice.meanDice p / 4 -- only partial protection IK.AddSkill Ability.SkArmorRanged p -> Dice.meanDice p / 4 IK.AddSkill Ability.SkMaxHP p -> Dice.meanDice p IK.AddSkill Ability.SkMaxCalm p -> Dice.meanDice p / 5 IK.AddSkill Ability.SkSpeed p -> Dice.meanDice p * 25 -- 1 speed ~ 5% melee; times 5 for no caps, escape, pillar-dancing, etc.; -- OTOH, it's 1 extra turn each 20 turns, so 100/20, so 5; figures IK.AddSkill Ability.SkSight p -> Dice.meanDice p * 5 IK.AddSkill Ability.SkSmell p -> Dice.meanDice p IK.AddSkill Ability.SkShine p -> Dice.meanDice p * 2 IK.AddSkill Ability.SkNocto p -> Dice.meanDice p * 30 -- > sight + light; stealth, slots IK.AddSkill Ability.SkHearing p -> Dice.meanDice p IK.AddSkill Ability.SkAggression _ -> 0 -- dunno IK.AddSkill Ability.SkOdor p -> - Dice.meanDice p / 4 -- rarely, if big enough, determines if one is trackable IK.AddSkill Ability.SkDeflectRanged p -> Dice.meanDice p * 100 IK.AddSkill Ability.SkDeflectMelee p -> Dice.meanDice p * 100 IK.SetFlag{} -> 0 -- valuing @UnderRanged@ and @UnderMelee@ vs retaining the charge -- and explicit applying is too hard, hence ignored IK.ELabel{} -> 0 IK.ToThrow{} -> 0 -- counted elsewhere IK.PresentAs{} -> 0 IK.EqpSlot{} -> 0 IK.Odds{} -> 0 -- Should be already rolled; if not, can't tell easily. -- In particular, any timeouts there or @Periodic@ flags -- would be ignored, so they should be avoided under @Odds@ -- in not fully-identified items, because they are so crucial -- for evaluation. -- We simplify, assuming stats are unlikely to be higher than 10 -- and to be affected by more than one non-organ item at a time. capStat :: Double -> Double capStat x = max (-10) $ min 10 x aspectRecordToBenefit :: IA.AspectRecord -> [Double] aspectRecordToBenefit arItem = map aspectToBenefit $ IA.aspectRecordToList arItem -- | Compute the whole 'Benefit' structure, containing various facets -- of AI item preference, for an item with the given effects and aspects. totalUsefulness :: COps -> FactionId -> FactionDict -> ItemFull -> Benefit totalUsefulness cops fid factionD itemFull@ItemFull{itemKind, itemSuspect} = let arItem = aspectRecordFull itemFull -- If the item is periodic, we only add effects to equipment benefit, -- because we assume it's in equipment and then -- we can't effectively apply it, because it's never recharged, -- because it activates as soon as recharged. -- We ignore the rare case of a periodic item kept in stash -- to be applied manually. AI is too silly to choose it and we -- certainly don't want AI to destroy periodic items out of silliness. -- We don't assign a special bonus or malus due to being periodic, -- because periodic items are bad in that one can't -- activate them at will and they take equipment space, -- and good in that one saves a turn, not having -- to manually activate them. Additionally, no weapon can be periodic, -- because damage would be applied to the fighter, so a large class -- of items with timeout is excluded from the consideration. -- Generally, periodic seems more helpful on items with low timeout -- and obviously beneficial effects, e.g., frequent periodic healing -- or nearby detection is better, but infrequent periodic teleportation -- or harmful outward explosion is worse. But the rule is not strict -- and also dependent on gameplay context of the moment, -- hence no numerical value. periodic = IA.checkFlag Ability.Periodic arItem -- Timeout between 0 and 1 means item usable each turn, so we consider -- it equivalent to a permanent item --- one without timeout restriction. -- Timeout 2 means two such items are needed to use the effect each turn, -- so a single such item may be worth half of the permanent value. -- E.g., when item heals 1 HP each turn, that's precisly the calculation. timeout = intToDouble $ IA.aTimeout arItem scalePeriodic value = value / max 1 timeout -- With non-periodic items, when we need to expend a turn to apply the -- item or, e.g., we lose the opportunity to use another weapon if we hit -- with this one, the loss of value due to timeout is lower. -- Also, by the time cooldown recharges, one of combatants is often dead -- or fled, so some effects are no longer useful (but 1 HP gain is). -- To balance all that, we consider a square root of timeout -- and assume we need to spend turn on other actions at least every other -- turn (hence @max 2@). Note that this makes AI like powerful weapons -- with high timeout a bit more, though it still prefers low timeouts. timeoutSqrt = sqrt $ max 2 timeout scaleTimeout v = v / timeoutSqrt (effSelf, effFoe) = effectToBenefits cops fid factionD (IK.ieffects itemKind) -- Durability doesn't have any numerical impact on @eqpSum, -- because item is never consumed by just being stored in equipment. -- Also no numerical impact for flinging, because we can't fling it again -- in the same skirmish and also enemy can pick up and fling back. -- Only @benMeleeAvg@ and @benApply@ are affected, regardless if the item -- is in equipment or not. As summands of @benPickup@ they should be -- impacted by durability, because picking an item to be used -- only once is less advantageous than when the item is durable. -- For deciding which item to apply or melee with, they should be -- impacted, because it makes more sense to use an item that is durable -- and save the option for using non-durable item for the future, e.g., -- when both items have timeouts, starting with durable is beneficial, -- because it recharges while the non-durable is prepared and used. durable = IA.checkFlag Ability.Durable arItem -- For applying, we add the self part only. benApply = max 0 $ -- because optional; I don't need to apply if periodic then 0 -- because always in eqp and so never recharged else scaleTimeout (effSelf + effDice) -- hits self with kintetic dice too, when applying / if durable then 1 else durabilityMult -- This assumes attacker hurt skill and enemy armor skill balance -- and so this value doesn't need to be recomputed at each equipment -- change and distributing weapons among AI actors doesn't need -- to match each weapon to each actor's equipment. However, -- a bad side-effect is that if an actor has terrible hurt skill, -- a weapon with high dice is still used by him before a burning weapon. -- Unless the opponent has even more terrible armor, unlikely, -- the chosen weapon is definitely not the best. effDice = - IK.damageUsefulness itemKind -- For melee, we add the foe part only. benMelee = if periodic then 0 -- because never recharged, so never ready for melee else effFoe + effDice -- @AddHurtMelee@ already in @eqpSum@ benMeleeAvg = scaleTimeout benMelee / if durable then 1 else durabilityMult -- Experimenting is fun, but it's better to risk foes' skin than ours, -- so we only buff flinging, not applying, when item not identified. -- It's also more gameplay fun when enemies throw at us rather than -- when they use items on themselves. benFling = min benFlingRaw $ if itemSuspect then -10 else 0 -- If periodic, we assume the item was in equipment, so effects -- were activated before flinging, so when projectile hits, -- it's discharged, so no kintetic damage value nor effect benefit -- is added to @benFling@. -- However, if item is not periodic, we assume the item was recharged, -- and so all the effects are activated at projectile impact, -- hence their full value is added to the kinetic damage value. benFlingRaw = min 0 $ if periodic then 0 else effFoe + benFlingDice benFlingDice | IK.idamage itemKind == 0 = 0 -- speedup | otherwise = assert (v <= 0) v where -- We assume victim completely unbuffed and not blocking. If not, -- let's hope the actor is similarly buffed to compensate. hurtMult = armorHurtCalculation True (IA.aSkills arItem) Ability.zeroSkills dmg = Dice.meanDice $ IK.idamage itemKind rawDeltaHP = ceiling $ intToDouble hurtMult * xD dmg / 100 -- For simplicity, we ignore range bonus/malus and @Lobable@. IK.ThrowMod{IK.throwVelocity} = IA.aToThrow arItem speed = speedFromWeight (IK.iweight itemKind) throwVelocity v = - int64ToDouble (modifyDamageBySpeed rawDeltaHP speed) * 10 / xD 1 -- 1 damage valued at 10, just as in @damageUsefulness@ -- If item is periodic, we factor in the self value of effects, -- because they are applied to self, whether the actor wants it or not. -- We don't add a bonus of @averageTurnValue@ to the value of periodic -- effects, even though they save a turn, by being auto-applied, -- because on the flip side, player is not in control of the precise -- timing of their activation and also occasionally needs to spend a turn -- unequipping them to prevent activation. Note also that periodic -- activations don't consume the item, whether it's durable or not. aspectBenefits = aspectRecordToBenefit arItem eqpBens = sum $ aspectBenefits ++ [scalePeriodic (effSelf + effDice) | periodic] -- Equipped items may incur crippling maluses via aspects (but rather -- not via periodic effects). Examples of crippling maluses are zeroing -- melee or move skills. AI can't live with those and can't -- value those competently against any equally enormous bonuses -- the item might provide to compensate and so be even considered. cripplingDrawback = not (null aspectBenefits) && minimum aspectBenefits < -25 eqpSum = eqpBens - if cripplingDrawback then 100 else 0 vApplyFling = max benApply (- benFling) -- If a weapon heals enemy at impact, given choice, it won't be used -- for melee, but can be equipped anyway, for beneficial aspects. -- OTOH, cif it harms wearer too much, it won't be worn -- but still may be flung and so may be worth picking up. (benInEqp, benPickupRaw) | IA.checkFlag Ability.Meleeable arItem -- the flag probably known even if item not identified && (benMelee < 0 || itemSuspect) && eqpSum >= -20 = let vEqp = eqpSum + maximum [benApply, - benMeleeAvg, 0] -- equip plus apply or melee or not v = if | vEqp > 0 -> vEqp -- pick up to equip; melee is crucial | vApplyFling > 0 -> vApplyFling -- at least pick up to apply or fling, if feasible, -- and equip just in case interesting effect; -- will be taken off if very harmful | otherwise -> vEqp -- do not pick up, but if forced, the best bet -- is equip anyway in (True, v) | (IA.goesIntoEqp arItem || IA.checkFlag Ability.Condition arItem) -- hack to record benefit, to use, e.g., to assign colour && (eqpSum > 0 || itemSuspect) = -- weapon or other equippable ( True -- equip; long time bonus usually outweighs fling or apply , eqpSum -- possibly spent turn equipping, so reap the benefits + if durable then benApply -- apply or not but don't fling else 0) -- don't remove from equipment by using up | otherwise = (False, vApplyFling) -- apply or fling benPickupRaw2 = max benPickupRaw $ if itemSuspect then 10 else 0 -- If periodic, pick up to deny to foes and sometimes to apply -- to activate the first effect only (easier than computing if the first -- effect is really beneficial, while all effects detrimental). benPickup = if periodic then max 1 benPickupRaw2 else benPickupRaw2 in Benefit{..} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/Request.hs0000644000000000000000000000326507346545000022151 0ustar0000000000000000-- | Abstract syntax of requests. -- -- See -- . module Game.LambdaHack.Client.Request ( RequestAI, ReqAI(..), RequestUI, ReqUI(..), RequestTimed(..) ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.ModeKind import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | Requests sent by AI clients to the server. If faction leader is to be -- changed, it's included as the second component. type RequestAI = (ReqAI, Maybe ActorId) -- | Possible forms of requests sent by AI clients. data ReqAI = ReqAINop | ReqAITimed RequestTimed deriving Show -- | Requests sent by UI clients to the server. If faction leader is to be -- changed, it's included as the second component. type RequestUI = (ReqUI, Maybe ActorId) -- | Possible forms of requests sent by UI clients. data ReqUI = ReqUINop | ReqUITimed RequestTimed | ReqUIGameRestart (GroupName ModeKind) Challenge | ReqUIGameDropAndExit | ReqUIGameSaveAndExit | ReqUIGameSave | ReqUIDoctrine Ability.Doctrine | ReqUIAutomate deriving Show -- | Requests that take game time. data RequestTimed = ReqMove Vector | ReqMelee ActorId ItemId CStore | ReqDisplace ActorId | ReqAlter Point | ReqWait | ReqWait10 | ReqYell | ReqMoveItems [(ItemId, Int, CStore, CStore)] | ReqProject Point Int ItemId CStore | ReqApply ItemId CStore deriving Show LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/Response.hs0000644000000000000000000000234007346545000022310 0ustar0000000000000000-- | Abstract syntax of responses. -- -- See -- . module Game.LambdaHack.Client.Response ( Response(..) ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Atomic import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types -- | Abstract syntax of responses sent by server to an AI or UI client -- (or a universal client that can handle both roles, which is why -- this type is not separated into distinct AI and UI types). -- A response tells a client how to update game state or what information -- to send to the server. data Response = RespUpdAtomicNoState UpdAtomic -- ^ change @State@ by performing this atomic update | RespUpdAtomic State UpdAtomic -- ^ put the given @State@, which results from performing the atomic update | RespQueryAI ActorId -- ^ compute an AI move for the actor and send (the semantics of) it | RespSfxAtomic SfxAtomic -- ^ perform special effects (animations, messages, etc.) | RespQueryUIunderAI -- ^ check if the UI client wants to regain control | RespQueryUI -- ^ prompt the human player for a command and send (the semantics of) it deriving Show LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/State.hs0000644000000000000000000002050207346545000021572 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Client-specific game state components. module Game.LambdaHack.Client.State ( StateClient(..), AlterLid, BfsAndPath(..) , TgtAndPath(..), Target(..), TGoal(..) , emptyStateClient, cycleMarkSuspect , updateTarget, getTarget, updateLeader, sside, sleader ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Primitive.PrimArray as PA import GHC.Generics (Generic) import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector -- | Client state, belonging to a single faction. data StateClient = StateClient { seps :: Int -- ^ a parameter of the aiming digital line , stargetD :: EM.EnumMap ActorId TgtAndPath -- ^ targets of our actors in the dungeon; this is only useful for AI -- and for directing non-pointmen, in particular with following -- doctrines, where non-pointmen go to the pointman's target , sfleeD :: EM.EnumMap ActorId (Point, Time) -- ^ the position and time of last fleeing -- attempt (regardless if succeeded) , sexplored :: ES.EnumSet LevelId -- ^ the set of fully explored levels , sbfsD :: EM.EnumMap ActorId BfsAndPath -- ^ pathfinding data for our actors , sundo :: () -- [CmdAtomic] -- ^ atomic commands performed to date , sdiscoBenefit :: DiscoveryBenefit -- ^ remembered AI benefits of items; could be recomputed at resume, -- but they are costly to generate and not too large , sfper :: PerLid -- ^ faction perception indexed by level , salter :: AlterLid -- ^ cached alter skill data for positions -- (actually, @Tile.alterMinWalk@ instead) , srandom :: SM.SMGen -- ^ current random generator , _sleader :: Maybe ActorId -- ^ candidate new leader of the faction; -- Faction.gleader is the old leader , _sside :: FactionId -- ^ faction controlled by the client , squit :: Bool -- ^ exit the game loop , scondInMelee :: ES.EnumSet LevelId -- ^ whether we are in melee, per level , soptions :: ClientOptions -- ^ client options , stabs :: (PA.PrimArray PointI, PA.PrimArray PointI) -- ^ Instead of a BFS queue (list) we use these two arrays, -- for (JS) speed. They need to be per-client distinct, -- because sometimes multiple clients interleave BFS computation. -- The three fields below only make sense for the UI faction, -- but can't be in SessionUI, because AI-moved actors of the UI faction -- require them for their action. Fortunately, being in StateClient -- of the UI client, these are never lost, even when a different faction -- becomes the UI faction. , scurChal :: Challenge -- ^ current game challenge setup , snxtChal :: Challenge -- ^ next game challenge setup , smarkSuspect :: Int -- ^ whether to mark suspect features } -- No @Show@ instance, because @stabs@ start undefined. type AlterLid = EM.EnumMap LevelId (PointArray.Array Word8) -- | Pathfinding distances to all reachable positions of an actor -- and a shortest paths to some of the positions. data BfsAndPath = BfsInvalid | BfsAndPath (PointArray.Array BfsDistance) (EM.EnumMap Point AndPath) deriving Show -- | Actor's target and a path to it, if any. data TgtAndPath = TgtAndPath {tapTgt :: Target, tapPath :: Maybe AndPath} deriving (Show, Generic) instance Binary TgtAndPath -- | The type of na actor target. data Target = TEnemy ActorId -- ^ target an enemy | TNonEnemy ActorId -- ^ target a friend or neutral | TPoint TGoal LevelId Point -- ^ target a concrete spot | TVector Vector -- ^ target position relative to actor deriving (Show, Eq, Generic) instance Binary Target -- | The goal of an actor. data TGoal = TStash FactionId -- ^ shared inventory stash of our or an enemy faction | TEnemyPos ActorId -- ^ last seen position of the targeted actor | TEmbed ItemBag Point -- ^ embedded item that can be triggered; -- in @TPoint (TEmbed bag p) _ q@ usually @bag@ is -- embbedded in @p@ and @q@ is an adjacent open tile | TItem ItemBag -- ^ item lying on the ground | TSmell -- ^ smell potentially left by enemies | TBlock -- ^ a blocking tile to be approached (and, e.g., revealed -- to be walkable or altered or searched) | TUnknown -- ^ an unknown tile to be explored | TKnown -- ^ a known tile to be patrolled | THideout -- ^ a hideout to either flee to or find a hidden enemy sniper in deriving (Show, Eq, Generic) instance Binary TGoal -- | Initial empty game client state. emptyStateClient :: FactionId -> StateClient emptyStateClient _sside = StateClient { seps = fromEnum _sside , stargetD = EM.empty , sfleeD = EM.empty , sexplored = ES.empty , sbfsD = EM.empty , sundo = () , sdiscoBenefit = EM.empty , sfper = EM.empty , salter = EM.empty , srandom = SM.mkSMGen 42 -- will get modified in this and future games , _sleader = Nothing -- no heroes yet alive , _sside , squit = False , scondInMelee = ES.empty , soptions = defClientOptions , stabs = (undefined, undefined) , scurChal = defaultChallenge , snxtChal = defaultChallenge , smarkSuspect = 1 } -- | Cycle the 'smarkSuspect' setting. cycleMarkSuspect :: Int -> StateClient -> StateClient cycleMarkSuspect delta cli = cli {smarkSuspect = (smarkSuspect cli + delta) `mod` 3} -- | Update target parameters within client state. updateTarget :: ActorId -> (Maybe Target -> Maybe Target) -> StateClient -> StateClient updateTarget aid f cli = let f2 tp = case f $ fmap tapTgt tp of Nothing -> Nothing Just tgt -> Just $ TgtAndPath tgt Nothing -- reset path in cli {stargetD = EM.alter f2 aid (stargetD cli)} -- | Get target parameters from client state. getTarget :: ActorId -> StateClient -> Maybe Target getTarget aid cli = fmap tapTgt $ EM.lookup aid $ stargetD cli -- | Update picked leader within state. Verify actor's faction. updateLeader :: ActorId -> State -> StateClient -> StateClient updateLeader leader s cli = let side1 = bfid $ getActorBody leader s side2 = sside cli in assert (side1 == side2 `blame` "enemy actor becomes our leader" `swith` (side1, side2, leader, s)) $ cli {_sleader = Just leader} sside :: StateClient -> FactionId sside = _sside sleader :: StateClient -> Maybe ActorId sleader = _sleader instance Binary StateClient where put StateClient{..} = do put seps put stargetD put sfleeD put sexplored put sdiscoBenefit put (show srandom) put _sleader put _sside put scondInMelee put soptions put scurChal put snxtChal put smarkSuspect #ifdef WITH_EXPENSIVE_ASSERTIONS put sfper #endif get = do seps <- get stargetD <- get sfleeD <- get sexplored <- get sdiscoBenefit <- get g <- get _sleader <- get _sside <- get scondInMelee <- get soptions <- get scurChal <- get snxtChal <- get smarkSuspect <- get let sbfsD = EM.empty sundo = () salter = EM.empty srandom = read g squit = False stabs = (undefined, undefined) #ifndef WITH_EXPENSIVE_ASSERTIONS sfper = EM.empty #else sfper <- get #endif return $! StateClient{..} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI.hs0000644000000000000000000002257707346545000021045 0ustar0000000000000000-- | Ways for the client to use player input via UI to produce server -- requests, based on the client's view (visualized for the player) -- of the game state. -- -- This module is leaking quite a bit of implementation details -- for the sake of "Game.LambdaHack.Client.LoopM". After multiplayer -- is enabled again and the new requirements sorted out, this should be -- redesigned and some code moved down the module hierarhy tree, -- exposing a smaller API here. module Game.LambdaHack.Client.UI ( -- * Querying the human player queryUI, queryUIunderAI -- * UI monad operations , MonadClientUI(..), putSession, anyKeyPressed, resetPressedKeys -- * UI session type , SessionUI(..), ReqDelay(..), emptySessionUI -- * Updating UI state wrt game state changes , watchRespUpdAtomicUI, watchRespSfxAtomicUI -- * Startup and initialization , CCUI(..) , UIOptions, applyUIOptions, uOverrideCmdline, mkUIOptions -- * Assorted operations and types , ChanFrontend, chanFrontend, tryRestore, clientPrintUI , pushReportFrame, msgAdd, MsgClassShow(..) #ifdef EXPOSE_INTERNAL -- * Internal operations , stepQueryUIwithLeader, stepQueryUI #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Map.Strict as M import qualified Data.Text as T import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.Content.Input import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.FrameM import Game.LambdaHack.Client.UI.Frontend import Game.LambdaHack.Client.UI.HandleHelperM import Game.LambdaHack.Client.UI.HandleHumanM import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Client.UI.UIOptionsParse import Game.LambdaHack.Client.UI.Watch import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State import Game.LambdaHack.Content.FactionKind -- | Handle the move of a human player. queryUI :: (MonadClient m, MonadClientUI m) => m (Maybe RequestUI) queryUI = do sreqQueried <- getsSession sreqQueried let !_A = assert (not sreqQueried) () -- querying not nested modifySession $ \sess -> sess {sreqQueried = True} let loop = do mres <- stepQueryUIwithLeader saimMode <- getsSession saimMode case mres of Nothing | isJust saimMode -> loop -- loop until aiming finished _ -> return mres mres <- loop modifySession $ \sess -> sess {sreqQueried = False} return mres queryUIunderAI :: (MonadClient m, MonadClientUI m) => m RequestUI queryUIunderAI = do -- Record history so the player can browse it later on. recordHistory -- As long as UI faction is under AI control, check, once per move, -- for immediate control regain or benchmark game stop. sregainControl <- getsSession sregainControl if sregainControl then do modifySession $ \sess -> sess { sregainControl = False , sreqDelay = ReqDelayNot , sreqPending = Nothing } -- just in case -- The keys mashed to gain control are not considered a command. resetPressedKeys -- Menu is entered in @displayRespUpdAtomicUI@ at @UpdAutoFaction@ -- and @stopAfter@ is canceled in @cmdAtomicSemCli@ -- when handling the results of the request below. return (ReqUIAutomate, Nothing) else do stopAfterFrames <- getsClient $ sstopAfterFrames . soptions bench <- getsClient $ sbenchmark . soptions let exitCmd = if bench then ReqUIGameDropAndExit else ReqUIGameSaveAndExit case stopAfterFrames of Nothing -> do stopAfterSeconds <- getsClient $ sstopAfterSeconds . soptions case stopAfterSeconds of Nothing -> return (ReqUINop, Nothing) Just stopS -> do sstartPOSIX <- getsSession sstart exit <- elapsedSessionTimeGT sstartPOSIX stopS if exit then do tellAllClipPS return (exitCmd, Nothing) -- ask server to exit else return (ReqUINop, Nothing) Just stopF -> do allNframes <- getsSession sallNframes gnframes <- getsSession snframes if allNframes + gnframes >= stopF then do tellAllClipPS return (exitCmd, Nothing) -- ask server to exit else return (ReqUINop, Nothing) stepQueryUIwithLeader :: (MonadClient m, MonadClientUI m) => m (Maybe RequestUI) stepQueryUIwithLeader = do side <- getsClient sside mleader <- getsState $ gleader . (EM.! side) . sfactionD mreq <- stepQueryUI case mreq of Nothing -> return Nothing Just req -> do mleader2 <- getsClient sleader -- Don't send the leader switch to the server with these commands, -- to avoid leader death at resume if his HP <= 0. That would violate -- the principle that save and reload doesn't change game state. let saveCmd cmd = case cmd of ReqUIGameDropAndExit -> True ReqUIGameSaveAndExit -> True ReqUIGameSave -> True _ -> False return $ Just (req, if mleader /= mleader2 && not (saveCmd req) then mleader2 else Nothing) -- | Let the human player issue commands until any command takes time. stepQueryUI :: (MonadClient m, MonadClientUI m) => m (Maybe ReqUI) stepQueryUI = do FontSetup{propFont} <- getFontSetup keyPressed <- anyKeyPressed macroFrame <- getsSession smacroFrame -- This message, in particular, disturbs. when (keyPressed && not (null (unKeyMacro (keyPending macroFrame)))) $ msgAdd MsgActionWarning "*interrupted*" report <- getsSession $ newReport . shistory modifySession $ \sess -> sess {sreportNull = nullVisibleReport report} slides <- reportToSlideshowKeepHalt False [] ovs <- case unsnoc slides of Nothing -> return EM.empty Just (allButLast, (ov, _)) -> if allButLast == emptySlideshow then do -- Display the only generated slide while waiting for next key. -- Strip the "--end-" prompt from it, by ignoring @MonoFont@. let ovProp = ov EM.! propFont return $! EM.singleton propFont $ if EM.size ov > 1 then ovProp else init ovProp else do -- Show, one by one, all slides, awaiting confirmation for each. void $ getConfirms ColorFull [K.spaceKM, K.escKM] slides -- Indicate that report wiped out. modifySession $ \sess -> sess {sreportNull = True} -- Display base frame at the end. return EM.empty mleader <- getsClient sleader case mleader of Nothing -> return () Just leader -> do body <- getsState $ getActorBody leader lastLost <- getsSession slastLost if bhp body <= 0 then do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD let gameOver = maybe False ((/= Camping) . stOutcome) (gquit fact) when (not gameOver && leader `ES.notMember` lastLost) $ do -- Hacky reuse of @slastLost@ for near-death spam prevention. modifySession $ \sess -> sess {slastLost = ES.insert leader lastLost} displayMore ColorBW "If you move, the exertion will kill you. Consider asking for first aid instead." else modifySession $ \sess -> sess {slastLost = ES.empty} km <- promptGetKey ColorFull ovs False [] abortOrCmd <- do -- Look up the key. CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui case km `M.lookup` bcmdMap of Just (_, _, cmd) -> do modifySession $ \sess -> sess {swaitTimes = if swaitTimes sess > 0 then - swaitTimes sess else 0} cmdSemInCxtOfKM km cmd _ -> let msgKey = "unknown command '" <> K.showKM km <> "'" in weaveJust <$> failWith (T.pack msgKey) -- GC macro stack if there are no actions left to handle, -- removing all unnecessary macro frames at once, -- but leaving the last one for user's in-game macros. modifySession $ \sess -> let (smacroFrameNew, smacroStackMew) = dropEmptyMacroFrames (smacroFrame sess) (smacroStack sess) in sess { smacroFrame = smacroFrameNew , smacroStack = smacroStackMew } -- The command was failed or successful and if the latter, -- possibly took some time. case abortOrCmd of Right cmdS -> -- Exit the loop and let other actors act. No next key needed -- and no report could have been generated. return $ Just cmdS Left Nothing -> return Nothing Left (Just err) -> do msgAdd MsgActionAlert $ showFailError err return Nothing LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/0000755000000000000000000000000007346545000020474 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/ActorUI.hs0000644000000000000000000000371107346545000022340 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | UI aspects of actors. module Game.LambdaHack.Client.UI.ActorUI ( ActorUI(..), ActorDictUI , keySelected, partActor, partPronoun, tryFindActor, tryFindHeroK ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.Char as Char import qualified Data.EnumMap.Strict as EM import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Definition.Color as Color data ActorUI = ActorUI { bsymbol :: Char -- ^ individual map symbol , bname :: Text -- ^ individual name , bpronoun :: Text -- ^ individual pronoun , bcolor :: Color.Color -- ^ individual map color } deriving (Show, Eq, Generic) instance Binary ActorUI type ActorDictUI = EM.EnumMap ActorId ActorUI keySelected :: (ActorId, Actor, ActorUI) -> (Bool, Bool, Bool, Char, Color.Color, ActorId) keySelected (aid, Actor{bhp, bwatch}, ActorUI{bsymbol, bcolor}) = (bhp > 0, bwatch /= WSleep, bsymbol /= '@', bsymbol, bcolor, aid) -- | The part of speech describing the actor. partActor :: ActorUI -> MU.Part partActor b = MU.Text $ bname b -- | The part of speech containing the actor's pronoun. partPronoun :: ActorUI -> MU.Part partPronoun b = MU.Text $ bpronoun b tryFindActor :: State -> (ActorId -> Actor -> Bool) -> Maybe (ActorId, Actor) tryFindActor s p = find (uncurry p) $ EM.assocs $ sactorD s tryFindHeroK :: ActorDictUI -> FactionId -> Int -> State -> Maybe (ActorId, Actor) tryFindHeroK d fid k s = let c | k == 0 = '@' | k > 0 && k < 10 = Char.intToDigit k | otherwise = ' ' -- no hero with such symbol in tryFindActor s (\aid body -> maybe False ((== c) . bsymbol) (EM.lookup aid d) && bfid body == fid) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Animation.hs0000644000000000000000000002212307346545000022747 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | Screen frames and animations. module Game.LambdaHack.Client.UI.Animation ( Animation, renderAnim , pushAndDelay, twirlSplash, twirlSplashShort, blockHit, blockMiss, subtleHit , deathBody, shortDeathBody, actorX, teleport, vanish, swapPlaces, fadeout #ifdef EXPOSE_INTERNAL -- * Internal operations , blank, cSym, mapPosToOffset, mzipSingleton, mzipPairs #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Bits (xor) import qualified Data.EnumMap.Strict as EM import Data.Word (Word32) import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Common.Point import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Color -- | Animation is a list of frame modifications to play one by one, -- where each modification if a map from positions to level map symbols. newtype Animation = Animation [OverlaySpace] deriving (Show, Eq) -- | Render animations on top of a screen frame. -- -- Located in this module to keep @Animation@ abstract. renderAnim :: Int -> PreFrame -> Animation -> PreFrames renderAnim width basicFrame (Animation anim) = let modifyFrame :: OverlaySpace -> PreFrame -- Overlay not truncated, because guaranteed within bounds. modifyFrame am = overlayFrame width am basicFrame modifyFrames :: OverlaySpace -> OverlaySpace -> Maybe PreFrame modifyFrames am amPrevious = if am == amPrevious then Nothing else Just $ modifyFrame am in Just basicFrame : zipWith modifyFrames anim ([] : anim) blank :: Maybe AttrCharW32 blank = Nothing cSym :: Color -> Char -> Maybe AttrCharW32 cSym color symbol = Just $ attrChar2ToW32 color symbol mapPosToOffset :: (Point, AttrCharW32) -> (PointUI, AttrString) mapPosToOffset (p, attr) = let pUI = squareToUI $ mapToSquare p in (pUI, [attr]) mzipSingleton :: Point -> Maybe AttrCharW32 -> OverlaySpace mzipSingleton p1 mattr1 = map mapPosToOffset $ let mzip (pos, mattr) = fmap (pos,) mattr in catMaybes [mzip (p1, mattr1)] mzipPairs :: (Point, Point) -> (Maybe AttrCharW32, Maybe AttrCharW32) -> OverlaySpace mzipPairs (p1, p2) (mattr1, mattr2) = map mapPosToOffset $ let mzip (pos, mattr) = fmap (pos,) mattr in catMaybes $ if p1 /= p2 then [mzip (p1, mattr1), mzip (p2, mattr2)] else -- If actor affects himself, show only the effect, -- not the action. [mzip (p1, mattr1)] -- | Empty animation with a frame of delay, to be used to momentarily display -- something for the player to see, e.g., the aiming line when swerving it. -- Don't use this if there are multi-line messages on the screen, -- because the text blinking is going to be distracting. pushAndDelay :: Animation pushAndDelay = Animation [[]] -- | Attack animation. A part of it also reused for self-damage and healing. twirlSplash :: (Point, Point) -> Color -> Color -> Animation twirlSplash poss c1 c2 = Animation $ map (mzipPairs poss) [ (blank , cSym BrCyan '&') , (blank , cSym BrCyan '&') , (blank , blank) , (cSym c1 '\\',blank) , (cSym c1 '|', cSym BrCyan '&') , (cSym c1 '%', blank) , (cSym c1 '/', blank) , (cSym c1 '-', blank) , (cSym c1 '\\',blank) , (cSym c2 '|', blank) , (cSym c2 '%', blank) ] -- | Short attack animation. twirlSplashShort :: (Point, Point) -> Color -> Color -> Animation twirlSplashShort poss c1 c2 = Animation $ map (mzipPairs poss) [ (blank , cSym BrCyan '&') , (blank , cSym BrCyan '&') , (cSym c1 '\\',blank) , (cSym c1 '|', cSym BrCyan '&') , (cSym c2 '%', blank) ] -- | Attack that hits through a block. blockHit :: (Point, Point) -> Color -> Color -> Animation blockHit poss c1 c2 = Animation $ map (mzipPairs poss) [ (blank , cSym BrCyan '&') , (blank , cSym BrCyan '&') , (blank , blank) , (cSym BrBlue '{', blank) , (cSym BrBlue '{', cSym BrCyan '&') , (cSym BrBlue '{', blank) , (cSym BrBlue '}', blank) , (cSym BrBlue '}', blank) , (cSym BrBlue '}', blank) , (cSym c1 '\\',blank) , (cSym c1 '|', blank) , (cSym c1 '/', blank) , (cSym c1 '-', blank) , (cSym c2 '\\',blank) , (cSym c2 '|', blank) , (cSym c2 '/', blank) ] -- | Attack that is blocked. blockMiss :: (Point, Point) -> Animation blockMiss poss = Animation $ map (mzipPairs poss) [ (blank , cSym BrCyan '&') , (blank , cSym BrCyan '&') , (blank , blank) , (cSym BrBlue '{', blank) , (cSym BrBlue '{', cSym BrCyan '&') , (cSym BrBlue '{', blank) , (cSym BrBlue '{', blank) , (cSym BrBlue '}', blank) , (cSym Blue '}', blank) , (cSym Blue '}', blank) , (cSym Blue '}', blank) ] -- | Attack that is subtle (e.g., damage dice 0). subtleHit :: (Point, Point) -> Animation subtleHit poss = Animation $ map (mzipPairs poss) [ (blank , cSym BrCyan '&') , (blank , blank) , (blank , cSym BrYellow '&') , (cSym BrBlue '\\',blank) , (blank , cSym BrYellow '&') , (cSym BrBlue '/', blank) , (blank , blank) ] -- | Death animation for an organic body. deathBody :: Point -> Animation deathBody pos = Animation $ map (mzipSingleton pos) [ cSym Red '%' , cSym Red '-' , cSym Red '-' , cSym Red '\\' , cSym Red '\\' , cSym Red '|' , cSym Red '|' , cSym Red '%' , cSym Red '%' , cSym Red '%' , cSym Red '%' , cSym Red ';' , cSym Red ';' ] -- | Death animation for an organic body, short version (e.g., for enemies). shortDeathBody :: Point -> Animation shortDeathBody pos = Animation $ map (mzipSingleton pos) [ cSym Red '%' , cSym Red '-' , cSym Red '\\' , cSym Red '|' , cSym Red '%' , cSym Red '%' , cSym Red '%' , cSym Red ';' , cSym Red ',' ] -- | Mark actor location animation. actorX :: Point -> Animation actorX pos = Animation $ map (mzipSingleton pos) [ cSym BrMagenta 'X' , cSym BrMagenta 'X' , blank , blank ] -- | Actor teleport animation. teleport :: (Point, Point) -> Animation teleport poss = Animation $ map (mzipPairs poss) [ (cSym BrMagenta 'o', cSym Magenta '.') , (cSym BrMagenta 'O', cSym Magenta '.') , (cSym Magenta 'o', cSym Magenta 'o') , (cSym Magenta '.', cSym BrMagenta 'O') , (cSym Magenta '.', cSym BrMagenta 'o') , (cSym Magenta '.', blank) , (blank , blank) ] -- | Terrain feature vanishing animation. vanish :: Point -> Animation vanish pos = Animation $ map (mzipSingleton pos) [ cSym BrMagenta 'o' , cSym BrMagenta 'O' , cSym Magenta 'o' , cSym Magenta '.' , cSym Magenta '.' , blank ] -- | Swap-places animation, both hostile and friendly. swapPlaces :: (Point, Point) -> Animation swapPlaces poss = Animation $ map (mzipPairs poss) [ (cSym BrMagenta 'o', cSym Magenta 'o') , (cSym BrMagenta 'd', cSym Magenta 'p') , (cSym BrMagenta '.', cSym Magenta 'p') , (cSym Magenta 'p', cSym Magenta '.') , (cSym Magenta 'p', cSym BrMagenta 'd') , (cSym Magenta 'p', cSym BrMagenta 'd') , (cSym Magenta 'o', blank) , (blank , blank) ] fadeout :: ScreenContent -> Bool -> Int -> Rnd Animation fadeout ScreenContent{rwidth, rheight} out step = do let xbound = rwidth - 1 ybound = rheight - 1 margin = (rwidth - 2 * rheight) `div` 2 - 2 edge = EM.fromDistinctAscList $ zip [1..] ".%&%;:,." fadeChar :: Int -> Int -> Int -> Int -> Char fadeChar !r !n !x !y = let d = x - 2 * y ndy = n - d - 2 * ybound ndx = n + d - xbound - 1 -- @-1@ for asymmetry mnx = if ndy > 0 && ndx > 0 then min ndy ndx else max ndy ndx v3 = (r `xor` (x * y)) `mod` 3 k | mnx < 3 || mnx > 10 = mnx | (min x (xbound - x - y) + n + v3) `mod` 15 < 11 && mnx > 6 = mnx - v3 | (x + 3 * y + v3) `mod` 30 < 19 = mnx + 1 | otherwise = mnx in EM.findWithDefault ' ' k edge rollFrame !n = do w <- randomWord32 -- @fromIntegralWrap@ is potentially costly, but arch-independent. -- Also, it's fine if it wraps. let fadeAttr !y !x = attrChar1ToW32 $ fadeChar ((fromIntegralWrap :: Word32 -> Int) w) n x y fadeLine !y = let x1 :: Int {-# INLINE x1 #-} x1 = min xbound (n - 2 * (ybound - y)) x2 :: Int {-# INLINE x2 #-} x2 = max 0 (xbound - (n - 2 * y)) in [ (PointUI 0 y, map (fadeAttr y) [0..x1]) , (PointUI (2 * x2) y, map (fadeAttr y) [x2..xbound]) ] return $! concatMap fadeLine [0..ybound] fs | out = [3, 3 + step .. rwidth - margin] | otherwise = [rwidth - margin, rwidth - margin - step .. 1] ++ [0] -- no remnants of fadein onscreen, in case of lag Animation <$> mapM rollFrame fs LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Content/0000755000000000000000000000000007346545000022106 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Content/Input.hs0000644000000000000000000002600207346545000023541 0ustar0000000000000000-- | The type of definitions of key-command mappings to be used for the UI -- and shorthands for specifying command triples in the content files. module Game.LambdaHack.Client.UI.Content.Input ( InputContentRaw(..), InputContent(..), makeData , evalKeyDef , addCmdCategory, replaceDesc, moveItemTriple, repeatTriple, repeatLastTriple , mouseLMB, mouseMMB, mouseMMBMute, mouseRMB , goToCmd, runToAllCmd, autoexploreCmd, autoexplore25Cmd , aimFlingCmd, projectI, projectA, flingTs, applyIK, applyI , grabItems, dropItems, descIs, defaultHeroSelect, macroRun25 , memberCycle, memberCycleLevel #ifdef EXPOSE_INTERNAL -- * Internal operations , replaceCmd, projectICmd, grabCmd, dropCmd #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Char as Char import qualified Data.Map.Strict as M import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.UI.HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Common.Misc import Game.LambdaHack.Definition.Defs -- | Key-command mappings to be specified in content and used for the UI. newtype InputContentRaw = InputContentRaw [(K.KM, CmdTriple)] -- | Bindings and other information about human player commands. data InputContent = InputContent { bcmdMap :: M.Map K.KM CmdTriple -- ^ binding of keys to commands , bcmdList :: [(K.KM, CmdTriple)] -- ^ the properly ordered list -- of commands for the help menu , brevMap :: M.Map HumanCmd [K.KM] -- ^ and from commands to their keys } -- | Create binding of keys to movement and other standard commands, -- as well as commands defined in the config file. makeData :: Maybe UIOptions -- ^ UI client options -> InputContentRaw -- ^ default key bindings from the content -> InputContent -- ^ concrete binding makeData muiOptions (InputContentRaw copsClient) = let (uCommands0, uVi0, uLeftHand0) = case muiOptions of Just UIOptions{uCommands, uVi, uLeftHand} -> (uCommands, uVi, uLeftHand) Nothing -> ([], True, True) waitTriple = ([CmdMove], "", Wait) wait10Triple = ([CmdMove], "", Wait10) moveXhairOr n cmd v = ByAimMode $ AimModeCmd { exploration = cmd v , aiming = MoveXhair v n } rawContent = copsClient ++ uCommands0 movementDefinitions = K.moveBinding uVi0 uLeftHand0 (\v -> ([CmdMove], "", moveXhairOr 1 MoveDir v)) (\v -> ([CmdMove], "", moveXhairOr 10 RunDir v)) ++ [ (K.mkKM "KP_Begin", waitTriple) , (K.mkKM "C-KP_Begin", wait10Triple) , (K.mkKM "KP_5", wait10Triple) , (K.mkKM "S-KP_5", wait10Triple) -- rxvt , (K.mkKM "C-KP_5", wait10Triple) ] ++ [(K.mkKM "period", waitTriple) | uVi0] ++ [(K.mkKM "C-period", wait10Triple) | uVi0] ++ [(K.mkKM "s", waitTriple) | uLeftHand0] ++ [(K.mkKM "S", wait10Triple) | uLeftHand0] -- This is the most common case of duplicate keys and it usually -- has an easy solution, so it's tested for first. !_A = flip assert () $ let movementKeys = map fst movementDefinitions filteredNoMovement = filter (\(k, _) -> k `notElem` movementKeys) rawContent in rawContent == filteredNoMovement `blame` "commands overwrite the enabled movement keys (you can disable some in config file and try again)" `swith` rawContent \\ filteredNoMovement bcmdList = rawContent ++ movementDefinitions -- This catches repetitions (usually) not involving movement keys. rejectRepetitions _ t1 (_, "", _) = t1 rejectRepetitions _ (_, "", _) t2 = t2 rejectRepetitions k t1 t2 = error $ "duplicate key among command definitions (you can instead disable some movement key sets in config file and overwrite the freed keys)" `showFailure` (k, t1, t2) in InputContent { bcmdMap = M.fromListWithKey rejectRepetitions bcmdList , bcmdList , brevMap = M.fromListWith (flip (++)) $ concat [ [(cmd, [k])] | (k, (cats, _desc, cmd)) <- bcmdList , not (null cats) && CmdDebug `notElem` cats ] } evalKeyDef :: (String, CmdTriple) -> (K.KM, CmdTriple) evalKeyDef (t, triple@(cats, _, _)) = let km = if CmdInternal `elem` cats then K.KM K.NoModifier $ K.Unknown t else K.mkKM t in (km, triple) addCmdCategory :: CmdCategory -> CmdTriple -> CmdTriple addCmdCategory cat (cats, desc, cmd) = (cat : cats, desc, cmd) replaceDesc :: Text -> CmdTriple -> CmdTriple replaceDesc desc (cats, _, cmd) = (cats, desc, cmd) replaceCmd :: HumanCmd -> CmdTriple -> CmdTriple replaceCmd cmd (cats, desc, _) = (cats, desc, cmd) moveItemTriple :: [CStore] -> CStore -> MU.Part -> Bool -> CmdTriple moveItemTriple stores1 store2 object auto = let verb = MU.Text $ verbCStore store2 desc = makePhrase [verb, object] in ([CmdItemMenu, CmdItem], desc, MoveItem stores1 store2 Nothing auto) repeatTriple :: Int -> [CmdCategory] -> CmdTriple repeatTriple n cats = ( cats , if n == 1 then "voice recorded macro again" else "voice recorded macro" <+> tshow n <+> "times" , Repeat n ) repeatLastTriple :: Int -> [CmdCategory] -> CmdTriple repeatLastTriple n cats = ( cats , if n == 1 then "voice last action again" else "voice last action" <+> tshow n <+> "times in a row" , RepeatLast n ) -- @AimFloor@ is not there, but @AimEnemy@ and @AimItem@ almost make up for it. mouseLMB :: HumanCmd -> Text -> CmdTriple mouseLMB goToOrRunTo desc = ([CmdMouse], desc, ByAimMode aimMode) where aimMode = AimModeCmd { exploration = ByArea $ common ++ -- exploration mode [ (CaMapLeader, grabCmd) , (CaMapParty, PickLeaderWithPointer) , (CaMap, goToOrRunTo) , (CaArenaName, Dashboard) , (CaPercentSeen, autoexploreCmd) ] , aiming = ByArea $ common ++ -- aiming mode [ (CaMap, aimFlingCmd) , (CaArenaName, Accept) , (CaPercentSeen, XhairStair True) ] } common = [ (CaMessage, AllHistory) , (CaLevelNumber, AimAscend 1) , (CaXhairDesc, AimEnemy) -- inits aiming and then cycles enemies , (CaSelected, PickLeaderWithPointer) -- , (CaCalmGauge, Macro ["KP_Begin", "C-v"]) , (CaCalmValue, Yell) , (CaHPGauge, Macro ["KP_Begin", "C-v"]) , (CaHPValue, Wait) , (CaLeaderDesc, projectICmd flingTs) ] mouseMMB :: CmdTriple mouseMMB = ( [CmdMouse] , "snap crosshair to floor under pointer/cycle detail level" , XhairPointerFloor ) mouseMMBMute :: CmdTriple mouseMMBMute = ([CmdMouse], "", XhairPointerMute) mouseRMB :: CmdTriple mouseRMB = ( [CmdMouse] , "start aiming at enemy under pointer/cycle detail level" , ByAimMode aimMode ) where aimMode = AimModeCmd { exploration = ByArea $ common ++ [ (CaMapLeader, dropCmd) , (CaMapParty, SelectWithPointer) , (CaMap, AimPointerEnemy) , (CaArenaName, ExecuteIfClear MainMenuAutoOff) , (CaPercentSeen, autoexplore25Cmd) ] , aiming = ByArea $ common ++ [ (CaMap, XhairPointerEnemy) -- hack; same effect, but matches LMB , (CaArenaName, Cancel) , (CaPercentSeen, XhairStair False) ] } common = [ (CaMessage, Hint) , (CaLevelNumber, AimAscend (-1)) , (CaXhairDesc, AimItem) , (CaSelected, SelectWithPointer) -- , (CaCalmGauge, Macro ["C-KP_Begin", "A-v"]) , (CaCalmValue, Yell) , (CaHPGauge, Macro ["C-KP_Begin", "A-v"]) , (CaHPValue, Wait10) , (CaLeaderDesc, ComposeUnlessError ClearTargetIfItemClear ItemClear) ] -- This is duplicated wrt content, instead of included via @semicolon@, -- because the C- commands are less likely to be modified by the player -- and so more dependable than @semicolon@, @colon@, etc. goToCmd :: HumanCmd goToCmd = Macro ["A-MiddleButtonRelease", "C-semicolon", "C-quotedbl", "C-v"] -- This is duplicated wrt content, instead of included via @colon@, -- because the C- commands are less likely to be modified by the player -- and so more dependable than @semicolon@, @colon@, etc. runToAllCmd :: HumanCmd runToAllCmd = Macro ["A-MiddleButtonRelease", "C-colon", "C-quotedbl", "C-v"] autoexploreCmd :: HumanCmd autoexploreCmd = Macro ["C-?", "C-quotedbl", "C-v"] autoexplore25Cmd :: HumanCmd autoexplore25Cmd = Macro ["'", "C-?", "C-quotedbl", "'", "C-V"] aimFlingCmd :: HumanCmd aimFlingCmd = ComposeIfLocal AimPointerEnemy (projectICmd flingTs) projectICmd :: [TriggerItem] -> HumanCmd projectICmd ts = ComposeUnlessError (ChooseItemProject ts) Project projectI :: [TriggerItem] -> CmdTriple projectI ts = ([CmdItem], descIs ts, projectICmd ts) projectA :: [TriggerItem] -> CmdTriple projectA ts = let fling = Compose2ndLocal Project ItemClear flingICmd = ComposeUnlessError (ChooseItemProject ts) fling in replaceCmd (ByAimMode AimModeCmd { exploration = AimTgt , aiming = flingICmd }) (projectI ts) -- | flingTs - list containing one flingable projectile -- >>> flingTs -- [TriggerItem {tiverb = Text "fling", tiobject = Text "in-range projectile", tisymbols = ""}] -- -- I question the value of that test. But would Bob Martin like it -- on the grounds it's like double-bookkeeping? flingTs :: [TriggerItem] flingTs = [TriggerItem { tiverb = "fling" , tiobject = "in-range projectile" , tisymbols = [] }] applyIK :: [TriggerItem] -> CmdTriple applyIK ts = ([CmdItem], descIs ts, ComposeUnlessError (ChooseItemApply ts) Apply) applyI :: [TriggerItem] -> CmdTriple applyI ts = let apply = Compose2ndLocal Apply ItemClear in ([CmdItem], descIs ts, ComposeUnlessError (ChooseItemApply ts) apply) grabCmd :: HumanCmd grabCmd = MoveItem [CGround] CStash (Just "grab") True -- @CStash@ is the implicit default; refined in HandleHumanGlobalM grabItems :: Text -> CmdTriple grabItems t = ([CmdItemMenu, CmdItem], t, grabCmd) dropCmd :: HumanCmd dropCmd = MoveItem [CStash, CEqp] CGround Nothing False dropItems :: Text -> CmdTriple dropItems t = ([CmdItemMenu, CmdItem], t, dropCmd) descIs :: [TriggerItem] -> Text descIs [] = "trigger an item" descIs (t : _) = makePhrase [tiverb t, tiobject t] defaultHeroSelect :: Int -> (String, CmdTriple) defaultHeroSelect k = ([Char.intToDigit k], ([CmdMeta], "", PickLeader k)) macroRun25 :: [String] macroRun25 = ["C-comma", "C-v"] memberCycle :: Direction -> [CmdCategory] -> CmdTriple memberCycle d cats = ( cats , "cycle" <+> (if d == Backward then "backwards" else "") <+> "among all party members" , PointmanCycle d ) memberCycleLevel :: Direction -> [CmdCategory] -> CmdTriple memberCycleLevel d cats = ( cats , "cycle" <+> (if d == Backward then "backwards" else "") <+> " among party members on the level" , PointmanCycleLevel d ) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Content/Screen.hs0000644000000000000000000000654407346545000023672 0ustar0000000000000000-- | The type of definitions of screen layout and features. module Game.LambdaHack.Client.UI.Content.Screen ( ScreenContent(..), emptyScreenContent, makeData #ifdef EXPOSE_INTERNAL -- * Internal operations , emptyScreenContentRaw, validateSingle #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.ByteString as BS import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.RuleKind as RK import Game.LambdaHack.Definition.Defs -- | Screen layout and features definition. -- -- Warning: this type is not abstract, but its values should not be -- created ad hoc, even for unit tests, but should be constructed -- with @makeData@, which includes validation, -- -- The @emptyScreenContent@ is one such valid by construction value -- of this type. It's suitable for bootstrapping and for testing. data ScreenContent = ScreenContent { rwidth :: X -- ^ screen width , rheight :: Y -- ^ screen height , rwebAddress :: String -- ^ an extra blurb line for the main menu , rintroScreen :: ([String], [[String]]) -- ^ the intro screen (first help screen) text -- and the rest of the manual , rapplyVerbMap :: EM.EnumMap (ContentSymbol ItemKind) T.Text -- ^ verbs to use for apply actions , rFontFiles :: [(FilePath, BS.ByteString)] -- ^ embedded game-supplied font files } emptyScreenContentRaw :: ScreenContent emptyScreenContentRaw = ScreenContent { rwidth = 5 , rheight = 5 , rwebAddress = "" , rintroScreen = ([], []) , rapplyVerbMap = EM.empty , rFontFiles = [] } emptyScreenContent :: ScreenContent emptyScreenContent = assert (null $ validateSingle RK.emptyRuleContent emptyScreenContentRaw) emptyScreenContentRaw -- | Catch invalid rule kind definitions. validateSingle :: RK.RuleContent -> ScreenContent -> [Text] validateSingle corule ScreenContent{..} = (let tsGt80 = filter ((> 80) . T.length) $ map T.pack [rwebAddress] in case tsGt80 of [] -> [] tGt80 : _ -> ["rwebAddress's length is over 80:" <> tGt80]) ++ (let tsGt41 = filter ((> 41) . T.length) $ map T.pack $ fst rintroScreen in case tsGt41 of [] -> [] tGt41 : _ -> ["intro screen has a line with length over 41:" <> tGt41]) ++ (let tsGt80 = filter ((> 80) . T.length) $ map T.pack $ intercalate [""] $ snd rintroScreen in case tsGt80 of [] -> [] tGt80 : _ -> ["manual has a line with length over 80:" <> tGt80]) -- The following reflect the only current UI implementation. ++ [ "rwidth /= RK.rWidthMax" | rwidth /= RK.rWidthMax corule ] ++ [ "rheight /= RK.rHeightMax + 3" | rheight /= RK.rHeightMax corule + 3] makeData :: RK.RuleContent -> ScreenContent -> ScreenContent makeData corule sc = let singleOffenders = validateSingle corule sc in assert (null singleOffenders `blame` "Screen Content not valid" `swith` singleOffenders) sc LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/ContentClientUI.hs0000644000000000000000000000103607346545000024037 0ustar0000000000000000-- | General content types and operations. module Game.LambdaHack.Client.UI.ContentClientUI ( CCUI(..), emptyCCUI ) where import Prelude () import qualified Data.Map.Strict as M import Game.LambdaHack.Client.UI.Content.Input import Game.LambdaHack.Client.UI.Content.Screen -- | Operations for all UI content types, gathered together. data CCUI = CCUI { coinput :: InputContent , coscreen :: ScreenContent } emptyCCUI :: CCUI emptyCCUI = CCUI { coinput = InputContent M.empty [] M.empty , coscreen = emptyScreenContent } LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/DrawM.hs0000644000000000000000000011712407346545000022050 0ustar0000000000000000-- {-# OPTIONS_GHC -fprof-auto #-} -- | Display game data on the screen using one of the available frontends -- (determined at compile time with cabal flags). module Game.LambdaHack.Client.UI.DrawM ( targetDesc, targetDescXhair, drawHudFrame , checkWarningHP, checkWarningCalm #ifdef EXPOSE_INTERNAL -- * Internal operations , drawFrameTerrain, drawFrameContent , drawFramePath, drawFrameActor, drawFrameExtra, drawFrameStatus , drawArenaStatus, drawLeaderStatus, drawLeaderDamage, drawSelected , checkWarnings #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Monad.ST.Strict import qualified Data.Char as Char import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Int (Int64) import qualified Data.IntMap.Strict as IM import qualified Data.IntSet as IS import qualified Data.Text as T import qualified Data.Vector.Unboxed as U import qualified Data.Vector.Unboxed.Mutable as VM import Data.Word (Word16, Word32) import GHC.Exts (inline) import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.Frontend (frontendName) import Game.LambdaHack.Client.UI.ItemDescription import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.CaveKind (cname) import qualified Game.LambdaHack.Content.FactionKind as FK import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind (TileKind, isUknownSpace) import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal targetDesc :: MonadClientUI m => Maybe Target -> m (Maybe Text, Maybe Text) targetDesc mtarget = do arena <- getArenaUI lidV <- viewedLevelUI mleader <- getsClient sleader let describeActorTarget aid = do side <- getsClient sside b <- getsState $ getActorBody aid bUI <- getsSession $ getActorUI aid actorMaxSk <- getsState $ getActorMaxSkills aid let percentage = 100 * bhp b `div` xM (max 5 $ Ability.getSk Ability.SkMaxHP actorMaxSk) chs n = "[" <> T.replicate (4 - n) "_" <> T.replicate n "*" <> "]" stars = chs $ fromEnum $ max 0 $ min 4 $ percentage `div` 20 hpIndicator = if bfid b == side then Nothing else Just stars return (Just $ bname bUI, hpIndicator) case mtarget of Just (TEnemy aid) -> describeActorTarget aid Just (TNonEnemy aid) -> describeActorTarget aid Just (TPoint tgoal lid p) -> case tgoal of TEnemyPos{} -> do let hotText = if lid == lidV && arena == lidV then "hot spot" <+> tshow p else "a hot spot on level" <+> tshow (abs $ fromEnum lid) return (Just hotText, Nothing) _ -> do -- the other goals can be invalidated by now anyway and it's -- better to say what there is rather than what there isn't pointedText <- if lid == lidV && arena == lidV then do bag <- getsState $ getFloorBag lid p case EM.assocs bag of [] -> return $! "spot" <+> tshow p [(iid, kit@(k, _))] -> do localTime <- getsState $ getLocalTime lid itemFull <- getsState $ itemToFull iid side <- getsClient sside factionD <- getsState sfactionD CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui let (name, powers) = partItem rwidth side factionD localTime itemFull kit return $! makePhrase [MU.Car1Ws k name, powers] _ -> return $! "many items at" <+> tshow p else return $! "an exact spot on level" <+> tshow (abs $ fromEnum lid) return (Just pointedText, Nothing) Just TVector{} -> do mtgtPos <- getsState $ aidTgtToPos mleader lidV mtarget let invalidMsg = "a relative shift" validMsg p = "shift to" <+> tshow p return (Just $ maybe invalidMsg validMsg mtgtPos, Nothing) Nothing -> return (Nothing, Nothing) targetDescXhair :: MonadClientUI m => m (Maybe Text, Maybe Text, Maybe Watchfulness) targetDescXhair = do sxhair <- getsSession sxhair (mhairDesc, mxhairHP) <- targetDesc sxhair let maid = case sxhair of Just (TEnemy a) -> Just a Just (TNonEnemy a) -> Just a _ -> Nothing case maid of Nothing -> return (mhairDesc, mxhairHP, Nothing) Just aid -> do watchfulness <- bwatch <$> getsState (getActorBody aid) return (mhairDesc, mxhairHP, Just watchfulness) drawFrameTerrain :: forall m. MonadClientUI m => LevelId -> m (U.Vector Word32) drawFrameTerrain drawnLevelId = do COps{corule=RuleContent{rWidthMax}, cotile, coTileSpeedup} <- getsState scops StateClient{smarkSuspect} <- getClient -- Not @ScreenContent@, because indexing in level's data. Level{ltile=PointArray.Array{avector}, lembed} <- getLevel drawnLevelId totVisible <- totalVisible <$> getPerFid drawnLevelId frameStatus <- drawFrameStatus drawnLevelId let dis :: PointI -> ContentId TileKind -> Color.AttrCharW32 {-# INLINE dis #-} dis pI tile = let TK.TileKind{tsymbol, tcolor, tcolor2} = okind cotile tile -- @smarkSuspect@ can be turned off easily, so let's overlay it -- over both visible and remembered tiles. fg :: Color.Color fg | smarkSuspect > 0 && Tile.isSuspect coTileSpeedup tile = Color.BrMagenta | smarkSuspect > 1 && Tile.isHideAs coTileSpeedup tile = Color.Magenta | -- Converting maps is cheaper than converting points -- and this function is a bottleneck, so we hack a bit. pI `IS.member` ES.enumSetToIntSet totVisible -- If all embeds spent, mark it with darker colour. && not (Tile.isEmbed coTileSpeedup tile && pI `IM.notMember` EM.enumMapToIntMap lembed) = tcolor | otherwise = tcolor2 in Color.attrChar2ToW32 fg tsymbol g :: PointI -> Word16 -> Word32 g !pI !tile = Color.attrCharW32 $ dis pI (DefsInternal.toContentId tile) caveVector :: U.Vector Word32 caveVector = U.imap g avector messageVector = U.replicate rWidthMax (Color.attrCharW32 Color.spaceAttrW32) statusVector = U.fromListN (2 * rWidthMax) $ map Color.attrCharW32 frameStatus -- The vector package is so smart that the 3 vectors are not allocated -- separately at all, but written to the big vector at once. -- But even with double allocation it would be faster than writing -- to a mutable vector via @FrameForall@. return $ U.concat [messageVector, caveVector, statusVector] drawFrameContent :: forall m. MonadClientUI m => LevelId -> m FrameForall drawFrameContent drawnLevelId = do COps{corule=RuleContent{rWidthMax}} <- getsState scops SessionUI{smarkSmell} <- getSession -- Not @ScreenContent@, because indexing in level's data. Level{lsmell, ltime, lfloor} <- getLevel drawnLevelId itemToF <- getsState $ flip itemToFull let {-# INLINE viewItemBag #-} viewItemBag _ floorBag = case EM.toDescList floorBag of (iid, _kit) : _ -> viewItem $ itemToF iid [] -> error $ "lfloor not sparse" `showFailure` () viewSmell :: PointI -> Time -> Color.AttrCharW32 {-# INLINE viewSmell #-} viewSmell pI sml = let fg = toEnum $ pI `rem` 13 + 2 smlt = smellTimeout `timeDeltaSubtract` (sml `timeDeltaToFrom` ltime) in Color.attrChar2ToW32 fg (timeDeltaToDigit smellTimeout smlt) mapVAL :: forall a s. (PointI -> a -> Color.AttrCharW32) -> [(PointI, a)] -> FrameST s {-# INLINE mapVAL #-} mapVAL f l v = do let g :: (PointI, a) -> ST s () g (!pI, !a0) = do let w = Color.attrCharW32 $ f pI a0 VM.write v (pI + rWidthMax) w mapM_ g l -- We don't usually show embedded items, because normally we don't -- want them to clutter the display. If they are really important, -- the tile they reside on has special colours and changes as soon -- as the item disappears. In the remaining cases, the main menu -- UI setting for suspect terrain highlights most tiles with embeds. upd :: FrameForall upd = FrameForall $ \v -> do mapVAL viewItemBag (IM.assocs $ EM.enumMapToIntMap lfloor) v when smarkSmell $ mapVAL viewSmell (filter ((> ltime) . snd) $ IM.assocs $ EM.enumMapToIntMap lsmell) v return upd drawFramePath :: forall m. MonadClientUI m => LevelId -> m (FrameForall, FrameForall) drawFramePath drawnLevelId = do SessionUI{saimMode} <- getSession sreportNull <- getsSession sreportNull let frameForallId = FrameForall $ const $ return () case saimMode of Just AimMode{detailLevel} | not sreportNull -> do COps{corule=RuleContent{rWidthMax, rHeightMax}, coTileSpeedup} <- getsState scops StateClient{seps} <- getClient -- Not @ScreenContent@, because pathing in level's map. Level{ltile=PointArray.Array{avector}} <- getLevel drawnLevelId totVisible <- totalVisible <$> getPerFid drawnLevelId mleader <- getsClient sleader xhairPos <- xhairToPos bline <- case mleader of Just leader -> do Actor{bpos, blid} <- getsState $ getActorBody leader return $! if blid /= drawnLevelId then [] else fromMaybe [] $ bresenhamsLineAlgorithm seps bpos xhairPos _ -> return [] mpath <- maybe (return Nothing) (\aid -> do mtgtMPath <- getsClient $ EM.lookup aid . stargetD case mtgtMPath of Just TgtAndPath{tapPath=tapPath@(Just AndPath{pathGoal})} | pathGoal == xhairPos -> return tapPath _ -> getCachePath aid xhairPos) mleader assocsAtxhair <- getsState $ posToAidAssocs xhairPos drawnLevelId let shiftedBTrajectory = case assocsAtxhair of (_, Actor{btrajectory = Just p, bpos = prPos}) : _ | detailLevel == defaultDetailLevel -> trajectoryToPath prPos (fst p) _ -> [] shiftedLine = delete xhairPos $ takeWhile (insideP (0, 0, rWidthMax - 1, rHeightMax - 1)) $ if null shiftedBTrajectory then bline else shiftedBTrajectory lpath = if not (null bline) && null shiftedBTrajectory then delete xhairPos $ maybe [] pathList mpath else [] acOnPathOrLine :: Char -> Point -> ContentId TileKind -> Color.AttrCharW32 acOnPathOrLine !ch !p0 !tile = let fgOnPathOrLine = case ( ES.member p0 totVisible , Tile.isWalkable coTileSpeedup tile ) of _ | isUknownSpace tile -> Color.BrBlack _ | Tile.isSuspect coTileSpeedup tile -> Color.BrMagenta (True, True) -> Color.BrGreen (True, False) -> Color.BrRed (False, True) -> Color.Green (False, False) -> Color.Red in Color.attrChar2ToW32 fgOnPathOrLine ch mapVTL :: forall s. (Point -> ContentId TileKind -> Color.AttrCharW32) -> [Point] -> FrameST s mapVTL f l v = do let g :: Point -> ST s () g !p0 = do let pI = fromEnum p0 tile = avector U.! pI w = Color.attrCharW32 $ f p0 (DefsInternal.toContentId tile) VM.write v (pI + rWidthMax) w mapM_ g l upd :: FrameForall upd = FrameForall $ \v -> do mapVTL (acOnPathOrLine ';') lpath v mapVTL (acOnPathOrLine '*') shiftedLine v -- overwrites path return (upd, if null shiftedBTrajectory then frameForallId else upd) _ -> return (frameForallId, frameForallId) drawFrameActor :: forall m. MonadClientUI m => LevelId -> m FrameForall drawFrameActor drawnLevelId = do COps{corule=RuleContent{rWidthMax}} <- getsState scops SessionUI{sactorUI, sselected, sUIOptions} <- getSession -- Not @ScreenContent@, because indexing in level's data. Level{lbig, lproj} <- getLevel drawnLevelId side <- getsClient sside s <- getState let {-# INLINE viewBig #-} viewBig aid = let Actor{bhp, bfid, btrunk, bwatch} = getActorBody aid s ActorUI{bsymbol, bcolor} = sactorUI EM.! aid Item{jfid} = getItemBody btrunk s symbol | bhp > 0 = bsymbol | otherwise = '%' dominated = maybe False (/= bfid) jfid bg = if | bwatch == WSleep -> Color.HighlightBlue | dominated -> if bfid == side -- dominated by us then Color.HighlightCyan else Color.HighlightBrown | ES.member aid sselected -> Color.HighlightGreen | otherwise -> Color.HighlightNone fg | bfid /= side || bhp <= 0 = bcolor | otherwise = let (hpCheckWarning, calmCheckWarning) = checkWarnings sUIOptions aid s in if hpCheckWarning || calmCheckWarning then Color.Red else bcolor in Color.attrCharToW32 $ Color.AttrChar Color.Attr{..} symbol {-# INLINE viewProj #-} viewProj as = case as of aid : _ -> let ActorUI{bsymbol, bcolor} = sactorUI EM.! aid bg = Color.HighlightNone fg = bcolor in Color.attrCharToW32 $ Color.AttrChar Color.Attr{..} bsymbol [] -> error $ "lproj not sparse" `showFailure` () mapVAL :: forall a s. (a -> Color.AttrCharW32) -> [(PointI, a)] -> FrameST s {-# INLINE mapVAL #-} mapVAL f l v = do let g :: (PointI, a) -> ST s () g (!pI, !a0) = do let w = Color.attrCharW32 $ f a0 VM.write v (pI + rWidthMax) w mapM_ g l upd :: FrameForall upd = FrameForall $ \v -> do mapVAL viewProj (IM.assocs $ EM.enumMapToIntMap lproj) v mapVAL viewBig (IM.assocs $ EM.enumMapToIntMap lbig) v -- big actor overlay projectiles return upd drawFrameExtra :: forall m. MonadClientUI m => ColorMode -> LevelId -> m FrameForall drawFrameExtra dm drawnLevelId = do -- Not @ScreenContent@, because indexing in level's data. COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops SessionUI{saimMode, smarkVision} <- getSession mleader <- getsClient sleader mbody <- getsState $ \s -> flip getActorBody s <$> mleader totVisible <- totalVisible <$> getPerFid drawnLevelId mxhairPos <- mxhairToPos mtgtPos <- do mtgt <- getsClient $ maybe (const Nothing) getTarget mleader getsState $ aidTgtToPos mleader drawnLevelId mtgt side <- getsClient sside factionD <- getsState sfactionD let visionMarks = IS.toList $ ES.enumSetToIntSet totVisible backlightVision :: Color.AttrChar -> Color.AttrChar backlightVision ac = case ac of Color.AttrChar (Color.Attr fg Color.HighlightNone) ch -> Color.AttrChar (Color.Attr fg Color.HighlightBackground) ch _ -> ac writeSquare !hi (Color.AttrChar (Color.Attr fg bg) ch) = let hiUnlessLeader | bg == Color.HighlightYellow = bg | otherwise = hi in Color.AttrChar (Color.Attr fg hiUnlessLeader) ch turnBW (Color.AttrChar _ ch) = Color.AttrChar Color.defAttr ch mapVL :: forall s. (Color.AttrChar -> Color.AttrChar) -> [PointI] -> FrameST s mapVL f l v = do let g :: PointI -> ST s () g !pI = do w0 <- VM.read v (pI + rWidthMax) let w = Color.attrCharW32 . Color.attrCharToW32 . f . Color.attrCharFromW32 . Color.AttrCharW32 $ w0 VM.write v (pI + rWidthMax) w mapM_ g l -- Here @rWidthMax@ and @rHeightMax@ are correct, because we are not -- turning the whole screen into black&white, but only the level map. lDungeon = [0..rWidthMax * rHeightMax - 1] leaderColor = if isJust saimMode then Color.HighlightYellowAim else Color.HighlightYellow xhairColor = if isJust saimMode then Color.HighlightRedAim else Color.HighlightRed locateStash (fid, fact) = case gstash fact of Just (lid, pos) | lid == drawnLevelId -> let stashColor = if fid == side then Color.HighlightWhite else Color.HighlightMagenta in Just (pos, stashColor) _ -> Nothing stashesToDisplay = mapMaybe locateStash $ EM.assocs factionD upd :: FrameForall upd = FrameForall $ \v -> do when (isJust saimMode && smarkVision >= 1 || smarkVision == 2) $ mapVL backlightVision visionMarks v case mtgtPos of Nothing -> return () Just p -> mapVL (writeSquare Color.HighlightGrey) [fromEnum p] v mapM_ (\(pos, color) -> mapVL (writeSquare color) [fromEnum pos] v) stashesToDisplay case mbody of -- overwrites target Just body | drawnLevelId == blid body -> mapVL (writeSquare leaderColor) [fromEnum $ bpos body] v _ -> return () case mxhairPos of -- overwrites target and non-aim leader box Nothing -> return () Just p -> mapVL (writeSquare xhairColor) [fromEnum p] v when (dm == ColorBW) $ mapVL turnBW lDungeon v return upd drawFrameStatus :: MonadClientUI m => LevelId -> m AttrString drawFrameStatus drawnLevelId = do cops@COps{corule=RuleContent{rWidthMax=_rWidthMax}} <- getsState scops SessionUI{sselected, saimMode, swaitTimes, sitemSel} <- getSession mleader <- getsClient sleader mxhairPos <- mxhairToPos mbfs <- maybe (return Nothing) (fmap Just . getCacheBfs) mleader (mhairDesc, mxhairHP, mxhairWatchfulness) <- targetDescXhair lvl <- getLevel drawnLevelId side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD (mblid, mbpos, mbodyUI) <- case mleader of Just leader -> do Actor{bpos, blid} <- getsState $ getActorBody leader bodyUI <- getsSession $ getActorUI leader return (Just blid, Just bpos, Just bodyUI) Nothing -> return (Nothing, Nothing, Nothing) let widthX = 80 widthTgt = 39 widthStatus = widthX - widthTgt - 1 arenaStatus = drawArenaStatus cops lvl widthStatus leaderStatusWidth = 23 leaderStatus <- drawLeaderStatus swaitTimes (selectedStatusWidth, selectedStatus) <- drawSelected drawnLevelId (widthStatus - leaderStatusWidth) sselected let speedStatusWidth = widthStatus - leaderStatusWidth - selectedStatusWidth speedDisplay <- case mleader of Nothing -> return [] Just leader -> do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader kitAssRaw <- getsState $ kitAssocs leader [CEqp, COrgan] let speed = Ability.getSk Ability.SkSpeed actorCurAndMaxSk unknownBonus = unknownSpeedBonus $ map (fst . snd) kitAssRaw speedString = displaySpeed speed ++ if unknownBonus then "?" else "" conditionBonus = conditionSpeedBonus $ map snd kitAssRaw cspeed = case compare conditionBonus 0 of LT -> Color.Red EQ -> Color.White GT -> Color.Green return $! map (Color.attrChar2ToW32 cspeed) speedString let speedStatus = if length speedDisplay >= speedStatusWidth then [] else speedDisplay ++ [Color.spaceAttrW32] displayPathText mp mt = let (plen, llen) | Just target <- mp , Just bfs <- mbfs , Just bpos <- mbpos , mblid == Just drawnLevelId = ( fromMaybe 0 (accessBfs bfs target) , chessDist bpos target ) | otherwise = (0, 0) pText | plen == 0 = "" | otherwise = "p" <> tshow plen lText | llen == 0 = "" | otherwise = "l" <> tshow llen text = fromMaybe (pText <+> lText) mt in if T.null text then "" else " " <> text -- The indicators must fit, they are the actual information. pathCsr = displayPathText mxhairPos mxhairHP trimTgtDesc n t = assert (not (T.null t) && n > 2 `blame` (t, n)) $ if T.length t <= n then t else T.take (n - 3) t <> "..." -- The indicators must fit, they are the actual information. widthXhairOrItem = widthTgt - T.length pathCsr nMember = MU.Ord $ 1 + sum (EM.elems $ gvictims fact) fallback = if FK.fhasPointman (gkind fact) then makePhrase ["Waiting for", nMember, "team member to spawn"] else "This faction never picks a pointman" leaderName bUI = trimTgtDesc (widthTgt - 10) (bname bUI) leaderBlurbLong = maybe fallback (\bUI -> "Pointman:" <+> leaderName bUI) mbodyUI leaderBlurbShort = maybe fallback leaderName mbodyUI ours <- getsState $ fidActorNotProjGlobalAssocs side ns <- getsState $ EM.size . getFactionStashBag side let na = length ours nl = ES.size $ ES.fromList $ map (blid . snd) ours -- To be replaced by something more useful. teamBlurb = textToAS $ trimTgtDesc widthTgt $ makePhrase [ "Team:" , MU.CarWs na "actor", "on" , MU.CarWs nl "level" <> "," , "stash", MU.Car ns ] markSleepTgtDesc | mxhairWatchfulness /= Just WSleep = textToAS | otherwise = textFgToAS Color.cSleep xdetail AimMode{detailLevel} = "x" <> tshow (1 + fromEnum detailLevel) xhairName aimMode = "Crosshair" <+> xdetail aimMode xhairBlurb = maybe teamBlurb (\t -> case saimMode of Just aimMode -> textToAS (xhairName aimMode <> ":") <+:> markSleepTgtDesc (trimTgtDesc (widthXhairOrItem - 14) t) Nothing -> markSleepTgtDesc (trimTgtDesc widthXhairOrItem t)) mhairDesc tgtOrItem | Just (iid, fromCStore, _) <- sitemSel , Just leader <- mleader = do b <- getsState $ getActorBody leader bag <- getsState $ getBodyStoreBag b fromCStore case iid `EM.lookup` bag of Nothing -> return (xhairBlurb, pathCsr) Just kit@(k, _) -> do localTime <- getsState $ getLocalTime (blid b) itemFull <- getsState $ itemToFull iid factionD <- getsState sfactionD CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui let (name, powers) = partItem rwidth (bfid b) factionD localTime itemFull kit t = makePhrase [MU.Car1Ws k name, powers] xhairHP = maybe "" (" " <>) mxhairHP (xItemWidth, xItemText) = case saimMode of Just aimMode -> (9, "Item" <+> xdetail aimMode) Nothing -> (6, "Item") trimTD = trimTgtDesc (widthTgt - T.length xhairHP - xItemWidth) t return (textToAS $ xItemText <> ":" <+> trimTD, xhairHP) | otherwise = return (xhairBlurb, pathCsr) (xhairLine, pathXhairOrNull) <- tgtOrItem damageStatus <- maybe (return []) (drawLeaderDamage widthTgt) mleader let damageStatusWidth = length damageStatus withForLeader = widthTgt - damageStatusWidth - 1 leaderBottom = if | T.length leaderBlurbShort > withForLeader -> "" | T.length leaderBlurbLong > withForLeader -> leaderBlurbShort | otherwise -> leaderBlurbLong damageGap = blankAttrString $ widthTgt - damageStatusWidth - T.length leaderBottom xhairGap = blankAttrString (widthTgt - T.length pathXhairOrNull - length xhairLine) xhairStatus = xhairLine ++ xhairGap ++ textToAS pathXhairOrNull selectedGap = blankAttrString (widthStatus - leaderStatusWidth - selectedStatusWidth - length speedStatus) status = arenaStatus <> [Color.spaceAttrW32] <> xhairStatus <> selectedStatus ++ selectedGap ++ speedStatus ++ leaderStatus <> [Color.spaceAttrW32] <> (textToAS leaderBottom ++ damageGap ++ damageStatus) -- Keep it at least partially lazy, to avoid allocating the whole list: return #ifdef WITH_EXPENSIVE_ASSERTIONS $ assert (length status == 2 * _rWidthMax `blame` map Color.charFromW32 status) #endif status -- | Draw the whole screen: level map and status area. drawHudFrame :: MonadClientUI m => ColorMode -> LevelId -> m PreFrame drawHudFrame dm drawnLevelId = do baseTerrain <- drawFrameTerrain drawnLevelId updContent <- drawFrameContent drawnLevelId (updPath, updTrajectory) <- drawFramePath drawnLevelId updActor <- drawFrameActor drawnLevelId updExtra <- drawFrameExtra dm drawnLevelId soptions <- getsClient soptions let upd = FrameForall $ \v -> do unFrameForall updContent v -- ANSI frontend is screen-reader friendly, so avoid visual fluff unless (frontendName soptions == "ANSI") $ unFrameForall updPath v unFrameForall updActor v unFrameForall updTrajectory v unFrameForall updExtra v return (baseTerrain, upd) -- Comfortably accomodates 3-digit level numbers and 25-character -- level descriptions (currently enforced max). -- -- Sometimes the level seems fully explored, but the display shows -- 99% or even goes from 100% to 99% at some moment. -- This is due to monsters, e.g., clearning rubble or burning bush, -- and so creating a new explorable terrain. drawArenaStatus :: COps -> Level -> Int -> AttrString drawArenaStatus COps{cocave} Level{lkind, ldepth=Dice.AbsDepth ld, lseen, lexpl} width = let ck = okind cocave lkind seenN = 100 * lseen `div` max 1 lexpl seenTxt | seenN >= 100 = "all" | otherwise = tshow seenN <> "%" lvlN = T.justifyLeft 2 ' ' (tshow ld) seenStatus = "[" <> seenTxt <+> "seen]" in textToAS $ T.take (width - 10) (T.justifyLeft (width - 10) ' ' (lvlN <+> cname ck)) <> T.justifyRight 10 ' ' seenStatus drawLeaderStatus :: MonadClientUI m => Int -> m AttrString drawLeaderStatus waitT = do time <- getsState stime let calmHeaderText = "Calm" hpHeaderText = "HP" slashes = ["/", "|", "\\", "|"] waitGlobal = timeFit time timeTurn sUIOptions <- getsSession sUIOptions mleader <- getsClient sleader case mleader of Just leader -> do b <- getsState $ getActorBody leader actorCurAndMaxSk <- getsState $ getActorMaxSkills leader (hpCheckWarning, calmCheckWarning) <- getsState $ checkWarnings sUIOptions leader bdark <- getsState $ not . actorInAmbient b let showTrunc x = let t = show x in if length t > 3 then if x > 0 then "***" else "---" else t waitSlash | bwatch b == WSleep = waitGlobal | otherwise = abs waitT -- This is a valuable feedback for the otherwise hard to observe -- 'wait' command or for passing of time when sole leader sleeps. slashPick = slashes !! (max 0 waitSlash `mod` length slashes) addColor c = map (Color.attrChar2ToW32 c) checkDelta ResDelta{..} | fst resCurrentTurn < 0 || fst resPreviousTurn < 0 = addColor Color.BrRed -- alarming news have priority | snd resCurrentTurn > 0 || snd resPreviousTurn > 0 = addColor Color.BrGreen | otherwise = stringToAS -- only if nothing at all noteworthy checkSleep body resDelta | bwatch body == WSleep = addColor Color.cSleep | otherwise = checkDelta resDelta calmAddAttr = checkSleep b $ bcalmDelta b -- We only show ambient light, because in fact client can't tell -- if a tile is lit, because it it's seen it may be due to ambient -- or dynamic light or due to infravision. darkPick | bdark = "." | otherwise = ":" calmHeader = calmAddAttr $ calmHeaderText <> darkPick maxCalm = max 0 $ Ability.getSk Ability.SkMaxCalm actorCurAndMaxSk calmText = showTrunc (bcalm b `divUp` oneM) <> (if bdark then slashPick else "/") <> showTrunc maxCalm bracePick | actorWaits b = "}" | otherwise = ":" hpAddAttr = checkDelta $ bhpDelta b hpHeader = hpAddAttr $ hpHeaderText <> bracePick maxHP = max 0 $ Ability.getSk Ability.SkMaxHP actorCurAndMaxSk hpText = showTrunc (bhp b `divUp` oneM) <> (if not bdark then slashPick else "/") <> showTrunc maxHP justifyRight n t = replicate (n - length t) ' ' ++ t colorWarning w enough full | w = addColor Color.Red | not enough = addColor Color.Brown | full = addColor Color.Magenta | otherwise = stringToAS return $! calmHeader <> colorWarning calmCheckWarning (calmEnough b actorCurAndMaxSk) (bcalm b > xM maxCalm) (justifyRight 7 calmText) <+:> hpHeader <> colorWarning hpCheckWarning True (bhp b > xM maxHP) (justifyRight 7 hpText) Nothing -> do -- This is a valuable feedback for passing of time while faction -- leaderless and especially while temporarily actor-less.. let slashPick = slashes !! (max 0 waitGlobal `mod` length slashes) return $! stringToAS (calmHeaderText ++ ": --" ++ slashPick ++ "--") <+:> stringToAS (hpHeaderText <> ": --/--") drawLeaderDamage :: MonadClientUI m => Int -> ActorId -> m AttrString drawLeaderDamage width leader = do kitAssRaw <- getsState $ kitAssocs leader [CEqp, COrgan] actorCurAndMaxSk <- getsState $ getActorMaxSkills leader let unBurn (IK.Burn d) = Just d unBurn _ = Nothing unRefillHP (IK.RefillHP n) = Just n unRefillHP _ = Nothing hasNonDamagesEffect itemFull = any (\eff -> IK.forApplyEffect eff && not (IK.forDamageEffect eff)) (IK.ieffects $ itemKind itemFull) ppDice :: Bool -> (Bool, Int, Int, ItemFullKit) -> [(Bool, (AttrString, AttrString))] ppDice showInBrief (hasEffect, timeout, ncha, (itemFull, (k, _))) = let dice = IK.idamage $ itemKind itemFull tdice = case Dice.reduceDice dice of Just d | showInBrief -> show d _ -> show dice -- We ignore nested effects because they are, in general, avoidable. -- We also ignore repeated effect kinds for HUD simplicity. tBurn = maybe "" (('+' :) . show) $ listToMaybe $ mapMaybe unBurn $ IK.ieffects $ itemKind itemFull nRefillHP = maybe 0 (min 0) $ listToMaybe $ mapMaybe unRefillHP $ IK.ieffects $ itemKind itemFull tRefillHP | nRefillHP < 0 = '+' : show (- nRefillHP) | otherwise = "" tdiceEffect = if hasEffect && hasNonDamagesEffect itemFull then map Char.toUpper tdice else tdice ldice color = map (Color.attrChar2ToW32 color) tdiceEffect lBurnHP charged = let cburn = if charged then Color.BrRed else Color.Red chp = if charged then Color.BrMagenta else Color.Magenta in map (Color.attrChar2ToW32 cburn) tBurn ++ map (Color.attrChar2ToW32 chp) tRefillHP possiblyHasTimeout = timeout > 0 || itemSuspect itemFull in if possiblyHasTimeout then replicate (k - ncha) (False, (ldice Color.Cyan, lBurnHP False)) ++ replicate ncha (True, (ldice Color.BrCyan, lBurnHP True)) else [(True, (ldice Color.BrBlue, lBurnHP True))] lbonus :: AttrString lbonus = let bonusRaw = Ability.getSk Ability.SkHurtMelee actorCurAndMaxSk bonus = min 200 $ max (-200) bonusRaw unknownBonus = unknownMeleeBonus $ map (fst . snd) kitAssRaw tbonus = if bonus == 0 then if unknownBonus then "+?" else "" else (if bonus > 0 then "+" else "") <> show bonus <> (if bonus /= bonusRaw then "$" else "") <> if unknownBonus then "%?" else "%" conditionBonus = conditionMeleeBonus $ map snd kitAssRaw cbonus = case compare conditionBonus 0 of LT -> Color.Red EQ -> Color.White GT -> Color.Green in map (Color.attrChar2ToW32 cbonus) tbonus let kitAssOnlyWeapons = filter (IA.checkFlag Ability.Meleeable . aspectRecordFull . fst . snd) kitAssRaw discoBenefit <- getsClient sdiscoBenefit strongest <- map (\(_, hasEffect, timeout, ncha, _, itemFullKit) -> (hasEffect, timeout, ncha, itemFullKit)) <$> pickWeaponM True (Just discoBenefit) kitAssOnlyWeapons actorCurAndMaxSk leader let possiblyHasTimeout (_, timeout, _, (itemFull, _)) = timeout > 0 || itemSuspect itemFull (lT, lTrest) = span possiblyHasTimeout strongest strongestToDisplay = lT ++ case lTrest of [] -> [] noTimeout : lTrest2 -> noTimeout : filter possiblyHasTimeout lTrest2 -- the second portion of timeout weapons won't ever be used -- but often it's the player's mistake, so show them anyway showStrongest showInBrief l = let lToDisplay = concatMap (ppDice showInBrief) l (ldischarged, lrest) = break fst lToDisplay lWithBonus = case map snd lrest of [] -> [] -- 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" then case lextra of [] -> ldmg _plus : lextraRest -> lextraRest else ldmg ++ lextra in intercalate [Color.spaceAttrW32] $ map displayDmgAndExtra $ map snd ldischarged ++ lWithBonus lFull = showStrongest False strongestToDisplay lBrief = showStrongest True strongestToDisplay lFits | length lFull <= width = lFull -- the prevailing case, so optimized for this case only | length lBrief <= width = lBrief | otherwise = take (width - 3) lBrief ++ stringToAS "..." return $! lFits drawSelected :: MonadClientUI m => LevelId -> Int -> ES.EnumSet ActorId -> m (Int, AttrString) drawSelected drawnLevelId width selected = do mleader <- getsClient sleader side <- getsClient sside sactorUI <- getsSession sactorUI ours <- getsState $ filter (not . bproj . snd) . inline actorAssocs (== side) drawnLevelId let oursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) ours viewOurs (aid, Actor{bhp, bwatch}, ActorUI{bsymbol, bcolor}) = -- Sleep considered before being selected, because sleeping -- actors can't move, so selection is mostly irrelevant. -- Domination not considered at all, because map already shows it -- and so here is the only place where selection is conveyed. let bg = if | mleader == Just aid -> Color.HighlightYellow | bwatch == WSleep -> Color.HighlightBlue | ES.member aid selected -> Color.HighlightGreen | otherwise -> Color.HighlightNone sattr = Color.Attr {Color.fg = bcolor, bg} in Color.attrCharToW32 $ Color.AttrChar sattr $ if bhp > 0 then bsymbol else '%' maxViewed = width - 2 len = length oursUI star = let fg = case ES.size selected of 0 -> Color.BrBlack n | n == len -> Color.BrWhite _ -> Color.defFG char = if len > maxViewed then '$' else '*' in Color.attrChar2ToW32 fg char viewed = map viewOurs $ take maxViewed $ sortOn keySelected oursUI return (min width (len + 2), [star] ++ viewed ++ [Color.spaceAttrW32]) checkWarningHP :: UIOptions -> ActorId -> Int64 -> State -> Bool checkWarningHP UIOptions{uhpWarningPercent} leader hp s = let actorCurAndMaxSk = getActorMaxSkills leader s maxHp = Ability.getSk Ability.SkMaxHP actorCurAndMaxSk in hp <= xM (uhpWarningPercent * maxHp `div` 100) checkWarningCalm :: UIOptions -> ActorId -> Int64 -> State -> Bool checkWarningCalm UIOptions{uhpWarningPercent} leader calm s = let b = getActorBody leader s actorCurAndMaxSk = getActorMaxSkills leader s isImpression iid = maybe False (> 0) $ lookup IK.S_IMPRESSED $ IK.ifreq $ getIidKind iid s isImpressed = any isImpression $ EM.keys $ borgan b maxCalm = Ability.getSk Ability.SkMaxCalm actorCurAndMaxSk in calm <= xM (uhpWarningPercent * maxCalm `div` 100) && isImpressed checkWarnings :: UIOptions -> ActorId -> State -> (Bool, Bool) checkWarnings uiOptions leader s = let b = getActorBody leader s in ( checkWarningHP uiOptions leader (bhp b) s , checkWarningCalm uiOptions leader (bcalm b) s ) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/EffectDescription.hs0000644000000000000000000006472507346545000024446 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Description of effects. module Game.LambdaHack.Client.UI.EffectDescription ( DetailLevel(..), defaultDetailLevel , effectToSuffix, detectToObject, detectToVerb , skillName, skillDesc, skillToDecorator, skillsInDisplayOrder , kindAspectToSuffix, aspectToSentence, affixDice , describeToolsAlternative, describeCrafting, wrapInParens #ifdef EXPOSE_INTERNAL -- * Internal operations , conditionToObject, activationFlagToObject, slotToSentence, tmodToSuff , affixBonus, wrapInChevrons #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.Text as T import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Time import Game.LambdaHack.Content.ItemKind import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Definition.Ability import Game.LambdaHack.Definition.Defs data DetailLevel = DetailLow | DetailMedium | DetailHigh | DetailAll deriving (Show, Eq, Ord, Enum, Bounded, Generic) instance Binary DetailLevel defaultDetailLevel :: DetailLevel defaultDetailLevel = DetailAll -- TODO: take from config file, #217 -- | Suffix to append to a basic content name if the content causes the effect. -- -- We show absolute time in seconds, not @moves@, because actors can have -- different speeds (and actions can potentially take different time intervals). -- We call the time taken by one player move, when walking, a @move@. -- @Turn@ and @clip@ are used mostly internally, the former as an absolute -- time unit. -- We show distances in @steps@, because one step, from a tile to another -- tile, is always 1 meter. We don't call steps @tiles@, reserving -- that term for the context of terrain kinds or units of area. effectToSuffix :: DetailLevel -> Effect -> Text effectToSuffix detailLevel effect = case effect of Burn d -> wrapInParens (tshow d <+> if Dice.supDice d > 1 then "burns" else "burn") Explode t -> "of" <+> displayGroupName t <+> "explosion" RefillHP p | p > 0 -> "of healing" <+> wrapInParens (affixBonus p) RefillHP 0 -> error $ "" `showFailure` effect RefillHP p -> "of wounding" <+> wrapInParens (tshow $ abs p) RefillCalm p | p > 0 -> "of soothing" <+> wrapInParens (affixBonus p) RefillCalm 0 -> error $ "" `showFailure` effect RefillCalm p -> "of dismaying" <+> wrapInParens (tshow $ abs p) Dominate -> "of domination" Impress -> "of impression" PutToSleep -> "of sleep" Yell -> "of alarm" -- minor, but if under timeout, differentiates items Summon grp d -> makePhrase [ "of summoning" , if Dice.supDice d <= 1 then "" else MU.Text $ tshow d , MU.Ws $ MU.Text $ displayGroupName grp ] ApplyPerfume -> "of smell removal" Ascend True -> "of ascending" Ascend False -> "of descending" Escape{} -> "of escaping" Paralyze dice -> let time = case Dice.reduceDice dice of Nothing -> tshow dice <+> "* 0.05s" Just p -> let dt = timeDeltaScale (Delta timeClip) p in timeDeltaInSecondsText dt in "of paralysis for" <+> time ParalyzeInWater dice -> let time = case Dice.reduceDice dice of Nothing -> tshow dice <+> "* 0.05s" Just p -> let dt = timeDeltaScale (Delta timeClip) p in timeDeltaInSecondsText dt in "of retardation for" <+> time InsertMove dice -> let moves = case Dice.reduceDice dice of Nothing -> tshow dice <+> "tenths of a move" Just p -> let (d, m) = p `divMod` 10 in if m == 0 then makePhrase [MU.CarWs d "move"] else makePhrase [MU.Car1Ws p "tenth", "of a move"] in "of speed surge for" <+> moves Teleport dice | Dice.supDice dice <= 9 -> "of blinking" <+> wrapInParens (tshow dice) Teleport dice -> "of teleport" <+> wrapInParens (tshow dice) CreateItem _ COrgan grp tim -> let stime = if isTimerNone tim then "" else "for" <+> tshow tim <> ":" in "(keep" <+> stime <+> displayGroupName grp <> ")" CreateItem _ _ grp _ -> makePhrase ["of gain", MU.AW $ MU.Text $ displayGroupName grp] DestroyItem{} -> "of loss" ConsumeItems{} -> "of consumption from the ground" -- too much noise from crafting DropItem n k store grp -> let (preT, postT) = if | n == 1 && k == maxBound -> ("one", "kind") | n == maxBound && k == maxBound -> ("all", "kinds") | k == 1 || store /= COrgan -> ("", "") | k == maxBound -> ("", "condition fully") | otherwise -> ("", "condition" <+> tshow k <> "-fold") (verb, fromStore) = if store == COrgan then ("nullify", "") else ("drop", "from" <+> snd (ppCStore store)) in "of" <+> verb <+> preT <+> displayGroupName grp <+> postT <+> fromStore Recharge n dice -> let times = if n == 1 then "" else tshow n <+> "times" in case Dice.reduceDice dice of Nothing -> "of recharge" <+> times <+> "by" <+> tshow dice <+> "* 0.05s" Just p -> let dt = timeDeltaScale (Delta timeClip) p in "of recharge" <+> times <+> "by" <+> timeDeltaInSecondsText dt Discharge n dice -> let times = if n == 1 then "" else tshow n <+> "times" in case Dice.reduceDice dice of Nothing -> "of discharge" <+> times <+> "by" <+> tshow dice <+> "* 0.05s" Just p -> let dt = timeDeltaScale (Delta timeClip) p in "of discharge" <+> times <+> "by" <+> timeDeltaInSecondsText dt PolyItem -> "of repurpose on the ground" RerollItem -> "of deeply reshape on the ground" DupItem -> "of multiplication on the ground" Identify -> "of identify" Detect d radius -> "of" <+> detectToObject d <+> "location" <+> wrapInParens (tshow radius) SendFlying tmod -> "of impact" <+> tmodToSuff "" tmod PushActor tmod -> "of pushing" <+> tmodToSuff "" tmod PullActor tmod -> "of pulling" <+> tmodToSuff "" tmod AtMostOneOf effs -> let ts = filter (/= "") $ map (effectToSuffix detailLevel) effs subject = "marvel" header = makePhrase ["of", MU.CardinalWs (length ts) subject] sometimes = if length effs > length ts then "(sometimes)" else "" in case ts of [] -> "" [wonder] -> wonder <+> sometimes _ | detailLevel < DetailAll -> header _ -> header <+> "[" <> T.intercalate ", " ts <> "]" <+> sometimes OneOf effs -> let ts = filter (/= "") $ map (effectToSuffix detailLevel) effs subject = "wonder" header = makePhrase ["of", MU.CardinalWs (length ts) subject] sometimes = if length effs > length ts then "(sometimes)" else "" in case ts of [] -> "" [wonder] -> wonder <+> sometimes _ | detailLevel < DetailAll -> header _ -> header <+> "[" <> T.intercalate ", " ts <> "]" <+> sometimes OnSmash _ -> "" -- printed inside a separate section OnCombine _ -> "" -- printed inside a separate section OnUser eff -> let t = effectToSuffix detailLevel eff in if T.null t then "" else "(on user:" <+> t <> ")" NopEffect -> "" -- never printed AndEffect (ConsumeItems tools raw) eff -> case detailLevel of DetailAll -> 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) $ map (effectToSuffix detailLevel) [eff1, eff2] in if T.null t then "of conjunctive processing" else t OrEffect eff1 eff2 -> let t = T.intercalate " or else " $ nub $ filter (not . T.null) $ map (effectToSuffix detailLevel) [eff1, eff2] in if T.null t then "of alternative processing" else t SeqEffect effs -> let t = T.intercalate " then " $ nub $ filter (not . T.null) $ map (effectToSuffix detailLevel) effs in if T.null t then "of sequential processing" else t When cond eff -> let object = conditionToObject cond object2 = effectToSuffix detailLevel eff in if T.null object2 then "" -- no 'conditional processing' --- probably a hack else "(when" <+> object <+> "then" <+> object2 <> ")" Unless cond eff -> let object = conditionToObject cond object2 = effectToSuffix detailLevel eff in if T.null object2 then "" else "(unless" <+> object <+> "then" <+> object2 <> ")" IfThenElse cond eff1 eff2 -> let object = conditionToObject cond object1 = effectToSuffix detailLevel eff1 object2 = effectToSuffix detailLevel eff2 in if T.null object1 && T.null object2 then "" else "(if" <+> object <+> "then" <+> object1 <+> "else" <+> object2 <> ")" VerbNoLonger{} -> "" -- no description for a flavour effect VerbMsg{} -> "" -- no description for an effect that prints a description VerbMsgFail{} -> "" conditionToObject :: Condition -> Text conditionToObject = \case HpLeq n -> "HP <=" <+> tshow n HpGeq n -> "HP >=" <+> tshow n CalmLeq n -> "Calm <=" <+> tshow n CalmGeq n -> "Calm >=" <+> tshow n TriggeredBy activationFlag -> "activated" <+> activationFlagToObject activationFlag activationFlagToObject :: ActivationFlag -> Text activationFlagToObject = \case ActivationMeleeable -> "by meleeing" ActivationPeriodic -> "periodically" ActivationUnderRanged -> "under ranged attack" ActivationUnderMelee -> "under melee attack" ActivationProjectile -> "when flung" ActivationTrigger -> "by triggering" ActivationOnSmash -> "on smash" ActivationOnCombine -> "when combined" ActivationEmbed -> "embedded in terrain" ActivationConsume -> "when consumed" detectToObject :: DetectKind -> Text detectToObject d = case d of DetectAll -> "detail" DetectActor -> "intruder" DetectLoot -> "merchandise" DetectExit -> "exit" DetectHidden -> "secret" DetectEmbed -> "feature" DetectStash -> "stash" detectToVerb :: DetectKind -> Text detectToVerb d = case d of DetectAll -> "map all" DetectActor -> "spot nearby" DetectLoot -> "locate nearby" DetectExit -> "learn nearby" DetectHidden -> "uncover nearby" DetectEmbed -> "notice nearby" DetectStash -> "locate" slotToSentence :: EqpSlot -> Text slotToSentence es = case es of EqpSlotMove -> "Those unskilled in locomotion equip it." EqpSlotMelee -> "Those unskilled in close combat equip it." EqpSlotDisplace -> "Those unskilled in moving in crowds equip it." EqpSlotAlter -> "Those unskilled in terrain modification equip it." EqpSlotWait -> "Those unskilled in watchfulness equip it." EqpSlotMoveItem -> "Those unskilled in inventory management equip it." EqpSlotProject -> "Those unskilled in item flinging equip it." EqpSlotApply -> "Those unskilled in applying items equip it." EqpSlotSwimming -> "Useful to any that wade or swim in water." EqpSlotFlying -> "Those not afraid to fly, put it on." EqpSlotHurtMelee -> "Veteran melee fighters are known to devote equipment slot to it." EqpSlotArmorMelee -> "Worn by people in risk of melee wounds." EqpSlotArmorRanged -> "People scared of shots in the dark wear it." EqpSlotMaxHP -> "The frail wear it to increase their Hit Point capacity." EqpSlotSpeed -> "The sluggish equip it to speed up their whole life." EqpSlotSight -> "The short-sighted wear it to notice their demise sooner." EqpSlotShine -> "Explorers brave enough to highlight themselves put it in their equipment." EqpSlotMiscBonus -> "Those that don't scorn minor bonuses may equip it." EqpSlotWeaponFast -> "Close range fighters pick it as their mainstay weapon." EqpSlotWeaponBig -> "Close range fighters pick it as their opening weapon." skillName :: Skill -> Text skillName SkMove = "move stat" skillName SkMelee = "melee stat" skillName SkDisplace = "displace stat" skillName SkAlter = "modify terrain stat" skillName SkWait = "wait stat" skillName SkMoveItem = "manage items stat" skillName SkProject = "fling stat" skillName SkApply = "trigger stat" skillName SkSwimming = "swimming" skillName SkFlying = "flying" skillName SkHurtMelee = "to melee damage" skillName SkArmorMelee = "melee armor" skillName SkArmorRanged = "ranged armor" skillName SkMaxHP = "max HP" skillName SkMaxCalm = "max Calm" skillName SkSpeed = "speed" skillName SkSight = "sight radius" skillName SkSmell = "smell radius" skillName SkShine = "shine radius" skillName SkNocto = "noctovision radius" skillName SkHearing = "hearing radius" skillName SkAggression = "aggression level" skillName SkOdor = "odor level" skillName SkDeflectRanged = "ranged deflection" skillName SkDeflectMelee = "melee deflection" skillDesc :: Skill -> Text skillDesc skill = let skName = skillName skill capSkillName = "The '" <> skName <> "' skill" capStatName = "The '" <> T.unwords (init $ T.words skName) <> "' stat" in case skill of SkMove -> capStatName <+> "determines whether the character can move. Actors not capable of movement can't be dominated." SkMelee -> capStatName <+> "determines whether the character can melee. Actors that can't melee can still cause damage by flinging missiles or by ramming (being pushed) at opponents." SkDisplace -> capStatName <+> "determines whether the character can displace adjacent actors. In some cases displacing is not possible regardless of skill: when the target is braced, dying, has no move skill or when both actors are supported by adjacent friendly units. Missiles can be displaced always, unless more than one occupies the map location." SkAlter -> capStatName <+> "determines which kinds of terrain can be activated and modified by the character. Opening doors and searching suspect tiles require skill 2, some stairs require 3, closing doors requires 4, others require 4 or 5. Actors not smart enough to be capable of using stairs can't be dominated." SkWait -> capStatName <+> "determines whether the character can wait, brace for combat (potentially blocking the effects of some attacks), sleep and lurk." SkMoveItem -> capStatName <+> "determines whether the character can pick up items and manage inventory." SkProject -> capStatName <+> "determines which kinds of items the character can propel. Items that can be lobbed to explode at a precise location, such as flasks, require skill 3. Other items travel until they meet an obstacle and skill 1 is enough to fling them. In some cases, e.g., of too intricate or two awkward items at low Calm, throwing is not possible regardless of the skill value." SkApply -> capStatName <+> "determines which kinds of items the character can use. Items that assume literacy require skill 2, others can be used already at skill 1. In some cases, e.g., when the item needs recharging, has no possible effects or is too intricate for distracted use, triggering may not be possible." SkSwimming -> capSkillName <+> "is the degree of avoidance of bad effects of terrain containing water, whether shallow or deep." SkFlying -> capSkillName <+> "is the degree of avoidance of bad effects of any hazards spread on the ground." SkHurtMelee -> capSkillName <+> "is a percentage of additional damage dealt by the actor (either a character or a missile) with any weapon. The value is capped at 200% and then the armor percentage of the defender is subtracted from it." SkArmorMelee -> capSkillName <+> "is a percentage of melee damage avoided by the actor. The value is capped at 200%, then the extra melee damage percentage of the attacker is subtracted from it and the resulting total is capped at 95% (always at least 5% of damage gets through). It includes 50% bonus from being braced for combat, if applicable." SkArmorRanged -> capSkillName <+> "is a percentage of ranged damage avoided by the actor. The value is capped at 200%, then the extra melee damage percentage of the attacker is subtracted from it and the resulting total is capped at 95% (always at least 5% of damage gets through). It includes 25% bonus from being braced for combat, if applicable." SkMaxHP -> capSkillName <+> "is a cap on HP of the actor, except for some rare effects able to overfill HP. At any direct enemy damage (but not, e.g., incremental poisoning damage or wounds inflicted by mishandling a device) HP is cut back to the cap." SkMaxCalm -> capSkillName <+> "is a cap on Calm of the actor, except for some rare effects able to overfill Calm. At any direct enemy damage (but not, e.g., incremental poisoning damage or wounds inflicted by mishandling a device) Calm is lowered, sometimes very significantly and always at least back down to the cap." SkSpeed -> capSkillName <+> "is expressed in meters per second, which corresponds to map location (1m by 1m) per two standard turns (0.5s each). Thus actor at standard speed of 2m/s moves one location per standard turn." SkSight -> capSkillName <+> "is the limit of visibility in light. The radius is measured from the middle of the map location occupied by the character to the edge of the furthest covered location." SkSmell -> capSkillName <+> "determines the maximal area smelled by the actor. The radius is measured from the middle of the map location occupied by the character to the edge of the furthest covered location." SkShine -> capSkillName <+> "determines the maximal area lit by the actor. The radius is measured from the middle of the map location occupied by the character to the edge of the furthest covered location." SkNocto -> capSkillName <+> "is the limit of visibility in dark. The radius is measured from the middle of the map location occupied by the character to the edge of the furthest covered location." SkHearing -> capSkillName <+> "is the limit of hearing. The radius is measured from the middle of the map location occupied by the character to the edge of the furthest covered location." SkAggression -> "The '" <> skName <> "' property" <+> "represents the willingness of the actor to engage in combat, especially close quarters, and conversely, to break engagement when overpowered." SkOdor -> "The '" <> skName <> "' property" <+> "represents the ability to communicate (more specifically, communicate one's presence) through personal odor. Zero or less means the odor is not trackable." SkDeflectRanged -> "The '" <> skName <> "' property" <+> "tells whether complete invulnerability to ranged attacks, piercing and of every other kind, is effective, and from how many sources." SkDeflectMelee -> "The '" <> skName <> "' property" <+> "tells whether complete invulnerability to melee attacks, piercing and of every other kind, is effective, and from how many sources." skillToDecorator :: Skill -> Actor -> Int -> Text skillToDecorator skill b t = let tshow200 n = let n200 = min 200 $ max (-200) n in tshow n200 <> if n200 /= n then "$" else "" -- Some values can be negative, for others 0 is equivalent but shorter. tshowRadius r = case compare r 0 of GT -> tshow (r - 1) <> ".5m" EQ -> "0m" LT -> tshow (r + 1) <> ".5m" in case skill of SkMove -> tshow t SkMelee -> tshow t SkDisplace -> tshow t SkAlter -> tshow t SkWait -> tshow t SkMoveItem -> tshow t SkProject -> tshow t SkApply -> tshow t SkSwimming -> tshow t SkFlying -> tshow t SkHurtMelee -> tshow200 t <> "%" SkArmorMelee -> "[" <> tshow200 t <> "%]" SkArmorRanged -> "{" <> tshow200 t <> "%}" SkMaxHP -> tshow $ max 0 t SkMaxCalm -> tshow $ max 0 t SkSpeed -> T.pack $ displaySpeed t SkSight -> let tcapped = min (fromEnum $ bcalm b `div` xM 5) t in tshowRadius tcapped <+> if tcapped == t then "" else "(max" <+> tshowRadius t <> ")" SkSmell -> tshowRadius t SkShine -> tshowRadius t SkNocto -> tshowRadius t SkHearing -> tshowRadius t SkAggression -> tshow t SkOdor -> tshow t SkDeflectRanged -> tshow t SkDeflectMelee -> tshow t skillsInDisplayOrder :: [Skill] skillsInDisplayOrder = [minBound .. maxBound] tmodToSuff :: Text -> ThrowMod -> Text tmodToSuff verb ThrowMod{..} = let vSuff | throwVelocity == 100 = "" | otherwise = "v=" <> tshow throwVelocity <> "%" tSuff | throwLinger == 100 = "" | otherwise = "t=" <> tshow throwLinger <> "%" hSuff | throwHP == 1 = "" | otherwise = "pierce=" <> tshow throwHP in if vSuff == "" && tSuff == "" && hSuff == "" then "" else verb <+> "with" <+> vSuff <+> tSuff <+> hSuff kindAspectToSuffix :: Aspect -> Text kindAspectToSuffix aspect = case aspect of Timeout{} -> "" -- printed specially AddSkill SkMove t -> wrapInParens $ affixDice t <+> "move" AddSkill SkMelee t -> wrapInParens $ affixDice t <+> "melee" AddSkill SkDisplace t -> wrapInParens $ affixDice t <+> "displace" AddSkill SkAlter t -> wrapInParens $ affixDice t <+> "modify" AddSkill SkWait t -> wrapInParens $ affixDice t <+> "wait" AddSkill SkMoveItem t -> wrapInParens $ affixDice t <+> "manage items" AddSkill SkProject t -> wrapInParens $ affixDice t <+> "fling" AddSkill SkApply t -> wrapInParens $ affixDice t <+> "trigger" AddSkill SkSwimming t -> wrapInParens $ affixDice t <+> "swimming" AddSkill SkFlying t -> wrapInParens $ affixDice t <+> "flying" AddSkill SkHurtMelee _ -> "" -- printed together with dice, even if dice is zero AddSkill SkArmorMelee t -> "[" <> affixDice t <> "%]" AddSkill SkArmorRanged t -> "{" <> affixDice t <> "%}" AddSkill SkMaxHP t -> wrapInParens $ affixDice t <+> "HP" AddSkill SkMaxCalm t -> wrapInParens $ affixDice t <+> "Calm" AddSkill SkSpeed t -> wrapInParens $ affixDice t <+> "speed" AddSkill SkSight t -> wrapInParens $ affixDice t <+> "sight" AddSkill SkSmell t -> wrapInParens $ affixDice t <+> "smell" AddSkill SkShine t -> wrapInParens $ affixDice t <+> "shine" AddSkill SkNocto t -> wrapInParens $ affixDice t <+> "night vision" AddSkill SkHearing t -> wrapInParens $ affixDice t <+> "hearing" AddSkill SkAggression t -> wrapInParens $ affixDice t <+> "aggression" AddSkill SkOdor t -> wrapInParens $ affixDice t <+> "odor" AddSkill SkDeflectRanged d -> if | Dice.infDice d >= 1 -> wrapInChevrons "deflecting ranged attacks" | Dice.supDice d <= -1 -> wrapInChevrons "vulnerable to ranged attacks" | otherwise -> "" -- bad content? AddSkill SkDeflectMelee d -> if | Dice.infDice d >= 1 -> wrapInChevrons "deflecting melee attacks" | Dice.supDice d <= -1 -> wrapInChevrons "vulnerable to melee attacks" | otherwise -> "" -- bad content? SetFlag Fragile -> wrapInChevrons "fragile" SetFlag Lobable -> wrapInChevrons "can be lobbed" SetFlag Durable -> wrapInChevrons "durable" SetFlag Equipable -> "" SetFlag Benign -> "" SetFlag Precious -> "" SetFlag Blast -> "" SetFlag Condition -> "" SetFlag Unique -> "" -- named specially by the content writer SetFlag MetaGame -> "" SetFlag MinorEffects -> "" -- cryptic override SetFlag MinorAspects -> "" -- cryptic override SetFlag Meleeable -> "" SetFlag Periodic -> "" -- printed specially SetFlag UnderRanged -> wrapInChevrons "applied under ranged attack" SetFlag UnderMelee -> wrapInChevrons "applied under melee attack" ELabel{} -> "" -- too late ToThrow tmod -> wrapInChevrons $ tmodToSuff "flies" tmod PresentAs{} -> "" EqpSlot{} -> "" -- used in @slotToSentence@ instead Odds{} -> "" aspectToSentence :: Aspect -> Maybe Text aspectToSentence feat = case feat of Timeout{} -> Nothing AddSkill{} -> Nothing SetFlag Fragile -> Nothing SetFlag Lobable -> Nothing SetFlag Durable -> Nothing SetFlag Equipable -> Nothing SetFlag Benign -> Just "It affects the opponent in a benign way." SetFlag Precious -> Just "It seems precious." SetFlag Blast -> Nothing SetFlag Condition -> Nothing SetFlag Unique -> Just "It is one of a kind." SetFlag MetaGame -> Just "It's so characteristic that it's recognizable every time after being identified once, even under very different circumstances." SetFlag MinorEffects -> Nothing SetFlag MinorAspects -> Nothing SetFlag Meleeable -> Just "It is considered for melee strikes." SetFlag Periodic -> Nothing SetFlag UnderRanged -> Nothing SetFlag UnderMelee -> Nothing ELabel{} -> Nothing ToThrow{} -> Nothing PresentAs{} -> Nothing EqpSlot es -> Just $ slotToSentence es Odds{} -> Just "Individual specimens sometimes have yet other properties." affixBonus :: Int -> Text affixBonus p = case compare p 0 of EQ -> "0" LT -> tshow p GT -> "+" <> tshow p wrapInParens :: Text -> Text wrapInParens "" = "" wrapInParens t = "(" <> t <> ")" wrapInChevrons :: Text -> Text wrapInChevrons "" = "" wrapInChevrons t = "<" <> t <> ">" affixDice :: Dice.Dice -> Text affixDice d = maybe "+?" affixBonus $ Dice.reduceDice d describeTools :: [(Int, GroupName ItemKind)] -> MU.Part describeTools = let carAWs (k, grp) = MU.CarAWs k (MU.Text $ displayGroupName grp) in MU.WWandW . map carAWs describeToolsAlternative :: [[(Int, GroupName ItemKind)]] -> Text describeToolsAlternative grps = T.intercalate " or " $ map (\grp -> makePhrase [describeTools grp]) $ filter (not . null) grps describeCrafting :: [(Int, GroupName ItemKind)] -> [(Int, GroupName ItemKind)] -> Effect -> (Text, Text, Text) describeCrafting tools raw eff = let unCreate (CreateItem (Just k) _ grp _) = [(k, grp)] unCreate (SeqEffect effs) = concatMap unCreate effs unCreate _ = [] grpsCreate = unCreate eff tcraft = makePhrase $ "of crafting" : (if null grpsCreate then ["nothing"] else [describeTools grpsCreate]) traw = makePhrase $ if null raw then [] else ["from", describeTools raw] ttools = makePhrase $ if null tools then [] else ["using", describeTools tools] in (tcraft, traw, ttools) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Frame.hs0000644000000000000000000002310607346545000022064 0ustar0000000000000000{-# LANGUAGE RankNTypes #-} -- | Screen frames. -- -- Note that @PointArray.Array@ here represents a screen frame and so -- screen positions are denoted by @Point@, contrary to the convention -- that @Point@ refers to game map coordinates, as outlined -- in description of 'PointSquare' that should normally be used in that role. module Game.LambdaHack.Client.UI.Frame ( ColorMode(..) , FrameST, FrameForall(..), FrameBase(..), Frame , PreFrame3, PreFrames3, PreFrame, PreFrames , SingleFrame(..), OverlaySpace , blankSingleFrame, truncateOverlay, overlayFrame #ifdef EXPOSE_INTERNAL -- * Internal operations , truncateAttrLine #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Monad.ST.Strict import Data.Function import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed as U import qualified Data.Vector.Unboxed.Mutable as VM import Data.Word import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import qualified Game.LambdaHack.Common.PointArray as PointArray import qualified Game.LambdaHack.Definition.Color as Color -- | Color mode for the display. data ColorMode = ColorFull -- ^ normal, with full colours | ColorBW -- ^ black and white only deriving Eq type FrameST s = G.Mutable U.Vector s Word32 -> ST s () -- | Efficiently composable representation of an operation -- on a frame, that is, on a mutable vector. When the composite operation -- is eventually performed, the vector is frozen to become a 'SingleFrame'. newtype FrameForall = FrameForall {unFrameForall :: forall s. FrameST s} -- | Action that results in a base frame, to be modified further. newtype FrameBase = FrameBase {unFrameBase :: forall s. ST s (G.Mutable U.Vector s Word32)} -- | A frame, that is, a base frame and all its modifications. type Frame = ( (FrameBase, FrameForall) , (OverlaySpace, OverlaySpace, OverlaySpace) ) -- | Components of a frame, before it's decided if the first can be overwritten -- in-place or needs to be copied. type PreFrame3 = (PreFrame, (OverlaySpace, OverlaySpace, OverlaySpace)) -- | Sequence of screen frames, including delays. Potentially based on a single -- base frame. type PreFrames3 = [Maybe PreFrame3] -- | A simpler variant of @PreFrame3@. type PreFrame = (U.Vector Word32, FrameForall) -- | A simpler variant of @PreFrames3@. type PreFrames = [Maybe PreFrame] -- | Representation of an operation of overwriting a frame with a single line -- at the given row. writeLine :: Int -> AttrString -> FrameForall {-# INLINE writeLine #-} writeLine offset al = FrameForall $ \v -> do let writeAt _ [] = return () writeAt off (ac32 : rest) = do VM.write v off (Color.attrCharW32 ac32) writeAt (off + 1) rest writeAt offset al -- | A frame that is padded to fill the whole screen with optional -- overlays to display in proportional, square and monospace fonts. -- -- Note that we don't provide a list of color-highlighed box positions -- to be drawn separately, because overlays need to obscure not only map, -- but the highlights as well, so highlights need to be included earlier. -- -- See the description of 'PointSquare' for explanation of why screen -- coordinates in @singleArray@ are @Point@ even though they should be -- 'PointSquare'. data SingleFrame = SingleFrame { singleArray :: PointArray.Array Color.AttrCharW32 , singlePropOverlay :: OverlaySpace , singleSquareOverlay :: OverlaySpace , singleMonoOverlay :: OverlaySpace } deriving (Show, Eq) type OverlaySpace = [(PointUI, AttrString)] blankSingleFrame :: ScreenContent -> SingleFrame blankSingleFrame ScreenContent{rwidth, rheight} = SingleFrame (PointArray.replicateA rwidth rheight Color.spaceAttrW32) [] [] [] -- | Truncate the overlay: for each line, if it's too long, it's truncated -- and if there are too many lines, excess is dropped and warning is appended. -- The width, in the second argument, is calculated in characters, -- not in UI (mono font) coordinates, so that taking and dropping characters -- is performed correctly. truncateOverlay :: Bool -> Int -> Int -> Bool -> Int -> Bool -> Overlay -> OverlaySpace truncateOverlay halveXstart width rheight wipeAdjacentRaw fillLen onBlank ov = let wipeAdjacent = wipeAdjacentRaw && not onBlank canvasLength = if onBlank then rheight else rheight - 2 supHeight = maxYofOverlay ov trimmedY = canvasLength - 1 -- Sadly, this does not trim the other, concurrent, overlays that may -- obscure the last line and so contend with the "trimmed" message. -- Tough luck; just avoid overrunning overlays in higher level code. ovTopFiltered = filter (\(PointUI _ y, _) -> y < trimmedY) ov trimmedAlert = ( PointUI 0 trimmedY , stringToAL "--a portion of the text trimmed--" ) extraLine | supHeight < 3 || supHeight >= trimmedY || not wipeAdjacent = [] | otherwise = let supHs = filter (\(PointUI _ y, _) -> y == supHeight) ov in if null supHs then [] else let (PointUI xLast yLast, _) = minimumBy (comparing $ \(PointUI x _, _) -> x) supHs in [(PointUI xLast (yLast + 1), emptyAttrLine)] -- This is crude, because an al at lower x may be longer, but KISS. -- This also gives a solid rule which al overwrite others -- when merging overlays, independent of the order of merging -- (except for duplicate x, for which initial order is retained). -- The order functions is cheap, we use @sortBy@, not @sortOn@. ovTop = groupBy ((==) `on` \(PointUI _ y, _) -> y) $ sortBy (comparing $ \(PointUI x y, _) -> (y, x)) $ if supHeight >= canvasLength then ovTopFiltered ++ [trimmedAlert] else ov ++ extraLine -- Unlike the trimming above, adding spaces around overlay depends -- on there being no gaps and a natural order. -- Probably also gives messy results when X offsets are not all the same. -- Below we at least mitigate the case of multiple lines per row. f _ _ [] = error "empty list of overlay lines at the given row" f (yPrev, lenPrev) (yNext, lenNext) (minAl@(PointUI _ yCur, _) : rest) = g (if yPrev == yCur - 1 then lenPrev else 0) (if yNext == yCur + 1 then lenNext else 0) fillLen minAl : map (g 0 0 0) rest g lenPrev lenNext fillL (p@(PointUI xstartRaw _), layerLine) = let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw -- TODO: lenPrev and lenNext is from the same kind of font; -- if fonts are mixed, too few spaces are added. -- We'd need to keep a global store of line lengths -- for every position on the screen, filled first going -- over all texts and only afterwards texts rendered. -- And prop font measure would still make this imprecise. -- TODO: rewrite ovBackdrop according to this idea, -- but then process square font only mode with the same mechanism. maxLen = if wipeAdjacent then max lenPrev lenNext else 0 fillFromStart = max fillL (1 + maxLen) - xstart available = width - xstart in (p, truncateAttrLine wipeAdjacent available fillFromStart layerLine) rightExtentOfLine (PointUI xstartRaw _, al) = let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw in min (width - 1) (xstart + length (attrLine al)) yAndLen [] = (-99, 0) yAndLen als@((PointUI _ y, _) : _) = (y, maximum $ map rightExtentOfLine als) lens = map yAndLen ovTop f2 = map g2 g2 (p@(PointUI xstartRaw _), layerLine) = let xstart = if halveXstart then xstartRaw `div` 2 else xstartRaw available = width - xstart in (p, truncateAttrLine False available 0 layerLine) in concat $ if onBlank then map f2 ovTop else zipWith3 f ((-9, 0) : lens) (drop 1 lens ++ [(999, 0)]) ovTop -- | Add a space at the message end, for display overlayed over the level map. -- Also trim (do not wrap!) too long lines. Also add many spaces when under -- longer lines. truncateAttrLine :: Bool -> Int -> Int -> AttrLine -> AttrString truncateAttrLine addSpaces available fillFromStart aLine = let al = attrLine aLine len = length al in if | null al -> if addSpaces then replicate fillFromStart Color.spaceAttrW32 else al | len == available - 1 && addSpaces -> al ++ [Color.spaceAttrW32] | otherwise -> case compare available len of LT -> take (available - 1) al ++ [Color.trimmedLineAttrW32] GT | addSpaces -> let alSpace = al ++ [Color.spaceAttrW32, Color.spaceAttrW32] whiteN = fillFromStart - len - 2 in if whiteN <= 0 then alSpace -- speedup (supposedly) for menus else alSpace ++ replicate whiteN Color.spaceAttrW32 _ -> al -- | Overlays either the game map only or the whole empty screen frame. -- We assume the lines of the overlay are not too long nor too many. overlayFrame :: Int -> OverlaySpace -> PreFrame -> PreFrame overlayFrame width ov (m, ff) = ( m , FrameForall $ \v -> do unFrameForall ff v mapM_ (\(PointUI px py, l) -> let offset = py * width + px `div` 2 in unFrameForall (writeLine offset l) v) ov ) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/FrameM.hs0000644000000000000000000003037507346545000022207 0ustar0000000000000000-- | A set of Frame monad operations. module Game.LambdaHack.Client.UI.FrameM ( drawOverlay, promptGetKey, addToMacro, dropEmptyMacroFrames , lastMacroFrame, stopPlayBack, renderAnimFrames, animate #ifdef EXPOSE_INTERNAL -- * Internal operations , resetPlayBack, restoreLeaderFromRun, basicFrameForAnimation #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Bifunctor as B import qualified Data.EnumMap.Strict as EM import qualified Data.Map.Strict as M import qualified Data.Vector.Unboxed as U import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.Animation import Game.LambdaHack.Client.UI.Content.Input import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.DrawM import Game.LambdaHack.Client.UI.Frame import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Definition.Color as Color -- | Draw the current level with the overlay on top. drawOverlay :: MonadClientUI m => ColorMode -> Bool -> FontOverlayMap -> LevelId -> m PreFrame3 drawOverlay dm onBlank ovs lid = do CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui basicFrame <- if onBlank then do let m = U.replicate (rwidth * rheight) (Color.attrCharW32 Color.spaceAttrW32) return (m, FrameForall $ \_v -> return ()) else drawHudFrame dm lid FontSetup{..} <- getFontSetup let propWidth = if isMonoFont propFont then 2 * rwidth else 4 * rwidth ovProp | not (isSquareFont propFont) = truncateOverlay False propWidth rheight False 0 onBlank $ EM.findWithDefault [] propFont ovs | otherwise = [] ovMono = if not (isSquareFont monoFont) then truncateOverlay False (2 * rwidth) rheight False 0 onBlank $ EM.findWithDefault [] monoFont ovs else [] ovSquare | not (isSquareFont propFont) = truncateOverlay False (2 * rwidth) rheight False 0 onBlank $ EM.findWithDefault [] squareFont ovs | otherwise = [] ovOther | not (isSquareFont propFont) = [] | otherwise = truncateOverlay True rwidth rheight True 20 onBlank $ concat $ EM.elems ovs -- 20 needed not to leave gaps in skill menu -- in the absence of backdrop ovBackdrop = if not (isSquareFont propFont) && not onBlank then let propOutline = truncateOverlay False propWidth rheight True 0 onBlank $ EM.findWithDefault [] propFont ovs monoOutline = truncateOverlay False (2 * rwidth) rheight True 0 onBlank $ EM.findWithDefault [] monoFont ovs squareOutline = truncateOverlay False (2 * rwidth) rheight True 0 onBlank $ EM.findWithDefault [] squareFont ovs g x al Nothing = Just (x, x + length al - 1) g x al (Just (xmin, xmax)) = Just (min xmin x, max xmax (x + length al - 1)) f em (PointUI x y, al) = EM.alter (g x al) y em extentMap = foldl' f EM.empty $ propOutline ++ monoOutline ++ squareOutline listBackdrop (y, (xmin, xmax)) = ( PointUI (2 * (xmin `div` 2)) y , blankAttrString $ min (rwidth - 2 * (xmin `div` 2)) (1 + xmax `divUp` 2 - xmin `div` 2) ) in map listBackdrop $ EM.assocs extentMap else [] overlayedFrame = overlayFrame rwidth ovOther $ overlayFrame rwidth ovBackdrop basicFrame return (overlayedFrame, (ovProp, ovSquare, ovMono)) promptGetKey :: MonadClientUI m => ColorMode -> FontOverlayMap -> Bool -> [K.KM] -> m K.KM promptGetKey dm ovs onBlank frontKeyKeys = do lidV <- viewedLevelUI report <- getsSession $ newReport . shistory sreqQueried <- getsSession sreqQueried macroFrame <- getsSession smacroFrame let interrupted = -- If server is not querying for request, then the key is needed due to -- a special event, not ordinary querying the player for command, -- so interrupt. not sreqQueried -- Any alarming message interupts macros, except when the macro -- displays help and ends, which is a helpful thing to do. || (anyInReport disturbsResting report && keyPending macroFrame /= KeyMacro [K.mkKM "F1"]) km <- case keyPending macroFrame of KeyMacro (km : kms) | not interrupted -- A faulty key in a macro is a good reason -- to interrupt it, as well. && (null frontKeyKeys || km `elem` frontKeyKeys) -> do -- No need to display the frame, because a frame was displayed -- when the player chose to play a macro and each turn or more often -- a frame is displayed elsewhere. -- The only excepton is when navigating menus through macros, -- but there the speed is particularly welcome. modifySession $ \sess -> sess {smacroFrame = (smacroFrame sess) {keyPending = KeyMacro kms}} msgAdd MsgMacroOperation $ "Voicing '" <> tshow km <> "'." return km KeyMacro kms -> do if null kms then do -- There was no macro. Not important if there was a reason -- for interrupt or not. when (dm /= ColorFull) $ do -- This marks a special event, regardless of @sreqQueried@. side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD unless (gunderAI fact) -- don't forget special autoplay keypresses -- Forget the furious keypresses just before a special event. resetPressedKeys -- Running, if any, must have ended naturally, because no macro. -- Therefore no need to restore leader back to initial run leader, -- but running itself is cancelled below. else do -- The macro was not empty, but not played, so it must have been -- interrupted, so we can't continue playback, so wipe out the macro. resetPlayBack -- This might have been an unexpected end of a run, too. restoreLeaderFromRun -- Macro was killed, so emergency, so reset input, too. resetPressedKeys frontKeyFrame <- drawOverlay dm onBlank ovs lidV recordHistory modifySession $ \sess -> sess { srunning = Nothing , sxhairGoTo = Nothing , sdisplayNeeded = False , sturnDisplayed = True } connFrontendFrontKey frontKeyKeys frontKeyFrame -- In-game macros need to be recorded here, not in @UI.humanCommand@, -- to also capture choice of items from menus, etc. -- Notice that keys coming from macros (from content, in-game, config) -- are recorded as well and this is well defined and essential. -- -- Only keys pressed when player is queried for a command are recorded. when sreqQueried $ do CCUI{coinput=InputContent{bcmdMap}} <- getsSession sccui modifySession $ \sess -> sess {smacroFrame = addToMacro bcmdMap km $ smacroFrame sess} return km addToMacro :: M.Map K.KM HumanCmd.CmdTriple -> K.KM -> KeyMacroFrame -> KeyMacroFrame addToMacro bcmdMap km macroFrame = case (\(_, _, cmd) -> cmd) <$> M.lookup km bcmdMap of Nothing -> macroFrame Just HumanCmd.Record -> macroFrame Just HumanCmd.RepeatLast{} -> macroFrame _ -> macroFrame { keyMacroBuffer = (km :) `B.first` keyMacroBuffer macroFrame } -- This is noop when not recording a macro, -- which is exactly the required semantics. dropEmptyMacroFrames :: KeyMacroFrame -> [KeyMacroFrame] -> (KeyMacroFrame, [KeyMacroFrame]) dropEmptyMacroFrames mf [] = (mf, []) dropEmptyMacroFrames (KeyMacroFrame _ (KeyMacro []) _) (mf : mfs) = dropEmptyMacroFrames mf mfs dropEmptyMacroFrames mf mfs = (mf, mfs) lastMacroFrame :: KeyMacroFrame -> [KeyMacroFrame] -> KeyMacroFrame lastMacroFrame mf [] = mf lastMacroFrame _ (mf : mfs) = lastMacroFrame mf mfs stopPlayBack :: MonadClientUI m => m () stopPlayBack = msgAdd MsgStopPlayback "!" -- | We wipe any actions in progress, but keep the data needed to repeat -- the last global macros and the last command. resetPlayBack :: MonadClientUI m => m () resetPlayBack = modifySession $ \sess -> let lastFrame = lastMacroFrame (smacroFrame sess) (smacroStack sess) in sess { smacroFrame = lastFrame {keyPending = mempty} , smacroStack = [] } restoreLeaderFromRun :: MonadClientUI m => m () restoreLeaderFromRun = do srunning <- getsSession srunning case srunning of Nothing -> return () Just RunParams{runLeader} -> do -- Switch to the original leader, from before the run start, -- unless dead or unless the faction never runs with multiple -- (but could have the leader changed automatically meanwhile). side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD arena <- getArenaUI memA <- getsState $ memActor runLeader arena when (memA && not (noRunWithMulti fact)) $ updateClientLeader runLeader -- This is not our turn, so we can't obstruct screen with messages -- and message reformatting causes distraction, so there's no point -- trying to squeeze the report into the single available line, -- except when it's not our turn permanently, because AI runs UI. basicFrameForAnimation :: MonadClientUI m => LevelId -> Maybe Bool -> m PreFrame3 basicFrameForAnimation arena forceReport = do FontSetup{propFont} <- getFontSetup sbenchMessages <- getsClient $ sbenchMessages . soptions side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD report <- getReportUI False let par1 = firstParagraph $ foldr (<+:>) [] $ renderReport True report -- If messages are benchmarked, they can't be displayed under AI, -- because this is not realistic when player is in control. truncRep | not sbenchMessages && fromMaybe (gunderAI fact) forceReport = EM.fromList [(propFont, [(PointUI 0 0, par1)])] | otherwise = EM.empty drawOverlay ColorFull False truncRep arena -- | Render animations on top of the current screen frame. renderAnimFrames :: MonadClientUI m => LevelId -> Animation -> Maybe Bool -> m PreFrames3 renderAnimFrames arena anim forceReport = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui snoAnim <- getsClient $ snoAnim . soptions basicFrame <- basicFrameForAnimation arena forceReport smuteMessages <- getsSession smuteMessages return $! if | smuteMessages -> [] | fromMaybe False snoAnim -> [Just basicFrame] | otherwise -> map (fmap (\fr -> (fr, snd basicFrame))) $ renderAnim rwidth (fst basicFrame) anim -- | Render and display animations on top of the current screen frame. animate :: MonadClientUI m => LevelId -> Animation -> m () animate arena anim = do -- The delay before reaction to keypress was too long in case of many -- projectiles hitting actors, so frames need to be skipped. keyPressed <- anyKeyPressed unless keyPressed $ do frames <- renderAnimFrames arena anim Nothing displayFrames arena frames LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Frontend.hs0000644000000000000000000002015307346545000022610 0ustar0000000000000000{-# LANGUAGE GADTs, KindSignatures, RankNTypes #-} -- | Display game data on the screen and receive user input -- using one of the available raw frontends and derived operations. module Game.LambdaHack.Client.UI.Frontend ( -- * Connection and initialization FrontReq(..), ChanFrontend(..), chanFrontendIO -- * Re-exported part of the raw frontend , frontendName #ifdef EXPOSE_INTERNAL -- * Internal operations , FrontSetup, getKey, fchanFrontend, display, defaultMaxFps, microInSec , frameTimeoutThread, lazyStartup, nullStartup, seqFrame #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import Control.Concurrent.Async import qualified Control.Concurrent.STM as STM import Control.Monad.ST.Strict import Data.Kind (Type) import qualified Data.Text.IO as T import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed as U import Data.Word import System.IO (hFlush, stdout) import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.Frontend.Common import qualified Game.LambdaHack.Client.UI.Frontend.Teletype as Teletype import Game.LambdaHack.Client.UI.Key (KMP (..)) import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Common.ClientOptions import qualified Game.LambdaHack.Common.PointArray as PointArray import qualified Game.LambdaHack.Definition.Color as Color #ifdef USE_BROWSER import qualified Game.LambdaHack.Client.UI.Frontend.Dom as Chosen #else import qualified Game.LambdaHack.Client.UI.Frontend.ANSI as ANSI import qualified Game.LambdaHack.Client.UI.Frontend.Sdl as Chosen #endif -- | The instructions sent by clients to the raw frontend, indexed -- by the returned value. data FrontReq :: Type -> Type where -- | Show a frame. FrontFrame :: Frame -> FrontReq () -- | Perform an explicit delay of the given length. FrontDelay :: Int -> FrontReq () -- | Flush frames, display a frame and ask for a keypress. FrontKey :: [K.KM] -> Frame -> FrontReq KMP -- | Tell if a keypress is pending. FrontPressed :: FrontReq Bool -- | Discard a single key in the queue, if any. FrontDiscardKey :: FrontReq () -- | Discard all keys in the queue. FrontResetKeys :: FrontReq () -- | Shut the frontend down. FrontShutdown :: FrontReq () -- | Take screenshot. FrontPrintScreen :: FrontReq () -- | Connection channel between a frontend and a client. Frontend acts -- as a server, serving keys, etc., when given frames to display. newtype ChanFrontend = ChanFrontend (forall a. FrontReq a -> IO a) -- | Machinery allocated for an individual frontend at its startup, -- unchanged for its lifetime. data FrontSetup = FrontSetup { fasyncTimeout :: Async () , fdelay :: MVar Int } -- | Initialize the frontend chosen by the player via client options. chanFrontendIO :: ScreenContent -> ClientOptions -> IO ChanFrontend chanFrontendIO coscreen soptions = do let startup | sfrontendNull soptions = nullStartup coscreen | sfrontendLazy soptions = lazyStartup coscreen #ifndef REMOVE_TELETYPE | sfrontendTeletype soptions = Teletype.startup coscreen #endif #ifndef USE_BROWSER | sfrontendANSI soptions = ANSI.startup coscreen #endif | otherwise = Chosen.startup coscreen soptions maxFps = fromMaybe defaultMaxFps $ smaxFps soptions delta = max 1 $ round $ intToDouble microInSec / max 0.000001 maxFps rf <- startup when (sdbgMsgCli soptions) $ do T.hPutStr stdout "Frontend startup up.\n" -- hPutStrLn not atomic enough hFlush stdout fdelay <- newMVar 0 fasyncTimeout <- async $ frameTimeoutThread delta fdelay rf -- Warning: not linking @fasyncTimeout@, so it'd better not crash. let fs = FrontSetup{..} chanFrontend = fchanFrontend fs rf return chanFrontend -- Display a frame, wait for any of the specified keys (for any key, -- if the list is empty). Repeat if an unexpected key received. getKey :: FrontSetup -> RawFrontend -> [K.KM] -> Frame -> IO KMP getKey fs rf@RawFrontend{fchanKey} keys frame = do -- Wait until timeout is up, not to skip the last frame of animation. display rf frame kmp <- STM.atomically $ STM.readTQueue fchanKey if null keys || kmpKeyMod kmp `elem` keys then return kmp else getKey fs rf keys frame -- Read UI requests from the client and send them to the frontend, fchanFrontend :: FrontSetup -> RawFrontend -> ChanFrontend fchanFrontend fs@FrontSetup{..} rf = ChanFrontend $ \case FrontFrame frontFrame -> display rf frontFrame FrontDelay k -> modifyMVar_ fdelay $ return . (+ k) FrontKey frontKeyKeys frontKeyFrame -> getKey fs rf frontKeyKeys frontKeyFrame FrontPressed -> do noKeysPending <- STM.atomically $ STM.isEmptyTQueue (fchanKey rf) return $! not noKeysPending FrontDiscardKey -> void $ STM.atomically $ STM.tryReadTQueue (fchanKey rf) FrontResetKeys -> resetChanKey (fchanKey rf) FrontShutdown -> do cancel fasyncTimeout -- In case the last frame display is pending: void $ tryTakeMVar $ fshowNow rf fshutdown rf FrontPrintScreen -> fprintScreen rf display :: RawFrontend -> Frame -> IO () display rf@RawFrontend{fshowNow, fcoscreen=ScreenContent{rwidth, rheight}} ((m, upd), (ovProp, ovSquare, ovMono)) = do let new :: forall s. ST s (G.Mutable U.Vector s Word32) new = do v <- unFrameBase m unFrameForall upd v return v singleArray = PointArray.Array rwidth rheight (U.create new) putMVar fshowNow () -- 1. wait for permission to display; 3. ack fdisplay rf $ SingleFrame singleArray ovProp ovSquare ovMono defaultMaxFps :: Double defaultMaxFps = 24 microInSec :: Int microInSec = 1000000 -- This thread is canceled forcefully, because the @threadDelay@ -- may be much longer than an acceptable shutdown time. frameTimeoutThread :: Int -> MVar Int -> RawFrontend -> IO () frameTimeoutThread delta fdelay RawFrontend{..} = do let loop = do threadDelay delta let delayLoop = do delay <- readMVar fdelay when (delay > 0) $ do threadDelay $ delta * delay modifyMVar_ fdelay $ return . subtract delay delayLoop delayLoop let showFrameAndRepeatIfKeys = do -- @fshowNow@ is full at this point, unless @saveKM@ emptied it, -- in which case we wait below until @display@ fills it takeMVar fshowNow -- 2. permit display -- @fshowNow@ is ever empty only here, unless @saveKM@ empties it readMVar fshowNow -- 4. wait for ack before starting delay -- @fshowNow@ is full at this point noKeysPending <- STM.atomically $ STM.isEmptyTQueue fchanKey unless noKeysPending $ do void $ swapMVar fdelay 0 -- cancel delays lest they accumulate showFrameAndRepeatIfKeys showFrameAndRepeatIfKeys loop loop -- | The name of the chosen frontend. frontendName :: ClientOptions -> String frontendName soptions = if | sfrontendNull soptions -> "null test" | sfrontendLazy soptions -> "lazy test" #ifndef REMOVE_TELETYPE | sfrontendTeletype soptions -> Teletype.frontendName #endif #ifndef USE_BROWSER | sfrontendANSI soptions -> ANSI.frontendName #endif | otherwise -> Chosen.frontendName lazyStartup :: ScreenContent -> IO RawFrontend lazyStartup coscreen = createRawFrontend coscreen (\_ -> return ()) (return ()) nullStartup :: ScreenContent -> IO RawFrontend nullStartup coscreen = createRawFrontend coscreen seqFrame (return ()) seqFrame :: SingleFrame -> IO () seqFrame SingleFrame{..} = let seqAttr () attr = Color.colorToRGB (Color.fgFromW32 attr) `seq` Color.bgFromW32 attr `seq` Color.charFromW32 attr == ' ' `seq` () !_Force1 = PointArray.foldlA' seqAttr () singleArray !_Force2 = length singlePropOverlay !_Force3 = length singleSquareOverlay !_Force4 = length singleMonoOverlay in return () LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Frontend/0000755000000000000000000000000007346545000022253 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Frontend/ANSI.hs0000644000000000000000000003205607346545000023347 0ustar0000000000000000-- | Text frontend based on ANSI (via ansi-terminal). module Game.LambdaHack.Client.UI.Frontend.ANSI ( startup, frontendName ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent.Async import Data.Char (chr, ord) import qualified Data.Text as T import qualified System.Console.ANSI as ANSI import System.Exit (die) import qualified System.IO as SIO import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.Frontend.Common import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Content.TileKind (floorSymbol) import qualified Game.LambdaHack.Definition.Color as Color -- No session data maintained by this frontend -- | The name of the frontend. frontendName :: String frontendName = "ANSI" -- | Starts the main program loop using the frontend input and output. startup :: ScreenContent -> IO RawFrontend startup coscreen@ScreenContent{rwidth, rheight} = do ANSI.clearScreen myx <- ANSI.getTerminalSize case myx of Just (y, x) | x < rwidth || y < rheight -> -- Unlike @error@, @die@ does not move savefiles aside. die $ T.unpack $ "The terminal is too small. It should have" <+> tshow rwidth <+> "columns and" <+> tshow rheight <+> "rows, but is has" <+> tshow x <+> "columns and" <+> tshow y <+> "rows. Resize it and run the program again." _ -> do rf <- createRawFrontend coscreen (display coscreen) (shutdown coscreen) let storeKeys :: IO () storeKeys = do c <- SIO.getChar -- blocks here, so no polling s <- do if c == '\ESC' then do ready <- SIO.hReady SIO.stdin if ready then do c2 <- SIO.getChar case c2 of '\ESC' -> return [c] '[' -> keycodeInput [c, c2] 'O' -> keycodeInput [c, c2] _ -> return [c, c2] -- Alt modifier else return [c] else return [c] let K.KM{..} = keyTranslate s saveKMP rf modifier key (PointUI 0 0) storeKeys keycodeInput :: String -> IO String keycodeInput inputSoFar = do ready <- SIO.hReady SIO.stdin if ready then do c <- SIO.getChar if ord '@' <= ord c && ord c <= ord '~' -- terminator then return $ inputSoFar ++ [c] else keycodeInput $ inputSoFar ++ [c] else return inputSoFar SIO.hSetBuffering SIO.stdin SIO.NoBuffering SIO.hSetBuffering SIO.stderr $ SIO.BlockBuffering $ Just $ 2 * rwidth * rheight void $ async storeKeys return $! rf -- This is contrived, because we don't want to depend on libraries -- that read and interpret terminfo or similar on different architectures. -- The "works" comments are mostly about Gnome terminal. -- On the Gnome terminal, fo the keys mention on game help screen, -- the following don't work: C-TAB, C-S-TAB, C-R, C-?. C-/, C-{, C-}, C-q, -- C-S, C-P, C-keypad. This is acceptable. No worth adding functionality -- for decoding modifiers that would, with much luck, enable C-keypad, -- but no other broken keys. Unless more is broken on other terminals. -- On rxvt, sadly, KP_5 is a dead key. keyTranslate :: String -> K.KM keyTranslate e = (\(key, modifier) -> K.KM modifier key) $ case e of "\ESC" -> (K.Esc, K.NoModifier) -- equals @^[@ '\ESC' : '[' : rest -> keycodeTranslate rest '\ESC' : 'O' : rest -> ocodeTranslate rest ['\ESC', c] -> (K.Char c, K.Alt) "\b" -> (K.BackSpace, K.NoModifier) -- same as "\BS" and "\^H" but fails "\DEL" -> (K.BackSpace, K.NoModifier) -- works; go figure "\n" -> (K.Return, K.NoModifier) "\r" -> (K.Return, K.NoModifier) " " -> (K.Space, K.NoModifier) "\t" -> (K.Tab, K.NoModifier) -- apparently equals @\^I@ and @\HT@ [c] | ord '\^A' <= ord c && ord c <= ord '\^Z' -> -- Alas, only lower-case letters. (K.Char $ chr $ ord c - ord '\^A' + ord 'a', K.Control) | -- On (some) terminal emulators Shift-keypad direction produces -- the same code as number keys. A sensible workaround for that -- is using Control for running, but it's not clear how portable -- this is, so we do not rely on this exclusively. Since movement -- keys are more important than leader picking, we are disabling -- the latter and interpreting the keypad numbers as movement. -- -- BTW, S-KP_5 and C-KP_5 are probably still not correctly handled -- on some terminals, so this may be the biggest portability problem. c `elem` ['1'..'9'] -> (K.KP c, K.NoModifier) | otherwise -> (K.Char c, K.NoModifier) _ -> (K.Unknown e, K.NoModifier) -- From https://en.wikipedia.org/wiki/ANSI_escape_code#Terminal_input_sequences keycodeTranslate :: String -> (K.Key, K.Modifier) keycodeTranslate e = case e of "1~" -> (K.Home, K.NoModifier) "2~" -> (K.Insert, K.NoModifier) "3~" -> (K.Delete, K.NoModifier) "4~" -> (K.End , K.NoModifier) "5~" -> (K.PgUp, K.NoModifier) "6~" -> (K.PgDn, K.NoModifier) "7~" -> (K.Home, K.NoModifier) "8~" -> (K.End, K.NoModifier) "9~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "10~" -> (K.Fun 0, K.NoModifier) "11~" -> (K.Fun 1, K.NoModifier) "12~" -> (K.Fun 2, K.NoModifier) "13~" -> (K.Fun 3, K.NoModifier) "14~" -> (K.Fun 4, K.NoModifier) "15~" -> (K.Fun 5, K.NoModifier) "17~" -> (K.Fun 6, K.NoModifier) "18~" -> (K.Fun 7, K.NoModifier) "19~" -> (K.Fun 8, K.NoModifier) "20~" -> (K.Fun 9, K.NoModifier) "21~" -> (K.Fun 10, K.NoModifier) "22~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "23~" -> (K.Fun 11, K.NoModifier) "24~" -> (K.Fun 12, K.NoModifier) "25~" -> (K.Fun 13, K.NoModifier) "26~" -> (K.Fun 14, K.NoModifier) "27~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "28~" -> (K.Fun 15, K.NoModifier) "29~" -> (K.Fun 16, K.NoModifier) "30~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "32~" -> (K.Fun 18 , K.NoModifier) "33~" -> (K.Fun 19 , K.NoModifier) "34~" -> (K.Fun 20 , K.NoModifier) "35~" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "A" -> (K.Up, K.NoModifier) "B" -> (K.Down, K.NoModifier) "C" -> (K.Right, K.NoModifier) "D" -> (K.Left, K.NoModifier) "E" -> (K.Begin, K.NoModifier) "F" -> (K.End, K.NoModifier) "G" -> (K.KP '5', K.NoModifier) "H" -> (K.Home, K.NoModifier) "I" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "J" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "K" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "L" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "M" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "N" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "O" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "1P" -> (K.Fun 1, K.NoModifier) "1Q" -> (K.Fun 2, K.NoModifier) "1R" -> (K.Fun 3, K.NoModifier) "1S" -> (K.Fun 4, K.NoModifier) "T" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "U" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "V" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "W" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "X" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "Y" -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) "Z" -> (K.BackTab, K.NoModifier) -- "r" -> (K.Begin, K.NoModifier) -- "u" -> (K.Begin, K.NoModifier) _ -> (K.Unknown $ "\\ESC[" ++ e, K.NoModifier) -- From guesswork, cargo-culting and @sed -n l@. ocodeTranslate :: String -> (K.Key, K.Modifier) ocodeTranslate e = case e of "P" -> (K.Fun 1, K.NoModifier) "Q" -> (K.Fun 2, K.NoModifier) "R" -> (K.Fun 3, K.NoModifier) "S" -> (K.Fun 4, K.NoModifier) "p" -> (K.KP '0', K.Shift) "q" -> (K.KP '1', K.Shift) "r" -> (K.KP '2', K.Shift) "s" -> (K.KP '3', K.Shift) "t" -> (K.KP '4', K.Shift) "u" -> (K.KP '5', K.Shift) "v" -> (K.KP '6', K.Shift) "w" -> (K.KP '7', K.Shift) "x" -> (K.KP '8', K.Shift) "y" -> (K.KP '9', K.Shift) _ -> (K.Unknown $ "\\ESCO" ++ e, K.NoModifier) shutdown :: ScreenContent -> IO () shutdown ScreenContent{rheight} = do -- The lowest position guaranteed to exist. ANSI.hSetCursorPosition SIO.stderr (rheight - 1) 0 SIO.hFlush SIO.stdout >> SIO.hFlush SIO.stderr -- | Output to the screen via the frontend. display :: ScreenContent -> SingleFrame -> IO () display ScreenContent{rwidth} SingleFrame{singleArray} = do ANSI.hHideCursor SIO.stderr let cutInChunks [] = [] cutInChunks l = let (ch, r) = splitAt rwidth l in ch : cutInChunks r f (!y, chunk) = do ANSI.hSetCursorPosition SIO.stderr y 0 SIO.hPutStr SIO.stderr $ g chunk g = concatMap h -- Not emitting ANSI if the previous character had the same fg and bg -- gains little in terms of maximal lag, due to checkerboard levels/rooms -- (even though it triples FPS in normal rooms; but comparing with -- previous frame gains even more in normal cases and copes well -- with checkerboard; both not worth the effort for this frontend). h !w = let acChar = squashChar $ Color.charFromW32 w (fg, bg) = setAttr $ Color.attrFromW32 w in ANSI.setSGRCode [ uncurry (ANSI.SetColor ANSI.Foreground) $ colorTranslate fg , uncurry (ANSI.SetColor ANSI.Background) $ colorTranslate bg ] ++ [acChar] {- This is dubious, because I can't force bright background colour with that, only bright foregrounds. And I have at least one bright backround: bright black. -- A hack to get bright colors via the bold attribute. -- Depending on terminal settings this is needed or not -- and the characters really get bold or not. -- HSCurses does this by default, in Vty you have to request the hack, -- with ANSI we probably need it as well. ANSI.hSetSGR SIO.stderr [ANSI.SetConsoleIntensity $ if Color.isBright fg then ANSI.BoldIntensity else ANSI.NormalIntensity] -} mapM_ f $ zip [0 ..] $ cutInChunks $ PointArray.toListA singleArray let Point{..} = PointArray.maxIndexByA (comparing Color.bgFromW32) singleArray ANSI.hSetCursorPosition SIO.stderr py px ANSI.hShowCursor SIO.stderr -- Do not trash people's terminals when interrupted: ANSI.hSetSGR SIO.stderr [uncurry (ANSI.SetColor ANSI.Foreground) $ colorTranslate Color.White] ANSI.hSetSGR SIO.stderr [uncurry (ANSI.SetColor ANSI.Background) $ colorTranslate Color.Black] SIO.hFlush SIO.stderr squashChar :: Char -> Char squashChar c = if c == floorSymbol then '.' else c setAttr :: Color.Attr -> (Color.Color, Color.Color) setAttr Color.Attr{..} = let (fg1, bg1) = case bg of Color.HighlightNone -> (fg, Color.Black) Color.HighlightWhite -> if fg /= Color.Magenta then (fg, Color.Magenta) else (fg, Color.BrBlack) Color.HighlightRed -> if fg /= Color.Red then (fg, Color.Red) else (fg, Color.defFG) Color.HighlightYellow -> (fg, Color.Black) -- cursor used instead Color.HighlightYellowAim -> (Color.Black, Color.defFG) Color.HighlightRedAim -> if fg /= Color.Red then (fg, Color.Red) else (fg, Color.defFG) Color.HighlightNoneCursor -> (fg, Color.Black) _ -> if fg /= Color.highlightToColor bg then (fg, Color.highlightToColor bg) else (fg, if fg == Color.BrBlack then Color.Black else Color.BrBlack) in (fg1, bg1) colorTranslate :: Color.Color -> (ANSI.ColorIntensity, ANSI.Color) colorTranslate Color.Black = (ANSI.Dull, ANSI.Black) colorTranslate Color.Red = (ANSI.Dull, ANSI.Red) colorTranslate Color.Green = (ANSI.Dull, ANSI.Green) colorTranslate Color.Brown = (ANSI.Dull, ANSI.Yellow) colorTranslate Color.Blue = (ANSI.Dull, ANSI.Blue) colorTranslate Color.Magenta = (ANSI.Dull, ANSI.Magenta) colorTranslate Color.Cyan = (ANSI.Dull, ANSI.Cyan) colorTranslate Color.White = (ANSI.Dull, ANSI.White) colorTranslate Color.AltWhite = (ANSI.Dull, ANSI.White) colorTranslate Color.BrBlack = (ANSI.Vivid, ANSI.Black) colorTranslate Color.BrRed = (ANSI.Vivid, ANSI.Red) colorTranslate Color.BrGreen = (ANSI.Vivid, ANSI.Green) colorTranslate Color.BrYellow = (ANSI.Vivid, ANSI.Yellow) colorTranslate Color.BrBlue = (ANSI.Vivid, ANSI.Blue) colorTranslate Color.BrMagenta = (ANSI.Vivid, ANSI.Magenta) colorTranslate Color.BrCyan = (ANSI.Vivid, ANSI.Cyan) colorTranslate Color.BrWhite = (ANSI.Vivid, ANSI.White) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Frontend/Common.hs0000644000000000000000000000647207346545000024050 0ustar0000000000000000-- | Screen frames and animations. module Game.LambdaHack.Client.UI.Frontend.Common ( RawFrontend(..) , startupBound, createRawFrontend, resetChanKey, saveKMP , modifierTranslate ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import qualified Control.Concurrent.STM as STM import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.Frame 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.Common.Misc -- | Raw frontend definition. The minimal closed set of values that need -- to depend on the specifics of the chosen frontend. data RawFrontend = RawFrontend { fdisplay :: SingleFrame -> IO () , fshutdown :: IO () , fshowNow :: MVar () , fchanKey :: STM.TQueue KMP , fprintScreen :: IO () , fcoscreen :: ScreenContent } -- | Start up a frontend on a bound thread. -- -- In fact, it is started on the very main thread, via a hack, because -- apparently some SDL backends are not thread-safe -- (; -- "this should only be run in the thread that initialized the video subsystem, -- and for extra safety, you should consider only doing those things -- on the main thread in any case") -- and at least the newer OS X obtusely requires the main thread, see -- https://github.com/AllureOfTheStars/Allure/issues/79 -- In case any other exotic architecture requires the main thread, -- we make the hack the default for all (on frontends that require a bound -- thread, e.g., SLD2). startupBound :: (MVar RawFrontend -> IO ()) -> IO RawFrontend startupBound k = do rfMVar <- newEmptyMVar putMVar workaroundOnMainThreadMVar $ k rfMVar -- The following would run frontend on a bound thread, but it's not enough: -- a <- asyncBound $ k rfMVar -- link a takeMVar rfMVar createRawFrontend :: ScreenContent -> (SingleFrame -> IO ()) -> IO () -> IO RawFrontend createRawFrontend fcoscreen fdisplay fshutdown = do -- Set up the channel for keyboard input. fchanKey <- STM.atomically STM.newTQueue -- Create the session record. fshowNow <- newEmptyMVar return $! RawFrontend { fdisplay , fshutdown , fshowNow , fchanKey , fprintScreen = return () -- dummy, except for SDL2 , fcoscreen } -- | Empty the keyboard channel. resetChanKey :: STM.TQueue KMP -> IO () resetChanKey fchanKey = do res <- STM.atomically $ STM.tryReadTQueue fchanKey when (isJust res) $ resetChanKey fchanKey saveKMP :: RawFrontend -> K.Modifier -> K.Key -> PointUI -> IO () saveKMP !rf !modifier !key !kmpPointer = do -- Instantly show any frame waiting for display. void $ tryTakeMVar $ fshowNow rf let kmp = KMP{kmpKeyMod = K.KM{..}, kmpPointer} unless (key == K.DeadKey) $ -- Store the key in the channel. STM.atomically $ STM.writeTQueue (fchanKey rf) kmp -- | Translates modifiers to our own encoding. modifierTranslate :: Bool -> Bool -> Bool -> Bool -> K.Modifier modifierTranslate modCtrl modShift modAlt modMeta | (modAlt || modMeta) && modShift = K.AltShift | modAlt || modMeta = K.Alt | modCtrl && modShift = K.ControlShift | modCtrl = K.Control | modShift = K.Shift | otherwise = K.NoModifier LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Frontend/Dom.hs0000644000000000000000000002607307346545000023336 0ustar0000000000000000-- | Text frontend running in a browser. module Game.LambdaHack.Client.UI.Frontend.Dom ( #ifdef USE_BROWSER -- to molify doctest, but don't break stylish-haskell parsing startup, frontendName #endif ) where #ifdef USE_BROWSER import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import qualified Control.Monad.IO.Class as IO import Control.Monad.Trans.Reader (ask) import Data.IORef import qualified Data.Vector as V import qualified Data.Vector.Unboxed as U import Data.Word (Word32) import GHCJS.DOM (currentDocument, currentWindow) import GHCJS.DOM.CSSStyleDeclaration (setProperty) import GHCJS.DOM.Document (createElement, getBodyUnchecked) import GHCJS.DOM.Element (Element (Element), setInnerHTML) import GHCJS.DOM.ElementCSSInlineStyle (getStyle) import GHCJS.DOM.EventM ( EventM , mouseAltKey , mouseButton , mouseCtrlKey , mouseMetaKey , mouseShiftKey , on , preventDefault , stopPropagation ) import GHCJS.DOM.GlobalEventHandlers (contextMenu, keyDown, mouseUp, wheel) import GHCJS.DOM.HTMLCollection (itemUnsafe) import GHCJS.DOM.HTMLElement (focus) import GHCJS.DOM.HTMLTableElement (HTMLTableElement (HTMLTableElement), getRows, setCellPadding, setCellSpacing) import GHCJS.DOM.HTMLTableRowElement (HTMLTableRowElement (HTMLTableRowElement), getCells) import GHCJS.DOM.KeyboardEvent (getAltGraphKey, getAltKey, getCtrlKey, getKey, getMetaKey, getShiftKey) import GHCJS.DOM.Node (appendChild_, replaceChild_, setTextContent) import GHCJS.DOM.NonElementParentNode (getElementByIdUnsafe) import GHCJS.DOM.RequestAnimationFrameCallback import GHCJS.DOM.Types ( CSSStyleDeclaration , DOM , HTMLDivElement (HTMLDivElement) , HTMLTableCellElement (HTMLTableCellElement) , IsMouseEvent , JSString , Window , runDOM , unsafeCastTo ) import GHCJS.DOM.WheelEvent (getDeltaY) import GHCJS.DOM.Window (requestAnimationFrame_) import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.Frontend.Common import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Content.TileKind (floorSymbol) import qualified Game.LambdaHack.Definition.Color as Color -- | Session data maintained by the frontend. data FrontendSession = FrontendSession { scurrentWindow :: Window , scharCells :: V.Vector (HTMLTableCellElement, CSSStyleDeclaration) , spreviousFrame :: IORef SingleFrame } -- | The name of the frontend. frontendName :: String frontendName = "browser" -- | Starts the main program loop using the frontend input and output. startup :: ScreenContent -> ClientOptions -> IO RawFrontend startup coscreen soptions = do rfMVar <- newEmptyMVar flip runDOM undefined $ runWeb coscreen soptions rfMVar takeMVar rfMVar runWeb :: ScreenContent -> ClientOptions -> MVar RawFrontend -> DOM () runWeb coscreen ClientOptions{..} rfMVar = do -- Init the document. Just doc <- currentDocument Just scurrentWindow <- currentWindow body <- getBodyUnchecked doc pageStyle <- getStyle body setProp pageStyle "background-color" (Color.colorToRGB Color.Black) setProp pageStyle "color" (Color.colorToRGB Color.AltWhite) -- Create the session record. divBlockRaw <- createElement doc ("div" :: Text) divBlock <- unsafeCastTo HTMLDivElement divBlockRaw let cell = "\x00a0" row = "" ++ concat (replicate (rwidth coscreen) cell) rows = concat (replicate (rheight coscreen) row) tableElemRaw <- createElement doc ("table" :: Text) tableElem <- unsafeCastTo HTMLTableElement tableElemRaw -- Get rid of table spacing. Spurious hacks just in case. setCellPadding tableElem ("0" :: Text) setCellSpacing tableElem ("0" :: Text) appendChild_ divBlock tableElem setInnerHTML tableElem rows scharCells <- flattenTable coscreen tableElem spreviousFrame <- newIORef $ blankSingleFrame coscreen let sess = FrontendSession{..} rf <- IO.liftIO $ createRawFrontend coscreen (display sess) shutdown let readMod = do modCtrl <- ask >>= getCtrlKey modShift <- ask >>= getShiftKey modAlt <- ask >>= getAltKey modMeta <- ask >>= getMetaKey modAltG <- ask >>= getAltGraphKey return $! modifierTranslate modCtrl modShift (modAlt || modAltG) modMeta gameMap <- getElementByIdUnsafe doc ("gameMap" :: Text) divMap <- unsafeCastTo HTMLDivElement gameMap focus divMap void $ divMap `on` keyDown $ do keyId <- ask >>= getKey modifier <- readMod -- This is currently broken at least for Shift-F1, etc., so won't be used: -- keyLoc <- ask >>= getKeyLocation -- let onKeyPad = case keyLoc of -- 3 {-KEY_LOCATION_NUMPAD-} -> True -- _ -> False let key = K.keyTranslateWeb keyId (modifier == K.Shift) modifierNoShift = case modifier of -- to prevent S-!, etc. K.Shift -> K.NoModifier K.ControlShift -> K.Control K.AltShift -> K.Alt _ -> modifier -- IO.liftIO $ do -- putStrLn $ "keyId: " ++ keyId -- putStrLn $ "key: " ++ K.showKey key -- putStrLn $ "modifier: " ++ show modifier when (key == K.Esc) $ IO.liftIO $ resetChanKey (fchanKey rf) IO.liftIO $ saveKMP rf modifierNoShift key (PointUI 0 0) -- Pass through C-+ and others, but disable special behaviour on Tab, etc. let browserKeys = "+-0tTnNdxcv" unless (modifier == K.Alt || modifier == K.Control && key `elem` map K.Char browserKeys || key == K.DeadKey) $ do -- NumLock in particular preventDefault stopPropagation -- Handle mouseclicks, per-cell. let setupMouse i a = let Point{..} = punindex (rwidth coscreen) i -- abuse of convention in that @Point@ used for screen, not map pUI = squareToUI $ PointSquare px py in handleMouse rf a pUI V.imapM_ setupMouse scharCells -- Display at the end to avoid redraw. Replace "Please wait". pleaseWait <- getElementByIdUnsafe doc ("pleaseWait" :: Text) replaceChild_ gameMap divBlock pleaseWait IO.liftIO $ putMVar rfMVar rf -- send to client only after the whole webpage is set up -- because there is no @mainGUI@ to start accepting shutdown :: IO () shutdown = return () -- nothing to clean up setProp :: CSSStyleDeclaration -> JSString -> Text -> DOM () setProp style propRef propValue = setProperty style propRef propValue (Nothing :: Maybe JSString) -- | Let each table cell handle mouse events inside. handleMouse :: RawFrontend -> (HTMLTableCellElement, CSSStyleDeclaration) -> PointUI -> DOM () handleMouse rf (cell, _) pUI = do let readMod :: IsMouseEvent e => EventM HTMLTableCellElement e K.Modifier readMod = do modCtrl <- mouseCtrlKey modShift <- mouseShiftKey modAlt <- mouseAltKey modMeta <- mouseMetaKey return $! modifierTranslate modCtrl modShift modAlt modMeta saveWheel = do wheelY <- ask >>= getDeltaY modifier <- readMod let mkey = if | wheelY < -0.01 -> Just K.WheelNorth | wheelY > 0.01 -> Just K.WheelSouth | otherwise -> Nothing -- probably a glitch maybe (return ()) (\key -> IO.liftIO $ saveKMP rf modifier key pUI) mkey saveMouse = do -- but <- mouseButton modifier <- readMod let key = case but of 0 -> K.LeftButtonRelease 1 -> K.MiddleButtonRelease 2 -> K.RightButtonRelease -- not handled in contextMenu _ -> K.LeftButtonRelease -- any other is alternate left -- IO.liftIO $ putStrLn $ -- "m: " ++ show but ++ show modifier ++ show pUI IO.liftIO $ saveKMP rf modifier key pUI void $ cell `on` wheel $ do saveWheel preventDefault stopPropagation void $ cell `on` contextMenu $ do preventDefault stopPropagation void $ cell `on` mouseUp $ do saveMouse preventDefault stopPropagation -- | Get the list of all cells of an HTML table. flattenTable :: ScreenContent -> HTMLTableElement -> DOM (V.Vector (HTMLTableCellElement, CSSStyleDeclaration)) flattenTable coscreen table = do rows <- getRows table let f y = do rowsItem <- itemUnsafe rows y unsafeCastTo HTMLTableRowElement rowsItem lrow <- mapM f [0 .. toEnum (rheight coscreen - 1)] let getC :: HTMLTableRowElement -> DOM [(HTMLTableCellElement, CSSStyleDeclaration)] getC row = do cells <- getCells row let g x = do cellsItem <- itemUnsafe cells x cell <- unsafeCastTo HTMLTableCellElement cellsItem style <- getStyle cell return (cell, style) mapM g [0 .. toEnum (rwidth coscreen - 1)] lrc <- mapM getC lrow return $! V.fromListN (rwidth coscreen * rheight coscreen) $ concat lrc -- | Output to the screen via the frontend. display :: FrontendSession -- ^ frontend session data -> SingleFrame -- ^ the screen frame to draw -> IO () display FrontendSession{..} !curFrame = flip runDOM undefined $ do let setChar :: Int -> (Word32, Word32) -> DOM Int setChar !i (!w, !wPrev) | w == wPrev = return $! i + 1 setChar i (w, wPrev) = do let Point{..} = toEnum i Color.AttrChar{acAttr=Color.Attr{fg=fgRaw,bg}, acChar} = Color.attrCharFromW32 $ Color.AttrCharW32 w fg | even py && fgRaw == Color.White = Color.AltWhite | otherwise = fgRaw (!cell, !style) = scharCells V.! i if | acChar == ' ' -> setTextContent cell $ Just ("\x00a0" :: JSString) | acChar == floorSymbol && not (Color.isBright fg) -> setTextContent cell $ Just ("\x22C5" :: JSString) | otherwise -> setTextContent cell $ Just [acChar] setProp style "color" $ Color.colorToRGB fg let bgPrev = Color.bgFromW32 $ Color.AttrCharW32 wPrev when (bg /= bgPrev) $ do let background = if bg == Color.HighlightBackground then "#251F1F" else Color.colorToRGB Color.Black setProp style "background-color" background setProp style "border-color" (Color.colorToRGB $ Color.highlightToColor bg) return $! i + 1 !prevFrame <- readIORef spreviousFrame writeIORef spreviousFrame curFrame -- This continues asynchronously, if can't otherwise. callback <- newRequestAnimationFrameCallbackSync $ \_ -> U.foldM'_ setChar 0 $ U.zip (PointArray.avector $ singleArray curFrame) (PointArray.avector $ singleArray prevFrame) -- This attempts to ensure no redraws while callback executes -- and a single redraw when it completes. requestAnimationFrame_ scurrentWindow callback #endif LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Frontend/Sdl.hs0000644000000000000000000012346307346545000023342 0ustar0000000000000000-- | Text frontend based on SDL2. module Game.LambdaHack.Client.UI.Frontend.Sdl ( startup, frontendName #ifdef EXPOSE_INTERNAL -- * Internal operations , FontAtlas, FrontendSession(..), startupFun, shutdown, forceShutdown , display, drawFrame, printScreen, modTranslate, keyTranslate, colorToRGBA #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import qualified Data.Char as Char import qualified Data.EnumMap.Strict as EM import Data.IORef import qualified Data.Text as T import Data.Time.Clock.POSIX import Data.Time.LocalTime import qualified Data.Vector.Storable as VS import qualified Data.Vector.Unboxed as U import Data.Word (Word32, Word8) import Foreign.C.String (withCString) import Foreign.C.Types (CInt) import Foreign.Ptr (nullPtr) import Foreign.Storable (peek) import System.Directory import System.Exit (die, exitSuccess) import System.FilePath import qualified SDL import qualified SDL.Font as TTF import SDL.Input.Keyboard.Codes import qualified SDL.Internal.Types import qualified SDL.Raw.Basic as SDL (logSetAllPriority) import qualified SDL.Raw.Enum import qualified SDL.Raw.Event import qualified SDL.Raw.Types import qualified SDL.Raw.Video import qualified SDL.Vect as Vect import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.Frontend.Common import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.File import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Content.TileKind (floorSymbol) import qualified Game.LambdaHack.Definition.Color as Color -- These are needed until SDL is fixed and all our devs move -- to the fixed version: import Control.Monad.IO.Class (MonadIO, liftIO) import SDL.Internal.Exception (throwIfNull) import qualified SDL.Raw.Event as Raw import Unsafe.Coerce (unsafeCoerce) --import qualified SDL.Raw.Enum as Raw type FontAtlas = EM.EnumMap Color.AttrCharW32 SDL.Texture -- | Session data maintained by the frontend. data FrontendSession = FrontendSession { swindow :: SDL.Window , srenderer :: SDL.Renderer , squareFont :: TTF.Font , squareFontSize :: Int , mapFontIsBitmap :: Bool , spropFont :: Maybe TTF.Font , sboldFont :: Maybe TTF.Font , smonoFont :: Maybe TTF.Font , squareAtlas :: IORef FontAtlas , smonoAtlas :: IORef FontAtlas , sbasicTexture :: IORef SDL.Texture , stexture :: IORef SDL.Texture , spreviousFrame :: IORef SingleFrame , sforcedShutdown :: IORef Bool , scontinueSdlLoop :: IORef Bool , sframeQueue :: MVar SingleFrame , sframeDrawn :: MVar () } -- | The name of the frontend. frontendName :: String frontendName = "sdl" -- | Set up and start the main loop providing input and output. -- -- Because of Windows and OS X, SDL2 needs to be on a bound thread, -- so we can't avoid the communication overhead of bound threads. startup :: ScreenContent -> ClientOptions -> IO RawFrontend startup coscreen soptions = startupBound $ startupFun coscreen soptions startupFun :: ScreenContent -> ClientOptions -> MVar RawFrontend -> IO () startupFun coscreen soptions@ClientOptions{..} rfMVar = do SDL.initialize [SDL.InitEvents] -- lowest: pattern SDL_LOG_PRIORITY_VERBOSE = (1) :: LogPriority -- our default: pattern SDL_LOG_PRIORITY_ERROR = (5) :: LogPriority SDL.logSetAllPriority $ toEnum $ fromMaybe 5 slogPriority TTF.initialize let title = T.pack $ fromJust stitle chosenFontsetID = fromJust schosenFontset -- Unlike @error@, @die@ does not move savefiles aside. chosenFontset <- case lookup chosenFontsetID sfontsets of Nothing -> die $ "Fontset not defined in config file" `showFailure` chosenFontsetID Just fs -> return fs -- If some auxiliary fonts are equal and at the same size, this wastefully -- opens them many times. However, native builds are efficient enough -- and slow machines should use the most frugal case (only square font) -- in which no waste occurs and all rendering is aided with an atlas. let findFontFile t = if T.null t then return Nothing else case lookup t sfonts of Nothing -> die $ "Font not defined in config file" `showFailure` t Just (FontProportional fname fsize fhint) -> do sdlFont <- loadFontFile fname fsize setHintMode sdlFont fhint -- TODO: when SDL_ttf can do it, check that not a bitmap font realSize <- TTF.height sdlFont let !_A = assert (realSize > 0) () -- sanity return $ Just (sdlFont, realSize) Just (FontMonospace fname fsize fhint) -> do sdlFont <- loadFontFile fname fsize setHintMode sdlFont fhint isFontMono <- TTF.isMonospace sdlFont realSize <- TTF.height sdlFont let !_A = assert (isFontMono && realSize > 0) () -- sanity return $ Just (sdlFont, realSize) Just (FontMapScalable fname fsize fhint cellSizeAdd) -> do sdlFont <- loadFontFile fname fsize setHintMode sdlFont fhint isFontMono <- TTF.isMonospace sdlFont realSize <- TTF.height sdlFont let !_A = assert (isFontMono && realSize > 0) () -- sanity return $ Just (sdlFont, realSize + cellSizeAdd) Just (FontMapBitmap fname cellSizeAdd) -> do sdlFont <- loadFontFile fname 0 -- size ignored for bitmap fonts isFontMono <- TTF.isMonospace sdlFont realSize <- TTF.height sdlFont let !_A = assert (isFontMono && realSize > 0) () -- sanity return $ Just (sdlFont, realSize + cellSizeAdd) loadFontFile fname fsize = do let fontFileName = T.unpack fname fontSize = round $ fromJust sallFontsScale * intToDouble fsize if isRelative fontFileName then do case lookup fontFileName $ rFontFiles coscreen of Nothing -> fail $ "Font file not supplied with the game: " ++ fontFileName ++ " within " ++ show (map fst $ rFontFiles coscreen) Just bs -> TTF.decode bs fontSize else do fontFileExists <- doesFileExist fontFileName if not fontFileExists then fail $ "Font file does not exist: " ++ fontFileName else TTF.load fontFileName fontSize setHintMode _ HintingHeavy = return () -- default setHintMode sdlFont HintingLight = TTF.setHinting sdlFont TTF.Light (squareFont, squareFontSize, mapFontIsBitmap) <- if fromJust sallFontsScale == 1.0 then do mfontMapBitmap <- findFontFile $ fontMapBitmap chosenFontset case mfontMapBitmap of Just (sdlFont, size) -> return (sdlFont, size, True) Nothing -> do mfontMapScalable <- findFontFile $ fontMapScalable chosenFontset case mfontMapScalable of Just (sdlFont, size) -> return (sdlFont, size, False) Nothing -> die "Neither bitmap nor scalable map font defined" else do mfontMapScalable <- findFontFile $ fontMapScalable chosenFontset case mfontMapScalable of Just (sdlFont, size) -> return (sdlFont, size, False) Nothing -> die "Scaling requested but scalable map font not defined" let halfSize = squareFontSize `div` 2 boxSize = 2 * halfSize -- map font determines cell size for all others -- Real size of these fonts ignored. spropFont <- fst <$$> findFontFile (fontPropRegular chosenFontset) sboldFont <- fst <$$> findFontFile (fontPropBold chosenFontset) smonoFont <- fst <$$> findFontFile (fontMono chosenFontset) let !_A = assert (isJust spropFont && isJust sboldFont && isJust smonoFont || isNothing spropFont && isNothing sboldFont && isNothing smonoFont `blame` "Either all auxiliary fonts should be defined or none" `swith` chosenFontset) () -- The hacky log priority 0 tells SDL frontend to init and quit at once, -- for testing on CIs without graphics access. if slogPriority == Just 0 then do rf <- createRawFrontend coscreen (\_ -> return ()) (return ()) putMVar rfMVar rf maybe (return ()) TTF.free spropFont maybe (return ()) TTF.free sboldFont maybe (return ()) TTF.free smonoFont TTF.free squareFont TTF.quit SDL.quit else do -- The code below fails without access to a graphics system. SDL.initialize [SDL.InitVideo] -- This cursor size if fine for default size and Full HD 1.5x size. let (cursorAlpha, cursorBW) = cursorXhair xhairCursor <- createCursor cursorBW cursorAlpha (SDL.V2 32 27) (SDL.P (SDL.V2 13 13)) SDL.activeCursor SDL.$= xhairCursor -- xhairCursor <- -- throwIfNull "SDL.Input.Mouse.createSystemCursor" "SDL_createSystemCursor" -- $ Raw.createSystemCursor Raw.SDL_SYSTEM_CURSOR_CROSSHAIR -- SDL.activeCursor SDL.$= unsafeCoerce xhairCursor let screenV2 = SDL.V2 (toEnum $ rwidth coscreen * boxSize) (toEnum $ rheight coscreen * boxSize) windowConfig = SDL.defaultWindow { SDL.windowInitialSize = screenV2 , SDL.windowMode = case fromMaybe NotFullscreen sfullscreenMode of ModeChange -> SDL.Fullscreen BigBorderlessWindow -> SDL.FullscreenDesktop NotFullscreen -> SDL.Windowed } rendererConfig = SDL.RendererConfig { rendererType = if sbenchmark then SDL.AcceleratedRenderer else SDL.AcceleratedVSyncRenderer , rendererTargetTexture = True } swindow <- SDL.createWindow title windowConfig srenderer <- SDL.createRenderer swindow (-1) rendererConfig unless (fromMaybe NotFullscreen sfullscreenMode == NotFullscreen) $ -- This is essential to preserve game map aspect ratio in fullscreen, etc., -- if the aspect ratios of video mode and game map view don't match. SDL.rendererLogicalSize srenderer SDL.$= Just screenV2 let clearScreen = do -- Display black screen ASAP to hide any garbage. This is also needed -- to clear trash on the margins in fullscreen. No idea why the double -- calls are needed, sometimes. Perhaps it's double-buffered. SDL.rendererRenderTarget srenderer SDL.$= Nothing SDL.clear srenderer -- clear the backbuffer SDL.present srenderer SDL.clear srenderer -- clear the other half of the double buffer? SDL.present srenderer clearScreen let initTexture = do texture <- SDL.createTexture srenderer SDL.ARGB8888 SDL.TextureAccessTarget screenV2 SDL.rendererRenderTarget srenderer SDL.$= Just texture SDL.rendererDrawBlendMode srenderer SDL.$= SDL.BlendNone SDL.rendererDrawColor srenderer SDL.$= blackRGBA SDL.clear srenderer -- clear the texture return texture basicTexture <- initTexture sbasicTexture <- newIORef basicTexture texture <- initTexture stexture <- newIORef texture squareAtlas <- newIORef EM.empty smonoAtlas <- newIORef EM.empty spreviousFrame <- newIORef $ blankSingleFrame coscreen sforcedShutdown <- newIORef False scontinueSdlLoop <- newIORef True sframeQueue <- newEmptyMVar sframeDrawn <- newEmptyMVar let sess = FrontendSession{..} rfWithoutPrintScreen <- createRawFrontend coscreen (display sess) (shutdown sess) let rf = rfWithoutPrintScreen {fprintScreen = printScreen sess} putMVar rfMVar rf let pointTranslate :: forall i. (Enum i) => Vect.Point Vect.V2 i -> PointUI pointTranslate (SDL.P (SDL.V2 x y)) = PointUI (fromEnum x `div` halfSize) (fromEnum y `div` boxSize) redraw = do -- Textures may be trashed and even invalid, especially on Windows. atlas <- readIORef squareAtlas writeIORef squareAtlas EM.empty monoAtlas <- readIORef smonoAtlas writeIORef smonoAtlas EM.empty oldBasicTexture <- readIORef sbasicTexture newBasicTexture <- initTexture oldTexture <- readIORef stexture newTexture <- initTexture mapM_ SDL.destroyTexture $ EM.elems atlas mapM_ SDL.destroyTexture $ EM.elems monoAtlas SDL.destroyTexture oldBasicTexture SDL.destroyTexture oldTexture writeIORef sbasicTexture newBasicTexture writeIORef stexture newTexture -- To clear the margins in fullscreen: clearScreen -- To overwrite each char: prevFrame <- readIORef spreviousFrame writeIORef spreviousFrame $ blankSingleFrame coscreen drawFrame coscreen soptions sess prevFrame SDL.pumpEvents SDL.Raw.Event.flushEvents minBound maxBound loopSDL :: IO () loopSDL = do me <- SDL.pollEvent -- events take precedence over frames case me of Nothing -> do mfr <- tryTakeMVar sframeQueue case mfr of Just fr -> do -- Some SDL2 (OpenGL) backends are very thread-unsafe, -- so we need to ensure we draw on the same (bound) OS thread -- that initialized SDL, hence we have to poll frames. drawFrame coscreen soptions sess fr putMVar sframeDrawn () -- signal that drawing ended Nothing -> threadDelay $ if sbenchmark then 150 else 15000 -- 60 polls per second, so keyboard snappy enough; -- max 6000 FPS when benchmarking Just e -> handleEvent e continueSdlLoop <- readIORef scontinueSdlLoop if continueSdlLoop then loopSDL else do maybe (return ()) TTF.free spropFont maybe (return ()) TTF.free sboldFont maybe (return ()) TTF.free smonoFont TTF.free squareFont TTF.quit SDL.destroyRenderer srenderer SDL.destroyWindow swindow SDL.quit forcedShutdown <- readIORef sforcedShutdown when forcedShutdown exitSuccess -- not in the main thread, so no exit yet, see "Main" handleEvent e = case SDL.eventPayload e of SDL.KeyboardEvent keyboardEvent | SDL.keyboardEventKeyMotion keyboardEvent == SDL.Pressed -> do let sym = SDL.keyboardEventKeysym keyboardEvent ksm = SDL.keysymModifier sym shiftPressed = SDL.keyModifierLeftShift ksm || SDL.keyModifierRightShift ksm key = keyTranslate shiftPressed $ SDL.keysymKeycode sym modifier = modTranslate ksm modifierNoShift = case modifier of -- to prevent S-!, etc. K.Shift -> K.NoModifier K.ControlShift -> K.Control K.AltShift -> K.Alt _ -> modifier p <- SDL.getAbsoluteMouseLocation when (key == K.Esc) $ resetChanKey (fchanKey rf) saveKMP rf modifierNoShift key (pointTranslate p) SDL.MouseButtonEvent mouseButtonEvent | SDL.mouseButtonEventMotion mouseButtonEvent == SDL.Released -> do modifier <- modTranslate <$> SDL.getModState let key = case SDL.mouseButtonEventButton mouseButtonEvent of SDL.ButtonLeft -> K.LeftButtonRelease SDL.ButtonMiddle -> K.MiddleButtonRelease SDL.ButtonRight -> K.RightButtonRelease _ -> K.LeftButtonRelease -- any other is spare left p = SDL.mouseButtonEventPos mouseButtonEvent saveKMP rf modifier key (pointTranslate p) SDL.MouseWheelEvent mouseWheelEvent -> do modifier <- modTranslate <$> SDL.getModState let SDL.V2 _ y = SDL.mouseWheelEventPos mouseWheelEvent mkey = case (compare y 0, SDL.mouseWheelEventDirection mouseWheelEvent) of (EQ, _) -> Nothing (LT, SDL.ScrollNormal) -> Just K.WheelSouth (GT, SDL.ScrollNormal) -> Just K.WheelNorth (LT, SDL.ScrollFlipped) -> Just K.WheelNorth (GT, SDL.ScrollFlipped) -> Just K.WheelSouth p <- SDL.getAbsoluteMouseLocation maybe (return ()) (\key -> saveKMP rf modifier key (pointTranslate p)) mkey SDL.WindowClosedEvent{} -> forceShutdown sess 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." redraw -- Probably not needed, because no textures nor their content lost: -- SDL.WindowShownEvent{} -> redraw _ -> return () loopSDL -- | Copied from SDL2 and fixed (packed booleans are needed). -- -- Create a cursor using the specified bitmap data and mask (in MSB format, -- packed). Width must be a multiple of 8. -- -- createCursor :: MonadIO m => VS.Vector Word8 -- ^ Whether this part of the cursor is black. Use bit 1 for white and bit 0 for black. -> VS.Vector Word8 -- ^ Whether or not pixels are visible. Use bit 1 for visible and bit 0 for transparent. -> Vect.V2 CInt -- ^ The width and height of the cursor. -> Vect.Point Vect.V2 CInt -- ^ The X- and Y-axis location of the upper left corner of the cursor relative to the actual mouse position -> m SDL.Cursor createCursor dta msk (Vect.V2 w h) (Vect.P (Vect.V2 hx hy)) = liftIO . fmap unsafeCoerce $ throwIfNull "SDL.Input.Mouse.createCursor" "SDL_createCursor" $ VS.unsafeWith dta $ \unsafeDta -> VS.unsafeWith msk $ \unsafeMsk -> Raw.createCursor unsafeDta unsafeMsk w h hx hy -- Ignores bits after the last 8 multiple. boolListToWord8List :: [Bool] -> [Word8] boolListToWord8List = let i True multiple = multiple i False _ = 0 in \case b1 : b2 : b3 : b4 : b5 : b6 : b7 : b8 : rest -> i b1 128 + i b2 64 + i b3 32 + i b4 16 + i b5 8 + i b6 4 + i b7 2 + i b8 1 : boolListToWord8List rest _ -> [] cursorXhair :: (VS.Vector Word8, VS.Vector Word8) -- alpha, BW cursorXhair = let charToBool '.' = (True, True) -- visible black charToBool '#' = (True, False) -- visible white charToBool _ = (False, False) -- transparent white toVS = VS.fromList . boolListToWord8List in toVS *** toVS $ unzip $ map charToBool $ concat [ " ... " , " .#. " , " .. .#. .. " , " ..## .#. ##.. " , " .## .#. ##. " , " .# .#. #. " , " .# .#. #. " , " .# ... #. " , " .# #. " , " .# #. " , " " , " . " , "........ .#. ........ " , ".######. .###. .######. " , "........ .#. ........ " , " . " , " " , " .# #. " , " .# #. " , " .# ... #. " , " .# .#. #. " , " .# .#. #. " , " .## .#. ##. " , " ..## .#. ##.. " , " .. .#. .. " , " .#. " , " ... " ] shutdown :: FrontendSession -> IO () shutdown FrontendSession{..} = writeIORef scontinueSdlLoop False forceShutdown :: FrontendSession -> IO () forceShutdown sess@FrontendSession{..} = do writeIORef sforcedShutdown True shutdown sess -- | Add a frame to be drawn. display :: FrontendSession -- ^ frontend session data -> SingleFrame -- ^ the screen frame to draw -> IO () display FrontendSession{..} curFrame = do continueSdlLoop <- readIORef scontinueSdlLoop if continueSdlLoop then do putMVar sframeQueue curFrame -- Wait until the frame is drawn. takeMVar sframeDrawn else do forcedShutdown <- readIORef sforcedShutdown when forcedShutdown $ -- When there's a forced shutdown, ignore displaying one frame -- and don't occupy the CPU creating new ones and moving on with the game -- (possibly also saving the new game state, surprising the player), -- but delay the server and client thread(s) for a long time -- and let the SDL-init thread clean up and exit via @exitSuccess@ -- to avoid exiting via "thread blocked". threadDelay 50000 drawFrame :: ScreenContent -- ^ e.g., game screen size -> ClientOptions -- ^ client options -> FrontendSession -- ^ frontend session data -> SingleFrame -- ^ the screen frame to draw -> IO () drawFrame coscreen ClientOptions{..} sess@FrontendSession{..} curFrame = do prevFrame <- readIORef spreviousFrame let halfSize = squareFontSize `div` 2 boxSize = 2 * halfSize tt2Square = Vect.V2 (toEnum boxSize) (toEnum boxSize) vp :: Int -> Int -> Vect.Point Vect.V2 CInt vp x y = Vect.P $ Vect.V2 (toEnum x) (toEnum y) drawHighlight !col !row !color = do SDL.rendererDrawColor srenderer SDL.$= colorToRGBA color let rect = SDL.Rectangle (vp (col * boxSize) (row * boxSize)) tt2Square SDL.drawRect srenderer $ Just rect SDL.rendererDrawColor srenderer SDL.$= blackRGBA -- reset back to black chooseAndDrawHighlight !col !row !bg = do -- Rectangle drawing is broken in SDL 2.0.16 -- (https://github.com/LambdaHack/LambdaHack/issues/281) -- and simple workarounds fail with old SDL, e.g., four lines instead of -- a rectangle, so we have to manually erase the broken rectangles -- instead of depending on glyphs overwriting them fully. let workaroundOverwriteHighlight = do let rect = SDL.Rectangle (vp (col * boxSize) (row * boxSize)) tt2Square SDL.drawRect srenderer $ Just rect case bg of Color.HighlightNone -> workaroundOverwriteHighlight Color.HighlightBackground -> workaroundOverwriteHighlight Color.HighlightNoneCursor -> workaroundOverwriteHighlight _ -> drawHighlight col row $ Color.highlightToColor bg -- workarounds end -- This also frees the surface it gets. scaleSurfaceToTexture :: Int -> SDL.Surface -> IO SDL.Texture scaleSurfaceToTexture xsize textSurfaceRaw = do Vect.V2 sw sh <- SDL.surfaceDimensions textSurfaceRaw let width = min xsize $ fromEnum sw height = min boxSize $ fromEnum sh xsrc = max 0 (fromEnum sw - width) `div` 2 ysrc = max 0 (fromEnum sh - height) `divUp` 2 srcR = SDL.Rectangle (vp xsrc ysrc) (Vect.V2 (toEnum width) (toEnum height)) xtgt = (xsize - width) `divUp` 2 ytgt = (boxSize - height) `div` 2 tgtR = vp xtgt ytgt tt2 = Vect.V2 (toEnum xsize) (toEnum boxSize) textSurface <- SDL.createRGBSurface tt2 SDL.ARGB8888 SDL.surfaceFillRect textSurface Nothing blackRGBA -- We crop surface rather than texture to set the resulting -- texture as @TextureAccessStatic@ via @createTextureFromSurface@, -- which otherwise we wouldn't be able to do. void $ SDL.surfaceBlit textSurfaceRaw (Just srcR) textSurface (Just tgtR) SDL.freeSurface textSurfaceRaw textTexture <- SDL.createTextureFromSurface srenderer textSurface SDL.freeSurface textSurface return textTexture -- This also frees the surface it gets. scaleSurfaceToTextureProp :: Int -> Int -> SDL.Surface -> Bool -> IO (Int, SDL.Texture) scaleSurfaceToTextureProp x row textSurfaceRaw allSpace = do Vect.V2 sw sh <- SDL.surfaceDimensions textSurfaceRaw let widthRaw = fromEnum sw remainingWidth = rwidth coscreen * boxSize - x width | widthRaw <= remainingWidth = widthRaw | allSpace = remainingWidth | otherwise = remainingWidth - boxSize height = min boxSize $ fromEnum sh xsrc = 0 ysrc = max 0 (fromEnum sh - height) `divUp` 2 srcR = SDL.Rectangle (vp xsrc ysrc) (Vect.V2 (toEnum width) (toEnum height)) xtgt = 0 ytgt = (boxSize - height) `div` 2 tgtR = vp xtgt ytgt tt2Prop = Vect.V2 (toEnum width) (toEnum boxSize) textSurface <- SDL.createRGBSurface tt2Prop SDL.ARGB8888 SDL.surfaceFillRect textSurface Nothing blackRGBA -- We crop surface rather than texture to set the resulting -- texture as @TextureAccessStatic@ via @createTextureFromSurface@, -- which otherwise we wouldn't be able to do. -- This is not essential for proportional font, for which we have -- no texture atlas, but it's consistent with other fonts -- and the bottleneck is the square font, anyway. void $ SDL.surfaceBlit textSurfaceRaw (Just srcR) textSurface (Just tgtR) SDL.freeSurface textSurfaceRaw textTexture <- SDL.createTextureFromSurface srenderer textSurface SDL.freeSurface textSurface when (width /= widthRaw && not allSpace) $ setSquareChar (rwidth coscreen - 1) row Color.trimmedLineAttrW32 return (width, textTexture) -- -- Note that @Point@ here refers to screen coordinates with square font -- (as @PointSquare@ normally should) and not game map coordinates. -- See "Game.LambdaHack.Client.UI.Frame" for explanation of this -- irregularity. setMapChar :: PointI -> (Word32, Word32) -> IO Int setMapChar !i (!w, !wPrev) = if w == wPrev then return $! i + 1 else do let Point{..} = toEnum i setSquareChar px py (Color.AttrCharW32 w) return $! i + 1 drawMonoOverlay :: OverlaySpace -> IO () drawMonoOverlay = mapM_ (\(PointUI x y, al) -> let lineCut = take (2 * rwidth coscreen - x) al in drawMonoLine x y lineCut) drawMonoLine :: Int -> Int -> AttrString -> IO () drawMonoLine _ _ [] = return () drawMonoLine x row (w : rest) = do setMonoChar x row w drawMonoLine (x + 1) row rest setMonoChar :: Int -> Int -> Color.AttrCharW32 -> IO () setMonoChar !x !row !w = do atlas <- readIORef smonoAtlas let Color.AttrChar{acAttr=Color.Attr{fg=fgRaw, bg}, acChar} = Color.attrCharFromW32 w fg | even row && fgRaw == Color.White = Color.AltWhite | otherwise = fgRaw ac = Color.attrChar2ToW32 fg acChar !_A = assert (bg `elem` [ Color.HighlightNone , Color.HighlightNoneCursor ]) () textTexture <- case EM.lookup ac atlas of Nothing -> do textSurfaceRaw <- TTF.shadedGlyph (fromJust smonoFont) (colorToRGBA fg) blackRGBA acChar textTexture <- scaleSurfaceToTexture halfSize textSurfaceRaw writeIORef smonoAtlas $ EM.insert ac textTexture atlas return textTexture Just textTexture -> return textTexture let tt2Mono = Vect.V2 (toEnum halfSize) (toEnum boxSize) tgtR = SDL.Rectangle (vp (x * halfSize) (row * boxSize)) tt2Mono SDL.copy srenderer textTexture Nothing (Just tgtR) drawSquareOverlay :: OverlaySpace -> IO () drawSquareOverlay = mapM_ (\(pUI, al) -> let PointSquare col row = uiToSquare pUI lineCut = take (rwidth coscreen - col) al in drawSquareLine col row lineCut) drawSquareLine :: Int -> Int -> AttrString -> IO () drawSquareLine _ _ [] = return () drawSquareLine col row (w : rest) = do setSquareChar col row w drawSquareLine (col + 1) row rest setSquareChar :: Int -> Int -> Color.AttrCharW32 -> IO () setSquareChar !col !row !w = do atlas <- readIORef squareAtlas let Color.AttrChar{ acAttr=Color.Attr{fg=fgRaw, bg} , acChar=acCharRaw } = Color.attrCharFromW32 w fg | even row && fgRaw == Color.White = Color.AltWhite | otherwise = fgRaw ac = if bg == Color.HighlightBackground then w else Color.attrChar2ToW32 fg acCharRaw textTexture <- case EM.lookup ac atlas of Nothing -> do -- Make all visible floors bold (no bold font variant for 16x16x, -- so only the dot can be bold). let acChar = if not (Color.isBright fg) && acCharRaw == floorSymbol -- '\x00B7' then if mapFontIsBitmap then '\x0007' else '\x22C5' else acCharRaw background = if bg == Color.HighlightBackground then greyRGBA else blackRGBA textSurfaceRaw <- TTF.shadedGlyph squareFont (colorToRGBA fg) background acChar textTexture <- scaleSurfaceToTexture boxSize textSurfaceRaw writeIORef squareAtlas $ EM.insert ac textTexture atlas return textTexture Just textTexture -> return textTexture let tgtR = SDL.Rectangle (vp (col * boxSize) (row * boxSize)) tt2Square SDL.copy srenderer textTexture Nothing (Just tgtR) -- Potentially overwrite a portion of the glyph. chooseAndDrawHighlight col row bg drawPropOverlay :: OverlaySpace -> IO () drawPropOverlay = mapM_ (\(PointUI x y, al) -> drawPropLine (x * halfSize) y al) drawPropLine :: Int -> Int -> AttrString -> IO () drawPropLine _ _ [] = return () drawPropLine x _ _ | x >= (rwidth coscreen - 1) * boxSize = -- This chunk starts at $ sign or beyond so, for KISS, reject it. return () drawPropLine x row (w : rest) = do let isSpace = (== Color.spaceAttrW32) Color.AttrChar{acAttr=Color.Attr{fg=fgRaw, bg}} = Color.attrCharFromW32 $ if isSpace w then case filter (not . isSpace) rest of w2 : _ -> w2 [] -> w else w sameAttr ac = Color.fgFromW32 ac == fgRaw || isSpace ac -- matches all colours (sameRest, otherRest) = span sameAttr rest !_A = assert (bg `elem` [ Color.HighlightNone , Color.HighlightNoneCursor ]) () fg | even row && fgRaw == Color.White = Color.AltWhite | otherwise = fgRaw t = T.pack $ map Color.charFromW32 $ w : sameRest width <- drawPropChunk x row fg t drawPropLine (x + width) row otherRest drawPropChunk :: Int -> Int -> Color.Color -> T.Text -> IO Int drawPropChunk x row fg t = do let font = if fg >= Color.White && fg /= Color.BrBlack then spropFont else sboldFont allSpace = T.all Char.isSpace t textSurfaceRaw <- TTF.shaded (fromJust font) (colorToRGBA fg) blackRGBA t (width, textTexture) <- scaleSurfaceToTextureProp x row textSurfaceRaw allSpace let tgtR = SDL.Rectangle (vp x (row * boxSize)) (Vect.V2 (toEnum width) (toEnum boxSize)) -- Potentially overwrite some of the screen. SDL.copy srenderer textTexture Nothing (Just tgtR) SDL.destroyTexture textTexture return width let arraysEqual = singleArray curFrame == singleArray prevFrame overlaysEqual = singleMonoOverlay curFrame == singleMonoOverlay prevFrame && singleSquareOverlay curFrame == singleSquareOverlay prevFrame && singlePropOverlay curFrame == singlePropOverlay prevFrame basicTexture <- readIORef sbasicTexture -- previous content still present unless arraysEqual $ do SDL.rendererRenderTarget srenderer SDL.$= Just basicTexture U.foldM'_ setMapChar 0 $ U.zip (PointArray.avector $ singleArray curFrame) (PointArray.avector $ singleArray prevFrame) unless (arraysEqual && overlaysEqual) $ do texture <- readIORef stexture SDL.rendererRenderTarget srenderer SDL.$= Just texture SDL.copy srenderer basicTexture Nothing Nothing -- overwrite last content -- Mono overlay rendered last, because more likely to come after -- the proportional one and so to have a warning message about overrun -- that needs to be overlaid on top of the proportional overlay. drawPropOverlay $ singlePropOverlay curFrame drawSquareOverlay $ singleSquareOverlay curFrame drawMonoOverlay $ singleMonoOverlay curFrame writeIORef spreviousFrame curFrame SDL.rendererRenderTarget srenderer SDL.$= Nothing SDL.copy srenderer texture Nothing Nothing -- overwrite the backbuffer SDL.present srenderer -- We can't print screen in @display@ due to thread-unsafety. when sprintEachScreen $ printScreen sess -- It can't seem to cope with SDL_PIXELFORMAT_INDEX8, so we are stuck -- with huge bitmaps. printScreen :: FrontendSession -> IO () printScreen FrontendSession{..} = do dataDir <- appDataDir tryCreateDir dataDir tryCreateDir $ dataDir "screenshots" utcTime <- getCurrentTime timezone <- getTimeZone utcTime let unspace = map $ \c -> case c of -- prevent the need for backquoting ' ' -> '_' ':' -> '.' _ -> c dateText = unspace $ take 25 $ show $ utcToLocalTime timezone utcTime fileName = dataDir "screenshots" "prtscn" <> dateText <.> "bmp" SDL.Internal.Types.Renderer renderer = srenderer Vect.V2 sw sh <- SDL.get $ SDL.windowSize swindow ptrOut <- SDL.Raw.Video.createRGBSurface 0 sw sh 32 0 0 0 0 surfaceOut <- peek ptrOut void $ SDL.Raw.Video.renderReadPixels renderer nullPtr SDL.Raw.Enum.SDL_PIXELFORMAT_ARGB8888 (SDL.Raw.Types.surfacePixels surfaceOut) (sw * 4) withCString fileName $ \fileNameCString -> void $! SDL.Raw.Video.saveBMP ptrOut fileNameCString SDL.Raw.Video.freeSurface ptrOut -- | Translates modifiers to our own encoding. modTranslate :: SDL.KeyModifier -> K.Modifier modTranslate m = modifierTranslate (SDL.keyModifierLeftCtrl m || SDL.keyModifierRightCtrl m) (SDL.keyModifierLeftShift m || SDL.keyModifierRightShift m) (SDL.keyModifierLeftAlt m || SDL.keyModifierRightAlt m || SDL.keyModifierAltGr m || SDL.keyModifierLeftGUI m || SDL.keyModifierRightGUI m) False keyTranslate :: Bool -> SDL.Keycode -> K.Key keyTranslate shiftPressed n = case n of KeycodeEscape -> K.Esc KeycodeReturn -> K.Return KeycodeBackspace -> K.BackSpace KeycodeTab -> if shiftPressed then K.BackTab else K.Tab KeycodeSpace -> K.Space KeycodeExclaim -> K.Char '!' KeycodeQuoteDbl -> K.Char '"' KeycodeHash -> K.Char '#' KeycodePercent -> K.Char '%' KeycodeDollar -> K.Char '$' KeycodeAmpersand -> K.Char '&' KeycodeQuote -> if shiftPressed then K.Char '"' else K.Char '\'' KeycodeLeftParen -> K.Char '(' KeycodeRightParen -> K.Char ')' KeycodeAsterisk -> K.Char '*' KeycodePlus -> K.Char '+' KeycodeComma -> if shiftPressed then K.Char '<' else K.Char ',' KeycodeMinus -> if shiftPressed then K.Char '_' else K.Char '-' KeycodePeriod -> if shiftPressed then K.Char '>' else K.Char '.' KeycodeSlash -> if shiftPressed then K.Char '?' else K.Char '/' Keycode1 -> if shiftPressed then K.Char '!' else K.Char '1' Keycode2 -> if shiftPressed then K.Char '@' else K.Char '2' Keycode3 -> if shiftPressed then K.Char '#' else K.Char '3' Keycode4 -> if shiftPressed then K.Char '$' else K.Char '4' Keycode5 -> if shiftPressed then K.Char '%' else K.Char '5' Keycode6 -> if shiftPressed then K.Char '^' else K.Char '6' Keycode7 -> if shiftPressed then K.Char '&' else K.Char '7' Keycode8 -> if shiftPressed then K.Char '*' else K.Char '8' Keycode9 -> if shiftPressed then K.Char '(' else K.Char '9' Keycode0 -> if shiftPressed then K.Char ')' else K.Char '0' KeycodeColon -> K.Char ':' KeycodeSemicolon -> if shiftPressed then K.Char ':' else K.Char ';' KeycodeLess -> K.Char '<' KeycodeEquals -> if shiftPressed then K.Char '+' else K.Char '=' KeycodeGreater -> K.Char '>' KeycodeQuestion -> K.Char '?' KeycodeAt -> K.Char '@' KeycodeLeftBracket -> if shiftPressed then K.Char '{' else K.Char '[' KeycodeBackslash -> if shiftPressed then K.Char '|' else K.Char '\\' KeycodeRightBracket -> if shiftPressed then K.Char '}' else K.Char ']' KeycodeCaret -> K.Char '^' KeycodeUnderscore -> K.Char '_' KeycodeBackquote -> if shiftPressed then K.Char '~' else K.Char '`' Keycode 167 -> if shiftPressed then K.Char '~' else K.Char '`' -- on some keyboards the key below ESC is paragraph and its scancode is 167 -- and moreover SDL sometimes gives this code even on normal keyboards KeycodeUp -> K.Up KeycodeDown -> K.Down KeycodeLeft -> K.Left KeycodeRight -> K.Right KeycodeHome -> K.Home KeycodeEnd -> K.End KeycodePageUp -> K.PgUp KeycodePageDown -> K.PgDn KeycodeInsert -> K.Insert KeycodeDelete -> K.Delete KeycodePrintScreen -> K.PrintScreen KeycodeClear -> K.Begin KeycodeKPClear -> K.Begin KeycodeKPDivide -> if shiftPressed then K.Char '?' else K.Char '/' -- KP and normal are merged here KeycodeKPMultiply -> K.Char '*' -- KP and normal are merged here KeycodeKPMinus -> K.Char '-' -- KP and normal are merged here KeycodeKPPlus -> K.Char '+' -- KP and normal are merged here KeycodeKPEnter -> K.Return KeycodeKPEquals -> K.Return -- in case of some funny layouts KeycodeKP1 -> if shiftPressed then K.KP '1' else K.End KeycodeKP2 -> if shiftPressed then K.KP '2' else K.Down KeycodeKP3 -> if shiftPressed then K.KP '3' else K.PgDn KeycodeKP4 -> if shiftPressed then K.KP '4' else K.Left KeycodeKP5 -> if shiftPressed then K.KP '5' else K.Begin KeycodeKP6 -> if shiftPressed then K.KP '6' else K.Right KeycodeKP7 -> if shiftPressed then K.KP '7' else K.Home KeycodeKP8 -> if shiftPressed then K.KP '8' else K.Up KeycodeKP9 -> if shiftPressed then K.KP '9' else K.PgUp KeycodeKP0 -> if shiftPressed then K.KP '0' else K.Insert KeycodeKPPeriod -> K.Char '.' -- dot and comma are merged here KeycodeKPComma -> K.Char '.' -- to sidestep national standards KeycodeF1 -> K.Fun 1 KeycodeF2 -> K.Fun 2 KeycodeF3 -> K.Fun 3 KeycodeF4 -> K.Fun 4 KeycodeF5 -> K.Fun 5 KeycodeF6 -> K.Fun 6 KeycodeF7 -> K.Fun 7 KeycodeF8 -> K.Fun 8 KeycodeF9 -> K.Fun 9 KeycodeF10 -> K.Fun 10 KeycodeF11 -> K.Fun 11 KeycodeF12 -> K.Fun 12 KeycodeLCtrl -> K.DeadKey KeycodeLShift -> K.DeadKey KeycodeLAlt -> K.DeadKey KeycodeLGUI -> K.DeadKey KeycodeRCtrl -> K.DeadKey KeycodeRShift -> K.DeadKey KeycodeRAlt -> K.DeadKey KeycodeRGUI -> K.DeadKey KeycodeMode -> K.DeadKey KeycodeNumLockClear -> K.DeadKey KeycodeUnknown -> K.Unknown "KeycodeUnknown" _ -> let i = fromEnum $ unwrapKeycode n in if | 97 <= i && i <= 122 && shiftPressed -> K.Char $ Char.chr $ i - 32 | 32 <= i && i <= 126 -> K.Char $ Char.chr i | otherwise -> K.Unknown $ show n sDL_ALPHA_OPAQUE :: Word8 sDL_ALPHA_OPAQUE = 255 blackRGBA :: SDL.V4 Word8 blackRGBA = SDL.V4 0 0 0 sDL_ALPHA_OPAQUE -- A third of @colorToRGBA Color.BrBlack@ to compensate for the use -- as background (high area) as opposed to glyphs (usually small area). greyRGBA :: SDL.V4 Word8 greyRGBA = SDL.V4 0x25 0x1F 0x1F sDL_ALPHA_OPAQUE -- This code is sadly duplicated from "Game.LambdaHack.Definition.Color". colorToRGBA :: Color.Color -> SDL.V4 Word8 colorToRGBA Color.Black = blackRGBA colorToRGBA Color.Red = SDL.V4 0xD5 0x05 0x05 sDL_ALPHA_OPAQUE colorToRGBA Color.Green = SDL.V4 0x05 0x9D 0x05 sDL_ALPHA_OPAQUE colorToRGBA Color.Brown = SDL.V4 0xCA 0x4A 0x05 sDL_ALPHA_OPAQUE colorToRGBA Color.Blue = SDL.V4 0x05 0x56 0xF4 sDL_ALPHA_OPAQUE colorToRGBA Color.Magenta = SDL.V4 0xAF 0x0E 0xAF sDL_ALPHA_OPAQUE colorToRGBA Color.Cyan = SDL.V4 0x05 0x96 0x96 sDL_ALPHA_OPAQUE colorToRGBA Color.White = SDL.V4 0xB8 0xBF 0xCB sDL_ALPHA_OPAQUE colorToRGBA Color.AltWhite = SDL.V4 0xC4 0xBE 0xB1 sDL_ALPHA_OPAQUE colorToRGBA Color.BrBlack = SDL.V4 0x6F 0x5F 0x5F sDL_ALPHA_OPAQUE colorToRGBA Color.BrRed = SDL.V4 0xFF 0x55 0x55 sDL_ALPHA_OPAQUE colorToRGBA Color.BrGreen = SDL.V4 0x65 0xF1 0x36 sDL_ALPHA_OPAQUE colorToRGBA Color.BrYellow = SDL.V4 0xEB 0xD6 0x42 sDL_ALPHA_OPAQUE colorToRGBA Color.BrBlue = SDL.V4 0x4D 0x98 0xF4 sDL_ALPHA_OPAQUE colorToRGBA Color.BrMagenta = SDL.V4 0xFF 0x77 0xFF sDL_ALPHA_OPAQUE colorToRGBA Color.BrCyan = SDL.V4 0x52 0xF4 0xE5 sDL_ALPHA_OPAQUE colorToRGBA Color.BrWhite = SDL.V4 0xFF 0xFF 0xFF sDL_ALPHA_OPAQUE LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Frontend/Teletype.hs0000644000000000000000000000572007346545000024406 0ustar0000000000000000-- | Line terminal text frontend based on stdin/stdout, intended for logging -- tests, but may be used on a teletype terminal, or with keyboard and printer. module Game.LambdaHack.Client.UI.Frontend.Teletype ( startup, frontendName ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent.Async import Data.Char (chr, ord) import qualified System.IO as SIO import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.Frontend.Common import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.PointUI import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Content.TileKind (floorSymbol) import qualified Game.LambdaHack.Definition.Color as Color -- No session data maintained by this frontend -- | The name of the frontend. frontendName :: String frontendName = "teletype" -- | Set up the frontend input and output. startup :: ScreenContent -> IO RawFrontend startup coscreen = do rf <- createRawFrontend coscreen (display coscreen) shutdown let storeKeys :: IO () storeKeys = do c <- SIO.getChar -- blocks here, so no polling SIO.hPutStrLn SIO.stderr "" -- prevent next line starting indented let K.KM{..} = keyTranslate c saveKMP rf modifier key (PointUI 0 0) storeKeys SIO.hSetBuffering SIO.stdin SIO.NoBuffering SIO.hSetBuffering SIO.stderr $ SIO.BlockBuffering $ Just $ 2 * rwidth coscreen * rheight coscreen void $ async storeKeys return $! rf shutdown :: IO () shutdown = SIO.hFlush SIO.stdout >> SIO.hFlush SIO.stderr -- | Output to the screen via the frontend. display :: ScreenContent -> SingleFrame -> IO () display coscreen SingleFrame{singleArray} = do let f w l = let acCharRaw = Color.charFromW32 w acChar = if acCharRaw == floorSymbol then '.' else acCharRaw in acChar : l levelChar = chunk $ PointArray.foldrA f [] singleArray chunk [] = [] chunk l = let (ch, r) = splitAt (rwidth coscreen) l in ch : chunk r SIO.hPutStr SIO.stderr $ unlines levelChar SIO.hFlush SIO.stderr keyTranslate :: Char -> K.KM keyTranslate e = (\(key, modifier) -> K.KM modifier key) $ case e of '\ESC' -> (K.Esc, K.NoModifier) '\n' -> (K.Return, K.NoModifier) '\r' -> (K.Return, K.NoModifier) ' ' -> (K.Space, K.NoModifier) '\t' -> (K.Tab, K.NoModifier) c | ord '\^A' <= ord c && ord c <= ord '\^Z' -> -- Alas, only lower-case letters. (K.Char $ chr $ ord c - ord '\^A' + ord 'a', K.Control) -- Movement keys are more important than leader picking, -- so disabling the latter and interpreting the keypad numbers -- as movement: | c `elem` ['1'..'9'] -> (K.KP c, K.NoModifier) | otherwise -> (K.Char c, K.NoModifier) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/HandleHelperM.hs0000644000000000000000000014520707346545000023511 0ustar0000000000000000-- | Helper functions for both inventory management and human commands. module Game.LambdaHack.Client.UI.HandleHelperM ( FailError, showFailError, MError, mergeMError, FailOrCmd, failWith , failSer, failMsg, weaveJust , pointmanCycle, pointmanCycleLevel, partyAfterLeader , pickLeader, doLook, pickLeaderWithPointer , itemOverlay, skillsOverlay, placesFromState, placesOverlay , factionsFromState, factionsOverlay , describeMode, modesOverlay , pickNumber, guardItemSize, lookAtItems, lookAtStash, lookAtPosition , displayOneMenuItem, okxItemLoreInline, okxItemLoreMsg, itemDescOverlays , cycleLore, spoilsBlurb, ppContainerWownW, nxtGameMode #ifdef EXPOSE_INTERNAL -- * Internal operations , itemOverlayFromState, lookAtTile, lookAtActors, guardItemVerbs #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Applicative import qualified Data.Char as Char import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Map.Strict as M import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.ItemDescription import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.HighScore as HighScore import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.FactionKind as FK import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.ModeKind as MK import qualified Game.LambdaHack.Content.PlaceKind as PK import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs -- | Message describing the cause of failure of human command. newtype FailError = FailError {failError :: Text} deriving (Show, Eq) showFailError :: FailError -> Text showFailError (FailError err) = "*" <> err <> "*" type MError = Maybe FailError mergeMError :: MError -> MError -> MError mergeMError Nothing Nothing = Nothing mergeMError merr1@Just{} Nothing = merr1 mergeMError Nothing merr2@Just{} = merr2 mergeMError (Just err1) (Just err2) = Just $ FailError $ failError err1 <+> "and" <+> failError err2 type FailOrCmd a = Either FailError a failWith :: MonadClientUI m => Text -> m (FailOrCmd a) failWith err = assert (not $ T.null err) $ return $ Left $ FailError err failSer :: MonadClientUI m => ReqFailure -> m (FailOrCmd a) failSer = failWith . showReqFailure failMsg :: MonadClientUI m => Text -> m MError failMsg err = assert (not $ T.null err) $ return $ Just $ FailError err weaveJust :: FailOrCmd a -> Either MError a weaveJust (Left ferr) = Left $ Just ferr weaveJust (Right a) = Right a -- | Switches current pointman to the next on the level, if any, wrapping. pointmanCycleLevel :: MonadClientUI m => ActorId -> Bool -> Direction -> m MError pointmanCycleLevel leader verbose direction = do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD lidV <- viewedLevelUI body <- getsState $ getActorBody leader hs <- partyAfterLeader leader let banned = bannedPointmanSwitchBetweenLevels fact hsSort = case direction of Forward -> hs Backward -> reverse hs case filter (\(_, b, _) -> blid b == lidV) hsSort of _ | 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)) () return Nothing -- | Switches current pointman to the previous in the whole dungeon, wrapping. pointmanCycle :: MonadClientUI m => ActorId -> Bool -> Direction -> m MError pointmanCycle leader verbose direction = do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD hs <- partyAfterLeader leader let banned = bannedPointmanSwitchBetweenLevels fact hsSort = case direction of Forward -> hs Backward -> reverse hs case hsSort of _ | banned -> failMsg $ showReqFailure NoChangeDunLeader [] -> failMsg "no other member in the party" (np, b, _) : _ -> do success <- pickLeader verbose np let !_A = assert (success `blame` "same leader" `swith` (leader, np, b)) () return Nothing partyAfterLeader :: MonadClientUI m => ActorId -> m [(ActorId, Actor, ActorUI)] partyAfterLeader leader = do side <- getsClient sside sactorUI <- getsSession sactorUI allOurs <- getsState $ fidActorNotProjGlobalAssocs side -- not only on level let allOursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) allOurs hs = sortOn keySelected allOursUI i = fromMaybe (-1) $ findIndex (\(aid, _, _) -> aid == leader) hs (lt, gt) = (take i hs, drop (i + 1) hs) return $! gt ++ lt -- | Select a faction leader. False, if nothing to do. pickLeader :: MonadClientUI m => Bool -> ActorId -> m Bool pickLeader verbose aid = do mleader <- getsClient sleader if mleader == Just aid then return False -- already picked else do body <- getsState $ getActorBody aid bodyUI <- getsSession $ getActorUI aid let !_A = assert (not (bproj body) `blame` "projectile chosen as the pointman" `swith` (aid, body)) () -- Even if it's already the leader, give his proper name, not 'you'. let subject = partActor bodyUI when verbose $ msgAdd MsgPointmanSwap $ makeSentence [subject, "picked as a pointman"] -- Update client state. updateClientLeader aid -- Move the xhair, if active, to the new level. modifySession $ \sess -> sess {saimMode = (\aimMode -> aimMode {aimLevelId = blid body}) <$> saimMode sess} -- Inform about items, etc. saimMode <- getsSession saimMode when verbose $ if isJust saimMode then doLook else do (itemsBlurb, _) <- lookAtItems True (bpos body) (blid body) (Just aid) Nothing stashBlurb <- lookAtStash (bpos body) (blid body) msgAdd MsgAtFeetMinor $ stashBlurb <+> itemsBlurb return True -- | Perform look around in the current position of the xhair. -- Does nothing outside aiming mode. doLook :: MonadClientUI m => m () doLook = do saimMode <- getsSession saimMode case saimMode of Just aimMode -> do let lidV = aimLevelId aimMode mxhairPos <- mxhairToPos xhairPos <- xhairToPos blurb <- lookAtPosition xhairPos lidV itemSel <- getsSession sitemSel mleader <- getsClient sleader outOfRangeBlurb <- case (itemSel, mxhairPos, mleader) of (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 then return [] else do itemFull <- getsState $ itemToFull iid let arItem = aspectRecordFull itemFull return [ (MsgPromptGeneric, "This position is out of range when flinging the selected item.") | 1 + IA.totalRange arItem (itemKind itemFull) < chessDist (bpos b) pos ] _ -> return [] mapM_ (uncurry msgAdd) $ blurb ++ outOfRangeBlurb _ -> return () pickLeaderWithPointer :: MonadClientUI m => ActorId -> m MError pickLeaderWithPointer leader = do CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui lidV <- viewedLevelUI side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD arena <- getArenaUI sactorUI <- getsSession sactorUI ours <- getsState $ filter (not . bproj . snd) . actorAssocs (== side) lidV let oursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) ours viewed = sortOn keySelected oursUI banned = bannedPointmanSwitchBetweenLevels fact pick (aid, b) = if blid b /= arena && banned then failMsg $ showReqFailure NoChangeDunLeader else do void $ pickLeader True aid return Nothing pUI <- getsSession spointer let p@(Point px py) = squareToMap $ uiToSquare pUI -- Pick even if no space in status line for the actor's symbol. if | py == rheight - 2 && px == 0 -> pointmanCycle leader True Forward | py == rheight - 2 -> case drop (px - 1) viewed of [] -> return Nothing -- relaxed, due to subtleties of display of selected actors (aid, b, _) : _ -> pick (aid, b) | otherwise -> case find (\(_, b, _) -> bpos b == p) oursUI of Nothing -> failMsg "not pointing at an actor" Just (aid, b, _) -> pick (aid, b) itemOverlayFromState :: LevelId -> [(ItemId, ItemQuant)] -> Bool -> CCUI -> FactionId -> DiscoveryBenefit -> FontSetup -> State -> OKX itemOverlayFromState arena iids displayRanged sccui side discoBenefit FontSetup{..} s = let CCUI{coscreen=ScreenContent{rwidth}} = sccui localTime = getLocalTime arena s itemToF = flip itemToFull s factionD = sfactionD s attrCursor = Color.defAttr {Color.bg = Color.HighlightNoneCursor} markEqp periodic k ncha = if | periodic -> '"' -- if equipped, no charges | ncha == 0 -> '-' -- no charges left | k > ncha -> '~' -- not all charges left | otherwise -> '+' pr :: MenuSlot -> (ItemId, ItemQuant) -> (AttrString, AttrString, KeyOrSlot) pr c (iid, kit@(k, _)) = let itemFull = itemToF iid arItem = aspectRecordFull itemFull colorSymbol = if IA.checkFlag Ability.Condition arItem then viewItemBenefitColored discoBenefit iid itemFull else viewItem itemFull phrase = makePhrase [partItemWsRanged rwidth side factionD displayRanged DetailMedium 4 k localTime itemFull kit] ncha = ncharges localTime kit periodic = IA.checkFlag Ability.Periodic arItem !cLab = Color.AttrChar { acAttr = attrCursor , acChar = markEqp periodic k ncha } asLab = [Color.attrCharToW32 cLab] ++ [Color.spaceAttrW32 | isSquareFont propFont] ++ [colorSymbol] !tDesc = " " <> phrase in (asLab, textToAS tDesc, Right c) l = zipWith pr natSlots iids in labDescOKX squareFont propFont l -- | Extract whole-dungeon statistics for each place kind, -- counting the number of occurrences of each type of -- `Game.LambdaHack.Content.PlaceKind.PlaceEntry` -- for the given place kind and gathering the set of levels -- on which any entry for that place kind can be found. placesFromState :: ContentData PK.PlaceKind -> Bool -> State -> EM.EnumMap (ContentId PK.PlaceKind) (ES.EnumSet LevelId, Int, Int, Int) placesFromState coplace sexposePlaces s = let addEntries (!es1, !nEntries1, !nArounds1, !nExists1) (!es2, !nEntries2, !nArounds2, !nExists2) = let !es = ES.union es1 es2 !nEntries = nEntries1 + nEntries2 !nArounds = nArounds1 + nArounds2 !nExists = nExists1 + nExists2 in (es, nEntries, nArounds, nExists) placesFromLevel :: (LevelId, Level) -> EM.EnumMap (ContentId PK.PlaceKind) (ES.EnumSet LevelId, Int, Int, Int) placesFromLevel (!lid, Level{lentry}) = let f (PK.PEntry pk) em = EM.insertWith addEntries pk (ES.singleton lid, 1, 0, 0) em f (PK.PAround pk) em = EM.insertWith addEntries pk (ES.singleton lid, 0, 1, 0) em f (PK.PExists pk) em = EM.insertWith addEntries pk (ES.singleton lid, 0, 0, 1) em in EM.foldr' f EM.empty lentry -- go through place entrances and depending on the place -- add an entry for it, whether Entry/Around/Exists, -- the effect being we're counting #s of each type insertZeros !em !pk _ = EM.insert pk (ES.empty, 0, 0, 0) em -- The initial places are overwritten except for those -- that have no entries in the dungeon at all, -- and in `sexposePlaces` debug mode these will be shown even though -- the stats will be zeros (which is a valuable warning!). initialPlaces | not sexposePlaces = EM.empty | otherwise = ofoldlWithKey' coplace insertZeros EM.empty in EM.unionWith addEntries initialPlaces (EM.unionsWith addEntries $ map placesFromLevel $ EM.assocs $ sdungeon s) -- gather per-place-kind statistics for each level, -- then aggregate them over all levels, remembering that the place -- appeared on the given level (but not how man times) -- TODO: if faction not known, it's info should not be updated -- by the server. But let's wait until server sends general state diffs -- and then block diffs that don't apply, because faction is missing. factionsFromState :: ItemRoles -> State -> [(FactionId, Faction)] factionsFromState (ItemRoles itemRoles) s = let seenTrunks = ES.toList $ itemRoles EM.! STrunk trunkBelongs fid iid = jfid (getItemBody iid s) == Just fid factionSeen (fid, fact) = not (EM.null (gvictims fact)) -- shortcut || any (trunkBelongs fid) seenTrunks in filter factionSeen $ EM.assocs $ sfactionD s itemOverlay :: MonadClientUI m => [(ItemId, ItemQuant)] -> ItemDialogMode -> m OKX itemOverlay iids dmode = do sccui <- getsSession sccui side <- getsClient sside arena <- getArenaUI discoBenefit <- getsClient sdiscoBenefit fontSetup <- getFontSetup let displayRanged = dmode `elem` [ MStore CGround, MStore CEqp, MStore CStash , MOwned, MLore SItem, MLore SBlast ] okx <- getsState $ itemOverlayFromState arena iids displayRanged sccui side discoBenefit fontSetup return $! okx skillsOverlay :: MonadClientUI m => ActorId -> m OKX skillsOverlay aid = do b <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid FontSetup{..} <- getFontSetup let prSlot :: MenuSlot -> Ability.Skill -> ((AttrLine, (Int, AttrLine), (Int, AttrLine)), KYX) prSlot c skill = let skName = " " <> skillName skill attrCursor = Color.defAttr {Color.bg = Color.HighlightNoneCursor} labAc = Color.AttrChar { acAttr = attrCursor , acChar = '+' } lab = attrStringToAL [Color.attrCharToW32 labAc] labLen = textSize squareFont $ attrLine lab indentation = if isSquareFont propFont then 52 else 26 valueText = skillToDecorator skill b $ Ability.getSk skill actorMaxSk triple = ( lab , (labLen, textToAL skName) , (indentation, textToAL valueText) ) lenButton = 26 + T.length valueText in (triple, (Right c, ( PointUI 0 (fromEnum c) , ButtonWidth propFont lenButton ))) (ts, kxs) = unzip $ zipWith prSlot natSlots skillsInDisplayOrder (skLab, skDescr, skValue) = unzip3 ts skillLab = EM.singleton squareFont $ offsetOverlay skLab skillDescr = EM.singleton propFont $ offsetOverlayX skDescr skillValue = EM.singleton monoFont $ offsetOverlayX skValue return (EM.unionsWith (++) [skillLab, skillDescr, skillValue], kxs) placesOverlay :: MonadClientUI m => m OKX placesOverlay = do COps{coplace} <- getsState scops soptions <- getsClient soptions FontSetup{..} <- getFontSetup places <- getsState $ placesFromState coplace (sexposePlaces soptions) let prSlot :: MenuSlot -> (ContentId PK.PlaceKind, (ES.EnumSet LevelId, Int, Int, Int)) -> (AttrString, AttrString, KeyOrSlot) prSlot c (pk, (es, _, _, _)) = let name = PK.pname $ okind coplace pk labChar = if ES.null es then '-' else '+' attrCursor = Color.defAttr {Color.bg = Color.HighlightNoneCursor} labAc = Color.AttrChar { acAttr = attrCursor , acChar = labChar } -- Bang required to free @places@ as you go. !asLab = [Color.attrCharToW32 labAc] !tDesc = " " <> name <+> if ES.null es then "" else "(" <> makePhrase [MU.CarWs (ES.size es) "level"] <> ")" in (asLab, textToAS tDesc, Right c) l = zipWith prSlot natSlots $ EM.assocs places return $! labDescOKX squareFont propFont l factionsOverlay :: MonadClientUI m => m OKX factionsOverlay = do FontSetup{..} <- getFontSetup sroles <- getsSession sroles factions <- getsState $ factionsFromState sroles let prSlot :: MenuSlot -> (FactionId, Faction) -> (AttrString, AttrString, KeyOrSlot) prSlot c (_, fact) = let name = FK.fname $ gkind fact -- we ignore "Controlled", etc. gameOver = isJust $ gquit fact labChar = if gameOver then '-' else '+' attrCursor = Color.defAttr {Color.bg = Color.HighlightNoneCursor} labAc = Color.AttrChar { acAttr = attrCursor , acChar = labChar } !asLab = [Color.attrCharToW32 labAc] !tDesc = " " <> name <+> case gquit fact of Just Status{stOutcome} | not $ isHorrorFact fact -> "(" <> FK.nameOutcomePast stOutcome <> ")" _ -> "" in (asLab, textToAS tDesc, Right c) l = zipWith prSlot natSlots factions return $! labDescOKX squareFont propFont l modesOverlay :: MonadClientUI m => m OKX modesOverlay = do COps{comode} <- getsState scops FontSetup{..} <- getFontSetup svictories <- getsSession svictories nxtChal <- getsClient snxtChal -- mark victories only for current difficulty let f !acc _p !i !a = (i, a) : acc campaignModes = ofoldlGroup' comode MK.CAMPAIGN_SCENARIO f [] prSlot :: MenuSlot -> (ContentId MK.ModeKind, MK.ModeKind) -> (AttrString, AttrString, KeyOrSlot) prSlot c (gameModeId, gameMode) = let modeName = MK.mname gameMode victories = case EM.lookup gameModeId svictories of Nothing -> 0 Just cm -> fromMaybe 0 (M.lookup nxtChal cm) labChar = if victories > 0 then '-' else '+' attrCursor = Color.defAttr {Color.bg = Color.HighlightNoneCursor} labAc = Color.AttrChar { acAttr = attrCursor , acChar = labChar } !asLab = [Color.attrCharToW32 labAc] !tDesc = " " <> modeName in (asLab, textToAS tDesc, Right c) l = zipWith prSlot natSlots campaignModes return $! labDescOKX squareFont propFont l describeMode :: MonadClientUI m => Bool -> ContentId MK.ModeKind -> m (EM.EnumMap DisplayFont Overlay) describeMode addTitle gameModeId = do COps{comode} <- getsState scops CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui FontSetup{..} <- getFontSetup scoreDict <- getsState shigh scampings <- getsSession scampings srestarts <- getsSession srestarts side <- getsClient sside total <- getsState $ snd . calculateTotal side dungeonTotal <- getsState sgold let screensaverBlurb = "This is one of the screensaver scenarios, not available from the main menu, with all factions controlled by AI. Feel free to take over or relinquish control at any moment, but to register a legitimate high score, choose a standard scenario instead.\n" let gameMode = okind comode gameModeId duplicateEOL '\n' = "\n\n" duplicateEOL c = T.singleton c sections = [ ( textFgToAS Color.BrGreen "The story so far:" , T.concatMap duplicateEOL (MK.mdesc gameMode) ) , ( textFgToAS Color.cMeta "Rules of the game:" , MK.mrules gameMode ) , ( textFgToAS Color.BrCyan "Running commentary:" , T.concatMap duplicateEOL (if MK.mattract gameMode then screensaverBlurb <> MK.mreason gameMode else MK.mreason gameMode) ) , ( textFgToAS Color.cGreed "Hints, not needed unless stuck:" , T.concatMap duplicateEOL (MK.mhint gameMode) ) ] renderSection :: (AttrString, Text) -> Maybe [(DisplayFont, AttrString)] renderSection (header, desc) = if T.null desc then Nothing else Just [(monoFont, header), (propFont, textToAS desc)] survivingHow = if | total == 0 -> "(barely)" | total < dungeonTotal `div` 2 -> "(so far)" | otherwise -> "" title = if addTitle then "\nYou are" <+> survivingHow <+> "surviving the '" <> MK.mname gameMode <> "' adventure.\n" else "" blurb = map (second $ splitAttrString (rwidth - 2) (rwidth - 2)) $ (propFont, textToAS (title <> "\n")) : intercalate [(monoFont, textToAS "\n")] (mapMaybe renderSection sections) -- Colour is used to delimit the section when displayed in one -- column, when using square fonts only. blurbEnd = map (second $ splitAttrString (rwidth - 2) (rwidth - 2)) $ ( propFont , textFgToAS Color.Brown "\nThis adventure's endings experienced so far:\n\n" ) : if null sectionsEndAS then [(monoFont, textToAS "*none*")] else sectionsEndAS sectionsEndAS = intercalate [(monoFont, textToAS "\n")] (mapMaybe renderSection sectionsEnd) sectionsEnd = map outcomeSection [minBound..maxBound] outcomeSection :: FK.Outcome -> (AttrString, Text) outcomeSection outcome = ( renderOutcome outcome , if not (outcomeSeen outcome) then "" -- a possible spoiler and lack of sense of progression else T.concatMap duplicateEOL $ fromMaybe "" $ lookup outcome $ MK.mendMsg gameMode ++ endMsgDefault -- left-biased ) -- These are not added to @mendMsg@, because they only fit here. endMsgDefault = [ (FK.Restart, "No shame there is in noble defeat and there is honour in perseverance. Sometimes there are ways and places to turn rout into victory.") , (FK.Camping, "Don't fear to take breaks. While you move, others move, even on distant floors, but while you stay still, the world stays still.") ] scoreRecords = maybe [] HighScore.unTable $ EM.lookup gameModeId scoreDict -- This doesn't use @svictories@, but high scores, because high scores -- are more persistent and granular (per-outcome). OTOH, @svictories@ -- are per-challenge, which is important in other cases. -- @Camping@ and @Restart@ are fine to be less persistent. outcomeSeen :: FK.Outcome -> Bool outcomeSeen outcome = case outcome of FK.Camping -> gameModeId `ES.member` scampings FK.Restart -> gameModeId `ES.member` srestarts _ -> outcome `elem` map (stOutcome . HighScore.getStatus) scoreRecords -- Camping not taken into account. lastOutcome :: FK.Outcome lastOutcome = if null scoreRecords then FK.Restart -- only if nothing else else stOutcome . HighScore.getStatus $ maximumBy (comparing HighScore.getDate) scoreRecords renderOutcome :: FK.Outcome -> AttrString renderOutcome outcome = let color | outcome `elem` FK.deafeatOutcomes = Color.cVeryBadEvent | outcome `elem` FK.victoryOutcomes = Color.cVeryGoodEvent | otherwise = Color.cNeutralEvent lastRemark | outcome /= lastOutcome = "" | outcome `elem` FK.deafeatOutcomes = "(last suffered ending)" | outcome `elem` FK.victoryOutcomes = "(last achieved ending)" | otherwise = "(last seen ending)" in textToAS "Game over message when" <+:> (textFgToAS color (T.toTitle $ FK.nameOutcomePast outcome) <+:> textToAS lastRemark) <> textToAS ":" return $! if isSquareFont propFont then EM.singleton squareFont -- single column, single font $ xtranslateOverlay 2 $ offsetOverlay $ concatMap snd $ blurb ++ blurbEnd else EM.unionWith (++) (EM.map (xtranslateOverlay 1) $ attrLinesToFontMap blurb) (EM.map (xtranslateOverlay $ rwidth + 1) $ attrLinesToFontMap blurbEnd) pickNumber :: MonadClientUI m => Bool -> Int -> m (Either MError Int) pickNumber askNumber kAll = assert (kAll >= 1) $ do let shownKeys = [ K.returnKM, K.spaceKM, K.mkChar '+', K.mkChar '-' , K.backspaceKM, K.escKM ] frontKeyKeys = shownKeys ++ map K.mkChar ['0'..'9'] gatherNumber kCur = assert (1 <= kCur && kCur <= kAll) $ do let kprompt = "Choose number:" <+> tshow kCur msgAdd MsgPromptGeneric kprompt sli <- reportToSlideshow shownKeys ekkm <- displayChoiceScreen "" ColorFull False sli frontKeyKeys case ekkm of Left kkm -> case K.key kkm of K.Char '+' -> gatherNumber $ if kCur + 1 > kAll then 1 else kCur + 1 K.Char '-' -> gatherNumber $ if kCur - 1 < 1 then kAll else kCur - 1 K.Char l | kCur * 10 + Char.digitToInt l > kAll -> gatherNumber $ if Char.digitToInt l == 0 then kAll else min kAll (Char.digitToInt l) K.Char l -> gatherNumber $ kCur * 10 + Char.digitToInt l K.BackSpace -> gatherNumber $ max 1 (kCur `div` 10) K.Return -> return $ Right kCur K.Esc -> weaveJust <$> failWith "never mind" K.Space -> return $ Left Nothing _ -> error $ "unexpected key" `showFailure` kkm Right slot -> error $ "unexpected menu slot" `showFailure` slot if kAll == 1 || not askNumber then return $ Right kAll else do res <- gatherNumber kAll case res of Right k | k <= 0 -> error $ "" `showFailure` (res, kAll) _ -> return res -- | Produces a textual description of the tile at a position. lookAtTile :: MonadClientUI m => Bool -- ^ can be seen right now? -> Point -- ^ position to describe -> LevelId -- ^ level the position is at -> Maybe ActorId -- ^ the actor that looks -> Maybe MU.Person -- ^ grammatical person of the item(s), if any -> m (Text, Text, [(Int, MU.Part)]) lookAtTile canSee p lidV maid mperson = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui cops@COps{cotile, coplace} <- getsState scops side <- getsClient sside factionD <- getsState sfactionD mb <- getsState $ \s -> flip getActorBody s <$> maid lvl <- getLevel lidV saimMode <- getsSession saimMode embeds <- getsState $ getEmbedBag lidV p itemToF <- getsState $ flip itemToFull seps <- getsClient seps localTime <- getsState $ getLocalTime lidV getKind <- getsState $ flip getIidKind let inhabitants = posToAidsLvl p lvl detail = maybe DetailAll detailLevel saimMode aims = isJust $ (\b -> makeLine False b p seps cops lvl) =<< mb tkid = lvl `at` p tile = okind cotile tkid vis | TK.tname tile == "unknown space" = "that is" | not (null inhabitants) && (bpos <$> mb) /= Just p = "the terrain here is" | not canSee = "you remember" | not aims = "you are aware of" -- walkable path a proxy for in LOS | otherwise = "you see" vperson = case mperson of Nothing -> vis Just MU.Sg1st -> error "an item speaks in first person" Just MU.Sg3rd -> "It is laying on" Just MU.PlEtc -> "They lay on" tilePart = MU.AW $ MU.Text $ TK.tname tile entrySentence pk blurb = makeSentence [blurb, MU.Text $ PK.pname $ okind coplace pk] placeBlurb = case EM.lookup p $ lentry lvl of Nothing -> "" Just (PK.PEntry pk) -> entrySentence pk "it is an entrance to" Just (PK.PAround pk) -> entrySentence pk "it surrounds" Just (PK.PExists _) -> "" embedLook (iid, kit@(k, _)) = let itemFull = itemToF iid nWs = partItemWsDetail detail rwidth side factionD k localTime itemFull kit in (k, nWs) embedKindList = map (\(iid, kit) -> (getKind iid, (iid, kit))) (EM.assocs embeds) embedList = map embedLook $ sortEmbeds cops tkid embedKindList return (makeSentence [vperson, tilePart], placeBlurb, embedList) -- | Produces a textual description of actors at a position. lookAtActors :: MonadClientUI m => Point -- ^ position to describe -> LevelId -- ^ level the position is at -> m (Text, Maybe (MU.Part, Bool), Text) lookAtActors p lidV = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui side <- getsClient sside inhabitants <- getsState $ posToAidAssocs p lidV factionD <- getsState sfactionD localTime <- getsState $ getLocalTime lidV saimMode <- getsSession saimMode let detail = maybe DetailAll detailLevel saimMode case inhabitants of [] -> return ("", Nothing, "") (aid, body) : rest -> do actorPronoun <- partPronounLeader aid itemFull <- getsState $ itemToFull $ btrunk body guardVerbs <- getsState $ guardItemVerbs body subjects <- mapM (partActorLeader . fst) inhabitants let bfact = factionD EM.! bfid body -- No "a" prefix even if singular and inanimate, to distinguish -- from items lying on the floor (and to simplify code). (subject, person) = squashedWWandW subjects resideVerb = case bwatch body of WWatch -> "be here" WWait 0 -> "idle here" WWait _ -> "brace for impact" WSleep -> "sleep here" WWake -> "be waking up" flyVerb | bproj body = "zip through here" | isJust $ btrajectory body = "move through here" | otherwise = resideVerb verbs = flyVerb : guardVerbs projDesc | not (bproj body) || detail < DetailAll = "" | otherwise = let kit = beqp body EM.! btrunk body ps = [partItemMediumAW rwidth side factionD localTime itemFull kit] tailWords = tail . T.words . makePhrase in if tailWords ps == tailWords subjects then "" else makeSentence $ "this is" : ps factDesc = case jfid $ itemBase itemFull of Just tfid | tfid /= bfid body -> let dominatedBy = if bfid body == side then "us" else gname bfact tfact = factionD EM.! tfid in "Originally of" <+> gname tfact <> ", now fighting for" <+> dominatedBy <> "." _ | detail < DetailAll -> "" -- 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 then "" else IK.idesc $ itemKind itemFull -- If many different actors, only list names. sameTrunks = all (\(_, b) -> btrunk b == btrunk body) rest desc = wrapInParens $ projDesc <+> factDesc <+> idesc onlyIs = bwatch body == WWatch && null guardVerbs allBlurb = makeSentence [MU.SubjectVVxV "and" person MU.Yes subject verbs] headBlurb = makeSentence [MU.SubjectVVxV "and" MU.Sg3rd MU.Yes (head subjects) verbs] andProjectiles = case subjects of _ : projs@(_ : _) -> let (subjectProjs, personProjs) = squashedWWandW projs in makeSentence [MU.SubjectVerb personProjs MU.Yes subjectProjs "can be seen"] _ -> "" actorAlive = bhp body >= 0 mactorPronounAlive = if bproj body then Nothing else Just (actorPronoun, actorAlive) return $! if | not actorAlive && not (bproj body) -> ( makeSentence (MU.SubjectVerbSg (head subjects) "lie here" : if null guardVerbs then [] else [ MU.SubjectVVxV "and" MU.Sg3rd MU.No "and" guardVerbs , "any more" ]) , mactorPronounAlive , wrapInParens desc <+> andProjectiles ) | sameTrunks -> -- only non-proj or several similar projectiles ( allBlurb , mactorPronounAlive , desc ) | not (bproj body) && onlyIs -> ( headBlurb , mactorPronounAlive , desc <+> andProjectiles ) | not (bproj body) -> ( makeSentence [subject, "can be seen"] <+> headBlurb , mactorPronounAlive , desc ) | otherwise -> assert (bproj body && not (null rest)) ( makeSentence [subject, "can be seen"] , Nothing , "" ) guardItemVerbs :: Actor -> State -> [MU.Part] guardItemVerbs body s = -- We only hint while, in reality, currently the client knows -- all the items in eqp of the foe. But we may remove the knowledge -- in the future and, anyway, it would require a dedicated -- UI mode beyond a couple of items per actor. let itemsSize = guardItemSize body s belongingsVerbs | itemsSize == 1 = ["fondle a trinket"] | itemsSize > 1 = ["haul a hoard"] | otherwise = [] in if bproj body then [] else belongingsVerbs guardItemSize :: Actor -> State -> Int guardItemSize body s = let toReport iid = let itemKind = getIidKind iid s in fromMaybe 0 (lookup IK.UNREPORTED_INVENTORY (IK.ifreq itemKind)) <= 0 in length $ filter toReport $ EM.keys (beqp body) -- | Produces a textual description of items at a position. lookAtItems :: MonadClientUI m => Bool -- ^ can be seen right now? -> Point -- ^ position to describe -> LevelId -- ^ level the position is at -> Maybe ActorId -- ^ the actor that looks -> Maybe (MU.Part, Bool) -- ^ pronoun for the big actor at the position, if any, -- and whether the big actor is alive -> m (Text, Maybe MU.Person) lookAtItems canSee p lidV maid mactorPronounAlive = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui side <- getsClient sside itemToF <- getsState $ flip itemToFull mb <- getsState $ \s -> flip getActorBody s <$> maid -- Not using @viewedLevelUI@, because @aid@ may be temporarily not a leader. saimMode <- getsSession saimMode let standingOn = Just p == (bpos <$> mb) && Just lidV == (blid <$> mb) -- In exploration mode the detail level depends on whether the actor -- 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 else DetailAll detail = maybe detailExploration detailLevel saimMode localTime <- getsState $ getLocalTime lidV is <- getsState $ getFloorBag lidV p factionD <- getsState sfactionD globalTime <- getsState stime getKind <- getsState $ flip getIidKindId mLeader <- case maid of Just aid | standingOn -> do leaderPronoun <- partPronounLeader aid return $ Just (leaderPronoun, (bhp <$> mb) >= Just 0) _ -> return Nothing let mactorPronounAliveLeader = mactorPronounAlive <|> mLeader (subject, verb) <- case mactorPronounAliveLeader of Just (actorPronoun, actorAlive) -> return (actorPronoun, if actorAlive then "stand over" else "fall over") Nothing -> case maid of Just aid -> do subjectAid <- partActorLeader aid return (subjectAid, if canSee then "notice" else "remember") Nothing -> return ("one", if canSee then "can see" else "may remember") let nWs (iid, kit@(k, _)) = 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) iis -> (MU.WWandW $ map nWs $ sortOn (getKind . fst) iis, MU.PlEtc) -- Here @squashedWWandW@ is not needed, because identical items at the same -- position are already merged in the floor item bag and multiple identical -- messages concerning different positions are merged with -- to distinguish from a stack of items at a single position. return ( if EM.null is || globalTime == timeZero then "" else makeSentence [MU.SubjectVerbSg subject (MU.Text verb), object] , if isNothing mactorPronounAlive then Just person else Nothing ) lookAtStash :: MonadClientUI m => Point -> LevelId -> m Text lookAtStash p lidV = do side <- getsClient sside factionD <- getsState sfactionD let locateStash (fid, fact) = case gstash fact of Just (lid, pos) | lid == lidV && pos == p -> Just $ if fid == side then "Here is the shared inventory stash of your team." else gname fact <+> "set up their shared inventory stash here." _ -> Nothing return $! T.intercalate " " $ mapMaybe locateStash $ EM.assocs factionD -- | Produces a textual description of everything at the requested -- level's position. lookAtPosition :: MonadClientUI m => Point -> LevelId -> m [(MsgClassShow, Text)] lookAtPosition p lidV = do COps{cotile} <- getsState scops side <- getsClient sside per <- getPerFid lidV let canSee = ES.member p (totalVisible per) (actorsBlurb, mactorPronounAlive, actorsDesc) <- lookAtActors p lidV mleader <- getsClient sleader (itemsBlurb, mperson) <- lookAtItems canSee p lidV mleader mactorPronounAlive let tperson = if T.null itemsBlurb then Nothing else mperson (tileBlurb, placeBlurb, embedsList) <- lookAtTile canSee p lidV mleader tperson inhabitants <- getsState $ posToAidAssocs p lidV let actorMsgClass = if (bfid . snd <$> inhabitants) == [side] then MsgPromptGeneric -- our single proj or non-proj; tame else MsgPromptActors stashBlurb <- lookAtStash p lidV lvl@Level{lsmell, ltime} <- getLevel lidV saimMode <- getsSession saimMode let detail = maybe DetailAll detailLevel saimMode smellBlurb = case EM.lookup p lsmell of Just sml | sml > ltime -> let Delta t = smellTimeout `timeDeltaSubtract` (sml `timeDeltaToFrom` ltime) seconds = t `timeFitUp` timeSecond in "A smelly body passed here around" <+> tshow seconds <> "s ago." _ -> "" embeds <- getsState $ getEmbedBag lidV p getKind <- getsState $ flip getIidKind let ppEmbedName :: (Int, MU.Part) -> Text ppEmbedName (k, part) = let verb = if k == 1 then "is" else "are" in makeSentence ["There", verb, part] embedKindList = map (\(iid, kit) -> (getKind iid, (iid, kit))) (EM.assocs embeds) feats = TK.tfeature $ okind cotile $ lvl `at` p tileActions = mapMaybe (parseTileAction False False embedKindList) feats isEmbedAction EmbedAction{} = True isEmbedAction _ = False embedVerb = [ "activated" | any isEmbedAction tileActions && any (\(itemKind, _) -> not $ null $ IK.ieffects itemKind) embedKindList ] isToAction ToAction{} = True isToAction _ = False isWithAction WithAction{} = True isWithAction _ = False isEmptyWithAction (WithAction [] _) = True isEmptyWithAction _ = False alterVerb | any isEmptyWithAction tileActions = ["very easily modified"] | any isToAction tileActions = ["easily modified"] | any isWithAction tileActions = ["potentially modified"] | otherwise = [] verbs = embedVerb ++ alterVerb alterBlurb = if null verbs then "" else makeSentence ["can be", MU.WWandW verbs] toolFromAction (WithAction grps _) = Just grps toolFromAction _ = Nothing toolsToAlterWith = mapMaybe toolFromAction tileActions tItems = describeToolsAlternative toolsToAlterWith transformBlurb = if T.null tItems then "" 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 || T.null stashBlurb && T.null actorsDesc || T.null smellBlurb && T.null itemsBlurb || null embedsList && T.null modifyBlurb then "" else "\n" ms = [ (MsgPromptAction, stashBlurb) , (actorMsgClass, actorsBlurb) , (MsgPromptGeneric, actorsDesc <> midEOL) ] ++ [(MsgPromptGeneric, smellBlurb) | detail >= DetailHigh] ++ [(MsgPromptItems, itemsBlurb <> midEOL)] ++ [(MsgPromptFocus, tileBlurb) | detail >= DetailHigh || detail == DetailMedium && not (null embedsList)] ++ [(MsgPromptGeneric, placeBlurb) | detail >= DetailHigh] ++ 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] return $! if all (T.null . snd) ms && detail > DetailLow then [(MsgPromptFocus, tileBlurb)] else ms displayOneMenuItem :: MonadClientUI m => (MenuSlot -> m OKX) -> [K.KM] -> Int -> MenuSlot -> m K.KM displayOneMenuItem renderOneItem extraKeys slotBound slot = do CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui let keys = [K.spaceKM, K.escKM] ++ [K.upKM | fromEnum slot > 0] ++ [K.downKM | fromEnum slot < slotBound] ++ extraKeys okx <- renderOneItem slot slides <- overlayToSlideshow (rheight - 2) keys okx km <- getConfirms ColorFull keys slides case K.key km of K.Up -> displayOneMenuItem renderOneItem extraKeys slotBound $ pred slot K.Down -> displayOneMenuItem renderOneItem extraKeys slotBound $ succ slot _ -> return km okxItemLoreInline :: MonadClientUI m => (ItemId -> ItemFull -> Int -> Text) -> Int -> ItemDialogMode -> [(ItemId, ItemQuant)] -> Int -> MenuSlot -> m OKX okxItemLoreInline promptFun meleeSkill dmode iids widthRaw slot = do FontSetup{..} <- getFontSetup let (iid, kit@(k, _)) = iids !! fromEnum slot -- Some prop fonts are wider than mono (e.g., in dejavuBold font set), -- so the width in these artificial texts full of digits and strange -- characters needs to be smaller than @rwidth - 2@ that would suffice -- for mono. width = widthRaw - 5 itemFull <- getsState $ itemToFull iid (ovLab, ovDesc) <- itemDescOverlays True meleeSkill dmode iid kit itemFull width let prompt = promptFun iid itemFull k promptBlurb | T.null prompt = [] | otherwise = offsetOverlay $ splitAttrString width width $ textFgToAS Color.Brown $ prompt <> "\n\n" len = length promptBlurb descSym2 = ytranslateOverlay len ovLab descBlurb2 = promptBlurb ++ ytranslateOverlay len ovDesc ov = EM.insertWith (++) squareFont descSym2 $ EM.singleton propFont descBlurb2 return (ov, []) okxItemLoreMsg :: MonadClientUI m => (ItemId -> ItemFull -> Int -> Text) -> Int -> ItemDialogMode -> [(ItemId, ItemQuant)] -> MenuSlot -> m OKX okxItemLoreMsg promptFun meleeSkill dmode iids slot = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui FontSetup{..} <- getFontSetup let (iid, kit@(k, _)) = iids !! fromEnum slot itemFull <- getsState $ itemToFull iid (ovLab, ovDesc) <- itemDescOverlays True meleeSkill dmode iid kit itemFull rwidth let prompt = promptFun iid itemFull k msgAdd MsgPromptGeneric prompt let ov = EM.insertWith (++) squareFont ovLab $ EM.singleton propFont ovDesc return (ov, []) itemDescOverlays :: MonadClientUI m => Bool -> Int -> ItemDialogMode -> ItemId -> ItemQuant -> ItemFull -> Int -> m (Overlay, Overlay) itemDescOverlays markParagraphs meleeSkill dmode iid kit itemFull width = do FontSetup{squareFont} <- getFontSetup side <- getsClient sside arena <- getArenaUI localTime <- getsState $ getLocalTime arena factionD <- getsState sfactionD -- The hacky level 0 marks items never seen, but sent by server at gameover. jlid <- getsSession $ fromMaybe (toEnum 0) <$> EM.lookup iid . sitemUI let descAs = itemDesc width markParagraphs side factionD meleeSkill dmode localTime jlid itemFull kit return $! labDescOverlay squareFont width descAs cycleLore :: MonadClientUI m => [m K.KM] -> [m K.KM] -> m () cycleLore _ [] = return () cycleLore seen (m : rest) = do -- @seen@ is needed for SPACE to end cycling km <- m if | km == K.spaceKM -> cycleLore (m : seen) rest | km == K.mkChar '>' -> if null rest then cycleLore [] (reverse $ m : seen) else cycleLore (m : seen) rest | km == K.mkChar '<' -> case seen of prev : ps -> cycleLore ps (prev : m : rest) [] -> case reverse (m : rest) of prev : ps -> cycleLore ps [prev] [] -> error "cycleLore: screens disappeared" | km == K.escKM -> return () | otherwise -> error "cycleLore: unexpected key" spoilsBlurb :: Text -> Int -> Int -> Text spoilsBlurb currencyName total dungeonTotal = if | dungeonTotal == 0 -> "All the spoils of your team are of the practical kind." | total == 0 -> "Your team haven't found any genuine treasure yet." | otherwise -> makeSentence [ "your team's spoils are worth" , MU.CarAWs total $ MU.Text currencyName , "out of the rumoured total" , MU.Cardinal dungeonTotal ] ppContainerWownW :: MonadClientUI m => (ActorId -> m MU.Part) -> Bool -> Container -> m [MU.Part] ppContainerWownW ownerFun addPrepositions c = case c of CFloor{} -> return ["nearby"] CEmbed{} -> return ["embedded nearby"] CActor aid store -> do side <- getsClient sside b <- getsState $ getActorBody aid owner <- ownerFun aid fidName <- getsState $ gname . (EM.! bfid b) . sfactionD let (preposition, noun) = ppCStore store prep = [MU.Text preposition | addPrepositions] return $! prep ++ case store of CGround -> MU.Text noun : if bproj b then [] else ["under", owner] CStash -> if bfid b /= side then [MU.WownW (MU.Text fidName) (MU.Text noun)] else [MU.Text noun] _ -> [MU.WownW owner (MU.Text noun)] CTrunk{} -> error $ "" `showFailure` c nxtGameMode :: COps -> Int -> (ContentId MK.ModeKind, MK.ModeKind) nxtGameMode COps{comode} snxtScenario = let f !acc _p !i !a = (i, a) : acc campaignModes = ofoldlGroup' comode MK.CAMPAIGN_SCENARIO f [] in campaignModes !! (snxtScenario `mod` length campaignModes) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/HandleHumanGlobalM.hs0000644000000000000000000027661207346545000024470 0ustar0000000000000000-- | Semantics of "Game.LambdaHack.Client.UI.HumanCmd" -- client commands that return server requests. -- A couple of them do not take time, the rest does. -- Here prompts and menus are displayed, but any feedback resulting -- from the commands (e.g., from inventory manipulation) is generated later on, -- by the server, for all clients that witness the results of the commands. module Game.LambdaHack.Client.UI.HandleHumanGlobalM ( -- * Meta commands byAreaHuman, byAimModeHuman , composeIfLocalHuman, composeUnlessErrorHuman, compose2ndLocalHuman , loopOnNothingHuman, executeIfClearHuman -- * Global commands that usually take time , waitHuman, waitHuman10, yellHuman, moveRunHuman , runOnceAheadHuman, moveOnceToXhairHuman , runOnceToXhairHuman, continueToXhairHuman , moveItemHuman, projectHuman, applyHuman , alterDirHuman, alterWithPointerHuman, closeDirHuman , helpHuman, hintHuman, dashboardHuman, itemMenuHuman, chooseItemMenuHuman , mainMenuHuman, mainMenuAutoOnHuman, mainMenuAutoOffHuman , settingsMenuHuman, challengeMenuHuman, gameDifficultyIncr , gameFishToggle, gameGoodsToggle, gameWolfToggle, gameKeeperToggle , gameScenarioIncr -- * Global commands that never take time , gameExitWithHuman, ExitStrategy(..), gameDropHuman, gameExitHuman , gameSaveHuman, doctrineHuman, automateHuman, automateToggleHuman , automateBackHuman #ifdef EXPOSE_INTERNAL -- * Internal operations , areaToRectangles, meleeAid, displaceAid, moveSearchAlter, alterCommon , goToXhair, goToXhairExplorationMode, goToXhairGoTo , multiActorGoTo, moveOrSelectItem, selectItemsToMove, moveItems , projectItem, applyItem, alterTileAtPos, verifyAlters, processTileActions , verifyEscape, verifyToolEffect, closeTileAtPos, msgAddDone, pickPoint , generateMenu #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Char as Char import Data.Either import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Map.Strict as M import qualified Data.Text as T import Data.Version import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.BfsM import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Content.Input import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.FrameM import Game.LambdaHack.Client.UI.HandleHelperM import Game.LambdaHack.Client.UI.HandleHumanLocalM import Game.LambdaHack.Client.UI.HumanCmd import Game.LambdaHack.Client.UI.InventoryM import Game.LambdaHack.Client.UI.ItemDescription import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.KeyBindings import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Client.UI.RunM import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.FactionKind as FK import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.ModeKind as MK import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal -- * ByArea -- | Pick command depending on area the mouse pointer is in. -- The first matching area is chosen. If none match, only interrupt. byAreaHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> [(CmdArea, HumanCmd)] -> m (Either MError ReqUI) byAreaHuman cmdSemInCxtOfKM l = do CCUI{coinput=InputContent{brevMap}} <- getsSession sccui pUI <- getsSession spointer let PointSquare px py = uiToSquare pUI p = Point {..} -- abuse of convention: @Point@, not @PointSquare@ used -- for the whole UI screen in square font coordinates pointerInArea a = do rs <- areaToRectangles a return $! any (`inside` p) $ catMaybes rs cmds <- filterM (pointerInArea . fst) l case cmds of [] -> do stopPlayBack return $ Left Nothing (_, cmd) : _ -> do let kmFound = case M.lookup cmd brevMap of Just (km : _) -> km _ -> K.escKM cmdSemInCxtOfKM kmFound cmd -- Many values here are shared with "Game.LambdaHack.Client.UI.DrawM". areaToRectangles :: MonadClientUI m => CmdArea -> m [Maybe Area] areaToRectangles ca = map toArea <$> do CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui case ca of CaMessage -> return [(0, 0, rwidth - 1, 0)] CaMapLeader -> do -- takes preference over @CaMapParty@ and @CaMap@ mleader <- getsClient sleader case mleader of Nothing -> return [] Just leader -> do b <- getsState $ getActorBody leader let PointSquare x y = mapToSquare $ bpos b return [(x, y, x, y)] CaMapParty -> do -- takes preference over @CaMap@ lidV <- viewedLevelUI side <- getsClient sside ours <- getsState $ filter (not . bproj) . map snd . actorAssocs (== side) lidV let rectFromB p = let PointSquare x y = mapToSquare p in (x, y, x, y) return $! map (rectFromB . bpos) ours CaMap -> let PointSquare xo yo = mapToSquare originPoint PointSquare xe ye = mapToSquare $ Point (rwidth - 1) (rheight - 4) in return [(xo, yo, xe, ye)] CaLevelNumber -> let y = rheight - 2 in return [(0, y, 1, y)] CaArenaName -> let y = rheight - 2 x = (rwidth - 1) `div` 2 - 11 in return [(3, y, x, y)] CaPercentSeen -> let y = rheight - 2 x = (rwidth - 1) `div` 2 in return [(x - 9, y, x, y)] CaXhairDesc -> let y = rheight - 2 x = (rwidth - 1) `div` 2 + 2 in return [(x, y, rwidth - 1, y)] CaSelected -> let y = rheight - 1 x = (rwidth - 1) `div` 2 in return [(0, y, x - 24, y)] CaCalmGauge -> let y = rheight - 1 x = (rwidth - 1) `div` 2 in return [(x - 22, y, x - 18, y)] CaCalmValue -> let y = rheight - 1 x = (rwidth - 1) `div` 2 in return [(x - 17, y, x - 11, y)] CaHPGauge -> let y = rheight - 1 x = (rwidth - 1) `div` 2 in return [(x - 9, y, x - 6, y)] CaHPValue -> let y = rheight - 1 x = (rwidth - 1) `div` 2 in return [(x - 6, y, x, y)] CaLeaderDesc -> let y = rheight - 1 x = (rwidth - 1) `div` 2 + 2 in return [(x, y, rwidth - 1, y)] -- * ByAimMode byAimModeHuman :: MonadClientUI m => m (Either MError ReqUI) -> m (Either MError ReqUI) -> m (Either MError ReqUI) byAimModeHuman cmdNotAimingM cmdAimingM = do aimMode <- getsSession saimMode if isNothing aimMode then cmdNotAimingM else cmdAimingM -- * ComposeIfLocal composeIfLocalHuman :: MonadClientUI m => m (Either MError ReqUI) -> m (Either MError ReqUI) -> m (Either MError ReqUI) composeIfLocalHuman c1 c2 = do slideOrCmd1 <- c1 case slideOrCmd1 of Left merr1 -> do slideOrCmd2 <- c2 case slideOrCmd2 of Left merr2 -> return $ Left $ mergeMError merr1 merr2 _ -> return slideOrCmd2 _ -> return slideOrCmd1 -- * ComposeUnlessError composeUnlessErrorHuman :: MonadClientUI m => m (Either MError ReqUI) -> m (Either MError ReqUI) -> m (Either MError ReqUI) composeUnlessErrorHuman c1 c2 = do slideOrCmd1 <- c1 case slideOrCmd1 of Left Nothing -> c2 _ -> return slideOrCmd1 -- * Compose2ndLocal compose2ndLocalHuman :: MonadClientUI m => m (Either MError ReqUI) -> m (Either MError ReqUI) -> m (Either MError ReqUI) compose2ndLocalHuman c1 c2 = do slideOrCmd1 <- c1 case slideOrCmd1 of Left merr1 -> do slideOrCmd2 <- c2 case slideOrCmd2 of Left merr2 -> return $ Left $ mergeMError merr1 merr2 _ -> return slideOrCmd1 -- ignore second request, keep effect req -> do void c2 -- ignore second request, keep effect return req -- * LoopOnNothing loopOnNothingHuman :: MonadClientUI m => m (Either MError ReqUI) -> m (Either MError ReqUI) loopOnNothingHuman cmd = do res <- cmd case res of Left Nothing -> loopOnNothingHuman cmd _ -> return res -- * ExecuteIfClear executeIfClearHuman :: MonadClientUI m => m (Either MError ReqUI) -> m (Either MError ReqUI) executeIfClearHuman c1 = do sreportNull <- getsSession sreportNull sreqDelay <- getsSession sreqDelay -- When server query delay is handled, don't complicate things by clearing -- screen instead of running the command. if sreportNull || sreqDelay == ReqDelayHandled then c1 else return $ Left Nothing -- * Wait -- | Leader waits a turn (and blocks, etc.). waitHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed) waitHuman leader = do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader if Ability.getSk Ability.SkWait actorCurAndMaxSk > 0 then do modifySession $ \sess -> sess {swaitTimes = abs (swaitTimes sess) + 1} return $ Right ReqWait else failSer WaitUnskilled -- * Wait10 -- | Leader waits a 1/10th of a turn (and doesn't block, etc.). waitHuman10 :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed) waitHuman10 leader = do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader if Ability.getSk Ability.SkWait actorCurAndMaxSk >= 4 then do modifySession $ \sess -> sess {swaitTimes = abs (swaitTimes sess) + 1} return $ Right ReqWait10 else failSer WaitUnskilled -- * Yell -- | Leader yells or yawns, if sleeping. yellHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed) yellHuman leader = do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader if Ability.getSk Ability.SkWait actorCurAndMaxSk > 0 -- If waiting drained and really, potentially, no other possible action, -- still allow yelling. || Ability.getSk Ability.SkMove actorCurAndMaxSk <= 0 || Ability.getSk Ability.SkDisplace actorCurAndMaxSk <= 0 || Ability.getSk Ability.SkMelee actorCurAndMaxSk <= 0 then return $ Right ReqYell else failSer WaitUnskilled -- * MoveDir and RunDir moveRunHuman :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> Bool -> Bool -> Bool -> Vector -> m (FailOrCmd RequestTimed) moveRunHuman leader initialStep finalGoal run runAhead dir = do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader arena <- getArenaUI sb <- getsState $ getActorBody leader fact <- getsState $ (EM.! bfid sb) . sfactionD -- Start running in the given direction. The first turn of running -- succeeds much more often than subsequent turns, because we ignore -- most of the disturbances, since the player is mostly aware of them -- and still explicitly requests a run, knowing how it behaves. sel <- getsSession sselected let runMembers = if runAhead || noRunWithMulti fact then [leader] else ES.elems (ES.delete leader sel) ++ [leader] runParams = RunParams { runLeader = leader , runMembers , runInitial = True , runStopMsg = Nothing , runWaiting = 0 } initRunning = when (initialStep && run) $ do modifySession $ \sess -> sess {srunning = Just runParams} when runAhead $ macroHuman macroRun25 -- When running, the invisible actor is hit (not displaced!), -- so that running in the presence of roving invisible -- actors is equivalent to moving (with visible actors -- this is not a problem, since runnning stops early enough). let tpos = bpos sb `shift` dir -- We start by checking actors at the target position, -- which gives a partial information (actors can be invisible), -- as opposed to accessibility (and items) which are always accurate -- (tiles can't be invisible). tgts <- getsState $ posToAidAssocs tpos arena case tgts of [] -> do -- move or search or alter runStopOrCmd <- moveSearchAlter leader run dir case runStopOrCmd of Left stopMsg -> return $ Left stopMsg Right runCmd -> do -- Don't check @initialStep@ and @finalGoal@ -- and don't stop going to target: door opening is mundane enough. initRunning return $ Right runCmd [(target, _)] | run && initialStep && Ability.getSk Ability.SkDisplace actorCurAndMaxSk > 0 -> -- No @stopPlayBack@: initial displace is benign enough. -- Displacing requires accessibility, but it's checked later on. displaceAid leader target _ : _ : _ | run && initialStep && Ability.getSk Ability.SkDisplace actorCurAndMaxSk > 0 -> failSer DisplaceMultiple (target, tb) : _ | not run && initialStep && finalGoal && bfid tb == bfid sb && not (bproj tb) -> do stopPlayBack -- don't ever auto-repeat leader choice -- We always see actors from our own faction. -- Select one of adjacent actors by bumping into him. Takes no time. success <- pickLeader True target let !_A = assert (success `blame` "bump self" `swith` (leader, target, tb)) () failWith "the pointman switched by bumping" (target, tb) : _ | not run && initialStep && finalGoal && (bfid tb /= bfid sb || bproj tb) -> do stopPlayBack -- don't ever auto-repeat melee if Ability.getSk Ability.SkMelee actorCurAndMaxSk > 0 then -- No problem if there are many projectiles at the spot. We just -- attack the first one. meleeAid leader target else failSer MeleeUnskilled _ : _ -> failWith "actor in the way" -- | Actor attacks an enemy actor or his own projectile. meleeAid :: (MonadClient m, MonadClientUI m) => ActorId -> ActorId -> m (FailOrCmd RequestTimed) meleeAid leader target = do side <- getsClient sside tb <- getsState $ getActorBody target sfact <- getsState $ (EM.! side) . sfactionD mel <- pickWeaponClient leader target case mel of Nothing -> failWith "nothing to melee with" Just wp -> do let returnCmd = do -- Set personal target to enemy, so that AI, if it takes over -- the actor, is likely to continue the fight even if the foe flees. modifyClient $ updateTarget leader $ const $ Just $ TEnemy target -- Also set xhair to see the foe's HP, because it's automatically -- set to any new spotted actor, so it needs to be reset -- and also it's not useful as permanent ranged target anyway. modifySession $ \sess -> sess {sxhair = Just $ TEnemy target} return $ Right wp res | bproj tb || isFoe side sfact (bfid tb) = returnCmd | isFriend side sfact (bfid tb) = do let !_A = assert (side /= bfid tb) () go1 <- displayYesNo ColorBW "You are bound by an alliance. Really attack?" if not go1 then failWith "attack canceled" else returnCmd | otherwise = do go2 <- displayYesNo ColorBW "This attack will start a war. Are you sure?" if not go2 then failWith "attack canceled" else returnCmd res -- Seeing the actor prevents altering a tile under it, but that -- does not limit the player, he just doesn't waste a turn -- on a failed altering. -- | Actor swaps position with another. displaceAid :: MonadClientUI m => ActorId -> ActorId -> m (FailOrCmd RequestTimed) displaceAid leader target = do COps{coTileSpeedup} <- getsState scops sb <- getsState $ getActorBody leader tb <- getsState $ getActorBody target tfact <- getsState $ (EM.! bfid tb) . sfactionD actorMaxSk <- getsState $ getActorMaxSkills target dEnemy <- getsState $ dispEnemy leader target actorMaxSk let immobile = Ability.getSk Ability.SkMove actorMaxSk <= 0 tpos = bpos tb adj = checkAdjacent sb tb atWar = isFoe (bfid tb) tfact (bfid sb) if | not adj -> failSer DisplaceDistant | not (bproj tb) && atWar && actorDying tb -> -- checked separately for a better message failSer DisplaceDying | not (bproj tb) && atWar && actorWaits tb -> -- checked separately for a better message failSer DisplaceBraced | not (bproj tb) && atWar && immobile -> -- checked separately for a better message failSer DisplaceImmobile | not dEnemy && atWar -> failSer DisplaceSupported | otherwise -> do let lid = blid sb lvl <- getLevel lid -- Displacing requires full access. if Tile.isWalkable coTileSpeedup $ lvl `at` tpos then case posToAidsLvl tpos lvl of [] -> error $ "" `showFailure` (leader, sb, target, tb) [_] -> return $ Right $ ReqDisplace target _ -> failSer DisplaceMultiple else failSer DisplaceAccess -- | Leader moves or searches or alters. No visible actor at the position. moveSearchAlter :: MonadClientUI m => ActorId -> Bool -> Vector -> m (FailOrCmd RequestTimed) moveSearchAlter leader run dir = do COps{coTileSpeedup} <- getsState scops actorCurAndMaxSk <- getsState $ getActorMaxSkills leader sb <- getsState $ getActorBody leader let moveSkill = Ability.getSk Ability.SkMove actorCurAndMaxSk spos = bpos sb -- source position tpos = spos `shift` dir -- target position alterable <- getsState $ tileAlterable (blid sb) tpos lvl <- getLevel $ blid sb let t = lvl `at` tpos runStopOrCmd <- if Tile.isWalkable coTileSpeedup t then -- Movement requires full access. if | moveSkill > 0 -> -- A potential invisible actor is hit. War started without asking. return $ Right $ ReqMove dir | bwatch sb == WSleep -> failSer MoveUnskilledAsleep | otherwise -> failSer MoveUnskilled else do -- Not walkable, so search and/or alter the tile. let sxhair = Just $ TPoint TUnknown (blid sb) tpos -- Point xhair to see details with `~`. setXHairFromGUI sxhair if run then do -- Explicit request to examine the terrain. blurb <- lookAtPosition tpos (blid sb) mapM_ (uncurry msgAdd) blurb failWith $ "the terrain is" <+> if | Tile.isModifiable coTileSpeedup t -> "potentially modifiable" | alterable -> "potentially triggerable" | otherwise -> "completely inert" else alterCommon leader True tpos return $! runStopOrCmd alterCommon :: MonadClientUI m => ActorId -> Bool -> Point -> m (FailOrCmd RequestTimed) alterCommon leader bumping tpos = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui cops@COps{cotile, coTileSpeedup} <- getsState scops side <- getsClient sside factionD <- getsState sfactionD actorCurAndMaxSk <- getsState $ getActorMaxSkills leader sb <- getsState $ getActorBody leader let alterSkill = Ability.getSk Ability.SkAlter actorCurAndMaxSk spos = bpos sb alterable <- getsState $ tileAlterable (blid sb) tpos lvl <- getLevel $ blid sb localTime <- getsState $ getLocalTime (blid sb) embeds <- getsState $ getEmbedBag (blid sb) tpos itemToF <- getsState $ flip itemToFull getKind <- getsState $ flip getIidKind let t = lvl `at` tpos underFeet = tpos == spos -- if enter and alter, be more permissive modificationFailureHint = msgAdd MsgTutorialHint "Some doors can be opened, stairs unbarred, treasures recovered, only if you find tools that increase your terrain modification ability and act as keys to the puzzle. To gather clues about the keys, listen to what's around you, examine items, inspect terrain, trigger, bump and harass. Once you uncover a likely tool, wield it, return and try to break through again." if | not alterable -> do let name = MU.Text $ TK.tname $ okind cotile t itemLook (iid, kit@(k, _)) = let itemFull = itemToF iid in partItemWsShort rwidth side factionD k localTime itemFull kit embedKindList = 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] ++ if EM.null embeds then [] else ["with", MU.WWandW ilooks] -- misclick? related to AlterNothing but no searching possible; -- this also rules out activating embeds that only cause -- raw damage, with no chance of altering the tile | Tile.isSuspect coTileSpeedup t && not underFeet && alterSkill <= 1 -> do modificationFailureHint failSer AlterUnskilled | not (Tile.isSuspect coTileSpeedup t) && not underFeet && alterSkill < Tile.alterMinSkill coTileSpeedup t -> do -- Rather rare (requires high skill), so describe the tile. blurb <- lookAtPosition tpos (blid sb) mapM_ (uncurry msgAdd) blurb modificationFailureHint failSer AlterUnwalked | chessDist tpos (bpos sb) > 1 -> -- Checked late to give useful info about distant tiles. failSer AlterDistant | not underFeet && (occupiedBigLvl tpos lvl || occupiedProjLvl tpos lvl) -> -- Don't mislead describing terrain, if other actor is to blame. failSer AlterBlockActor | otherwise -> do -- promising verAlters <- verifyAlters leader bumping tpos case verAlters of Right () -> if bumping then return $ Right $ ReqMove $ vectorToFrom tpos spos else do msgAddDone False leader tpos "modify" return $ Right $ ReqAlter tpos Left err -> return $ Left err -- Even when bumping, we don't use ReqMove, because we don't want -- to hit invisible actors, e.g., hidden in a wall. -- If server performed an attack for free -- on the invisible actor anyway, the player (or AI) -- would be tempted to repeatedly hit random walls -- in hopes of killing a monster residing within. -- If the action had a cost, misclicks would incur the cost, too. -- Right now the player may repeatedly alter tiles trying to learn -- about invisible pass-wall actors, but when an actor detected, -- it costs a turn and does not harm the invisible actors, -- so it's not so tempting. -- * RunOnceAhead runOnceAheadHuman :: MonadClientUI m => ActorId -> m (Either MError RequestTimed) runOnceAheadHuman leader = do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD keyPressed <- anyKeyPressed srunning <- getsSession srunning -- When running, stop if disturbed. If not running, stop at once. case srunning of Nothing -> do msgAdd MsgRunStopReason "run stop: nothing to do" return $ Left Nothing Just RunParams{runMembers} | noRunWithMulti fact && runMembers /= [leader] -> do msgAdd MsgRunStopReason "run stop: automatic pointman change" return $ Left Nothing Just _runParams | keyPressed -> do discardPressedKey msgAdd MsgRunStopReason "run stop: key pressed" weaveJust <$> failWith "interrupted" Just runParams -> do arena <- getArenaUI runOutcome <- continueRun arena runParams case runOutcome of Left stopMsg -> do msgAdd MsgRunStopReason ("run stop:" <+> stopMsg) return $ Left Nothing Right runCmd -> return $ Right runCmd -- * MoveOnceToXhair moveOnceToXhairHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed) moveOnceToXhairHuman leader = goToXhair leader True False goToXhair :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> Bool -> m (FailOrCmd RequestTimed) goToXhair leader initialStep run = do aimMode <- getsSession saimMode -- Movement is legal only outside aiming mode. if isJust aimMode then failWith "cannot move in aiming mode" else goToXhairExplorationMode leader initialStep run goToXhairExplorationMode :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> Bool -> m (FailOrCmd RequestTimed) goToXhairExplorationMode leader initialStep run = do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader sb <- getsState $ getActorBody leader let moveSkill = Ability.getSk Ability.SkMove actorCurAndMaxSk -- If skill is too low, no path in @Bfs@ is going to be found, -- but we check the skill (and sleep) to give a more accurate message. if | moveSkill > 0 -> do xhair <- getsSession sxhair xhairGoTo <- getsSession sxhairGoTo mfail <- if isJust xhairGoTo && xhairGoTo /= xhair then failWith "crosshair position changed" else do when (isNothing xhairGoTo) $ -- set it up for next steps modifySession $ \sess -> sess {sxhairGoTo = xhair} goToXhairGoTo leader initialStep run when (isLeft mfail) $ modifySession $ \sess -> sess {sxhairGoTo = Nothing} return mfail | bwatch sb == WSleep -> failSer MoveUnskilledAsleep | otherwise -> failSer MoveUnskilled goToXhairGoTo :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> Bool -> m (FailOrCmd RequestTimed) goToXhairGoTo leader initialStep run = do b <- getsState $ getActorBody leader mxhairPos <- mxhairToPos case mxhairPos of Nothing -> failWith "crosshair position invalid" Just c -> do running <- getsSession srunning case running of -- Don't use running params from previous run or goto-xhair. Just paramOld | not initialStep -> do arena <- getArenaUI runOutcome <- multiActorGoTo arena c paramOld case runOutcome of Left stopMsg -> return $ Left stopMsg Right (finalGoal, dir) -> moveRunHuman leader initialStep finalGoal run False dir _ | c == bpos b -> failWith "position reached" _ -> do let !_A = assert (initialStep || not run) () (bfs, mpath) <- getCacheBfsAndPath leader c xhairMoused <- getsSession sxhairMoused case mpath of _ | xhairMoused && isNothing (accessBfs bfs c) -> failWith "no route to crosshair (press again to go there anyway)" _ | initialStep && adjacent (bpos b) c -> do let dir = towards (bpos b) c moveRunHuman leader initialStep True run False dir Nothing -> failWith "no route to crosshair" Just AndPath{pathList=[]} -> failWith "almost there" Just AndPath{pathList = p1 : _} -> do let finalGoal = p1 == c dir = towards (bpos b) p1 moveRunHuman leader initialStep finalGoal run False dir multiActorGoTo :: (MonadClient m, MonadClientUI m) => LevelId -> Point -> RunParams -> m (FailOrCmd (Bool, Vector)) multiActorGoTo arena c paramOld = case paramOld of RunParams{runMembers = []} -> failWith "selected actors no longer there" RunParams{runMembers = r : rs, runWaiting} -> do onLevel <- getsState $ memActor r arena b <- getsState $ getActorBody r mxhairPos <- mxhairToPos if not onLevel || mxhairPos == Just (bpos b) then do let paramNew = paramOld {runMembers = rs} multiActorGoTo arena c paramNew else do sL <- getState modifyClient $ updateLeader r sL let runMembersNew = rs ++ [r] paramNew = paramOld { runMembers = runMembersNew , runWaiting = 0} (bfs, mpath) <- getCacheBfsAndPath r c xhairMoused <- getsSession sxhairMoused case mpath of _ | xhairMoused && isNothing (accessBfs bfs c) -> failWith "no route to crosshair (press again to go there anyway)" Nothing -> failWith "no route to crosshair" Just AndPath{pathList=[]} -> failWith "almost there" Just AndPath{pathList = p1 : _} -> do let finalGoal = p1 == c dir = towards (bpos b) p1 tgts <- getsState $ posToAids p1 arena case tgts of [] -> do modifySession $ \sess -> sess {srunning = Just paramNew} return $ Right (finalGoal, dir) [target] | target `elem` rs || runWaiting <= length rs -> -- Let r wait until all others move. Mark it in runWaiting -- to avoid cycles. When all wait for each other, fail. multiActorGoTo arena c paramNew{runWaiting=runWaiting + 1} _ -> failWith "collective running finished" -- usually OK -- * RunOnceToXhair runOnceToXhairHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed) runOnceToXhairHuman leader = goToXhair leader True True -- * ContinueToXhair continueToXhairHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed) continueToXhairHuman leader = goToXhair leader False False{-irrelevant-} -- * MoveItem moveItemHuman :: forall m. MonadClientUI m => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd RequestTimed) moveItemHuman leader stores destCStore mverb auto = do let !_A = assert (destCStore `notElem` stores) () actorCurAndMaxSk <- getsState $ getActorMaxSkills leader if Ability.getSk Ability.SkMoveItem actorCurAndMaxSk > 0 then moveOrSelectItem leader stores destCStore mverb auto else failSer MoveItemUnskilled -- This cannot be structured as projecting or applying, with @ByItemMode@ -- and @ChooseItemToMove@, because at least in case of grabbing items, -- more than one item is chosen, which doesn't fit @sitemSel@. Separating -- grabbing of multiple items as a distinct command is too high a price. moveOrSelectItem :: forall m. MonadClientUI m => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd RequestTimed) moveOrSelectItem leader storesRaw destCStore mverb auto = do b <- getsState $ getActorBody leader actorCurAndMaxSk <- getsState $ getActorMaxSkills leader mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b let calmE = calmEnough b actorCurAndMaxSk overStash = mstash == Just (blid b, bpos b) stores = case storesRaw of CEqp : rest@(_ : _) | not calmE -> rest ++ [CEqp] CGround : rest@(_ : _) | overStash -> rest ++ [CGround] _ -> storesRaw itemSel <- getsSession sitemSel modifySession $ \sess -> sess {sitemSel = Nothing} -- prevent surprise case itemSel of _ | stores == [CGround] && overStash -> failWith "you can't loot items from your own stash" Just (_, fromCStore@CEqp, _) | fromCStore /= destCStore && fromCStore `elem` stores && not calmE -> failWith "neither the selected item nor any other can be unequipped" Just (_, fromCStore@CGround, _) | fromCStore /= destCStore && fromCStore `elem` stores && overStash -> failWith "you vainly paw through your own hoard" Just (iid, fromCStore, _) | fromCStore /= destCStore && fromCStore `elem` stores -> do bag <- getsState $ getBodyStoreBag b fromCStore case iid `EM.lookup` bag of Nothing -> -- the case of old selection or selection from another actor moveOrSelectItem leader stores destCStore mverb auto Just (k, it) -> assert (k > 0) $ do let eqpFree = eqpFreeN b kToPick | destCStore == CEqp = min eqpFree k | otherwise = k if | destCStore == CEqp && not calmE -> failSer ItemNotCalm | destCStore == CGround && overStash -> failSer ItemOverStash | kToPick == 0 -> failWith "no more items can be equipped" | otherwise -> do socK <- pickNumber (not auto) kToPick case socK of Left Nothing -> moveOrSelectItem leader stores destCStore mverb auto Left (Just err) -> return $ Left err Right kChosen -> let is = (fromCStore, [(iid, (kChosen, take kChosen it))]) in Right <$> moveItems leader stores is destCStore _ -> do mis <- selectItemsToMove leader stores destCStore mverb auto case mis of Left err -> return $ Left err Right (fromCStore, [(iid, _)]) | stores /= [CGround] -> do modifySession $ \sess -> sess {sitemSel = Just (iid, fromCStore, False)} moveOrSelectItem leader stores destCStore mverb auto Right is@(fromCStore, _) -> if | fromCStore == CEqp && not calmE -> failSer ItemNotCalm | fromCStore == CGround && overStash -> failSer ItemOverStash | otherwise -> Right <$> moveItems leader stores is destCStore selectItemsToMove :: forall m. MonadClientUI m => ActorId -> [CStore] -> CStore -> Maybe Text -> Bool -> m (FailOrCmd (CStore, [(ItemId, ItemQuant)])) selectItemsToMove leader stores destCStore mverb auto = do let verb = fromMaybe (verbCStore destCStore) mverb actorCurAndMaxSk <- getsState $ getActorMaxSkills leader b <- getsState $ getActorBody leader mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b lastItemMove <- getsSession slastItemMove -- This calmE is outdated when one of the items increases max Calm -- (e.g., in pickup, which handles many items at once), but this is OK, -- the server accepts item movement based on calm at the start, not end -- or in the middle. -- The calmE is inaccurate also if an item not IDed, but that's intended -- and the server will ignore and warn (and content may avoid that, -- e.g., making all rings identified) let calmE = calmEnough b actorCurAndMaxSk overStash = mstash == Just (blid b, bpos b) if | destCStore == CEqp && not calmE -> failSer ItemNotCalm | destCStore == CGround && overStash -> failSer ItemOverStash | destCStore == CEqp && eqpOverfull b 1 -> failSer EqpOverfull | otherwise -> do let storesLast = case lastItemMove of Just (lastFrom, lastDest) | lastDest == destCStore && lastFrom `elem` stores -> lastFrom : delete lastFrom stores _ -> stores prompt = "What to" promptEqp = "What consumable to" eqpItemsN body = let n = sum $ map fst $ EM.elems $ beqp body in "(" <> makePhrase [MU.CarWs n "item"] ppItemDialogBody body actorSk cCur = case cCur of MStore CEqp | not $ calmEnough body actorSk -> "distractedly paw at" <+> ppItemDialogModeIn cCur MStore CGround | mstash == Just (blid body, bpos body) -> "greedily fondle" <+> ppItemDialogModeIn cCur _ -> case destCStore of CEqp | not $ calmEnough body actorSk -> "distractedly attempt to" <+> verb <+> ppItemDialogModeFrom cCur CEqp | eqpOverfull body 1 -> "attempt to fit into equipment" <+> ppItemDialogModeFrom cCur CGround | mstash == Just (blid body, bpos body) -> "greedily attempt to" <+> verb <+> ppItemDialogModeFrom cCur CEqp -> verb <+> eqpItemsN body <+> "so far)" <+> ppItemDialogModeFrom cCur _ -> verb <+> ppItemDialogModeFrom cCur <+> if cCur == MStore CEqp then eqpItemsN body <+> "now)" else "" (promptGeneric, psuit) = -- We prune item list only for eqp, because other stores don't have -- so clear cut heuristics. So when picking up a stash, either grab -- it to auto-store things, or equip first using the pruning -- and then stash the rest selectively or en masse. if destCStore == CEqp then (promptEqp, return $ SuitsSomething $ \_ itemFull _kit -> IA.goesIntoEqp $ aspectRecordFull itemFull) else (prompt, return SuitsEverything) ggi <- getFull leader psuit (\body _ actorSk cCur _ -> prompt <+> ppItemDialogBody body actorSk cCur) (\body _ actorSk cCur _ -> promptGeneric <+> ppItemDialogBody body actorSk cCur) storesLast (not auto) True case ggi of Right (fromCStore, l) -> do modifySession $ \sess -> sess {slastItemMove = Just (fromCStore, destCStore)} return $ Right (fromCStore, l) Left err -> failWith err moveItems :: forall m. MonadClientUI m => ActorId -> [CStore] -> (CStore, [(ItemId, ItemQuant)]) -> CStore -> m RequestTimed moveItems leader stores (fromCStore, l) destCStore = do let !_A = assert (fromCStore /= destCStore && fromCStore `elem` stores) () actorCurAndMaxSk <- getsState $ getActorMaxSkills leader b <- getsState $ getActorBody leader discoBenefit <- getsClient sdiscoBenefit let calmE = calmEnough b actorCurAndMaxSk ret4 :: [(ItemId, ItemQuant)] -> Int -> m [(ItemId, Int, CStore, CStore)] ret4 [] _ = return [] ret4 ((iid, (k, _)) : rest) oldN = do let !_A = assert (k > 0) () retRec toCStore = do let n = oldN + if toCStore == CEqp then k else 0 l4 <- ret4 rest n return $ (iid, k, fromCStore, toCStore) : l4 if stores == [CGround] && destCStore == CStash -- normal pickup then -- @CStash@ is the implicit default; refine: if | not $ benInEqp $ discoBenefit EM.! iid -> retRec CStash | eqpOverfull b (oldN + 1) -> do -- Action goes through, but changed, so keep in history. msgAdd MsgActionWarning $ "Warning:" <+> showReqFailure EqpOverfull <> "." retRec CStash | eqpOverfull b (oldN + k) -> do -- If this stack doesn't fit, we don't equip any part of it, -- but we may equip a smaller stack later of other items -- in the same pickup. msgAdd MsgActionWarning $ "Warning:" <+> showReqFailure EqpStackFull <> "." retRec CStash | not calmE -> do msgAdd MsgActionWarning $ "Warning:" <+> showReqFailure ItemNotCalm <> "." retRec CStash | otherwise -> -- Prefer @CEqp@ if all conditions hold: retRec CEqp else case destCStore of -- player forces store, so @benInEqp@ ignored CEqp | eqpOverfull b (oldN + 1) -> do -- Action aborted, so different colour and not in history. msgAdd MsgPromptItems $ "Failure:" <+> showReqFailure EqpOverfull <> "." -- No recursive call here, we exit item manipulation, -- but something is moved or else outer functions would not call us. return [] CEqp | eqpOverfull b (oldN + k) -> do msgAdd MsgPromptItems $ "Failure:" <+> showReqFailure EqpStackFull <> "." return [] _ -> retRec destCStore l4 <- ret4 l 0 if null l4 then error $ "" `showFailure` (stores, fromCStore, l, destCStore) else return $! ReqMoveItems l4 -- * Project projectHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m (FailOrCmd RequestTimed) projectHuman leader = do curChal <- getsClient scurChal actorCurAndMaxSk <- getsState $ getActorMaxSkills leader if | ckeeper curChal -> failSer ProjectFinderKeeper | Ability.getSk Ability.SkProject actorCurAndMaxSk <= 0 -> -- Detailed are check later. failSer ProjectUnskilled | otherwise -> do itemSel <- getsSession sitemSel case itemSel of Just (_, COrgan, _) -> failWith "can't fling an organ" Just (iid, fromCStore, _) -> do b <- getsState $ getActorBody leader bag <- getsState $ getBodyStoreBag b fromCStore case iid `EM.lookup` bag of Nothing -> failWith "no item to fling" Just _kit -> do itemFull <- getsState $ itemToFull iid let i = (fromCStore, (iid, itemFull)) projectItem leader i Nothing -> failWith "no item to fling" projectItem :: (MonadClient m, MonadClientUI m) => ActorId -> (CStore, (ItemId, ItemFull)) -> m (FailOrCmd RequestTimed) projectItem leader (fromCStore, (iid, itemFull)) = do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader b <- getsState $ getActorBody leader let calmE = calmEnough b actorCurAndMaxSk if fromCStore == CEqp && not calmE then failSer ItemNotCalm else do mpsuitReq <- psuitReq leader case mpsuitReq of Left err -> failWith err Right psuitReqFun -> case psuitReqFun itemFull of Left reqFail -> failSer reqFail Right (pos, _) -> do Benefit{benFling} <- getsClient $ (EM.! iid) . sdiscoBenefit go <- if benFling >= 0 then displayYesNo ColorFull "The item may be beneficial. Do you really want to fling it?" else return True if go then do -- Set personal target to enemy, so that AI, if it takes over -- the actor, is likely to continue the fight even if the foe -- flees. Similarly if the crosshair points at position, etc. sxhair <- getsSession sxhair modifyClient $ updateTarget leader (const sxhair) -- Project. eps <- getsClient seps return $ Right $ ReqProject pos eps iid fromCStore else do modifySession $ \sess -> sess {sitemSel = Nothing} failWith "never mind" -- * Apply applyHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed) applyHuman leader = do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader if Ability.getSk Ability.SkApply actorCurAndMaxSk <= 0 then -- detailed check later failSer ApplyUnskilled else do itemSel <- getsSession sitemSel case itemSel of Just (iid, fromCStore, _) -> do b <- getsState $ getActorBody leader bag <- getsState $ getBodyStoreBag b fromCStore case iid `EM.lookup` bag of Nothing -> failWith "no item to trigger" Just kit -> do itemFull <- getsState $ itemToFull iid applyItem leader (fromCStore, (iid, (itemFull, kit))) Nothing -> failWith "no item to trigger" applyItem :: MonadClientUI m => ActorId -> (CStore, (ItemId, ItemFullKit)) -> m (FailOrCmd RequestTimed) applyItem leader (fromCStore, (iid, (itemFull, kit))) = do COps{corule} <- getsState scops actorCurAndMaxSk <- getsState $ getActorMaxSkills leader b <- getsState $ getActorBody leader localTime <- getsState $ getLocalTime (blid b) let skill = Ability.getSk Ability.SkApply actorCurAndMaxSk calmE = calmEnough b actorCurAndMaxSk arItem = aspectRecordFull itemFull if fromCStore == CEqp && not calmE then failSer ItemNotCalm else case permittedApply corule localTime skill calmE (Just fromCStore) itemFull kit of Left reqFail -> failSer reqFail Right _ -> do Benefit{benApply} <- getsClient $ (EM.! iid) . sdiscoBenefit go <- if | IA.checkFlag Ability.Periodic arItem && not (IA.checkFlag Ability.Durable arItem) -> -- No warning if item durable, because activation weak, -- but price low, due to no destruction. displayYesNo ColorFull "Triggering this periodic item may not produce all its effects (check item description) and moreover, because it's not durable, will destroy it. Are you sure?" | benApply < 0 -> displayYesNo ColorFull "The item appears harmful. Do you really want to trigger it?" | otherwise -> return True if go then return $ Right $ ReqApply iid fromCStore else do modifySession $ \sess -> sess {sitemSel = Nothing} failWith "never mind" -- * AlterDir -- | Ask for a direction and alter a tile, if possible. alterDirHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed) alterDirHuman leader = pickPoint leader "modify" >>= \case Just p -> alterTileAtPos leader p Nothing -> failWith "never mind" -- | Try to alter a tile using a feature at the given position. -- -- We don't check if the tile is interesting, e.g., if any embedded -- item can be triggered, because the player explicitely requested -- the action. Consequently, even if all embedded items are recharching, -- the time will be wasted and the server will describe the failure in detail. alterTileAtPos :: MonadClientUI m => ActorId -> Point -> m (FailOrCmd RequestTimed) alterTileAtPos leader pos = do sb <- getsState $ getActorBody leader let sxhair = Just $ TPoint TUnknown (blid sb) pos -- Point xhair to see details with `~`. setXHairFromGUI sxhair alterCommon leader False pos -- | Verify that the tile can be transformed or any embedded item effect -- triggered and the player is aware if the effect is dangerous or grave, -- such as ending the game. verifyAlters :: forall m. MonadClientUI m => ActorId -> Bool -> Point -> m (FailOrCmd ()) verifyAlters leader bumping tpos = do COps{cotile, coTileSpeedup} <- getsState scops sb <- getsState $ getActorBody leader arItem <- getsState $ aspectRecordFromIid $ btrunk sb embeds <- getsState $ getEmbedBag (blid sb) tpos lvl <- getLevel $ blid sb getKind <- getsState $ flip getIidKind let embedKindList = if IA.checkFlag Ability.Blast arItem then [] -- prevent embeds triggering each other in a loop else map (\(iid, kit) -> (getKind iid, (iid, kit))) (EM.assocs embeds) underFeet = tpos == bpos sb -- if enter and alter, be more permissive blockedByItem = EM.member tpos (lfloor lvl) tile = lvl `at` tpos feats = TK.tfeature $ okind cotile tile tileActions = mapMaybe (parseTileAction (bproj sb) (underFeet || blockedByItem) -- avoids AlterBlockItem embedKindList) feats if null tileActions && blockedByItem && not underFeet && Tile.isModifiable coTileSpeedup tile then failSer AlterBlockItem else processTileActions leader bumping tpos tileActions processTileActions :: forall m. MonadClientUI m => ActorId -> Bool -> Point -> [TileAction] -> m (FailOrCmd ()) processTileActions leader bumping tpos tas = do COps{coTileSpeedup} <- getsState scops getKind <- getsState $ flip getIidKind sb <- getsState $ getActorBody leader lvl <- getLevel $ blid sb sar <- getsState $ aspectRecordFromIid $ btrunk sb let leaderIsMist = IA.checkFlag Ability.Blast sar && Dice.infDice (IK.idamage $ getKind $ btrunk sb) <= 0 tileMinSkill = Tile.alterMinSkill coTileSpeedup $ lvl `at` tpos processTA :: Maybe Bool -> [TileAction] -> Bool -> m (FailOrCmd (Maybe (Bool, Bool))) processTA museResult [] bumpFailed = do let useResult = fromMaybe False museResult -- No warning will be generated if during explicit modification -- an embed is activated but there is not enough tools -- for a subsequent transformation. This is fine. Bumping would -- produce the warning and S-dir also displays the tool info. -- We can't rule out the embed is the main feature and the tool -- transformation is not important despite following it. -- We don't want spam in such a case. return $ Right $ if Tile.isSuspect coTileSpeedup (lvl `at` tpos) || useResult && not bumpFailed then Nothing -- success of some kind else Just (useResult, bumpFailed) -- not quite processTA museResult (ta : rest) bumpFailed = case ta of EmbedAction (iid, _) -> do -- Embeds are activated in the order in tile definition -- and never after the tile is changed. -- We assume the item would trigger and we let the player -- take the risk of wasted turn to verify the assumption. -- If the item recharges, the wasted turns let the player wait. let useResult = fromMaybe False museResult if | leaderIsMist || bproj sb && tileMinSkill > 0 -> -- local skill check processTA (Just useResult) rest bumpFailed -- embed won't fire; try others | (not . any IK.isEffEscape) (IK.ieffects $ getKind iid) -> processTA (Just True) rest False -- no escape checking needed, effect found; -- also bumpFailed reset, because must have been -- marginal if an embed was following it | otherwise -> do mfail <- verifyEscape case mfail of Left err -> return $ Left err Right () -> processTA (Just True) rest False -- effect found, bumpFailed reset ToAction{} -> if fromMaybe True museResult && not (bproj sb && tileMinSkill > 0) -- local skill check then return $ Right Nothing -- tile changed, no more activations else processTA museResult rest bumpFailed -- failed, but not due to bumping WithAction tools0 _ -> if not bumping || null tools0 then if fromMaybe True museResult then do -- UI requested, so this is voluntary, so item loss is fine. kitAssG <- getsState $ kitAssocs leader [CGround] kitAssE <- getsState $ kitAssocs leader [CEqp] let kitAss = listToolsToConsume kitAssG kitAssE grps0 = map (\(x, y) -> (False, x, y)) tools0 -- apply if durable (_, iidsToApply, grps) = foldl' subtractIidfromGrps (EM.empty, [], grps0) kitAss if null grps then do let hasEffectOrDmg (_, (_, ItemFull{itemKind})) = IK.idamage itemKind /= 0 || any IK.forApplyEffect (IK.ieffects itemKind) mfail <- case filter hasEffectOrDmg iidsToApply of [] -> return $ Right () (store, (_, itemFull)) : _ -> verifyToolEffect (blid sb) store itemFull case mfail of Left err -> return $ Left err Right () -> return $ Right Nothing -- tile changed, done else processTA museResult rest bumpFailed -- not enough tools else processTA museResult rest bumpFailed -- embeds failed else processTA museResult rest True -- failed due to bumping mfail <- processTA Nothing tas False case mfail of Left err -> return $ Left err Right Nothing -> return $ Right () Right (Just (useResult, bumpFailed)) -> do let !_A = assert (not useResult || bumpFailed) () blurb <- lookAtPosition tpos (blid sb) mapM_ (uncurry msgAdd) blurb if bumpFailed then do revCmd <- revCmdMap let km = revCmd AlterDir msg = "bumping is not enough to transform this terrain; modify with the '" <> T.pack (K.showKM km) <> "' command instead" if useResult then do merr <- failMsg msg msgAdd MsgPromptAction $ showFailError $ fromJust merr return $ Right () -- effect the embed activation, though else failWith msg else failWith "unable to activate nor modify at this time" -- related to, among others, @SfxNoItemsForTile@ on the server verifyEscape :: MonadClientUI m => m (FailOrCmd ()) verifyEscape = do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD if not (FK.fcanEscape $ gkind fact) then failWith "This is the way out, but where would you go in this alien world?" -- exceptionally a full sentence, because a real question else do (_, total) <- getsState $ calculateTotal side dungeonTotal <- getsState sgold let prompt | dungeonTotal == 0 = "You finally reached your goal. Really leave now?" | total == 0 = "Afraid of the challenge? Leaving so soon and without any treasure? Are you sure?" | total < dungeonTotal = "You've finally found the way out, but you didn't gather all valuables rumoured to be laying around. Really leave already?" | otherwise = "This is the way out and you collected all treasure there is to find. Really leave now?" -- The player can back off, but we never insist, -- because possibly the score formula doesn't reward treasure -- or he is focused on winning only. go <- displayYesNo ColorBW prompt if not go then failWith "here's your chance" else return $ Right () verifyToolEffect :: MonadClientUI m => LevelId -> CStore -> ItemFull -> m (FailOrCmd ()) verifyToolEffect lid store itemFull = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui side <- getsClient sside localTime <- getsState $ getLocalTime lid factionD <- getsState sfactionD let (name1, powers) = partItemShort rwidth side factionD localTime itemFull quantSingle objectA = makePhrase [MU.AW name1, powers] -- "Potentially", because an unidentified items on the ground can take -- precedence (perhaps placed there in order to get identified!). prompt = "Do you really want to transform the terrain potentially using" <+> objectA <+> ppCStoreIn store <+> "that may cause substantial side-effects?" objectThe = makePhrase ["the", name1] go <- displayYesNo ColorBW prompt if not go then failWith $ "replace" <+> objectThe <+> "and try again" -- question capitalized and ended with a dot, answer neither else return $ Right () -- * AlterWithPointer -- | Try to alter a tile using a feature under the pointer. alterWithPointerHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed) alterWithPointerHuman leader = do COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops pUI <- getsSession spointer let p = squareToMap $ uiToSquare pUI if insideP (0, 0, rWidthMax - 1, rHeightMax - 1) p then alterTileAtPos leader p else failWith "never mind" -- * CloseDir -- | Close nearby open tile; ask for direction, if there is more than one. closeDirHuman :: MonadClientUI m => ActorId -> m (FailOrCmd RequestTimed) closeDirHuman leader = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody leader lvl <- getLevel $ blid b let vPts = vicinityUnsafe $ bpos b openPts = filter (Tile.isClosable coTileSpeedup . at lvl) vPts case openPts of [] -> failSer CloseNothing [o] -> closeTileAtPos leader o _ -> pickPoint leader "close" >>= \case Nothing -> failWith "never mind" Just p -> closeTileAtPos leader p -- | Close tile at given position. closeTileAtPos :: MonadClientUI m => ActorId -> Point -> m (FailOrCmd RequestTimed) closeTileAtPos leader tpos = do COps{coTileSpeedup} <- getsState scops actorCurAndMaxSk <- getsState $ getActorMaxSkills leader b <- getsState $ getActorBody leader alterable <- getsState $ tileAlterable (blid b) tpos lvl <- getLevel $ blid b let alterSkill = Ability.getSk Ability.SkAlter actorCurAndMaxSk t = lvl `at` tpos isOpen = Tile.isClosable coTileSpeedup t isClosed = Tile.isOpenable coTileSpeedup t case (alterable, isClosed, isOpen) of (False, _, _) -> failSer CloseNothing (True, False, False) -> failSer CloseNonClosable (True, True, False) -> failSer CloseClosed (True, True, True) -> error "TileKind content validation" (True, False, True) -> if | tpos `chessDist` bpos b > 1 -> failSer CloseDistant | alterSkill <= 1 -> failSer AlterUnskilled | EM.member tpos $ lfloor lvl -> failSer AlterBlockItem | occupiedBigLvl tpos lvl || occupiedProjLvl tpos lvl -> failSer AlterBlockActor | otherwise -> do msgAddDone True leader tpos "close" return $ Right (ReqAlter tpos) -- | Adds message with proper names. msgAddDone :: MonadClientUI m => Bool -> ActorId -> Point -> Text -> m () msgAddDone mentionTile leader p verb = do COps{cotile} <- getsState scops b <- getsState $ getActorBody leader lvl <- getLevel $ blid b let tname = TK.tname $ okind cotile $ lvl `at` p s = case T.words tname of [] -> "thing" ("open" : xs) -> T.unwords xs _ -> tname object | mentionTile = "the" <+> s | otherwise = "" v = p `vectorToFrom` bpos b dir | v == Vector 0 0 = "underneath" | otherwise = compassText v msgAdd MsgActionComplete $ "You" <+> verb <+> object <+> dir <> "." -- | Prompts user to pick a point. pickPoint :: MonadClientUI m => ActorId -> Text -> m (Maybe Point) pickPoint leader verb = do b <- getsState $ getActorBody leader UIOptions{uVi, uLeftHand} <- getsSession sUIOptions let dirKeys = K.dirAllKey uVi uLeftHand keys = K.escKM : K.leftButtonReleaseKM : map (K.KM K.NoModifier) dirKeys msgAdd MsgPromptGeneric $ "Where to" <+> verb <> "? [movement key] [pointer]" slides <- reportToSlideshow [K.escKM] km <- getConfirms ColorFull keys slides case K.key km of K.LeftButtonRelease -> do pUI <- getsSession spointer let p = squareToMap $ uiToSquare pUI return $ Just p _ -> return $ shift (bpos b) <$> K.handleDir dirKeys km -- * Help -- | Display command help. helpHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) helpHuman cmdSemInCxtOfKM = do ccui@CCUI{coinput, coscreen=ScreenContent{rwidth, rheight, rintroScreen}} <- getsSession sccui fontSetup@FontSetup{..} <- getFontSetup gameModeId <- getsState sgameModeId modeOv <- describeMode True gameModeId curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut let displayTutorialHints = fromMaybe curTutorial overrideTut modeH = ( "Press SPACE or PGDN to advance or ESC to see the map again." , (modeOv, []) ) keyH = keyHelp ccui fontSetup -- This takes a list of paragraphs and returns a list of screens. -- Both paragraph and screen is a list of lines. -- -- This would be faster, but less clear, if paragraphs were stored -- reversed in content. Not worth it, until we have huge manuals -- or run on weak mobiles. Even then, precomputation during -- compilation may be better. -- -- Empty lines may appear at the end of pages, but it's fine, -- it means there is a new section on the next page. packIntoScreens :: [[String]] -> [[String]] -> Int -> [[String]] packIntoScreens [] acc _ = [intercalate [""] (reverse acc)] packIntoScreens ([] : ls) [] _ = -- Ignore empty paragraphs at the start of screen. packIntoScreens ls [] 0 packIntoScreens (l : ls) [] h = assert (h == 0) $ -- If a paragraph, even alone, is longer than screen height, it's split. if length l <= rheight - 3 then packIntoScreens ls [l] (length l) else let (screen, rest) = splitAt (rheight - 3) l in screen : packIntoScreens (rest : ls) [] 0 packIntoScreens (l : ls) acc h = -- The extra @+ 1@ comes from the empty line separating paragraphs, -- as added in @intercalate@. if length l + 1 + h <= rheight - 3 then packIntoScreens ls (l : acc) (length l + 1 + h) else intercalate [""] (reverse acc) : packIntoScreens (l : ls) [] 0 manualScreens = packIntoScreens (snd rintroScreen) [] 0 sideBySide = if isSquareFont monoFont then \(screen1, screen2) -> -- single column, two screens map offsetOverlay $ filter (not . null) [screen1, screen2] else \(screen1, screen2) -> -- two columns, single screen [offsetOverlay screen1 ++ xtranslateOverlay rwidth (offsetOverlay screen2)] listPairs (a : b : rest) = (a, b) : listPairs rest listPairs [a] = [(a, [])] listPairs [] = [] -- Each screen begins with an empty line, to separate the header. manualOvs = map (EM.singleton monoFont) $ concatMap sideBySide $ listPairs $ map ((emptyAttrLine :) . map stringToAL) manualScreens addMnualHeader ov = ( "Showing PLAYING.md (best viewed in the browser)." , (ov, []) ) manualH = map addMnualHeader manualOvs splitHelp (t, okx) = splitOKX fontSetup True rwidth rheight rwidth (textToAS t) [K.spaceKM, K.returnKM, K.escKM] okx sli = toSlideshow fontSetup displayTutorialHints $ concatMap splitHelp $ modeH : keyH ++ manualH -- Thus, the whole help menu corresponds to a single menu of item or lore, -- e.g., shared stash menu. This is especially clear when the shared stash -- menu contains many pages. ekm <- displayChoiceScreen "help" ColorFull True sli [K.spaceKM, K.returnKM, K.escKM] case ekm of Left km | km `elem` [K.escKM, K.spaceKM] -> return $ Left Nothing Left km | km == K.returnKM -> do msgAdd MsgPromptGeneric "Press RET when a command help text is selected to invoke the command." return $ Left Nothing Left km -> case km `M.lookup` bcmdMap coinput of Just (_desc, _cats, cmd) -> cmdSemInCxtOfKM km cmd Nothing -> weaveJust <$> failWith "never mind" Right _slot -> error $ "" `showFailure` ekm -- * Hint -- | Display hint or, if already displayed, display help. hintHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) hintHuman cmdSemInCxtOfKM = do sreportNull <- getsSession sreportNull if sreportNull then do promptMainKeys return $ Left Nothing else helpHuman cmdSemInCxtOfKM -- * Dashboard -- | Display the dashboard. dashboardHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) dashboardHuman cmdSemInCxtOfKM = do CCUI{coinput, coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui fontSetup@FontSetup{..} <- getFontSetup curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut let displayTutorialHints = fromMaybe curTutorial overrideTut offsetCol2 = 3 (ov0, kxs0) = okxsN coinput monoFont propFont offsetCol2 (const False) False CmdDashboard ([], [], []) ([], []) al1 = textToAS "Dashboard" splitHelp (al, okx) = splitOKX fontSetup False rwidth (rheight - 2) rwidth al [K.returnKM, K.escKM] okx sli = toSlideshow fontSetup displayTutorialHints $ splitHelp (al1, (ov0, kxs0)) extraKeys = [K.returnKM, K.escKM] ekm <- displayChoiceScreen "dashboard" ColorFull False sli extraKeys case ekm of Left km -> case km `M.lookup` bcmdMap coinput of _ | km == K.escKM -> weaveJust <$> failWith "never mind" _ | km == K.returnKM -> do msgAdd MsgPromptGeneric "Press RET when a menu name is selected to browse the menu." return $ Left Nothing Just (_desc, _cats, cmd) -> cmdSemInCxtOfKM km cmd Nothing -> weaveJust <$> failWith "never mind" Right _slot -> error $ "" `showFailure` ekm -- * ItemMenu itemMenuHuman :: MonadClientUI m => ActorId -> (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) itemMenuHuman leader cmdSemInCxtOfKM = do COps{corule} <- getsState scops itemSel <- getsSession sitemSel fontSetup@FontSetup{..} <- getFontSetup case itemSel of Just (iid, fromCStore, _) -> do side <- getsClient sside b <- getsState $ getActorBody leader bUI <- getsSession $ getActorUI leader bag <- getsState $ getBodyStoreBag b fromCStore case iid `EM.lookup` bag of Nothing -> weaveJust <$> failWith "no item to open item menu for" Just kit -> do CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui actorCurAndMaxSk <- getsState $ getActorMaxSkills leader itemFull <- getsState $ itemToFull iid localTime <- getsState $ getLocalTime (blid b) found <- getsState $ findIid leader side iid let !_A = assert (not (null found) || fromCStore == CGround `blame` (iid, leader)) () fAlt (aid, (_, store)) = aid /= leader || store /= fromCStore foundAlt = filter fAlt found markParagraphs = rheight >= 45 meleeSkill = Ability.getSk Ability.SkHurtMelee actorCurAndMaxSk partRawActor aid = getsSession (partActor . getActorUI aid) ppLoc aid store = do parts <- ppContainerWownW partRawActor False (CActor aid store) return $! "[" ++ T.unpack (makePhrase parts) ++ "]" dmode = MStore fromCStore foundTexts <- mapM (\(aid, (_, store)) -> ppLoc aid store) foundAlt (ovLab, ovDesc) <- itemDescOverlays markParagraphs meleeSkill dmode iid kit itemFull rwidth let foundPrefix = textToAS $ if null foundTexts then "" else "The item is also in:" ovPrefix = ytranslateOverlay (length ovDesc) $ offsetOverlay $ splitAttrString rwidth rwidth foundPrefix ystart = length ovDesc + length ovPrefix - 1 xstart = textSize monoFont (Color.spaceAttrW32 : attrLine (snd $ last ovPrefix)) foundKeys = map (K.KM K.NoModifier . K.Fun) [1 .. length foundAlt] -- starting from 1! let ks = zip foundKeys foundTexts width = if isSquareFont monoFont then 2 * rwidth else rwidth (ovFoundRaw, kxsFound) = wrapOKX monoFont ystart xstart width ks ovFound = ovPrefix ++ ovFoundRaw report <- getReportUI True CCUI{coinput} <- getsSession sccui mstash <- getsState $ \s -> gstash $ sfactionD s EM.! side curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut let displayTutorialHints = fromMaybe curTutorial overrideTut calmE = calmEnough b actorCurAndMaxSk greyedOut cmd = not calmE && fromCStore == CEqp || mstash == Just (blid b, bpos b) && fromCStore == CGround || case cmd of ByAimMode AimModeCmd{..} -> greyedOut exploration || greyedOut aiming ComposeIfLocal cmd1 cmd2 -> greyedOut cmd1 || greyedOut cmd2 ComposeUnlessError cmd1 cmd2 -> greyedOut cmd1 || greyedOut cmd2 Compose2ndLocal cmd1 cmd2 -> greyedOut cmd1 || greyedOut cmd2 MoveItem stores destCStore _ _ -> fromCStore `notElem` stores || destCStore == CEqp && (not calmE || eqpOverfull b 1) || destCStore == CGround && mstash == Just (blid b, bpos b) Apply{} -> let skill = Ability.getSk Ability.SkApply actorCurAndMaxSk in not $ fromRight False $ permittedApply corule localTime skill calmE (Just fromCStore) itemFull kit Project{} -> let skill = Ability.getSk Ability.SkProject actorCurAndMaxSk in not $ fromRight False $ permittedProject False skill calmE itemFull _ -> False fmt n k h = " " <> T.justifyLeft n ' ' k <> " " <> h offsetCol2 = 11 keyCaption = fmt offsetCol2 "keys" "command" offset = 1 + maxYofOverlay (ovDesc ++ ovFound) (ov0, kxs0) = xytranslateOKX 0 offset $ okxsN coinput monoFont propFont offsetCol2 greyedOut True CmdItemMenu ([], [], ["", keyCaption]) ([], []) t0 = makeSentence [ MU.SubjectVerbSg (partActor bUI) "choose" , "an item", MU.Text $ ppCStoreIn fromCStore ] alRep = foldr (<+:>) [] $ renderReport True report al1 | null alRep = textToAS t0 | otherwise = alRep ++ stringToAS "\n" ++ textToAS t0 splitHelp (al, okx) = splitOKX fontSetup False rwidth (rheight - 2) rwidth al [K.spaceKM, K.escKM] okx sli = toSlideshow fontSetup displayTutorialHints $ splitHelp ( al1 , ( EM.insertWith (++) squareFont ovLab $ EM.insertWith (++) propFont ovDesc $ EM.insertWith (++) monoFont ovFound ov0 -- mono font, because there are buttons , kxsFound ++ kxs0 )) extraKeys = [K.spaceKM, K.escKM] ++ foundKeys recordHistory -- report shown (e.g., leader switch), save to history ekm <- displayChoiceScreen "item menu" ColorFull False sli extraKeys case ekm of Left km -> case km `M.lookup` bcmdMap coinput of _ | km == K.escKM -> weaveJust <$> failWith "never mind" _ | km == K.spaceKM -> chooseItemMenuHuman leader cmdSemInCxtOfKM dmode _ | km `elem` foundKeys -> case km of K.KM{key=K.Fun n} -> do let (newAid, (bNew, newCStore)) = foundAlt !! (n - 1) fact <- getsState $ (EM.! side) . sfactionD let banned = bannedPointmanSwitchBetweenLevels fact if blid bNew /= blid b && banned then weaveJust <$> failSer NoChangeDunLeader else do -- Verbosity not necessary to notice the switch -- and it's explicitly requested, so no surprise. void $ pickLeader False newAid modifySession $ \sess -> sess {sitemSel = Just (iid, newCStore, False)} itemMenuHuman newAid cmdSemInCxtOfKM _ -> error $ "" `showFailure` km Just (_desc, _cats, cmd) -> do modifySession $ \sess -> sess {sitemSel = Just (iid, fromCStore, True)} cmdSemInCxtOfKM km cmd Nothing -> weaveJust <$> failWith "never mind" Right _slot -> error $ "" `showFailure` ekm Nothing -> weaveJust <$> failWith "no item to open item menu for" -- * ChooseItemMenu chooseItemMenuHuman :: MonadClientUI m => ActorId -> (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> ItemDialogMode -> m (Either MError ReqUI) chooseItemMenuHuman leader1 cmdSemInCxtOfKM c1 = do res2 <- chooseItemDialogMode leader1 True c1 case res2 of Right leader2 -> itemMenuHuman leader2 cmdSemInCxtOfKM Left err -> return $ Left $ Just err -- * MainMenu generateMenu :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> FontOverlayMap -> [(Text, HumanCmd, Maybe HumanCmd, Maybe FontOverlayMap)] -> [String] -> String -> m (Either MError ReqUI) generateMenu cmdSemInCxtOfKM blurb kdsRaw gameInfo menuName = do COps{corule} <- getsState scops CCUI{ coinput=InputContent{brevMap} , coscreen=ScreenContent{rheight, rwebAddress} } <- getsSession sccui FontSetup{..} <- getFontSetup let matchKM slot kd@(_, cmd, _, _) = case M.lookup cmd brevMap of Just (km : _) -> (Left km, kd) _ -> (Right slot, kd) kds = zipWith matchKM natSlots kdsRaw bindings = -- key bindings to display let attrCursor = Color.defAttr {Color.bg = Color.HighlightNoneCursor} highAttr ac = ac {Color.acAttr = attrCursor} highW32 = Color.attrCharToW32 . highAttr . Color.attrCharFromW32 markFirst d = markFirstAS $ textToAS d markFirstAS [] = [] markFirstAS (ac : rest) = highW32 ac : rest fmt (ekm, (d, _, _, _)) = (ekm, markFirst d) in map fmt kds generate :: Int -> (KeyOrSlot, AttrString) -> KYX generate y (ekm, binding) = (ekm, (PointUI 0 y, ButtonWidth squareFont (length binding))) okxBindings = ( EM.singleton squareFont $ offsetOverlay $ map (attrStringToAL . snd) bindings , zipWith generate [0..] bindings ) titleLine = rtitle corule ++ " " ++ showVersion (rexeVersion corule) ++ " " titleAndInfo = map stringToAL ([ "" , titleLine ++ "[" ++ rwebAddress ++ "]" , "" ] ++ gameInfo) webButton = ( Left $ K.mkChar '@' -- to start the menu not here , ( PointUI (2 * length titleLine) 1 , ButtonWidth squareFont (2 + length rwebAddress) ) ) okxTitle = ( EM.singleton squareFont $ offsetOverlay titleAndInfo , [webButton] ) okx = xytranslateOKX 2 0 $ sideBySideOKX 2 (length titleAndInfo) okxTitle okxBindings prepareBlurb ovs = let introLen = 1 + maxYofFontOverlayMap ovs start0 = max 0 (rheight - introLen - if isSquareFont propFont then 1 else 2) in EM.map (xytranslateOverlay (-2) (start0 - 2)) ovs -- subtracting 2 from X and Y to negate the indentation in -- @displayChoiceScreenWithRightPane@ returnDefaultOKS = return (prepareBlurb blurb, []) displayInRightPane ekm = case ekm `lookup` kds of Just (_, _, _, mblurbRight) -> case mblurbRight of Nothing -> returnDefaultOKS Just blurbRight -> return (prepareBlurb blurbRight, []) Nothing | ekm == Left (K.mkChar '@') -> returnDefaultOKS Nothing -> error $ "generateMenu: unexpected key:" `showFailure` ekm keys = [K.leftKM, K.rightKM, K.escKM, K.mkChar '@'] loop = do kmkm <- displayChoiceScreenWithRightPaneKMKM displayInRightPane True menuName ColorFull True (menuToSlideshow okx) keys case kmkm of Left (km@(K.KM {key=K.Left}), ekm) -> case ekm `lookup` kds of Just (_, _, Nothing, _) -> loop Just (_, _, Just cmdReverse, _) -> cmdSemInCxtOfKM km cmdReverse Nothing -> weaveJust <$> failWith "never mind" Left (km@(K.KM {key=K.Right}), ekm) -> case ekm `lookup` kds of Just (_, cmd, _, _) -> cmdSemInCxtOfKM km cmd Nothing -> weaveJust <$> failWith "never mind" Left (K.KM {key=K.Char '@'}, _)-> do success <- tryOpenBrowser rwebAddress if success then generateMenu cmdSemInCxtOfKM blurb kdsRaw gameInfo menuName else weaveJust <$> failWith "failed to open web browser" Left (km, _) -> case Left km `lookup` kds of Just (_, cmd, _, _) -> cmdSemInCxtOfKM km cmd Nothing -> weaveJust <$> failWith "never mind" Right slot -> case Right slot `lookup` kds of Just (_, cmd, _, _) -> cmdSemInCxtOfKM K.escKM cmd Nothing -> weaveJust <$> failWith "never mind" loop -- | Display the main menu. mainMenuHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) mainMenuHuman cmdSemInCxtOfKM = do CCUI{coscreen=ScreenContent{rintroScreen}} <- getsSession sccui FontSetup{propFont} <- getFontSetup gameMode <- getGameMode curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut curChal <- getsClient scurChal let offOn b = if b then "on" else "off" -- Key-description-command tuples. kds = [ ("+ setup and start new game>", ChallengeMenu, Nothing, Nothing) , ("@ save and exit to desktop", GameExit, Nothing, Nothing) , ("+ tweak convenience settings>", SettingsMenu, Nothing, Nothing) , ("@ toggle autoplay", AutomateToggle, Nothing, Nothing) , ("@ see command help", Help, Nothing, Nothing) , ("@ switch to dashboard", Dashboard, Nothing, Nothing) , ("^ back to playing", AutomateBack, Nothing, Nothing) ] gameName = MK.mname gameMode displayTutorialHints = fromMaybe curTutorial overrideTut gameInfo = map T.unpack [ "Now playing:" <+> gameName , "" , " with difficulty:" <+> tshow (cdiff curChal) , " cold fish:" <+> offOn (cfish curChal) , " ready goods:" <+> offOn (cgoods curChal) , " lone wolf:" <+> offOn (cwolf curChal) , " finder keeper:" <+> offOn (ckeeper curChal) , " tutorial hints:" <+> offOn displayTutorialHints , "" ] glueLines (l1 : l2 : rest) = if | null l1 -> l1 : glueLines (l2 : rest) | null l2 -> l1 : l2 : glueLines rest | otherwise -> (l1 ++ l2) : glueLines rest glueLines ll = ll backstory | isSquareFont propFont = fst rintroScreen | otherwise = glueLines $ fst rintroScreen backstoryAL = map (stringToAL . dropWhile (== ' ')) backstory blurb = attrLinesToFontMap [(propFont, backstoryAL)] generateMenu cmdSemInCxtOfKM blurb kds gameInfo "main" -- * MainMenuAutoOn -- | Display the main menu and set @swasAutomated@. mainMenuAutoOnHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) mainMenuAutoOnHuman cmdSemInCxtOfKM = do modifySession $ \sess -> sess {swasAutomated = True} mainMenuHuman cmdSemInCxtOfKM -- * MainMenuAutoOff -- | Display the main menu and unset @swasAutomated@. mainMenuAutoOffHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) mainMenuAutoOffHuman cmdSemInCxtOfKM = do modifySession $ \sess -> sess {swasAutomated = False} mainMenuHuman cmdSemInCxtOfKM -- * SettingsMenu -- | Display the settings menu. settingsMenuHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) settingsMenuHuman cmdSemInCxtOfKM = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui UIOptions{uMsgWrapColumn} <- getsSession sUIOptions FontSetup{..} <- getFontSetup markSuspect <- getsClient smarkSuspect markVision <- getsSession smarkVision markSmell <- getsSession smarkSmell noAnim <- getsClient $ fromMaybe False . snoAnim . soptions side <- getsClient sside factDoctrine <- getsState $ gdoctrine . (EM.! side) . sfactionD overrideTut <- getsSession soverrideTut let offOn b = if b then "on" else "off" offOnAll n = case n of 0 -> "none" 1 -> "untried" 2 -> "all" _ -> error $ "" `showFailure` n neverEver n = case n of 0 -> "never" 1 -> "aiming" 2 -> "always" _ -> error $ "" `showFailure` n offOnUnset mb = case mb of Nothing -> "pass" Just b -> if b then "force on" else "force off" tsuspect = "@ mark suspect terrain:" <+> offOnAll markSuspect tvisible = "@ show visible zone:" <+> neverEver markVision tsmell = "@ display smell clues:" <+> offOn markSmell tanim = "@ play animations:" <+> offOn (not noAnim) tdoctrine = "@ squad doctrine:" <+> Ability.nameDoctrine factDoctrine toverride = "@ override tutorial hints:" <+> offOnUnset overrideTut width = if isSquareFont propFont then rwidth `div` 2 else min uMsgWrapColumn (rwidth - 2) textToBlurb t = Just $ attrLinesToFontMap [ ( propFont , splitAttrString width width $ textToAS t ) ] -- Key-description-command-text tuples. kds = [ ( tsuspect, MarkSuspect 1, Just (MarkSuspect (-1)) , textToBlurb "* mark suspect terrain\nThis setting affects the ongoing and the next games. It determines which suspect terrain is marked in special color on the map: none, untried (not searched nor revealed), all. It correspondingly determines which, if any, suspect tiles are considered for mouse go-to, auto-explore and for the command that marks the nearest unexplored position." ) , ( tvisible, MarkVision 1, Just (MarkVision (-1)) , textToBlurb "* show visible zone\nThis setting affects the ongoing and the next games. It determines the conditions under which the area visible to the party is marked on the map via a gray background: never, when aiming, always." ) , ( tsmell, MarkSmell, Just MarkSmell , textToBlurb "* display smell clues\nThis setting affects the ongoing and the next games. It determines whether the map displays any smell traces (regardless of who left them) detected by a party member that can track via smell (as determined by the smell radius skill; not common among humans)." ) , ( tanim, MarkAnim, Just MarkAnim , textToBlurb "* play animations\nThis setting affects the ongoing and the next games. It determines whether important events, such combat, are highlighted by animations. This overrides the corresponding config file setting." ) , ( tdoctrine, Doctrine, Nothing , textToBlurb "* squad doctrine\nThis setting affects the ongoing game, but does not persist to the next games. It determines the behaviour of henchmen (non-pointman characters) in the party and, in particular, if they are permitted to move autonomously or fire opportunistically (assuming they are able to, usually due to rare equipment). This setting has a poor UI that will be improved in the future." ) , ( toverride, OverrideTut 1, Just (OverrideTut (-1)) , textToBlurb "* override tutorial hints\nThis setting affects the ongoing and the next games. It determines whether tutorial hints are, respectively, not overridden with respect to the default game mode setting, forced to be off, forced to be on. Tutorial hints are rendered as pink messages and can afterwards be re-read from message history." ) , ( "^ back to main menu", MainMenu, Nothing, Just EM.empty ) ] gameInfo = map T.unpack [ "Tweak convenience settings:" , "" ] generateMenu cmdSemInCxtOfKM EM.empty kds gameInfo "settings" -- * ChallengeMenu -- | Display the challenge menu. challengeMenuHuman :: MonadClientUI m => (K.KM -> HumanCmd -> m (Either MError ReqUI)) -> m (Either MError ReqUI) challengeMenuHuman cmdSemInCxtOfKM = do cops <- getsState scops CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui UIOptions{uMsgWrapColumn} <- getsSession sUIOptions FontSetup{..} <- getFontSetup svictories <- getsSession svictories snxtScenario <- getsSession snxtScenario nxtChal <- getsClient snxtChal let (gameModeId, gameMode) = nxtGameMode cops snxtScenario victories = case EM.lookup gameModeId svictories of Nothing -> 0 Just cm -> fromMaybe 0 (M.lookup nxtChal cm) star t = if victories > 0 then "*" <> t else t tnextScenario = "@ adventure:" <+> star (MK.mname gameMode) offOn b = if b then "on" else "off" tnextDiff = "@ difficulty level:" <+> tshow (cdiff nxtChal) tnextFish = "@ cold fish (rather hard):" <+> offOn (cfish nxtChal) tnextGoods = "@ ready goods (hard):" <+> offOn (cgoods nxtChal) tnextWolf = "@ lone wolf (very hard):" <+> offOn (cwolf nxtChal) tnextKeeper = "@ finder keeper (hard):" <+> offOn (ckeeper nxtChal) width = if isSquareFont propFont then rwidth `div` 2 else min uMsgWrapColumn (rwidth - 2) widthFull = if isSquareFont propFont then rwidth `div` 2 else rwidth - 2 duplicateEOL '\n' = "\n\n" duplicateEOL c = T.singleton c blurb = Just $ attrLinesToFontMap [ ( propFont , splitAttrString width width $ textFgToAS Color.BrBlack $ T.concatMap duplicateEOL (MK.mdesc gameMode) <> "\n\n" ) , ( propFont , splitAttrString widthFull widthFull $ textToAS $ MK.mrules gameMode <> "\n\n" ) , ( propFont , splitAttrString width width $ textToAS $ T.concatMap duplicateEOL (MK.mreason gameMode) ) ] textToBlurb t = Just $ attrLinesToFontMap [ ( propFont , splitAttrString width width -- not widthFull! $ textToAS t ) ] -- Key-description-command-text tuples. kds = [ ( tnextScenario, GameScenarioIncr 1, Just (GameScenarioIncr (-1)) , blurb ) , ( tnextDiff, GameDifficultyIncr 1, Just (GameDifficultyIncr (-1)) , textToBlurb "* difficulty level\nThis determines the difficulty of survival in the next game that's about to be started. Lower numbers result in easier game. In particular, difficulty below 5 multiplies hitpoints of player characters and difficulty over 5 multiplies hitpoints of their enemies. Game score scales with difficulty.") , ( tnextFish, GameFishToggle, Just GameFishToggle , textToBlurb "* cold fish\nThis challenge mode setting will affect the next game that's about to be started. When on, it makes it impossible for player characters to be healed by actors from other factions (this is a significant restriction in the long crawl adventure).") , ( tnextGoods, GameGoodsToggle, Just GameGoodsToggle , textToBlurb "* ready goods\nThis challenge mode setting will affect the next game that's about to be started. When on, it disables crafting for the player, making the selection of equipment, especially melee weapons, very limited, unless the player has the luck to find the rare powerful ready weapons (this applies only if the chosen adventure supports crafting at all).") , ( tnextWolf, GameWolfToggle, Just GameWolfToggle , textToBlurb "* lone wolf\nThis challenge mode setting will affect the next game that's about to be started. When on, it reduces player's starting actors to exactly one, though later on new heroes may join the party. This makes the game very hard in the long run.") , ( tnextKeeper, GameKeeperToggle, Just GameKeeperToggle , textToBlurb "* finder keeper\nThis challenge mode setting will affect the next game that's about to be started. When on, it completely disables flinging projectiles by the player, which affects not only ranged damage dealing, but also throwing of consumables that buff teammates engaged in melee combat, weaken and distract enemies, light dark corners, etc.") , ( "@ start new game", GameRestart, Nothing, blurb ) , ( "^ back to main menu", MainMenu, Nothing, Nothing ) ] gameInfo = map T.unpack [ "Setup and start new game:" , "" ] generateMenu cmdSemInCxtOfKM EM.empty kds gameInfo "challenge" -- * GameDifficultyIncr gameDifficultyIncr :: MonadClient m => Int -> m () gameDifficultyIncr delta = do nxtDiff <- getsClient $ cdiff . snxtChal let d | nxtDiff + delta > difficultyBound = 1 | nxtDiff + delta < 1 = difficultyBound | otherwise = nxtDiff + delta modifyClient $ \cli -> cli {snxtChal = (snxtChal cli) {cdiff = d} } -- * GameFishToggle gameFishToggle :: MonadClient m => m () gameFishToggle = modifyClient $ \cli -> cli {snxtChal = (snxtChal cli) {cfish = not (cfish (snxtChal cli))} } -- * GameGoodsToggle gameGoodsToggle :: MonadClient m => m () gameGoodsToggle = modifyClient $ \cli -> cli {snxtChal = (snxtChal cli) {cgoods = not (cgoods (snxtChal cli))} } -- * GameWolfToggle gameWolfToggle :: MonadClient m => m () gameWolfToggle = modifyClient $ \cli -> cli {snxtChal = (snxtChal cli) {cwolf = not (cwolf (snxtChal cli))} } -- * GameKeeperToggle gameKeeperToggle :: MonadClient m => m () gameKeeperToggle = modifyClient $ \cli -> cli {snxtChal = (snxtChal cli) {ckeeper = not (ckeeper (snxtChal cli))} } -- * GameScenarioIncr gameScenarioIncr :: MonadClientUI m => Int -> m () gameScenarioIncr delta = do cops <- getsState scops oldScenario <- getsSession snxtScenario let snxtScenario = oldScenario + delta snxtTutorial = MK.mtutorial $ snd $ nxtGameMode cops snxtScenario modifySession $ \sess -> sess {snxtScenario, snxtTutorial} -- * GameRestart & GameQuit data ExitStrategy = Restart | Quit gameExitWithHuman :: MonadClientUI m => ExitStrategy -> m (FailOrCmd ReqUI) gameExitWithHuman exitStrategy = do snxtChal <- getsClient snxtChal cops <- getsState scops noConfirmsGame <- isNoConfirmsGame gameMode <- getGameMode snxtScenario <- getsSession snxtScenario let nxtGameName = MK.mname $ snd $ nxtGameMode cops snxtScenario exitReturn x = return $ Right $ ReqUIGameRestart x snxtChal displayExitMessage diff = displayYesNo ColorBW $ diff <+> "progress of the ongoing" <+> MK.mname gameMode <+> "game will be lost! Are you sure?" ifM (if' noConfirmsGame (return True) -- true case (displayExitMessage $ case exitStrategy of -- false case Restart -> "You just requested a new" <+> nxtGameName <+> "game. The " Quit -> "If you quit, the ")) (exitReturn $ case exitStrategy of -- ifM true case Restart -> let (mainName, _) = T.span (\c -> Char.isAlpha c || c == ' ') nxtGameName in DefsInternal.GroupName $ T.intercalate " " $ take 2 $ T.words mainName Quit -> MK.INSERT_COIN) (rndToActionUI (oneOf -- ifM false case [ "yea, would be a pity to leave them to die" , "yea, a shame to get your team stranded" ]) >>= failWith) ifM :: Monad m => m Bool -> m b -> m b -> m b ifM b t f = do b' <- b; if b' then t else f if' :: Bool -> p -> p -> p if' b t f = if b then t else f -- * GameDrop gameDropHuman :: MonadClientUI m => m ReqUI gameDropHuman = do modifySession $ \sess -> sess {sallNframes = -1} -- hack, but we crash anyway msgAdd MsgPromptGeneric "Interrupt! Trashing the unsaved game. The program exits now." clientPrintUI "Interrupt! Trashing the unsaved game. The program exits now." -- this is not shown by ANSI frontend, but at least shown by sdl2 one return ReqUIGameDropAndExit -- * GameExit gameExitHuman :: Monad m => m ReqUI gameExitHuman = return ReqUIGameSaveAndExit -- * GameSave gameSaveHuman :: MonadClientUI m => m ReqUI gameSaveHuman = do -- Announce before the saving started, since it can take a while. msgAdd MsgInnerWorkSpam "Saving game backup." return ReqUIGameSave -- * Doctrine -- Note that the difference between seek-target and follow-the-leader doctrine -- can influence even a faction with passive actors. E.g., if a passive actor -- has an extra active skill from equipment, he moves every turn. doctrineHuman :: MonadClientUI m => m (FailOrCmd ReqUI) doctrineHuman = do fid <- getsClient sside fromT <- getsState $ gdoctrine . (EM.! fid) . sfactionD let toT = if fromT == maxBound then minBound else succ fromT go <- displaySpaceEsc ColorFull $ "(Beware, work in progress!)" <+> "Current squad doctrine is '" <> Ability.nameDoctrine fromT <> "'" <+> "(" <> Ability.describeDoctrine fromT <> ")." <+> "Switching doctrine to '" <> Ability.nameDoctrine toT <> "'" <+> "(" <> Ability.describeDoctrine toT <> ")." <+> "This clears targets of all non-pointmen teammates." <+> "New targets will be picked according to new doctrine." if not go then failWith "squad doctrine change canceled" else return $ Right $ ReqUIDoctrine toT -- * Automate automateHuman :: MonadClientUI m => m (FailOrCmd ReqUI) automateHuman = do clearAimMode proceed <- displayYesNo ColorBW "Do you really want to cede control to AI?" if not proceed then failWith "automation canceled" else return $ Right ReqUIAutomate -- * AutomateToggle automateToggleHuman :: MonadClientUI m => m (FailOrCmd ReqUI) automateToggleHuman = do swasAutomated <- getsSession swasAutomated if swasAutomated then failWith "automation canceled" else automateHuman -- * AutomateBack automateBackHuman :: MonadClientUI m => m (Either MError ReqUI) automateBackHuman = do swasAutomated <- getsSession swasAutomated return $! if swasAutomated then Right ReqUIAutomate else Left Nothing LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/HandleHumanLocalM.hs0000644000000000000000000015366407346545000024323 0ustar0000000000000000-- | Semantics of "Game.LambdaHack.Client.UI.HumanCmd" -- client commands that do not return server requests,, -- but only change internal client state. -- None of such commands takes game time. module Game.LambdaHack.Client.UI.HandleHumanLocalM ( -- * Meta commands macroHuman, macroHumanTransition -- * Local commands , chooseItemHuman, chooseItemDialogMode , chooseItemProjectHuman, chooseItemApplyHuman , psuitReq, triggerSymbols, pickLeaderHuman, pickLeaderWithPointerHuman , pointmanCycleHuman, pointmanCycleLevelHuman , selectActorHuman, selectNoneHuman, selectWithPointerHuman , repeatHuman, repeatHumanTransition , repeatLastHuman, repeatLastHumanTransition , recordHuman, recordHumanTransition, allHistoryHuman , markVisionHuman, markSmellHuman, markSuspectHuman, markAnimHuman , overrideTutHuman , printScreenHuman -- * Commands specific to aiming , cancelHuman, acceptHuman, detailCycleHuman , clearTargetIfItemClearHuman, itemClearHuman , moveXhairHuman, aimTgtHuman, aimFloorHuman, aimEnemyHuman, aimItemHuman , aimAscendHuman, epsIncrHuman , xhairUnknownHuman, xhairItemHuman, xhairStairHuman , xhairPointerFloorHuman, xhairPointerMuteHuman, xhairPointerEnemyHuman , aimPointerFloorHuman, aimPointerEnemyHuman #ifdef EXPOSE_INTERNAL -- * Internal operations , chooseItemDialogModeLore, projectCheck , posFromXhair, permittedApplyClient, endAiming, endAimingMsg , flashAiming #endif -- * Operations both internal and used in unit tests , permittedProjectClient, xhairLegalEps ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Either import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Map.Strict as M import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.BfsM import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Animation import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.DrawM import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.FrameM import Game.LambdaHack.Client.UI.HandleHelperM import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import Game.LambdaHack.Client.UI.InventoryM import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.FactionKind as FK import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.ModeKind as MK import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs -- * Macro macroHuman :: MonadClientUI m => [String] -> m () macroHuman ks = do modifySession $ \sess -> let kms = K.mkKM <$> ks (smacroFrameNew, smacroStackMew) = macroHumanTransition kms (smacroFrame sess) (smacroStack sess) in sess { smacroFrame = smacroFrameNew , smacroStack = smacroStackMew } msgAdd MsgMacroOperation $ "Macro activated:" <+> T.pack (unwords ks) -- | Push a new macro frame to the stack whenever repeating a macro. macroHumanTransition :: [K.KM] -> KeyMacroFrame -> [KeyMacroFrame] -> (KeyMacroFrame, [KeyMacroFrame]) macroHumanTransition kms macroFrame macroFrames = let smacroFrameNew = emptyMacroFrame {keyPending = KeyMacro kms} in (smacroFrameNew, macroFrame : macroFrames) -- * ChooseItem -- | Display items from a given container store and possibly let the user -- chose one. chooseItemHuman :: MonadClientUI m => ActorId -> ItemDialogMode -> m MError chooseItemHuman leader c = either Just (const Nothing) <$> chooseItemDialogMode leader False c chooseItemDialogModeLore :: forall m . MonadClientUI m => m (Maybe ResultItemDialogMode) chooseItemDialogModeLore = do schosenLoreOld <- getsSession schosenLore (inhabitants, embeds) <- case schosenLoreOld of ChosenLore inh emb -> return (inh, emb) ChosenNothing -> computeChosenLore bagHuge <- getsState $ EM.map (const quantSingle) . sitemD itemToF <- getsState $ flip itemToFull ItemRoles itemRoles <- getsSession sroles let rlore :: ItemId -> SLore -> ChosenLore -> m (Maybe ResultItemDialogMode) rlore iid slore schosenLore = do let itemRole = itemRoles EM.! slore bagAll = EM.filterWithKey (\iid2 _ -> iid2 `ES.member` itemRole) bagHuge modifySession $ \sess -> sess {schosenLore} let iids = sortIids itemToF $ EM.assocs bagAll slot = toEnum $ fromMaybe (error $ "" `showFailure` (iid, iids)) $ elemIndex iid $ map fst iids return $ Just $ RLore slore slot iids case inhabitants of (_, b) : rest -> do let iid = btrunk b arItem <- getsState $ aspectRecordFromIid iid let slore | not $ bproj b = STrunk | IA.checkFlag Ability.Blast arItem = SBlast | otherwise = SItem rlore iid slore (ChosenLore rest embeds) [] -> case embeds of (iid, _) : rest -> do let slore = SEmbed rlore iid slore (ChosenLore inhabitants rest) [] -> do modifySession $ \sess -> sess {schosenLore = ChosenNothing} return Nothing chooseItemDialogMode :: forall m. MonadClientUI m => ActorId -> Bool -> ItemDialogMode -> m (FailOrCmd ActorId) chooseItemDialogMode leader0 permitLoreCycle c = do CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui FontSetup{propFont} <- getFontSetup side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD (ggi, loreFound) <- do mggiLore <- if permitLoreCycle && c == MLore SItem then chooseItemDialogModeLore else return Nothing case mggiLore of Just rlore -> return (Right rlore, True) Nothing -> do ggi <- getStoreItem leader0 c return (ggi, False) -- Pointman could have been changed in @getStoreItem@ above. mleader <- getsClient sleader -- When run inside a test, without mleader, assume leader not changed. let leader = fromMaybe leader0 mleader recordHistory -- item chosen, wipe out already shown msgs actorCurAndMaxSk <- getsState $ getActorMaxSkills leader let meleeSkill = Ability.getSk Ability.SkHurtMelee actorCurAndMaxSk bUI <- getsSession $ getActorUI leader case ggi of Right result -> case result of RStore fromCStore [iid] -> do modifySession $ \sess -> sess {sitemSel = Just (iid, fromCStore, False)} return $ Right leader RStore{} -> error $ "" `showFailure` result ROwned iid -> do found <- getsState $ findIid leader side iid let (newAid, bestStore) = case leader `lookup` found of Just (_, store) -> (leader, store) Nothing -> case found of (aid, (_, store)) : _ -> (aid, store) [] -> error $ "" `showFailure` result modifySession $ \sess -> sess {sitemSel = Just (iid, bestStore, False)} arena <- getArenaUI b2 <- getsState $ getActorBody newAid let banned = bannedPointmanSwitchBetweenLevels fact if | newAid == leader -> return $ Right leader | blid b2 /= arena && banned -> failSer NoChangeDunLeader | otherwise -> do -- We switch leader only here, not when processing results -- of lore screens, because lore is only about inspecting items. -- -- This is a bit too verbose in aiming mode, but verbosity -- here is good to turn player's attention to the switch. void $ pickLeader True newAid return $ Right newAid RLore slore slot iids -> do let promptFun _ itemFull _ = case slore of SBody -> let blurb = if IA.checkFlag Ability.Condition $ aspectRecordFull itemFull then "condition" else "organ" in makeSentence [partActor bUI, "is aware of" ,MU.AW blurb] _ -> makeSentence [ MU.SubjectVerbSg (partActor bUI) "remember" , MU.AW $ MU.Text (headingSLore slore) ] schosenLore <- getsSession schosenLore let lorePending = loreFound && case schosenLore of ChosenLore [] [] -> False _ -> True renderOneItem = okxItemLoreMsg promptFun meleeSkill (MLore slore) iids extraKeys = [K.mkChar '~' | lorePending] slotBound = length iids - 1 km <- displayOneMenuItem renderOneItem extraKeys slotBound slot case K.key km of K.Space -> do modifySession $ \sess -> sess {schosenLore = ChosenNothing} chooseItemDialogMode leader False (MLore slore) K.Char '~' -> chooseItemDialogMode leader True c K.Esc -> do modifySession $ \sess -> sess {schosenLore = ChosenNothing} failWith "never mind" _ -> error $ "" `showFailure` km RSkills slot0 -> do -- This can be used in the future, e.g., to increase stats from -- level-up stat points, so let's keep it even if it shows -- no extra info compared to right pane display in menu. let renderOneItem slot = do (prompt2, attrString) <- skillCloseUp leader slot let ov0 = EM.singleton propFont $ offsetOverlay $ splitAttrString rwidth rwidth attrString msgAdd MsgPromptGeneric prompt2 return (ov0, []) extraKeys = [] slotBound = length skillsInDisplayOrder - 1 km <- displayOneMenuItem renderOneItem extraKeys slotBound slot0 case K.key km of K.Space -> chooseItemDialogMode leader False MSkills K.Esc -> failWith "never mind" _ -> error $ "" `showFailure` km RPlaces slot0 -> do COps{coplace} <- getsState scops soptions <- getsClient soptions -- This is computed just once for the whole series of up and down arrow -- navigations, avoid quadratic blowup. places <- getsState $ EM.assocs . placesFromState coplace (sexposePlaces soptions) let renderOneItem slot = do (prompt2, blurbs) <- placeCloseUp places (sexposePlaces soptions) slot let splitText = splitAttrString rwidth rwidth ov0 = attrLinesToFontMap $ map (second $ concatMap splitText) blurbs msgAdd MsgPromptGeneric prompt2 return (ov0, []) extraKeys = [] slotBound = length places - 1 km <- displayOneMenuItem renderOneItem extraKeys slotBound slot0 case K.key km of K.Space -> chooseItemDialogMode leader False MPlaces K.Esc -> failWith "never mind" _ -> error $ "" `showFailure` km RFactions slot0 -> do sroles <- getsSession sroles factions <- getsState $ factionsFromState sroles let renderOneItem slot = do (prompt2, blurbs) <- factionCloseUp factions slot let splitText = splitAttrString rwidth rwidth ov0 = attrLinesToFontMap $ map (second $ concatMap splitText) blurbs msgAdd MsgPromptGeneric prompt2 return (ov0, []) extraKeys = [] slotBound = length factions - 1 km <- displayOneMenuItem renderOneItem extraKeys slotBound slot0 case K.key km of K.Space -> chooseItemDialogMode leader False MFactions K.Esc -> failWith "never mind" _ -> error $ "" `showFailure` km RModes slot0 -> do let displayOneMenuItemBig :: (MenuSlot -> m OKX) -> [K.KM] -> Int -> MenuSlot -> m K.KM displayOneMenuItemBig renderOneItem extraKeys slotBound slot = do let keys = [K.spaceKM, K.escKM] ++ [K.upKM | fromEnum slot > 0] ++ [K.downKM | fromEnum slot < slotBound] ++ extraKeys okx <- renderOneItem slot -- Here it differs from @displayOneMenuItem@, slides <- overlayToSlideshow rheight keys okx ekm2 <- displayChoiceScreen "" ColorFull True slides keys let km = either id (error $ "" `showFailure` ekm2) ekm2 -- Here it stops differing. case K.key km of K.Up -> displayOneMenuItemBig renderOneItem extraKeys slotBound $ pred slot K.Down -> displayOneMenuItemBig renderOneItem extraKeys slotBound $ succ slot _ -> return km COps{comode} <- getsState scops svictories <- getsSession svictories nxtChal <- getsClient snxtChal -- mark victories only for current difficulty let f !acc _p !i !a = (i, a) : acc campaignModes = ofoldlGroup' comode MK.CAMPAIGN_SCENARIO f [] renderOneItem slot = do let (gameModeId, gameMode) = campaignModes !! fromEnum slot ov0 <- describeMode False gameModeId let victories = case EM.lookup gameModeId svictories of Nothing -> 0 Just cm -> fromMaybe 0 (M.lookup nxtChal cm) verb = if victories > 0 then "remember" else "forsee" prompt2 = makeSentence [ MU.SubjectVerbSg "you" verb , MU.Text $ "the '" <> MK.mname gameMode <> "' adventure" ] msgAdd MsgPromptGeneric prompt2 return (ov0, []) extraKeys = [] slotBound = length campaignModes - 1 km <- displayOneMenuItemBig renderOneItem extraKeys slotBound slot0 case K.key km of K.Space -> chooseItemDialogMode leader False MModes K.Esc -> failWith "never mind" _ -> error $ "" `showFailure` km Left err -> failWith err -- * ChooseItemProject chooseItemProjectHuman :: forall m. (MonadClient m, MonadClientUI m) => ActorId -> [HumanCmd.TriggerItem] -> m MError chooseItemProjectHuman leader ts = do b <- getsState $ getActorBody leader mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b let overStash = mstash == Just (blid b, bpos b) storesBase = [CStash, CEqp] stores | overStash = storesBase ++ [CGround] | otherwise = CGround : storesBase (verb1, object1) = case ts of [] -> ("aim", "item") tr : _ -> (HumanCmd.tiverb tr, HumanCmd.tiobject tr) verb = makePhrase [verb1] triggerSyms = triggerSymbols ts mpsuitReq <- psuitReq leader case mpsuitReq of -- If xhair aim invalid, no item is considered a (suitable) missile. Left err -> failMsg err Right psuitReqFun -> do itemSel <- getsSession sitemSel case itemSel of Just (_, _, True) -> return Nothing Just (iid, fromCStore, False) -> do -- We don't validate vs @ts@ here, because player has selected -- this item, so he knows what he's doing (unless really absurd). itemFull <- getsState $ itemToFull iid bag <- getsState $ getBodyStoreBag b fromCStore case iid `EM.lookup` bag of Just _ | isRight (psuitReqFun itemFull) -> -- The player knows what he's doing. We warn him about range -- and experimenting with unknown precious items is fine. return Nothing _ -> do modifySession $ \sess -> sess {sitemSel = Nothing} chooseItemProjectHuman leader ts Nothing -> do let psuit = return $ SuitsSomething $ \_ itemFull _kit -> -- Here the player does not explicitly pick an item, -- so we may exclude precious unknown items, etc. either (const False) snd (psuitReqFun itemFull) && (null triggerSyms || IK.isymbol (itemKind itemFull) `elem` triggerSyms) prompt = makePhrase ["What", object1, "to"] promptGeneric = "What to" ggi <- getGroupItem leader psuit prompt promptGeneric verb "fling" stores case ggi of Right (fromCStore, iid) -> do modifySession $ \sess -> sess {sitemSel = Just (iid, fromCStore, False)} return Nothing Left err -> failMsg err permittedProjectClient :: MonadClientUI m => ActorId -> m (ItemFull -> Either ReqFailure Bool) permittedProjectClient leader = do actorCurAndMaxSk <- getsState $ getActorMaxSkills leader b <- getsState $ getActorBody leader let skill = Ability.getSk Ability.SkProject actorCurAndMaxSk calmE = calmEnough b actorCurAndMaxSk return $ permittedProject False skill calmE projectCheck :: MonadClientUI m => ActorId -> Point -> m (Maybe ReqFailure) projectCheck leader tpos = do COps{coTileSpeedup} <- getsState scops eps <- getsClient seps sb <- getsState $ getActorBody leader let lid = blid sb spos = bpos sb -- Not @ScreenContent@, because not drawing here. case bresenhamsLineAlgorithm eps spos tpos of Nothing -> return $ Just ProjectAimOnself Just [] -> error $ "project from the edge of level" `showFailure` (spos, tpos, sb) Just (pos : _) -> do lvl <- getLevel lid let t = lvl `at` pos if not $ Tile.isWalkable coTileSpeedup t then return $ Just ProjectBlockTerrain else if occupiedBigLvl pos lvl then return $ Just ProjectBlockActor else return Nothing -- | Check whether one is permitted to aim (for projecting) at a target. -- The check is stricter for actor targets, assuming the player simply wants -- to hit a single actor. In order to fine tune trick-shots, e.g., piercing -- many actors, other aiming modes should be used. -- Returns a different @seps@ if needed to reach the target. -- -- Note: Simple Perception check is not enough for the check, -- e.g., because the target actor can be obscured by a glass wall. xhairLegalEps :: MonadClientUI m => ActorId -> m (Either Text Int) xhairLegalEps leader = do cops@COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops b <- getsState $ getActorBody leader lidV <- viewedLevelUI let !_A = assert (lidV == blid b) () findNewEps onlyFirst pos = do lvl <- getLevel (blid b) oldEps <- getsClient seps return $! case makeLine onlyFirst b pos oldEps cops lvl of Just newEps -> Right newEps Nothing -> Left $ if onlyFirst then "aiming blocked at the first step" else "aiming line blocked somewhere" xhair <- getsSession sxhair case xhair of Nothing -> return $ Left "no aim designated" Just (TEnemy a) -> do body <- getsState $ getActorBody a let pos = bpos body if blid body == lidV then findNewEps False pos else return $ Left "can't fling at an enemy on remote level" Just (TNonEnemy a) -> do body <- getsState $ getActorBody a let pos = bpos body if blid body == lidV then findNewEps False pos else return $ Left "can't fling at a non-enemy on remote level" Just (TPoint TEnemyPos{} _ _) -> return $ Left "selected opponent not visible" Just (TPoint _ lid pos) -> if lid == lidV then findNewEps True pos -- @True@ to help pierce many foes, etc. else return $ Left "can't fling at a target on remote level" Just (TVector v) -> do -- Not @ScreenContent@, because not drawing here. let shifted = shiftBounded rWidthMax rHeightMax (bpos b) v if shifted == bpos b && v /= Vector 0 0 then return $ Left "selected translation is void" else findNewEps True shifted -- @True@, because the goal is vague anyway posFromXhair :: (MonadClient m, MonadClientUI m) => ActorId -> m (Either Text Point) posFromXhair leader = do canAim <- xhairLegalEps leader case canAim of Right newEps -> do -- Modify @seps@, permanently. modifyClient $ \cli -> cli {seps = newEps} mxhairPos <- mxhairToPos case mxhairPos of Nothing -> error $ "" `showFailure` mxhairPos Just pos -> do munit <- projectCheck leader pos case munit of Nothing -> return $ Right pos Just reqFail -> return $ Left $ showReqFailure reqFail Left cause -> return $ Left cause -- | On top of `permittedProjectClient`, it also checks legality -- of aiming at the target and projection range. It also modifies @eps@. psuitReq :: (MonadClient m, MonadClientUI m) => ActorId -> m (Either Text (ItemFull -> Either ReqFailure (Point, Bool))) psuitReq leader = do b <- getsState $ getActorBody leader lidV <- viewedLevelUI if lidV /= blid b then return $ Left "can't fling on remote level" else do mpos <- posFromXhair leader p <- permittedProjectClient leader case mpos of Left err -> return $ Left err Right pos -> return $ Right $ \itemFull -> case p itemFull of Left err -> Left err Right False -> Right (pos, False) Right True -> let arItem = aspectRecordFull itemFull in Right (pos, 1 + IA.totalRange arItem (itemKind itemFull) >= chessDist (bpos b) pos) -- $setup -- >>> import Game.LambdaHack.Definition.DefsInternal -- | -- >>> let trigger1 = HumanCmd.TriggerItem{tiverb="verb", tiobject="object", tisymbols=[toContentSymbol 'a', toContentSymbol 'b']} -- >>> let trigger2 = HumanCmd.TriggerItem{tiverb="verb2", tiobject="object2", tisymbols=[toContentSymbol 'c']} -- >>> triggerSymbols [trigger1, trigger2] -- "abc" -- -- >>> triggerSymbols [] -- "" triggerSymbols :: [HumanCmd.TriggerItem] -> [ContentSymbol IK.ItemKind] triggerSymbols [] = [] triggerSymbols (HumanCmd.TriggerItem{tisymbols} : ts) = tisymbols ++ triggerSymbols ts -- * ChooseItemApply chooseItemApplyHuman :: forall m. MonadClientUI m => ActorId -> [HumanCmd.TriggerItem] -> m MError chooseItemApplyHuman leader ts = do b <- getsState $ getActorBody leader mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b let overStash = mstash == Just (blid b, bpos b) storesBase = [CStash, CEqp, COrgan] stores | overStash = storesBase ++ [CGround] | otherwise = CGround : storesBase (verb1, object1) = case ts of [] -> ("trigger", "item") tr : _ -> (HumanCmd.tiverb tr, HumanCmd.tiobject tr) verb = makePhrase [verb1] triggerSyms = triggerSymbols ts prompt = makePhrase ["What", object1, "to"] promptGeneric = "What to" itemSel <- getsSession sitemSel case itemSel of Just (_, _, True) -> return Nothing Just (iid, fromCStore, False) -> do -- We don't validate vs @ts@ here, because player has selected -- this item, so he knows what he's doing (unless really absurd). itemFull <- getsState $ itemToFull iid bag <- getsState $ getBodyStoreBag b fromCStore mp <- permittedApplyClient leader case iid `EM.lookup` bag of Just kit | fromRight False (mp (Just fromCStore) itemFull kit) -> return Nothing _ -> do modifySession $ \sess -> sess {sitemSel = Nothing} chooseItemApplyHuman leader ts Nothing -> do let psuit :: m Suitability psuit = do mp <- permittedApplyClient leader return $ SuitsSomething $ \cstore itemFull kit -> fromRight False (mp cstore itemFull kit) && (null triggerSyms || IK.isymbol (itemKind itemFull) `elem` triggerSyms) ggi <- getGroupItem leader psuit prompt promptGeneric verb "trigger" stores case ggi of Right (fromCStore, iid) -> do modifySession $ \sess -> sess {sitemSel = Just (iid, fromCStore, False)} return Nothing Left err -> failMsg err permittedApplyClient :: MonadClientUI m => ActorId -> m (Maybe CStore -> ItemFull -> ItemQuant -> Either ReqFailure Bool) permittedApplyClient leader = do COps{corule} <- getsState scops actorCurAndMaxSk <- getsState $ getActorMaxSkills leader b <- getsState $ getActorBody leader let skill = Ability.getSk Ability.SkApply actorCurAndMaxSk calmE = calmEnough b actorCurAndMaxSk localTime <- getsState $ getLocalTime (blid b) return $ permittedApply corule localTime skill calmE -- * PickLeader pickLeaderHuman :: MonadClientUI m => Int -> m MError pickLeaderHuman k = do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD arena <- getArenaUI sactorUI <- getsSession sactorUI mhero <- getsState $ tryFindHeroK sactorUI side k allOurs <- getsState $ fidActorNotProjGlobalAssocs side -- not only on level let allOursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) allOurs hs = sortOn keySelected allOursUI mactor = case drop k hs of [] -> Nothing (aid, b, _) : _ -> Just (aid, b) mchoice = if FK.fhasGender (gkind fact) then mhero else mactor banned = bannedPointmanSwitchBetweenLevels fact case mchoice of Nothing -> failMsg "no such member of the party" Just (aid, b) | blid b /= arena && banned -> failMsg $ showReqFailure NoChangeDunLeader | otherwise -> do void $ pickLeader True aid return Nothing -- * PickLeaderWithPointer pickLeaderWithPointerHuman :: MonadClientUI m => ActorId -> m MError pickLeaderWithPointerHuman = pickLeaderWithPointer -- * PointmanCycle -- | Switch current pointman to the next on the viewed level, if any, wrapping. pointmanCycleLevelHuman :: MonadClientUI m => ActorId -> Direction -> m MError pointmanCycleLevelHuman leader = pointmanCycleLevel leader True -- * PointmanBack -- | Switch current pointman to the previous in the whole dungeon, wrapping. pointmanCycleHuman :: MonadClientUI m => ActorId -> Direction -> m MError pointmanCycleHuman leader = pointmanCycle leader True -- * SelectActor selectActorHuman :: MonadClientUI m => ActorId -> m () selectActorHuman leader = do bodyUI <- getsSession $ getActorUI leader wasMemeber <- getsSession $ ES.member leader . sselected let upd = if wasMemeber then ES.delete leader -- already selected, deselect instead else ES.insert leader modifySession $ \sess -> sess {sselected = upd $ sselected sess} let subject = partActor bodyUI msgAdd MsgActionAlert $ makeSentence [subject, if wasMemeber then "deselected" else "selected"] -- * SelectNone selectNoneHuman :: MonadClientUI m => m () selectNoneHuman = do side <- getsClient sside lidV <- viewedLevelUI oursIds <- getsState $ fidActorRegularIds side lidV let ours = ES.fromDistinctAscList oursIds oldSel <- getsSession sselected let wasNone = ES.null $ ES.intersection ours oldSel upd = if wasNone then ES.union -- already all deselected; select all instead else ES.difference modifySession $ \sess -> sess {sselected = upd (sselected sess) ours} let subject = "all party members on the level" msgAdd MsgActionAlert $ makeSentence [subject, if wasNone then "selected" else "deselected"] -- * SelectWithPointer selectWithPointerHuman :: MonadClientUI m => m MError selectWithPointerHuman = do COps{corule=RuleContent{rHeightMax}} <- getsState scops lidV <- viewedLevelUI -- Not @ScreenContent@, because not drawing here. side <- getsClient sside ours <- getsState $ filter (not . bproj . snd) . actorAssocs (== side) lidV sactorUI <- getsSession sactorUI let oursUI = map (\(aid, b) -> (aid, b, sactorUI EM.! aid)) ours viewed = sortOn keySelected oursUI pUI <- getsSession spointer let p@(Point px py) = squareToMap $ uiToSquare pUI -- Select even if no space in status line for the actor's symbol. if | py == rHeightMax + 1 && px == 0 -> selectNoneHuman >> return Nothing | py == rHeightMax + 1 -> case drop (px - 1) viewed of [] -> failMsg "not pointing at an actor" (aid, _, _) : _ -> selectActorHuman aid >> return Nothing | otherwise -> case find (\(_, b) -> bpos b == p) ours of Nothing -> failMsg "not pointing at an actor" Just (aid, _) -> selectActorHuman aid >> return Nothing -- * Repeat -- Note that walk followed by repeat should not be equivalent to run, -- because the player can really use a command that does not stop -- at terrain change or when walking over items. repeatHuman :: MonadClientUI m => Int -> m () repeatHuman n = modifySession $ \sess -> let (smacroFrameNew, smacroStackMew) = repeatHumanTransition n (smacroFrame sess) (smacroStack sess) in sess { smacroFrame = smacroFrameNew , smacroStack = smacroStackMew } repeatHumanTransition :: Int -> KeyMacroFrame -> [KeyMacroFrame] -> (KeyMacroFrame, [KeyMacroFrame]) repeatHumanTransition n macroFrame macroFrames = let kms = concat . replicate n . unKeyMacro . fromRight mempty $ keyMacroBuffer macroFrame in macroHumanTransition kms macroFrame macroFrames -- * RepeatLast -- Note that walk followed by repeat should not be equivalent to run, -- because the player can really use a command that does not stop -- at terrain change or when walking over items. repeatLastHuman :: MonadClientUI m => Int -> m () repeatLastHuman n = modifySession $ \sess -> sess {smacroFrame = repeatLastHumanTransition n (smacroFrame sess) } repeatLastHumanTransition :: Int -> KeyMacroFrame -> KeyMacroFrame repeatLastHumanTransition n macroFrame = let macro = KeyMacro . concat . replicate n . maybeToList $ keyLast macroFrame in macroFrame { keyPending = macro <> keyPending macroFrame } -- * Record -- | Starts and stops recording of macros. recordHuman :: MonadClientUI m => m () recordHuman = do smacroFrameOld <- getsSession smacroFrame let (smacroFrameNew, msg) = recordHumanTransition smacroFrameOld modifySession $ \sess -> sess {smacroFrame = smacroFrameNew} macroStack <- getsSession smacroStack unless (T.null msg || not (null macroStack)) $ msgAdd MsgPromptGeneric msg recordHumanTransition :: KeyMacroFrame -> (KeyMacroFrame, Text) recordHumanTransition macroFrame = let (buffer, msg) = case keyMacroBuffer macroFrame of Right _ -> -- Start recording in-game macro. (Left [], "Recording a macro. Stop recording with the same key.") Left xs -> -- Stop recording in-game macro. (Right . KeyMacro . reverse $ xs, "Macro recording stopped.") smacroFrameNew = macroFrame {keyMacroBuffer = buffer} in (smacroFrameNew, msg) -- * AllHistory allHistoryHuman :: forall m. MonadClientUI m => m () allHistoryHuman = do CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui history <- getsSession shistory arena <- getArenaUI localTime <- getsState $ getLocalTime arena global <- getsState stime FontSetup{..} <- getFontSetup let renderedHistoryRaw = renderHistory history histLenRaw = length renderedHistoryRaw placeholderLine = textFgToAS Color.BrBlack "Newest_messages_are_at_the_bottom._Press_END_to_get_there." placeholderCount = (- histLenRaw `mod` (rheight - 4)) `mod` (rheight - 4) renderedHistory = replicate placeholderCount placeholderLine ++ renderedHistoryRaw histLen = placeholderCount + histLenRaw splitRow as = let (tLab, tDesc) = span (/= Color.spaceAttrW32) as labLen = textSize monoFont tLab par1 = case filter (/= emptyAttrLine) $ linesAttr tDesc of [] -> emptyAttrLine [l] -> l ls -> attrStringToAL $ intercalate [Color.spaceAttrW32] $ map attrLine ls in (attrStringToAL tLab, (labLen, par1)) (tsLab, tsDesc) = unzip $ map splitRow renderedHistory ovs = EM.insertWith (++) monoFont (offsetOverlay tsLab) $ EM.singleton propFont $ offsetOverlayX tsDesc turnsGlobal = global `timeFitUp` timeTurn turnsLocal = localTime `timeFitUp` timeTurn msg = makeSentence [ "You survived for" , MU.CarWs turnsGlobal "half-second turn" , "(this level:" , MU.Car turnsLocal <> ")" ] kxs = [ (Right sn, ( PointUI 0 (fromEnum sn) , ButtonWidth propFont 1000 )) | sn <- take histLen natSlots ] msgAdd MsgPromptGeneric msg let keysAllHistory = K.returnKM #ifndef USE_JSFILE : K.mkChar '.' #endif : [K.spaceKM, K.escKM] slides <- overlayToSlideshow (rheight - 2) keysAllHistory (ovs, kxs) let historyLines = case reverse $ concatMap snd $ slideshow slides of (Left{}, _) : rest -> rest -- don't count the @--more--@ line l -> l maxIx = length historyLines - 1 - length keysAllHistory menuName = "history" modifySession $ \sess -> sess {smenuIxMap = M.insert menuName maxIx $ smenuIxMap sess} let displayAllHistory = do ekm <- displayChoiceScreen menuName ColorFull False slides keysAllHistory case ekm of Left km | km == K.mkChar '.' -> do let t = T.unlines $ map (T.pack . map Color.charFromW32) renderedHistoryRaw path <- dumpTextFile t "history.txt" msgAdd MsgPromptGeneric $ "All of history dumped to file" <+> T.pack path <> "." Left km | km == K.escKM -> msgAdd MsgPromptGeneric "Try to survive a few seconds more, if you can." Left km | km == K.spaceKM -> msgAdd MsgPromptGeneric "Steady on." Left km | km == K.returnKM -> msgAdd MsgPromptGeneric "Press RET when history message selected to see it in full." Right slot -> displayOneReport $ toEnum $ max 0 $ fromEnum slot - placeholderCount _ -> error $ "" `showFailure` ekm displayOneReport :: MenuSlot -> m () displayOneReport slot0 = do let renderOneItem slot = do let timeReport = case drop (fromEnum slot) renderedHistoryRaw of [] -> error $ "" `showFailure` slot tR : _ -> tR markParagraph c | Color.charFromW32 c == '\n' = [c, c] markParagraph c = [c] reportWithParagraphs = concatMap markParagraph timeReport (ovLab, ovDesc) = labDescOverlay monoFont rwidth reportWithParagraphs ov0 = EM.insertWith (++) monoFont ovLab $ EM.singleton propFont ovDesc prompt = makeSentence [ "the", MU.Ordinal $ fromEnum slot + 1 , "most recent record follows" ] msgAdd MsgPromptGeneric prompt return (ov0, []) extraKeys = [] slotBound = histLenRaw - 1 km <- displayOneMenuItem renderOneItem extraKeys slotBound slot0 case K.key km of K.Space -> displayAllHistory K.Esc -> msgAdd MsgPromptGeneric "Try to learn from your previous mistakes." _ -> error $ "" `showFailure` km displayAllHistory -- * MarkVision markVisionHuman :: MonadClientUI m => Int -> m () markVisionHuman delta = modifySession $ cycleMarkVision delta -- * MarkSmell markSmellHuman :: MonadClientUI m => m () markSmellHuman = modifySession toggleMarkSmell -- * MarkSuspect markSuspectHuman :: MonadClient m => Int -> m () markSuspectHuman delta = do -- @condBFS@ depends on the setting we change here. invalidateBfsAll modifyClient (cycleMarkSuspect delta) -- * MarkAnim markAnimHuman :: MonadClient m => m () markAnimHuman = do noAnim <- getsClient $ fromMaybe False . snoAnim . soptions modifyClient $ \cli -> cli {soptions = (soptions cli) {snoAnim = Just $ not noAnim}} -- * OverrideTut overrideTutHuman :: MonadClientUI m => Int -> m () overrideTutHuman delta = modifySession $ cycleOverrideTut delta -- * PrintScreen printScreenHuman :: MonadClientUI m => m () printScreenHuman = do msgAdd MsgActionAlert "Screenshot printed." printScreen -- * Cancel -- | End aiming mode, rejecting the current position, unless when on -- remote level, in which case, return to our level. cancelHuman :: MonadClientUI m => m () cancelHuman = do maimMode <- getsSession saimMode case maimMode of Just aimMode -> do let lidV = aimLevelId aimMode lidOur <- getArenaUI if lidV == lidOur then clearAimMode else do xhairPos <- xhairToPos let sxhair = Just $ TPoint TKnown lidOur xhairPos modifySession $ \sess -> sess {saimMode = Just aimMode {aimLevelId = lidOur}} setXHairFromGUI sxhair doLook Nothing -> return () -- * Accept -- | Accept the current crosshair position as target, ending -- aiming mode, if active. acceptHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m () acceptHuman leader = do endAiming leader endAimingMsg leader clearAimMode -- | End aiming mode, accepting the current position. endAiming :: (MonadClient m, MonadClientUI m) => ActorId -> m () endAiming leader = do sxhair <- getsSession sxhair modifyClient $ updateTarget leader $ const sxhair endAimingMsg :: MonadClientUI m => ActorId -> m () endAimingMsg leader = do subject <- partActorLeader leader tgt <- getsClient $ getTarget leader (mtargetMsg, _) <- targetDesc tgt msgAdd MsgActionAlert $ case mtargetMsg of Nothing -> makeSentence [MU.SubjectVerbSg subject "clear target"] Just targetMsg -> makeSentence [MU.SubjectVerbSg subject "target", MU.Text targetMsg] -- * DetailCycle -- | Cycle detail level of aiming mode descriptions, starting up. detailCycleHuman :: MonadClientUI m => m () detailCycleHuman = do modifySession $ \sess -> sess {saimMode = (\aimMode -> aimMode {detailLevel = detailCycle $ detailLevel aimMode}) <$> saimMode sess} doLook detailCycle :: DetailLevel -> DetailLevel detailCycle detail = if detail == maxBound then minBound else succ detail -- * ClearTargetIfItemClear clearTargetIfItemClearHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m () clearTargetIfItemClearHuman leader = do itemSel <- getsSession sitemSel when (isNothing itemSel) $ do setXHairFromGUI Nothing modifyClient $ updateTarget leader (const Nothing) doLook -- * ItemClear itemClearHuman :: MonadClientUI m => m () itemClearHuman = modifySession $ \sess -> sess {sitemSel = Nothing} -- * MoveXhair -- | Move the xhair. Assumes aiming mode. moveXhairHuman :: MonadClientUI m => Vector -> Int -> m MError moveXhairHuman dir n = do -- Not @ScreenContent@, because not drawing here. COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops saimMode <- getsSession saimMode let lidV = maybe (error $ "" `showFailure` (dir, n)) aimLevelId saimMode xhair <- getsSession sxhair xhairPos <- xhairToPos let shiftB pos = shiftBounded rWidthMax rHeightMax pos dir newPos = iterate shiftB xhairPos !! n if newPos == xhairPos then failMsg "never mind" else do mleader <- getsClient sleader sxhair <- case (xhair, mleader) of (Just TVector{}, Just leader) -> do lpos <- getsState $ bpos . getActorBody leader return $ Just $ TVector $ newPos `vectorToFrom` lpos _ -> return $ Just $ TPoint TKnown lidV newPos setXHairFromGUI sxhair doLook return Nothing -- * AimTgt -- | Start aiming. aimTgtHuman :: MonadClientUI m => m () aimTgtHuman = do -- (Re)start aiming at the current level. lidV <- viewedLevelUI modifySession $ \sess -> sess {saimMode = let newDetail = maybe defaultDetailLevel detailLevel (saimMode sess) in Just $ AimMode lidV newDetail} doLook msgAdd MsgPromptAction "*flinging started; press again to project*" -- * AimFloor -- | Cycle aiming mode. Do not change position of the xhair, -- switch target to point at different things at that position. aimFloorHuman :: MonadClientUI m => m () aimFloorHuman = do lidV <- viewedLevelUI mleader <- getsClient sleader mlpos <- case mleader of Nothing -> return Nothing Just leader -> getsState $ Just . bpos . getActorBody leader xhairPos <- xhairToPos xhair <- getsSession sxhair saimMode <- getsSession saimMode bsAll <- getsState $ actorAssocs (const True) lidV side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD let sxhair = case xhair of _ | isNothing saimMode -> -- first key press: keep target xhair Just TEnemy{} -> Just $ TPoint TKnown lidV xhairPos Just TNonEnemy{} -> Just $ TPoint TKnown lidV xhairPos Just TPoint{} | Just lpos <- mlpos, xhairPos /= lpos -> Just $ TVector $ xhairPos `vectorToFrom` lpos Just TVector{} -> -- If many actors, we pick here the first that would be picked -- by '*', so that all other projectiles on the tile come next, -- when pressing '*', without any intervening actors from other tiles. -- This is why we use @actorAssocs@ above instead of @posToAidAssocs@. case find (\(_, b) -> bpos b == xhairPos) bsAll of Just (aid, b) -> Just $ if isFoe side fact (bfid b) then TEnemy aid else TNonEnemy aid Nothing -> Just $ TPoint TUnknown lidV xhairPos _ -> xhair modifySession $ \sess -> sess {saimMode = let newDetail = maybe defaultDetailLevel detailLevel saimMode in Just $ AimMode lidV newDetail} setXHairFromGUI sxhair doLook -- * AimEnemy aimEnemyHuman :: MonadClientUI m => m () aimEnemyHuman = do lidV <- viewedLevelUI mleader <- getsClient sleader mlpos <- case mleader of Nothing -> return Nothing Just leader -> getsState $ Just . bpos . getActorBody leader mxhairPos <- mxhairToPos xhair <- getsSession sxhair saimMode <- getsSession saimMode side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD bsAll <- getsState $ actorAssocs (const True) lidV let -- On the same position, big actors come before projectiles. ordPos lpos (_, b) = (chessDist lpos $ bpos b, bpos b, bproj b) dbs = case mlpos of Nothing -> bsAll Just lpos -> sortOn (ordPos lpos) bsAll pickUnderXhair = -- switch to the actor under xhair, if any fromMaybe (-1) $ findIndex ((== mxhairPos) . Just . bpos . snd) dbs (pickEnemies, i) = case xhair of Just (TEnemy a) | isJust saimMode -> -- pick next enemy (True, 1 + fromMaybe (-1) (findIndex ((== a) . fst) dbs)) Just (TEnemy a) -> -- first key press, retarget old enemy (True, fromMaybe (-1) $ findIndex ((== a) . fst) dbs) Just (TNonEnemy a) | isJust saimMode -> -- pick next non-enemy (False, 1 + fromMaybe (-1) (findIndex ((== a) . fst) dbs)) Just (TNonEnemy a) -> -- first key press, retarget old non-enemy (False, fromMaybe (-1) $ findIndex ((== a) . fst) dbs) _ -> (True, pickUnderXhair) (lt, gt) = splitAt i dbs isEnemy b = isFoe side fact (bfid b) && not (bproj b) && bhp b > 0 cond = if pickEnemies then isEnemy else not . isEnemy lf = filter (cond . snd) $ gt ++ lt sxhair = case lf of (a, _) : _ -> Just $ if pickEnemies then TEnemy a else TNonEnemy a [] -> xhair -- no seen foes in sight, stick to last target -- Register the chosen enemy, to pick another on next invocation. modifySession $ \sess -> sess {saimMode = let newDetail = maybe defaultDetailLevel detailLevel saimMode in Just $ AimMode lidV newDetail} setXHairFromGUI sxhair doLook -- * AimItem aimItemHuman :: MonadClientUI m => m () aimItemHuman = do side <- getsClient sside lidV <- viewedLevelUI mleader <- getsClient sleader mlpos <- case mleader of Nothing -> return Nothing Just leader -> getsState $ Just . bpos . getActorBody leader mxhairPos <- mxhairToPos xhair <- getsSession sxhair saimMode <- getsSession saimMode Level{lfloor} <- getLevel lidV mstash <- getsState $ \s -> gstash $ sfactionD s EM.! side -- Don't consider own stash an ordinary pile of items. let lfloorBarStash = case mstash of Just (lid, pos) | lid == lidV -> EM.delete pos lfloor _ -> lfloor bsAll = EM.keys lfloorBarStash ordPos lpos p = (chessDist lpos p, p) dbs = case mlpos of Nothing -> bsAll Just lpos -> sortOn (ordPos lpos) bsAll pickUnderXhair = -- switch to the item under xhair, if any let i = fromMaybe (-1) $ findIndex ((== mxhairPos) . Just) dbs in splitAt i dbs (lt, gt) = case xhair of Just (TPoint _ lid pos) | isJust saimMode && lid == lidV -> -- pick next item let i = fromMaybe (-1) $ elemIndex pos dbs in splitAt (i + 1) dbs Just (TPoint _ lid pos) | lid == lidV -> -- first key press, retarget old item let i = fromMaybe (-1) $ elemIndex pos dbs in splitAt i dbs _ -> pickUnderXhair gtlt = gt ++ lt sxhair = case gtlt of p : _ -> Just $ TPoint TKnown lidV p -- don't force AI to collect it [] -> xhair -- no items remembered, stick to last target -- Register the chosen enemy, to pick another on next invocation. modifySession $ \sess -> sess {saimMode = let newDetail = maybe defaultDetailLevel detailLevel saimMode in Just $ AimMode lidV newDetail} setXHairFromGUI sxhair doLook -- * AimAscend -- | Change the displayed level in aiming mode to (at most) -- k levels shallower. Enters aiming mode, if not already in one. aimAscendHuman :: MonadClientUI m => Int -> m MError aimAscendHuman k = do dungeon <- getsState sdungeon lidV <- viewedLevelUI let up = k > 0 case ascendInBranch dungeon up lidV of [] -> failMsg "no more levels in this direction" _ : _ -> do let ascendOne lid = case ascendInBranch dungeon up lid of [] -> lid nlid : _ -> nlid lidK = iterate ascendOne lidV !! abs k xhairPos <- xhairToPos let sxhair = Just $ TPoint TKnown lidK xhairPos modifySession $ \sess -> sess {saimMode = let newDetail = maybe defaultDetailLevel detailLevel (saimMode sess) in Just $ AimMode lidK newDetail} setXHairFromGUI sxhair doLook return Nothing -- * EpsIncr -- | Tweak the @eps@ parameter of the aiming digital line. epsIncrHuman :: (MonadClient m, MonadClientUI m) => Direction -> m () epsIncrHuman d = do -- Perform the change: let sepsDelta = case d of Forward -> 1 Backward -> -1 modifyClient $ \cli -> cli {seps = seps cli + sepsDelta} invalidateBfsPathAll -- Provide UI feedback: -- Hack @sreportNull@ to display the new line even if no earlier messages. modifySession $ \sess -> sess {sreportNull = False} saimMode <- getsSession saimMode lidV <- viewedLevelUI modifySession $ \sess -> sess {saimMode = let newDetail = maybe DetailLow detailLevel saimMode in Just $ AimMode lidV newDetail} flashAiming modifySession $ \sess -> sess {saimMode} -- The change may not affect the line shape, hence 'possibly'. msgAdd MsgPromptAction "Aiming line (possibly) modified." -- Flash the aiming line and path. flashAiming :: MonadClientUI m => m () flashAiming = do lidV <- viewedLevelUI animate lidV pushAndDelay -- * XhairUnknown xhairUnknownHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m MError xhairUnknownHuman leader = do b <- getsState $ getActorBody leader mpos <- closestUnknown leader case mpos of Nothing -> failMsg "no more unknown spots left" Just p -> do let sxhair = Just $ TPoint TUnknown (blid b) p setXHairFromGUI sxhair doLook return Nothing -- * XhairItem xhairItemHuman :: (MonadClient m, MonadClientUI m) => ActorId -> m MError xhairItemHuman leader = do b <- getsState $ getActorBody leader items <- closestItems leader case items of [] -> failMsg "no more reachable items remembered or visible" _ -> do let (_, (p, bag)) = maximumBy (comparing fst) items sxhair = Just $ TPoint (TItem bag) (blid b) p setXHairFromGUI sxhair doLook return Nothing -- * XhairStair xhairStairHuman :: (MonadClient m, MonadClientUI m) => ActorId -> Bool -> m MError xhairStairHuman leader up = do b <- getsState $ getActorBody leader stairs <- closestTriggers (if up then ViaStairsUp else ViaStairsDown) leader case stairs of [] -> failMsg $ "no reachable stairs" <+> if up then "up" else "down" _ -> do let (_, (p, (p0, bag))) = maximumBy (comparing fst) stairs sxhair = Just $ TPoint (TEmbed bag p0) (blid b) p setXHairFromGUI sxhair doLook return Nothing -- * XhairPointerFloor xhairPointerFloorHuman :: MonadClientUI m => m () xhairPointerFloorHuman = do saimMode <- getsSession saimMode aimPointerFloorHuman when (isNothing saimMode) $ modifySession $ \sess -> sess {saimMode} -- * XhairPointerMute xhairPointerMuteHuman :: MonadClientUI m => m () xhairPointerMuteHuman = do saimMode <- getsSession saimMode aimPointerFloorLoud False when (isNothing saimMode) $ modifySession $ \sess -> sess {saimMode} -- * XhairPointerEnemy xhairPointerEnemyHuman :: MonadClientUI m => m () xhairPointerEnemyHuman = do saimMode <- getsSession saimMode aimPointerEnemyHuman when (isNothing saimMode) $ modifySession $ \sess -> sess {saimMode} -- * AimPointerFloor aimPointerFloorHuman :: MonadClientUI m => m () aimPointerFloorHuman = aimPointerFloorLoud True aimPointerFloorLoud :: MonadClientUI m => Bool -> m () aimPointerFloorLoud loud = do COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops lidV <- viewedLevelUI -- Not @ScreenContent@, because not drawing here. pUI <- getsSession spointer let p = squareToMap $ uiToSquare pUI if insideP (0, 0, rWidthMax - 1, rHeightMax - 1) p then do oldXhair <- getsSession sxhair let sxhair = Just $ TPoint TUnknown lidV p sxhairMoused = sxhair /= oldXhair detailSucc = if sxhairMoused then detailLevel else detailCycle . detailLevel modifySession $ \sess -> sess { saimMode = let newDetail = maybe defaultDetailLevel detailSucc (saimMode sess) in Just $ AimMode lidV newDetail , sxhairMoused } setXHairFromGUI sxhair when loud doLook else stopPlayBack -- * AimPointerEnemy aimPointerEnemyHuman :: MonadClientUI m => m () aimPointerEnemyHuman = do COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops lidV <- viewedLevelUI -- Not @ScreenContent@, because not drawing here. pUI <- getsSession spointer let p = squareToMap $ uiToSquare pUI if insideP (0, 0, rWidthMax - 1, rHeightMax - 1) p then do bsAll <- getsState $ actorAssocs (const True) lidV oldXhair <- getsSession sxhair side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD let sxhair = -- If many actors, we pick here the first that would be picked -- by '*', so that all other projectiles on the tile come next, -- when pressing '*', without any intervening actors from other tiles. -- This is why we use @actorAssocs@ above instead of @posToAidAssocs@. case find (\(_, b) -> bpos b == p) bsAll of Just (aid, b) -> Just $ if isFoe side fact (bfid b) then TEnemy aid else TNonEnemy aid Nothing -> Just $ TPoint TUnknown lidV p sxhairMoused = sxhair /= oldXhair detailSucc = if sxhairMoused then detailLevel else detailCycle . detailLevel modifySession $ \sess -> sess { saimMode = let newDetail = maybe defaultDetailLevel detailSucc (saimMode sess) in Just $ AimMode lidV newDetail , sxhairMoused } setXHairFromGUI sxhair doLook else stopPlayBack LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/HandleHumanM.hs0000644000000000000000000002401107346545000023327 0ustar0000000000000000-- | Semantics of human player commands. module Game.LambdaHack.Client.UI.HandleHumanM ( cmdSemInCxtOfKM, updateKeyLast #ifdef EXPOSE_INTERNAL -- * Internal operations , noRemoteHumanCmd, CmdLeaderNeed, cmdSemantics, cmdSemanticsLeader , addNoError, addLeader, weaveLeader #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.HandleHelperM import Game.LambdaHack.Client.UI.HandleHumanGlobalM import Game.LambdaHack.Client.UI.HandleHumanLocalM import Game.LambdaHack.Client.UI.HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Common.Types -- | Commands that are forbidden on a remote level, because they -- would usually take time when invoked on one, but not necessarily do -- what the player expects. Note that some commands that normally take time -- are not included, because they don't take time in aiming mode -- or their individual sanity conditions include a remote level check. noRemoteHumanCmd :: HumanCmd -> Bool noRemoteHumanCmd cmd = case cmd of Wait -> True Wait10 -> True MoveItem{} -> True Apply{} -> True AlterDir{} -> True AlterWithPointer{} -> True MoveOnceToXhair -> True RunOnceToXhair -> True ContinueToXhair -> True _ -> False updateKeyLast :: K.KM -> HumanCmd -> KeyMacroFrame -> KeyMacroFrame updateKeyLast km cmd macroFrame = case cmd of RepeatLast{} -> macroFrame Record{} -> macroFrame _ -> macroFrame {keyLast = Just km} -- | The semantics of human player commands in terms of the client monad, -- in context of the given @km@ as the last action. -- -- Some time cosuming commands are enabled even in aiming mode, but cannot be -- invoked in aiming mode on a remote level (level different than -- the level of the leader). Commands that require a pointman fail -- when no leader is designated. cmdSemInCxtOfKM :: (MonadClient m, MonadClientUI m) => K.KM -> HumanCmd -> m (Either MError ReqUI) cmdSemInCxtOfKM km cmd = do modifySession $ \sess -> sess {smacroFrame = updateKeyLast km cmd $ smacroFrame sess} cmdSemantics cmd data CmdLeaderNeed m = CmdNoNeed (m (Either MError ReqUI)) | CmdLeader (ActorId -> m (Either MError ReqUI)) cmdSemantics :: (MonadClient m, MonadClientUI m) => HumanCmd -> m (Either MError ReqUI) cmdSemantics cmd = case cmdSemanticsLeader cmd of CmdNoNeed mreq -> mreq CmdLeader f -> do mleader <- getsClient sleader case mleader of Nothing -> weaveJust <$> failWith "command disabled when no pointman designated, choose another command" Just leader -> do if noRemoteHumanCmd cmd then do -- If in aiming mode, check if the current level is the same -- as player level and refuse performing the action otherwise. arena <- getArenaUI lidV <- viewedLevelUI if arena /= lidV then weaveJust <$> failWith "command disabled on a remote level, press ESC to switch back" else f leader else f leader cmdSemanticsLeader :: (MonadClient m, MonadClientUI m) => HumanCmd -> CmdLeaderNeed m cmdSemanticsLeader cmd = case cmd of Macro kms -> addNoError $ macroHuman kms ByArea l -> CmdNoNeed $ byAreaHuman cmdSemInCxtOfKM l ByAimMode AimModeCmd{..} -> CmdNoNeed $ byAimModeHuman (cmdSemantics exploration) (cmdSemantics aiming) ComposeIfLocal cmd1 cmd2 -> CmdNoNeed $ composeIfLocalHuman (cmdSemantics cmd1) (cmdSemantics cmd2) ComposeUnlessError cmd1 cmd2 -> CmdNoNeed $ composeUnlessErrorHuman (cmdSemantics cmd1) (cmdSemantics cmd2) Compose2ndLocal cmd1 cmd2 -> CmdNoNeed $ compose2ndLocalHuman (cmdSemantics cmd1) (cmdSemantics cmd2) LoopOnNothing cmd1 -> CmdNoNeed $ loopOnNothingHuman (cmdSemantics cmd1) ExecuteIfClear cmd1 -> CmdNoNeed $ executeIfClearHuman (cmdSemantics cmd1) Wait -> weaveLeader $ \leader -> ReqUITimed <$$> waitHuman leader Wait10 -> weaveLeader $ \leader -> ReqUITimed <$$> waitHuman10 leader Yell -> weaveLeader $ \leader -> ReqUITimed <$$> yellHuman leader MoveDir v -> weaveLeader $ \leader -> ReqUITimed <$$> moveRunHuman leader True True False False v RunDir v -> weaveLeader $ \leader -> ReqUITimed <$$> moveRunHuman leader True True True True v RunOnceAhead -> CmdLeader $ \leader -> ReqUITimed <$$> runOnceAheadHuman leader MoveOnceToXhair -> weaveLeader $ \leader -> ReqUITimed <$$> moveOnceToXhairHuman leader RunOnceToXhair -> weaveLeader $ \leader -> ReqUITimed <$$> runOnceToXhairHuman leader ContinueToXhair -> weaveLeader $ \leader -> ReqUITimed <$$> continueToXhairHuman leader MoveItem stores toCStore mverb auto -> weaveLeader $ \leader -> ReqUITimed <$$> moveItemHuman leader stores toCStore mverb auto Project -> weaveLeader $ \leader -> ReqUITimed <$$> projectHuman leader Apply -> weaveLeader $ \leader -> ReqUITimed <$$> applyHuman leader AlterDir -> weaveLeader $ \leader -> ReqUITimed <$$> alterDirHuman leader AlterWithPointer -> weaveLeader $ \leader -> ReqUITimed <$$> alterWithPointerHuman leader CloseDir -> weaveLeader $ \leader -> ReqUITimed <$$> closeDirHuman leader Help -> CmdNoNeed $ helpHuman cmdSemInCxtOfKM Hint -> CmdNoNeed $ hintHuman cmdSemInCxtOfKM ItemMenu -> CmdLeader $ \leader -> itemMenuHuman leader cmdSemInCxtOfKM ChooseItemMenu dialogMode -> CmdLeader $ \leader -> chooseItemMenuHuman leader cmdSemInCxtOfKM dialogMode MainMenu -> CmdNoNeed $ mainMenuHuman cmdSemInCxtOfKM MainMenuAutoOn -> CmdNoNeed $ mainMenuAutoOnHuman cmdSemInCxtOfKM MainMenuAutoOff -> CmdNoNeed $ mainMenuAutoOffHuman cmdSemInCxtOfKM Dashboard -> CmdNoNeed $ dashboardHuman cmdSemInCxtOfKM GameDifficultyIncr delta -> CmdNoNeed $ gameDifficultyIncr delta >> challengeMenuHuman cmdSemInCxtOfKM GameFishToggle -> CmdNoNeed $ gameFishToggle >> challengeMenuHuman cmdSemInCxtOfKM GameGoodsToggle -> CmdNoNeed $ gameGoodsToggle >> challengeMenuHuman cmdSemInCxtOfKM GameWolfToggle -> CmdNoNeed $ gameWolfToggle >> challengeMenuHuman cmdSemInCxtOfKM GameKeeperToggle -> CmdNoNeed $ gameKeeperToggle >> challengeMenuHuman cmdSemInCxtOfKM GameScenarioIncr delta -> CmdNoNeed $ gameScenarioIncr delta >> challengeMenuHuman cmdSemInCxtOfKM GameRestart -> CmdNoNeed $ weaveJust <$> gameExitWithHuman Restart GameQuit -> CmdNoNeed $ weaveJust <$> gameExitWithHuman Quit GameDrop -> CmdNoNeed $ weaveJust <$> fmap Right gameDropHuman GameExit -> CmdNoNeed $ weaveJust <$> fmap Right gameExitHuman GameSave -> CmdNoNeed $ weaveJust <$> fmap Right gameSaveHuman Doctrine -> CmdNoNeed $ weaveJust <$> doctrineHuman Automate -> CmdNoNeed $ weaveJust <$> automateHuman AutomateToggle -> CmdNoNeed $ weaveJust <$> automateToggleHuman AutomateBack -> CmdNoNeed automateBackHuman ChooseItem dialogMode -> CmdLeader $ \leader -> Left <$> chooseItemHuman leader dialogMode ChooseItemProject ts -> CmdLeader $ \leader -> Left <$> chooseItemProjectHuman leader ts ChooseItemApply ts -> CmdLeader $ \leader -> Left <$> chooseItemApplyHuman leader ts PickLeader k -> CmdNoNeed $ Left <$> pickLeaderHuman k PickLeaderWithPointer -> CmdLeader $ fmap Left . pickLeaderWithPointerHuman PointmanCycle direction -> CmdLeader $ \leader -> Left <$> pointmanCycleHuman leader direction PointmanCycleLevel direction -> CmdLeader $ \leader -> Left <$> pointmanCycleLevelHuman leader direction SelectActor -> addLeader selectActorHuman SelectNone -> addNoError selectNoneHuman SelectWithPointer -> CmdNoNeed $ Left <$> selectWithPointerHuman Repeat n -> addNoError $ repeatHuman n RepeatLast n -> addNoError $ repeatLastHuman n Record -> addNoError recordHuman AllHistory -> addNoError allHistoryHuman MarkVision delta -> CmdNoNeed $ markVisionHuman delta >> settingsMenuHuman cmdSemInCxtOfKM MarkSmell -> CmdNoNeed $ markSmellHuman >> settingsMenuHuman cmdSemInCxtOfKM MarkSuspect delta -> CmdNoNeed $ markSuspectHuman delta >> settingsMenuHuman cmdSemInCxtOfKM MarkAnim -> CmdNoNeed $ markAnimHuman >> settingsMenuHuman cmdSemInCxtOfKM OverrideTut delta -> CmdNoNeed $ overrideTutHuman delta >> settingsMenuHuman cmdSemInCxtOfKM SettingsMenu -> CmdNoNeed $ settingsMenuHuman cmdSemInCxtOfKM ChallengeMenu -> CmdNoNeed $ challengeMenuHuman cmdSemInCxtOfKM PrintScreen -> addNoError printScreenHuman Cancel -> addNoError cancelHuman Accept -> addLeader acceptHuman DetailCycle -> addNoError detailCycleHuman ClearTargetIfItemClear -> addLeader clearTargetIfItemClearHuman ItemClear -> addNoError itemClearHuman MoveXhair v k -> CmdNoNeed $ Left <$> moveXhairHuman v k AimTgt -> addNoError aimTgtHuman AimFloor -> addNoError aimFloorHuman AimEnemy -> addNoError aimEnemyHuman AimItem -> addNoError aimItemHuman AimAscend k -> CmdNoNeed $ Left <$> aimAscendHuman k EpsIncr b -> addNoError $ epsIncrHuman b XhairUnknown -> CmdLeader $ fmap Left . xhairUnknownHuman XhairItem -> CmdLeader $ fmap Left . xhairItemHuman XhairStair up -> CmdLeader $ \leader -> Left <$> xhairStairHuman leader up XhairPointerFloor -> addNoError xhairPointerFloorHuman XhairPointerMute -> addNoError xhairPointerMuteHuman XhairPointerEnemy -> addNoError xhairPointerEnemyHuman AimPointerFloor -> addNoError aimPointerFloorHuman AimPointerEnemy -> addNoError aimPointerEnemyHuman addNoError :: Monad m => m () -> CmdLeaderNeed m addNoError cmdCli = CmdNoNeed $ cmdCli >> return (Left Nothing) addLeader :: Monad m => (ActorId -> m ()) -> CmdLeaderNeed m addLeader cmdCli = CmdLeader $ \leader -> cmdCli leader >> return (Left Nothing) weaveLeader :: Monad m => (ActorId -> m (FailOrCmd ReqUI)) -> CmdLeaderNeed m weaveLeader cmdCli = CmdLeader $ fmap weaveJust . cmdCli LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/HumanCmd.hs0000644000000000000000000001256707346545000022537 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Abstract syntax of human player commands. module Game.LambdaHack.Client.UI.HumanCmd ( CmdCategory(..), categoryDescription , CmdArea(..), areaDescription , CmdTriple, AimModeCmd(..), HumanCmd(..) , TriggerItem(..) ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.DeepSeq import Data.Binary import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.ItemKind (ItemKind) import Game.LambdaHack.Definition.Defs data CmdCategory = CmdDashboard | CmdItemMenu | CmdMove | CmdItem | CmdAim | CmdMeta | CmdMouse | CmdInternal | CmdDebug | CmdMinimal deriving (Show, Read, Eq, Generic) instance NFData CmdCategory instance Binary CmdCategory categoryDescription :: CmdCategory -> Text categoryDescription CmdDashboard = "Dashboard" categoryDescription CmdItemMenu = "Item menu commands" categoryDescription CmdMove = "Terrain exploration and modification commands" categoryDescription CmdItem = "All item-related commands" categoryDescription CmdAim = "All aiming commands" categoryDescription CmdMeta = "Assorted commands" categoryDescription CmdMouse = "Mouse" categoryDescription CmdInternal = "Internal" categoryDescription CmdDebug = "Debug" categoryDescription CmdMinimal = "The minimal command set" -- The constructors are sorted, roughly, wrt inclusion, then top to bottom, -- the left to right. -- | Symbolic representation of areas of the screen used to define the meaning -- of mouse button presses relative to where the mouse points to. data CmdArea = CaMessage | CaMapLeader | CaMapParty | CaMap | CaLevelNumber | CaArenaName | CaPercentSeen | CaXhairDesc | CaSelected | CaCalmGauge | CaCalmValue | CaHPGauge | CaHPValue | CaLeaderDesc deriving (Show, Read, Eq, Ord, Generic) instance NFData CmdArea instance Binary CmdArea areaDescription :: CmdArea -> Text areaDescription ca = case ca of CaMessage -> "message line" CaMapLeader -> "pointman tile" CaMapParty -> "party on map" CaMap -> "the map area" CaLevelNumber -> "level number" CaArenaName -> "level caption" CaPercentSeen -> "percent seen" CaXhairDesc -> "crosshair info" CaSelected -> "party roster" CaCalmGauge -> "Calm gauge" CaCalmValue -> "Calm value" CaHPGauge -> "HP gauge" CaHPValue -> "HP value" CaLeaderDesc -> "pointman info" -- 1234567890123 -- | This triple of command categories, description and the command term itself -- defines the meaning of a human command as entered via a keypress, -- mouse click or chosen from a menu. type CmdTriple = ([CmdCategory], Text, HumanCmd) data AimModeCmd = AimModeCmd {exploration :: HumanCmd, aiming :: HumanCmd} deriving (Show, Read, Eq, Ord, Generic) instance NFData AimModeCmd instance Binary AimModeCmd -- | Abstract syntax of human player commands. data HumanCmd = -- Meta. Macro [String] | ByArea [(CmdArea, HumanCmd)] -- if outside the areas, do nothing | ByAimMode AimModeCmd | ComposeIfLocal HumanCmd HumanCmd | ComposeUnlessError HumanCmd HumanCmd | Compose2ndLocal HumanCmd HumanCmd | LoopOnNothing HumanCmd | ExecuteIfClear HumanCmd -- Global. -- These usually take time. | Wait | Wait10 | Yell | MoveDir Vector | RunDir Vector | RunOnceAhead | MoveOnceToXhair | RunOnceToXhair | ContinueToXhair | MoveItem [CStore] CStore (Maybe Text) Bool | Project | Apply | AlterDir | AlterWithPointer | CloseDir | Help | Hint | ItemMenu | MainMenu | MainMenuAutoOn | MainMenuAutoOff | Dashboard -- Below this line, commands do not take time. | GameDifficultyIncr Int | GameFishToggle | GameGoodsToggle | GameWolfToggle | GameKeeperToggle | GameScenarioIncr Int | GameRestart | GameQuit | GameDrop | GameExit | GameSave | Doctrine | Automate | AutomateToggle | AutomateBack -- Local. Below this line, commands do not notify the server. | ChooseItem ItemDialogMode | ChooseItemMenu ItemDialogMode | ChooseItemProject [TriggerItem] | ChooseItemApply [TriggerItem] | PickLeader Int | PickLeaderWithPointer | PointmanCycle Direction | PointmanCycleLevel Direction | SelectActor | SelectNone | SelectWithPointer | Repeat Int | RepeatLast Int | Record | AllHistory | MarkVision Int | MarkSmell | MarkSuspect Int | MarkAnim | OverrideTut Int | SettingsMenu | ChallengeMenu | PrintScreen -- These are mostly related to aiming. | Cancel | Accept | DetailCycle | ClearTargetIfItemClear | ItemClear | MoveXhair Vector Int | AimTgt | AimFloor | AimEnemy | AimItem | AimAscend Int | EpsIncr Direction | XhairUnknown | XhairItem | XhairStair Bool | XhairPointerFloor | XhairPointerMute | XhairPointerEnemy | AimPointerFloor | AimPointerEnemy deriving (Show, Read, Eq, Ord, Generic) instance NFData HumanCmd instance Binary HumanCmd -- | Description of how item manipulation is triggered and communicated -- to the player. data TriggerItem = TriggerItem {tiverb :: MU.Part, tiobject :: MU.Part, tisymbols :: [ContentSymbol ItemKind]} deriving (Show, Eq, Ord, Generic) instance Read TriggerItem where readsPrec = error $ "parsing of TriggerItem not implemented" `showFailure` () instance NFData TriggerItem instance Binary TriggerItem LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/InventoryM.hs0000644000000000000000000010575507346545000023157 0ustar0000000000000000-- | UI of inventory management. module Game.LambdaHack.Client.UI.InventoryM ( Suitability(..), ResultItemDialogMode(..) , getFull, getGroupItem, getStoreItem , skillCloseUp, placeCloseUp, factionCloseUp #ifdef EXPOSE_INTERNAL -- * Internal operations , ItemDialogState(..), accessModeBag, storeItemPrompt, getItem , DefItemKey(..), transition , runDefMessage, runDefAction, runDefSkills, skillsInRightPane , runDefPlaces, placesInRightPane , runDefFactions, factionsInRightPane , runDefModes, runDefInventory #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Either import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Function import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.HandleHelperM import Game.LambdaHack.Client.UI.HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.Faction as Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.FactionKind as FK import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.PlaceKind as PK import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs data ItemDialogState = ISuitable | IAll deriving (Show, Eq) data ResultItemDialogMode = RStore CStore [ItemId] | ROwned ItemId | RLore SLore MenuSlot [(ItemId, ItemQuant)] | RSkills MenuSlot | RPlaces MenuSlot | RFactions MenuSlot | RModes MenuSlot deriving Show accessModeBag :: ActorId -> State -> ItemDialogMode -> ItemBag accessModeBag leader s (MStore cstore) = let b = getActorBody leader s in getBodyStoreBag b cstore s accessModeBag leader s MOwned = let fid = bfid $ getActorBody leader s in combinedItems fid s accessModeBag _ _ MSkills = EM.empty accessModeBag leader s (MLore SBody) = let b = getActorBody leader s in getBodyStoreBag b COrgan s accessModeBag _ s MLore{} = EM.map (const quantSingle) $ sitemD s accessModeBag _ _ MPlaces = EM.empty accessModeBag _ _ MFactions = EM.empty accessModeBag _ _ MModes = EM.empty -- | Let a human player choose any item from a given group. -- Note that this does not guarantee the chosen item belongs to the group, -- as the player can override the choice. -- Used e.g., for applying and projecting. getGroupItem :: MonadClientUI m => ActorId -> m Suitability -- ^ which items to consider suitable -> Text -- ^ specific prompt for only suitable items -> Text -- ^ generic prompt -> Text -- ^ the verb to use -> Text -- ^ the generic verb to use -> [CStore] -- ^ stores to cycle through -> m (Either Text (CStore, ItemId)) getGroupItem leader psuit prompt promptGeneric verb verbGeneric stores = do side <- getsClient sside mstash <- getsState $ \s -> gstash $ sfactionD s EM.! side let ppItemDialogBody v body actorSk cCur = case cCur of MStore CEqp | not $ calmEnough body actorSk -> "distractedly attempt to" <+> v <+> ppItemDialogModeIn cCur MStore CGround | mstash == Just (blid body, bpos body) -> "greedily attempt to" <+> v <+> ppItemDialogModeIn cCur _ -> v <+> ppItemDialogModeFrom cCur soc <- getFull leader psuit (\body _ actorSk cCur _ -> prompt <+> ppItemDialogBody verb body actorSk cCur) (\body _ actorSk cCur _ -> promptGeneric <+> ppItemDialogBody verbGeneric body actorSk cCur) stores True False case soc of Left err -> return $ Left err Right (rstore, [(iid, _)]) -> return $ Right (rstore, iid) Right _ -> error $ "" `showFailure` soc -- | Display all items from a store and let the human player choose any -- or switch to any other store. -- Used, e.g., for viewing inventory and item descriptions. getStoreItem :: MonadClientUI m => ActorId -- ^ the pointman -> ItemDialogMode -- ^ initial mode -> m (Either Text ResultItemDialogMode) getStoreItem leader cInitial = do side <- getsClient sside let -- No @COrgan@, because triggerable organs are rare and, -- if really needed, accessible directly from the trigger menu. itemCs = map MStore [CStash, CEqp, CGround] -- This should match, including order, the items in standardKeysAndMouse -- marked with CmdDashboard up to @MSkills@. leaderCs = itemCs ++ [MOwned, MLore SBody, MSkills] -- No @SBody@, because repeated in other lores and included elsewhere. itemLoreCs = map MLore [minBound..SEmbed] -- This should match, including order, the items in standardKeysAndMouse -- marked with CmdDashboard past @MSkills@ and up to @MModes@. loreCs = itemLoreCs ++ [MPlaces, MFactions, MModes] let !_A1 = assert (null (leaderCs `intersect` loreCs)) () !_A2 = assert (sort (leaderCs ++ loreCs ++ [MStore COrgan]) == map MStore [minBound..maxBound] ++ [MOwned, MSkills] ++ map MLore [minBound..maxBound] ++ [MPlaces, MFactions, MModes]) () allCs | cInitial `elem` leaderCs = leaderCs | cInitial `elem` loreCs = loreCs | otherwise = assert (cInitial == MStore COrgan) leaderCs -- werrd content, but let it be (pre, rest) = break (== cInitial) allCs post = dropWhile (== cInitial) rest remCs = post ++ pre prompt = storeItemPrompt side getItem leader (return SuitsEverything) prompt prompt cInitial remCs True False storeItemPrompt :: FactionId -> Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State -> Text storeItemPrompt side body bodyUI actorCurAndMaxSk c2 s = let COps{coitem} = scops s fact = sfactionD s EM.! side (tIn, t) = ppItemDialogMode c2 subject = partActor bodyUI f (k, _) acc = k + acc countItems store = EM.foldr' f 0 $ getBodyStoreBag body store s in case c2 of MStore CGround -> let n = countItems CGround nItems = MU.CarAWs n "item" verbGround = if gstash fact == Just (blid body, bpos body) then "fondle greedily" else "notice" in makePhrase [ MU.Capitalize $ MU.SubjectVerbSg subject verbGround , nItems, "at" , MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text "feet" ] MStore CEqp -> let n = countItems CEqp (verbEqp, nItems) = if | n == 0 -> ("find nothing", "") | calmEnough body actorCurAndMaxSk -> ("find", MU.CarAWs n "item") | otherwise -> ("paw distractedly at", MU.CarAWs n "item") in makePhrase [ MU.Capitalize $ MU.SubjectVerbSg subject verbEqp , nItems, MU.Text tIn , MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t ] MStore cstore -> let n = countItems cstore nItems = MU.CarAWs n "item" (verb, onLevel) = case cstore of COrgan -> ("feel", []) CStash -> ( "notice" , case gstash fact of 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] in makePhrase $ [ MU.Capitalize $ MU.SubjectVerbSg subject verb , nItems, MU.Text tIn ] ++ ownObject ++ onLevel MOwned -> -- We assume "gold grain", not "grain" with label "of gold": let currencyName = IK.iname $ okind coitem $ ouniqGroup coitem IK.S_CURRENCY dungeonTotal = sgold s (_, total) = calculateTotal side s in T.init $ spoilsBlurb currencyName total dungeonTotal -- no space for more, e.g., the pointman, but it can't be changed anyway MSkills -> makePhrase [ MU.Capitalize $ MU.SubjectVerbSg subject "estimate" , MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t ] MLore SBody -> makePhrase [ MU.Capitalize $ MU.SubjectVerbSg subject "feel" , MU.Text tIn , MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t ] MLore slore -> makePhrase [ MU.Capitalize $ MU.Text $ if slore == SEmbed then "terrain (including crafting recipes)" else t ] MPlaces -> makePhrase [ MU.Capitalize $ MU.Text t ] MFactions -> makePhrase [ MU.Capitalize $ MU.Text t ] MModes -> makePhrase [ MU.Capitalize $ MU.Text t ] -- | Let the human player choose a single, preferably suitable, -- item from a list of items. Don't display stores empty for all actors. -- Start with a non-empty store. getFull :: MonadClientUI m => ActorId -> m Suitability -- ^ which items to consider suitable -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State -> Text) -- ^ specific prompt for only suitable items -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State -> Text) -- ^ generic prompt -> [CStore] -- ^ stores to cycle through -> Bool -- ^ whether to ask, when the only item -- in the starting mode is suitable -> Bool -- ^ whether to permit multiple items as a result -> m (Either Text (CStore, [(ItemId, ItemQuant)])) getFull leader psuit prompt promptGeneric stores askWhenLone permitMulitple = do mpsuit <- psuit let psuitFun = case mpsuit of SuitsEverything -> \_ _ _ -> True SuitsSomething f -> f -- Move the first store that is non-empty for suitable items for this actor -- to the front, if any. b <- getsState $ getActorBody leader getCStoreBag <- getsState $ \s cstore -> getBodyStoreBag b cstore s let hasThisActor = not . EM.null . getCStoreBag case filter hasThisActor stores of [] -> do let dialogModes = map MStore stores ts = map (MU.Text . ppItemDialogModeIn) dialogModes return $ Left $ "no items" <+> makePhrase [MU.WWxW "nor" ts] haveThis@(headThisActor : _) -> do itemToF <- getsState $ flip itemToFull let suitsThisActor store = let bag = getCStoreBag store in any (\(iid, kit) -> psuitFun (Just store) (itemToF iid) kit) (EM.assocs bag) firstStore = fromMaybe headThisActor $ find suitsThisActor haveThis -- Don't display stores totally empty for all actors. breakStores cInit = let (pre, rest) = break (== cInit) stores post = dropWhile (== cInit) rest in (MStore cInit, map MStore $ post ++ pre) (modeFirst, modeRest) = breakStores firstStore res <- getItem leader psuit prompt promptGeneric modeFirst modeRest askWhenLone permitMulitple case res of Left t -> return $ Left t Right (RStore fromCStore iids) -> do let bagAll = getCStoreBag fromCStore f iid = (iid, bagAll EM.! iid) return $ Right (fromCStore, map f iids) Right _ -> error $ "" `showFailure` res -- | Let the human player choose a single, preferably suitable, -- item from a list of items. getItem :: MonadClientUI m => ActorId -> m Suitability -- ^ which items to consider suitable -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State -> Text) -- ^ specific prompt for only suitable items -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State -> Text) -- ^ generic prompt -> ItemDialogMode -- ^ first mode to display -> [ItemDialogMode] -- ^ the (rest of) modes -> Bool -- ^ whether to ask, when the only item -- in the starting mode is suitable -> Bool -- ^ whether to permit multiple items as a result -> m (Either Text ResultItemDialogMode) getItem leader psuit prompt promptGeneric cCur cRest askWhenLone permitMulitple = do accessCBag <- getsState $ accessModeBag leader let storeAssocs = EM.assocs . accessCBag allAssocs = concatMap storeAssocs (cCur : cRest) case (allAssocs, cCur) of ([(iid, _)], MStore rstore) | null cRest && not askWhenLone -> return $ Right $ RStore rstore [iid] _ -> transition leader psuit prompt promptGeneric permitMulitple cCur cRest ISuitable data DefItemKey m = DefItemKey { defLabel :: Either Text K.KM , defCond :: Bool , defAction :: ~(m (Either Text ResultItemDialogMode)) -- this field may be expensive or undefined when @defCond@ is false } data Suitability = SuitsEverything | SuitsSomething (Maybe CStore -> ItemFull -> ItemQuant -> Bool) transition :: forall m. MonadClientUI m => ActorId -> m Suitability -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State -> Text) -> (Actor -> ActorUI -> Ability.Skills -> ItemDialogMode -> State -> Text) -> Bool -> ItemDialogMode -> [ItemDialogMode] -> ItemDialogState -> m (Either Text ResultItemDialogMode) transition leader psuit prompt promptGeneric permitMulitple cCur cRest itemDialogState = do let recCall cCur2 cRest2 itemDialogState2 = do -- Pointman could have been changed by keypresses near the end of -- the current recursive call, so refresh it for the next call. mleader <- getsClient sleader -- When run inside a test, without mleader, assume leader not changed. let leader2 = fromMaybe leader mleader transition leader2 psuit prompt promptGeneric permitMulitple cCur2 cRest2 itemDialogState2 actorCurAndMaxSk <- getsState $ getActorMaxSkills leader body <- getsState $ getActorBody leader bodyUI <- getsSession $ getActorUI leader fact <- getsState $ (EM.! bfid body) . sfactionD hs <- partyAfterLeader leader revCmd <- revCmdMap promptChosen <- getsState $ \s -> case itemDialogState of ISuitable -> prompt body bodyUI actorCurAndMaxSk cCur s <> ":" IAll -> promptGeneric body bodyUI actorCurAndMaxSk cCur s <> ":" let keyDefsCommon :: [(K.KM, DefItemKey m)] keyDefsCommon = filter (defCond . snd) [ let km = K.mkChar '<' in (km, changeContainerDef Backward $ Right km) , let km = K.mkChar '>' in (km, changeContainerDef Forward $ Right km) , cycleKeyDef Forward , cycleKeyDef Backward , cycleLevelKeyDef Forward , cycleLevelKeyDef Backward , (K.KM K.NoModifier K.LeftButtonRelease, DefItemKey { defLabel = Left "" , defCond = maySwitchLeader cCur && not (null hs) , defAction = do -- This is verbose even in aiming mode, displaying -- terrain description, but it's fine, mouse may do that. merror <- pickLeaderWithPointer leader case merror of Nothing -> recCall cCur cRest itemDialogState Just{} -> return $ Left "not a menu item nor teammate position" -- don't inspect the error, it's expected }) , (K.escKM, DefItemKey { defLabel = Right K.escKM , defCond = True , defAction = return $ Left "never mind" }) ] cycleLevelKeyDef direction = let km = revCmd $ PointmanCycleLevel direction in (km, DefItemKey { defLabel = Left "" , defCond = maySwitchLeader cCur && any (\(_, b, _) -> blid b == blid body) hs , defAction = do err <- pointmanCycleLevel leader False direction let !_A = assert (isNothing err `blame` err) () recCall cCur cRest itemDialogState }) changeContainerDef direction defLabel = let (cCurAfterCalm, cRestAfterCalm) = nextContainers direction in DefItemKey { defLabel , defCond = cCurAfterCalm /= cCur , defAction = recCall cCurAfterCalm cRestAfterCalm itemDialogState } nextContainers direction = case direction of Forward -> case cRest ++ [cCur] of c1 : rest -> (c1, rest) [] -> error $ "" `showFailure` cRest Backward -> case reverse $ cCur : cRest of c1 : rest -> (c1, reverse rest) [] -> error $ "" `showFailure` cRest banned = bannedPointmanSwitchBetweenLevels fact maySwitchLeader MStore{} = True maySwitchLeader MOwned = False maySwitchLeader MSkills = True maySwitchLeader (MLore SBody) = True maySwitchLeader MLore{} = False maySwitchLeader MPlaces = False maySwitchLeader MFactions = False maySwitchLeader MModes = False cycleKeyDef direction = let km = revCmd $ PointmanCycle direction in (km, DefItemKey { defLabel = if direction == Forward then Right km else Left "" , defCond = maySwitchLeader cCur && not (banned || null hs) , defAction = do err <- pointmanCycle leader False direction let !_A = assert (isNothing err `blame` err) () recCall cCur cRest itemDialogState }) case cCur of MSkills -> runDefSkills keyDefsCommon promptChosen leader MPlaces -> runDefPlaces keyDefsCommon promptChosen MFactions -> runDefFactions keyDefsCommon promptChosen MModes -> runDefModes keyDefsCommon promptChosen _ -> do bagHuge <- getsState $ \s -> accessModeBag leader s cCur itemToF <- getsState $ flip itemToFull mpsuit <- psuit -- when throwing, this sets eps and checks xhair validity psuitFun <- case mpsuit of SuitsEverything -> return $ \_ _ _ -> True SuitsSomething f -> return f -- When throwing, this function takes -- missile range into accout. ItemRoles itemRoles <- getsSession sroles let slore = loreFromMode cCur itemRole = itemRoles EM.! slore bagAll = EM.filterWithKey (\iid _ -> iid `ES.member` itemRole) bagHuge mstore = case cCur of MStore store -> Just store _ -> Nothing filterP = psuitFun mstore . itemToF bagSuit = EM.filterWithKey filterP bagAll bagFiltered = case itemDialogState of ISuitable -> bagSuit IAll -> bagAll iids = sortIids itemToF $ EM.assocs bagFiltered keyDefsExtra = [ let km = K.mkChar '+' in (km, DefItemKey { defLabel = Right km , defCond = bagAll /= bagSuit , defAction = recCall cCur cRest $ case itemDialogState of ISuitable -> IAll IAll -> ISuitable }) , let km = K.mkChar '*' in (km, useMultipleDef $ Right km) , let km = K.mkChar '!' in (km, useMultipleDef $ Left "") -- alias close to 'g' ] useMultipleDef defLabel = DefItemKey { defLabel , defCond = permitMulitple && not (null iids) , defAction = case cCur of MStore rstore -> return $! Right $ RStore rstore $ map fst iids _ -> error "transition: multiple items not for MStore" } keyDefs = keyDefsCommon ++ filter (defCond . snd) keyDefsExtra runDefInventory keyDefs promptChosen leader cCur iids runDefMessage :: MonadClientUI m => [(K.KM, DefItemKey m)] -> Text -> m () runDefMessage keyDefs prompt = do let wrapB s = "[" <> s <> "]" keyLabelsRaw = lefts $ map (defLabel . snd) keyDefs keyLabels = filter (not . T.null) keyLabelsRaw choice = T.intercalate " " $ map wrapB $ nub keyLabels -- switch to Data.Containers.ListUtils.nubOrd when we drop GHC 8.4.4 msgAdd MsgPromptGeneric $ prompt <+> choice runDefAction :: MonadClientUI m => [(K.KM, DefItemKey m)] -> (MenuSlot -> Either Text ResultItemDialogMode) -> KeyOrSlot -> m (Either Text ResultItemDialogMode) runDefAction keyDefs slotDef ekm = case ekm of Left km -> case km `lookup` keyDefs of Just keyDef -> defAction keyDef Nothing -> error $ "unexpected key:" `showFailure` K.showKM km Right slot -> return $! slotDef slot runDefSkills :: MonadClientUI m => [(K.KM, DefItemKey m)] -> Text -> ActorId -> m (Either Text ResultItemDialogMode) runDefSkills keyDefsCommon promptChosen leader = do CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui runDefMessage keyDefsCommon promptChosen let itemKeys = map fst keyDefsCommon keys = rights $ map (defLabel . snd) keyDefsCommon okx <- skillsOverlay leader sli <- overlayToSlideshow (rheight - 2) keys okx ekm <- displayChoiceScreenWithDefItemKey (skillsInRightPane leader) sli itemKeys (show MSkills) runDefAction keyDefsCommon (Right . RSkills) ekm skillsInRightPane :: MonadClientUI m => ActorId -> Int -> MenuSlot -> m OKX skillsInRightPane leader width slot = do FontSetup{propFont} <- getFontSetup (prompt, attrString) <- skillCloseUp leader slot let promptAS | T.null prompt = [] | otherwise = textFgToAS Color.Brown $ prompt <> "\n\n" ov = EM.singleton propFont $ offsetOverlay $ splitAttrString width width $ promptAS ++ attrString return (ov, []) runDefPlaces :: MonadClientUI m => [(K.KM, DefItemKey m)] -> Text -> m (Either Text ResultItemDialogMode) runDefPlaces keyDefsCommon promptChosen = do COps{coplace} <- getsState scops CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui soptions <- getsClient soptions places <- getsState $ EM.assocs . placesFromState coplace (sexposePlaces soptions) runDefMessage keyDefsCommon promptChosen let itemKeys = map fst keyDefsCommon keys = rights $ map (defLabel . snd) keyDefsCommon okx <- placesOverlay sli <- overlayToSlideshow (rheight - 2) keys okx ekm <- displayChoiceScreenWithDefItemKey (placesInRightPane places) sli itemKeys (show MPlaces) runDefAction keyDefsCommon (Right . RPlaces) ekm placesInRightPane :: MonadClientUI m => [( ContentId PK.PlaceKind , (ES.EnumSet LevelId, Int, Int, Int) )] -> Int -> MenuSlot -> m OKX placesInRightPane places width slot = do FontSetup{propFont} <- getFontSetup soptions <- getsClient soptions (prompt, blurbs) <- placeCloseUp places (sexposePlaces soptions) slot let promptAS | T.null prompt = [] | otherwise = textFgToAS Color.Brown $ prompt <> "\n\n" splitText = splitAttrString width width ov = attrLinesToFontMap $ map (second $ concatMap splitText) $ (propFont, [promptAS]) : blurbs return (ov, []) runDefFactions :: MonadClientUI m => [(K.KM, DefItemKey m)] -> Text -> m (Either Text ResultItemDialogMode) runDefFactions keyDefsCommon promptChosen = do CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui sroles <- getsSession sroles factions <- getsState $ factionsFromState sroles runDefMessage keyDefsCommon promptChosen let itemKeys = map fst keyDefsCommon keys = rights $ map (defLabel . snd) keyDefsCommon okx <- factionsOverlay sli <- overlayToSlideshow (rheight - 2) keys okx ekm <- displayChoiceScreenWithDefItemKey (factionsInRightPane factions) sli itemKeys (show MFactions) runDefAction keyDefsCommon (Right . RFactions) ekm factionsInRightPane :: MonadClientUI m => [(FactionId, Faction)] -> Int -> MenuSlot -> m OKX factionsInRightPane factions width slot = do FontSetup{propFont} <- getFontSetup (prompt, blurbs) <- factionCloseUp factions slot let promptAS | T.null prompt = [] | otherwise = textFgToAS Color.Brown $ prompt <> "\n\n" splitText = splitAttrString width width ov = attrLinesToFontMap $ map (second $ concatMap splitText) $ (propFont, [promptAS]) : blurbs return (ov, []) runDefModes :: MonadClientUI m => [(K.KM, DefItemKey m)] -> Text -> m (Either Text ResultItemDialogMode) runDefModes keyDefsCommon promptChosen = do CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui runDefMessage keyDefsCommon promptChosen let itemKeys = map fst keyDefsCommon keys = rights $ map (defLabel . snd) keyDefsCommon okx <- modesOverlay sli <- overlayToSlideshow (rheight - 2) keys okx -- Modes would cover the whole screen, so we don't display in right pane. -- But we display and highlight menu bullets. ekm <- displayChoiceScreenWithDefItemKey (\_ _ -> return emptyOKX) sli itemKeys (show MModes) runDefAction keyDefsCommon (Right . RModes) ekm runDefInventory :: MonadClientUI m => [(K.KM, DefItemKey m)] -> Text -> ActorId -> ItemDialogMode -> [(ItemId, ItemQuant)] -> m (Either Text ResultItemDialogMode) runDefInventory keyDefs promptChosen leader dmode iids = do CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui actorCurAndMaxSk <- getsState $ getActorMaxSkills leader let meleeSkill = Ability.getSk Ability.SkHurtMelee actorCurAndMaxSk slotDef :: MenuSlot -> Either Text ResultItemDialogMode slotDef slot = let iid = fst $ iids !! fromEnum slot in Right $ case dmode of MStore rstore -> RStore rstore [iid] MOwned -> ROwned iid MLore rlore -> RLore rlore slot iids _ -> error $ "" `showFailure` dmode promptFun _iid _itemFull _k = "" -- TODO, e.g., if the party still owns any copies, if the actor -- was ever killed by us or killed ours, etc. -- This can be the same prompt or longer than what entering -- the item screen shows. runDefMessage keyDefs promptChosen let itemKeys = map fst keyDefs keys = rights $ map (defLabel . snd) keyDefs okx <- itemOverlay iids dmode sli <- overlayToSlideshow (rheight - 2) keys okx ekm <- displayChoiceScreenWithDefItemKey (okxItemLoreInline promptFun meleeSkill dmode iids) sli itemKeys (show dmode) runDefAction keyDefs slotDef ekm skillCloseUp :: MonadClientUI m => ActorId -> MenuSlot -> m (Text, AttrString) skillCloseUp leader slot = do b <- getsState $ getActorBody leader bUI <- getsSession $ getActorUI leader actorCurAndMaxSk <- getsState $ getActorMaxSkills leader let skill = skillsInDisplayOrder !! fromEnum slot valueText = skillToDecorator skill b $ Ability.getSk skill actorCurAndMaxSk prompt = makeSentence [ MU.WownW (partActor bUI) (MU.Text $ skillName skill) , "is", MU.Text valueText ] attrString = textToAS $ skillDesc skill return (prompt, attrString) placeCloseUp :: MonadClientUI m => [(ContentId PK.PlaceKind, (ES.EnumSet LevelId, Int, Int, Int))] -> Bool -> MenuSlot -> m (Text, [(DisplayFont, [AttrString])]) placeCloseUp places sexposePlaces slot = do COps{coplace} <- getsState scops FontSetup{..} <- getFontSetup let (pk, (es, ne, na, _)) = places !! fromEnum slot pkind = okind coplace pk prompt = makeSentence ["you remember", MU.Text $ PK.pname pkind] freqsText = "Frequencies:" <+> T.intercalate " " (map (\(grp, n) -> "(" <> displayGroupName grp <> ", " <> tshow n <> ")") $ PK.pfreq pkind) onLevels | ES.null es = [] | otherwise = [makeSentence [ "Appears on" , MU.CarWs (ES.size es) "level" <> ":" , MU.WWandW $ map MU.Car $ sort $ map (abs . fromEnum) $ ES.elems es ]] placeParts = ["it has" | ne > 0 || na > 0] ++ [MU.CarWs ne "entrance" | ne > 0] ++ ["and" | ne > 0 && na > 0] ++ [MU.CarWs na "surrounding" | na > 0] partsSentence | null placeParts = [] | otherwise = [makeSentence placeParts, "\n"] blurbs = [(propFont, partsSentence)] ++ [(monoFont, [freqsText, "\n"]) | sexposePlaces] ++ [(squareFont, PK.ptopLeft pkind ++ ["\n"]) | sexposePlaces] ++ [(propFont, onLevels)] return (prompt, map (second $ map textToAS) blurbs) factionCloseUp :: MonadClientUI m => [(FactionId, Faction)] -> MenuSlot -> m (Text, [(DisplayFont, [AttrString])]) factionCloseUp factions slot = do side <- getsClient sside FontSetup{propFont} <- getFontSetup factionD <- getsState sfactionD let (fid, fact@Faction{gkind=FK.FactionKind{..}, ..}) = factions !! fromEnum slot (name, person) = if fhasGender -- but we ignore "Controlled", etc. then (makePhrase [MU.Ws $ MU.Text fname], MU.PlEtc) else (fname, MU.Sg3rd) (youThey, prompt) = if fid == side then ("You", makeSentence ["you are the", MU.Text name]) else ("They", makeSentence ["you are wary of the", MU.Text name]) -- wary even if the faction is allied ts1 = -- Display only the main groups, not to spam. case map fst $ filter ((>= 100) . snd) fgroups of [] -> [] -- only initial actors in the faction? [fgroup] -> [makeSentence [ "the faction consists of" , MU.Ws $ MU.Text $ displayGroupName fgroup ]] grps -> [makeSentence [ "the faction attracts members such as:" , MU.WWandW $ map (MU.Text . displayGroupName) grps ]] ++ [if fskillsOther == Ability.zeroSkills -- simplified then youThey <+> "don't care about each other and crowd and stampede all at once, sometimes brutally colliding by accident." else youThey <+> "pay attention to each other and take care to move one at a time."] ++ [ if fcanEscape then "The faction is able to take part in races to an area exit." else "The faction doesn't escape areas of conflict and attempts to block exits instead."] ++ [ "When all members are incapacitated, the faction dissolves." | fneverEmpty ] ++ [if fhasGender then "Its members are known to have sexual dimorphism and use gender pronouns." else "Its members seem to prefer naked ground for sleeping."] ++ [ "Its ranks swell with time." | fspawnsFast ] ++ [ "The faction is able to maintain activity on a level on its own, with a pointman coordinating each tactical maneuver." | fhasPointman ] -- Changes to all of these have visibility @PosAll@, so the player -- knows them fully, except for @gvictims@, which is coupled to tracking -- other factions' actors and so only incremented when we've seen -- their actor killed (mostly likely killed by us). ts2 = -- reporting regardless of whether any of the factions are dead let renderDiplGroup [] = error "renderDiplGroup: null" renderDiplGroup ((fid2, diplomacy) : rest) = MU.Phrase [ MU.Text $ tshowDiplomacy diplomacy , "with" , MU.WWandW $ map renderFact2 $ fid2 : map fst rest ] renderFact2 fid2 = MU.Text $ Faction.gname (factionD EM.! fid2) valid (fid2, diplomacy) = isJust (lookup fid2 factions) && diplomacy /= Unknown knownAssocsGroups = groupBy ((==) `on` snd) $ sortOn snd $ filter valid $ EM.assocs gdipl in [ makeSentence [ MU.SubjectVerb person MU.Yes (MU.Text name) "be" , MU.WWandW $ map renderDiplGroup knownAssocsGroups ] | not (null knownAssocsGroups) ] ts3 = case gquit of Just Status{..} | not $ isHorrorFact fact -> ["The faction has already" <+> FK.nameOutcomePast stOutcome <+> "around level" <+> tshow (abs stDepth) <> "."] _ -> [] ++ let nkilled = sum $ EM.elems gvictims personKilled = if nkilled == 1 then MU.Sg3rd else MU.PlEtc in [ makeSentence $ [ "so far," | isNothing gquit ] ++ [ "at least" , MU.CardinalWs nkilled "member" , MU.SubjectVerb personKilled MU.Yes "of this faction" "have been incapacitated" ] | nkilled > 0 ] ++ let adjective = if isNothing gquit then "current" else "last" verb = if isNothing gquit then "is" else "was" in ["Its" <+> adjective <+> "doctrine" <+> verb <+> "'" <> Ability.nameDoctrine gdoctrine <> "' (" <> Ability.describeDoctrine gdoctrine <> ")."] -- Description of the score polynomial would go into a separate section, -- but it's hard to make it sound non-technical enough. blurbs = intersperse ["\n"] $ filter (not . null) [ts1, ts2, ts3] return (prompt, map (\t -> (propFont, map textToAS t)) blurbs) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/ItemDescription.hs0000644000000000000000000006244307346545000024143 0ustar0000000000000000-- | Descriptions of items. module Game.LambdaHack.Client.UI.ItemDescription ( partItem, partItemShort, partItemShortest, partItemHigh , partItemWsDetail, partItemWs, partItemWsShortest, partItemWsShort , partItemWsLong, partItemWsRanged , partItemShortAW, partItemMediumAW, partItemShortWownW , viewItem, viewItemBenefitColored, itemDesc #ifdef EXPOSE_INTERNAL -- * Internal operations , partItemN, textAllPowers #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Char (isAlpha, isAlphaNum) import qualified Data.EnumMap.Strict as EM import Data.Int (Int64) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.Flavour partItemN :: Int -> FactionId -> FactionDict -> Bool -> DetailLevel -> Int -> Time -> ItemFull -> ItemQuant -> (MU.Part, MU.Part) partItemN width side factionD ranged detailLevel maxWordsToShow localTime itemFull kit = let (_, r2, r3) = partItemN3 width side factionD ranged detailLevel maxWordsToShow localTime itemFull kit in (r2, r3) -- | The part of speech describing the item parameterized by the number -- of effects/aspects to show. partItemN3 :: Int -> FactionId -> FactionDict -> Bool -> DetailLevel -> Int -> Time -> ItemFull -> ItemQuant -> ([Text], MU.Part, MU.Part) partItemN3 width side factionD ranged detailLevel maxWordsToShow localTime itemFull@ItemFull{itemBase, itemKind, itemSuspect} (itemK, itemTimers) = let flav = flavourToName $ jflavour itemBase arItem = aspectRecordFull itemFull timeout = IA.aTimeout arItem temporary = IA.checkFlag Ability.Fragile arItem && IA.checkFlag Ability.Periodic arItem ncha = ncharges localTime (itemK, itemTimers) charges | temporary = case itemTimers of [] -> if itemK == ncha then "" else error $ "partItemN3: charges with null timer" `showFailure` (side, itemFull, itemK, itemTimers) t : _ -> if itemK == ncha then "(ready to expire)" else let total = deltaOfItemTimer localTime t in "for" <+> timeDeltaInSecondsText total | itemK == ncha = "" | itemK == 1 && ncha == 0 = "(charging)" | ncha == 0 = "(all charging)" | otherwise = "(" <> tshow (itemK - ncha) <+> "charging)" skipRecharging = detailLevel <= DetailLow && ncha == 0 (orTs, powerTs, rangedDamage) = textAllPowers width detailLevel skipRecharging itemFull lsource = case jfid itemBase of Just fid | IK.iname itemKind == "impressed" -> ["by" <+> if fid == side then "us" else gname (factionD EM.! fid)] _ -> [] powerTsBeginsWithAlphaOrNum = case map T.unpack powerTs of (c : _) : _ -> isAlpha c || isAlphaNum c _ -> False -- Ranged damage displayed even if lack of space, to prevent confusion -- and ... when only ranged damage is missing from the description. displayPowers = maxWordsToShow > 1 || powerTsBeginsWithAlphaOrNum && length powerTs == 1 ts = lsource ++ (if displayPowers then take maxWordsToShow powerTs else []) ++ ["(...)" | displayPowers && length powerTs > maxWordsToShow] ++ (if displayPowers && ranged then rangedDamage else []) ++ [charges | maxWordsToShow > 1] name | temporary = let adj = if timeout == 0 then "temporarily" else "impermanent" in adj <+> IK.iname itemKind | itemSuspect = flav <+> IK.iname itemKind | otherwise = IK.iname itemKind in (orTs, MU.Text name, if displayPowers then MU.Phrase $ map MU.Text ts else MU.Text $ IA.aELabel arItem) -- TODO: simplify the code a lot textAllPowers :: Int -> DetailLevel -> Bool -> ItemFull -> ([Text], [Text], [Text]) textAllPowers width detailLevel skipRecharging itemFull@ItemFull{itemKind, itemDisco} = let arItem = aspectRecordFull itemFull -- To handle both the cases of item identified and not, we represent -- aspects as a list with dice, not a record of integers as in @arItem@. -- If item fully known, the dice will be trivial and will display -- the same as integers would, so nothing is lost. -- If item not known fully and timeouts or any crucial flags -- are under @Odds@, they are ignored, so they should be avoided -- under @Odds@ in not fully-identified items. aspectsFull = case itemDisco of ItemDiscoMean IA.KindMean{..} | kmConst -> IA.aspectRecordToList kmMean -- exact and collated ItemDiscoMean{} -> IK.iaspects itemKind -- doesn't completely lose the @Odds@ case, so better than -- the above, even if does not collate multiple skill bonuses ItemDiscoFull iAspect -> IA.aspectRecordToList iAspect mtimeout = find IK.timeoutAspect aspectsFull elab = IA.aELabel arItem periodic = IA.checkFlag Ability.Periodic arItem hurtMeleeAspect :: IK.Aspect -> Bool hurtMeleeAspect (IK.AddSkill Ability.SkHurtMelee _) = True hurtMeleeAspect _ = False active = IA.goesIntoEqp arItem splitA :: DetailLevel -> [IK.Aspect] -> ([Text], [Text]) splitA detLev aspects = let ppA = kindAspectToSuffix ppE = effectToSuffix detLev reduce_a = maybe "?" tshow . Dice.reduceDice restEs | detLev >= DetailHigh || not (IA.checkFlag Ability.MinorEffects arItem) = IK.ieffects itemKind | otherwise = [] (smashEffs, noSmashEffs) = partition IK.onSmashEffect restEs unSmash (IK.OnSmash eff) = eff unSmash eff = eff onSmashTs = T.intercalate " " $ filter (not . T.null) $ map (ppE . unSmash) smashEffs unCombine (IK.OnCombine eff) = eff unCombine eff = eff (combineEffsRaw, noSmashCombineEffsRaw) = partition IK.onCombineEffect noSmashEffs onCombineRawTs = T.intercalate " " $ filter (not . T.null) $ map (ppE . unCombine) combineEffsRaw onCombineRawTsTooLarge = detailLevel >= DetailAll && T.length onCombineRawTs > 120 (combineEffs, noSmashCombineEffs) = if onCombineRawTsTooLarge then (combineEffsRaw, noSmashCombineEffsRaw) else ([], noSmashEffs) unOr (IK.OrEffect eff1 eff2) = unOr eff1 ++ unOr eff2 unOr eff = [eff] ppAnd (IK.AndEffect (IK.ConsumeItems tools raw) eff) = let (tcraft, traw, ttools) = describeCrafting tools raw eff in if T.length tcraft + T.length traw + T.length ttools <= width - 4 then tcraft <+> traw <+> ttools else tcraft <> "\n---" <+> traw <> "\n---" <+> ttools ppAnd eff = ppE eff ppOr eff = "*" <+> T.intercalate "\n* " (nub $ filter (not . T.null) $ map ppAnd $ unOr eff) onCombineTs = filter (not . T.null) $ map (ppOr . unCombine) combineEffs rechargingTs = T.intercalate " " $ [damageText | IK.idamage itemKind /= 0] ++ filter (not . T.null) (map ppE noSmashCombineEffs) fragile = IA.checkFlag Ability.Fragile arItem periodicText = if periodic && not skipRecharging && not (T.null rechargingTs) then case (mtimeout, fragile) of (Nothing, True) -> "(each turn until gone:" <+> rechargingTs <> ")" (Nothing, False) -> "(each turn:" <+> rechargingTs <> ")" -- timeout 0, so it just fires each turn and it's not -- fragile, so a copy is not destroyed each turn (Just (IK.Timeout t), True) -> "(every" <+> reduce_a t <+> "until gone:" <+> rechargingTs <> ")" (Just (IK.Timeout t), False) -> "(every" <+> reduce_a t <> ":" <+> rechargingTs <> ")" _ -> error $ "" `showFailure` mtimeout else "" ppERestEs = if periodic then [periodicText] else map ppE noSmashCombineEffs aes = if active then map ppA aspects ++ ppERestEs else ppERestEs ++ map ppA aspects onSmash = if T.null onSmashTs then "" else "(on smash:" <+> onSmashTs <> ")" onCombine = if null combineEffs && not (T.null onCombineRawTs) then "(on combine:" <+> onCombineRawTs <> ")" else "" -- Either exact value or dice of @SkHurtMelee@ needed, -- never the average, so @arItem@ not consulted directly. -- If item not known fully and @SkHurtMelee@ under @Odds@, -- it's ignored. damageText = case find hurtMeleeAspect aspects of Just (IK.AddSkill Ability.SkHurtMelee hurtMelee) -> (if IK.idamage itemKind == 0 then "0d0" else tshow (IK.idamage itemKind)) <> affixDice hurtMelee <> "%" _ -> if IK.idamage itemKind == 0 then "" else tshow (IK.idamage itemKind) timeoutText = case mtimeout of Nothing -> "" Just (IK.Timeout t) -> "(cooldown" <+> reduce_a t <> ")" -- timeout is called "cooldown" in UI _ -> error $ "" `showFailure` mtimeout in ( onCombineTs , [damageText] ++ [timeoutText | detLev > DetailLow && not periodic] ++ aes ++ if detLev >= DetailAll then [onCombine, onSmash] else [onCombineRawTs] ) hurtMult = armorHurtCalculation True (IA.aSkills arItem) Ability.zeroSkills dmg = Dice.meanDice $ IK.idamage itemKind rawDeltaHP = ceiling $ intToDouble hurtMult * xD dmg / 100 IK.ThrowMod{IK.throwVelocity} = IA.aToThrow arItem speed = speedFromWeight (IK.iweight itemKind) throwVelocity pdeltaHP = modifyDamageBySpeed rawDeltaHP speed rangedDamageDesc = [ "{avg" <+> show64With2 pdeltaHP <+> "ranged}" | pdeltaHP > 0 ] -- Note that avg melee damage would be too complex to display here, -- because in case of @MOwned@ the owner is different than leader, -- so the value would be different than when viewing the item. splitTry ass = let splits = map (`splitA` ass) [minBound..maxBound] splitsToTry = drop (fromEnum detailLevel) splits splitsValid | T.null elab = filter (/= ([], [])) splitsToTry | otherwise = splitsToTry in case splitsValid of (onCombineTsSplit, tsSplit) : _ -> (onCombineTsSplit, tsSplit) [] -> ([], []) (onCombineTsAss, aspectDescs) = let aMain IK.AddSkill{} = True aMain _ = False (aspectsMain, aspectsAux) = partition aMain aspectsFull (onCombineTsSplit, tsSplit) = splitTry aspectsMain in ( onCombineTsSplit , filter (/= "") $ elab : tsSplit ++ if detailLevel >= DetailAll then map kindAspectToSuffix aspectsAux else [] ) in (onCombineTsAss, aspectDescs, rangedDamageDesc) -- | The part of speech describing the item. partItem :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> (MU.Part, MU.Part) partItem width side factionD = partItemN width side factionD False DetailMedium 4 partItemShort :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> (MU.Part, MU.Part) partItemShort width side factionD = partItemN width side factionD False DetailLow 4 partItemShortest :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> (MU.Part, MU.Part) partItemShortest width side factionD = partItemN width side factionD False DetailLow 1 partItemHigh :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> ([Text], MU.Part, MU.Part) partItemHigh width side factionD = partItemN3 width side factionD False DetailAll 100 -- The @count@ can be different than @itemK@ in @ItemFull@, e.g., when picking -- a subset of items to drop. partItemWsRanged :: Int -> FactionId -> FactionDict -> Bool -> DetailLevel -> Int -> Int -> Time -> ItemFull -> ItemQuant -> MU.Part partItemWsRanged width side factionD ranged detail maxWordsToShow count localTime itemFull kit = let (name, powers) = partItemN width side factionD ranged detail maxWordsToShow localTime itemFull kit arItem = aspectRecordFull itemFull periodic = IA.checkFlag Ability.Periodic arItem condition = IA.checkFlag Ability.Condition arItem maxCount = Dice.supDice $ IK.icount $ itemKind itemFull in if | condition && count == 1 -> MU.Phrase [name, powers] | condition && not periodic && maxCount > 1 -> let percent = 100 * count `divUp` maxCount amount = tshow count <> "-strong" <+> "(" <> tshow percent <> "%)" in MU.Phrase [MU.Text amount, name, powers] | condition -> MU.Phrase [MU.Text $ tshow count <> "-fold", name, powers] | IA.checkFlag Ability.Unique arItem -> case count of 0 -> MU.Phrase ["none of", name, powers] 1 -> MU.Phrase [name, powers] _ -> MU.Phrase [MU.Car count, "of", MU.Ws name, powers] | otherwise -> MU.Phrase [MU.CarAWs count name, powers] partItemWsDetail :: DetailLevel -> Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull -> ItemQuant -> MU.Part partItemWsDetail DetailLow = \_ _ _ _ _ _ _ -> "" partItemWsDetail DetailMedium = partItemWsShortest partItemWsDetail DetailHigh = partItemWs partItemWsDetail DetailAll = partItemWsLong partItemWs :: Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull -> ItemQuant -> MU.Part partItemWs width side factionD = partItemWsRanged width side factionD False DetailMedium 4 partItemWsShortest :: Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull -> ItemQuant -> MU.Part partItemWsShortest width side factionD = partItemWsRanged width side factionD False DetailLow 1 partItemWsShort :: Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull -> ItemQuant -> MU.Part partItemWsShort width side factionD = partItemWsRanged width side factionD False DetailLow 4 partItemWsLong :: Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull -> ItemQuant -> MU.Part partItemWsLong width side factionD = partItemWsRanged width side factionD False DetailHigh 100 partItemShortAW :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> MU.Part partItemShortAW width side factionD localTime itemFull kit = let (name, _) = partItemShort width side factionD localTime itemFull kit arItem = aspectRecordFull itemFull in if IA.checkFlag Ability.Unique arItem then name else MU.AW name partItemMediumAW :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant -> MU.Part partItemMediumAW width side factionD localTime itemFull kit = let (name, powers) = partItemN width side factionD False DetailMedium 100 localTime itemFull kit arItem = aspectRecordFull itemFull phrase = MU.Phrase [name, powers] in if IA.checkFlag Ability.Unique arItem then phrase else MU.AW phrase partItemShortWownW :: Int -> FactionId -> FactionDict -> MU.Part -> Time -> ItemFull -> ItemQuant -> MU.Part partItemShortWownW width side factionD partA localTime itemFull kit = let (name, _) = partItemShort width side factionD localTime itemFull kit in MU.WownW partA name viewItem :: ItemFull -> Color.AttrCharW32 {-# INLINE viewItem #-} viewItem itemFull = Color.attrChar2ToW32 (flavourToColor $ jflavour $ itemBase itemFull) (displayContentSymbol $ IK.isymbol $ itemKind itemFull) viewItemBenefitColored :: DiscoveryBenefit -> ItemId -> ItemFull -> Color.AttrCharW32 viewItemBenefitColored discoBenefit iid itemFull = -- The map @discoBenefit@ is normally used by AI to tell it in what role -- an item can be employed. In particular, ` benInEqp` says if an item -- buffs stats enough (and nerfs not too much) to be worth equipping. -- Here it's (ab)used to tell if an item (only a status effect item -- in this case, marked with `Ability.Condition`) is beneficial or not -- and to signal that in the organs UI menu. let color = if benInEqp (discoBenefit EM.! iid) then Color.BrGreen else Color.BrRed in Color.attrChar2ToW32 color (displayContentSymbol $ IK.isymbol $ itemKind itemFull) itemDesc :: Int -> Bool -> FactionId -> FactionDict -> Int -> ItemDialogMode -> Time -> LevelId -> ItemFull -> ItemQuant -> AttrString itemDesc width markParagraphs side factionD aHurtMeleeOfOwner dmode localTime jlid itemFull@ItemFull{itemBase, itemKind, itemDisco, itemSuspect} kit = let (orTs, name, powers) = partItemHigh width side factionD localTime itemFull kit arItem = aspectRecordFull itemFull npowers = makePhrase [name, powers] IK.ThrowMod{IK.throwVelocity, IK.throwLinger} = IA.aToThrow arItem speed = speedFromWeight (IK.iweight itemKind) throwVelocity range = rangeFromSpeedAndLinger speed throwLinger plausiblyThrown = dmode `elem` [ MStore CGround, MStore CEqp, MStore CStash , MOwned, MLore SItem ] plausiblyFlies = dmode == MLore SBlast tspeed | not (plausiblyThrown || plausiblyFlies) = "" | speed < speedLimp = if plausiblyThrown then "When thrown, it drops at once." else "When airborne, it drops at once." | speed < speedWalk = if plausiblyThrown then "When thrown, it drops after one meter." else "When airborne, it drops after one meter." | otherwise = (if plausiblyThrown then "Can be thrown at" else "Travels at") <+> T.pack (displaySpeed $ fromSpeed speed) <> (if throwLinger /= 100 then let trange = if range == 0 then "immediately" else "after" <+> tshow range <> "m" in " dropping" <+> trange -- comma here is logical but looks bad else "") <> "." tsuspect = ["You are unsure what it does." | itemSuspect] (desc, aspectSentences, damageAnalysis) = let aspects = case itemDisco of ItemDiscoMean IA.KindMean{..} | kmConst -> IA.aspectRecordToList kmMean -- exact and collated ItemDiscoMean{} -> IK.iaspects itemKind -- doesn't completely lose the @Odds@ case, so better than -- the above, even if does not collate multiple skill bonuses ItemDiscoFull iAspect -> IA.aspectRecordToList iAspect sentences = tsuspect ++ mapMaybe aspectToSentence aspects aHurtMeleeOfItem = IA.getSkill Ability.SkHurtMelee arItem meanDmg = ceiling $ Dice.meanDice (IK.idamage itemKind) dmgAn = if meanDmg <= 0 then "" else let multRaw = aHurtMeleeOfOwner + if dmode `elem` [MStore CEqp, MStore COrgan] then 0 else aHurtMeleeOfItem mult = 100 + min 100 (max (-95) multRaw) percentDeltaHP = xM meanDmg `divUp` 100 rawDeltaHP = into @Int64 mult * percentDeltaHP pmult = 100 + min 100 (max (-95) aHurtMeleeOfItem) prawDeltaHP = into @Int64 pmult * percentDeltaHP pdeltaHP = modifyDamageBySpeed prawDeltaHP speed minDeltaHP = 5 * percentDeltaHP mDeltaHP = modifyDamageBySpeed minDeltaHP speed in "Against defenceless foes you'd inflict around" -- rounding and non-id items <+> tshow meanDmg <> "*" <> tshow mult <> "%" <> "=" <> show64With2 rawDeltaHP <+> "melee damage (min" <+> show64With2 minDeltaHP <> ")" <+> (if pdeltaHP <= 0 then "" else "and" <+> tshow meanDmg <> "*" <> tshow pmult <> "%" <> "*" <> "speed^2" <> "/" <> tshow (fromSpeed speedThrust `divUp` 10) <> "^2" <> "=" <> show64With2 pdeltaHP <+> "ranged damage (min" <+> show64With2 mDeltaHP <> ")") <+> "with it" <> if Dice.infDice (IK.idamage itemKind) == Dice.supDice (IK.idamage itemKind) then "." else "on average." in (IK.idesc itemKind, T.intercalate " " sentences, tspeed <+> dmgAn) weight = IK.iweight itemKind (scaledWeight, unitWeight) | weight > 1000 = (tshow $ intToDouble weight / 1000, "kg") | otherwise = (tshow weight, "g") onLevel = "on level" <+> tshow (abs $ fromEnum jlid) <> "." discoFirst = (if IA.checkFlag Ability.Unique arItem then "Discovered" else "First seen") <+> onLevel whose fid = gname (factionD EM.! fid) sourceDesc = case jfid itemBase of Just fid | IA.checkFlag Ability.Condition arItem -> "Caused by" <+> (if fid == side then "us" else whose fid) <> ". First observed" <+> onLevel Just fid -> "Coming from" <+> whose fid <> "." <+> discoFirst _ -> discoFirst -- Organs are almost always either empty or more than singular, -- so the "organs" below is fine. Also, some organs come in pairs -- or more, so we don't know the number without much more work, -- so @squashedWWandW@ would be out of place. Also, mentioning -- two hands and two legs is not that enlightening and the number -- is not shown in organ lore, so this should wait until we add -- proper hyperlinks both ways instead of relying of names. ikitToPart = MU.Text . T.intercalate ", " . map (displayGroupName . fst) (ikitOrganNames, ikitOtherNames) = partition ((== COrgan) . snd) $ IK.ikit itemKind ikitDesc | null ikitOrganNames = "" | otherwise = makeSentence [ "the actor has organs of this kind:" , ikitToPart ikitOrganNames ] <> if null ikitOtherNames then "" else "\n\n" <> makeSentence [ "the actor starts in possession of the following:" , ikitToPart ikitOtherNames ] colorSymbol = viewItem itemFull blurb = (((" " <> npowers <> (if markParagraphs then "\n\n" else " ") <> T.intercalate "\n\n" orTs <> (if markParagraphs && not (null orTs) then "\n\n" else "") <> desc <> (if markParagraphs && not (T.null desc) then "\n\n" else "")) <+> (if weight > 0 then makeSentence ["Weighs around", MU.Text scaledWeight <> unitWeight] else "")) <+> aspectSentences <+> sourceDesc <+> damageAnalysis) <> (if markParagraphs && not (T.null ikitDesc) then "\n\n" else "\n") <> ikitDesc in colorSymbol : textToAS blurb LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Key.hs0000644000000000000000000004467707346545000021602 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Frontend-independent keyboard input operations. module Game.LambdaHack.Client.UI.Key ( Key(..), Modifier(..), KM(..), KMP(..) , showKey, showKM , escKM, controlEscKM, spaceKM, safeSpaceKM, undefinedKM, returnKM , pgupKM, pgdnKM, wheelNorthKM, wheelSouthKM , upKM, downKM, leftKM, rightKM , homeKM, endKM, backspaceKM, controlP , leftButtonReleaseKM, middleButtonReleaseKM, rightButtonReleaseKM , cardinalAllKM, dirAllKey, handleCardinal, handleDir, moveBinding , mkKM, mkChar, keyTranslate, keyTranslateWeb , dirMoveNoModifier, dirRunNoModifier, dirRunControl, dirRunShift #ifdef EXPOSE_INTERNAL -- * Internal operations , dirKeypadKey, dirKeypadShiftChar, dirKeypadShiftKey , dirLeftHandKey, dirLeftHandShiftKey , dirViChar, dirViKey, dirViShiftKey #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude hiding (Left, Right) import Control.DeepSeq import Data.Binary import qualified Data.Char as Char import GHC.Generics (Generic) import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Common.Vector -- | Frontend-independent datatype to represent keys. data Key = Esc | Return | Space | Tab | BackTab | BackSpace | PgUp | PgDn | Left | Right | Up | Down | End | Begin | Insert | Delete | PrintScreen | Home | KP Char -- ^ a keypad key for a character (digits and operators) | Char Char -- ^ a single printable character | Fun Int -- ^ function key | LeftButtonPress -- ^ left mouse button pressed | MiddleButtonPress -- ^ middle mouse button pressed | RightButtonPress -- ^ right mouse button pressed | LeftButtonRelease -- ^ left mouse button released | MiddleButtonRelease -- ^ middle mouse button released | RightButtonRelease -- ^ right mouse button released | WheelNorth -- ^ mouse wheel rotated north | WheelSouth -- ^ mouse wheel rotated south | Unknown String -- ^ an unknown key, registered to warn the user | DeadKey deriving (Ord, Eq, Generic) instance Binary Key instance NFData Key -- | Our own encoding of modifiers. data Modifier = NoModifier | ControlShift | AltShift | Shift | Control | Alt deriving (Show, Ord, Eq, Generic) instance Binary Modifier instance NFData Modifier -- | Key and modifier. data KM = KM { modifier :: Modifier , key :: Key } deriving (Ord, Eq, Generic) instance Binary KM instance NFData KM instance Show KM where show = showKM -- | Key, modifier and position of mouse pointer. data KMP = KMP { kmpKeyMod :: KM , kmpPointer :: PointUI } -- | Common and terse names for keys. showKey :: Key -> String showKey Esc = "ESC" showKey Return = "RET" showKey Space = "SPACE" showKey Tab = "TAB" showKey BackTab = "S-TAB" showKey BackSpace = "BACKSPACE" showKey Up = "UP" showKey Down = "DOWN" showKey Left = "LEFT" showKey Right = "RIGHT" showKey Home = "HOME" showKey End = "END" showKey PgUp = "PGUP" showKey PgDn = "PGDN" showKey Begin = "BEGIN" showKey Insert = "INS" showKey Delete = "DEL" showKey PrintScreen = "PRTSCR" showKey (KP c) = "KP_" ++ [c] showKey (Char c) = [c] showKey (Fun n) = "F" ++ show n showKey LeftButtonPress = "LMB-PRESS" showKey MiddleButtonPress = "MMB-PRESS" showKey RightButtonPress = "RMB-PRESS" showKey LeftButtonRelease = "LMB" showKey MiddleButtonRelease = "MMB" showKey RightButtonRelease = "RMB" showKey WheelNorth = "WHEEL-UP" showKey WheelSouth = "WHEEL-DN" showKey (Unknown s) = s showKey DeadKey = "DEADKEY" -- | Show a key with a modifier, if any. showKM :: KM -> String showKM KM{modifier=NoModifier, key} = showKey key showKM KM{modifier=ControlShift, key} = "C-S-" ++ showKey key showKM KM{modifier=AltShift, key} = "A-S-" ++ showKey key showKM KM{modifier=Shift, key} = "S-" ++ showKey key showKM KM{modifier=Control, key} = "C-" ++ showKey key showKM KM{modifier=Alt, key} = "A-" ++ showKey key escKM :: KM escKM = KM NoModifier Esc controlEscKM :: KM controlEscKM = KM Control Esc spaceKM :: KM spaceKM = KM NoModifier Space safeSpaceKM :: KM safeSpaceKM = KM NoModifier $ Unknown "SAFE_SPACE" undefinedKM :: KM undefinedKM = KM NoModifier $ Unknown "UNDEFINED KEY" returnKM :: KM returnKM = KM NoModifier Return pgupKM :: KM pgupKM = KM NoModifier PgUp pgdnKM :: KM pgdnKM = KM NoModifier PgDn wheelNorthKM :: KM wheelNorthKM = KM NoModifier WheelNorth wheelSouthKM :: KM wheelSouthKM = KM NoModifier WheelSouth upKM :: KM upKM = KM NoModifier Up downKM :: KM downKM = KM NoModifier Down leftKM :: KM leftKM = KM NoModifier Left rightKM :: KM rightKM = KM NoModifier Right homeKM :: KM homeKM = KM NoModifier Home endKM :: KM endKM = KM NoModifier End backspaceKM :: KM backspaceKM = KM NoModifier BackSpace controlP :: KM controlP = KM Control (Char 'P') leftButtonReleaseKM :: KM leftButtonReleaseKM = KM NoModifier LeftButtonRelease middleButtonReleaseKM :: KM middleButtonReleaseKM = KM NoModifier MiddleButtonRelease rightButtonReleaseKM :: KM rightButtonReleaseKM = KM NoModifier RightButtonRelease cardinalKeypadKM :: [KM] cardinalKeypadKM = map (KM NoModifier) [Up, Right, Down, Left] dirKeypadKey :: [Key] dirKeypadKey = [Home, Up, PgUp, Right, PgDn, Down, End, Left] dirKeypadShiftChar :: [Char] dirKeypadShiftChar = ['7', '8', '9', '6', '3', '2', '1', '4'] dirKeypadShiftKey :: [Key] dirKeypadShiftKey = map KP dirKeypadShiftChar cardinalLeftHandKM :: [KM] cardinalLeftHandKM = map (KM NoModifier . Char) ['w', 'd', 'x', 'a'] dirLeftHandKey :: [Key] dirLeftHandKey = map Char ['q', 'w', 'e', 'd', 'c', 'x', 'z', 'a'] dirLeftHandShiftKey :: [Key] dirLeftHandShiftKey = map Char ['Q', 'W', 'E', 'D', 'C', 'X', 'Z', 'A'] cardinalViKM :: [KM] cardinalViKM = map (KM NoModifier . Char) ['k', 'l', 'j', 'h'] dirViChar :: [Char] dirViChar = ['y', 'k', 'u', 'l', 'n', 'j', 'b', 'h'] dirViKey :: [Key] dirViKey = map Char dirViChar dirViShiftKey :: [Key] dirViShiftKey = map (Char . Char.toUpper) dirViChar dirMoveNoModifier :: Bool -> Bool -> [Key] dirMoveNoModifier uVi uLeftHand = dirKeypadKey ++ (if uVi then dirViKey else []) ++ (if uLeftHand then dirLeftHandKey else []) dirRunNoModifier :: Bool -> Bool -> [Key] dirRunNoModifier uVi uLeftHand = dirKeypadShiftKey ++ (if uVi then dirViShiftKey else []) ++ (if uLeftHand then dirLeftHandShiftKey else []) dirRunControl :: [Key] dirRunControl = dirKeypadKey ++ dirKeypadShiftKey ++ map Char dirKeypadShiftChar dirRunShift :: [Key] dirRunShift = dirRunControl cardinalAllKM :: Bool -> Bool -> [KM] cardinalAllKM uVi uLeftHand = concat $ [cardinalKeypadKM] ++ [cardinalViKM | uVi] ++ [cardinalLeftHandKM | uLeftHand] dirAllKey :: Bool -> Bool -> [Key] dirAllKey uVi uLeftHand = dirMoveNoModifier uVi uLeftHand ++ dirRunNoModifier uVi uLeftHand ++ dirRunControl handleCardinal :: [KM] -> KM -> Maybe Vector handleCardinal dirKeys key = let assocs = zip dirKeys $ cycle movesCardinal in lookup key assocs -- | Configurable event handler for the direction keys. -- Used for directed commands such as close door. handleDir :: [Key] -> KM -> Maybe Vector handleDir dirKeys KM{modifier=NoModifier, key} = let assocs = zip dirKeys $ cycle moves in lookup key assocs handleDir _ _ = Nothing -- | Binding of both sets of movement keys, vi and laptop. moveBinding :: Bool -> Bool -> (Vector -> a) -> (Vector -> a) -> [(KM, a)] moveBinding uVi uLeftHand move run = let assign f km dir = (km, f dir) mapMove modifier keys = zipWith (assign move) (map (KM modifier) keys) (cycle moves) mapRun modifier keys = zipWith (assign run) (map (KM modifier) keys) (cycle moves) in mapMove NoModifier (dirMoveNoModifier uVi uLeftHand) ++ mapRun NoModifier (dirRunNoModifier uVi uLeftHand) ++ mapRun Control dirRunControl ++ mapRun Shift dirRunShift mkKM :: String -> KM mkKM s = let mkKey sk = case keyTranslate sk of Unknown _ -> error $ "unknown key" `showFailure` s key -> key in case s of 'C':'-':'S':'-':rest -> KM ControlShift (mkKey rest) 'S':'-':'C':'-':rest -> KM ControlShift (mkKey rest) 'A':'-':'S':'-':rest -> KM AltShift (mkKey rest) 'S':'-':'A':'-':rest -> KM AltShift (mkKey rest) 'S':'-':rest -> KM Shift (mkKey rest) 'C':'-':rest -> KM Control (mkKey rest) 'A':'-':rest -> KM Alt (mkKey rest) _ -> KM NoModifier (mkKey s) mkChar :: Char -> KM mkChar c = KM NoModifier $ Char c -- | Translate key from a GTK string description to our internal key type. -- To be used, in particular, for the command bindings and macros -- in the config file. -- -- See keyTranslate :: String -> Key keyTranslate "less" = Char '<' keyTranslate "greater" = Char '>' keyTranslate "period" = Char '.' keyTranslate "colon" = Char ':' keyTranslate "semicolon" = Char ';' keyTranslate "comma" = Char ',' keyTranslate "question" = Char '?' keyTranslate "numbersign" = Char '#' keyTranslate "dollar" = Char '$' keyTranslate "parenleft" = Char '(' keyTranslate "parenright" = Char ')' keyTranslate "asterisk" = Char '*' -- KP and normal are merged here keyTranslate "KP_Multiply" = Char '*' keyTranslate "slash" = Char '/' keyTranslate "KP_Divide" = Char '/' keyTranslate "bar" = Char '|' keyTranslate "backslash" = Char '\\' keyTranslate "asciicircum" = Char '^' keyTranslate "underscore" = Char '_' keyTranslate "minus" = Char '-' keyTranslate "KP_Subtract" = Char '-' -- KP and normal are merged here keyTranslate "plus" = Char '+' keyTranslate "KP_Add" = Char '+' -- KP and normal are merged here keyTranslate "equal" = Char '=' keyTranslate "bracketleft" = Char '[' keyTranslate "bracketright" = Char ']' keyTranslate "braceleft" = Char '{' keyTranslate "braceright" = Char '}' keyTranslate "caret" = Char '^' keyTranslate "ampersand" = Char '&' keyTranslate "at" = Char '@' keyTranslate "asciitilde" = Char '~' keyTranslate "grave" = Char '`' keyTranslate "exclam" = Char '!' keyTranslate "apostrophe" = Char '\'' keyTranslate "quotedbl" = Char '"' keyTranslate "Escape" = Esc keyTranslate "ESC" = Esc keyTranslate "Return" = Return keyTranslate "RET" = Return keyTranslate "space" = Space keyTranslate "SPACE" = Space keyTranslate "Tab" = Tab keyTranslate "TAB" = Tab keyTranslate "BackTab" = BackTab keyTranslate "ISO_Left_Tab" = BackTab keyTranslate "BackSpace" = BackSpace keyTranslate "BACKSPACE" = BackSpace keyTranslate "Up" = Up keyTranslate "UP" = Up keyTranslate "KP_Up" = Up keyTranslate "Down" = Down keyTranslate "DOWN" = Down keyTranslate "KP_Down" = Down keyTranslate "Left" = Left keyTranslate "LEFT" = Left keyTranslate "KP_Left" = Left keyTranslate "Right" = Right keyTranslate "RIGHT" = Right keyTranslate "KP_Right" = Right keyTranslate "Home" = Home keyTranslate "HOME" = Home keyTranslate "KP_Home" = Home keyTranslate "End" = End keyTranslate "END" = End keyTranslate "KP_End" = End keyTranslate "Page_Up" = PgUp keyTranslate "PGUP" = PgUp keyTranslate "KP_Page_Up" = PgUp keyTranslate "Prior" = PgUp keyTranslate "KP_Prior" = PgUp keyTranslate "Page_Down" = PgDn keyTranslate "PGDN" = PgDn keyTranslate "KP_Page_Down" = PgDn keyTranslate "Next" = PgDn keyTranslate "KP_Next" = PgDn keyTranslate "Begin" = Begin keyTranslate "BEGIN" = Begin keyTranslate "KP_Begin" = Begin keyTranslate "Clear" = Begin keyTranslate "KP_Clear" = Begin keyTranslate "Center" = Begin keyTranslate "KP_Center" = Begin keyTranslate "Insert" = Insert keyTranslate "INS" = Insert keyTranslate "KP_Insert" = Insert keyTranslate "Delete" = Delete keyTranslate "DEL" = Delete keyTranslate "KP_Delete" = Delete keyTranslate "KP_Enter" = Return keyTranslate "F1" = Fun 1 keyTranslate "F2" = Fun 2 keyTranslate "F3" = Fun 3 keyTranslate "F4" = Fun 4 keyTranslate "F5" = Fun 5 keyTranslate "F6" = Fun 6 keyTranslate "F7" = Fun 7 keyTranslate "F8" = Fun 8 keyTranslate "F9" = Fun 9 keyTranslate "F10" = Fun 10 keyTranslate "F11" = Fun 11 keyTranslate "F12" = Fun 12 keyTranslate "LeftButtonPress" = LeftButtonPress keyTranslate "LMB-PRESS" = LeftButtonPress keyTranslate "MiddleButtonPress" = MiddleButtonPress keyTranslate "MMB-PRESS" = MiddleButtonPress keyTranslate "RightButtonPress" = RightButtonPress keyTranslate "RMB-PRESS" = RightButtonPress keyTranslate "LeftButtonRelease" = LeftButtonRelease keyTranslate "LMB" = LeftButtonRelease keyTranslate "MiddleButtonRelease" = MiddleButtonRelease keyTranslate "MMB" = MiddleButtonRelease keyTranslate "RightButtonRelease" = RightButtonRelease keyTranslate "RMB" = RightButtonRelease keyTranslate "WheelNorth" = WheelNorth keyTranslate "WHEEL-UP" = WheelNorth keyTranslate "WheelSouth" = WheelSouth keyTranslate "WHEEL-DN" = WheelSouth -- dead keys keyTranslate "Shift_L" = DeadKey keyTranslate "Shift_R" = DeadKey keyTranslate "Control_L" = DeadKey keyTranslate "Control_R" = DeadKey keyTranslate "Super_L" = DeadKey keyTranslate "Super_R" = DeadKey keyTranslate "Menu" = DeadKey keyTranslate "Alt_L" = DeadKey keyTranslate "Alt_R" = DeadKey keyTranslate "Meta_L" = DeadKey keyTranslate "Meta_R" = DeadKey keyTranslate "ISO_Level2_Shift" = DeadKey keyTranslate "ISO_Level3_Shift" = DeadKey keyTranslate "ISO_Level2_Latch" = DeadKey keyTranslate "ISO_Level3_Latch" = DeadKey keyTranslate "Num_Lock" = DeadKey keyTranslate "NumLock" = DeadKey keyTranslate "Caps_Lock" = DeadKey keyTranslate "CapsLock" = DeadKey keyTranslate "VoidSymbol" = DeadKey -- numeric keypad keyTranslate ['K','P','_',c] = KP c -- standard characters keyTranslate [c] = Char c keyTranslate s = Unknown s -- | Translate key from a Web API string description -- () -- to our internal key type. To be used in web frontends. -- The argument says whether Shift is pressed. keyTranslateWeb :: String -> Bool -> Key keyTranslateWeb "1" True = KP '1' keyTranslateWeb "2" True = KP '2' keyTranslateWeb "3" True = KP '3' keyTranslateWeb "4" True = KP '4' keyTranslateWeb "5" True = KP '5' keyTranslateWeb "6" True = KP '6' keyTranslateWeb "7" True = KP '7' keyTranslateWeb "8" True = KP '8' keyTranslateWeb "9" True = KP '9' keyTranslateWeb "End" True = KP '1' keyTranslateWeb "ArrowDown" True = KP '2' keyTranslateWeb "PageDown" True = KP '3' keyTranslateWeb "ArrowLeft" True = KP '4' keyTranslateWeb "Begin" True = KP '5' keyTranslateWeb "Clear" True = KP '5' keyTranslateWeb "ArrowRight" True = KP '6' keyTranslateWeb "Home" True = KP '7' keyTranslateWeb "ArrowUp" True = KP '8' keyTranslateWeb "PageUp" True = KP '9' keyTranslateWeb "Backspace" _ = BackSpace keyTranslateWeb "Tab" True = BackTab keyTranslateWeb "Tab" False = Tab keyTranslateWeb "BackTab" _ = BackTab keyTranslateWeb "Begin" _ = Begin keyTranslateWeb "Clear" _ = Begin keyTranslateWeb "Enter" _ = Return keyTranslateWeb "Esc" _ = Esc keyTranslateWeb "Escape" _ = Esc keyTranslateWeb "Del" _ = Delete keyTranslateWeb "Delete" _ = Delete keyTranslateWeb "Home" _ = Home keyTranslateWeb "Up" _ = Up keyTranslateWeb "ArrowUp" _ = Up keyTranslateWeb "Down" _ = Down keyTranslateWeb "ArrowDown" _ = Down keyTranslateWeb "Left" _ = Left keyTranslateWeb "ArrowLeft" _ = Left keyTranslateWeb "Right" _ = Right keyTranslateWeb "ArrowRight" _ = Right keyTranslateWeb "PageUp" _ = PgUp keyTranslateWeb "PageDown" _ = PgDn keyTranslateWeb "End" _ = End keyTranslateWeb "Insert" _ = Insert keyTranslateWeb "space" _ = Space keyTranslateWeb "Equals" _ = Char '=' keyTranslateWeb "Multiply" _ = Char '*' -- KP and normal are merged here keyTranslateWeb "*" _ = Char '*' keyTranslateWeb "Add" _ = Char '+' -- KP and normal are merged here keyTranslateWeb "Subtract" _ = Char '-' -- KP and normal are merged here keyTranslateWeb "Divide" True = Char '?' keyTranslateWeb "Divide" False = Char '/' -- KP and normal are merged here keyTranslateWeb "/" True = Char '?' keyTranslateWeb "/" False = Char '/' -- KP and normal are merged here keyTranslateWeb "Decimal" _ = Char '.' -- dot and comma are merged here keyTranslateWeb "Separator" _ = Char '.' -- to sidestep national standards keyTranslateWeb "F1" _ = Fun 1 keyTranslateWeb "F2" _ = Fun 2 keyTranslateWeb "F3" _ = Fun 3 keyTranslateWeb "F4" _ = Fun 4 keyTranslateWeb "F5" _ = Fun 5 keyTranslateWeb "F6" _ = Fun 6 keyTranslateWeb "F7" _ = Fun 7 keyTranslateWeb "F8" _ = Fun 8 keyTranslateWeb "F9" _ = Fun 9 keyTranslateWeb "F10" _ = Fun 10 keyTranslateWeb "F11" _ = Fun 11 keyTranslateWeb "F12" _ = Fun 12 -- dead keys keyTranslateWeb "Dead" _ = DeadKey keyTranslateWeb "Shift" _ = DeadKey keyTranslateWeb "Control" _ = DeadKey keyTranslateWeb "Meta" _ = DeadKey keyTranslateWeb "Menu" _ = DeadKey keyTranslateWeb "ContextMenu" _ = DeadKey keyTranslateWeb "Alt" _ = DeadKey keyTranslateWeb "AltGraph" _ = DeadKey keyTranslateWeb "Num_Lock" _ = DeadKey keyTranslateWeb "NumLock" _ = DeadKey keyTranslateWeb "Caps_Lock" _ = DeadKey keyTranslateWeb "CapsLock" _ = DeadKey keyTranslateWeb "Win" _ = DeadKey -- browser quirks keyTranslateWeb "Unidentified" _ = Begin -- hack for Firefox keyTranslateWeb ['\ESC'] _ = Esc keyTranslateWeb [' '] _ = Space keyTranslateWeb ['\n'] _ = Return keyTranslateWeb ['\r'] _ = DeadKey keyTranslateWeb ['\t'] _ = Tab -- standard characters keyTranslateWeb [c] _ = Char c keyTranslateWeb s _ = Unknown s LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/KeyBindings.hs0000644000000000000000000003715007346545000023244 0ustar0000000000000000{-# LANGUAGE RankNTypes #-} -- | Verifying, aggregating and displaying binding of keys to commands. module Game.LambdaHack.Client.UI.KeyBindings ( keyHelp, okxsN ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.Map.Strict as M import qualified Data.Text as T import Game.LambdaHack.Client.UI.Content.Input import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Client.UI.Slideshow import qualified Game.LambdaHack.Definition.Color as Color -- | Produce a set of help/menu screens from the key bindings. -- -- When the intro screen mentions KP_5, this really is KP_Begin, -- but since that is harder to understand we assume a different, non-default -- state of NumLock in the help text than in the code that handles keys. keyHelp :: CCUI -> FontSetup -> [(Text, OKX)] keyHelp CCUI{ coinput=coinput@InputContent{..} , coscreen=ScreenContent{rwidth, rheight} } FontSetup{..} = let movBlurb1 = [ "Walk throughout a level with mouse or numeric keypad (right diagram below)" , "or the Vi editor keys (middle) or the left-hand movement keys (left). Run until" , "disturbed with Shift or Control. Go-to a position with LMB (left mouse button)." , "In aiming mode, the same keys (and mouse) move the aiming crosshair." ] movSchema = [ " q w e y k u 7 8 9" , " \\|/ \\|/ \\|/" , " a-s-d h-.-l 4-5-6" , " /|\\ /|\\ /|\\" , " z x c b j n 1 2 3" ] movBlurb2 = [ "Press `KP_5` (`5` on keypad) to wait, bracing for impact, which reduces any" , "damage taken and prevents displacement by foes. Press `S-KP_5` or `C-KP_5`" , "(the same key with Shift or Control) to lurk 0.1 of a turn, without bracing." , "" , "Displace enemies by running into them with Shift/Control or S-LMB. Search," , "open, descend and melee by bumping into walls, doors, stairs and enemies." , "The best, and not on cooldown, melee weapon is automatically chosen" , "for attack from your equipment and from among your body parts." ] minimalBlurb = [ "The following few commands, joined with the movement and running keys," , "let you accomplish almost anything in the game, though not necessarily" , "with the fewest keystrokes. You can also play the game exclusively" , "with a mouse, or both mouse and keyboard (e.g., mouse for go-to" , "and terrain inspection and keyboard for everything else). Lastly," , "you can select a command with arrows or mouse directly from the help" , "screen or the dashboard and execute it on the spot." ] itemAllEnding = [ "Note how lower case item commands (stash item, equip item) place items" , "into a particular item store, while upper case item commands (manage Inventory," , "manage Outfit) open management menu for a store. Once a store menu is opened," , "you can switch stores with `<` and `>`, so the multiple commands only determine" , "the starting item store. Each store is accessible from the dashboard as well." ] mouseBasicsBlurb = [ "Screen area and UI mode (exploration/aiming) determine mouse click" , "effects. Here we give an overview of effects of each button over" , "the game map area. The list includes not only left and right buttons," , "but also the optional middle mouse button (MMB) and the mouse wheel," , "which is also used over menus to move selection. For mice without RMB," , "one can use Control key with LMB and for mice without MMB, one can use" , "C-RMB or C-S-LMB." ] mouseAreasBlurb = [ "Next we show mouse button effects per screen area, in exploration and" , "(if different) aiming mode. Note that mouse is optional. Keyboard suffices," , "occasionally requiring a lookup for an obscure command key in help screens." ] mouseAreasMini = [ "Mouse button effects per screen area, in exploration and in aiming modes" ] movTextEnd = "Press SPACE or PGDN to advance or ESC to see the map again." lastHelpEnd = "Use PGUP to go back and ESC to see the map again." seeAlso = "For more playing instructions see file PLAYING.md." offsetCol2 = 12 pickLeaderDescription = [ fmt offsetCol2 "0, 1 ... 9" "pick a particular actor as the new pointman" ] casualDescription = "Minimal cheat sheet for casual play" fmt0 n k h = T.justifyLeft n ' ' k <> " " <> h fmt n k h = " " <> fmt0 n k h keyCaption = fmt offsetCol2 "keys" "command" mouseOverviewCaption = fmt offsetCol2 "keys" "command (exploration/aiming)" spLen = textSize monoFont " " okxs cat headers footers = xytranslateOKX spLen 0 $ okxsN coinput monoFont propFont offsetCol2 (const False) True cat headers footers mergeOKX :: OKX -> OKX -> OKX mergeOKX okx1 okx2 = let off = 1 + maxYofFontOverlayMap (fst okx1) in sideBySideOKX 0 off okx1 okx2 catLength cat = length $ filter (\(_, (cats, desc, _)) -> cat `elem` cats && (desc /= "" || CmdInternal `elem` cats)) bcmdList keyM = 13 keyB = 31 truncatem b = if T.length b > keyB then T.take (keyB - 1) b <> "$" else b fmm a b c = fmt (keyM + 1) a $ fmt0 keyB (truncatem b) (truncatem c) areaCaption t = fmm t "LMB (left mouse button)" "RMB (right mouse button)" keySel :: (forall a. (a, a) -> a) -> K.KM -> [(CmdArea, KeyOrSlot, Text)] keySel sel key = let cmd = case M.lookup key bcmdMap of Just (_, _, cmd2) -> cmd2 Nothing -> error $ "" `showFailure` key caCmds = case cmd of ByAimMode AimModeCmd{exploration=ByArea lexp, aiming=ByArea laim} -> sort $ sel (lexp, laim \\ lexp) _ -> error $ "" `showFailure` cmd caMakeChoice (ca, cmd2) = let (km, desc) = case M.lookup cmd2 brevMap of Just ks -> let descOfKM km2 = case M.lookup km2 bcmdMap of Just (_, "", _) -> Nothing Just (_, desc2, _) -> Just (km2, desc2) Nothing -> error $ "" `showFailure` km2 in case mapMaybe descOfKM ks of [] -> error $ "" `showFailure` (ks, cmd2) kmdesc3 : _ -> kmdesc3 Nothing -> (key, "(not described:" <+> tshow cmd2 <> ")") in (ca, Left km, desc) in map caMakeChoice caCmds doubleIfSquare n | isSquareFont monoFont = 2 * n | otherwise = n okm :: (forall a. (a, a) -> a) -> K.KM -> K.KM -> [Text] -> OKX okm sel key1 key2 header = let kst1 = keySel sel key1 kst2 = keySel sel key2 f (ca1, Left km1, _) (ca2, Left km2, _) y = assert (ca1 == ca2 `blame` (ca1, ca2, km1, km2, kst1, kst2)) [ (Left km1, ( PointUI (doubleIfSquare $ keyM + 4) y , ButtonWidth monoFont keyB )) , (Left km2, ( PointUI (doubleIfSquare $ keyB + keyM + 5) y , ButtonWidth monoFont keyB )) ] f c d e = error $ "" `showFailure` (c, d, e) kxs = concat $ zipWith3 f kst1 kst2 [1 + length header..] menuLeft = map (\(ca1, _, _) -> textToAL $ areaDescription ca1) kst1 menuMiddle = map (\(_, _, desc) -> textToAL desc) kst1 menuRight = map (\(_, _, desc) -> textToAL desc) kst2 y0 = 1 + length header in ( EM.unionsWith (++) [ typesetInMono $ "" : header , EM.singleton monoFont $ typesetXY (doubleIfSquare 2, y0) menuLeft , EM.singleton propFont $ typesetXY (doubleIfSquare $ keyM + 4, y0) menuMiddle , EM.singleton propFont $ typesetXY (doubleIfSquare $ keyB + keyM + 5, y0) menuRight ] , kxs ) typesetInSquare :: [Text] -> FontOverlayMap typesetInSquare = EM.singleton squareFont . typesetXY (spLen, 0) . map textToAL typesetInMono :: [Text] -> FontOverlayMap typesetInMono = EM.singleton monoFont . typesetXY (spLen, 0) . map textToAL typesetInProp :: [Text] -> FontOverlayMap typesetInProp = EM.singleton propFont . typesetXY (spLen, 0) . map textToAL sideBySide :: [(Text, OKX)] -> [(Text, OKX)] sideBySide ((_t1, okx1) : (t2, okx2) : rest) | not (isSquareFont propFont) = (t2, sideBySideOKX rwidth 0 okx1 okx2) : sideBySide rest sideBySide l = l in sideBySide $ concat [ if catLength CmdMinimal + length movBlurb1 + length movSchema + length movBlurb2 + length minimalBlurb + 6 > rheight then [ ( movTextEnd , mergeOKX (mergeOKX ( typesetInMono ["", casualDescription <+> "(1/2)", ""] , [] ) (mergeOKX (typesetInProp movBlurb1, []) (typesetInSquare $ "" : movSchema, []))) (typesetInProp $ "" : movBlurb2, []) ) , ( movTextEnd , okxs CmdMinimal ( ["", casualDescription <+> "(2/2)", ""] , minimalBlurb ++ [""] , [keyCaption] ) ([], []) ) ] else [ ( movTextEnd , mergeOKX (mergeOKX ( typesetInMono ["", casualDescription, ""] , [] ) (mergeOKX (typesetInProp movBlurb1, []) (typesetInSquare $ "" : movSchema, []))) (okxs CmdMinimal ( [] , [""] ++ movBlurb2 ++ [""] ++ minimalBlurb ++ [""] , [keyCaption] ) ([], [""])) ) ] , if 45 > rheight then [ ( movTextEnd , let (ls, _) = okxs CmdMouse ( ["", "Optional mouse commands", ""] , mouseBasicsBlurb ++ [""] , [mouseOverviewCaption] ) ([], []) in (ls, []) ) -- don't capture mouse wheel, etc. , ( movTextEnd , mergeOKX (typesetInMono $ "" : mouseAreasMini, []) (mergeOKX (okm fst K.leftButtonReleaseKM K.rightButtonReleaseKM [areaCaption "Exploration"]) (okm snd K.leftButtonReleaseKM K.rightButtonReleaseKM [areaCaption "Aiming Mode"])) ) ] else [ ( movTextEnd , let (ls, _) = okxs CmdMouse ( ["", "Optional mouse commands", ""] , mouseBasicsBlurb ++ [""] , [mouseOverviewCaption] ) ([], []) okx0 = (ls, []) -- don't capture mouse wheel, etc. in mergeOKX (mergeOKX okx0 (typesetInProp $ "" : mouseAreasBlurb, [])) (mergeOKX (okm fst K.leftButtonReleaseKM K.rightButtonReleaseKM [areaCaption "Exploration"]) (okm snd K.leftButtonReleaseKM K.rightButtonReleaseKM [areaCaption "Aiming Mode"] )) ) ] , if catLength CmdItem + catLength CmdMove + 9 + 9 > rheight then [ ( movTextEnd , okxs CmdItem (["", categoryDescription CmdItem], [], ["", keyCaption]) ([], "" : itemAllEnding) ) , ( movTextEnd , okxs CmdMove (["", categoryDescription CmdMove], [], ["", keyCaption]) (pickLeaderDescription, []) ) ] else [ ( movTextEnd , mergeOKX (okxs CmdItem (["", categoryDescription CmdItem], [], ["", keyCaption]) ([], "" : itemAllEnding)) (okxs CmdMove ( ["", "", categoryDescription CmdMove] , [] , ["", keyCaption] ) (pickLeaderDescription, [""])) ) ] , if catLength CmdAim + catLength CmdMeta + 9 > rheight then [ ( movTextEnd , okxs CmdAim (["", categoryDescription CmdAim], [], ["", keyCaption]) ([], []) ) , ( lastHelpEnd , okxs CmdMeta (["", categoryDescription CmdMeta], [], ["", keyCaption]) ([], ["", seeAlso]) ) ] else [ ( lastHelpEnd , mergeOKX (okxs CmdAim (["", categoryDescription CmdAim], [], ["", keyCaption]) ([], [])) (okxs CmdMeta ( ["", "", categoryDescription CmdMeta] , [] , ["", keyCaption] ) ([], ["", seeAlso, ""])) ) ] ] -- | Turn the specified portion of bindings into a menu. -- -- The length of the button may be wrong if the two supplied fonts -- have very different widths. okxsN :: InputContent -> DisplayFont -> DisplayFont -> Int -> (HumanCmd -> Bool) -> Bool -> CmdCategory -> ([Text], [Text], [Text]) -> ([Text], [Text]) -> OKX okxsN InputContent{..} labFont descFont offsetCol2 greyedOut showManyKeys cat (headerMono1, headerProp, headerMono2) (footerMono, footerProp) = let fmt k h = (T.singleton '\x00a0' <> k, h) coImage :: HumanCmd -> [K.KM] coImage cmd = M.findWithDefault (error $ "" `showFailure` cmd) cmd brevMap disp = T.intercalate " or " . map (T.pack . K.showKM) keyKnown km = case K.key km of K.Unknown{} -> False _ -> True keys :: [(KeyOrSlot, (Bool, (Text, Text)))] keys = [ (Left km, (greyedOut cmd, fmt keyNames desc)) | (_, (cats, desc, cmd)) <- bcmdList , let kms = coImage cmd knownKeys = filter keyKnown kms keyNames = disp $ (if showManyKeys then id else take 1) knownKeys kmsRes = if desc == "" then knownKeys else kms km = case kmsRes of [] -> K.escKM km1 : _ -> km1 , cat `elem` cats , desc /= "" || CmdInternal `elem` cats] spLen = textSize labFont " " f (ks, (_, (_, t2))) y = (ks, ( PointUI spLen y , ButtonWidth labFont (offsetCol2 + 2 + T.length t2 - 1))) kxs = zipWith f keys [length headerMono1 + length headerProp + length headerMono2 ..] ts = map (\t -> (False, (t, ""))) headerMono1 ++ map (\t -> (False, ("", t))) headerProp ++ map (\t -> (False, (t, ""))) headerMono2 ++ map snd keys ++ map (\t -> (False, (t, ""))) footerMono ++ map (\t -> (False, ("", t))) footerProp greyToAL (b, (t1, t2)) = if b then let al1 = textFgToAL Color.BrBlack t1 in (al1, ( if T.null t1 then 0 else spLen * (offsetCol2 + 2) , textFgToAL Color.BrBlack t2 )) else let al1 = textToAL t1 in (al1, ( if T.null t1 then 0 else spLen * (offsetCol2 + 2) , textToAL t2 )) (greyLab, greyDesc) = unzip $ map greyToAL ts in ( EM.insertWith (++) descFont (offsetOverlayX greyDesc) $ EM.singleton labFont (offsetOverlay greyLab) , kxs ) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/MonadClientUI.hs0000644000000000000000000005007707346545000023474 0ustar0000000000000000-- | Client monad for interacting with a human through UI. module Game.LambdaHack.Client.UI.MonadClientUI ( -- * Client UI monad MonadClientUI( getsSession , modifySession , updateClientLeader , getCacheBfs , getCachePath ) -- * Assorted primitives , clientPrintUI, debugPossiblyPrintUI, getSession, putSession, displayFrames , connFrontendFrontKey, setFrontAutoYes, frontendShutdown, printScreen , chanFrontend, anyKeyPressed, discardPressedKey, resetPressedKeys , revCmdMap, getReportUI, getMiniHintAiming, computeChosenLore , getArenaUI, viewedLevelUI, mxhairToPos, xhairToPos, setXHairFromGUI , clearAimMode, getFontSetup, scoreToSlideshow, defaultHistory , tellAllClipPS, tellGameClipPS, elapsedSessionTimeGT , resetSessionStart, resetGameStart, partActorLeader, partPronounLeader , tryRestore, rndToActionUI, tryOpenBrowser #ifdef EXPOSE_INTERNAL -- * Internal operations , connFrontend, displayFrame #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.Trans.State.Strict as St import qualified Data.EnumMap.Strict as EM import qualified Data.Map.Strict as M import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Time.Clock import Data.Time.Clock.POSIX import Data.Time.LocalTime import qualified Data.Vector.Unboxed as U import qualified NLP.Miniutter.English as MU import System.IO (hFlush, stdout) import Web.Browser (openBrowser) import Game.LambdaHack.Client.Bfs import Game.LambdaHack.Client.CommonM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Content.Input import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.Frame import qualified Game.LambdaHack.Client.UI.Frontend as Frontend import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.HighScore as HighScore import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Core.Random -- Assumes no interleaving with other clients, because each UI client -- in a different terminal/window/machine. clientPrintUI :: MonadClientUI m => Text -> m () clientPrintUI t = liftIO $ do T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough hFlush stdout debugPossiblyPrintUI :: MonadClientUI m => Text -> m () debugPossiblyPrintUI t = do sdbgMsgCli <- getsClient $ sdbgMsgCli . soptions when sdbgMsgCli $ liftIO $ do T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough hFlush stdout -- | The monad that gives the client access to UI operations, -- but not to modifying client state, except for the client-side pointman -- (as opposed to pointman stores in faction data in main game state), -- which is more of a UI concept, but is shared with AI to be able -- to keep it when switching AI on/off and to save on typing. class MonadClientRead m => MonadClientUI m where getsSession :: (SessionUI -> a) -> m a modifySession :: (SessionUI -> SessionUI) -> m () updateClientLeader :: ActorId -> m () getCacheBfs :: ActorId -> m (PointArray.Array BfsDistance) getCachePath :: ActorId -> Point -> m (Maybe AndPath) getSession :: MonadClientUI m => m SessionUI getSession = getsSession id putSession :: MonadClientUI m => SessionUI -> m () putSession s = modifySession (const s) -- | Write a UI request to the frontend and read a corresponding reply. connFrontend :: MonadClientUI m => Frontend.FrontReq a -> m a connFrontend req = do Frontend.ChanFrontend f <- getsSession schanF liftIO $ f req displayFrame :: MonadClientUI m => Maybe Frame -> m () displayFrame mf = do frame <- case mf of Nothing -> return $! Frontend.FrontDelay 1 Just fr -> do modifySession $ \cli -> cli {snframes = snframes cli + 1} return $! Frontend.FrontFrame fr connFrontend frame -- | Push frames or delays to the frame queue. The frames depict -- the @lid@ level. displayFrames :: MonadClientUI m => LevelId -> PreFrames3 -> m () displayFrames _ [] = return () displayFrames lid frs = do let framesRaw = case frs of [] -> [] [Just ((bfr, ffr), (ovProp, ovSquare, ovMono))] -> [Just ( (FrameBase $ U.unsafeThaw bfr, ffr) , (ovProp, ovSquare, ovMono) )] _ -> -- Due to the frames coming from the same base frame, -- we have to copy it to avoid picture corruption. map (fmap $ \((bfr, ffr), (ovProp, ovSquare, ovMono)) -> ((FrameBase $ U.thaw bfr, ffr), (ovProp, ovSquare, ovMono))) frs -- If display level different than the man viewed level, -- e.g., when our actor is attacked on a remote level, -- then pad with tripple delay to give more time to see the remote frames(s). lidV <- viewedLevelUI frames <- if lidV == lid then do modifySession $ \sess -> sess { sdisplayNeeded = False , sturnDisplayed = True } return framesRaw else return $ framesRaw ++ [Nothing, Nothing, Nothing] mapM_ displayFrame frames -- | Write 'Frontend.FrontKey' UI request to the frontend, read the reply, -- set pointer, return key. connFrontendFrontKey :: MonadClientUI m => [K.KM] -> PreFrame3 -> m K.KM connFrontendFrontKey frontKeyKeys ((bfr, ffr), (ovProp, ovSquare, ovMono)) = do let frontKeyFrame = ((FrameBase $ U.unsafeThaw bfr, ffr), (ovProp, ovSquare, ovMono)) sautoYes <- getsSession sautoYes if sautoYes && (null frontKeyKeys || K.spaceKM `elem` frontKeyKeys) then do connFrontend $ Frontend.FrontFrame frontKeyFrame return K.spaceKM else do kmp <- connFrontend $ Frontend.FrontKey frontKeyKeys frontKeyFrame modifySession $ \sess -> sess {spointer = K.kmpPointer kmp} return $! K.kmpKeyMod kmp setFrontAutoYes :: MonadClientUI m => Bool -> m () setFrontAutoYes b = modifySession $ \sess -> sess {sautoYes = b} frontendShutdown :: MonadClientUI m => m () frontendShutdown = connFrontend Frontend.FrontShutdown printScreen :: MonadClientUI m => m () printScreen = connFrontend Frontend.FrontPrintScreen -- | Initialize the frontend chosen by the player via client options. chanFrontend :: MonadClientUI m => ScreenContent -> ClientOptions -> m Frontend.ChanFrontend chanFrontend coscreen soptions = liftIO $ Frontend.chanFrontendIO coscreen soptions anyKeyPressed :: MonadClientUI m => m Bool anyKeyPressed = connFrontend Frontend.FrontPressed discardPressedKey :: MonadClientUI m => m () discardPressedKey = connFrontend Frontend.FrontDiscardKey resetPressedKeys :: MonadClientUI m => m () resetPressedKeys = connFrontend Frontend.FrontResetKeys revCmdMap :: MonadClientUI m => m (HumanCmd.HumanCmd -> K.KM) revCmdMap = do CCUI{coinput=InputContent{brevMap}} <- getsSession sccui let revCmd cmd = case M.lookup cmd brevMap of Nothing -> K.undefinedKM Just (k : _) -> k Just [] -> error $ "" `showFailure` brevMap return revCmd getReportUI :: MonadClientUI m => Bool -> m Report getReportUI insideMenu = do saimMode <- getsSession saimMode sUIOptions <- getsSession sUIOptions report <- getsSession $ newReport . shistory sreqDelay <- getsSession sreqDelay miniHintAiming <- getMiniHintAiming -- Different from ordinary tutorial hints in that shown more than once. let detailAtDefault = (detailLevel <$> saimMode) == Just defaultDetailLevel detailMinimal = (detailLevel <$> saimMode) == Just minBound prefixColors = uMessageColors sUIOptions promptAim = toMsgShared prefixColors MsgPromptGeneric (miniHintAiming <> "\n") promptDelay = toMsgShared prefixColors MsgPromptAction "" return $! if | not insideMenu && detailAtDefault && not detailMinimal -> consReport promptAim report | sreqDelay == ReqDelayAlarm && not insideMenu -> consReport promptDelay report | otherwise -> report getMiniHintAiming :: MonadClientUI m => m Text getMiniHintAiming = do saimMode <- getsSession saimMode (inhabitants, embeds) <- if isJust saimMode then computeChosenLore else return ([], []) sreqDelay <- getsSession sreqDelay mleader <- getsClient sleader let loreCommandAvailable = not (null inhabitants && null embeds) && isJust mleader -- Here we assume newbies don't override default keys. return $! T.unwords $ concat [ ["Aiming mode:"] , ["'~' for lore," | loreCommandAvailable ] , ["'f' to fling," | sreqDelay /= ReqDelayHandled] , [if loreCommandAvailable && sreqDelay /= ReqDelayHandled then "SPACE or RMB to hush," -- shorter, because less space left else "SPACE or RMB to cycle detail,"] , ["ESC to cancel."] ] computeChosenLore :: MonadClientUI m => m ([(ActorId, Actor)], [(ItemId, ItemQuant)]) computeChosenLore = do side <- getsClient sside xhairPos <- xhairToPos lidV <- viewedLevelUI let isOurs (_, b) = bfid b == side inhabitants0 <- getsState $ filter (not . isOurs) . posToAidAssocs xhairPos lidV embeds0 <- getsState $ EM.assocs . getEmbedBag lidV xhairPos return (inhabitants0, embeds0) getArenaUI :: MonadClientUI m => m LevelId getArenaUI = do let fallback = do side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD case gquit fact of Just Status{stDepth} -> return $! toEnum stDepth Nothing -> getEntryArena fact mleader <- getsClient sleader case mleader of Just leader -> do -- The leader may just be teleporting (e.g., due to displace -- over terrain not in FOV) so not existent momentarily. mem <- getsState $ EM.member leader . sactorD if mem then getsState $ blid . getActorBody leader else fallback Nothing -> fallback viewedLevelUI :: MonadClientUI m => m LevelId viewedLevelUI = do arena <- getArenaUI saimMode <- getsSession saimMode return $! maybe arena aimLevelId saimMode mxhairToPos :: MonadClientUI m => m (Maybe Point) mxhairToPos = do lidV <- viewedLevelUI mleader <- getsClient sleader sxhair <- getsSession sxhair getsState $ aidTgtToPos mleader lidV sxhair xhairToPos :: MonadClientUI m => m Point xhairToPos = do mxhairPos <- mxhairToPos mleader <- getsClient sleader fallback <- case mleader of Nothing -> return originPoint Just leader -> getsState $ bpos . getActorBody leader return $! fromMaybe fallback mxhairPos setXHairFromGUI :: MonadClientUI m => Maybe Target -> m () setXHairFromGUI xhair2 = do xhair0 <- getsSession sxhair modifySession $ \sess -> sess {sxhairGoTo = Nothing} when (xhair0 /= xhair2) $ modifySession $ \sess -> sess {sxhair = xhair2} -- If aim mode is exited, usually the player had the opportunity to deal -- with xhair on a foe spotted on another level, so now move xhair -- back to the leader level. clearAimMode :: MonadClientUI m => m () clearAimMode = do lidVOld <- viewedLevelUI -- not in aiming mode at this point xhairPos <- xhairToPos -- computed while still in aiming mode modifySession $ \sess -> sess {saimMode = Nothing} lidV <- viewedLevelUI -- not in aiming mode at this point when (lidVOld /= lidV) $ do sxhairOld <- getsSession sxhair let sxhair = case sxhairOld of Just TPoint{} -> Just $ TPoint TUnknown lidV xhairPos -- the point is possibly unknown on this level; unimportant anyway _ -> sxhairOld setXHairFromGUI sxhair -- We can't support setup @FontSetup SquareFont MonoFont MonoFont@ -- at this time, because the mono layer needs to overwrite the prop layer -- and so has to be distinct even if the underlying font is mono for both. getFontSetup :: MonadClientUI m => m FontSetup getFontSetup = do soptions@ClientOptions{schosenFontset, sfontsets} <- getsClient soptions let chosenFontsetID = fromJust schosenFontset chosenFontset = case lookup chosenFontsetID sfontsets of Nothing -> error $ "Fontset not defined in config file" `showFailure` chosenFontsetID Just fs -> fs multiFont = Frontend.frontendName soptions == "sdl" && not (T.null (fontPropRegular chosenFontset)) return $! if multiFont then multiFontSetup else singleFontSetup scoreToSlideshow :: MonadClientUI m => Int -> Status -> m Slideshow scoreToSlideshow total status = do CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui fid <- getsClient sside scoreDict <- getsState shigh gameModeId <- getsState sgameModeId gameMode <- getGameMode time <- getsState stime dungeonTotal <- getsState sgold date <- liftIO getPOSIXTime tz <- liftIO $ getTimeZone $ posixSecondsToUTCTime date curChalSer <- getsClient scurChal factionD <- getsState sfactionD let fact = factionD EM.! fid table = HighScore.getTable gameModeId scoreDict gameModeName = mname gameMode theirVic (fi, fa) | isFoe fid fact fi && not (isHorrorFact fa) = Just $ gvictims fa | otherwise = Nothing theirVictims = EM.unionsWith (+) $ mapMaybe theirVic $ EM.assocs factionD ourVic (fi, fa) | isFriend fid fact fi = Just $ gvictims fa | otherwise = Nothing ourVictims = EM.unionsWith (+) $ mapMaybe ourVic $ EM.assocs factionD (worthMentioning, (ntable, pos)) = HighScore.register table total dungeonTotal time status date curChalSer (T.unwords $ tail $ T.words $ gname fact) ourVictims theirVictims (fhiCondPoly $ gkind fact) fontSetup <- getFontSetup let sli = highSlideshow fontSetup False rwidth (rheight - 1) ntable pos gameModeName tz return $! if worthMentioning then sli else emptySlideshow defaultHistory :: MonadClientUI m => m History defaultHistory = do sUIOptions <- getsSession sUIOptions curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut let displayHints = fromMaybe curTutorial overrideTut liftIO $ do utcTime <- getCurrentTime timezone <- getTimeZone utcTime let curDate = T.pack $ take 19 $ show $ utcToLocalTime timezone utcTime emptyHist = emptyHistory $ uHistoryMax sUIOptions msg = toMsgShared (uMessageColors sUIOptions) MsgBookKeeping $ "History log started on " <> curDate <> "." -- Tuturial hints from initial message can be repeated. (_, nhistory, _) = addToReport S.empty displayHints False emptyHist msg timeZero return nhistory tellAllClipPS :: MonadClientUI m => m () tellAllClipPS = do bench <- getsClient $ sbenchmark . soptions when bench $ do sstartPOSIX <- getsSession sstart curPOSIX <- liftIO getPOSIXTime allTime <- getsSession sallTime gtime <- getsState stime allNframes <- getsSession sallNframes gnframes <- getsSession snframes let time = absoluteTimeAdd allTime gtime nframes = allNframes + gnframes diff = fromRational $ toRational $ curPOSIX - sstartPOSIX cps = intToDouble (timeFit time timeClip) / diff fps = intToDouble nframes / diff clientPrintUI $ "Session time:" <+> tshow diff <> "s; frames:" <+> tshow nframes <> "." <+> "Average clips per second:" <+> tshow cps <> "." <+> "Average FPS:" <+> tshow fps <> "." tellGameClipPS :: MonadClientUI m => m () tellGameClipPS = do bench <- getsClient $ sbenchmark . soptions when bench $ do sgstartPOSIX <- getsSession sgstart curPOSIX <- liftIO getPOSIXTime -- If loaded game, don't report anything. unless (sgstartPOSIX == 0) $ do time <- getsState stime nframes <- getsSession snframes let diff = fromRational $ toRational $ curPOSIX - sgstartPOSIX cps = intToDouble (timeFit time timeClip) / diff fps = intToDouble nframes / diff -- This means: "Game portion after last reload time:...". clientPrintUI $ "Game time:" <+> tshow diff <> "s; frames:" <+> tshow nframes <> "." <+> "Average clips per second:" <+> tshow cps <> "." <+> "Average FPS:" <+> tshow fps <> "." -- TODO: for speed and resolution use -- https://hackage.haskell.org/package/chronos -- or the number_of_nanonseconds functionality -- in Data.Time.Clock.System, once it arrives there elapsedSessionTimeGT :: MonadClientRead m => POSIXTime -> Int -> m Bool elapsedSessionTimeGT sstartPOSIX stopAfter = do current <- liftIO getPOSIXTime return $! (fromIntegralWrap :: Int -> NominalDiffTime) stopAfter + sstartPOSIX <= current resetSessionStart :: MonadClientUI m => m () resetSessionStart = do sstart <- liftIO getPOSIXTime modifySession $ \sess -> sess {sstart} resetGameStart resetGameStart :: MonadClientUI m => m () resetGameStart = do sgstart <- liftIO getPOSIXTime time <- getsState stime nframes <- getsSession snframes modifySession $ \sess -> sess { sgstart , sallTime = absoluteTimeAdd (sallTime sess) time , snframes = 0 , sallNframes = sallNframes sess + nframes } -- | The part of speech describing the actor or the "you" pronoun if he is -- the leader of the observer's faction. partActorLeader :: MonadClientUI m => ActorId -> m MU.Part partActorLeader aid = do mleader <- getsClient sleader bUI <- getsSession $ getActorUI aid b <- getsState $ getActorBody aid return $! case mleader of Just leader | aid == leader -> "you" _ | bhp b <= 0 && not (bproj b) -> -- avoid "the fallen falling" projectiles MU.Phrase ["the fallen", partActor bUI] _ -> partActor bUI -- | The part of speech with the actor's pronoun or "you" if a leader -- of the client's faction. partPronounLeader :: MonadClientUI m => ActorId -> m MU.Part partPronounLeader aid = do mleader <- getsClient sleader bUI <- getsSession $ getActorUI aid return $! case mleader of Just leader | aid == leader -> "you" _ -> partPronoun bUI -- | Try to read saved client game state from the file system. tryRestore :: MonadClientUI m => m (Maybe (StateClient, Maybe SessionUI)) tryRestore = do COps{corule} <- getsState scops clientOptions <- getsClient soptions if sbenchmark clientOptions then return Nothing else do side <- getsClient sside prefix <- getsClient $ ssavePrefixCli . soptions let fileName = prefix <> Save.saveNameCli corule side liftIO $ Save.restoreGame corule clientOptions fileName -- | Invoke pseudo-random computation with the generator kept in the session. rndToActionUI :: MonadClientUI m => Rnd a -> m a rndToActionUI r = do gen1 <- getsSession srandomUI let (a, gen2) = St.runState r gen1 modifySession $ \sess -> sess {srandomUI = gen2} return a tryOpenBrowser :: MonadClientUI m => String -> m Bool tryOpenBrowser address = liftIO $ openBrowser address LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Msg.hs0000644000000000000000000005434407346545000021570 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Game messages displayed on top of the screen for the player to read -- and then saved to player history. module Game.LambdaHack.Client.UI.Msg ( -- * Msg Msg, MsgShared, toMsgShared, toMsgDistinct , MsgClassShowAndSave(..), MsgClassShow(..), MsgClassSave(..) , MsgClassIgnore(..), MsgClassDistinct(..) , MsgClass, interruptsRunning, disturbsResting -- * Report , Report, nullVisibleReport, consReport, renderReport, anyInReport -- * History , History, newReport, emptyHistory, addToReport, addEolToNewReport , archiveReport, lengthHistory, renderHistory #ifdef EXPOSE_INTERNAL -- * Internal operations , UAttrString, uToAttrString, attrStringToU , toMsg, MsgPrototype, tripleFromProto , scrapsRepeats, isTutorialHint, msgColor , RepMsgNK, nullRepMsgNK , emptyReport, renderRepetition , scrapRepetitionSingle, scrapRepetition, renderTimeReport #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.Char as Char import qualified Data.Set as S import Data.Vector.Binary () import qualified Data.Vector.Unboxed as U import GHC.Generics (Generic) import Game.LambdaHack.Client.UI.Overlay import qualified Game.LambdaHack.Common.RingBuffer as RB import Game.LambdaHack.Common.Time import qualified Game.LambdaHack.Definition.Color as Color -- * UAttrString type UAttrString = U.Vector Word32 uToAttrString :: UAttrString -> AttrString uToAttrString v = map Color.AttrCharW32 $ U.toList v attrStringToU :: AttrString -> UAttrString attrStringToU l = U.fromList $ map Color.attrCharW32 l -- * Msg -- | The type of a single game message. data Msg = Msg { msgShow :: AttrString -- ^ the colours and characters of the message -- to be shown on the screen; not just text, -- in case there was some colour not coming -- from the message class , msgSave :: AttrString -- ^ the same to be saved in the message log only , msgClass :: MsgClass } deriving (Show, Eq, Ord, Generic) instance Binary Msg toMsg :: [(String, Color.Color)] -> MsgPrototype -> Msg toMsg prefixColors msgProto = let (tShow, tSave, msgClass) = tripleFromProto msgProto msgClassName = showSimpleMsgClass msgClass mprefixColor = find ((`isPrefixOf` msgClassName) . fst) prefixColors color = maybe (msgColor msgClass) snd mprefixColor msgShow = textFgToAS color tShow msgSave = textFgToAS color tSave in Msg {..} data MsgPrototype = MsgProtoShowAndSave MsgClassShowAndSave Text | MsgProtoShow MsgClassShow Text | MsgProtoSave MsgClassSave Text | MsgProtoIgnore MsgClassIgnore | MsgProtoDistinct MsgClassDistinct Text Text tripleFromProto :: MsgPrototype -> (Text, Text, MsgClass) tripleFromProto = \case MsgProtoShowAndSave x t -> (t, t, MsgClassShowAndSave x) MsgProtoShow x t -> (t, "", MsgClassShow x) MsgProtoSave x t -> ("", t, MsgClassSave x) MsgProtoIgnore x -> ("", "", MsgClassIgnore x) MsgProtoDistinct x t1 t2 -> (t1, t2, MsgClassDistinct x) class MsgShared a where toMsgShared :: [(String, Color.Color)] -> a -> Text -> Msg instance MsgShared MsgClassShowAndSave where toMsgShared prefixColors msgClass t = toMsg prefixColors $ MsgProtoShowAndSave msgClass t instance MsgShared MsgClassShow where toMsgShared prefixColors msgClass t = toMsg prefixColors $ MsgProtoShow msgClass t instance MsgShared MsgClassSave where toMsgShared prefixColors msgClass t = toMsg prefixColors $ MsgProtoSave msgClass t instance MsgShared MsgClassIgnore where toMsgShared prefixColors msgClass _ = toMsg prefixColors $ MsgProtoIgnore msgClass toMsgDistinct :: [(String, Color.Color)] -> MsgClassDistinct -> Text -> Text -> Msg toMsgDistinct prefixColors msgClass t1 t2 = toMsg prefixColors $ MsgProtoDistinct msgClass t1 t2 -- Each constructor name should have length as asserted in @emptyReport@, -- so that the message log with message classes (if set in config) looks tidy. data MsgClass = MsgClassShowAndSave MsgClassShowAndSave | MsgClassShow MsgClassShow | MsgClassSave MsgClassSave | MsgClassIgnore MsgClassIgnore | MsgClassDistinct MsgClassDistinct deriving (Show, Eq, Ord, Generic) instance Binary MsgClass showSimpleMsgClass :: MsgClass -> String showSimpleMsgClass = \case MsgClassShowAndSave x -> show x MsgClassShow x -> show x MsgClassSave x -> show x MsgClassIgnore x -> show x MsgClassDistinct x -> show x data MsgClassShowAndSave = MsgBookKeeping | MsgStatusWakeup | MsgStatusStopUs | MsgStatusStopThem | MsgItemCreation | MsgItemRuination | MsgDeathVictory | MsgDeathDeafeat | MsgDeathBoring | MsgRiskOfDeath | MsgPointmanSwap | MsgFactionIntel | MsgFinalOutcome | MsgBackdropInfo | MsgTerrainReveal | MsgItemDiscovery | MsgSpottedActor | MsgItemMovement | MsgActionMajor | MsgActionMinor | MsgEffectMajor | MsgEffectMedium | MsgEffectMinor | MsgMiscellanous | MsgHeardOutside | MsgHeardNearby | MsgHeardFaraway | MsgBackdropFocus | MsgActionWarning | MsgRangedMightyWe | MsgRangedMightyUs | MsgRangedOthers -- not ours or projectiles are hit | MsgRangedNormalUs | MsgGoodMiscEvent | MsgBadMiscEvent | MsgNeutralEvent | MsgSpecialEvent | MsgMeleeMightyWe | MsgMeleeMightyUs | MsgMeleeComplexWe | MsgMeleeComplexUs | MsgMeleeOthers -- not ours or projectiles are hit | MsgMeleeNormalUs | MsgActionComplete | MsgAtFeetMajor | MsgAtFeetMinor | MsgTutorialHint deriving (Show, Eq, Ord, Enum, Bounded, Generic) instance Binary MsgClassShowAndSave data MsgClassShow = MsgPromptGeneric | MsgPromptFocus | MsgPromptMention | MsgPromptModify | MsgPromptActors | MsgPromptItems | MsgPromptAction | MsgActionAlert | MsgSpottedThreat deriving (Show, Eq, Ord, Enum, Bounded, Generic) instance Binary MsgClassShow data MsgClassSave = MsgInnerWorkSpam | MsgNumericReport deriving (Show, Eq, Ord, Enum, Bounded, Generic) instance Binary MsgClassSave data MsgClassIgnore = MsgMacroOperation | MsgRunStopReason | MsgStopPlayback deriving (Show, Eq, Ord, Enum, Bounded, Generic) instance Binary MsgClassIgnore data MsgClassDistinct = MsgSpottedItem | MsgStatusSleep | MsgStatusGoodUs | MsgStatusBadUs | MsgStatusOthers | MsgStatusBenign | MsgStatusWarning | MsgStatusLongerUs | MsgStatusLongThem deriving (Show, Eq, Ord, Enum, Bounded, Generic) instance Binary MsgClassDistinct interruptsRunning :: MsgClass -> Bool interruptsRunning = \case MsgClassShowAndSave x -> case x of MsgBookKeeping -> False MsgStatusStopThem -> False MsgItemMovement -> False MsgActionMinor -> False MsgEffectMinor -> False MsgMiscellanous -> False -- taunts are colourful, but spammy MsgHeardOutside -> False -- cause must be 'profound', but even taunts are MsgHeardFaraway -> False -- MsgHeardNearby interrupts, even if running started while hearing close MsgRangedOthers -> False MsgAtFeetMinor -> False _ -> True MsgClassShow x -> case x of MsgPromptGeneric -> False MsgPromptFocus -> False MsgPromptMention -> False MsgPromptModify -> False MsgPromptActors -> False MsgPromptItems -> False MsgPromptAction -> False MsgActionAlert -> True -- action alerts or questions cause alarm MsgSpottedThreat -> True MsgClassSave x -> case x of MsgInnerWorkSpam -> False MsgNumericReport -> False MsgClassIgnore x -> case x of MsgMacroOperation -> False MsgRunStopReason -> True MsgStopPlayback -> True MsgClassDistinct x -> case x of MsgSpottedItem -> False MsgStatusLongThem -> False MsgStatusOthers -> False MsgStatusBenign -> False MsgStatusWarning -> False _ -> True disturbsResting :: MsgClass -> Bool disturbsResting = \case MsgClassShowAndSave x -> case x of MsgPointmanSwap -> False -- handled separately MsgItemDiscovery -> False -- medium importance MsgHeardNearby -> False -- handled separately; no disturbance if old _ -> interruptsRunning $ MsgClassShowAndSave x msgClass -> interruptsRunning msgClass scrapsRepeats :: MsgClass -> Bool scrapsRepeats = \case MsgClassShowAndSave x -> case x of MsgBookKeeping -> False -- too important to scrap MsgDeathDeafeat -> False MsgRiskOfDeath -> False MsgFinalOutcome -> False _ -> True MsgClassShow x -> case x of MsgPromptGeneric -> False MsgPromptFocus -> False MsgPromptMention -> False MsgPromptModify -> False MsgPromptActors -> False MsgPromptItems -> False MsgPromptAction -> False MsgActionAlert -> False MsgSpottedThreat -> True MsgClassSave x -> case x of MsgInnerWorkSpam -> True MsgNumericReport -> True MsgClassIgnore _ -> False -- ignored, so no need to scrap MsgClassDistinct _x -> True isTutorialHint :: MsgClass -> Bool isTutorialHint = \case MsgClassShowAndSave x -> case x of -- show and save: least surprise MsgTutorialHint -> True _ -> False MsgClassShow _ -> False MsgClassSave _ -> False MsgClassIgnore _ -> False MsgClassDistinct _ -> False -- Only initially @White@ colour in text (e.g., not highlighted @BrWhite@) -- gets replaced by the one indicated. msgColor :: MsgClass -> Color.Color msgColor = \case MsgClassShowAndSave x -> case x of MsgBookKeeping -> Color.cBoring MsgStatusWakeup -> Color.cWakeUp MsgStatusStopUs -> Color.cBoring MsgStatusStopThem -> Color.cBoring MsgItemCreation -> Color.cGreed MsgItemRuination -> Color.cBoring -- common, colourful components created MsgDeathVictory -> Color.cVeryGoodEvent MsgDeathDeafeat -> Color.cVeryBadEvent MsgDeathBoring -> Color.cBoring MsgRiskOfDeath -> Color.cGraveRisk MsgPointmanSwap -> Color.cBoring MsgFactionIntel -> Color.cMeta -- good or bad MsgFinalOutcome -> Color.cGameOver MsgBackdropInfo -> Color.cBoring MsgTerrainReveal -> Color.cIdentification MsgItemDiscovery -> Color.cIdentification MsgSpottedActor -> Color.cBoring -- common; warning in @MsgSpottedThreat@ MsgItemMovement -> Color.cBoring MsgActionMajor -> Color.cBoring MsgActionMinor -> Color.cBoring MsgEffectMajor -> Color.cRareNeutralEvent MsgEffectMedium -> Color.cNeutralEvent MsgEffectMinor -> Color.cBoring MsgMiscellanous -> Color.cBoring MsgHeardOutside -> Color.cBoring MsgHeardNearby -> Color.cGraveRisk MsgHeardFaraway -> Color.cRisk MsgBackdropFocus -> Color.cVista MsgActionWarning -> Color.cMeta MsgRangedMightyWe -> Color.cGoodEvent MsgRangedMightyUs -> Color.cVeryBadEvent MsgRangedOthers -> Color.cBoring MsgRangedNormalUs -> Color.cBadEvent MsgGoodMiscEvent -> Color.cGoodEvent MsgBadMiscEvent -> Color.cBadEvent MsgNeutralEvent -> Color.cNeutralEvent MsgSpecialEvent -> Color.cRareNeutralEvent MsgMeleeMightyWe -> Color.cGoodEvent MsgMeleeMightyUs -> Color.cVeryBadEvent MsgMeleeComplexWe -> Color.cGoodEvent MsgMeleeComplexUs -> Color.cBadEvent MsgMeleeOthers -> Color.cBoring MsgMeleeNormalUs -> Color.cBadEvent MsgActionComplete -> Color.cBoring MsgAtFeetMajor -> Color.cBoring MsgAtFeetMinor -> Color.cBoring MsgTutorialHint -> Color.cTutorialHint MsgClassShow x -> case x of MsgPromptGeneric -> Color.cBoring MsgPromptFocus -> Color.cVista MsgPromptMention -> Color.cNeutralEvent MsgPromptModify -> Color.cRareNeutralEvent MsgPromptActors -> Color.cRisk MsgPromptItems -> Color.cGreed MsgPromptAction -> Color.cMeta MsgActionAlert -> Color.cMeta MsgSpottedThreat -> Color.cGraveRisk MsgClassSave x -> case x of MsgInnerWorkSpam -> Color.cBoring MsgNumericReport -> Color.cBoring MsgClassIgnore x -> case x of MsgMacroOperation -> Color.cBoring MsgRunStopReason -> Color.cBoring MsgStopPlayback -> Color.cMeta MsgClassDistinct x -> case x of MsgSpottedItem -> Color.cBoring MsgStatusSleep -> Color.cSleep MsgStatusGoodUs -> Color.cGoodEvent MsgStatusBadUs -> Color.cBadEvent MsgStatusOthers -> Color.cBoring MsgStatusBenign -> Color.cBoring MsgStatusWarning -> Color.cMeta MsgStatusLongerUs -> Color.cBoring -- not important enough MsgStatusLongThem -> Color.cBoring -- not important enough, no disturb even -- * Report data RepMsgNK = RepMsgNK {repMsg :: Msg, _repShow :: Int, _repSave :: Int} deriving (Show, Generic) instance Binary RepMsgNK -- | If only one of the message components is non-empty and non-whitespace, -- but its count is zero, the message is considered empty. nullRepMsgNK :: RepMsgNK -> Bool nullRepMsgNK (RepMsgNK Msg{..} _ _) = all (Char.isSpace . Color.charFromW32) msgShow && all (Char.isSpace . Color.charFromW32) msgSave -- | The set of messages, with repetitions, to show at the screen at once. newtype Report = Report [RepMsgNK] deriving (Show, Binary) -- | Empty set of messages. emptyReport :: Report emptyReport = assert (let checkLen msgClass = let len = length (showSimpleMsgClass msgClass) in len >= 14 && len <= 17 l = map MsgClassShowAndSave [minBound .. maxBound] ++ map MsgClassShow [minBound .. maxBound] ++ map MsgClassSave [minBound .. maxBound] ++ map MsgClassIgnore [minBound .. maxBound] ++ map MsgClassDistinct [minBound .. maxBound] in allB checkLen l) $ Report [] -- as good place as any to verify display lengths -- | Test if the list of non-whitespace messages is empty. nullVisibleReport :: Report -> Bool nullVisibleReport (Report l) = all (all (Char.isSpace . Color.charFromW32) . msgShow . repMsg) l -- | Add a message to the start of report. consReport :: Msg -> Report -> Report consReport msg (Report r) = Report $ r ++ [RepMsgNK msg 1 1] -- | Render a report as a (possibly very long) list of 'AttrString'. renderReport :: Bool -> Report -> [AttrString] renderReport displaying (Report r) = let rep = map (\(RepMsgNK msg n k) -> if displaying then (msgShow msg, n) else (msgSave msg, k)) r in reverse $ map renderRepetition rep renderRepetition :: (AttrString, Int) -> AttrString renderRepetition (asRaw, n) = let as = dropWhileEnd (Char.isSpace . Color.charFromW32) asRaw in if n <= 1 || null as then as else as ++ stringToAS ("") anyInReport :: (MsgClass -> Bool) -> Report -> Bool anyInReport f (Report xns) = any (f . msgClass. repMsg) xns -- * History -- | The history of reports. This is a ring buffer of the given length -- containing old archived history and two most recent reports stored -- separately. data History = History { newReport :: Report , newTime :: Time , oldReport :: Report , oldTime :: Time , archivedHistory :: RB.RingBuffer UAttrString } deriving (Show, Generic) instance Binary History -- | Empty history of the given maximal length. emptyHistory :: Int -> History emptyHistory size = let ringBufferSize = size - 1 -- a report resides outside the buffer in History emptyReport timeZero emptyReport timeZero (RB.empty ringBufferSize U.empty) scrapRepetitionSingle :: (AttrString, Int) -> [(AttrString, Int)] -> [(AttrString, Int)] -> (Bool, [(AttrString, Int)], [(AttrString, Int)]) scrapRepetitionSingle (s1, n1) rest1 oldMsgs = let butLastEOLs = dropWhileEnd ((== '\n') . Color.charFromW32) eqs1 (s2, _) = butLastEOLs s1 == butLastEOLs s2 in case break eqs1 rest1 of (_, []) -> case break eqs1 oldMsgs of (noDup, (_, n2) : rest2) -> -- We keep the occurence of the message in the new report only. let newReport = (s1, n1 + n2) : rest1 oldReport = noDup ++ ([], 0) : rest2 in (True, newReport, oldReport) _ -> (False, (s1, n1) : rest1, oldMsgs) (noDup, (s2, n2) : rest3) -> -- We keep the older (and so, oldest) occurence of the message, -- to avoid visual disruption by moving the message around. let newReport = ([], 0) : noDup ++ (s2, n1 + n2) : rest3 oldReport = oldMsgs in (True, newReport, oldReport) scrapRepetition :: History -> Maybe History scrapRepetition History{ newReport = Report newMsgs , oldReport = Report oldMsgs , .. } = case newMsgs of -- We take into account only first message of the new report, -- because others were deduplicated as they were added. -- We keep the message in the new report, because it should not -- vanish from the screen. In this way the message may be passed -- along many reports. RepMsgNK msg1 n1 k1 : rest1 -> let -- We ignore message classes and scrap even if same strings -- come from different classes. Otherwise user would be confused. makeShow = map (\(RepMsgNK msg n _) -> (msgShow msg, n)) makeSave = map (\(RepMsgNK msg _ k) -> (msgSave msg, k)) (scrapShowNeeded, scrapShowNew, scrapShowOld) = scrapRepetitionSingle (msgShow msg1, n1) (makeShow rest1) (makeShow oldMsgs) (scrapSaveNeeded, scrapSaveNew, scrapSaveOld) = scrapRepetitionSingle (msgSave msg1, k1) (makeSave rest1) (makeSave oldMsgs) in if scrapShowNeeded || scrapSaveNeeded then let combineMsg _ ([], _) ([], _) = Nothing combineMsg msg (s, n) (t, k) = Just $ RepMsgNK msg{msgShow = s, msgSave = t} n k zipMsg l1 l2 l3 = Report $ catMaybes $ zipWith3 combineMsg (map repMsg l1) l2 l3 newReport = zipMsg newMsgs scrapShowNew scrapSaveNew oldReport = zipMsg oldMsgs scrapShowOld scrapSaveOld in Just History{..} else Nothing _ -> error "scrapRepetition: empty new report for scrapping" -- | Add a message to the new report of history, eliminating a possible -- duplicate and noting its existence in the result. addToReport :: S.Set Msg -> Bool -> Bool -> History -> Msg -> Time -> (S.Set Msg, History, Bool) addToReport usedHints displayHints inMelee oldHistory@History{newReport = Report r, ..} msgRaw time = -- When each turn we lose HP, stuff that wouldn't interrupt -- running should go at most to message log, not onto the screen, -- unless it goes only onto screen, so the message would be lost. let isMsgClassShow = \case MsgClassShow{} -> True _ -> False msg = if inMelee && not (interruptsRunning (msgClass msgRaw)) && not (isMsgClassShow $ msgClass msgRaw) then msgRaw {msgShow = []} else msgRaw repMsgNK = RepMsgNK msg 1 1 newH = History { newReport = Report $ repMsgNK : r , newTime = time , .. } msgIsHint = isTutorialHint (msgClass msg) msgUsedAsHint = S.member msg usedHints newUsedHints = if msgIsHint && displayHints && not msgUsedAsHint then S.insert msg usedHints else usedHints in -- Tutorial hint shown only when tutorial enabled and hint not yet shown. if | msgIsHint && (not displayHints || msgUsedAsHint) -> (usedHints, oldHistory, False) | not (scrapsRepeats $ msgClass msg) || nullRepMsgNK repMsgNK -> -- Don't waste time on never shown messages. (newUsedHints, newH, False) | otherwise -> case scrapRepetition newH of Just scrappedH -> (newUsedHints, scrappedH, True) Nothing -> (newUsedHints, newH, False) -- | Add a newline to end of the new report of history, unless empty. addEolToNewReport :: History -> History addEolToNewReport hist = let addEolToReport (Report []) = Report [] addEolToReport (Report (hd : tl)) = Report $ addEolToRepMsgNK hd : tl addEolToRepMsgNK rm = rm {repMsg = addEolToMsg $ repMsg rm} addEolToMsg msg = msg { msgShow = addEolToAS $ msgShow msg , msgSave = addEolToAS $ msgSave msg } addEolToAS as = as ++ stringToAS "\n" in hist {newReport = addEolToReport $ newReport hist} -- | Archive old report to history, filtering out messages with 0 duplicates -- and prompts. Set up new report with a new timestamp. archiveReport :: History -> History archiveReport History{newReport=Report newMsgs, ..} = let newFiltered@(Report r) = Report $ filter (not . nullRepMsgNK) newMsgs in if null r then -- Drop empty new report. History emptyReport timeZero oldReport oldTime archivedHistory else let lU = map attrStringToU $ renderTimeReport oldTime oldReport in History emptyReport timeZero newFiltered newTime $ foldl' (\ !h !v -> RB.cons v h) archivedHistory lU renderTimeReport :: Time -> Report -> [AttrString] renderTimeReport t rep@(Report r) = let turns = t `timeFitUp` timeTurn repMsgs = renderReport False rep mgsClasses = reverse $ map (showSimpleMsgClass . msgClass . repMsg) r turnsString = show turns isSpace32 = Char.isSpace . Color.charFromW32 worthSaving = not . all isSpace32 renderClass (as, msgClassString) = let lenUnderscore = 17 - length msgClassString + max 0 (3 - length turnsString) in stringToAS (turnsString ++ ":") ++ map (Color.attrChar2ToW32 Color.BrBlack) ("[" ++ replicate lenUnderscore '_' ++ msgClassString ++ "]") ++ [Color.spaceAttrW32] ++ dropWhile isSpace32 as in map renderClass $ filter (worthSaving . fst) $ zip repMsgs mgsClasses lengthHistory :: History -> Int lengthHistory History{oldReport, archivedHistory} = RB.length archivedHistory + length (renderTimeReport timeZero oldReport) -- matches @renderHistory@ -- | Render history as many lines of text. New report is not rendered. -- It's expected to be empty when history is shown. renderHistory :: History -> [AttrString] renderHistory History{..} = map uToAttrString (RB.toList archivedHistory) ++ renderTimeReport oldTime oldReport LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/MsgM.hs0000644000000000000000000001367707346545000021711 0ustar0000000000000000-- | Monadic operations on game messages. module Game.LambdaHack.Client.UI.MsgM ( msgAddDuplicate, msgAddDistinct, msgAdd, msgLnAdd , promptMainKeys, recordHistory ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State import Game.LambdaHack.Definition.Defs sniffMessages :: Bool sniffMessages = False -- | Add a shared message to the current report. Say if it was a duplicate. msgAddDuplicate :: (MonadClientUI m, MsgShared a) => a -> Text -> m Bool msgAddDuplicate msgClass t = do sUIOptions <- getsSession sUIOptions time <- getsState stime history <- getsSession shistory curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut usedHints <- getsSession susedHints lid <- getArenaUI condInMelee <- condInMeleeM lid smuteMessages <- getsSession smuteMessages let displayHints = fromMaybe curTutorial overrideTut msg = toMsgShared (uMessageColors sUIOptions) msgClass t (nusedHints, nhistory, duplicate) = addToReport usedHints displayHints condInMelee history msg time unless smuteMessages $ do modifySession $ \sess -> sess {shistory = nhistory, susedHints = nusedHints} when sniffMessages $ clientPrintUI t return duplicate -- | Add a message comprising of two different texts, one to show, the other -- to save to messages log, to the current report. msgAddDistinct :: MonadClientUI m => MsgClassDistinct -> (Text, Text) -> m () msgAddDistinct msgClass (t1, t2) = do sUIOptions <- getsSession sUIOptions time <- getsState stime history <- getsSession shistory curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut usedHints <- getsSession susedHints lid <- getArenaUI condInMelee <- condInMeleeM lid smuteMessages <- getsSession smuteMessages let displayHints = fromMaybe curTutorial overrideTut msg = toMsgDistinct (uMessageColors sUIOptions) msgClass t1 t2 (nusedHints, nhistory, _) = addToReport usedHints displayHints condInMelee history msg time unless smuteMessages $ do modifySession $ \sess -> sess {shistory = nhistory, susedHints = nusedHints} when sniffMessages $ clientPrintUI t1 -- | Add a message to the current report. msgAdd :: (MonadClientUI m, MsgShared a) => a -> Text -> m () msgAdd msgClass t = void $ msgAddDuplicate msgClass t -- | Add a message to the current report. End previously collected report, -- if any, with newline. msgLnAdd :: (MonadClientUI m, MsgShared a) => a -> Text -> m () msgLnAdd msgClass t = do smuteMessages <- getsSession smuteMessages unless smuteMessages $ modifySession $ \sess -> sess {shistory = addEolToNewReport $ shistory sess} msgAdd msgClass t -- | Add a prompt with basic keys description. promptMainKeys :: MonadClientUI m => m () promptMainKeys = do side <- getsClient sside ours <- getsState $ fidActorNotProjGlobalAssocs side revCmd <- revCmdMap let kmHelp = revCmd HumanCmd.Hint kmViewStash = revCmd (HumanCmd.ChooseItemMenu (MStore CStash)) kmItemStash = revCmd (HumanCmd.MoveItem [CGround, CEqp] CStash Nothing False) kmXhairPointerFloor = revCmd HumanCmd.XhairPointerFloor saimMode <- getsSession saimMode UIOptions{uVi, uLeftHand} <- getsSession sUIOptions xhair <- getsSession sxhair miniHintAiming <- getMiniHintAiming -- The silly "axwdqezc" name of keys is chosen to match "hjklyubn", -- which the usual way of writing them. let moveKeys | uVi && uLeftHand = "keypad or axwdqezc or hjklyubn" | uLeftHand = "keypad or axwdqezc" | uVi = "keypad or hjklyubn" | otherwise = "keypad" manyTeammates = length ours > 1 -- @Tab@ here is not a button, which we would write consistently -- as @TAB@, just as in our internal in-game key naming, but a key name -- as written on the keyboard, hence most useful to a newbie. keepTab = if manyTeammates then "Switch to another teammate with Tab, while all others auto-melee foes, if adjacent, but normally don't chase them." else "" describePos = if describeIsNormal then "Describe map position with MMB or RMB." else "" viewEquip = if stashKeysAreNormal then "View shared 'I'nventory stash and stash items into the 'i'nventory." else "" moreHelp = "Press '" <> tshow kmHelp <> "' for more help." describeIsNormal = kmXhairPointerFloor == K.middleButtonReleaseKM stashKeysAreNormal = kmViewStash == K.mkChar 'I' && kmItemStash == K.mkChar 'i' keys | isNothing saimMode = "Explore with" <+> moveKeys <+> "or mouse." <+> describePos <+> viewEquip <+> keepTab <+> moreHelp | otherwise = miniHintAiming <+> tgtKindVerb xhair <+> "with" <+> moveKeys <+> "keys or mouse." <+> keepTab <+> moreHelp void $ msgAdd MsgPromptGeneric keys tgtKindVerb :: Maybe Target -> Text tgtKindVerb mtgt = case mtgt of Just TEnemy{} -> "Aim at enemy" Just TNonEnemy{} -> "Aim at non-enemy" Just TPoint{} -> "Aim at position" Just TVector{} -> "Indicate a move vector" Nothing -> "Start aiming" -- | Store new report in the history and archive old report. recordHistory :: MonadClientUI m => m () recordHistory = modifySession $ \sess -> sess {shistory = archiveReport $ shistory sess} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Overlay.hs0000644000000000000000000002700007346545000022450 0ustar0000000000000000{-# LANGUAGE RankNTypes, TupleSections #-} -- | Screen overlays. module Game.LambdaHack.Client.UI.Overlay ( -- * DisplayFont DisplayFont, isPropFont, isSquareFont, isMonoFont, textSize , -- * FontSetup FontSetup(..), multiFontSetup, singleFontSetup , -- * AttrString AttrString, blankAttrString, textToAS, textFgToAS, stringToAS , (<+:>), (<\:>) -- * AttrLine , AttrLine, attrLine, emptyAttrLine, attrStringToAL, firstParagraph , textToAL, textFgToAL, stringToAL, linesAttr , splitAttrString, indentSplitAttrString -- * Overlay , Overlay, xytranslateOverlay, xtranslateOverlay, ytranslateOverlay , offsetOverlay, offsetOverlayX, typesetXY , updateLine, rectangleOfSpaces, maxYofOverlay, labDescOverlay #ifdef EXPOSE_INTERNAL -- * Internal operations , nonbreakableRev, isPrefixOfNonbreakable, breakAtSpace, splitAttrPhrase #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Char (isSpace) import qualified Data.Text as T import Game.LambdaHack.Client.UI.PointUI import qualified Game.LambdaHack.Definition.Color as Color -- * DisplayFont -- | Three types of fonts used in the UI. Overlays (layers, more or less) -- in proportional font are overwritten by layers in square font, -- which are overwritten by layers in mono font. -- All overlays overwrite the rendering of the game map, which is -- the underlying basic UI frame, comprised of square font glyps. -- -- This type needs to be kept abstract to ensure that frontend-enforced -- or user config-enforced font assignments in 'FontSetup' -- (e.g., stating that the supposedly proportional font is overriden -- to be the square font) can't be ignored. Otherwise a programmer -- could use arbirary @DisplayFont@, instead of the one taken from 'FontSetup', -- and so, e.g., calculating the width of an overlay so constructed -- in order to decide where another overlay can start would be inconsistent -- what what font is really eventually used when rendering. -- -- Note that the order of constructors has limited effect, -- but it illustrates how overwriting is explicitly implemented -- in frontends that support all fonts. data DisplayFont = PropFont | SquareFont | MonoFont deriving (Show, Eq, Enum) isPropFont, isSquareFont, isMonoFont :: DisplayFont -> Bool isPropFont = (== PropFont) isSquareFont = (== SquareFont) isMonoFont = (== MonoFont) textSize :: DisplayFont -> [a] -> Int textSize SquareFont l = 2 * length l textSize MonoFont l = length l textSize PropFont _ = error "size of proportional font texts is not defined" -- * FontSetup data FontSetup = FontSetup { squareFont :: DisplayFont , monoFont :: DisplayFont , propFont :: DisplayFont } deriving (Eq, Show) -- for unit tests multiFontSetup :: FontSetup multiFontSetup = FontSetup SquareFont MonoFont PropFont singleFontSetup :: FontSetup singleFontSetup = FontSetup SquareFont SquareFont SquareFont -- * AttrString -- | String of colourful text. End of line characters permitted. type AttrString = [Color.AttrCharW32] blankAttrString :: Int -> AttrString blankAttrString w = replicate w Color.spaceAttrW32 textToAS :: Text -> AttrString textToAS !t = let f c l = let !ac = Color.attrChar1ToW32 c in ac : l in T.foldr f [] t textFgToAS :: Color.Color -> Text -> AttrString textFgToAS !fg !t = let f ' ' l = Color.spaceAttrW32 : l -- for speed and simplicity (testing if char is a space) -- we always keep the space @White@ f c l = let !ac = Color.attrChar2ToW32 fg c in ac : l in T.foldr f [] t stringToAS :: String -> AttrString stringToAS = map Color.attrChar1ToW32 -- Follows minimorph.<+>. infixr 6 <+:> -- matches Monoid.<> (<+:>) :: AttrString -> AttrString -> AttrString (<+:>) [] l2 = l2 (<+:>) l1 [] = l1 (<+:>) l1 l2@(c2 : _) = if isSpace (Color.charFromW32 c2) || isSpace (Color.charFromW32 (last l1)) then l1 ++ l2 else l1 ++ [Color.spaceAttrW32] ++ l2 infixr 6 <\:> -- matches Monoid.<> (<\:>) :: AttrString -> AttrString -> AttrString (<\:>) [] l2 = l2 (<\:>) l1 [] = l1 (<\:>) l1 l2@(c2 : _) = if Color.charFromW32 c2 == '\n' || Color.charFromW32 (last l1) == '\n' then l1 ++ l2 else l1 ++ stringToAS "\n" ++ l2 -- We consider only these, because they are short and form a closed category. nonbreakableRev :: [String] nonbreakableRev = ["eht", "a", "na", "ehT", "A", "nA", "I"] isPrefixOfNonbreakable :: AttrString -> Bool isPrefixOfNonbreakable s = let isPrefixOfNb sRev nbRev = case stripPrefix nbRev sRev of Nothing -> False Just [] -> True Just (c : _) -> isSpace c in any (isPrefixOfNb $ map Color.charFromW32 s) nonbreakableRev breakAtSpace :: AttrString -> (AttrString, AttrString) breakAtSpace lRev = let (pre, post) = break (== Color.spaceAttrW32) lRev in case post of c : rest | c == Color.spaceAttrW32 -> if isPrefixOfNonbreakable rest then let (pre2, post2) = breakAtSpace rest in (pre ++ c : pre2, post2) else (pre, post) _ -> (pre, post) -- no space found, give up -- * AttrLine -- | Line of colourful text. End of line characters forbidden. Trailing -- @White@ space forbidden. newtype AttrLine = AttrLine {attrLine :: AttrString} deriving (Show, Eq) emptyAttrLine :: AttrLine emptyAttrLine = AttrLine [] attrStringToAL :: AttrString -> AttrLine attrStringToAL s = #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) $ -- only expensive for menus, but often violated by code changes, so disabled -- outside test runs #endif AttrLine s firstParagraph :: AttrString -> AttrLine firstParagraph s = case linesAttr s of [] -> emptyAttrLine l : _ -> l textToAL :: Text -> AttrLine textToAL !t = let f '\n' _ = error $ "illegal end of line in: " ++ T.unpack t f c l = let !ac = Color.attrChar1ToW32 c in ac : l s = T.foldr f [] t in AttrLine $ #ifdef WITH_EXPENSIVE_ASSERTIONS assert (null s || last s /= Color.spaceAttrW32 `blame` t) #endif s textFgToAL :: Color.Color -> Text -> AttrLine textFgToAL !fg !t = let f '\n' _ = error $ "illegal end of line in: " ++ T.unpack t f ' ' l = Color.spaceAttrW32 : l -- for speed and simplicity (testing if char is a space) -- we always keep the space @White@ f c l = let !ac = Color.attrChar2ToW32 fg c in ac : l s = T.foldr f [] t in AttrLine $ #ifdef WITH_EXPENSIVE_ASSERTIONS assert (null s || last s /= Color.spaceAttrW32 `blame` t) #endif s stringToAL :: String -> AttrLine stringToAL s = attrStringToAL $ map Color.attrChar1ToW32 s -- Mimics @lines@. linesAttr :: AttrString -> [AttrLine] linesAttr [] = [] linesAttr l = cons (case break (\ac -> Color.charFromW32 ac == '\n') l of (h, t) -> (attrStringToAL h, case t of [] -> [] _ : tt -> linesAttr tt)) where cons ~(h, t) = h : t -- | Split a string into lines. Avoid breaking the line at a character -- other than space. Remove the spaces on which lines are broken, -- keep other spaces. In expensive assertions mode (dev debug mode) -- fail at trailing spaces, but keep leading spaces, e.g., to make -- distance from a text in another font. Newlines are respected. -- -- Note that we only split wrt @White@ space, nothing else, -- and the width, in the first argument, is calculated in characters, -- not in UI (mono font) coordinates, so that taking and dropping characters -- is performed correctly. splitAttrString :: Int -> Int -> AttrString -> [AttrLine] splitAttrString w0 w1 l = case linesAttr l of [] -> [] x : xs -> splitAttrPhrase w0 w1 x ++ concatMap (splitAttrPhrase w1 w1) xs indentSplitAttrString :: DisplayFont -> Int -> AttrString -> [AttrLine] indentSplitAttrString font w l = assert (w > 4) $ -- Sadly this depends on how wide the space is in propotional font, -- which varies wildly, so we err on the side of larger indent. let nspaces = case font of SquareFont -> 1 MonoFont -> 2 PropFont -> 4 ts = splitAttrString w (w - nspaces) l -- Proportional spaces are very narrow. spaces = replicate nspaces Color.spaceAttrW32 in case ts of [] -> [] hd : tl -> hd : map (AttrLine . (spaces ++) . attrLine) tl -- We pass empty line along for the case of appended buttons, which need -- either space or new lines before them. splitAttrPhrase :: Int -> Int -> AttrLine -> [AttrLine] splitAttrPhrase w0 w1 (AttrLine xs) | w0 >= length xs = [AttrLine xs] -- no problem, everything fits | otherwise = let (pre, postRaw) = splitAt w0 xs preRev = reverse pre ((ppre, ppost), post) = case postRaw of c : rest | c == Color.spaceAttrW32 && not (isPrefixOfNonbreakable preRev) -> (([], preRev), rest) _ -> (breakAtSpace preRev, postRaw) in if all (== Color.spaceAttrW32) ppost then AttrLine (reverse $ dropWhile (== Color.spaceAttrW32) preRev) : splitAttrPhrase w1 w1 (AttrLine post) else AttrLine (reverse $ dropWhile (== Color.spaceAttrW32) ppost) : splitAttrPhrase w1 w1 (AttrLine $ reverse ppre ++ post) -- * Overlay -- | A series of screen lines with start positions at which they should -- be overlayed over the base frame or a blank screen, depending on context. -- The position point is represented as in integer that is an index into the -- frame character array. -- The lines either fit the width of the screen or are intended -- for truncation when displayed. The start positions of lines may fall outside -- the length of the screen, too, unlike in @SingleFrame@. Then they are -- simply not shown. type Overlay = [(PointUI, AttrLine)] xytranslateOverlay :: Int -> Int -> Overlay -> Overlay xytranslateOverlay dx dy = map (\(PointUI x y, al) -> (PointUI (x + dx) (y + dy), al)) xtranslateOverlay :: Int -> Overlay -> Overlay xtranslateOverlay dx = xytranslateOverlay dx 0 ytranslateOverlay :: Int -> Overlay -> Overlay ytranslateOverlay = xytranslateOverlay 0 offsetOverlay :: [AttrLine] -> Overlay offsetOverlay = zipWith (curry (first $ PointUI 0)) [0..] offsetOverlayX :: [(Int, AttrLine)] -> Overlay offsetOverlayX = zipWith (\y (x, al) -> (PointUI x y, al)) [0..] typesetXY :: (Int, Int) -> [AttrLine] -> Overlay typesetXY (xoffset, yoffset) = zipWith (\y al -> (PointUI xoffset (y + yoffset), al)) [0..] -- @f@ should not enlarge the line beyond screen width nor introduce linebreaks. updateLine :: Int -> (Int -> AttrString -> AttrString) -> Overlay -> Overlay updateLine y f ov = let upd (p@(PointUI px py), AttrLine l) = if py == y then (p, AttrLine $ f px l) else (p, AttrLine l) in map upd ov rectangleOfSpaces :: Int -> Int -> Overlay rectangleOfSpaces x y = let blankAttrLine = AttrLine $ replicate x Color.nbspAttrW32 in offsetOverlay $ replicate y blankAttrLine maxYofOverlay :: Overlay -> Int maxYofOverlay ov = let yOfOverlay (PointUI _ y, _) = y in maximum $ 0 : map yOfOverlay ov labDescOverlay :: DisplayFont -> Int -> AttrString -> (Overlay, Overlay) labDescOverlay labFont width as = let (tLab, tDesc) = span (/= Color.spaceAttrW32) as labLen = textSize labFont tLab len = max 0 $ width - length tLab -- not labLen; TODO: type more strictly ovLab = offsetOverlay [attrStringToAL tLab] ovDesc = offsetOverlayX $ case splitAttrString len width tDesc of [] -> [] l : ls -> (labLen, l) : map (0,) ls in (ovLab, ovDesc) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/PointUI.hs0000644000000000000000000000344307346545000022363 0ustar0000000000000000-- | UI screen coordinates. module Game.LambdaHack.Client.UI.PointUI ( PointUI(..), PointSquare(..), squareToUI, uiToSquare , squareToMap, mapToSquare #ifdef EXPOSE_INTERNAL -- * Internal operations , mapStartY #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Common.Point -- | UI screen coordinates, independent of whether square or monospace fonts -- are being placed on the screen (though square fonts are never placed -- on odd coordinates). These are not game map coordinates, -- becuse UI is larger and more fine-grained than just the game map. data PointUI = PointUI Int Int deriving (Show, Eq) -- | Coordinates of the big square fonts. These are not game map coordinates, -- because the latter are offset by @mapStartY@ and represented by @Point@. -- -- However, confusingly, @Point@ is also used for square font glyph coordinates, -- though exclusively in context of rendered frames to be sent to a frontend, -- namely @PointArray.Array@, which is indexed by @Point@ and is a vector, -- and so traditionally indexed starting from zero and not from minus one, -- as would be needed for consistency. data PointSquare = PointSquare Int Int deriving (Show, Eq) squareToUI :: PointSquare -> PointUI {-# INLINE squareToUI #-} squareToUI (PointSquare x y) = PointUI (x * 2) y uiToSquare :: PointUI -> PointSquare {-# INLINE uiToSquare #-} uiToSquare (PointUI x y) = PointSquare (x `div` 2) y -- | The row where the dungeon map starts, both in @PointUI@ -- and @PointSquare@ coordinates. mapStartY :: Int mapStartY = 1 squareToMap :: PointSquare -> Point {-# INLINE squareToMap #-} squareToMap (PointSquare x y) = Point x (y - mapStartY) mapToSquare :: Point -> PointSquare {-# INLINE mapToSquare #-} mapToSquare (Point x y) = PointSquare x (y + mapStartY) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/RunM.hs0000644000000000000000000003160707346545000021720 0ustar0000000000000000{-# LANGUAGE RankNTypes #-} -- | Running and disturbance. -- -- The general rule is: whatever is behind you (and so ignored previously), -- determines what you ignore moving forward. This is calcaulated -- separately for the tiles to the left, to the right and in the middle -- along the running direction. So, if you want to ignore something -- start running when you stand on it (or to the right or left, respectively) -- or by entering it (or passing to the right or left, respectively). -- -- Some things are never ignored, such as: enemies seen, imporant messages -- heard, solid tiles and actors in the way. module Game.LambdaHack.Client.UI.RunM ( continueRun #ifdef EXPOSE_INTERNAL -- * Internal operations , continueRunDir, walkableDir, tryTurning, checkAndRun #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import GHC.Exts (inline) import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.TileKind (TileKind) import Game.LambdaHack.Definition.Defs -- | Continue running in the given direction. continueRun :: MonadClientUI m => LevelId -> RunParams -> m (Either Text RequestTimed) continueRun arena paramOld = case paramOld of RunParams{ runMembers = [] , runStopMsg = Just stopMsg } -> return $ Left stopMsg RunParams{ runMembers = [] , runStopMsg = Nothing } -> return $ Left "selected actors no longer there" RunParams{ runLeader , runMembers = r : rs , runInitial , runStopMsg } -> do -- If runInitial and r == runLeader, it means the leader moves -- again, after all other members, in step 0, -- so we call continueRunDir with True to change direction once -- and then unset runInitial. let runInitialNew = runInitial && r /= runLeader paramIni = paramOld {runInitial = runInitialNew} onLevel <- getsState $ memActor r arena onLevelLeader <- getsState $ memActor runLeader arena if | not onLevel -> do let paramNew = paramIni {runMembers = rs } continueRun arena paramNew | not onLevelLeader -> do let paramNew = paramIni {runLeader = r} continueRun arena paramNew | otherwise -> do mdirOrRunStopMsgCurrent <- continueRunDir paramOld let runStopMsgCurrent = either Just (const Nothing) mdirOrRunStopMsgCurrent runStopMsgNew = runStopMsg `mplus` runStopMsgCurrent -- We check @runStopMsgNew@, because even if the current actor -- runs OK, we want to stop soon if some others had to stop. runMembersNew = if isJust runStopMsgNew then rs else rs ++ [r] paramNew = paramIni { runMembers = runMembersNew , runStopMsg = runStopMsgNew } case mdirOrRunStopMsgCurrent of Left _ -> continueRun arena paramNew -- run all others undisturbed; one time Right dir -> do updateClientLeader r modifySession $ \sess -> sess {srunning = Just paramNew} return $ Right $ ReqMove dir -- The potential invisible actor is hit. War is started without asking. -- | This function implements the actual logic of running. It checks if we -- have to stop running because something interesting cropped up, -- it ajusts the direction given by the vector if we reached -- a corridor's corner (we never change direction except in corridors) -- and it increments the counter of traversed tiles. -- -- Note that while goto-xhair commands ignore items on the way, -- here we stop wnenever we touch an item. Running is more cautious -- to compensate that the player cannot specify the end-point of running. -- It's also more suited to open, already explored terrain. Goto-xhair -- works better with unknown terrain, e.g., it stops whenever an item -- is spotted, but then ignores the item, leaving it to the player -- to mark the item position as a goal of the next goto. continueRunDir :: MonadClientUI m => RunParams -> m (Either Text Vector) continueRunDir params = case params of RunParams{ runMembers = [] } -> error $ "" `showFailure` params RunParams{ runLeader , runMembers = aid : _ , runInitial } -> do report <- getsSession $ newReport . shistory let msgInterrupts = anyInReport interruptsRunning report if msgInterrupts then return $ Left "message shown" else do cops@COps{coTileSpeedup} <- getsState scops rbody <- getsState $ getActorBody runLeader let rposHere = bpos rbody rposLast = fromMaybe (error $ "" `showFailure` (runLeader, rbody)) (boldpos rbody) -- Match run-leader dir, because we want runners to keep formation. dir = rposHere `vectorToFrom` rposLast body <- getsState $ getActorBody aid let lid = blid body lvl <- getLevel lid let posHere = bpos body posThere = posHere `shift` dir bigActorThere = occupiedBigLvl posThere lvl projsThere = occupiedProjLvl posThere lvl let openableLast = Tile.isOpenable coTileSpeedup (lvl `at` (posHere `shift` dir)) check | bigActorThere = return $ Left "actor in the way" | projsThere = return $ Left "projectile in the way" -- don't displace actors, except with leader in step 0 | walkableDir cops lvl posHere dir = if runInitial && aid /= runLeader then return $ Right dir -- zeroth step always OK else checkAndRun aid dir | not (runInitial && aid == runLeader) = return $ Left "blocked" -- don't change direction, except in step 1 and by run-leader | openableLast = return $ Left "blocked by a closed door" -- the player may prefer to open the door | otherwise = -- Assume turning is permitted, because this is the start -- of the run, so the situation is mostly known to the player tryTurning aid check walkableDir :: COps -> Level -> Point -> Vector -> Bool walkableDir COps{coTileSpeedup} lvl spos dir = Tile.isWalkable coTileSpeedup $ lvl `at` (spos `shift` dir) tryTurning :: MonadClientRead m => ActorId -> m (Either Text Vector) tryTurning aid = do cops@COps{coTileSpeedup} <- getsState scops body <- getsState $ getActorBody aid let lid = blid body lvl <- getLevel lid let posHere = bpos body posLast = fromMaybe (error $ "" `showFailure` (aid, body)) (boldpos body) dirLast = posHere `vectorToFrom` posLast let openableDir dir = Tile.isOpenable coTileSpeedup (lvl `at` (posHere `shift` dir)) dirWalkable dir = walkableDir cops lvl posHere dir || openableDir dir dirNearby dir1 dir2 = euclidDistSqVector dir1 dir2 == 1 -- Distance 2 could be useful, but surprising even to apt players. dirSimilar dir = dirNearby dirLast dir && dirWalkable dir dirsSimilar = filter dirSimilar moves case dirsSimilar of [] -> return $ Left "dead end" d1 : ds | all (dirNearby d1) ds -> -- only one or two directions possible case sortOn (euclidDistSqVector dirLast) $ filter (walkableDir cops lvl posHere) $ d1 : ds of [] -> return $ Left "blocked and all similar directions are non-walkable" d : _ -> checkAndRun aid d _ -> return $ Left "blocked and many distant similar directions found" -- The direction is different than the original, if called from @tryTurning@ -- and the same if from @continueRunDir@. checkAndRun :: MonadClientRead m => ActorId -> Vector -> m (Either Text Vector) checkAndRun aid dir = do COps{coTileSpeedup} <- getsState scops actorMaxSkills <- getsState sactorMaxSkills body <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid body) . sfactionD smarkSuspect <- getsClient smarkSuspect let lid = blid body lvl <- getLevel lid actorD <- getsState sactorD let posHere = bpos body posHasItems pos = EM.member pos $ lfloor lvl posThere = posHere `shift` dir bigActorThere = occupiedBigLvl posThere lvl enemyThreatensThere = let f !p = case posToBigLvl p lvl of Nothing -> False Just aid2 -> g aid2 $ actorD EM.! aid2 g aid2 !b2 = inline isFoe (bfid body) fact (bfid b2) && actorCanMeleeToHarm actorMaxSkills aid2 b2 && bhp b2 > 0 -- uncommon in any f $ vicinityUnsafe posThere projsThere = occupiedProjLvl posThere lvl let posLast = fromMaybe (error $ "" `showFailure` (aid, body)) (boldpos body) dirLast = posHere `vectorToFrom` posLast -- This is supposed to work on unit vectors --- diagonal, as well as, -- vertical and horizontal. anglePos :: Point -> Vector -> RadianAngle -> Point anglePos pos d angle = shift pos (rotate angle d) -- We assume the tiles have not changed since last running step. -- If they did, we don't care --- running should be stopped -- because of the change of nearby tiles then. -- We don't take into account the two tiles at the rear of last -- surroundings, because the actor may have come from there -- (via a diagonal move) and if so, he may be interested in such tiles. -- If he arrived directly from the right or left, he is responsible -- for starting the run further away, if he does not want to ignore -- such tiles as the ones he came from. tileLast = lvl `at` posLast tileHere = lvl `at` posHere tileThere = lvl `at` posThere leftPsLast = map (anglePos posHere dirLast) [pi/2, 3*pi/4] ++ map (anglePos posHere dir) [pi/2, 3*pi/4] rightPsLast = map (anglePos posHere dirLast) [-pi/2, -3*pi/4] ++ map (anglePos posHere dir) [-pi/2, -3*pi/4] leftForwardPosHere = anglePos posHere dir (pi/4) rightForwardPosHere = anglePos posHere dir (-pi/4) leftTilesLast = map (lvl `at`) leftPsLast rightTilesLast = map (lvl `at`) rightPsLast leftForwardTileHere = lvl `at` leftForwardPosHere rightForwardTileHere = lvl `at` rightForwardPosHere tilePropAt :: ContentId TileKind -> (Bool, Bool, Bool, Bool, Bool, Bool) tilePropAt tile = let suspect = smarkSuspect > 0 && Tile.isSuspect coTileSpeedup tile || smarkSuspect > 1 && Tile.isHideAs coTileSpeedup tile embed = Tile.isEmbed coTileSpeedup tile -- no matter if embeds left walkable = Tile.isWalkable coTileSpeedup tile openable = Tile.isOpenable coTileSpeedup tile closable = Tile.isClosable coTileSpeedup tile modifiable = Tile.isModifiable coTileSpeedup tile in (suspect, embed, walkable, openable, closable, modifiable) terrainChangeMiddle = tilePropAt tileThere `notElem` map tilePropAt [tileLast, tileHere] terrainChangeLeft = tilePropAt leftForwardTileHere `notElem` map tilePropAt leftTilesLast terrainChangeRight = tilePropAt rightForwardTileHere `notElem` map tilePropAt rightTilesLast itemChangeLeft = posHasItems leftForwardPosHere `notElem` map posHasItems leftPsLast itemChangeRight = posHasItems rightForwardPosHere `notElem` map posHasItems rightPsLast check | bigActorThere = return $ Left "actor in the way" | enemyThreatensThere = return $ Left "enemy threatens the position" | projsThere = return $ Left "projectile in the way" -- Actor in possibly another direction tnan original. -- (e.g., called from @tryTurning@). | terrainChangeLeft = return $ Left "terrain change on the left" | terrainChangeRight = return $ Left "terrain change on the right" | itemChangeLeft = return $ Left "item change on the left" | itemChangeRight = return $ Left "item change on the right" | terrainChangeMiddle = return $ Left "terrain change in the middle" | otherwise = return $ Right dir check LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/SessionUI.hs0000644000000000000000000003220607346545000022714 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | The client UI session state. module Game.LambdaHack.Client.UI.SessionUI ( SessionUI(..), ReqDelay(..), ItemDictUI, ItemRoles(..), AimMode(..) , KeyMacro(..), KeyMacroFrame(..), RunParams(..), ChosenLore(..) , emptySessionUI, emptyMacroFrame , cycleMarkVision, toggleMarkSmell, cycleOverrideTut, getActorUI ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Map.Strict as M import qualified Data.Set as S import Data.Time.Clock.POSIX import GHC.Generics (Generic) import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Client.Request import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.EffectDescription (DetailLevel (..)) import Game.LambdaHack.Client.UI.Frontend import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.ModeKind (ModeKind) import Game.LambdaHack.Definition.Defs -- | The information that is used across a human player playing session, -- including many consecutive games in a single session, -- including playing different teams. Some of it is saved, some is reset -- when a new playing session starts. Nothing is tied to a faction/team, -- but instead all to UI configuration and UI input and display history. -- An important component is the frontend session. data SessionUI = SessionUI { sreqPending :: Maybe RequestUI -- ^ request created by a UI query -- but not yet sent to the server , sreqDelay :: ReqDelay -- ^ server delayed sending query to client -- or receiving request from client , sreqQueried :: Bool -- ^ player is now queried for a command , sregainControl :: Bool -- ^ player requested to regain control -- from AI ASAP , sxhair :: Maybe Target -- ^ the common xhair , sxhairGoTo :: Maybe Target -- ^ xhair set for last GoTo , sactorUI :: ActorDictUI -- ^ assigned actor UI presentations , sitemUI :: ItemDictUI -- ^ assigned item first seen level , sroles :: ItemRoles -- ^ assignment of roles to items , slastItemMove :: Maybe (CStore, CStore) -- ^ last item move stores , schanF :: ChanFrontend -- ^ connection with the frontend , sccui :: CCUI -- ^ UI client content , sUIOptions :: UIOptions -- ^ UI options as set by the player , saimMode :: Maybe AimMode -- ^ aiming mode , sxhairMoused :: Bool -- ^ last mouse aiming not vacuus , sitemSel :: Maybe (ItemId, CStore, Bool) -- ^ selected item, if any, it's store and -- whether to override suitability check , sselected :: ES.EnumSet ActorId -- ^ the set of currently selected actors , srunning :: Maybe RunParams -- ^ parameters of the current run, if any , shistory :: History -- ^ history of messages , svictories :: EM.EnumMap (ContentId ModeKind) (M.Map Challenge Int) -- ^ the number of games won by the UI faction per game mode -- and per difficulty level , scampings :: ES.EnumSet (ContentId ModeKind) -- ^ camped games , srestarts :: ES.EnumSet (ContentId ModeKind) -- ^ restarted games , spointer :: PointUI -- ^ mouse pointer position , sautoYes :: Bool -- ^ whether to auto-clear prompts , smacroFrame :: KeyMacroFrame -- ^ the head of the key macro stack , smacroStack :: [KeyMacroFrame] -- ^ the tail of the key macro stack , slastLost :: ES.EnumSet ActorId -- ^ actors that just got out of sight , swaitTimes :: Int -- ^ player just waited this many times , swasAutomated :: Bool -- ^ the player just exited AI automation , smarkVision :: Int -- ^ mark leader and party FOV , smarkSmell :: Bool -- ^ mark smell, if the leader can smell , snxtScenario :: Int -- ^ next game scenario number , scurTutorial :: Bool -- ^ whether current game is a tutorial , snxtTutorial :: Bool -- ^ whether next game is to be tutorial , soverrideTut :: Maybe Bool -- ^ override display of tutorial hints , susedHints :: S.Set Msg -- ^ tutorial hints already shown this game , smuteMessages :: Bool -- ^ whether to mute all new messages , smenuIxMap :: M.Map String Int -- ^ indices of last used menu items , schosenLore :: ChosenLore -- ^ last lore chosen to display , sdisplayNeeded :: Bool -- ^ current level needs displaying , sturnDisplayed :: Bool -- ^ a frame was already displayed this turn , sreportNull :: Bool -- ^ whether no visible report created -- last UI faction turn or the report -- wiped out from screen since , sstart :: POSIXTime -- ^ this session start time , sgstart :: POSIXTime -- ^ this game start time , sallTime :: Time -- ^ clips from start of session -- to current game start , snframes :: Int -- ^ this game current frame count , sallNframes :: Int -- ^ frame count from start of session -- to current game start , srandomUI :: SM.SMGen -- ^ current random generator for UI } data ReqDelay = ReqDelayNot | ReqDelayHandled | ReqDelayAlarm deriving Eq -- | Local macro buffer frame. Predefined macros have their own in-game macro -- buffer, allowing them to record in-game macro, queue actions and repeat -- the last macro's action. -- Running predefined macro pushes new @KeyMacroFrame@ onto the stack. We pop -- buffers from the stack if locally there are no actions pending to be handled. data KeyMacroFrame = KeyMacroFrame { keyMacroBuffer :: Either [K.KM] KeyMacro -- ^ record keystrokes in Left; -- repeat from Right , keyPending :: KeyMacro -- ^ actions pending to be handled , keyLast :: Maybe K.KM -- ^ last pressed key } deriving Show -- This can stay a map forever, not a vector, because it's added to often, -- but never read from, except when the user requests item details. type ItemDictUI = EM.EnumMap ItemId LevelId -- | A collection of item identifier sets indicating what roles (possibly many) -- an item has assigned. newtype ItemRoles = ItemRoles (EM.EnumMap SLore (ES.EnumSet ItemId)) deriving (Show, Binary) -- | Current aiming mode of a client. data AimMode = AimMode { aimLevelId :: LevelId , detailLevel :: DetailLevel } deriving (Show, Eq, Generic) instance Binary AimMode -- | In-game macros. We record menu navigation keystrokes and keystrokes -- bound to commands with one exception --- we exclude keys that invoke -- the @Record@ command, to avoid surprises. -- Keys are kept in the same order in which they're meant to be replayed, -- i.e. the first element of the list is replayed also as the first one. newtype KeyMacro = KeyMacro {unKeyMacro :: [K.KM]} deriving (Show, Eq, Binary, Semigroup, Monoid) -- | Parameters of the current run. data RunParams = RunParams { runLeader :: ActorId -- ^ the original leader from run start , runMembers :: [ActorId] -- ^ the list of actors that take part , runInitial :: Bool -- ^ initial run continuation by any -- run participant, including run leader , runStopMsg :: Maybe Text -- ^ message with the next stop reason , runWaiting :: Int -- ^ waiting for others to move out of the way } deriving Show -- | Last lore being aimed at. data ChosenLore = ChosenLore [(ActorId, Actor)] [(ItemId, ItemQuant)] | ChosenNothing emptySessionUI :: UIOptions -> SessionUI emptySessionUI sUIOptions = SessionUI { sreqPending = Nothing , sreqDelay = ReqDelayNot , sreqQueried = False , sregainControl = False , sxhair = Nothing , sxhairGoTo = Nothing , sactorUI = EM.empty , sitemUI = EM.empty , sroles = ItemRoles $ EM.fromDistinctAscList $ zip [minBound..maxBound] (repeat ES.empty) , slastItemMove = Nothing , schanF = ChanFrontend $ const $ error $ "emptySessionUI: ChanFrontend" `showFailure` () , sccui = emptyCCUI , sUIOptions , saimMode = Nothing , sxhairMoused = True , sitemSel = Nothing , sselected = ES.empty , srunning = Nothing , shistory = emptyHistory 0 , svictories = EM.empty , scampings = ES.empty , srestarts = ES.empty , spointer = PointUI 0 0 , sautoYes = False , smacroFrame = emptyMacroFrame , smacroStack = [] , slastLost = ES.empty , swaitTimes = 0 , swasAutomated = False , smarkVision = 1 , smarkSmell = True , snxtScenario = 0 , scurTutorial = False , snxtTutorial = True -- matches @snxtScenario = 0@ , soverrideTut = Nothing , susedHints = S.empty , smuteMessages = False , smenuIxMap = M.empty , schosenLore = ChosenNothing , sdisplayNeeded = False , sturnDisplayed = False , sreportNull = True , sstart = 0 , sgstart = 0 , sallTime = timeZero , snframes = 0 , sallNframes = 0 , srandomUI = SM.mkSMGen 0 } emptyMacroFrame :: KeyMacroFrame emptyMacroFrame = KeyMacroFrame (Right mempty) mempty Nothing cycleMarkVision :: Int -> SessionUI -> SessionUI cycleMarkVision delta sess = sess {smarkVision = (smarkVision sess + delta) `mod` 3} toggleMarkSmell :: SessionUI -> SessionUI toggleMarkSmell sess = sess {smarkSmell = not (smarkSmell sess)} cycleOverrideTut :: Int -> SessionUI -> SessionUI cycleOverrideTut delta sess = let ordering = cycle [Nothing, Just False, Just True] in sess {soverrideTut = let ix = fromJust $ elemIndex (soverrideTut sess) ordering in ordering !! (ix + delta)} getActorUI :: ActorId -> SessionUI -> ActorUI getActorUI aid sess = EM.findWithDefault (error $ "" `showFailure` (aid, sactorUI sess)) aid $ sactorUI sess instance Binary SessionUI where put SessionUI{..} = do put sxhair put sactorUI put sitemUI put sroles put sUIOptions put saimMode put sitemSel put sselected put srunning put $ archiveReport shistory -- avoid displaying ending messages again at game start put svictories put scampings put srestarts put smarkVision put smarkSmell put snxtScenario put scurTutorial put snxtTutorial put soverrideTut put susedHints put (show srandomUI) get = do sxhair <- get sactorUI <- get sitemUI <- get sroles <- get sUIOptions <- get -- is overwritten ASAP, but useful for, e.g., crash debug saimMode <- get sitemSel <- get sselected <- get srunning <- get shistory <- get svictories <- get scampings <- get srestarts <- get smarkVision <- get smarkSmell <- get snxtScenario <- get scurTutorial <- get snxtTutorial <- get soverrideTut <- get susedHints <- get g <- get let sreqPending = Nothing sreqDelay = ReqDelayNot sreqQueried = False sregainControl = False sxhairGoTo = Nothing slastItemMove = Nothing schanF = ChanFrontend $ const $ error $ "Binary: ChanFrontend" `showFailure` () sccui = emptyCCUI sxhairMoused = True spointer = PointUI 0 0 sautoYes = False smacroFrame = emptyMacroFrame smacroStack = [] slastLost = ES.empty swaitTimes = 0 swasAutomated = False smuteMessages = False smenuIxMap = M.empty schosenLore = ChosenNothing sdisplayNeeded = False -- displayed regardless sturnDisplayed = False sreportNull = True sstart = 0 sgstart = 0 sallTime = timeZero snframes = 0 sallNframes = 0 srandomUI = read g return $! SessionUI{..} instance Binary RunParams where put RunParams{..} = do put runLeader put runMembers put runInitial put runStopMsg put runWaiting get = do runLeader <- get runMembers <- get runInitial <- get runStopMsg <- get runWaiting <- get return $! RunParams{..} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Slideshow.hs0000644000000000000000000004415707346545000023004 0ustar0000000000000000{-# LANGUAGE GeneralizedNewtypeDeriving #-} -- | Slideshows. module Game.LambdaHack.Client.UI.Slideshow ( FontOverlayMap, maxYofFontOverlayMap , KeyOrSlot, MenuSlot, natSlots , ButtonWidth(..) , KYX, xytranslateKXY, xtranslateKXY, ytranslateKXY, yrenumberKXY , OKX, emptyOKX, xytranslateOKX, sideBySideOKX, labDescOKX , Slideshow(slideshow), emptySlideshow, unsnoc, toSlideshow , attrLinesToFontMap, menuToSlideshow, wrapOKX, splitOverlay, splitOKX , highSlideshow #ifdef EXPOSE_INTERNAL -- * Internal operations , keysOKX, showTable, showNearbyScores #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import Data.Time.LocalTime import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import qualified Game.LambdaHack.Common.HighScore as HighScore import qualified Game.LambdaHack.Definition.Color as Color type FontOverlayMap = EM.EnumMap DisplayFont Overlay maxYofFontOverlayMap :: FontOverlayMap -> Int maxYofFontOverlayMap ovs = maximum (0 : map maxYofOverlay (EM.elems ovs)) type KeyOrSlot = Either K.KM MenuSlot newtype MenuSlot = MenuSlot Int deriving (Show, Eq, Ord, Binary, Enum) natSlots :: [MenuSlot] {-# INLINE natSlots #-} natSlots = [MenuSlot 0 ..] -- TODO: probably best merge the PointUI into that and represent -- the position as characters, too, translating to UI positions as needed. -- The problem is that then I need to do a lot of reverse translation -- when creating buttons. -- | Width of on-screen button text, expressed in characters, -- and so UI (mono font) width is deduced from the used font. data ButtonWidth = ButtonWidth { buttonFont :: DisplayFont , buttonWidth :: Int } deriving (Show, Eq) -- | A key or a menu slot at a given position on the screen. type KYX = (KeyOrSlot, (PointUI, ButtonWidth)) xytranslateKXY :: Int -> Int -> KYX -> KYX xytranslateKXY dx dy (km, (PointUI x y, len)) = (km, (PointUI (x + dx) (y + dy), len)) xtranslateKXY :: Int -> KYX -> KYX xtranslateKXY dx = xytranslateKXY dx 0 ytranslateKXY :: Int -> KYX -> KYX ytranslateKXY = xytranslateKXY 0 yrenumberKXY :: Int -> KYX -> KYX yrenumberKXY ynew (km, (PointUI x _, len)) = (km, (PointUI x ynew, len)) -- | An Overlay of text with an associated list of keys or slots -- that activate when the specified screen position is pointed at. -- The list should be sorted wrt rows and then columns. type OKX = (FontOverlayMap, [KYX]) emptyOKX :: OKX emptyOKX = (EM.empty, []) xytranslateOKX ::Int -> Int -> OKX -> OKX xytranslateOKX dx dy (ovs, kyxs) = ( EM.map (xytranslateOverlay dx dy) ovs , map (xytranslateKXY dx dy) kyxs ) sideBySideOKX :: Int -> Int -> OKX -> OKX -> OKX sideBySideOKX dx dy (ovs1, kyxs1) (ovs2, kyxs2) = let (ovs3, kyxs3) = xytranslateOKX dx dy (ovs2, kyxs2) in ( EM.unionWith (++) ovs1 ovs3 , sortOn (\(_, (PointUI x y, _)) -> (y, x)) $ kyxs1 ++ kyxs3 ) -- The bangs are to free the possibly very long input list ASAP. labDescOKX :: DisplayFont -> DisplayFont -> [(AttrString, AttrString, KeyOrSlot)] -> OKX labDescOKX labFont descFont l = let descFontSize | isPropFont descFont = length -- may be less or a bit more | otherwise = textSize descFont processRow :: (AttrString, AttrString, KeyOrSlot) -> (AttrLine, (Int, AttrLine), KYX) processRow (!tLab, !tDesc, !ekm) = let labLen = textSize labFont tLab lenButton = labLen + descFontSize tDesc in ( attrStringToAL tLab , (labLen, attrStringToAL tDesc) , (ekm, (PointUI 0 0, ButtonWidth descFont lenButton)) ) (tsLab, tsDesc, kxs) = unzip3 $ map processRow l ovs = EM.insertWith (++) labFont (offsetOverlay tsLab) $ EM.singleton descFont $ offsetOverlayX tsDesc in (ovs, zipWith yrenumberKXY [0..] kxs) -- | A list of active screenfulls to be shown one after another. -- Each screenful has an independent numbering of rows and columns. newtype Slideshow = Slideshow {slideshow :: [OKX]} deriving (Show, Eq) emptySlideshow :: Slideshow emptySlideshow = Slideshow [] unsnoc :: Slideshow -> Maybe (Slideshow, OKX) unsnoc Slideshow{slideshow} = case reverse slideshow of [] -> Nothing okx : rest -> Just (Slideshow $ reverse rest, okx) toSlideshow :: FontSetup -> Bool -> [OKX] -> Slideshow toSlideshow FontSetup{..}displayTutorialHints okxs = Slideshow $ addFooters False okxs where atEnd = flip (++) appendToFontOverlayMap :: FontOverlayMap -> String -> (FontOverlayMap, PointUI, DisplayFont, Int) appendToFontOverlayMap ovs msgPrefix = let msg | displayTutorialHints = msgPrefix ++ " (ESC to exit, PGUP, HOME, mouse, wheel, arrows, etc.)" | otherwise = msgPrefix maxYminXofOverlay ov = let ymxOfOverlay (PointUI x y, _) = (- y, x) in minimum $ maxBound : map ymxOfOverlay ov -- @sortOn@ less efficient here, because function cheap. assocsYX = sortBy (comparing snd) $ EM.assocs $ EM.map maxYminXofOverlay ovs (fontMax, yMax) = case assocsYX of [] -> (monoFont, 0) (font, (yNeg, _x)) : rest -> let unique = all (\(_, (yNeg2, _)) -> yNeg /= yNeg2) rest in ( if isSquareFont font && unique then font else monoFont , - yNeg ) pMax = PointUI 0 (yMax + 1) -- append after last line in ( EM.insertWith atEnd fontMax [(pMax, stringToAL msg)] ovs , pMax , fontMax , length msg ) addFooters :: Bool -> [OKX] -> [OKX] addFooters _ [] = error $ "" `showFailure` okxs addFooters _ [(als, [])] = -- TODO: make sure this case never coincides with the space button -- actually returning to top, as opposed to finishing preview. let (ovs, p, font, width) = appendToFontOverlayMap als "--end--" in [(ovs, [(Left K.safeSpaceKM, (p, ButtonWidth font width))])] addFooters False [(als, kxs)] = [(als, kxs)] addFooters True [(als, kxs)] = let (ovs, p, font, width) = appendToFontOverlayMap als "--back to top--" in [(ovs, kxs ++ [(Left K.safeSpaceKM, (p, ButtonWidth font width))])] addFooters _ ((als, kxs) : rest) = let (ovs, p, font, width) = appendToFontOverlayMap als "--more--" in (ovs, kxs ++ [(Left K.safeSpaceKM, (p, ButtonWidth font width))]) : addFooters True rest -- | This appends vertically a list of blurbs into a single font overlay map. -- Not to be used if some blurbs need to be places overlapping vertically, -- e.g., when the square font symbol needs to be in the same line -- as the start of the descritpion of the denoted item -- or when mono font buttons need to be after a prompt. attrLinesToFontMap :: [(DisplayFont, [AttrLine])] -> FontOverlayMap attrLinesToFontMap blurb = let zipAttrLines :: Int -> [AttrLine] -> (Overlay, Int) zipAttrLines start als = ( zipWith (curry (first $ PointUI 0)) [start ..] als , start + length als ) addOverlay :: (FontOverlayMap, Int) -> (DisplayFont, [AttrLine]) -> (FontOverlayMap, Int) addOverlay (!em, !start) (font, als) = let (als2, start2) = zipAttrLines start als in ( EM.insertWith (++) font als2 em , start2 ) (ov, _) = foldl' addOverlay (EM.empty, 0) blurb in ov menuToSlideshow :: OKX -> Slideshow menuToSlideshow (als, kxs) = assert (not (EM.null als || null kxs)) $ Slideshow [(als, kxs)] wrapOKX :: DisplayFont -> Int -> Int -> Int -> [(K.KM, String)] -> (Overlay, [KYX]) wrapOKX _ _ _ _ [] = ([], []) wrapOKX displayFont ystart xstart width ks = let overlayLineFromStrings :: Int -> Int -> [String] -> (PointUI, AttrLine) overlayLineFromStrings xlineStart y strings = let p = PointUI xlineStart y in (p, stringToAL $ unwords (reverse strings)) f :: ((Int, Int), (Int, [String], Overlay, [KYX])) -> (K.KM, String) -> ((Int, Int), (Int, [String], Overlay, [KYX])) f ((y, x), (xlineStart, kL, kV, kX)) (key, s) = let len = textSize displayFont s len1 = len + textSize displayFont " " in if x + len >= width then let iov = overlayLineFromStrings xlineStart y kL in f ((y + 1, 0), (0, [], iov : kV, kX)) (key, s) else ( (y, x + len1) , ( xlineStart , s : kL , kV , (Left key, ( PointUI x y , ButtonWidth displayFont (length s) )) : kX ) ) ((ystop, _), (xlineStop, kL1, kV1, kX1)) = foldl' f ((ystart, xstart), (xstart, [], [], [])) ks iov1 = overlayLineFromStrings xlineStop ystop kL1 in (reverse $ iov1 : kV1, reverse kX1) keysOKX :: DisplayFont -> Int -> Int -> Int -> [K.KM] -> (Overlay, [KYX]) keysOKX displayFont ystart xstart width keys = let wrapB :: String -> String wrapB s = "[" ++ s ++ "]" ks = map (\key -> (key, wrapB $ K.showKM key)) keys in wrapOKX displayFont ystart xstart width ks -- The font argument is for the report and keys overlay. Others already have -- assigned fonts. splitOverlay :: FontSetup -> Bool -> Int -> Int -> Int -> Report -> [K.KM] -> OKX -> Slideshow splitOverlay fontSetup displayTutorialHints width height wrap report keys (ls0, kxs0) = let renderedReport = renderReport True report reportAS = foldr (<\:>) [] renderedReport in toSlideshow fontSetup displayTutorialHints $ splitOKX fontSetup False width height wrap reportAS keys (ls0, kxs0) -- Note that we only split wrt @White@ space, nothing else. splitOKX :: FontSetup -> Bool -> Int -> Int -> Int -> AttrString -> [K.KM] -> OKX -> [OKX] splitOKX FontSetup{..} msgLong width height wrap reportAS keys (ls0, kxs0) = assert (width > 2 && height > 2) $ -- if the strings to split are long these minimums won't be enough, -- but content validation ensures larger values (perhaps large enough?) let reportParagraphs = linesAttr reportAS -- TODO: until SDL support for measuring prop font text is released, -- we have to use MonoFont for the paragraph that ends with buttons. (repProp, repMono) = if null keys then (reportParagraphs, emptyAttrLine) else case reverse reportParagraphs of [] -> ([], emptyAttrLine) l : rest -> (reverse rest, attrStringToAL $ attrLine l ++ [Color.nbspAttrW32]) msgWrap = if msgLong && not (isSquareFont propFont) then 2 * width else wrap -- TODO if with width fits on one screen, use it msgWidth = if msgLong && not (isSquareFont propFont) then 2 * width else width repProp0 = offsetOverlay $ case repProp of [] -> [] r : rs -> -- Make lines of first paragraph long if it has 2 lines at most. -- The first line does not obscure anything and the second line -- is often short anyway. let firstWidth = if length (attrLine r) <= 2 * msgWidth then msgWidth else msgWrap in (indentSplitAttrString propFont firstWidth . attrLine) r -- first possibly long ++ concatMap (indentSplitAttrString propFont msgWrap . attrLine) rs -- TODO: refactor this ugly pile of copy-paste repPropW = offsetOverlay $ concatMap (indentSplitAttrString propFont width . attrLine) repProp -- If the mono portion first on the line, let it take half width, -- but if previous lines shorter, match them and only buttons -- are permitted to stick out. monoWidth = if null repProp then msgWidth else msgWrap repMono0 = ytranslateOverlay (length repProp0) $ offsetOverlay $ indentSplitAttrString monoFont monoWidth $ attrLine repMono repMonoW = ytranslateOverlay (length repPropW) $ offsetOverlay $ indentSplitAttrString monoFont width $ attrLine repMono repWhole0 = offsetOverlay $ concatMap (indentSplitAttrString propFont msgWidth . attrLine) reportParagraphs repWhole1 = ytranslateOverlay 1 repWhole0 lenOfRep0 = length repProp0 + length repMono0 lenOfRepW = length repPropW + length repMonoW startOfKeys = if null repMono0 then 0 else textSize monoFont (attrLine $ snd $ last repMono0) startOfKeysW = if null repMonoW then 0 else textSize monoFont (attrLine $ snd $ last repMonoW) pressAKey = stringToAS "A long report is shown. Press a key:" ++ [Color.nbspAttrW32] (lX0, keysX0) = keysOKX monoFont 0 (length pressAKey) width keys (lX1, keysX1) = keysOKX monoFont 1 0 width keys (lX, keysX) = keysOKX monoFont (max 0 $ lenOfRep0 - 1) startOfKeys (2 * width) keys (lXW, keysXW) = keysOKX monoFont (max 0 $ lenOfRepW - 1) startOfKeysW (2 * width) keys splitO :: Int -> (Overlay, Overlay, [KYX]) -> OKX -> [OKX] splitO yoffset (hdrProp, hdrMono, rk) (ls, kxs) = let hdrOff | null hdrProp && null hdrMono = 0 | otherwise = 1 + maxYofOverlay hdrMono keyTranslate = map $ ytranslateKXY (hdrOff - yoffset) lineTranslate = EM.map $ ytranslateOverlay (hdrOff - yoffset) yoffsetNew = yoffset + height - hdrOff - 1 ltOffset :: (PointUI, a) -> Bool ltOffset (PointUI _ y, _) = y < yoffsetNew (pre, post) = ( filter ltOffset <$> ls , filter (not . ltOffset) <$> ls ) prependHdr = EM.insertWith (++) propFont hdrProp . EM.insertWith (++) monoFont hdrMono in if all null $ EM.elems post -- all fits on one screen then [(prependHdr $ lineTranslate pre, rk ++ keyTranslate kxs)] else let (preX, postX) = span (\(_, pa) -> ltOffset pa) kxs in (prependHdr $ lineTranslate pre, rk ++ keyTranslate preX) : splitO yoffsetNew (hdrProp, hdrMono, rk) (post, postX) firstParaReport = firstParagraph reportAS hdrShortened = ( [(PointUI 0 0, firstParaReport)] -- shortened for the main slides; in full beforehand , take 3 lX1 -- 3 lines ought to be enough for everyone , keysX1 ) ((lsInit, kxsInit), (headerProp, headerMono, rkxs)) = -- Check whether all space taken by report and keys. if | (lenOfRep0 + length lX) < height -> -- display normally (emptyOKX, (repProp0, lX ++ repMono0, keysX)) | (lenOfRepW + length lXW) < height -> -- display widely (emptyOKX, (repPropW, lXW ++ repMonoW, keysXW)) | length reportParagraphs == 1 && length (attrLine firstParaReport) <= 2 * width -> ( emptyOKX -- already shown in full in @hdrShortened@ , hdrShortened ) | otherwise -> case lX0 of [] -> ( (EM.singleton propFont repWhole0, []) -- showing in full in the init slide , hdrShortened ) lX0first : _ -> ( ( EM.insertWith (++) propFont repWhole1 $ EM.singleton monoFont [(PointUI 0 0, firstParagraph pressAKey), lX0first] , filter (\(_, (PointUI _ y, _)) -> y == 0) keysX0 ) , hdrShortened ) initSlides = if EM.null lsInit then assert (null kxsInit) [] else splitO 0 ([], [], []) (lsInit, kxsInit) -- If @ls0@ is not empty, we still want to display the report, -- one way or another. mainSlides = if EM.null ls0 && not (EM.null lsInit) then assert (null kxs0) [] else splitO 0 (headerProp, headerMono, rkxs) (ls0, kxs0) in initSlides ++ mainSlides -- | Generate a slideshow with the current and previous scores. highSlideshow :: FontSetup -> Bool -> Int -- ^ width of the display area -> Int -- ^ height of the display area -> HighScore.ScoreTable -- ^ current score table -> Int -- ^ position of the current score in the table -> Text -- ^ the name of the game mode -> TimeZone -- ^ the timezone where the game is run -> Slideshow highSlideshow fontSetup@FontSetup{monoFont} displayTutorialHints width height table pos gameModeName tz = let entries = (height - 3) `div` 3 msg = HighScore.showAward entries table pos gameModeName tts = map offsetOverlay $ showNearbyScores tz pos table entries al = textToAS msg splitScreen ts = splitOKX fontSetup False width height width al [K.spaceKM, K.escKM] (EM.singleton monoFont ts, []) in toSlideshow fontSetup displayTutorialHints $ concatMap splitScreen tts -- | Show a screenful of the high scores table. -- Parameter @entries@ is the number of (3-line) scores to be shown. showTable :: TimeZone -> Int -> HighScore.ScoreTable -> Int -> Int -> [AttrLine] showTable tz pos table start entries = let zipped = zip [1..] $ HighScore.unTable table screenful = take entries . drop (start - 1) $ zipped renderScore (pos1, score1) = map (if pos1 == pos then textFgToAL Color.BrWhite else textToAL) $ HighScore.showScore tz pos1 score1 in emptyAttrLine : intercalate [emptyAttrLine] (map renderScore screenful) -- | Produce a couple of renderings of the high scores table. showNearbyScores :: TimeZone -> Int -> HighScore.ScoreTable -> Int -> [[AttrLine]] showNearbyScores tz pos h entries = if pos <= entries then [showTable tz pos h 1 entries] else [ showTable tz pos h 1 entries , showTable tz pos h (max (entries + 1) (pos - entries `div` 2)) entries ] LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/SlideshowM.hs0000644000000000000000000006665307346545000023126 0ustar0000000000000000-- | Monadic operations on slideshows and related data. module Game.LambdaHack.Client.UI.SlideshowM ( overlayToSlideshow, reportToSlideshow, reportToSlideshowKeepHalt , displaySpaceEsc, displayMore, displayMoreKeep, displayYesNo, getConfirms , displayChoiceScreen , displayChoiceScreenWithRightPane , displayChoiceScreenWithDefItemKey , displayChoiceScreenWithRightPaneKMKM , pushFrame, pushReportFrame #ifdef EXPOSE_INTERNAL -- * Internal operations , getMenuIx, saveMenuIx, stepChoiceScreen, navigationKeys, findKYX , drawHighlight, basicFrameWithoutReport #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Char as Char import Data.Either import qualified Data.EnumMap.Strict as EM import qualified Data.Map.Strict as M import qualified Data.Text as T import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.FrameM import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.Overlay import Game.LambdaHack.Client.UI.PointUI import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Definition.Color as Color -- | Add current report to the overlay, split the result and produce, -- possibly, many slides. overlayToSlideshow :: MonadClientUI m => Int -> [K.KM] -> OKX -> m Slideshow overlayToSlideshow y keys okx = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui UIOptions{uMsgWrapColumn} <- getsSession sUIOptions report <- getReportUI True recordHistory -- report will be shown soon, remove it to history fontSetup <- getFontSetup curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut let displayTutorialHints = fromMaybe curTutorial overrideTut return $! splitOverlay fontSetup displayTutorialHints rwidth y uMsgWrapColumn report keys okx -- | Split current report into a slideshow. reportToSlideshow :: MonadClientUI m => [K.KM] -> m Slideshow reportToSlideshow keys = do CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui overlayToSlideshow (rheight - 2) keys emptyOKX -- | Split current report into a slideshow. Keep report unchanged. -- Assume the game either halts waiting for a key after this is shown, -- or many slides are produced, all but the last are displayed -- with player promts between and the last is either shown -- in full or ignored if inside macro (can be recovered from history, -- if important). Unless the prompts interrupt the macro, which is as well. reportToSlideshowKeepHalt :: MonadClientUI m => Bool -> [K.KM] -> m Slideshow reportToSlideshowKeepHalt insideMenu keys = do CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui UIOptions{uMsgWrapColumn} <- getsSession sUIOptions report <- getReportUI insideMenu -- Don't do @recordHistory@; the message is important, but related -- to the messages that come after, so should be shown together. fontSetup <- getFontSetup curTutorial <- getsSession scurTutorial overrideTut <- getsSession soverrideTut let displayTutorialHints = fromMaybe curTutorial overrideTut return $! splitOverlay fontSetup displayTutorialHints rwidth (rheight - 2) uMsgWrapColumn report keys emptyOKX -- | Display a message. Return value indicates if the player wants to continue. -- Feature: if many pages, only the last SPACE exits (but first ESC). displaySpaceEsc :: MonadClientUI m => ColorMode -> Text -> m Bool displaySpaceEsc dm prompt = do unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt -- Two frames drawn total (unless @prompt@ very long). slides <- reportToSlideshow [K.spaceKM, K.escKM] km <- getConfirms dm [K.spaceKM, K.escKM] slides return $! km == K.spaceKM -- | Display a message. Ignore keypresses. -- Feature: if many pages, only the last SPACE exits (but first ESC). displayMore :: MonadClientUI m => ColorMode -> Text -> m () displayMore dm prompt = do unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt slides <- reportToSlideshow [K.spaceKM] void $ getConfirms dm [K.spaceKM, K.escKM] slides displayMoreKeep :: MonadClientUI m => ColorMode -> Text -> m () displayMoreKeep dm prompt = do unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt slides <- reportToSlideshowKeepHalt True [K.spaceKM] void $ getConfirms dm [K.spaceKM, K.escKM] slides -- | Print a yes/no question and return the player's answer. Use black -- and white colours to turn player's attention to the choice. displayYesNo :: MonadClientUI m => ColorMode -> Text -> m Bool displayYesNo dm prompt = do unless (T.null prompt) $ msgLnAdd MsgPromptGeneric prompt let yn = map K.mkChar ['y', 'n'] slides <- reportToSlideshow yn km <- getConfirms dm (K.escKM : yn) slides return $! km == K.mkChar 'y' getConfirms :: MonadClientUI m => ColorMode -> [K.KM] -> Slideshow -> m K.KM getConfirms dm extraKeys slides = do ekm <- displayChoiceScreen "" dm False slides extraKeys return $! either id (error $ "" `showFailure` ekm) ekm -- | Display a, potentially, multi-screen menu and return the chosen -- key or menu slot (and save the index in the whole menu so that the cursor -- can again be placed at that spot next time menu is displayed). -- -- This function is one of only two sources of menus and so, -- effectively, UI modes. displayChoiceScreen :: forall m . MonadClientUI m => String -> ColorMode -> Bool -> Slideshow -> [K.KM] -> m KeyOrSlot displayChoiceScreen = do displayChoiceScreenWithRightPane (const $ return emptyOKX) False -- | Display a, potentially, multi-screen menu and return the chosen -- key or menu slot (and save the index in the whole menu so that the cursor -- can again be placed at that spot next time menu is displayed). -- Additionally, display something on the right half of the screen, -- depending on which menu item is currently highlighted -- -- This function is one of only two sources of menus and so, -- effectively, UI modes. displayChoiceScreenWithRightPane :: forall m . MonadClientUI m => (KeyOrSlot -> m OKX) -> Bool -> String -> ColorMode -> Bool -> Slideshow -> [K.KM] -> m KeyOrSlot displayChoiceScreenWithRightPane displayInRightPane highlightBullet menuName dm sfBlank frsX extraKeys = do kmkm <- displayChoiceScreenWithRightPaneKMKM displayInRightPane highlightBullet menuName dm sfBlank frsX extraKeys return $! case kmkm of Left (km, _) -> Left km Right slot -> Right slot -- | A specialized variant of 'displayChoiceScreenWithRightPane'. displayChoiceScreenWithDefItemKey :: MonadClientUI m => (Int -> MenuSlot -> m OKX) -> Slideshow -> [K.KM] -> String -> m KeyOrSlot displayChoiceScreenWithDefItemKey f sli itemKeys menuName = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui FontSetup{propFont} <- getFontSetup let g ekm = case ekm of Left{} -> return emptyOKX Right slot -> do if isSquareFont propFont then return emptyOKX else f (rwidth - 2) slot displayChoiceScreenWithRightPane g True menuName ColorFull False sli itemKeys -- | A variant providing for a keypress the information about the label -- of the menu slot which was selected during the keypress. displayChoiceScreenWithRightPaneKMKM :: forall m . MonadClientUI m => (KeyOrSlot -> m OKX) -> Bool -> String -> ColorMode -> Bool -> Slideshow -> [K.KM] -> m (Either (K.KM, KeyOrSlot) MenuSlot) displayChoiceScreenWithRightPaneKMKM displayInRightPane highlightBullet menuName dm sfBlank frsX extraKeys = do (maxIx, initIx, clearIx, m) <- stepChoiceScreen highlightBullet dm sfBlank frsX extraKeys let loop :: Int -> KeyOrSlot -> m (Either (K.KM, KeyOrSlot) MenuSlot, Int) loop pointer km = do okxRight <- displayInRightPane km (final, kmkm1, pointer1) <- m pointer okxRight if final then return (kmkm1, pointer1) else loop pointer1 $ case kmkm1 of Left (km1, _) -> Left km1 Right slot -> Right slot pointer0 <- getMenuIx menuName maxIx initIx clearIx let km0 = case findKYX pointer0 $ slideshow frsX of Nothing -> error $ "no menu keys" `showFailure` frsX Just (_, (ekm, _), _) -> ekm (km, pointer) <- loop pointer0 km0 saveMenuIx menuName initIx pointer return km getMenuIx :: MonadClientUI m => String -> Int -> Int -> Int -> m Int getMenuIx menuName maxIx initIx clearIx = do menuIxMap <- getsSession smenuIxMap -- Beware, values in @menuIxMap@ may be negative (meaning: a key, not slot). let menuIx = if menuName == "" then clearIx else maybe clearIx (+ initIx) (M.lookup menuName menuIxMap) -- this may still be negative, from different context return $! max clearIx $ min maxIx menuIx -- so clamp to point at item, not key saveMenuIx :: MonadClientUI m => String -> Int -> Int -> m () saveMenuIx menuName initIx pointer = unless (menuName == "") $ modifySession $ \sess -> sess {smenuIxMap = M.insert menuName (pointer - initIx) $ smenuIxMap sess} -- | This is one step of UI menu management user session. -- -- There is limited looping involved to return a changed position -- in the menu each time so that the surrounding code has anything -- interesting to do. The exception is when finally confirming a selection, -- in which case it's usually not changed compared to last step, -- but it's presented differently to indicate it was confirmed. -- -- Any extra keys in the `OKX` argument on top of the those in @Slideshow@ -- argument need to be contained in the @[K.KM]@ argument. Otherwise -- they are not accepted. stepChoiceScreen :: forall m . MonadClientUI m => Bool -> ColorMode -> Bool -> Slideshow -> [K.KM] -> m ( Int, Int, Int , Int -> OKX -> m (Bool, Either (K.KM, KeyOrSlot) MenuSlot, Int) ) stepChoiceScreen highlightBullet dm sfBlank frsX extraKeys = do CCUI{coscreen=ScreenContent{rwidth, rheight}} <- getsSession sccui FontSetup{..} <- getFontSetup UIOptions{uVi, uLeftHand} <- getsSession sUIOptions let !_A = assert (K.escKM `elem` extraKeys) () frs = slideshow frsX keys = concatMap (lefts . map fst . snd) frs ++ extraKeys cardinalKeys = K.cardinalAllKM uVi uLeftHand handleDir = K.handleCardinal cardinalKeys legalKeys = keys ++ navigationKeys ++ cardinalKeys allOKX = concatMap snd frs maxIx = length allOKX - 1 initIx = case findIndex (isRight . fst) allOKX of Just p -> p _ -> 0 -- can't be @length allOKX@ or a multi-page item menu -- mangles saved index of other item munus clearIx = if initIx > maxIx then 0 else initIx canvasLength = if sfBlank then rheight else rheight - 2 trimmedY = canvasLength - 1 - 2 -- will be translated down 2 lines trimmedAlert = ( PointUI 0 trimmedY , stringToAL "--a portion of the text trimmed--" ) page :: Int -> OKX -> m (Bool, Either (K.KM, KeyOrSlot) MenuSlot, Int) page pointer (ovsRight0, kyxsRight) = assert (pointer >= 0) $ case findKYX pointer frs of Nothing -> error $ "no menu keys" `showFailure` frs Just ( (ovs0, kyxs2) , (ekm, (PointUI x1 y, buttonWidth)) , ixOnPage ) -> do let ovs1 = EM.map (updateLine y $ drawHighlight x1 buttonWidth) ovs0 ovs2 = if highlightBullet then EM.map (highBullet kyxs2) ovs1 else ovs1 -- We add spaces in proportional font under the report rendered -- in mono font and the right pane text in prop font, -- but over menu lines in proportional font that can be -- very long an should not peek from under the right pane text. -- -- We translate the pane by two characters right, because it looks -- better when a couple last characters of a line vanish -- off-screen than when characters touch in the middle -- of the screen. The code producing right panes should take care -- to generate lines two shorter than usually. -- -- We move the pane two characters down, because normally -- reports should not be longer than three lines -- and the third no longer than half width. -- We also add two to three lines of backdrop at the bottom. ymax = maxYofFontOverlayMap ovsRight0 -- Apparently prop spaces can be really narrow, hence so many. -- With square font, this obscures the link in main menu, -- so would need to complicated. spaceRectangle | isSquareFont propFont = [] | otherwise = rectangleOfSpaces (rwidth * 4) (min canvasLength $ ymax + 5) trim = filter (\(PointUI _ yRight, _) -> yRight < trimmedY) -- The alert not clickable, because the player can enter -- the menu entry and scroll through the unabridged blurb. ovsRight1 = if ymax <= trimmedY then ovsRight0 else EM.unionWith (++) (EM.map trim ovsRight0) (EM.singleton monoFont [trimmedAlert]) ovsRight = EM.unionWith (++) (EM.singleton propFont spaceRectangle) (EM.map (xytranslateOverlay 2 2) ovsRight1) (ovs, kyxs) = if EM.null ovsRight0 then (ovs2, kyxs2) else sideBySideOKX rwidth 0 (ovs2, kyxs2) (ovsRight, kyxsRight) kmkm ekm2 = case ekm2 of Left km -> Left (km, ekm2) Right slot -> Right slot tmpResult pointer1 = case findKYX pointer1 frs of Nothing -> error $ "no menu keys" `showFailure` frs Just (_, (ekm1, _), _) -> return (False, kmkm ekm1, pointer1) ignoreKey = return (False, kmkm ekm, pointer) pageLen = length kyxs xix :: KYX -> Bool xix (_, (PointUI x1' _, _)) = x1' <= x1 + 2 && x1' >= x1 - 2 firstRowOfNextPage = pointer + pageLen - ixOnPage restOKX = drop firstRowOfNextPage allOKX -- This does not take into account the right pane, which is fine. firstItemOfNextPage = case findIndex (isRight . fst) restOKX of Just p -> p + firstRowOfNextPage _ -> firstRowOfNextPage interpretKey :: K.KM -> m (Bool, Either (K.KM, KeyOrSlot) MenuSlot, Int) interpretKey ikm = case K.key ikm of _ | ikm == K.controlP -> do -- Silent, because any prompt would be shown too late. printScreen ignoreKey K.Return -> case ekm of Left km -> if K.key km == K.Return then return (True, Left (km, ekm), pointer) else interpretKey km Right c -> return (True, Right c, pointer) K.LeftButtonRelease -> do PointUI mx my <- getsSession spointer let onChoice (_, (PointUI cx cy, ButtonWidth font clen)) = let blen | isSquareFont font = 2 * clen | otherwise = clen in my == cy && mx >= cx && mx < cx + blen case find onChoice kyxs of Nothing | ikm `elem` keys -> return (True, Left (ikm, ekm), pointer) Nothing -> if K.spaceKM `elem` keys then return (True, Left (K.spaceKM, ekm), pointer) else ignoreKey Just (ckm, _) -> case ckm of Left km -> if K.key km == K.Return && km `elem` keys then return (True, Left (km, ekm), pointer) else interpretKey km Right c -> return (True, Right c, pointer) K.RightButtonRelease -> if ikm `elem` keys then return (True, Left (ikm, ekm), pointer) else return (True, Left (K.escKM, ekm), pointer) K.Space | firstItemOfNextPage <= maxIx -> tmpResult firstItemOfNextPage K.Unknown "SAFE_SPACE" -> if firstItemOfNextPage <= maxIx then tmpResult firstItemOfNextPage else tmpResult clearIx _ | ikm `elem` keys -> return (True, Left (ikm, ekm), pointer) _ | K.key ikm == K.WheelNorth || handleDir ikm == Just (Vector 0 (-1)) -> case findIndex xix $ reverse $ take ixOnPage kyxs of Nothing -> if pointer == 0 then tmpResult maxIx else tmpResult (max 0 (pointer - 1)) Just ix -> tmpResult (max 0 (pointer - ix - 1)) _ | K.key ikm == K.WheelSouth || handleDir ikm == Just (Vector 0 1) -> case findIndex xix $ drop (ixOnPage + 1) kyxs of Nothing -> if pointer == maxIx then tmpResult 0 else tmpResult (min maxIx (pointer + 1)) Just ix -> tmpResult (pointer + ix + 1) _ | handleDir ikm == Just (Vector (-1) 0) -> case findKYX (max 0 (pointer - 1)) frs of Just (_, (_, (PointUI _ y2, _)), _) | y2 == y -> tmpResult (max 0 (pointer - 1)) _ -> ignoreKey _ | handleDir ikm == Just (Vector 1 0) -> case findKYX (min maxIx (pointer + 1)) frs of Just (_, (_, (PointUI _ y2, _)), _) | y2 == y -> tmpResult (min maxIx (pointer + 1)) _ -> ignoreKey K.Home -> tmpResult clearIx K.End -> tmpResult maxIx K.PgUp -> tmpResult (max 0 (pointer - ixOnPage - 1)) K.PgDn -> -- This doesn't scroll by screenful when header very long -- and menu non-empty, but that scenario is rare, so OK, -- arrow keys may be used instead. tmpResult (min maxIx firstItemOfNextPage) K.Space -> ignoreKey _ | K.key ikm `elem` [K.Char '?', K.Fun 1] -> do -- Clear macros and invoke the help macro. modifySession $ \sess -> sess { smacroFrame = emptyMacroFrame {keyPending = KeyMacro [K.mkKM "F1"]} , smacroStack = [] } return (True, Left (K.escKM, ekm), pointer) _ -> error $ "unknown key" `showFailure` ikm pkm <- promptGetKey dm ovs sfBlank legalKeys interpretKey pkm m pointer okxRight = if null frs then return (True, Left (K.escKM, Left K.escKM), pointer) else do (final, km, pointer1) <- page pointer okxRight let !_A1 = assert (either ((`elem` keys) . fst) (const True) km) () -- Pointer at a button included, hence greater than 0, not @clearIx@. let !_A2 = assert (0 <= pointer1 && pointer1 <= maxIx `blame` (pointer1, maxIx)) () return (final, km, pointer1) return (maxIx, initIx, clearIx, m) navigationKeys :: [K.KM] navigationKeys = [ K.leftButtonReleaseKM, K.rightButtonReleaseKM , K.returnKM, K.spaceKM, K.wheelNorthKM, K.wheelSouthKM , K.pgupKM, K.pgdnKM, K.homeKM, K.endKM, K.controlP , K.mkChar '?', K.mkKM "F1" ] -- | Find a position in a menu. -- The arguments go from first menu line and menu page to the last, -- in order. Their indexing is from 0. We select the nearest item -- with the index equal or less to the pointer. findKYX :: Int -> [OKX] -> Maybe (OKX, KYX, Int) findKYX _ [] = Nothing findKYX pointer (okx@(_, kyxs) : frs2) = case drop pointer kyxs of [] -> -- not enough menu items on this page case findKYX (pointer - length kyxs) frs2 of Nothing -> -- no more menu items in later pages case reverse kyxs of [] -> Nothing kyx : _ -> Just (okx, kyx, length kyxs - 1) res -> res kyx : _ -> Just (okx, kyx, pointer) drawHighlight :: Int -> ButtonWidth -> Int -> AttrString -> AttrString drawHighlight x1 (ButtonWidth font len) xstart as = let highableAttrs = [Color.defAttr, Color.defAttr {Color.fg = Color.BrBlack}] highAttr c | Color.acAttr c `notElem` highableAttrs || Color.acChar c == ' ' = c highAttr c = c {Color.acAttr = (Color.acAttr c) {Color.fg = Color.BrWhite}} cursorAttr c = c {Color.acAttr = (Color.acAttr c) {Color.bg = Color.HighlightNoneCursor}} noCursorAttr c = c {Color.acAttr = (Color.acAttr c) {Color.bg = Color.HighlightNone}} -- This also highlights dull white item symbols, but who cares. lenUI = if isSquareFont font then len * 2 else len x1MinusXStartChars = if isSquareFont font then (x1 - xstart) `div` 2 else x1 - xstart (as1, asRest) = splitAt x1MinusXStartChars as (as2, as3) = splitAt len asRest highW32 = Color.attrCharToW32 . highAttr . Color.attrCharFromW32 as2High = map highW32 as2 cursorW32 = Color.attrCharToW32 . cursorAttr . Color.attrCharFromW32 (nonAlpha, alpha) = break (Char.isAlphaNum . Color.charFromW32) as2High as2Cursor = case alpha of [] -> [] ch : chrest -> cursorW32 ch : chrest noCursorW32 = Color.attrCharToW32 . noCursorAttr . Color.attrCharFromW32 in if x1 + lenUI < xstart then as else as1 ++ map noCursorW32 nonAlpha ++ as2Cursor ++ as3 drawBullet :: Int -> ButtonWidth -> Int -> AttrString -> AttrString drawBullet x1 (ButtonWidth font len) xstart as0 = let diminishChar '-' = ' ' diminishChar '^' = '^' diminishChar '"' = '"' diminishChar _ = '·' highableAttr = Color.defAttr {Color.bg = Color.HighlightNoneCursor} highW32 ac32 = let ac = Color.attrCharFromW32 ac32 ch = diminishChar $ Color.acChar ac in if | Color.acAttr ac /= highableAttr -> ac32 | Color.acChar ac == ' ' -> error $ "drawBullet: HighlightNoneCursor space forbidden" `showFailure` (ac, map Color.charFromW32 as0) | ch == ' ' -> Color.spaceAttrW32 | otherwise -> Color.attrCharToW32 $ ac { Color.acAttr = Color.defAttr {Color.fg = Color.BrBlack} , Color.acChar = ch } lenUI = if isSquareFont font then len * 2 else len x1MinusXStartChars = if isSquareFont font then (x1 - xstart) `div` 2 else x1 - xstart (as1, asRest) = splitAt x1MinusXStartChars as0 (as2, as3) = splitAt len asRest highAs = \case toHighlight : rest -> highW32 toHighlight : rest [] -> [] in if x1 + lenUI < xstart then as0 else as1 ++ highAs as2 ++ as3 highBullet :: [KYX] -> Overlay -> Overlay highBullet kyxs ov0 = let f (_, (PointUI x1 y, buttonWidth)) = updateLine y $ drawBullet x1 buttonWidth in foldr f ov0 kyxs -- This is not our turn, so we can't obstruct screen with messages -- and message reformatting causes distraction, so there's no point -- trying to squeeze the report into the single available line, -- except when it's not our turn permanently, because AI runs UI. -- -- The only real drawback of this is that when resting for longer time -- I can't see the boring messages accumulate until a non-boring interrupts me. basicFrameWithoutReport :: MonadClientUI m => LevelId -> Maybe Bool -> m PreFrame3 basicFrameWithoutReport arena forceReport = do FontSetup{propFont} <- getFontSetup sbenchMessages <- getsClient $ sbenchMessages . soptions side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD truncRep <- if | sbenchMessages -> do slides <- reportToSlideshowKeepHalt False [] case slideshow slides of [] -> return EM.empty (ov, _) : _ -> do -- See @stepQueryUI@. This strips either "--end-" or "--more-". let ovProp = ov EM.! propFont return $! EM.singleton propFont $ if EM.size ov > 1 then ovProp else init ovProp | fromMaybe (gunderAI fact) forceReport -> do report <- getReportUI False let par1 = firstParagraph $ foldr (<+:>) [] $ renderReport True report return $! EM.fromList [(propFont, [(PointUI 0 0, par1)])] | otherwise -> return EM.empty drawOverlay ColorFull False truncRep arena -- | Push the frame depicting the current level to the frame queue. -- Only one line of the report is shown, as in animations, -- because it may not be our turn, so we can't clear the message -- to see what is underneath. pushFrame :: MonadClientUI m => Bool -> m () pushFrame delay = do -- The delay before reaction to keypress was too long in case of many -- projectiles flying and ending flight, so frames need to be skipped. keyPressed <- anyKeyPressed unless keyPressed $ do lidV <- viewedLevelUI frame <- basicFrameWithoutReport lidV Nothing -- Pad with delay before and after to let player see, e.g., door being -- opened a few ticks after it came into vision, the same turn. displayFrames lidV $ if delay then [Nothing, Just frame, Nothing] else [Just frame] pushReportFrame :: MonadClientUI m => m () pushReportFrame = do lidV <- viewedLevelUI frame <- basicFrameWithoutReport lidV (Just True) displayFrames lidV [Just frame] LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/UIOptions.hs0000644000000000000000000000263107346545000022723 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | UI client options specified in the config file. module Game.LambdaHack.Client.UI.UIOptions ( UIOptions(..) ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.DeepSeq import Data.Binary import GHC.Generics (Generic) import Game.LambdaHack.Client.UI.HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Common.ClientOptions (FullscreenMode) import Game.LambdaHack.Common.Misc import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs -- | Options that affect the UI of the client, specified in the config file. -- More documentation is in the default config file. data UIOptions = UIOptions { uCommands :: [(K.KM, CmdTriple)] , uHeroNames :: [(Int, (Text, Text))] , uVi :: Bool , uLeftHand :: Bool , uChosenFontset :: Text , uAllFontsScale :: Double , uFullscreenMode :: FullscreenMode , uhpWarningPercent :: Int , uMsgWrapColumn :: X , uHistoryMax :: Int , uMaxFps :: Double , uNoAnim :: Bool , uOverrideCmdline :: [String] , uFonts :: [(Text, FontDefinition)] , uFontsets :: [(Text, FontSet)] , uMessageColors :: [(String, Color.Color)] } deriving (Show, Generic) instance NFData UIOptions instance Binary UIOptions LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/UIOptionsParse.hs0000644000000000000000000001604307346545000023720 0ustar0000000000000000-- | UI client options. module Game.LambdaHack.Client.UI.UIOptionsParse ( mkUIOptions, applyUIOptions #ifdef EXPOSE_INTERNAL -- * Internal operations , configError, readError, parseConfig #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.DeepSeq import qualified Data.Ini as Ini import qualified Data.Ini.Reader as Ini import qualified Data.Ini.Types as Ini import qualified Data.Map.Strict as M import qualified Data.Text as T import Data.Version import System.FilePath import Text.ParserCombinators.ReadP (readP_to_S) import Text.Read import Game.LambdaHack.Client.UI.HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.File import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Save (compatibleVersion, delayPrint) import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Content.RuleKind configError :: String -> a configError err = error $ "Error when parsing configuration file. Please fix config.ui.ini or remove it altogether. The details:\n" ++ err readError :: Read a => String -> a readError s = either (configError . ("when reading:\n" ++ s `showFailure`)) id $ readEither s parseConfig :: Ini.Config -> UIOptions parseConfig cfg = let uCommands = let mkCommand (ident, keydef) = case stripPrefix "Cmd_" ident of Just _ -> let (key, def) = readError keydef in (K.mkKM key, def :: CmdTriple) Nothing -> configError $ "wrong macro id" `showFailure` ident section = Ini.allItems "additional_commands" cfg in map mkCommand section uHeroNames = let toNumber (ident, nameAndPronoun) = case stripPrefix "HeroName_" ident of Just n -> (readError n, readError nameAndPronoun) Nothing -> configError $ "wrong hero name id" `showFailure` ident section = Ini.allItems "hero_names" cfg in map toNumber section lookupFail :: forall b. String -> String -> b lookupFail optionName err = configError $ "config file access failed" `showFailure` (err, optionName, cfg) _getOptionMaybe :: forall a. Read a => String -> Maybe a _getOptionMaybe optionName = let ms = Ini.getOption "ui" optionName cfg in either (lookupFail optionName) id . readEither <$> ms getOption :: forall a. Read a => String -> a getOption optionName = let s = fromMaybe (lookupFail optionName "") $ Ini.getOption "ui" optionName cfg in either (lookupFail optionName) id $ readEither s uVi = getOption "movementViKeys_hjklyubn" uLeftHand = getOption "movementLeftHandKeys_axwdqezc" uChosenFontset = getOption "chosenFontset" uAllFontsScale = getOption "allFontsScale" uFullscreenMode = getOption "fullscreenMode" uhpWarningPercent = getOption "hpWarningPercent" uMsgWrapColumn = getOption "msgWrapColumn" uHistoryMax = getOption "historyMax" uMaxFps = max 1 $ getOption "maxFps" uNoAnim = getOption "noAnim" uOverrideCmdline = glueSeed $ words $ getOption "overrideCmdline" uFonts = let toFont (ident, fontString) = (T.pack ident, readError fontString) section = Ini.allItems "fonts" cfg in map toFont section uFontsets = let toFontSet (ident, fontSetString) = (T.pack ident, readError fontSetString) section = Ini.allItems "fontsets" cfg in map toFontSet section uMessageColors = map (second readError) $ Ini.allItems "message_colors" cfg in UIOptions{..} glueSeed :: [String] -> [String] glueSeed [] = [] glueSeed ("SMGen" : s1 : s2 : rest) = ("SMGen" ++ " " ++ s1 ++ " " ++ s2) : glueSeed rest glueSeed (s : rest) = s : glueSeed rest -- | Read and parse UI config file. mkUIOptions :: RuleContent -> ClientOptions -> IO UIOptions mkUIOptions corule clientOptions = do let benchmark = sbenchmark clientOptions cfgUIName = rcfgUIName corule (configText, cfgUIDefault) = rcfgUIDefault corule dataDir <- appDataDir let path bkp = dataDir bkp <> cfgUIName cfgUser <- if benchmark then return Ini.emptyConfig else do cpExists <- doesFileExist (path "") if not cpExists then return Ini.emptyConfig else do sUser <- readFile (path "") return $! either (configError . ("Ini.parse sUser" `showFailure`)) id $ Ini.parse sUser let cfgUI = M.unionWith M.union cfgUser cfgUIDefault -- user cfg preferred vExe1 = rexeVersion corule vExe2 = let optionName = "version" -- Lenient to parse, and reject, old config files: s = fromMaybe "" $ Ini.getOption "version" optionName cfgUser dummyVersion = makeVersion [] in case find ((== "") . snd) $ readP_to_S parseVersion s of Just (ver, "") -> ver _ -> dummyVersion if benchmark || compatibleVersion vExe1 vExe2 then do let conf = parseConfig cfgUI -- Catch syntax errors in complex expressions ASAP. return $! deepseq conf conf else do cpExists <- doesFileExist (path "") when cpExists $ do renameFile (path "") (path "bkp.") moveAside <- Save.bkpAllSaves corule clientOptions let msg = "Config file" <+> T.pack (path "") <+> "from an incompatible version '" <> T.pack (showVersion vExe2) <> "' detected while starting" <+> T.pack (showVersion vExe1) <+> "game." <+> if moveAside then "The config file and savefiles have been moved aside." else "The config file has been moved aside." delayPrint msg dataDirExists <- doesFileExist dataDir when dataDirExists $ -- may not exist, e.g., when testing tryWriteFile (path "") configText let confDefault = parseConfig cfgUIDefault return confDefault -- | Modify client options with UI options. applyUIOptions :: COps -> UIOptions -> ClientOptions -> ClientOptions applyUIOptions COps{corule} uioptions = (\opts -> opts {schosenFontset = schosenFontset opts `mplus` Just (uChosenFontset uioptions)}) . (\opts -> opts {sallFontsScale = sallFontsScale opts `mplus` Just (uAllFontsScale uioptions)}) . (\opts -> opts {sfullscreenMode = sfullscreenMode opts `mplus` Just (uFullscreenMode uioptions)}) . (\opts -> opts {smaxFps = smaxFps opts `mplus` Just (uMaxFps uioptions)}) . (\opts -> opts {snoAnim = snoAnim opts `mplus` Just (uNoAnim uioptions)}) . (\opts -> opts {stitle = stitle opts `mplus` Just (rtitle corule)}) . (\opts -> opts {sfonts = uFonts uioptions}) . (\opts -> opts {sfontsets = uFontsets uioptions}) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Watch.hs0000644000000000000000000000042707346545000022101 0ustar0000000000000000-- | Display atomic commands received by the client. module Game.LambdaHack.Client.UI.Watch ( watchRespUpdAtomicUI, watchRespSfxAtomicUI ) where import Prelude () import Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM import Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Watch/0000755000000000000000000000000007346545000021542 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Watch/WatchCommonM.hs0000644000000000000000000002344707346545000024444 0ustar0000000000000000-- | Common code for displaying atomic update and SFX commands. module Game.LambdaHack.Client.UI.Watch.WatchCommonM ( fadeOutOrIn, markDisplayNeeded, lookAtMove, stopAtMove , aidVerbMU, aidVerbDuplicateMU, itemVerbMUGeneral, itemVerbMU , itemVerbMUShort, itemAidVerbMU, mitemAidVerbMU, itemAidDistinctMU , manyItemsAidVerbMU #ifdef EXPOSE_INTERNAL -- * Internal operations #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.Animation import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.FrameM import Game.LambdaHack.Client.UI.HandleHelperM import Game.LambdaHack.Client.UI.ItemDescription import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Definition.Ability as Ability fadeOutOrIn :: MonadClientUI m => Bool -> m () fadeOutOrIn out = do arena <- getArenaUI CCUI{coscreen} <- getsSession sccui animMap <- rndToActionUI $ fadeout coscreen out 2 animFrs <- renderAnimFrames arena animMap (Just False) displayFrames arena (tail animFrs) -- no basic frame between fadeout and in markDisplayNeeded :: MonadClientUI m => LevelId -> m () markDisplayNeeded lid = do lidV <- viewedLevelUI when (lidV == lid) $ modifySession $ \sess -> sess {sdisplayNeeded = True} lookAtMove :: MonadClientUI m => ActorId -> m () lookAtMove aid = do mleader <- getsClient sleader body <- getsState $ getActorBody aid side <- getsClient sside aimMode <- getsSession saimMode when (not (bproj body) && bfid body == side && isNothing aimMode) $ do -- aiming does a more extensive look stashBlurb <- lookAtStash (bpos body) (blid body) (itemsBlurb, _) <- lookAtItems True (bpos body) (blid body) (Just aid) Nothing let msgClass = if Just aid == mleader then MsgAtFeetMajor else MsgAtFeetMinor blurb = stashBlurb <+> itemsBlurb unless (T.null blurb) $ msgAdd msgClass blurb stopAtMove :: MonadClientUI m => ActorId -> m () stopAtMove aid = do body <- getsState $ getActorBody aid side <- getsClient sside fact <- getsState $ (EM.! bfid body) . sfactionD adjBigAssocs <- getsState $ adjacentBigAssocs body adjProjAssocs <- getsState $ adjacentProjAssocs body if not (bproj body) && bfid body == side then do let foe (_, b2) = isFoe (bfid body) fact (bfid b2) adjFoes = filter foe $ adjBigAssocs ++ adjProjAssocs unless (null adjFoes) stopPlayBack else when (isFoe (bfid body) fact side) $ do let our (_, b2) = bfid b2 == side adjOur = filter our adjBigAssocs unless (null adjOur) stopPlayBack aidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> MU.Part -> m () aidVerbMU msgClass aid verb = do subject <- partActorLeader aid msgAdd msgClass $ makeSentence [MU.SubjectVerbSg subject verb] aidVerbDuplicateMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> MU.Part -> m Bool aidVerbDuplicateMU msgClass aid verb = do subject <- partActorLeader aid msgAddDuplicate msgClass (makeSentence [MU.SubjectVerbSg subject verb]) itemVerbMUGeneral :: MonadClientUI m => Bool -> ItemId -> ItemQuant -> MU.Part -> Container -> m Text itemVerbMUGeneral verbose iid kit@(k, _) verb c = assert (k > 0) $ do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui lid <- getsState $ lidFromC c localTime <- getsState $ getLocalTime lid itemFull <- getsState $ itemToFull iid side <- getsClient sside factionD <- getsState sfactionD let arItem = aspectRecordFull itemFull partItemWsChosen | verbose = partItemWs | otherwise = partItemWsShort subject = partItemWsChosen rwidth side factionD k localTime itemFull kit msg | k > 1 && not (IA.checkFlag Ability.Condition arItem) = makeSentence [MU.SubjectVerb MU.PlEtc MU.Yes subject verb] | otherwise = makeSentence [MU.SubjectVerbSg subject verb] return $! msg itemVerbMU :: (MonadClientUI m, MsgShared a) => a -> ItemId -> ItemQuant -> MU.Part -> Container -> m () itemVerbMU msgClass iid kit verb c = do msg <- itemVerbMUGeneral True iid kit verb c msgAdd msgClass msg itemVerbMUShort :: (MonadClientUI m, MsgShared a) => a -> ItemId -> ItemQuant -> MU.Part -> Container -> m () itemVerbMUShort msgClass iid kit verb c = do msg <- itemVerbMUGeneral False iid kit verb c msgAdd msgClass msg itemAidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> MU.Part -> ItemId -> Either Int Int -> m () itemAidVerbMU msgClass aid verb iid ek = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui body <- getsState $ getActorBody aid side <- getsClient sside factionD <- getsState sfactionD let lid = blid body fakeKit = quantSingle localTime <- getsState $ getLocalTime lid subject <- partActorLeader aid -- The item may no longer be in @c@, but it was. itemFull <- getsState $ itemToFull iid let object = case ek of Left n -> partItemWs rwidth side factionD n localTime itemFull fakeKit Right n -> let (name1, powers) = partItemShort rwidth side factionD localTime itemFull fakeKit in MU.Phrase ["the", MU.Car1Ws n name1, powers] msg = makeSentence [MU.SubjectVerbSg subject verb, object] msgAdd msgClass msg mitemAidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> MU.Part -> ItemId -> Maybe MU.Part -> m () mitemAidVerbMU msgClass aid verb iid msuffix = do itemD <- getsState sitemD case msuffix of Just suffix | iid `EM.member` itemD -> itemAidVerbMU msgClass aid (MU.Phrase [verb, suffix]) iid (Right 1) _ -> do #ifdef WITH_EXPENSIVE_ASSERTIONS side <- getsClient sside b <- getsState $ getActorBody aid bUI <- getsSession $ getActorUI aid -- It's not actually expensive, but it's particularly likely -- to fail with wild content, indicating server game rules logic -- needs to be fixed/extended. -- Observer from another faction may receive the effect information -- from the server, because the affected actor is visible, -- but the position of the item may be out of FOV. This is fine; -- the message is then shorter, because only the effect was seen, -- while the cause remains misterious. assert (isNothing msuffix -- item description not requested || bfid b /= side -- not from affected faction; only observing `blame` "item never seen by the affected actor" `swith` (aid, b, bUI, verb, iid, msuffix)) $ #endif aidVerbMU msgClass aid verb itemAidDistinctMU :: MonadClientUI m => MsgClassDistinct -> ActorId -> MU.Part -> MU.Part -> ItemId -> m () itemAidDistinctMU msgClass aid verbShow verbSave iid = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui body <- getsState $ getActorBody aid side <- getsClient sside factionD <- getsState sfactionD let lid = blid body fakeKit = quantSingle localTime <- getsState $ getLocalTime lid subject <- partActorLeader aid -- The item may no longer be in @c@, but it was. itemFull <- getsState $ itemToFull iid let object = let (name, powers) = partItem rwidth side factionD localTime itemFull fakeKit in MU.Phrase [name, powers] t1 = makeSentence [MU.SubjectVerbSg subject verbShow, object] t2 = makeSentence [MU.SubjectVerbSg subject verbSave, object] dotsIfShorter = if t1 == t2 then "" else ".." msgAddDistinct msgClass (t1 <> dotsIfShorter, t2) manyItemsAidVerbMU :: (MonadClientUI m, MsgShared a) => a -> ActorId -> MU.Part -> [(ItemId, ItemQuant)] -> (Int -> Either (Maybe Int) Int) -> m () manyItemsAidVerbMU msgClass aid verb sortedAssocs ekf = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui body <- getsState $ getActorBody aid side <- getsClient sside factionD <- getsState sfactionD let lid = blid body fakeKit = quantSingle localTime <- getsState $ getLocalTime lid subject <- partActorLeader aid -- The item may no longer be in @c@, but it was. itemToF <- getsState $ flip itemToFull let object (iid, (k, _)) = let itemFull = itemToF iid in case ekf k of Left (Just n) -> partItemWs rwidth side factionD n localTime itemFull fakeKit Left Nothing -> let (name, powers) = partItem rwidth side factionD localTime itemFull fakeKit in MU.Phrase [name, powers] Right n -> let (name1, powers) = partItemShort rwidth side factionD localTime itemFull fakeKit in MU.Phrase ["the", MU.Car1Ws n name1, powers] msg = makeSentence [ MU.SubjectVerbSg subject verb , MU.WWandW $ map object sortedAssocs] msgAdd msgClass msg LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Watch/WatchQuitM.hs0000644000000000000000000004254507346545000024136 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | Display all the initial (not including high scores) screens at game over. module Game.LambdaHack.Client.UI.Watch.WatchQuitM ( quitFactionUI #ifdef EXPOSE_INTERNAL -- * Internal operations , displayGameOverLoot, displayGameOverAnalytics, displayGameOverLore , viewFinalLore #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Map.Strict as M import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.HandleHelperM import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.Slideshow import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs quitFactionUI :: MonadClientUI m => FactionId -> Maybe Status -> Maybe (FactionAnalytics, GenerationAnalytics) -> m () quitFactionUI fid toSt manalytics = do side <- getsClient sside gameModeId <- getsState sgameModeId when (side == fid) $ case toSt of Just Status{stOutcome=Camping} -> modifySession $ \sess -> sess {scampings = ES.insert gameModeId $ scampings sess} Just Status{stOutcome=Restart} -> modifySession $ \sess -> sess {srestarts = ES.insert gameModeId $ srestarts sess} Just Status{stOutcome} | stOutcome `elem` victoryOutcomes -> do scurChal <- getsClient scurChal let sing = M.singleton scurChal 1 f = M.unionWith (+) g = EM.insertWith f gameModeId sing modifySession $ \sess -> sess {svictories = g $ svictories sess} _ -> return () ClientOptions{sexposeItems} <- getsClient soptions fact <- getsState $ (EM.! fid) . sfactionD let fidName = MU.Text $ gname fact person = if fhasGender $ gkind fact then MU.PlEtc else MU.Sg3rd horror = isHorrorFact fact camping = maybe True ((== Camping) . stOutcome) toSt when (fid == side && not camping) $ do tellGameClipPS resetGameStart gameMode <- getGameMode allNframes <- getsSession sallNframes let startingPart = case toSt of _ | horror -> Nothing -- Ignore summoned actors' factions. Just Status{stOutcome=stOutcome@Restart, stNewGame=Just gn} -> Just $ MU.Text $ nameOutcomeVerb stOutcome <+> "to restart in" <+> displayGroupName gn <+> "mode" -- when multiplayer: "order mission restart in" Just Status{stOutcome=Restart, stNewGame=Nothing} -> error $ "" `showFailure` (fid, toSt) Just Status{stOutcome} -> Just $ MU.Text $ nameOutcomeVerb stOutcome -- when multiplayer, for @Camping@: "order save and exit" Nothing -> Nothing middlePart = case toSt of _ | fid /= side -> Nothing Just Status{stOutcome} -> lookup stOutcome $ mendMsg gameMode Nothing -> Nothing partingPart = if fid /= side || allNframes == -1 then Nothing else endMessageOutcome . stOutcome <$> toSt case startingPart of Nothing -> return () Just sp -> let blurb = makeSentence [MU.SubjectVerb person MU.Yes fidName sp] in msgLnAdd MsgFinalOutcome blurb case (toSt, partingPart) of (Just status, Just pp) -> do noConfirmsGame <- isNoConfirmsGame go <- if noConfirmsGame then return False else displaySpaceEsc ColorFull "" -- short, just @startingPart@ recordHistory -- we are going to exit or restart, so record and clear, but only once (itemBag, total) <- getsState $ calculateTotal side when go $ do case middlePart of Nothing -> return () Just sp1 -> do factionD <- getsState sfactionD itemToF <- getsState $ flip itemToFull let getTrunkFull (aid, b) = (aid, itemToF $ btrunk b) ourTrunks <- getsState $ map getTrunkFull . fidActorNotProjGlobalAssocs side let smartFaction fact2 = fhasPointman (gkind fact2) canBeSmart = any (smartFaction . snd) canBeOurFaction = any (\(fid2, _) -> fid2 == side) smartEnemy trunkFull = let possible = possibleActorFactions [] (itemKind trunkFull) factionD in not (canBeOurFaction possible) && canBeSmart possible smartEnemiesOurs = filter (smartEnemy . snd) ourTrunks uniqueActor trunkFull = IA.checkFlag Ability.Unique $ aspectRecordFull trunkFull uniqueEnemiesOurs = filter (uniqueActor . snd) smartEnemiesOurs smartUniqueEnemyCaptured = not $ null uniqueEnemiesOurs smartEnemyCaptured = not $ null smartEnemiesOurs smartEnemySentence <- case uniqueEnemiesOurs ++ smartEnemiesOurs of [] -> return "" (enemyAid, _) : _ -> do bUI <- getsSession $ getActorUI enemyAid return $! makePhrase [MU.Capitalize (partActor bUI)] <> "?" let won = maybe False ((`elem` victoryOutcomes) . stOutcome) toSt lost = maybe False ((`elem` deafeatOutcomes) . stOutcome) toSt msgClass | won = MsgGoodMiscEvent | lost = MsgBadMiscEvent | otherwise = MsgNeutralEvent (sp2, escPrompt) = if | lost -> ("", "Accept the unacceptable?") | smartUniqueEnemyCaptured -> ( "\nOh, wait, who is this, towering behind your escaping crew?" <+> smartEnemySentence <+> "This changes everything. For everybody. Everywhere. Forever. Did you plan for this? Are you sure it was your idea?" , "What happens now?" ) | smartEnemyCaptured -> ( "\nOh, wait, who is this, hunched among your escaping crew?" <+> smartEnemySentence <+> "Suddenly, this makes your crazy story credible. Suddenly, the door of knowledge opens again." , "How will you play that move?" ) | otherwise -> ("", "Let's see what we've got here.") msgAdd msgClass sp1 msgAdd MsgFactionIntel sp2 void $ displaySpaceEsc ColorFull escPrompt case manalytics of Nothing -> return () Just (factionAn, generationAn) -> cycleLore [] [ displayGameOverLoot (itemBag, total) generationAn , displayGameOverLore SOrgan True generationAn , displayGameOverAnalytics factionAn generationAn , displayGameOverLore SCondition sexposeItems generationAn , displayGameOverLore SBlast True generationAn , displayGameOverLore SEmbed True generationAn ] go2 <- if noConfirmsGame then return False else do -- Show score for any UI client after any kind of game exit, -- even though it's saved only for human UI clients at game over -- (that is not a noConfirms or benchmark game). scoreSlides <- scoreToSlideshow total status km <- getConfirms ColorFull [K.spaceKM, K.escKM] scoreSlides return $! km == K.spaceKM let epilogue = do when camping $ msgAdd MsgPromptGeneric "Saving..." -- Don't leave frozen old prompts on the browser screen. pushReportFrame if go2 && not noConfirmsGame && not camping then do msgAdd MsgPromptGeneric $ pp <+> "(Press RET to have one last look at the arena of your struggle before it gets forgotten.)" slides <- reportToSlideshowKeepHalt True [K.returnKM, K.spaceKM, K.escKM] km <- getConfirms ColorFull [K.returnKM, K.spaceKM, K.escKM] slides if km == K.returnKM then do -- Enter aiming mode. At exit, game arena is wiped out. lidV <- viewedLevelUI let saimMode = Just $ AimMode lidV defaultDetailLevel modifySession $ \sess -> sess { sreqDelay = ReqDelayHandled , saimMode } else epilogue else do when (not noConfirmsGame || camping) $ do -- The last prompt stays onscreen during shutdown, etc. msgAdd MsgPromptGeneric pp epilogue _ -> when (isJust startingPart && (stOutcome <$> toSt) == Just Killed) $ do msgAdd MsgTutorialHint "When a whole faction gets eliminated, no new members of the party will ever appear and its stashed belongings may remain far off, unclaimed and undefended. While some adventures require elimination a faction (to be verified in the adventure description screen in the help menu), for others it's an optional task, if possible at all. Instead, finding an exit may be necessary to win. It's enough if one character finds and triggers the exit. Others automatically follow, duly hauling all common belongings. Similarly, if eliminating foes ends a challenge, it happens immediately, with no need to move party members anywhere." -- Needed not to overlook the competitor dying in raid scenario. displayMore ColorFull "This is grave news. What now?" displayGameOverLoot :: MonadClientUI m => (ItemBag, Int) -> GenerationAnalytics -> m K.KM displayGameOverLoot (heldBag, total) generationAn = do ClientOptions{sexposeItems} <- getsClient soptions COps{coitem} <- getsState scops -- We assume "gold grain", not "grain" with label "of gold": let currencyName = IK.iname $ okind coitem $ ouniqGroup coitem IK.S_CURRENCY generationItem = generationAn EM.! SItem itemBag = if sexposeItems then let generationBag = EM.map (\k -> (-k, [])) generationItem in heldBag `EM.union` generationBag else heldBag promptFun iid itemFull2 k = let worth = itemPrice 1 $ itemKind itemFull2 lootMsg = if worth == 0 then "" else let pile = if k <= 1 then "exemplar" else "hoard" in makeSentence $ ["this treasure", pile, "is worth"] ++ (if k > 1 then [ MU.Cardinal k, "times"] else []) ++ [MU.CarWs worth $ MU.Text currencyName] holdsMsg = let n = generationItem EM.! iid in if | max 0 k == 1 && n == 1 -> "You keep the only specimen extant:" | max 0 k == 0 && n == 1 -> "You don't have the only hypothesized specimen:" | max 0 k == 0 && n == 0 -> "No such specimen was recorded:" | otherwise -> makePhrase [ "You hold" , if k == n then "all pieces" else MU.CardinalAWs (max 0 k) "piece" , "out of" , MU.Car n , "scattered:" ] in lootMsg <+> holdsMsg dungeonTotal <- getsState sgold let promptGold = spoilsBlurb currencyName total dungeonTotal -- Total number of items is meaningless in the presence of so much junk. prompt = promptGold <+> (if sexposeItems then "Non-positive count means none held but this many generated." else "") viewFinalLore "GameOverLoot" itemBag prompt promptFun (MLore SItem) displayGameOverAnalytics :: MonadClientUI m => FactionAnalytics -> GenerationAnalytics -> m K.KM displayGameOverAnalytics factionAn generationAn = do ClientOptions{sexposeActors} <- getsClient soptions side <- getsClient sside ItemRoles itemRoles <- getsSession sroles let ourAn = akillCounts $ EM.findWithDefault emptyAnalytics side factionAn foesAn = EM.unionsWith (+) $ concatMap EM.elems $ mapMaybe (`EM.lookup` ourAn) [KillKineticMelee .. KillOtherPush] killedBagIncludingProjectiles = EM.map (, []) foesAn killedBag = EM.filterWithKey (\iid _ -> iid `ES.member` (itemRoles EM.! STrunk)) killedBagIncludingProjectiles generationTrunk = generationAn EM.! STrunk trunkBag = if sexposeActors then let generationBag = EM.map (\k -> (-k, [])) generationTrunk in killedBag `EM.union` generationBag else killedBag total = sum $ filter (> 0) $ map fst $ EM.elems trunkBag -- Not just "killed 1 out of 4", because it's sometimes "2 out of 1", -- if an enemy was revived. promptFun :: ItemId -> ItemFull-> Int -> Text promptFun iid _ k = let n = generationTrunk EM.! iid in makePhrase [ "You recall the adversary, which you killed on" , MU.CarWs (max 0 k) "occasion", "while reports mention" , MU.CarWs n "individual", "in total:" ] prompt = makeSentence ["your team vanquished", MU.CarWs total "adversary"] -- total reported would include our own, so not given <+> (if sexposeActors then "Non-positive count means none killed but this many reported." else "") viewFinalLore "GameOverAnalytics" trunkBag prompt promptFun (MLore STrunk) displayGameOverLore :: MonadClientUI m => SLore -> Bool -> GenerationAnalytics -> m K.KM displayGameOverLore slore exposeCount generationAn = do itemD <- getsState sitemD let -- In @sexposeItems@ mode this filtering passes all through -- thanks to @revealItems@. generationLore = EM.filterWithKey (\iid _ -> iid `EM.member` itemD) $ generationAn EM.! slore generationBag = EM.map (\k -> (if exposeCount then k else 1, [])) generationLore total = sum $ map fst $ EM.elems generationBag promptFun :: ItemId -> ItemFull-> Int -> Text promptFun _ _ k = makeSentence [ "this", MU.Text (ppSLore slore), "manifested during your quest" , MU.CarWs k "time" ] verb = if | slore `elem` [SCondition, SBlast] -> "experienced" | slore == SEmbed -> "ambled among" | otherwise -> "lived among" prompt = case total of 0 -> makeSentence [ "you didn't experience any" , MU.Ws $ MU.Text (headingSLore slore) , "this time" ] 1 -> makeSentence [ "you saw the following" , MU.Text (headingSLore slore) ] _ -> makeSentence [ "you", verb, "the following variety of" , MU.CarWs total $ MU.Text (headingSLore slore) ] viewFinalLore ("GameOverLore" ++ show slore) generationBag prompt promptFun (MLore slore) viewFinalLore :: forall m . MonadClientUI m => String -> ItemBag -> Text -> (ItemId -> ItemFull -> Int -> Text) -> ItemDialogMode -> m K.KM viewFinalLore menuName trunkBag prompt promptFun dmode = do CCUI{coscreen=ScreenContent{rheight}} <- getsSession sccui itemToF <- getsState $ flip itemToFull let iids = sortIids itemToF $ EM.assocs trunkBag viewAtSlot :: MenuSlot -> m K.KM viewAtSlot slot = do let renderOneItem = okxItemLoreMsg promptFun 0 dmode iids extraKeys = [] slotBound = length iids - 1 km <- displayOneMenuItem renderOneItem extraKeys slotBound slot case K.key km of K.Space -> viewFinalLore menuName trunkBag prompt promptFun dmode K.Esc -> return km _ -> error $ "" `showFailure` km msgAdd MsgPromptGeneric prompt let keys = [K.spaceKM, K.mkChar '<', K.mkChar '>', K.escKM] okx <- itemOverlay iids dmode sli <- overlayToSlideshow (rheight - 2) keys okx ekm <- displayChoiceScreenWithDefItemKey (okxItemLoreInline promptFun 0 dmode iids) sli keys menuName case ekm of Left km | km `elem` keys -> return km Left km -> error $ "" `showFailure` km Right slot -> viewAtSlot slot LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Watch/WatchSfxAtomicM.hs0000644000000000000000000014010407346545000025077 0ustar0000000000000000-- | Display atomic SFX commands received by the client. module Game.LambdaHack.Client.UI.Watch.WatchSfxAtomicM ( watchRespSfxAtomicUI #ifdef EXPOSE_INTERNAL -- * Internal operations , returnJustLeft, ppSfxMsg, strike #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Data.Int (Int64) import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Atomic import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Animation import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.EffectDescription import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.FrameM import Game.LambdaHack.Client.UI.HandleHelperM import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import Game.LambdaHack.Client.UI.ItemDescription import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Client.UI.Watch.WatchCommonM import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.CaveKind (cdesc) import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs -- | Display special effects (text, animation) sent to the client. -- Don't modify client state (except a few fields), but only client -- session (e.g., by displaying messages). This is enforced by types. watchRespSfxAtomicUI :: MonadClientUI m => SfxAtomic -> m () {-# INLINE watchRespSfxAtomicUI #-} watchRespSfxAtomicUI sfx = case sfx of SfxStrike source target iid -> strike False source target iid SfxRecoil source target iid -> do sourceSeen <- getsState $ EM.member source . sactorD if not sourceSeen then do tb <- getsState $ getActorBody target animate (blid tb) $ blockMiss (bpos tb, bpos tb) else do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui sb <- getsState $ getActorBody source tb <- getsState $ getActorBody source spart <- partActorLeader source tpart <- partActorLeader target side <- getsClient sside factionD <- getsState sfactionD localTime <- getsState $ getLocalTime (blid tb) itemFullWeapon <- getsState $ itemToFull iid let kitWeapon = quantSingle (weaponName, _) = partItemShort rwidth side factionD localTime itemFullWeapon kitWeapon weaponNameOwn = partItemShortWownW rwidth side factionD spart localTime itemFullWeapon kitWeapon verb = if bproj sb then "deflect" else "fend off" objects | iid == btrunk sb = ["the", spart] | iid `EM.member` borgan sb = ["the", weaponNameOwn] | otherwise = ["the", weaponName, "of", spart] msgAdd MsgActionMajor $ makeSentence $ MU.SubjectVerbSg tpart verb : objects animate (blid tb) $ blockMiss (bpos tb, bpos sb) SfxSteal source target iid -> strike True source target iid SfxRelease source target _ -> do spart <- partActorLeader source tpart <- partActorLeader target msgAdd MsgActionMajor $ makeSentence [MU.SubjectVerbSg spart "release", tpart] SfxProject aid iid -> itemAidVerbMU MsgActionMajor aid "fling" iid (Left 1) SfxReceive aid iid -> itemAidVerbMU MsgActionMajor aid "receive" iid (Left 1) SfxApply aid iid -> do CCUI{coscreen=ScreenContent{rapplyVerbMap}} <- getsSession sccui ItemFull{itemKind} <- getsState $ itemToFull iid let actionPart = maybe "trigger" MU.Text (EM.lookup (IK.isymbol itemKind) rapplyVerbMap) itemAidVerbMU MsgActionMajor aid actionPart iid (Left 1) SfxCheck aid iid -> itemAidVerbMU MsgActionMajor aid "recover" iid (Left 1) SfxTrigger _ _ _ fromTile -> do COps{cotile} <- getsState scops let subject = MU.Text $ TK.tname $ okind cotile fromTile verb = "shake" msg = makeSentence ["the", MU.SubjectVerbSg subject verb] msgAdd MsgNeutralEvent msg SfxShun aid _ _ _ -> aidVerbMU MsgActionMajor aid "shun it" SfxEffect fidSource aid iid effect hpDelta -> do -- In most messages below @iid@ is ignored, because it's too common, -- e.g., caused by some combat hits, or rather obvious, -- e.g., in case of embedded items, or would be counterintuitive, -- e.g., when actor is said to be intimidated by a particle, not explosion. CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui b <- getsState $ getActorBody aid bUI <- getsSession $ getActorUI aid side <- getsClient sside mleader <- getsClient sleader itemD <- getsState sitemD actorMaxSk <- getsState $ getActorMaxSkills aid let fid = bfid b isOurCharacter = fid == side && not (bproj b) isAlive = bhp b > 0 isOurAlive = isOurCharacter && isAlive isOurLeader = Just aid == mleader -- The message classes are close enough. It's melee or similar. feelLookHPBad bigAdj projAdj = do feelLook MsgBadMiscEvent MsgGoodMiscEvent bigAdj projAdj -- We can't know here if the hit was in melee, ranged or -- even triggering a harmful item. However, let's not talk -- about armor before the player has the most basic one. -- for melee. Most of the time the first hit in the game is, -- in fact, from melee, so that's a sensible default. -- -- Note that the @idamage@ is called piercing (or edged) damage, -- even though the distinction from impact damage is fleshed -- 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)." feelLookHPGood = feelLook MsgGoodMiscEvent MsgBadMiscEvent feelLookCalm bigAdj projAdj = when isAlive $ feelLook MsgEffectMinor MsgEffectMinor bigAdj projAdj -- Ignore @iid@, because it's usually obvious what item caused that -- and because the effects are not particularly disortienting. feelLook msgClassOur msgClassTheir bigAdj projAdj = let (verb, adjective) = if bproj b then ("get", projAdj) else ( if isOurCharacter then "feel" else "look" , if isAlive then bigAdj else projAdj ) -- dead body is an item, not a person msgClass = if | bproj b -> MsgEffectMinor | isOurCharacter -> msgClassOur | otherwise -> msgClassTheir in aidVerbMU msgClass aid $ MU.Text $ verb <+> adjective case effect of IK.Burn{} -> do feelLookHPBad "burned" "scorched" let ps = (bpos b, bpos b) animate (blid b) $ twirlSplash ps Color.BrRed Color.Brown IK.Explode{} -> return () -- lots of visual feedback IK.RefillHP p | p == 1 -> return () -- no spam from regeneration IK.RefillHP p | p == -1 -> return () -- no spam from poison IK.RefillHP{} | hpDelta > 0 -> do feelLookHPGood "healthier" "mended" let ps = (bpos b, bpos b) animate (blid b) $ twirlSplash ps Color.BrGreen Color.Green IK.RefillHP{} -> do feelLookHPBad "wounded" "broken" let ps = (bpos b, bpos b) animate (blid b) $ twirlSplash ps Color.BrRed Color.Red IK.RefillCalm{} | not isAlive -> return () IK.RefillCalm{} | bproj b -> return () IK.RefillCalm p | p == 1 -> return () -- no spam from regen items IK.RefillCalm p | p > 0 -> feelLookCalm "calmer" "stabilized" IK.RefillCalm _ -> feelLookCalm "agitated" "wobbly" IK.Dominate | not isAlive -> return () IK.Dominate -> do -- For subsequent messages use the proper name, never "you". let subject = partActor bUI if fid /= fidSource then do -- Before domination, possibly not seen if actor (yet) not ours. if bcalm b == 0 -- sometimes only a coincidence, but nm then aidVerbMU MsgEffectMedium aid "yield, under extreme pressure" else do let verb = if isOurAlive then "black out, dominated by foes" else "decide abruptly to switch allegiance" -- Faction is being switched, so item that caused domination -- and vanished may not be known to the new faction. msuffix = if iid == btrunk b || iid `EM.notMember` itemD then Nothing else Just $ if isOurAlive then "through" else "under the influence of" mitemAidVerbMU MsgEffectMedium aid verb iid msuffix fidNameRaw <- getsState $ gname . (EM.! fid) . sfactionD -- Avoid "controlled by Controlled foo". let fidName = T.unwords $ tail $ T.words fidNameRaw verb = "be no longer controlled by" msgLnAdd MsgEffectMajor $ makeSentence [MU.SubjectVerbSg subject verb, MU.Text fidName] when isOurAlive $ displayMoreKeep ColorFull "" -- Ln makes it short else do -- After domination, possibly not seen, if actor (already) not ours. fidSourceNameRaw <- getsState $ gname . (EM.! fidSource) . sfactionD -- Avoid "Controlled control". let fidSourceName = T.unwords $ tail $ T.words fidSourceNameRaw verb = "be now under" msgAdd MsgEffectMajor $ makeSentence [MU.SubjectVerbSg subject verb, MU.Text fidSourceName, "control"] IK.Impress | not isAlive -> return () IK.Impress -> aidVerbMU MsgEffectMinor aid "be awestruck" IK.PutToSleep | not isAlive -> return () IK.PutToSleep -> do let verb = "be put to sleep" msuffix = Just $ if fidSource == bfid b then "due to" else "by" mitemAidVerbMU MsgEffectMajor aid verb iid msuffix IK.Yell | not isAlive -> return () IK.Yell -> aidVerbMU MsgMiscellanous aid "start" IK.Summon grp p -> do let verbBase = if bproj b then "lure" else "summon" part = MU.Text $ displayGroupName grp object = if p == 1 -- works, because exact number sent, not dice then MU.AW part else MU.Ws part verb = MU.Phrase [verbBase, object] msuffix = Just "with" mitemAidVerbMU MsgEffectMajor aid verb iid msuffix IK.Ascend{} | not isAlive -> return () IK.Ascend up -> do COps{cocave} <- getsState scops aidVerbMU MsgEffectMajor aid $ MU.Text $ "find a way" <+> if up then "upstairs" else "downstairs" when isOurLeader $ do destinations <- getsState $ whereTo (blid b) (bpos b) up . sdungeon case destinations of (lid, _) : _ -> do -- only works until different levels possible lvl <- getLevel lid 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." [] -> return () -- spell fizzles; normally should not be sent IK.Escape{} | isOurCharacter -> do ours <- getsState $ fidActorNotProjGlobalAssocs side when (length ours > 1) $ do (_, total) <- getsState $ calculateTotal side if total == 0 then msgAdd MsgFactionIntel $ "The team joins" <+> makePhrase [partActor bUI] <> ", forms a perimeter and leaves triumphant." else msgAdd MsgItemCreation $ "The team joins" <+> makePhrase [partActor bUI] <> ", forms a perimeter, repacks its belongings and leaves triumphant." IK.Escape{} -> return () IK.Paralyze{} | not isAlive -> return () IK.Paralyze{} -> mitemAidVerbMU MsgEffectMedium aid "be paralyzed" iid (Just "with") IK.ParalyzeInWater{} | not isAlive -> return () IK.ParalyzeInWater{} -> aidVerbMU MsgEffectMinor aid "move with difficulty" IK.InsertMove{} | not isAlive -> return () IK.InsertMove d -> -- Usually self-inflicted of from embeds, so obvious, so no @iid@. if Dice.supDice d >= 10 then aidVerbMU MsgEffectMedium aid "act with extreme speed" else do let msgClass = if isOurCharacter then MsgEffectMedium else MsgEffectMinor aidVerbMU msgClass aid "move swiftly" IK.Teleport t | Dice.supDice t <= 9 -> do -- Actor may be sent away before noticing the item that did it. let msuffix = if iid `EM.notMember` itemD then Nothing else Just "due to" msgClass = if isOurCharacter then MsgEffectMedium else MsgEffectMinor mitemAidVerbMU msgClass aid "blink" iid msuffix IK.Teleport{} -> do -- Actor may be sent away before noticing the item that did it. let msuffix = if iid `EM.notMember` itemD then Nothing else Just "by the power of" mitemAidVerbMU MsgEffectMedium aid "teleport" iid msuffix IK.CreateItem{} -> return () IK.DestroyItem{} -> return () IK.ConsumeItems{} -> return () IK.DropItem _ _ COrgan _ -> return () IK.DropItem{} -> -- rare enough mitemAidVerbMU MsgEffectMedium aid "be stripped" iid (Just "with") IK.Recharge{} | not isAlive -> return () IK.Recharge{} -> aidVerbMU MsgEffectMedium aid "charge up" IK.Discharge{} | not isAlive -> return () IK.Discharge{} -> aidVerbMU MsgEffectMedium aid "lose charges" IK.PolyItem -> do subject <- partActorLeader aid let ppstore = MU.Text $ ppCStoreIn CGround msgAdd MsgEffectMedium $ makeSentence [ MU.SubjectVerbSg subject "repurpose", "what lies", ppstore , "to a common item of the current level" ] IK.RerollItem -> do subject <- partActorLeader aid let ppstore = MU.Text $ ppCStoreIn CGround msgAdd MsgEffectMedium $ makeSentence [ MU.SubjectVerbSg subject "reshape", "what lies", ppstore , "striving for the highest possible standards" ] IK.DupItem -> do subject <- partActorLeader aid let ppstore = MU.Text $ ppCStoreIn CGround msgAdd MsgEffectMedium $ makeSentence [MU.SubjectVerbSg subject "multiply", "what lies", ppstore] IK.Identify -> do subject <- partActorLeader aid pronoun <- partPronounLeader aid msgAdd MsgEffectMinor $ makeSentence [ MU.SubjectVerbSg subject "look at" , MU.WownW pronoun $ MU.Text "inventory" , "intensely" ] IK.Detect d _ -> do subject <- partActorLeader aid factionD <- getsState sfactionD localTime <- getsState $ getLocalTime $ blid b let verb = MU.Text $ detectToVerb d object = MU.Ws $ MU.Text $ detectToObject d (periodic, itemFull) <- if iid `EM.member` itemD then do itemFull <- getsState $ itemToFull iid let arItem = aspectRecordFull itemFull return (IA.checkFlag Ability.Periodic arItem, itemFull) else do #ifdef WITH_EXPENSIVE_ASSERTIONS -- It's not actually expensive, but it's particularly likely -- to fail with wild content, indicating server game rules logic -- needs to be fixed/extended: -- Observer from another faction may receive the effect information -- from the server, because the affected actor is visible, -- but the position of the item may be out of FOV. This is fine; -- the message is then shorter, because only the effect was seen, -- while the cause remains misterious. let !_A = if fid /= side -- not from affected faction; observing then () else error $ "item never seen by the affected actor" `showFailure` (aid, b, bUI, verb, iid, sfx) #endif return (False, undefined) let iidDesc = let (name1, powers) = partItemShort rwidth side factionD localTime itemFull quantSingle in makePhrase ["the", name1, powers] -- If item not periodic, most likely intentional, so don't spam. means = [MU.Text $ "(via" <+> iidDesc <> ")" | periodic] msgAdd MsgEffectMinor $ makeSentence $ [MU.SubjectVerbSg subject verb] ++ [object] ++ means -- Don't make it modal if all info remains after no longer seen. unless (fid /= side || d `elem` [IK.DetectHidden, IK.DetectExit]) $ displayMore ColorFull "" -- the sentence short IK.SendFlying{} | not isAlive -> return () IK.SendFlying{} -> aidVerbMU MsgEffectMedium aid "be sent flying" IK.PushActor{} | not isAlive -> return () IK.PushActor{} -> aidVerbMU MsgEffectMedium aid "be pushed" IK.PullActor{} | not isAlive -> return () IK.PullActor{} -> aidVerbMU MsgEffectMedium aid "be pulled" IK.ApplyPerfume -> msgAdd MsgEffectMinor "The fragrance quells all scents in the vicinity." IK.AtMostOneOf{} -> return () IK.OneOf{} -> return () IK.OnSmash{} -> error $ "" `showFailure` sfx IK.OnCombine{} -> error $ "" `showFailure` sfx IK.OnUser{} -> error $ "" `showFailure` sfx IK.NopEffect -> error $ "" `showFailure` sfx IK.AndEffect{} -> error $ "" `showFailure` sfx IK.OrEffect{} -> error $ "" `showFailure` sfx IK.SeqEffect{} -> error $ "" `showFailure` sfx IK.When{} -> error $ "" `showFailure` sfx IK.Unless{} -> error $ "" `showFailure` sfx IK.IfThenElse{} -> error $ "" `showFailure` sfx IK.VerbNoLonger{} | not isAlive -> return () IK.VerbNoLonger verb ending -> do let msgClass = if fid == side then MsgStatusStopUs else MsgStatusStopThem subject <- partActorLeader aid msgAdd msgClass $ makePhrase [MU.Capitalize $ MU.SubjectVerbSg subject $ MU.Text verb] <> ending IK.VerbMsg verb ending -> do subject <- partActorLeader aid msgAdd MsgEffectMedium $ makePhrase [MU.Capitalize $ MU.SubjectVerbSg subject $ MU.Text verb] <> ending IK.VerbMsgFail verb ending -> do subject <- partActorLeader aid msgAdd MsgActionWarning $ makePhrase [MU.Capitalize $ MU.SubjectVerbSg subject $ MU.Text verb] <> ending SfxItemApplied verbose iid c -> do if verbose then itemVerbMU MsgActionMinor iid (1, []) "have got activated" c else itemVerbMU MsgInnerWorkSpam iid (1, []) "have been triggered" c SfxMsgFid _ sfxMsg -> do mleader <- getsClient sleader case mleader of Just{} -> return () -- will flush messages when leader moves Nothing -> do lidV <- viewedLevelUI markDisplayNeeded lidV recordHistory mmsg <- ppSfxMsg sfxMsg case mmsg of Just (Left (msgClass, msg)) -> msgAdd msgClass msg Just (Right (msgClass, (t1, t2))) -> do let dotsIfShorter = if t1 == t2 then "" else ".." msgAddDistinct msgClass (t1 <> dotsIfShorter, t2) Nothing -> return () SfxRestart -> fadeOutOrIn True SfxCollideTile source pos -> do COps{cotile} <- getsState scops sb <- getsState $ getActorBody source lvl <- getLevel $ blid sb spart <- partActorLeader source let object = MU.AW $ MU.Text $ TK.tname $ okind cotile $ lvl `at` pos -- Neutral message, because minor damage and we don't say, which faction. msgAdd MsgNeutralEvent $! makeSentence [MU.SubjectVerbSg spart "collide", "painfully with", object] SfxTaunt voluntary aid -> do side <- getsClient sside b <- getsState $ getActorBody aid unless (bproj b && bfid b == side) $ do -- don't spam spart <- partActorLeader aid (_heardSubject, verb) <- displayTaunt voluntary rndToActionUI aid let msgClass = if voluntary && bfid b == side then MsgActionComplete -- give feedback after keypress else MsgMiscellanous msgAdd msgClass $! makeSentence [MU.SubjectVerbSg spart (MU.Text verb)] returnJustLeft :: MonadClientUI m => (MsgClassShowAndSave, Text) -> m (Maybe (Either (MsgClassShowAndSave, Text) (MsgClassDistinct, (Text, Text)))) returnJustLeft = return . Just . Left ppSfxMsg :: MonadClientUI m => SfxMsg -> m (Maybe (Either (MsgClassShowAndSave, Text) (MsgClassDistinct, (Text, Text)))) ppSfxMsg sfxMsg = case sfxMsg of SfxUnexpected reqFailure -> returnJustLeft ( MsgActionWarning , "Unexpected problem:" <+> showReqFailure reqFailure <> "." ) SfxExpected itemName reqFailure -> returnJustLeft ( MsgActionWarning , "The" <+> itemName <+> "is not triggered:" <+> showReqFailure reqFailure <> "." ) SfxExpectedEmbed iid lid reqFailure -> do iidSeen <- getsState $ EM.member iid . sitemD if iidSeen then do itemFull <- getsState $ itemToFull iid side <- getsClient sside factionD <- getsState sfactionD localTime <- getsState $ getLocalTime lid let (object1, object2) = partItemShortest maxBound side factionD localTime itemFull quantSingle name = makePhrase [object1, object2] returnJustLeft ( MsgActionWarning , "The" <+> "embedded" <+> name <+> "is not activated:" <+> showReqFailure reqFailure <> "." ) else return Nothing SfxFizzles iid c -> do msg <- itemVerbMUGeneral True iid (1, []) "do not work" c return $ Just $ Right (MsgStatusWarning, ("It didn't work.", msg)) SfxNothingHappens iid c -> do msg <- itemVerbMUGeneral True iid (1, []) "do nothing, predictably" c return $ Just $ Right (MsgStatusBenign, ("Nothing happens.", msg)) SfxNoItemsForTile toolsToAlterWith -> do revCmd <- revCmdMap let km = revCmd HumanCmd.AlterDir tItems = describeToolsAlternative toolsToAlterWith returnJustLeft ( MsgActionWarning , "To transform the terrain, prepare the following items on the ground or in equipment:" <+> tItems <+> "and use the '" <> T.pack (K.showKM km) <> "' terrain modification command." ) SfxVoidDetection d -> returnJustLeft ( MsgMiscellanous , makeSentence ["no new", MU.Text $ detectToObject d, "detected"] ) SfxUnimpressed aid -> do msbUI <- getsSession $ EM.lookup aid . sactorUI case msbUI of Nothing -> return Nothing Just sbUI -> do let subject = partActor sbUI verb = "be unimpressed" returnJustLeft ( MsgActionWarning , makeSentence [MU.SubjectVerbSg subject verb] ) SfxSummonLackCalm aid -> do msbUI <- getsSession $ EM.lookup aid . sactorUI case msbUI of Nothing -> return Nothing Just sbUI -> do let subject = partActor sbUI verb = "lack Calm to summon" returnJustLeft ( MsgActionWarning , makeSentence [MU.SubjectVerbSg subject verb] ) SfxSummonTooManyOwn aid -> do msbUI <- getsSession $ EM.lookup aid . sactorUI case msbUI of Nothing -> return Nothing Just sbUI -> do let subject = partActor sbUI verb = "can't keep track of their numerous friends, let alone summon any more" returnJustLeft (MsgActionWarning, makeSentence [subject, verb]) SfxSummonTooManyAll aid -> do msbUI <- getsSession $ EM.lookup aid . sactorUI case msbUI of Nothing -> return Nothing Just sbUI -> do let subject = partActor sbUI verb = "can't keep track of everybody around, let alone summon anyone else" returnJustLeft (MsgActionWarning, makeSentence [subject, verb]) SfxSummonFailure aid -> do msbUI <- getsSession $ EM.lookup aid . sactorUI case msbUI of Nothing -> return Nothing Just sbUI -> do let subject = partActor sbUI verb = "fail to summon anything" returnJustLeft ( MsgActionWarning , makeSentence [MU.SubjectVerbSg subject verb] ) SfxLevelNoMore -> returnJustLeft (MsgActionWarning, "No more levels in this direction.") SfxLevelPushed -> returnJustLeft (MsgActionWarning, "You notice somebody pushed to another level.") SfxBracedImmune aid -> do msbUI <- getsSession $ EM.lookup aid . sactorUI case msbUI of Nothing -> return Nothing Just sbUI -> do let subject = partActor sbUI verb = "be braced and so immune to translocation" returnJustLeft ( MsgMiscellanous , makeSentence [MU.SubjectVerbSg subject verb] ) -- too common SfxEscapeImpossible -> returnJustLeft ( MsgActionWarning , "Escaping outside is unthinkable for members of this faction." ) SfxStasisProtects -> returnJustLeft ( MsgMiscellanous -- too common , "Paralysis and speed surge require recovery time." ) SfxWaterParalysisResisted -> return Nothing -- don't spam SfxTransImpossible -> returnJustLeft (MsgActionWarning, "Translocation not possible.") SfxIdentifyNothing -> returnJustLeft (MsgActionWarning, "Nothing to identify.") SfxPurposeNothing -> returnJustLeft ( MsgActionWarning , "The purpose of repurpose cannot be availed without an item" <+> ppCStoreIn CGround <> "." ) SfxPurposeTooFew maxCount itemK -> returnJustLeft ( MsgActionWarning , "The purpose of repurpose is served by" <+> tshow maxCount <+> "pieces of this item, not by" <+> tshow itemK <> "." ) SfxPurposeUnique -> returnJustLeft (MsgActionWarning, "Unique items can't be repurposed.") SfxPurposeNotCommon -> returnJustLeft (MsgActionWarning, "Only ordinary common items can be repurposed.") SfxRerollNothing -> returnJustLeft ( MsgActionWarning , "The shape of reshape cannot be assumed without an item" <+> ppCStoreIn CGround <> "." ) SfxRerollNotRandom -> returnJustLeft (MsgActionWarning, "Only items of variable shape can be reshaped.") SfxDupNothing -> returnJustLeft ( MsgActionWarning , "Mutliplicity won't rise above zero without an item" <+> ppCStoreIn CGround <> "." ) SfxDupUnique -> returnJustLeft (MsgActionWarning, "Unique items can't be multiplied.") SfxDupValuable -> returnJustLeft (MsgActionWarning, "Valuable items can't be multiplied.") SfxColdFish -> returnJustLeft ( MsgMiscellanous -- repeatable , "Healing attempt from another faction is thwarted by your cold fish attitude." ) SfxReadyGoods -> returnJustLeft ( MsgMiscellanous -- repeatable , "Crafting is alien to you, accustomed to buying ready goods all your life." ) SfxTimerExtended aid iid cstore delta -> do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui aidSeen <- getsState $ EM.member aid . sactorD iidSeen <- getsState $ EM.member iid . sitemD if aidSeen && iidSeen then do b <- getsState $ getActorBody aid bUI <- getsSession $ getActorUI aid factionD <- getsState sfactionD localTime <- getsState $ getLocalTime (blid b) itemFull <- getsState $ itemToFull iid side <- getsClient sside bag <- getsState $ getBodyStoreBag b cstore let (name, powers) = partItem rwidth (bfid b) factionD localTime itemFull quantSingle total = case bag EM.! iid of (_, []) -> error $ "" `showFailure` (bag, iid, aid, cstore, delta) (_, t:_) -> deltaOfItemTimer localTime t -- only exceptionally not singleton list storeOwn <- ppContainerWownW partPronounLeader True (CActor aid cstore) -- Ideally we'd use a pronoun here, but the action (e.g., hit) -- that caused this extension can be invisible to some onlookers. -- So their narrative context needs to be taken into account. -- The upside is that the messages do not bind pronouns -- and so commute and so repetitions can be squashed. let cond = [ "condition" | IA.checkFlag Ability.Condition $ aspectRecordFull itemFull ] usShow = ["the", name, powers] ++ cond ++ storeOwn ++ ["will now last longer"] usSave = ["the", name, powers] ++ cond ++ storeOwn ++ ["will now last"] ++ [MU.Text $ timeDeltaInSecondsText delta <+> "longer"] ++ [MU.Text $ "(total:" <+> timeDeltaInSecondsText total <> ")"] -- Note that when enemy actor causes the extension to himself, -- the player is not notified at all. So the shorter blurb -- displayed on the screen is middle ground and full is in history. themShow = [partItemShortWownW rwidth side factionD (partActor bUI) localTime itemFull quantSingle] ++ cond ++ ["is extended"] themSave = [partItemShortWownW rwidth side factionD (partActor bUI) localTime itemFull quantSingle] ++ cond ++ ["is extended by"] ++ [MU.Text $ timeDeltaInSecondsText delta] ++ [MU.Text $ "(total:" <+> timeDeltaInSecondsText total <> ")"] (msgClass, parts1, parts2) = if bfid b == side then (MsgStatusLongerUs, usShow, usSave) else (MsgStatusLongThem, themShow, themSave) return $ Just $ Right (msgClass, (makeSentence parts1, makeSentence parts2)) else return Nothing SfxCollideActor source target -> do sourceSeen <- getsState $ EM.member source . sactorD targetSeen <- getsState $ EM.member target . sactorD if sourceSeen && targetSeen then do spart <- partActorLeader source tpart <- partActorLeader target -- Neutral message, because minor damage and we don't say, which faction. -- And the collision may even be intentional. returnJustLeft ( MsgSpecialEvent , makeSentence [MU.SubjectVerbSg spart "collide", "awkwardly with", tpart] ) else return Nothing SfxItemYield iid k lid -> do iidSeen <- getsState $ EM.member iid . sitemD if iidSeen then do let fakeKit = quantSingle fakeC = CFloor lid originPoint verb = MU.Text $ "yield" <+> makePhrase [MU.CardinalAWs k "item"] msg <- itemVerbMUGeneral False iid fakeKit verb fakeC returnJustLeft (MsgSpecialEvent, msg) -- differentiate wrt item creation else return Nothing strike :: MonadClientUI m => Bool -> ActorId -> ActorId -> ItemId -> m () strike catch source target iid = assert (source /= target) $ do sourceSeen <- getsState $ EM.member source . sactorD if not sourceSeen then do tb <- getsState $ getActorBody target animate (blid tb) $ blockMiss (bpos tb, bpos tb) else do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui tb <- getsState $ getActorBody target hurtMult <- getsState $ armorHurtBonus source target sb <- getsState $ getActorBody source sMaxSk <- getsState $ getActorMaxSkills source spart <- partActorLeader source tpart <- partActorLeader target spronoun <- partPronounLeader source tpronoun <- partPronounLeader target tbUI <- getsSession $ getActorUI target localTime <- getsState $ getLocalTime (blid tb) itemFullWeapon <- getsState $ itemToFull iid let kitWeapon = quantSingle side <- getsClient sside factionD <- getsState sfactionD tfact <- getsState $ (EM.! bfid tb) . sfactionD eqpOrgKit <- getsState $ kitAssocs target [CEqp, COrgan] orgKit <- getsState $ kitAssocs target [COrgan] let isCond (_, (itemFullArmor, _)) = IA.checkFlag Ability.Condition $ aspectRecordFull itemFullArmor -- We exclude genetic flaws, backstory items, etc., because they -- can't be easily taken off, so no point spamming the player. isOrdinaryCond ikit@(_, (itemFullArmor, _)) = not (IA.checkFlag Ability.MetaGame (aspectRecordFull itemFullArmor)) && isCond ikit relevantSkArmor = if bproj sb then Ability.SkArmorRanged else Ability.SkArmorMelee rateArmor (iidArmor, (itemFullArmor, (k, _))) = ( k * IA.getSkill relevantSkArmor (aspectRecordFull itemFullArmor) , ( iidArmor , itemFullArmor ) ) abs15 (v, _) = abs v >= 15 condArmor = filter abs15 $ map rateArmor $ filter isOrdinaryCond orgKit fstGt0 (v, _) = v > 0 wornArmor = filter fstGt0 $ map rateArmor $ filter (not . isCond) eqpOrgKit mblockArmor <- case wornArmor of [] -> return Nothing _ -> Just <$> rndToActionUI (frequency $ toFreq "msg armor" wornArmor) actorMaxSkills <- getsState sactorMaxSkills let (blockWithWhat, blockWithWeapon) = case mblockArmor of Just (iidArmor, itemFullArmor) | iidArmor /= btrunk tb -> let (object1, object2) = partItemShortest rwidth (bfid tb) factionD localTime itemFullArmor quantSingle name = MU.Phrase [object1, object2] in ( ["with", MU.WownW tpronoun name] , Dice.supDice (IK.idamage $ itemKind itemFullArmor) > 0 ) _ -> ([], False) verb = MU.Text $ IK.iverbHit $ itemKind itemFullWeapon partItemChoice = if iid `EM.member` borgan sb then partItemShortWownW rwidth side factionD spronoun localTime else partItemShortAW rwidth side factionD localTime weaponNameWith = if iid == btrunk sb then [] else ["with", partItemChoice itemFullWeapon kitWeapon] sleepy = if bwatch tb `elem` [WSleep, WWake] && tpart /= "you" && bhp tb > 0 then "the sleepy" else "" unBurn (IK.Burn d) = Just d unBurn _ = Nothing unRefillHP (IK.RefillHP n) | n < 0 = Just (-n) unRefillHP _ = Nothing kineticDmg = let dmg = Dice.supDice $ IK.idamage $ itemKind itemFullWeapon rawDeltaHP = into @Int64 sHurt * xM dmg `divUp` 100 in case btrajectory sb of Just (_, speed) | bproj sb -> - modifyDamageBySpeed rawDeltaHP speed _ -> - rawDeltaHP burnDmg = - (sum $ map Dice.supDice $ mapMaybe unBurn $ IK.ieffects $ itemKind itemFullWeapon) fillDmg = - (sum $ mapMaybe unRefillHP $ IK.ieffects $ itemKind itemFullWeapon) -- For variety, attack adverb is based on attacker's and weapon's -- damage potential as compared to victim's current HP. -- We are not taking into account victim's armor yet. sHurt = armorHurtCalculation (bproj sb) sMaxSk Ability.zeroSkills nonPiercingDmg = burnDmg + fillDmg sDamage = min 0 $ kineticDmg + xM nonPiercingDmg deadliness = 1000 * (- sDamage) `div` max 1 (bhp tb) strongly | deadliness >= 10000 = "artfully" | deadliness >= 5000 = "madly" | deadliness >= 2000 = "mercilessly" | deadliness >= 1000 = "murderously" -- one blow can wipe out all HP | deadliness >= 700 = "devastatingly" | deadliness >= 500 = "vehemently" | deadliness >= 400 = "forcefully" | deadliness >= 350 = "sturdily" | deadliness >= 300 = "accurately" | deadliness >= 20 = "" -- common, terse case, between 2% and 30% | deadliness >= 10 = "cautiously" | deadliness >= 5 = "guardedly" | deadliness >= 3 = "hesitantly" | deadliness >= 2 = "clumsily" | deadliness >= 1 = "haltingly" | otherwise = "feebly" -- Here we take into account armor, so we look at @hurtMult@, -- so we finally convey the full info about effectiveness of the strike. blockHowWell -- under some conditions, the message not shown at all | hurtMult > 90 = "incompetently" | hurtMult > 80 = "too late" | hurtMult > 70 = "too slowly" | hurtMult > 20 || nonPiercingDmg < 0 = if | deadliness >= 2000 -> "marginally" | deadliness >= 1000 -> "partially" | deadliness >= 100 -> "partly" -- common | deadliness >= 50 -> "to an extent" | deadliness >= 20 -> "to a large extent" | deadliness >= 5 -> "for the major part" | otherwise -> "for the most part" | hurtMult > 1 = if | actorWaits tb -> "doggedly" | deadliness >= 50 -> "easily" -- common | deadliness >= 20 -> "effortlessly" | deadliness >= 5 -> "nonchalantly" | otherwise -> "bemusedly" | otherwise = "almost completely" -- a fraction gets through, but if fast missile, can be deadly avertVerb = if actorWaits tb then "avert it" else "ward it off" blockPhrase = let (subjectBlock, verbBlock) = if | not $ bproj sb -> (tpronoun, if blockWithWeapon then "parry" else "block") | tpronoun == "it" || projectileHitsWeakly && tpronoun /= "you" -> -- Avoid ambiguity. (partActor tbUI, avertVerb) | otherwise -> (tpronoun, avertVerb) in MU.SubjectVerbSg subjectBlock verbBlock surprisinglyGoodDefense = deadliness >= 20 && hurtMult <= 70 surprisinglyBadDefense = deadliness < 20 && hurtMult > 70 yetButAnd | surprisinglyGoodDefense = ", but" | surprisinglyBadDefense = ", yet" | otherwise = " and" -- no surprises projectileHitsWeakly = bproj sb && deadliness < 20 msgArmor = if not projectileHitsWeakly -- ensures if attack msg terse, armor message -- mentions object, so we know who is hit && hurtMult > 90 -- at most minor armor relatively to skill of the hit && (null condArmor || deadliness < 100) || null blockWithWhat || kineticDmg >= -1000 -- -1/1000 HP then "" else yetButAnd <+> makePhrase ([blockPhrase, blockHowWell] ++ blockWithWhat) ps = (bpos tb, bpos sb) basicAnim | hurtMult > 70 = twirlSplash ps Color.BrRed Color.Red | hurtMult > 1 = if nonPiercingDmg >= 0 -- no extra anim then blockHit ps Color.BrRed Color.Red else blockMiss ps | otherwise = blockMiss ps targetIsFoe = bfid sb == side -- no big news if others hit our foes && isFoe (bfid tb) tfact side targetIsFriend = isFriend (bfid tb) tfact side -- warning if anybody hits our friends msgClassMelee = if targetIsFriend then MsgMeleeNormalUs else MsgMeleeOthers msgClassRanged = if targetIsFriend then MsgRangedNormalUs else MsgRangedOthers animateAlive lid anim = if bhp tb > 0 then animate lid anim else animate lid $ twirlSplashShort ps Color.BrRed Color.Red 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?" -- The messages about parrying and immediately afterwards dying -- sound goofy, but there is no easy way to prevent that. -- And it's consistent. -- If/when death blow instead sets HP to 1 and only the next below 1, -- we can check here for HP==1; also perhaps actors with HP 1 should -- not be able to block. if | catch -> do -- charge not needed when catching let msg = makeSentence [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." animate (blid tb) $ blockHit ps Color.BrGreen Color.Green | not (hasCharge localTime kitWeapon) -> do -- Can easily happen with a thrown discharged item. -- Much less plausible with a wielded weapon. -- Theoretically possible if the weapon not identified -- (then timeout is a mean estimate), but they usually should be, -- even in foes' possession. let msg = if bproj sb then makePhrase [MU.Capitalize $ MU.SubjectVerbSg spart "connect"] <> ", but it may be completely discharged." else makePhrase ([ MU.Capitalize $ MU.SubjectVerbSg spart "try" , "to" , verb , tpart ] ++ weaponNameWith) <> if null weaponNameWith then ", but there are no charges left." else ", but it may be not readied yet." msgAdd MsgSpecialEvent msg -- and no animation | bproj sb && bproj tb -> do -- server sends unless both are blasts -- Short message. msgAdd MsgSpecialEvent $ makeSentence [MU.SubjectVerbSg spart "intercept", tpart] -- Basic non-bloody animation regardless of stats. animateAlive (blid tb) $ blockHit ps Color.BrBlue Color.Blue | kineticDmg >= -1000 -- -1/1000 HP -- We ignore nested effects, because they are, in general, avoidable. && nonPiercingDmg >= 0 -> do let adverb | itemSuspect itemFullWeapon && bfid sb == side = "tentatively" -- we didn't identify the weapon before | bproj sb = "lightly" | otherwise = "delicately" msg = makeSentence $ [MU.SubjectVerbSg spart verb, tpart, adverb] ++ 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." animate (blid tb) $ subtleHit ps | bproj sb -> do -- more terse than melee, because sometimes very spammy let msgRangedPowerful | targetIsFoe = MsgRangedMightyWe | targetIsFriend = MsgRangedMightyUs | otherwise = msgClassRanged (attackParts, msgRanged) | projectileHitsWeakly = ( [MU.SubjectVerbSg spart "connect"] -- weak, so terse , msgClassRanged ) | deadliness >= 300 = ( [MU.SubjectVerbSg spart verb, tpart, "powerfully"] , if targetIsFriend || deadliness >= 700 then msgRangedPowerful else msgClassRanged ) | otherwise = ( [MU.SubjectVerbSg spart verb, tpart] -- strong, for a proj , msgClassRanged ) msgAdd msgRanged $ makePhrase [MU.Capitalize $ MU.Phrase attackParts] <> msgArmor <> "." tutorialHintBenignFoe animateAlive (blid tb) basicAnim | bproj tb -> do -- much less emotion and the victim not active. let attackParts = [MU.SubjectVerbSg spart verb, tpart] ++ weaponNameWith msgAdd MsgMeleeOthers $ makeSentence attackParts animateAlive (blid tb) basicAnim | otherwise -> do -- ordinary melee let msgMeleeInteresting | targetIsFoe = MsgMeleeComplexWe | targetIsFriend = MsgMeleeComplexUs | otherwise = msgClassMelee msgMeleePowerful | targetIsFoe = MsgMeleeMightyWe | targetIsFriend = MsgMeleeMightyUs | otherwise = msgClassMelee attackParts = [MU.SubjectVerbSg spart verb, sleepy, tpart, strongly] ++ weaponNameWith (tmpInfluenceBlurb, msgClassInfluence) = if null condArmor || T.null msgArmor then ("", msgClassMelee) else let (armor, (_, itemFullArmor)) = maximumBy (comparing $ abs . fst) condArmor (object1, object2) = partItemShortest rwidth (bfid tb) factionD localTime itemFullArmor quantSingle name = makePhrase [object1, object2] msgText = if hurtMult > 70 then (if armor <= -15 then ", due to being" else assert (armor >= 15) ", regardless of being") <+> name else (if armor >= 15 then ", thanks to being" else assert (armor <= -15) ", despite being") <+> name in (msgText, msgMeleeInteresting) msgClass = if targetIsFriend && deadliness >= 300 || deadliness >= 2000 then msgMeleePowerful else msgClassInfluence msgAdd msgClass $ makePhrase [MU.Capitalize $ MU.Phrase attackParts] <> msgArmor <> tmpInfluenceBlurb <> "." tutorialHintBenignFoe animateAlive (blid tb) basicAnim LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Client/UI/Watch/WatchUpdAtomicM.hs0000644000000000000000000015067307346545000025103 0ustar0000000000000000-- | Display atomic update commands received by the client. module Game.LambdaHack.Client.UI.Watch.WatchUpdAtomicM ( watchRespUpdAtomicUI #ifdef EXPOSE_INTERNAL -- * Internal operations , assignItemRole, Threat, createActorUI, destroyActorUI, spotItemBag , recordItemLid, moveActor, displaceActorUI, moveItemUI , discover, ppHearMsg, ppHearDistanceAdjective, ppHearDistanceAdverb #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent (threadDelay) import qualified Data.Char as Char import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Map.Strict as M import qualified Data.Text as T import GHC.Exts (inline) import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Atomic import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Animation import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.DrawM import Game.LambdaHack.Client.UI.Frame import Game.LambdaHack.Client.UI.FrameM import Game.LambdaHack.Client.UI.HandleHelperM import Game.LambdaHack.Client.UI.ItemDescription import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Msg import Game.LambdaHack.Client.UI.MsgM import Game.LambdaHack.Client.UI.SessionUI import Game.LambdaHack.Client.UI.SlideshowM import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Client.UI.Watch.WatchCommonM import Game.LambdaHack.Client.UI.Watch.WatchQuitM import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.CaveKind (cdesc) import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind import qualified Game.LambdaHack.Content.ModeKind as MK import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Content.TileKind as TK import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.Flavour -- | Visualize atomic updates sent to the client. This is done -- in the global state after the command is executed and after -- the client state is modified by the command. -- Doesn't modify client state (except a few fields), but only client -- session (e.g., by displaying messages). This is enforced by types. watchRespUpdAtomicUI :: MonadClientUI m => UpdAtomic -> m () {-# INLINE watchRespUpdAtomicUI #-} watchRespUpdAtomicUI cmd = case cmd of -- Create/destroy actors and items. UpdRegisterItems{} -> return () UpdCreateActor aid body _ -> createActorUI True aid body UpdDestroyActor aid body _ -> destroyActorUI True aid body UpdCreateItem verbose iid _ kit@(kAdd, _) c -> do recordItemLid iid c assignItemRole c iid when verbose $ case c of CActor aid store -> do b <- getsState $ getActorBody aid case store of _ | bproj b -> itemVerbMU MsgItemCreation iid kit "appear" c COrgan -> do localTime <- getsState $ getLocalTime (blid b) arItem <- getsState $ aspectRecordFromIid iid if | IA.checkFlag Ability.Blast arItem -> return () | IA.checkFlag Ability.Condition arItem -> do side <- getsClient sside discoBenefit <- getsClient sdiscoBenefit bag <- getsState $ getContainerBag c itemKind <- getsState $ getIidKind iid let more = case EM.lookup iid bag of Just (kTotal, _) | kTotal /= kAdd -> Just kTotal _ -> Nothing verbShow = MU.Text $ "become" <+> case kit of (1, _ : _) -> "somewhat" (1, []) | isNothing more -> "" _ | isNothing more -> "many-fold" _ -> "additionally" verbSave = MU.Text $ "become" <+> case kit of (1, t:_) -> -- only exceptionally not singleton list -- or even more than one copy total let total = deltaOfItemTimer localTime t in timeDeltaInSecondsText total (1, []) | isNothing more -> "" (k, _) -> -- usually the list empty; ignore anyway (if isJust more then "additionally" else "") <+> tshow k <> "-fold" <+> case more of Nothing -> "" Just kTotal -> "(total:" <+> tshow kTotal <> "-fold)" good = benInEqp (discoBenefit EM.! iid) msgClass = case lookup IK.S_ASLEEP $ IK.ifreq itemKind of Just n | n > 0 -> MsgStatusSleep _ -> if | bfid b /= side -> MsgStatusOthers | good -> MsgStatusGoodUs | otherwise -> MsgStatusBadUs -- This describes all such items already among organs, -- which is useful, because it shows "charging". itemAidDistinctMU msgClass aid verbShow verbSave iid 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." | otherwise -> do wown <- ppContainerWownW partActorLeader True c itemVerbMU MsgItemCreation iid kit (MU.Text $ makePhrase $ "grow" : wown) c _ -> do wown <- ppContainerWownW partActorLeader True c itemVerbMU MsgItemCreation iid kit (MU.Text $ makePhrase $ "appear" : wown) c CEmbed{} -> return () -- not visible so can't delay even if important CFloor lid _ -> do factionD <- getsState sfactionD itemVerbMU MsgItemCreation iid kit (MU.Text $ "appear" <+> ppContainer factionD c) c markDisplayNeeded lid CTrunk{} -> return () UpdDestroyItem verbose iid _ kit c -> when verbose $ case c of CActor aid _ -> do b <- getsState $ getActorBody aid if bproj b then itemVerbMUShort MsgItemRuination iid kit "break" c else do ownW <- ppContainerWownW partActorLeader False c let verb = MU.Text $ makePhrase $ "vanish from" : ownW itemVerbMUShort MsgItemRuination iid kit verb c CEmbed{} -> return () -- not visible so can't delay even if important CFloor lid _ -> do factionD <- getsState sfactionD itemVerbMUShort MsgItemRuination iid kit (MU.Text $ "break" <+> ppContainer factionD c) c markDisplayNeeded lid CTrunk{} -> return () UpdSpotActor aid body -> createActorUI False aid body UpdLoseActor aid body -> destroyActorUI False aid body UpdSpotItem verbose iid kit c -> spotItemBag verbose c $ EM.singleton iid kit UpdLoseItem True iid kit c@(CActor aid _) -> do b <- getsState $ getActorBody aid when (not (bproj b) && bhp b > 0) $ do -- don't spam ownW <- ppContainerWownW partActorLeader False c let verb = MU.Text $ makePhrase $ "be removed from" : ownW itemVerbMUShort MsgItemMovement iid kit verb c UpdLoseItem{} -> return () UpdSpotItemBag verbose c bag -> spotItemBag verbose c bag UpdLoseItemBag{} -> return () -- rarely interesting and can be very long -- Move actors and items. 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." 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." 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 -- Change actor attributes. UpdRefillHP _ 0 -> return () UpdRefillHP aid hpDelta -> do let coarseDelta = abs hpDelta `div` oneM tDelta = if coarseDelta == 0 then if hpDelta > 0 then "a little" else "a fraction of an HP" else tshow coarseDelta <+> "HP" b <- getsState $ getActorBody aid unless (bproj b) $ aidVerbMU MsgNumericReport aid $ MU.Text ((if hpDelta > 0 then "heal" else "lose") <+> tDelta) arena <- getArenaUI side <- getsClient sside if | bproj b && (EM.null (beqp b) || isNothing (btrajectory b)) -> return () -- ignore caught proj or one hitting a wall | bhp b <= 0 && hpDelta < 0 && (bfid b == side && not (bproj b) || arena == blid b) -> do let (firstFall, hurtExtra) = case (bfid b == side, bproj b) of (True, True) -> ("drop down", "tumble down") (True, False) -> ("fall down", "suffer woeful mutilation") (False, True) -> ("plummet", "crash") (False, False) -> ("collapse", "be reduced to a bloody pulp") verbDie = if alreadyDeadBefore then hurtExtra else firstFall -- Rarely, this is wrong, because 2 other actors hit the victim -- at exactly the same time. No big problem. Doubled "dies" -- messages appears instead of "dies; is mutilated". alreadyDeadBefore = bhp b - hpDelta <= 0 tfact <- getsState $ (EM.! bfid b) . sfactionD bUI <- getsSession $ getActorUI aid subjectRaw <- partActorLeader aid let subject = if alreadyDeadBefore || subjectRaw == "you" then subjectRaw else partActor bUI -- avoid "fallen" msgDie = makeSentence [MU.SubjectVerbSg subject verbDie] targetIsFoe = isFoe (bfid b) tfact side targetIsFriend = isFriend (bfid b) tfact side msgClass | bproj b = MsgDeathBoring | targetIsFoe = MsgDeathVictory | targetIsFriend = MsgDeathDeafeat | otherwise = MsgDeathBoring if | bproj b -> msgAdd msgClass msgDie | bfid b == side -> do msgLnAdd msgClass $ msgDie <+> "Alas!" displayMore ColorBW "" | otherwise -> msgLnAdd msgClass msgDie -- We show death anims only if not dead already before this refill. let deathAct = if bfid b == side then deathBody (bpos b) else shortDeathBody (bpos b) unless (bproj b || alreadyDeadBefore) $ animate (blid b) deathAct | otherwise -> do when (hpDelta >= bhp b && bhp b > 0) $ aidVerbMU MsgActionWarning aid "return from the brink of death" mleader <- getsClient sleader when (Just aid == mleader) $ do actorMaxSk <- getsState $ getActorMaxSkills aid -- Regenerating actors never stop gaining HP, so we need to stop -- reporting it after they reach full HP for the first time. -- Also, no spam for non-leaders. when (bhp b >= xM (Ability.getSk Ability.SkMaxHP actorMaxSk) && bhp b - hpDelta < xM (Ability.getSk Ability.SkMaxHP actorMaxSk)) $ msgAdd MsgSpecialEvent "You recover your health fully. Any further gains will be transient." 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." sUIOptions <- getsSession sUIOptions currentWarning <- getsState $ checkWarningHP sUIOptions aid (bhp b) when currentWarning $ do previousWarning <- getsState $ checkWarningHP sUIOptions aid (bhp b - hpDelta) unless previousWarning $ aidVerbMU MsgRiskOfDeath aid "be down to a dangerous health level" UpdRefillCalm _ 0 -> return () UpdRefillCalm aid calmDelta -> do side <- getsClient sside b <- getsState $ getActorBody aid when (bfid b == side && not (bproj b)) $ do if | calmDelta > 0 -> do -- regeneration or effect mleader <- getsClient sleader when (Just aid == mleader) $ do actorMaxSk <- getsState $ getActorMaxSkills aid 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." -- 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 -- shows Calm, so this only matters for macros, where speed is good. when (abs calmDelta > oneM) $ markDisplayNeeded (blid b) | calmDelta == minusM1 -> do fact <- getsState $ (EM.! side) . sfactionD s <- getState let closeFoe (!p, aid2) = -- mimics isHeardFoe let b2 = getActorBody aid2 s in inline chessDist p (bpos b) <= 3 && not (actorWaitsOrSleeps b2) -- uncommon && inline isFoe side fact (bfid b2) -- costly anyCloseFoes = any closeFoe $ EM.assocs $ lbig $ sdungeon s EM.! blid b unless anyCloseFoes $ do -- obvious where the feeling comes from duplicated <- aidVerbDuplicateMU MsgHeardNearby aid "hear something" unless duplicated stopPlayBack | otherwise -> -- low deltas from hits; displayed elsewhere return () when (calmDelta < 0) $ do sUIOptions <- getsSession sUIOptions currentWarning <- getsState $ checkWarningCalm sUIOptions aid (bcalm b) when currentWarning $ do previousWarning <- getsState $ checkWarningCalm sUIOptions aid (bcalm b - calmDelta) unless previousWarning $ -- This messages is not shown if impression happens after -- Calm is low enough. However, this is rare and HUD shows the red. aidVerbMU MsgRiskOfDeath aid "have grown agitated and impressed enough to be in danger of defecting" UpdTrajectory _ _ mt -> -- if projectile dies just after, force one frame when (isNothing mt) $ pushFrame False -- Change faction attributes. UpdQuitFaction fid _ toSt manalytics -> quitFactionUI fid toSt manalytics UpdSpotStashFaction verbose fid lid pos -> do side <- getsClient sside when verbose $ do if fid == side then msgLnAdd MsgFactionIntel "You set up the shared inventory stash of your team." else do fact <- getsState $ (EM.! fid) . sfactionD let fidName = MU.Text $ gname fact msgAdd MsgFactionIntel $ makeSentence [ "you have found the current" , MU.WownW fidName "hoard location" ] unless (fid == side) $ animate lid $ actorX pos UpdLoseStashFaction verbose fid lid pos -> do when verbose $ do side <- getsClient sside if fid == side then msgAdd MsgFactionIntel "You've lost access to your shared inventory stash!" else do fact <- getsState $ (EM.! fid) . sfactionD let fidName = MU.Text $ gname fact msgAdd MsgFactionIntel $ makeSentence [fidName, "no longer control their hoard"] animate lid $ vanish pos UpdLeadFaction fid (Just source) mtgt@(Just target) -> do mleader <- getsClient sleader when (mtgt /= mleader) $ do fact <- getsState $ (EM.! fid) . sfactionD lidV <- viewedLevelUI when (gunderAI fact) $ markDisplayNeeded lidV -- This faction can't run with multiple actors, so this is not -- a leader change while running, but rather server changing -- their leader, which the player should be alerted to. when (noRunWithMulti fact) stopPlayBack actorD <- getsState sactorD case EM.lookup source actorD of Just sb | bhp sb <= 0 -> assert (not $ bproj sb) $ do -- Regardless who the leader is, give proper names here, not 'you'. sbUI <- getsSession $ getActorUI source tbUI <- getsSession $ getActorUI target let subject = partActor tbUI object = partActor sbUI msgAdd MsgPointmanSwap $ makeSentence [ MU.SubjectVerbSg subject "take command" , "from", object ] _ -> return () lookAtMove target UpdLeadFaction _ Nothing mtgt@(Just target) -> do mleader <- getsClient sleader when (mtgt /= mleader) $ lookAtMove target UpdLeadFaction{} -> return () UpdDiplFaction fid1 fid2 _ toDipl -> do name1 <- getsState $ gname . (EM.! fid1) . sfactionD name2 <- getsState $ gname . (EM.! fid2) . sfactionD msgAdd MsgFactionIntel $ name1 <+> "and" <+> name2 <+> "are now" <+> tshowDiplomacy toDipl <> "." UpdDoctrineFaction{} -> return () UpdAutoFaction fid b -> do side <- getsClient sside lidV <- viewedLevelUI markDisplayNeeded lidV when (fid == side) $ do unless b $ -- Clear macros and invoke a special main menu entrance macro -- that sets @swasAutomated@, preparing for AI control at exit. modifySession $ \sess -> sess { smacroFrame = emptyMacroFrame {keyPending = KeyMacro [K.controlEscKM]} , smacroStack = [] } setFrontAutoYes b -- now can start/stop auto-accepting prompts UpdRecordKill{} -> return () -- Alter map. UpdAlterTile lid p fromTile toTile -> do COps{cotile} <- getsState scops markDisplayNeeded lid let feats = TK.tfeature $ okind cotile fromTile toAlter feat = case feat of TK.OpenTo tgroup -> Just tgroup TK.CloseTo tgroup -> Just tgroup TK.ChangeTo tgroup -> Just tgroup TK.OpenWith _ _ tgroup -> Just tgroup TK.CloseWith _ _ tgroup -> Just tgroup TK.ChangeWith _ _ tgroup -> Just tgroup _ -> Nothing groupsToAlterTo = mapMaybe toAlter feats freq = map fst $ filter (\(_, q) -> q > 0) $ TK.tfreq $ okind cotile toTile unexpected = null $ intersect freq groupsToAlterTo mactorAtPos <- getsState $ posToBig p lid mleader <- getsClient sleader when (unexpected || isJust mactorAtPos && mactorAtPos /= mleader) $ do -- Faction notices @fromTile@ can't be altered into @toTIle@, -- which is uncanny, so we produce a message. -- This happens when the player missed an earlier search of the tile -- performed by another faction. let subject = "" -- a hack, because we don't handle adverbs well verb = "turn into" msg = makeSentence $ [ "the", MU.Text $ TK.tname $ okind cotile fromTile , "at position", MU.Text $ tshow p ] ++ ["suddenly" | unexpected] -- adverb ++ [ MU.SubjectVerbSg subject verb , MU.AW $ MU.Text $ TK.tname $ okind cotile toTile ] msgAdd (if unexpected then MsgSpecialEvent else MsgNeutralEvent) msg UpdAlterExplorable lid _ -> markDisplayNeeded lid UpdAlterGold{} -> return () -- not displayed on HUD UpdSearchTile aid _p toTile -> do COps{cotile} <- getsState scops subject <- partActorLeader aid let fromTile = fromMaybe (error $ show toTile) $ Tile.hideAs cotile toTile subject2 = MU.Text $ TK.tname $ okind cotile fromTile object = MU.Text $ TK.tname $ okind cotile toTile let msg = makeSentence [ MU.SubjectVerbSg subject "reveal" , "that the" , MU.SubjectVerbSg subject2 "be" , 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." UpdHideTile{} -> return () UpdSpotTile{} -> return () UpdLoseTile{} -> return () UpdSpotEntry{} -> return () UpdLoseEntry{} -> return () UpdAlterSmell{} -> return () UpdSpotSmell{} -> return () UpdLoseSmell{} -> return () -- Assorted. UpdTimeItem{} -> return () UpdAgeGame{} -> do sdisplayNeeded <- getsSession sdisplayNeeded sturnDisplayed <- getsSession sturnDisplayed time <- getsState stime let clipN = time `timeFit` timeClip clipMod = clipN `mod` clipsInTurn turnPing = clipMod == 0 -- e.g., to see resting counter if | sdisplayNeeded -> pushFrame True -- adds delay, because it's not an extra animation-like frame, -- but showing some real information accumulated up to this point | turnPing && not sturnDisplayed -> pushFrame False | otherwise -> return () when turnPing $ modifySession $ \sess -> sess {sturnDisplayed = False} UpdUnAgeGame{} -> return () UpdDiscover c iid _ _ -> discover c iid UpdCover{} -> return () -- don't spam when doing undo UpdDiscoverKind{} -> return () -- don't spam when server tweaks stuff UpdCoverKind{} -> return () -- don't spam when doing undo UpdDiscoverAspect{} -> return () -- don't spam when server tweaks stuff UpdCoverAspect{} -> return () -- don't spam when doing undo UpdDiscoverServer{} -> error "server command leaked to client" UpdCoverServer{} -> error "server command leaked to client" UpdPerception{} -> return () UpdRestart fid _ _ _ _ srandom -> do cops@COps{cocave, comode, corule} <- getsState scops oldSess <- getSession snxtChal <- getsClient snxtChal noConfirmsGame <- isNoConfirmsGame let uiOptions = sUIOptions oldSess f !acc _p !i _a = i : acc modes = zip [0..] $ ofoldlGroup' comode CAMPAIGN_SCENARIO f [] g :: (Int, ContentId ModeKind) -> Int g (_, mode) = case EM.lookup mode (svictories oldSess) of Nothing -> 0 Just cm -> fromMaybe 0 (M.lookup snxtChal cm) (snxtScenario, _) = minimumBy (comparing g) modes nxtGameTutorial = MK.mtutorial $ snd $ nxtGameMode cops snxtScenario putSession $ (emptySessionUI uiOptions) { schanF = schanF oldSess , sccui = sccui oldSess , shistory = shistory oldSess , svictories = svictories oldSess , scampings = scampings oldSess , srestarts = srestarts oldSess , smarkVision = smarkVision oldSess , smarkSmell = smarkSmell oldSess , snxtScenario , scurTutorial = noConfirmsGame || snxtTutorial oldSess -- make sure a newbie interrupting a screensaver has ample help , snxtTutorial = nxtGameTutorial , soverrideTut = soverrideTut oldSess , sstart = sstart oldSess , sgstart = sgstart oldSess , sallTime = sallTime oldSess , snframes = snframes oldSess , sallNframes = sallNframes oldSess , srandomUI = srandom } when (sstart oldSess == 0) resetSessionStart when (lengthHistory (shistory oldSess) == 0) $ do -- Generate initial history. Only for UI clients. shistory <- defaultHistory modifySession $ \sess -> sess {shistory} let title = T.pack $ rtitle corule msgAdd MsgBookKeeping $ "Welcome to" <+> title <> "!" recordHistory -- to ensure EOL even at creation of history lid <- getArenaUI lvl <- getLevel lid gameMode <- getGameMode curChal <- getsClient scurChal fact <- getsState $ (EM.! fid) . sfactionD let loneMode = case ginitial fact of [] -> True [(_, 1, _)] -> True _ -> False msgAdd MsgBookKeeping "-------------------------------------------------" recordHistory msgAdd MsgPromptGeneric "A grand story starts right here! (Press '?' for mode description and help.)" if lengthHistory (shistory oldSess) > 1 then fadeOutOrIn False else pushReportFrame -- show anything ASAP msgAdd MsgActionWarning ("New game started in" <+> mname gameMode <+> "mode.") let desc = cdesc $ okind cocave $ lkind lvl unless (T.null desc) $ do msgLnAdd MsgBackdropFocus "You take in your surroundings." msgAdd MsgBackdropInfo desc -- We can fool the player only once (per scenario), but let's not do it -- in the same way each time. TODO: PCG blurb <- rndToActionUI $ oneOf [ "You think you saw movement." , "Something catches your peripherial vision." , "You think you felt a tremor under your feet." , "A whiff of chilly air passes around you." , "You notice a draft just when it dies down." , "The ground nearby is stained along some faint lines." , "Scarce black motes slowly settle on the ground." , "The ground in the immediate area is empty, as if just swiped." ] msgLnAdd MsgBadMiscEvent blurb -- being here is a bad turn of events when (cwolf curChal && not loneMode) $ msgAdd MsgActionWarning "Being a lone wolf, you begin without companions." setFrontAutoYes $ gunderAI fact -- Forget the furious keypresses when dying in the previous game. resetPressedKeys UpdRestartServer{} -> return () UpdResume fid _ -> do COps{cocave} <- getsState scops resetSessionStart fact <- getsState $ (EM.! fid) . sfactionD setFrontAutoYes $ gunderAI fact unless (gunderAI fact) $ do lid <- getArenaUI lvl <- getLevel lid gameMode <- getGameMode msgAdd MsgPromptGeneric "Welcome back! (Press '?' for mode description and help.)" pushReportFrame -- show anything ASAP msgAdd MsgActionAlert $ "Continuing" <+> mname gameMode <+> "mode." let desc = cdesc $ okind cocave $ lkind lvl unless (T.null desc) $ do msgLnAdd MsgPromptFocus "You remember your surroundings." msgAdd MsgPromptGeneric desc UpdResumeServer{} -> return () UpdKillExit{} -> do #ifdef USE_JSFILE -- Some browsers seem to trash Local Storage when page reloaded or closed -- or the browser closed, while they still internally finish the saving -- in the background, so wait 2s. If the exit is without a save, -- the wait is spurious, but it's not supposed to be common. -- TODO: replace the @liftIO@ with a @MonadClientUI@ delay function. liftIO $ threadDelay 2000000 #else liftIO $ threadDelay 200000 #endif -- The prompt is necessary to force frontend to show this before exiting. void $ displayMore ColorBW "Done." -- in case it follows "Saving..." side <- getsClient sside debugPossiblyPrintUI $ "Client" <+> tshow side <+> "closing frontend." frontendShutdown debugPossiblyPrintUI $ "Client" <+> tshow side <+> "closed frontend." UpdWriteSave -> msgAdd MsgInnerWorkSpam "Saving backup." UpdHearFid _ distance hearMsg -> do mleader <- getsClient sleader case mleader of Just{} -> return () -- will flush messages when leader moves Nothing -> do lidV <- viewedLevelUI markDisplayNeeded lidV recordHistory msg <- ppHearMsg distance hearMsg let msgClass = case distance of Nothing -> MsgHeardOutside Just 0 -> MsgHeardNearby Just _ -> MsgHeardFaraway 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." 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." _ -> return () UpdMuteMessages _ smuteMessages -> modifySession $ \sess -> sess {smuteMessages} assignItemRole :: MonadClientUI m => Container -> ItemId -> m () assignItemRole c iid = do arItem <- getsState $ aspectRecordFromIid iid let assignSingleRole lore = do ItemRoles itemRoles <- getsSession sroles let itemRole = itemRoles EM.! lore unless (iid `ES.member` itemRole) $ do let newRoles = ItemRoles $ EM.adjust (ES.insert iid) lore itemRoles modifySession $ \sess -> sess {sroles = newRoles} slore = IA.loreFromContainer arItem c assignSingleRole slore when (slore `elem` [SOrgan, STrunk, SCondition]) $ assignSingleRole SBody data Threat = ThreatNone | ThreatUnarmed | ThreatArmed | ThreatAnotherUnarmed | ThreatAnotherArmed deriving Eq createActorUI :: MonadClientUI m => Bool -> ActorId -> Actor -> m () createActorUI born aid body = do CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui side <- getsClient sside factionD <- getsState sfactionD let fact = factionD EM.! bfid body localTime <- getsState $ getLocalTime $ blid body itemFull@ItemFull{itemBase, itemKind} <- getsState $ itemToFull (btrunk body) actorUI <- getsSession sactorUI let arItem = aspectRecordFull itemFull unless (aid `EM.member` actorUI) $ do UIOptions{uHeroNames} <- getsSession sUIOptions let baseColor = flavourToColor $ jflavour itemBase basePronoun | not (bproj body) && IK.isymbol itemKind == '@' && fhasGender (gkind fact) = "he" | otherwise = "it" nameFromNumber fn k = if k == 0 then makePhrase [MU.Ws $ MU.Text fn, "Captain"] else fn <+> tshow k heroNamePronoun k = if gcolor fact /= Color.BrWhite then (nameFromNumber (fname $ gkind fact) k, "he") else fromMaybe (nameFromNumber (fname $ gkind fact) k, "he") $ lookup k uHeroNames (n, bsymbol) = if | bproj body -> (0, if IA.checkFlag Ability.Blast arItem then IK.isymbol itemKind else '*') | baseColor /= Color.BrWhite -> (0, IK.isymbol itemKind) | otherwise -> case bnumber body of Nothing -> error $ "numbered actor without server-assigned number" `showFailure` (aid, body) Just bn -> (bn, if 0 < bn && bn < 10 then Char.intToDigit bn else '@') (object1, object2) = partItemShortest rwidth (bfid body) factionD localTime itemFull quantSingle (bname, bpronoun) = if | bproj body -> let adj = case btrajectory body of Just (tra, _) | length tra < 5 -> "falling" _ -> "flying" in (makePhrase [adj, object1, object2], basePronoun) | baseColor /= Color.BrWhite -> (makePhrase [object1, object2], basePronoun) | otherwise -> heroNamePronoun n bcolor | bproj body = if IA.checkFlag Ability.Blast arItem then baseColor else Color.BrWhite | baseColor == Color.BrWhite = gcolor fact | otherwise = baseColor bUI = ActorUI{..} modifySession $ \sess -> sess {sactorUI = EM.insert aid bUI actorUI} mapM_ (\(iid, store) -> do let c = if not (bproj body) && iid == btrunk body then CTrunk (bfid body) (blid body) (bpos body) else CActor aid store assignItemRole c iid recordItemLid iid c) ((btrunk body, CEqp) -- store will be overwritten, unless projectile : filter ((/= btrunk body) . fst) (getCarriedIidCStore body)) if | bproj body -> do when (bfid body /= side) stopPlayBack pushFrame False -- make sure first (seen (again)) position displayed | bfid body == side -> do let upd = ES.insert aid modifySession $ \sess -> sess {sselected = upd $ sselected sess} unless (EM.null actorUI) $ do -- don't announce the very first party member 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 animate (blid body) $ actorX (bpos body) | otherwise -> do -- Don't spam if the actor was already visible -- (but, e.g., on a tile that is invisible this turn -- (in that case move is broken down to lose+spot) -- or on a distant tile, via teleport while the observer -- teleported, too). lastLost <- getsSession slastLost if ES.member aid lastLost then markDisplayNeeded (blid body) else do stopPlayBack let verb = if born then "appear suddenly" else "be spotted" threat <- if isFoe (bfid body) fact side then do -- Aim even if nobody can shoot at the enemy. -- Let's home in on him and then we can aim or melee. -- We set permit to False, because it's technically -- very hard to check aimability here, because we are -- in-between turns and, e.g., leader's move has not yet -- been taken into account. xhair <- getsSession sxhair case xhair of Just (TVector _) -> return () -- explicitly set; keep it _ -> modifySession $ \sess -> sess { sxhair = Just $ TEnemy aid , sitemSel = Nothing } -- reset flinging totally foes <- getsState $ foeRegularList side (blid body) itemsSize <- getsState $ guardItemSize body if length foes <= 1 then if itemsSize == 0 then do msgAdd MsgSpottedThreat "You are not alone!" return ThreatUnarmed else do msgAdd MsgSpottedThreat "Armed intrusion ahead!" return ThreatArmed else if itemsSize == 0 then return ThreatAnotherUnarmed else do msgAdd MsgSpottedThreat "Another threat, armed!" return ThreatAnotherArmed else return ThreatNone -- member of neutral faction aidVerbMU MsgSpottedActor aid verb friendAssocs <- getsState $ friendRegularAssocs side (blid body) 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)." 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)." _ | 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." 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." animate (blid body) $ actorX (bpos body) destroyActorUI :: MonadClientUI m => Bool -> ActorId -> Actor -> m () destroyActorUI destroy aid b = do trunk <- getsState $ getItemBody $ btrunk b let baseColor = flavourToColor $ jflavour trunk unless (baseColor == Color.BrWhite) $ -- keep setup for heroes, etc. modifySession $ \sess -> sess {sactorUI = EM.delete aid $ sactorUI sess} let dummyTarget = TPoint TKnown (blid b) (bpos b) affect tgt = case tgt of Just (TEnemy a) | a == aid -> Just $ if destroy then -- If *really* nothing more interesting, the actor will -- go to last known location to perhaps find other foes. dummyTarget else -- If enemy only hides (or we stepped behind obstacle) find him. TPoint (TEnemyPos a) (blid b) (bpos b) Just (TNonEnemy a) | a == aid -> Just dummyTarget _ -> tgt modifySession $ \sess -> sess {sxhair = affect $ sxhair sess} unless (bproj b || destroy) $ modifySession $ \sess -> sess {slastLost = ES.insert aid $ slastLost sess} side <- getsClient sside fact <- getsState $ (EM.! side) . sfactionD let gameOver = isJust $ gquit fact -- we are the UI faction, so we determine unless gameOver $ do when (bfid b == side && not (bproj b)) $ do stopPlayBack let upd = ES.delete aid modifySession $ \sess -> sess {sselected = upd $ sselected sess} when destroy $ do mleader <- getsClient sleader when (isJust mleader) -- This is especially handy when the dead actor was a leader -- on a different level than the new one: clearAimMode -- If pushed, animate spotting again, to draw attention to pushing. markDisplayNeeded (blid b) spotItemBag :: forall m. MonadClientUI m => Bool -> Container -> ItemBag -> m () spotItemBag verbose c bag = do -- This is due to a move, or similar, which will be displayed, -- so no extra @markDisplayNeeded@ needed here and in similar places. CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui side <- getsClient sside getKind <- getsState $ flip getIidKindId lid <- getsState $ lidFromC c localTime <- getsState $ getLocalTime lid factionD <- getsState sfactionD -- Queried just once, so many copies of a new item can be reported. OK. ItemRoles itemRoles <- getsSession sroles sxhairOld <- getsSession sxhair let resetXhair = case c of CFloor _ p -> case sxhairOld of Just TEnemy{} -> return () -- probably too important to overwrite Just (TPoint TEnemyPos{} _ _) -> return () Just (TPoint TStash{} _ _) -> return () Just (TVector _) -> return () -- explicitly set; keep it _ -> do -- Don't steal xhair if it's only an item on another level. -- For enemies, OTOH, capture xhair to alarm player. lidV <- viewedLevelUI when (lid == lidV) $ do bagFloor <- getsState $ getFloorBag lid p modifySession $ \sess -> sess { sxhair = Just $ TPoint (TItem bagFloor) lidV p , sitemSel = Nothing } -- reset flinging totally _ -> return () locatedWhere = ppContainer factionD c beLocated = MU.Text $ "be located" <+> if locatedWhere == ppContainer EM.empty c then "" -- boring else locatedWhere subjectMaybe :: (ItemId, ItemQuant) -> m (Maybe (Int, MU.Part, MU.Part)) subjectMaybe (iid, kit@(k, _)) = do recordItemLid iid c itemFull <- getsState $ itemToFull iid let arItem = aspectRecordFull itemFull slore = IA.loreFromContainer arItem c if iid `ES.member` (itemRoles EM.! slore) then return Nothing -- this item or another with the same @iid@ -- seen already (has a role assigned); old news else do -- never seen or would have a role assignItemRole c iid case c of CFloor{} -> do let subjectShort = partItemWsShortest rwidth side factionD k localTime itemFull kit subjectLong = partItemWsLong rwidth side factionD k localTime itemFull kit return $ Just (k, subjectShort, subjectLong) _ -> return Nothing -- @SortOn@ less efficient here, because function cheap. sortItems = sortOn (getKind . fst) sortedAssocs = sortItems $ EM.assocs bag subjectMaybes <- mapM subjectMaybe sortedAssocs let subjects = catMaybes subjectMaybes sendMsg plural = do let subjectShort = MU.WWandW $ map (\(_, part, _) -> part) subjects subjectLong = MU.WWandW $ map (\(_, _, part) -> part) subjects msg subject = if plural then makeSentence [MU.SubjectVerb MU.PlEtc MU.Yes subject beLocated] else makeSentence [MU.SubjectVerbSg subject beLocated] msgShort = msg subjectShort msgLong = msg subjectLong dotsIfShorter = if msgShort == msgLong then "" else ".." resetXhair msgAddDistinct MsgSpottedItem (msgShort <> dotsIfShorter, msgLong) case subjects of [] -> return () [(1, _, _)] -> sendMsg False _ -> sendMsg True when verbose $ case c of CActor aid store -> do let verb = MU.Text $ verbCStore store b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD mleader <- getsClient sleader if Just aid == mleader && not (gunderAI fact) then manyItemsAidVerbMU MsgItemMovement aid verb sortedAssocs Right else when (not (bproj b) && bhp b > 0) $ -- don't announce death drops manyItemsAidVerbMU MsgItemMovement aid verb sortedAssocs (Left . Just) _ -> return () recordItemLid :: MonadClientUI m => ItemId -> Container -> m () recordItemLid iid c = do mjlid <- getsSession $ EM.lookup iid . sitemUI when (isNothing mjlid) $ do lid <- getsState $ lidFromC c modifySession $ \sess -> sess {sitemUI = EM.insert iid lid $ sitemUI sess} moveActor :: MonadClientUI m => ActorId -> Point -> Point -> m () moveActor aid source target = do -- If source and target tile distant, assume it's a teleportation -- and display an animation. Note: jumps and pushes go through all -- intervening tiles, so won't be considered. Note: if source or target -- not seen, the (half of the) animation would be boring, just a delay, -- not really showing a transition, so we skip it (via 'breakUpdAtomic'). -- The message about teleportation is sometimes shown anyway, just as the X. body <- getsState $ getActorBody aid if adjacent source target then markDisplayNeeded (blid body) else do let ps = (source, target) animate (blid body) $ teleport ps lookAtMove aid stopAtMove aid displaceActorUI :: MonadClientUI m => ActorId -> ActorId -> m () displaceActorUI source target = do mleader <- getsClient sleader sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target spart <- partActorLeader source tpart <- partActorLeader target let msgClass = if mleader `elem` map Just [source, target] then MsgActionMajor -- to interrupt run after a displace; else MsgActionMinor -- configurable, animation is feedback msg = makeSentence [MU.SubjectVerbSg spart "displace", tpart] msgAdd msgClass msg lookAtMove source stopAtMove source when (bfid sb /= bfid tb) $ do lookAtMove target -- in case only this one is ours stopAtMove target side <- getsClient sside -- Ours involved, but definitely not requested by player via UI. when (side `elem` [bfid sb, bfid tb] && mleader /= Just source) stopPlayBack let ps = (bpos tb, bpos sb) animate (blid sb) $ swapPlaces ps -- @UpdMoveItem@ is relatively rare (except within the player's faction), -- but it ensures that even if only one of the stores is visible -- (e.g., stash floor is not or actor posision is not), some messages -- will be printed (via verbose @UpdLoseItem@). moveItemUI :: MonadClientUI m => ItemId -> Int -> ActorId -> CStore -> CStore -> m () moveItemUI iid k aid cstore1 cstore2 = do let verb = MU.Text $ verbCStore cstore2 b <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid b) . sfactionD mleader <- getsClient sleader ItemRoles itemRoles <- getsSession sroles if iid `ES.member` (itemRoles EM.! SItem) then -- So far organs can't be put into stash, so no need to call -- @assignItemRole@ to add or reassign lore category. if cstore1 == CGround && Just aid == mleader && not (gunderAI fact) then itemAidVerbMU MsgActionMajor aid verb iid (Right k) else when (not (bproj b) && bhp b > 0) $ -- don't announce death drops itemAidVerbMU MsgActionMajor aid verb iid (Left k) else error $ "" `showFailure` (iid, k, aid, cstore1, cstore2) -- The item may be used up already and so not present in the container, -- e.g., if the item destroyed itself. This is OK. Message is still needed. discover :: MonadClientUI m => Container -> ItemId -> m () discover c iid = do COps{coitem} <- getsState scops CCUI{coscreen=ScreenContent{rwidth}} <- getsSession sccui lid <- getsState $ lidFromC c globalTime <- getsState stime localTime <- getsState $ getLocalTime lid itemFull <- getsState $ itemToFull iid bag <- getsState $ getContainerBag c side <- getsClient sside factionD <- getsState sfactionD (noMsg, nameWhere) <- case c of CActor aidOwner storeOwner -> do bOwner <- getsState $ getActorBody aidOwner name <- if bproj bOwner then return [] else ppContainerWownW partActorLeader True c let arItem = aspectRecordFull itemFull inMetaGame = IA.checkFlag Ability.MetaGame arItem isOurOrgan = bfid bOwner == side && storeOwner == COrgan && not inMetaGame -- assume own faction organs known intuitively, -- except backstories and other meta game items return (isOurOrgan, name) CTrunk _ _ p | p == originPoint -> return (True, []) -- the special reveal at game over, using fake @CTrunk@; don't spam _ -> return (False, []) let kit = EM.findWithDefault quantSingle iid bag -- may be used up by that time knownName = makePhrase [partItemMediumAW rwidth side factionD localTime itemFull kit] flav = flavourToName $ jflavour $ itemBase itemFull (object1, object2) = partItemShortest rwidth side factionD localTime itemFull kit name1 = makePhrase [object1, object2] -- Make sure the two names in the message differ. (ikObvious, itemKind) = case jkind $ itemBase itemFull of IdentityObvious ik -> (True, ik) IdentityCovered _ix ik -> (False, ik) -- fake kind (template); OK, we talk about appearances name2 = IK.iname $ okind coitem itemKind name = if ikObvious && T.unwords (tail (T.words knownName)) /= name1 then name1 -- avoid "a pair turns out to be" else name2 -- avoid "chip of scientific explanation" unknownName = MU.Phrase $ [MU.Text flav, MU.Text name] ++ nameWhere msg = makeSentence [ "the" , MU.SubjectVerbSg unknownName "turn out to be" , MU.Text knownName ] unless (noMsg || globalTime == timeZero) $ -- no spam about initial equipment msgAdd MsgItemDiscovery msg ppHearMsg :: MonadClientUI m => Maybe Int -> HearMsg -> m Text ppHearMsg distance hearMsg = case hearMsg of HearUpd cmd -> do COps{coTileSpeedup} <- getsState scops let sound = case cmd of UpdDestroyActor{} -> "shriek" UpdCreateItem{} -> "clatter" UpdTrajectory{} -> "thud" -- A non-blast projectle hits a tile. UpdAlterTile _ _ fromTile toTile -> if | Tile.isOpenable coTileSpeedup fromTile && Tile.isClosable coTileSpeedup toTile || Tile.isClosable coTileSpeedup fromTile && Tile.isOpenable coTileSpeedup toTile -> "creaking sound" | Tile.isWalkable coTileSpeedup fromTile && Tile.isWalkable coTileSpeedup toTile -> "splash" | otherwise -> "rumble" UpdAlterExplorable _ k -> if k > 0 then "grinding noise" else "fizzing noise" _ -> error $ "" `showFailure` cmd adjective = MU.Text $ ppHearDistanceAdjective distance msg = makeSentence ["you hear", MU.AW $ MU.Phrase [adjective, sound]] return $! msg HearStrike ik -> do COps{coitem} <- getsState scops let verb = IK.iverbHit $ okind coitem ik adverb = MU.Text $ ppHearDistanceAdverb distance msg = makeSentence [ "you", adverb, "hear something" , MU.Text verb, "someone" ] return $! msg HearSummon isProj grp p -> do let verb = if isProj then "something lure" else "somebody summon" part = MU.Text $ displayGroupName grp object = if p == 1 -- works, because exact number sent, not dice then MU.AW part else MU.Ws part adverb = MU.Text $ ppHearDistanceAdverb distance return $! makeSentence ["you", adverb, "hear", verb, object] HearCollideTile -> do let adverb = MU.Text $ ppHearDistanceAdverb distance return $! makeSentence ["you", adverb, "hear someone crash into something"] HearTaunt t -> do let adverb = MU.Text $ ppHearDistanceAdverb distance return $! makePhrase ["You", adverb, "overhear", MU.Text t] ppHearDistanceAdjective :: Maybe Int -> Text ppHearDistanceAdjective Nothing = "indistinct" ppHearDistanceAdjective (Just 0) = "very close" ppHearDistanceAdjective (Just 1) = "close" ppHearDistanceAdjective (Just 2) = "" ppHearDistanceAdjective (Just 3) = "remote" ppHearDistanceAdjective (Just 4) = "distant" ppHearDistanceAdjective (Just _) = "far-off" ppHearDistanceAdverb :: Maybe Int -> Text ppHearDistanceAdverb Nothing = "indistinctly" ppHearDistanceAdverb (Just 0) = "very clearly" ppHearDistanceAdverb (Just 1) = "clearly" ppHearDistanceAdverb (Just 2) = "" ppHearDistanceAdverb (Just 3) = "remotely" ppHearDistanceAdverb (Just 4) = "distantly" ppHearDistanceAdverb (Just _) = "barely" LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/0000755000000000000000000000000007346545000020171 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Actor.hs0000644000000000000000000002747707346545000021616 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, TupleSections #-} -- | Actors in the game: heroes, monsters, etc. module Game.LambdaHack.Common.Actor ( -- * The@ Acto@r type, its components and operations on them Actor(..), ResDelta(..), ActorMaxSkills, Watchfulness(..) , deltasSerious, deltasSeriousThisTurn , deltasHears, deltaBenign, deltaWasBenign , actorCanMelee, actorCanMeleeToHarm, actorWorthChasing, actorWorthKilling , gearSpeed, actorTemplate, actorWaits, actorWaitsOrSleeps, actorDying , hpTooLow, calmEnough, calmFull, hpFull, canSleep, prefersSleep , checkAdjacent, eqpOverfull, eqpFreeN , getCarriedIidsAndTrunk, getCarriedIidCStore -- * Assorted , ActorDict, monsterGenChance, smellTimeout ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import Data.Int (Int64) import GHC.Generics (Generic) import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | Actor attributes that are changing throughout the game. -- If they appear to be dublets of aspects from actor kinds, e.g. HP, -- they may be results of casting the dice specified in their respective -- actor kind and/or may be modified temporarily, but return -- to the original value from their respective kind over time. -- -- Other properties of an actor, in particular its current aspects, -- are derived from the actor's trunk, organs and equipment. -- A class of the aspects, the boolean ones, are called flags. -- Another class are skills. Stats are a subclass that determines -- if particular actions are permitted for the actor (or faction). data Actor = Actor { -- The trunk of the actor's body (present also in @borgan@ or @beqp@) btrunk :: ItemId -- ^ the trunk organ of the actor's body , bnumber :: Maybe Int -- ^ continued team character identity -- index number in this game -- Resources , bhp :: Int64 -- ^ current hit points * 1M , bhpDelta :: ResDelta -- ^ HP delta this turn * 1M , bcalm :: Int64 -- ^ current calm * 1M , bcalmDelta :: ResDelta -- ^ calm delta this turn * 1M -- Location , bpos :: Point -- ^ current position , boldpos :: Maybe Point -- ^ previous position, if any , blid :: LevelId -- ^ current level , bfid :: FactionId -- ^ faction the actor currently belongs to , btrajectory :: Maybe ([Vector], Speed) -- ^ trajectory the actor must -- travel and his travel speed -- Items , borgan :: ItemBag -- ^ organs , beqp :: ItemBag -- ^ personal equipment , bweapon :: Int -- ^ number of weapons among eqp and organs , bweapBenign :: Int -- ^ number of benign items among weapons -- Assorted , bwatch :: Watchfulness -- ^ state of the actor's watchfulness , bproj :: Bool -- ^ is a projectile? affects being able -- to fly through other projectiles, etc. } deriving (Show, Eq, Generic) instance Binary Actor -- | Representation of recent changes to HP of Calm of an actor. -- This is reset every time the actor perfoms an action, so this is -- aggregated over actor turn (move), not time turn. -- The resource changes recorded in the tuple are, respectively, -- negative and positive. data ResDelta = ResDelta { resCurrentTurn :: (Int64, Int64) -- ^ resource change this move , resPreviousTurn :: (Int64, Int64) -- ^ resource change previous move } deriving (Show, Eq, Generic) instance Binary ResDelta type ActorMaxSkills = EM.EnumMap ActorId Ability.Skills -- | All actors on the level, indexed by actor identifier. type ActorDict = EM.EnumMap ActorId Actor data Watchfulness = WWatch | WWait Int | WSleep | WWake deriving (Show, Eq, Generic) instance Binary Watchfulness deltasSerious :: ResDelta -> Bool deltasSerious ResDelta{..} = fst resCurrentTurn <= minusM2 || fst resPreviousTurn <= minusM2 deltasSeriousThisTurn :: ResDelta -> Bool deltasSeriousThisTurn ResDelta{..} = fst resCurrentTurn <= minusM2 deltasHears :: ResDelta -> Bool deltasHears ResDelta{..} = fst resCurrentTurn == minusM1 || fst resPreviousTurn == minusM1 deltaBenign :: ResDelta -> Bool deltaBenign ResDelta{resCurrentTurn} = fst resCurrentTurn >= 0 -- only the current one deltaWasBenign :: ResDelta -> Bool deltaWasBenign ResDelta{resPreviousTurn} = fst resPreviousTurn >= 0 -- only the previous one actorCanMelee :: ActorMaxSkills -> ActorId -> Actor -> Bool {-# INLINE actorCanMelee #-} actorCanMelee actorMaxSkills aid b = let actorMaxSk = actorMaxSkills EM.! aid condUsableWeapon = bweapon b > 0 canMelee = Ability.getSk Ability.SkMelee actorMaxSk > 0 in condUsableWeapon && canMelee actorCanMeleeToHarm :: ActorMaxSkills -> ActorId -> Actor -> Bool {-# INLINE actorCanMeleeToHarm #-} actorCanMeleeToHarm actorMaxSkills aid b = let actorMaxSk = actorMaxSkills EM.! aid condUsableWeapon = bweapon b - bweapBenign b > 0 canMelee = Ability.getSk Ability.SkMelee actorMaxSk > 0 in condUsableWeapon && canMelee -- Don't target/melee nonmoving actors, including sleeping, because nonmoving -- can't be lured nor ambushed nor can chase us. However, do target -- if they have loot or can attack at range or may become very powerful -- through regeneration if left alone. actorWorthChasing :: ActorMaxSkills -> ActorId -> Actor -> Bool actorWorthChasing actorMaxSkills aid b = let hasLoot = not (EM.null $ beqp b) -- even consider "unreported inventory", for speed and KISS actorMaxSk = actorMaxSkills EM.! aid in bproj b || (Ability.getSk Ability.SkMove actorMaxSk > 0 || bwatch b == WWake -- probably will start moving very soon || hasLoot || Ability.getSk Ability.SkProject actorMaxSk > 0 || bwatch b == WSleep && Ability.getSk Ability.SkMaxHP actorMaxSk > 30) -- too dangerous when regenerates through sleep; -- heroes usually fall into this category && bhp b > 0 -- Whether worth killing if already chased down. actorWorthKilling :: ActorMaxSkills -> ActorId -> Actor -> Bool actorWorthKilling actorMaxSkills aid b = actorWorthChasing actorMaxSkills aid b || actorCanMeleeToHarm actorMaxSkills aid b && bhp b > 0 -- | The speed from organs and gear; being pushed is ignored. gearSpeed :: Ability.Skills -> Speed gearSpeed actorMaxSk = toSpeed $ max minSpeed (Ability.getSk Ability.SkSpeed actorMaxSk) -- see @minimalSpeed@ actorTemplate :: ItemId -> Maybe Int -> Int64 -> Int64 -> Point -> LevelId -> FactionId -> Bool -> Actor actorTemplate btrunk bnumber bhp bcalm bpos blid bfid bproj = let btrajectory = Nothing boldpos = Nothing borgan = EM.empty beqp = EM.empty bweapon = 0 bweapBenign = 0 bwatch = WWatch -- overriden elsewhere, sometimes bhpDelta = ResDelta (0, 0) (0, 0) bcalmDelta = ResDelta (0, 0) (0, 0) in Actor{..} actorWaits :: Actor -> Bool {-# INLINE actorWaits #-} actorWaits b = case bwatch b of WWait{} -> True _ -> False actorWaitsOrSleeps :: Actor -> Bool {-# INLINE actorWaitsOrSleeps #-} actorWaitsOrSleeps b = case bwatch b of WWait{} -> True WSleep -> True _ -> False -- | Projectile that ran out of steam or collided with obstacle, dies. -- Even if it pierced through an obstacle, but lost its payload -- while altering the obstacle during piercing, it dies, too. actorDying :: Actor -> Bool actorDying b = bhp b <= 0 || bproj b && (maybe True (null . fst) (btrajectory b) || EM.null (beqp b)) hpTooLow :: Actor -> Ability.Skills -> Bool hpTooLow b actorMaxSk = 5 * bhp b < xM (Ability.getSk Ability.SkMaxHP actorMaxSk) && bhp b <= xM 40 || bhp b <= oneM -- | Check if actor calm enough to perform some actions. -- -- If max Calm is zero, always holds, to permit removing disastrous -- equipped items, which would otherwise be stuck forever. calmEnough :: Actor -> Ability.Skills -> Bool calmEnough b actorMaxSk = let calmMax = Ability.getSk Ability.SkMaxCalm actorMaxSk in 2 * xM calmMax <= 3 * bcalm b calmFull :: Actor -> Ability.Skills -> Bool calmFull b actorMaxSk = let calmMax = Ability.getSk Ability.SkMaxCalm actorMaxSk in xM calmMax <= bcalm b hpFull :: Actor -> Ability.Skills -> Bool hpFull b actorMaxSk = xM (Ability.getSk Ability.SkMaxHP actorMaxSk) <= bhp b -- | Has the skill and can wake up easily, so can sleep safely. canSleep :: Ability.Skills -> Bool canSleep actorMaxSk = Ability.getSk Ability.SkWait actorMaxSk >= 3 && (Ability.getSk Ability.SkSight actorMaxSk > 0 || Ability.getSk Ability.SkHearing actorMaxSk > 0) -- | Can't loot, not too aggresive, so sometimes prefers to sleep -- instead of exploring. prefersSleep :: Ability.Skills -> Bool prefersSleep actorMaxSk = Ability.getSk Ability.SkMoveItem actorMaxSk <= 0 && Ability.getSk Ability.SkAggression actorMaxSk < 2 checkAdjacent :: Actor -> Actor -> Bool checkAdjacent sb tb = blid sb == blid tb && adjacent (bpos sb) (bpos tb) eqpOverfull :: Actor -> Int -> Bool eqpOverfull b n = let size = sum $ map fst $ EM.elems $ beqp b in assert (size <= 10 `blame` (b, n, size)) $ size + n > 10 eqpFreeN :: Actor -> Int eqpFreeN b = let size = sum $ map fst $ EM.elems $ beqp b in assert (size <= 10 `blame` (b, size)) $ 10 - size getCarriedIidsAndTrunk :: Actor -> [ItemId] getCarriedIidsAndTrunk b = -- The trunk is important for a case of spotting a caught projectile -- with a stolen projecting item. This actually does happen. let trunk = EM.singleton (btrunk b) quantSingle in EM.keys $ EM.unionsWith const [beqp b, borgan b, trunk] getCarriedIidCStore :: Actor -> [(ItemId, CStore)] getCarriedIidCStore b = let bagCarried (cstore, bag) = map (,cstore) $ EM.keys bag in concatMap bagCarried [(CEqp, beqp b), (COrgan, borgan b)] -- | Chance, in parts per million, that a new monster is generated. -- Depends on the number of monsters already present, and on the level depth -- and its cave kind. -- -- Note that sometimes monsters spawn in groups, increasing danger, -- but many monsters are generated asleep, decreasing initial danger. monsterGenChance :: Dice.AbsDepth -> Dice.AbsDepth -> Int -> Int -> Int monsterGenChance (Dice.AbsDepth ldepth) (Dice.AbsDepth totalDepth) lvlSpawned actorCoeff = assert (totalDepth > 0 && ldepth > 0) $ -- ensured by content validation -- Heroes have to endure a level-depth-proportional wave of almost -- immediate spawners for each level. Then the monsters start -- to trickle more and more slowly, at the speed dictated -- by @actorCoeff@ specified in cave kind. Finally, spawning flattens out -- to ensure that camping is never safe. let scaledDepth = ldepth * 10 `div` totalDepth maxCoeff = 100 * 30 -- spawning on a level with benign @actorCoeff@ flattens out -- after 30+depth spawns and on a level with fast spawning -- flattens out later, but ending at the same level coeff = max 1 $ min maxCoeff $ actorCoeff * (lvlSpawned - scaledDepth - 2) million = 1000000 in 10 * million `div` coeff -- | How long until an actor's smell vanishes from a tile. smellTimeout :: Delta Time smellTimeout = timeDeltaScale (Delta timeTurn) 200 LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/ActorState.hs0000644000000000000000000005103207346545000022577 0ustar0000000000000000-- | Operations on the 'Actor' type, and related, that need the 'State' type, -- but not our custom monad types. module Game.LambdaHack.Common.ActorState ( fidActorNotProjGlobalAssocs, actorAssocs, fidActorRegularAssocs , fidActorRegularIds, foeRegularAssocs, foeRegularList , friendRegularAssocs, friendRegularList, bagAssocs, bagAssocsK , posToBig, posToBigAssoc, posToProjs, posToProjAssocs , posToAids, posToAidAssocs, calculateTotal, itemPrice, findIid, combinedItems , getActorBody, getActorMaxSkills, actorCurrentSkills, canTraverse , getCarriedAssocsAndTrunk, getContainerBag , getFloorBag, getEmbedBag, getBodyStoreBag, getFactionStashBag , mapActorItems_, getActorAssocs , memActor, getLocalTime, regenCalmDelta, actorInAmbient , dispEnemy, itemToFull, fullAssocs, kitAssocs , getItemKindId, getIidKindId, getItemKind, getIidKind , getItemKindIdServer, getIidKindIdServer, getItemKindServer, getIidKindServer , tileAlterable, lidFromC, posFromC, anyFoeAdj, anyHarmfulFoeAdj , adjacentBigAssocs, adjacentProjAssocs, armorHurtBonus, inMelee ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Int (Int64) import GHC.Exts (inline) import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs fidActorNotProjGlobalAssocs :: FactionId -> State -> [(ActorId, Actor)] fidActorNotProjGlobalAssocs fid s = let f (_, b) = not (bproj b) && bfid b == fid in filter f $ EM.assocs $ sactorD s actorAssocs :: (FactionId -> Bool) -> LevelId -> State -> [(ActorId, Actor)] actorAssocs p lid s = let f (_, b) = blid b == lid && p (bfid b) in filter f $ EM.assocs $ sactorD s actorRegularAssocs :: (FactionId -> Bool) -> LevelId -> State -> [(ActorId, Actor)] {-# INLINE actorRegularAssocs #-} actorRegularAssocs p lid s = let f (_, b) = not (bproj b) && blid b == lid && p (bfid b) && bhp b > 0 in filter f $ EM.assocs $ sactorD s fidActorRegularAssocs :: FactionId -> LevelId -> State -> [(ActorId, Actor)] fidActorRegularAssocs fid = actorRegularAssocs (== fid) fidActorRegularIds :: FactionId -> LevelId -> State -> [ActorId] fidActorRegularIds fid lid s = map fst $ actorRegularAssocs (== fid) lid s foeRegularAssocs :: FactionId -> LevelId -> State -> [(ActorId, Actor)] foeRegularAssocs fid lid s = let fact = (EM.! fid) . sfactionD $ s in actorRegularAssocs (inline isFoe fid fact) lid s foeRegularList :: FactionId -> LevelId -> State -> [Actor] foeRegularList fid lid s = let fact = (EM.! fid) . sfactionD $ s in map snd $ actorRegularAssocs (inline isFoe fid fact) lid s friendRegularAssocs :: FactionId -> LevelId -> State -> [(ActorId, Actor)] friendRegularAssocs fid lid s = let fact = (EM.! fid) . sfactionD $ s in actorRegularAssocs (inline isFriend fid fact) lid s friendRegularList :: FactionId -> LevelId -> State -> [Actor] friendRegularList fid lid s = let fact = (EM.! fid) . sfactionD $ s in map snd $ actorRegularAssocs (inline isFriend fid fact) lid s bagAssocs :: State -> ItemBag -> [(ItemId, Item)] bagAssocs s bag = let iidItem iid = (iid, getItemBody iid s) in map iidItem $ EM.keys bag bagAssocsK :: State -> ItemBag -> [(ItemId, (Item, ItemQuant))] bagAssocsK s bag = let iidItem (iid, kit) = (iid, (getItemBody iid s, kit)) in map iidItem $ EM.assocs bag posToBig :: Point -> LevelId -> State -> Maybe ActorId posToBig pos lid s = posToBigLvl pos $ sdungeon s EM.! lid posToBigAssoc :: Point -> LevelId -> State -> Maybe (ActorId, Actor) posToBigAssoc pos lid s = let maid = posToBigLvl pos $ sdungeon s EM.! lid in fmap (\aid -> (aid, getActorBody aid s)) maid posToProjs :: Point -> LevelId -> State -> [ActorId] posToProjs pos lid s = posToProjsLvl pos $ sdungeon s EM.! lid posToProjAssocs :: Point -> LevelId -> State -> [(ActorId, Actor)] posToProjAssocs pos lid s = let l = posToProjsLvl pos $ sdungeon s EM.! lid in map (\aid -> (aid, getActorBody aid s)) l posToAids :: Point -> LevelId -> State -> [ActorId] posToAids pos lid s = posToAidsLvl pos $ sdungeon s EM.! lid posToAidAssocs :: Point -> LevelId -> State -> [(ActorId, Actor)] posToAidAssocs pos lid s = let l = posToAidsLvl pos $ sdungeon s EM.! lid in map (\aid -> (aid, getActorBody aid s)) l -- | Calculate loot's worth for a given faction. calculateTotal :: FactionId -> State -> (ItemBag, Int) calculateTotal fid s = let bag = combinedItems fid s items = map (\(iid, (k, _)) -> (getItemBody iid s, k)) $ EM.assocs bag price (item, k) = itemPrice k $ getItemKind item s in (bag, sum $ map price items) -- | Price an item, taking count into consideration. itemPrice :: Int -> IK.ItemKind -> Int itemPrice jcount itemKind = case lookup IK.VALUABLE $ IK.ifreq itemKind of Just k -> jcount * k Nothing -> 0 findIid :: ActorId -> FactionId -> ItemId -> State -> [(ActorId, (Actor, CStore))] findIid leader fid iid s = let actors = fidActorNotProjGlobalAssocs fid s itemsOfActor (aid, b) = let itemsOfCStore store = let bag = getBodyStoreBag b store s in map (\iid2 -> (iid2, (aid, (b, store)))) (EM.keys bag) stores = [CEqp, COrgan] ++ [CStash | aid == leader] in concatMap itemsOfCStore stores items = concatMap itemsOfActor actors in map snd $ filter ((== iid) . fst) items -- Trunk not considered (if stolen). combinedItems :: FactionId -> State -> ItemBag combinedItems fid s = let stashBag = getFactionStashBag fid s bs = map snd $ inline fidActorNotProjGlobalAssocs fid s in EM.unionsWith mergeItemQuant $ map beqp bs ++ [stashBag] getActorBody :: ActorId -> State -> Actor {-# INLINE getActorBody #-} getActorBody aid s = sactorD s EM.! aid -- For now, faction and doctrine skill modifiers only change -- the stats that affect permitted actions (@SkMove..SkApply@), -- so the expensive @actorCurrentSkills@ operation doesn't need to be used -- when checking the other skills, e.g., for FOV calculations, -- and the @getActorMaxSkills@ cheap operation suffices. -- (@ModeKind@ content is not currently validated in this respect.) getActorMaxSkills :: ActorId -> State -> Ability.Skills {-# INLINE getActorMaxSkills #-} getActorMaxSkills aid s = sactorMaxSkills s EM.! aid actorCurrentSkills :: Maybe ActorId -> ActorId -> State -> Ability.Skills actorCurrentSkills mleader aid s = let body = getActorBody aid s actorMaxSk = getActorMaxSkills aid s fact = (EM.! bfid body) . sfactionD $ s skillsFromDoctrine = Ability.doctrineSkills $ gdoctrine fact factionSkills | Just aid == mleader = Ability.zeroSkills | otherwise = fskillsOther (gkind fact) `Ability.addSkills` skillsFromDoctrine in actorMaxSk `Ability.addSkills` factionSkills -- Check that the actor can move, also between levels and through doors. -- Otherwise, it's too awkward for human player to control, e.g., -- being stuck in a room with revolving doors closing after one turn -- and the player needing to micromanage opening such doors with -- another actor all the time. Completely immovable actors -- e.g., an impregnable surveillance camera in a crowded corridor, -- are less of a problem due to micromanagment, but more due to -- the constant disturbing of other actor's running, etc. canTraverse :: ActorId -> State -> Bool canTraverse aid s = let actorMaxSk = getActorMaxSkills aid s in Ability.getSk Ability.SkMove actorMaxSk > 0 && Ability.getSk Ability.SkAlter actorMaxSk >= fromEnum TK.talterForStairs getCarriedAssocsAndTrunk :: Actor -> State -> [(ItemId, Item)] getCarriedAssocsAndTrunk b s = -- The trunk is important for a case of spotting a caught projectile -- with a stolen projecting item. This actually does happen. let trunk = EM.singleton (btrunk b) quantSingle in bagAssocs s $ EM.unionsWith const [beqp b, borgan b, trunk] getContainerBag :: Container -> State -> ItemBag getContainerBag c s = case c of CFloor lid p -> getFloorBag lid p s CEmbed lid p -> getEmbedBag lid p s CActor aid cstore -> let b = getActorBody aid s in getBodyStoreBag b cstore s CTrunk{} -> EM.empty -- for dummy/test/analytics cases getFloorBag :: LevelId -> Point -> State -> ItemBag getFloorBag lid p s = EM.findWithDefault EM.empty p $ lfloor (sdungeon s EM.! lid) getEmbedBag :: LevelId -> Point -> State -> ItemBag getEmbedBag lid p s = EM.findWithDefault EM.empty p $ lembed (sdungeon s EM.! lid) getBodyStoreBag :: Actor -> CStore -> State -> ItemBag getBodyStoreBag b cstore s = case cstore of CGround -> getFloorBag (blid b) (bpos b) s COrgan -> borgan b CEqp -> beqp b CStash -> getFactionStashBag (bfid b) s getFactionStashBag :: FactionId -> State -> ItemBag getFactionStashBag fid s = case gstash $ sfactionD s EM.! fid of Just (lid, pos) -> getFloorBag lid pos s Nothing -> EM.empty mapActorItems_ :: Monad m => (CStore -> ItemId -> ItemQuant -> m ()) -> Actor -> State -> m () mapActorItems_ f b s = do let notProcessed = [CGround] sts = [minBound..maxBound] \\ notProcessed g cstore = do let bag = getBodyStoreBag b cstore s mapM_ (uncurry $ f cstore) $ EM.assocs bag mapM_ g sts getActorAssocs :: ActorId -> CStore -> State -> [(ItemId, (Item, ItemQuant))] getActorAssocs aid cstore s = let b = getActorBody aid s in bagAssocsK s $ getBodyStoreBag b cstore s -- | Checks if the actor is present on the current level. -- The order of argument here and in other functions is set to allow -- -- > b <- getsState (memActor a) memActor :: ActorId -> LevelId -> State -> Bool memActor aid lid s = maybe False ((== lid) . blid) $ EM.lookup aid $ sactorD s -- | Get current time from the dungeon data. getLocalTime :: LevelId -> State -> Time getLocalTime lid s = ltime $ sdungeon s EM.! lid regenCalmDelta :: ActorId -> Actor -> State -> Int64 regenCalmDelta aid body s = let calmIncr = oneM -- normal rate of calm regen actorMaxSk = getActorMaxSkills aid s maxDeltaCalm = xM (Ability.getSk Ability.SkMaxCalm actorMaxSk) - bcalm body fact = (EM.! bfid body) . sfactionD $ s -- Worry actor by non-projectile enemies felt (even if not seen) -- on the level within 3 steps. Even dying, but not hiding in wait. isHeardFoe (!p, aid2) = let b = getActorBody aid2 s in inline chessDist p (bpos body) <= 3 && not (actorWaitsOrSleeps b) -- uncommon && inline isFoe (bfid body) fact (bfid b) -- costly actorRelaxed = deltaBenign $ bcalmDelta body actorWasRelaxed = deltaWasBenign $ bcalmDelta body in if | not actorRelaxed -> 0 -- if no foes around, do not compensate and obscure distress, -- otherwise, don't increase delta further and suggest grave harm; -- note that in the effect, an actor that first hears distant -- action and then hears nearby enemy, won't notice the latter, -- which can be justified by distraction and is KISS and tactical | any isHeardFoe $ EM.assocs $ lbig $ sdungeon s EM.! blid body -> minusM1 -- even if all calmness spent, keep informing the client; -- from above we know delta won't get too large here | actorWasRelaxed -> min calmIncr (max 0 maxDeltaCalm) -- if Calm is over max | otherwise -> 0 -- don't regenerate if shortly after stress, to make -- waking up actors via bad stealth easier actorInAmbient :: Actor -> State -> Bool actorInAmbient b s = let lvl = (EM.! blid b) . sdungeon $ s in Tile.isLit (coTileSpeedup $ scops s) (lvl `at` bpos b) -- Check whether an actor can displace another. We assume they are adjacent -- and they are foes. dispEnemy :: ActorId -> ActorId -> Ability.Skills -> State -> Bool dispEnemy source target actorMaxSk s = let hasBackup b = let adjAssocs = adjacentBigAssocs b s fact = sfactionD s EM.! bfid b friend (_, b2) = isFriend (bfid b) fact (bfid b2) && bhp b2 > 0 in any friend adjAssocs sb = getActorBody source s tb = getActorBody target s tfact = sfactionD s EM.! bfid tb in bproj tb || not (actorDying tb || actorWaits tb || Ability.getSk Ability.SkMove actorMaxSk <= 0 -- sometimes this comes from sleep, but it's transient -- and if we made exception for sleep, we would displace -- immobile sleeping actors || Just (blid tb, bpos tb) == gstash tfact || hasBackup sb && hasBackup tb) -- solo actors are flexible itemToFull :: ItemId -> State -> ItemFull itemToFull iid s = itemToFull6 (scops s) (sdiscoKind s) (sdiscoAspect s) iid (getItemBody iid s) fullAssocs :: ActorId -> [CStore] -> State -> [(ItemId, ItemFull)] fullAssocs aid cstores s = let allAssocs = concatMap (\cstore -> getActorAssocs aid cstore s) cstores iToFull (iid, (item, _kit)) = (iid, itemToFull6 (scops s) (sdiscoKind s) (sdiscoAspect s) iid item) in map iToFull allAssocs kitAssocs :: ActorId -> [CStore] -> State -> [(ItemId, ItemFullKit)] kitAssocs aid cstores s = let allAssocs = concatMap (\cstore -> getActorAssocs aid cstore s) cstores iToFull (iid, (item, kit)) = (iid, ( itemToFull6 (scops s) (sdiscoKind s) (sdiscoAspect s) iid item , kit )) in map iToFull allAssocs getItemKindId :: Item -> State -> ContentId IK.ItemKind getItemKindId item s = case jkind item of IdentityObvious ik -> ik IdentityCovered ix ik -> fromMaybe ik $ EM.lookup ix $ sdiscoKind s getIidKindId :: ItemId -> State -> ContentId IK.ItemKind getIidKindId iid s = getItemKindId (getItemBody iid s) s getItemKind :: Item -> State -> IK.ItemKind getItemKind item s = okind (coitem $ scops s) $ getItemKindId item s getIidKind :: ItemId -> State -> IK.ItemKind getIidKind iid s = getItemKind (getItemBody iid s) s getItemKindIdServer :: Item -> State -> ContentId IK.ItemKind getItemKindIdServer item s = case jkind item of IdentityObvious ik -> ik IdentityCovered ix _ik -> fromMaybe (error $ show $ jkind item) (EM.lookup ix $ sdiscoKind s) getIidKindIdServer :: ItemId -> State -> ContentId IK.ItemKind getIidKindIdServer iid s = getItemKindIdServer (getItemBody iid s) s getItemKindServer :: Item -> State -> IK.ItemKind getItemKindServer item s = okind (coitem $ scops s) $ getItemKindIdServer item s getIidKindServer :: ItemId -> State -> IK.ItemKind getIidKindServer iid s = getItemKindServer (getItemBody iid s) s tileAlterable :: LevelId -> Point -> State -> Bool tileAlterable lid pos s = let COps{coTileSpeedup} = scops s embeds = getEmbedBag lid pos s lvl = sdungeon s EM.! lid t = lvl `at` pos triggerable = any (\iid -> not $ null $ IK.ieffects $ getIidKind iid s) (EM.keys embeds) in Tile.isModifiable coTileSpeedup t || triggerable -- | Determine the dungeon level of the container. If the item is in -- the shared stash, the level depends on which actor asks, not where -- the stash is located physically. lidFromC :: Container -> State -> LevelId lidFromC (CFloor lid _) _ = lid lidFromC (CEmbed lid _) _ = lid lidFromC (CActor aid _) s = blid $ getActorBody aid s lidFromC (CTrunk _ lid _) _ = lid posFromC :: Container -> State -> Point posFromC (CFloor _ pos) _ = pos posFromC (CEmbed _ pos) _ = pos posFromC (CActor aid _) s = bpos $ getActorBody aid s posFromC c@CTrunk{} _ = error $ "" `showFailure` c vicinityFoeAdj :: ((ActorId, Actor) -> Bool) -> ActorId -> State -> Bool {-# INLINE vicinityFoeAdj #-} vicinityFoeAdj predicate aid s = let body = getActorBody aid s lvl = (EM.! blid body) . sdungeon $ s fact = (EM.! bfid body) . sfactionD $ s f !p = case posToBigLvl p lvl of Nothing -> False Just aid2 -> let b2 = getActorBody aid2 s in isFoe (bfid body) fact (bfid b2) && predicate (aid2, b2) h !p = case posToProjsLvl p lvl of [] -> False aid2 : _ -> isFoe (bfid body) fact . bfid $ getActorBody aid2 s in any (\p -> f p || h p) $ vicinityUnsafe $ bpos body -- | Require that any non-dying foe is adjacent. We include even -- projectiles that explode when stricken down, because they can be caught -- and then they don't explode, so it makes sense to focus on handling them. -- If there are many projectiles in a single adjacent position, we only test -- the first one, the one that would be hit in melee (this is not optimal -- if the actor would need to flee instead of meleeing, but fleeing -- with *many* projectiles adjacent is a possible waste of a move anyway). anyFoeAdj :: ActorId -> State -> Bool anyFoeAdj = vicinityFoeAdj (const True) anyHarmfulFoeAdj :: ActorMaxSkills -> ActorId -> State -> Bool anyHarmfulFoeAdj = vicinityFoeAdj . uncurry . actorWorthKilling adjacentBigAssocs :: Actor -> State -> [(ActorId, Actor)] {-# INLINE adjacentBigAssocs #-} adjacentBigAssocs body s = let lvl = (EM.! blid body) . sdungeon $ s f !p = posToBigLvl p lvl g !aid = (aid, getActorBody aid s) in map g $ mapMaybe f $ vicinityUnsafe $ bpos body adjacentProjAssocs :: Actor -> State -> [(ActorId, Actor)] {-# INLINE adjacentProjAssocs #-} adjacentProjAssocs body s = let lvl = (EM.! blid body) . sdungeon $ s f !p = posToProjsLvl p lvl g !aid = (aid, getActorBody aid s) in map g $ concatMap f $ vicinityUnsafe $ bpos body armorHurtBonus :: ActorId -> ActorId -> State -> Int armorHurtBonus source target s = let sb = getActorBody source s sMaxSk = getActorMaxSkills source s tMaxSk = getActorMaxSkills target s in armorHurtCalculation (bproj sb) sMaxSk tMaxSk -- | Check if any non-dying foe is adjacent to any of our normal actors -- and either can harm them via melee or can attack from a distance. -- Otherwise no point meleeing him. Projectiles are ignored, because -- they are not actively attempted to melee, see @meleeAny@. -- This is regardless of whether our actor can melee or just needs to flee, -- in which case alert is needed so that he is not slowed down by others. -- However, if our actor can't move nor melee, no real combat is taking place. -- This is needed only by AI and computed as lazily as possible. inMelee :: ActorMaxSkills -> FactionId -> LevelId -> State -> Bool inMelee !actorMaxSkills !fid !lid s = let fact = sfactionD s EM.! fid f (!aid, !b) = blid b == lid && not (bproj b) && inline isFoe fid fact (bfid b) -- costly && actorWorthKilling actorMaxSkills aid b allFoes = filter f $ EM.assocs $ sactorD s g (!aid, !b) = bfid b == fid && blid b == lid && not (bproj b) && bhp b > 0 && let actorMaxSk = actorMaxSkills EM.! aid in Ability.getSk Ability.SkMove actorMaxSk > 0 || actorCanMeleeToHarm actorMaxSkills aid b allOurs = filter g $ EM.assocs $ sactorD s -- We assume foes are less numerous, even though they may come -- from multiple factions and they contain projectiles, -- because we see all our actors, while many foes may be hidden. -- Consequently, we allocate the set of foe positions -- and avoid allocating ours, by iterating over our actors. -- This in O(mn) instead of O(m+n), but it allocates -- less and multiplicative constants are lower. -- We inspect adjacent locations of foe positions, not of ours, -- thus increasing allocation a bit, but not by much, because -- the set should be rather saturated. -- If there are no foes in sight, we don't iterate at all. setFoeVicinity = ES.fromList $ concatMap (vicinityUnsafe . bpos . snd) allFoes in not (ES.null setFoeVicinity) -- shortcut && any (\(_, b) -> bpos b `ES.member` setFoeVicinity) allOurs LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Analytics.hs0000644000000000000000000000512007346545000022452 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Per-actor analytics of personal feats. module Game.LambdaHack.Common.Analytics ( FactionAnalytics, ActorAnalytics, GenerationAnalytics , KillMap, Analytics(..), KillHow(..) , emptyAnalytics, addFactionKill, addActorKill #ifdef EXPOSE_INTERNAL -- * Internal operations , addKill #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import GHC.Generics (Generic) import Game.LambdaHack.Common.Types import Game.LambdaHack.Definition.Defs -- | Summary analytics data for each faction. type FactionAnalytics = EM.EnumMap FactionId Analytics -- | Analytics data for each live actor. type ActorAnalytics = EM.EnumMap ActorId Analytics -- | Statistics of possible and actual generation of items for each lore kind. type GenerationAnalytics = EM.EnumMap SLore (EM.EnumMap ItemId Int) -- | Labels of individual kill count analytics. data KillHow = KillKineticMelee | KillKineticRanged | KillKineticBlast | KillKineticPush | KillOtherMelee | KillOtherRanged | KillOtherBlast | KillOtherPush | KillActorLaunch | KillTileLaunch | KillDropLaunch | KillCatch deriving (Show, Eq, Enum, Generic) instance Binary KillHow type KillMap = EM.EnumMap FactionId (EM.EnumMap ItemId Int) -- | Statistics of past events concerning an actor. newtype Analytics = Analytics { akillCounts :: EM.EnumMap KillHow KillMap } deriving (Show, Eq, Binary) emptyAnalytics :: Analytics emptyAnalytics = Analytics { akillCounts = EM.empty } addKill :: KillHow -> FactionId -> ItemId -> Maybe Analytics -> Analytics addKill killHow fid iid = let f Nothing = Analytics {akillCounts = EM.singleton killHow $ EM.singleton fid $ EM.singleton iid 1} f (Just an) = an {akillCounts = EM.alter g killHow $ akillCounts an} g Nothing = Just $ EM.singleton fid $ EM.singleton iid 1 g (Just fidMap) = Just $ EM.alter h fid fidMap h Nothing = Just $ EM.singleton iid 1 h (Just iidMap) = Just $ EM.alter i iid iidMap i Nothing = Just 1 i (Just n) = Just $ n + 1 in f addFactionKill :: FactionId -> KillHow -> FactionId -> ItemId -> FactionAnalytics -> FactionAnalytics addFactionKill fidOfKiller killHow fid iid = EM.alter (Just . addKill killHow fid iid) fidOfKiller addActorKill :: ActorId -> KillHow -> FactionId -> ItemId -> ActorAnalytics -> ActorAnalytics addActorKill aid killHow fid iid = EM.alter (Just . addKill killHow fid iid) aid LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Area.hs0000644000000000000000000000532507346545000021402 0ustar0000000000000000-- | Rectangular areas of levels and their basic operations. module Game.LambdaHack.Common.Area ( Area, toArea, fromArea, spanArea, trivialArea, isTrivialArea , inside, shrink, expand, middlePoint, areaInnerBorder, sumAreas, punindex ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import Game.LambdaHack.Common.Point import Game.LambdaHack.Definition.Defs -- | The type of areas. The bottom left and the top right points. data Area = Area X Y X Y deriving (Show, Eq) -- | Checks if it's an area with at least one field. toArea :: (X, Y, X, Y) -> Maybe Area toArea (x0, y0, x1, y1) = if x0 <= x1 && y0 <= y1 then Just $ Area x0 y0 x1 y1 else Nothing fromArea :: Area -> (X, Y, X, Y) {-# INLINE fromArea #-} fromArea (Area x0 y0 x1 y1) = (x0, y0, x1, y1) -- Funny thing, Trivial area, a point, has span 1 in each dimension. spanArea :: Area -> (Point, X, Y) spanArea (Area x0 y0 x1 y1) = (Point x0 y0, x1 - x0 + 1, y1 - y0 + 1) trivialArea :: Point -> Area trivialArea (Point x y) = Area x y x y isTrivialArea :: Area -> Bool isTrivialArea (Area x0 y0 x1 y1) = x0 == x1 && y0 == y1 -- | Checks that a point belongs to an area. inside :: Area -> Point -> Bool {-# INLINE inside #-} inside = insideP . fromArea -- | Shrink the given area on all fours sides by the amount. shrink :: Area -> Maybe Area shrink (Area x0 y0 x1 y1) = toArea (x0 + 1, y0 + 1, x1 - 1, y1 - 1) expand :: Area -> Area expand (Area x0 y0 x1 y1) = Area (x0 - 1) (y0 - 1) (x1 + 1) (y1 + 1) middlePoint :: Area -> Point middlePoint (Area x0 y0 x1 y1) = Point (x0 + (x1 - x0) `div` 2) (y0 + (y1 - y0) `div` 2) areaInnerBorder :: Area -> [Point] areaInnerBorder (Area x0 y0 x1 y1) = [ Point x y | x <- [x0, x1], y <- [y0..y1] ] ++ [ Point x y | x <- [x0+1..x1-1], y <- [y0, y1] ] -- We assume the areas are adjacent. sumAreas :: Area -> Area -> Area sumAreas a@(Area x0 y0 x1 y1) a'@(Area x0' y0' x1' y1') = if | y1 == y0' -> assert (x0 == x0' && x1 == x1' `blame` (a, a')) $ Area x0 y0 x1 y1' | y0 == y1' -> assert (x0 == x0' && x1 == x1' `blame` (a, a')) $ Area x0' y0' x1' y1 | x1 == x0' -> assert (y0 == y0' && y1 == y1' `blame` (a, a')) $ Area x0 y0 x1' y1 | x0 == x1' -> assert (y0 == y0' && y1 == y1' `blame` (a, a')) $ Area x0' y0' x1 y1' | otherwise -> error $ "areas not adjacent" `showFailure` (a, a') punindex :: X -> Int -> Point {-# INLINE punindex #-} punindex xsize n = let (py, px) = n `quotRem` xsize in Point{..} instance Binary Area where put (Area x0 y0 x1 y1) = do put x0 put y0 put x1 put y1 get = Area <$> get <*> get <*> get <*> get LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/ClientOptions.hs0000644000000000000000000000764707346545000023335 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Options that affect the behaviour of the client. module Game.LambdaHack.Common.ClientOptions ( FullscreenMode(..), ClientOptions(..), defClientOptions ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.DeepSeq import Data.Binary import GHC.Generics (Generic) import Game.LambdaHack.Common.Misc -- | Kinds of fullscreen or windowed mode. See . data FullscreenMode = NotFullscreen -- ^ a normal window instead of fullscreen | BigBorderlessWindow -- ^ fake fullscreen; window the size of the desktop; -- this is the preferred one, if it works | ModeChange -- ^ real fullscreen with a video mode change deriving (Show, Read, Eq, Generic) instance NFData FullscreenMode instance Binary FullscreenMode -- | Options that affect the behaviour of the client (but not game rules). data ClientOptions = ClientOptions { schosenFontset :: Maybe Text -- ^ Font set chosen by the player for the whole UI. , sallFontsScale :: Maybe Double -- ^ The scale applied to all fonts, resizing the whole UI. , sfonts :: [(Text, FontDefinition)] -- ^ Available fonts as defined in config file. , sfontsets :: [(Text, FontSet)] -- ^ Available font sets as defined in config file. , sfullscreenMode :: Maybe FullscreenMode -- ^ Whether to start in fullscreen mode and in which one. , slogPriority :: Maybe Int -- ^ How much to log (e.g., from SDL). 1 is all, 5 is errors, the default. , smaxFps :: Maybe Double -- ^ Maximal frames per second. -- This is better low and fixed, to avoid jerkiness and delays -- that tell the player there are many intelligent enemies on the level. -- That's better than scaling AI sofistication down based -- on the FPS setting and machine speed. , sdisableAutoYes :: Bool -- ^ Never auto-answer all prompts, even if under AI control. , snoAnim :: Maybe Bool -- ^ Don't show any animations. , snewGameCli :: Bool -- ^ Start a new game, overwriting the save file. , sbenchmark :: Bool -- ^ Don't create directories and files and show time stats. , sbenchMessages :: Bool -- ^ Display messages in realistic was under AI control (e.g., for benchmarking). , stitle :: Maybe String , ssavePrefixCli :: String -- ^ Prefix of the save game file name. , sfrontendANSI :: Bool -- ^ Whether to use the ANSI frontend. , sfrontendTeletype :: Bool -- ^ Whether to use the stdout/stdin frontend. , sfrontendNull :: Bool -- ^ Whether to use null (no input/output) frontend. , sfrontendLazy :: Bool -- ^ Whether to use lazy (output not even calculated) frontend. , sdbgMsgCli :: Bool -- ^ Show clients' internal debug messages. , sstopAfterSeconds :: Maybe Int , sstopAfterFrames :: Maybe Int , sprintEachScreen :: Bool , sexposePlaces :: Bool , sexposeItems :: Bool , sexposeActors :: Bool } deriving (Show, Eq, Generic) instance Binary ClientOptions -- | Default value of client options. defClientOptions :: ClientOptions defClientOptions = ClientOptions { schosenFontset = Nothing , sallFontsScale = Nothing , sfonts = [] , sfontsets = [] , sfullscreenMode = Nothing , slogPriority = Nothing , smaxFps = Nothing , sdisableAutoYes = False , snoAnim = Nothing , snewGameCli = False , sbenchmark = False , sbenchMessages = False , stitle = Nothing , ssavePrefixCli = "" , sfrontendANSI = False , sfrontendTeletype = False , sfrontendNull = False , sfrontendLazy = False , sdbgMsgCli = False , sstopAfterSeconds = Nothing , sstopAfterFrames = Nothing , sprintEachScreen = False , sexposePlaces = False , sexposeItems = False , sexposeActors = False } LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Faction.hs0000644000000000000000000002061707346545000022116 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, TupleSections #-} -- | Factions taking part in the game, e.g., a hero faction, a monster faction -- and an animal faction. module Game.LambdaHack.Common.Faction ( FactionDict, Faction(..), Diplomacy(..) , Status(..), Challenge(..) , tshowDiplomacy, tshowChallenge, gleader, isHorrorFact, noRunWithMulti , bannedPointmanSwitchBetweenLevels, isFoe, isFriend , difficultyBound, difficultyDefault, difficultyCoeff , defaultChallenge, possibleActorFactions, ppContainer #ifdef EXPOSE_INTERNAL -- * Internal operations , Dipl #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import GHC.Generics (Generic) import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind (ModeKind) import Game.LambdaHack.Core.Frequency import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs -- | All factions in the game, indexed by faction identifier. type FactionDict = EM.EnumMap FactionId Faction -- | The faction datatype. data Faction = Faction { gkind :: FactionKind -- ^ the player spec for this faction, do not update! -- it is morally read-only, but not represented -- as @ContentId FactionKind@, because it's very small -- and it's looked up often enough in the code and during runtime; -- a side-effect is that if content changes mid-game, this stays; -- if we ever have thousands of factions in a single game, -- e.g., one for each separately spawned herd of animals, change this , gname :: Text -- ^ individual name , gcolor :: Color.Color -- ^ color of numbered actors , gdoctrine :: Ability.Doctrine -- ^ non-pointmen behave according to this , gunderAI :: Bool -- ^ whether the faction is under AI control , ginitial :: [(Int, Int, GroupName ItemKind)] -- ^ initial actors , gdipl :: Dipl -- ^ diplomatic standing , gquit :: Maybe Status -- ^ cause of game end/exit , _gleader :: Maybe ActorId -- ^ the leader of the faction; don't use -- in place of sleader on clients , gstash :: Maybe (LevelId, Point) -- ^ level and position of faction's -- shared inventory stash , gvictims :: EM.EnumMap (ContentId ItemKind) Int -- ^ members killed } deriving (Show, Eq, Generic) instance Binary Faction -- | Diplomacy states. Higher overwrite lower in case of asymmetric content. data Diplomacy = Unknown | Neutral | Alliance | War deriving (Show, Eq, Ord, Enum, Generic) instance Binary Diplomacy type Dipl = EM.EnumMap FactionId Diplomacy -- | Current game status. data Status = Status { stOutcome :: Outcome -- ^ current game outcome , stDepth :: Int -- ^ depth of the final encounter , stNewGame :: Maybe (GroupName ModeKind) -- ^ new game group to start, if any } deriving (Show, Eq, Ord, Generic) instance Binary Status -- | The difficulty level influencess HP of either the human player or the AI. -- The challenges restrict some abilities of the human player only. data Challenge = Challenge { cdiff :: Int -- ^ game difficulty level (HP bonus or malus) , cfish :: Bool -- ^ cold fish challenge (no healing from enemies) , cgoods :: Bool -- ^ ready goods challenge (crafting disabled) , cwolf :: Bool -- ^ lone wolf challenge (only one starting character) , ckeeper :: Bool -- ^ finder keeper challenge (ranged attacks disabled) } deriving (Show, Eq, Ord, Generic) instance Binary Challenge tshowDiplomacy :: Diplomacy -> Text tshowDiplomacy Unknown = "unknown to each other" tshowDiplomacy Neutral = "in neutral diplomatic relations" tshowDiplomacy Alliance = "allied" tshowDiplomacy War = "at war" tshowChallenge :: Challenge -> Text tshowChallenge Challenge{..} = "(" <> T.intercalate ", " (["difficulty" <+> tshow cdiff | cdiff /= difficultyDefault] ++ ["cold fish" | cfish] ++ ["ready goods" | cgoods] ++ ["lone wolf" | cwolf] ++ ["finder keeper" | ckeeper]) <> ")" gleader :: Faction -> Maybe ActorId gleader = _gleader -- | Tell whether the faction consists of summoned horrors only. -- -- Horror player is special, for summoned actors that don't belong to any -- of the main players of a given game. E.g., animals summoned during -- a skirmish game between two hero factions land in the horror faction. -- In every game, either all factions for which summoning items exist -- should be present or a horror player should be added to host them. isHorrorFact :: Faction -> Bool isHorrorFact fact = fromMaybe 0 (lookup IK.HORROR $ fgroups $ gkind fact) > 0 -- A faction where other actors move at once or where some of leader change -- is automatic can't run with multiple actors at once. That would be -- overpowered or too complex to keep correct. -- -- Note that this doesn't take into account individual actor skills, -- so this is overly restrictive and, OTOH, sometimes running will fail -- or behave wierdly regardless. But it's simple and easy to understand -- by the UI user. noRunWithMulti :: Faction -> Bool noRunWithMulti fact = let skillsOther = fskillsOther $ gkind fact in Ability.getSk Ability.SkMove skillsOther >= 0 || bannedPointmanSwitchBetweenLevels fact || not (fhasPointman (gkind fact)) bannedPointmanSwitchBetweenLevels :: Faction -> Bool bannedPointmanSwitchBetweenLevels = fspawnsFast . gkind -- | Check if factions are at war. Assumes symmetry. isFoe :: FactionId -> Faction -> FactionId -> Bool isFoe fid1 fact1 fid2 = fid1 /= fid2 -- shortcut && War == EM.findWithDefault Unknown fid2 (gdipl fact1) -- | Check if factions are allied. Assumes symmetry. isAlly :: Faction -> FactionId -> Bool {-# INLINE isAlly #-} isAlly fact1 fid2 = Alliance == EM.findWithDefault Unknown fid2 (gdipl fact1) -- | Check if factions are allied or are the same faction. Assumes symmetry. isFriend :: FactionId -> Faction -> FactionId -> Bool isFriend fid1 fact1 fid2 = fid1 == fid2 || isAlly fact1 fid2 difficultyBound :: Int difficultyBound = 9 difficultyDefault :: Int difficultyDefault = (1 + difficultyBound) `div` 2 -- The function is its own inverse. difficultyCoeff :: Int -> Int difficultyCoeff n = difficultyDefault - n defaultChallenge :: Challenge defaultChallenge = Challenge { cdiff = difficultyDefault , cfish = False , cgoods = False , cwolf = False , ckeeper = False } possibleActorFactions :: [GroupName ItemKind] -> ItemKind -> FactionDict -> Frequency (FactionId, Faction) possibleActorFactions itemGroups itemKind factionD = let candidatesFromGroups grps = let h (fid, fact) = let f grp (grp2, n) = [(n, (fid, fact)) | grp == grp2] g grp = concatMap (f grp) (fgroups (gkind fact)) in concatMap g grps in concatMap h $ EM.assocs factionD allCandidates = [ candidatesFromGroups itemGroups -- when origin known/matters , candidatesFromGroups $ map fst $ IK.ifreq itemKind -- otherwise , map (1,) $ filter (isHorrorFact . snd) $ EM.assocs factionD -- fall back , map (1,) $ EM.assocs factionD -- desperate fall back ] in case filter (not . null) allCandidates of [] -> error "possibleActorFactions: no faction found for an actor" candidates : _ -> toFreq "possibleActorFactions" candidates ppContainer :: FactionDict -> Container -> Text ppContainer factionD (CFloor lid p) = let f fact = case gstash fact of Just (slid, sp) | slid == lid && sp == p -> Just $ gname fact _ -> Nothing in case mapMaybe f $ EM.elems factionD of [] -> "nearby" [t] -> "in the shared inventory stash of" <+> t _ -> "in a shared zone of interests" ppContainer _ CEmbed{} = "embedded nearby" ppContainer _ (CActor _ cstore) = ppCStoreIn cstore ppContainer _ c@CTrunk{} = error $ "" `showFailure` c LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/File.hs0000644000000000000000000000051707346545000021407 0ustar0000000000000000-- | Saving/loading to files, with serialization and compression. module Game.LambdaHack.Common.File ( encodeEOF, strictDecodeEOF , tryCreateDir, doesFileExist, tryWriteFile, readFile, renameFile ) where import Prelude () #ifdef USE_JSFILE import Game.LambdaHack.Common.JSFile #else import Game.LambdaHack.Common.HSFile #endif LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/HSFile.hs0000644000000000000000000000647207346545000021650 0ustar0000000000000000-- | Saving/loading to files, with serialization and compression. module Game.LambdaHack.Common.HSFile ( encodeEOF, strictDecodeEOF , tryCreateDir, doesFileExist, tryWriteFile, readFile, renameFile #ifdef EXPOSE_INTERNAL -- * Internal operations , encodeData #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Codec.Compression.Zlib as Z import qualified Control.Exception as Ex import Data.Binary import qualified Data.ByteString.Lazy as LBS import qualified Data.Text.IO as T import Data.Version import System.Directory import System.FilePath import System.IO ( IOMode (..) , hClose , hSetEncoding , localeEncoding , openBinaryFile , readFile , utf8 , withBinaryFile , withFile ) -- | Serialize and save data. -- Note that LBS.writeFile opens the file in binary mode. encodeData :: Binary a => FilePath -> a -> IO () encodeData path a = do let tmpPath = path <.> "tmp" Ex.bracketOnError (openBinaryFile tmpPath WriteMode) (\h -> hClose h >> removeFile tmpPath) (\h -> do LBS.hPut h . encode $ a hClose h renameFile tmpPath path ) -- | Serialize, compress and save data with an EOF marker. -- The @OK@ is used as an EOF marker to ensure any apparent problems with -- corrupted files are reported to the user ASAP. encodeEOF :: Binary b => FilePath -> Version -> b -> IO () encodeEOF path v b = encodeData path (v, (Z.compress $ encode b, "OK" :: String)) -- | Read, decompress and deserialize data with an EOF marker. -- The @OK@ EOF marker ensures any easily detectable file corruption -- is discovered and reported before any value is decoded from -- the second component and before the file handle is closed. -- OTOH, binary encoding corruption is not discovered until a version -- check elswere ensures that binary formats are compatible. strictDecodeEOF :: Binary b => FilePath -> IO (Version, b) strictDecodeEOF path = withBinaryFile path ReadMode $ \h -> do c1 <- LBS.hGetContents h let (v1, (c2, s)) = decode c1 return $! if s == ("OK" :: String) then (v1, decode $ Z.decompress c2) else error $ "Fatal error: corrupted file " ++ path -- | Try to create a directory, if it doesn't exist. We catch exceptions -- in case many clients try to do the same thing at the same time. tryCreateDir :: FilePath -> IO () tryCreateDir dir = do dirExists <- doesDirectoryExist dir unless dirExists $ Ex.handle (\(_ :: Ex.IOException) -> return ()) (createDirectory dir) -- | Try to write a file, given content, if the file not already there. -- We catch exceptions in case many clients and/or the server try to do -- the same thing at the same time. Using `Text.IO` to avoid UTF conflicts -- with OS or filesystem. tryWriteFile :: FilePath -> Text -> IO () tryWriteFile path content = do fileExists <- doesFileExist path unless fileExists $ do -- With some luck, locale was already corrected in Main.hs, but just -- in case, we make sure not to save UTF files in too primitve encodings. let enc = localeEncoding Ex.handle (\(ex :: Ex.IOException) -> print $ show ex) $ withFile path WriteMode $ \h -> do when (show enc `elem` ["ASCII", "ISO-8859-1", "ISO-8859-2"]) $ hSetEncoding h utf8 T.hPutStr h content LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/HighScore.hs0000644000000000000000000001736507346545000022414 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | High score table operations. module Game.LambdaHack.Common.HighScore ( ScoreTable, ScoreDict , empty, register, showScore, showAward , getTable, unTable, getRecord, getStatus, getDate #ifdef EXPOSE_INTERNAL -- * Internal operations , ScoreRecord, insertPos #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import Data.Time.Clock.POSIX import Data.Time.LocalTime import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Time 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, -- from the best to the worst, in lexicographic ordering wrt the fields below. data ScoreRecord = ScoreRecord { points :: Int -- ^ the score , negTime :: Time -- ^ game time spent (negated, so less better) , 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 , ourVictims :: EM.EnumMap (ContentId ItemKind) Int -- ^ allies lost , theirVictims :: EM.EnumMap (ContentId ItemKind) Int -- ^ foes killed } deriving (Eq, Ord, Generic) instance Binary ScoreRecord -- | The list of scores, in decreasing order. newtype ScoreTable = ScoreTable {unTable :: [ScoreRecord]} deriving (Eq, Binary) instance Show ScoreTable where show _ = "a score table" -- | A dictionary from game mode IDs to scores tables. type ScoreDict = EM.EnumMap (ContentId ModeKind) ScoreTable -- | Empty score table empty :: ScoreDict empty = EM.empty -- | Insert a new score into the table, Return new table and the ranking. -- Make sure the table doesn't grow too large. insertPos :: ScoreRecord -> ScoreTable -> (ScoreTable, Int) insertPos s (ScoreTable table) = let (prefix, suffix) = span (> s) table pos = length prefix + 1 in (ScoreTable $ prefix ++ [s] ++ take (100 - pos) suffix, pos) -- | Register a new score in a score table. register :: ScoreTable -- ^ old table -> Int -- ^ the total value of faction items -> Int -- ^ the total value of dungeon items -> Time -- ^ game time spent -> Status -- ^ reason of the game interruption -> POSIXTime -- ^ current date -> Challenge -- ^ challenge setup -> Text -- ^ name of the faction's gkind -> EM.EnumMap (ContentId ItemKind) Int -- ^ allies lost -> EM.EnumMap (ContentId ItemKind) Int -- ^ foes killed -> HiCondPoly -> (Bool, (ScoreTable, Int)) register table total dungeonTotal time status@Status{stOutcome} date challenge gkindName ourVictims theirVictims hiCondPoly = let turnsSpent = intToDouble $ timeFitUp time timeTurn hiInValue (hi, c) = assert (total <= dungeonTotal) $ case hi of HiConst -> c 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) HiBlitz -> -- Up to 1000000/-c turns matter. sqrt $ max 0 (1000000 + c * turnsSpent) HiSurvival -> -- Up to 1000000/c turns matter. sqrt $ max 0 (min 1000000 $ c * turnsSpent) HiKill -> c * intToDouble (sum (EM.elems theirVictims)) HiLoss -> c * intToDouble (sum (EM.elems ourVictims)) hiPolynomialValue = sum . map hiInValue hiSummandValue (hiPoly, outcomes) = if stOutcome `elem` outcomes then max 0 (hiPolynomialValue hiPoly) else 0 hiCondValue = sum . map hiSummandValue -- Other challenges than HP difficulty are not reflected in score. points = ceiling $ hiCondValue hiCondPoly * 1.5 ^^ (- (difficultyCoeff (cdiff challenge))) negTime = absoluteTimeNegate time score = ScoreRecord{..} in (points > 0 || turnsSpent > 100, insertPos score table) -- even if stash looted and all gold lost, count highscore if long game -- | Show a single high score, from the given ranking in the high score table. showScore :: TimeZone -> Int -> ScoreRecord -> [Text] showScore tz pos score = let Status{stOutcome, stDepth} = status score died = nameOutcomePast stOutcome <+> case stOutcome of Killed -> "on level" <+> tshow (abs stDepth) _ -> "" curDate = T.take 19 . tshow . utcToLocalTime tz . posixSecondsToUTCTime . date $ score turns = absoluteTimeNegate (negTime score) `timeFitUp` timeTurn tpos = T.justifyRight 3 ' ' $ tshow pos tscore = T.justifyRight 6 ' ' $ tshow $ points score victims = let nkilled = sum $ EM.elems $ theirVictims score nlost = sum $ EM.elems $ ourVictims score in "killed" <+> tshow nkilled <> ", lost" <+> tshow nlost -- This may overfill the screen line, but with default fonts -- it's very unlikely and not a big problem in any case. chalText | challenge score == defaultChallenge = "" | otherwise = tshowChallenge (challenge score) tturns = makePhrase [MU.CarWs turns "turn"] in [ tpos <> "." <+> tscore <+> gkindName score <+> died <> "," <+> victims <> "," , " " <> "after" <+> tturns <+> chalText <+> "on" <+> curDate <> "." ] getTable :: ContentId ModeKind -> ScoreDict -> ScoreTable getTable = EM.findWithDefault (ScoreTable []) getRecord :: Int -> ScoreTable -> ScoreRecord getRecord pos (ScoreTable table) = fromMaybe (error $ "" `showFailure` pos) $ listToMaybe $ drop (pred pos) table getStatus :: ScoreRecord -> Status getStatus = status getDate :: ScoreRecord -> POSIXTime getDate = date showAward :: Int -- ^ number of (3-line) scores to be shown -> ScoreTable -- ^ current score table -> Int -- ^ position of the current score in the table -> Text -- ^ the name of the game mode -> Text showAward height table pos gameModeName = let posStatus = status $ getRecord pos table (efforts, person, msgUnless) = case stOutcome posStatus of Killed | stDepth posStatus <= 1 -> ("your short-lived struggle", MU.Sg3rd, "(no bonus)") Killed -> ("your heroic deeds", MU.PlEtc, "(no bonus)") Defeated -> ("your futile efforts", MU.PlEtc, "(no bonus)") Camping -> -- This is only according to the limited player knowledge; -- the final score can be different, which is fine: ("your valiant exploits", MU.PlEtc, "") Conquer -> ("your ruthless victory", MU.Sg3rd, if pos <= height && length (unTable table) > 3 then "among the best" -- "greatest heroes" doesn't fit else "(bonus included)") Escape -> ("your dashing coup", MU.Sg3rd, if pos <= height && length (unTable table) > 3 then "among the best" else "(bonus included)") Restart -> ("your abortive attempt", MU.Sg3rd, "(no bonus)") subject = makePhrase [efforts, "in", MU.Text gameModeName] in makeSentence [ MU.SubjectVerb person MU.Yes (MU.Text subject) "award you" , MU.Ordinal pos, "place", msgUnless ] LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Item.hs0000644000000000000000000005402607346545000021432 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, TupleSections #-} -- | Weapons, treasure and all the other items in the game. module Game.LambdaHack.Common.Item ( Item(..), ItemIdentity(..) , ItemKindIx, ItemDisco(..), ItemFull(..), ItemFullKit , DiscoveryKind, DiscoveryAspect, ItemIxMap, Benefit(..), DiscoveryBenefit , ItemTimer, ItemTimers, ItemQuant, ItemBag, ItemDict , toItemKindIx, quantSingle, itemToFull6, aspectRecordFull, strongestSlot , itemTimerZero, createItemTimer, shiftItemTimer , deltaOfItemTimer, charging, ncharges, hasCharge , strongestMelee, unknownMeleeBonus, unknownSpeedBonus , conditionMeleeBonus, conditionSpeedBonus, armorHurtCalculation , mergeItemQuant, listToolsToConsume, subtractIidfromGrps, sortIids , TileAction (..), parseTileAction #ifdef EXPOSE_INTERNAL -- * Internal operations , valueAtEqpSlot, unknownAspect, countIidConsumed #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Hashable (Hashable) import qualified Data.Ix as Ix import GHC.Generics (Generic) import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Definition.Ability (EqpSlot (..)) import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.Flavour -- | Game items in actor possesion or strewn around the dungeon. -- The information contained in this time is available to the player -- from the moment the item is first seen and is never mutated. -- -- Some items are not created identified (@IdentityCovered@). -- Then they are presented as having a template kind that is really -- not their own, though usually close. Full kind information about -- item's kind is available through the @ItemKindIx@ index once the item -- is identified and full information about the value of item's aspect record -- is available elsewhere (both @IdentityObvious@ and @IdentityCovered@ -- items may or may not need identification of their aspect record). data Item = Item { jkind :: ItemIdentity -- ^ the kind of the item, or an indirection , jfid :: Maybe FactionId -- ^ the faction that created the item, if any , jflavour :: Flavour -- ^ flavour, always the real one, -- it's not hidden; people may not recognize -- shape, but they remember colour and old -- vs fancy look } deriving (Show, Eq, Generic) instance Binary Item -- | Either the explicit obvious kind of the item or the kind it's hidden under, -- with the details covered under the index indirection. data ItemIdentity = IdentityObvious (ContentId IK.ItemKind) | IdentityCovered ItemKindIx (ContentId IK.ItemKind) deriving (Show, Eq, Generic) instance Hashable ItemIdentity instance Binary ItemIdentity -- | The map of item ids to item aspect record. The full map is known -- by the server. type DiscoveryAspect = EM.EnumMap ItemId IA.AspectRecord -- | An index of the kind identifier of an item. Clients have partial knowledge -- how these idexes map to kind ids. They gain knowledge by identifying items. -- The indexes and kind identifiers are 1-1. newtype ItemKindIx = ItemKindIx Word16 deriving (Show, Eq, Ord, Enum, Ix.Ix, Hashable, Binary) -- | The secret part of the information about an item. If a faction -- knows the aspect record of the item, this is the complete secret information. -- Items that don't need second identification (the @kmConst@ flag is set) -- may be identified or not and both cases are OK (their display flavour -- will differ and that may be the point). data ItemDisco = ItemDiscoFull IA.AspectRecord | ItemDiscoMean IA.KindMean deriving (Show, Ord, Eq) -- No speedup from making fields non-strict. -- | Full information about an item. data ItemFull = ItemFull { itemBase :: Item , itemKindId :: ContentId IK.ItemKind , itemKind :: IK.ItemKind , itemDisco :: ItemDisco , itemSuspect :: Bool } deriving Show type ItemFullKit = (ItemFull, ItemQuant) -- | The map of item kind indexes to item kind ids. -- The full map, as known by the server, is 1-1. -- Because it's sparse and changes, we don't represent it as an (unboxed) -- vector, until it becomes a bottleneck (if ever, likely on JS, where only -- vectors are fast). type DiscoveryKind = EM.EnumMap ItemKindIx (ContentId IK.ItemKind) -- | The map of item kind indexes to identifiers of items that have that kind. -- Used to update data about items when their kinds become known, e.g., -- AI item use benefit data. type ItemIxMap = EM.EnumMap ItemKindIx (ES.EnumSet ItemId) -- | The fields are, in order: -- 1. whether the item should be kept in equipment (not in stash) -- 2. the total benefit from picking the item up (to use or to put in equipment) -- 3. the benefit of applying the item to self -- 4. the (usually negative, for him) value of hitting a foe in melee with it -- 5. the (usually negative, for him) value of flinging the item at an opponent data Benefit = Benefit { benInEqp :: Bool , benPickup :: Double , benApply :: Double , benMelee :: Double , benFling :: Double } deriving (Show, Generic) instance Binary Benefit type DiscoveryBenefit = EM.EnumMap ItemId Benefit -- | The absolute level's local time at which an item's copy becomes -- operational again. Even if item is not identified and so its timeout -- unknown, it's enough to compare this to the local level time -- to learn whether an item is recharged. -- -- This schema causes timeout jumps for items in stash, but timeout -- is reset when items move, so this is a minor problem. -- Global time can't be used even only for items in stash, -- or exploit would be possible when an actor on a desolate level waits -- to recharge items for actors on a busy level. It's probably -- impossible to avoid such exploits or, otherwise, timeout jumps, -- particularly for faction where many actors move on many levels -- and so an item in stash is not used by a single actor at a time. newtype ItemTimer = ItemTimer {itemTimer :: Time} deriving (Show, Eq, Binary) type ItemTimers = [ItemTimer] -- | Number of items in a bag, together with recharging timer, in case of -- items that need recharging, exists only temporarily or auto-activate -- at regular intervals. Data invariant: the length of the timer -- should be less or equal to the number of items. type ItemQuant = (Int, ItemTimers) -- | A bag of items, e.g., one of the stores of an actor or the items -- on a particular floor position or embedded in a particular map tile. type ItemBag = EM.EnumMap ItemId ItemQuant -- | All items in the dungeon (including those carried by actors), -- indexed by item identifier. type ItemDict = EM.EnumMap ItemId Item toItemKindIx :: Word16 -> ItemKindIx {-# INLINE toItemKindIx #-} toItemKindIx = ItemKindIx quantSingle :: ItemQuant quantSingle = (1, []) itemToFull6 :: COps -> DiscoveryKind -> DiscoveryAspect -> ItemId -> Item -> ItemFull itemToFull6 COps{coitem, coItemSpeedup} discoKind discoAspect iid itemBase = let (itemKindId, itemSuspect) = case jkind itemBase of IdentityObvious ik -> (ik, False) IdentityCovered ix ik -> maybe (ik, True) (, False) $ ix `EM.lookup` discoKind itemKind = okind coitem itemKindId km = getKindMean itemKindId coItemSpeedup -- If the kind is not identified, we know nothing about the real -- aspect record, so we at least assume they are variable. itemAspectMean | itemSuspect = km {IA.kmConst = False} | otherwise = km itemDisco = case EM.lookup iid discoAspect of Just itemAspect -> ItemDiscoFull itemAspect Nothing -> ItemDiscoMean itemAspectMean in ItemFull {..} aspectRecordFull :: ItemFull -> IA.AspectRecord aspectRecordFull itemFull = case itemDisco itemFull of ItemDiscoFull itemAspect -> itemAspect ItemDiscoMean itemAspectMean -> IA.kmMean itemAspectMean -- This ignores items that don't go into equipment, as determined in @benInEqp@. -- They are removed from equipment elsewhere via @harmful@. strongestSlot :: DiscoveryBenefit -> Ability.EqpSlot -> [(ItemId, ItemFullKit)] -> [(Int, (ItemId, ItemFullKit))] strongestSlot discoBenefit eqpSlot is = let f (iid, (itemFull, kit)) = let Benefit{benInEqp, benPickup, benMelee} = discoBenefit EM.! iid in if not benInEqp then Nothing else Just $ let ben = case eqpSlot of EqpSlotWeaponFast -> -- For equipping/unequipping the main reliable weapon, -- we take into account not only melee damage, -- but also timeout, aspects, etc. ceiling benPickup EqpSlotWeaponBig -> -- For equipping/unequipping the one-shot big hitter -- weapon, we take into account only melee damage -- and we don't even care if it's durable. -- The backup is ready in the slot above, after all. ceiling (- benMelee) _ -> valueAtEqpSlot eqpSlot $ aspectRecordFull itemFull idBonus = if itemSuspect itemFull then 1000 else 0 arItem = aspectRecordFull itemFull -- Equip good uniques for flavour and fun from unique effects. uniqueBonus = if IA.checkFlag Ability.Unique arItem && ben > 20 then 1000 else 0 in (ben + idBonus + uniqueBonus, (iid, (itemFull, kit))) in sortBy (flip $ comparing fst) $ mapMaybe f is valueAtEqpSlot :: EqpSlot -> IA.AspectRecord -> Int valueAtEqpSlot eqpSlot arItem@IA.AspectRecord{..} = case eqpSlot of EqpSlotMove -> Ability.getSk Ability.SkMove aSkills EqpSlotMelee -> Ability.getSk Ability.SkMelee aSkills EqpSlotDisplace -> Ability.getSk Ability.SkDisplace aSkills EqpSlotAlter -> Ability.getSk Ability.SkAlter aSkills EqpSlotWait -> Ability.getSk Ability.SkWait aSkills EqpSlotMoveItem -> Ability.getSk Ability.SkMoveItem aSkills EqpSlotProject -> Ability.getSk Ability.SkProject aSkills EqpSlotApply -> Ability.getSk Ability.SkApply aSkills EqpSlotSwimming -> Ability.getSk Ability.SkSwimming aSkills EqpSlotFlying -> Ability.getSk Ability.SkFlying aSkills EqpSlotHurtMelee -> Ability.getSk Ability.SkHurtMelee aSkills EqpSlotArmorMelee -> Ability.getSk Ability.SkArmorMelee aSkills EqpSlotArmorRanged -> Ability.getSk Ability.SkArmorRanged aSkills EqpSlotMaxHP -> Ability.getSk Ability.SkMaxHP aSkills EqpSlotSpeed -> Ability.getSk Ability.SkSpeed aSkills EqpSlotSight -> Ability.getSk Ability.SkSight aSkills EqpSlotShine -> Ability.getSk Ability.SkShine aSkills EqpSlotMiscBonus -> aTimeout -- usually better items have longer timeout + Ability.getSk Ability.SkMaxCalm aSkills + Ability.getSk Ability.SkSmell aSkills + Ability.getSk Ability.SkNocto aSkills -- powerful, but hard to boost over aSight EqpSlotWeaponFast -> error $ "" `showFailure` arItem -- sum of all benefits EqpSlotWeaponBig -> error $ "" `showFailure` arItem -- sum of all benefits itemTimerZero :: ItemTimer itemTimerZero = ItemTimer timeZero createItemTimer :: Time -> Delta Time -> ItemTimer createItemTimer localTime delta = ItemTimer $ localTime `timeShift` delta shiftItemTimer :: Delta Time -> ItemTimer -> ItemTimer shiftItemTimer delta t = ItemTimer $ itemTimer t `timeShift` delta deltaOfItemTimer :: Time -> ItemTimer -> Delta Time deltaOfItemTimer localTime t = timeDeltaToFrom (itemTimer t) localTime charging :: Time -> ItemTimer -> Bool charging localTime = (> localTime) . itemTimer ncharges :: Time -> ItemQuant -> Int ncharges localTime (itemK, itemTimers) = itemK - length (filter (charging localTime) itemTimers) hasCharge :: Time -> ItemQuant -> Bool hasCharge localTime kit = ncharges localTime kit > 0 strongestMelee :: Bool -> Maybe DiscoveryBenefit -> Time -> [(ItemId, ItemFullKit)] -> [(Double, Bool, Int, Int, ItemId, ItemFullKit)] strongestMelee _ _ _ [] = [] strongestMelee ignoreCharges mdiscoBenefit localTime kitAss = -- For fighting, as opposed to equipping, we value weapon only for -- its raw damage and harming effects and at this very moment only, -- not in the future. Hehce, we exclude discharged weapons. let f (iid, ifk@(itemFull, kit)) = let rawDmg = IK.damageUsefulness $ itemKind itemFull unIDedBonus = if itemSuspect itemFull then 1000 else 0 totalValue = case mdiscoBenefit of Just discoBenefit -> let Benefit{benMelee} = discoBenefit EM.! iid in benMelee - unIDedBonus Nothing -> - rawDmg -- special case: not interested about ID arItem = aspectRecordFull itemFull timeout = IA.aTimeout arItem -- This is crucial for weapons for which AI is too silly -- to value the effects at more than 0, even though they are strong -- and also to prefer weapons with burn or wound over pure damage, -- which is a good rule of thumb before late game at least. hasEffect = any IK.forApplyEffect (IK.ieffects $ itemKind itemFull) ncha = ncharges localTime kit in if ignoreCharges || ncha > 0 then Just (totalValue, hasEffect, timeout, ncha, iid, ifk) else Nothing -- We can't filter out weapons that are not harmful to victim -- (@benMelee >= 0), because actors use them if nothing else available, -- e.g., geysers, bees. This is intended and fun. in sortOn (\(value, hasEffect, timeout, _, _, (itemFull, _)) -> -- Weapon with higher timeout activated first to increase -- the chance of using it again during this fight. -- No timeout is ever better, because no wait incurred. -- Optimal packing problem: start with the biggest. let timN = if timeout == 0 then -99999 else - timeout in (value, not hasEffect, timN, itemKindId itemFull)) (mapMaybe f kitAss) unknownAspect :: (IK.Aspect -> [Dice.Dice]) -> ItemFull -> Bool unknownAspect f itemFull@ItemFull{itemKind=IK.ItemKind{iaspects}, ..} = case itemDisco of ItemDiscoMean IA.KindMean{kmConst} -> let arItem = aspectRecordFull itemFull unknown x = let (minD, maxD) = Dice.infsupDice x in minD /= maxD in itemSuspect && not (IA.checkFlag Ability.MinorAspects arItem) || not kmConst && or (concatMap (map unknown . f) iaspects) ItemDiscoFull{} -> False -- all known -- We assume @SkHurtMelee@ never appears inside @Odds@. If it does, -- not much harm. unknownMeleeBonus :: [ItemFull] -> Bool unknownMeleeBonus = let p (IK.AddSkill Ability.SkHurtMelee k) = [k] p _ = [] f itemFull b = b || unknownAspect p itemFull in foldr f False -- We assume @SkSpeed@ never appears inside @Odds@. If it does, -- not much harm. unknownSpeedBonus :: [ItemFull] -> Bool unknownSpeedBonus = let p (IK.AddSkill Ability.SkSpeed k) = [k] p _ = [] f itemFull b = b || unknownAspect p itemFull in foldr f False conditionMeleeBonus :: [ItemFullKit] -> Int conditionMeleeBonus kitAss = let f (itemFull, (itemK, _)) k = let arItem = aspectRecordFull itemFull in if IA.checkFlag Ability.Condition arItem then k + itemK * IA.getSkill Ability.SkHurtMelee arItem else k in foldr f 0 kitAss conditionSpeedBonus :: [ItemFullKit] -> Int conditionSpeedBonus kitAss = let f (itemFull, (itemK, _)) k = let arItem = aspectRecordFull itemFull in if IA.checkFlag Ability.Condition arItem then k + itemK * IA.getSkill Ability.SkSpeed arItem else k in foldr f 0 kitAss -- | Damage calculation. The armor and hurt skills are additive. -- They can't be multiplicative, because then 100% armor would minimize -- damage regardless of even 200% hurt skill. -- However, additive skills make the relative effectiveness of weapons -- dependent on the enemy, so even with -100% hurt skill a kinetic weapon -- can't be removed from the list, because an enemy may have -- negative armor skill. This is bad, but also KISS. armorHurtCalculation :: Bool -> Ability.Skills -> Ability.Skills -> Int armorHurtCalculation proj sMaxSk tMaxSk = let trim200 n = min 200 $ max (-200) n itemBonus = trim200 (Ability.getSk Ability.SkHurtMelee sMaxSk) - if proj then trim200 (Ability.getSk Ability.SkArmorRanged tMaxSk) else trim200 (Ability.getSk Ability.SkArmorMelee tMaxSk) in 100 + max (-95) itemBonus -- at least 5% of damage gets through mergeItemQuant :: ItemQuant -> ItemQuant -> ItemQuant mergeItemQuant (k2, it2) (k1, it1) = (k1 + k2, it1 ++ it2) listToolsToConsume :: [(ItemId, ItemFullKit)] -> [(ItemId, ItemFullKit)] -> [((CStore, Bool), (ItemId, ItemFullKit))] listToolsToConsume kitAssG kitAssE = let isDurable = IA.checkFlag Ability.Durable . aspectRecordFull . fst . snd (kitAssGT, kitAssGF) = partition isDurable kitAssG (kitAssET, kitAssEF) = partition isDurable kitAssE -- Non-durable tools take precedence, because durable -- are applied and, usually being weapons, -- may be harmful or may have unintended effects. -- CGround takes precedence, too. in zip (repeat (CGround, False)) kitAssGF ++ zip (repeat (CEqp, False)) kitAssEF ++ zip (repeat (CGround, True)) kitAssGT ++ zip (repeat (CEqp, True)) kitAssET countIidConsumed :: ItemFullKit -> [(Bool, Int, GroupName IK.ItemKind)] -> (Int, Int, [(Bool, Int, GroupName IK.ItemKind)]) countIidConsumed (ItemFull{itemKind}, (k, _)) grps0 = let hasGroup grp = maybe False (> 0) $ lookup grp $ IK.ifreq itemKind matchGroup (nToApplyIfDurable, nToDestroyAlways, grps) (destroyAlways, n, grp) = if hasGroup grp then let mkn = min k n -- even if durable, use each copy only once grps2 = if n - mkn > 0 then (destroyAlways, n - mkn, grp) : grps else grps in if destroyAlways then ( nToApplyIfDurable , max nToDestroyAlways mkn , grps2 ) else ( max nToApplyIfDurable mkn , nToDestroyAlways , grps2 ) else ( nToApplyIfDurable , nToDestroyAlways , (destroyAlways, n, grp) : grps ) in foldl' matchGroup (0, 0, []) grps0 subtractIidfromGrps :: ( EM.EnumMap CStore ItemBag , [(CStore, (ItemId, ItemFull))] , [(Bool, Int, GroupName IK.ItemKind)] ) -> ((CStore, Bool), (ItemId, ItemFullKit)) -> ( EM.EnumMap CStore ItemBag , [(CStore, (ItemId, ItemFull))] , [(Bool, Int, GroupName IK.ItemKind)] ) subtractIidfromGrps (bagsToLose1, iidsToApply1, grps1) ((store, durable), (iid, itemFullKit@(itemFull, (_, it)))) = let (nToApplyIfDurable, nToDestroyAlways, grps2) = countIidConsumed itemFullKit grps1 (nToApply, nToDestroy) = if durable then (nToApplyIfDurable, nToDestroyAlways) else (0, max nToApplyIfDurable nToDestroyAlways) in ( if nToDestroy == 0 then bagsToLose1 -- avoid vacuus @UpdDestroyItem@ else let kit2 = (nToDestroy, take nToDestroy it) removedBags = EM.singleton store $ EM.singleton iid kit2 in EM.unionWith (EM.unionWith mergeItemQuant) removedBags bagsToLose1 , replicate nToApply (store, (iid, itemFull)) ++ iidsToApply1 , grps2 ) sortIids :: (ItemId -> ItemFull) -> [(ItemId, ItemQuant)] -> [(ItemId, ItemQuant)] sortIids itemToF = -- If appearance and aspects the same, keep the order from before sort. let kindAndAppearance (iid, _) = let ItemFull{itemBase=Item{..}, ..} = itemToF iid in ( not itemSuspect, itemKindId, itemDisco , IK.isymbol itemKind, IK.iname itemKind , jflavour, jfid ) in sortOn kindAndAppearance data TileAction = EmbedAction (ItemId, ItemQuant) | ToAction (GroupName TK.TileKind) | WithAction [(Int, GroupName IK.ItemKind)] (GroupName TK.TileKind) deriving Show parseTileAction :: Bool -> Bool -> [(IK.ItemKind, (ItemId, ItemQuant))] -> TK.Feature -> Maybe TileAction parseTileAction bproj underFeet embedKindList feat = case feat of TK.Embed igroup -> -- Greater or equal 0 to also cover template UNKNOWN items -- not yet identified by the client. let f (itemKind, _) = fromMaybe (-1) (lookup igroup $ IK.ifreq itemKind) >= 0 in case find f embedKindList of Nothing -> Nothing Just (_, iidkit) -> Just $ EmbedAction iidkit TK.OpenTo tgroup | not (underFeet || bproj) -> Just $ ToAction tgroup TK.CloseTo tgroup | not (underFeet || bproj) -> Just $ ToAction tgroup TK.ChangeTo tgroup | not bproj -> Just $ ToAction tgroup TK.OpenWith proj grps tgroup | not underFeet -> if proj == TK.ProjNo && bproj then Nothing else Just $ WithAction grps tgroup TK.CloseWith proj grps tgroup | not underFeet -> -- Not when standing on tile, not to autoclose doors under actor -- or close via dropping an item inside. if proj == TK.ProjNo && bproj then Nothing else Just $ WithAction grps tgroup TK.ChangeWith proj grps tgroup -> if proj == TK.ProjNo && bproj then Nothing else Just $ WithAction grps tgroup _ -> Nothing LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/ItemAspect.hs0000644000000000000000000001673507346545000022577 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | The type of item aspects and its operations. module Game.LambdaHack.Common.ItemAspect ( AspectRecord(..), KindMean(..) , emptyAspectRecord, addMeanAspect, castAspect, aspectsRandom , aspectRecordToList, rollAspectRecord, getSkill, checkFlag, meanAspect , onlyMinorEffects, itemTrajectory, totalRange, isHumanTrinket , goesIntoEqp, loreFromContainer #ifdef EXPOSE_INTERNAL -- * Internal operations , ceilingMeanDice #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.Trans.State.Strict as St import Data.Binary import qualified Data.EnumSet as ES import Data.Hashable (Hashable) import qualified Data.Text as T import GHC.Generics (Generic) import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | Record of skills conferred by an item as well as of item flags -- and other item aspects. data AspectRecord = AspectRecord { aTimeout :: Int , aSkills :: Ability.Skills , aFlags :: Ability.Flags , aELabel :: Text , aToThrow :: IK.ThrowMod , aPresentAs :: Maybe (GroupName IK.ItemKind) , aEqpSlot :: Maybe Ability.EqpSlot } deriving (Show, Eq, Ord, Generic) instance Hashable AspectRecord instance Binary AspectRecord -- | Partial information about an item, deduced from its item kind. -- These are assigned to each 'IK.ItemKind'. The @kmConst@ flag says whether -- the item's aspect record is constant rather than random or dependent -- on item creation dungeon level. data KindMean = KindMean { kmConst :: Bool -- ^ whether the item doesn't need second identification , kmMean :: AspectRecord -- ^ mean value of item's possible aspect records } deriving (Show, Eq, Ord) emptyAspectRecord :: AspectRecord emptyAspectRecord = AspectRecord { aTimeout = 0 , aSkills = Ability.zeroSkills , aFlags = Ability.Flags ES.empty , aELabel = "" , aToThrow = IK.ThrowMod 100 100 1 , aPresentAs = Nothing , aEqpSlot = Nothing } castAspect :: Dice.AbsDepth -> Dice.AbsDepth -> AspectRecord -> IK.Aspect -> Rnd AspectRecord castAspect !ldepth !totalDepth !ar !asp = case asp of IK.Timeout d -> do n <- castDice ldepth totalDepth d return $! assert (aTimeout ar == 0) $ ar {aTimeout = n} IK.AddSkill sk d -> do n <- castDice ldepth totalDepth d return $! if n /= 0 then ar {aSkills = Ability.addSk sk n (aSkills ar)} else ar IK.SetFlag feat -> return $! ar {aFlags = Ability.Flags $ ES.insert feat (Ability.flags $ aFlags ar)} IK.ELabel t -> return $! ar {aELabel = t} IK.ToThrow tt -> return $! ar {aToThrow = tt} IK.PresentAs ha -> return $! ar {aPresentAs = Just ha} IK.EqpSlot slot -> return $! ar {aEqpSlot = Just slot} IK.Odds d aspects1 aspects2 -> do pick1 <- oddsDice ldepth totalDepth d foldlM' (castAspect ldepth totalDepth) ar $ if pick1 then aspects1 else aspects2 -- If @False@, aspects of this kind are most probably fixed, not random -- nor dependent on dungeon level where the item is created. aspectsRandom :: [IK.Aspect] -> Bool aspectsRandom ass = let rollM depth = foldlM' (castAspect (Dice.AbsDepth depth) (Dice.AbsDepth 10)) emptyAspectRecord ass gen = SM.mkSMGen 0 (ar0, gen0) = St.runState (rollM 0) gen (ar1, gen1) = St.runState (rollM 10) gen0 in show gen /= show gen0 || show gen /= show gen1 || ar0 /= ar1 addMeanAspect :: AspectRecord -> IK.Aspect -> AspectRecord addMeanAspect !ar !asp = case asp of IK.Timeout d -> let n = ceilingMeanDice d in assert (aTimeout ar == 0) $ ar {aTimeout = n} IK.AddSkill sk d -> let n = ceilingMeanDice d in if n /= 0 then ar {aSkills = Ability.addSk sk n (aSkills ar)} else ar IK.SetFlag feat -> ar {aFlags = Ability.Flags $ ES.insert feat (Ability.flags $ aFlags ar)} IK.ELabel t -> ar {aELabel = t} IK.ToThrow tt -> ar {aToThrow = tt} IK.PresentAs ha -> ar {aPresentAs = Just ha} IK.EqpSlot slot -> ar {aEqpSlot = Just slot} IK.Odds{} -> ar -- can't tell, especially since we don't know the level ceilingMeanDice :: Dice.Dice -> Int ceilingMeanDice d = ceiling $ Dice.meanDice d aspectRecordToList :: AspectRecord -> [IK.Aspect] aspectRecordToList AspectRecord{..} = [IK.Timeout $ Dice.intToDice aTimeout | aTimeout /= 0] ++ [ IK.AddSkill sk $ Dice.intToDice n | (sk, n) <- Ability.skillsToList aSkills ] ++ [IK.SetFlag feat | feat <- ES.elems $ Ability.flags aFlags] ++ [IK.ELabel aELabel | not $ T.null aELabel] ++ [IK.ToThrow aToThrow | aToThrow /= IK.ThrowMod 100 100 1] ++ maybe [] (\ha -> [IK.PresentAs ha]) aPresentAs ++ maybe [] (\slot -> [IK.EqpSlot slot]) aEqpSlot rollAspectRecord :: [IK.Aspect] -> Dice.AbsDepth -> Dice.AbsDepth -> Rnd AspectRecord rollAspectRecord ass ldepth totalDepth = foldlM' (castAspect ldepth totalDepth) emptyAspectRecord ass getSkill :: Ability.Skill -> AspectRecord -> Int {-# INLINE getSkill #-} getSkill sk ar = Ability.getSk sk $ aSkills ar checkFlag :: Ability.Flag -> AspectRecord -> Bool {-# INLINE checkFlag #-} checkFlag flag ar = Ability.checkFl flag (aFlags ar) meanAspect :: IK.ItemKind -> AspectRecord meanAspect kind = foldl' addMeanAspect emptyAspectRecord (IK.iaspects kind) -- Kinetic damage is not considered major effect, even though it -- identifies an item, when one hits with it. However, it's tedious -- to wait for weapon identification until first hit and also -- if a weapon is periodically activated, the kinetic damage would not apply, -- so we'd need special cases that force identification or warn -- or here not consider kinetic damage a major effect if item is periodic. -- So we opt for KISS and identify effect-less weapons at pick-up, -- not at first hit. onlyMinorEffects :: AspectRecord -> IK.ItemKind -> Bool onlyMinorEffects ar kind = checkFlag Ability.MinorEffects ar -- override || all IK.alwaysDudEffect (IK.ieffects kind) -- exhibits no major effects itemTrajectory :: AspectRecord -> IK.ItemKind -> [Point] -> ([Vector], (Speed, Int)) itemTrajectory ar itemKind path = let IK.ThrowMod{..} = aToThrow ar in computeTrajectory (IK.iweight itemKind) throwVelocity throwLinger path totalRange :: AspectRecord -> IK.ItemKind -> Int totalRange ar itemKind = snd $ snd $ itemTrajectory ar itemKind [] isHumanTrinket :: IK.ItemKind -> Bool isHumanTrinket itemKind = maybe False (> 0) $ lookup IK.VALUABLE $ IK.ifreq itemKind -- risk from treasure hunters goesIntoEqp :: AspectRecord -> Bool goesIntoEqp ar = checkFlag Ability.Equipable ar || checkFlag Ability.Meleeable ar loreFromContainer :: AspectRecord -> Container -> SLore loreFromContainer arItem c = case c of CFloor{} -> SItem CEmbed{} -> SEmbed CActor _ store -> if | checkFlag Ability.Blast arItem -> SBlast | checkFlag Ability.Condition arItem -> SCondition | otherwise -> loreFromMode $ MStore store CTrunk{} -> if checkFlag Ability.Blast arItem then SBlast else STrunk LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/JSFile.hs0000644000000000000000000000775407346545000021656 0ustar0000000000000000{-# LANGUAGE JavaScriptFFI #-} -- | Saving/loading to JS storeage, mimicking operations on files. module Game.LambdaHack.Common.JSFile ( #ifdef USE_JSFILE -- to molify doctest, but don't break stylish-haskell parsing encodeEOF, strictDecodeEOF , tryCreateDir, doesFileExist, tryWriteFile, readFile, renameFile #endif ) where #ifdef USE_JSFILE import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.ByteString.Lazy.Char8 as LBS import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Data.Version import qualified Data.JSString as JSString import Data.JSString.Text (textToJSString) import GHCJS.DOM (currentWindow) import GHCJS.DOM.Storage (getItem, removeItem, setItem) import GHCJS.DOM.Types (JSString, runDOM) import GHCJS.DOM.Window (getLocalStorage) foreign import javascript safe "$r = LZString.compressToUTF16($1);" compressToUTF16 :: JSString -> IO JSString foreign import javascript safe "$r = LZString.decompressFromUTF16($1);" decompressFromUTF16 :: JSString -> IO JSString -- | Serialize and save data with an EOF marker, compressing. -- We treat the bytestring as Latin1 characters and so ensure -- we never run into illegal characters in the aribtrary binary data, -- unlike when treating it as UTF16 characters. This is also reasonably fast. -- The @OK@ is used as an EOF marker to ensure any apparent problems with -- corrupted files are reported to the user ASAP. encodeEOF :: Binary b => FilePath -> Version -> b -> IO () encodeEOF path v b = flip runDOM undefined $ do Just win <- currentWindow storage <- getLocalStorage win let t = decodeLatin1 $ LBS.toStrict $ encode (v, (encode b, "OK" :: String)) item <- compressToUTF16 $ textToJSString t setItem storage path item -- | Read and deserialize data with an EOF marker. -- The @OK@ EOF marker ensures any easily detectable file corruption -- is discovered and reported before any value is decoded from -- the second component. -- OTOH, binary encoding corruption is not discovered until a version -- check elsewhere ensures that binary formats are compatible. strictDecodeEOF :: Binary b => FilePath -> IO (Version, b) strictDecodeEOF path = flip runDOM undefined $ do Just win <- currentWindow storage <- getLocalStorage win Just item <- getItem storage path t <- decompressFromUTF16 item -- TODO: is @LBS.toLazy . encodeUtf8 . textFromJSString@ faster and correct? let c1 = LBS.pack $ JSString.unpack t (v1, (c2, s)) = decode c1 return $! if s == ("OK" :: String) then (v1, decode c2) else error $ "Fatal error: corrupted file " ++ path -- | Try to create a directory; not needed with local storage in JS. tryCreateDir :: FilePath -> IO () tryCreateDir _dir = return () doesFileExist :: FilePath -> IO Bool doesFileExist path = flip runDOM undefined $ do Just win <- currentWindow storage <- getLocalStorage win mitem <- getItem storage path let fileExists = isJust (mitem :: Maybe String) return $! fileExists tryWriteFile :: FilePath -> String -> IO () tryWriteFile path content = flip runDOM undefined $ do Just win <- currentWindow storage <- getLocalStorage win mitem <- getItem storage path let fileExists = isJust (mitem :: Maybe String) unless fileExists $ setItem storage path $ T.unpack content readFile :: FilePath -> IO String readFile path = flip runDOM undefined $ do Just win <- currentWindow storage <- getLocalStorage win mitem <- getItem storage path case mitem of Nothing -> fail $ "Fatal error: no file " ++ path Just item -> return item renameFile :: FilePath -> FilePath -> IO () renameFile path path2 = flip runDOM undefined $ do Just win <- currentWindow storage <- getLocalStorage win mitem <- getItem storage path case mitem :: Maybe String of Nothing -> fail $ "Fatal error: no file " ++ path Just item -> do setItem storage path2 item -- overwrites removeItem storage path #endif LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Kind.hs0000644000000000000000000001347707346545000021426 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | General content types and operations. module Game.LambdaHack.Common.Kind ( ContentData -- re-exported without some operations , COps(..) , emptyCOps , ItemSpeedup , getKindMean, speedupItem , okind, omemberGroup, oisSingletonGroup, ouniqGroup, opick , ofoldlWithKey', ofoldlGroup', omapVector, oimapVector , olength, linearInterpolation #ifdef EXPOSE_INTERNAL , emptyMultiGroupItem, emptyUnknownTile , emptyUIFactionGroupName, emptyMultiGroupMode #endif -- * Operations both internal and used in unit tests , emptyUIFaction ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Vector as V import qualified Game.LambdaHack.Common.ItemAspect as IA import qualified Game.LambdaHack.Common.Tile as Tile import qualified Game.LambdaHack.Content.CaveKind as CK import qualified Game.LambdaHack.Content.FactionKind as FK import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.ModeKind as MK import qualified Game.LambdaHack.Content.PlaceKind as PK import qualified Game.LambdaHack.Content.RuleKind as RK import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.DefsInternal (GroupName (GroupName), toContentSymbol) import Game.LambdaHack.Definition.Flavour (dummyFlavour) -- | Operations for all content types, gathered together. -- -- Warning: this type is not abstract, but its values should not be -- created ad hoc, even for unit tests, but should be constructed -- with @makeData@ for each particular content kind, which includes validation, -- and with @speedupItem@, etc., to ensure internal consistency. -- -- The @emptyCOps@ is one such valid by construction value of this type, -- except for the @cocave@ field. It's suitable for bootstrapping -- and for tests not involving dungeon generation from cave templates. data COps = COps { cocave :: ContentData CK.CaveKind -- server only , cofact :: ContentData FK.FactionKind , coitem :: ContentData IK.ItemKind , comode :: ContentData MK.ModeKind -- server only , coplace :: ContentData PK.PlaceKind -- server only, so far , corule :: RK.RuleContent , cotile :: ContentData TK.TileKind , coItemSpeedup :: ItemSpeedup , coTileSpeedup :: Tile.TileSpeedup } instance Show COps where show _ = "game content" instance Eq COps where (==) _ _ = True emptyMultiGroupItem :: IK.ItemKind emptyMultiGroupItem = IK.ItemKind { isymbol = toContentSymbol 'E' , iname = "emptyCOps item" , ifreq = map (, 1) $ IK.mandatoryGroups ++ IK.mandatoryGroupsSingleton , iflavour = [dummyFlavour] , icount = 0 , irarity = [] , iverbHit = "" , iweight = 0 , idamage = 0 , iaspects = [] , ieffects = [] , idesc = "" , ikit = [] } emptyUnknownTile :: TK.TileKind emptyUnknownTile = TK.TileKind -- needs to have index 0 and alter 1 { tsymbol = 'E' , tname = "unknown space" -- name checked in validation , tfreq = map (, 1) $ TK.mandatoryGroups ++ TK.mandatoryGroupsSingleton , tcolor = Color.BrMagenta , tcolor2 = Color.BrMagenta , talter = 1 , tfeature = [] } emptyUIFactionGroupName :: GroupName FK.FactionKind emptyUIFactionGroupName = GroupName "emptyUIFaction" emptyUIFaction :: FK.FactionKind emptyUIFaction = FK.FactionKind { fname = "emptyUIFaction" , ffreq = [(emptyUIFactionGroupName, 1)] , fteam = FK.TeamContinuity 999 -- must be > 0 , fgroups = [] , fskillsOther = Ability.zeroSkills , fcanEscape = False , fneverEmpty = True -- to keep the dungeon alive , fhiCondPoly = [] , fhasGender = False , finitDoctrine = Ability.TBlock , fspawnsFast = False , fhasPointman = False , fhasUI = True -- to own the UI frontend , finitUnderAI = False , fenemyTeams = [] , falliedTeams = [] } emptyMultiGroupMode :: MK.ModeKind emptyMultiGroupMode = MK.ModeKind { mname = "emptyMultiGroupMode" , mfreq = map (, 1) MK.mandatoryGroups , mtutorial = False , mattract = False , mroster = [(emptyUIFactionGroupName, [])] , mcaves = [] , mendMsg = [] , mrules = "" , mdesc = "" , mreason = "" , mhint = "" } -- | This is as empty, as possible, but still valid content, except for -- @cocave@ which is empty and not valid (making it valid would require -- bloating most other contents). emptyCOps :: COps emptyCOps = let corule = RK.emptyRuleContent coitem = IK.makeData (RK.ritemSymbols corule) [emptyMultiGroupItem] [] [] cotile = TK.makeData [emptyUnknownTile] [] [] cofact = FK.makeData [emptyUIFaction] [emptyUIFactionGroupName] [] in COps { cocave = emptyContentData -- not valid! beware when testing! -- to make valid cave content, we'd need to define a single cave kind, -- which involves creating and validating tile and place kinds, etc. , cofact , coitem , comode = MK.makeData cofact [emptyMultiGroupMode] [] [] , coplace = PK.makeData cotile [] [] [] , corule , cotile , coItemSpeedup = speedupItem coitem , coTileSpeedup = Tile.speedupTile False cotile } -- | Map from an item kind identifier to the mean aspect value for the kind. newtype ItemSpeedup = ItemSpeedup (V.Vector IA.KindMean) getKindMean :: ContentId IK.ItemKind -> ItemSpeedup -> IA.KindMean getKindMean kindId (ItemSpeedup is) = is V.! contentIdIndex kindId speedupItem :: ContentData IK.ItemKind -> ItemSpeedup speedupItem coitem = let f !kind = let kmMean = IA.meanAspect kind kmConst = not $ IA.aspectsRandom (IK.iaspects kind) in IA.KindMean{..} in ItemSpeedup $ omapVector coitem f LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Level.hs0000644000000000000000000003412607346545000021602 0ustar0000000000000000-- | Inhabited dungeon levels and the operations to query and change them -- as the game progresses. module Game.LambdaHack.Common.Level ( -- * Dungeon Dungeon, dungeonBounds, ascendInBranch, whereTo -- * The @Level@ type and its components , ItemFloor, BigActorMap, ProjectileMap, TileMap, SmellMap, Level(..) -- * Component updates , updateFloor, updateEmbed, updateBigMap, updateProjMap , updateTile, updateEntry, updateSmell -- * Level query , at , posToBigLvl, occupiedBigLvl, posToProjsLvl, occupiedProjLvl, posToAidsLvl , findPosTry, findPosTry2, nearbyPassablePoints, nearbyFreePoints -- * Misc , sortEmbeds #ifdef EXPOSE_INTERNAL -- * Internal operations , EntryMap , assertSparseItems, assertSparseProjectiles #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.CaveKind (CaveKind) import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.PlaceKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Defs -- | The complete dungeon is a map from level identifiers to levels. type Dungeon = EM.EnumMap LevelId Level dungeonBounds :: Dungeon -> (LevelId, LevelId) dungeonBounds dungeon | Just ((s, _), _) <- EM.minViewWithKey dungeon , Just ((e, _), _) <- EM.maxViewWithKey dungeon = (s, e) dungeonBounds dungeon = error $ "empty dungeon" `showFailure` dungeon -- | Levels in the current branch, one level up (or down) from the current. ascendInBranch :: Dungeon -> Bool -> LevelId -> [LevelId] ascendInBranch dungeon up lid = -- Currently there is just one branch, so the computation is simple. let (minD, maxD) = dungeonBounds dungeon ln = max minD $ min maxD $ toEnum $ fromEnum lid + if up then 1 else -1 in case EM.lookup ln dungeon of Just _ | ln /= lid -> [ln] _ | ln == lid -> [] _ -> ascendInBranch dungeon up ln -- jump over gaps -- | Compute the level identifier and stair position on the new level, -- after a level change. -- -- We assume there is never a staircase up and down at the same position. whereTo :: LevelId -- ^ level of the stairs -> Point -- ^ position of the stairs -> Bool -- ^ optional forced direction -> Dungeon -- ^ current game dungeon -> [(LevelId, Point)] -- ^ possible destinations whereTo lid pos up dungeon = let lvl = dungeon EM.! lid li = case elemIndex pos $ fst $ lstair lvl of Just ifst -> assert up [ifst] Nothing -> case elemIndex pos $ snd $ lstair lvl of Just isnd -> assert (not up) [isnd] Nothing -> let forcedPoss = (if up then fst else snd) (lstair lvl) in [0 .. length forcedPoss - 1] -- for ascending via, e.g., spells in case ascendInBranch dungeon up lid of [] -> [] -- spell fizzles ln : _ -> let lvlDest = dungeon EM.! ln stairsDest = (if up then snd else fst) (lstair lvlDest) posAtIndex i = case drop i stairsDest of [] -> error $ "not enough stairs:" `showFailure` (ln, i + 1) p : _ -> (ln, p) in map posAtIndex li -- | Items located on map tiles. type ItemFloor = EM.EnumMap Point ItemBag -- | Big actors located on map tiles. type BigActorMap = EM.EnumMap Point ActorId -- | Collections of projectiles located on map tiles. type ProjectileMap = EM.EnumMap Point [ActorId] -- | Tile kinds on the map. type TileMap = PointArray.Array (ContentId TileKind) -- | Current smell on map tiles. type SmellMap = EM.EnumMap Point Time -- | Entries of places on the map. type EntryMap = EM.EnumMap Point PlaceEntry -- | A view on single, inhabited dungeon level. "Remembered" fields -- carry a subset of the info in the client copies of levels. data Level = Level { lkind :: ContentId CaveKind -- ^ the kind of cave the level is an instance of , ldepth :: Dice.AbsDepth -- ^ absolute depth of the level , lfloor :: ItemFloor -- ^ remembered items lying on the floor , lembed :: ItemFloor -- ^ remembered items embedded in the tile , lbig :: BigActorMap -- ^ seen big (non-projectile) actors at positions -- on the level; -- could be recomputed at resume, but small enough , lproj :: ProjectileMap -- ^ seen projectiles at positions on the level; -- could be recomputed at resume , ltile :: TileMap -- ^ remembered level map , lentry :: EntryMap -- ^ room entrances on the level , larea :: Area -- ^ area of the level , lsmell :: SmellMap -- ^ remembered smells on the level , lstair :: ([Point], [Point]) -- ^ positions of (up, down) stairs , lescape :: [Point] -- ^ positions of IK.Escape tiles , lseen :: Int -- ^ currently remembered clear tiles , lexpl :: Int -- ^ total number of explorable tiles , ltime :: Time -- ^ local time on the level (possibly frozen) , lnight :: Bool -- ^ whether the level is covered in darkness } deriving (Show, Eq) assertSparseItems :: ItemFloor -> ItemFloor assertSparseItems m = assert (EM.null (EM.filter EM.null m) `blame` "null floors found" `swith` m) m hashConsSingle :: ItemFloor -> ItemFloor hashConsSingle = EM.map (EM.map (\case (1, []) -> quantSingle kit -> kit)) assertSparseProjectiles :: ProjectileMap -> ProjectileMap assertSparseProjectiles m = assert (EM.null (EM.filter null m) `blame` "null projectile lists found" `swith` m) m updateFloor :: (ItemFloor -> ItemFloor) -> Level -> Level {-# INLINE updateFloor #-} -- just in case inliner goes hiwire updateFloor f lvl = lvl {lfloor = f (lfloor lvl)} updateEmbed :: (ItemFloor -> ItemFloor) -> Level -> Level updateEmbed f lvl = lvl {lembed = f (lembed lvl)} updateBigMap :: (BigActorMap -> BigActorMap) -> Level -> Level updateBigMap f lvl = lvl {lbig = f (lbig lvl)} updateProjMap :: (ProjectileMap -> ProjectileMap) -> Level -> Level {-# INLINE updateProjMap #-} updateProjMap f lvl = lvl {lproj = f (lproj lvl)} updateTile :: (TileMap -> TileMap) -> Level -> Level updateTile f lvl = lvl {ltile = f (ltile lvl)} updateEntry :: (EntryMap -> EntryMap) -> Level -> Level updateEntry f lvl = lvl {lentry = f (lentry lvl)} updateSmell :: (SmellMap -> SmellMap) -> Level -> Level updateSmell f lvl = lvl {lsmell = f (lsmell lvl)} -- | Query for tile kinds on the map. at :: Level -> Point -> ContentId TileKind {-# INLINE at #-} at Level{ltile} p = ltile PointArray.! p posToBigLvl :: Point -> Level -> Maybe ActorId {-# INLINE posToBigLvl #-} posToBigLvl pos lvl = EM.lookup pos $ lbig lvl occupiedBigLvl :: Point -> Level -> Bool {-# INLINE occupiedBigLvl #-} occupiedBigLvl pos lvl = pos `EM.member` lbig lvl posToProjsLvl :: Point -> Level -> [ActorId] {-# INLINE posToProjsLvl #-} posToProjsLvl pos lvl = EM.findWithDefault [] pos $ lproj lvl occupiedProjLvl :: Point -> Level -> Bool {-# INLINE occupiedProjLvl #-} occupiedProjLvl pos lvl = pos `EM.member` lproj lvl posToAidsLvl :: Point -> Level -> [ActorId] {-# INLINE posToAidsLvl #-} posToAidsLvl pos lvl = maybeToList (posToBigLvl pos lvl) ++ posToProjsLvl pos lvl -- | Try to find a random position on the map satisfying -- conjunction of the mandatory and an optional predicate. -- If the permitted number of attempts is not enough, -- try again the same number of times without the next optional predicate, -- and fall back to trying with only the mandatory predicate. findPosTry :: Int -- ^ the number of tries -> Level -- ^ look up in this level -> (Point -> ContentId TileKind -> Bool) -- ^ mandatory predicate -> [Point -> ContentId TileKind -> Bool] -- ^ optional predicates -> Rnd (Maybe Point) {-# INLINE findPosTry #-} findPosTry numTries lvl m = findPosTry2 numTries lvl m [] undefined findPosTry2 :: Int -- ^ the number of tries -> Level -- ^ look up in this level -> (Point -> ContentId TileKind -> Bool) -- ^ mandatory predicate -> [Point -> ContentId TileKind -> Bool] -- ^ optional predicates -> (Point -> ContentId TileKind -> Bool) -- ^ good to have pred. -> [Point -> ContentId TileKind -> Bool] -- ^ worst case predicates -> Rnd (Maybe Point) {-# INLINE findPosTry2 #-} findPosTry2 numTries Level{ltile, larea} m0 l g r = assert (numTries > 0) $ let (Point x0 y0, xspan, yspan) = spanArea larea accomodate :: Rnd (Maybe Point) -> (Point -> ContentId TileKind -> Bool) -> [Point -> ContentId TileKind -> Bool] -> Rnd (Maybe Point) {-# INLINE accomodate #-} accomodate fallback m = go where go :: [Point -> ContentId TileKind -> Bool] -> Rnd (Maybe Point) go [] = fallback go (hd : tl) = search numTries where search 0 = go tl search !k = do pxyRelative <- randomR0 (xspan * yspan - 1) -- Here we can't use @fromEnum@ and/or work with the @Int@ -- representation, because the span is different than @rWidthMax@. let Point{..} = punindex xspan pxyRelative pos = Point (x0 + px) (y0 + py) tile = ltile PointArray.! pos if m pos tile && hd pos tile then return $ Just pos else search (k - 1) rAndOnceOnlym0 = r ++ [\_ _ -> True] in accomodate (accomodate (return Nothing) m0 rAndOnceOnlym0) -- @pos@ and @tile@ not always needed, so not strict; -- the function arguments determine that thanks to inlining. (\pos tile -> m0 pos tile && g pos tile) l -- | Generate a list of all passable points on (connected component of) -- the level in the order of path distance from the starting position (BFS). -- The starting position needn't be passable and is always included. nearbyPassablePoints :: COps -> Level -> Point -> [Point] nearbyPassablePoints cops@COps{corule=RuleContent{rWidthMax, rHeightMax}} lvl start = let passable p = Tile.isEasyOpen (coTileSpeedup cops) (lvl `at` p) -- The error is mostly probably caused by place content creating -- enclosed spaces in conjunction with map edges. To verify, -- change the error to @l@ and run with the same seed. semiRandomWrap l = if null l then error "nearbyPassablePoints: blocked" else let offset = fromEnum start `mod` length l in drop offset l ++ take offset l passableVic p = semiRandomWrap $ filter passable $ vicinityBounded rWidthMax rHeightMax p siftSingle :: Point -> (ES.EnumSet Point, [Point]) -> (ES.EnumSet Point, [Point]) siftSingle current (seen, sameDistance) = if current `ES.member` seen then (seen, sameDistance) else (ES.insert current seen, current : sameDistance) siftVicinity :: Point -> (ES.EnumSet Point, [Point]) -> (ES.EnumSet Point, [Point]) siftVicinity current seenAndSameDistance = let vic = passableVic current in foldr siftSingle seenAndSameDistance vic siftNearby :: (ES.EnumSet Point, [Point]) -> [Point] siftNearby (seen, sameDistance) = sameDistance ++ case foldr siftVicinity (seen, []) sameDistance of (_, []) -> [] (seen2, sameDistance2) -> siftNearby (seen2, sameDistance2) in siftNearby (ES.singleton start, [start]) nearbyFreePoints :: COps -> Level -> (ContentId TileKind -> Bool) -> Point -> [Point] nearbyFreePoints cops lvl f start = let good p = f (lvl `at` p) && Tile.isWalkable (coTileSpeedup cops) (lvl `at` p) && null (posToAidsLvl p lvl) in filter good $ nearbyPassablePoints cops lvl start -- We ignore stray embeds, not mentioned in the tile kind. -- OTOH, some of those mentioned may be used up and so not in the bag -- and it's OK. sortEmbeds :: COps -> ContentId TileKind -> [(IK.ItemKind, (ItemId, ItemQuant))] -> [(ItemId, ItemQuant)] sortEmbeds COps{cotile} tk embedKindList = let grpList = Tile.embeddedItems cotile tk -- Greater or equal 0 to also cover template UNKNOWN items -- not yet identified by the client. f grp (itemKind, _) = fromMaybe (-1) (lookup grp $ IK.ifreq itemKind) >= 0 in map snd $ mapMaybe (\grp -> find (f grp) embedKindList) grpList instance Binary Level where put Level{..} = do put lkind put ldepth put (assertSparseItems lfloor) put (assertSparseItems lembed) put lbig put (assertSparseProjectiles lproj) put ltile put lentry put larea put lsmell put lstair put lescape put lseen put lexpl put ltime put lnight get = do lkind <- get ldepth <- get lfloor <- hashConsSingle <$> get lembed <- hashConsSingle <$> get lbig <- get lproj <- get ltile <- get lentry <- get larea <- get lsmell <- get lstair <- get lescape <- get lseen <- get lexpl <- get ltime <- get lnight <- get return $! Level{..} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Misc.hs0000644000000000000000000001037307346545000021424 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Hacks that haven't found their home yet. module Game.LambdaHack.Common.Misc ( FontDefinition(..), HintingMode(..), FontSet(..) , makePhrase, makeSentence, squashedWWandW , appDataDir , xM, xD, minusM, minusM1, minusM2, oneM, tenthM , show64With2 , workaroundOnMainThreadMVar ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import Control.DeepSeq import Data.Binary import qualified Data.Char as Char import Data.Int (Int64) import qualified Data.Map as M import GHC.Generics (Generic) import qualified NLP.Miniutter.English as MU import System.Directory (getAppUserDataDirectory) import System.Environment (getProgName) import System.IO.Unsafe (unsafePerformIO) data FontDefinition = FontProportional Text Int HintingMode -- ^ filename, size, hinting mode | FontMonospace Text Int HintingMode | FontMapScalable Text Int HintingMode Int -- ^ extra cell extension | FontMapBitmap Text Int -- ^ size ignored for bitmap fonts and no hinting deriving (Show, Eq, Read, Generic) instance NFData FontDefinition instance Binary FontDefinition data HintingMode = HintingHeavy -- ^ current libfreetype6 default, thin, large letter spacing | HintingLight -- ^ mimics OTF, blurry, thick, tight tracking, accurate shape deriving (Show, Eq, Read, Generic) instance NFData HintingMode instance Binary HintingMode data FontSet = FontSet { fontMapScalable :: Text , fontMapBitmap :: Text , fontPropRegular :: Text , fontPropBold :: Text , fontMono :: Text } deriving (Show, Eq, Read, Generic) instance NFData FontSet instance Binary FontSet -- | Re-exported English phrase creation functions, applied to our custom -- irregular word sets. makePhrase, makeSentence :: [MU.Part] -> Text makePhrase = MU.makePhrase irregular makeSentence = MU.makeSentence irregular irregular :: MU.Irregular irregular = MU.Irregular { irrPlural = M.fromList [ ("merchandise", "merchandise") , ("Merchandise", "Merchandise") , ("stomach", "stomachs") ] -- this is both countable and uncountable, but I use it here -- only as uncountable, do I overwrite the default `M.union` MU.irrPlural MU.defIrregular , irrIndefinite = MU.irrIndefinite MU.defIrregular } -- | Apply the @WWandW@ constructor, first representing repetitions -- as @CardinalWs@. -- The parts are not sorted, only grouped, to keep the order. -- The internal structure of speech parts is compared, not their string -- rendering, so some coincidental clashes are avoided (and code is simpler). squashedWWandW :: [MU.Part] -> (MU.Part, MU.Person) squashedWWandW parts = let repetitions = group parts f [] = error $ "empty group" `showFailure` parts f [part] = (part, MU.Sg3rd) -- avoid prefixing hero names with "a" f l@(part : _) = (MU.CardinalWs (length l) part, MU.PlEtc) cars = map f repetitions person = case cars of [] -> error $ "empty cars" `showFailure` parts [(_, person1)] -> person1 _ -> MU.PlEtc in (MU.WWandW $ map fst cars, person) -- | Personal data directory for the game. Depends on the OS and the game, -- e.g., for LambdaHack under Linux it's @~\/.LambdaHack\/@. appDataDir :: IO FilePath appDataDir = do progName <- getProgName let name = takeWhile Char.isAlphaNum progName getAppUserDataDirectory name -- | Multiplies by a million. xM :: Int -> Int64 xM k = into @Int64 k * 1000000 -- | Multiplies by a million, double precision. xD :: Double -> Double xD k = k * 1000000 minusM, minusM1, minusM2, oneM, tenthM :: Int64 minusM = xM (-1) minusM1 = xM (-1) - 1 minusM2 = xM (-1) - 2 oneM = xM 1 tenthM = 100000 show64With2 :: Int64 -> Text show64With2 n = let k = 100 * n `divUp` oneM l = k `div` 100 x = k - l * 100 y = x `div` 10 in tshow l <> if | x == 0 -> "" | x == y * 10 -> "." <> tshow y | x < 10 -> ".0" <> tshow x | otherwise -> "." <> tshow x -- Global variable for passing the action to run on main thread, if any. workaroundOnMainThreadMVar :: MVar (IO ()) {-# NOINLINE workaroundOnMainThreadMVar #-} workaroundOnMainThreadMVar = unsafePerformIO newEmptyMVar LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/MonadStateRead.hs0000644000000000000000000001307507346545000023366 0ustar0000000000000000-- | Game state reading monad and basic operations. module Game.LambdaHack.Common.MonadStateRead ( MonadStateRead(..) , getState, getLevel , getGameMode, isNoConfirmsGame, getEntryArena, pickWeaponM, displayTaunt ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Either import qualified Data.EnumMap.Strict as EM import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability -- | Monad for reading game state. A state monad with state modification -- disallowed (another constraint is needed to permit that). -- The basic server and client monads are like that, because server -- and clients freely modify their internal session data, but don't modify -- the main game state, except in very restricted and synchronized way. class (Monad m, Functor m, Applicative m) => MonadStateRead m where getsState :: (State -> a) -> m a getState :: MonadStateRead m => m State getState = getsState id getLevel :: MonadStateRead m => LevelId -> m Level getLevel lid = getsState $ (EM.! lid) . sdungeon getGameMode :: MonadStateRead m => m ModeKind getGameMode = do COps{comode} <- getsState scops gameModeId <- getsState sgameModeId return $! okind comode gameModeId isNoConfirmsGame :: MonadStateRead m => m Bool isNoConfirmsGame = do gameMode <- getGameMode return $! mattract gameMode getEntryArena :: MonadStateRead m => Faction -> m LevelId getEntryArena fact = do dungeon <- getsState sdungeon let (minD, maxD) = dungeonBounds dungeon f [] = 0 f ((ln, _, _) : _) = ln return $! max minD $ min maxD $ toEnum $ f $ ginitial fact pickWeaponM :: MonadStateRead m => Bool -> Maybe DiscoveryBenefit -> [(ItemId, ItemFullKit)] -> Ability.Skills -> ActorId -> m [(Double, Bool, Int, Int, ItemId, ItemFullKit)] pickWeaponM ignoreCharges mdiscoBenefit kitAss actorSk source = do sb <- getsState $ getActorBody source localTime <- getsState $ getLocalTime (blid sb) actorMaxSk <- getsState $ getActorMaxSkills source let calmE = calmEnough sb actorMaxSk forced = bproj sb permitted = permittedPrecious forced calmE preferredPrecious = fromRight False . permitted permAssocs = filter (preferredPrecious . fst . snd) kitAss strongest = strongestMelee ignoreCharges mdiscoBenefit localTime permAssocs return $! if | forced -> map (\(iid, itemFullKit) -> (-1, False, 0, 1, iid, itemFullKit)) kitAss | Ability.getSk Ability.SkMelee actorSk <= 0 -> [] | otherwise -> strongest displayTaunt :: MonadStateRead m => Bool -> (Rnd (Text, Text) -> m (Text, Text)) -> ActorId -> m (Text, Text) displayTaunt _voluntary rndToAction aid = do b <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid let canApply = Ability.getSk Ability.SkApply actorMaxSk > 2 && canHear -- if applies complex items, probably intelligent and can speak canHear = Ability.getSk Ability.SkHearing actorMaxSk > 0 && canBrace -- if hears, probably also emits sound vocally; -- disabled even by ushanka and rightly so canBrace = Ability.getSk Ability.SkWait actorMaxSk >= 2 -- not an insect, plant, geyser, faucet, fence, etc. -- so can emit sound by hitting something with body parts || Ability.getSk Ability.SkApply actorMaxSk > 2 -- and neither an impatient intelligent actor braceUneasy = [ (2, ("something", "flail around")) , (1, ("something", "toss blindly")) , (1, ("something", "squirm dizzily")) ] braceEasy = [ (2, ("something", "stretch")) , (1, ("something", "fidget")) , (1, ("something", "fret")) ] uneasy = deltasSerious (bcalmDelta b) || not (calmEnough b actorMaxSk) if bwatch b `elem` [WSleep, WWake] then rndToAction $ frequency $ toFreq "SfxTaunt" $ if uneasy then if | canApply -> (5, ("somebody", "yell")) : (3, ("somebody", "bellow")) : braceUneasy | canHear -> (5, ("somebody", "bellow")) : (3, ("something", "hiss")) : braceUneasy | canBrace -> braceUneasy | otherwise -> [(1, ("something", "drone enquiringly"))] else if | canApply -> (5, ("somebody", "yawn")) : (3, ("somebody", "grunt")) : braceEasy | canHear -> (5, ("somebody", "grunt")) : (3, ("something", "wheeze")) : braceEasy | canBrace -> braceEasy | otherwise -> [(1, ("something", "hum silently"))] else return $! if | bproj b -> ("something", "ping") | canApply -> ("somebody", "holler a taunt") | canHear -> ("somebody", "growl menacingly") | canBrace -> ("something", "stomp repeatedly") | otherwise -> ("something", "buzz angrily") LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Perception.hs0000644000000000000000000000623607346545000022644 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Actors perceiving other actors and the dungeon level. -- -- Visibility works according to KISS. Everything that player sees is real. -- There are no unmarked hidden tiles and only solid tiles can be marked, -- so there are no invisible walls and to pass through an illusory wall, -- you have to use a turn bumping into it first. Only tiles marked with Suspect -- can turn out to be another tile. (So, if all tiles are marked with -- Suspect, the player knows nothing for sure, but this should be avoided, -- because searching becomes too time-consuming.) -- Each actor sees adjacent tiles, even when blind, so adjacent tiles are -- known, so the actor can decide accurately whether to pass thorugh -- or alter, etc. -- -- Items are always real and visible. Actors are real, but can be invisible. -- Invisible actors in walls can't be hit, but are hinted at when altering -- the tile, so the player can flee or block. Invisible actors in open -- space can be hit. module Game.LambdaHack.Common.Perception ( PerVisible(..) , PerSmelled(..) , Perception(..) , PerLid , PerFid , totalVisible, totalSmelled , emptyPer, nullPer, addPer, diffPer ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import GHC.Generics (Generic) import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Point -- | Visible positions. newtype PerVisible = PerVisible {pvisible :: ES.EnumSet Point} deriving (Show, Eq, Binary) -- | Smelled positions. newtype PerSmelled = PerSmelled {psmelled :: ES.EnumSet Point} deriving (Show, Eq, Binary) -- | The type representing the perception of a faction on a level. data Perception = Perception { psight :: PerVisible , psmell :: PerSmelled } deriving (Show, Eq, Generic) instance Binary Perception -- | Perception of a single faction, indexed by level identifier. type PerLid = EM.EnumMap LevelId Perception -- | Perception indexed by faction identifier. -- This can't be added to @FactionDict@, because clients can't see it -- for other factions. type PerFid = EM.EnumMap FactionId PerLid -- | The set of tiles visible by at least one hero. totalVisible :: Perception -> ES.EnumSet Point totalVisible = pvisible . psight -- | The set of tiles smelt by at least one hero. totalSmelled :: Perception -> ES.EnumSet Point totalSmelled = psmelled . psmell emptyPer :: Perception emptyPer = Perception { psight = PerVisible ES.empty , psmell = PerSmelled ES.empty } nullPer :: Perception -> Bool nullPer per = per == emptyPer addPer :: Perception -> Perception -> Perception addPer per1 per2 = Perception { psight = PerVisible $ totalVisible per1 `ES.union` totalVisible per2 , psmell = PerSmelled $ totalSmelled per1 `ES.union` totalSmelled per2 } diffPer :: Perception -> Perception -> Perception diffPer per1 per2 = Perception { psight = PerVisible $ totalVisible per1 ES.\\ totalVisible per2 , psmell = PerSmelled $ totalSmelled per1 ES.\\ totalSmelled per2 } LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Point.hs0000644000000000000000000001660507346545000021626 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Basic operations on 2D points represented as linear offsets. module Game.LambdaHack.Common.Point ( Point(..), PointI , chessDist, euclidDistSq, adjacent, bresenhamsLineAlgorithm, fromTo , originPoint, insideP , speedupHackXSize #ifdef EXPOSE_INTERNAL -- * Internal operations , bresenhamsLineAlgorithmBegin, balancedWord #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import Data.Int (Int32) import qualified Data.Primitive.PrimArray as PA import GHC.Generics (Generic) import Test.QuickCheck import Game.LambdaHack.Definition.Defs -- | This is a hack to pass the X size of the dungeon, defined -- in game content, to the @Enum@ instances of @Point@ and @Vector@. -- This is already slower and has higher allocation than -- hardcoding the value, so passing the value explicitly to -- a generalization of the @Enum@ conversions is out of the question. -- Perhaps this can be done cleanly and efficiently at link-time -- via Backpack, but it's probably not supported yet by GHCJS (not verified). -- For now, we need to be careful never to modify this array, -- except for setting it at program start before it's used for the first time. -- Which is easy, because @Point@ is never mentioned in content definitions. -- The @PrimArray@ has much smaller overhead than @IORef@ -- and reading from it looks cleaner, hence its use. speedupHackXSize :: PA.PrimArray X {-# NOINLINE speedupHackXSize #-} speedupHackXSize = PA.primArrayFromList [80] -- updated at program startup -- | 2D points in cartesian representation. Coordinates grow to the right -- and down, so that the (0, 0) point is in the top-left corner -- of the screen. Coordinates are never negative -- (unlike for 'Game.LambdaHack.Common.Vector.Vector') -- and the @X@ coordinate never reaches the screen width as read -- from 'speedupHackXSize'. data Point = Point { px :: X , py :: Y } deriving (Eq, Ord, Generic) instance Show Point where show (Point x y) = show (x, y) instance Binary Point where put = put . (toIntegralCrash :: Int -> Int32) . fromEnum get = fmap (toEnum . (fromIntegralWrap :: Int32 -> Int)) get -- `fromIntegralWrap` is fine here, because we converted the integer -- in the opposite direction first, so it fits even in 31 bit `Int` -- Note that @Ord@ on @Int@ is not monotonic wrt @Ord@ on @Point@. -- We need to keep it that way, because we want close xs to have close indexes, -- e.g., adjacent points in line to have adjacent enumerations, -- because some of the screen layout and most of processing is line-by-line. -- Consequently, one can use EM.fromDistinctAscList -- on @(1, 8)..(10, 8)@, but not on @(1, 7)..(10, 9)@. instance Enum Point where fromEnum Point{..} = let !xsize = PA.indexPrimArray speedupHackXSize 0 in #ifdef WITH_EXPENSIVE_ASSERTIONS assert (px >= 0 && py >= 0 && px < xsize `blame` "invalid point coordinates" `swith` (px, py)) #endif (px + py * xsize) toEnum n = let !xsize = PA.indexPrimArray speedupHackXSize 0 (py, px) = n `quotRem` xsize in Point{..} instance Arbitrary Point where arbitrary = do let xsize = PA.indexPrimArray speedupHackXSize 0 n <- getSize Point <$> choose (0, min n (xsize - 1)) <*> choose (0, n) -- | Enumeration representation of @Point@. type PointI = Int -- This is hidden from Haddock, but run by doctest: -- $ -- prop> (toEnum :: PointI -> Point) (fromEnum p) == p -- prop> \ (NonNegative i) -> (fromEnum :: Point -> PointI) (toEnum i) == i -- | The distance between two points in the chessboard metric. -- -- >>> chessDist (Point 0 0) (Point 0 0) -- 0 -- >>> chessDist (Point (-1) 0) (Point 0 0) -- 1 -- >>> chessDist (Point (-1) 0) (Point (-1) 1) -- 1 -- >>> chessDist (Point (-1) 0) (Point 0 1) -- 1 -- >>> chessDist (Point (-1) 0) (Point 1 1) -- 2 -- -- prop> chessDist p1 p2 >= 0 -- prop> chessDist p1 p2 ^ (2 :: Int) <= euclidDistSq p1 p2 chessDist :: Point -> Point -> Int chessDist (Point x0 y0) (Point x1 y1) = max (abs (x1 - x0)) (abs (y1 - y0)) -- | Squared euclidean distance between two points. euclidDistSq :: Point -> Point -> Int euclidDistSq (Point x0 y0) (Point x1 y1) = (x1 - x0) ^ (2 :: Int) + (y1 - y0) ^ (2 :: Int) -- | Checks whether two points are adjacent on the map -- (horizontally, vertically or diagonally). adjacent :: Point -> Point -> Bool {-# INLINE adjacent #-} adjacent s t = chessDist s t == 1 -- | Bresenham's line algorithm generalized to arbitrary starting @eps@ -- (@eps@ value of 0 gives the standard BLA). -- Skips the source point and goes through the second point to infinity. -- Gives @Nothing@ if the points are equal. The target is given as @Point@, -- not @PointI@, to permit aiming out of the level, e.g., to get -- uniform distributions of directions for explosions close to the edge -- of the level. -- -- >>> bresenhamsLineAlgorithm 0 (Point 0 0) (Point 0 0) -- Nothing -- >>> take 3 $ fromJust $ bresenhamsLineAlgorithm 0 (Point 0 0) (Point 1 0) -- [(1,0),(2,0),(3,0)] -- >>> take 3 $ fromJust $ bresenhamsLineAlgorithm 0 (Point 0 0) (Point 0 1) -- [(0,1),(0,2),(0,3)] -- >>> take 3 $ fromJust $ bresenhamsLineAlgorithm 0 (Point 0 0) (Point 1 1) -- [(1,1),(2,2),(3,3)] bresenhamsLineAlgorithm :: Int -> Point -> Point -> Maybe [Point] bresenhamsLineAlgorithm eps source target = if source == target then Nothing else Just $ tail $ bresenhamsLineAlgorithmBegin eps source target -- | Bresenham's line algorithm generalized to arbitrary starting @eps@ -- (@eps@ value of 0 gives the standard BLA). Includes the source point -- and goes through the target point to infinity. -- -- >>> take 4 $ bresenhamsLineAlgorithmBegin 0 (Point 0 0) (Point 2 0) -- [(0,0),(1,0),(2,0),(3,0)] bresenhamsLineAlgorithmBegin :: Int -> Point -> Point -> [Point] bresenhamsLineAlgorithmBegin eps (Point x0 y0) (Point x1 y1) = let (dx, dy) = (x1 - x0, y1 - y0) xyStep b (x, y) = (x + signum dx, y + signum dy * b) yxStep b (x, y) = (x + signum dx * b, y + signum dy) (p, q, step) | abs dx > abs dy = (abs dy, abs dx, xyStep) | otherwise = (abs dx, abs dy, yxStep) bw = balancedWord p q (eps `mod` max 1 q) walk w xy = xy : walk (tail w) (step (head w) xy) in map (uncurry Point) $ walk bw (x0, y0) -- | See . balancedWord :: Int -> Int -> Int -> [Int] balancedWord p q eps | eps + p < q = 0 : balancedWord p q (eps + p) balancedWord p q eps = 1 : balancedWord p q (eps + p - q) -- | A list of all points on a straight vertical or straight horizontal line -- between two points. Fails if no such line exists. -- -- >>> fromTo (Point 0 0) (Point 2 0) -- [(0,0),(1,0),(2,0)] fromTo :: Point -> Point -> [Point] fromTo (Point x0 y0) (Point x1 y1) = let fromTo1 :: Int -> Int -> [Int] fromTo1 z0 z1 | z0 <= z1 = [z0..z1] | otherwise = [z0,z0-1..z1] result | x0 == x1 = map (Point x0) (fromTo1 y0 y1) | y0 == y1 = map (`Point` y0) (fromTo1 x0 x1) | otherwise = error $ "diagonal fromTo" `showFailure` ((x0, y0), (x1, y1)) in result originPoint :: Point originPoint = Point 0 0 -- | Checks that a point belongs to an area. insideP :: (X, Y, X, Y) -> Point -> Bool {-# INLINE insideP #-} insideP (x0, y0, x1, y1) (Point x y) = x1 >= x && x >= x0 && y1 >= y && y >= y0 LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/PointArray.hs0000644000000000000000000002660007346545000022621 0ustar0000000000000000{-# LANGUAGE FlexibleContexts, StandaloneDeriving, TypeFamilies #-} -- | Arrays, based on Data.Vector.Unboxed, indexed by @Point@. module Game.LambdaHack.Common.PointArray ( UnboxRepClass(..), Array(..) , empty, (!), accessI, (//), replicateA, unfoldrNA , foldrA, foldrA', foldlA', ifoldlA', ifoldrA', foldMA' , mapA, imapA, imapMA_, minIndexesA, maxIndexA, maxIndexByA, maxLastIndexA , toListA #ifdef EXPOSE_INTERNAL -- * Internal operations , toUnboxRep #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import Data.Vector.Binary () import qualified Data.Vector.Fusion.Bundle as Bundle import qualified Data.Vector.Generic as G import qualified Data.Vector.Unboxed as U import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal class ( Ord c, Eq (UnboxRep c), Ord (UnboxRep c), Bounded (UnboxRep c) , Binary (UnboxRep c), U.Unbox (UnboxRep c) ) => UnboxRepClass c where type UnboxRep c type instance UnboxRep c = c toUnboxRepUnsafe :: c -> UnboxRep c -- has to be total fromUnboxRep :: UnboxRep c -> c -- has to be total instance UnboxRepClass Bool where toUnboxRepUnsafe c = c fromUnboxRep c = c instance UnboxRepClass Word8 where toUnboxRepUnsafe c = c fromUnboxRep c = c instance UnboxRepClass (ContentId k) where type UnboxRep (ContentId k) = Word16 toUnboxRepUnsafe = DefsInternal.fromContentId fromUnboxRep = DefsInternal.toContentId instance UnboxRepClass Color.AttrCharW32 where type UnboxRep Color.AttrCharW32 = Word32 toUnboxRepUnsafe = Color.attrCharW32 fromUnboxRep = Color.AttrCharW32 -- | Arrays indexed by @Point@. data Array c = Array { axsize :: X , aysize :: Y , avector :: U.Vector (UnboxRep c) } deriving instance UnboxRepClass c => Eq (Array c) instance Show (Array c) where show a = "PointArray.Array with size " ++ show (axsize a, aysize a) instance UnboxRepClass c => Binary (Array c) where put Array{..} = do put axsize put aysize put avector get = do axsize <- get aysize <- get avector <- get return $! Array{..} toUnboxRep :: UnboxRepClass c => c -> UnboxRep c {-# INLINE toUnboxRep #-} toUnboxRep c = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (c <= fromUnboxRep maxBound) $ #endif toUnboxRepUnsafe c empty :: UnboxRepClass c => Array c empty = Array 0 0 U.empty -- Note: there's no point specializing this to @Point@ arguments, -- since the extra few additions in @fromPoint@ may be less expensive than -- memory or register allocations needed for the extra @Int@ in @Point@. -- | Array lookup. (!) :: UnboxRepClass c => Array c -> Point -> c {-# INLINE (!) #-} (!) Array{..} p = fromUnboxRep $ avector U.! fromEnum p accessI :: UnboxRepClass c => Array c -> Int -> UnboxRep c {-# INLINE accessI #-} accessI Array{..} p = avector `vectorUnboxedUnsafeIndex` p -- | Construct an array updated with the association list. (//) :: UnboxRepClass c => Array c -> [(Point, c)] -> Array c {-# INLINE (//) #-} (//) Array{..} l = let v = avector U.// map (fromEnum *** toUnboxRep) l in Array{avector = v, ..} -- unsafeUpdateA :: UnboxRepClass c => Array c -> [(Point, c)] -> () -- {-# INLINE unsafeUpdateA #-} -- unsafeUpdateA Array{..} l = runST $ do -- vThawed <- U.unsafeThaw avector -- mapM_ (\(p, c) -> VM.write vThawed (fromEnum p) (toUnboxRep c)) l -- void $ U.unsafeFreeze vThawed -- unsafeWriteA :: UnboxRepClass c => Array c -> Point -> c -> () -- {-# INLINE unsafeWriteA #-} -- unsafeWriteA Array{..} p c = runST $ do -- vThawed <- U.unsafeThaw avector -- VM.write vThawed (fromEnum p) (toUnboxRep c) -- void $ U.unsafeFreeze vThawed -- unsafeWriteManyA :: UnboxRepClass c => Array c -> [Point] -> c -> () -- {-# INLINE unsafeWriteManyA #-} -- unsafeWriteManyA Array{..} l c = runST $ do -- vThawed <- U.unsafeThaw avector -- let d = toUnboxRep c -- mapM_ (\p -> VM.write vThawed (fromEnum p) d) l -- void $ U.unsafeFreeze vThawed -- | Create an array from a replicated element. replicateA :: UnboxRepClass c => X -> Y -> c -> Array c {-# INLINE replicateA #-} replicateA axsize aysize c = Array{avector = U.replicate (axsize * aysize) $ toUnboxRep c, ..} -- -- | Create an array from a replicated monadic action. -- replicateMA :: (Monad m, UnboxRepClass c) => X -> Y -> m c -> m (Array c) -- {-# INLINE replicateMA #-} -- replicateMA axsize aysize m = do -- v <- U.replicateM (axsize * aysize) $ liftM toUnboxRep m -- return $! Array{avector = v, ..} -- -- | Create an array from a function. -- generateA :: UnboxRepClass c => X -> Y -> (Point -> c) -> Array c -- {-# INLINE generateA #-} -- generateA axsize aysize f = -- let g n = toUnboxRep $ f $ toEnum n -- in Array{avector = U.generate (axsize * aysize) g, ..} -- -- | Create an array from a monadic function. -- generateMA :: (Monad m, UnboxRepClass c) -- => X -> Y -> (Point -> m c) -> m (Array c) -- {-# INLINE generateMA #-} -- generateMA axsize aysize fm = do -- let gm n = liftM toUnboxRep $ fm $ toEnum n -- v <- U.generateM (axsize * aysize) gm -- return $! Array{avector = v, ..} unfoldrNA :: UnboxRepClass c => X -> Y -> (b -> (c, b)) -> b -> Array c {-# INLINE unfoldrNA #-} unfoldrNA axsize aysize fm b = let gm = Just . first toUnboxRep . fm v = U.unfoldrN (axsize * aysize) gm b in Array {avector = v, ..} -- -- | Content identifiers array size. -- sizeA :: Array c -> (X, Y) -- {-# INLINE sizeA #-} -- sizeA Array{..} = (axsize, aysize) -- | Fold right over an array. foldrA :: UnboxRepClass c => (c -> a -> a) -> a -> Array c -> a {-# INLINE foldrA #-} foldrA f z0 Array{..} = U.foldr (f . fromUnboxRep) z0 avector -- | Fold right strictly over an array. foldrA' :: UnboxRepClass c => (c -> a -> a) -> a -> Array c -> a {-# INLINE foldrA' #-} foldrA' f z0 Array{..} = U.foldr' (f . fromUnboxRep) z0 avector -- | Fold left strictly over an array. foldlA' :: UnboxRepClass c => (a -> c -> a) -> a -> Array c -> a {-# INLINE foldlA' #-} foldlA' f z0 Array{..} = U.foldl' (\a c -> f a (fromUnboxRep c)) z0 avector -- | Fold left strictly over an array -- (function applied to each element and its index). ifoldlA' :: UnboxRepClass c => (a -> Point -> c -> a) -> a -> Array c -> a {-# INLINE ifoldlA' #-} ifoldlA' f z0 Array{..} = U.ifoldl' (\a n c -> f a (toEnum n) (fromUnboxRep c)) z0 avector -- -- | Fold right over an array -- -- (function applied to each element and its index). -- ifoldrA :: UnboxRepClass c => (Point -> c -> a -> a) -> a -> Array c -> a -- {-# INLINE ifoldrA #-} -- ifoldrA f z0 Array{..} = -- U.ifoldr (\n c a -> f (toEnum n) (fromUnboxRep c) a) z0 avector -- | Fold right strictly over an array -- (function applied to each element and its index). ifoldrA' :: UnboxRepClass c => (Point -> c -> a -> a) -> a -> Array c -> a {-# INLINE ifoldrA' #-} ifoldrA' f z0 Array{..} = U.ifoldr' (\n c a -> f (toEnum n) (fromUnboxRep c) a) z0 avector -- | Fold monadically strictly over an array. foldMA' :: (Monad m, UnboxRepClass c) => (a -> c -> m a) -> a -> Array c -> m a {-# INLINE foldMA' #-} foldMA' f z0 Array{..} = U.foldM' (\a c -> f a (fromUnboxRep c)) z0 avector -- -- | Fold monadically strictly over an array -- -- (function applied to each element and its index). -- ifoldMA' :: (Monad m, UnboxRepClass c) -- => (a -> Point -> c -> m a) -> a -> Array c -> m a -- {-# INLINE ifoldMA' #-} -- ifoldMA' f z0 Array{..} = -- U.ifoldM' (\a n c -> f a (toEnum n) (fromUnboxRep c)) z0 avector -- | Map over an array. mapA :: (UnboxRepClass c, UnboxRepClass d) => (c -> d) -> Array c -> Array d {-# INLINE mapA #-} mapA f Array{..} = Array{avector = U.map (toUnboxRep . f . fromUnboxRep) avector, ..} -- | Map over an array (function applied to each element and its index). imapA :: (UnboxRepClass c, UnboxRepClass d) => (Point -> c -> d) -> Array c -> Array d {-# INLINE imapA #-} imapA f Array{..} = let v = U.imap (\n c -> toUnboxRep $ f (toEnum n) (fromUnboxRep c)) avector in Array{avector = v, ..} -- | Map monadically over an array (function applied to each element -- and its index) and ignore the results. imapMA_ :: (Monad m, UnboxRepClass c) => (Point -> c -> m ()) -> Array c -> m () {-# INLINE imapMA_ #-} imapMA_ f Array{..} = U.imapM_ (\n c -> f (toEnum n) (fromUnboxRep c)) avector -- -- | Set all elements to the given value, in place. -- unsafeSetA :: UnboxRepClass c => c -> Array c -> Array c -- {-# INLINE unsafeSetA #-} -- unsafeSetA c Array{..} = runST $ do -- vThawed <- U.unsafeThaw avector -- VM.set vThawed (toUnboxRep c) -- vFrozen <- U.unsafeFreeze vThawed -- return $! Array{avector = vFrozen, ..} -- -- | Set all elements to the given value, in place, if possible. -- safeSetA :: UnboxRepClass c => c -> Array c -> Array c -- {-# INLINE safeSetA #-} -- safeSetA c Array{..} = -- Array{avector = U.modify (\v -> VM.set v (toUnboxRep c)) avector, ..} -- -- | Yield the point coordinates of a minimum element of the array. -- -- The array may not be empty. -- minIndexA :: UnboxRepClass c => Array c -> Point -- {-# INLINE minIndexA #-} -- minIndexA Array{..} = toEnum $ U.minIndex avector -- -- | Yield the point coordinates of the last minimum element of the array. -- -- The array may not be empty. -- minLastIndexA :: UnboxRepClass c => Array c -> Point -- {-# INLINE minLastIndexA #-} -- minLastIndexA Array{..} = -- toEnum -- $ fst . Bundle.foldl1' imin . Bundle.indexed . G.stream -- $ avector -- where -- imin (i, x) (j, y) = i `seq` j `seq` if x >= y then (j, y) else (i, x) -- | Yield the point coordinates of all the minimum elements of the array. -- The array may not be empty. minIndexesA :: UnboxRepClass c => Array c -> [Point] {-# INLINE minIndexesA #-} minIndexesA Array{..} = Bundle.foldr imin [] . Bundle.indexed . G.stream $ avector where imin (i, x) acc = if x == minE then let !j = toEnum i in j : acc else acc !minE = U.minimum avector -- | Yield the point coordinates of the first maximum element of the array. -- The array may not be empty. maxIndexA :: UnboxRepClass c => Array c -> Point {-# INLINE maxIndexA #-} maxIndexA Array{..} = toEnum $ U.maxIndex avector -- | Yield the point coordinates of the first maximum element of the array. -- The array may not be empty. maxIndexByA :: UnboxRepClass c => (c -> c -> Ordering) -> Array c -> Point {-# INLINE maxIndexByA #-} maxIndexByA f Array{..} = let g a b = f (fromUnboxRep a) (fromUnboxRep b) in toEnum $ U.maxIndexBy g avector -- | Yield the point coordinates of the last maximum element of the array. -- The array may not be empty. maxLastIndexA :: UnboxRepClass c => Array c -> Point {-# INLINE maxLastIndexA #-} maxLastIndexA Array{..} = toEnum $ fst . Bundle.foldl1' imax . Bundle.indexed . G.stream $ avector where imax (i, x) (j, y) = i `seq` j `seq` if x <= y then (j, y) else (i, x) -- -- | Force the array not to retain any extra memory. -- forceA :: UnboxRepClass c => Array c -> Array c -- {-# INLINE forceA #-} -- forceA Array{..} = Array{avector = U.force avector, ..} -- fromListA :: UnboxRepClass c => X -> Y -> [c] -> Array c -- {-# INLINE fromListA #-} -- fromListA axsize aysize l = -- Array{avector = U.fromListN (axsize * aysize) $ map toUnboxRep l, ..} toListA :: UnboxRepClass c => Array c -> [c] {-# INLINE toListA #-} toListA Array{..} = map fromUnboxRep $ U.toList avector LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/ReqFailure.hs0000644000000000000000000002461107346545000022570 0ustar0000000000000000-- | Possible causes of failure of request. module Game.LambdaHack.Common.ReqFailure ( ReqFailure(..) , impossibleReqFailure, showReqFailure , permittedPrecious, permittedProject, permittedProjectAI, permittedApply #ifdef EXPOSE_INTERNAL -- * Internal operations #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Time import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Content.RuleKind as RK import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | Possible causes of failure of request. data ReqFailure = MoveUnskilled | MoveUnskilledAsleep | MoveNothing | MeleeUnskilled | MeleeSelf | MeleeDistant | MeleeNotWeapon | DisplaceUnskilled | DisplaceDistant | DisplaceAccess | DisplaceMultiple | DisplaceDying | DisplaceBraced | DisplaceImmobile | DisplaceSupported | AlterUnskilled | AlterUnwalked | AlterDistant | AlterBlockActor | AlterBlockItem | AlterNothing | CloseDistant | CloseClosed | CloseNothing | CloseNonClosable | WaitUnskilled | YellUnskilled | MoveItemUnskilled | EqpOverfull | EqpStackFull | ApplyUnskilled | ApplyFood | ApplyRead | ApplyPeriodic | ApplyOutOfReach | ApplyCharging | ApplyNoEffects | ItemNothing | ItemNotCalm | ItemOverStash | NotCalmPrecious | ProjectUnskilled | ProjectAimOnself | ProjectBlockTerrain | ProjectBlockActor | ProjectLobable | ProjectOutOfReach | ProjectFinderKeeper | NoChangeDunLeader deriving (Show, Eq) impossibleReqFailure :: ReqFailure -> Bool impossibleReqFailure reqFailure = case reqFailure of MoveUnskilled -> False -- unidentified skill items MoveUnskilledAsleep -> False -- unidentified skill items MoveNothing -> True MeleeUnskilled -> False -- unidentified skill items MeleeSelf -> True MeleeDistant -> True MeleeNotWeapon -> False -- unidentified items DisplaceUnskilled -> False -- unidentified skill items DisplaceDistant -> True DisplaceAccess -> True DisplaceMultiple -> True DisplaceDying -> True DisplaceBraced -> True DisplaceImmobile -> False -- unidentified skill items DisplaceSupported -> False AlterUnskilled -> False -- unidentified skill items AlterUnwalked -> False AlterDistant -> True AlterBlockActor -> True -- adjacent actor always visible AlterBlockItem -> True -- adjacent item always visible AlterNothing -> True -- if tile known, its properties known CloseDistant -> True CloseClosed -> True CloseNothing -> True CloseNonClosable -> True WaitUnskilled -> False -- unidentified skill items YellUnskilled -> False -- unidentified skill items MoveItemUnskilled -> False -- unidentified skill items EqpOverfull -> True EqpStackFull -> True ApplyUnskilled -> False -- unidentified skill items ApplyFood -> False -- unidentified skill items ApplyRead -> False -- unidentified skill items ApplyPeriodic -> False -- unidentified skill items ApplyOutOfReach -> True ApplyCharging -> False -- if aspect record unknown, charging unknown ApplyNoEffects -> False -- if effects unknown, can't prevent it ItemNothing -> True ItemNotCalm -> False -- unidentified skill items ItemOverStash -> True NotCalmPrecious -> False -- unidentified skill items ProjectUnskilled -> False -- unidentified skill items ProjectAimOnself -> True ProjectBlockTerrain -> True -- adjacent terrain always visible ProjectBlockActor -> True -- adjacent actor always visible ProjectLobable -> False -- unidentified skill items ProjectOutOfReach -> True ProjectFinderKeeper -> False NoChangeDunLeader -> True showReqFailure :: ReqFailure -> Text showReqFailure reqFailure = case reqFailure of MoveUnskilled -> "too low movement stat; use equipment menu to take off stat draining gear or switch to another teammate or wait until a stat draining condition passes as seen in organ menu" MoveUnskilledAsleep -> "actor asleep; yawn to wake up" MoveNothing -> "wasting time on moving into obstacle" MeleeUnskilled -> "too low melee combat stat" MeleeSelf -> "trying to melee oneself" MeleeDistant -> "trying to melee a distant foe" MeleeNotWeapon -> "trying to melee with not a weapon" DisplaceUnskilled -> "too low actor displacing stat" DisplaceDistant -> "trying to displace a distant actor" DisplaceAccess -> "trying to switch places without access" DisplaceMultiple -> "trying to displace multiple actors" DisplaceDying -> "trying to displace a dying foe" DisplaceBraced -> "trying to displace a braced foe" DisplaceImmobile -> "trying to displace an immobile foe" DisplaceSupported -> "trying to displace a foe supported by teammates or supply stash" AlterUnskilled -> "modify stat is needed to search or activate or transform terrain" AlterUnwalked -> "too low modify stat to enter or activate or transform terrain; find and equip gear that improves the stat or try with a teammate whose skill menu shows a higher stat" AlterDistant -> "trying to modify distant terrain" AlterBlockActor -> "blocked by an actor" AlterBlockItem -> "jammed by an item" AlterNothing -> "wasting time on modifying nothing" CloseDistant -> "trying to close a distant terrain" CloseClosed -> "already closed" CloseNothing -> "no adjacent terrain can be closed" CloseNonClosable -> "cannot be closed" WaitUnskilled -> "too low wait stat" YellUnskilled -> "actors unskilled in waiting cannot yell/yawn" MoveItemUnskilled -> "too low item moving stat" EqpOverfull -> "cannot equip any more items" EqpStackFull -> "cannot equip the whole item stack" ApplyUnskilled -> "too low item triggering stat" ApplyFood -> "trigger stat 1 is enough only to eat food from the ground and trigger simple appendages" ApplyRead -> "activating cultural artifacts requires trigger stat 3" ApplyPeriodic -> "manually activating periodic items requires trigger stat 4" ApplyOutOfReach -> "cannot trigger an item out of reach" ApplyCharging -> "cannot trigger an item that is still charging" ApplyNoEffects -> "cannot trigger an item that produces no effect" ItemNothing -> "wasting time on void item manipulation" ItemNotCalm -> "you try to focus on your equipment but your calm fails you" ItemOverStash -> "you roll in your hoard a little" NotCalmPrecious -> "you are too distracted to handle such an exquisite item" ProjectUnskilled -> "too low item flinging stat" ProjectAimOnself -> "cannot aim at oneself" ProjectBlockTerrain -> "aiming obstructed by terrain" ProjectBlockActor -> "aiming blocked by an actor" ProjectLobable -> "flinging a lobable item that stops at target position requires fling stat 3" ProjectOutOfReach -> "cannot aim an item out of reach" ProjectFinderKeeper -> "flinging any projectile you've found is out of the question; you prefer to keep them pristine and safe" NoChangeDunLeader -> "no manual level change for your team" -- The item should not be applied nor thrown because it's too delicate -- to operate when not calm or because it's too precious to identify by use. permittedPrecious :: Bool -> Bool -> ItemFull -> Either ReqFailure Bool permittedPrecious forced calmE itemFull@ItemFull{itemDisco} = let arItem = aspectRecordFull itemFull isPrecious = IA.checkFlag Ability.Precious arItem in if not forced && not calmE && isPrecious then Left NotCalmPrecious else Right $ IA.checkFlag Ability.Durable arItem || case itemDisco of ItemDiscoFull{} -> True ItemDiscoMean itemAspectMean -> IA.kmConst itemAspectMean || not isPrecious -- Simplified, faster version, for inner AI loop. permittedPreciousAI :: Bool -> ItemFull -> Bool permittedPreciousAI calmE itemFull@ItemFull{itemDisco} = let arItem = aspectRecordFull itemFull isPrecious = IA.checkFlag Ability.Precious arItem in (calmE || not isPrecious) && IA.checkFlag Ability.Durable arItem || case itemDisco of ItemDiscoFull{} -> True ItemDiscoMean itemAspectMean -> IA.kmConst itemAspectMean || not isPrecious permittedProject :: Bool -> Int -> Bool -> ItemFull -> Either ReqFailure Bool permittedProject forced skill calmE itemFull = let arItem = aspectRecordFull itemFull in if | not forced && skill < 1 -> Left ProjectUnskilled | not forced && IA.checkFlag Ability.Lobable arItem && skill < 3 -> Left ProjectLobable | otherwise -> permittedPrecious forced calmE itemFull -- Simplified, faster and more permissive version, for inner AI loop. permittedProjectAI :: Int -> Bool -> ItemFull -> Bool permittedProjectAI skill calmE itemFull = let arItem = aspectRecordFull itemFull in if | skill < 1 -> False | IA.checkFlag Ability.Lobable arItem && skill < 3 -> False | otherwise -> permittedPreciousAI calmE itemFull permittedApply :: RK.RuleContent -> Time -> Int -> Bool -> Maybe CStore -> ItemFull -> ItemQuant -> Either ReqFailure Bool permittedApply corule localTime skill calmE mstore itemFull@ItemFull{itemKind, itemSuspect} kit = if | skill < 1 -> Left ApplyUnskilled | skill < 2 && IK.isymbol itemKind /= IK.rsymbolNecklace (RK.ritemSymbols corule) && (IK.isymbol itemKind /= IK.rsymbolFood (RK.ritemSymbols corule) || mstore /= Just CGround) -> Left ApplyFood | skill < 3 && IK.isymbol itemKind == IK.rsymbolScroll (RK.ritemSymbols corule) -> Left ApplyRead | skill < 4 && let arItem = aspectRecordFull itemFull in IA.checkFlag Ability.Periodic arItem -> Left ApplyPeriodic -- If the item is discharged, neither the kinetic hit nor -- any effects activate, so there's no point triggering. -- Note that if client doesn't know the timeout, here we may leak the fact -- that the item is still charging, but the client risks destruction -- if the item is, in fact, recharged and is not durable -- (likely in case of jewellery). So it's OK (the message may be -- somewhat alarming though). | not $ hasCharge localTime kit -> Left ApplyCharging | otherwise -> if null (IK.ieffects itemKind) && (not itemSuspect || IA.isHumanTrinket itemKind) then Left ApplyNoEffects else permittedPrecious False calmE itemFull LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/RingBuffer.hs0000644000000000000000000000242707346545000022563 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Ring buffers. module Game.LambdaHack.Common.RingBuffer ( RingBuffer , empty, cons, toList, length ) where import Prelude () import Game.LambdaHack.Core.Prelude hiding (length, uncons) import Data.Binary import qualified Data.Foldable as Foldable import qualified Data.Sequence as Seq import GHC.Generics (Generic) -- | Ring buffers of a size determined at initialization. data RingBuffer a = RingBuffer { rbCarrier :: Seq.Seq a , rbMaxSize :: Int , rbNext :: Int , rbLength :: Int } deriving (Show, Generic) instance Binary a => Binary (RingBuffer a) -- Only takes O(log n)). empty :: Int -> a -> RingBuffer a empty size dummy = let rbMaxSize = max 1 size in RingBuffer (Seq.replicate rbMaxSize dummy) rbMaxSize 0 0 cons :: a -> RingBuffer a -> RingBuffer a cons a RingBuffer{..} = let incNext = (rbNext + 1) `mod` rbMaxSize incLength = min rbMaxSize $ rbLength + 1 in RingBuffer (Seq.update rbNext a rbCarrier) rbMaxSize incNext incLength toList :: RingBuffer a -> [a] toList RingBuffer{..} = let l = Foldable.toList rbCarrier start = (rbNext + rbMaxSize - rbLength) `mod` rbMaxSize in take rbLength $ drop start $ l ++ l length :: RingBuffer a -> Int length RingBuffer{rbLength} = rbLength LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Save.hs0000644000000000000000000001542407346545000021431 0ustar0000000000000000-- | Saving and restoring game state, used by both server and clients. module Game.LambdaHack.Common.Save ( ChanSave, saveToChan, wrapInSaves, restoreGame , compatibleVersion, delayPrint , saveNameCli, saveNameSer, bkpAllSaves #ifdef EXPOSE_INTERNAL -- * Internal operations , loopSave #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import Control.Concurrent.Async import qualified Control.Exception as Ex import Data.Binary import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Version import System.FilePath import System.IO (hFlush, stdout) import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.File import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Core.Random type ChanSave a = MVar (Maybe a) saveToChan :: ChanSave a -> a -> IO () saveToChan toSave s = do -- Wipe out previous candidates for saving. void $ tryTakeMVar toSave putMVar toSave $ Just s -- | Repeatedly save serialized snapshots of current state. -- -- Running with @-N2@ ca reduce @Max pause@ from 0.2s to 0.01s -- and @bytes copied during GC@ 10-fold, but framerate nor the frequency -- of not making a backup save are unaffected (at standard backup settings), -- even with null frontend, because saving takes so few resources. -- So, generally, backup save settings are relevant only due to latency -- impact on very slow computers or in JS. loopSave :: Binary a => COps -> (a -> FilePath) -> ChanSave a -> IO () loopSave cops stateToFileName toSave = loop where loop = do -- Wait until anyting to save. ms <- takeMVar toSave case ms of Just s -> do dataDir <- appDataDir tryCreateDir (dataDir "saves") let fileName = stateToFileName s yield -- minimize UI lag due to saving encodeEOF (dataDir "saves" fileName) (rexeVersion $ corule cops) s -- Wait until the save finished. During that time, the mvar -- is continually updated to newest state values. loop Nothing -> return () -- exit wrapInSaves :: Binary a => COps -> (a -> FilePath) -> (ChanSave a -> IO ()) -> IO () {-# INLINE wrapInSaves #-} wrapInSaves cops stateToFileName exe = do -- We don't merge this with the other calls to waitForChildren, -- because, e.g., for server, we don't want to wait for clients to exit, -- if the server crashes (but we wait for the save to finish). toSave <- newEmptyMVar a <- async $ loopSave cops stateToFileName toSave link a let fin = do -- Wait until the last save (if any) starts -- and tell the save thread to end. putMVar toSave Nothing -- Wait 0.5s to flush debug and then until the save thread ends. threadDelay 500000 wait a exe toSave `Ex.finally` fin -- The creation of, e.g., the initial client state, is outside the 'finally' -- clause, but this is OK, since no saves are ordered until 'runActionCli'. -- We save often, not only in the 'finally' section, in case of -- power outages, kill -9, GHC runtime crashes, etc. For internal game -- crashes, C-c, etc., the finalizer would be enough. -- If we implement incremental saves, saving often will help -- to spread the cost, to avoid a long pause at game exit. -- | Restore a saved game, if it exists. Initialize directory structure -- and copy over data files, if needed. restoreGame :: Binary a => RuleContent -> ClientOptions -> FilePath -> IO (Maybe a) restoreGame corule clientOptions fileName = do -- Create user data directory and copy files, if not already there. dataDir <- appDataDir tryCreateDir dataDir let path = dataDir "saves" fileName saveExists <- doesFileExist path -- If the savefile exists but we get IO or decoding errors, -- we show them and start a new game. If the savefile was randomly -- corrupted or made read-only, that should solve the problem. -- OTOH, serious IO problems (e.g. failure to create a user data directory) -- terminate the program with an exception. res <- Ex.try $ if saveExists then do let vExe1 = rexeVersion corule (vExe2, s) <- strictDecodeEOF path if compatibleVersion vExe1 vExe2 then return $! s `seq` Just s else do let msg = "Savefile" <+> T.pack path <+> "from an incompatible version" <+> T.pack (showVersion vExe2) <+> "detected while trying to restore" <+> T.pack (showVersion vExe1) <+> "game." fail $ T.unpack msg else return Nothing let handler :: Ex.SomeException -> IO (Maybe a) handler e = do moveAside <- bkpAllSaves corule clientOptions let msg = "Restore failed." <+> (if moveAside then "The wrong file has been moved aside." else "") <+> "The error message is:" <+> (T.unwords . T.lines) (tshow e) delayPrint msg return Nothing either handler return res -- Minor version discrepancy permitted. compatibleVersion :: Version -> Version -> Bool compatibleVersion v1 v2 = take 3 (versionBranch v1) == take 3 (versionBranch v2) delayPrint :: Text -> IO () delayPrint t = do smgen <- SM.newSMGen let (delay, _) = nextRandom 10000 smgen threadDelay $ 100 * delay -- try not to interleave saves with other clients T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough hFlush stdout saveNameCli :: RuleContent -> FactionId -> String saveNameCli corule side = let gameShortName = case words $ rtitle corule of w : _ -> w _ -> "Game" in gameShortName ++ ".team_" ++ show (fromEnum side) ++ ".sav" saveNameSer :: RuleContent -> String saveNameSer corule = let gameShortName = case words $ rtitle corule of w : _ -> w _ -> "Game" in gameShortName ++ ".server.sav" bkpAllSaves :: RuleContent -> ClientOptions -> IO Bool bkpAllSaves corule clientOptions = do dataDir <- appDataDir let benchmark = sbenchmark clientOptions defPrefix = ssavePrefixCli defClientOptions moveAside = not benchmark && ssavePrefixCli clientOptions == defPrefix bkpOneSave name = do let pathSave bkp = dataDir "saves" bkp <> defPrefix <> name b <- doesFileExist (pathSave "") when b $ renameFile (pathSave "") (pathSave "bkp.") bkpAll = do bkpOneSave $ saveNameSer corule forM_ [-199..199] $ \n -> bkpOneSave $ saveNameCli corule (toEnum n) when moveAside bkpAll return moveAside LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/State.hs0000644000000000000000000002627107346545000021615 0ustar0000000000000000-- | The common, for server and clients, main game state type -- and its operations. module Game.LambdaHack.Common.State ( -- * Basic game state, local or global State -- * State components , sdungeon, stotalDepth, sactorD, sitemD, sitemIxMap, sfactionD, stime, scops , sgold, shigh, sgameModeId, sdiscoKind, sdiscoAspect, sactorMaxSkills -- * State construction , defStateGlobal, emptyState, localFromGlobal -- * State update , updateDungeon, updateDepth, updateActorD, updateItemD, updateItemIxMap , updateFactionD, updateTime, updateCOpsAndCachedData, updateGold , updateDiscoKind, updateDiscoAspect, updateActorMaxSkills -- * State operations , getItemBody, aspectRecordFromItem, aspectRecordFromIid , maxSkillsFromActor, maxSkillsInDungeon #ifdef EXPOSE_INTERNAL -- * Internal operations , unknownLevel #endif -- * Operations both internal and used in unit tests , unknownTileMap ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.HighScore as HighScore import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.CaveKind (CaveKind) import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs -- | The main game state, the basic one, pertaining to a single game, -- not to a single playing session or an intersection of both. -- This state persists between playing sessions, until the particular game ends. -- Anything that persists between games is stored in server state, -- client state or client UI session state. -- -- Another differentiating property of this state is that it's kept -- separately on the server and each of the clients (players, human or AI) -- and separately updated, according to what each player can observe. -- It's never updated directly, but always through atomic commands -- ("CmdAtomic") that are filtered and interpreted differently -- on server and on each client. Therefore, the type is a view on the -- game state, not the real game state, except on the server that -- alone stores the full game information. data State = State { _sdungeon :: Dungeon -- ^ remembered dungeon , _stotalDepth :: Dice.AbsDepth -- ^ absolute dungeon depth for item creation , _sactorD :: ActorDict -- ^ remembered actors in the dungeon , _sitemD :: ItemDict -- ^ remembered items in the dungeon , _sitemIxMap :: ItemIxMap -- ^ spotted items with the same kind index -- could be recomputed at resume, but small , _sfactionD :: FactionDict -- ^ remembered sides still in game , _stime :: Time -- ^ global game time, for UI display only , _scops :: COps -- ^ remembered content; warning: use only -- validated content, even for testing , _sgold :: Int -- ^ total value of human trinkets in dungeon , _shigh :: HighScore.ScoreDict -- ^ high score table , _sgameModeId :: ContentId ModeKind -- ^ current game mode , _sdiscoKind :: DiscoveryKind -- ^ item kind discoveries data , _sdiscoAspect :: DiscoveryAspect -- ^ item aspect data; could be recomputed , _sactorMaxSkills :: ActorMaxSkills -- ^ actor maximal skills; is recomputed } deriving (Show, Eq) instance Binary State where put State{..} = do put _sdungeon put _stotalDepth put _sactorD put _sitemD put _sitemIxMap put _sfactionD put _stime put _sgold put _shigh put _sgameModeId put _sdiscoKind put _sdiscoAspect get = do _sdungeon <- get _stotalDepth <- get _sactorD <- get _sitemD <- get _sitemIxMap <- get _sfactionD <- get _stime <- get _sgold <- get _shigh <- get _sgameModeId <- get _sdiscoKind <- get _sdiscoAspect <- get let _scops = emptyCOps _sactorMaxSkills = EM.empty return $! State{..} sdungeon :: State -> Dungeon sdungeon = _sdungeon stotalDepth :: State -> Dice.AbsDepth stotalDepth = _stotalDepth sactorD :: State -> ActorDict sactorD = _sactorD sitemD :: State -> ItemDict sitemD = _sitemD sitemIxMap :: State -> ItemIxMap sitemIxMap = _sitemIxMap sfactionD :: State -> FactionDict sfactionD = _sfactionD stime :: State -> Time stime = _stime scops :: State -> COps scops = _scops sgold :: State -> Int sgold = _sgold shigh :: State -> HighScore.ScoreDict shigh = _shigh sgameModeId :: State -> ContentId ModeKind sgameModeId = _sgameModeId sdiscoKind :: State -> DiscoveryKind sdiscoKind = _sdiscoKind sdiscoAspect :: State -> DiscoveryAspect sdiscoAspect = _sdiscoAspect sactorMaxSkills :: State -> ActorMaxSkills sactorMaxSkills = _sactorMaxSkills unknownLevel :: COps -> ContentId CaveKind -> Dice.AbsDepth -> Area -> ([Point], [Point]) -> [Point] -> Int -> Bool -> Level unknownLevel COps{corule, cotile} lkind ldepth larea lstair lescape lexpl lnight = let outerId = ouniqGroup cotile TK.S_UNKNOWN_OUTER_FENCE in Level { lkind , ldepth , lfloor = EM.empty , lembed = EM.empty , lbig = EM.empty , lproj = EM.empty , ltile = unknownTileMap larea outerId (rWidthMax corule) (rHeightMax corule) , lentry = EM.empty , larea , lsmell = EM.empty , lstair , lescape , lseen = 0 , lexpl , ltime = timeZero , lnight } -- | Create a map full of unknown tiles. -- -- >>> unknownTileMap (fromJust (toArea (0,0,0,0))) TK.unknownId 2 2 -- PointArray.Array with size (2,2) unknownTileMap :: Area -> ContentId TileKind -> X -> Y -> TileMap unknownTileMap larea outerId rWidthMax rHeightMax = let unknownMap = PointArray.replicateA rWidthMax rHeightMax TK.unknownId outerUpdate = zip (areaInnerBorder larea) $ repeat outerId in unknownMap PointArray.// outerUpdate -- | Initial complete global game state. defStateGlobal :: Dungeon -> Dice.AbsDepth -> FactionDict -> COps -> HighScore.ScoreDict -> ContentId ModeKind -> DiscoveryKind -> State defStateGlobal _sdungeon _stotalDepth _sfactionD _scops _shigh _sgameModeId _sdiscoKind = State { _sactorD = EM.empty , _sitemD = EM.empty , _sitemIxMap = EM.empty , _stime = timeZero , _sgold = 0 , _sdiscoAspect = EM.empty , _sactorMaxSkills = EM.empty , .. } -- | Initial empty state. emptyState :: State emptyState = State { _sdungeon = EM.empty , _stotalDepth = Dice.AbsDepth 0 , _sactorD = EM.empty , _sitemD = EM.empty , _sitemIxMap = EM.empty , _sfactionD = EM.empty , _stime = timeZero , _scops = emptyCOps , _sgold = 0 , _shigh = HighScore.empty , _sgameModeId = toEnum 0 -- the initial value is unused , _sdiscoKind = EM.empty , _sdiscoAspect = EM.empty , _sactorMaxSkills = EM.empty } -- | Local state created by removing secret information from global -- state components. localFromGlobal :: State -> State localFromGlobal State{..} = State { _sdungeon = EM.map (\Level{..} -> unknownLevel _scops lkind ldepth larea lstair lescape lexpl lnight) _sdungeon , .. } -- | Update dungeon data within state. updateDungeon :: (Dungeon -> Dungeon) -> State -> State updateDungeon f s = s {_sdungeon = f (_sdungeon s)} -- | Update dungeon depth. updateDepth :: (Dice.AbsDepth -> Dice.AbsDepth) -> State -> State updateDepth f s = s {_stotalDepth = f (_stotalDepth s)} -- | Update the actor dictionary. updateActorD :: (ActorDict -> ActorDict) -> State -> State {-# INLINE updateActorD #-} -- just in case inliner goes hiwire updateActorD f s = s {_sactorD = f (_sactorD s)} -- | Update the item dictionary. updateItemD :: (ItemDict -> ItemDict) -> State -> State {-# INLINE updateItemD #-} updateItemD f s = s {_sitemD = f (_sitemD s)} -- | Update the item kind index map. updateItemIxMap :: (ItemIxMap -> ItemIxMap) -> State -> State updateItemIxMap f s = s {_sitemIxMap = f (_sitemIxMap s)} -- | Update faction data within state. updateFactionD :: (FactionDict -> FactionDict) -> State -> State updateFactionD f s = s {_sfactionD = f (_sfactionD s)} -- | Update global time within state. updateTime :: (Time -> Time) -> State -> State {-# INLINE updateTime #-} updateTime f s = s {_stime = f (_stime s)} -- | Update content data within state and recompute the cached data. updateCOpsAndCachedData :: (COps -> COps) -> State -> State updateCOpsAndCachedData f s = let s2 = s {_scops = f (_scops s)} in s2 {_sactorMaxSkills = maxSkillsInDungeon s2} -- | Update total gold value in the dungeon. updateGold :: (Int -> Int) -> State -> State updateGold f s = s {_sgold = f (_sgold s)} updateDiscoKind :: (DiscoveryKind -> DiscoveryKind) -> State -> State updateDiscoKind f s = s {_sdiscoKind = f (_sdiscoKind s)} updateDiscoAspect :: (DiscoveryAspect -> DiscoveryAspect) -> State -> State updateDiscoAspect f s = s {_sdiscoAspect = f (_sdiscoAspect s)} updateActorMaxSkills :: (ActorMaxSkills -> ActorMaxSkills) -> State -> State updateActorMaxSkills f s = s {_sactorMaxSkills = f (_sactorMaxSkills s)} getItemBody :: ItemId -> State -> Item getItemBody iid s = sitemD s EM.! iid -- This is best guess, including mean aspect record, so we can take into -- consideration even the kind the item hides under. aspectRecordFromItem :: ItemId -> Item -> State -> IA.AspectRecord aspectRecordFromItem iid item s = let kindId = case jkind item of IdentityObvious ik -> ik IdentityCovered ix ik -> fromMaybe ik $ ix `EM.lookup` sdiscoKind s COps{coItemSpeedup} = scops s mean = IA.kmMean $ getKindMean kindId coItemSpeedup in fromMaybe mean $ EM.lookup iid $ sdiscoAspect s aspectRecordFromIid :: ItemId -> State -> IA.AspectRecord aspectRecordFromIid iid s = aspectRecordFromItem iid (getItemBody iid s) s maxSkillsFromActor :: Actor -> State -> Ability.Skills maxSkillsFromActor b s = let processIid (iid, (k, _)) = (IA.aSkills $ aspectRecordFromIid iid s, k) processBag sks = Ability.sumScaledSkills $ map processIid sks in processBag $ EM.assocs (borgan b) ++ EM.assocs (beqp b) maxSkillsInDungeon :: State -> ActorMaxSkills maxSkillsInDungeon s = EM.map (`maxSkillsFromActor` s) $ sactorD s LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Thread.hs0000644000000000000000000000146007346545000021735 0ustar0000000000000000-- | Keeping track of forked threads. module Game.LambdaHack.Common.Thread ( forkChild, waitForChildren ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent.Async import Control.Concurrent.MVar -- Swiped from -- Ported to Async to link exceptions, to let CI tests fail. forkChild :: MVar [Async ()] -> IO () -> IO () forkChild children io = do a <- async io link a childs <- takeMVar children putMVar children (a : childs) waitForChildren :: MVar [Async ()] -> IO () waitForChildren children = do cs <- takeMVar children case cs of [] -> do putMVar children [] return () m : ms -> do putMVar children ms wait m waitForChildren children LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Tile.hs0000644000000000000000000004073107346545000021427 0ustar0000000000000000-- | Operations concerning dungeon level tiles. -- -- Unlike for many other content types, there is no type @Tile@, -- of particular concrete tiles in the dungeon, -- corresponding to 'TileKind' (the type of kinds of terrain tiles). -- This is because the tiles are too numerous and there's not enough -- storage space for a well-rounded @Tile@ type, on one hand, -- and on the other hand, tiles are accessed -- too often in performance critical code -- to try to compress their representation and/or recompute them. -- Instead, of defining a @Tile@ type, we express various properties -- of concrete tiles by arrays or sparse EnumMaps, as appropriate. -- -- Actors at normal speed (2 m/s) take one turn to move one tile (1 m by 1 m). module Game.LambdaHack.Common.Tile ( -- * Tile property lookup speedup tables and their construction TileSpeedup(..), Tab(..), speedupTile -- * Speedup property lookups , isClear, isLit, isHideout, isWalkable, isDoor, isChangable , isSuspect, isHideAs, consideredByAI, isExplorable , isVeryOftenItem, isCommonItem, isOftenActor, isNoItem, isNoActor , isEasyOpen, isEmbed, isAquatic, alterMinSkill, alterMinWalk -- * Slow property lookups , kindHasFeature, openTo, closeTo, embeddedItems, revealAs , obscureAs, hideAs, buildAs , isEasyOpenKind, isOpenable, isClosable, isModifiable #ifdef EXPOSE_INTERNAL -- * Internal operations , createTab, createTabWithKey, accessTab, alterMinSkillKind, alterMinWalkKind #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Vector.Unboxed as U import Data.Word (Word8) import Game.LambdaHack.Content.ItemKind (ItemKind) import Game.LambdaHack.Content.TileKind (TileKind, isUknownSpace) import qualified Game.LambdaHack.Content.TileKind as TK import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.ContentData import Game.LambdaHack.Definition.Defs -- | A lot of tabulated maps from tile kind identifier to a property -- of the tile kind. data TileSpeedup = TileSpeedup { isClearTab :: Tab Bool , isLitTab :: Tab Bool , isHideoutTab :: Tab Bool , isWalkableTab :: Tab Bool , isDoorTab :: Tab Bool , isOpenableTab :: Tab Bool , isClosableTab :: Tab Bool , isChangableTab :: Tab Bool , isModifiableWithTab :: Tab Bool , isSuspectTab :: Tab Bool , isHideAsTab :: Tab Bool , consideredByAITab :: Tab Bool , isVeryOftenItemTab :: Tab Bool , isCommonItemTab :: Tab Bool , isOftenActorTab :: Tab Bool , isNoItemTab :: Tab Bool , isNoActorTab :: Tab Bool , isEasyOpenTab :: Tab Bool , isEmbedTab :: Tab Bool , isAquaticTab :: Tab Bool , alterMinSkillTab :: Tab Word8 , alterMinWalkTab :: Tab Word8 } -- Vectors of booleans can be slower than arrays, because they are not packed, -- but with growing cache sizes they may as well turn out faster at some point. -- The advantage of vectors are exposed internals, in particular unsafe -- indexing. Also, in JS, bool arrays are obviously not packed. -- An option: https://github.com/Bodigrim/bitvec -- | A map morally indexed by @ContentId TileKind@. newtype Tab a = Tab (U.Vector a) createTab :: U.Unbox a => ContentData TileKind -> (TileKind -> a) -> Tab a createTab cotile prop = Tab $ U.convert $ omapVector cotile prop createTabWithKey :: U.Unbox a => ContentData TileKind -> (ContentId TileKind -> TileKind -> a) -> Tab a createTabWithKey cotile prop = Tab $ U.convert $ oimapVector cotile prop -- Unsafe indexing is pretty safe here, because we guard the vector -- with the newtype. accessTab :: U.Unbox a => Tab a -> ContentId TileKind -> a {-# INLINE accessTab #-} accessTab (Tab tab) ki = tab `vectorUnboxedUnsafeIndex` contentIdIndex ki speedupTile :: Bool -> ContentData TileKind -> TileSpeedup speedupTile allClear cotile = -- Vectors pack bools as Word8 by default. No idea if the extra memory -- taken makes random lookups more or less efficient, so not optimizing -- further, until I have benchmarks. let isClearTab | allClear = createTab cotile $ (/= maxBound) . TK.talter | otherwise = createTab cotile $ kindHasFeature TK.Clear isLitTab = createTab cotile $ not . kindHasFeature TK.Dark isHideoutTab = createTab cotile $ \tk -> kindHasFeature TK.Walkable tk -- implies not unknown && kindHasFeature TK.Dark tk isWalkableTab = createTab cotile $ kindHasFeature TK.Walkable isDoorTab = createTab cotile $ \tk -> let getTo (TK.OpenTo grp) acc = grp : acc getTo _ acc = acc in case foldr getTo [] $ TK.tfeature tk of [grp] | oisSingletonGroup cotile grp -> TK.isClosableKind $ okind cotile $ ouniqGroup cotile grp _ -> let getTo2 (TK.CloseTo grp) acc = grp : acc getTo2 _ acc = acc in case foldr getTo2 [] $ TK.tfeature tk of [grp] | oisSingletonGroup cotile grp -> TK.isOpenableKind $ okind cotile $ ouniqGroup cotile grp _ -> False isOpenableTab = createTab cotile TK.isOpenableKind isClosableTab = createTab cotile TK.isClosableKind isChangableTab = createTab cotile $ \tk -> let getTo TK.ChangeTo{} = True getTo _ = False in any getTo $ TK.tfeature tk isModifiableWithTab = createTab cotile $ \tk -> let getTo TK.OpenWith{} = True getTo TK.CloseWith{} = True getTo TK.ChangeWith{} = True getTo _ = False in any getTo $ TK.tfeature tk isSuspectTab = createTab cotile TK.isSuspectKind isHideAsTab = createTab cotile $ \tk -> let getTo TK.HideAs{} = True getTo _ = False in any getTo $ TK.tfeature tk consideredByAITab = createTab cotile $ kindHasFeature TK.ConsideredByAI isVeryOftenItemTab = createTab cotile $ kindHasFeature TK.VeryOftenItem isCommonItemTab = createTab cotile $ \tk -> kindHasFeature TK.OftenItem tk || kindHasFeature TK.VeryOftenItem tk isOftenActorTab = createTab cotile $ kindHasFeature TK.OftenActor isNoItemTab = createTab cotile $ kindHasFeature TK.NoItem isNoActorTab = createTab cotile $ kindHasFeature TK.NoActor isEasyOpenTab = createTab cotile isEasyOpenKind isEmbedTab = createTab cotile $ \tk -> let getTo TK.Embed{} = True getTo _ = False in any getTo $ TK.tfeature tk isAquaticTab = createTab cotile $ \tk -> maybe False (> 0) $ lookup TK.AQUATIC $ TK.tfreq tk alterMinSkillTab = createTabWithKey cotile alterMinSkillKind alterMinWalkTab = createTabWithKey cotile alterMinWalkKind in TileSpeedup {..} -- Check that alter can be used, if not, @maxBound@. -- For now, we assume only items with @Embed@ may have embedded items, -- whether inserted at dungeon creation or later on. -- This is used by UI and server to validate (sensibility of) altering. -- See the comment for @alterMinWalkKind@ regarding @HideAs@. alterMinSkillKind :: ContentId TileKind -> TileKind -> Word8 alterMinSkillKind _k tk = let getTo TK.OpenTo{} = True getTo TK.CloseTo{} = True getTo TK.ChangeTo{} = True getTo TK.OpenWith{} = True getTo TK.CloseWith{} = True getTo TK.ChangeWith{} = True getTo TK.HideAs{} = True -- in case tile swapped, but server sends hidden getTo TK.RevealAs{} = True getTo TK.ObscureAs{} = True getTo TK.Embed{} = True getTo TK.ConsideredByAI = True getTo _ = False in if any getTo $ TK.tfeature tk then TK.talter tk else maxBound -- How high alter skill is needed to make it walkable. If already -- walkable, put @0@, if can't, put @maxBound@. Used only by AI and Bfs -- We don't include @HideAs@, because it's very unlikely anybody swapped -- the tile while AI was not looking so AI can assume it's still uninteresting. -- Pathfinding in UI will also not show such tile as passable, which is OK. -- If a human player has a suspicion the tile was swapped, he can check -- it manually, disregarding the displayed path hints. alterMinWalkKind :: ContentId TileKind -> TileKind -> Word8 alterMinWalkKind k tk = let getTo TK.OpenTo{} = True -- enable when AI and humans can cope with unreachable areas -- getTo TK.OpenWith{} = True -- -- opening this may not be possible, but AI has to try, for there may -- -- be no other path getTo TK.RevealAs{} = True getTo TK.ObscureAs{} = True getTo _ = False in if | kindHasFeature TK.Walkable tk -> 0 | isUknownSpace k -> TK.talter tk | any getTo $ TK.tfeature tk -> TK.talter tk | otherwise -> maxBound -- | Whether a tile does not block vision. -- Essential for efficiency of "FOV", hence tabulated. isClear :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isClear #-} isClear TileSpeedup{isClearTab} = accessTab isClearTab -- | Whether a tile has ambient light --- is lit on its own. -- Essential for efficiency of "Perception", hence tabulated. isLit :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isLit #-} isLit TileSpeedup{isLitTab} = accessTab isLitTab -- | Whether a tile is a good hideout: walkable and dark. isHideout :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isHideout #-} isHideout TileSpeedup{isHideoutTab} = accessTab isHideoutTab -- | Whether actors can walk into a tile. -- Essential for efficiency of pathfinding, hence tabulated. isWalkable :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isWalkable #-} isWalkable TileSpeedup{isWalkableTab} = accessTab isWalkableTab -- | Whether a tile is a door, open or closed. -- Essential for efficiency of pathfinding, hence tabulated. isDoor :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isDoor #-} isDoor TileSpeedup{isDoorTab} = accessTab isDoorTab -- | Whether a tile kind (specified by its id) has an @OpenTo@ feature. isOpenable :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isOpenable #-} isOpenable TileSpeedup{isOpenableTab} = accessTab isOpenableTab -- | Whether a tile kind (specified by its id) has a @CloseTo@ feature. isClosable :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isClosable #-} isClosable TileSpeedup{isClosableTab} = accessTab isClosableTab -- | Whether a tile is changable. isChangable :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isChangable #-} isChangable TileSpeedup{isChangableTab} = accessTab isChangableTab -- | Whether a tile is modifiable with some items. isModifiableWith :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isModifiableWith #-} isModifiableWith TileSpeedup{isModifiableWithTab} = accessTab isModifiableWithTab -- | Whether a tile is suspect. -- Essential for efficiency of pathfinding, hence tabulated. isSuspect :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isSuspect #-} isSuspect TileSpeedup{isSuspectTab} = accessTab isSuspectTab isHideAs :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isHideAs #-} isHideAs TileSpeedup{isHideAsTab} = accessTab isHideAsTab consideredByAI :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE consideredByAI #-} consideredByAI TileSpeedup{consideredByAITab} = accessTab consideredByAITab -- | Whether one can easily explore a tile, possibly finding a treasure, -- either spawned there or dropped there by a (dying from poison) foe. -- Doors can't be explorable since revealing a secret tile -- should not change it's explorable status. Also, door explorable status -- should not depend on whether they are open or not, so that -- a foe opening a door doesn't force us to backtrack to explore it. -- Still, a foe that digs through a wall will affect our exploration counter -- and if content lets walls contain threasure, such backtraking makes sense. isExplorable :: TileSpeedup -> ContentId TileKind -> Bool isExplorable coTileSpeedup t = isWalkable coTileSpeedup t && not (isDoor coTileSpeedup t) isVeryOftenItem :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isVeryOftenItem #-} isVeryOftenItem TileSpeedup{isVeryOftenItemTab} = accessTab isVeryOftenItemTab isCommonItem :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isCommonItem #-} isCommonItem TileSpeedup{isCommonItemTab} = accessTab isCommonItemTab isOftenActor :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isOftenActor #-} isOftenActor TileSpeedup{isOftenActorTab} = accessTab isOftenActorTab isNoItem :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isNoItem #-} isNoItem TileSpeedup{isNoItemTab} = accessTab isNoItemTab isNoActor :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isNoActor #-} isNoActor TileSpeedup{isNoActorTab} = accessTab isNoActorTab -- | Whether a tile kind (specified by its id) has an @OpenTo@ feature -- or is walkable even without opening. isEasyOpen :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isEasyOpen #-} isEasyOpen TileSpeedup{isEasyOpenTab} = accessTab isEasyOpenTab isEmbed :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isEmbed #-} isEmbed TileSpeedup{isEmbedTab} = accessTab isEmbedTab isAquatic :: TileSpeedup -> ContentId TileKind -> Bool {-# INLINE isAquatic #-} isAquatic TileSpeedup{isAquaticTab} = accessTab isAquaticTab alterMinSkill :: TileSpeedup -> ContentId TileKind -> Int {-# INLINE alterMinSkill #-} alterMinSkill TileSpeedup{alterMinSkillTab} = fromEnum . accessTab alterMinSkillTab alterMinWalk :: TileSpeedup -> ContentId TileKind -> Int {-# INLINE alterMinWalk #-} alterMinWalk TileSpeedup{alterMinWalkTab} = fromEnum . accessTab alterMinWalkTab -- | Whether a tile kind has the given feature. kindHasFeature :: TK.Feature -> TileKind -> Bool {-# INLINE kindHasFeature #-} kindHasFeature f t = f `elem` TK.tfeature t openTo :: ContentData TileKind -> ContentId TileKind -> Rnd (ContentId TileKind) openTo cotile t = do let getTo (TK.OpenTo grp) acc = grp : acc getTo _ acc = acc case foldr getTo [] $ TK.tfeature $ okind cotile t of [grp] -> fromMaybe (error $ "" `showFailure` grp) <$> opick cotile grp (const True) _ -> return t closeTo :: ContentData TileKind -> ContentId TileKind -> Rnd (ContentId TileKind) closeTo cotile t = do let getTo (TK.CloseTo grp) acc = grp : acc getTo _ acc = acc case foldr getTo [] $ TK.tfeature $ okind cotile t of [grp] -> fromMaybe (error $ "" `showFailure` grp) <$> opick cotile grp (const True) _ -> return t embeddedItems :: ContentData TileKind -> ContentId TileKind -> [GroupName ItemKind] embeddedItems cotile t = let getTo (TK.Embed igrp) acc = igrp : acc getTo _ acc = acc in foldr getTo [] $ TK.tfeature $ okind cotile t revealAs :: ContentData TileKind -> ContentId TileKind -> Rnd (ContentId TileKind) revealAs cotile t = do let getTo (TK.RevealAs grp) acc = grp : acc getTo _ acc = acc case foldr getTo [] $ TK.tfeature $ okind cotile t of [] -> return t groups -> do grp <- oneOf groups fromMaybe (error $ "" `showFailure` grp) <$> opick cotile grp (const True) obscureAs :: ContentData TileKind -> ContentId TileKind -> Rnd (ContentId TileKind) obscureAs cotile t = do let getTo (TK.ObscureAs grp) acc = grp : acc getTo _ acc = acc case foldr getTo [] $ TK.tfeature $ okind cotile $ buildAs cotile t of [] -> return t groups -> do grp <- oneOf groups fromMaybe (error $ "" `showFailure` grp) <$> opick cotile grp (const True) hideAs :: ContentData TileKind -> ContentId TileKind -> Maybe (ContentId TileKind) hideAs cotile t = let getTo TK.HideAs{} = True getTo _ = False in case find getTo $ TK.tfeature $ okind cotile t of Just (TK.HideAs grp) -> Just $ ouniqGroup cotile grp _ -> Nothing buildAs :: ContentData TileKind -> ContentId TileKind -> ContentId TileKind buildAs cotile t = let getTo TK.BuildAs{} = True getTo _ = False in case find getTo $ TK.tfeature $ okind cotile t of Just (TK.BuildAs grp) -> ouniqGroup cotile grp _ -> t isEasyOpenKind :: TileKind -> Bool isEasyOpenKind tk = let getTo TK.OpenTo{} = True getTo TK.Walkable = True -- very easy open getTo _ = False in TK.talter tk < 10 && any getTo (TK.tfeature tk) isModifiable :: TileSpeedup -> ContentId TileKind -> Bool isModifiable coTileSpeedup t = isOpenable coTileSpeedup t || isClosable coTileSpeedup t || isChangable coTileSpeedup t || isModifiableWith coTileSpeedup t || isSuspect coTileSpeedup t LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Time.hs0000644000000000000000000002652507346545000021435 0ustar0000000000000000{-# LANGUAGE DeriveFunctor, GeneralizedNewtypeDeriving #-} -- | Game time and speed. module Game.LambdaHack.Common.Time ( Time, timeTicks , timeZero, timeEpsilon, timeClip, timeTurn, timeSecond, clipsInTurn , absoluteTimeAdd, absoluteTimeSubtract, absoluteTimeNegate , timeFit, timeFitUp, timeRecent5 , Delta(..), timeShift, timeDeltaToFrom, timeDeltaAdd, timeDeltaSubtract , timeDeltaReverse, timeDeltaScale, timeDeltaPercent, timeDeltaDiv , timeDeltaToDigit, timeDeltaInSecondsText , Speed, toSpeed, fromSpeed, minSpeed, displaySpeed , speedWalk, speedLimp, speedThrust, modifyDamageBySpeed, speedScale, speedAdd , ticksPerMeter, speedFromWeight, rangeFromSpeedAndLinger #ifdef EXPOSE_INTERNAL -- * Internal operations , _timeTick, turnsInSecond, sInMs, minimalSpeed, rangeFromSpeed #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.Char as Char import Data.Int (Int64) import Game.LambdaHack.Common.Misc -- | Game time in ticks. The time dimension. -- One tick is 1 microsecond (one millionth of a second), -- one turn is 0.5 s. newtype Time = Time {timeTicks :: Int64} deriving (Show, Eq, Ord, Binary) -- | Start of the game time, or zero lenght time interval. timeZero :: Time timeZero = Time 0 -- | The smallest unit of time. Should not be exported and used elsewhere, -- because the proportion of turn to tick is an implementation detail. -- The significance of this detail is only that it determines resolution -- of the time dimension. _timeTick :: Time _timeTick = Time 1 -- | An infinitesimal time period. timeEpsilon :: Time timeEpsilon = _timeTick -- | At least once per clip all moves are resolved -- and a frame or a frame delay is generated. -- Currently one clip is 0.05 s, but it may change, -- and the code should not depend on this fixed value. timeClip :: Time timeClip = Time 50000 -- | One turn is 0.5 s. The code may depend on that. -- Actors at normal speed (2 m/s) take one turn to move one tile (1 m by 1 m). timeTurn :: Time timeTurn = Time 500000 -- | This many ticks fits in a single second. timeSecond :: Time timeSecond = Time $ timeTicks timeTurn * turnsInSecond -- | This many turns fit in a single second. turnsInSecond :: Int64 turnsInSecond = 2 -- | This many clips fit in one turn. Determines the resolution -- of actor move sampling and display updates. clipsInTurn :: Int clipsInTurn = let r = timeTurn `timeFit` timeClip in assert (r >= 5) r -- | Absolute time addition, e.g., for summing the total game session time -- from the times of individual games. absoluteTimeAdd :: Time -> Time -> Time {-# INLINE absoluteTimeAdd #-} absoluteTimeAdd (Time t1) (Time t2) = Time (t1 + t2) absoluteTimeSubtract :: Time -> Time -> Time {-# INLINE absoluteTimeSubtract #-} absoluteTimeSubtract (Time t1) (Time t2) = Time (t1 - t2) -- | Absolute time negation. To be used for reversing time flow, -- e.g., for comparing absolute times in the reverse order. absoluteTimeNegate :: Time -> Time {-# INLINE absoluteTimeNegate #-} absoluteTimeNegate (Time t) = Time (-t) -- | How many time intervals of the latter kind fits in an interval -- of the former kind. timeFit :: Time -> Time -> Int {-# INLINE timeFit #-} timeFit (Time t1) (Time t2) = fromEnum $ t1 `div` t2 -- | How many time intervals of the latter kind cover an interval -- of the former kind (rounded up). timeFitUp :: Time -> Time -> Int {-# INLINE timeFitUp #-} timeFitUp (Time t1) (Time t2) = fromEnum $ t1 `divUp` t2 timeRecent5 :: Time -> Time -> Bool timeRecent5 localTime time = timeDeltaToFrom localTime time < timeDeltaScale (Delta timeTurn) 5 -- | One-dimentional vectors. Introduced to tell apart the 2 uses of Time: -- as an absolute game time and as an increment. newtype Delta a = Delta a deriving (Show, Eq, Ord, Binary, Functor) -- | Shifting an absolute time by a time vector. timeShift :: Time -> Delta Time -> Time {-# INLINE timeShift #-} timeShift (Time t1) (Delta (Time t2)) = Time (t1 + t2) -- | Time time vector between the second and the first absolute times. -- The arguments are in the same order as in the underlying scalar subtraction. timeDeltaToFrom :: Time -> Time -> Delta Time {-# INLINE timeDeltaToFrom #-} timeDeltaToFrom (Time t1) (Time t2) = Delta $ Time (t1 - t2) -- | Addition of time deltas. timeDeltaAdd :: Delta Time -> Delta Time -> Delta Time {-# INLINE timeDeltaAdd #-} timeDeltaAdd (Delta (Time t1)) (Delta (Time t2)) = Delta $ Time (t1 - t2) -- | Subtraction of time deltas. -- The arguments are in the same order as in the underlying scalar subtraction. timeDeltaSubtract :: Delta Time -> Delta Time -> Delta Time {-# INLINE timeDeltaSubtract #-} timeDeltaSubtract (Delta (Time t1)) (Delta (Time t2)) = Delta $ Time (t1 - t2) -- | Reverse a time vector. timeDeltaReverse :: Delta Time -> Delta Time {-# INLINE timeDeltaReverse #-} timeDeltaReverse (Delta (Time t)) = Delta (Time (-t)) -- | Scale the time vector by an @Int@ scalar value. timeDeltaScale :: Delta Time -> Int -> Delta Time {-# INLINE timeDeltaScale #-} timeDeltaScale (Delta (Time t)) s = Delta (Time (t * into @Int64 s)) -- | Take the given percent of the time vector. timeDeltaPercent :: Delta Time -> Int -> Delta Time {-# INLINE timeDeltaPercent #-} timeDeltaPercent (Delta (Time t)) s = Delta (Time (t * into @Int64 s `div` 100)) -- | Divide a time vector. timeDeltaDiv :: Delta Time -> Int -> Delta Time {-# INLINE timeDeltaDiv #-} timeDeltaDiv (Delta (Time t)) n = Delta (Time (t `div` into @Int64 n)) -- | Represent the main 10 thresholds of a time range by digits, -- given the total length of the time range. timeDeltaToDigit :: Delta Time -> Delta Time -> Char {-# INLINE timeDeltaToDigit #-} timeDeltaToDigit (Delta (Time maxT)) (Delta (Time t)) = let n = (20 * t) `div` maxT k = (n + 1) `div` 2 digit | k > 9 = '9' | k < 1 = '1' | otherwise = Char.intToDigit $ fromEnum k in digit -- @oneM@ times the number of seconds represented by the time delta timeDeltaInSeconds :: Delta Time -> Int64 timeDeltaInSeconds (Delta (Time dt)) = oneM * dt `div` timeTicks timeSecond timeDeltaInSecondsText :: Delta Time -> Text timeDeltaInSecondsText delta = show64With2 (timeDeltaInSeconds delta) <> "s" -- | Speed in meters per 1 million seconds (m/Ms). -- Actors at normal speed (2 m/s) take one time turn (0.5 s) -- to make one step (move one tile, which is 1 m by 1 m). newtype Speed = Speed Int64 deriving (Eq, Ord, Binary) instance Show Speed where show s = show $ fromSpeed s -- | Number of seconds in a mega-second. sInMs :: Int64 sInMs = 1000000 -- | Constructor for content definitions. toSpeed :: Int -> Speed {-# INLINE toSpeed #-} toSpeed s = Speed $ into @Int64 s * sInMs `div` 10 -- | Readable representation of speed in the format used in content definitions. fromSpeed :: Speed -> Int {-# INLINE fromSpeed #-} fromSpeed (Speed s) = fromEnum $ s * 10 `div` sInMs minSpeed :: Int minSpeed = 5 -- | Pretty-print speed given in the format used in content definitions. displaySpeed :: Int -> String displaySpeed kRaw = let k = max minSpeed kRaw l = k `div` 10 x = k - l * 10 in show l <> (if x == 0 then "" else "." <> show x) <> "m/s" -- | The minimal speed is half a meter (half a step across a tile) -- per second (two standard turns, which the time span during which -- projectile moves, unless it has modified linger value). -- This is four times slower than standard human movement speed. -- -- It needen't be lower, because @rangeFromSpeed@ gives 0 steps -- with such speed, so the actor's trajectory is empty, so it drops down -- at once. Twice that speed already moves a normal projectile one step -- before it stops. It shouldn't be lower or a slow actor would incur -- such a time debt for performing a single action that he'd be paralyzed -- for many turns, e.g., leaving his dead body on the screen for very long. minimalSpeed :: Int64 minimalSpeed = let Speed msp = toSpeed minSpeed in assert (msp == sInMs `div` 2) msp -- | Fast walk speed (2 m/s) that suffices to move one tile in one turn. speedWalk :: Speed speedWalk = Speed $ 2 * sInMs -- | Limp speed (1 m/s) that suffices to move one tile in two turns. -- This is the minimal speed for projectiles to fly just one space and drop. speedLimp :: Speed speedLimp = Speed sInMs -- | Sword thrust speed (10 m/s). Base weapon damages, both melee and ranged, -- are given assuming this speed and ranged damage is modified -- accordingly when projectile speeds differ. Differences in melee -- weapon swing speeds are captured in damage bonuses instead, -- since many other factors influence total damage. -- -- Billiard ball is 25 m/s, sword swing at the tip is 35 m/s, -- medieval bow is 70 m/s, AK47 is 700 m/s. speedThrust :: Speed speedThrust = Speed $ 10 * sInMs -- | Modify damage when projectiles is at a non-standard speed. -- Energy and so damage is proportional to the square of speed, -- hence the formula. modifyDamageBySpeed :: Int64 -> Speed -> Int64 modifyDamageBySpeed dmg (Speed s) = let Speed sThrust = speedThrust in if s <= minimalSpeed then 0 -- needed mostly not to display useless ranged damage else round $ -- Double, because overflows Int64 int64ToDouble dmg * int64ToDouble s ^ (2 :: Int) / int64ToDouble sThrust ^ (2 :: Int) -- | Scale speed by a scalar value. speedScale :: Rational -> Speed -> Speed {-# INLINE speedScale #-} speedScale s (Speed v) = Speed (round $ (fromIntegralWrap :: Int64 -> Rational) v * s) -- | Speed addition. speedAdd :: Speed -> Speed -> Speed {-# INLINE speedAdd #-} speedAdd (Speed s1) (Speed s2) = Speed (s1 + s2) -- | The number of time ticks it takes to walk 1 meter at the given speed. ticksPerMeter :: Speed -> Delta Time {-# INLINE ticksPerMeter #-} ticksPerMeter (Speed v) = -- Prevent division by zero or infinite time taken for any action. Delta $ Time $ timeTicks timeSecond * sInMs `divUp` max minimalSpeed v -- | Calculate projectile speed from item weight in grams -- and velocity percent modifier. -- See . speedFromWeight :: Int -> Int -> Speed speedFromWeight !weight !throwVelocity = let w = into @Int64 weight mpMs | w < 250 = sInMs * 20 | w < 1500 = sInMs * 20 * 1250 `div` (w + 1000) | w < 10500 = sInMs * (11500 - w) `div` 1000 | otherwise = minimalSpeed * 2 -- move one step and drop v = mpMs * into @Int64 throwVelocity `div` 100 -- We round down to the nearest multiple of 2M (unless the speed -- is very low), to ensure both turns of flight cover the same distance -- and that the speed matches the distance traveled exactly. multiple2M = if v > 2 * sInMs then 2 * sInMs * (v `div` (2 * sInMs)) else v in Speed $ max minimalSpeed multiple2M -- | Calculate maximum range in meters of a projectile from its speed. -- See . -- With this formula, each projectile flies for at most 1 second, -- that is 2 standard turns, and then drops to the ground. rangeFromSpeed :: Speed -> Int {-# INLINE rangeFromSpeed #-} rangeFromSpeed (Speed v) = fromEnum $ v `div` sInMs -- | Calculate maximum range taking into account the linger percentage. rangeFromSpeedAndLinger :: Speed -> Int -> Int rangeFromSpeedAndLinger !speed !throwLinger = let range = rangeFromSpeed speed in throwLinger * range `divUp` 100 LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Types.hs0000644000000000000000000000370007346545000021631 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Abstract identifiers for the main types in the engine. This is imported -- by modules that don't need to know the internal structure -- of the types. As a side effect, this prevents mutual dependencies -- among modules. module Game.LambdaHack.Common.Types ( ItemId, FactionId, LevelId, ActorId , Container(..) ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import Data.Hashable import GHC.Generics (Generic) import Game.LambdaHack.Common.Point import Game.LambdaHack.Definition.Defs -- | A unique identifier of an item in the dungeon. newtype ItemId = ItemId Int deriving (Show, Eq, Ord, Enum, Binary) -- | A unique identifier of a faction in a game. It's assigned in the order -- from game mode roster, starting from one. We keep the @FactionId@ -- and @TeamContinuity@ types separate mostly to let @FactionId@ reflect -- the order, which influences starting faction positions, etc. -- We use @TeamContinuity@ for dictionaries containing teams that may -- or may not be active factions in the current game, while @FactionId@ are -- used only for factions in the game (in particular, because they vary -- depending on order in game mode roster, while @TeamContinuity@ are stable). newtype FactionId = FactionId Int deriving (Show, Eq, Ord, Enum, Hashable, Binary) -- | Abstract level identifiers. newtype LevelId = LevelId Int deriving (Show, Eq, Ord, Hashable, Binary) instance Enum LevelId where fromEnum (LevelId n) = n toEnum = LevelId -- picks the main branch of the dungeon -- | A unique identifier of an actor in the dungeon. newtype ActorId = ActorId Int deriving (Show, Eq, Ord, Enum, Binary) -- | Item container type. data Container = CFloor LevelId Point | CEmbed LevelId Point | CActor ActorId CStore | CTrunk FactionId LevelId Point -- ^ for bootstrapping actor bodies deriving (Show, Eq, Ord, Generic) instance Binary Container LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Common/Vector.hs0000644000000000000000000002323207346545000021771 0ustar0000000000000000{-# LANGUAGE DeriveGeneric #-} -- | Basic operations on bounded 2D vectors, with an efficient, but not 1-1 -- and not monotonic @Enum@ instance. module Game.LambdaHack.Common.Vector ( Vector(..), VectorI , isUnit, neg, chessDistVector, euclidDistSqVector , moves, movesCardinal, movesCardinalI, movesDiagonal, movesDiagonalI , compassText, vicinityBounded, vicinityUnsafe , vicinityCardinal, vicinityCardinalUnsafe, squareUnsafeSet , shift, shiftBounded, trajectoryToPath, trajectoryToPathBounded , vectorToFrom, computeTrajectory , RadianAngle, rotate, towards #ifdef EXPOSE_INTERNAL -- * Internal operations , longMoveTexts, movesSquare, pathToTrajectory , normalize, normalizeVector #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.DeepSeq import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Int (Int32) import qualified Data.IntSet as IS import qualified Data.Primitive.PrimArray as PA import GHC.Generics (Generic) import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Time import Game.LambdaHack.Definition.Defs -- | 2D vectors in cartesian representation. Coordinates grow to the right -- and down, so that the (1, 1) vector points to the bottom-right corner -- of the screen. data Vector = Vector { vx :: X , vy :: Y } deriving (Show, Read, Eq, Ord, Generic) instance Binary Vector where put = put . (toIntegralCrash :: Int -> Int32) . fromEnum get = fmap (toEnum . (fromIntegralWrap :: Int32 -> Int)) get -- `fromIntegralWrap` is fine here, because we converted the integer -- in the opposite direction first, so it fits even in 31 bit `Int` -- Note that the conversion is not monotonic wrt the natural @Ord@ instance, -- to keep it in sync with Point. instance Enum Vector where fromEnum Vector{..} = let !xsize = PA.indexPrimArray speedupHackXSize 0 in vx + vy * xsize toEnum n = let !xsize = PA.indexPrimArray speedupHackXSize 0 !xsizeHalf = xsize `div` 2 (!y, !x) = n `quotRem` xsize (!vx, !vy) | x >= xsizeHalf = (x - xsize, y + 1) | x <= - xsizeHalf = (x + xsize, y - 1) | otherwise = (x, y) in Vector{..} instance NFData Vector -- | Enumeration representation of @Vector@. type VectorI = Int -- | Tells if a vector has length 1 in the chessboard metric. isUnit :: Vector -> Bool {-# INLINE isUnit #-} isUnit v = chessDistVector v == 1 -- | Reverse an arbirary vector. neg :: Vector -> Vector {-# INLINE neg #-} neg (Vector vx vy) = Vector (-vx) (-vy) -- | The lenght of a vector in the chessboard metric, -- where diagonal moves cost 1. chessDistVector :: Vector -> Int {-# INLINE chessDistVector #-} chessDistVector (Vector x y) = max (abs x) (abs y) -- | Squared euclidean distance between two vectors. euclidDistSqVector :: Vector -> Vector -> Int euclidDistSqVector (Vector x0 y0) (Vector x1 y1) = (x1 - x0) ^ (2 :: Int) + (y1 - y0) ^ (2 :: Int) -- | Vectors of all unit moves in the chessboard metric, -- clockwise, starting north-west. moves :: [Vector] moves = map (uncurry Vector) [(-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0)] -- | Vectors of all cardinal direction unit moves, clockwise, starting north. movesCardinal :: [Vector] movesCardinal = map (uncurry Vector) [(0, -1), (1, 0), (0, 1), (-1, 0)] movesCardinalI :: [VectorI] movesCardinalI = map fromEnum movesCardinal -- | Vectors of all diagonal direction unit moves, clockwise, starting north. movesDiagonal :: [Vector] movesDiagonal = map (uncurry Vector) [(-1, -1), (1, -1), (1, 1), (-1, 1)] movesDiagonalI :: [VectorI] movesDiagonalI = map fromEnum movesDiagonal -- moveTexts :: [Text] -- moveTexts = ["NW", "N", "NE", "E", "SE", "S", "SW", "W"] longMoveTexts :: [Text] longMoveTexts = [ "northwest", "north", "northeast", "east" , "southeast", "south", "southwest", "west" ] compassText :: Vector -> Text compassText v = let m = EM.fromList $ zip moves longMoveTexts assFail = error $ "not a unit vector" `showFailure` v in EM.findWithDefault assFail v m -- | All (8 at most) closest neighbours of a point within an area. vicinityBounded :: X -> Y -- ^ limit the search to this area -> Point -- ^ position to find neighbours of -> [Point] vicinityBounded rWidthMax rHeightMax p = if insideP (1, 1, rWidthMax - 2, rHeightMax - 2) p then vicinityUnsafe p else [ res | dxy <- moves , let res = shift p dxy , insideP (0, 0, rWidthMax - 1, rHeightMax - 1) res ] vicinityUnsafe :: Point -> [Point] {-# INLINE vicinityUnsafe #-} vicinityUnsafe p = [ shift p dxy | dxy <- moves ] -- | All (4 at most) cardinal direction neighbours of a point within an area. vicinityCardinal :: X -> Y -- ^ limit the search to this area -> Point -- ^ position to find neighbours of -> [Point] vicinityCardinal rWidthMax rHeightMax p = [ res | dxy <- movesCardinal , let res = shift p dxy , insideP (0, 0, rWidthMax - 1, rHeightMax - 1) res ] vicinityCardinalUnsafe :: Point -> [Point] vicinityCardinalUnsafe p = [ shift p dxy | dxy <- movesCardinal ] -- Ascending list; includes the origin. movesSquare :: [VectorI] movesSquare = map (fromEnum . uncurry Vector) [ (-1, -1), (0, -1), (1, -1) , (-1, 0), (0, 0), (1, 0) , (-1, 1), (0, 1), (1, 1) ] squareUnsafeSet :: Point -> ES.EnumSet Point {-# INLINE squareUnsafeSet #-} squareUnsafeSet p = ES.intSetToEnumSet $ IS.fromDistinctAscList $ map (fromEnum p +) movesSquare -- | Translate a point by a vector. shift :: Point -> Vector -> Point {-# INLINE shift #-} shift (Point x0 y0) (Vector x1 y1) = Point (x0 + x1) (y0 + y1) -- | Translate a point by a vector, but only if the result fits in an area. shiftBounded :: X -> Y -> Point -> Vector -> Point shiftBounded rWidthMax rHeightMax pos v@(Vector xv yv) = if insideP (-xv, -yv, rWidthMax - xv - 1, rHeightMax - yv - 1) pos then shift pos v else pos -- | A list of points that a list of vectors leads to. trajectoryToPath :: Point -> [Vector] -> [Point] trajectoryToPath _ [] = [] trajectoryToPath start (v : vs) = let next = shift start v in next : trajectoryToPath next vs -- | A list of points that a list of vectors leads to, bounded by level size. trajectoryToPathBounded :: X -> Y -> Point -> [Vector] -> [Point] trajectoryToPathBounded _ _ _ [] = [] trajectoryToPathBounded rWidthMax rHeightMax start (v : vs) = let next = shiftBounded rWidthMax rHeightMax start v in next : trajectoryToPathBounded rWidthMax rHeightMax next vs -- | The vector between the second point and the first. We have -- -- > shift pos1 (pos2 `vectorToFrom` pos1) == pos2 -- -- The arguments are in the same order as in the underlying scalar subtraction. vectorToFrom :: Point -> Point -> Vector {-# INLINE vectorToFrom #-} vectorToFrom (Point x0 y0) (Point x1 y1) = Vector (x0 - x1) (y0 - y1) -- | A list of vectors between a list of points. pathToTrajectory :: [Point] -> [Vector] pathToTrajectory [] = [] pathToTrajectory lp1@(_ : lp2) = zipWith vectorToFrom lp2 lp1 computeTrajectory :: Int -> Int -> Int -> [Point] -> ([Vector], (Speed, Int)) computeTrajectory weight throwVelocity throwLinger path = let speed = speedFromWeight weight throwVelocity trange = rangeFromSpeedAndLinger speed throwLinger btrajectory = pathToTrajectory $ take (trange + 1) path in (btrajectory, (speed, trange)) type RadianAngle = Double -- | Rotate a vector by the given angle (expressed in radians) -- counterclockwise and return a unit vector approximately in the resulting -- direction. rotate :: RadianAngle -> Vector -> Vector rotate angle (Vector x' y') = let x = intToDouble x' y = intToDouble y' -- Minus before the angle comes from our coordinates being -- mirrored along the X axis (Y coordinates grow going downwards). dx = x * cos (-angle) - y * sin (-angle) dy = x * sin (-angle) + y * cos (-angle) in normalize dx dy -- | Given a vector of arbitrary non-zero length, produce a unit vector -- that points in the same direction (in the chessboard metric). -- Of several equally good directions it picks one of those that visually -- (in the euclidean metric) maximally align with the original vector. normalize :: Double -> Double -> Vector normalize dx dy = assert (dx /= 0 || dy /= 0 `blame` "can't normalize zero" `swith` (dx, dy)) $ let angle :: Double angle = atan (dy / dx) / (pi / 2) dxy | angle <= -0.75 && angle >= -1.25 = (0, -1) | angle <= -0.25 = (1, -1) | angle <= 0.25 = (1, 0) | angle <= 0.75 = (1, 1) | angle <= 1.25 = (0, 1) | otherwise = error $ "impossible angle" `showFailure` (dx, dy, angle) in if dx >= 0 then uncurry Vector dxy else neg $ uncurry Vector dxy normalizeVector :: Vector -> Vector normalizeVector v@(Vector vx vy) = let res = normalize (intToDouble vx) (intToDouble vy) in assert (not (isUnit v) || v == res `blame` "unit vector gets untrivially normalized" `swith` (v, res)) res -- | Given two distinct positions, determine the direction (a unit vector) -- in which one should move from the first in order to get closer -- to the second. Ignores obstacles. Of several equally good directions -- (in the chessboard metric) it picks one of those that visually -- (in the euclidean metric) maximally align with the vector between -- the two points. towards :: Point -> Point -> Vector towards pos0 pos1 = assert (pos0 /= pos1 `blame` "towards self" `swith` (pos0, pos1)) $ normalizeVector $ pos1 `vectorToFrom` pos0 LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server.hs0000644000000000000000000000144107346545000020543 0ustar0000000000000000-- | Semantics of requests that are sent by clients to the server, -- in terms of game state changes and responses to be sent to the clients. -- -- See -- . module Game.LambdaHack.Server ( -- * Re-exported from "Game.LambdaHack.Server.LoopM" loopSer -- * Re-exported from "Game.LambdaHack.Server.ProtocolM" , ChanServer (..) -- * Re-exported from "Game.LambdaHack.Server.Commandline" , serverOptionsPI -- * Re-exported from "Game.LambdaHack.Server.ServerOptions" , ServerOptions(..) ) where import Prelude () import Game.LambdaHack.Server.Commandline (serverOptionsPI) import Game.LambdaHack.Server.LoopM (loopSer) import Game.LambdaHack.Server.ProtocolM import Game.LambdaHack.Server.ServerOptions LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/0000755000000000000000000000000007346545000020207 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/BroadcastAtomic.hs0000644000000000000000000005011507346545000023604 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | Sending atomic commands to clients and executing them on the server. -- -- See -- . module Game.LambdaHack.Server.BroadcastAtomic ( handleAndBroadcast, sendPer, handleCmdAtomicServer #ifdef EXPOSE_INTERNAL -- * Internal operations , cmdItemsFromIids, hearUpdAtomic, hearSfxAtomic, filterHear, atomicForget , atomicRemember #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified NLP.Miniutter.English as MU import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.TileKind (isUknownSpace) import qualified Game.LambdaHack.Core.Dice as Dice import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.ProtocolM import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State --storeUndo :: MonadServer m => CmdAtomic -> m () --storeUndo _atomic = -- maybe skip (\a -> modifyServer $ \ser -> ser {sundo = a : sundo ser}) -- $ Nothing -- undoCmdAtomic atomic handleCmdAtomicServer :: MonadServerAtomic m => UpdAtomic -> m (PosAtomic, [UpdAtomic], Bool) handleCmdAtomicServer cmd = do ps <- posUpdAtomic cmd atomicBroken <- breakUpdAtomic cmd -- needs to be done before the states are changed and may make no sense executedOnServer <- if seenAtomicSer ps then execUpdAtomicSer cmd else return False return (ps, atomicBroken, executedOnServer) -- | Send an atomic action to all clients that can see it. handleAndBroadcast :: (MonadServerAtomic m, MonadServerComm m) => PosAtomic -> [UpdAtomic] -> CmdAtomic -> m () handleAndBroadcast ps atomicBroken atomic = do -- This is calculated in the server State before action (simulating -- current client State, because action has not been applied -- on the client yet). -- E.g., actor's position in @breakUpdAtomic@ is assumed to be pre-action. -- To get rid of breakUpdAtomic we'd need to send only Spot and Lose -- commands instead of Move and Displace (plus Sfx for Displace). -- So this only makes sense when we switch to sending state diffs. knowEvents <- getsServer $ sknowEvents . soptions sperFidOld <- getsServer sperFid -- Send some actions to the clients, one faction at a time. let sendAtomic fid (UpdAtomic cmd) = do let iids = iidUpdAtomic cmd s <- getState sClient <- getsServer $ (EM.! fid) . sclientStates mapM_ (sendUpdateCheck fid) $ cmdItemsFromIids iids sClient s sendUpdate fid cmd sendAtomic fid (SfxAtomic sfx) = do let iids = iidSfxAtomic sfx s <- getState sClient <- getsServer $ (EM.! fid) . sclientStates mapM_ (sendUpdateCheck fid) $ cmdItemsFromIids iids sClient s sendSfx fid sfx breakSend fid perFid = case lidOfPos ps of Nothing -> return () Just lidOriginal -> do psBroken <- mapM posUpdAtomic atomicBroken case psBroken of _ : _ -> do let send2 (cmd2, ps2) = when (seenAtomicCli knowEvents fid perFid ps2) $ sendAtomic fid (UpdAtomic cmd2) mapM_ send2 $ zip atomicBroken psBroken [] -> do -- hear only here; broken commands are never loud -- At most @minusM@ applied total over a single actor move, -- to avoid distress as if wounded (which is measured via deltas). -- So, if faction hits an enemy and it yells, hearnig yell will -- not decrease calm over the decrease from hearing strike. -- This may accumulate over time, though, to eventually wake up -- sleeping actors. let drainCalmOnce aid = do b <- getsState $ getActorBody aid when (deltaBenign $ bcalmDelta b) $ execUpdAtomic $ UpdRefillCalm aid minusM leaderDistance pos = do mleader <- getsState $ gleader . (EM.! fid) . sfactionD case mleader of Nothing -> return Nothing Just leader -> do b <- getsState $ getActorBody leader -- Leader's hearing as relevant as of any other actor, -- which prevents changing leader just to get hearing -- intel. However, leader's position affects accuracy -- of the distance to noise hints. return $ Just $ max 0 $ min 5 $ flip (-) 1 $ floor $ sqrt $ intToDouble $ chessDist pos (bpos b) -- Projectiles never hear, for speed and simplicity, -- even though they sometimes see. There are flying cameras, -- but no microphones --- drones make too much noise themselves. as <- getsState $ fidActorRegularAssocs fid lidOriginal case atomic of UpdAtomic cmd -> do (profound, mpos) <- hearUpdAtomic cmd case mpos of Nothing | profound -> sendUpdate fid $ UpdHearFid fid Nothing $ HearUpd cmd Nothing -> return () Just pos -> do aids <- filterHear pos as if null aids && not profound then return () else do distance <- if null aids then return Nothing else leaderDistance pos sendUpdate fid $ UpdHearFid fid distance $ HearUpd cmd mapM_ drainCalmOnce aids SfxAtomic cmd -> do mhear <- hearSfxAtomic cmd case mhear of Nothing -> return () Just (hearMsg, profound, pos) -> do aids <- filterHear pos as if null aids && not profound then return () else do distance <- if null aids then return Nothing else leaderDistance pos sendUpdate fid $ UpdHearFid fid distance hearMsg mapM_ drainCalmOnce aids -- We assume players perceive perception change before the action, -- so the action is perceived in the new perception, -- even though the new perception depends on the action's outcome -- (e.g., new actor created). send fid = do let perFid = sperFidOld EM.! fid if seenAtomicCli knowEvents fid perFid ps then sendAtomic fid atomic else breakSend fid perFid -- Factions that are eliminated by the command are processed as well, -- because they are not deleted from @sfactionD@. factionD <- getsState sfactionD mapM_ send $ EM.keys factionD cmdItemsFromIids :: [ItemId] -> State -> State -> [UpdAtomic] cmdItemsFromIids iids sClient s = let iidsUnknown = filter (\iid -> EM.notMember iid $ sitemD sClient) iids items = map (\iid -> (iid, sitemD s EM.! iid)) iidsUnknown in [UpdRegisterItems items | not $ null items] -- | Messages for some unseen atomic commands. hearUpdAtomic :: MonadStateRead m => UpdAtomic -> m (Bool, Maybe Point) hearUpdAtomic cmd = do COps{coTileSpeedup} <- getsState scops case cmd of UpdDestroyActor _ body _ | not $ bproj body -> return (True, Just $ bpos body) UpdCreateItem True iid item _ (CActor aid cstore) -> do -- Kinetic damage implies the explosion is loud enough to cause noise. itemKind <- getsState $ getItemKindServer item discoAspect <- getsState sdiscoAspect let arItem = discoAspect EM.! iid if cstore /= COrgan || IA.checkFlag Ability.Blast arItem && Dice.supDice (IK.idamage itemKind) > 0 then do body <- getsState $ getActorBody aid return (True, Just $ bpos body) else return (False, Nothing) UpdTrajectory aid (Just (l, _)) Nothing | not (null l) -> do -- Non-blast projectile hits a non-walkable tile. b <- getsState $ getActorBody aid discoAspect <- getsState sdiscoAspect let arTrunk = discoAspect EM.! btrunk b return ( False, if not (bproj b) || IA.checkFlag Ability.Blast arTrunk then Nothing else Just $ bpos b ) UpdAlterTile _ p _ toTile -> return (not $ Tile.isDoor coTileSpeedup toTile, Just p) UpdAlterExplorable{} -> return (True, Nothing) _ -> return (False, Nothing) -- | Messages for some unseen sfx. hearSfxAtomic :: MonadServer m => SfxAtomic -> m (Maybe (HearMsg, Bool, Point)) hearSfxAtomic cmd = case cmd of SfxStrike aid _ iid -> do -- Only the attacker position considered, for simplicity. b <- getsState $ getActorBody aid discoAspect <- getsState sdiscoAspect let arItem = discoAspect EM.! iid itemKindId <- getsState $ getIidKindIdServer iid -- Loud explosions cause enough noise, so ignoring particle hit spam. return $! if IA.checkFlag Ability.Blast arItem then Nothing else Just (HearStrike itemKindId, False, bpos b) SfxEffect _ aid _ (IK.Summon grp p) _ -> do b <- getsState $ getActorBody aid return $ Just (HearSummon (bproj b) grp p, False, bpos b) SfxEffect _ aid _ (IK.VerbMsg verb ending) _ -> do b <- getsState $ getActorBody aid discoAspect <- getsState sdiscoAspect let arTrunk = discoAspect EM.! btrunk b subject = "noises of someone that" phrase = makePhrase [MU.SubjectVerbSg subject (MU.Text verb)] <> ending return $! if IA.checkFlag Ability.Unique arTrunk then Just (HearTaunt phrase, True, bpos b) else Nothing SfxCollideTile _ p -> return $ Just (HearCollideTile, False, p) SfxTaunt voluntary aid -> do b <- getsState $ getActorBody aid (subject, verb) <- displayTaunt voluntary rndToAction aid discoAspect <- getsState sdiscoAspect let arTrunk = discoAspect EM.! btrunk b unique = if IA.checkFlag Ability.Unique arTrunk then "big" else "" phrase = subject <+> unique <+> verb <> "." return $ Just (HearTaunt phrase, True, bpos b) -- intentional _ -> return Nothing filterHear :: MonadStateRead m => Point -> [(ActorId, Actor)] -> m [ActorId] filterHear pos as = do let actorHear (aid, body) = do -- Actors hear as if they were leaders, for speed and to prevent -- micromanagement by switching leader to hear more. -- This is analogous to actors seeing as if they were leaders. actorMaxSk <- getsState $ getActorMaxSkills aid return $! Ability.getSk Ability.SkHearing actorMaxSk >= chessDist pos (bpos body) map fst <$> filterM actorHear as sendPer :: (MonadServerAtomic m, MonadServerComm m) => FactionId -> LevelId -> Perception -> Perception -> Perception -> m () sendPer fid lid outPer inPer perNew = do knowEvents <- getsServer $ sknowEvents . soptions unless knowEvents $ do -- inconsistencies would quickly manifest sendUpdNoState fid $ UpdPerception lid outPer inPer sClient <- getsServer $ (EM.! fid) . sclientStates let forget = atomicForget fid lid outPer sClient remember <- getsState $ atomicRemember lid inPer sClient let seenNew = seenAtomicCli False fid (EM.singleton lid perNew) onLevel UpdRegisterItems{} = True onLevel UpdLoseStashFaction{} = True onLevel _ = False psRem <- mapM posUpdAtomic $ filter (not . onLevel) remember -- Verify that we remember the currently seen things. let !_A = assert (allB seenNew psRem) () mapM_ (sendUpdateCheck fid) forget mapM_ (sendUpdate fid) remember -- Remembered items, map tiles, smells and stashes are not wiped out -- when they get out of FOV. Clients remember them. Only actors are forgotten. atomicForget :: FactionId -> LevelId -> Perception -> State -> [UpdAtomic] atomicForget side lid outPer sClient = -- Wipe out actors that just became invisible due to changed FOV. let outFov = totalVisible outPer fActor (aid, b) = -- We forget only currently invisible actors. Actors can be outside -- perception, but still visible, if they belong to our faction, -- e.g., if they teleport to outside of current perception -- or if they have disabled senses. UpdLoseActor aid b -- this command always succeeds, the actor can be always removed, -- because the actor is taken from the state outPrioBig = mapMaybe (\p -> posToBigAssoc p lid sClient) $ ES.elems outFov outPrioProj = concatMap (\p -> posToProjAssocs p lid sClient) $ ES.elems outFov in map fActor $ filter ((/= side) . bfid . snd) outPrioBig ++ outPrioProj -- The second argument are the points newly in FOV. atomicRemember :: LevelId -> Perception -> State -> State -> [UpdAtomic] {-# INLINE atomicRemember #-} atomicRemember lid inPer sClient s = let COps{cotile, coTileSpeedup} = scops s locateStash ((fidClient, factClient), (fid, fact)) = assert (fidClient == fid) $ case (gstash factClient, gstash fact) of (Just (lidStash, pos), Nothing) | lidStash == lid && pos `ES.member` totalVisible inPer -> [UpdLoseStashFaction False fid lid pos] (Nothing, Just (lidStash, pos)) | lidStash == lid && pos `ES.member` totalVisible inPer -> [UpdSpotStashFaction True fid lid pos] (Just (lidStash1, pos1), Just (lidStash2, pos2)) | gstash factClient /= gstash fact -> if | lidStash2 == lid && pos2 `ES.member` totalVisible inPer -> [ UpdLoseStashFaction False fid lidStash1 pos1 , UpdSpotStashFaction True fid lid pos2 ] | lidStash1 == lid && pos1 `ES.member` totalVisible inPer -> [UpdLoseStashFaction False fid lid pos1] | otherwise -> [] _ -> [] atomicStash = concatMap locateStash $ zip (EM.assocs $ sfactionD sClient) (EM.assocs $ sfactionD s) inFov = ES.elems $ totalVisible inPer lvl = sdungeon s EM.! lid -- Wipe out remembered items on tiles that now came into view -- and spot items on these tiles. Optimized away, when items match. lvlClient = sdungeon sClient EM.! lid inContainer allow fc bagEM bagEMClient = let f p = case (EM.lookup p bagEM, EM.lookup p bagEMClient) of (Nothing, Nothing) -> [] -- most common, no items ever (Just bag, Nothing) -> -- common, client unaware cmdItemsFromIids (EM.keys bag) sClient s ++ [UpdSpotItemBag True (fc lid p) bag | allow p] (Nothing, Just bagClient) -> -- uncommon, all items vanished -- We don't check @allow@, because client sees items there, -- so we assume he's aware of the tile enough to notice. [UpdLoseItemBag True (fc lid p) bagClient] (Just bag, Just bagClient) -> -- We don't check @allow@, because client sees items there, -- so we assume he's aware of the tile enough to see new items. if bag == bagClient then [] -- common, nothing has changed, so optimized else -- uncommon, surprise; because it's rare, we send -- whole bags and don't optimize by sending only delta cmdItemsFromIids (EM.keys bag) sClient s ++ [ UpdLoseItemBag True (fc lid p) bagClient , UpdSpotItemBag True (fc lid p) bag ] in concatMap f inFov inFloor = inContainer (const True) CFloor (lfloor lvl) (lfloor lvlClient) -- Check that client may be shown embedded items, assuming he's not seeing -- any at this position so far. If he's not shown now, the items will be -- revealed via searching the tile later on. -- This check is essential to prevent embedded items from leaking -- tile identity. allowEmbed p = not (Tile.isHideAs coTileSpeedup $ lvl `at` p) || lvl `at` p == lvlClient `at` p inEmbed = inContainer allowEmbed CEmbed (lembed lvl) (lembed lvlClient) -- Spot tiles. atomicTile = -- We ignore the server resending us hidden versions of the tiles -- (or resending us the same data we already got). -- If the tiles are changed to other variants of the hidden tile, -- we can still verify by searching. let f p (loses1, spots1, entries1) = let t = lvl `at` p tHidden = fromMaybe t $ Tile.hideAs cotile t tClient = lvlClient `at` p entries2 = case EM.lookup p $ lentry lvl of Nothing -> entries1 Just entry2 -> case EM.lookup p $ lentry lvlClient of Nothing -> (p, entry2) : entries1 Just entry3 -> assert (entry3 == entry2) entries1 -- avoid resending entries if client previously saw -- another not hidden tile at that position in if tClient `elem` [t, tHidden] then (loses1, spots1, entries1) else ( if isUknownSpace tClient then loses1 else (p, tClient) : loses1 , (p, tHidden) : spots1 -- send the hidden version , if tHidden == t then entries2 else entries1) (loses, spots, entries) = foldr f ([], [], []) inFov in [UpdLoseTile lid loses | not $ null loses] ++ [UpdSpotTile lid spots | not $ null spots] ++ [UpdSpotEntry lid entries | not $ null entries] -- Wipe out remembered smell on tiles that now came into smell Fov. -- Smell radius is small, so we can just wipe and send all. -- TODO: only send smell younger than ltime (states get out of sync) -- or remove older smell elsewhere in the code each turn (expensive). -- For now clients act as if this was the case, not peeking into old. inSmellFov = ES.elems $ totalSmelled inPer inSm = mapMaybe (\p -> (p,) <$> EM.lookup p (lsmell lvlClient)) inSmellFov inSmell = [UpdLoseSmell lid inSm | not $ null inSm] -- Spot smells. inSm2 = mapMaybe (\p -> (p,) <$> EM.lookup p (lsmell lvl)) inSmellFov atomicSmell = [UpdSpotSmell lid inSm2 | not $ null inSm2] -- Actors come last to report the environment they land on. inAssocs = concatMap (\p -> posToAidAssocs p lid s) inFov -- Here, the actor may be already visible, e.g., when teleporting, -- so the exception is caught in @sendUpdate@ above. fActor (aid, b) = cmdItemsFromIids (getCarriedIidsAndTrunk b) sClient s ++ [UpdSpotActor aid b] inActor = concatMap fActor inAssocs in atomicStash ++ inActor ++ inSmell ++ atomicSmell ++ inFloor ++ atomicTile ++ inEmbed LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/Commandline.hs0000644000000000000000000002567607346545000023011 0ustar0000000000000000{-# LANGUAGE ApplicativeDo #-} -- | Parsing of commandline arguments. module Game.LambdaHack.Server.Commandline ( serverOptionsPI #ifdef EXPOSE_INTERNAL -- * Internal operations , serverOptionsP -- other internal operations too numerous and changing, so not listed #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude -- Cabal import qualified Paths_LambdaHack as Self (version) import qualified Data.Text as T import Data.Version import Options.Applicative import qualified System.Random.SplitMix32 as SM -- Dependence on ClientOptions is an anomaly. Instead, probably the raw -- remaining commandline should be passed and parsed by the client to extract -- client and ui options from and singnal an error if anything was left. import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Definition.Defs import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal import Game.LambdaHack.Server.ServerOptions -- | Parser for server options from commandline arguments. serverOptionsPI :: ParserInfo ServerOptions serverOptionsPI = info (serverOptionsP <**> helper <**> version) $ fullDesc <> progDesc "Configure debug options here, gameplay options in configuration file." version :: Parser (a -> a) version = infoOption (showVersion Self.version) (long "version" <> help "Print engine version information") serverOptionsP :: Parser ServerOptions serverOptionsP = do ~(snewGameSer, scurChalSer) <- serToChallenge <$> newGameP sfullscreenMode <- fullscreenModeP knowMap <- knowMapP knowEvents <- knowEventsP knowItems <- knowItemsP showItemSamples <- showItemSamplesP sexposePlaces <- exposePlacesP sexposeItems <- exposeItemsP sexposeActors <- exposeActorsP sniff <- sniffP sallClear <- allClearP sboostRandomItem <- boostRandItemP sgameMode <- gameModeP sautomateAll <- automateAllP skeepAutomated <- keepAutomatedP sstopAfterSeconds <- stopAfterSecsP sstopAfterFrames <- stopAfterFramesP sstopAfterGameOver <- stopAfterGameOverP sprintEachScreen <- printEachScreenP sbenchmark <- benchmarkP sbenchMessages <- benchMessagesP sdungeonRng <- setDungeonRngP smainRng <- setMainRngP sdumpInitRngs <- dumpInitRngsP sdbgMsgCli <- dbgMsgCliP sdbgMsgSer <- dbgMsgSerP slogPriority <- logPriorityP sassertExplored <- assertExploredP schosenFontset <- chosenFontsetP sallFontsScale <- allFontsScaleP smaxFps <- maxFpsP sdisableAutoYes <- disableAutoYesP snoAnim <- noAnimP ssavePrefixSer <- savePrefixP sfrontendANSI <- frontendANSIP sfrontendTeletype <- frontendTeletypeP sfrontendNull <- frontendNullP sfrontendLazy <- frontendLazyP pure ServerOptions { sclientOptions = ClientOptions { sfonts = [] -- comes only from config file , sfontsets = [] -- comes only from config file , stitle = Nothing , snewGameCli = snewGameSer , ssavePrefixCli = ssavePrefixSer , .. } , sknowMap = knowMap || knowEvents || knowItems , sknowEvents = knowEvents || knowItems , sknowItems = knowItems , sshowItemSamples = not (knowEvents || knowItems) && showItemSamples , .. } where serToChallenge :: Maybe Int -> (Bool, Challenge) serToChallenge Nothing = (False, defaultChallenge) serToChallenge (Just cdiff) = (True, defaultChallenge {cdiff}) knowMapP :: Parser Bool knowMapP = switch ( long "knowMap" <> help "Reveal map for all clients in the next game" ) knowEventsP :: Parser Bool knowEventsP = switch ( long "knowEvents" <> help "Show all events in the next game (implies --knowMap)" ) knowItemsP :: Parser Bool knowItemsP = switch ( long "knowItems" <> help "Auto-identify all items in the next game (implies --knowEvents)" ) exposePlacesP :: Parser Bool exposePlacesP = switch ( long "exposePlaces" <> help "Expose all possible places in the next game" ) exposeItemsP :: Parser Bool exposeItemsP = switch ( long "exposeItems" <> help "Expose all possible items in the next game" ) exposeActorsP :: Parser Bool exposeActorsP = switch ( long "exposeActors" <> help "Expose all killable actors in the next game" ) showItemSamplesP :: Parser Bool showItemSamplesP = switch ( long "showItemSamples" <> help "At game over show samples of all items (--knowEvents disables this)" ) sniffP :: Parser Bool sniffP = switch ( long "sniff" <> help "Monitor all trafic between server and clients" ) allClearP :: Parser Bool allClearP = switch ( long "allClear" <> help "Let all map tiles be translucent" ) boostRandItemP :: Parser Bool boostRandItemP = switch ( long "boostRandomItem" <> help "Pick a random item and make it very common" ) gameModeP :: Parser (Maybe (GroupName ModeKind)) gameModeP = optional $ toGameMode <$> option nonEmptyStr ( long "gameMode" <> metavar "MODE" <> help "Start next game in the scenario indicated by MODE" ) where -- This ignores all but the first word of a game mode name -- and assumes the fist word is present among its frequencies. toGameMode :: String -> GroupName ModeKind toGameMode = DefsInternal.GroupName . head . T.words . T.pack nonEmptyStr :: ReadM String nonEmptyStr = eitherReader $ \case "" -> Left "name of game mode cannot be empty" ns -> Right ns automateAllP :: Parser Bool automateAllP = switch ( long "automateAll" <> help "Give control of all UI teams to computer" ) keepAutomatedP :: Parser Bool keepAutomatedP = switch ( long "keepAutomated" <> help "Keep factions automated after game over" ) newGameP :: Parser (Maybe Int) newGameP = optional $ max 1 . min difficultyBound <$> option auto ( long "newGame" <> help "Start a new game, overwriting the save file and often forgetting history, with difficulty for all UI players set to N" <> metavar "N" ) fullscreenModeP :: Parser (Maybe FullscreenMode) fullscreenModeP = optional $ option auto ( long "fullscreenMode" <> short 'f' <> metavar "MODE" <> help "Display in MODE, one of NotFullscreen (default), BigBorderlessWindow (preferred), ModeChange" ) stopAfterSecsP :: Parser (Maybe Int) stopAfterSecsP = optional $ max 0 <$> option auto ( long "stopAfterSeconds" <> help "Exit game session after around N seconds" <> metavar "N" ) stopAfterFramesP :: Parser (Maybe Int) stopAfterFramesP = optional $ max 0 <$> option auto ( long "stopAfterFrames" <> help "Exit game session after around N frames" <> metavar "N" ) stopAfterGameOverP :: Parser Bool stopAfterGameOverP = switch ( long "stopAfterGameOver" <> help "Exit the application after game over" ) printEachScreenP :: Parser Bool printEachScreenP = switch ( long "printEachScreen" <> help "Take a screenshot of each rendered distinct frame (SDL only)" ) benchmarkP :: Parser Bool benchmarkP = switch ( long "benchmark" <> help "Restrict file IO, print timing stats" ) benchMessagesP :: Parser Bool benchMessagesP = switch ( long "benchMessages" <> help "Display messages in realistic was under AI control (for benchmarks)" ) setDungeonRngP :: Parser (Maybe SM.SMGen) setDungeonRngP = optional $ option auto ( long "setDungeonRng" <> metavar "RNG_SEED" <> help "Set dungeon generation RNG seed to string RNG_SEED" ) setMainRngP :: Parser (Maybe SM.SMGen) setMainRngP = optional $ option auto ( long "setMainRng" <> metavar "RNG_SEED" <> help "Set the main game RNG seed to string RNG_SEED" ) dumpInitRngsP :: Parser Bool dumpInitRngsP = switch ( long "dumpInitRngs" <> help "Dump the RNG seeds used to initialize the game" ) dbgMsgCliP :: Parser Bool dbgMsgCliP = switch ( long "dbgMsgCli" <> help "Emit extra internal client debug messages" ) dbgMsgSerP :: Parser Bool dbgMsgSerP = switch ( long "dbgMsgSer" <> help "Emit extra internal server debug messages" ) logPriorityP :: Parser (Maybe Int) logPriorityP = optional $ option (auto >>= verifyLogPriority) $ long "logPriority" <> showDefault <> value 5 <> metavar "N" <> help ( "Log only messages of priority at least N, where 1 (all) is " ++ "the lowest and 5 logs errors only; use value 0 for testing on " ++ "CIs without graphics access; setting priority to 0 causes " ++ "SDL frontend to init and quit at once" ) where verifyLogPriority n = if n >= 0 && n <= 5 then return n else readerError "N has to be 0 or a positive integer not larger than 5" assertExploredP :: Parser (Maybe Int) assertExploredP = optional $ max 1 <$> option auto ( long "assertExplored" <> help "Check that when the session ends, the indicated level has been explored" <> metavar "N" ) chosenFontsetP :: Parser (Maybe Text) chosenFontsetP = optional $ T.pack <$> strOption ( long "fontset" <> metavar "FONTSET_ID" <> help "Render UI using the given fontset from config file" ) allFontsScaleP :: Parser (Maybe Double) allFontsScaleP = optional $ max 0 <$> option auto ( long "allFontsScale" <> metavar "D" <> help "Scale all fonts by D, resizing the whole UI" ) maxFpsP :: Parser (Maybe Double) maxFpsP = optional $ max 0 <$> option auto ( long "maxFps" <> metavar "D" <> help "Display at most D frames per second" ) disableAutoYesP :: Parser Bool disableAutoYesP = switch ( long "disableAutoYes" <> help "Never auto-answer prompts, not even when UI faction is automated" ) noAnimP :: Parser (Maybe Bool) noAnimP = flag Nothing (Just True) ( long "noAnim" <> help "Don't show any animations" ) savePrefixP :: Parser String savePrefixP = strOption ( long "savePrefix" <> metavar "PREFIX" <> showDefault <> value "" <> help "Prepend PREFIX to all savefile names" ) frontendANSIP :: Parser Bool frontendANSIP = switch ( long "frontendANSI" <> help "Use the ANSI terminal frontend (best for screen readers)" ) frontendTeletypeP :: Parser Bool frontendTeletypeP = switch ( long "frontendTeletype" <> help "Use the line terminal frontend (for tests)" ) frontendNullP :: Parser Bool frontendNullP = switch ( long "frontendNull" <> help "Use frontend with no display (for benchmarks)" ) frontendLazyP :: Parser Bool frontendLazyP = switch ( long "frontendLazy" <> help "Use frontend that not even computes frames (for benchmarks)" ) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/CommonM.hs0000644000000000000000000012143307346545000022114 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | Server operations common to many modules. module Game.LambdaHack.Server.CommonM ( revealAll, generalMoveItem, deduceQuits , writeSaveAll, verifyCaches, deduceKilled, electLeader, setFreshLeader , updatePer, projectFail, addActorFromGroup, registerActor , discoverIfMinorEffects, pickWeaponServer, currentSkillsServer, allGroupItems , addCondition, removeConditionSingle, addSleep, removeSleepSingle , addKillToAnalytics #ifdef EXPOSE_INTERNAL -- * Internal operations , revealItems, revealPerceptionLid, containerMoveItem, quitF, keepArenaFact , anyActorsAlive, updatePerFromNew, recomputeCachePer , projectBla, addProjectile, addNonProjectile, addActorIid , getCacheLucid, getCacheTotal #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.IntMap.Strict as IM import Data.Ratio import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.CaveKind as CK import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal import Game.LambdaHack.Server.Fov import Game.LambdaHack.Server.ItemM import Game.LambdaHack.Server.ItemRev import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State revealItems :: MonadServerAtomic m => FactionId -> m () revealItems fid = do COps{coitem} <- getsState scops ServerOptions{sclientOptions} <- getsServer soptions discoAspect <- getsState sdiscoAspect let keptSecret kind ar = IA.isHumanTrinket kind || IA.checkFlag Ability.MetaGame ar discover aid store iid _ = do itemKindId <- getsState $ getIidKindIdServer iid let arItem = discoAspect EM.! iid c = CActor aid store itemKind = okind coitem itemKindId unless (keptSecret itemKind arItem) $ -- a hack execUpdAtomic $ UpdDiscover c iid itemKindId arItem f (aid, b) = -- CStash is IDed for each actor of each faction, which is fine, -- even though it may introduce a slight lag at gameover. join $ getsState $ mapActorItems_ (discover aid) b -- Don't ID projectiles, their items are not really owned by the party. aids <- getsState $ fidActorNotProjGlobalAssocs fid mapM_ f aids dungeon <- getsState sdungeon let minLid = fst $ minimumBy (comparing (ldepth . snd)) $ EM.assocs dungeon discoverSample iid = do itemKindId <- getsState $ getIidKindIdServer iid let arItem = discoAspect EM.! iid cdummy = CTrunk fid minLid originPoint itemKind = okind coitem itemKindId -- Due to @cdummy@, the met and unmet secret things will appear -- at gameover among actors in the debug mode. Tough luck. execUpdAtomic $ if keptSecret itemKind arItem then UpdSpotItem False iid quantSingle cdummy else UpdDiscover cdummy iid itemKindId arItem generationAn <- getsServer sgenerationAn getKindId <- getsState $ flip getIidKindIdServer let kindsEqual iid iid2 = getKindId iid == getKindId iid2 && iid /= iid2 nonDupSample em iid 0 = not $ any (kindsEqual iid) $ EM.keys em nonDupSample _ _ _ = True nonDupGen = EM.map (\em -> EM.filterWithKey (nonDupSample em) em) generationAn -- Remove samples that are supplanted by real items. -- If there are mutliple UI factions, the second run will be vacuus, -- but it's important to do that before the first try to identify things -- to prevent spam from identifying samples that are not needed. modifyServer $ \ser -> ser {sgenerationAn = nonDupGen} when (sexposeActors sclientOptions) $ -- Few, if any, need ID, but we can't rule out unusual content. mapM_ discoverSample $ EM.keys $ nonDupGen EM.! STrunk when (sexposeItems sclientOptions) $ do mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SItem mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SEmbed mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SOrgan mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SCondition mapM_ discoverSample $ EM.keys $ nonDupGen EM.! SBlast revealAll :: MonadServerAtomic m => FactionId -> m () revealAll fid = do revealItems fid execUpdAtomic $ UpdMuteMessages fid True dungeon <- getsState sdungeon -- Perception needs to be sent explicitly, because normal management -- assumes an action must happen on a level to invalidate and regenerate -- perception on the level (and actors must survive!). -- Also, we'd rather hack here and in `verifyCaches` that complicate -- the already complex perception creation and caching code. mapM_ (revealPerceptionLid fid) $ EM.assocs dungeon execUpdAtomic $ UpdMuteMessages fid False revealPerceptionLid :: MonadServerAtomic m => FactionId -> (LevelId, Level) -> m () revealPerceptionLid fid (lid, lvl) = do let (x0, y0, x1, y1) = fromArea $ larea lvl fullSet = ES.fromDistinctAscList [ Point x y | y <- [y0 .. y1] , x <- [x0 .. x1] ] perNew = Perception { psight = PerVisible fullSet , psmell = PerSmelled ES.empty -- don't obscure } updatePerFromNew fid lid perNew -- | Generate the atomic updates that jointly perform a given item move. generalMoveItem :: MonadStateRead m => Bool -> ItemId -> Int -> Container -> Container -> m [UpdAtomic] generalMoveItem _ iid k (CActor aid1 cstore1) c2@(CActor aid2 cstore2) | aid1 == aid2 = do moveStash <- moveStashIfNeeded c2 return $! moveStash ++ [UpdMoveItem iid k aid1 cstore1 cstore2] generalMoveItem verbose iid k c1 c2 = containerMoveItem verbose iid k c1 c2 containerMoveItem :: MonadStateRead m => Bool -> ItemId -> Int -> Container -> Container -> m [UpdAtomic] containerMoveItem verbose iid k c1 c2 = do bag <- getsState $ getContainerBag c1 case iid `EM.lookup` bag of Nothing -> error $ "" `showFailure` (iid, k, c1, c2) Just (_, it) -> do moveStash <- moveStashIfNeeded c2 return $ [UpdLoseItem verbose iid (k, take k it) c1] ++ moveStash ++ [UpdSpotItem verbose iid (k, take k it) c2] quitF :: MonadServerAtomic m => Status -> FactionId -> m () quitF status fid = do fact <- getsState $ (EM.! fid) . sfactionD let oldSt = gquit fact -- Note that it's the _old_ status that we check here. case stOutcome <$> oldSt of Just Killed -> return () -- Do not overwrite in case Just Defeated -> return () -- many things happen in 1 turn. Just Conquer -> return () Just Escape -> return () _ -> do let !_A = assert (stOutcome status `notElem` [Camping, Restart] `blame` "Camping and Restart are handled separately" `swith` (stOutcome <$> oldSt, status, fid)) () -- This runs regardless of the _new_ status. manalytics <- if fhasUI $ gkind fact then do keepAutomated <- getsServer $ skeepAutomated . soptions -- Try to remove AI control of the UI faction, to show gameover info. when (gunderAI fact && not keepAutomated) $ execUpdAtomic $ UpdAutoFaction fid False revealAll fid -- Likely, by this time UI faction is no longer AI-controlled, -- so the score will get registered. registerScore status fid factionAn <- getsServer sfactionAn generationAn <- getsServer sgenerationAn return $ Just (factionAn, generationAn) else return Nothing execUpdAtomic $ UpdQuitFaction fid oldSt (Just status) manalytics modifyServer $ \ser -> ser {sbreakLoop = True} -- check game over -- Send any UpdQuitFaction actions that can be deduced from factions' -- current state. deduceQuits :: MonadServerAtomic m => FactionId -> Status -> m () deduceQuits fid0 status@Status{stOutcome} | stOutcome `elem` [Defeated, Camping, Restart, Conquer] = error $ "no quitting to deduce" `showFailure` (fid0, status) deduceQuits fid0 status = do fact0 <- getsState $ (EM.! fid0) . sfactionD let factHasUI = fhasUI . gkind quitFaction (stOutcome, (fid, _)) = quitF status{stOutcome} fid mapQuitF outfids = do let (withUI, withoutUI) = partition (factHasUI . snd . snd) ((stOutcome status, (fid0, fact0)) : outfids) mapM_ quitFaction (withoutUI ++ withUI) inGameOutcome (fid, fact) = do let mout | fid == fid0 = Just $ stOutcome status | otherwise = stOutcome <$> gquit fact case mout of Just Killed -> False Just Defeated -> False Just Restart -> False -- effectively, commits suicide _ -> True factionD <- getsState sfactionD let assocsInGame = filter inGameOutcome $ EM.assocs factionD assocsKeepArena = filter (keepArenaFact . snd) assocsInGame assocsUI = filter (factHasUI . snd) assocsInGame nonHorrorAIG = filter (not . isHorrorFact . snd) assocsInGame worldPeace = all (\(fid1, _) -> all (\(fid2, fact2) -> not $ isFoe fid2 fact2 fid1) nonHorrorAIG) nonHorrorAIG othersInGame = filter ((/= fid0) . fst) assocsInGame if | null assocsUI -> -- Only non-UI players left in the game and they all win. mapQuitF $ zip (repeat Conquer) othersInGame | null assocsKeepArena -> -- Only leaderless and spawners remain (the latter may sometimes -- have no leader, just as the former), so they win, -- or we could get stuck in a state with no active arena -- and so no spawns. mapQuitF $ zip (repeat Conquer) othersInGame | worldPeace -> -- Nobody is at war any more, so all win (e.g., horrors, but never mind). mapQuitF $ zip (repeat Conquer) othersInGame | stOutcome status == Escape -> do -- Otherwise, in a game with many warring teams alive, -- only complete Victory matters, until enough of them die. let (victors, losers) = partition (\(fi, _) -> isFriend fid0 fact0 fi) othersInGame mapQuitF $ zip (repeat Escape) victors ++ zip (repeat Defeated) losers | otherwise -> quitF status fid0 -- | Save game on server and all clients. writeSaveAll :: MonadServerAtomic m => Bool -> Bool -> m () writeSaveAll uiRequested evenForNoConfirmGames = do bench <- getsServer $ sbenchmark . sclientOptions . soptions noConfirmsGame <- isNoConfirmsGame when (uiRequested || not bench && (not noConfirmsGame || evenForNoConfirmGames)) $ do execUpdAtomic UpdWriteSave saveServer #ifdef WITH_EXPENSIVE_ASSERTIONS -- This check is sometimes repeated in @gameExit@, but we don't care about -- speed of shutdown and even more so in WITH_EXPENSIVE_ASSERTIONS mode. verifyCaches #endif verifyCaches :: MonadServer m => m () verifyCaches = do sperCacheFid <- getsServer sperCacheFid sperValidFid <- getsServer sperValidFid sactorMaxSkills2 <- getsState sactorMaxSkills sfovLucidLid <- getsServer sfovLucidLid sfovClearLid <- getsServer sfovClearLid sfovLitLid <- getsServer sfovLitLid sperFid <- getsServer sperFid actorMaxSkills <- getsState maxSkillsInDungeon ( fovLitLid, fovClearLid, fovLucidLid ,perValidFid, perCacheFid, perFid ) <- getsState perFidInDungeon rngs <- getsServer srngs -- initial display may scroll off terminal memory factionD <- getsState sfactionD -- Perception off UI faction at game over is illegal (revealed to the player -- in 'revealAll'), which is fine, because it's never used. -- Don't verify perception in such cases. All the caches from which -- legal perception would be created at that point are legal and verified, -- which is almost as tight. let gameOverUI fact = fhasUI (gkind fact) && maybe False ((/= Camping) . stOutcome) (gquit fact) isGameOverUI = any gameOverUI $ EM.elems factionD !_A7 = assert (sfovLitLid == fovLitLid `blame` "wrong accumulated sfovLitLid" `swith` (sfovLitLid, fovLitLid, rngs)) () !_A6 = assert (sfovClearLid == fovClearLid `blame` "wrong accumulated sfovClearLid" `swith` (sfovClearLid, fovClearLid, rngs)) () !_A5 = assert (sactorMaxSkills2 == actorMaxSkills `blame` "wrong accumulated sactorMaxSkills" `swith` (sactorMaxSkills2, actorMaxSkills, rngs)) () !_A4 = assert (sfovLucidLid == fovLucidLid `blame` "wrong accumulated sfovLucidLid" `swith` (sfovLucidLid, fovLucidLid, rngs)) () !_A3 = assert (sperValidFid == perValidFid `blame` "wrong accumulated sperValidFid" `swith` (sperValidFid, perValidFid, rngs)) () !_A2 = assert (sperCacheFid == perCacheFid `blame` "wrong accumulated sperCacheFid" `swith` (sperCacheFid, perCacheFid, rngs)) () !_A1 = assert (isGameOverUI || sperFid == perFid `blame` "wrong accumulated perception" `swith` (sperFid, perFid, rngs)) () return () -- | Tell whether a faction that we know is still in game, keeps arena. -- Keeping arena means, if the faction is still in game, -- it always has a leader in the dungeon somewhere. -- So, leaderless factions and spawner factions do not keep an arena, -- even though the latter usually has a leader for most of the game. keepArenaFact :: Faction -> Bool keepArenaFact fact = fhasPointman (gkind fact) && fneverEmpty (gkind fact) -- We assume the actor in the second argument has HP <= 0 or is going to be -- dominated right now. Even if the actor is to be dominated, -- @bfid@ of the actor body is still the old faction. deduceKilled :: MonadServerAtomic m => ActorId -> m () deduceKilled aid = do body <- getsState $ getActorBody aid fact <- getsState $ (EM.! bfid body) . sfactionD when (fneverEmpty $ gkind fact) $ do actorsAlive <- anyActorsAlive (bfid body) aid unless actorsAlive $ deduceQuits (bfid body) $ Status Killed (fromEnum $ blid body) Nothing anyActorsAlive :: MonadServer m => FactionId -> ActorId -> m Bool anyActorsAlive fid aid = do as <- getsState $ fidActorNotProjGlobalAssocs fid -- We test HP here, in case more than one actor goes to 0 HP in the same turn. return $! any (\(aid2, b2) -> aid2 /= aid && bhp b2 > 0) as electLeader :: MonadServerAtomic m => FactionId -> LevelId -> ActorId -> m () electLeader fid lid aidToReplace = do mleader <- getsState $ gleader . (EM.! fid) . sfactionD when (mleader == Just aidToReplace) $ do allOurs <- getsState $ fidActorNotProjGlobalAssocs fid -- not only on level let -- Prefer actors on this level and with positive HP and not sleeping. -- Exclude @aidToReplace@, even if not dead (e.g., if being dominated). (positive, negative) = partition (\(_, b) -> bhp b > 0) allOurs (awake, sleeping) = partition (\(_, b) -> bwatch b /= WSleep) positive onThisLevel <- getsState $ fidActorRegularAssocs fid lid let candidates = filter (\(_, b) -> bwatch b /= WSleep) onThisLevel ++ awake ++ sleeping ++ negative mleaderNew = find (/= aidToReplace) $ map fst candidates execUpdAtomic $ UpdLeadFaction fid mleader mleaderNew setFreshLeader :: MonadServerAtomic m => FactionId -> ActorId -> m () setFreshLeader fid aid = do fact <- getsState $ (EM.! fid) . sfactionD when (fhasPointman (gkind fact)) $ do -- First update and send Perception so that the new leader -- may report his environment. b <- getsState $ getActorBody aid let !_A = assert (not $ bproj b) () valid <- getsServer $ (EM.! blid b) . (EM.! fid) . sperValidFid unless valid $ updatePer fid (blid b) execUpdAtomic $ UpdLeadFaction fid (gleader fact) (Just aid) updatePer :: MonadServerAtomic m => FactionId -> LevelId -> m () updatePer fid lid = do -- Performed in the State after action, e.g., with a new actor. perNew <- recomputeCachePer fid lid updatePerFromNew fid lid perNew updatePerFromNew :: MonadServerAtomic m => FactionId -> LevelId -> Perception -> m () updatePerFromNew fid lid perNew = do -- Even if nothing needed to be done, perception is now validated. modifyServer $ \ser -> ser {sperValidFid = EM.adjust (EM.insert lid True) fid $ sperValidFid ser} sperFidOld <- getsServer sperFid let perOld = sperFidOld EM.! fid EM.! lid inPer = diffPer perNew perOld outPer = diffPer perOld perNew unless (nullPer outPer && nullPer inPer) $ do -- Perception is modified on the server and sent to the client -- together with all the revealed info. let fper = EM.adjust (EM.insert lid perNew) fid modifyServer $ \ser -> ser {sperFid = fper $ sperFid ser} execSendPer fid lid outPer inPer perNew recomputeCachePer :: MonadServer m => FactionId -> LevelId -> m Perception recomputeCachePer fid lid = do total <- getCacheTotal fid lid fovLucid <- getCacheLucid lid getsState $ perceptionFromPTotal fid lid fovLucid total -- The missile item is removed from the store only if the projection -- went into effect (no failure occured). projectFail :: MonadServerAtomic m => ActorId -- ^ actor causing the projection -> ActorId -- ^ actor projecting the item (is on current level) -> Point -- ^ starting position of the projectile; -- usually, but not always, position of @origin@ -> Point -- ^ target position of the projectile -> Int -- ^ digital line parameter -> Bool -- ^ whether to start at the origin's position -> ItemId -- ^ the item to be projected -> CStore -- ^ which store the items comes from -> Bool -- ^ whether the item is a blast -> m (Maybe ReqFailure) projectFail propeller origin oxy tpxy eps center iid cstore blast = do COps{coTileSpeedup} <- getsState scops body <- getsState $ getActorBody origin let lid = blid body lvl <- getLevel lid case bresenhamsLineAlgorithm eps oxy tpxy of Nothing -> return $ Just ProjectAimOnself Just [] -> error $ "projecting from the edge of level" `showFailure` (oxy, tpxy) Just (pos : restUnlimited) -> do bag <- getsState $ getBodyStoreBag body cstore case EM.lookup iid bag of Nothing -> return $ Just ProjectOutOfReach Just _kit -> do itemFull <- getsState $ itemToFull iid actorSk <- currentSkillsServer origin actorMaxSk <- getsState $ getActorMaxSkills origin let skill = Ability.getSk Ability.SkProject actorSk forced = blast || bproj body calmE = calmEnough body actorMaxSk legal = permittedProject forced skill calmE itemFull arItem = aspectRecordFull itemFull case legal of Left reqFail -> return $ Just reqFail Right _ -> do let lobable = IA.checkFlag Ability.Lobable arItem rest = if lobable then take (chessDist oxy tpxy - 1) restUnlimited else restUnlimited t = lvl `at` pos if | not $ Tile.isWalkable coTileSpeedup t -> return $ Just ProjectBlockTerrain | occupiedBigLvl pos lvl -> if blast then do -- Hit the blocking actor by starting the explosion -- particle where the projectile landed, not a step away. -- The same when the spot has the explosive embed, -- regardless if it's walkable (@pos@ is, that's enough). -- No problem even if there's a big actor where -- the projectile starts, though it's wierd it may get -- away unharmed sometimes. projectBla propeller origin oxy (pos:rest) iid cstore blast return Nothing else return $ Just ProjectBlockActor | otherwise -> do -- Make the explosion less regular and weaker at the edges. if blast && center then -- Start in the center, not around, even if the center -- is a non-walkable tile with the exploding embed -- or if a big actor is there. projectBla propeller origin oxy (pos:rest) iid cstore blast else projectBla propeller origin pos rest iid cstore blast return Nothing projectBla :: MonadServerAtomic m => ActorId -- ^ actor causing the projection -> ActorId -- ^ actor projecting the item (is on current lvl) -> Point -- ^ starting point of the projectile -> [Point] -- ^ rest of the trajectory of the projectile -> ItemId -- ^ the item to be projected -> CStore -- ^ which store the items comes from -> Bool -- ^ whether the item is a blast -> m () projectBla propeller origin pos rest iid cstore blast = do body <- getsState $ getActorBody origin let lid = blid body localTime <- getsState $ getLocalTime lid unless blast $ execSfxAtomic $ SfxProject origin iid bag <- getsState $ getBodyStoreBag body cstore ItemFull{itemKind} <- getsState $ itemToFull iid case iid `EM.lookup` bag of Nothing -> error $ "" `showFailure` (origin, pos, rest, iid, cstore) Just kit@(_, it) -> do let delay = if IK.iweight itemKind == 0 then timeTurn -- big delay at start, e.g., to easily read hologram else timeZero -- avoid running into own projectiles btime = absoluteTimeAdd delay localTime addProjectile propeller pos rest iid kit lid (bfid body) btime let c = CActor origin cstore execUpdAtomic $ UpdLoseItem False iid (1, take 1 it) c addActorFromGroup :: MonadServerAtomic m => GroupName ItemKind -> FactionId -> Point -> LevelId -> Time -> m (Maybe ActorId) addActorFromGroup actorGroup fid pos lid time = do Level{ldepth} <- getLevel lid -- We bootstrap the actor by first creating the trunk of the actor's body -- that contains the fixed properties of all actors of that kind. freq <- prepareItemKind 0 ldepth [(actorGroup, 1)] m2 <- rollItemAspect freq ldepth case m2 of NoNewItem -> return Nothing NewItem _ itemKnown itemFull itemQuant -> do let itemFullKit = (itemFull, itemQuant) Just <$> registerActor False itemKnown itemFullKit fid pos lid time registerActor :: MonadServerAtomic m => Bool -> ItemKnown -> ItemFullKit -> FactionId -> Point -> LevelId -> Time -> m ActorId registerActor summoned (ItemKnown kindIx ar _) (itemFullRaw, kit) bfid pos lid time = do COps{cocave} <- getsState scops let container = CTrunk bfid lid pos jfid = Just bfid itemKnown = ItemKnown kindIx ar jfid itemFull = itemFullRaw {itemBase = (itemBase itemFullRaw) {jfid}} trunkId <- registerItem False (itemFull, kit) itemKnown container aid <- addNonProjectile summoned trunkId (itemFull, kit) bfid pos lid time fact <- getsState $ (EM.! bfid) . sfactionD actorMaxSk <- getsState $ getActorMaxSkills aid condAnyFoeAdj <- getsState $ anyFoeAdj aid Level{lkind} <- getLevel lid let cinitSleep = CK.cinitSleep $ okind cocave lkind when (cinitSleep /= CK.InitSleepBanned && canSleep actorMaxSk && not condAnyFoeAdj && not summoned && not (fhasGender (gkind fact))) $ do -- heroes never start asleep -- A lot of actors will wake up at once anyway, so let most start sleeping. let sleepOdds = if prefersSleep actorMaxSk then 19%20 else 2%3 sleeps <- rndToAction $ chance sleepOdds when (cinitSleep == CK.InitSleepAlways || sleeps) $ addSleep aid return aid addProjectile :: MonadServerAtomic m => ActorId -> Point -> [Point] -> ItemId -> ItemQuant -> LevelId -> FactionId -> Time -> m () addProjectile propeller pos rest iid (_, it) lid fid time = do itemFull <- getsState $ itemToFull iid let arItem = aspectRecordFull itemFull IK.ThrowMod{IK.throwHP} = IA.aToThrow arItem (trajectory, (speed, _)) = IA.itemTrajectory arItem (itemKind itemFull) (pos : rest) -- Trunk is added to equipment, not to organs, because it's the -- projected item, so it's carried, not grown. tweakBody b = b { bhp = xM throwHP , btrajectory = Just (trajectory, speed) , beqp = EM.singleton iid (1, take 1 it) } aid <- addActorIid iid itemFull True fid pos lid tweakBody bp <- getsState $ getActorBody propeller -- If propeller is a projectile, it may produce other projectiles, e.g., -- by exploding, so it's not voluntary, so others are to blame. -- However, we can't easily see whether a pushed non-projectile actor -- produced a projectile due to colliding or voluntarily, so we assign -- blame to him. originator <- if bproj bp then getsServer $ EM.findWithDefault propeller propeller . strajPushedBy else return propeller modifyServer $ \ser -> ser { strajTime = updateActorTime fid lid aid time $ strajTime ser , strajPushedBy = EM.insert aid originator $ strajPushedBy ser } addNonProjectile :: MonadServerAtomic m => Bool -> ItemId -> ItemFullKit -> FactionId -> Point -> LevelId -> Time -> m ActorId addNonProjectile summoned trunkId (itemFull, kit) fid pos lid time = do let tweakBody b = b { borgan = EM.singleton trunkId kit , bcalm = if summoned then xM 5 -- a tiny buffer before domination else bcalm b } aid <- addActorIid trunkId itemFull False fid pos lid tweakBody -- We assume actor is never born pushed. modifyServer $ \ser -> ser {sactorTime = updateActorTime fid lid aid time $ sactorTime ser} return aid addActorIid :: MonadServerAtomic m => ItemId -> ItemFull -> Bool -> FactionId -> Point -> LevelId -> (Actor -> Actor) -> m ActorId addActorIid trunkId ItemFull{itemBase, itemKind, itemDisco=ItemDiscoFull arItem} bproj fid pos lid tweakBody = do COps{coitem} <- getsState scops -- Initial HP and Calm is based only on trunk and ignores organs. let trunkMaxHP = max 2 $ IA.getSkill Ability.SkMaxHP arItem hp = xM trunkMaxHP `div` 2 -- Slightly reduced starting Calm to auto-id items that refill Calm -- and to let animals do some initial exploration before going to sleep. -- Higher reduction would cause confusingly low sight range at game -- start and even inability to handle equipment. calm = xM (max 1 $ IA.getSkill Ability.SkMaxCalm arItem - 10) -- Create actor. factionD <- getsState sfactionD curChalSer <- getsServer $ scurChalSer . soptions let fact = factionD EM.! fid teamContinuityOurs = fteam (gkind fact) bnumberTeam <- if bproj then return Nothing else do stcounter <- getsServer stcounter let number = EM.findWithDefault 0 teamContinuityOurs stcounter modifyServer $ \ser -> ser {stcounter = EM.insert teamContinuityOurs (succ number) stcounter} return $ Just (number, teamContinuityOurs) let bnumber = fst <$> bnumberTeam -- If difficulty is below standard, HP is added to the UI factions, -- otherwise HP is added to their enemies. -- If no UI factions, their role is taken by the escapees (for testing). let diffBonusCoeff = difficultyCoeff $ cdiff curChalSer -- For most projectiles (exceptions are, e.g., maxHP boosting rings), -- SkMaxHP is zero, which means they drop after one hit regardless -- of extra bhp they have due to piercing. That is fine. -- If we want armoured missiles, that should not be done via piercing, -- but via SkMaxHP of the thrown items. Rings that are piercing -- by coincidence are harmless, too. However, piercing should not be -- added to missiles via SkMaxHP or equipping them would be beneficial -- in a hard to balance way (e.g., one bullet adds 10 SkMaxHP). boostFact = not bproj && if diffBonusCoeff > 0 then any (fhasUI . gkind . snd) (filter (\(fi, fa) -> isFriend fi fa fid) (EM.assocs factionD)) else any (fhasUI . gkind . snd) (filter (\(fi, fa) -> isFoe fi fa fid) (EM.assocs factionD)) finalHP | boostFact = min (xM 899) -- no more than UI can stand (hp * 2 ^ abs diffBonusCoeff) | otherwise = hp -- Prevent too high max HP resulting in panic when low HP/max HP ratio. maxHP = min (finalHP + xM 100) (2 * finalHP) bonusHP = fromEnum (maxHP `div` oneM) - trunkMaxHP healthOrgans = [(Just bonusHP, (IK.S_BONUS_HP, COrgan)) | bonusHP /= 0] b = actorTemplate trunkId bnumber finalHP calm pos lid fid bproj withTrunk = b { bweapon = if IA.checkFlag Ability.Meleeable arItem then 1 else 0 , bweapBenign = if IA.checkFlag Ability.Meleeable arItem && IA.checkFlag Ability.Benign arItem then 1 else 0 } bodyTweaked = tweakBody withTrunk aid <- getsServer sacounter modifyServer $ \ser -> ser {sacounter = succ aid} execUpdAtomic $ UpdCreateActor aid bodyTweaked [(trunkId, itemBase)] unless bproj $ do steamGearCur <- getsServer steamGearCur let gearList = case bnumberTeam of Nothing -> [] Just (number, teamContinuity) -> case teamContinuity `EM.lookup` steamGearCur of Nothing -> [] Just im -> IM.findWithDefault [] number im -- Create, register and insert all initial actor items, including -- the bonus health organs from difficulty setting. forM_ (healthOrgans ++ map (Nothing,) (IK.ikit itemKind)) $ \(mk, (ikGrp, cstore)) -> do -- TODO: remove ASAP. This is a hack that prevents AI from stealing -- backstories until there is enough of them in Allure. -- Instead, pre-generate 20 player heroes to make sure all unique -- backstories are available to the player and so that the order -- of games played doesn't affect their availability. if ikGrp == DefsInternal.GroupName "backstory" && isJust bnumberTeam && (snd <$> bnumberTeam) /= Just teamExplorer then return () else do let container = CActor aid cstore Level{ldepth} <- getLevel lid mIidEtc <- case lookup ikGrp gearList of Nothing -> do let itemFreq = [(ikGrp, 1)] -- Power depth of new items unaffected by number of spawned actors. freq <- prepareItemKind 0 ldepth itemFreq mIidEtc <- rollAndRegisterItem False ldepth freq container mk case (bnumberTeam, mIidEtc) of (Just (number, teamContinuity), Just (_, (itemFull2, _))) -> do let arItem2 = aspectRecordFull itemFull2 inMetaGame = IA.checkFlag Ability.MetaGame arItem2 itemKindId2 = itemKindId itemFull2 when inMetaGame $ do let altInner ml = Just $ (ikGrp, itemKindId2) : fromMaybe [] ml alt mim = Just $ IM.alter altInner number $ fromMaybe IM.empty mim modifyServer $ \ser -> ser {steamGear = EM.alter alt teamContinuity $ steamGear ser} _ -> return () return mIidEtc Just itemKindId2 -> do let gearListNew = delete (ikGrp, itemKindId2) gearList (number, teamContinuity) = fromJust bnumberTeam alt mim = Just $ IM.insert number gearListNew $ fromMaybe IM.empty mim modifyServer $ \ser -> ser {steamGearCur = EM.alter alt teamContinuity steamGearCur} let itemKind2 = okind coitem itemKindId2 freq = pure (ikGrp, itemKindId2, itemKind2) rollAndRegisterItem False ldepth freq container mk case mIidEtc of Nothing -> error $ "" `showFailure` (lid, ikGrp, container, mk) Just (iid, (itemFull2, _)) -> when (cstore /= CGround) $ -- The items are created owned by actors, so won't be picked up, -- so we have to discover them now, if eligible. discoverIfMinorEffects container iid (itemKindId itemFull2) return aid addActorIid _ _ _ _ _ _ _ = error "addActorIid: server ignorant about an item" discoverIfMinorEffects :: MonadServerAtomic m => Container -> ItemId -> ContentId ItemKind -> m () discoverIfMinorEffects c iid itemKindId = do COps{coitem} <- getsState scops discoAspect <- getsState sdiscoAspect let arItem = discoAspect EM.! iid itemKind = okind coitem itemKindId -- Otherwise, discover by use when item's effects get activated later on. when (IA.onlyMinorEffects arItem itemKind && not (IA.isHumanTrinket itemKind)) $ execUpdAtomic $ UpdDiscover c iid itemKindId arItem pickWeaponServer :: MonadServer m => ActorId -> ActorId -> m (Maybe (ItemId, CStore)) pickWeaponServer source target = do eqpAssocs <- getsState $ kitAssocs source [CEqp] bodyAssocs <- getsState $ kitAssocs source [COrgan] actorSk <- currentSkillsServer source sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target let kitAssRaw = eqpAssocs ++ bodyAssocs forced = bproj sb kitAss | forced = kitAssRaw -- for projectiles, anything is weapon | otherwise = filter (IA.checkFlag Ability.Meleeable . aspectRecordFull . fst . snd) kitAssRaw benign itemFull = let arItem = aspectRecordFull itemFull in IA.checkFlag Ability.Benign arItem -- Server ignores item effects or it would leak item discovery info. -- Hence, weapons with powerful burning or wouding are undervalued. -- In particular, it even uses weapons that would heal an opponent. -- But server decides only in exceptiona cases, e.g. projectile collision -- or melee in place of an impossible displace. Otherwise, client decides. strongest <- pickWeaponM False Nothing kitAss actorSk source case strongest of [] -> return Nothing (_, _, _, _, _, (itemFull, _)) : _ | not forced && benign itemFull && bproj tb -> return Nothing -- if strongest is benign, don't waste fun on a projectile iis@((value1, hasEffect1, timeout1, _, _, _) : _) -> do let minIis = takeWhile (\(value, hasEffect, timeout, _, _, _) -> value == value1 && hasEffect == hasEffect1 && timeout == timeout1) iis (_, _, _, _, iid, _) <- rndToAction $ oneOf minIis let cstore = if isJust (lookup iid bodyAssocs) then COrgan else CEqp return $ Just (iid, cstore) -- @MonadStateRead@ would be enough, but the logic is sound only on server. currentSkillsServer :: MonadServer m => ActorId -> m Ability.Skills currentSkillsServer aid = do body <- getsState $ getActorBody aid mleader <- getsState $ gleader . (EM.! bfid body) . sfactionD getsState $ actorCurrentSkills mleader aid getCacheLucid :: MonadServer m => LevelId -> m FovLucid getCacheLucid lid = do fovClearLid <- getsServer sfovClearLid fovLitLid <- getsServer sfovLitLid fovLucidLid <- getsServer sfovLucidLid let getNewLucid = getsState $ \s -> lucidFromLevel fovClearLid fovLitLid s lid (sdungeon s EM.! lid) case EM.lookup lid fovLucidLid of Just (FovValid fovLucid) -> return fovLucid _ -> do newLucid <- getNewLucid modifyServer $ \ser -> ser {sfovLucidLid = EM.insert lid (FovValid newLucid) $ sfovLucidLid ser} return newLucid getCacheTotal :: MonadServer m => FactionId -> LevelId -> m CacheBeforeLucid getCacheTotal fid lid = do sperCacheFidOld <- getsServer sperCacheFid let perCacheOld = sperCacheFidOld EM.! fid EM.! lid case ptotal perCacheOld of FovValid total -> return total FovInvalid -> do actorMaxSkills <- getsState sactorMaxSkills fovClearLid <- getsServer sfovClearLid getActorB <- getsState $ flip getActorBody let perActorNew = perActorFromLevel (perActor perCacheOld) getActorB actorMaxSkills (fovClearLid EM.! lid) -- We don't check if any actor changed, because almost surely one is. -- Exception: when an actor is destroyed, but then union differs, too. total = totalFromPerActor perActorNew perCache = PerceptionCache { ptotal = FovValid total , perActor = perActorNew } fperCache = EM.adjust (EM.insert lid perCache) fid modifyServer $ \ser -> ser {sperCacheFid = fperCache $ sperCacheFid ser} return total allGroupItems :: MonadServerAtomic m => CStore -> GroupName ItemKind -> ActorId -> m [(ItemId, ItemQuant)] allGroupItems store grp target = do COps{coitem} <- getsState scops b <- getsState $ getActorBody target assocsCStore <- getsState $ EM.assocs . getBodyStoreBag b store getKindId <- getsState $ flip getIidKindIdServer let assocsKindId = map (\as@(iid, _) -> (getKindId iid, as)) assocsCStore hasGroup (itemKindId, _) = maybe False (> 0) $ lookup grp $ IK.ifreq $ okind coitem itemKindId return $! map snd $ sortBy (comparing fst) $ filter hasGroup assocsKindId addCondition :: MonadServerAtomic m => Bool -> GroupName ItemKind -> ActorId -> m () addCondition verbose name aid = do b <- getsState $ getActorBody aid Level{ldepth} <- getLevel $ blid b let c = CActor aid COrgan -- Power depth of new items unaffected by number of spawned actors. freq <- prepareItemKind 0 ldepth [(name, 1)] mresult <- rollAndRegisterItem verbose ldepth freq c Nothing assert (isJust mresult) $ return () removeConditionSingle :: MonadServerAtomic m => GroupName ItemKind -> ActorId -> m Int removeConditionSingle name aid = do let c = CActor aid COrgan is <- allGroupItems COrgan name aid case is of [(iid, (nAll, itemTimer))] -> do execUpdAtomic $ UpdLoseItem False iid (1, itemTimer) c return $ nAll - 1 _ -> error $ "missing or multiple item" `showFailure` (name, is) addSleep :: MonadServerAtomic m => ActorId -> m () addSleep aid = do b <- getsState $ getActorBody aid addCondition True IK.S_ASLEEP aid execUpdAtomic $ UpdWaitActor aid (bwatch b) WSleep removeSleepSingle :: MonadServerAtomic m => ActorId -> m () removeSleepSingle aid = do nAll <- removeConditionSingle IK.S_ASLEEP aid when (nAll == 0) $ execUpdAtomic $ UpdWaitActor aid WWake WWatch addKillToAnalytics :: MonadServerAtomic m => ActorId -> KillHow -> FactionId -> ItemId -> m () addKillToAnalytics aid killHow fid iid = do actorD <- getsState sactorD case EM.lookup aid actorD of Just b -> modifyServer $ \ser -> ser { sfactionAn = addFactionKill (bfid b) killHow fid iid $ sfactionAn ser , sactorAn = addActorKill aid killHow fid iid $ sactorAn ser } Nothing -> return () -- killer dead, too late to assign blame LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/DebugM.hs0000644000000000000000000000773107346545000021716 0ustar0000000000000000-- | Debug output for requests and responses. module Game.LambdaHack.Server.DebugM ( debugResponse , debugRequestAI, debugRequestUI #ifdef EXPOSE_INTERNAL -- * Internal operations , debugShow, debugPretty, debugPlain, DebugAid(..), debugAid #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Int (Int64) import qualified Data.Text as T import qualified Text.Show.Pretty as Show.Pretty import Game.LambdaHack.Atomic import Game.LambdaHack.Client (Response (..)) import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.State -- We debug these on the server, not on the clients, because we want -- a single log, knowing the order in which the server received requests -- and sent responseQs. Clients interleave and block non-deterministically -- so their logs would be harder to interpret. debugShow :: Show a => a -> Text debugShow = T.pack . Show.Pretty.ppShow debugResponse :: MonadServer m => FactionId -> Response -> m () debugResponse fid resp = case resp of RespUpdAtomic _ cmd@UpdPerception{} -> debugPlain fid "RespUpdAtomic" cmd RespUpdAtomic _ cmd@UpdResume{} -> debugPlain fid "RespUpdAtomic" cmd RespUpdAtomic _ cmd@UpdRestart{} -> debugPlain fid "RespUpdAtomic" cmd RespUpdAtomic _ cmd@UpdSpotTile{} -> debugPlain fid "RespUpdAtomic" cmd RespUpdAtomic _ cmd@(UpdCreateActor aid _ _) -> do d <- debugAid aid "UpdCreateActor" serverPrint d debugPretty fid "RespUpdAtomic" cmd RespUpdAtomic _ cmd@(UpdSpotActor aid _) -> do d <- debugAid aid "UpdSpotActor" serverPrint d debugPretty fid "RespUpdAtomic" cmd RespUpdAtomic _ cmd -> debugPretty fid "RespUpdAtomic" cmd RespUpdAtomicNoState cmd@UpdPerception{} -> debugPlain fid "RespUpdAtomicNoState" cmd RespUpdAtomicNoState cmd@UpdResume{} -> debugPlain fid "RespUpdAtomicNoState" cmd RespUpdAtomicNoState cmd@UpdSpotTile{} -> debugPlain fid "RespUpdAtomicNoState" cmd RespUpdAtomicNoState cmd -> debugPretty fid "RespUpdAtomicNoState" cmd RespQueryAI aid -> do d <- debugAid aid "RespQueryAI" serverPrint d RespSfxAtomic sfx -> do -- not so crucial so no details ps <- posSfxAtomic sfx serverPrint $ debugShow (fid, "RespSfxAtomic" :: Text, ps) RespQueryUIunderAI -> serverPrint "RespQueryUIunderAI" RespQueryUI -> serverPrint "RespQueryUI" debugPretty :: MonadServer m => FactionId -> Text -> UpdAtomic -> m () debugPretty fid t cmd = do ps <- posUpdAtomic cmd serverPrint $ debugShow (fid, t, ps, cmd) debugPlain :: MonadServer m => FactionId -> Text -> UpdAtomic -> m () debugPlain fid t cmd = do ps <- posUpdAtomic cmd serverPrint $ T.pack $ show (fid, t, ps, cmd) -- too large for pretty printing debugRequestAI :: MonadServer m => ActorId -> m () debugRequestAI aid = do d <- debugAid aid "AI request" serverPrint d debugRequestUI :: MonadServer m => ActorId -> m () debugRequestUI aid = do d <- debugAid aid "UI request" serverPrint d data DebugAid = DebugAid { label :: Text , aid :: ActorId , faction :: FactionId , lid :: LevelId , bHP :: Int64 , btime :: Maybe Time , btrTime :: Maybe Time , time :: Time } deriving Show debugAid :: MonadServer m => ActorId -> Text -> m Text debugAid aid label = do b <- getsState $ getActorBody aid time <- getsState $ getLocalTime (blid b) btime <- getsServer $ lookupActorTime (bfid b) (blid b) aid . sactorTime btrTime <- getsServer $ lookupActorTime (bfid b) (blid b) aid . strajTime return $! debugShow DebugAid { label , aid , faction = bfid b , lid = blid b , bHP = bhp b , btime , btrTime , time } LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/DungeonGen.hs0000644000000000000000000004704407346545000022605 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | The dungeon generation routine. It creates empty dungeons, without -- actors and without items, either lying on the floor or embedded inside tiles. module Game.LambdaHack.Server.DungeonGen ( FreshDungeon(..), dungeonGen #ifdef EXPOSE_INTERNAL -- * Internal operations , convertTileMaps, buildTileMap, anchorDown, buildLevel , snapToStairList, placeDownStairs, levelFromCave #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.Trans.State.Strict as St import Data.Either (rights) import qualified Data.EnumMap.Strict as EM import qualified Data.IntMap.Strict as IM import qualified Data.Text as T import qualified Data.Text.IO as T import System.IO (hFlush, stdout) import System.IO.Unsafe (unsafePerformIO) import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.CaveKind import Game.LambdaHack.Content.ModeKind import qualified Game.LambdaHack.Content.PlaceKind as PK import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Defs import qualified Game.LambdaHack.Definition.DefsInternal as DefsInternal import Game.LambdaHack.Server.DungeonGen.AreaRnd import Game.LambdaHack.Server.DungeonGen.Cave import Game.LambdaHack.Server.DungeonGen.Place import Game.LambdaHack.Server.ServerOptions convertTileMaps :: COps -> Bool -> Rnd (ContentId TileKind) -> Maybe (Rnd (ContentId TileKind)) -> Area -> TileMapEM -> Rnd TileMap convertTileMaps COps{ corule=RuleContent{rWidthMax, rHeightMax} , cotile , coTileSpeedup } areAllWalkable cdefTile mpickPassable darea ltile = do let outerId = ouniqGroup cotile TK.S_UNKNOWN_OUTER_FENCE runCdefTile :: (SM.SMGen, (Int, [(Int, ContentId TileKind)])) -> ( ContentId TileKind , (SM.SMGen, (Int, [(Int, ContentId TileKind)])) ) runCdefTile (gen1, (pI, assocs)) = let p = toEnum pI in if inside darea p then case assocs of (p2, t2) : rest | p2 == pI -> (t2, (gen1, (pI + 1, rest))) _ -> let (tile, gen2) = St.runState cdefTile gen1 in (tile, (gen2, (pI + 1, assocs))) else (outerId, (gen1, (pI + 1, assocs))) runUnfold gen = let (gen1, gen2) = SM.splitSMGen gen in (PointArray.unfoldrNA rWidthMax rHeightMax runCdefTile (gen1, (0, IM.assocs $ EM.enumMapToIntMap ltile)), gen2) converted1 <- St.state runUnfold case mpickPassable of _ | areAllWalkable -> return converted1 -- all walkable; passes OK Nothing -> return converted1 -- no walkable tiles for filling the map Just pickPassable -> do -- some tiles walkable, so ensure connectivity let passes p array = Tile.isWalkable coTileSpeedup (array PointArray.! p) -- If no point blocks on both ends, then I can eventually go -- from bottom to top of the map and from left to right -- unless there are disconnected areas inside rooms). blocksHorizontal (Point x y) array = not (passes (Point (x + 1) y) array || passes (Point (x - 1) y) array) blocksVertical (Point x y) array = not (passes (Point x (y + 1)) array || passes (Point x (y - 1)) array) activeArea = fromMaybe (error $ "" `showFailure` darea) $ shrink darea connect included blocks walkableTile array = let g p c = if inside activeArea p && included p && not (Tile.isEasyOpen coTileSpeedup c) && p `EM.notMember` ltile && blocks p array then walkableTile else c in PointArray.imapA g array walkable2 <- pickPassable let converted2 = connect (even . px) blocksHorizontal walkable2 converted1 walkable3 <- pickPassable let converted3 = connect (even . py) blocksVertical walkable3 converted2 walkable4 <- pickPassable let converted4 = connect (odd . px) blocksHorizontal walkable4 converted3 walkable5 <- pickPassable let converted5 = connect (odd . py) blocksVertical walkable5 converted4 return converted5 buildTileMap :: COps -> Cave -> Rnd TileMap buildTileMap cops@COps{cotile, cocave} Cave{dkind, darea, dmap} = do let CaveKind{cpassable, cdefTile} = okind cocave dkind pickDefTile = fromMaybe (error $ "" `showFailure` cdefTile) <$> opick cotile cdefTile (const True) wcond = Tile.isEasyOpenKind mpickPassable = if cpassable then Just $ fromMaybe (error $ "" `showFailure` cdefTile) <$> opick cotile cdefTile wcond else Nothing nwcond = not . Tile.kindHasFeature TK.Walkable areAllWalkable <- isNothing <$> opick cotile cdefTile nwcond convertTileMaps cops areAllWalkable pickDefTile mpickPassable darea dmap anchorDown :: Y anchorDown = 5 -- not 4, asymmetric vs up, for staircase variety; -- symmetry kept for @cfenceApart@ caves, to save real estate -- Create a level from a cave. buildLevel :: COps -> ServerOptions -> LevelId -> ContentId CaveKind -> CaveKind -> Int -> Int -> Dice.AbsDepth -> [(Point, Text)] -> Rnd (Level, [(Point, Text)]) buildLevel cops@COps{coplace, corule=RuleContent{..}} serverOptions lid dkind kc doubleDownStairs singleDownStairs totalDepth stairsFromUp = do let d = if cfenceApart kc then 1 else 0 -- Simple rule for now: level @lid@ has depth (difficulty) @abs lid@. ldepth = Dice.AbsDepth $ abs $ fromEnum lid darea = let (lxPrev, lyPrev) = unzip $ map ((px &&& py) . fst) stairsFromUp -- Stairs take some space, hence the additions. lxMin = max 0 $ -4 - d + minimum (rWidthMax - 1 : lxPrev) lxMax = min (rWidthMax - 1) $ 4 + d + maximum (0 : lxPrev) lyMin = max 0 $ -3 - d + minimum (rHeightMax - 1 : lyPrev) lyMax = min (rHeightMax - 1) $ 3 + d + maximum (0 : lyPrev) -- Pick minimal cave size that fits all previous stairs. xspan = max (lxMax - lxMin + 1) $ cXminSize kc yspan = max (lyMax - lyMin + 1) $ cYminSize kc x0 = min lxMin (rWidthMax - xspan) y0 = min lyMin (rHeightMax - yspan) in fromMaybe (error $ "" `showFailure` kc) $ toArea (x0, y0, x0 + xspan - 1, y0 + yspan - 1) (lstairsDouble, lstairsSingleUp) = splitAt doubleDownStairs stairsFromUp pstairsSingleUp = map fst lstairsSingleUp pstairsDouble = map fst lstairsDouble pallUpStairs = pstairsDouble ++ pstairsSingleUp boot = let (x0, y0, x1, y1) = fromArea darea in rights $ map (snapToStairList 0 pallUpStairs) [ Point (x0 + 4 + d) (y0 + 3 + d) , Point (x1 - 4 - d) (y1 - anchorDown + 1) ] fixedEscape <- case cescapeFreq kc of [] -> return [] escapeFreq -> do -- Escapes don't extend to other levels, so corners not harmful -- (actually neither are the other restrictions inherited from stairs -- placement, but we respect them to keep a uniform visual layout). -- Allowing corners and generating before stars, because they are more -- important that stairs (except the first stairs, but they are guaranteed -- unless the level has no incoming stairs, but if so, plenty of space). mepos <- placeDownStairs "escape" True serverOptions lid kc darea pallUpStairs boot case mepos of Just epos -> return [(epos, escapeFreq)] Nothing -> return [] -- with some luck, there is an escape elsewhere let pescape = map fst fixedEscape pallUpAndEscape = pescape ++ pallUpStairs addSingleDown :: [Point] -> Int -> Rnd [Point] addSingleDown acc 0 = return acc addSingleDown acc k = do mpos <- placeDownStairs "stairs" False serverOptions lid kc darea (pallUpAndEscape ++ acc) boot case mpos of Just pos -> addSingleDown (pos : acc) (k - 1) Nothing -> return acc -- calling again won't change anything pstairsSingleDown <- addSingleDown [] singleDownStairs let freqDouble carried = filter (\(gn, _) -> carried `elem` T.words (DefsInternal.fromGroupName gn)) $ cstairFreq kc ++ cstairAllowed kc fixedStairsDouble = map (second freqDouble) lstairsDouble freqUp carried = renameFreqs (<+> "up") $ freqDouble carried fixedStairsUp = map (second freqUp) lstairsSingleUp freqDown = renameFreqs (<+> "down") $ cstairFreq kc fixedStairsDown = map (, freqDown) pstairsSingleDown pallExits = pallUpAndEscape ++ pstairsSingleDown fixedCenters = EM.fromList $ fixedEscape ++ fixedStairsDouble ++ fixedStairsUp ++ fixedStairsDown -- Avoid completely uniform levels (e.g., uniformly merged places). bootExtra <- if EM.null fixedCenters then do mpointExtra <- placeDownStairs "extra boot" False serverOptions lid kc darea pallExits boot -- With sane content, @Nothing@ should never appear. return $! maybeToList mpointExtra else return [] let posUp Point{..} = Point (px - 1) py posDn Point{..} = Point (px + 1) py -- This and other places ensure there is always a continuous -- staircase from bottom to top. This makes moving between empty -- level much less boring. For new levels, it may be blocked by enemies -- or not offer enough cover, so other staircases may be preferable. lstair = ( map posUp $ pstairsDouble ++ pstairsSingleUp , map posDn $ pstairsDouble ++ pstairsSingleDown ) cellSize <- castDiceXY ldepth totalDepth $ ccellSize kc let subArea = fromMaybe (error $ "" `showFailure` kc) $ shrink darea area = if cfenceApart kc then subArea else darea (lgr, gs) = grid fixedCenters (boot ++ bootExtra) area cellSize dsecret <- randomWord32 cave <- buildCave cops ldepth totalDepth darea dsecret dkind lgr gs bootExtra cmap <- buildTileMap cops cave -- The bang is needed to prevent caves memory drag until levels used. let !lvl = levelFromCave cops cave ldepth cmap lstair pescape stairCarried p0 = let Place{qkind} = dstairs cave EM.! p0 freq = map (first $ T.words . tshow) (PK.pfreq $ okind coplace qkind) carriedAll = filter (\t -> any (\(ws, _) -> t `elem` ws) freq) rstairWordCarried in case carriedAll of [t] -> (p0, t) _ -> error $ "wrong carried stair word" `showFailure` (freq, carriedAll, kc) return (lvl, lstairsDouble ++ map stairCarried pstairsSingleDown) snapToStairList :: Int -> [Point] -> Point -> Either Point Point snapToStairList _ [] p = Right p snapToStairList a (pos : rest) p = let nx = if px pos > px p + 5 + a || px pos < px p - 5 - a then px p else px pos ny = if py pos > py p + 3 + a || py pos < py p - 3 - a then py p else py pos np = Point nx ny in if np == pos then Left np else snapToStairList a rest np -- Places yet another staircase (or escape), taking into account only -- the already existing stairs. placeDownStairs :: Text -> Bool -> ServerOptions -> LevelId -> CaveKind -> Area -> [Point] -> [Point] -> Rnd (Maybe Point) placeDownStairs object cornerPermitted serverOptions lid CaveKind{cminStairDist, cfenceApart} darea ps boot = do let dist cmin p = all (\pos -> chessDist p pos > cmin) ps (x0, y0, x1, y1) = fromArea darea -- Stairs in corners often enlarge next caves, so refrain from -- generating stairs, if only corner available (escapes special-cased). -- The bottom-right corner is exempt, becuase far from messages -- Also, avoid generating stairs at all on upper and left margins -- to keep subsequent small levels away from messages on top-right. rx = 9 -- enough to fit smallest stairs ry = 6 -- enough to fit smallest stairs wx = x1 - x0 + 1 wy = y1 - y0 + 1 notInCornerEtc Point{..} = cornerPermitted || wx < 3 * rx + 3 || wy < 3 * ry + 3 -- everything is a corner || px > x0 + (wx - 3) `div` 3 && py > y0 + (wy - 3) `div` 3 inCorner Point{..} = (px <= x0 + rx || px >= x1 - rx) && (py <= y0 + ry || py >= y1 - ry) gpreference = if cornerPermitted then inCorner else notInCornerEtc f p = case snapToStairList 0 ps p of Left{} -> Nothing Right np -> let nnp = either id id $ snapToStairList 0 boot np in if notInCornerEtc nnp then Just nnp else Nothing g p = case snapToStairList 2 ps p of Left{} -> Nothing Right np -> let nnp = either id id $ snapToStairList 2 boot np in if gpreference nnp && dist cminStairDist nnp then Just nnp else Nothing focusArea = let d = if cfenceApart then 1 else 0 in fromMaybe (error $ "" `showFailure` darea) $ toArea ( x0 + 4 + d, y0 + 3 + d , x1 - 4 - d, y1 - anchorDown + 1 ) mpos <- findPointInArea focusArea g 500 f -- The message fits this debugging level: let !_ = if isNothing mpos && sdumpInitRngs serverOptions then unsafePerformIO $ do T.hPutStrLn stdout $ "Failed to place" <+> object <+> "on level" <+> tshow lid <> ", in" <+> tshow darea hFlush stdout -- Not really expensive, but shouldn't disrupt normal testing nor play. #ifdef WITH_EXPENSIVE_ASSERTIONS error "possible, but unexpected; alarm!" #endif else () return mpos -- Build rudimentary level from a cave kind. levelFromCave :: COps -> Cave -> Dice.AbsDepth -> TileMap -> ([Point], [Point]) -> [Point] -> Level levelFromCave COps{coTileSpeedup} Cave{..} ldepth ltile lstair lescape = let f n t | Tile.isExplorable coTileSpeedup t = n + 1 | otherwise = n lexpl = PointArray.foldlA' f 0 ltile in Level { lkind = dkind , ldepth , lfloor = EM.empty , lembed = EM.empty , lbig = EM.empty , lproj = EM.empty , ltile , lentry = dentry , larea = darea , lsmell = EM.empty , lstair , lescape , lseen = 0 , lexpl , ltime = timeZero , lnight = dnight } -- | Freshly generated and not yet populated dungeon. data FreshDungeon = FreshDungeon { freshDungeon :: Dungeon -- ^ maps for all levels , freshTotalDepth :: Dice.AbsDepth -- ^ absolute dungeon depth } -- | Generate the dungeon for a new game. dungeonGen :: COps -> ServerOptions -> Caves -> Rnd FreshDungeon dungeonGen cops@COps{cocave} serverOptions caves = do let shuffleSegment :: ([Int], [GroupName CaveKind]) -> Rnd [(Int, GroupName CaveKind)] shuffleSegment (ns, l) = assert (length ns == length l) $ do lShuffled <- shuffle l return $! zip ns lShuffled cavesShuffled <- mapM shuffleSegment caves let cavesFlat = concat cavesShuffled absKeys = map (abs . fst) cavesFlat freshTotalDepth = Dice.AbsDepth $ maximum $ 10 : absKeys getCaveKindNum :: (Int, GroupName CaveKind) -> Rnd ((LevelId, ContentId CaveKind, CaveKind), Int) getCaveKindNum (ln, genName) = do dkind <- fromMaybe (error $ "" `showFailure` genName) <$> opick cocave genName (const True) let kc = okind cocave dkind ldepth = Dice.AbsDepth $ abs ln maxStairsNum <- castDice ldepth freshTotalDepth $ cmaxStairsNum kc return ((toEnum ln, dkind, kc), maxStairsNum) caveKindNums <- mapM getCaveKindNum cavesFlat let (caveKinds, caveNums) = unzip caveKindNums caveNumNexts = zip caveNums $ drop 1 caveNums ++ [0] placeStairs :: ([(Int, Int, Int)], Int) -> (Int, Int) -> ([(Int, Int, Int)], Int) placeStairs (acc, nstairsFromUp) (maxStairsNum, maxStairsNumNext) = let !_A1 = assert (nstairsFromUp <= maxStairsNum) () -- Any stairs coming from above are kept and if they exceed -- @maxStairsNumNext@, the remainder ends here. -- If they don't exceed the minimum of @maxStairsNum@ -- and @maxStairsNumNext@, the difference is filled up -- with single downstairs. The computation below maximizes -- the number of stairs at the cost of breaking some long -- staircases, except for the first one, which is always kept. -- Even without this exception, sometimes @maxStairsNum@ -- could not be reached. doubleKept = minimum [1, nstairsFromUp, maxStairsNum, maxStairsNumNext] nstairsFromUp1 = nstairsFromUp - doubleKept maxStairsNum1 = maxStairsNum - doubleKept maxStairsNumNext1 = maxStairsNumNext - doubleKept singleDownStairs = min maxStairsNumNext1 $ maxStairsNum1 - nstairsFromUp1 remainingNext = maxStairsNumNext1 - singleDownStairs doubleDownStairs = doubleKept + min nstairsFromUp1 remainingNext !_A2 = assert (singleDownStairs >= 0) () !_A3 = assert (doubleDownStairs >= doubleKept) () in ( (nstairsFromUp, doubleDownStairs, singleDownStairs) : acc , doubleDownStairs + singleDownStairs ) (caveStairs, nstairsFromUpLast) = foldl' placeStairs ([], 0) caveNumNexts caveZipped = assert (nstairsFromUpLast == 0) $ zip caveKinds (reverse caveStairs) placeCaveKind :: ([(LevelId, Level)], [(Point, Text)]) -> ( (LevelId, ContentId CaveKind, CaveKind) , (Int, Int, Int) ) -> Rnd ([(LevelId, Level)], [(Point, Text)]) placeCaveKind (lvls, stairsFromUp) ( (lid, dkind, kc) , (nstairsFromUp, doubleDownStairs, singleDownStairs) ) = do let !_A = assert (length stairsFromUp == nstairsFromUp) () (newLevel, ldown2) <- -- lstairUp for the next level is lstairDown for the current level buildLevel cops serverOptions lid dkind kc doubleDownStairs singleDownStairs freshTotalDepth stairsFromUp return ((lid, newLevel) : lvls, ldown2) (levels, stairsFromUpLast) <- foldlM' placeCaveKind ([], []) caveZipped let freshDungeon = assert (null stairsFromUpLast) $ EM.fromList levels return $! FreshDungeon{..} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/DungeonGen/0000755000000000000000000000000007346545000022240 5ustar0000000000000000LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/DungeonGen/AreaRnd.hs0000644000000000000000000003755407346545000024126 0ustar0000000000000000-- | Operations on the 'Area' type that involve random numbers. module Game.LambdaHack.Server.DungeonGen.AreaRnd ( -- * Picking points inside areas mkFixed, pointInArea, findPointInArea, mkVoidRoom, mkRoom -- * Choosing connections , connectGrid, randomConnection -- * Plotting corridors , HV(..), Corridor, connectPlaces , SpecialArea(..), grid #ifdef EXPOSE_INTERNAL -- * Internal operations , connectGrid', sortPoint, mkCorridor, borderPlace #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Functor.Identity (runIdentity) import qualified Data.IntSet as IS import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.PlaceKind import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Defs -- Doesn't respect minimum sizes, because staircases are specified verbatim, -- so can't be arbitrarily scaled up. -- The size may be one more than what maximal size hint requests, -- but this is safe (limited by area size) and makes up for the rigidity -- of the fixed room sizes (e.g., that the size is always odd). mkFixed :: (X, Y) -- ^ maximum size -> Area -- ^ the containing area, not the room itself -> Point -- ^ the center point -> Area mkFixed (xMax, yMax) area p@Point{..} = let (x0, y0, x1, y1) = fromArea area xradius = min ((xMax + 1) `div` 2) $ min (px - x0) (x1 - px) yradius = min ((yMax + 1) `div` 2) $ min (py - y0) (y1 - py) a = (px - xradius, py - yradius, px + xradius, py + yradius) in fromMaybe (error $ "" `showFailure` (a, xMax, yMax, area, p)) $ toArea a -- | Pick a random point within an area. pointInArea :: Area -> Rnd Point pointInArea area = do let (Point x0 y0, xspan, yspan) = spanArea area pxy <- randomR0 (xspan * yspan - 1) let Point{..} = punindex xspan pxy return $! Point (x0 + px) (y0 + py) -- | Find a suitable position in the area, based on random points -- and a preference predicate and fallback acceptability predicate. findPointInArea :: Area -> (Point -> Maybe Point) -> Int -> (Point -> Maybe Point) -> Rnd (Maybe Point) findPointInArea area g gnumTries f = let (Point x0 y0, xspan, yspan) = spanArea area checkPoint :: Applicative m => (Point -> Maybe Point) -> m (Maybe Point) -> Int -> m (Maybe Point) {-# INLINE checkPoint #-} checkPoint check fallback pxyRelative = let Point{..} = punindex xspan pxyRelative pos = Point (x0 + px) (y0 + py) in case check pos of Just p -> pure $ Just p Nothing -> fallback gsearch 0 = fsearch (xspan * yspan * 10) gsearch count = do pxy <- randomR0 (xspan * yspan - 1) checkPoint g (gsearch (count - 1)) pxy fsearch 0 = return $! runIdentity $ searchAll (xspan * yspan - 1) fsearch count = do pxy <- randomR0 (xspan * yspan - 1) checkPoint f (fsearch (count - 1)) pxy searchAll (-1) = pure Nothing searchAll pxyRelative = checkPoint f (searchAll (pxyRelative - 1)) pxyRelative in gsearch gnumTries -- | Create a void room, i.e., a single point area within the designated area. mkVoidRoom :: Area -> Rnd Area mkVoidRoom area = do -- Pass corridors closer to the middle of the grid area, if possible. let core = fromMaybe area $ shrink area pxy <- pointInArea core return $! trivialArea pxy -- | Create a random room according to given parameters. mkRoom :: (X, Y) -- ^ minimum size -> (X, Y) -- ^ maximum size -> Area -- ^ the containing area, not the room itself -> Rnd Area mkRoom (xm, ym) (xM, yM) area = do let (x0, y0, x1, y1) = fromArea area xspan = x1 - x0 + 1 yspan = y1 - y0 + 1 aW = (min xm xspan, min ym yspan, min xM xspan, min yM yspan) areaW = fromMaybe (error $ "" `showFailure` aW) $ toArea aW Point xW yW <- pointInArea areaW -- roll size let a1 = (x0, y0, max x0 (x1 - xW + 1), max y0 (y1 - yW + 1)) area1 = fromMaybe (error $ "" `showFailure` a1) $ toArea a1 Point rx1 ry1 <- pointInArea area1 -- roll top-left corner let a3 = (rx1, ry1, rx1 + xW - 1, ry1 + yW - 1) area3 = fromMaybe (error $ "" `showFailure` a3) $ toArea a3 return $! area3 -- Choosing connections between areas in a grid -- | Pick a subset of connections between adjacent areas within a grid until -- there is only one connected component in the graph of all areas. connectGrid :: ES.EnumSet Point -> (X, Y) -> Rnd [(Point, Point)] connectGrid voidPlaces (nx, ny) = do let unconnected = ES.fromDistinctAscList [ Point x y | y <- [0..ny-1], x <- [0..nx-1] ] -- Candidates are neighbours that are still unconnected. We start with -- a random choice. p <- oneOf $ ES.elems $ unconnected ES.\\ voidPlaces let candidates = ES.singleton p connectGrid' voidPlaces (nx, ny) unconnected candidates [] connectGrid' :: ES.EnumSet Point -> (X, Y) -> ES.EnumSet Point -> ES.EnumSet Point -> [(Point, Point)] -> Rnd [(Point, Point)] connectGrid' voidPlaces (nx, ny) unconnected candidates !acc | unconnected `ES.isSubsetOf` voidPlaces = return acc | otherwise = do let candidatesBest = candidates ES.\\ voidPlaces c <- oneOf $ ES.elems $ if ES.null candidatesBest then candidates else candidatesBest -- potential new candidates: let ns = ES.fromList $ vicinityCardinal nx ny c nu = ES.delete c unconnected -- new unconnected -- (new candidates, potential connections): (nc, ds) = ES.partition (`ES.member` nu) ns new <- if ES.null ds then return id else do d <- oneOf (ES.elems ds) return (sortPoint (c, d) :) connectGrid' voidPlaces (nx, ny) nu (ES.delete c (candidates `ES.union` nc)) (new acc) -- | Sort the sequence of two points, in the derived lexicographic order. sortPoint :: (Point, Point) -> (Point, Point) sortPoint (a, b) | a <= b = (a, b) | otherwise = (b, a) -- | Pick a single random connection between adjacent areas within a grid. randomConnection :: (X, Y) -> Rnd (Point, Point) randomConnection (nx, ny) = assert (nx > 1 && ny > 0 || nx > 0 && ny > 1 `blame` (nx, ny)) $ do rb <- oneOf [False, True] if rb && nx > 1 || ny <= 1 then do rx <- randomR0 (nx - 2) ry <- randomR0 (ny - 1) return (Point rx ry, Point (rx+1) ry) else do rx <- randomR0 (nx - 1) ry <- randomR0 (ny - 2) return (Point rx ry, Point rx (ry+1)) -- Plotting individual corridors between two areas -- | The choice of horizontal and vertical orientation. data HV = Horiz | Vert deriving Eq -- | The coordinates of consecutive fields of a corridor. type Corridor = (Point, Point, Point, Point) -- | Create a corridor, either horizontal or vertical, with -- a possible intermediate part that is in the opposite direction. -- There might not always exist a good intermediate point -- if the places are allowed to be close together -- and then we let the intermediate part degenerate. mkCorridor :: HV -- ^ orientation of the starting section -> Point -- ^ starting point -> Bool -- ^ starting is inside @FGround@ or @FFloor@ -> Point -- ^ ending point -> Bool -- ^ ending is inside @FGround@ or @FFloor@ -> Area -- ^ the area containing the intermediate point -> Rnd Corridor -- ^ straight sections of the corridor mkCorridor hv (Point x0 y0) p0floor (Point x1 y1) p1floor area = do Point rxRaw ryRaw <- pointInArea area let (sx0, sy0, sx1, sy1) = fromArea area -- Avoid corridors that run along @FGround@ or @FFloor@ fence, -- unless not possible. rx = if | rxRaw == sx0 + 1 && p0floor -> sx0 | rxRaw == sx1 - 1 && p1floor -> sx1 | otherwise -> rxRaw ry = if | ryRaw == sy0 + 1 && p0floor -> sy0 | ryRaw == sy1 - 1 && p1floor -> sy1 | otherwise -> ryRaw return $! case hv of Horiz -> (Point x0 y0, Point rx y0, Point rx y1, Point x1 y1) Vert -> (Point x0 y0, Point x0 ry, Point x1 ry, Point x1 y1) -- | Try to connect two interiors of places with a corridor. -- Choose entrances some steps away from the edges, if the place -- is big enough. Note that with @pfence == FNone@, the inner area considered -- is the strict interior of the place, without the outermost tiles. -- -- The corridor connects (touches) the inner areas and the turning point -- of the corridor (if any) is outside of the outer areas -- and inside the grid areas. connectPlaces :: (Area, Fence, Area) -> (Area, Fence, Area) -> Rnd (Maybe Corridor) connectPlaces (_, _, sg) (_, _, tg) | sg == tg = return Nothing connectPlaces s3@(sqarea, spfence, sg) t3@(tqarea, tpfence, tg) = do let (sa, so, stiny) = borderPlace sqarea spfence (ta, to, ttiny) = borderPlace tqarea tpfence trim area = let (x0, y0, x1, y1) = fromArea area dx = case (x1 - x0) `div` 2 of 0 -> 0 1 -> 1 2 -> 1 3 -> 1 _ -> 3 dy = case (y1 - y0) `div` 2 of 0 -> 0 1 -> 1 2 -> 1 3 -> 1 _ -> 3 in fromMaybe (error $ "" `showFailure` (area, s3, t3)) $ toArea (x0 + dx, y0 + dy, x1 - dx, y1 - dy) Point sx sy <- pointInArea $ trim sa Point tx ty <- pointInArea $ trim ta -- If the place (e.g., void place) is slim (at most 2-tile wide, no fence), -- overwrite it with corridor. The place may not even be built (e.g., void) -- and the overwrite ensures connections through it are not broken. let (_, _, sax1Raw, say1Raw) = fromArea sa -- inner area sslim = stiny && spfence == FNone (sax1, say1) = if sslim then (sax1Raw - 1, say1Raw - 1) else (sax1Raw, say1Raw) (tax0Raw, tay0Raw, _, _) = fromArea ta tslim = ttiny && tpfence == FNone (tax0, tay0) = if tslim then (tax0Raw + 1, tay0Raw + 1) else (tax0Raw, tay0Raw) (_, _, sox1, soy1) = fromArea so -- outer area (tox0, toy0, _, _) = fromArea to (sgx0, sgy0, sgx1, sgy1) = fromArea sg -- grid area (tgx0, tgy0, tgx1, tgy1) = fromArea tg (hv, area, p0, p1) | sgx1 == tgx0 = let x0 = if sgy0 <= ty && ty <= sgy1 then sox1 + 1 else sgx1 x1 = if tgy0 <= sy && sy <= tgy1 then tox0 - 1 else sgx1 in case toArea (x0, min sy ty, x1, max sy ty) of Just a -> (Horiz, a, Point (sax1 + 1) sy, Point (tax0 - 1) ty) Nothing -> error $ "" `showFailure` (sx, sy, tx, ty, s3, t3) | otherwise = assert (sgy1 == tgy0) $ let y0 = if sgx0 <= tx && tx <= sgx1 then soy1 + 1 else sgy1 y1 = if tgx0 <= sx && sx <= tgx1 then toy0 - 1 else sgy1 in case toArea (min sx tx, y0, max sx tx, y1) of Just a -> (Vert, a, Point sx (say1 + 1), Point tx (tay0 - 1)) Nothing -> error $ "" `showFailure` (sx, sy, tx, ty, s3, t3) nin p = not $ inside sa p || inside ta p !_A = assert (sslim || tslim || allB nin [p0, p1] `blame` (sx, sy, tx, ty, s3, t3)) () cor@(c1, c2, c3, c4) <- mkCorridor hv p0 (sa == so) p1 (ta == to) area let !_A2 = assert (sslim || tslim || allB nin [c1, c2, c3, c4] `blame` (cor, sx, sy, tx, ty, s3, t3)) () return $ Just cor borderPlace :: Area -> Fence -> (Area, Area, Bool) borderPlace qarea pfence = case pfence of FWall -> (qarea, expand qarea, False) FFloor -> (qarea, qarea, False) FGround -> (qarea, qarea, False) FNone -> case shrink qarea of Nothing -> (qarea, qarea, True) Just sr -> (sr, qarea, False) data SpecialArea = SpecialArea Area | SpecialFixed Point (Freqs PlaceKind) Area | SpecialMerged SpecialArea Point deriving Show -- | Divide uniformly a larger area into the given number of smaller areas -- overlapping at the edges. -- -- The list of fixed centers (some important points inside) -- of (non-overlapping) areas is given. Incorporate those, -- with as little disruption, as possible. -- Assume each of four boundaries of the cave are covered by a fixed centre. grid :: EM.EnumMap Point (Freqs PlaceKind) -> [Point] -> Area -> (X, Y) -> ((X, Y), EM.EnumMap Point SpecialArea) grid fixedCenters boot area cellSize = let (x0, y0, x1, y1) = fromArea area f zsize z1 n prev (c1 : c2 : rest) = let len = c2 - c1 cn = len * n `div` zsize in -- traceShow ( zsize, z1, n, prev, len, cn -- , len `div` max 1 (2 * cn) ) $ if cn < 2 then let mid1 = (c1 + c2) `div` 2 mid2 = (c1 + c2) `divUp` 2 mid = if mid1 - prev > 4 then mid1 else mid2 in (prev, mid, Just c1) : f zsize z1 n mid (c2 : rest) else (prev, c1 + len `div` (2 * cn), Just c1) : [ ( c1 + len * (2 * z - 1) `div` (2 * cn) , c1 + len * (2 * z + 1) `div` (2 * cn) , Nothing ) | z <- [1 .. cn - 1] ] ++ f zsize z1 n (c1 + len * (2 * cn - 1) `div` (2 * cn)) (c2 : rest) f _ z1 _ prev [c1] = [(prev, z1, Just c1)] f _ _ _ _ [] = error $ "empty list of centers" `showFailure` fixedCenters (xCenters, yCenters) = IS.fromList *** IS.fromList $ unzip $ map (px &&& py) $ EM.keys fixedCenters distFromIS is z = - minimum (maxBound : map (\i -> abs (i - z)) (IS.toList is)) xboot = nub $ sortOn (distFromIS xCenters) $ filter (`IS.notMember` xCenters) $ map px boot yboot = nub $ sortOn (distFromIS yCenters) $ filter (`IS.notMember` yCenters) $ map py boot -- Don't let boots ignore cell size too much, esp. in small caves. xcellsInArea = (x1 - x0 + 1) `div` fst cellSize ycellsInArea = (y1 - y0 + 1) `div` snd cellSize xbootN = assert (xcellsInArea > 0) $ xcellsInArea - IS.size xCenters ybootN = assert (ycellsInArea > 0) $ ycellsInArea - IS.size yCenters xset = xCenters `IS.union` IS.fromList (take xbootN xboot) yset = yCenters `IS.union` IS.fromList (take ybootN yboot) xsize = IS.findMax xset - IS.findMin xset ysize = IS.findMax yset - IS.findMin yset -- This is precisely how the cave will be divided among places, -- if there are no fixed centres except at boot coordinates. -- In any case, places, except for at boot points and fixed centres, -- are guaranteed at least the rolled minimal size of their -- enclosing cell (with one shared fence). Fixed centres are guaranteed -- a size between the cave cell size and the one implied by their -- placement wrt to cave fence and other fixed centers. lgrid = ( xsize `div` fst cellSize , ysize `div` snd cellSize ) xallSegments = zip [0..] $ f xsize x1 (fst lgrid) x0 $ IS.toList xset yallSegments = zip [0..] $ f ysize y1 (snd lgrid) y0 $ IS.toList yset in -- traceShow (xallSegments, yallSegments) $ ( (length xallSegments, length yallSegments) , EM.fromDistinctAscList [ ( Point x y , case (mcx, mcy) of (Just cx, Just cy) -> case EM.lookup (Point cx cy) fixedCenters of Nothing -> SpecialArea sarea Just placeFreq -> SpecialFixed (Point cx cy) placeFreq sarea _ -> SpecialArea sarea ) | (y, (cy0, cy1, mcy)) <- yallSegments , (x, (cx0, cx1, mcx)) <- xallSegments , let sarea = fromMaybe (error $ "" `showFailure` (x, y)) $ toArea (cx0, cy0, cx1, cy1) ] ) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/DungeonGen/Cave.hs0000644000000000000000000004534307346545000023463 0ustar0000000000000000-- | Generation of caves (not yet inhabited dungeon levels) from cave kinds. module Game.LambdaHack.Server.DungeonGen.Cave ( Cave(..), buildCave #ifdef EXPOSE_INTERNAL -- * Internal operations , pickOpening #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Key (mapWithKeyM) import Data.Word (Word32) import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.CaveKind import Game.LambdaHack.Content.PlaceKind import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.DungeonGen.AreaRnd import Game.LambdaHack.Server.DungeonGen.Place -- | The type of caves (not yet inhabited dungeon levels). data Cave = Cave { dkind :: ContentId CaveKind -- ^ the kind of the cave , darea :: Area -- ^ map area of the cave , dmap :: TileMapEM -- ^ tile kinds in the cave , dstairs :: EM.EnumMap Point Place -- ^ stair places indexed by their center , dentry :: EM.EnumMap Point PlaceEntry -- ^ room entrances in the cave , dnight :: Bool -- ^ whether the cave is dark } deriving Show {- | Generate a cave using an algorithm inspired by the original Rogue, as follows (in gross simplification): * The available area is divided into a grid, e.g, 3 by 3, where each of the 9 grid cells has approximately the same size. * In some of the 9 grid cells a room is placed at a random position and with a random size, but larger than the minimum size, e.g, 2 by 2 floor tiles. * Rooms that are on horizontally or vertically adjacent grid cells may be connected by a corridor. Corridors consist of 3 segments of straight lines (either "horizontal, vertical, horizontal" or "vertical, horizontal, vertical"). They end in openings in the walls of the room they connect. It is possible that one or two of the 3 segments have length 0, such that the resulting corridor is L-shaped or even a single straight line. * Corridors are generated randomly in such a way that at least every room on the grid is connected, and a few more might be. It is not sufficient to always connect all adjacent rooms, because not each cell holds a room. -} buildCave :: COps -- ^ content definitions -> Dice.AbsDepth -- ^ depth of the level to generate -> Dice.AbsDepth -- ^ absolute depth -> Area -- ^ map area of the cave -> Word32 -- ^ secret tile seed -> ContentId CaveKind -- ^ cave kind to use for generation -> (X, Y) -- ^ the dimensions of the grid of places -> EM.EnumMap Point SpecialArea -- ^ pos of stairs, etc. -> [Point] -- ^ boot positions to be treated as fixed -> Rnd Cave buildCave cops@COps{cocave, coplace, cotile, coTileSpeedup} ldepth totalDepth darea dsecret dkind lgr@(gx, gy) gs bootExtra = do let kc@CaveKind{..} = okind cocave dkind darkCorTile <- fromMaybe (error $ "" `showFailure` cdarkCorTile) <$> opick cotile cdarkCorTile (const True) litCorTile <- fromMaybe (error $ "" `showFailure` clitCorTile) <$> opick cotile clitCorTile (const True) dnight <- oddsDice ldepth totalDepth cnightOdds let createPlaces = do minPlaceSize <- castDiceXY ldepth totalDepth cminPlaceSize maxPlaceSize <- castDiceXY ldepth totalDepth cmaxPlaceSize let mergeFixed :: EM.EnumMap Point SpecialArea -> (Point, SpecialArea) -> EM.EnumMap Point SpecialArea mergeFixed !gs0 (!i, !special) = let mergeSpecial ar p2 f = case EM.lookup p2 gs0 of Just (SpecialArea ar2) -> let aSum = sumAreas ar ar2 sp = SpecialMerged (f aSum) p2 in EM.insert i sp $ EM.delete p2 gs0 _ -> gs0 mergable :: X -> Y -> Maybe HV mergable x y = case EM.lookup (Point x y) gs0 of Just (SpecialArea ar) -> let (_, xspan, yspan) = spanArea ar isFixed p = p `elem` bootExtra || case gs EM.! p of SpecialFixed{} -> True _ -> False in if -- Limit (the aggresive) merging of normal places -- and leave extra place for merging stairs. | any isFixed $ vicinityCardinal gx gy (Point x y) -> Nothing -- Bias: prefer extending vertically. -- Not @-2@, but @-4@, to merge aggressively. | yspan - 4 < snd minPlaceSize -> Just Vert | xspan - 4 < fst minPlaceSize -> Just Horiz | otherwise -> Nothing _ -> Nothing in case special of SpecialArea ar -> case mergable (px i) (py i) of Nothing -> gs0 Just hv -> case hv of -- Bias; vertical minimal sizes are smaller. -- -- The commented out cases never happen, because @mergable@ -- is symmetric and we proceed top-left to bottom-right. -- -- Vert | py i - 1 >= 0 -- && mergable (px i) (py i - 1) == Just Vert -> -- mergeSpecial ar i{py = py i - 1} SpecialArea Vert | py i + 1 < gy && mergable (px i) (py i + 1) == Just Vert -> mergeSpecial ar i{py = py i + 1} SpecialArea -- Horiz | px i - 1 >= 0 -- && mergable (px i - 1) (py i) == Just Horiz -> -- mergeSpecial ar i{px = px i - 1} SpecialArea Horiz | px i + 1 < gx && mergable (px i + 1) (py i) == Just Horiz -> mergeSpecial ar i{px = px i + 1} SpecialArea _ -> gs0 SpecialFixed p placeGroup ar -> -- If single merge is sufficient to extend the fixed place -- to full size, and the merge is possible, we perform it. -- An empty inner list signifies some merge is needed, -- but not possible, and then we abort and don't waste space. let (x0, y0, x1, y1) = fromArea ar dy = 3 -- arbitrary, matches common content dx = 5 -- arbitrary, matches common content vics :: [[Point]] vics = [ [i {py = py i - 1} | py i - 1 >= 0] -- possible | py p - y0 < dy ] -- needed ++ [ [i {py = py i + 1} | py i + 1 < gy] | y1 - py p < dy ] ++ [ [i {px = px i - 1} | px i - 1 >= 0] | px p - x0 < dx ] ++ [ [i {px = px i + 1} | px i + 1 < gx] | x1 - px p < dx ] in case vics of [[p2]] -> mergeSpecial ar p2 (SpecialFixed p placeGroup) _ -> gs0 SpecialMerged{} -> error $ "" `showFailure` (gs, gs0, i) gs2 = foldl' mergeFixed gs $ EM.assocs gs voidPlaces <- do let gridArea = fromMaybe (error $ "" `showFailure` lgr) $ toArea (0, 0, gx - 1, gy - 1) voidNum = round $ cmaxVoid * (fromIntegralWrap :: Int -> Rational) (EM.size gs2) isOrdinaryArea p = case p `EM.lookup` gs2 of Just SpecialArea{} -> True _ -> False reps <- replicateM voidNum (pointInArea gridArea) -- repetitions are OK; variance is low anyway return $! ES.fromList $ filter isOrdinaryArea reps let decidePlace :: Bool -> ( TileMapEM , EM.EnumMap Point (Place, Area) , EM.EnumMap Point Place ) -> (Point, SpecialArea) -> Rnd ( TileMapEM , EM.EnumMap Point (Place, Area) , EM.EnumMap Point Place ) decidePlace noVoid (!m, !qls, !qstairs) (!i, !special) = case special of SpecialArea ar -> do -- Reserved for corridors and the global fence. let innerArea = fromMaybe (error $ "" `showFailure` (i, ar)) $ shrink ar !_A0 = shrink innerArea !_A1 = assert (isJust _A0 `blame` (innerArea, gs, kc)) () if not noVoid && i `ES.member` voidPlaces then do qarea <- mkVoidRoom innerArea let qkind = deadEndId qmap = EM.empty qfence = EM.empty return (m, EM.insert i (Place{..}, ar) qls, qstairs) else do r <- mkRoom minPlaceSize maxPlaceSize innerArea place <- buildPlace cops kc dnight darkCorTile litCorTile ldepth totalDepth dsecret r (Just innerArea) [] return ( EM.unions [qmap place, qfence place, m] , EM.insert i (place, ar) qls , qstairs ) SpecialFixed p placeFreq ar -> do -- Reserved for corridors and the global fence. let innerArea = fromMaybe (error $ "" `showFailure` (i, ar)) $ shrink ar !_A0 = shrink innerArea !_A1 = assert (isJust _A0 `blame` (innerArea, gs2, kc)) () !_A2 = assert (inside (fromJust _A0) p `blame` (p, innerArea, gs)) () r = mkFixed maxPlaceSize innerArea p !_A3 = assert (isJust (shrink r) `blame` ( r, ar, p, innerArea, gs , gs2, qls, kc )) () place <- buildPlace cops kc dnight darkCorTile litCorTile ldepth totalDepth dsecret r Nothing placeFreq return ( EM.unions [qmap place, qfence place, m] , EM.insert i (place, ar) qls , EM.insert p place qstairs ) SpecialMerged sp p2 -> do (lplaces, dplaces, dstairs) <- decidePlace True (m, qls, qstairs) (i, sp) return ( lplaces , EM.insert p2 (dplaces EM.! i) dplaces , dstairs ) places <- foldlM' (decidePlace False) (EM.empty, EM.empty, EM.empty) $ EM.assocs gs2 return (voidPlaces, lgr, places) (voidPlaces, lgrid, (lplaces, dplaces, dstairs)) <- createPlaces let lcorridorsFun :: Rnd ( EM.EnumMap Point ( ContentId TileKind , ContentId PlaceKind ) , TileMapEM ) lcorridorsFun = do connects <- connectGrid voidPlaces lgrid addedConnects <- do let cauxNum = round $ cauxConnects * (fromIntegralWrap :: Int -> Rational) (uncurry (*) lgrid) cns <- map head . group . sort <$> replicateM cauxNum (randomConnection lgrid) -- This allows connections through a single void room, -- if a non-void room on both ends. let notDeadEnd (p, q) = if | p `ES.member` voidPlaces -> q `ES.notMember` voidPlaces && sndInCns p | q `ES.member` voidPlaces -> fstInCns q | otherwise -> True sndInCns p = any (\(p0, q0) -> q0 == p && p0 `ES.notMember` voidPlaces) cns fstInCns q = any (\(p0, q0) -> p0 == q && q0 `ES.notMember` voidPlaces) cns return $! filter notDeadEnd cns let allConnects = connects `union` addedConnects connectPos :: (Point, Point) -> Rnd (Maybe ( ContentId PlaceKind , Corridor , ContentId PlaceKind )) connectPos (p0, p1) = do let (place0, area0) = dplaces EM.! p0 (place1, area1) = dplaces EM.! p1 savePlaces cor = (qkind place0, cor, qkind place1) connected <- connectPlaces (qarea place0, pfence $ okind coplace (qkind place0), area0) (qarea place1, pfence $ okind coplace (qkind place1), area1) return $! savePlaces <$> connected cs <- catMaybes <$> mapM connectPos allConnects let pickedCorTile = if dnight then darkCorTile else litCorTile digCorridorSection :: a -> Point -> Point -> EM.EnumMap Point a digCorridorSection a p1 p2 = EM.fromList $ zip (fromTo p1 p2) (repeat a) digCorridor (sqkind, (p1, p2, p3, p4), tqkind) = ( EM.union (digCorridorSection (pickedCorTile, sqkind) p1 p2) (digCorridorSection (pickedCorTile, tqkind) p3 p4) , digCorridorSection pickedCorTile p2 p3 ) (lplOuter, lInner) = unzip $ map digCorridor cs return (EM.unions lplOuter, EM.unions lInner) (lplcorOuter, lcorInner) <- lcorridorsFun -- The hacks below are instead of unionWithKeyM, which is costly. let mergeCor _ pl (cor, pk) = if Tile.isWalkable coTileSpeedup pl then Nothing -- tile already open else Just (pl, cor, pk) {-# INLINE intersectionWithKeyMaybe #-} intersectionWithKeyMaybe combine = EM.mergeWithKey combine (const EM.empty) (const EM.empty) interCor = intersectionWithKeyMaybe mergeCor lplaces lplcorOuter -- fast doorMap <- foldlM' (pickOpening cops kc lplaces litCorTile dsecret) EM.empty (EM.assocs interCor) -- very small let subArea = fromMaybe (error $ "" `showFailure` kc) $ shrink darea fence <- buildFenceRnd cops cfenceTileN cfenceTileE cfenceTileS cfenceTileW subArea -- The obscured tile, e.g., scratched wall, stays on the server forever, -- only the suspect variant on client gets replaced by this upon searching. let sub2Area = fromMaybe (error $ "" `showFailure` kc) $ shrink subArea sub3Area = fromMaybe (error $ "" `showFailure` kc) $ shrink sub2Area likelySecret = inside sub3Area obscure p t = if isChancePos 1 chidden dsecret p && likelySecret p then Tile.obscureAs cotile t else return t lplacesObscured <- mapWithKeyM obscure lplaces let lcorOuter = EM.map fst lplcorOuter aroundFence Place{..} = if pfence (okind coplace qkind) `elem` [FFloor, FGround] then EM.map (const $ PAround qkind) qfence else EM.empty pickRepresentant Place{..} = let (representant, _, _) = spanArea qarea in EM.singleton representant $ PExists qkind dentry = EM.unions $ [EM.map (\(_, _, pk) -> PEntry pk) interCor] ++ map (\(place, _) -> aroundFence place) (EM.elems dplaces) ++ -- for @FNone@ fences with walkable tiles on the edges -- that may have no intersection with corridots, -- particularly if @X@ is used map (\(place, _) -> pickRepresentant place) (EM.elems dplaces) dmap = EM.unions [doorMap, lplacesObscured, lcorOuter, lcorInner, fence] -- order matters return $! Cave {..} pickOpening :: COps -> CaveKind -> TileMapEM -> ContentId TileKind -> Word32 -> EM.EnumMap Point (ContentId TileKind) -> ( Point , (ContentId TileKind, ContentId TileKind, ContentId PlaceKind) ) -> Rnd (EM.EnumMap Point (ContentId TileKind)) pickOpening COps{cotile, coTileSpeedup} CaveKind{cdoorChance, copenChance, chidden} lplaces litCorTile dsecret !acc (pos, (pl, cor, _)) = do let nicerCorridor = if Tile.isLit coTileSpeedup cor then cor else -- If any cardinally adjacent walkable room tile is lit, -- make the opening lit, as well. let roomTileLit p = case EM.lookup p lplaces of Nothing -> False Just tile -> Tile.isWalkable coTileSpeedup tile && Tile.isLit coTileSpeedup tile vic = vicinityCardinalUnsafe pos in if any roomTileLit vic then litCorTile else cor vicAll = vicinityUnsafe pos vicNewTiles = mapMaybe (`EM.lookup` acc) vicAll newTile <- case vicNewTiles of vicNewTile : _ -> return vicNewTile -- disallow a door beside an opening [] -> do -- Openings have a certain chance to be doors and doors have a certain -- chance to be open. rd <- chance cdoorChance if rd then do let hidden = Tile.buildAs cotile pl doorTrappedId <- Tile.revealAs cotile hidden let !_A = assert (Tile.buildAs cotile doorTrappedId == doorTrappedId) () -- Not all solid tiles can hide a door (or any other openable tile), -- so @doorTrappedId@ may in fact not be a door at all, hence the check. if Tile.isOpenable coTileSpeedup doorTrappedId then do -- door created ro <- chance copenChance if ro then Tile.openTo cotile doorTrappedId else if isChancePos 1 chidden dsecret pos then return doorTrappedId -- server will hide it else do doorOpenId <- Tile.openTo cotile doorTrappedId Tile.closeTo cotile doorOpenId -- mail do nothing; OK else return doorTrappedId -- assume this is what content enforces else return nicerCorridor return $! EM.insert pos newTile acc LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/DungeonGen/Place.hs0000644000000000000000000003531507346545000023627 0ustar0000000000000000{-# LANGUAGE RankNTypes #-} -- | Generation of places from place kinds. module Game.LambdaHack.Server.DungeonGen.Place ( Place(..), TileMapEM, buildPlace, isChancePos, buildFenceRnd #ifdef EXPOSE_INTERNAL -- * Internal operations , placeCheck, interiorArea, pover, buildFence, buildFenceMap , tilePlace #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Bits as Bits import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import Data.Word (Word32) import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Content.CaveKind import Game.LambdaHack.Content.PlaceKind import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.DungeonGen.AreaRnd -- | The map of tile kinds in a place (and generally anywhere in a cave). -- The map is sparse. The default tile that eventually fills the empty spaces -- is specified in the cave kind specification with @cdefTile@. type TileMapEM = EM.EnumMap Point (ContentId TileKind) -- | The parameters of a place. All are immutable and rolled and fixed -- at the time when a place is generated. data Place = Place { qkind :: ContentId PlaceKind , qarea :: Area , qmap :: TileMapEM , qfence :: TileMapEM } deriving Show -- | For @CAlternate@ tiling, require the place be comprised -- of an even number of whole corners, with exactly one square -- overlap between consecutive coners and no trimming. -- For other tiling methods, check that the area is large enough for tiling -- the corner twice in each direction, with a possible one row/column overlap. placeCheck :: Area -- ^ the area to fill -> PlaceKind -- ^ the kind of place to construct -> Bool placeCheck r pk@PlaceKind{..} = case interiorArea pk r of Nothing -> False Just area -> let (_, xspan, yspan) = spanArea area dxcorner = case ptopLeft of [] -> 0 ; l : _ -> T.length l dycorner = length ptopLeft wholeOverlapped d dcorner = d > 1 && dcorner > 1 && (d - 1) `mod` (2 * (dcorner - 1)) == 0 largeEnough = xspan >= 2 * dxcorner - 1 && yspan >= 2 * dycorner - 1 in case pcover of CAlternate -> wholeOverlapped xspan dxcorner && wholeOverlapped yspan dycorner CStretch -> largeEnough CReflect -> largeEnough CVerbatim -> True CMirror -> True -- | Calculate interior room area according to fence type, based on the -- total area for the room and it's fence. This is used for checking -- if the room fits in the area, for digging up the place and the fence -- and for deciding if the room is dark or lit later in the dungeon -- generation process. interiorArea :: PlaceKind -> Area -> Maybe Area interiorArea kr r = let requiredForFence = case pfence kr of FWall -> 1 FFloor -> 1 FGround -> 1 FNone -> 0 in if pcover kr `elem` [CVerbatim, CMirror] then let (Point x0 y0, xspan, yspan) = spanArea r dx = case ptopLeft kr of [] -> error $ "" `showFailure` kr l : _ -> T.length l dy = length $ ptopLeft kr mx = (xspan - dx) `div` 2 my = (yspan - dy) `div` 2 in if mx < requiredForFence || my < requiredForFence then Nothing else toArea (x0 + mx, y0 + my, x0 + mx + dx - 1, y0 + my + dy - 1) else case requiredForFence of 0 -> Just r 1 -> shrink r _ -> error $ "" `showFailure` kr -- | Given a few parameters, roll and construct a 'Place' datastructure -- and fill a cave section acccording to it. buildPlace :: COps -- ^ the game content -> CaveKind -- ^ current cave kind -> Bool -- ^ whether the cave is dark -> ContentId TileKind -- ^ dark fence tile, if fence hollow -> ContentId TileKind -- ^ lit fence tile, if fence hollow -> Dice.AbsDepth -- ^ current level depth -> Dice.AbsDepth -- ^ absolute depth -> Word32 -- ^ secret tile seed -> Area -- ^ whole area of the place, fence included -> Maybe Area -- ^ whole inner area of the grid cell -> Freqs PlaceKind -- ^ optional fixed place freq -> Rnd Place buildPlace cops@COps{coplace, coTileSpeedup} kc@CaveKind{..} dnight darkCorTile litCorTile levelDepth@(Dice.AbsDepth ldepth) totalDepth@(Dice.AbsDepth tdepth) dsecret r minnerArea mplaceGroup = do let f !q !acc !p !pk !kind = let rarity = linearInterpolation ldepth tdepth (prarity kind) !fr = q * p * rarity in (fr, (pk, kind)) : acc g (placeGroup, q) = ofoldlGroup' coplace placeGroup (f q) [] pfreq = case mplaceGroup of [] -> cplaceFreq _ -> mplaceGroup placeFreq = concatMap g pfreq checkedFreq = filter (\(_, (_, kind)) -> placeCheck r kind) placeFreq freq = toFreq "buildPlace" checkedFreq let !_A = assert (not (nullFreq freq) `blame` (placeFreq, checkedFreq, r)) () (qkind, kr) <- frequency freq let smallPattern = pcover kr `elem` [CVerbatim, CMirror] && (length (ptopLeft kr) < 10 || T.length (head (ptopLeft kr)) < 10) -- Below we apply a heuristics to estimate if there are floor tiles -- in the place that are adjacent to floor tiles of the cave and so both -- should have the same lit condition. -- A false positive is walled staircases in LambdaHack, but it's OK. dark <- if cpassable && not (dnight && Tile.isLit coTileSpeedup darkCorTile) -- the colonnade can be illuminated just as the trail is && (pfence kr `elem` [FFloor, FGround] || pfence kr == FNone && smallPattern) then return dnight else oddsDice levelDepth totalDepth cdarkOdds rBetter <- case minnerArea of Just innerArea | pcover kr `elem` [CVerbatim, CMirror] -> do -- A hack: if a verbatim place was rolled, redo computing the area -- taking into account that often much smaller portion is taken by place. let requiredForFence = case pfence kr of FWall -> 1 FFloor -> 1 FGround -> 1 FNone -> 0 sizeBetter = ( 2 * requiredForFence + T.length (head (ptopLeft kr)) , 2 * requiredForFence + length (ptopLeft kr) ) mkRoom sizeBetter sizeBetter innerArea _ -> return r let qarea = fromMaybe (error $ "" `showFailure` (kr, r)) $ interiorArea kr rBetter plegend = if dark then plegendDark kr else plegendLit kr mOneIn <- pover cops plegend cmap <- tilePlace qarea kr let lookupOneIn :: Point -> Char -> ContentId TileKind lookupOneIn xy c = let tktk = EM.findWithDefault (error $ "" `showFailure` (c, mOneIn)) c mOneIn in case tktk of (Just (k, n, tkSpice), _) | isChancePos k n dsecret xy -> tkSpice (_, tk) -> tk qmap = EM.mapWithKey lookupOneIn cmap qfence <- buildFence cops kc dnight darkCorTile litCorTile dark (pfence kr) qarea return $! Place {..} isChancePos :: Int -> Int -> Word32 -> Point -> Bool isChancePos k' n' dsecret (Point x' y') = k' > 0 && n' > 0 && let k = toEnum k' n = toEnum n' x = toEnum x' y = toEnum y' z = dsecret `Bits.rotateR` x' `Bits.xor` y + x in if k < n then z `mod` ((n + k) `divUp` k) == 0 else z `mod` ((n + k) `divUp` n) /= 0 -- This can't be optimized by memoization (storing these results per place), -- because it would fix random assignment of tiles to groups -- for all instances of a place throughout dungeon. Right now the assignment -- is fixed for any single place instance and it's consistent and interesting. -- Even fixing this per level would make levels less interesting. -- -- This could be precomputed for groups that contain only one tile, -- but for these, no random rolls are performed, so little would be saved. pover :: COps -> EM.EnumMap Char (GroupName TileKind) -> Rnd ( EM.EnumMap Char ( Maybe (Int, Int, ContentId TileKind) , ContentId TileKind ) ) pover COps{cotile} plegend = let assignKN :: GroupName TileKind -> ContentId TileKind -> ContentId TileKind -> (Int, Int, ContentId TileKind) assignKN cgroup tk tkSpice = -- Very likely that legends have spice. let n = fromMaybe (error $ show cgroup) (lookup cgroup (TK.tfreq (okind cotile tk))) k = fromMaybe (error $ show cgroup) (lookup cgroup (TK.tfreq (okind cotile tkSpice))) in (k, n, tkSpice) getLegend :: GroupName TileKind -> Rnd ( Maybe (Int, Int, ContentId TileKind) , ContentId TileKind ) getLegend cgroup = do mtkSpice <- opick cotile cgroup (Tile.kindHasFeature TK.Spice) tk <- fromMaybe (error $ "" `showFailure` (cgroup, plegend)) <$> opick cotile cgroup (not . Tile.kindHasFeature TK.Spice) return (assignKN cgroup tk <$> mtkSpice, tk) in mapM getLegend plegend -- | Construct a fence around a place. buildFence :: COps -> CaveKind -> Bool -> ContentId TileKind -> ContentId TileKind -> Bool -> Fence -> Area -> Rnd TileMapEM buildFence COps{cotile} CaveKind{ccornerTile, cwallTile} dnight darkCorTile litCorTile dark fence qarea = do qFWall <- fromMaybe (error $ "" `showFailure` cwallTile) <$> opick cotile cwallTile (const True) qFCorner <- fromMaybe (error $ "" `showFailure` ccornerTile) <$> opick cotile ccornerTile (const True) let qFFloor = if dark then darkCorTile else litCorTile qFGround = if dnight then darkCorTile else litCorTile return $! case fence of FWall -> buildFenceMap qFWall qFCorner qarea FFloor -> buildFenceMap qFFloor qFFloor qarea FGround -> buildFenceMap qFGround qFGround qarea FNone -> EM.empty -- | Construct a fence around an area, with the given tile kind. -- Corners have a different kind, e.g., to avoid putting doors there. buildFenceMap :: ContentId TileKind -> ContentId TileKind -> Area -> TileMapEM buildFenceMap wallId cornerId area = let (x0, y0, x1, y1) = fromArea area in EM.fromList $ [ (Point x y, wallId) | x <- [x0-1, x1+1], y <- [y0..y1] ] ++ [ (Point x y, wallId) | x <- [x0..x1], y <- [y0-1, y1+1] ] ++ [ (Point x y, cornerId) | x <- [x0-1, x1+1], y <- [y0-1, y1+1] ] -- | Construct a fence around an area, with the given tile group. buildFenceRnd :: COps -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> GroupName TileKind -> Area -> Rnd TileMapEM buildFenceRnd COps{cotile} cfenceTileN cfenceTileE cfenceTileS cfenceTileW area = do let (x0, y0, x1, y1) = fromArea area allTheSame = all (== cfenceTileN) [cfenceTileE, cfenceTileS, cfenceTileW] fenceIdRnd couterFenceTile (xf, yf) = do let isCorner x y = x `elem` [x0-1, x1+1] && y `elem` [y0-1, y1+1] tileGroup | isCorner xf yf && not allTheSame = TK.S_BASIC_OUTER_FENCE | otherwise = couterFenceTile fenceId <- fromMaybe (error $ "" `showFailure` tileGroup) <$> opick cotile tileGroup (const True) return (Point xf yf, fenceId) pointListN = [(x, y0-1) | x <- [x0-1..x1+1]] pointListE = [(x1+1, y) | y <- [y0..y1]] pointListS = [(x, y1+1) | x <- [x0-1..x1+1]] pointListW = [(x0-1, y) | y <- [y0..y1]] fenceListN <- mapM (fenceIdRnd cfenceTileN) pointListN fenceListE <- mapM (fenceIdRnd cfenceTileE) pointListE fenceListS <- mapM (fenceIdRnd cfenceTileS) pointListS fenceListW <- mapM (fenceIdRnd cfenceTileW) pointListW return $! EM.fromList $ fenceListN ++ fenceListE ++ fenceListS ++ fenceListW -- | Create a place by tiling patterns. tilePlace :: Area -- ^ the area to fill -> PlaceKind -- ^ the place kind to construct -> Rnd (EM.EnumMap Point Char) tilePlace area pl@PlaceKind{..} = do let (Point x0 y0, xspan, yspan) = spanArea area dxcorner = case ptopLeft of [] -> error $ "" `showFailure` (area, pl) l : _ -> T.length l (dx, dy) = assert (xspan >= dxcorner && yspan >= length ptopLeft `blame` (area, pl)) (xspan, yspan) fromX (x2, y2) = map (`Point` y2) [x2..] fillInterior :: (Int -> String -> String) -> (Int -> [String] -> [String]) -> [(Point, Char)] fillInterior f g = let tileInterior (y, row) = let fx = f dx row xStart = x0 + ((xspan - length fx) `div` 2) in filter ((/= 'X') . snd) $ zip (fromX (xStart, y)) fx reflected = let gy = g dy $ map T.unpack ptopLeft yStart = y0 + ((yspan - length gy) `div` 2) in zip [yStart..] gy in concatMap tileInterior reflected tileReflect :: Int -> [a] -> [a] tileReflect d pat = let lstart = take (d `divUp` 2) pat lend = take (d `div` 2) pat in lstart ++ reverse lend interior <- case pcover of CAlternate -> do let tile :: Int -> [a] -> [a] tile _ [] = error $ "nothing to tile" `showFailure` pl tile d pat = take d (cycle $ init pat ++ init (reverse pat)) return $! fillInterior tile tile CStretch -> do let stretch :: Int -> [a] -> [a] stretch _ [] = error $ "nothing to stretch" `showFailure` pl stretch d pat = tileReflect d (pat ++ repeat (last pat)) return $! fillInterior stretch stretch CReflect -> do let reflect :: Int -> [a] -> [a] reflect d pat = tileReflect d (cycle pat) return $! fillInterior reflect reflect CVerbatim -> return $! fillInterior (\ _ x -> x) (\ _ x -> x) CMirror -> do mirror1 <- oneOf [id, reverse] mirror2 <- oneOf [id, reverse] return $! fillInterior (\_ l -> mirror1 l) (\_ l -> mirror2 l) return $! EM.fromList interior LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/Fov.hs0000644000000000000000000003703407346545000021304 0ustar0000000000000000-- | Field Of View scanning. -- -- See -- for discussion. module Game.LambdaHack.Server.Fov ( -- * Perception cache FovValid(..), PerValidFid , PerReachable(..), CacheBeforeLucid(..), PerActor , PerceptionCache(..), PerCacheLid, PerCacheFid -- * Data used in FOV computation and cached to speed it up , FovShine(..), FovLucid(..), FovLucidLid , FovClear(..), FovClearLid, FovLit (..), FovLitLid -- * Operations , perceptionFromPTotal, perActorFromLevel, boundSightByCalm , totalFromPerActor, lucidFromLevel, perFidInDungeon #ifdef EXPOSE_INTERNAL -- * Internal operations , perceptionFromPTotalNoStash, cacheBeforeLucidFromActor, shineFromLevel , floorLightSources, lucidFromItems, litFromLevel , litInDungeon, clearFromLevel, clearInDungeon, lucidInDungeon , perLidFromFaction, perceptionCacheFromLevel , Matrix, fullscan #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Int (Int64) import qualified Data.IntSet as IS import GHC.Exts (inline) import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Server.FovDigital -- * Perception cache types data FovValid a = FovValid a | FovInvalid deriving (Show, Eq) -- | Main perception validity map, for all factions. -- -- The inner type is not a set, due to an unbenchmarked theory -- that a constant shape map is faster. type PerValidFid = EM.EnumMap FactionId (EM.EnumMap LevelId Bool) -- | Visually reachable positions (light passes through them to the actor). -- They need to be intersected with lucid positions to obtain visible positions. newtype PerReachable = PerReachable {preachable :: ES.EnumSet Point} deriving (Show, Eq) data CacheBeforeLucid = CacheBeforeLucid { creachable :: PerReachable , cnocto :: PerVisible , csmell :: PerSmelled } deriving (Show, Eq) type PerActor = EM.EnumMap ActorId (FovValid CacheBeforeLucid) -- We might cache even more effectively in terms of Enum{Set,Map} unions -- if we recorded for each field how many actors see it (and how many -- lights lit it). But this is complex and unions of EnumSets are cheaper -- than the EnumMaps that would be required. data PerceptionCache = PerceptionCache { ptotal :: FovValid CacheBeforeLucid , perActor :: PerActor } deriving (Show, Eq) -- | Server cache of perceptions of a single faction, -- indexed by level identifier. type PerCacheLid = EM.EnumMap LevelId PerceptionCache -- | Server cache of perceptions, indexed by faction identifier. type PerCacheFid = EM.EnumMap FactionId PerCacheLid -- * Data used in FOV computation -- | Map from level positions that currently hold item or actor(s) with shine -- to the maximum of radiuses of the shining lights. -- -- Note that floor and (many projectile) actors light on a single tile -- should be additive for @FovShine@ to be incrementally updated. -- -- @FovShine@ should not even be kept in @StateServer@, because it's cheap -- to compute, compared to @FovLucid@ and invalidated almost as often -- (not invalidated only by @UpdAlterTile@). newtype FovShine = FovShine {fovShine :: EM.EnumMap Point Int} deriving (Show, Eq) -- | Level positions with either ambient light or shining items or actors. newtype FovLucid = FovLucid {fovLucid :: ES.EnumSet Point} deriving (Show, Eq) type FovLucidLid = EM.EnumMap LevelId (FovValid FovLucid) -- | Level positions that pass through light and vision. newtype FovClear = FovClear {fovClear :: PointArray.Array Bool} deriving (Show, Eq) type FovClearLid = EM.EnumMap LevelId FovClear -- | Level positions with tiles that have ambient light. newtype FovLit = FovLit {fovLit :: ES.EnumSet Point} deriving (Show, Eq) type FovLitLid = EM.EnumMap LevelId FovLit -- * Update of invalidated Fov data -- | Compute positions visible (reachable and seen) by the party. -- A position is lucid, if it's lit by an ambient light or by a weak, portable -- light source, e.g,, carried by an actor. A reachable and lucid position -- is visible. Additionally, positions directly adjacent to an actor are -- assumed to be visible to him (through sound, touch, noctovision, whatever). perceptionFromPTotal :: FactionId -> LevelId -> FovLucid -> CacheBeforeLucid -> State -> Perception perceptionFromPTotal fid lidPer fovLucid ptotal s = let per = perceptionFromPTotalNoStash fovLucid ptotal in case gstash $ sfactionD s EM.! fid of Just (lid, pos) | lid == lidPer -> per {psight = (psight per) {pvisible = ES.insert pos $ pvisible (psight per)}} _ -> per perceptionFromPTotalNoStash :: FovLucid -> CacheBeforeLucid -> Perception perceptionFromPTotalNoStash FovLucid{fovLucid} ptotal = let nocto = pvisible $ cnocto ptotal reach = preachable $ creachable ptotal psight = PerVisible $ nocto `ES.union` (reach `ES.intersection` fovLucid) psmell = csmell ptotal in Perception{..} perActorFromLevel :: PerActor -> (ActorId -> Actor) -> ActorMaxSkills -> FovClear -> PerActor perActorFromLevel perActorOld getActorB actorMaxSkills fovClear = -- Dying actors included, to let them see their own demise. let f _ fv@FovValid{} = fv f aid FovInvalid = let actorMaxSk = actorMaxSkills EM.! aid b = getActorB aid in FovValid $ cacheBeforeLucidFromActor fovClear b actorMaxSk in EM.mapWithKey f perActorOld boundSightByCalm :: Int -> Int64 -> Int boundSightByCalm sight calm = min (fromEnum $ calm `div` xM 5) sight -- | Compute positions reachable by the actor. Reachable are all fields -- on a visually unblocked path from the actor position. -- Also compute positions seen by noctovision and perceived by smell. cacheBeforeLucidFromActor :: FovClear -> Actor -> Ability.Skills -> CacheBeforeLucid cacheBeforeLucidFromActor clearPs body actorMaxSk = let radius = boundSightByCalm (Ability.getSk Ability.SkSight actorMaxSk) (bcalm body) spectatorPos = bpos body creachable = PerReachable $ fullscan radius spectatorPos clearPs cnocto = PerVisible $ fullscan (Ability.getSk Ability.SkNocto actorMaxSk) spectatorPos clearPs smellRadius = if Ability.getSk Ability.SkSmell actorMaxSk >= 2 then 2 else 0 csmell = PerSmelled $ fullscan smellRadius spectatorPos clearPs in CacheBeforeLucid{..} totalFromPerActor :: PerActor -> CacheBeforeLucid totalFromPerActor perActor = let fromValid = \case FovValid x -> x FovInvalid -> error $ "" `showFailure` perActor addCacheBeforeLucid x cbl1 = let cbl2 = fromValid x in CacheBeforeLucid { creachable = PerReachable $ ES.union (preachable $ creachable cbl1) (preachable $ creachable cbl2) , cnocto = PerVisible $ ES.union (pvisible $ cnocto cbl1) (pvisible $ cnocto cbl2) , csmell = PerSmelled $ ES.union (psmelled $ csmell cbl1) (psmelled $ csmell cbl2) } emptyCacheBeforeLucid = CacheBeforeLucid { creachable = PerReachable ES.empty , cnocto = PerVisible ES.empty , csmell = PerSmelled ES.empty } in foldr addCacheBeforeLucid emptyCacheBeforeLucid $ EM.elems perActor -- | Update lights on the level. This is needed every (even enemy) -- actor move to show thrown torches. -- We need to update lights even if cmd doesn't change any perception, -- so that for next cmd that does, but doesn't change lights, -- and operates on the same level, the lights are up to date. -- We could make lights lazy to ensure no computation is wasted, -- but it's rare that cmd changed them, but not the perception -- (e.g., earthquake in an uninhabited corner of the active arena, -- but the we'd probably want some feedback, at least sound). lucidFromLevel :: FovClearLid -> FovLitLid -> State -> LevelId -> Level -> FovLucid lucidFromLevel fovClearLid fovLitLid s lid lvl = let shine = shineFromLevel s lid lvl lucids = lucidFromItems (fovClearLid EM.! lid) $ EM.assocs $ fovShine shine litTiles = fovLitLid EM.! lid in FovLucid $ ES.unions $ fovLit litTiles : map fovLucid lucids shineFromLevel :: State -> LevelId -> Level -> FovShine shineFromLevel s lid lvl = -- Actors shine as if they were leaders, for speed and to prevent -- micromanagement by switching leader to see more. let actorLights = [ (bpos b, radius) | (aid, b) <- inline actorAssocs (const True) lid s , let radius = Ability.getSk Ability.SkShine $ getActorMaxSkills aid s , radius > 0 ] floorLights = floorLightSources (sdiscoAspect s) lvl allLights = floorLights ++ actorLights -- If there is light both on the floor and carried by actor -- (or several projectile actors), its radius is the maximum. in FovShine $ EM.fromListWith max allLights floorLightSources :: DiscoveryAspect -> Level -> [(Point, Int)] floorLightSources discoAspect lvl = -- Not enough oxygen to have more than one light lit on a given tile. -- Items obscuring or dousing off fire are not cumulative as well. let processIid (accLight, accDouse) (iid, _) = let shine = IA.getSkill Ability.SkShine $ discoAspect EM.! iid in case compare shine 0 of EQ -> (accLight, accDouse) GT -> (max shine accLight, accDouse) LT -> (accLight, min shine accDouse) processBag bag acc = foldl' processIid acc $ EM.assocs bag in [ (p, radius) | (p, bag) <- EM.assocs $ lfloor lvl -- lembed are hidden , let (maxLight, maxDouse) = processBag bag (0, 0) radius = maxLight + maxDouse , radius > 0 ] -- | Compute all dynamically lit positions on a level, whether lit by actors -- or shining floor items. Note that an actor can be blind, -- in which case he doesn't see his own light (but others, -- from his or other factions, possibly do). lucidFromItems :: FovClear -> [(Point, Int)] -> [FovLucid] lucidFromItems clearPs allItems = let lucidPos (!p, !shine) = FovLucid $ fullscan shine p clearPs in map lucidPos allItems -- * Computation of initial perception and caches -- | Calculate the perception and its caches for the whole dungeon. perFidInDungeon :: State -> ( FovLitLid, FovClearLid, FovLucidLid , PerValidFid, PerCacheFid, PerFid) perFidInDungeon s = let fovLitLid = litInDungeon s fovClearLid = clearInDungeon s fovLucidLid = lucidInDungeon fovClearLid fovLitLid s perValidLid = EM.map (const True) (sdungeon s) perValidFid = EM.map (const perValidLid) (sfactionD s) f fid _ = perLidFromFaction fovLucidLid fovClearLid fid s em = EM.mapWithKey f $ sfactionD s in ( fovLitLid, fovClearLid, fovLucidLid , perValidFid, EM.map snd em, EM.map fst em) litFromLevel :: COps -> Level -> FovLit litFromLevel COps{coTileSpeedup} Level{ltile} = let litSet p t set = if Tile.isLit coTileSpeedup t then p : set else set in FovLit $ ES.fromDistinctAscList $ PointArray.ifoldrA' litSet [] ltile litInDungeon :: State -> FovLitLid litInDungeon s = EM.map (litFromLevel (scops s)) $ sdungeon s clearFromLevel :: COps -> Level -> FovClear clearFromLevel COps{coTileSpeedup} Level{ltile} = FovClear $ PointArray.mapA (Tile.isClear coTileSpeedup) ltile clearInDungeon :: State -> FovClearLid clearInDungeon s = EM.map (clearFromLevel (scops s)) $ sdungeon s lucidInDungeon :: FovClearLid -> FovLitLid -> State -> FovLucidLid lucidInDungeon fovClearLid fovLitLid s = EM.mapWithKey (\lid lvl -> FovValid $ lucidFromLevel fovClearLid fovLitLid s lid lvl) $ sdungeon s -- | Calculate perception of a faction. perLidFromFaction :: FovLucidLid -> FovClearLid -> FactionId -> State -> (PerLid, PerCacheLid) perLidFromFaction fovLucidLid fovClearLid fid s = let em = EM.mapWithKey (\lid _ -> perceptionCacheFromLevel fovClearLid fid lid s) (sdungeon s) fovLucid lid = case EM.lookup lid fovLucidLid of Just (FovValid fl) -> fl _ -> error $ "" `showFailure` (lid, fovLucidLid) getValid (FovValid pc) = pc getValid FovInvalid = error $ "" `showFailure` fid per lid pc = perceptionFromPTotal fid lid (fovLucid lid) (getValid (ptotal pc)) s in (EM.mapWithKey per em, em) perceptionCacheFromLevel :: FovClearLid -> FactionId -> LevelId -> State -> PerceptionCache perceptionCacheFromLevel fovClearLid fid lid s = let fovClear = fovClearLid EM.! lid lvlBodies = inline actorAssocs (== fid) lid s f (aid, b) = -- Actors see and smell as if they were leaders, for speed -- and to prevent micromanagement by switching leader to see more. let actorMaxSk = getActorMaxSkills aid s in if Ability.getSk Ability.SkSight actorMaxSk <= 0 && Ability.getSk Ability.SkNocto actorMaxSk <= 0 && Ability.getSk Ability.SkSmell actorMaxSk <= 0 then Nothing -- dumb missile else Just (aid, FovValid $ cacheBeforeLucidFromActor fovClear b actorMaxSk) lvlCaches = mapMaybe f lvlBodies perActor = EM.fromDistinctAscList lvlCaches total = totalFromPerActor perActor in PerceptionCache{ptotal = FovValid total, perActor} -- * The actual Fov algorithm type Matrix = (Int, Int, Int, Int) -- | Perform a full scan for a given position. Returns the positions -- that are currently in the field of view. -- The actor's own position is considred in his field of view. fullscan :: Int -- ^ scanning radius -> Point -- ^ position of the spectator -> FovClear -- ^ the array with clear positions -> ES.EnumSet Point fullscan !radius spectatorPos fc = case radius of 2 -> squareUnsafeSet spectatorPos 1 -> ES.singleton spectatorPos 0 -> ES.empty -- e.g., smell for non-smelling _ | radius <= 0 -> ES.empty _ -> let !FovClear{fovClear} = fc !spectatorI = fromEnum spectatorPos mapTr :: Matrix -> [PointI] mapTr m@(!_, !_, !_, !_) = scan (radius - 1) isClear (trV m) trV :: Matrix -> Bump -> PointI {-# INLINE trV #-} trV (x1, y1, x2, y2) B{..} = spectatorI + fromEnum (Vector (x1 * bx + y1 * by) (x2 * bx + y2 * by)) isClear :: PointI -> Bool {-# INLINE isClear #-} isClear = PointArray.accessI fovClear in ES.intSetToEnumSet $ IS.fromList $ [spectatorI] ++ mapTr (1, 0, 0, -1) -- quadrant I ++ mapTr (0, 1, 1, 0) -- II (counter-clockwise) ++ mapTr (-1, 0, 0, 1) -- III ++ mapTr (0, -1, -1, 0) -- IV LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/FovDigital.hs0000644000000000000000000002777307346545000022613 0ustar0000000000000000-- | DFOV (Digital Field of View) implemented according to specification at . -- This fast version of the algorithm, based on PFOV, has AFAIK -- never been described nor implemented before. -- -- The map is processed in depth-first-search manner, that is, as soon -- as we detect on obstacle we move away from the viewer up to the -- FOV radius and then restart on the other side of the obstacle. -- This has better cache behaviour than breadth-firsts-search, -- where we would process all tiles equally distant from the viewer -- in the same round, because then we'd need to keep the many convex hulls -- and edges, not just a single set, and we'd potentially traverse all -- of them each round. module Game.LambdaHack.Server.FovDigital ( scan -- * Scanning coordinate system , Bump(..) -- * Assorted minor operations #ifdef EXPOSE_INTERNAL -- * Current scan parameters , Distance, Progress -- * Geometry in system @Bump@ , LineOrdering, Line(..), ConvexHull(..), CHull(..), Edge, EdgeInterval -- * Internal operations , steepestInHull, foldlCHull', addToHull, addToHullGo , createLine, steepness, intersect , _debugSteeper, _debugLine #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude hiding (intersect) import Game.LambdaHack.Common.Point (PointI) -- | Distance from the (0, 0) point where FOV originates. type Distance = Int -- | Progress along an arc with a constant distance from (0, 0). type Progress = Int -- | Rotated and translated coordinates of 2D points, so that the points fit -- in a single quadrant area (e, g., quadrant I for Permissive FOV, hence both -- coordinates positive; adjacent diagonal halves of quadrant I and II -- for Digital FOV, hence y positive). -- The special coordinates are written using the standard mathematical -- coordinate setup, where quadrant I, with x and y positive, -- is on the upper right. data Bump = B { bx :: Int , by :: Int } deriving Show -- | Two strict orderings of lines with a common point. data LineOrdering = Steeper | Shallower -- | Straight line between points. data Line = Line Bump Bump deriving Show -- | Convex hull represented as a non-empty list of points. data ConvexHull = ConvexHull Bump CHull deriving Show data CHull = CHNil | CHCons Bump CHull deriving Show -- | An edge (comprising of a line and a convex hull) of the area to be scanned. type Edge = (Line, ConvexHull) -- | The contiguous area left to be scanned, delimited by edges. type EdgeInterval = (Edge, Edge) -- | Calculates the list of tiles visible from (0, 0) within the given -- sight range. scan :: Distance -- ^ visiblity distance -> (PointI -> Bool) -- ^ visually clear position predicate -> (Bump -> PointI) -- ^ coordinate transformation -> [PointI] {-# INLINE scan #-} scan !r isClear tr = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (r > 0 `blame` r) $ -- not really expensive, but obfuscates Core #endif -- The scanned area is a square, which is a sphere in the chessboard metric. dscan 1 ( (Line (B 1 0) (B (-r) r), ConvexHull (B 0 0) CHNil) , (Line (B 0 0) (B (r+1) r), ConvexHull (B 1 0) CHNil) ) where dscan :: Distance -> EdgeInterval -> [PointI] {-# INLINE dscan #-} dscan !d ( (sl{-shallow line-}, sHull), (el{-steep line-}, eHull) ) = dgo d sl sHull el eHull -- Speed (mosty JS) and generally convincing GHC to unbox stuff. dgo :: Distance -> Line -> ConvexHull -> Line -> ConvexHull -> [PointI] dgo !d !sl sHull !el eHull = -- @sHull@ and @eHull@ may be unused let !ps0 = let (n, k) = intersect sl d -- minimal progress to consider in n `div` k !pe = let (n, k) = intersect el d -- maximal progress to consider -- Corners obstruct view, so the steep line, constructed -- from corners, is itself not a part of the view, -- so if its intersection with the horizonstal line at distance -- @d@ is only at a corner, we choose the position leading -- to a smaller view. in -1 + n `divUp` k outside = if d < r then let !trBump = bump ps0 in if isClear trBump then trBump : mscanVisible sl sHull (ps0+1) -- start visible else trBump : mscanShadowed (ps0+1) -- start in shadow else map bump [ps0..pe] bump :: Progress -> PointI bump !px = tr $ B px d -- We're in a visible interval. mscanVisible :: Line -> ConvexHull -> Progress -> [PointI] mscanVisible line hull = goVisible where goVisible :: Progress -> [PointI] goVisible !ps = if ps <= pe then let !trBump = bump ps in if isClear trBump -- not entering shadow then trBump : goVisible (ps+1) else let steepBump = B ps d nep = steepestInHull Shallower steepBump hull neLine = createLine nep steepBump neHull = addToHull Shallower steepBump eHull in trBump : dgo (d+1) line hull neLine neHull ++ mscanShadowed (ps+1) -- note how we recursively scan more and more -- distant tiles, up to the FOV radius, -- before starting to process the shadow else dgo (d+1) line hull el eHull -- reached end, scan next row -- We're in a shadowed interval. mscanShadowed :: Progress -> [PointI] mscanShadowed !ps = if ps <= pe then let !trBump = bump ps in if not $ isClear trBump -- not moving out of shadow then trBump : mscanShadowed (ps+1) else let shallowBump = B ps d nsp = steepestInHull Steeper shallowBump eHull nsLine = createLine nsp shallowBump nsHull = addToHull Steeper shallowBump sHull in trBump : mscanVisible nsLine nsHull (ps+1) else [] -- reached end while in shadow in #ifdef WITH_EXPENSIVE_ASSERTIONS assert (r >= d && d >= 0 && pe >= ps0 `blame` (r,d,sl,sHull,el,eHull,ps0,pe)) #endif outside -- | Specialized implementation for speed in the inner loop. Not partial. steepestInHull :: LineOrdering -> Bump -> ConvexHull -> Bump {-# NOINLINE steepestInHull #-} steepestInHull !lineOrdering !new (ConvexHull !b !ch) = foldlCHull' max' b ch where max' !x !y = if steepness lineOrdering new x y then x else y -- | Standard @foldl'@ over @CHull@. foldlCHull' :: (a -> Bump -> a) -> a -> CHull -> a {-# INLINE foldlCHull' #-} foldlCHull' f = fgo where fgo !z CHNil = z fgo z (CHCons b ch) = fgo (f z b) ch -- | Extends a convex hull of bumps with a new bump. The new bump makes -- some old bumps unnecessary, e.g. those that are joined with the new steep -- bump with lines that are not shallower than any newer lines in the hull. -- Removing such unnecessary bumps slightly speeds up computation -- of 'steepestInHull'. -- -- Recursion in @addToHullGo@ seems spurious, but it's called each time with -- potentially different comparison predicate, so it's necessary. addToHull :: LineOrdering -- ^ the line ordering to use -> Bump -- ^ a new bump to consider -> ConvexHull -- ^ a convex hull of bumps represented as a list -> ConvexHull {-# INLINE addToHull #-} addToHull lineOrdering new (ConvexHull old ch) = ConvexHull new $ addToHullGo lineOrdering new $ CHCons old ch -- This worker is needed to avoid Core returning a pair (new, result) -- and also Bump-packing new (steepBump/shallowBump) twice, losing sharing. addToHullGo :: LineOrdering -> Bump -> CHull -> CHull {-# NOINLINE addToHullGo #-} addToHullGo !lineOrdering !new = hgo where hgo :: CHull -> CHull hgo (CHCons a ch@(CHCons b _)) | not (steepness lineOrdering new b a) = hgo ch hgo ch = ch -- | Create a line from two points. -- -- Debug: check if well-defined. createLine :: Bump -> Bump -> Line {-# INLINE createLine #-} createLine p1 p2 = let line = Line p1 p2 in #ifdef WITH_EXPENSIVE_ASSERTIONS assert (uncurry blame $ _debugLine line) #endif line -- | Strictly compare steepness of lines @(b1, bf)@ and @(b2, bf)@, -- according to the @LineOrdering@ given. This is related to comparing -- the slope (gradient, angle) of two lines, but simplified wrt signs -- to work fast in this particular setup. -- -- Debug: Verify that the results of 2 independent checks are equal. steepness :: LineOrdering -> Bump -> Bump -> Bump -> Bool {-# INLINE steepness #-} steepness lineOrdering (B xf yf) (B x1 y1) (B x2 y2) = let y2x1 = (yf - y2) * (xf - x1) y1x2 = (yf - y1) * (xf - x2) res = case lineOrdering of Steeper -> y2x1 > y1x2 Shallower -> y2x1 < y1x2 in #ifdef WITH_EXPENSIVE_ASSERTIONS assert (res == _debugSteeper lineOrdering (B xf yf) (B x1 y1) (B x2 y2)) #endif res {- | A pair @(a, b)@ such that @a@ divided by @b@ is the X coordinate of the intersection of a given line and the horizontal line at distance @d@ above the X axis. Derivation of the formula: The intersection point @(xt, yt)@ satisfies the following equalities: > yt = d > (yt - y) (xf - x) = (xt - x) (yf - y) hence > (yt - y) (xf - x) = (xt - x) (yf - y) > (d - y) (xf - x) = (xt - x) (yf - y) > (d - y) (xf - x) + x (yf - y) = xt (yf - y) > xt = ((d - y) (xf - x) + x (yf - y)) / (yf - y) General remarks: The FOV agrees with physical properties of tiles as diamonds and visibility from any point to any point. A diamond is denoted by the left corner of its encompassing tile. Hero is at (0, 0). Order of processing in the first quadrant rotated by 45 degrees is > 45678 > 123 > @ so the first processed diamond is at (-1, 1). The order is similar as for the restrictive shadow casting algorithm and reversed wrt PFOV. The fast moving line when scanning is called the shallow line, and it's the one that delimits the view from the left, while the steep line is on the right, opposite to PFOV. We start scanning from the left. The 'PointI' ('Enum' representation of @Point@) coordinates are cartesian. The 'Bump' coordinates are cartesian, translated so that the hero is at (0, 0) and rotated so that he always looks at the first (rotated 45 degrees) quadrant. The ('Progress', 'Distance') cordinates coincide with the @Bump@ coordinates, unlike in PFOV. Debug: check that the line fits in the upper half-plane. -} intersect :: Line -> Distance -> (Int, Int) {-# INLINE intersect #-} intersect (Line (B x y) (B xf yf)) d = #ifdef WITH_EXPENSIVE_ASSERTIONS assert (allB (>= 0) [y, yf]) #endif ((d - y)*(xf - x) + x*(yf - y), yf - y) -- | Debug functions for DFOV: -- | Debug: calculate steepness for DFOV in another way and compare results. _debugSteeper :: LineOrdering -> Bump -> Bump -> Bump -> Bool {-# INLINE _debugSteeper #-} _debugSteeper lineOrdering f@(B _xf yf) p1@(B _x1 y1) p2@(B _x2 y2) = assert (allB (>= 0) [yf, y1, y2]) $ let (n1, k1) = intersect (Line p1 f) 0 (n2, k2) = intersect (Line p2 f) 0 sign = case lineOrdering of Steeper -> GT Shallower -> LT in compare (k1 * n2) (n1 * k2) == sign -- | Debug: check if a view border line for DFOV is legal. _debugLine :: Line -> (Bool, String) {-# INLINE _debugLine #-} _debugLine line@(Line (B x1 y1) (B x2 y2)) | not (allB (>= 0) [y1, y2]) = (False, "negative Y coordinates: " ++ show line) | y1 == y2 && x1 == x2 = (False, "ill-defined line: " ++ show line) | y1 == y2 = (False, "horizontal line: " ++ show line) | crossL0 = (False, "crosses the X axis below 0: " ++ show line) | crossG1 = (False, "crosses the X axis above 1: " ++ show line) | otherwise = (True, "") where (n, k) = line `intersect` 0 (q, r) = if k == 0 then (0, 0) else n `divMod` k crossL0 = q < 0 -- q truncated toward negative infinity crossG1 = q >= 1 && (q > 1 || r /= 0) LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/HandleAtomicM.hs0000644000000000000000000004042007346545000023210 0ustar0000000000000000-- | Handle atomic commands on the server, after they are executed -- to change server 'State' and before they are sent to clients. module Game.LambdaHack.Server.HandleAtomicM ( cmdAtomicSemSer #ifdef EXPOSE_INTERNAL -- * Internal operations , validateFloor, validateFloorBag, levelOfStash , invalidateArenas, updateSclear, updateSlit , invalidateLucidLid, invalidateLucidAid , actorHasShine, itemAffectsShineRadius, itemAffectsPerRadius , addPerActor, addPerActorAny, deletePerActor, deletePerActorAny , invalidatePerActor, reconsiderPerActor, invalidatePerLid #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.Fov import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.State -- | Effect of atomic actions on server state is calculated -- with the global state from after the command is executed -- (except where the supplied @oldState@ is used). cmdAtomicSemSer :: MonadServer m => State -> UpdAtomic -> m () cmdAtomicSemSer oldState cmd = case cmd of UpdRegisterItems{} -> return () UpdCreateActor aid b _ -> do actorMaxSkills <- getsState sactorMaxSkills when (actorHasShine actorMaxSkills aid) $ invalidateLucidLid $ blid b addPerActor aid b UpdDestroyActor aid b _ -> do let actorMaxSkillsOld = sactorMaxSkills oldState when (actorHasShine actorMaxSkillsOld aid) $ invalidateLucidLid $ blid b deletePerActor actorMaxSkillsOld aid b modifyServer $ \ser -> ser { sactorTime = EM.adjust (EM.adjust (EM.delete aid) (blid b)) (bfid b) (sactorTime ser) , strajTime = EM.adjust (EM.adjust (EM.delete aid) (blid b)) (bfid b) (strajTime ser) , strajPushedBy = EM.delete aid (strajPushedBy ser) , sactorAn = EM.delete aid (sactorAn ser) , sactorStasis = ES.delete aid (sactorStasis ser) } UpdCreateItem _ iid _ _ (CFloor lid _) -> validateFloor iid lid UpdCreateItem _ iid _ _ (CActor aid CStash) -> do lid <- levelOfStash aid validateFloor iid lid UpdCreateItem _ iid _ _ (CActor aid CGround) -> do lid <- getsState $ blid . getActorBody aid validateFloor iid lid UpdCreateItem _ iid _ _ (CActor aid _) -> do discoAspect <- getsState sdiscoAspect when (itemAffectsShineRadius discoAspect iid) $ invalidateLucidAid aid when (itemAffectsPerRadius discoAspect iid) $ reconsiderPerActor aid UpdCreateItem{} -> return () UpdDestroyItem _ iid _ _ (CFloor lid _) -> validateFloor iid lid UpdDestroyItem _ iid _ _ (CActor aid CStash) -> do lid <- levelOfStash aid validateFloor iid lid UpdDestroyItem _ iid _ _ (CActor aid CGround) -> do lid <- getsState $ blid . getActorBody aid validateFloor iid lid UpdDestroyItem _ iid _ _ (CActor aid _) -> do discoAspect <- getsState sdiscoAspect when (itemAffectsShineRadius discoAspect iid) $ invalidateLucidAid aid when (itemAffectsPerRadius discoAspect iid) $ reconsiderPerActor aid UpdDestroyItem{} -> return () UpdSpotActor aid b -> do -- On server, it does't affect aspects, but does affect lucid (Ascend). actorMaxSkills <- getsState sactorMaxSkills when (actorHasShine actorMaxSkills aid) $ invalidateLucidLid $ blid b addPerActor aid b UpdLoseActor aid b -> do -- On server, it does't affect aspects, but does affect lucid (Ascend). let actorMaxSkillsOld = sactorMaxSkills oldState when (actorHasShine actorMaxSkillsOld aid) $ invalidateLucidLid $ blid b deletePerActor actorMaxSkillsOld aid b modifyServer $ \ser -> ser { sactorTime = EM.adjust (EM.adjust (EM.delete aid) (blid b)) (bfid b) (sactorTime ser) , strajTime = EM.adjust (EM.adjust (EM.delete aid) (blid b)) (bfid b) (strajTime ser) , strajPushedBy = EM.delete aid (strajPushedBy ser) , sactorAn = EM.delete aid (sactorAn ser) , sactorStasis = ES.delete aid (sactorStasis ser) } UpdSpotItem _ iid _ (CFloor lid _) -> validateFloor iid lid UpdSpotItem _ iid _ (CActor aid CStash) -> do lid <- levelOfStash aid validateFloor iid lid UpdSpotItem _ iid _ (CActor aid CGround) -> do lid <- getsState $ blid . getActorBody aid validateFloor iid lid UpdSpotItem _ iid _ (CActor aid _) -> do discoAspect <- getsState sdiscoAspect when (itemAffectsShineRadius discoAspect iid) $ invalidateLucidAid aid when (itemAffectsPerRadius discoAspect iid) $ reconsiderPerActor aid UpdSpotItem{} -> return () UpdLoseItem _ iid _ (CFloor lid _) -> validateFloor iid lid UpdLoseItem _ iid _ (CActor aid CStash) -> do lid <- levelOfStash aid validateFloor iid lid UpdLoseItem _ iid _ (CActor aid CGround) -> do lid <- getsState $ blid . getActorBody aid validateFloor iid lid UpdLoseItem _ iid _ (CActor aid _) -> do discoAspect <- getsState sdiscoAspect when (itemAffectsShineRadius discoAspect iid) $ invalidateLucidAid aid when (itemAffectsPerRadius discoAspect iid) $ reconsiderPerActor aid UpdLoseItem{} -> return () UpdSpotItemBag _ (CFloor lid _) bag -> validateFloorBag bag lid UpdSpotItemBag _ (CActor aid CStash) bag -> do lid <- levelOfStash aid validateFloorBag bag lid UpdSpotItemBag _ (CActor aid CGround) bag -> do lid <- getsState $ blid . getActorBody aid validateFloorBag bag lid UpdSpotItemBag _ (CActor aid _) bag -> do discoAspect <- getsState sdiscoAspect let iids = EM.keys bag when (any (itemAffectsShineRadius discoAspect) iids) $ invalidateLucidAid aid when (any (itemAffectsPerRadius discoAspect) iids) $ reconsiderPerActor aid UpdSpotItemBag{} -> return () UpdLoseItemBag _ (CFloor lid _) bag -> validateFloorBag bag lid UpdLoseItemBag _ (CActor aid CStash) bag -> do lid <- levelOfStash aid validateFloorBag bag lid UpdLoseItemBag _ (CActor aid CGround) bag -> do lid <- levelOfStash aid validateFloorBag bag lid UpdLoseItemBag _ (CActor aid _) bag -> do discoAspect <- getsState sdiscoAspect let iids = EM.keys bag when (any (itemAffectsShineRadius discoAspect) iids) $ invalidateLucidAid aid when (any (itemAffectsPerRadius discoAspect) iids) $ reconsiderPerActor aid UpdLoseItemBag{} -> return () UpdMoveActor aid _ _ -> do actorMaxSkills <- getsState sactorMaxSkills when (actorHasShine actorMaxSkills aid) $ invalidateLucidAid aid invalidatePerActor aid UpdWaitActor{} -> return () UpdDisplaceActor aid1 aid2 -> do actorMaxSkills <- getsState sactorMaxSkills when (actorHasShine actorMaxSkills aid1 || actorHasShine actorMaxSkills aid2) $ invalidateLucidAid aid1 -- the same lid as aid2 invalidatePerActor aid1 invalidatePerActor aid2 UpdMoveItem iid _k aid s1 s2 -> do let dummyVerbose = False dummyKit = quantSingle cmdAtomicSemSer oldState $ UpdLoseItem dummyVerbose iid dummyKit (CActor aid s1) cmdAtomicSemSer oldState $ UpdSpotItem dummyVerbose iid dummyKit (CActor aid s2) UpdRefillHP{} -> return () UpdRefillCalm aid _ -> do actorMaxSk <- getsState $ getActorMaxSkills aid body <- getsState $ getActorBody aid let sight = Ability.getSk Ability.SkSight actorMaxSk oldBody = getActorBody aid oldState radiusOld = boundSightByCalm sight (bcalm oldBody) radiusNew = boundSightByCalm sight (bcalm body) when (radiusOld /= radiusNew) $ invalidatePerActor aid UpdTrajectory{} -> return () UpdQuitFaction{} -> return () UpdSpotStashFaction _ fid lid _ -> invalidatePerFidLid fid lid UpdLoseStashFaction _ fid lid _ -> invalidatePerFidLid fid lid UpdLeadFaction{} -> invalidateArenas UpdDiplFaction{} -> return () UpdDoctrineFaction{} -> return () UpdAutoFaction{} -> return () UpdRecordKill{} -> invalidateArenas UpdAlterTile lid pos fromTile toTile -> do clearChanged <- updateSclear lid pos fromTile toTile litChanged <- updateSlit lid pos fromTile toTile when (clearChanged || litChanged) $ invalidateLucidLid lid when clearChanged $ invalidatePerLid lid UpdAlterExplorable{} -> return () UpdAlterGold{} -> return () UpdSearchTile{} -> return () UpdHideTile{} -> return () UpdSpotTile{} -> return () UpdLoseTile{} -> return () UpdSpotEntry{} -> return () UpdLoseEntry{} -> return () UpdAlterSmell{} -> return () UpdSpotSmell{} -> return () UpdLoseSmell{} -> return () UpdTimeItem{} -> return () UpdAgeGame{} -> return () UpdUnAgeGame{} -> return () UpdDiscover{} -> return () UpdCover{} -> return () UpdDiscoverKind{} -> return () UpdCoverKind{} -> return () UpdDiscoverAspect{} -> return () UpdCoverAspect{} -> return () UpdDiscoverServer{} -> return () UpdCoverServer{} -> return () UpdPerception{} -> return () UpdRestart{} -> return () UpdRestartServer{} -> return () UpdResume{} -> return () UpdResumeServer{} -> return () UpdKillExit{} -> return () UpdWriteSave{} -> return () UpdHearFid{} -> return () UpdMuteMessages{} -> return () validateFloor :: MonadServer m => ItemId -> LevelId -> m () validateFloor iid lid = do discoAspect <- getsState sdiscoAspect when (itemAffectsShineRadius discoAspect iid) $ invalidateLucidLid lid validateFloorBag :: MonadServer m => ItemBag -> LevelId -> m () validateFloorBag bag lid = do discoAspect <- getsState sdiscoAspect let iids = EM.keys bag when (any (itemAffectsShineRadius discoAspect) iids) $ invalidateLucidLid lid levelOfStash :: MonadStateRead m => ActorId -> m LevelId levelOfStash aid = do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, _) -> return lid Nothing -> error $ "" `showFailure` (aid, b) invalidateArenas :: MonadServer m => m () invalidateArenas = modifyServer $ \ser -> ser {svalidArenas = False} updateSclear :: MonadServer m => LevelId -> Point -> ContentId TileKind -> ContentId TileKind -> m Bool updateSclear lid pos fromTile toTile = do COps{coTileSpeedup} <- getsState scops let fromClear = Tile.isClear coTileSpeedup fromTile toClear = Tile.isClear coTileSpeedup toTile if fromClear == toClear then return False else do let f FovClear{fovClear} = FovClear $ fovClear PointArray.// [(pos, toClear)] modifyServer $ \ser -> ser {sfovClearLid = EM.adjust f lid $ sfovClearLid ser} return True updateSlit :: MonadServer m => LevelId -> Point -> ContentId TileKind -> ContentId TileKind -> m Bool updateSlit lid pos fromTile toTile = do COps{coTileSpeedup} <- getsState scops let fromLit = Tile.isLit coTileSpeedup fromTile toLit = Tile.isLit coTileSpeedup toTile if fromLit == toLit then return False else do let f (FovLit set) = FovLit $ if toLit then ES.insert pos set else ES.delete pos set modifyServer $ \ser -> ser {sfovLitLid = EM.adjust f lid $ sfovLitLid ser} return True invalidateLucidLid :: MonadServer m => LevelId -> m () invalidateLucidLid lid = modifyServer $ \ser -> ser { sfovLucidLid = EM.insert lid FovInvalid $ sfovLucidLid ser , sperValidFid = EM.map (EM.insert lid False) $ sperValidFid ser } invalidateLucidAid :: MonadServer m => ActorId -> m () invalidateLucidAid aid = do lid <- getsState $ blid . getActorBody aid invalidateLucidLid lid actorHasShine :: ActorMaxSkills -> ActorId -> Bool actorHasShine actorMaxSkills aid = case EM.lookup aid actorMaxSkills of Just actorMaxSk -> Ability.getSk Ability.SkShine actorMaxSk > 0 Nothing -> error $ "" `showFailure` aid itemAffectsShineRadius :: DiscoveryAspect -> ItemId -> Bool itemAffectsShineRadius discoAspect iid = case EM.lookup iid discoAspect of Just arItem -> IA.getSkill Ability.SkShine arItem /= 0 Nothing -> error $ "" `showFailure` iid itemAffectsPerRadius :: DiscoveryAspect -> ItemId -> Bool itemAffectsPerRadius discoAspect iid = case EM.lookup iid discoAspect of Just arItem -> IA.getSkill Ability.SkSight arItem /= 0 || IA.getSkill Ability.SkSmell arItem /= 0 || IA.getSkill Ability.SkNocto arItem /= 0 Nothing -> error $ "" `showFailure` iid addPerActor :: MonadServer m => ActorId -> Actor -> m () addPerActor aid b = do actorMaxSk <- getsState $ getActorMaxSkills aid unless (Ability.getSk Ability.SkSight actorMaxSk <= 0 && Ability.getSk Ability.SkNocto actorMaxSk <= 0 && Ability.getSk Ability.SkSmell actorMaxSk <= 0) $ addPerActorAny aid b addPerActorAny :: MonadServer m => ActorId -> Actor -> m () addPerActorAny aid b = do let fid = bfid b lid = blid b f PerceptionCache{perActor} = PerceptionCache { ptotal = FovInvalid , perActor = EM.insert aid FovInvalid perActor } modifyServer $ \ser -> ser { sperCacheFid = EM.adjust (EM.adjust f lid) fid $ sperCacheFid ser , sperValidFid = EM.adjust (EM.insert lid False) fid $ sperValidFid ser } deletePerActor :: MonadServer m => ActorMaxSkills -> ActorId -> Actor -> m () deletePerActor actorMaxSkillsOld aid b = do let actorMaxSk = actorMaxSkillsOld EM.! aid unless (Ability.getSk Ability.SkSight actorMaxSk <= 0 && Ability.getSk Ability.SkNocto actorMaxSk <= 0 && Ability.getSk Ability.SkSmell actorMaxSk <= 0) $ deletePerActorAny aid b deletePerActorAny :: MonadServer m => ActorId -> Actor -> m () deletePerActorAny aid b = do let fid = bfid b lid = blid b f PerceptionCache{perActor} = PerceptionCache { ptotal = FovInvalid , perActor = EM.delete aid perActor } modifyServer $ \ser -> ser { sperCacheFid = EM.adjust (EM.adjust f lid) fid $ sperCacheFid ser , sperValidFid = EM.adjust (EM.insert lid False) fid $ sperValidFid ser } invalidatePerActor :: MonadServer m => ActorId -> m () invalidatePerActor aid = do actorMaxSk <- getsState $ getActorMaxSkills aid unless (Ability.getSk Ability.SkSight actorMaxSk <= 0 && Ability.getSk Ability.SkNocto actorMaxSk <= 0 && Ability.getSk Ability.SkSmell actorMaxSk <= 0) $ do b <- getsState $ getActorBody aid addPerActorAny aid b reconsiderPerActor :: MonadServer m => ActorId -> m () reconsiderPerActor aid = do b <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid if Ability.getSk Ability.SkSight actorMaxSk <= 0 && Ability.getSk Ability.SkNocto actorMaxSk <= 0 && Ability.getSk Ability.SkSmell actorMaxSk <= 0 then do perCacheFid <- getsServer sperCacheFid when (EM.member aid $ perActor ((perCacheFid EM.! bfid b) EM.! blid b)) $ deletePerActorAny aid b else addPerActorAny aid b invalidatePerLid :: MonadServer m => LevelId -> m () invalidatePerLid lid = do let f pc@PerceptionCache{perActor} | EM.null perActor = pc | otherwise = PerceptionCache { ptotal = FovInvalid , perActor = EM.map (const FovInvalid) perActor } modifyServer $ \ser -> let perCacheFidNew = EM.map (EM.adjust f lid) $ sperCacheFid ser g fid valid | ptotal ((perCacheFidNew EM.! fid) EM.! lid) == FovInvalid = EM.insert lid False valid g _ valid = valid in ser { sperCacheFid = perCacheFidNew , sperValidFid = EM.mapWithKey g $ sperValidFid ser } invalidatePerFidLid :: MonadServer m => FactionId -> LevelId -> m () invalidatePerFidLid fid lid = do let adj = EM.insert lid False modifyServer $ \ser -> ser {sperValidFid = EM.adjust adj fid $ sperValidFid ser} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/HandleEffectM.hs0000644000000000000000000032256607346545000023206 0ustar0000000000000000{-# LANGUAGE TupleSections #-} -- | Handle effects. They are most often caused by requests sent by clients -- but sometimes also caused by projectiles or periodically activated items. module Game.LambdaHack.Server.HandleEffectM ( UseResult(..), EffToUse(..), EffApplyFlags(..) , applyItem, cutCalm, kineticEffectAndDestroy, effectAndDestroyAndAddKill , itemEffectEmbedded, highestImpression, dominateFidSfx , dropAllEquippedItems, pickDroppable, consumeItems, dropCStoreItem #ifdef EXPOSE_INTERNAL -- * Internal operations , applyKineticDamage, refillHP, effectAndDestroy, imperishableKit , itemEffectDisco, effectSem , effectBurn, effectExplode, effectRefillHP, effectRefillCalm , effectDominate, dominateFid, effectImpress, effectPutToSleep, effectYell , effectSummon, effectAscend, findStairExit, switchLevels1, switchLevels2 , effectEscape, effectParalyze, paralyze, effectParalyzeInWater , effectInsertMove, effectTeleport, effectCreateItem , effectDestroyItem, effectDropItem, effectConsumeItems , effectRecharge, effectPolyItem, effectRerollItem, effectDupItem , effectIdentify, identifyIid, effectDetect, effectDetectX, effectSendFlying , sendFlyingVector, effectApplyPerfume, effectAtMostOneOf, effectOneOf , effectAndEffect, effectAndEffectSem, effectOrEffect, effectSeqEffect , effectWhen, effectUnless, effectIfThenElse , effectVerbNoLonger, effectVerbMsg, effectVerbMsgFail #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Bits (xor) import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.HashMap.Strict as HM import Data.Int (Int64) import Data.Key (mapWithKeyM_) import qualified Data.Text as T import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Random import Game.LambdaHack.Definition.Ability (ActivationFlag (..)) import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.CommonM import Game.LambdaHack.Server.ItemM import Game.LambdaHack.Server.ItemRev import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.PeriodicM import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State -- * Semantics of effects data UseResult = UseDud | UseId | UseUp deriving (Eq, Ord) data EffToUse = EffBare | EffBareAndOnCombine | EffOnCombine deriving Eq data EffApplyFlags = EffApplyFlags { effToUse :: EffToUse , effVoluntary :: Bool , effUseAllCopies :: Bool , effKineticPerformed :: Bool , effActivation :: Ability.ActivationFlag , effMayDestroy :: Bool } applyItem :: MonadServerAtomic m => ActorId -> ItemId -> CStore -> m () applyItem aid iid cstore = do execSfxAtomic $ SfxApply aid iid let c = CActor aid cstore -- Treated as if the actor hit himself with the item as a weapon, -- incurring both the kinetic damage and effect, hence the same call -- as in @reqMelee@. let effApplyFlags = EffApplyFlags { effToUse = EffBareAndOnCombine , effVoluntary = True , effUseAllCopies = False , effKineticPerformed = False , effActivation = ActivationTrigger , effMayDestroy = True } void $ kineticEffectAndDestroy effApplyFlags aid aid aid iid c applyKineticDamage :: MonadServerAtomic m => ActorId -> ActorId -> ItemId -> m Bool applyKineticDamage source target iid = do itemKind <- getsState $ getIidKindServer iid if IK.idamage itemKind == 0 then return False else do -- speedup sb <- getsState $ getActorBody source hurtMult <- getsState $ armorHurtBonus source target totalDepth <- getsState stotalDepth Level{ldepth} <- getLevel (blid sb) dmg <- rndToAction $ castDice ldepth totalDepth $ IK.idamage itemKind let rawDeltaHP = into @Int64 hurtMult * xM dmg `divUp` 100 speedDeltaHP = case btrajectory sb of Just (_, speed) | bproj sb -> - modifyDamageBySpeed rawDeltaHP speed _ -> - rawDeltaHP if speedDeltaHP < 0 then do -- damage the target, never heal refillHP source target speedDeltaHP return True else return False refillHP :: MonadServerAtomic m => ActorId -> ActorId -> Int64 -> m () refillHP source target speedDeltaHP = assert (speedDeltaHP /= 0) $ do tbOld <- getsState $ getActorBody target actorMaxSk <- getsState $ getActorMaxSkills target -- We don't ignore even tiny HP drains, because they can be very weak -- enemy projectiles and so will recur and in total can be deadly -- and also AI should rather be stupidly aggressive than stupidly lethargic. let serious = source /= target && not (bproj tbOld) hpMax = Ability.getSk Ability.SkMaxHP actorMaxSk deltaHP0 | serious && speedDeltaHP < minusM = -- If overfull, at least cut back to max, unless minor drain. min speedDeltaHP (xM hpMax - bhp tbOld) | otherwise = speedDeltaHP deltaHP = if | deltaHP0 > 0 && bhp tbOld > xM 999 -> -- UI limit tenthM -- avoid nop, to avoid loops | deltaHP0 < 0 && bhp tbOld < - xM 999 -> -tenthM | otherwise -> deltaHP0 execUpdAtomic $ UpdRefillHP target deltaHP when serious $ cutCalm target tb <- getsState $ getActorBody target fact <- getsState $ (EM.! bfid tb) . sfactionD when (not (bproj tb) && fhasPointman (gkind fact)) $ -- If leader just lost all HP, change the leader early (not when destroying -- the actor), to let players rescue him, especially if he's slowed -- by the attackers. when (bhp tb <= 0 && bhp tbOld > 0) $ do -- If all other party members dying, leadership will switch -- to one of them, which seems questionable, but it's rare -- and the disruption servers to underline the dire circumstance. electLeader (bfid tb) (blid tb) target mleader <- getsState $ gleader . (EM.! bfid tb) . sfactionD -- If really nobody else in the party, make him the leader back again -- on the oft chance that he gets revived by a projectile, etc. when (isNothing mleader) $ execUpdAtomic $ UpdLeadFaction (bfid tb) Nothing $ Just target cutCalm :: MonadServerAtomic m => ActorId -> m () cutCalm target = do tb <- getsState $ getActorBody target actorMaxSk <- getsState $ getActorMaxSkills target let upperBound = if hpTooLow tb actorMaxSk then 2 -- to trigger domination on next attack, etc. else xM $ Ability.getSk Ability.SkMaxCalm actorMaxSk deltaCalm = min minusM2 (upperBound - bcalm tb) -- HP loss decreases Calm by at least @minusM2@ to avoid "hears something", -- which is emitted when decreasing Calm by @minusM1@. updateCalm target deltaCalm -- Here kinetic damage is applied. This is necessary so that the same -- AI benefit calculation may be used for flinging and for applying items. kineticEffectAndDestroy :: MonadServerAtomic m => EffApplyFlags -> ActorId -> ActorId -> ActorId -> ItemId -> Container -> m UseResult kineticEffectAndDestroy effApplyFlags0@EffApplyFlags{..} killer source target iid c = do bag <- getsState $ getContainerBag c case iid `EM.lookup` bag of Nothing -> error $ "" `showFailure` (source, target, iid, c) Just kit -> do itemFull <- getsState $ itemToFull iid tbOld <- getsState $ getActorBody target localTime <- getsState $ getLocalTime (blid tbOld) let recharged = hasCharge localTime kit -- If neither kinetic hit nor any effect is activated, there's no chance -- the items can be destroyed or even timeout changes, so we abort early. if not recharged then return UseDud else do effKineticPerformed2 <- applyKineticDamage source target iid tb <- getsState $ getActorBody target -- Sometimes victim heals just after we registered it as killed, -- but that's OK, an actor killed two times is similar enough -- to two killed. when (effKineticPerformed2 -- speedup && bhp tb <= 0 && bhp tbOld > 0) $ do sb <- getsState $ getActorBody source arWeapon <- getsState $ (EM.! iid) . sdiscoAspect let killHow | not (bproj sb) = if effVoluntary then KillKineticMelee else KillKineticPush | IA.checkFlag Ability.Blast arWeapon = KillKineticBlast | otherwise = KillKineticRanged addKillToAnalytics killer killHow (bfid tbOld) (btrunk tbOld) let effApplyFlags = effApplyFlags0 { effUseAllCopies = fst kit <= 1 , effKineticPerformed = effKineticPerformed2 } effectAndDestroyAndAddKill effApplyFlags killer source target iid c itemFull effectAndDestroyAndAddKill :: MonadServerAtomic m => EffApplyFlags -> ActorId -> ActorId -> ActorId -> ItemId -> Container -> ItemFull -> m UseResult effectAndDestroyAndAddKill effApplyFlags0@EffApplyFlags{..} killer source target iid c itemFull = do tbOld <- getsState $ getActorBody target triggered <- effectAndDestroy effApplyFlags0 source target iid c itemFull tb <- getsState $ getActorBody target -- Sometimes victim heals just after we registered it as killed, -- but that's OK, an actor killed two times is similar enough to two killed. when (bhp tb <= 0 && bhp tbOld > 0) $ do sb <- getsState $ getActorBody source arWeapon <- getsState $ (EM.! iid) . sdiscoAspect let killHow | not (bproj sb) = if effVoluntary then KillOtherMelee else KillOtherPush | IA.checkFlag Ability.Blast arWeapon = KillOtherBlast | otherwise = KillOtherRanged addKillToAnalytics killer killHow (bfid tbOld) (btrunk tbOld) return triggered effectAndDestroy :: MonadServerAtomic m => EffApplyFlags -> ActorId -> ActorId -> ItemId -> Container -> ItemFull -> m UseResult effectAndDestroy effApplyFlags0@EffApplyFlags{..} source target iid container itemFull@ItemFull{itemDisco, itemKindId, itemKind} = do bag <- getsState $ getContainerBag container let (itemK, itemTimers) = bag EM.! iid effs = case effToUse of EffBare -> if effActivation == ActivationOnSmash then IK.strengthOnSmash itemKind else IK.ieffects itemKind EffBareAndOnCombine -> IK.ieffects itemKind ++ IK.strengthOnCombine itemKind EffOnCombine -> IK.strengthOnCombine itemKind arItem = case itemDisco of ItemDiscoFull itemAspect -> itemAspect _ -> error "effectAndDestroy: server ignorant about an item" timeout = IA.aTimeout arItem lid <- getsState $ lidFromC container localTime <- getsState $ getLocalTime lid let it1 = filter (charging localTime) itemTimers len = length it1 recharged = len < itemK || effActivation `elem` [ActivationOnSmash, ActivationConsume] -- If the item has no charges and the special cases don't apply -- we speed up by shortcutting early, because we don't need to activate -- effects and we know kinetic hit was not performed (no charges to do so -- and in case of @OnSmash@ and @ActivationConsume@, -- only effects are triggered). if not recharged then return UseDud else do let timeoutTurns = timeDeltaScale (Delta timeTurn) timeout newItemTimer = createItemTimer localTime timeoutTurns it2 = if timeout > 0 && recharged then if effActivation == ActivationPeriodic && IA.checkFlag Ability.Fragile arItem then replicate (itemK - length it1) newItemTimer ++ it1 -- copies are spares only; one fires, all discharge else take (itemK - length it1) [newItemTimer] ++ it1 -- copies all fire, turn by turn; <= 1 discharges else itemTimers kit2 = (1, take 1 it2) !_A = assert (len <= itemK `blame` (source, target, iid, container)) () -- We use up the charge even if eventualy every effect fizzles. Tough luck. -- At least we don't destroy the item in such case. -- Also, we ID it regardless. unless (itemTimers == it2) $ execUpdAtomic $ UpdTimeItem iid container itemTimers it2 -- We have to destroy the item before the effect affects the item -- or affects the actor holding it or standing on it (later on we could -- lose track of the item and wouldn't be able to destroy it) . -- This is OK, because we don't remove the item type from various -- item dictionaries, just an individual copy from the container, -- so, e.g., the item can be identified after it's removed. let imperishable = not effMayDestroy || imperishableKit effActivation itemFull unless imperishable $ execUpdAtomic $ UpdLoseItem False iid kit2 container -- At this point, the item is potentially no longer in container -- @container@, therefore beware of assuming so in the code below. triggeredEffect <- itemEffectDisco effApplyFlags0 source target iid itemKindId itemKind container effs sb <- getsState $ getActorBody source let triggered = if effKineticPerformed then UseUp else triggeredEffect mEmbedPos = case container of CEmbed _ p -> Just p _ -> Nothing if | triggered == UseUp && mEmbedPos /= Just (bpos sb) -- treading water, etc. && effActivation `notElem` [ActivationTrigger, ActivationMeleeable] -- do not repeat almost the same msg && (effActivation /= ActivationOnSmash -- only tells condition ends && effActivation /= ActivationPeriodic || not (IA.checkFlag Ability.Condition arItem)) -> do -- Effects triggered; main feedback comes from them, -- but send info so that clients can log it. let verbose = effActivation == ActivationUnderRanged || effActivation == ActivationUnderMelee execSfxAtomic $ SfxItemApplied verbose iid container | triggered /= UseUp && effActivation /= ActivationOnSmash && effActivation /= ActivationPeriodic -- periodic effects repeat and so spam && effActivation `notElem` [ActivationUnderRanged, ActivationUnderMelee] -- and so do effects under attack && not (bproj sb) -- projectiles can be very numerous && isNothing mEmbedPos -> -- embeds may be just flavour -- Announce no effect, which is rare and wastes time, so noteworthy. execSfxAtomic $ SfxMsgFid (bfid sb) $ if any IK.forApplyEffect effs then SfxFizzles iid container -- something didn't work despite promising effects else SfxNothingHappens iid container -- fully expected | otherwise -> return () -- all the spam cases -- If none of item's effects nor a kinetic hit were performed, -- we recreate the item (assuming we deleted the item above). -- Regardless, we don't rewind the time, because some info is gained -- (that the item does not exhibit any effects in the given context). unless (imperishable || triggered == UseUp) $ execUpdAtomic $ UpdSpotItem False iid kit2 container return triggered imperishableKit :: ActivationFlag -> ItemFull -> Bool imperishableKit effActivation itemFull = let arItem = aspectRecordFull itemFull in IA.checkFlag Ability.Durable arItem || effActivation == ActivationPeriodic && not (IA.checkFlag Ability.Fragile arItem) -- The item is triggered exactly once. If there are more copies, -- they are left to be triggered next time. -- If the embed no longer exists at the given position, effect fizzles. itemEffectEmbedded :: MonadServerAtomic m => EffToUse -> Bool -> ActorId -> LevelId -> Point -> ItemId -> m UseResult itemEffectEmbedded effToUse effVoluntary aid lid tpos iid = do embeds2 <- getsState $ getEmbedBag lid tpos -- might have changed due to other embedded items invocations if iid `EM.notMember` embeds2 then return UseDud else do -- First embedded item may move actor to another level, so @lid@ -- may be unequal to @blid sb@. let c = CEmbed lid tpos -- Treated as if the actor hit himself with the embedded item as a weapon, -- incurring both the kinetic damage and effect, hence the same call -- as in @reqMelee@. Information whether this happened due to being pushed -- is preserved, but how did the pushing is lost, so we blame the victim. let effApplyFlags = EffApplyFlags { effToUse , effVoluntary , effUseAllCopies = False , effKineticPerformed = False , effActivation = if effToUse == EffOnCombine then ActivationOnCombine else ActivationEmbed , effMayDestroy = True } kineticEffectAndDestroy effApplyFlags aid aid aid iid c -- | The source actor affects the target actor, with a given item. -- If any of the effects fires up, the item gets identified. -- Even using raw damage (beating the enemy with the magic wand, -- for example) identifies the item. This means a costly @UpdDiscover@ -- is processed for each random timeout weapon hit and for most projectiles, -- but at least not for most explosion particles nor plain organs. -- And if not needed, the @UpdDiscover@ are eventually not sent to clients. -- So, enemy missiles that hit us are no longer mysterious until picked up, -- which is for the better, because the client knows their charging status -- and so can generate accurate messages in the case when not recharged. -- This also means that thrown consumables in flasks sturdy enough to cause -- damage are always identified at hit, even if no effect activated. -- So throwing them at foes is a better identification method than applying. -- -- Note that if we activate a durable non-passive item, e.g., a spiked shield, -- from the ground, it will get identified, which is perfectly fine, -- until we want to add sticky armor that can't be easily taken off -- (and, e.g., has some maluses). itemEffectDisco :: MonadServerAtomic m => EffApplyFlags -> ActorId -> ActorId -> ItemId -> ContentId ItemKind -> ItemKind -> Container -> [IK.Effect] -> m UseResult itemEffectDisco effApplyFlags0@EffApplyFlags{..} source target iid itemKindId itemKind c effs = do urs <- mapM (effectSem effApplyFlags0 source target iid c) effs let ur = case urs of [] -> UseDud -- there was no effects _ -> maximum urs -- Note: @UseId@ suffices for identification, @UseUp@ is not necessary. when (ur >= UseId || effKineticPerformed) $ identifyIid iid c itemKindId itemKind return ur -- | Source actor affects target actor, with a given effect and it strength. -- Both actors are on the current level and can be the same actor. -- The item may or may not still be in the container. effectSem :: MonadServerAtomic m => EffApplyFlags -> ActorId -> ActorId -> ItemId -> Container -> IK.Effect -> m UseResult effectSem effApplyFlags0@EffApplyFlags{..} source target iid c effect = do let recursiveCall = effectSem effApplyFlags0 source target iid c sb <- getsState $ getActorBody source -- @execSfx@ usually comes last in effect semantics, but not always -- and we are likely to introduce more variety. let execSfx = execSfxAtomic $ SfxEffect (bfid sb) target iid effect 0 execSfxSource = execSfxAtomic $ SfxEffect (bfid sb) source iid effect 0 case effect of IK.Burn nDm -> effectBurn nDm source target iid IK.Explode t -> effectExplode execSfx t source target c IK.RefillHP p -> effectRefillHP p source target iid IK.RefillCalm p -> effectRefillCalm execSfx p source target IK.Dominate -> effectDominate source target iid IK.Impress -> effectImpress recursiveCall execSfx source target IK.PutToSleep -> effectPutToSleep execSfx target IK.Yell -> effectYell execSfx target IK.Summon grp nDm -> effectSummon grp nDm iid source target effActivation IK.Ascend p -> effectAscend recursiveCall execSfx p source target c IK.Escape{} -> effectEscape execSfx source target IK.Paralyze nDm -> effectParalyze execSfx nDm source target IK.ParalyzeInWater nDm -> effectParalyzeInWater execSfx nDm source target IK.InsertMove nDm -> effectInsertMove execSfx nDm source target IK.Teleport nDm -> effectTeleport execSfx nDm source target IK.CreateItem mcount store grp tim -> effectCreateItem (Just $ bfid sb) mcount source target (Just iid) store grp tim IK.DestroyItem n k store grp -> effectDestroyItem execSfx n k store target grp IK.ConsumeItems tools raw -> effectConsumeItems execSfx iid target tools raw IK.DropItem n k store grp -> effectDropItem execSfx iid n k store grp target IK.Recharge n dice -> effectRecharge True execSfx iid n dice target IK.Discharge n dice -> effectRecharge False execSfx iid n dice target IK.PolyItem -> effectPolyItem execSfx iid target IK.RerollItem -> effectRerollItem execSfx iid target IK.DupItem -> effectDupItem execSfx iid target IK.Identify -> effectIdentify execSfx iid target IK.Detect d radius -> effectDetect execSfx d radius target c IK.SendFlying tmod -> effectSendFlying execSfx tmod source target c Nothing IK.PushActor tmod -> effectSendFlying execSfx tmod source target c (Just True) IK.PullActor tmod -> effectSendFlying execSfx tmod source target c (Just False) IK.ApplyPerfume -> effectApplyPerfume execSfx target IK.AtMostOneOf l -> effectAtMostOneOf recursiveCall l IK.OneOf l -> effectOneOf recursiveCall l IK.OnSmash _ -> return UseDud -- ignored under normal circumstances IK.OnCombine _ -> return UseDud -- ignored under normal circumstances IK.OnUser eff -> effectSem effApplyFlags0 source source iid c eff IK.NopEffect -> return UseDud -- all there is IK.AndEffect eff1 eff2 -> effectAndEffect recursiveCall source eff1 eff2 IK.OrEffect eff1 eff2 -> effectOrEffect recursiveCall (bfid sb) eff1 eff2 IK.SeqEffect effs -> effectSeqEffect recursiveCall effs IK.When cond eff -> effectWhen recursiveCall source cond eff effActivation IK.Unless cond eff -> effectUnless recursiveCall source cond eff effActivation IK.IfThenElse cond eff1 eff2 -> effectIfThenElse recursiveCall source cond eff1 eff2 effActivation IK.VerbNoLonger{} -> effectVerbNoLonger effUseAllCopies execSfxSource source IK.VerbMsg{} -> effectVerbMsg execSfxSource source IK.VerbMsgFail{} -> effectVerbMsgFail execSfxSource source conditionSem :: MonadServer m => ActorId -> IK.Condition -> ActivationFlag -> m Bool conditionSem source cond effActivation = do sb <- getsState $ getActorBody source return $! case cond of IK.HpLeq n -> bhp sb <= xM n IK.HpGeq n -> bhp sb >= xM n IK.CalmLeq n -> bcalm sb <= xM n IK.CalmGeq n -> bcalm sb >= xM n IK.TriggeredBy activationFlag -> activationFlag == effActivation -- * Individual semantic functions for effects -- ** Burn -- Damage from fire. Not affected by armor. effectBurn :: MonadServerAtomic m => Dice.Dice -> ActorId -> ActorId -> ItemId -> m UseResult effectBurn nDm source target iid = do tb <- getsState $ getActorBody target totalDepth <- getsState stotalDepth Level{ldepth} <- getLevel (blid tb) n0 <- rndToAction $ castDice ldepth totalDepth nDm let n = max 1 n0 -- avoid 0 and negative burn; validated in content anyway deltaHP = - xM n sb <- getsState $ getActorBody source -- Display the effect more accurately. let reportedEffect = IK.Burn $ Dice.intToDice n execSfxAtomic $ SfxEffect (bfid sb) target iid reportedEffect deltaHP refillHP source target deltaHP return UseUp -- ** Explode effectExplode :: MonadServerAtomic m => m () -> GroupName ItemKind -> ActorId -> ActorId -> Container -> m UseResult effectExplode execSfx cgroup source target containerOrigin = do execSfx tb <- getsState $ getActorBody target oxy@(Point x y) <- getsState $ posFromC containerOrigin let itemFreq = [(cgroup, 1)] -- Explosion particles are placed among organs of the victim. -- TODO: when changing this code, perhaps use @containerOrigin@ -- in place of @container@, but then remove @borgan@ from several -- functions that have the store hardwired. container = CActor target COrgan -- Power depth of new items unaffected by number of spawned actors. Level{ldepth} <- getLevel $ blid tb freq <- prepareItemKind 0 ldepth itemFreq m2 <- rollAndRegisterItem False ldepth freq container Nothing acounter <- getsServer $ fromEnum . sacounter let (iid, (ItemFull{itemKind}, (itemK, _))) = fromMaybe (error $ "" `showFailure` cgroup) m2 semiRandom = T.length (IK.idesc itemKind) -- We pick a point at the border, not inside, to have a uniform -- distribution for the points the line goes through at each distance -- from the source. Otherwise, e.g., the points on cardinal -- and diagonal lines from the source would be more common. projectN k10 n = do -- Shape is deterministic for the explosion kind, except that is has -- two variants chosen according to time-dependent @veryRandom@. -- Choice from the variants prevents diagonal or cardinal directions -- being always safe for a given explosion kind. let shapeRandom = k10 `xor` (semiRandom + n) veryRandom = shapeRandom + acounter + acounter `div` 3 fuzz = 5 + shapeRandom `mod` 5 k | n < 16 && n >= 12 = 12 | n < 12 && n >= 8 = 8 | n < 8 && n >= 4 = 4 | otherwise = min n 16 -- fire in groups of 16 including old duds psDir4 = [ Point (x - 12) (y + 12) , Point (x + 12) (y + 12) , Point (x - 12) (y - 12) , Point (x + 12) (y - 12) ] psDir8 = [ Point (x - 12) y , Point (x + 12) y , Point x (y + 12) , Point x (y - 12) ] psFuzz = [ Point (x - 12) $ y + fuzz , Point (x + 12) $ y + fuzz , Point (x - 12) $ y - fuzz , Point (x + 12) $ y - fuzz , flip Point (y - 12) $ x + fuzz , flip Point (y + 12) $ x + fuzz , flip Point (y - 12) $ x - fuzz , flip Point (y + 12) $ x - fuzz ] randomReverse = if even veryRandom then id else reverse ps = take k $ concat $ randomReverse [ zip (repeat True) -- diagonal particles don't reach that far $ take 4 (drop ((k10 + itemK + fuzz) `mod` 4) $ cycle psDir4) , zip (repeat False) -- only some cardinal reach far $ take 4 (drop ((k10 + n) `mod` 4) $ cycle psDir8) ] ++ [zip (repeat True) $ take 8 (drop ((k10 + fuzz) `mod` 8) $ cycle psFuzz)] forM_ ps $ \(centerRaw, tpxy) -> do let center = centerRaw && itemK >= 8 -- if few, keep them regular mfail <- projectFail source target oxy tpxy shapeRandom center iid COrgan True case mfail of Nothing -> return () Just ProjectBlockTerrain -> return () Just ProjectBlockActor -> return () Just failMsg -> execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxUnexpected failMsg tryFlying 0 = return () tryFlying k10 = do -- Explosion particles were placed among organs of the victim: bag2 <- getsState $ borgan . getActorBody target -- We stop bouncing old particles when less than two thirds remain, -- to prevent hoarding explosives to use only in cramped spaces. case EM.lookup iid bag2 of Just (n2, _) | n2 * 2 >= itemK `div` 3 -> do projectN k10 n2 tryFlying $ k10 - 1 _ -> return () -- Some of the particles that fail to take off, bounce off obstacles -- up to 10 times in total, trying to fly in different directions. tryFlying 10 bag3 <- getsState $ borgan . getActorBody target let mn3 = EM.lookup iid bag3 -- Give up and destroy the remaining particles, if any. maybe (return ()) (\kit -> execUpdAtomic $ UpdLoseItem False iid kit container) mn3 return UseUp -- we neglect verifying that at least one projectile got off -- ** RefillHP -- Unaffected by armor. effectRefillHP :: MonadServerAtomic m => Int -> ActorId -> ActorId -> ItemId -> m UseResult effectRefillHP power0 source target iid = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target curChalSer <- getsServer $ scurChalSer . soptions fact <- getsState $ (EM.! bfid tb) . sfactionD let power = if power0 <= -1 then power0 else max 1 power0 -- avoid 0 deltaHP = xM power if cfish curChalSer && deltaHP > 0 && fhasUI (gkind fact) && bfid sb /= bfid tb then do execSfxAtomic $ SfxMsgFid (bfid tb) SfxColdFish return UseId else do let reportedEffect = IK.RefillHP power execSfxAtomic $ SfxEffect (bfid sb) target iid reportedEffect deltaHP refillHP source target deltaHP return UseUp -- ** RefillCalm effectRefillCalm :: MonadServerAtomic m => m () -> Int -> ActorId -> ActorId -> m UseResult effectRefillCalm execSfx power0 source target = do tb <- getsState $ getActorBody target actorMaxSk <- getsState $ getActorMaxSkills target let power = if power0 <= -1 then power0 else max 1 power0 -- avoid 0 rawDeltaCalm = xM power calmMax = Ability.getSk Ability.SkMaxCalm actorMaxSk serious = rawDeltaCalm <= minusM2 && source /= target && not (bproj tb) deltaCalm0 | serious = -- if overfull, at least cut back to max min rawDeltaCalm (xM calmMax - bcalm tb) | otherwise = rawDeltaCalm deltaCalm = if | deltaCalm0 > 0 && bcalm tb > xM 999 -> -- UI limit tenthM -- avoid nop, to avoid loops | deltaCalm0 < 0 && bcalm tb < - xM 999 -> -tenthM | otherwise -> deltaCalm0 execSfx updateCalm target deltaCalm return UseUp -- ** Dominate -- The is another way to trigger domination (the normal way is by zeroed Calm). -- Calm is here irrelevant. The other conditions are the same. effectDominate :: MonadServerAtomic m => ActorId -> ActorId -> ItemId -> m UseResult effectDominate source target iid = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target if | bproj tb -> return UseDud | bfid tb == bfid sb -> return UseDud -- accidental hit; ignore | otherwise -> do fact <- getsState $ (EM.! bfid tb) . sfactionD hiImpression <- highestImpression tb let permitted = case hiImpression of Nothing -> False -- no impression, no domination Just (hiImpressionFid, hiImpressionK) -> hiImpressionFid == bfid sb -- highest impression needs to be by us && (fhasPointman (gkind fact) || hiImpressionK >= 10) -- to tame/hack animal/robot, impress them a lot first if permitted then do b <- dominateFidSfx source target iid (bfid sb) return $! if b then UseUp else UseDud else do execSfxAtomic $ SfxMsgFid (bfid sb) $ SfxUnimpressed target when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxUnimpressed target return UseDud highestImpression :: MonadServerAtomic m => Actor -> m (Maybe (FactionId, Int)) highestImpression tb = do getKind <- getsState $ flip getIidKindServer getItem <- getsState $ flip getItemBody let isImpression iid = maybe False (> 0) $ lookup IK.S_IMPRESSED $ IK.ifreq $ getKind iid impressions = EM.filterWithKey (\iid _ -> isImpression iid) $ borgan tb f (_, (k, _)) = k maxImpression = maximumBy (comparing f) $ EM.assocs impressions if EM.null impressions then return Nothing else case jfid $ getItem $ fst maxImpression of Nothing -> return Nothing Just fid -> assert (fid /= bfid tb) $ return $ Just (fid, fst $ snd maxImpression) dominateFidSfx :: MonadServerAtomic m => ActorId -> ActorId -> ItemId -> FactionId -> m Bool dominateFidSfx source target iid fid = do tb <- getsState $ getActorBody target let !_A = assert (not $ bproj tb) () -- Actors that don't move freely can't be dominated, for otherwise, -- when they are the last survivors, they could get stuck and the game -- wouldn't end. Also, they are a hassle to guide through the dungeon. canTra <- getsState $ canTraverse target -- Being pushed protects from domination, for simplicity. -- A possible interesting exploit, but much help from content would be needed -- to make it practical. if isNothing (btrajectory tb) && canTra && bhp tb > 0 then do let execSfx = execSfxAtomic $ SfxEffect fid target iid IK.Dominate 0 execSfx -- if actor ours, possibly the last occasion to see him dominateFid fid source target -- If domination resulted in game over, the message won't be seen -- before the end game screens, but at least it will be seen afterwards -- and browsable in history while inside subsequent game, revealing -- the cause of the previous game over. Better than no message at all. execSfx -- see the actor as theirs, unless position not visible return True else return False dominateFid :: MonadServerAtomic m => FactionId -> ActorId -> ActorId -> m () dominateFid fid source target = do tb0 <- getsState $ getActorBody target -- Game over deduced very early, so no further animation nor message -- will appear before game end screens. This is good in that our last actor -- that yielded will still be on screen when end game messages roll. -- This is bad in that last enemy actor that got dominated by us -- may not be on screen and we have no clue how we won until -- we see history in the next game. Even worse if our ally dominated -- the enemy actor. Then we may never learn. Oh well, that's realism. deduceKilled target electLeader (bfid tb0) (blid tb0) target -- Drop all items so that domiation is not too nasty, especially -- if the dominated hero runs off or teleports away with gold -- or starts hitting with the most potent artifact weapon in the game. -- Drop items while still of the original faction -- to mark them on the map for other party members to collect. dropAllEquippedItems target tb0 tb <- getsState $ getActorBody target actorMaxSk <- getsState $ getActorMaxSkills target getKind <- getsState $ flip getIidKindServer let isImpression iid = maybe False (> 0) $ lookup IK.S_IMPRESSED $ IK.ifreq $ getKind iid dropAllImpressions = EM.filterWithKey (\iid _ -> not $ isImpression iid) borganNoImpression = dropAllImpressions $ borgan tb -- Actor is not pushed nor projectile, so @sactorTime@ suffices. btime <- getsServer $ fromJust . lookupActorTime (bfid tb) (blid tb) target . sactorTime execUpdAtomic $ UpdLoseActor target tb let maxCalm = Ability.getSk Ability.SkMaxCalm actorMaxSk maxHp = Ability.getSk Ability.SkMaxHP actorMaxSk bNew = tb { bfid = fid , bcalm = max (xM 10) $ xM maxCalm `div` 2 , bhp = min (xM maxHp) $ bhp tb + xM 10 , borgan = borganNoImpression} modifyServer $ \ser -> ser {sactorTime = updateActorTime fid (blid tb) target btime $ sactorTime ser} execUpdAtomic $ UpdSpotActor target bNew -- Focus on the dominated actor, by making him a leader. setFreshLeader fid target factionD <- getsState sfactionD let inGame fact2 = case gquit fact2 of Nothing -> True Just Status{stOutcome=Camping} -> True _ -> False gameOver = not $ any inGame $ EM.elems factionD -- Avoid the spam of identifying items, if game over. unless gameOver $ do -- Add some nostalgia for the old faction. void $ effectCreateItem (Just $ bfid tb) (Just 10) source target Nothing COrgan IK.S_IMPRESSED IK.timerNone -- Identify organs that won't get identified by use. getKindId <- getsState $ flip getIidKindIdServer let discoverIf (iid, cstore) = do let itemKindId = getKindId iid c = CActor target cstore assert (cstore /= CGround) $ discoverIfMinorEffects c iid itemKindId aic = (btrunk tb, COrgan) : filter ((/= btrunk tb) . fst) (getCarriedIidCStore tb) mapM_ discoverIf aic -- | Drop all actor's equipped items. dropAllEquippedItems :: MonadServerAtomic m => ActorId -> Actor -> m () dropAllEquippedItems aid b = mapActorCStore_ CEqp (void <$$> dropCStoreItem False False CEqp aid b maxBound) b -- ** Impress effectImpress :: MonadServerAtomic m => (IK.Effect -> m UseResult) -> m () -> ActorId -> ActorId -> m UseResult effectImpress recursiveCall execSfx source target = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target if | bproj tb -> return UseDud | bfid tb == bfid sb -> -- Unimpress wrt others, but only once. The recursive Sfx suffices. recursiveCall $ IK.DropItem 1 1 COrgan IK.S_IMPRESSED | otherwise -> do -- Actors that don't move freely and so are stupid, can't be impressed. canTra <- getsState $ canTraverse target if canTra then do unless (bhp tb <= 0) execSfx -- avoid spam just before death effectCreateItem (Just $ bfid sb) (Just 1) source target Nothing COrgan IK.S_IMPRESSED IK.timerNone else return UseDud -- no message, because common and not crucial -- ** PutToSleep effectPutToSleep :: MonadServerAtomic m => m () -> ActorId -> m UseResult effectPutToSleep execSfx target = do tb <- getsState $ getActorBody target if | bproj tb -> return UseDud | bwatch tb `elem` [WSleep, WWake] -> return UseDud -- can't increase sleep | otherwise -> do actorMaxSk <- getsState $ getActorMaxSkills target if not $ canSleep actorMaxSk then return UseId -- no message about the cause, so at least ID else do let maxCalm = xM $ Ability.getSk Ability.SkMaxCalm actorMaxSk deltaCalm = maxCalm - bcalm tb when (deltaCalm > 0) $ updateCalm target deltaCalm -- max Calm, but asleep vulnerability execSfx case bwatch tb of WWait n | n > 0 -> do nAll <- removeConditionSingle IK.S_BRACED target let !_A = assert (nAll == 0) () return () _ -> return () -- Forced sleep. No check if the actor can sleep naturally. addSleep target return UseUp -- ** Yell -- This is similar to 'reqYell', but also mentions that the actor is startled, -- because, presumably, he yells involuntarily. It doesn't wake him up -- via Calm instantly, just like yelling in a dream not always does. effectYell :: MonadServerAtomic m => m () -> ActorId -> m UseResult effectYell execSfx target = do tb <- getsState $ getActorBody target if bhp tb <= 0 then -- avoid yelling corpses return UseDud -- the yell never manifested else do unless (bproj tb) execSfx execSfxAtomic $ SfxTaunt False target when (not (bproj tb) && deltaBenign (bcalmDelta tb)) $ execUpdAtomic $ UpdRefillCalm target minusM return UseUp -- ** Summon -- Note that the Calm expended doesn't depend on the number of actors summoned. effectSummon :: MonadServerAtomic m => GroupName ItemKind -> Dice.Dice -> ItemId -> ActorId -> ActorId -> ActivationFlag -> m UseResult effectSummon grp nDm iid source target effActivation = do -- Obvious effect, nothing announced. sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target sMaxSk <- getsState $ getActorMaxSkills source tMaxSk <- getsState $ getActorMaxSkills target totalDepth <- getsState stotalDepth Level{ldepth, lbig} <- getLevel (blid tb) nFriends <- getsState $ length . friendRegularAssocs (bfid sb) (blid sb) discoAspect <- getsState sdiscoAspect power0 <- rndToAction $ castDice ldepth totalDepth nDm fact <- getsState $ (EM.! bfid sb) . sfactionD let arItem = discoAspect EM.! iid power = max power0 1 -- KISS, always at least one summon -- We put @source@ instead of @target@ and @power@ instead of dice -- to make the message more accurate. effect = IK.Summon grp $ Dice.intToDice power durable = IA.checkFlag Ability.Durable arItem warnBothActors warning = unless (bproj sb) $ do execSfxAtomic $ SfxMsgFid (bfid sb) warning when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) warning deltaCalm = - xM 30 -- Verify Calm only at periodic activations or if the item is durable. -- Otherwise summon uses up the item, which prevents summoning getting -- out of hand. I don't verify Calm otherwise, to prevent an exploit -- via draining one's calm on purpose when an item with good activation -- has a nasty summoning side-effect (the exploit still works on durables). if | bproj tb || source /= target && not (isFoe (bfid sb) fact (bfid tb)) -> return UseDud -- hitting friends or projectiles to summon is too cheap | (effActivation == ActivationPeriodic || durable) && not (bproj sb) && (bcalm sb < - deltaCalm || not (calmEnough sb sMaxSk)) -> do warnBothActors $ SfxSummonLackCalm source return UseId | nFriends >= 20 -> do -- We assume the actor tries to summon his teammates or allies. -- As he repeats such summoning, he is going to bump into this limit. -- If he summons others, see the next condition. warnBothActors $ SfxSummonTooManyOwn source return UseId | EM.size lbig >= 200 -> do -- lower than the 300 limit for spawning -- Even if the actor summons foes, he is prevented from exploiting it -- too many times and stopping natural monster spawning on the level -- (e.g., by filling the level with harmless foes). warnBothActors $ SfxSummonTooManyAll source return UseId | otherwise -> do unless (bproj sb) $ updateCalm source deltaCalm localTime <- getsState $ getLocalTime (blid tb) -- Make sure summoned actors start acting after the victim. let actorTurn = ticksPerMeter $ gearSpeed tMaxSk targetTime = timeShift localTime actorTurn afterTime = timeShift targetTime $ Delta timeClip -- Mark as summoned to prevent immediate chain summoning. -- Summon from current depth, not deeper due to many spawns already. anySummoned <- addManyActors True 0 [(grp, 1)] (blid tb) afterTime (Just $ bpos tb) power if anySummoned then do execSfxAtomic $ SfxEffect (bfid sb) source iid effect 0 return UseUp else do -- We don't display detailed warnings when @addAnyActor@ fails, -- e.g., because the actor groups can't be generated on a given level. -- However, we at least don't claim any summoning happened -- and we offer a general summoning failure messages. warnBothActors $ SfxSummonFailure source return UseId -- ** Ascend -- Note that projectiles can be teleported, too, for extra fun. effectAscend :: MonadServerAtomic m => (IK.Effect -> m UseResult) -> m () -> Bool -> ActorId -> ActorId -> Container -> m UseResult effectAscend recursiveCall execSfx up source target container = do b1 <- getsState $ getActorBody target pos <- getsState $ posFromC container let lid1 = blid b1 destinations <- getsState $ whereTo lid1 pos up . sdungeon sb <- getsState $ getActorBody source actorMaxSk <- getsState $ getActorMaxSkills target if | source /= target && Ability.getSk Ability.SkMove actorMaxSk <= 0 -> do execSfxAtomic $ SfxMsgFid (bfid sb) SfxTransImpossible when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid b1) SfxTransImpossible return UseId | actorWaits b1 && source /= target -> do execSfxAtomic $ SfxMsgFid (bfid sb) $ SfxBracedImmune target when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid b1) $ SfxBracedImmune target return UseId | null destinations -> do execSfxAtomic $ SfxMsgFid (bfid sb) SfxLevelNoMore when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid b1) SfxLevelNoMore -- We keep it useful even in shallow dungeons. recursiveCall $ IK.Teleport 30 -- powerful teleport | otherwise -> do (lid2, pos2) <- rndToAction $ oneOf destinations execSfx mbtime_bOld <- getsServer $ lookupActorTime (bfid b1) lid1 target . sactorTime mbtimeTraj_bOld <- getsServer $ lookupActorTime (bfid b1) lid1 target . strajTime pos3 <- findStairExit (bfid sb) up lid2 pos2 let switch1 = void $ switchLevels1 (target, b1) switch2 = do -- Make the initiator of the stair move the leader, -- to let him clear the stairs for others to follow. let mlead = if bproj b1 then Nothing else Just target -- Move the actor to where the inhabitants were, if any. switchLevels2 lid2 pos3 (target, b1) mbtime_bOld mbtimeTraj_bOld mlead -- The actor will be added to the new level, -- but there can be other actors at his new position. inhabitants <- getsState $ posToAidAssocs pos3 lid2 case inhabitants of (_, b2) : _ | not $ bproj b1 -> do -- Alert about the switch. execSfxAtomic $ SfxMsgFid (bfid sb) SfxLevelPushed -- Only tell one pushed player, even if many actors, because then -- they are projectiles, so not too important. when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid b2) SfxLevelPushed -- Move the actor out of the way. switch1 -- Move the inhabitants out of the way and to where the actor was. let moveInh inh = do -- Preserve the old leader, since the actor is pushed, -- so possibly has nothing worhwhile to do on the new level -- (and could try to switch back, if made a leader, -- leading to a loop). mbtime_inh <- getsServer $ lookupActorTime (bfid (snd inh)) lid2 (fst inh) . sactorTime mbtimeTraj_inh <- getsServer $ lookupActorTime (bfid (snd inh)) lid2 (fst inh) . strajTime inhMLead <- switchLevels1 inh switchLevels2 lid1 (bpos b1) inh mbtime_inh mbtimeTraj_inh inhMLead mapM_ moveInh inhabitants -- Move the actor to his destination. switch2 _ -> do -- no inhabitants or the stair-taker a projectile switch1 switch2 return UseUp findStairExit :: MonadStateRead m => FactionId -> Bool -> LevelId -> Point -> m Point findStairExit side moveUp lid pos = do COps{coTileSpeedup} <- getsState scops fact <- getsState $ (EM.! side) . sfactionD lvl <- getLevel lid let defLanding = uncurry Vector $ if moveUp then (1, 0) else (-1, 0) center = uncurry Vector $ if moveUp then (-1, 0) else (1, 0) (mvs2, mvs1) = break (== defLanding) moves mvs = center : filter (/= center) (mvs1 ++ mvs2) ps = filter (Tile.isWalkable coTileSpeedup . (lvl `at`)) $ map (shift pos) mvs posOcc :: State -> Int -> Point -> Bool posOcc s k p = case posToAidAssocs p lid s of [] -> k == 0 (_, b) : _ | bproj b -> k == 3 (_, b) : _ | isFoe side fact (bfid b) -> k == 1 -- non-proj foe _ -> k == 2 -- moving a non-projectile friend unocc <- getsState posOcc case concatMap (\k -> filter (unocc k) ps) [0..3] of [] -> error $ "" `showFailure` ps posRes : _ -> return posRes switchLevels1 :: MonadServerAtomic m => (ActorId, Actor) -> m (Maybe ActorId) switchLevels1 (aid, bOld) = do let side = bfid bOld mleader <- getsState $ gleader . (EM.! side) . sfactionD -- Prevent leader pointing to a non-existing actor. mlead <- if not (bproj bOld) && isJust mleader then do execUpdAtomic $ UpdLeadFaction side mleader Nothing return mleader -- outside of a client we don't know the real tgt of aid, hence fst else return Nothing -- Remove the actor from the old level. -- Onlookers see somebody disappear suddenly. -- @UpdDestroyActor@ is too loud, so use @UpdLoseActor@ instead. execUpdAtomic $ UpdLoseActor aid bOld return mlead switchLevels2 ::MonadServerAtomic m => LevelId -> Point -> (ActorId, Actor) -> Maybe Time -> Maybe Time -> Maybe ActorId -> m () switchLevels2 lidNew posNew (aid, bOld) mbtime_bOld mbtimeTraj_bOld mlead = do let lidOld = blid bOld side = bfid bOld let !_A = assert (lidNew /= lidOld `blame` "stairs looped" `swith` lidNew) () -- Sync actor's items' timeouts with the new local time of the level. -- We need to sync organs and equipment due to periodic activations, -- but also due to timeouts after use, e.g., for some weapons -- (they recharge also in the stash; however, this doesn't encourage -- micromanagement for periodic items, because the timeout is randomised -- upon move to equipment). -- -- We don't rebase timeouts for items in stash, because they are -- used by many actors on levels with different local times, -- so there is no single rebase that would match all. -- This is not a big problem: after a single use by an actor the timeout is -- set to his current local time, so further uses by that actor have -- not anomalously short or long recharge times. If the recharge time -- is very long, the player has an option of moving the item away from stash -- and back, to reset the timeout. An abuse is possible when recently -- used item is put from equipment to stash and at once used on another level -- taking advantage of local time difference, but this only works once -- and using the item back again at the original level makes the recharge -- time longer, in turn. timeOld <- getsState $ getLocalTime lidOld timeLastActive <- getsState $ getLocalTime lidNew let delta = timeLastActive `timeDeltaToFrom` timeOld computeNewTimeout :: ItemQuant -> ItemQuant computeNewTimeout (k, it) = (k, map (shiftItemTimer delta) it) rebaseTimeout :: ItemBag -> ItemBag rebaseTimeout = EM.map computeNewTimeout bNew = bOld { blid = lidNew , bpos = posNew , boldpos = Just posNew -- new level, new direction , borgan = rebaseTimeout $ borgan bOld , beqp = rebaseTimeout $ beqp bOld } shiftByDelta = (`timeShift` delta) -- Sync the actor time with the level time. -- This time shift may cause a double move of a foe of the same speed, -- but this is OK --- the foe didn't have a chance to move -- before, because the arena went inactive, so he moves now one more time. maybe (return ()) (\btime_bOld -> modifyServer $ \ser -> ser {sactorTime = updateActorTime (bfid bNew) lidNew aid (shiftByDelta btime_bOld) $ sactorTime ser}) mbtime_bOld maybe (return ()) (\btime_bOld -> modifyServer $ \ser -> ser {strajTime = updateActorTime (bfid bNew) lidNew aid (shiftByDelta btime_bOld) $ strajTime ser}) mbtimeTraj_bOld -- Materialize the actor at the new location. -- Onlookers see somebody appear suddenly. The actor himself -- sees new surroundings and has to reset his perception. execUpdAtomic $ UpdSpotActor aid bNew forM_ mlead $ -- The leader is fresh in the sense that he's on a new level -- and so doesn't have up to date Perception. setFreshLeader side -- ** Escape -- | The faction leaves the dungeon. effectEscape :: MonadServerAtomic m => m () -> ActorId -> ActorId -> m UseResult effectEscape execSfx source target = do -- Obvious effect, nothing announced. sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target let fid = bfid tb fact <- getsState $ (EM.! fid) . sfactionD if | bproj tb -> return UseDud -- basically a misfire | not (fcanEscape $ gkind fact) -> do execSfxAtomic $ SfxMsgFid (bfid sb) SfxEscapeImpossible when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) SfxEscapeImpossible return UseId | otherwise -> do execSfx deduceQuits (bfid tb) $ Status Escape (fromEnum $ blid tb) Nothing return UseUp -- ** Paralyze -- | Advance target actor time by this many time clips. Not by actor moves, -- to hurt fast actors more. effectParalyze :: MonadServerAtomic m => m () -> Dice.Dice -> ActorId -> ActorId -> m UseResult effectParalyze execSfx nDm source target = do tb <- getsState $ getActorBody target if bproj tb then return UseDud -- shortcut for speed else paralyze execSfx nDm source target paralyze :: MonadServerAtomic m => m () -> Dice.Dice -> ActorId -> ActorId -> m UseResult paralyze execSfx nDm source target = do tb <- getsState $ getActorBody target totalDepth <- getsState stotalDepth Level{ldepth} <- getLevel (blid tb) power0 <- rndToAction $ castDice ldepth totalDepth nDm let power = max power0 1 -- KISS, avoid special case actorStasis <- getsServer sactorStasis if ES.member target actorStasis then do sb <- getsState $ getActorBody source execSfxAtomic $ SfxMsgFid (bfid sb) SfxStasisProtects when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) SfxStasisProtects return UseId else do execSfx let t = timeDeltaScale (Delta timeClip) power -- Only the normal time, not the trajectory time, is affected. modifyServer $ \ser -> ser { sactorTime = ageActor (bfid tb) (blid tb) target t $ sactorTime ser , sactorStasis = ES.insert target (sactorStasis ser) } -- actor's time warped, so he is in stasis, -- immune to further warps return UseUp -- ** ParalyzeInWater -- | Advance target actor time by this many time clips. Not by actor moves, -- to hurt fast actors more. Due to water, so resistable. effectParalyzeInWater :: MonadServerAtomic m => m () -> Dice.Dice -> ActorId -> ActorId -> m UseResult effectParalyzeInWater execSfx nDm source target = do tb <- getsState $ getActorBody target if bproj tb then return UseDud else do -- shortcut for speed actorMaxSk <- getsState $ getActorMaxSkills target let swimmingOrFlying = max (Ability.getSk Ability.SkSwimming actorMaxSk) (Ability.getSk Ability.SkFlying actorMaxSk) if Dice.supDice nDm > swimmingOrFlying then paralyze execSfx nDm source target -- no help at all else -- fully resisted -- Don't spam: -- sb <- getsState $ getActorBody source -- execSfxAtomic $ SfxMsgFid (bfid sb) SfxWaterParalysisResisted return UseId -- ** InsertMove -- | Give target actor the given number of tenths of extra move. Don't give -- an absolute amount of time units, to benefit slow actors more. effectInsertMove :: MonadServerAtomic m => m () -> Dice.Dice -> ActorId -> ActorId -> m UseResult effectInsertMove execSfx nDm source target = do tb <- getsState $ getActorBody target actorMaxSk <- getsState $ getActorMaxSkills target totalDepth <- getsState stotalDepth Level{ldepth} <- getLevel (blid tb) actorStasis <- getsServer sactorStasis power0 <- rndToAction $ castDice ldepth totalDepth nDm let power = max power0 1 -- KISS, avoid special case actorTurn = ticksPerMeter $ gearSpeed actorMaxSk t = timeDeltaScale (timeDeltaPercent actorTurn 10) (-power) if | bproj tb -> return UseDud -- shortcut for speed | ES.member target actorStasis -> do sb <- getsState $ getActorBody source execSfxAtomic $ SfxMsgFid (bfid sb) SfxStasisProtects when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) SfxStasisProtects return UseId | otherwise -> do execSfx -- Only the normal time, not the trajectory time, is affected. modifyServer $ \ser -> ser { sactorTime = ageActor (bfid tb) (blid tb) target t $ sactorTime ser , sactorStasis = ES.insert target (sactorStasis ser) } -- actor's time warped, so he is in stasis, -- immune to further warps return UseUp -- ** Teleport -- | Teleport the target actor. -- Note that projectiles can be teleported, too, for extra fun. effectTeleport :: MonadServerAtomic m => m () -> Dice.Dice -> ActorId -> ActorId -> m UseResult effectTeleport execSfx nDm source target = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target actorMaxSk <- getsState $ getActorMaxSkills target if | source /= target && Ability.getSk Ability.SkMove actorMaxSk <= 0 -> do execSfxAtomic $ SfxMsgFid (bfid sb) SfxTransImpossible when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) SfxTransImpossible return UseId | source /= target && actorWaits tb -> do -- immune only against not own effects, to enable teleport -- as beneficial's necklace drawback; also consistent -- with sleep not protecting execSfxAtomic $ SfxMsgFid (bfid sb) $ SfxBracedImmune target when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxBracedImmune target return UseId | otherwise -> do COps{coTileSpeedup} <- getsState scops totalDepth <- getsState stotalDepth lvl@Level{ldepth} <- getLevel (blid tb) range <- rndToAction $ castDice ldepth totalDepth nDm let spos = bpos tb dMinMax !delta !pos = let d = chessDist spos pos in d >= range - delta && d <= range + delta dist !delta !pos _ = dMinMax delta pos mtpos <- rndToAction $ findPosTry 200 lvl (\p !t -> Tile.isWalkable coTileSpeedup t && not (Tile.isNoActor coTileSpeedup t) && not (occupiedBigLvl p lvl) && not (occupiedProjLvl p lvl)) [ dist 1 , dist $ 1 + range `div` 9 , dist $ 1 + range `div` 7 , dist $ 1 + range `div` 5 , dist 5 , dist 7 , dist 9 ] case mtpos of Nothing -> do -- really very rare, so debug debugPossiblyPrint "Server: effectTeleport: failed to find any free position" execSfxAtomic $ SfxMsgFid (bfid sb) SfxTransImpossible when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) SfxTransImpossible return UseId Just tpos -> do execSfx execUpdAtomic $ UpdMoveActor target spos tpos return UseUp -- ** CreateItem effectCreateItem :: MonadServerAtomic m => Maybe FactionId -> Maybe Int -> ActorId -> ActorId -> Maybe ItemId -> CStore -> GroupName ItemKind -> IK.TimerDice -> m UseResult effectCreateItem jfidRaw mcount source target miidOriginal store grp tim = do tb <- getsState $ getActorBody target if bproj tb && store == COrgan -- other stores OK not to lose possible loot then return UseDud -- don't make a projectile hungry, etc. else do cops <- getsState scops sb <- getsState $ getActorBody source actorMaxSk <- getsState $ getActorMaxSkills target totalDepth <- getsState stotalDepth lvlTb <- getLevel (blid tb) let -- If the number of items independent of depth in @mcount@, -- make also the timer, the item kind choice and aspects -- independent of depth, via fixing the generation depth of the item -- to @totalDepth@. Prime example of provided @mcount@ is crafting. -- TODO: base this on a resource that can be consciously spent, -- not on a skill that grows over time or that only one actor -- maxes out and so needs to always be chosen for crafting. -- See https://www.reddit.com/r/roguelikedev/comments/phukcq/game_design_question_how_to_base_item_generation/ depth = if isJust mcount then totalDepth else ldepth lvlTb fscale unit nDm = do k0 <- rndToAction $ castDice depth totalDepth nDm let k = max 1 k0 -- KISS, don't freak out if dice permit 0 return $! timeDeltaScale unit k fgame = fscale (Delta timeTurn) factor nDm = do -- A bit added to make sure length 1 effect doesn't randomly -- end, or not, before the end of first turn, which would make, -- e.g., hasting, useless. This needs to be higher than 10% -- to compensate for overhead of animals, etc. (no leaders). let actorTurn = timeDeltaPercent (ticksPerMeter $ gearSpeed actorMaxSk) 111 fscale actorTurn nDm delta <- IK.foldTimer (return $ Delta timeZero) fgame factor tim let c = CActor target store bagBefore <- getsState $ getBodyStoreBag tb store uniqueSet <- getsServer suniqueSet -- Power depth of new items unaffected by number of spawned actors, so 0. let freq = newItemKind cops uniqueSet [(grp, 1)] depth totalDepth 0 m2 <- rollItemAspect freq depth case m2 of NoNewItem -> return UseDud -- e.g., unique already generated NewItem _ itemKnownRaw itemFullRaw (kRaw, itRaw) -> do -- Avoid too many different item identifiers (one for each faction) -- for blasts or common item generating tiles. Conditions are -- allowed to be duplicated, because they provide really useful info -- (perpetrator). However, if timer is none, they are not duplicated -- to make sure that, e.g., poisons stack with each other regardless -- of perpetrator and we don't get "no longer poisoned" message -- while still poisoned due to another faction. With timed aspects, -- e.g., slowness, the message is less misleading, and it's interesting -- that I'm twice slower due to aspects from two factions and not -- as deadly as being poisoned at twice the rate from two factions. let jfid = if store == COrgan && not (IK.isTimerNone tim) || grp == IK.S_IMPRESSED then jfidRaw else Nothing ItemKnown kindIx arItem _ = itemKnownRaw (itemKnown, itemFull) = ( ItemKnown kindIx arItem jfid , itemFullRaw {itemBase = (itemBase itemFullRaw) {jfid}} ) itemRev <- getsServer sitemRev let mquant = case HM.lookup itemKnown itemRev of Nothing -> Nothing Just iid -> (iid,) <$> iid `EM.lookup` bagBefore case mquant of Just (iid, (_, afterIt@(timer : rest))) | not $ IK.isTimerNone tim -> do -- Already has such items and timer change requested, so only increase -- the timer of the first item by the delta, but don't create items. let newIt = shiftItemTimer delta timer : rest if afterIt /= newIt then do execUpdAtomic $ UpdTimeItem iid c afterIt newIt -- It's hard for the client to tell this timer change from charge -- use, timer reset on pickup, etc., so we create the msg manually. -- Sending to both involved factions lets the player notice -- both the extensions he caused and suffered. Other faction causing -- that on themselves or on others won't be noticed. TMI. execSfxAtomic $ SfxMsgFid (bfid sb) $ SfxTimerExtended target iid store delta when (bfid sb /= bfid tb) $ execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxTimerExtended target iid store delta return UseUp else return UseDud -- probably incorrect content, but let it be _ -> do localTime <- getsState $ getLocalTime (blid tb) let newTimer = createItemTimer localTime delta extraIt k = if IK.isTimerNone tim then itRaw -- don't break @applyPeriodicLevel@ else replicate k newTimer -- randomized and overwritten in @registerItem@ -- if an organ or created in equipment kitNew = case mcount of Just itemK -> (itemK, extraIt itemK) Nothing -> (kRaw, extraIt kRaw) case miidOriginal of Just iidOriginal | store /= COrgan -> execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxItemYield iidOriginal (fst kitNew) (blid tb) _ -> return () -- No such items or some items, but void delta, so create items. -- If it's, e.g., a periodic poison, the new items will stack with any -- already existing items. iid <- registerItem True (itemFull, kitNew) itemKnown c -- If created not on the ground, ID it, because it won't be on pickup. -- If ground and stash coincide, unindentified item enters stash, -- so will be identified when equipped, used or dropped -- and picked again. if isJust mcount -- not a random effect, so probably crafting && not (IA.isHumanTrinket (itemKind itemFull)) then execUpdAtomic $ UpdDiscover c iid (itemKindId itemFull) arItem else when (store /= CGround) $ discoverIfMinorEffects c iid (itemKindId itemFull) return UseUp -- ** DestroyItem -- | Make the target actor destroy items in a store from the given group. -- The item that caused the effect itself is *not* immune, because often -- the item needs to destroy itself, e.g., to model wear and tear. -- In such a case, the item may need to be identified, in a container, -- when it no longer exists, at least in the container. This is OK. -- Durable items are not immune, unlike the tools in @ConsumeItems@. effectDestroyItem :: MonadServerAtomic m => m () -> Int -> Int -> CStore -> ActorId -> GroupName ItemKind -> m UseResult effectDestroyItem execSfx ngroup kcopy store target grp = do tb <- getsState $ getActorBody target is <- allGroupItems store grp target if null is then return UseDud else do execSfx urs <- mapM (uncurry (dropCStoreItem True True store target tb kcopy)) (take ngroup is) return $! case urs of [] -> UseDud -- there was no effects _ -> maximum urs -- | Drop a single actor's item (though possibly multiple copies). -- Note that if there are multiple copies, at most one explodes -- to avoid excessive carnage and UI clutter (let's say, -- the multiple explosions interfere with each other or perhaps -- larger quantities of explosives tend to be packaged more safely). -- Note also that @OnSmash@ effects are activated even if item discharged. dropCStoreItem :: MonadServerAtomic m => Bool -> Bool -> CStore -> ActorId -> Actor -> Int -> ItemId -> ItemQuant -> m UseResult dropCStoreItem verbose destroy store aid b kMax iid (k, _) = do let c = CActor aid store bag0 <- getsState $ getContainerBag c -- @OnSmash@ effects of previous items may remove next items, so better check. if iid `EM.notMember` bag0 then return UseDud else do itemFull <- getsState $ itemToFull iid let arItem = aspectRecordFull itemFull fragile = IA.checkFlag Ability.Fragile arItem durable = IA.checkFlag Ability.Durable arItem isDestroyed = destroy || bproj b && (bhp b <= 0 && not durable || fragile) || store == COrgan -- just as organs are destroyed at death -- but also includes conditions if isDestroyed then do let effApplyFlags = EffApplyFlags { effToUse = EffBare -- the embed could be combined at this point but @iid@ cannot , effVoluntary = True -- we don't know if it's effVoluntary, so we conservatively assume -- it is and we blame @aid@ , effUseAllCopies = kMax >= k , effKineticPerformed = False , effActivation = ActivationOnSmash , effMayDestroy = True } void $ effectAndDestroyAndAddKill effApplyFlags aid aid aid iid c itemFull -- One copy was destroyed (or none if the item was discharged), -- so let's mop up. bag <- getsState $ getContainerBag c maybe (return ()) (\(k1, it) -> do let destroyedSoFar = k - k1 k2 = min (kMax - destroyedSoFar) k1 kit2 = (k2, take k2 it) -- Don't spam if the effect already probably made noise -- and also the number could be surprising to the player. verbose2 = verbose && k1 == k when (k2 > 0) $ execUpdAtomic $ UpdDestroyItem verbose2 iid (itemBase itemFull) kit2 c) (EM.lookup iid bag) return UseUp else do cDrop <- pickDroppable False aid b -- drop over fog, etc. mvCmd <- generalMoveItem verbose iid (min kMax k) (CActor aid store) cDrop mapM_ execUpdAtomic mvCmd return UseUp pickDroppable :: MonadStateRead m => Bool -> ActorId -> Actor -> m Container pickDroppable respectNoItem aid b = do cops@COps{coTileSpeedup} <- getsState scops lvl <- getLevel (blid b) let validTile t = not (respectNoItem && Tile.isNoItem coTileSpeedup t) if validTile $ lvl `at` bpos b then return $! CActor aid CGround else do let ps = nearbyFreePoints cops lvl validTile (bpos b) return $! case filter (adjacent $ bpos b) $ take 8 ps of [] -> CActor aid CGround -- fallback; still correct, though not ideal pos : _ -> CFloor (blid b) pos -- ** ConsumeItems -- | Make the target actor destroy the given items, if all present, -- or none at all, if any is missing. To be used in crafting. -- The item that caused the effect itself is not considered (any copies). effectConsumeItems :: MonadServerAtomic m => m () -> ItemId -> ActorId -> [(Int, GroupName ItemKind)] -> [(Int, GroupName ItemKind)] -> m UseResult effectConsumeItems execSfx iidOriginal target tools0 raw0 = do kitAssG <- getsState $ kitAssocs target [CGround] let kitAss = listToolsToConsume kitAssG [] -- equipment too dangerous to use is = filter ((/= iidOriginal) . fst . snd) kitAss grps0 = map (\(x, y) -> (False, x, y)) tools0 -- apply if durable ++ map (\(x, y) -> (True, x, y)) raw0 -- destroy always (bagsToLose3, iidsToApply3, grps3) = foldl' subtractIidfromGrps (EM.empty, [], grps0) is if null grps3 then do execSfx consumeItems target bagsToLose3 iidsToApply3 return UseUp else return UseDud consumeItems :: MonadServerAtomic m => ActorId -> EM.EnumMap CStore ItemBag -> [(CStore, (ItemId, ItemFull))] -> m () consumeItems target bagsToLose iidsToApply = do COps{coitem} <- getsState scops tb <- getsState $ getActorBody target arTrunk <- getsState $ (EM.! btrunk tb) . sdiscoAspect let isBlast = IA.checkFlag Ability.Blast arTrunk identifyStoreBag store bag = mapM_ (identifyStoreIid store) $ EM.keys bag identifyStoreIid store iid = do discoAspect2 <- getsState sdiscoAspect -- might have changed due to embedded items invocations itemKindId <- getsState $ getIidKindIdServer iid let arItem = discoAspect2 EM.! iid c = CActor target store itemKind = okind coitem itemKindId unless (IA.isHumanTrinket itemKind) $ -- a hack execUpdAtomic $ UpdDiscover c iid itemKindId arItem -- We don't invoke @OnSmash@ effects, so we avoid the risk -- of the first removed item displacing the actor, destroying -- or scattering some pending items ahead of time, etc. -- The embed should provide any requisite fireworks instead. forM_ (EM.assocs bagsToLose) $ \(store, bagToLose) -> unless (EM.null bagToLose) $ do identifyStoreBag store bagToLose -- Not @UpdLoseItemBag@, to be verbose. -- The bag is small, anyway. let c = CActor target store itemD <- getsState sitemD mapWithKeyM_ (\iid kit -> do let verbose = not isBlast -- no spam item = itemD EM.! iid execUpdAtomic $ UpdDestroyItem verbose iid item kit c) bagToLose -- But afterwards we do apply normal effects of durable items, -- even if the actor or other items displaced in the process, -- as long as a number of the items is still there. -- So if a harmful double-purpose tool-component is both to be used -- and destroyed, it will be lost, but at least it won't harm anybody. let applyItemIfPresent (store, (iid, itemFull)) = do let c = CActor target store bag <- getsState $ getContainerBag c when (iid `EM.member` bag) $ do execSfxAtomic $ SfxApply target iid -- Treated as if the actor only activated the item on himself, -- without kinetic damage, to avoid the exploit of wearing armor -- when using tools or transforming terrain. -- Also, timeouts of the item ignored to prevent exploit -- by discharging the item before using it. let effApplyFlags = EffApplyFlags { effToUse = EffBare -- crafting not intended , effVoluntary = True , effUseAllCopies = False , effKineticPerformed = False , effActivation = ActivationConsume , effMayDestroy = False } void $ effectAndDestroyAndAddKill effApplyFlags target target target iid c itemFull mapM_ applyItemIfPresent iidsToApply -- ** DropItem -- | Make the target actor drop items in a store from the given group. -- The item that caused the effect itself is immune (any copies). effectDropItem :: MonadServerAtomic m => m () -> ItemId -> Int -> Int -> CStore -> GroupName ItemKind -> ActorId -> m UseResult effectDropItem execSfx iidOriginal ngroup kcopy store grp target = do tb <- getsState $ getActorBody target fact <- getsState $ (EM.! bfid tb) . sfactionD isRaw <- allGroupItems store grp target curChalSer <- getsServer $ scurChalSer . soptions factionD <- getsState sfactionD let is = filter ((/= iidOriginal) . fst) isRaw if | bproj tb || null is -> return UseDud | ngroup == maxBound && kcopy == maxBound && store `elem` [CStash, CEqp] && fhasGender (gkind fact) -- hero in Allure's decontamination chamber && (cdiff curChalSer == 1 -- at lowest difficulty for its faction && any (fhasUI . gkind . snd) (filter (\(fi, fa) -> isFriend fi fa (bfid tb)) (EM.assocs factionD)) || cdiff curChalSer == difficultyBound && any (fhasUI . gkind . snd) (filter (\(fi, fa) -> isFoe fi fa (bfid tb)) (EM.assocs factionD))) -> {- A hardwired hack, because AI heroes don't cope with Allure's decontamination chamber; beginners may struggle too, so this is trigered by difficulty. - AI heroes don't switch leader to the hero past laboratory to equip weapons from stash between the in-lab hero picks up the loot pile and himself enters the decontamination chamber - the items of the last actor would be lost anyway, unless AI is taught the foolproof solution of this puzzle, which is yet a bit more specific than the two abilities above -} return UseUp | otherwise -> do unless (store == COrgan) execSfx urs <- mapM (uncurry (dropCStoreItem True False store target tb kcopy)) (take ngroup is) return $! case urs of [] -> UseDud -- there was no effects _ -> maximum urs -- ** Recharge and Discharge effectRecharge :: forall m. MonadServerAtomic m => Bool -> m () -> ItemId -> Int -> Dice.Dice -> ActorId -> m UseResult effectRecharge reducingCooldown execSfx iidOriginal n0 dice target = do tb <- getsState $ getActorBody target if bproj tb then return UseDud else do -- slows down, but rarely any effect localTime <- getsState $ getLocalTime (blid tb) totalDepth <- getsState stotalDepth Level{ldepth} <- getLevel $ blid tb power <- rndToAction $ castDice ldepth totalDepth dice let timeUnit = if reducingCooldown then absoluteTimeNegate timeClip else timeClip delta = timeDeltaScale (Delta timeUnit) power localTimer = createItemTimer localTime (Delta timeZero) addToCooldown :: CStore -> (Int, UseResult) -> (ItemId, ItemFullKit) -> m (Int, UseResult) addToCooldown _ (0, ur) _ = return (0, ur) addToCooldown store (n, ur) (iid, (_, (k0, itemTimers0))) = do let itemTimers = filter (charging localTime) itemTimers0 kt = length itemTimers lenToShift = min n $ if reducingCooldown then kt else k0 - kt (itToShift, itToKeep) = if reducingCooldown then splitAt lenToShift itemTimers else (replicate lenToShift localTimer, itemTimers) -- No problem if this overcharges; equivalent to pruned timer. it2 = map (shiftItemTimer delta) itToShift ++ itToKeep if itemTimers0 == it2 then return (n, ur) else do let c = CActor target store execUpdAtomic $ UpdTimeItem iid c itemTimers0 it2 return (n - lenToShift, UseUp) selectWeapon i@(iid, (itemFull, _)) (weapons, others) = let arItem = aspectRecordFull itemFull in if | IA.aTimeout arItem == 0 || iid == iidOriginal -> (weapons, others) | IA.checkFlag Ability.Meleeable arItem -> (i : weapons, others) | otherwise -> (weapons, i : others) partitionWeapon = foldr selectWeapon ([],[]) ignoreCharges = True -- handled above depending on @reducingCooldown@ benefits = Nothing -- only raw damage counts (client knows benefits) sortWeapons ass = map (\(_, _, _, _, iid, itemFullKit) -> (iid, itemFullKit)) $ strongestMelee ignoreCharges benefits localTime ass eqpAss <- getsState $ kitAssocs target [CEqp] let (eqpAssWeapons, eqpAssOthers) = partitionWeapon eqpAss organAss <- getsState $ kitAssocs target [COrgan] let (organAssWeapons, organAssOthers) = partitionWeapon organAss (nEqpWeapons, urEqpWeapons) <- foldM (addToCooldown CEqp) (n0, UseDud) $ sortWeapons eqpAssWeapons (nOrganWeapons, urOrganWeapons) <- foldM (addToCooldown COrgan) (nEqpWeapons, urEqpWeapons) $ sortWeapons organAssWeapons (nEqpOthers, urEqpOthers) <- foldM (addToCooldown CEqp) (nOrganWeapons, urOrganWeapons) eqpAssOthers (_nOrganOthers, urOrganOthers) <- foldM (addToCooldown COrgan) (nEqpOthers, urEqpOthers) organAssOthers if urOrganOthers == UseDud then return UseDud else do execSfx return UseUp -- ** PolyItem -- Can't apply to the item itself (any copies). effectPolyItem :: MonadServerAtomic m => m () -> ItemId -> ActorId -> m UseResult effectPolyItem execSfx iidOriginal target = do tb <- getsState $ getActorBody target let cstore = CGround kitAss <- getsState $ kitAssocs target [cstore] case filter ((/= iidOriginal) . fst) kitAss of [] -> do execSfxAtomic $ SfxMsgFid (bfid tb) SfxPurposeNothing -- Do not spam the source actor player about the failures. return UseId (iid, ( itemFull@ItemFull{itemBase, itemKindId, itemKind} , (itemK, itemTimer) )) : _ -> do let arItem = aspectRecordFull itemFull maxCount = Dice.supDice $ IK.icount itemKind if | IA.checkFlag Ability.Unique arItem -> do execSfxAtomic $ SfxMsgFid (bfid tb) SfxPurposeUnique return UseId | maybe True (<= 0) $ lookup IK.COMMON_ITEM $ IK.ifreq itemKind -> do execSfxAtomic $ SfxMsgFid (bfid tb) SfxPurposeNotCommon return UseId | itemK < maxCount -> do execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxPurposeTooFew maxCount itemK return UseId | otherwise -> do -- Only the required number of items is used up, not all of them. let c = CActor target cstore kit = (maxCount, take maxCount itemTimer) execSfx identifyIid iid c itemKindId itemKind execUpdAtomic $ UpdDestroyItem True iid itemBase kit c effectCreateItem (Just $ bfid tb) Nothing target target Nothing cstore IK.COMMON_ITEM IK.timerNone -- ** RerollItem -- Can't apply to the item itself (any copies). effectRerollItem :: forall m . MonadServerAtomic m => m () -> ItemId -> ActorId -> m UseResult effectRerollItem execSfx iidOriginal target = do COps{coItemSpeedup} <- getsState scops tb <- getsState $ getActorBody target let cstore = CGround -- if ever changed, call @discoverIfMinorEffects@ kitAss <- getsState $ kitAssocs target [cstore] case filter ((/= iidOriginal) . fst) kitAss of [] -> do execSfxAtomic $ SfxMsgFid (bfid tb) SfxRerollNothing -- Do not spam the source actor player about the failures. return UseId (iid, ( ItemFull{ itemBase, itemKindId, itemKind , itemDisco=ItemDiscoFull itemAspect } , (_, itemTimer) )) : _ -> if IA.kmConst $ getKindMean itemKindId coItemSpeedup then do execSfxAtomic $ SfxMsgFid (bfid tb) SfxRerollNotRandom return UseId else do let c = CActor target cstore kit = (1, take 1 itemTimer) -- prevent micromanagement freq = pure (IK.HORROR, itemKindId, itemKind) execSfx identifyIid iid c itemKindId itemKind execUpdAtomic $ UpdDestroyItem False iid itemBase kit c totalDepth <- getsState stotalDepth let roll100 :: Int -> m (ItemKnown, ItemFull) roll100 n = do -- Not only rerolled, but at highest depth possible, -- resulting in highest potential for bonuses. m2 <- rollItemAspect freq totalDepth case m2 of NoNewItem -> error "effectRerollItem: can't create rerolled item" NewItem _ itemKnown@(ItemKnown _ ar2 _) itemFull _ -> if ar2 == itemAspect && n > 0 then roll100 (n - 1) else return (itemKnown, itemFull) (itemKnown, itemFull) <- roll100 100 void $ registerItem True (itemFull, kit) itemKnown c return UseUp _ -> error "effectRerollItem: server ignorant about an item" -- ** DupItem -- Can't apply to the item itself (any copies). effectDupItem :: MonadServerAtomic m => m () -> ItemId -> ActorId -> m UseResult effectDupItem execSfx iidOriginal target = do tb <- getsState $ getActorBody target let cstore = CGround -- beware of other options, e.g., creating in eqp -- and not setting timeout to a random value kitAss <- getsState $ kitAssocs target [cstore] case filter ((/= iidOriginal) . fst) kitAss of [] -> do execSfxAtomic $ SfxMsgFid (bfid tb) SfxDupNothing -- Do not spam the source actor player about the failures. return UseId (iid, ( itemFull@ItemFull{itemKindId, itemKind} , _ )) : _ -> do let arItem = aspectRecordFull itemFull if | IA.checkFlag Ability.Unique arItem -> do execSfxAtomic $ SfxMsgFid (bfid tb) SfxDupUnique return UseId | maybe False (> 0) $ lookup IK.VALUABLE $ IK.ifreq itemKind -> do execSfxAtomic $ SfxMsgFid (bfid tb) SfxDupValuable return UseId | otherwise -> do let c = CActor target cstore execSfx identifyIid iid c itemKindId itemKind let slore = IA.loreFromContainer arItem c modifyServer $ \ser -> ser {sgenerationAn = EM.adjust (EM.insertWith (+) iid 1) slore (sgenerationAn ser)} execUpdAtomic $ UpdCreateItem True iid (itemBase itemFull) quantSingle c return UseUp -- ** Identify effectIdentify :: MonadServerAtomic m => m () -> ItemId -> ActorId -> m UseResult effectIdentify execSfx iidOriginal target = do COps{coItemSpeedup} <- getsState scops discoAspect <- getsState sdiscoAspect -- The actor that causes the application does not determine what item -- is identifiable, becuase it's the target actor that identifies -- his possesions. tb <- getsState $ getActorBody target sClient <- getsServer $ (EM.! bfid tb) . sclientStates let tryFull store as = case as of [] -> return False (iid, _) : rest | iid == iidOriginal -> tryFull store rest -- don't id itself (iid, ItemFull{itemBase, itemKindId, itemKind}) : rest -> do let arItem = discoAspect EM.! iid kindIsKnown = case jkind itemBase of IdentityObvious _ -> True IdentityCovered ix _ -> ix `EM.member` sdiscoKind sClient if iid `EM.member` sdiscoAspect sClient -- already fully identified || IA.isHumanTrinket itemKind -- hack; keep them non-identified || store == CGround && IA.onlyMinorEffects arItem itemKind -- will be identified when picked up, so don't bother || IA.kmConst (getKindMean itemKindId coItemSpeedup) && kindIsKnown -- constant aspects and known kind; no need to identify further; -- this should normally not be needed, since clients should -- identify such items for free then tryFull store rest else do let c = CActor target store execSfx identifyIid iid c itemKindId itemKind return True tryStore stores = case stores of [] -> do execSfxAtomic $ SfxMsgFid (bfid tb) SfxIdentifyNothing return UseId -- the message tells it's ID effect store : rest -> do allAssocs <- getsState $ fullAssocs target [store] go <- tryFull store allAssocs if go then return UseUp else tryStore rest tryStore [CGround, CStash, CEqp] -- The item need not be in the container. It's used for a message only. identifyIid :: MonadServerAtomic m => ItemId -> Container -> ContentId ItemKind -> ItemKind -> m () identifyIid iid c itemKindId itemKind = unless (IA.isHumanTrinket itemKind) $ do discoAspect <- getsState sdiscoAspect execUpdAtomic $ UpdDiscover c iid itemKindId $ discoAspect EM.! iid -- ** Detect effectDetect :: MonadServerAtomic m => m () -> IK.DetectKind -> Int -> ActorId -> Container -> m UseResult effectDetect execSfx d radius target container = do COps{coitem, coTileSpeedup} <- getsState scops b <- getsState $ getActorBody target lvl <- getLevel $ blid b sClient <- getsServer $ (EM.! bfid b) . sclientStates let lvlClient = (EM.! blid b) . sdungeon $ sClient s <- getState getKind <- getsState $ flip getIidKindServer factionD <- getsState sfactionD let lootPredicate p = p `EM.member` lfloor lvl || (case posToBigAssoc p (blid b) s of Nothing -> False Just (_, body) -> let belongings = EM.keys (beqp body) -- shared stash ignored in any belongingIsLoot belongings) || any embedHasLoot (EM.keys $ getEmbedBag (blid b) p s) itemKindIsLoot = isNothing . lookup IK.UNREPORTED_INVENTORY . IK.ifreq belongingIsLoot iid = itemKindIsLoot $ getKind iid embedHasLoot iid = any effectHasLoot $ IK.ieffects $ getKind iid reported acc _ _ itemKind = acc && itemKindIsLoot itemKind effectHasLoot (IK.CreateItem _ cstore grp _) = cstore `elem` [CGround, CStash, CEqp] && ofoldlGroup' coitem grp reported True effectHasLoot IK.PolyItem = True effectHasLoot IK.RerollItem = True effectHasLoot IK.DupItem = True effectHasLoot (IK.AtMostOneOf l) = any effectHasLoot l effectHasLoot (IK.OneOf l) = any effectHasLoot l effectHasLoot (IK.OnSmash eff) = effectHasLoot eff effectHasLoot (IK.OnUser eff) = effectHasLoot eff effectHasLoot (IK.AndEffect eff1 eff2) = effectHasLoot eff1 || effectHasLoot eff2 effectHasLoot (IK.OrEffect eff1 eff2) = effectHasLoot eff1 || effectHasLoot eff2 effectHasLoot (IK.SeqEffect effs) = any effectHasLoot effs effectHasLoot (IK.When _ eff) = effectHasLoot eff effectHasLoot (IK.Unless _ eff) = effectHasLoot eff effectHasLoot (IK.IfThenElse _ eff1 eff2) = effectHasLoot eff1 || effectHasLoot eff2 effectHasLoot _ = False stashPredicate p = any (onStash p) $ EM.assocs factionD onStash p (fid, fact) = case gstash fact of Just (lid, pos) -> pos == p && lid == blid b && fid /= bfid b Nothing -> False (predicate, action) = case d of IK.DetectAll -> (const True, const $ return False) IK.DetectActor -> ((`EM.member` lbig lvl), const $ return False) IK.DetectLoot -> (lootPredicate, const $ return False) IK.DetectExit -> let (ls1, ls2) = lstair lvl in ((`elem` ls1 ++ ls2 ++ lescape lvl), const $ return False) IK.DetectHidden -> let predicateH p = let tClient = lvlClient `at` p tServer = lvl `at` p in Tile.isHideAs coTileSpeedup tServer && tClient /= tServer -- the actor searches only tiles he doesn't know already, -- preventing misleading messages (and giving less information -- to eavesdropping parties) revealEmbed p = do embeds <- getsState $ getEmbedBag (blid b) p unless (EM.null embeds) $ execUpdAtomic $ UpdSpotItemBag True (CEmbed (blid b) p) embeds actionH l = do pos <- getsState $ posFromC container let f p = when (p /= pos) $ do let t = lvl `at` p execUpdAtomic $ UpdSearchTile target p t -- This is safe searching; embedded items -- are not triggered, but they are revealed. revealEmbed p case EM.lookup p $ lentry lvl of Nothing -> return () Just entry -> execUpdAtomic $ UpdSpotEntry (blid b) [(p, entry)] mapM_ f l return $! not $ null l in (predicateH, actionH) IK.DetectEmbed -> ((`EM.member` lembed lvl), const $ return False) IK.DetectStash -> (stashPredicate, const $ return False) effectDetectX d predicate action execSfx radius target -- This is not efficient at all, so optimize iff detection is added -- to periodic organs or common periodic items or often activated embeds. effectDetectX :: MonadServerAtomic m => IK.DetectKind -> (Point -> Bool) -> ([Point] -> m Bool) -> m () -> Int -> ActorId -> m UseResult effectDetectX d predicate action execSfx radius target = do COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops b <- getsState $ getActorBody target sperFidOld <- getsServer sperFid let perOld = sperFidOld EM.! bfid b EM.! blid b Point x0 y0 = bpos b perList = filter predicate [ Point x y | y <- [max 0 (y0 - radius) .. min (rHeightMax - 1) (y0 + radius)] , x <- [max 0 (x0 - radius) .. min (rWidthMax - 1) (x0 + radius)] ] extraPer = emptyPer {psight = PerVisible $ ES.fromDistinctAscList perList} inPer = diffPer extraPer perOld unless (nullPer inPer) $ do -- Perception is modified on the server and sent to the client -- together with all the revealed info. let perNew = addPer inPer perOld fper = EM.adjust (EM.insert (blid b) perNew) (bfid b) modifyServer $ \ser -> ser {sperFid = fper $ sperFid ser} execSendPer (bfid b) (blid b) emptyPer inPer perNew pointsModified <- action perList if not (nullPer inPer) || pointsModified then do execSfx -- Perception is reverted. This is necessary to ensure save and restore -- doesn't change game state. unless (nullPer inPer) $ do modifyServer $ \ser -> ser {sperFid = sperFidOld} execSendPer (bfid b) (blid b) inPer emptyPer perOld else execSfxAtomic $ SfxMsgFid (bfid b) $ SfxVoidDetection d return UseUp -- even if nothing spotted, in itself it's still useful data -- ** SendFlying -- | Send the target actor flying like a projectile. If the actors are adjacent, -- the vector is directed outwards, if no, inwards, if it's the same actor, -- boldpos is used, if it can't, a random outward vector of length 10 -- is picked. effectSendFlying :: MonadServerAtomic m => m () -> IK.ThrowMod -> ActorId -> ActorId -> Container -> Maybe Bool -> m UseResult effectSendFlying execSfx IK.ThrowMod{..} source target container modePush = do v <- sendFlyingVector source target container modePush sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target let eps = 0 fpos = bpos tb `shift` v isEmbed = case container of CEmbed{} -> True _ -> False if bhp tb <= 0 -- avoid dragging around corpses || bproj tb && isEmbed then -- flying projectiles can't slip on the floor return UseDud -- the impact never manifested else if actorWaits tb && source /= target && isNothing (btrajectory tb) then do execSfxAtomic $ SfxMsgFid (bfid sb) $ SfxBracedImmune target when (source /= target) $ execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxBracedImmune target return UseUp -- waste it to prevent repeated throwing at immobile actors else do case bresenhamsLineAlgorithm eps (bpos tb) fpos of Nothing -> error $ "" `showFailure` (fpos, tb) Just [] -> error $ "projecting from the edge of level" `showFailure` (fpos, tb) Just (pos : rest) -> do weightAssocs <- getsState $ fullAssocs target [CEqp, COrgan] let weight = sum $ map (IK.iweight . itemKind . snd) weightAssocs path = bpos tb : pos : rest (trajectory, (speed, _)) = -- Note that the @ThrowMod@ aspect of the actor's trunk is ignored. computeTrajectory weight throwVelocity throwLinger path ts = Just (trajectory, speed) -- Old and new trajectories are not added; the old one is replaced. if btrajectory tb == ts then return UseId -- e.g., actor is too heavy; but a jerk is noticeable else do execSfx execUpdAtomic $ UpdTrajectory target (btrajectory tb) ts -- If propeller is a projectile, it pushes involuntarily, -- so its originator is to blame. -- However, we can't easily see whether a pushed non-projectile actor -- pushed another due to colliding or voluntarily, so we assign -- blame to him. originator <- if bproj sb then getsServer $ EM.findWithDefault source source . strajPushedBy else return source modifyServer $ \ser -> ser {strajPushedBy = EM.insert target originator $ strajPushedBy ser} -- In case of pre-existing pushing, don't touch the time -- so that the pending @advanceTimeTraj@ can do its job -- (it will, because non-empty trajectory is here set, unless, e.g., -- subsequent effects from the same item change the trajectory). when (isNothing $ btrajectory tb) $ do -- Set flying time to almost now, so that the push happens ASAP, -- because it's the first one, so almost no delay is needed. localTime <- getsState $ getLocalTime (blid tb) -- But add a slight overhead to avoid displace-slide loops -- of 3 actors in a line. However, add even more overhead -- to normal actor move, so that it doesn't manage to land -- a hit before it flies away safely. let overheadTime = timeShift localTime (Delta timeClip) doubleClip = timeDeltaScale (Delta timeClip) 2 modifyServer $ \ser -> ser { strajTime = updateActorTime (bfid tb) (blid tb) target overheadTime $ strajTime ser , sactorTime = ageActor (bfid tb) (blid tb) target doubleClip $ sactorTime ser } return UseUp sendFlyingVector :: MonadServerAtomic m => ActorId -> ActorId -> Container -> Maybe Bool -> m Vector sendFlyingVector source target container modePush = do sb <- getsState $ getActorBody source if source == target then do pos <- getsState $ posFromC container lid <- getsState $ lidFromC container let (start, end) = -- Without the level the pushing stair trap moved actor back upstairs. if bpos sb /= pos && blid sb == lid then (bpos sb, pos) else (fromMaybe (bpos sb) (boldpos sb), bpos sb) if start == end then rndToAction $ do z <- randomR (-10, 10) oneOf [Vector 10 z, Vector (-10) z, Vector z 10, Vector z (-10)] else do let pushV = vectorToFrom end start pullV = vectorToFrom start end return $! case modePush of Just True -> pushV Just False -> pullV Nothing -> pushV else do tb <- getsState $ getActorBody target let pushV = vectorToFrom (bpos tb) (bpos sb) pullV = vectorToFrom (bpos sb) (bpos tb) return $! case modePush of Just True -> pushV Just False -> pullV Nothing | adjacent (bpos sb) (bpos tb) -> pushV Nothing -> pullV -- ** ApplyPerfume effectApplyPerfume :: MonadServerAtomic m => m () -> ActorId -> m UseResult effectApplyPerfume execSfx target = do tb <- getsState $ getActorBody target Level{lsmell} <- getLevel $ blid tb unless (EM.null lsmell) $ do execSfx let f p fromSm = execUpdAtomic $ UpdAlterSmell (blid tb) p fromSm timeZero mapWithKeyM_ f lsmell return UseUp -- even if no smell before, the perfume is noticeable -- ** AtMostOneOf effectAtMostOneOf :: MonadServerAtomic m => (IK.Effect -> m UseResult) -> [IK.Effect] -> m UseResult effectAtMostOneOf recursiveCall l = do chosen <- rndToAction $ oneOf l recursiveCall chosen -- no @execSfx@, because the individual effect sents it -- ** OneOf effectOneOf :: MonadServerAtomic m => (IK.Effect -> m UseResult) -> [IK.Effect] -> m UseResult effectOneOf recursiveCall l = do shuffled <- rndToAction $ shuffle l let f eff result = do ur <- recursiveCall eff -- We stop at @UseId@ activation and in this ways avoid potentially -- many calls to fizzling effects that only spam a failure message -- and ID the item. if ur == UseDud then result else return ur foldr f (return UseDud) shuffled -- no @execSfx@, because the individual effect sents it -- ** AndEffect effectAndEffect :: forall m. MonadServerAtomic m => (IK.Effect -> m UseResult) -> ActorId -> IK.Effect -> IK.Effect -> m UseResult effectAndEffect recursiveCall source eff1@IK.ConsumeItems{} eff2 = do -- So far, this is the only idiom used for crafting. If others appear, -- either formalize it by a specialized crafting effect constructor -- or add here and to effect printing code. sb <- getsState $ getActorBody source curChalSer <- getsServer $ scurChalSer . soptions fact <- getsState $ (EM.! bfid sb) . sfactionD if cgoods curChalSer && fhasUI (gkind fact) then do execSfxAtomic $ SfxMsgFid (bfid sb) SfxReadyGoods return UseId else effectAndEffectSem recursiveCall eff1 eff2 effectAndEffect recursiveCall _ eff1 eff2 = effectAndEffectSem recursiveCall eff1 eff2 effectAndEffectSem :: forall m. MonadServerAtomic m => (IK.Effect -> m UseResult) -> IK.Effect -> IK.Effect -> m UseResult effectAndEffectSem recursiveCall eff1 eff2 = do ur1 <- recursiveCall eff1 if ur1 == UseUp then recursiveCall eff2 else return ur1 -- No @execSfx@, because individual effects sent them. -- ** OrEffect effectOrEffect :: forall m. MonadServerAtomic m => (IK.Effect -> m UseResult) -> FactionId -> IK.Effect -> IK.Effect -> m UseResult effectOrEffect recursiveCall fid eff1 eff2 = do curChalSer <- getsServer $ scurChalSer . soptions fact <- getsState $ (EM.! fid) . sfactionD case eff1 of IK.AndEffect IK.ConsumeItems{} _ | cgoods curChalSer && fhasUI (gkind fact) -> do -- Stop forbidden crafting ASAP to avoid spam. execSfxAtomic $ SfxMsgFid fid SfxReadyGoods return UseId _ -> do ur1 <- recursiveCall eff1 if ur1 == UseUp then return UseUp else recursiveCall eff2 -- no @execSfx@, because individual effects sent them -- ** SeqEffect effectSeqEffect :: forall m. MonadServerAtomic m => (IK.Effect -> m UseResult) -> [IK.Effect] -> m UseResult effectSeqEffect recursiveCall effs = do mapM_ (void <$> recursiveCall) effs return UseUp -- no @execSfx@, because individual effects sent them -- ** When effectWhen :: forall m. MonadServerAtomic m => (IK.Effect -> m UseResult) -> ActorId -> IK.Condition -> IK.Effect -> ActivationFlag -> m UseResult effectWhen recursiveCall source cond eff effActivation = do go <- conditionSem source cond effActivation if go then recursiveCall eff else return UseDud -- ** Unless effectUnless :: forall m. MonadServerAtomic m => (IK.Effect -> m UseResult) -> ActorId -> IK.Condition -> IK.Effect -> ActivationFlag -> m UseResult effectUnless recursiveCall source cond eff effActivation = do go <- conditionSem source cond effActivation if not go then recursiveCall eff else return UseDud -- ** IfThenElse effectIfThenElse :: forall m. MonadServerAtomic m => (IK.Effect -> m UseResult) -> ActorId -> IK.Condition -> IK.Effect -> IK.Effect -> ActivationFlag -> m UseResult effectIfThenElse recursiveCall source cond eff1 eff2 effActivation = do c <- conditionSem source cond effActivation if c then recursiveCall eff1 else recursiveCall eff2 -- ** VerbNoLonger effectVerbNoLonger :: MonadServerAtomic m => Bool -> m () -> ActorId -> m UseResult effectVerbNoLonger effUseAllCopies execSfx source = do b <- getsState $ getActorBody source when (effUseAllCopies -- @UseUp@ ensures that if all used, all destroyed && not (bproj b)) -- no spam when projectiles activate execSfx -- announce that all copies have run out (or whatever message) return UseUp -- help to destroy the copy, even if not all used up -- ** VerbMsg effectVerbMsg :: MonadServerAtomic m => m () -> ActorId -> m UseResult effectVerbMsg execSfx source = do b <- getsState $ getActorBody source unless (bproj b) execSfx -- don't spam when projectiles activate return UseUp -- announcing always successful and this helps -- to destroy the item -- ** VerbMsgFail effectVerbMsgFail :: MonadServerAtomic m => m () -> ActorId -> m UseResult effectVerbMsgFail execSfx source = do b <- getsState $ getActorBody source unless (bproj b) execSfx -- don't spam when projectiles activate return UseId -- not @UseDud@ so that @OneOf@ doesn't ignore it LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/HandleRequestM.hs0000644000000000000000000017010607346545000023431 0ustar0000000000000000-- | Semantics of requests -- . -- A couple of them do not take time, the rest does. -- Note that since the results are atomic commands, which are executed -- only later (on the server and some of the clients), all condition -- are checkd by the semantic functions in the context of the state -- before the server command. Even if one or more atomic actions -- are already issued by the point an expression is evaluated, they do not -- influence the outcome of the evaluation. module Game.LambdaHack.Server.HandleRequestM ( handleRequestAI, handleRequestUI, handleRequestTimed, switchLeader , reqMoveGeneric, reqDisplaceGeneric, reqAlterFail , reqGameDropAndExit, reqGameSaveAndExit #ifdef EXPOSE_INTERNAL -- * Internal operations , execFailure, checkWaiting, processWatchfulness, affectStash , managePerRequest, handleRequestTimedCases, affectSmell , reqMove, reqMelee, reqMeleeChecked, reqDisplace, reqAlter , reqWait, reqWait10, reqYell, reqMoveItems, reqMoveItem, reqProject, reqApply , reqGameRestart, reqGameSave, reqDoctrine, reqAutomate #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import qualified Text.Show.Pretty as Show.Pretty import Game.LambdaHack.Atomic import Game.LambdaHack.Client (ReqAI (..), ReqUI (..), RequestTimed (..)) import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind import qualified Game.LambdaHack.Content.TileKind as TK import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.CommonM import Game.LambdaHack.Server.HandleEffectM import Game.LambdaHack.Server.ItemM import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.PeriodicM import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State execFailure :: MonadServerAtomic m => ActorId -> RequestTimed -> ReqFailure -> m () execFailure aid req failureSer = do -- Clients should rarely do that (only in case of invisible actors) -- so we report it to the client, but do not crash -- (server should work OK with stupid clients, too). body <- getsState $ getActorBody aid let fid = bfid body msg = showReqFailure failureSer impossible = impossibleReqFailure failureSer debugShow :: Show a => a -> Text debugShow = T.pack . Show.Pretty.ppShow possiblyAlarm = if impossible then debugPossiblyPrintAndExit else debugPossiblyPrint possiblyAlarm $ "Server: execFailure:" <+> msg <> "\n" <> debugShow body <> "\n" <> debugShow req <> "\n" <> debugShow failureSer execSfxAtomic $ SfxMsgFid fid $ SfxUnexpected failureSer -- | The semantics of server commands. -- AI always takes time and so doesn't loop. handleRequestAI :: MonadServerAtomic m => ReqAI -> m (Maybe RequestTimed) handleRequestAI cmd = case cmd of ReqAITimed cmdT -> return $ Just cmdT ReqAINop -> return Nothing -- | The semantics of server commands. Only the first two cases affect time. handleRequestUI :: MonadServerAtomic m => FactionId -> ActorId -> ReqUI -> m (Maybe RequestTimed) handleRequestUI fid aid cmd = case cmd of ReqUITimed cmdT -> return $ Just cmdT ReqUIGameRestart t d -> reqGameRestart aid t d >> return Nothing ReqUIGameDropAndExit -> reqGameDropAndExit aid >> return Nothing ReqUIGameSaveAndExit -> reqGameSaveAndExit aid >> return Nothing ReqUIGameSave -> reqGameSave >> return Nothing ReqUIDoctrine toT -> reqDoctrine fid toT >> return Nothing ReqUIAutomate -> reqAutomate fid >> return Nothing ReqUINop -> return Nothing checkWaiting :: RequestTimed -> Maybe Bool checkWaiting cmd = case cmd of ReqWait -> Just True -- true wait, with bracing, no overhead, etc. ReqWait10 -> Just False -- false wait, only one clip at a time _ -> Nothing -- | This is a shorthand. Instead of setting @bwatch@ in @ReqWait@ -- and unsetting in all other requests, we call this once after -- executing a request. -- In game state, we collect the number of server requests pertaining -- to the actor (the number of actor's "moves"), through which -- the actor was waiting. processWatchfulness :: MonadServerAtomic m => Maybe Bool -> ActorId -> m () processWatchfulness mwait aid = do b <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid let uneasy = deltasSerious (bcalmDelta b) || not (calmEnough b actorMaxSk) case bwatch b of WWatch -> when (mwait == Just True) $ -- only long wait switches to wait state if Ability.getSk Ability.SkWait actorMaxSk >= 2 then do addCondition False IK.S_BRACED aid execUpdAtomic $ UpdWaitActor aid WWatch (WWait 1) else execUpdAtomic $ UpdWaitActor aid WWatch (WWait 0) WWait 0 -> case mwait of -- actor couldn't brace last time Just True -> return () -- if he still waits, keep him stuck unbraced _ -> execUpdAtomic $ UpdWaitActor aid (WWait 0) WWatch WWait n -> case mwait of Just True -> -- only proper wait prevents switching to watchfulness if n >= 500 then -- enough dozing to fall asleep if not uneasy -- won't wake up at once && canSleep actorMaxSk -- enough skills then do nAll <- removeConditionSingle IK.S_BRACED aid let !_A = assert (nAll == 0) () addSleep aid else -- Start dozing from scratch to prevent hopeless skill checks. execUpdAtomic $ UpdWaitActor aid (WWait n) (WWait 1) else -- Doze some more before checking sleep eligibility. execUpdAtomic $ UpdWaitActor aid (WWait n) (WWait $ n + 1) _ -> do nAll <- removeConditionSingle IK.S_BRACED aid let !_A = assert (nAll == 0 `blame` nAll) () execUpdAtomic $ UpdWaitActor aid (WWait n) WWatch WSleep -> if mwait /= Just False -- lurk can't wake up regardless; too short && (isNothing mwait -- not a wait || uneasy -- spooked || not (deltaBenign $ bhpDelta b)) -- any HP lost then execUpdAtomic $ UpdWaitActor aid WSleep WWake else execUpdAtomic $ UpdRefillHP aid 10000 -- no @xM@, so slow, but each turn HP gauge green; -- this is 1HP per 100 turns, so it's 10 times slower -- than a necklace that gives 1HP per 10 turns; -- so if an actor sleeps for the duration of a 1000 turns, -- which may be the time it takes to fully explore a level, -- 10HP would be gained, so weak actors would wake up twice over, -- which is fine: sleeping long enough to sidestep them at will, -- but attacking, e.g., a group with explosives, is good choice -- as well; so both stealth and mayhem fun correct tactically WWake -> unless (mwait == Just False) $ -- lurk can't wake up; too fast removeSleepSingle aid -- Even very primitive actors that can't pick up items can take over stash, -- to prevent them inadvertedly protecting enemy stash from skilled ones -- by standing over it (which AI tends to do). affectStash :: MonadServerAtomic m => Actor -> m () affectStash b = do let locateStash (fid, fact) = case gstash fact of Just (lidS, posS) | lidS == blid b && posS == bpos b && fid /= bfid b -> execUpdAtomic $ UpdLoseStashFaction True fid lidS posS _ -> return () factionD <- getsState sfactionD mapM_ locateStash $ EM.assocs factionD handleRequestTimed :: MonadServerAtomic m => FactionId -> ActorId -> RequestTimed -> m Bool handleRequestTimed fid aid cmd = do let mwait = checkWaiting cmd b <- getsState $ getActorBody aid -- Note that only the ordinary 1-turn wait eliminates overhead. -- The more fine-graned waits don't make actors braced and induce -- overhead, so that they have some drawbacks in addition to the -- benefit of seeing approaching danger up to almost a turn faster. -- It may be too late to block then, but not too late to sidestep or attack. unless (mwait == Just True) $ overheadActorTime fid (blid b) advanceTime aid (if mwait == Just False then 10 else 100) True handleRequestTimedCases aid cmd managePerRequest aid -- Note that due to the order, actor was still braced or sleeping -- throughout request processing, etc. So, if he hits himself kinetically, -- his armor from bracing previous turn is still in effect. processWatchfulness mwait aid return $! isNothing mwait -- for speed, we report if @cmd@ harmless -- | Clear deltas for Calm and HP for proper UI display and AI hints. managePerRequest :: MonadServerAtomic m => ActorId -> m () managePerRequest aid = do b <- getsState $ getActorBody aid affectStash b let clearMark = 0 unless (bcalmDelta b == ResDelta (0, 0) (0, 0)) $ -- Clear delta for the next actor move. execUpdAtomic $ UpdRefillCalm aid clearMark unless (bhpDelta b == ResDelta (0, 0) (0, 0)) $ -- Clear delta for the next actor move. execUpdAtomic $ UpdRefillHP aid clearMark handleRequestTimedCases :: MonadServerAtomic m => ActorId -> RequestTimed -> m () handleRequestTimedCases aid cmd = case cmd of ReqMove target -> reqMove aid target ReqMelee target iid cstore -> reqMelee aid target iid cstore ReqDisplace target -> reqDisplace aid target ReqAlter tpos -> reqAlter aid tpos ReqWait -> reqWait aid ReqWait10 -> reqWait10 aid ReqYell -> reqYell aid ReqMoveItems l -> reqMoveItems aid l ReqProject p eps iid cstore -> reqProject aid p eps iid cstore ReqApply iid cstore -> reqApply aid iid cstore switchLeader :: MonadServerAtomic m => FactionId -> ActorId -> m () {-# INLINE switchLeader #-} switchLeader fid aidNew = do fact <- getsState $ (EM.! fid) . sfactionD bPre <- getsState $ getActorBody aidNew let mleader = gleader fact !_A1 = assert (Just aidNew /= mleader && not (bproj bPre) `blame` (aidNew, bPre, fid, fact)) () !_A2 = assert (bfid bPre == fid `blame` "client tries to move other faction actors" `swith` (aidNew, bPre, fid, fact)) () let banned = bannedPointmanSwitchBetweenLevels fact arena <- case mleader of Nothing -> return $! blid bPre Just leader -> do b <- getsState $ getActorBody leader return $! blid b if blid bPre /= arena && banned -- catch the cheating clients then execFailure aidNew ReqWait{-hack-} NoChangeDunLeader else do execUpdAtomic $ UpdLeadFaction fid mleader (Just aidNew) -- We exchange times of the old and new leader. -- This permits an abuse, because a slow tank can be moved fast -- by alternating between it and many fast actors (until all of them -- get slowed down by this and none remain). But at least the sum -- of all times of a faction is conserved. And we avoid double moves -- against the UI player caused by his leader changes. There may still -- happen double moves caused by AI leader changes, but that's rare. -- The flip side is the possibility of multi-moves of the UI player -- as in the case of the tank. -- Warning: when the action is performed on the server, -- the time of the actor is different than when client prepared that -- action, so any client checks involving time should discount this. case mleader of Just aidOld | aidOld /= aidNew -> swapTime aidOld aidNew _ -> return () -- * ReqMove -- | Add a smell trace for the actor to the level. If smell already there -- and the actor can smell, remove smell. Projectiles are ignored. -- As long as an actor can smell, he doesn't leave any smell ever. -- Smell trace is never left in water tiles. affectSmell :: MonadServerAtomic m => ActorId -> m () affectSmell aid = do COps{coTileSpeedup} <- getsState scops b <- getsState $ getActorBody aid lvl <- getLevel $ blid b let aquatic = Tile.isAquatic coTileSpeedup $ lvl `at` bpos b unless (bproj b || aquatic) $ do actorMaxSk <- getsState $ getActorMaxSkills aid let smellRadius = Ability.getSk Ability.SkSmell actorMaxSk hasOdor = Ability.getSk Ability.SkOdor actorMaxSk > 0 when (hasOdor || smellRadius > 0) $ do localTime <- getsState $ getLocalTime $ blid b let oldS = fromMaybe timeZero $ EM.lookup (bpos b) . lsmell $ lvl newTime = timeShift localTime smellTimeout newS = if smellRadius > 0 then timeZero else newTime when (oldS /= newS) $ execUpdAtomic $ UpdAlterSmell (blid b) (bpos b) oldS newS -- | Actor moves or attacks or alters by bumping. -- Note that client may not be able to see an invisible monster -- so it's the server that determines if melee took place, etc. -- Also, only the server is authorized to check if a move is legal -- and it needs full context for that, e.g., the initial actor position -- to check if melee attack does not try to reach to a distant tile. reqMove :: MonadServerAtomic m => ActorId -> Vector -> m () reqMove = reqMoveGeneric True True reqMoveGeneric :: MonadServerAtomic m => Bool -> Bool -> ActorId -> Vector -> m () reqMoveGeneric voluntary mayAttack source dir = do COps{coTileSpeedup} <- getsState scops actorSk <- currentSkillsServer source sb <- getsState $ getActorBody source let abInSkill sk = isJust (btrajectory sb) || Ability.getSk sk actorSk > 0 lid = blid sb lvl <- getLevel lid let spos = bpos sb tpos = spos `shift` dir -- This predicate is symmetric wrt source and target, though the effect -- of collision may not be (the source projectiles applies its effect -- on the target particles, but loses 1 HP due to the collision). -- The condition implies that it's impossible to shoot down a bullet -- with a bullet, but a bullet can shoot down a burstable target, -- as well as be swept away by it, and two burstable projectiles -- burst when meeting mid-air. Projectiles that are not bursting -- nor damaging never collide with any projectile. collides <- getsState $ \s tb -> let sitemKind = getIidKindServer (btrunk sb) s titemKind = getIidKindServer (btrunk tb) s sar = sdiscoAspect s EM.! btrunk sb tar = sdiscoAspect s EM.! btrunk tb -- Such projectiles are prone to bursting or are themselves -- particles of an explosion shockwave. bursting arItem = IA.checkFlag Ability.Fragile arItem && IA.checkFlag Ability.Lobable arItem sbursting = bursting sar tbursting = bursting tar -- Such projectiles, even if not bursting themselves, can cause -- another projectile to burst. sdamaging = IK.isDamagingKind sitemKind tdamaging = IK.isDamagingKind titemKind -- Avoid explosion extinguishing itself via its own particles colliding. sameBlast = IA.checkFlag Ability.Blast sar && getIidKindIdServer (btrunk sb) s == getIidKindIdServer (btrunk tb) s in not sameBlast && (sbursting && (tdamaging || tbursting) || (tbursting && (sdamaging || sbursting))) -- We start by checking actors at the target position. tgt <- getsState $ posToAidAssocs tpos lid case tgt of (target, tb) : _ | mayAttack && (not (bproj sb) || not (bproj tb) || collides tb) -> do -- A projectile is too small and insubstantial to hit another projectile, -- unless it's large enough or tends to explode (fragile and lobable). -- The actor in the way is visible or not; server sees him always. -- Below the only weapon (the only item) of projectiles is picked. mweapon <- pickWeaponServer source target case mweapon of Just (wp, cstore) | abInSkill Ability.SkMelee -> reqMeleeChecked voluntary source target wp cstore _ -> return () -- waiting, even if no @SkWait@ skill -- Movement of projectiles only happens after melee and a check -- if they survive, so that if they don't, they explode in front -- of enemy, not under him, so that already first explosion blasts -- reach him, not only potential secondary explosions. when (bproj sb) $ do b2 <- getsState $ getActorBody source unless (actorDying b2) $ reqMoveGeneric voluntary False source dir _ -> -- Either the position is empty, or all involved actors are proj. -- Movement requires full access and skill. if Tile.isWalkable coTileSpeedup $ lvl `at` tpos then if abInSkill Ability.SkMove then do execUpdAtomic $ UpdMoveActor source spos tpos affectSmell source -- No remote ransacking nor underfoot effects by projectiles, -- through which a projectile could cook its only item, -- but retain the old raw name and which would spam water -- slowness every time a projectile flies over water. unless (bproj sb) $ -- Counts as bumping, because terrain transformation probably -- not intended, because the goal was probably just to move -- and then modifying the terrain is an unwelcome side effect. -- Barged into a tile, so normal effects need to activate, -- while crafting requires explicit altering. void $ reqAlterFail True EffBare voluntary source tpos else execFailure source (ReqMove dir) MoveUnskilled else do -- If not walkable, this must be altering by bumping. -- If voluntary then probably intentional so report any errors. mfail <- reqAlterFail True EffBare voluntary source tpos when voluntary $ do let req = ReqMove dir maybe (return ()) (execFailure source req) mfail -- * ReqMelee -- | Resolves the result of an actor moving into another. -- Actors on unwalkable positions can be attacked without any restrictions. -- For instance, an actor embedded in a wall can be attacked from -- an adjacent position. This function is analogous to projectGroupItem, -- but for melee and not using up the weapon. -- No problem if there are many projectiles at the spot. We just -- attack the one specified. reqMelee :: MonadServerAtomic m => ActorId -> ActorId -> ItemId -> CStore -> m () reqMelee source target iid cstore = do actorSk <- currentSkillsServer source if Ability.getSk Ability.SkMelee actorSk > 0 then reqMeleeChecked True source target iid cstore else execFailure source (ReqMelee target iid cstore) MeleeUnskilled reqMeleeChecked :: forall m. MonadServerAtomic m => Bool -> ActorId -> ActorId -> ItemId -> CStore -> m () reqMeleeChecked voluntary source target iid cstore = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target discoAspect <- getsState sdiscoAspect let req = ReqMelee target iid cstore arWeapon = discoAspect EM.! iid meleeableEnough = bproj sb || IA.checkFlag Ability.Meleeable arWeapon if source == target then execFailure source req MeleeSelf else if not (checkAdjacent sb tb) then execFailure source req MeleeDistant else if not meleeableEnough then execFailure source req MeleeNotWeapon else do -- If @voluntary@ is set, blame is exact, otherwise, an approximation. killer <- if | voluntary -> assert (not (bproj sb)) $ return source | bproj sb -> getsServer $ EM.findWithDefault source source . strajPushedBy | otherwise -> return source actorSk <- currentSkillsServer source let arTrunk = discoAspect EM.! btrunk tb sfid = bfid sb tfid = bfid tb -- Let the missile drop down, but don't remove its trajectory -- so that it doesn't pretend to have hit a wall. haltTrajectory :: KillHow -> ActorId -> Actor -> m () haltTrajectory killHow aid b = case btrajectory b of btra@(Just (l, speed)) | not $ null l -> do execUpdAtomic $ UpdTrajectory aid btra $ Just ([], speed) let arTrunkAid = discoAspect EM.! btrunk b when (bproj b && not (IA.checkFlag Ability.Blast arTrunkAid)) $ addKillToAnalytics killer killHow (bfid b) (btrunk b) _ -> return () -- Only catch if braced. Never steal trunk from an already caught -- projectile or one with many items inside. if bproj tb && EM.size (beqp tb) == 1 && not (IA.checkFlag Ability.Blast arTrunk) && actorWaits sb -- still valid while request being processed && Ability.getSk Ability.SkMoveItem actorSk > 0 -- animals can't then do -- Catching the projectile, that is, stealing the item from its eqp. -- No effect from our weapon (organ) is applied to the projectile -- and the weapon (organ) is never destroyed, even if not durable. -- Pushed actor doesn't stop flight by catching the projectile -- nor does he lose 1HP. -- This is not overpowered, because usually at least one partial wait -- is needed to sync (if not, attacker should switch missiles) -- and so only every other missile can be caught. Normal sidestepping -- or sync and displace, if in a corridor, is as effective -- and blocking can be even more so, depending on powers of the missile. -- Missiles are really easy to defend against, but sight (and so, Calm) -- is the key, as well as light, ambush around a corner, etc. execSfxAtomic $ SfxSteal source target iid case EM.assocs $ beqp tb of [(iid2, (k, _))] -> do upds <- generalMoveItem True iid2 k (CActor target CEqp) (CActor source CStash) mapM_ execUpdAtomic upds itemFull <- getsState $ itemToFull iid2 discoverIfMinorEffects (CActor source CStash) iid2 (itemKindId itemFull) err -> error $ "" `showFailure` err haltTrajectory KillCatch target tb else do if bproj sb && bproj tb then do -- Special case for collision of projectiles, because they just -- symmetrically ram into each other, so picking one to hit another, -- based on random timing, would be wrong. -- Instead of suffering melee attack, let the target projectile -- get smashed and burst (if fragile and if not piercing). -- The source projectile terminates flight (unless pierces) later on. when (bhp tb > oneM) $ execUpdAtomic $ UpdRefillHP target minusM when (bhp tb <= oneM) $ do -- If projectile has too low HP to pierce, terminate its flight. let killHow | IA.checkFlag Ability.Blast arWeapon = KillKineticBlast | otherwise = KillKineticRanged haltTrajectory killHow target tb -- Avoid spam when two explosions collide. unless (IA.checkFlag Ability.Blast arWeapon && IA.checkFlag Ability.Blast arTrunk) $ execSfxAtomic $ SfxStrike source target iid else do -- Normal hit, with effects, but first auto-apply defences. let mayDestroyTarget = not (bproj tb) || bhp tb <= oneM effApplyFlagsTarget = EffApplyFlags { effToUse = EffBare , effVoluntary = voluntary , effUseAllCopies = False , effKineticPerformed = False , effActivation = if bproj sb then Ability.ActivationUnderRanged else Ability.ActivationUnderMelee , effMayDestroy = mayDestroyTarget } unless (bproj tb) $ autoApply effApplyFlagsTarget killer target tb $ if bproj sb then Ability.UnderRanged else Ability.UnderMelee -- This might have changed the actors. sb2 <- getsState $ getActorBody source targetMaxSk <- getsState $ getActorMaxSkills target if | bproj sb2 && Ability.getSk Ability.SkDeflectRanged targetMaxSk > 0 -> do cutCalm target execSfxAtomic $ SfxRecoil source target iid | Ability.getSk Ability.SkDeflectMelee targetMaxSk > 0 -> do cutCalm target execSfxAtomic $ SfxRecoil source target iid | otherwise -> do -- Msgs inside @SfxStrike@ describe the source part -- of the strike. execSfxAtomic $ SfxStrike source target iid let c = CActor source cstore mayDestroySource = not (bproj sb2) || bhp sb2 <= oneM -- piercing projectiles may not have their weapon destroyed -- Msgs inside @itemEffect@ describe the target part -- of the strike. -- If any effects and aspects, this is also where they are -- identified. -- Here also the kinetic damage is applied, -- before any effects are. -- -- Note: that "hornet swarm detect items" via a scrolls -- is intentional, -- even though unrealistic and funny. Otherwise actors -- could protect -- themselves from some projectiles by lowering their apply stat. -- Also, the animal faction won't have too much benefit -- from that info, -- so the problem is not balance, but the goofy message. let effApplyFlagsSource = EffApplyFlags { effToUse = EffBare , effVoluntary = voluntary , effUseAllCopies = False , effKineticPerformed = False , effActivation = Ability.ActivationMeleeable , effMayDestroy = mayDestroySource } void $ kineticEffectAndDestroy effApplyFlagsSource killer source target iid c sb2 <- getsState $ getActorBody source case btrajectory sb2 of Just{} | not voluntary -> do -- Deduct a hitpoint for a pierce of a projectile -- or due to a hurled actor colliding with another (seen from -- @voluntary@, as opposed to hurled actor actively meleeing another). -- Don't deduct if no pierce, to prevent spam. -- Never kill in this way. when (bhp sb2 > oneM) $ do execUpdAtomic $ UpdRefillHP source minusM unless (bproj sb2) $ do execSfxAtomic $ SfxMsgFid (bfid sb2) $ SfxCollideActor source target unless (bproj tb) $ execSfxAtomic $ SfxMsgFid (bfid tb) $ SfxCollideActor source target when (not (bproj sb2) || bhp sb2 <= oneM) $ -- Non-projectiles can't pierce, so terminate their flight. -- If projectile has too low HP to pierce, ditto. haltTrajectory KillActorLaunch source sb2 _ -> return () -- The only way to start a war is to slap an enemy voluntarily.. -- Being hit by and hitting projectiles, as well as via pushing, -- count as unintentional friendly fire. sfact <- getsState $ (EM.! sfid) . sfactionD let friendlyFire = bproj sb2 || bproj tb || not voluntary fromDipl = EM.findWithDefault Unknown tfid (gdipl sfact) unless (friendlyFire || isFoe sfid sfact tfid -- already at war || isFriend sfid sfact tfid) $ -- allies never at war execUpdAtomic $ UpdDiplFaction sfid tfid fromDipl War autoApply :: MonadServerAtomic m => EffApplyFlags -> ActorId -> ActorId -> Actor -> Ability.Flag -> m () autoApply effApplyFlags killer target tb flag = do let autoApplyIid c iid = do itemFull <- getsState $ itemToFull iid let arItem = aspectRecordFull itemFull when (IA.checkFlag flag arItem) $ void $ effectAndDestroyAndAddKill effApplyFlags killer target target iid c itemFull mapM_ (autoApplyIid $ CActor target CEqp) $ EM.keys $ beqp tb mapM_ (autoApplyIid $ CActor target COrgan) $ EM.keys $ borgan tb -- * ReqDisplace -- | Actor tries to swap positions with another. reqDisplace :: MonadServerAtomic m => ActorId -> ActorId -> m () reqDisplace = reqDisplaceGeneric True reqDisplaceGeneric :: MonadServerAtomic m => Bool -> ActorId -> ActorId -> m () reqDisplaceGeneric voluntary source target = do COps{coTileSpeedup} <- getsState scops actorSk <- currentSkillsServer source sb <- getsState $ getActorBody source let abInSkill sk = isJust (btrajectory sb) || Ability.getSk sk actorSk > 0 tb <- getsState $ getActorBody target tfact <- getsState $ (EM.! bfid tb) . sfactionD let spos = bpos sb tpos = bpos tb atWar = isFoe (bfid tb) tfact (bfid sb) req = ReqDisplace target actorMaxSk <- getsState $ getActorMaxSkills target dEnemy <- getsState $ dispEnemy source target actorMaxSk if | not (abInSkill Ability.SkDisplace) -> execFailure source req DisplaceUnskilled | not (checkAdjacent sb tb) -> execFailure source req DisplaceDistant | atWar && not dEnemy -> do -- if not at war, can displace always -- We don't fail with DisplaceImmobile and DisplaceSupported. -- because it's quite common they can't be determined by the attacker, -- and so the failure would be too alarming to the player. -- If the character melees instead, the player can tell displace failed. -- As for the other failures, they are impossible and we don't -- verify here that they don't occur, for simplicity. mweapon <- pickWeaponServer source target case mweapon of Just (wp, cstore) | abInSkill Ability.SkMelee -> reqMeleeChecked voluntary source target wp cstore _ -> return () -- waiting, even if no @SkWait@ skill | otherwise -> do let lid = blid sb lvl <- getLevel lid -- Displacing requires full access. if Tile.isWalkable coTileSpeedup $ lvl `at` tpos then case posToAidsLvl tpos lvl of [] -> error $ "" `showFailure` (source, sb, target, tb) [_] -> do execUpdAtomic $ UpdDisplaceActor source target -- We leave or wipe out smell, for consistency, but it's not -- absolute consistency, e.g., blinking doesn't touch smell, -- so sometimes smellers will backtrack once to wipe smell. OK. affectSmell source affectSmell target -- Counts as bumping, because terrain transformation not intended. void $ reqAlterFail True EffBare False source tpos -- possibly alter or activate void $ reqAlterFail True EffBare False target spos _ -> execFailure source req DisplaceMultiple else -- Client foolishly tries to displace an actor without access. execFailure source req DisplaceAccess -- * ReqAlter -- | Search and/or alter the tile. reqAlter :: MonadServerAtomic m => ActorId -> Point -> m () reqAlter source tpos = do COps{coTileSpeedup} <- getsState scops sb <- getsState $ getActorBody source lvl <- getLevel $ blid sb -- This is explicit tile triggering. Walkable tiles are sparse enough -- that crafting effects can be activated without any others -- and without changing the tile and this is usually beneficial, -- so always attempted. OTOH, squeezing a hand into a non-walkable tile -- or barging into walkable tiles (but not as a projectile) activates all. let effToUse = if Tile.isWalkable coTileSpeedup (lvl `at` tpos) then EffOnCombine else EffBareAndOnCombine mfail <- reqAlterFail False effToUse True source tpos let req = ReqAlter tpos maybe (return ()) (execFailure source req) mfail reqAlterFail :: forall m. MonadServerAtomic m => Bool -> EffToUse -> Bool -> ActorId -> Point -> m (Maybe ReqFailure) reqAlterFail bumping effToUse voluntary source tpos = do cops@COps{cotile, coTileSpeedup, corule} <- getsState scops sb <- getsState $ getActorBody source actorMaxSk <- getsState $ getActorMaxSkills source let calmE = calmEnough sb actorMaxSk lid = blid sb sClient <- getsServer $ (EM.! bfid sb) . sclientStates itemToF <- getsState $ flip itemToFull actorSk <- currentSkillsServer source localTime <- getsState $ getLocalTime lid embeds <- getsState $ getEmbedBag lid tpos lvl <- getLevel lid getKind <- getsState $ flip getIidKindServer let serverTile = lvl `at` tpos lvlClient = (EM.! lid) . sdungeon $ sClient clientTile = lvlClient `at` tpos hiddenTile = Tile.hideAs cotile serverTile alterSkill = Ability.getSk Ability.SkAlter actorSk tileMinSkill = Tile.alterMinSkill coTileSpeedup serverTile revealEmbeds = unless (EM.null embeds) $ execUpdAtomic $ UpdSpotItemBag True (CEmbed lid tpos) embeds embedKindList = map (\(iid, kit) -> (getKind iid, (iid, kit))) (EM.assocs embeds) sbItemKind = getKind $ btrunk sb -- Prevent embeds triggering each other's exploding embeds -- via feeble mists, in the worst case, in a loop. However, -- if a tile can be changed with an item (e.g., the mist trunk) -- but without activating embeds, mists do fine. projNoDamage = bproj sb && not (IK.isDamagingKind sbItemKind) tryApplyEmbed (iid, kit) = do let itemFull = itemToF iid -- Let even completely apply-unskilled actors trigger basic embeds. -- See the note about no skill check when melee triggers effects. legal = permittedApply corule localTime maxBound calmE Nothing itemFull kit case legal of Left ApplyNoEffects -> return UseDud -- pure flavour embed Left reqFail -> do -- The failure is fully expected, because client may choose -- to trigger some embeds, knowing that others won't fire. execSfxAtomic $ SfxMsgFid (bfid sb) $ SfxExpectedEmbed iid lid reqFail return UseDud _ -> itemEffectEmbedded effToUse voluntary source lid tpos iid -- when @effToUse == EffOnCombine@, terrain, e.g., fire, -- may be removed safely, without adverse effects -- by crafting, even any silly crafting as an exploit; OK underFeet = tpos == bpos sb -- if enter and alter, be more permissive blockedByItem = EM.member tpos (lfloor lvl) if chessDist tpos (bpos sb) > 1 then return $ Just AlterDistant else if Just clientTile == hiddenTile then -- searches -- Only non-projectile actors with SkAlter > 1 can search terrain. -- Even projectiles with large altering bonuses can't. if bproj sb || not underFeet && alterSkill <= 1 then return $ Just AlterUnskilled -- don't leak about searching else do -- Blocking by items nor actors does not prevent searching. -- Searching broadcasted, in case actors from other factions are present -- so that they can learn the tile and learn our action. -- If they already know the tile, they will just consider our action -- a waste of time and ignore the command. execUpdAtomic $ UpdSearchTile source tpos serverTile -- Searching also reveals the embedded items of the tile. -- If the items are already seen by the client -- (e.g., due to item detection, despite tile being still hidden), -- the command is ignored on the client. revealEmbeds -- If the entries are already seen by the client -- the command is ignored on the client. case EM.lookup tpos $ lentry lvl of Nothing -> return () Just entry -> execUpdAtomic $ UpdSpotEntry lid [(tpos, entry)] -- Seaching triggers the embeds as well, after they are revealed. -- The rationale is that the items were all the time present -- (just invisible to the client), so they need to be triggered. -- The exception is changable tiles, because they are not so easy -- to trigger; they need previous or subsequent altering. unless (Tile.isModifiable coTileSpeedup serverTile || projNoDamage) $ mapM_ (void <$> tryApplyEmbed) (sortEmbeds cops serverTile embedKindList) return Nothing -- searching is always success else -- Here either @clientTile == serverTile@ or the client -- is misguided re tile at that position, e.g., it is a projectile -- that can't see the tile and the tile was not revealed so far. -- In either case, try to alter the tile. If the messages -- are confusing, that's fair, situation is confusing. if not (bproj sb || underFeet) -- no global skill check in these cases && alterSkill < tileMinSkill then return $ Just AlterUnskilled -- don't leak about altering else do -- Save the original content of ground and eqp to abort transformations -- if any item is removed, possibly an item intended as the fuel. groundBag <- getsState $ getBodyStoreBag sb CGround eqpBag <- getsState $ getBodyStoreBag sb CEqp -- Compute items to use for transformation early, before any extra -- items added by activated embeds, to use only intended items as fuel. -- Use even unidentified items --- one more way to id by use. kitAssG <- getsState $ kitAssocs source [CGround] kitAssE <- getsState $ kitAssocs source [CEqp] let kitAss = listToolsToConsume kitAssG kitAssE announceTileChange = -- If no embeds and the only thing that happens is the change -- of the tile, don't display a message, because the change -- is visible on the map (unless it changes into itself) -- and there's nothing more to speak about. -- However, even with embeds, don't spam if wading through -- terrain and changing it each step. unless (underFeet || EM.null embeds) $ execSfxAtomic $ SfxTrigger source lid tpos serverTile changeTo tgroup = do -- No @SfxAlter@, because the effect is obvious (e.g., opened door). let nightCond kt = not (Tile.kindHasFeature TK.Walkable kt && Tile.kindHasFeature TK.Clear kt) || (if lnight lvl then id else not) (Tile.kindHasFeature TK.Dark kt) -- Sometimes the tile is determined precisely by the ambient light -- of the source tiles. If not, default to cave day/night condition. mtoTile <- rndToAction $ opick cotile tgroup nightCond toTile <- maybe (rndToAction $ fromMaybe (error $ "" `showFailure` tgroup) <$> opick cotile tgroup (const True)) return mtoTile embeds2 <- getsState $ getEmbedBag lid tpos let newHasEmbeds = Tile.isEmbed coTileSpeedup toTile -- Don't regenerate same tile, unless it had embeds, but all spent. when (serverTile /= toTile || EM.null embeds2 && newHasEmbeds) $ do -- At most one of these two will be accepted on any given client. when (serverTile /= toTile) $ execUpdAtomic $ UpdAlterTile lid tpos serverTile toTile -- This case happens when a client does not see a searching -- action by another faction, but sees the subsequent altering -- or if another altering takes place in between. case hiddenTile of Just tHidden -> execUpdAtomic $ UpdAlterTile lid tpos tHidden toTile Nothing -> return () -- @UpdAlterExplorable@ is received by any client regardless -- of whether the alteration was seen and how. case (Tile.isExplorable coTileSpeedup serverTile, Tile.isExplorable coTileSpeedup toTile) of (False, True) -> execUpdAtomic $ UpdAlterExplorable lid 1 (True, False) -> execUpdAtomic $ UpdAlterExplorable lid (-1) _ -> return () -- At the end we replace old embeds (even if partially used up) -- with new ones. -- If the source tile was hidden, the items could not be visible -- on a client, in which case the command would be ignored -- on the client, without causing any problems. Otherwise, -- if the position is in view, client has accurate info. unless (EM.null embeds2) $ execUpdAtomic $ UpdLoseItemBag True (CEmbed lid tpos) embeds2 -- Altering always reveals the outcome tile, so it's not hidden -- and so its embedded items are always visible. embedItemOnPos lid tpos toTile tryChangeWith :: ( [(Int, GroupName IK.ItemKind)] , GroupName TK.TileKind ) -> m Bool tryChangeWith (tools0, tgroup) = do let grps0 = map (\(x, y) -> (False, x, y)) tools0 -- apply if durable (bagsToLose, iidsToApply, grps) = foldl' subtractIidfromGrps (EM.empty, [], grps0) kitAss if null grps then do announceTileChange -- first the result is foretold consumeItems source bagsToLose iidsToApply -- then the cost changeTo tgroup -- then result is seen return True else return False feats = TK.tfeature $ okind cotile serverTile tileActions = mapMaybe (parseTileAction (bproj sb) (underFeet || blockedByItem) -- avoids AlterBlockItem embedKindList) feats groupWithFromAction action = case action of WithAction grps _ | not bumping -> Just grps _ -> Nothing groupsToAlterWith = mapMaybe groupWithFromAction tileActions processTileActions :: Maybe UseResult -> [TileAction] -> m Bool processTileActions museResult [] = return $! maybe False (/= UseDud) museResult processTileActions museResult (ta : rest) = case ta of EmbedAction (iid, kit) -> -- Embeds are activated in the order in tile definition -- and never after the tile is changed. -- If any embedded item was present and processed, -- but none was triggered, both free and item-consuming terrain -- alteration is disabled. The exception is projectiles -- not being able to process embeds due to skill required, -- which does not block future terrain alteration. -- Skill check for non-projectiles is performed much earlier. -- All projectiles have 0 skill for the purpose of embed -- activation, regardless of their trunk. if | bproj sb && tileMinSkill > 0 -> -- local skill check processTileActions museResult rest -- not blocking future terrain altering, e.g., oil mist -- not slowed over water tile that has @talter@ equal to 2, -- but able to change it into oil spill soon after | projNoDamage -> processTileActions (Just UseDud) rest -- projectiles having enough skill, but no damage, -- not only can't activate embeds, but block future -- terrain altering, e.g., oil mist not puncturing -- a barrel and causing explosion, and so also -- not causing it to disappear later on | otherwise -> do -- here falls the case of fragmentation blast puncturing -- a barrel and so causing an explosion triggered <- tryApplyEmbed (iid, kit) let useResult = fromMaybe UseDud museResult processTileActions (Just $ max useResult triggered) rest -- max means that even one activated embed is enough -- to alter terrain in a future action ToAction tgroup -> assert (not (bproj sb)) $ -- @parseTileAction@ ensures the above assertion -- so that projectiles never cause normal transitions and, -- e.g., mists douse fires or two flames thrown, first ignites, -- second douses immediately afterwards if maybe True (== UseUp) museResult then do announceTileChange changeTo tgroup return True else processTileActions museResult rest WithAction grps tgroup -> do -- Note that there is no skill check if the source actors -- is a projectile. Permission is conveyed in @ProjYes@ instead. groundBag2 <- getsState $ getBodyStoreBag sb CGround eqpBag2 <- getsState $ getBodyStoreBag sb CEqp if (not bumping || null grps) -- 'M' confirmation needed to consume items, bump not enough && (bproj sb || voluntary || null grps) -- consume only if voluntary or released as projectile && (maybe True (== UseUp) museResult || effToUse == EffOnCombine) -- unwanted crafting shouldn't block transformations && let f (k1, _) (k2, _) = k1 <= k2 in EM.isSubmapOfBy f groundBag groundBag2 && EM.isSubmapOfBy f eqpBag eqpBag2 -- don't transform if items, possibly intended for -- transformation, removed; also when only crafting -- was intended, which almost always removes some items then do altered <- tryChangeWith (grps, tgroup) if altered then return True else processTileActions museResult rest else processTileActions museResult rest -- Note that stray embedded items (not from tile content definition) -- are never activated. if null tileActions then return $! if blockedByItem && not underFeet && Tile.isModifiable coTileSpeedup serverTile then Just AlterBlockItem -- likely cause else Just AlterNothing -- can't do; silly client; fail else if underFeet || not (occupiedBigLvl tpos lvl) && not (occupiedProjLvl tpos lvl) then do -- The items are first revealed for the sake of clients that -- may see the tile as hidden. Note that the tile is not revealed -- (unless it's altered later on, in which case the new one is). revealEmbeds tileTriggered <- processTileActions Nothing tileActions let potentiallyMissing = filter (not . null) groupsToAlterWith when (not tileTriggered && not underFeet && voluntary && not (null potentiallyMissing)) $ execSfxAtomic $ SfxMsgFid (bfid sb) $ SfxNoItemsForTile potentiallyMissing return Nothing -- altered as much as items allowed; success else return $ Just AlterBlockActor -- * ReqWait -- | Do nothing. Wait skill 1 required. Bracing requires 2, sleep 3, lurking 4. -- -- Something is sometimes done in 'processWatchfulness'. reqWait :: MonadServerAtomic m => ActorId -> m () {-# INLINE reqWait #-} reqWait source = do actorSk <- currentSkillsServer source unless (Ability.getSk Ability.SkWait actorSk > 0) $ execFailure source ReqWait WaitUnskilled -- * ReqWait10 -- | Do nothing. -- -- Something is sometimes done in 'processWatchfulness'. reqWait10 :: MonadServerAtomic m => ActorId -> m () {-# INLINE reqWait10 #-} reqWait10 source = do actorSk <- currentSkillsServer source unless (Ability.getSk Ability.SkWait actorSk >= 4) $ execFailure source ReqWait10 WaitUnskilled -- * ReqYell -- | Yell/yawn/stretch/taunt. -- Wakes up (gradually) from sleep. Causes noise heard by enemies on the level -- even if out of their hearing range. -- -- Governed by the waiting skill (because everyone is supposed to have it). -- unlike @ReqWait@, induces overhead. -- -- This is similar to the effect @Yell@, but always voluntary. reqYell :: MonadServerAtomic m => ActorId -> m () reqYell aid = do actorSk <- currentSkillsServer aid if | Ability.getSk Ability.SkWait actorSk > 0 -> -- Last yawn before waking up is displayed as a yell, but that's fine. -- To fix that, we'd need to move the @SfxTaunt@ -- to @processWatchfulness@. execSfxAtomic $ SfxTaunt True aid | Ability.getSk Ability.SkMove actorSk <= 0 || Ability.getSk Ability.SkDisplace actorSk <= 0 || Ability.getSk Ability.SkMelee actorSk <= 0 -> -- Potentially, only waiting is possible, so given that it's drained, -- don't let the actor be stuck nor alarm about server failure. execSfxAtomic $ SfxTaunt False aid | otherwise -> do -- In most situation one of the 3 actions above -- can be performed and waiting skill is not needed for that, -- so given the 3 skills are available, waste turn, waiting until -- they can be performed, but don't alarm, because it does happen -- sometimes in crowds. No bracing granted, either, but mark -- waiting so that AI knows to change leader. -- execFailure aid ReqYell YellUnskilled b <- getsState $ getActorBody aid case bwatch b of WWait _ -> return () _ -> execUpdAtomic $ UpdWaitActor aid (bwatch b) (WWait 0) -- * ReqMoveItems reqMoveItems :: MonadServerAtomic m => ActorId -> [(ItemId, Int, CStore, CStore)] -> m () reqMoveItems source l = do actorSk <- currentSkillsServer source if Ability.getSk Ability.SkMoveItem actorSk > 0 then do b <- getsState $ getActorBody source actorMaxSk <- getsState $ getActorMaxSkills source -- Server accepts item movement based on calm at the start, not end -- or in the middle, to avoid interrupted or partially ignored commands. let calmE = calmEnough b actorMaxSk case l of [] -> execFailure source (ReqMoveItems l) ItemNothing iid : rest -> do reqMoveItem False source calmE iid -- Dropping previous may destroy next items. mapM_ (reqMoveItem True source calmE) rest else execFailure source (ReqMoveItems l) MoveItemUnskilled reqMoveItem :: MonadServerAtomic m => Bool -> ActorId -> Bool -> (ItemId, Int, CStore, CStore) -> m () reqMoveItem absentPermitted aid calmE (iid, kOld, fromCStore, toCStore) = do b <- getsState $ getActorBody aid let fromC = CActor aid fromCStore req = ReqMoveItems [(iid, kOld, fromCStore, toCStore)] toC <- case toCStore of CGround -> pickDroppable False aid b -- drop over fog, etc. _ -> return $! CActor aid toCStore bagFrom <- getsState $ getContainerBag (CActor aid fromCStore) bagBefore <- getsState $ getContainerBag toC -- The effect of dropping previous items from this series may have -- increased or decreased the number of this item. let k = min kOld $ fst $ EM.findWithDefault (0, []) iid bagFrom let !_A = absentPermitted || k == kOld if | absentPermitted && k == 0 -> return () | k < 1 || fromCStore == toCStore -> execFailure aid req ItemNothing | fromCStore == CEqp && not calmE -> execFailure aid req ItemNotCalm | toCStore == CEqp && not calmE -> execFailure aid req ItemNotCalm | toCStore == CEqp && eqpOverfull b k -> execFailure aid req EqpOverfull | otherwise -> do upds <- generalMoveItem True iid k fromC toC mapM_ execUpdAtomic upds itemFull <- getsState $ itemToFull iid -- Let any item manipulation attempt to identify, in case the item -- got into stash, e.g., by being thrown at the stash location, -- and gets identified only when equipped or dropped and picked up again. discoverIfMinorEffects toC iid (itemKindId itemFull) -- The first recharging period after equipping is random, -- between 1 and 2 standard timeouts of the item. -- Timeouts for items in shared stash are not consistent wrt the actor's -- local time, because actors from many levels put items there -- all the time (and don't rebase it to the clock of the stash's level). -- If wrong local time in shared stash causes an item to recharge -- for a very long time wrt actor on some level, -- the player can reset it by dropping the item and picking up again -- (as a flip side, a charging item in stash may sometimes -- be used at once on another level, with different local time, but only -- once, because after first use, the timeout is set to local time). -- This is not terribly consistent, but not recharging in stash is -- not better, because either we block activation of any items with timeout, -- or encourage moving items out of stash, recharging and moving in. -- Which is not fun at all, but one more thing to remember doing regularly. when (toCStore `elem` [CEqp, COrgan] && fromCStore `notElem` [CEqp, COrgan] || fromCStore == CStash) $ do let beforeIt = case iid `EM.lookup` bagBefore of Nothing -> [] -- no such items before move Just (_, it2) -> it2 randomResetTimeout k iid itemFull beforeIt toC -- * ReqProject reqProject :: MonadServerAtomic m => ActorId -- ^ actor projecting the item (is on current lvl) -> Point -- ^ target position of the projectile -> Int -- ^ digital line parameter -> ItemId -- ^ the item to be projected -> CStore -- ^ which store the items comes from -> m () reqProject source tpxy eps iid cstore = do let req = ReqProject tpxy eps iid cstore b <- getsState $ getActorBody source curChalSer <- getsServer $ scurChalSer . soptions fact <- getsState $ (EM.! bfid b) . sfactionD actorMaxSk <- getsState $ getActorMaxSkills source let calmE = calmEnough b actorMaxSk if | ckeeper curChalSer && fhasUI (gkind fact) -> execFailure source req ProjectFinderKeeper | cstore == CEqp && not calmE -> execFailure source req ItemNotCalm | otherwise -> do mfail <- projectFail source source (bpos b) tpxy eps False iid cstore False maybe (return ()) (execFailure source req) mfail -- * ReqApply reqApply :: MonadServerAtomic m => ActorId -- ^ actor applying the item (is on current level) -> ItemId -- ^ the item to be applied -> CStore -- ^ the location of the item -> m () reqApply aid iid cstore = do COps{corule} <- getsState scops let req = ReqApply iid cstore b <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid let calmE = calmEnough b actorMaxSk if cstore == CEqp && not calmE then execFailure aid req ItemNotCalm else do bag <- getsState $ getBodyStoreBag b cstore case EM.lookup iid bag of Nothing -> execFailure aid req ApplyOutOfReach Just kit -> do itemFull <- getsState $ itemToFull iid actorSk <- currentSkillsServer aid localTime <- getsState $ getLocalTime (blid b) let skill = Ability.getSk Ability.SkApply actorSk legal = permittedApply corule localTime skill calmE (Just cstore) itemFull kit case legal of Left reqFail -> execFailure aid req reqFail Right _ -> applyItem aid iid cstore -- * ReqGameRestart reqGameRestart :: MonadServerAtomic m => ActorId -> GroupName ModeKind -> Challenge -> m () reqGameRestart aid groupName scurChalSer = do noConfirmsGame <- isNoConfirmsGame factionD <- getsState sfactionD let fidsUI = map fst $ filter (\(_, fact) -> fhasUI (gkind fact)) (EM.assocs factionD) -- This call to `revealItems` and `revealPerception` is really needed, -- because the other happens only at natural game conclusion, -- not at forced quitting. unless noConfirmsGame $ mapM_ revealAll fidsUI -- Announcing end of game, we send lore, because game is over. b <- getsState $ getActorBody aid oldSt <- getsState $ gquit . (EM.! bfid b) . sfactionD factionAn <- getsServer sfactionAn generationAn <- getsServer sgenerationAn execUpdAtomic $ UpdQuitFaction (bfid b) oldSt (Just $ Status Restart (fromEnum $ blid b) (Just groupName)) (Just (factionAn, generationAn)) -- We don't save game and don't wait for clips end. ASAP. modifyServer $ \ser -> ser { sbreakASAP = True , soptionsNxt = (soptionsNxt ser) {scurChalSer} } -- * ReqGameDropAndExit -- After we break out of the game loop, we will notice from @Camping@ -- we shouldn exit the game. reqGameDropAndExit :: MonadServerAtomic m => ActorId -> m () reqGameDropAndExit aid = do verifyAssertExplored b <- getsState $ getActorBody aid oldSt <- getsState $ gquit . (EM.! bfid b) . sfactionD execUpdAtomic $ UpdQuitFaction (bfid b) oldSt (Just $ Status Camping (fromEnum $ blid b) Nothing) Nothing modifyServer $ \ser -> ser { sbreakASAP = True , sbreakLoop = True } verifyAssertExplored :: MonadServer m => m () verifyAssertExplored = do assertExplored <- getsServer $ sassertExplored . soptions case assertExplored of Nothing -> return () Just lvlN -> do -- Exploration (by any party) verfied via spawning; beware of levels -- with disabled spawning. snumSpawned <- getsServer snumSpawned let !_A = assert (toEnum lvlN `EM.member` snumSpawned || toEnum (- lvlN) `EM.member` snumSpawned `blame` "by game end, exploration haven't reached the expected level depth, indicating stuck AI (or just very busy initial levels)" `swith` lvlN) () return () -- * ReqGameSaveAndExit -- After we break out of the game loop, we will notice from @Camping@ -- we shouldn exit the game. reqGameSaveAndExit :: MonadServerAtomic m => ActorId -> m () reqGameSaveAndExit aid = do verifyAssertExplored b <- getsState $ getActorBody aid oldSt <- getsState $ gquit . (EM.! bfid b) . sfactionD execUpdAtomic $ UpdQuitFaction (bfid b) oldSt (Just $ Status Camping (fromEnum $ blid b) Nothing) Nothing modifyServer $ \ser -> ser { sbreakASAP = True , swriteSave = True } -- * ReqGameSave -- After we break out of the game loop, we will notice we shouldn't quit -- the game and we will enter the game loop again. reqGameSave :: MonadServer m => m () reqGameSave = modifyServer $ \ser -> ser { sbreakASAP = True , swriteSave = True } -- * ReqDoctrine reqDoctrine :: MonadServerAtomic m => FactionId -> Ability.Doctrine -> m () reqDoctrine fid toT = do fromT <- getsState $ gdoctrine . (EM.! fid) . sfactionD execUpdAtomic $ UpdDoctrineFaction fid toT fromT -- * ReqAutomate reqAutomate :: MonadServerAtomic m => FactionId -> m () reqAutomate fid = execUpdAtomic $ UpdAutoFaction fid True LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/ItemM.hs0000644000000000000000000003236507346545000021567 0ustar0000000000000000-- | Server operations for items. module Game.LambdaHack.Server.ItemM ( registerItem, moveStashIfNeeded, randomResetTimeout, embedItemOnPos , prepareItemKind, rollItemAspect, rollAndRegisterItem , placeItemsInDungeon, embedItemsInDungeon, mapActorCStore_ #ifdef EXPOSE_INTERNAL -- * Internal operations , onlyRegisterItem, computeRndTimeout, createCaveItem, createEmbedItem #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.HashMap.Strict as HM import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.CaveKind (citemFreq, citemNum) import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.TileKind (TileKind) import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.ItemRev import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State onlyRegisterItem :: MonadServerAtomic m => ItemKnown -> m ItemId onlyRegisterItem itemKnown@(ItemKnown _ arItem _) = do itemRev <- getsServer sitemRev case HM.lookup itemKnown itemRev of Just iid -> return iid Nothing -> do icounter <- getsServer sicounter executedOnServer <- execUpdAtomicSer $ UpdDiscoverServer icounter arItem let !_A = assert executedOnServer () modifyServer $ \ser -> ser { sitemRev = HM.insert itemKnown icounter (sitemRev ser) , sicounter = succ icounter } return $! icounter registerItem :: MonadServerAtomic m => Bool -> ItemFullKit -> ItemKnown -> Container -> m ItemId registerItem verbose (itemFull@ItemFull{itemBase, itemKindId, itemKind}, kit) itemKnown@(ItemKnown _ arItem _) containerRaw = do container <- case containerRaw of CActor aid CEqp -> do b <- getsState $ getActorBody aid return $! if eqpFreeN b >= fst kit then containerRaw else CActor aid CStash _ -> return containerRaw iid <- onlyRegisterItem itemKnown let slore = IA.loreFromContainer arItem container modifyServer $ \ser -> ser {sgenerationAn = EM.adjust (EM.insertWith (+) iid (fst kit)) slore (sgenerationAn ser)} moveStash <- moveStashIfNeeded container mapM_ execUpdAtomic moveStash execUpdAtomic $ UpdCreateItem verbose iid itemBase kit container let worth = itemPrice (fst kit) itemKind case container of _ | worth == 0 -> return () CActor _ COrgan -> return () -- destroyed on drop CTrunk{} -> return () -- we assume any valuables in CEmbed can be dug out _ -> execUpdAtomic $ UpdAlterGold worth knowItems <- getsServer $ sknowItems . soptions when knowItems $ case container of CTrunk{} -> return () _ -> execUpdAtomic $ UpdDiscover container iid itemKindId arItem -- The first recharging period after creation is random, -- between 1 and 2 standard timeouts of the item. -- In this way we avoid many rattlesnakes rattling in unison. case container of CActor _ cstore | cstore `elem` [CEqp, COrgan] -> randomResetTimeout (fst kit) iid itemFull [] container _ -> return () return iid moveStashIfNeeded :: MonadStateRead m => Container -> m [UpdAtomic] moveStashIfNeeded c = case c of CActor aid CStash -> do b <- getsState $ getActorBody aid mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b case mstash of Just (lid, pos) -> do bagStash <- getsState $ getFloorBag lid pos return $! if EM.null bagStash then [ UpdLoseStashFaction False (bfid b) lid pos , UpdSpotStashFaction True (bfid b) (blid b) (bpos b) ] else [] Nothing -> return [UpdSpotStashFaction True (bfid b) (blid b) (bpos b)] _ -> return [] randomResetTimeout :: MonadServerAtomic m => Int -> ItemId -> ItemFull -> [ItemTimer] -> Container -> m () randomResetTimeout k iid itemFull beforeIt toC = do lid <- getsState $ lidFromC toC localTime <- getsState $ getLocalTime lid mrndTimeout <- rndToAction $ computeRndTimeout localTime itemFull -- The created or moved item set (not the items previously at destination) -- has its timeouts reset to a random value between timeout and twice timeout. -- This prevents micromanagement via swapping items in and out of eqp -- and via exact prediction of first timeout after equip. case mrndTimeout of Just rndT -> do bagAfter <- getsState $ getContainerBag toC let afterIt = case iid `EM.lookup` bagAfter of Nothing -> error $ "" `showFailure` (iid, bagAfter, toC) Just (_, it2) -> it2 resetIt = beforeIt ++ replicate k rndT when (afterIt /= resetIt) $ execUpdAtomic $ UpdTimeItem iid toC afterIt resetIt Nothing -> return () -- no @Timeout@ aspect; don't touch computeRndTimeout :: Time -> ItemFull -> Rnd (Maybe ItemTimer) computeRndTimeout localTime ItemFull{itemDisco=ItemDiscoFull itemAspect} = do let t = IA.aTimeout itemAspect if t > 0 then do rndT <- randomR0 t let rndTurns = timeDeltaScale (Delta timeTurn) (t + rndT) return $ Just $ createItemTimer localTime rndTurns else return Nothing computeRndTimeout _ _ = error "computeRndTimeout: server ignorant about an item" createCaveItem :: MonadServerAtomic m => Point -> LevelId -> m () createCaveItem pos lid = do COps{cocave} <- getsState scops Level{lkind, ldepth} <- getLevel lid let container = CFloor lid pos litemFreq = citemFreq $ okind cocave lkind -- Power depth of new items unaffected by number of spawned actors. freq <- prepareItemKind 0 ldepth litemFreq mIidEtc <- rollAndRegisterItem True ldepth freq container Nothing createKitItems lid pos mIidEtc createEmbedItem :: MonadServerAtomic m => LevelId -> Point -> GroupName ItemKind -> m () createEmbedItem lid pos grp = do Level{ldepth} <- getLevel lid let container = CEmbed lid pos -- Power depth of new items unaffected by number of spawned actors. freq <- prepareItemKind 0 ldepth [(grp, 1)] mIidEtc <- rollAndRegisterItem True ldepth freq container Nothing createKitItems lid pos mIidEtc -- Create, register and insert all initial kit items. createKitItems :: MonadServerAtomic m => LevelId -> Point -> Maybe (ItemId, ItemFullKit) -> m () createKitItems lid pos mIidEtc = case mIidEtc of Nothing -> error $ "" `showFailure` (lid, pos, mIidEtc) Just (_, (itemFull, _)) -> do cops <- getsState scops lvl@Level{ldepth} <- getLevel lid let ikit = IK.ikit $ itemKind itemFull nearbyPassable = take (20 + length ikit) $ nearbyPassablePoints cops lvl pos walkable p = Tile.isWalkable (coTileSpeedup cops) (lvl `at` p) good p = walkable p && p `EM.notMember` lfloor lvl kitPos = zip ikit $ filter good nearbyPassable ++ filter walkable nearbyPassable ++ repeat pos forM_ kitPos $ \((ikGrp, cstore), p) -> do let container = if cstore == CGround then CFloor lid p else CEmbed lid pos itemFreq = [(ikGrp, 1)] -- Power depth of new items unaffected by number of spawned actors. freq <- prepareItemKind 0 ldepth itemFreq mresult <- rollAndRegisterItem False ldepth freq container Nothing assert (isJust mresult) $ return () -- Tiles already placed, so it's possible to scatter companion items -- over walkable tiles. embedItemOnPos :: MonadServerAtomic m => LevelId -> Point -> ContentId TileKind -> m () embedItemOnPos lid pos tk = do COps{cotile} <- getsState scops let embedGroups = Tile.embeddedItems cotile tk mapM_ (createEmbedItem lid pos) embedGroups prepareItemKind :: MonadServerAtomic m => Int -> Dice.AbsDepth -> Freqs ItemKind -> m (Frequency (GroupName ItemKind, ContentId IK.ItemKind, ItemKind)) prepareItemKind lvlSpawned ldepth itemFreq = do cops <- getsState scops uniqueSet <- getsServer suniqueSet totalDepth <- getsState stotalDepth return $! newItemKind cops uniqueSet itemFreq ldepth totalDepth lvlSpawned rollItemAspect :: MonadServerAtomic m => Frequency (GroupName ItemKind, ContentId IK.ItemKind, ItemKind) -> Dice.AbsDepth -> m NewItem rollItemAspect freq ldepth = do cops <- getsState scops flavour <- getsServer sflavour discoRev <- getsServer sdiscoKindRev totalDepth <- getsState stotalDepth m2 <- rndToAction $ newItem cops freq flavour discoRev ldepth totalDepth case m2 of NewItem _ (ItemKnown _ arItem _) ItemFull{itemKindId} _ -> do when (IA.checkFlag Ability.Unique arItem) $ modifyServer $ \ser -> ser {suniqueSet = ES.insert itemKindId (suniqueSet ser)} NoNewItem -> return () return m2 rollAndRegisterItem :: MonadServerAtomic m => Bool -> Dice.AbsDepth -> Frequency (GroupName ItemKind, ContentId IK.ItemKind, ItemKind) -> Container -> Maybe Int -> m (Maybe (ItemId, ItemFullKit)) rollAndRegisterItem verbose ldepth freq container mk = do m2 <- rollItemAspect freq ldepth case m2 of NoNewItem -> return Nothing NewItem _ itemKnown itemFull kit -> do let f k = if k == 1 && null (snd kit) then quantSingle else (k, snd kit) !kit2 = maybe kit f mk iid <- registerItem verbose (itemFull, kit2) itemKnown container return $ Just (iid, (itemFull, kit2)) -- Tiles already placed, so it's possible to scatter over walkable tiles. placeItemsInDungeon :: forall m. MonadServerAtomic m => EM.EnumMap LevelId (EM.EnumMap FactionId Point) -> m () placeItemsInDungeon factionPositions = do COps{cocave, coTileSpeedup} <- getsState scops totalDepth <- getsState stotalDepth let initialItems (lid, lvl@Level{lkind, ldepth}) = do litemNum <- rndToAction $ castDice ldepth totalDepth (citemNum $ okind cocave lkind) let alPos = EM.elems $ EM.findWithDefault EM.empty lid factionPositions placeItems :: Int -> m () placeItems n | n == litemNum = return () placeItems !n = do Level{lfloor} <- getLevel lid -- Don't generate items around initial actors or in bunches. let distAndNotFloor !p _ = let f !k = chessDist p k > 4 in p `EM.notMember` lfloor && all f alPos mpos <- rndToAction $ findPosTry2 10 lvl (\_ !t -> Tile.isWalkable coTileSpeedup t && not (Tile.isNoItem coTileSpeedup t)) [ \_ !t -> Tile.isVeryOftenItem coTileSpeedup t , \_ !t -> Tile.isCommonItem coTileSpeedup t ] distAndNotFloor (replicate 10 distAndNotFloor) case mpos of Just pos -> do createCaveItem pos lid placeItems (n + 1) Nothing -> debugPossiblyPrint "Server: placeItemsInDungeon: failed to find positions" placeItems 0 dungeon <- getsState sdungeon -- Make sure items on easy levels are generated first, to avoid all -- artifacts on deep levels. let fromEasyToHard = sortBy (comparing (ldepth . snd)) $ EM.assocs dungeon mapM_ initialItems fromEasyToHard -- Tiles already placed, so it's possible to scatter companion items -- over walkable tiles. embedItemsInDungeon :: MonadServerAtomic m => m () embedItemsInDungeon = do let embedItemsOnLevel (lid, Level{ltile}) = PointArray.imapMA_ (embedItemOnPos lid) ltile dungeon <- getsState sdungeon -- Make sure items on easy levels are generated first, to avoid all -- artifacts on deep levels. let fromEasyToHard = sortBy (comparing (ldepth . snd)) $ EM.assocs dungeon mapM_ embedItemsOnLevel fromEasyToHard -- | Mapping over actor's items from a give store. mapActorCStore_ :: MonadServer m => CStore -> (ItemId -> ItemQuant -> m ()) -> Actor -> m () mapActorCStore_ cstore f b = do bag <- getsState $ getBodyStoreBag b cstore mapM_ (uncurry f) $ EM.assocs bag LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/ItemRev.hs0000644000000000000000000002615207346545000022124 0ustar0000000000000000{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving #-} -- | Creation of items on the server. Types and operations that don't involve -- server state nor our custom monads. module Game.LambdaHack.Server.ItemRev ( ItemKnown(..), NewItem(..), ItemRev, UniqueSet , newItemKind, newItem -- * Item discovery types , DiscoveryKindRev, emptyDiscoveryKindRev, serverDiscos -- * The @FlavourMap@ type , FlavourMap, emptyFlavourMap, dungeonFlavourMap -- * Important implementation parts, exposed for tests , rollFlavourMap #ifdef EXPOSE_INTERNAL -- * Internal operations , buildItem, keepMetaGameInformation #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.HashMap.Strict as HM import Data.Hashable (Hashable) import Data.Vector.Binary () import qualified Data.Vector.Unboxed as U import GHC.Generics (Generic) import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.Flavour -- | The essential item properties, used for the @ItemRev@ hash table -- from items to their ids, needed to assign ids to newly generated items. -- All the other meaningful properties can be derived from them. -- Note: item seed instead of @AspectRecord@ is not enough, -- becaused different seeds may result in the same @AspectRecord@ -- and we don't want such items to be distinct in UI and elsewhere. data ItemKnown = ItemKnown ItemIdentity IA.AspectRecord (Maybe FactionId) deriving (Show, Eq, Generic) instance Binary ItemKnown instance Hashable ItemKnown data NewItem = NewItem (GroupName ItemKind) ItemKnown ItemFull ItemQuant | NoNewItem -- | Reverse item map, for item creation, to keep items and item identifiers -- in bijection. type ItemRev = HM.HashMap ItemKnown ItemId type UniqueSet = ES.EnumSet (ContentId ItemKind) -- | Build an item with the given kind and aspects. buildItem :: COps -> IA.AspectRecord -> FlavourMap -> DiscoveryKindRev -> ContentId ItemKind -> Item buildItem COps{coitem} arItem (FlavourMap flavourMap) (DiscoveryKindRev discoRev) ikChosen = let jkind = case IA.aPresentAs arItem of Just grp -> let kindHidden = ouniqGroup coitem grp in IdentityCovered (toItemKindIx $ discoRev U.! contentIdIndex ikChosen) kindHidden Nothing -> IdentityObvious ikChosen jfid = Nothing -- the default jflavour = toEnum $ fromEnum $ flavourMap U.! contentIdIndex ikChosen in Item{..} -- | Roll an item kind based on given @Freqs@ and kind rarities newItemKind :: COps -> UniqueSet -> Freqs ItemKind -> Dice.AbsDepth -> Dice.AbsDepth -> Int -> Frequency (GroupName ItemKind, ContentId IK.ItemKind, ItemKind) newItemKind COps{coitem, coItemSpeedup} uniqueSet itemFreq (Dice.AbsDepth ldepth) (Dice.AbsDepth totalDepth) lvlSpawned = assert (any (\(_, n) -> n > 0) itemFreq) $ -- Effective generation depth of actors (not items) increases with spawns. -- Up to 10 spawns, no effect. With 20 spawns, depth + 5, and then -- each 10 spawns adds 5 depth. let numSpawnedCoeff = max 0 $ lvlSpawned `div` 2 - 5 ldSpawned = ldepth + numSpawnedCoeff f _ _ acc _ ik _ | ik `ES.member` uniqueSet = acc f !itemGroup !q !acc !p !ik !kind = -- Don't consider lvlSpawned for uniques, except those that have -- @Unique@ under @Odds@. let ld = if IA.checkFlag Ability.Unique $ IA.kmMean $ getKindMean ik coItemSpeedup then ldepth else ldSpawned rarity = linearInterpolation ld totalDepth (IK.irarity kind) !fr = q * p * rarity in (fr, (itemGroup, ik, kind)) : acc g (!itemGroup, !q) = ofoldlGroup' coitem itemGroup (f itemGroup q) [] freqDepth = concatMap g itemFreq in toFreq "newItemKind" freqDepth -- | Given item kind frequency, roll item kind, generate item aspects -- based on level and put together the full item data set. newItem :: COps -> Frequency (GroupName ItemKind, ContentId IK.ItemKind, ItemKind) -> FlavourMap -> DiscoveryKindRev -> Dice.AbsDepth -> Dice.AbsDepth -> Rnd NewItem newItem cops freq flavourMap discoRev levelDepth totalDepth = if nullFreq freq then return NoNewItem -- e.g., rare tile has a unique embed, only first time else do (itemGroup, itemKindId, itemKind) <- frequency freq -- Number of new items/actors unaffected by number of spawned actors. itemN <- castDice levelDepth totalDepth (IK.icount itemKind) arItem <- IA.rollAspectRecord (IK.iaspects itemKind) levelDepth totalDepth let itemBase = buildItem cops arItem flavourMap discoRev itemKindId itemIdentity = jkind itemBase !itemK = max 1 itemN !itemTimer = [itemTimerZero | IA.checkFlag Ability.Periodic arItem] -- enable optimization in @applyPeriodicLevel@ itemSuspect = False -- Bonuses on items/actors unaffected by number of spawned actors. itemDisco = ItemDiscoFull arItem itemFull = ItemFull {..} itemKnown = ItemKnown itemIdentity arItem (jfid itemBase) itemQuant = if itemK == 1 && null itemTimer then quantSingle else (itemK, itemTimer) return $! NewItem itemGroup itemKnown itemFull itemQuant -- | The reverse map to @DiscoveryKind@, needed for item creation. -- This is total and never changes, hence implemented as vector. -- Morally, it's indexed by @ContentId ItemKind@ and elements are @ItemKindIx@. newtype DiscoveryKindRev = DiscoveryKindRev (U.Vector Word16) deriving (Show, Binary) emptyDiscoveryKindRev :: DiscoveryKindRev emptyDiscoveryKindRev = DiscoveryKindRev U.empty serverDiscos :: COps -> DiscoveryKindRev -> Rnd (DiscoveryKind, DiscoveryKindRev) serverDiscos COps{coitem} (DiscoveryKindRev discoRevFromPreviousGame) = do let ixs = [0..toEnum (olength coitem - 1)] shuffled <- if U.null discoRevFromPreviousGame then shuffle ixs else shuffleExcept (keepMetaGameInformation coitem discoRevFromPreviousGame) (olength coitem) ixs let udiscoRev = U.fromListN (olength coitem) shuffled f :: ContentId ItemKind -> Word16 -> (ItemKindIx, ContentId ItemKind) f ik ikx = (toItemKindIx ikx, ik) -- Not @fromDistinctAscList@, because it's the reverse map. discoS = EM.fromList $ zipWith f [toEnum 0 ..] $ U.toList udiscoRev return (discoS, DiscoveryKindRev udiscoRev) -- | Keep in a vector the information that is retained from playthrough -- to playthrough. The information being, e.g., @ItemKindIx@ or @Flavour@. -- The information is morally indexed by @ContentId ItemKind@ and its @Enum@ -- instance fits in @Word16@. keepMetaGameInformation :: ContentData ItemKind -> U.Vector Word16 -> U.Vector Word16 keepMetaGameInformation coitem informationFromPreviousGame = let inMetaGame :: ContentId ItemKind -> Bool inMetaGame kindId = IK.SetFlag Ability.MetaGame `elem` IK.iaspects (okind coitem kindId) keepMeta :: Int -> Word16 -> Word16 keepMeta i ix = if inMetaGame (toEnum i) then ix else invalidInformationCode in U.imap keepMeta informationFromPreviousGame -- | Flavours assigned by the server to item kinds, in this particular game. -- This is total and never changes, hence implemented as vector. -- Morally, it's indexed by @ContentId ItemKind@ and elements are @Flavour@. newtype FlavourMap = FlavourMap (U.Vector Word16) deriving (Show, Binary) emptyFlavourMap :: FlavourMap emptyFlavourMap = FlavourMap U.empty -- | Assigns flavours to item kinds. Assures no flavor is repeated for the same -- symbol, except for items with only one permitted flavour. rollFlavourMap :: U.Vector Word16 -> Rnd ( EM.EnumMap (ContentId ItemKind) Flavour , EM.EnumMap (ContentSymbol ItemKind) (ES.EnumSet Flavour) ) -> ContentId ItemKind -> ItemKind -> Rnd ( EM.EnumMap (ContentId ItemKind) Flavour , EM.EnumMap (ContentSymbol ItemKind) (ES.EnumSet Flavour) ) rollFlavourMap uFlavMeta !rnd !key !ik = case IK.iflavour ik of [] -> error "empty iflavour" [flavour] -> do (!assocs, !availableMap) <- rnd return ( EM.insert key flavour assocs , availableMap ) flvs -> do (!assocs, !availableMap) <- rnd let a0 = uFlavMeta U.! toEnum (fromEnum key) if a0 == invalidInformationCode then do if length flvs < 6 then do -- too few to even attempt unique assignment flavour <- oneOf flvs return ( EM.insert key flavour assocs , availableMap ) else do let available = availableMap EM.! IK.isymbol ik proper = ES.fromList flvs `ES.intersection` available assert (not (ES.null proper) `blame` "not enough flavours for items" `swith` (flvs, available, ik, availableMap)) $ do flavour <- oneOf $ ES.elems proper let availableReduced = ES.delete flavour available return ( EM.insert key flavour assocs , EM.insert (IK.isymbol ik) availableReduced availableMap ) else return ( EM.insert key (toEnum $ fromEnum a0) assocs , availableMap ) -- | Randomly chooses flavour for all item kinds for this game. dungeonFlavourMap :: COps -> FlavourMap -> Rnd FlavourMap dungeonFlavourMap COps{coitem} (FlavourMap flavourMapFromPreviousGame) = do let uFlavMeta = if U.null flavourMapFromPreviousGame then U.replicate (olength coitem) invalidInformationCode else keepMetaGameInformation coitem flavourMapFromPreviousGame flavToAvailable :: EM.EnumMap Char (ES.EnumSet Flavour) -> Int -> Word16 -> EM.EnumMap Char (ES.EnumSet Flavour) flavToAvailable em i fl = let ik = okind coitem (toEnum i) setBase = EM.findWithDefault (ES.fromList stdFlavList) (IK.isymbol ik) em setMeta = if fl == invalidInformationCode then setBase else ES.delete (toEnum $ fromEnum fl) setBase in EM.insert (IK.isymbol ik) setMeta em availableMap = U.ifoldl' flavToAvailable EM.empty uFlavMeta (assocsFlav, _) <- ofoldlWithKey' coitem (rollFlavourMap uFlavMeta) (return (EM.empty, availableMap)) let uFlav = U.fromListN (olength coitem) $ map (toEnum . fromEnum) $ EM.elems assocsFlav return $! FlavourMap uFlav LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/LoopM.hs0000644000000000000000000010752707346545000021605 0ustar0000000000000000-- | The main loop of the server, processing human and computer player -- moves turn by turn. module Game.LambdaHack.Server.LoopM ( loopSer #ifdef EXPOSE_INTERNAL -- * Internal operations , factionArena, arenasForLoop, handleFidUpd, loopUpd, endClip , manageCalmAndDomination, applyPeriodicLevel , handleTrajectories, hTrajectories, advanceTrajectory , handleActors, hActors, handleUIunderAI, dieSer, restartGame #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Game.LambdaHack.Atomic import Game.LambdaHack.Client (ReqUI (..), Response (..)) import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import qualified Game.LambdaHack.Common.ItemAspect as IA import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Common.Vector import Game.LambdaHack.Content.FactionKind import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Content.RuleKind import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.CommonM import Game.LambdaHack.Server.HandleEffectM import Game.LambdaHack.Server.HandleRequestM import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.PeriodicM import Game.LambdaHack.Server.ProtocolM import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.StartM import Game.LambdaHack.Server.State -- | Start a game session, including the clients, and then loop, -- communicating with the clients. -- -- The loop is started in server state that is empty, see 'emptyStateServer'. loopSer :: (MonadServerAtomic m, MonadServerComm m) => ServerOptions -- ^ player-supplied server options -> (Bool -> FactionId -> ChanServer -> IO ()) -- ^ function that initializes a client and runs its main loop -> m () loopSer serverOptions executorClient = do -- Recover states and launch clients. modifyServer $ \ser -> ser { soptionsNxt = serverOptions , soptions = serverOptions } cops <- getsState scops let updConn startsNewGame = updateConn $ executorClient startsNewGame restored <- tryRestore case restored of Just (sRaw, ser) | not $ snewGameSer serverOptions -> do -- a restored game execUpdAtomic $ UpdResumeServer $ updateCOpsAndCachedData (const cops) sRaw putServer ser {soptionsNxt = serverOptions} applyDebug factionD <- getsState sfactionD let f fid = let cmd = UpdResumeServer $ updateCOpsAndCachedData (const cops) $ sclientStates ser EM.! fid in execUpdAtomicFidCatch fid cmd mapM_ (void <$> f) $ EM.keys factionD updConn False initPer pers <- getsServer sperFid let clear = const emptyPer persFid fid | sknowEvents serverOptions = EM.map clear (pers EM.! fid) | otherwise = pers EM.! fid mapM_ (\fid -> sendUpdate fid $ UpdResume fid (persFid fid)) (EM.keys factionD) arenasNew <- arenasForLoop modifyServer $ \ser2 -> ser2 {sarenas = arenasNew, svalidArenas = True} -- We dump RNG seeds here, based on @soptionsNxt@, in case the game -- wasn't run with @--dumpInitRngs@ previously, but we need the seeds, -- e.g., to diagnose a crash. rngs <- getsServer srngs when (sdumpInitRngs serverOptions) $ dumpRngs rngs _ -> do -- starting new game for this savefile (--newGame or fresh save) factionDold <- getsState sfactionD s <- gameReset serverOptions Nothing Nothing -- get RNG from item boost -- Set up commandline options. let optionsBarRngs = serverOptions {sdungeonRng = Nothing, smainRng = Nothing} modifyServer $ \ser -> ser { soptionsNxt = optionsBarRngs , soptions = optionsBarRngs } execUpdAtomic $ UpdRestartServer s updConn True initPer reinitGame factionDold writeSaveAll False False loopUpd $ updConn True factionArena :: MonadStateRead m => Faction -> m (Maybe LevelId) factionArena fact = case gleader fact of -- Even spawners need an active arena for their leader, -- or they start clogging stairs. Just leader -> do b <- getsState $ getActorBody leader return $ Just $ blid b Nothing -> return Nothing -- This means Allure heroes can kill all aliens on lvl 4, retreat, -- hide and sleep on lvl 3 and they are guaranteed aliens don't spawn. -- However, animals still spawn, if slowly, and aliens resume -- spawning when heroes move on again. arenasForLoop :: MonadStateRead m => m (ES.EnumSet LevelId) {-# INLINE arenasForLoop #-} arenasForLoop = do factionD <- getsState sfactionD marenas <- mapM factionArena $ EM.elems factionD let arenas = ES.fromList $ catMaybes marenas !_A = assert (not (ES.null arenas) `blame` "game over not caught earlier" `swith` factionD) () return $! arenas handleFidUpd :: forall m. (MonadServerAtomic m, MonadServerComm m) => (FactionId -> m ()) -> FactionId -> Faction -> m () {-# INLINE handleFidUpd #-} handleFidUpd updatePerFid fid fact = do -- Update perception on all levels at once, -- in case a leader is changed to actor on another -- (possibly not even currently active) level. -- This runs for all factions even if save is requested by UI. -- Let players ponder new game state while the engine is busy saving. -- Also, this ensures perception before game save is exactly the same -- as at game resume, which is an invariant we check elsewhere. -- However, if perception is not updated after the action, the actor -- may not see his vicinity, so may not see enemy that displaces (or hits) him -- resulting in breaking the displace action and temporary leader loss, -- which is fine, though a bit alarming. So, we update it at the end. updatePerFid fid -- Move a single actor only. Note that the skipped actors are not marked -- as waiting. Normally they will act in the next clip or the next few, -- so that's natural. But if there are dozens of them, this is wierd. -- E.g., they don't move, but still make nearby foes lose Calm. -- However, for KISS, we leave it be. -- -- Bail out if immediate loop break- requested by UI. No check -- for @sbreakLoop@ needed, for the same reasons as in @handleActors@. let handle :: [LevelId] -> m Bool handle [] = return False handle (lid : rest) = do breakASAP <- getsServer sbreakASAP if breakASAP then return False else do nonWaitMove <- handleActors lid fid if nonWaitMove then return True else handle rest killDying :: [LevelId] -> m () killDying = mapM_ killDyingLid killDyingLid :: LevelId -> m () killDyingLid lid = do localTime <- getsState $ getLocalTime lid levelTime <- getsServer $ (EM.! lid) . (EM.! fid) . sactorTime let l = filter (\(_, atime) -> atime <= localTime) $ EM.assocs levelTime killAid (aid, _) = do b1 <- getsState $ getActorBody aid when (bhp b1 <= 0) $ dieSer aid b1 mapM_ killAid l -- Start on arena with leader, if available. This is crucial to ensure -- that no actor (even ours) moves before UI declares save(&exit). fa <- factionArena fact arenas <- getsServer sarenas let myArenas = case fa of Just myArena -> myArena : delete myArena (ES.elems arenas) Nothing -> ES.elems arenas nonWaitMove <- handle myArenas breakASAP <- getsServer sbreakASAP unless breakASAP $ killDying myArenas -- We update perception at the end, see comment above. This is usually -- cheap, and when not, if it's AI faction, it's a waste, but if it's UI, -- that's exactly where it prevents lost attack messages, etc. -- If the move was a wait, perception unchanged, so no need to update, -- unless the actor starts sleeping, in which case his perception -- is reduced a bit later, so no harm done. when nonWaitMove $ updatePerFid fid -- | Handle a clip (the smallest fraction of a game turn for which a frame may -- potentially be generated). Run the leader and other actors moves. -- Eventually advance the time and repeat. loopUpd :: forall m. (MonadServerAtomic m, MonadServerComm m) => m () -> m () loopUpd updConn = do let updatePerFid :: FactionId -> m () {-# NOINLINE updatePerFid #-} updatePerFid fid = do -- {-# SCC updatePerFid #-} do perValid <- getsServer $ (EM.! fid) . sperValidFid mapM_ (\(lid, valid) -> unless valid $ updatePer fid lid) (EM.assocs perValid) handleFid :: (FactionId, Faction) -> m () {-# NOINLINE handleFid #-} handleFid (fid, fact) = do breakASAP <- getsServer sbreakASAP -- Don't process other factions, even their perceptions, -- if UI saves and/or exits. unless breakASAP $ handleFidUpd updatePerFid fid fact loopConditionally = do factionD <- getsState sfactionD -- Update perception one last time to satisfy save/resume assertions, -- because we may get here at arbitrary moment due to game over -- and so have outdated perception. mapM_ updatePerFid (EM.keys factionD) modifyServer $ \ser -> ser { sbreakLoop = False , sbreakASAP = False } endOrLoop loopUpdConn (restartGame updConn loopUpdConn) loopUpdConn = do factionD <- getsState sfactionD -- Start handling actors with the single UI faction, -- to safely save/exit. Note that this hack fails if there are many UI -- factions (when we reenable multiplayer). Then players will request -- save&exit and others will vote on it and it will happen -- after the clip has ended, not at the start. -- Note that at most a single actor with a time-consuming action -- is processed per faction, so it's fair, but many loops are needed. let hasUI (_, fact) = fhasUI (gkind fact) (factionUI, factionsRest) = case break hasUI $ EM.assocs factionD of (noUI1, ui : noUI2) -> (ui, noUI1 ++ noUI2) _ -> error "no UI faction in the game" mapM_ handleFid $ factionUI : factionsRest breakASAP <- getsServer sbreakASAP breakLoop <- getsServer sbreakLoop if breakASAP || breakLoop then loopConditionally else do -- Projectiles are processed last and not at all if the UI leader -- decides to save or exit or restart or if there is game over. -- This and UI leader acting before any other ordinary actors -- ensures state is not changed and so the clip doesn't need -- to be carried through before save. arenas <- getsServer sarenas mapM_ (\fid -> mapM_ (`handleTrajectories` fid) $ ES.elems arenas) (EM.keys factionD) endClip updatePerFid -- must be last, in case performs a bkp save -- The condition can be changed in @handleTrajectories@ by pushing -- onto an escape and in @endClip@. breakLoop2 <- getsServer sbreakLoop if breakLoop2 then loopConditionally else loopUpdConn -- process next iteration unconditionally loopUpdConn -- | Handle the end of every clip. Do whatever has to be done -- every fixed number of clips, e.g., monster generation. -- Advance time. Perform periodic saves, if applicable. -- -- This is never run if UI requested save or exit or restart and it's correct, -- because we know nobody moved and no time was or needs to be advanced -- and arenas are not changed. After game was saved and exited, -- on game resume the first clip is performed with empty arenas, -- so arena time is not updated and nobody moves, nor anything happens, -- but arenas are here correctly updated. endClip :: forall m. MonadServerAtomic m => (FactionId -> m ()) -> m () {-# INLINE endClip #-} endClip updatePerFid = do COps{corule} <- getsState scops time <- getsState stime let clipN = time `timeFit` timeClip -- No check if @sbreakASAP@ is set, because then the function is not called. breakLoop <- getsServer sbreakLoop -- We don't send a lot of useless info to the client if the game has already -- ended. At best wasteful, at worst the player sees strange messages. unless breakLoop $ do -- I need to send time updates, because I can't add time to each command, -- because I'd need to send also all arenas, which should be updated, -- and this is too expensive data for each, e.g., projectile move. -- I send even if nothing changes so that UI time display can progress. -- Possibly @arenas@ are invalid here, but all moves were performed -- according to this value, so time should be replenished according -- to this value as well. -- This is crucial, because tiny time discrepancies can accumulate -- magnified by hunders of actors that share the clip slots due to the -- restriction that at most one faction member acts each clip. arenas <- getsServer sarenas execUpdAtomic $ UpdAgeGame arenas -- Perform periodic dungeon maintenance. when (clipN `mod` rleadLevelClips corule == 0) leadLevelSwitch case clipN `mod` clipsInTurn of 0 -> -- Spawn monsters at most once per 3 turns. when (clipN `mod` (3 * clipsInTurn) == 0) spawnMonster 4 -> -- Periodic activation only once per turn, for speed, -- but on all active arenas. Calm updates and domination -- happen there as well. Once per turn is too rare for accurate -- expiration of short conditions, e.g., 1-turn haste. TODO. applyPeriodicLevel _ -> return () -- @applyPeriodicLevel@ might have, e.g., dominated actors, ending the game. -- It could not have unended the game, though. breakLoop2 <- getsServer sbreakLoop unless breakLoop2 $ do -- Possibly a leader change due to @leadLevelSwitch@, so update arenas here -- for 100% accuracy at least at the start of actor moves, before they -- change leaders as part of their moves. -- -- After game resume, this is the first non-vacuus computation. -- Next call to @loopUpdConn@ really moves actors and updates arena times -- so we start in exactly the same place that UI save ended in. validArenas <- getsServer svalidArenas unless validArenas $ do arenasNew <- arenasForLoop modifyServer $ \ser -> ser {sarenas = arenasNew, svalidArenas = True} -- Update all perception for visual feedback and to make sure saving -- and resuming game doesn't affect gameplay (by updating perception). -- Perception updates in @handleFidUpd@ are not enough, because -- periodic actions could have invalidated them. factionD <- getsState sfactionD mapM_ updatePerFid (EM.keys factionD) -- Saving on the browser causes a huge lag, hence autosave disabled. #ifndef USE_JSFILE unless breakLoop2 $ -- if by chance requested and periodic saves coincide -- Periodic save needs to be at the end, so that restore can start -- at the beginning. Double save on first turn is avoided with @succ@. when (succ clipN `mod` rwriteSaveClips corule == 0) $ writeSaveAll False False #endif -- | Check if the given actor is dominated and update his calm. manageCalmAndDomination :: MonadServerAtomic m => ActorId -> Actor -> m () manageCalmAndDomination aid b = do performedDomination <- if bcalm b > 0 then return False else do -- triggered by zeroed Calm hiImpression <- highestImpression b case hiImpression of Nothing -> return False Just (hiImpressionFid, hiImpressionK) -> do fact <- getsState $ (EM.! bfid b) . sfactionD if fhasPointman (gkind fact) -- animals/robots/human drones never Calm-dominated || hiImpressionK >= 10 -- unless very high impression, e.g., in a dominated hero then dominateFidSfx aid aid (btrunk b) hiImpressionFid else return False unless performedDomination $ do newCalmDelta <- getsState $ regenCalmDelta aid b unless (newCalmDelta == 0) $ -- Update delta for the current player turn. updateCalm aid newCalmDelta -- | Trigger periodic items for all actors on the given level. applyPeriodicLevel :: MonadServerAtomic m => m () applyPeriodicLevel = do arenas <- getsServer sarenas let applyPeriodicItem _ _ (_, (_, [])) = return () -- periodic items always have at least one timer applyPeriodicItem aid cstore (iid, _) = do itemFull <- getsState $ itemToFull iid let arItem = aspectRecordFull itemFull when (IA.checkFlag Ability.Periodic arItem) $ do -- Check if the item is still in the bag (previous items act!). b2 <- getsState $ getActorBody aid bag <- getsState $ getBodyStoreBag b2 cstore case iid `EM.lookup` bag of Nothing -> return () -- item dropped Just (k, _) -> do -- Activate even if effects null or vacuous, to possibly -- destroy the item. let effApplyFlags = EffApplyFlags { effToUse = EffBare -- no periodic crafting , effVoluntary = True , effUseAllCopies = k <= 1 , effKineticPerformed = False , effActivation = Ability.ActivationPeriodic , effMayDestroy = True } void $ effectAndDestroyAndAddKill effApplyFlags aid aid aid iid (CActor aid cstore) itemFull applyPeriodicActor (aid, b) = -- While it's fun when projectiles flash or speed up mid-air, -- it's very exotic and quite time-intensive whenever hundreds -- of projectiles exist due to ongoing explosions. -- Nothing activates when actor dying to prevent a regenerating -- actor from resurrecting each turn, resulting in silly gameover stats. when (not (bproj b) && bhp b > 0 && blid b `ES.member` arenas) $ do -- Equipment goes first, to refresh organs before they expire, -- to avoid the message that organ expired. mapM_ (applyPeriodicItem aid CEqp) $ EM.assocs $ beqp b mapM_ (applyPeriodicItem aid COrgan) $ EM.assocs $ borgan b -- While we are at it, also update his Calm. manageCalmAndDomination aid b allActors <- getsState sactorD mapM_ applyPeriodicActor $ EM.assocs allActors handleTrajectories :: MonadServerAtomic m => LevelId -> FactionId -> m () handleTrajectories lid fid = do localTime <- getsState $ getLocalTime lid levelTime <- getsServer $ (EM.! lid) . (EM.! fid) . strajTime let l = sort $ map fst $ filter (\(_, atime) -> atime <= localTime) $ EM.assocs levelTime -- The @strajTime@ map may be outdated before @hTrajectories@ -- call (due to other actors following their trajectories), -- so it's only used to decide which actors are processed in this -- @handleTrajectories@ call. If an actor is added to the map, -- the recursive call to @handleTrajectories@ will detect that -- and process him later on. -- If the actor is no longer on the level or no longer belongs -- to the faction, it is nevertheless processed without a problem. -- We are guaranteed the actor still exists. mapM_ hTrajectories l -- Avoid frames between fadeout and fadein. breakLoop <- getsServer sbreakLoop unless (null l || breakLoop) $ handleTrajectories lid fid -- for speeds > tile/clip hTrajectories :: MonadServerAtomic m => ActorId -> m () {-# INLINE hTrajectories #-} hTrajectories aid = do b1 <- getsState $ getActorBody aid let removePushed b = -- No longer fulfills criteria and was not removed by dying; remove him. modifyServer $ \ser -> ser { strajTime = EM.adjust (EM.adjust (EM.delete aid) (blid b)) (bfid b) (strajTime ser) , strajPushedBy = EM.delete aid (strajPushedBy ser) } removeTrajectory b = -- Non-projectile actor stops flying (a projectile with empty trajectory -- would be intercepted earlier on as dead). -- Will be removed from @strajTime@ in recursive call -- to @handleTrajectories@. assert (not $ bproj b) $ execUpdAtomic $ UpdTrajectory aid (btrajectory b) Nothing breakLoop <- getsServer sbreakLoop if breakLoop then return () -- don't move if game over via pushing else if actorDying b1 then dieSer aid b1 else case btrajectory b1 of Nothing -> removePushed b1 Just ([], _) -> removeTrajectory b1 >> removePushed b1 Just{} -> do advanceTrajectory aid b1 -- Here, @advanceTrajectory@ might have affected @actorDying@, -- so we check again ASAP to make sure the body of the projectile -- (or pushed actor) doesn't block movement of other actors, -- but vanishes promptly. -- Bodies of actors that die not flying remain on the battlefied until -- their natural next turn, to give them a chance of rescue. -- Note that domination of pushed actors is not checked -- nor is their calm updated. They are helpless wrt movement, -- but also invulnerable in this respect. b2 <- getsState $ getActorBody aid if actorDying b2 then dieSer aid b2 else case btrajectory b2 of Nothing -> removePushed b2 Just ([], _) -> removeTrajectory b2 >> removePushed b2 Just{} -> -- delay next iteration only if still flying advanceTimeTraj aid -- if @actorDying@ due to @bhp b <= 0@: -- If @b@ is a projectile, it means hits an actor or is hit by actor. -- Then the carried item is destroyed and that's all. -- If @b@ is not projectile, it dies, his items drop to the ground -- and possibly a new leader is elected. -- -- if @actorDying@ due to @btrajectory@ null: -- A projectile drops to the ground due to obstacles or range. -- The carried item is not destroyed, unless it's fragile, -- but drops to the ground. -- | Manage trajectory of a projectile or a pushed other actor. -- -- Colliding with a wall or actor doesn't take time, because -- the projectile does not move (the move is blocked). -- Not advancing time forces dead projectiles to be destroyed ASAP. -- Otherwise, with some timings, it can stay on the game map dead, -- blocking path of human-controlled actors and alarming the hapless human. advanceTrajectory :: MonadServerAtomic m => ActorId -> Actor -> m () advanceTrajectory aid b1 = do COps{coTileSpeedup} <- getsState scops lvl <- getLevel $ blid b1 arTrunk <- getsState $ (EM.! btrunk b1) . sdiscoAspect let registerKill killHow = -- Kill counts for each blast particle is TMI. when (bproj b1 && not (IA.checkFlag Ability.Blast arTrunk)) $ do killer <- getsServer $ EM.findWithDefault aid aid . strajPushedBy addKillToAnalytics killer killHow (bfid b1) (btrunk b1) case btrajectory b1 of Just (d : lv, speed) -> do let tpos = bpos b1 `shift` d -- target position if Tile.isWalkable coTileSpeedup $ lvl `at` tpos then do -- Hit will clear trajectories in @reqMelee@, -- so no need to do that here. execUpdAtomic $ UpdTrajectory aid (btrajectory b1) (Just (lv, speed)) when (null lv) $ registerKill KillDropLaunch let occupied = occupiedBigLvl tpos lvl || occupiedProjLvl tpos lvl reqMoveHit = reqMoveGeneric False True aid d reqDisp = reqDisplaceGeneric False aid if | bproj b1 -> reqMoveHit -- projectiles always hit | occupied -> -- Non-projectiles displace if they are ending their flight -- or if only a projectile is in the way. -- So, no chaos of displacing a whole line of enemies. case (posToBigLvl tpos lvl, posToProjsLvl tpos lvl) of (Nothing, []) -> error "advanceTrajectory: not occupied" (Nothing, [target]) -> reqDisp target (Nothing, _) -> reqMoveHit -- can't displace multiple (Just target, []) -> if null lv then reqDisp target else reqMoveHit (Just _, _) -> reqMoveHit -- can't displace multiple | otherwise -> reqMoveHit -- if not occupied, just move else do -- Will be removed from @strajTime@ in recursive call -- to @handleTrajectories@. unless (bproj b1) $ execSfxAtomic $ SfxCollideTile aid tpos embedsPre <- getsState $ getEmbedBag (blid b1) tpos -- No crafting by projectiles that bump tiles nor by pushed actors. -- The only way is if they land in a tile (are engulfed by it) -- and have enough skill. But projectiles transform when hitting, -- if terrain permits, not just bump off the obstacle. mfail <- reqAlterFail (not $ bproj b1) EffBare False aid tpos embedsPost <- getsState $ getEmbedBag (blid b1) tpos b2 <- getsState $ getActorBody aid let tpos2 = bpos b2 `shift` d -- possibly another level and/or bpos lvl2 <- getLevel $ blid b2 case mfail of Nothing | Tile.isWalkable coTileSpeedup $ lvl2 `at` tpos2 -> -- Too late to announce anything, but given that the way -- is opened, continue flight. Don't even normally lose any HP, -- because it's not a hard collision, but altering. -- However, if embed was possibly triggered/removed, lose HP. if embedsPre /= embedsPost && not (EM.null embedsPre) then if bhp b2 > oneM then do execUpdAtomic $ UpdRefillHP aid minusM b3 <- getsState $ getActorBody aid advanceTrajectory aid b3 else do -- Projectile has too low HP to pierce; terminate its flight. execUpdAtomic $ UpdTrajectory aid (btrajectory b2) $ Just ([], speed) registerKill KillTileLaunch else -- Try again with the cleared path and possibly actors -- spawned in the way, etc. advanceTrajectory aid b2 _ -> do -- Altering failed to open the passage, probably just a wall, -- so lose HP due to being pushed into an obstacle. -- Never kill in this way. -- Note that sometimes this may come already after one faction -- wins the game and end game screens are show. This is OK-ish. -- @Nothing@ trajectory of signals an obstacle hit. -- If projectile, second call of @actorDying@ above -- will take care of dropping dead. execUpdAtomic $ UpdTrajectory aid (btrajectory b2) Nothing -- If projectile, losing HP due to hitting an obstacle -- not needed, because trajectory is halted, so projectile -- will die soon anyway if bproj b2 then registerKill KillTileLaunch else when (bhp b2 > oneM) $ do execUpdAtomic $ UpdRefillHP aid minusM let effect = IK.RefillHP (-2) -- -2 is a lie to ensure display execSfxAtomic $ SfxEffect (bfid b2) aid (btrunk b2) effect (-1) _ -> error $ "Nothing or empty trajectory" `showFailure` (aid, b1) handleActors :: (MonadServerAtomic m, MonadServerComm m) => LevelId -> FactionId -> m Bool handleActors lid fid = do localTime <- getsState $ getLocalTime lid levelTime <- getsServer $ (EM.! lid) . (EM.! fid) . sactorTime let l = sort $ map fst $ filter (\(_, atime) -> atime <= localTime) $ EM.assocs levelTime -- The @sactorTime@ map may be outdated before @hActors@ -- call (due to other actors on the list acting), -- so it's only used to decide which actors are processed in this call. -- If the actor is no longer on the level or no longer belongs -- to the faction, it is nevertheless processed without a problem -- (the client may act wrt slightly outdated Perception and that's all). -- We are guaranteed the actor still exists. mleader <- getsState $ gleader . (EM.! fid) . sfactionD -- Leader acts first, so that UI leader can save&exit before state changes. hActors $ case mleader of Just aid | aid `elem` l -> aid : delete aid l _ -> l hActors :: forall m. (MonadServerAtomic m, MonadServerComm m) => [ActorId] -> m Bool hActors [] = return False hActors as@(aid : rest) = do b1 <- getsState $ getActorBody aid let !_A = assert (not $ bproj b1) () if bhp b1 <= 0 then -- Will be killed in a later pass, making it possible to revive him now. hActors rest else do let side = bfid b1 fact <- getsState $ (EM.! side) . sfactionD breakLoop <- getsServer sbreakLoop let mleader = gleader fact aidIsLeader = mleader == Just aid mainUIactor = fhasUI (gkind fact) && (aidIsLeader || not (fhasPointman (gkind fact))) -- Checking @breakLoop@, to avoid doubly setting faction status to Camping -- in case AI-controlled UI client asks to exit game at exactly -- the same moment as natural game over was detected. mainUIunderAI = mainUIactor && gunderAI fact && not breakLoop when mainUIunderAI $ handleUIunderAI side aid factNew <- getsState $ (EM.! side) . sfactionD let doQueryAI = not mainUIactor || gunderAI factNew breakASAP <- getsServer sbreakASAP -- If breaking out of the game loop, pretend there was a non-wait move. -- we don't need additionally to check @sbreakLoop@, because it occurs alone -- only via action of an actor and at most one action is performed here. if breakASAP then return True else do let mswitchLeader :: Maybe ActorId -> m ActorId {-# NOINLINE mswitchLeader #-} mswitchLeader (Just aidNew) = switchLeader side aidNew >> return aidNew mswitchLeader Nothing = return aid (aidNew, mtimed) <- if doQueryAI then do (cmd, maid) <- sendQueryAI side aid aidNew <- mswitchLeader maid mtimed <- handleRequestAI cmd return (aidNew, mtimed) else do (cmd, maid) <- sendQueryUI RespQueryUI side aid aidNew <- mswitchLeader maid mtimed <- handleRequestUI side aidNew cmd return (aidNew, mtimed) case mtimed of Just timed -> do nonWaitMove <- handleRequestTimed side aidNew timed -- Even if the actor got a free turn of time via a scroll, -- he will not act again this clip, only next clip. -- Clip is small, so not a big deal and it's faster and avoids -- complete game time freezes, e.g., due to an exploit. if nonWaitMove then return True else hActors rest Nothing -> do breakASAP2 <- getsServer sbreakASAP -- If breaking out of the game lopp, pretend there was a non-wait move. if breakASAP2 then return True else hActors as handleUIunderAI :: (MonadServerAtomic m, MonadServerComm m) => FactionId -> ActorId -> m () handleUIunderAI side aid = do cmdS <- sendQueryUI RespQueryUIunderAI side aid case fst cmdS of ReqUINop -> return () ReqUIAutomate -> execUpdAtomic $ UpdAutoFaction side False ReqUIGameDropAndExit -> reqGameDropAndExit aid ReqUIGameSaveAndExit -> reqGameSaveAndExit aid _ -> error $ "" `showFailure` cmdS dieSer :: MonadServerAtomic m => ActorId -> Actor -> m () dieSer aid b2 = do if bproj b2 then when (isJust $ btrajectory b2) $ execUpdAtomic $ UpdTrajectory aid (btrajectory b2) Nothing -- needed only to ensure display of the last position of projectile else do kindId <- getsState $ getIidKindIdServer $ btrunk b2 execUpdAtomic $ UpdRecordKill aid kindId 1 -- At this point the actor's body exists and his items are not dropped. deduceKilled aid -- Most probabaly already done, but just in case (e.g., when actor -- created with 0 HP): electLeader (bfid b2) (blid b2) aid -- If an explosion blast, before the particle is destroyed, it tries -- to modify terrain with it as well as do some easy crafting, -- e.g., cooking on fire. arTrunk <- getsState $ (EM.! btrunk b2) . sdiscoAspect let spentProj = bproj b2 && EM.null (beqp b2) isBlast = IA.checkFlag Ability.Blast arTrunk -- Let thrown food cook in fire (crafting) and other projectiles -- transform terrain they fall onto. Big actors are inert at death. (effScope, bumping) = if bproj b2 then (EffBareAndOnCombine, False) else (EffBare, True) when (not spentProj && isBlast) $ void $ reqAlterFail bumping effScope False aid (bpos b2) b3 <- getsState $ getActorBody aid -- Items need to do dropped now, so that they can be transformed by effects -- of the embedded items, if they are activated. -- If the actor was a projectile and no effect was triggered by hitting -- an enemy, the item still exists and @OnSmash@ effects will be triggered. dropAllEquippedItems aid b3 -- Also destroy, not just drop, all organs, to trigger any effects. -- Note that some effects may be invoked on an actor that has -- no trunk any more. Conditions are ignored to avoid spam about them ending. bag <- getsState $ getBodyStoreBag b3 COrgan discoAspect <- getsState sdiscoAspect let f = void <$$> dropCStoreItem False True COrgan aid b3 maxBound isCondition = IA.checkFlag Ability.Condition . (discoAspect EM.!) mapM_ (uncurry f) $ filter (not . isCondition . fst) $ EM.assocs bag -- As the last act of heroism, the actor (even if projectile) -- changes the terrain with its embedded items, if possible. -- Note that all the resulting effects are invoked on an actor that has -- no trunk any more. when (not spentProj && not isBlast) $ void $ reqAlterFail bumping effScope False aid (bpos b2) -- old bpos; OK, safer b4 <- getsState $ getActorBody aid execUpdAtomic $ UpdDestroyActor aid b4 [] restartGame :: MonadServerAtomic m => m () -> m () -> Maybe (GroupName ModeKind) -> m () restartGame updConn loop mgameMode = do -- This goes only to the old UI client. execSfxAtomic SfxRestart soptionsNxt <- getsServer soptionsNxt srandom <- getsServer srandom factionDold <- getsState sfactionD -- Create new factions. s <- gameReset soptionsNxt mgameMode (Just srandom) -- Note how we also no longer assert exploration, because there may not be -- enough time left in the debug run to explore again in a new game. let optionsBarRngs = soptionsNxt { sdungeonRng = Nothing , smainRng = Nothing , sassertExplored = Nothing } modifyServer $ \ser -> ser { soptionsNxt = optionsBarRngs , soptions = optionsBarRngs } -- This reaches only the intersection of old and new clients. execUpdAtomic $ UpdRestartServer s -- Spawn new clients, as needed, according to new factions. updConn initPer reinitGame factionDold -- Save a just started noConfirm game to preserve history of the just -- ended normal game, in case the user exits brutally. writeSaveAll False True loop LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/MonadServer.hs0000644000000000000000000002154707346545000023001 0ustar0000000000000000-- | Basic server monads and related operations. module Game.LambdaHack.Server.MonadServer ( -- * The server monad MonadServer( getsServer , modifyServer , chanSaveServer -- exposed only to be implemented, not used , liftIO -- exposed only to be implemented, not used ) , MonadServerAtomic(..) -- * Assorted primitives , getServer, putServer, debugPossiblyPrint, debugPossiblyPrintAndExit , serverPrint, saveServer, dumpRngs, restoreScore, registerScore , rndToAction, getSetGen ) where import Prelude () import Game.LambdaHack.Core.Prelude -- Cabal import qualified Paths_LambdaHack as Self (version) import qualified Control.Exception as Ex import qualified Control.Monad.Trans.State.Strict as St import qualified Data.EnumMap.Strict as EM import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Time.Clock.POSIX import Data.Time.LocalTime import System.Exit (exitFailure) import System.FilePath import System.IO (hFlush, stdout) import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Atomic import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.ClientOptions (sbenchmark) import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.File import qualified Game.LambdaHack.Common.HighScore as HighScore import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Core.Random import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State class MonadStateRead m => MonadServer m where getsServer :: (StateServer -> a) -> m a modifyServer :: (StateServer -> StateServer) -> m () chanSaveServer :: m (Save.ChanSave (State, StateServer)) -- We do not provide a MonadIO instance, so that outside -- nobody can subvert the action monads by invoking arbitrary IO. liftIO :: IO a -> m a -- | The monad for executing atomic game state transformations. class MonadServer m => MonadServerAtomic m where -- | Execute an atomic command that changes the state -- on the server and on all clients that can notice it. execUpdAtomic :: UpdAtomic -> m () -- | Execute an atomic command that changes the state -- on the server only. execUpdAtomicSer :: UpdAtomic -> m Bool -- | Execute an atomic command that changes the state -- on the given single client only. execUpdAtomicFid :: FactionId -> UpdAtomic -> m () -- | Execute an atomic command that changes the state -- on the given single client only. -- Catch 'AtomicFail' and indicate if it was in fact raised. execUpdAtomicFidCatch :: FactionId -> UpdAtomic -> m Bool -- | Execute an atomic command that only displays special effects. execSfxAtomic :: SfxAtomic -> m () execSendPer :: FactionId -> LevelId -> Perception -> Perception -> Perception -> m () getServer :: MonadServer m => m StateServer getServer = getsServer id putServer :: MonadServer m => StateServer -> m () putServer s = modifyServer (const s) debugPossiblyPrint :: MonadServer m => Text -> m () debugPossiblyPrint t = do debug <- getsServer $ sdbgMsgSer . soptions when debug $ liftIO $ do T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough hFlush stdout -- No moving savefiles aside, to debug more easily. debugPossiblyPrintAndExit :: MonadServer m => Text -> m () debugPossiblyPrintAndExit t = do debug <- getsServer $ sdbgMsgSer . soptions when debug $ liftIO $ do T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough hFlush stdout exitFailure serverPrint :: MonadServer m => Text -> m () serverPrint t = liftIO $ do T.hPutStr stdout $! t <> "\n" -- hPutStrLn not atomic enough hFlush stdout saveServer :: MonadServer m => m () saveServer = do s <- getState ser <- getServer toSave <- chanSaveServer liftIO $ Save.saveToChan toSave (s, ser) -- | Dumps to stdout the RNG states from the start of the game. dumpRngs :: MonadServer m => RNGs -> m () dumpRngs rngs = liftIO $ do T.hPutStr stdout $! tshow rngs <> "\n" -- hPutStrLn not atomic enough hFlush stdout -- | Read the high scores dictionary. Return the empty table if no file. restoreScore :: forall m. MonadServer m => COps -> m HighScore.ScoreDict restoreScore COps{corule} = do benchmark <- getsServer $ sbenchmark . sclientOptions . soptions mscore <- if benchmark then return Nothing else do let scoresFileName = rscoresFileName corule dataDir <- liftIO appDataDir let path bkp = dataDir bkp <> scoresFileName configExists <- liftIO $ doesFileExist (path "") res <- liftIO $ Ex.try $ if configExists then do (vlib2, s) <- strictDecodeEOF (path "") if Save.compatibleVersion vlib2 Self.version then return $! s `seq` Just s else do let msg = "High score file from incompatible version of game detected." fail msg else return Nothing savePrefix <- getsServer $ ssavePrefixSer . soptions let defPrefix = ssavePrefixSer defServerOptions moveAside = savePrefix == defPrefix handler :: Ex.SomeException -> m (Maybe a) handler e = do when moveAside $ liftIO $ renameFile (path "") (path "bkp.") let msg = "High score restore failed." <+> (if moveAside then "The wrong file moved aside." else "") <+> "The error message is:" <+> (T.unwords . T.lines) (tshow e) serverPrint msg return Nothing either handler return res maybe (return HighScore.empty) return mscore -- | Generate a new score, register it and save. registerScore :: MonadServer m => Status -> FactionId -> m () registerScore status fid = do cops@COps{corule} <- getsState scops total <- getsState $ snd . calculateTotal fid let scoresFileName = rscoresFileName corule dataDir <- liftIO appDataDir -- Re-read the table in case it's changed by a concurrent game. scoreDict <- restoreScore cops gameModeId <- getsState sgameModeId time <- getsState stime dungeonTotal <- getsState sgold date <- liftIO getPOSIXTime tz <- liftIO $ getTimeZone $ posixSecondsToUTCTime date curChalSer <- getsServer $ scurChalSer . soptions factionD <- getsState sfactionD bench <- getsServer $ sbenchmark . sclientOptions . soptions noConfirmsGame <- isNoConfirmsGame sbandSpawned <- getsServer sbandSpawned let fact = factionD EM.! fid path = dataDir scoresFileName outputScore (worthMentioning, (ntable, pos)) = -- If testing or fooling around, dump instead of registering. -- In particular don't register score for the auto-* scenarios. if bench || noConfirmsGame || gunderAI fact then debugPossiblyPrint $ T.intercalate "\n" $ HighScore.showScore tz pos (HighScore.getRecord pos ntable) ++ [" Spawned groups:" <+> T.unwords (tail (T.words (tshow sbandSpawned)))] else let nScoreDict = EM.insert gameModeId ntable scoreDict in when worthMentioning $ liftIO $ encodeEOF path Self.version (nScoreDict :: HighScore.ScoreDict) theirVic (fi, fa) | isFoe fid fact fi && not (isHorrorFact fa) = Just $ gvictims fa | otherwise = Nothing theirVictims = EM.unionsWith (+) $ mapMaybe theirVic $ EM.assocs factionD ourVic (fi, fa) | isFriend fid fact fi = Just $ gvictims fa | otherwise = Nothing ourVictims = EM.unionsWith (+) $ mapMaybe ourVic $ EM.assocs factionD table = HighScore.getTable gameModeId scoreDict registeredScore = HighScore.register table total dungeonTotal time status date curChalSer (T.unwords $ tail $ T.words $ gname fact) ourVictims theirVictims (fhiCondPoly $ gkind fact) outputScore registeredScore -- | Invoke pseudo-random computation with the generator kept in the state. rndToAction :: MonadServer m => Rnd a -> m a rndToAction r = do gen1 <- getsServer srandom let (a, gen2) = St.runState r gen1 modifyServer $ \ser -> ser {srandom = gen2} return a -- | Gets a random generator from the user-submitted options or, if not present, -- generates one. getSetGen :: MonadServer m => Maybe SM.SMGen -> m SM.SMGen getSetGen mrng = case mrng of Just rnd -> return rnd Nothing -> liftIO SM.newSMGen LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/PeriodicM.hs0000644000000000000000000005600607346545000022425 0ustar0000000000000000-- | Server operations performed periodically in the game loop -- and related operations. module Game.LambdaHack.Server.PeriodicM ( spawnMonster, addManyActors , advanceTime, advanceTimeTraj, overheadActorTime, swapTime , updateCalm, leadLevelSwitch , endOrLoop #ifdef EXPOSE_INTERNAL -- * Internal operations , addAnyActor, rollSpawnPos, gameExit #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Int (Int64) import qualified Data.IntMap.Strict as IM import qualified Data.Text as T import Game.LambdaHack.Atomic import Game.LambdaHack.Common.Actor import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.CaveKind as CK import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.CommonM import Game.LambdaHack.Server.ItemM import Game.LambdaHack.Server.ItemRev import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.ProtocolM import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State -- | Spawn, possibly, a monster according to the level's actor groups. -- We assume heroes are never spawned. spawnMonster :: MonadServerAtomic m => m () spawnMonster = do COps{cocave} <- getsState scops arenas <- getsServer sarenas unless (ES.null arenas) $ do -- Do this on only one of the arenas to prevent micromanagement, -- e.g., spreading leaders across levels to bump monster generation. arena <- rndToAction $ oneOf $ ES.elems arenas Level{lkind, ldepth, lbig, ltime=localTime} <- getLevel arena let ck = okind cocave lkind if | CK.cactorCoeff ck == 0 || null (CK.cactorFreq ck) -> return () | EM.size lbig >= 300 -> -- probably not so rare, but debug anyway -- Gameplay consideration: not fun to slog through so many actors. -- Caves rarely start with more than 100. debugPossiblyPrint "Server: spawnMonster: too many big actors on level" | otherwise -> do totalDepth <- getsState stotalDepth lvlSpawned <- getsServer $ fromMaybe 0 . EM.lookup arena . snumSpawned let perMillion = monsterGenChance ldepth totalDepth lvlSpawned (CK.cactorCoeff ck) million = 1000000 k <- rndToAction $ randomR (1, million) when (k <= perMillion && localTime > timeTurn) $ do let numToSpawn | 25 * k <= perMillion = 3 | 10 * k <= perMillion = 2 | otherwise = 1 alt Nothing = Just 1 alt (Just n) = Just $ n + 1 modifyServer $ \ser -> ser { snumSpawned = EM.insert arena (lvlSpawned + numToSpawn) $ snumSpawned ser , sbandSpawned = IM.alter alt numToSpawn $ sbandSpawned ser } void $ addManyActors False lvlSpawned (CK.cactorFreq ck) arena localTime Nothing numToSpawn addAnyActor :: MonadServerAtomic m => Bool -> Int -> Freqs ItemKind -> LevelId -> Time -> Maybe Point -> m (Maybe (ActorId, Point)) addAnyActor summoned lvlSpawned actorFreq lid time mpos = do -- We bootstrap the actor by first creating the trunk of the actor's body -- that contains the fixed properties of all actors of that kind. cops <- getsState scops lvl@Level{ldepth} <- getLevel lid factionD <- getsState sfactionD freq <- prepareItemKind lvlSpawned ldepth actorFreq m2 <- rollItemAspect freq ldepth case m2 of NoNewItem -> do debugPossiblyPrint $ T.pack $ "Server: addAnyActor: trunk failed to roll" `showFailure` (summoned, lvlSpawned, actorFreq, freq, lid, time, mpos) return Nothing NewItem itemGroup itemKnownRaw itemFullRaw itemQuant -> do (fid, _) <- rndToAction $ frequency $ possibleActorFactions [itemGroup] (itemKind itemFullRaw) factionD let fact = factionD EM.! fid if isJust $ gquit fact then return Nothing -- the faction that spawns the monster is dead else do pers <- getsServer sperFid let allPers = ES.unions $ map (totalVisible . (EM.! lid)) $ EM.elems $ EM.delete fid pers -- expensive :( -- Checking skill would be more accurate, but skills can be -- inside organs, equipment, condition organs, created organs, etc. freqNames = map fst $ IK.ifreq $ itemKind itemFullRaw mobile = IK.MOBILE `elem` freqNames aquatic = IK.AQUATIC `elem` freqNames mrolledPos <- case mpos of Just{} -> return mpos Nothing -> do rollPos <- getsState $ rollSpawnPos cops allPers mobile aquatic lid lvl fid rndToAction rollPos case mrolledPos of Just pos -> Just . (\aid -> (aid, pos)) <$> registerActor summoned itemKnownRaw (itemFullRaw, itemQuant) fid pos lid time Nothing -> do debugPossiblyPrint "Server: addAnyActor: failed to find any free position" return Nothing addManyActors :: MonadServerAtomic m => Bool -> Int -> Freqs ItemKind -> LevelId -> Time -> Maybe Point -> Int -> m Bool addManyActors summoned lvlSpawned actorFreq lid time mpos howMany = assert (howMany >= 1) $ do mInitialLAidPos <- case mpos of Just pos -> return $ Just ([], pos) Nothing -> (\(aid, pos) -> ([aid], pos)) <$$> addAnyActor summoned lvlSpawned actorFreq lid time Nothing case mInitialLAidPos of Nothing -> return False -- suspect content; server debug elsewhere Just (laid, pos) -> do cops@COps{coTileSpeedup} <- getsState scops lvl <- getLevel lid let validTile t = not $ Tile.isNoActor coTileSpeedup t ps = nearbyFreePoints cops lvl validTile pos psNeeded = take (howMany - length laid) ps when (length psNeeded < howMany - length laid) $ debugPossiblyPrint $ "Server: addManyActors: failed to find enough free positions at" <+> tshow (lid, pos) maidposs <- forM psNeeded $ addAnyActor summoned lvlSpawned actorFreq lid time . Just case laid ++ map fst (catMaybes maidposs) of [] -> return False aid : _ -> do b <- getsState $ getActorBody aid mleader <- getsState $ gleader . (EM.! bfid b) . sfactionD when (isNothing mleader) $ setFreshLeader (bfid b) aid return True rollSpawnPos :: COps -> ES.EnumSet Point -> Bool -> Bool -> LevelId -> Level -> FactionId -> State -> Rnd (Maybe Point) rollSpawnPos COps{coTileSpeedup} visible mobile aquatic lid lvl@Level{larea} fid s = do let inhabitants = foeRegularList fid lid s nearInh !d !p = any (\ !b -> chessDist (bpos b) p < d) inhabitants farInh !d !p = all (\ !b -> chessDist (bpos b) p > d) inhabitants (_, xspan, yspan) = spanArea larea averageSpan = (xspan + yspan) `div` 2 distantMiddle !d !p = chessDist p (middlePoint larea) < d -- Don't spawn very far from foes, to keep the player entertained, -- but not too close, so that standing on positions with better -- visibility does not influence the spawn places too often, -- to avoid unnatural position micromanagement using AI predictability. condList | mobile = [ \p -> nearInh (max 15 $ averageSpan `div` 2) p && farInh 10 p , \p -> nearInh (max 15 $ 2 * averageSpan `div` 3) p && farInh 5 p ] | otherwise = [ distantMiddle 8 , distantMiddle 16 , distantMiddle 24 , distantMiddle 26 , distantMiddle 28 , distantMiddle 30 ] -- Not considering TK.OftenActor, because monsters emerge from hidden ducts, -- which are easier to hide in crampy corridors that lit halls. findPosTry2 (if mobile then 500 else 50) lvl ( \p !t -> Tile.isWalkable coTileSpeedup t && not (Tile.isNoActor coTileSpeedup t) && not (occupiedBigLvl p lvl) && not (occupiedProjLvl p lvl) ) (map (\f p _ -> f p) condList) (\ !p t -> farInh 3 p -- otherwise actors in dark rooms swarmed && not (p `ES.member` visible) -- visibility and plausibility && (not aquatic || Tile.isAquatic coTileSpeedup t)) [ \ !p _ -> farInh 3 p && not (p `ES.member` visible) , \ !p _ -> farInh 2 p -- otherwise actors hit on entering level && not (p `ES.member` visible) , \ !p _ -> not (p `ES.member` visible) ] -- | Advance the move time for the given actor. advanceTime :: MonadServerAtomic m => ActorId -> Int -> Bool -> m () advanceTime aid percent breakStasis = do b <- getsState $ getActorBody aid actorMaxSk <- getsState $ getActorMaxSkills aid let t = timeDeltaPercent (ticksPerMeter $ gearSpeed actorMaxSk) percent -- @t@ may be negative; that's OK. modifyServer $ \ser -> ser {sactorTime = ageActor (bfid b) (blid b) aid t $ sactorTime ser} when breakStasis $ modifyServer $ \ser -> ser {sactorStasis = ES.delete aid (sactorStasis ser)} -- actor moved, so he broke the time stasis, he can be -- paralyzed as well as propelled again -- | Advance the trajectory following time for the given actor. advanceTimeTraj :: MonadServerAtomic m => ActorId -> m () advanceTimeTraj aid = do b <- getsState $ getActorBody aid let speedTraj = case btrajectory b of Nothing -> error $ "" `showFailure` b Just (_, speed) -> speed t = ticksPerMeter speedTraj -- @t@ may be negative; that's OK. modifyServer $ \ser -> ser {strajTime = ageActor (bfid b) (blid b) aid t $ strajTime ser} -- | Add communication overhead time delta to all non-projectile, non-dying -- faction's actors, except the leader. Effectively, this limits moves -- of a faction on a level to 10, regardless of the number of actors -- and their speeds. To avoid animals suddenly acting extremely sluggish -- whenever monster's leader visits a distant arena that has a crowd -- of animals, overhead applies only to actors on the same level. -- Since the number of active levels is limited, this bounds the total moves -- per turn of each faction as well. -- -- Leader is immune from overhead and so he is faster than other faction -- members and of equal speed to leaders of other factions (of equal -- base speed) regardless how numerous the faction is. -- Thanks to this, there is no problem with leader of a numerous faction -- having very long UI turns, introducing UI lag. overheadActorTime :: MonadServerAtomic m => FactionId -> LevelId -> m () overheadActorTime fid lid = do -- Only non-projectiles processed, because @strajTime@ ignored. actorTimeFid <- getsServer $ (EM.! fid) . sactorTime let actorTimeLid = actorTimeFid EM.! lid getActorB <- getsState $ flip getActorBody mleader <- getsState $ gleader . (EM.! fid) . sfactionD let f !aid !time = let body = getActorB aid in if bhp body > 0 -- speed up all-move-at-once carcass removal && Just aid /= mleader -- leader fast, for UI to be fast then timeShift time (Delta timeClip) else time actorTimeLid2 = EM.mapWithKey f actorTimeLid actorTimeFid2 = EM.insert lid actorTimeLid2 actorTimeFid modifyServer $ \ser -> ser {sactorTime = EM.insert fid actorTimeFid2 $ sactorTime ser} -- | Swap the relative move times of two actors (e.g., when switching -- a UI leader). Notice that their trajectory move times are not swapped. swapTime :: MonadServerAtomic m => ActorId -> ActorId -> m () swapTime source target = do sb <- getsState $ getActorBody source tb <- getsState $ getActorBody target slvl <- getsState $ getLocalTime (blid sb) tlvl <- getsState $ getLocalTime (blid tb) btime_sb <- getsServer $ fromJust . lookupActorTime (bfid sb) (blid sb) source . sactorTime btime_tb <- getsServer $ fromJust . lookupActorTime (bfid tb) (blid tb) target . sactorTime let lvlDelta = slvl `timeDeltaToFrom` tlvl bDelta = btime_sb `timeDeltaToFrom` btime_tb sdelta = timeDeltaSubtract lvlDelta bDelta tdelta = timeDeltaReverse sdelta -- Equivalent, for the assert: let !_A = let sbodyDelta = btime_sb `timeDeltaToFrom` slvl tbodyDelta = btime_tb `timeDeltaToFrom` tlvl sgoal = slvl `timeShift` tbodyDelta tgoal = tlvl `timeShift` sbodyDelta sdelta' = sgoal `timeDeltaToFrom` btime_sb tdelta' = tgoal `timeDeltaToFrom` btime_tb in assert (sdelta == sdelta' && tdelta == tdelta' `blame` ( slvl, tlvl, btime_sb, btime_tb , sdelta, sdelta', tdelta, tdelta' )) () when (sdelta /= Delta timeZero) $ modifyServer $ \ser -> ser {sactorTime = ageActor (bfid sb) (blid sb) source sdelta $ sactorTime ser} when (tdelta /= Delta timeZero) $ modifyServer $ \ser -> ser {sactorTime = ageActor (bfid tb) (blid tb) target tdelta $ sactorTime ser} updateCalm :: MonadServerAtomic m => ActorId -> Int64 -> m () updateCalm target deltaCalm = do tb <- getsState $ getActorBody target actorMaxSk <- getsState $ getActorMaxSkills target let calmMax64 = xM $ Ability.getSk Ability.SkMaxCalm actorMaxSk execUpdAtomic $ UpdRefillCalm target deltaCalm when (bcalm tb < calmMax64 && bcalm tb + deltaCalm >= calmMax64) $ return () -- We don't dominate the actor here, because if so, players would -- disengage after one of their actors is dominated and wait for him -- to regenerate Calm. This is unnatural and boring. Better fight -- and hope he gets his Calm again to 0 and then defects back. -- We could instead tell here that Calm is fully regenerated, -- but that would be too verbose. leadLevelSwitch :: MonadServerAtomic m => m () leadLevelSwitch = do COps{cocave} <- getsState scops factionD <- getsState sfactionD -- Leader switching between levels can be done by the client -- (e.g,. UI client of the human) or by the server -- (the frequency of leader level switching done by the server -- is controlled by @RuleKind.rleadLevelClips@). Regardless, the server -- alwayw does a subset of the switching, e.g., when the old leader dies -- and no other actor of the faction resides on his level. -- Here we check if the server is permitted to handle the mundane cases. let serverMaySwitch fact = bannedPointmanSwitchBetweenLevels fact -- client banned from switching, so the sever has to step in || gunderAI fact -- a hack to help AI, until AI client can switch levels flipFaction (_, fact) | not $ serverMaySwitch fact = return () flipFaction (fid, fact) = case gleader fact of Nothing -> return () Just leader -> do body <- getsState $ getActorBody leader let !_A = assert (fid == bfid body) () s <- getsServer $ (EM.! fid) . sclientStates let leaderStuck = actorWaits body lvlsRaw = [ ((lid, lvl), (allSeen, as)) | (lid, lvl) <- EM.assocs $ sdungeon s , lid /= blid body || not leaderStuck , let asRaw = -- Drama levels ignored, hence @Regular@. fidActorRegularAssocs fid lid s isAlert (_, b) = case bwatch b of WWatch -> True WWait n -> n == 0 WSleep -> False WWake -> True -- probably in danger (alert, relaxed) = partition isAlert asRaw as = alert ++ relaxed -- best switch leader to alert , not (null as) , let allSeen = lexpl lvl <= lseen lvl || CK.cactorCoeff (okind cocave $ lkind lvl) > 150 && not (fhasGender $ gkind fact) ] (lvlsSeen, lvlsNotSeen) = partition (fst . snd) lvlsRaw -- Monster AI changes leadership mostly to move from level -- to level and, in particular, to quickly bring troops -- to the frontline level and so prevent human from killing -- monsters at numerical advantage. -- However, an AI boss that can't move between levels -- disrupts this by hogging leadership. To prevent that, -- assuming the boss resides below the frontline level, -- only the two shallowest levels that are not yet fully -- explored are considered to choose the new leader from. -- This frontier moves as the levels are explored or emptied -- and sometimes the level with the boss is counted among -- them, but it never happens in the crucial periods when -- AI armies are transferred from level to level. f ((_, lvl), _) = ldepth lvl lvls = lvlsSeen ++ take 2 (sortBy (comparing f) lvlsNotSeen) -- Actors on desolate levels (not many own or enemy non-projectiles) -- tend to become (or stay) leaders so that they can join the main -- force where it matters ASAP. Unfortunately, this keeps hero -- scouts as leader, but foes spawn very fast early on , -- so they give back leadership rather quickly to let others follow. -- We count non-mobile and sleeping actors, because they may -- be dangerous, especially if adjacent to stairs. let overOwnStash b = Just (blid b, bpos b) == gstash fact freqList = [ (k, (lid, aid)) | ((lid, lvl), (_, (aid, b) : rest)) <- lvls , let len = min 20 (EM.size $ lbig lvl) n = 1000000 `div` (1 + len) -- Visit the stash guard rarely, but not too -- rarely, to regen Calm and fling at foes. k = max 1 $ if null rest && overOwnStash b then n `div` 30 else n ] closeToFactStash (fid2, fact2) = case gstash fact2 of Just (lid, pos) -> (fid == fid2 || isFoe fid (factionD EM.! fid) fid2) && lid == blid body && chessDist pos (bpos body) == 1 -- visible Nothing -> False closeToEnemyStash = any closeToFactStash $ EM.assocs factionD foes <- getsState $ foeRegularList fid (blid body) ours <- getsState $ map snd <$> fidActorRegularAssocs fid (blid body) let foesClose = filter (\b -> chessDist (bpos body) (bpos b) <= 2) foes oursCloseMelee = filter (\b -> chessDist (bpos body) (bpos b) <= 2 && bweapon b - bweapBenign b > 0) ours canHelpMelee = not leaderStuck && length oursCloseMelee >= 2 && not (null foesClose) && not (all (\b -> any (adjacent (bpos b) . bpos) foes) oursCloseMelee) unless (closeToEnemyStash || canHelpMelee || null freqList) $ do (lid, a) <- rndToAction $ frequency $ toFreq "leadLevel" freqList unless (lid == blid body) $ -- flip levels rather than actors setFreshLeader fid a mapM_ flipFaction $ EM.assocs factionD -- | Continue or exit or restart the game. endOrLoop :: (MonadServerAtomic m, MonadServerComm m) => m () -> (Maybe (GroupName ModeKind) -> m ()) -> m () {-# INLINE endOrLoop #-} endOrLoop loop restart = do factionD <- getsState sfactionD let inGame fact = case gquit fact of Nothing -> True Just Status{stOutcome=Camping} -> True _ -> False gameOver = not $ any inGame $ EM.elems factionD let getQuitter fact = case gquit fact of Just Status{stOutcome=Restart, stNewGame} -> stNewGame _ -> Nothing quitters = mapMaybe getQuitter $ EM.elems factionD restartNeeded = gameOver || not (null quitters) let isCamper fact = case gquit fact of Just Status{stOutcome=Camping} -> True _ -> False campers = filter (isCamper . snd) $ EM.assocs factionD -- Wipe out the quit flag for the savegame files. mapM_ (\(fid, fact) -> execUpdAtomic $ UpdQuitFaction fid (gquit fact) Nothing Nothing) campers swriteSave <- getsServer swriteSave sstopAfterGameOver <- getsServer $ sstopAfterGameOver . soptions when swriteSave $ do modifyServer $ \ser -> ser {swriteSave = False} writeSaveAll True False if | gameOver && sstopAfterGameOver -> gameExit | restartNeeded -> restart (listToMaybe quitters) | not $ null campers -> gameExit -- and @loop@ is not called | otherwise -> loop -- continue current game gameExit :: (MonadServerAtomic m, MonadServerComm m) => m () gameExit = do -- debugPossiblyPrint "Server: Verifying all perceptions." -- Verify that the possibly not saved caches are equal to future -- reconstructed. Otherwise, save/restore would change game state. -- This is done even in released binaries, because it only prolongs -- game shutdown a bit. The same checks at each periodic game save -- would icrease the game saving lag, so they are normally avoided. verifyCaches -- Kill all clients, including those that did not take part -- in the current game. -- Clients exit not now, but after they print all ending screens. -- debugPossiblyPrint "Server: Killing all clients." killAllClients -- debugPossiblyPrint "Server: All clients killed." return () LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/ProtocolM.hs0000644000000000000000000002236107346545000022465 0ustar0000000000000000-- | The server definitions for the server-client communication protocol. module Game.LambdaHack.Server.ProtocolM ( -- * The communication channels CliSerQueue, ConnServerDict, ChanServer(..) -- * The server-client communication monad , MonadServerComm ( getsDict -- exposed only to be implemented, not used , putDict -- exposed only to be implemented, not used , liftIO -- exposed only to be implemented, not used ) -- * Protocol , sendUpdate, sendUpdateCheck, sendUpdNoState , sendSfx, sendQueryAI, sendQueryUI -- * Assorted , killAllClients, childrenServer, updateConn, tryRestore #ifdef EXPOSE_INTERNAL -- * Internal operations , writeQueue, readQueueAI, readQueueUI, newQueue #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Concurrent import Control.Concurrent.Async import qualified Data.EnumMap.Strict as EM import Data.Key (mapWithKeyM_) import System.FilePath import System.IO.Unsafe (unsafePerformIO) import Game.LambdaHack.Atomic import Game.LambdaHack.Client (RequestAI, RequestUI, Response (..)) import Game.LambdaHack.Common.ClientOptions (sbenchmark) import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.File import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import qualified Game.LambdaHack.Common.Save as Save import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Thread import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Server.DebugM import Game.LambdaHack.Server.MonadServer hiding (liftIO) import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State writeQueue :: MonadServerComm m => Response -> CliSerQueue Response -> m () {-# INLINE writeQueue #-} writeQueue cmd responseS = liftIO $ putMVar responseS cmd readQueueAI :: MonadServerComm m => CliSerQueue RequestAI -> m RequestAI {-# INLINE readQueueAI #-} readQueueAI requestS = liftIO $ takeMVar requestS readQueueUI :: MonadServerComm m => CliSerQueue RequestUI -> m RequestUI {-# INLINE readQueueUI #-} readQueueUI requestS = liftIO $ takeMVar requestS newQueue :: IO (CliSerQueue a) newQueue = newEmptyMVar type CliSerQueue = MVar -- | Connection information for all factions, indexed by faction identifier. type ConnServerDict = EM.EnumMap FactionId ChanServer -- | Connection channel between the server and a single client. data ChanServer = ChanServer { responseS :: CliSerQueue Response , requestAIS :: CliSerQueue RequestAI , requestUIS :: Maybe (CliSerQueue RequestUI) } -- | The server monad with the ability to communicate with clients. class MonadServer m => MonadServerComm m where getsDict :: (ConnServerDict -> a) -> m a putDict :: ConnServerDict -> m () liftIO :: IO a -> m a getDict :: MonadServerComm m => m ConnServerDict getDict = getsDict id -- | If the @AtomicFail@ conditions hold, send a command to client, -- otherwise do nothing. sendUpdate :: (MonadServerAtomic m, MonadServerComm m) => FactionId -> UpdAtomic -> m () sendUpdate !fid !cmd = do succeeded <- execUpdAtomicFidCatch fid cmd when succeeded $ sendUpd fid cmd -- | Send a command to client, crashing if the @AtomicFail@ conditions -- don't hold when executed on the client's state. sendUpdateCheck :: (MonadServerAtomic m, MonadServerComm m) => FactionId -> UpdAtomic -> m () sendUpdateCheck !fid !cmd = do execUpdAtomicFid fid cmd sendUpd fid cmd sendUpd :: MonadServerComm m => FactionId -> UpdAtomic -> m () sendUpd !fid !cmd = do chan <- getsDict (EM.! fid) s <- getsServer $ (EM.! fid) . sclientStates let resp = RespUpdAtomic s cmd debug <- getsServer $ sniff . soptions when debug $ debugResponse fid resp writeQueue resp $ responseS chan sendUpdNoState :: MonadServerComm m => FactionId -> UpdAtomic -> m () sendUpdNoState !fid !cmd = do chan <- getsDict (EM.! fid) let resp = RespUpdAtomicNoState cmd debug <- getsServer $ sniff . soptions when debug $ debugResponse fid resp writeQueue resp $ responseS chan sendSfx :: MonadServerComm m => FactionId -> SfxAtomic -> m () sendSfx !fid !sfx = do let resp = RespSfxAtomic sfx debug <- getsServer $ sniff . soptions when debug $ debugResponse fid resp chan <- getsDict (EM.! fid) case chan of ChanServer{requestUIS=Just{}} -> writeQueue resp $ responseS chan _ -> return () sendQueryAI :: MonadServerComm m => FactionId -> ActorId -> m RequestAI sendQueryAI fid aid = do let respAI = RespQueryAI aid debug <- getsServer $ sniff . soptions when debug $ debugResponse fid respAI chan <- getsDict (EM.! fid) req <- do writeQueue respAI $ responseS chan readQueueAI $ requestAIS chan when debug $ debugRequestAI aid return req sendQueryUI :: (MonadServerAtomic m, MonadServerComm m) => Response -> FactionId -> ActorId -> m RequestUI sendQueryUI respUI fid _aid = do debug <- getsServer $ sniff . soptions when debug $ debugResponse fid respUI chan <- getsDict (EM.! fid) req <- do writeQueue respUI $ responseS chan readQueueUI $ fromJust $ requestUIS chan when debug $ debugRequestUI _aid return req killAllClients :: (MonadServerAtomic m, MonadServerComm m) => m () killAllClients = do d <- getDict let sendKill fid _ = sendUpdNoState fid $ UpdKillExit fid -- We can't interate over sfactionD, because client can be from an old game. -- For the same reason we can't look up and send client's state. mapWithKeyM_ sendKill d -- Global variable for all children threads of the server. childrenServer :: MVar [Async ()] {-# NOINLINE childrenServer #-} childrenServer = unsafePerformIO (newMVar []) -- | Update connections to the new definition of factions. -- Connect to clients in old or newly spawned threads -- that read and write directly to the channels. updateConn :: (MonadServerAtomic m, MonadServerComm m) => (FactionId -> ChanServer -> IO ()) -> m () updateConn executorClient = do -- Prepare connections based on factions. oldD <- getDict let mkChanServer :: Faction -> IO ChanServer mkChanServer fact = do responseS <- newQueue requestAIS <- newQueue requestUIS <- if fhasUI $ gkind fact then assert (EM.null oldD) $ Just <$> newQueue else return Nothing return ChanServer{..} forkClient fid = forkChild childrenServer . executorClient fid factionD <- getsState sfactionD if EM.null oldD then do -- Easy case, nothing to recycle, frontend not spawned yet. newD <- liftIO $ mapM mkChanServer factionD putDict newD liftIO $ mapWithKeyM_ forkClient newD else do -- Hard case, but we know there is exactly one UI connection in oldD, -- so we can reuse it for any new UI faction (to keep history). -- UI session (history in particular) is preserved even over game -- save and reload. It gets saved with the savefile of the team -- that is a UI faction and restored intact. However, when a new game -- is started from commandline (@--newGame@), even if it's using the same -- save prefix (@--savePrefix@), the session data is often lost. -- AI factions don't care which client they use, so we don't always -- preserve the old assignments either of factions or teams. let -- Find the new UI faction. (fidUI, _) = fromJust $ find (fhasUI . gkind . snd) $ EM.assocs factionD -- Swap UI and AI connections around. swappedD = case find (isJust . requestUIS . snd) $ EM.assocs oldD of Nothing -> error "updateConn: no UI connection found" Just (fid, conn) -> if fid == fidUI then oldD -- UI connection at the same place; nothing to do else let -- Move the AI connection that was at new UI faction spot, -- to the freed old UI spot. alt _ = EM.lookup fidUI oldD in EM.alter alt fid $ EM.insert fidUI conn oldD -- Add extra AI connections. extraFacts = EM.filterWithKey (\fid _ -> EM.notMember fid swappedD) factionD extraD <- liftIO $ mapM mkChanServer extraFacts let exclusiveUnion = EM.unionWith $ \_ _ -> error "forbidden duplicate" newD = swappedD `exclusiveUnion` extraD putDict newD -- Spawn the extra AI client threads. liftIO $ mapWithKeyM_ forkClient extraD tryRestore :: MonadServerComm m => m (Maybe (State, StateServer)) tryRestore = do COps{corule} <- getsState scops soptions <- getsServer soptions if sbenchmark $ sclientOptions soptions then return Nothing else do let prefix = ssavePrefixSer soptions fileName = prefix <> Save.saveNameSer corule res <- liftIO $ Save.restoreGame corule (sclientOptions soptions) fileName let cfgUIName = rcfgUIName corule (configText, _) = rcfgUIDefault corule dataDir <- liftIO appDataDir liftIO $ tryWriteFile (dataDir cfgUIName) configText return $! res LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/ServerOptions.hs0000644000000000000000000000712007346545000023365 0ustar0000000000000000-- | Server and client game state types and operations. module Game.LambdaHack.Server.ServerOptions ( ServerOptions(..), RNGs(..), defServerOptions ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Common.ClientOptions import Game.LambdaHack.Common.Faction import Game.LambdaHack.Content.ModeKind import Game.LambdaHack.Definition.Defs -- | Options that affect the behaviour of the server (including game rules). data ServerOptions = ServerOptions { sknowMap :: Bool , sknowEvents :: Bool , sknowItems :: Bool , sniff :: Bool , sallClear :: Bool , sboostRandomItem :: Bool , sgameMode :: Maybe (GroupName ModeKind) , sautomateAll :: Bool , skeepAutomated :: Bool , sdungeonRng :: Maybe SM.SMGen , smainRng :: Maybe SM.SMGen , snewGameSer :: Bool , scurChalSer :: Challenge , sdumpInitRngs :: Bool , ssavePrefixSer :: String , sdbgMsgSer :: Bool , sassertExplored :: Maybe Int , sshowItemSamples :: Bool , sstopAfterGameOver :: Bool , sclientOptions :: ClientOptions -- The client debug inside server debug only holds the client commandline -- options and is never updated with config options, etc. } deriving Show data RNGs = RNGs { dungeonRandomGenerator :: Maybe SM.SMGen , startingRandomGenerator :: Maybe SM.SMGen } instance Show RNGs where show RNGs{..} = let args = [ maybe "" (\gen -> "--setDungeonRng \"" ++ show gen ++ "\"") dungeonRandomGenerator , maybe "" (\gen -> "--setMainRng \"" ++ show gen ++ "\"") startingRandomGenerator ] in unwords args instance Binary ServerOptions where put ServerOptions{..} = do put sknowMap put sknowEvents put sknowItems put sniff put sallClear put sboostRandomItem put sgameMode put sautomateAll put skeepAutomated put scurChalSer put ssavePrefixSer put sdbgMsgSer put sassertExplored put sshowItemSamples put sclientOptions get = do sknowMap <- get sknowEvents <- get sknowItems <- get sniff <- get sallClear <- get sboostRandomItem <- get sgameMode <- get sautomateAll <- get skeepAutomated <- get scurChalSer <- get ssavePrefixSer <- get sdbgMsgSer <- get sassertExplored <- get sshowItemSamples <- get sclientOptions <- get let sdungeonRng = Nothing smainRng = Nothing snewGameSer = False sdumpInitRngs = False sstopAfterGameOver = False return $! ServerOptions{..} instance Binary RNGs where put RNGs{..} = do put (show dungeonRandomGenerator) put (show startingRandomGenerator) get = do dg <- get sg <- get let dungeonRandomGenerator = read dg startingRandomGenerator = read sg return $! RNGs{..} -- | Default value of server options. defServerOptions :: ServerOptions defServerOptions = ServerOptions { sknowMap = False , sknowEvents = False , sknowItems = False , sniff = False , sallClear = False , sboostRandomItem = False , sgameMode = Nothing , sautomateAll = False , skeepAutomated = False , sdungeonRng = Nothing , smainRng = Nothing , snewGameSer = False , scurChalSer = defaultChallenge , sdumpInitRngs = False , ssavePrefixSer = "" , sdbgMsgSer = False , sassertExplored = Nothing , sshowItemSamples = False , sstopAfterGameOver = False , sclientOptions = defClientOptions } LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/StartM.hs0000644000000000000000000005332707346545000021767 0ustar0000000000000000-- | Operations for starting and restarting the game. module Game.LambdaHack.Server.StartM ( initPer, reinitGame, gameReset, applyDebug #ifdef EXPOSE_INTERNAL -- * Internal operations , sampleTrunks, sampleItems , mapFromFuns, resetFactions, populateDungeon, findEntryPoss #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.Trans.State.Strict as St import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import Data.Key (mapWithKeyM_) import qualified Data.Map.Strict as M import qualified Data.Set as S import qualified Data.Text as T import qualified NLP.Miniutter.English as MU import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Atomic import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.Area import Game.LambdaHack.Common.Faction import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Kind import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Misc import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import qualified Game.LambdaHack.Common.Tile as Tile import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import qualified Game.LambdaHack.Content.CaveKind as CK import Game.LambdaHack.Content.FactionKind import Game.LambdaHack.Content.ItemKind (ItemKind) import qualified Game.LambdaHack.Content.ItemKind as IK import Game.LambdaHack.Content.ModeKind import qualified Game.LambdaHack.Core.Dice as Dice import Game.LambdaHack.Core.Frequency import Game.LambdaHack.Core.Random import qualified Game.LambdaHack.Definition.Ability as Ability import qualified Game.LambdaHack.Definition.Color as Color import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Definition.Flavour import Game.LambdaHack.Server.CommonM import qualified Game.LambdaHack.Server.DungeonGen as DungeonGen import Game.LambdaHack.Server.Fov import Game.LambdaHack.Server.ItemM import Game.LambdaHack.Server.ItemRev import Game.LambdaHack.Server.MonadServer import Game.LambdaHack.Server.ServerOptions import Game.LambdaHack.Server.State initPer :: MonadServer m => m () initPer = do ( sfovLitLid, sfovClearLid, sfovLucidLid ,sperValidFid, sperCacheFid, sperFid ) <- getsState perFidInDungeon modifyServer $ \ser -> ser { sfovLitLid, sfovClearLid, sfovLucidLid , sperValidFid, sperCacheFid, sperFid } reinitGame :: MonadServerAtomic m => FactionDict -> m () reinitGame factionDold = do COps{coitem} <- getsState scops pers <- getsServer sperFid ServerOptions{scurChalSer, sknowMap, sshowItemSamples, sclientOptions} <- getsServer soptions -- This state is quite small, fit for transmition to the client. -- The biggest part is content, which needs to be updated in clients -- at this point to keep them in sync with changes on the server. s <- getState discoS <- getsState sdiscoKind -- Thanks to the following, for any item with not hidden identity, -- the client has its kind from the start. The client needs to know this -- to have a fast way (faster that looking for @PresentAs@ flag on a list) -- of determining whether an item kind is already identified -- or needs identification. let discoKindFiltered = let f kindId = isNothing $ IK.getMandatoryPresentAsFromKind $ okind coitem kindId in EM.filter f discoS defL | sknowMap = s | otherwise = localFromGlobal s defLocal = updateDiscoKind (const discoKindFiltered) defL factionD <- getsState sfactionD clientStatesOld <- getsServer sclientStates metaBackupOld <- getsServer smetaBackup -- Some item kinds preserve their identity and flavour throughout -- the whole meta-game, until the savefiles are removed. -- These are usually not common man-made items, because these can be made -- in many flavours so it may be hard to recognize them. -- Character backstories and rare artifacts are uncommon enough -- to requiring learning their identify only once. -- However, the exact properties of even natural items may vary, -- so the random aspects of items, stored in @sdiscoAspect@ -- are not preserved (a lot of other state components would need -- to be partially preserved, too, both on server and clients). let inMetaGame kindId = IK.SetFlag Ability.MetaGame `elem` IK.iaspects (okind coitem kindId) metaDiscoOldFid = EM.map (EM.filter inMetaGame . sdiscoKind) clientStatesOld fidToTeam :: FactionId -> TeamContinuity fidToTeam fid = fteam $ gkind $ factionDold EM.! fid metaDiscoOldTeam = EM.fromList $ map (first fidToTeam) $ EM.assocs metaDiscoOldFid exclusiveUnion = EM.unionWith $ \_ _ -> error "forbidden duplicate" metaDiscoAll = metaDiscoOldTeam `exclusiveUnion` metaBackupOld currentTeams = ES.fromList $ map (fteam . gkind) $ EM.elems factionD metaBackupNew = EM.withoutKeys metaDiscoAll currentTeams stateNew fact = case EM.lookup (fteam $ gkind fact) metaDiscoAll of Nothing -> defLocal Just disco -> updateDiscoKind (disco `EM.union`) defLocal clientStatesNew = EM.map stateNew factionD modifyServer $ \ser -> ser { sclientStates = clientStatesNew , smetaBackup = metaBackupNew } let updRestart fid = UpdRestart fid (pers EM.! fid) (clientStatesNew EM.! fid) scurChalSer sclientOptions mapWithKeyM_ (\fid _ -> do -- Different seed for each client, to make sure behaviour is varied. gen1 <- getsServer srandom let (clientRandomSeed, gen2) = SM.splitSMGen gen1 modifyServer $ \ser -> ser {srandom = gen2} execUpdAtomic $ updRestart fid clientRandomSeed) factionD dungeon <- getsState sdungeon let sactorTime = EM.map (const (EM.map (const EM.empty) dungeon)) factionD strajTime = EM.map (const (EM.map (const EM.empty) dungeon)) factionD modifyServer $ \ser -> ser {sactorTime, strajTime} when sshowItemSamples $ do genOrig <- getsServer srandom uniqueSetOrig <- getsServer suniqueSet genOld <- getsServer sgenerationAn genSampleTrunks <- sampleTrunks dungeon genSampleItems <- sampleItems dungeon let sgenerationAn = EM.unions [genSampleTrunks, genSampleItems, genOld] modifyServer $ \ser -> ser {sgenerationAn} -- Make sure the debug generations don't affect future RNG behaviour. -- However, in the long run, AI behaviour is affected anyway, -- because the items randomly chosen for AI actions are ordered by their -- @ItemId@, which is affected by the sample item generation. modifyServer $ \ser -> ser {srandom = genOrig, suniqueSet = uniqueSetOrig} populateDungeon mapM_ (\fid -> mapM_ (updatePer fid) (EM.keys dungeon)) (EM.keys factionD) -- For simplicity only spawnable actors are taken into account, not starting -- actors of any faction nor summonable actors. sampleTrunks :: MonadServerAtomic m => Dungeon -> m GenerationAnalytics sampleTrunks dungeon = do COps{cocave, coitem} <- getsState scops factionD <- getsState sfactionD let getGroups Level{lkind} = map fst $ CK.cactorFreq $ okind cocave lkind groups = S.elems $ S.fromList $ concatMap getGroups $ EM.elems dungeon addGroupToSet !s0 !grp = ofoldlGroup' coitem grp (\s _ ik _ -> ES.insert ik s) s0 trunkKindIds = ES.elems $ foldl' addGroupToSet ES.empty groups minLid = fst $ minimumBy (comparing (ldepth . snd)) $ EM.assocs dungeon Level{ldepth} <- getLevel minLid let regItem itemKindId = do let itemKind = okind coitem itemKindId freq = pure (IK.HORROR, itemKindId, itemKind) case runFrequency $ possibleActorFactions [] itemKind factionD of [] -> error "sampleTrunks: null faction frequency" (_, (fid, _)) : _ -> do let c = CTrunk fid minLid originPoint jfid = Just fid m2 <- rollItemAspect freq ldepth case m2 of NoNewItem -> error "sampleTrunks: can't create actor trunk" NewItem _ (ItemKnown kindIx ar _) itemFullRaw itemQuant -> do let itemKnown = ItemKnown kindIx ar jfid itemFull = itemFullRaw {itemBase = (itemBase itemFullRaw) {jfid}} Just <$> registerItem False (itemFull, itemQuant) itemKnown c miids <- mapM regItem trunkKindIds return $! EM.singleton STrunk $ EM.fromDistinctAscList $ zip (catMaybes miids) $ repeat 0 -- For simplicity, only actors generated on the ground are taken into account. -- not starting items of any actors nor items that can be create by effects -- occuring in the game. sampleItems :: MonadServerAtomic m => Dungeon -> m GenerationAnalytics sampleItems dungeon = do COps{cocave, coitem} <- getsState scops let getGroups Level{lkind} = map fst $ CK.citemFreq $ okind cocave lkind groups = S.elems $ S.fromList $ concatMap getGroups $ EM.elems dungeon addGroupToSet !s0 !grp = ofoldlGroup' coitem grp (\s _ ik _ -> ES.insert ik s) s0 itemKindIds = ES.elems $ foldl' addGroupToSet ES.empty groups minLid = fst $ minimumBy (comparing (ldepth . snd)) $ EM.assocs dungeon Level{ldepth} <- getLevel minLid let regItem itemKindId = do let itemKind = okind coitem itemKindId freq = pure (IK.HORROR, itemKindId, itemKind) c = CFloor minLid originPoint m2 <- rollItemAspect freq ldepth case m2 of NoNewItem -> error "sampleItems: can't create sample item" NewItem _ itemKnown itemFull _ -> Just <$> registerItem False (itemFull, (0, [])) itemKnown c miids <- mapM regItem itemKindIds return $! EM.singleton SItem $ EM.fromDistinctAscList $ zip (catMaybes miids) $ repeat 0 mapFromFuns :: Ord b => [a] -> [a -> b] -> M.Map b a mapFromFuns domain = let fromFun f m1 = let invAssocs = map (\c -> (f c, c)) domain m2 = M.fromList invAssocs in m2 `M.union` m1 in foldr fromFun M.empty resetFactions :: ContentData FactionKind -> Dice.AbsDepth -> ModeKind -> Bool -> Rnd FactionDict resetFactions cofact totalDepth mode automateAll = do let rawCreate (fid, (fkGroup, initialActors)) = do -- Validation of content guarantess the existence of such faction kind. gkindId <- fromJust <$> opick cofact fkGroup (const True) let gkind@FactionKind{..} = okind cofact gkindId castInitialActors (ln, d, actorGroup) = do n <- castDice (Dice.AbsDepth $ abs ln) totalDepth d return (ln, n, actorGroup) ginitial <- mapM castInitialActors initialActors let cmap = mapFromFuns Color.legalFgCol [colorToTeamName, colorToPlainName, colorToFancyName] colorName = T.toLower $ head $ T.words fname prefix = case (fhasPointman, finitUnderAI) of (False, False) -> "Uncoordinated" (False, True) -> "Loose" (True, False) -> "Autonomous" (True, True) -> "Controlled" gnameNew = prefix <+> if fhasGender then makePhrase [MU.Ws $ MU.Text fname] else fname gcolor = M.findWithDefault Color.BrWhite colorName cmap let gname = gnameNew gdoctrine = finitDoctrine gunderAI = finitUnderAI || mattract mode || automateAll gdipl = EM.empty -- fixed below gquit = Nothing _gleader = Nothing gvictims = EM.empty gstash = Nothing return (fid, Faction{..}) lFs <- mapM rawCreate $ zip [toEnum 1 ..] $ mroster mode let mkDipl diplMode = let f (ix1, ix2) = let adj1 fact = fact {gdipl = EM.insert ix2 diplMode (gdipl fact)} in EM.adjust adj1 ix1 in foldr f -- Only symmetry is ensured, everything else is permitted, -- e.g., a faction in alliance with two others that are at war. pairsFromFaction :: (FactionKind -> [TeamContinuity]) -> (FactionId, Faction) -> [(FactionId, FactionId)] pairsFromFaction selector (fid, fact) = let teams = selector $ gkind fact hasTeam team (_, fact2) = team == fteam (gkind fact2) pairsFromTeam team = case find (hasTeam team) lFs of Just (fid2, _) -> [(fid, fid2), (fid2, fid)] Nothing -> [] in concatMap pairsFromTeam teams rawFs = EM.fromList lFs -- War overrides alliance, so 'warFs' second. Consequently, if a faction -- is allied with a faction that is at war with them, they will be -- symmetrically at war. allianceFs = mkDipl Alliance rawFs $ concatMap (pairsFromFaction falliedTeams) $ EM.assocs rawFs warFs = mkDipl War allianceFs $ concatMap (pairsFromFaction fenemyTeams) $ EM.assocs allianceFs return $! warFs gameReset :: MonadServer m => ServerOptions -> Maybe (GroupName ModeKind) -> Maybe SM.SMGen -> m State gameReset serverOptions mGameMode mrandom = do -- Dungeon seed generation has to come first, to ensure item boosting -- is determined by the dungeon RNG. cops@COps{cofact, comode} <- getsState scops dungeonSeed <- getSetGen $ sdungeonRng serverOptions `mplus` mrandom srandom <- getSetGen $ smainRng serverOptions `mplus` mrandom let srngs = RNGs (Just dungeonSeed) (Just srandom) when (sdumpInitRngs serverOptions) $ dumpRngs srngs scoreTable <- restoreScore cops teamGearOld <- getsServer steamGear flavourOld <- getsServer sflavour discoKindRevOld <- getsServer sdiscoKindRev clientStatesOld <- getsServer sclientStates let gameMode = fromMaybe INSERT_COIN $ mGameMode `mplus` sgameMode serverOptions rnd :: Rnd (FactionDict, FlavourMap, DiscoveryKind, DiscoveryKindRev, DungeonGen.FreshDungeon, ContentId ModeKind) rnd = do modeKindId <- fromMaybe (error $ "Unknown game mode:" `showFailure` gameMode) <$> opick comode gameMode (const True) let mode = okind comode modeKindId flavour <- dungeonFlavourMap cops flavourOld (discoKind, sdiscoKindRev) <- serverDiscos cops discoKindRevOld freshDng <- DungeonGen.dungeonGen cops serverOptions $ mcaves mode factionD <- resetFactions cofact (DungeonGen.freshTotalDepth freshDng) mode (sautomateAll serverOptions) return ( factionD, flavour, discoKind , sdiscoKindRev, freshDng, modeKindId ) let ( factionD, sflavour, discoKind ,sdiscoKindRev, DungeonGen.FreshDungeon{..}, modeKindId ) = St.evalState rnd dungeonSeed defState = defStateGlobal freshDungeon freshTotalDepth factionD cops scoreTable modeKindId discoKind defSer = emptyStateServer { srandom , srngs } putServer defSer modifyServer $ \ser -> ser { steamGear = teamGearOld , steamGearCur = teamGearOld , sclientStates = clientStatesOld -- reset later , sdiscoKindRev , sflavour } return $! defState -- Spawn initial actors. Clients should notice this, to set their leaders. populateDungeon :: forall m. MonadServerAtomic m => m () populateDungeon = do cops@COps{coTileSpeedup} <- getsState scops factionD <- getsState sfactionD curChalSer <- getsServer $ scurChalSer . soptions let nGt0 (_, n, _) = n > 0 ginitialWolf fact1 = if cwolf curChalSer && fhasUI (gkind fact1) then case filter nGt0 $ ginitial fact1 of [] -> [] (ln, _, grp) : _ -> [(ln, 1, grp)] else ginitial fact1 -- Keep the same order of factions as in roster. needInitialCrew = sortBy (comparing fst) $ filter (not . null . ginitialWolf . snd) $ EM.assocs factionD getEntryLevels (_, fact) = map (\(ln, _, _) -> toEnum ln) $ ginitialWolf fact arenas = ES.elems $ ES.fromList $ concatMap getEntryLevels needInitialCrew hasActorsOnArena lid (_, fact) = any (\(ln, _, _) -> toEnum ln == lid) $ ginitialWolf fact initialActorPositions :: LevelId -> m (LevelId, EM.EnumMap FactionId Point) initialActorPositions lid = do lvl <- getLevel lid let arenaFactions = map fst $ filter (hasActorsOnArena lid) needInitialCrew entryPoss <- rndToAction $ findEntryPoss cops lvl (length arenaFactions) when (length entryPoss < length arenaFactions) $ debugPossiblyPrint "Server: populateDungeon: failed to find enough distinct faction starting positions; some factions share positions" let usedPoss = EM.fromList $ zip arenaFactions $ cycle entryPoss return (lid, usedPoss) factionPositions <- EM.fromDistinctAscList <$> mapM initialActorPositions arenas let initialActors :: (FactionId, Faction) -> m () initialActors (fid3, fact3) = mapM_ (placeActors fid3) $ ginitialWolf fact3 placeActors :: FactionId -> (Int, Int, GroupName ItemKind) -> m () placeActors fid3 (ln, n, actorGroup) = do let lid = toEnum ln lvl <- getLevel lid let ppos = factionPositions EM.! lid EM.! fid3 validTile t = not $ Tile.isNoActor coTileSpeedup t -- This takes into account already spawned actors of this -- and other factions. If not enough space, some are skipped. psFree = nearbyFreePoints cops lvl validTile ppos ps = take n psFree when (length ps < n) $ debugPossiblyPrint "Server: populateDungeon: failed to find enough initial actor positions; some actors are not generated" localTime <- getsState $ getLocalTime lid forM_ ps $ \p -> do rndDelay <- rndToAction $ randomR (1, clipsInTurn - 1) let delta = timeDeltaScale (Delta timeClip) rndDelay rndTime = timeShift localTime delta maid <- addActorFromGroup actorGroup fid3 p lid rndTime case maid of Nothing -> error $ "can't spawn initial actors" `showFailure` (lid, fid3) Just aid -> do mleader <- getsState $ gleader . (EM.! fid3) . sfactionD -- Sleeping actor may become a leader, but it's quickly corrected. when (isNothing mleader) $ setFreshLeader fid3 aid placeItemsInDungeon factionPositions embedItemsInDungeon mapM_ initialActors needInitialCrew -- | Find starting postions for all factions. Try to make them distant -- from each other. Place as many of the factions, as possible, -- over stairs. Place the first faction(s) over escape(s) -- (we assume they are guardians of the escapes). -- This implies the inital factions (if any) start far from escapes. findEntryPoss :: COps -> Level -> Int -> Rnd [Point] findEntryPoss COps{cocave, coTileSpeedup} lvl@Level{lkind, larea, lstair, lescape} kRaw = do let lskip = CK.cskip $ okind cocave lkind k = kRaw + length lskip -- if @lskip@ is bogus, will be too large; OK (_, xspan, yspan) = spanArea larea factionDist = max xspan yspan - 10 dist !poss !cmin !l _ = all (\ !pos -> chessDist l pos > cmin) poss tryFind _ 0 = return [] tryFind !ps !n = do let ds = [ dist ps factionDist , dist ps $ factionDist `div` 2 , dist ps $ factionDist `div` 3 , dist ps $ max 5 $ factionDist `div` 5 , dist ps $ max 2 $ factionDist `div` 10 ] mp <- findPosTry2 500 lvl -- try really hard, for skirmish fairness (\_ !t -> Tile.isWalkable coTileSpeedup t && not (Tile.isNoActor coTileSpeedup t)) (take 2 ds) -- don't pick too close @isOftenActor@ locations (\_ !t -> Tile.isOftenActor coTileSpeedup t) ds case mp of Just np -> do nps <- tryFind (np : ps) (n - 1) return $! np : nps Nothing -> return [] sameStaircase :: [Point] -> Point -> Bool sameStaircase upStairs Point{..} = any (\(Point ux uy) -> uy == py && ux + 2 == px) upStairs upAndSomeDownStairs = fst lstair ++ filter (not . sameStaircase (fst lstair)) (snd lstair) skipIndexes ixs l = map snd $ filter (\(ix, _) -> ix `notElem` ixs) $ zip [0..] l let !_A = assert (k > 0 && factionDist > 0) () onEscapes = take k lescape onStairs = take (k - length onEscapes) upAndSomeDownStairs nk = k - length onEscapes - length onStairs -- Starting in the middle is too easy. found <- tryFind (middlePoint larea : onEscapes ++ onStairs) nk return $! skipIndexes lskip $ onEscapes ++ onStairs ++ found -- | Apply options that don't need a new game. applyDebug :: MonadServer m => m () applyDebug = do ServerOptions{..} <- getsServer soptionsNxt modifyServer $ \ser -> ser {soptions = (soptions ser) { sniff , sallClear , sdbgMsgSer , snewGameSer , sassertExplored , sdumpInitRngs , sclientOptions }} LambdaHack-0.11.0.0/engine-src/Game/LambdaHack/Server/State.hs0000644000000000000000000002063407346545000021630 0ustar0000000000000000-- | Server and client game state types and operations. module Game.LambdaHack.Server.State ( StateServer(..), ActorTime, ActorPushedBy , emptyStateServer, updateActorTime, lookupActorTime, ageActor #ifdef EXPOSE_INTERNAL , GearOfTeams #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Data.Binary import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.HashMap.Strict as HM import qualified Data.IntMap.Strict as IM import qualified System.Random.SplitMix32 as SM import Game.LambdaHack.Common.Analytics import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.FactionKind (TeamContinuity) import Game.LambdaHack.Content.ItemKind (ItemKind) import Game.LambdaHack.Definition.Defs import Game.LambdaHack.Server.Fov import Game.LambdaHack.Server.ItemRev import Game.LambdaHack.Server.ServerOptions -- | State with server-specific data, including a copy of each client's -- basic game state, but not the server's basic state. data StateServer = StateServer { sactorTime :: ActorTime -- ^ absolute times of actors next actions , strajTime :: ActorTime -- ^ and same for actors with trajectories , strajPushedBy :: ActorPushedBy -- ^ culprits for actors with trajectories , steamGear :: GearOfTeams -- ^ metagame persistent personal -- characteristics and favourite gear -- of each numbered continued team member , steamGearCur :: GearOfTeams -- ^ gear preferences to be taken into -- account in the current game , stcounter :: EM.EnumMap TeamContinuity Int -- ^ stores next continued team character -- identity index number in this game , sfactionAn :: FactionAnalytics -- ^ various past events data for factions , sactorAn :: ActorAnalytics -- ^ various past events data for actors , sgenerationAn :: GenerationAnalytics -- ^ item creation statistics, by item lore , sactorStasis :: ES.EnumSet ActorId -- ^ actors currently in time stasis, -- invulnerable to time warps until move , sdiscoKindRev :: DiscoveryKindRev -- ^ reverse map, used for item creation , suniqueSet :: UniqueSet -- ^ already generated unique items , sitemRev :: ItemRev -- ^ reverse id map, used for item creation , sflavour :: FlavourMap -- ^ association of flavour to item kinds , sacounter :: ActorId -- ^ stores next actor index , sicounter :: ItemId -- ^ stores next item index , snumSpawned :: EM.EnumMap LevelId Int -- ^ how many spawned so far on the level , sbandSpawned :: IM.IntMap Int -- ^ how many times such group spawned , sundo :: () -- [CmdAtomic] -- ^ atomic commands performed to date , sclientStates :: EM.EnumMap FactionId State -- ^ each faction state, as seen by clients , smetaBackup :: EM.EnumMap TeamContinuity DiscoveryKind -- ^ discovery info for absent factions , sperFid :: PerFid -- ^ perception of all factions , sperValidFid :: PerValidFid -- ^ perception validity for all factions , sperCacheFid :: PerCacheFid -- ^ perception cache of all factions , sfovLucidLid :: FovLucidLid -- ^ ambient or shining light positions , sfovClearLid :: FovClearLid -- ^ clear tiles positions , sfovLitLid :: FovLitLid -- ^ ambient light positions , sarenas :: ES.EnumSet LevelId -- ^ the set of active arenas , svalidArenas :: Bool -- ^ whether active arenas valid , srandom :: SM.SMGen -- ^ current random generator , srngs :: RNGs -- ^ initial random generators , sbreakLoop :: Bool -- ^ exit game loop after clip's end; -- usually no game save follows , sbreakASAP :: Bool -- ^ exit game loop ASAP; usually with save , swriteSave :: Bool -- ^ write savegame to file after loop exit , soptions :: ServerOptions -- ^ current commandline options , soptionsNxt :: ServerOptions -- ^ options for the next game } deriving Show -- | Position in time for each actor, grouped by level and by faction. type ActorTime = EM.EnumMap FactionId (EM.EnumMap LevelId (EM.EnumMap ActorId Time)) -- | Record who last propelled a given actor with trajectory. type ActorPushedBy = EM.EnumMap ActorId ActorId -- | Per-team, per-actor metagame persistent favourite organs and gear. type GearOfTeams = EM.EnumMap TeamContinuity (IM.IntMap [(GroupName ItemKind, ContentId ItemKind)]) -- | Initial, empty game server state. emptyStateServer :: StateServer emptyStateServer = StateServer { sactorTime = EM.empty , strajTime = EM.empty , strajPushedBy = EM.empty , steamGear = EM.empty , steamGearCur = EM.empty , stcounter = EM.empty , sfactionAn = EM.empty , sactorAn = EM.empty , sgenerationAn = EM.fromDistinctAscList $ zip [minBound..maxBound] (repeat EM.empty) , sactorStasis = ES.empty , sdiscoKindRev = emptyDiscoveryKindRev , suniqueSet = ES.empty , sitemRev = HM.empty , sflavour = emptyFlavourMap , sacounter = toEnum 0 , sicounter = toEnum 0 , snumSpawned = EM.empty , sbandSpawned = IM.fromList [(1, 0), (2, 0), (3, 0)] , sundo = () , sclientStates = EM.empty , smetaBackup = EM.empty , sperFid = EM.empty , sperValidFid = EM.empty , sperCacheFid = EM.empty , sfovLucidLid = EM.empty , sfovClearLid = EM.empty , sfovLitLid = EM.empty , sarenas = ES.empty , svalidArenas = False , srandom = SM.mkSMGen 42 , srngs = RNGs { dungeonRandomGenerator = Nothing , startingRandomGenerator = Nothing } , sbreakLoop = False , sbreakASAP = False , swriteSave = False , soptions = defServerOptions , soptionsNxt = defServerOptions } updateActorTime :: FactionId -> LevelId -> ActorId -> Time -> ActorTime -> ActorTime updateActorTime !fid !lid !aid !time = EM.adjust (EM.adjust (EM.insert aid time) lid) fid lookupActorTime :: FactionId -> LevelId -> ActorId -> ActorTime -> Maybe Time lookupActorTime !fid !lid !aid !atime = do m1 <- EM.lookup fid atime m2 <- EM.lookup lid m1 EM.lookup aid m2 ageActor :: FactionId -> LevelId -> ActorId -> Delta Time -> ActorTime -> ActorTime ageActor !fid !lid !aid !delta = EM.adjust (EM.adjust (EM.adjust (`timeShift` delta) aid) lid) fid instance Binary StateServer where put StateServer{..} = do put sactorTime put strajTime put strajPushedBy put steamGear put steamGearCur put stcounter put sfactionAn put sactorAn put sgenerationAn put sactorStasis put sdiscoKindRev put suniqueSet put sitemRev put sflavour put sacounter put sicounter put snumSpawned put sbandSpawned put sclientStates put smetaBackup put (show srandom) put srngs put soptions get = do sactorTime <- get strajTime <- get strajPushedBy <- get steamGear <- get steamGearCur <- get stcounter <- get sfactionAn <- get sactorAn <- get sgenerationAn <- get sactorStasis <- get sdiscoKindRev <- get suniqueSet <- get sitemRev <- get sflavour <- get sacounter <- get sicounter <- get snumSpawned <- get sbandSpawned <- get sclientStates <- get smetaBackup <- get g <- get srngs <- get soptions <- get let srandom = read g sundo = () sperFid = EM.empty sperValidFid = EM.empty sperCacheFid = EM.empty sfovLucidLid = EM.empty sfovClearLid = EM.empty sfovLitLid = EM.empty sarenas = ES.empty svalidArenas = False sbreakLoop = False sbreakASAP = False swriteSave = False soptionsNxt = defServerOptions return $! StateServer{..} LambdaHack-0.11.0.0/test/0000755000000000000000000000000007346545000013046 5ustar0000000000000000LambdaHack-0.11.0.0/test/ActorStateUnitTests.hs0000644000000000000000000000147207346545000017342 0ustar0000000000000000module ActorStateUnitTests (actorStateUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import Test.Tasty import Test.Tasty.HUnit import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Definition.Ability as Ability import UnitTestHelpers actorStateUnitTests :: TestTree actorStateUnitTests = testGroup "actorStateUnitTests" [ testCase "getActorBody verify stubCliState has testActor" $ getActorBody testActorId (cliState stubCliState) @?= testActor , testCase "getActorMaxSkills verify stubCliState has zeroSkills" $ getActorMaxSkills testActorId (cliState stubCliState) @?= Ability.zeroSkills , testCase "fidActorNotProjGlobalAssocs" $ fidActorNotProjGlobalAssocs testFactionId (cliState testCliStateWithItem) @?= [(testActorId, testActorWithItem)] ] LambdaHack-0.11.0.0/test/CommonMUnitTests.hs0000644000000000000000000000320107346545000016626 0ustar0000000000000000module CommonMUnitTests (commonMUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM 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 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 } commonMUnitTests :: TestTree commonMUnitTests = testGroup "commonMUnitTests" [ testCase "getPerFid stubCliState returns emptyPerception" $ do result <- executorCli (getPerFid testLevelId) stubCliState fst result @?= emptyPer , testCase "makeLine, when actor stands at the target position, fails" $ Nothing @?= makeLine False testActor (Point 0 0) 1 emptyCOps testLevel , testCase "makeLine unknownTiles succeeds" $ Just 1 @?= makeLine False testActor (Point 2 0) 1 emptyCOps testLevel ] LambdaHack-0.11.0.0/test/HandleHelperMUnitTests.hs0000644000000000000000000000111707346545000017735 0ustar0000000000000000module HandleHelperMUnitTests (handleHelperMUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import Test.Tasty import Test.Tasty.HUnit import Game.LambdaHack.Client.UI.HandleHelperM import UnitTestHelpers handleHelperMUnitTests :: TestTree handleHelperMUnitTests = testGroup "handleHelperMUnitTests" [ testCase "partyAfterLeader" $ do -- You've got to fight for your right to party! let testFunc = partyAfterLeader testActorId partyInMonad <- executorCli testFunc testCliStateWithItem let party = fst partyInMonad party @?= [] ] LambdaHack-0.11.0.0/test/HandleHumanLocalMUnitTests.hs0000644000000000000000000000762607346545000020554 0ustar0000000000000000module HandleHumanLocalMUnitTests (handleHumanLocalMUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM import Test.Tasty import Test.Tasty.HUnit import Game.LambdaHack.Client.UI.HandleHelperM import Game.LambdaHack.Client.UI.HandleHumanLocalM import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import Game.LambdaHack.Common.ActorState import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.ItemAspect import Game.LambdaHack.Common.Level import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.ReqFailure import Game.LambdaHack.Common.State 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 } handleHumanLocalMUnitTests :: TestTree handleHumanLocalMUnitTests = testGroup "handleHumanLocalMUnitTests" [ testCase "verify stubLevel has tile element" $ case EM.lookup testLevelId (sdungeon stubState) of Nothing -> assertFailure "stubLevel lost in dungeon" Just level -> ltile level ! Point 0 0 @?= unknownId , testCase "verify stubCliState has actor" $ getActorBody testActorId (cliState stubCliState) @?= testActor , testCase "permittedProjectClient stubCliState returns ProjectUnskilled" $ do let testFn = permittedProjectClient testActorId permittedProjectClientResultFnInMonad <- executorCli testFn stubCliState let ultimateResult = fst permittedProjectClientResultFnInMonad testItemFull ultimateResult @?= Left ProjectUnskilled , testCase "chooseItemProjectHuman" $ do let testFn = let triggerItems = [ HumanCmd.TriggerItem {tiverb = "verb", tiobject = "object", tisymbols = [toContentSymbol 'a', toContentSymbol 'b']} , HumanCmd.TriggerItem {tiverb = "verb2", tiobject = "object2", tisymbols = [toContentSymbol 'c']} ] in chooseItemProjectHuman testActorId triggerItems result <- executorCli testFn testCliStateWithItem showFailError (fromJust (fst result)) @?= "*aiming obstructed by terrain*" , testCase "psuitReq" $ do let testFn = psuitReq testActorId mpsuitReqMonad <- executorCli testFn testCliStateWithItem let mpsuitReq = fst mpsuitReqMonad case mpsuitReq of Left err -> do err @?= "aiming obstructed by terrain" -- TODO: I'd split the test into three tests, each taking a different branch and fail in the remaining two branches that the particular branch doesn't take. Here it takes the first branch, because unknown tiles are not walkable (regardless what I claimed previously) and so the player is surrounded by walls, basically, so aiming fails, because the projectiles wouldn't even leave the position of the actor. I think. Right psuitReqFun -> case psuitReqFun testItemFull of Left reqFail -> do reqFail @?= ProjectUnskilled Right (pos, _) -> do pos @?= Point 0 0 , testCase "xhairLegalEps" $ do let testFn = xhairLegalEps testActorId result <- executorCli testFn testCliStateWithItem fst result @?= Right 114 -- not a coincidence this matches testFactionId, -- because @eps@ is initialized that way, -- for "randomness" ] LambdaHack-0.11.0.0/test/InventoryMUnitTests.hs0000644000000000000000000000351307346545000017401 0ustar0000000000000000-- TODO: at some point we'll want our unit test hierarchy to match the -- main codebase file hierarchy module InventoryMUnitTests (inventoryMUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import Test.Tasty import Test.Tasty.HUnit import Game.LambdaHack.Client.UI.InventoryM import Game.LambdaHack.Definition.Defs import UnitTestHelpers inventoryMUnitTests :: TestTree inventoryMUnitTests = testGroup "inventoryMUnitTests" [ testCase "getFull no stores " $ do let testFn = getFull testActorId (return SuitsEverything) -- :: m Suitability (\_ _ _ _ _ -> "specific prompt") (\_ _ _ _ _ -> "generic prompt") [] -- :: [CStore] False False result <- executorCli testFn stubCliState fst result @?= Left "no items" , testCase "getFull no item in eqp store" $ do let testFn = getFull testActorId (return SuitsEverything) (\_ _ _ _ _ -> "specific prompt") (\_ _ _ _ _ -> "generic prompt") [CEqp] False False result <- executorCli testFn stubCliState fst result @?= Left "no items in equipment outfit" , testCase "getFull an item in eqp store" $ do let testFn = getFull testActorId (return SuitsEverything) (\_ _ _ _ _ -> "specific prompt") (\_ _ _ _ _ -> "generic prompt") [CEqp] False False result <- executorCli testFn testCliStateWithItem fst result @?= Right (CEqp, [(testItemId, (1, []))]) ] LambdaHack-0.11.0.0/test/ItemDescriptionUnitTests.hs0000644000000000000000000000457307346545000020400 0ustar0000000000000000module ItemDescriptionUnitTests (itemDescriptionUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM 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 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 , itemKindId = toEnum 667 , itemKind = testItemKind , itemDisco = ItemDiscoFull emptyAspectRecord , itemSuspect = True } in [ testCase "testItem_viewItem_Blackx" $ viewItem testItemFull @?= attrChar2ToW32 Green 'x' , testCase "testItem_viewItem_Black!" $ viewItem testItemFull {itemKind = testItemKind {isymbol = '!'}} @?= attrChar2ToW32 Green '!' , testCase "testItem_viewItemBenefitColored_isEquip_Greenx" $ viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit True 0 0 0 0)) (toEnum 42) testItemFull @?= attrChar2ToW32 BrGreen 'x' , testCase "testItem_viewItemBenefitColored_isNotEquip_Redx" $ viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit False 0 0 0 0)) (toEnum 42) testItemFull @?= attrChar2ToW32 BrRed 'x' ] LambdaHack-0.11.0.0/test/ItemKindUnitTests.hs0000644000000000000000000000627607346545000017004 0ustar0000000000000000module ItemKindUnitTests (itemKindUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import Test.Tasty import Test.Tasty.HUnit 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" } @?= ["iname longer than 23"] , testCase "shortEnoughiname_validateSingle_noErr" $ validateSingle standardSymbols testItemKind @?= [] , testCase "equipableNoSlotxSymbol_validateSingle_errs" $ validateSingle standardSymbols testItemKind { 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 = ',' , iaspects = [ SetFlag Ability.Equipable ] } @?= [] , testCase "equipableNoSlot\"Symbol_validateSingle_noErr" $ validateSingle standardSymbols testItemKind { isymbol = '"' , iaspects = [ SetFlag Ability.Equipable ] } @?= [] , testCase "equipableNoSlot/Symbol_validateSingle_noErr" $ validateSingle standardSymbols testItemKind { isymbol = '/' , iaspects = [ SetFlag Ability.Equipable ] } @?= [] , testCase "equipableNoSlot*CustomRules_validateSingle_noErr" $ validateSingle customSymbols testItemKind { isymbol = '*' , iaspects = [ SetFlag Ability.Equipable ] } @?= [] , testCase "equipableNoSlot\"CustomRules_validateSingle_errs" $ validateSingle customSymbols testItemKind { isymbol = '"' , iaspects = [ SetFlag Ability.Equipable ] } @?= ["EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template"] ] LambdaHack-0.11.0.0/test/ItemRevUnitTests.hs0000644000000000000000000000770207346545000016646 0ustar0000000000000000module ItemRevUnitTests (itemRevUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.Trans.State.Strict as St import qualified Data.EnumMap.Strict as EM import qualified Data.EnumSet as ES import qualified Data.Vector.Unboxed as U import qualified System.Random.SplitMix32 as SM 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 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 { iflavour = zipStory [Black,Green] } emptyIdToFlavourSymbolToFlavourSetPair = ( EM.empty, EM.empty ) singletonIdToFlavourSymbolToFlavourSetPair = ( EM.singleton (toContentId 0) dummyFlavour , EM.singleton 'x' (ES.singleton dummyFlavour) ) flavourBlack = head $ zipStory [Black] flavourGreen = head $ zipStory [Green] 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 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 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 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 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 (mapPair1, _) = St.runState (rollFlavourMap (U.singleton invalidInformationCode) rndMapPair0 (toContentId 0) testItemKind2Flavours) $ SM.mkSMGen 1 in fst mapPair1 @?= EM.singleton (toContentId 0) flavourBlack , testCase "rollFlavourMap on two flavours -> second flavour can be rolled" $ -- relies on us not messing with RNG let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair (mapPair1, _) = St.runState (rollFlavourMap (U.singleton invalidInformationCode) rndMapPair0 (toContentId 0) testItemKind2Flavours) $ SM.mkSMGen 2 in fst mapPair1 @?= EM.singleton (toContentId 0) flavourGreen ] LambdaHack-0.11.0.0/test/LevelUnitTests.hs0000644000000000000000000000354307346545000016341 0ustar0000000000000000module LevelUnitTests (levelUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.EnumMap.Strict as EM 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 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 } testDungeonWithLevel :: State testDungeonWithLevel = let singletonDungeonUpdate _ = EM.singleton testLevelId testLevel unknownTileState = localFromGlobal emptyState oneLevelDungeonState = updateDungeon singletonDungeonUpdate unknownTileState in oneLevelDungeonState levelUnitTests :: TestTree levelUnitTests = testGroup "levelUnitTests" [ testCase "testDungeonWithLevel has min level id" $ do let ((minKey, _), _) = fromJust $ EM.minViewWithKey (sdungeon testDungeonWithLevel) minKey @?= testLevelId , testCase "testDungeonWithLevel has max level id" $ do let ((minKey, _), _) = fromJust $ EM.maxViewWithKey (sdungeon testDungeonWithLevel) minKey @?= testLevelId , testCase "dungeonBounds testDungeonWithLevel returns (0,0)" $ do let bounds = dungeonBounds (sdungeon testDungeonWithLevel) bounds @?= (testLevelId, testLevelId) ] LambdaHack-0.11.0.0/test/MonadClientUIUnitTests.hs0000644000000000000000000000203407346545000017717 0ustar0000000000000000module MonadClientUIUnitTests (monadClientUIUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import Test.Tasty import Test.Tasty.HUnit import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI.MonadClientUI import Game.LambdaHack.Client.UI.Overlay import UnitTestHelpers monadClientUIUnitTests :: TestTree monadClientUIUnitTests = testGroup "handleHumanLocalMUnitTests" [ testCase "getsClient sside" $ do sideInMonad <- executorCli (getsClient sside) stubCliState fst sideInMonad @?= testFactionId , testCase "getArenaUI works in stub" $ do levelIdInMonad <- executorCli getArenaUI stubCliState fst levelIdInMonad @?= testLevelId , testCase "viewedLevelUI works in stub" $ do levelIdInMonad <- executorCli viewedLevelUI stubCliState fst levelIdInMonad @?= testLevelId , testCase "getFontSetup works in stub" $ do fontSetupInMonad <- executorCli getFontSetup stubCliState fst fontSetupInMonad @?= multiFontSetup ] LambdaHack-0.11.0.0/test/ReqFailureUnitTests.hs0000644000000000000000000000573607346545000017337 0ustar0000000000000000module ReqFailureUnitTests (reqFailureUnitTests) where import Prelude () import Game.LambdaHack.Core.Prelude import Test.Tasty import Test.Tasty.HUnit import Game.LambdaHack.Common.Item import Game.LambdaHack.Common.ItemAspect 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 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 } , itemKindId = toEnum 667 , itemKind = testItemKind , itemDisco = ItemDiscoFull emptyAspectRecord , itemSuspect = True } standardRules = Content.RuleKind.standardRules in [ testCase "permittedApply: One Skill and x symbol -> FailureApplyFood" $ permittedApply standardRules timeZero 1 True Nothing testItemFull quantSingle @?= Left ApplyFood , testCase "permittedApply: One Skill and , symbol And CGround -> True" $ permittedApply standardRules timeZero 1 True (Just CGround) testItemFull {itemKind = testItemKind{isymbol = ','}} quantSingle @?= Right True , testCase "permittedApply: One Skill and \" symbol -> True" $ permittedApply standardRules timeZero 1 True Nothing testItemFull {itemKind = testItemKind{isymbol = '"'}} quantSingle @?= Right True , testCase "permittedApply: Two Skill and ? symbol -> FailureApplyRead" $ permittedApply standardRules timeZero 2 True Nothing testItemFull {itemKind = testItemKind{isymbol = '?'}} quantSingle @?= Left ApplyRead , testCase "permittedApply: Two Skill and , symbol -> True" $ permittedApply standardRules timeZero 2 True Nothing testItemFull {itemKind = testItemKind{isymbol = ','}} quantSingle @?= Right True ] LambdaHack-0.11.0.0/test/SessionUIMock.hs0000644000000000000000000001306307346545000016100 0ustar0000000000000000module SessionUIMock ( unwindMacros ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Monad.Trans.Class import Control.Monad.Trans.State.Lazy import Control.Monad.Trans.Writer.Lazy import Data.Bifunctor (bimap) import qualified Data.Map.Strict as M import qualified Game.LambdaHack.Client.UI.Content.Input as IC import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.FrameM import Game.LambdaHack.Client.UI.HandleHumanLocalM import Game.LambdaHack.Client.UI.HandleHumanM import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.SessionUI (KeyMacro (..), KeyMacroFrame (..), emptyMacroFrame) data SessionUIMock = SessionUIMock { smacroFrame :: KeyMacroFrame , smacroStack :: [KeyMacroFrame] , sccui :: CCUI , unwindTicks :: Int } type KeyMacroBufferMock = Either String String type KeyPendingMock = String type KeyLastMock = String type BufferTrace = [(KeyMacroBufferMock, KeyPendingMock, KeyLastMock)] type ActionLog = String data Op = Looped | HeadEmpty humanCommandMock :: WriterT [(BufferTrace, ActionLog)] (State SessionUIMock) () humanCommandMock = do abuffs <- lift $ do sess <- get return $ renderTrace (smacroFrame sess : smacroStack sess) -- log session abortOrCmd <- lift iterationMock -- do stuff -- GC macro stack if there are no actions left to handle, -- removing all unnecessary macro frames at once, -- but leaving the last one for user's in-game macros. lift $ modify $ \sess -> let (smacroFrameNew, smacroStackMew) = dropEmptyMacroFrames (smacroFrame sess) (smacroStack sess) in sess { smacroFrame = smacroFrameNew , smacroStack = smacroStackMew } case abortOrCmd of Left Looped -> tell [(abuffs, "Macro looped")] >> pure () Left HeadEmpty -> tell [(abuffs, "")] >> pure () -- exit loop Right Nothing -> tell [(abuffs, "")] >> humanCommandMock Right (Just out) -> tell [(abuffs, show out)] >> humanCommandMock iterationMock :: State SessionUIMock (Either Op (Maybe K.KM)) iterationMock = do SessionUIMock _ _ CCUI{coinput=IC.InputContent{bcmdMap}} ticks <- get if ticks <= 1000 then do modify $ \sess -> sess {unwindTicks = ticks + 1} mkm <- promptGetKeyMock case mkm of Nothing -> return $ Left HeadEmpty -- macro finished Just km -> case km `M.lookup` bcmdMap of Just (_, _, cmd) -> Right <$> cmdSemInCxtOfKMMock km cmd _ -> return $ Right $ Just km -- unknown command; fine for tests else return $ Left Looped cmdSemInCxtOfKMMock :: K.KM -> HumanCmd.HumanCmd -> State SessionUIMock (Maybe K.KM) cmdSemInCxtOfKMMock km cmd = do modify $ \sess -> sess {smacroFrame = updateKeyLast km cmd $ smacroFrame sess} cmdSemanticsMock km cmd cmdSemanticsMock :: K.KM -> HumanCmd.HumanCmd -> State SessionUIMock (Maybe K.KM) cmdSemanticsMock km = \case HumanCmd.Macro s -> do modify $ \sess -> let kms = K.mkKM <$> s (smacroFrameNew, smacroStackMew) = macroHumanTransition kms (smacroFrame sess) (smacroStack sess) in sess { smacroFrame = smacroFrameNew , smacroStack = smacroStackMew } return Nothing HumanCmd.Repeat n -> do modify $ \sess -> let (smacroFrameNew, smacroStackMew) = repeatHumanTransition n (smacroFrame sess) (smacroStack sess) in sess { smacroFrame = smacroFrameNew , smacroStack = smacroStackMew } return Nothing HumanCmd.RepeatLast n -> do modify $ \sess -> sess {smacroFrame = repeatLastHumanTransition n (smacroFrame sess) } return Nothing HumanCmd.Record -> do modify $ \sess -> sess {smacroFrame = fst $ recordHumanTransition (smacroFrame sess) } return Nothing _ -> return $ Just km promptGetKeyMock :: State SessionUIMock (Maybe K.KM) promptGetKeyMock = do SessionUIMock macroFrame _ CCUI{coinput=IC.InputContent{bcmdMap}} _ <- get case keyPending macroFrame of KeyMacro (km : kms) -> do modify $ \sess -> sess {smacroFrame = (smacroFrame sess) {keyPending = KeyMacro kms}} modify $ \sess -> sess {smacroFrame = addToMacro bcmdMap km $ smacroFrame sess} return (Just km) KeyMacro [] -> return Nothing unwindMacrosFull :: IC.InputContent -> KeyMacro -> [(BufferTrace, ActionLog)] unwindMacrosFull coinput keyPending = let initSession = SessionUIMock { smacroFrame = emptyMacroFrame {keyPending} , smacroStack = [] , sccui = emptyCCUI {coinput} , unwindTicks = 0 } in evalState (execWriterT humanCommandMock) initSession accumulateActions :: [(BufferTrace, ActionLog)] -> [(BufferTrace, ActionLog)] accumulateActions ba = let (buffers, actions) = unzip ba actionlog = concat <$> inits actions in if snd (last ba) == "Macro looped" then ba else zip buffers actionlog unwindMacros :: IC.InputContent -> KeyMacro -> [(BufferTrace, ActionLog)] unwindMacros coinput keyPending = accumulateActions $ unwindMacrosFull coinput keyPending renderTrace :: [KeyMacroFrame] -> BufferTrace renderTrace macroFrames = let buffers = bimap (concatMap K.showKM) (concatMap K.showKM . unKeyMacro) . keyMacroBuffer <$> macroFrames pendingKeys = concatMap K.showKM . unKeyMacro . keyPending <$> macroFrames lastKeys = maybe "" K.showKM . keyLast <$> macroFrames in zip3 buffers pendingKeys lastKeys LambdaHack-0.11.0.0/test/SessionUIUnitTests.hs0000644000000000000000000003574507346545000017164 0ustar0000000000000000module SessionUIUnitTests (macroTests) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Map.Strict as M import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck import qualified Game.LambdaHack.Client.UI.Content.Input as IC import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd import qualified Game.LambdaHack.Client.UI.Key as K import Game.LambdaHack.Client.UI.SessionUI import qualified Client.UI.Content.Input as Content.Input import SessionUIMock -- Run @test -p "In-game" --quickcheck-verbose@ to verify that quickcheck -- properties are not too often satisfied voidly. macroTests :: TestTree macroTests = testGroup "macroTests" $ let coinput = IC.makeData Nothing Content.Input.standardKeysAndMouse stringToKeyMacro = KeyMacro . map (K.mkKM . (: [])) listToKeyMacro = KeyMacro . map K.mkKM bindInput l input = let ltriple = M.fromList $ map (\(k, ks) -> (K.mkKM k, ([], "", HumanCmd.Macro $ map (: []) ks))) l in input {IC.bcmdMap = M.union ltriple $ IC.bcmdMap input} in [ testCase "Macro 1 from PR#192 description" $ fst <$> unwindMacros coinput (stringToKeyMacro "'j''j'") @?= [ [ (Right "", "'j''j'", "") ] , [ (Left "", "j''j'", "") ] , [ (Left "j", "''j'", "j") ] , [ (Right "j", "'j'", "j") ] , [ (Left "", "j'", "j") ] , [ (Left "j", "'", "j") ] , [ (Right "j", "", "j") ] ] , testCase "Macro 1 from Issue#189 description" $ snd (last (unwindMacros (bindInput [ ("a", "'bc'V") , ("c", "'aaa'V") ] coinput) (stringToKeyMacro "a"))) @?= "Macro looped" , testCase "Macro 2 from Issue#189 description" $ snd (last (unwindMacros (bindInput [("a", "'x'")] coinput) (stringToKeyMacro "'a'"))) @?= "x" , testCase "Macro 3 from Issue#189 description" $ snd (last (unwindMacros coinput (stringToKeyMacro "'x''x'"))) @?= "xx" , testCase "Macro 4 from Issue#189 description" $ snd (last (unwindMacros coinput (stringToKeyMacro "'x''x'V"))) @?= "xxx" , testCase "Macro 5 from Issue#189 description" $ snd (last (unwindMacros coinput (stringToKeyMacro "x'x'V"))) @?= "xxx" , testCase "Macro test 10" $ snd (last (unwindMacros coinput (stringToKeyMacro "x'y'V"))) @?= "xyy" , testCase "Macro test 11" $ snd (last (unwindMacros coinput (stringToKeyMacro "'x''y'V"))) @?= "xyy" , testCase "Macro test 12" $ snd (last (unwindMacros coinput (listToKeyMacro ["x", "C-V"]))) @?= "x" , testCase "Macro test 13" $ snd (last (unwindMacros coinput (listToKeyMacro ["'", "x", "'", "C-V"]))) @?= "xxxxxxxxxxxxxxxxxxxxxxxxxx" , testCase "Macro test 14" $ snd (last (unwindMacros coinput (listToKeyMacro ["'", "x", "'", "y", "C-V"]))) @?= "xyxxxxxxxxxxxxxxxxxxxxxxxxx" , testCase "Macro test 15" $ snd (last (unwindMacros (bindInput [("a", "x")] coinput) (stringToKeyMacro "'a'V"))) @?= "xx" , testCase "Macro test 16" $ snd (last (unwindMacros (bindInput [("a", "'x'")] coinput) (stringToKeyMacro "'a'V"))) @?= "xx" , testCase "Macro test 17" $ snd (last (unwindMacros (bindInput [("a", "'x'V")] coinput) (stringToKeyMacro "a"))) @?= "xx" , testCase "Macro test 18" $ snd (last (unwindMacros (bindInput [("a", "'x'V")] coinput) (stringToKeyMacro "'a'"))) @?= "xx" , testCase "Macro test 19" $ snd (last (unwindMacros (bindInput [("a", "'x'V")] coinput) (stringToKeyMacro "'a'V"))) @?= "xxxx" , testCase "Macro test 20" $ snd (last (unwindMacros (bindInput [ ("a", "'bz'V") , ("c", "'aaa'V") ] coinput) (stringToKeyMacro "c"))) @?= "bzbzbzbzbzbzbzbzbzbzbzbz" , testCase "RepeatLast test 10" $ snd (last (unwindMacros coinput (stringToKeyMacro "x'y'v"))) @?= "xyy" , testCase "RepeatLast test 11" $ snd (last (unwindMacros coinput (stringToKeyMacro "'x'yv"))) @?= "xyy" , testCase "RepeatLast test 12" $ snd (last (unwindMacros coinput (listToKeyMacro ["v", "C-v"]))) @?= "" , testCase "RepeatLast test 13" $ snd (last (unwindMacros coinput (listToKeyMacro ["'", "x", "'", "C-v"]))) @?= "xxxxxxxxxxxxxxxxxxxxxxxxxx" , testCase "RepeatLast test 14" $ snd (last (unwindMacros coinput (listToKeyMacro ["'", "x", "'", "V", "C-v"]))) @?= "xxxxxxxxxxxxxxxxxxxxxxxxxxx" , testCase "RepeatLast test 15" $ snd (last (unwindMacros (bindInput [("a", "x")] coinput) (stringToKeyMacro "av"))) @?= "xx" , testCase "RepeatLast test 16" $ snd (last (unwindMacros (bindInput [("a", "'x'")] coinput) (stringToKeyMacro "'a'v"))) @?= "xx" , testCase "RepeatLast test 17" $ snd (last (unwindMacros (bindInput [("a", "'x'v")] coinput) (stringToKeyMacro "a"))) @?= "xx" , testCase "RepeatLast test 18" $ snd (last (unwindMacros (bindInput [("a", "'x'v")] coinput) (stringToKeyMacro "'a'"))) @?= "xx" , testCase "RepeatLast test 19" $ snd (last (unwindMacros (bindInput [("a", "'x'v")] coinput) (stringToKeyMacro "'a'v"))) @?= "xxxx" , testCase "RepeatLast test 20" $ snd (last (unwindMacros (bindInput [ ("a", "'bz'v") , ("c", "'aaa'v") ] coinput) (stringToKeyMacro "c"))) @?= "bzzbzzbzzbzz" , testCase "RepeatLast test 21" $ snd (last (unwindMacros (bindInput [("a", "'x'V")] coinput) (stringToKeyMacro "'a'v"))) @?= "xxxx" , testCase "RepeatLast test 22" $ snd (last (unwindMacros (bindInput [("a", "'xy'V")] coinput) (stringToKeyMacro "'aa'v"))) @?= "xyxyxyxyxyxy" , testCase "RepeatLast test 23" $ snd (last (unwindMacros (bindInput [("a", "'xy'v")] coinput) (stringToKeyMacro "'aa'V"))) @?= "xyyxyyxyyxyy" , testCase "RepeatLast test 24" $ snd (last (unwindMacros (bindInput [("a", "'xy'vv")] coinput) (stringToKeyMacro "'aa'vv"))) @?= "xyyyxyyyxyyyxyyy" , testCase "RepeatLast test 25" $ snd (last (unwindMacros (bindInput [("a", "'xyv'v")] coinput) (stringToKeyMacro "'a'a'vv'"))) @?= "xyyyxyyyxyyyxyyy" , testCase "RepeatLast test 26" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("b", "'za'v") , ("c", "'ab'v") ] coinput) (stringToKeyMacro "'c'v"))) @?= "xyyzxyyxyyzxyyxyyxyyzxyyxyyzxyyxyy" , testCase "RepeatLast test 27" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'V") , ("b", "'za'v") , ("c", "'ab'v") ] coinput) (stringToKeyMacro "'c'v"))) @?= "xyxyzxyxyxyxyzxyxyxyxyxyxyzxyxyxyxyzxyxyxyxy" , testCase "RepeatLast test 28" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("b", "'za'V") , ("c", "'ab'v") ] coinput) (stringToKeyMacro "'c'v"))) @?= "xyyzxyyzxyyzxyyzxyyxyyzxyyzxyyzxyyzxyy" , testCase "RepeatLast test 29" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("b", "'za'V") , ("c", "'ab'V") ] coinput) (stringToKeyMacro "'c'v"))) @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy" , testCase "RepeatLast test 30" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("b", "'za'V") , ("c", "'ab'V") ] coinput) (stringToKeyMacro "'c'V"))) @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy" , testCase "RepeatLast test 31" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("b", "'za'v") , ("c", "'ab'V") ] coinput) (stringToKeyMacro "'c'V"))) @?= "xyyzxyyxyyxyyzxyyxyyxyyzxyyxyyxyyzxyyxyy" , testCase "RepeatLast test 32" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("b", "'za'v") ] coinput) (stringToKeyMacro "'ab'vv"))) @?= "xyyzxyyxyyzxyyxyyzxyyxyy" , testCase "RepeatLast test 33" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'V") ] coinput) (stringToKeyMacro "a'za'vvv"))) @?= "xyxyzxyxyxyxyxyxyxyxy" , testCase "RepeatLast test 34" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("c", "a'za'Vv") ] coinput) (stringToKeyMacro "'c'v"))) @?= "xyyzxyyzxyyzxyyxyyzxyyzxyyzxyy" , testCase "RepeatLast test 35" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("b", "'za'V") ] coinput) (stringToKeyMacro "'ab'Vv"))) @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy" , testCase "RepeatLast test 36" $ snd (last (unwindMacros (bindInput [ ("a", "'xy'v") , ("b", "za'za'") ] coinput) (stringToKeyMacro "'ab'V'ab'V"))) @?= "xyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyyxyyzxyyzxyy" , testCase "RepeatLast test 37" $ snd (last (unwindMacros (bindInput [ ("b", "z'xy'vv") , ("c", "'xyvb'V") ] coinput) (stringToKeyMacro "'c'V"))) @?= "xyyzxyyyxyyzxyyyxyyzxyyyxyyzxyyy" , testCase "RepeatLast test 38" $ snd (last (unwindMacros coinput (stringToKeyMacro "'xv'V"))) @?= "xxxx" , testCase "RepeatLast test 39" $ fst <$> unwindMacros coinput (stringToKeyMacro "'xv'V") @?= [[(Right "", "'xv'V", "")], [(Left "", "xv'V", "")], [(Left "x", "v'V", "x")], [(Left "x", "x'V", "x")], [(Left "xx", "'V", "x")], [(Right "xx", "V", "x")], [(Right "", "xx", ""), (Right "xx", "", "V")], [(Right "", "x", "x"), (Right "xx", "", "V")], [(Right "xx", "", "V")]] , testCase "RepeatLast test 40" $ snd (last (unwindMacros coinput (stringToKeyMacro "'xy'Vv"))) @?= "xyxyxy" , testCase "RepeatLast test 41; named macros not referentially transparent" $ snd (last (unwindMacros (bindInput [("a", "'xy'V")] coinput) (stringToKeyMacro "av"))) @?= "xyxyxyxy" -- because @a@ repeated; good! , testCase "RepeatLast test 42" $ snd (last (unwindMacros (bindInput [("a", "xy")] coinput) (stringToKeyMacro "'a'Vv"))) @?= "xyxyxy" -- because @V@ repeated; good! , testCase "RepeatLast test 43" $ snd (last (unwindMacros coinput (stringToKeyMacro "'xyV'V"))) @?= "xyxy" , testCase "RepeatLast test 44" $ snd (last (unwindMacros coinput (stringToKeyMacro "'xyV'v"))) @?= "xyxy" , testCase "RepeatLast test 45" $ snd (last (unwindMacros (bindInput [("a", "xyV")] coinput) (stringToKeyMacro "'a'V"))) @?= "xyxy" , testCase "RepeatLast test 46" $ snd (last (unwindMacros (bindInput [("a", "xyV")] coinput) (stringToKeyMacro "'a'v"))) @?= "xyxy" , testProperty "In-game macro and equivalent predefined macro agree" $ forAll (listOf (elements "`ABvV")) $ \macro -> let bindings = bindInput [("a", macro)] coinput inGameResult = snd (last (unwindMacros coinput (stringToKeyMacro macro))) in inGameResult === snd (last (unwindMacros bindings (stringToKeyMacro "a"))) .&&. inGameResult =/= "Macro looped" -- may not loop , testProperty "In-game and predefined with limited minimal bindings" $ forAll (listOf (elements "````''''ABCABCABCABCvVvVvVa")) $ -- may loop \macro -> let bindings = bindInput [("a", macro)] coinput in snd (last (unwindMacros bindings (stringToKeyMacro macro))) === snd (last (unwindMacros bindings (stringToKeyMacro "a"))) , testProperty "In-game and predefined with limited multiple bindings" $ forAll (listOf (elements "```ABCDvVabccc")) $ \macro -> -- The macros may still loop due to mutual recursion, -- even though direct recursion is ruled out by filtering. let macroA = filter (/= 'a') macro macroB = filter (/= 'b') $ take 5 $ reverse macro bindings = bindInput [ ("a", macroA) , ("b", macroB) , ("c", "A'B''CD'") ] coinput in snd (last (unwindMacros bindings (stringToKeyMacro macroA))) === snd (last (unwindMacros bindings (stringToKeyMacro "a"))) ] LambdaHack-0.11.0.0/test/Spec.hs0000644000000000000000000000641607346545000014303 0ustar0000000000000000module Main (main) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Data.Text as T import Options.Applicative import System.IO.Unsafe (unsafePerformIO) import Test.Tasty import Test.Tasty.HUnit import Game.LambdaHack.Client.UI.UIOptions import Game.LambdaHack.Client.UI.UIOptionsParse import Game.LambdaHack.Common.ClientOptions import qualified Game.LambdaHack.Content.RuleKind as RK import Game.LambdaHack.Server import qualified Content.RuleKind import TieKnot import ActorStateUnitTests import CommonMUnitTests import HandleHelperMUnitTests import HandleHumanLocalMUnitTests import InventoryMUnitTests import ItemDescriptionUnitTests import ItemKindUnitTests import ItemRevUnitTests import LevelUnitTests import MonadClientUIUnitTests import ReqFailureUnitTests import SessionUIUnitTests main :: IO () main = defaultMain tests tests :: TestTree tests = testGroup "Tests" [ actorStateUnitTests , commonMUnitTests , handleHelperMUnitTests , handleHumanLocalMUnitTests , inventoryMUnitTests , itemDescriptionUnitTests , itemKindUnitTests , itemRevUnitTests , levelUnitTests , reqFailureUnitTests , macroTests , monadClientUIUnitTests , integrationTests ] integrationTests :: TestTree integrationTests = testGroup "integrationTests" $ [ testCase "Null frontend; 5 frames" $ do let seed = "SMGen 131 141" args = words "--dbgMsgSer --logPriority 4 --newGame 1 --noAnim --maxFps 100000 --frontendNull --benchmark --stopAfterFrames 5 --automateAll --keepAutomated --gameMode crawl" ++ [ "--setDungeonRng", seed, "--setMainRng", seed] serverOptions <- handleParseResult $ execParserPure defaultPrefs serverOptionsPI args tieKnot serverOptions ] #ifndef USE_BROWSER ++ let corule = RK.makeData Content.RuleKind.standardRules uiOptions = unsafePerformIO $ mkUIOptions corule defClientOptions testFontset :: Int -> String -> TestTree testFontset n fontsetName = testCase ("SDL fronted; 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 -- and quit at once, for testing on CIs without graphics access. let seed = "SMGen " ++ show (13 + 2 * n) ++ " " ++ show (15 + 4 * n) args2 = words "--dbgMsgSer --logPriority 0 --newGame 3 --maxFps 100000 --benchmark --stopAfterFrames 5 --automateAll --keepAutomated --gameMode battle" ++ [ "--setDungeonRng", seed, "--setMainRng", seed , "--fontset", fontsetName ] serverOptions2 <- handleParseResult $ execParserPure defaultPrefs serverOptionsPI args2 tieKnot serverOptions2 in zipWith testFontset [0..] $ map (T.unpack . fst) $ uFontsets uiOptions #endif LambdaHack-0.11.0.0/test/UnitTestHelpers.hs0000644000000000000000000002713707346545000016516 0ustar0000000000000000{-# LANGUAGE GADTs, GeneralizedNewtypeDeriving #-} -- | Monadic test harness and other stubs for unit tests. module UnitTestHelpers ( CliState(..) , emptyCliState , executorCli , stubLevel , stubState , stubCliState , testActor , testActorId , testActorWithItem , testCliStateWithItem , testFactionId , testItemId , testItemKind , testLevelId #ifdef EXPOSE_INTERNAL -- * Internal operations , CliMock(..) , fchanFrontendStub #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import qualified Control.Monad.IO.Class as IO import Control.Monad.Trans.State.Strict (StateT (StateT, runStateT), gets, state) import qualified Data.EnumMap.Strict as EM import Game.LambdaHack.Atomic (MonadStateWrite (..)) import Game.LambdaHack.Client import qualified Game.LambdaHack.Client.BfsM as BfsM import Game.LambdaHack.Client.HandleResponseM import Game.LambdaHack.Client.MonadClient import Game.LambdaHack.Client.State import Game.LambdaHack.Client.UI import Game.LambdaHack.Client.UI.ActorUI import Game.LambdaHack.Client.UI.Content.Screen import Game.LambdaHack.Client.UI.ContentClientUI import Game.LambdaHack.Client.UI.Frontend 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.Common.Actor import Game.LambdaHack.Common.Area 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 import Game.LambdaHack.Common.MonadStateRead import Game.LambdaHack.Common.Perception import Game.LambdaHack.Common.Point import Game.LambdaHack.Common.State import Game.LambdaHack.Common.Time import Game.LambdaHack.Common.Types import Game.LambdaHack.Content.ItemKind import Game.LambdaHack.Content.RuleKind import Game.LambdaHack.Content.TileKind 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.Flavour -- * UI frontend stub -- Read UI requests from the client and send them to the frontend, fchanFrontendStub :: ChanFrontend fchanFrontendStub = ChanFrontend $ \case FrontFrame _ -> putStr "FrontFrame" FrontDelay _ -> putStr "FrontDelay" FrontKey _ _ -> return KMP {kmpKeyMod = K.escKM, kmpPointer = PointUI 0 0} FrontPressed -> return False FrontDiscardKey -> putStr "FrontDiscardKey" FrontResetKeys -> putStr "FrontResetKeys" FrontShutdown -> putStr "FrontShutdown" FrontPrintScreen -> putStr "FrontPrintScreen" -- * Mock client state implementation data CliState = CliState { cliState :: State -- ^ current global state , cliClient :: StateClient -- ^ current client state , cliSession :: Maybe SessionUI -- ^ UI state, empty for AI clients -- Not needed for the mock monad (and blank line needed to avoid making this -- comment a haddock for @cliSession@ field): -- , cliDict :: ChanServer -- , cliToSave :: Save.ChanSave (StateClient, Maybe SessionUI) } -- * Option stubs stubUIOptions :: UIOptions stubUIOptions = UIOptions { uCommands = [] , uHeroNames = [] , uVi = False , uLeftHand = False , uChosenFontset = "" , uAllFontsScale = 0.0 , uFullscreenMode = NotFullscreen , uhpWarningPercent = 0 , uMsgWrapColumn = 0 , uHistoryMax = 0 , uMaxFps = 0.0 , uNoAnim = False , uOverrideCmdline = [] , uFonts = [] , uFontsets = [] , uMessageColors = [] } stubClientOptions :: ClientOptions stubClientOptions = defClientOptions { schosenFontset = Just "snoopy" , sfontsets = [("snoopy", FontSet { fontMapScalable = "scalable" , fontMapBitmap = "bitmap" , fontPropRegular = "propRegular" , fontPropBold = "propBold" , fontMono = "mono" })] } -- * Stub identifiers -- Using different arbitrary numbers for these so that if tests fail -- due to missing keys we'll have more of a clue. testLevelId :: LevelId testLevelId = toEnum 111 testActorId :: ActorId testActorId = toEnum 112 testItemId :: ItemId testItemId = toEnum 113 testFactionId :: FactionId testFactionId = toEnum 114 -- * Game arena element stubs testArea :: Area testArea = fromJust(toArea (0, 0, 0, 0)) testLevelDimension :: Int testLevelDimension = 3 stubLevel :: Level stubLevel = Level { lkind = toEnum 0 , ldepth = Dice.AbsDepth 1 , lfloor = EM.empty , lembed = EM.empty , lbig = EM.empty , lproj = EM.empty , ltile = unknownTileMap testArea unknownId testLevelDimension testLevelDimension , lentry = EM.empty , larea = trivialArea (Point 0 0) , lsmell = EM.empty , lstair = ([],[]) , lescape = [] , lseen = 0 , lexpl = 0 , ltime = timeZero , lnight = False } testFaction :: Faction testFaction = Faction { gkind = emptyUIFaction , gname = "" , gcolor = Black , gdoctrine = Ability.TBlock , gunderAI = True , ginitial = [] , gdipl = EM.empty , gquit = Nothing , _gleader = Nothing , gstash = Nothing , gvictims = EM.empty } testActor :: Actor testActor = Actor { btrunk = testItemId , bnumber = Nothing , bhp = 0 , bhpDelta = ResDelta (0,0) (0,0) , bcalm = 0 , bcalmDelta = ResDelta (0,0) (0,0) , bpos = Point 0 0 , boldpos = Nothing , blid = testLevelId , bfid = testFactionId , btrajectory = Nothing , borgan = EM.empty , beqp = EM.empty , bweapon = 0 , bweapBenign = 0 , bwatch = WWatch , 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,[])} -- Stublike state that should barely function for testing. stubState :: State stubState = let singletonFactionUpdate _ = EM.singleton testFactionId testFaction singletonDungeonUpdate _ = EM.singleton testLevelId stubLevel singletonActorDUpdate _ = EM.singleton testActorId testActor singletonActorMaxSkillsUpdate _ = EM.singleton testActorId Ability.zeroSkills copsUpdate oldCOps = oldCOps {corule = (corule oldCOps) { rWidthMax = testLevelDimension , rHeightMax = testLevelDimension }} stateWithMaxLevelDimension = updateCOpsAndCachedData copsUpdate emptyState stateWithFaction = updateFactionD singletonFactionUpdate stateWithMaxLevelDimension stateWithActorD = updateActorD singletonActorDUpdate stateWithFaction stateWithActorMaxSkills = updateActorMaxSkills singletonActorMaxSkillsUpdate stateWithActorD stateWithDungeon = updateDungeon singletonDungeonUpdate stateWithActorMaxSkills in stateWithDungeon testStateWithItem :: State testStateWithItem = let swapToItemActor _ = EM.singleton testActorId testActorWithItem in updateActorD swapToItemActor stubState emptyCliState :: CliState emptyCliState = CliState { cliState = emptyState , cliClient = emptyStateClient testFactionId , cliSession = Nothing } stubSessionUI :: SessionUI stubSessionUI = let actorUI = ActorUI { bsymbol = 'j' , bname = "Jamie" , bpronoun = "he/him" , bcolor = BrCyan } in (emptySessionUI stubUIOptions) { sactorUI = EM.singleton testActorId actorUI , sccui = emptyCCUI { coscreen = emptyScreenContent { rwidth = testLevelDimension , rheight = testLevelDimension + 3 } } , schanF = fchanFrontendStub } stubCliState :: CliState stubCliState = CliState { cliState = stubState , cliClient = (emptyStateClient testFactionId) { soptions = stubClientOptions , sfper = EM.singleton testLevelId emptyPer } , cliSession = let target = TPoint TUnknown testLevelId (Point 1 0) in Just (stubSessionUI {sxhair = Just target}) } testCliStateWithItem :: CliState testCliStateWithItem = stubCliState { cliState = testStateWithItem } -- * Monad harness mock -- | Client state transformation monad mock. newtype CliMock a = CliMock { runCliMock :: StateT CliState IO a } -- we build off io so we can compile but we don't want to use it; -- TODO: let's try to get rid of the IO. I can't see any problem right now. -- We'd need to to define dummy liftIO in some monads, etc. deriving (Monad, Functor, Applicative) instance MonadStateRead CliMock where {-# INLINE getsState #-} getsState f = CliMock $ gets $ f . cliState instance MonadStateWrite CliMock where {-# INLINE modifyState #-} modifyState f = CliMock $ state $ \cliS -> let !newCliS = cliS {cliState = f $ cliState cliS} in ((), newCliS) {-# INLINE putState #-} putState newCliState = CliMock $ state $ \cliS -> let !newCliS = cliS {cliState = newCliState} in ((), newCliS) instance MonadClientRead CliMock where {-# INLINE getsClient #-} getsClient f = CliMock $ gets $ f . cliClient liftIO = CliMock . IO.liftIO instance MonadClient CliMock where {-# INLINE modifyClient #-} modifyClient f = CliMock $ state $ \cliS -> 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 {-# INLINE modifySession #-} modifySession f = CliMock $ state $ \cliS -> let !newCliSession = f $ fromJust $ cliSession cliS !newCliS = cliS {cliSession = Just newCliSession} in ((), newCliS) updateClientLeader aid = do s <- getState modifyClient $ updateLeader aid s 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 -- Don't catch anything; assume exceptions impossible. {-# INLINE execPutState #-} execPutState = putState executorCli :: CliMock a -> CliState -> IO (a, CliState) executorCli = runStateT . runCliMock