Line data Source code
1 : /*
2 : * Copyright (C) 2025 aeml
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 :
18 : // FullDemoScenario — exercises every AtlasCore subsystem in a single scene.
19 : //
20 : // Systems / features shown:
21 : // ECS — CreateEntity, AddComponent, GetComponent, GetStorage,
22 : // ForEach<T>, View<T1,T2> (multi-component intersection)
23 : // Physics — EnvironmentForces (gravity + drag), PhysicsSettings
24 : // (substeps, constraintIterations), ConfigureCircleInertia,
25 : // ConfigureBoxInertia, restitution, friction, angularDrag
26 : // Bodies — static AABB floor/walls, static AABB platform,
27 : // dynamic AABB box tower, dynamic circle particles,
28 : // dynamic circle chain links + heavy pendulum ball
29 : // Joints — DistanceJointComponent rigid chain (compliance = 0)
30 : // Custom sys — WindGustSystem: custom ISystem, alternating horizontal
31 : // impulse every kWindPeriod seconds
32 : // JobSystem — m_jobs owned by scenario, passed to PhysicsSystem
33 : // ASCII — TextRenderer with all 8 Color values,
34 : // DrawRect / DrawLine / DrawCircle / DrawEllipse /
35 : // FillEllipse / Put
36 : // Logger — core::Logger for setup-time diagnostic messages
37 :
38 : #include "simlab/Scenario.hpp"
39 : #include "ecs/World.hpp"
40 : #include "physics/Components.hpp"
41 : #include "physics/Systems.hpp"
42 : #include "jobs/JobSystem.hpp"
43 : #include "ascii/TextRenderer.hpp"
44 : #include "core/Logger.hpp"
45 :
46 : #include <cmath>
47 : #include <cstring>
48 : #include <iostream>
49 : #include <random>
50 : #include <string>
51 : #include <vector>
52 :
53 : namespace simlab
54 : {
55 : // =========================================================================
56 : // WindGustSystem — custom ISystem
57 : //
58 : // Applies a brief, alternating horizontal impulse to every dynamic body
59 : // once every kWindPeriod seconds. Demonstrates:
60 : // - Subclassing ecs::ISystem
61 : // - Time accumulation inside a system Update hook
62 : // - world.ForEach<T> on RigidBodyComponent
63 : // =========================================================================
64 : namespace
65 : {
66 : class WindGustSystem : public ecs::ISystem
67 : {
68 : public:
69 540 : void Update(ecs::World& world, float dt) override
70 : {
71 540 : m_elapsed += dt;
72 540 : if (m_elapsed < kWindPeriod)
73 540 : return;
74 :
75 0 : m_elapsed = 0.0f;
76 0 : m_gustDir = -m_gustDir; // alternate direction
77 0 : const float impulse = m_gustDir * kWindImpulse;
78 :
79 0 : world.ForEach<physics::RigidBodyComponent>(
80 0 : [&](ecs::EntityId, physics::RigidBodyComponent& rb)
81 : {
82 0 : if (rb.invMass > 0.0f)
83 0 : rb.vx += impulse;
84 0 : });
85 : }
86 :
87 : private:
88 : float m_elapsed {0.0f};
89 : float m_gustDir {1.0f};
90 :
91 : static constexpr float kWindPeriod {6.0f}; // seconds between gusts
92 : static constexpr float kWindImpulse{3.0f}; // m/s applied per gust
93 : };
94 : } // anonymous namespace
95 :
96 :
97 : // =========================================================================
98 : // FullDemoScenario
99 : // =========================================================================
100 : class FullDemoScenario : public IScenario
101 : {
102 : public:
103 :
104 : // ---------------------------------------------------------------------
105 : // Setup
106 : // ---------------------------------------------------------------------
107 3 : void Setup(ecs::World& world) override
108 : {
109 3 : m_renderer = std::make_unique<ascii::TextRenderer>(kW, kH);
110 :
111 : // ---- Logger (instance-based) ------------------------------------
112 3 : core::Logger log;
113 3 : log.Info("FullDemoScenario: initialising");
114 :
115 : // ---- Physics system + JobSystem + PhysicsSettings ---------------
116 3 : physics::EnvironmentForces env;
117 3 : env.gravityY = -9.81f;
118 3 : env.drag = 0.02f; // air resistance — damps pendulum to rest
119 :
120 3 : auto phys = std::make_unique<physics::PhysicsSystem>();
121 3 : physics::PhysicsSettings cfg;
122 3 : cfg.substeps = 16; // high stability
123 3 : cfg.constraintIterations = 16; // stable rigid chain
124 3 : cfg.positionIterations = 20;
125 3 : phys->SetSettings(cfg);
126 3 : phys->SetEnvironment(env);
127 3 : phys->SetJobSystem(&m_jobs); // pass owned JobSystem
128 3 : world.AddSystem(std::move(phys));
129 :
130 : // ---- Custom system (added after physics so it runs each step) ---
131 3 : world.AddSystem(std::make_unique<WindGustSystem>());
132 :
133 : // ---- Static arena walls (thick to prevent tunnelling) -----------
134 : // Inner bounds: X ∈ [kLeftX, kRightX], Y ∈ [kFloorY, kArenaTop]
135 3 : MakeWall(world, 0.0f, kFloorY - 50.0f, 80.0f, 100.0f); // floor
136 3 : MakeWall(world, kLeftX - 50.0f, 0.0f, 100.0f, 40.0f); // left
137 3 : MakeWall(world, kRightX + 50.0f, 0.0f, 100.0f, 40.0f); // right
138 :
139 : // ---- Tower platform (right of centre, surface Y = 0) -----------
140 : // Raised shelf for the box tower so the pendulum can reach it.
141 3 : MakeWall(world, 2.0f, -1.0f, 8.0f, 2.0f);
142 :
143 : // ---- Pendulum chain + heavy wrecking ball -----------------------
144 : // Anchor at (kAnchorX, kAnchorY) — static, attached to ceiling.
145 : // kChainCount links placed at 45° SW so the chain falls clockwise
146 : // (rightward) into the box tower on the right side.
147 : // Each inter-link step is (−1.41, −1.41): dist = √(1.41²+1.41²) ≈ 2.0
148 : // which matches kLinkDist, so the chain starts nearly taut.
149 3 : auto anchor = world.CreateEntity();
150 3 : world.AddComponent<physics::TransformComponent>(anchor, kAnchorX, kAnchorY, 0.0f);
151 : {
152 3 : auto& ab = world.AddComponent<physics::RigidBodyComponent>(anchor);
153 3 : ab.mass = 0.0f; ab.invMass = 0.0f;
154 : }
155 3 : m_anchorId = anchor;
156 :
157 3 : ecs::EntityId prev = anchor;
158 15 : for (int i = 0; i < kChainCount; ++i)
159 : {
160 12 : const float lx = kAnchorX - static_cast<float>(i + 1) * 1.41f;
161 12 : const float ly = kAnchorY - static_cast<float>(i + 1) * 1.41f;
162 :
163 12 : auto link = world.CreateEntity();
164 12 : world.AddComponent<physics::TransformComponent>(link, lx, ly, 0.0f);
165 12 : auto& lb = world.AddComponent<physics::RigidBodyComponent>(link);
166 :
167 12 : const bool isBall = (i == kChainCount - 1);
168 12 : if (isBall)
169 : {
170 : // Heavy wrecking ball — large mass, circle collider
171 3 : lb.mass = 25.0f;
172 3 : lb.invMass = 1.0f / 25.0f;
173 3 : lb.restitution = 0.4f;
174 3 : lb.friction = 0.3f;
175 3 : lb.angularDrag = 0.15f;
176 3 : world.AddComponent<physics::CircleColliderComponent>(link, kBallR);
177 3 : physics::ConfigureCircleInertia(lb, kBallR);
178 3 : m_ballId = link;
179 : }
180 : else
181 : {
182 : // Small chain link
183 9 : lb.mass = 0.6f;
184 9 : lb.invMass = 1.0f / 0.6f;
185 9 : lb.restitution = 0.1f;
186 9 : world.AddComponent<physics::CircleColliderComponent>(link, 0.22f);
187 9 : physics::ConfigureCircleInertia(lb, 0.22f);
188 : }
189 :
190 : // Distance joint connecting this link to the previous one
191 12 : auto& jc = world.AddComponent<physics::DistanceJointComponent>(link);
192 12 : jc.entityA = prev;
193 12 : jc.entityB = link;
194 12 : jc.targetDistance = kLinkDist;
195 12 : jc.compliance = 0.0f; // fully rigid
196 :
197 12 : m_chainIds.push_back(link);
198 12 : prev = link;
199 : }
200 :
201 : // ---- Tower of dynamic AABB boxes (on raised platform) -----------
202 : // kTowerCols columns × kTowerRows rows, sitting on the tower platform.
203 3 : log.Info("FullDemoScenario: building box tower");
204 12 : for (int row = 0; row < kTowerRows; ++row)
205 : {
206 45 : for (int col = 0; col < kTowerCols; ++col)
207 : {
208 36 : const float bx = kTowerLeft + (static_cast<float>(col) + 0.5f) * kBoxSize;
209 36 : const float by = kTowerBaseY + (static_cast<float>(row) + 0.5f) * kBoxSize;
210 :
211 36 : auto box = world.CreateEntity();
212 36 : world.AddComponent<physics::TransformComponent>(box, bx, by, 0.0f);
213 36 : auto& bb = world.AddComponent<physics::RigidBodyComponent>(box);
214 36 : bb.mass = 1.5f;
215 36 : bb.invMass = 1.0f / 1.5f;
216 36 : bb.friction = 0.7f;
217 36 : bb.restitution = 0.1f;
218 36 : bb.angularDrag = 0.15f;
219 36 : world.AddComponent<physics::AABBComponent>(
220 : box,
221 0 : bx - kBoxSize * 0.5f, by - kBoxSize * 0.5f,
222 36 : bx + kBoxSize * 0.5f, by + kBoxSize * 0.5f);
223 36 : physics::ConfigureBoxInertia(bb, kBoxSize, kBoxSize);
224 36 : m_boxIds.push_back(box);
225 : }
226 : }
227 :
228 : // ---- Bouncing circle particles (left half of arena) ------------
229 : // Randomised starting positions and upward velocities.
230 : // Demonstrates high-entity-count circle collisions.
231 3 : log.Info("FullDemoScenario: spawning particles");
232 3 : std::mt19937 rng{2025u};
233 3 : std::uniform_real_distribution<float> rndX{kLeftX + 1.0f, -2.0f};
234 3 : std::uniform_real_distribution<float> rndY{kFloorY + 0.5f, 3.0f};
235 3 : std::uniform_real_distribution<float> rndVx{-4.0f, 4.0f};
236 3 : std::uniform_real_distribution<float> rndVy{ 2.0f, 10.0f};
237 :
238 93 : for (int i = 0; i < kParticles; ++i)
239 : {
240 90 : const float px = rndX(rng), py = rndY(rng);
241 90 : auto p = world.CreateEntity();
242 90 : world.AddComponent<physics::TransformComponent>(p, px, py, 0.0f);
243 90 : auto& pb = world.AddComponent<physics::RigidBodyComponent>(p);
244 90 : pb.mass = 0.12f;
245 90 : pb.invMass = 1.0f / 0.12f;
246 90 : pb.restitution = 0.80f;
247 90 : pb.friction = 0.05f;
248 90 : pb.vx = rndVx(rng);
249 90 : pb.vy = rndVy(rng);
250 90 : world.AddComponent<physics::CircleColliderComponent>(p, 0.30f);
251 90 : physics::ConfigureCircleInertia(pb, 0.30f);
252 90 : m_partIds.push_back(p);
253 : }
254 :
255 : // ---- GetStorage demo — log total RigidBody count after setup ----
256 3 : if (auto* rbs = world.GetStorage<physics::RigidBodyComponent>())
257 : {
258 3 : log.Info("FullDemoScenario: total RigidBody entities = "
259 6 : + std::to_string(rbs->Size()));
260 : }
261 3 : }
262 :
263 : // ---------------------------------------------------------------------
264 : // Update — scenario logic hook only.
265 : // The engine owns world.Update(); calling it here would violate the
266 : // IScenario contract tested by scenario_update_contract_tests.
267 : // ---------------------------------------------------------------------
268 540 : void Update(ecs::World& /*world*/, float /*dt*/) override {}
269 :
270 : // ---------------------------------------------------------------------
271 : // Render — full ASCII showcase using every drawing primitive and all
272 : // eight Color values.
273 : // ---------------------------------------------------------------------
274 180 : void Render(ecs::World& world, std::ostream& out) override
275 : {
276 180 : m_renderer->Clear();
277 :
278 : // ---- Arena border: DrawRect (White '+') -------------------------
279 : // Maps inner bounds [kLeftX..kRightX] × [kFloorY..kArenaTop]
280 : // to screen rectangle.
281 180 : const int bx0 = toSX(kLeftX), by0 = toSY(kArenaTop);
282 180 : const int bx1 = toSX(kRightX), by1 = toSY(kFloorY);
283 180 : m_renderer->DrawRect(bx0, by0, bx1 - bx0, by1 - by0,
284 : '+', ascii::Color::White);
285 :
286 : // ---- Tower platform: DrawLine (Yellow '=') ---------------------
287 180 : m_renderer->DrawLine(toSX(-2.0f), toSY(0.0f),
288 : toSX( 6.0f), toSY(0.0f),
289 : '=', ascii::Color::Yellow);
290 :
291 : // ---- Tower boxes: View<TransformComponent, AABBComponent> ------
292 : // Uses multi-component View to select only entities that have
293 : // both a transform and an AABB. Static bodies (invMass==0) are
294 : // skipped so only the dynamic tower blocks are drawn.
295 180 : world.View<physics::TransformComponent, physics::AABBComponent>(
296 180 : [&](ecs::EntityId id,
297 : physics::TransformComponent& t,
298 : physics::AABBComponent& /*aabb*/)
299 : {
300 2880 : auto* rb = world.GetComponent<physics::RigidBodyComponent>(id);
301 2880 : if (!rb || rb->invMass == 0.0f) return;
302 2160 : const int sx = toSX(t.x), sy = toSY(t.y);
303 2160 : if (inBounds(sx, sy))
304 2057 : m_renderer->Put(sx, sy, '#', ascii::Color::Cyan);
305 : });
306 :
307 : // ---- Particles: iterate tracked IDs (Green '.') ----------------
308 5580 : for (ecs::EntityId pid : m_partIds)
309 : {
310 5400 : if (auto* t = world.GetComponent<physics::TransformComponent>(pid))
311 : {
312 5400 : const int sx = toSX(t->x), sy = toSY(t->y);
313 5400 : if (inBounds(sx, sy))
314 4789 : m_renderer->Put(sx, sy, '.', ascii::Color::Green);
315 : }
316 : }
317 :
318 : // ---- Chain segments: DrawLine (Yellow '-') ---------------------
319 : // Draws a line between each consecutive pair of link transforms
320 : // starting from the anchor.
321 : {
322 180 : int px = toSX(kAnchorX), py = toSY(kAnchorY);
323 900 : for (ecs::EntityId cid : m_chainIds)
324 : {
325 720 : if (auto* t = world.GetComponent<physics::TransformComponent>(cid))
326 : {
327 720 : const int nx = toSX(t->x), ny = toSY(t->y);
328 720 : m_renderer->DrawLine(px, py, nx, ny, '-', ascii::Color::Yellow);
329 720 : px = nx; py = ny;
330 : }
331 : }
332 : }
333 :
334 : // ---- Wrecking ball: DrawEllipse outer ring + FillEllipse body --
335 : // DrawEllipse (Red ':') draws the impact halo one cell larger than
336 : // the filled body, giving the ball a distinct outline.
337 180 : if (auto* bt = world.GetComponent<physics::TransformComponent>(m_ballId))
338 : {
339 180 : const int bsx = toSX(bt->x), bsy = toSY(bt->y);
340 180 : const int rx = static_cast<int>(kBallR * kSX); // = 3
341 180 : const int ry = static_cast<int>(kBallR * kSY); // = 3
342 180 : m_renderer->DrawEllipse(bsx, bsy, rx + 1, ry + 1, ':', ascii::Color::Red);
343 180 : m_renderer->FillEllipse (bsx, bsy, rx, ry, 'O', ascii::Color::Red);
344 : }
345 :
346 : // ---- Anchor: DrawCircle pivot ring (Blue) + Put 'X' (Magenta) --
347 : // DrawCircle draws a small pivot indicator around the anchor point.
348 180 : const int asx = toSX(kAnchorX), asy = toSY(kAnchorY);
349 180 : m_renderer->DrawCircle(asx, asy, 2, 'o', ascii::Color::Blue);
350 180 : m_renderer->Put(asx, asy, 'X', ascii::Color::Magenta);
351 :
352 : // ---- Title bar (Magenta, row 0) ---------------------------------
353 : static constexpr const char* kTitle = " ATLASCORE FULL DEMO ";
354 180 : const int tx = (kW - static_cast<int>(std::strlen(kTitle))) / 2;
355 3960 : for (int i = 0; kTitle[i] != '\0'; ++i)
356 3780 : m_renderer->Put(tx + i, 0, kTitle[i], ascii::Color::Magenta);
357 :
358 : // ---- Feature legend (White, bottom two rows) -------------------
359 : static constexpr const char* kLegend[2] =
360 : {
361 : "ECS|JOBS|PHYSICS|JOINTS",
362 : "ASCII|CUSTOM-SYS|ALL-8-CLR",
363 : };
364 540 : for (int r = 0; r < 2; ++r)
365 : {
366 360 : const int lx = kW - static_cast<int>(std::strlen(kLegend[r])) - 1;
367 360 : const int ly = kH - 2 + r;
368 9180 : for (int i = 0; kLegend[r][i] != '\0'; ++i)
369 8820 : m_renderer->Put(lx + i, ly, kLegend[r][i], ascii::Color::White);
370 : }
371 :
372 180 : m_renderer->SetHeadless(simlab::IsHeadlessRendering());
373 180 : m_renderer->PresentDiff(out);
374 180 : }
375 :
376 : private:
377 :
378 : // ---- World / screen constants ---------------------------------------
379 : static constexpr float kFloorY = -9.5f;
380 : static constexpr float kLeftX = -19.0f;
381 : static constexpr float kRightX = 19.0f;
382 : static constexpr float kArenaTop = 9.5f;
383 :
384 : static constexpr int kW = 80;
385 : static constexpr int kH = 40;
386 : static constexpr float kSX = 2.0f; // screen units per world unit (X)
387 : static constexpr float kSY = 2.0f; // screen units per world unit (Y)
388 :
389 : // ---- Chain / ball ---------------------------------------------------
390 : static constexpr float kAnchorX = -5.0f;
391 : static constexpr float kAnchorY = 9.0f;
392 : static constexpr float kLinkDist = 2.0f;
393 : static constexpr float kBallR = 1.5f;
394 : static constexpr int kChainCount = 4; // 3 small links + 1 ball
395 :
396 : // ---- Tower ----------------------------------------------------------
397 : static constexpr float kBoxSize = 1.4f;
398 : static constexpr float kTowerLeft = -1.0f;
399 : static constexpr float kTowerBaseY = 0.0f; // top of tower platform
400 : static constexpr int kTowerCols = 4;
401 : static constexpr int kTowerRows = 3;
402 :
403 : // ---- Particles ------------------------------------------------------
404 : static constexpr int kParticles = 30;
405 :
406 : // ---- Coordinate mapping helpers ------------------------------------
407 : // World X ∈ [−20, 20] → screen X ∈ [0, 80]
408 : // World Y ∈ [−10, 10] → screen Y ∈ [40, 0] (Y-axis flipped)
409 9540 : int toSX(float wx) const
410 : {
411 9540 : return static_cast<int>((wx + 20.0f) * kSX);
412 : }
413 9540 : int toSY(float wy) const
414 : {
415 9540 : return static_cast<int>((10.0f - wy) * kSY);
416 : }
417 7560 : bool inBounds(int sx, int sy) const
418 : {
419 7560 : return sx >= 0 && sx < kW && sy >= 0 && sy < kH;
420 : }
421 :
422 : // ---- Entity factory: static thick-walled AABB ----------------------
423 12 : static void MakeWall(ecs::World& world,
424 : float cx, float cy, float w, float h)
425 : {
426 12 : auto e = world.CreateEntity();
427 12 : world.AddComponent<physics::TransformComponent>(e, cx, cy, 0.0f);
428 12 : auto& b = world.AddComponent<physics::RigidBodyComponent>(e);
429 12 : b.mass = 0.0f; b.invMass = 0.0f;
430 12 : world.AddComponent<physics::AABBComponent>(
431 : e,
432 0 : cx - w * 0.5f, cy - h * 0.5f,
433 12 : cx + w * 0.5f, cy + h * 0.5f);
434 12 : }
435 :
436 : // ---- Owned resources -----------------------------------------------
437 : std::unique_ptr<ascii::TextRenderer> m_renderer;
438 : jobs::JobSystem m_jobs;
439 :
440 : // ---- Entity handles ------------------------------------------------
441 : ecs::EntityId m_anchorId{0};
442 : ecs::EntityId m_ballId {0};
443 : std::vector<ecs::EntityId> m_chainIds;
444 : std::vector<ecs::EntityId> m_boxIds;
445 : std::vector<ecs::EntityId> m_partIds;
446 : };
447 :
448 : // -------------------------------------------------------------------------
449 : // Factory function — registered in ScenarioRegistry
450 : // -------------------------------------------------------------------------
451 4 : std::unique_ptr<IScenario> CreateFullDemoScenario()
452 : {
453 4 : return std::make_unique<FullDemoScenario>();
454 : }
455 :
456 : } // namespace simlab
|