Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Source/CombatExtended/CombatExtended/BlackSmokeTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Verse;

namespace CombatExtended;

/// <summary>
/// Manages ticking for black smoke.
/// </summary>
public class BlackSmokeTracker(Map map) : MapComponent(map)
{
private readonly List<Smoke> _smoke = [];

public override void MapComponentTick()
{
Parallel.ForEach(_smoke, smoke => smoke.ParallelTick());

// Apply previously calculated smoke spread.
// This hopefully avoids destroying and recreating low-density smoke in the same cell within one tick.
for (int i = 0; i < _smoke.Count; i++)
{
_smoke[i].DoSpreadToAdjacentCells();
}
}

public void Register(Smoke smoke) => _smoke.Add(smoke);

public void Unregister(Smoke smoke) => _smoke.Remove(smoke);
}
250 changes: 157 additions & 93 deletions Source/CombatExtended/CombatExtended/Things/Smoke.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using CombatExtended.AI;
using RimWorld;
using UnityEngine;
Expand All @@ -8,14 +7,26 @@
namespace CombatExtended;
public class Smoke : Gas
{
public const int UpdateIntervalTicks = 30;
public const int UpdateIntervalTicks = 60;
private const float InhalationPerSec = 0.045f * UpdateIntervalTicks / GenTicks.TicksPerRealSecond;
private const float DensityDissipationThreshold = 3.0f;
private const float MinSpreadDensity = 1.0f; //ensures smoke clouds don't spread to infinitely small densities. Should be lower than DensityDissipationThreshold to avoid clouds stuck indoors.
private const float DensityDissipationThreshold = 30.0f;
private const float MinSpreadDensity = 10.0f; //ensures smoke clouds don't spread to infinitely small densities. Should be lower than DensityDissipationThreshold to avoid clouds stuck indoors.
private const float MaxDensity = 12800f;
private const float BugConfusePercent = 0.15f; // Percent of MaxDensity where bugs go into confused wander
private const float LethalAirPPM = 10000f; // Level of PPM where target severity hits 100% (about 2x the WHO/FDA immediately-dangerous-to-everyone threshold).

private struct DensityTransfer
{
public Smoke Target;
public IntVec3 Position;
public float Amount;
}

/// <summary>
/// List of pending density transfers to neighboring cells.
/// </summary>
private List<DensityTransfer> _transfers = [];

private float density;

private DangerTracker _dangerTracker = null;
Expand All @@ -41,52 +52,58 @@ public override string LabelNoCount
}
}

/// <summary>
/// Overridden to a fixed tick rate, this also avoids expensive checks for whether the smoke is in the viewport.
/// </summary>
public override int UpdateRateTicks => 15;

public override void SpawnSetup(Map map, bool respawningAfterLoad)
{
base.SpawnSetup(map, respawningAfterLoad);

// BlackSmokeTracker manages ticking for Smoke instances.
Map.GetComponent<BlackSmokeTracker>().Register(this);
}

public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish)
{
Map.GetComponent<BlackSmokeTracker>().Unregister(this);
base.DeSpawn(mode);
}

private bool CanMoveTo(IntVec3 pos)
{
return
pos.InBounds(Map)
&& (
!pos.Filled(Map)
|| (pos.GetDoor(Map)?.Open ?? false)
|| (pos.GetFirstThing<Building_Vent>(Map) is Building_Vent vent && vent.TryGetComp<CompFlickable>().SwitchIsOn)
);
Map map = Map;
if (!pos.InBounds(map))
{
return false;
}

Building edifice = pos.GetEdifice(map);
if (edifice?.def.Fillage != FillCategory.Full)
{
return true;
}

return edifice is Building_Door { Open: true } ||
edifice is Building_Vent vent && FlickUtility.WantsToBeOn(vent);
}

public override void TickInterval(int delta)
{
if (density > DensityDissipationThreshold) //very low density smoke clouds eventually dissipate on their own
{
destroyTick += delta;
if (Rand.Range(0, 10) == 5)
{
float d = density * 0.0001f * delta;
if (density > 300)
{
if (Rand.Range(0, (int)(MaxDensity)) < d)
{
FilthMaker.TryMakeFilth(Position, Map, ThingDefOf.Filth_Ash, 1, FilthSourceFlags.None);
}
}
density -= d;

}
}
float dissipation = Mathf.Max(0.001f, density * 0.00001f) * delta;

if (this.IsHashIntervalTick(UpdateIntervalTicks, delta))
{
if (!CanMoveTo(Position)) //cloud is in inaccessible cell, probably a recently closed door or vent. Spread to nearby cells and delete.
if (density > 300 && Rand.Range(0, (int)(MaxDensity)) < dissipation)
{
SpreadToAdjacentCells();
Destroy();
return;
FilthMaker.TryMakeFilth(Position, Map, ThingDefOf.Filth_Ash, 1, FilthSourceFlags.None);
}
if (!Position.Roofed(Map))
{
UpdateDensityBy(-60);
}
SpreadToAdjacentCells();
ApplyHediffs();

density -= dissipation;
}

if (this.IsHashIntervalTick(120, delta))
{
DangerTracker?.Notify_SmokeAt(Position, density / MaxDensity);
Expand All @@ -95,93 +112,140 @@ public override void TickInterval(int delta)
base.TickInterval(delta);
}

private void ApplyHediffs()
public void DoSpreadToAdjacentCells()
{
if (!Position.InBounds(Map))
if (this.IsHashIntervalTick(UpdateIntervalTicks))
{
return;
}
for (int i = 0; i < _transfers.Count; i++)
{
DensityTransfer transfer = _transfers[i];
Smoke target = transfer.Target ?? (Smoke)GenSpawn.Spawn(CE_ThingDefOf.Gas_BlackSmoke, transfer.Position, Map);
TransferDensityTo(target, transfer.Amount);
}

var pawns = Position.GetThingList(Map).Where(t => t is Pawn).ToList();
var baseTargetSeverity = Mathf.Pow(density / LethalAirPPM, 1.25f);
var baseSeverityRate = InhalationPerSec * density / MaxDensity;
_transfers.Clear();
}
}

foreach (Pawn pawn in pawns)
public void ParallelTick()
{
if (this.IsHashIntervalTick(UpdateIntervalTicks))
{
if (pawn.RaceProps.FleshType == FleshTypeDefOf.Insectoid)
if (!CanMoveTo(Position)) //cloud is in inaccessible cell, probably a recently closed door or vent. Spread to nearby cells and delete.
{
if (density > MaxDensity * BugConfusePercent)
{
pawn.mindState.mentalStateHandler.TryStartMentalState(CE_MentalStateDefOf.WanderConfused);
}
continue;
destroyTick = 0;
}
if (pawn.RaceProps.Humanlike && !pawn.IsSubhuman)
if (!Position.Roofed(Map))
{
pawn.TryGetComp<CompTacticalManager>()?.GetTacticalComp<CompGasMask>()?.Notify_ShouldEquipGasMask(false);
UpdateDensityBy(-60);
}
var sensitivity = pawn.GetStatValue(CE_StatDefOf.SmokeSensitivity);
var breathing = PawnCapacityUtility.CalculateCapacityLevel(pawn.health.hediffSet, PawnCapacityDefOf.Breathing);
float curSeverity = pawn.health.hediffSet.GetFirstHediffOfDef(CE_HediffDefOf.SmokeInhalation, false)?.Severity ?? 0f;
CalcSpreadToAdjacentCells();
}
}

public void ApplyHediffs(Pawn pawn)
{
var baseTargetSeverity = Mathf.Pow(density / LethalAirPPM, 1.25f);
var baseSeverityRate = InhalationPerSec * density / MaxDensity;

if (breathing < 0.01f)
{
breathing = 0.01f;
}
var targetSeverity = sensitivity / breathing * baseTargetSeverity;
if (targetSeverity > 1.5f)
if (pawn.RaceProps.FleshType == FleshTypeDefOf.Insectoid)
{
if (density > MaxDensity * BugConfusePercent)
{
targetSeverity = 1.5f;
pawn.mindState.mentalStateHandler.TryStartMentalState(CE_MentalStateDefOf.WanderConfused);
}

var severityDelta = targetSeverity - curSeverity;
return;
}

bool downed = pawn.Downed;
bool awake = pawn.Awake();
if (pawn.RaceProps.Humanlike && !pawn.IsSubhuman)
{
pawn.TryGetComp<CompTacticalManager>()?.GetTacticalComp<CompGasMask>()?.Notify_ShouldEquipGasMask(false);
}
var sensitivity = pawn.GetStatValue(CE_StatDefOf.SmokeSensitivity);
var breathing = PawnCapacityUtility.CalculateCapacityLevel(pawn.health.hediffSet, PawnCapacityDefOf.Breathing);
float curSeverity = pawn.health.hediffSet.GetFirstHediffOfDef(CE_HediffDefOf.SmokeInhalation, false)?.Severity ?? 0f;


var severityRate = baseSeverityRate * sensitivity / breathing * Mathf.Pow(severityDelta, 1.5f);
if (breathing < 0.01f)
{
breathing = 0.01f;
}
var targetSeverity = sensitivity / breathing * baseTargetSeverity;
if (targetSeverity > 1.5f)
{
targetSeverity = 1.5f;
}

if (downed)
{
severityRate /= 100;
}
var severityDelta = targetSeverity - curSeverity;

if (!awake)
{
severityRate /= 2;
if (curSeverity > 0.1)
{
RestUtility.WakeUp(pawn);
}
}
bool downed = pawn.Downed;
bool awake = pawn.Awake();

if (severityRate > 0 && severityDelta > 0)

var severityRate = baseSeverityRate * sensitivity / breathing * Mathf.Pow(severityDelta, 1.5f);

if (downed)
{
severityRate /= 100;
}

if (!awake)
{
severityRate /= 2;
if (curSeverity > 0.1)
{
HealthUtility.AdjustSeverity(pawn, CE_HediffDefOf.SmokeInhalation, severityRate);
RestUtility.WakeUp(pawn);
}
}

if (severityRate > 0 && severityDelta > 0)
{
HealthUtility.AdjustSeverity(pawn, CE_HediffDefOf.SmokeInhalation, severityRate);
}
}

private void SpreadToAdjacentCells()
private void CalcSpreadToAdjacentCells()
{
if (density >= MinSpreadDensity)
if (density < MinSpreadDensity)
{
return;
}

Map map = Map;
IntVec3 position = Position;
float curDensity = density;

foreach (IntVec3 cardinal in GenAdj.CardinalDirections.InRandomOrder())
{
var freeCells = GenAdjFast.AdjacentCellsCardinal(Position).InRandomOrder().Where(CanMoveTo).ToList();
foreach (var freeCell in freeCells)
IntVec3 freeCell = position + cardinal;

if (!CanMoveTo(freeCell))
{
if (freeCell.GetGas(Map) is Smoke existingSmoke)
continue;
}

float transferred;
if (freeCell.GetGas(map) is Smoke existingSmoke)
{
transferred = (curDensity - existingSmoke.density) / 2;
_transfers.Add(new DensityTransfer
{
var densityDiff = this.density - existingSmoke.density;
TransferDensityTo(existingSmoke, densityDiff / 2);
}
else
Target = existingSmoke,
Amount = transferred
});
}
else
{
transferred = curDensity / 2;
_transfers.Add(new DensityTransfer
{
var newSmokeCloud = (Smoke)GenSpawn.Spawn(CE_ThingDefOf.Gas_BlackSmoke, freeCell, Map);
TransferDensityTo(newSmokeCloud, this.density / 2);
}
Position = freeCell,
Amount = transferred
});
}

curDensity -= transferred;
}
}

Expand Down
2 changes: 1 addition & 1 deletion Source/CombatExtended/Harmony/Harmony_Fire.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal static void Postfix(Fire __instance)
[HarmonyPatch(typeof(Fire), "Tick")]
internal static class Harmony_Fire_Tick
{
private const float SmokeDensityPerInterval = 900f;
private const int SmokeDensityPerInterval = 1800;

internal static void Postfix(Fire __instance)
{
Expand Down
Loading