391 lines
9.3 KiB
C
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, ¶ms->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);
|
|
}
|