Преглед изворни кода

Merge pinball0 from https://github.com/rdefeo/pinball0

Willy-JL пре 1 година
родитељ
комит
fa87c2d6a6

+ 10 - 0
pinball0/CHANGELOG.md

@@ -1,3 +1,13 @@
+## 0.5.1
+
+- Stop scores when moving ball in debug mode
+- Reset dynamic objects during tilt
+
+## 0.5.0
+
+- Turbo boosts
+- Signals for improved gameplay
+
 ## 0.4.0
 
 - Table Tilt!

+ 57 - 12
pinball0/README.md

@@ -1,13 +1,14 @@
 # Pinball0 (Pinball Zero)
 Play pinball on your Flipperzero!
 
-Still a work in progress...
-
-[Latest version v0.4](https://github.com/rdefeo/pinball0/releases)
+Get the latest version:
+* [Flipper Lab](https://lab.flipper.net/apps/pinball0)
+* [Latest builds](https://github.com/rdefeo/pinball0/releases) - manual install of .fap
+* Or build yourself with [ufbt](https://github.com/flipperdevices/flipperzero-ufbt)
 
 ![build status badge](https://github.com/rdefeo/pinball0/actions/workflows/build.yml/badge.svg)
 
-> The default tables and example tables may / will change
+> Note: The default tables and example tables may / will change
 
 ## Screenshots
 
@@ -23,16 +24,16 @@ Still a work in progress...
 * Scores! (no high scores yet, just a running tally as you play)
 * Table bumps! (Don't tilt the table!)
 * Portals!
-* Rollover items
+* Rollover items, Turbo boosts
 * Sounds! Blinky lights! Annoying vibrations!
 * Customizable notification settings: sound, LED, vibration
-* Idle timeout to save battery - will exit after ~120 seconds of no key-presses
+* Idle timeout to save battery - will exit after ~2 minutes of no key-presses on menu screen
 
 ## Controls
 * **Ok** to release the ball
 * **Left** and **Right** to activate the left and right flippers
 * **Back** to return to the main menu or exit
-* **Up** to "bump" the table if the ball gets stuck
+* **Up** to "bump" the table if the ball gets stuck. Table bumps are limited to 1 per second. if a table has `tilt_detect` enabled (default is `true`) then your 3rd table bump will tilt the machine and you'll lose the current ball!
 
 I find it easiest to hold the flipper with both hands so I can hit left/right with my thumbs!
 
@@ -42,9 +43,7 @@ The **SETTINGS** menu will be the "last" table listed. You can Enable / Disable
 **Debug** mode allows you to move the ball using the directional pad _before_ the ball is launched. This is useful for testing and may be removed in the future. (May result in unexpected behavior.) It also displays test tables on the main menu. The test tables will only show/hide after you exit and restart the app. This feature is mainly for me - lol.
 
 ## Tables
-Pinball0 ships with several default tables. These tables are automatically deployed into the assets folder (`/apps_data/pinball0`) on your SD card. Tables are simple JSON which means you can define your own! Your tables should be stored in the data folder (`/apps_data/pinball0`). On the main menu, tables are sorted alphabetically. In order to "force" a sorting order, you can prepend any filename with `NN_` where `NN` is between `00` and `99`. When the files are displayed on the menu, if they start with `NN_`, that will be stripped - but their sorted order will be preserved.
-
-> The default tables may change over time.
+Pinball0 ships with several default tables. These tables are automatically deployed into the assets folder (`/apps_assets/pinball0/tables`) on your SD card. Tables are simple JSON which means you can define your own! Your tables should be stored in the data folder (`/apps_data/pinball0/tables`). On the main menu, tables are sorted alphabetically. In order to "force" a sorting order, you can prepend any filename with `NN_` where `NN` is between `00` and `99`. When the files are displayed on the menu, if they start with `NN_`, that will be stripped - but their sorted order will be preserved.
 
 In **Debug** mode, test tables will be shown. A test table is one that begins with the text `dbg`. Given that you can prefix table names for sorting purposes, here are two valid table filenames for a test table called `my FLIPS`: `dbg my FLIPS.json` and `04_dbg my FLIPS.json`. In both cases it will be displayed as `dbg my FLIPS` on the menu. I doubt that you will use this feature, but I'm documenting it anyway.
 
@@ -52,9 +51,9 @@ In **Debug** mode, test tables will be shown. A test table is one that begins wi
 ### File Format
 Table units are specified at a 10x scale. This means our table is **630 x 1270** in size (as the F0 display is 64 pixels x 128 pixels). Our origin is in the top-left at 0, 0. Check out the default tables in the `assets/tables` folder for example usage.
 
-These JSON elements are all defined at the top-level. The JSON can include comments - because why not!
+> There is some basic error checking when reading / parsing the table files. If the error is serious enough, you will see an error message in the app. Otherwise, check the console logs. For those familiar with `ufbt`, simply run `ufbt cli` and issue the `log` command. Then launch Pinball0. All informational and higher logs will be displayed. These logs are useful when reporting bugs/issues!
 
-> **DISCLAIMER:** The file format may change from release to release. Sorry. There is some basic error checking when reading / parsing the table files. If the error is serious enough, you will see an error message in the app. Otherwise, check the console logs. For those familiar with `ufbt`, simply run `ufbt cli` and issue the `log` command. Then launch Pinball0. All informational and higher logs will be displayed. These logs are useful when reporting bugs/issues!
+These JSON elements are all defined at the top-level. The JSON can include comments - because why not!
 
 #### lives : object (optional)
 Defines how many lives/balls you start with, and display information
@@ -118,6 +117,52 @@ When the ball passes over/through a rollover object, the symbol will appear. Onl
 
 Defines two portals, **a** and **b**. They are bi-drectional. Like rails, their "surface" - or in this case, their "entry surface" - is "on the left" from their respective start to end direction. You can't "enter" a portal from it's reverse side, you will pass through.
 
+#### turbos : list of objects (optional)
+* `"position": [ X, Y ]`
+* `"angle": N` : in degrees from 0 to 360
+* `"boost": N` : optional, defaults to `5`
+* `"radius" : N` : optional, defaults to `20`
+
+When the ball hits a turbo boost, it will force the new velocity to be in the direction of `angle`.
+
+#### signal : object attribute (optional)
+* `"tx": N` : id to transmit
+* `"rx": N` : id to receive
+* `"any": bool` : signal trigger type, defaults to `false` (i.e. "all")
+
+Can be added to a **bumper** or **rollover** to send notifications of collisions. When defining a signal, you can define a `tx` or a `rx` or both. The id value can be any number. If you add a `"tx": 4` to a rollover, when the ball collides with the rollover, it will trigger a signal to any object that has a `"rx": 4` defined.
+
+The `"any"` bool will determine __when__ to send the signal. Let's look at an example:
+
+```json
+"rollovers": [
+    {
+        "position": [ 100, 100 ],
+        "symbol" : "A",
+        "signal": { "tx": 3 }
+    },
+    {
+        "position": [ 150, 100 ],
+        "symbol" : "B",
+        "signal": { "tx": 3 }
+    }
+]
+```
+
+Since we didn't specify `"any": true` within the `signal` objects for each rollover, then ALL rollovers must be hit by the ball before a signal for id 3 is "sent". If we specifyed `"any": true`, then the signal with id 3 will be sent when the ball hits any of the rollovers.
+
+All signals with a `tx` must have a corresponding `rx` with the same id on the table - and vice versa - otherwise an error will be thrown.
+
+All signals with the same `tx` id must have the same trigger type (i.e. any or all).
+
+The default behavior for objects when a signal is received / sent:
+* **rollover**
+  * received - rollover reset to non-activated (i.e. symbol is hidden)
+  * sent - nothing
+* **bumper**
+  * received - becomes visible and can be hit by ball
+  * sent - disappears
+
 #### tilt_detect : boolean
 * `"tilt_detect": bool` : optional, defaults to `true`
 

+ 3 - 3
pinball0/README_flipperlab.md

@@ -5,9 +5,9 @@ Play pinball on your Flipperzero!
 * Realistic physics and collisions
 * User-defined tables via JSON files
 * Bumpers, flat surfaces, curved surfaces
-* Table bumps
+* Table bumps (Don't tilt the table!)
 * Portals!
-* Rollover items
+* Rollover items, Turbo boosts
 * Sounds! Blinky lights! Annoying vibrations!
 * Customizable notification settings: sound, LED, vibration
 * Idle timeout
@@ -21,7 +21,7 @@ Play pinball on your Flipperzero!
 I find it easiest to hold the flipper with both hands so I can hit left/right with my thumbs!
 
 ## Tables
-Pinball0 ships with several default tables. These tables are automatically deployed into the assets folder (/apps_data/pinball0). Tables are simple JSON which means you can define your own! Your tables should be stored in the data folder (/apps_data/pinball0). 
+Pinball0 ships with several default tables. These tables are automatically deployed into the assets folder (/apps_assets/pinball0/tables). Tables are simple JSON which means you can define your own! Your tables should be stored in the data folder (/apps_data/pinball0/tables). 
 
 View the github repo for the JSON format specification: https://github.com/rdefeo/pinball0
 

+ 1 - 1
pinball0/application.fam

@@ -9,7 +9,7 @@ App(
     fap_category="Games",
     requires=["gui"],
     # Optional values
-    fap_version="0.4",
+    fap_version="0.5.1",
     fap_icon="pinball0.png",  # 10x10 1-bit PNG
     fap_description="Pinball game",
     fap_author="Roberto De Feo",

+ 54 - 4
pinball0/assets/tables/02_Classic.json

@@ -43,6 +43,40 @@
         {
             "position": [ 480, 500 ],
             "radius": 40
+        },
+        // gutter guards - for Z,E,R,O rollover
+        {
+            "position": [ 310, 1265 ],
+            "radius": 20,
+            "physical": false,
+            "hidden": true,
+            "signal": {
+                "tx": 1,
+                "rx": 0,
+                "any": true
+            }
+        },
+        {
+            "position": [ 35, 900 ],
+            "radius": 20,
+            "physical": false,
+            "hidden": true,
+            "signal": {
+                "tx": 1,
+                "rx": 0,
+                "any": true
+            }
+        },
+        {
+            "position": [ 595, 900 ],
+            "radius": 20,
+            "physical": false,
+            "hidden": true,
+            "signal": {
+                "tx": 1,
+                "rx": 0,
+                "any": true
+            }
         }
     ],
     "arcs": [
@@ -125,19 +159,35 @@
     "rollovers": [
         {
             "position": [ 200, 800 ],
-            "symbol": "Z"
+            "symbol": "Z",
+            "signal": {
+                "tx": 0,
+                "rx": 1
+            }
         },
         {
             "position": [ 280, 770 ],
-            "symbol": "E"
+            "symbol": "E",
+            "signal": {
+                "tx": 0,
+                "rx": 1
+            }
         },
         {
             "position": [ 360, 770 ],
-            "symbol": "R"
+            "symbol": "R",
+            "signal": {
+                "tx": 0,
+                "rx": 1
+            }
         },
         {
             "position": [ 440, 800 ],
-            "symbol": "O"
+            "symbol": "O",
+            "signal": {
+                "tx": 0,
+                "rx": 1
+            }
         }
     ]
 }

+ 82 - 0
pinball0/assets/tables/45_dbg Turbos.json

@@ -0,0 +1,82 @@
+{
+    "lives": 3,
+    "balls": [
+        {
+            "position": [ 110, 200 ]
+        },
+        {
+            "position": [ 610, 900 ]
+        }
+    ],
+    "flippers": [
+        {
+            "position": [ 130, 1180 ],
+            "side": "LEFT",
+            "size": 120
+        },
+        {
+            "position": [ 490, 1180 ],
+            "side": "RIGHT",
+            "size": 120
+        }
+    ],
+    "rails": [
+        // left wall
+        {
+            "start": [ 0, 0 ],
+            "end": [ 0, 1080 ]
+        },
+        // bottom left
+        {
+            "start": [ 0, 1080 ],
+            "end": [ 130, 1160 ]
+        },
+        // bottom right
+        {
+            "start": [ 490, 1160 ],
+            "end": [ 630, 1080 ]
+        },
+        // right wall
+        {
+            "start": [ 630, 1080 ],
+            "end": [ 630, 0 ]
+        }
+    ],
+    "turbos": [
+        // rect
+        {
+            "position": [ 100, 400 ],
+            "angle": 0
+        },
+        {
+            "position": [ 300, 400 ],
+            "angle": 270
+        },
+        {
+            "position": [ 300, 600 ],
+            "angle": 180
+        },
+        {
+            "position": [ 100, 600 ],
+            "angle": 90
+        },
+        // loop
+        {
+            "position": [ 440, 740 ],
+            "angle": 0
+        },
+        {
+            "position": [ 440, 1050 ],
+            "angle": 180
+        }
+    ],
+    "arcs": [
+        {
+            "position": [ 440, 900 ],
+            "radius": 180,
+            "surface": "INSIDE",
+            "start_angle": 0,
+            "end_angle": 360
+        }
+    ]
+}

+ 0 - 1
pinball0/notifications.cxx

@@ -120,7 +120,6 @@ void notify_game_over(void* ctx) {
 }
 
 void notify_bumper_hit(void* ctx) {
-    FURI_LOG_I(TAG, "notify_bumper_hit");
     PinballApp* app = (PinballApp*)ctx;
     int n = 0;
     if(app->settings.led_enabled) {

+ 55 - 48
pinball0/objects.cxx

@@ -117,14 +117,14 @@ bool Flipper::collide(Ball& ball) {
     perp *= -1.0f;
     perp.normalize();
     Vec2 surface_velocity = perp * 1.7f; // TODO: flipper power??
-    FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y);
+    // FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y);
     if(current_omega != 0.0f) surface_velocity *= current_omega;
-    FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y);
+    // FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y);
 
     // TODO: Flippers currently aren't "bouncy" when they are still
     float v = ball_v.dot(dir);
     float v_new = surface_velocity.dot(dir);
-    FURI_LOG_I(TAG, "v_new: %.4f, v: %.4f", (double)v_new, (double)v);
+    // FURI_LOG_I(TAG, "v_new: %.4f, v: %.4f", (double)v_new, (double)v);
     ball_v += dir * (v_new - v);
     ball.prev_p = ball.p - ball_v;
     return true;
@@ -137,6 +137,28 @@ Vec2 Flipper::get_tip() const {
     return tip;
 }
 
+// The default action for receiving a signal is to "appear"
+void FixedObject::signal_receive() {
+    physical = true;
+    hidden = false;
+}
+
+// The default action for a sending signal to have completed is to "hide"
+void FixedObject::signal_send() {
+    physical = false;
+    hidden = true;
+}
+
+void FixedObject::save_state() {
+    saved.physical = physical;
+    saved.hidden = hidden;
+}
+
+void FixedObject::reset_state() {
+    physical = saved.physical;
+    hidden = saved.hidden;
+}
+
 void Polygon::draw(Canvas* canvas) {
     if(!hidden) {
         for(size_t i = 0; i < points.size() - 1; i++) {
@@ -208,48 +230,9 @@ bool Polygon::collide(Ball& ball) {
     return true;
 }
 
-// Works-ish - 11/5/2024
-// bool Polygon::collide(Ball& ball) {
-//     Vec2 ball_v = ball.p - ball.prev_p;
-//     // We need to check for collisions across all line segments
-//     for(size_t i = 0; i < points.size() - 1; i++) {
-//         // If ball is moving away from the line, we can't have a collision!
-//         if(normals[i].dot(ball_v) > 0) {
-//             continue;
-//         }
-
-//         Vec2& p1 = points[i];
-//         Vec2& p2 = points[i + 1];
-//         // bool isLeft_prev = Vec2_ccw(p1, p2, ball.prev_p);
-//         // bool isLeft = Vec2_ccw(p1, p2, ball.p);
-//         Vec2 closest = Vec2_closest(p1, p2, ball.p);
-//         float dist = ball.p.dist(closest);
-
-//         if(dist < ball.r) {
-//             // FURI_LOG_I(TAG, "... within collision distance!");
-//             // ball_v.dot
-
-//             // float factor = (ball.r - dist) / ball.r;
-//             // ball.p -= normals[i] * factor;
-//             float depth = ball.r - dist;
-//             ball.p -= normals[i] * depth * 1.05f;
-
-//             Vec2 rel_v = ball_v * -1;
-//             float velAlongNormal = rel_v.dot(normals[i]);
-//             float j = (-(1 + 1) * velAlongNormal);
-//             Vec2 impulse = j * normals[i];
-//             ball_v -= impulse;
-
-//             ball.prev_p = ball.p - ball_v;
-//             return true;
-//         }
-//     }
-//     return false;
-// }
-
 void Polygon::finalize() {
     if(points.size() < 2) {
-        FURI_LOG_E(TAG, "Polygon: FINALIZE_ERROR - insufficient points");
+        FURI_LOG_E(TAG, "Polygon: FINALIZE ERROR - insufficient points");
         return;
     }
     // compute and store normals on all segments
@@ -405,9 +388,16 @@ Arc::Arc(const Vec2& p_, float r_, float s_, float e_, Surface surf_)
     , start(s_)
     , end(e_)
     , surface(surf_) {
+    // Vec2 s(p.x + r * cosf(start), p.y - r * sinf(start));
+    // Vec2 e(p.x + r * cosf(end), p.y - r * sinf(end));
+    // FURI_LOG_I(
+    //     TAG, "ARC: %.2f,%.2f - %.2f,%.2f", (double)s.x, (double)s.y, (double)e.x, (double)e.y);
 }
 
 void Arc::draw(Canvas* canvas) {
+    if(hidden) {
+        return;
+    }
     if(start == 0 && end == (float)M_PI * 2) {
         gfx_draw_circle(canvas, p, r);
     } else {
@@ -552,14 +542,31 @@ void Rollover::draw(Canvas* canvas) {
 }
 
 bool Rollover::collide(Ball& ball) {
+    if(activated) {
+        return false; // we've already rolled over it, prevent further signals
+    }
     Vec2 dir = ball.p - p;
     float dist = dir.mag();
     if(dist < 30) {
         activated = true;
+        return true;
     }
     return false;
 }
 
+// Reset the rollover
+void Rollover::signal_receive() {
+    activated = false;
+}
+
+void Rollover::signal_send() {
+    // maybe we should start a blink animation of the letters?
+}
+
+void Rollover::reset_state() {
+    activated = false;
+}
+
 void Turbo::draw(Canvas* canvas) {
     gfx_draw_line(canvas, chevron_1[0], chevron_1[1]);
     gfx_draw_line(canvas, chevron_1[1], chevron_1[2]);
@@ -569,12 +576,12 @@ void Turbo::draw(Canvas* canvas) {
 }
 
 bool Turbo::collide(Ball& ball) {
-    Vec2 dir = ball.p - p;
-    float dist = dir.mag();
-    if(dist < 30) {
+    float dist = (ball.p - p).mag();
+    // our distance check doesn't include the ball radius as we want the ball
+    // to enter the turbo area a bit before being affected by the boost
+    if(dist < r + 10) {
         // apply the turbo in 'dir' with force of 'boost'
-        FURI_LOG_I(TAG, "TURBO! dir: %.3f,%.3f", (double)dir.x, (double)dir.y);
-        ball.accelerate(dir * (boost / 50.0f));
+        ball.prev_p = ball.p - (dir * (boost));
     }
     return false;
 }

+ 34 - 9
pinball0/objects.h

@@ -3,11 +3,15 @@
 #include "vec2.h"
 #include <gui/canvas.h> // for Canvas*
 
+#include "signals.h"
+
 #define DEF_BALL_RADIUS   20
 #define DEF_BUMPER_RADIUS 40
 #define DEF_BUMPER_BOUNCE 1.0f
 #define DEF_FLIPPER_SIZE  120
 #define DEF_RAIL_BOUNCE   0.9f
+#define DEF_TURBO_RADIUS  20
+#define DEF_TURBO_BOOST   5
 
 #define ARC_TANGENT_RESTITUTION 1.0f
 #define ARC_NORMAL_RESTITUTION  0.8f
@@ -90,21 +94,38 @@ public:
         , physical(true)
         , hidden(false)
         , score(0)
+        , tx_id(INVALID_ID)
+        , rx_id(INVALID_ID)
+        , tx_type(SignalType::ALL)
         , notification(nullptr) {
     }
     virtual ~FixedObject() = default;
 
     float bounce;
-    bool physical; // can be hit
+    bool physical; // interacts with ball vs table decoration
     bool hidden; // do not draw
     int score;
+    int tx_id;
+    int rx_id;
+    SignalType tx_type;
 
     void (*notification)(void* app);
 
+    struct {
+        bool physical;
+        bool hidden;
+    } saved;
+
     virtual void draw(Canvas* canvas) = 0;
     virtual bool collide(Ball& ball) = 0;
     virtual void reset_animation() {};
     virtual void step_animation() {};
+
+    virtual void signal_receive();
+    virtual void signal_send();
+
+    virtual void save_state();
+    virtual void reset_state();
 };
 
 class Polygon : public FixedObject {
@@ -211,19 +232,25 @@ public:
 
     void draw(Canvas* canvas);
     bool collide(Ball& ball);
+
+    void signal_receive();
+    void signal_send();
+
+    void reset_state();
 };
 
 class Turbo : public FixedObject {
 public:
-    Turbo(const Vec2& p_, float angle_, float boost_)
+    Turbo(const Vec2& p_, float angle_, float boost_, float radius_)
         : FixedObject()
         , p(p_)
         , angle(angle_)
-        , boost(boost_) {
+        , boost(boost_)
+        , r(radius_) {
+        // Our boost direction
         dir = Vec2(cosf(angle), -sinf(angle));
 
-        // for now, fix the radius to 30 or whatever
-        size_t r = 30;
+        // define the points of the chevrons at the 0 angle
         chevron_1[0] = Vec2(p.x, p.y - r);
         chevron_1[1] = Vec2(p.x + r, p.y);
         chevron_1[2] = Vec2(p.x, p.y + r);
@@ -232,6 +259,7 @@ public:
         chevron_2[1] = Vec2(p.x, p.y);
         chevron_2[2] = Vec2(p.x - r, p.y + r);
 
+        // rotate the chevrons to the correct angle
         for(size_t i = 0; i < 3; i++) {
             Vec2& v = chevron_1[i];
             Vec2 d = v - p;
@@ -249,6 +277,7 @@ public:
     Vec2 p;
     float angle;
     float boost;
+    float r;
 
     Vec2 dir; // unit normal of turbo direction
 
@@ -289,7 +318,3 @@ public:
     void draw(Canvas* canvas);
     void step_animation();
 };
-
-// class IconImage : public Object {
-//     Vec2 v;
-// };

+ 24 - 12
pinball0/pinball0.cxx

@@ -18,7 +18,7 @@
 #define GAME_FPS          30
 #define MANUAL_ADJUSTMENT 20
 #define IDLE_TIMEOUT      120 * 1000 // 120 seconds * 1000 ticks/sec
-#define BUMP_DELAY        2 * 1000 // 2 seconds
+#define BUMP_COOLDOWN     1 * 1000 // 1 seconds
 #define BUMP_MAX          3
 
 void solve(PinballApp* pb, float dt) {
@@ -79,12 +79,16 @@ void solve(PinballApp* pb, float dt) {
         for(auto& b : table->balls) {
             for(auto& o : table->objects) {
                 if(o->physical && o->collide(b)) {
-                    if(pb->game_mode == GM_Tilted) {
+                    if(pb->game_mode == GM_Tilted || table->balls_released == false) {
+                        o->reset_state(); // ensure we do nothing!
                         continue;
                     }
                     if(o->notification) {
                         (*o->notification)(pb);
                     }
+                    // Send this object's signal (if defined)
+                    table->sm.send(o);
+
                     table->score.value += o->score;
                     o->reset_animation();
                     continue;
@@ -172,8 +176,8 @@ static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
                 AlignTop,
                 furi_string_get_cstr(menu_item.name));
             if(i == half_way) {
-                canvas_draw_disc(canvas, 8, y + 3, 2);
-                canvas_draw_disc(canvas, 56, y + 3, 2);
+                canvas_draw_disc(canvas, 6, y + 3, 2);
+                canvas_draw_disc(canvas, 58, y + 3, 2);
             }
             y += 12;
         }
@@ -450,6 +454,7 @@ extern "C" int32_t pinball0_app(void* p) {
                     if(app.settings.debug_mode && app.table->balls_released == false) {
                         app.table->balls[0].p.x += MANUAL_ADJUSTMENT;
                         app.table->balls[0].prev_p.x += MANUAL_ADJUSTMENT;
+                        break;
                     }
                     bool flipper_pressed = false;
                     for(auto& f : app.table->flippers) {
@@ -474,6 +479,7 @@ extern "C" int32_t pinball0_app(void* p) {
                     if(app.settings.debug_mode && app.table->balls_released == false) {
                         app.table->balls[0].p.x -= MANUAL_ADJUSTMENT;
                         app.table->balls[0].prev_p.x -= MANUAL_ADJUSTMENT;
+                        break;
                     }
                     bool flipper_pressed = false;
                     for(auto& f : app.table->flippers) {
@@ -491,10 +497,15 @@ extern "C" int32_t pinball0_app(void* p) {
                 case InputKeyUp:
                     switch(app.game_mode) {
                     case GM_Playing:
+                        if(app.settings.debug_mode && app.table->balls_released == false) {
+                            app.table->balls[0].p.y -= MANUAL_ADJUSTMENT;
+                            app.table->balls[0].prev_p.y -= MANUAL_ADJUSTMENT;
+                            break;
+                        }
                         if(event.type == InputTypePress) {
                             // Table bump and Tilt tracking
                             uint32_t current_tick = furi_get_tick();
-                            if(current_tick - app.table->last_bump >= BUMP_DELAY) {
+                            if(current_tick - app.table->last_bump >= BUMP_COOLDOWN) {
                                 app.table->bump_count++;
                                 app.table->last_bump = current_tick;
                                 if(!app.table->tilt_detect_enabled ||
@@ -505,14 +516,13 @@ extern "C" int32_t pinball0_app(void* p) {
                                     FURI_LOG_W(TAG, "TABLE TILTED!");
                                     app.game_mode = GM_Tilted;
                                     app.table->bump_count = 0;
+                                    for(auto& o : app.table->objects) {
+                                        o->reset_state();
+                                    }
                                     notify_table_tilted(&app);
                                 }
                             }
                         }
-                        if(app.settings.debug_mode && app.table->balls_released == false) {
-                            app.table->balls[0].p.y -= MANUAL_ADJUSTMENT;
-                            app.table->balls[0].prev_p.y -= MANUAL_ADJUSTMENT;
-                        }
                         break;
                     case GM_TableSelect:
                         app.table_list.selected =
@@ -532,11 +542,12 @@ extern "C" int32_t pinball0_app(void* p) {
                 case InputKeyDown:
                     switch(app.game_mode) {
                     case GM_Playing:
-                        app.keys[InputKeyDown] = true;
                         if(app.settings.debug_mode && app.table->balls_released == false) {
                             app.table->balls[0].p.y += MANUAL_ADJUSTMENT;
                             app.table->balls[0].prev_p.y += MANUAL_ADJUSTMENT;
+                            break;
                         }
+                        app.keys[InputKeyDown] = true;
                         break;
                     case GM_TableSelect:
                         app.table_list.selected =
@@ -631,14 +642,15 @@ extern "C" int32_t pinball0_app(void* p) {
         view_port_update(view_port);
         furi_mutex_release(app.mutex);
 
-        // game timing + idle check
+        // idle timeout check
         uint32_t current_tick = furi_get_tick();
-        if(current_tick - app.idle_start >= IDLE_TIMEOUT) {
+        if(app.game_mode == GM_TableSelect && current_tick - app.idle_start >= IDLE_TIMEOUT) {
             FURI_LOG_W(TAG, "Idle timeout! Exiting Pinball0...");
             app.processing = false;
             break;
         }
 
+        // game loop timing
         uint32_t time_lapsed = current_tick - last_frame_time;
         dt = time_lapsed / 1000.0f;
         while(dt < 1.0f / GAME_FPS) {

+ 1 - 1
pinball0/pinball0.h

@@ -21,7 +21,7 @@
 // #define DRAW_NORMALS
 
 #define TAG     "Pinball0"
-#define VERSION "v0.4"
+#define VERSION "v0.5.1"
 
 // Vertical orientation
 #define LCD_WIDTH  64

+ 118 - 0
pinball0/signals.cxx

@@ -0,0 +1,118 @@
+#include <furi.h>
+#include "objects.h"
+#include "signals.h"
+
+void SignalManager::register_signal(int id, void* ctx) {
+    // FURI_LOG_I("SIGNAL", "Registered signal, id = %d", id);
+    signals.push_back({id, ctx, false});
+}
+
+void SignalManager::register_slot(int id, void* ctx) {
+    // FURI_LOG_I("SIGNAL", "Registered slot, id = %d", id);
+    slots.push_back({id, ctx, false});
+}
+
+// Send signal 'id' and account for type ALL and ANY
+void SignalManager::send(void* ctx) {
+    FixedObject* obj = (FixedObject*)ctx;
+    int id = obj->tx_id;
+
+    if(id == INVALID_ID) {
+        return;
+    }
+    // FURI_LOG_I("SIGNAL", "Attempting to send signal %d", id);
+    bool signal = true;
+    for(auto& s : signals) {
+        if(s.id == id) {
+            if(s.ctx == ctx) {
+                s.triggered = true;
+                if(((FixedObject*)(ctx))->tx_type == SignalType::ANY) {
+                    signal = true;
+                    break;
+                }
+            }
+            signal = signal & s.triggered;
+        }
+    }
+    if(signal) {
+        // Send the signal to all objects who want it
+        // FURI_LOG_I(
+        //     "SIGNAL",
+        //     "Signals for id=%d, and type=%s",
+        //     id,
+        //     ((FixedObject*)(ctx))->tx_type == SignalType::ALL ? "ALL" : "ANY");
+        for(auto& s : slots) {
+            if(s.id == id) {
+                ((FixedObject*)(s.ctx))->signal_receive();
+            }
+        }
+        // Clear our internal state of what triggered (used for type ALL)
+        reset(id);
+        // Let the signal initiating objects know that we have sent the signal
+        for(auto& s : signals) {
+            if(s.id == id) {
+                ((FixedObject*)(s.ctx))->signal_send();
+            }
+        }
+    }
+}
+
+void SignalManager::reset(int id) {
+    for(auto& s : signals) {
+        if(s.id == id) {
+            s.triggered = false;
+        }
+    }
+}
+
+// better data structures would make this function more efficient
+bool SignalManager::validate(char* err, std::size_t err_size) {
+    // Verify that there is at least one slot for every signal
+    for(const auto& signal : signals) {
+        bool found = false;
+        for(const auto& slot : slots) {
+            if(signal.id == slot.id) {
+                found = true;
+                break;
+            }
+        }
+        if(!found) {
+            FURI_LOG_E("PB0 SIGNAL", "Signal %d has no slots!", signal.id);
+            snprintf(err, err_size, "Signal %d\nhas no\nslots!", signal.id);
+            return false;
+        }
+    }
+    // Verify that there is at least one signal for every slot
+    for(const auto& slot : slots) {
+        bool found = false;
+        for(const auto& signal : signals) {
+            if(slot.id == signal.id) {
+                found = true;
+                break;
+            }
+        }
+        if(!found) {
+            FURI_LOG_E("PB0 SIGNAL", "Slot %d has no signals!", slot.id);
+            snprintf(err, err_size, "Slot %d\nhas no\nsignals!", slot.id);
+            return false;
+        }
+    }
+    // Verify that all objects with the same signal id have the same trigger type
+    bool valid_types = true;
+    for(const auto& signal : signals) {
+        const SignalType signal_type = ((FixedObject*)signal.ctx)->tx_type;
+        for(const auto& s : signals) {
+            FixedObject* s_obj = (FixedObject*)signal.ctx;
+            if(signal.id == s.id && signal_type != s_obj->tx_type) {
+                valid_types = false;
+                FURI_LOG_E("PB0 SIGNAL", "Signal %d has differing type!", s.id);
+                snprintf(err, err_size, "Signal %d\nhas diff\ntype!", s.id);
+                break;
+            }
+        }
+        if(!valid_types) {
+            break;
+        }
+    }
+    return valid_types;
+}

+ 32 - 0
pinball0/signals.h

@@ -0,0 +1,32 @@
+#pragma once
+#include <vector>
+
+#define INVALID_ID -1
+
+typedef enum {
+    ALL,
+    ANY
+} SignalType;
+
+typedef struct SignalData {
+    int id;
+    void* ctx;
+    bool triggered;
+} SignalData;
+
+class SignalManager {
+public:
+    SignalManager() = default;
+
+    void register_signal(int id, void* ctx);
+    void register_slot(int id, void* ctx);
+
+    void send(void* ctx);
+    void reset(int id);
+
+    bool validate(char* err, std::size_t err_size);
+
+    // all id + ctx pairs must have triggered before we send
+    std::vector<SignalData> signals;
+    std::vector<SignalData> slots;
+};

+ 3 - 0
pinball0/table.h

@@ -4,6 +4,7 @@
 #include <vector>
 #include "pinball0.h"
 #include "objects.h"
+#include "signals.h"
 
 #define TABLE_SELECT       0
 #define TABLE_ERROR        1
@@ -75,6 +76,8 @@ public:
     uint32_t last_bump;
     uint32_t bump_count;
 
+    SignalManager sm;
+
     void draw(Canvas* canvas);
 };
 

+ 55 - 5
pinball0/table_parser.cxx

@@ -143,6 +143,25 @@ bool table_file_parse_float(const nx_json* json, const char* key, float& v) {
     return true;
 }
 
+void table_file_parse_signal(const nx_json* json, Table* table, FixedObject* obj) {
+    const nx_json* signal = nx_json_get(json, "signal");
+    if(signal) {
+        int tx = INVALID_ID;
+        int rx = INVALID_ID;
+        if(table_file_parse_int(signal, "tx", tx) && tx != INVALID_ID) {
+            obj->tx_id = tx;
+            table->sm.register_signal(tx, obj);
+        }
+        if(table_file_parse_int(signal, "rx", rx) && rx != INVALID_ID) {
+            obj->rx_id = rx;
+            table->sm.register_slot(rx, obj);
+        }
+        bool any = false;
+        table_file_parse_bool(signal, "any", any);
+        obj->tx_type = any ? SignalType::ANY : SignalType::ALL;
+    }
+}
+
 Table* table_load_table_from_file(PinballApp* pb, size_t index) {
     auto& tmi = pb->table_list.menu_items[index];
 
@@ -165,10 +184,14 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
         storage_file_free(file);
         return NULL;
     }
-    FURI_LOG_I(TAG, "File size is ok!");
     bool ok =
         storage_file_open(file, furi_string_get_cstr(tmi.filename), FSAM_READ, FSOM_OPEN_EXISTING);
-    FURI_LOG_I(TAG, "File opened? %s", ok ? "YES" : "NO");
+    if(!ok) {
+        FURI_LOG_E(TAG, "Failed to open table file: %s", furi_string_get_cstr(tmi.filename));
+        snprintf(pb->text, 256, "Failed\nto open\nfile!");
+        storage_file_free(file);
+        return NULL;
+    }
 
     // read the file as a string
     uint8_t* buffer;
@@ -342,9 +365,20 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 float bnc = DEF_BUMPER_BOUNCE;
                 table_file_parse_float(bumper, "bounce", bnc);
 
+                bool physical = true;
+                table_file_parse_bool(bumper, "physical", physical);
+
+                bool hidden = false;
+                table_file_parse_bool(bumper, "hidden", hidden);
+
                 Bumper* new_bumper = new Bumper(p, r);
                 new_bumper->bounce = bnc;
                 new_bumper->notification = notify_bumper_hit;
+                new_bumper->physical = physical;
+                new_bumper->hidden = hidden;
+
+                table_file_parse_signal(bumper, table, new_bumper);
+
                 table->objects.push_back(new_bumper);
             }
         }
@@ -534,6 +568,9 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                     sym = symbol->text_value[0];
                 }
                 Rollover* new_rollover = new Rollover(p, sym);
+
+                table_file_parse_signal(rollover, table, new_rollover);
+
                 table->objects.push_back(new_rollover);
             }
         }
@@ -559,17 +596,30 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 table_file_parse_float(turbo, "angle", angle);
                 angle *= pi_180;
 
-                float boost = 10;
+                float boost = DEF_TURBO_BOOST;
                 table_file_parse_float(turbo, "boost", boost);
 
-                Turbo* new_turbo = new Turbo(p, angle, boost);
+                float radius = DEF_TURBO_RADIUS;
+                table_file_parse_float(turbo, "radius", radius);
+
+                Turbo* new_turbo = new Turbo(p, angle, boost, radius);
 
                 table->objects.push_back(new_turbo);
             }
         }
-        break;
+
+        for(auto& o : table->objects) {
+            o->save_state();
+        }
+
     } while(false);
 
+    if(!table->sm.validate(pb->text, 256)) {
+        FURI_LOG_E(TAG, "Signal validation failed!");
+        delete table;
+        table = NULL;
+    }
+
     nx_json_free(json);
     free(json_buffer);