hyperDice & arrowRoulette (#3291)

Co-authored-by: Sidzaru <gruvlai@mail.ru>
This commit is contained in:
Sidzaru 2025-12-25 19:59:41 +03:00 committed by GitHub
parent 9e9675c558
commit 1196b9e268
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 581 additions and 4 deletions

View file

@ -23,7 +23,10 @@ public sealed class DiceSystem : SharedDiceSystem
var state = _sprite.LayerGetRsiState((entity.Owner, sprite), 0).Name;
if (state == null)
return;
// Sunrise-Edit
if (entity.Comp.IsNotStandardDice)
return;
// Sunrise-Edit-End
var prefix = state.Substring(0, state.IndexOf('_'));
_sprite.LayerSetRsiState((entity.Owner, sprite), 0, $"{prefix}_{entity.Comp.CurrentValue}");
}

View file

@ -0,0 +1,42 @@
using Content.Shared._Sunrise.Dice;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client._Sunrise.Dice.UI
{
[UsedImplicitly]
public sealed class ChangeDiceUserInterface : BoundUserInterface
{
[Dependency] private readonly ILogManager _logManager = default!;
private IEntityManager _entManager;
private EntityUid _owner;
[ViewVariables]
private ChangeDiceWindow? _window;
public ChangeDiceUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_owner = owner;
_entManager = IoCManager.Resolve<IEntityManager>();
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<ChangeDiceWindow>();
_window.OpenCentered();
_window.ApplyButton.OnPressed += _ =>
{
if (int.TryParse(_window.AmountStartLineEdit.Text, out var x) && int.TryParse(_window.AmountEndLineEdit.Text, out var y))
{
SendMessage(new ChangeDiceSetValueMessage(FixedPoint2.New(x), FixedPoint2.New(y)));
_window.Close();
}
};
}
}
}

View file

@ -0,0 +1,38 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Sunrise.Dice
{
[GenerateTypedNameReferences]
public sealed partial class ChangeDiceWindow : DefaultWindow
{
public ChangeDiceWindow()
{
RobustXamlLoader.Load(this);
AmountStartLineEdit.OnTextChanged += OnValueChanged;
AmountEndLineEdit.OnTextChanged += OnValueChanged;
}
private void OnValueChanged(LineEdit.LineEditEventArgs args)
{
if (int.TryParse(AmountStartLineEdit.Text, out var startAmount) &&
int.TryParse(AmountEndLineEdit.Text, out var endAmount))
{
if (startAmount < 1 || endAmount > 1000000 || endAmount < startAmount)
{
ApplyButton.Disabled = true;
}
else
{
ApplyButton.Disabled = false;
}
}
else
{
ApplyButton.Disabled = true;
}
}
}
}

View file

@ -0,0 +1,12 @@
<DefaultWindow xmlns="https://spacestation14.io"
Resizable="False"
Title="{Loc 'ui-sides-amount-title'}">
<BoxContainer Orientation="Vertical" SeparationOverride="6" MinSize="310 80">
<BoxContainer Orientation="Horizontal" SeparationOverride="8">
<LineEdit Name="AmountStartLineEdit" Access="Public" HorizontalExpand="True" PlaceHolder="{Loc 'ui-sides-amount-start-placeholder'}"/>
<LineEdit Name="AmountEndLineEdit" Access="Public" HorizontalExpand="True" PlaceHolder="{Loc 'ui-sides-amount-end-placeholder'}"/>
</BoxContainer>
<Button Name="ApplyButton" Access="Public" Text="{Loc 'ui-sides-amount-apply'}"/>
<Label Margin="0 0 0 0" Text="{Loc 'ui-sides-amount-bottom-text'}" SetHeight="44" VerticalExpand="True" StyleClasses="WindowFooterText" />
</BoxContainer>
</DefaultWindow>

View file

@ -0,0 +1,128 @@
using Content.Shared.Interaction;
using Robust.Shared.Random;
using Content.Shared._Sunrise.Fun;
using Content.Shared.Popups;
using Content.Shared.Hands;
using Content.Shared.Verbs;
using Content.Shared.Ghost;
namespace Content.Server._Sunrise.Fun
{
public sealed partial class SpinnerSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpinnerComponent, ActivateInWorldEvent>(OnActivateInWorld);
SubscribeLocalEvent<SpinnerComponent, GotEquippedHandEvent>(OnGotEquippedHand);
SubscribeLocalEvent<SpinnerComponent, GetVerbsEvent<AlternativeVerb>>(OnAlternativeInteract);
}
private void OnAlternativeInteract(Entity<SpinnerComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (CompOrNull<GhostComponent>(args.User) is not null || CompOrNull<TransformComponent>(ent) is null)
return;
HandleSpinnerActivation(ent, args.User);
}
private void OnGotEquippedHand(Entity<SpinnerComponent> ent, ref GotEquippedHandEvent args)
{
ent.Comp.IsSpinning = false;
ent.Comp.RemainingSeconds = 0f;
ent.Comp.CurrentDegPerSec = 0f;
Dirty(ent, ent.Comp);
return;
}
private void OnActivateInWorld(Entity<SpinnerComponent> ent, ref ActivateInWorldEvent args)
{
var transformComp = CompOrNull<TransformComponent>(ent);
if (transformComp is null || !transformComp.Anchored)
return;
HandleSpinnerActivation(ent, args.User);
}
private void HandleSpinnerActivation(Entity<SpinnerComponent> ent, EntityUid userId)
{
var userName = CompOrNull<MetaDataComponent>(userId)?.EntityName;
if (!ent.Comp.IsSpinning)
{
StartSpin(ent, ent.Comp);
_popupSystem.PopupEntity($"{userName} {Loc.GetString("arrow-spin-start")}", userId);
return;
}
if (ent.Comp.RemainingSeconds > ent.Comp.MaxSpinSeconds)
return;
if (ent.Comp.CurrentDegPerSec > ent.Comp.MaxDegPerSec)
return;
var seconds = _random.NextFloat(ent.Comp.MinSpinSeconds, ent.Comp.MaxSpinSeconds);
var degPerSec = _random.NextFloat(ent.Comp.MinDegPerSec, ent.Comp.MaxDegPerSec);
ent.Comp.RemainingSeconds += seconds;
ent.Comp.CurrentDegPerSec += degPerSec;
_popupSystem.PopupEntity($"{userName} {Loc.GetString("arrow-speed-up")}", userId);
Dirty(ent, ent.Comp);
}
private void StartSpin(EntityUid uid, SpinnerComponent comp)
{
var seconds = _random.NextFloat(comp.MinSpinSeconds, comp.MaxSpinSeconds);
var degPerSec = _random.NextFloat(comp.MinDegPerSec, comp.MaxDegPerSec);
comp.IsSpinning = true;
comp.RemainingSeconds = seconds;
comp.CurrentDegPerSec = degPerSec;
Dirty(uid, comp);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<SpinnerComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var xform))
{
if (!comp.IsSpinning)
continue;
var dt = frameTime;
var deltaDeg = comp.CurrentDegPerSec * dt;
var newAngle = xform.LocalRotation.Degrees + deltaDeg;
_xform.SetLocalRotation(uid, Angle.FromDegrees(newAngle));
comp.RemainingSeconds -= dt;
if (comp.RemainingSeconds <= 0f)
{
comp.CurrentDegPerSec *= comp.BrakeFactor;
if (MathF.Abs(comp.CurrentDegPerSec) < comp.ForceStopSpeed)
{
comp.IsSpinning = false;
comp.CurrentDegPerSec = 0f;
comp.RemainingSeconds = 0f;
Dirty(uid, comp);
continue;
}
}
if (comp.RemainingSeconds > 0f && comp.RemainingSeconds < comp.SmoothStopAtSecond)
comp.CurrentDegPerSec *= comp.SmoothStopBrakeFactor;
Dirty(uid, comp);
}
}
}
}

View file

@ -23,8 +23,11 @@ public sealed partial class DiceComponent : Component
[DataField]
public int Offset { get; private set; } = 0;
// Sunrise-Edit
[DataField]
public int Sides { get; private set; } = 20;
[AutoNetworkedField]
public int Sides { get; set; } = 20;
// Sunrise-Edit-End
/// <summary>
/// The currently displayed value.
@ -33,4 +36,17 @@ public sealed partial class DiceComponent : Component
[AutoNetworkedField]
public int CurrentValue { get; set; } = 20;
[DataField]
[AutoNetworkedField]
public int StartFromSide { get; set; } = 1;
public void SetSides(int startValue, int endValue)
{
StartFromSide = startValue;
Sides = endValue;
CurrentValue = endValue;
}
[DataField("IsNotStandardDice")]
public bool IsNotStandardDice = false;
}

View file

@ -1,3 +1,4 @@
using Content.Shared._Sunrise.Dice;
using Content.Shared.Examine;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
@ -20,6 +21,8 @@ public abstract class SharedDiceSystem : EntitySystem
SubscribeLocalEvent<DiceComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<DiceComponent, LandEvent>(OnLand);
SubscribeLocalEvent<DiceComponent, ExaminedEvent>(OnExamined);
// Sunrise-Edit
SubscribeLocalEvent<DiceComponent, ChangeDiceSetValueMessage>(OnChangeDiceSetValueMessage);
}
private void OnUseInHand(Entity<DiceComponent> entity, ref UseInHandEvent args)
@ -41,7 +44,16 @@ public abstract class SharedDiceSystem : EntitySystem
//No details check, since the sprite updates to show the side.
using (args.PushGroup(nameof(DiceComponent)))
{
args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-1", ("sidesAmount", entity.Comp.Sides)));
// Sunrise-Edit
if (entity.Comp.IsNotStandardDice)
{
args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-3", ("startSide", entity.Comp.StartFromSide), ("endSide", entity.Comp.Sides)));
}
else
{
args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-1", ("sidesAmount", entity.Comp.Sides)));
}
// Sunrise-Edit-End
args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-2",
("currentSide", entity.Comp.CurrentValue)));
}
@ -74,7 +86,9 @@ public abstract class SharedDiceSystem : EntitySystem
{
var rand = new System.Random((int)_timing.CurTick.Value);
var roll = rand.Next(1, entity.Comp.Sides + 1);
// Sunrise-Edit
var roll = rand.Next(entity.Comp.StartFromSide, entity.Comp.Sides + 1);
// Sunrise-Edit-End
SetCurrentSide(entity, roll);
var popupString = Loc.GetString("dice-component-on-roll-land",
@ -83,4 +97,12 @@ public abstract class SharedDiceSystem : EntitySystem
_popup.PopupPredicted(popupString, entity, user);
_audio.PlayPredicted(entity.Comp.Sound, entity, user);
}
// Sunrise-Edit
private void OnChangeDiceSetValueMessage(Entity<DiceComponent> entity, ref ChangeDiceSetValueMessage args)
{
entity.Comp.SetSides((int)args.StartValue, (int)args.EndValue);
_popup.PopupPredicted(Loc.GetString("comp-change-dice-sides-amount", ("startAmount", (int)args.StartValue), ("endAmount", (int)args.EndValue)), entity, entity.Owner);
Dirty(entity);
}
}

View file

@ -0,0 +1,37 @@
using Content.Shared.FixedPoint;
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.Dice
{
[Serializable, NetSerializable]
public sealed class ChangeDiceInterfaceState : BoundUserInterfaceState
{
public FixedPoint2 Max;
public FixedPoint2 Min;
public ChangeDiceInterfaceState(FixedPoint2 max, FixedPoint2 min)
{
Max = max;
Min = min;
}
}
[Serializable, NetSerializable]
public sealed class ChangeDiceSetValueMessage : BoundUserInterfaceMessage
{
public FixedPoint2 StartValue;
public FixedPoint2 EndValue;
public ChangeDiceSetValueMessage(FixedPoint2 startAmount, FixedPoint2 endAmount)
{
StartValue = startAmount;
EndValue = endAmount;
}
}
[Serializable, NetSerializable]
public enum ChangeDiceUiKey
{
Key,
}
}

View file

@ -0,0 +1,37 @@
using Content.Shared.Dice;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Shared._Sunrise.Dice;
public sealed class ChangeDiceVerbSystem : EntitySystem
{
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DiceComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerb);
}
private void OnGetVerb(Entity<DiceComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
var (uid, comp) = ent;
if (!args.CanAccess || !args.CanInteract || args.Hands == null || !ent.Comp.IsNotStandardDice)
return;
var @event = args;
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("comp-change-dice-sides-number"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/die.svg.192dpi.png")),
Act = () =>
{
_ui.OpenUi(uid, ChangeDiceUiKey.Key, @event.User);
},
Priority = 1
});
}
}

View file

@ -0,0 +1,33 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.Fun
{
[RegisterComponent, NetworkedComponent]
public sealed partial class SpinnerComponent : Component
{
[DataField]
public float MinSpinSeconds = 3.0f;
[DataField]
public float MaxSpinSeconds = 6.0f;
[DataField]
public float MinDegPerSec = 500f;
[DataField]
public float MaxDegPerSec = 2000f;
[DataField]
public float BrakeFactor = 0.968f;
[DataField]
public float ForceStopSpeed = 10f;
[DataField]
public float SmoothStopAtSecond = 0.5f;
[DataField]
public float SmoothStopBrakeFactor = 0.995f;
[ViewVariables]
public bool IsSpinning;
[ViewVariables]
public float RemainingSeconds;
[ViewVariables]
public float CurrentDegPerSec;
}
}

View file

@ -0,0 +1,4 @@
ent-hyperDice = Hypercube
.desc =
A customizable multidimensional singular hypercube for numerical generation. The latest technology in board games.
One cube to rule them all.

View file

@ -0,0 +1,13 @@
ent-ArrowRouletteEros = Arrow of Eros
.desc =
A roulette arrow designed to bring quality randomness to your love games.
The manufacturer is not responsible for any misuse of the arrow.
ent-ArrowRouletteErot = Arrow of Erot
.desc =
A roulette arrow designed to add quality randomness to your love games.
The manufacturer is not responsible for any misuse of the arrow.
arrow-speed-up = speeds up the arrow
arrow-spin-start = spins the arrow

View file

@ -0,0 +1,2 @@
# Sunrise Edit
dice-component-on-examine-message-part-3 = Dice with sides from [color=lightgray]{ $startSide }[/color] to [color=lightgray]{ $endSide }[/color].

View file

@ -0,0 +1,13 @@
comp-change-dice-sides-amount = Sides selected from {$startAmount} to {$endAmount}.
comp-change-dice-sides-number = Adjust number of sides
ui-sides-amount-title = Setting the hypercube range:
ui-sides-amount-start-placeholder = Generation from:
ui-sides-amount-end-placeholder = Generation to:
ui-sides-amount-apply = Apply changes
ui-sides-amount-bottom-text = Harmless entertainment guarantees an almost zero probability
of singularity when using the hypercube.
Attempting to understand how the hypercube works can lead to insanity.

View file

@ -0,0 +1,13 @@
comp-change-dice-sides-amount = Выбраны стороны от {$startAmount} до {$endAmount}.
comp-change-dice-sides-number = Изменить кол-во граней
ui-sides-amount-title = Настройка диапазона гиперкуба:
ui-sides-amount-start-placeholder = Генерация от:
ui-sides-amount-end-placeholder = Генерация до:
ui-sides-amount-apply = Применить изменения
ui-sides-amount-bottom-text = Безобидные развлечения гарантируют почти нулевую вероятность
возникновения сингулярности при использовании гиперкуба.
Попытка осознания принципа работы гиперкуба может привести вас к безумию.

View file

@ -0,0 +1 @@
dice-component-on-examine-message-part-3 = Кость с гранями от [color=lightgray]{ $startSide }[/color] до [color=lightgray]{ $endSide }[/color].

View file

@ -0,0 +1,5 @@
ent-hyperDice = Гиперкуб
.desc =
Настраиваемый многомерный сингулярный гиперкуб для числовой генерации
от 1 до 1 000 000. Последнее слово техники в области настольных игр.
Один куб, чтоб править всеми.

View file

@ -0,0 +1,11 @@
ent-ArrowRouletteEros = стрела Эроса
.desc =
Стрела рулетка, созданная для качественного рандома в ваших любовных играх.
Производитель не несет ответственности при использовании стрелы не по назначению.
ent-ArrowRouletteErot = стрела Эрота
.desc = { ent-ArrowRouletteEros.desc }
arrow-speed-up = ускоряет стрелу
arrow-spin-start = раскручивает стрелу

View file

@ -4,6 +4,8 @@
#Sunrise-start
PokerCardClassicBoxFilled: 2
PokerCardBoxFilled: 2
ArrowRouletteErot: 3
ArrowRouletteEros: 3
#Sunrise-end
DiceBag: 6
Paper: 8

View file

@ -13,6 +13,7 @@
- id: d12Dice
- id: d20Dice
- id: PercentileDie
- id: hyperDice
- type: Sprite
sprite: Objects/Fun/dice.rsi
state: dicebag

View file

@ -0,0 +1,48 @@
- type: entity
parent: BaseItem
abstract: true
id: BaseArrowRoulette
save: true
description: Spin the oxygen tank - last century
components:
- type: Anchorable
delay: 2
- type: Spinner
- type: entity
id: ArrowRouletteErot
name: Arrow of Erot
parent: BaseArrowRoulette
components:
- type: Sprite
offset: -0.00,0.00
sprite: _Sunrise/Objects/Fun/lucky_arrow.rsi
state: arrow_red
- type: Item
size: Small
inhandVisuals:
left:
- sprite: _Sunrise/Objects/Fun/lucky_arrow_inhand.rsi
state: inhand_left_red
right:
- sprite: _Sunrise/Objects/Fun/lucky_arrow_inhand.rsi
state: inhand_right_red
- type: entity
id: ArrowRouletteEros
name: Arrow of Eros
parent: BaseArrowRoulette
components:
- type: Sprite
offset: -0.00,-0.00
sprite: _Sunrise/Objects/Fun/lucky_arrow.rsi
state: arrow_blue
- type: Item
size: Small
inhandVisuals:
left:
- sprite: _Sunrise/Objects/Fun/lucky_arrow_inhand.rsi
state: inhand_left_blue
right:
- sprite: _Sunrise/Objects/Fun/lucky_arrow_inhand.rsi
state: inhand_right_blue

View file

@ -0,0 +1,36 @@
- type: entity
parent: BaseDice
id: hyperDice
name: Hyper Dice
description: A customizable multidimensional singular hypercube for numerical generation. The latest technology in board games. One cube to rule them all.
components:
- type: Dice
IsNotStandardDice: true
sides: 1000
currentValue: 1000
- type: Sprite
sprite: _Sunrise/Objects/Fun/dice.rsi
state: icon
scale: 0.65,0.65
- type: CollisionWake
enabled: false
- type: Fixtures
fixtures:
slips:
shape:
!type:PhysShapeAabb
bounds: "-0.2,-0.2,0.2,0.2"
hard: false
layer:
- LowImpassable
fix1:
shape:
!type:PhysShapeAabb
bounds: "-0.2,-0.2,0.2,0.2"
density: 30
mask:
- ItemMask
- type: UserInterface
interfaces:
enum.ChangeDiceUiKey.Key:
type: ChangeDiceUserInterface

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,14 @@
{
"version": 1,
"copyright": "Sprited by vimenant2 (discord)",
"license": "CLA",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,19 @@
{
"version": 1,
"copyright": "Sprited by vimenant2 (discord)",
"license": "CLA",
"size": {
"x": 32,
"y": 48
},
"states": [
{
"name": "arrow_blue",
"directions": 1
},
{
"name": "arrow_red",
"directions": 1
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,27 @@
{
"version": 1,
"copyright": "Sprited by vimenant2 (discord)",
"license": "CLA",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "inhand_left_blue",
"directions": 4
},
{
"name": "inhand_right_blue",
"directions": 4
},
{
"name": "inhand_left_red",
"directions": 4
},
{
"name": "inhand_right_red",
"directions": 4
}
]
}