Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main/java/net/tfminecraft/magic/gear/orb/GearOrb.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.bukkit.Location;

import net.tfminecraft.magic.util.OrbTrail;

/**
* One virtual orb circling a crafting station. Particle only, no entity, so it is never
* hit by anything but the plugin's own hitscan.
Expand All @@ -10,6 +12,7 @@ public final class GearOrb {

private final Location anchor;
private Location location;
private final OrbTrail trail = new OrbTrail();
private final boolean good;
private double angle;
private final double radius;
Expand Down Expand Up @@ -54,6 +57,15 @@ public Location getLocation() {

public void setLocation(Location location) {
this.location = location.clone();
trail.push(this.location);
}

/**
* Where the orb was {@code ticksAgo} ticks back, for ping compensated hits, or null
* if it had not spawned yet.
*/
public Location getLocation(int ticksAgo) {
return trail.ticksAgo(ticksAgo);
}

public boolean isGood() {
Expand Down
36 changes: 21 additions & 15 deletions src/main/java/net/tfminecraft/magic/gear/orb/GearOrbService.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import net.tfminecraft.magic.gear.WeaponLore;
import net.tfminecraft.magic.gear.WeaponRequirement;
import net.tfminecraft.magic.gear.WeaponRift;
import net.tfminecraft.magic.util.OrbTrail;

/**
* Drives the orb runs. One session per station, so two players cannot attune the same
Expand Down Expand Up @@ -251,24 +252,29 @@ private static GearOrb hitscan(Player player, GearOrbSession session) {
double range = OrbCache.clickRange;
double radiusSq = OrbCache.hitRadius * OrbCache.hitRadius;

// Judge the click against where the orb was when this player saw it.
int[] rewind = OrbTrail.rewindTicks(player, OrbCache.maxRewindTicks);

GearOrb best = null;
double bestT = range + 1;
for (GearOrb orb : session.livingOrbs()) {
Location loc = orb.getLocation();
if (loc.getWorld() == null || !loc.getWorld().equals(world)) {
continue;
}
Vector oc = loc.toVector().subtract(origin);
double t = oc.dot(dir);
if (t < 0 || t > range) {
continue;
}
if (oc.lengthSquared() - t * t > radiusSq) {
continue;
}
if (t < bestT) {
bestT = t;
best = orb;
for (int ticksAgo : rewind) {
Location loc = orb.getLocation(ticksAgo);
if (loc == null || loc.getWorld() == null || !loc.getWorld().equals(world)) {
continue;
}
Vector oc = loc.toVector().subtract(origin);
double t = oc.dot(dir);
if (t < 0 || t > range) {
continue;
}
if (oc.lengthSquared() - t * t > radiusSq) {
continue;
}
if (t < bestT) {
bestT = t;
best = orb;
}
}
}
if (best == null) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/net/tfminecraft/magic/gear/orb/OrbCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public final class OrbCache {

public static double hitRadius = 0.5;
public static double clickRange = 12.0;
/** Most ticks a click is rewound for the player's ping (10 = 500 ms). */
public static int maxRewindTicks = 10;

public static double orbitRadius = 2.4;
public static double orbitHeight = 1.1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ private static void loadMeditation(ConfigurationSection section) {
MeditationCache.startOrbRange = section.getDouble("start_orb_range", MeditationCache.startOrbRange);
MeditationCache.orbHitRadius = section.getDouble("orb_hit_radius", MeditationCache.orbHitRadius);
MeditationCache.orbClickRange = section.getDouble("orb_click_range", MeditationCache.orbClickRange);
MeditationCache.maxRewindTicks = Math.max(0, section.getInt("max_rewind_ticks", MeditationCache.maxRewindTicks));
MeditationCache.mentalCostPerHit = section.getInt("mental_cost_per_hit", MeditationCache.mentalCostPerHit);
MeditationCache.surgeLockSeconds = section.getInt("surge_lock_seconds", MeditationCache.surgeLockSeconds);
MeditationCache.maxLiveOrbs = section.getInt("max_live_orbs", MeditationCache.maxLiveOrbs);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/net/tfminecraft/magic/loader/GearLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ private static boolean loadOrbs(File file) {
}
OrbCache.hitRadius = Math.max(0.1, config.getDouble("hit_radius", OrbCache.hitRadius));
OrbCache.clickRange = Math.max(1.0, config.getDouble("click_range", OrbCache.clickRange));
OrbCache.maxRewindTicks = Math.max(0, config.getInt("max_rewind_ticks", OrbCache.maxRewindTicks));
OrbCache.orbitRadius = Math.max(0.5, config.getDouble("orbit_radius", OrbCache.orbitRadius));
OrbCache.orbitHeight = config.getDouble("orbit_height", OrbCache.orbitHeight);
OrbCache.orbitBob = Math.max(0.0, config.getDouble("orbit_bob", OrbCache.orbitBob));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public final class MeditationCache {
public static double startOrbRange = 2.5;
public static double orbHitRadius = 0.5;
public static double orbClickRange = 12.0;
/** Most ticks a click is rewound for the player's ping (10 = 500 ms). */
public static int maxRewindTicks = 10;
public static int mentalCostPerHit = 1;
public static int surgeLockSeconds = 10;
public static int maxLiveOrbs = 8;
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/net/tfminecraft/magic/meditation/MeditationOrb.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

import org.bukkit.Location;

import net.tfminecraft.magic.util.OrbTrail;

public final class MeditationOrb {

private Location location;
private final OrbTrail trail = new OrbTrail();
private final Location spawnLocation;
private Location returnFrom;
private final UUID sourceId;
Expand Down Expand Up @@ -64,6 +67,19 @@ public Location getLocation() {

public void setLocation(Location location) {
this.location = location.clone();
trail.push(this.location);
}

/**
* Where the orb was {@code ticksAgo} ticks back, for ping compensated hits, or null
* if it had not spawned yet. The starter follows the player's aim, so it is always
* judged where it is now.
*/
public Location getLocation(int ticksAgo) {
if (starter) {
return location;
}
return trail.ticksAgo(ticksAgo);
}

public Location getSpawnLocation() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import net.tfminecraft.magic.profile.MagicProfileService;
import net.tfminecraft.magic.session.ResonanceSession;
import net.tfminecraft.magic.session.ResonanceSessionManager;
import net.tfminecraft.magic.util.OrbTrail;
import net.tfminecraft.rpcharacters.RPCharacters;
import net.tfminecraft.rpcharacters.focus.FocusService;

Expand Down Expand Up @@ -250,25 +251,30 @@ private MeditationOrb hitscan(Player player, MeditationSession session) {
double radiusSq = radius * radius;
Vector origin = eye.toVector();

// Judge the click against where the orb was when this player saw it.
int[] rewind = OrbTrail.rewindTicks(player, MeditationCache.maxRewindTicks);

MeditationOrb best = null;
double bestT = range + 1;
for (MeditationOrb orb : session.livingOrbs()) {
Location loc = orb.getLocation();
if (loc.getWorld() == null || !loc.getWorld().equals(world)) {
continue;
}
Vector oc = loc.toVector().subtract(origin);
double t = oc.dot(dir);
if (t < 0 || t > range) {
continue;
}
double distSq = oc.lengthSquared() - t * t;
if (distSq > radiusSq) {
continue;
}
if (t < bestT) {
bestT = t;
best = orb;
for (int ticksAgo : rewind) {
Location loc = orb.getLocation(ticksAgo);
if (loc == null || loc.getWorld() == null || !loc.getWorld().equals(world)) {
continue;
}
Vector oc = loc.toVector().subtract(origin);
double t = oc.dot(dir);
if (t < 0 || t > range) {
continue;
}
double distSq = oc.lengthSquared() - t * t;
if (distSq > radiusSq) {
continue;
}
if (t < bestT) {
bestT = t;
best = orb;
}
}
}
if (best == null) {
Expand Down
66 changes: 66 additions & 0 deletions src/main/java/net/tfminecraft/magic/util/OrbTrail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package net.tfminecraft.magic.util;

import org.bukkit.Location;
import org.bukkit.entity.Player;

/**
* Recent positions of a moving orb, one per tick, so a click can be judged against where
* the player actually saw it.
*
* <p>A player sees an orb about half their ping late and their click reaches us another
* half ping later, so by the time the swing lands the orb has moved on by roughly one
* round trip. Rewinding by the player's own ping keeps the game the same for everyone
* instead of widening the hit radius for all.
*/
public final class OrbTrail {

/** Ticks kept per orb. Also the hard ceiling on any configured rewind. */
public static final int CAPACITY = 20;

private final Location[] positions = new Location[CAPACITY];
private int head = -1;
private int size;

/** Records this tick's position. Call once per tick, after the orb has moved. */
public void push(Location location) {
head = (head + 1) % CAPACITY;
positions[head] = location.clone();
if (size < CAPACITY) {
size++;
}
}

/**
* Position {@code ticksAgo} ticks back, or null when the orb did not exist yet, so a
* late click is never matched to a frame the player could not have seen.
*/
public Location ticksAgo(int ticksAgo) {
int back = Math.max(0, ticksAgo);
if (back >= size) {
return null;
}
return positions[Math.floorMod(head - back, CAPACITY)];
}

/**
* Ticks to rewind for this player's ping, as the two whole ticks either side of it so
* a ping between ticks is judged against both frames the player could have seen. A
* ping of exactly whole ticks also gets the newer frame. Zero ping gets {@code [0, 0]}
* and low ping {@code [0, 1]}, so their game is unchanged.
*/
public static int[] rewindTicks(Player player, int maxTicks) {
int ping = player == null ? 0 : Math.max(0, player.getPing());
return rewindTicks(ping, maxTicks);
}

public static int[] rewindTicks(int pingMs, int maxTicks) {
int cap = Math.max(0, Math.min(maxTicks, CAPACITY - 1));
double ticks = Math.min(cap, Math.max(0, pingMs) / 50.0);
int low = (int) Math.floor(ticks);
int high = Math.min(cap, (int) Math.ceil(ticks));
if (low == high) {
low = Math.max(0, low - 1);
}
return new int[] {low, high};
}
}
3 changes: 3 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ meditation:
start_orb_range: 2.5
orb_hit_radius: 0.5
orb_click_range: 12
# A click is judged against where the orb was when the player saw it, rewound by
# their ping up to this many ticks (10 = 500 ms). 0 turns it off.
max_rewind_ticks: 10
mental_cost_per_hit: 1
surge_lock_seconds: 10
max_live_orbs: 8
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/gear/orbs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
# Hitscan. Radius is the cylinder around the aim ray an orb must fall inside.
hit_radius: 0.5
click_range: 12
# A click is judged against where the orb was when the player saw it, rewound by
# their ping up to this many ticks (10 = 500 ms). 0 turns it off.
max_rewind_ticks: 10

# Orbit around the station block centre.
orbit_radius: 2.4
Expand Down