[WIP] Realistic Viscous Radiator Fan Clutch Dynamics, Dynamic RPM Audio & Dual-Mode Failure Simulation
まだ誰も着手していません。
評価
調査の方向性
The implementation touches four Lua files: scripts/core/ADS_Breakdowns.lua for failure mode determination, scripts/core/ADS_Thermal.lua for viscous fan engagement calculation, scripts/core/ADS_Specialization.lua for sound updates and savegame persistence, and gui/ADS_SettingsPage.lua for the new settings options. Start by reading the existing fanClutchHealth handling and fanNoice sample usage. 'Done' means the new fan clutch dynamics, dual-mode failures, dynamic audio, and localization strings are merged and tested.
索引モデルが issue の本文から書いたものです。
説明
Status: Implemented on top of current ADS codebase; currently in active in-game testing.
Hi @id577 and @Squallqt!
ADS already has a great foundation with spec.fanClutchHealth, cooling interaction, and the fanNoice.ogg sample.
I decided to expand upon your existing implementation to bring even more realism to the fan clutch in three key areas:
1. Dynamic Rev-Matched Fan Roar (Thermal Engagement)
Currently, fanNoice is mainly triggered as a static-pitch defect sound.
- In this update, as engine coolant temperature climbs under heavy field draft (between 85°C and 94°C), the fan clutch engages progressively with hydraulic slew-rate smoothing.
- While engaged,
updateViscousFanSound(dt)dynamically modulates both the volume and pitch offanNoice.oggwith actual engine RPM (getLastModulatedMotorRpm()). - You get that iconic heavy-diesel fan roar when pulling hard on the field, which winds down naturally as the engine cools or idles.
- Added a volume slider in the Thermal Settings page (
Off,10%to100%, default80%).
2. Dual-Mode Failure Realism (Configurable in Settings Menu)
Instead of a single generic defect behavior, when FAN_CLUTCH_FAILURE triggers, it rolls one of the two real-world failure modes:
- Mode 2 — Slipping ("Solta / Desengatada"): The clutch loses silicone fluid or the bimetal spring fatigues. It fails to lock up under heat, stays silent, and causes the engine to overheat rapidly during low-speed field work (
brokenFanModifierdrops to 0.30). - Mode 1 — Seized / Locked Solid ("Agarrada / Bloqueada"): Mechanical bearing seizure. The fan is permanently locked 1:1 with engine RPM, roaring aggressively all the time (even cold), providing continuous forced airflow, with a slight parasitic load (+3%).
- Mode 0: Normal operation (cleared upon repair).
- Player-Configurable Probability: Added an option under Thermal Settings (
Fan Clutch Breakdown Behavior) with default 70% / 30% (adjustable from 100/0 to 0/100), synced across multiplayer and persisted tosettings.xml.
3. Localization & ADS Architecture Integration
- Full Localization: Added translation strings for all new options across all 15 languages supported by ADS (
en,pt,br,de,fr,es,it,pl,ru,uk,cz,hu,tr,fi,kr). - HUD Debug: Displays
Fan: % | Clutch: % | Mode [NORM/LOCKED/SLIP] | Vol: %under theEngine Tempdebug block. - Console Command: Integrated into
ads_getDebugVehicleInfo. - Persistence:
#fanClutchFailureModeand#viscousFanEngagementare registered inschemaSavegameand saved/loaded seamlessly.
View Implementation Diffs (Click to expand)
scripts/core/ADS_Breakdowns.lua
--- a/scripts/core/ADS_Breakdowns.lua
+++ b/scripts/core/ADS_Breakdowns.lua
@@ -4617,11 +4617,23 @@
ADS_Breakdowns.EffectApplicators.FAN_CLUTCH_MODIFIER = {
apply = function(vehicle, effectData, handler)
log_dbg("Applying FAN_CLUTCH_MODIFIER:", effectData.value)
local spec = vehicle.spec_AdvancedDamageSystem
spec.fanClutchHealth = math.max(1.0 + effectData.value, 0.1)
+
+ -- Determine dual-mode failure (0 = normal, 1 = locked, 2 = slipping)
+ if spec.fanClutchFailureMode == nil or spec.fanClutchFailureMode == 0 then
+ local lockedChance = (ADS_Config.THERMAL and ADS_Config.THERMAL.VISCOUS_FAN_MODE_LOCKED_CHANCE) or 0.30
+ spec.fanClutchFailureMode = (math.random() <= lockedChance) and 1 or 2
+ log_dbg(string.format("FAN_CLUTCH_MODIFIER mode rolled: %d (%s)",
+ spec.fanClutchFailureMode,
+ spec.fanClutchFailureMode == 1 and "LOCKED" or "SLIPPING"
+ ))
+ end
end,
remove = function(vehicle, handler)
log_dbg("Removing FAN_CLUTCH_MODIFIER effect.")
local spec = vehicle.spec_AdvancedDamageSystem
spec.fanClutchHealth = 1.0
+ spec.fanClutchFailureMode = 0
end
}
scripts/core/ADS_Thermal.lua
--- a/scripts/core/ADS_Thermal.lua
+++ b/scripts/core/ADS_Thermal.lua
@@ -216,6 +216,36 @@
end
+ -- --- Viscous / Electro-Viscous Fan Clutch Simulation ---
+ if isMotorStarted then
+ local targetEngagement = 0.0
+
+ if spec.fanClutchFailureMode == 1 then
+ targetEngagement = 1.0 -- Locked solid
+ elseif spec.fanClutchFailureMode == 2 then
+ targetEngagement = 0.03 * sanitizeNumber(spec.fanClutchHealth, 1.0, 0, 1) -- Slipping
+ else
+ local engageStartTemp = (C.VISCOUS_FAN_ENGAGE_START_TEMP or 85.0)
+ local engageFullTemp = (C.VISCOUS_FAN_ENGAGE_FULL_TEMP or 94.0)
+ targetEngagement = math.clamp((rawEngineTemp - engageStartTemp) / (engageFullTemp - engageStartTemp), 0.0, 1.0)
+
+ if spec.thermostatState and spec.thermostatState > 0.80 then
+ targetEngagement = math.max(targetEngagement, (spec.thermostatState - 0.80) * 2.5)
+ end
+
+ local fanClutchHealth = sanitizeNumber(spec.fanClutchHealth, 1.0, 0, 1)
+ targetEngagement = targetEngagement * fanClutchHealth
+ end
+
+ -- Viscous fluid has hydraulic inertia (~2.5s to fully engage or disengage)
+ local slewRate = (safeDt > 0 and (safeDt / 2500)) or 0.05
+ spec.viscousFanEngagement = (spec.viscousFanEngagement or 0.0) + (targetEngagement - (spec.viscousFanEngagement or 0.0)) * math.clamp(slewRate, 0.0, 1.0)
+ else
+ spec.viscousFanEngagement = 0.0
+ end
+
dbg.totalHeat = heat
dbg.totalCooling = cooling
dbg.radiatorCooling = radiatorCooling
scripts/core/ADS_Specialization.lua
--- a/scripts/core/ADS_Specialization.lua
+++ b/scripts/core/ADS_Specialization.lua
@@ -1041,2 +1041,4 @@
schemaSavegame:register(XMLValueType.FLOAT, baseKey .. "#transmissionThermostatState", "Transmission Thermostat Position")
+ schemaSavegame:register(XMLValueType.INT, baseKey .. "#fanClutchFailureMode", "Fan Clutch Failure Mode (0=Normal, 1=Locked, 2=Slipping)")
+ schemaSavegame:register(XMLValueType.FLOAT, baseKey .. "#viscousFanEngagement", "Viscous Fan Clutch Engagement")
@@ -1193,4 +1195,5 @@
SpecializationUtil.registerFunction(vehicleType, "updateTransmissionThermalModel", ADS_Thermal.updateTransmissionThermalModel)
SpecializationUtil.registerFunction(vehicleType, "getSmoothedTemperature", ADS_Thermal.getSmoothedTemperature)
+ SpecializationUtil.registerFunction(vehicleType, "updateViscousFanSound", AdvancedDamageSystem.updateViscousFanSound)
@@ -1645,2 +1648,4 @@
xmlFile:setValue(key .. "#transmissionThermostatState", AdvancedDamageSystem.sanitizeNumber(spec.transmissionThermostatState, 0.0, 0.0, 1.0))
+ xmlFile:setValue(key .. "#fanClutchFailureMode", tonumber(spec.fanClutchFailureMode) or 0)
+ xmlFile:setValue(key .. "#viscousFanEngagement", AdvancedDamageSystem.sanitizeNumber(spec.viscousFanEngagement, 0.0, 0.0, 1.0))
@@ -1854,2 +1859,4 @@
self.spec_AdvancedDamageSystem.fanClutchHealth = 1.0
+ self.spec_AdvancedDamageSystem.fanClutchFailureMode = 0
+ self.spec_AdvancedDamageSystem.viscousFanEngagement = 0.0
@@ -4523,4 +4534,63 @@
+function AdvancedDamageSystem:updateViscousFanSound(dt)
+ local spec = self.spec_AdvancedDamageSystem
+ if spec == nil or spec.samples == nil then return end
+ local sample = spec.samples.fanNoice
+ if sample == nil then return end
+
+ if spec.fanClutchFailureMode == 2 then
+ if g_soundManager:getIsSamplePlaying(sample) then
+ g_soundManager:stopSample(sample, 0, 0)
+ end
+ return
+ end
+
+ local motor = self:getMotor()
+ if motor == nil or not self:getIsMotorStarted() then
+ if g_soundManager:getIsSamplePlaying(sample) then
+ g_soundManager:stopSample(sample, 0, 0)
+ end
+ return
+ end
+
+ local configVolume = (ADS_Config.THERMAL and ADS_Config.THERMAL.VISCOUS_FAN_VOLUME) or 0.80
+ local engagement = spec.viscousFanEngagement or 0.0
+
+ if configVolume <= 0.01 or engagement <= 0.02 then
+ if g_soundManager:getIsSamplePlaying(sample) then
+ g_soundManager:stopSample(sample, 0, 0)
+ end
+ return
+ end
+
+ local minRpm = tonumber(motor.minRpm) or 800
+ local maxRpm = math.max(tonumber(motor.maxRpm) or 2200, minRpm + 1)
+ local curRpm = (motor.getLastModulatedMotorRpm ~= nil and tonumber(motor:getLastModulatedMotorRpm())) or tonumber(motor.lastMotorRpm) or minRpm
+ local rpmN = math.clamp((curRpm - minRpm) / (maxRpm - minRpm), 0, 1)
+
+ local baseVolume = configVolume * engagement * (0.25 + 0.75 * (rpmN ^ 1.15))
+ local pitchOffset = (0.35 * (rpmN ^ 1.20)) * engagement
+
+ sample.loops = 0
+ sample.volumeScale = (sample.adsOriginalVolumeScale or sample.volumeScale) * baseVolume
+
+ if not g_soundManager:getIsSamplePlaying(sample) then
+ g_soundManager:playSample(sample)
+ end
+
+ g_soundManager:setSamplePitchOffset(sample, pitchOffset)
+ g_soundManager:setSampleVolumeOffset(sample, 0)
+end
@@ -4590,2 +4660,3 @@
ADS_ThrottleControl.update(self)
+ self:updateViscousFanSound(dt)
gui/ADS_SettingsPage.lua
--- a/gui/ADS_SettingsPage.lua
+++ b/gui/ADS_SettingsPage.lua
@@ -54,4 +54,6 @@
maxDirtInfluence = ADS_Config.THERMAL.MAX_DIRT_INFLUENCE,
+ viscousFanVolume = ADS_Config.THERMAL.VISCOUS_FAN_VOLUME,
+ viscousFanModeLockedChance = ADS_Config.THERMAL.VISCOUS_FAN_MODE_LOCKED_CHANCE,
warmingBoostPower = ADS_Config.THERMAL.WARMING_BOOST_POWER,
@@ -579,4 +581,18 @@
page.ads_viscousFanVolume = ADS_InGameSettings:addMultiTextOption(
page,
"onViscousFanVolumeChanged",
ADS_InGameSettings.steps.viscousFanVolume.texts,
g_i18n:getText("ads_viscousFanVolume_label"),
g_i18n:getText("ads_viscousFanVolume_tooltip")
)
+ page.ads_viscousFanModeLockedChance = ADS_InGameSettings:addMultiTextOption(
+ page,
+ "onViscousFanModeLockedChanceChanged",
+ ADS_InGameSettings.steps.viscousFanModeLockedChance.texts,
+ g_i18n:getText("ads_viscousFanModeLockedChance_label"),
+ g_i18n:getText("ads_viscousFanModeLockedChance_tooltip")
+ )
- 主要言語
- Lua
- スター
- 282
- フォーク
- 17
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
id577/FS25_AdvancedDamageSystem のほかの issue
-
question
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
id577/FS25_AdvancedDamageSystem#238 · コメント 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 73/100
id577/FS25_AdvancedDamageSystem#197 · リアクション 1 件 ·
-
Shop no longer opens with ADS version 9.2.8 with Powertrain Upgrade mod and Capacity Upgrade modオープン
難易度 3/5 1〜2日 初心者へのやさしさ 48/100
id577/FS25_AdvancedDamageSystem#243 · コメント 3 件 ·
-
bug
難易度 3/5 1〜2日 初心者へのやさしさ 56/100
id577/FS25_AdvancedDamageSystem#241 · コメント 1 件 ·
-
question
難易度 3/5 1〜2日 初心者へのやさしさ 48/100
id577/FS25_AdvancedDamageSystem#237 · コメント 1 件 ·
id577/FS25_AdvancedDamageSystem の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
scp-fs2open/fs2open.github.com#7946 ·
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1〜3時間 初心者へのやさしさ 88/100
-
bug code-annotation
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
quarto-dev/quarto-cli#15215 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
CopilotC-Nvim/CopilotChat.nvim#1588 · コメント 3 件 ·