General UI improvements:
- Overhauled the main and sub menus of the game via new BaseMenu class. They now act like navigation pages through softcoded @export vars with some optional animation support through Tweens for transitions. Should be sufficient for most use cases by overriding these. As a side effect of the refactor it makes cleaning up pages when they're no longer in use much easier.
- Added Settings menu. Syncs with user_settings.json. I won't go too crazy on a settings system, but I wanted to at least get volume sliders and mute-in-background in.
- Much better tooltip support in UI. Centralized into one UI component that's easier to manage.
- Keyword tooltips can be supplied status effects and will display the relevant icon.
- Added some general reusable UI components. Experimenting with more advanced UI patterns since writing 10 million XContainer scripts gets old fast.
- Fixed a loooot of UI bugs. Combat is much less buggy in general and has less chance of freezing in the many weird edge cases one can create in these types of games.
- Added ImageFade effects over Combatants. This is used for block but can be used as an interface to display any kind of image. Going to try getting more support for effects over time.
- Run time is tracked and stored via a timer in the top bar.
- More UI elements will hide when the player dies, fixing some out of combat related edge cases.
- Run summary can display stats throughout a run at the end. This is easily configurable to display various stats.
Run History:
Signal API hooks are now handled via StatHandler singleton.
Detailed run history tracking through RunStatsData (entire run), CombatStatsData (single combat instance), and RunStatsData
This is all stored in profile.json. Given how ridiculously bloated this file can now become as a result of including run history, I've added several fields to StatsHandler which will remove all the more detailed stuff at the end of a run, or throttle the number of stored runs under a given max value. Tweak as needed for your specific game.
This works in tandem with ProfileData, which just manages a running total of everything across all runs. Added in current/total win/loss streaks across profile and characters.
Total run time and shortest victory is also tracked on a profile and character basis.
Codex:
- Added in ways to organize and sort cards by color (card packs), alpha, cost, and name, and type.
- Consumables, artifacts, and enemies have been added to the codex.
Rest Action Improvements:
- Added in a special ActionRestActionEnd action which will signify to the UI that a rest action is completely done. These can be automatically supplied via a flag in RestActionData. Otherwise for rest actions that you can cancel out of like upgrading cards, the ActionRestActionEnd can be manually supplied in the action payload to prevent it from being consumed if you back out.
- RestActionData can have an optional rest_action_stat_name which will be tracked in RunStatsData. This will let you track things like the times you rested, upgraded cards, or whatever at a rest site in the run history, or listen to them for artifact interactions.
Card Playing:
Tons of work has been done to improve how cards are picked, displayed, and played and unifying it all under one consistent inferface.
- Refactored Hand into HandManager singleton and Hand UI element, This cleaned up a lot of ugly code and let me remove many janky "request" signals since messaging to Hand was otherwise a mess.
- Card piles are now unified under a single string based interface, moving away from multiple enums. Much more human readable, and the refactor has let me do cool stuff like changing where cards go as they're played (think StS Rebound) or have cards with conditional destinations like exhausting after 3 plays.
- I've improved Card and Hand's energy/playability calculations to utilize interception-preview tech, similar to how card descriptions and intent work. This has allowed for new interceptors that can affect how cards are played by changing card costs, making cards (un)playable, or ignoring play validators. I added a test status effect that makes the next non-zero cost attack cost 0 to showcase how this can be used (all attacks will display as 0 cost with the status active, showing UI integration).
- As part of the above, cards that cannot be played either by failing validators or having insufficient energy will display their card cost in red, similar to how StS works.
- CardPlayRequest now tracks the cards in hand at time of play, which can be hooked into via card pick actions and some validators. This opened the door for cards that check for or mess with adjacent cards, of which I've added some examples for.
- Added a very barebones CardTrail UI effect. Nothing fancy but it shows where cards are going when played.
- If the player doesn't have enough energy, a speech bubble will appear saying as much.
General Card Improvements:
- Cards now have a special ActionCardPlayEnd action that is injected automatically at the bottom of the stack each time one is played that signals the card is completely done with its effects. This should allow for you to have actions that play multiple cards at once and resolve much more consistently. More complex testing is needed but it seems to work well.
- Cards have a card_first_upgrade_value_changes field.
- Cards have improved shuffling. Added a simple card_reshuffle_priority property to CardData. This allows you to have cards that are top or bottom decked on subsequent shuffles, which mimics some modded cards/relics.
- In addition to discrete shuffle priority values, cards can have a card_shuffle_weighting which makes them more or less likely to appear in the front or back of the draw pile. This supports an extremely niche StS1 modded relic I wanted to try.
- Upgrading cards has been improved. I've added flags to CardData.upgrade_card() to accept multiple upgrades at once, as well as bypass the max upgrade amount. I've also added these to their respective upgrade actions. This should allow you to do things like linearly upgrade cards multiple times. Inspired by a few StS1 mods with similar mechanics.
- Card energy icons will now load based on the card's color rather than the character's color. It was intended that the player's mana would be displayed, but it created an annoying edge case when displaying cards with no player character loaded, such as on the codex.
page. This change makes it more consistent with how StS displays energy icons anyways. - Cards generate their descriptions and load their textures better. Most main keywords can be automatically appended to the card descriptions.
- Just for fun I added the ability to swap two cards in hand, mainly to stress test the Hand UI. It broke a lot of stuff, which means it was good to do.
Card Decorators:
Refactored CardListener into the more comprehensive CardDecorator system. Card decorators provide similar functionality to those in StS 2's enchantments system as well as in modded StS1. You can dynamically attach decorators to a card, which can do various things such as mutating card properties and card_values, listening for combat related events (previous behavior of card listeners), and dynamically modifying Card visuals. They can also be used to apply "counters/tokens" to Cards, inspired by some of PackMaster's cards. This is a system I've wanted for a very long time but wasn't sure how I wanted it done until now since it combines a lot of different features under one interface.
- ListenerCardValueModifier and ListenerCardCostModifier (provides tech for cards like Eviscerate, Masterful Stab, and Gold Axes) have been turned into CardDecoratorDynamicValueModifier and CardDecoratorDynamicCostModifier.
- Added various decorators to include things like prepending a blocking action to a card when played, removing exhaust, and drawing cards the first time the card is drawn.
Consumables:
- Consumables have a verb string property like "drink", "throw", "use" etc that will display in the UI.
- Consumables now have a values property like cards do which is injected into a CardPlayRequest for the consumable's actions. Previously it was just a simple action payload.
- Consumables are intercepted before/during use like card plays are through the ActionConsumable special action, allowing interceptors to affect them.
- Consumables can cost energy. This value can be intercepted and modified.
- Consumables can be made to not be usable by the player, useful for things like automatic consumables
- Through some clever hackery, I was able to get auto-reviving consumables working in a way that's consistent with the code base, so others don't have to implement that.
Audio Support:
- Added support for music and sounds through ActionPlayMusic and ActionPlaySound and a sound manager addon.
- These are controlled through volume sliders in settings menu with option to mute in background.
- Music can be specified on a per act, location type, event, and default basis (in FileLoader), allowing a high degree of control for what plays when with multiple fallbacks. Admittedly I haven't thoroughly tested this beyond a single track, but it should work in theory fingers crossed.
- Characters can have a selection audio that plays on the new run screen.
- ActionAttackGenerator can be supplied audio, which will play for each attack.
- Enemies will play an intent sound if one is specified. If attacks are made, it will play for the attacks. Otherwise it will play once.
Animation Support:
- Combatants now use AnimatedSprite2D instead of just a Sprite.
- Added support for animation through ActionPlayAnimation and AnimationData.
- ActionAttackGenerator can take in a per attack or across all attacks animation. ie: a multihit attack will attack either once or for each attack, depending on your liking.
- Animations use state machine logic and will automatically transition, loop, or end as provided in AnimationData.
- Added helper methods to rapidly generate most standard animations (attacking, idle, death)
VFX Support:
- Added AnimatedCombatEffect which provides a simple AnimatedSprite2D hooked into AnimationData
- Impact animations can be supplied to ActionAttackGenerator or EnemyIntentData
- A default animation has been provided and supplied to enemies and attack cards.
- Support for random offsets in vfx animations
Status Effects:
Significant refactor of status effects simplifies logic, makes it more data oriented, and adds more features.
- Added more hooks for when statuses can process. This includes at the start of and end of enemy turns before/after individual intents, and allows fine control over player phases such as before/after discarding cards at end of turn similar to pre/post draw at start of turn.
- Status effects can have better defined charge ranges.
- They can be allowed to under/overflow their charges and perform special actions when it happens.
- Added a zero out decay rate to make a status always last one turn.
- Statuses that don't allow duplicates have specifiable collision strategies for secondary charges.
- StatusEffectTimedStatus class provides an easy way to have generically implemented statuses that count down.
- Removed Bomb test status script, as the configurable StatusEffectTimedStatus rendered it unnecessary.
- Fixed PRE_DRAW_PLAYER_START_TURN and POST_DRAW_PLAYER_START_TURN status processing times, which were flipped (oopsies)
Events
- Added some more test events and cleaned up the data for them.
- Events have a death message attached to them.
Artifacts
- Added ArtifactPackData and ArtifactFilter. This allows querying and grouping artifacts up similarly to CardPackData and CardFilter. The player's artifact pool (what determines the order you see artifacts appear) has also been greatly improved.
- Artifacts can be flagged as disabled, preventing them from firing action payloads.
- Artifacts can generate interceptors. Added a test artifact similar to StS 1's ectoplasm, giving max energy but preventing money gain.
Difficulty Improvements
- Difficulty levels can be restricted to the highest difficulty the character has beaten the game at. This behavior can be changed via ProfileData.ENABLE_ALL_DIFFICULTIES.
- Added 3 functioning test difficulties that affect enemy health and attack patterns for standard enemies, minibosses, and bosses, to show off the enemy improvements.
Improved Enemies
Enemies have been refactored to be much easier to generate using code, have better intent logic, and make better use of difficulty levels:
- Enemy intents are no longer stored in a variant dictionary. Instead an EnemyIntentData embedded object is used, improving type safety.
- Enemies can now use "intent overriding". This allows you to define an intent graph composed of standard intents, and then based on the difficulty level they will begin to override existing intents by aliasing them on lookup. This is much, much cleaner to use and to write than the previous system which required redefining the entire intent graph as an enemy difficulty modifier.
- Enemy intent can include intent types. Attacking, blocking, buffing, debuffing, and summoning are included by default and the game will automatically infer them. These map to icons above the enemy's head when their intent is being displayed.
- Enemies can now have random health ranges using enemy_health_max_random_lower and enemy_health_max_random_upper. This uses its own RNG track.
I've also cleaned up the enemy generation code a little using helper functions. These make everything into nice parameterized one-liners that factor in difficulty:
- Adding intents has been streamlined using EnemyData.add_intent_state().
- Enemy health ranges can be easily defined using EnemyData.add_health_bounds(), which cleanly hooks into the existing enemy modifiers system.
General API Improvements:
-
Combat events can specify no rewards.
-
Run Modifiers can be specified as automatic, meaning they'll be registered as always on regardless of difficulty or custom modifiers. This allows for registering universal interceptors without using artifacts.
-
BaseCombatant, Player, and Enemy have shared methods for manipulating health directly.
Added and improved many actions and validators:
- Added force_dead_targets flag to BaseAction. Normally targeting will auto filter out dead targets, however this allows you to bypass that. Useful for auto revive consumables, cause you're you know, dead.
- Added ActionSetHealth. ActionAddHealth and ActionHealPercent now work properly for enemies as well as player. Updated test data to include this.
- Turn energy logic (resetting and then granting energy) now uses action pipeline like rest of code base, making it interceptable.
- Added ActionTalk, to create speech bubbles.
- When enemies hit 0 HP they provide an ActionDeath instant action which can be intercepted right before death.
- ActionUseConsumable can optionally instant process all actions. This combined with the above means you can heal the player right before they die which enables things like auto revive consumables.
- Post combat actions.
- Damage calculations for ActionAttack and ActionDirectDamage are much more sophisticated and will store their results in CardPlayRequest. This allows things like raw unblocked damage (previous behavior), overkill damage beyond 0 HP, and unblocked damage capped to 0 HP, which can be piped into other effects.
- ActionCycleEnemyIntent no longer crashes on targeting dead enemies.
- ActionHandler manages async actions better on combat/run end, forcing them to resolve where possible. It will no longer clear the stack or sometimes hang infinitely (fingers crossed). Also allows for post combat actions to work.
- Many CardsetActions now all use the same variable naming conventions. This makes action interception more consistent for them.
- Interceptors now support custom_key_names which was working improperly at the interception layer.
- Removed ActionCorrosion test action, as action tags have rendered it unnecessary.
Misc code hygiene and bug fixes:
- Lots of comments.
- Moved the thousands of lines of test data code in Global into a GlobalTestDataGenerator singleton. Production data generation also has its own singleton now. Should declutter a lot of code and make switching between test and prod data easier. I've also massively cleaned up the test code to make everything more functional and consistent.
- Moved around a large number of files in an attempt to better organize everything. Scene/Script/UI files are always in general a pain to manage consistently regarding grouping by purpose or by filetype and I attempted to find a healthy compromise. Still a lot of work on that front.
- Added missing texture and missing sound (quack!) files in FileLoader to make it obvious when stuff is missing/broken.
- UIDGenerator singleton has simply been rolled into PrototypeData as static methods/variables, since it wasn't being used anywhere else.
- Run start options now use their own RNG track.
- More code uses DebugLogger.
- StatusEffect and Consumable UI elements will load their textures which was an oversight.
- RunModiferData.run_modifier_interceptor_ids was named run_modifier_interceptor_script_paths for some reason. It otherwise works as intended but would cause crashing if you input script paths rather than RunModifierData IDs. Test data did not include this functionality until now.
- RichLabelAutoSizer no longer fails if multiple images are embedded in one rich text. The regex to make it work is rather flimsy but it works well enough.
- Fixed the spelling of "license" in the readme. Words are hard.