Add own-body spectator camera and player names; record Workshop 0.0.139
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-03 21:16:05 +01:00
parent 4f02ad64b4
commit 828bb0a1a7
17 changed files with 1048 additions and 35 deletions
@@ -52,7 +52,7 @@ class ERR_ObserverGuidance
}
if (!survivorAvailable)
return message + " No teammate or body is currently available to follow. Use camera look controls to orbit the last known position and zoom controls to change distance.";
return message + " Press Left / Right arrow to change teammate or body (or /esc spectate next / /esc spectate previous). Press Enter to switch between third-person and the living teammate's camera. Player view waits in third-person if no live camera stream is available. Use camera look controls to orbit and zoom controls to change distance in third-person.";
return message + " Press Left / Right arrow to change teammate or body, including your own (or /esc spectate next / /esc spectate previous). Press Enter to switch between third-person and the living teammate's camera. Player view waits in third-person if no live camera stream is available. Use camera look controls to orbit and zoom controls to change distance in third-person.";
}
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
{
@@ -225,7 +225,7 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
{
target.GetWorldTransform(transform);
aim = transform[3] + "0 1.5 0";
if (!ERR_Actors.Alive(target)) aim = transform[3] + "0 0.4 0";
if (!ERR_Actors.CanFight(target)) aim = transform[3] + "0 0.4 0";
}
if (target != m_Target || vehicle != m_Vehicle)
{
@@ -242,7 +242,7 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
m_Target = null; m_Vehicle = null;
m_Distance = Math.Clamp(m_Distance, 2, 12);
}
if ((!m_PlayerView || !target || !ERR_Actors.Alive(target)) && param.isManualInputEnabled)
if ((!m_PlayerView || !target || !ERR_Actors.CanFight(target)) && param.isManualInputEnabled)
{
m_Yaw += GetInputManager().GetActionValue("ManualCameraRotateYaw") * 90 * param.timeSlice;
m_Pitch = Math.Clamp(m_Pitch - GetInputManager().GetActionValue("ManualCameraRotatePitch") * 90 * param.timeSlice, 5, 75);
@@ -366,10 +366,13 @@ modded class SCR_PlayerController
if (survivors[i] == body || (casualty && !ERR_Actors.CanFight(survivors[i])) || !friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
// Dead teammates remain selectable alongside survivors, including our own body.
ERR_ObserverTargets.Bodies(mode, survivors);
// Lethal damage normally leaves a treatable casualty, not a native corpse.
// Keep that player's own canonical body in the cycle without possessing it.
if (casualty && !survivors.Contains(body)) survivors.Insert(body);
int index = survivors.Find(m_ERRServerObserved);
if (index < 0)
{
if (life && life.Actor) index = survivors.Find(life.Actor);
if (!casualty && life && life.Actor) index = survivors.Find(life.Actor);
if (index < 0) index = 0;
}
else index += direction;
@@ -64,7 +64,7 @@ modded class SCR_PlayerController
bool ERR_ProbeOrbitFrame(float yaw, float pitch, float zoom, bool enabled, out vector transform[4])
{
string outcome;
if (!System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "unconscious-camera" || this != GetGame().GetPlayerController() || !ERR_ProbeCameraActive() || ERR_ObservedEntity()) return false;
if (!System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "unconscious-camera" || this != GetGame().GetPlayerController() || !ERR_ProbeCameraActive() || ERR_ObservedEntity() != GetControlledEntity() || ERR_Actors.CanFight(GetControlledEntity())) return false;
ERR_FollowSurvivorCameraComponent follow = ERR_ProbeFollowComponent();
InputManager input = GetGame().GetInputManager();
if (!follow || !input) return false;
@@ -162,17 +162,30 @@ modded class ERR_GameMode
foreach (SCR_PersistentDamageEffect effect : effects) m_ERRCameraDamage.TerminateDamageEffect(effect);
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
}
protected bool ERR_CameraNoTargetOrbit(SCR_PlayerController pc)
protected bool ERR_CameraOwnBodyOrbit(SCR_PlayerController pc)
{
vector body = m_ERRCameraBody.GetOrigin();
pc.ERR_UpdateObserverServer(1);
pc.ERR_UpdateObserverServer(-1);
if (!ERR_CameraCheck(pc.ERR_ObservedEntity() == m_ERRCameraBody, "solo forward/backward cycling retains the actual own body")) return false;
ERR_FollowSurvivorCameraComponent follow = pc.ERR_ProbeFollowComponent();
follow.Input(true, false, false, true);
follow.Input(true, false, false, false);
if (!ERR_CameraCheck(follow.IsPlayerView(), "own-body orbit is tested with player-view mode selected")) return false;
vector baseline[4], yaw[4], pitch[4], zoom[4], blocked[4];
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, 0, 0, true, baseline), "no-target frame accepts native orbit action values")) return false;
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(1, 0, 0, true, yaw) && vector.Distance(baseline[2], yaw[2]) > 0.05, "no-target yaw action changes production camera direction")) return false;
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, -1, 0, true, pitch) && Math.AbsFloat(yaw[2][1] - pitch[2][1]) > 0.03, "no-target pitch action changes production camera elevation")) return false;
vector aim = pc.ERR_ObserverAnchor() + "0 1.5 0";
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, 0, -1, true, zoom) && vector.Distance(zoom[3], aim) < vector.Distance(pitch[3], aim) - 0.05, "no-target zoom action moves production camera closer to casualty anchor")) return false;
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(1, -1, -1, false, blocked) && vector.Distance(zoom[3], blocked[3]) < 0.001 && vector.Distance(zoom[2], blocked[2]) < 0.001, "disabled no-target input preserves camera pose")) return false;
if (!ERR_CameraCheck(vector.Distance(body, m_ERRCameraBody.GetOrigin()) < 0.001 && pc.GetControlledEntity() == m_ERRCameraBody && !pc.ERR_ObservedEntity(), "no-target orbit leaves casualty and possession unchanged")) return false;
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, 0, 0, true, baseline), "own-body frame accepts native orbit action values")) return false;
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(1, 0, 0, true, yaw) && vector.Distance(baseline[2], yaw[2]) > 0.05, "own-body yaw action changes production camera direction")) return false;
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, -1, 0, true, pitch) && Math.AbsFloat(yaw[2][1] - pitch[2][1]) > 0.03, "own-body pitch action changes production camera elevation")) return false;
vector aim = m_ERRCameraBody.GetOrigin() + "0 0.4 0";
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(0, 0, -1, true, zoom) && vector.Distance(zoom[3], aim) < vector.Distance(pitch[3], aim) - 0.05, "own-body zoom action moves production camera closer to casualty anchor")) return false;
if (!ERR_CameraCheck(pc.ERR_ProbeOrbitFrame(1, -1, -1, false, blocked) && vector.Distance(zoom[3], blocked[3]) < 0.001 && vector.Distance(zoom[2], blocked[2]) < 0.001, "disabled own-body input preserves camera pose")) return false;
if (!ERR_CameraCheck(vector.Distance(body, m_ERRCameraBody.GetOrigin()) < 0.001 && pc.GetControlledEntity() == m_ERRCameraBody && pc.ERR_ObservedEntity() == m_ERRCameraBody, "own-body orbit leaves casualty and possession unchanged")) return false;
// The disabled frame starts the normal two-frame input-resume gate.
follow.Input(true, false, false, false);
follow.Input(true, false, false, false);
follow.Input(true, false, false, true);
follow.Input(true, false, false, false);
if (!ERR_CameraCheck(!follow.IsPlayerView(), "own-body camera returns to ordinary third-person selection")) return false;
Math3D.MatrixCopy(zoom, m_ERRCameraOrbitPose);
Print("ERR CAMERA ORBIT: native action/frame regression; physical mouse delivery and multiplayer are not tested");
return true;
@@ -192,6 +205,9 @@ modded class ERR_GameMode
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_ERRCameraBody.FindComponent(InventoryStorageManagerComponent));
m_ERRCameraWeapon = ERR_Actors.Spawn("{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et", m_ERRCameraBody.GetOrigin()); Own(m_ERRCameraWeapon);
if (!ERR_CameraCheck(m_ERRCameraWeapon && inventory && inventory.TryInsertItem(m_ERRCameraWeapon, EStoragePurpose.PURPOSE_WEAPON_PROXY), "one physical rifle granted to the player")) return;
// Keep this deliberately unconscious solo fixture recoverable under the all-down rules.
IEntity injector = ERR_Actors.Spawn(ERR_EnemyMedicalCatalog.EPI, m_ERRCameraBody.GetOrigin()); Own(injector);
if (!ERR_CameraCheck(injector && inventory.TryInsertItem(injector) && Settings().Set("selfEpinephrine", 1), "carried injector keeps the camera fixture recoverable")) return;
m_ERRCameraStage = -1; m_ERRCameraWait = now + 1500; return;
}
SCR_CharacterControllerComponent character = SCR_CharacterControllerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterControllerComponent));
@@ -215,16 +231,17 @@ modded class ERR_GameMode
{
if (!ERR_CameraCheck(pc.ERR_ObserverState() == ERR_ObserverReason.UNCONSCIOUS && pc.ERR_ProbeCameraActive(), "native knockout automatically opens the manual third-person camera")) return;
if (!ERR_CameraCheck(pc.GetControlledEntity() == m_ERRCameraBody && ERR_Actors.Alive(m_ERRCameraBody) && !ERR_Actors.CanFight(m_ERRCameraBody), "possession and living unconscious body unchanged")) return;
if (!ERR_CameraCheck(!pc.ERR_ObservedEntity() && vector.DistanceXZ(pc.ERR_ObserverAnchor(), m_ERRCameraBody.GetOrigin()) < 1, "no teammate falls back to the casualty body")) return;
if (!ERR_CameraCheck(pc.ERR_ObservedEntity() == m_ERRCameraBody && vector.DistanceXZ(pc.ERR_ObserverAnchor(), m_ERRCameraBody.GetOrigin()) < 1, "own casualty body is a real selected camera target")) return;
if (!ERR_CameraCheck(playerInventory.Contains(m_ERRCameraWeapon), "enabled retention keeps the exact rifle with the unconscious player")) return;
if (!ERR_CameraCheck(!RecoveryStatus().IsEmpty() && RecoveryStatus().Contains("Treatment or natural recovery") && pc.ERR_ProbeAcceptedRecoveryStatus() == RecoveryStatus(), "accepted observer hint contains the current ongoing recovery status")) return;
if (!ERR_CameraNoTargetOrbit(pc)) return;
string expectedRecovery = RecoveryStatus() + "\n" + pc.ERR_SelfEpinephrineHint();
if (!ERR_CameraCheck(RecoveryStatus().Contains("Self epinephrine is available") && pc.ERR_ProbeAcceptedRecoveryStatus() == expectedRecovery, "accepted observer hint contains the current recovery status and self-injection controls")) return;
if (!ERR_CameraOwnBodyOrbit(pc)) return;
m_ERRCameraStage = 11; m_ERRCameraWait = now + 500; return;
}
if (m_ERRCameraStage == 11)
{
vector applied[4]; GetGame().GetCameraManager().CurrentCamera().GetWorldTransform(applied);
if (!ERR_CameraCheck(pc.ERR_ProbeCameraActive() && vector.Distance(applied[2], m_ERRCameraOrbitPose[2]) < 0.01, "native camera applies and retains no-target orbit direction on subsequent frames")) return;
if (!ERR_CameraCheck(pc.ERR_ProbeCameraActive() && vector.Distance(applied[2], m_ERRCameraOrbitPose[2]) < 0.01, "native camera applies and retains own-body orbit direction on subsequent frames")) return;
if (!ERR_CameraOpenChat(pc, false)) return;
m_ERRCameraStage = 111; m_ERRCameraWait = now + 500; return;
}