taisei/src/dialog.c
Andrei Alexeyev 80fda75ffb
Dialogue updates with dynamic facial expressions and defeated character portraits (#175)
* WIP portrait faces

* character expressions for dialogue in first 3 stages (wip)

* dialog: simplified face system, new faces, script revisions (wip)

* fix wrong face name in reimu script

* cirno angry face; elly beaten variant

* render_character_portrait(): fix garbage bg and filter-bleeding

* workaround for incorrect face blending on dialog fade

* render_character_portrait(): fix bogus debug labels

* Replace sprite offsets with the more powerful paddings

* new faces for youmu, wriggle, kurumi; dialogue updates

* fix release build

* Update portraits; add Iku faces and Elly shouting face

* Add Reimu sigh face and Marisa sweat_smile face

* Add Kurumi dissatisfied face

* update Hina
2019-08-22 22:43:34 +03:00

417 lines
9.5 KiB
C

/*
* This software is licensed under the terms of the MIT License.
* See COPYING for further information.
* ---
* Copyright (c) 2011-2019, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2019, Andrei Alexeyev <akari@taisei-project.org>.
*/
#include "taisei.h"
#include "dialog.h"
#include "global.h"
Dialog *dialog_create(void) {
Dialog *d = calloc(1, sizeof(Dialog));
d->page_time = global.frames;
d->birthtime = global.frames;
return d;
}
void dialog_set_base(Dialog *d, DialogSide side, const char *sprite) {
d->spr_base[side] = sprite ? get_sprite(sprite) : NULL;
d->valid_composites &= ~(1 << side);
}
void dialog_set_base_p(Dialog *d, DialogSide side, Sprite *sprite) {
d->spr_base[side] = sprite;
d->valid_composites &= ~(1 << side);
}
void dialog_set_face(Dialog *d, DialogSide side, const char *sprite) {
d->spr_face[side] = sprite ? get_sprite(sprite) : NULL;
d->valid_composites &= ~(1 << side);
}
void dialog_set_face_p(Dialog *d, DialogSide side, Sprite *sprite) {
d->spr_face[side] = sprite;
d->valid_composites &= ~(1 << side);
}
void dialog_set_char(Dialog *d, DialogSide side, const char *char_name, const char *char_face, const char *char_variant) {
size_t name_len = strlen(char_name);
size_t face_len = strlen(char_face);
size_t variant_len;
size_t lenfull_base = sizeof("dialog/") + name_len - 1;
size_t lenfull_face = lenfull_base + sizeof("_face_") + face_len - 1;
if(char_variant) {
variant_len = strlen(char_variant);
lenfull_base += sizeof("_variant_") + variant_len - 1;
}
char buf[imax(lenfull_base, lenfull_face) + 1];
char *dst = buf;
char *variant_dst;
dst = memcpy(dst, "dialog/", sizeof("dialog/") - 1);
dst = memcpy(dst + sizeof("dialog/") - 1, char_name, name_len + 1);
if(char_variant) {
variant_dst = dst + name_len;
} else {
d->spr_base[side] = get_sprite(buf);
}
dst = memcpy(dst + name_len, "_face_", sizeof("_face_") - 1);
dst = memcpy(dst + sizeof("_face_") - 1, char_face, face_len + 1);
d->spr_face[side] = get_sprite(buf);
if(!char_variant) {
return;
}
dst = memcpy(variant_dst, "_variant_", sizeof("_variant_") - 1);
dst = memcpy(dst + sizeof("_variant_") - 1, char_variant, variant_len + 1);
d->spr_base[side] = get_sprite(buf);
d->valid_composites &= ~(1 << side);
}
static int message_index(Dialog *d, int offset) {
int idx = d->pos + offset;
if(idx >= d->count) {
idx = d->count - 1;
}
while(
idx >= 0 &&
d->actions[idx].type != DIALOG_MSG_LEFT &&
d->actions[idx].type != DIALOG_MSG_RIGHT
) {
--idx;
}
return idx;
}
DialogAction *dialog_add_action(Dialog *d, const DialogAction *action) {
d->actions = realloc(d->actions, (++d->count)*sizeof(DialogAction));
d->actions[d->count - 1] = *action;
return d->actions + d->count - 1;
}
static void update_composite(Dialog *d, DialogSide side) {
if(d->valid_composites & (1 << side)) {
return;
}
Sprite *composite = d->spr_composite + side;
Sprite *spr_base = d->spr_base[side];
Sprite *spr_face = d->spr_face[side];
if(composite->tex != NULL) {
r_texture_destroy(composite->tex);
}
if(spr_base != NULL) {
assert(spr_face != NULL);
render_character_portrait(spr_base, spr_face, composite);
} else {
composite->tex = NULL;
}
d->valid_composites |= (1 << side);
}
static void update_composites(Dialog *d) {
update_composite(d, DIALOG_LEFT);
update_composite(d, DIALOG_RIGHT);
}
void dialog_destroy(Dialog *d) {
memset(&d->spr_base, 0, sizeof(d->spr_base));
memset(&d->spr_face, 0, sizeof(d->spr_face));
d->valid_composites = 0;
update_composites(d);
free(d->actions);
free(d);
}
void dialog_draw(Dialog *dialog) {
if(dialog == NULL) {
return;
}
float o = dialog->opacity;
if(o == 0) {
return;
}
update_composites(dialog);
r_state_push();
r_state_push();
r_shader("sprite_default");
r_mat_push();
r_mat_translate(VIEWPORT_X, 0, 0);
const double dialog_width = VIEWPORT_W * 1.2;
r_mat_push();
r_mat_translate(dialog_width/2.0, 64, 0);
int cur_idx = message_index(dialog, 0);
int pre_idx = message_index(dialog, -1);
assume(cur_idx >= 0);
int cur_side = dialog->actions[cur_idx].type;
int pre_side = pre_idx >= 0 ? dialog->actions[pre_idx].type : 2;
Color clr = { 0 };
const float page_time = 10;
float page_alpha = min(global.frames - (dialog->page_time), page_time) / page_time;
const float page_text_time = 60;
float page_text_alpha = min(global.frames - dialog->page_time, page_text_time) / page_text_time;
int loop_start = 1;
int loop_incr = 1;
if(cur_side == 0) {
loop_start = 1;
loop_incr = -1;
} else {
loop_start = 0;
loop_incr = 1;
}
for(int i = loop_start; i < 2 && i >= 0; i += loop_incr) {
Sprite *portrait = dialog->spr_composite + i;
if(portrait->tex == NULL) {
continue;
}
float portrait_w = sprite_padded_width(portrait);
float portrait_h = sprite_padded_height(portrait);
r_mat_push();
if(i == DIALOG_MSG_LEFT) {
r_cull(CULL_FRONT);
r_mat_scale(-1, 1, 1);
} else {
r_cull(CULL_BACK);
}
if(o < 1) {
r_mat_translate(120 * (1 - o), 0, 0);
}
float dir = (1 - 2 * (i == cur_side));
float ofs = 10 * dir;
if(page_alpha < 10 && ((i != pre_side && i == cur_side) || (i == pre_side && i != cur_side))) {
r_mat_translate(ofs * page_alpha, ofs * page_alpha, 0);
float brightness = min(1.0 - 0.5 * page_alpha * dir, 1);
clr.r = clr.g = clr.b = brightness;
clr.a = 1;
} else {
r_mat_translate(ofs, ofs, 0);
clr = *RGB(1 - (dir > 0) * 0.5, 1 - (dir > 0) * 0.5, 1 - (dir > 0) * 0.5);
}
color_mul_scalar(&clr, o);
r_draw_sprite(&(SpriteParams) {
.blend = BLEND_PREMUL_ALPHA,
.color = &clr,
.pos.x = (dialog_width - portrait_w) / 2 + 32,
.pos.y = VIEWPORT_H - portrait_h / 2,
.sprite_ptr = portrait,
});
r_mat_pop();
}
r_mat_pop();
r_state_pop();
o *= smooth(clamp((global.frames - dialog->birthtime - 10) / 30.0, 0, 1));
FloatRect dialog_bg_rect = {
.extent = { VIEWPORT_W-40, 110 },
.offset = { VIEWPORT_W/2, VIEWPORT_H-55 },
};
r_mat_push();
if(o < 1) {
r_mat_translate(0, 100 * (1 - o), 0);
}
r_color4(0, 0, 0, 0.8 * o);
r_mat_push();
r_mat_translate(dialog_bg_rect.x, dialog_bg_rect.y, 0);
r_mat_scale(dialog_bg_rect.w, dialog_bg_rect.h, 1);
r_shader_standard_notex();
r_draw_quad();
r_mat_pop();
Font *font = get_font("standard");
r_mat_mode(MM_TEXTURE);
r_mat_push();
// r_mat_scale(2, 0.2, 0);
// r_mat_translate(0, -global.frames/page_text_time, 0);
r_mat_mode(MM_MODELVIEW);
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;
// dialog_bg_rect.h = dialog_bg_rect.w;
if(pre_idx >= 0 && page_text_alpha < 1) {
if(pre_side == DIALOG_MSG_RIGHT) {
clr = *RGB(0.6, 0.6, 1.0);
} else {
clr = *RGB(1.0, 1.0, 1.0);
}
color_mul_scalar(&clr, o);
text_draw_wrapped(dialog->actions[pre_idx].data, VIEWPORT_W * 0.86, &(TextParams) {
.shader = "text_dialog",
.aux_textures = { get_tex("cell_noise") },
.shader_params = &(ShaderCustomParams) {{ o * (1.0 - (0.2 + 0.8 * page_text_alpha)), 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(cur_side == DIALOG_MSG_RIGHT) {
clr = *RGB(0.6, 0.6, 1.0);
} else {
clr = *RGB(1.0, 1.0, 1.0);
}
color_mul_scalar(&clr, o);
text_draw_wrapped(dialog->actions[cur_idx].data, VIEWPORT_W * 0.86, &(TextParams) {
.shader = "text_dialog",
.aux_textures = { get_tex("cell_noise") },
.shader_params = &(ShaderCustomParams) {{ o * page_text_alpha, 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_mode(MM_TEXTURE);
r_mat_pop();
r_mat_mode(MM_MODELVIEW);
r_mat_pop();
r_mat_pop();
r_state_pop();
}
bool dialog_page(Dialog **pdialog) {
Dialog *d = *pdialog;
if(!d || d->pos >= d->count) {
return false;
}
int to = d->actions[d->pos].timeout;
if(to && to > global.frames) {
assert(d->actions[d->pos].type == DIALOG_MSG_LEFT || d->actions[d->pos].type == DIALOG_MSG_RIGHT);
return false;
}
DialogAction *a = d->actions + d->pos;
d->pos++;
d->page_time = global.frames;
if(d->pos >= d->count) {
// XXX: maybe this can be handled elsewhere?
if(!global.boss)
global.timer++;
}
switch(a->type) {
case DIALOG_SET_BGM:
stage_start_bgm(a->data);
break;
case DIALOG_SET_FACE_RIGHT:
case DIALOG_SET_FACE_LEFT: {
DialogSide side = (
a->type == DIALOG_SET_FACE_RIGHT
? DIALOG_RIGHT
: DIALOG_LEFT
);
dialog_set_face(d, side, a->data);
break;
}
default:
break;
}
return true;
}
void dialog_update(Dialog **d) {
if(!*d) {
return;
}
if(dialog_is_active(*d)) {
while((*d)->pos < (*d)->count) {
if(
(*d)->actions[(*d)->pos].type != DIALOG_MSG_LEFT &&
(*d)->actions[(*d)->pos].type != DIALOG_MSG_RIGHT
) {
dialog_page(d);
} else {
int to = (*d)->actions[(*d)->pos].timeout;
if(
(to && to >= global.frames) ||
((global.plr.inputflags & INFLAG_SKIP) && global.frames - (*d)->page_time > 3)
) {
dialog_page(d);
}
break;
}
}
}
// important to check this again; the dialog_page call may have ended the dialog
if(dialog_is_active(*d)) {
fapproach_asymptotic_p(&(*d)->opacity, 1, 0.05, 1e-3);
} else {
fapproach_asymptotic_p(&(*d)->opacity, 0, 0.1, 1e-3);
if((*d)->opacity == 0) {
dialog_destroy(*d);
*d = NULL;
}
}
}
bool dialog_is_active(Dialog *d) {
return d && (d->pos < d->count);
}