Skip to content

Commit b61a13c

Browse files
authored
[Feat] Ability pickup refund, depleted slot interactions, and chargeable ability system (#278)
* Feat: Ability system foundation — typed abilities, slot management, throwDirect API - Add ABILITY_ID and ABILITY_SLOT PDC keys to KeyRegistry - Uncomment and wire AbilityItemBuilder/AbilityItemReader (PDC tagging now live) - Fix AbilitySlotItem: isSatisfied/setEquipped/buildPlaceholder now use ABILITY_SLOT_KEY - AbilitySkill: replace single useType() with useTypes() Set<AbilityUseType> — STACK, DURABILITY, and COOLDOWN are now non-exclusive and combinable per ability - AbilitySlotManager: tracks remainingUses and remainingDurability independently per slot; deplete fires when any finite resource hits zero - AbilityStateRepository: stores both remaining_uses and remaining_durability columns; returns SlotState record; save/saveAll updated to match - VisualProjectile: add displayScale field (default 1.0f) — determineOrientation() now respects it, enabling per-throw scale overrides without subclassing - ThrowAction.throwDirect(): new API that spawns a ThrownItem and releases immediately without the onReady() windup loop — suitable for instant-release abilities - KnifeThrowAbility: fires a diamond sword projectile at 0.5 scale via throwDirect; useTypes() = {STACK, COOLDOWN}, 3 max uses, 10-tick cooldown overlay * Feat: Skill availability, slot state persistence, and ability casting - Add SkillAvailability (AVAILABLE/DEPLETED/RELINQUISHED) tracked per player - Add SkillSlotState record persisting uses, durability, and cooldown expiry per ability slot so cooldowns survive log-off cycles - Persist slot states via new SkillStateRepository (player_skill_slot_state table) - Migrate player_available_skills schema to v2 (adds availability column) - Refactor AbilitySlotManager to delegate uses/durability tracking to PlayerSkillContainer instead of local fields - Implement handleAbilityInput in SwordPlayer — LEFT tap / RIGHT_HOLD cast with canPerform, persistent cooldown guard, consumeUse, and cooldown write-back - Remove registerActiveSkillSlot from InputRegistrar (replaced by handleAbilityInput) - Fix AbilitySlotItem EMPTY/LOCKED restore to clear stale ability items on unequip - Add TestAlpha/Beta/Gamma no-op abilities with colored chat feedback for testing - Add skill system README * Feat: Slot-index ability detection, throw prevention, and unequip fix - Replace PDC tag lookup in handleAbilityInput with slot-index check via AbilitySlotManager.getActiveTypeForHeldSlot — more reliable than reading ITEM_TYPE_KEY from the held item which may be gunpowder or untagged - Add boolean[] slotEnabled to AbilitySlotManager, synced after initialize, refresh, and deplete; drives getActiveTypeForHeldSlot - Implement notHoldingAbilityItem() in SwordPlayer using getActiveTypeForHeldSlot; wired into canBeginThrow() which gates the Throw Ready input node - Fix AbilitySlotItem EMPTY/LOCKED restore to clear stale ability items on unequip * Feat: Knife throw pickup refund, ability re-cast fix, and CharacterMenu replenish button - KnifeThrowAbility now extends ActivatableAbility (not ThrowableAbility) — throw is click-to-cast with no aim windup - Tag knife projectile with ABILITY_ID_KEY so pickup can identify it - InteractiveItemArbiter refunds one use to the original thrower on projectile pickup, regardless of who picks it up - AbilitySlotManager.addUse() transitions depleted slots back to EQUIPPED when a use is returned - ThrowAction.throwDirect resets attemptingThrow on completion, fixing ability re-cast lock - CharacterMenu: dev replenish button (TODO: remove) resets both ability slots to full uses * Feat: Ability pickup refund, depleted slot interactions, chargeable ability system, and IceSpellAbility - Dash and grab now pick up ability projectiles when holding an equipped or depleted ability item - Thrown ability projectile pickup (dash/grab/catch) refunds a use via AbilitySlotManager - DEPLETED slot state now recognized as an ability slot for all pickup interactions - handleAbilityInput only consumes LEFT inputs; RIGHT paths flow through the input tree - Fixed input tree lock-out: ability slot always returns true to prevent tree advancement - Added AbilityType.CHARGEABLE, ChargeableAbility abstract class, ChargeSession, ChargeAction - Chargeable abilities hook into RIGHT (charge start) and RIGHT_HOLD (charge release) in the input tree - IceSpellAbility: BLUE_ICE block, scale 0.25→3.0 over 3s, sine-wave bob, Y-axis rotation, releaseable at 0.5 scale - addUse() calls updateInventory() to force client sync after projectile refund - Non-ability items in an equipped slot are relocated gracefully on equip * Add: Potion effect infinite duration constants, plugin dependency auto-download, and remove stale TODOs Add PotionEffect.INFINITE_DURATION constant instead of magic number 99999999. Add downloadPlugins block to build.gradle for DisplayEntityUtils, PacketEvents, and ProtocolLib. Remove obsolete TODO about ability casting from different actions. * Fix: Spotless
1 parent 2d1375d commit b61a13c

55 files changed

Lines changed: 3768 additions & 151 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/btm/sword/config/Config.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2605,7 +2605,7 @@ public static class Movement {
26052605
); }
26062606

26072607
/** Delay for flat dash height boost in milliseconds. */
2608-
public static int FLAT_DASH_HEIGHT_BOOST_DELAY_MS = 100;
2608+
public static int FLAT_DASH_HEIGHT_BOOST_DELAY_MS = 75;
26092609
static { register(
26102610
"movement.flat_dash_height_boost_delay_ms",
26112611
FLAT_DASH_HEIGHT_BOOST_DELAY_MS, Integer.class,

src/main/java/btm/sword/system/action/movement/Dash.java

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import btm.sword.system.control.SwordScheduler;
1919
import btm.sword.system.control.TimeArbiter;
2020
import btm.sword.system.entity.impl.Combatant;
21+
import btm.sword.system.entity.impl.SwordPlayer;
2122
import btm.sword.system.entity.umbral.UmbralBlade;
2223
import btm.sword.system.entity.umbral.statemachine.state.LodgedState;
2324
import btm.sword.system.entity.umbral.statemachine.state.WaitingState;
@@ -26,6 +27,7 @@
2627
import btm.sword.utility.display.ParticleWrapper;
2728
import btm.sword.utility.entity.EntityUtil;
2829
import btm.sword.utility.entity.HitboxUtil;
30+
import btm.sword.utility.math.VectorUtil;
2931
import btm.sword.utility.sound.SoundUtil;
3032
import btm.sword.utility.sound.SwordSoundType;
3133

@@ -40,6 +42,7 @@ public class Dash {
4042
private DashType dashType;
4143
private double dashPower;
4244
private boolean holdingLink;
45+
private boolean itemRetrieved;
4346

4447
private static final Supplier<Double> MAX_STRAIGHT_DASH_DISTANCE = () -> Config.Movement.DASH_MAX_DISTANCE;
4548
private static final Supplier<Double> NORMAL_ITEM_RAY_WIDTH = () -> Config.Movement.DASH_RAY_HITBOX_RADIUS;
@@ -64,6 +67,7 @@ private enum DashType {
6467
}
6568

6669
public void execute() {
70+
itemRetrieved = false;
6771
dashType = DashType.NOTHING;
6872
onGround = EntityUtil.isOnGround(ex);
6973
holdingLink = executor.holdingSoulLink();
@@ -93,6 +97,13 @@ else if (holdingLink) {
9397
}
9498
}
9599
}
100+
// Holding an ability item — only pick up matching ability projectiles
101+
else if (executor instanceof SwordPlayer sp && !sp.notHoldingAbilityItem()) {
102+
targetedDisplay = raycastForAbilityProjectile();
103+
if (targetedDisplay != null) {
104+
dashType = flatDash ? DashType.FLAT_TO_ITEM : DashType.NORMAL_TO_ITEM;
105+
}
106+
}
96107

97108
if (dashType == DashType.NOTHING || (targetedDisplay != null && isDashToItemImpeded())) {
98109
if (flatDash) {
@@ -168,6 +179,27 @@ public ItemDisplay raycastForNormalItem() {
168179
return itemDisplay;
169180
}
170181

182+
/**
183+
* Raycasts for ability projectiles only (items tagged with {@code ABILITY_ID_KEY}).
184+
* Used when the executor is holding an ability item and can only pick up matching projectiles.
185+
*/
186+
public ItemDisplay raycastForAbilityProjectile() {
187+
ItemDisplay itemDisplay = (ItemDisplay) HitboxUtil.ray(
188+
executor.eyeLoc(),
189+
executor.dir(),
190+
MAX_STRAIGHT_DASH_DISTANCE.get(),
191+
NORMAL_ITEM_RAY_WIDTH.get(),
192+
entity -> entity instanceof ItemDisplay id &&
193+
!entity.isDead() &&
194+
InteractiveItemArbiter.checkIfInteractive(id) &&
195+
InteractiveItemArbiter.isAbilityProjectile(id)
196+
);
197+
198+
if (itemDisplay != null) Debug.movement("Ability Projectile Targeted.. itemDisplay=" + itemDisplay.getName());
199+
200+
return itemDisplay;
201+
}
202+
171203
private void dash() {
172204
dashPower = flatDash ? Config.Movement.DASH_BASE_POWER * Config.Movement.FLAT_DASH_POWER_MULTIPLIER : Config.Movement.DASH_BASE_POWER;
173205

@@ -225,16 +257,27 @@ private void performFlatDashToItem(double distanceToItem) {
225257

226258
boolean umbralHeightBoost = !holdingLink || targetedBlade.inState(WaitingState.class) || targetedBlade.inState(LodgedState.class);
227259

260+
final Vector exDir = executor.dir();
261+
228262
if (umbralHeightBoost &&
229263
heightDiff <= Config.Movement.DASH_FLAT_HEIGHT_UPPER &&
230264
heightDiff > Config.Movement.DASH_FLAT_HEIGHT_LOWER) {
231-
executor.setVelocity(executor.dir()
232-
.add(Config.Direction.UP().multiply(Config.Movement.DASH_FLAT_ITEM_DASH_UPWARD_SCALER)));
265+
266+
Vector dashVelocity = exDir.add(
267+
Config.Direction.UP().multiply(Config.Movement.DASH_FLAT_ITEM_DASH_UPWARD_SCALER)
268+
);
269+
executor.setVelocity(dashVelocity);
270+
}
271+
272+
final Vector toItem = calcVectorToItem().normalize();
273+
if (VectorUtil.isBroken(toItem)) return;
274+
275+
if (!itemRetrieved && distanceToItem > 3) { // TODO: COnfig
276+
SwordScheduler.runBukkitTaskLater(() -> // Velocity applied after the initial height boost
277+
executor.setVelocity(toItem.multiply(Math.log(distanceToItem * Config.Movement.DASH_FLAT_ITEM_DASH_DISTANCE_SCALER))),
278+
!umbralHeightBoost ? 0 : Config.Movement.FLAT_DASH_HEIGHT_BOOST_DELAY_MS, TimeUnit.MILLISECONDS
279+
);
233280
}
234-
SwordScheduler.runBukkitTaskLater(() ->
235-
executor.setVelocity(executor.dir().multiply(Math.log(distanceToItem * Config.Movement.DASH_FLAT_ITEM_DASH_DISTANCE_SCALER))),
236-
!umbralHeightBoost ? 0 : Config.Movement.FLAT_DASH_HEIGHT_BOOST_DELAY_MS, TimeUnit.MILLISECONDS
237-
);
238281
}
239282

240283
private void scheduleCheckForItemPickup() {
@@ -263,6 +306,7 @@ private void onItemRetrieval() {
263306
SoundUtil.playSound(ex, SwordSoundType.ENTITY_ENDER_DRAGON_FLAP, Config.Movement.DASH_FLAP_SOUND_VOLUME, Config.Movement.DASH_FLAP_SOUND_PITCH);
264307
SoundUtil.playSound(ex, SwordSoundType.ENTITY_PLAYER_ATTACK_SWEEP, Config.Movement.DASH_SWEEP_SOUND_VOLUME, Config.Movement.DASH_SWEEP_SOUND_PITCH);
265308
executor.setVelocity(calcPostRetrievalVector());
309+
itemRetrieved = true;
266310
InteractiveItemArbiter.onGrab(targetedDisplay, executor); // here is where the display is taken care of
267311
}
268312

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package btm.sword.system.action.skill;
2+
3+
/**
4+
* Classifies the physical and interaction behavior of an ability.
5+
*
6+
* <ul>
7+
* <li>{@link #ACTIVATABLE} — Press to activate; has a cooldown; never consumed.</li>
8+
* <li>{@link #CHARGEABLE} — Hold right-click to charge; release to fire a scaled projectile.</li>
9+
* <li>{@link #THROWABLE} — Aimed-throw on hold; the physical item is consumed on each throw.</li>
10+
* <li>{@link #CONSUMABLE} — One-shot activation; the physical item is consumed on use.</li>
11+
* <li>{@link #PASSIVE} — Always active while the slot is equipped; no activation required.</li>
12+
* <li>{@link #QUEST} — Fully usable ability <em>and</em> surrenderable as a quest turn-in.</li>
13+
* </ul>
14+
*/
15+
public enum AbilityType {
16+
ACTIVATABLE,
17+
CHARGEABLE,
18+
THROWABLE,
19+
CONSUMABLE,
20+
PASSIVE,
21+
QUEST
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package btm.sword.system.action.skill;
2+
3+
/**
4+
* Describes how an ability's uses are consumed when activated.
5+
*
6+
* <ul>
7+
* <li>{@link #STACK} — each use decrements the item's stack count; depleted at zero.</li>
8+
* <li>{@link #DURABILITY} — each use damages the item's durability bar; depleted when broken.</li>
9+
* <li>{@link #COOLDOWN} — the item is never consumed; activation only triggers a cooldown.</li>
10+
* </ul>
11+
*/
12+
public enum AbilityUseType {
13+
14+
/** Item stack count decrements each use; depleted at zero. */
15+
STACK,
16+
17+
/** Single item whose durability bar decreases; depleted when broken. */
18+
DURABILITY,
19+
20+
/** Never consumed — activation only triggers a cooldown. */
21+
COOLDOWN
22+
}

0 commit comments

Comments
 (0)