mirror of
https://github.com/DeltaV-Station/Delta-v.git
synced 2026-09-29 23:57:29 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e44ee3ee | ||
|
|
fc28b60175 |
@@ -10,28 +10,22 @@
|
||||
<!-- Summary of code changes for easier review. -->
|
||||
|
||||
## Media
|
||||
<!-- Attach media if the PR makes in-game changes (clothing, items, features, etc).
|
||||
<!-- Attach media if the PR makes ingame changes (clothing, items, features, etc).
|
||||
Small fixes/refactors are exempt. -->
|
||||
|
||||
## Requirements
|
||||
<!-- Confirm the following by placing an X in the brackets without spaces inside (for example: [X] ): -->
|
||||
<!-- Confirm the following by placing an X in the brackets: [X] -->
|
||||
- [ ] I have tested all added content and changes.
|
||||
- [ ] I have added media to this PR or it does not require an in-game showcase.
|
||||
- [ ] I have added media to this PR or it does not require an ingame showcase.
|
||||
<!-- You should understand that not following the above may get your PR closed at maintainer’s discretion -->
|
||||
|
||||
## Licensing
|
||||
<!-- This is for the benefit of other forks wishing to port features from DeltaV
|
||||
Our repository is AGPL, meaning projects using the MIT license would have to ask for permission from the PR author (ideally that's you, reading this) before being allowed to port it over
|
||||
This just saves them the trouble. Note that AGPL or MIT licensing is only applicable to SOFTWARE, and that assets such as textures and audio have their own licensing scheme that is defined in the codebase itself. -->
|
||||
- [ ] I confirm that I am the creator of the code in this PR, and allow licensing it under the following license(s), or that the original author(s) has given me permission to do so:
|
||||
- [X] AGPL (https://github.com/DeltaV-Station/Delta-v/blob/master/LICENSE-AGPLv3.txt) <!-- This one is required to contribute to DeltaV -->
|
||||
- [X] MIT (https://github.com/DeltaV-Station/Delta-v/blob/master/LICENSE-MIT.txt) <!-- Optional - A lot of other SS14 forks use MIT -->
|
||||
<!-- Feel free to add more licenses as you see fit -->
|
||||
## Breaking changes
|
||||
<!-- List any breaking changes, including namespaces, public class/method/field changes, prototype renames; and provide instructions for fixing them. -->
|
||||
|
||||
**Changelog**
|
||||
<!-- Add a Changelog entry to make players aware of new features or changes that could affect gameplay.
|
||||
Make sure to read the guidelines and take this Changelog template out of the comment block in order for it to show up.
|
||||
Changelog must have a :cl: symbol, so the bot recognizes the changes and adds them to the game's changelog. -->
|
||||
Changelogs must have a :cl: symbol, so the bot recognizes the changes and adds them to the game's changelog. -->
|
||||
<!--
|
||||
:cl:
|
||||
- add: Added fun!
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, staging, trying ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Build & Test Debug
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, staging, trying ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [review_requested, opened]
|
||||
types: [review_requested]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request_target'
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
@@ -14,3 +22,24 @@ jobs:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: "S: Awaiting Changes"
|
||||
|
||||
# Doesn't work on prs from foreign repos currently
|
||||
#handle_review:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event_name == 'pull_request_review'
|
||||
# steps:
|
||||
# - uses: actions-ecosystem/action-add-labels@v1
|
||||
# if: |
|
||||
# github.event.review.state == 'changes_requested' &&
|
||||
# (github.event.review.author_association == 'OWNER' ||
|
||||
# github.event.review.author_association == 'MEMBER')
|
||||
# with:
|
||||
# labels: "S: Awaiting Changes"
|
||||
#
|
||||
# - uses: actions-ecosystem/action-remove-labels@v1
|
||||
# if: |
|
||||
# github.event.review.state == 'changes_requested' &&
|
||||
# (github.event.review.author_association == 'OWNER' ||
|
||||
# github.event.review.author_association == 'MEMBER')
|
||||
# with:
|
||||
# labels: "S: Needs Review"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# The PAT does not have the correct permissions to make this work; Eventually we'll want to replace this workflow with a proper bot
|
||||
# name: "Labels: Approved"
|
||||
# on:
|
||||
# pull_request_review:
|
||||
# types: [submitted]
|
||||
# jobs:
|
||||
# add_label:
|
||||
# # Change the repository name after you've made sure the team name is correct for your fork!
|
||||
# if: ${{ (github.repository == 'DeltaV-Station/Delta-v') && (github.event.review.state == 'APPROVED') }}
|
||||
# permissions:
|
||||
# contents: read
|
||||
# pull-requests: write
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: tspascoal/get-user-teams-membership@v3
|
||||
# id: checkUserMember
|
||||
# with:
|
||||
# username: ${{ github.actor }}
|
||||
# team: "maintainers"
|
||||
# GITHUB_TOKEN: ${{ secrets.LABELER_PAT }}
|
||||
# - if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
|
||||
# uses: actions-ecosystem/action-add-labels@v1
|
||||
# with:
|
||||
# labels: "S: Approved"
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, staging, trying ]
|
||||
paths:
|
||||
- '**.cs'
|
||||
- '**.csproj'
|
||||
|
||||
@@ -34,19 +34,17 @@ jobs:
|
||||
# Uncomment this and comment the other line if you do this.
|
||||
# https://github.com/stefanzweifel/git-auto-commit-action#push-to-protected-branches
|
||||
|
||||
# - name: Commit new credit files
|
||||
# uses: stefanzweifel/git-auto-commit-action@v7
|
||||
# with:
|
||||
# commit_message: Update Credits
|
||||
# commit_author: DeltaV-Bot <github@deltav.gay>
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
#- name: Commit new credit files
|
||||
# uses: stefanzweifel/git-auto-commit-action@v4
|
||||
# with:
|
||||
# commit_message: Update Credits
|
||||
# commit_author: PJBot <pieterjan.briers+bot@gmail.com>
|
||||
|
||||
# This will make a PR
|
||||
- name: Set current date as env variable
|
||||
run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
with:
|
||||
commit-message: Update Credits
|
||||
@@ -54,14 +52,3 @@ jobs:
|
||||
body: This is an automated Pull Request. This PR updates the github contributors in the credits section.
|
||||
author: DeltaV-Bot <github@deltav.gay>
|
||||
branch: automated/credits-${{env.NOW}}
|
||||
|
||||
- name: Auto-merge PR with bypass
|
||||
if: steps.cpr.outputs.pull-request-number != ''
|
||||
env:
|
||||
GH_TOKEN: ${{secrets.CREDITS_TOKEN}}
|
||||
run: |
|
||||
gh pr merge ${{ steps.cpr.outputs.pull-request-number }} \
|
||||
--squash \
|
||||
--admin \
|
||||
--delete-branch \
|
||||
--repo ${{ github.repository }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: RGA schema validator
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, staging, trying ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
|
||||
@@ -2,7 +2,7 @@ name: RSI Validator
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ staging, trying ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
paths:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Map file schema validator
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, staging, trying ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
|
||||
@@ -2,7 +2,7 @@ name: YAML Linter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ master, staging, trying ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
|
||||
+21
-21
@@ -204,49 +204,49 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|
||||
|style="border:1px solid black;" | 301
|
||||
|style= "border:1px solid black;"| [[File:SL_Manslaughter.png]]
|
||||
|style= "border:1px solid black;"| Manslaughter
|
||||
|style= "border:1px solid black;"| 10 minutes
|
||||
|style= "border:1px solid black;"| 16 minutes
|
||||
|style= "border:1px solid black;"| To act without malice in a manner which, directly or indirectly, leads to the death of an Employee.
|
||||
|-
|
||||
|style= "border:1px solid black;"| 302
|
||||
|style= "border:1px solid black;"| [[File:SL_Kidnapping.png]]
|
||||
|style= "border:1px solid black;"| Kidnapping
|
||||
|style= "border:1px solid black;"| 10 minutes
|
||||
|style= "border:1px solid black;"| 16 minutes
|
||||
|style= "border:1px solid black;"| To unlawfully confine or restrict the free movement of an Employee against their will.
|
||||
|-
|
||||
| style="border:1px solid black;" | 303
|
||||
| style="border:1px solid black;" | WIP
|
||||
| style="border:1px solid black;" | Grand Possession
|
||||
| style="border:1px solid black;" | 12-20 minutes
|
||||
| style="border:1px solid black;" | 15-25 minutes
|
||||
| style="border:1px solid black;" | To be in the possession of a Category C, D, or E Controlled Armament.
|
||||
|-
|
||||
| style="border:1px solid black;" | 304
|
||||
| style="border:1px solid black;" | [[File:SL_Mindbreaking.png]]
|
||||
| style="border:1px solid black;" | Noöspheric Tampering
|
||||
| style="border:1px solid black;" | 8 minutes
|
||||
| style="border:1px solid black;" | 14 minutes
|
||||
| style="border:1px solid black;" | To maliciously tamper with or create noöspheric anomalies, entities, aberrations, or other elements; Or to otherwise deliberately perform actions that would raise Glimmer or other anomalous reading levels in a way that threatens normative reality.
|
||||
|-
|
||||
| style="border:1px solid black;" | 305
|
||||
| style="border:1px solid black;" | [[File:SL_Sabotage.png]]
|
||||
| style="border:1px solid black;" | Sabotage
|
||||
| style="border:1px solid black;" | 8 minutes
|
||||
| style="border:1px solid black;" | 14 minutes
|
||||
| style="border:1px solid black;" | To maliciously commit an act that, directly or indirectly, hinders the operation of a vessel of its part; or, to commit an act that modifies and/or damages technology or equipment one is not authorized to access.
|
||||
|-
|
||||
| style="border:1px solid black;" | 306
|
||||
| style="border:1px solid black;" | [[File:SL_AbuseOfPower.png]]
|
||||
| style="border:1px solid black;" | Abuse of Power
|
||||
| style="border:1px solid black;" | 10 minutes
|
||||
| style="border:1px solid black;" | 14 minutes
|
||||
| style="border:1px solid black;" | To intentionally misuse or wrongfully exercise one’s own authority, influence, or control, resulting in harm, unjust treatment, or demonstratable loss to another Employee.
|
||||
|-
|
||||
| style="border:1px solid black;" | 307
|
||||
| style="border:1px solid black;" | [[File:SL_GrandTheft.png]]
|
||||
| style="border:1px solid black;" | Grand Larceny
|
||||
| style="border:1px solid black;" | 8 minutes
|
||||
| style="border:1px solid black;" | 12 minutes
|
||||
| style="border:1px solid black;" | To deprive an Employee of any Controlled Item in their lawful possession.
|
||||
|-
|
||||
| style="border:1px solid black;" | 308
|
||||
| style="border:1px solid black;" | [[File:SL_BlackMarketeering.png]]
|
||||
| style="border:1px solid black;" | Black Marketeering
|
||||
| style="border:1px solid black;" | 8 minutes
|
||||
| style="border:1px solid black;" | 12 minutes
|
||||
| style="border:1px solid black;" | To sell, distribute, or otherwise circulate Controlled Items to unauthorized entities.
|
||||
|}
|
||||
----
|
||||
@@ -262,49 +262,49 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|
||||
| style="border: 1px solid black;" | 201
|
||||
| style="border: 1px solid black;" | [[File:SL_Assault.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Assault}}Assault
|
||||
| style="border: 1px solid black;" | 5 minutes
|
||||
| style="border: 1px solid black;" | 10 minutes
|
||||
| style="border: 1px solid black;" | To cause physical harm or to effect unwanted physical contact on an Employee, without the apparent intent to kill them, or to threaten such actions with both capability and intent to do so.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 202
|
||||
| style="border: 1px solid black;" | [[File:SL_BreakingAndEntering.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Breaking and Entering}}Breaking and Entering
|
||||
| style="border: 1px solid black;" | 5 minutes
|
||||
| style="border: 1px solid black;" | 10 minutes
|
||||
| style="border: 1px solid black;" | To break and enter into a high security area where one is not authorised nor invited, with intent to commit a crime within.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 203
|
||||
| style="border: 1px solid black;" | [[File:SL_Rioting.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Rioting}}Rioting
|
||||
| style="border: 1px solid black;" | 4 minutes
|
||||
| style="border: 1px solid black;" | 8 minutes
|
||||
| style="border: 1px solid black;" | To partake in an unauthorised riotous, tumultuous, and disruptive public assembly that refuses to disperse after warning.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 204
|
||||
| style="border: 1px solid black;" | [[File:SL_Endangerment.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Endangerment}}Endangerment
|
||||
| style="border: 1px solid black;" | 6 minutes
|
||||
| style="border: 1px solid black;" | 8 minutes
|
||||
| style="border: 1px solid black;" | To recklessly abandon obligations involving the continued wellbeing and/or protection of life and property, through malpractice, action, or inaction.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 205
|
||||
| style="border: 1px solid black;" | [[File:SL_Possession.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Possession}}Possession
|
||||
| style="border: 1px solid black;" | 3-8 minutes
|
||||
| style="border: 1px solid black;" | 5-10 minutes
|
||||
| style="border: 1px solid black;" | To be in unauthorised possession of restricted items or items of particular danger. See [[Standard_Operating_Procedure#Controlled_Substances/Items|Controlled Substances/Items]], [[Standard_Operating_Procedure#Controlled_Armaments|Controlled Armaments]].
|
||||
|-
|
||||
| style="border: 1px solid black;" | 206
|
||||
| style="border: 1px solid black;" | [[File:SL_ObstructionOfJustice.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Obstruction of Justice}}Obstruction of Justice
|
||||
| style="border: 1px solid black;" | 4 minutes
|
||||
| style="border: 1px solid black;" | 8 minutes
|
||||
| style="border: 1px solid black;" | To wilfully disobey, interfere with, or refuse a decree of the court, warrant, or arrest.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 207
|
||||
| style="border: 1px solid black;" | [[File:SL_PerjuryOrFalseReport.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Perjury or False Report}}Perjury or False Report
|
||||
| style="border: 1px solid black;" | 4 minutes
|
||||
| style="border: 1px solid black;" | 6 minutes
|
||||
| style="border: 1px solid black;" | To wilfully and maliciously tell an untruth either in court or in the process of making an actionable report to law enforcement.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 208
|
||||
| style="border: 1px solid black;" | [[File:SL_ContemptOfCourt.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Contempt of Court}}Contempt of Court
|
||||
| style="border: 1px solid black;" | 4 minutes
|
||||
| style="border: 1px solid black;" | 6 minutes
|
||||
| style="border: 1px solid black;" | To conduct oneself disruptively and disrespectfully before the court.
|
||||
|-
|
||||
|style= "border:1px solid black;"| 209
|
||||
@@ -329,13 +329,13 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|
||||
| style="border: 1px solid black;" | 101
|
||||
| style="border: 1px solid black;" | [[File:SL_AnimalCruelty.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Animal Cruelty}}Animal Cruelty
|
||||
| style="border: 1px solid black;" | 2 minutes
|
||||
| style="border: 1px solid black;" | 5 minutes
|
||||
| style="border: 1px solid black;" | To inflict unnecessary suffering on a Pet with malicious intent.
|
||||
|-
|
||||
|style= "border:1px solid black;"| 102
|
||||
|style= "border:1px solid black;"| WIP
|
||||
!style= "border:1px solid black;"| Harassment
|
||||
|style= "border:1px solid black;"| 3 minutes
|
||||
|style= "border:1px solid black;"| 5 minutes
|
||||
|style= "border:1px solid black;"| To verbally demean, humiliate, or harass an Employee with malice; or maliciously spread false information amongst a population.
|
||||
|-
|
||||
|style= "border:1px solid black;"| 103
|
||||
@@ -347,19 +347,19 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
|
||||
| style="border: 1px solid black;" | 104
|
||||
| style="border: 1px solid black;" | [[File:SL_Theft.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Theft}}Petty Larceny
|
||||
| style="border: 1px solid black;" | 2 minutes
|
||||
| style="border: 1px solid black;" | 4 minutes
|
||||
| style="border: 1px solid black;" | To deprive an Employee of an item in their lawful property without consent.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 105
|
||||
| style="border: 1px solid black;" | [[File:SL_Trespass.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Trespass}}Trespass
|
||||
| style="border: 1px solid black;" | 2 minutes
|
||||
| style="border: 1px solid black;" | 3 minutes
|
||||
| style="border: 1px solid black;" | To enter into an area where one is not authorised nor invited.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 106
|
||||
| style="border: 1px solid black;" | [[File:SL_Vandalism.png]]
|
||||
! style="border: 1px solid black;" | {{anchor|Vandalism}}Vandalism
|
||||
| style="border: 1px solid black;" | 2 minutes
|
||||
| style="border: 1px solid black;" | 3 minutes
|
||||
| style="border: 1px solid black;" | To deface or superficially damage public property, or property belonging to another Employee.
|
||||
|-
|
||||
| style="border: 1px solid black;" | 107
|
||||
|
||||
@@ -102,25 +102,25 @@ A list of such Substances follow:
|
||||
|
||||
Controlled Armaments are grouped into categories of increasing severity. If the offender possesses several categories of contraband, only the highest severity of said items should be charged.
|
||||
|
||||
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 3min brig and further legal action as the situation demands.''
|
||||
'''Category A:''' ''Illegal possession of Cat. A controlled armaments may incur a sentence of up to 5min brig and further legal action as the situation demands.''
|
||||
|
||||
* Any object or instrument specifically designed, adapted, or used to cause physical harm in close combat
|
||||
* Any object or equipment specifically designed or adapted to protect against physical harm caused by weapons, projectiles, or other forms of direct assault
|
||||
* Any object or instrument specifically designed or adapted to incapacitate or disorient by using light or sound without causing permanent injury
|
||||
* Any object or equipment specifically designed or adapted for explicitly training purposes that would otherwise fall under another category
|
||||
|
||||
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 8min brig and further legal action as the situation demands.''
|
||||
'''Category B:''' ''Illegal possession of Cat. B controlled armaments may incur a sentence of up to 10min brig and further legal action as the situation demands.''
|
||||
|
||||
* Any object or instrument specifically designed or adapted to cause physical harm in close combat, which employs a powered system to enhance performance
|
||||
* Any manually-operated ballistic arms that possess an internal magazine or feeding system, or lack an internal magazine or feeding system altogether
|
||||
|
||||
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 12min brig and further legal action as the situation demands.''
|
||||
'''Category C:''' ''Illegal possession of Cat. C controlled armaments may incur a sentence of up to 15min brig and further legal action as the situation demands.''
|
||||
|
||||
* Any manually-operated ballistic arms that use an external magazine or feeding system
|
||||
* Any semi-automatic ballistic arms not easily concealable within a pocket or coat
|
||||
* Any object or instrument specifically designed or adapted for explicitly less-lethal purposes that would otherwise fall under another category
|
||||
|
||||
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 15min brig and further legal action as the situation demands.''
|
||||
'''Category D:''' ''Illegal possession of Cat. D controlled armaments may incur a sentence of up to 20min brig and further legal action as the situation demands.''
|
||||
|
||||
* Any semi-automatic ballistic arms that are easily concealable
|
||||
* Any fully-automatic ballistic arms
|
||||
@@ -128,7 +128,7 @@ Controlled Armaments are grouped into categories of increasing severity. If the
|
||||
* Any object or device specifically designed or adapted to cause physical harm through detonation of an explosive charge
|
||||
* Any object or equipment specifically designed or adapted to protect against both a space environment and physical harm caused by weapons, projectiles, or other forms of direct assault
|
||||
|
||||
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 20min brig and further legal action as the situation demands.''
|
||||
'''Category E:''' ''Illegal possession of Cat. E controlled armaments may incur a sentence of up to 25min brig and further legal action as the situation demands.''
|
||||
|
||||
* Any crew-served or emplaced weapon
|
||||
* Any object or instrument specifically designed or adapted to launch large-calibre explosives or projectiles for offensive or defensive purposes
|
||||
|
||||
@@ -23,8 +23,6 @@ public class RaiseEventBenchmark
|
||||
PoolManager.Startup(typeof(BenchSystem).Assembly);
|
||||
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
|
||||
var entMan = _pair.Server.EntMan;
|
||||
var fact = _pair.Server.ResolveDependency<IComponentFactory>();
|
||||
var bus = (EntityEventBus)entMan.EventBus;
|
||||
_sys = entMan.System<BenchSystem>();
|
||||
|
||||
_pair.Server.WaitPost(() =>
|
||||
@@ -32,8 +30,6 @@ public class RaiseEventBenchmark
|
||||
var uid = entMan.Spawn();
|
||||
_sys.Ent = new(uid, entMan.GetComponent<TransformComponent>(uid));
|
||||
_sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp);
|
||||
_sys.NetId = fact.GetRegistration<TransformComponent>().NetID!.Value;
|
||||
_sys.EvSubs = bus.GetNetCompEventHandlers<BenchSystem.BenchEv>();
|
||||
})
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
@@ -64,12 +60,6 @@ public class RaiseEventBenchmark
|
||||
return _sys.RaiseICompEvent();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int RaiseNetEvent()
|
||||
{
|
||||
return _sys.RaiseNetIdEvent();
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public int RaiseCSharpEvent()
|
||||
{
|
||||
@@ -84,8 +74,6 @@ public class RaiseEventBenchmark
|
||||
public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev);
|
||||
|
||||
public event EntityEventHandler? OnCSharpEvent;
|
||||
public ushort NetId;
|
||||
internal EntityEventBus.DirectedEventHandler?[] EvSubs = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -104,7 +92,7 @@ public class RaiseEventBenchmark
|
||||
public int RaiseCompEvent()
|
||||
{
|
||||
var ev = new BenchEv();
|
||||
RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
|
||||
EntityManager.EventBus.RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
@@ -112,16 +100,7 @@ public class RaiseEventBenchmark
|
||||
{
|
||||
// Raise with an IComponent instead of concrete type
|
||||
var ev = new BenchEv();
|
||||
RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
public int RaiseNetIdEvent()
|
||||
{
|
||||
// Raise a "IComponent" event using a net-id index delegate array (for PVS & client game-state events)
|
||||
var ev = new BenchEv();
|
||||
ref var unitEv = ref Unsafe.As<BenchEv, EntityEventBus.Unit>(ref ev);
|
||||
EvSubs[NetId]?.Invoke(Ent2.Owner, Ent2.Comp, ref unitEv);
|
||||
EntityManager.EventBus.RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev);
|
||||
return ev.N;
|
||||
}
|
||||
|
||||
@@ -139,7 +118,6 @@ public class RaiseEventBenchmark
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
[ComponentEvent(Exclusive = false)]
|
||||
public struct BenchEv
|
||||
{
|
||||
public int N;
|
||||
|
||||
@@ -60,24 +60,8 @@ public sealed class ClientAlertsSystem : AlertsSystem
|
||||
if (args.Current is not AlertComponentState cast)
|
||||
return;
|
||||
|
||||
// Save all client-sided alerts to later put back in
|
||||
var clientAlerts = new Dictionary<AlertKey, AlertState>();
|
||||
foreach (var alert in alerts.Comp.Alerts)
|
||||
{
|
||||
if (alert.Key.AlertType != null && TryGet(alert.Key.AlertType.Value, out var alertProto))
|
||||
{
|
||||
if (alertProto.ClientHandled)
|
||||
clientAlerts[alert.Key] = alert.Value;
|
||||
}
|
||||
}
|
||||
|
||||
alerts.Comp.Alerts = new(cast.Alerts);
|
||||
|
||||
foreach (var alert in clientAlerts)
|
||||
{
|
||||
alerts.Comp.Alerts[alert.Key] = alert.Value;
|
||||
}
|
||||
|
||||
UpdateHud(alerts);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ using Content.Client.Atmos.Components;
|
||||
using Content.Client.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -24,7 +23,6 @@ namespace Content.Client.Atmos.Overlays
|
||||
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IMapManager _mapManager;
|
||||
private readonly SharedAtmosphereSystem _atmosphereSystem;
|
||||
private readonly SharedMapSystem _mapSystem;
|
||||
private readonly SharedTransformSystem _xformSys;
|
||||
|
||||
@@ -56,7 +54,6 @@ namespace Content.Client.Atmos.Overlays
|
||||
{
|
||||
_entManager = entManager;
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
_atmosphereSystem = entManager.System<SharedAtmosphereSystem>();
|
||||
_mapSystem = entManager.System<SharedMapSystem>();
|
||||
_xformSys = xformSys;
|
||||
_shader = protoMan.Index(UnshadedShader).Instance();
|
||||
@@ -70,7 +67,7 @@ namespace Content.Client.Atmos.Overlays
|
||||
|
||||
for (var i = 0; i < _gasCount; i++)
|
||||
{
|
||||
var gasPrototype = _atmosphereSystem.GetGas(system.VisibleGasId[i]);
|
||||
var gasPrototype = protoMan.Index<GasPrototype>(system.VisibleGasId[i].ToString());
|
||||
|
||||
SpriteSpecifier overlay;
|
||||
|
||||
|
||||
@@ -52,18 +52,14 @@ namespace Content.Client.Atmos.UI
|
||||
|
||||
private void OnSelectGasPressed()
|
||||
{
|
||||
if (_window is null)
|
||||
return;
|
||||
|
||||
if (_window is null) return;
|
||||
if (_window.SelectedGas is null)
|
||||
{
|
||||
SendMessage(new GasFilterSelectGasMessage(null));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Enum.TryParse<Gas>(_window.SelectedGas, out var gas))
|
||||
return;
|
||||
|
||||
if (!int.TryParse(_window.SelectedGas, out var gas)) return;
|
||||
SendMessage(new GasFilterSelectGasMessage(gas));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Client.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the a <see cref="CCVars.DebugQuickInspect"/> CVar to the name of a component, which allows the client to quickly open a VV window for that component
|
||||
/// by using the Alt+C or Alt+B hotkeys.
|
||||
/// </summary>
|
||||
public sealed class QuickInspectCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
|
||||
public override string Command => "quickinspect";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
_configurationManager.SetCVar(CCVars.DebugQuickInspect, args[0]);
|
||||
|
||||
var serverKey = _inputManager.GetKeyFunctionButtonString(ContentKeyFunctions.InspectServerComponent);
|
||||
var clientKey = _inputManager.GetKeyFunctionButtonString(ContentKeyFunctions.InspectClientComponent);
|
||||
shell.WriteLine(Loc.GetString($"cmd-quickinspect-success", ("component", args[0]), ("serverKeybind", serverKey), ("clientKeybind", clientKey)));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
// Not ideal since it only shows client-side components, but you can still type in any name you want.
|
||||
// If you know how to get server component names on the client then please fix this.
|
||||
var options = EntityManager.ComponentFactory.AllRegisteredTypes
|
||||
.Select(p => new CompletionOption(
|
||||
EntityManager.ComponentFactory.GetComponentName(p)
|
||||
));
|
||||
|
||||
return CompletionResult.FromOptions(options);
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
@@ -73,19 +73,7 @@ public sealed class PuddleSystem : SharedPuddleSystem
|
||||
// Maybe someday we'll have clientside prediction for entity spawning, but not today.
|
||||
// Until then, these methods do nothing on the client.
|
||||
/// <inheritdoc/>
|
||||
public override bool TrySplashSpillAt(Entity<SpillableComponent?> entity, EntityCoordinates coordinates, out EntityUid puddleUid, out Solution solution, bool sound = true, EntityUid? user = null)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
solution = new Solution();
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool TrySplashSpillAt(EntityUid entity,
|
||||
EntityCoordinates coordinates,
|
||||
Solution spilled,
|
||||
out EntityUid puddleUid,
|
||||
bool sound = true,
|
||||
EntityUid? user = null)
|
||||
public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
|
||||
{
|
||||
puddleUid = EntityUid.Invalid;
|
||||
return false;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.Fluids.EntitySystems;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Fluids;
|
||||
|
||||
public sealed class SpraySystem : SharedSpraySystem;
|
||||
@@ -3,7 +3,6 @@ using System.Numerics;
|
||||
using Content.Client.Clickable;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -14,7 +13,6 @@ using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Input;
|
||||
@@ -42,7 +40,6 @@ namespace Content.Client.Gameplay
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IViewVariablesManager _vvm = default!;
|
||||
[Dependency] private readonly IConsoleHost _conHost = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
private ClickableEntityComparer _comparer = default!;
|
||||
|
||||
@@ -86,8 +83,6 @@ namespace Content.Client.Gameplay
|
||||
_comparer = new ClickableEntityComparer();
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.InspectEntity, new PointerInputCmdHandler(HandleInspect, outsidePrediction: true))
|
||||
.Bind(ContentKeyFunctions.InspectServerComponent, new PointerInputCmdHandler(HandleInspectServerComponent, outsidePrediction: true))
|
||||
.Bind(ContentKeyFunctions.InspectClientComponent, new PointerInputCmdHandler(HandleInspectClientComponent, outsidePrediction: true))
|
||||
.Register<GameplayStateBase>();
|
||||
}
|
||||
|
||||
@@ -104,21 +99,6 @@ namespace Content.Client.Gameplay
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HandleInspectServerComponent(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
|
||||
{
|
||||
var component = _configurationManager.GetCVar(CCVars.DebugQuickInspect);
|
||||
if (_entityManager.TryGetNetEntity(uid, out var net))
|
||||
_conHost.ExecuteCommand($"vv /entity/{net.Value.Id}/{component}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HandleInspectClientComponent(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
|
||||
{
|
||||
var component = _configurationManager.GetCVar(CCVars.DebugQuickInspect);
|
||||
_conHost.ExecuteCommand($"vv /c/entity/{uid}/{component}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public EntityUid? GetClickedEntity(MapCoordinates coordinates)
|
||||
{
|
||||
return GetClickedEntity(coordinates, _eyeManager.CurrentEye);
|
||||
|
||||
@@ -38,8 +38,6 @@ namespace Content.Client.Input
|
||||
common.AddFunction(ContentKeyFunctions.ZoomIn);
|
||||
common.AddFunction(ContentKeyFunctions.ResetZoom);
|
||||
common.AddFunction(ContentKeyFunctions.InspectEntity);
|
||||
common.AddFunction(ContentKeyFunctions.InspectServerComponent);
|
||||
common.AddFunction(ContentKeyFunctions.InspectClientComponent);
|
||||
common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow);
|
||||
// Begin DeltaV Additions
|
||||
common.AddFunction(ContentKeyFunctions.OpenCHelp);
|
||||
|
||||
@@ -14,7 +14,6 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
using Content.Client._Impstation.ReadyManifest; // Impstation - Ready Manifest
|
||||
|
||||
namespace Content.Client.Lobby
|
||||
{
|
||||
@@ -32,7 +31,6 @@ namespace Content.Client.Lobby
|
||||
|
||||
private ClientGameTicker _gameTicker = default!;
|
||||
private ContentAudioSystem _contentAudioSystem = default!;
|
||||
private ReadyManifestSystem _readyManifest = default!; // Impstation - Ready Manifest
|
||||
|
||||
protected override Type? LinkedScreenType { get; } = typeof(LobbyGui);
|
||||
public LobbyGui? Lobby;
|
||||
@@ -50,7 +48,6 @@ namespace Content.Client.Lobby
|
||||
_gameTicker = _entityManager.System<ClientGameTicker>();
|
||||
_contentAudioSystem = _entityManager.System<ContentAudioSystem>();
|
||||
_contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo;
|
||||
_readyManifest = _entityManager.EntitySysManager.GetEntitySystem<ReadyManifestSystem>(); // Impstation - Ready Manifest
|
||||
|
||||
chatController.SetMainChat(true);
|
||||
|
||||
@@ -72,7 +69,6 @@ namespace Content.Client.Lobby
|
||||
Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
|
||||
Lobby.ReadyButton.OnPressed += OnReadyPressed;
|
||||
Lobby.ReadyButton.OnToggled += OnReadyToggled;
|
||||
Lobby.ManifestButton.OnPressed += OnManifestPressed; // Impstation - Ready Manifest
|
||||
|
||||
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
|
||||
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
|
||||
@@ -93,7 +89,6 @@ namespace Content.Client.Lobby
|
||||
Lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
|
||||
Lobby!.ReadyButton.OnPressed -= OnReadyPressed;
|
||||
Lobby!.ReadyButton.OnToggled -= OnReadyToggled;
|
||||
Lobby!.ManifestButton.OnPressed -= OnManifestPressed; // Impstation - Ready Manifest
|
||||
|
||||
Lobby = null;
|
||||
}
|
||||
@@ -125,12 +120,6 @@ namespace Content.Client.Lobby
|
||||
SetReady(args.Pressed);
|
||||
}
|
||||
|
||||
// Impstation - Ready Manifest
|
||||
private void OnManifestPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
_readyManifest.RequestReadyManifest();
|
||||
}
|
||||
|
||||
public override void FrameUpdate(FrameEventArgs e)
|
||||
{
|
||||
if (_gameTicker.IsGameStarted)
|
||||
@@ -193,7 +182,6 @@ namespace Content.Client.Lobby
|
||||
Lobby!.ReadyButton.ToggleMode = false;
|
||||
Lobby!.ReadyButton.Pressed = false;
|
||||
Lobby!.ObserveButton.Disabled = false;
|
||||
Lobby!.ManifestButton.Disabled = true; //imp
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -203,7 +191,6 @@ namespace Content.Client.Lobby
|
||||
Lobby!.ReadyButton.ToggleMode = true;
|
||||
Lobby!.ReadyButton.Disabled = false;
|
||||
Lobby!.ObserveButton.Disabled = true;
|
||||
Lobby!.ManifestButton.Disabled = false; // imp
|
||||
}
|
||||
|
||||
if (_gameTicker.ServerInfoBlob != null)
|
||||
|
||||
@@ -123,10 +123,10 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
|
||||
_profileEditor.RefreshSpecies();
|
||||
}
|
||||
|
||||
// if (obj.WasModified<TraitPrototype>()) // DeltaV - Refreshed in TraitsTab
|
||||
// {
|
||||
// _profileEditor.RefreshTraits();
|
||||
// }
|
||||
if (obj.WasModified<TraitPrototype>())
|
||||
{
|
||||
_profileEditor.RefreshTraits();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:ui="clr-namespace:Content.Client.Lobby.UI"
|
||||
xmlns:traits="clr-namespace:Content.Client._DV.Traits.UI"
|
||||
HorizontalExpand="True">
|
||||
<!-- Left side -->
|
||||
<BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
|
||||
@@ -134,15 +133,12 @@
|
||||
<BoxContainer Name="AntagList" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<!-- DV - Traits -->
|
||||
<!-- <BoxContainer Orientation="Vertical" Margin="10"> -->
|
||||
<!-- ~1~ Traits @1@ -->
|
||||
<!-- <ScrollContainer VerticalExpand="True"> -->
|
||||
<!-- <BoxContainer Name="TraitsList" Orientation="Vertical" /> -->
|
||||
<!-- </ScrollContainer> -->
|
||||
<!-- </BoxContainer> -->
|
||||
<traits:TraitsTab Name="Traits"/>
|
||||
<!-- End DV -->
|
||||
<BoxContainer Orientation="Vertical" Margin="10">
|
||||
<!-- Traits -->
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Name="TraitsList" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
|
||||
<!-- Markings -->
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
|
||||
@@ -39,7 +39,6 @@ using System.Globalization;
|
||||
using Content.Client._CD.Records.UI;
|
||||
using Content.Shared._CD.Records;
|
||||
// End CD - Character Records
|
||||
using Content.Shared._DV.Traits; // DV - Traits
|
||||
|
||||
namespace Content.Client.Lobby.UI
|
||||
{
|
||||
@@ -181,8 +180,6 @@ namespace Content.Client.Lobby.UI
|
||||
Save?.Invoke();
|
||||
};
|
||||
|
||||
Traits.OnTraitsChanged += OnTraitsSelectionChanged; // DeltaV
|
||||
|
||||
#region Left
|
||||
|
||||
#region Name
|
||||
@@ -242,6 +239,7 @@ namespace Content.Client.Lobby.UI
|
||||
{
|
||||
SpeciesButton.SelectId(args.Id);
|
||||
SetSpecies(_species[args.Id].ID);
|
||||
RefreshTraits(); // DeltaV - Allows for hiding traits
|
||||
UpdateHairPickers();
|
||||
OnSkinColorOnValueChanged();
|
||||
};
|
||||
@@ -464,7 +462,7 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
|
||||
|
||||
// RefreshTraits(); // DeltaV
|
||||
RefreshTraits();
|
||||
|
||||
#region Markings
|
||||
|
||||
@@ -517,58 +515,6 @@ namespace Content.Client.Lobby.UI
|
||||
IsDirty = false;
|
||||
}
|
||||
|
||||
// Begin DeltaV - Traits Integration
|
||||
/// <summary>
|
||||
/// Called when trait selection changes in the TraitsTab.
|
||||
/// Updates the profile with the new trait selection.
|
||||
/// </summary>
|
||||
private void OnTraitsSelectionChanged(HashSet<ProtoId<TraitPrototype>> traits)
|
||||
{
|
||||
if (Profile is null)
|
||||
return;
|
||||
|
||||
// Remove all existing traits - iterate directly over readonly collection
|
||||
foreach (var existingTrait in Profile.TraitPreferences)
|
||||
{
|
||||
Profile = Profile.WithoutTraitPreference(existingTrait, _prototypeManager);
|
||||
}
|
||||
|
||||
// Add newly selected traits
|
||||
foreach (var trait in traits)
|
||||
{
|
||||
Profile = Profile.WithTraitPreference(trait.Id, _prototypeManager);
|
||||
}
|
||||
|
||||
SetDirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the traits tab with the current profile's selected traits.
|
||||
/// </summary>
|
||||
private void UpdateTraitsSelection()
|
||||
{
|
||||
if (Profile is null)
|
||||
{
|
||||
Traits.SetSelectedTraits(new HashSet<ProtoId<TraitPrototype>>());
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert profile's trait preferences (strings) to ProtoId<TraitPrototype>
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>(Profile.TraitPreferences.Count);
|
||||
foreach (var traitId in Profile.TraitPreferences)
|
||||
{
|
||||
// Validate that the trait still exists in prototypes
|
||||
if (_prototypeManager.HasIndex(traitId))
|
||||
{
|
||||
selectedTraits.Add(new ProtoId<TraitPrototype>(traitId));
|
||||
}
|
||||
}
|
||||
|
||||
Traits.SetSelectedTraits(selectedTraits);
|
||||
Traits.UpdateConditions(Profile);
|
||||
}
|
||||
// End DeltaV - Traits Integration
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the flavor text editor status.
|
||||
/// </summary>
|
||||
@@ -603,122 +549,122 @@ namespace Content.Client.Lobby.UI
|
||||
/// <summary>
|
||||
/// Refreshes traits selector
|
||||
/// </summary>
|
||||
// public void RefreshTraits()
|
||||
// {
|
||||
// TraitsList.RemoveAllChildren();
|
||||
//
|
||||
// var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
|
||||
// TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
||||
//
|
||||
// if (traits.Count < 1)
|
||||
// {
|
||||
// TraitsList.AddChild(new Label
|
||||
// {
|
||||
// Text = Loc.GetString("humanoid-profile-editor-no-traits"),
|
||||
// FontColorOverride = Color.Gray,
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Setup model
|
||||
// Dictionary<string, List<string>> traitGroups = new();
|
||||
// List<string> defaultTraits = new();
|
||||
// traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
|
||||
//
|
||||
// foreach (var trait in traits)
|
||||
// {
|
||||
// // Begin DeltaV Additions - Species trait exclusion
|
||||
// if (Profile?.Species is { } selectedSpecies && trait.ExcludedSpecies.Contains(selectedSpecies))
|
||||
// {
|
||||
// Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||
// continue;
|
||||
// }
|
||||
// // End DeltaV Additions
|
||||
//
|
||||
// if (trait.Category == null)
|
||||
// {
|
||||
// defaultTraits.Add(trait.ID);
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// if (!_prototypeManager.HasIndex(trait.Category))
|
||||
// continue;
|
||||
//
|
||||
// var group = traitGroups.GetOrNew(trait.Category);
|
||||
// group.Add(trait.ID);
|
||||
// }
|
||||
//
|
||||
// // Create UI view from model
|
||||
// foreach (var (categoryId, categoryTraits) in traitGroups)
|
||||
// {
|
||||
// TraitCategoryPrototype? category = null;
|
||||
//
|
||||
// if (categoryId != TraitCategoryPrototype.Default)
|
||||
// {
|
||||
// category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
|
||||
// // Label
|
||||
// TraitsList.AddChild(new Label
|
||||
// {
|
||||
// Text = Loc.GetString(category.Name),
|
||||
// Margin = new Thickness(0, 10, 0, 0),
|
||||
// StyleClasses = { StyleClass.LabelHeading },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// List<TraitPreferenceSelector?> selectors = new();
|
||||
// var selectionCount = 0;
|
||||
//
|
||||
// foreach (var traitProto in categoryTraits)
|
||||
// {
|
||||
// var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
|
||||
// var selector = new TraitPreferenceSelector(trait);
|
||||
//
|
||||
// selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
|
||||
// if (selector.Preference)
|
||||
// selectionCount += trait.Cost;
|
||||
//
|
||||
// selector.PreferenceChanged += preference =>
|
||||
// {
|
||||
// if (preference)
|
||||
// {
|
||||
// Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||
// }
|
||||
//
|
||||
// SetDirty();
|
||||
// RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
|
||||
// };
|
||||
// selectors.Add(selector);
|
||||
// }
|
||||
//
|
||||
// // Selection counter
|
||||
// if (category is { MaxTraitPoints: >= 0 })
|
||||
// {
|
||||
// TraitsList.AddChild(new Label
|
||||
// {
|
||||
// Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
|
||||
// FontColorOverride = Color.Gray
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// foreach (var selector in selectors)
|
||||
// {
|
||||
// if (selector == null)
|
||||
// continue;
|
||||
//
|
||||
// if (category is { MaxTraitPoints: >= 0 } &&
|
||||
// selector.Cost + selectionCount > category.MaxTraitPoints)
|
||||
// {
|
||||
// selector.Checkbox.Label.FontColorOverride = Color.Red;
|
||||
// }
|
||||
//
|
||||
// TraitsList.AddChild(selector);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
public void RefreshTraits()
|
||||
{
|
||||
TraitsList.RemoveAllChildren();
|
||||
|
||||
var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
|
||||
TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
|
||||
|
||||
if (traits.Count < 1)
|
||||
{
|
||||
TraitsList.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("humanoid-profile-editor-no-traits"),
|
||||
FontColorOverride = Color.Gray,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup model
|
||||
Dictionary<string, List<string>> traitGroups = new();
|
||||
List<string> defaultTraits = new();
|
||||
traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
|
||||
|
||||
foreach (var trait in traits)
|
||||
{
|
||||
// Begin DeltaV Additions - Species trait exclusion
|
||||
if (Profile?.Species is { } selectedSpecies && trait.ExcludedSpecies.Contains(selectedSpecies))
|
||||
{
|
||||
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||
continue;
|
||||
}
|
||||
// End DeltaV Additions
|
||||
|
||||
if (trait.Category == null)
|
||||
{
|
||||
defaultTraits.Add(trait.ID);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_prototypeManager.HasIndex(trait.Category))
|
||||
continue;
|
||||
|
||||
var group = traitGroups.GetOrNew(trait.Category);
|
||||
group.Add(trait.ID);
|
||||
}
|
||||
|
||||
// Create UI view from model
|
||||
foreach (var (categoryId, categoryTraits) in traitGroups)
|
||||
{
|
||||
TraitCategoryPrototype? category = null;
|
||||
|
||||
if (categoryId != TraitCategoryPrototype.Default)
|
||||
{
|
||||
category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
|
||||
// Label
|
||||
TraitsList.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString(category.Name),
|
||||
Margin = new Thickness(0, 10, 0, 0),
|
||||
StyleClasses = { StyleClass.LabelHeading },
|
||||
});
|
||||
}
|
||||
|
||||
List<TraitPreferenceSelector?> selectors = new();
|
||||
var selectionCount = 0;
|
||||
|
||||
foreach (var traitProto in categoryTraits)
|
||||
{
|
||||
var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
|
||||
var selector = new TraitPreferenceSelector(trait);
|
||||
|
||||
selector.Preference = Profile?.TraitPreferences.Contains(trait.ID) == true;
|
||||
if (selector.Preference)
|
||||
selectionCount += trait.Cost;
|
||||
|
||||
selector.PreferenceChanged += preference =>
|
||||
{
|
||||
if (preference)
|
||||
{
|
||||
Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
|
||||
}
|
||||
|
||||
SetDirty();
|
||||
RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
|
||||
};
|
||||
selectors.Add(selector);
|
||||
}
|
||||
|
||||
// Selection counter
|
||||
if (category is { MaxTraitPoints: >= 0 })
|
||||
{
|
||||
TraitsList.AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("humanoid-profile-editor-trait-count-hint", ("current", selectionCount) ,("max", category.MaxTraitPoints)),
|
||||
FontColorOverride = Color.Gray
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var selector in selectors)
|
||||
{
|
||||
if (selector == null)
|
||||
continue;
|
||||
|
||||
if (category is { MaxTraitPoints: >= 0 } &&
|
||||
selector.Cost + selectionCount > category.MaxTraitPoints)
|
||||
{
|
||||
selector.Checkbox.Label.FontColorOverride = Color.Red;
|
||||
}
|
||||
|
||||
TraitsList.AddChild(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the species selector.
|
||||
@@ -899,13 +845,11 @@ namespace Content.Client.Lobby.UI
|
||||
_recordsTab.Update(profile);
|
||||
// End CD - Character Records
|
||||
|
||||
UpdateTraitsSelection(); // DeltaV - Traits
|
||||
|
||||
RefreshAntags();
|
||||
RefreshJobs();
|
||||
RefreshLoadouts();
|
||||
RefreshSpecies();
|
||||
// RefreshTraits(); // DeltaV
|
||||
RefreshTraits();
|
||||
RefreshFlavorText();
|
||||
ReloadPreview();
|
||||
|
||||
|
||||
@@ -36,10 +36,6 @@
|
||||
Align="Left"
|
||||
FontColorOverride="{x:Static maths:Color.DarkGray}"
|
||||
StyleClasses="LabelBig" HorizontalExpand="True" />
|
||||
<!-- Imp manifest button -->
|
||||
<Button Name="ManifestButton" Access="Public"
|
||||
Text="Manifest"
|
||||
StyleClasses="ButtonBig" MinWidth="137" />
|
||||
<Button Name="ReadyButton" Access="Public" ToggleMode="True"
|
||||
Text="{Loc 'ui-lobby-ready-up-button'}"
|
||||
StyleClasses="ButtonBig" MinWidth="137" />
|
||||
|
||||
@@ -19,24 +19,22 @@ public sealed partial class TraitPreferenceSelector : Control
|
||||
|
||||
public event Action<bool>? PreferenceChanged;
|
||||
|
||||
// DeltaV - This whole control is generally unused but the compiler wouldn't compile if I removed it
|
||||
// So I'm just gonna comment this part out
|
||||
// public TraitPreferenceSelector(TraitPrototype trait)
|
||||
// {
|
||||
// RobustXamlLoader.Load(this);
|
||||
//
|
||||
// var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
|
||||
// text += Loc.GetString(trait.Name);
|
||||
//
|
||||
// Cost = trait.Cost;
|
||||
// Checkbox.Text = text;
|
||||
// Checkbox.OnToggled += OnCheckBoxToggled;
|
||||
//
|
||||
// if (trait.Description is { } desc)
|
||||
// {
|
||||
// Checkbox.ToolTip = Loc.GetString(desc);
|
||||
// }
|
||||
// }
|
||||
public TraitPreferenceSelector(TraitPrototype trait)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var text = trait.Cost != 0 ? $"[{trait.Cost}] " : "";
|
||||
text += Loc.GetString(trait.Name);
|
||||
|
||||
Cost = trait.Cost;
|
||||
Checkbox.Text = text;
|
||||
Checkbox.OnToggled += OnCheckBoxToggled;
|
||||
|
||||
if (trait.Description is { } desc)
|
||||
{
|
||||
Checkbox.ToolTip = Loc.GetString(desc);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Client._DV.UserInterfaces.BuildInfo; // DeltaV - More info in Escape Menu
|
||||
using Content.Client.Credits; // DeltaV - More info in Escape Menu
|
||||
using Content.Client.MainMenu.UI;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
using Robust.Client;
|
||||
@@ -50,8 +48,6 @@ namespace Content.Client.MainMenu
|
||||
_mainMenuControl.DirectConnectButton.OnPressed += DirectConnectButtonPressed;
|
||||
_mainMenuControl.AddressBox.OnTextEntered += AddressBoxEntered;
|
||||
_mainMenuControl.ChangelogButton.OnPressed += ChangelogButtonPressed;
|
||||
_mainMenuControl.BuildInfoButton.OnPressed += BuildInfoButtonPressed; // DeltaV - More info in Escape Menu
|
||||
_mainMenuControl.CreditsButton.OnPressed += CreditsButtonPressed; // DeltaV - More info in Escape Menu
|
||||
|
||||
_client.RunLevelChanged += RunLevelChanged;
|
||||
}
|
||||
@@ -70,18 +66,6 @@ namespace Content.Client.MainMenu
|
||||
_userInterfaceManager.GetUIController<ChangelogUIController>().ToggleWindow();
|
||||
}
|
||||
|
||||
// Begin DeltaV - More info in Escape Menu
|
||||
private void BuildInfoButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
_userInterfaceManager.GetUIController<BuildInfoUIController>().ToggleWindow();
|
||||
}
|
||||
|
||||
private void CreditsButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
new CreditsWindow().Open();
|
||||
}
|
||||
// End DeltaV - More info in Escape Menu
|
||||
|
||||
private void OptionsButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
_userInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
|
||||
|
||||
@@ -48,18 +48,5 @@
|
||||
<clog:ChangelogButton
|
||||
Name="ChangelogButton"
|
||||
Access="Public"/>
|
||||
<!-- Begin DeltaV - More info in Escape Menu -->
|
||||
<Button
|
||||
Name="CreditsButton"
|
||||
Access="Public"
|
||||
Text="{Loc server-info-credits-button}"
|
||||
TextAlign="Center" />
|
||||
<Button
|
||||
Name="BuildInfoButton"
|
||||
Access="Public"
|
||||
Text="{Loc build-info-button}"
|
||||
ToolTip="{Loc build-info-tooltip}"
|
||||
TextAlign="Center" />
|
||||
<!-- End DeltaV - More info in Escape Menu -->
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using Content.Shared.NameIdentifier;
|
||||
|
||||
namespace Content.Client.NameIdentifier;
|
||||
|
||||
public sealed class NameIdentifierSystem : SharedNameIdentifierSystem;
|
||||
@@ -17,10 +17,5 @@
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
|
||||
<Button Access="Public" Name="DisconnectButton" Text="{Loc 'ui-escape-disconnect'}" />
|
||||
<Button Access="Public" Name="QuitButton" Text="{Loc 'ui-escape-quit'}" StyleClasses="ButtonColorRed" />
|
||||
<!-- Begin DeltaV - More info in Escape Menu -->
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
|
||||
<Button Access="Public" Name="CreditsButton" Text="{Loc server-info-credits-button}" />
|
||||
<Button Access="Public" Name="BuildInfoButton" Text="{Loc build-info-button}" ToolTip="{Loc build-info-tooltip}" />
|
||||
<!-- End DeltaV - More info in Escape Menu -->
|
||||
</BoxContainer>
|
||||
</ui1:EscapeMenu>
|
||||
|
||||
@@ -292,8 +292,6 @@ namespace Content.Client.Options.UI.Tabs
|
||||
AddButton(EngineKeyFunctions.ShowDebugMonitors);
|
||||
AddButton(EngineKeyFunctions.HideUI);
|
||||
AddButton(ContentKeyFunctions.InspectEntity);
|
||||
AddButton(ContentKeyFunctions.InspectServerComponent);
|
||||
AddButton(ContentKeyFunctions.InspectClientComponent);
|
||||
|
||||
AddHeader("ui-options-header-text-cursor");
|
||||
AddButton(EngineKeyFunctions.TextCursorLeft);
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<!-- Power On/Off -->
|
||||
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
|
||||
StyleClasses="highlight" MinWidth="120"/>
|
||||
<BoxContainer Orientation="Horizontal" MinWidth="150">
|
||||
<BoxContainer Orientation="Horizontal" MinWidth="90">
|
||||
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
|
||||
</BoxContainer>
|
||||
<!--Charging Status-->
|
||||
|
||||
@@ -40,14 +40,7 @@ namespace Content.Client.Power.APC.UI
|
||||
|
||||
if (PowerLabel != null)
|
||||
{
|
||||
if (castState.Tripped)
|
||||
{
|
||||
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-tripped");
|
||||
}
|
||||
else
|
||||
{
|
||||
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power), ("maxLoad", castState.MaxLoad));
|
||||
}
|
||||
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power));
|
||||
}
|
||||
|
||||
if (ExternalPowerStateLabel != null)
|
||||
|
||||
@@ -18,9 +18,7 @@ public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequir
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.Silent)
|
||||
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
|
||||
|
||||
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
|
||||
namespace Content.Client.Power.EntitySystems;
|
||||
|
||||
public sealed class ChargerSystem : SharedChargerSystem;
|
||||
@@ -0,0 +1,67 @@
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.PowerCell.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class PowerCellSystem : SharedPowerCellSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PowerCellVisualsComponent, AppearanceChangeEvent>(OnPowerCellVisualsChange);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null,
|
||||
EntityUid? user = null)
|
||||
{
|
||||
if (!Resolve(uid, ref battery, ref cell, false))
|
||||
return true;
|
||||
|
||||
return battery.CanUse;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool HasDrawCharge(
|
||||
EntityUid uid,
|
||||
PowerCellDrawComponent? battery = null,
|
||||
PowerCellSlotComponent? cell = null,
|
||||
EntityUid? user = null)
|
||||
{
|
||||
if (!Resolve(uid, ref battery, ref cell, false))
|
||||
return true;
|
||||
|
||||
return battery.CanDraw;
|
||||
}
|
||||
|
||||
private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
|
||||
return;
|
||||
|
||||
// If no appearance data is set, rely on whatever existing sprite state is set being correct.
|
||||
if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
|
||||
return;
|
||||
|
||||
var positiveCharge = level > 0;
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);
|
||||
|
||||
if (positiveCharge)
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), PowerCellVisualLayers.Unshaded, $"o{level}");
|
||||
}
|
||||
|
||||
private enum PowerCellVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
Unshaded,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
/// <summary>
|
||||
/// Sprite layers for power cells.
|
||||
/// For use with the generic visualizer.
|
||||
/// </summary>
|
||||
public enum PowerCellVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
Unshaded,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class PowerCellVisualsComponent : Component {}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power;
|
||||
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
@@ -9,13 +9,15 @@ public sealed partial class PowerChargerVisualsComponent : Component
|
||||
/// <summary>
|
||||
/// The base sprite state used if the power cell charger does not contain a power cell.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[DataField("emptyState")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string EmptyState = "empty";
|
||||
|
||||
/// <summary>
|
||||
/// The base sprite state used if the power cell charger contains a power cell.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[DataField("occupiedState")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string OccupiedState = "full";
|
||||
|
||||
/// <summary>
|
||||
@@ -25,7 +27,8 @@ public sealed partial class PowerChargerVisualsComponent : Component
|
||||
/// <see cref="CellChargerStatus.Charging"/> Maps to the state used when the charger is charging a power cell.
|
||||
/// <see cref="CellChargerStatus.Charged"/> Maps to the state used when the charger contains a fully charged power cell.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[DataField("lightStates")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Dictionary<CellChargerStatus, string> LightStates = new()
|
||||
{
|
||||
[CellChargerStatus.Off] = "light-off",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
|
||||
[Dependency] private readonly IPlacementManager _placementManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly HandsSystem _hands = default!;
|
||||
|
||||
|
||||
private Direction _placementDirection = default;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -42,11 +42,6 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
|
||||
|
||||
var heldEntity = _hands.GetActiveItem(player);
|
||||
|
||||
// Don't open the placement overlay for client-side RCDs.
|
||||
// This may happen when predictively spawning one in your hands.
|
||||
if (heldEntity != null && IsClientSide(heldEntity.Value))
|
||||
return;
|
||||
|
||||
if (!TryComp<RCDComponent>(heldEntity, out var rcd))
|
||||
{
|
||||
// If the player was holding an RCD, but is no longer, cancel placement
|
||||
@@ -74,7 +69,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
|
||||
MobUid = heldEntity.Value,
|
||||
PlacementOption = PlacementMode,
|
||||
EntityType = prototype.Prototype,
|
||||
Range = (int)Math.Ceiling(SharedInteractionSystem.InteractionRange),
|
||||
Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
|
||||
IsTile = (prototype.Mode == RcdMode.ConstructTile),
|
||||
UseEditorContext = false,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared._DV.Silicons.Borgs; // DeltaV
|
||||
using Content.Shared.Silicons.Borgs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Silicons.Borgs;
|
||||
@@ -24,31 +25,32 @@ public sealed class BorgBoundUserInterface : BoundUserInterface
|
||||
|
||||
_menu.BrainButtonPressed += () =>
|
||||
{
|
||||
SendPredictedMessage(new BorgEjectBrainBuiMessage());
|
||||
SendMessage(new BorgEjectBrainBuiMessage());
|
||||
};
|
||||
_menu.IdChipButtonPressed += () => SendMessage(new BorgEjectIdChipMessage()); // DeltaV
|
||||
|
||||
_menu.EjectBatteryButtonPressed += () =>
|
||||
{
|
||||
SendPredictedMessage(new BorgEjectBatteryBuiMessage());
|
||||
SendMessage(new BorgEjectBatteryBuiMessage());
|
||||
};
|
||||
|
||||
_menu.NameChanged += name =>
|
||||
{
|
||||
SendPredictedMessage(new BorgSetNameBuiMessage(name));
|
||||
SendMessage(new BorgSetNameBuiMessage(name));
|
||||
};
|
||||
|
||||
_menu.RemoveModuleButtonPressed += module =>
|
||||
{
|
||||
SendPredictedMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
|
||||
SendMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
|
||||
};
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
_menu?.UpdateBatteryButton();
|
||||
_menu?.UpdateBrainButton();
|
||||
_menu?.UpdateIdChipButton(); // DeltaV
|
||||
_menu?.UpdateModulePanel();
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not BorgBuiState msg)
|
||||
return;
|
||||
_menu?.UpdateState(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ using Content.Shared.Access.Components; // DeltaV
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.NameIdentifier;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Silicons.Borgs;
|
||||
using Content.Shared.Silicons.Borgs.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -22,8 +22,6 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
private readonly NameModifierSystem _nameModifier;
|
||||
private readonly PowerCellSystem _powerCell;
|
||||
private readonly SharedBatterySystem _battery;
|
||||
|
||||
public Action? BrainButtonPressed;
|
||||
public Action? IdChipButtonPressed; // DeltaV
|
||||
@@ -46,8 +44,6 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_nameModifier = _entity.System<NameModifierSystem>();
|
||||
_powerCell = _entity.System<PowerCellSystem>();
|
||||
_battery = _entity.System<SharedBatterySystem>();
|
||||
|
||||
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
|
||||
|
||||
@@ -68,6 +64,8 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
NameLineEdit.OnTextChanged += OnNameChanged;
|
||||
NameLineEdit.OnTextEntered += OnNameEntered;
|
||||
NameLineEdit.OnFocusExit += OnNameFocusExit;
|
||||
|
||||
UpdateBrainButton();
|
||||
}
|
||||
|
||||
public void SetEntity(EntityUid entity)
|
||||
@@ -88,10 +86,6 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
NameIdentifierLabel.Visible = false;
|
||||
NameLineEdit.Text = _entity.GetComponent<MetaDataComponent>(Entity).EntityName;
|
||||
}
|
||||
|
||||
UpdateBatteryButton();
|
||||
UpdateBrainButton();
|
||||
UpdateModulePanel();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
@@ -99,24 +93,22 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
base.FrameUpdate(args);
|
||||
|
||||
AccumulatedTime += args.DeltaSeconds;
|
||||
BorgSprite.OverrideDirection = (Direction)((int)AccumulatedTime % 4 * 2);
|
||||
|
||||
var chargeFraction = 0f;
|
||||
|
||||
if (_powerCell.TryGetBatteryFromSlot(Entity, out var battery))
|
||||
chargeFraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
|
||||
|
||||
ChargeBar.Value = chargeFraction;
|
||||
ChargeLabel.Text = Loc.GetString("borg-ui-charge-label",
|
||||
("charge", (int)MathF.Round(chargeFraction * 100)));
|
||||
BorgSprite.OverrideDirection = (Direction) ((int) AccumulatedTime % 4 * 2);
|
||||
}
|
||||
|
||||
public void UpdateBatteryButton()
|
||||
public void UpdateState(BorgBuiState state)
|
||||
{
|
||||
EjectBatteryButton.Disabled = !_powerCell.HasBattery(Entity);
|
||||
EjectBatteryButton.Disabled = !state.HasBattery;
|
||||
ChargeBar.Value = state.ChargePercent;
|
||||
ChargeLabel.Text = Loc.GetString("borg-ui-charge-label",
|
||||
("charge", (int) MathF.Round(state.ChargePercent * 100)));
|
||||
|
||||
UpdateBrainButton();
|
||||
UpdateIdChipButton(); // DeltaV
|
||||
UpdateModulePanel();
|
||||
}
|
||||
|
||||
public void UpdateBrainButton()
|
||||
private void UpdateBrainButton()
|
||||
{
|
||||
if (_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis) && chassis.BrainEntity is { } brain)
|
||||
{
|
||||
@@ -138,7 +130,7 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
/// <summary>
|
||||
/// DeltaV: Updates the Eject ID Chip button text and enabled status.
|
||||
/// </summary>
|
||||
public void UpdateIdChipButton()
|
||||
private void UpdateIdChipButton()
|
||||
{
|
||||
if (!_entity.TryGetComponent<IdChipSlotComponent>(Entity, out var comp))
|
||||
{
|
||||
@@ -159,7 +151,7 @@ public sealed partial class BorgMenu : FancyWindow
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateModulePanel()
|
||||
private void UpdateModulePanel()
|
||||
{
|
||||
if (!_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis))
|
||||
return;
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
using Content.Shared.PowerCell.Components;
|
||||
using Content.Shared.Silicons.Borgs.Components;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Silicons.Borgs;
|
||||
|
||||
public sealed partial class BorgSystem
|
||||
{
|
||||
// How often to update the battery alert.
|
||||
// Also gets updated instantly when switching bodies or a battery is inserted or removed.
|
||||
private static readonly TimeSpan AlertUpdateDelay = TimeSpan.FromSeconds(0.5f);
|
||||
|
||||
// Don't put this on the component because we only need to track the time for a single entity
|
||||
// and we don't want to TryComp it every single tick.
|
||||
private TimeSpan _nextAlertUpdate = TimeSpan.Zero;
|
||||
private EntityQuery<BorgChassisComponent> _chassisQuery;
|
||||
private EntityQuery<PowerCellSlotComponent> _slotQuery;
|
||||
|
||||
public void InitializeBattery()
|
||||
{
|
||||
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
_chassisQuery = GetEntityQuery<BorgChassisComponent>();
|
||||
_slotQuery = GetEntityQuery<PowerCellSlotComponent>();
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(Entity<BorgChassisComponent> ent, ref LocalPlayerAttachedEvent args)
|
||||
{
|
||||
UpdateBatteryAlert((ent.Owner, ent.Comp, null));
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(Entity<BorgChassisComponent> ent, ref LocalPlayerDetachedEvent args)
|
||||
{
|
||||
// Remove all borg related alerts.
|
||||
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
|
||||
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
|
||||
}
|
||||
|
||||
private void UpdateBatteryAlert(Entity<BorgChassisComponent, PowerCellSlotComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp2, false))
|
||||
return;
|
||||
|
||||
if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, ent.Comp2), out var battery))
|
||||
{
|
||||
_alerts.ShowAlert(ent.Owner, ent.Comp1.NoBatteryAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alert levels from 0 to 10.
|
||||
var chargeLevel = (short)MathF.Round(_battery.GetChargeLevel(battery.Value.AsNullable()) * 10f);
|
||||
|
||||
// we make sure 0 only shows if they have absolutely no battery.
|
||||
// also account for floating point imprecision
|
||||
if (chargeLevel == 0 && _powerCell.HasDrawCharge((ent.Owner, null, ent.Comp2)))
|
||||
{
|
||||
chargeLevel = 1;
|
||||
}
|
||||
|
||||
_alerts.ShowAlert(ent.Owner, ent.Comp1.BatteryAlert, chargeLevel);
|
||||
}
|
||||
|
||||
// Periodically update the charge indicator.
|
||||
// We do this with a client-side alert so that we don't have to network the charge level.
|
||||
public void UpdateBattery(float frameTime)
|
||||
{
|
||||
if (_player.LocalEntity is not { } localPlayer)
|
||||
return;
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
|
||||
if (curTime < _nextAlertUpdate)
|
||||
return;
|
||||
|
||||
_nextAlertUpdate = curTime + AlertUpdateDelay;
|
||||
|
||||
if (!_chassisQuery.TryComp(localPlayer, out var chassis) || !_slotQuery.TryComp(localPlayer, out var slot))
|
||||
return;
|
||||
|
||||
UpdateBatteryAlert((localPlayer, chassis, slot));
|
||||
}
|
||||
}
|
||||
@@ -1,93 +1,72 @@
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Silicons.Borgs;
|
||||
using Content.Shared.Silicons.Borgs.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Silicons.Borgs;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed partial class BorgSystem : SharedBorgSystem
|
||||
public sealed class BorgSystem : SharedBorgSystem
|
||||
{
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private readonly SharedBatterySystem _battery = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
InitializeBattery();
|
||||
|
||||
SubscribeLocalEvent<BorgChassisComponent, AppearanceChangeEvent>(OnBorgAppearanceChanged);
|
||||
SubscribeLocalEvent<MMIComponent, AppearanceChangeEvent>(OnMMIAppearanceChanged);
|
||||
}
|
||||
|
||||
public override void UpdateUI(Entity<BorgChassisComponent?> chassis)
|
||||
{
|
||||
if (_ui.TryGetOpenUi(chassis.Owner, BorgUiKey.Key, out var bui))
|
||||
bui.Update();
|
||||
}
|
||||
|
||||
private void OnBorgAppearanceChanged(Entity<BorgChassisComponent> chassis, ref AppearanceChangeEvent args)
|
||||
private void OnBorgAppearanceChanged(EntityUid uid, BorgChassisComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
UpdateBorgAppearance((chassis.Owner, chassis.Comp, args.Component, args.Sprite));
|
||||
UpdateBorgAppearance(uid, component, args.Component, args.Sprite);
|
||||
}
|
||||
|
||||
protected override void OnInserted(Entity<BorgChassisComponent> chassis, ref EntInsertedIntoContainerMessage args)
|
||||
protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (!chassis.Comp.Initialized)
|
||||
if (!component.Initialized)
|
||||
return;
|
||||
|
||||
base.OnInserted(chassis, ref args);
|
||||
UpdateUI(chassis.AsNullable());
|
||||
UpdateBorgAppearance((chassis, chassis.Comp));
|
||||
UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
|
||||
base.OnInserted(uid, component, args);
|
||||
UpdateBorgAppearance(uid, component);
|
||||
}
|
||||
|
||||
protected override void OnRemoved(Entity<BorgChassisComponent> chassis, ref EntRemovedFromContainerMessage args)
|
||||
protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if (!chassis.Comp.Initialized)
|
||||
if (!component.Initialized)
|
||||
return;
|
||||
|
||||
base.OnRemoved(chassis, ref args);
|
||||
UpdateUI(chassis.AsNullable());
|
||||
UpdateBorgAppearance((chassis, chassis.Comp));
|
||||
UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
|
||||
base.OnRemoved(uid, component, args);
|
||||
UpdateBorgAppearance(uid, component);
|
||||
}
|
||||
|
||||
private void UpdateBorgAppearance(Entity<BorgChassisComponent?, AppearanceComponent?, SpriteComponent?> ent)
|
||||
private void UpdateBorgAppearance(EntityUid uid,
|
||||
BorgChassisComponent? component = null,
|
||||
AppearanceComponent? appearance = null,
|
||||
SpriteComponent? sprite = null)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, ref ent.Comp3))
|
||||
if (!Resolve(uid, ref component, ref appearance, ref sprite))
|
||||
return;
|
||||
|
||||
if (_appearance.TryGetData<MobState>(ent.Owner, MobStateVisuals.State, out var state, ent.Comp2))
|
||||
if (_appearance.TryGetData<MobState>(uid, MobStateVisuals.State, out var state, appearance))
|
||||
{
|
||||
if (state != MobState.Alive)
|
||||
{
|
||||
_sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, false);
|
||||
_sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_appearance.TryGetData<bool>(ent.Owner, BorgVisuals.HasPlayer, out var hasPlayer, ent.Comp2))
|
||||
if (!_appearance.TryGetData<bool>(uid, BorgVisuals.HasPlayer, out var hasPlayer, appearance))
|
||||
hasPlayer = false;
|
||||
|
||||
_sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, ent.Comp1.BrainEntity != null || hasPlayer);
|
||||
_sprite.LayerSetRsiState((ent.Owner, ent.Comp3), BorgVisualLayers.Light, hasPlayer ? ent.Comp1.HasMindState : ent.Comp1.NoMindState);
|
||||
_sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, component.BrainEntity != null || hasPlayer);
|
||||
_sprite.LayerSetRsiState((uid, sprite), BorgVisualLayers.Light, hasPlayer ? component.HasMindState : component.NoMindState);
|
||||
}
|
||||
|
||||
private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref AppearanceChangeEvent args)
|
||||
@@ -128,10 +107,4 @@ public sealed partial class BorgSystem : SharedBorgSystem
|
||||
borg.Comp.HasMindState = hasMindState;
|
||||
borg.Comp.NoMindState = noMindState;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
UpdateBattery(frameTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'thief-backpack-window-title'}"
|
||||
MinSize="700 700">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<!-- First Informational panel -->
|
||||
@@ -24,6 +25,7 @@
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Third approve button panel -->
|
||||
<PanelContainer Margin="10">
|
||||
<Button Name="ApproveButton"
|
||||
|
||||
@@ -46,8 +46,7 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
|
||||
selectedNumber++;
|
||||
}
|
||||
|
||||
Title = Loc.GetString(state.ToolName);
|
||||
Description.Text = Loc.GetString(state.ToolDesc, ("maxCount", state.MaxSelectedSets));
|
||||
Description.Text = Loc.GetString("thief-backpack-window-description", ("maxCount", state.MaxSelectedSets));
|
||||
SelectedSets.Text = Loc.GetString("thief-backpack-window-selected", ("selectedCount", selectedNumber), ("maxCount", state.MaxSelectedSets));
|
||||
ApproveButton.Disabled = selectedNumber != state.MaxSelectedSets;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using Content.Shared.Tips;
|
||||
|
||||
namespace Content.Client.Tips;
|
||||
|
||||
public sealed class TipsSystem : SharedTipsSystem;
|
||||
@@ -147,7 +147,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
|
||||
if (emote.Category == EmoteCategory.Invalid
|
||||
|| emote.ChatTriggers.Count == 0
|
||||
|| !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value))
|
||||
|| whitelistSystem.IsWhitelistPass(emote.Blacklist, player.Value))
|
||||
|| whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
|
||||
continue;
|
||||
|
||||
if (!emote.Available
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Content.Client._DV.UserInterfaces.BuildInfo; // DeltaV - More info in Escape Menu
|
||||
using Content.Client.Credits; // DeltaV - More info in Escape Menu
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Guidebook;
|
||||
using Content.Client.UserInterface.Systems.Info;
|
||||
@@ -27,7 +25,6 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
[Dependency] private readonly InfoUIController _info = default!;
|
||||
[Dependency] private readonly OptionsUIController _options = default!;
|
||||
[Dependency] private readonly GuidebookUIController _guidebook = default!;
|
||||
[Dependency] private readonly BuildInfoUIController _buildInfo = default!; // DeltaV - More info in Escape Menu
|
||||
|
||||
private Options.UI.EscapeMenu? _escapeWindow;
|
||||
|
||||
@@ -106,19 +103,6 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
_guidebook.ToggleGuidebook();
|
||||
};
|
||||
|
||||
// Begin DeltaV - More info in Escape Menu
|
||||
_escapeWindow.BuildInfoButton.OnPressed += _ =>
|
||||
{
|
||||
CloseEscapeWindow();
|
||||
_buildInfo.OpenWindow();
|
||||
};
|
||||
|
||||
_escapeWindow.CreditsButton.OnPressed += _ =>
|
||||
{
|
||||
new CreditsWindow().Open();
|
||||
};
|
||||
// End DeltaV - More info in Escape Menu
|
||||
|
||||
// Hide wiki button if we don't have a link for it.
|
||||
_escapeWindow.WikiButton.Visible = _cfg.GetCVar(CCVars.InfoLinksWiki) != "";
|
||||
|
||||
|
||||
@@ -7,20 +7,23 @@ public sealed partial class GunSystem
|
||||
protected override void InitializeBattery()
|
||||
{
|
||||
base.InitializeBattery();
|
||||
// Hitscan
|
||||
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
|
||||
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
|
||||
|
||||
SubscribeLocalEvent<BatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
|
||||
SubscribeLocalEvent<BatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
|
||||
// Projectile
|
||||
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
|
||||
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
|
||||
}
|
||||
|
||||
private void OnAmmoCountUpdate(Entity<BatteryAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
|
||||
private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args)
|
||||
{
|
||||
if (args.Control is not BoxesStatusControl boxes)
|
||||
return;
|
||||
if (args.Control is not BoxesStatusControl boxes) return;
|
||||
|
||||
boxes.Update(ent.Comp.Shots, ent.Comp.Capacity);
|
||||
boxes.Update(component.Shots, component.Capacity);
|
||||
}
|
||||
|
||||
private void OnControl(Entity<BatteryAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
|
||||
private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args)
|
||||
{
|
||||
args.Control = new BoxesStatusControl();
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
base.Initialize();
|
||||
UpdatesOutsidePrediction = true;
|
||||
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
|
||||
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
|
||||
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
|
||||
|
||||
// Plays animated effects on the client.
|
||||
@@ -89,6 +90,10 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
InitializeSpentAmmo();
|
||||
}
|
||||
|
||||
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
|
||||
{
|
||||
UpdateAmmoCount(uid, ammoComp);
|
||||
}
|
||||
|
||||
private void OnMuzzleFlash(MuzzleFlashEvent args)
|
||||
{
|
||||
@@ -153,8 +158,6 @@ public sealed partial class GunSystem : SharedGunSystem
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!Timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
using Content.Client._DV.ChronicPain.Overlays;
|
||||
using Content.Shared._DV.ChronicPain.Components;
|
||||
using Content.Shared._DV.ChronicPain.EntitySystems;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client._DV.ChronicPain.EntitySystems;
|
||||
|
||||
public sealed class ChronicPainSystem : SharedChronicPainSystem
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||
|
||||
private ChronicPainOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_overlay = new();
|
||||
}
|
||||
|
||||
|
||||
protected override void OnChronicPainInit(Entity<ChronicPainComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (entity.Owner != _playerManager.LocalEntity)
|
||||
return;
|
||||
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
protected override void OnChronicPainShutdown(Entity<ChronicPainComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
if (entity.Owner != _playerManager.LocalEntity)
|
||||
return;
|
||||
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
protected override void OnPlayerAttached(Entity<ChronicPainComponent> entity, ref LocalPlayerAttachedEvent args)
|
||||
{
|
||||
if (!IsChronicPainSuppressed((entity.Owner, entity.Comp)))
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
protected override void OnPlayerDetached(Entity<ChronicPainComponent> entity, ref LocalPlayerDetachedEvent args)
|
||||
{
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// Handle showing/hiding overlay based on suppression status
|
||||
if (_playerManager.LocalEntity is not { } player)
|
||||
return;
|
||||
|
||||
if (!TryComp<ChronicPainComponent>(player, out var comp))
|
||||
return;
|
||||
|
||||
var isSuppressed = IsChronicPainSuppressed((player, comp));
|
||||
|
||||
if (isSuppressed && _overlayMan.TryGetOverlay<ChronicPainOverlay>(out var overlay))
|
||||
_overlayMan.RemoveOverlay(overlay);
|
||||
|
||||
if (isSuppressed) // If its suppressed and we don't have an overlay, just return
|
||||
return;
|
||||
|
||||
if (!isSuppressed && !_overlayMan.HasOverlay<ChronicPainOverlay>())
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using Content.Client.NetworkConfigurator.Systems;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Client._DV.NetworkConfigurator;
|
||||
|
||||
/// <summary>
|
||||
/// Mapping version of <see cref="NetworkConfiguratorLinkOverlay"> that shows the linkages
|
||||
/// as we move around the map, rather than using component markers.
|
||||
/// Using markers means it only applies to objects the user is currently in PVS range, but this
|
||||
/// version continually searches for device networks to draw.
|
||||
/// </summary>
|
||||
public sealed class MappingNetworkConfiguratorLinkOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
private readonly DeviceListSystem _deviceListSystem;
|
||||
private readonly SharedTransformSystem _transformSystem;
|
||||
|
||||
public Dictionary<EntityUid, Color> Colors = [];
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
|
||||
public MappingNetworkConfiguratorLinkOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_deviceListSystem = _entityManager.System<DeviceListSystem>();
|
||||
_transformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var query = _entityManager.AllEntityQueryEnumerator<DeviceNetworkComponent>();
|
||||
while (query.MoveNext(out var uid, out _))
|
||||
{
|
||||
if (_entityManager.Deleted(uid) || !_entityManager.TryGetComponent(uid, out DeviceListComponent? deviceList))
|
||||
continue; // Deleted or doesn't have any device linkages
|
||||
|
||||
var sourceTransform = _entityManager.GetComponent<TransformComponent>(uid);
|
||||
if (sourceTransform.MapID == MapId.Nullspace)
|
||||
{
|
||||
// Can happen if the item is outside the client's view. In that case,
|
||||
// we don't have a sensible transform to draw, so we need to skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Colors.TryGetValue(uid, out var color))
|
||||
{
|
||||
color = new Color(
|
||||
_random.NextByte(0, 255),
|
||||
_random.NextByte(0, 255),
|
||||
_random.NextByte(0, 255));
|
||||
Colors.Add(uid, color);
|
||||
}
|
||||
|
||||
foreach (var device in _deviceListSystem.GetAllDevices(uid, deviceList))
|
||||
{
|
||||
if (_entityManager.Deleted(device))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var linkTransform = _entityManager.GetComponent<TransformComponent>(device);
|
||||
if (linkTransform.MapID == MapId.Nullspace)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
args.WorldHandle.DrawLine(_transformSystem.GetWorldPosition(sourceTransform), _transformSystem.GetWorldPosition(linkTransform), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Client._DV.NetworkConfigurator;
|
||||
|
||||
public sealed class ToggleNetworkLinksCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlay = default!;
|
||||
|
||||
public override string Command => "togglenetworklinks";
|
||||
public override string Description => Loc.GetString("cmd-togglenetworklinks-desc");
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (_overlay.RemoveOverlay<MappingNetworkConfiguratorLinkOverlay>())
|
||||
return;
|
||||
|
||||
_overlay.AddOverlay(new MappingNetworkConfiguratorLinkOverlay());
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
<tabs:DeltaTab xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:tabs="clr-namespace:Content.Client._DV.Options.UI.Tabs"
|
||||
xmlns:ui="clr-namespace:Content.Client.Options.UI">
|
||||
xmlns:xNamespace="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:optionsUI="clr-namespace:Content.Client.Options.UI"
|
||||
xmlns:s="clr-namespace:Content.Client.Stylesheets"
|
||||
xmlns:ui="clr-namespace:Content.Client.Options.UI"
|
||||
xmlns:dvui="clr-namespace:Content.Client._DV.Options.UI">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Vertical" Margin="8 8 8 8" VerticalExpand="True">
|
||||
<Label Text="{Loc 'ui-options-general-forknotice'}"
|
||||
FontColorOverride="{xNamespace:Static s:StyleNano.ConcerningOrangeFore}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<Label Text="{Loc 'ui-options-general-accessibility'}"
|
||||
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="DisableFiltersCheckBox" Text="{Loc 'ui-options-no-filters'}" />
|
||||
<CheckBox Name="DisableGlimmerEffectCheckBox" Text="{Loc 'ui-options-disable-glimmer-effect'}" />
|
||||
<Label Text="{Loc 'ui-options-delta-tips'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<CheckBox Name="DisableTipsCheckBox" Text="{Loc 'ui-options-delta-disable-tips'}" />
|
||||
<Button Name="ResetTipsButton" Text="{Loc 'ui-options-delta-reset-tips'}" MaxWidth="200" Margin="0 4 0 0" />
|
||||
</BoxContainer>
|
||||
<ui:OptionsTabControlRow Name="Control" Access="Public" />
|
||||
</BoxContainer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Content.Shared._DV.CCVars;
|
||||
using Content.Shared._DV.Tips;
|
||||
using Content.Client.Options.UI;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
@@ -9,8 +9,6 @@ namespace Content.Client._DV.Options.UI.Tabs;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DeltaTab : Control
|
||||
{
|
||||
[Dependency] private readonly IEntityNetworkManager _entNet = default!;
|
||||
|
||||
public DeltaTab()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -18,15 +16,9 @@ public sealed partial class DeltaTab : Control
|
||||
|
||||
Control.AddOptionCheckBox(DCCVars.NoVisionFilters, DisableFiltersCheckBox);
|
||||
Control.AddOptionCheckBox(DCCVars.DisableGlimmerShader, DisableGlimmerEffectCheckBox);
|
||||
Control.AddOptionCheckBox(DCCVars.DisableTips, DisableTipsCheckBox);
|
||||
|
||||
ResetTipsButton.OnPressed += _ => OnResetTipsPressed();
|
||||
//Control.AddOptionCheckBox(DCCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox); // Migrating to the new system
|
||||
//Control.AddOptionColorSlider(DCCVars.ChatHighlightsColor, HighlightsColorSlider); // Migrating to the new system
|
||||
|
||||
Control.Initialize();
|
||||
}
|
||||
|
||||
private void OnResetTipsPressed()
|
||||
{
|
||||
_entNet.SendSystemNetworkMessage(new ResetAllSeenTipsRequest());
|
||||
}
|
||||
}
|
||||
|
||||
+10
-5
@@ -1,31 +1,36 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared._DV.Pain;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._DV.ChronicPain.Overlays;
|
||||
namespace Content.Client._DV.Overlays;
|
||||
|
||||
public sealed partial class ChronicPainOverlay : Overlay
|
||||
public sealed partial class PainOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
|
||||
public override bool RequestScreenTexture => true;
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
private readonly ShaderInstance _painShader;
|
||||
private readonly ProtoId<ShaderPrototype> _shaderProto = "ChromaticAberration";
|
||||
|
||||
public ChronicPainOverlay()
|
||||
public PainOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_painShader = _prototype.Index(_shaderProto).InstanceUnique();
|
||||
_painShader = _prototype.Index(_shaderProto).Instance().Duplicate();
|
||||
}
|
||||
|
||||
protected override bool BeforeDraw(in OverlayDrawArgs args)
|
||||
{
|
||||
if (_player.LocalEntity is not { Valid: true })
|
||||
if (_player.LocalEntity is not { Valid: true } player
|
||||
|| !_entity.HasComponent<PainComponent>(player))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.BeforeDraw(in args);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Content.Shared._DV.Pain;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client._DV.Overlays;
|
||||
|
||||
public sealed partial class PainSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _playerMan = default!;
|
||||
|
||||
private PainOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PainComponent, ComponentInit>(OnPainInit);
|
||||
SubscribeLocalEvent<PainComponent, ComponentShutdown>(OnPainShutdown);
|
||||
SubscribeLocalEvent<PainComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<PainComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
_overlay = new();
|
||||
}
|
||||
|
||||
private void OnPainInit(Entity<PainComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
if (ent.Owner == _playerMan.LocalEntity && !ent.Comp.Suppressed)
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void OnPainShutdown(Entity<PainComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
if (ent.Owner == _playerMan.LocalEntity)
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(Entity<PainComponent> ent, ref LocalPlayerAttachedEvent args)
|
||||
{
|
||||
if (!ent.Comp.Suppressed)
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(Entity<PainComponent> ent, ref LocalPlayerDetachedEvent args)
|
||||
{
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
// Handle showing/hiding overlay based on suppression status
|
||||
if (_playerMan.LocalEntity is not { } player)
|
||||
return;
|
||||
|
||||
if (!TryComp<PainComponent>(player, out var comp))
|
||||
return;
|
||||
|
||||
if (comp.Suppressed && _overlayMan.HasOverlay<PainOverlay>())
|
||||
_overlayMan.RemoveOverlay(_overlay);
|
||||
else if (!comp.Suppressed && !_overlayMan.HasOverlay<PainOverlay>())
|
||||
_overlayMan.AddOverlay(_overlay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared._DV.Pain;
|
||||
|
||||
namespace Content.Client._DV.Pain;
|
||||
|
||||
public sealed class PainSystem : SharedPainSystem;
|
||||
@@ -19,12 +19,10 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
|
||||
public event Action<string>? OnPurchased;
|
||||
|
||||
private readonly List<VesselPrototype> _vessels = [];
|
||||
private readonly List<VesselCategoryPrototype> _categories = [];
|
||||
private readonly List<string> _categories = [];
|
||||
|
||||
public Entity<ShipyardConsoleComponent> Console;
|
||||
|
||||
// The currently selected category
|
||||
private ProtoId<VesselCategoryPrototype>? _category;
|
||||
private string? _category;
|
||||
|
||||
public ShipyardConsoleMenu(EntityUid console, IPrototypeManager proto, IEntityManager entMan, IPlayerManager player, AccessReaderSystem access, EntityWhitelistSystem whitelist)
|
||||
{
|
||||
@@ -35,44 +33,46 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
|
||||
_access = access;
|
||||
_player = player;
|
||||
|
||||
_categories.Clear();
|
||||
foreach (var vesselCategoryProto in Console.Comp.Categories)
|
||||
{
|
||||
if (proto.Resolve(vesselCategoryProto, out var vesselCategory))
|
||||
_categories.Add(vesselCategory);
|
||||
}
|
||||
|
||||
// don't include ships that aren't allowed by whitelist, server won't accept them anyway
|
||||
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
|
||||
{
|
||||
if (whitelist.IsWhitelistPassOrNull(vessel.Whitelist, console))
|
||||
_vessels.Add(vessel);
|
||||
}
|
||||
|
||||
_vessels.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.CurrentCultureIgnoreCase));
|
||||
|
||||
// inserting here and not adding at the start so it doesn't get affected by sort
|
||||
PopulateCategories(Console);
|
||||
// only list categories in said ships
|
||||
foreach (var vessel in _vessels)
|
||||
{
|
||||
foreach (var category in vessel.Categories)
|
||||
{
|
||||
if (!_categories.Contains(category))
|
||||
_categories.Add(category);
|
||||
}
|
||||
}
|
||||
|
||||
SearchBar.OnTextChanged += _ => PopulateProducts(Console);
|
||||
_categories.Sort();
|
||||
// inserting here and not adding at the start so it doesn't get affected by sort
|
||||
_categories.Insert(0, Loc.GetString("cargo-console-menu-populate-categories-all-text"));
|
||||
PopulateCategories();
|
||||
|
||||
SearchBar.OnTextChanged += _ => PopulateProducts();
|
||||
Categories.OnItemSelected += args =>
|
||||
{
|
||||
_category = _categories[args.Id];
|
||||
_category = args.Id == 0 ? null : _categories[args.Id];
|
||||
Categories.SelectId(args.Id);
|
||||
PopulateProducts(Console);
|
||||
PopulateProducts();
|
||||
};
|
||||
|
||||
PopulateProducts(Console);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populates the list of products that will actually be shown, using the current filters.
|
||||
/// </summary>
|
||||
private void PopulateProducts(Entity<ShipyardConsoleComponent> entity)
|
||||
private void PopulateProducts()
|
||||
{
|
||||
Vessels.RemoveAllChildren();
|
||||
|
||||
var access = _player.LocalSession?.AttachedEntity is { } player
|
||||
var access = _player.LocalSession?.AttachedEntity is {} player
|
||||
&& _access.IsAllowed(player, Console);
|
||||
|
||||
var search = SearchBar.Text.Trim().ToLowerInvariant();
|
||||
@@ -80,11 +80,10 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
|
||||
{
|
||||
if (search.Length != 0 && !vessel.Name.Contains(search, StringComparison.InvariantCultureIgnoreCase))
|
||||
continue;
|
||||
|
||||
if (_category != null && !vessel.Categories.Contains(_category.Value.Id))
|
||||
if (_category != null && !vessel.Categories.Contains(_category))
|
||||
continue;
|
||||
|
||||
var vesselEntry = new VesselRow(vessel, access, isFree: !entity.Comp.UseStationFunds);
|
||||
var vesselEntry = new VesselRow(vessel, access);
|
||||
vesselEntry.OnPurchasePressed += () => OnPurchased?.Invoke(vessel.ID);
|
||||
Vessels.AddChild(vesselEntry);
|
||||
}
|
||||
@@ -93,29 +92,18 @@ public sealed partial class ShipyardConsoleMenu : FancyWindow
|
||||
/// <summary>
|
||||
/// Populates the list categories that will actually be shown, using the current filters.
|
||||
/// </summary>
|
||||
private void PopulateCategories(Entity<ShipyardConsoleComponent> entity)
|
||||
private void PopulateCategories()
|
||||
{
|
||||
Categories.Clear();
|
||||
// Guh, there's gotta be an easier way to select a category by default...
|
||||
var selectedId = 0; // Default to the first category. May get overridden later.
|
||||
for (var i = 0; i < _categories.Count; i++)
|
||||
foreach (var category in _categories)
|
||||
{
|
||||
Categories.AddItem(_categories[i].LocalizedName, i);
|
||||
|
||||
if (entity.Comp.DefaultCategory.HasValue && entity.Comp.DefaultCategory.Value.Id.Equals(_categories[i].ID, StringComparison.CurrentCultureIgnoreCase))
|
||||
selectedId = i;
|
||||
}
|
||||
|
||||
if (selectedId >= 0 && selectedId < _categories.Count)
|
||||
{
|
||||
_category = _categories[selectedId];
|
||||
Categories.SelectId(selectedId);
|
||||
Categories.AddItem(category);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateState(ShipyardConsoleState state)
|
||||
{
|
||||
BankAccountLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", state.Balance.ToString()));
|
||||
PopulateProducts(Console);
|
||||
PopulateProducts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public sealed partial class VesselRow : PanelContainer
|
||||
{
|
||||
public event Action? OnPurchasePressed;
|
||||
|
||||
public VesselRow(VesselPrototype vessel, bool access = false, bool isFree = false)
|
||||
public VesselRow(VesselPrototype vessel, bool access)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
@@ -24,9 +24,6 @@ public sealed partial class VesselRow : PanelContainer
|
||||
Purchase.Disabled = !access;
|
||||
Purchase.OnPressed += _ => OnPurchasePressed?.Invoke();
|
||||
|
||||
if (isFree)
|
||||
Purchase.Text = Loc.GetString("shipyard-console-price-free");
|
||||
else
|
||||
Purchase.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", vessel.Price));
|
||||
Price.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", vessel.Price));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
using Content.Client._DV.Tips.UI;
|
||||
using Content.Shared._DV.CCVars;
|
||||
using Content.Shared._DV.Tips;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._DV.Tips;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side system that receives tip events from the server and displays the tip popup UI.
|
||||
/// </summary>
|
||||
public sealed class TipSystem : SharedTipSystem
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Queue of tips waiting to be shown. Only one tip is displayed at a time.
|
||||
/// </summary>
|
||||
private readonly Queue<ShowTipEvent> _tipQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// Currently displayed tip popup, if any.
|
||||
/// </summary>
|
||||
private TipPopup? _currentPopup;
|
||||
|
||||
/// <summary>
|
||||
/// The tip ID of the currently displayed popup, used for dismiss events.
|
||||
/// </summary>
|
||||
private ProtoId<TipPrototype>? _currentTipId;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<ShowTipEvent>(OnShowTip);
|
||||
}
|
||||
|
||||
private void OnShowTip(ShowTipEvent ev)
|
||||
{
|
||||
// Return if tips are disabled globally
|
||||
if (_cfg.GetCVar(DCCVars.DisableTipsGlobal))
|
||||
return;
|
||||
|
||||
// Return if tips are disabled locally and IgnoreCvar is the default value
|
||||
if (_cfg.GetCVar(DCCVars.DisableTips) && !ev.IgnoreCvar)
|
||||
return;
|
||||
|
||||
_tipQueue.Enqueue(ev);
|
||||
TryShowNextTip();
|
||||
}
|
||||
|
||||
private void TryShowNextTip()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_currentPopup != null)
|
||||
return;
|
||||
|
||||
if (_tipQueue.Count == 0)
|
||||
return;
|
||||
|
||||
var tipEvent = _tipQueue.Dequeue();
|
||||
|
||||
if (!Prototype.TryIndex(tipEvent.TipId, out var tipProto))
|
||||
{
|
||||
Log.Warning($"Received tip event for unknown prototype: {tipEvent.TipId}");
|
||||
continue;
|
||||
}
|
||||
|
||||
ShowTipPopup(tipProto, tipEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowTipPopup(TipPrototype tip, ShowTipEvent ev)
|
||||
{
|
||||
_currentPopup = new TipPopup(tip);
|
||||
_currentPopup.OnClose += OnPopupClosed;
|
||||
_currentTipId = tip.ID;
|
||||
|
||||
_ui.RootControl.AddChild(_currentPopup);
|
||||
|
||||
// Play the sound
|
||||
_audio.PlayGlobal(ev.Sound, Filter.Local(), false);
|
||||
}
|
||||
|
||||
private void OnPopupClosed(bool dontShowAgain)
|
||||
{
|
||||
if (_currentPopup != null && _currentTipId != null)
|
||||
{
|
||||
// Send dismiss event to server
|
||||
RaiseNetworkEvent(new TipDismissedEvent(_currentTipId.Value, dontShowAgain));
|
||||
|
||||
_currentPopup.OnClose -= OnPopupClosed;
|
||||
_currentPopup?.Close();
|
||||
_currentPopup = null;
|
||||
_currentTipId = null;
|
||||
}
|
||||
|
||||
TryShowNextTip();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the current tip popup if one is open.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void CloseCurrentTip()
|
||||
{
|
||||
_currentPopup?.Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all queued tips without showing them.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public void ClearTipQueue()
|
||||
{
|
||||
_tipQueue.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Margin="80 100 0 0">
|
||||
<PanelContainer Name="BackgroundPanel"
|
||||
MinWidth="320"
|
||||
MaxWidth="420">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="15">
|
||||
<!-- Title at the top, centered -->
|
||||
<Label Name="TitleLabel"
|
||||
StyleClasses="LabelBig"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 0 0 10" />
|
||||
|
||||
<!-- Description in a flexible vertical container -->
|
||||
<PanelContainer StyleClasses="BackgroundDark"
|
||||
Margin="0 0 0 12">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="10">
|
||||
<RichTextLabel Name="DescriptionLabel"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Bottom controls -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalAlignment="Center">
|
||||
<CheckBox Name="DontShowAgainCheckBox"
|
||||
Text="{Loc 'tip-popup-dont-show-again'}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 0 0 8" />
|
||||
<Button Name="OkayButton"
|
||||
Text="{Loc 'tip-popup-okay-button'}"
|
||||
HorizontalAlignment="Center"
|
||||
MinWidth="100" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
@@ -1,98 +0,0 @@
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Content.Shared._DV.Tips;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._DV.Tips.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Popup control that displays a tip to the player.
|
||||
/// Appears in the top-left corner with a semi-transparent background.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TipPopup : Control
|
||||
{
|
||||
/// <summary>
|
||||
/// Event raised when the popup is closed. The bool parameter indicates
|
||||
/// whether the "Don't show again" checkbox was checked.
|
||||
/// </summary>
|
||||
public event Action<bool>? OnClose;
|
||||
|
||||
private readonly TipPrototype _tip;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the "Don't show again" checkbox is checked.
|
||||
/// </summary>
|
||||
public bool DontShowAgain => DontShowAgainCheckBox.Pressed;
|
||||
|
||||
public TipPopup(TipPrototype tip)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_tip = tip;
|
||||
|
||||
InitializeUI();
|
||||
InitializeEvents();
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
TitleLabel.Text = Loc.GetString(_tip.Title);
|
||||
|
||||
var description = Loc.GetString(_tip.Description);
|
||||
DescriptionLabel.SetMessage(FormattedMessage.FromMarkupPermissive(description),
|
||||
tagsAllowed:
|
||||
[
|
||||
typeof(ItalicTag),
|
||||
typeof(ColorTag),
|
||||
typeof(BoldTag),
|
||||
typeof(BoldItalicTag),
|
||||
typeof(KeyBindTag),
|
||||
]);
|
||||
|
||||
ApplyTransparentStyle();
|
||||
}
|
||||
|
||||
private void ApplyTransparentStyle()
|
||||
{
|
||||
var styleBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#1a1a1a").WithAlpha(0.85f),
|
||||
BorderColor = Color.FromHex("#3a3a3a"),
|
||||
BorderThickness = new Thickness(1),
|
||||
ContentMarginLeftOverride = 0,
|
||||
ContentMarginRightOverride = 0,
|
||||
ContentMarginTopOverride = 0,
|
||||
ContentMarginBottomOverride = 0,
|
||||
};
|
||||
|
||||
BackgroundPanel.PanelOverride = styleBox;
|
||||
}
|
||||
|
||||
private void InitializeEvents()
|
||||
{
|
||||
OkayButton.OnPressed += OnOkayPressed;
|
||||
}
|
||||
|
||||
private void OnOkayPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (Parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dontShowAgain = DontShowAgain;
|
||||
Parent.RemoveChild(this);
|
||||
OnClose?.Invoke(dontShowAgain);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Content.Client._DV.Traits.UI;
|
||||
using Content.Shared._DV.CCVars;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._DV.Traits;
|
||||
|
||||
/// <summary>
|
||||
/// Client system that shows a popup when traits are disabled due to unmet conditions.
|
||||
/// </summary>
|
||||
public sealed class DisabledTraitsPopupSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private DisabledTraitsPopup? _window;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<DisabledTraitsEvent>(OnDisabledTraits);
|
||||
}
|
||||
|
||||
private void OnDisabledTraits(DisabledTraitsEvent ev)
|
||||
{
|
||||
// Don't show if user has opted to skip this popup
|
||||
if (_cfg.GetCVar(DCCVars.SkipDisabledTraitsPopup))
|
||||
return;
|
||||
|
||||
// Don't show if no traits were actually disabled
|
||||
if (ev.DisabledTraits.Count == 0)
|
||||
return;
|
||||
|
||||
OpenDisabledTraitsPopup(ev.DisabledTraits);
|
||||
}
|
||||
|
||||
private void OpenDisabledTraitsPopup(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
|
||||
{
|
||||
// Close existing window if one is open
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Close();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
_window = new DisabledTraitsPopup(disabledTraits);
|
||||
_window.OpenCentered();
|
||||
_window.OnClose += () => _window = null;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc disabled-traits-popup-title}"
|
||||
MinSize="400 200"
|
||||
SetSize="500 350">
|
||||
<BoxContainer Orientation="Vertical" Margin="10">
|
||||
<!-- Header -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 10">
|
||||
<TextureRect TexturePath="/Textures/Interface/info.svg.192dpi.png"
|
||||
TextureScale="0.5 0.5"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 10 0" />
|
||||
<Label Name="TitleLabel"
|
||||
StyleClasses="LabelHeading"
|
||||
VAlign="Center" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Message -->
|
||||
<RichTextLabel Name="MessageLabel"
|
||||
Margin="0 0 0 15" />
|
||||
|
||||
<!-- List of disabled traits -->
|
||||
<Label Text="{Loc disabled-traits-popup-list-header}"
|
||||
StyleClasses="LabelSubText"
|
||||
Margin="0 0 0 5" />
|
||||
|
||||
<PanelContainer StyleClasses="BackgroundDark" VerticalExpand="True">
|
||||
<ScrollContainer HScrollEnabled="False" VerticalExpand="True">
|
||||
<BoxContainer Name="DisabledTraitsContainer"
|
||||
Orientation="Vertical"
|
||||
Margin="5" />
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Footer with checkbox and close button -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
|
||||
<CheckBox Name="SkipCheckBox"
|
||||
Text="{Loc disabled-traits-popup-skip-checkbox}"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<Button Name="ButtonClose"
|
||||
Text="{Loc disabled-traits-popup-close-button}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0 10 0 0"
|
||||
MinSize="120 0" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -1,100 +0,0 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared._DV.CCVars;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DisabledTraitsPopup : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private bool _initialSkipState;
|
||||
|
||||
public DisabledTraitsPopup(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
InitializeUI(disabledTraits);
|
||||
InitializeEvents();
|
||||
}
|
||||
|
||||
private void InitializeUI(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
|
||||
{
|
||||
TitleLabel.Text = Loc.GetString("disabled-traits-popup-label");
|
||||
MessageLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(
|
||||
Loc.GetString("disabled-traits-popup-message")));
|
||||
|
||||
_initialSkipState = _cfg.GetCVar(DCCVars.SkipDisabledTraitsPopup);
|
||||
SkipCheckBox.Pressed = _initialSkipState;
|
||||
|
||||
PopulateDisabledTraits(disabledTraits);
|
||||
}
|
||||
|
||||
private void PopulateDisabledTraits(Dictionary<ProtoId<TraitPrototype>, List<string>> disabledTraits)
|
||||
{
|
||||
DisabledTraitsContainer.RemoveAllChildren();
|
||||
|
||||
foreach (var (traitId, reasons) in disabledTraits)
|
||||
{
|
||||
if (!_prototype.TryIndex(traitId, out var trait))
|
||||
continue;
|
||||
|
||||
var container = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Margin = new Thickness(0, 0, 0, 10)
|
||||
};
|
||||
|
||||
// Trait name
|
||||
var nameLabel = new Label
|
||||
{
|
||||
Text = Loc.GetString(trait.Name),
|
||||
FontColorOverride = Color.FromHex("#ff6b6b"),
|
||||
StyleClasses = { "LabelSubText" }
|
||||
};
|
||||
container.AddChild(nameLabel);
|
||||
|
||||
// Reasons why it was disabled
|
||||
foreach (var reason in reasons)
|
||||
{
|
||||
var reasonLabel = new RichTextLabel
|
||||
{
|
||||
Margin = new Thickness(10, 2, 0, 0)
|
||||
};
|
||||
reasonLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(reason));
|
||||
container.AddChild(reasonLabel);
|
||||
}
|
||||
|
||||
DisabledTraitsContainer.AddChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeEvents()
|
||||
{
|
||||
OnClose += SaveSkipState;
|
||||
ButtonClose.OnPressed += OnClosePressed;
|
||||
}
|
||||
|
||||
private void SaveSkipState()
|
||||
{
|
||||
if (SkipCheckBox.Pressed == _initialSkipState)
|
||||
return;
|
||||
|
||||
_cfg.SetCVar(DCCVars.SkipDisabledTraitsPopup, SkipCheckBox.Pressed);
|
||||
_cfg.SaveToFile();
|
||||
}
|
||||
|
||||
private void OnClosePressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 8">
|
||||
|
||||
<!-- Category Header -->
|
||||
<PanelContainer Name="HeaderPanel" StyleClasses="TraitsCategoryHeader">
|
||||
<Button Name="HeaderButton"
|
||||
StyleClasses="TraitsCategoryHeaderButton"
|
||||
HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" Margin="8 6">
|
||||
<!-- Expand/Collapse Icon -->
|
||||
<Label Name="ExpandIcon"
|
||||
Text="▼"
|
||||
StyleClasses="TraitsCategoryExpandIcon"
|
||||
Margin="0 0 8 0"/>
|
||||
|
||||
<!-- Category Name -->
|
||||
<Label Name="CategoryNameLabel"
|
||||
StyleClasses="TraitsCategoryNameLabel"/>
|
||||
|
||||
<Control HorizontalExpand="True"/>
|
||||
|
||||
<!-- Category Stats -->
|
||||
<Label Name="CategoryStatsLabel"
|
||||
StyleClasses="TraitsCategoryStatsLabel"
|
||||
Margin="0 0 4 0"/>
|
||||
|
||||
<!-- Category Points (if applicable) -->
|
||||
<Label Name="CategoryPointsLabel"
|
||||
StyleClasses="TraitsCategoryPointsLabel"
|
||||
Visible="False"/>
|
||||
</BoxContainer>
|
||||
</Button>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Accent Bar -->
|
||||
<PanelContainer Name="AccentBar"
|
||||
StyleClasses="TraitsCategoryAccent"
|
||||
SetHeight="3"
|
||||
Margin="0 0 0 0"/>
|
||||
|
||||
<!-- Traits Container -->
|
||||
<PanelContainer Name="ContentPanel" StyleClasses="TraitsCategoryContent">
|
||||
<BoxContainer Name="TraitsContainer"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="8 8"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
@@ -1,192 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TraitCategory : BoxContainer
|
||||
{
|
||||
public event Action<ProtoId<TraitPrototype>, bool>? OnTraitToggled;
|
||||
|
||||
private readonly TraitCategoryPrototype _category;
|
||||
private readonly List<TraitPrototype> _allTraits;
|
||||
private readonly Dictionary<ProtoId<TraitPrototype>, TraitEntry> _traitEntries = new();
|
||||
|
||||
private bool _isExpanded;
|
||||
|
||||
public int SelectedCount;
|
||||
public int PointsSpent;
|
||||
|
||||
public TraitCategory(TraitCategoryPrototype category, List<TraitPrototype> traits)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_category = category;
|
||||
_allTraits = traits;
|
||||
_isExpanded = category.DefaultExpanded;
|
||||
|
||||
CategoryNameLabel.Text = Loc.GetString(category.Name);
|
||||
SetAccentColor(category.AccentColor);
|
||||
|
||||
HeaderButton.OnPressed += _ => ToggleExpanded();
|
||||
|
||||
PopulateTraits();
|
||||
UpdateExpandedState();
|
||||
UpdateStats();
|
||||
}
|
||||
|
||||
private void SetAccentColor(Color color)
|
||||
{
|
||||
AccentBar.PanelOverride = new StyleBoxFlat { BackgroundColor = color }; // dumb stylesheet modulation workaround
|
||||
}
|
||||
|
||||
private void PopulateTraits()
|
||||
{
|
||||
TraitsContainer.RemoveAllChildren();
|
||||
_traitEntries.Clear();
|
||||
|
||||
foreach (var trait in _allTraits)
|
||||
{
|
||||
var entry = new TraitEntry(trait);
|
||||
entry.OnToggled += selected => OnTraitEntryToggled(trait.ID, selected);
|
||||
_traitEntries[trait.ID] = entry;
|
||||
TraitsContainer.AddChild(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTraitEntryToggled(ProtoId<TraitPrototype> traitId, bool selected)
|
||||
{
|
||||
OnTraitToggled?.Invoke(traitId, selected);
|
||||
}
|
||||
|
||||
private void ToggleExpanded()
|
||||
{
|
||||
_isExpanded = !_isExpanded;
|
||||
UpdateExpandedState();
|
||||
}
|
||||
|
||||
private void UpdateExpandedState()
|
||||
{
|
||||
ContentPanel.Visible = _isExpanded;
|
||||
ExpandIcon.Text = _isExpanded ? "▼" : "▶";
|
||||
}
|
||||
|
||||
public void UpdateStats()
|
||||
{
|
||||
SelectedCount = _traitEntries.Values.Count(e => e.IsSelected);
|
||||
PointsSpent = _traitEntries.Values
|
||||
.Where(e => e.IsSelected)
|
||||
.Sum(e => e.TraitCost);
|
||||
|
||||
if (_category.MaxTraits.HasValue)
|
||||
{
|
||||
CategoryStatsLabel.Text = Loc.GetString("trait-category-traits",
|
||||
("selected", SelectedCount),
|
||||
("max", _category.MaxTraits.Value));
|
||||
}
|
||||
else
|
||||
{
|
||||
CategoryStatsLabel.Text = Loc.GetString("trait-category-traits-unlimited",
|
||||
("selected", SelectedCount));
|
||||
}
|
||||
|
||||
if (_category.MaxPoints.HasValue)
|
||||
{
|
||||
CategoryPointsLabel.Visible = true;
|
||||
CategoryPointsLabel.Text = Loc.GetString("trait-category-points",
|
||||
("selected", PointsSpent),
|
||||
("max", _category.MaxPoints.Value));
|
||||
}
|
||||
else
|
||||
{
|
||||
CategoryPointsLabel.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTraitSelected(ProtoId<TraitPrototype> traitId, bool selected)
|
||||
{
|
||||
if (_traitEntries.TryGetValue(traitId, out var entry))
|
||||
{
|
||||
entry.SetSelected(selected);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
foreach (var (_, entry) in _traitEntries)
|
||||
{
|
||||
entry.SetSelected(false);
|
||||
}
|
||||
|
||||
SelectedCount = 0;
|
||||
PointsSpent = 0;
|
||||
UpdateStats();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IDs of all currently selected traits in this category.
|
||||
/// </summary>
|
||||
public IEnumerable<ProtoId<TraitPrototype>> GetSelectedTraitIds()
|
||||
{
|
||||
return _traitEntries
|
||||
.Where(kvp => kvp.Value.IsSelected)
|
||||
.Select(kvp => kvp.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters traits based on search text only
|
||||
/// </summary>
|
||||
public void FilterTraits(string searchText)
|
||||
{
|
||||
var hasVisibleTraits = false;
|
||||
|
||||
foreach (var (traitId, entry) in _traitEntries)
|
||||
{
|
||||
var trait = _allTraits.First(t => t.ID == traitId);
|
||||
var name = Loc.GetString(trait.Name);
|
||||
var description = Loc.GetString(trait.Description);
|
||||
|
||||
var matchesSearch = string.IsNullOrEmpty(searchText) ||
|
||||
name.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
description.Contains(searchText, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
entry.Visible = matchesSearch;
|
||||
|
||||
if (entry.Visible)
|
||||
hasVisibleTraits = true;
|
||||
}
|
||||
|
||||
// Hide entire category if no traits match search
|
||||
Visible = hasVisibleTraits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates condition states for all trait entries based on current job/species.
|
||||
/// Traits that don't meet conditions are disabled but still visible.
|
||||
/// </summary>
|
||||
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
|
||||
{
|
||||
foreach (var (_, entry) in _traitEntries)
|
||||
{
|
||||
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences, traits);
|
||||
}
|
||||
|
||||
// Update stats since some traits may have been deselected
|
||||
UpdateStats();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a trait in this category meets its conditions.
|
||||
/// </summary>
|
||||
public bool TraitMeetsConditions(ProtoId<TraitPrototype> traitId)
|
||||
{
|
||||
return _traitEntries.TryGetValue(traitId, out var entry) && entry.MeetsConditions;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
StyleClasses="TraitsEntryPanel"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 0 4">
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="10 8">
|
||||
|
||||
<!-- Toggle Container -->
|
||||
<BoxContainer Name="ToggleContainer"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0 2 10 0"
|
||||
SetWidth="24">
|
||||
|
||||
<!-- Checkbox -->
|
||||
<CheckBox Name="TraitCheckbox"
|
||||
StyleClasses="TraitsEntryCheckbox"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- Lock Icon -->
|
||||
<TextureRect Name="LockIcon"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
SetSize="20 20"
|
||||
Stretch="KeepAspectCentered"
|
||||
TexturePath="/Textures/Interface/VerbIcons/lock.svg.192dpi.png"
|
||||
Visible="False"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Trait Info -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center"
|
||||
Name="TraitInfo">
|
||||
|
||||
<!-- Name and Cost Row -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="TraitNameLabel"
|
||||
StyleClasses="TraitsEntryNameLabel"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="TraitCostLabel"
|
||||
StyleClasses="TraitsEntryCostLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Description -->
|
||||
<RichTextLabel Name="TraitDescriptionLabel"
|
||||
StyleClasses="TraitsEntryDescriptionLabel"
|
||||
Margin="0 4 0 0"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
@@ -1,266 +0,0 @@
|
||||
using System.Text;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Content.Shared._DV.Traits.Conditions;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TraitEntry : PanelContainer
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
public event Action<bool>? OnToggled;
|
||||
|
||||
public bool IsSelected => TraitCheckbox.Pressed;
|
||||
public readonly int TraitCost;
|
||||
public bool MeetsConditions { get; private set; } = true;
|
||||
|
||||
private readonly TraitPrototype _trait;
|
||||
private bool _isUpdating;
|
||||
|
||||
public TraitEntry(TraitPrototype trait)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_trait = trait;
|
||||
TraitCost = trait.Cost;
|
||||
|
||||
// Enable mouse events so tooltips work
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
|
||||
TraitNameLabel.Text = Loc.GetString(trait.Name);
|
||||
TraitDescriptionLabel.SetMessage(Loc.GetString(trait.Description));
|
||||
|
||||
// Format cost display
|
||||
var costPrefix = trait.Cost > 0 ? "+" : "";
|
||||
var costColor = trait.Cost > 0 ? "#ff6b6b" : trait.Cost < 0 ? "#6bff6b" : "#888888";
|
||||
TraitCostLabel.Text = $"{costPrefix}{trait.Cost}";
|
||||
TraitCostLabel.ModulateSelfOverride = Color.FromHex(costColor);
|
||||
|
||||
TraitCheckbox.OnToggled += OnCheckboxToggled;
|
||||
|
||||
// Build condition tooltips
|
||||
UpdateConditionTooltips();
|
||||
}
|
||||
|
||||
private void UpdateConditionTooltips()
|
||||
{
|
||||
var conditionsTooltip = new StringBuilder();
|
||||
|
||||
foreach (var condition in _trait.Conditions)
|
||||
{
|
||||
conditionsTooltip.Append(condition.GetTooltip(_prototype, _loc, 0));
|
||||
}
|
||||
|
||||
if (conditionsTooltip.Length > 0)
|
||||
{
|
||||
var tooltip = Loc.GetString("trait-conditions-tooltip", ("conditions", conditionsTooltip.ToString())).ToString().Trim();
|
||||
var markupTooltip = CreateMarkupTooltip(tooltip);
|
||||
TooltipSupplier = _ => markupTooltip;
|
||||
}
|
||||
else
|
||||
{
|
||||
TooltipSupplier = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a tooltip control that properly parses markup.
|
||||
/// </summary>
|
||||
private static Tooltip CreateMarkupTooltip(string markupText)
|
||||
{
|
||||
var tooltip = new Tooltip();
|
||||
|
||||
// Parse the markup into a FormattedMessage
|
||||
tooltip.SetMessage(FormattedMessage.FromMarkupOrThrow(markupText));
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates whether conditions are met based on current job/species.
|
||||
/// </summary>
|
||||
public void UpdateConditionsMet(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
|
||||
{
|
||||
MeetsConditions = true;
|
||||
|
||||
foreach (var condition in _trait.Conditions)
|
||||
{
|
||||
var result = condition switch
|
||||
{
|
||||
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
|
||||
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
|
||||
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
|
||||
HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic
|
||||
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
|
||||
HasTraitCondition hasTrait => CheckHasTraitCondition(hasTrait, traits),
|
||||
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences, traits),
|
||||
_ => true,
|
||||
};
|
||||
|
||||
// Apply inversion
|
||||
result ^= condition.Invert;
|
||||
|
||||
if (!result)
|
||||
{
|
||||
MeetsConditions = false;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateDisabledState();
|
||||
}
|
||||
|
||||
private bool CheckSpeciesCondition(IsSpeciesCondition condition, ProtoId<SpeciesPrototype>? speciesId)
|
||||
{
|
||||
if (!_prototype.TryIndex(speciesId, out var species))
|
||||
return false;
|
||||
|
||||
return species == condition.Species;
|
||||
}
|
||||
|
||||
private bool CheckJobCondition(HasJobCondition condition, ProtoId<JobPrototype>? jobId)
|
||||
{
|
||||
if (!_prototype.TryIndex(jobId, out var job))
|
||||
return false;
|
||||
|
||||
return job == condition.Job;
|
||||
}
|
||||
|
||||
private bool CheckDepartmentCondition(InDepartmentCondition condition, ProtoId<JobPrototype>? jobId)
|
||||
{
|
||||
if (!_prototype.TryIndex(jobId, out var job))
|
||||
return false;
|
||||
|
||||
if (!_prototype.TryIndex(condition.Department, out var department))
|
||||
return false;
|
||||
|
||||
return department.Roles.Contains(job);
|
||||
}
|
||||
|
||||
private bool CheckAntagEligibleCondition(IsAntagEligibleCondition condition, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
|
||||
{
|
||||
if (antagPreferences is null)
|
||||
return false;
|
||||
|
||||
return antagPreferences.Contains(condition.Antag);
|
||||
}
|
||||
|
||||
private bool CheckHasTraitCondition(HasTraitCondition condition, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
|
||||
{
|
||||
if (traits is null)
|
||||
return false;
|
||||
|
||||
return traits.Contains(condition.Trait);
|
||||
}
|
||||
|
||||
private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? traits)
|
||||
{
|
||||
if (condition.Conditions.Count == 0)
|
||||
return false;
|
||||
|
||||
// Return true if ANY child condition evaluates to true
|
||||
foreach (var childCondition in condition.Conditions)
|
||||
{
|
||||
var result = childCondition switch
|
||||
{
|
||||
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
|
||||
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
|
||||
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
|
||||
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
|
||||
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
|
||||
HasTraitCondition hasTrait => CheckHasTraitCondition(hasTrait, traits),
|
||||
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences, traits), // Recursive!
|
||||
_ => true,
|
||||
};
|
||||
|
||||
// Apply child's inversion
|
||||
result ^= childCondition.Invert;
|
||||
|
||||
// If any child passes, the AnyOf passes
|
||||
if (result)
|
||||
return true;
|
||||
}
|
||||
|
||||
// None of the children passed
|
||||
return false;
|
||||
}
|
||||
|
||||
private void UpdateDisabledState()
|
||||
{
|
||||
if (!MeetsConditions)
|
||||
{
|
||||
// Hide checkbox, show lock icon
|
||||
TraitCheckbox.Visible = false;
|
||||
LockIcon.Visible = true;
|
||||
|
||||
// Deselect if conditions no longer met
|
||||
if (TraitCheckbox.Pressed)
|
||||
{
|
||||
_isUpdating = true;
|
||||
TraitCheckbox.Pressed = false;
|
||||
UpdateSelectedStyle();
|
||||
_isUpdating = false;
|
||||
OnToggled?.Invoke(false);
|
||||
}
|
||||
|
||||
// Add disabled styling
|
||||
AddStyleClass("TraitsEntryDisabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Show checkbox, hide lock icon
|
||||
TraitCheckbox.Visible = true;
|
||||
LockIcon.Visible = false;
|
||||
|
||||
// Remove disabled styling - stylesheet restores normal colors
|
||||
RemoveStyleClass("TraitsEntryDisabled");
|
||||
|
||||
// Reset to normal tooltips
|
||||
UpdateConditionTooltips();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCheckboxToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return;
|
||||
|
||||
if (!MeetsConditions)
|
||||
{
|
||||
// This shouldn't happen since checkbox is hidden, but just in case
|
||||
_isUpdating = true;
|
||||
TraitCheckbox.Pressed = false;
|
||||
_isUpdating = false;
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateSelectedStyle();
|
||||
OnToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
_isUpdating = true;
|
||||
TraitCheckbox.Pressed = selected && MeetsConditions;
|
||||
UpdateSelectedStyle();
|
||||
_isUpdating = false;
|
||||
}
|
||||
|
||||
private void UpdateSelectedStyle()
|
||||
{
|
||||
if (TraitCheckbox.Pressed)
|
||||
AddStyleClass("TraitsEntrySelected");
|
||||
else
|
||||
RemoveStyleClass("TraitsEntrySelected");
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class TraitsSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet
|
||||
{
|
||||
public override StyleRule[] GetRules(T sheet, object config)
|
||||
{
|
||||
// Color palette
|
||||
// sorry but the default ColorPalette just sucks in terms of ligher/darker colors
|
||||
var bgDark = Color.FromHex("#1a1a22");
|
||||
var bgMedium = Color.FromHex("#22222a");
|
||||
var bgLight = Color.FromHex("#2a2a35");
|
||||
var bgLighter = Color.FromHex("#32323e");
|
||||
var textPrimary = Color.FromHex("#e0e0e0");
|
||||
var textSecondary = Color.FromHex("#a0a0a0");
|
||||
var textMuted = Color.FromHex("#707070");
|
||||
var accentGreen = Color.FromHex("#4ade80");
|
||||
var accentYellow = Color.FromHex("#fbbf24");
|
||||
var accentRed = Color.FromHex("#f87171");
|
||||
var accentBlue = Color.FromHex("#60a5fa");
|
||||
|
||||
// StyleBoxes
|
||||
var headerPanelBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgLight,
|
||||
BorderColor = bgLighter,
|
||||
BorderThickness = new Thickness(0, 0, 0, 1)
|
||||
};
|
||||
headerPanelBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var searchBarBox = new StyleBoxFlat { BackgroundColor = bgMedium };
|
||||
searchBarBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var searchInputBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgDark,
|
||||
ContentMarginLeftOverride = 8,
|
||||
ContentMarginRightOverride = 8
|
||||
};
|
||||
|
||||
var footerPanelBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgMedium,
|
||||
BorderColor = bgLighter,
|
||||
BorderThickness = new Thickness(0, 1, 0, 0)
|
||||
};
|
||||
|
||||
var categoryHeaderBox = new StyleBoxFlat { BackgroundColor = bgLight };
|
||||
categoryHeaderBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var categoryHeaderButtonBox = new StyleBoxFlat { BackgroundColor = Color.Transparent };
|
||||
categoryHeaderButtonBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var categoryContentBox = new StyleBoxFlat { BackgroundColor = bgMedium };
|
||||
|
||||
var categoryAccentBox = new StyleBoxFlat { BackgroundColor = accentBlue };
|
||||
|
||||
var entryPanelBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgLight,
|
||||
BorderColor = bgLighter,
|
||||
BorderThickness = new Thickness(1)
|
||||
};
|
||||
entryPanelBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var entrySelectedBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#2a3a4a"),
|
||||
BorderColor = accentBlue,
|
||||
BorderThickness = new Thickness(1, 1, 1, 1)
|
||||
};
|
||||
entrySelectedBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var entryDisabledBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#1a1a22"),
|
||||
BorderColor = Color.FromHex("#2a2a2a"),
|
||||
BorderThickness = new Thickness(1)
|
||||
};
|
||||
entryDisabledBox.SetContentMarginOverride(StyleBox.Margin.All, 0);
|
||||
|
||||
var progressBarBgBox = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = bgDark,
|
||||
BorderColor = bgLighter,
|
||||
BorderThickness = new Thickness(1)
|
||||
};
|
||||
|
||||
var progressBarFillFull = new StyleBoxFlat { BackgroundColor = accentGreen };
|
||||
var progressBarFillPartial = new StyleBoxFlat { BackgroundColor = accentYellow };
|
||||
var progressBarFillLow = new StyleBoxFlat { BackgroundColor = accentRed };
|
||||
var progressBarFillEmpty = new StyleBoxFlat { BackgroundColor = bgDark };
|
||||
|
||||
var rules = new List<StyleRule>
|
||||
{
|
||||
// ===== HEADER PANEL =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsHeaderPanel")
|
||||
.Panel(headerPanelBox),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsTitleLabel")
|
||||
.Font(sheet.BaseFont.GetFont(14))
|
||||
.FontColor(textPrimary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsSubtitleLabel")
|
||||
.Font(sheet.BaseFont.GetFont(11))
|
||||
.FontColor(textSecondary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsStatLabel")
|
||||
.Font(sheet.BaseFont.GetFont(12))
|
||||
.FontColor(accentBlue),
|
||||
|
||||
// ===== PROGRESS BAR =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarBg")
|
||||
.Panel(progressBarBgBox),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarFill")
|
||||
.Panel(progressBarFillFull),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarFull")
|
||||
.Panel(progressBarFillFull),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarPartial")
|
||||
.Panel(progressBarFillPartial),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarLow")
|
||||
.Panel(progressBarFillLow),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsProgressBarEmpty")
|
||||
.Panel(progressBarFillEmpty),
|
||||
|
||||
// ===== SEARCH BAR =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsSearchBar")
|
||||
.Panel(searchBarBox),
|
||||
|
||||
E<LineEdit>()
|
||||
.Class("TraitsSearchInput")
|
||||
.Prop(LineEdit.StylePropertyStyleBox, searchInputBox),
|
||||
|
||||
// ===== FOOTER =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsFooterPanel")
|
||||
.Panel(footerPanelBox),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsFooterText")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textMuted),
|
||||
|
||||
// ===== CATEGORY HEADER =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsCategoryHeader")
|
||||
.Panel(categoryHeaderBox),
|
||||
|
||||
E<Button>()
|
||||
.Class("TraitsCategoryHeaderButton")
|
||||
.Prop(ContainerButton.StylePropertyStyleBox, categoryHeaderButtonBox),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryExpandIcon")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textSecondary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryNameLabel")
|
||||
.Font(sheet.BaseFont.GetFont(12))
|
||||
.FontColor(textPrimary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryStatsLabel")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textSecondary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsCategoryPointsLabel")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textMuted),
|
||||
|
||||
// ===== CATEGORY ACCENT =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsCategoryAccent")
|
||||
.Panel(categoryAccentBox),
|
||||
|
||||
// ===== CATEGORY CONTENT =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsCategoryContent")
|
||||
.Panel(categoryContentBox),
|
||||
|
||||
// ===== TRAIT ENTRY =====
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsEntryPanel")
|
||||
.Panel(entryPanelBox),
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsEntryPanel", "TraitsEntrySelected")
|
||||
.Panel(entrySelectedBox),
|
||||
|
||||
// Disabled entry styling
|
||||
E<PanelContainer>()
|
||||
.Class("TraitsEntryPanel", "TraitsEntryDisabled")
|
||||
.Panel(entryDisabledBox)
|
||||
.Modulate(new Color(1f, 1f, 1f, 0.5f)),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsEntryNameLabel")
|
||||
.Font(sheet.BaseFont.GetFont(11))
|
||||
.FontColor(textPrimary),
|
||||
|
||||
E<Label>()
|
||||
.Class("TraitsEntryCostLabel")
|
||||
.Font(sheet.BaseFont.GetFont(11)),
|
||||
|
||||
E<RichTextLabel>()
|
||||
.Class("TraitsEntryDescriptionLabel")
|
||||
.Font(sheet.BaseFont.GetFont(10))
|
||||
.FontColor(textSecondary),
|
||||
};
|
||||
|
||||
return rules.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
|
||||
<!-- Header Panel with Global Stats -->
|
||||
<PanelContainer StyleClasses="TraitsHeaderPanel" Margin="0 0 0 8">
|
||||
<BoxContainer Orientation="Vertical" Margin="12 10">
|
||||
<!-- Title Row -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 8">
|
||||
<Label Text="{Loc 'trait-editor-title'}"
|
||||
StyleClasses="TraitsTitleLabel"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="GlobalTraitCountLabel"
|
||||
StyleClasses="TraitsStatLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Points Progress Bar -->
|
||||
<BoxContainer Orientation="Vertical" Margin="0 4 0 0">
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 4">
|
||||
<Label Text="{Loc 'trait-editor-points-label'}"
|
||||
StyleClasses="TraitsSubtitleLabel"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="GlobalPointsLabel"
|
||||
StyleClasses="TraitsStatLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Progress Bar Container -->
|
||||
<PanelContainer StyleClasses="TraitsProgressBarBg" SetHeight="12">
|
||||
<PanelContainer Name="GlobalPointsBar"
|
||||
StyleClasses="TraitsProgressBarFill"
|
||||
HorizontalAlignment="Left"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<PanelContainer StyleClasses="TraitsSearchBar" Margin="0 0 0 8">
|
||||
<LineEdit Name="SearchBar"
|
||||
PlaceHolder="{Loc 'trait-editor-search-placeholder'}"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="TraitsSearchInput"
|
||||
Margin="10 8"/>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Categories Container (Scrollable) -->
|
||||
<ScrollContainer VerticalExpand="True"
|
||||
HScrollEnabled="False">
|
||||
<BoxContainer Name="CategoriesContainer"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 4 0"/>
|
||||
</ScrollContainer>
|
||||
|
||||
<!-- Footer Info -->
|
||||
<PanelContainer StyleClasses="TraitsFooterPanel" Margin="0 8 0 0">
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 6">
|
||||
<Label Name="FooterHintLabel"
|
||||
Text="{Loc 'trait-editor-footer-hint'}"
|
||||
StyleClasses="TraitsFooterText"/>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<Label Name="FooterInfoLabel"
|
||||
Text="{Loc 'trait-editor-footer-info'}"
|
||||
StyleClasses="TraitsFooterText"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
@@ -1,355 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Shared._DV.CCVars;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client._DV.Traits.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TraitsTab : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when trait selection changes.
|
||||
/// </summary>
|
||||
public event Action<HashSet<ProtoId<TraitPrototype>>>? OnTraitsChanged;
|
||||
|
||||
private readonly Dictionary<ProtoId<TraitCategoryPrototype>, TraitCategory> _categoryUis = new();
|
||||
private readonly HashSet<ProtoId<TraitPrototype>> _selectedTraits = new();
|
||||
|
||||
private int _maxGlobalTraits;
|
||||
private int _maxGlobalPoints;
|
||||
private int _currentTraitCount;
|
||||
private int _currentPointsSpent;
|
||||
|
||||
private string _currentSearchText = string.Empty;
|
||||
private bool _awaitingLayoutUpdate;
|
||||
|
||||
private HumanoidCharacterProfile? _profile;
|
||||
|
||||
public TraitsTab()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
SearchBar.OnTextChanged += OnSearchTextChanged;
|
||||
_prototype.PrototypesReloaded += OnProtoReload;
|
||||
|
||||
// Subscribe to CVars
|
||||
_cfg.OnValueChanged(DCCVars.MaxTraitCount, OnMaxTraitCountChanged, true);
|
||||
_cfg.OnValueChanged(DCCVars.MaxTraitPoints, OnMaxTraitPointsChanged, true);
|
||||
|
||||
PopulateCategories();
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void OnMaxTraitCountChanged(int value)
|
||||
{
|
||||
_maxGlobalTraits = value;
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void OnMaxTraitPointsChanged(int value)
|
||||
{
|
||||
_maxGlobalPoints = value;
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void OnProtoReload(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
// Don't refresh if control has been disposed
|
||||
if (Disposed)
|
||||
return;
|
||||
|
||||
if (args.WasModified<TraitPrototype>() || args.WasModified<TraitCategoryPrototype>())
|
||||
RefreshTraits();
|
||||
}
|
||||
|
||||
public void RefreshTraits()
|
||||
{
|
||||
PopulateCategories();
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void PopulateCategories()
|
||||
{
|
||||
CategoriesContainer.RemoveAllChildren();
|
||||
_categoryUis.Clear();
|
||||
|
||||
var categories = _prototype.EnumeratePrototypes<TraitCategoryPrototype>()
|
||||
.OrderBy(c => c.Priority)
|
||||
.ThenBy(c => Loc.GetString(c.Name))
|
||||
.ToList();
|
||||
|
||||
var traitsByCategory = _prototype.EnumeratePrototypes<TraitPrototype>()
|
||||
.GroupBy(t => t.Category)
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(t => Loc.GetString(t.Name)).ToList());
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (!traitsByCategory.TryGetValue(category.ID, out var traits) || traits.Count == 0)
|
||||
continue;
|
||||
|
||||
var categoryUi = new TraitCategory(category, traits);
|
||||
categoryUi.OnTraitToggled += OnTraitToggled;
|
||||
_categoryUis[category.ID] = categoryUi;
|
||||
CategoriesContainer.AddChild(categoryUi);
|
||||
}
|
||||
|
||||
// Apply current filters and conditions
|
||||
ApplySearchFilter();
|
||||
UpdateAllConditions();
|
||||
}
|
||||
|
||||
private void OnTraitToggled(ProtoId<TraitPrototype> traitId, bool selected)
|
||||
{
|
||||
var trait = _prototype.Index(traitId);
|
||||
|
||||
if (selected)
|
||||
{
|
||||
// Check global limits
|
||||
if (_currentTraitCount >= _maxGlobalTraits)
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentPointsSpent + trait.Cost > _maxGlobalPoints)
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check category limits
|
||||
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
|
||||
{
|
||||
var categoryProto = _prototype.Index(trait.Category);
|
||||
if (categoryProto.MaxTraits.HasValue &&
|
||||
categoryUi.SelectedCount >= categoryProto.MaxTraits.Value)
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (categoryProto.MaxPoints.HasValue &&
|
||||
categoryUi.PointsSpent + trait.Cost > categoryProto.MaxPoints.Value)
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if trait meets conditions
|
||||
if (!categoryUi.TraitMeetsConditions(traitId))
|
||||
{
|
||||
RevertTraitToggle(traitId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedTraits.Add(traitId);
|
||||
_currentTraitCount++;
|
||||
_currentPointsSpent += trait.Cost;
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedTraits.Remove(traitId);
|
||||
_currentTraitCount--;
|
||||
_currentPointsSpent -= trait.Cost;
|
||||
}
|
||||
|
||||
UpdateGlobalStats();
|
||||
UpdateCategoryStats(trait.Category);
|
||||
UpdateAllConditions();
|
||||
OnTraitsChanged?.Invoke(_selectedTraits);
|
||||
}
|
||||
|
||||
private void RevertTraitToggle(ProtoId<TraitPrototype> traitId)
|
||||
{
|
||||
var trait = _prototype.Index(traitId);
|
||||
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
|
||||
{
|
||||
categoryUi.SetTraitSelected(traitId, _selectedTraits.Contains(traitId));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGlobalStats()
|
||||
{
|
||||
GlobalTraitCountLabel.Text = $"{_currentTraitCount} / {_maxGlobalTraits}";
|
||||
GlobalPointsLabel.Text = $"{_maxGlobalPoints - _currentPointsSpent} / {_maxGlobalPoints}";
|
||||
|
||||
// Calculate remaining points (clamped to not go below 0 in display)
|
||||
var remainingPoints = _maxGlobalPoints - _currentPointsSpent;
|
||||
GlobalPointsLabel.Text = $"{remainingPoints} / {_maxGlobalPoints}";
|
||||
|
||||
// Calculate progress bar percentage - clamp between 0 and 1
|
||||
var percentage = _maxGlobalPoints > 0
|
||||
? Math.Clamp((float)remainingPoints / _maxGlobalPoints, 0f, 1f)
|
||||
: 0f;
|
||||
|
||||
// Update progress bar using percentage-based sizing
|
||||
var parent = GlobalPointsBar.Parent;
|
||||
if (parent != null)
|
||||
{
|
||||
var parentWidth = parent.Width;
|
||||
// If parent width is 0 (not laid out yet), defer until layout happens
|
||||
if (parentWidth > 0)
|
||||
{
|
||||
GlobalPointsBar.SetWidth = (int)((parentWidth - 2) * percentage);
|
||||
_awaitingLayoutUpdate = false;
|
||||
}
|
||||
else if (!_awaitingLayoutUpdate)
|
||||
{
|
||||
// Schedule update after parent layout (only once)
|
||||
_awaitingLayoutUpdate = true;
|
||||
parent.OnResized += OnProgressBarParentResized;
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress bar color class
|
||||
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarFull");
|
||||
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarPartial");
|
||||
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarLow");
|
||||
GlobalPointsBar.RemoveStyleClass("TraitsProgressBarEmpty");
|
||||
|
||||
GlobalPointsBar.AddStyleClass(percentage switch
|
||||
{
|
||||
>= 0.99f => "TraitsProgressBarFull",
|
||||
>= 0.5f => "TraitsProgressBarPartial",
|
||||
> 0f => "TraitsProgressBarLow",
|
||||
_ => "TraitsProgressBarEmpty"
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProgressBarParentResized()
|
||||
{
|
||||
_awaitingLayoutUpdate = false;
|
||||
UpdateGlobalStats();
|
||||
}
|
||||
|
||||
private void UpdateCategoryStats(ProtoId<TraitCategoryPrototype> categoryId)
|
||||
{
|
||||
if (_categoryUis.TryGetValue(categoryId, out var categoryUi))
|
||||
{
|
||||
categoryUi.UpdateStats();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSearchTextChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
_currentSearchText = args.Text.Trim();
|
||||
ApplySearchFilter();
|
||||
}
|
||||
|
||||
private void ApplySearchFilter()
|
||||
{
|
||||
foreach (var (_, categoryUi) in _categoryUis)
|
||||
{
|
||||
categoryUi.FilterTraits(_currentSearchText);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates all trait conditions based on current job and species.
|
||||
/// Call this when job or species changes in the character creator.
|
||||
/// </summary>
|
||||
/// <param name="profile">Current profile</param>
|
||||
public void UpdateConditions(HumanoidCharacterProfile profile)
|
||||
{
|
||||
_profile = profile;
|
||||
UpdateAllConditions();
|
||||
}
|
||||
|
||||
private void UpdateAllConditions()
|
||||
{
|
||||
foreach (var (_, categoryUi) in _categoryUis)
|
||||
{
|
||||
// If some fork wants to use the top selected job as well, just add that to the UpdateConditions method in the editor
|
||||
categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences, _selectedTraits);
|
||||
}
|
||||
|
||||
RecalculateStats();
|
||||
}
|
||||
|
||||
private void RecalculateStats()
|
||||
{
|
||||
_currentTraitCount = 0;
|
||||
_currentPointsSpent = 0;
|
||||
|
||||
// Rebuild selected traits based on what's actually selected in the UI
|
||||
var previouslySelected = new HashSet<ProtoId<TraitPrototype>>(_selectedTraits);
|
||||
_selectedTraits.Clear();
|
||||
|
||||
foreach (var (_, categoryUi) in _categoryUis)
|
||||
{
|
||||
// Get the selected traits from this category
|
||||
var selectedInCategory = categoryUi.GetSelectedTraitIds();
|
||||
|
||||
foreach (var traitId in selectedInCategory)
|
||||
{
|
||||
if (!_prototype.TryIndex(traitId, out var trait))
|
||||
continue;
|
||||
|
||||
_selectedTraits.Add(traitId);
|
||||
_currentTraitCount++;
|
||||
_currentPointsSpent += trait.Cost;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateGlobalStats();
|
||||
foreach (var (categoryId, _) in _categoryUis)
|
||||
{
|
||||
UpdateCategoryStats(categoryId);
|
||||
}
|
||||
|
||||
// Fire event if selection changed
|
||||
if (!_selectedTraits.SetEquals(previouslySelected))
|
||||
{
|
||||
OnTraitsChanged?.Invoke(_selectedTraits);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the currently selected traits (e.g., when loading a profile).
|
||||
/// </summary>
|
||||
public void SetSelectedTraits(IEnumerable<ProtoId<TraitPrototype>> traits)
|
||||
{
|
||||
// Clear current selection
|
||||
foreach (var (_, categoryUi) in _categoryUis)
|
||||
{
|
||||
categoryUi.ClearSelection();
|
||||
}
|
||||
|
||||
_selectedTraits.Clear();
|
||||
_currentTraitCount = 0;
|
||||
_currentPointsSpent = 0;
|
||||
|
||||
// Apply new selection
|
||||
foreach (var traitId in traits)
|
||||
{
|
||||
if (!_prototype.TryIndex(traitId, out var trait))
|
||||
continue;
|
||||
|
||||
_selectedTraits.Add(traitId);
|
||||
_currentTraitCount++;
|
||||
_currentPointsSpent += trait.Cost;
|
||||
|
||||
if (_categoryUis.TryGetValue(trait.Category, out var categoryUi))
|
||||
{
|
||||
categoryUi.SetTraitSelected(traitId, true);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateGlobalStats();
|
||||
foreach (var (categoryId, _) in _categoryUis)
|
||||
{
|
||||
UpdateCategoryStats(categoryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Content.Client._DV.UserInterfaces.BuildInfo.Controls;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client._DV.UserInterfaces.BuildInfo;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class BuildInfoUIController : UIController
|
||||
{
|
||||
private BuildInfoWindow _changeLogWindow = default!;
|
||||
|
||||
public void OpenWindow()
|
||||
{
|
||||
EnsureWindow();
|
||||
|
||||
_changeLogWindow.OpenCentered();
|
||||
_changeLogWindow.MoveToFront();
|
||||
}
|
||||
|
||||
private void EnsureWindow()
|
||||
{
|
||||
if (_changeLogWindow is { Disposed: false })
|
||||
{
|
||||
// return; ADD THIS BACK WHEN I DON'T NEED HOT RELOAD
|
||||
}
|
||||
|
||||
_changeLogWindow = UIManager.CreateWindow<BuildInfoWindow>();
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
if (_changeLogWindow is { IsOpen: true })
|
||||
{
|
||||
_changeLogWindow.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
OpenWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
<ui:BuildInfoRow xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client._DV.UserInterfaces.BuildInfo.Controls"
|
||||
Orientation="Horizontal">
|
||||
<Label Name="Label" HorizontalAlignment="Left" />
|
||||
<Label Name="Value" HorizontalAlignment="Right" HorizontalExpand="True" Margin="4" />
|
||||
<Button Name="CopyButton" HorizontalAlignment="Right" Text="{Loc ui-options-function-text-copy}" />
|
||||
</ui:BuildInfoRow>
|
||||
@@ -1,38 +0,0 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client._DV.UserInterfaces.BuildInfo.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BuildInfoRow : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IClipboardManager _clipboard = default!;
|
||||
|
||||
public string? LabelText
|
||||
{
|
||||
get => Label.Text;
|
||||
set => Label.Text = value;
|
||||
}
|
||||
|
||||
public string? ValueText
|
||||
{
|
||||
get => Value.Text;
|
||||
set => Value.Text = value;
|
||||
}
|
||||
|
||||
private void CopyValue(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (ValueText is { } value)
|
||||
_clipboard.SetText(value);
|
||||
}
|
||||
|
||||
public BuildInfoRow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
CopyButton.OnPressed += CopyValue;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<buildInfo:BuildInfoWindow xmlns="https://spacestation14.io"
|
||||
xmlns:buildInfo="clr-namespace:Content.Client._DV.UserInterfaces.BuildInfo.Controls"
|
||||
Title="{Loc build-info-window-title}"
|
||||
Resizable="True">
|
||||
<BoxContainer Orientation="Vertical" Margin="4">
|
||||
<buildInfo:BuildInfoRow Name="EngineVersion" LabelText="{Loc build-info-engine-version-label}" />
|
||||
<buildInfo:BuildInfoRow Name="Fork" LabelText="{Loc build-info-fork-label}" />
|
||||
<buildInfo:BuildInfoRow Name="Version" LabelText="{Loc build-info-version-label}" />
|
||||
<buildInfo:BuildInfoRow Name="DownloadUrl" LabelText="{Loc build-info-download-url-label}" />
|
||||
<buildInfo:BuildInfoRow Name="BuildHash" LabelText="{Loc build-info-build-hash-label}" />
|
||||
<buildInfo:BuildInfoRow Name="BuildManifestUrl" LabelText="{Loc build-info-build-manifest-url-label}" />
|
||||
<buildInfo:BuildInfoRow Name="BuildManifestHash" LabelText="{Loc build-info-build-manifest-hash-label}" />
|
||||
</BoxContainer>
|
||||
</buildInfo:BuildInfoWindow>
|
||||
@@ -1,27 +0,0 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Client._DV.UserInterfaces.BuildInfo.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BuildInfoWindow : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public BuildInfoWindow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Version.ValueText = _cfg.GetCVar(CVars.BuildVersion);
|
||||
EngineVersion.ValueText = _cfg.GetCVar(CVars.BuildEngineVersion);
|
||||
Fork.ValueText = _cfg.GetCVar(CVars.BuildForkId);
|
||||
DownloadUrl.ValueText = _cfg.GetCVar(CVars.BuildDownloadUrl);
|
||||
BuildHash.ValueText = _cfg.GetCVar(CVars.BuildHash);
|
||||
BuildManifestUrl.ValueText = _cfg.GetCVar(CVars.BuildManifestUrl);
|
||||
BuildManifestHash.ValueText = _cfg.GetCVar(CVars.BuildManifestHash);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using Content.Client.Eui;
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.ReadyManifest;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Client._Impstation.ReadyManifest;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class ReadyManifestEui : BaseEui
|
||||
{
|
||||
private readonly ReadyManifestUi _window;
|
||||
|
||||
public ReadyManifestEui()
|
||||
{
|
||||
_window = new();
|
||||
|
||||
_window.OnClose += () =>
|
||||
{
|
||||
SendMessage(new CloseEuiMessage());
|
||||
};
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
base.Opened();
|
||||
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
public override void Closed()
|
||||
{
|
||||
base.Closed();
|
||||
|
||||
_window.Close();
|
||||
}
|
||||
|
||||
public override void HandleState(EuiStateBase state)
|
||||
{
|
||||
base.HandleState(state);
|
||||
|
||||
if (state is not ReadyManifestEuiState cast)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_window.RebuildUI(cast.JobCounts);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Content.Shared.ReadyManifest;
|
||||
|
||||
namespace Content.Client._Impstation.ReadyManifest;
|
||||
|
||||
public sealed class ReadyManifestSystem : EntitySystem
|
||||
{
|
||||
private HashSet<string> _departments = new();
|
||||
|
||||
public IReadOnlySet<string> Departments => _departments;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
public void RequestReadyManifest()
|
||||
{
|
||||
RaiseNetworkEvent(new RequestReadyManifestMessage());
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="Ready Manifest"
|
||||
SetSize="450 750">
|
||||
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
|
||||
<BoxContainer Name="ReadyManifestListing" Orientation="Vertical"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
@@ -1,137 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._Impstation.ReadyManifest;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ReadyManifestUi : DefaultWindow
|
||||
{
|
||||
private readonly IEntitySystemManager _entitySystem;
|
||||
private readonly IPrototypeManager _prototypeManager;
|
||||
private readonly ISharedPlayerManager _playerManager;
|
||||
private readonly Dictionary<string, BoxContainer> _jobCategories;
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
public ReadyManifestUi()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_jobCategories = new Dictionary<string, BoxContainer>();
|
||||
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
_playerManager = IoCManager.Resolve<ISharedPlayerManager>();
|
||||
_entitySystem = IoCManager.Resolve<IEntitySystemManager>();
|
||||
_sprite = _entitySystem.GetEntitySystem<SpriteSystem>();
|
||||
}
|
||||
|
||||
// Currently rebuilds the UI every time it's updated, should probably split department lookup and job counts into separate functions
|
||||
public void RebuildUI(Dictionary<ProtoId<JobPrototype>, int> jobCounts)
|
||||
{
|
||||
ReadyManifestListing.DisposeAllChildren();
|
||||
|
||||
_jobCategories.Clear();
|
||||
var departments = new List<DepartmentPrototype>();
|
||||
|
||||
foreach (var department in _prototypeManager.EnumeratePrototypes<DepartmentPrototype>())
|
||||
{
|
||||
if (department.EditorHidden)
|
||||
continue;
|
||||
|
||||
departments.Add(department);
|
||||
}
|
||||
|
||||
departments.Sort(DepartmentUIComparer.Instance);
|
||||
|
||||
foreach (var department in departments)
|
||||
{
|
||||
var departmentName = Loc.GetString($"department-{department.ID}");
|
||||
|
||||
if (!_jobCategories.TryGetValue(department.ID, out var category))
|
||||
{
|
||||
category = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
HorizontalExpand = true,
|
||||
Name = department.ID,
|
||||
ToolTip = Loc.GetString("humanoid-profile-editor-jobs-amount-in-department-tooltip",
|
||||
("departmentName", departmentName))
|
||||
};
|
||||
|
||||
category.AddChild(new Label()
|
||||
{
|
||||
StyleClasses = { "LabelBig" },
|
||||
Text = Loc.GetString($"department-{department.ID}")
|
||||
});
|
||||
|
||||
_jobCategories[department.ID] = category;
|
||||
ReadyManifestListing.AddChild(category);
|
||||
}
|
||||
|
||||
var jobs = department.Roles.Select(jobId => _prototypeManager.Index(jobId))
|
||||
.Where(job => job.SetPreference)
|
||||
.ToArray();
|
||||
|
||||
Array.Sort(jobs, JobUIComparer.Instance);
|
||||
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
|
||||
var gridContainer = new GridContainer()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Columns = 2
|
||||
};
|
||||
|
||||
var jobContainer = new BoxContainer()
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
};
|
||||
|
||||
var title = new RichTextLabel()
|
||||
{
|
||||
HorizontalExpand = true
|
||||
};
|
||||
title.SetMessage(job.LocalizedName + ":");
|
||||
|
||||
var icon = new TextureRect
|
||||
{
|
||||
TextureScale = new Vector2(2, 2),
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 4, 0)
|
||||
};
|
||||
|
||||
var readyCount = new RichTextLabel()
|
||||
{
|
||||
HorizontalExpand = true
|
||||
};
|
||||
|
||||
if (jobCounts.ContainsKey(job.ID))
|
||||
{
|
||||
var jobCount = jobCounts[job.ID];
|
||||
var color = jobCount > 0 ? Color.White : Color.Red;
|
||||
readyCount.SetMessage(jobCount.ToString(), null, color);
|
||||
}
|
||||
else
|
||||
{
|
||||
readyCount.SetMessage("0", null, Color.Red);
|
||||
}
|
||||
|
||||
var jobIcon = _prototypeManager.Index(job.Icon);
|
||||
icon.Texture = _sprite.Frame0(jobIcon.Icon);
|
||||
jobContainer.AddChild(icon);
|
||||
jobContainer.AddChild(title);
|
||||
gridContainer.AddChild(jobContainer);
|
||||
gridContainer.AddChild(readyCount);
|
||||
category.AddChild(gridContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,12 @@ public sealed class ConstantsTest
|
||||
Assert.That(Atmospherics.GasAbbreviations, Has.Count.EqualTo(Atmospherics.TotalNumberOfGases),
|
||||
$"GasAbbreviations size is not equal to TotalNumberOfGases.");
|
||||
|
||||
// the ID for each gas has to correspond to a value in the Gas enum (converted to a string)
|
||||
// the ID for each gas has to be a number from 0 to TotalNumberOfGases-1
|
||||
foreach (var gas in gasProtos)
|
||||
{
|
||||
Assert.That(Enum.TryParse<Gas>(gas.ID, out _), $"GasPrototype {gas.ID} has an invalid ID. It must correspond to a value in the {nameof(Gas)} enum.");
|
||||
var validInteger = int.TryParse(gas.ID, out var number);
|
||||
Assert.That(validInteger, Is.True, $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
|
||||
Assert.That(number, Is.InRange(0, Atmospherics.TotalNumberOfGases - 1), $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Reflection;
|
||||
@@ -66,16 +64,17 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
||||
var server = pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var entityManager = server.EntMan;
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
// That it finishes successfully
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var tickTime = 1.0f / timing.TickRate;
|
||||
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod / 2, ev, null) { Broadcast = true };
|
||||
var args = new DoAfterArgs(entityManager, mob, tickTime / 2, ev, null) { Broadcast = true };
|
||||
#pragma warning disable NUnit2045 // Interdependent assertions.
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args));
|
||||
Assert.That(ev.Cancelled, Is.False);
|
||||
@@ -93,17 +92,23 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var entityManager = server.EntMan;
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 2, ev, null) { Broadcast = true };
|
||||
var tickTime = 1.0f / timing.TickRate;
|
||||
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args, out var id));
|
||||
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, tickTime * 2, ev, null) { Broadcast = true };
|
||||
|
||||
if (!doAfterSystem.TryStartDoAfter(args, out var id))
|
||||
{
|
||||
Assert.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.That(!ev.Cancelled);
|
||||
doAfterSystem.Cancel(id);
|
||||
@@ -116,67 +121,5 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns two sets of mobs with a targeted DoAfter to check that the GetEntitiesInteractingWithTarget result
|
||||
/// includes the correct interacting entities.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestGetInteractingEntities()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var entityManager = server.EntMan;
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var interactionSystem = entityManager.System<SharedInteractionSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
EntityUid mob = default;
|
||||
EntityUid target = default;
|
||||
|
||||
EntityUid mob2 = default;
|
||||
EntityUid mob3 = default;
|
||||
EntityUid target2 = default;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
// Spawn two targets to interact with
|
||||
target = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
target2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
|
||||
// Spawn a mob which is interacting with the first target
|
||||
mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 5, ev, null, target) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args));
|
||||
|
||||
// Spawn two more mobs which are interacting with the second target
|
||||
mob2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args2 = new DoAfterArgs(entityManager, mob2, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args2));
|
||||
|
||||
mob3 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args3 = new DoAfterArgs(entityManager, mob3, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args3));
|
||||
});
|
||||
|
||||
var list = new HashSet<EntityUid>();
|
||||
interactionSystem.GetEntitiesInteractingWithTarget(target, list);
|
||||
Assert.That(list, Is.EquivalentTo([mob]), $"{mob} was not considered to be interacting with {target}");
|
||||
|
||||
interactionSystem.GetEntitiesInteractingWithTarget(target2, list);
|
||||
Assert.That(list, Is.EquivalentTo([mob2, mob3]), $"{mob2} and {mob3} were not considered to be interacting with {target2}");
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
entityManager.DeleteEntity(mob);
|
||||
entityManager.DeleteEntity(mob2);
|
||||
entityManager.DeleteEntity(mob3);
|
||||
entityManager.DeleteEntity(target);
|
||||
entityManager.DeleteEntity(target2);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,6 @@ public abstract partial class InteractionTest
|
||||
public static implicit operator EntitySpecifier(string prototype)
|
||||
=> new(prototype, 1);
|
||||
|
||||
public static implicit operator EntitySpecifier(EntProtoId prototype)
|
||||
=> new(prototype.Id, 1);
|
||||
|
||||
public static implicit operator EntitySpecifier((string, int) tuple)
|
||||
=> new(tuple.Item1, tuple.Item2);
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
nodeGroupID: HVPower
|
||||
- type: PowerNetworkBattery
|
||||
- type: Battery
|
||||
netsync: false
|
||||
- type: BatteryCharger
|
||||
|
||||
- type: entity
|
||||
@@ -69,7 +68,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
nodeGroupID: HVPower
|
||||
- type: PowerNetworkBattery
|
||||
- type: Battery
|
||||
netsync: false
|
||||
- type: BatteryDischarger
|
||||
|
||||
- type: entity
|
||||
@@ -87,7 +85,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
nodeGroupID: HVPower
|
||||
- type: PowerNetworkBattery
|
||||
- type: Battery
|
||||
netsync: false
|
||||
- type: BatteryDischarger
|
||||
node: output
|
||||
- type: BatteryCharger
|
||||
@@ -113,7 +110,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
maxSupply: 1000
|
||||
supplyRampTolerance: 1000
|
||||
- type: Battery
|
||||
netsync: false
|
||||
maxCharge: 1000
|
||||
startingCharge: 1000
|
||||
- type: Transform
|
||||
@@ -123,7 +119,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
id: ApcDummy
|
||||
components:
|
||||
- type: Battery
|
||||
netsync: false
|
||||
maxCharge: 10000
|
||||
startingCharge: 10000
|
||||
- type: PowerNetworkBattery
|
||||
@@ -385,8 +380,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
const float startingCharge = 100_000;
|
||||
|
||||
PowerNetworkBatteryComponent netBattery = default!;
|
||||
EntityUid generatorEnt = default!;
|
||||
EntityUid consumerEnt = default!;
|
||||
BatteryComponent battery = default!;
|
||||
PowerConsumerComponent consumer = default!;
|
||||
|
||||
@@ -402,8 +395,8 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
|
||||
}
|
||||
|
||||
generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
|
||||
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
|
||||
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
|
||||
netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(generatorEnt);
|
||||
battery = entityManager.GetComponent<BatteryComponent>(generatorEnt);
|
||||
@@ -448,8 +441,7 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
|
||||
// Trivial integral to calculate expected power spent.
|
||||
const double spentExpected = (200 + 100) / 2.0 * 0.25;
|
||||
var currentCharge = batterySys.GetCharge((generatorEnt, battery));
|
||||
Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
|
||||
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -468,8 +460,7 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
|
||||
// Trivial integral to calculate expected power spent.
|
||||
const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25;
|
||||
var currentCharge = batterySys.GetCharge((generatorEnt, battery));
|
||||
Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
|
||||
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -585,8 +576,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var batterySys = entityManager.System<BatterySystem>();
|
||||
var mapSys = entityManager.System<SharedMapSystem>();
|
||||
EntityUid generatorEnt = default!;
|
||||
EntityUid batteryEnt = default!;
|
||||
PowerSupplierComponent supplier = default!;
|
||||
BatteryComponent battery = default!;
|
||||
|
||||
@@ -602,8 +591,8 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
|
||||
}
|
||||
|
||||
generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
|
||||
batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
|
||||
var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
|
||||
supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
|
||||
var netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(batteryEnt);
|
||||
@@ -626,8 +615,7 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
{
|
||||
// half a second @ 500 W = 250
|
||||
// 50% efficiency, so 125 J stored total.
|
||||
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
|
||||
Assert.That(currentCharge, Is.EqualTo(125).Within(0.1));
|
||||
Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1));
|
||||
Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1));
|
||||
});
|
||||
});
|
||||
@@ -645,9 +633,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
var gameTiming = server.ResolveDependency<IGameTiming>();
|
||||
var batterySys = entityManager.System<BatterySystem>();
|
||||
var mapSys = entityManager.System<SharedMapSystem>();
|
||||
EntityUid batteryEnt = default!;
|
||||
EntityUid supplyEnt = default!;
|
||||
EntityUid consumerEnt = default!;
|
||||
PowerConsumerComponent consumer = default!;
|
||||
PowerSupplierComponent supplier = default!;
|
||||
PowerNetworkBatteryComponent netBattery = default!;
|
||||
@@ -668,9 +653,9 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
|
||||
entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
|
||||
|
||||
batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
|
||||
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
|
||||
var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
|
||||
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
|
||||
|
||||
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
|
||||
supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
|
||||
@@ -709,8 +694,7 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1));
|
||||
|
||||
const int expectedSpent = 200;
|
||||
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
|
||||
Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
|
||||
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -727,9 +711,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
var gameTiming = server.ResolveDependency<IGameTiming>();
|
||||
var batterySys = entityManager.System<BatterySystem>();
|
||||
var mapSys = entityManager.System<SharedMapSystem>();
|
||||
EntityUid batteryEnt = default!;
|
||||
EntityUid supplyEnt = default!;
|
||||
EntityUid consumerEnt = default!;
|
||||
PowerConsumerComponent consumer = default!;
|
||||
PowerSupplierComponent supplier = default!;
|
||||
PowerNetworkBatteryComponent netBattery = default!;
|
||||
@@ -750,9 +731,9 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
|
||||
entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
|
||||
|
||||
batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
|
||||
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
|
||||
var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
|
||||
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
|
||||
|
||||
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
|
||||
supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
|
||||
@@ -791,8 +772,7 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1));
|
||||
|
||||
const int expectedSpent = 400;
|
||||
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
|
||||
Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
|
||||
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1243,9 +1223,6 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var batterySys = entityManager.System<BatterySystem>();
|
||||
var mapSys = entityManager.System<SharedMapSystem>();
|
||||
EntityUid generatorEnt = default!;
|
||||
EntityUid substationEnt = default!;
|
||||
EntityUid apcEnt = default!;
|
||||
PowerNetworkBatteryComponent substationNetBattery = default!;
|
||||
BatteryComponent apcBattery = default!;
|
||||
|
||||
@@ -1265,9 +1242,9 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 1));
|
||||
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 2));
|
||||
|
||||
generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
|
||||
substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
|
||||
apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
|
||||
var substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
|
||||
var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
|
||||
|
||||
var generatorSupplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
|
||||
substationNetBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(substationEnt);
|
||||
@@ -1285,9 +1262,8 @@ namespace Content.IntegrationTests.Tests.Power
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var currentCharge = batterySys.GetCharge((apcEnt, apcBattery));
|
||||
Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power
|
||||
Assert.That(currentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
|
||||
Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,16 +3,15 @@ using System.Linq;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Power.NodeGroups;
|
||||
using Content.Server.Power.Pow3r;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Power;
|
||||
|
||||
[Explicit]
|
||||
public sealed class StationPowerTests
|
||||
{
|
||||
/// <summary>
|
||||
@@ -20,32 +19,29 @@ public sealed class StationPowerTests
|
||||
/// </summary>
|
||||
private const float MinimumPowerDurationSeconds = 10 * 60;
|
||||
|
||||
// Begin DeltaV Additions - DeltaV maps for testing
|
||||
private static readonly string[] GameMaps =
|
||||
[
|
||||
"Academy",
|
||||
"Arena",
|
||||
"Asterisk",
|
||||
"Byoin",
|
||||
"Chibi",
|
||||
"Division",
|
||||
"Edge",
|
||||
"Elegance",
|
||||
"Glacier",
|
||||
"Hammurabi",
|
||||
"TheHive",
|
||||
"Lighthouse",
|
||||
"Micro",
|
||||
"Ovni",
|
||||
"Pebble",
|
||||
"Shoukou",
|
||||
"Submarine",
|
||||
"Terra",
|
||||
"Tortuga",
|
||||
"Fland",
|
||||
"Meta",
|
||||
"Packed",
|
||||
"Omega",
|
||||
"Bagel",
|
||||
"Box",
|
||||
"Core",
|
||||
"Marathon",
|
||||
"Saltern",
|
||||
"Reach",
|
||||
"Train",
|
||||
"Oasis",
|
||||
"Gate",
|
||||
"Amber",
|
||||
"Loop",
|
||||
"Plasma",
|
||||
"Elkridge",
|
||||
"Convex",
|
||||
"Relic",
|
||||
];
|
||||
// Begin DeltaV Additions - DeltaV maps for testing
|
||||
|
||||
[Explicit]
|
||||
[Test, TestCaseSource(nameof(GameMaps))]
|
||||
public async Task TestStationStartingPowerWindow(string mapProtoId)
|
||||
{
|
||||
@@ -58,7 +54,6 @@ public sealed class StationPowerTests
|
||||
var entMan = server.EntMan;
|
||||
var protoMan = server.ProtoMan;
|
||||
var ticker = entMan.System<GameTicker>();
|
||||
var batterySys = entMan.System<BatterySystem>();
|
||||
|
||||
// Load the map
|
||||
await server.WaitAssertion(() =>
|
||||
@@ -82,8 +77,7 @@ public sealed class StationPowerTests
|
||||
if (node.NodeGroup is not IBasePowerNet group)
|
||||
continue;
|
||||
networks.TryGetValue(group.NetworkNode, out var charge);
|
||||
var currentCharge = batterySys.GetCharge((uid, battery));
|
||||
networks[group.NetworkNode] = charge + currentCharge;
|
||||
networks[group.NetworkNode] = charge + battery.CurrentCharge;
|
||||
}
|
||||
var totalStartingCharge = networks.MaxBy(n => n.Value).Value;
|
||||
|
||||
@@ -106,54 +100,6 @@ public sealed class StationPowerTests
|
||||
$"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!");
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(GameMaps))]
|
||||
public async Task TestApcLoad(string mapProtoId)
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Dirty = true,
|
||||
});
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.EntMan;
|
||||
var protoMan = server.ProtoMan;
|
||||
var ticker = entMan.System<GameTicker>();
|
||||
var xform = entMan.System<TransformSystem>();
|
||||
|
||||
// Load the map
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(protoMan.TryIndex<GameMapPrototype>(mapProtoId, out var mapProto));
|
||||
var opts = DeserializationOptions.Default with { InitializeMaps = true };
|
||||
ticker.LoadGameMap(mapProto, out var mapId, opts);
|
||||
});
|
||||
|
||||
// Wait long enough for power to ramp up, but before anything can trip
|
||||
await pair.RunSeconds(2);
|
||||
|
||||
// Check that no APCs start overloaded
|
||||
var apcQuery = entMan.EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent>();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
while (apcQuery.MoveNext(out var uid, out var apc, out var battery))
|
||||
{
|
||||
// Uncomment the following line to log starting APC load to the console
|
||||
//Console.WriteLine($"ApcLoad:{mapProtoId}:{uid}:{battery.CurrentSupply}");
|
||||
if (xform.TryGetMapOrGridCoordinates(uid, out var coord))
|
||||
{
|
||||
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
|
||||
$"APC {uid} on {mapProtoId} ({coord.Value.X}, {coord.Value.Y}) is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
|
||||
$"APC {uid} on {mapProtoId} is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Movement.Pulling.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Puller;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public sealed class InteractingEntitiesTest : InteractionTest
|
||||
{
|
||||
private static readonly EntProtoId MobHuman = "MobHuman";
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a Target mob, and a second mob which drags it,
|
||||
/// and checks that the dragger is considered to be interacting with the dragged mob.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PullerIsConsideredInteractingTest()
|
||||
{
|
||||
await SpawnTarget(MobHuman);
|
||||
var puller = await SpawnEntity(MobHuman, ToServer(TargetCoords));
|
||||
|
||||
var pullSys = SEntMan.System<PullingSystem>();
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(pullSys.TryStartPull(puller, ToServer(Target.Value)),
|
||||
$"{puller} failed to start pulling {Target}");
|
||||
});
|
||||
|
||||
var list = new HashSet<EntityUid>();
|
||||
Server.System<SharedInteractionSystem>()
|
||||
.GetEntitiesInteractingWithTarget(ToServer(Target.Value), list);
|
||||
Assert.That(list, Is.EquivalentTo([puller]), $"{puller} was not considered to be interacting with {Target}");
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ public sealed class DockTest : ContentUnitTest
|
||||
{
|
||||
mapGrid = entManager.AddComponent<MapGridComponent>(map.MapUid);
|
||||
entManager.DeleteEntity(map.Grid);
|
||||
var path = new ResPath("/Maps/_DV/Shuttles/Evac/ntes_dart.yml"); // DeltaV - Made evac DV specific, Was: /Maps/Shuttles/emergency.yml
|
||||
var path = new ResPath("/Maps/Shuttles/emergency.yml");
|
||||
Assert.That(entManager.System<MapLoaderSystem>().TryLoadGrid(otherMap.MapId, path, out var grid));
|
||||
shuttle = grid!.Value.Owner;
|
||||
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Containers;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Storage;
|
||||
|
||||
public sealed class StorageTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Can an item store more than itself weighs.
|
||||
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StorageSizeArbitrageTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
|
||||
storage.Whitelist != null ||
|
||||
storage.MaxItemSize == null ||
|
||||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
continue;
|
||||
|
||||
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
|
||||
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
|
||||
$"Found storage arbitrage on {proto.ID}");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestStorageFillPrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
|
||||
continue;
|
||||
|
||||
foreach (var entry in storage.Contents)
|
||||
{
|
||||
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
|
||||
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
|
||||
allSizes.Sort();
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<EntityStorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
StorageComponent? storage = null;
|
||||
ItemComponent? item = null;
|
||||
var size = 0;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("Storage", out storage))
|
||||
{
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
return;
|
||||
}
|
||||
|
||||
proto.TryGetComponent("Item", out item);
|
||||
size = GetFillSize(fill, false, protoMan, itemSys);
|
||||
});
|
||||
|
||||
if (storage == null)
|
||||
continue;
|
||||
|
||||
var maxSize = storage.MaxItemSize;
|
||||
if (storage.MaxItemSize == null)
|
||||
{
|
||||
if (item?.Size == null)
|
||||
{
|
||||
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
|
||||
var index = Math.Max(0, curIndex - 1);
|
||||
maxSize = allSizes[index].ID;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSize == null)
|
||||
continue;
|
||||
|
||||
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
|
||||
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
continue;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
|
||||
continue;
|
||||
|
||||
ItemComponent? entryItem = null;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
fillItem.TryGetComponent("Item", out entryItem);
|
||||
});
|
||||
|
||||
if (entryItem == null)
|
||||
continue;
|
||||
|
||||
Assert.That(protoMan.Index(entryItem.Size).Weight,
|
||||
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
|
||||
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForEntityStorageFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<StorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
|
||||
if (entStorage == null)
|
||||
return;
|
||||
|
||||
var size = GetFillSize(fill, true, protoMan, itemSys);
|
||||
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
|
||||
$"{proto.ID} storage fill is too large.");
|
||||
});
|
||||
}
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
return 0;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
|
||||
{
|
||||
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (getCount)
|
||||
return entry.Amount;
|
||||
|
||||
|
||||
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
|
||||
|
||||
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
var totalSize = 0;
|
||||
var groups = new Dictionary<string, int>();
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
|
||||
|
||||
if (entry.GroupId == null)
|
||||
totalSize += size;
|
||||
else
|
||||
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
|
||||
}
|
||||
|
||||
return totalSize + groups.Values.Sum();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that prototypes are not using multiple container fill components at the same time.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task NoMultipleContainerFillsTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var compFact = pair.Server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<EntityTableContainerFillComponent>())
|
||||
{
|
||||
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(StorageFillComponent)}.");
|
||||
Assert.That(!proto.HasComponent<ContainerFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(ContainerFillComponent)}.");
|
||||
}
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<ContainerFillComponent>())
|
||||
{
|
||||
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(ContainerFillComponent)} and {nameof(StorageFillComponent)}.");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public sealed class StorageTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Can an item store more than itself weighs.
|
||||
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StorageSizeArbitrageTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
|
||||
storage.Whitelist != null ||
|
||||
storage.MaxItemSize == null ||
|
||||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
continue;
|
||||
|
||||
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
|
||||
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
|
||||
$"Found storage arbitrage on {proto.ID}");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestStorageFillPrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
|
||||
continue;
|
||||
|
||||
foreach (var entry in storage.Contents)
|
||||
{
|
||||
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
|
||||
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
|
||||
allSizes.Sort();
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<EntityStorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
StorageComponent? storage = null;
|
||||
ItemComponent? item = null;
|
||||
var size = 0;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("Storage", out storage))
|
||||
{
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
return;
|
||||
}
|
||||
|
||||
proto.TryGetComponent("Item", out item);
|
||||
size = GetFillSize(fill, false, protoMan, itemSys);
|
||||
});
|
||||
|
||||
if (storage == null)
|
||||
continue;
|
||||
|
||||
var maxSize = storage.MaxItemSize;
|
||||
if (storage.MaxItemSize == null)
|
||||
{
|
||||
if (item?.Size == null)
|
||||
{
|
||||
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
|
||||
var index = Math.Max(0, curIndex - 1);
|
||||
maxSize = allSizes[index].ID;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSize == null)
|
||||
continue;
|
||||
|
||||
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
|
||||
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
continue;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
|
||||
continue;
|
||||
|
||||
ItemComponent? entryItem = null;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
fillItem.TryGetComponent("Item", out entryItem);
|
||||
});
|
||||
|
||||
if (entryItem == null)
|
||||
continue;
|
||||
|
||||
Assert.That(protoMan.Index(entryItem.Size).Weight,
|
||||
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
|
||||
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForEntityStorageFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<StorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
|
||||
if (entStorage == null)
|
||||
return;
|
||||
|
||||
var size = GetFillSize(fill, true, protoMan, itemSys);
|
||||
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
|
||||
$"{proto.ID} storage fill is too large.");
|
||||
});
|
||||
}
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
return 0;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
|
||||
{
|
||||
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (getCount)
|
||||
return entry.Amount;
|
||||
|
||||
|
||||
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
|
||||
|
||||
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
var totalSize = 0;
|
||||
var groups = new Dictionary<string, int>();
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
|
||||
|
||||
if (entry.GroupId == null)
|
||||
totalSize += size;
|
||||
else
|
||||
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
|
||||
}
|
||||
|
||||
return totalSize + groups.Values.Sum();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,11 @@ using System.Collections.Generic;
|
||||
using Content.Server.VendingMachines;
|
||||
using Content.Server.Wires;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Content.Shared.Containers;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Content.Shared.Wires;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -117,7 +115,6 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var entityTable = server.EntMan.System<EntityTableSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
@@ -137,23 +134,17 @@ namespace Content.IntegrationTests.Tests
|
||||
restocks.Add(proto.ID);
|
||||
}
|
||||
|
||||
// Collect all the prototypes with EntityTableContainerFills referencing those entities.
|
||||
// Collect all the prototypes with StorageFills referencing those entities.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<EntityTableContainerFillComponent>(out var storage, compFact))
|
||||
continue;
|
||||
|
||||
var containers = storage.Containers;
|
||||
|
||||
if (!containers.TryGetValue(SharedEntityStorageSystem.ContainerName, out var container)) // We only care about this container type.
|
||||
if (!proto.TryGetComponent<StorageFillComponent>(out var storage, compFact))
|
||||
continue;
|
||||
|
||||
List<string> restockStore = new();
|
||||
|
||||
foreach (var spawnEntry in entityTable.GetSpawns(container))
|
||||
foreach (var spawnEntry in storage.Contents)
|
||||
{
|
||||
if (restocks.Contains(spawnEntry))
|
||||
restockStore.Add(spawnEntry);
|
||||
if (spawnEntry.PrototypeId != null && restocks.Contains(spawnEntry.PrototypeId))
|
||||
restockStore.Add(spawnEntry.PrototypeId);
|
||||
}
|
||||
|
||||
if (restockStore.Count > 0)
|
||||
@@ -162,7 +153,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
// Iterate through every CargoProduct and make sure each
|
||||
// prototype with a restock component is referenced in a
|
||||
// purchaseable entity with an EntityTableContianerFill.
|
||||
// purchaseable entity with a StorageFill.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<CargoProductPrototype>())
|
||||
{
|
||||
if (restockStores.ContainsKey(proto.Product))
|
||||
|
||||
@@ -28,9 +28,7 @@ public sealed class ShipyardTest
|
||||
{
|
||||
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
|
||||
{
|
||||
var shuttleCreated = shipyard.TryCreateShuttle(vessel.Path, out var shuttle);
|
||||
|
||||
Assert.That(shuttleCreated, Is.True, $"TryCreateShuttle returned false for {vessel.ID}!");
|
||||
var shuttle = shipyard.TryCreateShuttle(vessel.Path);
|
||||
Assert.That(shuttle, Is.Not.Null, $"Failed to spawn shuttle {vessel.ID}!");
|
||||
|
||||
var value = pricing.AppraiseGrid(shuttle.Value);
|
||||
@@ -59,9 +57,7 @@ public sealed class ShipyardTest
|
||||
{
|
||||
foreach (var vessel in proto.EnumeratePrototypes<VesselPrototype>())
|
||||
{
|
||||
var shuttleCreated = shipyard.TryCreateShuttle(vessel.Path, out var shuttle);
|
||||
|
||||
Assert.That(shuttleCreated, Is.True, $"TryCreateShuttle returned false for {vessel.ID}!");
|
||||
var shuttle = shipyard.TryCreateShuttle(vessel.Path);
|
||||
Assert.That(shuttle, Is.Not.Null, $"Failed to spawn shuttle {vessel.ID}!");
|
||||
|
||||
var console = FindComponent<ShuttleConsoleComponent>(entities, shuttle.Value);
|
||||
|
||||
@@ -1,801 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Content.Server._DV.Traits;
|
||||
using Content.Shared._DV.Traits;
|
||||
using Content.Shared._DV.Traits.Conditions;
|
||||
using Content.Shared._DV.Traits.Effects;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests._DV;
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive integration tests for the trait system.
|
||||
/// Tests all conditions, effects, and validation logic.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[TestOf(typeof(TraitSystem))]
|
||||
public sealed partial class TraitSystemTest
|
||||
{
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
# Test Trait Categories
|
||||
- type: traitCategory
|
||||
id: TestCategoryUnlimited
|
||||
name: trait-dysgraphia-name
|
||||
maxTraits: null
|
||||
maxPoints: null
|
||||
|
||||
- type: traitCategory
|
||||
id: TestCategoryLimited
|
||||
name: trait-dysgraphia-name
|
||||
maxTraits: 2
|
||||
maxPoints: 10
|
||||
|
||||
# Test Traits - Conditions
|
||||
- type: trait
|
||||
id: TestTraitHasComp
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
conditions:
|
||||
- !type:HasCompCondition
|
||||
component: Hunger
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
|
||||
# Test Traits - Effects
|
||||
- type: trait
|
||||
id: TestTraitAddComps
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
- type: Hunger
|
||||
|
||||
- type: trait
|
||||
id: TestTraitOverrideComps
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:OverrideCompsEffect
|
||||
components:
|
||||
- type: Hunger
|
||||
|
||||
- type: trait
|
||||
id: TestTraitRemComps
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:RemCompsEffect
|
||||
components:
|
||||
- Hunger
|
||||
- Thirst
|
||||
|
||||
- type: trait
|
||||
id: TestTraitSpawnItem
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:SpawnItemInHandEffect
|
||||
item: Pen
|
||||
|
||||
# Test Traits - Validation
|
||||
- type: trait
|
||||
id: TestTraitConflictA
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
conditions:
|
||||
- !type:HasTraitCondition
|
||||
trait: TestTraitConflictB
|
||||
invert: true
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
- type: trait
|
||||
id: TestTraitConflictB
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryUnlimited
|
||||
cost: 0
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
- type: trait
|
||||
id: TestTraitLimited1
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryLimited
|
||||
cost: 5
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
- type: trait
|
||||
id: TestTraitLimited2
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryLimited
|
||||
cost: 5
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
|
||||
- type: trait
|
||||
id: TestTraitLimited3
|
||||
name: trait-dysgraphia-name
|
||||
description: trait-dysgraphia-name
|
||||
category: TestCategoryLimited
|
||||
cost: 5
|
||||
effects:
|
||||
- !type:AddCompsEffect
|
||||
components:
|
||||
- type: Test
|
||||
";
|
||||
|
||||
#region Condition Tests
|
||||
|
||||
[RegisterComponent]
|
||||
private sealed partial class TestComponent : Component;
|
||||
|
||||
[Test]
|
||||
public async Task HasCompCondition_WithComponent_ReturnsTrue()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
var condition = new HasCompCondition { Component = "Hunger" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player);
|
||||
|
||||
Assert.That(condition.Evaluate(ctx), Is.True, "HasCompCondition should return true when component exists");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasCompCondition_WithoutComponent_ReturnsFalse()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new HasCompCondition { Component = "Hunger" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player);
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.False,
|
||||
"HasCompCondition should return false when component doesn't exist");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasCompCondition_Inverted_ReturnsOpposite()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
var condition = new HasCompCondition { Component = "Hunger", Invert = true };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player);
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.False,
|
||||
"Inverted HasCompCondition should return false when component exists");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasJobCondition_MatchingJob_ReturnsTrue()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new HasJobCondition { Job = "MedicalDoctor" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, "MedicalDoctor");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx), Is.True, "HasJobCondition should return true for matching job");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task HasJobCondition_DifferentJob_ReturnsFalse()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new HasJobCondition { Job = "MedicalDoctor" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, "SecurityOfficer");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx), Is.False, "HasJobCondition should return false for different job");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InDepartmentCondition_JobInDepartment_ReturnsTrue()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new InDepartmentCondition { Department = "Medical" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, "MedicalDoctor");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.True,
|
||||
"InDepartmentCondition should return true when job is in department");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InDepartmentCondition_JobNotInDepartment_ReturnsFalse()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new InDepartmentCondition { Department = "Medical" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, "SecurityOfficer");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.False,
|
||||
"InDepartmentCondition should return false when job is not in department");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsSpeciesCondition_MatchingSpecies_ReturnsTrue()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new IsSpeciesCondition { Species = "Human" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, speciesId: "Human");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx), Is.True, "IsSpeciesCondition should return true for matching species");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task IsSpeciesCondition_DifferentSpecies_ReturnsFalse()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var condition = new IsSpeciesCondition { Species = "Human" };
|
||||
var ctx = CreateContext(entMan, protoMan, factory, player, speciesId: "Vox");
|
||||
|
||||
Assert.That(condition.Evaluate(ctx),
|
||||
Is.False,
|
||||
"IsSpeciesCondition should return false for different species");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Effect Tests
|
||||
|
||||
[Test]
|
||||
public async Task AddCompsEffect_AddsComponents()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
Assert.That(entMan.HasComponent<HungerComponent>(player),
|
||||
Is.False,
|
||||
"Player should not start with HungerComponent");
|
||||
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitAddComps"));
|
||||
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
effect.Apply(ctx);
|
||||
}
|
||||
|
||||
Assert.That(entMan.HasComponent<HungerComponent>(player),
|
||||
Is.True,
|
||||
"AddCompsEffect should add HungerComponent");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task AddCompsEffect_DoesNotOverwrite()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var hungerBefore = entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitAddComps"));
|
||||
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
effect.Apply(ctx);
|
||||
}
|
||||
|
||||
var hungerAfter = entMan.GetComponent<HungerComponent>(player);
|
||||
Assert.That(hungerAfter,
|
||||
Is.SameAs(hungerBefore),
|
||||
"AddCompsEffect should not replace existing component instance");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OverrideCompsEffect_OverwritesComponent()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
var hungerBefore = entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitOverrideComps"));
|
||||
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
effect.Apply(ctx);
|
||||
}
|
||||
|
||||
var hungerAfter = entMan.GetComponent<HungerComponent>(player);
|
||||
Assert.That(hungerAfter,
|
||||
Is.Not.SameAs(hungerBefore),
|
||||
"OverrideCompsEffect should replace existing component instance");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RemCompsEffect_RemovesComponents()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var factory = server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
entMan.AddComponent<HungerComponent>(player);
|
||||
entMan.AddComponent<ThirstComponent>(player);
|
||||
|
||||
Assert.That(entMan.HasComponent<HungerComponent>(player),
|
||||
Is.True,
|
||||
"Player should start with HungerComponent");
|
||||
Assert.That(entMan.HasComponent<ThirstComponent>(player),
|
||||
Is.True,
|
||||
"Player should start with ThirstComponent");
|
||||
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitRemComps"));
|
||||
var ctx = CreateEffectContext(entMan, protoMan, factory, player);
|
||||
|
||||
foreach (var effect in trait.Effects)
|
||||
{
|
||||
effect.Apply(ctx);
|
||||
}
|
||||
|
||||
Assert.That(entMan.HasComponent<HungerComponent>(player),
|
||||
Is.False,
|
||||
"RemCompsEffect should remove HungerComponent");
|
||||
Assert.That(entMan.HasComponent<ThirstComponent>(player),
|
||||
Is.False,
|
||||
"RemCompsEffect should remove ThirstComponent");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task SpawnItemInHandEffect_SpawnsItem()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity("MobHuman", MapCoordinates.Nullspace);
|
||||
var handsSys = entMan.System<SharedHandsSystem>();
|
||||
var hands = entMan.GetComponent<HandsComponent>(player);
|
||||
|
||||
Assert.That(handsSys.GetActiveItem((player, hands)), Is.Null, "Player should start with empty hands");
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var trait = protoMan.Index(new ProtoId<TraitPrototype>("TestTraitSpawnItem"));
|
||||
|
||||
// We need to use reflection to call the private ApplyTrait method
|
||||
var method = typeof(TraitSystem).GetMethod("ApplyTrait",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
method?.Invoke(traitSys, new object[] { player, trait });
|
||||
|
||||
var item = handsSys.GetActiveItem((player, hands));
|
||||
Assert.That(item, Is.Not.Null, "SpawnItemInHandEffect should spawn item in hand");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validation Tests
|
||||
|
||||
[Test]
|
||||
public async Task RespectsConflicts()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ProtoMan;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
var profile = new HumanoidCharacterProfile()
|
||||
.WithTraitPreference("TestTraitConflictA", protoMan)
|
||||
.WithTraitPreference("TestTraitConflictB", protoMan);
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var disabledTraits = new Dictionary<ProtoId<TraitPrototype>, List<string>>();
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, profile.TraitPreferences, null, null, null, profile, disabledTraits });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(validTraits?.Count, Is.EqualTo(1), "Only one conflicting trait should be valid");
|
||||
Assert.That(validTraits.Contains("TestTraitConflictB"), Is.True, "Non-complaining trait should be kept");
|
||||
Assert.That(disabledTraits.ContainsKey("TestTraitConflictA"), Is.True, "Trait with conflicts should be rejected");
|
||||
});
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RespectsCategoryLimits()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
// TestCategoryLimited has maxTraits: 2
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitLimited1",
|
||||
"TestTraitLimited2",
|
||||
"TestTraitLimited3",
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
|
||||
|
||||
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxTraits limit");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RespectsCategoryPointLimits()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
|
||||
// TestCategoryLimited has maxPoints: 10, each trait costs 5
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitLimited1",
|
||||
"TestTraitLimited2",
|
||||
"TestTraitLimited3", // This would exceed the 10 point limit
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
|
||||
|
||||
Assert.That(validTraits?.Count, Is.EqualTo(2), "Should respect category maxPoints limit");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ChecksConditionsOnSpawn()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
entMan.AddComponent<HungerComponent>(player);
|
||||
|
||||
// Trait requires HungerComponent
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitHasComp",
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
|
||||
|
||||
Assert.That(validTraits?.Contains("TestTraitHasComp"), Is.True, "Trait with met condition should be valid");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RejectsTraitsWithUnmetConditions()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true });
|
||||
var server = pair.Server;
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = entMan.SpawnEntity(null, MapCoordinates.Nullspace);
|
||||
// Player does NOT have HungerComponent
|
||||
|
||||
// Trait requires HungerComponent
|
||||
var selectedTraits = new HashSet<ProtoId<TraitPrototype>>
|
||||
{
|
||||
"TestTraitHasComp",
|
||||
};
|
||||
|
||||
var traitSys = entMan.System<TraitSystem>();
|
||||
var method = typeof(TraitSystem).GetMethod("ValidateTraits",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
var validTraits = (HashSet<ProtoId<TraitPrototype>>)method?.Invoke(traitSys,
|
||||
new object[] { player, selectedTraits, null, null, null, null, new Dictionary<ProtoId<TraitPrototype>, List<string>>() });
|
||||
|
||||
Assert.That(validTraits?.Contains("TestTraitHasComp"),
|
||||
Is.False,
|
||||
"Trait with unmet condition should be rejected");
|
||||
|
||||
entMan.DeleteEntity(player);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static TraitConditionContext CreateContext(
|
||||
IEntityManager entMan,
|
||||
IPrototypeManager protoMan,
|
||||
IComponentFactory factory,
|
||||
EntityUid player,
|
||||
ProtoId<JobPrototype>? jobId = null,
|
||||
ProtoId<SpeciesPrototype>? speciesId = null)
|
||||
{
|
||||
return new TraitConditionContext
|
||||
{
|
||||
Player = player,
|
||||
Session = null,
|
||||
EntMan = entMan,
|
||||
Proto = protoMan,
|
||||
CompFactory = factory,
|
||||
LogMan = IoCManager.Resolve<ILogManager>(),
|
||||
JobId = jobId,
|
||||
SpeciesId = speciesId,
|
||||
};
|
||||
}
|
||||
|
||||
private static TraitEffectContext CreateEffectContext(
|
||||
IEntityManager entMan,
|
||||
IPrototypeManager protoMan,
|
||||
IComponentFactory factory,
|
||||
EntityUid player)
|
||||
{
|
||||
return new TraitEffectContext
|
||||
{
|
||||
Player = player,
|
||||
EntMan = entMan,
|
||||
Proto = protoMan,
|
||||
CompFactory = factory,
|
||||
LogMan = IoCManager.Resolve<ILogManager>(),
|
||||
Transform = entMan.GetComponent<TransformComponent>(player),
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// File to store DeltaV-specific database models
|
||||
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Content.Server.Database;
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class DVModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores tip dismissal preferences for a player, tracking which tips they've marked
|
||||
/// as "Don't show again".
|
||||
/// </summary>
|
||||
[Table("dv_seen_tips")]
|
||||
[Index(nameof(PlayerUserId), nameof(TipProtoId), IsUnique = true)]
|
||||
public class SeenTip
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The player's user ID (GUID). References the Player table.
|
||||
/// </summary>
|
||||
public Guid PlayerUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The prototype ID of the tip that was dismissed.
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string TipProtoId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When the tip was dismissed.
|
||||
/// </summary>
|
||||
public DateTime DismissedAt { get; set; }
|
||||
}
|
||||
}
|
||||
-2273
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user