forked from space-syndicate/space-station-14
Compare commits
59 Commits
backup-mas
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cbe725b93b | |||
|
|
ef44a42bf8 | ||
|
|
9dbe10cbe7 | ||
|
|
66b43fd8dc | ||
| 2002a26c8e | |||
|
|
8a48801421 | ||
|
|
f3d79333a0 | ||
| da6fca5780 | |||
| eab76b4a67 | |||
| 6bb874ddb2 | |||
| a25b98fe0c | |||
| c1e56d9c98 | |||
| b840ef7255 | |||
| 8a46ade2b0 | |||
| cf2b1337cb | |||
| 91dea91380 | |||
| c36b5cbcb4 | |||
| ac7c948c04 | |||
| 8c1c2e7c32 | |||
| 11723d91e7 | |||
| b4bb992ea3 | |||
| 11fe0d71b4 | |||
| 5b727c1ce7 | |||
| 5149bb5388 | |||
| fbdbf6448b | |||
| efecd13de3 | |||
| 116307b057 | |||
| 0d352835fe | |||
| d6690e2c92 | |||
| e62028546a | |||
| b6f500a292 | |||
| 0a5b85f717 | |||
| 4492041b23 | |||
| d4fe79b62f | |||
| d5cf4f9c13 | |||
| 77bb43e231 | |||
| 159c0c1aab | |||
| 394279fb67 | |||
| 3748846615 | |||
| f358028232 | |||
| e7370ce980 | |||
| 242f56a4da | |||
| 4044ce7cf5 | |||
| b3ae4dce38 | |||
| 40ea691463 | |||
| 5d9b97d867 | |||
| aaba6ddda9 | |||
| 33698e02a1 | |||
| 0da8201aaf | |||
| da289f3b1d | |||
| de14c9ad56 | |||
| 150f0f5958 | |||
| 9ae22ebdde | |||
|
|
b05226b96e | ||
|
|
a3c821e65d | ||
|
|
f1af1d6f9d | ||
|
|
ea7d8e012b | ||
|
|
f92728c3c6 | ||
|
|
c24087dcce |
15
.github/actions/cache-dotnet/action.yml
vendored
Normal file
15
.github/actions/cache-dotnet/action.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: Cache .NET dependencies
|
||||
description: Cache NuGet packages using Gitea Actions cache server
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
ACTIONS_CACHE_URL: ${{ vars.ACTIONS_CACHE_URL }}
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
56
.github/workflows/benchmarks.yml
vendored
56
.github/workflows/benchmarks.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Benchmarks
|
||||
name: Benchmarks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
@@ -10,38 +11,31 @@ jobs:
|
||||
benchmark:
|
||||
name: Run Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
- name: Get Engine version
|
||||
run: |
|
||||
cd RobustToolbox
|
||||
git fetch --depth=1
|
||||
echo "::set-output name=out::$(git rev-parse HEAD)"
|
||||
id: engine_version
|
||||
- name: Run script on centcomm
|
||||
uses: appleboy/ssh-action@master
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
host: centcomm.spacestation14.io
|
||||
username: robust-benchmark-runner
|
||||
key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }}
|
||||
command_timeout: 100000m
|
||||
script: |
|
||||
mkdir benchmark_run_content_${{ github.sha }}
|
||||
cd benchmark_run_content_${{ github.sha }}
|
||||
git clone https://github.com/space-wizards/space-station-14.git repo_dir --recursive
|
||||
cd repo_dir
|
||||
git checkout ${{ github.sha }}
|
||||
cd Content.Benchmarks
|
||||
dotnet restore
|
||||
export ROBUST_BENCHMARKS_ENABLE_SQL=1
|
||||
export ROBUST_BENCHMARKS_SQL_ADDRESS="${{ secrets.BENCHMARKS_WRITE_ADDRESS }}"
|
||||
export ROBUST_BENCHMARKS_SQL_PORT="${{ secrets.BENCHMARKS_WRITE_PORT }}"
|
||||
export ROBUST_BENCHMARKS_SQL_USER="${{ secrets.BENCHMARKS_WRITE_USER }}"
|
||||
export ROBUST_BENCHMARKS_SQL_PASSWORD="${{ secrets.BENCHMARKS_WRITE_PASSWORD }}"
|
||||
export ROBUST_BENCHMARKS_SQL_DATABASE="content_benchmarks"
|
||||
export GITHUB_SHA="${{ github.sha }}"
|
||||
dotnet run --filter '*' --configuration Release
|
||||
cd ../../..
|
||||
rm -rf benchmark_run_content_${{ github.sha }}
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install Dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Run Benchmarks
|
||||
env:
|
||||
ROBUST_BENCHMARKS_ENABLE_SQL: "1"
|
||||
ROBUST_BENCHMARKS_SQL_ADDRESS: ${{ secrets.BENCHMARKS_SQL_HOST }}
|
||||
ROBUST_BENCHMARKS_SQL_PORT: ${{ secrets.BENCHMARKS_SQL_PORT }}
|
||||
ROBUST_BENCHMARKS_SQL_USER: ${{ secrets.BENCHMARKS_SQL_USER }}
|
||||
ROBUST_BENCHMARKS_SQL_PASSWORD: ${{ secrets.BENCHMARKS_SQL_PASSWORD }}
|
||||
ROBUST_BENCHMARKS_SQL_DATABASE: ${{ secrets.BENCHMARKS_SQL_DATABASE }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
cd Content.Benchmarks
|
||||
dotnet run --filter '*' --configuration Release
|
||||
|
||||
2
.github/workflows/build-docfx.yml
vendored
2
.github/workflows/build-docfx.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
jobs:
|
||||
docfx:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Setup submodule
|
||||
|
||||
2
.github/workflows/build-map-renderer.yml
vendored
2
.github/workflows/build-map-renderer.yml
vendored
@@ -16,6 +16,8 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
|
||||
12
.github/workflows/build-test-debug.yml
vendored
12
.github/workflows/build-test-debug.yml
vendored
@@ -16,11 +16,19 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Delete Wylab override files (duplicates upstream for customization)
|
||||
run: |
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/events.yml
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/pests.yml
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/subgamemodes.yml
|
||||
|
||||
- name: Setup Submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
@@ -48,10 +56,8 @@ jobs:
|
||||
run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
|
||||
|
||||
- name: Run Content.IntegrationTests
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:DOTNET_gcServer=1
|
||||
dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
|
||||
DOTNET_gcServer=1 dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
|
||||
ci-success:
|
||||
name: Build & Test Debug
|
||||
needs:
|
||||
|
||||
14
.github/workflows/labeler-approve.yml
vendored
14
.github/workflows/labeler-approve.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: Approve"
|
||||
name: "Labels: Approve"
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
@@ -11,8 +11,10 @@ jobs:
|
||||
if: github.event.review.state == 'approved'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
Status: Needs Review
|
||||
Status: Awaiting Changes
|
||||
- name: Remove review labels
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Needs%20Review" || true
|
||||
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Awaiting%20Changes" || true
|
||||
|
||||
16
.github/workflows/labeler-changes.yml
vendored
16
.github/workflows/labeler-changes.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: Changes"
|
||||
name: "Labels: Changes"
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
@@ -11,9 +11,11 @@ jobs:
|
||||
if: github.event.review.state == 'changes_requested'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "Status: Awaiting Changes"
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: "Status: Needs Review"
|
||||
- name: Update labels
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
curl -sS -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"labels":["Status: Awaiting Changes"]}' "$API"
|
||||
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/Status%3A%20Needs%20Review" || true
|
||||
|
||||
61
.github/workflows/labeler-conflict.yml
vendored
61
.github/workflows/labeler-conflict.yml
vendored
@@ -9,13 +9,58 @@ on:
|
||||
- ready_for_review
|
||||
|
||||
jobs:
|
||||
Label:
|
||||
if: ( github.event.pull_request.draft == false ) && ( github.actor != 'IanComradeBot' )
|
||||
check-conflicts:
|
||||
if: github.event.pull_request.draft == false && github.actor != 'IanComradeBot'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for Merge Conflicts
|
||||
uses: eps1lon/actions-label-merge-conflict@v3.0.0
|
||||
with:
|
||||
dirtyLabel: "S: Merge Conflict"
|
||||
repoToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."
|
||||
- name: Check mergeable status and label
|
||||
env:
|
||||
API_TOKEN: ${{ secrets.API_TOKEN }}
|
||||
run: |
|
||||
PR_INDEX=${{ github.event.pull_request.number }}
|
||||
REPO_OWNER=${{ github.repository_owner }}
|
||||
REPO_NAME=${{ github.event.repository.name }}
|
||||
API_URL="${{ github.server_url }}/api/v1"
|
||||
|
||||
# Get PR mergeable status
|
||||
PR_DATA=$(curl -s -H "Authorization: token $API_TOKEN" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/pulls/$PR_INDEX")
|
||||
MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable')
|
||||
|
||||
echo "PR #$PR_INDEX mergeable status: $MERGEABLE"
|
||||
|
||||
LABEL_NAME="S: Merge Conflict"
|
||||
|
||||
if [ "$MERGEABLE" = "false" ]; then
|
||||
echo "PR has merge conflicts, adding label and comment..."
|
||||
|
||||
# Add label
|
||||
curl -s -X POST -H "Authorization: token $API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels" \
|
||||
-d "{\"labels\":[\"$LABEL_NAME\"]}"
|
||||
|
||||
# Add comment
|
||||
curl -s -X POST -H "Authorization: token $API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/comments" \
|
||||
-d '{"body":"This pull request has conflicts, please resolve those before we can evaluate the pull request."}'
|
||||
|
||||
echo "Label and comment added."
|
||||
else
|
||||
echo "PR is mergeable, no conflicts detected."
|
||||
|
||||
# Check if label exists and remove it
|
||||
LABELS=$(curl -s -H "Authorization: token $API_TOKEN" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels")
|
||||
HAS_LABEL=$(echo "$LABELS" | jq -r ".[] | select(.name == \"$LABEL_NAME\") | .id")
|
||||
|
||||
if [ -n "$HAS_LABEL" ]; then
|
||||
echo "Removing stale conflict label..."
|
||||
# URL-encode the label name (handles spaces, colons, etc.)
|
||||
LABEL_NAME_ENCODED=$(echo "$LABEL_NAME" | jq -rR @uri)
|
||||
curl -s -X DELETE -H "Authorization: token $API_TOKEN" \
|
||||
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels/$LABEL_NAME_ENCODED"
|
||||
echo "Conflict label removed."
|
||||
fi
|
||||
fi
|
||||
|
||||
16
.github/workflows/labeler-needsreview.yml
vendored
16
.github/workflows/labeler-needsreview.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: Review"
|
||||
name: "Labels: Review"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -8,9 +8,11 @@ jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "S: Needs Review"
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: "S: Awaiting Changes"
|
||||
- name: Update labels
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
curl -sS -X POST -H "Authorization: token $GITHUB_TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"labels":["S: Needs Review"]}' "$API"
|
||||
curl -sS -X DELETE -H "Authorization: token $GITHUB_TOKEN" "$API/S%3A%20Awaiting%20Changes" || true
|
||||
|
||||
3
.github/workflows/labeler-pr.yml
vendored
3
.github/workflows/labeler-pr.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: PR"
|
||||
name: "Labels: PR"
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
@@ -11,4 +11,5 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/labeler@v5
|
||||
|
||||
23
.github/workflows/labeler-review.yml
vendored
23
.github/workflows/labeler-review.yml
vendored
@@ -1,23 +0,0 @@
|
||||
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 == 'space-wizards/space-station-14') && (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: "content-maintainers,junior-maintainers"
|
||||
GITHUB_TOKEN: ${{ secrets.LABELER_PAT }}
|
||||
- if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
|
||||
uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "S: Approved"
|
||||
12
.github/workflows/labeler-stable.yml
vendored
12
.github/workflows/labeler-stable.yml
vendored
@@ -11,6 +11,12 @@ jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "Branch: Stable"
|
||||
- name: Add branch label
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"labels":["Branch: Stable"]}' \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
|
||||
12
.github/workflows/labeler-staging.yml
vendored
12
.github/workflows/labeler-staging.yml
vendored
@@ -11,6 +11,12 @@ jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
with:
|
||||
labels: "Branch: Staging"
|
||||
- name: Add branch label
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"labels":["Branch: Staging"]}' \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels"
|
||||
|
||||
17
.github/workflows/labeler-untriaged.yml
vendored
17
.github/workflows/labeler-untriaged.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Labels: Untriaged"
|
||||
name: "Labels: Untriaged"
|
||||
|
||||
on:
|
||||
issues:
|
||||
@@ -10,7 +10,14 @@ jobs:
|
||||
add_label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@v1
|
||||
if: join(github.event.issue.labels) == ''
|
||||
with:
|
||||
labels: "S: Untriaged"
|
||||
- name: Add untriaged label
|
||||
if: github.event.issue.labels[0] == null || github.event.pull_request.labels[0] == null
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
NUMBER="${{ github.event.pull_request.number || github.event.issue.number }}"
|
||||
curl -sS -X POST \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"labels":["S: Untriaged"]}' \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/$NUMBER/labels"
|
||||
|
||||
5
.github/workflows/publish-public.yml
vendored
5
.github/workflows/publish-public.yml
vendored
@@ -11,6 +11,8 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
# - name: Install dependencies
|
||||
@@ -42,6 +44,9 @@ jobs:
|
||||
- name: Package client
|
||||
run: dotnet run --project Content.Packaging client --no-wipe-release
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install --break-system-packages requests
|
||||
|
||||
- name: Publish version
|
||||
run: Tools/publish_multi_request.py
|
||||
env:
|
||||
|
||||
5
.github/workflows/publish-testing.yml
vendored
5
.github/workflows/publish-testing.yml
vendored
@@ -12,6 +12,8 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3.6.0
|
||||
@@ -39,6 +41,9 @@ jobs:
|
||||
- name: Package client
|
||||
run: dotnet run --project Content.Packaging client --no-wipe-release
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install --break-system-packages requests
|
||||
|
||||
- name: Publish version
|
||||
run: Tools/publish_multi_request.py --fork-id wizards-testing
|
||||
env:
|
||||
|
||||
85
.github/workflows/publish.yml
vendored
85
.github/workflows/publish.yml
vendored
@@ -6,11 +6,17 @@ concurrency:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CDN_MANIFEST_URL: https://cdn.wylab.me/fork/wylab/manifest
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
steps:
|
||||
- name: Fail if we are attempting to run on the master branch
|
||||
if: ${{GITHUB.REF_NAME == 'master' && github.repository == 'space-wizards/space-station-14'}}
|
||||
@@ -23,7 +29,35 @@ jobs:
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
# Corvax-Secrets-Start
|
||||
- name: Check if build already published
|
||||
id: cdn-check
|
||||
run: |
|
||||
SHA=$(echo "$GITHUB_SHA" | tr '[:upper:]' '[:lower:]')
|
||||
if curl -sSf "$CDN_MANIFEST_URL" | jq -e ".builds[\"$SHA\"]" > /dev/null 2>&1; then
|
||||
echo "Build $SHA already present on CDN; skipping."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Build $SHA not found on CDN; continuing."
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', 'global.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
- name: Cache RobustToolbox build output
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: RobustToolbox/bin
|
||||
key: ${{ runner.os }}-robust-${{ hashFiles('RobustToolbox/**/*.csproj', 'global.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-robust-
|
||||
|
||||
# Wylab-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
@@ -32,46 +66,69 @@ jobs:
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo " Hostname github.com" >> ~/.ssh/config
|
||||
echo " Port 22" >> ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git clone git@github.com:corvax-nexus/secrets.git Secrets
|
||||
cp -R Secrets/Resources/Prototypes Resources/Prototypes/CorvaxSecrets
|
||||
cp -R Secrets/Resources/ServerPrototypes Resources/Prototypes/CorvaxSecretsServer
|
||||
cp -R Secrets/Resources/Locale Resources/Locale/ru-RU/corvax-secrets
|
||||
cp -R Secrets/Resources/Textures Resources/Textures/CorvaxSecrets
|
||||
cp -R Secrets/Resources/Audio Resources/Audio/CorvaxSecrets
|
||||
# Corvax-Secrets-End
|
||||
printf '%s\n' 'Host git.wylab.me' ' Hostname git.wylab.me' ' Port 22' ' User git' ' IdentityFile ~/.ssh/id_rsa' ' StrictHostKeyChecking no' ' UserKnownHostsFile /dev/null' > ~/.ssh/config
|
||||
chmod 600 ~/.ssh/config
|
||||
git clone git@git.wylab.me:wylab/secrets.git Secrets
|
||||
[ -d Secrets/Resources/Prototypes ] && cp -R Secrets/Resources/Prototypes Resources/Prototypes/WylabSecrets
|
||||
[ -d Secrets/Resources/ServerPrototypes ] && cp -R Secrets/Resources/ServerPrototypes Resources/Prototypes/WylabSecretsServer
|
||||
[ -d Secrets/Resources/Locale ] && cp -R Secrets/Resources/Locale Resources/Locale/ru-RU/wylab-secrets
|
||||
[ -d Secrets/Resources/Textures ] && cp -R Secrets/Resources/Textures Resources/Textures/WylabSecrets
|
||||
[ -d Secrets/Resources/Audio ] && cp -R Secrets/Resources/Audio Resources/Audio/WylabSecrets
|
||||
# Wylab-Secrets-End
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Get Engine Tag
|
||||
run: |
|
||||
cd RobustToolbox
|
||||
git fetch --depth=1
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Build Packaging
|
||||
run: dotnet build Content.Packaging --configuration Release --no-restore /m
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Package server
|
||||
run: dotnet run --project Content.Packaging server --platform win-x64 --platform win-arm64 --platform linux-x64 --platform linux-arm64 --platform osx-x64 --platform osx-arm64
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Package client
|
||||
run: dotnet run --project Content.Packaging client --no-wipe-release
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Publish version
|
||||
run: Tools/publish_multi_request.py
|
||||
env:
|
||||
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
|
||||
FORK_ID: ${{ vars.FORK_ID }}
|
||||
GITHUB_REPOSITORY: wylab/wylab-station-14
|
||||
FORK_ID: wylab
|
||||
ROBUST_CDN_URL: https://cdn.wylab.me/
|
||||
if: ${{ steps.cdn-check.outputs.skip != 'true' }}
|
||||
|
||||
- name: Trigger Docker image rebuild
|
||||
if: ${{ success() && steps.cdn-check.outputs.skip != 'true' }}
|
||||
env:
|
||||
DISPATCH_TOKEN: ${{ secrets.DOCKER_TRIGGER_TOKEN }}
|
||||
TARGET_REPO: wylab/WS14-Docker-Linux-Server
|
||||
PAYLOAD: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "${DISPATCH_TOKEN}" ]; then
|
||||
echo "No DOCKER_TRIGGER_TOKEN configured; skipping dispatch."
|
||||
exit 0
|
||||
fi
|
||||
curl -sSL -X POST \
|
||||
-H "Authorization: token ${DISPATCH_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://git.wylab.me/api/v1/repos/${TARGET_REPO}/actions/workflows/main.yml/dispatches \
|
||||
-d "{\"ref\":\"main\",\"inputs\":{\"commit\":\"${PAYLOAD}\"}}"
|
||||
|
||||
# - name: Publish changelog (Discord)
|
||||
# continue-on-error: true
|
||||
|
||||
69
.github/workflows/rsi-diff.yml
vendored
69
.github/workflows/rsi-diff.yml
vendored
@@ -1,69 +0,0 @@
|
||||
name: Diff RSIs
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- '**.rsi/**.png'
|
||||
|
||||
jobs:
|
||||
diff:
|
||||
name: Diff
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Get changed files
|
||||
id: files
|
||||
uses: Ana06/get-changed-files@v2.3.0
|
||||
with:
|
||||
format: 'space-delimited'
|
||||
filter: |
|
||||
**.rsi
|
||||
**.png
|
||||
|
||||
- name: Diff changed RSIs
|
||||
id: diff
|
||||
uses: space-wizards/RSIDiffBot@v1.1
|
||||
with:
|
||||
modified: ${{ steps.files.outputs.modified }}
|
||||
removed: ${{ steps.files.outputs.removed }}
|
||||
added: ${{ steps.files.outputs.added }}
|
||||
basename: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
basesha: ${{ github.event.pull_request.base.sha }}
|
||||
headname: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
headsha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Potentially find comment
|
||||
uses: peter-evans/find-comment@v1
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: RSI Diff Bot
|
||||
|
||||
- name: Create comment if it doesn't exist
|
||||
if: steps.fc.outputs.comment-id == ''
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
body: |
|
||||
${{ steps.diff.outputs.summary-details }}
|
||||
|
||||
- name: Update comment if it exists
|
||||
if: steps.fc.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
${{ steps.diff.outputs.summary-details }}
|
||||
|
||||
- name: Update comment to read that it has been edited
|
||||
if: steps.fc.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@v1
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
edit-mode: append
|
||||
body: |
|
||||
Edit: diff updated after ${{ github.event.pull_request.head.sha }}
|
||||
12
.github/workflows/test-packaging.yml
vendored
12
.github/workflows/test-packaging.yml
vendored
@@ -31,6 +31,8 @@ jobs:
|
||||
name: Test Packaging
|
||||
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
@@ -48,19 +50,19 @@ jobs:
|
||||
cd RobustToolbox/
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Corvax-Secrets-Start
|
||||
# Wylab-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
if: ${{ env.SSH_KEY != '' }}
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo "StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
echo "HOST git.wylab.me" > ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Corvax-Secrets-End
|
||||
# Wylab-Secrets-End
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4.1.0
|
||||
|
||||
28
.github/workflows/update-wiki.yml
vendored
28
.github/workflows/update-wiki.yml
vendored
@@ -3,7 +3,7 @@ name: Update Wiki
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ master, jsondump ]
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '.github/workflows/update-wiki.yml'
|
||||
- 'Content.Shared/Chemistry/**.cs'
|
||||
@@ -19,6 +19,8 @@ jobs:
|
||||
update-wiki:
|
||||
name: Build and Publish JSON blobs to wiki
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
|
||||
steps:
|
||||
- name: Checkout Master
|
||||
@@ -51,42 +53,42 @@ jobs:
|
||||
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload chem_prototypes.json to wiki
|
||||
- name: Upload chem_prototypes to wiki
|
||||
uses: jtmullen/mediawiki-edit-action@v0.1.1
|
||||
with:
|
||||
wiki_text_file: ./bin/Content.Server/data/chem_prototypes.json
|
||||
edit_summary: Update chem_prototypes.json via GitHub Actions
|
||||
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json"
|
||||
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
|
||||
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/chem_prototypes.json
|
||||
api_url: https://wiki.wylab.me/api.php
|
||||
username: ${{ secrets.WIKI_BOT_USER }}
|
||||
password: ${{ secrets.WIKI_BOT_PASS }}
|
||||
|
||||
- name: Upload react_prototypes.json to wiki
|
||||
- name: Upload react_prototypes to wiki
|
||||
uses: jtmullen/mediawiki-edit-action@v0.1.1
|
||||
with:
|
||||
wiki_text_file: ./bin/Content.Server/data/react_prototypes.json
|
||||
edit_summary: Update react_prototypes.json via GitHub Actions
|
||||
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json"
|
||||
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
|
||||
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/react_prototypes.json
|
||||
api_url: https://wiki.wylab.me/api.php
|
||||
username: ${{ secrets.WIKI_BOT_USER }}
|
||||
password: ${{ secrets.WIKI_BOT_PASS }}
|
||||
|
||||
- name: Upload entity_prototypes.json to wiki
|
||||
- name: Upload entity_prototypes to wiki
|
||||
uses: jtmullen/mediawiki-edit-action@v0.1.1
|
||||
with:
|
||||
wiki_text_file: ./bin/Content.Server/data/entity_prototypes.json
|
||||
edit_summary: Update entity_prototypes.json via GitHub Actions
|
||||
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json"
|
||||
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
|
||||
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/entity_prototypes.json
|
||||
api_url: https://wiki.wylab.me/api.php
|
||||
username: ${{ secrets.WIKI_BOT_USER }}
|
||||
password: ${{ secrets.WIKI_BOT_PASS }}
|
||||
|
||||
- name: Upload mealrecipes_prototypes.json to wiki
|
||||
- name: Upload mealrecipes_prototypes to wiki
|
||||
uses: jtmullen/mediawiki-edit-action@v0.1.1
|
||||
with:
|
||||
wiki_text_file: ./bin/Content.Server/data/mealrecipes_prototypes.json
|
||||
edit_summary: Update mealrecipes_prototypes.json via GitHub Actions
|
||||
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json"
|
||||
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
|
||||
page_name: ${{ secrets.WIKI_PAGE_ROOT }}/mealrecipes_prototypes.json
|
||||
api_url: https://wiki.wylab.me/api.php
|
||||
username: ${{ secrets.WIKI_BOT_USER }}
|
||||
password: ${{ secrets.WIKI_BOT_PASS }}
|
||||
|
||||
58
.github/workflows/upstream-sync-merge.yml
vendored
Normal file
58
.github/workflows/upstream-sync-merge.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Upstream Sync Auto-Merge
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build & Test"]
|
||||
types: [completed]
|
||||
branches: [upstream-sync]
|
||||
|
||||
jobs:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Find and merge upstream-sync PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// Find open PR from upstream-sync branch
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:upstream-sync`,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (prs.length === 0) {
|
||||
console.log('No open upstream-sync PR found');
|
||||
return;
|
||||
}
|
||||
|
||||
const pr = prs[0];
|
||||
console.log(`Found PR #${pr.number}: ${pr.title}`);
|
||||
|
||||
// Merge the PR using rebase
|
||||
try {
|
||||
await github.rest.pulls.merge({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
merge_method: 'rebase'
|
||||
});
|
||||
console.log(`Successfully merged PR #${pr.number}`);
|
||||
} catch (error) {
|
||||
console.log(`Failed to merge: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Delete the upstream-sync branch after merge
|
||||
try {
|
||||
await github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: 'heads/upstream-sync'
|
||||
});
|
||||
console.log('Deleted upstream-sync branch');
|
||||
} catch (error) {
|
||||
console.log(`Failed to delete branch: ${error.message}`);
|
||||
}
|
||||
99
.github/workflows/upstream-sync.yml
vendored
Normal file
99
.github/workflows/upstream-sync.yml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
name: Upstream Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Daily at 6am UTC
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
jobs:
|
||||
check-and-sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Add upstream remote
|
||||
run: |
|
||||
git remote add syndicate https://github.com/space-syndicate/space-station-14.git
|
||||
git fetch syndicate master
|
||||
|
||||
- name: Check for upstream updates
|
||||
id: check
|
||||
run: |
|
||||
BEHIND=$(git rev-list HEAD..syndicate/master --count)
|
||||
echo "behind=$BEHIND" >> $GITHUB_OUTPUT
|
||||
if [ "$BEHIND" -gt 0 ]; then
|
||||
echo "Upstream has $BEHIND new commits"
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Already up to date"
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Rebase onto upstream
|
||||
if: steps.check.outputs.has_updates == 'true'
|
||||
id: rebase
|
||||
run: |
|
||||
# Create sync branch
|
||||
git checkout -b upstream-sync
|
||||
|
||||
# Try rebase
|
||||
if git rebase syndicate/master; then
|
||||
echo "rebase_success=true" >> $GITHUB_OUTPUT
|
||||
git push -f origin upstream-sync
|
||||
else
|
||||
git rebase --abort
|
||||
echo "rebase_success=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create PR for CI verification
|
||||
if: steps.check.outputs.has_updates == 'true' && steps.rebase.outputs.rebase_success == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const behind = '${{ steps.check.outputs.behind }}';
|
||||
|
||||
// Check if PR already exists
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:upstream-sync`,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (prs.length > 0) {
|
||||
console.log('PR already exists, skipping creation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create PR
|
||||
await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `Upstream sync: ${behind} new commits from syndicate/master`,
|
||||
head: 'upstream-sync',
|
||||
base: 'master',
|
||||
body: `Automatic rebase of wylab commits onto updated syndicate/master.\n\n**${behind} new upstream commits**\n\nThis PR will be auto-merged when CI passes.`
|
||||
});
|
||||
|
||||
- name: Create issue on rebase conflict
|
||||
if: steps.check.outputs.has_updates == 'true' && steps.rebase.outputs.rebase_success == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const behind = '${{ steps.check.outputs.behind }}';
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `Upstream sync failed - ${behind} commits behind`,
|
||||
body: `Automatic rebase onto syndicate/master failed due to conflicts.\n\nManual intervention required:\n\`\`\`bash\ngit fetch syndicate\ngit rebase syndicate/master\n# resolve conflicts\ngit push --force-with-lease origin master\n\`\`\``,
|
||||
labels: ['upstream-sync', 'needs-attention']
|
||||
});
|
||||
|
||||
10
.github/workflows/validate-rgas.yml
vendored
10
.github/workflows/validate-rgas.yml
vendored
@@ -15,19 +15,19 @@ jobs:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Setup Submodule
|
||||
run: git submodule update --init
|
||||
# Corvax-Secrets-Start
|
||||
# Wylab-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
if: ${{ env.SSH_KEY != '' }}
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo "StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
echo "HOST git.wylab.me" > ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Corvax-Secrets-End
|
||||
# Wylab-Secrets-End
|
||||
- name: Pull engine updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
- uses: PaulRitter/yaml-schema-validator@v1
|
||||
|
||||
40
.github/workflows/validate-rsis.yml
vendored
40
.github/workflows/validate-rsis.yml
vendored
@@ -5,35 +5,57 @@ on:
|
||||
branches: [ master, staging, stable ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.rsi/**'
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
branches: [ master, staging, stable ]
|
||||
|
||||
jobs:
|
||||
validate_rsis:
|
||||
name: Validate RSIs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for RSI changes
|
||||
id: check_rsi
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
rsi:
|
||||
- '**.rsi/**'
|
||||
|
||||
- name: Skip if no RSI changes
|
||||
if: steps.check_rsi.outputs.rsi != 'true' && github.event_name == 'pull_request'
|
||||
run: echo "No RSI files changed, skipping validation"
|
||||
|
||||
- uses: actions/checkout@v4.2.2
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
|
||||
- name: Setup Submodule
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
run: git submodule update --init
|
||||
# Corvax-Secrets-Start
|
||||
|
||||
# Wylab-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
if: ${{ env.SSH_KEY != '' }}
|
||||
if: (steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request') && env.SSH_KEY != ''
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo "StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
echo "HOST git.wylab.me" > ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Corvax-Secrets-End
|
||||
# Wylab-Secrets-End
|
||||
|
||||
- name: Pull engine updates
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
|
||||
- name: Install Python dependencies
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
run: |
|
||||
pip3 install --ignore-installed --user pillow jsonschema
|
||||
python3 -m pip install --user --break-system-packages pillow jsonschema
|
||||
|
||||
- name: Validate RSIs
|
||||
if: steps.check_rsi.outputs.rsi == 'true' || github.event_name != 'pull_request'
|
||||
run: |
|
||||
python3 RobustToolbox/Schemas/validate_rsis.py Resources/
|
||||
|
||||
10
.github/workflows/validate_mapfiles.yml
vendored
10
.github/workflows/validate_mapfiles.yml
vendored
@@ -15,19 +15,19 @@ jobs:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Setup Submodule
|
||||
run: git submodule update --init
|
||||
# Corvax-Secrets-Start
|
||||
# Wylab-Secrets-Start
|
||||
- name: Setup secrets
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.SECRETS_PRIVATE_KEY }}
|
||||
if: ${{ env.SSH_KEY != '' }}
|
||||
run: |
|
||||
mkdir ~/.ssh
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SECRETS_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
echo "HOST *" > ~/.ssh/config
|
||||
echo "StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
echo "HOST git.wylab.me" > ~/.ssh/config
|
||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||
git -c submodule.Secrets.update=checkout submodule update --init
|
||||
# Corvax-Secrets-End
|
||||
# Wylab-Secrets-End
|
||||
- name: Pull engine updates
|
||||
uses: space-wizards/submodule-dependency@v0.1.5
|
||||
- uses: PaulRitter/yaml-schema-validator@v1
|
||||
|
||||
7
.github/workflows/yaml-linter.yml
vendored
7
.github/workflows/yaml-linter.yml
vendored
@@ -12,8 +12,15 @@ jobs:
|
||||
name: YAML Linter
|
||||
if: github.actor != 'IanComradeBot' && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUNNER_TOOL_CACHE: /toolcache
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- name: Delete Wylab override files (duplicates upstream for customization)
|
||||
run: |
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/events.yml
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/pests.yml
|
||||
rm -f Resources/Prototypes/_Wylab/GameRules/subgamemodes.yml
|
||||
- name: Setup submodule
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.Communications;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Communications;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class CommunicationsConsoleComponent : SharedCommunicationsConsoleComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The prototype ID to use in the UI to show what entities a broadcast will display on
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId ScreenDisplayId = "Screen";
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Communications;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Communications.UI
|
||||
{
|
||||
@@ -23,37 +22,31 @@ namespace Content.Client.Communications.UI
|
||||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<CommunicationsConsoleMenu>();
|
||||
_menu.OnAnnounce += AnnounceButtonPressed;
|
||||
_menu.OnBroadcast += BroadcastButtonPressed;
|
||||
_menu.OnAlertLevel += AlertLevelSelected;
|
||||
_menu.OnEmergencyLevel += EmergencyShuttleButtonPressed;
|
||||
_menu.OnRadioAnnounce += RadioAnnounceButtonPressed;
|
||||
_menu.OnScreenBroadcast += ScreenBroadcastButtonPressed;
|
||||
_menu.OnAlertLevelChanged += AlertLevelSelected;
|
||||
_menu.OnShuttleCalled += CallShuttle;
|
||||
_menu.OnShuttleRecalled += RecallShuttle;
|
||||
|
||||
if (EntMan.TryGetComponent<CommunicationsConsoleComponent>(Owner, out var console))
|
||||
{
|
||||
_menu.SetBroadcastDisplayEntity(console.ScreenDisplayId);
|
||||
}
|
||||
}
|
||||
|
||||
public void AlertLevelSelected(string level)
|
||||
{
|
||||
if (_menu!.AlertLevelSelectable)
|
||||
{
|
||||
_menu.CurrentLevel = level;
|
||||
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
|
||||
}
|
||||
SendMessage(new CommunicationsConsoleSelectAlertLevelMessage(level));
|
||||
}
|
||||
|
||||
public void EmergencyShuttleButtonPressed()
|
||||
{
|
||||
if (_menu!.CountdownStarted)
|
||||
RecallShuttle();
|
||||
else
|
||||
CallShuttle();
|
||||
}
|
||||
|
||||
public void AnnounceButtonPressed(string message)
|
||||
public void RadioAnnounceButtonPressed(string message)
|
||||
{
|
||||
var maxLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
var msg = SharedChatSystem.SanitizeAnnouncement(message, maxLength);
|
||||
SendMessage(new CommunicationsConsoleAnnounceMessage(msg));
|
||||
}
|
||||
|
||||
public void BroadcastButtonPressed(string message)
|
||||
public void ScreenBroadcastButtonPressed(string message)
|
||||
{
|
||||
SendMessage(new CommunicationsConsoleBroadcastMessage(message));
|
||||
}
|
||||
@@ -77,20 +70,7 @@ namespace Content.Client.Communications.UI
|
||||
|
||||
if (_menu != null)
|
||||
{
|
||||
_menu.CanAnnounce = commsState.CanAnnounce;
|
||||
_menu.CanBroadcast = commsState.CanBroadcast;
|
||||
_menu.CanCall = commsState.CanCall;
|
||||
_menu.CountdownStarted = commsState.CountdownStarted;
|
||||
_menu.AlertLevelSelectable = commsState.AlertLevels != null && !float.IsNaN(commsState.CurrentAlertDelay) && commsState.CurrentAlertDelay <= 0;
|
||||
_menu.CurrentLevel = commsState.CurrentAlert;
|
||||
_menu.CountdownEnd = commsState.ExpectedCountdownEnd;
|
||||
|
||||
_menu.UpdateCountdown();
|
||||
_menu.UpdateAlertLevels(commsState.AlertLevels, _menu.CurrentLevel);
|
||||
_menu.AlertLevelButton.Disabled = !_menu.AlertLevelSelectable;
|
||||
_menu.EmergencyShuttleButton.Disabled = !_menu.CanCall;
|
||||
_menu.AnnounceButton.Disabled = !_menu.CanAnnounce;
|
||||
_menu.BroadcastButton.Disabled = !_menu.CanBroadcast;
|
||||
_menu.UpdateState(commsState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,32 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
<comms:CommunicationsConsoleMenu xmlns="https://spacestation14.io"
|
||||
xmlns:comms="clr-namespace:Content.Client.Communications.UI"
|
||||
xmlns:widgets="clr-namespace:Content.Client.Communications.UI.Widgets"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'comms-console-menu-title'}"
|
||||
MinSize="400 300">
|
||||
MouseFilter="Stop" MinSize="400 660" SetWidth="450">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<controls:LayeredImageContainer StyleClasses="BrightAngleRectOutline" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" Margin="12 8 12 12" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'comms-console-menu-title'}"
|
||||
StyleClasses="FancyWindowTitle" HorizontalExpand="True"/>
|
||||
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton"
|
||||
Modulate="#646464" VerticalAlignment="Center" Margin="0 0 0 6"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<PanelContainer StyleClasses="PanelDark" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<widgets:MessagingControls Name="MessagingControls" VerticalExpand="True" />
|
||||
|
||||
<!-- Main Container -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="False"
|
||||
VerticalExpand="True"
|
||||
Margin="6 6 6 5">
|
||||
|
||||
<TextEdit Name="MessageInput"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="100"/>
|
||||
|
||||
<!-- ButtonsPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalAlignment="Bottom"
|
||||
SeparationOverride="4">
|
||||
|
||||
<!-- AnnouncePart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="0 2">
|
||||
|
||||
<Button Name="AnnounceButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'comms-console-menu-announcement-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}"
|
||||
StyleClasses="OpenLeft"
|
||||
Margin="0 0 1 0"
|
||||
Disabled="True"/>
|
||||
|
||||
<Button Name="BroadcastButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'comms-console-menu-broadcast-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}"
|
||||
StyleClasses="OpenBoth"/>
|
||||
|
||||
<OptionButton Name="AlertLevelButton"
|
||||
Access="Public"
|
||||
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
|
||||
StyleClasses="OpenRight"/>
|
||||
<controls:HSpacer Spacing="20" />
|
||||
|
||||
<widgets:AlertLevelControls Name="AlertLevelControls" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:LayeredImageContainer>
|
||||
|
||||
<!-- EmergencyPart -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="6">
|
||||
<widgets:ShuttleControls Name="ShuttleControls" />
|
||||
|
||||
<RichTextLabel Name="CountdownLabel"/>
|
||||
|
||||
<Button Name="EmergencyShuttleButton"
|
||||
Access="Public"
|
||||
Text="Placeholder Text"
|
||||
ToolTip="{Loc 'comms-console-menu-emergency-shuttle-button-tooltip'}"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
</comms:CommunicationsConsoleMenu>
|
||||
|
||||
@@ -1,137 +1,83 @@
|
||||
using System.Globalization;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.CCVar;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Communications;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Communications.UI
|
||||
namespace Content.Client.Communications.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CommunicationsConsoleMenu : BaseWindow
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CommunicationsConsoleMenu : FancyWindow
|
||||
private const int DragMoveSize = 40;
|
||||
private const int DragResizeSize = 7;
|
||||
|
||||
public event Action? OnShuttleCalled;
|
||||
public event Action? OnShuttleRecalled;
|
||||
public event Action<string>? OnAlertLevelChanged;
|
||||
public event Action<string>? OnRadioAnnounce;
|
||||
public event Action<string>? OnScreenBroadcast;
|
||||
|
||||
public CommunicationsConsoleMenu()
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
public bool CanAnnounce;
|
||||
public bool CanBroadcast;
|
||||
public bool CanCall;
|
||||
public bool AlertLevelSelectable;
|
||||
public bool CountdownStarted;
|
||||
public string CurrentLevel = string.Empty;
|
||||
public TimeSpan? CountdownEnd;
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
|
||||
public event Action? OnEmergencyLevel;
|
||||
public event Action<string>? OnAlertLevel;
|
||||
public event Action<string>? OnAnnounce;
|
||||
public event Action<string>? OnBroadcast;
|
||||
MessagingControls.OnRadioAnnounce += message => OnRadioAnnounce?.Invoke(message);
|
||||
MessagingControls.OnScreenBroadcast += message => OnScreenBroadcast?.Invoke(message);
|
||||
|
||||
public CommunicationsConsoleMenu()
|
||||
AlertLevelControls.OnAlertLevelChanged += newLevel => OnAlertLevelChanged?.Invoke(newLevel);
|
||||
|
||||
ShuttleControls.OnShuttleCalled += () => OnShuttleCalled?.Invoke();
|
||||
ShuttleControls.OnShuttleRecalled += () => OnShuttleRecalled?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the specified prototype ID as an example display in the
|
||||
/// UI subsection where users type broadcast messages
|
||||
/// </summary>
|
||||
public void SetBroadcastDisplayEntity(EntProtoId broadcastEntityId)
|
||||
{
|
||||
MessagingControls.SetBroadcastDisplayEntity(broadcastEntityId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure UI to be consistent with the input state
|
||||
/// </summary>
|
||||
public void UpdateState(CommunicationsConsoleInterfaceState commsState)
|
||||
{
|
||||
MessagingControls.CanRadioAnnounce = commsState.CanAnnounce;
|
||||
MessagingControls.CanScreenBroadcast = commsState.CanBroadcast;
|
||||
|
||||
var alertLevelSelectable = commsState.AlertLevels != null && commsState.CurrentAlertDelay <= 0;
|
||||
AlertLevelControls.UpdateAlertLevels(commsState.AlertLevels, commsState.CurrentAlert, commsState.CurrentAlertColor, alertLevelSelectable);
|
||||
|
||||
ShuttleControls.UpdateState(commsState.CanCall, commsState.CountdownStarted, commsState.ExpectedCountdownEnd);
|
||||
}
|
||||
|
||||
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
|
||||
{
|
||||
if (relativeMousePos.Y < DragMoveSize)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
MessageInput.Placeholder = new Rope.Leaf(_loc.GetString("comms-console-menu-announcement-placeholder"));
|
||||
|
||||
var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
MessageInput.OnTextChanged += (args) =>
|
||||
{
|
||||
if (args.Control.TextLength > maxAnnounceLength)
|
||||
{
|
||||
AnnounceButton.Disabled = true;
|
||||
AnnounceButton.ToolTip = Loc.GetString("comms-console-message-too-long");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnnounceButton.Disabled = !CanAnnounce;
|
||||
AnnounceButton.ToolTip = null;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
AnnounceButton.OnPressed += _ => OnAnnounce?.Invoke(Rope.Collapse(MessageInput.TextRope));
|
||||
AnnounceButton.Disabled = !CanAnnounce;
|
||||
|
||||
BroadcastButton.OnPressed += _ => OnBroadcast?.Invoke(Rope.Collapse(MessageInput.TextRope));
|
||||
BroadcastButton.Disabled = !CanBroadcast;
|
||||
|
||||
AlertLevelButton.OnItemSelected += args =>
|
||||
{
|
||||
var metadata = AlertLevelButton.GetItemMetadata(args.Id);
|
||||
if (metadata != null && metadata is string cast)
|
||||
{
|
||||
OnAlertLevel?.Invoke(cast);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
AlertLevelButton.Disabled = !AlertLevelSelectable;
|
||||
|
||||
EmergencyShuttleButton.OnPressed += _ => OnEmergencyLevel?.Invoke();
|
||||
EmergencyShuttleButton.Disabled = !CanCall;
|
||||
return DragMode.Move;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
else
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateCountdown();
|
||||
}
|
||||
var mode = DragMode.None;
|
||||
|
||||
// The current alert could make levels unselectable, so we need to ensure that the UI reacts properly.
|
||||
// If the current alert is unselectable, the only item in the alerts list will be
|
||||
// the current alert. Otherwise, it will be the list of alerts, with the current alert
|
||||
// selected.
|
||||
public void UpdateAlertLevels(List<string>? alerts, string currentAlert)
|
||||
{
|
||||
AlertLevelButton.Clear();
|
||||
|
||||
if (alerts == null)
|
||||
if (relativeMousePos.Y > Size.Y - DragResizeSize)
|
||||
{
|
||||
var name = currentAlert;
|
||||
if (_loc.TryGetString($"alert-level-{currentAlert}", out var locName))
|
||||
{
|
||||
name = locName;
|
||||
}
|
||||
AlertLevelButton.AddItem(name);
|
||||
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, currentAlert);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var alert in alerts)
|
||||
{
|
||||
var name = alert;
|
||||
if (_loc.TryGetString($"alert-level-{alert}", out var locName))
|
||||
{
|
||||
name = locName;
|
||||
}
|
||||
AlertLevelButton.AddItem(name);
|
||||
AlertLevelButton.SetItemMetadata(AlertLevelButton.ItemCount - 1, alert);
|
||||
if (alert == currentAlert)
|
||||
{
|
||||
AlertLevelButton.Select(AlertLevelButton.ItemCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateCountdown()
|
||||
{
|
||||
if (!CountdownStarted)
|
||||
{
|
||||
CountdownLabel.SetMessage(string.Empty);
|
||||
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-call-shuttle");
|
||||
return;
|
||||
mode |= DragMode.Bottom;
|
||||
}
|
||||
|
||||
var diff = MathHelper.Max((CountdownEnd - _timing.CurTime) ?? TimeSpan.Zero, TimeSpan.Zero);
|
||||
|
||||
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-recall-shuttle");
|
||||
var infoText = Loc.GetString($"comms-console-menu-time-remaining",
|
||||
("time", diff.ToString(@"hh\:mm\:ss", CultureInfo.CurrentCulture)));
|
||||
CountdownLabel.SetMessage(infoText);
|
||||
if (relativeMousePos.X > Size.X - DragResizeSize)
|
||||
{
|
||||
mode |= DragMode.Right;
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.Stylesheets.Stylesheets;
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class CommunicationsConsoleSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, IButtonConfig, IIconConfig
|
||||
{
|
||||
public override StyleRule[] GetRules(T sheet, object config)
|
||||
{
|
||||
var lcdFontLarge = ResCache.GetFont("/Fonts/7SegmentDisplayDigits.ttf", 20);
|
||||
|
||||
return [
|
||||
E<Label>().Class("LabelLCDBig")
|
||||
.Prop("font-color", sheet.NegativePalette.Text)
|
||||
.Prop("font", lcdFontLarge),
|
||||
|
||||
/// Large red texture button
|
||||
E<TextureButton>().Identifier("TemptingRedButton")
|
||||
.Prop(TextureButton.StylePropertyTexture, sheet.GetTextureOr(sheet.RoundedButtonPath, NanotrasenStylesheet.TextureRoot))
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.Element),
|
||||
|
||||
E<TextureButton>().Identifier("TemptingRedButton")
|
||||
.Pseudo(ContainerButton.StylePseudoClassNormal)
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.Element),
|
||||
E<TextureButton>().Identifier("TemptingRedButton")
|
||||
.Pseudo(ContainerButton.StylePseudoClassDisabled)
|
||||
.Prop(TextureButton.StylePropertyTexture, ResCache.GetTexture("/Textures/Interface/Nano/rounded_locked_button.svg.96dpi.png"))
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.DisabledElement),
|
||||
E<TextureButton>().Identifier("TemptingRedButton").Pseudo(ContainerButton.StylePseudoClassHover)
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.NegativePalette.HoveredElement),
|
||||
|
||||
E<TextureRect>().Identifier("ScrewHead")
|
||||
.Prop(TextureRect.StylePropertyTexture, ResCache.GetTexture("/Textures/Interface/Diegetic/screw.svg.96dpi.png")),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical">
|
||||
<!-- Info about the current alert level -->
|
||||
<BoxContainer HorizontalAlignment="Center">
|
||||
<Label StyleClasses="StatusFieldTitle" Margin="0 0 9 0"
|
||||
Text="{Loc 'comms-console-alert-current-level-header'}" />
|
||||
<Label Name="CurrentAlertLevelLabel" Margin="9 0 0 0"/>
|
||||
</BoxContainer>
|
||||
|
||||
<Label Name="CurrentAlertLevelFlavorLabel" Align="Center" />
|
||||
|
||||
<!-- Controls for changing the alert level -->
|
||||
<BoxContainer Margin="9 12 9 6">
|
||||
<OptionButton Name="AlertLevelSelector" StyleClasses="OpenRight"
|
||||
ToolTip="{Loc 'comms-console-menu-alert-level-button-tooltip'}"
|
||||
HorizontalExpand="True" SizeFlagsStretchRatio="4" />
|
||||
<Button Name="ConfirmAlertLevelButton" StyleClasses="OpenLeft"
|
||||
HorizontalExpand="True" SizeFlagsStretchRatio="1"
|
||||
Text="{Loc 'comms-console-confirm-alert-level-button'}"
|
||||
Disabled="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
@@ -0,0 +1,137 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Communications.UI.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AlertLevelControls : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
private bool _alertLevelSelectable;
|
||||
private string _currentAlertLevel = string.Empty;
|
||||
|
||||
public event Action<string>? OnAlertLevelChanged;
|
||||
|
||||
public AlertLevelControls()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AlertLevelSelector.OnItemSelected += args =>
|
||||
{
|
||||
AlertLevelSelector.Select(args.Id);
|
||||
EnableDisableConfirmLevelChangeButton();
|
||||
};
|
||||
|
||||
ConfirmAlertLevelButton.OnPressed += _ =>
|
||||
{
|
||||
var metadata = AlertLevelSelector.GetItemMetadata(AlertLevelSelector.SelectedId);
|
||||
if (metadata is string cast)
|
||||
{
|
||||
OnAlertLevelChanged?.Invoke(cast);
|
||||
}
|
||||
};
|
||||
|
||||
AlertLevelSelector.Disabled = !_alertLevelSelectable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the UI components to display the current alert level and the
|
||||
/// selectable alert levels
|
||||
/// </summary>
|
||||
public void UpdateAlertLevels(List<string>? alerts,
|
||||
string currentAlert,
|
||||
Color currentAlertColor,
|
||||
bool alertLevelSelectable)
|
||||
{
|
||||
_alertLevelSelectable = alertLevelSelectable;
|
||||
_currentAlertLevel = currentAlert;
|
||||
CurrentAlertLevelLabel.Text = GetLocalizedAlertName(currentAlert);
|
||||
CurrentAlertLevelLabel.ModulateSelfOverride = currentAlertColor;
|
||||
|
||||
AlertLevelSelector.Disabled = alerts == null || !_alertLevelSelectable;
|
||||
|
||||
// If user had changed the selection, but hadn't pressed the confirm
|
||||
// button at the point we received an update message, we want to
|
||||
// remember what they had selected, so we can attempt to re-select it
|
||||
// if it's still an option in the new set of alert level possibilities.
|
||||
string? previousSelection = null;
|
||||
if (AlertLevelSelector.ItemCount > 1)
|
||||
{
|
||||
var metadata = AlertLevelSelector.GetItemMetadata(AlertLevelSelector.SelectedId);
|
||||
if (metadata is string cast)
|
||||
{
|
||||
previousSelection = cast;
|
||||
}
|
||||
}
|
||||
|
||||
// The server will only send alert levels which are selectable, but
|
||||
// unfortunately, for a short time after changing the alert level, no
|
||||
// level is selectable, so we won't have any levels to put into the
|
||||
// alert level list. Here, we make a dummy item to handle that; this
|
||||
// item also informs the user what this combo box is for.
|
||||
AlertLevelSelector.Clear();
|
||||
AlertLevelSelector.AddItem(Loc.GetString("comms-console-change-alert-level-button"));
|
||||
AlertLevelSelector.Select(0);
|
||||
|
||||
if (alerts != null)
|
||||
{
|
||||
foreach (var alert in alerts)
|
||||
{
|
||||
AlertLevelSelector.AddItem(GetLocalizedAlertName(alert));
|
||||
AlertLevelSelector.SetItemMetadata(AlertLevelSelector.ItemCount - 1, alert);
|
||||
|
||||
if (alert == previousSelection)
|
||||
{
|
||||
AlertLevelSelector.Select(AlertLevelSelector.ItemCount - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_loc.TryGetString($"comms-console-level-{currentAlert}-flavour-label", out var flavour))
|
||||
{
|
||||
CurrentAlertLevelFlavorLabel.Text = flavour;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentAlertLevelFlavorLabel.Text = string.Empty;
|
||||
}
|
||||
|
||||
EnableDisableConfirmLevelChangeButton();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the "confirm alert level changed" button to be consistent with
|
||||
/// the server state and user interactions
|
||||
/// </summary>
|
||||
private void EnableDisableConfirmLevelChangeButton()
|
||||
{
|
||||
// Disable the button when:
|
||||
// 1. Server says we can't change level
|
||||
// 2. User selected special info option (id 0)
|
||||
// 3. User selected the same level the station is currently on
|
||||
var selectedId = AlertLevelSelector.SelectedId;
|
||||
ConfirmAlertLevelButton.Disabled = !_alertLevelSelectable || selectedId == 0;
|
||||
if (selectedId != 0)
|
||||
{
|
||||
var metadata = AlertLevelSelector.GetItemMetadata(selectedId);
|
||||
if (metadata is string selected)
|
||||
{
|
||||
ConfirmAlertLevelButton.Disabled |= selected == _currentAlertLevel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Utility function to convert an alert level identifier into a localized string
|
||||
/// </summary>
|
||||
private string GetLocalizedAlertName(string alertName)
|
||||
{
|
||||
if (_loc.TryGetString($"alert-level-{alertName}", out var locName))
|
||||
{
|
||||
return locName;
|
||||
}
|
||||
return alertName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<TabContainer xmlns="https://spacestation14.io" >
|
||||
<!-- Controls for sending radio announcements -->
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True"
|
||||
TabContainer.TabTitle="{Loc 'comms-console-announce-tab-title'}">
|
||||
<Label StyleClasses="StatusFieldTitle" Align="Center"
|
||||
Text="{Loc 'comms-console-station-announcements-header'}" />
|
||||
<PanelContainer StyleClasses="highlight"
|
||||
Margin="6" VerticalExpand="True">
|
||||
<PanelContainer StyleClasses="BackgroundDark" Margin="2"
|
||||
HorizontalExpand="True" VerticalExpand="True">
|
||||
<TextEdit Name="RadioMessageInput"
|
||||
Margin="2 0 0 0" MinHeight="100"
|
||||
HorizontalExpand="True" VerticalExpand="True" />
|
||||
</PanelContainer>
|
||||
</PanelContainer>
|
||||
<Button Name="AnnounceButton" HorizontalExpand="True" Margin="6"
|
||||
Text="{Loc 'comms-console-menu-announcement-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-announcement-button-tooltip'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Controls for putting messages on station screens (aka broadcasting) -->
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True"
|
||||
TabContainer.TabTitle="{Loc 'comms-console-broadcast-tab-title'}">
|
||||
<Label StyleClasses="StatusFieldTitle" Align="Center" VAlign="Top"
|
||||
VerticalExpand="True" VerticalAlignment="Top"
|
||||
Text="{Loc 'comms-console-station-broadcast-header'}" />
|
||||
<SpriteView Name="BroadcastEntityDisplay" Scale="8 8" />
|
||||
<LineEdit Name="ScreenMessageInput" Margin="6 0 6 0"
|
||||
PlaceHolder="{Loc 'comms-console-menu-broadcast-placeholder'}" />
|
||||
<Button Name="BroadcastButton" HorizontalExpand="True" Margin="6"
|
||||
Text="{Loc 'comms-console-menu-broadcast-button'}"
|
||||
ToolTip="{Loc 'comms-console-menu-broadcast-button-tooltip'}" />
|
||||
</BoxContainer>
|
||||
</TabContainer>
|
||||
@@ -0,0 +1,117 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.TextScreen;
|
||||
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.Communications.UI.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class MessagingControls : TabContainer
|
||||
{
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
// Entity temporarily created to display a screen preview
|
||||
private EntityUid _broadcastDisplayEntity = EntityUid.Invalid;
|
||||
|
||||
public event Action<string>? OnRadioAnnounce;
|
||||
public event Action<string>? OnScreenBroadcast;
|
||||
|
||||
private bool _canRadioAnnounce;
|
||||
public bool CanRadioAnnounce
|
||||
{
|
||||
set
|
||||
{
|
||||
_canRadioAnnounce = value;
|
||||
SyncButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _canScreenBroadcast;
|
||||
public bool CanScreenBroadcast
|
||||
{
|
||||
set
|
||||
{
|
||||
_canScreenBroadcast = value;
|
||||
SyncButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
public MessagingControls()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
RadioMessageInput.Placeholder = new Rope.Leaf(_loc.GetString("comms-console-menu-announcement-placeholder"));
|
||||
|
||||
RadioMessageInput.OnTextChanged += (_) => SyncButtonState();
|
||||
|
||||
AnnounceButton.OnPressed += _ =>
|
||||
{
|
||||
OnRadioAnnounce?.Invoke(Rope.Collapse(RadioMessageInput.TextRope));
|
||||
};
|
||||
|
||||
var appearanceSystem = _entMan.System<SharedAppearanceSystem>();
|
||||
ScreenMessageInput.OnTextChanged += args =>
|
||||
{
|
||||
if (_broadcastDisplayEntity.IsValid())
|
||||
{
|
||||
appearanceSystem.SetData(_broadcastDisplayEntity, TextScreenVisuals.ScreenText, args.Text);
|
||||
}
|
||||
};
|
||||
|
||||
BroadcastButton.OnPressed += _ =>
|
||||
{
|
||||
OnScreenBroadcast?.Invoke(ScreenMessageInput.Text);
|
||||
};
|
||||
|
||||
SyncButtonState();
|
||||
}
|
||||
|
||||
public void SetBroadcastDisplayEntity(EntProtoId broadcastEntityId)
|
||||
{
|
||||
_broadcastDisplayEntity = _entMan.Spawn(broadcastEntityId);
|
||||
if (_broadcastDisplayEntity.IsValid())
|
||||
{
|
||||
BroadcastEntityDisplay.SetEntity(_broadcastDisplayEntity);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
if (_broadcastDisplayEntity.IsValid())
|
||||
{
|
||||
_entMan.DeleteEntity(_broadcastDisplayEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncButtonState()
|
||||
{
|
||||
if (_canRadioAnnounce)
|
||||
{
|
||||
var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
|
||||
if (RadioMessageInput.TextLength > maxAnnounceLength)
|
||||
{
|
||||
AnnounceButton.Disabled = true;
|
||||
AnnounceButton.ToolTip = _loc.GetString("comms-console-message-too-long");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnnounceButton.Disabled = false;
|
||||
AnnounceButton.ToolTip = _loc.GetString("comms-console-menu-announcement-button-tooltip");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AnnounceButton.Disabled = true;
|
||||
AnnounceButton.ToolTip = _loc.GetString("comms-console-message-cannot-send");
|
||||
}
|
||||
|
||||
BroadcastButton.Disabled = !_canScreenBroadcast;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
<controls:StripeBack
|
||||
HasTopEdge="False" HasBottomEdge="False" StyleClasses="status-warning" >
|
||||
|
||||
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="4"/>
|
||||
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="4"/>
|
||||
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="4"/>
|
||||
<TextureRect StyleIdentifier="ScrewHead" TextureScale="0.25 0.25"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="4"/>
|
||||
|
||||
<!-- Controls for calling, recalling and displaying info for the emergency shuttle -->
|
||||
<PanelContainer StyleClasses="PanelDark" Margin="20">
|
||||
<BoxContainer Orientation="Vertical" Margin="8">
|
||||
<Label StyleClasses="FancyWindowTitle" Align="Center"
|
||||
Text="{Loc 'comms-console-shuttle-controls-header'}" />
|
||||
|
||||
<!-- This contains some blank `Controls` to act as dummies. This is because the
|
||||
GridContainer will always left-align the children, while we want them centered.
|
||||
The dummy Controls will attempt to take an even amount of space each, which
|
||||
will re-center our real controls -->
|
||||
<GridContainer Columns="7" HorizontalExpand="True" Margin="0 8 0 0">
|
||||
<!-- Row 1: the buttons themselves -->
|
||||
<Control HorizontalExpand="True" />
|
||||
<controls:LayeredImageContainer>
|
||||
<Control.StyleClasses>
|
||||
<sys:String>PanelMount</sys:String>
|
||||
<sys:String>PanelDark</sys:String>
|
||||
</Control.StyleClasses>
|
||||
<TextureButton Name="EmergencyShuttleCallButton"
|
||||
StyleIdentifier="TemptingRedButton" MinSize="60 60"
|
||||
ToolTip="{Loc 'comms-console-menu-call-shuttle'}" />
|
||||
</controls:LayeredImageContainer>
|
||||
<Control HorizontalExpand="True" />
|
||||
<controls:LayeredImageContainer
|
||||
VerticalAlignment="Center">
|
||||
<Control.StyleClasses>
|
||||
<sys:String>PanelMount</sys:String>
|
||||
<sys:String>PanelDark</sys:String>
|
||||
</Control.StyleClasses>
|
||||
<Label Name="CountdownLabel" StyleClasses="LabelLCDBig"
|
||||
ReservesSpace="True" Margin="6"/>
|
||||
</controls:LayeredImageContainer>
|
||||
<Control HorizontalExpand="True" />
|
||||
<controls:LayeredImageContainer>
|
||||
<Control.StyleClasses>
|
||||
<sys:String>PanelMount</sys:String>
|
||||
<sys:String>PanelDark</sys:String>
|
||||
</Control.StyleClasses>
|
||||
<TextureButton Name="EmergencyShuttleRecallButton"
|
||||
StyleIdentifier="TemptingRedButton" MinSize="60 60"
|
||||
ToolTip="{Loc 'comms-console-menu-recall-shuttle'}" />
|
||||
</controls:LayeredImageContainer>
|
||||
<Control HorizontalExpand="True" />
|
||||
|
||||
<!-- Row 2: The labels for the buttons -->
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Text="{Loc 'comms-console-call-button-label'}" Align="Center" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Text="{Loc 'comms-console-shuttle-status-label'}" Align="Center" />
|
||||
<Control HorizontalExpand="True" />
|
||||
<Label Text="{Loc 'comms-console-recall-button-label'}" Align="Center" />
|
||||
<Control HorizontalExpand="True" />
|
||||
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</controls:StripeBack>
|
||||
</Control>
|
||||
@@ -0,0 +1,99 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Communications.UI.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ShuttleControls : Control
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private bool _canCall;
|
||||
private bool _countdownStarted;
|
||||
private TimeSpan? _countdownEnd;
|
||||
|
||||
public event Action? OnShuttleCalled;
|
||||
public event Action? OnShuttleRecalled;
|
||||
|
||||
public ShuttleControls()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
EmergencyShuttleCallButton.OnPressed += _ => OnShuttleCalled?.Invoke();
|
||||
EmergencyShuttleRecallButton.OnPressed += _ => OnShuttleRecalled?.Invoke();
|
||||
SyncButtonState();
|
||||
}
|
||||
|
||||
public void UpdateState(bool canCallShuttle, bool countdownStarted, TimeSpan? expectedCountdownEnd)
|
||||
{
|
||||
_canCall = canCallShuttle;
|
||||
_countdownStarted = countdownStarted;
|
||||
_countdownEnd = expectedCountdownEnd;
|
||||
SyncButtonState();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
UpdateCountdown();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Syncs and animates the display of the time-to-shuttle-arrival label
|
||||
/// </summary>
|
||||
private void UpdateCountdown()
|
||||
{
|
||||
// Set the label on the LCD countdown
|
||||
var countdown = _countdownEnd == null ? 0.0f : (float)Math.Max(_countdownEnd.Value.Subtract(_timing.CurTime).TotalSeconds, 0);
|
||||
|
||||
var remainingWholeSeconds = _countdownStarted ? (int)Math.Ceiling(countdown) : 0;
|
||||
var message = (remainingWholeSeconds / 60).ToString("D2") + ":" + (remainingWholeSeconds % 60).ToString("D2");
|
||||
CountdownLabel.Text = message;
|
||||
|
||||
CountdownLabel.Visible = _countdownStarted;
|
||||
if(countdown == 0)
|
||||
{
|
||||
CountdownLabel.ModulateSelfOverride = Color.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Blink the LCD
|
||||
var alpha = 1.0f;
|
||||
if (!_cfg.GetCVar(CCVars.ReducedMotion))
|
||||
{
|
||||
var subSecondsRemaining = countdown - (float)Math.Floor(countdown);
|
||||
var lightEnableBlend = SmoothStep(0.1f, 0.3f, subSecondsRemaining);
|
||||
var lightDisableBlend = SmoothStep(0.9f, 0.95f, subSecondsRemaining);
|
||||
alpha = lightEnableBlend - lightDisableBlend;
|
||||
}
|
||||
CountdownLabel.ModulateSelfOverride = new Color(1.0f, 1.0f, 1.0f, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private float SmoothStep(float stepBegin, float stepEnd, float x)
|
||||
{
|
||||
if (x < stepBegin)
|
||||
return 0;
|
||||
|
||||
if (x >= stepEnd)
|
||||
return 1;
|
||||
|
||||
var t = (x - stepBegin) / (stepEnd - stepBegin);
|
||||
return (3 * t * t) - (2 * t * t * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure the shuttle call/recall buttons to have a consistent state
|
||||
/// </summary>
|
||||
private void SyncButtonState()
|
||||
{
|
||||
EmergencyShuttleCallButton.Disabled = !(_canCall && !_countdownStarted);
|
||||
EmergencyShuttleRecallButton.Disabled = !(_canCall && _countdownStarted);
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,8 @@ public sealed class PdaSheetlet : Sheetlet<NanotrasenStylesheet>
|
||||
|
||||
E<PanelContainer>()
|
||||
.Class("PdaBorderRect")
|
||||
.Prop(PanelContainer.StylePropertyPanel, angleBorderRect),
|
||||
.Prop(PanelContainer.StylePropertyPanel, angleBorderRect)
|
||||
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#00000040")),
|
||||
|
||||
//PDA - Buttons
|
||||
E<PdaSettingsButton>()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.Stylesheets.Palette;
|
||||
using Content.Client.Stylesheets.Sheetlets;
|
||||
using Content.Client.Stylesheets.Stylesheets;
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class LayeredImageContainerSheetlet : Sheetlet<NanotrasenStylesheet>
|
||||
{
|
||||
public override StyleRule[] GetRules(NanotrasenStylesheet sheet, object config)
|
||||
{
|
||||
IPanelConfig panelCfg = sheet;
|
||||
var panelMountBaseTex = ResCache.GetTexture("/Textures/Interface/Diegetic/PanelMountBase.svg.96dpi.png");
|
||||
var panelMountHighlightTex = ResCache.GetTexture("/Textures/Interface/Diegetic/PanelMountHighlight.svg.96dpi.png");
|
||||
var panelMountBaseStyleBox = new StyleBoxTexture
|
||||
{
|
||||
Texture = panelMountBaseTex,
|
||||
PatchMarginLeft = 16,
|
||||
PatchMarginTop = 16,
|
||||
PatchMarginRight = 24,
|
||||
PatchMarginBottom = 24
|
||||
};
|
||||
var panelMountHighlightStyleBox = new StyleBoxTexture
|
||||
{
|
||||
Texture = panelMountHighlightTex,
|
||||
PatchMarginLeft = 16,
|
||||
PatchMarginTop = 16,
|
||||
PatchMarginRight = 24,
|
||||
PatchMarginBottom = 24
|
||||
};
|
||||
|
||||
var borderTex = sheet.GetTexture(panelCfg.GeometricPanelBorderPath).IntoPatch(StyleBox.Margin.All, 10);
|
||||
|
||||
return [
|
||||
// Adds a raised border with rounded corners around a UI element
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
|
||||
.Prop(LayeredImageContainer.StylePropertyMinimumContentMargin, new Thickness(10, 10, 16, 16)),
|
||||
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Foreground1"))
|
||||
.Prop(PanelContainer.StylePropertyPanel, panelMountBaseStyleBox),
|
||||
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassPanelMount)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Foreground2"))
|
||||
.Prop(PanelContainer.StylePropertyPanel, panelMountHighlightStyleBox),
|
||||
|
||||
E<LayeredImageContainer>().Class(StyleClass.PanelDark)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Foreground1"))
|
||||
.Prop(Control.StylePropertyModulateSelf, sheet.SecondaryPalette.BackgroundDark),
|
||||
|
||||
/// Bright AngleRect with a subtle outline
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassBrightAngleRect)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Background1"))
|
||||
.Prop(PanelContainer.StylePropertyPanel, StyleBoxHelpers.BaseStyleBox(sheet))
|
||||
.Prop(Control.StylePropertyModulateSelf, Palettes.Cyan.BackgroundLight),
|
||||
|
||||
E<LayeredImageContainer>().Class(LayeredImageContainer.StyleClassBrightAngleRect)
|
||||
.ParentOf(E<PanelContainer>().Identifier("Background2"))
|
||||
.Prop(PanelContainer.StylePropertyPanel, borderTex)
|
||||
.Prop(Control.StylePropertyModulateSelf, Color.FromHex("#00000040")),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Content.Client.Stylesheets.Palette;
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Content.Client.Stylesheets.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -14,16 +15,25 @@ public sealed class StripebackSheetlet<T> : Sheetlet<T> where T : PalettedStyles
|
||||
{
|
||||
IStripebackConfig stripebackCfg = sheet;
|
||||
|
||||
var stripeTex = sheet.GetTextureOr(stripebackCfg.StripebackPath, NanotrasenStylesheet.TextureRoot);
|
||||
var stripeBack = new StyleBoxTexture
|
||||
{
|
||||
Texture = sheet.GetTextureOr(stripebackCfg.StripebackPath, NanotrasenStylesheet.TextureRoot),
|
||||
Texture = stripeTex,
|
||||
Mode = StyleBoxTexture.StretchMode.Tile,
|
||||
Modulate = sheet.PrimaryPalette.BackgroundDark
|
||||
};
|
||||
var stripeBackWarning = new StyleBoxTexture {
|
||||
Texture = stripeTex,
|
||||
Mode = StyleBoxTexture.StretchMode.Tile,
|
||||
Modulate = Palettes.Amber.Element
|
||||
};
|
||||
|
||||
return
|
||||
[
|
||||
E<StripeBack>()
|
||||
.Prop(StripeBack.StylePropertyBackground, stripeBack),
|
||||
E<StripeBack>().Class(StyleClass.StatusWarning)
|
||||
.Prop(StripeBack.StylePropertyBackground, stripeBackWarning),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ public static class StyleClass
|
||||
public const string ButtonBig = "ButtonBig";
|
||||
|
||||
public const string CrossButtonRed = "CrossButtonRed";
|
||||
|
||||
public const string RefreshButton = "RefreshButton";
|
||||
|
||||
public const string ItemStatus = "ItemStatus";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<controls:LayeredImageContainer xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<PanelContainer StyleIdentifier="Background1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
<PanelContainer StyleIdentifier="Background2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
<Control Name="ContentsContainer"/>
|
||||
<PanelContainer StyleIdentifier="Foreground1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
<PanelContainer StyleIdentifier="Foreground2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
|
||||
</controls:LayeredImageContainer>
|
||||
@@ -0,0 +1,46 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A generic container which can layer multiple styleboxes over/under the
|
||||
/// contents of of the child controls. This container contains two forground
|
||||
/// panels and two background panels, each of which can be styled independently.
|
||||
/// Background panels will be rendered underneath the child controls, while
|
||||
/// foreground panels will be rendered above. The use of two panels for each
|
||||
/// level allows for one level to use a stylized graphic, typically including
|
||||
/// some modulation to color-match the surrounding UI, while the second level
|
||||
/// can be used for shadows or highlights, which would typically not have such
|
||||
/// modulation.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public partial class LayeredImageContainer : Container
|
||||
{
|
||||
// Adds a raised border with rounded corners around a UI element
|
||||
public const string StyleClassPanelMount = "PanelMount";
|
||||
|
||||
// Bright AngleRect with a subtle outline
|
||||
public const string StyleClassBrightAngleRect = "BrightAngleRectOutline";
|
||||
|
||||
// The least amount of margin that a child needs to have to avoid drawing under
|
||||
// undesirable parts of the images. Children can add additional margins if desired
|
||||
public const string StylePropertyMinimumContentMargin = "MinimumContentMargin";
|
||||
|
||||
public LayeredImageContainer()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
XamlChildren = ContentsContainer.Children;
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
if (TryGetStyleProperty<Thickness>(StylePropertyMinimumContentMargin, out var contentMargin))
|
||||
{
|
||||
ContentsContainer.Margin = contentMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Content.Client/_Wega/Ghost/GhostRespawnSystem.cs
Normal file
20
Content.Client/_Wega/Ghost/GhostRespawnSystem.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Content.Shared.Wega.Ghost.Respawn;
|
||||
|
||||
namespace Content.Client.Wega.Ghost.Respawn;
|
||||
|
||||
public sealed class GhostRespawnSystem : EntitySystem
|
||||
{
|
||||
public TimeSpan? GhostRespawnTime { get; private set; }
|
||||
public event Action? GhostRespawn;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeNetworkEvent<GhostRespawnEvent>(OnGhostRespawnReset);
|
||||
}
|
||||
|
||||
private void OnGhostRespawnReset(GhostRespawnEvent e)
|
||||
{
|
||||
GhostRespawnTime = e.Time;
|
||||
GhostRespawn?.Invoke();
|
||||
}
|
||||
}
|
||||
41
Content.Client/_Wega/Vampire/Ui/SelectClassMenu.xaml
Normal file
41
Content.Client/_Wega/Vampire/Ui/SelectClassMenu.xaml
Normal file
@@ -0,0 +1,41 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:local="clr-namespace:Content.Client.Select.Class.UI;assembly=Content.Client"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Content.Client.Select.Class.UI.SelectClassMenu"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Main Radial Menu Container -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<!-- Button 1: Hemomancer -->
|
||||
<ui:RadialMenuButton Name="HemomancerButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-hemomancer'}" TargetLayerControlName="Hemomancer">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/claws.png"/>
|
||||
</ui:RadialMenuButton>
|
||||
|
||||
<!-- Button 2: Umbrae -->
|
||||
<ui:RadialMenuButton Name="UmbraeButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-umbrae'}" TargetLayerControlName="Umbrae">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/cloak.png"/>
|
||||
</ui:RadialMenuButton>
|
||||
|
||||
<!-- Button 3: Gargantua -->
|
||||
<ui:RadialMenuButton Name="GargantuaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-gargantua'}" TargetLayerControlName="Gargantua">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/swell.png"/>
|
||||
</ui:RadialMenuButton>
|
||||
|
||||
<!-- Button 4: Dantalion -->
|
||||
<ui:RadialMenuButton Name="DantalionButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-dantalion'}" TargetLayerControlName="Dantalion">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/enthrall.png"/>
|
||||
</ui:RadialMenuButton>
|
||||
|
||||
<!-- Button 5: Bestia -->
|
||||
<!-- <ui:RadialMenuButton Name="BestiaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-bestia'}" TargetLayerControlName="Bestia">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/rush.png"/>
|
||||
</ui:RadialMenuButton> -->
|
||||
|
||||
</ui:RadialContainer>
|
||||
|
||||
</ui:RadialMenu>
|
||||
43
Content.Client/_Wega/Vampire/Ui/SelectClassMenu.xaml.cs
Normal file
43
Content.Client/_Wega/Vampire/Ui/SelectClassMenu.xaml.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Vampire;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Select.Class.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SelectClassMenu : RadialMenu
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityNetworkManager _entityNetworkManager = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||
|
||||
public event Action<string>? OnSelectClass;
|
||||
|
||||
public SelectClassMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
InitializeButtons();
|
||||
}
|
||||
|
||||
private void InitializeButtons()
|
||||
{
|
||||
HemomancerButton.OnButtonUp += _ => HandleClassSelection("Hemomancer");
|
||||
UmbraeButton.OnButtonUp += _ => HandleClassSelection("Umbrae");
|
||||
GargantuaButton.OnButtonUp += _ => HandleClassSelection("Gargantua");
|
||||
DantalionButton.OnButtonUp += _ => HandleClassSelection("Dantalion");
|
||||
//BestiaButton.OnButtonUp += _ => HandleClassSelection("Bestia");
|
||||
}
|
||||
|
||||
private void HandleClassSelection(string className)
|
||||
{
|
||||
OnSelectClass?.Invoke(className);
|
||||
var netEntity = _entityManager.GetNetEntity(_playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid);
|
||||
_entityNetworkManager.SendSystemNetworkMessage(new VampireSelectClassMenuClosedEvent(netEntity, className));
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
46
Content.Client/_Wega/Vampire/Ui/SelectClassUIController.cs
Normal file
46
Content.Client/_Wega/Vampire/Ui/SelectClassUIController.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Content.Client.Select.Class.UI;
|
||||
using Content.Shared.Vampire;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Select.Class
|
||||
{
|
||||
public sealed class SelectClassUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private SelectClassMenu? _menu;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<SelectClassPressedEvent>(OnSelectClassMenuReceived);
|
||||
}
|
||||
|
||||
private void OnSelectClassMenuReceived(SelectClassPressedEvent args, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
var session = IoCManager.Resolve<IPlayerManager>().LocalSession;
|
||||
var userEntity = _entityManager.GetEntity(args.Uid);
|
||||
if (session?.AttachedEntity.HasValue == true && session.AttachedEntity.Value == userEntity)
|
||||
{
|
||||
if (_menu is null)
|
||||
{
|
||||
_menu = _uiManager.CreateWindow<SelectClassMenu>();
|
||||
_menu.OnClose += OnMenuClosed;
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
else
|
||||
{
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMenuClosed()
|
||||
{
|
||||
_menu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Content.Client/_Wega/Vampire/VampireSystem.cs
Normal file
63
Content.Client/_Wega/Vampire/VampireSystem.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Content.Client.Alerts;
|
||||
using Content.Client.Movement.Systems;
|
||||
using Content.Shared.StatusIcon.Components;
|
||||
using Content.Shared.Vampire;
|
||||
using Content.Shared.Vampire.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Vampire;
|
||||
|
||||
public sealed class VampireSystem : SharedVampireSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly ContentEyeSystem _contentEye = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<VampireToggleFovEvent>(OnToggleFoV);
|
||||
SubscribeLocalEvent<VampireComponent, GetStatusIconsEvent>(GetVampireIcons);
|
||||
SubscribeLocalEvent<ThrallComponent, GetStatusIconsEvent>(GetThrallIcons);
|
||||
SubscribeLocalEvent<VampireComponent, UpdateAlertSpriteEvent>(OnUpdateAlert);
|
||||
}
|
||||
|
||||
private void OnToggleFoV(VampireToggleFovEvent args)
|
||||
{
|
||||
var userEntity = _entityManager.GetEntity(args.User);
|
||||
var eyeComponent = _entityManager.GetComponent<EyeComponent>(userEntity);
|
||||
if (userEntity == _playerManager.LocalEntity)
|
||||
_contentEye.RequestToggleFov(userEntity, eyeComponent);
|
||||
}
|
||||
|
||||
private void GetVampireIcons(Entity<VampireComponent> ent, ref GetStatusIconsEvent args)
|
||||
{
|
||||
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
|
||||
args.StatusIcons.Add(iconPrototype);
|
||||
}
|
||||
|
||||
private void GetThrallIcons(Entity<ThrallComponent> ent, ref GetStatusIconsEvent args)
|
||||
{
|
||||
if (HasComp<VampireComponent>(ent))
|
||||
return;
|
||||
|
||||
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
|
||||
args.StatusIcons.Add(iconPrototype);
|
||||
}
|
||||
|
||||
private void OnUpdateAlert(Entity<VampireComponent> ent, ref UpdateAlertSpriteEvent args)
|
||||
{
|
||||
if (args.Alert.ID != ent.Comp.BloodAlert)
|
||||
return;
|
||||
|
||||
var blood = Math.Clamp(ent.Comp.CurrentBlood.Int(), 0, 999);
|
||||
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit1, $"{(blood / 100) % 10}");
|
||||
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit2, $"{(blood / 10) % 10}");
|
||||
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit3, $"{blood % 10}");
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
public sealed class PoolManagerTestEventHandler
|
||||
{
|
||||
// This value is completely arbitrary.
|
||||
private static TimeSpan MaximumTotalTestingTimeLimit => TimeSpan.FromMinutes(20);
|
||||
private static TimeSpan MaximumTotalTestingTimeLimit => TimeSpan.FromMinutes(40);
|
||||
private static TimeSpan HardStopTimeLimit => MaximumTotalTestingTimeLimit.Add(TimeSpan.FromMinutes(1));
|
||||
|
||||
[OneTimeSetUp]
|
||||
|
||||
@@ -226,15 +226,20 @@ public sealed class NukeOpsTest
|
||||
Assert.That(total, Is.GreaterThan(3));
|
||||
|
||||
// Check the nukie commander passed basic training and figured out how to breathe.
|
||||
// Skip respirator checks for IPC (they don't breathe)
|
||||
var isIpc = entMan.GetComponent<MetaDataComponent>(player).EntityPrototype?.ID == "MobIpc";
|
||||
var totalSeconds = 30;
|
||||
var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds);
|
||||
var increment = 5;
|
||||
var resp = entMan.GetComponent<RespiratorComponent>(player);
|
||||
RespiratorComponent? resp = null;
|
||||
if (!isIpc)
|
||||
resp = entMan.GetComponent<RespiratorComponent>(player);
|
||||
var damage = entMan.GetComponent<DamageableComponent>(player);
|
||||
for (var tick = 0; tick < totalTicks; tick += increment)
|
||||
{
|
||||
await pair.RunTicksSync(increment);
|
||||
Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
|
||||
if (!isIpc)
|
||||
Assert.That(resp!.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
|
||||
Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ public static class ServerPackaging
|
||||
"Content.Server",
|
||||
"Content.Shared",
|
||||
"Content.Shared.Database",
|
||||
"Content.Packaging",
|
||||
};
|
||||
|
||||
private static readonly List<string> ServerExtraAssemblies = new()
|
||||
|
||||
@@ -31,6 +31,8 @@ public sealed partial class AdminVerbSystem
|
||||
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
|
||||
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
|
||||
private static readonly EntProtoId DefaultWizardRule = "Wizard";
|
||||
private static readonly EntProtoId DefaultVampireRule = "Vampire";
|
||||
private static readonly EntProtoId DefaultBloodBrothersRule = "BloodBrothers";
|
||||
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
|
||||
|
||||
// All antag verbs have names so invokeverb works.
|
||||
@@ -207,6 +209,36 @@ public sealed partial class AdminVerbSystem
|
||||
};
|
||||
args.Verbs.Add(wizard);
|
||||
|
||||
var vampireName = Loc.GetString("admin-verb-text-make-vampire");
|
||||
Verb vampire = new()
|
||||
{
|
||||
Text = vampireName,
|
||||
Category = VerbCategory.Antag,
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_Wega/Interface/Actions/actions_vampire.rsi"), "bite"),
|
||||
Act = () =>
|
||||
{
|
||||
_antag.ForceMakeAntag<VampireRuleComponent>(targetPlayer, DefaultVampireRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = string.Join(": ", vampireName, Loc.GetString("admin-verb-make-vampire")),
|
||||
};
|
||||
args.Verbs.Add(vampire);
|
||||
|
||||
var bloodBrothersName = Loc.GetString("admin-verb-text-make-blood-brothers");
|
||||
Verb bloodBrothers = new()
|
||||
{
|
||||
Text = bloodBrothersName,
|
||||
Category = VerbCategory.Antag,
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_Wega/Interface/Actions/actions_vampire.rsi"), "blood_bond"),
|
||||
Act = () =>
|
||||
{
|
||||
_antag.ForceMakeAntag<BloodBrotherRuleComponent>(targetPlayer, DefaultBloodBrothersRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = string.Join(": ", bloodBrothersName, Loc.GetString("admin-verb-make-blood-brothers")),
|
||||
};
|
||||
args.Verbs.Add(bloodBrothers);
|
||||
|
||||
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
|
||||
args.Verbs.Add(paradox);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,33 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
|
||||
}
|
||||
}
|
||||
|
||||
// WyLab-Wega-Start
|
||||
public bool TryAddMetabolizerType(MetabolizerComponent component, string metabolizerType)
|
||||
{
|
||||
if (!_prototypeManager.HasIndex<MetabolizerTypePrototype>(metabolizerType))
|
||||
return false;
|
||||
|
||||
if (component.MetabolizerTypes == null)
|
||||
component.MetabolizerTypes = new();
|
||||
|
||||
return component.MetabolizerTypes.Add(metabolizerType);
|
||||
}
|
||||
|
||||
public bool TryRemoveMetabolizerType(MetabolizerComponent component, string metabolizerType)
|
||||
{
|
||||
if (component.MetabolizerTypes == null)
|
||||
return true;
|
||||
|
||||
return component.MetabolizerTypes.Remove(metabolizerType);
|
||||
}
|
||||
|
||||
public void ClearMetabolizerTypes(MetabolizerComponent component)
|
||||
{
|
||||
if (component.MetabolizerTypes != null)
|
||||
component.MetabolizerTypes.Clear();
|
||||
}
|
||||
// WyLab-Wega-End
|
||||
|
||||
private void OnApplyMetabolicMultiplier(Entity<MetabolizerComponent> ent, ref ApplyMetabolicMultiplierEvent args)
|
||||
{
|
||||
ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
|
||||
|
||||
@@ -135,6 +135,7 @@ namespace Content.Server.Communications
|
||||
List<string>? levels = null;
|
||||
string currentLevel = default!;
|
||||
float currentDelay = 0;
|
||||
var currentAlertColor = Color.White;
|
||||
|
||||
if (stationUid != null)
|
||||
{
|
||||
@@ -150,6 +151,11 @@ namespace Content.Server.Communications
|
||||
{
|
||||
levels.Add(id);
|
||||
}
|
||||
|
||||
if (id == alertComp.CurrentLevel)
|
||||
{
|
||||
currentAlertColor = detail.Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +169,7 @@ namespace Content.Server.Communications
|
||||
CanCallOrRecall(comp),
|
||||
levels,
|
||||
currentLevel,
|
||||
currentAlertColor,
|
||||
currentDelay,
|
||||
_roundEndSystem.ExpectedCountdownEnd
|
||||
));
|
||||
|
||||
@@ -207,7 +207,7 @@ namespace Content.Server.Entry
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
var dest = _cfg.GetCVar(CCVars.DestinationFile);
|
||||
if (!string.IsNullOrEmpty(dest))
|
||||
if (string.IsNullOrEmpty(dest))
|
||||
{
|
||||
_playTimeTracking.Shutdown();
|
||||
_dbManager.Shutdown();
|
||||
|
||||
127
Content.Server/_WL/Administration/Commands/DayNightCommand.cs
Normal file
127
Content.Server/_WL/Administration/Commands/DayNightCommand.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Content.Server._WL.DayNight;
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Server._WL.Administration.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Mapping)]
|
||||
public sealed partial class DayNightCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
[Dependency] private readonly IMapManager _mapMan = default!;
|
||||
|
||||
public override string Command => "daynight";
|
||||
public override string Description
|
||||
=>
|
||||
"""
|
||||
Добавляет карте смену дня и ночи.
|
||||
Желательно, чтоб это была планета.
|
||||
Также желательно, чтобы эта команда использовалась только с неинициализированными картами.
|
||||
""";
|
||||
|
||||
public override string Help => "daynight <mapId> <fullCycle> <dayRatio> <nightRatio> <dayColor> <nightColor>";
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(_mapMan.GetAllMapIds().Select(x => x.ToString()), "MapId");
|
||||
}
|
||||
else if (args.Length == 2)
|
||||
{
|
||||
return CompletionResult.FromHint("FullCycle in seconds");
|
||||
}
|
||||
else if (args.Length == 3)
|
||||
{
|
||||
return CompletionResult.FromHint("Day ratio an integer");
|
||||
}
|
||||
else if (args.Length == 4)
|
||||
{
|
||||
return CompletionResult.FromHint("Night ration an integer");
|
||||
}
|
||||
else if (args.Length == 5)
|
||||
{
|
||||
return CompletionResult.FromHint("Day Hex");
|
||||
}
|
||||
else if (args.Length == 6)
|
||||
{
|
||||
return CompletionResult.FromHint("Night Hex");
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 6 && args.Length != 4)
|
||||
{
|
||||
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
var mapSys = _entMan.System<MapSystem>();
|
||||
|
||||
if (!int.TryParse(args[0], out var mapIntegerId))
|
||||
{
|
||||
shell.WriteError("MapId должно быть числом!");
|
||||
return;
|
||||
}
|
||||
|
||||
var mapId = new MapId(mapIntegerId);
|
||||
|
||||
if (!mapSys.MapExists(mapId))
|
||||
{
|
||||
shell.WriteError($"Карты с ID равнм {mapIntegerId} не существует!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[1], out var fullCycleTime) || fullCycleTime <= 0)
|
||||
{
|
||||
shell.WriteError("fullCycleTime должен представлять целое число большее нуля!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[2], out var dayRatio) || dayRatio <= 0)
|
||||
{
|
||||
shell.WriteError("dayRatio должен представлять целое число большее нуля!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[3], out var nightRatio) || nightRatio <= 0)
|
||||
{
|
||||
shell.WriteError("nightRatio должен представлять целое число большее нуля!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mapSys.TryGetMap(mapId, out var mapUid) || mapUid == null)
|
||||
{
|
||||
shell.WriteError("Неизвестная ошибка.");
|
||||
return;
|
||||
}
|
||||
|
||||
var dayNnightComp = _entMan.EnsureComponent<DayNightComponent>(mapUid.Value);
|
||||
|
||||
dayNnightComp.DayNightRatio = new Vector2(dayRatio, nightRatio);
|
||||
dayNnightComp.FullCycle = TimeSpan.FromSeconds(fullCycleTime);
|
||||
|
||||
if (args.Length != 6)
|
||||
return;
|
||||
|
||||
var dayColor = Color.TryFromHex(args[4]);
|
||||
var nightColor = Color.TryFromHex(args[5]);
|
||||
if (dayColor != null)
|
||||
{
|
||||
dayNnightComp.DayHex = args[4];
|
||||
}
|
||||
if (nightColor != null)
|
||||
{
|
||||
dayNnightComp.NightHex = args[5];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Content.Server/_WL/DayNight/DayNightComponent.cs
Normal file
30
Content.Server/_WL/DayNight/DayNightComponent.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Server._WL.DayNight
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class DayNightComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField]
|
||||
public TimeSpan FullCycle = TimeSpan.FromSeconds(1200);
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("ratio")]
|
||||
public Vector2 DayNightRatio = new(6, 4);
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("day")]
|
||||
public string DayHex = "#F7CA68FF";
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("night")]
|
||||
public string NightHex = "#0f1026";
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public bool WasInit = false;
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public TimeSpan NextCycle;
|
||||
}
|
||||
}
|
||||
97
Content.Server/_WL/DayNight/DayNightSystem.cs
Normal file
97
Content.Server/_WL/DayNight/DayNightSystem.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Server._WL.DayNight
|
||||
{
|
||||
public sealed partial class DayNightSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTime = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly MapSystem _mapSys = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DayNightComponent, MapInitEvent>(OnMapInit, after: [typeof(SharedMapSystem)]);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<DayNightComponent>();
|
||||
while (query.MoveNext(out var map, out var dayNightComp))
|
||||
{
|
||||
if (!TryComp<MapLightComponent>(map, out var mapLightComp))
|
||||
continue;
|
||||
|
||||
if (!TryComp<MapComponent>(map, out var mapComponent))
|
||||
continue;
|
||||
|
||||
if (!dayNightComp.WasInit || mapComponent.MapPaused)
|
||||
continue;
|
||||
|
||||
if (_gameTime.CurTime >= dayNightComp.NextCycle)
|
||||
dayNightComp.NextCycle += dayNightComp.FullCycle;
|
||||
|
||||
var color = CalculateColor(
|
||||
_gameTime.CurTime,
|
||||
dayNightComp.FullCycle,
|
||||
dayNightComp.NextCycle,
|
||||
Color.FromHex(dayNightComp.DayHex),
|
||||
Color.FromHex(dayNightComp.NightHex),
|
||||
dayNightComp.DayNightRatio);
|
||||
|
||||
if (color == mapLightComp.AmbientLightColor) //Оптимизация для случаев, если цикл дня и ночи огромен.
|
||||
continue;
|
||||
|
||||
_mapSys.SetAmbientLight(mapComponent.MapId, color);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid station, DayNightComponent comp, MapInitEvent args)
|
||||
{
|
||||
if (!TryComp<MapComponent>(station, out var mapComponent))
|
||||
return;
|
||||
|
||||
_mapSys.SetAmbientLight(mapComponent.MapId, Color.FromHex(comp.DayHex));
|
||||
comp.NextCycle = _gameTime.CurTime + comp.FullCycle;
|
||||
comp.WasInit = true;
|
||||
}
|
||||
|
||||
public static Color CalculateColor(TimeSpan currentTime, TimeSpan fullCycle, TimeSpan nextCycle, Color dayColor, Color nightColor, Vector2 dayNightRatio)
|
||||
{
|
||||
currentTime = currentTime - (nextCycle - fullCycle);
|
||||
|
||||
var pair = dayNightRatio.X + dayNightRatio.Y;
|
||||
|
||||
var dayTime = fullCycle.TotalMinutes / pair * dayNightRatio.X;
|
||||
var nightTime = fullCycle.TotalMinutes / pair * dayNightRatio.Y;
|
||||
|
||||
var isDay = currentTime.TotalMinutes <= dayTime;
|
||||
|
||||
var filledPercentage = isDay
|
||||
? currentTime.TotalMinutes / dayTime
|
||||
: (currentTime.TotalMinutes - dayTime) / nightTime;
|
||||
|
||||
var r = isDay
|
||||
? dayColor.R + (nightColor.R - dayColor.R) * filledPercentage
|
||||
: nightColor.R + (dayColor.R - nightColor.R) * filledPercentage;
|
||||
var g = isDay
|
||||
? dayColor.G + (nightColor.G - dayColor.G) * filledPercentage
|
||||
: nightColor.G + (dayColor.G - nightColor.G) * filledPercentage;
|
||||
var b = isDay
|
||||
? dayColor.B + (nightColor.B - dayColor.B) * filledPercentage
|
||||
: nightColor.B + (dayColor.B - nightColor.B) * filledPercentage;
|
||||
|
||||
var result = new Color((float) r, (float) g, (float) b);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._WL.Destructible.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class FrozenComponent : Component
|
||||
{
|
||||
[DataField] public LocId FrozenPrefix = "frozen-entity-prefix";
|
||||
|
||||
[DataField] public LocId FrozenPopup = "frozen-entity-popup";
|
||||
[DataField] public LocId FrozenHealthString = "frozen-entity-health-string";
|
||||
|
||||
[DataField] public string BaseName;
|
||||
[DataField] public Color BaseSkinColor;
|
||||
|
||||
[DataField] public ProtoId<DamageTypePrototype> FrozenDamage = "Cold";
|
||||
}
|
||||
}
|
||||
62
Content.Server/_WL/Destructible/Systems/FrozenSystem.cs
Normal file
62
Content.Server/_WL/Destructible/Systems/FrozenSystem.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Content.Server._WL.Destructible.Components;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Shared.Cloning;
|
||||
using Content.Shared.Cloning.Events;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Events;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.HealthExaminable;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Rejuvenate;
|
||||
|
||||
namespace Content.Server._WL.Destructible.Systems
|
||||
{
|
||||
public sealed partial class FrozenSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly HumanoidAppearanceSystem _appearance = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<FrozenComponent, RefreshNameModifiersEvent>(OnRefreshName);
|
||||
SubscribeLocalEvent<FrozenComponent, BeforeDamageChangedEvent>(BeforeDamageChanged);
|
||||
SubscribeLocalEvent<FrozenComponent, CloningEvent>(OnClone);
|
||||
SubscribeLocalEvent<FrozenComponent, HealthBeingExaminedEvent>(OnHealthExamine);
|
||||
SubscribeLocalEvent<FrozenComponent, RejuvenateEvent>(OnRejuvenate);
|
||||
}
|
||||
|
||||
private void OnRefreshName(EntityUid ent, FrozenComponent comp, RefreshNameModifiersEvent args)
|
||||
{
|
||||
args.AddModifier(comp.FrozenPrefix);
|
||||
args.AddModifier(comp.BaseName, int.MinValue);
|
||||
}
|
||||
|
||||
private void BeforeDamageChanged(EntityUid ent, FrozenComponent comp, ref BeforeDamageChangedEvent args)
|
||||
{
|
||||
args.Damage.DamageDict[comp.FrozenDamage.Id] = 0f;
|
||||
args.Damage.TrimZeros();
|
||||
}
|
||||
|
||||
private void OnClone(EntityUid ent, FrozenComponent comp, ref CloningEvent args)
|
||||
{
|
||||
var target = args.CloneUid;
|
||||
_metaData.SetEntityName(target, comp.BaseName, raiseEvents: true);
|
||||
_appearance.SetSkinColor(target, comp.BaseSkinColor);
|
||||
}
|
||||
|
||||
private void OnHealthExamine(EntityUid ent, FrozenComponent comp, HealthBeingExaminedEvent args)
|
||||
{
|
||||
args.Message.AddMarkupOrThrow("\n" + Loc.GetString(comp.FrozenHealthString));
|
||||
}
|
||||
|
||||
private void OnRejuvenate(EntityUid ent, FrozenComponent comp, RejuvenateEvent args)
|
||||
{
|
||||
_metaData.SetEntityName(ent, comp.BaseName, raiseEvents: true);
|
||||
_appearance.SetSkinColor(ent, comp.BaseSkinColor);
|
||||
|
||||
RemComp<FrozenComponent>(ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Content.Server._WL.Destructible.Components;
|
||||
using Content.Server.Destructible;
|
||||
using Content.Server.Destructible.Thresholds.Behaviors;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Shared.Atmos.Rotting;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.NameModifier.Components;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Popups;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Enums;
|
||||
|
||||
namespace Content.Server._WL.Destructible.Thresholds.Behaviors
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed partial class FrozeBodyBehavior : IThresholdBehavior
|
||||
{
|
||||
public const float InterpolateStrength = 0.88f;
|
||||
public static readonly Color InterpolateColor = Color.CadetBlue;
|
||||
|
||||
public void Execute(EntityUid bodyId, DestructibleSystem system, EntityUid? cause = null)
|
||||
{
|
||||
var entMan = system.EntityManager;
|
||||
var humanoidAppearanceSys = entMan.System<HumanoidAppearanceSystem>();
|
||||
var transformSys = entMan.System<TransformSystem>();
|
||||
var popupSys = entMan.System<SharedPopupSystem>();
|
||||
var metaDataSys = entMan.System<MetaDataSystem>();
|
||||
|
||||
var frozenComp = entMan.EnsureComponent<FrozenComponent>(bodyId);
|
||||
|
||||
//Обновляем цвет кожи
|
||||
if (!entMan.TryGetComponent<HumanoidAppearanceComponent>(bodyId, out var humanoidAppearnceComp))
|
||||
return;
|
||||
|
||||
var curColor = humanoidAppearnceComp.SkinColor;
|
||||
frozenComp.BaseSkinColor = curColor;
|
||||
|
||||
humanoidAppearanceSys.SetSkinColor(
|
||||
bodyId,
|
||||
Color.InterpolateBetween(curColor, InterpolateColor, InterpolateStrength),
|
||||
sync: true,
|
||||
verify: false
|
||||
);
|
||||
|
||||
//Устанавливаем префикс
|
||||
var baseName = Identity.Name(bodyId, entMan);
|
||||
frozenComp.BaseName = baseName;
|
||||
|
||||
var genderString = humanoidAppearnceComp.Gender switch
|
||||
{
|
||||
Gender.Male => "male",
|
||||
Gender.Female => "female",
|
||||
_ => "other"
|
||||
};
|
||||
|
||||
var newName = $"{Loc.GetString(frozenComp.FrozenPrefix, ("gender", genderString))} {baseName}";
|
||||
|
||||
metaDataSys.SetEntityName(bodyId, newName);
|
||||
|
||||
//Запрещаем хил тела и разрешаем клонирование, убрав компонент гниения
|
||||
entMan.RemoveComponent<PerishableComponent>(bodyId);
|
||||
entMan.RemoveComponent<InjectableSolutionComponent>(bodyId);
|
||||
|
||||
//Поп-ап
|
||||
var msg = Loc.GetString(frozenComp.FrozenPopup,
|
||||
("name", baseName),
|
||||
("gender", genderString));
|
||||
|
||||
popupSys.PopupCoordinates(
|
||||
msg,
|
||||
transformSys.GetMoverCoordinates(bodyId),
|
||||
Robust.Shared.Player.Filter.Pvs(bodyId),
|
||||
true,
|
||||
PopupType.LargeCaution);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Content.Server._WL.Nutrition.Systems;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server._WL.Nutrition.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class SuckableFoodComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public string Solution { get; set; } = "food";
|
||||
|
||||
/// <summary>
|
||||
/// Количество поглощаемой из контейнера жидкости в секунду.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public FixedPoint2 DissolveAmount { get; set; } = FixedPoint2.New(0.05f);
|
||||
|
||||
/// <summary>
|
||||
/// Не указывайте сущности в прототипе, у которых есть <see cref="SuckableFoodComponent"/>, иначе будет runtime-ошибочка.
|
||||
/// </summary>
|
||||
[DataField("entityOnDissolve")]
|
||||
public EntProtoId<ClothingComponent>? EquippedEntityOnDissolve { get; set; }
|
||||
|
||||
[DataField]
|
||||
public ComponentRegistry? ComponentsOverride { get; set; }
|
||||
|
||||
[DataField]
|
||||
public bool CanSuck { get; set; } = true;
|
||||
|
||||
[DataField]
|
||||
public bool DeleteOnEmpty { get; set; } = true;
|
||||
|
||||
public bool IsSucking => SuckingEntity != null && CanSuck;
|
||||
|
||||
[Access(typeof(SuckableFoodSystem))]
|
||||
public EntityUid? SuckingEntity;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Content.Server._WL.Nutrition.Components;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server._WL.Nutrition.Events;
|
||||
|
||||
public sealed partial class SuckableFoodDissolvedEvent : EntityEventArgs
|
||||
{
|
||||
public Entity<SuckableFoodComponent> Suckable { get; }
|
||||
public BaseContainer Container { get; }
|
||||
|
||||
public EntityUid Sucker { get; }
|
||||
|
||||
public SuckableFoodDissolvedEvent(Entity<SuckableFoodComponent> suckable, BaseContainer container, EntityUid sucker)
|
||||
{
|
||||
Suckable = suckable;
|
||||
Container = container;
|
||||
Sucker = sucker;
|
||||
}
|
||||
}
|
||||
189
Content.Server/_WL/Nutrition/Systems/SuckableFoodSystem.cs
Normal file
189
Content.Server/_WL/Nutrition/Systems/SuckableFoodSystem.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using Content.Server._WL.Nutrition.Components;
|
||||
using Content.Server._WL.Nutrition.Events;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Containers.EntitySystems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Prototypes;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Content.Server._WL.Nutrition.Systems;
|
||||
|
||||
public sealed partial class SuckableFoodSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly ForensicsSystem _forensics = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly FlavorProfileSystem _flavor = default!;
|
||||
|
||||
private const float UpdatePeriod = 2f; // in seconds
|
||||
private float _updateTimer = 0f;
|
||||
|
||||
private static readonly LocId PutInMouthLoc = "food-sweets-put-in-mouth-popup-message";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SuckableFoodComponent, GotEquippedEvent>(OnEquip);
|
||||
SubscribeLocalEvent<SuckableFoodComponent, GotUnequippedEvent>(ResetSucker);
|
||||
|
||||
SubscribeLocalEvent<SuckableFoodComponent, ComponentShutdown>(ResetSucker);
|
||||
|
||||
SubscribeLocalEvent<SuckableFoodComponent, SuckableFoodDissolvedEvent>(OnDissolved);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
_updateTimer += frameTime;
|
||||
|
||||
var isNewLoop = _updateTimer >= UpdatePeriod;
|
||||
|
||||
var query = EntityQueryEnumerator<SuckableFoodComponent, SolutionContainerManagerComponent>();
|
||||
while (query.MoveNext(out var food, out var suckableComp, out var solContainerManComp))
|
||||
{
|
||||
if (!Exists(suckableComp.SuckingEntity))
|
||||
{
|
||||
suckableComp.SuckingEntity = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isNewLoop)
|
||||
{
|
||||
var sucker = suckableComp.SuckingEntity.Value;
|
||||
|
||||
if (!TryComp<BloodstreamComponent>(sucker, out var bloodstreamComp))
|
||||
continue;
|
||||
|
||||
suckableComp.CanSuck = _mobState.IsAlive(sucker); // TODO: вынести в отдельное событие
|
||||
if (!suckableComp.IsSucking)
|
||||
continue;
|
||||
|
||||
if (!EnsureSolutionEntity((food, suckableComp, solContainerManComp), out var solutionEntity, out var solution))
|
||||
continue;
|
||||
|
||||
var dissolvedSol = _solutionContainerSystem.SplitSolution(solutionEntity.Value, suckableComp.DissolveAmount * UpdatePeriod);
|
||||
|
||||
if (solution.Volume == FixedPoint2.Zero)
|
||||
{
|
||||
if (_container.TryGetContainingContainer(food, out var container))
|
||||
{
|
||||
var ev = new SuckableFoodDissolvedEvent((food, suckableComp), container, sucker);
|
||||
|
||||
RaiseLocalEvent(food, ev);
|
||||
RaiseLocalEvent(ev);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
_reactiveSystem.DoEntityReaction(sucker, dissolvedSol, ReactionMethod.Ingestion);
|
||||
_bloodstreamSystem.TryAddToBloodstream((sucker, bloodstreamComp), dissolvedSol);
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewLoop)
|
||||
_updateTimer -= UpdatePeriod;
|
||||
}
|
||||
|
||||
public void SetState(Entity<SuckableFoodComponent> foodEnt, EntityUid? sucker)
|
||||
{
|
||||
var (food, comp) = foodEnt;
|
||||
|
||||
comp.SuckingEntity = sucker;
|
||||
}
|
||||
|
||||
public bool EnsureSolutionEntity(
|
||||
Entity<SuckableFoodComponent, SolutionContainerManagerComponent?> foodEnt,
|
||||
[NotNullWhen(true)] out Entity<SolutionComponent>? solEnt,
|
||||
[NotNullWhen(true)] out Solution? solution)
|
||||
{
|
||||
solEnt = null;
|
||||
solution = null;
|
||||
|
||||
if (!Resolve(foodEnt, ref foodEnt.Comp2, false))
|
||||
return false;
|
||||
|
||||
if (!_solutionContainerSystem.EnsureSolutionEntity((foodEnt, foodEnt.Comp2), foodEnt.Comp1.Solution, out var ent))
|
||||
return false;
|
||||
|
||||
solEnt = ent;
|
||||
solution = ent.Value.Comp.Solution;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnEquip(EntityUid food, SuckableFoodComponent comp, GotEquippedEvent ev)
|
||||
{
|
||||
if (ev.SlotFlags.HasFlag(SlotFlags.MASK))
|
||||
_forensics.TransferDna(food, ev.Equipee);
|
||||
|
||||
SetState((food, comp), ev.Equipee);
|
||||
|
||||
if (!EnsureSolutionEntity((food, comp), out _, out var sol))
|
||||
return;
|
||||
|
||||
var flavor = _flavor.GetLocalizedFlavorsMessage(food, ev.Equipee, sol);
|
||||
if (string.IsNullOrEmpty(flavor))
|
||||
return;
|
||||
|
||||
var msg = Loc.GetString(PutInMouthLoc, ("flavor", flavor), ("entity", Identity.Name(food, EntityManager, ev.Equipee)));
|
||||
|
||||
_popup.PopupEntity(msg, ev.Equipee, Filter.Entities(ev.Equipee), false);
|
||||
}
|
||||
|
||||
private void ResetSucker<T>(EntityUid food, SuckableFoodComponent comp, T ev)
|
||||
{
|
||||
SetState((food, comp), null);
|
||||
}
|
||||
|
||||
|
||||
private void OnDissolved(EntityUid food, SuckableFoodComponent comp, SuckableFoodDissolvedEvent ev)
|
||||
{
|
||||
if (comp.DeleteOnEmpty)
|
||||
{
|
||||
_inventory.TryUnequip(ev.Sucker, ev.Container.ID, true, true);
|
||||
|
||||
var msg = Loc.GetString("food-sweets-got-dissolved-popup-message", ("entity", Identity.Name(food, EntityManager)));
|
||||
_popup.PopupEntity(msg, ev.Sucker, Filter.Entities(ev.Sucker), true, Shared.Popups.PopupType.Medium);
|
||||
|
||||
TryQueueDel(food);
|
||||
}
|
||||
|
||||
if (comp.EquippedEntityOnDissolve != null)
|
||||
{
|
||||
if (_protoMan.TryIndex(comp.EquippedEntityOnDissolve.Value, out var proto)
|
||||
&& proto.HasComponent<SuckableFoodComponent>(_componentFactory))
|
||||
{
|
||||
Log.Error($"EquippedEntityOnDissolve {comp.EquippedEntityOnDissolve.Value} on entity {ToPrettyString(food)} has {nameof(SuckableFoodComponent)}!");
|
||||
return;
|
||||
}
|
||||
|
||||
var ent = SpawnNextToOrDrop(comp.EquippedEntityOnDissolve.Value, ev.Sucker, overrides: comp.ComponentsOverride);
|
||||
_inventory.TryEquip(ev.Sucker, ent, ev.Container.ID, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Content.Server/_Wega/Commands/GhostRespawnCommand.cs
Normal file
67
Content.Server/_Wega/Commands/GhostRespawnCommand.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Mind;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Ghost;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Wega.Commands;
|
||||
|
||||
[AnyCommand()]
|
||||
public sealed class GhostRespawnCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
public string Command => "ghostrespawn";
|
||||
public string Description => "Allows the player to return to the lobby if they've been dead long enough, allowing re-entering the round AS ANOTHER CHARACTER.";
|
||||
public string Help => $"{Command}";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (!_configurationManager.GetCVar(WegaCVars.GhostRespawnEnabled))
|
||||
{
|
||||
shell.WriteLine("Respawning is disabled, ask an admin to respawn you.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (shell.Player is null)
|
||||
{
|
||||
shell.WriteLine("You cannot run this from the console!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (shell.Player.AttachedEntity is null)
|
||||
{
|
||||
shell.WriteLine("You cannot run this in the lobby, or without an entity.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entityManager.TryGetComponent<GhostComponent>(shell.Player.AttachedEntity, out var ghost))
|
||||
{
|
||||
shell.WriteLine("You are not a ghost.");
|
||||
return;
|
||||
}
|
||||
|
||||
var mindSystem = _entityManager.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
if (!mindSystem.TryGetMind(shell.Player, out _, out _))
|
||||
{
|
||||
shell.WriteLine("You have no mind.");
|
||||
return;
|
||||
}
|
||||
var time = (_gameTiming.CurTime - ghost.TimeOfDeath);
|
||||
var respawnTime = _configurationManager.GetCVar(WegaCVars.GhostRespawnTime);
|
||||
|
||||
if (respawnTime > time.TotalSeconds)
|
||||
{
|
||||
shell.WriteLine($"You haven't been dead long enough. You have been dead {time.TotalSeconds} seconds of the required {respawnTime}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameTicker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
||||
gameTicker.Respawn(shell.Player);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Friendly.Faction;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
|
||||
namespace Content.Server.Friendly.Faction
|
||||
{
|
||||
public sealed partial class FriendlyFactionSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<FriendlyFactionComponent, MeleeHitEvent>(OnMeleeHit);
|
||||
}
|
||||
|
||||
private void OnMeleeHit(EntityUid uid, FriendlyFactionComponent component, MeleeHitEvent args)
|
||||
{
|
||||
if (!TryComp<FriendlyFactionComponent>(args.User, out _))
|
||||
return;
|
||||
|
||||
if (!args.HitEntities.Any())
|
||||
return;
|
||||
|
||||
foreach (var entity in args.HitEntities)
|
||||
{
|
||||
if (args.User == entity)
|
||||
continue;
|
||||
|
||||
if (!TryComp<MobStateComponent>(entity, out _))
|
||||
continue;
|
||||
|
||||
if (TryComp<FriendlyFactionComponent>(entity, out var friendlyFaction)
|
||||
&& friendlyFaction.Faction == component.Faction)
|
||||
{
|
||||
args.BonusDamage = -args.BaseDamage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
269
Content.Server/_Wega/GameTicking/Rules/BloodBrotherRuleSystem.cs
Normal file
269
Content.Server/_Wega/GameTicking/Rules/BloodBrotherRuleSystem.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
using System.Text;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Components;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Content.Shared.Blood.Brother;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class BloodBrotherRuleSystem : GameRuleSystem<BloodBrotherRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly ObjectivesSystem _objectives = default!;
|
||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
|
||||
[Dependency] private readonly BloodBrotherSharedStealConditionSystem _stealCondition = default!;
|
||||
[Dependency] private readonly BloodBrotherSharedKillConditionSystem _killCondition = default!;
|
||||
|
||||
|
||||
private static readonly Color BloodBrotherColor = Color.FromHex("#8b0000");
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<BloodBrotherRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
|
||||
}
|
||||
|
||||
private void AfterEntitySelected(Entity<BloodBrotherRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
CreateBloodBrotherPair(args.EntityUid, ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pair of blood brothers
|
||||
/// </summary>
|
||||
public void CreateBloodBrotherPair(EntityUid bloodBrother, Entity<BloodBrotherRuleComponent> component)
|
||||
{
|
||||
if (!_mindSystem.TryGetMind(bloodBrother, out var mindId, out var mind))
|
||||
return;
|
||||
|
||||
component.Comp.BloodBrotherMinds.Add(mindId);
|
||||
|
||||
EntityUid? brotherMindId = FindUnpairedBrother(mindId, component.Comp);
|
||||
|
||||
if (brotherMindId != null)
|
||||
{
|
||||
CreateBloodBrotherPairInternal(mindId, brotherMindId.Value, component);
|
||||
GenerateSharedObjectives(mindId, brotherMindId.Value, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateSharedObjectives(EntityUid mindId1, EntityUid mindId2, Entity<BloodBrotherRuleComponent> component)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mindId1, out var mind1) || !TryComp<MindComponent>(mindId2, out var mind2))
|
||||
return;
|
||||
|
||||
var currentDifficulty = 0f;
|
||||
var selectedObjectives = new List<EntityUid>();
|
||||
|
||||
foreach (var set in component.Comp.ObjectiveSets)
|
||||
{
|
||||
if (!_random.Prob(set.Prob))
|
||||
continue;
|
||||
|
||||
for (var pick = 0; pick < set.MaxPicks && component.Comp.MaxDifficulty > currentDifficulty; pick++)
|
||||
{
|
||||
var objective = _objectives.GetRandomObjective(mindId1, mind1, set.Groups, component.Comp.MaxDifficulty - currentDifficulty);
|
||||
if (objective == null)
|
||||
continue;
|
||||
|
||||
var objectiveComp = Comp<ObjectiveComponent>(objective.Value);
|
||||
currentDifficulty += objectiveComp.Difficulty;
|
||||
selectedObjectives.Add(objective.Value);
|
||||
Log.Debug($"Selected random objective {ToPrettyString(objective)} for blood brothers pair");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var mandatoryObjectiveProto in component.Comp.RequiredObjectives)
|
||||
{
|
||||
var objective = _objectives.TryCreateObjective(mindId1, mind1, mandatoryObjectiveProto);
|
||||
if (objective != null)
|
||||
{
|
||||
var objectiveComp = Comp<ObjectiveComponent>(objective.Value);
|
||||
currentDifficulty += objectiveComp.Difficulty;
|
||||
selectedObjectives.Add(objective.Value);
|
||||
Log.Debug($"Added mandatory objective {mandatoryObjectiveProto} for blood brothers pair");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"Failed to create mandatory objective {mandatoryObjectiveProto} for blood brothers");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var objective in selectedObjectives)
|
||||
{
|
||||
var proto = MetaData(objective).EntityPrototype?.ID;
|
||||
if (proto == null)
|
||||
continue;
|
||||
|
||||
var objective2 = _objectives.TryCreateObjective(mindId2, mind2, proto);
|
||||
if (objective2 != null)
|
||||
{
|
||||
_mindSystem.AddObjective(mindId2, mind2, objective2.Value);
|
||||
|
||||
CopyObjectiveData(objective, objective2.Value, mindId1, mindId2);
|
||||
|
||||
Log.Debug($"Created shared objective {proto} for both brothers");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var objective in selectedObjectives)
|
||||
{
|
||||
_mindSystem.AddObjective(mindId1, mind1, objective);
|
||||
}
|
||||
|
||||
Log.Info($"Generated {selectedObjectives.Count} shared objectives for blood brothers pair ({mindId1} and {mindId2})");
|
||||
}
|
||||
|
||||
private void CopyObjectiveData(EntityUid sourceObjective, EntityUid targetObjective, EntityUid mindId1, EntityUid mindId2)
|
||||
{
|
||||
if (TryComp<TargetObjectiveComponent>(sourceObjective, out var sourceTarget)
|
||||
&& sourceTarget.Target.HasValue && TryComp<TargetObjectiveComponent>(targetObjective, out var targetTarget))
|
||||
{
|
||||
_target.SetTarget(targetObjective, sourceTarget.Target.Value, targetTarget);
|
||||
}
|
||||
|
||||
_sharedCondition.CopySharedConditionData(sourceObjective, targetObjective, mindId1, mindId2);
|
||||
_stealCondition.CopySharedStealConditionData(sourceObjective, targetObjective);
|
||||
_killCondition.CopySharedKillConditionData(sourceObjective, targetObjective);
|
||||
}
|
||||
|
||||
private EntityUid? FindUnpairedBrother(EntityUid mindId, BloodBrotherRuleComponent component)
|
||||
{
|
||||
foreach (var otherMindId in component.BloodBrotherMinds)
|
||||
{
|
||||
if (otherMindId == mindId || component.BloodBrotherPairs.ContainsKey(mindId)
|
||||
|| component.BloodBrotherPairs.ContainsKey(otherMindId)
|
||||
|| component.BloodBrotherPairs.ContainsValue(mindId)
|
||||
|| component.BloodBrotherPairs.ContainsValue(otherMindId))
|
||||
continue;
|
||||
|
||||
return otherMindId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CreateBloodBrotherPairInternal(EntityUid mindId1, EntityUid mindId2, Entity<BloodBrotherRuleComponent> component)
|
||||
{
|
||||
component.Comp.BloodBrotherPairs[mindId1] = mindId2;
|
||||
component.Comp.BloodBrotherPairs[mindId2] = mindId1;
|
||||
|
||||
SetupBloodBrother(mindId1, mindId2, component.Comp);
|
||||
SetupBloodBrother(mindId2, mindId1, component.Comp);
|
||||
}
|
||||
|
||||
private void SetupBloodBrother(EntityUid mindId, EntityUid brotherMindId, BloodBrotherRuleComponent component)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mindId, out var mind) || mind.OwnedEntity == null)
|
||||
return;
|
||||
|
||||
_roleSystem.MindAddRole(mindId, component.BloodBrotherPrototypeId, silent: true);
|
||||
_roleSystem.MindHasRole<BloodBrotherRoleComponent>(mindId, out var bloodBrotherRole);
|
||||
|
||||
if (bloodBrotherRole is not null)
|
||||
{
|
||||
EnsureComp<BloodBrotherComponent>(mindId, out var bloodBrotherComp);
|
||||
bloodBrotherComp.BrotherMind = brotherMindId;
|
||||
bloodBrotherComp.RequireBothAlive = component.RequireBothAlive;
|
||||
|
||||
// Get brother info for RoleBriefingComponent
|
||||
var brotherName = GetBrotherName(brotherMindId);
|
||||
var brotherJob = GetBrotherJob(brotherMindId);
|
||||
var brotherBriefing = Loc.GetString("bloodbrother-role-brother-info",
|
||||
("brotherName", brotherName),
|
||||
("brotherJob", brotherJob));
|
||||
|
||||
EnsureComp<RoleBriefingComponent>(bloodBrotherRole.Value.Owner, out var briefingComp);
|
||||
briefingComp.Briefing = brotherBriefing;
|
||||
|
||||
if (component.GiveBriefing)
|
||||
{
|
||||
SendFullBriefing(mindId, brotherMindId, component);
|
||||
}
|
||||
}
|
||||
|
||||
if (mind.OwnedEntity != null)
|
||||
{
|
||||
_npcFaction.RemoveFaction(mind.OwnedEntity.Value, component.NanoTrasenFaction, false);
|
||||
_npcFaction.AddFaction(mind.OwnedEntity.Value, component.SyndicateFaction);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendFullBriefing(EntityUid mindId, EntityUid brotherMindId, BloodBrotherRuleComponent component)
|
||||
{
|
||||
if (!TryComp<MindComponent>(mindId, out var mind) || mind.OwnedEntity == null)
|
||||
return;
|
||||
|
||||
var briefing = GenerateFullBriefing(mindId, brotherMindId, component);
|
||||
_antag.SendBriefing(mind.OwnedEntity.Value, briefing, BloodBrotherColor, component.GreetSoundNotification);
|
||||
}
|
||||
|
||||
private string GenerateFullBriefing(EntityUid mindId, EntityUid brotherMindId, BloodBrotherRuleComponent component)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var issuerPrototype = _prototypeManager.Index(component.ObjectiveIssuers);
|
||||
var issuer = Loc.GetString(_random.Pick(issuerPrototype.Values));
|
||||
|
||||
sb.AppendLine(Loc.GetString("bloodbrother-role-greeting",
|
||||
("corporation", issuer ?? Loc.GetString("objective-issuer-unknown"))));
|
||||
|
||||
var brotherName = GetBrotherName(brotherMindId);
|
||||
var brotherJob = GetBrotherJob(brotherMindId);
|
||||
|
||||
sb.AppendLine("");
|
||||
sb.AppendLine(Loc.GetString("bloodbrother-role-brother-info",
|
||||
("brotherName", brotherName),
|
||||
("brotherJob", brotherJob)));
|
||||
|
||||
sb.AppendLine("");
|
||||
if (component.RequireBothAlive)
|
||||
{
|
||||
sb.AppendLine("-> " + Loc.GetString("bloodbrother-role-both-alive-requirement"));
|
||||
}
|
||||
|
||||
sb.AppendLine("-> " + Loc.GetString("bloodbrother-role-both-escape-requirement"));
|
||||
|
||||
sb.AppendLine("-> " + Loc.GetString("bloodbrother-role-no-uplink-warning"));
|
||||
sb.AppendLine("");
|
||||
sb.AppendLine(Loc.GetString("bloodbrother-role-good-luck"));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GetBrotherName(EntityUid brotherMindId)
|
||||
{
|
||||
if (TryComp<MindComponent>(brotherMindId, out var brotherMind) && brotherMind.CharacterName != null)
|
||||
return brotherMind.CharacterName;
|
||||
|
||||
return Loc.GetString("bloodbrother-unknown-name");
|
||||
}
|
||||
|
||||
private string GetBrotherJob(EntityUid brotherMindId)
|
||||
{
|
||||
if (_jobs.MindTryGetJobName(brotherMindId) is { } jobName)
|
||||
return jobName;
|
||||
|
||||
return Loc.GetString("bloodbrother-unknown-job");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Content.Server.Codewords;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.Random;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(BloodBrotherRuleSystem))]
|
||||
public sealed partial class BloodBrotherRuleComponent : Component
|
||||
{
|
||||
public readonly List<EntityUid> BloodBrotherMinds = new();
|
||||
public readonly Dictionary<EntityUid, EntityUid> BloodBrotherPairs = new();
|
||||
|
||||
[DataField]
|
||||
public EntProtoId BloodBrotherPrototypeId = "MindRoleBloodBrother";
|
||||
|
||||
[DataField]
|
||||
public ProtoId<CodewordFactionPrototype> CodewordFactionPrototypeId = "Traitor";
|
||||
|
||||
[DataField]
|
||||
public ProtoId<NpcFactionPrototype> NanoTrasenFaction = "NanoTrasen";
|
||||
|
||||
[DataField]
|
||||
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
|
||||
|
||||
[DataField]
|
||||
public ProtoId<LocalizedDatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
|
||||
|
||||
/// <summary>
|
||||
/// Give blood brothers a briefing in chat.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool GiveBriefing = true;
|
||||
|
||||
public int TotalBloodBrothers => BloodBrotherMinds.Count;
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Whether both brothers must survive for individual objectives to count
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequireBothAlive = true;
|
||||
|
||||
/// <summary>
|
||||
/// A list of required goals for all pairs of brothers
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntProtoId> RequiredObjectives = new();
|
||||
|
||||
/// <summary>
|
||||
/// A list of goal sets for the brothers
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<BloodBrotherObjectiveSet> ObjectiveSets = new();
|
||||
|
||||
/// <summary>
|
||||
/// Maximum difficulty of goals for a pair of brothers
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxDifficulty = 10f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A set of goals for blood brothers
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed partial class BloodBrotherObjectiveSet
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public ProtoId<WeightedRandomPrototype> Groups;
|
||||
|
||||
[DataField]
|
||||
public float Prob = 1.0f;
|
||||
|
||||
[DataField]
|
||||
public int MaxPicks = 2;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Stores data for <see cref="VampireRuleSystem"/>.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(VampireRuleSystem))]
|
||||
public sealed partial class VampireRuleComponent : Component
|
||||
{
|
||||
}
|
||||
179
Content.Server/_Wega/GameTicking/Rules/VampireRuleSystem.cs
Normal file
179
Content.Server/_Wega/GameTicking/Rules/VampireRuleSystem.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Actions;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Rotting;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chemistry;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Clumsy;
|
||||
using Content.Shared.CombatMode.Pacification;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Temperature.Components;
|
||||
using Content.Shared.Vampire.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules
|
||||
{
|
||||
public sealed class VampireRuleSystem : GameRuleSystem<VampireRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly BodySystem _body = default!;
|
||||
[Dependency] private readonly MetabolizerSystem _metabolism = default!;
|
||||
[Dependency] private readonly ActionsSystem _actions = default!;
|
||||
[Dependency] private readonly DamageableSystem _damage = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<VampireRuleComponent, AfterAntagEntitySelectedEvent>(OnVampireSelected);
|
||||
SubscribeLocalEvent<VampireRoleComponent, GetBriefingEvent>(OnVampireBriefing);
|
||||
}
|
||||
|
||||
private void OnVampireSelected(Entity<VampireRuleComponent> mindId, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
var ent = args.EntityUid;
|
||||
MakeVampire(ent);
|
||||
_antag.SendBriefing(ent, MakeBriefing(ent), Color.Purple, null);
|
||||
}
|
||||
|
||||
private void OnVampireBriefing(Entity<VampireRoleComponent> vampire, ref GetBriefingEvent args)
|
||||
{
|
||||
var ent = args.Mind.Comp.OwnedEntity;
|
||||
if (ent is null)
|
||||
return;
|
||||
|
||||
args.Append(MakeBriefing(ent.Value));
|
||||
}
|
||||
|
||||
private string MakeBriefing(EntityUid ent)
|
||||
{
|
||||
var isHuman = HasComp<HumanoidAppearanceComponent>(ent);
|
||||
var briefing = isHuman
|
||||
? Loc.GetString("vampire-role-greeting-human")
|
||||
: Loc.GetString("vampire-role-greeting-animal");
|
||||
|
||||
return briefing;
|
||||
}
|
||||
|
||||
protected override void AppendRoundEndText(EntityUid uid,
|
||||
VampireRuleComponent component,
|
||||
GameRuleComponent gameRule,
|
||||
ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
var totalBloodDrank = GetTotalBloodDrankInRound();
|
||||
args.AddLine(Loc.GetString("vampires-drank-total-blood", ("bloodAmount", totalBloodDrank)));
|
||||
}
|
||||
|
||||
private float GetTotalBloodDrankInRound()
|
||||
{
|
||||
var totalBloodDrank = 0f;
|
||||
foreach (var vampireEntity in EntityManager.EntityQuery<VampireComponent>(true))
|
||||
{
|
||||
totalBloodDrank += vampireEntity.TotalBloodDrank;
|
||||
}
|
||||
|
||||
return totalBloodDrank;
|
||||
}
|
||||
|
||||
private void MakeVampire(EntityUid vampire)
|
||||
{
|
||||
var vampireComponent = EnsureComp<VampireComponent>(vampire);
|
||||
|
||||
RemoveUnnecessaryComponents(vampire);
|
||||
HandleMetabolismAndOrgans(vampire);
|
||||
SetVampireComponents(vampire, vampireComponent);
|
||||
UpdateAppearance(vampire);
|
||||
AddVampireActions(vampire);
|
||||
}
|
||||
|
||||
private void RemoveUnnecessaryComponents(EntityUid vampire)
|
||||
{
|
||||
var componentsToRemove = new[]
|
||||
{
|
||||
typeof(PacifiedComponent),
|
||||
typeof(PerishableComponent),
|
||||
typeof(BarotraumaComponent),
|
||||
typeof(TemperatureSpeedComponent),
|
||||
typeof(ThirstComponent),
|
||||
typeof(ClumsyComponent)
|
||||
};
|
||||
|
||||
foreach (var compType in componentsToRemove)
|
||||
{
|
||||
if (HasComp(vampire, compType))
|
||||
RemComp(vampire, compType);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleMetabolismAndOrgans(EntityUid vampire)
|
||||
{
|
||||
if (TryComp<BodyComponent>(vampire, out var bodyComponent))
|
||||
{
|
||||
foreach (var organ in _body.GetBodyOrgans(vampire, bodyComponent))
|
||||
{
|
||||
if (TryComp<MetabolizerComponent>(organ.Id, out var metabolizer))
|
||||
{
|
||||
if (TryComp<StomachComponent>(organ.Id, out _))
|
||||
_metabolism.ClearMetabolizerTypes(metabolizer);
|
||||
|
||||
_metabolism.TryAddMetabolizerType(metabolizer, VampireComponent.MetabolizerVampire);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetVampireComponents(EntityUid vampire, VampireComponent _)
|
||||
{
|
||||
if (TryComp<TemperatureComponent>(vampire, out var temperatureComponent))
|
||||
temperatureComponent.ColdDamageThreshold = Atmospherics.TCMB;
|
||||
|
||||
EnsureComp<UnholyComponent>(vampire);
|
||||
EnsureComp<VampireComponent>(vampire);
|
||||
|
||||
_damage.SetDamageModifierSetId(vampire, "Vampire");
|
||||
|
||||
if (TryComp<ReactiveComponent>(vampire, out var reactive))
|
||||
{
|
||||
reactive.ReactiveGroups ??= new();
|
||||
|
||||
if (!reactive.ReactiveGroups.ContainsKey("Unholy"))
|
||||
{
|
||||
reactive.ReactiveGroups.Add("Unholy", new() { ReactionMethod.Touch });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EntityUid vampire)
|
||||
{
|
||||
if (TryComp<HumanoidAppearanceComponent>(vampire, out var appearanceComponent))
|
||||
{
|
||||
appearanceComponent.EyeColor = Color.FromHex("#E22218FF");
|
||||
Dirty(vampire, appearanceComponent);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddVampireActions(EntityUid vampire)
|
||||
{
|
||||
var actionPrototypes = new[]
|
||||
{
|
||||
VampireComponent.DrinkActionPrototype,
|
||||
VampireComponent.SelectClassActionPrototype,
|
||||
VampireComponent.RejuvenateActionPrototype,
|
||||
VampireComponent.GlareActionPrototype
|
||||
};
|
||||
|
||||
foreach (var actionPrototype in actionPrototypes)
|
||||
{
|
||||
_actions.AddAction(vampire, actionPrototype);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Content.Server/_Wega/Ghost/GhostRespawnSystem.cs
Normal file
76
Content.Server/_Wega/Ghost/GhostRespawnSystem.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Shared.Wega.Ghost.Respawn;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Wega.Ghost.Respawn;
|
||||
|
||||
public sealed class GhostRespawnSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private readonly Dictionary<ICommonSession, TimeSpan> _respawnResetTimes = [];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<MindContainerComponent, MindRemovedMessage>(OnMindRemoved);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
||||
_player.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(MobStateChangedEvent e)
|
||||
{
|
||||
if (e.NewMobState != MobState.Dead)
|
||||
return;
|
||||
if (!_player.TryGetSessionByEntity(e.Target, out var session))
|
||||
return;
|
||||
ResetRespawnTime(e.Target, session);
|
||||
}
|
||||
|
||||
private void OnMindRemoved(EntityUid entity, MindContainerComponent component, MindRemovedMessage e)
|
||||
{
|
||||
if (e.Mind.Comp.UserId is null)
|
||||
return;
|
||||
if (TryComp<MobStateComponent>(entity, out var state) && state.CurrentState == MobState.Dead)
|
||||
return;
|
||||
if (!_player.TryGetSessionById(e.Mind.Comp.UserId.Value, out var session))
|
||||
return;
|
||||
|
||||
ResetRespawnTime(entity, session);
|
||||
}
|
||||
|
||||
private void OnRoundRestartCleanup(RoundRestartCleanupEvent e)
|
||||
{
|
||||
_respawnResetTimes.Clear();
|
||||
}
|
||||
|
||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus == Robust.Shared.Enums.SessionStatus.Connected)
|
||||
SendRespawnResetTime(e.Session, GetRespawnResetTime(e.Session));
|
||||
}
|
||||
|
||||
private void ResetRespawnTime(EntityUid entity, ICommonSession session)
|
||||
{
|
||||
ref var respawnTime = ref CollectionsMarshal.GetValueRefOrAddDefault(_respawnResetTimes, session, out _);
|
||||
respawnTime = _timing.CurTime;
|
||||
SendRespawnResetTime(session, _timing.CurTime);
|
||||
}
|
||||
|
||||
private void SendRespawnResetTime(ICommonSession session, TimeSpan? time)
|
||||
{
|
||||
RaiseNetworkEvent(new GhostRespawnEvent(time), session);
|
||||
}
|
||||
|
||||
public TimeSpan? GetRespawnResetTime(ICommonSession session)
|
||||
{
|
||||
return _respawnResetTimes.TryGetValue(session, out var time) ? time : null;
|
||||
}
|
||||
}
|
||||
138
Content.Server/_Wega/Hallucinations/HallucinationsSystem.cs
Normal file
138
Content.Server/_Wega/Hallucinations/HallucinationsSystem.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Hallucinations;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Hallucinations;
|
||||
|
||||
// TODO: Full refactor this shit
|
||||
public sealed partial class HallucinationsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedEyeSystem _eye = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _status = default!;
|
||||
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HallucinationsComponent, MapInitEvent>(OnHallucinationsInit);
|
||||
SubscribeLocalEvent<HallucinationsComponent, ComponentShutdown>(OnHallucinationsShutdown);
|
||||
}
|
||||
|
||||
private void OnHallucinationsInit(EntityUid uid, HallucinationsComponent component, MapInitEvent args)
|
||||
{
|
||||
component.Layer = _random.Next(100, 150);
|
||||
if (!_entityManager.TryGetComponent<EyeComponent>(uid, out var eye))
|
||||
return;
|
||||
UpdatePreset(component);
|
||||
_eye.SetVisibilityMask(uid, eye.VisibilityMask | component.Layer, eye);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium,
|
||||
$"{ToPrettyString(uid):player} began to hallucinate.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates hallucinations component settings to match prototype
|
||||
/// </summary>
|
||||
/// <param name="component">Active HallucinationsComponent</param>
|
||||
public void UpdatePreset(HallucinationsComponent component)
|
||||
{
|
||||
if (component.Proto == null)
|
||||
return;
|
||||
var preset = component.Proto;
|
||||
|
||||
component.Spawns = preset.Entities;
|
||||
component.Range = preset.Range;
|
||||
component.SpawnRate = preset.SpawnRate;
|
||||
component.MinChance = preset.MinChance;
|
||||
component.MaxChance = preset.MaxChance;
|
||||
component.MaxSpawns = preset.MaxSpawns;
|
||||
component.IncreaseChance = preset.IncreaseChance;
|
||||
}
|
||||
|
||||
private void OnHallucinationsShutdown(EntityUid uid, HallucinationsComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent<EyeComponent>(uid, out var eye))
|
||||
return;
|
||||
_eye.SetVisibilityMask(uid, eye.VisibilityMask & ~component.Layer, eye);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium,
|
||||
$"{ToPrettyString(uid):player} stopped hallucinating.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to start hallucinations for target
|
||||
/// </summary>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="key">Status effect key.</param>
|
||||
/// <param name="time">Duration of hallucinations effect.</param>
|
||||
/// <param name="refresh">Refresh active effects.</param>
|
||||
/// <param name="proto">Hallucinations pack prototype.</param>
|
||||
public bool StartHallucinations(EntityUid target, string key, TimeSpan time, bool refresh, string proto)
|
||||
{
|
||||
if (proto == null)
|
||||
return false;
|
||||
if (!_proto.TryIndex<HallucinationsPrototype>(proto, out var prototype))
|
||||
return false;
|
||||
if (!_status.TryAddStatusEffect<HallucinationsComponent>(target, key, time, refresh))
|
||||
return false;
|
||||
|
||||
var hallucinations = _entityManager.GetComponent<HallucinationsComponent>(target);
|
||||
hallucinations.Proto = prototype;
|
||||
UpdatePreset(hallucinations);
|
||||
hallucinations.CurChance = prototype.MinChance;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<HallucinationsComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out var stat, out var xform))
|
||||
{
|
||||
if (_timing.CurTime < stat.NextSecond)
|
||||
continue;
|
||||
var rate = stat.SpawnRate;
|
||||
stat.NextSecond = _timing.CurTime + TimeSpan.FromSeconds(rate);
|
||||
|
||||
if (stat.CurChance < stat.MaxChance && stat.CurChance + stat.IncreaseChance <= 1)
|
||||
stat.CurChance = stat.CurChance + stat.IncreaseChance;
|
||||
|
||||
if (!_random.Prob(stat.CurChance))
|
||||
continue;
|
||||
|
||||
var selectedEntity = _random.Pick(stat.Spawns);
|
||||
int spawnCount = _random.Next(3, 6);
|
||||
stat.SpawnedCount = 0;
|
||||
|
||||
var range = stat.Range * 4;
|
||||
|
||||
UpdatePreset(stat);
|
||||
|
||||
for (int i = 0; i < spawnCount; i++)
|
||||
{
|
||||
var newCoords = Transform(uid).MapPosition.Offset(_random.NextVector2(stat.Range));
|
||||
|
||||
if (stat.SpawnedCount >= stat.MaxSpawns)
|
||||
continue;
|
||||
|
||||
stat.SpawnedCount++;
|
||||
|
||||
var hallucination = Spawn(selectedEntity, newCoords);
|
||||
EnsureComp<VisibilityComponent>(hallucination, out var visibility);
|
||||
_visibilitySystem.SetLayer(hallucination, (ushort)stat.Layer, false);
|
||||
_visibilitySystem.RefreshVisibility(hallucination, visibilityComponent: visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Content.Server/_Wega/Interaction/DeleteOnDropSystem.cs
Normal file
45
Content.Server/_Wega/Interaction/DeleteOnDropSystem.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory.Events;
|
||||
|
||||
namespace Content.Server.Interaction;
|
||||
|
||||
public sealed class DeleteOnDropSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DeleteOnDropComponent, GotUnequippedEvent>(OnUnequip);
|
||||
SubscribeLocalEvent<DeleteOnDropComponent, GotUnequippedHandEvent>(OnUnequipHand);
|
||||
SubscribeLocalEvent<DeleteOnDropComponent, DroppedEvent>(OnDropped);
|
||||
}
|
||||
|
||||
private void OnUnequip(EntityUid uid, DeleteOnDropComponent item, GotUnequippedEvent args)
|
||||
{
|
||||
if (!item.DeleteOnDrop || !_entityManager.EntityExists(uid))
|
||||
return;
|
||||
|
||||
QueueDel(uid);
|
||||
}
|
||||
|
||||
private void OnUnequipHand(EntityUid uid, DeleteOnDropComponent item, GotUnequippedHandEvent args)
|
||||
{
|
||||
if (!item.DeleteOnDrop || !_entityManager.EntityExists(uid))
|
||||
return;
|
||||
|
||||
QueueDel(uid);
|
||||
}
|
||||
|
||||
private void OnDropped(EntityUid uid, DeleteOnDropComponent item, DroppedEvent args)
|
||||
{
|
||||
if (!item.DeleteOnDrop || !_entityManager.EntityExists(uid))
|
||||
return;
|
||||
|
||||
QueueDel(uid);
|
||||
}
|
||||
}
|
||||
|
||||
51
Content.Server/_Wega/NullRod/NullRodSystem.cs
Normal file
51
Content.Server/_Wega/NullRod/NullRodSystem.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Content.Server.Bible.Components;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.NullRod.Components;
|
||||
|
||||
namespace Content.Server.NullRod;
|
||||
|
||||
public sealed class NullRodSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<NullRodComponent, GotEquippedEvent>(OnDidEquip);
|
||||
SubscribeLocalEvent<NullRodComponent, GotEquippedHandEvent>(OnHandEquipped);
|
||||
SubscribeLocalEvent<NullRodComponent, GotUnequippedEvent>(OnDidUnequip);
|
||||
SubscribeLocalEvent<NullRodComponent, GotUnequippedHandEvent>(OnHandUnequipped);
|
||||
}
|
||||
|
||||
private void OnDidEquip(Entity<NullRodComponent> ent, ref GotEquippedEvent args)
|
||||
{
|
||||
if (!HasComp<BibleUserComponent>(args.Equipee) || HasComp<NullRodOwnerComponent>(args.Equipee))
|
||||
return;
|
||||
|
||||
EnsureComp<NullRodOwnerComponent>(args.Equipee);
|
||||
}
|
||||
|
||||
private void OnHandEquipped(Entity<NullRodComponent> ent, ref GotEquippedHandEvent args)
|
||||
{
|
||||
if (!HasComp<BibleUserComponent>(args.User) || HasComp<NullRodOwnerComponent>(args.User))
|
||||
return;
|
||||
|
||||
EnsureComp<NullRodOwnerComponent>(args.User);
|
||||
}
|
||||
|
||||
private void OnDidUnequip(Entity<NullRodComponent> ent, ref GotUnequippedEvent args)
|
||||
{
|
||||
if (!HasComp<NullRodOwnerComponent>(args.Equipee))
|
||||
return;
|
||||
|
||||
RemComp<NullRodOwnerComponent>(args.Equipee);
|
||||
}
|
||||
|
||||
private void OnHandUnequipped(Entity<NullRodComponent> ent, ref GotUnequippedHandEvent args)
|
||||
{
|
||||
if (!HasComp<NullRodOwnerComponent>(args.User))
|
||||
return;
|
||||
|
||||
RemComp<NullRodOwnerComponent>(args.User);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A basic component for the common goals of blood brothers
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(BloodBrotherSharedConditionSystem))]
|
||||
public sealed partial class BloodBrotherSharedConditionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// ID brother mind
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? BrotherMind;
|
||||
|
||||
/// <summary>
|
||||
/// Do you need both brothers to be alive to achieve your goals?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequireBothAlive = true;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A component for the common escape goals of blood brothers
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(BloodBrotherSharedEscapeConditionSystem))]
|
||||
public sealed partial class BloodBrotherSharedEscapeConditionComponent : Component;
|
||||
@@ -0,0 +1,11 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Objective condition that requires both blood brothers to hijack the shuttle together
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(BloodBrotherSharedHijackConditionSystem))]
|
||||
public sealed partial class BloodBrotherSharedHijackConditionComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A component for the common survival goals of blood brothers
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(BloodBrotherSharedKeepAliveConditionSystem))]
|
||||
public sealed partial class BloodBrotherSharedKeepAliveConditionComponent : Component;
|
||||
@@ -0,0 +1,22 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A component for the common purpose of killing blood brothers
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(BloodBrotherSharedKillConditionSystem))]
|
||||
public sealed partial class BloodBrotherSharedKillConditionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the target must be dead
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequireDead = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the target must not escape
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequireMaroon = true;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Content.Server.Objectives.Systems;
|
||||
using Content.Shared.Objectives;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Objectives.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A component for the common purpose of stealing blood brothers
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(BloodBrotherSharedStealConditionSystem))]
|
||||
public sealed partial class BloodBrotherSharedStealConditionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// A group of items to be stolen
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<StealTargetGroupPrototype> StealGroup;
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, disables generation of this target if there is no entity on the map
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool VerifyMapExistence = true;
|
||||
|
||||
/// <summary>
|
||||
/// If true, counts objects that are close to steal areas.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool CheckStealAreas = false;
|
||||
|
||||
/// <summary>
|
||||
/// If the target may be alive but has died, it will not be counted
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool CheckAlive = false;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum number of items you need to steal to fulfill a objective
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MinCollectionSize = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of items you need to steal to fulfill a objective
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxCollectionSize = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Target collection size after calculation
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int CollectionSize;
|
||||
|
||||
/// <summary>
|
||||
/// Help newer players by saying e.g. "steal the chief engineer's advanced magboots"
|
||||
/// </summary>
|
||||
[DataField("owner")]
|
||||
public string? OwnerText;
|
||||
|
||||
[DataField(required: true)]
|
||||
public LocId ObjectiveText;
|
||||
|
||||
[DataField(required: true)]
|
||||
public LocId ObjectiveNoOwnerText;
|
||||
|
||||
[DataField(required: true)]
|
||||
public LocId DescriptionText;
|
||||
|
||||
[DataField(required: true)]
|
||||
public LocId DescriptionMultiplyText;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Shared.Mind;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
public sealed class BloodBrotherSharedConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
public bool CheckBaseConditions(EntityUid mindId, BloodBrotherSharedConditionComponent comp, MindComponent? mind = null)
|
||||
{
|
||||
if (!Resolve(mindId, ref mind))
|
||||
return false;
|
||||
|
||||
if (comp.RequireBothAlive && _mind.IsCharacterDeadIc(mind))
|
||||
return false;
|
||||
|
||||
if (comp.RequireBothAlive && comp.BrotherMind.HasValue)
|
||||
{
|
||||
if (!TryComp<MindComponent>(comp.BrotherMind.Value, out var brotherMind) ||
|
||||
_mind.IsCharacterDeadIc(brotherMind))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetSharedCondition(EntityUid objectiveUid, EntityUid mindId, [NotNullWhen(true)] out BloodBrotherSharedConditionComponent? sharedCondition)
|
||||
{
|
||||
sharedCondition = null;
|
||||
return TryComp(objectiveUid, out sharedCondition);
|
||||
}
|
||||
|
||||
public void CopySharedConditionData(EntityUid sourceObjective, EntityUid targetObjective, EntityUid mindId1, EntityUid mindId2)
|
||||
{
|
||||
if (TryComp<BloodBrotherSharedConditionComponent>(sourceObjective, out var sourceCondition)
|
||||
&& TryComp<BloodBrotherSharedConditionComponent>(targetObjective, out var targetCondition))
|
||||
{
|
||||
targetCondition.BrotherMind = mindId1;
|
||||
targetCondition.RequireBothAlive = sourceCondition.RequireBothAlive;
|
||||
|
||||
sourceCondition.BrotherMind = mindId2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
public sealed class BloodBrotherSharedEscapeConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<BloodBrotherSharedEscapeConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||
}
|
||||
|
||||
private void OnGetProgress(EntityUid uid, BloodBrotherSharedEscapeConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
||||
{
|
||||
args.Progress = GetProgress(uid, args.MindId, args.Mind);
|
||||
}
|
||||
|
||||
private float GetProgress(EntityUid objectiveUid, EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
|
||||
&& !_sharedCondition.CheckBaseConditions(mindId, sharedCondition, mind))
|
||||
return 0f;
|
||||
|
||||
var currentEscape = CheckEscape(mindId, mind);
|
||||
|
||||
var brotherEscape = 0f;
|
||||
if (sharedCondition?.BrotherMind != null &&
|
||||
TryComp<MindComponent>(sharedCondition.BrotherMind.Value, out var brotherMind))
|
||||
{
|
||||
brotherEscape = CheckEscape(sharedCondition.BrotherMind.Value, brotherMind);
|
||||
}
|
||||
|
||||
return Math.Min(currentEscape, brotherEscape);
|
||||
}
|
||||
|
||||
private float CheckEscape(EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (mind.OwnedEntity == null || _mind.IsCharacterDeadIc(mind))
|
||||
return 0f;
|
||||
|
||||
if (TryComp<CuffableComponent>(mind.OwnedEntity, out var cuffed) && cuffed.CuffedHandCount > 0)
|
||||
return _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) ? 0.5f : 0f;
|
||||
|
||||
return _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) ? 1f : 0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
public sealed class BloodBrotherSharedHijackConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<BloodBrotherSharedHijackConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||
}
|
||||
|
||||
private void OnGetProgress(EntityUid uid, BloodBrotherSharedHijackConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
||||
{
|
||||
args.Progress = GetProgress(uid, args.MindId, args.Mind);
|
||||
}
|
||||
|
||||
private float GetProgress(EntityUid objectiveUid, EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (!CheckBaseHijackConditions(objectiveUid, mindId, mind))
|
||||
return 0f;
|
||||
|
||||
if (!_emergencyShuttle.EmergencyShuttleArrived)
|
||||
return 0f;
|
||||
|
||||
foreach (var stationData in EntityQuery<StationEmergencyShuttleComponent>())
|
||||
{
|
||||
if (stationData.EmergencyShuttle == null)
|
||||
continue;
|
||||
|
||||
if (IsShuttleHijackedByBloodBrothers(stationData.EmergencyShuttle.Value, objectiveUid, mindId))
|
||||
return 1f;
|
||||
}
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private bool CheckBaseHijackConditions(EntityUid objectiveUid, EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (mind.OwnedEntity == null || TryComp<CuffableComponent>(mind.OwnedEntity, out var cuffed) && cuffed.CuffedHandCount > 0)
|
||||
return false;
|
||||
|
||||
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
|
||||
&& !_sharedCondition.CheckBaseConditions(mindId, sharedCondition, mind))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsShuttleHijackedByBloodBrothers(EntityUid shuttleGridId, EntityUid objectiveUid, EntityUid mindId)
|
||||
{
|
||||
var gridPlayers = Filter.BroadcastGrid(shuttleGridId).Recipients;
|
||||
var humanoids = GetEntityQuery<HumanoidAppearanceComponent>();
|
||||
var cuffable = GetEntityQuery<CuffableComponent>();
|
||||
EntityQuery<MobStateComponent>();
|
||||
|
||||
var firstBrotherOnShuttle = false;
|
||||
var secondBrotherOnShuttle = false;
|
||||
EntityUid? brotherMindId = null;
|
||||
|
||||
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
|
||||
&& sharedCondition.BrotherMind.HasValue)
|
||||
{
|
||||
brotherMindId = sharedCondition.BrotherMind.Value;
|
||||
}
|
||||
|
||||
foreach (var player in gridPlayers)
|
||||
{
|
||||
if (player.AttachedEntity == null ||
|
||||
!_mind.TryGetMind(player.AttachedEntity.Value, out var crewMindId, out _))
|
||||
continue;
|
||||
|
||||
if (mindId == crewMindId)
|
||||
{
|
||||
firstBrotherOnShuttle = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (brotherMindId.HasValue && brotherMindId.Value == crewMindId)
|
||||
{
|
||||
secondBrotherOnShuttle = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var isHumanoid = humanoids.HasComponent(player.AttachedEntity.Value);
|
||||
if (!isHumanoid)
|
||||
continue;
|
||||
|
||||
var isAntagonist = _role.MindIsAntagonist(crewMindId);
|
||||
if (isAntagonist)
|
||||
continue;
|
||||
|
||||
var isPersonIncapacitated = _mobState.IsIncapacitated(player.AttachedEntity.Value);
|
||||
if (isPersonIncapacitated)
|
||||
continue;
|
||||
|
||||
var isPersonCuffed =
|
||||
cuffable.TryGetComponent(player.AttachedEntity.Value, out var cuffed)
|
||||
&& cuffed.CuffedHandCount > 0;
|
||||
if (isPersonCuffed)
|
||||
continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return firstBrotherOnShuttle && secondBrotherOnShuttle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
public sealed class BloodBrotherSharedKeepAliveConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<BloodBrotherSharedKeepAliveConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||
}
|
||||
|
||||
private void OnGetProgress(EntityUid uid, BloodBrotherSharedKeepAliveConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
||||
{
|
||||
args.Progress = GetProgress(uid, args.MindId, args.Mind);
|
||||
}
|
||||
|
||||
private float GetProgress(EntityUid objectiveUid, EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
|
||||
&& !_sharedCondition.CheckBaseConditions(mindId, sharedCondition, mind))
|
||||
return 0f;
|
||||
|
||||
if (!_target.GetTarget(objectiveUid, out var target))
|
||||
return 0f;
|
||||
|
||||
return CalculateProtectProgress(target.Value);
|
||||
}
|
||||
|
||||
private float CalculateProtectProgress(EntityUid target)
|
||||
{
|
||||
if (!TryComp<MindComponent>(target, out var mind))
|
||||
return 0f;
|
||||
|
||||
return _mind.IsCharacterDeadIc(mind) ? 0f : 1f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
public sealed class BloodBrotherSharedKillConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly TargetObjectiveSystem _target = default!;
|
||||
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<BloodBrotherSharedKillConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||
}
|
||||
|
||||
private void OnGetProgress(EntityUid uid, BloodBrotherSharedKillConditionComponent comp, ref ObjectiveGetProgressEvent args)
|
||||
{
|
||||
if (!_target.GetTarget(uid, out var target))
|
||||
return;
|
||||
|
||||
args.Progress = GetProgress(uid, target.Value, comp.RequireDead, comp.RequireMaroon, args.MindId, args.Mind);
|
||||
}
|
||||
|
||||
private float GetProgress(EntityUid objectiveUid, EntityUid target, bool requireDead, bool requireMaroon, EntityUid mindId, MindComponent mind)
|
||||
{
|
||||
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mindId, out var sharedCondition)
|
||||
&& !_sharedCondition.CheckBaseConditions(mindId, sharedCondition, mind))
|
||||
return 0f;
|
||||
|
||||
return CalculateKillProgress(target, requireDead, requireMaroon);
|
||||
}
|
||||
|
||||
private float CalculateKillProgress(EntityUid target, bool requireDead, bool requireMaroon)
|
||||
{
|
||||
if (!TryComp<MindComponent>(target, out var mind) || mind.OwnedEntity == null)
|
||||
return 1f;
|
||||
|
||||
var targetDead = _mind.IsCharacterDeadIc(mind);
|
||||
var targetMarooned = !_emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) || _mind.IsCharacterUnrevivableIc(mind);
|
||||
|
||||
if (!_config.GetCVar(CCVars.EmergencyShuttleEnabled) && requireMaroon)
|
||||
{
|
||||
requireDead = true;
|
||||
requireMaroon = false;
|
||||
}
|
||||
|
||||
if (requireDead && !targetDead)
|
||||
return 0f;
|
||||
|
||||
if (requireMaroon && !_emergencyShuttle.EmergencyShuttleArrived)
|
||||
return 0f;
|
||||
|
||||
if (requireMaroon && !_emergencyShuttle.ShuttlesLeft)
|
||||
return targetMarooned ? 0.5f : 0f;
|
||||
|
||||
if (requireMaroon && _emergencyShuttle.ShuttlesLeft)
|
||||
return targetMarooned ? 1f : 0f;
|
||||
|
||||
return 1f;
|
||||
}
|
||||
|
||||
public void CopySharedKillConditionData(EntityUid sourceObjective, EntityUid targetObjective)
|
||||
{
|
||||
if (TryComp<BloodBrotherSharedKillConditionComponent>(sourceObjective, out var sourceCondition)
|
||||
&& TryComp<BloodBrotherSharedKillConditionComponent>(targetObjective, out var targetCondition))
|
||||
{
|
||||
targetCondition.RequireDead = sourceCondition.RequireDead;
|
||||
targetCondition.RequireMaroon = sourceCondition.RequireMaroon;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.Objectives.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
|
||||
namespace Content.Server.Objectives.Systems;
|
||||
|
||||
public sealed class BloodBrotherSharedStealConditionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly BloodBrotherSharedConditionSystem _sharedCondition = default!;
|
||||
|
||||
private EntityQuery<ContainerManagerComponent> _containerQuery;
|
||||
private HashSet<Entity<TransformComponent>> _nearestEnts = new();
|
||||
private HashSet<EntityUid> _countedItems = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_containerQuery = GetEntityQuery<ContainerManagerComponent>();
|
||||
|
||||
SubscribeLocalEvent<BloodBrotherSharedStealConditionComponent, ObjectiveAssignedEvent>(OnAssigned);
|
||||
SubscribeLocalEvent<BloodBrotherSharedStealConditionComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
|
||||
SubscribeLocalEvent<BloodBrotherSharedStealConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
|
||||
}
|
||||
|
||||
private void OnAssigned(Entity<BloodBrotherSharedStealConditionComponent> condition, ref ObjectiveAssignedEvent args)
|
||||
{
|
||||
List<StealTargetComponent?> targetList = new();
|
||||
|
||||
var query = AllEntityQuery<StealTargetComponent>();
|
||||
while (query.MoveNext(out var target))
|
||||
{
|
||||
if (condition.Comp.StealGroup != target.StealGroup)
|
||||
continue;
|
||||
|
||||
targetList.Add(target);
|
||||
}
|
||||
|
||||
if (targetList.Count == 0 && condition.Comp.VerifyMapExistence)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxSize = condition.Comp.VerifyMapExistence
|
||||
? Math.Min(targetList.Count, condition.Comp.MaxCollectionSize)
|
||||
: condition.Comp.MaxCollectionSize;
|
||||
var minSize = condition.Comp.VerifyMapExistence
|
||||
? Math.Min(targetList.Count, condition.Comp.MinCollectionSize)
|
||||
: condition.Comp.MinCollectionSize;
|
||||
|
||||
condition.Comp.CollectionSize = _random.Next(minSize, maxSize);
|
||||
}
|
||||
|
||||
private void OnAfterAssign(Entity<BloodBrotherSharedStealConditionComponent> condition, ref ObjectiveAfterAssignEvent args)
|
||||
{
|
||||
var group = _proto.Index(condition.Comp.StealGroup);
|
||||
string localizedName = Loc.GetString(group.Name);
|
||||
|
||||
var title = condition.Comp.OwnerText == null
|
||||
? Loc.GetString(condition.Comp.ObjectiveNoOwnerText, ("itemName", localizedName))
|
||||
: Loc.GetString(condition.Comp.ObjectiveText, ("owner", Loc.GetString(condition.Comp.OwnerText)), ("itemName", localizedName));
|
||||
|
||||
var description = condition.Comp.CollectionSize > 1
|
||||
? Loc.GetString(condition.Comp.DescriptionMultiplyText, ("itemName", localizedName), ("count", condition.Comp.CollectionSize))
|
||||
: Loc.GetString(condition.Comp.DescriptionText, ("itemName", localizedName));
|
||||
|
||||
_metaData.SetEntityName(condition.Owner, title, args.Meta);
|
||||
_metaData.SetEntityDescription(condition.Owner, description, args.Meta);
|
||||
_objectives.SetIcon(condition.Owner, group.Sprite, args.Objective);
|
||||
}
|
||||
|
||||
private void OnGetProgress(Entity<BloodBrotherSharedStealConditionComponent> condition, ref ObjectiveGetProgressEvent args)
|
||||
{
|
||||
args.Progress = GetProgress(condition.Owner, (args.MindId, args.Mind), condition);
|
||||
}
|
||||
|
||||
private float GetProgress(EntityUid objectiveUid, Entity<MindComponent> mind, BloodBrotherSharedStealConditionComponent condition)
|
||||
{
|
||||
if (_sharedCondition.TryGetSharedCondition(objectiveUid, mind.Owner, out var sharedCondition)
|
||||
&& !_sharedCondition.CheckBaseConditions(mind.Owner, sharedCondition, mind.Comp))
|
||||
return 0f;
|
||||
|
||||
var currentCount = CountStolenItems(mind, condition);
|
||||
var brotherCount = 0;
|
||||
|
||||
if (sharedCondition?.BrotherMind != null && TryComp<MindComponent>(sharedCondition.BrotherMind.Value, out var brotherMind))
|
||||
brotherCount = CountStolenItems((sharedCondition.BrotherMind.Value, brotherMind), condition);
|
||||
|
||||
var totalCount = Math.Max(currentCount, brotherCount);
|
||||
var result = totalCount / (float)condition.CollectionSize;
|
||||
return Math.Clamp(result, 0, 1);
|
||||
}
|
||||
|
||||
private int CountStolenItems(Entity<MindComponent> mind, BloodBrotherSharedStealConditionComponent condition)
|
||||
{
|
||||
if (mind.Comp.OwnedEntity == null || !_containerQuery.TryGetComponent(mind.Comp.OwnedEntity.Value, out var currentManager))
|
||||
return 0;
|
||||
|
||||
var containerStack = new Stack<ContainerManagerComponent>();
|
||||
var count = 0;
|
||||
|
||||
_countedItems.Clear();
|
||||
|
||||
if (condition.CheckStealAreas)
|
||||
{
|
||||
var areasQuery = AllEntityQuery<StealAreaComponent, TransformComponent>();
|
||||
while (areasQuery.MoveNext(out var uid, out var area, out var xform))
|
||||
{
|
||||
if (!IsOwnerOfStealArea(uid, mind.Owner, area))
|
||||
continue;
|
||||
|
||||
_nearestEnts.Clear();
|
||||
_lookup.GetEntitiesInRange(xform.Coordinates, area.Range, _nearestEnts);
|
||||
foreach (var ent in _nearestEnts)
|
||||
{
|
||||
if (!_interaction.InRangeUnobstructed((uid, xform), (ent, ent.Comp), range: area.Range))
|
||||
continue;
|
||||
|
||||
CheckEntity(ent, condition, ref containerStack, ref count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TryComp<PullerComponent>(mind.Comp.OwnedEntity, out var pull))
|
||||
{
|
||||
var pulledEntity = pull.Pulling;
|
||||
if (pulledEntity != null)
|
||||
{
|
||||
CheckEntity(pulledEntity.Value, condition, ref containerStack, ref count);
|
||||
}
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
foreach (var container in currentManager.Containers.Values)
|
||||
{
|
||||
foreach (var entity in container.ContainedEntities)
|
||||
{
|
||||
count += CheckStealTarget(entity, condition);
|
||||
if (_containerQuery.TryGetComponent(entity, out var containerManager))
|
||||
containerStack.Push(containerManager);
|
||||
}
|
||||
}
|
||||
} while (containerStack.TryPop(out currentManager));
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private bool IsOwnerOfStealArea(EntityUid areaUid, EntityUid mindId, StealAreaComponent area)
|
||||
{
|
||||
var owners = new HashSet<EntityUid>();
|
||||
foreach (var owner in area.Owners)
|
||||
{
|
||||
owners.Add(owner);
|
||||
}
|
||||
return owners.Contains(mindId);
|
||||
}
|
||||
|
||||
private void CheckEntity(EntityUid entity, BloodBrotherSharedStealConditionComponent condition, ref Stack<ContainerManagerComponent> containerStack, ref int counter)
|
||||
{
|
||||
counter += CheckStealTarget(entity, condition);
|
||||
if (!TryComp<MindContainerComponent>(entity, out var pullMind))
|
||||
{
|
||||
if (_containerQuery.TryGetComponent(entity, out var containerManager))
|
||||
containerStack.Push(containerManager);
|
||||
}
|
||||
}
|
||||
|
||||
private int CheckStealTarget(EntityUid entity, BloodBrotherSharedStealConditionComponent condition)
|
||||
{
|
||||
if (_countedItems.Contains(entity))
|
||||
return 0;
|
||||
|
||||
if (!TryComp<StealTargetComponent>(entity, out var target))
|
||||
return 0;
|
||||
|
||||
if (target.StealGroup != condition.StealGroup)
|
||||
return 0;
|
||||
|
||||
if (TryComp<CartridgeComponent>(entity, out var cartridge) &&
|
||||
cartridge.InstallationStatus is not InstallationStatus.Cartridge)
|
||||
return 0;
|
||||
|
||||
if (condition.CheckAlive)
|
||||
{
|
||||
if (TryComp<MobStateComponent>(entity, out var state))
|
||||
{
|
||||
if (!_mobState.IsAlive(entity, state))
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
_countedItems.Add(entity);
|
||||
return TryComp<StackComponent>(entity, out var stack) ? stack.Count : 1;
|
||||
}
|
||||
|
||||
public void CopySharedStealConditionData(EntityUid sourceObjective, EntityUid targetObjective)
|
||||
{
|
||||
if (TryComp<BloodBrotherSharedStealConditionComponent>(sourceObjective, out var sourceCondition)
|
||||
&& TryComp<BloodBrotherSharedStealConditionComponent>(targetObjective, out var targetCondition))
|
||||
{
|
||||
targetCondition.StealGroup = sourceCondition.StealGroup;
|
||||
targetCondition.VerifyMapExistence = sourceCondition.VerifyMapExistence;
|
||||
targetCondition.CheckStealAreas = sourceCondition.CheckStealAreas;
|
||||
targetCondition.CheckAlive = sourceCondition.CheckAlive;
|
||||
targetCondition.MinCollectionSize = sourceCondition.MinCollectionSize;
|
||||
targetCondition.MaxCollectionSize = sourceCondition.MaxCollectionSize;
|
||||
targetCondition.CollectionSize = sourceCondition.CollectionSize;
|
||||
targetCondition.OwnerText = sourceCondition.OwnerText;
|
||||
targetCondition.ObjectiveText = sourceCondition.ObjectiveText;
|
||||
targetCondition.ObjectiveNoOwnerText = sourceCondition.ObjectiveNoOwnerText;
|
||||
targetCondition.DescriptionText = sourceCondition.DescriptionText;
|
||||
targetCondition.DescriptionMultiplyText = sourceCondition.DescriptionMultiplyText;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Content.Server/_Wega/Roles/BloodBrotherRoleComponent.cs
Normal file
9
Content.Server/_Wega/Roles/BloodBrotherRoleComponent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Content.Shared.Roles.Components;
|
||||
|
||||
namespace Content.Server.Roles;
|
||||
|
||||
/// <summary>
|
||||
/// Added to mind role entities to tag that they are a blood brother.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class BloodBrotherRoleComponent : BaseMindRoleComponent;
|
||||
9
Content.Server/_Wega/Roles/VampireRoleComponent.cs
Normal file
9
Content.Server/_Wega/Roles/VampireRoleComponent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Content.Shared.Roles.Components;
|
||||
|
||||
namespace Content.Server.Roles;
|
||||
|
||||
/// <summary>
|
||||
/// Added to mind role entities to tag that they are a vampire.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class VampireRoleComponent : BaseMindRoleComponent;
|
||||
1539
Content.Server/_Wega/Vampire/VampireSystem.Abilities.cs
Normal file
1539
Content.Server/_Wega/Vampire/VampireSystem.Abilities.cs
Normal file
File diff suppressed because it is too large
Load Diff
598
Content.Server/_Wega/Vampire/VampireSystem.cs
Normal file
598
Content.Server/_Wega/Vampire/VampireSystem.cs
Normal file
@@ -0,0 +1,598 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Rotting;
|
||||
using Content.Server.Bible.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Polymorph.Systems;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Mindshield.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.NullRod.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Vampire;
|
||||
using Content.Shared.Vampire.Components;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared.Genetics;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Surgery.Components;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Damage.Components;
|
||||
|
||||
namespace Content.Server.Vampire;
|
||||
|
||||
public sealed partial class VampireSystem : SharedVampireSystem
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _admin = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _blood = default!;
|
||||
[Dependency] private readonly FlammableSystem _flammable = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly RottingSystem _rotting = default!;
|
||||
[Dependency] private readonly StomachSystem _stomach = default!;
|
||||
[Dependency] private readonly PolymorphSystem _polymorph = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly IMapManager _mapMan = default!;
|
||||
[Dependency] private readonly SharedMapSystem _map = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _action = default!;
|
||||
[Dependency] private readonly SharedBodySystem _body = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
|
||||
[Dependency] private readonly SharedStunSystem _stun = default!;
|
||||
[Dependency] private readonly DamageableSystem _damage = default!;
|
||||
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
|
||||
private static readonly ProtoId<EmotePrototype> Scream = "Scream";
|
||||
|
||||
private readonly Dictionary<EntityUid, Dictionary<EntityUid, FixedPoint2>> _bloodConsumedTracker = new();
|
||||
private bool _isDamageBeingHandled = false;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// Start
|
||||
SubscribeLocalEvent<VampireComponent, ComponentStartup>(OnStartup);
|
||||
|
||||
// Drinking Blood
|
||||
SubscribeLocalEvent<VampireComponent, VampireDrinkingBloodActionEvent>(OnDrinkBlood);
|
||||
SubscribeLocalEvent<VampireComponent, VampireDrinkingBloodDoAfterEvent>(DrinkDoAfter);
|
||||
|
||||
// Distribute Damage
|
||||
SubscribeLocalEvent<VampireComponent, DamageChangedEvent>(OnDamageChanged);
|
||||
SubscribeLocalEvent<ThrallComponent, DamageChangedEvent>(OnDamageChanged);
|
||||
|
||||
// Thralls
|
||||
SubscribeLocalEvent<MindShieldComponent, ComponentStartup>(MindShieldImplanted);
|
||||
|
||||
InitializePowers();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var vampireQuery = EntityQueryEnumerator<VampireComponent>();
|
||||
while (vampireQuery.MoveNext(out var uid, out var vampireComponent))
|
||||
{
|
||||
if (IsInSpace(uid))
|
||||
{
|
||||
if (vampireComponent.NextSpaceDamageTick <= 0)
|
||||
{
|
||||
vampireComponent.NextSpaceDamageTick = 1;
|
||||
DoSpaceDamage((uid, vampireComponent));
|
||||
}
|
||||
vampireComponent.NextSpaceDamageTick -= frameTime;
|
||||
}
|
||||
|
||||
if (vampireComponent.NullDamage > 0)
|
||||
{
|
||||
if (vampireComponent.NextNullDamageTick <= 0)
|
||||
{
|
||||
vampireComponent.NextNullDamageTick = 2;
|
||||
vampireComponent.NullDamage -= FixedPoint2.New(2);
|
||||
if (vampireComponent.NullDamage < 0)
|
||||
{
|
||||
vampireComponent.NullDamage = FixedPoint2.Zero;
|
||||
}
|
||||
}
|
||||
vampireComponent.NextNullDamageTick -= frameTime;
|
||||
}
|
||||
}
|
||||
|
||||
var holyPointQuery = EntityQueryEnumerator<HolyPointComponent>();
|
||||
while (holyPointQuery.MoveNext(out var uid, out var holyPoint))
|
||||
{
|
||||
if (holyPoint.NextTimeTick <= 0)
|
||||
{
|
||||
holyPoint.NextTimeTick = 10;
|
||||
var vampires = _entityLookup.GetEntitiesInRange<VampireComponent>(Transform(uid).Coordinates, holyPoint.Range);
|
||||
foreach (var vampire in vampires)
|
||||
{
|
||||
if (vampire.Comp.TruePowerActive) continue;
|
||||
|
||||
if (TryComp(vampire.Owner, out FlammableComponent? flammable))
|
||||
{
|
||||
flammable.FireStacks = flammable.MaximumFireStacks;
|
||||
_flammable.Ignite(vampire.Owner, uid);
|
||||
_chat.TryEmoteWithoutChat(vampire, _prototypeManager.Index(Scream), true);
|
||||
_popup.PopupEntity(Loc.GetString("vampire-holy-point"), vampire.Owner, vampire.Owner, PopupType.LargeCaution);
|
||||
}
|
||||
}
|
||||
}
|
||||
holyPoint.NextTimeTick -= frameTime;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Alerts
|
||||
private void OnStartup(EntityUid uid, VampireComponent component, ComponentStartup args)
|
||||
{
|
||||
_alerts.ShowAlert(uid, component.BloodAlert);
|
||||
}
|
||||
|
||||
#region Drinking blood
|
||||
private void OnDrinkBlood(EntityUid uid, VampireComponent component, VampireDrinkingBloodActionEvent args)
|
||||
{
|
||||
if (TryDrink(uid, component, args))
|
||||
{
|
||||
var doAfterDelay = TimeSpan.FromSeconds(3);
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager, uid, doAfterDelay,
|
||||
new VampireDrinkingBloodDoAfterEvent() { Volume = 5f },
|
||||
eventTarget: uid,
|
||||
target: args.Target,
|
||||
used: args.Target)
|
||||
{
|
||||
BreakOnMove = true,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.01f,
|
||||
DistanceThreshold = 0.5f,
|
||||
NeedHand = true
|
||||
};
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterEventArgs);
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-countion"), uid, args.Target, PopupType.MediumCaution);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryDrink(EntityUid uid, VampireComponent component, VampireDrinkingBloodActionEvent args)
|
||||
{
|
||||
if (args.Target == uid)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-self"), uid, uid, PopupType.SmallCaution);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(uid, args.Target, popup: true) || HasComp<SyntheticOperatedComponent>(args.Target))
|
||||
return false;
|
||||
|
||||
IngestionBlockerComponent? blocker;
|
||||
if (_inventory.TryGetSlotEntity(uid, "mask", out var maskUid) &&
|
||||
TryComp(maskUid, out blocker) && blocker.Enabled)
|
||||
return false;
|
||||
|
||||
if (_inventory.TryGetSlotEntity(uid, "head", out var headUid) &&
|
||||
TryComp(headUid, out blocker) && blocker.Enabled)
|
||||
return false;
|
||||
|
||||
if (_rotting.IsRotten(args.Target))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-rotted"), uid, uid, PopupType.SmallCaution);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryComp<VampireComponent>(args.Target, out var targetVampireComponent))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-not-vampire"), uid, uid, PopupType.SmallCaution);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryComp<ThrallComponent>(args.Target, out var targetThrallComponent))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-not-thrall"), uid, uid, PopupType.SmallCaution);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DrinkDoAfter(EntityUid uid, VampireComponent component, ref VampireDrinkingBloodDoAfterEvent args)
|
||||
{
|
||||
if (args.Cancelled || !TryComp<BloodstreamComponent>(args.Target, out var targetBloodstream)
|
||||
|| targetBloodstream?.BloodSolution is null)
|
||||
return;
|
||||
|
||||
if (_rotting.IsRotten(args.Target!.Value))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-rotted"), args.User, args.User, PopupType.SmallCaution);
|
||||
return;
|
||||
}
|
||||
|
||||
var victimBloodRemaining = targetBloodstream.BloodSolution.Value.Comp.Solution.Volume;
|
||||
if (victimBloodRemaining <= 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-empty"), uid, uid, PopupType.SmallCaution);
|
||||
return;
|
||||
}
|
||||
|
||||
var bloodAlreadyConsumed = GetBloodConsumedByVampire(uid, args.Target.Value);
|
||||
|
||||
var maxBloodToConsume = 200;
|
||||
var maxAvailableBlood = (FixedPoint2)Math.Min((float)victimBloodRemaining, (float)(maxBloodToConsume - bloodAlreadyConsumed));
|
||||
|
||||
if (maxAvailableBlood <= 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-maxed-out"), uid, uid, PopupType.SmallCaution);
|
||||
return;
|
||||
}
|
||||
|
||||
var volumeToConsume = (FixedPoint2)Math.Min((float)victimBloodRemaining.Value, args.Volume * 2);
|
||||
|
||||
_audio.PlayPvs(component.BloodDrainSound, uid, AudioParams.Default.WithVolume(-3f));
|
||||
_blood.TryModifyBloodLevel(args.Target.Value, -(byte)(volumeToConsume * 0.5f));
|
||||
|
||||
if (HasComp<BibleUserComponent>(args.Target) && !component.TruePowerActive)
|
||||
{
|
||||
_damage.TryChangeDamage(uid, VampireComponent.HolyDamage, true);
|
||||
_popup.PopupEntity(Loc.GetString("vampire-ingest-holyblood"), uid, uid, PopupType.LargeCaution);
|
||||
_admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(uid):user} attempted to drink {volumeToConsume}u of {ToPrettyString(args.Target):target}'s holy blood");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var bloodSolution = _solution.SplitSolution(targetBloodstream.BloodSolution.Value, volumeToConsume);
|
||||
|
||||
if (!TryIngestBlood(uid, component, bloodSolution))
|
||||
{
|
||||
_solution.AddSolution(targetBloodstream.BloodSolution.Value, bloodSolution);
|
||||
return;
|
||||
}
|
||||
|
||||
_admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(uid):user} drank {volumeToConsume}u of {ToPrettyString(args.Target):target}'s blood");
|
||||
if (HasComp<HumanoidAppearanceComponent>(args.Target) && !HasComp<DnaModifiedComponent>(args.Target))
|
||||
AddBloodEssence(uid, volumeToConsume * 0.95);
|
||||
SetBloodConsumedByVampire(uid, args.Target.Value, bloodAlreadyConsumed + volumeToConsume);
|
||||
|
||||
if (args.Target.HasValue)
|
||||
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-countion-doafter"), uid, args.Target.Value, PopupType.SmallCaution);
|
||||
|
||||
args.Repeat = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryIngestBlood(EntityUid uid, VampireComponent component, Solution ingestedSolution, bool force = false)
|
||||
{
|
||||
if (TryComp<BodyComponent>(uid, out var body) && _body.TryGetBodyOrganEntityComps<StomachComponent>(uid, out var stomachs))
|
||||
{
|
||||
var firstStomach = stomachs.FirstOrNull(stomach => _stomach.CanTransferSolution(stomach.Owner, ingestedSolution, stomach));
|
||||
if (firstStomach is null)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("vampire-full-stomach"), uid, uid, PopupType.SmallCaution);
|
||||
return false;
|
||||
}
|
||||
return _stomach.TryTransferSolution(firstStomach.Value.Owner, ingestedSolution, firstStomach.Value);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private FixedPoint2 GetBloodConsumedByVampire(EntityUid vampireUid, EntityUid targetUid)
|
||||
{
|
||||
if (!_bloodConsumedTracker.ContainsKey(vampireUid))
|
||||
_bloodConsumedTracker[vampireUid] = new Dictionary<EntityUid, FixedPoint2>();
|
||||
|
||||
return _bloodConsumedTracker[vampireUid].GetValueOrDefault(targetUid, 0);
|
||||
}
|
||||
|
||||
private void SetBloodConsumedByVampire(EntityUid vampireUid, EntityUid targetUid, FixedPoint2 amount)
|
||||
{
|
||||
if (!_bloodConsumedTracker.ContainsKey(vampireUid))
|
||||
_bloodConsumedTracker[vampireUid] = new Dictionary<EntityUid, FixedPoint2>();
|
||||
|
||||
_bloodConsumedTracker[vampireUid][targetUid] = amount;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Blood Manipulation
|
||||
private bool AddBloodEssence(EntityUid uid, FixedPoint2 quantity)
|
||||
{
|
||||
if (quantity < 0 || !TryComp<VampireComponent>(uid, out var vampireComponent))
|
||||
return false;
|
||||
|
||||
vampireComponent.CurrentBlood += quantity;
|
||||
vampireComponent.TotalBloodDrank += (float)quantity;
|
||||
|
||||
Dirty(uid, vampireComponent);
|
||||
_alerts.ShowAlert(uid, vampireComponent.BloodAlert);
|
||||
|
||||
UpdatePowers(uid, vampireComponent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SubtractBloodEssence(EntityUid uid, FixedPoint2 quantity)
|
||||
{
|
||||
if (!TryComp<VampireComponent>(uid, out var vampireComponent))
|
||||
return false;
|
||||
|
||||
var adjustedQuantity = quantity * (1 + vampireComponent.NullDamage.Float() / 100);
|
||||
if (adjustedQuantity <= 0 || vampireComponent.CurrentBlood < adjustedQuantity)
|
||||
return false;
|
||||
|
||||
vampireComponent.CurrentBlood -= adjustedQuantity;
|
||||
|
||||
Dirty(uid, vampireComponent);
|
||||
_alerts.ShowAlert(uid, vampireComponent.BloodAlert);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CheckBloodEssence(EntityUid uid, FixedPoint2 quantity)
|
||||
{
|
||||
if (!TryComp<VampireComponent>(uid, out var vampireComponent))
|
||||
return false;
|
||||
|
||||
var adjustedQuantity = quantity * (1 + vampireComponent.NullDamage.Float() / 100);
|
||||
return vampireComponent.CurrentBlood >= adjustedQuantity;
|
||||
}
|
||||
|
||||
private void UpdatePowers(EntityUid uid, VampireComponent component)
|
||||
{
|
||||
if (component.CurrentEvolution == null)
|
||||
return;
|
||||
|
||||
var currentBlood = component.CurrentBlood;
|
||||
var vampireClass = component.CurrentEvolution;
|
||||
var thresholds = GetThresholdsForClass(vampireClass);
|
||||
foreach (var threshold in thresholds)
|
||||
{
|
||||
if (currentBlood >= threshold.Key)
|
||||
{
|
||||
foreach (var skill in threshold.Value)
|
||||
{
|
||||
if (!HasSkill(component, skill))
|
||||
{
|
||||
AddSkill(uid, component, skill);
|
||||
_admin.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(uid)}: added {skill} for {vampireClass}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBlood >= 1000 && !component.TruePowerActive)
|
||||
{
|
||||
MakeImmuneToHoly(uid, component);
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasSkill(VampireComponent component, string skill)
|
||||
{
|
||||
return component.AcquiredSkills.Contains(skill);
|
||||
}
|
||||
|
||||
private void AddSkill(EntityUid uid, VampireComponent component, string skill)
|
||||
{
|
||||
if (!HasSkill(component, skill))
|
||||
{
|
||||
component.AcquiredSkills.Add(skill);
|
||||
_action.AddAction(uid, skill);
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<float, List<string>> GetThresholdsForClass(string vampireClass)
|
||||
{
|
||||
switch (vampireClass)
|
||||
{
|
||||
case "Hemomancer":
|
||||
return new Dictionary<float, List<string>>
|
||||
{
|
||||
{ 150f, new List<string> { "ActionVampireClaws" } },
|
||||
{ 250f, new List<string> { "ActionVampireBloodTendrils", "ActionVampireBloodBarrier" } },
|
||||
{ 400f, new List<string> { "ActionVampireSanguinePool" } },
|
||||
{ 600f, new List<string> { "ActionVampirePredatorSenses" } },
|
||||
{ 800f, new List<string> { "ActionVampireBloodEruption" } },
|
||||
{ 1000f, new List<string> { "ActionVampireBloodBringersRite" } }
|
||||
};
|
||||
|
||||
case "Umbrae":
|
||||
return new Dictionary<float, List<string>>
|
||||
{
|
||||
{ 150f, new List<string> { "ActionVampireCloakOfDarkness" } },
|
||||
{ 250f, new List<string> { "ActionVampireShadowSnare", "ActionVampireSoulAnchor" } },
|
||||
{ 400f, new List<string> { "ActionVampireDarkPassage" } },
|
||||
{ 600f, new List<string> { "ActionVampireExtinguish" } },
|
||||
{ 800f, new List<string> { "ActionVampireShadowBoxing" } },
|
||||
{ 1000f, new List<string> { "ActionVampireEternalDarkness" } }
|
||||
};
|
||||
|
||||
case "Gargantua":
|
||||
return new Dictionary<float, List<string>>
|
||||
{
|
||||
{ 150f, new List<string> { "ActionVampireBloodSwell" } },
|
||||
{ 250f, new List<string> { "ActionVampireBloodRush", "ActionVampireSeismicStomp" } },
|
||||
{ 400f, new List<string> { "ActionVampireBloodSwellAdvanced" } },
|
||||
{ 600f, new List<string> { "ActionVampireOverwhelmingForce" } },
|
||||
{ 800f, new List<string> { "ActionDemonicGrasp" } },
|
||||
{ 1000f, new List<string> { "ActionVampireCharge" } }
|
||||
};
|
||||
|
||||
case "Dantalion":
|
||||
return new Dictionary<float, List<string>>
|
||||
{
|
||||
{ 150f, new List<string> { "ActionEnthrall", "ActionCommune" } },
|
||||
{ 250f, new List<string> { "ActionPacify", "ActionSubspaceSwap" } },
|
||||
{ 400f, new List<string> { /*"ActionDeployDecoy",*/"ActionMaxThrallCountUpdate1" } },
|
||||
{ 600f, new List<string> { "ActionRallyThralls", "ActionMaxThrallCountUpdate2" } },
|
||||
{ 800f, new List<string> { "ActionBloodBond" } },
|
||||
{ 1000f, new List<string> { "ActionMassHysteria", "ActionMaxThrallCountUpdate3" } }
|
||||
};
|
||||
|
||||
default:
|
||||
return new Dictionary<float, List<string>>();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Space Damage
|
||||
private void DoSpaceDamage(Entity<VampireComponent> vampire)
|
||||
{
|
||||
_damage.TryChangeDamage(vampire.Owner, VampireComponent.SpaceDamage, true, origin: vampire);
|
||||
_popup.PopupEntity(Loc.GetString("vampire-startlight-burning"), vampire, vampire, PopupType.LargeCaution);
|
||||
}
|
||||
|
||||
private bool IsInSpace(EntityUid vampireUid)
|
||||
{
|
||||
var vampirePosition = _transform.GetMapCoordinates(Transform(vampireUid));
|
||||
if (!_mapMan.TryFindGridAt(vampirePosition, out _, out _))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Distribute Damage
|
||||
private void OnDamageChanged(EntityUid uid, VampireComponent component, ref DamageChangedEvent args)
|
||||
{
|
||||
// Null Rode Damage
|
||||
if (args.Origin.HasValue && HasComp<BibleUserComponent>(args.Origin.Value) && !component.TruePowerActive)
|
||||
{
|
||||
var heldEntity = _hands.GetActiveItem(uid);
|
||||
if (TryComp<NullRodComponent>(heldEntity, out var nullRodComp))
|
||||
{
|
||||
var damageToApply = component.NullDamage > 0
|
||||
? nullRodComp.NullDamage
|
||||
: nullRodComp.FirstNullDamage;
|
||||
|
||||
component.NullDamage += damageToApply;
|
||||
component.NullDamage = FixedPoint2.Clamp(component.NullDamage, FixedPoint2.Zero, 120);
|
||||
}
|
||||
}
|
||||
|
||||
// Distribute Damage
|
||||
if (_isDamageBeingHandled || !component.IsDamageSharingActive
|
||||
|| component.ThrallOwned.Count == 0 || args.DamageDelta is null
|
||||
|| IsNegativeDamage(args.DamageDelta))
|
||||
return;
|
||||
|
||||
_isDamageBeingHandled = true;
|
||||
_damage.TryChangeDamage(uid, -args.DamageDelta, true);
|
||||
DistributeDamage(uid, component, args.DamageDelta, ref args);
|
||||
_isDamageBeingHandled = false;
|
||||
}
|
||||
|
||||
private void OnDamageChanged(EntityUid uid, ThrallComponent component, ref DamageChangedEvent args)
|
||||
{
|
||||
if (_isDamageBeingHandled || !TryComp(component.VampireOwner, out VampireComponent? vampire)
|
||||
|| !vampire.IsDamageSharingActive || args.DamageDelta is null
|
||||
|| IsNegativeDamage(args.DamageDelta))
|
||||
return;
|
||||
|
||||
_isDamageBeingHandled = true;
|
||||
_damage.TryChangeDamage(uid, -args.DamageDelta, true);
|
||||
DistributeDamage(component.VampireOwner.Value, vampire, args.DamageDelta, ref args);
|
||||
_isDamageBeingHandled = false;
|
||||
}
|
||||
|
||||
private void DistributeDamage(
|
||||
EntityUid vampireUid,
|
||||
VampireComponent vampireComponent,
|
||||
DamageSpecifier damage,
|
||||
ref DamageChangedEvent args,
|
||||
EntityUid? excludedEntity = null)
|
||||
{
|
||||
if (damage == null)
|
||||
return;
|
||||
|
||||
var participants = new List<EntityUid> { vampireUid };
|
||||
participants.AddRange(vampireComponent.ThrallOwned.Where(thrall => Exists(thrall) && thrall != excludedEntity));
|
||||
|
||||
if (participants.Count == 0)
|
||||
return;
|
||||
|
||||
var sharedDamage = damage / participants.Count;
|
||||
|
||||
foreach (var participant in participants)
|
||||
{
|
||||
if (!HasComp<DamageableComponent>(participant))
|
||||
continue;
|
||||
|
||||
_damage.TryChangeDamage(participant, sharedDamage, true);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsNegativeDamage(DamageSpecifier damage)
|
||||
{
|
||||
var totalDamage = damage.DamageDict.Values.Aggregate(FixedPoint2.Zero, (sum, value) => sum + value);
|
||||
return totalDamage < FixedPoint2.Zero;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Thralls
|
||||
private void MindShieldImplanted(EntityUid uid, MindShieldComponent comp, ComponentStartup init)
|
||||
{
|
||||
if (TryComp<ThrallComponent>(uid, out var thrall))
|
||||
{
|
||||
var stunTime = TimeSpan.FromSeconds(4);
|
||||
var name = Identity.Entity(uid, EntityManager);
|
||||
if (TryComp<VampireComponent>(thrall.VampireOwner, out var vampire))
|
||||
{
|
||||
vampire.ThrallOwned.Remove(uid);
|
||||
vampire.ThrallCount--;
|
||||
}
|
||||
|
||||
RemComp<ThrallComponent>(uid);
|
||||
_stun.TryUpdateParalyzeDuration(uid, stunTime);
|
||||
_popup.PopupEntity(Loc.GetString("thrall-break-control", ("name", name)), uid);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region True Power
|
||||
private void MakeImmuneToHoly(EntityUid vampire, VampireComponent component)
|
||||
{
|
||||
if (TryComp<ReactiveComponent>(vampire, out var reactive))
|
||||
{
|
||||
if (reactive.ReactiveGroups == null)
|
||||
return;
|
||||
|
||||
reactive.ReactiveGroups.Remove("Unholy");
|
||||
}
|
||||
|
||||
component.TruePowerActive = true;
|
||||
RemComp<UnholyComponent>(vampire);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("vampire-true-power"), vampire, vampire, PopupType.Medium);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -17,9 +17,10 @@ namespace Content.Shared.Communications
|
||||
public readonly bool CountdownStarted;
|
||||
public List<string>? AlertLevels;
|
||||
public string CurrentAlert;
|
||||
public Color CurrentAlertColor;
|
||||
public float CurrentAlertDelay;
|
||||
|
||||
public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null)
|
||||
public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, Color currentAlertColor, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null)
|
||||
{
|
||||
CanAnnounce = canAnnounce;
|
||||
CanCall = canCall;
|
||||
@@ -27,6 +28,7 @@ namespace Content.Shared.Communications
|
||||
CountdownStarted = expectedCountdownEnd != null;
|
||||
AlertLevels = alertLevels;
|
||||
CurrentAlert = currentAlert;
|
||||
CurrentAlertColor = currentAlertColor;
|
||||
CurrentAlertDelay = currentAlertDelay;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,6 +452,24 @@ public abstract partial class SharedStaminaSystem : EntitySystem
|
||||
_movementMod.TryUpdateMovementStatus(ent.Owner, status.Value, ent.Comp.StunModifierThresholds[closest]);
|
||||
}
|
||||
|
||||
// WyLab-Wega-Start
|
||||
public void RemoveStaminaDamage(Entity<StaminaComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
|
||||
if (ent.Comp.StaminaDamage >= ent.Comp.CritThreshold)
|
||||
ExitStamCrit(ent);
|
||||
|
||||
ent.Comp.StaminaDamage = 0;
|
||||
AdjustStatus(ent.Owner);
|
||||
RemComp<ActiveStaminaComponent>(ent);
|
||||
_status.TryRemoveStatusEffect(ent, StaminaLow);
|
||||
UpdateStaminaVisuals((ent.Owner, ent.Comp));
|
||||
Dirty(ent);
|
||||
}
|
||||
// WyLab-Wega-End
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StaminaAnimationEvent(NetEntity entity) : EntityEventArgs
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed class MovementModStatusSystem : EntitySystem
|
||||
public static readonly EntProtoId VomitingSlowdown = "VomitingSlowdownStatusEffect";
|
||||
public static readonly EntProtoId TaserSlowdown = "TaserSlowdownStatusEffect";
|
||||
public static readonly EntProtoId FlashSlowdown = "FlashSlowdownStatusEffect";
|
||||
public static readonly EntProtoId Slowdown = "BasicSlowdownStatusEffect"; // WyLab-Wega
|
||||
public static readonly EntProtoId StatusEffectFriction = "StatusEffectFriction";
|
||||
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||
|
||||
25
Content.Shared/_Wega/BloodBrother/BloodBrotherComponent.cs
Normal file
25
Content.Shared/_Wega/BloodBrother/BloodBrotherComponent.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Blood.Brother;
|
||||
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class BloodBrotherComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Mind entity ID of your blood brother
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid? BrotherMind;
|
||||
|
||||
/// <summary>
|
||||
/// Whether both brothers must survive for victory
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequireBothAlive = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether both brothers must escape for victory
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequireBothEscape = true;
|
||||
}
|
||||
121
Content.Shared/_Wega/CCCVars/CCVars.cs
Normal file
121
Content.Shared/_Wega/CCCVars/CCVars.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Shared.CCVar;
|
||||
|
||||
[CVarDefs]
|
||||
public sealed class WegaCVars
|
||||
{
|
||||
/*
|
||||
Ghost Respawn CVars
|
||||
*/
|
||||
/// <summary>
|
||||
/// Whether or not respawning is enabled.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> GhostRespawnEnabled =
|
||||
CVarDef.Create("wega.respawn_enabled", false, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Respawn time, how long the player has to wait in seconds after death.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> GhostRespawnTime =
|
||||
CVarDef.Create("wega.respawn_time", 1200.0f, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/*
|
||||
Barks CVars
|
||||
*/
|
||||
/// <summary>
|
||||
/// Responsible for turning on and off the bark system.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> BarksEnabled =
|
||||
CVarDef.Create("wega.barks_enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Default volume setting of Barks sound.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> BarksVolume =
|
||||
CVarDef.Create("wega.barks_volume", 0f, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/*
|
||||
Night Light System CVars
|
||||
*/
|
||||
/// <summary>
|
||||
/// Responsible for switching the night light system.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> NightLightEnabled =
|
||||
CVarDef.Create("wega.night_light_enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
|
||||
|
||||
/// <summary>
|
||||
/// Switching adjusts all the lamps to the holiday mode according to the logic of updating the night lighting.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> PartyEnabled =
|
||||
CVarDef.Create("wega.party_enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
|
||||
|
||||
/*
|
||||
Sound insulation CVars
|
||||
*/
|
||||
/// <summary>
|
||||
/// If you enable this mode, it will process the sound with sound isolation.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> SoundInsulationEnabled =
|
||||
CVarDef.Create("wega.sound_insulation_enabled", false, CVar.SERVER | CVar.REPLICATED | CVar.ARCHIVE);
|
||||
|
||||
/*
|
||||
Vote CVars
|
||||
*/
|
||||
/// <summary>
|
||||
/// If enabled forcibly, it will trigger a vote for the mode at the end of the round.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VoteRoundEndEnabled =
|
||||
CVarDef.Create("wega.roundend_vote_enabled", false, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
Ic Flavors
|
||||
*/
|
||||
/// <summary>
|
||||
/// Sets the maximum length for OOC flavor text.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> OOCMaxFlavorTextLength =
|
||||
CVarDef.Create("ic.oocflavor_text_length", 2048, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum length for character description text.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> CharacterDescriptionLength =
|
||||
CVarDef.Create("ic.character_description_length", 2048, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum length for green preferences text.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> GreenPreferencesLength =
|
||||
CVarDef.Create("ic.green_preferences_length", 256, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum length for yellow preferences text.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> YellowPreferencesLength =
|
||||
CVarDef.Create("ic.yellow_preferences_length", 256, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum length for red preferences text.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> RedPreferencesLength =
|
||||
CVarDef.Create("ic.red_preferences_length", 256, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum length for tags text.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> TagsLength =
|
||||
CVarDef.Create("ic.tags_length", 128, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum length for links text.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> LinksLength =
|
||||
CVarDef.Create("ic.links_length", 512, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum length for NSFW preferences text.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> NSFWPreferencesLength =
|
||||
CVarDef.Create("ic.nsfw_preferences_length", 1024, CVar.SERVER | CVar.REPLICATED);
|
||||
}
|
||||
11
Content.Shared/_Wega/Food/EdibleMatterComponent.cs
Normal file
11
Content.Shared/_Wega/Food/EdibleMatterComponent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Content.Shared.Edible.Matter;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class EdibleMatterComponent : Component
|
||||
{
|
||||
[DataField("nutritionValue")]
|
||||
public float NutritionValue = 5f;
|
||||
|
||||
[DataField("canBeEaten")]
|
||||
public bool CanBeEaten = true;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Friendly.Faction;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class FriendlyFactionComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public ProtoId<NpcFactionPrototype>? Faction;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Content.Shared.Genetics;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class DnaModifiedComponent : Component;
|
||||
9
Content.Shared/_Wega/Ghost/GhostRespawnEvent.cs
Normal file
9
Content.Shared/_Wega/Ghost/GhostRespawnEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Wega.Ghost.Respawn;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class GhostRespawnEvent(TimeSpan? time) : EntityEventArgs
|
||||
{
|
||||
public readonly TimeSpan? Time = time;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Hallucinations;
|
||||
|
||||
[RegisterComponent, Serializable]
|
||||
public sealed partial class HallucinationsComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public TimeSpan NextSecond = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// How far from humanoid can appear hallucination
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Range = 7f;
|
||||
|
||||
/// <summary>
|
||||
/// How often (in seconds) hallucinations spawned
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float SpawnRate = 15f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum spawn chance per humanoid
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MinChance = 0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// Max spawn chance per humanoid
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float MaxChance = 0.8f;
|
||||
|
||||
/// <summary>
|
||||
/// How much chance increased per spawn
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float IncreaseChance = 0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// Max spawned hallucinations count for one spawn
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxSpawns = 5;
|
||||
|
||||
/// <summary>
|
||||
/// How much entities already spawned
|
||||
/// </summary>
|
||||
public int SpawnedCount = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Current spawn chance
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float CurChance = 0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// List of prototypes that are spawned as a hallucination.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntProtoId> Spawns = new();
|
||||
|
||||
/// <summary>
|
||||
/// Hallucinations pack proto
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HallucinationsPrototype? Proto;
|
||||
|
||||
/// <summary>
|
||||
/// Currently selected for hallucinations layer
|
||||
/// </summary>
|
||||
public int Layer = 50;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user