mirror of
https://github.com/TheCriicom/omega-dashboard-ps5.git
synced 2026-10-17 11:01:45 +02:00
- Server: registro delle sessioni (migrazione 018) e GET /api/v1/wrap con tempo totale, giochi preferiti, ritmo per ora e giorno, record, trofei del periodo, amici con cui hai giocato (solo chi mostra la sua attività), confronto anonimo con la community e profilo di giocatore. - Console: schede animate in Community › Tempo di gioco (Triangolo), scelta del periodo, condivisione in bacheca e avviso settimanale. - Web app: pagina /wrap con le stesse schede e immagine da condividere. - Testi tradotti in 27 lingue; nuovo test con 38 controlli.
584 lines
29 KiB
C
584 lines
29 KiB
C
// Omega UI — Il tuo riepilogo, come il Wrap-Up di PlayStation: una settimana,
|
||
// un mese o un anno di gioco raccontati a schede, una dopo l'altra, come le
|
||
// storie. Tempo giocato, il gioco preferito, i primi cinque, il ritmo (ore e
|
||
// giorni), i record, i trofei, gli amici con cui hai giocato, il confronto con
|
||
// la community e, alla fine, il tuo profilo da condividere sulla bacheca.
|
||
// I numeri li calcola il server (GET /api/v1/wrap, endpoints/wrap.js) nel fuso
|
||
// della console. Una volta a settimana, se la settimana prima hai giocato
|
||
// almeno mezz'ora, una notifica avvisa che il riepilogo è pronto.
|
||
#include "app.h"
|
||
#include <math.h>
|
||
#include <stdlib.h>
|
||
#include <time.h>
|
||
|
||
#define SLIDE_SECS 7.0f
|
||
|
||
enum { S_INTRO, S_TOTAL, S_TOP, S_TOP5, S_RHYTHM, S_RECORDS, S_TROPHIES, S_TOGETHER, S_COMMUNITY, S_PERSONA, S_EMPTY };
|
||
|
||
typedef struct { char gid[24], name[100], icon[20]; long secs; int sessions; } WGame;
|
||
typedef struct { char oid[32]; int avatar; long secs; char game[100]; } WFriend;
|
||
|
||
static const struct { const char *period; int back; const char *label; const char *title; } OPTS[] = {
|
||
{ "week", 0, N_("Questa settimana"), N_("La tua settimana") },
|
||
{ "week", 1, N_("Settimana scorsa"), N_("La tua settimana scorsa") },
|
||
{ "month", 0, N_("Questo mese"), N_("Il tuo mese") },
|
||
{ "month", 1, N_("Mese scorso"), N_("Il tuo mese scorso") },
|
||
{ "year", 0, N_("Quest'anno"), N_("Il tuo anno") },
|
||
};
|
||
#define NOPTS (int)(sizeof OPTS / sizeof *OPTS)
|
||
|
||
static const struct { const char *key; int icon; Col c; const char *title, *desc; } PERSONAS[] = {
|
||
{ "night_owl", IC_STAR, { 120, 110, 255, 255 }, N_("Nottambulo"), N_("Le tue partite migliori arrivano quando gli altri dormono.") },
|
||
{ "early_bird", IC_IDEA, { 255, 190, 70, 255 }, N_("Mattiniero"), N_("Giochi presto, prima di tutto il resto.") },
|
||
{ "marathoner", IC_FIRE, { 255, 110, 70, 255 }, N_("Maratoneta"), N_("Quando inizi una partita, non ti fermi pi\xC3\xB9.") },
|
||
{ "hunter", IC_TROPHY, { 255, 200, 60, 255 }, N_("Cacciatore di trofei"), N_("Ogni trofeo prima o poi \xC3\xA8 tuo.") },
|
||
{ "explorer", IC_GLOBE, { 70, 200, 170, 255 }, N_("Esploratore"), N_("Tanti giochi diversi: ti piace scoprire cose nuove.") },
|
||
{ "loyal", IC_HEART, { 255, 90, 130, 255 }, N_("Fedelissimo"), N_("Un gioco sopra tutti: lo conosci a memoria.") },
|
||
{ "weekend", IC_PARTY, { 200, 100, 255, 255 }, N_("Guerriero del weekend"), N_("Il fine settimana \xC3\xA8 fatto per giocare.") },
|
||
{ "daily", IC_CLOCK, { 90, 170, 255, 255 }, N_("Costante"), N_("Un po' ogni giorno, senza saltarne uno.") },
|
||
{ "social", IC_FRIENDS, { 255, 120, 200, 255 }, N_("Giocatore di squadra"), N_("Con gli amici giochi meglio.") },
|
||
{ "casual", IC_GAMEPAD, { 110, 200, 255, 255 }, N_("Rilassato"), N_("Giochi quando ti va, senza fretta.") },
|
||
};
|
||
#define NPERSONAS (int)(sizeof PERSONAS / sizeof *PERSONAS)
|
||
|
||
static struct {
|
||
int opt, loading, status, loaded;
|
||
long total, prev; int games_count, sessions, new_games, days, streak, percentile, players, complete;
|
||
WGame games[5]; int ngames;
|
||
int has_rhythm; long hours[24], wd[7];
|
||
int has_longest; WGame longest;
|
||
int tr_count, tr[4]; char tr_name[130], tr_game[110], tr_icon[20]; int tr_grade, tr_pct, has_rarest;
|
||
WFriend friends[3]; int nfriends;
|
||
int persona;
|
||
time_t from, to;
|
||
int slides[12], nslides, cur;
|
||
float st, enter;
|
||
} W;
|
||
|
||
static const Col GRADE_COL[4] = { { 176, 222, 255, 255 }, { 255, 200, 60, 255 }, { 200, 210, 225, 255 }, { 215, 140, 80, 255 } };
|
||
|
||
// ------------------------------------------------------------------ utilità --
|
||
int wrap_tz(void) {
|
||
time_t t = time(NULL); struct tm g = *gmtime(&t), l = *localtime(&t);
|
||
int d = (l.tm_hour - g.tm_hour) * 60 + (l.tm_min - g.tm_min);
|
||
if (l.tm_yday != g.tm_yday) d += (l.tm_yday > g.tm_yday || (l.tm_yday == 0 && g.tm_yday > 300)) ? 1440 : -1440;
|
||
return d;
|
||
}
|
||
|
||
static time_t parse_iso(const char *s) {
|
||
struct tm tm; memset(&tm, 0, sizeof tm);
|
||
if (sscanf(s, "%d-%d-%dT%d:%d:%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) < 3) return 0;
|
||
tm.tm_year -= 1900; tm.tm_mon -= 1;
|
||
return timegm(&tm);
|
||
}
|
||
|
||
// "3 h 20 min", "45 min"
|
||
static void dur(long secs, char *out, size_t n) {
|
||
long h = secs / 3600, m = (secs % 3600) / 60;
|
||
if (!h) snprintf(out, n, _("%ld min"), m);
|
||
else if (!m || h >= 100) snprintf(out, n, _("%ld h"), h);
|
||
else snprintf(out, n, _("%ld h %ld min"), h, m);
|
||
}
|
||
|
||
// "28 set – 4 ott"
|
||
static void range_text(char *out, size_t n) {
|
||
static const char *const MO[12] = { N_("gen"), N_("feb"), N_("mar"), N_("apr"), N_("mag"), N_("giu"), N_("lug"), N_("ago"), N_("set"), N_("ott"), N_("nov"), N_("dic") };
|
||
if (!W.from || !W.to) { out[0] = 0; return; }
|
||
time_t a = W.from, b = W.to - 1;
|
||
struct tm la = *localtime(&a), lb = *localtime(&b);
|
||
time_t now = time(NULL); if (b > now) { b = now; lb = *localtime(&b); }
|
||
if (!strcmp(OPTS[W.opt].period, "year")) snprintf(out, n, "%d", la.tm_year + 1900);
|
||
else if (strchr("jkz", i18n_code()[0])) // ja, ko, zh: 9/28 – 10/4
|
||
snprintf(out, n, "%d/%d \xE2\x80\x93 %d/%d", la.tm_mon + 1, la.tm_mday, lb.tm_mon + 1, lb.tm_mday);
|
||
else snprintf(out, n, "%d %s \xE2\x80\x93 %d %s", la.tm_mday, _(MO[la.tm_mon % 12]), lb.tm_mday, _(MO[lb.tm_mon % 12]));
|
||
}
|
||
|
||
static void wgame_icon(const char *gid, const char *media, int x, int y, int s, int a) {
|
||
for (int k = 0; k < napps; k++) if (apps[k].tex && !strcmp(apps[k].tid, gid)) { draw_tex(apps[k].tex, x, y, s, s, a); return; }
|
||
if (media[0] && draw_media_frames(media, 1, x, y, s, s, 128, 128, s / 6, 0, a)) return;
|
||
fill_rrect(x, y, s, s, s / 6, RGB(40, 48, 70), a);
|
||
draw_icon(IC_GAMEPAD, x + s / 2, y + s / 2, s * 55 / 100, C_DIM, a);
|
||
}
|
||
|
||
static void parse_game(JVal *o, WGame *g) {
|
||
memset(g, 0, sizeof *g);
|
||
jcpy(g->gid, sizeof g->gid, o, "game_id"); jcpy(g->name, sizeof g->name, o, "game_name"); jcpy(g->icon, sizeof g->icon, o, "game_icon");
|
||
if (!g->name[0]) snprintf(g->name, sizeof g->name, "%s", g->gid);
|
||
g->secs = (long)jnum(o, "seconds", 0); g->sessions = (int)jnum(o, "sessions", 0);
|
||
}
|
||
|
||
static int grade_index(const char *g) { return g[0] == 'P' ? 0 : g[0] == 'G' ? 1 : g[0] == 'S' ? 2 : 3; }
|
||
|
||
static void build_slides(void) {
|
||
int n = 0;
|
||
W.slides[n++] = S_INTRO;
|
||
if (W.total <= 0) W.slides[n++] = S_EMPTY;
|
||
else {
|
||
W.slides[n++] = S_TOTAL;
|
||
if (W.ngames) W.slides[n++] = S_TOP;
|
||
if (W.ngames >= 2) W.slides[n++] = S_TOP5;
|
||
if (W.has_rhythm) W.slides[n++] = S_RHYTHM;
|
||
if (W.has_longest || W.streak >= 2 || W.new_games) W.slides[n++] = S_RECORDS;
|
||
if (W.tr_count) W.slides[n++] = S_TROPHIES;
|
||
if (W.nfriends) W.slides[n++] = S_TOGETHER;
|
||
if (W.percentile >= 0) W.slides[n++] = S_COMMUNITY;
|
||
W.slides[n++] = S_PERSONA;
|
||
}
|
||
W.nslides = n;
|
||
if (W.cur >= n) W.cur = 0;
|
||
}
|
||
|
||
static void on_wrap(int st, JVal *j, const char *raw, void *ud) {
|
||
(void)raw;
|
||
if ((intptr_t)ud != W.opt) return; // risposta di un periodo già lasciato
|
||
W.loading = 0; W.status = st;
|
||
if (st != 200) { set_msg(st == 429 ? _("Troppe richieste: riprova tra poco") : _("Riepilogo non disponibile"), 1); return; }
|
||
W.total = (long)jnum(j, "total_seconds", 0); W.prev = (long)jnum(j, "prev_total_seconds", 0);
|
||
W.games_count = (int)jnum(j, "games_count", 0); W.sessions = (int)jnum(j, "sessions", 0);
|
||
W.new_games = (int)jnum(j, "new_games", 0); W.days = (int)jnum(j, "days_active", 0); W.streak = (int)jnum(j, "streak", 0);
|
||
JVal *pc = jget(j, "percentile");
|
||
W.percentile = pc && pc->t == J_NUM ? (int)pc->n : -1;
|
||
W.players = (int)jnum(j, "players", 0); W.complete = jbool(j, "complete");
|
||
W.from = parse_iso(jstr(j, "from", "")); W.to = parse_iso(jstr(j, "to", ""));
|
||
W.ngames = 0;
|
||
JFOR(o, jget(j, "games")) { if (W.ngames >= 5) break; parse_game(o, &W.games[W.ngames++]); }
|
||
W.has_rhythm = 0;
|
||
JVal *h = jget(j, "hours"), *d = jget(j, "weekdays");
|
||
if (h && h->t == J_ARR && jlen(h) == 24 && d && d->t == J_ARR && jlen(d) == 7) {
|
||
int i = 0; JFOR(o, h) W.hours[i++] = (long)o->n;
|
||
i = 0; JFOR(o, d) W.wd[i++] = (long)o->n;
|
||
W.has_rhythm = 1;
|
||
}
|
||
JVal *l = jget(j, "longest");
|
||
W.has_longest = l && l->t == J_OBJ;
|
||
if (W.has_longest) parse_game(l, &W.longest);
|
||
JVal *t = jget(j, "trophies");
|
||
W.tr_count = (int)jnum(t, "count", 0);
|
||
W.tr[0] = (int)jnum(t, "p", 0); W.tr[1] = (int)jnum(t, "g", 0); W.tr[2] = (int)jnum(t, "s", 0); W.tr[3] = (int)jnum(t, "b", 0);
|
||
JVal *r = jget(t, "rarest");
|
||
W.has_rarest = r && r->t == J_OBJ;
|
||
if (W.has_rarest) {
|
||
jcpy(W.tr_name, sizeof W.tr_name, r, "name"); jcpy(W.tr_game, sizeof W.tr_game, r, "game"); jcpy(W.tr_icon, sizeof W.tr_icon, r, "icon_media");
|
||
W.tr_grade = grade_index(jstr(r, "grade", "B"));
|
||
JVal *p = jget(r, "pct"); W.tr_pct = p && p->t == J_NUM ? (int)p->n : -1;
|
||
}
|
||
W.nfriends = 0;
|
||
JFOR(o, jget(j, "together")) {
|
||
if (W.nfriends >= 3) break;
|
||
WFriend *f = &W.friends[W.nfriends++]; memset(f, 0, sizeof *f);
|
||
JVal *u = jget(o, "user");
|
||
jcpy(f->oid, sizeof f->oid, u, "online_id"); f->avatar = (int)jnum(u, "avatar", 0); media_note_json(f->oid, u);
|
||
f->secs = (long)jnum(o, "seconds", 0); jcpy(f->game, sizeof f->game, o, "game_name");
|
||
}
|
||
W.persona = NPERSONAS - 1;
|
||
const char *p = jstr(j, "persona", "casual");
|
||
for (int i = 0; i < NPERSONAS; i++) if (!strcmp(PERSONAS[i].key, p)) W.persona = i;
|
||
W.loaded = 1; W.cur = 0; W.st = 0; W.enter = 0;
|
||
build_slides();
|
||
}
|
||
|
||
static void load(void) {
|
||
char path[160];
|
||
snprintf(path, sizeof path, OMEGA_API "/wrap?period=%s&back=%d&tz=%d", OPTS[W.opt].period, OPTS[W.opt].back, wrap_tz());
|
||
W.loading = 1; W.loaded = 0; W.nslides = 0;
|
||
net_req(HTTP_GET, path, NULL, on_wrap, (void *)(intptr_t)W.opt);
|
||
}
|
||
|
||
// opt: 0 questa settimana, 1 settimana scorsa, 2 questo mese, 3 mese scorso, 4 quest'anno
|
||
void wrap_open(int opt) {
|
||
memset(&W, 0, sizeof W);
|
||
W.opt = opt >= 0 && opt < NOPTS ? opt : 0;
|
||
W.percentile = -1;
|
||
load();
|
||
if (ov_top() != OV_WRAP) ov_push(OV_WRAP);
|
||
}
|
||
|
||
// ------------------------------------------------------------------ disegno --
|
||
static Col slide_col(int s) {
|
||
switch (s) {
|
||
case S_TOTAL: return RGB(20, 150, 160);
|
||
case S_TOP: return RGB(60, 90, 220);
|
||
case S_TOP5: return RGB(110, 70, 200);
|
||
case S_RHYTHM: return RGB(40, 50, 140);
|
||
case S_RECORDS: return RGB(220, 100, 50);
|
||
case S_TROPHIES: return RGB(200, 150, 30);
|
||
case S_TOGETHER: return RGB(210, 70, 140);
|
||
case S_COMMUNITY: return RGB(40, 160, 90);
|
||
case S_PERSONA: return PERSONAS[W.persona].c;
|
||
default: return C_ACC;
|
||
}
|
||
}
|
||
|
||
// entrata: 0 → 1 nel primo mezzo secondo della scheda, ritardata di `delay`
|
||
static float in(float delay) { float t = (W.st - delay) / 0.55f; return t <= 0 ? 0 : t >= 1 ? 1 : ease_out(t); }
|
||
static int rise(float delay) { return (int)((1 - in(delay)) * 46); }
|
||
static int fa(int a, float delay) { return (int)(a * in(delay)); }
|
||
|
||
static void big_center(const char *s, int y, int size, Col c, int a, float delay) {
|
||
draw_text_fit(font(W_LIGHT, size), s, SCREEN_W / 2, y + rise(delay), SCREEN_W - 240, c, fa(a, delay), AL_C);
|
||
}
|
||
static void line_center(const char *s, int y, int size, Col c, int a, float delay) {
|
||
draw_text_wrap_al(font(W_REG, size), s, SCREEN_W / 2, y + rise(delay), SCREEN_W - 520, 3, size + 14, c, fa(a, delay), AL_C);
|
||
}
|
||
|
||
static void draw_bg(int s, int a) {
|
||
Col c = slide_col(s), dark = RGB(8, 10, 18);
|
||
fill_rect(0, 0, SCREEN_W, SCREEN_H, dark, a);
|
||
grad_v(0, 0, SCREEN_W, SCREEN_H, mix(c, dark, 0.35f), a, mix(c, dark, 0.85f), a);
|
||
float t = (float)g_time;
|
||
glow((int)(SCREEN_W * 0.2f + sinf(t * 0.31f) * 160), (int)(SCREEN_H * 0.3f + cosf(t * 0.23f) * 90), 520, mix(c, C_WHITE, 0.25f), a * 30 / 100);
|
||
glow((int)(SCREEN_W * 0.8f + cosf(t * 0.27f) * 150), (int)(SCREEN_H * 0.75f + sinf(t * 0.19f) * 80), 600, mix(c, RGB(255, 80, 160), 0.3f), a * 22 / 100);
|
||
}
|
||
|
||
static void draw_progress(int a) {
|
||
int x0 = 120, w = SCREEN_W - 240, gap = 10, n = W.nslides;
|
||
if (n < 2) return;
|
||
int sw = (w - gap * (n - 1)) / n;
|
||
for (int i = 0; i < n; i++) {
|
||
int x = x0 + i * (sw + gap);
|
||
fill_rrect(x, 46, sw, 6, 3, C_WHITE, a * 25 / 100);
|
||
float f = i < W.cur ? 1 : i > W.cur ? 0 : (W.cur == n - 1 ? 1 : fminf(1, W.st / SLIDE_SECS));
|
||
if (f > 0) fill_rrect(x, 46, (int)(sw * f), 6, 3, C_WHITE, a * 90 / 100);
|
||
}
|
||
}
|
||
|
||
static void s_intro(int a) {
|
||
char r[80]; range_text(r, sizeof r);
|
||
draw_logo(SCREEN_W / 2, 300 + rise(0), 120, fa(a, 0));
|
||
big_center(_(OPTS[W.opt].title), 420, 92, C_WHITE, a, 0.15f);
|
||
big_center(r, 560, 40, C_DIM, a, 0.3f);
|
||
draw_avatar(S.me, S.my_avatar, SCREEN_W / 2, 720 + rise(0.45f), 110, fa(a, 0.45f));
|
||
big_center(S.me, 800, 34, C_TXT, a, 0.5f);
|
||
if (W.loaded && W.total > 0) line_center(_("Ecco com'\xC3\xA8 andata. Premi X per andare avanti."), 900, 28, C_DIM, a, 0.8f);
|
||
}
|
||
|
||
static void s_empty(int a) {
|
||
draw_icon(IC_GAMEPAD, SCREEN_W / 2, 330 + rise(0), 140, C_WHITE, fa(a, 0) * 70 / 100);
|
||
big_center(_("Nessuna partita in questo periodo"), 440, 62, C_WHITE, a, 0.1f);
|
||
line_center(_("Gioca qualcosa e torna qui: il riepilogo si riempie da solo. Con Quadrato scegli un altro periodo."), 560, 30, C_DIM, a, 0.25f);
|
||
}
|
||
|
||
static void s_total(int a) {
|
||
big_center(_("Hai giocato"), 250, 44, C_DIM, a, 0);
|
||
char t[48]; dur((long)(W.total * in(0.1f) + 0.5f), t, sizeof t);
|
||
draw_text_fit(font(W_LIGHT, 170), t, SCREEN_W / 2, 330 + rise(0.1f), SCREEN_W - 200, C_WHITE, fa(a, 0.1f), AL_C);
|
||
char l[160] = "";
|
||
if (W.prev > 0) {
|
||
int pct = (int)lround((double)(W.total - W.prev) * 100.0 / (double)W.prev);
|
||
snprintf(l, sizeof l, pct >= 0 ? _("+%d%% rispetto al periodo precedente") : _("%d%% rispetto al periodo precedente"), pct);
|
||
int w = text_w(font(W_MED, 30), l) + 60;
|
||
fill_rrect(SCREEN_W / 2 - w / 2, 580 + rise(0.5f), w, 60, 30, pct >= 0 ? C_OK : C_WARN, fa(a, 0.5f) * 30 / 100);
|
||
draw_text(font(W_MED, 30), l, SCREEN_W / 2, 592 + rise(0.5f), C_WHITE, fa(a, 0.5f), AL_C);
|
||
}
|
||
char s1[64] = "", s2[64] = "", s3[64] = "";
|
||
snprintf(s1, sizeof s1, W.games_count == 1 ? _("%d gioco") : _("%d giochi"), W.games_count);
|
||
if (W.sessions > 0) snprintf(s2, sizeof s2, W.sessions == 1 ? _("%d sessione") : _("%d sessioni"), W.sessions);
|
||
if (W.days > 0) snprintf(s3, sizeof s3, W.days == 1 ? _("%d giorno di gioco") : _("%d giorni di gioco"), W.days);
|
||
const char *items[3] = { s1, s2, s3 }; int k = 0, x = SCREEN_W / 2;
|
||
int tw = 0; for (int i = 0; i < 3; i++) if (items[i][0]) tw += text_w(font(W_MED, 34), items[i]) + 90;
|
||
x -= tw / 2;
|
||
for (int i = 0; i < 3; i++) {
|
||
if (!items[i][0]) continue;
|
||
int w = text_w(font(W_MED, 34), items[i]) + 60;
|
||
float dl = 0.7f + 0.12f * k++;
|
||
fill_rrect(x, 720 + rise(dl), w, 76, 38, C_WHITE, fa(a, dl) * 12 / 100);
|
||
draw_text(font(W_MED, 34), items[i], x + w / 2, 738 + rise(dl), C_WHITE, fa(a, dl), AL_C);
|
||
x += w + 30;
|
||
}
|
||
}
|
||
|
||
static void s_top(int a) {
|
||
WGame *g = &W.games[0];
|
||
big_center(_("Il tuo gioco"), 150, 44, C_DIM, a, 0);
|
||
float pop = in(0.1f); int s = (int)(330 * (0.8f + 0.2f * pop));
|
||
glow(SCREEN_W / 2, 420, 300, C_WHITE, (int)(a * pop * 18 / 100));
|
||
wgame_icon(g->gid, g->icon, SCREEN_W / 2 - s / 2, 420 - s / 2, s, fa(a, 0.1f));
|
||
big_center(g->name, 620, 64, C_WHITE, a, 0.3f);
|
||
char t[48], l[200]; dur(g->secs, t, sizeof t);
|
||
int share = W.total > 0 ? (int)lround(g->secs * 100.0 / W.total) : 0;
|
||
if (g->sessions > 0) snprintf(l, sizeof l, g->sessions == 1 ? _("%s in %d sessione \xC2\xB7 il %d%% del tuo tempo") : _("%s in %d sessioni \xC2\xB7 il %d%% del tuo tempo"), t, g->sessions, share);
|
||
else snprintf(l, sizeof l, _("%s \xC2\xB7 il %d%% del tuo tempo"), t, share);
|
||
big_center(l, 730, 36, C_TXT, a, 0.45f);
|
||
}
|
||
|
||
static void s_top5(int a) {
|
||
big_center(_("I tuoi giochi pi\xC3\xB9 giocati"), 150, 56, C_WHITE, a, 0);
|
||
int x = 360, w = SCREEN_W - 720, y = 280;
|
||
long maxs = W.games[0].secs > 0 ? W.games[0].secs : 1;
|
||
for (int i = 0; i < W.ngames; i++, y += 140) {
|
||
WGame *g = &W.games[i]; float dl = 0.15f + 0.12f * i; int ra = fa(a, dl), dy = rise(dl);
|
||
char n[8]; snprintf(n, sizeof n, "%d", i + 1);
|
||
draw_text(font(W_BOLD, 54), n, x - 40, y + 24 + dy, i == 0 ? RGB(255, 210, 90) : C_DIM, ra, AL_R);
|
||
wgame_icon(g->gid, g->icon, x, y + dy, 110, ra);
|
||
draw_text_fit(font(W_MED, 36), g->name, x + 140, y + 4 + dy, w - 400, C_WHITE, ra, AL_L);
|
||
char t[48]; dur(g->secs, t, sizeof t);
|
||
draw_text(font(W_MED, 34), t, x + w, y + 6 + dy, C_TXT, ra, AL_R);
|
||
int bw = w - 140;
|
||
fill_rrect(x + 140, y + 70 + dy, bw, 14, 7, C_WHITE, ra * 15 / 100);
|
||
fill_rrect(x + 140, y + 70 + dy, (int)(bw * (double)g->secs / maxs * in(dl + 0.2f)), 14, 7, C_WHITE, ra * 85 / 100);
|
||
}
|
||
if (W.games_count > W.ngames) {
|
||
char m[64]; snprintf(m, sizeof m, _("e altri %d"), W.games_count - W.ngames);
|
||
big_center(m, y + 10, 28, C_DIM, a, 0.9f);
|
||
}
|
||
}
|
||
|
||
static void s_rhythm(int a) {
|
||
static const char *const WD[7] = { N_("luned\xC3\xAC"), N_("marted\xC3\xAC"), N_("mercoled\xC3\xAC"), N_("gioved\xC3\xAC"), N_("venerd\xC3\xAC"), N_("sabato"), N_("domenica") };
|
||
int peak = 0, wpeak = 0;
|
||
long hmax = 1;
|
||
for (int i = 0; i < 24; i++) { if (W.hours[i] > W.hours[peak]) peak = i; if (W.hours[i] > hmax) hmax = W.hours[i]; }
|
||
for (int i = 0; i < 7; i++) if (W.wd[i] > W.wd[wpeak]) wpeak = i;
|
||
const char *when = peak >= 5 && peak < 12 ? _("Giochi soprattutto la mattina") : peak >= 12 && peak < 18 ? _("Giochi soprattutto il pomeriggio")
|
||
: peak >= 18 && peak < 22 ? _("Giochi soprattutto la sera") : _("Giochi soprattutto di notte");
|
||
big_center(_("Il tuo ritmo"), 140, 44, C_DIM, a, 0);
|
||
big_center(when, 210, 68, C_WHITE, a, 0.1f);
|
||
char l[160]; snprintf(l, sizeof l, _("Ora preferita: %02d:00\xE2\x80\x93%02d:00 \xC2\xB7 giorno preferito: %s"), peak, (peak + 1) % 24, _(WD[wpeak]));
|
||
big_center(l, 320, 32, C_TXT, a, 0.25f);
|
||
int x0 = 260, w = SCREEN_W - 520, base = 820, hmaxpx = 380, bw = w / 24;
|
||
for (int i = 0; i < 24; i++) {
|
||
float dl = 0.35f + 0.02f * i;
|
||
int h = (int)(hmaxpx * (double)W.hours[i] / hmax * in(dl));
|
||
if (h < 4 && W.hours[i] > 0) h = 4;
|
||
Col c = i == peak ? RGB(255, 210, 90) : C_WHITE;
|
||
fill_rrect(x0 + i * bw + 6, base - h, bw - 12, h, 8, c, a * (i == peak ? 95 : 55) / 100);
|
||
if (i % 3 == 0) { char hh[8]; snprintf(hh, sizeof hh, "%02d", i); draw_text(font(W_REG, 24), hh, x0 + i * bw + bw / 2, base + 16, C_DIM, a, AL_C); }
|
||
}
|
||
}
|
||
|
||
static void stat_card(int x, int y, int w, int icon, const char *big, const char *sub, int a, float dl) {
|
||
int ra = fa(a, dl), dy = rise(dl);
|
||
fill_rrect(x, y + dy, w, 300, 36, C_WHITE, ra * 10 / 100);
|
||
draw_icon(icon, x + w / 2, y + 70 + dy, 64, C_WHITE, ra);
|
||
draw_text_fit(font(W_LIGHT, 64), big, x + w / 2, y + 120 + dy, w - 40, C_WHITE, ra, AL_C);
|
||
draw_text_wrap_al(font(W_REG, 27), sub, x + w / 2, y + 210 + dy, w - 60, 2, 36, C_TXT, ra, AL_C);
|
||
}
|
||
|
||
static void s_records(int a) {
|
||
big_center(_("I tuoi record"), 150, 60, C_WHITE, a, 0);
|
||
int n = (W.has_longest ? 1 : 0) + (W.streak >= 2 ? 1 : 0) + (W.new_games ? 1 : 0);
|
||
int w = 480, gap = 50, x = SCREEN_W / 2 - (n * w + (n - 1) * gap) / 2, k = 0;
|
||
if (W.has_longest) {
|
||
char t[48], s[200]; dur(W.longest.secs, t, sizeof t);
|
||
snprintf(s, sizeof s, _("La sessione pi\xC3\xB9 lunga, a %s"), W.longest.name);
|
||
stat_card(x, 380, w, IC_FIRE, t, s, a, 0.2f + 0.15f * k); x += w + gap; k++;
|
||
}
|
||
if (W.streak >= 2) {
|
||
char t[32]; snprintf(t, sizeof t, "%d", W.streak);
|
||
stat_card(x, 380, w, IC_CLOCK, t, _("giorni di fila con almeno una partita"), a, 0.2f + 0.15f * k); x += w + gap; k++;
|
||
}
|
||
if (W.new_games) {
|
||
char t[32]; snprintf(t, sizeof t, "%d", W.new_games);
|
||
stat_card(x, 380, w, IC_STAR, t, W.new_games == 1 ? _("gioco nuovo, mai giocato prima") : _("giochi nuovi, mai giocati prima"), a, 0.2f + 0.15f * k);
|
||
}
|
||
}
|
||
|
||
static void s_trophies(int a) {
|
||
big_center(_("Trofei ottenuti"), 140, 44, C_DIM, a, 0);
|
||
char n[16]; snprintf(n, sizeof n, "%d", (int)lround(W.tr_count * in(0.1f)));
|
||
draw_text(font(W_LIGHT, 170), n, SCREEN_W / 2, 200 + rise(0.1f), C_WHITE, fa(a, 0.1f), AL_C);
|
||
int cw = 200, x = SCREEN_W / 2 - 2 * cw;
|
||
for (int i = 0; i < 4; i++) {
|
||
float dl = 0.3f + 0.1f * i; int ra = fa(a, dl), cx = x + i * cw + cw / 2;
|
||
fill_circle(cx, 500 + rise(dl), 44, GRADE_COL[i], ra);
|
||
draw_icon(IC_STAR, cx, 500 + rise(dl), 56, RGB(20, 22, 32), ra);
|
||
char c[12]; snprintf(c, sizeof c, "%d", W.tr[i]);
|
||
draw_text(font(W_MED, 40), c, cx, 560 + rise(dl), C_WHITE, ra, AL_C);
|
||
}
|
||
if (W.has_rarest) {
|
||
float dl = 0.75f; int ra = fa(a, dl), dy = rise(dl);
|
||
int w = 1100, x0 = SCREEN_W / 2 - w / 2, y = 700 + dy;
|
||
fill_rrect(x0, y, w, 170, 34, C_WHITE, ra * 12 / 100);
|
||
wgame_icon("", W.tr_icon, x0 + 30, y + 25, 120, ra);
|
||
char head[120];
|
||
if (W.tr_pct >= 0) snprintf(head, sizeof head, _("Il pi\xC3\xB9 raro: ce l'ha il %d%% dei giocatori"), W.tr_pct);
|
||
else snprintf(head, sizeof head, "%s", _("Il pi\xC3\xB9 prezioso"));
|
||
draw_text(font(W_REG, 26), head, x0 + 180, y + 26, C_DIM, ra, AL_L);
|
||
fill_circle(x0 + 196, y + 98, 16, GRADE_COL[W.tr_grade], ra);
|
||
draw_text_fit(font(W_MED, 36), W.tr_name[0] ? W.tr_name : _("Trofeo nascosto"), x0 + 226, y + 76, w - 260, C_WHITE, ra, AL_L);
|
||
draw_text_fit(font(W_REG, 24), W.tr_game, x0 + 180, y + 126, w - 220, C_TXT, ra, AL_L);
|
||
}
|
||
}
|
||
|
||
static void s_together(int a) {
|
||
big_center(_("Hai giocato insieme a"), 150, 56, C_WHITE, a, 0);
|
||
int n = W.nfriends, w = 440, gap = 60, x = SCREEN_W / 2 - (n * w + (n - 1) * gap) / 2;
|
||
for (int i = 0; i < n; i++, x += w + gap) {
|
||
WFriend *f = &W.friends[i]; float dl = 0.2f + 0.15f * i; int ra = fa(a, dl), dy = rise(dl);
|
||
fill_rrect(x, 330 + dy, w, 460, 40, C_WHITE, ra * 10 / 100);
|
||
draw_avatar(f->oid, f->avatar, x + w / 2, 460 + dy, 170, ra);
|
||
draw_text_fit(font(W_MED, 38), f->oid, x + w / 2, 580 + dy, w - 40, C_WHITE, ra, AL_C);
|
||
char t[48]; dur(f->secs, t, sizeof t);
|
||
draw_text(font(W_LIGHT, 44), t, x + w / 2, 640 + dy, C_TXT, ra, AL_C);
|
||
draw_text_fit(font(W_REG, 25), f->game, x + w / 2, 712 + dy, w - 40, C_DIM, ra, AL_C);
|
||
}
|
||
line_center(_("Contano le partite allo stesso gioco nello stesso momento."), 860, 26, C_DIM, a, 0.7f);
|
||
}
|
||
|
||
static void s_community(int a) {
|
||
big_center(_("Rispetto alla community"), 180, 44, C_DIM, a, 0);
|
||
char n[16]; snprintf(n, sizeof n, "%d%%", (int)lround(W.percentile * in(0.1f)));
|
||
draw_text(font(W_LIGHT, 220), n, SCREEN_W / 2, 260 + rise(0.1f), C_WHITE, fa(a, 0.1f), AL_C);
|
||
char l[200]; snprintf(l, sizeof l, _("Hai giocato pi\xC3\xB9 del %d%% dei %d giocatori di Omega in questo periodo"), W.percentile, W.players);
|
||
line_center(l, 560, 40, C_WHITE, a, 0.35f);
|
||
const char *tag = W.percentile >= 90 ? _("Sei tra i pi\xC3\xB9 attivi in assoluto.") : W.percentile >= 50 ? _("Pi\xC3\xB9 della met\xC3\xA0 dei giocatori dietro di te.") : _("C'\xC3\xA8 sempre tempo per un'altra partita.");
|
||
line_center(tag, 700, 30, C_TXT, a, 0.55f);
|
||
line_center(_("Il confronto usa solo totali anonimi."), 900, 24, C_DIM, a, 0.8f);
|
||
}
|
||
|
||
static void s_persona(int a) {
|
||
const int p = W.persona;
|
||
int cx = SCREEN_W / 2;
|
||
big_center(_("Il tuo profilo"), 110, 40, C_DIM, a, 0);
|
||
float pop = in(0.1f);
|
||
glow(cx, 290, 260, PERSONAS[p].c, (int)(a * pop * 45 / 100));
|
||
fill_circle(cx, 290 + rise(0.1f), (int)(110 * (0.7f + 0.3f * pop)), mix(PERSONAS[p].c, C_BLACK, 0.2f), fa(a, 0.1f));
|
||
draw_icon(PERSONAS[p].icon, cx, 290 + rise(0.1f), 120, C_WHITE, fa(a, 0.1f));
|
||
big_center(_(PERSONAS[p].title), 430, 84, C_WHITE, a, 0.25f);
|
||
big_center(_(PERSONAS[p].desc), 545, 34, C_TXT, a, 0.35f);
|
||
// la carta riassuntiva
|
||
float dl = 0.55f; int ra = fa(a, dl), dy = rise(dl);
|
||
int w = 1300, x0 = cx - w / 2, y = 640 + dy, cw = w / 4;
|
||
fill_rrect(x0, y, w, 210, 36, C_WHITE, ra * 10 / 100);
|
||
char v[4][110], *lab[4];
|
||
dur(W.total, v[0], sizeof v[0]); lab[0] = (char *)_("di gioco");
|
||
snprintf(v[1], sizeof v[1], "%s", W.ngames ? W.games[0].name : "-"); lab[1] = (char *)_("il pi\xC3\xB9 giocato");
|
||
if (W.days > 0) { snprintf(v[2], sizeof v[2], "%d", W.days); lab[2] = (char *)(W.days == 1 ? _("giorno di gioco") : _("giorni di gioco")); }
|
||
else { snprintf(v[2], sizeof v[2], "%d", W.games_count); lab[2] = (char *)(W.games_count == 1 ? _("gioco") : _("giochi")); }
|
||
snprintf(v[3], sizeof v[3], "%d", W.tr_count); lab[3] = (char *)(W.tr_count == 1 ? _("trofeo") : _("trofei"));
|
||
for (int i = 0; i < 4; i++) {
|
||
int sz = i == 1 ? 34 : 50;
|
||
while (sz > 28 && text_w(font(W_MED, sz), v[i]) > cw - 40) sz -= 2; // "17 Std. 25 Min." in tedesco
|
||
draw_text_fit(font(W_MED, sz), v[i], x0 + cw * i + cw / 2, y + 44 + (50 - sz) * 6 / 10, cw - 40, C_WHITE, ra, AL_C);
|
||
draw_text_fit(font(W_REG, 24), lab[i], x0 + cw * i + cw / 2, y + 128, cw - 30, C_DIM, ra, AL_C);
|
||
}
|
||
char r[80], foot[160]; range_text(r, sizeof r);
|
||
snprintf(foot, sizeof foot, "%s \xC2\xB7 %s \xC2\xB7 Omega", S.me, r);
|
||
draw_text(font(W_REG, 24), foot, cx, y + 230, C_DIM, ra, AL_C);
|
||
}
|
||
|
||
void wrap_draw(float t) {
|
||
int a = (int)(255 * t);
|
||
int s = W.nslides ? W.slides[W.cur] : S_INTRO;
|
||
draw_bg(s, a);
|
||
if (W.loaded && ov_top() == OV_WRAP) {
|
||
W.st += g_dt;
|
||
if (W.cur < W.nslides - 1 && W.st >= SLIDE_SECS) { W.cur++; W.st = 0; }
|
||
}
|
||
W.enter = approach(W.enter, 1, 6.0f);
|
||
int ca = (int)(a * W.enter);
|
||
draw_progress(a);
|
||
char per[80]; snprintf(per, sizeof per, "%s", _(OPTS[W.opt].label));
|
||
draw_text(font(W_MED, 26), per, 120, 72, C_WHITE, a * 80 / 100, AL_L);
|
||
if (W.loading) { draw_spinner(SCREEN_W / 2, 640, 26, a); s_intro(ca); }
|
||
else if (!W.loaded) { big_center(_("Riepilogo non disponibile"), 460, 54, C_WHITE, a, 0); line_center(_("Controlla la connessione e riprova con Quadrato."), 560, 30, C_DIM, a, 0); }
|
||
else switch (s) {
|
||
case S_INTRO: s_intro(ca); break;
|
||
case S_EMPTY: s_empty(ca); break;
|
||
case S_TOTAL: s_total(ca); break;
|
||
case S_TOP: s_top(ca); break;
|
||
case S_TOP5: s_top5(ca); break;
|
||
case S_RHYTHM: s_rhythm(ca); break;
|
||
case S_RECORDS: s_records(ca); break;
|
||
case S_TROPHIES: s_trophies(ca); break;
|
||
case S_TOGETHER: s_together(ca); break;
|
||
case S_COMMUNITY: s_community(ca); break;
|
||
case S_PERSONA: s_persona(ca); break;
|
||
}
|
||
int ic[5]; const char *lb[5]; int n = 0;
|
||
int last = W.loaded && W.cur == W.nslides - 1;
|
||
if (W.loaded && W.total > 0 && last) { ic[n] = IC_BTN_X; lb[n++] = _("Condividi sulla bacheca"); }
|
||
else if (W.loaded && !last) { ic[n] = IC_BTN_X; lb[n++] = _("Avanti"); }
|
||
ic[n] = IC_BTN_SQ; lb[n++] = _("Periodo");
|
||
if (W.loaded && W.total > 0 && !last) { ic[n] = IC_BTN_TRI; lb[n++] = _("Condividi"); }
|
||
ic[n] = IC_BTN_O; lb[n++] = _("Chiudi");
|
||
hints(ic, lb, n, a);
|
||
}
|
||
|
||
// --------------------------------------------------------------- comandi --
|
||
static void go(int d) {
|
||
int c = W.cur + d;
|
||
if (c < 0 || c >= W.nslides) return;
|
||
W.cur = c; W.st = 0; W.enter = 0.3f;
|
||
sfx_play(SFX_MOVE);
|
||
}
|
||
|
||
static void on_shared(int st, JVal *j, const char *raw, void *ud) {
|
||
(void)j; (void)raw; (void)ud;
|
||
if (st == 201 || st == 200) set_msg(_("Riepilogo condiviso sulla bacheca"), 0);
|
||
else set_msg(st == 429 ? _("Hai scritto molto: riprova pi\xC3\xB9 tardi") : _("Pubblicazione non riuscita"), 1);
|
||
}
|
||
static void share_yes(int idx, void *ud) {
|
||
(void)ud;
|
||
if (idx != 0) return;
|
||
char t[48], txt[600], e[1300], body[1400];
|
||
dur(W.total, t, sizeof t);
|
||
snprintf(txt, sizeof txt, _("Il mio riepilogo su Omega (%s): %s di gioco, il pi\xC3\xB9 giocato \xC3\xA8 %s. Il mio profilo: %s \xE2\x80\x93 %s"),
|
||
_(OPTS[W.opt].label), t, W.ngames ? W.games[0].name : "-", _(PERSONAS[W.persona].title), _(PERSONAS[W.persona].desc));
|
||
json_escape(e, sizeof e, txt);
|
||
snprintf(body, sizeof body, "{\"text\":\"%s\"}", e);
|
||
net_req(HTTP_POST, OMEGA_API "/posts", body, on_shared, NULL);
|
||
}
|
||
static void share(void) {
|
||
if (!W.loaded || W.total <= 0) return;
|
||
confirm_open(_("Condividere il riepilogo sulla bacheca? Lo vedono i tuoi amici."), _("Condividi"), share_yes, NULL);
|
||
}
|
||
|
||
static void period_pick(int idx, void *ud) {
|
||
(void)ud;
|
||
if (idx < 0 || idx >= NOPTS) return;
|
||
W.opt = idx; W.cur = 0; W.st = 0;
|
||
load();
|
||
}
|
||
|
||
void wrap_input(int b) {
|
||
switch (b) {
|
||
case B_O: ov_pop(); return;
|
||
case B_RIGHT: case B_R1: go(1); return;
|
||
case B_LEFT: case B_L1: go(-1); return;
|
||
case B_X:
|
||
if (!W.loaded) return;
|
||
if (W.cur == W.nslides - 1) share(); else go(1);
|
||
return;
|
||
case B_TRI: share(); return;
|
||
case B_SQ: {
|
||
const char *items[NOPTS];
|
||
for (int i = 0; i < NOPTS; i++) items[i] = _(OPTS[i].label);
|
||
menu_open(_("Periodo del riepilogo"), items, NOPTS, period_pick, NULL);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------- «il tuo riepilogo è pronto» --
|
||
// Una volta per settimana (dal lunedì), poco dopo l'accesso: se la settimana
|
||
// prima c'è almeno mezz'ora di gioco arriva una notifica. La settimana già
|
||
// annunciata sta in OMEGA_DIR/wrap-seen-<utente>.
|
||
static Uint32 prompt_at; static char prompt_for[32], prompt_week[16];
|
||
|
||
static void on_prompt(int st, JVal *j, const char *raw, void *ud) {
|
||
(void)raw; (void)ud;
|
||
if (st != 200 || jnum(j, "total_seconds", 0) < 1800) return;
|
||
toast(IC_STAR, NULL, 0, _("Il tuo riepilogo \xC3\xA8 pronto"), _("Community \xE2\x80\xBA Tempo di gioco \xE2\x80\xBA Triangolo"));
|
||
}
|
||
|
||
void wrap_tick(void) {
|
||
if (!g_token[0] || !S.me[0] || !S.synced_once) return;
|
||
if (strcmp(prompt_for, S.me)) { snprintf(prompt_for, sizeof prompt_for, "%s", S.me); prompt_at = SDL_GetTicks() + 45000; return; }
|
||
if (!prompt_at || SDL_GetTicks() < prompt_at) return;
|
||
prompt_at = 0;
|
||
time_t now = time(NULL); struct tm l = *localtime(&now);
|
||
time_t mon = now - (time_t)(((l.tm_wday + 6) % 7) * 86400);
|
||
struct tm m = *localtime(&mon);
|
||
snprintf(prompt_week, sizeof prompt_week, "%04d%02d%02d", m.tm_year + 1900, m.tm_mon + 1, m.tm_mday);
|
||
char path[160]; snprintf(path, sizeof path, OMEGA_DIR "/wrap-seen-%s", S.me);
|
||
char *seen = file_read(path, 32, NULL);
|
||
int same = seen && !strncmp(seen, prompt_week, 8);
|
||
free(seen);
|
||
if (same) return;
|
||
FILE *f = fopen(path, "w");
|
||
if (f) { fprintf(f, "%s\n", prompt_week); fclose(f); }
|
||
char q[160]; snprintf(q, sizeof q, OMEGA_API "/wrap?period=week&back=1&tz=%d", wrap_tz());
|
||
net_req(HTTP_GET, q, NULL, on_prompt, NULL);
|
||
}
|