Make first/second/gem spell selection less confusing

- cast and shoot now both use the slot that's visually selected in the hud
- swapping between first/second slot is done only by sneaking
- there are now messages in the hud indicating the spell being cast
- gems now have a cooldown after being used
- holding a gem now changes the selector icon and title to make it clear that spells can be swapped
- activating an ability whilst holding a gem changes the message to indicate you're casting the spell from the gem rather than your spell slot
- ability titles now reflect what they do when sneaking
- equipping a spell from a gem whilst in creative mode no longer consumes the gem's spell
This commit is contained in:
Sollace 2023-05-29 14:13:52 +01:00
parent a8e5c50e9b
commit 4202723731
16 changed files with 164 additions and 95 deletions

View file

@ -91,17 +91,25 @@ public interface Ability<T extends Hit> {
/**
* The icon representing this ability on the UI and HUD.
*/
default Identifier getIcon(Pony player, boolean swap) {
default Identifier getIcon(Pony player) {
Identifier id = Abilities.REGISTRY.getId(this);
return new Identifier(id.getNamespace(), "textures/gui/ability/" + id.getPath() + ".png");
}
default Text getName(Pony player) {
return getName();
}
/**
* The display name for this ability.
*/
default Text getName() {
return Text.translatable(getTranslationKey());
}
default String getTranslationKey() {
Identifier id = Abilities.REGISTRY.getId(this);
return Text.translatable("ability." + id.getNamespace() + "." + id.getPath().replace('/', '.'));
return "ability." + id.getNamespace() + "." + id.getPath().replace('/', '.');
}
/**

View file

@ -1,21 +1,24 @@
package com.minelittlepony.unicopia.ability;
public enum AbilitySlot {
/**
* No slot. Corresponds to abilities that are not equipped.
*/
NONE,
/**
* The primary ability. Corresponds to the largest ring in the HUD
* The primary ability slot. Corresponds to the largest ring in the HUD
*/
PRIMARY,
/**
* THe secondary ability. Corresponds to the top small ring in the HUD
* The secondary ability slot. Corresponds to the top small ring in the HUD
*/
SECONDARY,
/**
* The tertiary ability. Corresponds to the bottom small ring in the HUD.
* The tertiary ability slot. Corresponds to the bottom small ring in the HUD.
*/
TERTIARY,
/**
* The passive primary ability. Appears in place of the primary ability whilst sneaking.
* The passive primary ability slot. Appears in place of the primary ability whilst sneaking.
*/
PASSIVE;

View file

@ -0,0 +1,70 @@
package com.minelittlepony.unicopia.ability;
import com.minelittlepony.unicopia.*;
import com.minelittlepony.unicopia.ability.data.Hit;
import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType;
import com.minelittlepony.unicopia.entity.player.Pony;
import com.minelittlepony.unicopia.particle.MagicParticleEffect;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
import net.minecraft.util.TypedActionResult;
abstract class AbstractSpellCastingAbility implements Ability<Hit> {
@Override
public int getCooldownTime(Pony player) {
return 0;
}
@Override
public boolean canUse(Race race) {
return race.canCast();
}
@Override
public Text getName(Pony player) {
CustomisedSpellType<?> spell = player.getCharms().getEquippedSpell(player.getCharms().getHand());
TypedActionResult<CustomisedSpellType<?>> gemSpell = player.getCharms().getSpellInHand(false);
var active = !player.getAbilities().getStat(AbilitySlot.PRIMARY).getActiveAbility().isEmpty();
if (!spell.isEmpty()) {
if (active) {
if (gemSpell.getResult().isAccepted()) {
return Text.translatable(getTranslationKey() + ".with_spell.hand",
gemSpell.getValue().type().getName().copy().formatted(gemSpell.getValue().type().getAffinity().getColor())
);
}
return Text.translatable(getTranslationKey() + ".with_spell.active",
spell.type().getName().copy().formatted(spell.type().getAffinity().getColor())
);
}
return Text.translatable(getTranslationKey() + ".with_spell" + (gemSpell.getResult().isAccepted() ? ".replacing" : ""),
spell.type().getName().copy().formatted(spell.type().getAffinity().getColor()),
gemSpell.getValue().type().getName().copy().formatted(gemSpell.getValue().type().getAffinity().getColor())
);
}
return getName();
}
@Override
public int getColor(Pony player) {
TypedActionResult<CustomisedSpellType<?>> newSpell = player.getCharms().getSpellInHand(false);
if (newSpell.getResult() != ActionResult.FAIL) {
return newSpell.getValue().type().getColor();
}
return -1;
}
@Override
public Hit.Serializer<Hit> getSerializer() {
return Hit.SERIALIZER;
}
@Override
public void postApply(Pony player, AbilitySlot slot) {
player.spawnParticles(MagicParticleEffect.UNICORN, 5);
}
}

View file

@ -39,7 +39,7 @@ public class PegasusFlightToggleAbility implements Ability<Hit> {
}
@Override
public Identifier getIcon(Pony player, boolean swap) {
public Identifier getIcon(Pony player) {
Identifier id = Abilities.REGISTRY.getId(this);
return new Identifier(id.getNamespace(), "textures/gui/ability/" + id.getPath()
+ (player.getPhysics().isFlying() ? "_land" : "_takeoff")

View file

@ -33,23 +33,13 @@ import net.minecraft.util.math.random.Random;
* 2. If the player is holding a gem, consumes it and casts whatever spell is contained within onto the user.
* 3. If the player is holding a amulet, charges it.
*/
public class UnicornCastingAbility implements Ability<Hit> {
public class UnicornCastingAbility extends AbstractSpellCastingAbility {
@Override
public int getWarmupTime(Pony player) {
return 20;
}
@Override
public int getCooldownTime(Pony player) {
return 0;
}
@Override
public boolean canUse(Race race) {
return race.canCast();
}
@Override
@Nullable
public Hit tryActivate(Pony player) {
@ -59,11 +49,6 @@ public class UnicornCastingAbility implements Ability<Hit> {
return Hit.of(player.getMagicalReserves().getMana().get() >= getCostEstimate(player));
}
@Override
public Hit.Serializer<Hit> getSerializer() {
return Hit.SERIALIZER;
}
@Override
public double getCostEstimate(Pony player) {
TypedActionResult<ItemStack> amulet = getAmulet(player);
@ -74,8 +59,7 @@ public class UnicornCastingAbility implements Ability<Hit> {
return Math.min(manaLevel, ((AmuletItem)amulet.getValue().getItem()).getChargeRemainder(amulet.getValue()));
}
Hand hand = player.asEntity().isSneaking() ? Hand.OFF_HAND : Hand.MAIN_HAND;
TypedActionResult<CustomisedSpellType<?>> spell = player.getCharms().getSpellInHand(hand);
TypedActionResult<CustomisedSpellType<?>> spell = player.getCharms().getSpellInHand(false);
return !spell.getResult().isAccepted() || spell.getValue().isOn(player) ? 2 : 4;
}
@ -87,13 +71,7 @@ public class UnicornCastingAbility implements Ability<Hit> {
return 0x000000;
}
Hand hand = player.asEntity().isSneaking() ? Hand.OFF_HAND : Hand.MAIN_HAND;
TypedActionResult<CustomisedSpellType<?>> newSpell = player.getCharms().getSpellInHand(hand);
if (newSpell.getResult() != ActionResult.FAIL) {
return newSpell.getValue().type().getColor();
}
return -1;
return super.getColor(player);
}
@Override
@ -118,8 +96,7 @@ public class UnicornCastingAbility implements Ability<Hit> {
}
}
} else {
Hand hand = player.asEntity().isSneaking() ? Hand.OFF_HAND : Hand.MAIN_HAND;
TypedActionResult<CustomisedSpellType<?>> newSpell = player.getCharms().getSpellInHand(hand);
TypedActionResult<CustomisedSpellType<?>> newSpell = player.getCharms().getSpellInHand(true);
if (newSpell.getResult() != ActionResult.FAIL) {
CustomisedSpellType<?> spell = newSpell.getValue();
@ -178,9 +155,4 @@ public class UnicornCastingAbility implements Ability<Hit> {
player.spawnParticles(MagicParticleEffect.UNICORN, 5);
}
}
@Override
public void postApply(Pony player, AbilitySlot slot) {
player.spawnParticles(MagicParticleEffect.UNICORN, 5);
}
}

View file

@ -43,7 +43,7 @@ public class UnicornDispellAbility implements Ability<Pos> {
}
@Override
public Identifier getIcon(Pony player, boolean swap) {
public Identifier getIcon(Pony player) {
Identifier id = Abilities.REGISTRY.getId(this);
return new Identifier(id.getNamespace(), "textures/gui/ability/" + id.getPath() + (player.getSpecies() == Race.CHANGELING ? "_changeling" : "") + ".png");
}

View file

@ -1,6 +1,5 @@
package com.minelittlepony.unicopia.ability;
import com.minelittlepony.unicopia.Race;
import com.minelittlepony.unicopia.ability.data.Hit;
import com.minelittlepony.unicopia.ability.magic.spell.HomingSpell;
import com.minelittlepony.unicopia.ability.magic.spell.Spell;
@ -11,7 +10,6 @@ import com.minelittlepony.unicopia.particle.MagicParticleEffect;
import com.minelittlepony.unicopia.util.TraceHelper;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.TypedActionResult;
/**
@ -20,30 +18,15 @@ import net.minecraft.util.TypedActionResult;
* 1. If the player is holding nothing, casts their equipped offensive spell (currently only vortex - inverse of shield)
* 2. If the player is holding a gem, consumes it and casts whatever spell is contained within onto a projectile.
*/
public class UnicornProjectileAbility implements Ability<Hit> {
public class UnicornProjectileAbility extends AbstractSpellCastingAbility {
@Override
public int getWarmupTime(Pony player) {
return 8;
}
@Override
public int getCooldownTime(Pony player) {
return 0;
}
@Override
public boolean canUse(Race race) {
return race.canCast();
}
@Override
public Hit tryActivate(Pony player) {
return Hit.of(player.getCharms().getSpellInHand(Hand.OFF_HAND).getResult() != ActionResult.FAIL);
}
@Override
public Hit.Serializer<Hit> getSerializer() {
return Hit.SERIALIZER;
return Hit.of(player.getCharms().getSpellInHand(false).getResult() != ActionResult.FAIL);
}
@Override
@ -55,8 +38,7 @@ public class UnicornProjectileAbility implements Ability<Hit> {
public boolean onQuickAction(Pony player, ActivationType type) {
if (type == ActivationType.DOUBLE_TAP) {
if (!player.isClient()) {
Hand hand = player.asEntity().isSneaking() ? Hand.MAIN_HAND : Hand.OFF_HAND;
TypedActionResult<CustomisedSpellType<?>> thrown = player.getCharms().getSpellInHand(hand);
TypedActionResult<CustomisedSpellType<?>> thrown = player.getCharms().getSpellInHand(true);
if (thrown.getResult() != ActionResult.FAIL) {
thrown.getValue().create().toThrowable().throwProjectile(player).ifPresent(projectile -> {
@ -73,8 +55,7 @@ public class UnicornProjectileAbility implements Ability<Hit> {
@Override
public void apply(Pony player, Hit data) {
Hand hand = player.asEntity().isSneaking() ? Hand.MAIN_HAND : Hand.OFF_HAND;
TypedActionResult<CustomisedSpellType<?>> thrown = player.getCharms().getSpellInHand(hand);
TypedActionResult<CustomisedSpellType<?>> thrown = player.getCharms().getSpellInHand(true);
if (thrown.getResult() != ActionResult.FAIL) {
Spell spell = thrown.getValue().create();
@ -96,9 +77,4 @@ public class UnicornProjectileAbility implements Ability<Hit> {
player.getMagicalReserves().getExhaustion().multiply(3.3F);
player.spawnParticles(MagicParticleEffect.UNICORN, 5);
}
@Override
public void postApply(Pony player, AbilitySlot slot) {
player.spawnParticles(MagicParticleEffect.UNICORN, 5);
}
}

View file

@ -20,6 +20,7 @@ import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.predicate.entity.EntityPredicates;
import net.minecraft.sound.SoundCategory;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
@ -31,9 +32,17 @@ import net.minecraft.world.World;
public class UnicornTeleportAbility implements Ability<Pos> {
@Override
public Identifier getIcon(Pony player, boolean swap) {
public Identifier getIcon(Pony player) {
Identifier id = Abilities.REGISTRY.getId(this);
return new Identifier(id.getNamespace(), "textures/gui/ability/" + id.getPath() + (swap ? "_far" : "_near") + ".png");
return new Identifier(id.getNamespace(), "textures/gui/ability/" + id.getPath() + (player.asEntity().isSneaking() ? "_far" : "_near") + ".png");
}
@Override
public Text getName(Pony player) {
if (player.asEntity().isSneaking()) {
return Text.translatable(getTranslationKey() + ".far");
}
return getName();
}
@Override

View file

@ -8,7 +8,6 @@ import java.util.Set;
import org.lwjgl.glfw.GLFW;
import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.ability.Ability;
import com.minelittlepony.unicopia.ability.AbilityDispatcher;
import com.minelittlepony.unicopia.ability.AbilitySlot;
import com.minelittlepony.unicopia.ability.ActivationType;
@ -87,7 +86,7 @@ public class KeyBindingsHandler {
if (state != PressedState.UNCHANGED) {
if (state == PressedState.PRESSED) {
abilities.activate(slot, page).map(Ability::getName).ifPresent(UHud.INSTANCE::setMessage);
abilities.activate(slot, page).map(a -> a.getName(iplayer)).ifPresent(UHud.INSTANCE::setMessage);
} else {
abilities.clear(slot, ActivationType.NONE, page);
}

View file

@ -66,6 +66,10 @@ public class UHud extends DrawableHelper {
@Nullable
private LoopingSoundInstance<PlayerEntity> partySound;
private boolean prevPointed;
private boolean prevReplacing;
private SpellType<?> focusedType = SpellType.empty();
public void render(InGameHud hud, MatrixStack matrices, float tickDelta) {
if (client.player == null) {
@ -119,23 +123,29 @@ public class UHud extends DrawableHelper {
slots.forEach(slot -> slot.renderBackground(matrices, abilities, swap, tickDelta));
if (pony.getObservedSpecies().canCast()) {
Ability<?> ability = pony.getAbilities().getStat(AbilitySlot.PRIMARY)
AbilitySlot slot = swap ? AbilitySlot.PASSIVE : AbilitySlot.PRIMARY;
Ability<?> ability = pony.getAbilities().getStat(slot)
.getAbility(Unicopia.getConfig().hudPage.get())
.orElse(null);
if (ability == Abilities.CAST || ability == Abilities.SHOOT) {
matrices.push();
matrices.translate(PRIMARY_SLOT_SIZE / 2F, PRIMARY_SLOT_SIZE / 2F, 0);
boolean first = ability == Abilities.CAST;
if (pony.asEntity().isSneaking()) {
first = !first;
boolean first = !pony.asEntity().isSneaking();
TypedActionResult<CustomisedSpellType<?>> inHand = pony.getCharms().getSpellInHand(false);
boolean replacing = inHand.getResult().isAccepted() && pony.getAbilities().getStat(slot).getActiveAbility().isEmpty();
if (first != prevPointed || replacing != prevReplacing || inHand.getValue().type() != focusedType) {
focusedType = inHand.getValue().type();
prevPointed = first;
prevReplacing = replacing;
setMessage(ability.getName(pony));
}
matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(first ? 37 : 63));
matrices.translate(-23, 0, 0);
matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(-26));
matrices.scale(0.8F, 0.8F, 1);
UHud.drawTexture(matrices, 0, 0, 3, 120, 15, 7, 128, 128);
int u = replacing ? 16 : 3;
UHud.drawTexture(matrices, 0, 0, u, 120, 13, 7, 128, 128);
matrices.pop();
}
}
@ -327,8 +337,6 @@ public class UHud extends DrawableHelper {
getZOffset(), Math.min(1, radius));
}
public void setMessage(Text message) {
this.message = message;
this.messageTime = 60;
@ -342,7 +350,7 @@ public class UHud extends DrawableHelper {
void renderAbilityIcon(MatrixStack matrices, AbilityDispatcher.Stat stat, int x, int y, int u, int v, int frameWidth, int frameHeight) {
stat.getAbility(Unicopia.getConfig().hudPage.get()).ifPresent(ability -> {
RenderSystem.setShaderTexture(0, ability.getIcon(Pony.of(client.player), client.options.sneakKey.isPressed()));
RenderSystem.setShaderTexture(0, ability.getIcon(Pony.of(client.player)));
drawTexture(matrices, x, y, 0, 0, frameWidth, frameHeight, u, v);
RenderSystem.setShaderTexture(0, HUD_TEXTURE);
});

View file

@ -34,7 +34,6 @@ public class SpellbookProfilePageContent extends DrawableHelper implements Spell
int x = screen.getX() + bounds.left + bounds.width / 4 - size + 5;
int y = screen.getY() + bounds.top + bounds.height / 2 + 3;
screen.addDrawable(new SpellbookScreen.ImageButton(x, y, size, size))
.getStyle()
.setIcon(TribeButton.createSprite(pony.getActualSpecies(), 0, 0, size))

View file

@ -34,10 +34,18 @@ public class PlayerCharmTracker implements NbtSerialisable {
return handSpells[hand.ordinal()] == null ? SpellType.EMPTY_KEY.withTraits() : handSpells[hand.ordinal()];
}
public TypedActionResult<CustomisedSpellType<?>> getSpellInHand(Hand hand) {
public Hand getHand() {
return pony.asEntity().isSneaking() ? Hand.OFF_HAND : Hand.MAIN_HAND;
}
public TypedActionResult<CustomisedSpellType<?>> getSpellInHand(boolean consume) {
return getSpellInHand(getHand(), consume);
}
public TypedActionResult<CustomisedSpellType<?>> getSpellInHand(Hand hand, boolean consume) {
return Streams.stream(pony.asEntity().getHandItems())
.filter(EnchantableItem::isEnchanted)
.map(stack -> EnchantableItem.consumeSpell(stack, pony.asEntity(), null))
.map(stack -> EnchantableItem.consumeSpell(stack, pony.asEntity(), null, consume))
.findFirst()
.orElse(getEquippedSpell(hand).toAction());
}

View file

@ -27,31 +27,34 @@ public interface EnchantableItem extends ItemConvertible {
return EnchantableItem.getSpellKey(stack).withTraits(SpellTraits.of(stack));
}
static TypedActionResult<CustomisedSpellType<?>> consumeSpell(ItemStack stack, PlayerEntity player, @Nullable Predicate<CustomisedSpellType<?>> filter) {
static TypedActionResult<CustomisedSpellType<?>> consumeSpell(ItemStack stack, PlayerEntity player, @Nullable Predicate<CustomisedSpellType<?>> filter, boolean consume) {
if (!isEnchanted(stack)) {
return TypedActionResult.pass(null);
return TypedActionResult.pass(SpellType.EMPTY_KEY.withTraits());
}
SpellType<Spell> key = EnchantableItem.getSpellKey(stack);
if (key.isEmpty()) {
return TypedActionResult.fail(null);
return TypedActionResult.fail(SpellType.EMPTY_KEY.withTraits());
}
CustomisedSpellType<?> result = key.withTraits(SpellTraits.of(stack));
if (filter != null && !filter.test(result)) {
return TypedActionResult.fail(null);
return TypedActionResult.fail(SpellType.EMPTY_KEY.withTraits());
}
if (!player.world.isClient) {
if (!player.world.isClient && consume) {
player.swingHand(player.getStackInHand(Hand.OFF_HAND) == stack ? Hand.OFF_HAND : Hand.MAIN_HAND);
player.getItemCooldownManager().set(stack.getItem(), 20);
if (stack.getCount() == 1) {
unenchant(stack);
} else {
player.giveItemStack(unenchant(stack.split(1)));
if (!player.isCreative()) {
if (stack.getCount() == 1) {
unenchant(stack);
} else {
player.giveItemStack(unenchant(stack.split(1)));
}
}
}

View file

@ -44,7 +44,9 @@ public class GemstoneItem extends Item implements MultiItem, EnchantableItem {
return result;
}
TypedActionResult<CustomisedSpellType<?>> spell = EnchantableItem.consumeSpell(stack, user, ((Predicate<CustomisedSpellType<?>>)charms.getEquippedSpell(hand)::equals).negate());
hand = user.isSneaking() ? Hand.OFF_HAND : Hand.MAIN_HAND;
TypedActionResult<CustomisedSpellType<?>> spell = EnchantableItem.consumeSpell(stack, user, ((Predicate<CustomisedSpellType<?>>)charms.getEquippedSpell(hand)::equals).negate(), true);
CustomisedSpellType<?> existing = charms.getEquippedSpell(hand);
@ -67,6 +69,8 @@ public class GemstoneItem extends Item implements MultiItem, EnchantableItem {
charms.equipSpell(hand, SpellType.EMPTY_KEY.withTraits());
}
user.getItemCooldownManager().set(this, 20);
return TypedActionResult.success(stack, true);
}

View file

@ -326,11 +326,21 @@
"toxicity.severe.name": "Toxic",
"toxicity.lethal.name": "Lethal",
"ability.unicopia.shoot": "Throw Magic",
"ability.unicopia.shoot": "Shoot Magic",
"ability.unicopia.shoot.with_spell": "Shoot %s",
"ability.unicopia.shoot.with_spell.active": "Shooting %s",
"ability.unicopia.shoot.with_spell.hand": "Shooting %s from hand",
"ability.unicopia.shoot.with_spell.replacing": "Replace %s with %s",
"ability.unicopia.cast": "Cast Spell",
"ability.unicopia.cast.with_spell": "Cast %s",
"ability.unicopia.cast.with_spell.active": "Casting %s",
"ability.unicopia.cast.with_spell.hand": "Casting %s from hand",
"ability.unicopia.cast.with_spell.replacing": "Replace %s with %s",
"ability.unicopia.dispell": "Dispell Magic",
"ability.unicopia.teleport": "Teleport",
"ability.unicopia.teleport.far": "Teleport (Far)",
"ability.unicopia.teleport_group": "Group Teleport",
"ability.unicopia.teleport_group.far": "Group Teleport (Far)",
"ability.unicopia.grow": "Nourish Earth",
"ability.unicopia.stomp": "Ground Pound",
"ability.unicopia.kick": "Crushing Blow",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB