Wizard Staff of Animation (#34649)

This commit is contained in:
ActiveMammmoth
2025-02-12 23:46:02 -05:00
committed by GitHub
parent 0bb6f1979d
commit 9fad86342f
26 changed files with 272 additions and 36 deletions

View File

@@ -88,6 +88,7 @@ namespace Content.Client.Actions
return; return;
component.Whitelist = state.Whitelist; component.Whitelist = state.Whitelist;
component.Blacklist = state.Blacklist;
component.CanTargetSelf = state.CanTargetSelf; component.CanTargetSelf = state.CanTargetSelf;
BaseHandleState<EntityTargetActionComponent>(uid, component, state); BaseHandleState<EntityTargetActionComponent>(uid, component, state);
} }

View File

@@ -1,4 +1,4 @@
using Content.Shared.Magic; using Content.Shared.Magic;
using Content.Shared.Magic.Events; using Content.Shared.Magic.Events;
namespace Content.Client.Magic; namespace Content.Client.Magic;

View File

@@ -32,6 +32,11 @@ public sealed class TargetOutlineSystem : EntitySystem
/// </summary> /// </summary>
public EntityWhitelist? Whitelist = null; public EntityWhitelist? Whitelist = null;
/// <summary>
/// Blacklist that the target must satisfy.
/// </summary>
public EntityWhitelist? Blacklist = null;
/// <summary> /// <summary>
/// Predicate the target must satisfy. /// Predicate the target must satisfy.
/// </summary> /// </summary>
@@ -93,15 +98,16 @@ public sealed class TargetOutlineSystem : EntitySystem
RemoveHighlights(); RemoveHighlights();
} }
public void Enable(float range, bool checkObstructions, Func<EntityUid, bool>? predicate, EntityWhitelist? whitelist, CancellableEntityEventArgs? validationEvent) public void Enable(float range, bool checkObstructions, Func<EntityUid, bool>? predicate, EntityWhitelist? whitelist, EntityWhitelist? blacklist, CancellableEntityEventArgs? validationEvent)
{ {
Range = range; Range = range;
CheckObstruction = checkObstructions; CheckObstruction = checkObstructions;
Predicate = predicate; Predicate = predicate;
Whitelist = whitelist; Whitelist = whitelist;
Blacklist = blacklist;
ValidationEvent = validationEvent; ValidationEvent = validationEvent;
_enabled = Predicate != null || Whitelist != null || ValidationEvent != null; _enabled = Predicate != null || Whitelist != null || Blacklist != null || ValidationEvent != null;
} }
public override void Update(float frameTime) public override void Update(float frameTime)

View File

@@ -952,7 +952,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
var range = entityAction.CheckCanAccess ? action.Range : -1; var range = entityAction.CheckCanAccess ? action.Range : -1;
_interactionOutline?.SetEnabled(false); _interactionOutline?.SetEnabled(false);
_targetOutline?.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null); _targetOutline?.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, entityAction.Blacklist, null);
} }
/// <summary> /// <summary>

View File

@@ -1,4 +1,4 @@
using Content.Shared.Whitelist; using Content.Shared.Whitelist;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -25,6 +25,12 @@ public sealed partial class EntityTargetActionComponent : BaseTargetActionCompon
/// <remarks>No whitelist check when null.</remarks> /// <remarks>No whitelist check when null.</remarks>
[DataField("whitelist")] public EntityWhitelist? Whitelist; [DataField("whitelist")] public EntityWhitelist? Whitelist;
/// <summary>
/// Determines which entities are NOT valid targets for this action.
/// </summary>
/// <remarks>No blacklist check when null.</remarks>
[DataField] public EntityWhitelist? Blacklist;
/// <summary> /// <summary>
/// Whether this action considers the user as a valid target entity when using this action. /// Whether this action considers the user as a valid target entity when using this action.
/// </summary> /// </summary>
@@ -35,11 +41,13 @@ public sealed partial class EntityTargetActionComponent : BaseTargetActionCompon
public sealed class EntityTargetActionComponentState : BaseActionComponentState public sealed class EntityTargetActionComponentState : BaseActionComponentState
{ {
public EntityWhitelist? Whitelist; public EntityWhitelist? Whitelist;
public EntityWhitelist? Blacklist;
public bool CanTargetSelf; public bool CanTargetSelf;
public EntityTargetActionComponentState(EntityTargetActionComponent component, IEntityManager entManager) : base(component, entManager) public EntityTargetActionComponentState(EntityTargetActionComponent component, IEntityManager entManager) : base(component, entManager)
{ {
Whitelist = component.Whitelist; Whitelist = component.Whitelist;
Blacklist = component.Blacklist;
CanTargetSelf = component.CanTargetSelf; CanTargetSelf = component.CanTargetSelf;
} }
} }

View File

@@ -538,6 +538,7 @@ public abstract class SharedActionsSystem : EntitySystem
if (!ValidateEntityTargetBase(user, if (!ValidateEntityTargetBase(user,
target, target,
comp.Whitelist, comp.Whitelist,
comp.Blacklist,
comp.CheckCanInteract, comp.CheckCanInteract,
comp.CanTargetSelf, comp.CanTargetSelf,
comp.CheckCanAccess, comp.CheckCanAccess,
@@ -552,6 +553,7 @@ public abstract class SharedActionsSystem : EntitySystem
private bool ValidateEntityTargetBase(EntityUid user, private bool ValidateEntityTargetBase(EntityUid user,
EntityUid? targetEntity, EntityUid? targetEntity,
EntityWhitelist? whitelist, EntityWhitelist? whitelist,
EntityWhitelist? blacklist,
bool checkCanInteract, bool checkCanInteract,
bool canTargetSelf, bool canTargetSelf,
bool checkCanAccess, bool checkCanAccess,
@@ -563,6 +565,9 @@ public abstract class SharedActionsSystem : EntitySystem
if (_whitelistSystem.IsWhitelistFail(whitelist, target)) if (_whitelistSystem.IsWhitelistFail(whitelist, target))
return false; return false;
if (_whitelistSystem.IsBlacklistPass(blacklist, target))
return false;
if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target)) if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
return false; return false;
@@ -637,6 +642,7 @@ public abstract class SharedActionsSystem : EntitySystem
var entityValidated = ValidateEntityTargetBase(user, var entityValidated = ValidateEntityTargetBase(user,
entity, entity,
comp.Whitelist, comp.Whitelist,
null,
comp.CheckCanInteract, comp.CheckCanInteract,
comp.CanTargetSelf, comp.CanTargetSelf,
comp.CheckCanAccess, comp.CheckCanAccess,

View File

@@ -84,15 +84,18 @@ namespace Content.Shared.Damage
public sealed class DamageableComponentState : ComponentState public sealed class DamageableComponentState : ComponentState
{ {
public readonly Dictionary<string, FixedPoint2> DamageDict; public readonly Dictionary<string, FixedPoint2> DamageDict;
public readonly string? DamageContainerId;
public readonly string? ModifierSetId; public readonly string? ModifierSetId;
public readonly FixedPoint2? HealthBarThreshold; public readonly FixedPoint2? HealthBarThreshold;
public DamageableComponentState( public DamageableComponentState(
Dictionary<string, FixedPoint2> damageDict, Dictionary<string, FixedPoint2> damageDict,
string? damageContainerId,
string? modifierSetId, string? modifierSetId,
FixedPoint2? healthBarThreshold) FixedPoint2? healthBarThreshold)
{ {
DamageDict = damageDict; DamageDict = damageDict;
DamageContainerId = damageContainerId;
ModifierSetId = modifierSetId; ModifierSetId = modifierSetId;
HealthBarThreshold = healthBarThreshold; HealthBarThreshold = healthBarThreshold;
} }

View File

@@ -228,12 +228,12 @@ namespace Content.Shared.Damage
{ {
if (_netMan.IsServer) if (_netMan.IsServer)
{ {
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageModifierSetId, component.HealthBarThreshold); args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold);
} }
else else
{ {
// avoid mispredicting damage on newly spawned entities. // avoid mispredicting damage on newly spawned entities.
args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageModifierSetId, component.HealthBarThreshold); args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold);
} }
} }
@@ -266,6 +266,7 @@ namespace Content.Shared.Damage
return; return;
} }
component.DamageContainerID = state.DamageContainerId;
component.DamageModifierSetId = state.ModifierSetId; component.DamageModifierSetId = state.ModifierSetId;
component.HealthBarThreshold = state.HealthBarThreshold; component.HealthBarThreshold = state.HealthBarThreshold;

View File

@@ -0,0 +1,11 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Magic.Components;
// Used on whitelist for animate spell/wand
[RegisterComponent, NetworkedComponent]
public sealed partial class AnimateableComponent : Component
{
}

View File

@@ -0,0 +1,16 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
namespace Content.Shared.Magic.Events;
public sealed partial class AnimateSpellEvent : EntityTargetActionEvent, ISpeakSpell
{
[DataField]
public string? Speech { get; private set; }
[DataField]
public ComponentRegistry AddComponents = new();
[DataField]
public HashSet<string> RemoveComponents = new();
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Shared.Actions; using Content.Shared.Actions;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
@@ -26,7 +27,10 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager;
using Robust.Shared.Spawners; using Robust.Shared.Spawners;
@@ -80,6 +84,7 @@ public abstract class SharedMagicSystem : EntitySystem
SubscribeLocalEvent<RandomGlobalSpawnSpellEvent>(OnRandomGlobalSpawnSpell); SubscribeLocalEvent<RandomGlobalSpawnSpellEvent>(OnRandomGlobalSpawnSpell);
SubscribeLocalEvent<MindSwapSpellEvent>(OnMindSwapSpell); SubscribeLocalEvent<MindSwapSpellEvent>(OnMindSwapSpell);
SubscribeLocalEvent<VoidApplauseSpellEvent>(OnVoidApplause); SubscribeLocalEvent<VoidApplauseSpellEvent>(OnVoidApplause);
SubscribeLocalEvent<AnimateSpellEvent>(OnAnimateSpell);
} }
private void OnBeforeCastSpell(Entity<MagicComponent> ent, ref BeforeCastSpellEvent args) private void OnBeforeCastSpell(Entity<MagicComponent> ent, ref BeforeCastSpellEvent args)
@@ -298,22 +303,8 @@ public abstract class SharedMagicSystem : EntitySystem
ev.Handled = true; ev.Handled = true;
Speak(ev); Speak(ev);
foreach (var toRemove in ev.ToRemove) RemoveComponents(ev.Target, ev.ToRemove);
{ AddComponents(ev.Target, ev.ToAdd);
if (_compFact.TryGetRegistration(toRemove, out var registration))
RemComp(ev.Target, registration.Type);
}
foreach (var (name, data) in ev.ToAdd)
{
if (HasComp(ev.Target, data.Component.GetType()))
continue;
var component = (Component)_compFact.GetComponent(name);
var temp = (object)component;
_seriMan.CopyTo(data.Component, ref temp);
EntityManager.AddComponent(ev.Target, (Component)temp!);
}
} }
// End Change Component Spells // End Change Component Spells
#endregion #endregion
@@ -370,6 +361,29 @@ public abstract class SharedMagicSystem : EntitySystem
comp.Uid = performer; comp.Uid = performer;
} }
} }
private void AddComponents(EntityUid target, ComponentRegistry comps)
{
foreach (var (name, data) in comps)
{
if (HasComp(target, data.Component.GetType()))
continue;
var component = (Component)_compFact.GetComponent(name);
var temp = (object)component;
_seriMan.CopyTo(data.Component, ref temp);
EntityManager.AddComponent(target, (Component)temp!);
}
}
private void RemoveComponents(EntityUid target, HashSet<string> comps)
{
foreach (var toRemove in comps)
{
if (_compFact.TryGetRegistration(toRemove, out var registration))
RemComp(target, registration.Type);
}
}
// End Spell Helpers // End Spell Helpers
#endregion #endregion
#region Touch Spells #region Touch Spells
@@ -514,6 +528,33 @@ public abstract class SharedMagicSystem : EntitySystem
_stun.TryParalyze(ev.Performer, ev.PerformerStunDuration, true); _stun.TryParalyze(ev.Performer, ev.PerformerStunDuration, true);
} }
#endregion
#region Animation Spells
private void OnAnimateSpell(AnimateSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !TryComp<FixturesComponent>(ev.Target, out var fixtures) ||
!TryComp<PhysicsComponent>(ev.Target, out var physics))
return;
ev.Handled = true;
//Speak(ev);
RemoveComponents(ev.Target, ev.RemoveComponents);
AddComponents(ev.Target, ev.AddComponents);
var xform = Transform(ev.Target);
var fixture = fixtures.Fixtures.First();
_transform.Unanchor(ev.Target);
_physics.SetCanCollide(ev.Target, true, true, false, fixtures, physics);
_physics.SetCollisionMask(ev.Target, fixture.Key, fixture.Value, (int)CollisionGroup.FlyingMobMask, fixtures, physics);
_physics.SetCollisionLayer(ev.Target, fixture.Key, fixture.Value, (int)CollisionGroup.FlyingMobLayer, fixtures, physics);
_physics.SetBodyType(ev.Target, BodyType.KinematicController, fixtures, physics, xform);
_physics.SetBodyStatus(ev.Target, physics, BodyStatus.InAir, true);
_physics.SetFixedRotation(ev.Target, false, true, fixtures, physics);
}
#endregion #endregion
// End Spells // End Spells
#endregion #endregion

View File

@@ -26,6 +26,9 @@ spellbook-ethereal-jaunt-description = Slip into the ethereal plane to slip away
spellbook-mind-swap-name = Mind Swap spellbook-mind-swap-name = Mind Swap
spellbook-mind-swap-description = Exchange bodies with another person! spellbook-mind-swap-description = Exchange bodies with another person!
spellbook-animate-name = Animate
spellbook-animate-description = Bring an inanimate object to life!
spellbook-smite-name = Smite spellbook-smite-name = Smite
spellbook-smite-desc = Don't like them? EXPLODE them into giblets! Requires Wizard Robe & Hat. spellbook-smite-desc = Don't like them? EXPLODE them into giblets! Requires Wizard Robe & Hat.
@@ -49,6 +52,9 @@ spellbook-wand-polymorph-carp-description = For when you need a carp filet quick
spellbook-wand-locker-name = Wand of the Locker spellbook-wand-locker-name = Wand of the Locker
spellbook-wand-locker-description = Shoot cursed lockers at your enemies and lock em away! spellbook-wand-locker-description = Shoot cursed lockers at your enemies and lock em away!
spellbook-staff-animation-name = Staff of Animation
spellbook-staff-animation-description = Bring inanimate objects to life!
# Events # Events
spellbook-event-summon-ghosts-name = Summon Ghosts spellbook-event-summon-ghosts-name = Summon Ghosts

View File

@@ -197,6 +197,19 @@
- !type:ListingLimitedStockCondition - !type:ListingLimitedStockCondition
stock: 1 stock: 1
- type: listing
id: SpellbookStaffAnimation
name: spellbook-staff-animation-name
description: spellbook-staff-animation-description
productEntity: AnimationStaff
cost:
WizCoin: 3
categories:
- SpellbookEquipment
conditions:
- !type:ListingLimitedStockCondition
stock: 1
# Event # Event
- type: listing - type: listing
id: SpellbookEventSummonGhosts id: SpellbookEventSummonGhosts

View File

@@ -1,4 +1,4 @@
# To be implemented: see #9072 # To be implemented: see #9072
- type: entity - type: entity
name: staff of healing name: staff of healing

View File

@@ -5,6 +5,7 @@
components: components:
- type: Item - type: Item
size: Small size: Small
- type: Animateable
- type: Clickable - type: Clickable
- type: InteractionOutline - type: InteractionOutline
- type: MovedByPressure - type: MovedByPressure

View File

@@ -5,6 +5,7 @@
description: A square piece of metal standing on four metal legs. description: A square piece of metal standing on four metal legs.
abstract: true abstract: true
components: components:
- type: Animateable
- type: Damageable - type: Damageable
damageContainer: StructuralInorganic damageContainer: StructuralInorganic
damageModifierSet: Metallic damageModifierSet: Metallic

View File

@@ -6,6 +6,7 @@
placement: placement:
mode: PlaceFree mode: PlaceFree
components: components:
- type: Animateable
- type: Clickable - type: Clickable
- type: InteractionOutline - type: InteractionOutline
- type: Physics - type: Physics

View File

@@ -5,6 +5,7 @@
placement: placement:
mode: SnapgridCenter mode: SnapgridCenter
components: components:
- type: Animateable
- type: MeleeSound - type: MeleeSound
soundGroups: soundGroups:
Brute: Brute:

View File

@@ -3,6 +3,7 @@
parent: BaseStructure parent: BaseStructure
id: BaseMachine id: BaseMachine
components: components:
- type: Animateable
- type: InteractionOutline - type: InteractionOutline
- type: Anchorable - type: Anchorable
delay: 2 delay: 2
@@ -57,6 +58,7 @@
abstract: true abstract: true
id: ConstructibleMachine id: ConstructibleMachine
components: components:
- type: Animateable
- type: Machine - type: Machine
- type: ContainerContainer - type: ContainerContainer
containers: containers:

View File

@@ -5,6 +5,7 @@
name: gas canister name: gas canister
description: A canister that can contain any type of gas. It can be attached to connector ports using a wrench. description: A canister that can contain any type of gas. It can be attached to connector ports using a wrench.
components: components:
- type: Animateable
- type: InteractionOutline - type: InteractionOutline
- type: Transform - type: Transform
noRot: true noRot: true

View File

@@ -5,6 +5,7 @@
description: A standard-issue Nanotrasen storage unit. description: A standard-issue Nanotrasen storage unit.
abstract: true abstract: true
components: components:
- type: Animateable
- type: ResistLocker - type: ResistLocker
- type: Transform - type: Transform
noRot: true noRot: true

View File

@@ -5,6 +5,7 @@
name: crate name: crate
description: A large container for items. description: A large container for items.
components: components:
- type: Animateable
- type: Transform - type: Transform
noRot: true noRot: true
- type: Icon - type: Icon

View File

@@ -1,10 +1,11 @@
- type: entity - type: entity
id: StorageTank id: StorageTank
parent: BaseStructureDynamic parent: BaseStructureDynamic
name: storage tank name: storage tank
description: A liquids storage tank. description: A liquids storage tank.
abstract: true abstract: true
components: components:
- type: Animateable
- type: Sprite - type: Sprite
noRot: true noRot: true
- type: InteractionOutline - type: InteractionOutline

View File

@@ -0,0 +1,70 @@
- type: entity
id: ActionAnimateSpell
name: Animate
description: Bring an inanimate object to life!
components:
- type: EntityTargetAction
useDelay: 0
charges: 5
itemIconStyle: BigAction
whitelist:
components:
- Animateable # Currently on: SeatBase, TableBase, ClosetBase, BaseMachine, ConstructibleMachine, BaseComputer, BaseItem, CrateGeneric, StorageTank, GasCanister
blacklist:
components:
- MindContainer
- NukeDisk
- GravityGenerator
- AnomalyGenerator
canTargetSelf: false
interactOnMiss: false
sound: !type:SoundPathSpecifier
path: /Audio/Magic/staff_animation.ogg
icon:
sprite: Objects/Magic/magicactions.rsi
state: spell_default
event: !type:AnimateSpellEvent
addComponents:
- type: MindContainer
- type: InputMover
- type: MobMover
- type: MovementSpeedModifier
- type: HTN
rootTask:
task: SimpleHostileCompound
- type: CombatMode
- type: MeleeWeapon
animation: WeaponArcPunch
wideAnimation: WeaponArcPunch
altDisarm: false
soundHit: /Audio/Weapons/smash.ogg
range: 1.2
angle: 0.0
damage:
types:
Blunt: 10
- type: NpcFactionMember
factions:
- Wizard
- type: NoSlip
- type: MovementAlwaysTouching
- type: CanMoveInAir
- type: Damageable
damageContainer: ManifestedSpirit
damageModifierSet: ManifestedSpirit
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:DoActsBehavior
acts: ["Destruction"]
- !type:PlaySoundBehavior
sound:
collection: MetalBreak
- type: Hands
- type: CanEscapeInventory
removeComponents:
- RequireProjectileTarget
speech: action-speech-spell-animate

View File

@@ -1,4 +1,4 @@
# non-projectile / "gun" staves # non-projectile / "gun" staves
# wand that gives lights an RGB effect. # wand that gives lights an RGB effect.
- type: entity - type: entity
@@ -32,6 +32,30 @@
enabled: true enabled: true
radius: 2 radius: 2
- type: entity
id: AnimationStaff
parent: BaseItem
name: staff of animation
description: Brings inanimate objects to life!
components:
- type: Sprite
sprite: Objects/Weapons/Guns/Basic/staves.rsi
layers:
- state: animation
- type: ActionOnInteract
actions:
- ActionAnimateSpell
- type: Item
size: Normal
inhandVisuals:
left:
- state: animation-inhand-left
right:
- state: animation-inhand-right
- type: Tag
tags:
- WizardWand
- type: entity - type: entity
id: ActionRgbLight id: ActionRgbLight
components: components:

View File

@@ -8,6 +8,7 @@
- Zombie - Zombie
- Revolutionary - Revolutionary
- AllHostile - AllHostile
- Wizard
- type: npcFaction - type: npcFaction
id: NanoTrasen id: NanoTrasen
@@ -19,6 +20,7 @@
- Revolutionary - Revolutionary
- Dragon - Dragon
- AllHostile - AllHostile
- Wizard
- type: npcFaction - type: npcFaction
id: Mouse id: Mouse
@@ -48,6 +50,7 @@
- Zombie - Zombie
- Revolutionary - Revolutionary
- AllHostile - AllHostile
- Wizard
- type: npcFaction - type: npcFaction
id: SimpleNeutral id: SimpleNeutral
@@ -62,6 +65,7 @@
- Zombie - Zombie
- Dragon - Dragon
- AllHostile - AllHostile
- Wizard
- type: npcFaction - type: npcFaction
id: Xeno id: Xeno
@@ -73,6 +77,7 @@
- Zombie - Zombie
- Revolutionary - Revolutionary
- AllHostile - AllHostile
- Wizard
- type: npcFaction - type: npcFaction
id: Zombie id: Zombie
@@ -85,6 +90,7 @@
- PetsNT - PetsNT
- Revolutionary - Revolutionary
- AllHostile - AllHostile
- Wizard
- type: npcFaction - type: npcFaction
id: Revolutionary id: Revolutionary
@@ -94,6 +100,7 @@
- SimpleHostile - SimpleHostile
- Dragon - Dragon
- AllHostile - AllHostile
- Wizard
- type: npcFaction - type: npcFaction
id: AllHostile id: AllHostile
@@ -109,3 +116,16 @@
- Xeno - Xeno
- Zombie - Zombie
- Revolutionary - Revolutionary
- Wizard
- type: npcFaction
id: Wizard
hostile:
- NanoTrasen
- Dragon
- SimpleHostile
- Syndicate
- Xeno
- Zombie
- Revolutionary
- AllHostile