basic advanced example
This commit is contained in:
1 parent
6fe57915e8
commit
bd8edf5afb
29 files changed
+1501
-38
No files matched your search
@@ -0,0 +1,45 @@
|
||||
"""Offline, statically declared enums and timing metadata for editor use."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "Content/Python"))
|
||||
from ue_workshop.animation_clip import AnimationClip
|
||||
from ue_workshop.animations import FighterAnimations, CreatureAnimations
|
||||
from ue_workshop.characters import FIGHTER_MODEL, CREATURE_MODEL, _clips
|
||||
|
||||
|
||||
class TimingClip(AnimationClip):
|
||||
WALK = ("walk", 2.0)
|
||||
FLOAT_DURATION = ("float_duration", 3.200000047683716)
|
||||
FRACTIONAL = ("fractional", 2.1)
|
||||
|
||||
|
||||
class CatalogTests(unittest.TestCase):
|
||||
def test_catalog_covers_all_source_files_without_duplicate_keys(self):
|
||||
for catalog, model in ((FighterAnimations, FIGHTER_MODEL), (CreatureAnimations, CREATURE_MODEL)):
|
||||
with self.subTest(model=model):
|
||||
keys = [clip.key for clip in catalog]
|
||||
self.assertEqual(set(keys), set(_clips(model)))
|
||||
self.assertEqual(len(keys), len(set(keys)))
|
||||
self.assertTrue(all(clip.seconds > 0 for clip in catalog))
|
||||
|
||||
def test_metadata_is_available_without_loading_unreal(self):
|
||||
self.assertNotIn("unreal", sys.modules)
|
||||
self.assertEqual(FighterAnimations.DRAW_A_GREAT_SWORD_1.key, "draw_a_great_sword_1")
|
||||
self.assertEqual(FighterAnimations.DRAW_A_GREAT_SWORD_1.seconds, 0.5)
|
||||
self.assertEqual(FighterAnimations.DRAW_A_GREAT_SWORD_1.frames(24), 12)
|
||||
|
||||
def test_frames_respect_fps_speed_and_fractional_end(self):
|
||||
self.assertEqual(TimingClip.WALK.frames(24), 48)
|
||||
self.assertEqual(TimingClip.WALK.frames(30, speed=2), 30)
|
||||
self.assertEqual(TimingClip.FRACTIONAL.frames(24), 51)
|
||||
self.assertEqual(TimingClip.FLOAT_DURATION.frames(30), 96)
|
||||
|
||||
def test_invalid_fps_and_speed_are_rejected(self):
|
||||
for fps in (0, -1, True, 24.5):
|
||||
with self.subTest(fps=fps), self.assertRaises(ValueError):
|
||||
TimingClip.WALK.frames(fps)
|
||||
for speed in (0, -1, True, float("nan"), float("inf")):
|
||||
with self.subTest(speed=speed), self.assertRaises(ValueError):
|
||||
TimingClip.WALK.frames(24, speed)
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Automatic catalogs preserve other models, partial selections and offline imports."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import ModuleType
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "Content/Python"))
|
||||
from ue_workshop import animation_catalog
|
||||
from ue_workshop.animation_clip import AnimationClip
|
||||
|
||||
|
||||
class AutomaticCatalogTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.folder = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.folder.cleanup)
|
||||
self.output = Path(self.folder.name) / "animations.py"
|
||||
|
||||
def load(self):
|
||||
namespace = {"__name__": "ue_workshop._catalog_test", "__package__": "ue_workshop"}
|
||||
exec(compile(self.output.read_text(encoding="utf-8"), str(self.output), "exec"), namespace)
|
||||
return namespace
|
||||
|
||||
def update(self, name, clips, **options):
|
||||
return animation_catalog.update_catalog(name, clips, output=self.output, **options)
|
||||
|
||||
def test_new_model_produces_enum_with_durations_without_unreal(self):
|
||||
self.assertEqual(self.update("StudentHero", {"walking": 2.0}), "StudentHeroAnimations")
|
||||
clip = self.load()["StudentHeroAnimations"].WALKING
|
||||
self.assertEqual(clip.key, "walking")
|
||||
self.assertEqual(clip.seconds, 2)
|
||||
self.assertEqual(clip.frames(24), 48)
|
||||
self.assertNotIn("unreal", sys.modules)
|
||||
|
||||
def test_full_update_preserves_other_models_and_removes_deleted_clips(self):
|
||||
self.update("Fighter", {"walk": 1.0, "attack": 2.0})
|
||||
self.update("StudentHero", {"jump": 3.0})
|
||||
self.update("Fighter", {"walk": 1.5})
|
||||
namespace = self.load()
|
||||
self.assertEqual([clip.key for clip in namespace["FighterAnimations"]], ["walk"])
|
||||
self.assertEqual(namespace["StudentHeroAnimations"].JUMP.seconds, 3.0)
|
||||
|
||||
def test_partial_selection_updates_duration_without_erasing_other_clips(self):
|
||||
self.update("Hero", {"walk": 1.0, "attack": 2.0})
|
||||
self.update("Hero", {"walk": 1.5}, replace=False)
|
||||
catalog = self.load()["HeroAnimations"]
|
||||
self.assertEqual(catalog.WALK.seconds, 1.5)
|
||||
self.assertEqual(catalog.ATTACK.seconds, 2.0)
|
||||
|
||||
def test_unchanged_metadata_does_not_rewrite_file(self):
|
||||
self.update("Hero", {"walk": 1.0})
|
||||
with patch.object(animation_catalog.tempfile, "NamedTemporaryFile", side_effect=AssertionError("rewritten")):
|
||||
self.update("Hero", {"walk": 1.0})
|
||||
|
||||
def test_empty_model_is_valid(self):
|
||||
self.update("Hero", {})
|
||||
self.assertEqual(list(self.load()["HeroAnimations"]), [])
|
||||
|
||||
def test_invalid_metadata_does_not_damage_existing_catalog(self):
|
||||
self.update("Hero", {"walk": 1.0})
|
||||
before = self.output.read_bytes()
|
||||
for data in ({"walk": 0}, {"walk": float("nan")}, {"walk": 1.0, "WALK": 2.0}):
|
||||
with self.subTest(data=data), self.assertRaises(ValueError):
|
||||
self.update("Hero", data)
|
||||
self.assertEqual(self.output.read_bytes(), before)
|
||||
|
||||
def test_publish_adds_class_to_loaded_module_without_replacing_other_enums(self):
|
||||
module = ModuleType("ue_workshop.animations")
|
||||
module.AnimationClip = AnimationClip
|
||||
module.ExistingAnimations = object()
|
||||
original = module.ExistingAnimations
|
||||
source = animation_catalog._class_source("StudentHeroAnimations", {"walk": 2.0})
|
||||
with patch.dict(sys.modules, {"ue_workshop.animations": module}):
|
||||
animation_catalog._publish_class(source, self.output)
|
||||
self.assertEqual(module.StudentHeroAnimations.WALK.frames(24), 48)
|
||||
self.assertIs(module.ExistingAnimations, original)
|
||||
@@ -19,6 +19,9 @@ class Clip:
|
||||
def get_name(self):
|
||||
return "EmbeddedTake"
|
||||
|
||||
def get_play_length(self):
|
||||
return 1.5
|
||||
|
||||
|
||||
class DiscoveryTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -40,6 +43,7 @@ class DiscoveryTests(unittest.TestCase):
|
||||
self.cache = patch.object(self.module, "_cached", return_value=[Mesh()]).start()
|
||||
self.importer = patch.object(self.module, "_import", return_value=[Mesh()]).start()
|
||||
self.animation = patch.object(self.module, "import_animation", side_effect=lambda *a, **kw: Clip()).start()
|
||||
self.catalog = patch.object(self.module, "update_catalog").start()
|
||||
self.addCleanup(patch.stopall)
|
||||
|
||||
def test_discovery_excludes_model_and_derives_names_without_aliases(self):
|
||||
@@ -55,6 +59,8 @@ class DiscoveryTests(unittest.TestCase):
|
||||
self.assertEqual(self.animation.call_count, 2)
|
||||
for call in self.animation.call_args_list:
|
||||
self.assertFalse(call.kwargs["reimport"])
|
||||
self.catalog.assert_called_once_with("Hero", {
|
||||
"great_sword_attack_2": 1.5, "great_sword_walk": 1.5}, replace=True)
|
||||
|
||||
def test_empty_dictionary_disables_discovery_and_keeps_embedded_name(self):
|
||||
self.cache.return_value = [Mesh(), Clip()]
|
||||
@@ -67,6 +73,7 @@ class DiscoveryTests(unittest.TestCase):
|
||||
self.assertEqual(set(result.animations), {"selected"})
|
||||
self.animation.assert_called_once()
|
||||
self.assertEqual(self.animation.call_args.args[0], self.walk)
|
||||
self.catalog.assert_called_once_with("Hero", {"selected": 1.5}, replace=False)
|
||||
|
||||
def test_reimport_true_reimports_model_and_discovered_clips(self):
|
||||
self.module.import_model(self.model, name="Hero", reimport=True)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Validate light keys without an Unreal installation."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
class LightKeyTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.unreal = MagicMock()
|
||||
self.unreal.FrameNumber.side_effect = lambda frame: frame
|
||||
path = Path(__file__).resolve().parents[1] / "Content/Python/ue_workshop/sequence.py"
|
||||
spec = importlib.util.spec_from_file_location("ue_workshop._light_key_test", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, {"unreal": self.unreal}):
|
||||
spec.loader.exec_module(module)
|
||||
self.shot = module.Sequence.__new__(module.Sequence)
|
||||
self.shot.frames = 48
|
||||
self.shot._light_tracks = {}
|
||||
self.shot.bind = MagicMock()
|
||||
self.track = self.shot.bind.return_value.add_track.return_value
|
||||
self.channels = [MagicMock() for _ in range(4)]
|
||||
self.track.add_section.return_value.get_all_channels.return_value = self.channels
|
||||
self.actor = MagicMock()
|
||||
self.light = self.actor.get_component_by_class.return_value
|
||||
self.light.get_path_name.return_value = "Light"
|
||||
|
||||
def test_color_keys_rgb_and_leaves_alpha_at_default(self):
|
||||
self.shot.light_key(self.actor, 10, color=(1, 0.5, 0))
|
||||
self.shot.bind.assert_called_once_with(self.light)
|
||||
self.track.set_property_name_and_path.assert_called_once_with("LightColor", "LightColor")
|
||||
self.assertEqual(self.channels[1].add_key.call_args.args, (10, 0.5))
|
||||
self.channels[3].set_default.assert_called_once_with(1.0)
|
||||
self.channels[3].add_key.assert_not_called()
|
||||
|
||||
def test_later_keys_reuse_the_track(self):
|
||||
self.shot.light_key(self.actor, 0, intensity=0)
|
||||
self.shot.light_key(self.actor, 47, intensity=5000)
|
||||
self.track.add_section.assert_called_once()
|
||||
self.track.set_property_name_and_path.assert_called_once_with("Intensity", "Intensity")
|
||||
self.assertEqual(self.channels[0].add_key.call_count, 2)
|
||||
|
||||
def test_color_and_intensity_use_separate_tracks(self):
|
||||
self.shot.light_key(self.actor, 0, color=(1, 1, 1), intensity=100)
|
||||
self.assertEqual(self.track.add_section.call_count, 2)
|
||||
|
||||
def test_invalid_arguments_create_no_track(self):
|
||||
for args in [dict(frame=48, color=(1, 1, 1)), dict(frame=0, color=(1, 1)),
|
||||
dict(frame=0, intensity=1, interpolation="smooth")]:
|
||||
with self.assertRaises(ValueError):
|
||||
self.shot.light_key(self.actor, **args)
|
||||
self.shot.bind.assert_not_called()
|
||||
|
||||
def test_actor_without_light_is_rejected(self):
|
||||
self.actor.get_component_by_class.return_value = None
|
||||
with self.assertRaises(ValueError):
|
||||
self.shot.light_key(self.actor, 0, intensity=1)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Horizontal root travel, interpolation and accumulated playback loops."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
class Vector:
|
||||
def __init__(self, x, y, z):
|
||||
self.x, self.y, self.z = x, y, z
|
||||
|
||||
|
||||
class RootMotionTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
unreal = MagicMock()
|
||||
unreal.Vector = Vector
|
||||
path = Path(__file__).resolve().parents[1] / "Content/Python/ue_workshop/root_motion.py"
|
||||
spec = importlib.util.spec_from_file_location("ue_workshop._root_motion_test", path)
|
||||
self.module = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, {"unreal": unreal}):
|
||||
spec.loader.exec_module(self.module)
|
||||
self.poses = [SimpleNamespace(translation=Vector(0, 0, 100)),
|
||||
SimpleNamespace(translation=Vector(0, 50, 110)),
|
||||
SimpleNamespace(translation=Vector(0, 100, 100))]
|
||||
|
||||
def test_horizontal_interpolation_keeps_vertical_motion_in_clip(self):
|
||||
value = self.module.displacement(self.poses, 2.0, 0.5)
|
||||
self.assertEqual((value.x, value.y, value.z), (0, 25, 0))
|
||||
|
||||
def test_end_and_repeated_playback_accumulate_instead_of_reset(self):
|
||||
self.assertEqual(self.module.displacement(self.poses, 2.0, 2).y, 100)
|
||||
self.assertEqual(self.module.displacement(self.poses, 2.0, 5).y, 250)
|
||||
|
||||
def test_before_start_has_no_displacement(self):
|
||||
self.assertEqual(self.module.displacement(self.poses, 2.0, -1).y, 0)
|
||||
|
||||
def test_reverse_accumulates_backwards_without_initial_jump(self):
|
||||
self.assertEqual(self.module.displacement(self.poses, 2.0, 0, reverse=True).y, 0)
|
||||
self.assertEqual(self.module.displacement(self.poses, 2.0, 1, reverse=True).y, -50)
|
||||
self.assertEqual(self.module.displacement(self.poses, 2.0, 5, reverse=True).y, -250)
|
||||
|
||||
def test_reverse_samples_the_end_of_a_nonuniform_trajectory(self):
|
||||
self.poses[1].translation.y = 20
|
||||
self.assertEqual(self.module.displacement(self.poses, 2.0, 0.5, reverse=True).y, -40)
|
||||
@@ -7,6 +7,11 @@ import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "Content/Python"))
|
||||
from ue_workshop.animation_clip import AnimationClip
|
||||
|
||||
|
||||
class KnownClip(AnimationClip):
|
||||
WALK = ("walk", 2.0)
|
||||
|
||||
|
||||
class Clip:
|
||||
@@ -17,6 +22,12 @@ class Clip:
|
||||
assert name == "skeleton"
|
||||
return self.skeleton
|
||||
|
||||
def get_path_name(self):
|
||||
return "OriginalClip"
|
||||
|
||||
def get_play_length(self):
|
||||
return 2.0
|
||||
|
||||
|
||||
class AnimationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -35,6 +46,10 @@ class AnimationTests(unittest.TestCase):
|
||||
self.shot.asset = MagicMock()
|
||||
self.shot.asset.get_tick_resolution.return_value = SimpleNamespace(numerator=24000, denominator=1)
|
||||
self.shot._animations = {}
|
||||
self.shot._transforms = {}
|
||||
self.shot._root_motion = {}
|
||||
self.shot._root_motion_copies = {}
|
||||
self.shot._pose_holds = {}
|
||||
self.shot.bind = MagicMock()
|
||||
self.actor = MagicMock()
|
||||
self.actor.get_path_name.return_value = "Parent"
|
||||
@@ -44,7 +59,7 @@ class AnimationTests(unittest.TestCase):
|
||||
self.clip = Clip(self.skeleton)
|
||||
self.imported = SimpleNamespace(mesh=MagicMock(), animations={"walk": self.clip})
|
||||
self.imported.mesh.get_editor_property.return_value = self.skeleton
|
||||
self.shot.scene = SimpleNamespace(_skeletal_models={"Parent": (self.child, self.imported)})
|
||||
self.shot.scene = SimpleNamespace(name="TestScene", _skeletal_models={"Parent": (self.child, self.imported)})
|
||||
|
||||
def test_adjacent_clips_share_child_track_and_set_speed(self):
|
||||
self.shot.animation(self.actor, "walk", start=0, end=48)
|
||||
@@ -55,6 +70,24 @@ class AnimationTests(unittest.TestCase):
|
||||
track.add_section.return_value.set_range.assert_called_with(48, 120)
|
||||
self.unreal.MovieSceneTimeWarpVariant.return_value.set_fixed_play_rate.assert_called_with(0.8)
|
||||
|
||||
def test_enum_plays_once_when_end_is_omitted(self):
|
||||
section = self.shot.animation(self.actor, KnownClip.WALK, start=10)
|
||||
section.set_range.assert_called_with(10, 58)
|
||||
section.get_editor_property.return_value.set_editor_property.assert_any_call("animation", self.clip)
|
||||
|
||||
def test_enum_automatic_end_respects_speed(self):
|
||||
section = self.shot.animation(self.actor, KnownClip.WALK, start=10, speed=2)
|
||||
section.set_range.assert_called_with(10, 34)
|
||||
|
||||
def test_explicit_end_overrides_enum_duration(self):
|
||||
section = self.shot.animation(self.actor, KnownClip.WALK, start=0, end=120)
|
||||
section.set_range.assert_called_with(0, 120)
|
||||
|
||||
def test_enum_cannot_exceed_sequence_length(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.shot.animation(self.actor, KnownClip.WALK, start=100)
|
||||
self.shot.bind.assert_not_called()
|
||||
|
||||
def test_nested_clip_is_rejected_without_creating_section(self):
|
||||
self.shot.animation(self.actor, "walk", start=0, end=48)
|
||||
with self.assertRaises(ValueError):
|
||||
@@ -64,11 +97,62 @@ class AnimationTests(unittest.TestCase):
|
||||
def test_invalid_ranges_and_speeds_do_not_create_track(self):
|
||||
for options in ({"start": -1}, {"end": 121}, {"start": 48, "end": 48},
|
||||
{"start": True}, {"speed": 0}, {"speed": float("nan")},
|
||||
{"speed": float("inf")}):
|
||||
{"speed": float("inf")}, {"keep_end_position": 1}, {"reverse": 1},
|
||||
{"keep_end_pose": 1}):
|
||||
with self.subTest(options=options), self.assertRaises(ValueError):
|
||||
self.shot.animation(self.actor, "walk", **options)
|
||||
self.shot.bind.assert_not_called()
|
||||
|
||||
def test_reverse_is_set_on_native_animation_parameters(self):
|
||||
section = self.shot.animation(self.actor, "walk", start=0, end=60, reverse=True)
|
||||
section.get_editor_property.return_value.set_editor_property.assert_any_call("reverse", True)
|
||||
|
||||
def test_keep_end_pose_uses_native_keep_state(self):
|
||||
main, hold = MagicMock(), MagicMock()
|
||||
self.shot.bind.return_value.add_track.return_value.add_section.side_effect = [main, hold]
|
||||
section = self.shot.animation(self.actor, KnownClip.WALK, start=10, keep_end_pose=True)
|
||||
section.set_completion_mode.assert_called_once_with(self.unreal.MovieSceneCompletionMode.KEEP_STATE)
|
||||
section.set_range.assert_called_with(10, 58)
|
||||
hold.set_range.assert_called_with(58, 120)
|
||||
self.unreal.MovieSceneTimeWarpVariant.return_value.set_fixed_play_rate.assert_called_with(0.0)
|
||||
|
||||
def test_later_clip_truncates_frozen_pose(self):
|
||||
main, hold, next_section = MagicMock(), MagicMock(), MagicMock()
|
||||
self.shot.bind.return_value.add_track.return_value.add_section.side_effect = [main, hold, next_section]
|
||||
self.shot.animation(self.actor, KnownClip.WALK, start=0, keep_end_pose=True)
|
||||
self.shot.animation(self.actor, KnownClip.WALK, start=60)
|
||||
hold.set_range.assert_called_with(48, 60)
|
||||
|
||||
def test_relative_move_replaces_baked_keys_and_offsets_future_motion(self):
|
||||
axes = [MagicMock() for _ in range(3)]
|
||||
future_keys = []
|
||||
for axis in axes:
|
||||
interior, future = MagicMock(), MagicMock()
|
||||
interior.get_time.return_value.frame_number.value = 90
|
||||
future.get_time.return_value.frame_number.value = 150
|
||||
future.get_value.return_value = 100
|
||||
axis.get_keys.return_value = [interior, future]
|
||||
future_keys.append(future)
|
||||
self.shot._transforms["Parent"] = axes
|
||||
self.shot.position = MagicMock(side_effect=[(0, 300, 0), (0, 350, 0)])
|
||||
self.shot.transform_key = MagicMock()
|
||||
self.assertEqual(self.shot.move(self.actor, 80, 110, offset=(0, -200, 0)), (0, 100, 0))
|
||||
for axis in axes:
|
||||
axis.remove_key.assert_called_once_with(axis.get_keys.return_value[0])
|
||||
future_keys[1].set_value.assert_called_once_with(-150)
|
||||
|
||||
def test_keep_end_position_uses_copy_and_adds_transform_motion(self):
|
||||
copy = Clip(self.skeleton)
|
||||
prepare = MagicMock(return_value=(copy, ["poses"], 2.0))
|
||||
self.shot._keep_end_position = MagicMock()
|
||||
with patch.dict(sys.modules, {"ue_workshop.root_motion": SimpleNamespace(prepare=prepare)}):
|
||||
section = self.shot.animation(self.actor, "walk", start=0, end=48, keep_end_position=True)
|
||||
self.shot.animation(self.actor, "walk", start=60, end=108, keep_end_position=True)
|
||||
prepare.assert_called_once()
|
||||
self.assertIs(self.imported.animations["walk"], self.clip)
|
||||
section.get_editor_property.return_value.set_editor_property.assert_any_call("animation", copy)
|
||||
self.assertEqual(self.shot._keep_end_position.call_count, 2)
|
||||
|
||||
def test_crossfade_sets_complementary_weight_curves(self):
|
||||
track = self.shot.bind.return_value.add_track.return_value
|
||||
first, second = MagicMock(), MagicMock()
|
||||
@@ -143,3 +227,33 @@ class AnimationTests(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
self.shot.animation(self.actor, "walk")
|
||||
self.shot.bind.assert_not_called()
|
||||
|
||||
def test_position_uses_existing_sequencer_curves(self):
|
||||
axes = [MagicMock(), MagicMock(), MagicMock()]
|
||||
for channel, value in zip(axes, (10, 250, 3)):
|
||||
channel.evaluate_keys.return_value = [value]
|
||||
self.shot._transforms["Parent"] = axes
|
||||
self.assertEqual(self.shot.position(self.actor, 80), (10, 250, 3))
|
||||
self.shot.asset.make_range.assert_called_once_with(80, 1)
|
||||
|
||||
def test_position_without_track_uses_spawn_position(self):
|
||||
self.actor.get_actor_location.return_value.to_tuple.return_value = (30, -850, 0)
|
||||
self.assertEqual(self.shot.position(self.actor, 80), (30, -850, 0))
|
||||
|
||||
def test_relative_move_anchors_existing_position_and_adds_offset(self):
|
||||
self.shot.position = MagicMock(return_value=(10, 250, 3))
|
||||
self.shot.transform_key = MagicMock()
|
||||
target = self.shot.move(self.actor, start=80, end=110, offset=(0, 50, 0))
|
||||
self.assertEqual(target, (10, 300, 3))
|
||||
self.shot.position.assert_called_once_with(self.actor, 80)
|
||||
self.shot.transform_key.assert_any_call(self.actor, 80, location=(10, 250, 3), interpolation="auto")
|
||||
self.shot.transform_key.assert_any_call(self.actor, 110, location=(10, 300, 3), interpolation="auto")
|
||||
|
||||
def test_invalid_relative_move_does_not_write_keys(self):
|
||||
self.shot.transform_key = MagicMock()
|
||||
for options in ({"start": 80, "end": 80}, {"start": 80, "end": 120},
|
||||
{"start": 80, "end": 110, "offset": (0, float("nan"), 0)}):
|
||||
options.setdefault("offset", (0, 50, 0))
|
||||
with self.subTest(options=options), self.assertRaises(ValueError):
|
||||
self.shot.move(self.actor, **options)
|
||||
self.shot.transform_key.assert_not_called()
|
||||
Reference in new issue
Block a user