66 Commits

Author SHA1 Message Date
Codex
d2c9e4a41e fix: Update TryAddToChemicals to TryAddToBloodstream
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 3m14s
Publish / build (push) Successful in 7m46s
RGA schema validator / YAML RGA schema validator (push) Successful in 36s
RSI Validator / Validate RSIs (push) Successful in 28s
Map file schema validator / YAML map schema validator (push) Successful in 2m30s
YAML Linter / YAML Linter (push) Successful in 3m6s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 1s
Test Packaging / Test Packaging (push) Successful in 30m32s
Build & Test Debug / build (ubuntu-latest) (push) Successful in 42m32s
Build & Test Debug / Build & Test Debug (push) Successful in 3s
CRLF Check / CRLF Check (pull_request) Successful in 19s
RGA schema validator / YAML RGA schema validator (pull_request) Failing after 26s
RSI Validator / Validate RSIs (pull_request) Failing after 28s
Map file schema validator / YAML map schema validator (pull_request) Failing after 33s
YAML Linter / YAML Linter (pull_request) Successful in 3m39s
Close PRs on master / run (pull_request_target) Failing after 4s
Check Merge Conflicts / Label (pull_request_target) Failing after 2s
Labels: Review / add_label (pull_request_target) Failing after 2s
Labels: PR / labeler (pull_request_target) Failing after 10s
Labels: Size / size-label (pull_request_target) Successful in 2s
Labels: Untriaged / add_label (pull_request_target) Failing after 2s
Diff RSIs / Diff (pull_request_target) Failing after 18s
Upstream renamed BloodstreamSystem.TryAddToChemicals to TryAddToBloodstream.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 03:26:53 +01:00
Codex
dcf254f0c2 Merge upstream syndicate/master (212 commits)
Some checks failed
Publish / build (push) Failing after 3m4s
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Update Wiki / Build and Publish JSON blobs to wiki (push) Failing after 7m48s
Resolve conflict in Resources/Prototypes/Reagents/toxins.yml:
- Combined Vampire (wylab) and Plant (upstream) metabolizer types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 03:19:44 +01:00
29b91de9c0 ci: Add checkout step to labeler-pr.yml (fixes Gitea compatibility)
All checks were successful
Test Packaging / Test Packaging (push) Successful in 10m53s
Publish / build (push) Successful in 11m21s
RGA schema validator / YAML RGA schema validator (push) Successful in 45s
RSI Validator / Validate RSIs (push) Successful in 1m1s
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 16m1s
Map file schema validator / YAML map schema validator (push) Successful in 11m11s
YAML Linter / YAML Linter (push) Successful in 13m5s
Build & Test Debug / build (ubuntu-latest) (push) Successful in 43m11s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 4s
Build & Test Debug / Build & Test Debug (push) Successful in 3s
2025-12-19 16:28:51 +01:00
0cae8cab95 ci: Rewrite labeler-conflict.yml for Gitea API (fixes GitHub-only action)
Some checks failed
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Publish / build (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
2025-12-19 16:28:36 +01:00
247e2ceee4 ci: Remove rsi-diff.yml (GitHub-only actions incompatible with Gitea)
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Waiting to run
Publish / build (push) Waiting to run
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 14m44s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m7s
RSI Validator / Validate RSIs (push) Successful in 1m6s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 1m49s
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Successful in 10m50s
YAML Linter / YAML Linter (push) Successful in 10m38s
2025-12-19 15:15:37 +01:00
78fb363d47 fix: add missing wega alerts locale file
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 14m46s
Publish / build (push) Successful in 31m46s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m5s
RSI Validator / Validate RSIs (push) Successful in 1m0s
Build & Test Debug / build (ubuntu-latest) (push) Successful in 40m17s
Map file schema validator / YAML map schema validator (push) Successful in 11m20s
YAML Linter / YAML Linter (push) Successful in 10m31s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
Build & Test Debug / Build & Test Debug (push) Successful in 6s
Publish Public / build (push) Failing after 20m24s
Benchmarks / Run Benchmarks (push) Successful in 22m4s
Publish Testing / build (push) Failing after 20m12s
Add alerts/alerts.ftl with vampire blood, strangle, and offer alert
localizations that were missing from the wega port. Fixes runtime
locale errors for alerts-vampire-blood-name/desc keys.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 02:39:46 +01:00
cbe6e355bb fix: add Vampire immunity to UncookedAnimalProteins
Some checks failed
Publish / build (push) Failing after 18s
Test Packaging / Test Packaging (push) Failing after 2s
Update Wiki / Build and Publish JSON blobs to wiki (push) Failing after 4s
RSI Validator / Validate RSIs (push) Failing after 1s
Map file schema validator / YAML map schema validator (push) Failing after 2s
YAML Linter / YAML Linter (push) Failing after 2s
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 15m23s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 4s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m2s
Build & Test Debug / build (ubuntu-latest) (push) Successful in 41m19s
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Port Corvax-Wega-Edit: Vampires no longer get sick, vomit, or take
poison damage when drinking blood. The UncookedAnimalProteins reagent
(produced when Blood is metabolized) now checks for Vampire metabolizer
type in addition to Animal and Vox.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 01:59:43 +01:00
5099ee8bb4 Порт: Улучшение UX и дизайна консоли связи
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 17m27s
RSI Validator / Validate RSIs (push) Successful in 50s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m0s
Build & Test Debug / build (ubuntu-latest) (push) Successful in 46m13s
Build & Test Debug / Build & Test Debug (push) Successful in 3s
Test Packaging / Test Packaging (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Successful in 11m24s
Publish / build (push) Successful in 28m13s
YAML Linter / YAML Linter (push) Has been cancelled
Портировано из space-wizards/space-station-14#41899

Изменения:
- Добавлено подтверждение при изменении уровня угрозы (предотвращает случайные изменения)
- Разделены кнопки вызова/отзыва шаттла в отдельные UI области
- Созданы отдельные вкладки для объявлений и трансляции
- Разбито монолитное меню на 3 виджета: AlertLevelControls, MessagingControls, ShuttleControls
- Добавлен LCD-дисплей с таймером обратного отсчета
- Обновлены текстуры и шрифты

Локализация:
- Полностью переведены новые строки на русский язык в стиле Corvax

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 22:19:40 +01:00
69d4134d71 Fix NukeOpsTest for IPC species
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 16m53s
Build & Test Debug / build (ubuntu-latest) (push) Successful in 45m52s
Publish / build (push) Successful in 34m44s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m12s
RSI Validator / Validate RSIs (push) Successful in 1m5s
Map file schema validator / YAML map schema validator (push) Successful in 11m26s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Skip respirator checks when nukie spawns as IPC (android) since they
don't have lungs. Fixes test failure with RespiratorComponent.

Based on Corvax fix: space-syndicate/space-station-14#3472

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 21:04:01 +01:00
a1a795d2d4 fix: hide BeaconSoul from spawn menu to fix UninitializedSaveTest
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 18m49s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 46m24s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Successful in 40m54s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m13s
RSI Validator / Validate RSIs (push) Successful in 1m2s
Map file schema validator / YAML map schema validator (push) Successful in 11m5s
Test Packaging / Test Packaging (push) Successful in 34m22s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 4s
YAML Linter / YAML Linter (push) Has been cancelled
The test requires entities with runtime-assigned EntityUid fields to be
hidden from the spawn menu. BeaconSoul.VampireOwner is set at runtime,
not from prototype data, so it should not be spawnable directly.

This matches the pattern used by other vampire effects like VampireMistEffect.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 19:40:51 +01:00
c8087d82d1 ci: exclude Wylab override files from build-test-debug
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 15m6s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 40m55s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Successful in 30m48s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m11s
RSI Validator / Validate RSIs (push) Successful in 1m3s
Map file schema validator / YAML map schema validator (push) Successful in 10m57s
Test Packaging / Test Packaging (push) Successful in 28m17s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
YAML Linter / YAML Linter (push) Successful in 11m21s
Same approach as yaml-linter - delete _Wylab override files before
testing to avoid "Duplicate ID" prototype errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 12:52:29 +01:00
07ed10782a fix: add --break-system-packages to pip install in publish workflows
Some checks failed
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Publish / build (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Ubuntu 23.04+ uses PEP 668 to protect system Python packages.
The --break-system-packages flag allows pip to install in externally-managed environments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 12:48:09 +01:00
e048406782 ci: exclude Wylab override files from yaml-linter
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 15m23s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 39m12s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Successful in 29m55s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m3s
RSI Validator / Validate RSIs (push) Successful in 1m0s
Map file schema validator / YAML map schema validator (push) Successful in 10m33s
Test Packaging / Test Packaging (push) Successful in 27m16s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
YAML Linter / YAML Linter (push) Successful in 11m0s
Benchmarks / Run Benchmarks (push) Successful in 21m33s
Publish Testing / build (push) Failing after 22m57s
The Robust engine's prototype validator flags duplicate IDs as errors,
but duplicate IDs are the intended SS14 pattern for fork customizations
(last loaded wins). Delete _Wylab override files before linting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 06:08:14 +01:00
047241e3ce fix: add missing _Wega textures and remove duplicate locale
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 15m10s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 37m48s
Build & Test Debug / Build & Test Debug (push) Has been skipped
RGA schema validator / YAML RGA schema validator (push) Successful in 1m4s
RSI Validator / Validate RSIs (push) Successful in 56s
Publish / build (push) Successful in 29m2s
Map file schema validator / YAML map schema validator (push) Successful in 10m24s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
YAML Linter / YAML Linter (push) Failing after 9m26s
- Copy missing texture files from wega fork (/Textures/_Wega/*.png)
- Remove duplicate bloodbrother locale that caused "already exist" errors

Fixes integration test failures for TestWindows and SandboxTest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 04:24:53 +01:00
723b9f893c chore: increase integration test timeout to 40 minutes
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 14m20s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 39m41s
Build & Test Debug / Build & Test Debug (push) Has been skipped
RGA schema validator / YAML RGA schema validator (push) Successful in 1m3s
RSI Validator / Validate RSIs (push) Successful in 1m0s
Map file schema validator / YAML map schema validator (push) Successful in 10m56s
Test Packaging / Test Packaging (push) Successful in 27m37s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
YAML Linter / YAML Linter (push) Failing after 11m49s
Publish / build (push) Failing after 22m45s
Publish Public / build (push) Failing after 17m20s
Tests took 30m 24s, exceeding the previous 30-minute limit.
Adding more buffer to prevent mid-test shutdowns.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 01:30:10 +01:00
f98fa916fd ci: increase integration test timeout from 20 to 30 minutes
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 14m57s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 43m40s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Successful in 31m17s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m2s
RSI Validator / Validate RSIs (push) Successful in 1m1s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
The 20-minute timeout was triggering mid-test, killing all test pairs
and causing cascading failures with "Pool manager has not been
initialized" errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 00:36:29 +01:00
1c87bf9d1b Fix namespace error for VampireRuleComponent
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 14m39s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 33m7s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Successful in 32m21s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m3s
RSI Validator / Validate RSIs (push) Successful in 1m0s
Map file schema validator / YAML map schema validator (push) Successful in 11m7s
Test Packaging / Test Packaging (push) Successful in 28m0s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 6s
YAML Linter / YAML Linter (push) Failing after 9m34s
The component uses Content.Server.GameTicking.Rules.Components namespace,
not Content.Server._Wega.GameTicking.Rules.Components.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:02:21 +01:00
31f769be43 Add Vampire and Blood Brothers to admin antag menu
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 8m19s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 8m32s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Failing after 7m45s
Publish / build (push) Failing after 8m0s
RSI Validator / Validate RSIs (push) Successful in 1m8s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m14s
YAML Linter / YAML Linter (push) Failing after 6m13s
Map file schema validator / YAML map schema validator (push) Successful in 10m53s
- Add admin verbs for making targets into Vampire and Blood Brothers
- Use _Wega vampire action icons (bite, blood_bond)
- Add localization strings for en-US and ru-RU

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 22:49:29 +01:00
6c3a12f5c1 tweak: add Wylab game rules overrides for low-pop tuning
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 5m3s
Test Packaging / Test Packaging (push) Successful in 6m11s
RGA schema validator / YAML RGA schema validator (push) Failing after 25s
RSI Validator / Validate RSIs (push) Successful in 28s
Map file schema validator / YAML map schema validator (push) Failing after 24s
YAML Linter / YAML Linter (push) Failing after 2m44s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
Publish / build (push) Failing after 32m8s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 32m45s
Build & Test Debug / Build & Test Debug (push) Has been skipped
- Add SubVampire (30%) and SubBloodBrothers (30%) to all SubGamemode variants
- Cap antag spawn events (Dragon, Ninja, Revenant, Wizard, LoneOps, SleeperAgents) to maxOccurrences: 1
- Lower minPlayers thresholds for low-pop server

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 20:04:59 +01:00
32130b1cf2 tweak: lower minPlayers to 10 for all antag rules
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 3m48s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Successful in 5m44s
RGA schema validator / YAML RGA schema validator (push) Failing after 24s
RSI Validator / Validate RSIs (push) Successful in 27s
Map file schema validator / YAML map schema validator (push) Failing after 56s
YAML Linter / YAML Linter (push) Failing after 1m40s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 31m51s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Successful in 32m17s
Benchmarks / Run Benchmarks (push) Failing after 10m10s
Publish Testing / build (push) Failing after 6m33s
Adjusted for low-pop server (~20 players):
- Nukeops: 20 → 10
- Changeling: 25 → 10
- Revolutionary: 15 → 10
- Zombie: 20 → 10
- Xenoborgs: 40 → 10

Ratios kept intact.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 08:16:57 +01:00
c21f12fb73 tweak: lower minPlayers for low-pop server
Some checks failed
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Publish / build (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
- BloodBrothers: 20 → 10 players
- Vampire: 20 → 5 players
- SubVampire: 15 → 5 players

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 08:13:08 +01:00
dafb20afdb fix: add missing locale files for vampire/blood brothers
Some checks failed
Publish / build (push) Failing after 2m29s
RGA schema validator / YAML RGA schema validator (push) Failing after 34s
RSI Validator / Validate RSIs (push) Successful in 45s
Map file schema validator / YAML map schema validator (push) Failing after 22s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Added missing locale entries:
- MindRoleVampire and MindRoleBloodBrother entity names
- Game rule entities (Vampire, BloodBrothers, SubVampire, SubBloodBrothers)

These were causing the admin antagonist menu to break.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 08:07:01 +01:00
6c6fd192af fix: add missing vampire and blood brother role localization
Some checks failed
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Publish / build (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Added locale files for roles-antag-vampire-name/objective and
roles-antag-bloodbrother-name/objective which were referenced in
prototypes but missing from locale files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 08:04:46 +01:00
bba643846e ci: replace pwsh with bash in build-test-debug workflow
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 1m56s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Successful in 6m24s
RGA schema validator / YAML RGA schema validator (push) Failing after 23s
RSI Validator / Validate RSIs (push) Successful in 26s
Map file schema validator / YAML map schema validator (push) Failing after 22s
YAML Linter / YAML Linter (push) Failing after 2m22s
Publish / build (push) Successful in 31m25s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 31m42s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Gitea runner images don't have PowerShell installed.
Changed integration test step to use bash syntax for env var.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 05:08:31 +01:00
0670e5079a feat: port wega vampire component checks and slowdown system
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 3m42s
Publish / build (push) Failing after 33s
Test Packaging / Test Packaging (push) Successful in 5m40s
RGA schema validator / YAML RGA schema validator (push) Failing after 23s
RSI Validator / Validate RSIs (push) Successful in 46s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 11m3s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Map file schema validator / YAML map schema validator (push) Failing after 27s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
YAML Linter / YAML Linter (push) Successful in 10m44s
- Add SyntheticOperatedComponent marker for android/synthetic check
- Add DnaModifiedComponent marker for genetics modification check
- Add BasicSlowdownStatusEffect for vampire abilities
- Update VampireSystem to use SyntheticOperatedComponent and DnaModifiedComponent
- Update VampireSystem.Abilities to use MovementModStatusSystem.Slowdown

This completes the wega vampire port by matching exact wega fork behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 03:05:58 +01:00
1602fbd742 feat: add MetabolizerSystem methods for vampire blood metabolism
Some checks failed
Publish / build (push) Successful in 6m42s
Test Packaging / Test Packaging (push) Failing after 1m18s
RGA schema validator / YAML RGA schema validator (push) Failing after 29s
RSI Validator / Validate RSIs (push) Successful in 27s
Map file schema validator / YAML map schema validator (push) Failing after 24s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 11m28s
Build & Test Debug / Build & Test Debug (push) Has been skipped
YAML Linter / YAML Linter (push) Successful in 3m14s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Publish Public / build (push) Failing after 22m53s
Port ClearMetabolizerTypes, TryAddMetabolizerType, and TryRemoveMetabolizerType
methods from wega fork to enable vampire blood metabolism functionality.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 02:52:15 +01:00
cad2785a97 feat: add RemoveStaminaDamage method for vampire healing
Some checks failed
Publish / build (push) Failing after 6m14s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 10m53s
Build & Test Debug / Build & Test Debug (push) Has been skipped
RGA schema validator / YAML RGA schema validator (push) Successful in 1m2s
Test Packaging / Test Packaging (push) Successful in 6m9s
Map file schema validator / YAML map schema validator (push) Failing after 23s
RSI Validator / Validate RSIs (push) Successful in 58s
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 14m3s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
YAML Linter / YAML Linter (push) Successful in 3m13s
Port RemoveStaminaDamage from wega fork to enable proper stamina
reset in vampire abilities. Enables vampires and thralls to have
their stamina damage cleared during healing abilities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 02:24:01 +01:00
11219e47f0 fix: add missing prototypes and localization for Vampire/Blood Brothers
Some checks failed
Publish / build (push) Successful in 6m44s
Test Packaging / Test Packaging (push) Failing after 1m21s
RGA schema validator / YAML RGA schema validator (push) Failing after 32s
RSI Validator / Validate RSIs (push) Successful in 31s
Map file schema validator / YAML map schema validator (push) Failing after 23s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 11m4s
Build & Test Debug / Build & Test Debug (push) Has been skipped
YAML Linter / YAML Linter (push) Successful in 3m11s
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 14m32s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 5s
- Add Vampire NPC faction prototype (ai_factions.yml)
- Add Counter alert category for vampire blood counter
- Add WeaponVampireClaws entity for vampire abilities
- Add guidebook entries for Vampires and BloodBrothers
- Add localization for metabolizer-type-vampire and guidebook entries
- Add missing null.ogg audio for hallucinations
- Remove non-existent Android/Ipc components from blacklists
- Remove references to missing StealTargetGroups (GlovesKravMaga, WeaponBlueLaserPistol)
- Remove references to missing BlueShieldOfficer job

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 02:02:17 +01:00
1efd22e648 fix: comment out metabolizer manipulation to fix RA0002 permission error
Some checks failed
Test Packaging / Test Packaging (push) Successful in 5m51s
RGA schema validator / YAML RGA schema validator (push) Failing after 27s
RSI Validator / Validate RSIs (push) Successful in 27s
Map file schema validator / YAML map schema validator (push) Failing after 25s
YAML Linter / YAML Linter (push) Failing after 3m4s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 12m14s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 14m45s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
Publish / build (push) Failing after 18s
wega fork has ClearMetabolizerTypes/TryAddMetabolizerType methods that wylab lacks.
Direct field access to MetabolizerTypes violates Robust Analyzer permissions.
Commented out for later implementation when metabolizer API is ported.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 01:18:28 +01:00
4213d083f9 fix: stub out missing wega APIs in Vampire system
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 2m19s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Failing after 1m12s
RGA schema validator / YAML RGA schema validator (push) Failing after 24s
RSI Validator / Validate RSIs (push) Successful in 26s
Map file schema validator / YAML map schema validator (push) Failing after 26s
YAML Linter / YAML Linter (push) Failing after 2m9s
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 7m15s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 7m30s
- RemoveStaminaDamage -> commented (3 occurrences)
- MobState.PreCritical -> use Critical instead
- SyntheticOperatedComponent -> commented (2 occurrences)
- MovementModStatusSystem.Slowdown -> use FlashSlowdown
- DnaModifiedComponent -> commented
- MetabolizerSystem.ClearMetabolizerTypes/TryAddMetabolizerType -> direct HashSet manipulation

These features can be implemented later when porting the respective systems from wega.
2025-12-17 01:06:48 +01:00
34bba67a59 fix: comment out missing Surgery/Genetics imports for later implementation
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 2m20s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Failing after 2m1s
RGA schema validator / YAML RGA schema validator (push) Failing after 24s
RSI Validator / Validate RSIs (push) Successful in 25s
Map file schema validator / YAML map schema validator (push) Failing after 27s
YAML Linter / YAML Linter (push) Failing after 2m8s
Publish / build (push) Failing after 7m57s
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 8m18s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
2025-12-17 00:52:52 +01:00
f1d4f62047 feat: add Vampire antagonist from wega fork
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 2m10s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Failing after 1m57s
RGA schema validator / YAML RGA schema validator (push) Failing after 25s
RSI Validator / Validate RSIs (push) Successful in 28s
Map file schema validator / YAML map schema validator (push) Failing after 26s
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 7m30s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
YAML Linter / YAML Linter (push) Failing after 2m12s
Publish / build (push) Failing after 7m32s
Vampires are solo antagonists who must drink blood to gain power.
Features 4 class archetypes (Hemomancer, Umbrae, Gargantua, Dantalion)
each with 8 unique abilities unlocked through blood consumption.

Includes:
- Core vampire system with blood drinking mechanics
- 4 class archetypes with 32 total abilities
- Thrall system for Dantalion class
- Blood economy & skill progression
- Holy/unholy damage interactions
- Hallucinations system dependency
- Client UI for class selection
- 40+ sprite assets
- Russian localization

Also adds Hallucinations system (used by Mass Hysteria ability).

Requirements: 15h playtime
Min players: 20

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 00:41:55 +01:00
6152a7b166 feat: add Blood Brothers antagonist from wega fork
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 58s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 2m36s
RGA schema validator / YAML RGA schema validator (push) Failing after 23s
RSI Validator / Validate RSIs (push) Successful in 28s
Map file schema validator / YAML map schema validator (push) Failing after 24s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Blood Brothers are paired antagonists who must work together with shared
objectives. Two players are bonded and must:
- Escape together (mandatory)
- Complete shared kill/steal/protect objectives
- Both survive to win

Includes:
- 16 C# files (rule system, objective systems, components)
- Game rule and sub-gamemode prototypes
- 29 objective entity prototypes
- Russian localization
- Guidebook documentation

Requirements: 50h playtime + 25h Security
Min players: 20, Max antags: 8 (4 pairs)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 00:32:45 +01:00
cc7e88f934 revert: remove unused GolemHeat feature
Wylab doesn't have playable golem species - only hostile Flesh Golems.
This code does nothing without the GolemHeatComponent on entities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 00:16:45 +01:00
1bfd73febc revert: remove Barks and Height System features (missing dependencies)
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 3m2s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Failing after 1m44s
RGA schema validator / YAML RGA schema validator (push) Failing after 25s
RSI Validator / Validate RSIs (push) Successful in 25s
Map file schema validator / YAML map schema validator (push) Failing after 23s
YAML Linter / YAML Linter (push) Successful in 3m49s
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 15m18s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 4s
Publish / build (push) Successful in 29m36s
- Barks depends on SoundInsulationSystem (not in wylab)
- Height System depends on SpeciesPrototype.MaxItemHeight and
  HumanoidAppearanceComponent.Height (not in wylab)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 23:45:38 +01:00
d8f34b184f revert: remove Sleep on Buckle feature (requires wl-specific StandingStateComponent)
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 2m59s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Failing after 2m32s
RGA schema validator / YAML RGA schema validator (push) Failing after 24s
RSI Validator / Validate RSIs (push) Successful in 25s
Map file schema validator / YAML map schema validator (push) Failing after 25s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 7m5s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 7m13s
YAML Linter / YAML Linter (push) Failing after 2m5s
This feature depends on StandingStateComponent.SleepAction which doesn't exist
in wylab. Removing instead of modifying base components.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 23:33:51 +01:00
73b0052649 fix: remove StandingStateComponent.SleepAction dependency in SleepOnBuckle
Some checks failed
Publish / build (push) Failing after 2m17s
Test Packaging / Test Packaging (push) Failing after 1m17s
RGA schema validator / YAML RGA schema validator (push) Failing after 24s
RSI Validator / Validate RSIs (push) Successful in 27s
Map file schema validator / YAML map schema validator (push) Failing after 30s
YAML Linter / YAML Linter (push) Failing after 2m5s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
wylab's StandingStateComponent doesn't have SleepAction property (added by wl fork).
Removed cleanup code that referenced it - feature works without it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 23:26:40 +01:00
9ae469991f feat: add 11 copy-paste features from ss14-wega and ss14-wl
Some checks failed
Publish / build (push) Failing after 2m19s
Test Packaging / Test Packaging (push) Failing after 2m1s
RGA schema validator / YAML RGA schema validator (push) Failing after 25s
RSI Validator / Validate RSIs (push) Successful in 26s
Map file schema validator / YAML map schema validator (push) Failing after 23s
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 6m30s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 6m36s
Build & Test Debug / Build & Test Debug (push) Has been skipped
YAML Linter / YAML Linter (push) Failing after 1m16s
From ss14-wega (_Wega):
- DeleteOnDrop: auto-delete items when dropped
- FriendlyFaction: prevent friendly fire by faction
- NullRod: holy weapon that removes magic
- EdibleMatter: edible entity component
- Ghost Respawn: allow ghosts to respawn to lobby
- Barks: NPC voice sounds system (99 audio files)

From ss14-wl (_WL):
- Day/Night Cycle: automatic lighting cycle for maps
- Sleep on Buckle: sleep action when buckled
- Height System: tall entities become large items
- Freeze Component: freeze entities at high cold damage
- Suckable Food: mouth-slot consumables (lollipops, gum, etc.)
- GolemHeat: bonus feature (heat mechanics for golems)

Includes:
- 34 C# files
- 99 audio files
- 68 texture files
- 9 prototype files
- 2 locale files
- WegaCVars configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 23:13:24 +01:00
2a8530eaf6 feat: switch wiki workflow to MediaWiki with jtmullen/mediawiki-edit-action
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 3m36s
Test Packaging / Test Packaging (push) Failing after 5m59s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 10m58s
Build & Test Debug / Build & Test Debug (push) Has been skipped
RGA schema validator / YAML RGA schema validator (push) Successful in 1m4s
Update Wiki / Build and Publish JSON blobs to wiki (push) Failing after 2m43s
Map file schema validator / YAML map schema validator (push) Failing after 26s
RSI Validator / Validate RSIs (push) Successful in 56s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 3s
YAML Linter / YAML Linter (push) Successful in 3m20s
Publish / build (push) Successful in 27m44s
Replace Gitea wiki API with MediaWiki API using jtmullen/mediawiki-edit-action@v0.1.1.
Pages will be uploaded to User:WikiBot/<filename>.json following Corvax pattern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 20:00:45 +01:00
93f160652e fix: use PATCH instead of PUT for Gitea wiki API
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 31s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Successful in 5m41s
Update Wiki / Build and Publish JSON blobs to wiki (push) Successful in 2m31s
RGA schema validator / YAML RGA schema validator (push) Failing after 26s
RSI Validator / Validate RSIs (push) Successful in 30s
Map file schema validator / YAML map schema validator (push) Failing after 23s
YAML Linter / YAML Linter (push) Failing after 29s
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 13m51s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 4s
Publish / build (push) Successful in 28m24s
Gitea's wiki API requires PATCH for updates, not PUT.
PUT returns HTTP 405 (Method Not Allowed) which curl ignores
by default, so uploads were silently failing.

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 17:49:17 +01:00
3aaa315702 debug: add file listing to update-wiki workflow
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 1m44s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Publish / build (push) Successful in 6m4s
Test Packaging / Test Packaging (push) Failing after 3m52s
RGA schema validator / YAML RGA schema validator (push) Failing after 23s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 12m0s
Build & Test Debug / Build & Test Debug (push) Has been skipped
RSI Validator / Validate RSIs (push) Successful in 27s
YAML Linter / YAML Linter (push) Successful in 2m49s
Update Wiki / Build and Publish JSON blobs to wiki (push) Successful in 7m11s
Map file schema validator / YAML map schema validator (push) Successful in 10m24s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 17:08:39 +01:00
497d6d46eb fix: remove cache-dotnet from all workflows to prevent OOM
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 2m40s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Failing after 2m12s
Update Wiki / Build and Publish JSON blobs to wiki (push) Successful in 2m14s
RGA schema validator / YAML RGA schema validator (push) Failing after 23s
RSI Validator / Validate RSIs (push) Successful in 30s
Map file schema validator / YAML map schema validator (push) Failing after 22s
YAML Linter / YAML Linter (push) Successful in 3m27s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Publish / build (push) Has been cancelled
The actions/cache@v4 tar compression with zstdmt causes OOM (exit code
137) on runners with limited memory. Removing cache entirely as a
temporary fix until a better solution is implemented.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 16:54:49 +01:00
d88e65e0b5 fix: remove cache step from publish.yml to prevent hangs
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 3m24s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Publish / build (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Cache action was hanging, blocking the entire workflow.
2025-12-16 16:46:16 +01:00
86a584c502 fix: invert Dispose() logic to only shutdown managers that were initialized
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 3m32s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Publish / build (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
In autogen mode (DestinationFile set), Init() returns early before
_dbManager.Init() is called. But Dispose() was calling _dbManager.Shutdown()
specifically when DestinationFile was set, causing NullReferenceException.

Inverted the condition so shutdown only happens in normal mode when
managers were actually initialized.
2025-12-16 16:41:51 +01:00
0afe78f6c6 fix: pass ACTIONS_CACHE_URL from Gitea variable to cache action
Some checks failed
Publish / build (push) Failing after 2m42s
RGA schema validator / YAML RGA schema validator (push) Failing after 28s
RSI Validator / Validate RSIs (push) Successful in 36s
Map file schema validator / YAML map schema validator (push) Failing after 23s
Test Packaging / Test Packaging (push) Failing after 13m8s
Build & Test Debug / build (ubuntu-latest) (push) Failing after 17m52s
Build & Test Debug / Build & Test Debug (push) Has been skipped
YAML Linter / YAML Linter (push) Successful in 3m8s
Build & Test Map Renderer / build (ubuntu-latest) (push) Successful in 20m36s
Build & Test Map Renderer / Build & Test Debug (push) Successful in 2s
2025-12-16 16:05:47 +01:00
42ad3b8313 fix: use file-based curl input to avoid ARG_MAX limit in wiki uploads
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 1m4s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Test Packaging / Test Packaging (push) Failing after 5m45s
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 10m15s
Publish / build (push) Failing after 10m11s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Map file schema validator / YAML map schema validator (push) Failing after 28s
YAML Linter / YAML Linter (push) Failing after 22s
RSI Validator / Validate RSIs (push) Successful in 1m16s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m23s
Update Wiki / Build and Publish JSON blobs to wiki (push) Failing after 7m18s
The previous implementation stored base64 content in shell variables,
which exceeded the ~2MB ARG_MAX limit when JSON files were large.

Now writes payload to a temp file and uses curl -d @file instead.
2025-12-16 15:49:15 +01:00
72b0f113ae ci: self-host benchmarks and wiki workflows
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
RSI Validator / Validate RSIs (push) Successful in 1m5s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m12s
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Test Packaging / Test Packaging (push) Successful in 7m23s
Update Wiki / Build and Publish JSON blobs to wiki (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Successful in 10m37s
YAML Linter / YAML Linter (push) Successful in 15m15s
Publish Testing / build (push) Has been cancelled
Publish / build (push) Has been cancelled
- benchmarks.yml: Run locally with PostgreSQL instead of SSH to centcomm
- update-wiki.yml: Use Gitea wiki API instead of MediaWiki

Required secrets:
- BENCHMARKS_SQL_HOST, BENCHMARKS_SQL_PORT, BENCHMARKS_SQL_USER
- BENCHMARKS_SQL_PASSWORD, BENCHMARKS_SQL_DATABASE
- WIKI_TOKEN (Gitea API token with wiki write access)
2025-12-16 09:43:00 +01:00
57bc9cca27 fix: revert cache probe logic that caused YAML parse error
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 9m28s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 11m25s
Build & Test Debug / Build & Test Debug (push) Has been skipped
RGA schema validator / YAML RGA schema validator (push) Successful in 1m0s
RSI Validator / Validate RSIs (push) Successful in 59s
Map file schema validator / YAML map schema validator (push) Successful in 10m23s
Test Packaging / Test Packaging (push) Failing after 8m13s
Publish / build (push) Successful in 31m9s
YAML Linter / YAML Linter (push) Successful in 10m18s
Benchmarks / Run Benchmarks (push) Failing after 57s
Root cause: Gitea Actions' YAML parser doesn't support heredoc syntax
(python - <<'PY') in composite action run blocks.

Error was: "yaml: line 18: could not find expected ':'"

Solution: Use simple cache action - the cache handles 502 errors
gracefully with warnings, so probing is unnecessary.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 03:30:49 +01:00
251d29f8d3 fix: use public CDN URL for remote runner compatibility
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 46s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 1m4s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 45s
RGA schema validator / YAML RGA schema validator (push) Successful in 34s
Test Packaging / Test Packaging (push) Failing after 1m7s
RSI Validator / Validate RSIs (push) Successful in 26s
YAML Linter / YAML Linter (push) Failing after 40s
Map file schema validator / YAML map schema validator (push) Has been cancelled
- Set ROBUST_CDN_URL to https://cdn.wylab.me/ for remote runner access
- Add cache server availability probing to prevent 502 errors
- Update publish script to respect ROBUST_CDN_URL environment variable
- Fix Docker trigger dispatch API endpoint format

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 03:11:48 +01:00
Wylabb
b9ebcaa1a2 ci: switch to Gitea-native caching
Some checks failed
Build & Test Debug / build (ubuntu-latest) (push) Failing after 29m47s
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 29m49s
RGA schema validator / YAML RGA schema validator (push) Successful in 1m35s
RSI Validator / Validate RSIs (push) Successful in 1m2s
Test Packaging / Test Packaging (push) Failing after 24m42s
Map file schema validator / YAML map schema validator (push) Successful in 10m53s
Update Wiki / Build and Publish JSON blobs to wiki (push) Failing after 24m32s
YAML Linter / YAML Linter (push) Successful in 23m25s
Benchmarks / Run Benchmarks (push) Failing after 49s
Publish Testing / build (push) Failing after 39m4s
Publish / build (push) Failing after 1s
Publish Public / build (push) Failing after 17m27s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Replace actions/cache@v4 with v3 and add RUNNER_TOOL_CACHE to use Gitea's native cache infrastructure. The v4 cache was incompatible with Gitea and caused 5-minute delays.

Changes:
- Use actions/cache@v3 (Gitea-compatible)
- Add RUNNER_TOOL_CACHE=/toolcache to all build jobs
- Update cache key to use github.run_id instead of hashFiles

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 05:12:55 +01:00
Wylabb
d2f3e0929c ci: cache dotnet dependencies
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 6m29s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Test Packaging / Test Packaging (push) Failing after 10m15s
Update Wiki / Build and Publish JSON blobs to wiki (push) Failing after 10m14s
Map file schema validator / YAML map schema validator (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Successful in 59s
RSI Validator / Validate RSIs (push) Successful in 40s
YAML Linter / YAML Linter (push) Failing after 12m5s
Publish / build (push) Failing after 0s
Publish Public / build (push) Failing after 1s
2025-12-14 12:33:30 +01:00
Wylabb
c702820686 ci: fix pip install for validate rsis
Some checks failed
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Publish / build (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
2025-12-14 12:26:51 +01:00
Wylabb
1ca32d40cf chore: trigger CI
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 4s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 4s
Map file schema validator / YAML map schema validator (push) Successful in 3m12s
RSI Validator / Validate RSIs (push) Failing after 30s
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
2025-12-14 11:41:51 +01:00
Wylabb
af791c5fb7 Test without go script
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 3s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 3s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 2s
RGA schema validator / YAML RGA schema validator (push) Failing after 2s
RSI Validator / Validate RSIs (push) Failing after 2s
Map file schema validator / YAML map schema validator (push) Failing after 2s
YAML Linter / YAML Linter (push) Failing after 3s
2025-12-14 11:17:19 +01:00
Wylabb
ff48caac72 Test after persistent DNS config
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 3s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 3s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 2s
RGA schema validator / YAML RGA schema validator (push) Failing after 3s
RSI Validator / Validate RSIs (push) Failing after 2s
Map file schema validator / YAML map schema validator (push) Failing after 3s
YAML Linter / YAML Linter (push) Failing after 2s
2025-12-14 11:15:34 +01:00
Wylabb
62cf5e8dcc Test workflow auto-trigger after DNS fix
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 3s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 3s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 2s
RGA schema validator / YAML RGA schema validator (push) Failing after 2s
RSI Validator / Validate RSIs (push) Failing after 2s
Map file schema validator / YAML map schema validator (push) Failing after 2s
YAML Linter / YAML Linter (push) Failing after 3s
2025-12-14 11:12:56 +01:00
Wylabb
2ebc70e24b Remove DNS workaround from workflow
Some checks failed
YAML Linter / YAML Linter (push) Waiting to run
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 57s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 1m3s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 58s
Test Packaging / Test Packaging (push) Failing after 51s
RGA schema validator / YAML RGA schema validator (push) Failing after 53s
RSI Validator / Validate RSIs (push) Failing after 51s
Map file schema validator / YAML map schema validator (push) Failing after 1m4s
Runner now uses host networking which resolves DNS issues properly
for all job containers.
2025-12-14 11:00:57 +01:00
Wylabb
24336d6df5 Add DNS workaround for runner job containers
Some checks failed
Map file schema validator / YAML map schema validator (push) Waiting to run
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 52s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 1m0s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 58s
Test Packaging / Test Packaging (push) Failing after 1m0s
RGA schema validator / YAML RGA schema validator (push) Failing after 1m2s
RSI Validator / Validate RSIs (push) Failing after 57s
YAML Linter / YAML Linter (push) Failing after 1m6s
Publish Testing / build (push) Failing after 1m5s
Adds git.wylab.me to /etc/hosts before checkout to work around
DNS resolution issues in Gitea Actions job containers.
2025-12-14 10:53:36 +01:00
Wylabb
147e6c9ce6 Test workflow auto-trigger
Some checks failed
Build & Test Map Renderer / build (ubuntu-latest) (push) Failing after 1m16s
Build & Test Map Renderer / Build & Test Debug (push) Has been skipped
Build & Test Debug / build (ubuntu-latest) (push) Failing after 58s
Build & Test Debug / Build & Test Debug (push) Has been skipped
Publish / build (push) Failing after 59s
RGA schema validator / YAML RGA schema validator (push) Failing after 1m2s
RSI Validator / Validate RSIs (push) Failing after 58s
Map file schema validator / YAML map schema validator (push) Failing after 57s
YAML Linter / YAML Linter (push) Failing after 1m2s
2025-12-14 10:28:01 +01:00
Wylabb
40c89f04ad Revert workflow to last working version
The CDN check and caching additions were breaking Gitea's workflow parser,
preventing automatic workflow triggers on push events.

Reverting to the last known working version at bce50cad4c.
2025-12-14 10:26:18 +01:00
Wylabb
8fe3a33813 Fix server launcher connectivity by including Content.Packaging assembly
The server was failing to create ACZ packages for launcher clients because
Content.Packaging.dll was not included in the packaged server deployments.
This caused a FileNotFoundException when ContentMagicAczProvider attempted
to call ClientPackaging.WriteResources at runtime.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 10:15:07 +01:00
Codex Bot
c190b3a8af Cache NuGet and RobustToolbox outputs 2025-12-14 08:08:58 +01:00
bce50cad4c Update .github/workflows/publish.yml 2025-12-14 07:21:21 +01:00
506ba9fcd2 Update .github/workflows/publish.yml 2025-12-14 07:18:53 +01:00
100e967e86 Update .github/workflows/publish.yml 2025-12-14 07:16:41 +01:00
Codex Bot
fa969b0315 Wire publish workflow to wylab CDN 2025-12-14 06:17:56 +01:00
355 changed files with 12202 additions and 378 deletions

15
.github/actions/cache-dotnet/action.yml vendored Normal file
View 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-

View File

@@ -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

View File

@@ -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

View File

@@ -16,6 +16,8 @@ jobs:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout Master

View File

@@ -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:

View File

@@ -9,13 +9,56 @@ 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..."
curl -s -X DELETE -H "Authorization: token $API_TOKEN" \
"$API_URL/repos/$REPO_OWNER/$REPO_NAME/issues/$PR_INDEX/labels/$LABEL_NAME"
echo "Conflict label removed."
fi
fi

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -6,18 +6,23 @@ concurrency:
on:
workflow_dispatch:
push:
branches:
- master
schedule:
- cron: '0 1 * * *'
jobs:
build:
runs-on: ubuntu-latest
env:
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'}}
run: exit 1
# - name: Install dependencies
# run: sudo apt-get install -y python3-paramiko python3-lxml
- name: Install Python dependencies
run: pip install --break-system-packages requests paramiko lxml
- uses: actions/checkout@v4.2.2
with:
@@ -70,8 +75,26 @@ jobs:
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/
- name: Trigger Docker image rebuild
if: ${{ success() }}
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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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 }}

View File

@@ -33,7 +33,7 @@ jobs:
uses: space-wizards/submodule-dependency@v0.1.5
- name: Install Python dependencies
run: |
pip3 install --ignore-installed --user pillow jsonschema
python3 -m pip install --user --break-system-packages pillow jsonschema
- name: Validate RSIs
run: |
python3 RobustToolbox/Schemas/validate_rsis.py Resources/

View File

@@ -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

View File

@@ -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";
}

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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")),
];
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>()

View File

@@ -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")),
];
}
}

View File

@@ -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),
];
}
}

View File

@@ -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";

View File

@@ -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>

View File

@@ -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;
}
}
}

View 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();
}
}

View 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>

View 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();
}
}

View 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;
}
}
}

View 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}");
}
}

View File

@@ -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]

View File

@@ -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));
}

View File

@@ -47,6 +47,7 @@ public static class ServerPackaging
"Content.Server",
"Content.Shared",
"Content.Shared.Database",
"Content.Packaging",
};
private static readonly List<string> ServerExtraAssemblies = new()

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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
));

View File

@@ -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();

View 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];
}
}
}
}

View 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;
}
}

View 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;
}
}
}

View File

@@ -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";
}
}

View 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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View 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);
}
}
}

View 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);
}
}

View File

@@ -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;
}
}
}
}
}

View 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");
}
}

View File

@@ -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;
}

View File

@@ -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
{
}

View 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);
}
}
}
}

View 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;
}
}

View 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);
}
}
}
}

View 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);
}
}

View 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);
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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
{
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View 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;

View 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;

File diff suppressed because it is too large Load Diff

View 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
}

View File

@@ -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;
}
}

View File

@@ -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
{

View File

@@ -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!;

View 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;
}

View 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);
}

View 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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
namespace Content.Shared.Genetics;
[RegisterComponent]
public sealed partial class DnaModifiedComponent : Component;

View 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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,55 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Hallucinations;
/// <summary>
/// Packs of entities that can become a hallucination
/// </summary>
[Serializable, Prototype("hallucinationsPack")]
public sealed partial class HallucinationsPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// List of prototypes that are spawned as a hallucination.
/// </summary>
[DataField]
public List<EntProtoId> Entities = new();
/// <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.8f;
/// <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;
}

View File

@@ -0,0 +1,11 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Interaction.Components
{
[RegisterComponent, NetworkedComponent]
public sealed partial class DeleteOnDropComponent : Component
{
[DataField("deleteOnDrop")]
public bool DeleteOnDrop = true;
}
}

View File

@@ -0,0 +1,13 @@
using Content.Shared.FixedPoint;
namespace Content.Shared.NullRod.Components;
[RegisterComponent]
public sealed partial class NullRodComponent : Component
{
[DataField]
public FixedPoint2 FirstNullDamage = 30;
[DataField]
public FixedPoint2 NullDamage = 15;
}

View File

@@ -0,0 +1,6 @@
using Content.Shared.FixedPoint;
namespace Content.Shared.NullRod.Components;
[RegisterComponent]
public sealed partial class NullRodOwnerComponent : Component;

View File

@@ -0,0 +1,9 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
[RegisterComponent, Access(typeof(BloodConditionSystem))]
public sealed partial class BloodConditionComponent : Component
{
public Dictionary<EntityUid, float> BloodTargets = new();
}

View File

@@ -0,0 +1,60 @@
using Content.Shared.Objectives.Components;
using Content.Shared.Vampire.Components;
using Content.Server.Objectives.Components;
using Robust.Shared.Random;
namespace Content.Server.Objectives.Systems;
public sealed class BloodConditionSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BloodConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<BloodConditionComponent, ObjectiveAssignedEvent>(OnAssigned);
SubscribeLocalEvent<BloodConditionComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
}
private void OnAssigned(EntityUid uid, BloodConditionComponent comp, ref ObjectiveAssignedEvent args)
{
if (args.Mind.OwnedEntity.HasValue)
{
var ownedEntity = args.Mind.OwnedEntity.Value;
comp.BloodTargets[ownedEntity] = _random.Next(200, 300);
}
}
private void OnAfterAssign(EntityUid uid, BloodConditionComponent comp, ref ObjectiveAfterAssignEvent args)
{
if (args.Mind.OwnedEntity.HasValue)
{
var ownedEntity = args.Mind.OwnedEntity.Value;
var description = Loc.GetString("objective-condition-blood-description", ("condition", comp.BloodTargets[ownedEntity]));
_metaData.SetEntityDescription(uid, description, args.Meta);
}
}
private void OnGetProgress(EntityUid uid, BloodConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
if (args.Mind.OwnedEntity.HasValue)
{
var ownedEntity = args.Mind.OwnedEntity.Value;
args.Progress = GetProgress(ownedEntity, comp);
}
}
private float GetProgress(EntityUid uid, BloodConditionComponent comp)
{
if (!TryComp<VampireComponent>(uid, out var vampireComponent))
return 0f;
float targetBlood = comp.BloodTargets.GetValueOrDefault(uid, 0);
float bloodDrank = vampireComponent.TotalBloodDrank;
return bloodDrank >= targetBlood ? 1f : bloodDrank / targetBlood;
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Shared.Surgery.Components;
[RegisterComponent]
public sealed partial class SyntheticOperatedComponent : Component
{
}

View File

@@ -0,0 +1,6 @@
namespace Content.Shared.Vampire;
public abstract class SharedVampireSystem : EntitySystem
{
// Clear
}

View File

@@ -0,0 +1,15 @@
using Content.Shared.StatusIcon;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Vampire.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class ThrallComponent : Component
{
[DataField]
public EntityUid? VampireOwner = null;
[DataField]
public ProtoId<FactionIconPrototype> StatusIcon = "ThrallFaction";
}

View File

@@ -0,0 +1,117 @@
using Content.Shared.Alert;
using Content.Shared.Body.Prototypes;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.StatusIcon;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Vampire.Components;
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class VampireComponent : Component
{
public static readonly ProtoId<MetabolizerTypePrototype> MetabolizerVampire = "Vampire";
public static readonly DamageSpecifier HolyDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>() { { "Heat", 10 } }
};
public static readonly DamageSpecifier SpaceDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>() { { "Heat", 2.5 } }
};
public static readonly EntProtoId DrinkActionPrototype = "ActionDrinkBlood";
public static readonly EntProtoId SelectClassActionPrototype = "ActionVampireSelectClass";
public static readonly EntProtoId RejuvenateActionPrototype = "ActionVampireRejuvenate";
public static readonly EntProtoId GlareActionPrototype = "ActionVampireGlare";
public readonly SoundSpecifier BloodDrainSound = new SoundPathSpecifier(
"/Audio/Items/drink.ogg",
new AudioParams() { Volume = -3f, MaxDistance = 3f }
);
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string? CurrentEvolution { get; set; }
/// <summary>
/// The current amount of blood in the vampire's account.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public FixedPoint2 CurrentBlood = 0;
[DataField("vampireStatusIcon")]
public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "VampireFaction";
[DataField]
public ProtoId<AlertPrototype> BloodAlert = "BloodAlert";
/// <summary>
/// Fields for counting the total amount of blood consumed after the end of the round
/// </summary>
public float TotalBloodDrank = 0;
public float NextSpaceDamageTick { get; set; }
public float NextNullDamageTick { get; set; }
public bool TruePowerActive = false;
public bool PowerActive = false;
public bool IsDamageSharingActive = false;
[DataField]
public FixedPoint2 NullDamage = 0;
[DataField]
public int ThrallCount = 0;
[DataField]
public int MaxThrallCount = 1;
[DataField, ViewVariables(VVAccess.ReadOnly)]
public List<EntityUid> ThrallOwned = new();
[DataField, ViewVariables(VVAccess.ReadOnly)]
public List<string> AcquiredSkills = new List<string>();
}
/// <summary>
/// Marks an entity as taking damage when hit by a bible, rather than being healed
/// </summary>
[RegisterComponent]
public sealed partial class UnholyComponent : Component;
[RegisterComponent]
public sealed partial class BeaconSoulComponent : Component
{
[DataField]
public EntityUid VampireOwner = EntityUid.Invalid;
}
/// <summary>
/// A component for testing vampire arson near holy sites.
/// </summary>
[RegisterComponent]
public sealed partial class HolyPointComponent : Component
{
[DataField]
public float Range = 6f;
public float NextTimeTick { get; set; }
}
[Serializable, NetSerializable]
public enum VampireVisualLayers : byte
{
Digit1,
Digit2,
Digit3
}

View File

@@ -0,0 +1,193 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Vampire;
// Base
public sealed partial class VampireSelectClassActionEvent : InstantActionEvent { }
public sealed partial class VampireRejuvenateActionEvent : InstantActionEvent { }
public sealed partial class VampireGlareActionEvent : EntityTargetActionEvent { }
public sealed partial class VampireDrinkingBloodActionEvent : EntityTargetActionEvent { }
[Serializable, NetSerializable]
public sealed partial class VampireDrinkingBloodDoAfterEvent : SimpleDoAfterEvent
{
[DataField]
public float Volume = 0;
}
[Serializable, NetSerializable]
public sealed class SelectClassPressedEvent : EntityEventArgs
{
public NetEntity Uid { get; }
public SelectClassPressedEvent(NetEntity uid)
{
Uid = uid;
}
}
[Serializable, NetSerializable]
public sealed class VampireSelectClassMenuClosedEvent : EntityEventArgs
{
public NetEntity Uid { get; }
public string SelectedClass { get; }
public VampireSelectClassMenuClosedEvent(NetEntity uid, string selectedClass)
{
Uid = uid;
SelectedClass = selectedClass;
}
}
// Hemomancer Abilities
public sealed partial class VampireClawsActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodTentacleAction : WorldTargetActionEvent
{
[DataField]
public EntProtoId EntityId = "EffectBloodTentacleSpawn";
[DataField]
public List<Direction> OffsetDirections = new()
{
Direction.North,
Direction.South,
Direction.East,
Direction.West,
Direction.NorthEast,
Direction.NorthWest,
Direction.SouthEast,
Direction.SouthWest,
};
[DataField]
public int ExtraSpawns = 8;
}
public sealed partial class VampireBloodBarrierActionEvent : WorldTargetActionEvent
{
[DataField]
public EntProtoId EntityId = "BloodBarrier";
public bool UseCasterDirection { get; set; } = true;
}
public sealed partial class VampireSanguinePoolActionEvent : InstantActionEvent
{
[DataField]
public string PolymorphProto = "VampireBlood";
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
}
public sealed partial class VampirePredatorSensesActionEvent : InstantActionEvent
{
[DataField]
public string Proto = "PuddleBlood";
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
}
public sealed partial class VampireBloodEruptionActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodBringersRiteActionEvent : InstantActionEvent
{
[DataField]
public string Proto = "PuddleBlood";
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
}
// Umbrae Abilities
public sealed partial class VampireCloakOfDarknessActionEvent : InstantActionEvent { }
public sealed partial class VampireShadowSnareActionEvent : WorldTargetActionEvent
{
[DataField]
public EntProtoId EntityId = "ShadowTrap";
}
public sealed partial class VampireSoulAnchorActionEvent : InstantActionEvent { }
[Serializable, NetSerializable]
public sealed partial class SoulAnchorDoAfterEvent : SimpleDoAfterEvent { }
public sealed partial class VampireDarkPassageActionEvent : WorldTargetActionEvent { }
public sealed partial class VampireExtinguishActionEvent : InstantActionEvent { }
public sealed partial class VampireShadowBoxingActionEvent : EntityTargetActionEvent { }
public sealed partial class VampireEternalDarknessActionEvent : InstantActionEvent { }
[Serializable, NetSerializable]
public sealed partial class VampireToggleFovEvent : EntityEventArgs
{
public NetEntity User { get; }
public VampireToggleFovEvent(NetEntity user)
{
User = user;
}
}
// Gargantua Abilities
public sealed partial class VampireRejuvenateAdvancedActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodSwellActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodRushActionEvent : InstantActionEvent { }
public sealed partial class VampireSeismicStompActionEvent : InstantActionEvent
{
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Footsteps/largethud.ogg");
}
public sealed partial class VampireBloodSwellAdvancedActionEvent : InstantActionEvent { }
public sealed partial class VampireOverwhelmingForceActionEvent : InstantActionEvent { }
public sealed partial class VampireDemonicGraspActionEvent : EntityTargetActionEvent { }
public sealed partial class VampireChargeActionEvent : WorldTargetActionEvent
{
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Footsteps/largethud.ogg");
}
// Dantalion Abilities
public sealed partial class MaxThrallCountUpdateEvent : InstantActionEvent { }
public sealed partial class VampireEnthrallActionEvent : EntityTargetActionEvent { }
[Serializable, NetSerializable]
public sealed partial class EnthrallDoAfterEvent : SimpleDoAfterEvent
{
public new NetEntity Target { get; set; }
public EnthrallDoAfterEvent(NetEntity target)
{
Target = target;
}
}
public sealed partial class VampireCommuneActionEvent : InstantActionEvent { }
public sealed partial class VampirePacifyActionEvent : EntityTargetActionEvent { }
public sealed partial class VampireSubspaceSwapActionEvent : EntityTargetActionEvent { }
//public sealed partial class VampireDeployDecoyActionEvent : InstantActionEvent { }
public sealed partial class VampireRallyThrallsActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodBondActionEvent : InstantActionEvent { }
public sealed partial class VampireMassHysteriaActionEvent : InstantActionEvent { }

Some files were not shown because too many files have changed in this diff Show More