[WIP] Realistic Viscous Radiator Fan Clutch Dynamics, Dynamic RPM Audio & Dual-Mode Failure Simulation
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 50/100
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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")
+ )
- Ngôn ngữ chính
- Lua
- Star
- 282
- Fork
- 17
- Chỉ số merge pull request
- Không có pull request nào được merge trong 30 ngày
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của id577/FS25_AdvancedDamageSystem
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
id577/FS25_AdvancedDamageSystem#238 · 1 bình luận ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 73/100
id577/FS25_AdvancedDamageSystem#197 · 1 reaction ·
-
Shop no longer opens with ADS version 9.2.8 with Powertrain Upgrade mod and Capacity Upgrade modĐang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 48/100
id577/FS25_AdvancedDamageSystem#243 · 1 bình luận ·
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 56/100
-
Issue or not?Đang mởquestion
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 54/100
id577/FS25_AdvancedDamageSystem#240 · 5 bình luận ·
Tất cả issue của id577/FS25_AdvancedDamageSystem
Issue tương tự
-
enhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
-
token validation accepts numeric aud claims instead of rejecting unsupported typesCó thể đã có người làm @GioeleB00 đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
zmartzone/lua-resty-openidc#577 ·
-
test-net.lua fails on linuxCó thể đã có người làm @PatrickDahlin đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
rcarriga/nvim-dap-ui#490 ·