using { /Fortnite.com/Devices } using { /Fortnite.com/Playspaces } using { /Verse.org/Simulation } using { /Verse.org/Random } using { /UnrealEngine.com/Temporary/Diagnostics } # ----------------------------------------------------------------------------- # dropper_manager — Indie Island tutorial 03: Rotating resource nodes # # Of all Nodes, only ActiveCount are awake at any moment. An awake node fills # up over time. After RotateSeconds -- or as soon as someone collects it, # whichever comes first -- it goes back to sleep and a random other node wakes # up. That keeps players moving across the map instead of camping one spot. # # Setup (Details panel): # 1. Place a Trigger, Billboard, Beacon, Audio Player and VFX Spawner at each # node, and add one entry per node to Nodes. Wire every field. # 2. ScoreManager: a Score Manager device. Collecting awards the pending # amount as score. # 3. Optional: HideMarkersTrigger / ShowMarkersTrigger. Wire these to the # concert's ShowStartTrigger and ConcertEndTrigger so node beacons stay # hidden during a show (tutorial 01, step 9). # ----------------------------------------------------------------------------- # Text on billboards has to be a message, not a string. # Unique name so it never clashes with helpers in other files. DropperText(Value : string) : message = "{Value}" # One collection point. Pure data, not a device. resource_node := class: @editable CollectorTrigger : trigger_device = trigger_device{} @editable Display : billboard_device = billboard_device{} @editable CollectSound : audio_player_device = audio_player_device{} @editable CollectVFX : vfx_spawner_device = vfx_spawner_device{} @editable Beacon : beacon_device = beacon_device{} @editable VFXDuration : float = 1.5 @editable BaseDropAmount : int = 10 @editable DropInterval : float = 2.0 @editable MaxPending : int = 150 dropper_manager := class(creative_device): @editable ScoreManager : score_manager_device = score_manager_device{} @editable Nodes : []resource_node = array{} # How many nodes are awake at once, and the longest one stays awake. @editable ActiveCount : int = 2 @editable RotateSeconds : float = 30.0 # Optional: hide/show all node beacons from another system (e.g. a concert). @editable HideMarkersTrigger : trigger_device = trigger_device{} @editable ShowMarkersTrigger : trigger_device = trigger_device{} # Texts shown on a node's billboard. Change to fit your island. @editable SleepingText : string = "Not active" @editable AmountPrefix : string = "$ " var NodePending : [int]int = map{} var NodeLevels : [int]int = map{} var ActiveIndices : []int = array{} # Checked INSIDE the rotation loop -- otherwise the loop re-enables beacons # a few seconds after they were hidden. var SuppressMarkers : logic = false OnBegin() : void = for (Node : Nodes): Node.CollectorTrigger.Disable() Node.Display.SetText(DropperText(SleepingText)) Node.Beacon.Disable() HideMarkersTrigger.TriggeredEvent.Subscribe(OnHideMarkers) ShowMarkersTrigger.TriggeredEvent.Subscribe(OnShowMarkers) for (PositionIdx := 0..ActiveCount - 1): spawn { ActivePositionLoop(PositionIdx) } Print("[dropper_manager] {Nodes.Length} nodes, {ActiveCount} awake at once, rotating after max {RotateSeconds}s") IsActive(Idx : int) : logic = Snapshot := ActiveIndices var Found : logic = false for (A : Snapshot): if (A = Idx): set Found = true return Found PickRandomInactiveNode() : ?int = var Candidates : []int = array{} for (Idx -> Node : Nodes): if (IsActive(Idx) = false): set Candidates += array{Idx} if (Candidates.Length = 0): return false RandPos := GetRandomInt(0, Candidates.Length - 1) if (Picked := Candidates[RandPos]): return option{Picked} return false # Runs one of the ActiveCount "awake slots": picks a sleeping node, wakes it, # waits until it's collected or the time runs out, then puts it back to sleep. ActivePositionLoop(PositionIdx : int) : void = loop: if (Idx := PickRandomInactiveNode()?): if (Node := Nodes[Idx]): set ActiveIndices += array{Idx} if (set NodePending[Idx] = 0) {} Node.CollectorTrigger.Enable() Node.Display.SetText(DropperText("{AmountPrefix}0")) if (SuppressMarkers = false): Node.Beacon.Enable() spawn { PlayVFX(Node, 0.5) } spawn { DropLoop(Idx, Node) } # Whichever finishes first wins; the other is cancelled. race: Sleep(RotateSeconds) WaitForCollect(Idx, Node) Node.CollectorTrigger.Disable() Node.Display.SetText(DropperText(SleepingText)) Node.Beacon.Disable() if (set NodePending[Idx] = 0) {} var Remaining : []int = array{} for (A : ActiveIndices): if (A <> Idx): set Remaining += array{A} set ActiveIndices = Remaining else: Sleep(1.0) # Adds to the node's pending amount on a timer; stops once the node sleeps. DropLoop(Idx : int, Node : resource_node) : void = loop: Sleep(Node.DropInterval) if (IsActive(Idx) = false): break Level := if (L := NodeLevels[Idx]) then L else 0 Amount := Node.BaseDropAmount + (Level * 5) Pending := if (P := NodePending[Idx]) then P else 0 NewPending := Min(Pending + Amount, Node.MaxPending) if (set NodePending[Idx] = NewPending) {} Node.Display.SetText(DropperText("{AmountPrefix}{NewPending}")) # Waits until a player collects a node that has something in it. WaitForCollect(Idx : int, Node : resource_node) : void = loop: MaybeAgent := Node.CollectorTrigger.TriggeredEvent.Await() if (A := MaybeAgent?): Pending := if (P := NodePending[Idx]) then P else 0 if (Pending > 0): ScoreManager.SetScoreAward(Pending) ScoreManager.Activate(A) Node.CollectSound.Play() spawn { PlayVFX(Node, Node.VFXDuration) } break PlayVFX(Node : resource_node, Seconds : float) : void = Node.CollectVFX.Enable() Sleep(Seconds) Node.CollectVFX.Disable() # Call from an upgrade pad: every player benefits straight away. UpgradeNode(Idx : int) : void = Level := if (L := NodeLevels[Idx]) then L else 0 if (set NodeLevels[Idx] = Level + 1) {} Print("[dropper_manager] Node {Idx} upgraded to level {Level + 1}") GetNodeLevel(Idx : int) : int = if (L := NodeLevels[Idx]): return L return 0 OnHideMarkers(Agent : ?agent) : void = SetMarkersHidden(true) OnShowMarkers(Agent : ?agent) : void = SetMarkersHidden(false) # Hide or show every node beacon at once. The rotation loop respects the # flag, so a node that wakes up during a show keeps its beacon off. SetMarkersHidden(Hidden : logic) : void = set SuppressMarkers = Hidden if (Hidden = true): for (Node : Nodes): Node.Beacon.Disable() else: for (Idx -> Node : Nodes): if (IsActive(Idx) = true): Node.Beacon.Enable()