diff --git a/include/scratchcpp/istacktimer.h b/include/scratchcpp/istacktimer.h index c482042b..d81c28f5 100644 --- a/include/scratchcpp/istacktimer.h +++ b/include/scratchcpp/istacktimer.h @@ -28,6 +28,9 @@ class LIBSCRATCHCPP_EXPORT IStackTimer /*! Returns true if the timer has elapsed. */ virtual bool elapsed() const = 0; + + /*! Returns the elapsed time in seconds. */ + virtual double elapsedTime() const = 0; }; } // namespace libscratchcpp diff --git a/src/blocks/motionblocks.cpp b/src/blocks/motionblocks.cpp index a249cdff..ae565a28 100644 --- a/src/blocks/motionblocks.cpp +++ b/src/blocks/motionblocks.cpp @@ -1,9 +1,27 @@ // SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "motionblocks.h" using namespace libscratchcpp; +static const double pi = std::acos(-1); // TODO: Use std::numbers::pi in C++20 + std::string MotionBlocks::name() const { return "Motion"; @@ -21,4 +39,712 @@ Rgb MotionBlocks::color() const void MotionBlocks::registerBlocks(IEngine *engine) { + engine->addCompileFunction(this, "motion_movesteps", &compileMoveSteps); + engine->addCompileFunction(this, "motion_turnright", &compileTurnRight); + engine->addCompileFunction(this, "motion_turnleft", &compileTurnLeft); + engine->addCompileFunction(this, "motion_pointindirection", &compilePointInDirection); + engine->addCompileFunction(this, "motion_pointtowards", &compilePointTowards); + engine->addCompileFunction(this, "motion_gotoxy", &compileGoToXY); + engine->addCompileFunction(this, "motion_goto", &compileGoTo); + engine->addCompileFunction(this, "motion_glidesecstoxy", &compileGlideSecsToXY); + engine->addCompileFunction(this, "motion_glideto", &compileGlideTo); + engine->addCompileFunction(this, "motion_changexby", &compileChangeXBy); + engine->addCompileFunction(this, "motion_setx", &compileSetX); + engine->addCompileFunction(this, "motion_changeyby", &compileChangeYBy); + engine->addCompileFunction(this, "motion_sety", &compileSetY); + engine->addCompileFunction(this, "motion_ifonedgebounce", &compileIfOnEdgeBounce); + engine->addCompileFunction(this, "motion_setrotationstyle", &compileSetRotationStyle); + engine->addCompileFunction(this, "motion_xposition", &compileXPosition); + engine->addCompileFunction(this, "motion_yposition", &compileYPosition); + engine->addCompileFunction(this, "motion_direction", &compileDirection); +} + +CompilerValue *MotionBlocks::compileMoveSteps(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *steps = compiler->addInput("STEPS"); + compiler->addTargetFunctionCall("motion_movesteps", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { steps }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileTurnRight(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *degrees = compiler->addInput("DEGREES"); + compiler->addTargetFunctionCall("motion_turnright", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { degrees }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileTurnLeft(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *degrees = compiler->addInput("DEGREES"); + compiler->addTargetFunctionCall("motion_turnleft", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { degrees }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compilePointInDirection(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *direction = compiler->addInput("DIRECTION"); + compiler->addTargetFunctionCall("motion_pointindirection", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { direction }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compilePointTowards(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return nullptr; + + Input *input = compiler->input("TOWARDS"); + + if (input->pointsToDropdownMenu()) { + std::string value = input->selectedMenuItem(); + + if (value == "_mouse_") + compiler->addTargetFunctionCall("motion_point_towards_mouse"); + else if (value == "_random_") + compiler->addFunctionCallWithCtx("motion_point_towards_random_direction"); + else { + int index = compiler->engine()->findTarget(value); + Target *anotherTarget = compiler->engine()->targetAt(index); + + if (anotherTarget && !anotherTarget->isStage()) + compiler->addTargetFunctionCall("motion_point_towards_target_by_index", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { compiler->addConstValue(index) }); + } + } else { + CompilerValue *towards = compiler->addInput(input); + compiler->addFunctionCallWithCtx("motion_pointtowards", Compiler::StaticType::Void, { Compiler::StaticType::String }, { towards }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileGoToXY(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *x = compiler->addInput("X"); + CompilerValue *y = compiler->addInput("Y"); + compiler->addTargetFunctionCall("motion_gotoxy", Compiler::StaticType::Void, { Compiler::StaticType::Number, Compiler::StaticType::Number }, { x, y }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileGoTo(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return nullptr; + + Input *input = compiler->input("TO"); + + if (input->pointsToDropdownMenu()) { + std::string value = input->selectedMenuItem(); + + if (value == "_mouse_") + compiler->addTargetFunctionCall("motion_go_to_mouse"); + else if (value == "_random_") + compiler->addFunctionCallWithCtx("motion_go_to_random_pos"); + else { + int index = compiler->engine()->findTarget(value); + Target *anotherTarget = compiler->engine()->targetAt(index); + + if (anotherTarget && !anotherTarget->isStage()) + compiler->addTargetFunctionCall("motion_go_to_target_by_index", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { compiler->addConstValue(index) }); + } + } else { + CompilerValue *to = compiler->addInput(input); + compiler->addFunctionCallWithCtx("motion_goto", Compiler::StaticType::Void, { Compiler::StaticType::String }, { to }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileGlideSecsToXY(Compiler *compiler) +{ + Target *target = compiler->target(); + CompilerValue *duration = compiler->addInput("SECS"); + CompilerValue *startX = target->isStage() ? compiler->addConstValue(0) : compiler->addTargetFunctionCall("motion_xposition", Compiler::StaticType::Number); + CompilerValue *startY = target->isStage() ? compiler->addConstValue(0) : compiler->addTargetFunctionCall("motion_yposition", Compiler::StaticType::Number); + CompilerValue *endX = compiler->addInput("X"); + CompilerValue *endY = compiler->addInput("Y"); + + compiler->addFunctionCallWithCtx("motion_start_glide", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { duration }); + compiler->createYield(); + + compiler->beginLoopCondition(); + auto numType = Compiler::StaticType::Number; + CompilerValue *elapsed = compiler->addFunctionCallWithCtx("motion_glide", Compiler::StaticType::Bool, { numType, numType, numType, numType, numType }, { duration, startX, startY, endX, endY }); + compiler->beginRepeatUntilLoop(elapsed); + compiler->endLoop(); + + return nullptr; +} + +CompilerValue *MotionBlocks::compileGlideTo(Compiler *compiler) +{ + Target *target = compiler->target(); + CompilerValue *duration = compiler->addInput("SECS"); + CompilerValue *startX = target->isStage() ? compiler->addConstValue(0) : compiler->addTargetFunctionCall("motion_xposition", Compiler::StaticType::Number); + CompilerValue *startY = target->isStage() ? compiler->addConstValue(0) : compiler->addTargetFunctionCall("motion_yposition", Compiler::StaticType::Number); + CompilerValue *endX = target->isStage() ? compiler->addConstValue(0) : nullptr; + CompilerValue *endY = target->isStage() ? compiler->addConstValue(0) : nullptr; + + Input *input = compiler->input("TO"); + bool ifStatement = false; + + if (input->pointsToDropdownMenu()) { + std::string value = input->selectedMenuItem(); + + if (value == "_mouse_") { + if (!target->isStage()) { + endX = compiler->addFunctionCallWithCtx("motion_get_mouse_x", Compiler::StaticType::Number); + endY = compiler->addFunctionCallWithCtx("motion_get_mouse_y", Compiler::StaticType::Number); + } + } else if (value == "_random_") { + if (!target->isStage()) { + endX = compiler->addFunctionCallWithCtx("motion_get_random_x", Compiler::StaticType::Number); + endY = compiler->addFunctionCallWithCtx("motion_get_random_y", Compiler::StaticType::Number); + } + } else { + int index = compiler->engine()->findTarget(value); + Target *anotherTarget = compiler->engine()->targetAt(index); + + if (anotherTarget && !anotherTarget->isStage()) { + if (!target->isStage()) { + endX = compiler->addFunctionCallWithCtx("motion_get_sprite_x_by_index", Compiler::StaticType::Number, { Compiler::StaticType::Number }, { compiler->addConstValue(index) }); + endY = compiler->addFunctionCallWithCtx("motion_get_sprite_y_by_index", Compiler::StaticType::Number, { Compiler::StaticType::Number }, { compiler->addConstValue(index) }); + } + } else + return nullptr; + } + } else { + CompilerValue *to = compiler->addInput(input); + CompilerValue *valid = compiler->addFunctionCallWithCtx("motion_is_target_valid", Compiler::StaticType::Bool, { Compiler::StaticType::String }, { to }); + ifStatement = true; + compiler->beginIfStatement(valid); + + if (!target->isStage()) { + endX = compiler->addFunctionCallWithCtx("motion_get_target_x", Compiler::StaticType::Number, { Compiler::StaticType::String }, { to }); + endY = compiler->addFunctionCallWithCtx("motion_get_target_y", Compiler::StaticType::Number, { Compiler::StaticType::String }, { to }); + } + } + + assert(endX && endY); + assert(!target->isStage() || (target->isStage() && endX->isConst() && endY->isConst())); + + compiler->addFunctionCallWithCtx("motion_start_glide", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { duration }); + compiler->createYield(); + + compiler->beginLoopCondition(); + auto numType = Compiler::StaticType::Number; + CompilerValue *elapsed = compiler->addFunctionCallWithCtx("motion_glide", Compiler::StaticType::Bool, { numType, numType, numType, numType, numType }, { duration, startX, startY, endX, endY }); + compiler->beginRepeatUntilLoop(elapsed); + compiler->endLoop(); + + if (ifStatement) + compiler->endIf(); + + return nullptr; +} + +CompilerValue *MotionBlocks::compileChangeXBy(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *dx = compiler->addInput("DX"); + compiler->addTargetFunctionCall("motion_changexby", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { dx }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileSetX(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *x = compiler->addInput("X"); + compiler->addTargetFunctionCall("motion_setx", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { x }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileChangeYBy(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *dy = compiler->addInput("DY"); + compiler->addTargetFunctionCall("motion_changeyby", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { dy }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileSetY(Compiler *compiler) +{ + if (!compiler->target()->isStage()) { + CompilerValue *y = compiler->addInput("Y"); + compiler->addTargetFunctionCall("motion_sety", Compiler::StaticType::Void, { Compiler::StaticType::Number }, { y }); + } + + return nullptr; +} + +CompilerValue *MotionBlocks::compileIfOnEdgeBounce(Compiler *compiler) +{ + if (!compiler->target()->isStage()) + compiler->addTargetFunctionCall("motion_ifonedgebounce"); + + return nullptr; +} + +CompilerValue *MotionBlocks::compileSetRotationStyle(Compiler *compiler) +{ + Target *target = compiler->target(); + + if (target->isStage()) + return nullptr; + + Sprite *sprite = static_cast(target); + Field *field = compiler->field("STYLE"); + + if (!field) + return nullptr; + + std::string option = field->value().toString(); + sprite->setRotationStyle(field->value().toString()); + + if (option == "left-right") + compiler->addTargetFunctionCall("motion_set_left_right_style"); + else if (option == "don't rotate") + compiler->addTargetFunctionCall("motion_set_do_not_rotate_style"); + else if (option == "all around") + compiler->addTargetFunctionCall("motion_set_all_around_style"); + + return nullptr; +} + +CompilerValue *MotionBlocks::compileXPosition(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return compiler->addConstValue(0); + else + return compiler->addTargetFunctionCall("motion_xposition", Compiler::StaticType::Number); +} + +CompilerValue *MotionBlocks::compileYPosition(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return compiler->addConstValue(0); + else + return compiler->addTargetFunctionCall("motion_yposition", Compiler::StaticType::Number); +} + +CompilerValue *MotionBlocks::compileDirection(Compiler *compiler) +{ + if (compiler->target()->isStage()) + return compiler->addConstValue(90); + else + return compiler->addTargetFunctionCall("motion_direction", Compiler::StaticType::Number); +} + +extern "C" void motion_movesteps(Sprite *sprite, double steps) +{ + double dir = sprite->direction(); + sprite->setPosition(sprite->x() + std::sin(dir * pi / 180) * steps, sprite->y() + std::cos(dir * pi / 180) * steps); +} + +extern "C" void motion_turnright(Sprite *sprite, double degrees) +{ + sprite->setDirection(sprite->direction() + degrees); +} + +extern "C" void motion_turnleft(Sprite *sprite, double degrees) +{ + sprite->setDirection(sprite->direction() - degrees); +} + +extern "C" void motion_pointindirection(Sprite *sprite, double direction) +{ + sprite->setDirection(direction); +} + +inline void motion_point_towards_pos(Sprite *sprite, double x, double y) +{ + // https://en.scratch-wiki.info/wiki/Point_Towards_()_(block)#Workaround + double deltaX = x - sprite->x(); + double deltaY = y - sprite->y(); + + if (deltaY == 0) { + if (deltaX < 0) + sprite->setDirection(-90); + else + sprite->setDirection(90); + } else if (deltaY < 0) + sprite->setDirection(180 + (180 / pi) * std::atan(deltaX / deltaY)); + else + sprite->setDirection((180 / pi) * std::atan(deltaX / deltaY)); +} + +extern "C" void motion_point_towards_mouse(Sprite *sprite) +{ + IEngine *engine = sprite->engine(); + motion_point_towards_pos(sprite, engine->mouseX(), engine->mouseY()); +} + +extern "C" void motion_point_towards_random_direction(ExecutionContext *ctx) +{ + Sprite *sprite = static_cast(ctx->thread()->target()); + IRandomGenerator *rng = ctx->rng(); + sprite->setDirection(rng->randint(-180, 179)); +} + +extern "C" void motion_point_towards_target_by_index(Sprite *sprite, double index) +{ + Sprite *anotherSprite = static_cast(sprite->engine()->targetAt(index)); + motion_point_towards_pos(sprite, anotherSprite->x(), anotherSprite->y()); +} + +extern "C" void motion_pointtowards(ExecutionContext *ctx, const StringPtr *towards) +{ + static const StringPtr MOUSE_STR("_mouse_"); + static const StringPtr RANDOM_STR("_random_"); + + Sprite *sprite = static_cast(ctx->thread()->target()); + + if (string_compare_case_sensitive(towards, &MOUSE_STR) == 0) + motion_point_towards_mouse(sprite); + else if (string_compare_case_sensitive(towards, &RANDOM_STR) == 0) + motion_point_towards_random_direction(ctx); + else { + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(towards->data)); + IEngine *engine = ctx->engine(); + Target *anotherTarget = engine->targetAt(engine->findTarget(u8name)); + + if (anotherTarget && !anotherTarget->isStage()) { + Sprite *anotherSprite = static_cast(anotherTarget); + motion_point_towards_pos(sprite, anotherSprite->x(), anotherSprite->y()); + } + } +} + +extern "C" void motion_gotoxy(Sprite *sprite, double x, double y) +{ + sprite->setPosition(x, y); +} + +extern "C" void motion_go_to_mouse(Sprite *sprite) +{ + IEngine *engine = sprite->engine(); + sprite->setPosition(engine->mouseX(), engine->mouseY()); +} + +extern "C" void motion_go_to_random_pos(ExecutionContext *ctx) +{ + Sprite *sprite = static_cast(ctx->thread()->target()); + IEngine *engine = ctx->engine(); + const int stageWidth = engine->stageWidth(); + const int stageHeight = engine->stageHeight(); + IRandomGenerator *rng = ctx->rng(); + sprite->setPosition(rng->randintDouble(-stageWidth / 2.0, stageWidth / 2.0), rng->randintDouble(-stageHeight / 2.0, stageHeight / 2.0)); +} + +extern "C" void motion_go_to_target_by_index(Sprite *sprite, double index) +{ + Sprite *anotherSprite = static_cast(sprite->engine()->targetAt(index)); + sprite->setPosition(anotherSprite->x(), anotherSprite->y()); +} + +extern "C" void motion_goto(ExecutionContext *ctx, const StringPtr *towards) +{ + static const StringPtr MOUSE_STR("_mouse_"); + static const StringPtr RANDOM_STR("_random_"); + + Sprite *sprite = static_cast(ctx->thread()->target()); + + if (string_compare_case_sensitive(towards, &MOUSE_STR) == 0) + motion_go_to_mouse(sprite); + else if (string_compare_case_sensitive(towards, &RANDOM_STR) == 0) + motion_go_to_random_pos(ctx); + else { + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(towards->data)); + IEngine *engine = ctx->engine(); + Target *anotherTarget = engine->targetAt(engine->findTarget(u8name)); + + if (anotherTarget && !anotherTarget->isStage()) { + Sprite *anotherSprite = static_cast(anotherTarget); + sprite->setPosition(anotherSprite->x(), anotherSprite->y()); + } + } +} + +extern "C" void motion_start_glide(ExecutionContext *ctx, double duration) +{ + ctx->stackTimer()->start(duration); +} + +extern "C" bool motion_glide(ExecutionContext *ctx, double duration, double startX, double startY, double endX, double endY) +{ + IStackTimer *timer = ctx->stackTimer(); + Target *target = ctx->thread()->target(); + Sprite *sprite = nullptr; + double elapsedTime = timer->elapsedTime(); + + if (!target->isStage()) + sprite = static_cast(target); + + if (elapsedTime >= duration) { + if (sprite) + sprite->setPosition(endX, endY); + + return true; + } else { + if (sprite) { + double factor = elapsedTime / duration; + assert(factor >= 0 && factor < 1); + sprite->setPosition(startX + (endX - startX) * factor, startY + (endY - startY) * factor); + } + + return false; + } +} + +extern "C" double motion_get_mouse_x(ExecutionContext *ctx) +{ + return ctx->engine()->mouseX(); +} + +extern "C" double motion_get_mouse_y(ExecutionContext *ctx) +{ + return ctx->engine()->mouseY(); +} + +extern "C" double motion_get_random_x(ExecutionContext *ctx) +{ + const int stageWidth = ctx->engine()->stageWidth(); + return ctx->rng()->randintDouble(-stageWidth / 2.0, stageWidth / 2.0); +} + +extern "C" double motion_get_random_y(ExecutionContext *ctx) +{ + const int stageHeight = ctx->engine()->stageHeight(); + return ctx->rng()->randintDouble(-stageHeight / 2.0, stageHeight / 2.0); +} + +extern "C" double motion_get_sprite_x_by_index(ExecutionContext *ctx, double index) +{ + assert(!ctx->engine()->targetAt(index)->isStage()); + Sprite *sprite = static_cast(ctx->engine()->targetAt(index)); + return sprite->x(); +} + +extern "C" double motion_get_sprite_y_by_index(ExecutionContext *ctx, double index) +{ + assert(!ctx->engine()->targetAt(index)->isStage()); + Sprite *sprite = static_cast(ctx->engine()->targetAt(index)); + return sprite->y(); +} + +extern "C" double motion_get_target_x(ExecutionContext *ctx, const StringPtr *name) +{ + static const StringPtr MOUSE_STR("_mouse_"); + static const StringPtr RANDOM_STR("_random_"); + + if (string_compare_case_sensitive(name, &MOUSE_STR) == 0) + return ctx->engine()->mouseX(); + else if (string_compare_case_sensitive(name, &RANDOM_STR) == 0) + return motion_get_random_x(ctx); + else { + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(name->data)); + IEngine *engine = ctx->engine(); + Target *target = engine->targetAt(engine->findTarget(u8name)); + + if (target && !target->isStage()) { + Sprite *sprite = static_cast(target); + return sprite->x(); + } else + return 0; + } +} + +extern "C" double motion_get_target_y(ExecutionContext *ctx, const StringPtr *name) +{ + static const StringPtr MOUSE_STR("_mouse_"); + static const StringPtr RANDOM_STR("_random_"); + + if (string_compare_case_sensitive(name, &MOUSE_STR) == 0) + return ctx->engine()->mouseY(); + else if (string_compare_case_sensitive(name, &RANDOM_STR) == 0) + return motion_get_random_y(ctx); + else { + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(name->data)); + IEngine *engine = ctx->engine(); + Target *target = engine->targetAt(engine->findTarget(u8name)); + + if (target && !target->isStage()) { + Sprite *sprite = static_cast(target); + return sprite->y(); + } else + return 0; + } +} + +extern "C" bool motion_is_target_valid(ExecutionContext *ctx, const StringPtr *name) +{ + static const StringPtr MOUSE_STR("_mouse_"); + static const StringPtr RANDOM_STR("_random_"); + + if (string_compare_case_sensitive(name, &MOUSE_STR) == 0 || string_compare_case_sensitive(name, &RANDOM_STR) == 0) + return true; + else { + // TODO: Use UTF-16 in engine + std::string u8name = utf8::utf16to8(std::u16string(name->data)); + IEngine *engine = ctx->engine(); + Target *target = engine->targetAt(engine->findTarget(u8name)); + return (target && !target->isStage()); + } +} + +extern "C" void motion_changexby(Sprite *sprite, double dx) +{ + sprite->setX(sprite->x() + dx); +} + +extern "C" void motion_setx(Sprite *sprite, double x) +{ + sprite->setX(x); +} + +extern "C" void motion_changeyby(Sprite *sprite, double dy) +{ + sprite->setY(sprite->y() + dy); +} + +extern "C" void motion_sety(Sprite *sprite, double y) +{ + sprite->setY(y); +} + +extern "C" void motion_ifonedgebounce(Sprite *sprite) +{ + // https://github.com/scratchfoundation/scratch-vm/blob/c37745e97e6d8a77ad1dc31a943ea728dd17ba78/src/blocks/scratch3_motion.js#L186-L240 + IEngine *engine = sprite->engine(); + Rect bounds = sprite->boundingRect(); + + // Measure distance to edges + // Values are zero when the sprite is beyond + double stageWidth = engine->stageWidth(); + double stageHeight = engine->stageHeight(); + double distLeft = std::max(0.0, (stageWidth / 2.0) + bounds.left()); + double distTop = std::max(0.0, (stageHeight / 2.0) - bounds.top()); + double distRight = std::max(0.0, (stageWidth / 2.0) - bounds.right()); + double distBottom = std::max(0.0, (stageHeight / 2.0) + bounds.bottom()); + + // Find the nearest edge + // 1 - left + // 2 - top + // 3 - right + // 4 - bottom + unsigned short nearestEdge = 0; + double minDist = std::numeric_limits::infinity(); + + if (distLeft < minDist) { + minDist = distLeft; + nearestEdge = 1; + } + + if (distTop < minDist) { + minDist = distTop; + nearestEdge = 2; + } + + if (distRight < minDist) { + minDist = distRight; + nearestEdge = 3; + } + + if (distBottom < minDist) { + minDist = distBottom; + nearestEdge = 4; + } + + if (minDist > 0) + return; // Not touching any edge + + assert(nearestEdge != 0); + + // Point away from the nearest edge + double radians = (90 - sprite->direction()) * pi / 180; + double dx = std::cos(radians); + double dy = -std::sin(radians); + + switch (nearestEdge) { + case 1: + // Left + dx = std::max(0.2, std::abs(dx)); + break; + + case 2: + // Top + dy = std::max(0.2, std::abs(dy)); + break; + + case 3: + // Right + dx = 0 - std::max(0.2, std::abs(dx)); + break; + + case 4: + // Bottom + dy = 0 - std::max(0.2, std::abs(dy)); + break; + } + + double newDirection = (180 / pi) * (std::atan2(dy, dx)) + 90; + sprite->setDirection(newDirection); + + // Keep within the stage + double fencedX, fencedY; + sprite->keepInFence(sprite->x(), sprite->y(), &fencedX, &fencedY); + sprite->setPosition(fencedX, fencedY); +} + +extern "C" void motion_set_left_right_style(Sprite *sprite) +{ + sprite->setRotationStyle(Sprite::RotationStyle::LeftRight); +} + +extern "C" void motion_set_do_not_rotate_style(Sprite *sprite) +{ + sprite->setRotationStyle(Sprite::RotationStyle::DoNotRotate); +} + +extern "C" void motion_set_all_around_style(Sprite *sprite) +{ + sprite->setRotationStyle(Sprite::RotationStyle::AllAround); +} + +extern "C" double motion_xposition(Sprite *sprite) +{ + return sprite->x(); +} + +extern "C" double motion_yposition(Sprite *sprite) +{ + return sprite->y(); +} + +extern "C" double motion_direction(Sprite *sprite) +{ + return sprite->direction(); } diff --git a/src/blocks/motionblocks.h b/src/blocks/motionblocks.h index 44d7c0bd..bf81b123 100644 --- a/src/blocks/motionblocks.h +++ b/src/blocks/motionblocks.h @@ -15,6 +15,26 @@ class MotionBlocks : public IExtension Rgb color() const override; void registerBlocks(IEngine *engine) override; + + private: + static CompilerValue *compileMoveSteps(Compiler *compiler); + static CompilerValue *compileTurnRight(Compiler *compiler); + static CompilerValue *compileTurnLeft(Compiler *compiler); + static CompilerValue *compilePointInDirection(Compiler *compiler); + static CompilerValue *compilePointTowards(Compiler *compiler); + static CompilerValue *compileGoToXY(Compiler *compiler); + static CompilerValue *compileGoTo(Compiler *compiler); + static CompilerValue *compileGlideSecsToXY(Compiler *compiler); + static CompilerValue *compileGlideTo(Compiler *compiler); + static CompilerValue *compileChangeXBy(Compiler *compiler); + static CompilerValue *compileSetX(Compiler *compiler); + static CompilerValue *compileChangeYBy(Compiler *compiler); + static CompilerValue *compileSetY(Compiler *compiler); + static CompilerValue *compileIfOnEdgeBounce(Compiler *compiler); + static CompilerValue *compileSetRotationStyle(Compiler *compiler); + static CompilerValue *compileXPosition(Compiler *compiler); + static CompilerValue *compileYPosition(Compiler *compiler); + static CompilerValue *compileDirection(Compiler *compiler); }; } // namespace libscratchcpp diff --git a/src/engine/internal/llvm/llvmcodebuilder.cpp b/src/engine/internal/llvm/llvmcodebuilder.cpp index d676187f..e94e0701 100644 --- a/src/engine/internal/llvm/llvmcodebuilder.cpp +++ b/src/engine/internal/llvm/llvmcodebuilder.cpp @@ -1022,7 +1022,6 @@ std::shared_ptr LLVMCodeBuilder::finalize() } case LLVMInstruction::Type::Yield: - // TODO: Do not allow use after suspend (use after free) createSuspend(coro.get(), warpArg, targetVariables); break; @@ -3167,7 +3166,6 @@ void LLVMCodeBuilder::createSuspend(LLVMCoroutine *coro, llvm::Value *warpArg, l m_builder.SetInsertPoint(suspendBranch); } - freeScopeHeap(); syncVariables(targetVariables); coro->createSuspend(); reloadVariables(targetVariables); diff --git a/src/engine/internal/stacktimer.cpp b/src/engine/internal/stacktimer.cpp index 01ae3bc7..3aa6e449 100644 --- a/src/engine/internal/stacktimer.cpp +++ b/src/engine/internal/stacktimer.cpp @@ -43,3 +43,11 @@ bool StackTimer::elapsed() const return std::chrono::duration_cast(m_clock->currentSteadyTime() - m_startTime).count() >= m_timeLimit; } + +double StackTimer::elapsedTime() const +{ + if (m_stopped) + return 0; + + return std::chrono::duration_cast(m_clock->currentSteadyTime() - m_startTime).count() / 1000.0; +} diff --git a/src/engine/internal/stacktimer.h b/src/engine/internal/stacktimer.h index 54ece0c8..e99a1a3a 100644 --- a/src/engine/internal/stacktimer.h +++ b/src/engine/internal/stacktimer.h @@ -22,6 +22,7 @@ class StackTimer : public IStackTimer bool stopped() const override; bool elapsed() const override; + double elapsedTime() const override; private: std::chrono::steady_clock::time_point m_startTime; diff --git a/test/blocks/motion_blocks_test.cpp b/test/blocks/motion_blocks_test.cpp index f5e4ecf8..a6788c57 100644 --- a/test/blocks/motion_blocks_test.cpp +++ b/test/blocks/motion_blocks_test.cpp @@ -1,15 +1,2013 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include "../common.h" #include "blocks/motionblocks.h" using namespace libscratchcpp; +using namespace libscratchcpp::test; + +using ::testing::Return; class MotionBlocksTest : public testing::Test { public: - void SetUp() override { m_extension = std::make_unique(); } + void SetUp() override + { + m_extension = std::make_unique(); + m_engine = m_project.engine().get(); + m_extension->registerBlocks(m_engine); + } std::unique_ptr m_extension; + Project m_project; + IEngine *m_engine = nullptr; EngineMock m_engineMock; }; + +TEST_F(MotionBlocksTest, MoveSteps) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_movesteps"); + builder.addValueInput("STEPS", 30.25); + + sprite->setX(5.2); + sprite->setY(-0.25); + sprite->setDirection(-61.42); + + builder.build(); + builder.run(); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, -21.36); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, 14.22); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_movesteps"); + builder.addValueInput("STEPS", 30.25); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, TurnRight) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_turnright"); + builder.addValueInput("DEGREES", 12.05); + + sprite->setDirection(124.37); + + builder.build(); + builder.run(); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, 136.42); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_turnright"); + builder.addValueInput("DEGREES", 12.05); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, TurnLeft) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_turnleft"); + builder.addValueInput("DEGREES", 12.05); + + sprite->setDirection(124.37); + + builder.build(); + builder.run(); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, 112.32); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_turnleft"); + builder.addValueInput("DEGREES", 12.05); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, PointInDirection) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_pointindirection"); + builder.addValueInput("DIRECTION", -60.5); + + sprite->setDirection(50.02); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->direction(), -60.5); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_pointindirection"); + builder.addValueInput("DIRECTION", -60.5); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, PointTowardsMouse) +{ + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addDropdownInput("TOWARDS", "_mouse_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(-45.12)); + EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-123.48)); + thread.run(); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, -101.51); + } + + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addValueInput("TOWARDS", "_mouse_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(125.23)); + EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-3.21)); + thread.run(); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, 29.66); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_pointtowards"); + builder.addDropdownInput("TOWARDS", "_mouse_"); + builder.addBlock("motion_pointtowards"); + builder.addValueInput("TOWARDS", "_mouse_"); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, PointTowardsRandomDirection) +{ + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addDropdownInput("TOWARDS", "_random_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + RandomGeneratorMock rng; + ctx->setRng(&rng); + + EXPECT_CALL(rng, randint(-180, 179)).WillOnce(Return(95)); + code->run(ctx.get()); + ASSERT_EQ(sprite->direction(), 95); + } + + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addValueInput("TOWARDS", "_random_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + RandomGeneratorMock rng; + ctx->setRng(&rng); + + EXPECT_CALL(rng, randint(-180, 179)).WillOnce(Return(-42)); + code->run(ctx.get()); + ASSERT_EQ(sprite->direction(), -42); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_pointtowards"); + builder.addDropdownInput("TOWARDS", "_random_"); + builder.addBlock("motion_pointtowards"); + builder.addValueInput("TOWARDS", "_random_"); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, PointTowardsSprite) +{ + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + auto anotherSprite = std::make_shared(); + anotherSprite->setName("def"); + anotherSprite->setEngine(&m_engineMock); + anotherSprite->setX(-45.12); + anotherSprite->setY(-123.48); + anotherSprite->setDirection(-2.5); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addDropdownInput("TOWARDS", "def"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillRepeatedly(Return(anotherSprite.get())); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + thread.run(); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, -101.51); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addDropdownInput("TOWARDS", "def"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillRepeatedly(Return(nullptr)); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + thread.run(); + ASSERT_EQ(sprite->direction(), 21.58); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + auto stage = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addDropdownInput("TOWARDS", "_stage_"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, targetAt(0)).WillOnce(Return(stage.get())); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + thread.run(); + ASSERT_EQ(sprite->direction(), 21.58); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + auto anotherSprite = std::make_shared(); + anotherSprite->setName("def"); + anotherSprite->setEngine(&m_engineMock); + anotherSprite->setX(125.23); + anotherSprite->setY(-3.21); + anotherSprite->setDirection(82.54); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addValueInput("TOWARDS", "def"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillOnce(Return(anotherSprite.get())); + thread.run(); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, 29.66); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addValueInput("TOWARDS", "def"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillOnce(Return(nullptr)); + thread.run(); + ASSERT_EQ(sprite->direction(), 21.58); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + sprite->setDirection(21.58); + + auto stage = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_pointtowards"); + builder.addValueInput("TOWARDS", "_stage_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, targetAt(0)).WillOnce(Return(stage.get())); + thread.run(); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, 21.58); + } + + { + auto stage = std::make_shared(); + + auto sprite = std::make_shared(); + sprite->setName("Test"); + sprite->setEngine(&m_engineMock); + sprite->setX(153.2); + sprite->setY(59.27); + sprite->setDirection(-21.58); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_pointtowards"); + builder.addDropdownInput("TOWARDS", "Test"); + builder.addBlock("motion_pointtowards"); + builder.addValueInput("TOWARDS", "Test"); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, GoToXY) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_gotoxy"); + builder.addValueInput("X", -55.2); + builder.addValueInput("Y", 23.254); + + sprite->setX(51.28); + sprite->setY(-0.5); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->x(), -55.2); + ASSERT_EQ(sprite->y(), 23.254); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_gotoxy"); + builder.addValueInput("X", -55.2); + builder.addValueInput("Y", 23.254); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, GoToMouse) +{ + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addDropdownInput("TO", "_mouse_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(-45.12)); + EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-123.48)); + thread.run(); + ASSERT_EQ(sprite->x(), -45.12); + ASSERT_EQ(sprite->y(), -123.48); + } + + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addValueInput("TO", "_mouse_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(125.23)); + EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-3.21)); + thread.run(); + ASSERT_EQ(sprite->x(), 125.23); + ASSERT_EQ(sprite->y(), -3.21); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_goto"); + builder.addDropdownInput("TO", "_mouse_"); + builder.addBlock("motion_goto"); + builder.addValueInput("TO", "_mouse_"); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, GoToRandomPosition) +{ + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addDropdownInput("TO", "_random_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + RandomGeneratorMock rng; + ctx->setRng(&rng); + + EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(640)); + EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(500)); + EXPECT_CALL(rng, randintDouble(-320, 320)).WillOnce(Return(95.2)); + EXPECT_CALL(rng, randintDouble(-250, 250)).WillOnce(Return(-100.025)); + code->run(ctx.get()); + ASSERT_EQ(sprite->x(), 95.2); + ASSERT_EQ(sprite->y(), -100.025); + } + + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addValueInput("TO", "_random_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + RandomGeneratorMock rng; + ctx->setRng(&rng); + + EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(640)); + EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(500)); + EXPECT_CALL(rng, randintDouble(-320, 320)).WillOnce(Return(-21.28)); + EXPECT_CALL(rng, randintDouble(-250, 250)).WillOnce(Return(-100.025)); + code->run(ctx.get()); + ASSERT_EQ(sprite->x(), -21.28); + ASSERT_EQ(sprite->y(), -100.025); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_goto"); + builder.addDropdownInput("TO", "_random_"); + builder.addBlock("motion_goto"); + builder.addValueInput("TO", "_random_"); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, GoToSprite) +{ + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + auto anotherSprite = std::make_shared(); + anotherSprite->setName("def"); + anotherSprite->setEngine(&m_engineMock); + anotherSprite->setX(-45.12); + anotherSprite->setY(-123.48); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addDropdownInput("TO", "def"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillRepeatedly(Return(anotherSprite.get())); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + thread.run(); + ASSERT_EQ(sprite->x(), -45.12); + ASSERT_EQ(sprite->y(), -123.48); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addDropdownInput("TO", "def"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillRepeatedly(Return(nullptr)); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + thread.run(); + ASSERT_EQ(sprite->x(), 70.1); + ASSERT_EQ(sprite->y(), -100.025); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + auto stage = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addDropdownInput("TO", "_stage_"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, targetAt(0)).WillOnce(Return(stage.get())); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + thread.run(); + ASSERT_EQ(sprite->x(), 70.1); + ASSERT_EQ(sprite->y(), -100.025); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + auto anotherSprite = std::make_shared(); + anotherSprite->setName("def"); + anotherSprite->setEngine(&m_engineMock); + anotherSprite->setX(125.23); + anotherSprite->setY(-3.21); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addValueInput("TO", "def"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillOnce(Return(anotherSprite.get())); + thread.run(); + ASSERT_EQ(sprite->x(), 125.23); + ASSERT_EQ(sprite->y(), -3.21); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addValueInput("TO", "def"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillOnce(Return(nullptr)); + thread.run(); + ASSERT_EQ(sprite->x(), 70.1); + ASSERT_EQ(sprite->y(), -100.025); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + auto stage = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_goto"); + builder.addValueInput("TO", "_stage_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, targetAt(0)).WillOnce(Return(stage.get())); + thread.run(); + ASSERT_EQ(sprite->x(), 70.1); + ASSERT_EQ(sprite->y(), -100.025); + } + + { + auto stage = std::make_shared(); + + auto sprite = std::make_shared(); + sprite->setName("Test"); + sprite->setEngine(&m_engineMock); + sprite->setX(153.2); + sprite->setY(59.27); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_goto"); + builder.addDropdownInput("TO", "Test"); + builder.addBlock("motion_goto"); + builder.addValueInput("TO", "Test"); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, GlideSecsToXY) +{ + { + auto sprite = std::make_shared(); + sprite->setX(51.28); + sprite->setY(-0.5); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_glidesecstoxy"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("X", -55.2); + builder.addValueInput("Y", 23.254); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(0.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, 29.98); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, 4.25); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(1.75)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, -23.26); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, 16.13); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.55)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->x(), -55.2); + ASSERT_EQ(sprite->y(), 23.254); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_glidesecstoxy"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("X", -55.2); + builder.addValueInput("Y", 23.254); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(0.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(1.75)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.55)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } +} + +TEST_F(MotionBlocksTest, GlideToMouse) +{ + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addDropdownInput("TO", "_mouse_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(-45.12)); + EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-123.48)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(0.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, 47.06); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, -104.72); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(1.75)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, -10.55); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, -116.44); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.55)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->x(), -45.12); + ASSERT_EQ(sprite->y(), -123.48); + } + + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("TO", "_mouse_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + EXPECT_CALL(m_engineMock, mouseX()).WillOnce(Return(-45.12)); + EXPECT_CALL(m_engineMock, mouseY()).WillOnce(Return(-123.48)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(0.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, 47.06); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, -104.72); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(1.75)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, -10.55); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, -116.44); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.55)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->x(), -45.12); + ASSERT_EQ(sprite->y(), -123.48); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addDropdownInput("TO", "_mouse_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + + EXPECT_CALL(timer, start(2.5)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(1.3)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("TO", "_mouse_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + + EXPECT_CALL(timer, start(2.5)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.51)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } +} + +TEST_F(MotionBlocksTest, GlideToRandomPosition) +{ + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addDropdownInput("TO", "_random_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + RandomGeneratorMock rng; + StackTimerMock timer; + ctx->setRng(&rng); + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(640)); + EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(500)); + EXPECT_CALL(rng, randintDouble(-320, 320)).WillOnce(Return(-45.12)); + EXPECT_CALL(rng, randintDouble(-250, 250)).WillOnce(Return(-123.48)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(0.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, 47.06); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, -104.72); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.55)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->x(), -45.12); + ASSERT_EQ(sprite->y(), -123.48); + } + + { + auto sprite = std::make_shared(); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("TO", "_random_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + RandomGeneratorMock rng; + StackTimerMock timer; + ctx->setRng(&rng); + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + EXPECT_CALL(m_engineMock, stageWidth()).WillOnce(Return(640)); + EXPECT_CALL(m_engineMock, stageHeight()).WillOnce(Return(500)); + EXPECT_CALL(rng, randintDouble(-320, 320)).WillOnce(Return(-45.12)); + EXPECT_CALL(rng, randintDouble(-250, 250)).WillOnce(Return(-123.48)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(0.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, 47.06); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, -104.72); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.55)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->x(), -45.12); + ASSERT_EQ(sprite->y(), -123.48); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addDropdownInput("TO", "_random_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + + EXPECT_CALL(timer, start(2.5)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(1.3)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("TO", "_random_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + + EXPECT_CALL(timer, start(2.5)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.51)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } +} + +TEST_F(MotionBlocksTest, GlideToSprite) +{ + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + auto anotherSprite = std::make_shared(); + anotherSprite->setName("def"); + anotherSprite->setEngine(&m_engineMock); + anotherSprite->setX(-45.12); + anotherSprite->setY(-123.48); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addDropdownInput("TO", "def"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillRepeatedly(Return(anotherSprite.get())); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(0.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, 47.06); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, -104.72); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.55)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->x(), -45.12); + ASSERT_EQ(sprite->y(), -123.48); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addDropdownInput("TO", "def"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(2)); + EXPECT_CALL(m_engineMock, targetAt(2)).WillRepeatedly(Return(nullptr)); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + EXPECT_CALL(timer, start).Times(0); + + code->run(ctx.get()); + ASSERT_EQ(sprite->x(), 70.1); + ASSERT_EQ(sprite->y(), -100.025); + } + + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + auto stage = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addDropdownInput("TO", "_stage_"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, targetAt(0)).WillOnce(Return(stage.get())); + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + EXPECT_CALL(timer, start).Times(0); + + code->run(ctx.get()); + ASSERT_EQ(sprite->x(), 70.1); + ASSERT_EQ(sprite->y(), -100.025); + } + + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + auto anotherSprite = std::make_shared(); + anotherSprite->setName("def"); + anotherSprite->setEngine(&m_engineMock); + anotherSprite->setX(-45.12); + anotherSprite->setY(-123.48); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("TO", "def"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillRepeatedly(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillRepeatedly(Return(anotherSprite.get())); + + EXPECT_CALL(timer, start(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(0.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + ASSERT_EQ(std::round(sprite->x() * 100) / 100, 47.06); + ASSERT_EQ(std::round(sprite->y() * 100) / 100, -104.72); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.55)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + ASSERT_EQ(sprite->x(), -45.12); + ASSERT_EQ(sprite->y(), -123.48); + } + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("TO", "def"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + EXPECT_CALL(timer, start).Times(0); + + EXPECT_CALL(m_engineMock, findTarget("def")).WillOnce(Return(5)); + EXPECT_CALL(m_engineMock, targetAt(5)).WillOnce(Return(nullptr)); + code->run(ctx.get()); + ASSERT_EQ(sprite->x(), 70.1); + ASSERT_EQ(sprite->y(), -100.025); + } + + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + + { + auto sprite = std::make_shared(); + sprite->setName("abc"); + sprite->setEngine(&m_engineMock); + sprite->setX(70.1); + sprite->setY(-100.025); + + auto stage = std::make_shared(); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("TO", "_stage_"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + EXPECT_CALL(timer, start).Times(0); + + EXPECT_CALL(m_engineMock, findTarget("_stage_")).WillOnce(Return(0)); + EXPECT_CALL(m_engineMock, targetAt(0)).WillOnce(Return(stage.get())); + code->run(ctx.get()); + ASSERT_EQ(sprite->x(), 70.1); + ASSERT_EQ(sprite->y(), -100.025); + } + + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + + { + auto stage = std::make_shared(); + + auto sprite = std::make_shared(); + sprite->setName("Test"); + sprite->setEngine(&m_engineMock); + sprite->setX(153.2); + sprite->setY(59.27); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addDropdownInput("TO", "Test"); + auto block = builder.currentBlock(); + + EXPECT_CALL(m_engineMock, findTarget("Test")).WillOnce(Return(9)); + EXPECT_CALL(m_engineMock, targetAt(9)).WillOnce(Return(sprite.get())); + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + + EXPECT_CALL(timer, start(2.5)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(1.3)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } + + { + auto stage = std::make_shared(); + + auto sprite = std::make_shared(); + sprite->setName("Test"); + sprite->setEngine(&m_engineMock); + sprite->setX(153.2); + sprite->setY(59.27); + + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_glideto"); + builder.addValueInput("SECS", 2.5); + builder.addValueInput("TO", "Test"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, stage.get()); + auto code = compiler.compile(block); + Script script(stage.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(stage.get(), &m_engineMock, &script); + auto ctx = code->createExecutionContext(&thread); + StackTimerMock timer; + ctx->setStackTimer(&timer); + + EXPECT_CALL(m_engineMock, requestRedraw).Times(0); + + EXPECT_CALL(m_engineMock, findTarget("Test")).WillOnce(Return(9)); + EXPECT_CALL(m_engineMock, targetAt(9)).WillOnce(Return(sprite.get())); + EXPECT_CALL(timer, start(2.5)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(1.3)); + code->run(ctx.get()); + ASSERT_FALSE(code->isFinished(ctx.get())); + + EXPECT_CALL(timer, elapsedTime()).WillOnce(Return(2.5)); + EXPECT_CALL(m_engineMock, requestRedraw()).WillRepeatedly(Return()); + code->run(ctx.get()); + ASSERT_TRUE(code->isFinished(ctx.get())); + } +} + +TEST_F(MotionBlocksTest, ChangeXBy) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_changexby"); + builder.addValueInput("DX", 30.25); + + sprite->setX(5.2); + sprite->setY(-0.25); + sprite->setDirection(-61.42); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->x(), 35.45); + ASSERT_EQ(sprite->y(), -0.25); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_changexby"); + builder.addValueInput("DX", 30.25); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, SetX) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_setx"); + builder.addValueInput("X", 30.25); + + sprite->setX(5.2); + sprite->setY(-0.25); + sprite->setDirection(-61.42); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->x(), 30.25); + ASSERT_EQ(sprite->y(), -0.25); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_setx"); + builder.addValueInput("X", 30.25); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, ChangeYBy) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_changeyby"); + builder.addValueInput("DY", 30.25); + + sprite->setX(5.2); + sprite->setY(-0.25); + sprite->setDirection(-61.42); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->x(), 5.2); + ASSERT_EQ(sprite->y(), 30); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_changeyby"); + builder.addValueInput("DY", 30.25); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, SetY) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_sety"); + builder.addValueInput("Y", 30.25); + + sprite->setX(5.2); + sprite->setY(-0.25); + sprite->setDirection(-61.42); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->x(), 5.2); + ASSERT_EQ(sprite->y(), 30.25); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_sety"); + builder.addValueInput("Y", 30.25); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, IfOnEdgeBounce) +{ + { + auto sprite = std::make_shared(); + SpriteHandlerMock handler; + sprite->setInterface(&handler); + sprite->setEngine(&m_engineMock); + + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_ifonedgebounce"); + auto block = builder.currentBlock(); + + Compiler compiler(&m_engineMock, sprite.get()); + auto code = compiler.compile(block); + Script script(sprite.get(), block, &m_engineMock); + script.setCode(code); + Thread thread(sprite.get(), &m_engineMock, &script); + + EXPECT_CALL(m_engineMock, stageWidth()).WillRepeatedly(Return(480)); + EXPECT_CALL(m_engineMock, stageHeight()).WillRepeatedly(Return(360)); + EXPECT_CALL(m_engineMock, spriteFencingEnabled()).WillRepeatedly(Return(false)); + + // No edge + EXPECT_CALL(handler, boundingRect()).WillOnce(Return(Rect(80, 80, 120, 40))); + sprite->setX(100); + sprite->setY(60); + sprite->setDirection(-45); + thread.run(); + + ASSERT_EQ(sprite->x(), 100); + ASSERT_EQ(sprite->y(), 60); + ASSERT_EQ(sprite->direction(), -45); + + // Left edge + EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(-260, 80, -220, 40))); + sprite->setX(-240); + sprite->setY(60); + thread.reset(); + thread.run(); + + ASSERT_EQ(sprite->x(), -220); + ASSERT_EQ(sprite->y(), 60); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, 45); + + // Top edge + EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(80, 200, 120, 160))); + sprite->setX(100); + sprite->setY(180); + sprite->setDirection(45); + thread.reset(); + thread.run(); + + ASSERT_EQ(sprite->x(), 100); + ASSERT_EQ(sprite->y(), 160); + ASSERT_EQ(sprite->direction(), 135); + + // Right edge + EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(220, 80, 260, 40))); + sprite->setX(240); + sprite->setY(60); + thread.reset(); + thread.run(); + + ASSERT_EQ(sprite->x(), 220); + ASSERT_EQ(sprite->y(), 60); + ASSERT_EQ(sprite->direction(), -135); + + // Bottom edge + EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(-120, -160, -80, -200))); + sprite->setX(-100); + sprite->setY(-180); + thread.reset(); + thread.run(); + + ASSERT_EQ(sprite->x(), -100); + ASSERT_EQ(sprite->y(), -160); + ASSERT_EQ(std::round(sprite->direction() * 100) / 100, -45); + } + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_ifonedgebounce"); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, SetRotationStyle) +{ + // left-right + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_setrotationstyle"); + builder.addDropdownField("STYLE", "left-right"); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->rotationStyle(), Sprite::RotationStyle::LeftRight); + } + + m_engine->clear(); + + // don't rotate + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_setrotationstyle"); + builder.addDropdownField("STYLE", "don't rotate"); + + builder.build(); + builder.run(); + ASSERT_EQ(sprite->rotationStyle(), Sprite::RotationStyle::DoNotRotate); + } + + m_engine->clear(); + + // all around + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_setrotationstyle"); + builder.addDropdownField("STYLE", "all around"); + builder.build(); + + sprite->setRotationStyle(Sprite::RotationStyle::DoNotRotate); + builder.run(); + ASSERT_EQ(sprite->rotationStyle(), Sprite::RotationStyle::AllAround); + } + + m_engine->clear(); + + // invalid + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_setrotationstyle"); + builder.addDropdownField("STYLE", "invalid"); + builder.build(); + + sprite->setRotationStyle(Sprite::RotationStyle::LeftRight); + builder.run(); + ASSERT_EQ(sprite->rotationStyle(), Sprite::RotationStyle::LeftRight); + + sprite->setRotationStyle(Sprite::RotationStyle::AllAround); + builder.run(); + ASSERT_EQ(sprite->rotationStyle(), Sprite::RotationStyle::AllAround); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_setrotationstyle"); + builder.addDropdownField("STYLE", "all around"); + + builder.build(); + builder.run(); + } +} + +TEST_F(MotionBlocksTest, XPosition) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_xposition"); + builder.captureBlockReturnValue(); + builder.build(); + + sprite->setX(5.2); + sprite->setY(-0.25); + sprite->setDirection(-61.42); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 1); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 5.2); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_xposition"); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 1); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 0); + } +} + +TEST_F(MotionBlocksTest, YPosition) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_yposition"); + builder.captureBlockReturnValue(); + builder.build(); + + sprite->setX(5.2); + sprite->setY(-0.25); + sprite->setDirection(-61.42); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 1); + ASSERT_EQ(Value(list->data()[0]).toDouble(), -0.25); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_yposition"); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 1); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 0); + } +} + +TEST_F(MotionBlocksTest, Direction) +{ + { + auto sprite = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, sprite); + + builder.addBlock("motion_direction"); + builder.captureBlockReturnValue(); + builder.build(); + + sprite->setX(5.2); + sprite->setY(-0.25); + sprite->setDirection(-61.42); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 1); + ASSERT_EQ(Value(list->data()[0]).toDouble(), -61.42); + } + + m_engine->clear(); + + { + auto stage = std::make_shared(); + ScriptBuilder builder(m_extension.get(), m_engine, stage); + + builder.addBlock("motion_direction"); + builder.captureBlockReturnValue(); + + builder.build(); + builder.run(); + + List *list = builder.capturedValues(); + ASSERT_EQ(list->size(), 1); + ASSERT_EQ(Value(list->data()[0]).toDouble(), 90); + } +} diff --git a/test/mocks/stacktimermock.h b/test/mocks/stacktimermock.h index 9a614d3e..90de3ed9 100644 --- a/test/mocks/stacktimermock.h +++ b/test/mocks/stacktimermock.h @@ -13,4 +13,5 @@ class StackTimerMock : public IStackTimer MOCK_METHOD(bool, stopped, (), (const, override)); MOCK_METHOD(bool, elapsed, (), (const, override)); + MOCK_METHOD(double, elapsedTime, (), (const, override)); }; diff --git a/test/timer/stacktimer_test.cpp b/test/timer/stacktimer_test.cpp index 6d06da90..b8f61224 100644 --- a/test/timer/stacktimer_test.cpp +++ b/test/timer/stacktimer_test.cpp @@ -20,27 +20,32 @@ TEST(StackTimerTest, StartStopElapsed) EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2)); timer.start(0.5); - EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time2)); + EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(time2)); ASSERT_FALSE(timer.elapsed()); ASSERT_FALSE(timer.stopped()); + ASSERT_EQ(timer.elapsedTime(), 0); std::chrono::steady_clock::time_point time3(std::chrono::milliseconds(520)); - EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time3)); + EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(time3)); ASSERT_FALSE(timer.elapsed()); ASSERT_FALSE(timer.stopped()); + ASSERT_EQ(timer.elapsedTime(), 0.447); std::chrono::steady_clock::time_point time4(std::chrono::milliseconds(573)); - EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time4)); + EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(time4)); ASSERT_TRUE(timer.elapsed()); ASSERT_FALSE(timer.stopped()); + ASSERT_EQ(timer.elapsedTime(), 0.5); std::chrono::steady_clock::time_point time5(std::chrono::milliseconds(580)); - EXPECT_CALL(clock, currentSteadyTime()).WillOnce(Return(time5)); + EXPECT_CALL(clock, currentSteadyTime()).Times(2).WillRepeatedly(Return(time5)); ASSERT_TRUE(timer.elapsed()); ASSERT_FALSE(timer.stopped()); + ASSERT_EQ(timer.elapsedTime(), 0.507); timer.stop(); EXPECT_CALL(clock, currentSteadyTime).Times(0); ASSERT_FALSE(timer.elapsed()); ASSERT_TRUE(timer.stopped()); + ASSERT_EQ(timer.elapsedTime(), 0); }