Files
2026-07-19 16:49:55 +08:00

712 lines
39 KiB
YAML

# Knobuss - optional carousel screen (base id 10). A Gyruss-style tube shooter: your ship orbits the rim, you
# auto-fire inward, foes spiral out from the centre. Turn the knob to aim, blast them before they
# reach you. Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
# Scores (g_gy_top / g_gy_best) + the "Reset scores" settings action live here - fully self-contained.
#
# Perf: ship + bullet pool (6) + enemy pool (10) are all positioned each tick from float arrays in
# ONE lambda (lv_obj_set_pos over a pooled lv_obj_t* array - LVGL can't index a widget by a runtime
# var), so per-tick cost is constant and the action chain stays shallow. Sprites have a fixed src
# (enemies alternate enemy/enemy2 by slot), so there are no per-tick image swaps.
esphome:
on_boot:
- priority: 655
then:
- lambda: 'id(g_present)[10] = true;'
- priority: 650
then:
- lambda: |-
int g = id(g_wgrp_n)++;
id(g_wgrp_labels) += (g ? "\n" : ""); id(g_wgrp_labels) += "Knobuss";
int o = id(g_wopt_n)++;
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_gy_reset);
globals:
- id: g_gy_best
type: int
restore_value: yes
initial_value: '0'
- id: g_gy_top
type: int[10]
restore_value: yes
- id: g_gy_reset
type: bool
restore_value: no
initial_value: 'false'
- id: g_gy_state # 0=menu 1=playing 2=game over 3=how-to 4=scores
type: int
restore_value: no
initial_value: '0'
- id: g_gy_score
type: int
restore_value: no
initial_value: '0'
- id: g_gy_lives
type: int
restore_value: no
initial_value: '3'
- id: g_gy_ship_ang # ship orbit angle (radians)
type: float
restore_value: no
initial_value: '0.0'
- id: g_gy_dead
type: bool
restore_value: no
initial_value: 'false'
- id: g_gy_fire_clk
type: int
restore_value: no
initial_value: '0'
- id: g_gy_spawn_clk
type: int
restore_value: no
initial_value: '0'
- id: g_gy_hud_dirty # score/lives changed this tick -> refresh the HUD labels
type: bool
restore_value: no
initial_value: 'false'
# bullet pool (fly inward along a fixed radial)
- id: g_gy_br # radius (px from centre)
type: float[6]
restore_value: no
- id: g_gy_ba # angle (radians)
type: float[6]
restore_value: no
- id: g_gy_bon
type: bool[6]
restore_value: no
# enemy pool (spiral outward from the centre)
- id: g_gy_er
type: float[10]
restore_value: no
- id: g_gy_ea
type: float[10]
restore_value: no
- id: g_gy_eon
type: bool[10]
restore_value: no
# starfield (depth illusion): dots rush outward from the centre, faster as they near the rim
- id: g_gy_sr # radius (px from centre)
type: float[10]
restore_value: no
- id: g_gy_sa # angle (radians)
type: float[10]
restore_value: no
# explosion pool (expanding shockwave ring of dots) - fired when a foe reaches the rim
- id: g_gy_exx # centre X (relative to screen centre, px)
type: float[2]
restore_value: no
- id: g_gy_exy # centre Y
type: float[2]
restore_value: no
- id: g_gy_exage # -1 = inactive, else explosion frame 0..7
type: int[2]
restore_value: no
- id: g_gy_shipframe # current ship rotation frame (0..15), -1 forces a refresh
type: int
restore_value: no
initial_value: '-1'
- id: g_gy_shipdirty # ship rotation bucket changed this tick
type: bool
restore_value: no
initial_value: 'false'
- id: g_gy_exact # an explosion is active or expired this tick (refresh its sprite)
type: bool
restore_value: no
initial_value: 'false'
- id: g_gy_tick # frame counter (throttles the starfield + ship-sprite refresh)
type: int
restore_value: no
initial_value: '0'
script:
# position every active widget from the float arrays (constant cost, one shallow lambda)
- id: gy_render
then:
- lambda: |-
const float R = 148.0f;
// starfield (background depth) - small dots rushing outward
lv_obj_t *SF[6] = { id(gy_s0), id(gy_s1), id(gy_s2), id(gy_s3), id(gy_s4), id(gy_s5) };
for (int k = 0; k < 6; k++) {
float r = id(g_gy_sr)[k], a = id(g_gy_sa)[k];
lv_obj_set_pos(SF[k], 180 + (int) lroundf(cosf(a) * r) - 1, 180 + (int) lroundf(sinf(a) * r) - 1);
}
lv_obj_t *BB[6] = { id(gy_b0), id(gy_b1), id(gy_b2), id(gy_b3), id(gy_b4), id(gy_b5) };
for (int k = 0; k < 6; k++) {
if (id(g_gy_bon)[k]) {
float r = id(g_gy_br)[k], a = id(g_gy_ba)[k];
lv_obj_set_pos(BB[k], 180 + (int) lroundf(cosf(a) * r) - 6, 180 + (int) lroundf(sinf(a) * r) - 6);
} else lv_obj_set_pos(BB[k], 400, 400);
}
lv_obj_t *EE[10] = { id(gy_e0), id(gy_e1), id(gy_e2), id(gy_e3), id(gy_e4), id(gy_e5), id(gy_e6), id(gy_e7), id(gy_e8), id(gy_e9) };
for (int k = 0; k < 10; k++) {
if (id(g_gy_eon)[k]) {
float r = id(g_gy_er)[k], a = id(g_gy_ea)[k];
lv_obj_set_pos(EE[k], 180 + (int) lroundf(cosf(a) * r) - 13, 180 + (int) lroundf(sinf(a) * r) - 13);
} else lv_obj_set_pos(EE[k], 400, 400);
}
float sa = id(g_gy_ship_ang);
lv_obj_set_pos(id(gy_ship), 180 + (int) lroundf(cosf(sa) * R) - 18, 180 + (int) lroundf(sinf(sa) * R) - 18);
// explosions (32px sprites) - positioned while alive, parked otherwise
lv_obj_t *XX[2] = { id(gy_x0), id(gy_x1) };
for (int s = 0; s < 2; s++) {
if (id(g_gy_exage)[s] >= 0) lv_obj_set_pos(XX[s], 180 + (int) lroundf(id(g_gy_exx)[s]) - 16, 180 + (int) lroundf(id(g_gy_exy)[s]) - 16);
else lv_obj_set_pos(XX[s], 400, 400);
}
# set the ship sprite to its current rotation frame (called only when the bucket changes)
- id: gy_shipspr
then:
- if: { condition: { lambda: 'return id(g_gy_shipframe)==0;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship0 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==1;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship1 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==2;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship2 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==3;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship3 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==4;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship4 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==5;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship5 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==6;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship6 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==7;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship7 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==8;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship8 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==9;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship9 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==10;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship10 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==11;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship11 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==12;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship12 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==13;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship13 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==14;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship14 } ] }
- if: { condition: { lambda: 'return id(g_gy_shipframe)==15;' }, then: [ lvgl.image.update: { id: gy_ship, src: img_gy_ship15 } ] }
# advance/hide the explosion sprites by their frame (called only while a burst is live)
- id: gy_explspr
then:
- if:
condition: { lambda: 'return id(g_gy_exage)[0] < 0;' }
then: [ lvgl.widget.hide: gy_x0 ]
else:
- lvgl.widget.show: gy_x0
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==0;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl0 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==1;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl1 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==2;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl2 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==3;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl3 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==4;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl4 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==5;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl5 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]==6;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl6 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[0]>=7;' }, then: [ lvgl.image.update: { id: gy_x0, src: img_gy_expl7 } ] }
- if:
condition: { lambda: 'return id(g_gy_exage)[1] < 0;' }
then: [ lvgl.widget.hide: gy_x1 ]
else:
- lvgl.widget.show: gy_x1
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==0;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl0 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==1;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl1 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==2;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl2 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==3;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl3 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==4;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl4 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==5;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl5 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]==6;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl6 } ] }
- if: { condition: { lambda: 'return id(g_gy_exage)[1]>=7;' }, then: [ lvgl.image.update: { id: gy_x1, src: img_gy_expl7 } ] }
- id: gy_hud
then:
- lvgl.label.update: { id: lbl_gy_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_gy_score)); return std::string(b);' }
- lvgl.label.update: { id: lbl_gy_lives, text: !lambda 'char b[16]; snprintf(b,sizeof(b),"LIVES %d",id(g_gy_lives)<0?0:id(g_gy_lives)); return std::string(b);' }
# park every gameplay widget off-screen
- id: gy_clear
then:
- lambda: |-
lv_obj_t *BB[6] = { id(gy_b0), id(gy_b1), id(gy_b2), id(gy_b3), id(gy_b4), id(gy_b5) };
for (int k = 0; k < 6; k++) lv_obj_set_pos(BB[k], 400, 400);
lv_obj_t *EE[10] = { id(gy_e0), id(gy_e1), id(gy_e2), id(gy_e3), id(gy_e4), id(gy_e5), id(gy_e6), id(gy_e7), id(gy_e8), id(gy_e9) };
for (int k = 0; k < 10; k++) lv_obj_set_pos(EE[k], 400, 400);
lv_obj_t *SF[6] = { id(gy_s0), id(gy_s1), id(gy_s2), id(gy_s3), id(gy_s4), id(gy_s5) };
for (int k = 0; k < 6; k++) lv_obj_set_pos(SF[k], 400, 400);
lv_obj_set_pos(id(gy_ship), 400, 400);
- lvgl.widget.hide: gy_x0
- lvgl.widget.hide: gy_x1
- id: gy_start
then:
- lambda: |-
id(g_gy_state) = 1; id(g_gy_score) = 0; id(g_gy_lives) = 3; id(g_gy_dead) = false;
id(g_gy_ship_ang) = -1.5708f; // start at the top
id(g_gy_fire_clk) = 0; id(g_gy_spawn_clk) = 0;
for (int k = 0; k < 6; k++) id(g_gy_bon)[k] = false;
for (int k = 0; k < 10; k++) id(g_gy_eon)[k] = false;
for (int k = 0; k < 6; k++) { id(g_gy_sr)[k] = 6.0f + (esp_random() % 168); id(g_gy_sa)[k] = (esp_random() % 62832) / 10000.0f; }
for (int s = 0; s < 2; s++) id(g_gy_exage)[s] = -1;
id(g_gy_shipframe) = -1; // force a ship-sprite refresh on the first tick
id(g_knob_capture) = true; id(g_knob_delta) = 0;
- script.execute: gy_screen
- script.execute: gy_hud
- id: gy_over
then:
- lambda: |-
id(g_gy_state) = 2;
int s = id(g_gy_score);
if (s > id(g_gy_best)) id(g_gy_best) = s;
for (int i = 0; i < 10; i++) {
if (s > id(g_gy_top)[i]) {
for (int j = 9; j > i; j--) id(g_gy_top)[j] = id(g_gy_top)[j-1];
id(g_gy_top)[i] = s;
break;
}
}
- script.execute: gy_screen
# screen state + the single knob-capture authority (captured only in play + game over)
- id: gy_screen
then:
- lambda: 'id(g_knob_capture) = (id(g_gy_state) == 1 || id(g_gy_state) == 2);'
- script.execute: gy_clear
- lvgl.widget.hide: gy_logo
- lvgl.widget.hide: gy_ring
- lvgl.widget.hide: gy_ring2
- lvgl.widget.hide: gy_ring3
- lvgl.widget.hide: gy_core
- lvgl.widget.hide: lbl_gy_score
- lvgl.widget.hide: lbl_gy_lives
- lvgl.widget.hide: lbl_gy_title
- lvgl.widget.hide: lbl_gy_sub
- lvgl.widget.hide: btn_gy_start
- lvgl.widget.hide: btn_gy_howto
- lvgl.widget.hide: btn_gy_scores
- lvgl.widget.hide: lbl_gy_over
- lvgl.widget.hide: lbl_gy_rec
- lvgl.widget.hide: btn_gy_replay
- lvgl.widget.hide: btn_gy_exit
- lvgl.widget.hide: lbl_gy_howto_t
- lvgl.widget.hide: gy_hb1
- lvgl.widget.hide: gy_hb2
- lvgl.widget.hide: gy_hb3
- lvgl.widget.hide: gy_hb4
- lvgl.widget.hide: lbl_gy_howto1
- lvgl.widget.hide: lbl_gy_howto2
- lvgl.widget.hide: lbl_gy_howto3
- lvgl.widget.hide: lbl_gy_howto4
- lvgl.widget.hide: gy_life_icon
- lvgl.widget.hide: lbl_gy_scores_t
- lvgl.widget.hide: lbl_gy_scores1
- lvgl.widget.hide: lbl_gy_scores2
- lvgl.widget.hide: btn_gy_back
- if: # MENU
condition: { lambda: 'return id(g_gy_state) == 0;' }
then:
- lvgl.widget.show: gy_logo
- lvgl.widget.show: btn_gy_start
- lvgl.widget.show: btn_gy_howto
- lvgl.widget.show: btn_gy_scores
- if: # PLAY
condition: { lambda: 'return id(g_gy_state) == 1;' }
then:
- lvgl.widget.show: gy_ring
- lvgl.widget.show: gy_ring2
- lvgl.widget.show: gy_ring3
- lvgl.widget.show: gy_core
- lvgl.widget.show: lbl_gy_score
- lvgl.widget.show: lbl_gy_lives
- script.execute: gy_render
- if: # GAME OVER
condition: { lambda: 'return id(g_gy_state) == 2;' }
then:
- lvgl.widget.show: gy_logo
- lvgl.widget.show: lbl_gy_over
- lvgl.widget.show: lbl_gy_rec
- lvgl.widget.show: btn_gy_replay
- lvgl.widget.show: btn_gy_exit
- lvgl.label.update: { id: lbl_gy_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_gy_score)); return std::string(b);' }
- lvgl.label.update: { id: lbl_gy_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_gy_best)); return std::string(b);' }
- if: # HOW TO PLAY
condition: { lambda: 'return id(g_gy_state) == 3;' }
then:
- lvgl.widget.show: lbl_gy_howto_t
- lvgl.widget.show: gy_hb1
- lvgl.widget.show: gy_hb2
- lvgl.widget.show: gy_hb3
- lvgl.widget.show: gy_hb4
- lvgl.widget.show: lbl_gy_howto1
- lvgl.widget.show: lbl_gy_howto2
- lvgl.widget.show: lbl_gy_howto3
- lvgl.widget.show: lbl_gy_howto4
- lvgl.widget.show: gy_life_icon
- lvgl.widget.show: btn_gy_back
- if: # SCORES (top 10)
condition: { lambda: 'return id(g_gy_state) == 4;' }
then:
- lvgl.widget.show: lbl_gy_scores_t
- lvgl.widget.show: lbl_gy_scores1
- lvgl.widget.show: lbl_gy_scores2
- lvgl.widget.show: btn_gy_back
- lvgl.label.update:
id: lbl_gy_scores1
text: !lambda |-
std::string s; char b[24];
for (int i = 0; i < 5; i++) { if(i) s += "\n"; if(id(g_gy_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_gy_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
return s;
- lvgl.label.update:
id: lbl_gy_scores2
text: !lambda |-
std::string s; char b[24];
for (int i = 5; i < 10; i++) { if(i>5) s += "\n"; if(id(g_gy_top)[i]>0){ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_gy_top)[i]); } else { snprintf(b,sizeof(b),"%d. -",i+1); } s += b; }
return s;
interval:
- interval: 250ms
then:
- if:
condition: { lambda: 'return id(g_gy_reset) || id(g_factory_reset);' }
then:
- lambda: 'for (int i = 0; i < 10; i++) id(g_gy_top)[i] = 0; id(g_gy_best) = 0; id(g_gy_reset) = false;'
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_nav_req) && id(g_base)==10;' }
then:
- lambda: 'id(g_nav_req) = false; id(g_gy_state) = 0; id(g_knob_delta) = 0;'
- if: { condition: { lambda: 'return id(g_nav_anim)==0;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_left, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==1;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_right, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==2;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_top, time: 250ms }] }
- if: { condition: { lambda: 'return id(g_nav_anim)==3;' }, then: [lvgl.page.show: { id: page_gyruss, animation: move_bottom, time: 250ms }] }
- script.execute: gy_screen
# Game tick: aim (knob) + auto-fire + spiral the foes + collisions; all positioning in gy_render.
- interval: 50ms
then:
- if:
condition: { lambda: 'return id(g_base) == 10 && id(g_gy_state) == 1;' }
then:
- lambda: |-
id(g_gy_hud_dirty) = false; id(g_gy_shipdirty) = false;
id(g_gy_tick)++;
int kd = id(g_knob_delta); id(g_knob_delta) = 0;
id(g_gy_ship_ang) += kd * 0.30f; // aim sensitivity: ~17 deg per knob detent
// auto-fire a bullet down the ship's radial
if (++id(g_gy_fire_clk) >= 5) {
id(g_gy_fire_clk) = 0;
for (int k = 0; k < 6; k++) if (!id(g_gy_bon)[k]) { id(g_gy_bon)[k] = true; id(g_gy_br)[k] = 138.0f; id(g_gy_ba)[k] = id(g_gy_ship_ang); break; }
}
for (int k = 0; k < 6; k++) if (id(g_gy_bon)[k]) { id(g_gy_br)[k] -= 9.0f; if (id(g_gy_br)[k] < 12.0f) id(g_gy_bon)[k] = false; }
// spawn foes from the core (faster as the score climbs)
int sw = 24 - id(g_gy_score) / 200; if (sw < 9) sw = 9;
if (++id(g_gy_spawn_clk) >= sw) {
id(g_gy_spawn_clk) = 0;
for (int k = 0; k < 10; k++) if (!id(g_gy_eon)[k]) {
float a = (esp_random() % 62832) / 10000.0f;
id(g_gy_eon)[k] = true; id(g_gy_er)[k] = 32.0f; id(g_gy_ea)[k] = a; break; // start at the core's rim (not under it) to avoid central overdraw
}
}
// age existing explosions first (8-frame sprite); refresh sprite while any is live/expiring
id(g_gy_exact) = false;
for (int s = 0; s < 2; s++) if (id(g_gy_exage)[s] >= 0) {
id(g_gy_exact) = true;
if (++id(g_gy_exage)[s] > 7) id(g_gy_exage)[s] = -1;
}
// foes spiral outward; ANY foe that reaches the rim costs a life and explodes there
float esp = 1.5f + id(g_gy_score) / 1500.0f; if (esp > 3.2f) esp = 3.2f;
for (int k = 0; k < 10; k++) if (id(g_gy_eon)[k]) {
id(g_gy_er)[k] += esp; id(g_gy_ea)[k] += 0.05f;
if (id(g_gy_er)[k] >= 146.0f) {
id(g_gy_eon)[k] = false;
id(g_gy_lives)--; id(g_gy_hud_dirty) = true; if (id(g_gy_lives) <= 0) id(g_gy_dead) = true;
for (int s = 0; s < 2; s++) if (id(g_gy_exage)[s] < 0) { id(g_gy_exx)[s] = cosf(id(g_gy_ea)[k]) * 146.0f; id(g_gy_exy)[s] = sinf(id(g_gy_ea)[k]) * 146.0f; id(g_gy_exage)[s] = 0; id(g_gy_exact) = true; break; }
}
}
// depth starfield (throttled to every 2nd tick): rush outward, respawn near the centre
if ((id(g_gy_tick) & 1) == 0) for (int k = 0; k < 6; k++) {
id(g_gy_sr)[k] += id(g_gy_sr)[k] * 0.10f + 2.0f;
if (id(g_gy_sr)[k] > 176.0f) { id(g_gy_sr)[k] = 4.0f + (esp_random() % 14); id(g_gy_sa)[k] = (esp_random() % 62832) / 10000.0f; }
}
// ship rotation frame (every 2nd tick, on the ODD ticks so it doesn't share a tick with
// the starfield move above - spreads the two throttled costs; position stays 50ms)
if ((id(g_gy_tick) & 1) == 1) {
int bk = (int) lroundf(id(g_gy_ship_ang) / 0.3926991f); bk %= 16; if (bk < 0) bk += 16;
id(g_gy_shipdirty) = (bk != id(g_gy_shipframe));
if (id(g_gy_shipdirty)) id(g_gy_shipframe) = bk;
}
// bullet vs foe (polar proximity)
for (int b = 0; b < 6; b++) if (id(g_gy_bon)[b]) for (int e = 0; e < 10; e++) if (id(g_gy_eon)[e]) {
float dr = id(g_gy_br)[b] - id(g_gy_er)[e]; if (dr < 0) dr = -dr;
if (dr < 13.0f) {
float da = id(g_gy_ba)[b] - id(g_gy_ea)[e]; da = atan2f(sinf(da), cosf(da));
if (fabsf(da) < 0.28f) { id(g_gy_bon)[b] = false; id(g_gy_eon)[e] = false; id(g_gy_score) += 10; id(g_gy_hud_dirty) = true; break; }
}
}
- if: { condition: { lambda: 'return id(g_gy_shipdirty);' }, then: [ script.execute: gy_shipspr ] }
- if: { condition: { lambda: 'return id(g_gy_exact);' }, then: [ script.execute: gy_explspr ] }
- script.execute: gy_render
- if:
condition: { lambda: 'return id(g_gy_dead);' }
then:
- script.execute: gy_over
else:
- if:
condition: { lambda: 'return id(g_gy_hud_dirty);' }
then:
- script.execute: gy_hud
# Sprites are pulled from the repo /main/ raw URLs at compile time.
# Third-party art (see assets/sprites/knobuss/CREDITS.txt).
image:
- id: img_gy_ship0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship0.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship1.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship2.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship3.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship4.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship5.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship6.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship7.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship8
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship8.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship9
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship9.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship10
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship10.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship11
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship11.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship12
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship12.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship13
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship13.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship14
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship14.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_ship15
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/ship15.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_enemy0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy0.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_enemy1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy1.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_enemy2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy2.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_enemy3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/enemy3.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl0
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl0.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl1
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl1.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl2
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl2.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl3
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl3.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl4
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl4.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl5
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl5.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl6
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl6.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_expl7
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/expl7.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_bullet
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/bullet.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_core
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/core.png"
type: RGB565
transparency: alpha_channel
- id: img_gy_logo
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/knobuss-logo.png"
type: RGB565
- id: img_gy_life
file: "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/knobuss/life.png"
type: RGB565
transparency: alpha_channel
lvgl:
pages:
- id: page_gyruss
bg_color: 0x070611
scrollbar_mode: "off"
scrollable: false
widgets:
# Knobuss title screen (full-screen menu backdrop, behind the buttons) - hidden in play
- image: { id: gy_logo, align: CENTER, x: 0, y: 0, src: img_gy_logo, hidden: true }
# starfield (declared first = drawn behind everything) - parked until play
- obj: { id: gy_s0, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s1, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s2, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s3, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s4, x: 400, y: 400, width: 3, height: 3, radius: 999, bg_color: 0x6076A4, bg_opa: COVER, border_width: 0, pad_all: 0 }
- obj: { id: gy_s5, x: 400, y: 400, width: 2, height: 2, radius: 999, bg_color: 0x32466E, bg_opa: COVER, border_width: 0, pad_all: 0 }
# concentric guide rings (static) suggest a tube receding to the core - free depth cue
- obj: { id: gy_ring3, align: CENTER, x: 0, y: 0, width: 112, height: 112, radius: 56, bg_opa: TRANSP, border_width: 1, border_color: 0x1A2742, hidden: true }
- obj: { id: gy_ring2, align: CENTER, x: 0, y: 0, width: 200, height: 200, radius: 100, bg_opa: TRANSP, border_width: 1, border_color: 0x1E2C4A, hidden: true }
- obj: { id: gy_ring, align: CENTER, x: 0, y: 0, width: 300, height: 300, radius: 150, bg_opa: TRANSP, border_width: 2, border_color: 0x2A3C66, hidden: true }
- image: { id: gy_core, align: CENTER, x: 0, y: 0, src: img_gy_core, hidden: true }
# foes: 4 glowing-orb colours cycled across the 10 slots (fixed src - no runtime swap)
- image: { id: gy_e0, x: 400, y: 400, src: img_gy_enemy0 }
- image: { id: gy_e1, x: 400, y: 400, src: img_gy_enemy1 }
- image: { id: gy_e2, x: 400, y: 400, src: img_gy_enemy2 }
- image: { id: gy_e3, x: 400, y: 400, src: img_gy_enemy3 }
- image: { id: gy_e4, x: 400, y: 400, src: img_gy_enemy0 }
- image: { id: gy_e5, x: 400, y: 400, src: img_gy_enemy1 }
- image: { id: gy_e6, x: 400, y: 400, src: img_gy_enemy2 }
- image: { id: gy_e7, x: 400, y: 400, src: img_gy_enemy3 }
- image: { id: gy_e8, x: 400, y: 400, src: img_gy_enemy0 }
- image: { id: gy_e9, x: 400, y: 400, src: img_gy_enemy1 }
- image: { id: gy_b0, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b1, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b2, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b3, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b4, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_b5, x: 400, y: 400, src: img_gy_bullet }
- image: { id: gy_ship, x: 400, y: 400, src: img_gy_ship0 }
# explosion sprites (8-frame), parked + hidden until a foe hits the rim
- image: { id: gy_x0, x: 400, y: 400, src: img_gy_expl0, hidden: true }
- image: { id: gy_x1, x: 400, y: 400, src: img_gy_expl0, hidden: true }
- label: { id: lbl_gy_score, align: CENTER, x: 0, y: -150, text: "0", text_font: font_med, text_color: 0xCFE4FF, hidden: true }
- label: { id: lbl_gy_lives, align: CENTER, x: 0, y: 150, text: "LIVES 3", text_font: font_artist, text_color: 0xFF7A6A, hidden: true }
- label: { id: lbl_gy_title, align: CENTER, x: 0, y: -86, text: "KNOBUSS", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
- label: { id: lbl_gy_sub, align: CENTER, x: 0, y: -52, text: "tube shooter", text_font: font_artist, text_color: 0x6B7BA6, hidden: true }
- button:
id: btn_gy_start
align: CENTER
y: -8
width: 150
height: 44
radius: 22
bg_color: 0x39C6E6
bg_opa: COVER
border_width: 0
on_click: [ script.execute: gy_start ]
widgets:
- label: { align: CENTER, text: "Start", text_font: font_med, text_color: 0x06131A }
- button:
id: btn_gy_howto
align: CENTER
y: 40
width: 150
height: 44
radius: 22
bg_color: 0x1C2A44
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_gy_state) = 3;', script.execute: gy_screen ]
widgets:
- label: { align: CENTER, text: "How to play?", text_font: font_med, text_color: 0xC4D2F0 }
- button:
id: btn_gy_scores
align: CENTER
y: 88
width: 150
height: 44
radius: 22
bg_color: 0x1C2A44
bg_opa: COVER
border_width: 0
on_click: [ lambda: 'id(g_gy_state) = 4;', script.execute: gy_screen ]
widgets:
- label: { align: CENTER, text: "Scores", text_font: font_med, text_color: 0xC4D2F0 }
- label: { id: lbl_gy_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: 0xCFE4FF, hidden: true }
- label: { id: lbl_gy_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: 0x6B7BA6, hidden: true }
- button:
id: btn_gy_replay
align: CENTER
x: -66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0x39C6E6
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ script.execute: gy_start ]
widgets:
- label: { align: CENTER, text: "Play", text_font: font_med, text_color: 0x06131A }
- button:
id: btn_gy_exit
align: CENTER
x: 66
y: 62
width: 112
height: 48
radius: 24
bg_color: 0x2A2E3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_gy_state) = 0;', script.execute: gy_screen ]
widgets:
- label: { align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF }
- label: { id: lbl_gy_howto_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
- obj: { id: gy_hb1, align: CENTER, x: -96, y: -52, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: gy_hb2, align: CENTER, x: -96, y: -16, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: gy_hb3, align: CENTER, x: -96, y: 20, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF } ] }
- obj: { id: gy_hb4, align: CENTER, x: -96, y: 56, width: 24, height: 24, radius: 12, bg_color: 0x2C6FB2, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: { align: CENTER, text: "4", text_font: font_artist, text_color: 0xFFFFFF } ] }
- label: { id: lbl_gy_howto1, align_to: { id: gy_hb1, align: OUT_RIGHT_MID, x: 8 }, text: "Turn knob to aim", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
- label: { id: lbl_gy_howto2, align_to: { id: gy_hb2, align: OUT_RIGHT_MID, x: 8 }, text: "You auto-fire inward", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
- label: { id: lbl_gy_howto3, align_to: { id: gy_hb3, align: OUT_RIGHT_MID, x: 8 }, text: "Blast foes before the rim", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
- label: { id: lbl_gy_howto4, align_to: { id: gy_hb4, align: OUT_RIGHT_MID, x: 8 }, text: "You have three lives", text_font: font_med, text_color: 0xC4D2F0, hidden: true }
- image: { id: gy_life_icon, align_to: { id: lbl_gy_howto4, align: OUT_RIGHT_MID, x: 8 }, src: img_gy_life, hidden: true }
- label: { id: lbl_gy_scores_t, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: 0x46E3D0, hidden: true }
- label: { id: lbl_gy_scores1, align: CENTER, x: -52, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC4D2F0, hidden: true }
- label: { id: lbl_gy_scores2, align: CENTER, x: 56, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: 0xC4D2F0, hidden: true }
- button:
id: btn_gy_back
align: CENTER
y: 120
width: 130
height: 42
radius: 21
bg_color: 0x2A2E3D
bg_opa: COVER
border_width: 0
hidden: true
on_click: [ lambda: 'id(g_gy_state) = 0;', script.execute: gy_screen ]
widgets:
- label: { align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF }