rdefeo пре 1 година
родитељ
комит
84b8067861

+ 8 - 0
CHANGELOG.md

@@ -1,3 +1,11 @@
+## 0.2.0
+
+- User tables from /apps_data/pinball0 folder
+- Sounds, LED blinking, vibrations
+- Basic scores
+- Collision bug fixes
+- Mem leak fix
+
 ## 0.1.0
 ## 0.1.0
 
 
 - BETA release
 - BETA release

+ 36 - 22
README.md

@@ -1,9 +1,9 @@
 # Pinball0 (Pinball Zero)
 # Pinball0 (Pinball Zero)
 Play pinball on your Flipperzero!
 Play pinball on your Flipperzero!
 
 
-This is a BETA release - like, I'm surprised it works! You may encounter crashes and/or memory leaks.
+This is a BETA release!! Still a work in progress...
 
 
-> The default tables and example tables may / will change
+> The default tables and example tables may / _will_ change
 
 
 ## Screenshots
 ## Screenshots
 
 
@@ -15,10 +15,13 @@ This is a BETA release - like, I'm surprised it works! You may encounter crashes
 ## Features
 ## Features
 * Realistic physics and collisions
 * Realistic physics and collisions
 * User-defined tables via JSON files
 * User-defined tables via JSON files
-* Portals!
 * Bumpers, flat surfaces, curved surfaces
 * Bumpers, flat surfaces, curved surfaces
+* Scores! (no high scores yet, just a running tally as you play)
+* Table bumps!
+* Portals!
 * Rollover items
 * Rollover items
-* Fancy animations (-ish)
+* Sounds! Blinky lights! Annoying vibrations!
+* Customizable notification settings: sound, LED, vibration
 
 
 ## Controls
 ## Controls
 * **Ok** to release the ball
 * **Ok** to release the ball
@@ -28,42 +31,61 @@ This is a BETA release - like, I'm surprised it works! You may encounter crashes
 
 
 I find it easiest to hold the flipper with both hands so I can hit left/right with my thumbs!
 I find it easiest to hold the flipper with both hands so I can hit left/right with my thumbs!
 
 
+## Settings
+The **SETTINGS** menu will be the "last" table listed. You can Enable / Disable the following: Sound, LED light, Vibration, and Manual mode. Move Up/Down to select your setting and press **OK** to toggle. Settings are saved in `/data/.pinball0.conf` as a native Flipper Format file. **Back** will return you to the main menu.
+
+**Manual** 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.
+
 ## Tables
 ## Tables
-Pinball0 ships with several default tables. These tables are automatically deployed into the assets folder. Tables are simple JSON which means you can define your own! Your tables should be stored in the data folder. **The default tables may change over time.**
+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`). **The default tables may change over time.**
 
 
 ### File Format
 ### 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.
+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.
 
 
 The JSON can include comments - because why not!
 The JSON can include comments - because why not!
 
 
 > **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.
 > **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.
 
 
+#### lives : object (optional)
+Defines how many lives/balls you start with, and display information
+
+* `"display": bool` : optional, defaults to false
+* `"position": [ X, Y ]`
+* `"value": N` : Number of balls - optional, defaults to 3
+* `"align": A` : optional, defaults to `"HORIZONTAL"` (also, `"VERTICAL"`)
+
 #### balls : list of objects
 #### balls : list of objects
 Every table needs at least 1 ball, otherwise it will fail to load.
 Every table needs at least 1 ball, otherwise it will fail to load.
 
 
 * `"position": [ X, Y ]`
 * `"position": [ X, Y ]`
-* `"velocity": [ VX, VY ]` : optional, defaults to `[ 0, 0 ]`
+* `"velocity": [ VX, VY ]` : optional, defaults to `[ 0, 0 ]`. The default tables have an initial velocity magnitude in the 10-18 range. Test your own values!
 * `"radius" : N` : optional, defaults to `20`
 * `"radius" : N` : optional, defaults to `20`
 
 
-#### flippers : list of objects
+#### score : object (optional)
+* `"display" : bool` : optional, defaults to false
+* `"position" : [ X, Y ]` : optional, defaults to `[ 63, 0 ]`
+
+The position units are in absolute LCD pixels within the range [0..63], [0..127].
+
+#### flippers : list of objects (optional)
 * `"position": [ X, Y ]` : location of the pivot point
 * `"position": [ X, Y ]` : location of the pivot point
 * `"side": S` : valid values are `"LEFT"` or `"RIGHT"`
 * `"side": S` : valid values are `"LEFT"` or `"RIGHT"`
-* `"size": N` : optional, defaults to `130`
+* `"size": N` : optional, defaults to `120`
 
 
 You can have more than 2 flippers! Try it!
 You can have more than 2 flippers! Try it!
 
 
-#### bumpers : list of objects
+#### bumpers : list of objects (optional)
 * `"position": [ X, Y ]`
 * `"position": [ X, Y ]`
 * `"radius": [ N ]` : optional, defaults to `40`
 * `"radius": [ N ]` : optional, defaults to `40`
 
 
-#### rails : list of objects
+#### rails : list of objects (optional)
 * `"start": [ X, Y ]`
 * `"start": [ X, Y ]`
 * `"end": [ X, Y ]`
 * `"end": [ X, Y ]`
 * `"double_sided": bool` : optional, defaults to `false`
 * `"double_sided": bool` : optional, defaults to `false`
 
 
 The "surface" of a rail is "on the left" as we move from the `"start"` to the `"end"`. This means that when thinking about your table, the outer perimiter should be defined in a counter-clockwise order. Arriving at a rail from it's revrese side will result in a pass-through - think of it as a one-way mirror.
 The "surface" of a rail is "on the left" as we move from the `"start"` to the `"end"`. This means that when thinking about your table, the outer perimiter should be defined in a counter-clockwise order. Arriving at a rail from it's revrese side will result in a pass-through - think of it as a one-way mirror.
 
 
-#### arcs : list of objects
+#### arcs : list of objects (optional)
 * `"position": [ X, Y ]`
 * `"position": [ X, Y ]`
 * `"radius" : N`
 * `"radius" : N`
 * `"start_angle": N` : in degrees from 0 to 360
 * `"start_angle": N` : in degrees from 0 to 360
@@ -72,24 +94,16 @@ The "surface" of a rail is "on the left" as we move from the `"start"` to the `"
 
 
 Start and End angles should be specified in **counter-clockwise** order. The **surface** defines which side will bounce/reflect.
 Start and End angles should be specified in **counter-clockwise** order. The **surface** defines which side will bounce/reflect.
 
 
-#### rollovers : list of objects
+#### rollovers : list of objects (optional)
 * `"position": [ X, Y ]`
 * `"position": [ X, Y ]`
 * `"symbol": C` : where C is a string representing a single character, like `"Z"`
 * `"symbol": C` : where C is a string representing a single character, like `"Z"`
 
 
 When the ball passes over/through a rollover object, the symbol will appear. Only the first character of the string is used.
 When the ball passes over/through a rollover object, the symbol will appear. Only the first character of the string is used.
 
 
-#### portals : list of objects
+#### portals : list of objects (optional)
 * `"a_start": [ X, Y]`
 * `"a_start": [ X, Y]`
 * `"a_end": [ X, Y]`
 * `"a_end": [ X, Y]`
 * `"b_start": [ X, Y]`
 * `"b_start": [ X, Y]`
 * `"b_end": [ X, Y]`
 * `"b_end": [ X, Y]`
 
 
 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.
 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.
-
-#### lives : int
-* `"lives": N` : optional, defaults to 1
-
-The number of starting lives/balls.
-
-#### lives_position : list of int
-* `"lives_position": [ X, Y ]` : where to draw the number of remaining lives

+ 10 - 3
README_flipperlab.md

@@ -6,9 +6,12 @@ This is a BETA release
 ## Features
 ## Features
 * Realistic physics and collisions
 * Realistic physics and collisions
 * User-defined tables via JSON files
 * User-defined tables via JSON files
-* Portals!
 * Bumpers, flat surfaces, curved surfaces
 * Bumpers, flat surfaces, curved surfaces
-* Fancy animations (-ish)
+* Table bumps
+* Portals!
+* Rollover items
+* Sounds! Blinky lights! Annoying vibrations!
+* Customizable notification settings: sound, LED, vibration
 
 
 ## Controls
 ## Controls
 * **Ok** to release the ball
 * **Ok** to release the ball
@@ -19,5 +22,9 @@ This is a BETA release
 I find it easiest to hold the flipper with both hands so I can hit left/right with my thumbs!
 I find it easiest to hold the flipper with both hands so I can hit left/right with my thumbs!
 
 
 ## Tables
 ## Tables
-Pinball0 ships with several default tables. These tables are automatically deployed into the assets folder. Tables are simple JSON which means you can define your own! Your tables should be stored in the data folder. **The default tables may change over time.**
+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). 
+
+View the github repo for the JSON format specification: https://github.com/rdefeo/pinball0
+
+**The default tables may change over time.**
 
 

+ 2 - 2
application.fam

@@ -5,11 +5,11 @@ App(
     name="Pinball0",
     name="Pinball0",
     apptype=FlipperAppType.EXTERNAL,
     apptype=FlipperAppType.EXTERNAL,
     entry_point="pinball0_app",
     entry_point="pinball0_app",
-    stack_size=2 * 1024,
+    stack_size=2 * 1024,  # neede?
     fap_category="Games",
     fap_category="Games",
     requires=["gui"],
     requires=["gui"],
     # Optional values
     # Optional values
-    fap_version="0.1",
+    fap_version="0.2",
     fap_icon="pinball0.png",  # 10x10 1-bit PNG
     fap_icon="pinball0.png",  # 10x10 1-bit PNG
     fap_description="Pinball game",
     fap_description="Pinball game",
     fap_author="Roberto De Feo",
     fap_author="Roberto De Feo",

+ 6 - 1
assets/tables/01_Basic.json

@@ -1,12 +1,17 @@
 {
 {
     "name": "Basic",
     "name": "Basic",
+    "lives": {
+        "display": true
+    },
+    "score": {
+        "display": true
+    },
     "balls": [
     "balls": [
         {
         {
             "position": [ 600, 1110 ],
             "position": [ 600, 1110 ],
             "velocity": [ 0, -16.0 ]
             "velocity": [ 0, -16.0 ]
         }
         }
     ],
     ],
-    // "released": true,
     // "plunger": {
     // "plunger": {
     //     "position": [ 600, 1200 ],
     //     "position": [ 600, 1200 ],
     //     "size": 100
     //     "size": 100

+ 18 - 3
assets/tables/02_Classic.json

@@ -1,5 +1,14 @@
 {
 {
     "name": "Classic",
     "name": "Classic",
+    "lives": {
+        "display": true,
+        "position": [ 20, 480 ],
+        "align": "VERTICAL"
+    },
+    "score": {
+        "display": true,
+        "position": [ 23, 0 ]
+    },
     "balls": [
     "balls": [
         {
         {
             "position": [ 600, 1110 ],
             "position": [ 600, 1110 ],
@@ -58,6 +67,10 @@
         // left wall
         // left wall
         {
         {
             "start": [ 0, 240 ],
             "start": [ 0, 240 ],
+            "end": [ 0, 400 ]
+        },
+        {
+            "start": [ 0, 740 ],
             "end": [ 0, 1200 ]
             "end": [ 0, 1200 ]
         },
         },
         // right wall
         // right wall
@@ -73,15 +86,17 @@
         // left wall fixture
         // left wall fixture
         {
         {
             "start": [ 0, 400 ],
             "start": [ 0, 400 ],
-            "end": [ 80, 480 ]
+            "end": [ 80, 480 ],
+            "bounce": 1.08
         },
         },
         {
         {
             "start": [ 80, 480 ],
             "start": [ 80, 480 ],
-            "end": [ 80, 660 ]
+            "end": [ 80, 660 ],
+            "bounce": 1.1
         },
         },
         {
         {
             "start": [ 80, 660 ],
             "start": [ 80, 660 ],
-            "end": [ 0, 720 ],
+            "end": [ 0, 740 ],
             "bounce": 1.08
             "bounce": 1.08
         },
         },
         // left bottom rail
         // left bottom rail

+ 0 - 1
assets/tables/03_El Ocho.json

@@ -1,6 +1,5 @@
 {
 {
     "name": "El Ocho",
     "name": "El Ocho",
-    "lives": 3,
     "balls": [
     "balls": [
         {
         {
             "position": [ 580, 580 ],
             "position": [ 580, 580 ],

+ 1 - 1
assets/tables/04_Chamber.json

@@ -1,5 +1,5 @@
 {
 {
-    "lives_position": [ 290, 20 ],
+    "name": "Chamber",
     "balls": [
     "balls": [
         {
         {
             "position": [ 390, 400 ]
             "position": [ 390, 400 ]

+ 3 - 1
assets/tables/05_Endless.json

@@ -1,6 +1,8 @@
 {
 {
     "name": "Endless",
     "name": "Endless",
-    "lives": 1,
+    "lives": {
+        "value": 1
+    },
     "balls": [
     "balls": [
         {
         {
             "position": [ 600, 510 ],
             "position": [ 600, 510 ],

+ 3 - 1
assets/tables/40_ex Arc Test.json

@@ -1,5 +1,7 @@
 {
 {
-    "lives": 1,
+    "lives": {
+        "value": 1
+    },
     "balls": [
     "balls": [
         {
         {
             "position": [ 50, 140 ]
             "position": [ 50, 140 ]

+ 1 - 6
assets/tables/50_ex Bumpers.json

@@ -1,12 +1,7 @@
 {
 {
-    "lives": 3,
     "balls": [
     "balls": [
         {
         {
-            "position": [
-                250,
-                50
-            ],
-            "radius": 20
+            "position": [ 250, 50 ]
         }
         }
     ],
     ],
     "flippers": [
     "flippers": [

+ 0 - 2
assets/tables/70_ex Platforms.json

@@ -1,7 +1,5 @@
 {
 {
     "name": "Platforms",
     "name": "Platforms",
-    "lives": 3,
-    "released": false,
     "balls": [
     "balls": [
         {
         {
             "position": [
             "position": [

+ 1 - 2
assets/tables/95_ex Error.json

@@ -1,8 +1,7 @@
 {
 {
-    "lives_position": [ 2, 2 ],
     "balls": [
     "balls": [
         {
         {
-            // "position": [ 50, 140 ]
+            // oh noes! we don't have a position!
         }
         }
     ],
     ],
     "arcs": [
     "arcs": [

+ 83 - 0
graphics.cxx

@@ -0,0 +1,83 @@
+#include "graphics.h"
+
+#define SCALE 10
+
+/*
+  Fontname: micro
+  Copyright: Public domain font.  Share and enjoy.
+  Glyphs: 18/128
+  BBX Build Mode: 0
+*/
+const uint8_t u8g2_font_micro_tn[148] =
+    "\22\0\2\3\2\3\1\4\4\3\5\0\0\5\0\5\0\0\0\0\0\0w \4`\63*\10\67\62Q"
+    "j\312\0+\7or\321\24\1,\5*r\3-\5\247\62\3.\5*\62\4/\10\67\262\251\60\12"
+    "\1\60\10\67r)U\12\0\61\6\66rS\6\62\7\67\62r\224\34\63\7\67\62r$\22\64\7\67"
+    "\62\221\212\14\65\7\67\62\244<\1\66\6\67r#E\67\10\67\62c*\214\0\70\6\67\62TE\71"
+    "\7\67\62\24\71\1:\6\66\62$\1\0\0\0\4\377\377\0";
+
+// TODO: allow points to be located outside the canvas. currently, the canvas_* methods
+// choke on this in some cases, resulting in large vertical/horizontal lines
+void gfx_draw_line(Canvas* canvas, float x1, float y1, float x2, float y2) {
+    canvas_draw_line(
+        canvas, roundf(x1 / SCALE), roundf(y1 / SCALE), roundf(x2 / SCALE), roundf(y2 / SCALE));
+}
+
+void gfx_draw_line(Canvas* canvas, const Vec2& p1, const Vec2& p2) {
+    gfx_draw_line(canvas, p1.x, p1.y, p2.x, p2.y);
+}
+
+void gfx_draw_disc(Canvas* canvas, float x, float y, float r) {
+    canvas_draw_disc(canvas, roundf(x / SCALE), roundf(y / SCALE), roundf(r / SCALE));
+}
+void gfx_draw_disc(Canvas* canvas, const Vec2& p, float r) {
+    gfx_draw_disc(canvas, p.x, p.y, r);
+}
+
+void gfx_draw_circle(Canvas* canvas, float x, float y, float r) {
+    canvas_draw_circle(canvas, roundf(x / SCALE), roundf(y / SCALE), roundf(r / SCALE));
+}
+void gfx_draw_circle(Canvas* canvas, const Vec2& p, float r) {
+    gfx_draw_circle(canvas, p.x, p.y, r);
+}
+
+void gfx_draw_dot(Canvas* canvas, float x, float y) {
+    canvas_draw_dot(canvas, roundf(x / SCALE), roundf(y / SCALE));
+}
+void gfx_draw_dot(Canvas* canvas, const Vec2& p) {
+    gfx_draw_dot(canvas, p.x, p.y);
+}
+
+void gfx_draw_arc(Canvas* canvas, const Vec2& p, float r, float start, float end) {
+    float adj_end = end;
+    if(end < start) {
+        adj_end += (float)M_PI * 2;
+    }
+    // initialize to start of arc
+    float sx = p.x + r * cosf(start);
+    float sy = p.y - r * sinf(start);
+    size_t segments = r / 8;
+    for(size_t i = 1; i <= segments; i++) { // for now, use r to determin number of segments
+        float nx = p.x + r * cosf(start + i / (segments / (adj_end - start)));
+        float ny = p.y - r * sinf(start + i / (segments / (adj_end - start)));
+        gfx_draw_line(canvas, sx, sy, nx, ny);
+        sx = nx;
+        sy = ny;
+    }
+}
+
+void gfx_draw_str(Canvas* canvas, int x, int y, Align h, Align v, const char* str) {
+    canvas_set_custom_u8g2_font(canvas, u8g2_font_micro_tn);
+
+    canvas_set_color(canvas, ColorWhite);
+    int w = canvas_string_width(canvas, str);
+    if(h == AlignRight) {
+        canvas_draw_box(canvas, x - 1 - w, y, w + 2, 6);
+    } else if(h == AlignLeft) {
+        canvas_draw_box(canvas, x - 1, y, w + 2, 6);
+    }
+
+    canvas_set_color(canvas, ColorBlack);
+    canvas_draw_str_aligned(canvas, x, y, h, v, str);
+
+    canvas_set_font(canvas, FontSecondary); // reset?
+}

+ 26 - 0
graphics.h

@@ -0,0 +1,26 @@
+#pragma once
+
+#include <gui/gui.h>
+#include "vec2.h"
+
+// Use to draw table elements, which live on a 640 x 1280 grid
+// These methods will scale and round the coordinates
+// Also, they will (eventually) handle cases where the thing we're drawing
+// lies outside the table bounds.
+
+void gfx_draw_line(Canvas* canvas, float x1, float y1, float x2, float y2);
+void gfx_draw_line(Canvas* canvas, const Vec2& p1, const Vec2& p2);
+
+void gfx_draw_disc(Canvas* canvas, float x, float y, float r);
+void gfx_draw_disc(Canvas* canvas, const Vec2& p, float r);
+
+void gfx_draw_circle(Canvas* canvas, float x, float y, float r);
+void gfx_draw_circle(Canvas* canvas, const Vec2& p, float r);
+
+void gfx_draw_dot(Canvas* canvas, float x, float y);
+void gfx_draw_dot(Canvas* canvas, const Vec2& p);
+
+void gfx_draw_arc(Canvas* canvas, const Vec2& p, float r, float start, float end);
+
+// Uses the micro font
+void gfx_draw_str(Canvas* canvas, int x, int y, Align h, Align v, const char* str);

+ 215 - 3
notifications.cxx

@@ -1,8 +1,32 @@
 #include "notifications.h"
 #include "notifications.h"
+#include "pinball0.h"
 
 
-static const NotificationMessage* nm_list[16];
+static const NotificationMessage* nm_list[32];
+static FuriMutex* nm_mutex;
 
 
-void notify_table_bump(PinballApp* app) {
+void notify_init() {
+    nm_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+}
+void notify_free() {
+    furi_mutex_free(nm_mutex);
+}
+
+void notify_ball_released(void* ctx) {
+    PinballApp* app = (PinballApp*)ctx;
+    int n = 0;
+    if(app->settings.vibrate_enabled) {
+        nm_list[n++] = &message_vibro_on;
+    }
+    nm_list[n++] = &message_delay_100;
+    if(app->settings.vibrate_enabled) {
+        nm_list[n++] = &message_vibro_off;
+    }
+    nm_list[n] = NULL;
+    notification_message(app->notify, &nm_list);
+}
+
+void notify_table_bump(void* ctx) {
+    PinballApp* app = (PinballApp*)ctx;
     int n = 0;
     int n = 0;
     if(app->settings.vibrate_enabled) {
     if(app->settings.vibrate_enabled) {
         nm_list[n++] = &message_vibro_on;
         nm_list[n++] = &message_vibro_on;
@@ -21,7 +45,8 @@ void notify_table_bump(PinballApp* app) {
     notification_message(app->notify, &nm_list);
     notification_message(app->notify, &nm_list);
 }
 }
 
 
-void notify_error_message(PinballApp* app) {
+void notify_error_message(void* ctx) {
+    PinballApp* app = (PinballApp*)ctx;
     int n = 0;
     int n = 0;
     if(app->settings.sound_enabled) {
     if(app->settings.sound_enabled) {
         nm_list[n++] = &message_note_c6;
         nm_list[n++] = &message_note_c6;
@@ -35,3 +60,190 @@ void notify_error_message(PinballApp* app) {
     nm_list[n] = NULL;
     nm_list[n] = NULL;
     notification_message(app->notify, &nm_list);
     notification_message(app->notify, &nm_list);
 }
 }
+
+void notify_game_over(void* ctx) {
+    PinballApp* app = (PinballApp*)ctx;
+    int n = 0;
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_delay_500;
+        nm_list[n++] = &message_note_b5;
+        nm_list[n++] = &message_delay_250;
+        nm_list[n++] = &message_note_f6;
+        nm_list[n++] = &message_delay_250;
+        nm_list[n++] = &message_sound_off;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_f6;
+        nm_list[n++] = &message_delay_100;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_f6;
+        nm_list[n++] = &message_delay_100;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_e6;
+        nm_list[n++] = &message_delay_100;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_d6;
+        nm_list[n++] = &message_delay_100;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_c6;
+        nm_list[n++] = &message_delay_1000;
+        nm_list[n++] = &message_sound_off;
+    }
+    nm_list[n] = NULL;
+    notification_message(app->notify, &nm_list);
+}
+
+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) {
+        nm_list[n++] = &message_blue_255;
+    }
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_f4;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_note_f5;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_sound_off;
+    }
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_blue_0;
+    }
+    nm_list[n] = NULL;
+    notification_message(app->notify, &nm_list);
+}
+
+void notify_rail_hit(void* ctx) {
+    PinballApp* app = (PinballApp*)ctx;
+    int n = 0;
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_d4;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_note_d5;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_sound_off;
+    }
+    nm_list[n] = NULL;
+    notification_message(app->notify, &nm_list);
+}
+
+void notify_portal(void* ctx) {
+    PinballApp* app = (PinballApp*)ctx;
+    int n = 0;
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_blue_255;
+        nm_list[n++] = &message_red_255;
+    }
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_c4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_e4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_b4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_c5;
+        nm_list[n++] = &message_delay_50;
+    }
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_blue_255;
+        nm_list[n++] = &message_red_0;
+    }
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_e4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_g4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_c5;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_e5;
+        nm_list[n++] = &message_delay_50;
+
+        nm_list[n++] = &message_sound_off;
+    }
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_blue_0;
+    }
+    nm_list[n] = NULL;
+    notification_message(app->notify, &nm_list);
+}
+
+void notify_lost_life(void* ctx) {
+    PinballApp* app = (PinballApp*)ctx;
+    int n = 0;
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_red_255;
+        nm_list[n++] = &message_green_255;
+    }
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_c5;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_c4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_b4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_b3;
+        nm_list[n++] = &message_delay_50;
+    }
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_green_0;
+    }
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_as4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_as3;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_a4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_a3;
+        nm_list[n++] = &message_delay_50;
+    }
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_green_255;
+    }
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_gs4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_gs3;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_g4;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_g4;
+        nm_list[n++] = &message_delay_50;
+
+        nm_list[n++] = &message_sound_off;
+    }
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_red_0;
+        nm_list[n++] = &message_green_0;
+    }
+    furi_assert(n < 32);
+    nm_list[n] = NULL;
+    notification_message_block(app->notify, &nm_list);
+}
+
+void notify_flipper(void* ctx) {
+    PinballApp* app = (PinballApp*)ctx;
+    int n = 0;
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_c4;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_note_cs4;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_sound_off;
+    }
+    nm_list[n] = NULL;
+    notification_message(app->notify, &nm_list);
+}
+
+/*
+Mario coin sound - ish
+        nm_list[n++] = &message_note_b5;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_delay_10;
+        nm_list[n++] = &message_sound_off;
+        nm_list[n++] = &message_note_e6;
+        nm_list[n++] = &message_delay_250;
+        nm_list[n++] = &message_delay_100;
+
+*/

+ 15 - 3
notifications.h

@@ -2,7 +2,19 @@
 
 
 #include <furi.h>
 #include <furi.h>
 #include <notification/notification.h>
 #include <notification/notification.h>
-#include "pinball0.h"
 
 
-void notify_table_bump(PinballApp* app);
-void notify_error_message(PinballApp* app);
+void notify_init();
+void notify_free();
+
+void notify_ball_released(void* ctx);
+void notify_table_bump(void* ctx);
+void notify_error_message(void* ctx);
+void notify_game_over(void* ctx);
+
+void notify_bumper_hit(void* ctx);
+void notify_rail_hit(void* ctx);
+
+void notify_portal(void* ctx);
+void notify_lost_life(void* ctx);
+
+void notify_flipper(void* ctx);

+ 60 - 72
objects.cxx

@@ -3,6 +3,7 @@
 
 
 #include "objects.h"
 #include "objects.h"
 #include "pinball0.h"
 #include "pinball0.h"
+#include "graphics.h"
 
 
 Object::Object(const Vec2& p_, float r_)
 Object::Object(const Vec2& p_, float r_)
     : p(p_)
     : p(p_)
@@ -28,7 +29,7 @@ void Object::update(float dt) {
 }
 }
 
 
 void Ball::draw(Canvas* canvas) {
 void Ball::draw(Canvas* canvas) {
-    canvas_draw_disc(canvas, p.x / 10.0f, p.y / 10.0f, r / 10.0f);
+    gfx_draw_disc(canvas, p, r);
 }
 }
 
 
 Flipper::Flipper(const Vec2& p_, Side side_, size_t size_)
 Flipper::Flipper(const Vec2& p_, Side side_, size_t size_)
@@ -39,7 +40,9 @@ Flipper::Flipper(const Vec2& p_, Side side_, size_t size_)
     , max_rotation(1.0f)
     , max_rotation(1.0f)
     , omega(4.0f)
     , omega(4.0f)
     , rotation(0.0f)
     , rotation(0.0f)
-    , powered(false) {
+    , powered(false)
+    , score(50)
+    , notification(nullptr) {
     if(side_ == Side::LEFT) {
     if(side_ == Side::LEFT) {
         rest_angle = -0.4f;
         rest_angle = -0.4f;
         sign = 1;
         sign = 1;
@@ -51,7 +54,7 @@ Flipper::Flipper(const Vec2& p_, Side side_, size_t size_)
 
 
 void Flipper::draw(Canvas* canvas) {
 void Flipper::draw(Canvas* canvas) {
     // base / pivot
     // base / pivot
-    canvas_draw_circle(canvas, p.x / 10, p.y / 10, r / 10);
+    gfx_draw_circle(canvas, p, r);
 
 
     // tip
     // tip
     float angle = rest_angle + sign * rotation;
     float angle = rest_angle + sign * rotation;
@@ -59,19 +62,19 @@ void Flipper::draw(Canvas* canvas) {
 
 
     // draw the tip
     // draw the tip
     Vec2 tip = p + dir * size;
     Vec2 tip = p + dir * size;
-    canvas_draw_circle(canvas, tip.x / 10, tip.y / 10, r / 10);
+    gfx_draw_circle(canvas, tip, r);
 
 
     // top and bottom lines
     // top and bottom lines
     Vec2 perp(-dir.y, dir.x);
     Vec2 perp(-dir.y, dir.x);
     perp.normalize();
     perp.normalize();
     Vec2 start = p + perp * r;
     Vec2 start = p + perp * r;
     Vec2 end = start + dir * size;
     Vec2 end = start + dir * size;
-    canvas_draw_line(canvas, start.x / 10, start.y / 10, end.x / 10, end.y / 10);
+    gfx_draw_line(canvas, start, end);
 
 
     perp *= -1.0f;
     perp *= -1.0f;
     start = p + perp * r;
     start = p + perp * r;
     end = start + dir * size;
     end = start + dir * size;
-    canvas_draw_line(canvas, start.x / 10, start.y / 10, end.x / 10, end.y / 10);
+    gfx_draw_line(canvas, start, end);
 }
 }
 
 
 void Flipper::update(float dt) {
 void Flipper::update(float dt) {
@@ -128,17 +131,11 @@ Vec2 Flipper::get_tip() const {
 void Polygon::draw(Canvas* canvas) {
 void Polygon::draw(Canvas* canvas) {
     if(!hidden) {
     if(!hidden) {
         for(size_t i = 0; i < points.size() - 1; i++) {
         for(size_t i = 0; i < points.size() - 1; i++) {
-            canvas_draw_line(
-                canvas,
-                points[i].x / 10,
-                points[i].y / 10,
-                points[i + 1].x / 10,
-                points[i + 1].y / 10);
-
+            gfx_draw_line(canvas, points[i], points[i + 1]);
 #ifdef DRAW_NORMALS
 #ifdef DRAW_NORMALS
             Vec2 c = (points[i] + points[i + 1]) / 2.0f;
             Vec2 c = (points[i] + points[i + 1]) / 2.0f;
             Vec2 e = c + normals[i] * 40.0f;
             Vec2 e = c + normals[i] * 40.0f;
-            canvas_draw_line(canvas, c.x / 10, c.y / 10, e.x / 10, e.y / 10);
+            gfX_draw_line(canvas, c, e);
 #endif
 #endif
         }
         }
     }
     }
@@ -176,7 +173,7 @@ bool Polygon::collide(Ball& ball) {
         dist = normal.mag();
         dist = normal.mag();
     }
     }
     dir = dir / dist;
     dir = dir / dist;
-    if(dir.dot(normal) >= 0.0f) {
+    if(ball_v.dot(normal) < 0.0f) {
         // FURI_LOG_I(TAG, "Collision Moving TOWARDS");
         // FURI_LOG_I(TAG, "Collision Moving TOWARDS");
         ball.p += dir * (ball.r - dist);
         ball.p += dir * (ball.r - dist);
     } else {
     } else {
@@ -260,49 +257,45 @@ void Portal::draw(Canvas* canvas) {
         Vec2 e;
         Vec2 e;
 
 
         // Portal A
         // Portal A
-        canvas_draw_line(canvas, a1.x / 10, a1.y / 10, a2.x / 10, a2.y / 10);
+        gfx_draw_line(canvas, a1, a2);
         d = a1 + au * amag * 0.33f;
         d = a1 + au * amag * 0.33f;
         e = d + na * 20.0f;
         e = d + na * 20.0f;
-        canvas_draw_line(canvas, d.x / 10, d.y / 10, e.x / 10, e.y / 10);
+        gfx_draw_line(canvas, d, e);
         d += au * amag * 0.33f;
         d += au * amag * 0.33f;
         e = d + na * 20.0f;
         e = d + na * 20.0f;
-        canvas_draw_line(canvas, d.x / 10, d.y / 10, e.x / 10, e.y / 10);
+        gfx_draw_line(canvas, d, e);
 
 
         // Portal B
         // Portal B
-        canvas_draw_line(canvas, b1.x / 10, b1.y / 10, b2.x / 10, b2.y / 10);
+        gfx_draw_line(canvas, b1, b2);
         d = b1 + bu * bmag * 0.33f;
         d = b1 + bu * bmag * 0.33f;
         e = d + nb * 20.0f;
         e = d + nb * 20.0f;
-        canvas_draw_line(canvas, d.x / 10, d.y / 10, e.x / 10, e.y / 10);
+        gfx_draw_line(canvas, d, e);
         d += bu * bmag * 0.33f;
         d += bu * bmag * 0.33f;
         e = d + nb * 20.0f;
         e = d + nb * 20.0f;
-        canvas_draw_line(canvas, d.x / 10, d.y / 10, e.x / 10, e.y / 10);
+        gfx_draw_line(canvas, d, e);
 
 
         if(decay > 0) {
         if(decay > 0) {
-            canvas_draw_circle(canvas, enter_p.x / 10, enter_p.y / 10, 2);
+            gfx_draw_circle(canvas, enter_p, 20);
         }
         }
     }
     }
 #ifdef DRAW_NORMALS
 #ifdef DRAW_NORMALS
     Vec2 c = (a1 + a2) / 2.0f;
     Vec2 c = (a1 + a2) / 2.0f;
     Vec2 e = c + na * 40.0f;
     Vec2 e = c + na * 40.0f;
-    canvas_draw_line(canvas, c.x / 10, c.y / 10, e.x / 10, e.y / 10);
+    gfx_draw_line(canvas, c, e);
     c = (b1 + b2) / 2.0f;
     c = (b1 + b2) / 2.0f;
     e = c + nb * 40.0f;
     e = c + nb * 40.0f;
-    canvas_draw_line(canvas, c.x / 10, c.y / 10, e.x / 10, e.y / 10);
+    gfx_draw_line(canvas, c, e);
 #endif
 #endif
 }
 }
 
 
+// TODO: simplify this code?
 bool Portal::collide(Ball& ball) {
 bool Portal::collide(Ball& ball) {
     Vec2 ball_v = ball.p - ball.prev_p;
     Vec2 ball_v = ball.p - ball.prev_p;
-    Vec2 dir;
-    Vec2 closest;
-    Vec2 normal;
     float dist;
     float dist;
 
 
     Vec2 a_cl = Vec2_closest(a1, a2, ball.p);
     Vec2 a_cl = Vec2_closest(a1, a2, ball.p);
-    dir = ball.p - a_cl;
-    dist = dir.mag();
-    dir = dir / dist;
-    if(dist <= ball.r && dir.dot(na) >= 0.0f) {
+    dist = (ball.p - a_cl).mag();
+    if(dist <= ball.r && ball_v.dot(na) < 0.0f) {
         // entering portal a! move it to portal b
         // entering portal a! move it to portal b
         // how far "along" the portal are we?
         // how far "along" the portal are we?
         enter_p = a_cl;
         enter_p = a_cl;
@@ -315,17 +308,17 @@ bool Portal::collide(Ball& ball) {
         float m = -ball_v.dot(au); // tangent magnitude
         float m = -ball_v.dot(au); // tangent magnitude
         float n = ball_v.dot(na); // normal magnitude
         float n = ball_v.dot(na); // normal magnitude
 
 
-        FURI_LOG_I(
-            TAG,
-            "v: %.3f,%.3f  u: %.3f,%.3f  n: %.3f,%.3f  M: %.3f  N: %.3f",
-            (double)ball_v.x,
-            (double)ball_v.y,
-            (double)au.x,
-            (double)au.y,
-            (double)na.x,
-            (double)na.y,
-            (double)m,
-            (double)n);
+        // FURI_LOG_I(
+        //     TAG,
+        //     "v: %.3f,%.3f  u: %.3f,%.3f  n: %.3f,%.3f  M: %.3f  N: %.3f",
+        //     (double)ball_v.x,
+        //     (double)ball_v.y,
+        //     (double)au.x,
+        //     (double)au.y,
+        //     (double)na.x,
+        //     (double)na.y,
+        //     (double)m,
+        //     (double)n);
 
 
         // transform to exit portal
         // transform to exit portal
         ball_v.x = bu.x * m - nb.x * n;
         ball_v.x = bu.x * m - nb.x * n;
@@ -337,10 +330,8 @@ bool Portal::collide(Ball& ball) {
     }
     }
 
 
     Vec2 b_cl = Vec2_closest(b1, b2, ball.p);
     Vec2 b_cl = Vec2_closest(b1, b2, ball.p);
-    dir = ball.p - b_cl;
-    dist = dir.mag();
-    dir = dir / dist;
-    if(dist <= ball.r && dir.dot(nb) >= 0.0f) {
+    dist = (ball.p - b_cl).mag();
+    if(dist <= ball.r && ball_v.dot(nb) < 0.0f) {
         // entering portal b! move it to portal a
         // entering portal b! move it to portal a
         // how far "along" the portal are we?
         // how far "along" the portal are we?
         enter_p = b_cl;
         enter_p = b_cl;
@@ -353,17 +344,17 @@ bool Portal::collide(Ball& ball) {
         float m = -ball_v.dot(bu); // tangent magnitude
         float m = -ball_v.dot(bu); // tangent magnitude
         float n = ball_v.dot(nb); // normal magnitude
         float n = ball_v.dot(nb); // normal magnitude
 
 
-        FURI_LOG_I(
-            TAG,
-            "v: %.3f,%.3f  u: %.3f,%.3f  n: %.3f,%.3f  M: %.3f  N: %.3f",
-            (double)ball_v.x,
-            (double)ball_v.y,
-            (double)bu.x,
-            (double)bu.y,
-            (double)nb.x,
-            (double)nb.y,
-            (double)m,
-            (double)n);
+        // FURI_LOG_I(
+        //     TAG,
+        //     "v: %.3f,%.3f  u: %.3f,%.3f  n: %.3f,%.3f  M: %.3f  N: %.3f",
+        //     (double)ball_v.x,
+        //     (double)ball_v.y,
+        //     (double)bu.x,
+        //     (double)bu.y,
+        //     (double)nb.x,
+        //     (double)nb.y,
+        //     (double)m,
+        //     (double)n);
 
 
         // transform to exit portal
         // transform to exit portal
         ball_v.x = au.x * m - na.x * n;
         ball_v.x = au.x * m - na.x * n;
@@ -409,7 +400,7 @@ Arc::Arc(const Vec2& p_, float r_, float s_, float e_, Surface surf_)
 
 
 void Arc::draw(Canvas* canvas) {
 void Arc::draw(Canvas* canvas) {
     if(start == 0 && end == (float)M_PI * 2) {
     if(start == 0 && end == (float)M_PI * 2) {
-        canvas_draw_circle(canvas, p.x / 10.0f, p.y / 10.0f, r / 10.0f);
+        gfx_draw_circle(canvas, p, r);
     } else {
     } else {
         float adj_end = end;
         float adj_end = end;
         if(end < start) {
         if(end < start) {
@@ -422,8 +413,7 @@ void Arc::draw(Canvas* canvas) {
         for(size_t i = 1; i <= segments; i++) { // for now, use r to determin number of segments
         for(size_t i = 1; i <= segments; i++) { // for now, use r to determin number of segments
             float nx = p.x + r * cosf(start + i / (segments / (adj_end - start)));
             float nx = p.x + r * cosf(start + i / (segments / (adj_end - start)));
             float ny = p.y - r * sinf(start + i / (segments / (adj_end - start)));
             float ny = p.y - r * sinf(start + i / (segments / (adj_end - start)));
-            canvas_draw_line(
-                canvas, roundf(sx / 10), roundf(sy / 10), roundf(nx / 10), roundf(ny / 10));
+            gfx_draw_line(canvas, sx, sy, nx, ny);
             sx = nx;
             sx = nx;
             sy = ny;
             sy = ny;
         }
         }
@@ -487,7 +477,7 @@ bool Arc::collide(Ball& ball) {
         if(prev_dist < r && dist + ball.r > r) {
         if(prev_dist < r && dist + ball.r > r) {
             // FURI_LOG_I(TAG, "Inside an arc!");
             // FURI_LOG_I(TAG, "Inside an arc!");
             float angle = vector_to_angle(dir.x, -dir.y);
             float angle = vector_to_angle(dir.x, -dir.y);
-            FURI_LOG_I(TAG, "%f : %f : %f", (double)start, (double)angle, (double)end);
+            // FURI_LOG_I(TAG, "%f : %f : %f", (double)start, (double)angle, (double)end);
             // if(angle >= start && angle <= end) {
             // if(angle >= start && angle <= end) {
             if((start < end && start <= angle && angle <= end) ||
             if((start < end && start <= angle && angle <= end) ||
                (start > end && (angle >= start || angle <= end))) {
                (start > end && (angle >= start || angle <= end))) {
@@ -522,12 +512,14 @@ bool Arc::collide(Ball& ball) {
 
 
 Bumper::Bumper(const Vec2& p_, float r_)
 Bumper::Bumper(const Vec2& p_, float r_)
     : Arc(p_, r_) {
     : Arc(p_, r_) {
+    score = 500;
 }
 }
 
 
 void Bumper::draw(Canvas* canvas) {
 void Bumper::draw(Canvas* canvas) {
     Arc::draw(canvas);
     Arc::draw(canvas);
     if(decay) {
     if(decay) {
-        canvas_draw_disc(canvas, p.x / 10, p.y / 10, (r / 10) * 0.8f * (decay / 30.0f));
+        // canvas_draw_disc(canvas, p.x / 10, p.y / 10, (r / 10) * 0.8f * (decay / 30.0f));
+        gfx_draw_disc(canvas, p, r * 0.8f * (decay / 30.0f));
     }
     }
 }
 }
 void Bumper::reset_animation() {
 void Bumper::reset_animation() {
@@ -546,7 +538,7 @@ void Rollover::draw(Canvas* canvas) {
     if(activated) {
     if(activated) {
         canvas_draw_str_aligned(canvas, p.x / 10, p.y / 10, AlignCenter, AlignCenter, c);
         canvas_draw_str_aligned(canvas, p.x / 10, p.y / 10, AlignCenter, AlignCenter, c);
     } else {
     } else {
-        canvas_draw_dot(canvas, p.x / 10, p.y / 10);
+        gfx_draw_dot(canvas, p);
     }
     }
 }
 }
 
 
@@ -560,15 +552,11 @@ bool Rollover::collide(Ball& ball) {
 }
 }
 
 
 void Turbo::draw(Canvas* canvas) {
 void Turbo::draw(Canvas* canvas) {
-    canvas_draw_line(
-        canvas, chevron_1[0].x / 10, chevron_1[0].y / 10, chevron_1[1].x / 10, chevron_1[1].y / 10);
-    canvas_draw_line(
-        canvas, chevron_1[1].x / 10, chevron_1[1].y / 10, chevron_1[2].x / 10, chevron_1[2].y / 10);
-
-    canvas_draw_line(
-        canvas, chevron_2[0].x / 10, chevron_2[0].y / 10, chevron_2[1].x / 10, chevron_2[1].y / 10);
-    canvas_draw_line(
-        canvas, chevron_2[1].x / 10, chevron_2[1].y / 10, chevron_2[2].x / 10, chevron_2[2].y / 10);
+    gfx_draw_line(canvas, chevron_1[0], chevron_1[1]);
+    gfx_draw_line(canvas, chevron_1[1], chevron_1[2]);
+
+    gfx_draw_line(canvas, chevron_2[0], chevron_2[1]);
+    gfx_draw_line(canvas, chevron_2[1], chevron_2[2]);
 }
 }
 
 
 bool Turbo::collide(Ball& ball) {
 bool Turbo::collide(Ball& ball) {

+ 12 - 2
objects.h

@@ -42,7 +42,7 @@ public:
 
 
 class Ball : public Object {
 class Ball : public Object {
 public:
 public:
-    Ball(const Vec2& p_, float r_ = DEF_BALL_RADIUS)
+    Ball(const Vec2& p_ = Vec2(), float r_ = DEF_BALL_RADIUS)
         : Object(p_, r_) {
         : Object(p_, r_) {
     }
     }
     void draw(Canvas* canvas);
     void draw(Canvas* canvas);
@@ -77,6 +77,9 @@ public:
     float current_omega;
     float current_omega;
 
 
     bool powered; // is this flipper being activated? i.e. is keypad pressed?
     bool powered; // is this flipper being activated? i.e. is keypad pressed?
+
+    int score;
+    void (*notification)(void* app);
 };
 };
 
 
 // A static object that never moves and can be any shape
 // A static object that never moves and can be any shape
@@ -85,13 +88,18 @@ public:
     FixedObject()
     FixedObject()
         : bounce(1.0f)
         : bounce(1.0f)
         , physical(true)
         , physical(true)
-        , hidden(false) {
+        , hidden(false)
+        , score(0)
+        , notification(nullptr) {
     }
     }
     virtual ~FixedObject() = default;
     virtual ~FixedObject() = default;
 
 
     float bounce;
     float bounce;
     bool physical; // can be hit
     bool physical; // can be hit
     bool hidden; // do not draw
     bool hidden; // do not draw
+    int score;
+
+    void (*notification)(void* app);
 
 
     virtual void draw(Canvas* canvas) = 0;
     virtual void draw(Canvas* canvas) = 0;
     virtual bool collide(Ball& ball) = 0;
     virtual bool collide(Ball& ball) = 0;
@@ -123,6 +131,7 @@ public:
         , a2(a2_)
         , a2(a2_)
         , b1(b1_)
         , b1(b1_)
         , b2(b2_) {
         , b2(b2_) {
+        score = 200;
     }
     }
     Vec2 a1, a2; // portal 'a'
     Vec2 a1, a2; // portal 'a'
     Vec2 b1, b2; // portal 'b'
     Vec2 b1, b2; // portal 'b'
@@ -193,6 +202,7 @@ public:
         , p(p_) {
         , p(p_) {
         c[0] = c_;
         c[0] = c_;
         c[1] = '\0';
         c[1] = '\0';
+        score = 400;
     }
     }
 
 
     Vec2 p;
     Vec2 p;

+ 100 - 47
pinball0.cxx

@@ -31,9 +31,9 @@ void pinball_load_settings(PinballApp* pb) {
     // init the settings to default values, then overwrite them if
     // init the settings to default values, then overwrite them if
     // they appear in the settings file
     // they appear in the settings file
     pb->settings.sound_enabled = true;
     pb->settings.sound_enabled = true;
-    pb->settings.led_enabled = false;
-    pb->settings.vibrate_enabled = false;
-    pb->settings.manual_mode = true;
+    pb->settings.led_enabled = true;
+    pb->settings.vibrate_enabled = true;
+    pb->settings.manual_mode = false;
     pb->selected_setting = 0;
     pb->selected_setting = 0;
     pb->max_settings = 4;
     pb->max_settings = 4;
 
 
@@ -46,7 +46,12 @@ void pinball_load_settings(PinballApp* pb) {
             FURI_LOG_E(TAG, "SETTINGS: Missing or incorrect header");
             FURI_LOG_E(TAG, "SETTINGS: Missing or incorrect header");
             break;
             break;
         }
         }
-        // do settings file version here? eh..
+        if(!strcmp(furi_string_get_cstr(tmp_str), PINBALL_SETTINGS_FILE_TYPE) &&
+           (tmp_data32 == PINBALL_SETTINGS_FILE_VERSION)) {
+        } else {
+            FURI_LOG_E(TAG, "SETTINGS: Type or version mismatch");
+            break;
+        }
         if(flipper_format_read_uint32(fff_settings, "Sound", &tmp_data32, 1)) {
         if(flipper_format_read_uint32(fff_settings, "Sound", &tmp_data32, 1)) {
             pb->settings.sound_enabled = (tmp_data32 == 0) ? false : true;
             pb->settings.sound_enabled = (tmp_data32 == 0) ? false : true;
         }
         }
@@ -121,6 +126,7 @@ void solve(PinballApp* pb, float dt) {
             }
             }
             for(auto& b : table->balls) {
             for(auto& b : table->balls) {
                 // We multiply GRAVITY by dt since gravity is based on seconds
                 // We multiply GRAVITY by dt since gravity is based on seconds
+                FURI_LOG_I(TAG, "GRAVI-TAYYY");
                 b.accelerate(Vec2(0, GRAVITY * bump_amt * sub_dt));
                 b.accelerate(Vec2(0, GRAVITY * bump_amt * sub_dt));
             }
             }
         }
         }
@@ -165,12 +171,20 @@ void solve(PinballApp* pb, float dt) {
         for(auto& b : table->balls) {
         for(auto& b : table->balls) {
             for(auto& o : table->objects) {
             for(auto& o : table->objects) {
                 if(o->physical && o->collide(b)) {
                 if(o->physical && o->collide(b)) {
+                    if(o->notification) {
+                        (*o->notification)(pb);
+                    }
+                    table->score.value += o->score;
                     o->reset_animation();
                     o->reset_animation();
                     continue;
                     continue;
                 }
                 }
             }
             }
             for(auto& f : table->flippers) {
             for(auto& f : table->flippers) {
                 if(f.collide(b)) {
                 if(f.collide(b)) {
+                    if(f.notification) {
+                        (*f.notification)(pb);
+                    }
+                    table->score.value += f.score;
                     continue;
                     continue;
                 }
                 }
             }
             }
@@ -188,22 +202,28 @@ void solve(PinballApp* pb, float dt) {
     }
     }
 
 
     // Did any balls fall off the table?
     // Did any balls fall off the table?
-    size_t num_in_play = table->balls.size();
-    auto i = table->balls.begin();
-    while(i != table->balls.end()) {
-        if(i->p.y > 1280 + 100) {
-            FURI_LOG_I(TAG, "ball off table!");
-            i = table->balls.erase(i);
-            num_in_play--;
-        } else {
-            ++i;
+    if(table->balls.size()) {
+        auto num_in_play = table->balls.size();
+        auto i = table->balls.begin();
+        while(i != table->balls.end()) {
+            if(i->p.y > 1280 + 100) {
+                FURI_LOG_I(TAG, "ball off table!");
+                i = table->balls.erase(i);
+                num_in_play--;
+                notify_lost_life(pb);
+            } else {
+                ++i;
+            }
         }
         }
-    }
-    if(num_in_play == 0) {
-        table->balls_released = false;
-        table->num_lives--;
-        if(table->num_lives > 0) {
-            table->balls = table->balls_initial;
+        if(num_in_play == 0) {
+            table->balls_released = false;
+            table->lives.value--;
+            if(table->lives.value > 0) {
+                // Reset our ball to it's starting position
+                table->balls = table->balls_initial;
+            } else {
+                table->game_over = true;
+            }
         }
         }
     }
     }
 }
 }
@@ -212,6 +232,7 @@ void pinball_app_init(PinballApp* pb) {
     furi_assert(pb);
     furi_assert(pb);
     pb->storage = (Storage*)furi_record_open(RECORD_STORAGE);
     pb->storage = (Storage*)furi_record_open(RECORD_STORAGE);
     pb->notify = (NotificationApp*)furi_record_open(RECORD_NOTIFICATION);
     pb->notify = (NotificationApp*)furi_record_open(RECORD_NOTIFICATION);
+    notify_init();
 
 
     pb->table = NULL;
     pb->table = NULL;
     pb->tick = 0;
     pb->tick = 0;
@@ -269,23 +290,28 @@ static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
         pb->table->draw(canvas);
         pb->table->draw(canvas);
         break;
         break;
     case GM_GameOver: {
     case GM_GameOver: {
-        // pb->table->draw(canvas);
-        int32_t y = 56;
-        size_t interval = 40;
-        float theta = (float)((pb->tick % interval) / (interval * 1.0f)) * (float)(M_PI * 2);
-        FURI_LOG_I(TAG, "tick: %lu, theta: %.4f", pb->tick, (double)theta);
-        // float theta_offset = (float)((pb->tick % 16) / 16.0);
-        float theta_offset = 0;
-        // int32_t y_offset = y + sinf(theta) * 4;
-        canvas_draw_icon(canvas, 16, y + sinf(theta) * 4, &I_Arcade_G);
-        canvas_draw_icon(canvas, 24, y + sinf(theta + theta_offset) * 4, &I_Arcade_A);
-        canvas_draw_icon(canvas, 32, y + sinf(theta + theta_offset * 2) * 4, &I_Arcade_M);
-        canvas_draw_icon(canvas, 40, y + sinf(theta + theta_offset * 3) * 4, &I_Arcade_E);
-
-        canvas_draw_icon(canvas, 16, y + sinf(theta) * 4 + 8, &I_Arcade_O);
-        canvas_draw_icon(canvas, 24, y + sinf(theta + theta_offset) * 4 + 8, &I_Arcade_V);
-        canvas_draw_icon(canvas, 32, y + sinf(theta + theta_offset * 2) * 4 + 8, &I_Arcade_E);
-        canvas_draw_icon(canvas, 40, y + sinf(theta + theta_offset * 3) * 4 + 8, &I_Arcade_R);
+        pb->table->draw(canvas);
+
+        const int32_t y = 56;
+        const size_t interval = 40;
+        const float theta = (float)((pb->tick % interval) / (interval * 1.0f)) * (float)(M_PI * 2);
+        const float sin_theta_4 = sinf(theta) * 4;
+
+        const int border = 3;
+        canvas_set_color(canvas, ColorWhite);
+        canvas_draw_box(
+            canvas, 16 - border, y + sin_theta_4 - border, 32 + border * 2, 16 + border * 2);
+        canvas_set_color(canvas, ColorBlack);
+
+        canvas_draw_icon(canvas, 16, y + sin_theta_4, &I_Arcade_G);
+        canvas_draw_icon(canvas, 24, y + sin_theta_4, &I_Arcade_A);
+        canvas_draw_icon(canvas, 32, y + sin_theta_4, &I_Arcade_M);
+        canvas_draw_icon(canvas, 40, y + sin_theta_4, &I_Arcade_E);
+
+        canvas_draw_icon(canvas, 16, y + sin_theta_4 + 8, &I_Arcade_O);
+        canvas_draw_icon(canvas, 24, y + sin_theta_4 + 8, &I_Arcade_V);
+        canvas_draw_icon(canvas, 32, y + sin_theta_4 + 8, &I_Arcade_E);
+        canvas_draw_icon(canvas, 40, y + sin_theta_4 + 8, &I_Arcade_R);
     } break;
     } break;
     case GM_Error: {
     case GM_Error: {
         // pb->text contains error message
         // pb->text contains error message
@@ -322,6 +348,7 @@ static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
         pb->table->draw(canvas);
         pb->table->draw(canvas);
     } break;
     } break;
     case GM_Settings: {
     case GM_Settings: {
+        // TODO: like... do better here. maybe vector of settings strings, etc
         canvas_draw_str_aligned(canvas, 2, 10, AlignLeft, AlignTop, "SETTINGS");
         canvas_draw_str_aligned(canvas, 2, 10, AlignLeft, AlignTop, "SETTINGS");
 
 
         int x = 55;
         int x = 55;
@@ -366,6 +393,12 @@ static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
             canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
             canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
         }
         }
 
 
+        // About information
+        canvas_draw_str_aligned(canvas, 2, 88, AlignLeft, AlignTop, "Pinball0 " VERSION);
+        canvas_draw_str_aligned(canvas, 2, 98, AlignLeft, AlignTop, "github.com/");
+        canvas_draw_str_aligned(canvas, 2, 108, AlignLeft, AlignTop, "  rdefeo/");
+        canvas_draw_str_aligned(canvas, 2, 118, AlignLeft, AlignTop, "    pinball0");
+
         pb->table->draw(canvas);
         pb->table->draw(canvas);
     } break;
     } break;
     default:
     default:
@@ -383,14 +416,20 @@ static void pinball_input_callback(InputEvent* input_event, void* ctx) {
     furi_message_queue_put(event_queue, &event, FuriWaitForever);
     furi_message_queue_put(event_queue, &event, FuriWaitForever);
 }
 }
 
 
+PinballApp::~PinballApp() {
+    furi_mutex_free(mutex);
+    delete table;
+    notify_free();
+}
+
 extern "C" int32_t pinball0_app(void* p) {
 extern "C" int32_t pinball0_app(void* p) {
     UNUSED(p);
     UNUSED(p);
 
 
-    PinballApp* app = (PinballApp*)malloc(sizeof(PinballApp));
+    PinballApp* app = (PinballApp*)new PinballApp;
     app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
     app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
     if(!app->mutex) {
     if(!app->mutex) {
         FURI_LOG_E(TAG, "Cannot create mutex!");
         FURI_LOG_E(TAG, "Cannot create mutex!");
-        free(app);
+        delete app;
         return 0;
         return 0;
     }
     }
 
 
@@ -447,32 +486,44 @@ extern "C" int32_t pinball0_app(void* p) {
                             app->processing = false;
                             app->processing = false;
                         }
                         }
                         break;
                         break;
-                    case InputKeyRight:
+                    case InputKeyRight: {
                         app->keys[InputKeyRight] = true;
                         app->keys[InputKeyRight] = true;
 
 
                         if(app->settings.manual_mode && app->table->balls_released == false) {
                         if(app->settings.manual_mode && app->table->balls_released == false) {
                             app->table->balls[0].p.x += MANUAL_ADJUSTMENT;
                             app->table->balls[0].p.x += MANUAL_ADJUSTMENT;
                             app->table->balls[0].prev_p.x += MANUAL_ADJUSTMENT;
                             app->table->balls[0].prev_p.x += MANUAL_ADJUSTMENT;
                         }
                         }
+                        bool flipper_pressed = false;
                         for(auto& f : app->table->flippers) {
                         for(auto& f : app->table->flippers) {
                             if(f.side == Flipper::RIGHT) {
                             if(f.side == Flipper::RIGHT) {
                                 f.powered = true;
                                 f.powered = true;
+                                flipper_pressed = true;
                             }
                             }
                         }
                         }
-                        break;
-                    case InputKeyLeft:
+                        if(flipper_pressed) {
+                            notify_flipper(app);
+                        }
+                    } break;
+                    case InputKeyLeft: {
                         app->keys[InputKeyLeft] = true;
                         app->keys[InputKeyLeft] = true;
 
 
                         if(app->settings.manual_mode && app->table->balls_released == false) {
                         if(app->settings.manual_mode && app->table->balls_released == false) {
                             app->table->balls[0].p.x -= MANUAL_ADJUSTMENT;
                             app->table->balls[0].p.x -= MANUAL_ADJUSTMENT;
                             app->table->balls[0].prev_p.x -= MANUAL_ADJUSTMENT;
                             app->table->balls[0].prev_p.x -= MANUAL_ADJUSTMENT;
                         }
                         }
+                        bool flipper_pressed = false;
                         for(auto& f : app->table->flippers) {
                         for(auto& f : app->table->flippers) {
                             if(f.side == Flipper::LEFT) {
                             if(f.side == Flipper::LEFT) {
                                 f.powered = true;
                                 f.powered = true;
+                                if(f.rotation != f.max_rotation) {
+                                    flipper_pressed = true;
+                                }
                             }
                             }
                         }
                         }
-                        break;
+                        if(flipper_pressed) {
+                            notify_flipper(app);
+                        }
+                    } break;
                     case InputKeyUp:
                     case InputKeyUp:
                         switch(app->game_mode) {
                         switch(app->game_mode) {
                         case GM_Playing:
                         case GM_Playing:
@@ -514,6 +565,7 @@ extern "C" int32_t pinball0_app(void* p) {
                             app->table_list.selected = (app->table_list.selected + 1 +
                             app->table_list.selected = (app->table_list.selected + 1 +
                                                         app->table_list.menu_items.size()) %
                                                         app->table_list.menu_items.size()) %
                                                        app->table_list.menu_items.size();
                                                        app->table_list.menu_items.size();
+                            // notify_game_over(app);
                             break;
                             break;
                         case GM_Settings:
                         case GM_Settings:
                             if(app->selected_setting < app->max_settings - 1) {
                             if(app->selected_setting < app->max_settings - 1) {
@@ -530,6 +582,7 @@ extern "C" int32_t pinball0_app(void* p) {
                             if(!app->table->balls_released) {
                             if(!app->table->balls_released) {
                                 app->gameStarted = true;
                                 app->gameStarted = true;
                                 app->table->balls_released = true;
                                 app->table->balls_released = true;
+                                notify_ball_released(app);
                             }
                             }
                             break;
                             break;
                         case GM_TableSelect: {
                         case GM_TableSelect: {
@@ -608,9 +661,11 @@ extern "C" int32_t pinball0_app(void* p) {
             o->step_animation();
             o->step_animation();
         }
         }
         // check game state
         // check game state
-        if(app->game_mode == GM_Playing && app->table->num_lives == 0) {
+        // if(app->game_mode == GM_Playing && app->table->lives.value == 0) {
+        if(app->game_mode != GM_GameOver && app->table->game_over) {
             FURI_LOG_W(TAG, "GAME OVER!");
             FURI_LOG_W(TAG, "GAME OVER!");
             app->game_mode = GM_GameOver;
             app->game_mode = GM_GameOver;
+            notify_game_over(app);
         }
         }
 
 
         // no keys pressed - we should clear all input keys?
         // no keys pressed - we should clear all input keys?
@@ -630,6 +685,7 @@ extern "C" int32_t pinball0_app(void* p) {
 
 
     // general cleanup
     // general cleanup
     notification_message(app->notify, &sequence_display_backlight_enforce_auto);
     notification_message(app->notify, &sequence_display_backlight_enforce_auto);
+    notification_message(app->notify, &sequence_reset_rgb);
 
 
     view_port_enabled_set(view_port, false);
     view_port_enabled_set(view_port, false);
     gui_remove_view_port(gui, view_port);
     gui_remove_view_port(gui, view_port);
@@ -639,10 +695,7 @@ extern "C" int32_t pinball0_app(void* p) {
     view_port_free(view_port);
     view_port_free(view_port);
     furi_message_queue_free(event_queue);
     furi_message_queue_free(event_queue);
 
 
-    furi_mutex_free(app->mutex);
-
-    delete app->table;
-    free(app);
+    delete app;
 
 
     furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
     furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
     return 0;
     return 0;

+ 6 - 3
pinball0.h

@@ -19,7 +19,8 @@
 
 
 // #define DRAW_NORMALS
 // #define DRAW_NORMALS
 
 
-#define TAG "Pinball0"
+#define TAG     "Pinball0"
+#define VERSION "v0.2"
 
 
 // Vertical orientation
 // Vertical orientation
 #define LCD_WIDTH  64
 #define LCD_WIDTH  64
@@ -44,7 +45,9 @@ typedef enum GameMode {
     GM_About
     GM_About
 } GameMode;
 } GameMode;
 
 
-typedef struct {
+typedef struct PinballApp {
+    ~PinballApp();
+
     FuriMutex* mutex;
     FuriMutex* mutex;
 
 
     TableList table_list;
     TableList table_list;
@@ -55,7 +58,7 @@ typedef struct {
 
 
     bool gameStarted;
     bool gameStarted;
     bool keys[4]; // which key was pressed?
     bool keys[4]; // which key was pressed?
-    bool processing; // controls game loop and physics threads
+    bool processing; // controls game loop and game objects
 
 
     // user settings
     // user settings
     struct {
     struct {

+ 189 - 84
table.cxx

@@ -6,12 +6,40 @@
 
 
 #include "nxjson/nxjson.h"
 #include "nxjson/nxjson.h"
 #include "pinball0.h"
 #include "pinball0.h"
+#include "graphics.h"
 #include "table.h"
 #include "table.h"
+#include "notifications.h"
 
 
 // Table defaults
 // Table defaults
 #define LIVES     3
 #define LIVES     3
 #define LIVES_POS Vec2(20, 20)
 #define LIVES_POS Vec2(20, 20)
 
 
+bool ON_TABLE(const Vec2& p) {
+    return 0 <= p.x && p.x <= 630 && 0 <= p.y && p.y <= 1270;
+}
+
+void Lives::draw(Canvas* canvas) {
+    // we don't draw the last one, as it's in play!
+    constexpr float r = 20;
+    if(display && value > 0) {
+        float x = p.x;
+        float y = p.y;
+        float x_off = alignment == Align::Horizontal ? (2 * r) + r : 0;
+        float y_off = alignment == Align::Vertical ? (2 * r) + r : 0;
+        for(auto l = 0; l < value - 1; x += x_off, y += y_off, l++) {
+            gfx_draw_disc(canvas, x + r, y + r, 20);
+        }
+    }
+}
+
+void Score::draw(Canvas* canvas) {
+    if(display) {
+        char buf[32];
+        snprintf(buf, 32, "%d", value);
+        gfx_draw_str(canvas, p.x, p.y, AlignRight, AlignTop, buf);
+    }
+}
+
 Table::~Table() {
 Table::~Table() {
     for(size_t i = 0; i < objects.size(); i++) {
     for(size_t i = 0; i < objects.size(); i++) {
         delete objects[i];
         delete objects[i];
@@ -22,6 +50,8 @@ Table::~Table() {
 }
 }
 
 
 void Table::draw(Canvas* canvas) {
 void Table::draw(Canvas* canvas) {
+    lives.draw(canvas);
+
     // da balls
     // da balls
     for(auto& b : balls) {
     for(auto& b : balls) {
         b.draw(canvas);
         b.draw(canvas);
@@ -40,19 +70,7 @@ void Table::draw(Canvas* canvas) {
         plunger->draw(canvas);
         plunger->draw(canvas);
     }
     }
 
 
-    // we don't draw the last one, as it's in play!
-    if(num_lives > 0) {
-        float x = num_lives_pos.x;
-        float y = num_lives_pos.y;
-        for(size_t l = 0; l < num_lives - 1; x += (2 * 20) + 20, l++) {
-            canvas_draw_disc(canvas, x / 10, y / 10, 2);
-        }
-    }
-
-    // score
-    // char buf[32];
-    // snprintf(buf, 32, "%8u", score);
-    // canvas_draw_str_aligned(canvas, LCD_WIDTH - 30, 1, AlignLeft, AlignTop, buf);
+    score.draw(canvas);
 }
 }
 
 
 void table_table_list_init(void* ctx) {
 void table_table_list_init(void* ctx) {
@@ -129,30 +147,36 @@ void table_table_list_init(void* ctx) {
 }
 }
 
 
 // json parse helper function
 // json parse helper function
-bool table_file_parse_vec2(const nx_json* json, const char* key, Vec2* v) {
-    furi_assert(v);
+bool table_file_parse_vec2(const nx_json* json, const char* key, Vec2& v) {
     const nx_json* item = nx_json_get(json, key);
     const nx_json* item = nx_json_get(json, key);
     if(!item || item->children.length != 2) {
     if(!item || item->children.length != 2) {
         return false;
         return false;
     }
     }
-    v->x = nx_json_item(item, 0)->num.dbl_value;
-    v->y = nx_json_item(item, 1)->num.dbl_value;
+    v.x = nx_json_item(item, 0)->num.dbl_value;
+    v.y = nx_json_item(item, 1)->num.dbl_value;
     return true;
     return true;
 }
 }
 
 
-bool table_file_parse_int(const nx_json* json, const char* key, int* v) {
-    furi_assert(v);
+bool table_file_parse_int(const nx_json* json, const char* key, int& v) {
     const nx_json* item = nx_json_get(json, key);
     const nx_json* item = nx_json_get(json, key);
     if(!item) return false;
     if(!item) return false;
-    *v = item->num.u_value;
+    v = item->num.u_value;
     return true;
     return true;
 }
 }
 
 
-bool table_file_parse_float(const nx_json* json, const char* key, float* v) {
-    furi_assert(v);
+bool table_file_parse_bool(const nx_json* json, const char* key, bool& v) {
+    int value = v == true ? 1 : 0; // set default value
+    if(table_file_parse_int(json, key, value)) {
+        v = value > 0 ? true : false;
+        return true;
+    }
+    return false;
+}
+
+bool table_file_parse_float(const nx_json* json, const char* key, float& v) {
     const nx_json* item = nx_json_get(json, key);
     const nx_json* item = nx_json_get(json, key);
     if(!item) return false;
     if(!item) return false;
-    *v = item->num.dbl_value;
+    v = item->num.dbl_value;
     return true;
     return true;
 }
 }
 
 
@@ -227,39 +251,47 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
 
 
     Table* table = new Table();
     Table* table = new Table();
 
 
-    int lives = LIVES;
-    table_file_parse_int(json, "lives", &lives);
-    table->num_lives = lives;
-
-    table->num_lives_pos = LIVES_POS;
-    table_file_parse_vec2(json, "lives_position", &table->num_lives_pos);
-
-    // TODO: this should be an attribute of balls?
-    table->balls_released = false;
-    const nx_json* released = nx_json_get(json, "released");
-    if(released) {
-        table->balls_released = released->num.u_value > 0;
-    }
-
     do {
     do {
+        const nx_json* lives = nx_json_get(json, "lives");
+        if(lives) {
+            table_file_parse_int(lives, "value", table->lives.value);
+            table_file_parse_bool(lives, "display", table->lives.display);
+            table_file_parse_vec2(lives, "position", table->lives.p);
+            const nx_json* align = nx_json_get(lives, "align");
+            if(align && !strcmp(align->text_value, "VERTICAL")) {
+                table->lives.alignment = Lives::Vertical;
+            }
+        }
+        const nx_json* score = nx_json_get(json, "score");
+        if(score) {
+            table_file_parse_bool(score, "display", table->score.display);
+            table_file_parse_vec2(score, "position", table->score.p);
+        }
+
         const nx_json* balls = nx_json_get(json, "balls");
         const nx_json* balls = nx_json_get(json, "balls");
         if(balls) {
         if(balls) {
             for(int i = 0; i < balls->children.length; i++) {
             for(int i = 0; i < balls->children.length; i++) {
                 const nx_json* ball = nx_json_item(balls, i);
                 const nx_json* ball = nx_json_item(balls, i);
+                if(!ball) continue;
 
 
                 Vec2 p;
                 Vec2 p;
-                if(!table_file_parse_vec2(ball, "position", &p)) {
+                if(!table_file_parse_vec2(ball, "position", p)) {
                     FURI_LOG_E(TAG, "Ball missing \"position\", skipping");
                     FURI_LOG_E(TAG, "Ball missing \"position\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(p)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Ball with position %.1f,%.1f is not on table!",
+                        (double)p.x,
+                        (double)p.y);
+                }
 
 
-                int r = DEF_BALL_RADIUS;
-                table_file_parse_int(ball, "radius", &r);
+                Ball new_ball(p);
+                table_file_parse_float(ball, "radius", new_ball.r);
 
 
                 Vec2 v = (Vec2){0, 0};
                 Vec2 v = (Vec2){0, 0};
-                table_file_parse_vec2(ball, "velocity", &v);
-
-                Ball new_ball(p, r);
+                table_file_parse_vec2(ball, "velocity", v);
                 new_ball.accelerate(v);
                 new_ball.accelerate(v);
 
 
                 table->balls_initial.push_back(new_ball);
                 table->balls_initial.push_back(new_ball);
@@ -278,12 +310,13 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
         const nx_json* plunger = nx_json_get(json, "plunger");
         const nx_json* plunger = nx_json_get(json, "plunger");
         if(plunger) {
         if(plunger) {
             Vec2 p;
             Vec2 p;
-            table_file_parse_vec2(plunger, "position", &p);
+            table_file_parse_vec2(plunger, "position", p);
             int s = 100;
             int s = 100;
-            table_file_parse_int(plunger, "size", &s);
+            table_file_parse_int(plunger, "size", s);
             table->plunger = new Plunger(p);
             table->plunger = new Plunger(p);
         } else {
         } else {
-            FURI_LOG_E(TAG, "Table has NO PLUNGER");
+            FURI_LOG_W(
+                TAG, "Table has NO PLUNGER - s'ok, we don't really support one anyway (yet)");
         }
         }
 
 
         const nx_json* flippers = nx_json_get(json, "flippers");
         const nx_json* flippers = nx_json_get(json, "flippers");
@@ -292,10 +325,17 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 const nx_json* flipper = nx_json_item(flippers, i);
                 const nx_json* flipper = nx_json_item(flippers, i);
 
 
                 Vec2 p;
                 Vec2 p;
-                if(!table_file_parse_vec2(flipper, "position", &p)) {
+                if(!table_file_parse_vec2(flipper, "position", p)) {
                     FURI_LOG_E(TAG, "Flipper missing \"position\", skipping");
                     FURI_LOG_E(TAG, "Flipper missing \"position\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(p)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Flipper with position %.1f,%.1f is not on table!",
+                        (double)p.x,
+                        (double)p.y);
+                }
 
 
                 const nx_json* side = nx_json_get(flipper, "side");
                 const nx_json* side = nx_json_get(flipper, "side");
                 Flipper::Side sd = Flipper::LEFT;
                 Flipper::Side sd = Flipper::LEFT;
@@ -304,8 +344,9 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 }
                 }
 
 
                 int sz = DEF_FLIPPER_SIZE;
                 int sz = DEF_FLIPPER_SIZE;
-                table_file_parse_int(flipper, "size", &sz);
+                table_file_parse_int(flipper, "size", sz);
                 Flipper flip(p, sd, sz);
                 Flipper flip(p, sd, sz);
+                // flip.notification = &notify_flipper;
                 table->flippers.push_back(flip);
                 table->flippers.push_back(flip);
             }
             }
         }
         }
@@ -316,20 +357,27 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 const nx_json* bumper = nx_json_item(bumpers, i);
                 const nx_json* bumper = nx_json_item(bumpers, i);
 
 
                 Vec2 p;
                 Vec2 p;
-                if(!table_file_parse_vec2(bumper, "position", &p)) {
+                if(!table_file_parse_vec2(bumper, "position", p)) {
                     FURI_LOG_E(TAG, "Bumper missing \"position\", skipping");
                     FURI_LOG_E(TAG, "Bumper missing \"position\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(p)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Bumper with position %.1f,%.1f is not on table!",
+                        (double)p.x,
+                        (double)p.y);
+                }
 
 
                 int r = DEF_BUMPER_RADIUS;
                 int r = DEF_BUMPER_RADIUS;
-                table_file_parse_int(bumper, "radius", &r);
+                table_file_parse_int(bumper, "radius", r);
 
 
                 float bnc = DEF_BUMPER_BOUNCE;
                 float bnc = DEF_BUMPER_BOUNCE;
-                table_file_parse_float(bumper, "bounce", &bnc);
+                table_file_parse_float(bumper, "bounce", bnc);
 
 
                 Bumper* new_bumper = new Bumper(p, r);
                 Bumper* new_bumper = new Bumper(p, r);
-                FURI_LOG_I(TAG, "new bumper: %.3f,%.3f", (double)p.x, (double)p.y);
                 new_bumper->bounce = bnc;
                 new_bumper->bounce = bnc;
+                new_bumper->notification = notify_bumper_hit;
                 table->objects.push_back(new_bumper);
                 table->objects.push_back(new_bumper);
             }
             }
         }
         }
@@ -341,22 +389,29 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 const nx_json* arc = nx_json_item(arcs, i);
                 const nx_json* arc = nx_json_item(arcs, i);
 
 
                 Vec2 p;
                 Vec2 p;
-                if(!table_file_parse_vec2(arc, "position", &p)) {
+                if(!table_file_parse_vec2(arc, "position", p)) {
                     FURI_LOG_E(TAG, "Arc missing \"position\"");
                     FURI_LOG_E(TAG, "Arc missing \"position\"");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(p)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Arc with position %.1f,%.1f is not on table!",
+                        (double)p.x,
+                        (double)p.y);
+                }
 
 
                 int r = DEF_BUMPER_RADIUS;
                 int r = DEF_BUMPER_RADIUS;
-                table_file_parse_int(arc, "radius", &r);
+                table_file_parse_int(arc, "radius", r);
 
 
                 float bnc = 0.95f; // DEF_BUMPER_BOUNCE?
                 float bnc = 0.95f; // DEF_BUMPER_BOUNCE?
-                table_file_parse_float(arc, "bounce", &bnc);
+                table_file_parse_float(arc, "bounce", bnc);
 
 
                 float start_angle = 0.0;
                 float start_angle = 0.0;
-                table_file_parse_float(arc, "start_angle", &start_angle);
+                table_file_parse_float(arc, "start_angle", start_angle);
                 start_angle *= pi_180;
                 start_angle *= pi_180;
                 float end_angle = 0.0;
                 float end_angle = 0.0;
-                table_file_parse_float(arc, "end_angle", &end_angle);
+                table_file_parse_float(arc, "end_angle", end_angle);
                 end_angle *= pi_180;
                 end_angle *= pi_180;
 
 
                 Arc::Surface surface = Arc::OUTSIDE;
                 Arc::Surface surface = Arc::OUTSIDE;
@@ -377,28 +432,43 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 const nx_json* rail = nx_json_item(rails, i);
                 const nx_json* rail = nx_json_item(rails, i);
 
 
                 Vec2 s;
                 Vec2 s;
-                if(!table_file_parse_vec2(rail, "start", &s)) {
+                if(!table_file_parse_vec2(rail, "start", s)) {
                     FURI_LOG_E(TAG, "Rail missing \"start\", skipping");
                     FURI_LOG_E(TAG, "Rail missing \"start\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(s)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Rail with starting position %.1f,%.1f is not on table!",
+                        (double)s.x,
+                        (double)s.y);
+                }
                 Vec2 e;
                 Vec2 e;
-                if(!table_file_parse_vec2(rail, "end", &e)) {
+                if(!table_file_parse_vec2(rail, "end", e)) {
                     FURI_LOG_E(TAG, "Rail missing \"end\", skipping");
                     FURI_LOG_E(TAG, "Rail missing \"end\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(e)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Rail with ending position %.1f,%.1f is not on table!",
+                        (double)e.x,
+                        (double)e.y);
+                }
 
 
                 Polygon* new_rail = new Polygon();
                 Polygon* new_rail = new Polygon();
                 new_rail->add_point(s);
                 new_rail->add_point(s);
                 new_rail->add_point(e);
                 new_rail->add_point(e);
 
 
                 float bnc = DEF_RAIL_BOUNCE;
                 float bnc = DEF_RAIL_BOUNCE;
-                table_file_parse_float(rail, "bounce", &bnc);
+                table_file_parse_float(rail, "bounce", bnc);
                 new_rail->bounce = bnc;
                 new_rail->bounce = bnc;
 
 
                 int double_sided = 0;
                 int double_sided = 0;
-                table_file_parse_int(rail, "double_sided", &double_sided);
+                table_file_parse_int(rail, "double_sided", double_sided);
 
 
                 new_rail->finalize();
                 new_rail->finalize();
+                new_rail->notification = &notify_rail_hit;
                 table->objects.push_back(new_rail);
                 table->objects.push_back(new_rail);
 
 
                 if(double_sided) {
                 if(double_sided) {
@@ -407,6 +477,7 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                     new_rail->add_point(s);
                     new_rail->add_point(s);
                     new_rail->bounce = bnc;
                     new_rail->bounce = bnc;
                     new_rail->finalize();
                     new_rail->finalize();
+                    new_rail->notification = &notify_rail_hit;
                     table->objects.push_back(new_rail);
                     table->objects.push_back(new_rail);
                 }
                 }
             }
             }
@@ -418,28 +489,57 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 const nx_json* portal = nx_json_item(portals, i);
                 const nx_json* portal = nx_json_item(portals, i);
 
 
                 Vec2 a1;
                 Vec2 a1;
-                if(!table_file_parse_vec2(portal, "a_start", &a1)) {
+                if(!table_file_parse_vec2(portal, "a_start", a1)) {
                     FURI_LOG_E(TAG, "Portal missing \"a_start\", skipping");
                     FURI_LOG_E(TAG, "Portal missing \"a_start\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(a1)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Portal A with starting position %.1f,%.1f is not on table!",
+                        (double)a1.x,
+                        (double)a1.y);
+                }
                 Vec2 a2;
                 Vec2 a2;
-                if(!table_file_parse_vec2(portal, "a_end", &a2)) {
+                if(!table_file_parse_vec2(portal, "a_end", a2)) {
                     FURI_LOG_E(TAG, "Portal missing \"a_end\", skipping");
                     FURI_LOG_E(TAG, "Portal missing \"a_end\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(a2)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Portal A with ending position %.1f,%.1f is not on table!",
+                        (double)a2.x,
+                        (double)a2.y);
+                }
                 Vec2 b1;
                 Vec2 b1;
-                if(!table_file_parse_vec2(portal, "b_start", &b1)) {
+                if(!table_file_parse_vec2(portal, "b_start", b1)) {
                     FURI_LOG_E(TAG, "Portal missing \"b_start\", skipping");
                     FURI_LOG_E(TAG, "Portal missing \"b_start\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(b1)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Portal B with starting position %.1f,%.1f is not on table!",
+                        (double)b1.x,
+                        (double)b1.y);
+                }
                 Vec2 b2;
                 Vec2 b2;
-                if(!table_file_parse_vec2(portal, "b_end", &b2)) {
+                if(!table_file_parse_vec2(portal, "b_end", b2)) {
                     FURI_LOG_E(TAG, "Portal missing \"b_end\", skipping");
                     FURI_LOG_E(TAG, "Portal missing \"b_end\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(b2)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Portal B with ending position %.1f,%.1f is not on table!",
+                        (double)b2.x,
+                        (double)b2.y);
+                }
 
 
                 Portal* new_portal = new Portal(a1, a2, b1, b2);
                 Portal* new_portal = new Portal(a1, a2, b1, b2);
                 new_portal->finalize();
                 new_portal->finalize();
+                new_portal->notification = &notify_portal;
                 table->objects.push_back(new_portal);
                 table->objects.push_back(new_portal);
             }
             }
         }
         }
@@ -450,10 +550,17 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 const nx_json* rollover = nx_json_item(rollovers, i);
                 const nx_json* rollover = nx_json_item(rollovers, i);
 
 
                 Vec2 p;
                 Vec2 p;
-                if(!table_file_parse_vec2(rollover, "position", &p)) {
+                if(!table_file_parse_vec2(rollover, "position", p)) {
                     FURI_LOG_E(TAG, "Rollover missing \"position\", skipping");
                     FURI_LOG_E(TAG, "Rollover missing \"position\", skipping");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(p)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Rollover with position %.1f,%.1f is not on table!",
+                        (double)p.x,
+                        (double)p.y);
+                }
                 char sym = '*';
                 char sym = '*';
                 const nx_json* symbol = nx_json_get(rollover, "symbol");
                 const nx_json* symbol = nx_json_get(rollover, "symbol");
                 if(symbol) {
                 if(symbol) {
@@ -470,16 +577,23 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 const nx_json* turbo = nx_json_item(turbos, i);
                 const nx_json* turbo = nx_json_item(turbos, i);
 
 
                 Vec2 p;
                 Vec2 p;
-                if(!table_file_parse_vec2(turbo, "position", &p)) {
+                if(!table_file_parse_vec2(turbo, "position", p)) {
                     FURI_LOG_E(TAG, "Turbo missing \"position\"");
                     FURI_LOG_E(TAG, "Turbo missing \"position\"");
                     continue;
                     continue;
                 }
                 }
+                if(!ON_TABLE(p)) {
+                    FURI_LOG_W(
+                        TAG,
+                        "Turbo with position %.1f,%.1f is not on table!",
+                        (double)p.x,
+                        (double)p.y);
+                }
                 float angle = 0;
                 float angle = 0;
-                table_file_parse_float(turbo, "angle", &angle);
+                table_file_parse_float(turbo, "angle", angle);
                 angle *= pi_180;
                 angle *= pi_180;
 
 
                 float boost = 10;
                 float boost = 10;
-                table_file_parse_float(turbo, "boost", &boost);
+                table_file_parse_float(turbo, "boost", boost);
 
 
                 Turbo* new_turbo = new Turbo(p, angle, boost);
                 Turbo* new_turbo = new Turbo(p, angle, boost);
 
 
@@ -603,12 +717,12 @@ Table* table_init_table_settings(void* ctx) {
     UNUSED(ctx);
     UNUSED(ctx);
     Table* table = new Table();
     Table* table = new Table();
 
 
-    table->balls.push_back(Ball(Vec2(20, 880), 10));
-    table->balls.back().add_velocity(Vec2(7, 0), .10f);
-    table->balls.push_back(Ball(Vec2(610, 920), 10));
-    table->balls.back().add_velocity(Vec2(-8, 0), .10f);
-    table->balls.push_back(Ball(Vec2(250, 980), 10));
-    table->balls.back().add_velocity(Vec2(10, 0), .10f);
+    // table->balls.push_back(Ball(Vec2(20, 880), 10));
+    // table->balls.back().add_velocity(Vec2(7, 0), .10f);
+    // table->balls.push_back(Ball(Vec2(610, 920), 10));
+    // table->balls.back().add_velocity(Vec2(-8, 0), .10f);
+    // table->balls.push_back(Ball(Vec2(250, 980), 10));
+    // table->balls.back().add_velocity(Vec2(10, 0), .10f);
 
 
     table->balls_released = true;
     table->balls_released = true;
 
 
@@ -633,15 +747,6 @@ Table* table_init_table_settings(void* ctx) {
     new_rail->hidden = true;
     new_rail->hidden = true;
     table->objects.push_back(new_rail);
     table->objects.push_back(new_rail);
 
 
-    // int gap = 8;
-    // int speed = 3;
-    // float top = 20;
-
-    // table->objects.push_back(new Chaser(Vec2(2, top), Vec2(61, top), gap, speed, Chaser::SLASH));
-    // table->objects.push_back(new Chaser(Vec2(2, top), Vec2(2, 84), gap, speed, Chaser::SLASH));
-    // table->objects.push_back(new Chaser(Vec2(2, 84), Vec2(61, 84), gap, speed, Chaser::SLASH));
-    // table->objects.push_back(new Chaser(Vec2(61, top), Vec2(61, 84), gap, speed, Chaser::SLASH));
-
     return table;
     return table;
 }
 }
 
 

+ 45 - 6
table.h

@@ -9,27 +9,65 @@
 #define TABLE_SETTINGS     2
 #define TABLE_SETTINGS     2
 #define TABLE_INDEX_OFFSET 3
 #define TABLE_INDEX_OFFSET 3
 
 
+class DataDisplay {
+public:
+    enum Align {
+        Horizontal,
+        Vertical
+    };
+    DataDisplay(const Vec2& pos, int val, bool disp, Align align)
+        : p(pos)
+        , value(val)
+        , display(disp)
+        , alignment(align) {
+    }
+    Vec2 p;
+    int value;
+    bool display;
+    Align alignment;
+    virtual void draw(Canvas* canvas) = 0;
+};
+class Lives : public DataDisplay {
+public:
+    Lives()
+        : DataDisplay(Vec2(), 3, false, Horizontal) {
+    }
+    void draw(Canvas* canvas);
+};
+
+class Score : public DataDisplay {
+public:
+    Score()
+        : DataDisplay(Vec2(64 - 1, 1), 0, false, Horizontal) {
+    }
+    void draw(Canvas* canvas);
+};
+
 // Defines all of the elements on a pinball table:
 // Defines all of the elements on a pinball table:
 // edges, bumpers, flipper locations, scoreboard
 // edges, bumpers, flipper locations, scoreboard
-// eventually read stae from file and dynamically allocate
+//
+// Also used for other app "views", like the main menu (table select)
+// and the Settings screen.
+// TODO: make this better? eh, it works for now...
 class Table {
 class Table {
 public:
 public:
     Table()
     Table()
-        : balls_released(false)
-        , num_lives(1)
+        : game_over(false)
+        , balls_released(false)
         , plunger(nullptr) {
         , plunger(nullptr) {
     }
     }
 
 
     ~Table();
     ~Table();
+
     std::vector<FixedObject*> objects;
     std::vector<FixedObject*> objects;
     std::vector<Ball> balls; // current state of balls
     std::vector<Ball> balls; // current state of balls
     std::vector<Ball> balls_initial; // original positions, before release
     std::vector<Ball> balls_initial; // original positions, before release
     std::vector<Flipper> flippers;
     std::vector<Flipper> flippers;
 
 
+    bool game_over;
     bool balls_released; // is ball in play?
     bool balls_released; // is ball in play?
-    size_t num_lives;
-    Vec2 num_lives_pos;
-    size_t score;
+    Lives lives;
+    Score score;
 
 
     Plunger* plunger;
     Plunger* plunger;
 
 
@@ -43,6 +81,7 @@ typedef struct {
 
 
 class TableList {
 class TableList {
 public:
 public:
+    TableList() = default;
     ~TableList();
     ~TableList();
     std::vector<TableMenuItem> menu_items;
     std::vector<TableMenuItem> menu_items;
     int display_size; // how many can fit on screen
     int display_size; // how many can fit on screen