Skip to content

Commit 70c1500

Browse files
committed
feat: add jetpack skill and update skill configuration
1 parent db17d9b commit 70c1500

5 files changed

Lines changed: 320 additions & 8 deletions

File tree

resources/translations/en.jsonc

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,20 @@
1414
"teleport.freeze_time": "Cannot teleport while in freeze time! You cheater!!!",
1515
"tank.description": "You're a [red]TANK[default]! You get {0} HP + NEGEV + UNLIMITED AMMO AND LOWER SPEED!",
1616
"tank.only_negev": "You can only use NEGEV as a tank!",
17-
"jetpack.description": "Hold CTRL to fly. Consumes fuel over time",
17+
"noclip.duration": "<font class='fontSize-m'>Noclip duration: <font color='red'>{0}</font></font>",
18+
"noclip.description": "Enables Noclip for {0} seconds when pressing {1}. Cooldown: [red]{2}",
19+
"jetpack.description": "Hold [red]{0}[default] to fly. Fuel: [red]{1}[default]. Refill starts after [red]{2}s[default].",
20+
"jetpack.out_of_fuel": "Jetpack fuel empty! Release [red]{0}[default] to start refilling.",
21+
"jetpack.fuel_hud": "<div align='center'><font color='#b7bec8' class='fontSize-s'>JETPACK FUEL</font><br><font color='{3}' class='fontSize-m'><b>{0}%</b></font><br><font class='fontSize-sm'><font color='{3}'>{1}</font><font color='#5f6670'>{2}</font></font><br><font color='{4}' class='fontSize-s'>{5}</font></div>",
22+
"jetpack.status.boosting": "BOOSTING",
23+
"jetpack.status.empty": "EMPTY",
24+
"jetpack.status.refill_wait": "REFILL IN {0}s",
25+
"jetpack.status.refilling": "REFILLING",
26+
"jetpack.status.full": "FULL",
1827
"double_jump.description": "Press SPACE in mid-air to jump again",
1928
"invisibility.description": "Become semi-invisible to enemies for {0} seconds",
2029
"activate_key": "Activate with: {0}",
2130
"skill_cooldown": "Skill on cooldown: {0}",
2231
"html.roll_title": "<font color='#00ffff' class='fontSize-m'><b>&lt; Random Skills &gt;</b></font>",
2332
"html.roll_rolling": "<font color='#00ff88' class='fontSize-s'>Rolling...</font>"
24-
}
33+
}

src/Config/Config.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
using System.Text.Json.Serialization;
22
using Microsoft.Extensions.Configuration;
3-
using Microsoft.Extensions.Configuration.Binder;
4-
using SwiftlyS2.Shared.Events;
5-
using SwiftlyS2.Shared.SchemaDefinitions;
63

74
namespace SW2_RandomSkills;
85

@@ -64,5 +61,6 @@ public enum SkillType
6461
Shield,
6562
SwapPosition,
6663
Jetpack,
67-
Teleport
64+
Teleport,
65+
Noclip
6866
}

src/SW2-RandomSkills.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
namespace SW2_RandomSkills;
1616

17-
[PluginMetadata(Id = "SW2_RandomSkills", Version = "1.0.2", Name = "SW2-RandomSkills", Author = "T3Marius", Description = "Random skills with roll animation")]
17+
[PluginMetadata(Id = "SW2_RandomSkills", Version = "1.0.3", Name = "SW2-RandomSkills", Author = "T3Marius", Description = "Random skills with roll animation")]
1818
public sealed class SW2_RandomSkills(ISwiftlyCore core) : BasePlugin(core)
1919
{
2020
private ServiceProvider? _provider;
@@ -73,6 +73,8 @@ public override void Load(bool hotReload)
7373
_skillManager.RegisterSkill(new RandomHealthSkill(_localizer, Core));
7474
_skillManager.RegisterSkill(new TeleportSkill(_localizer, Core));
7575
_skillManager.RegisterSkill(new TankSkill(_localizer, Core));
76+
_skillManager.RegisterSkill(new NoclipSkill(_localizer, Core));
77+
_skillManager.RegisterSkill(new JetpackSkill(_localizer, Core));
7678
}
7779

7880
private HookResult OnPlayerDisconnect(EventPlayerDisconnect e)
@@ -524,4 +526,4 @@ public class PlayerRollData
524526
public interface IActivatableSkill
525527
{
526528
void OnActivate(IPlayer player);
527-
}
529+
}

src/Skills/Jetpack.cs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
using SwiftlyS2.Shared;
2+
using SwiftlyS2.Shared.Natives;
3+
using SwiftlyS2.Shared.Players;
4+
using SwiftlyS2.Shared.Translation;
5+
6+
namespace SW2_RandomSkills;
7+
8+
public class JetpackSkill(ILocalizer localizer, ISwiftlyCore core) : BaseSkill(localizer, core)
9+
{
10+
public override SkillType Type => SkillType.Jetpack;
11+
public override string Name => "Jetpack";
12+
13+
private IPlayer? _player;
14+
15+
private float _maxFuel;
16+
private float _fuelDrainPerSecond;
17+
private float _fuelRefillPerSecond;
18+
private float _refillDelay;
19+
private float _liftVelocity;
20+
private float _maxVerticalSpeed;
21+
private float _hudUpdateInterval;
22+
private int _fuelBarSegments;
23+
private string _fuelFillChar = "\u2588";
24+
private string _fuelEmptyChar = "\u2588";
25+
26+
private float _currentFuel;
27+
private float _refillDelayRemaining;
28+
private float _hudAccumulator;
29+
private bool _activationHeld;
30+
private bool _outOfFuelNotified;
31+
32+
protected override string GetDefaultDescription()
33+
{
34+
return $"Hold {Config.ActivationKey} to fly. Fuel refills after {_refillDelay:0.##}s.";
35+
}
36+
37+
protected override object[] GetDescriptionParameters()
38+
{
39+
return new object[] { Config.ActivationKey, _maxFuel, _refillDelay };
40+
}
41+
42+
public override void Initialize(SkillConfig config)
43+
{
44+
base.Initialize(config);
45+
46+
_maxFuel = Math.Max(1f, GetParameter("MaxFuel", 100f));
47+
_fuelDrainPerSecond = Math.Max(0.01f, GetParameter("FuelDrainPerSecond", 25f));
48+
_fuelRefillPerSecond = Math.Max(0.01f, GetParameter("FuelRefillPerSecond", 15f));
49+
_refillDelay = Math.Max(0f, GetParameter("RefillTimer", GetParameter("RefillDelay", 1.5f)));
50+
_liftVelocity = GetParameter("LiftVelocity", 280f);
51+
_maxVerticalSpeed = Math.Max(50f, GetParameter("MaxVerticalSpeed", 320f));
52+
_hudUpdateInterval = Math.Max(0.05f, GetParameter("HudUpdateInterval", 0.1f));
53+
_fuelBarSegments = Math.Max(5, GetParameter("FuelBarSegments", 18));
54+
55+
string fillChar = GetParameter("FuelFillChar", GetParameter("FuelBarFillChar", "\u2588"));
56+
string emptyChar = GetParameter("FuelEmptyChar", GetParameter("FuelBarEmptyChar", "\u2588"));
57+
58+
_fuelFillChar = string.IsNullOrWhiteSpace(fillChar) ? "\u2588" : fillChar;
59+
_fuelEmptyChar = string.IsNullOrWhiteSpace(emptyChar) ? "\u2588" : emptyChar;
60+
61+
_currentFuel = _maxFuel;
62+
_refillDelayRemaining = 0f;
63+
_hudAccumulator = 0f;
64+
_activationHeld = false;
65+
_outOfFuelNotified = false;
66+
}
67+
68+
public override void Apply(IPlayer player)
69+
{
70+
_player = player;
71+
_currentFuel = _maxFuel;
72+
_refillDelayRemaining = 0f;
73+
_hudAccumulator = _hudUpdateInterval;
74+
_activationHeld = false;
75+
_outOfFuelNotified = false;
76+
}
77+
78+
public override void OnActivationButtonHeld(IPlayer player)
79+
{
80+
base.OnActivationButtonHeld(player);
81+
82+
_activationHeld = true;
83+
if (_currentFuel <= 0f)
84+
{
85+
return;
86+
}
87+
88+
ApplyLift(player);
89+
}
90+
91+
public override void OnActivationButtonReleased(IPlayer player)
92+
{
93+
base.OnActivationButtonReleased(player);
94+
_activationHeld = false;
95+
}
96+
97+
public override void OnTick(IPlayer player, float deltaTime)
98+
{
99+
bool heldThisTick = _activationHeld;
100+
_activationHeld = false;
101+
102+
if (heldThisTick)
103+
{
104+
if (_currentFuel > 0f)
105+
{
106+
_currentFuel = Math.Max(0f, _currentFuel - (_fuelDrainPerSecond * deltaTime));
107+
_refillDelayRemaining = _refillDelay;
108+
109+
if (_currentFuel <= 0f && !_outOfFuelNotified)
110+
{
111+
player.SendChat(Localizer["prefix"] + Localizer["jetpack.out_of_fuel", Config.ActivationKey.ToUpper()]);
112+
_outOfFuelNotified = true;
113+
}
114+
}
115+
else
116+
{
117+
_refillDelayRemaining = _refillDelay;
118+
}
119+
}
120+
else
121+
{
122+
if (_refillDelayRemaining > 0f)
123+
{
124+
_refillDelayRemaining = Math.Max(0f, _refillDelayRemaining - deltaTime);
125+
}
126+
else
127+
{
128+
_currentFuel = Math.Min(_maxFuel, _currentFuel + (_fuelRefillPerSecond * deltaTime));
129+
if (_currentFuel > 0f)
130+
{
131+
_outOfFuelNotified = false;
132+
}
133+
}
134+
}
135+
136+
UpdateFuelHud(player, deltaTime, heldThisTick);
137+
}
138+
139+
public override void Remove(IPlayer player)
140+
{
141+
if (_player != null && _player.IsValid)
142+
{
143+
_player.SendCenterHTML("");
144+
}
145+
146+
_activationHeld = false;
147+
_player = null;
148+
}
149+
150+
private void ApplyLift(IPlayer player)
151+
{
152+
if (player.Pawn == null)
153+
{
154+
return;
155+
}
156+
157+
var velocity = player.Pawn.AbsVelocity;
158+
float boostedZ = Math.Min(_maxVerticalSpeed, Math.Max(velocity.Z, _liftVelocity));
159+
Vector newVelocity = new Vector(velocity.X, velocity.Y, boostedZ);
160+
161+
162+
player.Pawn.Teleport(null, null, newVelocity);
163+
}
164+
165+
private void UpdateFuelHud(IPlayer player, float deltaTime, bool heldThisTick)
166+
{
167+
_hudAccumulator += deltaTime;
168+
if (_hudAccumulator < _hudUpdateInterval)
169+
{
170+
return;
171+
}
172+
173+
_hudAccumulator = 0f;
174+
float fuelPercent = (_currentFuel / _maxFuel) * 100f;
175+
176+
int filled = (int)MathF.Round((_currentFuel / _maxFuel) * _fuelBarSegments);
177+
filled = Math.Clamp(filled, 0, _fuelBarSegments);
178+
int empty = _fuelBarSegments - filled;
179+
180+
string filledBar = string.Concat(Enumerable.Repeat(_fuelFillChar, filled));
181+
string emptyBar = string.Concat(Enumerable.Repeat(_fuelEmptyChar, empty));
182+
string fuelColor = GetFuelColor(fuelPercent);
183+
184+
string statusText;
185+
string statusColor;
186+
187+
if (heldThisTick && _currentFuel > 0f)
188+
{
189+
statusText = Localizer["jetpack.status.boosting"];
190+
statusColor = "#67e8f9";
191+
}
192+
else if (_currentFuel <= 0f)
193+
{
194+
statusText = Localizer["jetpack.status.empty"];
195+
statusColor = "#ef4444";
196+
}
197+
else if (_refillDelayRemaining > 0f)
198+
{
199+
statusText = Localizer["jetpack.status.refill_wait", _refillDelayRemaining.ToString("0.0")];
200+
statusColor = "#9ca3af";
201+
}
202+
else if (_currentFuel >= _maxFuel - 0.01f)
203+
{
204+
statusText = Localizer["jetpack.status.full"];
205+
statusColor = "#22c55e";
206+
}
207+
else
208+
{
209+
statusText = Localizer["jetpack.status.refilling"];
210+
statusColor = "#f59e0b";
211+
}
212+
213+
player.SendCenterHTML(Localizer["jetpack.fuel_hud", fuelPercent.ToString("0"), filledBar, emptyBar, fuelColor, statusColor, statusText]);
214+
}
215+
216+
private static string GetFuelColor(float fuelPercent)
217+
{
218+
if (fuelPercent <= 25f)
219+
{
220+
return "#ef4444";
221+
}
222+
223+
if (fuelPercent <= 60f)
224+
{
225+
return "#f59e0b";
226+
}
227+
228+
return "#22c55e";
229+
}
230+
}

src/Skills/Noclip.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using SwiftlyS2.Shared;
2+
using SwiftlyS2.Shared.Players;
3+
using SwiftlyS2.Shared.SchemaDefinitions;
4+
using SwiftlyS2.Shared.Translation;
5+
6+
namespace SW2_RandomSkills;
7+
8+
public class NoclipSkill(ILocalizer localizer, ISwiftlyCore core) : BaseSkill(localizer, core)
9+
{
10+
public override SkillType Type => SkillType.Noclip;
11+
public override string Name => "Noclip";
12+
protected override string GetDefaultDescription()
13+
{
14+
return $"Enables Noclip for {_noClipDuration} seconds when pressing {Config.ActivationKey}. Cooldown: {Config.Cooldown}";
15+
}
16+
protected override object[] GetDescriptionParameters()
17+
{
18+
return new object[] { _noClipDuration, Config.ActivationKey, Config.Cooldown };
19+
}
20+
private int _noClipDuration;
21+
private CancellationTokenSource? _noClipTimer;
22+
public override void Apply(IPlayer player) { }
23+
public override void Initialize(SkillConfig config)
24+
{
25+
base.Initialize(config);
26+
_noClipDuration = GetParameter("NoclipDuration", 5);
27+
}
28+
public override void Remove(IPlayer player)
29+
{
30+
_noClipTimer?.Cancel();
31+
_noClipTimer = null;
32+
}
33+
public override void OnActivationButtonPressed(IPlayer player)
34+
{
35+
if (IsOnCooldown())
36+
{
37+
player.SendChat(Localizer["prefix"] + Localizer["skill_cooldown", GetCooldownLeft().ToString("0")]);
38+
return;
39+
}
40+
41+
int duration = _noClipDuration;
42+
43+
ChangeMoveType(player, MoveType_t.MOVETYPE_NOCLIP);
44+
_noClipTimer = Core.Scheduler.RepeatBySeconds(1.0f, () =>
45+
{
46+
duration--;
47+
if (duration > 0)
48+
{
49+
player.SendCenterHTML(Localizer["noclip.duration", duration.ToString("0")]);
50+
}
51+
else
52+
{
53+
player.SendCenterHTML("");
54+
ChangeMoveType(player, MoveType_t.MOVETYPE_WALK);
55+
_noClipDuration = GetParameter("NoclipDuration", 5);
56+
_noClipTimer?.Cancel();
57+
_noClipTimer = null;
58+
}
59+
});
60+
StartCooldown();
61+
}
62+
private void ChangeMoveType(IPlayer player, MoveType_t moveType)
63+
{
64+
var playerPawn = player.PlayerPawn;
65+
if (playerPawn == null)
66+
return;
67+
68+
playerPawn.MoveType = moveType;
69+
playerPawn.ActualMoveType = moveType;
70+
playerPawn.MoveTypeUpdated();
71+
}
72+
73+
}

0 commit comments

Comments
 (0)