From e92be317f51490bf398344a1d4d75f7345e701bd Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sun, 12 Jul 2026 20:44:18 +0100 Subject: [PATCH 1/8] M1: player stand + control handoff + Cinemachine camera system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Install Cinemachine 3.1.7 - Stand (raised platform behind north wall) with StandPoint_0/_1 - PlayerAvatar: seat-ownership drives the mode — hold a seat => teleport to your stand, freeze, refuse input (control transfers/locks to your tread); release => teleport back, resume on-foot. Resolves the WASD overlap off one flag. - CameraDirector on Main Camera: 4 Cinemachine vcams (AvatarFollow / RobotFollow / StandView / CinematicOrbit), Brain blends; V cycles; default auto-follows the seat flag (on-foot=Avatar, driving=Robot). Retires the old FollowCamera. - Verified in play: claim->teleport+freeze+RobotFollow, release->return+AvatarFollow Co-Authored-By: Claude Opus 4.8 (1M context) --- Assets/Scenes/M1_Arena.unity | 228 +++++++++++++++++++ Assets/Scripts/Camera/CameraDirector.cs | 143 ++++++++++++ Assets/Scripts/Camera/CameraDirector.cs.meta | 2 + Assets/Scripts/Player/PlayerAvatar.cs | 113 +++++++-- Packages/manifest.json | 1 + Packages/packages-lock.json | 28 +++ 6 files changed, 490 insertions(+), 25 deletions(-) create mode 100644 Assets/Scripts/Camera/CameraDirector.cs create mode 100644 Assets/Scripts/Camera/CameraDirector.cs.meta diff --git a/Assets/Scenes/M1_Arena.unity b/Assets/Scenes/M1_Arena.unity index a9f7918..2983be5 100644 --- a/Assets/Scenes/M1_Arena.unity +++ b/Assets/Scenes/M1_Arena.unity @@ -469,6 +469,118 @@ Transform: m_CorrespondingSourceObject: {fileID: 400000, guid: 463e9d66ec2b7ad428d27c80a2b2d1db, type: 3} m_PrefabInstance: {fileID: 67875927} m_PrefabAsset: {fileID: 0} +--- !u!1 &101108170 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 101108171} + - component: {fileID: 101108174} + - component: {fileID: 101108173} + - component: {fileID: 101108172} + m_Layer: 0 + m_Name: Stand_Platform + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 2147483647 + m_IsActive: 1 +--- !u!4 &101108171 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 101108170} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1.5, z: 0} + m_LocalScale: {x: 12, y: 3, z: 5} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1532509717} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!23 &101108172 +MeshRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 101108170} + m_Enabled: 1 + m_CastShadows: 1 + m_ReceiveShadows: 1 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 2 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 1 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 3 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_AdditionalVertexStreams: {fileID: 0} +--- !u!65 &101108173 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 101108170} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 1, y: 1, z: 1} + m_Center: {x: 0, y: 0, z: 0} +--- !u!33 &101108174 +MeshFilter: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 101108170} + m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} --- !u!1001 &130342001 PrefabInstance: m_ObjectHideFlags: 0 @@ -1372,6 +1484,37 @@ BoxCollider: serializedVersion: 3 m_Size: {x: 150.5, y: 4, z: 0.5} m_Center: {x: 0, y: 0, z: 0} +--- !u!1 &506345465 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 506345466} + m_Layer: 0 + m_Name: StandPoint_1 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &506345466 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 506345465} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 1, z: 0, w: -0.00000004371139} + m_LocalPosition: {x: 2.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1532509717} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &562137767 GameObject: m_ObjectHideFlags: 0 @@ -1384,6 +1527,7 @@ GameObject: - component: {fileID: 562137769} - component: {fileID: 562137768} - component: {fileID: 562137771} + - component: {fileID: 562137772} m_Layer: 0 m_Name: Main Camera m_TagString: MainCamera @@ -1483,6 +1627,24 @@ MonoBehaviour: positionSmoothTime: 0.25 lookAtDegreesPerSecond: 180 lookAtHeightOffset: 1 +--- !u!114 &562137772 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 562137767} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cd21368c9a8b1064d913c375fb607c0b, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::Jankenbots.Prototype.CameraDirector + avatarOffset: {x: 0, y: 4, z: -6} + robotOffset: {x: 0, y: 8, z: -12} + orbitRadius: 20 + orbitHeight: 9 + orbitSpeed: 18 + lookHeight: 1.2 --- !u!1001 &579422633 PrefabInstance: m_ObjectHideFlags: 0 @@ -3779,6 +3941,37 @@ Transform: m_CorrespondingSourceObject: {fileID: 400000, guid: 5161c2c79aa7dc44b8a812f624bf37d7, type: 3} m_PrefabInstance: {fileID: 893957807} m_PrefabAsset: {fileID: 0} +--- !u!1 &903945605 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 903945606} + m_Layer: 0 + m_Name: StandPoint_0 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &903945606 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 903945605} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 1, z: 0, w: -0.00000004371139} + m_LocalPosition: {x: -2.5, y: 3, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1532509717} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &912005198 PrefabInstance: m_ObjectHideFlags: 0 @@ -6914,6 +7107,40 @@ Transform: m_CorrespondingSourceObject: {fileID: 400000, guid: 034ba005b2423b84ebc087e917744363, type: 3} m_PrefabInstance: {fileID: 1531216050} m_PrefabAsset: {fileID: 0} +--- !u!1 &1532509716 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1532509717} + m_Layer: 0 + m_Name: Stand + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1532509717 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1532509716} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 82} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 101108171} + - {fileID: 903945606} + - {fileID: 506345466} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1001 &1537562849 PrefabInstance: m_ObjectHideFlags: 0 @@ -8460,3 +8687,4 @@ SceneRoots: - {fileID: 1378796740} - {fileID: 850707848} - {fileID: 326006527} + - {fileID: 1532509717} diff --git a/Assets/Scripts/Camera/CameraDirector.cs b/Assets/Scripts/Camera/CameraDirector.cs new file mode 100644 index 0000000..ecc6e50 --- /dev/null +++ b/Assets/Scripts/Camera/CameraDirector.cs @@ -0,0 +1,143 @@ +using Unity.Cinemachine; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace Jankenbots.Prototype +{ + /// + /// JANKENBOTS — client-local camera director. Owns four Cinemachine cameras and + /// lets the local player cycle between them; the CinemachineBrain on this Camera + /// blends smoothly on every switch. + /// + /// • AvatarFollow — 3rd-person chase on YOUR character (on-foot). + /// • RobotFollow — chase cam on the bot (driving; the robot-follow cam). + /// • StandView — fixed view from your player stand, looking at the bot. + /// • CinematicOrbit— slow auto-orbit of the bot (spectate / flavor). + /// + /// The default mode auto-follows the control state: on foot → AvatarFollow, seated + /// (driving) → RobotFollow — reading the SAME seat flag as the control handoff + /// (). Press V to cycle manually; claiming + /// or leaving a seat re-asserts the sensible default. + /// + /// The vcams are intentionally "bare" (no procedural body/aim) — the director poses + /// their transforms each LateUpdate from the live targets, and the Brain just blends + /// between whichever is prioritised. Cameras are purely client-local, never networked. + /// + [RequireComponent(typeof(Camera))] + public class CameraDirector : MonoBehaviour + { + public enum Mode { AvatarFollow, RobotFollow, StandView, CinematicOrbit } + + [Header("Framing")] + public Vector3 avatarOffset = new Vector3(0f, 4f, -6f); + public Vector3 robotOffset = new Vector3(0f, 8f, -12f); + public float orbitRadius = 20f; + public float orbitHeight = 9f; + public float orbitSpeed = 18f; // deg/sec + public float lookHeight = 1.2f; // aim a bit above the target pivot + + Transform _bot, _standOverlook; + CinemachineCamera _avatar, _robot, _stand, _orbit; + CinemachineCamera[] _all; + Mode _mode = Mode.AvatarFollow; + bool _userOverride; + bool _lastSeated; + float _orbitAngle; + PlayerAvatar _local; + + void Start() + { + if (GetComponent() == null) gameObject.AddComponent(); + // Retire the old bespoke bot-only follow cam if it's still on this camera. + var legacy = GetComponent(); + if (legacy != null) legacy.enabled = false; + + var b = GameObject.Find("Bot"); if (b != null) _bot = b.transform; + var s = GameObject.Find("StandPoint_0"); if (s != null) _standOverlook = s.transform; + + var holder = new GameObject("CameraRigs").transform; + _avatar = Make(holder, "CM_AvatarFollow"); + _robot = Make(holder, "CM_RobotFollow"); + _stand = Make(holder, "CM_StandView"); + _orbit = Make(holder, "CM_CinematicOrbit"); + _all = new[] { _avatar, _robot, _stand, _orbit }; + Apply(_mode); + } + + CinemachineCamera Make(Transform parent, string name) + { + var go = new GameObject(name); + go.transform.SetParent(parent, false); + var vc = go.AddComponent(); + vc.Priority = 0; + return vc; + } + + void Update() + { + if (_local == null) _local = FindLocal(); + + var kb = Keyboard.current; + if (kb != null && kb.vKey.wasPressedThisFrame) + { + _mode = (Mode)(((int)_mode + 1) % _all.Length); + _userOverride = true; + Apply(_mode); + } + + // A seat claim/leave re-asserts the sensible default (clears a manual override). + if (_local != null && _local.IsSeatedLocal != _lastSeated) + { + _lastSeated = _local.IsSeatedLocal; + _userOverride = false; + } + + if (!_userOverride && _local != null) + { + Mode want = _local.IsSeatedLocal ? Mode.RobotFollow : Mode.AvatarFollow; + if (want != _mode) { _mode = want; Apply(_mode); } + } + } + + void LateUpdate() + { + if (_local != null) PoseFollow(_avatar, _local.transform, avatarOffset); + + if (_bot != null) + { + PoseFollow(_robot, _bot, robotOffset); + + _orbitAngle += orbitSpeed * Time.deltaTime; + Vector3 off = Quaternion.Euler(0f, _orbitAngle, 0f) * new Vector3(0f, orbitHeight, -orbitRadius); + PoseFollow(_orbit, _bot, off); + + if (_standOverlook != null) + { + _stand.transform.position = _standOverlook.position + Vector3.up * 1.5f; + _stand.transform.rotation = Quaternion.LookRotation( + (_bot.position + Vector3.up * lookHeight) - _stand.transform.position); + } + } + } + + void PoseFollow(CinemachineCamera vc, Transform target, Vector3 worldOffset) + { + Vector3 pos = target.position + worldOffset; + vc.transform.position = pos; + vc.transform.rotation = Quaternion.LookRotation((target.position + Vector3.up * lookHeight) - pos); + } + + PlayerAvatar FindLocal() + { + foreach (var a in FindObjectsByType(FindObjectsSortMode.None)) + if (a.IsOwner) return a; + return null; + } + + void Apply(Mode m) + { + for (int i = 0; i < _all.Length; i++) + _all[i].Priority = ((int)m == i) ? 20 : 0; + } + } +} diff --git a/Assets/Scripts/Camera/CameraDirector.cs.meta b/Assets/Scripts/Camera/CameraDirector.cs.meta new file mode 100644 index 0000000..1e54c67 --- /dev/null +++ b/Assets/Scripts/Camera/CameraDirector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cd21368c9a8b1064d913c375fb607c0b \ No newline at end of file diff --git a/Assets/Scripts/Player/PlayerAvatar.cs b/Assets/Scripts/Player/PlayerAvatar.cs index 6c9eaaf..097acd6 100644 --- a/Assets/Scripts/Player/PlayerAvatar.cs +++ b/Assets/Scripts/Player/PlayerAvatar.cs @@ -1,4 +1,5 @@ using Unity.Netcode; +using Unity.Netcode.Components; using UnityEngine; using UnityEngine.InputSystem; @@ -6,25 +7,24 @@ namespace Jankenbots.Prototype { /// /// JANKENBOTS — networked run-around PLAYER avatar (the rigged low-poly - /// character). This is the foundation for the timed junkyard "gather parts" - /// phase: each player controls one of these, runs around, and (later) hauls - /// scrap back to build the bot. Driving the finished bot happens from player - /// stands, so the avatar and the tread controls are separate systems. + /// character), PLUS the on-foot ↔ driving control handoff. /// - /// NETCODE: OWNER-authoritative. Only the owning client reads input and moves - /// its CharacterController; an on the - /// same object replicates the resulting motion to everyone. No host round-trip - /// on your own movement → it feels responsive. + /// CONTROL MODEL (the spine): a player is in exactly one mode, derived from a + /// single fact — do I currently hold a bot seat? (). + /// • ON FOOT (no seat) → WASD drives THIS avatar; free-roaming. + /// • DRIVING (holds a seat)→ avatar TELEPORTS to its player stand and FREEZES; + /// it refuses all input, so control transfers and + /// locks to the claimed tread (TreadPart already only + /// listens when you hold its seat). Release the seat → + /// teleport back and resume on-foot control. + /// This is what resolves the WASD overlap: PlayerAvatar and TreadPart become + /// mutually exclusive off one flag — never both live at once. /// - /// ANIMATION: derived LOCALLY on every client from the avatar's measured - /// horizontal speed (position delta / dt), fed into the Animator's "Speed" - /// float, which drives an Idle→Walk→Run 1-D blend tree. Because observers - /// measure the same replicated motion the owner produces, the animation stays - /// in sync with zero extra network traffic — no NetworkAnimator needed. + /// NETCODE: OWNER-authoritative movement (see ). + /// Teleports use NetworkTransform.Teleport so observers don't see a slide. /// - /// NOTE: reads WASD, same keys as the bot's TreadPart — fine while testing the - /// avatar in isolation; the real game separates "running" from "driving" via - /// the (future) player-stand flow, so a player is never doing both at once. + /// ANIMATION: derived locally on every client from measured horizontal speed → + /// Animator "Speed" → Idle/Walk/Run blend (stays in sync with no extra traffic). /// [RequireComponent(typeof(CharacterController))] public class PlayerAvatar : NetworkBehaviour @@ -45,28 +45,41 @@ namespace Jankenbots.Prototype [Tooltip("Gravity (m/s^2, negative). Keeps the CharacterController grounded.")] public float gravity = -20f; - [Header("Animation")] - [Tooltip("Speed value the Animator treats as full-run, for normalising the blend.")] - public float runAnimSpeed = 5f; - CharacterController _cc; Animator _anim; + NetworkTransform _nt; + SeatManager _seats; + Transform _standLeft, _standRight; // where each pilot teleports (found by name) + float _vy; // vertical velocity (gravity) Vector3 _horizVel; // smoothed horizontal velocity (ramped, not instant) Vector3 _lastPos; float _animSpeed; + + bool _seated; // am I (owner) currently piloting a tread? + Vector3 _returnPos; // where to drop me back when I leave the seat + bool _hasReturn; + static readonly int SpeedHash = Animator.StringToHash("Speed"); + /// True while the LOCAL owner is seated/driving — read by the camera director. + public bool IsSeatedLocal => _seated; + void Awake() { _cc = GetComponent(); _anim = GetComponentInChildren(); + _nt = GetComponent(); _lastPos = transform.position; } public override void OnNetworkSpawn() { _lastPos = transform.position; + _seats = FindFirstObjectByType(); + var sl = GameObject.Find("StandPoint_0"); if (sl != null) _standLeft = sl.transform; + var sr = GameObject.Find("StandPoint_1"); if (sr != null) _standRight = sr.transform; + // Fan owners out a little so multiple players don't stack on the origin. if (IsOwner) { @@ -80,7 +93,20 @@ namespace Jankenbots.Prototype { if (!IsSpawned) return; - if (IsOwner) OwnerMove(); + if (IsOwner) + { + // Derive mode from seat ownership and handle the transition. + SeatManager.Seat? seat = LocalSeat(); + bool wantSeated = seat.HasValue; + if (wantSeated != _seated) + { + if (wantSeated) EnterSeated(seat.Value); + else ExitSeated(); + } + + // On foot → drive the avatar. Seated → do nothing (control is on the tread). + if (!_seated) OwnerMove(); + } // Animation on EVERY client, from measured horizontal speed. Vector3 delta = transform.position - _lastPos; @@ -91,6 +117,46 @@ namespace Jankenbots.Prototype if (_anim != null) _anim.SetFloat(SpeedHash, _animSpeed); } + // Which seat (if any) the local player holds — the single source of truth for mode. + SeatManager.Seat? LocalSeat() + { + if (_seats == null || NetworkManager.Singleton == null) return null; + ulong me = NetworkManager.Singleton.LocalClientId; + if (_seats.PilotOf(SeatManager.Seat.Left) == me) return SeatManager.Seat.Left; + if (_seats.PilotOf(SeatManager.Seat.Right) == me) return SeatManager.Seat.Right; + return null; + } + + void EnterSeated(SeatManager.Seat seat) + { + _seated = true; + _returnPos = transform.position; // remember where I left the arena + _hasReturn = true; + _horizVel = Vector3.zero; _vy = 0f; + + Transform sp = seat == SeatManager.Seat.Left ? _standLeft : _standRight; + if (sp != null) TeleportTo(sp.position, sp.rotation); + } + + void ExitSeated() + { + _seated = false; + _horizVel = Vector3.zero; _vy = 0f; + if (_hasReturn) TeleportTo(_returnPos + Vector3.up * 0.2f, transform.rotation); + } + + // Interpolation-free teleport (CharacterController must be off to move it directly; + // NetworkTransform.Teleport keeps observers from seeing a slide across the map). + void TeleportTo(Vector3 pos, Quaternion rot) + { + bool ccWas = _cc.enabled; + _cc.enabled = false; + transform.SetPositionAndRotation(pos, rot); + _cc.enabled = ccWas; + _lastPos = pos; + if (_nt != null) _nt.Teleport(pos, rot, transform.localScale); + } + void OwnerMove() { var kb = Keyboard.current; @@ -107,8 +173,7 @@ namespace Jankenbots.Prototype if (input.sqrMagnitude > 1f) input.Normalize(); Vector3 desired = input * moveSpeed; - // Ramp velocity toward the target instead of snapping — this is what kills - // the "jerky/jarring" feel and lets the anim blend Idle→Walk→Run smoothly. + // Ramp velocity toward the target instead of snapping (smooth start/stop). float rate = input.sqrMagnitude > 0.001f ? acceleration : deceleration; _horizVel = Vector3.MoveTowards(_horizVel, desired, rate * Time.deltaTime); @@ -117,8 +182,6 @@ namespace Jankenbots.Prototype _cc.Move((_horizVel + new Vector3(0f, _vy, 0f)) * Time.deltaTime); - // Face the way we're actually moving (uses the smoothed velocity, so turns - // ease too rather than snapping on a key change). if (_horizVel.sqrMagnitude > 0.05f) { Quaternion target = Quaternion.LookRotation(new Vector3(_horizVel.x, 0f, _horizVel.z)); diff --git a/Packages/manifest.json b/Packages/manifest.json index 8eb26be..d4e8afd 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -2,6 +2,7 @@ "dependencies": { "com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#main", "com.unity.ai.navigation": "2.0.13", + "com.unity.cinemachine": "3.1.7", "com.unity.collab-proxy": "2.12.4", "com.unity.ide.rider": "3.0.38", "com.unity.ide.visualstudio": "2.0.26", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 5639ded..67e0777 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -36,6 +36,16 @@ }, "url": "https://packages.unity.com" }, + "com.unity.cinemachine": { + "version": "3.1.7", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.splines": "2.0.0", + "com.unity.modules.imgui": "1.0.0" + }, + "url": "https://packages.unity.com" + }, "com.unity.collab-proxy": { "version": "2.12.4", "depth": 0, @@ -242,6 +252,13 @@ }, "url": "https://packages.unity.com" }, + "com.unity.settings-manager": { + "version": "2.1.1", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.shadergraph": { "version": "17.5.0", "depth": 1, @@ -251,6 +268,17 @@ "com.unity.searcher": "4.9.3" } }, + "com.unity.splines": { + "version": "2.9.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1", + "com.unity.modules.imgui": "1.0.0", + "com.unity.settings-manager": "1.0.3" + }, + "url": "https://packages.unity.com" + }, "com.unity.test-framework": { "version": "1.7.0", "depth": 0, From 3082644dbd275942f609197568923a1ee85a3ee4 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sun, 12 Jul 2026 20:50:30 +0100 Subject: [PATCH 2/8] camera: RMB-drag look + scroll zoom on follow cams, big zoom-out range Avatar/Robot follow cams now orbit from player input: hold right-mouse + drag to look around (yaw/pitch clamped), scroll to zoom (3..80m so you can pull back and survey the arena). Per-mode default distance on switch, look angle persists. Stand/Cinematic unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- Assets/Scripts/Camera/CameraDirector.cs | 110 +++++++++++++++++------- 1 file changed, 77 insertions(+), 33 deletions(-) diff --git a/Assets/Scripts/Camera/CameraDirector.cs b/Assets/Scripts/Camera/CameraDirector.cs index ecc6e50..c30d6f2 100644 --- a/Assets/Scripts/Camera/CameraDirector.cs +++ b/Assets/Scripts/Camera/CameraDirector.cs @@ -5,36 +5,49 @@ using UnityEngine.InputSystem; namespace Jankenbots.Prototype { /// - /// JANKENBOTS — client-local camera director. Owns four Cinemachine cameras and - /// lets the local player cycle between them; the CinemachineBrain on this Camera - /// blends smoothly on every switch. + /// JANKENBOTS — client-local camera director. Owns four Cinemachine cameras, lets + /// the local player cycle between them and orbit/zoom the follow cams; the + /// CinemachineBrain on this Camera blends smoothly on every switch. /// - /// • AvatarFollow — 3rd-person chase on YOUR character (on-foot). - /// • RobotFollow — chase cam on the bot (driving; the robot-follow cam). + /// • AvatarFollow — orbitable 3rd-person cam on YOUR character (on-foot). + /// • RobotFollow — orbitable chase cam on the bot (driving). /// • StandView — fixed view from your player stand, looking at the bot. /// • CinematicOrbit— slow auto-orbit of the bot (spectate / flavor). /// - /// The default mode auto-follows the control state: on foot → AvatarFollow, seated - /// (driving) → RobotFollow — reading the SAME seat flag as the control handoff - /// (). Press V to cycle manually; claiming - /// or leaving a seat re-asserts the sensible default. + /// LOOK / ZOOM (Avatar & Robot only): hold RIGHT MOUSE and drag to orbit + /// (yaw/pitch), scroll wheel to zoom in/out (out to so + /// you can pull back and see the arena). RMB-drag keeps the cursor free for the + /// Claim/Leave buttons until the walk-up interaction exists. /// - /// The vcams are intentionally "bare" (no procedural body/aim) — the director poses - /// their transforms each LateUpdate from the live targets, and the Brain just blends - /// between whichever is prioritised. Cameras are purely client-local, never networked. + /// Default mode auto-follows the control state (on foot → Avatar, seated → Robot), + /// reading the same seat flag as the control handoff (). + /// Press V to cycle manually. Cameras are purely client-local, never networked. /// [RequireComponent(typeof(Camera))] public class CameraDirector : MonoBehaviour { public enum Mode { AvatarFollow, RobotFollow, StandView, CinematicOrbit } - [Header("Framing")] - public Vector3 avatarOffset = new Vector3(0f, 4f, -6f); - public Vector3 robotOffset = new Vector3(0f, 8f, -12f); - public float orbitRadius = 20f; - public float orbitHeight = 9f; - public float orbitSpeed = 18f; // deg/sec - public float lookHeight = 1.2f; // aim a bit above the target pivot + [Header("Follow distances")] + public float avatarDistance = 8f; + public float robotDistance = 16f; + + [Header("Look / zoom (Avatar & Robot)")] + [Tooltip("Degrees of orbit per pixel of mouse drag (while holding RMB).")] + public float lookSensitivity = 0.18f; + [Tooltip("Metres of zoom per scroll notch.")] + public float zoomSensitivity = 4f; + public float minDistance = 3f; + [Tooltip("How far you can pull the camera back. Big so you can survey the arena.")] + public float maxDistance = 80f; + public float minPitch = -8f; + public float maxPitch = 78f; + public float lookHeight = 1.2f; // aim a bit above the target pivot + + [Header("Cinematic orbit")] + public float orbitRadius = 22f; + public float orbitHeight = 10f; + public float orbitSpeed = 16f; // deg/sec Transform _bot, _standOverlook; CinemachineCamera _avatar, _robot, _stand, _orbit; @@ -42,18 +55,23 @@ namespace Jankenbots.Prototype Mode _mode = Mode.AvatarFollow; bool _userOverride; bool _lastSeated; - float _orbitAngle; + + // Orbit state shared by the two follow cams (player-controlled). + float _yaw; + float _pitch = 18f; + float _dist = 8f; + float _cineAngle; + PlayerAvatar _local; void Start() { if (GetComponent() == null) gameObject.AddComponent(); - // Retire the old bespoke bot-only follow cam if it's still on this camera. var legacy = GetComponent(); if (legacy != null) legacy.enabled = false; - var b = GameObject.Find("Bot"); if (b != null) _bot = b.transform; - var s = GameObject.Find("StandPoint_0"); if (s != null) _standOverlook = s.transform; + var b = GameObject.Find("Bot"); if (b != null) _bot = b.transform; + var s = GameObject.Find("StandPoint_0"); if (s != null) _standOverlook = s.transform; var holder = new GameObject("CameraRigs").transform; _avatar = Make(holder, "CM_AvatarFollow"); @@ -85,31 +103,50 @@ namespace Jankenbots.Prototype Apply(_mode); } - // A seat claim/leave re-asserts the sensible default (clears a manual override). + // Seat claim/leave re-asserts the sensible default (clears a manual override). if (_local != null && _local.IsSeatedLocal != _lastSeated) { _lastSeated = _local.IsSeatedLocal; _userOverride = false; } - if (!_userOverride && _local != null) { Mode want = _local.IsSeatedLocal ? Mode.RobotFollow : Mode.AvatarFollow; if (want != _mode) { _mode = want; Apply(_mode); } } + + // Look / zoom input — only for the orbitable follow cams. + if (_mode == Mode.AvatarFollow || _mode == Mode.RobotFollow) + { + var m = Mouse.current; + if (m != null) + { + if (m.rightButton.isPressed) + { + Vector2 d = m.delta.ReadValue(); + _yaw += d.x * lookSensitivity; + _pitch = Mathf.Clamp(_pitch - d.y * lookSensitivity, minPitch, maxPitch); + } + float scroll = m.scroll.ReadValue().y; + if (Mathf.Abs(scroll) > 0.01f) + _dist = Mathf.Clamp(_dist - Mathf.Sign(scroll) * zoomSensitivity, minDistance, maxDistance); + } + } } void LateUpdate() { - if (_local != null) PoseFollow(_avatar, _local.transform, avatarOffset); + if (_local != null) PoseOrbit(_avatar, _local.transform); if (_bot != null) { - PoseFollow(_robot, _bot, robotOffset); + PoseOrbit(_robot, _bot); - _orbitAngle += orbitSpeed * Time.deltaTime; - Vector3 off = Quaternion.Euler(0f, _orbitAngle, 0f) * new Vector3(0f, orbitHeight, -orbitRadius); - PoseFollow(_orbit, _bot, off); + _cineAngle += orbitSpeed * Time.deltaTime; + Vector3 focus = _bot.position + Vector3.up * lookHeight; + Vector3 cineOff = Quaternion.Euler(0f, _cineAngle, 0f) * new Vector3(0f, orbitHeight, -orbitRadius); + _orbit.transform.position = focus + cineOff; + _orbit.transform.rotation = Quaternion.LookRotation(focus - _orbit.transform.position); if (_standOverlook != null) { @@ -120,11 +157,14 @@ namespace Jankenbots.Prototype } } - void PoseFollow(CinemachineCamera vc, Transform target, Vector3 worldOffset) + // Player-controllable orbit: spherical offset from the target driven by _yaw/_pitch/_dist. + void PoseOrbit(CinemachineCamera vc, Transform target) { - Vector3 pos = target.position + worldOffset; + Vector3 focus = target.position + Vector3.up * lookHeight; + Vector3 dir = Quaternion.Euler(_pitch, _yaw, 0f) * Vector3.back; + Vector3 pos = focus + dir * _dist; vc.transform.position = pos; - vc.transform.rotation = Quaternion.LookRotation((target.position + Vector3.up * lookHeight) - pos); + vc.transform.rotation = Quaternion.LookRotation(focus - pos); } PlayerAvatar FindLocal() @@ -138,6 +178,10 @@ namespace Jankenbots.Prototype { for (int i = 0; i < _all.Length; i++) _all[i].Priority = ((int)m == i) ? 20 : 0; + + // Reset zoom to the mode's default distance on switch (keep the look angle). + if (m == Mode.AvatarFollow) _dist = avatarDistance; + else if (m == Mode.RobotFollow) _dist = robotDistance; } } } From 30d0b278136c9a4276f40dbe21e4c479c31cefc7 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sun, 12 Jul 2026 21:02:22 +0100 Subject: [PATCH 3/8] controls: on-foot always-on mouse-look + camera-relative WASD + E/Q claim - CameraDirector: AvatarFollow now uses always-on mouse-look (no RMB) with a locked/hidden cursor; Esc toggles the cursor free; RobotFollow keeps RMB-look. - PlayerAvatar: movement is camera-relative (W = camera forward, character turns to face where it runs) instead of world/cardinal. - PlayerAvatar: E claims a tread on foot, Q leaves while seated (locked cursor makes the OnGUI buttons unclickable). - Verified in play: cursor locks in on-foot mode, camera-relative basis correct, no runtime errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- Assets/Scripts/Camera/CameraDirector.cs | 13 +++++++++++-- Assets/Scripts/Player/PlayerAvatar.cs | 23 ++++++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Assets/Scripts/Camera/CameraDirector.cs b/Assets/Scripts/Camera/CameraDirector.cs index c30d6f2..770120d 100644 --- a/Assets/Scripts/Camera/CameraDirector.cs +++ b/Assets/Scripts/Camera/CameraDirector.cs @@ -61,6 +61,7 @@ namespace Jankenbots.Prototype float _pitch = 18f; float _dist = 8f; float _cineAngle; + bool _cursorFree; // Esc toggles the locked cursor free (for GUI / to escape play) PlayerAvatar _local; @@ -115,13 +116,21 @@ namespace Jankenbots.Prototype if (want != _mode) { _mode = want; Apply(_mode); } } - // Look / zoom input — only for the orbitable follow cams. + // Cursor + look. On-foot (AvatarFollow) uses ALWAYS-ON mouse-look with a + // locked cursor (no RMB needed); other modes keep the cursor free (RobotFollow + // orbits while RMB is held). Esc toggles the cursor free to reach GUI / escape. + if (kb != null && kb.escapeKey.wasPressedThisFrame) _cursorFree = !_cursorFree; + bool avatarLook = _mode == Mode.AvatarFollow && !_cursorFree; + Cursor.lockState = avatarLook ? CursorLockMode.Locked : CursorLockMode.None; + Cursor.visible = !avatarLook; + if (_mode == Mode.AvatarFollow || _mode == Mode.RobotFollow) { var m = Mouse.current; if (m != null) { - if (m.rightButton.isPressed) + bool look = _mode == Mode.AvatarFollow ? avatarLook : m.rightButton.isPressed; + if (look) { Vector2 d = m.delta.ReadValue(); _yaw += d.x * lookSensitivity; diff --git a/Assets/Scripts/Player/PlayerAvatar.cs b/Assets/Scripts/Player/PlayerAvatar.cs index 097acd6..97a22fa 100644 --- a/Assets/Scripts/Player/PlayerAvatar.cs +++ b/Assets/Scripts/Player/PlayerAvatar.cs @@ -104,6 +104,15 @@ namespace Jankenbots.Prototype else ExitSeated(); } + // Claim / leave a tread by KEY. On-foot mouse-look locks the cursor, so + // the OnGUI Claim/Leave buttons aren't clickable — E claims, Q leaves. + var kb = Keyboard.current; + if (_seats != null && kb != null) + { + if (!_seated && kb.eKey.wasPressedThisFrame) _seats.ClaimSeatRpc(); + else if (_seated && kb.qKey.wasPressedThisFrame) _seats.ReleaseSeatRpc(); + } + // On foot → drive the avatar. Seated → do nothing (control is on the tread). if (!_seated) OwnerMove(); } @@ -169,7 +178,19 @@ namespace Jankenbots.Prototype if (kb.sKey.isPressed || kb.downArrowKey.isPressed) v -= 1f; } - Vector3 input = new Vector3(h, 0f, v); + // CAMERA-RELATIVE movement: W goes the way the camera (mouse) is looking, + // not world +Z. Basis is the active game camera flattened onto the ground. + Vector3 camF = Vector3.forward, camR = Vector3.right; + var cam = Camera.main; + if (cam != null) + { + camF = Vector3.ProjectOnPlane(cam.transform.forward, Vector3.up); + if (camF.sqrMagnitude < 1e-4f) camF = Vector3.ProjectOnPlane(cam.transform.up, Vector3.up); // looking straight down + camF.Normalize(); + camR = Vector3.ProjectOnPlane(cam.transform.right, Vector3.up).normalized; + } + + Vector3 input = camR * h + camF * v; if (input.sqrMagnitude > 1f) input.Normalize(); Vector3 desired = input * moveSpeed; From 93b8fae66bc345d08ef72574c4e1545a82c44a54 Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sun, 12 Jul 2026 21:20:05 +0100 Subject: [PATCH 4/8] bot: fix slow/unsteerable driving + controls tooltip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnosis (agent + live values): tuning & tread geometry were fine, debugAutoDrive off — the real drag was the box chassis sliding on default ~0.6 friction, which ate forward speed AND resisted yaw (dead steering). - Bot_LowFriction physics material (0.3, Minimum combine) on the chassis → slides + yaws freely. - TreadPart yaw-assist (220 N·m, sign reinforces natural differential) so a throttle difference gives a clear turn; cancels to zero on equal throttle (straight kept). - SeatManager: context-aware controls tooltip under the seat box (on-foot vs driving), with a 'turn: differ vs teammate' hint. Co-Authored-By: Claude Opus 4.8 (1M context) --- Assets/Physics/Bot_LowFriction.physicsMaterial | 15 +++++++++++++++ .../Physics/Bot_LowFriction.physicsMaterial.meta | 7 +++++++ Assets/Scenes/M1_Arena.unity | 16 ++++++++++++---- Assets/Scripts/Bot/SeatManager.cs | 14 ++++++++++++++ Assets/Scripts/Bot/TreadPart.cs | 10 ++++++++++ 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 Assets/Physics/Bot_LowFriction.physicsMaterial create mode 100644 Assets/Physics/Bot_LowFriction.physicsMaterial.meta diff --git a/Assets/Physics/Bot_LowFriction.physicsMaterial b/Assets/Physics/Bot_LowFriction.physicsMaterial new file mode 100644 index 0000000..8f8abf7 --- /dev/null +++ b/Assets/Physics/Bot_LowFriction.physicsMaterial @@ -0,0 +1,15 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!134 &13400000 +PhysicsMaterial: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Bot_LowFriction + serializedVersion: 2 + m_DynamicFriction: 0.3 + m_StaticFriction: 0.3 + m_Bounciness: 0 + m_FrictionCombine: 2 + m_BounceCombine: 2 diff --git a/Assets/Physics/Bot_LowFriction.physicsMaterial.meta b/Assets/Physics/Bot_LowFriction.physicsMaterial.meta new file mode 100644 index 0000000..3fc2129 --- /dev/null +++ b/Assets/Physics/Bot_LowFriction.physicsMaterial.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6c687d24b6f5fbb4783d28d635f71a29 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/M1_Arena.unity b/Assets/Scenes/M1_Arena.unity index 2983be5..9627236 100644 --- a/Assets/Scenes/M1_Arena.unity +++ b/Assets/Scenes/M1_Arena.unity @@ -1639,12 +1639,18 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: cd21368c9a8b1064d913c375fb607c0b, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::Jankenbots.Prototype.CameraDirector - avatarOffset: {x: 0, y: 4, z: -6} - robotOffset: {x: 0, y: 8, z: -12} + avatarDistance: 8 + robotDistance: 16 + lookSensitivity: 0.18 + zoomSensitivity: 4 + minDistance: 3 + maxDistance: 80 + minPitch: -8 + maxPitch: 78 + lookHeight: 1.2 orbitRadius: 20 orbitHeight: 9 orbitSpeed: 18 - lookHeight: 1.2 --- !u!1001 &579422633 PrefabInstance: m_ObjectHideFlags: 0 @@ -3441,7 +3447,7 @@ BoxCollider: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 781174154} - m_Material: {fileID: 0} + m_Material: {fileID: 13400000, guid: 6c687d24b6f5fbb4783d28d635f71a29, type: 2} m_IncludeLayers: serializedVersion: 2 m_Bits: 0 @@ -5564,6 +5570,7 @@ MonoBehaviour: chassis: {fileID: 0} cruiseForce: 450 throttleDeadzone: 0.08 + yawAssistTorque: 220 pivotAnchorStrength: 1500 pivotAngularResistance: 400 leanTorque: 500 @@ -5699,6 +5706,7 @@ MonoBehaviour: chassis: {fileID: 0} cruiseForce: 450 throttleDeadzone: 0.08 + yawAssistTorque: 220 pivotAnchorStrength: 1500 pivotAngularResistance: 400 leanTorque: 500 diff --git a/Assets/Scripts/Bot/SeatManager.cs b/Assets/Scripts/Bot/SeatManager.cs index 433ad70..abc1f88 100644 --- a/Assets/Scripts/Bot/SeatManager.cs +++ b/Assets/Scripts/Bot/SeatManager.cs @@ -179,6 +179,20 @@ namespace Jankenbots.Prototype { ReleaseSeatRpc(); } + + // Controls hint under the seat box — context-aware (on foot vs driving). + int ty = y + h + 6, th = 54; + GUI.Box(new Rect(x, ty, w, th), "Controls"); + if (mine.HasValue) + { + GUI.Label(new Rect(x + 10, ty + 20, w - 20, 18), "W throttle · LShift pivot · A/D lean"); + GUI.Label(new Rect(x + 10, ty + 34, w - 20, 18), "Q leave · V camera · turn: differ vs teammate"); + } + else + { + GUI.Label(new Rect(x + 10, ty + 20, w - 20, 18), "WASD move · Mouse look · Scroll zoom"); + GUI.Label(new Rect(x + 10, ty + 34, w - 20, 18), "E claim tread · V camera · Esc free cursor"); + } } static string SeatLabel(ulong pilot) => pilot == NoPilot ? "free" : $"#{pilot}"; diff --git a/Assets/Scripts/Bot/TreadPart.cs b/Assets/Scripts/Bot/TreadPart.cs index 50c6e0d..c852ea9 100644 --- a/Assets/Scripts/Bot/TreadPart.cs +++ b/Assets/Scripts/Bot/TreadPart.cs @@ -63,6 +63,9 @@ namespace Jankenbots.Prototype [Range(0f, 0.5f)] public float throttleDeadzone = 0.08f; + [Tooltip("Extra yaw torque (N·m) at full throttle, REINFORCING the natural differential turn so steering reads clearly even with ground friction. Left/Right push opposite ways, so equal throttle cancels to zero → straight line preserved. Set 0 to rely on pure emergent differential. Flip sign in code if it ever fights the turn.")] + public float yawAssistTorque = 220f; + // ---- (b) LOCK-PIVOT tuning ----------------------------------------------- [Header("(b) Lock-pivot — plant this tread as an anchor")] [Tooltip("How hard the planted tread resists the chassis sliding at its position.")] @@ -192,6 +195,13 @@ namespace Jankenbots.Prototype { Vector3 drive = transform.forward * (_throttle * cruiseForce); chassis.AddForceAtPosition(drive, treadPos, ForceMode.Force); + + // Yaw assist: reinforce the differential turn (Left throttles → nose right, + // Right throttles → nose left). Cancels to zero when both throttle equally, + // so straight-line driving is untouched; the maxAngularVelocity clamp still + // caps total spin so this can't reintroduce an uncontrolled tip. + float yawSign = side == Side.Left ? 1f : -1f; + chassis.AddTorque(chassis.transform.up * (yawSign * _throttle * yawAssistTorque), ForceMode.Force); } // ---- (c) PASSIVE LEAN — anti-tip ballast --------------------------- From c25407be15cb634a0c13a5dd80744192f66573fc Mon Sep 17 00:00:00 2001 From: megaproxy Date: Sun, 12 Jul 2026 21:22:35 +0100 Subject: [PATCH 5/8] Bump version 0.1.0 -> 0.2.0 --- ProjectSettings/ProjectSettings.asset | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index ecbbda3..e54c43c 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -148,7 +148,7 @@ PlayerSettings: loadStoreDebugModeEnabled: 0 visionOSBundleVersion: 1.0 tvOSBundleVersion: 1.0 - bundleVersion: 0.1.0 + bundleVersion: 0.2.0 preloadedAssets: - {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3} metroInputSource: 0 From bba60d4527dd4b74593ca8a8a7274114879ae234 Mon Sep 17 00:00:00 2001 From: Monqui Date: Mon, 13 Jul 2026 19:16:49 -0400 Subject: [PATCH 6/8] Test commit. --- ... Collection 3 - Built-In.unitypackage.meta | 15 ----- ...cter Collection 3 - HDRP.unitypackage.meta | 15 ----- Assets/Scenes/M1_Arena.unity | 58 +++++++++++++++++++ ...niversalRenderPipelineGlobalSettings.asset | 17 +----- 4 files changed, 59 insertions(+), 46 deletions(-) delete mode 100644 Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low Poly Character Collection 3 - Built-In.unitypackage.meta delete mode 100644 Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low Poly Character Collection 3 - HDRP.unitypackage.meta diff --git a/Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low Poly Character Collection 3 - Built-In.unitypackage.meta b/Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low Poly Character Collection 3 - Built-In.unitypackage.meta deleted file mode 100644 index f84d6fe..0000000 --- a/Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low Poly Character Collection 3 - Built-In.unitypackage.meta +++ /dev/null @@ -1,15 +0,0 @@ -fileFormatVersion: 2 -guid: fe35bb42b8a0bd144a6e5d1d670079ed -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 388758 - packageName: Low Poly Character Collection 3 - packageVersion: 1.1 - assetPath: Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low - Poly Character Collection 3 - Built-In.unitypackage - uploadId: 946460 diff --git a/Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low Poly Character Collection 3 - HDRP.unitypackage.meta b/Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low Poly Character Collection 3 - HDRP.unitypackage.meta deleted file mode 100644 index 0c49352..0000000 --- a/Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low Poly Character Collection 3 - HDRP.unitypackage.meta +++ /dev/null @@ -1,15 +0,0 @@ -fileFormatVersion: 2 -guid: 6236278daa4d5c441a8232dab5ebc22d -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: -AssetOrigin: - serializedVersion: 1 - productId: 388758 - packageName: Low Poly Character Collection 3 - packageVersion: 1.1 - assetPath: Assets/Hodaart/HodaartLowPolyCharacterCollection3/Packages/Hodaart Low - Poly Character Collection 3 - HDRP.unitypackage - uploadId: 946460 diff --git a/Assets/Scenes/M1_Arena.unity b/Assets/Scenes/M1_Arena.unity index 9627236..b25770a 100644 --- a/Assets/Scenes/M1_Arena.unity +++ b/Assets/Scenes/M1_Arena.unity @@ -4398,6 +4398,63 @@ Transform: m_CorrespondingSourceObject: {fileID: 400000, guid: 00ba75879d6ef5745a4888259bde7c0a, type: 3} m_PrefabInstance: {fileID: 954444310} m_PrefabAsset: {fileID: 0} +--- !u!1001 &974200701 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 100000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_Name + value: Bath + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalPosition.x + value: 6.622743 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalPosition.y + value: 0.10000037 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalPosition.z + value: -0.529232 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 400000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: ae8352c51c6124747aa9e8a7374b86af, type: 3} --- !u!1001 &974860797 PrefabInstance: m_ObjectHideFlags: 0 @@ -8696,3 +8753,4 @@ SceneRoots: - {fileID: 850707848} - {fileID: 326006527} - {fileID: 1532509717} + - {fileID: 974200701} diff --git a/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset b/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset index f005b0b..2b0e035 100644 --- a/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset +++ b/Assets/Settings/UniversalRenderPipelineGlobalSettings.asset @@ -68,22 +68,7 @@ MonoBehaviour: - rid: 7808513024541851664 - rid: 7808513024541851665 m_RuntimeSettings: - m_List: - - rid: 6852985685364965378 - - rid: 6852985685364965379 - - rid: 6852985685364965380 - - rid: 6852985685364965381 - - rid: 6852985685364965384 - - rid: 6852985685364965392 - - rid: 6852985685364965394 - - rid: 8712630790384254976 - - rid: 7808513024541851653 - - rid: 7808513024541851655 - - rid: 7808513024541851656 - - rid: 7808513024541851657 - - rid: 7808513024541851658 - - rid: 7808513024541851662 - - rid: 7808513024541851664 + m_List: [] m_AssetVersion: 10 m_ObsoleteDefaultVolumeProfile: {fileID: 0} m_RenderingLayerNames: From 8e4a19b7d8309232d4c235972a87d200af8215bd Mon Sep 17 00:00:00 2001 From: megaproxy Date: Tue, 14 Jul 2026 21:06:43 +0100 Subject: [PATCH 7/8] scripts: add SessionStart git/Forgejo digest check --- scripts/session-git-check.sh | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 scripts/session-git-check.sh diff --git a/scripts/session-git-check.sh b/scripts/session-git-check.sh new file mode 100644 index 0000000..61b1ccf --- /dev/null +++ b/scripts/session-git-check.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# SessionStart digest: fetch remote, report incoming commits + open Forgejo +# issues/PRs. Emits a Claude hook JSON blob on stdout (additionalContext) so +# Claude can surface anything actionable at the top of the session. +# Always exits 0 and always prints valid JSON — must never block a session. + +set -o pipefail +cd "$(dirname "$0")/.." 2>/dev/null || exit 0 + +digest="" +add() { digest+="$1"$'\n'; } + +# --- Remote fetch + incoming/outgoing commit comparison ------------------ +if git rev-parse --git-dir >/dev/null 2>&1; then + git fetch --all --quiet 2>/dev/null + upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) + if [ -n "$upstream" ]; then + incoming=$(git log --oneline "HEAD..$upstream" 2>/dev/null) + outgoing=$(git log --oneline "$upstream..HEAD" 2>/dev/null) + if [ -n "$incoming" ]; then + add "INCOMING commits on $upstream (you are behind — pull to get them):" + while IFS= read -r l; do add " $l"; done <<< "$incoming" + else + add "Up to date with $upstream (no incoming commits)." + fi + [ -n "$outgoing" ] && add "You have $(wc -l <<< "$outgoing") local commit(s) not yet pushed." + fi +fi + +# --- Forgejo open issues + PRs ------------------------------------------- +remote_url=$(git remote get-url origin 2>/dev/null) +if [[ "$remote_url" =~ ^https?://([^/]+)/([^/]+)/([^/.]+) ]]; then + host="${BASH_REMATCH[1]}"; owner="${BASH_REMATCH[2]}"; repo="${BASH_REMATCH[3]}" + cred=$(grep -m1 "$host" "$HOME/.git-credentials" 2>/dev/null) + tok=$(sed -E 's#https?://([^@]+)@.*#\1#' <<< "$cred") + api="https://$host/api/v1/repos/$owner/$repo/issues" + parse='import sys,json +try: d=json.load(sys.stdin) +except: sys.exit(0) +for i in d: print(" #%d %s (by %s)"%(i["number"],i["title"],i["user"]["login"]))' + if [ -n "$tok" ]; then + issues=$(curl -sf --max-time 8 -u "$tok" "$api?state=open&type=issues" 2>/dev/null | python3 -c "$parse" 2>/dev/null) + prs=$(curl -sf --max-time 8 -u "$tok" "$api?state=open&type=pulls" 2>/dev/null | python3 -c "$parse" 2>/dev/null) + if [ -n "$issues" ]; then add "OPEN ISSUES:"; add "$issues"; else add "No open issues."; fi + if [ -n "$prs" ]; then add "OPEN PULL REQUESTS:"; add "$prs"; else add "No open PRs."; fi + fi +fi + +# --- Emit hook JSON ------------------------------------------------------ +ctx="[Session start — repo/remote status] +$digest +If there are INCOMING commits or any open issues/PRs, briefly tell the user at the start of your first reply. If everything is up to date with no open items, do not mention this." + +python3 - "$ctx" <<'PY' +import json,sys +print(json.dumps({"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":sys.argv[1]},"suppressOutput":True})) +PY +exit 0 From 5dd5c760d7ffe6acc2fd75b436f72199b57129bf Mon Sep 17 00:00:00 2001 From: megaproxy Date: Tue, 14 Jul 2026 21:10:32 +0100 Subject: [PATCH 8/8] scripts: warn when incoming commits touch Unity scene/asset files --- scripts/session-git-check.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/session-git-check.sh b/scripts/session-git-check.sh index 61b1ccf..61cbab8 100644 --- a/scripts/session-git-check.sh +++ b/scripts/session-git-check.sh @@ -20,6 +20,14 @@ if git rev-parse --git-dir >/dev/null 2>&1; then if [ -n "$incoming" ]; then add "INCOMING commits on $upstream (you are behind — pull to get them):" while IFS= read -r l; do add " $l"; done <<< "$incoming" + # Flag incoming edits to Unity scene/prefab/asset files — risky to pull + # while the Editor is open (it may overwrite them or hold a stale copy). + scenefiles=$(git diff --name-only "HEAD..$upstream" 2>/dev/null | grep -Ei '\.(unity|prefab|asset|mat|controller)$') + if [ -n "$scenefiles" ]; then + add " *** WARNING: incoming commits change Unity scene/asset files:" + while IFS= read -r f; do add " $f"; done <<< "$scenefiles" + add " *** CLOSE or save the Unity Editor before pulling to avoid clobbering scene state." + fi else add "Up to date with $upstream (no incoming commits)." fi