diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/Animation.java b/api/src/main/java/team/unnamed/hephaestus/animation/Animation.java index ea6edd1e..90be4e24 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/Animation.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/Animation.java @@ -26,7 +26,8 @@ import net.kyori.examination.Examinable; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import team.unnamed.hephaestus.animation.timeline.BoneTimeline; +import team.unnamed.hephaestus.animation.timeline.bone.BoneTimeline; +import team.unnamed.hephaestus.animation.timeline.effects.EffectsTimeline; import java.util.Map; @@ -53,9 +54,10 @@ public interface Animation extends Examinable { final @NotNull String name, final int length, final @NotNull LoopMode loopMode, - final @NotNull Map timelines + final @NotNull Map timelines, + final @NotNull EffectsTimeline effectsTimeline ) { - return new AnimationImpl(name, length, loopMode, timelines); + return new AnimationImpl(name, length, loopMode, timelines, effectsTimeline); } /** @@ -110,6 +112,8 @@ public interface Animation extends Examinable { */ @NotNull Map timelines(); + @NotNull EffectsTimeline effectsTimeline(); + /** * An enum containing all the possible * loop mode values, they specify what the @@ -192,6 +196,15 @@ interface Builder { */ @NotNull Builder timelines(final @NotNull Map timelines); + /** + * Sets the animation effects timeline + * + * @param timeline The animation effects timeline + * @return This builder + * @since 1.0.0 + */ + @NotNull Builder effectsTimeline(final @NotNull EffectsTimeline timeline); + /** * Adds a bone timeline to the animation * diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/AnimationImpl.java b/api/src/main/java/team/unnamed/hephaestus/animation/AnimationImpl.java index c4a6df23..1e6abecc 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/AnimationImpl.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/AnimationImpl.java @@ -26,7 +26,8 @@ import net.kyori.examination.ExaminableProperty; import net.kyori.examination.string.StringExaminer; import org.jetbrains.annotations.NotNull; -import team.unnamed.hephaestus.animation.timeline.BoneTimeline; +import team.unnamed.hephaestus.animation.timeline.bone.BoneTimeline; +import team.unnamed.hephaestus.animation.timeline.effects.EffectsTimeline; import java.util.HashMap; import java.util.Map; @@ -41,17 +42,20 @@ final class AnimationImpl implements Animation { private final int length; private final LoopMode loopMode; private final Map timelines; + private final EffectsTimeline effectsTimeline; AnimationImpl( final @NotNull String name, final int length, final @NotNull LoopMode loopMode, - final @NotNull Map timelines + final @NotNull Map timelines, + final @NotNull EffectsTimeline effectsTimeline ) { this.name = requireNonNull(name, "name"); this.length = length; this.loopMode = requireNonNull(loopMode, "loopMode"); this.timelines = requireNonNull(timelines, "timelines"); + this.effectsTimeline = requireNonNull(effectsTimeline, "effectsTimeline");; } @Override @@ -74,6 +78,11 @@ public int length() { return timelines; } + @Override + public @NotNull EffectsTimeline effectsTimeline() { + return effectsTimeline; + } + @Override public @NotNull Stream examinableProperties() { return Stream.of( @@ -111,6 +120,7 @@ static final class BuilderImpl implements Builder { private int length; private LoopMode loopMode; private Map timelines; + private EffectsTimeline effectsTimeline; @Override public @NotNull Builder name(final @NotNull String name) { @@ -136,6 +146,12 @@ static final class BuilderImpl implements Builder { return this; } + @Override + public @NotNull Builder effectsTimeline(@NotNull EffectsTimeline timeline) { + this.effectsTimeline = requireNonNull(timeline, "effectsTimeline"); + return this; + } + @Override public @NotNull Builder timeline(@NotNull String boneName, @NotNull BoneTimeline timeline) { requireNonNull(boneName, "boneName"); @@ -149,7 +165,7 @@ static final class BuilderImpl implements Builder { @Override public @NotNull Animation build() { - return new AnimationImpl(name, length, loopMode, timelines); + return new AnimationImpl(name, length, loopMode, timelines, effectsTimeline); } } } diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/controller/NormalAnimationController.java b/api/src/main/java/team/unnamed/hephaestus/animation/controller/NormalAnimationController.java index d0b43223..b8912b22 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/controller/NormalAnimationController.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/controller/NormalAnimationController.java @@ -23,14 +23,19 @@ */ package team.unnamed.hephaestus.animation.controller; +import net.kyori.adventure.sound.Sound; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import team.unnamed.creative.base.Vector3Float; import team.unnamed.hephaestus.Bone; import team.unnamed.hephaestus.animation.Animation; -import team.unnamed.hephaestus.animation.timeline.BoneFrame; -import team.unnamed.hephaestus.animation.timeline.BoneTimeline; -import team.unnamed.hephaestus.animation.timeline.BoneTimelinePlayhead; +import team.unnamed.hephaestus.animation.timeline.bone.BoneFrame; +import team.unnamed.hephaestus.animation.timeline.bone.BoneTimeline; +import team.unnamed.hephaestus.animation.timeline.bone.BoneTimelinePlayhead; import team.unnamed.hephaestus.animation.timeline.Timeline; +import team.unnamed.hephaestus.animation.timeline.effects.EffectsFrame; +import team.unnamed.hephaestus.animation.timeline.effects.EffectsTimeline; +import team.unnamed.hephaestus.animation.timeline.effects.EffectsTimelinePlayhead; import team.unnamed.hephaestus.util.Quaternion; import team.unnamed.hephaestus.util.Vectors; import team.unnamed.hephaestus.view.BaseBoneView; @@ -49,6 +54,7 @@ class NormalAnimationController implements AnimationController { private final BaseModelView view; private final Map iterators = new HashMap<>(); + private @NotNull EffectsTimelinePlayhead effectsIterator = new EffectsTimelinePlayhead(EffectsTimeline.empty().build()); private final Map lastFrames = new HashMap<>(); @@ -76,7 +82,8 @@ public synchronized void queue(Animation animation, int transitionTicks) { final Animation.Builder transitionAnimationBuilder = Animation.animation() .name("$$hephaestus_transition_animation") .length(transitionTicks) - .loopMode(Animation.LoopMode.HOLD); + .loopMode(Animation.LoopMode.HOLD) + .effectsTimeline(EffectsTimeline.empty().build()); for (Map.Entry entry : lastFrames.entrySet()) { String boneName = entry.getKey(); @@ -175,6 +182,21 @@ public synchronized void tick(Quaternion initialRotation, Vector3Float initialPo Vector3Float.ONE ); } + + if (currentAnimation == null) { + return; + } + + EffectsFrame effectsFrame = effectsIterator.next(); + if (effectsIterator.tick() + 1 >= currentAnimation.length()) { + return; + } + + Sound[] sounds = effectsFrame.sounds(); + + for (Sound sound : sounds) { + view.playSound(sound); + } } private void nextAnimation() { @@ -186,7 +208,9 @@ private void nextAnimation() { private void createIterators(Animation animation) { iterators.clear(); lastFrames.clear(); + animation.timelines().forEach((name, list) -> iterators.put(name, list.createPlayhead())); + effectsIterator = animation.effectsTimeline().createPlayhead(); } private BoneFrame nextFrame(String boneName) { diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/interpolation/Interpolator.java b/api/src/main/java/team/unnamed/hephaestus/animation/interpolation/Interpolator.java index 34c50f36..05504c29 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/interpolation/Interpolator.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/interpolation/Interpolator.java @@ -163,6 +163,10 @@ public interface Interpolator { return CatmullRomInterpolator.INSTANCE; } + static @NotNull Interpolator staticInterpolator(T empty) { + return new StaticInterpolator<>(empty); + } + /** * Returns an interpolator that will create interpolations that always * return the provided {@code interpolated} value and will not perform diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/interpolation/StaticInterpolator.java b/api/src/main/java/team/unnamed/hephaestus/animation/interpolation/StaticInterpolator.java new file mode 100644 index 00000000..841b2c44 --- /dev/null +++ b/api/src/main/java/team/unnamed/hephaestus/animation/interpolation/StaticInterpolator.java @@ -0,0 +1,76 @@ +/* + * This file is part of hephaestus-engine, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package team.unnamed.hephaestus.animation.interpolation; + +import org.jetbrains.annotations.NotNull; + +final class StaticInterpolator implements Interpolator { + + private final T empty; + + StaticInterpolator(T empty) { + this.empty = empty; + } + + @Override + public @NotNull Interpolation interpolation(@NotNull T from, @NotNull T to) { + return new StaticInterpolation<>(empty, from, to); + } + + static final class StaticInterpolation implements Interpolation { + + private final T empty; + private final T from; + private final T to; + + StaticInterpolation( + final @NotNull T empty, + final @NotNull T from, + final @NotNull T to + ) { + this.empty = empty; + this.from = from; + this.to = to; + } + + @Override + public @NotNull T from() { + return from; + } + + @Override + public @NotNull T to() { + return to; + } + + @Override + public @NotNull T interpolate(final double progress) { + if (progress == 0) { + return to; + } + + return empty; + } + } +} diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/KeyFrame.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/KeyFrame.java index 7cafaaa0..539d893f 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/KeyFrame.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/KeyFrame.java @@ -54,4 +54,12 @@ public Interpolator interpolatorOr(Interpolator fallback) { return interpolator == null ? fallback : interpolator; } + @Override + public String toString() { + return "KeyFrame{" + + "time=" + time + + ", value=" + value + + ", interpolator=" + interpolator + + '}'; + } } diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/Timeline.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/Timeline.java index 483324bf..078b7a6b 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/Timeline.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/Timeline.java @@ -42,6 +42,7 @@ * @param The type of values in this timeline */ public interface Timeline extends Examinable { + static Builder timeline() { return new TimelineImpl.BuilderImpl<>(); } diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/TimelineImpl.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/TimelineImpl.java index 18d532c8..75f17720 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/TimelineImpl.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/TimelineImpl.java @@ -67,6 +67,15 @@ public Playhead createPlayhead() { return Playhead.playhead(this); } + @Override + public String toString() { + return "TimelineImpl{" + + "initialValue=" + initialValue + + ", defaultInterpolator=" + defaultInterpolator + + ", keyFrames=" + keyFrames + + '}'; + } + static final class BuilderImpl implements Builder { private T initialValue; diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneFrame.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneFrame.java similarity index 98% rename from api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneFrame.java rename to api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneFrame.java index 722ad37b..b27811d5 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneFrame.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneFrame.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package team.unnamed.hephaestus.animation.timeline; +package team.unnamed.hephaestus.animation.timeline.bone; import net.kyori.examination.Examinable; import net.kyori.examination.ExaminableProperty; diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimeline.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimeline.java similarity index 95% rename from api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimeline.java rename to api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimeline.java index 091c27cd..80f6790c 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimeline.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimeline.java @@ -21,11 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package team.unnamed.hephaestus.animation.timeline; +package team.unnamed.hephaestus.animation.timeline.bone; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import team.unnamed.creative.base.Vector3Float; +import team.unnamed.hephaestus.animation.timeline.Timeline; /** * diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimelineImpl.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimelineImpl.java similarity index 96% rename from api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimelineImpl.java rename to api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimelineImpl.java index 4a4244bb..c6d402f2 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimelineImpl.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimelineImpl.java @@ -21,10 +21,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package team.unnamed.hephaestus.animation.timeline; +package team.unnamed.hephaestus.animation.timeline.bone; import org.jetbrains.annotations.NotNull; import team.unnamed.creative.base.Vector3Float; +import team.unnamed.hephaestus.animation.timeline.Timeline; import static java.util.Objects.requireNonNull; diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimelinePlayhead.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimelinePlayhead.java similarity index 97% rename from api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimelinePlayhead.java rename to api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimelinePlayhead.java index e5a8c118..837a3a7b 100644 --- a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/BoneTimelinePlayhead.java +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/bone/BoneTimelinePlayhead.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package team.unnamed.hephaestus.animation.timeline; +package team.unnamed.hephaestus.animation.timeline.bone; import team.unnamed.creative.base.Vector3Float; import team.unnamed.hephaestus.animation.timeline.playhead.Playhead; diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsFrame.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsFrame.java new file mode 100644 index 00000000..19322139 --- /dev/null +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsFrame.java @@ -0,0 +1,75 @@ +/* + * This file is part of hephaestus-engine, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package team.unnamed.hephaestus.animation.timeline.effects; + +import net.kyori.adventure.sound.Sound; +import net.kyori.examination.Examinable; +import net.kyori.examination.ExaminableProperty; +import net.kyori.examination.string.StringExaminer; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.stream.Stream; + +public class EffectsFrame implements Examinable { + + public static EffectsFrame INITIAL = new EffectsFrame( + new Sound[0] + ); + + private final Sound[] sounds; + + public EffectsFrame(Sound[] sounds) { + this.sounds = sounds; + } + + public Sound[] sounds() { + return sounds; + } + + @Override + public @NotNull Stream examinableProperties() { + return Stream.of( + ExaminableProperty.of("sounds", sounds) + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EffectsFrame that = (EffectsFrame) o; + return Arrays.equals(sounds, that.sounds); + } + + @Override + public int hashCode() { + return Arrays.hashCode(sounds); + } + + @Override + public String toString() { + return examine(StringExaminer.simpleEscaping()); + } +} \ No newline at end of file diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimeline.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimeline.java new file mode 100644 index 00000000..9a026ea1 --- /dev/null +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimeline.java @@ -0,0 +1,73 @@ +/* + * This file is part of hephaestus-engine, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package team.unnamed.hephaestus.animation.timeline.effects; + +import net.kyori.adventure.sound.Sound; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import team.unnamed.hephaestus.animation.interpolation.Interpolator; +import team.unnamed.hephaestus.animation.timeline.Timeline; + +/** + * + * @since 1.0.0 + */ +public interface EffectsTimeline { + + static @NotNull EffectsTimeline.Builder effectsTimeline() { + return new EffectsTimelineImpl.BuilderImpl(); + } + + static @NotNull EffectsTimeline.Builder empty() { + return effectsTimeline() + .sounds(Timeline.timeline() + .initial(new Sound[0]) + .defaultInterpolator(Interpolator.staticInterpolator(new Sound[0])) + .build() + ); + } + + default @NotNull EffectsTimelinePlayhead createPlayhead() { + return new EffectsTimelinePlayhead(this); + } + + @NotNull Timeline sounds(); + + interface Builder { + + /** + * Set the sounds timeline + * + * @param sounds The sounds timeline + * @return This builder + * @since 1.0.0 + */ + @Contract("_ -> this") + @NotNull Builder sounds(final @NotNull Timeline sounds); + + @NotNull EffectsTimeline build(); + + } + +} diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimelineImpl.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimelineImpl.java new file mode 100644 index 00000000..bb249bd2 --- /dev/null +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimelineImpl.java @@ -0,0 +1,70 @@ +/* + * This file is part of hephaestus-engine, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package team.unnamed.hephaestus.animation.timeline.effects; + +import net.kyori.adventure.sound.Sound; +import org.jetbrains.annotations.NotNull; +import team.unnamed.hephaestus.animation.timeline.Timeline; + +import static java.util.Objects.requireNonNull; + +final class EffectsTimelineImpl implements EffectsTimeline { + + private final Timeline sounds; + + EffectsTimelineImpl(Timeline sounds) { + this.sounds = requireNonNull(sounds, "sounds"); + } + + @Override + public @NotNull Timeline sounds() { + return sounds; + } + + @Override + public String toString() { + return "EffectsTimelineImpl{" + + "sounds=" + sounds + + '}'; + } + + static final class BuilderImpl implements Builder { + + private Timeline sounds; + + BuilderImpl() { + } + + @Override + public @NotNull Builder sounds(@NotNull Timeline sounds) { + this.sounds = requireNonNull(sounds, "sounds"); + return this; + } + + @Override + public @NotNull EffectsTimeline build() { + return new EffectsTimelineImpl(sounds); + } + } +} diff --git a/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimelinePlayhead.java b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimelinePlayhead.java new file mode 100644 index 00000000..0040c337 --- /dev/null +++ b/api/src/main/java/team/unnamed/hephaestus/animation/timeline/effects/EffectsTimelinePlayhead.java @@ -0,0 +1,48 @@ +/* + * This file is part of hephaestus-engine, licensed under the MIT license + * + * Copyright (c) 2021-2023 Unnamed Team + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package team.unnamed.hephaestus.animation.timeline.effects; + +import net.kyori.adventure.sound.Sound; +import team.unnamed.hephaestus.animation.timeline.playhead.Playhead; + +public class EffectsTimelinePlayhead { + + private final Playhead sounds; + private int tick = -1; + + public EffectsTimelinePlayhead(EffectsTimeline effectsTimeline) { + this.sounds = effectsTimeline.sounds().createPlayhead(); + } + + public int tick() { + return tick; + } + + public EffectsFrame next() { + tick++; + return new EffectsFrame( + sounds.next() + ); + } +} diff --git a/api/src/main/java/team/unnamed/hephaestus/view/BaseModelView.java b/api/src/main/java/team/unnamed/hephaestus/view/BaseModelView.java index afca016d..87c3eb34 100644 --- a/api/src/main/java/team/unnamed/hephaestus/view/BaseModelView.java +++ b/api/src/main/java/team/unnamed/hephaestus/view/BaseModelView.java @@ -23,6 +23,7 @@ */ package team.unnamed.hephaestus.view; +import net.kyori.adventure.sound.Sound; import org.jetbrains.annotations.Nullable; import team.unnamed.hephaestus.Model; import team.unnamed.hephaestus.animation.controller.AnimationController; @@ -55,6 +56,8 @@ public interface BaseModelView { boolean removeViewer(TViewer viewer); + void playSound(Sound sound); + /** * Colorizes this view using the specified * {@code r} (red), {@code g} (green) and diff --git a/reader-blockbench/src/main/java/team/unnamed/hephaestus/reader/blockbench/AnimationReader.java b/reader-blockbench/src/main/java/team/unnamed/hephaestus/reader/blockbench/AnimationReader.java index 5d6b1a64..9bdf3c1f 100644 --- a/reader-blockbench/src/main/java/team/unnamed/hephaestus/reader/blockbench/AnimationReader.java +++ b/reader-blockbench/src/main/java/team/unnamed/hephaestus/reader/blockbench/AnimationReader.java @@ -23,13 +23,17 @@ */ package team.unnamed.hephaestus.reader.blockbench; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; import team.unnamed.creative.base.Vector3Float; import team.unnamed.hephaestus.animation.Animation; import team.unnamed.hephaestus.animation.interpolation.Interpolator; -import team.unnamed.hephaestus.animation.timeline.BoneTimeline; +import team.unnamed.hephaestus.animation.timeline.bone.BoneTimeline; import team.unnamed.hephaestus.animation.timeline.Timeline; +import team.unnamed.hephaestus.animation.timeline.effects.EffectsTimeline; import team.unnamed.hephaestus.process.ElementScale; import java.io.IOException; @@ -67,11 +71,18 @@ static void readAnimations( if (GsonUtil.isNullOrAbsent(animationJson, "animators")) { // empty animation, no keyframes of any kind - animations.put(name, Animation.animation(name, length, loopMode, Collections.emptyMap())); + animations.put(name, Animation.animation( + name, + length, + loopMode, + Collections.emptyMap(), + EffectsTimeline.empty().build() + )); continue; } Map animators = new HashMap<>(); + EffectsTimeline.Builder effectsTimeline = EffectsTimeline.empty(); for (Map.Entry animatorEntry : animationJson.get("animators") .getAsJsonObject() @@ -79,80 +90,123 @@ static void readAnimations( JsonObject animatorJson = animatorEntry.getValue().getAsJsonObject(); String boneName = animatorJson.get("name").getAsString(); - - Timeline.Builder positionsTimeline = Timeline.timeline() - .initial(Vector3Float.ZERO) - .defaultInterpolator(Interpolator.lerpVector3Float()); - Timeline.Builder rotationsTimeline = Timeline.timeline() - .initial(Vector3Float.ZERO) - .defaultInterpolator(Interpolator.lerpVector3Float()); - Timeline.Builder scalesTimeline = Timeline.timeline() - .initial(Vector3Float.ONE) - .defaultInterpolator(Interpolator.lerpVector3Float()); - - for (JsonElement keyFrameElement : animatorJson.get("keyframes").getAsJsonArray()) { - - JsonObject keyframeJson = keyFrameElement.getAsJsonObject(); - JsonObject dataPoint = keyframeJson.get("data_points").getAsJsonArray().get(0).getAsJsonObject(); - - float x = GsonUtil.parseLenientFloat(dataPoint.get("x")); - float y = GsonUtil.parseLenientFloat(dataPoint.get("y")); - float z = GsonUtil.parseLenientFloat(dataPoint.get("z")); - - Vector3Float value = new Vector3Float(x, y, z); - - String channel = keyframeJson.get("channel").getAsString(); - int time = Math.round(GsonUtil.parseLenientFloat(keyframeJson.get("time")) * TICKS_PER_SECOND); - - if (channel.equals("position")) { - value = value.divide(ElementScale.BLOCK_SIZE, ElementScale.BLOCK_SIZE, -ElementScale.BLOCK_SIZE); - } - - if (channel.equals("rotation")) { - value = value.multiply(1, -1, -1); + String type = animatorJson.get("type").getAsString(); + + if (type.equals("effect")) { + Timeline.Builder soundsTimeline = Timeline.timeline() + .initial(new Sound[0]) + .defaultInterpolator(Interpolator.staticInterpolator(new Sound[0])); + + for (JsonElement keyFrameElement : animatorJson.get("keyframes").getAsJsonArray()) { + JsonObject keyframeJson = keyFrameElement.getAsJsonObject(); + JsonArray dataPoints = keyframeJson.get("data_points").getAsJsonArray(); + String channel = keyframeJson.get("channel").getAsString(); + int time = Math.round(GsonUtil.parseLenientFloat(keyframeJson.get("time")) * TICKS_PER_SECOND); + + switch (channel) { + case "sound": + Sound[] sounds = new Sound[dataPoints.size()]; + + for (int i = 0; i < sounds.length; i++) { + JsonObject dataPoint = dataPoints.get(i).getAsJsonObject(); + String soundName = dataPoint.get("effect").getAsString(); + + sounds[i] = Sound.sound( + Key.key("hephaestus", soundName), + Sound.Source.AMBIENT, + 1, + 1 + ); + } + + soundsTimeline.keyFrame(time, sounds, Interpolator.always(sounds)); + break; + } } - String interpolation = keyframeJson.has("interpolation") - ? keyframeJson.get("interpolation").getAsString() - : "linear"; - Interpolator interpolator; - switch (interpolation.toLowerCase()) { - case "bezier": // <-- parse bezier as linear - case "linear": - interpolator = Interpolator.lerpVector3Float(); - break; - case "catmullrom": - case "smooth": // <-- smooth is the displayed name of catmullrom, it is the same - interpolator = Interpolator.catmullRomSplineVector3Float(); - break; - case "step": - interpolator = Interpolator.stepVector3Float(); - break; - default: - throw new IllegalArgumentException("Unsupported interpolation type: '" + interpolation + "'"); + effectsTimeline.sounds(soundsTimeline.build()); + } else if (type.equals("bone")) { + Timeline.Builder positionsTimeline = Timeline.timeline() + .initial(Vector3Float.ZERO) + .defaultInterpolator(Interpolator.lerpVector3Float()); + Timeline.Builder rotationsTimeline = Timeline.timeline() + .initial(Vector3Float.ZERO) + .defaultInterpolator(Interpolator.lerpVector3Float()); + Timeline.Builder scalesTimeline = Timeline.timeline() + .initial(Vector3Float.ONE) + .defaultInterpolator(Interpolator.lerpVector3Float()); + + for (JsonElement keyFrameElement : animatorJson.get("keyframes").getAsJsonArray()) { + JsonObject keyframeJson = keyFrameElement.getAsJsonObject(); + JsonArray dataPoints = keyframeJson.get("data_points").getAsJsonArray(); + + String channel = keyframeJson.get("channel").getAsString(); + int time = Math.round(GsonUtil.parseLenientFloat(keyframeJson.get("time")) * TICKS_PER_SECOND); + JsonObject dataPoint = dataPoints.get(0).getAsJsonObject(); + + float x = GsonUtil.parseLenientFloat(dataPoint.get("x")); + float y = GsonUtil.parseLenientFloat(dataPoint.get("y")); + float z = GsonUtil.parseLenientFloat(dataPoint.get("z")); + + Vector3Float value = new Vector3Float(x, y, z); + + if (channel.equals("position")) { + value = value.divide(ElementScale.BLOCK_SIZE, ElementScale.BLOCK_SIZE, -ElementScale.BLOCK_SIZE); + } + + if (channel.equals("rotation")) { + value = value.multiply(1, -1, -1); + } + + String interpolation = keyframeJson.has("interpolation") + ? keyframeJson.get("interpolation").getAsString() + : "linear"; + Interpolator interpolator; + switch (interpolation.toLowerCase()) { + case "bezier": // <-- parse bezier as linear + case "linear": + interpolator = Interpolator.lerpVector3Float(); + break; + case "catmullrom": + case "smooth": // <-- smooth is the displayed name of catmullrom, it is the same + interpolator = Interpolator.catmullRomSplineVector3Float(); + break; + case "step": + interpolator = Interpolator.stepVector3Float(); + break; + default: + throw new IllegalArgumentException("Unsupported interpolation type: '" + interpolation + "'"); + } + + switch (channel.toLowerCase(Locale.ROOT)) { + case "position": + positionsTimeline.keyFrame(time, value, interpolator); + break; + case "rotation": + rotationsTimeline.keyFrame(time, value, interpolator); + break; + case "scale": + scalesTimeline.keyFrame(time, value, interpolator); + break; + } } - switch (channel.toLowerCase(Locale.ROOT)) { - case "position": - positionsTimeline.keyFrame(time, value, interpolator); - break; - case "rotation": - rotationsTimeline.keyFrame(time, value, interpolator); - break; - case "scale": - scalesTimeline.keyFrame(time, value, interpolator); - break; - } + animators.put(boneName, BoneTimeline.boneTimeline() + .positions(positionsTimeline.build()) + .rotations(rotationsTimeline.build()) + .scales(scalesTimeline.build()) + .build() + ); } - - animators.put(boneName, BoneTimeline.boneTimeline() - .positions(positionsTimeline.build()) - .rotations(rotationsTimeline.build()) - .scales(scalesTimeline.build()) - .build()); } - animations.put(name, Animation.animation(name, length, loopMode, animators)); + animations.put(name, Animation.animation( + name, + length, + loopMode, + animators, + effectsTimeline.build() + )); } } diff --git a/runtime-minestom-ce/src/main/java/team/unnamed/hephaestus/minestomce/ModelEntity.java b/runtime-minestom-ce/src/main/java/team/unnamed/hephaestus/minestomce/ModelEntity.java index 1506ad7b..4076aa53 100644 --- a/runtime-minestom-ce/src/main/java/team/unnamed/hephaestus/minestomce/ModelEntity.java +++ b/runtime-minestom-ce/src/main/java/team/unnamed/hephaestus/minestomce/ModelEntity.java @@ -23,6 +23,7 @@ */ package team.unnamed.hephaestus.minestomce; +import net.kyori.adventure.sound.Sound; import net.minestom.server.color.Color; import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Entity; @@ -46,6 +47,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; public class ModelEntity extends EntityCreature implements BaseModelView { @@ -63,7 +65,7 @@ public ModelEntity(EntityType type, Model model, float scale) { this.animationController = AnimationController.nonDelayed(this); // model entity is not auto-viewable by default - super.setAutoViewable(false); // "super" so it doesn't call our override + setAutoViewable(false); initialize(); } @@ -94,6 +96,14 @@ public Collection viewers() { return super.viewers; } + @Override + public void playSound(Sound sound) { + System.out.println("play sound"); + for (Player viewer : viewers()) { + viewer.playSound(sound, position); + } + } + @Override public Model model() { return model; @@ -145,6 +155,15 @@ public void setAutoViewable(boolean autoViewable) { } } + @Override + public void updateViewableRule(@Nullable Predicate predicate) { + super.updateViewableRule(predicate); + + for (GenericBoneEntity boneEntity : bones.values()) { + boneEntity.updateViewableRule(predicate); + } + } + @Override public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { return super.setInstance(instance, spawnPosition)