Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

[WIP] Realistic Viscous Radiator Fan Clutch Dynamics, Dynamic RPM Audio & Dual-Mode Failure Simulation

オープン
#216 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
50/100
issue の種類
機能追加
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
lua
領域
game-dev

調査の方向性

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 の本文から書いたものです。

説明

enhancement

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 of fanNoice.ogg with 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% to 100%, default 80%).
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 (brokenFanModifier drops 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 to settings.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 the Engine Temp debug block.
  • Console Command: Integrated into ads_getDebugVehicleInfo.
  • Persistence: #fanClutchFailureMode and #viscousFanEngagement are registered in schemaSavegame and 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 を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

id577/FS25_AdvancedDamageSystem のほかの issue

id577/FS25_AdvancedDamageSystem の issue をすべて見る

似ている issue

Lua の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。