/
ProximityTrigger.java
332 lines (305 loc) · 14.1 KB
/
ProximityTrigger.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
package com.denizenscript.denizen.scripts.triggers.core;
import com.denizenscript.denizen.scripts.containers.core.InteractScriptContainer;
import com.denizenscript.denizen.utilities.DenizenAPI;
import com.denizenscript.denizen.utilities.debugging.Debug;
import com.denizenscript.denizen.npc.traits.TriggerTrait;
import com.denizenscript.denizen.objects.NPCTag;
import com.denizenscript.denizen.objects.PlayerTag;
import com.denizenscript.denizen.scripts.triggers.AbstractTrigger;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import java.util.*;
public class ProximityTrigger extends AbstractTrigger implements Listener {
// <--[language]
// @name Proximity Triggers
// @group NPC Interact Scripts
// @description
// Proximity Triggers are triggered when when a player moves in the area around the NPC.
//
// Proximity triggers must have a sub-key identifying what type of proximity trigger to use.
// The three types are "entry", "exit", and "move".
//
// Entry and exit do exactly as the names imply: Entry fires when the player walks into range of the NPC, and exit fires when the player walks out of range.
//
// Move is a bit more subtle: it fires very rapidly so long as a player remains within range of the NPC.
// This is useful for eg script logic that needs to be constantly updating whenever a player is nearby (eg a combat NPC script needs to constantly update its aim).
//
// The radius that the proximity trigger detects at is set by <@link command trigger>.
//
// -->
//
// Default to 75, but dynamically set by checkMaxProximities().
// If a Player is further than this distance from an NPC, less
// logic is run in checking.
//
private static int maxProximityDistance = 75; // TODO: is this reasonable to have?
// <--[action]
// @Actions
// enter proximity
//
// @Triggers when a player enters the NPC's proximity trigger's radius.
//
// @Context
// None
//
// -->
// <--[action]
// @Actions
// exit proximity
//
// @Triggers when a player exits the NPC's proximity trigger's radius.
//
// @Context
// None
//
// -->
// <--[action]
// @Actions
// move proximity
//
// @Triggers when a player moves inside the NPC's proximity trigger's radius.
//
// @Context
// None
//
// -->
// Technically defined in TriggerTrait, but placing here instead.
// <--[action]
// @Actions
// proximity
//
// @Triggers when a player moves inside the NPC's proximity trigger's radius.
//
// @Context
// None
//
// -->
int taskID = -1;
@Override
public void onEnable() {
Bukkit.getServer().getPluginManager().registerEvents(this, DenizenAPI.getCurrentInstance());
final ProximityTrigger trigger = this;
taskID = Bukkit.getScheduler().scheduleSyncRepeatingTask(DenizenAPI.getCurrentInstance(), new Runnable() {
@Override
public void run() {
Collection<? extends Player> allPlayers = Bukkit.getOnlinePlayers();
//
// Iterate over all of the NPCs
//
for (NPC citizensNPC : CitizensAPI.getNPCRegistry()) {
if (citizensNPC == null || !citizensNPC.isSpawned()) {
continue;
}
//
// If the NPC doesn't have triggers, or the Proximity Trigger is not enabled,
// then just return.
//
if (!citizensNPC.hasTrait(TriggerTrait.class) || !citizensNPC.getOrAddTrait(TriggerTrait.class).isEnabled(name)) {
continue;
}
NPCTag npc = new NPCTag(citizensNPC);
TriggerTrait triggerTrait = npc.getTriggerTrait();
// Loop through all players
for (Player bukkitPlayer : allPlayers) {
//
// If this NPC is not spawned or in a different world, no need to check,
// unless the Player hasn't yet triggered an Exit Proximity after Entering
//
if (!npc.getWorld().equals(bukkitPlayer.getWorld())
&& hasExitedProximityOf(bukkitPlayer, npc)) {
continue;
}
//
// If this NPC is more than the maxProximityDistance, skip it, unless
// the Player hasn't yet triggered an 'Exit Proximity' after entering.
//
if (!isCloseEnough(bukkitPlayer, npc)
&& hasExitedProximityOf(bukkitPlayer, npc)) {
continue;
}
// Get the player
PlayerTag player = PlayerTag.mirrorBukkitPlayer(bukkitPlayer);
//
// Check to make sure the NPC has an assignment. If no assignment, a script doesn't need to be parsed,
// but it does still need to trigger for cooldown and action purposes.
//
InteractScriptContainer script = npc.getInteractScriptQuietly(player, ProximityTrigger.class);
//
// Set default ranges with information from the TriggerTrait. This allows per-npc overrides and will
// automatically check the config for defaults.
//
double entryRadius = triggerTrait.getRadius(name);
double exitRadius = triggerTrait.getRadius(name);
double moveRadius = triggerTrait.getRadius(name);
//
// If a script was found, it might have custom ranges.
//
if (script != null) {
try {
if (script.hasTriggerOptionFor(ProximityTrigger.class, player, null, "ENTRY RADIUS")) {
entryRadius = Integer.valueOf(script.getTriggerOptionFor(ProximityTrigger.class, player, null, "ENTRY RADIUS"));
}
}
catch (NumberFormatException nfe) {
Debug.echoDebug(script, "Entry Radius was not an integer. Assuming " + entryRadius + " as the radius.");
}
try {
if (script.hasTriggerOptionFor(ProximityTrigger.class, player, null, "EXIT RADIUS")) {
exitRadius = Integer.valueOf(script.getTriggerOptionFor(ProximityTrigger.class, player, null, "EXIT RADIUS"));
}
}
catch (NumberFormatException nfe) {
Debug.echoDebug(script, "Exit Radius was not an integer. Assuming " + exitRadius + " as the radius.");
}
try {
if (script.hasTriggerOptionFor(ProximityTrigger.class, player, null, "MOVE RADIUS")) {
moveRadius = Integer.valueOf(script.getTriggerOptionFor(ProximityTrigger.class, player, null, "MOVE RADIUS"));
}
}
catch (NumberFormatException nfe) {
Debug.echoDebug(script, "Move Radius was not an integer. Assuming " + moveRadius + " as the radius.");
}
}
Location npcLocation = npc.getLocation();
//
// If the Player switches worlds while in range of an NPC, trigger still needs to
// fire since technically they have exited proximity. Let's check that before
// trying to calculate a distance between the Player and NPC, which will throw
// an exception if worlds do not match.
//
boolean playerChangedWorlds = false;
if (npcLocation.getWorld() != player.getWorld()) {
playerChangedWorlds = true;
}
//
// If the user is outside the range, and was previously within the
// range, then execute the "Exit" script.
//
// If the user entered the range and were not previously within the
// range, then execute the "Entry" script.
//
// If the user was previously within the range and moved, then execute
// the "Move" script.
//
boolean exitedProximity = hasExitedProximityOf(bukkitPlayer, npc);
double distance = 0;
if (!playerChangedWorlds) {
distance = npcLocation.distance(player.getLocation());
}
if (!exitedProximity
&& (playerChangedWorlds || distance >= exitRadius)) {
if (!triggerTrait.triggerCooldownOnly(trigger, player)) {
continue;
}
// Remember that NPC has exited proximity.
exitProximityOf(bukkitPlayer, npc);
// Exit Proximity Action
npc.action("exit proximity", player);
// Parse Interact Script
parse(npc, player, script, "EXIT");
}
else if (exitedProximity && distance <= entryRadius) {
// Cooldown
if (!triggerTrait.triggerCooldownOnly(trigger, player)) {
continue;
}
// Remember that Player has entered proximity of the NPC
enterProximityOf(bukkitPlayer, npc);
// Enter Proximity Action
npc.action("enter proximity", player);
// Parse Interact Script
parse(npc, player, script, "ENTRY");
}
else if (!exitedProximity && distance <= moveRadius) {
// TODO: Remove this? Constantly cooling down on move may make
// future entry/exit proximities 'lag' behind. Temporarily removing
// cooldown on 'move proximity'.
// if (!npc.getTriggerTrait().triggerCooldownOnly(this, event.getPlayer()))
// continue;
// Move Proximity Action
npc.action("move proximity", player);
// Parse Interact Script
parse(npc, player, script, "MOVE");
}
}
}
}
}, 5, 5);
}
@Override
public void onDisable() {
Bukkit.getScheduler().cancelTask(taskID);
}
/**
* Checks if the Player in Proximity is close enough to be calculated.
*
* @param player the Player
* @param npc the NPC
* @return true if within maxProximityDistance in all directions
*/
private boolean isCloseEnough(Player player, NPCTag npc) {
Location pLoc = player.getLocation();
Location nLoc = npc.getLocation();
if (Math.abs(pLoc.getX() - nLoc.getX()) > maxProximityDistance) {
return false;
}
if (Math.abs(pLoc.getY() - nLoc.getY()) > maxProximityDistance) {
return false;
}
if (Math.abs(pLoc.getZ() - nLoc.getZ()) > maxProximityDistance) {
return false;
}
return true;
}
private static Map<UUID, Set<Integer>> proximityTracker = new HashMap<>();
//
// Ensures that a Player who has entered proximity of an NPC also fires Exit Proximity.
//
private boolean hasExitedProximityOf(Player player, NPCTag npc) {
// If Player hasn't entered proximity, it's not in the Map. Return true, must be exited.
Set<Integer> existing = proximityTracker.get(player.getUniqueId());
if (existing == null) {
return true;
}
// If Player has no entry for this NPC, return true.
if (!existing.contains(npc.getId())) {
return true;
}
// Entry is present, NPC has not yet triggered exit proximity.
return false;
}
/**
* Called when a 'Enter Proximity' has been called to make sure an exit
* proximity will be called.
*
* @param player the Player
* @param npc the NPC
*/
private void enterProximityOf(Player player, NPCTag npc) {
Set<Integer> npcs = proximityTracker.get(player.getUniqueId());
if (npcs == null) {
npcs = new HashSet<>();
proximityTracker.put(player.getUniqueId(), npcs);
}
npcs.add(npc.getId());
}
/**
* Called when an 'Exit Proximity' has been called. Once successfully exited,
* a Player can enter proximity again.
*
* @param player the Player
* @param npc the NPC
*/
private void exitProximityOf(Player player, NPCTag npc) {
Set<Integer> npcs = proximityTracker.get(player.getUniqueId());
if (npcs == null) {
npcs = new HashSet<>();
proximityTracker.put(player.getUniqueId(), npcs);
}
npcs.remove(npc.getId());
}
}