powolnik commited on
Commit
0bc3a67
·
1 Parent(s): 96c9e7e

prop hunt and stronghold .verse files added

Browse files
Files changed (30) hide show
  1. Code_Reference/PropHunt/txt/base_team.txt +81 -0
  2. Code_Reference/PropHunt/txt/billboard_cinematic_flythrough.txt +187 -0
  3. Code_Reference/PropHunt/txt/heartbeat.txt +121 -0
  4. Code_Reference/PropHunt/txt/hunter_team.txt +28 -0
  5. Code_Reference/PropHunt/txt/prop_hunt.txt +189 -0
  6. Code_Reference/PropHunt/txt/prop_team.txt +147 -0
  7. Code_Reference/PropHunt/txt/round_timer.txt +158 -0
  8. Code_Reference/PropHunt/txt/waiting_for_more_players.txt +131 -0
  9. Code_Reference/PropHunt/verse/base_team.verse +81 -0
  10. Code_Reference/PropHunt/verse/billboard_cinematic_flythrough.verse +187 -0
  11. Code_Reference/PropHunt/verse/heartbeat.verse +121 -0
  12. Code_Reference/PropHunt/verse/hunter_team.verse +28 -0
  13. Code_Reference/PropHunt/verse/prop_hunt.verse +189 -0
  14. Code_Reference/PropHunt/verse/prop_team.verse +147 -0
  15. Code_Reference/PropHunt/verse/round_timer.verse +158 -0
  16. Code_Reference/PropHunt/verse/waiting_for_more_players.verse +131 -0
  17. Code_Reference/Stronghold/txt/stronghold_alert_manager.txt +0 -0
  18. Code_Reference/Stronghold/txt/stronghold_bark_manager.txt +0 -0
  19. Code_Reference/Stronghold/txt/stronghold_billboard_cinematic.txt +0 -0
  20. Code_Reference/Stronghold/txt/stronghold_game_manager.txt +0 -0
  21. Code_Reference/Stronghold/txt/stronghold_intro_explosion.txt +0 -0
  22. Code_Reference/Stronghold/txt/stronghold_leash_position.txt +0 -0
  23. Code_Reference/Stronghold/txt/stronghold_log.txt +0 -0
  24. Code_Reference/Stronghold/verse/stronghold_alert_manager.verse +87 -0
  25. Code_Reference/Stronghold/verse/stronghold_bark_manager.verse +247 -0
  26. Code_Reference/Stronghold/verse/stronghold_billboard_cinematic.verse +149 -0
  27. Code_Reference/Stronghold/verse/stronghold_game_manager.verse +359 -0
  28. Code_Reference/Stronghold/verse/stronghold_intro_explosion.verse +108 -0
  29. Code_Reference/Stronghold/verse/stronghold_leash_position.verse +85 -0
  30. Code_Reference/Stronghold/verse/stronghold_log.verse +12 -0
Code_Reference/PropHunt/txt/base_team.txt ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Characters }
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/UI }
4
+ using { /UnrealEngine.com/Temporary/Diagnostics }
5
+ using { /UnrealEngine.com/Temporary/SpatialMath }
6
+ using { /UnrealEngine.com/Temporary/UI }
7
+ using { /Verse.org/Colors }
8
+ using { /Verse.org/Simulation }
9
+
10
+ log_team := class(log_channel){}
11
+
12
+ # This class defines the devices needed for the different teams in the experience.
13
+ # This class is abstract so it cannot be used on its own. It has to be inherited by another class.
14
+ base_team := class<abstract>:
15
+ Logger:log = log{Channel:=log_team}
16
+
17
+ @editable # Used to set a player to the team.
18
+ ClassSelector:class_and_team_selector_device = class_and_team_selector_device{}
19
+
20
+ @editable # Used to award score to agents on the team.
21
+ ScoreManager:score_manager_device = score_manager_device{}
22
+
23
+ @editable # Used to display the team assignment title.
24
+ TeamTitle:hud_message_device = hud_message_device{}
25
+
26
+ @editable # Used to display the team assignment description.
27
+ TeamDescription:hud_message_device = hud_message_device{}
28
+
29
+ @editable # Used to subscribe to team member (prop team) or enemy (hunter team) eliminated events.
30
+ TeamManager:team_settings_and_inventory_device = team_settings_and_inventory_device{}
31
+
32
+ # This is an array of agents on the team.
33
+ var TeamAgents<private>:[]agent = array{}
34
+
35
+ # This event is signaled when the TeamAgents array becomes empty (signaling the end of the round).
36
+ TeamEmptyEvent:event() = event(){}
37
+
38
+ # Returns the current TeamAgents array.
39
+ # This is required because the TeamAgents array is private, so other classes cannot access it directly.
40
+ GetAgents()<decides><transacts>:[]agent =
41
+ TeamAgents
42
+
43
+ # Return the size of the TeamAgents array
44
+ # This requires a function because the TeamAgents array is private, so other classes cannot access it directly.
45
+ Count()<transacts>:int =
46
+ TeamAgents.Length
47
+
48
+ # Returns an index in the TeamAgents array of an agent, fails otherwise.
49
+ FindOnTeam(Agent:agent)<decides><transacts>: int =
50
+ Index := TeamAgents.Find[Agent]
51
+
52
+ # Set the agent to the team and notify the player.
53
+ InitializeAgent(Agent:agent):void =
54
+ AddAgentToTeam(Agent)
55
+ ClassSelector.ChangeTeamAndClass(Agent)
56
+ DisplayTeamInformation(Agent)
57
+
58
+ # Add an agent to TeamAgents.
59
+ AddAgentToTeam(AgentToAdd:agent):void =
60
+ if (not FindOnTeam[AgentToAdd]):
61
+ Logger.Print("Adding agent to team.")
62
+ set TeamAgents += array{AgentToAdd}
63
+
64
+ # Activates hud message devices to show the player what team they are on
65
+ DisplayTeamInformation(Agent:agent):void =
66
+ TeamTitle.Show(Agent)
67
+ TeamDescription.Show(Agent)
68
+
69
+ # When an agent leaves the match, remove them from the TeamAgents array and check for the end of the round.
70
+ EliminateAgent(Agent:agent)<suspends>:void =
71
+ Sleep(0.0) # Delaying 1 game tick to ensure the player is respawned before proceeding.
72
+ RemoveAgentFromTeam(Agent)
73
+
74
+ # Remove an agent from TeamAgents.
75
+ # If the agent removed was the last, signal TeamEmptyEvent.
76
+ RemoveAgentFromTeam(AgentToRemove:agent):void =
77
+ set TeamAgents = TeamAgents.RemoveAllElements(AgentToRemove)
78
+ Logger.Print("{Count()} agent(s) on team remaining.")
79
+ if (Count() < 1):
80
+ Logger.Print("No agents on team remaining. Ending the round.")
81
+ TeamEmptyEvent.Signal()
Code_Reference/PropHunt/txt/billboard_cinematic_flythrough.txt ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Characters }
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/FortPlayerUtilities }
4
+ using { /Fortnite.com/UI }
5
+ using { /UnrealEngine.com/Temporary/Diagnostics }
6
+ using { /UnrealEngine.com/Temporary/SpatialMath}
7
+ using { /UnrealEngine.com/Temporary/UI }
8
+ using { /Verse.org/Simulation }
9
+ using { /Verse.org/Simulation/Tags }
10
+
11
+ # Gameplay tag for objects that should be displayed or hidden during cinematic
12
+ cinematic_objects := class(tag):
13
+
14
+ # Script that sets the player into a cinematic mode so its not in the cinematic, and then resets them when the cinematic is done
15
+ billboard_cinematic := class<concrete>:
16
+
17
+ # A reference to the device that contains the level sequence to be played
18
+ @editable
19
+ CinematicSequence:cinematic_sequence_device := cinematic_sequence_device{}
20
+
21
+ # A reference to the device that will hide the HUD
22
+ @editable
23
+ HUDController:hud_controller_device := hud_controller_device{}
24
+
25
+ # Should the Skip Intro button be visible during the cinematic
26
+ @editable
27
+ ShowUI:logic := true
28
+
29
+ # Should Devices and Billboards be visible during the cinematic
30
+ @editable
31
+ ShowCreativeObjects:logic := true
32
+
33
+ # Should play the cinematic at the beginning of game
34
+ @editable
35
+ PlayAtStart:logic = true
36
+
37
+ # Reference to the Radio that plays music
38
+ @editable
39
+ Music:radio_device := radio_device{}
40
+
41
+ # Signaled when the cinematic has completed.
42
+ CinematicCompleted<public>:event() = event(){}
43
+
44
+ # Runs at the beginning of the script.
45
+ EnterCinematicModeForAll<public>(InPlayers:[]player):void=
46
+ HUDController.Enable()
47
+
48
+ if (ShowCreativeObjects?):
49
+ ShowObjectsInCinematic()
50
+ else:
51
+ HideObjectsInCinematic()
52
+
53
+ for:
54
+ Player : InPlayers
55
+ not Player.IsSpectator[]
56
+ do:
57
+ spawn{EnterCinematicMode(Player)}
58
+
59
+ Music.Play()
60
+
61
+ # Sets players to visible, re-enables their movement, and removes the skip button.
62
+ EndCinematicModeForAll<public>(InPlayers:[]player):void=
63
+ Music.Stop()
64
+ for:
65
+ Player : InPlayers
66
+ not Player.IsSpectator[]
67
+ do:
68
+ EndCinematicMode(Player)
69
+
70
+ HUDController.Disable()
71
+ HideObjectsInCinematic()
72
+
73
+ # UI to show during the cinematic
74
+ CinematicUI<private>:cinematic_ui = cinematic_ui{}
75
+
76
+ # Handler for player choosing to skip cinematic
77
+ HandleSkipIntroInteraction<private>(WidgetMessage:widget_message):void=
78
+ EndCinematicMode(WidgetMessage.Player)
79
+
80
+ # Sets players to invisible, removes their movement, optionally gives them a skip button, then waits for the cinematic to end before signaling that it has completed.
81
+ EnterCinematicMode<private>(Player:player)<suspends>:void=
82
+ HidePlayer(Player)
83
+
84
+ if:
85
+ ShowUI?
86
+ PlayerUI := GetPlayerUI[Player]
87
+ then:
88
+ Listenable := CinematicUI.Show(Player, PlayerUI)
89
+ Listenable.Subscribe(HandleSkipIntroInteraction)
90
+
91
+ CinematicSequence.Play(Player)
92
+
93
+ CinematicSequence.StoppedEvent.Await()
94
+ CinematicCompleted.Signal()
95
+
96
+ # Stop cinematic for player
97
+ EndCinematicMode<private>(Player:player):void=
98
+ CinematicSequence.Stop(Player)
99
+ ShowPlayer(Player)
100
+ CinematicUI.Hide(Player)
101
+
102
+ # Make player invisible and unmoveable
103
+ HidePlayer<private>(Player:player):void=
104
+ if (FortCharacter := Player.GetFortCharacter[]):
105
+ CinematicStasis := stasis_args{AllowTurning := false, AllowFalling := false, AllowEmotes := false}
106
+
107
+ FortCharacter.PutInStasis(CinematicStasis)
108
+ FortCharacter.Hide()
109
+
110
+ # Make player visible and moveable
111
+ ShowPlayer<private>(Player:player):void=
112
+ if (FortCharacter := Player.GetFortCharacter[]):
113
+ FortCharacter.ReleaseFromStasis()
114
+ FortCharacter.Show()
115
+
116
+ # Find all objects tagged and make them visible
117
+ ShowObjectsInCinematic<private>():void=
118
+ ObjectsToShow := GetCreativeObjectsWithTag(cinematic_objects{})
119
+
120
+ for (Object : ObjectsToShow):
121
+ if (Billboard := billboard_device[Object]):
122
+ Billboard.ShowText()
123
+ else if (Device := creative_device[Object]):
124
+ Device.Show()
125
+ else if (Radio := radio_device[Object]):
126
+ Radio.Show()
127
+ else if (AudioPlayer := audio_player_device[Object]):
128
+ AudioPlayer.Show()
129
+
130
+ # Find all objects tagged and make them invisible
131
+ HideObjectsInCinematic<private>():void=
132
+ ObjectsToHide := GetCreativeObjectsWithTag(cinematic_objects{})
133
+
134
+ for (Object : ObjectsToHide):
135
+ if (Billboard := billboard_device[Object]):
136
+ Billboard.HideText()
137
+ else if (Device := creative_device[Object]):
138
+ Device.Hide()
139
+ else if (Radio := radio_device[Object]):
140
+ Radio.Hide()
141
+ else if (AudioPlayer := audio_player_device[Object]):
142
+ AudioPlayer.Hide()
143
+
144
+ # Class in charge of UI for cinematic
145
+ cinematic_ui := class:
146
+
147
+ # Create a canvas widget that displays a skip button for the cinematic
148
+ CreateUI():tuple(canvas, listenable(widget_message))=
149
+ SkipButton:button_regular = button_regular{DefaultText := SkipCinematicText}
150
+ SkipButton.SetEnabled(true)
151
+
152
+ UICanvas:canvas = canvas:
153
+ Slots := array:
154
+ canvas_slot:
155
+ Anchors := anchors{Minimum := vector2{X := 0.9, Y := 0.9}, Maximum := vector2{X := 0.9, Y := 0.9}}
156
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
157
+ Alignment := vector2{X := 1.0, Y := 1.0}
158
+ SizeToContent := true
159
+ Widget := SkipButton
160
+
161
+ return (UICanvas, SkipButton.OnClick())
162
+
163
+ # Shows the UI for the player and returns the listenable for the button
164
+ Show(Player:player, PlayerUI:player_ui):listenable(widget_message)=
165
+ UIResult := CreateUI()
166
+ UICanvas := UIResult(0)
167
+ Listenable := UIResult(1)
168
+
169
+ PlayerUISlot := player_ui_slot{ZOrder := 0, InputMode := ui_input_mode.All}
170
+ PlayerUI.AddWidget(UICanvas, PlayerUISlot)
171
+ if (set UIPerPlayer[Player] = option{UICanvas}):
172
+
173
+ return Listenable
174
+
175
+ # Hides the UI for the player
176
+ Hide(Player:player):void=
177
+ if:
178
+ PlayerUI := GetPlayerUI[Player]
179
+ CustomUI := UIPerPlayer[Player]?
180
+ then:
181
+ PlayerUI.RemoveWidget(CustomUI)
182
+ if (set UIPerPlayer[Player] = false):
183
+
184
+ # A localizable message to display as text in the UI
185
+ SkipCinematicText<localizes><private>:message = "Skip Intro"
186
+
187
+ var UIPerPlayer<private>:[player]?canvas = map{}
Code_Reference/PropHunt/txt/heartbeat.txt ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Characters }
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/UI }
4
+ using { /UnrealEngine.com/Temporary/SpatialMath }
5
+ using { /UnrealEngine.com/Temporary/Diagnostics }
6
+ using { /UnrealEngine.com/Temporary/UI }
7
+ using { /Verse.org/Colors }
8
+ using { /Verse.org/Simulation }
9
+
10
+ log_heart_beat := class(log_channel){}
11
+
12
+ # These messages are used to notify a prop agent with a message (or to hide it) when they need to move to avoid their heart beat from becoming visible.
13
+ HeartBeatWarningMessage<localizes>(Time:int):message = "Heart Beat in {Time} Seconds. Move!"
14
+ HeartBeatWarningClear<localizes>:message = ""
15
+
16
+ # This class exposed the editable properties for the heartbeat to the prop_hunt device.
17
+ heart_beat := class<concrete>():
18
+ Logger:log = log{Channel:=log_heart_beat}
19
+
20
+ @editable # The number of seconds before a prop agent must move before the heart beat reveals their position.
21
+ MoveTime:float = 15.0
22
+
23
+ @editable # The seconds remaining before the heart beat warning appears. Shouldn't be > than HeartBeatTimer.
24
+ WarningTime:float = 5.0
25
+
26
+ @editable # An array of heart beat VFX devices. There is one per player.
27
+ AgentVFX:[]heartbeat_vfx = array{}
28
+
29
+ @editable # The audio player device used to play the heart beat sound effects (SFX).
30
+ SFXPlayer:radio_device = radio_device{}
31
+
32
+ # This map associates a UI for displaying the heart beat warning to each prop agent.
33
+ var WarningUI:[agent]heartbeat_warning_ui = map{}
34
+
35
+ # Keeps track of how many players have an active heartbeat so we can manage the SFX device.
36
+ var NumberOfHeartBeats:int = 0
37
+
38
+ # Sets up heart beat UI for the agent.
39
+ SetUpUI(PropAgent:agent):void =
40
+ if:
41
+ not WarningUI[PropAgent]
42
+ AsPlayer := player[PropAgent]
43
+ PlayerUI := GetPlayerUI[AsPlayer]
44
+ then:
45
+ UIData:heartbeat_warning_ui = heartbeat_warning_ui{}
46
+ UIData.CreateCanvas()
47
+ PlayerUI.AddWidget(UIData.Canvas, player_ui_slot{ZOrder := 1})
48
+ if (set WarningUI[PropAgent] = UIData) {}
49
+
50
+ # Activates the heartbeat VFX and SFX for the specified player.
51
+ Enable(PropAgent:agent, HeartBeatVFXData:heartbeat_vfx):void =
52
+ if:
53
+ # Get the character, which is used to find the prop agent's position in the scene.
54
+ Character := PropAgent.GetFortCharacter[]
55
+ then:
56
+ # Set the heart beat VFX's position to the prop agent's position.
57
+ HeartBeatVFXData.Activate(Character.GetTransform())
58
+ # Increment the heartbeat count, and if this is the first heartbeat playing, we need to play the audio to get it started.
59
+ set NumberOfHeartBeats += 1
60
+ if (NumberOfHeartBeats = 1) then SFXPlayer.Play()
61
+ # Register the prop agent to the audio player device so the heart beat audio will play from that position.
62
+ SFXPlayer.Register(PropAgent)
63
+ else:
64
+ Logger.Print("Character, Index, or HeartBeatVFXData not found. Cannot start the heartbeat")
65
+
66
+ # Clears the heartbeat VFX and SFX for the specified prop agent.
67
+ Disable(PropAgent:agent, HeartBeatVFXData:heartbeat_vfx):void =
68
+ Logger.Print("Disabling heart beat.")
69
+ # Deactivate the VFX visuals.
70
+ HeartBeatVFXData.Deactivate()
71
+ # Unregister the prop agent from the audio player device, causing the heart beat audio to stop.
72
+ SFXPlayer.Unregister(PropAgent)
73
+ # Decrement the heartbeat counter. This counter should never drop below 0.
74
+ set NumberOfHeartBeats -= 1
75
+ if (NumberOfHeartBeats < 0) then set NumberOfHeartBeats = 0
76
+
77
+ # Clears all heartbeat VFX and SFX for all prop agents.
78
+ DisableAll():void =
79
+ Logger.Print("Disabling all heart beats.")
80
+ # Iterate through all VFX and move them to 0,0,0.
81
+ for (HeartBeatVFXDevice : AgentVFX):
82
+ HeartBeatVFXDevice.Deactivate()
83
+ # Unregister all players from the heart beat audio.
84
+ SFXPlayer.UnregisterAll()
85
+ # Reinitialize the heartbeat counter to 0
86
+ set NumberOfHeartBeats = 0
87
+
88
+ # The heartbeat_warning_ui class contains a struct of data to track the UI canvas and text_block per player as well as the function to create a new heartbeat warning UI canvas.
89
+ heartbeat_warning_ui := class:
90
+ var Canvas:canvas = canvas{}
91
+ var Text:text_block = text_block{}
92
+
93
+ # Creates the UI canvas for the warning message.
94
+ CreateCanvas():void =
95
+ set Text = text_block{DefaultTextColor := NamedColors.White, DefaultShadowColor := NamedColors.Black}
96
+ set Canvas = canvas:
97
+ Slots := array:
98
+ canvas_slot:
99
+ Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.75}, Maximum := vector2{X := 0.5, Y := 0.75}}
100
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
101
+ Alignment := vector2{X := 0.5, Y := 1.0}
102
+ SizeToContent := true
103
+ Widget := Text
104
+
105
+ # The heartbeat_vfx class contains a struct of data to track the VFX's root and vfx_spawner_device objects per player as well as the functions to set the VFX to a position or reset it.
106
+ heartbeat_vfx := class<concrete>:
107
+ @editable # The VFX device for each heart beat.
108
+ VFXDevice:vfx_spawner_device = vfx_spawner_device{}
109
+
110
+ # This offset is used to position the heartbeat above a prop agent's head.
111
+ HeartBeatVFXOffset:vector3 = vector3{X := 0.0, Y := 0.0, Z := 110.0}
112
+
113
+ # Sets the position of the heart beat VFX then enables the VFX.
114
+ Activate(Transform:transform):void =
115
+ VFXPosition := Transform.Translation + HeartBeatVFXOffset
116
+ if (VFXDevice.TeleportTo[VFXPosition, Transform.Rotation]):
117
+ VFXDevice.Enable()
118
+
119
+ # Disables the VFX, hiding the heart beat visuals.
120
+ Deactivate():void =
121
+ VFXDevice.Disable()
Code_Reference/PropHunt/txt/hunter_team.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Devices }
2
+ using { /UnrealEngine.com/Temporary/Diagnostics }
3
+ using { /Verse.org/Simulation }
4
+
5
+ # Inheriting from the base_team class, The hunter_team class contains the device definitions and functions related to the hunter team and its agents.
6
+ hunter_team := class<concrete>(base_team):
7
+ @editable # One hunter agent is created each round for each n players. Example: HunterTeamPerNumberOfPlayers = 5.0 is 1 per 5 players. If players = 6, 2 hunter agents are created.
8
+ HunterAgentPerNumberOfPlayers:float = 5.0 # Minimum 1.1 is enforced to ensure at least 1 prop agent is created.
9
+
10
+ @editable # The number of seconds before the hunter agents are spawned, giving the prop agents a head start to hide.
11
+ SpawnDelay:float = 15.0
12
+
13
+ @editable # The maximum base points a hunter agent gets for eliminating a prop agent. These points are divided by the number of prop agents remaining.
14
+ MaxEliminationScore:int = 5000
15
+
16
+ @editable # The timer device used to give props a grace period to hide.
17
+ WaitTimer:timer_device = timer_device{}
18
+
19
+ # Set the agent to a hunter agent.
20
+ InitializeAgent<override>(NewHunterAgent:agent):void =
21
+ Logger.Print("Setting a new hunter agent.")
22
+ (super:)InitializeAgent(NewHunterAgent)
23
+
24
+ # When a hunter agent leaves the match, remove them from the HunterAgents array and check for the end of the round.
25
+ # Notice that we're overriding this function because we don't need to pass extra data here like we do for prop team.
26
+ EliminateAgent<override>(HunterAgent:agent)<suspends>:void =
27
+ Logger.Print("Hunter agent eliminated.")
28
+ (super:)EliminateAgent(HunterAgent)
Code_Reference/PropHunt/txt/prop_hunt.txt ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Devices }
2
+ using { /Fortnite.com/UI }
3
+ using { /UnrealEngine.com/Temporary/Diagnostics }
4
+ using { /UnrealEngine.com/Temporary/SpatialMath }
5
+ using { /UnrealEngine.com/Temporary/UI }
6
+ using { /Verse.org/Colors }
7
+ using { /Verse.org/Random }
8
+ using { /Verse.org/Simulation }
9
+
10
+ log_prop_hunt_device := class(log_channel){}
11
+
12
+ # This class contains the logic for starting and ending a round. It also handles events for players joining and leaving when a round is in progress.
13
+ prop_hunt := class(creative_device):
14
+ Logger:log = log{Channel:=log_prop_hunt_device}
15
+
16
+ @editable # An instance of the prop_team class, used to manage the Prop team.
17
+ PropTeam:prop_team = prop_team{}
18
+
19
+ @editable # An instance of the hunter_team class, used to manage the Hunter team.
20
+ HunterTeam:hunter_team = hunter_team{}
21
+
22
+ @editable # The round manager device is used to end the round when the last prop agent is eliminated.
23
+ RoundSettings:round_settings_device = round_settings_device{}
24
+
25
+ @editable # The teleporter in the lobby area. This device is enabled and disabled to allow props and hunters out of the pre-game lobby.
26
+ LobbyTeleporter:teleporter_device = teleporter_device{}
27
+
28
+ @editable # The waiting for more players device. This device checks if there are enough players to start the round.
29
+ WaitingForMorePlayers:waiting_for_more_players = waiting_for_more_players{}
30
+
31
+ @editable # This hud controller is used when a round is waiting to start.
32
+ # Set Priority to "Highest" for this device and have a second HUD controller set to < "Highest" to use during the round.
33
+ HUDControllerWaiting:hud_controller_device = hud_controller_device{}
34
+
35
+ @editable # The round timer device. This device keeps track of the time left in the round and is started within StartTheHunt().
36
+ RoundTimer:round_timer = round_timer{}
37
+
38
+ @editable # The device that controls the game music.
39
+ GameMusic:radio_device = radio_device{}
40
+
41
+ @editable # Message for when the hunter agents are set loose in the game.
42
+ StartTheHuntMessage:hud_message_device = hud_message_device{}
43
+
44
+ @editable # The Verse class that controls the opening cinematic.
45
+ Cinematic:billboard_cinematic = billboard_cinematic{}
46
+
47
+ # When the device is started in a running game, initialize subscriptions and launch the team setup. This runs at the start of every round.
48
+ OnBegin<override>()<suspends>:void =
49
+ Sleep(1.0)
50
+ Logger.Print("Round loaded.")
51
+
52
+ # Play cinematic at beginning if user has it enabled
53
+ if (Cinematic.PlayAtStart?):
54
+ Cinematic.EnterCinematicModeForAll(GetPlayspace().GetPlayers())
55
+ Cinematic.CinematicCompleted.Await()
56
+ Cinematic.EndCinematicModeForAll(GetPlayspace().GetPlayers())
57
+ Logger.Print("Cinematic Completed.")
58
+ else:
59
+ Logger.Print("Cinematic Skipped.")
60
+
61
+ GameMusic.Play()
62
+ SetUpTeams()
63
+ race: # When there are no more prop or hunter agents (whichever happens first), or the round timer finishes, the round ends
64
+ PropTeam.TeamEmptyEvent.Await()
65
+ HunterTeam.TeamEmptyEvent.Await()
66
+ RoundTimer.AwaitEnd()
67
+ Logger.Print("Round ending.")
68
+ EndRound()
69
+
70
+ # When a round is started, subscribe to team devices, randomly pick the hunter agents, enable the hunter timer, set the prop agents, and teleport them into the game area.
71
+ SetUpTeams()<suspends>:void =
72
+ Logger.Print("Setting up teams.")
73
+
74
+ # Subscribe to the prop team score timer, set the score award, and subscribe to the prop team's eliminated event.
75
+ PropTeam.ScoreManager.SetScoreAward(PropTeam.ScorePerSecond)
76
+ PropTeam.TeamManager.TeamMemberEliminatedEvent.Subscribe(OnPropEliminated) # Occurs when a prop agent is eliminated.
77
+
78
+ # Subscribe to the hunter team's wait timer and set the duration. Also subscribe to the hunter team's elimination event.
79
+ HunterTeam.WaitTimer.SuccessEvent.Subscribe(HuntersGo)
80
+ HunterTeam.WaitTimer.SetMaxDuration(HunterTeam.SpawnDelay)
81
+ HunterTeam.TeamManager.EnemyEliminatedEvent.Subscribe(OnHunterEliminated) # Occurs when a hunter agent eliminates a prop agent.
82
+
83
+ # Initialize the starting hunter and prop agents arrays. Get the players and find the number of players in the server.
84
+ var StartingHunterAgents:[]agent = array{}
85
+ var StartingPropAgents:[]agent = array{}
86
+ var Players:[]player = GetPlayspace().GetPlayers()
87
+
88
+ # Enable the HUD appropriate for waiting for players.
89
+ HUDControllerWaiting.Enable()
90
+
91
+ # Check if there are enough players to start the round.
92
+ set Players = WaitingForMorePlayers.WaitForMinimumNumberOfPlayers(Players)
93
+ Logger.Print("Round started.")
94
+
95
+ # Disable the waiting HUD to use the next highest priority HUD.
96
+ HUDControllerWaiting.Disable()
97
+
98
+ # Now that the round has started, we need to handle players being added or removed from the match. Subscribe to those events.
99
+ GetPlayspace().PlayerAddedEvent().Subscribe(OnPlayerAdded)
100
+ GetPlayspace().PlayerRemovedEvent().Subscribe(OnPlayerRemoved)
101
+
102
+ # Calculate the number of starting hunter agents for the game based on the player count and the number of hunter agents per number of players.
103
+ NumberOfPlayers:float = 1.0 * Players.Length # Converts Players.Length to a float so it can be used in the next Ceil function.
104
+ if (NumberOfStartingHunters:int = Ceil[NumberOfPlayers / Max(1.1, HunterTeam.HunterAgentPerNumberOfPlayers)]):
105
+ # Shuffle the players and then slice the array to get the starting hunter agents. The remaining players are the starting prop agents.
106
+ var RandomizedPlayers:[]agent = Shuffle(Players)
107
+ if (set StartingHunterAgents = RandomizedPlayers.Slice[0,NumberOfStartingHunters]) {}
108
+ if (set StartingPropAgents = RandomizedPlayers.Slice[NumberOfStartingHunters,RandomizedPlayers.Length]) {}
109
+
110
+ # Iterate through the starting hunter agents and assign them to the hunter team. Then start the hunter wait timer.
111
+ Logger.Print("Setting {StartingHunterAgents.Length} hunter agent(s).")
112
+ for (StartingHunterAgent : StartingHunterAgents):
113
+ HunterTeam.InitializeAgent(StartingHunterAgent)
114
+ HunterTeam.WaitTimer.Start()
115
+
116
+ # Iterate through the starting prop agents and assign them to the prop team. Teleport them into the play area.
117
+ Logger.Print("Setting {StartingPropAgents.Length} prop agent(s).")
118
+ LobbyTeleporter.Enable()
119
+ for (StartingPropAgent : StartingPropAgents):
120
+ PropTeam.InitializeAgent(StartingPropAgent)
121
+ PropTeam.HeartBeat.SetUpUI(StartingPropAgent)
122
+ LobbyTeleporter.Activate(StartingPropAgent)
123
+ LobbyTeleporter.Disable()
124
+ # Set the props remaining tracker's Target and Value to the current number of props.
125
+ # In the future, we'll only update Value as props are eliminated.
126
+ PropTeam.PropsRemainingTracker.SetTarget(PropTeam.Count())
127
+ PropTeam.UpdatePropsRemainingTracker()
128
+
129
+ # When the hunter timer finishes, spawn the function that unleashes the hunter agents into the game and begins the round.
130
+ # HunterInstigator is needed for the event subscription but not used.
131
+ HuntersGo(HunterInstigator:?agent):void =
132
+ spawn{ StartTheHunt() }
133
+
134
+ # Enable the lobby teleporter, teleport all the hunter agents to the game area and then run the prop game loop for each prop agent.
135
+ StartTheHunt()<suspends>:void =
136
+ Logger.Print("Teleporting hunter agent(s).")
137
+ LobbyTeleporter.Enable()
138
+ Sleep(0.5) # Delaying half a second to give the teleporter time to enable.
139
+ for (HunterAgent : HunterTeam.GetAgents[]):
140
+ LobbyTeleporter.Activate(HunterAgent)
141
+ StartTheHuntMessage.Show(HunterAgent)
142
+ Logger.Print("Starting prop(s) game logic.")
143
+ for (PropAgent : PropTeam.GetAgents[]):
144
+ spawn {PropTeam.RunPropGameLoop(PropAgent)}
145
+ StartTheHuntMessage.Show(PropAgent)
146
+
147
+ # Signal that the round has started to start the round and scoring timers.
148
+ RoundTimer.Start()
149
+ PropTeam.ScoreTimer.Start()
150
+
151
+ # When a player joins the match mid-round, make them a hunter if the round has started.
152
+ OnPlayerAdded(Player:player):void =
153
+ Logger.Print("A player joined the game.")
154
+ if (PropTeam.Count() > 0): # if there are any prop agents, the round has started.
155
+ HunterTeam.InitializeAgent(Player)
156
+
157
+ # When a hunter agent eliminates a prop agent, award score. The score is divided by the number of prop agents remaining.
158
+ OnHunterEliminated(HunterAgent:agent):void =
159
+ Logger.Print("Hunter agent eliminated a prop agent.")
160
+ PropTeamSize := PropTeam.Count()
161
+ if (EliminationAward := Floor(HunterTeam.MaxEliminationScore / PropTeamSize)):
162
+ Logger.Print("Awarding {EliminationAward} points.")
163
+ HunterTeam.ScoreManager.SetScoreAward(EliminationAward)
164
+ HunterTeam.ScoreManager.Activate(HunterAgent)
165
+
166
+ # When a prop agent is eliminated, remove the prop from the prop team, check for round end, and set them as a hunter.
167
+ OnPropEliminated(PropAgent:agent):void =
168
+ Logger.Print("Prop agent eliminated.")
169
+ spawn{ PropTeam.EliminateAgent(PropAgent) }
170
+ HunterTeam.InitializeAgent(PropAgent)
171
+
172
+ # When a player leaves the match, check which team they were on and then check for round end.
173
+ OnPlayerRemoved(Player:player):void =
174
+ Logger.Print("A player left the game.")
175
+ if (PropTeam.FindOnTeam[Player]):
176
+ Logger.Print("Player was a Prop.")
177
+ spawn{ PropTeam.EliminateAgent(Player) }
178
+ if (HunterTeam.FindOnTeam[Player]):
179
+ Logger.Print("Player was a Hunter.")
180
+ spawn{ HunterTeam.EliminateAgent(Player) }
181
+
182
+ # Cleans up the heart beat VFX and then ends the round.
183
+ EndRound():void=
184
+ PropTeam.HeartBeat.DisableAll()
185
+ # Get any player to pass to EndRound
186
+ Players:[]player = GetPlayspace().GetPlayers()
187
+ if (RoundEndInstigator := Players[0]):
188
+ Logger.Print("Round ended.")
189
+ RoundSettings.EndRound(RoundEndInstigator)
Code_Reference/PropHunt/txt/prop_team.txt ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Characters }
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/UI }
4
+ using { /UnrealEngine.com/Temporary/Diagnostics }
5
+ using { /UnrealEngine.com/Temporary/SpatialMath }
6
+ using { /UnrealEngine.com/Temporary/UI }
7
+ using { /Verse.org/Colors }
8
+ using { /Verse.org/Simulation }
9
+
10
+ # This message is used to print the number of props remaining to all players during a round.
11
+ PropAgentsRemainingMessage<localizes>(Count:int):message = "{Count} Prop(s) Remaining"
12
+
13
+ # Inheriting from the base_team class, the prop_team class contains the device definitions and functions related to the prop team and its agents.
14
+ # Notably, a prop agent's heart beat behavior can be found in this class.
15
+ prop_team := class<concrete>(base_team):
16
+ @editable # The score a prop agent receives per second.
17
+ ScorePerSecond:int = 10
18
+
19
+ @editable # The minimum distance a prop agent must move to reset the heart beat timer.
20
+ MinimumMoveDistance:float = 100.0
21
+
22
+ @editable # The timer device used to award score to a prop.
23
+ ScoreTimer:timer_device = timer_device{}
24
+
25
+ @editable # This tracker device is used to display the props remaining to the screen.
26
+ PropsRemainingTracker:tracker_device = tracker_device{}
27
+
28
+ @editable # Get the properties from the heart_beat class.
29
+ HeartBeat:heart_beat = heart_beat{}
30
+
31
+ # Set the agent to a prop agent and assign heart beat warning UI.
32
+ InitializeAgent<override>(NewPropAgent:agent):void =
33
+ Logger.Print("Setting a new prop agent.")
34
+ (super:)InitializeAgent(NewPropAgent)
35
+
36
+ # When a prop agent is eliminated or leaves the match, remove them from the PropAgents array and check for the end of the round.
37
+ # Note that this isn't overridding because we're passing all players to the function to update the props remaining message.
38
+ EliminateAgent<override>(PropAgent:agent)<suspends>:void =
39
+ Logger.Print("Prop agent eliminated.")
40
+ (super:)EliminateAgent(PropAgent)
41
+ # Update the props remaining number.
42
+ UpdatePropsRemainingTracker()
43
+
44
+ # Updates the value of the tracker device showing the number of props remaining.
45
+ UpdatePropsRemainingTracker():void =
46
+ PropsRemainingTracker.SetValue(Count())
47
+
48
+ # If the prop agent stops moving then race to see if the prop agent moves beyond the MinimumMoveDistance, the heart beat timer completes, or the prop agent is eliminated.
49
+ RunPropGameLoop(PropAgent:agent)<suspends>:void =
50
+ Logger.Print("Starting prop agent game loop.")
51
+ # Loop forever through the prop behavior and scoring until the prop agent is eliminated or the player leaves the session.
52
+ race:
53
+ PropAgent.AwaitNoLongerAProp()
54
+ loop:
55
+ # Wait until the prop agent moves less than the minimum distance, then advance.
56
+ PropAgent.AwaitStopMoving(MinimumMoveDistance)
57
+ # Until the prop agent moves beyond the minimum distance, countdown to the heart beat and then play the heart beat indefinitely.
58
+ race:
59
+ PropAgent.AwaitStartMoving(MinimumMoveDistance)
60
+ block:
61
+ CountdownTimer(PropAgent)
62
+ PropAgent.StartHeartbeat()
63
+ Sleep(0.0) # Once the race completes (the prop agent moves), start the loop again.
64
+ loop:
65
+ # Every second, award the prop agent points.
66
+ Sleep(1.0)
67
+ ScoreManager.Activate(PropAgent)
68
+
69
+
70
+ # Loop until the prop agent is no longer a part of the PropAgents array. Removal happens if the prop agent is eliminated and turned into a hunter or if the player leaves the session.
71
+ (PropAgent:agent).AwaitNoLongerAProp()<suspends>:void =
72
+ loop:
73
+ if (not FindOnTeam[PropAgent]):
74
+ Logger.Print("Cancelling prop agent behavior.")
75
+ break
76
+ Sleep(0.0) # Advance to the next game tick.
77
+
78
+ # Loops until the agent moves less than the MinimumDistance.
79
+ (PropAgent:agent).AwaitStopMoving(MinimumDistance:float)<suspends>:void =
80
+ Logger.Print("Checking if the agent has moved less than the minimum distance.")
81
+ # Get the initial position of the agent from the agent's character in the scene.
82
+ if (Tracked := PropAgent.GetFortCharacter[]):
83
+ var StartPosition:vector3 = Tracked.GetTransform().Translation
84
+ loop:
85
+ Sleep(0.0) # Get the position of the agent in the next game tick.
86
+ NewPosition := Tracked.GetTransform().Translation
87
+ # If the distance of the new position from the starting position is less than MinimumDistance, the agent has not moved and we break the loop.
88
+ if (Distance(StartPosition, NewPosition) < MinimumDistance):
89
+ Logger.Print("Agent has moved less than the minimum distance.")
90
+ break
91
+ # Otherwise, we reset StartPosition to make sure the player moves from the new position.
92
+ else:
93
+ set StartPosition = NewPosition
94
+
95
+ # Loops until the agent moves more than the MinimumDistance.
96
+ (PropAgent:agent).AwaitStartMoving(MinimumDistance:float)<suspends>:void =
97
+ Logger.Print("Checking if the agent moves further than the minimum distance.")
98
+ # Get the initial position of the agent from the agent's character in the scene.
99
+ if (Tracked := PropAgent.GetFortCharacter[]):
100
+ StartPosition:vector3 = Tracked.GetTransform().Translation
101
+ loop:
102
+ Sleep(0.0) # Get the position of the agent in the next game tick.
103
+ NewPosition := Tracked.GetTransform().Translation
104
+ # If the distance of the new position from the starting position is greater than or equal to MinimumDistance, the agent has moved and we break the loop.
105
+ if (Distance(StartPosition, NewPosition) >= MinimumDistance):
106
+ Logger.Print("Agent has moved more than or equal to the minimum distance.")
107
+ break
108
+
109
+ # Delays until HeartBeatWarningTime should start. Then counts down by HeartBeatWarningTime and sets the countdown text. Clears the text when deferred.
110
+ CountdownTimer(PropAgent:agent)<suspends>:void =
111
+ Logger.Print("Starting heart beat countdown.")
112
+ if (UIData := HeartBeat.WarningUI[PropAgent]):
113
+ Sleep(HeartBeat.MoveTime - HeartBeat.WarningTime) # Sleep for the amount of time before the warning appears.
114
+ Logger.Print("Starting heart beat warning.")
115
+ var WarningTimeRemaining:int = 0
116
+ if (set WarningTimeRemaining = Ceil[HeartBeat.WarningTime]) {}
117
+ # A defer happens when the function completes or if it is cancelled, such as if it loses a race.
118
+ # So in this case, the warning text is cleared when the countdown finishes or if the prop agent moves before the countdown finishes.
119
+ defer:
120
+ UIData.Text.SetText(HeartBeatWarningClear)
121
+ # Set the warning text to the time remaining, wait a second, and then decrement the time remaining. If the countdown completes, break the loop.
122
+ loop:
123
+ Logger.Print("Heart beat in {WarningTimeRemaining} seconds.")
124
+ UIData.Text.SetText(HeartBeatWarningMessage(WarningTimeRemaining))
125
+ Sleep(1.0)
126
+ set WarningTimeRemaining -= 1
127
+ if (WarningTimeRemaining <= 0):
128
+ break
129
+ else:
130
+ Logger.Print("UIData not found.")
131
+
132
+ # Turns on the heart beat VFX and SFX. Waits infinitely until deferred then disables the heart beat VFX and SFX.
133
+ (PropAgent:agent).StartHeartbeat()<suspends>:void =
134
+ Logger.Print("Spawning heart beat.")
135
+ # Save the heartbeat data so we can pass it in the defer later, after the PropAgent is destroyed or leaves the game.
136
+ var HeartBeatVFXData:heartbeat_vfx = heartbeat_vfx{}
137
+ if:
138
+ # Get the index of the prop agent in the PropAgents array to then access the corresponding heart beat VFX actor.
139
+ Index := FindOnTeam[PropAgent]
140
+ set HeartBeatVFXData = HeartBeat.AgentVFX[Index]
141
+ then:
142
+ HeartBeat.Enable(PropAgent, HeartBeatVFXData)
143
+ # When this function is cancelled by the prop agent moving, being eliminated, or the player leaving the session, disable the heartbeat.
144
+ defer:
145
+ HeartBeat.Disable(PropAgent, HeartBeatVFXData)
146
+ Sleep(Inf) # Do not stop sleeping until the race completes.
147
+
Code_Reference/PropHunt/txt/round_timer.txt ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Devices }
2
+ using { /Fortnite.com/UI }
3
+ using { /UnrealEngine.com/Temporary/Diagnostics }
4
+ using { /UnrealEngine.com/Temporary/SpatialMath }
5
+ using { /UnrealEngine.com/Temporary/UI }
6
+ using { /Verse.org/Colors }
7
+ using { /Verse.org/Simulation }
8
+
9
+ log_round_timer_device := class(log_channel){}
10
+
11
+ # An int that allows values between the ranges specified. This type is required by player_ui_slot.ZOrder.
12
+ round_int_clamped := type{_X:int where 0 <= _X, _X <= 2147483647}
13
+
14
+ # This message is used to print the time remaining before a round ends.
15
+ TimeRemainingMessage<localizes>(Minutes:string, Seconds:string):message = "{Minutes}:{Seconds}"
16
+
17
+ <#
18
+ This class contains all the logic for managing the round time and displaying the time to the screen.
19
+ You can use this device with a round_settings_device to actually end the round.
20
+ This device manages time without the use of a timer.
21
+ To use this class:
22
+ 1) Add the file to your project.
23
+ 2) Compile Verse Code from the Verse menu on the toolbar.
24
+ 3) Drag the device into your island from your island's Content/Creative Devices folder in the Content Browser.
25
+ 4) Include the waiting_for_more_players class in another Verse script with:
26
+ @editable
27
+ RoundTimer:round_timer = round_timer{}
28
+ 5) Compile Verse Code from the Verse menu on the toolbar.
29
+ 6) Connect the device you made in step 3 to the Verse device.
30
+ 7) Start the round timer with the following Verse:
31
+ RoundTimer.Start()
32
+ 8) Restart or Stop the timer with the equivalent functions.
33
+ 9) Wait for the timer to start with:
34
+ RoundTimer.AwaitStart()
35
+ 10) Wait for the timer to complete with:
36
+ RoundTimer.AwaitEnd()
37
+ Call the EndRound function on a round_settings_device to actually end the game round.
38
+ #>
39
+ round_timer := class(creative_device):
40
+ Logger:log = log{Channel:=log_round_timer_device}
41
+
42
+ @editable # The time, in minutes, a round lasts.
43
+ RoundTimeInMinutes:float = 5.0
44
+
45
+ @editable # The horizontal and vertical position of the timer UI on screen. X 0-1 is left-right and Y 0-1 is top-bottom.
46
+ UIPosition:vector2 = vector2{X:= 0.98, Y:=0.13}
47
+
48
+ @editable # The horizontal and vertical position of the timer UI on screen. X 0-1 is left-right and Y 0-1 is top-bottom.
49
+ UIAlignment:vector2 = vector2{X := 1.0, Y := 0.0}
50
+
51
+ @editable # The ZOrder of the UI compared to other UI elements.
52
+ UIZOrder:round_int_clamped = 4
53
+
54
+ # Signaled when the round has been started.
55
+ RoundStarted:event() = event(){}
56
+
57
+ # Signaled when the round is about to be ended.
58
+ RoundEndedEvent:event() = event(){}
59
+
60
+ # This map associates a text box for displaying the time to each player.
61
+ var TimeRemainingTextBlocks:[player]text_block = map{}
62
+
63
+ # The time remaining before the round completes, as an integer.
64
+ var TimeRemainingInSeconds:int = 0
65
+
66
+ # Waits until the round timer is started.
67
+ AwaitStart()<suspends>:void =
68
+ RoundStarted.Await()
69
+ Logger.Print("Round timer started.")
70
+
71
+ # Used to start the round timer.
72
+ Start():void =
73
+ Logger.Print("Starting the round timer.")
74
+ RoundStarted.Signal()
75
+ set TimeRemainingInSeconds = GetRoundTimeInSeconds()
76
+ spawn{ Running() }
77
+
78
+ # Restarts the timer to RoundTime
79
+ Restart():void =
80
+ Logger.Print("Restarting the round timer.")
81
+ set TimeRemainingInSeconds = GetRoundTimeInSeconds()
82
+
83
+ # Runs the timer logic.
84
+ Running()<suspends>:void =
85
+ Logger.Print("Round timer running.")
86
+ loop:
87
+ UpdateTimeUI()
88
+ Sleep(1.0)
89
+ # Decrement TimeRemaining by 1 second then check if the time has run out. If so, end the round.
90
+ set TimeRemainingInSeconds -= 1
91
+ if (TimeRemainingInSeconds < 0):
92
+ Stop()
93
+ break
94
+
95
+ # Stops the timer and ends the round.
96
+ Stop():void =
97
+ Logger.Print("Ending the round timer.")
98
+ # We get a player from the players remaining in the scene to end the round.
99
+ Players:[]player = GetPlayspace().GetPlayers()
100
+ if (Instigator := Players[0]):
101
+ RoundEndedEvent.Signal()
102
+
103
+ # Waits until the round timer is just about to end.
104
+ AwaitEnd()<suspends>:void =
105
+ RoundEndedEvent.Await()
106
+ Logger.Print("Round timer ended.")
107
+
108
+ # Accepts a time value in minutes and returns the value in seconds.
109
+ GetRoundTimeInSeconds():int =
110
+ var InSeconds:int = 0
111
+ if (set InSeconds = Round[RoundTimeInMinutes * 60.0]) {}
112
+ InSeconds
113
+
114
+ # When the timer completes, update the time remaining and check if time has expired.
115
+ UpdateTimeUI():void =
116
+ # Set Minutes to TimeRemainingInSeconds/60 without the remainder.
117
+ var Minutes:int = 0
118
+ if (set Minutes = Floor(TimeRemainingInSeconds / 60)) {}
119
+ # Set Seconds to the remainder of TimeRemainingInSeconds/60.
120
+ var Seconds:int = 0
121
+ if (set Seconds = Mod[TimeRemainingInSeconds, 60]) {}
122
+ # Convert Minutes and Seconds to strings.
123
+ MinutesAsString := string("{Minutes}")
124
+ # If Seconds < 10, then we need to add a 0 in front of the value so it displays as :0# instead of :#
125
+ SecondsAsString := if (Seconds < 10) then Join(array{string("{0}"),string("{Seconds}")},string()) else string("{Seconds}")
126
+
127
+ # Iterate through all players, check if they have a TimeRemainingTextBlock, if not, give them one. Then update the text.
128
+ Players:[]player = GetPlayspace().GetPlayers()
129
+ for (Player : Players):
130
+ var TextBlock:text_block = text_block{}
131
+ if (set TextBlock = TimeRemainingTextBlocks[Player]):
132
+ else:
133
+ Logger.Print("Creating new round time text block.")
134
+ set TextBlock = SetUpTimeRemainingUI(Player)
135
+ TextBlock.SetText(TimeRemainingMessage(MinutesAsString, SecondsAsString))
136
+
137
+ # Accepts a player and then adds a round time ui canvas to their screen and stores their TimeRemainingTextBlock for updating later.
138
+ SetUpTimeRemainingUI(Player:player):text_block =
139
+ Logger.Print("Adding round timer UI to a player.")
140
+ # This is the text_block that prints the time remaining text to the screen.
141
+ TextBlock:text_block = text_block:
142
+ DefaultTextColor := NamedColors.White
143
+ DefaultShadowColor := NamedColors.Black
144
+ if (PlayerUI := GetPlayerUI[Player]):
145
+ if (set TimeRemainingTextBlocks[Player] = TextBlock) {}
146
+ # This is the canvas that holds and positions the text_block on the screen.
147
+ Canvas := canvas:
148
+ Slots := array:
149
+ canvas_slot:
150
+ Anchors := anchors{Minimum := UIPosition, Maximum := UIPosition}
151
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
152
+ Alignment := UIAlignment
153
+ SizeToContent := true
154
+ Widget := TextBlock
155
+ # The canvas is assigned to the player.
156
+ PlayerUI.AddWidget(Canvas, player_ui_slot{ZOrder := UIZOrder})
157
+ # The text_block is returned so it can be saved to the map and updated later as time ticks down.
158
+ return TextBlock
Code_Reference/PropHunt/txt/waiting_for_more_players.txt ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/UI }
4
+ using { /UnrealEngine.com/Temporary/Diagnostics }
5
+ using { /UnrealEngine.com/Temporary/SpatialMath }
6
+ using { /UnrealEngine.com/Temporary/UI }
7
+ using { /Verse.org/Colors }
8
+ using { /Verse.org/Simulation }
9
+
10
+ log_waiting_for_more_players_device := class(log_channel){}
11
+
12
+ # An int that allows values between the ranges specified. This type is required by player_ui_slot.ZOrder.
13
+ waiting_int_clamped := type{_X:int where 0 <= _X, _X <= 2147483647}
14
+
15
+ # This message is used to print the number of players needed before a round can begin.
16
+ WaitingForMorePlayersMessage<localizes>(Count:int):message = "Waiting for {Count} more Player(s)"
17
+
18
+ # This class is for showing how many players are needed to start the round.
19
+ waiting_for_more_players_ui := class:
20
+ var Canvas:canvas
21
+ var TextBlock:text_block
22
+
23
+ <#
24
+ This class contains all the logic for setting a minimum number of players and checking if there are enough to start the round.
25
+ To use this class:
26
+ 1) Add the file to your project.
27
+ 2) Compile Verse Code from the Verse menu on the toolbar.
28
+ 3) Drag the device into your island from your island's Content/Creative Devices folder in the Content Browser.
29
+ 4) Connect a Timer device to this device's "WaitingForMorePlayersTimer" property.
30
+ 5) Include the waiting_for_more_players class in another Verse script with:
31
+ @editable
32
+ WaitingForMorePlayers:waiting_for_more_players = waiting_for_more_players{}
33
+ 6) Compile Verse Code from the Verse menu on the toolbar.
34
+ 7) Connect the device you made in step 3 to the Verse device and property you exposed in step 6.
35
+ 8) Await the CheckForMinimumNumberOfPlayers functions by passing it a player. For example:
36
+ Players = GetPlayspace().GetPlayers()
37
+ CheckForMinimumNumberOfPlayers(Players)
38
+ 9) On IslandSettings, Set the Game Start Countdown to 0.0.
39
+ #>
40
+ waiting_for_more_players := class(creative_device):
41
+ Logger:log = log{Channel:=log_waiting_for_more_players_device}
42
+
43
+ @editable # The minimum number of players needed in the match for a round to start.
44
+ MinimumNumberOfPlayers:int = 2
45
+
46
+ @editable # The horizontal and vertical position of the timer UI on screen. X 0-1 is left-right and Y 0-1 is top-bottom.
47
+ UIPosition:vector2 = vector2{X:= 0.5, Y:=0.4}
48
+
49
+ @editable # The horizontal and vertical position of the timer UI on screen. X 0-1 is left-right and Y 0-1 is top-bottom.
50
+ UIAlignment:vector2 = vector2{X := 0.5, Y := 0.5}
51
+
52
+ @editable # The ZOrder of the UI compared to other UI elements.
53
+ UIZOrder:waiting_int_clamped = 3
54
+
55
+ @editable # This timer is used to countdown to round start after waiting for players to join the match.
56
+ WaitingForMorePlayersTimer:timer_device = timer_device{}
57
+
58
+ # This map associates a UI canvas for displaying the number of players needed to start a round to each player.
59
+ var WaitingForMorePlayersUI:[player]?waiting_for_more_players_ui = map{}
60
+
61
+ # Check if there are enough players to start the round. If not, wait until number of players >= MinimumNumberOfPlayers.
62
+ WaitForMinimumNumberOfPlayers(Players:[]player)<suspends>:[]player =
63
+ Logger.Print("Waiting if there are enough players for the round to start.")
64
+ # Creating a new variable so I can modify it as more players join. Initializing it with an array of players passed to the function.
65
+ var PlayersWaiting:[]player = Players
66
+ # If the number of players is less than the minimum needed to start the round...
67
+ if (PlayersWaiting.Length < MinimumNumberOfPlayers):
68
+ loop: # Loop until the number of players is greater than or equal to the minimum needed.
69
+ Logger.Print("{PlayersWaiting.Length}/{MinimumNumberOfPlayers} players needed for the round to start.")
70
+ # Update the waiting for more players UI.
71
+ DisplayWaitingForMorePlayers(PlayersWaiting)
72
+ Sleep(2.0) # Wait to see if more players join the match then check if there are enough players to start the round.
73
+ set PlayersWaiting = GetPlayspace().GetPlayers()
74
+ if (PlayersWaiting.Length >= MinimumNumberOfPlayers):
75
+ # If there are now enough players, clear the waiting for more players UI,
76
+ Logger.Print("{PlayersWaiting.Length}/{MinimumNumberOfPlayers} players in round, preparing for round start.")
77
+ ClearWaitingForMorePlayers()
78
+ # Then break out of the loop.
79
+ break
80
+ # start the round start countdown, and wait until the countdown completes.
81
+ WaitingForMorePlayersTimer.Start()
82
+ WaitingForMorePlayersTimer.SuccessEvent.Await()
83
+ Logger.Print("Starting round.")
84
+ # Return the list of players in the session.
85
+ return PlayersWaiting
86
+
87
+ # Displays a "Waiting for more players" UI message for each player if they don't already have one. Updates the player counter for all players.
88
+ DisplayWaitingForMorePlayers(Players:[]player):void =
89
+ PlayersNeededCount := MinimumNumberOfPlayers - Players.Length
90
+ Logger.Print("{Players.Length} players in round, waiting for {PlayersNeededCount} more player(s) to join.")
91
+ for (Player: Players):
92
+ # If the player has a WaitingForMorePlayersUI, get the TextBlock and refresh the text so it shows the correct number of players needed to start the round.
93
+ if (UIData := WaitingForMorePlayersUI[Player]?):
94
+ UIData.TextBlock.SetText(WaitingForMorePlayersMessage(PlayersNeededCount))
95
+ # Else create a WaitingForMorePlayersUI for the player.
96
+ else:
97
+ SetUpWaitingForMorePlayersUI(Player, PlayersNeededCount)
98
+
99
+ # Accepts a player and player_ui and adds a waiting for more players ui canvas to their screen.
100
+ SetUpWaitingForMorePlayersUI(Player:player, PlayersNeededCount:int):void =
101
+ Logger.Print("Creating 'waiting for more players' UI.")
102
+ if (PlayerUI := GetPlayerUI[Player]):
103
+ # This is the text_block that prints the waiting for more players text to the screen.
104
+ TextBlock:text_block = text_block:
105
+ DefaultText := WaitingForMorePlayersMessage(PlayersNeededCount)
106
+ DefaultTextColor := NamedColors.White
107
+ DefaultShadowColor := NamedColors.Black
108
+ # This is the canvas that holds and positions the text_block on the screen.
109
+ Canvas := canvas:
110
+ Slots := array:
111
+ canvas_slot:
112
+ Anchors := anchors{Minimum := UIPosition, Maximum := UIPosition}
113
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
114
+ Alignment := UIAlignment
115
+ SizeToContent := true
116
+ Widget := TextBlock
117
+ # The canvas is assigned to the player.
118
+ PlayerUI.AddWidget(Canvas, player_ui_slot{ZOrder := UIZOrder})
119
+ # The text_block is saved to the map so we can update the text later as more players join the game.
120
+ if (set WaitingForMorePlayersUI[Player] = option{ waiting_for_more_players_ui{Canvas := Canvas, TextBlock := TextBlock} }) {}
121
+
122
+ # Removes the "Waiting for more players" UI message for each player who has one.
123
+ ClearWaitingForMorePlayers():void =
124
+ Logger.Print("Clearing 'waiting for more players' UI.")
125
+ for (Player -> UIData : WaitingForMorePlayersUI):
126
+ if:
127
+ PlayerUI := GetPlayerUI[Player]
128
+ Canvas := UIData?.Canvas
129
+ set WaitingForMorePlayersUI[Player] = false
130
+ then:
131
+ PlayerUI.RemoveWidget(Canvas)
Code_Reference/PropHunt/verse/base_team.verse ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Characters }
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/UI }
4
+ using { /UnrealEngine.com/Temporary/Diagnostics }
5
+ using { /UnrealEngine.com/Temporary/SpatialMath }
6
+ using { /UnrealEngine.com/Temporary/UI }
7
+ using { /Verse.org/Colors }
8
+ using { /Verse.org/Simulation }
9
+
10
+ log_team := class(log_channel){}
11
+
12
+ # This class defines the devices needed for the different teams in the experience.
13
+ # This class is abstract so it cannot be used on its own. It has to be inherited by another class.
14
+ base_team := class<abstract>:
15
+ Logger:log = log{Channel:=log_team}
16
+
17
+ @editable # Used to set a player to the team.
18
+ ClassSelector:class_and_team_selector_device = class_and_team_selector_device{}
19
+
20
+ @editable # Used to award score to agents on the team.
21
+ ScoreManager:score_manager_device = score_manager_device{}
22
+
23
+ @editable # Used to display the team assignment title.
24
+ TeamTitle:hud_message_device = hud_message_device{}
25
+
26
+ @editable # Used to display the team assignment description.
27
+ TeamDescription:hud_message_device = hud_message_device{}
28
+
29
+ @editable # Used to subscribe to team member (prop team) or enemy (hunter team) eliminated events.
30
+ TeamManager:team_settings_and_inventory_device = team_settings_and_inventory_device{}
31
+
32
+ # This is an array of agents on the team.
33
+ var TeamAgents<private>:[]agent = array{}
34
+
35
+ # This event is signaled when the TeamAgents array becomes empty (signaling the end of the round).
36
+ TeamEmptyEvent:event() = event(){}
37
+
38
+ # Returns the current TeamAgents array.
39
+ # This is required because the TeamAgents array is private, so other classes cannot access it directly.
40
+ GetAgents()<decides><transacts>:[]agent =
41
+ TeamAgents
42
+
43
+ # Return the size of the TeamAgents array
44
+ # This requires a function because the TeamAgents array is private, so other classes cannot access it directly.
45
+ Count()<transacts>:int =
46
+ TeamAgents.Length
47
+
48
+ # Returns an index in the TeamAgents array of an agent, fails otherwise.
49
+ FindOnTeam(Agent:agent)<decides><transacts>: int =
50
+ Index := TeamAgents.Find[Agent]
51
+
52
+ # Set the agent to the team and notify the player.
53
+ InitializeAgent(Agent:agent):void =
54
+ AddAgentToTeam(Agent)
55
+ ClassSelector.ChangeTeamAndClass(Agent)
56
+ DisplayTeamInformation(Agent)
57
+
58
+ # Add an agent to TeamAgents.
59
+ AddAgentToTeam(AgentToAdd:agent):void =
60
+ if (not FindOnTeam[AgentToAdd]):
61
+ Logger.Print("Adding agent to team.")
62
+ set TeamAgents += array{AgentToAdd}
63
+
64
+ # Activates hud message devices to show the player what team they are on
65
+ DisplayTeamInformation(Agent:agent):void =
66
+ TeamTitle.Show(Agent)
67
+ TeamDescription.Show(Agent)
68
+
69
+ # When an agent leaves the match, remove them from the TeamAgents array and check for the end of the round.
70
+ EliminateAgent(Agent:agent)<suspends>:void =
71
+ Sleep(0.0) # Delaying 1 game tick to ensure the player is respawned before proceeding.
72
+ RemoveAgentFromTeam(Agent)
73
+
74
+ # Remove an agent from TeamAgents.
75
+ # If the agent removed was the last, signal TeamEmptyEvent.
76
+ RemoveAgentFromTeam(AgentToRemove:agent):void =
77
+ set TeamAgents = TeamAgents.RemoveAllElements(AgentToRemove)
78
+ Logger.Print("{Count()} agent(s) on team remaining.")
79
+ if (Count() < 1):
80
+ Logger.Print("No agents on team remaining. Ending the round.")
81
+ TeamEmptyEvent.Signal()
Code_Reference/PropHunt/verse/billboard_cinematic_flythrough.verse ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Characters }
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/FortPlayerUtilities }
4
+ using { /Fortnite.com/UI }
5
+ using { /UnrealEngine.com/Temporary/Diagnostics }
6
+ using { /UnrealEngine.com/Temporary/SpatialMath}
7
+ using { /UnrealEngine.com/Temporary/UI }
8
+ using { /Verse.org/Simulation }
9
+ using { /Verse.org/Simulation/Tags }
10
+
11
+ # Gameplay tag for objects that should be displayed or hidden during cinematic
12
+ cinematic_objects := class(tag):
13
+
14
+ # Script that sets the player into a cinematic mode so its not in the cinematic, and then resets them when the cinematic is done
15
+ billboard_cinematic := class<concrete>:
16
+
17
+ # A reference to the device that contains the level sequence to be played
18
+ @editable
19
+ CinematicSequence:cinematic_sequence_device := cinematic_sequence_device{}
20
+
21
+ # A reference to the device that will hide the HUD
22
+ @editable
23
+ HUDController:hud_controller_device := hud_controller_device{}
24
+
25
+ # Should the Skip Intro button be visible during the cinematic
26
+ @editable
27
+ ShowUI:logic := true
28
+
29
+ # Should Devices and Billboards be visible during the cinematic
30
+ @editable
31
+ ShowCreativeObjects:logic := true
32
+
33
+ # Should play the cinematic at the beginning of game
34
+ @editable
35
+ PlayAtStart:logic = true
36
+
37
+ # Reference to the Radio that plays music
38
+ @editable
39
+ Music:radio_device := radio_device{}
40
+
41
+ # Signaled when the cinematic has completed.
42
+ CinematicCompleted<public>:event() = event(){}
43
+
44
+ # Runs at the beginning of the script.
45
+ EnterCinematicModeForAll<public>(InPlayers:[]player):void=
46
+ HUDController.Enable()
47
+
48
+ if (ShowCreativeObjects?):
49
+ ShowObjectsInCinematic()
50
+ else:
51
+ HideObjectsInCinematic()
52
+
53
+ for:
54
+ Player : InPlayers
55
+ not Player.IsSpectator[]
56
+ do:
57
+ spawn{EnterCinematicMode(Player)}
58
+
59
+ Music.Play()
60
+
61
+ # Sets players to visible, re-enables their movement, and removes the skip button.
62
+ EndCinematicModeForAll<public>(InPlayers:[]player):void=
63
+ Music.Stop()
64
+ for:
65
+ Player : InPlayers
66
+ not Player.IsSpectator[]
67
+ do:
68
+ EndCinematicMode(Player)
69
+
70
+ HUDController.Disable()
71
+ HideObjectsInCinematic()
72
+
73
+ # UI to show during the cinematic
74
+ CinematicUI<private>:cinematic_ui = cinematic_ui{}
75
+
76
+ # Handler for player choosing to skip cinematic
77
+ HandleSkipIntroInteraction<private>(WidgetMessage:widget_message):void=
78
+ EndCinematicMode(WidgetMessage.Player)
79
+
80
+ # Sets players to invisible, removes their movement, optionally gives them a skip button, then waits for the cinematic to end before signaling that it has completed.
81
+ EnterCinematicMode<private>(Player:player)<suspends>:void=
82
+ HidePlayer(Player)
83
+
84
+ if:
85
+ ShowUI?
86
+ PlayerUI := GetPlayerUI[Player]
87
+ then:
88
+ Listenable := CinematicUI.Show(Player, PlayerUI)
89
+ Listenable.Subscribe(HandleSkipIntroInteraction)
90
+
91
+ CinematicSequence.Play(Player)
92
+
93
+ CinematicSequence.StoppedEvent.Await()
94
+ CinematicCompleted.Signal()
95
+
96
+ # Stop cinematic for player
97
+ EndCinematicMode<private>(Player:player):void=
98
+ CinematicSequence.Stop(Player)
99
+ ShowPlayer(Player)
100
+ CinematicUI.Hide(Player)
101
+
102
+ # Make player invisible and unmoveable
103
+ HidePlayer<private>(Player:player):void=
104
+ if (FortCharacter := Player.GetFortCharacter[]):
105
+ CinematicStasis := stasis_args{AllowTurning := false, AllowFalling := false, AllowEmotes := false}
106
+
107
+ FortCharacter.PutInStasis(CinematicStasis)
108
+ FortCharacter.Hide()
109
+
110
+ # Make player visible and moveable
111
+ ShowPlayer<private>(Player:player):void=
112
+ if (FortCharacter := Player.GetFortCharacter[]):
113
+ FortCharacter.ReleaseFromStasis()
114
+ FortCharacter.Show()
115
+
116
+ # Find all objects tagged and make them visible
117
+ ShowObjectsInCinematic<private>():void=
118
+ ObjectsToShow := GetCreativeObjectsWithTag(cinematic_objects{})
119
+
120
+ for (Object : ObjectsToShow):
121
+ if (Billboard := billboard_device[Object]):
122
+ Billboard.ShowText()
123
+ else if (Device := creative_device[Object]):
124
+ Device.Show()
125
+ else if (Radio := radio_device[Object]):
126
+ Radio.Show()
127
+ else if (AudioPlayer := audio_player_device[Object]):
128
+ AudioPlayer.Show()
129
+
130
+ # Find all objects tagged and make them invisible
131
+ HideObjectsInCinematic<private>():void=
132
+ ObjectsToHide := GetCreativeObjectsWithTag(cinematic_objects{})
133
+
134
+ for (Object : ObjectsToHide):
135
+ if (Billboard := billboard_device[Object]):
136
+ Billboard.HideText()
137
+ else if (Device := creative_device[Object]):
138
+ Device.Hide()
139
+ else if (Radio := radio_device[Object]):
140
+ Radio.Hide()
141
+ else if (AudioPlayer := audio_player_device[Object]):
142
+ AudioPlayer.Hide()
143
+
144
+ # Class in charge of UI for cinematic
145
+ cinematic_ui := class:
146
+
147
+ # Create a canvas widget that displays a skip button for the cinematic
148
+ CreateUI():tuple(canvas, listenable(widget_message))=
149
+ SkipButton:button_regular = button_regular{DefaultText := SkipCinematicText}
150
+ SkipButton.SetEnabled(true)
151
+
152
+ UICanvas:canvas = canvas:
153
+ Slots := array:
154
+ canvas_slot:
155
+ Anchors := anchors{Minimum := vector2{X := 0.9, Y := 0.9}, Maximum := vector2{X := 0.9, Y := 0.9}}
156
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
157
+ Alignment := vector2{X := 1.0, Y := 1.0}
158
+ SizeToContent := true
159
+ Widget := SkipButton
160
+
161
+ return (UICanvas, SkipButton.OnClick())
162
+
163
+ # Shows the UI for the player and returns the listenable for the button
164
+ Show(Player:player, PlayerUI:player_ui):listenable(widget_message)=
165
+ UIResult := CreateUI()
166
+ UICanvas := UIResult(0)
167
+ Listenable := UIResult(1)
168
+
169
+ PlayerUISlot := player_ui_slot{ZOrder := 0, InputMode := ui_input_mode.All}
170
+ PlayerUI.AddWidget(UICanvas, PlayerUISlot)
171
+ if (set UIPerPlayer[Player] = option{UICanvas}):
172
+
173
+ return Listenable
174
+
175
+ # Hides the UI for the player
176
+ Hide(Player:player):void=
177
+ if:
178
+ PlayerUI := GetPlayerUI[Player]
179
+ CustomUI := UIPerPlayer[Player]?
180
+ then:
181
+ PlayerUI.RemoveWidget(CustomUI)
182
+ if (set UIPerPlayer[Player] = false):
183
+
184
+ # A localizable message to display as text in the UI
185
+ SkipCinematicText<localizes><private>:message = "Skip Intro"
186
+
187
+ var UIPerPlayer<private>:[player]?canvas = map{}
Code_Reference/PropHunt/verse/heartbeat.verse ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Characters }
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/UI }
4
+ using { /UnrealEngine.com/Temporary/SpatialMath }
5
+ using { /UnrealEngine.com/Temporary/Diagnostics }
6
+ using { /UnrealEngine.com/Temporary/UI }
7
+ using { /Verse.org/Colors }
8
+ using { /Verse.org/Simulation }
9
+
10
+ log_heart_beat := class(log_channel){}
11
+
12
+ # These messages are used to notify a prop agent with a message (or to hide it) when they need to move to avoid their heart beat from becoming visible.
13
+ HeartBeatWarningMessage<localizes>(Time:int):message = "Heart Beat in {Time} Seconds. Move!"
14
+ HeartBeatWarningClear<localizes>:message = ""
15
+
16
+ # This class exposed the editable properties for the heartbeat to the prop_hunt device.
17
+ heart_beat := class<concrete>():
18
+ Logger:log = log{Channel:=log_heart_beat}
19
+
20
+ @editable # The number of seconds before a prop agent must move before the heart beat reveals their position.
21
+ MoveTime:float = 15.0
22
+
23
+ @editable # The seconds remaining before the heart beat warning appears. Shouldn't be > than HeartBeatTimer.
24
+ WarningTime:float = 5.0
25
+
26
+ @editable # An array of heart beat VFX devices. There is one per player.
27
+ AgentVFX:[]heartbeat_vfx = array{}
28
+
29
+ @editable # The audio player device used to play the heart beat sound effects (SFX).
30
+ SFXPlayer:radio_device = radio_device{}
31
+
32
+ # This map associates a UI for displaying the heart beat warning to each prop agent.
33
+ var WarningUI:[agent]heartbeat_warning_ui = map{}
34
+
35
+ # Keeps track of how many players have an active heartbeat so we can manage the SFX device.
36
+ var NumberOfHeartBeats:int = 0
37
+
38
+ # Sets up heart beat UI for the agent.
39
+ SetUpUI(PropAgent:agent):void =
40
+ if:
41
+ not WarningUI[PropAgent]
42
+ AsPlayer := player[PropAgent]
43
+ PlayerUI := GetPlayerUI[AsPlayer]
44
+ then:
45
+ UIData:heartbeat_warning_ui = heartbeat_warning_ui{}
46
+ UIData.CreateCanvas()
47
+ PlayerUI.AddWidget(UIData.Canvas, player_ui_slot{ZOrder := 1})
48
+ if (set WarningUI[PropAgent] = UIData) {}
49
+
50
+ # Activates the heartbeat VFX and SFX for the specified player.
51
+ Enable(PropAgent:agent, HeartBeatVFXData:heartbeat_vfx):void =
52
+ if:
53
+ # Get the character, which is used to find the prop agent's position in the scene.
54
+ Character := PropAgent.GetFortCharacter[]
55
+ then:
56
+ # Set the heart beat VFX's position to the prop agent's position.
57
+ HeartBeatVFXData.Activate(Character.GetTransform())
58
+ # Increment the heartbeat count, and if this is the first heartbeat playing, we need to play the audio to get it started.
59
+ set NumberOfHeartBeats += 1
60
+ if (NumberOfHeartBeats = 1) then SFXPlayer.Play()
61
+ # Register the prop agent to the audio player device so the heart beat audio will play from that position.
62
+ SFXPlayer.Register(PropAgent)
63
+ else:
64
+ Logger.Print("Character, Index, or HeartBeatVFXData not found. Cannot start the heartbeat")
65
+
66
+ # Clears the heartbeat VFX and SFX for the specified prop agent.
67
+ Disable(PropAgent:agent, HeartBeatVFXData:heartbeat_vfx):void =
68
+ Logger.Print("Disabling heart beat.")
69
+ # Deactivate the VFX visuals.
70
+ HeartBeatVFXData.Deactivate()
71
+ # Unregister the prop agent from the audio player device, causing the heart beat audio to stop.
72
+ SFXPlayer.Unregister(PropAgent)
73
+ # Decrement the heartbeat counter. This counter should never drop below 0.
74
+ set NumberOfHeartBeats -= 1
75
+ if (NumberOfHeartBeats < 0) then set NumberOfHeartBeats = 0
76
+
77
+ # Clears all heartbeat VFX and SFX for all prop agents.
78
+ DisableAll():void =
79
+ Logger.Print("Disabling all heart beats.")
80
+ # Iterate through all VFX and move them to 0,0,0.
81
+ for (HeartBeatVFXDevice : AgentVFX):
82
+ HeartBeatVFXDevice.Deactivate()
83
+ # Unregister all players from the heart beat audio.
84
+ SFXPlayer.UnregisterAll()
85
+ # Reinitialize the heartbeat counter to 0
86
+ set NumberOfHeartBeats = 0
87
+
88
+ # The heartbeat_warning_ui class contains a struct of data to track the UI canvas and text_block per player as well as the function to create a new heartbeat warning UI canvas.
89
+ heartbeat_warning_ui := class:
90
+ var Canvas:canvas = canvas{}
91
+ var Text:text_block = text_block{}
92
+
93
+ # Creates the UI canvas for the warning message.
94
+ CreateCanvas():void =
95
+ set Text = text_block{DefaultTextColor := NamedColors.White, DefaultShadowColor := NamedColors.Black}
96
+ set Canvas = canvas:
97
+ Slots := array:
98
+ canvas_slot:
99
+ Anchors := anchors{Minimum := vector2{X := 0.5, Y := 0.75}, Maximum := vector2{X := 0.5, Y := 0.75}}
100
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
101
+ Alignment := vector2{X := 0.5, Y := 1.0}
102
+ SizeToContent := true
103
+ Widget := Text
104
+
105
+ # The heartbeat_vfx class contains a struct of data to track the VFX's root and vfx_spawner_device objects per player as well as the functions to set the VFX to a position or reset it.
106
+ heartbeat_vfx := class<concrete>:
107
+ @editable # The VFX device for each heart beat.
108
+ VFXDevice:vfx_spawner_device = vfx_spawner_device{}
109
+
110
+ # This offset is used to position the heartbeat above a prop agent's head.
111
+ HeartBeatVFXOffset:vector3 = vector3{X := 0.0, Y := 0.0, Z := 110.0}
112
+
113
+ # Sets the position of the heart beat VFX then enables the VFX.
114
+ Activate(Transform:transform):void =
115
+ VFXPosition := Transform.Translation + HeartBeatVFXOffset
116
+ if (VFXDevice.TeleportTo[VFXPosition, Transform.Rotation]):
117
+ VFXDevice.Enable()
118
+
119
+ # Disables the VFX, hiding the heart beat visuals.
120
+ Deactivate():void =
121
+ VFXDevice.Disable()
Code_Reference/PropHunt/verse/hunter_team.verse ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Devices }
2
+ using { /UnrealEngine.com/Temporary/Diagnostics }
3
+ using { /Verse.org/Simulation }
4
+
5
+ # Inheriting from the base_team class, The hunter_team class contains the device definitions and functions related to the hunter team and its agents.
6
+ hunter_team := class<concrete>(base_team):
7
+ @editable # One hunter agent is created each round for each n players. Example: HunterTeamPerNumberOfPlayers = 5.0 is 1 per 5 players. If players = 6, 2 hunter agents are created.
8
+ HunterAgentPerNumberOfPlayers:float = 5.0 # Minimum 1.1 is enforced to ensure at least 1 prop agent is created.
9
+
10
+ @editable # The number of seconds before the hunter agents are spawned, giving the prop agents a head start to hide.
11
+ SpawnDelay:float = 15.0
12
+
13
+ @editable # The maximum base points a hunter agent gets for eliminating a prop agent. These points are divided by the number of prop agents remaining.
14
+ MaxEliminationScore:int = 5000
15
+
16
+ @editable # The timer device used to give props a grace period to hide.
17
+ WaitTimer:timer_device = timer_device{}
18
+
19
+ # Set the agent to a hunter agent.
20
+ InitializeAgent<override>(NewHunterAgent:agent):void =
21
+ Logger.Print("Setting a new hunter agent.")
22
+ (super:)InitializeAgent(NewHunterAgent)
23
+
24
+ # When a hunter agent leaves the match, remove them from the HunterAgents array and check for the end of the round.
25
+ # Notice that we're overriding this function because we don't need to pass extra data here like we do for prop team.
26
+ EliminateAgent<override>(HunterAgent:agent)<suspends>:void =
27
+ Logger.Print("Hunter agent eliminated.")
28
+ (super:)EliminateAgent(HunterAgent)
Code_Reference/PropHunt/verse/prop_hunt.verse ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Devices }
2
+ using { /Fortnite.com/UI }
3
+ using { /UnrealEngine.com/Temporary/Diagnostics }
4
+ using { /UnrealEngine.com/Temporary/SpatialMath }
5
+ using { /UnrealEngine.com/Temporary/UI }
6
+ using { /Verse.org/Colors }
7
+ using { /Verse.org/Random }
8
+ using { /Verse.org/Simulation }
9
+
10
+ log_prop_hunt_device := class(log_channel){}
11
+
12
+ # This class contains the logic for starting and ending a round. It also handles events for players joining and leaving when a round is in progress.
13
+ prop_hunt := class(creative_device):
14
+ Logger:log = log{Channel:=log_prop_hunt_device}
15
+
16
+ @editable # An instance of the prop_team class, used to manage the Prop team.
17
+ PropTeam:prop_team = prop_team{}
18
+
19
+ @editable # An instance of the hunter_team class, used to manage the Hunter team.
20
+ HunterTeam:hunter_team = hunter_team{}
21
+
22
+ @editable # The round manager device is used to end the round when the last prop agent is eliminated.
23
+ RoundSettings:round_settings_device = round_settings_device{}
24
+
25
+ @editable # The teleporter in the lobby area. This device is enabled and disabled to allow props and hunters out of the pre-game lobby.
26
+ LobbyTeleporter:teleporter_device = teleporter_device{}
27
+
28
+ @editable # The waiting for more players device. This device checks if there are enough players to start the round.
29
+ WaitingForMorePlayers:waiting_for_more_players = waiting_for_more_players{}
30
+
31
+ @editable # This hud controller is used when a round is waiting to start.
32
+ # Set Priority to "Highest" for this device and have a second HUD controller set to < "Highest" to use during the round.
33
+ HUDControllerWaiting:hud_controller_device = hud_controller_device{}
34
+
35
+ @editable # The round timer device. This device keeps track of the time left in the round and is started within StartTheHunt().
36
+ RoundTimer:round_timer = round_timer{}
37
+
38
+ @editable # The device that controls the game music.
39
+ GameMusic:radio_device = radio_device{}
40
+
41
+ @editable # Message for when the hunter agents are set loose in the game.
42
+ StartTheHuntMessage:hud_message_device = hud_message_device{}
43
+
44
+ @editable # The Verse class that controls the opening cinematic.
45
+ Cinematic:billboard_cinematic = billboard_cinematic{}
46
+
47
+ # When the device is started in a running game, initialize subscriptions and launch the team setup. This runs at the start of every round.
48
+ OnBegin<override>()<suspends>:void =
49
+ Sleep(1.0)
50
+ Logger.Print("Round loaded.")
51
+
52
+ # Play cinematic at beginning if user has it enabled
53
+ if (Cinematic.PlayAtStart?):
54
+ Cinematic.EnterCinematicModeForAll(GetPlayspace().GetPlayers())
55
+ Cinematic.CinematicCompleted.Await()
56
+ Cinematic.EndCinematicModeForAll(GetPlayspace().GetPlayers())
57
+ Logger.Print("Cinematic Completed.")
58
+ else:
59
+ Logger.Print("Cinematic Skipped.")
60
+
61
+ GameMusic.Play()
62
+ SetUpTeams()
63
+ race: # When there are no more prop or hunter agents (whichever happens first), or the round timer finishes, the round ends
64
+ PropTeam.TeamEmptyEvent.Await()
65
+ HunterTeam.TeamEmptyEvent.Await()
66
+ RoundTimer.AwaitEnd()
67
+ Logger.Print("Round ending.")
68
+ EndRound()
69
+
70
+ # When a round is started, subscribe to team devices, randomly pick the hunter agents, enable the hunter timer, set the prop agents, and teleport them into the game area.
71
+ SetUpTeams()<suspends>:void =
72
+ Logger.Print("Setting up teams.")
73
+
74
+ # Subscribe to the prop team score timer, set the score award, and subscribe to the prop team's eliminated event.
75
+ PropTeam.ScoreManager.SetScoreAward(PropTeam.ScorePerSecond)
76
+ PropTeam.TeamManager.TeamMemberEliminatedEvent.Subscribe(OnPropEliminated) # Occurs when a prop agent is eliminated.
77
+
78
+ # Subscribe to the hunter team's wait timer and set the duration. Also subscribe to the hunter team's elimination event.
79
+ HunterTeam.WaitTimer.SuccessEvent.Subscribe(HuntersGo)
80
+ HunterTeam.WaitTimer.SetMaxDuration(HunterTeam.SpawnDelay)
81
+ HunterTeam.TeamManager.EnemyEliminatedEvent.Subscribe(OnHunterEliminated) # Occurs when a hunter agent eliminates a prop agent.
82
+
83
+ # Initialize the starting hunter and prop agents arrays. Get the players and find the number of players in the server.
84
+ var StartingHunterAgents:[]agent = array{}
85
+ var StartingPropAgents:[]agent = array{}
86
+ var Players:[]player = GetPlayspace().GetPlayers()
87
+
88
+ # Enable the HUD appropriate for waiting for players.
89
+ HUDControllerWaiting.Enable()
90
+
91
+ # Check if there are enough players to start the round.
92
+ set Players = WaitingForMorePlayers.WaitForMinimumNumberOfPlayers(Players)
93
+ Logger.Print("Round started.")
94
+
95
+ # Disable the waiting HUD to use the next highest priority HUD.
96
+ HUDControllerWaiting.Disable()
97
+
98
+ # Now that the round has started, we need to handle players being added or removed from the match. Subscribe to those events.
99
+ GetPlayspace().PlayerAddedEvent().Subscribe(OnPlayerAdded)
100
+ GetPlayspace().PlayerRemovedEvent().Subscribe(OnPlayerRemoved)
101
+
102
+ # Calculate the number of starting hunter agents for the game based on the player count and the number of hunter agents per number of players.
103
+ NumberOfPlayers:float = 1.0 * Players.Length # Converts Players.Length to a float so it can be used in the next Ceil function.
104
+ if (NumberOfStartingHunters:int = Ceil[NumberOfPlayers / Max(1.1, HunterTeam.HunterAgentPerNumberOfPlayers)]):
105
+ # Shuffle the players and then slice the array to get the starting hunter agents. The remaining players are the starting prop agents.
106
+ var RandomizedPlayers:[]agent = Shuffle(Players)
107
+ if (set StartingHunterAgents = RandomizedPlayers.Slice[0,NumberOfStartingHunters]) {}
108
+ if (set StartingPropAgents = RandomizedPlayers.Slice[NumberOfStartingHunters,RandomizedPlayers.Length]) {}
109
+
110
+ # Iterate through the starting hunter agents and assign them to the hunter team. Then start the hunter wait timer.
111
+ Logger.Print("Setting {StartingHunterAgents.Length} hunter agent(s).")
112
+ for (StartingHunterAgent : StartingHunterAgents):
113
+ HunterTeam.InitializeAgent(StartingHunterAgent)
114
+ HunterTeam.WaitTimer.Start()
115
+
116
+ # Iterate through the starting prop agents and assign them to the prop team. Teleport them into the play area.
117
+ Logger.Print("Setting {StartingPropAgents.Length} prop agent(s).")
118
+ LobbyTeleporter.Enable()
119
+ for (StartingPropAgent : StartingPropAgents):
120
+ PropTeam.InitializeAgent(StartingPropAgent)
121
+ PropTeam.HeartBeat.SetUpUI(StartingPropAgent)
122
+ LobbyTeleporter.Activate(StartingPropAgent)
123
+ LobbyTeleporter.Disable()
124
+ # Set the props remaining tracker's Target and Value to the current number of props.
125
+ # In the future, we'll only update Value as props are eliminated.
126
+ PropTeam.PropsRemainingTracker.SetTarget(PropTeam.Count())
127
+ PropTeam.UpdatePropsRemainingTracker()
128
+
129
+ # When the hunter timer finishes, spawn the function that unleashes the hunter agents into the game and begins the round.
130
+ # HunterInstigator is needed for the event subscription but not used.
131
+ HuntersGo(HunterInstigator:?agent):void =
132
+ spawn{ StartTheHunt() }
133
+
134
+ # Enable the lobby teleporter, teleport all the hunter agents to the game area and then run the prop game loop for each prop agent.
135
+ StartTheHunt()<suspends>:void =
136
+ Logger.Print("Teleporting hunter agent(s).")
137
+ LobbyTeleporter.Enable()
138
+ Sleep(0.5) # Delaying half a second to give the teleporter time to enable.
139
+ for (HunterAgent : HunterTeam.GetAgents[]):
140
+ LobbyTeleporter.Activate(HunterAgent)
141
+ StartTheHuntMessage.Show(HunterAgent)
142
+ Logger.Print("Starting prop(s) game logic.")
143
+ for (PropAgent : PropTeam.GetAgents[]):
144
+ spawn {PropTeam.RunPropGameLoop(PropAgent)}
145
+ StartTheHuntMessage.Show(PropAgent)
146
+
147
+ # Signal that the round has started to start the round and scoring timers.
148
+ RoundTimer.Start()
149
+ PropTeam.ScoreTimer.Start()
150
+
151
+ # When a player joins the match mid-round, make them a hunter if the round has started.
152
+ OnPlayerAdded(Player:player):void =
153
+ Logger.Print("A player joined the game.")
154
+ if (PropTeam.Count() > 0): # if there are any prop agents, the round has started.
155
+ HunterTeam.InitializeAgent(Player)
156
+
157
+ # When a hunter agent eliminates a prop agent, award score. The score is divided by the number of prop agents remaining.
158
+ OnHunterEliminated(HunterAgent:agent):void =
159
+ Logger.Print("Hunter agent eliminated a prop agent.")
160
+ PropTeamSize := PropTeam.Count()
161
+ if (EliminationAward := Floor(HunterTeam.MaxEliminationScore / PropTeamSize)):
162
+ Logger.Print("Awarding {EliminationAward} points.")
163
+ HunterTeam.ScoreManager.SetScoreAward(EliminationAward)
164
+ HunterTeam.ScoreManager.Activate(HunterAgent)
165
+
166
+ # When a prop agent is eliminated, remove the prop from the prop team, check for round end, and set them as a hunter.
167
+ OnPropEliminated(PropAgent:agent):void =
168
+ Logger.Print("Prop agent eliminated.")
169
+ spawn{ PropTeam.EliminateAgent(PropAgent) }
170
+ HunterTeam.InitializeAgent(PropAgent)
171
+
172
+ # When a player leaves the match, check which team they were on and then check for round end.
173
+ OnPlayerRemoved(Player:player):void =
174
+ Logger.Print("A player left the game.")
175
+ if (PropTeam.FindOnTeam[Player]):
176
+ Logger.Print("Player was a Prop.")
177
+ spawn{ PropTeam.EliminateAgent(Player) }
178
+ if (HunterTeam.FindOnTeam[Player]):
179
+ Logger.Print("Player was a Hunter.")
180
+ spawn{ HunterTeam.EliminateAgent(Player) }
181
+
182
+ # Cleans up the heart beat VFX and then ends the round.
183
+ EndRound():void=
184
+ PropTeam.HeartBeat.DisableAll()
185
+ # Get any player to pass to EndRound
186
+ Players:[]player = GetPlayspace().GetPlayers()
187
+ if (RoundEndInstigator := Players[0]):
188
+ Logger.Print("Round ended.")
189
+ RoundSettings.EndRound(RoundEndInstigator)
Code_Reference/PropHunt/verse/prop_team.verse ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Characters }
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/UI }
4
+ using { /UnrealEngine.com/Temporary/Diagnostics }
5
+ using { /UnrealEngine.com/Temporary/SpatialMath }
6
+ using { /UnrealEngine.com/Temporary/UI }
7
+ using { /Verse.org/Colors }
8
+ using { /Verse.org/Simulation }
9
+
10
+ # This message is used to print the number of props remaining to all players during a round.
11
+ PropAgentsRemainingMessage<localizes>(Count:int):message = "{Count} Prop(s) Remaining"
12
+
13
+ # Inheriting from the base_team class, the prop_team class contains the device definitions and functions related to the prop team and its agents.
14
+ # Notably, a prop agent's heart beat behavior can be found in this class.
15
+ prop_team := class<concrete>(base_team):
16
+ @editable # The score a prop agent receives per second.
17
+ ScorePerSecond:int = 10
18
+
19
+ @editable # The minimum distance a prop agent must move to reset the heart beat timer.
20
+ MinimumMoveDistance:float = 100.0
21
+
22
+ @editable # The timer device used to award score to a prop.
23
+ ScoreTimer:timer_device = timer_device{}
24
+
25
+ @editable # This tracker device is used to display the props remaining to the screen.
26
+ PropsRemainingTracker:tracker_device = tracker_device{}
27
+
28
+ @editable # Get the properties from the heart_beat class.
29
+ HeartBeat:heart_beat = heart_beat{}
30
+
31
+ # Set the agent to a prop agent and assign heart beat warning UI.
32
+ InitializeAgent<override>(NewPropAgent:agent):void =
33
+ Logger.Print("Setting a new prop agent.")
34
+ (super:)InitializeAgent(NewPropAgent)
35
+
36
+ # When a prop agent is eliminated or leaves the match, remove them from the PropAgents array and check for the end of the round.
37
+ # Note that this isn't overridding because we're passing all players to the function to update the props remaining message.
38
+ EliminateAgent<override>(PropAgent:agent)<suspends>:void =
39
+ Logger.Print("Prop agent eliminated.")
40
+ (super:)EliminateAgent(PropAgent)
41
+ # Update the props remaining number.
42
+ UpdatePropsRemainingTracker()
43
+
44
+ # Updates the value of the tracker device showing the number of props remaining.
45
+ UpdatePropsRemainingTracker():void =
46
+ PropsRemainingTracker.SetValue(Count())
47
+
48
+ # If the prop agent stops moving then race to see if the prop agent moves beyond the MinimumMoveDistance, the heart beat timer completes, or the prop agent is eliminated.
49
+ RunPropGameLoop(PropAgent:agent)<suspends>:void =
50
+ Logger.Print("Starting prop agent game loop.")
51
+ # Loop forever through the prop behavior and scoring until the prop agent is eliminated or the player leaves the session.
52
+ race:
53
+ PropAgent.AwaitNoLongerAProp()
54
+ loop:
55
+ # Wait until the prop agent moves less than the minimum distance, then advance.
56
+ PropAgent.AwaitStopMoving(MinimumMoveDistance)
57
+ # Until the prop agent moves beyond the minimum distance, countdown to the heart beat and then play the heart beat indefinitely.
58
+ race:
59
+ PropAgent.AwaitStartMoving(MinimumMoveDistance)
60
+ block:
61
+ CountdownTimer(PropAgent)
62
+ PropAgent.StartHeartbeat()
63
+ Sleep(0.0) # Once the race completes (the prop agent moves), start the loop again.
64
+ loop:
65
+ # Every second, award the prop agent points.
66
+ Sleep(1.0)
67
+ ScoreManager.Activate(PropAgent)
68
+
69
+
70
+ # Loop until the prop agent is no longer a part of the PropAgents array. Removal happens if the prop agent is eliminated and turned into a hunter or if the player leaves the session.
71
+ (PropAgent:agent).AwaitNoLongerAProp()<suspends>:void =
72
+ loop:
73
+ if (not FindOnTeam[PropAgent]):
74
+ Logger.Print("Cancelling prop agent behavior.")
75
+ break
76
+ Sleep(0.0) # Advance to the next game tick.
77
+
78
+ # Loops until the agent moves less than the MinimumDistance.
79
+ (PropAgent:agent).AwaitStopMoving(MinimumDistance:float)<suspends>:void =
80
+ Logger.Print("Checking if the agent has moved less than the minimum distance.")
81
+ # Get the initial position of the agent from the agent's character in the scene.
82
+ if (Tracked := PropAgent.GetFortCharacter[]):
83
+ var StartPosition:vector3 = Tracked.GetTransform().Translation
84
+ loop:
85
+ Sleep(0.0) # Get the position of the agent in the next game tick.
86
+ NewPosition := Tracked.GetTransform().Translation
87
+ # If the distance of the new position from the starting position is less than MinimumDistance, the agent has not moved and we break the loop.
88
+ if (Distance(StartPosition, NewPosition) < MinimumDistance):
89
+ Logger.Print("Agent has moved less than the minimum distance.")
90
+ break
91
+ # Otherwise, we reset StartPosition to make sure the player moves from the new position.
92
+ else:
93
+ set StartPosition = NewPosition
94
+
95
+ # Loops until the agent moves more than the MinimumDistance.
96
+ (PropAgent:agent).AwaitStartMoving(MinimumDistance:float)<suspends>:void =
97
+ Logger.Print("Checking if the agent moves further than the minimum distance.")
98
+ # Get the initial position of the agent from the agent's character in the scene.
99
+ if (Tracked := PropAgent.GetFortCharacter[]):
100
+ StartPosition:vector3 = Tracked.GetTransform().Translation
101
+ loop:
102
+ Sleep(0.0) # Get the position of the agent in the next game tick.
103
+ NewPosition := Tracked.GetTransform().Translation
104
+ # If the distance of the new position from the starting position is greater than or equal to MinimumDistance, the agent has moved and we break the loop.
105
+ if (Distance(StartPosition, NewPosition) >= MinimumDistance):
106
+ Logger.Print("Agent has moved more than or equal to the minimum distance.")
107
+ break
108
+
109
+ # Delays until HeartBeatWarningTime should start. Then counts down by HeartBeatWarningTime and sets the countdown text. Clears the text when deferred.
110
+ CountdownTimer(PropAgent:agent)<suspends>:void =
111
+ Logger.Print("Starting heart beat countdown.")
112
+ if (UIData := HeartBeat.WarningUI[PropAgent]):
113
+ Sleep(HeartBeat.MoveTime - HeartBeat.WarningTime) # Sleep for the amount of time before the warning appears.
114
+ Logger.Print("Starting heart beat warning.")
115
+ var WarningTimeRemaining:int = 0
116
+ if (set WarningTimeRemaining = Ceil[HeartBeat.WarningTime]) {}
117
+ # A defer happens when the function completes or if it is cancelled, such as if it loses a race.
118
+ # So in this case, the warning text is cleared when the countdown finishes or if the prop agent moves before the countdown finishes.
119
+ defer:
120
+ UIData.Text.SetText(HeartBeatWarningClear)
121
+ # Set the warning text to the time remaining, wait a second, and then decrement the time remaining. If the countdown completes, break the loop.
122
+ loop:
123
+ Logger.Print("Heart beat in {WarningTimeRemaining} seconds.")
124
+ UIData.Text.SetText(HeartBeatWarningMessage(WarningTimeRemaining))
125
+ Sleep(1.0)
126
+ set WarningTimeRemaining -= 1
127
+ if (WarningTimeRemaining <= 0):
128
+ break
129
+ else:
130
+ Logger.Print("UIData not found.")
131
+
132
+ # Turns on the heart beat VFX and SFX. Waits infinitely until deferred then disables the heart beat VFX and SFX.
133
+ (PropAgent:agent).StartHeartbeat()<suspends>:void =
134
+ Logger.Print("Spawning heart beat.")
135
+ # Save the heartbeat data so we can pass it in the defer later, after the PropAgent is destroyed or leaves the game.
136
+ var HeartBeatVFXData:heartbeat_vfx = heartbeat_vfx{}
137
+ if:
138
+ # Get the index of the prop agent in the PropAgents array to then access the corresponding heart beat VFX actor.
139
+ Index := FindOnTeam[PropAgent]
140
+ set HeartBeatVFXData = HeartBeat.AgentVFX[Index]
141
+ then:
142
+ HeartBeat.Enable(PropAgent, HeartBeatVFXData)
143
+ # When this function is cancelled by the prop agent moving, being eliminated, or the player leaving the session, disable the heartbeat.
144
+ defer:
145
+ HeartBeat.Disable(PropAgent, HeartBeatVFXData)
146
+ Sleep(Inf) # Do not stop sleeping until the race completes.
147
+
Code_Reference/PropHunt/verse/round_timer.verse ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Devices }
2
+ using { /Fortnite.com/UI }
3
+ using { /UnrealEngine.com/Temporary/Diagnostics }
4
+ using { /UnrealEngine.com/Temporary/SpatialMath }
5
+ using { /UnrealEngine.com/Temporary/UI }
6
+ using { /Verse.org/Colors }
7
+ using { /Verse.org/Simulation }
8
+
9
+ log_round_timer_device := class(log_channel){}
10
+
11
+ # An int that allows values between the ranges specified. This type is required by player_ui_slot.ZOrder.
12
+ round_int_clamped := type{_X:int where 0 <= _X, _X <= 2147483647}
13
+
14
+ # This message is used to print the time remaining before a round ends.
15
+ TimeRemainingMessage<localizes>(Minutes:string, Seconds:string):message = "{Minutes}:{Seconds}"
16
+
17
+ <#
18
+ This class contains all the logic for managing the round time and displaying the time to the screen.
19
+ You can use this device with a round_settings_device to actually end the round.
20
+ This device manages time without the use of a timer.
21
+ To use this class:
22
+ 1) Add the file to your project.
23
+ 2) Compile Verse Code from the Verse menu on the toolbar.
24
+ 3) Drag the device into your island from your island's Content/Creative Devices folder in the Content Browser.
25
+ 4) Include the waiting_for_more_players class in another Verse script with:
26
+ @editable
27
+ RoundTimer:round_timer = round_timer{}
28
+ 5) Compile Verse Code from the Verse menu on the toolbar.
29
+ 6) Connect the device you made in step 3 to the Verse device.
30
+ 7) Start the round timer with the following Verse:
31
+ RoundTimer.Start()
32
+ 8) Restart or Stop the timer with the equivalent functions.
33
+ 9) Wait for the timer to start with:
34
+ RoundTimer.AwaitStart()
35
+ 10) Wait for the timer to complete with:
36
+ RoundTimer.AwaitEnd()
37
+ Call the EndRound function on a round_settings_device to actually end the game round.
38
+ #>
39
+ round_timer := class(creative_device):
40
+ Logger:log = log{Channel:=log_round_timer_device}
41
+
42
+ @editable # The time, in minutes, a round lasts.
43
+ RoundTimeInMinutes:float = 5.0
44
+
45
+ @editable # The horizontal and vertical position of the timer UI on screen. X 0-1 is left-right and Y 0-1 is top-bottom.
46
+ UIPosition:vector2 = vector2{X:= 0.98, Y:=0.13}
47
+
48
+ @editable # The horizontal and vertical position of the timer UI on screen. X 0-1 is left-right and Y 0-1 is top-bottom.
49
+ UIAlignment:vector2 = vector2{X := 1.0, Y := 0.0}
50
+
51
+ @editable # The ZOrder of the UI compared to other UI elements.
52
+ UIZOrder:round_int_clamped = 4
53
+
54
+ # Signaled when the round has been started.
55
+ RoundStarted:event() = event(){}
56
+
57
+ # Signaled when the round is about to be ended.
58
+ RoundEndedEvent:event() = event(){}
59
+
60
+ # This map associates a text box for displaying the time to each player.
61
+ var TimeRemainingTextBlocks:[player]text_block = map{}
62
+
63
+ # The time remaining before the round completes, as an integer.
64
+ var TimeRemainingInSeconds:int = 0
65
+
66
+ # Waits until the round timer is started.
67
+ AwaitStart()<suspends>:void =
68
+ RoundStarted.Await()
69
+ Logger.Print("Round timer started.")
70
+
71
+ # Used to start the round timer.
72
+ Start():void =
73
+ Logger.Print("Starting the round timer.")
74
+ RoundStarted.Signal()
75
+ set TimeRemainingInSeconds = GetRoundTimeInSeconds()
76
+ spawn{ Running() }
77
+
78
+ # Restarts the timer to RoundTime
79
+ Restart():void =
80
+ Logger.Print("Restarting the round timer.")
81
+ set TimeRemainingInSeconds = GetRoundTimeInSeconds()
82
+
83
+ # Runs the timer logic.
84
+ Running()<suspends>:void =
85
+ Logger.Print("Round timer running.")
86
+ loop:
87
+ UpdateTimeUI()
88
+ Sleep(1.0)
89
+ # Decrement TimeRemaining by 1 second then check if the time has run out. If so, end the round.
90
+ set TimeRemainingInSeconds -= 1
91
+ if (TimeRemainingInSeconds < 0):
92
+ Stop()
93
+ break
94
+
95
+ # Stops the timer and ends the round.
96
+ Stop():void =
97
+ Logger.Print("Ending the round timer.")
98
+ # We get a player from the players remaining in the scene to end the round.
99
+ Players:[]player = GetPlayspace().GetPlayers()
100
+ if (Instigator := Players[0]):
101
+ RoundEndedEvent.Signal()
102
+
103
+ # Waits until the round timer is just about to end.
104
+ AwaitEnd()<suspends>:void =
105
+ RoundEndedEvent.Await()
106
+ Logger.Print("Round timer ended.")
107
+
108
+ # Accepts a time value in minutes and returns the value in seconds.
109
+ GetRoundTimeInSeconds():int =
110
+ var InSeconds:int = 0
111
+ if (set InSeconds = Round[RoundTimeInMinutes * 60.0]) {}
112
+ InSeconds
113
+
114
+ # When the timer completes, update the time remaining and check if time has expired.
115
+ UpdateTimeUI():void =
116
+ # Set Minutes to TimeRemainingInSeconds/60 without the remainder.
117
+ var Minutes:int = 0
118
+ if (set Minutes = Floor(TimeRemainingInSeconds / 60)) {}
119
+ # Set Seconds to the remainder of TimeRemainingInSeconds/60.
120
+ var Seconds:int = 0
121
+ if (set Seconds = Mod[TimeRemainingInSeconds, 60]) {}
122
+ # Convert Minutes and Seconds to strings.
123
+ MinutesAsString := string("{Minutes}")
124
+ # If Seconds < 10, then we need to add a 0 in front of the value so it displays as :0# instead of :#
125
+ SecondsAsString := if (Seconds < 10) then Join(array{string("{0}"),string("{Seconds}")},string()) else string("{Seconds}")
126
+
127
+ # Iterate through all players, check if they have a TimeRemainingTextBlock, if not, give them one. Then update the text.
128
+ Players:[]player = GetPlayspace().GetPlayers()
129
+ for (Player : Players):
130
+ var TextBlock:text_block = text_block{}
131
+ if (set TextBlock = TimeRemainingTextBlocks[Player]):
132
+ else:
133
+ Logger.Print("Creating new round time text block.")
134
+ set TextBlock = SetUpTimeRemainingUI(Player)
135
+ TextBlock.SetText(TimeRemainingMessage(MinutesAsString, SecondsAsString))
136
+
137
+ # Accepts a player and then adds a round time ui canvas to their screen and stores their TimeRemainingTextBlock for updating later.
138
+ SetUpTimeRemainingUI(Player:player):text_block =
139
+ Logger.Print("Adding round timer UI to a player.")
140
+ # This is the text_block that prints the time remaining text to the screen.
141
+ TextBlock:text_block = text_block:
142
+ DefaultTextColor := NamedColors.White
143
+ DefaultShadowColor := NamedColors.Black
144
+ if (PlayerUI := GetPlayerUI[Player]):
145
+ if (set TimeRemainingTextBlocks[Player] = TextBlock) {}
146
+ # This is the canvas that holds and positions the text_block on the screen.
147
+ Canvas := canvas:
148
+ Slots := array:
149
+ canvas_slot:
150
+ Anchors := anchors{Minimum := UIPosition, Maximum := UIPosition}
151
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
152
+ Alignment := UIAlignment
153
+ SizeToContent := true
154
+ Widget := TextBlock
155
+ # The canvas is assigned to the player.
156
+ PlayerUI.AddWidget(Canvas, player_ui_slot{ZOrder := UIZOrder})
157
+ # The text_block is returned so it can be saved to the map and updated later as time ticks down.
158
+ return TextBlock
Code_Reference/PropHunt/verse/waiting_for_more_players.verse ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ using { /Fortnite.com/Devices }
3
+ using { /Fortnite.com/UI }
4
+ using { /UnrealEngine.com/Temporary/Diagnostics }
5
+ using { /UnrealEngine.com/Temporary/SpatialMath }
6
+ using { /UnrealEngine.com/Temporary/UI }
7
+ using { /Verse.org/Colors }
8
+ using { /Verse.org/Simulation }
9
+
10
+ log_waiting_for_more_players_device := class(log_channel){}
11
+
12
+ # An int that allows values between the ranges specified. This type is required by player_ui_slot.ZOrder.
13
+ waiting_int_clamped := type{_X:int where 0 <= _X, _X <= 2147483647}
14
+
15
+ # This message is used to print the number of players needed before a round can begin.
16
+ WaitingForMorePlayersMessage<localizes>(Count:int):message = "Waiting for {Count} more Player(s)"
17
+
18
+ # This class is for showing how many players are needed to start the round.
19
+ waiting_for_more_players_ui := class:
20
+ var Canvas:canvas
21
+ var TextBlock:text_block
22
+
23
+ <#
24
+ This class contains all the logic for setting a minimum number of players and checking if there are enough to start the round.
25
+ To use this class:
26
+ 1) Add the file to your project.
27
+ 2) Compile Verse Code from the Verse menu on the toolbar.
28
+ 3) Drag the device into your island from your island's Content/Creative Devices folder in the Content Browser.
29
+ 4) Connect a Timer device to this device's "WaitingForMorePlayersTimer" property.
30
+ 5) Include the waiting_for_more_players class in another Verse script with:
31
+ @editable
32
+ WaitingForMorePlayers:waiting_for_more_players = waiting_for_more_players{}
33
+ 6) Compile Verse Code from the Verse menu on the toolbar.
34
+ 7) Connect the device you made in step 3 to the Verse device and property you exposed in step 6.
35
+ 8) Await the CheckForMinimumNumberOfPlayers functions by passing it a player. For example:
36
+ Players = GetPlayspace().GetPlayers()
37
+ CheckForMinimumNumberOfPlayers(Players)
38
+ 9) On IslandSettings, Set the Game Start Countdown to 0.0.
39
+ #>
40
+ waiting_for_more_players := class(creative_device):
41
+ Logger:log = log{Channel:=log_waiting_for_more_players_device}
42
+
43
+ @editable # The minimum number of players needed in the match for a round to start.
44
+ MinimumNumberOfPlayers:int = 2
45
+
46
+ @editable # The horizontal and vertical position of the timer UI on screen. X 0-1 is left-right and Y 0-1 is top-bottom.
47
+ UIPosition:vector2 = vector2{X:= 0.5, Y:=0.4}
48
+
49
+ @editable # The horizontal and vertical position of the timer UI on screen. X 0-1 is left-right and Y 0-1 is top-bottom.
50
+ UIAlignment:vector2 = vector2{X := 0.5, Y := 0.5}
51
+
52
+ @editable # The ZOrder of the UI compared to other UI elements.
53
+ UIZOrder:waiting_int_clamped = 3
54
+
55
+ @editable # This timer is used to countdown to round start after waiting for players to join the match.
56
+ WaitingForMorePlayersTimer:timer_device = timer_device{}
57
+
58
+ # This map associates a UI canvas for displaying the number of players needed to start a round to each player.
59
+ var WaitingForMorePlayersUI:[player]?waiting_for_more_players_ui = map{}
60
+
61
+ # Check if there are enough players to start the round. If not, wait until number of players >= MinimumNumberOfPlayers.
62
+ WaitForMinimumNumberOfPlayers(Players:[]player)<suspends>:[]player =
63
+ Logger.Print("Waiting if there are enough players for the round to start.")
64
+ # Creating a new variable so I can modify it as more players join. Initializing it with an array of players passed to the function.
65
+ var PlayersWaiting:[]player = Players
66
+ # If the number of players is less than the minimum needed to start the round...
67
+ if (PlayersWaiting.Length < MinimumNumberOfPlayers):
68
+ loop: # Loop until the number of players is greater than or equal to the minimum needed.
69
+ Logger.Print("{PlayersWaiting.Length}/{MinimumNumberOfPlayers} players needed for the round to start.")
70
+ # Update the waiting for more players UI.
71
+ DisplayWaitingForMorePlayers(PlayersWaiting)
72
+ Sleep(2.0) # Wait to see if more players join the match then check if there are enough players to start the round.
73
+ set PlayersWaiting = GetPlayspace().GetPlayers()
74
+ if (PlayersWaiting.Length >= MinimumNumberOfPlayers):
75
+ # If there are now enough players, clear the waiting for more players UI,
76
+ Logger.Print("{PlayersWaiting.Length}/{MinimumNumberOfPlayers} players in round, preparing for round start.")
77
+ ClearWaitingForMorePlayers()
78
+ # Then break out of the loop.
79
+ break
80
+ # start the round start countdown, and wait until the countdown completes.
81
+ WaitingForMorePlayersTimer.Start()
82
+ WaitingForMorePlayersTimer.SuccessEvent.Await()
83
+ Logger.Print("Starting round.")
84
+ # Return the list of players in the session.
85
+ return PlayersWaiting
86
+
87
+ # Displays a "Waiting for more players" UI message for each player if they don't already have one. Updates the player counter for all players.
88
+ DisplayWaitingForMorePlayers(Players:[]player):void =
89
+ PlayersNeededCount := MinimumNumberOfPlayers - Players.Length
90
+ Logger.Print("{Players.Length} players in round, waiting for {PlayersNeededCount} more player(s) to join.")
91
+ for (Player: Players):
92
+ # If the player has a WaitingForMorePlayersUI, get the TextBlock and refresh the text so it shows the correct number of players needed to start the round.
93
+ if (UIData := WaitingForMorePlayersUI[Player]?):
94
+ UIData.TextBlock.SetText(WaitingForMorePlayersMessage(PlayersNeededCount))
95
+ # Else create a WaitingForMorePlayersUI for the player.
96
+ else:
97
+ SetUpWaitingForMorePlayersUI(Player, PlayersNeededCount)
98
+
99
+ # Accepts a player and player_ui and adds a waiting for more players ui canvas to their screen.
100
+ SetUpWaitingForMorePlayersUI(Player:player, PlayersNeededCount:int):void =
101
+ Logger.Print("Creating 'waiting for more players' UI.")
102
+ if (PlayerUI := GetPlayerUI[Player]):
103
+ # This is the text_block that prints the waiting for more players text to the screen.
104
+ TextBlock:text_block = text_block:
105
+ DefaultText := WaitingForMorePlayersMessage(PlayersNeededCount)
106
+ DefaultTextColor := NamedColors.White
107
+ DefaultShadowColor := NamedColors.Black
108
+ # This is the canvas that holds and positions the text_block on the screen.
109
+ Canvas := canvas:
110
+ Slots := array:
111
+ canvas_slot:
112
+ Anchors := anchors{Minimum := UIPosition, Maximum := UIPosition}
113
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
114
+ Alignment := UIAlignment
115
+ SizeToContent := true
116
+ Widget := TextBlock
117
+ # The canvas is assigned to the player.
118
+ PlayerUI.AddWidget(Canvas, player_ui_slot{ZOrder := UIZOrder})
119
+ # The text_block is saved to the map so we can update the text later as more players join the game.
120
+ if (set WaitingForMorePlayersUI[Player] = option{ waiting_for_more_players_ui{Canvas := Canvas, TextBlock := TextBlock} }) {}
121
+
122
+ # Removes the "Waiting for more players" UI message for each player who has one.
123
+ ClearWaitingForMorePlayers():void =
124
+ Logger.Print("Clearing 'waiting for more players' UI.")
125
+ for (Player -> UIData : WaitingForMorePlayersUI):
126
+ if:
127
+ PlayerUI := GetPlayerUI[Player]
128
+ Canvas := UIData?.Canvas
129
+ set WaitingForMorePlayersUI[Player] = false
130
+ then:
131
+ PlayerUI.RemoveWidget(Canvas)
Code_Reference/Stronghold/txt/stronghold_alert_manager.txt ADDED
File without changes
Code_Reference/Stronghold/txt/stronghold_bark_manager.txt ADDED
File without changes
Code_Reference/Stronghold/txt/stronghold_billboard_cinematic.txt ADDED
File without changes
Code_Reference/Stronghold/txt/stronghold_game_manager.txt ADDED
File without changes
Code_Reference/Stronghold/txt/stronghold_intro_explosion.txt ADDED
File without changes
Code_Reference/Stronghold/txt/stronghold_leash_position.txt ADDED
File without changes
Code_Reference/Stronghold/txt/stronghold_log.txt ADDED
File without changes
Code_Reference/Stronghold/verse/stronghold_alert_manager.verse ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Verse.org/Simulation }
2
+ using { /Verse.org/Simulation/Tags }
3
+ using { /Verse.org/Colors }
4
+ using { /UnrealEngine.com/Temporary/Diagnostics }
5
+ using { /Fortnite.com/Devices }
6
+
7
+
8
+ # Script that handles music and turn on lights when guards are alerted
9
+ stronghold_alert_manager := class(creative_device):
10
+
11
+ # Reference to the Game Manager to monitor perception events
12
+ @editable
13
+ StrongholdGameManager:stronghold_game_manager := stronghold_game_manager{}
14
+
15
+ # Reference to Radio that plays combat music
16
+ @editable
17
+ RadioCombat:radio_device := radio_device{}
18
+
19
+ # Reference to Radio that plays alerted music
20
+ @editable
21
+ RadioAlerted:radio_device := radio_device{}
22
+
23
+ # VFX to play when alarm/flare is shot
24
+ @editable
25
+ FlareAlarmVFXCreator:vfx_creator_device := vfx_creator_device{}
26
+
27
+ # Lights to turn on when alerted
28
+ @editable
29
+ CustomizableLightDevicesAlerted: []customizable_light_device = array{}
30
+
31
+ # Lights to turn on during combat
32
+ @editable
33
+ CustomizableLightDevicesCombat: []customizable_light_device = array{}
34
+
35
+ # Change the camp to alerted when player is lost / killed
36
+ WaitForAlerted()<suspends>:void=
37
+ # do not go back to alerted after fallback
38
+ if (StrongholdGameManager.FallbackTriggered?):
39
+ Sleep(Inf)
40
+ StrongholdGameManager.GuardsUnawareEvent.Await()
41
+ Sleep(3.0)
42
+ SetAlertedMood()
43
+
44
+ # Change the camp to combat when player is spotted
45
+ WaitForCombat()<suspends>:void=
46
+ race:
47
+ StrongholdGameManager.PlayerDetectedEvent.Await()
48
+ StrongholdGameManager.FallbackEvent.Await()
49
+ Sleep(2.0)
50
+ SetCombatMood()
51
+
52
+ # Runs when the device is started in a running game
53
+ OnBegin<override>()<suspends>:void=
54
+ MonitorStrongholdAlertStatus()
55
+
56
+ # Main Loop checking if stronghold is in combat or alerted
57
+ MonitorStrongholdAlertStatus()<suspends>:void=
58
+ loop:
59
+ WaitForCombat()
60
+ WaitForAlerted()
61
+
62
+ # Sets Base to Combat by toggling lights red and playing high intensity music
63
+ SetCombatMood():void=
64
+ # Loop through Combat Lights and turn them on
65
+ for(LightsToTurnOn: CustomizableLightDevicesCombat):
66
+ LightsToTurnOn.TurnOn()
67
+ # Loop through Alert Lights and turn them off
68
+ for(LightsToTurnOff: CustomizableLightDevicesAlerted):
69
+ LightsToTurnOff.TurnOff()
70
+ # Turn on combat audio and turn off alerted audio
71
+ RadioCombat.Play()
72
+ RadioAlerted.Stop()
73
+ FlareAlarmVFXCreator.Toggle()
74
+ Log("Alert manager - Combat Lights & Music On")
75
+
76
+ # Sets Base to Alerted by toggling lights yellow and playing tense music
77
+ SetAlertedMood():void=
78
+ for(LightsToTurnOn: CustomizableLightDevicesAlerted):
79
+ LightsToTurnOn.TurnOn()
80
+ for(LightsToTurnOff: CustomizableLightDevicesCombat):
81
+ LightsToTurnOff.TurnOff()
82
+ RadioCombat.Stop()
83
+ RadioAlerted.Play()
84
+ Log("Alert manager - Alert Lights & Music On")
85
+
86
+
87
+
Code_Reference/Stronghold/verse/stronghold_bark_manager.verse ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Devices }
2
+ using { /Fortnite.com/Game }
3
+ using { /Fortnite.com/Characters }
4
+ using { /Verse.org/Random }
5
+ using { /Verse.org/Simulation }
6
+ using { /UnrealEngine.com/Temporary/Diagnostics }
7
+ using { /UnrealEngine.com/Temporary/SpatialMath }
8
+
9
+ # Audio bark that can be played on a NPC
10
+ audio_npc_bark := class<concrete>:
11
+
12
+ # Audio device to play barks
13
+ @editable
14
+ BarkDevice:audio_player_device := audio_player_device{}
15
+
16
+ # Option to allow NPCs to repeat the bark
17
+ @editable
18
+ CanRepeat:logic = true
19
+
20
+ # Delay between the event and the beginning of the bark
21
+ @editable
22
+ Delay:float = 0.0
23
+
24
+ # Delay before repeating this bark
25
+ @editable
26
+ Cooldown:float = 5.0
27
+
28
+ # Bark name for logging
29
+ @editable
30
+ BarkID:string = "Missing ID"
31
+
32
+ # Is the cooldown timer elapsed
33
+ var<private> IsInCooldown:logic = false
34
+
35
+ # Event to stop the bark
36
+ StopBarkEvent<private>:event() = event(){}
37
+
38
+ PlayBark(Agent:agent)<suspends>:void=
39
+ var IsPlaying:logic = false;
40
+ defer:
41
+ if (IsPlaying?):
42
+ set IsPlaying = false
43
+ set IsInCooldown = false
44
+ BarkDevice.Stop(Agent)
45
+ Log("Bark manager - PlayBark - {BarkID} - Stopped")
46
+ race:
47
+ block:
48
+ StopBarkEvent.Await()
49
+ return
50
+ block:
51
+ AwaitAgentDown(Agent)
52
+ return
53
+ block:
54
+ if (Delay > 0.0):
55
+ Sleep(Delay)
56
+ if (IsInCooldown?):
57
+ return
58
+ BarkDevice.Play(Agent)
59
+ set IsPlaying = true
60
+ set IsInCooldown = true
61
+ Sleep(2.0)
62
+ set IsPlaying = false
63
+ Log("Bark manager - PlayBark - {BarkID} - Success")
64
+
65
+ if (CanRepeat?):
66
+ Sleep(Cooldown)
67
+ set IsInCooldown = false
68
+
69
+ StopBark():void=
70
+ StopBarkEvent.Signal()
71
+
72
+ AwaitAgentDown<private>(Agent:agent)<suspends>:void=
73
+ if (Character := Agent.GetFortCharacter[]):
74
+ loop:
75
+ if (Character.GetHealth() <= 0.0):
76
+ return
77
+ Character.DamagedEvent().Await()
78
+
79
+ # Script that handles barks from guards
80
+ stronghold_bark_manager := class(creative_device):
81
+
82
+ # Reference to the Game Manager to monitor perception events
83
+ @editable
84
+ StrongholdGameManager:stronghold_game_manager := stronghold_game_manager{}
85
+
86
+ # Audio Player Devices
87
+ @editable
88
+ BarkNPCDown:audio_npc_bark = audio_npc_bark{BarkID := "Man Down", Delay := 0.3}
89
+
90
+ @editable
91
+ BarkFallback:audio_npc_bark = audio_npc_bark{BarkID := "Fallback", CanRepeat := false, Delay := 3.0}
92
+
93
+ @editable
94
+ BarkNeedBackup:audio_npc_bark = audio_npc_bark{BarkID := "Need Backup", CanRepeat := false, Delay := 2.0}
95
+
96
+ @editable
97
+ BarkGoToLeash:audio_npc_bark = audio_npc_bark{BarkID := "Reinforcements En Route", CanRepeat := false, Delay := 4.0}
98
+
99
+ @editable
100
+ BarkDamageTaken:audio_npc_bark = audio_npc_bark{BarkID := "Took Damage", Delay := 0.2}
101
+
102
+ @editable
103
+ BarkDamagePlayer:audio_npc_bark = audio_npc_bark{BarkID := "Hit Player", Delay := 0.2}
104
+
105
+ @editable
106
+ BarkEliminatedPlayer:audio_npc_bark = audio_npc_bark{BarkID := "Eliminated Player", Delay := 0.3}
107
+
108
+ @editable
109
+ BarkPlayerSpotted:audio_npc_bark = audio_npc_bark{BarkID := "Spotted Player", CanRepeat := false}
110
+
111
+ @editable
112
+ BarkPlayerLost:audio_npc_bark = audio_npc_bark{BarkID := "Lost Player", Cooldown := 10.0}
113
+
114
+ @editable
115
+ BarkGuardSuspicious:audio_npc_bark = audio_npc_bark{BarkID := "Suspicious", Cooldown := 10.0}
116
+
117
+ @editable
118
+ BarkGuardUnaware:audio_npc_bark = audio_npc_bark{BarkID := "Unaware", Cooldown := 10.0}
119
+
120
+ # Variable to store if guards were looking for targets
121
+ var<private> HasLostTarget:logic := false
122
+
123
+ # Runs when the device is started in a running game
124
+ OnBegin<override>()<suspends>:void=
125
+ ConfigureBarks()
126
+
127
+ sync:
128
+ AwaitReinforcements()
129
+ AwaitFallback()
130
+ PlayAwarenessBarks()
131
+
132
+ PlayBark(Bark:audio_npc_bark, Guard:agent):void=
133
+ spawn {Bark.PlayBark(Guard)}
134
+
135
+ # Play a bark when reinforcement is called
136
+ AwaitReinforcements<private>()<suspends>:void=
137
+ AlertedGuard := StrongholdGameManager.ReinforcementsCalledEvent.Await()
138
+ PlayBark(BarkNeedBackup, AlertedGuard)
139
+
140
+ # Play a bark when guards regroup in the stronghold
141
+ AwaitFallback<private>()<suspends>:void=
142
+ StrongholdGameManager.FallbackEvent.Await()
143
+ if:
144
+ Guard := StrongholdGameManager.AlertedGuards[0]
145
+ then:
146
+ PlayBark(BarkFallback, Guard)
147
+
148
+ PlayAwarenessBarks<private>()<suspends>:void=
149
+ loop:
150
+ race:
151
+ PlayGuardsSuspiciousBark()
152
+ PlayPlayerSpottedBark()
153
+ PlayPlayerLostBark()
154
+ PlayGuardsUnawareBark()
155
+
156
+ PlayPlayerSpottedBark<private>()<suspends>:void=
157
+ Guard:=StrongholdGameManager.PlayerDetectedEvent.Await();
158
+ set HasLostTarget = false
159
+ PlayBark(BarkPlayerSpotted, Guard)
160
+
161
+ PlayPlayerLostBark<private>()<suspends>:void=
162
+ Guard:=StrongholdGameManager.PlayerLostEvent.Await();
163
+ set HasLostTarget = true
164
+ PlayBark(BarkPlayerLost, Guard)
165
+
166
+ PlayGuardsSuspiciousBark<private>()<suspends>:void=
167
+ Guard:=StrongholdGameManager.GuardsSuspiciousEvent.Await();
168
+ PlayBark(BarkGuardSuspicious, Guard)
169
+
170
+ PlayGuardsUnawareBark<private>()<suspends>:void=
171
+ Guard:=StrongholdGameManager.GuardsUnawareEvent.Await();
172
+ if (HasLostTarget?):
173
+ set HasLostTarget = false
174
+ if (not StrongholdGameManager.FallbackTriggered?):
175
+ PlayBark(BarkGuardUnaware, Guard)
176
+
177
+ SubscribeToGuardSpawnerEvents(GuardSpawnerDevice:guard_spawner_device):void =
178
+ GuardSpawnerDevice.DamagedEvent.Subscribe(OnGuardDamaged)
179
+ GuardSpawnerDevice.EliminatedEvent.Subscribe(OnGuardEliminated)
180
+ GuardSpawnerDevice.EliminatingEvent.Subscribe(OnPlayerEliminated)
181
+
182
+ # Configure all barks
183
+ ConfigureBarks():void=
184
+
185
+ # Subscribe To Player Damage Event
186
+ AllPlayers := GetPlayspace().GetPlayers()
187
+ for (StrongholdPlayer : AllPlayers, StrongholdPC := StrongholdPlayer.GetFortCharacter[]):
188
+ StrongholdPC.DamagedEvent().Subscribe(OnPlayerDamaged)
189
+
190
+ # Run through guards spawner list from stronghold manager and subscribe to all key events
191
+ for (GuardSpawner : StrongholdGameManager.GuardsInitialSpawners):
192
+ SubscribeToGuardSpawnerEvents(GuardSpawner)
193
+ for (GuardSpawner : StrongholdGameManager.GuardsReinforcementSpawners):
194
+ SubscribeToGuardSpawnerEvents(GuardSpawner)
195
+
196
+ # Have a separate case for when the reinforcements spawn
197
+ if:
198
+ FirstReinforcementSpawner := StrongholdGameManager.GuardsReinforcementSpawners[0]
199
+ then:
200
+ FirstReinforcementSpawner.SpawnedEvent.Subscribe(HandleReinforcementSpawned)
201
+
202
+ # Guard is down, try to play a bark on the closest alerted guard
203
+ OnGuardEliminated(InteractionResult:device_ai_interaction_result):void=
204
+ if (EliminatedGuard := InteractionResult.Target?):
205
+
206
+ # Find closest alive guard to play this bark
207
+ var ClosestGuard:?agent = false
208
+ if:
209
+ set ClosestGuard = option{StrongholdGameManager.AlertedGuards[0]}
210
+ EliminatedGuardCharacter := EliminatedGuard.GetFortCharacter[]
211
+ then:
212
+ for (AlertedGuard : StrongholdGameManager.AlertedGuards, AlertedGuardCharacter := AlertedGuard.GetFortCharacter[]):
213
+ if:
214
+ not ClosestGuard? = AlertedGuard
215
+ ClosestGuardCharacter := ClosestGuard?.GetFortCharacter[]
216
+ DistanceSquaredToAlertedGuard := DistanceSquared(AlertedGuardCharacter.GetTransform().Translation, EliminatedGuardCharacter.GetTransform().Translation)
217
+ DistanceSquaredToClosestGuard := DistanceSquared(ClosestGuardCharacter.GetTransform().Translation, EliminatedGuardCharacter.GetTransform().Translation)
218
+ DistanceSquaredToAlertedGuard < DistanceSquaredToClosestGuard
219
+ then:
220
+ set ClosestGuard = option{AlertedGuard}
221
+
222
+ if (Guard := ClosestGuard?):
223
+ spawn {BarkNPCDown.PlayBark(Guard)}
224
+
225
+ # Guard is hit, try to play a bark if the guard is not down
226
+ OnGuardDamaged(InteractionResult:device_ai_interaction_result):void=
227
+ if (Guard := InteractionResult.Target?):
228
+ spawn {BarkDamageTaken.PlayBark(Guard)}
229
+
230
+ # Player is hit, try to play a bark on the guard that damaged the player
231
+ OnPlayerDamaged(DamageResult:damage_result):void=
232
+ if:
233
+ fort_character[DamageResult.Target].GetHealth() > 0.0
234
+ Guard := DamageResult.Instigator?.GetInstigatorAgent[]
235
+ then:
236
+ spawn {BarkDamagePlayer.PlayBark(Guard)}
237
+
238
+ # Player is down, try to play a bark on the guard that eliminated the player
239
+ OnPlayerEliminated(InteractionResult:device_ai_interaction_result):void=
240
+ if (Guard := InteractionResult.Source?):
241
+ spawn {BarkEliminatedPlayer.PlayBark(Guard)}
242
+
243
+ HandleReinforcementSpawned(Guard:agent):void=
244
+ spawn {BarkGoToLeash.PlayBark(Guard)}
245
+
246
+
247
+
Code_Reference/Stronghold/verse/stronghold_billboard_cinematic.verse ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/Devices }
2
+ using { /Fortnite.com/Characters }
3
+ using { /Fortnite.com/FortPlayerUtilities }
4
+ using { /Fortnite.com/UI }
5
+ using { /Verse.org/Simulation }
6
+ using { /UnrealEngine.com/Temporary/Diagnostics }
7
+ using { /UnrealEngine.com/Temporary/SpatialMath}
8
+ using { /UnrealEngine.com/Temporary/UI }
9
+
10
+ # Script that sets the player into a cinematic mode so its not in the cinematic, and then resets them when the cinematic is done
11
+ stronghold_billboard_cinematic := class(creative_device):
12
+
13
+ # A reference to the device that contains the level sequence to be played
14
+ @editable
15
+ CinematicSequenceDeviceToPlay:cinematic_sequence_device := cinematic_sequence_device{}
16
+
17
+ # A reference to the device that will hide the HUD
18
+ @editable
19
+ HudController:hud_controller_device := hud_controller_device{}
20
+
21
+ # Should the Skip Intro button be visible during the cinematic
22
+ @editable
23
+ ShowSkipIntroButton:logic := true
24
+
25
+ # Should Devices and Billboards be visible during the cinematic
26
+ @editable
27
+ ShowDevices:logic := true
28
+
29
+ # References to the billboard that will be visible during the cinematic
30
+ @editable
31
+ BillboardsToShow:[]billboard_device := array{}
32
+
33
+ # References to the devices that will be hidden after the cinematic
34
+ @editable
35
+ DevicesToHide:[]creative_device := array{}
36
+ @editable
37
+ RadiosToHide:[]radio_device := array{}
38
+ @editable
39
+ AudioPlayersToHide:[]audio_player_device := array{}
40
+
41
+ # Reference to Radio that plays music
42
+ @editable
43
+ Music:radio_device := radio_device{}
44
+
45
+ # Reference to the Game Manager to start gameplay after the cinematic
46
+ @editable
47
+ StrongholdGameManager:stronghold_game_manager := stronghold_game_manager{}
48
+
49
+ # A localizable message to display as text in the UI
50
+ SkipCinematicText<localizes> : message = "Skip intro"
51
+
52
+ var SkipButtonPerPlayer : [player]?canvas = map{}
53
+
54
+ # Runs at the beginning of the script.
55
+ OnBegin<override>()<suspends>:void=
56
+
57
+ # Handles when the cinematic sequence device finishes playing
58
+ CinematicSequenceDeviceToPlay.StoppedEvent.Subscribe(HandleCinematicEnd)
59
+
60
+ # Sets the player to invisible and removes their movement
61
+ CinematicStasis := stasis_args{AllowTurning := false, AllowFalling := false, AllowEmotes := false}
62
+
63
+ for (StrongholdPlayer : GetPlayspace().GetPlayers(), not StrongholdPlayer.IsSpectator[], PlayerCharacter:= StrongholdPlayer.GetFortCharacter[], PlayerUI := GetPlayerUI[StrongholdPlayer]):
64
+ PlayerCharacter.PutInStasis(CinematicStasis)
65
+ PlayerCharacter.Hide()
66
+
67
+ if (ShowSkipIntroButton?):
68
+ SkipButton := CreateSkipButton(PlayerUI)
69
+ option { set SkipButtonPerPlayer[StrongholdPlayer] = option{SkipButton} }
70
+
71
+ HudController.Enable()
72
+
73
+ if (ShowDevices?):
74
+ ShowBillboardsAndDevices()
75
+ else:
76
+ HideBillboardsAndDevices()
77
+
78
+ CinematicSequenceDeviceToPlay.Play()
79
+ Music.Play()
80
+ Log("Cinematic Mode Engaged - Players should be frozen and invisible")
81
+
82
+ # Sets the player back to visible and free to move
83
+ HandleCinematicEnd():void=
84
+
85
+ Music.Stop()
86
+
87
+ for (StrongholdPlayer : GetPlayspace().GetPlayers(), not StrongholdPlayer.IsSpectator[], PlayerCharacter:= StrongholdPlayer.GetFortCharacter[], PlayerUI := GetPlayerUI[StrongholdPlayer]):
88
+ PlayerCharacter.ReleaseFromStasis()
89
+ PlayerCharacter.Show()
90
+
91
+ if (SkipButtonUI := SkipButtonPerPlayer[StrongholdPlayer]?):
92
+ PlayerUI.RemoveWidget(SkipButtonUI)
93
+ option { set SkipButtonPerPlayer[StrongholdPlayer] = false }
94
+
95
+ HudController.Disable()
96
+
97
+ HideBillboardsAndDevices()
98
+
99
+ Log("Cinematic Mode Disengaged - Players should be free to move and visible.")
100
+
101
+ spawn{ StrongholdGameManager.StartGameplay() }
102
+
103
+ HandleSkipIntroInteraction(Message : widget_message) : void =
104
+
105
+ CinematicSequenceDeviceToPlay.Stop(Message.Player)
106
+
107
+ # A canvas widget that displays a Skip Button
108
+ CreateSkipButton(PlayerUI:player_ui):canvas=
109
+
110
+ SkipButton :button_regular = button_regular{DefaultText := SkipCinematicText}
111
+ SkipButton.OnClick().Subscribe(HandleSkipIntroInteraction)
112
+ SkipButton.SetEnabled(true)
113
+
114
+ SkipButtonCanvas : canvas = canvas:
115
+ Slots := array:
116
+ canvas_slot:
117
+ Anchors := anchors{Minimum := vector2{X := 0.9, Y := 0.9}, Maximum := vector2{X := 0.9, Y := 0.9}}
118
+ Offsets := margin{Top := 0.0, Left := 0.0, Right := 0.0, Bottom := 0.0}
119
+ Alignment := vector2{X := 1.0, Y := 1.0}
120
+ SizeToContent := true
121
+ Widget := SkipButton
122
+
123
+ PlayerUISlot:=player_ui_slot{ZOrder:=0, InputMode:=ui_input_mode.All}
124
+ PlayerUI.AddWidget(SkipButtonCanvas, PlayerUISlot)
125
+
126
+ return SkipButtonCanvas
127
+
128
+
129
+ ShowBillboardsAndDevices():void=
130
+ for (Billboard : BillboardsToShow):
131
+ Billboard.ShowText()
132
+ Show()
133
+ for (Device : DevicesToHide):
134
+ Device.Show()
135
+ for (Radio : RadiosToHide):
136
+ Radio.Show()
137
+ for (AudioPlayer : AudioPlayersToHide):
138
+ AudioPlayer.Show()
139
+
140
+ HideBillboardsAndDevices():void=
141
+ for (Billboard : BillboardsToShow):
142
+ Billboard.HideText()
143
+ Hide()
144
+ for (Device : DevicesToHide):
145
+ Device.Hide()
146
+ for (Radio : RadiosToHide):
147
+ Radio.Hide()
148
+ for (AudioPlayer : AudioPlayersToHide):
149
+ AudioPlayer.Hide()
Code_Reference/Stronghold/verse/stronghold_game_manager.verse ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/AI }
2
+ using { /Fortnite.com/Characters }
3
+ using { /Fortnite.com/Devices }
4
+ using { /Fortnite.com/FortPlayerUtilities }
5
+ using { /Fortnite.com/Game }
6
+ using { /UnrealEngine.com/Temporary/Diagnostics }
7
+ using { /UnrealEngine.com/Temporary/SpatialMath }
8
+ using { /Verse.org/Simulation }
9
+ using { /Verse.org/Verse }
10
+
11
+
12
+ # The Stronghold is a game mode in which the goal is for players to eliminate all hostile enemies at a heavily guarded Stronghold
13
+ # The Stronghold Game Manager Verse device is used to manage, monitor, and control the AIs at the Stronghold
14
+ stronghold_game_manager := class(creative_device):
15
+
16
+ # Device reference to guard spawner device to keep track of for eliminations
17
+ @editable
18
+ GuardsInitialSpawners:[]guard_spawner_device := array{}
19
+
20
+ # Device reference to guard spawner device to keep track of for eliminations for multiplayer scaling
21
+ @editable
22
+ GuardsInitialSpawnersAdditional:[]guard_spawner_device := array{}
23
+
24
+ # Device reference to reinforcement guard spawner device to trigger when one of the Stronghold guards is alerted
25
+ @editable
26
+ GuardsReinforcementSpawners:[]guard_spawner_device := array{}
27
+
28
+ # Device reference to reinforcement guard spawner device to trigger when one of the Stronghold guards is alerted for multiplayer scaling
29
+ @editable
30
+ GuardsReinforcementSpawnersAdditional:[]guard_spawner_device := array{}
31
+
32
+ # Device reference to display and track objectives
33
+ @editable
34
+ ObjectiveTracker:tracker_device := tracker_device{}
35
+
36
+ # Device reference to display reinforcement in-game message
37
+ @editable
38
+ MessageDeviceReinforcement:hud_message_device := hud_message_device{}
39
+
40
+ # Device reference to display fallback in-game message
41
+ @editable
42
+ MessageDeviceFallback:hud_message_device := hud_message_device{}
43
+
44
+ # Device reference to end the game with a victory if the players completed the Stronghold without being detected
45
+ @editable
46
+ EndGameVictoryDeviceUndetected:end_game_device := end_game_device{}
47
+
48
+ # Device reference to end the game with a victory if the players completed the Stronghold while being detected
49
+ @editable
50
+ EndGameVictoryDeviceDetected:end_game_device := end_game_device{}
51
+
52
+ # Device reference to end the game with a fail if the players ran out of retries
53
+ @editable
54
+ EndGameFailDevice:end_game_device := end_game_device{}
55
+
56
+ # Adjustable number of player lives
57
+ @editable
58
+ var PlayerRetries:int = 2
59
+
60
+ # Device to reference Stronghold leash position
61
+ @editable
62
+ ReinforcementLeashReference:stronghold_leash_position := stronghold_leash_position{}
63
+
64
+ # Device to reference Fallback leash position
65
+ @editable
66
+ FallbackLeashReference:stronghold_leash_position := stronghold_leash_position{}
67
+
68
+ # Leashes that must be disabled after fallback
69
+ @editable
70
+ LeashesToDisableForFallback:[]stronghold_leash_position := array{}
71
+
72
+ # Device for the explosion
73
+ @editable
74
+ ExplosiveDevice:explosive_device := explosive_device{}
75
+
76
+ # Guards perception is monitored by this script, the other scripts can subscribe to those events
77
+
78
+ # Event broadcasted when a guard call for reinforcement
79
+ ReinforcementsCalledEvent:event(agent) = event(agent){}
80
+
81
+ # Event broadcasted when guards defend the center of the Stronghold
82
+ FallbackEvent:event() = event(){}
83
+
84
+ # Event broadcasted when a guard becomes suspicious
85
+ GuardsSuspiciousEvent:event(agent) = event(agent){}
86
+
87
+ # Event broadcasted when all guards become unaware
88
+ GuardsUnawareEvent:event(agent) = event(agent){}
89
+
90
+ # Event broadcasted when a player is detected
91
+ PlayerDetectedEvent:event(agent) = event(agent){}
92
+
93
+ # Event broadcasted when all guards have lost their target
94
+ PlayerLostEvent:event(agent) = event(agent){}
95
+
96
+ # Lists of guards in a specific alert state to monitor perception changes
97
+
98
+ # Variable to store reinforcement guards
99
+ var<private> NumGuardsSpawned:int := 0
100
+
101
+ # Variable to store all Stronghold guards
102
+ var<private> StrongholdGuards:[]agent := array{}
103
+
104
+ # Variable to store reinforcement guards
105
+ var<private> ReinforcementGuards:[]agent := array{}
106
+
107
+ # List of guards currently suspicious
108
+ var<private> SuspiciousGuards : []agent = array{}
109
+
110
+ # List of guards currently alerted
111
+ var<private> AlertedGuards : []agent = array{}
112
+
113
+ # List of guards currently investigating
114
+ var<private> InvestigatingGuards : []agent = array{}
115
+
116
+ # Initial guard spawners, will include additional spawners with multiplayer session
117
+ var<private> GuardsInitialSpawnersInternal:[]guard_spawner_device = array{}
118
+
119
+ # Reinforcement guard spawners, will include additional spawners with multiplayer session
120
+ var<private> GuardsReinforcementSpawnersInternal:[]guard_spawner_device = array{}
121
+
122
+ # Gameplay logic variables
123
+
124
+ # Variable to track the number of eliminations from all Stronghold guard spawners
125
+ var<private> GuardsEliminated:int := 0
126
+
127
+ # Variable to track if the reinforcement were called or not
128
+ var<private> ReinforcementTriggered:logic := false
129
+
130
+ # Variable to track if the fallback was triggered
131
+ var<private> FallbackTriggered:logic := false
132
+
133
+ # Variable to store the first player agent that gets detected by the guards
134
+ var<private> DetectedPlayer:?player := false
135
+
136
+
137
+ # Runs when the device is started in a running game.
138
+ OnBegin<override>()<suspends>:void=
139
+ Log("Stronghold verse manager is starting")
140
+
141
+ # Check active player for difficulty scaling
142
+ var NumberOfActivePlayers:int = 0
143
+ for (StrongholdPlayer : GetPlayspace().GetPlayers(), not StrongholdPlayer.IsSpectator[], StrongholdPC := StrongholdPlayer.GetFortCharacter[]):
144
+ set NumberOfActivePlayers += 1
145
+ # Subscribing to player elimination event
146
+ StrongholdPC.EliminatedEvent().Subscribe(OnPlayerEliminated)
147
+
148
+ Log("Game manager - Total player number is {NumberOfActivePlayers}")
149
+
150
+ set GuardsInitialSpawnersInternal = GuardsInitialSpawners
151
+ set GuardsReinforcementSpawnersInternal = GuardsReinforcementSpawners
152
+
153
+ # Add additional Guard Spawner when there is more than 2 players
154
+ if (NumberOfActivePlayers > 2):
155
+ set GuardsInitialSpawnersInternal += GuardsInitialSpawnersAdditional
156
+ set GuardsReinforcementSpawnersInternal += GuardsReinforcementSpawnersAdditional
157
+
158
+ var NumInitialGuards:int = 0
159
+ for (GuardSpawner : GuardsInitialSpawnersInternal):
160
+ GuardSpawner.Enable()
161
+ SubscribeToGuardSpawnerEvents(GuardSpawner);
162
+ set NumInitialGuards += GuardSpawner.GetSpawnLimit()
163
+
164
+ ObjectiveTracker.SetTarget(NumInitialGuards)
165
+
166
+ for (GuardReinforcementSpawner : GuardsReinforcementSpawnersInternal):
167
+ SubscribeToGuardSpawnerEvents(GuardReinforcementSpawner);
168
+
169
+ # Subscribing to reinforcement spawned event
170
+ GuardReinforcementSpawner.SpawnedEvent.Subscribe(OnReinforcementSpawned)
171
+ GuardReinforcementSpawner.AlertedEvent.Subscribe(OnReinforcementAlerted)
172
+ GuardReinforcementSpawner.UnawareEvent.Subscribe(OnReinforcementUnaware)
173
+
174
+ SubscribeToGuardSpawnerEvents(SpawnerDevice:guard_spawner_device):void =
175
+ SpawnerDevice.SpawnedEvent.Subscribe(OnGuardSpawned)
176
+ SpawnerDevice.EliminatedEvent.Subscribe(OnGuardEliminated)
177
+ SpawnerDevice.SuspiciousEvent.Subscribe(OnGuardSuspicious)
178
+ SpawnerDevice.AlertedEvent.Subscribe(OnGuardAlerted)
179
+ SpawnerDevice.TargetLostEvent.Subscribe(OnGuardLostTarget)
180
+ SpawnerDevice.UnawareEvent.Subscribe(OnGuardUnaware)
181
+
182
+ # Start tracking eliminated guards and trigger the explosion
183
+ StartGameplay()<suspends>:void =
184
+ ObjectiveTracker.AssignToAll()
185
+ Sleep(3.0)
186
+ if (FirstPlayer:=GetPlayspace().GetPlayers()[0]):
187
+ ExplosiveDevice.Explode(FirstPlayer)
188
+
189
+ # Runs when guard spawner receives an alerted event and considers only the first alert event
190
+ OnGuardAlerted(InteractionResult:device_ai_interaction_result):void=
191
+ if:
192
+ not ReinforcementTriggered?
193
+ set DetectedPlayer = option{player[InteractionResult.Target?]}
194
+ Guard:=InteractionResult.Source?
195
+ then:
196
+ Log("Game manager - Stronghold is alerted. Incoming reinforcement")
197
+
198
+ var NumGuards:int = ObjectiveTracker.GetTarget()
199
+
200
+ # Enabling the reinforcement guard spawner device ensures that we spawn the amount of guards configured in the guard spawner device.
201
+ for (GuardReinforcementSpawner : GuardsReinforcementSpawnersInternal):
202
+ GuardReinforcementSpawner.Enable()
203
+ set NumGuards += GuardReinforcementSpawner.GetSpawnLimit()
204
+
205
+ ObjectiveTracker.SetTarget(NumGuards)
206
+
207
+ # Displaying in-game message for detection and incoming reinforcement
208
+ MessageDeviceReinforcement.Show()
209
+ set ReinforcementTriggered = true
210
+ # Signal Reinforcement event
211
+ ReinforcementsCalledEvent.Signal(Guard)
212
+
213
+ # Add the guard to the list of alerted guards if it hasn't been previously added
214
+ if(Guard:=InteractionResult.Source?):
215
+ if (not AlertedGuards.Find[Guard]):
216
+ set AlertedGuards += array{Guard}
217
+
218
+ option {set SuspiciousGuards = SuspiciousGuards.RemoveFirstElement[Guard]}
219
+ option {set InvestigatingGuards = InvestigatingGuards.RemoveFirstElement[Guard]}
220
+
221
+ # Broadcast the Player Detected Event when one guard is alerted
222
+ if (AlertedGuards.Length = 1):
223
+ PlayerDetectedEvent.Signal(Guard)
224
+
225
+ # Runs when reinforcement guard spawner receives an alerted event
226
+ OnReinforcementAlerted(InteractionResult:device_ai_interaction_result):void=
227
+ if:
228
+ not FallbackTriggered?
229
+ Guard:=InteractionResult.Source?
230
+ then:
231
+ # Clear leash for reinforcement on alerted so they attack their target
232
+ ReinforcementLeashReference.ClearLeashOnGuard(Guard)
233
+ Log("Game manager - Reinforcement is alerted. Clear Leash.")
234
+
235
+ # Runs when reinforcement guard spawner receives an unaware event
236
+ OnReinforcementUnaware(Guard:agent):void=
237
+ if (not FallbackTriggered?):
238
+ # Set back the leash
239
+ Log("Game manager - Reinforcement is unaware. Reset leash.")
240
+ ReinforcementLeashReference.ApplyLeashOnGuard(Guard)
241
+
242
+ # Runs when guard spawner receives an unaware event
243
+ OnGuardSuspicious(Guard:agent):void=
244
+ if (not SuspiciousGuards.Find[Guard]):
245
+ set SuspiciousGuards += array{Guard}
246
+
247
+ # Broadcast the Suspicious Event when one guard is suspicious
248
+ if:
249
+ SuspiciousGuards.Length = 1
250
+ AlertedGuards.Length = 0
251
+ InvestigatingGuards.Length = 0
252
+ then:
253
+ GuardsSuspiciousEvent.Signal(Guard)
254
+
255
+ # Runs when guard spawner receives an unaware event
256
+ OnGuardUnaware(Guard:agent):void=
257
+ option {set AlertedGuards = AlertedGuards.RemoveFirstElement[Guard]}
258
+ option {set SuspiciousGuards = SuspiciousGuards.RemoveFirstElement[Guard]}
259
+ option {set InvestigatingGuards = InvestigatingGuards.RemoveFirstElement[Guard]}
260
+
261
+ # Broadcast the Unaware Event when no guard is suspicious, alerted or investigating
262
+ if:
263
+ SuspiciousGuards.Length = 0
264
+ AlertedGuards.Length = 0
265
+ InvestigatingGuards.Length = 0
266
+ then:
267
+ GuardsUnawareEvent.Signal(Guard)
268
+
269
+ # When a guard loses track of the player, remove it from the alerted guards list, when all guards have lost player, signal the event
270
+ OnGuardLostTarget(InteractionResult:device_ai_interaction_result):void=
271
+ if (Guard := InteractionResult.Source?):
272
+ if (not InvestigatingGuards.Find[Guard]):
273
+ set InvestigatingGuards += array{Guard}
274
+
275
+ # Broadcast the Player Lost Event when no guard is alerted
276
+ if (set AlertedGuards = AlertedGuards.RemoveFirstElement[Guard]):
277
+ if (AlertedGuards.Length = 0):
278
+ PlayerLostEvent.Signal(Guard)
279
+
280
+
281
+ # Runs when a reinforcement guard is spawned. Each reinforcement guard is forced to attack the player that alerted the Stronghold
282
+ OnReinforcementSpawned(Guard:agent):void=
283
+ set ReinforcementGuards += array{Guard}
284
+ ReinforcementLeashReference.ApplyLeashOnGuard(Guard)
285
+
286
+ # Assigns the player that alerted the Stronghold guards as the target
287
+ if (Target := DetectedPlayer?):
288
+ for (GuardReinforcementSpawner : GuardsReinforcementSpawnersInternal):
289
+ GuardReinforcementSpawner.ForceAttackTarget(Target, ?ForgetTime:=30.0)
290
+ Log("Game manager - ForceAttackTarget given to reinforcement guard")
291
+
292
+
293
+ # Runs when any guard from the Stronghold is spawned
294
+ OnGuardSpawned(Guard:agent):void=
295
+ set StrongholdGuards += array{Guard}
296
+ set NumGuardsSpawned += 1
297
+
298
+ # Runs when initial or reinforcement spawners receive an elimination
299
+ OnGuardEliminated(InteractionResult:device_ai_interaction_result):void=
300
+ set GuardsEliminated += 1
301
+ Log("Game manager - Stronghold guard eliminated count = {GuardsEliminated} / {NumGuardsSpawned}")
302
+
303
+ if (EliminatedAgent := InteractionResult.Target?):
304
+ # Remove eliminated guard from the alerted guards list
305
+ option {set AlertedGuards = AlertedGuards.RemoveFirstElement[EliminatedAgent]}
306
+ option {set SuspiciousGuards = SuspiciousGuards.RemoveFirstElement[EliminatedAgent]}
307
+ option {set InvestigatingGuards = InvestigatingGuards.RemoveFirstElement[EliminatedAgent]}
308
+ option {set StrongholdGuards = StrongholdGuards.RemoveFirstElement[EliminatedAgent]}
309
+ else:
310
+ Log("Game manager - No interaction target for elimination", ?Level:=log_level.Error)
311
+
312
+ if (EliminationAgent := InteractionResult.Source?):
313
+ OnGuardEliminatedByPlayer(EliminationAgent)
314
+ else:
315
+ Log("Game manager - No interaction source for elimination.", ?Level:=log_level.Error)
316
+
317
+ OnGuardEliminatedByPlayer(EliminationPlayer:agent):void=
318
+
319
+ # Increasing progress value for tracker device for each elimination
320
+ ObjectiveTracker.Increment(EliminationPlayer)
321
+
322
+ if (ReinforcementTriggered?):
323
+
324
+ if (NumGuardsSpawned - GuardsEliminated = 3):
325
+ StartFallback()
326
+
327
+ # Ends the game mode if all guards were eliminated without the reinforcements
328
+ if (GuardsEliminated >= NumGuardsSpawned):
329
+ EndGameVictoryDeviceDetected.Activate(EliminationPlayer)
330
+ Log("Game manager - Completed Stronghold detected")
331
+ else:
332
+ # Ends the game mode if all guards were eliminated with the reinforcements
333
+ if (GuardsEliminated >= NumGuardsSpawned):
334
+ EndGameVictoryDeviceUndetected.Activate(EliminationPlayer)
335
+ Log("Game manager - Completed Stronghold undetected")
336
+
337
+ # Assigns a new fallback leash when a few alerted guards are remaining to defend the center of the Stronghold
338
+ StartFallback():void=
339
+ # Displaying in-game message for guards retreating inside the Stronghold building
340
+ MessageDeviceFallback.Show()
341
+ set FallbackTriggered = true
342
+
343
+ for (LeashDevice : LeashesToDisableForFallback):
344
+ LeashDevice.DisableLeashAndPatrolPaths()
345
+
346
+ FallbackLeashPosition := FallbackLeashReference.GetTransform().Translation
347
+ FallbackEvent.Signal()
348
+ for (Guard : StrongholdGuards):
349
+ FallbackLeashReference.ApplyLeashOnGuard(Guard)
350
+ Log("Game manager - Fallback leash is assigned")
351
+
352
+
353
+ # Runs when a player elimination event is received
354
+ OnPlayerEliminated(EliminationResult:elimination_result):void=
355
+ set PlayerRetries -= 1
356
+ if (PlayerRetries = 0, Agent := EliminationResult.EliminatedCharacter.GetAgent[]):
357
+ EndGameFailDevice.Activate(Agent)
358
+ Log("Game manager - Player was eliminated")
359
+
Code_Reference/Stronghold/verse/stronghold_intro_explosion.verse ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/AI }
2
+ using { /Fortnite.com/Characters }
3
+ using { /Fortnite.com/Devices }
4
+ using { /Verse.org/Random }
5
+ using { /Verse.org/Simulation }
6
+ using { /UnrealEngine.com/Temporary/Diagnostics }
7
+ using { /UnrealEngine.com/Temporary/SpatialMath }
8
+
9
+ # Script that handles the explosion sequence
10
+ stronghold_intro_explosion := class(creative_device):
11
+
12
+ @editable
13
+ StrongholdGameManager:stronghold_game_manager := stronghold_game_manager{}
14
+
15
+ # Device for guard spawner who goes to investigate the explosion outside of the base
16
+ @editable
17
+ GuardsInvestigateSpawner:guard_spawner_device := guard_spawner_device{}
18
+
19
+ # Device for the explosion
20
+ @editable
21
+ ExplosiveDevice:explosive_device := explosive_device{}
22
+
23
+ # Device for the fire volume around the vehicle
24
+ @editable
25
+ FireVolumeAroundVehicle:fire_volume_device := fire_volume_device{}
26
+
27
+ # Device for the damage volume around the vehicle
28
+ @editable
29
+ DamageVolumeAroundVehicle:damage_volume_device := damage_volume_device{}
30
+
31
+ # Device for the vehicle explosion
32
+ @editable
33
+ VehicleSpawnerIntroExplosion:vehicle_spawner_big_rig_device := vehicle_spawner_big_rig_device{}
34
+
35
+ # Bark to play when the investigation start
36
+ @editable
37
+ AudioPlayerInvestigateBark:audio_player_device := audio_player_device{}
38
+
39
+ # A reference to the device that contains the level sequence to be played
40
+ @editable
41
+ CinematicSequenceDeviceToPlay:cinematic_sequence_device := cinematic_sequence_device{}
42
+
43
+ StartDamageVolume<private>()<suspends>:void=
44
+
45
+ VehicleSpawnerIntroExplosion.VehicleSpawnedEvent.Await()
46
+ Log("Intro Sequence - Enable Damage Volume")
47
+ DamageVolumeAroundVehicle.Enable()
48
+ ExplosiveDevice.ExplodedEvent.Await()
49
+ Log("Intro Sequence - Start post-explosion")
50
+ DamageVolumeAroundVehicle.Disable()
51
+ FireVolumeAroundVehicle.Ignite()
52
+ CinematicSequenceDeviceToPlay.Play()
53
+ Log("Intro Sequence - Damage Volume Disabled")
54
+
55
+ # Runs when the device is started in a running game
56
+ OnBegin<override>()<suspends>:void=
57
+
58
+ spawn{ StartDamageVolume() }
59
+
60
+ Log("Intro Sequence - Waiting for Guard to Spawn")
61
+ GuardToGoInvestigate:=GuardsInvestigateSpawner.SpawnedEvent.Await()
62
+
63
+ race:
64
+ # stop the intro if guard is alerted
65
+ block:
66
+ GuardsInvestigateSpawner.AlertedEvent.Await()
67
+ Log("Intro Sequence - Guard Alerted")
68
+ return
69
+
70
+ # stop the intro if guard is eliminated
71
+ block:
72
+ GuardsInvestigateSpawner.EliminatedEvent.Await()
73
+ Log("Intro Sequence - Guard Eliminated")
74
+ return
75
+
76
+ block:
77
+
78
+ if:
79
+ Navigatable:=GuardToGoInvestigate.GetFortCharacter[].GetNavigatable[]
80
+ FocusInterface:=GuardToGoInvestigate.GetFortCharacter[].GetFocusInterface[]
81
+ then:
82
+ Log("Intro Sequence - Guard Spawned - Waiting for explosion")
83
+ race:
84
+ ExplosiveDevice.ExplodedEvent.Await()
85
+ Navigatable.Wait()
86
+
87
+ Navigatable.Wait(?Duration:=3.0)
88
+ AudioPlayerInvestigateBark.Play(GuardToGoInvestigate)
89
+
90
+ Log("Intro Sequence - Investigate Bark Played - Setting NavTarget")
91
+ Navigatable.Wait(?Duration:=2.0)
92
+ Log("Intro Sequence - Walking to Explosion")
93
+ InvestigatePosition:= MakeNavigationTarget(Self.GetTransform().Translation)
94
+ Navigatable.NavigateTo(InvestigatePosition,?AllowPartialPath:=true, ?ReachRadius:=100.0)
95
+
96
+ race:
97
+ Sleep(6.0)
98
+ loop:
99
+ RandomOffset := vector3{ X:=GetRandomFloat(-1000.0, 1000.0), Y:=GetRandomFloat(-1000.0, 1000.0), Z:=GetRandomFloat(50.0, 200.0)}
100
+ race:
101
+ FocusInterface.MaintainFocus(Self.GetTransform().Translation + RandomOffset)
102
+ Sleep(GetRandomFloat(0.5, 1.5))
103
+
104
+ else:
105
+ Log("Intro Sequence - No navigation interface", ?Level:=log_level.Error)
106
+
107
+
108
+
Code_Reference/Stronghold/verse/stronghold_leash_position.verse ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /Fortnite.com/AI }
2
+ using { /Fortnite.com/Characters }
3
+ using { /Fortnite.com/Devices }
4
+ using { /UnrealEngine.com/Temporary/Diagnostics }
5
+ using { /UnrealEngine.com/Temporary/SpatialMath }
6
+ using { /Verse.org/Colors }
7
+ using { /Verse.org/Random }
8
+ using { /Verse.org/Simulation }
9
+
10
+ # Defines a leash volume that can be assigned to guards
11
+ stronghold_leash_position := class(creative_device):
12
+
13
+ # Leash is applied by default to all guards spawned by those devices
14
+ @editable
15
+ GuardsSpawners:[]guard_spawner_device := array{}
16
+
17
+ # Guards on those patrol paths can go outside the leash (only one device per path)
18
+ @editable
19
+ PatrolPaths:[]ai_patrol_path_device := array{}
20
+
21
+ # Set the leash inner radius. This value must be in centimeters.
22
+ # This defines the volume that must be reached when this leash is assigned to guards
23
+ @editable
24
+ LeashInnerRadius<private>:float = 2300.0
25
+
26
+ # Set the leash outer radius. This value must be in centimeters
27
+ # This defines the volume in which guards must stay in when this leash is assigned
28
+ @editable
29
+ LeashOuterRadius<private>:float = 2400.0
30
+
31
+ # List of guards currently assigned to this leash
32
+ var<private> Guards : []agent = array{}
33
+
34
+ OnBegin<override>()<suspends>:void=
35
+ DebugDraw : debug_draw = debug_draw{}
36
+ DebugDraw.DrawCylinder(GetTransform().Translation, GetTransform().Translation + vector3{X:=0.0, Y:=0.0, Z:=500.0}, ?NumSegments:=64, ?Radius:= LeashInnerRadius, ?Color:=NamedColors.LightGreen, ?DrawDurationPolicy:=debug_draw_duration_policy.Persistent, ?Thickness:= 3.0)
37
+ DebugDraw.DrawCylinder(GetTransform().Translation, GetTransform().Translation + vector3{X:=0.0, Y:=0.0, Z:=500.0}, ?NumSegments:=64, ?Radius:= LeashOuterRadius, ?Color:=NamedColors.Orange, ?DrawDurationPolicy:=debug_draw_duration_policy.Persistent, ?Thickness:= 3.0)
38
+
39
+ for (GuardSpawner : GuardsSpawners):
40
+ GuardSpawner.SpawnedEvent.Subscribe(ApplyLeashOnGuard)
41
+
42
+ for (PatrolPath : PatrolPaths):
43
+ PatrolPath.PatrolPathStartedEvent.Subscribe(PatrolPathStarted)
44
+ PatrolPath.PatrolPathStoppedEvent.Subscribe(PatrolPathStopped)
45
+
46
+ ApplyLeashOnGuard(Guard:agent):void=
47
+ Log("Leash position - Applying leash {LeashInnerRadius} / {LeashOuterRadius} on guard")
48
+ if (Leashable:=Guard.GetFortCharacter[].GetFortLeashable[]):
49
+ Leashable.SetLeashPosition(GetTransform().Translation, LeashInnerRadius, LeashOuterRadius)
50
+
51
+ set Guards += array{Guard}
52
+
53
+ ClearLeashOnGuard(Guard:agent):void=
54
+ Log("Leash position - Clearing leash {LeashInnerRadius} / {LeashOuterRadius} on guard")
55
+ if (Leashable:=Guard.GetFortCharacter[].GetFortLeashable[]):
56
+ Leashable.ClearLeash()
57
+
58
+ option {set Guards = Guards.RemoveFirstElement[Guard]}
59
+
60
+ DisableLeashAndPatrolPaths():void=
61
+ Log("Leash position - Disabling leash {LeashInnerRadius} / {LeashOuterRadius}")
62
+ for (Guard : Guards):
63
+ if (Leashable:=Guard.GetFortCharacter[].GetFortLeashable[]):
64
+ Leashable.ClearLeash()
65
+
66
+ for (PatrolPath : PatrolPaths):
67
+ PatrolPath.Disable()
68
+
69
+ set Guards = array{}
70
+
71
+ PatrolPathStarted(Guard:agent):void=
72
+ if:
73
+ Guards.Find[Guard]
74
+ Leashable:=Guard.GetFortCharacter[].GetFortLeashable[]
75
+ then:
76
+ Log("Leash position - Patrol started - Clearing leash {LeashInnerRadius} / {LeashOuterRadius} on guard")
77
+ Leashable.ClearLeash()
78
+
79
+ PatrolPathStopped(Guard:agent):void=
80
+ if:
81
+ Guards.Find[Guard]
82
+ Leashable:=Guard.GetFortCharacter[].GetFortLeashable[]
83
+ then:
84
+ Log("Leash position - Patrol stopped - Setting leash {LeashInnerRadius} / {LeashOuterRadius} on guard")
85
+ Leashable.SetLeashPosition(GetTransform().Translation, LeashInnerRadius, LeashOuterRadius)
Code_Reference/Stronghold/verse/stronghold_log.verse ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using { /UnrealEngine.com/Temporary/Diagnostics }
2
+
3
+ # Logger for the Stronghold experience. This log channel prints the Stronghold game's progression to the output log of UEFN.
4
+
5
+ stronghold_log_channel := class(log_channel){}
6
+
7
+ Log<internal>(Text:string, ?Level:log_level=log_level.Debug):void=
8
+
9
+ Logger:log = log{Channel:=stronghold_log_channel}
10
+ Logger.Print(Text, ?Level:=Level)
11
+
12
+