Outward
You are viewing a potentially older version of this package. View Latest Version
Install with App

Details

Date Uploaded
5 days ago
Downloads
1.1K
Size
77KB

Overview

No code, more effects. Custom Effects is a library for mod authors: nine new SL_Effect types and one SL_ItemExtension, each one usable straight from your XML.

Effects and extensions

Name What it does
SL_UseSkill Casts a skill for a character, as if from their quickslot
SL_LearnSkillHidden Teaches a skill that never shows in the skill menu
SL_Banish Takes a character out of the fight for a set time
SL_PlayVFXOnWeapon Plays a weapon VFX on bows and off-hand weapons too
SL_PlayVFXOnChar Anchors a VFX to the character it affects
SL_PlayVFXPulsed Makes a VFX flicker at chosen moments
SL_PlayTimedSound Plays a sound that ends on your schedule
SL_ReliableVisuals Plays a status's VFX and sounds on every client in co-op
SL_RemoveStatusParentOwner Removes a status from whoever fired or swung, not the one hit
SL_ItemVFX Gives any item a vanilla weapon's particles

SL_UseSkill

Makes a character cast a skill they already know, either the one who triggered the effect or the one it hit, waiting until they are free to act.

<SL_Effect xsi:type="SL_UseSkill">
  <Delay>0</Delay>
  <SyncType>OwnerSync</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <SkillID>8100102</SkillID>
  <Target>Owner</Target>
  <MaxWait>3</MaxWait>
</SL_Effect>
Field Meaning
SkillID Item ID of the skill. The target must already know it
Target Owner (the caster) or AffectedCharacter
MaxWait Seconds to wait for the character to be free before giving up

It goes through Item.TryQuickSlotUse — the same path as pressing a quickslot key — so the game handles requirements, cooldown, costs, animation and networking.

To make another player cast a skill from a projectile or blast, have the hit give them a short hidden status that carries SL_UseSkill; on the projectile itself, SL_UseSkill fails part of the time in co-op.

SL_LearnSkillHidden

Teaches a skill permanently without it appearing in the skill menu, for skills your mod triggers itself rather than ones the player casts.

<SL_Effect xsi:type="SL_LearnSkillHidden">
  <Delay>0</Delay>
  <SyncType>OwnerSync</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <SkillID>-88002</SkillID>
</SL_Effect>

The skill is genuinely learned, castable and networked — it simply never shows in the menu. It survives saves and reloads.

Why you need this

SL_UseSkill cannot cast a skill the character has not learned — Item.TryQuickSlotUse refuses one outright. Without a way to teach invisibly, a mod that fires its own skill must either put it in the player's menu or not work at all.

The learning is vanilla's TryUnlockSkill. Only the display lists are patched; the authoritative knowledge is left alone. Skill-limit mods count those same display lists, so they do not see it either.

Nothing is persisted: the display lists are rebuilt from scratch every session, which is why uninstalling this library just makes the skill visible again rather than corrupting a save.

SL_Banish

Sends a character beyond the veil (inert, untouchable, untargetable and unseen) for a duration, then returns it exactly as it was.

<SL_Effect xsi:type="SL_Banish">
  <Delay>0</Delay>
  <SyncType>MasterSync</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <Duration>10</Duration>
</SL_Effect>

Use MasterSync on enemies. Ends early if the character dies, restoring it as a lootable corpse.

A banished character is silent and cannot start an attack while it is away. An attack it had already begun is not cancelled — it is held exactly as it was and resolves normally when the banish ends.

Damage already under way still lands: DoTs applied before the banish keep ticking, and a banished creature can die from them. What it cannot take is a fresh hit — its hitboxes are gone.

Loading a save mid-banish releases the creature immediately rather than starting the duration over, and in co-op a client that joins while a banish is running follows the host's banish rather than running a clock of its own.

Why you need this

Vanilla has no equivalent. SL_Stun is cancelled by almost any state change, and SL_Petrify makes the target count as dead and revivable. Neither gives you a timed removal from the fight that returns the character untouched.

There are no toggles: Duration is the only field.

SL_PlayVFXOnWeapon

SL_PlayVFX, but able to land on weapons the vanilla hook silently refuses.

<SL_Effect xsi:type="SL_PlayVFXOnWeapon">
  <Delay>0</Delay>
  <SyncType>OwnerSync</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <VFXPrefab>VFXRunicBlade</VFXPrefab>
  <HitPos>false</HitPos>
  <ParentMode>This</ParentMode>
  <DontInstantiateNew>false</DontInstantiateNew>
  <AllowSkinnedMesh>true</AllowSkinnedMesh>
  <Slot>Keep</Slot>
</SL_Effect>
Field Meaning
VFXPrefab Same list as SL_PlayVFX
AllowSkinnedMesh Let the emitters find a SkinnedMeshRenderer. Default true
Slot Keep (the prefab's own value), MainHand or OffHand

Everything else behaves exactly as SL_PlayVFX.

Why you need this

A weapon VFX like VFXRunicBlade has no position of its own. Its particle systems each carry a VFXParticlesOnWeapon, which does not follow the hand — it takes the weapon's mesh and makes it the particle emission shape.

Two things make it give up, silently: it skips skinned weapons such as bows, and reads only the hand the prefab was made for. When it gives up, the particle system keeps whatever shape the prefab was authored with and the effect still plays — just in the wrong place and the wrong form, with nothing logged. That fallback looks exactly like a positioning bug, which it is not: no Position, Rotation, ParentMode or Delay can affect it, because position was never the mechanism.

This effect fixes both on its own copy of the VFX before the game clones it. Everything else is left to vanilla PlayVFX.

SL_PlayVFXOnChar

SL_PlayVFX, but it puts the VFX on the affected character instead of wherever the prefab expects to hang from.

<SL_Effect xsi:type="SL_PlayVFXOnChar">
  <Delay>0</Delay>
  <SyncType>OwnerSync</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <VFXPrefab>VFXBoonEthereal</VFXPrefab>
  <HitPos>false</HitPos>
  <ParentMode>This</ParentMode>
  <DontInstantiateNew>false</DontInstantiateNew>
  <Offset>
    <x>0</x><y>0</y><z>0</z>
  </Offset>
  <Detach>Character</Detach>
  <DetachLifetime>5</DetachLifetime>
</SL_Effect>
Field Meaning
VFXPrefab Same list as SL_PlayVFX
Offset Added to the character's position. Zero puts it on their root, at the feet
Detach Where the VFX goes when the status ends: None, Character or World
DetachLifetime Seconds before an outliving VFX is destroyed. Ignored when Detach is None

Everything else behaves exactly as SL_PlayVFX.

Why you need this

A status effect's transforms hang below StatusManager, which sits a metre above the character's root. A prefab carrying its own vertical offset stacks the two and plays two metres up, with the second metre inside the prefab itself. Nothing is logged and nothing errors; the effect simply plays in the wrong place.

You cannot fix it from stock XML. SL_PlayVFX has no Position field, and ParentMode offers only This and FXWorld — neither anchors to a character. Adding the vanilla VFXPositionOnChar hook does not work either: it answers the broadcast VFXSystem.Play sends and reads the character passed to it, and that character is null here, so it does nothing. This effect places the VFX itself.

Detach solves a separate problem. A VFX near the end of a status forces you to extend the status Lifespan to give it room — and that extends every other effect too. Character gives the VFX its own clock while it keeps riding the character; World leaves it where it stood, for a VFX that marks a place rather than a creature.

SL_PlayVFXPulsed

SL_PlayVFX that can blink its VFX off and on again at given times, so a long-lived effect can mark moments instead of merely existing.

<SL_Effect xsi:type="SL_PlayVFXPulsed">
  <Delay>0</Delay>
  <SyncType>OwnerSync</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <VFXPrefab>HexHauntedVFX</VFXPrefab>
  <HitPos>false</HitPos>
  <ParentMode>This</ParentMode>
  <DontInstantiateNew>false</DontInstantiateNew>
  <PulseAt>
    <float>8.68</float>
    <float>15.4</float>
    <float>19.98</float>
  </PulseAt>
  <PulseLength>0.4</PulseLength>
  <HardClear>true</HardClear>
  <StopAt>0</StopAt>
</SL_Effect>
Field Meaning
PulseAt Seconds after this effect activates at which the VFX blinks off. Order does not matter
PulseLength How long each blink stays dark. Default 0.3
HardClear Destroy the particles already in the air instead of letting them fade. Default true
StopAt Seconds after activation at which the VFX stops for good. Zero means never

Everything else behaves exactly as SL_PlayVFX.

If the log says no PulseAt times and no StopAt were read, or reports 0 pulse times, the entries were dropped — each must be a <float> element.

Why you need this

Two separate things make a flicker impossible from stock XML:

  • Every PlayVFX instantiates its own copy of the prefab and keeps it in a private field. Two effects pointed at the same VFXPrefab therefore produce two independent VFX, and neither can stop the other. Stacking SL_PlayTimedVFX entries at intervals does not pulse one effect — it spawns several, each stopping on its own clock
  • VFXSystem.Stop() calls ParticleSystem.Stop() with no arguments, which means StopEmitting. Emission halts, but every particle already in the air lives out its startLifetime first

This effect owns one instance for its whole life and drives it from a single coroutine. HardClear uses StopEmittingAndClear to destroy the live particles rather than let them fade, which is what makes a short blink actually read as a blink.

PulseAt is relative to activation, not to the status. Effect.Delay is applied before the effect activates, so subtract it: a mark at 9.83 seconds on an effect with <Delay>1.15</Delay> is 8.68.

SL_PlayTimedSound

SL_PlaySoundEffect that stops after a given time, instead of running until its clip ends or the status does.

<SL_Effect xsi:type="SL_PlayTimedSound">
  <Delay>0</Delay>
  <SyncType>OwnerSync</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <Sounds>
    <Sounds>ENV_MagicSourceLoop</Sounds>
  </Sounds>
  <Follow>true</Follow>
  <MinPitch>1</MinPitch>
  <MaxPitch>1</MaxPitch>
  <StopAfter>10</StopAfter>
  <FadeIn>0</FadeIn>
  <FadeOut>0.15</FadeOut>
</SL_Effect>
Field Meaning
Sounds Same list as SL_PlaySoundEffect; one is chosen, as vanilla does
StopAfter Seconds after activation at which the stop begins. Zero means never
FadeIn Seconds to fade in. Zero starts at full volume
FadeOut Seconds to fade out; the sound is silent at StopAfter + FadeOut. Zero cuts at StopAfter

Follow, MinPitch and MaxPitch behave exactly as SL_PlaySoundEffect.

Why you need this

SL_PlaySoundEffect fires and forgets. The audio manager hands back a sound source and the vanilla effect discards it, so there is no handle to stop — which makes the status Lifespan the only control you have over when a sound ends. Extend a Lifespan for any other reason and every sound in the status is dragged along with it.

This plays the sound itself so it can keep that handle. Everything else was checked against the decompiled vanilla effect rather than assumed, so swapping one for the other changes nothing except that it now stops.

SL_ReliableVisuals

A marker. It does nothing on its own. Put one on a status effect and that status's VFX and sounds play on every client, not only on the one that owns the affected character.

<SL_Effect xsi:type="SL_ReliableVisuals">
  <Delay>0</Delay>
  <SyncType>OwnerSync</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <IncludeBlasts>false</IncludeBlasts>
</SL_Effect>
Field Meaning
IncludeBlasts Also play SL_ShootBlast. Default false

WARNING: IncludeBlasts changes gameplay, not just visuals

A blast normally deals damage, and playing one on a client that does not own the character is a gameplay change rather than a visual. Turn this on only for a blast you have deliberately made inert — RefreshTime -1, HitOnShoot false — and are using purely for its particles.

Why you need this

A status applied to an AI is owned by the master client, so only the master runs its effects locally. Every other client depends on this:

if (m_statuses.TryGetValue((UID)_statusUID, out status))
    status.OnReceiveActivatedEffects(_affectedCharacter, _activatedEffectsInfos);

The receiving client must already hold that exact status UID — and every application mints a new one, so an announcement that arrives before the status itself is discarded silently.

This acts when the status arrives, which happens reliably on every client, rather than when its effects are announced. Nothing is sent, so nothing can be lost: both clients hold the same XML and replay it themselves. If the announcement does arrive afterwards it is suppressed, so nothing plays twice.

Limits:

  • Conditions are evaluated locally: an effect gated on state a remote client cannot read correctly may decide differently there. Avoid EffectConditions on effects you mark
  • Timing is per-client: each client times from its own status arrival, so a long sequence can sit tens of milliseconds apart between screens. Invisible for visuals; do not build gameplay on it
  • It reports itself in the log — played N visual effects, plus (N FAILED) and a warning naming the effect if any throws

SL_RemoveStatusParentOwner

SL_RemoveStatusEffect, but on the character who fires the blast or projectile, or wields the weapon, instead of the one it hits.

<SL_Effect xsi:type="SL_RemoveStatusParentOwner">
  <Delay>0</Delay>
  <SyncType>Everyone</SyncType>
  <OverrideCategory>None</OverrideCategory>
  <CleanseType>StatusSpecific</CleanseType>
  <SelectorValue>RunicStealth</SelectorValue>
  <Requires_AffectedChar>true</Requires_AffectedChar>
</SL_Effect>
Field Meaning
CleanseType Same as SL_RemoveStatusEffect: StatusSpecific, StatusFamily, StatusType, StatusNameContains or NegativeStatuses
SelectorValue Same as SL_RemoveStatusEffect
Requires_AffectedChar Remove it only when the blast, projectile or weapon actually hit a character. Default true. With false, a projectile removes it on any end: a hit, a wall or running out its lifespan

Put it in an SL_ShootBlast's BlastEffects, an SL_ShootProjectile's ProjectileEffects, a weapon's HitEffects or an imbue's effects with OverrideCategory Hit. On a skill's or a status's own effects it removes from the caster or the bearer, exactly as SL_RemoveStatusEffect would.

Why you need this

Inside a blast, a projectile or a weapon's hit, the affected character is whoever was hit, so SL_RemoveStatusEffect there strips the status from the target. Nothing in stock XML reaches back to the shooter or the wielder for a status.

This is vanilla's own RemoveStatusEffect, handed the shooter or the wielder instead of the target, so every CleanseType behaves exactly as it does there.

SL_ItemVFX

Puts a vanilla weapon's own particles on any item. It goes in an item's <ItemExtensions>.

<ItemExtensions>
  <SL_ItemExtension xsi:type="SL_ItemVFX">
    <VFX>
      <VFX>Obsidian</VFX>
    </VFX>
    <Fit>Auto</Fit>
    <Offset><x>0</x><y>0</y><z>0</z></Offset>
    <Rotation><x>0</x><y>0</y><z>0</z></Rotation>
    <Scale>1</Scale>
    <SizeMultiplier>1</SizeMultiplier>
    <ShowInHand>true</ShowInHand>
    <ShowSheathed>true</ShowSheathed>
    <ShowOnGround>true</ShowOnGround>
    <ShowOnBag>false</ShowOnBag>
  </SL_ItemExtension>
</ItemExtensions>
Field Meaning
VFX Whose particles: a weapon set or a unique weapon, from the lists below. List several and they all play
Fit How they are fitted to the item: Auto, Stretch, Original, Mesh, Bounds or Blade. Default Auto
Offset, Rotation Fine-tuning, in the item's own space
Scale Enlarges the area the particles come from
SizeMultiplier Enlarges the particles themselves
ShowInHand, ShowSheathed, ShowOnGround, ShowOnBag When they show: drawn, sheathed, lying on the ground, hung on a backpack. Default true, except ShowOnBag
  • Purely visual and local: nothing is sent over the network or saved, and every client shows it for itself
  • The log names where each item's particles came from and how they were fitted: SL_ItemVFX: <item> (<ID>) - Obsidian from 2000260, 1 emitters, Fit=Auto (Stretch) ratio=1.13.
Weapon sets

The item gets the set's weapon of its own type. For a type the set does not have, it uses the first weapon listed, and by default its particles cover the item's blade.

Entry Weapons
Obsidian, Meteoric Sword, Axe, Mace, Claymore, Greataxe, Greatmace, Spear, Halberd, Knuckles, Bow, Chakram, Shield, Dagger
Palladium Sword, Axe, Mace, Claymore, Greataxe, Greathammer, Spear, Halberd
Hailfrost Sword, Axe, Mace, Claymore, Greataxe, Hammer, Spear, Halberd, Knuckles, Bow, Dagger, Pistol, Shield, Chakram
Astral Sword, Axe, Mace, Claymore, Greataxe, Greatmace, Spear, Halberd, Knuckles, Bow, Shield, Dagger, Chakram, Pistol
Smoke Sword, Axe, Mace, Claymore, Greataxe, Hammer, Spear, Halberd, Knuckles, Bow, Shield
Calygrey, AncientCalygrey Mace, Staff
JadeLich Mace, Staff
Blood Blood Sword, Great BloodSword, BloodSpear

Staves and halberds share a weapon type in Outward, so each gets the other's particles: a staff gets a set's halberd particles and a halberd its staff particles (Calygrey, AncientCalygrey, JadeLich). Astral has both and uses its halberd's for both.

Unique weapons
Entry Weapon Entry Weapon
Brand Brand Grind Grind
Maelstrom Maelstrom Blade Shriek Shriek
Sunfall Sunfall Axe Frostburn Frostburn Staff
ButcherCleaver Butcher Cleaver Duty Duty
Skycrown Skycrown Mace Tokebakicit Tokebakicit
MaceOfSeasons Mace of Seasons Murmure Murmure
Starchild Starchild Claymore RedLady Red Lady's Dagger
Kelvin Kelvin's Greataxe Manticore Manticore Dagger
Challenger Challenger Greathammer FrozenChakram Frozen Chakram
Werlig Werlig Spear MysteriousChakram Mysterious Chakram
ThriceWrought Thrice-Wrought Halberd ExperimentalChakram Experimental Chakram
Dreamer Dreamer Halberd DistortedExperiment Distorted Experiment
GhostReaper Ghost Reaper GildedShiver Gilded Shiver of Tramontane
ScholarsStaff Scholar's Staff LionmanMace Lionman Electric Mace
MagesPokingStick Mage's Poking Stick LionmanStaff Lionman Frost Staff
Sandrose Sandrose GiantArchPriest Giant Arch Priest Blade Guandao
SealedMace Sealed Mace MilitiaAxe Militia Axe Josef
CruelPriest Scepter of the Cruel Priest MilitiaClaymore Militia Claymore
MilitiaSpear Militia Spear
  • Brand stops short of the tip on any blade, as the vanilla Brand does
  • ScholarsStaff and MagesPokingStick glow at the item's tip; on the vanilla staves the glow sits in the staff's head, below the end, which a plain staff does not have
Fit options
Value What it does
Auto The default: Stretch on a weapon of the entry's own type, Blade on any other
Stretch The source weapon's layout, stretched to the item's blade and centred on it. Anything that is not blade-shaped, such as a bow given a set's sword particles, gets Bounds instead
Original Exactly where the source weapon has them
Mesh Emits from the item's whole model, hilt included. Falls back to Bounds when the game does not let particles read the model
Bounds One box around the item's whole model
Blade Emits from the item's blade or head only, not its handle or guard. Falls back to Mesh on an item with no blade or head, such as a quarterstaff
Why you need this

A weapon's own particles are not VFX prefabs: they are built into its visual, the 3D model the game loads for it. No SL_PlayVFX name reaches them, and nothing in stock XML attaches anything to an item's visual.

This copies them from the vanilla weapon onto your item's visual and keeps them there as the item is drawn, sheathed, dropped or stored. The game reuses item visuals between copies of an item, so the copies are cleaned up whenever the visual changes hands.

Compatibility

  • Requires SideLoader
  • Mods built on Custom Effects must list it as a dependency, or SideLoader drops their XML
  • Tested in single player and in co-op; in co-op every player needs the mod installed

Thanks and credit

  • Thanks to my dear friend Rogismund, who sits through the most tedious co-op sessions hunting online bugs with me
  • Thanks to Sinai for SideLoader and for all their incredible work
  • Thanks to Joh-Benji and Proboina for all the help they have given me!
  • Thanks to the entire modding community that has tirelessly made (and is making!) my favourite game go from epic to legendary

Contacts

Thunderstore development is made possible with ads. Please consider making an exception to your adblock.