taisei/src/dialog.c
2024-05-17 14:11:48 +02:00

391 lines
9.3 KiB
C

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "dialog.h"
#include "global.h"
#include "portrait.h"
#include "resource/font.h"
void dialog_init(Dialog *d) {
memset(d, 0, sizeof(*d));
d->state = DIALOG_STATE_IDLE;
d->text.current = &d->text.buffers[0];
d->text.fading_out = &d->text.buffers[1];
COEVENT_INIT_ARRAY(d->events);
}
void dialog_deinit(Dialog *d) {
COEVENT_CANCEL_ARRAY(d->events);
for(DialogActor *a = d->actors.first; a; a = a->next) {
if(a->composite.tex) {
r_texture_destroy(a->composite.tex);
}
}
}
void dialog_add_actor(Dialog *d, DialogActor *a, const char *name, DialogSide side) {
memset(a, 0, sizeof(*a));
a->name = name;
a->face = "normal";
a->side = side;
a->target_opacity = 1;
a->composite_dirty = true;
if(side == DIALOG_SIDE_RIGHT) {
a->speech_color = *RGB(0.6, 0.6, 1.0);
} else {
a->speech_color = *RGB(1.0, 1.0, 1.0);
}
alist_append(&d->actors, a);
}
void dialog_actor_set_face(DialogActor *a, const char *face) {
log_debug("[%s] %s --> %s", a->name, a->face, face);
if(a->face != face) {
a->face = face;
a->composite_dirty = true;
}
}
void dialog_actor_set_variant(DialogActor *a, const char *variant) {
log_debug("[%s] %s --> %s", a->name, a->variant, variant);
if(a->variant != variant) {
a->variant = variant;
a->composite_dirty = true;
}
}
void dialog_update(Dialog *d) {
if(d->text.current->text) {
fapproach_p(&d->text.current->opacity, 1, 1/120.0f);
} else {
d->text.current->opacity = 0;
}
if(d->text.fading_out->text) {
fapproach_p(&d->text.fading_out->opacity, 0, 1/60.0f);
} else {
d->text.fading_out->opacity = 0;
}
if(
d->state == DIALOG_STATE_FADEOUT ||
(
d->text.current->opacity == 0 &&
d->text.fading_out->opacity < 0.25
)
) {
fapproach_asymptotic_p(&d->opacity, 0, 0.1, 1e-3);
} else {
fapproach_asymptotic_p(&d->opacity, 1, 0.05, 1e-3);
}
const float offset_per_actor = 32;
float target_offsets[2] = { 0 };
for(DialogActor *a = d->actors.last; a; a = a->prev) {
fapproach_asymptotic_p(&a->offset.x, target_offsets[a->side], 0.10, 1e-3);
target_offsets[a->side] += offset_per_actor;
if(d->state == DIALOG_STATE_FADEOUT) {
fapproach_asymptotic_p(&a->opacity, 0, 0.12, 1e-3);
} else {
fapproach_asymptotic_p(&a->opacity, a->target_opacity, 0.04, 1e-3);
}
fapproach_asymptotic_p(&a->focus, a->target_focus, 0.12, 1e-3);
}
}
void dialog_skippable_wait(Dialog *d, int timeout) {
CoEventSnapshot snap = coevent_snapshot(&d->events.skip_requested);
assert(d->state == DIALOG_STATE_IDLE);
d->state = DIALOG_STATE_WAITING_FOR_SKIP;
while(timeout > 0) {
dialog_update(d);
--timeout;
YIELD;
if(coevent_poll(&d->events.skip_requested, &snap) != CO_EVENT_PENDING) {
log_debug("Skipped with %i remaining", timeout);
break;
}
}
assert(d->state == DIALOG_STATE_WAITING_FOR_SKIP);
d->state = DIALOG_STATE_IDLE;
if(timeout == 0) {
log_debug("Timed out");
}
}
int dialog_util_estimate_wait_timeout_from_text(const char *text) {
return 1800;
}
static void dialog_set_text(Dialog *d, const char *text, const Color *clr) {
DialogTextBuffer *temp = d->text.current;
d->text.current = d->text.fading_out;
d->text.fading_out = temp;
d->text.current->color = *clr;
d->text.current->text = text;
}
void dialog_focus_actor(Dialog *d, DialogActor *actor) {
for(DialogActor *a = d->actors.first; a; a = a->next) {
a->target_focus = 0;
}
actor->target_focus = 1;
// make focused actor drawn on top of everyone else
alist_unlink(&d->actors, actor);
alist_append(&d->actors, actor);
}
void dialog_message_ex(Dialog *d, const DialogMessageParams *params) {
assume(params->actor != NULL);
assume(params->text != NULL);
log_debug("%s: %s", params->actor->name, params->text);
dialog_set_text(d, params->text, &params->actor->speech_color);
dialog_focus_actor(d, params->actor);
if(params->implicit_wait) {
assume(params->wait_timeout > 0);
if(params->wait_skippable) {
dialog_skippable_wait(d, params->wait_timeout);
} else {
WAIT(params->wait_timeout);
}
}
}
static void _dialog_message(Dialog *d, DialogActor *actor, const char *text, bool skippable, int delay) {
DialogMessageParams p = { 0 };
p.actor = actor;
p.text = text;
p.implicit_wait = true;
p.wait_skippable = skippable;
p.wait_timeout = delay;
dialog_message_ex(d, &p);
}
void dialog_message(Dialog *d, DialogActor *actor, const char *text) {
_dialog_message(d, actor, text, true, dialog_util_estimate_wait_timeout_from_text(text));
}
void dialog_message_unskippable(Dialog *d, DialogActor *actor, const char *text, int delay) {
_dialog_message(d, actor, text, false, delay);
}
void dialog_end(Dialog *d) {
d->state = DIALOG_STATE_FADEOUT;
coevent_signal(&d->events.fadeout_began);
for(DialogActor *a = d->actors.first; a; a = a->next) {
a->target_opacity = 0;
}
wait_for_fadeout: {
if(d->opacity > 0) {
YIELD;
goto wait_for_fadeout;
}
for(DialogActor *a = d->actors.first; a; a = a->next) {
if(a->opacity > 0) {
YIELD;
goto wait_for_fadeout;
}
}
}
coevent_signal(&d->events.fadeout_ended);
dialog_deinit(d);
}
static void dialog_actor_update_composite(DialogActor *a) {
assume(a->name != NULL);
assume(a->face != NULL);
if(!a->composite_dirty) {
return;
}
log_debug("%s (%p) is dirty; face=%s; variant=%s", a->name, (void*)a, a->face, a->variant);
if(a->composite.tex != NULL) {
log_debug("destroyed texture at %p", (void*)a->composite.tex);
r_texture_destroy(a->composite.tex);
}
portrait_render_byname(a->name, a->variant, a->face, &a->composite);
log_debug("created texture at %p", (void*)a->composite.tex);
a->composite_dirty = false;
}
void dialog_draw(Dialog *dialog) {
if(dialog == NULL) {
return;
}
float o = dialog->opacity;
for(DialogActor *a = dialog->actors.first; a; a = a->next) {
dialog_actor_update_composite(a);
}
r_state_push();
r_state_push();
r_shader("sprite_default");
r_mat_mv_push();
r_mat_mv_translate(VIEWPORT_X, 0, 0);
const double dialog_width = VIEWPORT_W * 1.2;
r_mat_mv_push();
r_mat_mv_translate(dialog_width/2.0, 64, 0);
Color clr = { 0 };
for(DialogActor *a = dialog->actors.first; a; a = a->next) {
if(a->opacity <= 0) {
continue;
}
dialog_actor_update_composite(a);
Sprite *portrait = &a->composite;
assume(portrait->tex != NULL);
r_mat_mv_push();
if(a->side == DIALOG_SIDE_LEFT) {
r_cull(CULL_FRONT);
r_mat_mv_scale(-1, 1, 1);
} else {
r_cull(CULL_BACK);
}
if(a->opacity < 1) {
r_mat_mv_translate(120 * (1 - a->opacity), 0, 0);
}
float ofs = 10 * (1 - a->focus);
r_mat_mv_translate(ofs, ofs, 0);
float brightness = 0.5 + 0.5 * a->focus;
clr.r = clr.g = clr.b = brightness;
clr.a = 1;
color_mul_scalar(&clr, a->opacity);
r_flush_sprites();
r_draw_sprite(&(SpriteParams) {
.blend = BLEND_PREMUL_ALPHA,
.color = &clr,
.pos.x = (dialog_width - portrait->w) / 2 + 32 + a->offset.x,
.pos.y = VIEWPORT_H - portrait->h / 2 + a->offset.y,
.sprite_ptr = portrait,
});
r_mat_mv_pop();
}
r_mat_mv_pop();
r_state_pop();
FloatRect dialog_bg_rect = {
.extent = { VIEWPORT_W-40, 110 },
.offset = { VIEWPORT_W/2, VIEWPORT_H-55 },
};
r_mat_mv_push();
if(o < 1) {
r_mat_mv_translate(0, 100 * (1 - o), 0);
}
r_color4(0, 0, 0, 0.8 * o);
r_mat_mv_push();
r_mat_mv_translate(dialog_bg_rect.x, dialog_bg_rect.y, 0);
r_mat_mv_scale(dialog_bg_rect.w, dialog_bg_rect.h, 1);
r_shader_standard_notex();
r_draw_quad();
r_mat_mv_pop();
Font *font = res_font("standard");
r_mat_tex_push();
dialog_bg_rect.w = VIEWPORT_W * 0.86;
dialog_bg_rect.x -= dialog_bg_rect.w * 0.5;
dialog_bg_rect.y -= dialog_bg_rect.h * 0.5;
if(dialog->text.fading_out->opacity > 0) {
clr = dialog->text.fading_out->color;
color_mul_scalar(&clr, o);
text_draw_wrapped(dialog->text.fading_out->text, dialog_bg_rect.w, &(TextParams) {
.shader = "text_dialog",
.aux_textures = { res_texture("cell_noise") },
.shader_params = &(ShaderCustomParams) {{ o * (1.0 - (0.2 + 0.8 * (1 - dialog->text.fading_out->opacity))), 1 }},
.color = &clr,
.pos = { VIEWPORT_W/2, VIEWPORT_H-110 + font_get_lineskip(font) },
.align = ALIGN_CENTER,
.font_ptr = font,
.overlay_projection = &dialog_bg_rect,
});
}
if(dialog->text.current->opacity > 0) {
clr = dialog->text.current->color;
color_mul_scalar(&clr, o);
text_draw_wrapped(dialog->text.current->text, dialog_bg_rect.w, &(TextParams) {
.shader = "text_dialog",
.aux_textures = { res_texture("cell_noise") },
.shader_params = &(ShaderCustomParams) {{ o * dialog->text.current->opacity, 0 }},
.color = &clr,
.pos = { VIEWPORT_W/2, VIEWPORT_H-110 + font_get_lineskip(font) },
.align = ALIGN_CENTER,
.font_ptr = font,
.overlay_projection = &dialog_bg_rect,
});
}
r_mat_tex_pop();
r_mat_mv_pop();
r_mat_mv_pop();
r_state_pop();
}
bool dialog_page(Dialog *d) {
if(d->state == DIALOG_STATE_WAITING_FOR_SKIP) {
coevent_signal(&d->events.skip_requested);
return true;
}
return false;
}
bool dialog_is_active(Dialog *d) {
return d && d->state != DIALOG_STATE_FADEOUT;
}
void dialog_preload(ResourceGroup *rg) {
res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT, "text_dialog", NULL);
}