{"TotalCount":48,"Files":[{"Ident":"fieldguide.tips","Path":"Code/Demo/TipsDemoBootstrap.cs","FileName":"TipsDemoBootstrap.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// Wires the demo scene in one component so the whole kit runs from a single press of play: it points the\r\n/// \u003Csee cref=\u0022TipsWorld\u0022/\u003E seams at the demo pawn, creates the coach and its display with\r\n/// \u003Csee cref=\u0022TipsCoach.Ensure\u0022/\u003E, and pushes a context every frame carrying the demo flag the demo tips\r\n/// gate on. The tips themselves are authored assets (\u003Cc\u003EAssets/demo/*.tip\u003C/c\u003E plus\r\n/// \u003Cc\u003EAssets/starter.tip\u003C/c\u003E), not code, so the scene also shows the no-code authoring path.\r\n///\r\n/// THE DEMO FLAG. Every demo tip\u0027s Relevance is \u003Cc\u003EFlag(\u0022fg_tips_demo\u0022)\u003C/c\u003E, and only this component sets\r\n/// that flag. A game that vendors the kit and never runs this scene therefore never sees a demo tip, even\r\n/// if it forgets to delete the assets: the tips merge into the catalog but stay irrelevant forever. Delete\r\n/// them anyway.\r\n///\r\n/// THE ENDING. The walkthrough hands the player to the authoring tool: the last tip coaches \u003Cc\u003ET\u003C/c\u003E, and\r\n/// opening the Tips Studio is what retires it. Two pieces do that, both of them demo wiring rather than\r\n/// kit runtime. This component builds the Studio\u0027s host object at boot (the same one line a game writes\r\n/// from the README), and it raises the \u003Csee cref=\u0022StudioOpenedSignal\u0022/\u003E string signal while the Studio is\r\n/// open, which is the completion \u003Cc\u003Edemo_wrap.tip\u003C/c\u003E waits on.\r\n///\r\n/// Not part of the kit\u0027s runtime surface: delete \u003Cc\u003ECode/Demo\u003C/c\u003E and \u003Cc\u003EAssets/demo\u003C/c\u003E when you drop the\r\n/// kit into your own project, and write your own bootstrap from the README instead.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Tips Demo Bootstrap\u0022 )]\r\n[Category( \u0022Field Guide Tips\u0022 )]\r\n[Icon( \u0022auto_awesome\u0022 )]\r\npublic sealed class TipsDemoBootstrap : Component\r\n{\r\n\t/// \u003Csummary\u003EThe context flag every demo tip\u0027s Relevance reads, so demo content is inert anywhere this\r\n\t/// component is not running.\u003C/summary\u003E\r\n\tpublic const string DemoFlag = \u0022fg_tips_demo\u0022;\r\n\r\n\t/// \u003Csummary\u003EProgress file the demo writes, kept apart from the kit default so replaying the demo never\r\n\t/// touches the progress your own game saves.\u003C/summary\u003E\r\n\tpublic const string DemoSaveFile = \u0022fieldguide_tips_demo.json\u0022;\r\n\r\n\t/// \u003Csummary\u003EThe string signal this component latches while the Tips Studio is open, and the completion\r\n\t/// \u003Cc\u003Edemo_wrap.tip\u003C/c\u003E waits on. A demo-side name: the kit knows nothing about it, which is the point.\r\n\t/// Any game can retire a tip on its own UI the same way, with one \u003Csee cref=\u0022TipsCoach.Signal(string)\u0022/\u003E\r\n\t/// call and a Signal trigger on the tip.\u003C/summary\u003E\r\n\tpublic const string StudioOpenedSignal = \u0022fg_tips_studio_opened\u0022;\r\n\r\n\t/// \u003Csummary\u003ERaw key that resets progress and replays the sequence from the top.\u003C/summary\u003E\r\n\t[Property] public string ReplayKey { get; set; } = \u0022r\u0022;\r\n\r\n\tprivate TipsCoach _coach;\r\n\tprivate TipsDemoPawn _pawn;\r\n\tprivate string _previousSaveFile;\r\n\tprivate Func\u003Cbool\u003E _previousHasPlayer;\r\n\tprivate Func\u003CVector3\u003E _previousPlayerPosition;\r\n\tprivate Func\u003CRay\u003E _previousAimRay;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_pawn = Scene.GetAllComponents\u003CTipsDemoPawn\u003E().FirstOrDefault();\r\n\r\n\t\t// Set the save file BEFORE Ensure: the first load reads whatever name is set then.\r\n\t\t_previousSaveFile = TipsCoach.SaveFileName;\r\n\t\tTipsCoach.SaveFileName = DemoSaveFile;\r\n\r\n\t\t// Always start the demo from the top, the way a first-time player would see it.\r\n\t\tTipsCoach.ResetProgress();\r\n\r\n\t\t// World seams. A library cannot reach into your player, so the demo hands the kit its pawn. The\r\n\t\t// marker zone\u0027s PlayerEntered trigger reads LocalPlayerPosition; nothing here uses LookedAt, so\r\n\t\t// AimRay stays null and those triggers stay inert. They are statics, so the old values are kept\r\n\t\t// and handed back in OnDestroy: a demo pawn that no longer exists must not answer for a game\r\n\t\t// scene opened later in the same session.\r\n\t\t_previousHasPlayer = TipsWorld.HasLocalPlayer;\r\n\t\t_previousPlayerPosition = TipsWorld.LocalPlayerPosition;\r\n\t\t_previousAimRay = TipsWorld.AimRay;\r\n\r\n\t\tTipsWorld.HasLocalPlayer = () =\u003E _pawn.IsValid();\r\n\t\tTipsWorld.LocalPlayerPosition = () =\u003E _pawn.IsValid() ? _pawn.WorldPosition : Vector3.Zero;\r\n\t\tTipsWorld.AimRay = null;\r\n\r\n\t\t_coach = TipsCoach.Ensure( Scene );\r\n\t\tEnsureStudioHost();\r\n\r\n\t\tLog.Info( \u0022[tips] demo ready: move with W A S D or the left stick, jump with Space or A, \u0022 \u002B\r\n\t\t\t\u0022switch the marker on inside its ring, then press T for the Tips Studio. \u0022 \u002B\r\n\t\t\t$\u0022Press {ReplayKey.ToUpperInvariant()} to replay.\u0022 );\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( _coach is null )\r\n\t\t\treturn;\r\n\r\n\t\t// Ignored while the Studio is open. The key is read raw, so without this an \u0022r\u0022 typed into one of\r\n\t\t// the Studio\u0027s text boxes would restart the walkthrough under the panel, and the walkthrough now\r\n\t\t// ENDS in that panel. Closed, the last beat is there to walk again.\r\n\t\tif ( !TipsStudio.Open \u0026\u0026 Input.Keyboard.Pressed( ReplayKey ) )\r\n\t\t\tReplay();\r\n\r\n\t\t// The last beat. Latched while the Studio is OPEN rather than on the frame it opens: a latch is\r\n\t\t// idempotent, so pushing it every frame costs nothing, and it leaves no edge state to go stale\r\n\t\t// when the walkthrough is reset out from under it. Raised before the tick below, because the tick\r\n\t\t// is what reads it.\r\n\t\tif ( TipsStudio.Open )\r\n\t\t\t_coach.Signal( StudioOpenedSignal );\r\n\r\n\t\t// The context path: one small neutral struct per frame. The demo only needs two things in it, a\r\n\t\t// player and the demo flag; a real game fills the fields its own tips read.\r\n\t\tvar ctx = new TipContext { HasPlayer = _pawn.IsValid() };\r\n\t\tctx.SetFlag( DemoFlag, true );\r\n\t\t_coach.Tick( ctx );\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\t// Hand the statics back so a game scene opened later in the same editor session sees the kit\r\n\t\t// exactly as it was before the demo ran.\r\n\t\tif ( !string.IsNullOrEmpty( _previousSaveFile ) )\r\n\t\t\tTipsCoach.SaveFileName = _previousSaveFile;\r\n\r\n\t\tif ( _previousHasPlayer is not null )\r\n\t\t\tTipsWorld.HasLocalPlayer = _previousHasPlayer;\r\n\t\telse\r\n\t\t\tTipsWorld.HasLocalPlayer = static () =\u003E true;\r\n\r\n\t\tTipsWorld.LocalPlayerPosition = _previousPlayerPosition;\r\n\t\tTipsWorld.AimRay = _previousAimRay;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Build the Tips Studio\u0027s host: one \u003Csee cref=\u0022ScreenPanel\u0022/\u003E carrying the panel, created in code so\r\n\t/// the demo scene file holds no razor reference (the same idiom the other kits\u0027 demos use). It sits\r\n\t/// above the coach\u0027s own panel, since the Studio is a modal over the card it is editing. The Studio\r\n\t/// ships CLOSED and opens on its own OpenKey, which is T; nothing here opens it.\r\n\t///\r\n\t/// Idempotent: a scene that already carries a Studio panel of its own keeps it.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void EnsureStudioHost()\r\n\t{\r\n\t\tif ( Scene.GetAllComponents\u003CTipsStudioPanel\u003E().Any() )\r\n\t\t\treturn;\r\n\r\n\t\tvar go = Scene.CreateObject();\r\n\t\tgo.Name = \u0022UI.TipsStudio\u0022;\r\n\r\n\t\tvar screen = go.Components.Create\u003CScreenPanel\u003E();\r\n\t\tscreen.ZIndex = 60; // above the coach\u0027s card at 50\r\n\r\n\t\tgo.Components.Create\u003CTipsStudioPanel\u003E();\r\n\t}\r\n\r\n\tprivate void Replay()\r\n\t{\r\n\t\tTipsCoach.ResetProgress();\r\n\r\n\t\tforeach ( var marker in Scene.GetAllComponents\u003CTipsDemoMarker\u003E() )\r\n\t\t\tmarker.ResetMarker();\r\n\r\n\t\t_pawn?.ResetPawn();\r\n\t\tLog.Info( \u0022[tips] demo replayed from the first tip.\u0022 );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Demo/TipsDemoMarker.cs","FileName":"TipsDemoMarker.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The demo scene\u0027s one interactable: a marker the citizen walks up to and switches on. It owns its own\r\n/// state (off / on) and tells the kit about the use through \u003Csee cref=\u0022TipTriggerObject.NotifyInteracted\u0022/\u003E,\r\n/// which is exactly the one line a real game writes from wherever it already handles \u0022the player used\r\n/// this object\u0022. The proximity half of the beat is a second \u003Csee cref=\u0022TipTriggerObject\u0022/\u003E on the sibling\r\n/// zone object, in \u003Csee cref=\u0022TipTriggerObject.Mode.PlayerEntered\u0022/\u003E, so no code is involved there at all.\r\n///\r\n/// Note the ordering: the marker gates on ITS OWN rule (the pawn is inside the ring and the marker is\r\n/// still off), never on which tip is showing. Game logic drives tips, not the other way round.\r\n///\r\n/// Not part of the kit\u0027s runtime surface: delete \u003Cc\u003ECode/Demo\u003C/c\u003E and \u003Cc\u003EAssets/demo\u003C/c\u003E when you drop\r\n/// the kit into your own project.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Tips Demo Marker\u0022 )]\r\n[Category( \u0022Field Guide Tips\u0022 )]\r\n[Icon( \u0022emoji_objects\u0022 )]\r\npublic sealed class TipsDemoMarker : Component\r\n{\r\n\t/// \u003Csummary\u003EHow close the pawn has to be, in world units, before the marker accepts the switch.\u003C/summary\u003E\r\n\t[Property] public float Radius { get; set; } = 110f;\r\n\r\n\t/// \u003Csummary\u003EThe Input.config action that switches the marker on. The demo reuses Jump so the prompt\r\n\t/// reads Space on a keyboard and A on a pad with no extra binding.\u003C/summary\u003E\r\n\t[Property] public string SwitchAction { get; set; } = \u0022Jump\u0022;\r\n\r\n\t[Property] public Color OffTint { get; set; } = new Color( 1f, 0.62f, 0.18f );\r\n\t[Property] public Color OnTint { get; set; } = new Color( 0.35f, 0.95f, 0.5f );\r\n\r\n\tprivate TipsDemoPawn _pawn;\r\n\tprivate ModelRenderer _renderer;\r\n\tprivate bool _on;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_renderer = Components.Get\u003CModelRenderer\u003E();\r\n\t\t_pawn = Scene.GetAllComponents\u003CTipsDemoPawn\u003E().FirstOrDefault();\r\n\t\tPaint();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( _on || !PawnInside() )\r\n\t\t\treturn;\r\n\t\tif ( !Input.Pressed( SwitchAction ) )\r\n\t\t\treturn;\r\n\r\n\t\t_on = true;\r\n\t\tPaint();\r\n\r\n\t\t// The world-trigger seam: the game says \u0022this object was used\u0022 and the TipTriggerObject on it\r\n\t\t// retires the tip it is bound to. A game that vendors fieldguide.interaction wires the same call\r\n\t\t// to its InteractionPerformed event instead.\r\n\t\tTipTriggerObject.NotifyInteracted( GameObject );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue while the demo pawn stands inside the marker ring (flat distance, height ignored so a\r\n\t/// hop does not drop the player out of the ring).\u003C/summary\u003E\r\n\tpublic bool PawnInside()\r\n\t\t=\u003E _pawn.IsValid() \u0026\u0026 WorldPosition.WithZ( 0f ).Distance( _pawn.WorldPosition.WithZ( 0f ) ) \u003C= Radius;\r\n\r\n\t/// \u003Csummary\u003ESwitch the marker back off (the demo\u0027s replay key).\u003C/summary\u003E\r\n\tpublic void ResetMarker()\r\n\t{\r\n\t\t_on = false;\r\n\t\tPaint();\r\n\t}\r\n\r\n\tprivate void Paint()\r\n\t{\r\n\t\tif ( _renderer.IsValid() )\r\n\t\t\t_renderer.Tint = _on ? OnTint : OffTint;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Demo/TipsDemoPawn.cs","FileName":"TipsDemoPawn.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The demo scene\u0027s stand-in player: a citizen that slides along the ground on the movement stick (or\r\n/// W/A/S/D) and hops on the jump action. Deliberately the simplest thing that can be coached: plain\r\n/// transform movement, one hand-integrated hop, no rigidbody, no collider, no controller. It exists so\r\n/// the tips in \u003Cc\u003EAssets/demo/\u003C/c\u003E have real actions to retire on.\r\n///\r\n/// THE LOOK. The scene authors this object with a plain box renderer. At boot the pawn switches that off\r\n/// and builds a dressed stock citizen in its place (\u003Csee cref=\u0022TipsDemoCitizen\u0022/\u003E), so the scene file\r\n/// stays as authored and the editor viewport still shows the simple block when nothing is playing. The\r\n/// movement, the play radius and the hop are untouched by the swap: only the visual changed.\r\n///\r\n/// Not part of the kit\u0027s runtime surface: delete \u003Cc\u003ECode/Demo\u003C/c\u003E and \u003Cc\u003EAssets/demo\u003C/c\u003E when you drop\r\n/// the kit into your own project, and coach your own player instead.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Tips Demo Pawn\u0022 )]\r\n[Category( \u0022Field Guide Tips\u0022 )]\r\n[Icon( \u0022smart_toy\u0022 )]\r\npublic sealed class TipsDemoPawn : Component\r\n{\r\n\t/// \u003Csummary\u003EGround speed in world units per second.\u003C/summary\u003E\r\n\t[Property] public float MoveSpeed { get; set; } = 220f;\r\n\r\n\t/// \u003Csummary\u003EUpward speed of one hop, in world units per second.\u003C/summary\u003E\r\n\t[Property] public float JumpSpeed { get; set; } = 260f;\r\n\r\n\t/// \u003Csummary\u003EDownward acceleration applied to a hop, in world units per second squared.\u003C/summary\u003E\r\n\t[Property] public float Gravity { get; set; } = 900f;\r\n\r\n\t/// \u003Csummary\u003EHow far from its start the citizen may wander. The demo camera is fixed, so this is what\r\n\t/// keeps the citizen in frame, and the scene\u0027s camera is framed to contain exactly this disc. The\r\n\t/// marker sits 205 units from the start, so 220 lets the citizen walk onto it and a little past\r\n\t/// without opening up a corner of the yard that the camera would then have to cover for nothing.\u003C/summary\u003E\r\n\t[Property] public float PlayRadius { get; set; } = 220f;\r\n\r\n\t/// \u003Csummary\u003EThe Input.config action that hops. Bound to Space on a keyboard and A on a pad in the\r\n\t/// s\u0026amp;box default config, which is what the demo tips prompt.\u003C/summary\u003E\r\n\t[Property] public string JumpAction { get; set; } = \u0022Jump\u0022;\r\n\r\n\t/// \u003Csummary\u003EYaw the demo camera looks along, so pushing forward moves the citizen away from the camera\r\n\t/// instead of sideways. Change it with the camera.\u003C/summary\u003E\r\n\t[Property] public float CameraYaw { get; set; } = 45f;\r\n\r\n\t/// \u003Csummary\u003ELocal Z of the citizen visual. The pawn object sits half a block above the ground because\r\n\t/// the authored box is centred on it, and the citizen\u0027s origin is at its feet, so the visual drops by\r\n\t/// that half height to stand on the floor instead of hovering.\u003C/summary\u003E\r\n\t[Property] public float VisualZOffset { get; set; } = -25f;\r\n\r\n\t/// \u003Csummary\u003EHow briskly the citizen turns to face where it is going, in turns per second-ish. High\r\n\t/// enough to read as responsive, low enough that a flick of the stick does not snap it.\u003C/summary\u003E\r\n\t[Property] public float TurnSpeed { get; set; } = 12f;\r\n\r\n\tprivate Vector3 _start;\r\n\tprivate float _height;\r\n\tprivate float _riseSpeed;\r\n\tprivate SkinnedModelRenderer _visual;\r\n\tprivate Rotation _facing;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_start = WorldPosition;\r\n\r\n\t\t// Resting yaw looks back down the camera\u0027s line, so the citizen greets the player instead of\r\n\t\t// showing its back on the first frame.\r\n\t\t_facing = Rotation.FromYaw( CameraYaw \u002B 180f );\r\n\r\n\t\tHideAuthoredBlock();\r\n\r\n\t\t_visual = TipsDemoCitizen.Build( GameObject, VisualZOffset );\r\n\t\tif ( _visual.IsValid() )\r\n\t\t{\r\n\t\t\t_visual.WorldRotation = _facing;\r\n\t\t\tLog.Info( \u0022[tips] demo pawn: dressed citizen built in code, authored block renderer switched off.\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tvar facing = Rotation.FromYaw( CameraYaw );\r\n\t\tvar move = ReadMove();\r\n\t\tvar dir = facing.Forward * move.x \u002B facing.Left * move.y;\r\n\t\tif ( dir.Length \u003E 1f )\r\n\t\t\tdir = dir.Normal;\r\n\r\n\t\tvar flat = ( WorldPosition \u002B dir * MoveSpeed * Time.Delta - _start ).WithZ( 0f );\r\n\t\tif ( flat.Length \u003E PlayRadius )\r\n\t\t\tflat = flat.Normal * PlayRadius;\r\n\r\n\t\tvar hopped = false;\r\n\t\tif ( _height \u003C= 0f \u0026\u0026 _riseSpeed \u003C= 0f \u0026\u0026 Input.Pressed( JumpAction ) )\r\n\t\t{\r\n\t\t\t_riseSpeed = JumpSpeed;\r\n\t\t\thopped = true;\r\n\t\t}\r\n\r\n\t\tif ( _height \u003E 0f || _riseSpeed \u003E 0f )\r\n\t\t{\r\n\t\t\t_riseSpeed -= Gravity * Time.Delta;\r\n\t\t\t_height \u002B= _riseSpeed * Time.Delta;\r\n\t\t\tif ( _height \u003C= 0f )\r\n\t\t\t{\r\n\t\t\t\t_height = 0f;\r\n\t\t\t\t_riseSpeed = 0f;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar previous = WorldPosition;\r\n\t\tWorldPosition = _start \u002B flat \u002B Vector3.Up * _height;\r\n\r\n\t\tDriveVisual( previous, dir, hopped );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Movement as a forward/left pair. \u003Cc\u003EInput.AnalogMove\u003C/c\u003E carries the movement stick and, in a\r\n\t/// project whose Input.config binds the standard movement actions, the keyboard too. The raw W/A/S/D\r\n\t/// fallback keeps the demo drivable in a project that binds movement under other names, which is the\r\n\t/// same reason the movement tip completes on either the stick or those keys.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static Vector3 ReadMove()\r\n\t{\r\n\t\tvar move = Input.AnalogMove;\r\n\t\tif ( move.Length \u003E 0.01f )\r\n\t\t\treturn move;\r\n\r\n\t\tvar forward = ( Input.Keyboard.Down( \u0022w\u0022 ) ? 1f : 0f ) - ( Input.Keyboard.Down( \u0022s\u0022 ) ? 1f : 0f );\r\n\t\tvar left = ( Input.Keyboard.Down( \u0022a\u0022 ) ? 1f : 0f ) - ( Input.Keyboard.Down( \u0022d\u0022 ) ? 1f : 0f );\r\n\t\treturn new Vector3( forward, left, 0f );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Turn the citizen to face its travel and hand the animgraph a frame of locomotion. The legs read the\r\n\t/// distance actually covered rather than the stick, so at the play radius the clamp reads as standing\r\n\t/// still instead of running on the spot.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void DriveVisual( Vector3 previous, Vector3 wishDirection, bool hopped )\r\n\t{\r\n\t\tif ( !_visual.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar travelled = ( WorldPosition - previous ).WithZ( 0f );\r\n\t\tvar velocity = ( Time.Delta \u003E 0f ? travelled / Time.Delta : Vector3.Zero ).WithZ( _riseSpeed );\r\n\r\n\t\tif ( travelled.Length \u003E 0.01f )\r\n\t\t{\r\n\t\t\tvar target = Rotation.LookAt( travelled.Normal, Vector3.Up );\r\n\t\t\t_facing = Rotation.Slerp( _facing, target, ( Time.Delta * TurnSpeed ).Clamp( 0f, 1f ) );\r\n\t\t}\r\n\r\n\t\t_visual.WorldRotation = _facing;\r\n\r\n\t\tvar grounded = _height \u003C= 0f \u0026\u0026 _riseSpeed \u003C= 0f;\r\n\t\tTipsDemoCitizen.Drive( _visual, velocity, wishDirection * MoveSpeed, grounded );\r\n\r\n\t\tif ( hopped )\r\n\t\t\tTipsDemoCitizen.TriggerJump( _visual );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Switch off the box renderer the scene authors on this object, so the citizen stands alone. Disabling\r\n\t/// the component at runtime leaves the scene file untouched: reopen it in the editor and the simple\r\n\t/// authored block is still what you see. The skinned renderer is skipped by type because it derives\r\n\t/// from \u003Cc\u003EModelRenderer\u003C/c\u003E too.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void HideAuthoredBlock()\r\n\t{\r\n\t\tvar authored = Components.GetAll\u003CModelRenderer\u003E( FindMode.EverythingInSelf )\r\n\t\t\t.Where( r =\u003E r is not SkinnedModelRenderer )\r\n\t\t\t.ToArray();\r\n\r\n\t\tforeach ( var renderer in authored )\r\n\t\t\trenderer.Enabled = false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPut the citizen back where it started (the demo\u0027s replay key).\u003C/summary\u003E\r\n\tpublic void ResetPawn()\r\n\t{\r\n\t\t_height = 0f;\r\n\t\t_riseSpeed = 0f;\r\n\t\tWorldPosition = _start;\r\n\r\n\t\t_facing = Rotation.FromYaw( CameraYaw \u002B 180f );\r\n\t\tif ( _visual.IsValid() )\r\n\t\t\t_visual.WorldRotation = _facing;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"TipCatalogMerge.cs","FileName":"TipCatalogMerge.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The merged catalog as one value: the ordered tips and the label saying where each id came from, built\r\n/// together so a reader can never pair a list from one build with labels from another.\r\n/// \u003C/summary\u003E\r\npublic sealed class TipCatalogView\r\n{\r\n\t/// \u003Csummary\u003EThe deduped, precedence-ordered tips.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CTipDefinition\u003E Tips { get; init; } = new List\u003CTipDefinition\u003E();\r\n\r\n\t/// \u003Csummary\u003ETip id to source label (\u0022code\u0022 / \u0022asset\u0022 / \u0022draft\u0022 / \u0022example\u0022).\u003C/summary\u003E\r\n\tpublic IReadOnlyDictionary\u003Cstring, string\u003E SourceById { get; init; } = new Dictionary\u003Cstring, string\u003E();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The pure merge rule behind \u003Csee cref=\u0022TipsCatalog.Active\u0022/\u003E: which source wins an id collision, what order\r\n/// the survivors come out in, and when the shipped example stands in. Lifted out of the catalog so it has no\r\n/// \u003Cc\u003ESandbox\u003C/c\u003E reference and the harness can assert precedence and dedupe without a running engine, which\r\n/// is the half of the catalog that a typo actually breaks.\r\n/// \u003C/summary\u003E\r\npublic static class TipCatalogMerge\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Merge the three sources into one view, highest precedence first: CODE, then ASSETS, then DRAFTS. The\r\n\t/// first tip seen for an id wins and later ones are dropped, so a draft never shadows the real tip it is\r\n\t/// a draft of. When all three come back empty, \u003Cparamref name=\u0022fallback\u0022/\u003E is used and labelled\r\n\t/// \u0022example\u0022. Null sources are treated as empty; a null tip, or one with a blank id, is skipped.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static TipCatalogView Merge(\r\n\t\tIEnumerable\u003CTipDefinition\u003E code,\r\n\t\tIEnumerable\u003CTipDefinition\u003E assets,\r\n\t\tIEnumerable\u003CTipDefinition\u003E drafts,\r\n\t\tIEnumerable\u003CTipDefinition\u003E fallback )\r\n\t{\r\n\t\tvar order = new List\u003CTipDefinition\u003E();\r\n\t\tvar sources = new Dictionary\u003Cstring, string\u003E( System.StringComparer.Ordinal );\r\n\r\n\t\tAdd( code, \u0022code\u0022, order, sources );\r\n\t\tAdd( assets, \u0022asset\u0022, order, sources );\r\n\t\tAdd( drafts, \u0022draft\u0022, order, sources );\r\n\r\n\t\tif ( order.Count == 0 )\r\n\t\t\tAdd( fallback, \u0022example\u0022, order, sources );\r\n\r\n\t\treturn new TipCatalogView { Tips = order, SourceById = sources };\r\n\t}\r\n\r\n\t// The source map IS the dedupe guard, and it is filled in the same pass as the list it guards. Guarding\r\n\t// inserts to one collection by querying a different, longer-lived one is how these two drift apart.\r\n\tprivate static void Add( IEnumerable\u003CTipDefinition\u003E source, string label,\r\n\t\tList\u003CTipDefinition\u003E order, Dictionary\u003Cstring, string\u003E sources )\r\n\t{\r\n\t\tif ( source is null )\r\n\t\t\treturn;\r\n\r\n\t\tforeach ( var def in source )\r\n\t\t{\r\n\t\t\tif ( def is null || string.IsNullOrEmpty( def.Id ) )\r\n\t\t\t\tcontinue;\r\n\t\t\tif ( sources.ContainsKey( def.Id ) )\r\n\t\t\t\tcontinue; // a higher-precedence source already claimed this id\r\n\t\t\torder.Add( def );\r\n\t\t\tsources[def.Id] = label;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// What a built catalog view was built FROM: the code list it saw, and the draft / asset revision numbers at\r\n/// the time. A read compares the stamp against the live sources; anything that moved means the view is stale\r\n/// and gets rebuilt. Comparing is three field reads with no allocation, which is what lets the coach ask for\r\n/// the catalog several times a frame without a rescan.\r\n///\r\n/// The code source is compared BY REFERENCE, not by content: registering is a whole-list swap, so a new list\r\n/// is a new catalog, and a caller mutating a list it already registered is expected to say so\r\n/// (\u003Csee cref=\u0022TipsCatalog.Rebuild\u0022/\u003E) the same way it always was.\r\n/// \u003C/summary\u003E\r\npublic readonly struct TipCatalogStamp\r\n{\r\n\t/// \u003Csummary\u003EThe code catalog this view merged.\u003C/summary\u003E\r\n\tpublic object Code { get; }\r\n\r\n\t/// \u003Csummary\u003EThe draft revision this view merged.\u003C/summary\u003E\r\n\tpublic int DraftRevision { get; }\r\n\r\n\t/// \u003Csummary\u003EThe asset revision this view merged.\u003C/summary\u003E\r\n\tpublic int AssetRevision { get; }\r\n\r\n\tpublic TipCatalogStamp( object code, int draftRevision, int assetRevision )\r\n\t{\r\n\t\tCode = code;\r\n\t\tDraftRevision = draftRevision;\r\n\t\tAssetRevision = assetRevision;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when nothing has moved since this view was built.\u003C/summary\u003E\r\n\tpublic bool Matches( object code, int draftRevision, int assetRevision )\r\n\t\t=\u003E ReferenceEquals( Code, code ) \u0026\u0026 DraftRevision == draftRevision \u0026\u0026 AssetRevision == assetRevision;\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/Studio/TipsStudio.cs","FileName":"TipsStudio.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The Tips Studio\u0027s state and every action its panel takes. The panel\r\n/// (\u003Csee cref=\u0022TipsStudioPanel\u0022/\u003E) is markup over this; everything that decides something lives here, so the\r\n/// razor stays readable and this stays testable by eye.\r\n///\r\n/// WHAT IT IS. An authoring surface for tips that runs inside your game: list the merged catalog, open any\r\n/// tip in an editor, watch the real card change as you type, fire the tip and its completion for real, and\r\n/// bake the result out as a \u003Cc\u003E.tip\u003C/c\u003E file. Nothing here is part of a shipped game\u0027s runtime: the panel\r\n/// only exists if you add it, it starts closed, and \u003Cc\u003Efg_tips_studio\u003C/c\u003E is off by default.\r\n///\r\n/// TWO WAYS A DRAFT REACHES THE COACH, and they are deliberately different:\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n/// \u003Citem\u003EPREVIEW registers the draft under \u003Csee cref=\u0022PreviewId\u0022/\u003E, an id nothing else uses, and force-shows\r\n/// it. It is a picture of the card. It cannot be shadowed by a real tip with the same id, which is what would\r\n/// happen if it registered under the draft\u0027s own id (drafts are the lowest-precedence source), and it cannot\r\n/// mark anything complete.\u003C/item\u003E\r\n/// \u003Citem\u003ETEST FIRE registers the draft under its OWN id and shows it, so completing it retires the real tip\r\n/// and the chain advances the way it will in the game. If a code or asset tip already owns that id, that one\r\n/// wins, which is correct: you are testing the chain, not the draft.\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n///\r\n/// CLEAN-UP. Everything it touches is static and would otherwise outlive the scene: the preview draft, the\r\n/// test-fire draft, and the pinned preview device. \u003Csee cref=\u0022Shutdown\u0022/\u003E hands all of it back, and the panel\r\n/// calls it from OnDestroy.\r\n/// \u003C/summary\u003E\r\npublic static class TipsStudio\r\n{\r\n\t/// \u003Csummary\u003EThe id the live preview registers under. Long and namespaced on purpose: it must never\r\n\t/// collide with a real tip, because a collision would silently show the real tip instead of the draft.\u003C/summary\u003E\r\n\tpublic const string PreviewId = \u0022fg_tips_studio_preview\u0022;\r\n\r\n\t/// \u003Csummary\u003EFolder under \u003Cc\u003EFileSystem.Data\u003C/c\u003E the Studio stages baked tips in, for the editor menu\r\n\t/// action to pick up. Staging through a file rather than a live static is what lets you bake in play and\r\n\t/// write the asset after you have stopped playing.\u003C/summary\u003E\r\n\tpublic const string StageFolder = \u0022fieldguide_tips_studio\u0022;\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Open / close\r\n\t// ------------------------------------------------------------------\r\n\r\n\tprivate static bool _open;\r\n\r\n\t/// \u003Csummary\u003EOpen or close the Tips Studio. Off by default, and the panel forces it off at boot: s\u0026amp;box\r\n\t/// persists convars between sessions, so without that a value set weeks ago would open an authoring panel\r\n\t/// over someone\u0027s game.\u003C/summary\u003E\r\n\t[ConVar( \u0022fg_tips_studio\u0022, Help = \u0022Open or close the Tips Studio authoring panel (dev tool, off by default)\u0022 )]\r\n\tpublic static bool Open\r\n\t{\r\n\t\tget =\u003E _open;\r\n\t\tset =\u003E _open = value;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhich tab is showing.\u003C/summary\u003E\r\n\tpublic static StudioTab Tab { get; set; } = StudioTab.Tips;\r\n\r\n\t/// \u003Csummary\u003EThe Studio\u0027s three tabs.\u003C/summary\u003E\r\n\tpublic enum StudioTab\r\n\t{\r\n\t\t/// \u003Csummary\u003EThe merged catalog, with source labels.\u003C/summary\u003E\r\n\t\tTips,\r\n\r\n\t\t/// \u003Csummary\u003EThe draft editor: wording, order, triggers, preview and test fire.\u003C/summary\u003E\r\n\t\tDraft,\r\n\r\n\t\t/// \u003Csummary\u003EThe bake-out surface: the .tip JSON, Copy, and staging for the editor.\u003C/summary\u003E\r\n\t\tBake,\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// The draft\r\n\t// ------------------------------------------------------------------\r\n\r\n\tprivate static TipStudioDraft _draft = new();\r\n\r\n\t/// \u003Csummary\u003EThe tip being authored. Never null.\u003C/summary\u003E\r\n\tpublic static TipStudioDraft Draft\r\n\t{\r\n\t\tget =\u003E _draft ??= new TipStudioDraft();\r\n\t\tset =\u003E _draft = value ?? new TipStudioDraft();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe catalog id the draft was opened from, or null for a new tip. Shown so it is obvious\r\n\t/// whether you are editing something that already exists.\u003C/summary\u003E\r\n\tpublic static string OpenedFrom { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EStart a new, empty tip.\u003C/summary\u003E\r\n\tpublic static void NewDraft()\r\n\t{\r\n\t\tDraft = new TipStudioDraft { Priority = 100 };\r\n\t\tOpenedFrom = null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpen a catalog tip in the editor. The two code-only predicates have no authored form and are\r\n\t/// dropped; \u003Csee cref=\u0022DroppedPredicates\u0022/\u003E says so on screen.\u003C/summary\u003E\r\n\tpublic static void OpenTip( string id )\r\n\t{\r\n\t\tvar def = TipsCatalog.Active.FirstOrDefault( t =\u003E t.Id == id );\r\n\t\tif ( def is null )\r\n\t\t\treturn;\r\n\r\n\t\tDraft = TipStudioDraft.FromDefinition( def );\r\n\t\tOpenedFrom = id;\r\n\t\tDroppedPredicates = HasCodePredicates( def );\r\n\t\tTab = StudioTab.Draft;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when the tip currently open was carrying a \u003Cc\u003ETrigger\u003C/c\u003E or \u003Cc\u003ECompleteWhen\u003C/c\u003E\r\n\t/// predicate, which a \u003Cc\u003E.tip\u003C/c\u003E file cannot hold. Baking it out keeps the declarative triggers and\r\n\t/// loses the predicate, so the panel warns before you do.\u003C/summary\u003E\r\n\tpublic static bool DroppedPredicates { get; private set; }\r\n\r\n\tprivate static bool HasCodePredicates( TipDefinition def )\r\n\t{\r\n\t\t// A tip that never set them carries the record\u0027s defaults. Comparing against a fresh default is the\r\n\t\t// only way to tell \u0022the author wrote a predicate\u0022 from \u0022the record filled one in\u0022.\r\n\t\tvar plain = new TipDefinition { Id = \u0022probe\u0022, Text = \u0022\u0022 };\r\n\t\treturn def.Trigger != plain.Trigger || def.CompleteWhen != plain.CompleteWhen;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe authoring notes for the current draft (grey-block run lengths, triggers that can never\r\n\t/// fire). Recomputed on read; the panel refreshes them when you press Enter in a box or click anything,\r\n\t/// because rebuilding the panel while you type would take the cursor out of the box.\u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003Cstring\u003E Notes =\u003E TipStudioText.Warnings( Draft );\r\n\r\n\t/// \u003Csummary\u003EThe draft as \u003Cc\u003E.tip\u003C/c\u003E JSON: what Copy puts on the clipboard and what a bake writes.\u003C/summary\u003E\r\n\tpublic static string Json =\u003E TipStudioJson.Write( Draft );\r\n\r\n\t/// \u003Csummary\u003ETrue when the draft\u0027s id already names a tip from a HIGHER-precedence source, so a bake would\r\n\t/// be shadowed until that source lets go. Worth saying out loud before someone wonders why their new file\r\n\t/// does nothing.\u003C/summary\u003E\r\n\tpublic static string ShadowedBy\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrWhiteSpace( Draft.Id ) )\r\n\t\t\t\treturn null;\r\n\r\n\t\t\tvar source = TipsCatalog.SourceOf( Draft.Id );\r\n\t\t\treturn source == \u0022code\u0022 ? \u0022code\u0022 : null;\r\n\t\t}\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Live preview\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EWhether the real card is mirroring the draft right now.\u003C/summary\u003E\r\n\tpublic static bool PreviewOn { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EPush the draft onto the real card, or refresh what is already there. Registers under\r\n\t/// \u003Csee cref=\u0022PreviewId\u0022/\u003E so a draft of an existing tip is not shadowed by the tip it copies.\u003C/summary\u003E\r\n\tpublic static void PushPreview( Scene scene )\r\n\t{\r\n\t\tvar def = Draft.ToDefinition();\r\n\r\n\t\t// The preview stands in for the draft even before it has an id, so an author sees the card from the\r\n\t\t// first character typed rather than after they remember to name it.\r\n\t\tvar preview = new TipDefinition\r\n\t\t{\r\n\t\t\tId = PreviewId,\r\n\t\t\tText = Draft.Text ?? \u0022\u0022,\r\n\t\t\tTextPad = string.IsNullOrEmpty( Draft.TextPad ) ? null : Draft.TextPad,\r\n\t\t\tIcon = Draft.Icon ?? \u0022\u0022,\r\n\t\t\tPriority = def?.Priority ?? 0,\r\n\t\t};\r\n\r\n\t\tTipsCatalog.RegisterRuntime( preview );\r\n\t\tPreviewOn = true;\r\n\r\n\t\tvar coach = LiveCoach( scene );\r\n\t\tcoach?.ForceShow( PreviewId );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETake the preview off the card and out of the catalog.\u003C/summary\u003E\r\n\tpublic static void StopPreview( Scene scene )\r\n\t{\r\n\t\tPreviewOn = false;\r\n\t\tTipsCatalog.UnregisterRuntime( PreviewId );\r\n\t\tTipsCoach.PreviewDevice = null;\r\n\r\n\t\t// The card may still be showing a tip that no longer exists. Drop it rather than dismiss it: dismissing\r\n\t\t// would write the fake preview id into the player\u0027s saved progress and leave it there for good.\r\n\t\tif ( TipsCoach.ActiveTip?.Id == PreviewId )\r\n\t\t\tLiveCoach( scene )?.DropActive();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhich device the preview card is pinned to, or null for whatever the player last used.\u003C/summary\u003E\r\n\tpublic static TipDevice? PinnedDevice\r\n\t{\r\n\t\tget =\u003E TipsCoach.PreviewDevice;\r\n\t\tset =\u003E TipsCoach.PreviewDevice = value;\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Test fire\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Make the draft the live tip UNDER ITS OWN ID and show it now. From here its completion is the real\r\n\t/// thing: fire the trigger in the game, or press Complete, and the tip retires and the chain moves on.\r\n\t/// Returns false when the draft has no id yet.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool TestFire( Scene scene )\r\n\t{\r\n\t\tvar def = Draft.ToDefinition();\r\n\t\tif ( def is null )\r\n\t\t\treturn false;\r\n\r\n\t\t// A test fire of a tip already marked complete would retire the moment it appeared.\r\n\t\tTipsCoach.Uncomplete( def.Id );\r\n\t\tTipsCatalog.UnregisterRuntime( PreviewId );\r\n\t\tPreviewOn = false;\r\n\t\tTipsCatalog.RegisterRuntime( def );\r\n\r\n\t\tvar coach = LiveCoach( scene );\r\n\t\tif ( coach is null )\r\n\t\t{\r\n\t\t\tLog.Warning( \u0022fg_tips: no TipsCoach in the scene, so there is nothing to show the tip on.\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\treturn coach.ForceShow( def.Id );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFire the draft\u0027s completion by hand, the same path a world trigger uses. The tip retires and\r\n\t/// whatever waits on it becomes eligible.\u003C/summary\u003E\r\n\tpublic static void CompleteNow()\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( Draft.Id ) )\r\n\t\t\treturn;\r\n\r\n\t\tTipsCoach.Complete( Draft.Id );\r\n\t}\r\n\r\n\tprivate static TipsCoach LiveCoach( Scene scene )\r\n\t\t=\u003E scene?.GetAllComponents\u003CTipsCoach\u003E().FirstOrDefault();\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Input actions (the action picker)\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The project\u0027s real input actions, for the InputAction picker: \u003Cc\u003EInput.ActionNames\u003C/c\u003E, which is the\r\n\t/// engine\u0027s list from the current game\u0027s input settings, the same list the \u003Cc\u003E[InputAction]\u003C/c\u003E inspector\r\n\t/// dropdown draws from. Sorted, and empty rather than throwing outside a running game.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003Cstring\u003E ActionNames\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tvar names = Input.ActionNames?.Where( n =\u003E !string.IsNullOrWhiteSpace( n ) ).ToList();\r\n\t\t\t\tif ( names is null || names.Count == 0 )\r\n\t\t\t\t\treturn Array.Empty\u003Cstring\u003E();\r\n\r\n\t\t\t\tnames.Sort( StringComparer.OrdinalIgnoreCase );\r\n\t\t\t\treturn names;\r\n\t\t\t}\r\n\t\t\tcatch ( Exception )\r\n\t\t\t{\r\n\t\t\t\treturn Array.Empty\u003Cstring\u003E();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Bake out\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003ECopy the draft\u0027s \u003Cc\u003E.tip\u003C/c\u003E JSON to the system clipboard, from in game. Paste it into a new\r\n\t/// file under your project\u0027s \u003Cc\u003EAssets/\u003C/c\u003E and the editor picks it up as a tip.\u003C/summary\u003E\r\n\tpublic static void CopyJson()\r\n\t{\r\n\t\tSandbox.UI.Clipboard.SetText( Json );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write the draft into \u003Csee cref=\u0022StageFolder\u0022/\u003E under \u003Cc\u003EFileSystem.Data\u003C/c\u003E, where the editor menu\r\n\t/// action \u0022Field Guide / Write staged tips\u0022 picks it up and writes the real asset. Two steps because game\r\n\t/// code cannot write into a project\u0027s \u003Cc\u003EAssets/\u003C/c\u003E folder, and because staging survives the end of the\r\n\t/// play session, so you can author in play and land the file afterwards.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Creturns\u003EA line for the panel saying what happened.\u003C/returns\u003E\r\n\tpublic static string Stage()\r\n\t{\r\n\t\tvar file = TipStudioJson.FileNameFor( Draft.Id );\r\n\t\tif ( file is null )\r\n\t\t\treturn \u0022Give the tip an id first.\u0022;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tFileSystem.Data.CreateDirectory( StageFolder );\r\n\t\t\tvar path = $\u0022{StageFolder}/{file}\u0022;\r\n\t\t\tFileSystem.Data.WriteAllText( path, Json );\r\n\t\t\tLog.Info( $\u0022fg_tips: staged {file}. In the editor, run Field Guide / Write staged tips to Assets/tips.\u0022 );\r\n\r\n\t\t\t// Short on purpose: this lands in a one-line status slot in the panel, and the console line\r\n\t\t\t// above already carries the full instruction.\r\n\t\t\treturn $\u0022staged {file}\u0022;\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022fg_tips: could not stage {file} ({e.Message}).\u0022 );\r\n\t\t\treturn $\u0022Could not stage {file}: {e.Message}\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EHow many tips are waiting in the staging folder, so the panel can say whether there is\r\n\t/// anything for the editor action to do.\u003C/summary\u003E\r\n\tpublic static int StagedCount\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\treturn FileSystem.Data.DirectoryExists( StageFolder )\r\n\t\t\t\t\t? FileSystem.Data.FindFile( StageFolder, \u0022*.tip\u0022, false ).Count()\r\n\t\t\t\t\t: 0;\r\n\t\t\t}\r\n\t\t\tcatch ( Exception )\r\n\t\t\t{\r\n\t\t\t\treturn 0;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEmpty the staging folder, for when a bake was a mistake or the files have landed.\u003C/summary\u003E\r\n\tpublic static string ClearStaged()\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !FileSystem.Data.DirectoryExists( StageFolder ) )\r\n\t\t\t\treturn \u0022Nothing staged.\u0022;\r\n\r\n\t\t\tvar cleared = 0;\r\n\t\t\tforeach ( var file in FileSystem.Data.FindFile( StageFolder, \u0022*.tip\u0022, false ).ToList() )\r\n\t\t\t{\r\n\t\t\t\tFileSystem.Data.DeleteFile( $\u0022{StageFolder}/{file}\u0022 );\r\n\t\t\t\tcleared\u002B\u002B;\r\n\t\t\t}\r\n\r\n\t\t\treturn cleared == 0 ? \u0022Nothing staged.\u0022 : $\u0022Cleared {cleared} staged tip(s).\u0022;\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\treturn $\u0022Could not clear the staging folder: {e.Message}\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Shutdown\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Hand back everything the Studio pinned: the preview and test-fire drafts, and the pinned preview\r\n\t/// device. Called from the panel\u0027s OnDestroy, because all of it is static and would otherwise follow the\r\n\t/// developer into the next scene, exactly the trap a scene-registered code catalog falls into.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void Shutdown()\r\n\t{\r\n\t\tPreviewOn = false;\r\n\t\tTipsCoach.PreviewDevice = null;\r\n\t\tTipsCatalog.ClearRuntime();\r\n\t\tOpen = false;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/Studio/TipStudioJson.cs","FileName":"TipStudioJson.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Text.Json;\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// Writes and reads the \u003Cc\u003E.tip\u003C/c\u003E file format: the exact JSON the s\u0026amp;box editor puts on disk for a\r\n/// \u003Csee cref=\u0022TipResource\u0022/\u003E. What the Tips Studio\u0027s Copy button puts on the clipboard, and what its Editor\r\n/// menu action writes into \u003Cc\u003EAssets/tips/\u003C/c\u003E, is a file the editor will happily open, edit and re-save.\r\n///\r\n/// THE SHAPE, derived from the shipped assets under \u003Cc\u003EAssets/\u003C/c\u003E and from \u003Csee cref=\u0022TipResource\u0022/\u003E itself:\r\n/// every \u003Cc\u003E[Property]\u003C/c\u003E in declaration order, then the two editor bookkeeping keys. Every trigger field is\r\n/// written whether or not its kind reads it, because that is what a GameResource does (it serializes the\r\n/// object, not the interesting parts of it), and a hand-written file missing those keys would gain them the\r\n/// first time the editor saved it, producing a spurious diff.\r\n///\r\n/// \u003Ccode\u003E\r\n/// {\r\n///   \u0022Id\u0022: \u0022jump\u0022,\r\n///   \u0022Text\u0022: \u0022Press *Space* to jump.\u0022,\r\n///   \u0022TextPad\u0022: \u0022\u0022,\r\n///   \u0022Icon\u0022: \u0022\u0022,\r\n///   \u0022Priority\u0022: 100,\r\n///   \u0022PrerequisiteTipIds\u0022: [],\r\n///   \u0022Completion\u0022: { \u0022Kind\u0022: \u0022InputAction\u0022, \u0022Action\u0022: \u0022Jump\u0022, ... },\r\n///   \u0022MaxShowSeconds\u0022: 0,\r\n///   \u0022Relevance\u0022: { \u0022Kind\u0022: \u0022Always\u0022, ... },\r\n///   \u0022__references\u0022: [],\r\n///   \u0022__version\u0022: 0\r\n/// }\r\n/// \u003C/code\u003E\r\n///\r\n/// Enums go out as their names through hand-written maps rather than a converter, so the on-disk vocabulary\r\n/// is a stated format instead of a by-product of how the enum happens to be spelled today. The round trip\r\n/// (write then read) is asserted headlessly in \u003Cc\u003Etools/tips_harness\u003C/c\u003E.\r\n/// \u003C/summary\u003E\r\npublic static class TipStudioJson\r\n{\r\n\t/// \u003Csummary\u003ETwo-space indentation, matching what the editor writes, so a Studio-authored file and an\r\n\t/// editor-saved one diff cleanly against each other.\u003C/summary\u003E\r\n\tprivate static readonly JsonSerializerOptions WriteOptions = new()\r\n\t{\r\n\t\tWriteIndented = true,\r\n\t};\r\n\r\n\tprivate static readonly JsonSerializerOptions ReadOptions = new()\r\n\t{\r\n\t\tPropertyNameCaseInsensitive = false,\r\n\t};\r\n\r\n\t/// \u003Csummary\u003EThe file name a draft bakes out to, \u003Cc\u003E\u0026lt;id\u0026gt;.tip\u003C/c\u003E, with anything awkward for a file\r\n\t/// name folded to an underscore. A blank id yields null: an unnamed tip has nowhere to land.\u003C/summary\u003E\r\n\tpublic static string FileNameFor( string id )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( id ) )\r\n\t\t\treturn null;\r\n\r\n\t\tvar chars = id.Trim().ToCharArray();\r\n\t\tfor ( var i = 0; i \u003C chars.Length; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar c = chars[i];\r\n\t\t\tvar ok = ( c \u003E= \u0027a\u0027 \u0026\u0026 c \u003C= \u0027z\u0027 ) || ( c \u003E= \u0027A\u0027 \u0026\u0026 c \u003C= \u0027Z\u0027 ) || ( c \u003E= \u00270\u0027 \u0026\u0026 c \u003C= \u00279\u0027 ) || c == \u0027_\u0027 || c == \u0027-\u0027;\r\n\t\t\tif ( !ok )\r\n\t\t\t\tchars[i] = \u0027_\u0027;\r\n\t\t}\r\n\r\n\t\treturn new string( chars ) \u002B \u0022.tip\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ESerialize a draft as \u003Cc\u003E.tip\u003C/c\u003E JSON. Never throws: a null draft writes an empty tip.\u003C/summary\u003E\r\n\tpublic static string Write( TipStudioDraft draft )\r\n\t\t=\u003E RelaxEscapes( JsonSerializer.Serialize( ToWire( draft ), WriteOptions ) );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Put back the characters the default JSON writer escapes but the editor does not. An icon of \u0022\u2705\u0022 is\r\n\t/// written to a \u003Cc\u003E.tip\u003C/c\u003E by the editor as that character; the default encoder emits \u003Cc\u003E\u2705\u003C/c\u003E,\r\n\t/// which parses to the same string but is not the same FILE, so the first inspector save of a\r\n\t/// Studio-written tip would show a diff on a line nobody touched. The same goes for the apostrophes and\r\n\t/// angle brackets the default encoder escapes out of HTML caution, which a tip line is full of.\r\n\t///\r\n\t/// Done as a pass over the finished text rather than by swapping in a relaxed encoder, so the writer\r\n\t/// needs no API beyond the JSON serializer a sibling kit already ships. Escapes JSON actually requires\r\n\t/// (the quote, the backslash, and the control characters below 0x20) are left exactly as they are, and an\r\n\t/// escaped backslash is stepped over as a pair so \u003Cc\u003E\\\\u0041\u003C/c\u003E stays literal.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static string RelaxEscapes( string json )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( json ) || json.IndexOf( \u0027\\\\\u0027 ) \u003C 0 )\r\n\t\t\treturn json;\r\n\r\n\t\tvar sb = new System.Text.StringBuilder( json.Length );\r\n\t\tvar i = 0;\r\n\r\n\t\twhile ( i \u003C json.Length )\r\n\t\t{\r\n\t\t\tvar c = json[i];\r\n\t\t\tif ( c != \u0027\\\\\u0027 || i \u002B 1 \u003E= json.Length )\r\n\t\t\t{\r\n\t\t\t\tsb.Append( c );\r\n\t\t\t\ti\u002B\u002B;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( json[i \u002B 1] == \u0027u\u0027 \u0026\u0026 i \u002B 5 \u003C json.Length \u0026\u0026 TryHex( json, i \u002B 2, out var code )\r\n\t\t\t\t\u0026\u0026 code \u003E= 0x20 \u0026\u0026 code != \u0027\u0022\u0027 \u0026\u0026 code != \u0027\\\\\u0027 )\r\n\t\t\t{\r\n\t\t\t\tsb.Append( (char)code );\r\n\t\t\t\ti \u002B= 6;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// Any other escape (including \\\\) is copied as a PAIR, so its second character can never be read\r\n\t\t\t// as the start of a new escape.\r\n\t\t\tsb.Append( c );\r\n\t\t\tsb.Append( json[i \u002B 1] );\r\n\t\t\ti \u002B= 2;\r\n\t\t}\r\n\r\n\t\treturn sb.ToString();\r\n\t}\r\n\r\n\tprivate static bool TryHex( string text, int start, out int value )\r\n\t{\r\n\t\tvalue = 0;\r\n\t\tfor ( var i = start; i \u003C start \u002B 4; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar c = text[i];\r\n\t\t\tint digit;\r\n\t\t\tif ( c \u003E= \u00270\u0027 \u0026\u0026 c \u003C= \u00279\u0027 )\r\n\t\t\t\tdigit = c - \u00270\u0027;\r\n\t\t\telse if ( c \u003E= \u0027a\u0027 \u0026\u0026 c \u003C= \u0027f\u0027 )\r\n\t\t\t\tdigit = c - \u0027a\u0027 \u002B 10;\r\n\t\t\telse if ( c \u003E= \u0027A\u0027 \u0026\u0026 c \u003C= \u0027F\u0027 )\r\n\t\t\t\tdigit = c - \u0027A\u0027 \u002B 10;\r\n\t\t\telse\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tvalue = ( value \u003C\u003C 4 ) | digit;\r\n\t\t}\r\n\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EParse \u003Cc\u003E.tip\u003C/c\u003E JSON back into a draft. Returns null on malformed input rather than\r\n\t/// throwing, so a hand-edited file with a stray comma reports a problem instead of taking the panel\r\n\t/// down with it.\u003C/summary\u003E\r\n\tpublic static TipStudioDraft Read( string json )\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( json ) )\r\n\t\t\treturn null;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar wire = JsonSerializer.Deserialize\u003CTipWire\u003E( json, ReadOptions );\r\n\t\t\treturn wire is null ? null : FromWire( wire );\r\n\t\t}\r\n\t\tcatch ( Exception )\r\n\t\t{\r\n\t\t\treturn null;\r\n\t\t}\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Wire types. Property ORDER here is the on-disk field order.\r\n\t// ------------------------------------------------------------------\r\n\r\n\tprivate sealed class TipWire\r\n\t{\r\n\t\tpublic string Id { get; set; } = \u0022\u0022;\r\n\t\tpublic string Text { get; set; } = \u0022\u0022;\r\n\t\tpublic string TextPad { get; set; } = \u0022\u0022;\r\n\t\tpublic string Icon { get; set; } = \u0022\u0022;\r\n\t\tpublic int Priority { get; set; }\r\n\t\tpublic List\u003Cstring\u003E PrerequisiteTipIds { get; set; } = new();\r\n\t\tpublic TriggerWire Completion { get; set; } = new();\r\n\t\tpublic float MaxShowSeconds { get; set; }\r\n\t\tpublic TriggerWire Relevance { get; set; } = new();\r\n\r\n\t\t[JsonPropertyName( \u0022__references\u0022 )]\r\n\t\tpublic List\u003Cstring\u003E References { get; set; } = new();\r\n\r\n\t\t[JsonPropertyName( \u0022__version\u0022 )]\r\n\t\tpublic int Version { get; set; }\r\n\t}\r\n\r\n\tprivate sealed class TriggerWire\r\n\t{\r\n\t\tpublic string Kind { get; set; } = \u0022Always\u0022;\r\n\t\tpublic string Action { get; set; } = \u0022\u0022;\r\n\t\tpublic string Key { get; set; } = \u0022\u0022;\r\n\t\tpublic string Name { get; set; } = \u0022\u0022;\r\n\t\tpublic float Threshold { get; set; }\r\n\t\tpublic float Seconds { get; set; }\r\n\t\tpublic string AnalogSource { get; set; } = \u0022AnalogMove\u0022;\r\n\t\tpublic float Magnitude { get; set; }\r\n\t\tpublic List\u003CTriggerWire\u003E Children { get; set; } = new();\r\n\t}\r\n\r\n\tprivate static TipWire ToWire( TipStudioDraft draft )\r\n\t{\r\n\t\tdraft ??= new TipStudioDraft();\r\n\r\n\t\treturn new TipWire\r\n\t\t{\r\n\t\t\tId = draft.Id ?? \u0022\u0022,\r\n\t\t\tText = draft.Text ?? \u0022\u0022,\r\n\t\t\tTextPad = draft.TextPad ?? \u0022\u0022,\r\n\t\t\tIcon = draft.Icon ?? \u0022\u0022,\r\n\t\t\tPriority = draft.Priority,\r\n\t\t\tPrerequisiteTipIds = draft.PrerequisiteTipIds is null\r\n\t\t\t\t? new List\u003Cstring\u003E()\r\n\t\t\t\t: new List\u003Cstring\u003E( draft.PrerequisiteTipIds ),\r\n\t\t\tCompletion = ToWire( draft.Completion ),\r\n\t\t\tMaxShowSeconds = draft.MaxShowSeconds,\r\n\t\t\tRelevance = ToWire( draft.Relevance ),\r\n\t\t};\r\n\t}\r\n\r\n\tprivate static TriggerWire ToWire( TipStudioTrigger spec )\r\n\t{\r\n\t\tspec ??= new TipStudioTrigger();\r\n\r\n\t\tvar wire = new TriggerWire\r\n\t\t{\r\n\t\t\tKind = TipStudioTrigger.KindName( spec.Kind ),\r\n\t\t\tAction = spec.Action ?? \u0022\u0022,\r\n\t\t\tKey = spec.Key ?? \u0022\u0022,\r\n\t\t\tName = spec.Name ?? \u0022\u0022,\r\n\t\t\tThreshold = spec.Threshold,\r\n\t\t\tSeconds = spec.Seconds,\r\n\t\t\tAnalogSource = TipStudioTrigger.SourceName( spec.AnalogSource ),\r\n\t\t\tMagnitude = spec.Magnitude,\r\n\t\t};\r\n\r\n\t\tif ( spec.Children is not null )\r\n\t\t\tforeach ( var child in spec.Children )\r\n\t\t\t\twire.Children.Add( ToWire( child ) );\r\n\r\n\t\treturn wire;\r\n\t}\r\n\r\n\tprivate static TipStudioDraft FromWire( TipWire wire ) =\u003E new()\r\n\t{\r\n\t\tId = wire.Id ?? \u0022\u0022,\r\n\t\tText = wire.Text ?? \u0022\u0022,\r\n\t\tTextPad = wire.TextPad ?? \u0022\u0022,\r\n\t\tIcon = wire.Icon ?? \u0022\u0022,\r\n\t\tPriority = wire.Priority,\r\n\t\tPrerequisiteTipIds = wire.PrerequisiteTipIds is null\r\n\t\t\t? new List\u003Cstring\u003E()\r\n\t\t\t: new List\u003Cstring\u003E( wire.PrerequisiteTipIds ),\r\n\t\tCompletion = FromWire( wire.Completion ),\r\n\t\tMaxShowSeconds = wire.MaxShowSeconds,\r\n\t\tRelevance = FromWire( wire.Relevance ),\r\n\t};\r\n\r\n\tprivate static TipStudioTrigger FromWire( TriggerWire wire )\r\n\t{\r\n\t\tif ( wire is null )\r\n\t\t\treturn new TipStudioTrigger();\r\n\r\n\t\tvar spec = new TipStudioTrigger\r\n\t\t{\r\n\t\t\tKind = TipStudioTrigger.ParseKind( wire.Kind ),\r\n\t\t\tAction = wire.Action ?? \u0022\u0022,\r\n\t\t\tKey = wire.Key ?? \u0022\u0022,\r\n\t\t\tName = wire.Name ?? \u0022\u0022,\r\n\t\t\tThreshold = wire.Threshold,\r\n\t\t\tSeconds = wire.Seconds,\r\n\t\t\tAnalogSource = TipStudioTrigger.ParseSource( wire.AnalogSource ),\r\n\t\t\tMagnitude = wire.Magnitude,\r\n\t\t};\r\n\r\n\t\tif ( wire.Children is not null )\r\n\t\t\tforeach ( var child in wire.Children )\r\n\t\t\t\tspec.Children.Add( FromWire( child ) );\r\n\r\n\t\treturn spec;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/TipDevice.cs","FileName":"TipDevice.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// Which input device the player last used. Backed by \u003Cc\u003EInput.UsingController\u003C/c\u003E, which the engine\r\n/// flaps to the last-used device (there is no event; it is polled per frame). The coach exposes the\r\n/// live value as \u003Csee cref=\u0022TipsCoach.ActiveDevice\u0022/\u003E and the display folds it into its BuildHash so a\r\n/// device flip re-renders the active tip with the right wording and chips immediately.\r\n/// \u003C/summary\u003E\r\npublic enum TipDevice\r\n{\r\n\t/// \u003Csummary\u003EThe player is on keyboard and mouse (the default when no controller has been used).\u003C/summary\u003E\r\n\tKeyboardMouse,\r\n\r\n\t/// \u003Csummary\u003EThe player is on a game controller (the last button pressed was a pad button).\u003C/summary\u003E\r\n\tGamepad,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Pure, engine-free render-time helpers for device-aware tip text: which wording a tip shows for a\r\n/// device, and how a keycap label remaps when the player is on a pad. Kept free of any \u003Cc\u003ESandbox\u003C/c\u003E\r\n/// reference so the exact rules a game sees on screen can be exercised headlessly (the display calls\r\n/// these; a harness calls them with fakes and asserts the truth table).\r\n/// \u003C/summary\u003E\r\npublic static class TipDeviceText\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The pad-mode wording for a tip: its \u003Cparamref name=\u0022textPad\u0022/\u003E when authored, else its\r\n\t/// \u003Cparamref name=\u0022text\u0022/\u003E. This is the single-text fallback (build plan point 3): a tip that leaves\r\n\t/// TextPad unset reads identically from either device, so the \u0022either\u0022 idiom\r\n\t/// (\u003Cc\u003E\u0022Press *W* or \u0060RT\u0060\u0022\u003C/c\u003E) keeps rendering both chips with no auto-stripping.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string PadTextOr( string text, string textPad )\r\n\t\t=\u003E string.IsNullOrEmpty( textPad ) ? ( text ?? \u0022\u0022 ) : textPad;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The wording a tip shows for \u003Cparamref name=\u0022device\u0022/\u003E: \u003Cparamref name=\u0022text\u0022/\u003E on keyboard/mouse,\r\n\t/// and \u003Csee cref=\u0022PadTextOr(string, string)\u0022/\u003E on a pad. A single seam so the device-to-text choice is\r\n\t/// identical in the display and the harness.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string ForDevice( string text, string textPad, TipDevice device )\r\n\t\t=\u003E device == TipDevice.Gamepad ? PadTextOr( text, textPad ) : ( text ?? \u0022\u0022 );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Remap a keyboard keycap label for pad mode (build plan point 4), generalizing World Builder\u0027s\r\n\t/// \u003Cc\u003ECap()\u003C/c\u003E. Applied to keycap chips at render time only when the player is on a pad:\r\n\t/// \u003Clist type=\u0022bullet\u0022\u003E\r\n\t/// \u003Citem\u003E\u003Cparamref name=\u0022padLabelFor\u0022/\u003E null: no map is set, the label passes through unchanged (the\r\n\t/// single-text behaviour, so a game that never sets a map is unaffected).\u003C/item\u003E\r\n\t/// \u003Citem\u003Ethe map returns the same or a different non-empty string: that label is shown (an unmapped\r\n\t/// label the game\u0027s map passes through stays unchanged; a mapped one, e.g. \u0022RMB\u0022 to \u0022LT\u0022, swaps).\u003C/item\u003E\r\n\t/// \u003Citem\u003Ethe map returns null or empty: the chip is unpressable on a pad and is skipped (the caller\r\n\t/// renders nothing for it), matching WB\u0027s \u0022skip the unpressable chip\u0022 behaviour.\u003C/item\u003E\r\n\t/// \u003C/list\u003E\r\n\t/// Returns the label to render, or null to skip the chip.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string PadCap( string keyLabel, Func\u003Cstring, string\u003E padLabelFor )\r\n\t{\r\n\t\tif ( padLabelFor is null )\r\n\t\t\treturn keyLabel; // no map set: passthrough\r\n\r\n\t\tvar mapped = padLabelFor( keyLabel );\r\n\t\treturn string.IsNullOrEmpty( mapped ) ? null : mapped; // null/empty = unpressable on pad, skip\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"TipDevice.cs","FileName":"TipDevice.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// Which input device the player last used. Backed by \u003Cc\u003EInput.UsingController\u003C/c\u003E, which the engine\r\n/// flaps to the last-used device (there is no event; it is polled per frame). The coach exposes the\r\n/// live value as \u003Csee cref=\u0022TipsCoach.ActiveDevice\u0022/\u003E and the display folds it into its BuildHash so a\r\n/// device flip re-renders the active tip with the right wording and chips immediately.\r\n/// \u003C/summary\u003E\r\npublic enum TipDevice\r\n{\r\n\t/// \u003Csummary\u003EThe player is on keyboard and mouse (the default when no controller has been used).\u003C/summary\u003E\r\n\tKeyboardMouse,\r\n\r\n\t/// \u003Csummary\u003EThe player is on a game controller (the last button pressed was a pad button).\u003C/summary\u003E\r\n\tGamepad,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Pure, engine-free render-time helpers for device-aware tip text: which wording a tip shows for a\r\n/// device, and how a keycap label remaps when the player is on a pad. Kept free of any \u003Cc\u003ESandbox\u003C/c\u003E\r\n/// reference so the exact rules a game sees on screen can be exercised headlessly (the display calls\r\n/// these; a harness calls them with fakes and asserts the truth table).\r\n/// \u003C/summary\u003E\r\npublic static class TipDeviceText\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// The pad-mode wording for a tip: its \u003Cparamref name=\u0022textPad\u0022/\u003E when authored, else its\r\n\t/// \u003Cparamref name=\u0022text\u0022/\u003E. This is the single-text fallback (build plan point 3): a tip that leaves\r\n\t/// TextPad unset reads identically from either device, so the \u0022either\u0022 idiom\r\n\t/// (\u003Cc\u003E\u0022Press *W* or \u0060RT\u0060\u0022\u003C/c\u003E) keeps rendering both chips with no auto-stripping.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string PadTextOr( string text, string textPad )\r\n\t\t=\u003E string.IsNullOrEmpty( textPad ) ? ( text ?? \u0022\u0022 ) : textPad;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The wording a tip shows for \u003Cparamref name=\u0022device\u0022/\u003E: \u003Cparamref name=\u0022text\u0022/\u003E on keyboard/mouse,\r\n\t/// and \u003Csee cref=\u0022PadTextOr(string, string)\u0022/\u003E on a pad. A single seam so the device-to-text choice is\r\n\t/// identical in the display and the harness.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string ForDevice( string text, string textPad, TipDevice device )\r\n\t\t=\u003E device == TipDevice.Gamepad ? PadTextOr( text, textPad ) : ( text ?? \u0022\u0022 );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Remap a keyboard keycap label for pad mode (build plan point 4), generalizing World Builder\u0027s\r\n\t/// \u003Cc\u003ECap()\u003C/c\u003E. Applied to keycap chips at render time only when the player is on a pad:\r\n\t/// \u003Clist type=\u0022bullet\u0022\u003E\r\n\t/// \u003Citem\u003E\u003Cparamref name=\u0022padLabelFor\u0022/\u003E null: no map is set, the label passes through unchanged (the\r\n\t/// single-text behaviour, so a game that never sets a map is unaffected).\u003C/item\u003E\r\n\t/// \u003Citem\u003Ethe map returns the same or a different non-empty string: that label is shown (an unmapped\r\n\t/// label the game\u0027s map passes through stays unchanged; a mapped one, e.g. \u0022RMB\u0022 to \u0022LT\u0022, swaps).\u003C/item\u003E\r\n\t/// \u003Citem\u003Ethe map returns null or empty: the chip is unpressable on a pad and is skipped (the caller\r\n\t/// renders nothing for it), matching WB\u0027s \u0022skip the unpressable chip\u0022 behaviour.\u003C/item\u003E\r\n\t/// \u003C/list\u003E\r\n\t/// Returns the label to render, or null to skip the chip.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static string PadCap( string keyLabel, Func\u003Cstring, string\u003E padLabelFor )\r\n\t{\r\n\t\tif ( padLabelFor is null )\r\n\t\t\treturn keyLabel; // no map set: passthrough\r\n\r\n\t\tvar mapped = padLabelFor( keyLabel );\r\n\t\treturn string.IsNullOrEmpty( mapped ) ? null : mapped; // null/empty = unpressable on pad, skip\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"TipSegment.cs","FileName":"TipSegment.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// What a run of tip text renders as: plain prose, a keyboard keycap, or a gamepad button chip.\r\n/// \u003C/summary\u003E\r\npublic enum TipSegmentKind\r\n{\r\n\t/// \u003Csummary\u003EOrdinary text, rendered as-is.\u003C/summary\u003E\r\n\tPlain,\r\n\r\n\t/// \u003Csummary\u003EA keyboard key or mouse-button name, rendered as a square keycap. Markup: \u003Cc\u003E*W*\u003C/c\u003E.\u003C/summary\u003E\r\n\tKey,\r\n\r\n\t/// \u003Csummary\u003EA gamepad button or stick, rendered as a rounded controller chip. Markup: \u003Cc\u003E\u0060A\u0060\u003C/c\u003E (backticks).\u003C/summary\u003E\r\n\tGamepadButton,\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// One run of tip text: either plain prose, a keyboard key that renders as a square keycap, or a\r\n/// gamepad button that renders as a rounded controller chip. \u003Csee cref=\u0022TipsDisplay\u0022/\u003E walks a tip\u0027s\r\n/// segments and styles each kind, so tip authors write a single string with markup around input names\r\n/// instead of embedding UI.\r\n///\r\n/// Markup:\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n/// \u003Citem\u003E\u003Cc\u003E*W*\u003C/c\u003E (asterisks) is a keyboard / mouse keycap, e.g. \u003Cc\u003E\u0022Hold *B* to block.\u0022\u003C/c\u003E\u003C/item\u003E\r\n/// \u003Citem\u003E\u003Cc\u003E\u0060A\u0060\u003C/c\u003E (backticks) is a gamepad button, e.g. \u003Cc\u003E\u0022Press \u0060A\u0060 to jump.\u0022\u003C/c\u003E\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// The two are independent, so one line can carry both for a keyboard-and-gamepad prompt:\r\n/// \u003Cc\u003E\u0022Attack with *LMB* / \u0060RT\u0060.\u0022\u003C/c\u003E. Everything outside the markers is plain.\r\n/// \u003C/summary\u003E\r\npublic readonly record struct TipSegment( string Text, TipSegmentKind Kind )\r\n{\r\n\t/// \u003Csummary\u003ETrue for a keyboard keycap run (\u003Csee cref=\u0022TipSegmentKind.Key\u0022/\u003E). Kept for readers that\r\n\t/// only distinguish keyboard chips from plain text.\u003C/summary\u003E\r\n\tpublic bool IsKey =\u003E Kind == TipSegmentKind.Key;\r\n\r\n\t/// \u003Csummary\u003ETrue for a gamepad button run (\u003Csee cref=\u0022TipSegmentKind.GamepadButton\u0022/\u003E).\u003C/summary\u003E\r\n\tpublic bool IsGamepadButton =\u003E Kind == TipSegmentKind.GamepadButton;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Split a tip line into alternating plain / key / gamepad runs. A matched pair of \u003Cc\u003E*\u003C/c\u003E marks a\r\n\t/// keyboard keycap; a matched pair of \u003Cc\u003E\u0060\u003C/c\u003E marks a gamepad button; everything else is plain. An\r\n\t/// unmatched trailing marker degrades gracefully: its run stays plain.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CTipSegment\u003E Parse( string text )\r\n\t{\r\n\t\tvar segments = new List\u003CTipSegment\u003E();\r\n\t\tif ( string.IsNullOrEmpty( text ) )\r\n\t\t\treturn segments;\r\n\r\n\t\tvar i = 0;\r\n\t\twhile ( i \u003C text.Length )\r\n\t\t{\r\n\t\t\tvar c = text[i];\r\n\r\n\t\t\t// A marker only opens a chip if it has a matching partner later in the line; otherwise it is\r\n\t\t\t// literal text (so a lone \u0060*\u0060 or backtick reads plainly instead of eating the rest of the line).\r\n\t\t\tif ( c == \u0027*\u0027 || c == \u0027\u0060\u0027 )\r\n\t\t\t{\r\n\t\t\t\tvar close = text.IndexOf( c, i \u002B 1 );\r\n\t\t\t\tif ( close \u003E i )\r\n\t\t\t\t{\r\n\t\t\t\t\tif ( close \u003E i \u002B 1 ) // non-empty run between the markers\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar kind = c == \u0027*\u0027 ? TipSegmentKind.Key : TipSegmentKind.GamepadButton;\r\n\t\t\t\t\t\tsegments.Add( new TipSegment( text.Substring( i \u002B 1, close - i - 1 ), kind ) );\r\n\t\t\t\t\t}\r\n\t\t\t\t\ti = close \u002B 1;\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t// Accumulate a plain run up to the next marker (or end of line).\r\n\t\t\tvar start = i;\r\n\t\t\twhile ( i \u003C text.Length \u0026\u0026 text[i] != \u0027*\u0027 \u0026\u0026 text[i] != \u0027\u0060\u0027 )\r\n\t\t\t\ti\u002B\u002B;\r\n\r\n\t\t\t// A marker with no partner ahead is literal: fold it into the plain run and keep going.\r\n\t\t\tif ( i \u003C text.Length \u0026\u0026 text.IndexOf( text[i], i \u002B 1 ) \u003C 0 )\r\n\t\t\t\ti\u002B\u002B;\r\n\r\n\t\t\tif ( i \u003E start )\r\n\t\t\t\tsegments.Add( new TipSegment( text.Substring( start, i - start ), TipSegmentKind.Plain ) );\r\n\t\t}\r\n\r\n\t\treturn segments;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"TipTriggerEval.cs","FileName":"TipTriggerEval.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The environment a \u003Csee cref=\u0022TipTrigger\u0022/\u003E is evaluated against: the four world reads plus the active\r\n/// tip\u0027s elapsed visible time. \u003Csee cref=\u0022TipsCoach\u0022/\u003E fills these from \u003Csee cref=\u0022Sandbox.Input\u0022/\u003E, the\r\n/// pushed \u003Csee cref=\u0022TipContext\u0022/\u003E and its own timer; a headless harness fills them with fakes. Keeping\r\n/// the reads behind delegates is what lets the trigger truth table be unit-tested without the engine.\r\n/// \u003C/summary\u003E\r\npublic sealed class TipTriggerEnv\r\n{\r\n\t/// \u003Csummary\u003EWas the named Input.config action pressed this frame? (Input.Pressed)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, bool\u003E ActionPressed { get; init; } = static _ =\u003E false;\r\n\r\n\t/// \u003Csummary\u003EWas the named raw key / mouse button pressed this frame? (Input.Keyboard.Pressed)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, bool\u003E KeyPressed { get; init; } = static _ =\u003E false;\r\n\r\n\t/// \u003Csummary\u003EHas the named string signal been latched this session? (Signal / Ever kinds)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, bool\u003E SignalLatched { get; init; } = static _ =\u003E false;\r\n\r\n\t/// \u003Csummary\u003EIs the named custom context flag true this frame? (TipContext.Flag)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, bool\u003E Flag { get; init; } = static _ =\u003E false;\r\n\r\n\t/// \u003Csummary\u003EThe named custom context number this frame. (TipContext.Number)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, float\u003E Number { get; init; } = static _ =\u003E 0f;\r\n\r\n\t/// \u003Csummary\u003ESeconds the active tip has been visible, for the Timer kind.\u003C/summary\u003E\r\n\tpublic float Elapsed { get; init; }\r\n\r\n\t/// \u003Csummary\u003EThe current magnitude (0..1-ish) of the given analog stick, for the AnalogAxis kind. (Input.AnalogMove / Input.AnalogLook length.)\u003C/summary\u003E\r\n\tpublic Func\u003CTipTriggerAnalogSource, float\u003E AnalogMagnitude { get; init; } = static _ =\u003E 0f;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The pure, engine-free evaluation core for a declarative \u003Csee cref=\u0022TipTrigger\u0022/\u003E: the per-kind rule and\r\n/// the AnyOf / AllOf recursion, with every world read behind a \u003Csee cref=\u0022TipTriggerEnv\u0022/\u003E delegate. This\r\n/// holds the whole completion/relevance truth table, so it can be exercised headlessly (the coach passes\r\n/// real Input / context reads; a harness passes fakes) and stays identical between the two.\r\n/// \u003C/summary\u003E\r\npublic static class TipTriggerEval\r\n{\r\n\t/// \u003Csummary\u003EEvaluate a trigger against the given environment. Null trigger evaluates false.\u003C/summary\u003E\r\n\tpublic static bool Evaluate( TipTrigger t, TipTriggerEnv env )\r\n\t{\r\n\t\tif ( t is null || env is null )\r\n\t\t\treturn false;\r\n\r\n\t\tswitch ( t.Kind )\r\n\t\t{\r\n\t\t\tcase TipTriggerKind.Always:\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase TipTriggerKind.InputAction:\r\n\t\t\t\tforeach ( var a in t.Actions )\r\n\t\t\t\t\tif ( !string.IsNullOrEmpty( a ) \u0026\u0026 env.ActionPressed( a ) )\r\n\t\t\t\t\t\treturn true;\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tcase TipTriggerKind.Key:\r\n\t\t\t\tforeach ( var k in t.Keys )\r\n\t\t\t\t\tif ( !string.IsNullOrEmpty( k ) \u0026\u0026 env.KeyPressed( k ) )\r\n\t\t\t\t\t\treturn true;\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tcase TipTriggerKind.Signal:\r\n\t\t\tcase TipTriggerKind.Ever:\r\n\t\t\t\treturn !string.IsNullOrEmpty( t.Key ) \u0026\u0026 env.SignalLatched( t.Key );\r\n\r\n\t\t\tcase TipTriggerKind.Flag:\r\n\t\t\t\treturn !string.IsNullOrEmpty( t.Key ) \u0026\u0026 env.Flag( t.Key );\r\n\r\n\t\t\tcase TipTriggerKind.AtLeast:\r\n\t\t\t\treturn !string.IsNullOrEmpty( t.Key ) \u0026\u0026 env.Number( t.Key ) \u003E= t.Threshold;\r\n\r\n\t\t\tcase TipTriggerKind.Timer:\r\n\t\t\t\treturn env.Elapsed \u003E= t.Seconds;\r\n\r\n\t\t\tcase TipTriggerKind.AnalogAxis:\r\n\t\t\t\treturn env.AnalogMagnitude( t.AnalogSource ) \u003E= t.Threshold;\r\n\r\n\t\t\tcase TipTriggerKind.AnyOf:\r\n\t\t\t\tforeach ( var c in t.Children )\r\n\t\t\t\t\tif ( Evaluate( c, env ) )\r\n\t\t\t\t\t\treturn true;\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tcase TipTriggerKind.AllOf:\r\n\t\t\t\tif ( t.Children.Length == 0 )\r\n\t\t\t\t\treturn false;\r\n\t\t\t\tforeach ( var c in t.Children )\r\n\t\t\t\t\tif ( !Evaluate( c, env ) )\r\n\t\t\t\t\t\treturn false;\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/Studio/TipsStudioPanel.razor","FileName":"TipsStudioPanel.razor","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"@using Sandbox\r\n@using Sandbox.UI\r\n@using System\r\n@using System.Collections.Generic\r\n@using System.Linq\r\n@namespace FieldGuide.Tips\r\n@inherits PanelComponent\r\n@attribute [StyleSheet]\r\n\r\n@*\r\n\tTIPS STUDIO - author a tip inside the running game and watch the real card change as you type.\r\n\r\n\tOne modal, three columns, over a dim scrim (the approved Field Kits layout, docs/design/ui-system).\r\n\tLEFT is the merged catalog with the source each id resolved from, which is also how a tip left behind\r\n\tby another scene gives itself away. MIDDLE is the editor: wording, order, prerequisites, and the two\r\n\ttrigger pickers, whose action list comes from the project\u0027s own input actions. RIGHT is the payoff:\r\n\tthe same card the player sees, rendered twice side by side so the keyboard and controller wordings\r\n\tread together, and under it the bake-out (Copy to clipboard, or stage it for the editor menu action\r\n\tthat writes Assets/tips).\r\n\r\n\tThe lower-left card is still the REAL one: \u0022show it\u0022 pushes the draft into the coach and TipsDisplay\r\n\tdraws it with the shipped stylesheet, and the device chips pin TipsCoach.PreviewDevice. The two cards\r\n\tin the rail are the same component restated inside this panel, because a preview you have to look\r\n\taway from is not a preview.\r\n\r\n\tToggle: \u0060fg_tips_studio 1\u0060 in the console, or press T (a plain letter; the editor eats F1-F12 in\r\n\tplay-in-editor). The key OPENS only, never closes, so pressing T inside a text box types a T and\r\n\tnothing else. Close with the x in the header or \u0060fg_tips_studio 0\u0060.\r\n\r\n\tPanel rules this file follows, each of which has cost someone a session: rows come from @foreach in\r\n\tmain markup, never a RenderFragment; the root takes no pointer events and the scrim and modal take\r\n\tall of them; both scroll regions are a FIXED pixel height, never a percentage; no field being TYPED\r\n\tinto is folded into BuildHash, because a rebuild takes the cursor out of the box.\r\n*@\r\n\r\n\u003Croot class=\u0022ts-root\u0022\u003E\r\n@if ( TipsStudio.Open )\r\n{\r\n\t\u003Cdiv class=\u0022ts-scrim\u0022\u003E\r\n\t\t\u003Cdiv class=\u0022ts-modal\u0022\u003E\r\n\r\n\t\t\t@* ================= header ================= *@\r\n\t\t\t\u003Cdiv class=\u0022ts-hdr\u0022\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022ts-hdr-left\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-title\u0022\u003ETIPS STUDIO\u003C/div\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-hdr-meta\u0022\u003E@HeaderMeta\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\u003Cdiv class=\u0022ts-x\u0022 onclick=@Close\u003E\u00D7\u003C/div\u003E\r\n\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\u003Cdiv class=\u0022ts-cols\u0022\u003E\r\n\r\n\t\t\t\t@* ================= left: the merged catalog ================= *@\r\n\t\t\t\t\u003Cdiv class=\u0022ts-left\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-list-hdr\u0022\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-list-t\u0022\u003ETips\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-list-m\u0022\u003Eby priority\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-list\u0022 @ref=\u0022ListBody\u0022\u003E\r\n\t\t\t\t\t\t@if ( Entries.Count == 0 )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-empty\u0022\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-empty-t\u0022\u003ENo tips yet\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-empty-l\u0022\u003EPress New tip to write one.\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t@foreach ( var e in Entries )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tvar entry = e;\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-row @(entry.Selected ? \u0022on\u0022 : \u0022\u0022)\u0022 onclick=@(() =\u003E OpenTip( entry.Id ))\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-row-id\u0022\u003E@entry.Id\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t@* State REPLACES the source rather than sitting beside it: the row is 248px wide\r\n\t\t\t\t\t\t\t\t\tand a third thing in it squeezes the id until it blanks out. *@\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-row-meta\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t@if ( entry.State is null )\r\n\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-row-src\u0022\u003E@entry.Source\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\telse\r\n\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-row-state\u0022\u003E@entry.State\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-left-btns\u0022\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn pri grow gap\u0022 onclick=@NewTip\u003ENew tip\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn\u0022 onclick=@Rescan\u003ERescan\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t@* ================= middle: the draft ================= *@\r\n\t\t\t\t\u003Cdiv class=\u0022ts-mid\u0022 @ref=\u0022MidBody\u0022\u003E\r\n\r\n\t\t\t\t\t@if ( TipsStudio.DroppedPredicates )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-b\u0022\u003E!\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-t\u0022\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-l\u0022\u003EThis tip carries a code predicate.\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-l\u0022\u003EA .tip file cannot hold one, so baking\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-l\u0022\u003Ekeeps the triggers and drops the predicate.\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-frow\u0022\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w200\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EId\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003CTextEntry class=\u0022ts-in\u0022 Value=@DraftId\r\n\t\t\t\t\t\t\t\tOnTextEdited=@((string v) =\u003E { TipsStudio.Draft.Id = v; }) onsubmit=@Commit /\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w150\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EPriority\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-step\u0022\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpPriority( -10 ))\u003E\u2212\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-val\u0022\u003E@DraftPriority\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpPriority( 10 ))\u003E\u002B\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field grow last\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EPrerequisites\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop\u0022\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-face @(IsOpen( PrereqDrop ) ? \u0022open\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\t\t\tonclick=@(() =\u003E ToggleDrop( PrereqDrop ))\u003E\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-val\u0022\u003E@PrereqFace\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chev\u0022\u003Eexpand_more\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t@if ( IsOpen( PrereqDrop ) )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-list @(Tall( AvailablePrerequisites.Count ))\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t@if ( AvailablePrerequisites.Count == 0 )\r\n\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-opt\u0022\u003Eno other tip ids yet\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t@foreach ( var p in AvailablePrerequisites )\r\n\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\tvar prereq = p;\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-opt\u0022 onclick=@(() =\u003E AddPrerequisite( prereq ))\u003E@prereq\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t@* Its own block under the field, with its own bottom margin: this row used to collapse\r\n\t\t\t\t\t\tinto the wording section and paint its chips over the Text label. *@\r\n\t\t\t\t\t@if ( PrerequisiteList.Count \u003E 0 )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chips block\u0022\u003E\r\n\t\t\t\t\t\t\t@foreach ( var p in PrerequisiteList )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\tvar prereq = p;\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip on mono\u0022\r\n\t\t\t\t\t\t\t\t\tonclick=@(() =\u003E RemovePrerequisite( prereq ))\u003E@($\u0022{prereq} \u00D7\u0022)\u003C/div\u003E\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-frow\u0022\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field grow last\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab-row\u0022\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EText\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap\u0022\u003E*Space* keycap \u00B7 \u0060A\u0060 pad button\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003CTextEntry class=\u0022ts-in\u0022 Value=@DraftText\r\n\t\t\t\t\t\t\t\tOnTextEdited=@((string v) =\u003E { TipsStudio.Draft.Text = v; Refresh(); }) onsubmit=@Commit /\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-frow\u0022\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field grow\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EPad text \u00B7 optional\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003CTextEntry class=\u0022ts-in\u0022 Value=@DraftTextPad\r\n\t\t\t\t\t\t\t\tOnTextEdited=@((string v) =\u003E { TipsStudio.Draft.TextPad = v; Refresh(); }) onsubmit=@Commit /\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w120 last\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EIcon\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003CTextEntry class=\u0022ts-in\u0022 Value=@DraftIcon\r\n\t\t\t\t\t\t\t\tOnTextEdited=@((string v) =\u003E { TipsStudio.Draft.Icon = v; Refresh(); }) onsubmit=@Commit /\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t@* ---- the two trigger pickers, from one block of markup ---- *@\r\n\t\t\t\t\t@foreach ( var s in TriggerSlots )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar slot = s;\r\n\t\t\t\t\t\tvar trig = slot.Trigger;\r\n\t\t\t\t\t\tvar actionDrop = slot.Key;\r\n\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-sec\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-sec-hd\u0022\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-sec-t\u0022\u003E@slot.Title\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap flat\u0022\u003E@slot.Blurb\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chips\u0022\u003E\r\n\t\t\t\t\t\t\t\t@foreach ( var k in TipStudioTrigger.AllKinds )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\tvar kind = k;\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip @(trig.Kind == kind ? \u0022on\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\t\t\t\tonclick=@(() =\u003E SetKind( trig, kind ))\u003E@TipStudioTrigger.KindName( kind )\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-frow\u0022\u003E\r\n\t\t\t\t\t\t\t\t@if ( trig.Kind == TipTriggerKind.InputAction )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w200\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EInput action\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-face @(IsOpen( actionDrop ) ? \u0022open\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\t\t\t\t\t\tonclick=@(() =\u003E ToggleDrop( actionDrop ))\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-val\u0022\u003E@ActionFace( trig )\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chev\u0022\u003Eexpand_more\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t@if ( IsOpen( actionDrop ) )\r\n\t\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-list @(Tall( ActionNames.Count ))\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t@if ( ActionNames.Count == 0 )\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-opt\u0022\u003Eno input actions bound\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t@foreach ( var a in ActionNames )\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvar action = a;\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-drop-opt @(trig.Action == action ? \u0022on\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonclick=@(() =\u003E PickAction( trig, action ))\u003E@action\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap\u0022\u003Efrom Input.config\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t@if ( trig.Kind == TipTriggerKind.Key )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w200\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EKey\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003CTextEntry class=\u0022ts-in\u0022 Value=@TrigKey( trig )\r\n\t\t\t\t\t\t\t\t\t\t\tOnTextEdited=@((string v) =\u003E { trig.Key = v; }) onsubmit=@Commit /\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap\u0022\u003Espace, w, mouse1\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t@if ( slot.NeedsName )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w200\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EName\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003CTextEntry class=\u0022ts-in\u0022 Value=@TrigName( trig )\r\n\t\t\t\t\t\t\t\t\t\t\tOnTextEdited=@((string v) =\u003E { trig.Name = v; }) onsubmit=@Commit /\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap\u0022\u003E@slot.NameHint\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t@if ( trig.Kind == TipTriggerKind.AtLeast )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w150\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EAt least\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-step\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpThreshold( trig, -1f ))\u003E\u2212\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-val\u0022\u003E@TrigThreshold( trig )\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpThreshold( trig, 1f ))\u003E\u002B\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t@if ( trig.Kind == TipTriggerKind.Timer )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w150\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003ESeconds\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-step\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpSeconds( trig, -1f ))\u003E\u2212\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-val\u0022\u003E@TrigSeconds( trig )\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpSeconds( trig, 1f ))\u003E\u002B\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t@if ( trig.Kind == TipTriggerKind.AnalogAxis )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field w150\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EMagnitude\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-step\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpMagnitude( trig, -0.1f ))\u003E\u2212\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-val\u0022\u003E@TrigMagnitude( trig )\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpMagnitude( trig, 0.1f ))\u003E\u002B\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap\u0022\u003E0 to 1\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t@if ( slot.IsCompletion )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field grow last\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EMax show seconds\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-step\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpMaxShow( -1f ))\u003E\u2212\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-val w80\u0022\u003E@DraftMaxShow\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-stp\u0022 onclick=@(() =\u003E BumpMaxShow( 1f ))\u003E\u002B\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap\u0022\u003E0 = never auto-complete\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\t\t@if ( trig.Kind == TipTriggerKind.AnalogAxis )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chips\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t@foreach ( var src in TipStudioTrigger.AllAnalogSources )\r\n\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\tvar source = src;\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip @(trig.AnalogSource == source ? \u0022on\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\t\t\t\t\tonclick=@(() =\u003E SetSource( trig, source ))\u003E@TipStudioTrigger.SourceName( source )\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t@if ( slot.IsComposite )\r\n\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chips\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip\u0022 onclick=@(() =\u003E AddChild( trig ))\u003Eadd one\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t@if ( trig.Children.Count == 0 )\r\n\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap flat\u0022\u003E@slot.EmptyCompositeHint\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\t\t\t@foreach ( var c in trig.Children.ToList() )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\tvar child = c;\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-child\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chips\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t@foreach ( var k in TipStudioTrigger.AllKinds )\r\n\t\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\tvar kind = k;\r\n\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip small @(child.Kind == kind ? \u0022on\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tonclick=@(() =\u003E SetKind( child, kind ))\u003E@TipStudioTrigger.KindName( kind )\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip small drop\u0022 onclick=@(() =\u003E RemoveChild( trig, child ))\u003Eremove\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-frow\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-field grow last\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab-row\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-lab\u0022\u003EValue\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap flat\u0022\u003E@ChildHint( child )\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\u003CTextEntry class=\u0022ts-in\u0022 Value=@ChildValue( child )\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tOnTextEdited=@((string v) =\u003E SetChildValue( child, v )) onsubmit=@Commit /\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t@* ---- what the draft would do wrong ---- *@\r\n\t\t\t\t\t@if ( TipsStudio.ShadowedBy is not null )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-b\u0022\u003E!\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-t\u0022\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-l\u0022\u003EA code tip already owns this id.\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-l\u0022\u003EYour file sits behind it in the catalog.\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t@foreach ( var n in NoteBlocks )\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar note = n;\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-b\u0022\u003E!\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-t\u0022\u003E\r\n\t\t\t\t\t\t\t\t@foreach ( var l in note.Lines )\r\n\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\tvar line = l;\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-warn-l\u0022\u003E@line\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t}\r\n\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t@* ================= right: preview and bake ================= *@\r\n\t\t\t\t\u003Cdiv class=\u0022ts-rail\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-rail-hdr\u0022\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-kicker\u0022\u003ELIVE PREVIEW\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn-row\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn small gap\u0022 onclick=@CompleteNow\u003EComplete it\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn pri small\u0022 onclick=@TestFire\u003ETest fire\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t@* Pinned above the scrolling preview: these four pick what the REAL lower-left card\r\n\t\t\t\t\t\tshows, and a control that scrolls out of sight is a control nobody finds. *@\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-pv-row\u0022\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-pv-key\u0022\u003ELive card\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip @(TipsStudio.PreviewOn ? \u0022on\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\tonclick=@TogglePreview\u003E@(TipsStudio.PreviewOn ? \u0022showing\u0022 : \u0022show it\u0022)\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip @(TipsStudio.PinnedDevice == TipDevice.KeyboardMouse ? \u0022on\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\tonclick=@(() =\u003E PinDevice( TipDevice.KeyboardMouse ))\u003Ekeyboard\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip @(TipsStudio.PinnedDevice == TipDevice.Gamepad ? \u0022on\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\tonclick=@(() =\u003E PinDevice( TipDevice.Gamepad ))\u003Epad\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-chip @(TipsStudio.PinnedDevice is null ? \u0022on\u0022 : \u0022\u0022)\u0022\r\n\t\t\t\t\t\t\tonclick=@(() =\u003E PinDevice( null ))\u003Elive\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-rail-body\u0022 @ref=\u0022RailBody\u0022\u003E\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-well\u0022\u003E\r\n\t\t\t\t\t\t@foreach ( var p in PreviewCards )\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tvar card = p;\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-pv @(card.Last ? \u0022last\u0022 : \u0022\u0022)\u0022\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-pv-lab\u0022\u003E@card.Label\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022tsp-card\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022tsp-stripe\u0022\u003E\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022tsp-in\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t@if ( !string.IsNullOrEmpty( card.Icon ) )\r\n\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022tsp-glyph\u0022\u003E@card.Icon\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022tsp-body\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022tsp-kicker\u0022\u003EGUIDE\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022tsp-text\u0022\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t@foreach ( var seg in card.Segments )\r\n\t\t\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tvar run = seg;\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tif ( run.Kind == TipSegmentKind.Key )\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022tsp-key\u0022\u003E@run.Text\u003C/span\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\t\telse if ( run.Kind == TipSegmentKind.GamepadButton )\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cspan class=\u0022tsp-pad\u0022\u003E@run.Text\u003C/span\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\t\telse\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\u003Cspan\u003E@run.Text\u003C/span\u003E\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\t\u003Cdiv class=\u0022tsp-x\u0022\u003E\u00D7\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-cap well\u0022\u003Erenders exactly what the coach will show\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\t\t\u003Cdiv class=\u0022ts-out\u0022\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn-row\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn grow gap\u0022 onclick=@CopyJson\u003E@_copyLabel\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn pri grow\u0022 onclick=@Stage\u003EWrite to project\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-out-line\u0022\u003E@BakeTarget\u003C/div\u003E\r\n\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn-row pad\u0022\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-btn small\u0022 onclick=@ClearStaged\u003EClear staged\u003C/div\u003E\r\n\t\t\t\t\t\t\t\u003Cdiv class=\u0022ts-out-line inline\u0022\u003E@StatusLine\u003C/div\u003E\r\n\t\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\t\u003C/div\u003E\r\n\t\t\t\t\u003C/div\u003E\r\n\r\n\t\t\t\u003C/div\u003E\r\n\t\t\u003C/div\u003E\r\n\t\u003C/div\u003E\r\n}\r\n\u003C/root\u003E\r\n\r\n@code\r\n{\r\n\t// ---- mounting ----\r\n\r\n\t/// \u003Csummary\u003EThe raw key that OPENS the Studio. A plain letter on purpose: the editor eats F1 to F12 in\r\n\t/// play-in-editor. It only opens, never closes, so pressing it inside a text box just types the letter.\r\n\t/// Close with the header\u0027s \u00D7 or \u003Cc\u003Efg_tips_studio 0\u003C/c\u003E.\u003C/summary\u003E\r\n\t[Property] public string OpenKey { get; set; } = \u0022T\u0022;\r\n\r\n\t/// \u003Csummary\u003EWhether the Studio starts open. Off by default: an authoring panel that appears unbidden over\r\n\t/// a game is a bug. This, and only this, decides the boot state; the persisted convar never does.\u003C/summary\u003E\r\n\t[Property] public bool OpenOnStart { get; set; }\r\n\r\n\t/// \u003Csummary\u003EForce the Studio shut anywhere but the editor. On by default: it is an authoring tool, and a\r\n\t/// published build has nothing to author. Turn it off if you want it in your own standalone dev build.\u003C/summary\u003E\r\n\t[Property] public bool EditorOnly { get; set; } = true;\r\n\r\n\t// @ref binds to an auto-PROPERTY. On a bare private field it silently never assigns, and the\r\n\t// CanDragScroll fix below would quietly do nothing.\r\n\tSandbox.UI.Panel ListBody { get; set; }\r\n\tSandbox.UI.Panel MidBody { get; set; }\r\n\tSandbox.UI.Panel RailBody { get; set; }\r\n\r\n\tbool _booted;\r\n\tbool _wasOpen;\r\n\tint _revision;\r\n\tstring _copyLabel = \u0022Copy .tip JSON\u0022;\r\n\tstring _stageLine = \u0022\u0022;\r\n\r\n\t/// \u003Csummary\u003EWhich dropdown is showing its options, or null. One at a time: the lists sit in flow under\r\n\t/// their field, so two open at once would push the column around for no reason.\u003C/summary\u003E\r\n\tstring _openDrop;\r\n\r\n\t/// \u003Csummary\u003EThe prerequisite picker\u0027s key. The trigger pickers key off their slot name.\u003C/summary\u003E\r\n\tconst string PrereqDrop = \u0022prereq\u0022;\r\n\r\n\tbool IsOpen( string key ) =\u003E _openDrop == key;\r\n\r\n\tvoid ToggleDrop( string key )\r\n\t{\r\n\t\t_openDrop = _openDrop == key ? null : key;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\t// ---- the catalog list ----\r\n\r\n\t/// \u003Csummary\u003EOne row of the tip list. A struct of finished strings so the markup interpolates single\r\n\t/// identifiers only, never a chained member read (which renders blank in several razor cases).\u003C/summary\u003E\r\n\tpublic struct Entry\r\n\t{\r\n\t\tpublic string Id;\r\n\t\tpublic string Source;\r\n\t\tpublic string State;\r\n\t\tpublic bool Selected;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEvery tip in the merged catalog, read fresh (so an edited .tip appears the moment the\r\n\t/// catalog rebuilds), labelled with the source it resolved from and ordered the way the coach picks\r\n\t/// them: highest priority first.\u003C/summary\u003E\r\n\tList\u003CEntry\u003E Entries\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar list = new List\u003CEntry\u003E();\r\n\t\t\tvar view = TipsCatalog.View;\r\n\t\t\tvar activeId = TipsCoach.ActiveTip?.Id;\r\n\t\t\tvar opened = TipsStudio.OpenedFrom;\r\n\r\n\t\t\tforeach ( var def in view.Tips.OrderByDescending( t =\u003E t.Priority ).ThenBy( t =\u003E t.Id, StringComparer.Ordinal ) )\r\n\t\t\t{\r\n\t\t\t\tvar source = view.SourceById.TryGetValue( def.Id, out var s ) ? s : \u0022unknown\u0022;\r\n\r\n\t\t\t\tlist.Add( new Entry\r\n\t\t\t\t{\r\n\t\t\t\t\tId = def.Id,\r\n\t\t\t\t\tSource = source,\r\n\t\t\t\t\tState = def.Id == activeId ? \u0022on screen\u0022 : ( TipsCoach.IsCompleted( def.Id ) ? \u0022done\u0022 : null ),\r\n\t\t\t\t\tSelected = def.Id == opened,\r\n\t\t\t\t} );\r\n\t\t\t}\r\n\r\n\t\t\treturn list;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe header\u0027s one line: the package, the catalog, and what the middle column is editing.\r\n\t/// One interpolated string rather than three text nodes, because the in-editor codegen drops the\r\n\t/// whitespace between a literal and an expression.\u003C/summary\u003E\r\n\tstring HeaderMeta =\u003E $\u0022fieldguide.tips \u00B7 {CatalogSummary} \u00B7 {DraftOrigin}\u0022;\r\n\r\n\t/// \u003Csummary\u003EHow many tips and where they came from. A source you did not expect is a tip left over\r\n\t/// from somewhere else.\u003C/summary\u003E\r\n\tstring CatalogSummary\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar view = TipsCatalog.View;\r\n\t\t\tif ( view.Tips.Count == 0 )\r\n\t\t\t\treturn \u0022no tips yet\u0022;\r\n\r\n\t\t\tvar counts = new Dictionary\u003Cstring, int\u003E();\r\n\t\t\tforeach ( var kv in view.SourceById )\r\n\t\t\t\tcounts[kv.Value] = counts.TryGetValue( kv.Value, out var n ) ? n \u002B 1 : 1;\r\n\r\n\t\t\tvar parts = counts.OrderBy( kv =\u003E kv.Key, StringComparer.Ordinal ).Select( kv =\u003E $\u0022{kv.Value} {kv.Key}\u0022 );\r\n\t\t\treturn $\u0022{view.Tips.Count} tips \u00B7 {string.Join( \u0022, \u0022, parts )}\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid OpenTip( string id )\r\n\t{\r\n\t\tTipsStudio.OpenTip( id );\r\n\t\tResetLabels();\r\n\t\t_openDrop = null;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid NewTip()\r\n\t{\r\n\t\tTipsStudio.NewDraft();\r\n\t\tResetLabels();\r\n\t\t_openDrop = null;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid Rescan()\r\n\t{\r\n\t\tTipsCatalog.NoteAssetsChanged();\r\n\t\tRefresh();\r\n\t}\r\n\r\n\t// ---- draft editing ----\r\n\r\n\tstring DraftOrigin =\u003E string.IsNullOrEmpty( TipsStudio.OpenedFrom )\r\n\t\t? \u0022new tip\u0022\r\n\t\t: $\u0022editing {TipsStudio.OpenedFrom}\u0022;\r\n\r\n\t// Single-identifier reads for the markup. A razor interpolation of a CHAINED member read\r\n\t// (TipsStudio.Draft.Priority) renders blank in several cases; a plain property or a method call does not.\r\n\tstring DraftId =\u003E TipsStudio.Draft.Id;\r\n\tstring DraftText =\u003E TipsStudio.Draft.Text;\r\n\tstring DraftTextPad =\u003E TipsStudio.Draft.TextPad;\r\n\tstring DraftIcon =\u003E TipsStudio.Draft.Icon;\r\n\tint DraftPriority =\u003E TipsStudio.Draft.Priority;\r\n\tstring DraftMaxShow =\u003E Show( TipsStudio.Draft.MaxShowSeconds );\r\n\r\n\tstatic string TrigKey( TipStudioTrigger t ) =\u003E t.Key;\r\n\tstatic string TrigName( TipStudioTrigger t ) =\u003E t.Name;\r\n\tstatic string TrigThreshold( TipStudioTrigger t ) =\u003E Show( t.Threshold );\r\n\tstatic string TrigSeconds( TipStudioTrigger t ) =\u003E Show( t.Seconds );\r\n\tstatic string TrigMagnitude( TipStudioTrigger t ) =\u003E Show( t.Magnitude );\r\n\r\n\tstatic string Show( float value ) =\u003E value.ToString( \u00220.##\u0022 );\r\n\r\n\tvoid BumpPriority( int delta )\r\n\t{\r\n\t\tTipsStudio.Draft.Priority \u002B= delta;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid BumpMaxShow( float delta )\r\n\t{\r\n\t\tTipsStudio.Draft.MaxShowSeconds = MathF.Max( 0f, TipsStudio.Draft.MaxShowSeconds \u002B delta );\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tList\u003Cstring\u003E PrerequisiteList =\u003E TipsStudio.Draft.PrerequisiteTipIds ?? new List\u003Cstring\u003E();\r\n\r\n\t/// \u003Csummary\u003EWhat the prerequisite field reads at rest. The list underneath ADDS one; the chips below the\r\n\t/// row remove them, which is the only honest shape for a field that holds several values.\u003C/summary\u003E\r\n\tstring PrereqFace\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar have = PrerequisiteList;\r\n\t\t\tif ( have.Count == 0 )\r\n\t\t\t\treturn \u0022none\u0022;\r\n\r\n\t\t\treturn have.Count == 1 ? have[0] : $\u0022{have.Count} tips\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECatalog ids this tip could wait on: everything except itself, the preview id, and the ones it\r\n\t/// already waits on.\u003C/summary\u003E\r\n\tList\u003Cstring\u003E AvailablePrerequisites\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar have = new HashSet\u003Cstring\u003E( PrerequisiteList, StringComparer.Ordinal );\r\n\t\t\tvar mine = TipsStudio.Draft.Id ?? \u0022\u0022;\r\n\t\t\treturn TipsCatalog.Active\r\n\t\t\t\t.Select( t =\u003E t.Id )\r\n\t\t\t\t.Where( id =\u003E id != mine \u0026\u0026 id != TipsStudio.PreviewId \u0026\u0026 !have.Contains( id ) )\r\n\t\t\t\t.OrderBy( id =\u003E id, StringComparer.Ordinal )\r\n\t\t\t\t.ToList();\r\n\t\t}\r\n\t}\r\n\r\n\tvoid AddPrerequisite( string id )\r\n\t{\r\n\t\tTipsStudio.Draft.PrerequisiteTipIds.Add( id );\r\n\t\t_openDrop = null;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid RemovePrerequisite( string id )\r\n\t{\r\n\t\tTipsStudio.Draft.PrerequisiteTipIds.Remove( id );\r\n\t\tRefresh();\r\n\t}\r\n\r\n\t// ---- the two trigger pickers ----\r\n\r\n\t/// \u003Csummary\u003EThe Completion and Relevance pickers as data, so ONE block of markup renders both. A\r\n\t/// RenderFragment would be the other way to share it, and RenderFragments under-measure here.\u003C/summary\u003E\r\n\tpublic struct TriggerSlot\r\n\t{\r\n\t\tpublic string Key;\r\n\t\tpublic string Title;\r\n\t\tpublic string Blurb;\r\n\t\tpublic TipStudioTrigger Trigger;\r\n\t\tpublic bool IsCompletion;\r\n\t\tpublic bool NeedsName;\r\n\t\tpublic string NameHint;\r\n\t\tpublic bool IsComposite;\r\n\t\tpublic string EmptyCompositeHint;\r\n\t}\r\n\r\n\tList\u003CTriggerSlot\u003E TriggerSlots\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar completion = TipsStudio.Draft.Completion ??= new TipStudioTrigger();\r\n\t\t\tvar relevance = TipsStudio.Draft.Relevance ??= new TipStudioTrigger();\r\n\r\n\t\t\treturn new List\u003CTriggerSlot\u003E\r\n\t\t\t{\r\n\t\t\t\tSlot( \u0022completion\u0022, \u0022COMPLETION TRIGGER\u0022, \u0022what retires this tip\u0022, completion, true ),\r\n\t\t\t\tSlot( \u0022relevance\u0022, \u0022RELEVANCE TRIGGER\u0022, \u0022an extra gate before it shows\u0022, relevance, false ),\r\n\t\t\t};\r\n\t\t}\r\n\t}\r\n\r\n\tstatic TriggerSlot Slot( string key, string title, string blurb, TipStudioTrigger trigger, bool isCompletion )\r\n\t{\r\n\t\tvar kind = trigger.Kind;\r\n\t\tvar needsName = kind == TipTriggerKind.Signal || kind == TipTriggerKind.Ever\r\n\t\t\t|| kind == TipTriggerKind.Flag || kind == TipTriggerKind.AtLeast;\r\n\r\n\t\tvar hint = kind switch\r\n\t\t{\r\n\t\t\tTipTriggerKind.Signal =\u003E \u0022Signal(...) string\u0022,\r\n\t\t\tTipTriggerKind.Ever =\u003E \u0022ctx.Ever(...)\u0022,\r\n\t\t\tTipTriggerKind.Flag =\u003E \u0022ctx.SetFlag(...)\u0022,\r\n\t\t\t_ =\u003E \u0022ctx.SetNumber(...)\u0022,\r\n\t\t};\r\n\r\n\t\treturn new TriggerSlot\r\n\t\t{\r\n\t\t\tKey = key,\r\n\t\t\tTitle = title,\r\n\t\t\tBlurb = blurb,\r\n\t\t\tTrigger = trigger,\r\n\t\t\tIsCompletion = isCompletion,\r\n\t\t\tNeedsName = needsName,\r\n\t\t\tNameHint = hint,\r\n\t\t\tIsComposite = kind == TipTriggerKind.AnyOf || kind == TipTriggerKind.AllOf,\r\n\t\t\tEmptyCompositeHint = isCompletion \u0026\u0026 kind == TipTriggerKind.AllOf\r\n\t\t\t\t? \u0022empty: the shape a TipTriggerObject retires\u0022\r\n\t\t\t\t: \u0022empty, so it never fires\u0022,\r\n\t\t};\r\n\t}\r\n\r\n\tvoid SetKind( TipStudioTrigger trigger, TipTriggerKind kind )\r\n\t{\r\n\t\ttrigger.Kind = kind;\r\n\t\t_openDrop = null;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tstatic string ActionFace( TipStudioTrigger trigger )\r\n\t\t=\u003E string.IsNullOrEmpty( trigger.Action ) ? \u0022pick an action\u0022 : trigger.Action;\r\n\r\n\tvoid PickAction( TipStudioTrigger trigger, string action )\r\n\t{\r\n\t\ttrigger.Action = action;\r\n\t\t_openDrop = null;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid SetSource( TipStudioTrigger trigger, TipTriggerAnalogSource source )\r\n\t{\r\n\t\ttrigger.AnalogSource = source;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid BumpThreshold( TipStudioTrigger trigger, float delta )\r\n\t{\r\n\t\ttrigger.Threshold = MathF.Max( 0f, trigger.Threshold \u002B delta );\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid BumpSeconds( TipStudioTrigger trigger, float delta )\r\n\t{\r\n\t\ttrigger.Seconds = MathF.Max( 0f, trigger.Seconds \u002B delta );\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid BumpMagnitude( TipStudioTrigger trigger, float delta )\r\n\t{\r\n\t\ttrigger.Magnitude = Math.Clamp( trigger.Magnitude \u002B delta, 0f, 1f );\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid AddChild( TipStudioTrigger parent )\r\n\t{\r\n\t\tparent.Children.Add( new TipStudioTrigger { Kind = TipTriggerKind.Key } );\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid RemoveChild( TipStudioTrigger parent, TipStudioTrigger child )\r\n\t{\r\n\t\tparent.Children.Remove( child );\r\n\t\tRefresh();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EA composed child edits its one parameter through a single box, whichever box its kind reads.\r\n\t/// Nesting a full picker per child would triple the panel for a case the format barely uses.\u003C/summary\u003E\r\n\tstatic string ChildValue( TipStudioTrigger child ) =\u003E child.Kind switch\r\n\t{\r\n\t\tTipTriggerKind.InputAction =\u003E child.Action,\r\n\t\tTipTriggerKind.Key =\u003E child.Key,\r\n\t\tTipTriggerKind.Signal or TipTriggerKind.Ever or TipTriggerKind.Flag or TipTriggerKind.AtLeast =\u003E child.Name,\r\n\t\tTipTriggerKind.Timer =\u003E child.Seconds.ToString( \u00220.##\u0022 ),\r\n\t\tTipTriggerKind.AnalogAxis =\u003E child.Magnitude.ToString( \u00220.##\u0022 ),\r\n\t\t_ =\u003E \u0022\u0022,\r\n\t};\r\n\r\n\tstatic void SetChildValue( TipStudioTrigger child, string value )\r\n\t{\r\n\t\tswitch ( child.Kind )\r\n\t\t{\r\n\t\t\tcase TipTriggerKind.InputAction: child.Action = value; break;\r\n\t\t\tcase TipTriggerKind.Key: child.Key = value; break;\r\n\t\t\tcase TipTriggerKind.Signal:\r\n\t\t\tcase TipTriggerKind.Ever:\r\n\t\t\tcase TipTriggerKind.Flag:\r\n\t\t\tcase TipTriggerKind.AtLeast: child.Name = value; break;\r\n\t\t\tcase TipTriggerKind.Timer:\r\n\t\t\t\tif ( float.TryParse( value, out var seconds ) ) child.Seconds = MathF.Max( 0f, seconds );\r\n\t\t\t\tbreak;\r\n\t\t\tcase TipTriggerKind.AnalogAxis:\r\n\t\t\t\tif ( float.TryParse( value, out var magnitude ) ) child.Magnitude = Math.Clamp( magnitude, 0f, 1f );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic string ChildHint( TipStudioTrigger child ) =\u003E TipStudioTrigger.FieldFor( child.Kind ) switch\r\n\t{\r\n\t\t\u0022action\u0022 =\u003E \u0022an input action name\u0022,\r\n\t\t\u0022key\u0022 =\u003E \u0022a raw key name\u0022,\r\n\t\t\u0022name\u0022 =\u003E \u0022the named condition\u0022,\r\n\t\t\u0022name\u002Bthreshold\u0022 =\u003E \u0022the named number\u0022,\r\n\t\t\u0022seconds\u0022 =\u003E \u0022seconds\u0022,\r\n\t\t\u0022stick\u002Bmagnitude\u0022 =\u003E \u0022magnitude, 0 to 1\u0022,\r\n\t\t\u0022children\u0022 =\u003E \u0022nest one level only\u0022,\r\n\t\t_ =\u003E \u0022this kind takes no value\u0022,\r\n\t};\r\n\r\n\t// ---- the two preview cards ----\r\n\r\n\t/// \u003Csummary\u003EOne rendered card in the rail: the label above it and the runs inside it. Finished data, so\r\n\t/// the markup walks a list rather than calling into the parser mid-tree.\u003C/summary\u003E\r\n\tpublic struct PreviewCard\r\n\t{\r\n\t\tpublic string Label;\r\n\t\tpublic string Icon;\r\n\t\tpublic List\u003CTipSegment\u003E Segments;\r\n\t\tpublic bool Last;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe draft as the player will read it on each device, side by side. The pad card runs the same\r\n\t/// keycap remap the shipped display does (TipsCoach.PadLabelFor), so a chip with no controller equivalent\r\n\t/// disappears here exactly as it would in the game.\u003C/summary\u003E\r\n\tList\u003CPreviewCard\u003E PreviewCards\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar text = TipsStudio.Draft.Text ?? \u0022\u0022;\r\n\t\t\tvar pad = TipsStudio.Draft.TextPad ?? \u0022\u0022;\r\n\t\t\tvar icon = TipsStudio.Draft.Icon ?? \u0022\u0022;\r\n\r\n\t\t\treturn new List\u003CPreviewCard\u003E\r\n\t\t\t{\r\n\t\t\t\tnew PreviewCard\r\n\t\t\t\t{\r\n\t\t\t\t\tLabel = \u0022KEYBOARD\u0022,\r\n\t\t\t\t\tIcon = icon,\r\n\t\t\t\t\tSegments = TipSegment.Parse( text ).ToList(),\r\n\t\t\t\t},\r\n\t\t\t\tnew PreviewCard\r\n\t\t\t\t{\r\n\t\t\t\t\tLabel = \u0022CONTROLLER\u0022,\r\n\t\t\t\t\tIcon = icon,\r\n\t\t\t\t\tSegments = PadRuns( text, pad ),\r\n\t\t\t\t\tLast = true,\r\n\t\t\t\t},\r\n\t\t\t};\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe pad-mode runs for a wording: its pad text when authored, then every keycap put through\r\n\t/// the game\u0027s pad label map. A mapped label reads as a controller chip; an unmappable one is dropped, the\r\n\t/// same two rules the shipped card follows.\u003C/summary\u003E\r\n\tstatic List\u003CTipSegment\u003E PadRuns( string text, string textPad )\r\n\t{\r\n\t\tvar runs = new List\u003CTipSegment\u003E();\r\n\r\n\t\tforeach ( var seg in TipSegment.Parse( TipDeviceText.PadTextOr( text, textPad ) ) )\r\n\t\t{\r\n\t\t\tif ( seg.Kind != TipSegmentKind.Key )\r\n\t\t\t{\r\n\t\t\t\truns.Add( seg );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tvar mapped = TipDeviceText.PadCap( seg.Text, TipsCoach.PadLabelFor );\r\n\t\t\tif ( string.IsNullOrEmpty( mapped ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\truns.Add( new TipSegment( mapped, mapped == seg.Text ? TipSegmentKind.Key : TipSegmentKind.GamepadButton ) );\r\n\t\t}\r\n\r\n\t\treturn runs;\r\n\t}\r\n\r\n\t// ---- preview, test fire ----\r\n\r\n\tList\u003Cstring\u003E ActionNames =\u003E TipsStudio.ActionNames.ToList();\r\n\r\n\t/// \u003Csummary\u003EOne authoring note, already broken into lines that fit.\u003C/summary\u003E\r\n\tpublic struct NoteBlock\r\n\t{\r\n\t\tpublic List\u003Cstring\u003E Lines;\r\n\t}\r\n\r\n\tList\u003CNoteBlock\u003E NoteBlocks =\u003E TipsStudio.Notes\r\n\t\t.Select( n =\u003E new NoteBlock { Lines = Lines( n ) } )\r\n\t\t.ToList();\r\n\r\n\t/// \u003Csummary\u003EAn option list longer than this scrolls at a fixed height instead of growing the column.\u003C/summary\u003E\r\n\tstatic string Tall( int count ) =\u003E count \u003E 6 ? \u0022tall\u0022 : \u0022\u0022;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Chunk a sentence into lines short enough to lay out as text. A run that overflows its box does not\r\n\t/// wrap here: the style engine rasterizes it as a solid grey block, or drops it to an empty box. 46\r\n\t/// characters is one comfortable line in the widest box this panel has, and it is the same ceiling\r\n\t/// TipStudioText warns tip authors about.\r\n\t/// \u003C/summary\u003E\r\n\tstatic List\u003Cstring\u003E Lines( string text )\r\n\t{\r\n\t\tvar lines = new List\u003Cstring\u003E();\r\n\t\tif ( string.IsNullOrWhiteSpace( text ) )\r\n\t\t\treturn lines;\r\n\r\n\t\tvar line = \u0022\u0022;\r\n\r\n\t\tforeach ( var word in text.Split( \u0027 \u0027 ) )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrEmpty( word ) )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tif ( line.Length == 0 )\r\n\t\t\t\tline = word;\r\n\t\t\telse if ( line.Length \u002B 1 \u002B word.Length \u003E 46 )\r\n\t\t\t{\r\n\t\t\t\tlines.Add( line );\r\n\t\t\t\tline = word;\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t\tline = line \u002B \u0022 \u0022 \u002B word;\r\n\t\t}\r\n\r\n\t\tif ( line.Length \u003E 0 )\r\n\t\t\tlines.Add( line );\r\n\r\n\t\treturn lines;\r\n\t}\r\n\r\n\tvoid TogglePreview()\r\n\t{\r\n\t\tif ( TipsStudio.PreviewOn )\r\n\t\t\tTipsStudio.StopPreview( Scene );\r\n\t\telse\r\n\t\t\tTipsStudio.PushPreview( Scene );\r\n\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid PinDevice( TipDevice? device )\r\n\t{\r\n\t\tTipsStudio.PinnedDevice = device;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid TestFire()\r\n\t{\r\n\t\tTipsStudio.TestFire( Scene );\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid CompleteNow()\r\n\t{\r\n\t\tTipsStudio.CompleteNow();\r\n\t\tRefresh();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe one-line status under the bake buttons: whatever the last bake action said, or what\r\n\t/// is waiting in the staging folder when it has said nothing yet.\u003C/summary\u003E\r\n\tstring StatusLine\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar text = string.IsNullOrEmpty( _stageLine ) ? StagedLine : _stageLine;\r\n\t\t\tvar lines = Lines( text );\r\n\t\t\treturn lines.Count == 0 ? \u0022\u0022 : lines[0];\r\n\t\t}\r\n\t}\r\n\r\n\t// ---- bake ----\r\n\r\n\tstring BakeTarget\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar file = TipStudioJson.FileNameFor( TipsStudio.Draft.Id );\r\n\t\t\treturn file is null\r\n\t\t\t\t? \u0022Give the tip an id: the file takes its name.\u0022\r\n\t\t\t\t: $\u0022writes Assets/tips/{file}\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\tstring StagedLine\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar count = TipsStudio.StagedCount;\r\n\t\t\treturn count == 0 ? \u0022nothing staged\u0022 : $\u0022{count} staged for the editor\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid CopyJson()\r\n\t{\r\n\t\tTipsStudio.CopyJson();\r\n\t\t_copyLabel = \u0022Copied!\u0022;\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid Stage()\r\n\t{\r\n\t\t_stageLine = TipsStudio.Stage();\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid ClearStaged()\r\n\t{\r\n\t\t_stageLine = TipsStudio.ClearStaged();\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid ResetLabels()\r\n\t{\r\n\t\t_copyLabel = \u0022Copy .tip JSON\u0022;\r\n\t\t_stageLine = \u0022\u0022;\r\n\t}\r\n\r\n\t// ---- open / close, boot, cursor ----\r\n\r\n\t/// \u003Csummary\u003EBump the panel\u0027s own revision so the next frame rebuilds it. Every click calls this; typing\r\n\t/// into the id box does NOT, because a rebuild would take the cursor out of the box you are typing in.\u003C/summary\u003E\r\n\tvoid Refresh() =\u003E _revision\u002B\u002B;\r\n\r\n\t/// \u003Csummary\u003EEnter in a text box: push the wording at the preview card and refresh everything derived\r\n\t/// from it.\u003C/summary\u003E\r\n\tvoid Commit()\r\n\t{\r\n\t\tif ( TipsStudio.PreviewOn )\r\n\t\t\tTipsStudio.PushPreview( Scene );\r\n\r\n\t\tRefresh();\r\n\t}\r\n\r\n\tvoid Close()\r\n\t{\r\n\t\tTipsStudio.StopPreview( Scene );\r\n\t\tTipsStudio.Open = false;\r\n\t\t_openDrop = null;\r\n\t\tResetLabels();\r\n\t}\r\n\r\n\tprotected override void OnTreeBuilt()\r\n\t{\r\n\t\t// A background press-drag over a scrolling region must not pan the content or eat a button click; the\r\n\t\t// wheel and the scrollbar still scroll.\r\n\t\tif ( ListBody is not null )\r\n\t\t\tListBody.CanDragScroll = false;\r\n\r\n\t\tif ( MidBody is not null )\r\n\t\t\tMidBody.CanDragScroll = false;\r\n\r\n\t\tif ( RailBody is not null )\r\n\t\t\tRailBody.CanDragScroll = false;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\t// BOOT. \u0060fg_tips_studio\u0060 is a convar and s\u0026box persists convars across sessions, so a session could\r\n\t\t// otherwise come up with an authoring panel open from a value set weeks ago. This component\u0027s own\r\n\t\t// OpenOnStart decides the boot state and the persisted value never does. Deliberately in the FIRST\r\n\t\t// UPDATE, not OnStart: a panel created in code is configured by whatever created it, and OnStart would\r\n\t\t// race that assignment.\r\n\t\tif ( !_booted )\r\n\t\t{\r\n\t\t\t_booted = true;\r\n\t\t\tTipsStudio.Open = OpenOnStart;\r\n\t\t}\r\n\r\n\t\t// The Studio is an authoring tool; a published build has nothing to author with it.\r\n\t\tif ( EditorOnly \u0026\u0026 !Application.IsEditor )\r\n\t\t{\r\n\t\t\tTipsStudio.Open = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Opens only. Closing is the header \u00D7 or the convar, so this key can never fight a text box.\r\n\t\tif ( !TipsStudio.Open \u0026\u0026 !string.IsNullOrEmpty( OpenKey ) \u0026\u0026 Input.Keyboard.Pressed( OpenKey ) )\r\n\t\t{\r\n\t\t\tTipsStudio.Open = true;\r\n\t\t\tRefresh();\r\n\t\t}\r\n\r\n\t\tif ( TipsStudio.Open )\r\n\t\t{\r\n\t\t\tMouse.Visibility = MouseVisibility.Visible;\r\n\t\t\t_wasOpen = true;\r\n\t\t}\r\n\t\telse if ( _wasOpen )\r\n\t\t{\r\n\t\t\t_wasOpen = false;\r\n\t\t\tResetLabels();\r\n\t\t\tTipsStudio.StopPreview( Scene );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\t// Everything the Studio pins is static and would otherwise follow the developer into the next scene:\r\n\t\t// the preview draft, a test-fire draft, and the pinned preview device.\r\n\t\tTipsStudio.Shutdown();\r\n\t}\r\n\r\n\t// Fold the things a CLICK changes, and nothing anyone TYPES into. A rebuild rehomes every TextEntry, which\r\n\t// takes the cursor out of the box mid-word, so Id and a trigger\u0027s Key and Name are deliberately absent:\r\n\t// the panel repaints when you press Enter or click, via _revision.\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\tvar hc = new HashCode();\r\n\t\thc.Add( TipsStudio.Open );\r\n\t\thc.Add( TipsStudio.OpenedFrom );\r\n\t\thc.Add( TipsStudio.PreviewOn );\r\n\t\thc.Add( TipsStudio.PinnedDevice );\r\n\t\thc.Add( TipsCoach.ActiveTip?.Id );\r\n\t\thc.Add( _copyLabel );\r\n\t\thc.Add( _stageLine );\r\n\t\thc.Add( _openDrop );\r\n\t\thc.Add( _revision );\r\n\t\thc.Add( TipsStudio.Draft.Priority );\r\n\t\thc.Add( TipsStudio.Draft.MaxShowSeconds );\r\n\t\thc.Add( PrerequisiteList.Count );\r\n\r\n\t\tforeach ( var slot in TriggerSlots )\r\n\t\t{\r\n\t\t\thc.Add( slot.Trigger.Kind );\r\n\t\t\thc.Add( slot.Trigger.Action );\r\n\t\t\thc.Add( slot.Trigger.Threshold );\r\n\t\t\thc.Add( slot.Trigger.Seconds );\r\n\t\t\thc.Add( slot.Trigger.AnalogSource );\r\n\t\t\thc.Add( slot.Trigger.Magnitude );\r\n\t\t\thc.Add( slot.Trigger.Children.Count );\r\n\t\t\tforeach ( var child in slot.Trigger.Children )\r\n\t\t\t\thc.Add( child.Kind );\r\n\t\t}\r\n\r\n\t\treturn hc.ToHashCode();\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/TipsWorld.cs","FileName":"TipsWorld.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// Optional world seams a game sets once at bootstrap so world-anchored triggers can read the local\r\n/// player. A library cannot reach into a game\u0027s player, so these stay null until the game wires them.\r\n///\r\n/// All are fail-inert: a trigger that needs an unset seam is simply INERT (it never fires and never\r\n/// throws), so nothing crashes when a game skips them. Input, Signal and Timer triggers need no seam at\r\n/// all. Set the seams you use, leave the rest null.\r\n///\r\n/// \u003Ccode\u003E\r\n/// using Sandbox;\r\n/// using FieldGuide.Tips;\r\n///\r\n/// TipsWorld.LocalPlayerPosition = () =\u003E MyLocalPlayer.WorldPosition;\r\n/// TipsWorld.AimRay = () =\u003E new Ray( MyCamera.WorldPosition, MyCamera.WorldRotation.Forward );\r\n/// \u003C/code\u003E\r\n/// \u003C/summary\u003E\r\npublic static class TipsWorld\r\n{\r\n\t/// \u003Csummary\u003EWhether a local player exists to coach. Read by the self-driving coach when no context is\r\n\t/// pushed. Default: always true, so input-only tips show without any bootstrap wiring.\u003C/summary\u003E\r\n\tpublic static Func\u003Cbool\u003E HasLocalPlayer { get; set; } = static () =\u003E true;\r\n\r\n\t/// \u003Csummary\u003ELocal player world position, for \u003Csee cref=\u0022TipTriggerObject.Mode.PlayerEntered\u0022/\u003E triggers.\r\n\t/// Null leaves those triggers inert.\u003C/summary\u003E\r\n\tpublic static Func\u003CVector3\u003E LocalPlayerPosition { get; set; }\r\n\r\n\t/// \u003Csummary\u003ECamera / aim ray, for \u003Csee cref=\u0022TipTriggerObject.Mode.LookedAt\u0022/\u003E triggers. Null leaves\r\n\t/// those triggers inert.\u003C/summary\u003E\r\n\tpublic static Func\u003CRay\u003E AimRay { get; set; }\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Studio/TipsStudio.cs","FileName":"TipsStudio.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The Tips Studio\u0027s state and every action its panel takes. The panel\r\n/// (\u003Csee cref=\u0022TipsStudioPanel\u0022/\u003E) is markup over this; everything that decides something lives here, so the\r\n/// razor stays readable and this stays testable by eye.\r\n///\r\n/// WHAT IT IS. An authoring surface for tips that runs inside your game: list the merged catalog, open any\r\n/// tip in an editor, watch the real card change as you type, fire the tip and its completion for real, and\r\n/// bake the result out as a \u003Cc\u003E.tip\u003C/c\u003E file. Nothing here is part of a shipped game\u0027s runtime: the panel\r\n/// only exists if you add it, it starts closed, and \u003Cc\u003Efg_tips_studio\u003C/c\u003E is off by default.\r\n///\r\n/// TWO WAYS A DRAFT REACHES THE COACH, and they are deliberately different:\r\n/// \u003Clist type=\u0022bullet\u0022\u003E\r\n/// \u003Citem\u003EPREVIEW registers the draft under \u003Csee cref=\u0022PreviewId\u0022/\u003E, an id nothing else uses, and force-shows\r\n/// it. It is a picture of the card. It cannot be shadowed by a real tip with the same id, which is what would\r\n/// happen if it registered under the draft\u0027s own id (drafts are the lowest-precedence source), and it cannot\r\n/// mark anything complete.\u003C/item\u003E\r\n/// \u003Citem\u003ETEST FIRE registers the draft under its OWN id and shows it, so completing it retires the real tip\r\n/// and the chain advances the way it will in the game. If a code or asset tip already owns that id, that one\r\n/// wins, which is correct: you are testing the chain, not the draft.\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n///\r\n/// CLEAN-UP. Everything it touches is static and would otherwise outlive the scene: the preview draft, the\r\n/// test-fire draft, and the pinned preview device. \u003Csee cref=\u0022Shutdown\u0022/\u003E hands all of it back, and the panel\r\n/// calls it from OnDestroy.\r\n/// \u003C/summary\u003E\r\npublic static class TipsStudio\r\n{\r\n\t/// \u003Csummary\u003EThe id the live preview registers under. Long and namespaced on purpose: it must never\r\n\t/// collide with a real tip, because a collision would silently show the real tip instead of the draft.\u003C/summary\u003E\r\n\tpublic const string PreviewId = \u0022fg_tips_studio_preview\u0022;\r\n\r\n\t/// \u003Csummary\u003EFolder under \u003Cc\u003EFileSystem.Data\u003C/c\u003E the Studio stages baked tips in, for the editor menu\r\n\t/// action to pick up. Staging through a file rather than a live static is what lets you bake in play and\r\n\t/// write the asset after you have stopped playing.\u003C/summary\u003E\r\n\tpublic const string StageFolder = \u0022fieldguide_tips_studio\u0022;\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Open / close\r\n\t// ------------------------------------------------------------------\r\n\r\n\tprivate static bool _open;\r\n\r\n\t/// \u003Csummary\u003EOpen or close the Tips Studio. Off by default, and the panel forces it off at boot: s\u0026amp;box\r\n\t/// persists convars between sessions, so without that a value set weeks ago would open an authoring panel\r\n\t/// over someone\u0027s game.\u003C/summary\u003E\r\n\t[ConVar( \u0022fg_tips_studio\u0022, Help = \u0022Open or close the Tips Studio authoring panel (dev tool, off by default)\u0022 )]\r\n\tpublic static bool Open\r\n\t{\r\n\t\tget =\u003E _open;\r\n\t\tset =\u003E _open = value;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhich tab is showing.\u003C/summary\u003E\r\n\tpublic static StudioTab Tab { get; set; } = StudioTab.Tips;\r\n\r\n\t/// \u003Csummary\u003EThe Studio\u0027s three tabs.\u003C/summary\u003E\r\n\tpublic enum StudioTab\r\n\t{\r\n\t\t/// \u003Csummary\u003EThe merged catalog, with source labels.\u003C/summary\u003E\r\n\t\tTips,\r\n\r\n\t\t/// \u003Csummary\u003EThe draft editor: wording, order, triggers, preview and test fire.\u003C/summary\u003E\r\n\t\tDraft,\r\n\r\n\t\t/// \u003Csummary\u003EThe bake-out surface: the .tip JSON, Copy, and staging for the editor.\u003C/summary\u003E\r\n\t\tBake,\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// The draft\r\n\t// ------------------------------------------------------------------\r\n\r\n\tprivate static TipStudioDraft _draft = new();\r\n\r\n\t/// \u003Csummary\u003EThe tip being authored. Never null.\u003C/summary\u003E\r\n\tpublic static TipStudioDraft Draft\r\n\t{\r\n\t\tget =\u003E _draft ??= new TipStudioDraft();\r\n\t\tset =\u003E _draft = value ?? new TipStudioDraft();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe catalog id the draft was opened from, or null for a new tip. Shown so it is obvious\r\n\t/// whether you are editing something that already exists.\u003C/summary\u003E\r\n\tpublic static string OpenedFrom { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EStart a new, empty tip.\u003C/summary\u003E\r\n\tpublic static void NewDraft()\r\n\t{\r\n\t\tDraft = new TipStudioDraft { Priority = 100 };\r\n\t\tOpenedFrom = null;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EOpen a catalog tip in the editor. The two code-only predicates have no authored form and are\r\n\t/// dropped; \u003Csee cref=\u0022DroppedPredicates\u0022/\u003E says so on screen.\u003C/summary\u003E\r\n\tpublic static void OpenTip( string id )\r\n\t{\r\n\t\tvar def = TipsCatalog.Active.FirstOrDefault( t =\u003E t.Id == id );\r\n\t\tif ( def is null )\r\n\t\t\treturn;\r\n\r\n\t\tDraft = TipStudioDraft.FromDefinition( def );\r\n\t\tOpenedFrom = id;\r\n\t\tDroppedPredicates = HasCodePredicates( def );\r\n\t\tTab = StudioTab.Draft;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when the tip currently open was carrying a \u003Cc\u003ETrigger\u003C/c\u003E or \u003Cc\u003ECompleteWhen\u003C/c\u003E\r\n\t/// predicate, which a \u003Cc\u003E.tip\u003C/c\u003E file cannot hold. Baking it out keeps the declarative triggers and\r\n\t/// loses the predicate, so the panel warns before you do.\u003C/summary\u003E\r\n\tpublic static bool DroppedPredicates { get; private set; }\r\n\r\n\tprivate static bool HasCodePredicates( TipDefinition def )\r\n\t{\r\n\t\t// A tip that never set them carries the record\u0027s defaults. Comparing against a fresh default is the\r\n\t\t// only way to tell \u0022the author wrote a predicate\u0022 from \u0022the record filled one in\u0022.\r\n\t\tvar plain = new TipDefinition { Id = \u0022probe\u0022, Text = \u0022\u0022 };\r\n\t\treturn def.Trigger != plain.Trigger || def.CompleteWhen != plain.CompleteWhen;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe authoring notes for the current draft (grey-block run lengths, triggers that can never\r\n\t/// fire). Recomputed on read; the panel refreshes them when you press Enter in a box or click anything,\r\n\t/// because rebuilding the panel while you type would take the cursor out of the box.\u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003Cstring\u003E Notes =\u003E TipStudioText.Warnings( Draft );\r\n\r\n\t/// \u003Csummary\u003EThe draft as \u003Cc\u003E.tip\u003C/c\u003E JSON: what Copy puts on the clipboard and what a bake writes.\u003C/summary\u003E\r\n\tpublic static string Json =\u003E TipStudioJson.Write( Draft );\r\n\r\n\t/// \u003Csummary\u003ETrue when the draft\u0027s id already names a tip from a HIGHER-precedence source, so a bake would\r\n\t/// be shadowed until that source lets go. Worth saying out loud before someone wonders why their new file\r\n\t/// does nothing.\u003C/summary\u003E\r\n\tpublic static string ShadowedBy\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrWhiteSpace( Draft.Id ) )\r\n\t\t\t\treturn null;\r\n\r\n\t\t\tvar source = TipsCatalog.SourceOf( Draft.Id );\r\n\t\t\treturn source == \u0022code\u0022 ? \u0022code\u0022 : null;\r\n\t\t}\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Live preview\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003EWhether the real card is mirroring the draft right now.\u003C/summary\u003E\r\n\tpublic static bool PreviewOn { get; private set; }\r\n\r\n\t/// \u003Csummary\u003EPush the draft onto the real card, or refresh what is already there. Registers under\r\n\t/// \u003Csee cref=\u0022PreviewId\u0022/\u003E so a draft of an existing tip is not shadowed by the tip it copies.\u003C/summary\u003E\r\n\tpublic static void PushPreview( Scene scene )\r\n\t{\r\n\t\tvar def = Draft.ToDefinition();\r\n\r\n\t\t// The preview stands in for the draft even before it has an id, so an author sees the card from the\r\n\t\t// first character typed rather than after they remember to name it.\r\n\t\tvar preview = new TipDefinition\r\n\t\t{\r\n\t\t\tId = PreviewId,\r\n\t\t\tText = Draft.Text ?? \u0022\u0022,\r\n\t\t\tTextPad = string.IsNullOrEmpty( Draft.TextPad ) ? null : Draft.TextPad,\r\n\t\t\tIcon = Draft.Icon ?? \u0022\u0022,\r\n\t\t\tPriority = def?.Priority ?? 0,\r\n\t\t};\r\n\r\n\t\tTipsCatalog.RegisterRuntime( preview );\r\n\t\tPreviewOn = true;\r\n\r\n\t\tvar coach = LiveCoach( scene );\r\n\t\tcoach?.ForceShow( PreviewId );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETake the preview off the card and out of the catalog.\u003C/summary\u003E\r\n\tpublic static void StopPreview( Scene scene )\r\n\t{\r\n\t\tPreviewOn = false;\r\n\t\tTipsCatalog.UnregisterRuntime( PreviewId );\r\n\t\tTipsCoach.PreviewDevice = null;\r\n\r\n\t\t// The card may still be showing a tip that no longer exists. Drop it rather than dismiss it: dismissing\r\n\t\t// would write the fake preview id into the player\u0027s saved progress and leave it there for good.\r\n\t\tif ( TipsCoach.ActiveTip?.Id == PreviewId )\r\n\t\t\tLiveCoach( scene )?.DropActive();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhich device the preview card is pinned to, or null for whatever the player last used.\u003C/summary\u003E\r\n\tpublic static TipDevice? PinnedDevice\r\n\t{\r\n\t\tget =\u003E TipsCoach.PreviewDevice;\r\n\t\tset =\u003E TipsCoach.PreviewDevice = value;\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Test fire\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Make the draft the live tip UNDER ITS OWN ID and show it now. From here its completion is the real\r\n\t/// thing: fire the trigger in the game, or press Complete, and the tip retires and the chain moves on.\r\n\t/// Returns false when the draft has no id yet.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static bool TestFire( Scene scene )\r\n\t{\r\n\t\tvar def = Draft.ToDefinition();\r\n\t\tif ( def is null )\r\n\t\t\treturn false;\r\n\r\n\t\t// A test fire of a tip already marked complete would retire the moment it appeared.\r\n\t\tTipsCoach.Uncomplete( def.Id );\r\n\t\tTipsCatalog.UnregisterRuntime( PreviewId );\r\n\t\tPreviewOn = false;\r\n\t\tTipsCatalog.RegisterRuntime( def );\r\n\r\n\t\tvar coach = LiveCoach( scene );\r\n\t\tif ( coach is null )\r\n\t\t{\r\n\t\t\tLog.Warning( \u0022fg_tips: no TipsCoach in the scene, so there is nothing to show the tip on.\u0022 );\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\treturn coach.ForceShow( def.Id );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFire the draft\u0027s completion by hand, the same path a world trigger uses. The tip retires and\r\n\t/// whatever waits on it becomes eligible.\u003C/summary\u003E\r\n\tpublic static void CompleteNow()\r\n\t{\r\n\t\tif ( string.IsNullOrWhiteSpace( Draft.Id ) )\r\n\t\t\treturn;\r\n\r\n\t\tTipsCoach.Complete( Draft.Id );\r\n\t}\r\n\r\n\tprivate static TipsCoach LiveCoach( Scene scene )\r\n\t\t=\u003E scene?.GetAllComponents\u003CTipsCoach\u003E().FirstOrDefault();\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Input actions (the action picker)\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The project\u0027s real input actions, for the InputAction picker: \u003Cc\u003EInput.ActionNames\u003C/c\u003E, which is the\r\n\t/// engine\u0027s list from the current game\u0027s input settings, the same list the \u003Cc\u003E[InputAction]\u003C/c\u003E inspector\r\n\t/// dropdown draws from. Sorted, and empty rather than throwing outside a running game.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003Cstring\u003E ActionNames\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tvar names = Input.ActionNames?.Where( n =\u003E !string.IsNullOrWhiteSpace( n ) ).ToList();\r\n\t\t\t\tif ( names is null || names.Count == 0 )\r\n\t\t\t\t\treturn Array.Empty\u003Cstring\u003E();\r\n\r\n\t\t\t\tnames.Sort( StringComparer.OrdinalIgnoreCase );\r\n\t\t\t\treturn names;\r\n\t\t\t}\r\n\t\t\tcatch ( Exception )\r\n\t\t\t{\r\n\t\t\t\treturn Array.Empty\u003Cstring\u003E();\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Bake out\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003ECopy the draft\u0027s \u003Cc\u003E.tip\u003C/c\u003E JSON to the system clipboard, from in game. Paste it into a new\r\n\t/// file under your project\u0027s \u003Cc\u003EAssets/\u003C/c\u003E and the editor picks it up as a tip.\u003C/summary\u003E\r\n\tpublic static void CopyJson()\r\n\t{\r\n\t\tSandbox.UI.Clipboard.SetText( Json );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Write the draft into \u003Csee cref=\u0022StageFolder\u0022/\u003E under \u003Cc\u003EFileSystem.Data\u003C/c\u003E, where the editor menu\r\n\t/// action \u0022Field Guide / Write staged tips\u0022 picks it up and writes the real asset. Two steps because game\r\n\t/// code cannot write into a project\u0027s \u003Cc\u003EAssets/\u003C/c\u003E folder, and because staging survives the end of the\r\n\t/// play session, so you can author in play and land the file afterwards.\r\n\t/// \u003C/summary\u003E\r\n\t/// \u003Creturns\u003EA line for the panel saying what happened.\u003C/returns\u003E\r\n\tpublic static string Stage()\r\n\t{\r\n\t\tvar file = TipStudioJson.FileNameFor( Draft.Id );\r\n\t\tif ( file is null )\r\n\t\t\treturn \u0022Give the tip an id first.\u0022;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tFileSystem.Data.CreateDirectory( StageFolder );\r\n\t\t\tvar path = $\u0022{StageFolder}/{file}\u0022;\r\n\t\t\tFileSystem.Data.WriteAllText( path, Json );\r\n\t\t\tLog.Info( $\u0022fg_tips: staged {file}. In the editor, run Field Guide / Write staged tips to Assets/tips.\u0022 );\r\n\r\n\t\t\t// Short on purpose: this lands in a one-line status slot in the panel, and the console line\r\n\t\t\t// above already carries the full instruction.\r\n\t\t\treturn $\u0022staged {file}\u0022;\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022fg_tips: could not stage {file} ({e.Message}).\u0022 );\r\n\t\t\treturn $\u0022Could not stage {file}: {e.Message}\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EHow many tips are waiting in the staging folder, so the panel can say whether there is\r\n\t/// anything for the editor action to do.\u003C/summary\u003E\r\n\tpublic static int StagedCount\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\treturn FileSystem.Data.DirectoryExists( StageFolder )\r\n\t\t\t\t\t? FileSystem.Data.FindFile( StageFolder, \u0022*.tip\u0022, false ).Count()\r\n\t\t\t\t\t: 0;\r\n\t\t\t}\r\n\t\t\tcatch ( Exception )\r\n\t\t\t{\r\n\t\t\t\treturn 0;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EEmpty the staging folder, for when a bake was a mistake or the files have landed.\u003C/summary\u003E\r\n\tpublic static string ClearStaged()\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( !FileSystem.Data.DirectoryExists( StageFolder ) )\r\n\t\t\t\treturn \u0022Nothing staged.\u0022;\r\n\r\n\t\t\tvar cleared = 0;\r\n\t\t\tforeach ( var file in FileSystem.Data.FindFile( StageFolder, \u0022*.tip\u0022, false ).ToList() )\r\n\t\t\t{\r\n\t\t\t\tFileSystem.Data.DeleteFile( $\u0022{StageFolder}/{file}\u0022 );\r\n\t\t\t\tcleared\u002B\u002B;\r\n\t\t\t}\r\n\r\n\t\t\treturn cleared == 0 ? \u0022Nothing staged.\u0022 : $\u0022Cleared {cleared} staged tip(s).\u0022;\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\treturn $\u0022Could not clear the staging folder: {e.Message}\u0022;\r\n\t\t}\r\n\t}\r\n\r\n\t// ------------------------------------------------------------------\r\n\t// Shutdown\r\n\t// ------------------------------------------------------------------\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Hand back everything the Studio pinned: the preview and test-fire drafts, and the pinned preview\r\n\t/// device. Called from the panel\u0027s OnDestroy, because all of it is static and would otherwise follow the\r\n\t/// developer into the next scene, exactly the trap a scene-registered code catalog falls into.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void Shutdown()\r\n\t{\r\n\t\tPreviewOn = false;\r\n\t\tTipsCoach.PreviewDevice = null;\r\n\t\tTipsCatalog.ClearRuntime();\r\n\t\tOpen = false;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"TipsCatalog.cs","FileName":"TipsCatalog.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The registry the coach reads. Three sources merge into \u003Csee cref=\u0022Active\u0022/\u003E, keyed by\r\n/// \u003Csee cref=\u0022TipDefinition.Id\u0022/\u003E, highest precedence first:\r\n/// \u003Clist type=\u0022number\u0022\u003E\r\n/// \u003Citem\u003ECODE: whatever the game passed to \u003Csee cref=\u0022Register(IReadOnlyList{TipDefinition})\u0022/\u003E.\u003C/item\u003E\r\n/// \u003Citem\u003EASSETS: every \u003Cc\u003E.tip\u003C/c\u003E \u003Csee cref=\u0022TipResource\u0022/\u003E found via\r\n/// \u003Cc\u003EResourceLibrary.GetAll\u0026lt;TipResource\u0026gt;()\u003C/c\u003E, mapped through \u003Csee cref=\u0022TipResource.ToDefinition\u0022/\u003E.\u003C/item\u003E\r\n/// \u003Citem\u003EDRAFTS: runtime tips injected via \u003Csee cref=\u0022RegisterRuntime(TipDefinition)\u0022/\u003E (a test-fire / dev\r\n/// tool seam), lowest precedence.\u003C/item\u003E\r\n/// \u003C/list\u003E\r\n/// On an id collision CODE wins, then the asset, then the draft: a game\u0027s own code catalog is authored\r\n/// intent, and assets are usually additive or mod content. This is the inverse of the RPG kit, where\r\n/// authored assets override code demos. When all three sources are empty the shipped \u003Csee cref=\u0022Example\u0022/\u003E\r\n/// surfaces so the panel still does something before any content is wired in.\r\n///\r\n/// Back-compat: \u003Csee cref=\u0022Register(IReadOnlyList{TipDefinition})\u0022/\u003E and \u003Csee cref=\u0022Active\u0022/\u003E keep their\r\n/// v0.2 signatures. A game that only calls Register with no assets and no drafts reads exactly its own\r\n/// list, in its own order, from \u003Csee cref=\u0022Active\u0022/\u003E just as before.\r\n///\r\n/// LIFETIME. Every source here is STATIC, so a catalog registered from a scene component outlives that\r\n/// scene and that play session: load another scene in the same editor process and the old tips are still\r\n/// the highest-priority thing the coach can pick, with nothing over there able to retire them. Register\r\n/// from a component and you want \u003Csee cref=\u0022RegisterScoped(IReadOnlyList{TipDefinition})\u0022/\u003E, which hands the\r\n/// previous catalog back when you dispose it in OnDestroy. Runtime drafts have the same rule and the same\r\n/// answer, \u003Csee cref=\u0022ClearRuntime\u0022/\u003E.\r\n///\r\n/// FRESHNESS. The merged view is DERIVED state: one \u003Csee cref=\u0022TipCatalogView\u0022/\u003E holding the ordered list\r\n/// and the id-to-source map together, rebuilt whenever a source moves and swapped in whole, so the two can\r\n/// never disagree with each other. Sources announce their own moves: \u003Csee cref=\u0022Register\u0022/\u003E and the draft calls\r\n/// invalidate directly, and \u003Csee cref=\u0022TipResource\u0022/\u003E calls \u003Csee cref=\u0022NoteAssetsChanged\u0022/\u003E from its PostLoad\r\n/// / PostReload, which is what makes an edited \u003Cc\u003E.tip\u003C/c\u003E reach a running session. \u003Cc\u003Efg_tips_rebuild\u003C/c\u003E is\r\n/// the manual reset for anything that slips past (a deleted asset, a code hotload).\r\n///\r\n/// Game-specific tip text (which keys open which panels, what the dev overlay does, spell and\r\n/// potion lines) belongs in YOUR catalog, not in the kit. The kit owns the mechanic (priority,\r\n/// prerequisites, trigger, complete-when, timeout); you own the words.\r\n/// \u003C/summary\u003E\r\npublic static class TipsCatalog\r\n{\r\n\t// CODE source: the game\u0027s own registered catalog (highest precedence).\r\n\tprivate static IReadOnlyList\u003CTipDefinition\u003E _code = Array.Empty\u003CTipDefinition\u003E();\r\n\r\n\t// DRAFT source: runtime-authored tips (lowest precedence), kept in their own store so a draft never\r\n\t// shadows a code or asset tip and a Rebuild can rescan assets without dropping live drafts.\r\n\tprivate static readonly Dictionary\u003Cstring, TipDefinition\u003E _drafts = new( StringComparer.Ordinal );\r\n\r\n\t// Source revisions. Bumped whenever that source moves; the snapshot records the three it was built from\r\n\t// and a read that finds any of them changed rebuilds. Comparing them is three field reads, no allocation,\r\n\t// which is what lets the coach ask for Active several times a frame.\r\n\tprivate static int _draftRevision;\r\n\tprivate static int _assetRevision;\r\n\r\n\t// The derived view (list \u002B labels together, from the pure TipCatalogMerge) and the stamp saying what it\r\n\t// was built from. One reference, swapped in whole: there is no second static that could disagree with it.\r\n\tprivate static TipCatalogView _view;\r\n\tprivate static TipCatalogStamp _stamp;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The merged, deduped walkthrough the coach reads (code \u002B assets \u002B drafts by the precedence above),\r\n\t/// or \u003Csee cref=\u0022Example\u0022/\u003E when every source is empty. Priority breaks ties; PrerequisiteTipIds\r\n\t/// sequences the spine. Derived and cached against the source revisions, so an edited or newly created\r\n\t/// \u003Cc\u003E.tip\u003C/c\u003E shows up on the next read; \u003Cc\u003Efg_tips_rebuild\u003C/c\u003E forces a rescan by hand.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CTipDefinition\u003E Active =\u003E Current().Tips;\r\n\r\n\t/// \u003Csummary\u003EThe merged catalog and its source labels together, for a reader that needs both and must not\r\n\t/// see them from two different builds (the Tips Studio\u0027s tip list).\u003C/summary\u003E\r\n\tpublic static TipCatalogView View =\u003E Current();\r\n\r\n\t/// \u003Csummary\u003EInstall your own tutorial line (the CODE source). Call once at bootstrap; a null or empty\r\n\t/// list is ignored. Additive to any \u003Cc\u003E.tip\u003C/c\u003E assets and drafts; code wins id collisions.\r\n\t///\r\n\t/// The catalog is static and outlives the scene that registered it. Registering from a component that\r\n\t/// can be destroyed (a scene bootstrap, a dev harness) wants\r\n\t/// \u003Csee cref=\u0022RegisterScoped(IReadOnlyList{TipDefinition})\u0022/\u003E instead.\u003C/summary\u003E\r\n\tpublic static void Register( IReadOnlyList\u003CTipDefinition\u003E tips )\r\n\t{\r\n\t\tif ( tips is null || tips.Count == 0 )\r\n\t\t\treturn;\r\n\t\t_code = tips;\r\n\t\tInvalidate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Register a code catalog that HANDS ITSELF BACK. Dispose the returned token (from your component\u0027s\r\n\t/// OnDestroy, or with a \u003Cc\u003Eusing\u003C/c\u003E) and whatever was registered before is restored, so a scene\u0027s tips\r\n\t/// cannot follow the player into the next scene. Disposing twice is a no-op, and disposing out of order\r\n\t/// still restores what this call displaced rather than clearing the catalog outright.\r\n\t///\r\n\t/// This is the seam the kit\u0027s own demo bootstrap discipline generalizes: statics outlive scenes, so\r\n\t/// whoever set one puts it back.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IDisposable RegisterScoped( IReadOnlyList\u003CTipDefinition\u003E tips )\r\n\t{\r\n\t\tvar previous = _code;\r\n\t\tRegister( tips );\r\n\t\treturn new ScopedCode( previous, tips );\r\n\t}\r\n\r\n\tprivate sealed class ScopedCode : IDisposable\r\n\t{\r\n\t\tprivate readonly IReadOnlyList\u003CTipDefinition\u003E _previous;\r\n\t\tprivate IReadOnlyList\u003CTipDefinition\u003E _mine;\r\n\r\n\t\tpublic ScopedCode( IReadOnlyList\u003CTipDefinition\u003E previous, IReadOnlyList\u003CTipDefinition\u003E mine )\r\n\t\t{\r\n\t\t\t_previous = previous ?? Array.Empty\u003CTipDefinition\u003E();\r\n\t\t\t_mine = mine;\r\n\t\t}\r\n\r\n\t\tpublic void Dispose()\r\n\t\t{\r\n\t\t\tif ( _mine is null )\r\n\t\t\t\treturn; // already handed back\r\n\r\n\t\t\t// Only restore if OUR list is still the installed one. A later Register replaced us, and stomping\r\n\t\t\t// that would be worse than leaving it: the newer registration is the live intent.\r\n\t\t\tif ( ReferenceEquals( _code, _mine ) )\r\n\t\t\t{\r\n\t\t\t\t_code = _previous;\r\n\t\t\t\tInvalidate();\r\n\t\t\t}\r\n\r\n\t\t\t_mine = null;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Inject (or replace) a live runtime draft tip (the lowest-precedence DRAFT source). An id that also\r\n\t/// names a code tip or an authored \u003Cc\u003E.tip\u003C/c\u003E asset resolves to that real tip, never the draft. The\r\n\t/// seam a test-fire / dev tool uses so an in-progress tip is previewable without an editor compile.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void RegisterRuntime( TipDefinition def )\r\n\t{\r\n\t\tif ( def is null || string.IsNullOrEmpty( def.Id ) )\r\n\t\t\treturn;\r\n\t\t_drafts[def.Id] = def;\r\n\t\t_draftRevision\u002B\u002B;\r\n\t\tInvalidate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERemove a draft tip previously injected via \u003Csee cref=\u0022RegisterRuntime(TipDefinition)\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic static void UnregisterRuntime( string id )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( id ) || !_drafts.Remove( id ) )\r\n\t\t\treturn;\r\n\t\t_draftRevision\u002B\u002B;\r\n\t\tInvalidate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EDrop every runtime draft, leaving the code catalog and the \u003Cc\u003E.tip\u003C/c\u003E assets alone. The\r\n\t/// Tips Studio calls this when it shuts down: an authoring draft must not follow the developer into\r\n\t/// another scene or another session.\u003C/summary\u003E\r\n\tpublic static void ClearRuntime()\r\n\t{\r\n\t\tif ( _drafts.Count == 0 )\r\n\t\t\treturn;\r\n\t\t_drafts.Clear();\r\n\t\t_draftRevision\u002B\u002B;\r\n\t\tInvalidate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe ids of the live runtime drafts, so a dev tool can list or clean up exactly what it put\r\n\t/// in. Ordered for a stable readout.\u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003Cstring\u003E RuntimeIds\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tvar ids = new List\u003Cstring\u003E( _drafts.Keys );\r\n\t\t\tids.Sort( StringComparer.Ordinal );\r\n\t\t\treturn ids;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EForce a fresh merge (rescans \u003Cc\u003E.tip\u003C/c\u003E assets). Call after hot-loading new tip assets;\r\n\t/// otherwise the lazy build is enough. Code catalog and live drafts are preserved.\u003C/summary\u003E\r\n\tpublic static void Rebuild() =\u003E Invalidate();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The \u003Cc\u003E.tip\u003C/c\u003E asset source moved: a tip was loaded for the first time, or recompiled from disk after\r\n\t/// an edit. \u003Csee cref=\u0022TipResource\u0022/\u003E calls this from its PostLoad / PostReload, which is the engine hook\r\n\t/// a kit can reach (\u003Cc\u003EResourceLibrary.IEventListener\u003C/c\u003E is internal), and it is what makes an edited tip\r\n\t/// appear in a running session. Cheap and idempotent: it bumps a counter and drops the derived snapshot.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void NoteAssetsChanged()\r\n\t{\r\n\t\t_assetRevision\u002B\u002B;\r\n\t\tInvalidate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ERescan \u003Cc\u003E.tip\u003C/c\u003E assets and rebuild the merged catalog by hand, then print what came back.\r\n\t/// The fallback for anything the automatic hooks cannot see: a deleted asset, or a code hotload that left\r\n\t/// the derived view holding pre-hotload tips.\u003C/summary\u003E\r\n\t[ConCmd( \u0022fg_tips_rebuild\u0022 )]\r\n\tpublic static void RebuildCommand()\r\n\t{\r\n\t\tNoteAssetsChanged();\r\n\t\tvar view = Current();\r\n\t\tvar counts = new Dictionary\u003Cstring, int\u003E( StringComparer.Ordinal );\r\n\t\tforeach ( var kv in view.SourceById )\r\n\t\t\tcounts[kv.Value] = counts.TryGetValue( kv.Value, out var n ) ? n \u002B 1 : 1;\r\n\r\n\t\tvar parts = new List\u003Cstring\u003E();\r\n\t\tforeach ( var kv in counts )\r\n\t\t\tparts.Add( $\u0022{kv.Key}={kv.Value}\u0022 );\r\n\t\tparts.Sort( StringComparer.Ordinal );\r\n\r\n\t\tLog.Info( $\u0022fg_tips: catalog rebuilt, {view.Tips.Count} tip(s) [{( parts.Count \u003E 0 ? string.Join( \u0022 \u0022, parts ) : \u0022empty\u0022 )}].\u0022 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EClear the code catalog and drafts so \u003Csee cref=\u0022Active\u0022/\u003E falls back to any authored assets,\r\n\t/// or to the shipped \u003Csee cref=\u0022Example\u0022/\u003E when there are none (mostly for tests / demos).\u003C/summary\u003E\r\n\tpublic static void Reset()\r\n\t{\r\n\t\t_code = Array.Empty\u003CTipDefinition\u003E();\r\n\t\t_drafts.Clear();\r\n\t\t_draftRevision\u002B\u002B;\r\n\t\tInvalidate();\r\n\t}\r\n\r\n\tprivate static void Invalidate() =\u003E _view = null;\r\n\r\n\t/// \u003Csummary\u003EThe current derived view, rebuilt when any source has moved since it was made.\u003C/summary\u003E\r\n\tprivate static TipCatalogView Current()\r\n\t{\r\n\t\tvar view = _view;\r\n\t\tif ( view is not null \u0026\u0026 _stamp.Matches( _code, _draftRevision, _assetRevision ) )\r\n\t\t\treturn view;\r\n\r\n\t\t_stamp = new TipCatalogStamp( _code, _draftRevision, _assetRevision );\r\n\t\treturn _view = TipCatalogMerge.Merge( _code, AssetDefinitions(), _drafts.Values, BuildExample() );\r\n\t}\r\n\r\n\tprivate static IEnumerable\u003CTipDefinition\u003E AssetDefinitions()\r\n\t{\r\n\t\tforeach ( var res in LoadAssets() )\r\n\t\t{\r\n\t\t\tvar def = res?.ToDefinition();\r\n\t\t\tif ( def is not null )\r\n\t\t\t\tyield return def;\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EWhich source a tip id resolved from (\u0022code\u0022 / \u0022asset\u0022 / \u0022draft\u0022 / \u0022example\u0022), or \u0022unknown\u0022.\r\n\t/// Used by the \u003Cc\u003Efg_tips_list\u003C/c\u003E console command and the Tips Studio\u0027s tip list, where the label is how\r\n\t/// a tip left over from another scene gives itself away.\u003C/summary\u003E\r\n\tpublic static string SourceOf( string id )\r\n\t{\r\n\t\tvar view = Current();\r\n\t\treturn !string.IsNullOrEmpty( id ) \u0026\u0026 view.SourceById.TryGetValue( id, out var src ) ? src : \u0022unknown\u0022;\r\n\t}\r\n\r\n\tprivate static IEnumerable\u003CTipResource\u003E LoadAssets()\r\n\t{\r\n\t\t// ResourceLibrary is only meaningful inside a running game/editor; guard so a headless or unit\r\n\t\t// context (no resource system) merges cleanly instead of throwing.\r\n\t\ttry\r\n\t\t{\r\n\t\t\treturn ResourceLibrary.GetAll\u003CTipResource\u003E();\r\n\t\t}\r\n\t\tcatch\r\n\t\t{\r\n\t\t\treturn Array.Empty\u003CTipResource\u003E();\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// A minimal, GENERIC illustration, enough to show every mechanic (a completed-by-behaviour spine,\r\n\t/// a prerequisite chain, a contextual interrupt via \u003Csee cref=\u0022TipDefinition.Trigger\u0022/\u003E, and a\r\n\t/// timed \u0022glance\u0022 beat via \u003Csee cref=\u0022TipDefinition.MaxShowSeconds\u0022/\u003E). It is meant to be REPLACED:\r\n\t/// register your own game\u0027s tips with \u003Csee cref=\u0022Register(IReadOnlyList{TipDefinition})\u0022/\u003E.\r\n\t///\r\n\t/// A computed property, not a \u003Cc\u003Estatic readonly\u003C/c\u003E field: a field\u0027s initializer runs once, so after a\r\n\t/// code hotload the old list survives and an edit to these tips is invisible until the editor restarts.\r\n\t/// A property is a method, and methods come back fresh from a hotload.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003CTipDefinition\u003E Example =\u003E BuildExample();\r\n\r\n\tprivate static IReadOnlyList\u003CTipDefinition\u003E BuildExample() =\u003E new List\u003CTipDefinition\u003E\r\n\t{\r\n\t\t// A calm spine beat that retires when the player performs its action. Note the mixed input\r\n\t\t// markup: *asterisks* render a keyboard keycap, \u0060backticks\u0060 render a gamepad button chip, so one\r\n\t\t// line can prompt both control schemes.\r\n\t\tnew()\r\n\t\t{\r\n\t\t\tId = \u0022move\u0022, Icon = \u0022\uD83E\uDDED\u0022, Priority = 100,\r\n\t\t\tText = \u0022Move with *W* *A* *S* *D* or the \u0060Left Stick\u0060. Hold *Shift* / \u0060LB\u0060 to sprint.\u0022,\r\n\t\t\tCompleteWhen = static c =\u003E c.EverMoved,\r\n\t\t},\r\n\t\t// A second beat gated behind the first (prerequisite chain).\r\n\t\t// The same beat with pad-specific wording: on a controller the display shows TextPad instead of\r\n\t\t// Text (build plan point 2), so the prompt reads the button the player actually has.\r\n\t\tnew()\r\n\t\t{\r\n\t\t\tId = \u0022interact\u0022, Icon = \u0022\uD83D\uDCAC\u0022, Priority = 90, PrerequisiteTipIds = new[] { \u0022move\u0022 },\r\n\t\t\tText = \u0022Walk up to someone and press *E* to interact.\u0022,\r\n\t\t\tTextPad = \u0022Walk up to someone and press \u0060X\u0060 to interact.\u0022,\r\n\t\t\tCompleteWhen = static c =\u003E c.EverTalked,\r\n\t\t},\r\n\t\t// A \u0022just glance\u0022 beat with no behavioural signal, it times out on its own.\r\n\t\tnew()\r\n\t\t{\r\n\t\t\tId = \u0022look\u0022, Icon = \u0022\uD83D\uDDFA\uFE0F\u0022, Priority = 80, PrerequisiteTipIds = new[] { \u0022interact\u0022 },\r\n\t\t\tText = \u0022Look around with the mouse to get your bearings.\u0022,\r\n\t\t\tMaxShowSeconds = 6f,\r\n\t\t},\r\n\t\t// A contextual interrupt: it outranks the calm spine while a fight is live.\r\n\t\tnew()\r\n\t\t{\r\n\t\t\tId = \u0022combat\u0022, Icon = \u0022\u2757\u0022, Priority = 130,\r\n\t\t\tText = \u0022Something\u0027s hostile! Attack with *LMB*, and hold *B* to block.\u0022,\r\n\t\t\tTrigger = static c =\u003E c.EverAggroed \u0026\u0026 c.InCombat,\r\n\t\t\tCompleteWhen = static c =\u003E !c.InCombat,\r\n\t\t},\r\n\t};\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/Demo/TipsDemoPawn.cs","FileName":"TipsDemoPawn.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System.Linq;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The demo scene\u0027s stand-in player: a citizen that slides along the ground on the movement stick (or\r\n/// W/A/S/D) and hops on the jump action. Deliberately the simplest thing that can be coached: plain\r\n/// transform movement, one hand-integrated hop, no rigidbody, no collider, no controller. It exists so\r\n/// the tips in \u003Cc\u003EAssets/demo/\u003C/c\u003E have real actions to retire on.\r\n///\r\n/// THE LOOK. The scene authors this object with a plain box renderer. At boot the pawn switches that off\r\n/// and builds a dressed stock citizen in its place (\u003Csee cref=\u0022TipsDemoCitizen\u0022/\u003E), so the scene file\r\n/// stays as authored and the editor viewport still shows the simple block when nothing is playing. The\r\n/// movement, the play radius and the hop are untouched by the swap: only the visual changed.\r\n///\r\n/// Not part of the kit\u0027s runtime surface: delete \u003Cc\u003ECode/Demo\u003C/c\u003E and \u003Cc\u003EAssets/demo\u003C/c\u003E when you drop\r\n/// the kit into your own project, and coach your own player instead.\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Tips Demo Pawn\u0022 )]\r\n[Category( \u0022Field Guide Tips\u0022 )]\r\n[Icon( \u0022smart_toy\u0022 )]\r\npublic sealed class TipsDemoPawn : Component\r\n{\r\n\t/// \u003Csummary\u003EGround speed in world units per second.\u003C/summary\u003E\r\n\t[Property] public float MoveSpeed { get; set; } = 220f;\r\n\r\n\t/// \u003Csummary\u003EUpward speed of one hop, in world units per second.\u003C/summary\u003E\r\n\t[Property] public float JumpSpeed { get; set; } = 260f;\r\n\r\n\t/// \u003Csummary\u003EDownward acceleration applied to a hop, in world units per second squared.\u003C/summary\u003E\r\n\t[Property] public float Gravity { get; set; } = 900f;\r\n\r\n\t/// \u003Csummary\u003EHow far from its start the citizen may wander. The demo camera is fixed, so this is what\r\n\t/// keeps the citizen in frame, and the scene\u0027s camera is framed to contain exactly this disc. The\r\n\t/// marker sits 205 units from the start, so 220 lets the citizen walk onto it and a little past\r\n\t/// without opening up a corner of the yard that the camera would then have to cover for nothing.\u003C/summary\u003E\r\n\t[Property] public float PlayRadius { get; set; } = 220f;\r\n\r\n\t/// \u003Csummary\u003EThe Input.config action that hops. Bound to Space on a keyboard and A on a pad in the\r\n\t/// s\u0026amp;box default config, which is what the demo tips prompt.\u003C/summary\u003E\r\n\t[Property] public string JumpAction { get; set; } = \u0022Jump\u0022;\r\n\r\n\t/// \u003Csummary\u003EYaw the demo camera looks along, so pushing forward moves the citizen away from the camera\r\n\t/// instead of sideways. Change it with the camera.\u003C/summary\u003E\r\n\t[Property] public float CameraYaw { get; set; } = 45f;\r\n\r\n\t/// \u003Csummary\u003ELocal Z of the citizen visual. The pawn object sits half a block above the ground because\r\n\t/// the authored box is centred on it, and the citizen\u0027s origin is at its feet, so the visual drops by\r\n\t/// that half height to stand on the floor instead of hovering.\u003C/summary\u003E\r\n\t[Property] public float VisualZOffset { get; set; } = -25f;\r\n\r\n\t/// \u003Csummary\u003EHow briskly the citizen turns to face where it is going, in turns per second-ish. High\r\n\t/// enough to read as responsive, low enough that a flick of the stick does not snap it.\u003C/summary\u003E\r\n\t[Property] public float TurnSpeed { get; set; } = 12f;\r\n\r\n\tprivate Vector3 _start;\r\n\tprivate float _height;\r\n\tprivate float _riseSpeed;\r\n\tprivate SkinnedModelRenderer _visual;\r\n\tprivate Rotation _facing;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_start = WorldPosition;\r\n\r\n\t\t// Resting yaw looks back down the camera\u0027s line, so the citizen greets the player instead of\r\n\t\t// showing its back on the first frame.\r\n\t\t_facing = Rotation.FromYaw( CameraYaw \u002B 180f );\r\n\r\n\t\tHideAuthoredBlock();\r\n\r\n\t\t_visual = TipsDemoCitizen.Build( GameObject, VisualZOffset );\r\n\t\tif ( _visual.IsValid() )\r\n\t\t{\r\n\t\t\t_visual.WorldRotation = _facing;\r\n\t\t\tLog.Info( \u0022[tips] demo pawn: dressed citizen built in code, authored block renderer switched off.\u0022 );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tvar facing = Rotation.FromYaw( CameraYaw );\r\n\t\tvar move = ReadMove();\r\n\t\tvar dir = facing.Forward * move.x \u002B facing.Left * move.y;\r\n\t\tif ( dir.Length \u003E 1f )\r\n\t\t\tdir = dir.Normal;\r\n\r\n\t\tvar flat = ( WorldPosition \u002B dir * MoveSpeed * Time.Delta - _start ).WithZ( 0f );\r\n\t\tif ( flat.Length \u003E PlayRadius )\r\n\t\t\tflat = flat.Normal * PlayRadius;\r\n\r\n\t\tvar hopped = false;\r\n\t\tif ( _height \u003C= 0f \u0026\u0026 _riseSpeed \u003C= 0f \u0026\u0026 Input.Pressed( JumpAction ) )\r\n\t\t{\r\n\t\t\t_riseSpeed = JumpSpeed;\r\n\t\t\thopped = true;\r\n\t\t}\r\n\r\n\t\tif ( _height \u003E 0f || _riseSpeed \u003E 0f )\r\n\t\t{\r\n\t\t\t_riseSpeed -= Gravity * Time.Delta;\r\n\t\t\t_height \u002B= _riseSpeed * Time.Delta;\r\n\t\t\tif ( _height \u003C= 0f )\r\n\t\t\t{\r\n\t\t\t\t_height = 0f;\r\n\t\t\t\t_riseSpeed = 0f;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tvar previous = WorldPosition;\r\n\t\tWorldPosition = _start \u002B flat \u002B Vector3.Up * _height;\r\n\r\n\t\tDriveVisual( previous, dir, hopped );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Movement as a forward/left pair. \u003Cc\u003EInput.AnalogMove\u003C/c\u003E carries the movement stick and, in a\r\n\t/// project whose Input.config binds the standard movement actions, the keyboard too. The raw W/A/S/D\r\n\t/// fallback keeps the demo drivable in a project that binds movement under other names, which is the\r\n\t/// same reason the movement tip completes on either the stick or those keys.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static Vector3 ReadMove()\r\n\t{\r\n\t\tvar move = Input.AnalogMove;\r\n\t\tif ( move.Length \u003E 0.01f )\r\n\t\t\treturn move;\r\n\r\n\t\tvar forward = ( Input.Keyboard.Down( \u0022w\u0022 ) ? 1f : 0f ) - ( Input.Keyboard.Down( \u0022s\u0022 ) ? 1f : 0f );\r\n\t\tvar left = ( Input.Keyboard.Down( \u0022a\u0022 ) ? 1f : 0f ) - ( Input.Keyboard.Down( \u0022d\u0022 ) ? 1f : 0f );\r\n\t\treturn new Vector3( forward, left, 0f );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Turn the citizen to face its travel and hand the animgraph a frame of locomotion. The legs read the\r\n\t/// distance actually covered rather than the stick, so at the play radius the clamp reads as standing\r\n\t/// still instead of running on the spot.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void DriveVisual( Vector3 previous, Vector3 wishDirection, bool hopped )\r\n\t{\r\n\t\tif ( !_visual.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tvar travelled = ( WorldPosition - previous ).WithZ( 0f );\r\n\t\tvar velocity = ( Time.Delta \u003E 0f ? travelled / Time.Delta : Vector3.Zero ).WithZ( _riseSpeed );\r\n\r\n\t\tif ( travelled.Length \u003E 0.01f )\r\n\t\t{\r\n\t\t\tvar target = Rotation.LookAt( travelled.Normal, Vector3.Up );\r\n\t\t\t_facing = Rotation.Slerp( _facing, target, ( Time.Delta * TurnSpeed ).Clamp( 0f, 1f ) );\r\n\t\t}\r\n\r\n\t\t_visual.WorldRotation = _facing;\r\n\r\n\t\tvar grounded = _height \u003C= 0f \u0026\u0026 _riseSpeed \u003C= 0f;\r\n\t\tTipsDemoCitizen.Drive( _visual, velocity, wishDirection * MoveSpeed, grounded );\r\n\r\n\t\tif ( hopped )\r\n\t\t\tTipsDemoCitizen.TriggerJump( _visual );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Switch off the box renderer the scene authors on this object, so the citizen stands alone. Disabling\r\n\t/// the component at runtime leaves the scene file untouched: reopen it in the editor and the simple\r\n\t/// authored block is still what you see. The skinned renderer is skipped by type because it derives\r\n\t/// from \u003Cc\u003EModelRenderer\u003C/c\u003E too.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void HideAuthoredBlock()\r\n\t{\r\n\t\tvar authored = Components.GetAll\u003CModelRenderer\u003E( FindMode.EverythingInSelf )\r\n\t\t\t.Where( r =\u003E r is not SkinnedModelRenderer )\r\n\t\t\t.ToArray();\r\n\r\n\t\tforeach ( var renderer in authored )\r\n\t\t\trenderer.Enabled = false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EPut the citizen back where it started (the demo\u0027s replay key).\u003C/summary\u003E\r\n\tpublic void ResetPawn()\r\n\t{\r\n\t\t_height = 0f;\r\n\t\t_riseSpeed = 0f;\r\n\t\tWorldPosition = _start;\r\n\r\n\t\t_facing = Rotation.FromYaw( CameraYaw \u002B 180f );\r\n\t\tif ( _visual.IsValid() )\r\n\t\t\t_visual.WorldRotation = _facing;\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/TipCatalogMerge.cs","FileName":"TipCatalogMerge.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The merged catalog as one value: the ordered tips and the label saying where each id came from, built\r\n/// together so a reader can never pair a list from one build with labels from another.\r\n/// \u003C/summary\u003E\r\npublic sealed class TipCatalogView\r\n{\r\n\t/// \u003Csummary\u003EThe deduped, precedence-ordered tips.\u003C/summary\u003E\r\n\tpublic IReadOnlyList\u003CTipDefinition\u003E Tips { get; init; } = new List\u003CTipDefinition\u003E();\r\n\r\n\t/// \u003Csummary\u003ETip id to source label (\u0022code\u0022 / \u0022asset\u0022 / \u0022draft\u0022 / \u0022example\u0022).\u003C/summary\u003E\r\n\tpublic IReadOnlyDictionary\u003Cstring, string\u003E SourceById { get; init; } = new Dictionary\u003Cstring, string\u003E();\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The pure merge rule behind \u003Csee cref=\u0022TipsCatalog.Active\u0022/\u003E: which source wins an id collision, what order\r\n/// the survivors come out in, and when the shipped example stands in. Lifted out of the catalog so it has no\r\n/// \u003Cc\u003ESandbox\u003C/c\u003E reference and the harness can assert precedence and dedupe without a running engine, which\r\n/// is the half of the catalog that a typo actually breaks.\r\n/// \u003C/summary\u003E\r\npublic static class TipCatalogMerge\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Merge the three sources into one view, highest precedence first: CODE, then ASSETS, then DRAFTS. The\r\n\t/// first tip seen for an id wins and later ones are dropped, so a draft never shadows the real tip it is\r\n\t/// a draft of. When all three come back empty, \u003Cparamref name=\u0022fallback\u0022/\u003E is used and labelled\r\n\t/// \u0022example\u0022. Null sources are treated as empty; a null tip, or one with a blank id, is skipped.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static TipCatalogView Merge(\r\n\t\tIEnumerable\u003CTipDefinition\u003E code,\r\n\t\tIEnumerable\u003CTipDefinition\u003E assets,\r\n\t\tIEnumerable\u003CTipDefinition\u003E drafts,\r\n\t\tIEnumerable\u003CTipDefinition\u003E fallback )\r\n\t{\r\n\t\tvar order = new List\u003CTipDefinition\u003E();\r\n\t\tvar sources = new Dictionary\u003Cstring, string\u003E( System.StringComparer.Ordinal );\r\n\r\n\t\tAdd( code, \u0022code\u0022, order, sources );\r\n\t\tAdd( assets, \u0022asset\u0022, order, sources );\r\n\t\tAdd( drafts, \u0022draft\u0022, order, sources );\r\n\r\n\t\tif ( order.Count == 0 )\r\n\t\t\tAdd( fallback, \u0022example\u0022, order, sources );\r\n\r\n\t\treturn new TipCatalogView { Tips = order, SourceById = sources };\r\n\t}\r\n\r\n\t// The source map IS the dedupe guard, and it is filled in the same pass as the list it guards. Guarding\r\n\t// inserts to one collection by querying a different, longer-lived one is how these two drift apart.\r\n\tprivate static void Add( IEnumerable\u003CTipDefinition\u003E source, string label,\r\n\t\tList\u003CTipDefinition\u003E order, Dictionary\u003Cstring, string\u003E sources )\r\n\t{\r\n\t\tif ( source is null )\r\n\t\t\treturn;\r\n\r\n\t\tforeach ( var def in source )\r\n\t\t{\r\n\t\t\tif ( def is null || string.IsNullOrEmpty( def.Id ) )\r\n\t\t\t\tcontinue;\r\n\t\t\tif ( sources.ContainsKey( def.Id ) )\r\n\t\t\t\tcontinue; // a higher-precedence source already claimed this id\r\n\t\t\torder.Add( def );\r\n\t\t\tsources[def.Id] = label;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// What a built catalog view was built FROM: the code list it saw, and the draft / asset revision numbers at\r\n/// the time. A read compares the stamp against the live sources; anything that moved means the view is stale\r\n/// and gets rebuilt. Comparing is three field reads with no allocation, which is what lets the coach ask for\r\n/// the catalog several times a frame without a rescan.\r\n///\r\n/// The code source is compared BY REFERENCE, not by content: registering is a whole-list swap, so a new list\r\n/// is a new catalog, and a caller mutating a list it already registered is expected to say so\r\n/// (\u003Csee cref=\u0022TipsCatalog.Rebuild\u0022/\u003E) the same way it always was.\r\n/// \u003C/summary\u003E\r\npublic readonly struct TipCatalogStamp\r\n{\r\n\t/// \u003Csummary\u003EThe code catalog this view merged.\u003C/summary\u003E\r\n\tpublic object Code { get; }\r\n\r\n\t/// \u003Csummary\u003EThe draft revision this view merged.\u003C/summary\u003E\r\n\tpublic int DraftRevision { get; }\r\n\r\n\t/// \u003Csummary\u003EThe asset revision this view merged.\u003C/summary\u003E\r\n\tpublic int AssetRevision { get; }\r\n\r\n\tpublic TipCatalogStamp( object code, int draftRevision, int assetRevision )\r\n\t{\r\n\t\tCode = code;\r\n\t\tDraftRevision = draftRevision;\r\n\t\tAssetRevision = assetRevision;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when nothing has moved since this view was built.\u003C/summary\u003E\r\n\tpublic bool Matches( object code, int draftRevision, int assetRevision )\r\n\t\t=\u003E ReferenceEquals( Code, code ) \u0026\u0026 DraftRevision == draftRevision \u0026\u0026 AssetRevision == assetRevision;\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/TipTriggerEval.cs","FileName":"TipTriggerEval.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The environment a \u003Csee cref=\u0022TipTrigger\u0022/\u003E is evaluated against: the four world reads plus the active\r\n/// tip\u0027s elapsed visible time. \u003Csee cref=\u0022TipsCoach\u0022/\u003E fills these from \u003Csee cref=\u0022Sandbox.Input\u0022/\u003E, the\r\n/// pushed \u003Csee cref=\u0022TipContext\u0022/\u003E and its own timer; a headless harness fills them with fakes. Keeping\r\n/// the reads behind delegates is what lets the trigger truth table be unit-tested without the engine.\r\n/// \u003C/summary\u003E\r\npublic sealed class TipTriggerEnv\r\n{\r\n\t/// \u003Csummary\u003EWas the named Input.config action pressed this frame? (Input.Pressed)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, bool\u003E ActionPressed { get; init; } = static _ =\u003E false;\r\n\r\n\t/// \u003Csummary\u003EWas the named raw key / mouse button pressed this frame? (Input.Keyboard.Pressed)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, bool\u003E KeyPressed { get; init; } = static _ =\u003E false;\r\n\r\n\t/// \u003Csummary\u003EHas the named string signal been latched this session? (Signal / Ever kinds)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, bool\u003E SignalLatched { get; init; } = static _ =\u003E false;\r\n\r\n\t/// \u003Csummary\u003EIs the named custom context flag true this frame? (TipContext.Flag)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, bool\u003E Flag { get; init; } = static _ =\u003E false;\r\n\r\n\t/// \u003Csummary\u003EThe named custom context number this frame. (TipContext.Number)\u003C/summary\u003E\r\n\tpublic Func\u003Cstring, float\u003E Number { get; init; } = static _ =\u003E 0f;\r\n\r\n\t/// \u003Csummary\u003ESeconds the active tip has been visible, for the Timer kind.\u003C/summary\u003E\r\n\tpublic float Elapsed { get; init; }\r\n\r\n\t/// \u003Csummary\u003EThe current magnitude (0..1-ish) of the given analog stick, for the AnalogAxis kind. (Input.AnalogMove / Input.AnalogLook length.)\u003C/summary\u003E\r\n\tpublic Func\u003CTipTriggerAnalogSource, float\u003E AnalogMagnitude { get; init; } = static _ =\u003E 0f;\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// The pure, engine-free evaluation core for a declarative \u003Csee cref=\u0022TipTrigger\u0022/\u003E: the per-kind rule and\r\n/// the AnyOf / AllOf recursion, with every world read behind a \u003Csee cref=\u0022TipTriggerEnv\u0022/\u003E delegate. This\r\n/// holds the whole completion/relevance truth table, so it can be exercised headlessly (the coach passes\r\n/// real Input / context reads; a harness passes fakes) and stays identical between the two.\r\n/// \u003C/summary\u003E\r\npublic static class TipTriggerEval\r\n{\r\n\t/// \u003Csummary\u003EEvaluate a trigger against the given environment. Null trigger evaluates false.\u003C/summary\u003E\r\n\tpublic static bool Evaluate( TipTrigger t, TipTriggerEnv env )\r\n\t{\r\n\t\tif ( t is null || env is null )\r\n\t\t\treturn false;\r\n\r\n\t\tswitch ( t.Kind )\r\n\t\t{\r\n\t\t\tcase TipTriggerKind.Always:\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase TipTriggerKind.InputAction:\r\n\t\t\t\tforeach ( var a in t.Actions )\r\n\t\t\t\t\tif ( !string.IsNullOrEmpty( a ) \u0026\u0026 env.ActionPressed( a ) )\r\n\t\t\t\t\t\treturn true;\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tcase TipTriggerKind.Key:\r\n\t\t\t\tforeach ( var k in t.Keys )\r\n\t\t\t\t\tif ( !string.IsNullOrEmpty( k ) \u0026\u0026 env.KeyPressed( k ) )\r\n\t\t\t\t\t\treturn true;\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tcase TipTriggerKind.Signal:\r\n\t\t\tcase TipTriggerKind.Ever:\r\n\t\t\t\treturn !string.IsNullOrEmpty( t.Key ) \u0026\u0026 env.SignalLatched( t.Key );\r\n\r\n\t\t\tcase TipTriggerKind.Flag:\r\n\t\t\t\treturn !string.IsNullOrEmpty( t.Key ) \u0026\u0026 env.Flag( t.Key );\r\n\r\n\t\t\tcase TipTriggerKind.AtLeast:\r\n\t\t\t\treturn !string.IsNullOrEmpty( t.Key ) \u0026\u0026 env.Number( t.Key ) \u003E= t.Threshold;\r\n\r\n\t\t\tcase TipTriggerKind.Timer:\r\n\t\t\t\treturn env.Elapsed \u003E= t.Seconds;\r\n\r\n\t\t\tcase TipTriggerKind.AnalogAxis:\r\n\t\t\t\treturn env.AnalogMagnitude( t.AnalogSource ) \u003E= t.Threshold;\r\n\r\n\t\t\tcase TipTriggerKind.AnyOf:\r\n\t\t\t\tforeach ( var c in t.Children )\r\n\t\t\t\t\tif ( Evaluate( c, env ) )\r\n\t\t\t\t\t\treturn true;\r\n\t\t\t\treturn false;\r\n\r\n\t\t\tcase TipTriggerKind.AllOf:\r\n\t\t\t\tif ( t.Children.Length == 0 )\r\n\t\t\t\t\treturn false;\r\n\t\t\t\tforeach ( var c in t.Children )\r\n\t\t\t\t\tif ( !Evaluate( c, env ) )\r\n\t\t\t\t\t\treturn false;\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tdefault:\r\n\t\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"TipTriggerObject.cs","FileName":"TipTriggerObject.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// Drop this on a GameObject (an NPC, a door, a pickup) to retire a tip when the player does something to\r\n/// that object. Pick the tip id and the mode in the inspector; no code for the common cases. So \u0022talk to\r\n/// this NPC\u0022 is: add this component to the NPC, set \u003Csee cref=\u0022TipId\u0022/\u003E, set\r\n/// \u003Csee cref=\u0022CompleteOn\u0022/\u003E = \u003Csee cref=\u0022Mode.Interacted\u0022/\u003E, and call \u003Csee cref=\u0022Interacted\u0022/\u003E (or the\r\n/// static \u003Csee cref=\u0022NotifyInteracted\u0022/\u003E) from wherever your game already knows an interaction happened.\r\n///\r\n/// \u003Csee cref=\u0022Mode.PlayerEntered\u0022/\u003E and \u003Csee cref=\u0022Mode.LookedAt\u0022/\u003E read the \u003Csee cref=\u0022TipsWorld\u0022/\u003E\r\n/// seams and are INERT until those seams are set (they never fire and never throw), so a game that skips\r\n/// the seams still runs. Input, Signal and Timer completion do not use this component at all; they live on\r\n/// the tip itself (declarative triggers or the coach\u0027s input pass).\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Tip Trigger\u0022 )]\r\n[Category( \u0022Field Guide Tips\u0022 )]\r\n[Icon( \u0022ads_click\u0022 )]\r\npublic sealed class TipTriggerObject : Component\r\n{\r\n\tpublic enum Mode\r\n\t{\r\n\t\t/// \u003Csummary\u003EYour interaction code (or the fieldguide.interaction bridge) calls \u003Csee cref=\u0022Interacted\u0022/\u003E.\u003C/summary\u003E\r\n\t\tInteracted,\r\n\r\n\t\t/// \u003Csummary\u003EThe local player is within \u003Csee cref=\u0022Radius\u0022/\u003E of this object (needs \u003Csee cref=\u0022TipsWorld.LocalPlayerPosition\u0022/\u003E).\u003C/summary\u003E\r\n\t\tPlayerEntered,\r\n\r\n\t\t/// \u003Csummary\u003EThe aim ray hits this object within \u003Csee cref=\u0022Radius\u0022/\u003E (needs \u003Csee cref=\u0022TipsWorld.AimRay\u0022/\u003E).\u003C/summary\u003E\r\n\t\tLookedAt,\r\n\r\n\t\t/// \u003Csummary\u003ERaise a named signal instead of completing directly (fans out to every coach\u0027s Signal).\u003C/summary\u003E\r\n\t\tSignal,\r\n\t}\r\n\r\n\t[Property] public string TipId { get; set; } = \u0022\u0022;\r\n\t[Property] public Mode CompleteOn { get; set; } = Mode.Interacted;\r\n\r\n\t/// \u003Csummary\u003ETrigger distance for PlayerEntered, and the aim-ray length for LookedAt, in world units.\u003C/summary\u003E\r\n\t[Property] public float Radius { get; set; } = 128f;\r\n\r\n\t/// \u003Csummary\u003ESignal name raised in \u003Csee cref=\u0022Mode.Signal\u0022/\u003E.\u003C/summary\u003E\r\n\t[Property] public string SignalName { get; set; } = \u0022\u0022;\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( CompleteOn == Mode.PlayerEntered \u0026\u0026 WithinPlayerRadius() )\r\n\t\t\tFire();\r\n\t\telse if ( CompleteOn == Mode.LookedAt \u0026\u0026 AimRayHitsSelf() )\r\n\t\t\tFire();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ECall from your interaction code when this object is interacted with. Inert unless the mode\r\n\t/// is \u003Csee cref=\u0022Mode.Interacted\u0022/\u003E.\u003C/summary\u003E\r\n\tpublic void Interacted()\r\n\t{\r\n\t\tif ( CompleteOn == Mode.Interacted )\r\n\t\t\tFire();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Convenience for an interaction bridge: fire the \u003Csee cref=\u0022Interacted\u0022/\u003E trigger on a target\r\n\t/// GameObject if it carries a \u003Csee cref=\u0022TipTriggerObject\u0022/\u003E. Null-safe on the target and on a missing\r\n\t/// component, so a bridge can call it for every interacted object without guarding.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void NotifyInteracted( GameObject target )\r\n\t\t=\u003E target?.Components.Get\u003CTipTriggerObject\u003E( FindMode.EnabledInSelfAndDescendants )?.Interacted();\r\n\r\n\tprivate void Fire()\r\n\t{\r\n\t\tif ( CompleteOn == Mode.Signal )\r\n\t\t{\r\n\t\t\tif ( string.IsNullOrEmpty( SignalName ) )\r\n\t\t\t\treturn;\r\n\t\t\tforeach ( var coach in Scene.GetAllComponents\u003CTipsCoach\u003E() )\r\n\t\t\t\tcoach.Signal( SignalName );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tTipsCoach.Complete( TipId );\r\n\t}\r\n\r\n\tprivate bool WithinPlayerRadius()\r\n\t{\r\n\t\tvar seam = TipsWorld.LocalPlayerPosition; // fail-inert: null seam disables PlayerEntered\r\n\t\tif ( seam is null )\r\n\t\t\treturn false;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\treturn WorldPosition.Distance( seam() ) \u003C= Radius;\r\n\t\t}\r\n\t\tcatch\r\n\t\t{\r\n\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate bool AimRayHitsSelf()\r\n\t{\r\n\t\tvar seam = TipsWorld.AimRay; // fail-inert: null seam disables LookedAt\r\n\t\tif ( seam is null )\r\n\t\t\treturn false;\r\n\r\n\t\ttry\r\n\t\t{\r\n\t\t\tvar ray = seam();\r\n\t\t\tvar tr = Scene.Trace.Ray( ray.Position, ray.Position \u002B ray.Forward * Radius ).Run();\r\n\t\t\treturn tr.Hit \u0026\u0026 tr.GameObject.IsValid()\r\n\t\t\t\t\u0026\u0026 ( tr.GameObject == GameObject || GameObject.IsDescendant( tr.GameObject ) );\r\n\t\t}\r\n\t\tcatch\r\n\t\t{\r\n\t\t\treturn false;\r\n\t\t}\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/Demo/TipsDemoCitizen.cs","FileName":"TipsDemoCitizen.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System;\r\nusing Sandbox;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// Builds and drives the demo pawn\u0027s look: a dressed stock citizen, spawned in code as a child of the pawn\r\n/// object. Everything here ships with the engine (the citizen model, its clothing, its animgraph), so the\r\n/// demo still carries zero art of its own.\r\n///\r\n/// WHY IN CODE. The pawn object is authored in \u003Cc\u003Etips_demo.scene\u003C/c\u003E with a plain box renderer, and the\r\n/// scene stays exactly that on disk. \u003Csee cref=\u0022TipsDemoPawn\u0022/\u003E switches the box off at boot and builds\r\n/// this instead, so the swap costs no scene churn and the editor viewport still shows the simple authored\r\n/// block when nothing is playing.\r\n///\r\n/// WHY NOT CitizenAnimationHelper. The engine ships a helper component that wraps these same animgraph\r\n/// parameters, but it lives in a namespace a Field Guide kit is not allowed to reference (the isolation\r\n/// lint severs it). The parameter names below are the ones that helper sets, driven directly on the\r\n/// renderer, which is also what the placement and vehicle physics kits do.\r\n///\r\n/// Not part of the kit\u0027s runtime surface: delete \u003Cc\u003ECode/Demo\u003C/c\u003E and \u003Cc\u003EAssets/demo\u003C/c\u003E when you drop the\r\n/// kit into your own project.\r\n/// \u003C/summary\u003E\r\ninternal static class TipsDemoCitizen\r\n{\r\n\tprivate const string ModelPath = \u0022models/citizen/citizen.vmdl\u0022;\r\n\r\n\t/// \u003Csummary\u003EA plain outfit from the shipped citizen clothing resources, the same two items the\r\n\t/// placement kit\u0027s demo wears. Each is null-checked, so a missing asset degrades to a barer citizen\r\n\t/// rather than a broken spawn.\u003C/summary\u003E\r\n\tprivate static readonly string[] Outfit =\r\n\t{\r\n\t\t\u0022models/citizen_clothes/shirt/Jumpsuit/blue_jumpsuit.clothing\u0022,\r\n\t\t\u0022models/citizen_clothes/shoes/Trainers/trainers.clothing\u0022,\r\n\t};\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Spawn the citizen as a child of the pawn. \u003Cparamref name=\u0022footOffset\u0022/\u003E is its local Z: the pawn\r\n\t/// object sits half a block above the ground because the authored box is centred on it, and the citizen\r\n\t/// model\u0027s origin is at its feet, so the visual drops by that half height to stand on the floor.\r\n\t/// Returns null if the model did not load, which leaves the caller free to keep the block.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static SkinnedModelRenderer Build( GameObject pawn, float footOffset )\r\n\t{\r\n\t\tvar go = pawn.Scene.CreateObject();\r\n\t\tgo.Name = \u0022Demo Citizen Visual\u0022;\r\n\t\tgo.SetParent( pawn, false );\r\n\t\tgo.LocalPosition = Vector3.Up * footOffset;\r\n\r\n\t\tvar model = Model.Load( ModelPath );\r\n\t\tif ( model is null || model.IsError )\r\n\t\t{\r\n\t\t\tLog.Warning( $\u0022[tips] demo citizen model \u0027{ModelPath}\u0027 did not load; keeping the authored block.\u0022 );\r\n\t\t\tgo.Destroy();\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tvar renderer = go.Components.Create\u003CSkinnedModelRenderer\u003E();\r\n\t\trenderer.Model = model;\r\n\t\tDress( renderer );\r\n\r\n\t\t// Grounded with no move input is the citizen animgraph\u0027s rest state: a standing idle that breathes\r\n\t\t// and shifts weight, which is what the demo wants between tips.\r\n\t\trenderer.Set( \u0022b_grounded\u0022, true );\r\n\r\n\t\treturn renderer;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Push a frame of locomotion at the animgraph. \u003Cparamref name=\u0022velocity\u0022/\u003E is what the pawn actually\r\n\t/// travelled, \u003Cparamref name=\u0022wishVelocity\u0022/\u003E is what the stick asked for; the graph uses the first for\r\n\t/// the legs and the second for arm swing in the air, which is why both go across.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static void Drive( SkinnedModelRenderer renderer, Vector3 velocity, Vector3 wishVelocity, bool grounded )\r\n\t{\r\n\t\tif ( !renderer.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\trenderer.Set( \u0022b_grounded\u0022, grounded );\r\n\t\tSetMotion( renderer, \u0022move\u0022, velocity );\r\n\t\tSetMotion( renderer, \u0022wish\u0022, wishVelocity );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EFire the animgraph\u0027s one-shot hop. It self-clears, so it is set and forgotten.\u003C/summary\u003E\r\n\tpublic static void TriggerJump( SkinnedModelRenderer renderer )\r\n\t{\r\n\t\tif ( renderer.IsValid() )\r\n\t\t\trenderer.Set( \u0022b_jump\u0022, true );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003EThe six parameters the citizen animgraph reads per motion channel, resolved against the\r\n\t/// renderer\u0027s own facing so a sideways walk plays the strafe blend rather than a forward one.\u003C/summary\u003E\r\n\tprivate static void SetMotion( SkinnedModelRenderer renderer, string channel, Vector3 velocity )\r\n\t{\r\n\t\tvar rotation = renderer.WorldRotation;\r\n\t\tvar forward = rotation.Forward.Dot( velocity );\r\n\t\tvar sideward = rotation.Right.Dot( velocity );\r\n\t\tvar angle = MathF.Atan2( sideward, forward ).RadianToDegree().NormalizeDegrees();\r\n\r\n\t\trenderer.Set( $\u0022{channel}_direction\u0022, angle );\r\n\t\trenderer.Set( $\u0022{channel}_speed\u0022, velocity.Length );\r\n\t\trenderer.Set( $\u0022{channel}_groundspeed\u0022, velocity.WithZ( 0f ).Length );\r\n\t\trenderer.Set( $\u0022{channel}_x\u0022, forward );\r\n\t\trenderer.Set( $\u0022{channel}_y\u0022, sideward );\r\n\t\trenderer.Set( $\u0022{channel}_z\u0022, velocity.z );\r\n\t}\r\n\r\n\tprivate static void Dress( SkinnedModelRenderer renderer )\r\n\t{\r\n\t\tvar outfit = new ClothingContainer();\r\n\t\tvar any = false;\r\n\r\n\t\tforeach ( var path in Outfit )\r\n\t\t{\r\n\t\t\tvar item = ResourceLibrary.Get\u003CClothing\u003E( path );\r\n\t\t\tif ( item is null )\r\n\t\t\t{\r\n\t\t\t\tLog.Warning( $\u0022[tips] demo clothing \u0027{path}\u0027 did not resolve, skipping that slot.\u0022 );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\toutfit.Add( item );\r\n\t\t\tany = true;\r\n\t\t}\r\n\r\n\t\tif ( any )\r\n\t\t\toutfit.Apply( renderer );\r\n\t}\r\n}\r\n"},{"Ident":"fieldguide.tips","Path":"Code/Studio/TipStudioText.cs","FileName":"TipStudioText.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":337872,"Code":"using System.Collections.Generic;\r\n\r\nnamespace FieldGuide.Tips;\r\n\r\n/// \u003Csummary\u003E\r\n/// The authoring checks the Tips Studio runs on a draft while you type: the things that produce a card that\r\n/// looks broken, or a tip that can never retire, and that are cheap to catch before the file is written.\r\n/// Pure (no \u003Cc\u003ESandbox\u003C/c\u003E reference, no engine state), so the harness asserts the same rules the panel shows.\r\n///\r\n/// Every message is a NOTE, never a block. The Studio will happily bake a tip with warnings on it, because\r\n/// several of them are legitimate on purpose (an empty \u003Cc\u003EAllOf\u003C/c\u003E is the documented \u0022a world trigger\r\n/// retires this one\u0022 shape).\r\n/// \u003C/summary\u003E\r\npublic static class TipStudioText\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// How long a single unbroken run of prose can get before the card is at risk of the grey-block quirk:\r\n\t/// the style engine rasterizes a text run that overflows one card line as a solid filled rectangle\r\n\t/// instead of wrapped glyphs. Chips break a line into separate runs, which is why a tip full of keycaps\r\n\t/// stays safe while one long sentence does not. Roughly one line at the card\u0027s 500px width; deliberately\r\n\t/// a round number rather than a measured one, because the real threshold moves with the wording.\r\n\t/// \u003C/summary\u003E\r\n\tpublic const int MaxRunLength = 50;\r\n\r\n\t/// \u003Csummary\u003EThe length of the longest PLAIN run in a tip line. Chips (\u003Cc\u003E*keycap*\u003C/c\u003E,\r\n\t/// \u003Cc\u003E\u0060padchip\u0060\u003C/c\u003E) are separate runs and never count toward it, which mirrors how the card lays out.\u003C/summary\u003E\r\n\tpublic static int LongestRun( string text )\r\n\t{\r\n\t\tvar longest = 0;\r\n\t\tforeach ( var segment in TipSegment.Parse( text ) )\r\n\t\t{\r\n\t\t\tif ( segment.Kind != TipSegmentKind.Plain )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\tvar length = segment.Text is null ? 0 : segment.Text.Trim().Length;\r\n\t\t\tif ( length \u003E longest )\r\n\t\t\t\tlongest = length;\r\n\t\t}\r\n\r\n\t\treturn longest;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003ETrue when a line carries a run long enough to risk the grey-block quirk.\u003C/summary\u003E\r\n\tpublic static bool RunTooLong( string text ) =\u003E LongestRun( text ) \u003E MaxRunLength;\r\n\r\n\t/// \u003Csummary\u003EThe note for a too-long run, or null when the line is fine.\u003C/summary\u003E\r\n\tpublic static string RunWarning( string text, string label )\r\n\t{\r\n\t\tvar longest = LongestRun( text );\r\n\t\tif ( longest \u003C= MaxRunLength )\r\n\t\t\treturn null;\r\n\r\n\t\treturn $\u0022{label}: one run is {longest} characters. A run longer than a card line can render as a grey \u0022 \u002B\r\n\t\t\t$\u0022block. Break the sentence, or put a key chip in it.\u0022;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Every note for a draft, in the order the panel lists them. An empty list means nothing to flag.\r\n\t/// \u003C/summary\u003E\r\n\tpublic static IReadOnlyList\u003Cstring\u003E Warnings( TipStudioDraft draft )\r\n\t{\r\n\t\tvar notes = new List\u003Cstring\u003E();\r\n\t\tif ( draft is null )\r\n\t\t\treturn notes;\r\n\r\n\t\tif ( string.IsNullOrWhiteSpace( draft.Id ) )\r\n\t\t\tnotes.Add( \u0022No id yet. The catalog keys tips by id, and the file is named after it.\u0022 );\r\n\r\n\t\tif ( string.IsNullOrWhiteSpace( draft.Text ) )\r\n\t\t\tnotes.Add( \u0022No text yet. This is the line the player reads.\u0022 );\r\n\r\n\t\tvar textNote = RunWarning( draft.Text, \u0022Text\u0022 );\r\n\t\tif ( textNote is not null )\r\n\t\t\tnotes.Add( textNote );\r\n\r\n\t\tif ( !string.IsNullOrEmpty( draft.TextPad ) )\r\n\t\t{\r\n\t\t\tvar padNote = RunWarning( draft.TextPad, \u0022Pad text\u0022 );\r\n\t\t\tif ( padNote is not null )\r\n\t\t\t\tnotes.Add( padNote );\r\n\t\t}\r\n\r\n\t\tAddTriggerNotes( notes, draft.Completion, \u0022Completion\u0022, isCompletion: true );\r\n\t\tAddTriggerNotes( notes, draft.Relevance, \u0022Relevance\u0022, isCompletion: false );\r\n\r\n\t\treturn notes;\r\n\t}\r\n\r\n\tprivate static void AddTriggerNotes( List\u003Cstring\u003E notes, TipStudioTrigger trigger, string label, bool isCompletion )\r\n\t{\r\n\t\tif ( trigger is null )\r\n\t\t\treturn;\r\n\r\n\t\tswitch ( trigger.Kind )\r\n\t\t{\r\n\t\t\tcase TipTriggerKind.InputAction when string.IsNullOrWhiteSpace( trigger.Action ):\r\n\t\t\t\tnotes.Add( $\u0022{label} is InputAction with no action picked, so it never fires.\u0022 );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase TipTriggerKind.Key when string.IsNullOrWhiteSpace( trigger.Key ):\r\n\t\t\t\tnotes.Add( $\u0022{label} is Key with no key name, so it never fires.\u0022 );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase TipTriggerKind.Signal or TipTriggerKind.Ever or TipTriggerKind.Flag or TipTriggerKind.AtLeast\r\n\t\t\t\twhen string.IsNullOrWhiteSpace( trigger.Name ):\r\n\t\t\t\tnotes.Add( $\u0022{label} is {TipStudioTrigger.KindName( trigger.Kind )} with no name, so it never fires.\u0022 );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase TipTriggerKind.Timer when trigger.Seconds \u003C= 0f \u0026\u0026 isCompletion:\r\n\t\t\t\tnotes.Add( $\u0022{label} is Timer with 0 seconds, so the tip retires the moment it is readable. Set Seconds.\u0022 );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase TipTriggerKind.AnalogAxis when trigger.Magnitude \u003C= 0f:\r\n\t\t\t\tnotes.Add( $\u0022{label} is AnalogAxis with a magnitude of 0, so a resting stick already fires it.\u0022 );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase TipTriggerKind.AllOf when CountChildren( trigger ) == 0 \u0026\u0026 isCompletion:\r\n\t\t\t\tnotes.Add( $\u0022{label} is an empty AllOf, which never fires. That is the right shape when a \u0022 \u002B\r\n\t\t\t\t\t\u0022TipTriggerObject in the scene retires this tip.\u0022 );\r\n\t\t\t\tbreak;\r\n\r\n\t\t\tcase TipTriggerKind.AnyOf when CountChildren( trigger ) == 0:\r\n\t\t\t\tnotes.Add( $\u0022{label} is an empty AnyOf, so it never fires.\u0022 );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\r\n\t\tif ( trigger.Children is null )\r\n\t\t\treturn;\r\n\r\n\t\tforeach ( var child in trigger.Children )\r\n\t\t\tAddTriggerNotes( notes, child, $\u0022{label} child\u0022, isCompletion );\r\n\t}\r\n\r\n\tprivate static int CountChildren( TipStudioTrigger trigger )\r\n\t\t=\u003E trigger.Children is null ? 0 : trigger.Children.Count;\r\n}\r\n"}]}