Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
2110f6d
Add blood path records, bound items and the underground blood altar
XxFran10xX Oct 25, 2026
5fc0686
Add the whisper meter and the blood path voices
XxFran10xX Oct 25, 2026
128f693
Add animal sacrifices, altar corruption and stolen-animal clues
XxFran10xX Oct 25, 2026
fb99be8
Add the god's question, the devotee mark and bound blood armour
XxFran10xX Oct 25, 2026
8785046
Add the hunt, hidden bonds and the final sacrifices
XxFran10xX Oct 25, 2026
2ead407
Document the blood path staff commands
XxFran10xX Oct 25, 2026
7f12c28
Drag knocked-out victims to the altar and give the blood path real looks
XxFran10xX Oct 25, 2026
df12a40
Give every blood path moment its own sounds and particles
XxFran10xX Oct 25, 2026
9bcbb31
Write the blood path's voices and lore as finished roleplay
XxFran10xX Oct 25, 2026
262a7c0
Make Lorvear, the Blood Father, the voice of the blood path
XxFran10xX Oct 25, 2026
def05d5
Let the Void Reaper speak as the mage's own dark self
XxFran10xX Oct 25, 2026
c152a6f
Close the holes found in review
XxFran10xX Oct 25, 2026
36e8d81
Tighten the review fixes
XxFran10xX Oct 25, 2026
1aebfd2
Check the floor layer too before pulling a body to the altar
XxFran10xX Oct 25, 2026
92eac59
Name the Sanguinar armour after the hunt's victims
XxFran10xX Oct 25, 2026
547b824
Give blood-path characters a permission for their skins
XxFran10xX Oct 26, 2026
b8a3a00
Let a blood path hold more than one permission
XxFran10xX Oct 26, 2026
58cf35b
Open bound animals in one stroke and quiet the first voice
XxFran10xX Oct 26, 2026
250254e
Corrupt the altar room only with blocks a bloodmagic shrine counts
XxFran10xX Oct 26, 2026
6e8f937
Move the blood path's config into wildmagic/bloodmagic
XxFran10xX Oct 26, 2026
ddca7b6
Guard every block the altar has changed
XxFran10xX Oct 26, 2026
d1d4ab5
Shape the altar's room for the meditation circle
XxFran10xX Oct 26, 2026
ae614ee
Sit on the altar to meditate
XxFran10xX Oct 26, 2026
a0a7210
Describe the blood knife as winding, not curved
XxFran10xX Oct 26, 2026
5e8125c
Vein-Reaver from MMOItems, hide Unbreakable on blood items
XxFran10xX Oct 26, 2026
a87181a
Note the blood knife's new name
XxFran10xX Oct 26, 2026
ae49624
Meditate on the altar with /sit; both voices in the animal stage, dri…
XxFran10xX Oct 26, 2026
5d4d6f3
The altar sets out its own pedestal ring
XxFran10xX Oct 26, 2026
0649834
Corruption never decorates on or under a barrier
XxFran10xX Oct 26, 2026
841ee37
The altar lays a default chamber and locks it
XxFran10xX Oct 26, 2026
033fc38
Animal rite bleeds out; voices wait for the chamber and grow colder; …
XxFran10xX Oct 26, 2026
d668abb
No candles in the chamber by default
XxFran10xX Oct 26, 2026
37a9db4
Rite music while the animal bleeds; voices keep quiet without an altar
XxFran10xX Oct 26, 2026
23e9a44
Rite sounds heard nearby, quietly; shared sounds stay in the room
XxFran10xX Oct 26, 2026
52ca34a
Read animal owners from BreedingBuddies, then Cooking
XxFran10xX Oct 26, 2026
abb6a3a
Revert "Read animal owners from BreedingBuddies, then Cooking"
XxFran10xX Oct 26, 2026
d82c96e
Leads come off on the stone; corruption grows in patches, candles bef…
XxFran10xX Oct 26, 2026
bdd05ee
The god's question as a spoken dialogue; altar lines by stage; stone …
XxFran10xX Oct 26, 2026
9b96dec
Question music by default, looping while the question lasts
XxFran10xX Oct 26, 2026
92c1a11
corruption.ambient_blocks: how many blocks the slow spread changes ea…
XxFran10xX Oct 26, 2026
28da311
Light the blood altar's candles with vanilla flame particles
XxFran10xX Oct 26, 2026
618c3c1
Raise the altar candle flames to sit on the wick tips
XxFran10xX Oct 26, 2026
fa5878b
Centre the altar candle flames over the new vanilla-style wicks
XxFran10xX Oct 26, 2026
98178f9
Assign the blood classes and give them their final-sacrifice rewards
XxFran10xX Oct 27, 2026
036beda
Give Bloodsworn 2 skill points per final sacrifice
XxFran10xX Oct 27, 2026
143f0ff
Give final-sacrifice boons to the true path only
XxFran10xX Oct 27, 2026
84598e1
Make the true path's mana boon a health boon
XxFran10xX Oct 27, 2026
6a9ff32
Test the boon stats with two kinds after the mana boon went
XxFran10xX Oct 27, 2026
16e18e1
Give the blood paths artefacts and a pool of blood that needs feeding
XxFran10xX Oct 27, 2026
aab41b7
Charge blood artefacts in the pool and meditate on the altar for reso…
XxFran10xX Oct 27, 2026
debdd7b
Use the chant for blood meditation and charging; drop the room ambience
XxFran10xX Oct 28, 2026
db20771
Keep blood magic's disk writes off the main thread's busy paths
XxFran10xX Oct 28, 2026
c35ed6b
Clear stray altar displays from chunks that load before Magic
XxFran10xX Oct 28, 2026
83e5350
Leave blood altar seats to blood meditation and move its displays hal…
XxFran10xX Oct 28, 2026
2479d18
Mark devotees Manaless and let the victim on the altar hear their death
XxFran10xX Oct 28, 2026
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
29 changes: 29 additions & 0 deletions ADMIN-COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,32 @@ semantics. `tools/dev-admin-values.cjs` exercises both this plugin and the
RPCharacters Focus commands on TFMCDev01 through its existing Dev bot helpers.
It uses only the dedicated `AdminValueProbe` character, removes its temporary
operator status, resets its test values and clears its test inventory afterward.

## Blood path

Staff tools for the hidden blood magic progression (`bloodmagic.yml`). Player
targets must be online on the character being edited. Records and altars are
saved under `data/bloodmagic/`.

```text
/magic blood list
/magic blood info <player>
/magic blood stage <player> <none|whispered|animal|choice|hunt|final|ascended>
/magic blood path <player> <none|devotee|true>
/magic blood give <player> <altar|ritual_knife|blood_knife>
/magic blood whisper <player> [amount]
/magic blood voice <player>
/magic blood target <player> <list|reroll|skip|set <character name>>
/magic blood bonds <player>
/magic blood altar <info|tp|refresh|purify> <player|altar id>
/magic blood reset <player>
```

`whisper` without an amount wakes the voices at once; with an amount it adds to
the hidden meter. `voice` speaks the next line now. `target` manages the final
sacrifices: `reroll` drops the named target for a fresh pick, `set` names a living
character, `skip` counts the named target as done without a death. `bonds` lists
the character's ten closest bonds with their hidden scores. Altars cannot be
broken by anyone; `altar purify` removes one and puts back every block it
corrupted, and an altar purifies itself when its owner character dies. `reset`
purifies the altar and clears the whole path; items already handed out stay.
34 changes: 33 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.proc>full</maven.compiler.proc>
<interactiblefurniture.version>0.2.0</interactiblefurniture.version>
<rpcharacters.version>2.16.0</rpcharacters.version>
<rpcharacters.version>2.31.0</rpcharacters.version>
<tlibs.version>2.6.0</tlibs.version>
</properties>

Expand Down Expand Up @@ -88,6 +88,16 @@
</exclusion>
</exclusions>
</dependency>
<!-- NBT-API's NBT class references authlib; mocking it in tests needs the type. -->
<dependency>
<groupId>com.mojang</groupId>
<artifactId>authlib</artifactId>
<version>7.0.61</version>
<scope>test</scope>
<exclusions>
<exclusion><groupId>*</groupId><artifactId>*</artifactId></exclusion>
</exclusions>
</dependency>
<!-- TLibs' ItemSkinPreserver needs NBTAPI types; the NBTAPI plugin provides them at runtime. -->
<dependency>
<groupId>local</groupId>
Expand Down Expand Up @@ -143,13 +153,35 @@
</exclusion>
</exclusions>
</dependency>
<!-- Optional at runtime: softdepend in plugin.yml. Blood altars refuse WorldGuard regions. -->
<dependency>
<groupId>com.sk89q.worldguard</groupId>
<artifactId>worldguard-bukkit</artifactId>
<version>7.0.9</version>
<scope>provided</scope>
</dependency>
<!-- WorldGuard references metrics classes omitted from its published POM. -->
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>enginehub</id>
<url>https://maven.enginehub.org/repo/</url>
</repository>
<repository>
<id>minecraft-libraries</id>
<url>https://libraries.minecraft.net/</url>
</repository>
</repositories>

<build>
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/net/tfminecraft/magic/Magic.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import net.tfminecraft.magic.artifact.shrine.ShrineChargeService;
import net.tfminecraft.magic.attunement.ArtifactDisplayIndex;
import net.tfminecraft.magic.attunement.AuraLog;
import net.tfminecraft.magic.blood.BloodMagic;
import net.tfminecraft.magic.listener.ArtifactFillChestListener;
import net.tfminecraft.magic.listener.ArtifactListener;
import net.tfminecraft.magic.listener.CastDriftListener;
Expand Down Expand Up @@ -157,6 +158,7 @@ public void onEnable() {
MagicTickService.register(ctx -> SpellModifierApplyService.syncChangedOnline(
resonanceGuiManager.getSessionManager()));
}
BloodMagic.start(this);
MagicTickService.start();
getServer().getScheduler().runTask(this, ArtifactDisplayIndex::rebuildFromSaved);
getServer().getScheduler().runTask(this, GearStationStore::load);
Expand Down Expand Up @@ -197,6 +199,7 @@ public void onDisable() {
shutdownStep("fill chests", ArtifactFillChestService::clearAll);
shutdownStep("shrine charges", ShrineChargeService::clearAll);
shutdownStep("sacrifice rites", SacrificeRiteService::clearAll);
shutdownStep("blood magic", BloodMagic::stop);
shutdownStep("spell modifiers", () -> {
if (isMmoStackPresent()) {
SpellModifierApplyService.clearAll();
Expand Down Expand Up @@ -277,6 +280,7 @@ private boolean loadConfigs() {
ok &= chargesLoader.loadSafe(new File(getDataFolder(), "charges.yml"));
ok &= gearLoader.loadFolder(new File(getDataFolder(), "gear"));
ok &= ArtifactConfigLoader.loadFolder(new File(getDataFolder(), "artifacts"));
ok &= BloodMagic.loadConfig(getDataFolder(), getLogger());
AuraLog.configure(Cache.loggingEnabled, Cache.wipeLog, getDataFolder());
if (ok) {
getLogger().info("[Magic] Configs loaded.");
Expand Down Expand Up @@ -304,6 +308,7 @@ private void createFolders() {
mkdir("artifacts");
mkdir("gear");
mkdir("data/characters");
mkdir("data/bloodmagic/characters");
}

private void mkdir(String relativePath) {
Expand All @@ -320,6 +325,16 @@ private void createConfigs() {
"gui.yml",
"skills.yml",
"charges.yml",
"wildmagic/bloodmagic/settings.yml",
"wildmagic/bloodmagic/items.yml",
"wildmagic/bloodmagic/voices.yml",
"wildmagic/bloodmagic/animals.yml",
"wildmagic/bloodmagic/corruption.yml",
"wildmagic/bloodmagic/choice.yml",
"wildmagic/bloodmagic/armour.yml",
"wildmagic/bloodmagic/artefacts.yml",
"wildmagic/bloodmagic/fx.yml",
"wildmagic/bloodmagic/messages.yml",
"gear/archetypes.yml",
"gear/part-types.yml",
"gear/parts.yml",
Expand Down
39 changes: 37 additions & 2 deletions src/main/java/net/tfminecraft/magic/Messages.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,44 @@ public static void loadFromResources() {
}
}

/**
* Lays a module's own message file over messages.yml: its live keys win over the main file, and its
* bundled copy (resourcePath in the jar) backs them up the same way messages.yml's does.
*/
public static void overlay(File file, String resourcePath) {
FileConfiguration part = readResource(resourcePath);
if (part != null) {
bundled = copyInto(part, bundled);
}
if (file == null || !file.isFile()) {
return;
}
FileConfiguration live = new YamlConfiguration();
try {
live.load(file);
config = copyInto(live, config);
} catch (IOException | InvalidConfigurationException ex) {
Magic.plugin.getLogger().severe("[Magic] Failed to load " + file.getName() + ": " + ex.getMessage());
}
}

private static FileConfiguration copyInto(FileConfiguration from, FileConfiguration to) {
FileConfiguration target = to != null ? to : new YamlConfiguration();
for (String key : from.getKeys(true)) {
if (!from.isConfigurationSection(key)) {
target.set(key, from.get(key));
}
}
return target;
}

private static FileConfiguration readBundled() {
return readResource("messages.yml");
}

private static FileConfiguration readResource(String name) {
try {
InputStream resource = Magic.plugin.getResource("messages.yml");
InputStream resource = Magic.plugin.getResource(name);
if (resource == null) {
return null;
}
Expand All @@ -50,7 +85,7 @@ private static FileConfiguration readBundled() {
return loaded;
}
} catch (Exception ex) {
Magic.plugin.getLogger().warning("[Magic] Failed to load bundled messages.yml: " + ex.getMessage());
Magic.plugin.getLogger().warning("[Magic] Failed to load bundled " + name + ": " + ex.getMessage());
return null;
}
}
Expand Down
146 changes: 146 additions & 0 deletions src/main/java/net/tfminecraft/magic/blood/AltarCandles.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package net.tfminecraft.magic.blood;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.function.BiConsumer;

import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitTask;

/**
* The flames on the altar's candles are vanilla's own candle particles (fx.yml candles:): every few
* ticks a small flame on each wick of the altar's model, now and then a wisp of smoke, only while a
* player is near. The wick positions are in the model's pixels and follow the altar's facing and scale.
*/
public final class AltarCandles {

/** A wick top in model pixels, and the highest altar level that still has that candle. */
record Wick(double x, double y, double z, int maxLevel) {}

private static boolean enabled;
private static int everyTicks;
private static double smokeChance;
private static double viewRange;
private static float yawOffset;
private static Particle flame;
private static List<Wick> wicks = List.of();
private static BukkitTask task;
private static long ticks;
static Random random = new Random();

static {
reset();
}

private AltarCandles() {}

static void reset() {
enabled = false;
everyTicks = 8;
smokeChance = 0.1;
viewRange = 24;
yawOffset = 180;
flame = Particle.SMALL_FLAME;
wicks = List.of();
}

static void load(ConfigurationSection section, java.util.logging.Logger log) {
reset();
if (section == null) {
return;
}
enabled = section.getBoolean("enabled", true);
everyTicks = Math.max(1, section.getInt("every_ticks", 8));
smokeChance = Math.max(0, Math.min(1, section.getDouble("smoke_chance", 0.1)));
viewRange = Math.max(1, section.getDouble("view_range", 24));
yawOffset = (float) section.getDouble("yaw_offset", 180);
String name = section.getString("particle", "SMALL_FLAME");
try {
flame = Particle.valueOf(name.trim().toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ex) {
log.warning("[Magic] blood magic config fx.candles.particle: unknown particle '" + name + "'");
}
List<Wick> out = new ArrayList<>();
for (Object raw : section.getList("wicks", List.of())) {
if (raw instanceof List<?> list && list.size() >= 3 && list.get(0) instanceof Number x
&& list.get(1) instanceof Number y && list.get(2) instanceof Number z) {
int max = list.size() > 3 && list.get(3) instanceof Number m ? m.intValue() : Integer.MAX_VALUE;
out.add(new Wick(x.doubleValue(), y.doubleValue(), z.doubleValue(), max));
} else {
log.warning("[Magic] blood magic config fx.candles.wicks: '" + raw + "' is not [x, y, z, max_level]");
}
}
wicks = List.copyOf(out);
}

static void startTicking(Plugin plugin) {
stop();
task = plugin.getServer().getScheduler().runTaskTimer(plugin, AltarCandles::tick, 20L, 1L);
}

static void stop() {
if (task != null) {
task.cancel();
task = null;
}
}

/** Every tick; flames go up every every_ticks for altars with a player in view. */
static void tick() {
if (!enabled || wicks.isEmpty() || ++ticks % everyTicks != 0) {
return;
}
for (BloodAltar altar : BloodAltars.all()) {
Location center = altar.center();
if (center == null || !center.getWorld().isChunkLoaded(altar.getX() >> 4, altar.getZ() >> 4)
|| !watched(center)) {
continue;
}
for (Wick wick : wicks) {
if (altar.getLevel() <= wick.maxLevel()) {
light(position(center, altar.getYaw(), wick));
}
}
}
}

private static void light(Location at) {
spawner.accept(flame, at);
if (random.nextDouble() < smokeChance) {
spawner.accept(Particle.SMOKE, at.clone().add(0, 0.05, 0));
}
}

/** Puts one particle at a spot for the players around it. */
static BiConsumer<Particle, Location> spawner = (particle, at) -> at.getWorld().spawnParticle(particle, at, 1, 0, 0, 0, 0);

private static boolean watched(Location center) {
for (Player player : center.getWorld().getPlayers()) {
if (player.getLocation().distanceSquared(center) <= viewRange * viewRange) {
return true;
}
}
return false;
}

/**
* Where a wick of the model is in the world: the model is centred on the altar's block, scaled by
* altar.display_scale and turned by the altar's yaw (plus yaw_offset for how item displays face).
*/
static Location position(Location center, float yaw, Wick wick) {
double scale = BloodConfig.displayScale();
double x = (wick.x() - 8) / 16 * scale;
double y = (wick.y() - 8) / 16 * scale;
double z = (wick.z() - 8) / 16 * scale;
double angle = Math.toRadians(-(yaw + yawOffset));
double cos = Math.cos(angle);
double sin = Math.sin(angle);
return center.clone().add(x * cos + z * sin, y, -x * sin + z * cos);
}
}
Loading
Loading