The game has an effect system, which can be used to cause changes to other objects from a source. This system is used for things like:
Effects are composed of three parts, a range, a condition, and a type. They also optionally have a representation. These are used to control how the effect works, and when, and each should be a component implementing the respective interface:
IEffectRange
- the range of the effect, determining which other things should be effected by this effect. Inbuilt examples include CEffectRangeGlobal
, CEffectRangeTiles
, and CEffectRangeSelf
IEffectCondition
- the conditions that determine when the effect should be active (on a per-effect basis, not a per-target basis). Inbuild examples include CEffectAlways
, CEffectWhileBeingUsed
, and CEffectAtNight
IEffectType
- the consequence of the effect being applied, i.e. what this effect applies to things in range when the condition is active. Inbuilt examples include CCabinetModifier
, CTableModifier
, and CApplianceSpeedModifier
IEffectRepresentation
- this is optional and specifies the icon and text to use when displaying this effect, such as when Hobs are placed next to tablesThese are some examples of the effects as used in the game
Example | Range | Condition | Type |
---|---|---|---|
Hob | CEffectRangeTiles |
CEffectAlways |
CTableModifier |
Sink | CEffectRangeTiles |
CEffectAlways |
CTableModifier |
Candles | CEffectRangeTiles |
CEffectAtNight |
CTableModifier |
Gas Override | CEffectRangeDirectional |
CEffectWhileBeingUsed |
CApplianceSpeedModifier |
All You Can Eat | n/a (CEffectGlobal ) |
CEffectAlways |
CTableModifier |
Specifying an effect depends on what kind of object you are defining the effect on.
For Appliances, you can specify the effect properties directly as EffectRange
, EffectCondition
and EffectType
. These will then be attached to any new copies of the appliance.
Items can't apply effects directly but can apply effects when attached to tables. To do so, give your Item the property CEffectCreator
and specify the Effect, which is a GameDataObject that specifies the properties of your effect.
Unlocks can apply effects when selected; this is mainly used to apply effects to tables for customer-affecting cards. Because the source of these is the Unlock itself, the range is always effectively CEffectRangeGlobal
. You can specify an effect for a card by adding a GlobalEffect to the Unlock's list of Effects
You can also create custom effects. For any new implementation you should create a new struct that implements the correct interface and IModComponent
.
The game uses a set of system groups to work out and apply effects. This has three stages, Activate, Determine, and Apply. When writing systems for these, it is important to put your system in the corresponding group so that it runs at the correct point in the update cycle.
Things that create effects have the component CAppliesEffect
; things that can receive effects have the (buffer) component CAffectedBy
.
In the Activate stage, all effects are switched off and systems should enable effects that should be active by setting CAppliesEffect.IsActive
to true. In the Determine stage, all entities have their CAffectBy
buffer cleared and systems should add any affects that should be effecting the entity to this buffer.
In the Apply stage, systems should use the CAffectedBy
buffer to apply effects to the entities for which the effect is Active.
Because of a quirk in the way these are attached, you currently have to manually attach your properties to appliances and items.
As a temporary workaround, you can use Harmony to patch into EffectHelpers.AddApplianceEffectComponents and EffectHelpers.AddAttachedEffectComponents and manually attach the correct component. This is because Unity requires a compile-time reference to your component in order to include it, so you must explicitly attach the component through a non-generic call.
Custom ranges should implement IEffectRange
. You can specify any parameters for your range in your component. For example, if we wanted to implement an effect that applied only to things further than X tiles away, we could implement our Range:
public struct CEffectRangeFarAway : IEffectRange, IModComponent {
public float MinimumDistance;
}
We should then create a system which uses this data to enable or disable the effect. This system should inherit from GameEffectSystemBase
(this means it'll only run when the game wants to update effects).
The game checks which objects are affected by which effects in the Determine stage of calculating effects, so we also want to make sure this system is updating in this stage, so we should mark it [UpdateInGroup(typeof(DetermineEffectsGroup))]
.
In the system, we should loop over candidate entities and add the source entity into the buffers of entities we want to affect:
[UpdateInGroup(typeof(DetermineEffectsGroup))]
public class DetermineFarAway : GameEffectSystemBase {
private EntityQuery EffectAppliers;
private EntityQuery EffectTargets;
protected override void Initialise() {
base.Initialise();
// create two entity queries; one for the sources of our effects ...
EffectAppliers = GetEntityQuery(
typeof(CAppliesEffect),
typeof(CEffectRangeFarAway),
typeof(CPosition)
);
// ... and one for the targets
EffectTargets = GetEntityQuery(
typeof(CAffectedBy),
typeof(CPosition)
);
}
protected override void OnUpdate() {
using var effectors = EffectAppliers.ToEntityArray(Allocator.Temp);
using var targets = EffectTargets.ToEntityArray(Allocator.Temp);
foreach (var target in targets) {
// look up the components we want from the target
if(!RequireBuffer(target, out DynamicBuffer<CAffectedBy> affected_by)) continue;
if(!Require(target, out CPosition position)) continue;
if(!Require(target, out CEffectRangeFarAway effect_data)) continue;
foreach (var effector in effectors) {
// perform the check, in this case we want tile-based distance
var dist = (position - GetComponent<CPosition>(effector).Position).Chebyshev();
// if the distance is too little, do nothing
if(dist < effect_data.MinimumDistance + 0.25f) continue;
// otherwise add the effector to the buffer
affected_by.Add(effector);
}
}
}
}
Custom conditions should implement IEffectCondition
. For example, if we wanted a condition that was only active when there were more than 2 players:
public struct CEffectPlayerCount : IEffectCondition, IModComponent {
public int MinimumPlayers;
}
We also have to implement the corresponding system in the Activate stage, by marking it [UpdateInGroup(typeof(ActivateEffectsGroup))]
. Because this might change often, we will inherit from GameSystemBase
so that the effect is switched on or off as soon as a player leaves or joins over the limit.
This system should loop over all entities which have your condition and set them active if necessary. If you don't set them active, they will default to being inactive.
[UpdateInGroup(typeof(ActivateEffectsGroup))]
public class DeterminePlayerCount : GameSystemBase {
private EntityQuery EffectAppliers;
private EntityQuery Players;
protected override void Initialise() {
base.Initialise();
EffectAppliers = GetEntityQuery(
typeof(CAppliesEffect),
typeof(CEffectPlayerCount),
typeof(CPosition)
);
Players = GetEntityQuery(
typeof(CPlayer)
);
}
protected override void OnUpdate() {
var player_count = Players.CalculateEntityCount();
using var effectors = EffectAppliers.ToEntityArray(Allocator.Temp);
foreach (var effector in effectors) {
if(!Require(effector, out CAppliesEffect applies_effect)) continue;
if(!Require(effector, out CEffectPlayerCount condition)) continue;
applies_effect.IsActive = player_count >= condition.MinimumPlayers;
// applies_effect is a struct so we have to set it back explicitly
Set(effector, applies_effect);
}
}
}
Custom effect types should implement IEffectType
. For example, we could have an effect which sets tables on fire randomly.
public struct CEffectSetOnFire : IEffectType, IModComponent {
public float ProbabilityPerSecond;
}
The system for this effect should loop over each entity with a CAffectBy
buffer and find relevant, active effects, then apply the consequences accordingly.
[UpdateInGroup(typeof(DetermineEffectsGroup))]
public class ApplySetOnFire : GameEffectSystemBase {
private EntityQuery EffectTargets;
protected override void Initialise() {
base.Initialise();
EffectTargets = GetEntityQuery(
typeof(CAffectedBy),
typeof(CPosition),
typeof(CApplianceTable)
);
}
protected override void OnUpdate() {
using var targets = EffectTargets.ToEntityArray(Allocator.Temp);
var dt = Time.DeltaTime;
foreach (var target in targets) {
if(!RequireBuffer(target, out DynamicBuffer<CAffectedBy> affected_by)) continue;
foreach (var effector in affected_by) {
// check the effect is active and is the correct type
if(!Require(effector, out CAppliesEffect eff) || !eff.IsActive) continue;
if(!Require(effector, out CEffectSetOnFire set_on_fire)) continue;
// now apply the effect
if (Random.value < dt * set_on_fire.ProbabilityPerSecond) {
Set<CIsOnFire>(target);
break;
}
}
}
}
}