Skip to content

Commit 25c7014

Browse files
committed
Implement motion_ifonedgebounce block
1 parent e5bba37 commit 25c7014

File tree

3 files changed

+186
-0
lines changed

3 files changed

+186
-0
lines changed

src/blocks/motionblocks.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ void MotionBlocks::registerBlocks(IEngine *engine)
5151
engine->addCompileFunction(this, "motion_setx", &compileSetX);
5252
engine->addCompileFunction(this, "motion_changeyby", &compileChangeYBy);
5353
engine->addCompileFunction(this, "motion_sety", &compileSetY);
54+
engine->addCompileFunction(this, "motion_ifonedgebounce", &compileIfOnEdgeBounce);
5455
}
5556

5657
CompilerValue *MotionBlocks::compileMoveSteps(Compiler *compiler)
@@ -290,6 +291,14 @@ CompilerValue *MotionBlocks::compileSetY(Compiler *compiler)
290291
return nullptr;
291292
}
292293

294+
CompilerValue *MotionBlocks::compileIfOnEdgeBounce(Compiler *compiler)
295+
{
296+
if (!compiler->target()->isStage())
297+
compiler->addTargetFunctionCall("motion_ifonedgebounce");
298+
299+
return nullptr;
300+
}
301+
293302
extern "C" void motion_movesteps(Sprite *sprite, double steps)
294303
{
295304
double dir = sprite->direction();
@@ -574,6 +583,90 @@ extern "C" void motion_sety(Sprite *sprite, double y)
574583
sprite->setY(y);
575584
}
576585

586+
extern "C" void motion_ifonedgebounce(Sprite *sprite)
587+
{
588+
// https://github.com/scratchfoundation/scratch-vm/blob/c37745e97e6d8a77ad1dc31a943ea728dd17ba78/src/blocks/scratch3_motion.js#L186-L240
589+
IEngine *engine = sprite->engine();
590+
Rect bounds = sprite->boundingRect();
591+
592+
// Measure distance to edges
593+
// Values are zero when the sprite is beyond
594+
double stageWidth = engine->stageWidth();
595+
double stageHeight = engine->stageHeight();
596+
double distLeft = std::max(0.0, (stageWidth / 2.0) + bounds.left());
597+
double distTop = std::max(0.0, (stageHeight / 2.0) - bounds.top());
598+
double distRight = std::max(0.0, (stageWidth / 2.0) - bounds.right());
599+
double distBottom = std::max(0.0, (stageHeight / 2.0) + bounds.bottom());
600+
601+
// Find the nearest edge
602+
// 1 - left
603+
// 2 - top
604+
// 3 - right
605+
// 4 - bottom
606+
unsigned short nearestEdge = 0;
607+
double minDist = std::numeric_limits<double>::infinity();
608+
609+
if (distLeft < minDist) {
610+
minDist = distLeft;
611+
nearestEdge = 1;
612+
}
613+
614+
if (distTop < minDist) {
615+
minDist = distTop;
616+
nearestEdge = 2;
617+
}
618+
619+
if (distRight < minDist) {
620+
minDist = distRight;
621+
nearestEdge = 3;
622+
}
623+
624+
if (distBottom < minDist) {
625+
minDist = distBottom;
626+
nearestEdge = 4;
627+
}
628+
629+
if (minDist > 0)
630+
return; // Not touching any edge
631+
632+
assert(nearestEdge != 0);
633+
634+
// Point away from the nearest edge
635+
double radians = (90 - sprite->direction()) * pi / 180;
636+
double dx = std::cos(radians);
637+
double dy = -std::sin(radians);
638+
639+
switch (nearestEdge) {
640+
case 1:
641+
// Left
642+
dx = std::max(0.2, std::abs(dx));
643+
break;
644+
645+
case 2:
646+
// Top
647+
dy = std::max(0.2, std::abs(dy));
648+
break;
649+
650+
case 3:
651+
// Right
652+
dx = 0 - std::max(0.2, std::abs(dx));
653+
break;
654+
655+
case 4:
656+
// Bottom
657+
dy = 0 - std::max(0.2, std::abs(dy));
658+
break;
659+
}
660+
661+
double newDirection = (180 / pi) * (std::atan2(dy, dx)) + 90;
662+
sprite->setDirection(newDirection);
663+
664+
// Keep within the stage
665+
double fencedX, fencedY;
666+
sprite->keepInFence(sprite->x(), sprite->y(), &fencedX, &fencedY);
667+
sprite->setPosition(fencedX, fencedY);
668+
}
669+
577670
extern "C" double motion_xposition(Sprite *sprite)
578671
{
579672
return sprite->x();

src/blocks/motionblocks.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class MotionBlocks : public IExtension
3030
static CompilerValue *compileSetX(Compiler *compiler);
3131
static CompilerValue *compileChangeYBy(Compiler *compiler);
3232
static CompilerValue *compileSetY(Compiler *compiler);
33+
static CompilerValue *compileIfOnEdgeBounce(Compiler *compiler);
3334
};
3435

3536
} // namespace libscratchcpp

test/blocks/motion_blocks_test.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <enginemock.h>
1111
#include <randomgeneratormock.h>
1212
#include <stacktimermock.h>
13+
#include <spritehandlermock.h>
1314

1415
#include "../common.h"
1516
#include "blocks/motionblocks.h"
@@ -1730,3 +1731,94 @@ TEST_F(MotionBlocksTest, SetY)
17301731
builder.run();
17311732
}
17321733
}
1734+
1735+
TEST_F(MotionBlocksTest, IfOnEdgeBounce)
1736+
{
1737+
{
1738+
auto sprite = std::make_shared<Sprite>();
1739+
SpriteHandlerMock handler;
1740+
sprite->setInterface(&handler);
1741+
sprite->setEngine(&m_engineMock);
1742+
1743+
ScriptBuilder builder(m_extension.get(), m_engine, sprite);
1744+
1745+
builder.addBlock("motion_ifonedgebounce");
1746+
auto block = builder.currentBlock();
1747+
1748+
Compiler compiler(&m_engineMock, sprite.get());
1749+
auto code = compiler.compile(block);
1750+
Script script(sprite.get(), block, &m_engineMock);
1751+
script.setCode(code);
1752+
Thread thread(sprite.get(), &m_engineMock, &script);
1753+
1754+
EXPECT_CALL(m_engineMock, stageWidth()).WillRepeatedly(Return(480));
1755+
EXPECT_CALL(m_engineMock, stageHeight()).WillRepeatedly(Return(360));
1756+
EXPECT_CALL(m_engineMock, spriteFencingEnabled()).WillRepeatedly(Return(false));
1757+
1758+
// No edge
1759+
EXPECT_CALL(handler, boundingRect()).WillOnce(Return(Rect(80, 80, 120, 40)));
1760+
sprite->setX(100);
1761+
sprite->setY(60);
1762+
sprite->setDirection(-45);
1763+
thread.run();
1764+
1765+
ASSERT_EQ(sprite->x(), 100);
1766+
ASSERT_EQ(sprite->y(), 60);
1767+
ASSERT_EQ(sprite->direction(), -45);
1768+
1769+
// Left edge
1770+
EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(-260, 80, -220, 40)));
1771+
sprite->setX(-240);
1772+
sprite->setY(60);
1773+
thread.reset();
1774+
thread.run();
1775+
1776+
ASSERT_EQ(sprite->x(), -220);
1777+
ASSERT_EQ(sprite->y(), 60);
1778+
ASSERT_EQ(std::round(sprite->direction() * 100) / 100, 45);
1779+
1780+
// Top edge
1781+
EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(80, 200, 120, 160)));
1782+
sprite->setX(100);
1783+
sprite->setY(180);
1784+
sprite->setDirection(45);
1785+
thread.reset();
1786+
thread.run();
1787+
1788+
ASSERT_EQ(sprite->x(), 100);
1789+
ASSERT_EQ(sprite->y(), 160);
1790+
ASSERT_EQ(sprite->direction(), 135);
1791+
1792+
// Right edge
1793+
EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(220, 80, 260, 40)));
1794+
sprite->setX(240);
1795+
sprite->setY(60);
1796+
thread.reset();
1797+
thread.run();
1798+
1799+
ASSERT_EQ(sprite->x(), 220);
1800+
ASSERT_EQ(sprite->y(), 60);
1801+
ASSERT_EQ(sprite->direction(), -135);
1802+
1803+
// Bottom edge
1804+
EXPECT_CALL(handler, boundingRect()).Times(2).WillRepeatedly(Return(Rect(-120, -160, -80, -200)));
1805+
sprite->setX(-100);
1806+
sprite->setY(-180);
1807+
thread.reset();
1808+
thread.run();
1809+
1810+
ASSERT_EQ(sprite->x(), -100);
1811+
ASSERT_EQ(sprite->y(), -160);
1812+
ASSERT_EQ(std::round(sprite->direction() * 100) / 100, -45);
1813+
}
1814+
1815+
{
1816+
auto stage = std::make_shared<Stage>();
1817+
ScriptBuilder builder(m_extension.get(), m_engine, stage);
1818+
1819+
builder.addBlock("motion_ifonedgebounce");
1820+
1821+
builder.build();
1822+
builder.run();
1823+
}
1824+
}

0 commit comments

Comments
 (0)