Spaces:
Runtime error
Runtime error
prop hunt and stronghold .verse files added
Browse files- Code_Reference/PropHunt/txt/base_team.txt +81 -0
- Code_Reference/PropHunt/txt/billboard_cinematic_flythrough.txt +187 -0
- Code_Reference/PropHunt/txt/heartbeat.txt +121 -0
- Code_Reference/PropHunt/txt/hunter_team.txt +28 -0
- Code_Reference/PropHunt/txt/prop_hunt.txt +189 -0
- Code_Reference/PropHunt/txt/prop_team.txt +147 -0
- Code_Reference/PropHunt/txt/round_timer.txt +158 -0
- Code_Reference/PropHunt/txt/waiting_for_more_players.txt +131 -0
- Code_Reference/PropHunt/verse/base_team.verse +81 -0
- Code_Reference/PropHunt/verse/billboard_cinematic_flythrough.verse +187 -0
- Code_Reference/PropHunt/verse/heartbeat.verse +121 -0
- Code_Reference/PropHunt/verse/hunter_team.verse +28 -0
- Code_Reference/PropHunt/verse/prop_hunt.verse +189 -0
- Code_Reference/PropHunt/verse/prop_team.verse +147 -0
- Code_Reference/PropHunt/verse/round_timer.verse +158 -0
- Code_Reference/PropHunt/verse/waiting_for_more_players.verse +131 -0
- Code_Reference/Stronghold/txt/stronghold_alert_manager.txt +0 -0
- Code_Reference/Stronghold/txt/stronghold_bark_manager.txt +0 -0
- Code_Reference/Stronghold/txt/stronghold_billboard_cinematic.txt +0 -0
- Code_Reference/Stronghold/txt/stronghold_game_manager.txt +0 -0
- Code_Reference/Stronghold/txt/stronghold_intro_explosion.txt +0 -0
- Code_Reference/Stronghold/txt/stronghold_leash_position.txt +0 -0
- Code_Reference/Stronghold/txt/stronghold_log.txt +0 -0
- Code_Reference/Stronghold/verse/stronghold_alert_manager.verse +87 -0
- Code_Reference/Stronghold/verse/stronghold_bark_manager.verse +247 -0
- Code_Reference/Stronghold/verse/stronghold_billboard_cinematic.verse +149 -0
- Code_Reference/Stronghold/verse/stronghold_game_manager.verse +359 -0
- Code_Reference/Stronghold/verse/stronghold_intro_explosion.verse +108 -0
- Code_Reference/Stronghold/verse/stronghold_leash_position.verse +85 -0
- 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 |
+
|