Makes another character's Star-cost cards usable when they arrive through Kaleidoscope, Prismatic Gem, or any other cross-class source.
In vanilla, Stars are Regent-exclusive. Any Regent card with a Star cost that
lands in another character's deck is unplayable forever - the game even has a
dedicated UnplayableReason.StarCostTooHigh for it. Starless converts those
cards instead of leaving them as bricks.
Applies only when a card's pool differs from its owner's character. Regent players are entirely unaffected.
| Card shape | Treatment |
|---|---|
| Fixed Star cost | Star cost removed, Energy cost raised by ceil(stars * 2/3) |
| X-Star cost | Filtered out of cross-class reward pools (see Limitations) |
| Only produces Stars | Filtered out of cross-class reward pools |
The ratio comes from the game's own card values, where 3 Stars is worth about 2 Energy. Comet (0 Energy / 5*) becomes a 4-Energy card; Crescent Spear (0 Energy / 1*) becomes 1 Energy.
Cards that grant Stars in addition to doing something real - ShiningStrike, SolarStrike, KnockoutBlow, GatherLight, Glow, BigBang, HiddenCache - are kept. They just lose a rider.
Requires the .NET 9 SDK. No NuGet packages: the project references the game's
own sts2.dll, GodotSharp.dll and 0Harmony.dll directly.
bash deploy.sh
Builds and copies Starless.dll + Starless.json into
<game>\mods\Starless\.
The game skips loading all mods until you accept the mods warning in-game once. Until then the log reads
Skipping loading mod Starless, user has not yet seen the mods warning.
src/StarlessConfig.cs - master toggle, the star:energy ratio, the reward
filter toggle, and verbose logging. Currently compile-time; wiring to ModConfig
is a to-do.
Tested by driving the real game, not simulated.
| Check | Result |
|---|---|
| Mod loads, initializer runs, Harmony patches bind | pass, no exceptions |
Comet on Ironclad (vanilla 0 Energy + 5*) |
shows 4 Energy, no star pip |
Crescent Spear on Ironclad (vanilla 1 Energy + 1*) |
shows 2 Energy, no star pip |
| Control: same cards with mod compiled inert | shows vanilla 0+5* and 1+1* |
| In-combat hook path (card in hand) | same converted costs |
| Playing a converted card | Energy 3/3 -> 1/3, card resolved |
| Unaffordable converted card (Comet at 4, with 3 Energy) | correctly greyed out |
| Regent regression: same cards on the Regent | unchanged, 0+5 and 1+1** |
| Kaleidoscope pickup (exercises the reward filter) | ran without throwing |
| Foreign cards with no Star cost (Sow, Compile Driver, Tracking) | untouched |
Crescent Spear reads "Deals 2 additional damage for ALL your cards that have a Star cost". Under Starless it still counts converted cross-class cards, so on a non-Regent it keeps scaling (+2 per Star card, including itself).
Measured: with only Crescent Spear present it dealt 10 (8 + 2x1); with Crescent Spear and Comet, 12 (8 + 2x2).
This is deliberate, not a leak. Crescent Spear is the only card in the game
that inspects Star costs (all 606 card types were scanned for reads of
CanonicalStarCost, CurrentStarCost, GetStarCostWithModifiers,
HasStarCostX and CostsEnergyOrStars), and it reads CanonicalStarCost -
the printed value, which nothing can modify even in vanilla. Starless changes
what a card costs you to play, never what the card is, so the count is the
game's own semantics. It also makes Crescent Spear a real payoff card for
collecting Regent cards cross-class.
Known cosmetic wart: converted cards hide their Star pip, so the player cannot see which cards are being counted. See "Possible future work" below.
Confirmed in-game two ways rather than by hoping a random reward appeared.
Runtime classifier audit (enable VerboseLogging, start any run):
[Starless][audit] cards with a star cost: 23
[Starless][audit] pure star generators (2): CARD.ROYAL_GAMBLE, CARD.VENERATE
The filter firing on a real Prismatic Gem reward roll as the Ironclad:
FILTERED from reward pool: CARD.ROYAL_GAMBLE (pool=REGENT_CARD_POOL, player=IRONCLAD)
FILTERED from reward pool: CARD.STARDUST (pool=REGENT_CARD_POOL, player=IRONCLAD)
FILTERED from reward pool: CARD.VENERATE (pool=REGENT_CARD_POOL, player=IRONCLAD)
Rewards still generated normally, so the filter composes with Prismatic Gem rather than breaking pool generation.
To fix the cosmetic wart, a converted card could still render its printed Star
pip greyed out. The rendering path is NCard.UpdateStarCostVisuals(PileType),
which owns three private nodes: _starIcon, _starLabel and - usefully -
_unplayableStarIcon, a greyed variant that already exists. A Harmony postfix
could re-show the icon and write CanonicalStarCost into the label.
Difficulty: low-to-moderate, roughly 30-60 lines, no new art and no Godot scene
editing. Caveats: UpdateStarCostColor / UpdateStarCostText are separate
entry points that would need covering too, _unplayableStarIcon may be claimed
by the game's own affordability rendering, and this is the most patch-fragile
kind of change in the mod because it reaches into private Godot node fields
rather than the sanctioned hook API. Worth doing only if players report
confusion.
- Multiplayer, entirely.
- X-Star cards are filtered, not converted. On v0.111.0 that is exactly one
card,
Stardust. Converting it needs patches to the asyncSpendResourcesstate machine andResolveStarXValue, which is a poor risk trade for a single card. Filtering it is strictly better than shipping it broken. - Not yet play-tested. See the verification checklist in the plan.
- Multiplayer untested. As with all STS2 mods, every player needs it installed at a matching load order.
- Cost conversion registers via
ModHelper.SubscribeForCombatStateHooks, not the run-state variant:Hook.ModifyStarCostandHook.ModifyEnergyCostInCombatboth iterate combat hook listeners. - Reward filtering registers via
SubscribeForRunStateHooks, becauseHook.ModifyCardRewardCreationOptionsiterates the run-level listeners. - "No star cost" is encoded as -1, not 0. Writing 0 leaves the card rendering a Star pip.
- Those hooks only fire in combat, so
DisplayPatchescovers the reward, shop and deck screens. Otherwise you would pick cards based on costs that do not match what you pay. - Star-generator detection scans card IL for
PlayerCmd.GainStarsrather than using a hardcoded list, so it keeps working when cards are added.