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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Beyond a name and appearance, characters have traits, professions, injuries, and
- **Progression and rolls** — bring professions, attributes, and dice rolls into character gameplay.
- **Injuries and recovery** — represent injuries and prosthetics, with related treatment and progression systems.
- **Consequences and investigation** — support lethal or nonlethal PvP, graves, and discoverable clues left in the world.
- **Nonlethal knockouts** — GSit holds downed players in a crawl pose for the knockout duration, alongside freeze and blindness. The pose uses GSit's API, bypasses command restrictions, respects other plugins' crawl vetoes, and releases only knockout-created crawls on recovery. GSit is optional for the rest of RPCharacters; without it, knockouts retain freeze and blindness only.
- **PvP strikes** — after `/pvp start`, the fight lasts 15 minutes unless the player who started it runs `/pvp end`. When it ends, players who have not died see a title that a new RP interaction is needed. Whoever kills or knocks someone out chooses to spare them or give a strike; the third strike kills the character, though a killer can wound or maim instead of killing. Lockpicking, robbing, pickpocketing and looting locked graves start a timed evil RP session, during which any strike kills and a death leaves an unlocked grave.

## Beyond the game
Expand Down
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
</properties>

<dependencies>
<dependency>
<groupId>maven.modrinth</groupId>
<artifactId>gsit</artifactId>
<version>3.3.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down Expand Up @@ -136,6 +142,10 @@
</dependencies>

<repositories>
<repository>
<id>modrinth</id>
<url>https://api.modrinth.com/maven</url>
</repository>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/net/tfminecraft/rpcharacters/pvp/KnockoutCrawl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package net.tfminecraft.rpcharacters.pvp;

import java.util.function.Predicate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;

import dev.geco.gsit.api.GSitAPI;
import dev.geco.gsit.api.event.PrePlayerStopCrawlEvent;
import dev.geco.gsit.model.Crawl;
import dev.geco.gsit.model.StopReason;

/** Loaded only when GSit is enabled. Uses the crawl API shared by GSit 3.2 and 3.3. */
final class KnockoutCrawl implements Listener {

private final Predicate<Player> knockedOut;
private final Map<UUID, Crawl> ownedCrawls = new HashMap<>();

KnockoutCrawl(Predicate<Player> knockedOut) {
this.knockedOut = knockedOut;
}

void enforce(Player player) {
// The command checks permissions/ground state and toggles an existing crawl off.
// The API starts the pose directly and also handles the player's client-side crawl.
if (!GSitAPI.isPlayerCrawling(player)) {
Crawl crawl = GSitAPI.startCrawl(player);
if (crawl != null) {
ownedCrawls.put(player.getUniqueId(), crawl);
}
}
}

void release(Player player) {
Crawl crawl = ownedCrawls.remove(player.getUniqueId());
if (crawl != null && GSitAPI.getCrawlByPlayer(player) == crawl) {
GSitAPI.stopCrawl(crawl, StopReason.PLUGIN);
}
}

@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onStopCrawl(PrePlayerStopCrawlEvent event) {
if (event.getReason().isCancellable() && knockedOut.test(event.getPlayer())) {
event.setCancelled(true);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.entity.Pose;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.HandlerList;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
Expand All @@ -34,8 +35,15 @@ public final class PvpKnockoutManager implements Listener {

private final Map<UUID, Knockout> knockouts = new ConcurrentHashMap<>();
private BukkitTask tickTask;
private KnockoutCrawl crawl;

public void start() {
if (crawl == null && Bukkit.getPluginManager().isPluginEnabled("GSit")) {
crawl = new KnockoutCrawl(this::isKnockedOut);
Bukkit.getPluginManager().registerEvents(crawl, RPCharacters.plugin);
} else if (crawl == null) {
RPCharacters.plugin.getLogger().warning("GSit is not enabled; knockout freeze and blindness will work, but the downed crawl pose is unavailable.");
}
if (tickTask != null) {
tickTask.cancel();
}
Expand All @@ -52,7 +60,17 @@ public void shutdown() {
tickTask.cancel();
tickTask = null;
}
for (UUID id : knockouts.keySet()) {
Player player = Bukkit.getPlayer(id);
if (player != null) {
releaseKnockout(player);
}
}
knockouts.clear();
if (crawl != null) {
HandlerList.unregisterAll(crawl);
crawl = null;
}
}

/**
Expand Down Expand Up @@ -115,7 +133,23 @@ static Player attackingPlayer(EntityDamageEvent event) {

@EventHandler
public void onQuit(PlayerQuitEvent event) {
knockouts.remove(event.getPlayer().getUniqueId());
releaseKnockout(event.getPlayer());
}

@EventHandler
public void onDeath(PlayerDeathEvent event) {
releaseKnockout(event.getEntity());
}

private void releaseKnockout(Player player) {
if (knockouts.remove(player.getUniqueId()) != null && crawl != null) {
crawl.release(player);
}
}

private boolean isKnockedOut(Player player) {
Knockout knockout = knockouts.get(player.getUniqueId());
return knockout != null && System.currentTimeMillis() < knockout.untilMs && !shouldSkipTick(player);
}

private void applyKnockout(Player player) {
Expand All @@ -126,7 +160,6 @@ private void applyKnockout(Player player) {
}
player.setHealth(Math.max(0.1, Math.min(1.0, maxHealth)));

player.performCommand("crawl");
int durationTicks = PvpLoader.getKnockoutSeconds() * 20;
player.addPotionEffect(new PotionEffect(
PotionEffectType.BLINDNESS,
Expand All @@ -139,6 +172,9 @@ private void applyKnockout(Player player) {
knockouts.put(player.getUniqueId(), new Knockout(
player.getLocation().clone(),
System.currentTimeMillis() + PvpLoader.getKnockoutSeconds() * 1000L));
if (crawl != null) {
crawl.enforce(player);
}
}

private void tick() {
Expand All @@ -147,17 +183,17 @@ private void tick() {
while (it.hasNext()) {
Map.Entry<UUID, Knockout> entry = it.next();
Player player = Bukkit.getPlayer(entry.getKey());
if (player == null || !player.isOnline()) {
if (player == null) {
it.remove();
continue;
}
if (now >= entry.getValue().untilMs || shouldSkipTick(player)) {
it.remove();
if (!player.isOnline() || now >= entry.getValue().untilMs || shouldSkipTick(player)) {
releaseKnockout(player);
continue;
}
enforceFreeze(player, entry.getValue().location);
if (!isCrawling(player)) {
player.performCommand("crawl");
if (crawl != null) {
crawl.enforce(player);
}
}
}
Expand Down Expand Up @@ -198,14 +234,6 @@ private void enforceFreeze(Player player, Location freezeAt) {
player.teleport(dest);
}

private boolean isCrawling(Player player) {
Pose pose = player.getPose();
if ("CRAWLING".equals(pose.name())) {
return true;
}
return pose == Pose.SWIMMING && !player.isInWater();
}

private static final class Knockout {
private final Location location;
private final long untilMs;
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ version: ${project.version}
author: Drefvelin

depend: [TLibs, MMOCore, TFMCWeb, MMOItems, MythicLib]
softdepend: [ProtocolLib, PlaceholderAPI, LuckPerms, WorldGuard, SimpleFactions]
softdepend: [ProtocolLib, PlaceholderAPI, LuckPerms, WorldGuard, SimpleFactions, GSit]

commands:
focus:
Expand Down Expand Up @@ -56,4 +56,4 @@ permissions:
rpchar.grave.admin:
default: op
professions.admin:
default: op
default: op
Loading
Loading