-
-
Notifications
You must be signed in to change notification settings - Fork 10
add: Система зависимостей от алкоголя, никотина и наркотиков, трайты и лечение #419
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ultradyper
wants to merge
2
commits into
ss14-ganimed:master
Choose a base branch
from
ultradyper:add/addictions
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
141 changes: 141 additions & 0 deletions
141
Content.Server/_Ganimed/Addiction/AddictionSymptomsSystem.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| // SPDX-FileCopyrightText: 2026 ultradyper <ultradyper@users.noreply.github.com> | ||
| // | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
|
|
||
| using Content.Shared._Ganimed.Addiction; | ||
| using Content.Shared.Jittering; | ||
| using Content.Shared.StatusEffectNew; | ||
| using Robust.Shared.Timing; | ||
|
|
||
| namespace Content.Server._Ganimed.Addiction; | ||
|
|
||
| /// <summary> | ||
| /// Применяет симптомы ломки (дрожь, косноязычие, слабость, галлюцинации). | ||
| /// Слушает AddictionSymptomsChangedEvent от AddictionSystem и пересчитывает симптомы | ||
| /// по всем каналам: доза по одному каналу не снимает симптомы другого. | ||
| /// Продлевает симптомы по таймеру, пока идёт ломка. | ||
| /// </summary> | ||
| public sealed partial class AddictionSymptomsSystem : EntitySystem | ||
| { | ||
| [Dependency] private readonly IGameTiming _timing = default!; | ||
| [Dependency] private readonly SharedJitteringSystem _jitter = default!; | ||
| [Dependency] private readonly StatusEffectsSystem _status = default!; | ||
|
|
||
| public override void Initialize() | ||
| { | ||
| base.Initialize(); | ||
|
|
||
| // Directed-подписка: событие рейзится на сущность (RaiseLocalEvent без broadcast), | ||
| // broadcast-подписчики в этом форке такое не получают (см. грабли #21). | ||
| SubscribeLocalEvent<AddictionComponent, AddictionSymptomsChangedEvent>(OnSymptomsChanged); | ||
| } | ||
|
|
||
| public override void Update(float frameTime) | ||
| { | ||
| base.Update(frameTime); | ||
|
|
||
| var query = EntityQueryEnumerator<AddictionComponent>(); | ||
| while (query.MoveNext(out var uid, out var comp)) | ||
| { | ||
| var anyWithdrawal = false; | ||
| var due = false; | ||
| foreach (var channel in comp.Channels) | ||
| { | ||
| if (!channel.InWithdrawal) | ||
| continue; | ||
|
|
||
| anyWithdrawal = true; | ||
| if (_timing.CurTime >= channel.NextSymptomsTime) | ||
| due = true; | ||
| } | ||
|
|
||
| if (!anyWithdrawal || !due) | ||
| continue; | ||
|
|
||
| RefreshSymptoms(uid, comp); | ||
|
|
||
| foreach (var channel in comp.Channels) | ||
| { | ||
| if (channel.InWithdrawal) | ||
| channel.NextSymptomsTime = _timing.CurTime + comp.SymptomRefreshInterval; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void OnSymptomsChanged(EntityUid uid, AddictionComponent comp, ref AddictionSymptomsChangedEvent args) | ||
| { | ||
| RefreshSymptoms(uid, comp); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Пересчитывает симптомы по всем каналам: применяет нужные, убирает лишние. | ||
| /// Stutter/Slurred/Rainbow не снимает: эти эффекты могут висеть от других источников | ||
| /// (алкоголь, ЛСД, THC), они истекают сами. Снимаются только свои: слабость | ||
| /// (уникальный прототип) и дрожь. | ||
| /// </summary> | ||
| private void RefreshSymptoms(EntityUid uid, AddictionComponent comp) | ||
| { | ||
| var anyWithdrawal = false; | ||
| var maxStage = 0; | ||
| var wantSlurred = false; | ||
| var wantStutter = false; | ||
| var wantWeakness = false; | ||
| var wantRainbow = false; | ||
|
|
||
| foreach (var channel in comp.Channels) | ||
| { | ||
| if (!channel.InWithdrawal) | ||
| continue; | ||
|
|
||
| anyWithdrawal = true; | ||
| maxStage = Math.Max(maxStage, channel.Stage); | ||
|
|
||
| if (channel.Stage >= 1) | ||
| { | ||
| if (channel.Kind == AddictionKind.Alcohol) | ||
| wantSlurred = true; | ||
| else | ||
| wantStutter = true; | ||
| } | ||
|
|
||
| if (channel.Stage >= 2) | ||
| { | ||
| wantWeakness = true; | ||
| if (channel.Kind == AddictionKind.Drug) | ||
| wantRainbow = true; | ||
| } | ||
| } | ||
|
|
||
| // Дрожь - косметика на любой стадии, амплитуда по самой тяжёлой. | ||
| // refresh: true, чтобы время не копилось при повторных вызовах. | ||
| if (anyWithdrawal) | ||
| { | ||
| var amplitude = maxStage switch | ||
| { | ||
| 0 => comp.MildJitterAmplitude, | ||
| 1 => comp.MediumJitterAmplitude, | ||
| _ => comp.SevereJitterAmplitude, | ||
| }; | ||
| _jitter.DoJitter(uid, comp.SymptomDuration, refresh: true, amplitude, comp.JitterFrequency); | ||
| } | ||
| else | ||
| { | ||
| RemComp<JitteringComponent>(uid); | ||
| } | ||
|
|
||
| if (wantSlurred) | ||
| _status.TrySetStatusEffectDuration(uid, comp.SlurredEffect, comp.SymptomDuration); | ||
|
|
||
| if (wantStutter) | ||
| _status.TrySetStatusEffectDuration(uid, comp.StutterEffect, comp.SymptomDuration); | ||
|
|
||
| if (wantWeakness) | ||
| _status.TrySetStatusEffectDuration(uid, comp.WeaknessEffect, comp.SymptomDuration); | ||
| else | ||
| _status.TryRemoveStatusEffect(uid, comp.WeaknessEffect); | ||
|
|
||
| if (wantRainbow) | ||
| _status.TrySetStatusEffectDuration(uid, comp.RainbowEffect, comp.SymptomDuration); | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| // SPDX-FileCopyrightText: 2026 ultradyper <ultradyper@users.noreply.github.com> | ||
| // | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
|
|
||
| using System.Linq; | ||
| using Content.Server.Body.Systems; | ||
| using Content.Server.Popups; | ||
| using Content.Server.Traits; | ||
| using Content.Shared.GameTicking; | ||
| using Content.Shared._Ganimed.Addiction; | ||
| using Content.Shared.Chemistry.Reagent; | ||
| using Content.Shared.Mobs.Systems; | ||
| using Robust.Shared.Prototypes; | ||
| using Robust.Shared.Timing; | ||
|
|
||
| namespace Content.Server._Ganimed.Addiction; | ||
|
|
||
| /// <summary> | ||
| /// Система зависимости: ловит приём доз (GetReagentEffectsEvent), копит уровень привыкания, | ||
| /// при долгом воздержании запускает ломку и рейзит AddictionSymptomsChangedEvent, | ||
| /// чтобы симптомы применила AddictionSymptomsSystem. | ||
| /// Компонент есть у всех игроков со спавна, канал зависимости появляется | ||
| /// при первом употреблении (подсесть может каждый), трайты дают стартовую | ||
| /// зависимость с высоким уровнем. | ||
| /// </summary> | ||
| public sealed partial class AddictionSystem : EntitySystem | ||
| { | ||
| [Dependency] private readonly IGameTiming _timing = default!; | ||
| [Dependency] private readonly IPrototypeManager _proto = default!; | ||
| [Dependency] private readonly PopupSystem _popup = default!; | ||
| [Dependency] private readonly MobStateSystem _mobState = default!; | ||
|
|
||
| public override void Initialize() | ||
| { | ||
| base.Initialize(); | ||
|
|
||
| SubscribeLocalEvent<AddictionComponent, GetReagentEffectsEvent>(OnGetReagentEffects); | ||
| SubscribeLocalEvent<AddictionComponent, ComponentInit>(OnComponentInit); | ||
| // After TraitSystem: EnsureComp не должен перебить каналы, добавленные трайтом | ||
| SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawnComplete, after: [typeof(TraitSystem)]); | ||
| } | ||
|
|
||
| public override void Update(float frameTime) | ||
| { | ||
| base.Update(frameTime); | ||
|
|
||
| var query = EntityQueryEnumerator<AddictionComponent>(); | ||
| while (query.MoveNext(out var uid, out var comp)) | ||
| { | ||
| UpdateAddiction(uid, comp, frameTime); | ||
| } | ||
| } | ||
|
|
||
| private void OnComponentInit(EntityUid uid, AddictionComponent comp, ComponentInit args) | ||
| { | ||
| // Каналы из трайта приходят с LastDoseTime = 0, иначе ломка началась бы мгновенно. | ||
| // Уровень выше порога означает, что зависимость уже есть: без WasAddicted | ||
| // лечение не показало бы поп-ап выздоровления, а доза до лечения - поп-ап подсадки. | ||
| foreach (var channel in comp.Channels) | ||
| { | ||
| if (channel.LastDoseTime == TimeSpan.Zero) | ||
| channel.LastDoseTime = _timing.CurTime; | ||
|
|
||
| if (channel.Level >= comp.Threshold) | ||
| channel.WasAddicted = true; | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| private void UpdateAddiction(EntityUid uid, AddictionComponent comp, float frameTime) | ||
| { | ||
| var dead = _mobState.IsDead(uid); | ||
|
|
||
| foreach (var channel in comp.Channels) | ||
| { | ||
| // Привыкание медленно спадает само | ||
| channel.Level = MathF.Max(0f, channel.Level - comp.DecayRate * frameTime); | ||
|
|
||
| var timeSinceDose = _timing.CurTime - channel.LastDoseTime; | ||
|
|
||
| // Доза была недавно - ломки нет | ||
| if (timeSinceDose < comp.WithdrawalDelay) | ||
| { | ||
| if (channel.InWithdrawal) | ||
| { | ||
| channel.InWithdrawal = false; | ||
| channel.Stage = 0; | ||
| RaiseSymptomsChanged(uid); | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| // Уровень упал ниже порога - зависимость отпустила | ||
| if (channel.Level <= comp.Threshold) | ||
| { | ||
| if (channel.WasAddicted) | ||
| { | ||
| channel.WasAddicted = false; | ||
| if (!dead) | ||
| _popup.PopupEntity(Loc.GetString($"addiction-cured-{KindLoc(channel.Kind)}"), uid, uid); | ||
| } | ||
|
|
||
| if (channel.InWithdrawal) | ||
| { | ||
| channel.InWithdrawal = false; | ||
| channel.Stage = 0; | ||
| RaiseSymptomsChanged(uid); | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| if (dead) | ||
| continue; | ||
|
|
||
| // Ломка | ||
| var stageTime = timeSinceDose - comp.WithdrawalDelay; | ||
| var stage = stageTime < comp.MildStageDuration | ||
| ? 0 | ||
| : stageTime < comp.MildStageDuration + comp.MediumStageDuration | ||
| ? 1 | ||
| : 2; | ||
|
|
||
| if (!channel.InWithdrawal || channel.Stage != stage) | ||
| { | ||
| channel.InWithdrawal = true; | ||
| channel.Stage = stage; | ||
| RaiseSymptomsChanged(uid); | ||
| } | ||
|
|
||
| if (_timing.CurTime >= channel.NextPopupTime) | ||
| { | ||
| channel.NextPopupTime = _timing.CurTime + comp.PopupInterval; | ||
| _popup.PopupEntity(Loc.GetString($"addiction-withdrawal-{KindLoc(channel.Kind)}-{stage}"), uid, uid); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args) | ||
| { | ||
| // Любой игрок может подсесть: компонент есть у всех с момента спавна | ||
| EnsureComp<AddictionComponent>(args.Mob); | ||
| } | ||
|
|
||
| private void OnGetReagentEffects(EntityUid uid, AddictionComponent comp, ref GetReagentEffectsEvent args) | ||
| { | ||
| var kind = GetKind(args.Reagent, comp); | ||
| if (kind is not { } kindValue) | ||
| return; | ||
|
|
||
| var channel = comp.Channels.FirstOrDefault(c => c.Kind == kindValue); | ||
| if (channel == null) | ||
| { | ||
| channel = new AddictionChannel { Kind = kindValue }; | ||
| comp.Channels.Add(channel); | ||
| } | ||
|
|
||
| channel.Level = MathF.Min(100f, channel.Level + comp.GainPerTick); | ||
| channel.LastDoseTime = _timing.CurTime; | ||
| channel.NextPopupTime = TimeSpan.Zero; | ||
|
|
||
| // Доза снимает ломку (поп-ап только если ломка реально была) | ||
| if (channel.InWithdrawal) | ||
| { | ||
| channel.InWithdrawal = false; | ||
| channel.Stage = 0; | ||
| RaiseSymptomsChanged(uid); | ||
| _popup.PopupEntity(Loc.GetString($"addiction-dose-{KindLoc(kindValue)}"), uid, uid); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| // Первое превышение порога - подсадка | ||
| else if (!channel.WasAddicted && channel.Level >= comp.Threshold) | ||
| { | ||
| channel.WasAddicted = true; | ||
| _popup.PopupEntity(Loc.GetString($"addiction-begin-{KindLoc(kindValue)}"), uid, uid); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Определяет тип зависимости по рецепту: никотин по id, алкоголь по группе Alcohol, | ||
| /// наркотики по группе Narcotic. Группы и реагент настраиваются в компоненте. | ||
| /// </summary> | ||
| private AddictionKind? GetKind(ReagentId reagent, AddictionComponent comp) | ||
| { | ||
| if (reagent.Prototype == comp.NicotineReagent) | ||
| return AddictionKind.Nicotine; | ||
|
|
||
| if (!_proto.TryIndex(reagent.Prototype, out ReagentPrototype? proto) || proto.Metabolisms is not { } metabolisms) | ||
| return null; | ||
|
|
||
| if (metabolisms.ContainsKey(comp.AlcoholMetabolismGroup)) | ||
| return AddictionKind.Alcohol; | ||
|
|
||
| if (metabolisms.ContainsKey(comp.NarcoticMetabolismGroup)) | ||
| return AddictionKind.Drug; | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Сообщает AddictionSymptomsSystem, что набор симптомов нужно пересчитать. | ||
| /// </summary> | ||
| private void RaiseSymptomsChanged(EntityUid uid) | ||
| { | ||
| var ev = new AddictionSymptomsChangedEvent(uid); | ||
| RaiseLocalEvent(uid, ref ev); | ||
| } | ||
|
|
||
| private static string KindLoc(AddictionKind kind) => kind switch | ||
| { | ||
| AddictionKind.Alcohol => "alcohol", | ||
| AddictionKind.Nicotine => "nicotine", | ||
| AddictionKind.Drug => "drug", | ||
| _ => "alcohol", | ||
| }; | ||
| } | ||
28 changes: 28 additions & 0 deletions
28
Content.Server/_Ganimed/Addiction/AdjustAddictionLevelEffectSystem.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // SPDX-FileCopyrightText: 2026 ultradyper <ultradyper@users.noreply.github.com> | ||
| // | ||
| // SPDX-License-Identifier: AGPL-3.0-or-later | ||
|
|
||
| using Content.Shared._Ganimed.Addiction; | ||
| using Content.Shared._Ganimed.Addiction.Effects; | ||
| using Content.Shared.EntityEffects; | ||
|
|
||
| namespace Content.Server._Ganimed.Addiction; | ||
|
|
||
| /// <summary> | ||
| /// Применяет эффект AdjustAddictionLevel к компоненту зависимости. | ||
| /// </summary> | ||
| public sealed partial class AdjustAddictionLevelEffectSystem : EntityEffectSystem<AddictionComponent, AdjustAddictionLevel> | ||
| { | ||
| protected override void Effect(Entity<AddictionComponent> entity, ref EntityEffectEvent<AdjustAddictionLevel> args) | ||
| { | ||
| var amount = args.Effect.Amount * args.Scale; | ||
|
|
||
| foreach (var channel in entity.Comp.Channels) | ||
| { | ||
| if (args.Effect.Kind is { } kind && channel.Kind != kind) | ||
| continue; | ||
|
|
||
| channel.Level = MathF.Max(0f, channel.Level + amount); | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.