Compare commits

...

19 Commits

Author SHA1 Message Date
522fc7c8c4 firmware: Disable performance monitor 2024-12-16 23:27:53 +00:00
f62f2414b0 controller: Fix sudden death outcome and add last round in half 2024-12-16 00:04:36 +00:00
f501dc046e controller: Improve shutdown handling 2024-12-15 23:34:39 +00:00
244a340764 controller: Add thread for HID handler to app 2024-12-15 17:41:07 +00:00
9543851867 firmware+controller: Add round start state 2024-12-15 17:30:08 +00:00
1caf43c6de firmware+controller: Add game start and over states 2024-12-15 15:36:52 +00:00
d9c063cba7 firmware+controller: Add generic in-game state 2024-12-15 14:37:31 +00:00
8adb89ef2b firmware+controller: Add pregame state 2024-12-15 13:57:31 +00:00
ea5f1a7902 firmware+controller: Add match found state 2024-12-15 12:05:36 +00:00
18831c3e0e firmware+controller: Queue start state 2024-12-14 16:26:34 +00:00
4d1074e60c firmware: Add repeating animations 2024-12-14 13:18:17 +00:00
514198d136 controller: Fix USB get_input_report on Windows 2024-12-13 17:57:49 +00:00
277eb4ee3b firmware+controller: Initial working HID state control 2024-12-13 17:51:46 +00:00
6d0db71305 firmware: Add menu and idle states 2024-12-13 16:32:53 +00:00
4fa183ab2d firmware: Finished (?) none state 2024-12-13 14:06:00 +00:00
76d8557f36 firmware: Semi-working emoji 2024-12-13 04:01:54 +00:00
facb46c068 firmware: Add second message to none state 2024-12-13 03:03:21 +00:00
e550c6d016 firmware: Call lv_disp_flush_ready() in vsync handler 2024-12-13 02:45:36 +00:00
84fb7553c5 firmware: Initial animation 2024-12-13 02:29:27 +00:00
17 changed files with 1177 additions and 98 deletions

View File

@@ -1,6 +1,7 @@
import configparser
import logging
import os
import signal
import sys
import infi.systray
@@ -42,12 +43,16 @@ def main():
conf = configparser.ConfigParser()
conf.read_dict({
'general': {'log_level': 'info'},
'general': {
'log_level': 'info',
'dummy_impl': False,
},
'valorant': {'player_uuid': ''},
})
conf.read(CONFIG_FILE)
with open(CONFIG_FILE, 'w') as f:
conf.write(f)
use_dummy_impl = conf.getboolean('general', 'dummy_impl')
log_level = parse_log_level(conf['general']['log_level'])
logging.basicConfig(
@@ -59,20 +64,28 @@ def main():
log.error(f'No player UUID set, exiting...')
sys.exit(1)
val_handler = valconomy.ValconomyHandler()
if use_dummy_impl:
val_handler = valconomy.ValconomyHandler()
else:
val_handler = valconomy.HIDValconomyHandler()
val_handler.start()
val_sm = valconomy.ValconomyStateMachine(conf['valorant']['player_uuid'], val_handler)
val_client = valconomy.ValorantLocalClient(val_sm.handle_presence)
def do_quit(tray: infi.systray.SysTrayIcon):
def do_quit(*args):
log.info('Shutting down')
val_client.stop()
if not use_dummy_impl:
val_handler.stop()
with infi.systray.SysTrayIcon(
data_file('icon.ico'), 'Valconomy', on_quit=do_quit,
menu_options=(
('Open data directory', None, do_open_datadir),
)) as tray:
val_client.run()
signal.signal(signal.SIGINT, do_quit)
signal.signal(signal.SIGTERM, do_quit)
val_client.run()
if __name__ == '__main__':
main()

View File

@@ -1,17 +1,47 @@
#!/usr/bin/env python3
import hid
import logging
USB_VID = 0x6969
USB_PID = 0x0004
import valconomy
from valconomy import ValorantPlayerInfo, RiotPlayerInfo, EconomyDecision
def main():
hid_handle = hid.device()
hid_handle.open(vendor_id=USB_VID, product_id=USB_PID)
logging.basicConfig(
format='%(asctime)s %(name)s %(levelname)s %(message)s', level=logging.INFO)
h = valconomy.HIDValconomyHandler()
try:
hid_handle.write(b'\x00test')
h.menu(None, False)
h.none()
h.queue_start(RiotPlayerInfo.dummy(
valorant=ValorantPlayerInfo()))
h.menu(None, True)
h.idle(None)
h.queue_start(RiotPlayerInfo.dummy(
valorant=ValorantPlayerInfo(queue_type='unrated', is_party_owner=True)))
h.match_found(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(queue_type='premier-seasonmatch')))
h.pregame(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(map='/Game/Maps/Bonsai/Bonsai')))
h.match_found(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo()))
h.pregame(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo()))
h.game_generic(RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(queue_type='ggteam')))
h.game_start(None)
h.game_over(None, False)
h.game_over(None, True)
h.round_start(
RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(score=3, enemy_score=7)),
won=True, economy=EconomyDecision.SAVE)
h.round_start(
RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(score=10, enemy_score=2)),
won=False, economy=EconomyDecision.BUY)
h.round_start(
RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(score=10, enemy_score=2)),
won=None, economy=EconomyDecision.MATCH_TEAM)
h.round_start(
RiotPlayerInfo.dummy(valorant=ValorantPlayerInfo(score=2, enemy_score=0)),
won=True, economy=EconomyDecision.BONUS)
h.service()
finally:
hid_handle.close()
h.close()
if __name__ == '__main__':
main()

View File

@@ -81,7 +81,8 @@ class TestSm:
('round_start', True, 2, 0, EconomyDecision.BONUS),
('round_start', False, 2, 1, EconomyDecision.MATCH_TEAM),
] + [
('round_start', False, 2, 2 + i, EconomyDecision.MATCH_TEAM) for i in range(8)] + [
('round_start', False, 2, 2 + i, EconomyDecision.MATCH_TEAM) for i in range(7)] + [
('round_start', False, 2, 9, EconomyDecision.BUY), # last round in half
('round_start', False, 2, 10, EconomyDecision.BUY),
('round_start', False, 2, 11, EconomyDecision.SAVE),
('round_start', False, 2, 12, EconomyDecision.BUY),
@@ -112,7 +113,8 @@ class TestSm:
('round_start', True, 1, 0, EconomyDecision.BUY),
('round_start', False, 1, 1, EconomyDecision.SAVE),
] + [
('round_start', False, 1, 2 + i, EconomyDecision.MATCH_TEAM) for i in range(9)] + [
('round_start', False, 1, 2 + i, EconomyDecision.MATCH_TEAM) for i in range(8)] + [
('round_start', False, 1, 10, EconomyDecision.BUY), # last round in half
('round_start', False, 1, 11, EconomyDecision.BUY),
('round_start', False, 1, 12, EconomyDecision.BUY),
] + [
@@ -133,8 +135,10 @@ class TestSm:
self.do(queue_type='swiftplay', game_state='PREGAME')
self.do(queue_type='swiftplay', game_state='INGAME')
for i in range(5):
for i in range(4):
self.do(queue_type='swiftplay', game_state='INGAME', enemy_score=1 + i)
for i in range(5):
self.do(queue_type='swiftplay', game_state='INGAME', score=1 + i, enemy_score=4)
self.do(queue_type='swiftplay', game_state='INGAME')
assert self.mock.seq == [
@@ -144,9 +148,15 @@ class TestSm:
('pregame', False),
'game_start',
('round_start', None, 0, 0, EconomyDecision.BUY),
] + [
('round_start', False, 0, 1 + i, EconomyDecision.BUY) for i in range(4)] + [
('game_over', False),
('round_start', False, 0, 1, EconomyDecision.BUY),
('round_start', False, 0, 2, EconomyDecision.BUY),
('round_start', False, 0, 3, EconomyDecision.BUY),
('round_start', False, 0, 4, EconomyDecision.BUY),
('round_start', True, 1, 4, EconomyDecision.BUY),
('round_start', True, 2, 4, EconomyDecision.BUY),
('round_start', True, 3, 4, EconomyDecision.BUY),
('round_start', True, 4, 4, EconomyDecision.BUY),
('game_over', True),
]
def test_tdm(self):

View File

@@ -5,6 +5,9 @@ import json
import logging
from pprint import pprint
import os
import queue
import struct
import threading
import time
from typing import Callable
@@ -45,16 +48,20 @@ class RiotPlayerInfo:
valorant: ValorantPlayerInfo = None
@classmethod
def dummy(cls, **kwargs):
return cls('00000000-0000-0000-0000-000000000000', 'Player', 'gamer', 'dnd', **kwargs)
def full_name(self) -> str:
return f'{self.name}#{self.tag}'
class ValorantLocalClient:
lockfile_path = os.path.join(os.environ['LOCALAPPDATA'], r'Riot Games\Riot Client\Config\lockfile')
poll_interval = 0.5
def __init__(self, callback: Callable[[RiotPlayerInfo, bool], None]):
self.callback = callback
self.lockfile_path = os.path.join(os.environ['LOCALAPPDATA'], r'Riot Games\Riot Client\Config\lockfile')
self.port, self.password = None, None
self.running = True
self.players = {}
@@ -228,7 +235,143 @@ class ValconomyHandler:
log.info('Hard luck...')
class HIDValconomyHandler(ValconomyHandler):
pass
vid = 0x6969
pid = 0x0004
def __init__(self):
self.dev = None
self.running = True
self.queue = queue.Queue(128)
self._thread = None
def close(self):
if self.dev is not None:
self.dev.close()
def _dev_ready(self):
try:
if self.dev is None:
dev = hid.device()
dev.open(vendor_id=self.vid, product_id=self.pid)
self.dev = dev
log.info(f'USB device opened')
# 2 bytes: report ID and returned value
# We get back the same report ID and value
# Set to report size + 1 to satisfy W*ndoze
data = self.dev.get_input_report(0, 65)
assert len(data) == 2
return data[1] == 1
except OSError as ex:
if self.dev is not None:
log.warning(f'USB device lost')
self.dev = None
return False
def _do(self, cmd, *vals, fmt=None):
if fmt is None:
fmt = ''
fmt = '<BB' + fmt
# Prepend report ID 0
data = struct.pack(fmt, *(0, cmd) + vals)
self.dev.write(data)
def _enq(self, cmd, *vals, fmt=None):
self.queue.put((cmd, vals, fmt))
def service(self):
while not self.queue.empty():
cmd, vals, fmt = self.queue.get()
while not self._dev_ready():
if not self.running:
return
time.sleep(0.1)
try:
self._do(cmd, *vals, fmt=fmt)
except OSError as ex:
log.warning(f'USB device lost, state dequeuing stalled')
def run(self):
while self.running:
while self.queue.empty():
if not self.running:
return
time.sleep(0.5)
self.service()
def start(self):
assert self._thread is None
self._thread = threading.Thread(name='HIDValconomyHandler', target=self.run)
self._thread.start()
def stop(self):
if self._thread is None or not self.running:
return
self.running = False
self._thread.join()
def none(self):
self._enq(0)
def menu(self, info: RiotPlayerInfo, was_idle: bool=False):
self._enq(1, 1 if was_idle else 0, fmt='B')
def idle(self, info: RiotPlayerInfo):
self._enq(2)
def queue_start(self, info: RiotPlayerInfo):
ms_not_comp = info.valorant.is_party_owner and info.valorant.queue_type == 'unrated'
self._enq(3, 1 if ms_not_comp else 0, fmt='B')
def match_found(self, info: RiotPlayerInfo):
self._enq(4, 1 if info.valorant.queue_type == 'premier-seasonmatch' else 0, fmt='B')
def pregame(self, info: RiotPlayerInfo):
self._enq(5, 1 if info.valorant.map == '/Game/Maps/Bonsai/Bonsai' else 0, fmt='B')
def game_generic(self, info: RiotPlayerInfo):
match info.valorant.queue_type:
case 'hurm': # tdm
gm = 0
case 'deathmatch':
gm = 1
case 'ggteam': # escalation
gm = 2
case 'spikerush':
gm = 3
case _:
if info.valorant.max_party_size == 12: # custom
gm = 4
else:
gm = 5
self._enq(6, gm, fmt='B')
def game_start(self, info: RiotPlayerInfo):
self._enq(7)
def round_start(self, info: RiotPlayerInfo, won: bool, economy: EconomyDecision):
match won:
case False:
won_val = 0
case True:
won_val = 1
case None:
won_val = 2
self._enq(
8,
info.valorant.score, info.valorant.enemy_score,
won_val,
economy.value,
fmt='BBBB')
def game_over(self, info: RiotPlayerInfo, won: bool):
self._enq(9, 1 if won else 0, fmt='B')
class GameState(Enum):
NONE = 0
@@ -265,17 +408,16 @@ class ValconomyStateMachine:
won_last = True if p.valorant.score > self.score else False
self.round_history.append(won_last)
over = False
if p.valorant.queue_type == 'swiftplay':
if p.valorant.score == 5 or p.valorant.enemy_score == 5:
# Game is over
return
over = True
eco = EconomyDecision.BUY
else:
if p.valorant.score > 12 or p.valorant.enemy_score > 12:
if p.valorant.queue_type == 'unrated' or abs(p.valorant.score - p.valorant.enemy_score) >= 2:
# Match is over
return
over = True
eco = EconomyDecision.MATCH_TEAM
rounds_played = p.valorant.score + p.valorant.enemy_score
@@ -300,8 +442,12 @@ class ValconomyStateMachine:
elif rounds_played >= 24:
# Sudden death or overtime (buy either way)
eco = EconomyDecision.BUY
elif rounds_played == 11:
# Last round of half
eco = EconomyDecision.BUY
self.handler.round_start(p, won_last, eco)
if not over:
self.handler.round_start(p, won_last, eco)
self.score = p.valorant.score
self.score_enemy = p.valorant.enemy_score
self.last = GameState.IN_GAME

3
firmware/.gitignore vendored
View File

@@ -5,3 +5,6 @@ sdkconfig.old
*.swp
/main/font/*.c
/main/img/*.c
/assets/moon.png
/assets/star.png
/assets/sleep.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -2,43 +2,6 @@
perSystem = { libMy, pkgs, ... }:
let
genImgsPy = pkgs.python3.withPackages (ps: with ps; [ pillow ]);
genImgs = pkgs.writeScriptBin "gen-imgs" ''
#!${genImgsPy}/bin/python
import itertools
import os
from PIL import Image
imgs = ['bg.png']
for fname in imgs:
with Image.open(os.path.join('assets/', fname)) as img:
w = img.width
h = img.height
data = [(r >> 3) << 11 | (g >> 2) << 5 | (b >> 3) for r, g, b in img.getdata()]
basename = os.path.splitext(fname)[0]
with open(os.path.join('main/img/', basename + '.c'), 'w') as f:
f.write(
'#include <inttypes.h>\n\n'
'#include "lvgl.h"\n\n'
'static const uint16_t _data[] = {\n')
for group in itertools.batched(data, 20):
f.write(' ' + ' '.join(map(lambda i: f'{i:#x},', group)) + '\n')
f.write('};\n\n')
f.write(
f'const lv_image_dsc_t ui_img_{basename} = {{\n'
' .header.magic = LV_IMAGE_HEADER_MAGIC,\n'
' .header.cf = LV_COLOR_FORMAT_RGB565,\n'
f' .header.w = {w},\n'
f' .header.h = {h},\n'
' .data_size = sizeof(_data) * 2,\n'
' .data = (const uint8_t *)_data,\n'
'};\n')
'';
in
{
devenv.shells.firmware = libMy.withRootdir {
@@ -46,7 +9,7 @@
esp-idf-esp32s3
picocom
lv_font_conv
genImgs
imagemagick
];
env = {
@@ -58,11 +21,18 @@
idf.py set-target esp32s3
'';
gen-fonts.exec = ''
for s in 120; do
for s in 40 180; do
DEBUG='*' lv_font_conv --font assets/Tungsten-Bold.ttf --bpp 4 --size $s -r 0x20-0x7F --no-compress \
--format lvgl --lv-include lvgl.h --lv-font-name lv_font_tungsten_"$s" -o main/font/tungsten_"$s".c
done
'';
gen-imgs.exec = ''
for e in moon star sleep; do
magick assets/''${e}_orig.png -resize 120x120 assets/$e.png
done
${genImgsPy}/bin/python gen-imgs.py
'';
};
};
};

54
firmware/gen-imgs.py Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import itertools
import os
import struct
from PIL import Image
def rgb2h(r, g, b):
return (r >> 3) << 11 | (g >> 2) << 5 | (b >> 3)
def bytes_line(bs):
return ' ' + ' '.join(map(lambda i: f'{i:#x},', bs)) + '\n'
imgs = ['bg.png', 'moon.png', 'star.png', 'sleep.png']
for fname in imgs:
alpha = []
with Image.open(os.path.join('assets/', fname)) as img:
w = img.width
h = img.height
if img.has_transparency_data:
has_alpha = True
data = [rgb2h(r, g, b) for r, g, b, _ in img.getdata()]
alpha = [a for _, _, _, a in img.getdata()]
else:
data = [rgb2h(r, g, b) for r, g, b in img.getdata()]
basename = os.path.splitext(fname)[0]
with open(os.path.join('main/img/', basename + '.c'), 'w') as f:
f.write(
'#include <inttypes.h>\n\n'
'#include "lvgl.h"\n\n'
'static const uint8_t _data[] = {\n')
for group in itertools.batched(data, 10):
bs = bytearray()
for c in group:
bs += struct.pack('<H', c)
f.write(bytes_line(bs))
for group in itertools.batched(alpha, 20):
f.write(bytes_line(group))
f.write('};\n\n')
f.write(
f'const lv_image_dsc_t ui_img_{basename} = {{\n'
' .header.magic = LV_IMAGE_HEADER_MAGIC,\n'
f' .header.cf = LV_COLOR_FORMAT_RGB565{"A8" if alpha else ""},\n'
f' .header.w = {w},\n'
f' .header.h = {h},\n'
' .data_size = sizeof(_data),\n'
' .data = _data,\n'
'};\n')

View File

@@ -1,3 +1,6 @@
idf_component_register(
SRCS "valconomy.c" "ui.c" "lcd.c" "usb.c" "font/tungsten_120.c" "img/bg.c"
SRCS
"font/tungsten_40.c" "font/tungsten_180.c"
"img/bg.c" "img/moon.c" "img/star.c" "img/sleep.c"
"valconomy.c" "ui.c" "lcd.c" "usb.c"
INCLUDE_DIRS ".")

View File

@@ -26,20 +26,38 @@
#define LVGL_TASK_PRIORITY 2
#define LVGL_TASK_STACK_SIZE (8 * 1024)
#define WAIT_VSYNC 0
static const char *TAG = "valconomy-lcd";
// LVGL library is not thread-safe, we will call LVGL APIs from different tasks, so use a mutex to protect it
static SemaphoreHandle_t lvgl_mtx = NULL;
#if WAIT_VSYNC
static SemaphoreHandle_t sem_vsync_end = NULL;
static SemaphoreHandle_t sem_gui_ready = NULL;
#endif
static bool val_on_vsync(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_ctx) {
return false;
BaseType_t high_task_awoken = pdFALSE;
#if WAIT_VSYNC
if (xSemaphoreTakeFromISR(sem_gui_ready, &high_task_awoken) == pdTRUE) {
xSemaphoreGiveFromISR(sem_vsync_end, &high_task_awoken);
}
#endif
lv_disp_flush_ready(user_ctx);
return high_task_awoken == pdTRUE;
}
static void val_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) {
esp_lcd_panel_handle_t panel_handle = lv_display_get_user_data(disp);
#if WAIT_VSYNC
xSemaphoreGive(sem_gui_ready);
xSemaphoreTake(sem_vsync_end, portMAX_DELAY);
#endif
// pass the draw buffer to the driver
esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, px_map);
lv_disp_flush_ready(disp);
}
static void val_increase_lvgl_tick(void *arg) {
@@ -225,6 +243,13 @@ lv_display_t *val_setup_lvgl(esp_lcd_panel_handle_t lcd_panel, esp_lcd_touch_han
// set the callback which can copy the rendered image to an area of the display
lv_display_set_flush_cb(display, val_lvgl_flush_cb);
#if WAIT_VSYNC
sem_vsync_end = xSemaphoreCreateBinary();
assert(sem_vsync_end);
sem_gui_ready = xSemaphoreCreateBinary();
assert(sem_gui_ready);
#endif
ESP_LOGI(TAG, "Register event callbacks");
esp_lcd_rgb_panel_event_callbacks_t cbs = {
.on_vsync = val_on_vsync,

View File

@@ -2,35 +2,704 @@
#include "ui.h"
const char *val_ext_gamemodes[] = {
"TDM",
"DEATHMATCH",
"ESCALATION",
"SPIKE RUSH",
"CUSTOM",
"GAME",
};
static const char* M_VAL_TIME = "VAL TIME?";
static const char *val_eco_titles[] = {
"BUY",
"SAVE",
"BONUS",
"MATCH TEAM",
};
static const char *val_eco_subtitles[] = {
"SPEND ALL YOUR MONEY!",
"MAKE SURE YOU CAN BUY NEXT ROUND!",
"KEEP YOUR GUN FROM LAST ROUND!",
"FOLLOW THE TEAM ECONOMY",
};
static const lv_font_t *font_normal = &lv_font_montserrat_24;
static lv_color_t color_primary, color_secondary;
static lv_color_t color_text_hero;
static lv_color_t color_text_hero, color_text_subtitle;
static lv_color_t color_text_won, color_text_lost;
static lv_style_t s_hero;
static lv_style_t s_hero, s_subtitle;
static lv_obj_t *o_container = NULL;
static lv_anim_timeline_t *at_active = NULL;
static lv_anim_timeline_t *at_active_rep = NULL;
static lv_obj_t *o_active = NULL;
static bool state_ready = true;
static const void* imgfont_get_path(
const lv_font_t *font, uint32_t unicode, uint32_t unicode_next, int32_t *offset_y, void *user_data) {
LV_UNUSED(font);
LV_UNUSED(unicode_next);
LV_UNUSED(offset_y);
LV_UNUSED(user_data);
switch (unicode) {
case 0x1F319:
return &ui_img_moon;
case 0x2B50:
return &ui_img_star;
case 0x1F634:
return &ui_img_sleep;
default:
return NULL;
}
}
static void b_cfg_cb(lv_event_t *e) {
lv_obj_t *box = lv_msgbox_create(NULL);
lv_msgbox_add_title(box, "Hello");
lv_msgbox_add_text(box, "test message");
lv_msgbox_add_title(box, "Settings");
lv_msgbox_add_text(box, "Sorry, there aren't any right now.");
lv_msgbox_add_close_button(box);
}
static void ui_home() {
lv_obj_t *i_bg = lv_image_create(lv_screen_active());
lv_image_set_src(i_bg, &ui_img_bg);
static void setup_next_state() {
assert(state_ready);
state_ready = false;
lv_obj_t *l_test = lv_label_create(lv_screen_active());
lv_obj_add_style(l_test, &s_hero, 0);
lv_label_set_text(l_test, "VAL TIME");
if (at_active) {
lv_anim_timeline_delete(at_active);
at_active = NULL;
}
if (at_active_rep) {
lv_anim_timeline_delete(at_active_rep);
at_active_rep = NULL;
}
if (o_active) {
lv_obj_delete(o_active);
o_active = NULL;
}
lv_obj_t *b_cfg = lv_button_create(lv_screen_active());
lv_obj_align(b_cfg, LV_ALIGN_BOTTOM_RIGHT, -10, -10);
lv_obj_add_event_cb(b_cfg, b_cfg_cb, LV_EVENT_CLICKED, NULL);
o_active = lv_obj_create(o_container);
lv_obj_center(o_active);
lv_obj_set_style_bg_opa(o_active, LV_OPA_0, 0);
lv_obj_set_style_border_width(o_active, 0, 0);
lv_obj_set_size(o_active, lv_pct(100), lv_pct(100));
lv_obj_set_scrollbar_mode(o_active, LV_SCROLLBAR_MODE_OFF);
lv_obj_t *l_cfg = lv_label_create(b_cfg);
lv_label_set_text(l_cfg, LV_SYMBOL_SETTINGS);
lv_obj_center(l_cfg);
at_active = lv_anim_timeline_create();
at_active_rep = lv_anim_timeline_create();
lv_anim_timeline_set_repeat_count(at_active_rep, LV_ANIM_REPEAT_INFINITE);
}
bool val_ui_state_ready() {
return state_ready;
}
static void anim_state_ready_cb(lv_anim_t *anim) {
lv_anim_timeline_start(at_active_rep);
state_ready = true;
}
static void anim_x_cb(void *var, int32_t v) {
lv_obj_set_x(var, v);
}
static void anim_y_cb(void *var, int32_t v) {
lv_obj_set_y(var, v);
}
static void anim_opa_cb(void *var, int32_t v) {
lv_obj_set_style_opa(var, v, 0);
}
static void anim_val_time_text(lv_anim_t *anim) {
lv_label_set_text_static(anim->var, M_VAL_TIME);
}
void val_ui_none() {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, M_VAL_TIME);
lv_obj_t *l_subtitle = lv_label_create(o_active);
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
lv_obj_center(l_subtitle);
lv_label_set_text_static(l_subtitle, "SOME VAL IS ALWAYS ON THE MENU...");
lv_obj_update_layout(o_active);
lv_obj_set_pos(
l_subtitle,
-(lv_obj_get_width(l_main) - lv_obj_get_width(l_subtitle)) / 2,
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
lv_label_set_text_static(l_main, "HELLO \U0001F319\u2B50!");
lv_obj_update_layout(o_active);
// Animations
lv_anim_t a_hello_in;
lv_anim_init(&a_hello_in);
lv_anim_set_var(&a_hello_in, l_main);
lv_anim_set_values(
&a_hello_in,
(lv_obj_get_height(o_container) - lv_obj_get_height(l_main)) / 2, 0);
lv_anim_set_exec_cb(&a_hello_in, anim_y_cb);
lv_anim_set_path_cb(&a_hello_in, lv_anim_path_ease_in);
lv_anim_set_duration(&a_hello_in, 500);
lv_anim_t a_hello_out;
lv_anim_init(&a_hello_out);
lv_anim_set_var(&a_hello_out, l_main);
lv_anim_set_values(&a_hello_out, 255, 0);
lv_anim_set_exec_cb(&a_hello_out, anim_opa_cb);
lv_anim_set_path_cb(&a_hello_out, lv_anim_path_linear);
lv_anim_set_duration(&a_hello_out, 500);
lv_anim_t a_val;
lv_anim_init(&a_val);
lv_anim_set_early_apply(&a_val, false);
lv_anim_set_var(&a_val, l_main);
lv_anim_set_values(&a_val, 0, 255);
lv_anim_set_start_cb(&a_val, anim_val_time_text);
lv_anim_set_exec_cb(&a_val, anim_opa_cb);
lv_anim_set_path_cb(&a_val, lv_anim_path_linear);
lv_anim_set_duration(&a_val, 500);
lv_anim_t a_sub;
lv_anim_init(&a_sub);
lv_anim_set_var(&a_sub, l_subtitle);
lv_anim_set_values(&a_sub, 0, 255);
lv_anim_set_exec_cb(&a_sub, anim_opa_cb);
lv_anim_set_path_cb(&a_sub, lv_anim_path_linear);
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
lv_anim_set_duration(&a_sub, 750);
lv_anim_timeline_add(at_active, 0, &a_hello_in);
lv_anim_timeline_add(at_active, 3000, &a_hello_out);
lv_anim_timeline_add(at_active, 3250, &a_val);
lv_anim_timeline_add(at_active, 3750, &a_sub);
lv_anim_timeline_start(at_active);
}
void val_ui_menu(bool was_idle) {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, "LET'S GO");
lv_obj_t *l_subtitle = lv_label_create(o_active);
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
lv_obj_center(l_subtitle);
lv_label_set_text_static(l_subtitle, was_idle ? "WELCOME BACK!" : "GET QUEUEING!");
lv_obj_update_layout(o_active);
// Animations
lv_anim_t a_title;
lv_anim_init(&a_title);
lv_anim_set_var(&a_title, l_main);
lv_anim_set_values(
&a_title,
-(lv_obj_get_height(o_container) + lv_obj_get_height(l_main)) / 2, 0);
lv_anim_set_exec_cb(&a_title, anim_y_cb);
lv_anim_set_path_cb(&a_title, lv_anim_path_ease_in);
lv_anim_set_duration(&a_title, 500);
lv_anim_t a_sub;
lv_anim_init(&a_sub);
lv_anim_set_var(&a_sub, l_subtitle);
lv_anim_set_values(
&a_sub,
(lv_obj_get_height(o_container) + lv_obj_get_height(l_subtitle)) / 2,
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
lv_anim_set_exec_cb(&a_sub, anim_y_cb);
lv_anim_set_path_cb(&a_sub, lv_anim_path_ease_in);
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
lv_anim_set_duration(&a_sub, 500);
lv_anim_timeline_add(at_active, 0, &a_title);
lv_anim_timeline_add(at_active, 0, &a_sub);
// Repeating loop swing back and forth
lv_anim_t a_title_swing;
lv_anim_init(&a_title_swing);
lv_anim_set_early_apply(&a_title_swing, false);
lv_anim_set_var(&a_title_swing, l_main);
lv_anim_set_exec_cb(&a_title_swing, anim_x_cb);
// Left
lv_anim_set_path_cb(&a_title_swing, lv_anim_path_ease_out);
lv_anim_set_values(&a_title_swing, 0, -10);
lv_anim_set_duration(&a_title_swing, 500);
lv_anim_timeline_add(at_active_rep, 0, &a_title_swing);
// Left to right
lv_anim_set_path_cb(&a_title_swing, lv_anim_path_ease_in_out);
lv_anim_set_values(&a_title_swing, -10, 10);
lv_anim_set_duration(&a_title_swing, 1000);
lv_anim_timeline_add(at_active_rep, 500, &a_title_swing);
// Right to centre
lv_anim_set_path_cb(&a_title_swing, lv_anim_path_ease_in);
lv_anim_set_values(&a_title_swing, 10, 0);
lv_anim_set_duration(&a_title_swing, 500);
lv_anim_timeline_add(at_active_rep, 1500, &a_title_swing);
lv_anim_timeline_start(at_active);
}
void val_ui_idle() {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, "\U0001F634");
lv_obj_t *l_subtitle = lv_label_create(o_active);
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
lv_obj_center(l_subtitle);
lv_label_set_text_static(l_subtitle, "COME BACK SOON...");
lv_obj_update_layout(o_active);
lv_obj_set_y(
l_subtitle,
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
// Animations
lv_anim_t a_fade_in;
lv_anim_init(&a_fade_in);
lv_anim_set_values(&a_fade_in, 0, 255);
lv_anim_set_exec_cb(&a_fade_in, anim_opa_cb);
lv_anim_set_path_cb(&a_fade_in, lv_anim_path_ease_in);
lv_anim_set_duration(&a_fade_in, 1000);
lv_anim_set_var(&a_fade_in, l_main);
lv_anim_timeline_add(at_active, 0, &a_fade_in);
lv_anim_set_var(&a_fade_in, l_subtitle);
lv_anim_set_completed_cb(&a_fade_in, anim_state_ready_cb);
lv_anim_timeline_add(at_active, 0, &a_fade_in);
lv_anim_timeline_start(at_active);
}
static void anim_text_color_mix_hero_sub(void *var, int32_t v) {
lv_obj_set_style_text_color(
var, lv_color_mix(color_text_hero, color_text_subtitle, v), 0);
}
void val_ui_queue_start(bool ms_not_comp) {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, "SEARCHING...");
lv_obj_t *l_subtitle = lv_label_create(o_active);
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
lv_obj_center(l_subtitle);
lv_label_set_text_static(l_subtitle, ms_not_comp ? "UHHH SHOULD THAT BE COMP?" : "HOPE YOU FIND A GAME QUICKLY!");
lv_obj_t *spinner = lv_spinner_create(o_active);
lv_obj_set_size(spinner, 100, 100);
lv_obj_center(spinner);
lv_obj_set_style_arc_color(spinner, color_text_hero, LV_PART_INDICATOR);
lv_spinner_set_anim_params(spinner, 1500, 200);
lv_obj_update_layout(o_active);
const int32_t offset = -60;
lv_obj_set_y(l_main, offset);
lv_obj_set_y(
l_subtitle,
offset + (lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
lv_obj_set_y(
spinner,
lv_obj_get_height(o_container) / 4);
// Animations
lv_anim_t a_title;
lv_anim_init(&a_title);
lv_anim_set_var(&a_title, l_main);
lv_anim_set_values(
&a_title,
-(lv_obj_get_width(o_container) + lv_obj_get_width(l_main)) / 2, 0);
lv_anim_set_exec_cb(&a_title, anim_x_cb);
lv_anim_set_path_cb(&a_title, lv_anim_path_ease_out);
lv_anim_set_duration(&a_title, 750);
lv_anim_t a_sub;
lv_anim_init(&a_sub);
lv_anim_set_var(&a_sub, l_subtitle);
lv_anim_set_values(
&a_sub,
(lv_obj_get_width(o_container) + lv_obj_get_width(l_subtitle)) / 2, 0);
lv_anim_set_exec_cb(&a_sub, anim_x_cb);
lv_anim_set_path_cb(&a_sub, lv_anim_path_ease_out);
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
lv_anim_set_duration(&a_sub, 750);
lv_anim_timeline_add(at_active, 0, &a_title);
lv_anim_timeline_add(at_active, 0, &a_sub);
if (ms_not_comp) {
lv_anim_t a_warn;
lv_anim_init(&a_warn);
lv_anim_set_early_apply(&a_warn, false);
lv_anim_set_var(&a_warn, l_subtitle);
lv_anim_set_path_cb(&a_warn, lv_anim_path_linear);
lv_anim_set_duration(&a_warn, 1000);
lv_anim_set_exec_cb(&a_warn, anim_text_color_mix_hero_sub);
lv_anim_set_values(&a_warn, 0, 255);
lv_anim_timeline_add(at_active_rep, 0, &a_warn);
lv_anim_set_values(&a_warn, 255, 0);
lv_anim_timeline_add(at_active_rep, 1000, &a_warn);
}
lv_anim_timeline_start(at_active);
}
void val_ui_match_found(bool is_premier) {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, "MATCH FOUND");
lv_obj_t *l_subtitle = lv_label_create(o_active);
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
lv_obj_center(l_subtitle);
lv_label_set_text_static(l_subtitle, is_premier ? "DO THE COSMONAUTS PROUD!" : "I HOPE IT'S NOT SPLIT...");
lv_obj_update_layout(o_active);
// Animations
lv_anim_t a_title;
lv_anim_init(&a_title);
lv_anim_set_var(&a_title, l_main);
lv_anim_set_values(
&a_title,
(lv_obj_get_height(o_container) + lv_obj_get_height(l_main)) / 2, 0);
lv_anim_set_exec_cb(&a_title, anim_y_cb);
lv_anim_set_path_cb(&a_title, lv_anim_path_ease_in);
lv_anim_set_duration(&a_title, 500);
lv_anim_t a_sub;
lv_anim_init(&a_sub);
lv_anim_set_var(&a_sub, l_subtitle);
lv_anim_set_values(
&a_sub,
-(lv_obj_get_height(o_container) + lv_obj_get_height(l_main)) / 2,
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
lv_anim_set_exec_cb(&a_sub, anim_y_cb);
lv_anim_set_path_cb(&a_sub, lv_anim_path_ease_in);
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
lv_anim_set_duration(&a_sub, 500);
lv_anim_timeline_add(at_active, 0, &a_title);
lv_anim_timeline_add(at_active, 0, &a_sub);
lv_anim_timeline_start(at_active);
}
void val_ui_pregame(bool is_split) {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, "CHOOSE AGENT");
lv_obj_t *l_subtitle = lv_label_create(o_active);
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
lv_obj_center(l_subtitle);
lv_label_set_text_static(l_subtitle, is_split ? "EWWW, SPLIT..." : "PICK SOMETHING GOOD!");
lv_obj_update_layout(o_active);
lv_obj_set_y(
l_subtitle,
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
// Animations
lv_anim_t a_fade_in;
lv_anim_init(&a_fade_in);
lv_anim_set_values(&a_fade_in, 0, 255);
lv_anim_set_exec_cb(&a_fade_in, anim_opa_cb);
lv_anim_set_path_cb(&a_fade_in, lv_anim_path_ease_in);
lv_anim_set_duration(&a_fade_in, 500);
lv_anim_set_var(&a_fade_in, l_main);
lv_anim_timeline_add(at_active, 0, &a_fade_in);
lv_anim_set_var(&a_fade_in, l_subtitle);
lv_anim_set_completed_cb(&a_fade_in, anim_state_ready_cb);
lv_anim_timeline_add(at_active, 0, &a_fade_in);
lv_anim_t a_warn;
lv_anim_init(&a_warn);
lv_anim_set_early_apply(&a_warn, false);
lv_anim_set_var(&a_warn, l_main);
lv_anim_set_path_cb(&a_warn, lv_anim_path_linear);
lv_anim_set_duration(&a_warn, 2000);
lv_anim_set_exec_cb(&a_warn, anim_text_color_mix_hero_sub);
lv_anim_set_values(&a_warn, 255, 0);
lv_anim_timeline_add(at_active_rep, 0, &a_warn);
lv_anim_set_values(&a_warn, 0, 255);
lv_anim_timeline_add(at_active_rep, 2000, &a_warn);
lv_anim_timeline_start(at_active);
}
void val_ui_game_generic(const char *gamemode) {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, "HAVE FUN!");
lv_obj_t *l_subtitle = lv_label_create(o_active);
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
lv_obj_center(l_subtitle);
lv_label_set_text_fmt(l_subtitle, "ENJOY YOUR %s!", gamemode);
lv_obj_update_layout(o_active);
lv_obj_set_y(
l_subtitle,
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
// Animations
lv_anim_t a_title;
lv_anim_init(&a_title);
lv_anim_set_var(&a_title, l_main);
lv_anim_set_values(
&a_title,
-(lv_obj_get_width(o_container) + lv_obj_get_width(l_main)) / 2, 0);
lv_anim_set_exec_cb(&a_title, anim_x_cb);
lv_anim_set_path_cb(&a_title, lv_anim_path_bounce);
lv_anim_set_duration(&a_title, 750);
lv_anim_t a_sub;
lv_anim_init(&a_sub);
lv_anim_set_var(&a_sub, l_subtitle);
lv_anim_set_values(
&a_sub,
-(lv_obj_get_width(o_container) + lv_obj_get_width(l_subtitle)) / 2,
-(lv_obj_get_width(l_main) - lv_obj_get_width(l_subtitle)) / 2);
lv_anim_set_exec_cb(&a_sub, anim_x_cb);
lv_anim_set_path_cb(&a_sub, lv_anim_path_bounce);
lv_anim_set_completed_cb(&a_sub, anim_state_ready_cb);
lv_anim_set_duration(&a_sub, 750);
lv_anim_timeline_add(at_active, 0, &a_title);
lv_anim_timeline_add(at_active, 0, &a_sub);
lv_anim_timeline_start(at_active);
}
void val_ui_game_start() {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, "GOOD LUCK!");
state_ready = true;
}
static void anim_text_color_mix_hero_won(void *var, int32_t v) {
lv_obj_set_style_text_color(
var, lv_color_mix(color_text_hero, color_text_won, v), 0);
}
static void anim_text_color_mix_hero_lost(void *var, int32_t v) {
lv_obj_set_style_text_color(
var, lv_color_mix(color_text_hero, color_text_lost, v), 0);
}
void val_ui_round_start(uint8_t score, uint8_t score_enemy, val_won_t won, val_eco_decision_t eco) {
assert(won <= ROUND_NONE && eco <= ECO_MATCH_TEAM);
setup_next_state();
// Widgets
lv_obj_t *l_score = lv_label_create(o_active);
lv_obj_add_style(l_score, &s_hero, 0);
lv_obj_center(l_score);
lv_label_set_text_fmt(l_score, "%hhu-%hhu", score, score_enemy);
lv_obj_t *l_eco = lv_label_create(o_active);
lv_obj_add_style(l_eco, &s_hero, 0);
lv_obj_center(l_eco);
lv_label_set_text_static(l_eco, val_eco_titles[eco]);
lv_obj_t *l_advice = lv_label_create(o_active);
lv_obj_add_style(l_advice, &s_subtitle, 0);
lv_obj_center(l_advice);
lv_label_set_text_static(l_advice, val_eco_subtitles[eco]);
lv_obj_update_layout(o_active);
lv_obj_set_y(
l_score,
-(lv_obj_get_height(l_score) / 2) - 20);
lv_obj_set_y(
l_eco,
lv_obj_get_height(l_eco) / 2);
// Animations
// Score and eco fade in
lv_anim_t a_fade_in;
lv_anim_init(&a_fade_in);
lv_anim_set_values(&a_fade_in, 0, 255);
lv_anim_set_exec_cb(&a_fade_in, anim_opa_cb);
lv_anim_set_path_cb(&a_fade_in, lv_anim_path_ease_in);
lv_anim_set_duration(&a_fade_in, 750);
lv_anim_set_var(&a_fade_in, l_score);
lv_anim_timeline_add(at_active, 0, &a_fade_in);
lv_anim_set_var(&a_fade_in, l_eco);
lv_anim_timeline_add(at_active, 0, &a_fade_in);
// Advice slide up
lv_anim_t a_advice;
lv_anim_init(&a_advice);
lv_anim_set_var(&a_advice, l_advice);
lv_anim_set_values(
&a_advice,
(lv_obj_get_height(o_container) + lv_obj_get_height(l_advice)) / 2,
lv_obj_get_height(l_eco) + (lv_obj_get_height(l_advice) / 2) + 5);
lv_anim_set_exec_cb(&a_advice, anim_y_cb);
lv_anim_set_path_cb(&a_advice, lv_anim_path_ease_out);
lv_anim_set_duration(&a_advice, 750);
if (won != ROUND_NONE) {
// Score colour
lv_anim_t a_score;
lv_anim_init(&a_score);
lv_anim_set_var(&a_score, l_score);
lv_anim_set_path_cb(&a_score, lv_anim_path_ease_in);
lv_anim_set_exec_cb(
&a_score,
won == ROUND_WON ? anim_text_color_mix_hero_won : anim_text_color_mix_hero_lost);
lv_anim_set_duration(&a_score, 2000);
lv_anim_set_values(&a_score, 0, 255);
lv_anim_set_completed_cb(&a_score, anim_state_ready_cb);
lv_anim_timeline_add(at_active, 0, &a_score);
} else {
lv_anim_set_completed_cb(&a_advice, anim_state_ready_cb);
}
lv_anim_timeline_add(at_active, 0, &a_advice);
// Eco flash
lv_anim_t a_eco;
lv_anim_init(&a_eco);
lv_anim_set_early_apply(&a_eco, false);
lv_anim_set_var(&a_eco, l_eco);
lv_anim_set_path_cb(&a_eco, lv_anim_path_linear);
lv_anim_set_duration(&a_eco, 1000);
lv_anim_set_exec_cb(&a_eco, anim_text_color_mix_hero_sub);
lv_anim_set_values(&a_eco, 255, 0);
lv_anim_timeline_add(at_active_rep, 0, &a_eco);
lv_anim_set_values(&a_eco, 0, 255);
lv_anim_timeline_add(at_active_rep, 1000, &a_eco);
lv_anim_timeline_start(at_active);
}
void val_ui_game_over(bool won) {
setup_next_state();
// Widgets
lv_obj_t *l_main = lv_label_create(o_active);
lv_obj_add_style(l_main, &s_hero, 0);
lv_obj_center(l_main);
lv_label_set_text_static(l_main, won ? "VICTORY" : "DEFEAT");
lv_obj_set_style_text_color(l_main, won ? color_text_won : color_text_lost, 0);
lv_obj_t *l_subtitle = lv_label_create(o_active);
lv_obj_add_style(l_subtitle, &s_subtitle, 0);
lv_obj_center(l_subtitle);
lv_label_set_text_static(l_subtitle, won ? "WELL PLAYED!" : "HARD LUCK...");
lv_obj_update_layout(o_active);
lv_obj_set_y(
l_subtitle,
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
// Animations
if (won) {
lv_anim_t a_fly;
lv_anim_init(&a_fly);
lv_anim_set_path_cb(&a_fly, lv_anim_path_ease_out);
lv_anim_set_duration(&a_fly, 750);
// Main text from top-left
// Y
lv_anim_set_var(&a_fly, l_main);
lv_anim_set_exec_cb(&a_fly, anim_y_cb);
lv_anim_set_values(
&a_fly,
-(lv_obj_get_height(o_container) + lv_obj_get_height(l_main)) / 2, 0);
lv_anim_timeline_add(at_active, 0, &a_fly);
// X
lv_anim_set_exec_cb(&a_fly, anim_x_cb);
lv_anim_set_values(
&a_fly,
-(lv_obj_get_width(o_container) + lv_obj_get_width(l_main)) / 2, 0);
lv_anim_timeline_add(at_active, 0, &a_fly);
// Subtitle from bottom-right
// Y
lv_anim_set_var(&a_fly, l_subtitle);
lv_anim_set_exec_cb(&a_fly, anim_y_cb);
lv_anim_set_values(
&a_fly,
(lv_obj_get_height(o_container) + lv_obj_get_height(l_subtitle)) / 2,
(lv_obj_get_height(l_main) + lv_obj_get_height(l_subtitle)) / 2 + 5);
lv_anim_timeline_add(at_active, 0, &a_fly);
// X
lv_anim_set_exec_cb(&a_fly, anim_x_cb);
lv_anim_set_values(
&a_fly,
(lv_obj_get_width(o_container) + lv_obj_get_width(l_subtitle)) / 2, 0);
lv_anim_set_completed_cb(&a_fly, anim_state_ready_cb);
lv_anim_timeline_add(at_active, 0, &a_fly);
} else {
lv_anim_t a_fade_in;
lv_anim_init(&a_fade_in);
lv_anim_set_values(&a_fade_in, 0, 255);
lv_anim_set_exec_cb(&a_fade_in, anim_opa_cb);
lv_anim_set_path_cb(&a_fade_in, lv_anim_path_ease_in);
lv_anim_set_duration(&a_fade_in, 1000);
lv_anim_set_var(&a_fade_in, l_main);
lv_anim_timeline_add(at_active, 0, &a_fade_in);
lv_anim_set_var(&a_fade_in, l_subtitle);
lv_anim_set_completed_cb(&a_fade_in, anim_state_ready_cb);
lv_anim_timeline_add(at_active, 0, &a_fade_in);
}
lv_anim_timeline_start(at_active);
}
void val_lvgl_ui(lv_display_t *disp) {
@@ -38,17 +707,49 @@ void val_lvgl_ui(lv_display_t *disp) {
color_secondary = lv_color_hex(0xf7518f);
color_text_hero = lv_palette_lighten(LV_PALETTE_GREY, 2);
color_text_subtitle = lv_palette_darken(LV_PALETTE_GREY, 1);
color_text_won = lv_palette_lighten(LV_PALETTE_GREEN, 1);
color_text_lost = lv_palette_darken(LV_PALETTE_RED, 4);
// init default theme
lv_theme_default_init(
disp, color_primary, color_secondary,
true, // dark theme
font_normal);
// lv_sysmon_hide_performance(disp);
lv_sysmon_hide_performance(disp);
lv_font_t *f_hero_emoji = lv_imgfont_create(120, imgfont_get_path, NULL);
assert(f_hero_emoji);
f_hero_emoji->fallback = &lv_font_tungsten_180;
lv_style_init(&s_hero);
lv_style_set_text_font(&s_hero, &lv_font_tungsten_120);
lv_style_set_text_font(&s_hero, f_hero_emoji);
lv_style_set_text_color(&s_hero, color_text_hero);
ui_home();
lv_style_init(&s_subtitle);
lv_style_set_text_font(&s_subtitle, &lv_font_tungsten_40);
lv_style_set_text_color(&s_subtitle, color_text_subtitle);
// Background
lv_obj_t *i_bg = lv_image_create(lv_screen_active());
lv_image_set_src(i_bg, &ui_img_bg);
// Content container
o_container = lv_obj_create(lv_screen_active());
lv_obj_set_style_bg_opa(o_container, LV_OPA_0, 0);
lv_obj_set_style_border_width(o_container, 0, 0);
lv_obj_set_style_pad_all(o_container, 0, 0);
lv_obj_set_size(o_container, lv_pct(100), lv_pct(100));
lv_obj_set_scrollbar_mode(o_container, LV_SCROLLBAR_MODE_OFF);
// Settings button
lv_obj_t *b_cfg = lv_button_create(lv_screen_active());
lv_obj_align(b_cfg, LV_ALIGN_BOTTOM_RIGHT, -10, -10);
lv_obj_add_event_cb(b_cfg, b_cfg_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *l_cfg = lv_label_create(b_cfg);
lv_label_set_text(l_cfg, LV_SYMBOL_SETTINGS);
lv_obj_center(l_cfg);
val_ui_none();
}

View File

@@ -2,8 +2,40 @@
#include "lvgl.h"
LV_FONT_DECLARE(lv_font_tungsten_120)
extern const char *val_ext_gamemodes[];
#define VAL_EXT_GAMEMODES_SIZE 6
typedef enum val_eco_decision {
ECO_BUY,
ECO_SAVE,
ECO_BONUS,
ECO_MATCH_TEAM,
} val_eco_decision_t;
typedef enum val_won {
ROUND_LOST,
ROUND_WON,
ROUND_NONE,
} val_won_t;
LV_FONT_DECLARE(lv_font_tungsten_40)
LV_FONT_DECLARE(lv_font_tungsten_180)
LV_IMAGE_DECLARE(ui_img_bg);
LV_IMAGE_DECLARE(ui_img_moon);
LV_IMAGE_DECLARE(ui_img_star);
LV_IMAGE_DECLARE(ui_img_sleep);
bool val_ui_state_ready();
void val_ui_none();
void val_ui_menu(bool was_idle);
void val_ui_idle();
void val_ui_queue_start(bool ms_not_comp);
void val_ui_match_found(bool is_premier);
void val_ui_pregame(bool is_split);
void val_ui_game_generic(const char *gamemode);
void val_ui_game_start();
void val_ui_round_start(uint8_t score, uint8_t score_enemy, val_won_t won, val_eco_decision_t eco);
void val_ui_game_over(bool won);
void val_lvgl_ui(lv_display_t *disp);

View File

@@ -7,6 +7,8 @@
#include "common.h"
#include "usb.h"
#include "lcd.h"
#include "ui.h"
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN)
@@ -76,24 +78,100 @@ uint8_t const *tud_hid_descriptor_report_cb(uint8_t instance) {
// Invoked when received GET_REPORT control request
// Application must fill buffer report's content and return its length.
// Return zero will cause the stack to STALL request
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) {
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buf, uint16_t reqlen) {
(void) instance;
(void) report_id;
(void) report_type;
(void) buffer;
(void) reqlen;
if (report_id != 0 || reqlen < 1) {
return 0;
}
return 0;
// ESP_LOGI(TAG, "Got %hu bytes report %hhu", reqlen, report_id);
// for (uint16_t i = 0; i < reqlen; i++) {
// ESP_LOGI(TAG, "b: %02hhx", buf[i]);
// }
buf[0] = val_ui_state_ready();
return 1;
}
// Invoked when received SET_REPORT control request or
// received data on OUT endpoint ( Report ID = 0, Type = 0 )
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) {
assert(report_id == 0 && report_type == HID_REPORT_TYPE_OUTPUT);
ESP_LOGI(TAG, "Got %hu bytes report %hhu", bufsize, report_id);
for (uint16_t i = 0; i < bufsize; i++) {
ESP_LOGI(TAG, "b: %02hhx", buffer[i]);
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buf, uint16_t bufsize) {
(void) instance;
assert(report_id == 0 && report_type == HID_REPORT_TYPE_OUTPUT && bufsize >= 1);
if (!val_lvgl_lock(-1)) {
ESP_LOGE(TAG, "Failed to grab LVGL lock");
return;
}
if (buf[0] > ST_GAME_OVER) {
ESP_LOGW(TAG, "Unknown state %hhu", buf[0]);
goto ret;
}
if (!val_ui_state_ready()) {
goto ret;
}
switch (buf[0]) {
case ST_NONE:
val_ui_none();
break;
case ST_MENU:
if (bufsize < 2) {
ESP_LOGE(TAG, "Invalid ST_MENU command");
goto ret;
}
val_ui_menu((bool)buf[1]);
break;
case ST_IDLE:
val_ui_idle();
break;
case ST_QUEUE_START:
if (bufsize < 2) {
ESP_LOGE(TAG, "Invalid ST_QUEUE_START command");
goto ret;
}
val_ui_queue_start((bool)buf[1]);
break;
case ST_MATCH_FOUND:
if (bufsize < 2) {
ESP_LOGE(TAG, "Invalid ST_MATCH_FOUND command");
goto ret;
}
val_ui_match_found((bool)buf[1]);
break;
case ST_PREGAME:
if (bufsize < 2) {
ESP_LOGE(TAG, "Invalid ST_PREGAME command");
goto ret;
}
val_ui_pregame((bool)buf[1]);
break;
case ST_GAME_GENERIC:
if (bufsize < 2 || buf[1] > VAL_EXT_GAMEMODES_SIZE) {
ESP_LOGE(TAG, "Invalid ST_PREGAME command");
goto ret;
}
val_ui_game_generic(val_ext_gamemodes[buf[1]]);
break;
case ST_GAME_START:
val_ui_game_start();
break;
case ST_ROUND_START:
if (bufsize < 5 || buf[3] > ROUND_NONE || buf[4] > ECO_MATCH_TEAM) {
ESP_LOGE(TAG, "Invalid ST_ROUND_START command");
goto ret;
}
val_ui_round_start(buf[1], buf[2], buf[3], buf[4]);
break;
case ST_GAME_OVER:
if (bufsize < 2) {
ESP_LOGE(TAG, "Invalid ST_GAME_OVER command");
goto ret;
}
val_ui_game_over((bool)buf[1]);
break;
}
ret:
val_lvgl_unlock();
}
void val_usb_init(void) {

View File

@@ -5,4 +5,17 @@
#define EPNUM_HID 0x01
#define USB_EP_BUFSIZE 64
typedef enum val_state {
ST_NONE = 0,
ST_MENU,
ST_IDLE,
ST_QUEUE_START,
ST_MATCH_FOUND,
ST_PREGAME,
ST_GAME_GENERIC,
ST_GAME_START,
ST_ROUND_START,
ST_GAME_OVER,
} val_state_t;
void val_usb_init(void);

View File

@@ -7,6 +7,7 @@ CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_120M=y
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_USJ_ENABLE_USB_SERIAL_JTAG=n
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
@@ -20,10 +21,10 @@ CONFIG_ESP_WIFI_DPP_SUPPORT=y
CONFIG_FREERTOS_HZ=1000
CONFIG_TINYUSB_DEBUG_LEVEL=0
CONFIG_TINYUSB_HID_COUNT=1
CONFIG_LV_DEF_REFR_PERIOD=24
CONFIG_LV_USE_LOG=y
CONFIG_LV_LOG_PRINTF=y
CONFIG_LV_FONT_MONTSERRAT_24=y
CONFIG_LV_USE_SYSMON=y
CONFIG_LV_USE_PERF_MONITOR=y
CONFIG_LV_USE_IMGFONT=y
CONFIG_IDF_EXPERIMENTAL_FEATURES=y