Cutscenes (#249)
* WIP cutscenes * cutscene tweaks * cutscene: erase background drawing under text * Make text outlines thicker * Prepare an interface for adding new cutscenes * Basic progress tracking for cutscenes * cutscene: support specifying scene name and BGM * cutscene: exit with transition after scene ends * Implement --cutscene ID and --list-cutscenes CLI flags * fix progress_write_cmd_unlock_cutscenes * Play intro cutscene before entering main menu for the first time Also added --intro parameter in dev builds to force playing the intro cutscene * Add intro cutscene * cutscenes: update opening/01 scene * add Reimu Good End * remove Bonus Data * split up a bit of dialogue, revert an image change in intro * small typo * most cutscenes complete * smartquotify * finish Extra intros * new cutscenes routed into main game * fix ENDING_ID * rough 'mediaroom' menu * fix cutscene menu crash * derp * PR changes * fixing imports * more PR fixes * PR fixes, including updating the script to #255 * add in newlines for readability Co-authored-by: Alice D <alice@starwitch.productions>
This commit is contained in:
parent
b257f665f6
commit
9b5d515721
72 changed files with 1871 additions and 592 deletions
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/locations/hakurei.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/locations/hakurei.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/locations/moriya.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/locations/moriya.basis
Normal file
Binary file not shown.
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/locations/sdm.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/locations/sdm.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/locations/tower.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/locations/tower.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_bad/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_bad/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_bad/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_bad/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_extra/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_extra/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_extra/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_extra/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_good/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_good/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_good/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/marisa_good/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/opening/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/opening/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/opening/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/opening/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/opening/03.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/opening/03.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/opening/04.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/opening/04.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/paper.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/paper.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_bad/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_bad/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_bad/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_bad/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_extra/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_extra/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_extra/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_extra/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_good/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_good/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_good/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_good/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_good/03.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/reimu_good/03.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_bad/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_bad/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_bad/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_bad/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_extra/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_extra/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_extra/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_extra/02.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_good/01.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_good/01.basis
Normal file
Binary file not shown.
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_good/02.basis
Normal file
BIN
resources/00-taisei.pkgdir/gfx/cutscenes/youmu_good/02.basis
Normal file
Binary file not shown.
68
resources/00-taisei.pkgdir/shader/cutscene.frag.glsl
Normal file
68
resources/00-taisei.pkgdir/shader/cutscene.frag.glsl
Normal file
|
@ -0,0 +1,68 @@
|
|||
#version 330 core
|
||||
|
||||
#include "lib/render_context.glslh"
|
||||
#include "interface/standard.glslh"
|
||||
#include "lib/util.glslh"
|
||||
|
||||
UNIFORM(1) sampler2D noise_tex;
|
||||
UNIFORM(2) sampler2D paper_tex;
|
||||
UNIFORM(3) sampler2D erase_mask_tex;
|
||||
UNIFORM(4) float distort_strength = 0.01;
|
||||
UNIFORM(5) vec2 thresholds;
|
||||
|
||||
float render_fademap(vec2 uv, vec2 distort) {
|
||||
// Technically this could be pre-rendered, but 8 bits of precision is not enough,
|
||||
// and renderdoc won't let me save a 16-bit texture for some dumb reason, so whatever.
|
||||
|
||||
vec2 ntc_n = (uv - 0.5) + (distort * 3) / 0.2;
|
||||
vec2 ntc = 0.2 * ntc_n + vec2(0.23, 0.43);
|
||||
float n = texture(noise_tex, ntc).r;
|
||||
float r = 1 - 2 * length(ntc_n);
|
||||
float f = r * r * 0.4 + n * 0.6;
|
||||
|
||||
// hardcoded range adjustment; crucify me
|
||||
f -= 0.0012944491858334999;
|
||||
f *= 1.559332605644784;
|
||||
f = clamp(f, 0, 1);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
float project_drawing(float drawing, float fademap, float grainmap) {
|
||||
return 1 - smoothstep(0.0, 1.0, fademap * grainmap) * (1 - drawing);
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
vec4 paper = texture(paper_tex, texCoord);
|
||||
vec2 distort_offset = vec2(-0.7, -0.75); // NOTE: tuned for the paper texture
|
||||
vec2 distort = (vec2(paper.g, paper.r) + distort_offset) * distort_strength;
|
||||
|
||||
float fademap = render_fademap(texCoord, distort);
|
||||
fragColor = vec4(vec3(fademap), 1);
|
||||
fragColor.gb = mod(fragColor.gb, vec2(2.0));
|
||||
|
||||
float drawing = texture(tex, texCoord + distort).r;
|
||||
|
||||
float erase_mask = texture(erase_mask_tex, texCoord + distort * 10).a;
|
||||
erase_mask = smoothstep(0, 0.3, erase_mask * (paper.b + 1));
|
||||
drawing = min(1, drawing + erase_mask);
|
||||
|
||||
vec2 fade_thresholds = pow(thresholds, 1 / vec2(paper.b));
|
||||
fademap = smoothstep(fade_thresholds.x, fade_thresholds.y, fademap);
|
||||
|
||||
float grainmap = pow(smoothstep(0, 0.9, paper.r), 4.0);
|
||||
|
||||
float fademap2 = fademap * fademap;
|
||||
float fademap3 = fademap2 * fademap;
|
||||
float fademap5 = fademap3 * fademap2;
|
||||
|
||||
float pd = 0;
|
||||
pd += project_drawing(drawing, fademap, grainmap) * 0.4;
|
||||
pd += project_drawing(drawing, fademap3, grainmap) * 0.3;
|
||||
pd += project_drawing(drawing, fademap5, grainmap) * 0.3;
|
||||
drawing = pd;
|
||||
|
||||
vec3 color = mix(paper.rgb * paper.rgb * 0.34, paper.rgb, drawing);
|
||||
|
||||
fragColor = vec4(color, 1);
|
||||
}
|
2
resources/00-taisei.pkgdir/shader/cutscene.prog
Normal file
2
resources/00-taisei.pkgdir/shader/cutscene.prog
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
objects = cutscene.frag standard.vert
|
|
@ -19,6 +19,7 @@ glsl_files = files(
|
|||
'boss_zoom.frag.glsl',
|
||||
'circle_distort.frag.glsl',
|
||||
'copy_depth.frag.glsl',
|
||||
'cutscene.frag.glsl',
|
||||
'extra_bg.frag.glsl',
|
||||
'extra_tower_apply_mask.frag.glsl',
|
||||
'extra_tower_mask.frag.glsl',
|
||||
|
@ -69,6 +70,7 @@ glsl_files = files(
|
|||
'standard.vert.glsl',
|
||||
'standardnotex.frag.glsl',
|
||||
'standardnotex.vert.glsl',
|
||||
'text_cutscene.frag.glsl',
|
||||
'text_default.frag.glsl',
|
||||
'text_default.vert.glsl',
|
||||
'text_dialog.frag.glsl',
|
||||
|
|
27
resources/00-taisei.pkgdir/shader/text_cutscene.frag.glsl
Normal file
27
resources/00-taisei.pkgdir/shader/text_cutscene.frag.glsl
Normal file
|
@ -0,0 +1,27 @@
|
|||
#version 330 core
|
||||
|
||||
#include "lib/sprite_main.frag.glslh"
|
||||
|
||||
float sampleNoise(vec2 tc) {
|
||||
tc.y *= fwidth(tc.x) / fwidth(tc.y);
|
||||
return texture(tex_aux[0], tc).r;
|
||||
}
|
||||
|
||||
void spriteMain(out vec4 fragColor) {
|
||||
float mask = sampleNoise(texCoordOverlay);
|
||||
float o = customParams.r;
|
||||
float xpos = 0.5 + texCoordOverlay.x;
|
||||
float slide_factor = 8;
|
||||
mask = 1.0 - smoothstep(slide_factor * o * o, slide_factor * o, mask + (slide_factor - 1.0) * xpos);
|
||||
mask = smoothstep(0.2, 0.8, mask);
|
||||
|
||||
vec3 outlines = texture(tex, texCoord).rgb;
|
||||
|
||||
vec4 highlight = color;
|
||||
vec4 fill = vec4(color.rgb * 0.9, color.a) * mask;
|
||||
vec4 border = vec4(color.rgb * 0.3, color.a) * mask * mask;
|
||||
|
||||
fragColor = outlines.g * mix(border, mix(fill, highlight, outlines.b), outlines.r);
|
||||
fragColor.rgb *= mask * mask;
|
||||
fragColor *= mask;
|
||||
}
|
2
resources/00-taisei.pkgdir/shader/text_cutscene.prog
Normal file
2
resources/00-taisei.pkgdir/shader/text_cutscene.prog
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
objects = text_dialog.vert text_cutscene.frag
|
29
src/cli.c
29
src/cli.c
|
@ -17,11 +17,16 @@
|
|||
#include "stage.h"
|
||||
#include "plrmodes.h"
|
||||
#include "version.h"
|
||||
#include "cutscenes/cutscene.h"
|
||||
#include "cutscenes/scenes.h"
|
||||
|
||||
struct TsOption { struct option opt; const char *help; const char *argname;};
|
||||
|
||||
enum {
|
||||
OPT_RENDERER = INT_MIN,
|
||||
OPT_CUTSCENE,
|
||||
OPT_CUTSCENE_LIST,
|
||||
OPT_FORCE_INTRO,
|
||||
};
|
||||
|
||||
static void print_help(struct TsOption* opts) {
|
||||
|
@ -77,6 +82,9 @@ int cli_args(int argc, char **argv, CLIAction *a) {
|
|||
{{"shotmode", required_argument, 0, 's'}, "Select a shotmode (marisaA/youmuA/marisaB/youmuB)", "SMODE"},
|
||||
{{"dumpstages", no_argument, 0, 'u'}, "Print a list of all stages in the game"},
|
||||
{{"vfs-tree", required_argument, 0, 't'}, "Print the virtual filesystem tree starting from %s", "PATH"},
|
||||
{{"cutscene", required_argument, 0, OPT_CUTSCENE}, "Play cutscene by numeric %s and exit", "ID"},
|
||||
{{"list-cutscenes", no_argument, 0, OPT_CUTSCENE_LIST}, "List all registered cutscenes with their numeric IDs and names, then exit" },
|
||||
{{"intro", no_argument, 0, OPT_FORCE_INTRO}, "Play the intro cutscene even if already seen"},
|
||||
#endif
|
||||
{{"frameskip", optional_argument, 0, 'f'}, "Disable FPS limiter, render only every %s frame", "FRAME"},
|
||||
{{"credits", no_argument, 0, 'c'}, "Show the credits scene and exit"},
|
||||
|
@ -86,7 +94,7 @@ int cli_args(int argc, char **argv, CLIAction *a) {
|
|||
{ 0 }
|
||||
};
|
||||
|
||||
memset(a,0,sizeof(CLIAction));
|
||||
memset(a, 0, sizeof(*a));
|
||||
|
||||
int nopts = sizeof(taisei_opts)/sizeof(taisei_opts[0]);
|
||||
struct option opts[nopts];
|
||||
|
@ -197,6 +205,25 @@ int cli_args(int argc, char **argv, CLIAction *a) {
|
|||
case OPT_RENDERER:
|
||||
env_set("TAISEI_RENDERER", optarg, true);
|
||||
break;
|
||||
case OPT_CUTSCENE:
|
||||
a->type = CLI_Cutscene;
|
||||
a->cutscene = strtol(optarg, &endptr, 16);
|
||||
|
||||
if(!*optarg || endptr == optarg || (uint)a->cutscene >= NUM_CUTSCENE_IDS) {
|
||||
log_fatal("Invalid cutscene ID '%s'", optarg);
|
||||
}
|
||||
|
||||
break;
|
||||
case OPT_CUTSCENE_LIST:
|
||||
for(CutsceneID i = 0; i < NUM_CUTSCENE_IDS; ++i) {
|
||||
const Cutscene *cs = g_cutscenes + i;
|
||||
tsfprintf(stdout, "%2d : %s\n", i, cs->phases ? cs->name : "!! UNIMPLEMENTED !!");
|
||||
}
|
||||
|
||||
exit(0);
|
||||
case OPT_FORCE_INTRO:
|
||||
a->force_intro = true;
|
||||
break;
|
||||
case 'v':
|
||||
tsfprintf(stdout, "%s %s\n", TAISEI_VERSION_FULL, TAISEI_VERSION_BUILD_TYPE);
|
||||
exit(0);
|
||||
|
|
|
@ -22,15 +22,18 @@ typedef enum {
|
|||
CLI_DumpVFSTree,
|
||||
CLI_Quit,
|
||||
CLI_Credits,
|
||||
CLI_Cutscene,
|
||||
} CLIActionType;
|
||||
|
||||
typedef struct CLIAction CLIAction;
|
||||
struct CLIAction {
|
||||
CLIActionType type;
|
||||
char *filename;
|
||||
bool force_intro;
|
||||
int stageid;
|
||||
int diff;
|
||||
int frameskip;
|
||||
CutsceneID cutscene;
|
||||
char *filename;
|
||||
PlayerMode *plrmode;
|
||||
};
|
||||
|
||||
|
|
485
src/cutscenes/cutscene.c
Normal file
485
src/cutscenes/cutscene.c
Normal file
|
@ -0,0 +1,485 @@
|
|||
/*
|
||||
* 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 "cutscene.h"
|
||||
#include "scene_impl.h"
|
||||
#include "scenes.h"
|
||||
|
||||
#include "audio/audio.h"
|
||||
#include "color.h"
|
||||
#include "global.h"
|
||||
#include "progress.h"
|
||||
#include "renderer/api.h"
|
||||
#include "util/fbmgr.h"
|
||||
#include "util/glm.h"
|
||||
#include "video.h"
|
||||
|
||||
#define SKIP_DELAY 3
|
||||
#define AUTO_ADVANCE_TIME_BEFORE_TEXT FPS * 2
|
||||
#define AUTO_ADVANCE_TIME_MID_SCENE FPS * 20
|
||||
#define AUTO_ADVANCE_TIME_CROSS_SCENE FPS * 180
|
||||
|
||||
// TODO maybe make transitions configurable?
|
||||
#define CUTSCENE_FADE_OUT 200
|
||||
|
||||
typedef struct CutsceneBGState {
|
||||
Texture *scene;
|
||||
Texture *next_scene;
|
||||
float alpha;
|
||||
float transition_rate;
|
||||
bool fade_out;
|
||||
} CutsceneBGState;
|
||||
|
||||
typedef struct CutsceneTextVisual {
|
||||
LIST_INTERFACE(struct CutsceneTextVisual);
|
||||
const CutscenePhaseTextEntry *entry;
|
||||
float alpha;
|
||||
float target_alpha;
|
||||
} CutsceneTextVisual;
|
||||
|
||||
typedef struct CutsceneState {
|
||||
const CutscenePhase *phase;
|
||||
const CutscenePhaseTextEntry *text_entry;
|
||||
CallChain cc;
|
||||
|
||||
CutsceneBGState bg_state;
|
||||
LIST_ANCHOR(CutsceneTextVisual) text_visuals;
|
||||
|
||||
ManagedFramebufferGroup *mfb_group;
|
||||
Framebuffer *text_fb;
|
||||
FBPair erase_mask_fbpair;
|
||||
|
||||
int skip_timer;
|
||||
int advance_timer;
|
||||
int fadeout_timer;
|
||||
} CutsceneState;
|
||||
|
||||
static void clear_text(CutsceneState *st) {
|
||||
for(CutsceneTextVisual *tv = st->text_visuals.first; tv; tv = tv->next) {
|
||||
tv->target_alpha = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void switch_bg(CutsceneState *st, const char *texture) {
|
||||
Texture *scene = *texture ? res_texture(texture) : NULL;
|
||||
|
||||
if(st->bg_state.scene == NULL) {
|
||||
st->bg_state.scene = scene;
|
||||
st->bg_state.fade_out = false;
|
||||
} else {
|
||||
st->bg_state.next_scene = scene;
|
||||
|
||||
if(st->bg_state.scene == st->bg_state.next_scene) {
|
||||
st->bg_state.next_scene = NULL;
|
||||
st->bg_state.fade_out = false;
|
||||
} else {
|
||||
st->bg_state.fade_out = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_timers(CutsceneState *st) {
|
||||
st->skip_timer = SKIP_DELAY;
|
||||
|
||||
if(st->text_entry) {
|
||||
if(st->text_entry[1].text) {
|
||||
st->advance_timer = AUTO_ADVANCE_TIME_MID_SCENE;
|
||||
} else {
|
||||
st->advance_timer = AUTO_ADVANCE_TIME_CROSS_SCENE;
|
||||
}
|
||||
} else {
|
||||
st->advance_timer = AUTO_ADVANCE_TIME_BEFORE_TEXT;
|
||||
}
|
||||
|
||||
log_debug("st->advance_timer = %i", st->advance_timer);
|
||||
}
|
||||
|
||||
static bool skip_text_animation(CutsceneState *st) {
|
||||
bool animation_skipped = false;
|
||||
|
||||
for(CutsceneTextVisual *tv = st->text_visuals.first; tv; tv = tv->next) {
|
||||
if(tv->target_alpha > 0 && tv->alpha < tv->target_alpha * 0.6) {
|
||||
tv->alpha = tv->target_alpha;
|
||||
animation_skipped = true;
|
||||
}
|
||||
}
|
||||
|
||||
return animation_skipped;
|
||||
}
|
||||
|
||||
static void begin_fadeout(CutsceneState *st) {
|
||||
const int fade_frames = CUTSCENE_FADE_OUT;
|
||||
audio_bgm_stop((FPS * fade_frames) / 4000.0);
|
||||
set_transition(TransFadeBlack, fade_frames, fade_frames);
|
||||
st->fadeout_timer = fade_frames;
|
||||
st->bg_state.next_scene = NULL;
|
||||
st->bg_state.fade_out = true;
|
||||
st->bg_state.transition_rate = 1.0f / fade_frames;
|
||||
}
|
||||
|
||||
static void cutscene_advance(CutsceneState *st) {
|
||||
if(st->skip_timer > 0) {
|
||||
log_debug("Skip too soon");
|
||||
return;
|
||||
}
|
||||
|
||||
if(st->phase) {
|
||||
if(st->text_entry == NULL) {
|
||||
st->text_entry = &st->phase->text_entries[0];
|
||||
} else if((++st->text_entry)->text == NULL) {
|
||||
if(skip_text_animation(st)) {
|
||||
--st->text_entry;
|
||||
return;
|
||||
}
|
||||
|
||||
st->text_entry = NULL;
|
||||
clear_text(st);
|
||||
|
||||
if((++st->phase)->background == NULL) {
|
||||
st->phase = NULL;
|
||||
begin_fadeout(st);
|
||||
} else {
|
||||
switch_bg(st, st->phase->background);
|
||||
}
|
||||
}
|
||||
|
||||
if(st->text_entry && st->text_entry->text) {
|
||||
CutsceneTextVisual *tv = calloc(1, sizeof(*tv));
|
||||
tv->alpha = 0.1;
|
||||
tv->target_alpha = 1;
|
||||
tv->entry = st->text_entry;
|
||||
alist_append(&st->text_visuals, tv);
|
||||
}
|
||||
}
|
||||
|
||||
reset_timers(st);
|
||||
}
|
||||
|
||||
static bool cutscene_event(SDL_Event *evt, void *ctx) {
|
||||
CutsceneState *st = ctx;
|
||||
|
||||
if(evt->type == MAKE_TAISEI_EVENT(TE_MENU_ACCEPT)) {
|
||||
cutscene_advance(st);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static LogicFrameAction cutscene_logic_frame(void *ctx) {
|
||||
CutsceneState *st = ctx;
|
||||
|
||||
update_transition();
|
||||
events_poll((EventHandler[]) {
|
||||
{ .proc = cutscene_event, .arg = st, .priority = EPRIO_NORMAL },
|
||||
{ NULL },
|
||||
}, EFLAG_MENU);
|
||||
|
||||
if(st->skip_timer > 0) {
|
||||
st->skip_timer--;
|
||||
}
|
||||
|
||||
if(st->advance_timer > 0) {
|
||||
st->advance_timer--;
|
||||
}
|
||||
|
||||
if(st->advance_timer == 0 || gamekeypressed(KEY_SKIP)) {
|
||||
cutscene_advance(st);
|
||||
}
|
||||
|
||||
if(st->bg_state.fade_out) {
|
||||
if(fapproach_p(&st->bg_state.alpha, 0, st->bg_state.transition_rate) == 0) {
|
||||
st->bg_state.scene = st->bg_state.next_scene;
|
||||
st->bg_state.next_scene = NULL;
|
||||
st->bg_state.fade_out = false;
|
||||
}
|
||||
} else if(st->bg_state.scene != NULL) {
|
||||
fapproach_p(&st->bg_state.alpha, 1, st->bg_state.transition_rate);
|
||||
}
|
||||
|
||||
for(CutsceneTextVisual *tv = st->text_visuals.first, *next; tv; tv = next) {
|
||||
next = tv->next;
|
||||
|
||||
float rate = 1/120.0f;
|
||||
|
||||
if(tv->target_alpha > 0 && st->text_entry != tv->entry) {
|
||||
// if we skipped past this one and it's still fading in, speed it up
|
||||
rate *= 5;
|
||||
}
|
||||
|
||||
if(fapproach_p(&tv->alpha, tv->target_alpha, rate) == 0) {
|
||||
free(alist_unlink(&st->text_visuals, tv));
|
||||
}
|
||||
}
|
||||
|
||||
if(st->fadeout_timer > 0) {
|
||||
if(!(--st->fadeout_timer)) {
|
||||
return LFRAME_STOP;
|
||||
}
|
||||
}
|
||||
|
||||
return LFRAME_WAIT;
|
||||
}
|
||||
|
||||
static void draw_background(CutsceneState *st, Texture *erase_mask) {
|
||||
r_state_push();
|
||||
r_blend(BLEND_NONE);
|
||||
r_mat_mv_push();
|
||||
r_mat_mv_scale(SCREEN_W, SCREEN_H, 1);
|
||||
r_mat_mv_translate(0.5, 0.5, 0);
|
||||
|
||||
float th0, th1, x;
|
||||
x = 1.0f - st->bg_state.alpha;
|
||||
|
||||
th0 = x;
|
||||
th1 = x * (x + 1.5f * (1.0f - x));
|
||||
|
||||
if(st->bg_state.fade_out) {
|
||||
th0 = 1 - th0;
|
||||
th1 = 1 - th1;
|
||||
}
|
||||
|
||||
r_shader("cutscene");
|
||||
r_uniform_vec2("thresholds", th0, th1);
|
||||
|
||||
if(st->bg_state.scene) {
|
||||
r_uniform_sampler("tex", st->bg_state.scene);
|
||||
}
|
||||
|
||||
r_uniform_sampler("noise_tex", "cell_noise");
|
||||
r_uniform_sampler("paper_tex", "cutscenes/paper");
|
||||
r_uniform_sampler("erase_mask_tex", erase_mask);
|
||||
r_uniform_float("distort_strength", 0.01);
|
||||
|
||||
r_draw_quad();
|
||||
|
||||
r_mat_mv_pop();
|
||||
r_state_pop();
|
||||
}
|
||||
|
||||
static void draw_center_text(
|
||||
const CutsceneTextVisual *tv,
|
||||
const CutscenePhaseTextEntry *e,
|
||||
TextParams p
|
||||
) {
|
||||
p.font_ptr = res_font("big");
|
||||
p.align = ALIGN_CENTER;
|
||||
p.pos.x = SCREEN_W/2;
|
||||
p.pos.y = SCREEN_H/2 - font_get_lineskip(p.font_ptr);
|
||||
text_draw(e->header, &p);
|
||||
p.pos.y += font_get_lineskip(p.font_ptr) * 1.2;
|
||||
p.font_ptr = res_font("standard");
|
||||
text_draw(e->text, &p);
|
||||
}
|
||||
|
||||
static void draw_text(CutsceneState *st) {
|
||||
Font *font = res_font("standard");
|
||||
const float lines = 7;
|
||||
const float offset_x = 16;
|
||||
const float offset_y = 24;
|
||||
|
||||
float width = SCREEN_W - 2 * offset_x;
|
||||
float speaker_width = width * 0.1;
|
||||
float height = lines * font_get_lineskip(font);
|
||||
|
||||
float x = offset_x, y = offset_y;
|
||||
|
||||
FloatRect textbox = {
|
||||
.extent = { width + offset_x * 2, height + offset_y * 2 },
|
||||
.offset = { x + width/2, y + height/2 - 2*offset_y/3 },
|
||||
};
|
||||
|
||||
r_state_push();
|
||||
|
||||
ShaderCustomParams cparams = { 0 };
|
||||
|
||||
TextParams p = {
|
||||
.shader_ptr = res_shader("text_cutscene"),
|
||||
.aux_textures = { res_texture("cell_noise") },
|
||||
.shader_params = &cparams,
|
||||
.pos = { x, y },
|
||||
.align = ALIGN_LEFT,
|
||||
.font_ptr = font,
|
||||
.overlay_projection = &textbox,
|
||||
};
|
||||
|
||||
for(CutsceneTextVisual *tv = st->text_visuals.first; tv; tv = tv->next) {
|
||||
const CutscenePhaseTextEntry *e = tv->entry;
|
||||
|
||||
p.color = &e->color;
|
||||
cparams.vector[0] = tv->alpha;
|
||||
|
||||
if(e->type == CTT_CENTERED) {
|
||||
draw_center_text(tv, e, p);
|
||||
continue;
|
||||
}
|
||||
|
||||
float w = width;
|
||||
p.pos.x = x;
|
||||
p.pos.y = y;
|
||||
|
||||
if(e->header != NULL) {
|
||||
float ofs = 8;
|
||||
p.pos.x += speaker_width - ofs;
|
||||
w -= speaker_width;
|
||||
p.align = ALIGN_RIGHT;
|
||||
text_draw(e->header, &p);
|
||||
p.align = ALIGN_LEFT;
|
||||
p.pos.x += ofs;
|
||||
}
|
||||
|
||||
char buf[strlen(e->text) * 2 + 1];
|
||||
text_wrap(font, e->text, w, buf, sizeof(buf));
|
||||
text_draw(buf, &p);
|
||||
|
||||
y += text_height(font, buf, 0) * glm_ease_quad_in(fminf(3 * tv->alpha, 1));
|
||||
}
|
||||
|
||||
r_state_pop();
|
||||
}
|
||||
|
||||
static RenderFrameAction cutscene_render_frame(void *ctx) {
|
||||
CutsceneState *st = ctx;
|
||||
r_clear(CLEAR_ALL, RGBA(0, 0, 0, 1), 1);
|
||||
set_ortho(SCREEN_W, SCREEN_H);
|
||||
|
||||
r_state_push();
|
||||
|
||||
r_framebuffer(st->text_fb);
|
||||
r_clear(CLEAR_ALL, RGBA(0, 0, 0, 0), 1);
|
||||
draw_text(st);
|
||||
|
||||
r_shader_standard();
|
||||
r_blend(BLEND_NONE);
|
||||
|
||||
r_framebuffer(st->erase_mask_fbpair.back);
|
||||
r_clear(CLEAR_ALL, RGBA(0, 0, 0, 0), 1);
|
||||
draw_framebuffer_tex(st->text_fb, SCREEN_W, SCREEN_H);
|
||||
fbpair_swap(&st->erase_mask_fbpair);
|
||||
|
||||
FloatRect mask_vp;
|
||||
r_framebuffer_viewport_current(st->erase_mask_fbpair.back, &mask_vp);
|
||||
r_shader("blur9");
|
||||
r_uniform_vec2("blur_resolution", mask_vp.w, mask_vp.h);
|
||||
|
||||
r_framebuffer(st->erase_mask_fbpair.back);
|
||||
r_clear(CLEAR_ALL, RGBA(0, 0, 0, 0), 1);
|
||||
r_uniform_vec2("blur_direction", 1, 0);
|
||||
draw_framebuffer_tex(st->erase_mask_fbpair.front, SCREEN_W, SCREEN_H);
|
||||
fbpair_swap(&st->erase_mask_fbpair);
|
||||
|
||||
r_framebuffer(st->erase_mask_fbpair.back);
|
||||
r_clear(CLEAR_ALL, RGBA(0, 0, 0, 0), 1);
|
||||
r_uniform_vec2("blur_direction", 0, 1);
|
||||
draw_framebuffer_tex(st->erase_mask_fbpair.front, SCREEN_W, SCREEN_H);
|
||||
fbpair_swap(&st->erase_mask_fbpair);
|
||||
|
||||
r_state_pop();
|
||||
|
||||
draw_background(st, r_framebuffer_get_attachment(st->erase_mask_fbpair.front, FRAMEBUFFER_ATTACH_COLOR0));
|
||||
draw_framebuffer_tex(st->text_fb, SCREEN_W, SCREEN_H);
|
||||
|
||||
draw_transition();
|
||||
|
||||
return RFRAME_SWAP;
|
||||
}
|
||||
|
||||
static void cutscene_end_loop(void *ctx) {
|
||||
CutsceneState *st = ctx;
|
||||
|
||||
for(CutsceneTextVisual *tv = st->text_visuals.first, *next; tv; tv = next) {
|
||||
next = tv->next;
|
||||
free(tv);
|
||||
}
|
||||
|
||||
fbmgr_group_destroy(st->mfb_group);
|
||||
|
||||
CallChain cc = st->cc;
|
||||
free(st);
|
||||
run_call_chain(&cc, NULL);
|
||||
}
|
||||
|
||||
static void cutscene_preload(const CutscenePhase phases[]) {
|
||||
for(const CutscenePhase *p = phases; p->background; ++p) {
|
||||
if(*p->background) {
|
||||
preload_resource(RES_TEXTURE, p->background, RESF_DEFAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void resize_fb(void *userdata, IntExtent *out_dimensions, FloatRect *out_viewport) {
|
||||
float w, h;
|
||||
video_get_viewport_size(&w, &h);
|
||||
|
||||
out_dimensions->w = w;
|
||||
out_dimensions->h = h;
|
||||
|
||||
out_viewport->w = w;
|
||||
out_viewport->h = h;
|
||||
out_viewport->x = 0;
|
||||
out_viewport->y = 0;
|
||||
}
|
||||
|
||||
static CutsceneState *cutscene_state_new(const CutscenePhase phases[]) {
|
||||
cutscene_preload(phases);
|
||||
CutsceneState *st = calloc(1, sizeof(*st));
|
||||
st->phase = &phases[0];
|
||||
switch_bg(st, st->phase->background);
|
||||
reset_timers(st);
|
||||
|
||||
st->mfb_group = fbmgr_group_create();
|
||||
|
||||
FBAttachmentConfig a = { 0 };
|
||||
a.attachment = FRAMEBUFFER_ATTACH_COLOR0;
|
||||
a.tex_params.type = TEX_TYPE_RGBA_8;
|
||||
a.tex_params.filter.min = TEX_FILTER_LINEAR;
|
||||
a.tex_params.filter.mag = TEX_FILTER_LINEAR;
|
||||
a.tex_params.wrap.s = TEX_WRAP_MIRROR;
|
||||
a.tex_params.wrap.s = TEX_WRAP_MIRROR;
|
||||
|
||||
FramebufferConfig fbconf = { 0 };
|
||||
fbconf.attachments = &a;
|
||||
fbconf.num_attachments = 1;
|
||||
fbconf.resize_strategy.resize_func = resize_fb;
|
||||
|
||||
st->text_fb = fbmgr_group_framebuffer_create(st->mfb_group, "Cutscene text", &fbconf);
|
||||
|
||||
fbconf.resize_strategy.resize_func = NULL;
|
||||
a.tex_params.width = SCREEN_W / 4;
|
||||
a.tex_params.height= SCREEN_H / 4;
|
||||
a.tex_params.type = TEX_TYPE_RGBA_8;
|
||||
fbmgr_group_fbpair_create(st->mfb_group, "Cutscene erase mask", &fbconf, &st->erase_mask_fbpair);
|
||||
|
||||
return st;
|
||||
}
|
||||
|
||||
void cutscene_enter(CallChain next, CutsceneID id) {
|
||||
assert((uint)id < NUM_CUTSCENE_IDS);
|
||||
|
||||
progress_unlock_cutscene(id);
|
||||
const Cutscene *cs = g_cutscenes + id;
|
||||
|
||||
if(cs->phases == NULL) {
|
||||
log_error("Cutscene %i not implemented!", id);
|
||||
run_call_chain(&next, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("Playing cutscene ID: #%i", id);
|
||||
CutsceneState *st = cutscene_state_new(cs->phases);
|
||||
st->cc = next;
|
||||
st->bg_state.transition_rate = 1/80.0f;
|
||||
audio_bgm_play(res_bgm(cs->bgm), true, 0, 1);
|
||||
eventloop_enter(st, cutscene_logic_frame, cutscene_render_frame, cutscene_end_loop, FPS);
|
||||
}
|
||||
|
||||
const char *cutscene_get_name(CutsceneID id) {
|
||||
assert((uint)id < NUM_CUTSCENE_IDS);
|
||||
return g_cutscenes[id].name;
|
||||
}
|
38
src/cutscenes/cutscene.h
Normal file
38
src/cutscenes/cutscene.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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>.
|
||||
*/
|
||||
|
||||
#ifndef IGUARD_cutscenes_cutscene_h
|
||||
#define IGUARD_cutscenes_cutscene_h
|
||||
|
||||
#include "taisei.h"
|
||||
|
||||
#include "eventloop/eventloop.h"
|
||||
|
||||
typedef enum CutsceneID {
|
||||
// NOTE: These IDs are used for progress tracking, do not reorder!
|
||||
// Only ever append to the end of this enum.
|
||||
// Removed cutscenes must be replaced with a stub.
|
||||
|
||||
CUTSCENE_ID_INTRO = 0,
|
||||
CUTSCENE_ID_REIMU_BAD_END = 1,
|
||||
CUTSCENE_ID_REIMU_GOOD_END = 2,
|
||||
CUTSCENE_ID_REIMU_EXTRA_INTRO = 3,
|
||||
CUTSCENE_ID_MARISA_BAD_END = 4,
|
||||
CUTSCENE_ID_MARISA_GOOD_END = 5,
|
||||
CUTSCENE_ID_MARISA_EXTRA_INTRO = 6,
|
||||
CUTSCENE_ID_YOUMU_BAD_END = 7,
|
||||
CUTSCENE_ID_YOUMU_GOOD_END = 8,
|
||||
CUTSCENE_ID_YOUMU_EXTRA_INTRO = 9,
|
||||
|
||||
NUM_CUTSCENE_IDS,
|
||||
} CutsceneID;
|
||||
|
||||
void cutscene_enter(CallChain next, CutsceneID id);
|
||||
const char *cutscene_get_name(CutsceneID id);
|
||||
|
||||
#endif // IGUARD_cutscenes_cutscene_h
|
85
src/cutscenes/intro.inc.h
Normal file
85
src/cutscenes/intro.inc.h
Normal file
|
@ -0,0 +1,85 @@
|
|||
|
||||
.name = "Introduction",
|
||||
.bgm = "intro",
|
||||
|
||||
.phases = (CutscenePhase[]) {
|
||||
{ "cutscenes/locations/hakurei", {
|
||||
T_NARRATOR("— The Hakurei Shrine\n"),
|
||||
T_NARRATOR("The shrine at the border of fantasy and reality.\n"),
|
||||
T_NARRATOR("Three heroines of Gensōkyō sat drinking tea on a calm day at the end of summer.\n"),
|
||||
T_NARRATOR("Two of them seemed unsettled, while the third—"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/01", {
|
||||
T_MARISA("What’s got ya both so down? Did I mess up the tea?"),
|
||||
T_NARRATOR("\nReimu and Yōmu shared a knowing glance.\n"),
|
||||
T_REIMU("The tea’s fine. I just feel weird. I can’t put my finger on it."),
|
||||
T_YOUMU("I’m feeling something similar, I’m afraid."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/01", {
|
||||
T_NARRATOR("The bells of the shrine tolled outside. Someone had made a donation.\n"),
|
||||
T_NARRATOR("A minute later, there was another.\n"),
|
||||
T_NARRATOR("And another."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/01", {
|
||||
T_REIMU("Something’s definitely not right…"),
|
||||
T_YOUMU("Isn’t it normally comforting for the shrine to receive donations?"),
|
||||
T_REIMU("Normally it *would* be comforting, but…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_bad/01", {
|
||||
T_NARRATOR("Outside the shrine, nearly every resident of Yōkai Mountain were lined up one after another to make a donation - including the Moriya Shrine Gods, Kanako and Suwako.\n"),
|
||||
T_NARRATOR("Everyone seemed vaguely fearful."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/02", {
|
||||
T_REIMU("Eh? What’s all this, then?"),
|
||||
T_KANAKO("We’ve come to ask a favour."),
|
||||
T_MARISA("What kinda favour?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/02", {
|
||||
T_NARRATOR("A mysterious force was slowly causing yōkai and humans alike to succumb to an intense feeling of “knowing everything.”\n"),
|
||||
T_NARRATOR("It would start with figuring out simple problems that they’d been struggling with, such as a technical issue with a machine or perhaps a eye-catching newspaper headline.\n"),
|
||||
T_NARRATOR("But as it progressed, they’d become completely enveloped by it, unable to act rationally.\n"),
|
||||
T_NARRATOR("Yōkai Mountain was usually protected by the Moriya shrine maiden, Sanae Kochiya, but even she had been carried away into madness."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/02", {
|
||||
T_SUWAKO("Sanae just keeps talking about ‘video game consoles.’"),
|
||||
T_MARISA("Eh? Like those toys you hook up to TVs, the kind you find at Kōrindō?"),
|
||||
T_KANAKO("Right, but she just won’t stop. I’ve seen her excited, but not like this."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/03", {
|
||||
T_SUMIREKO("Hmm. Quite fascinating."),
|
||||
T_REIMU("Where the heck did *you* come from?!"),
|
||||
T_SUMIREKO("I couldn’t help overhearing your conversation. Based on my gut feeling, and what you’ve described, the answer is obvious."),
|
||||
T_YOUMU("Is that so? Care to share it with us?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/03", {
|
||||
T_SUMIREKO("Some entity on Yōkai Mountain is emitting a powerful eldritch lunacy, likely in all directions from a singular point, given how indiscriminate it is."),
|
||||
T_SUMIREKO("Too much knowledge all at once… mm, mm, it can potentially be fatal, at least for those with weaker minds."),
|
||||
T_MARISA("Heh, wanna handle it then, Sumi?"),
|
||||
T_SUMIREKO("A-ah, n-no, you all know the lay of the land better, a-after all, and with it being such a dire situation—"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/opening/04", {
|
||||
T_NARRATOR("Yōmu became distracted at the sight of Lady Yuyuko peeking out from behind the Moriya Gods.\n"),
|
||||
T_YOUMU("Lady Yuyuko, why are you—?"),
|
||||
T_YUYUKO("The spirits seem drawn to Yōkai Mountain as well, as if they long for something. It’s growing difficult to placate them all…"),
|
||||
T_YOUMU("I see. Shall I investigate as well?"),
|
||||
T_YUYUKO("Please do."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/locations/hakurei", {
|
||||
T_NARRATOR("Elsewhere at the shrine, the kappa were all huddled into a group, talking intensely in hushed tones…\n"),
|
||||
T_NARRATOR("And the tengu were furiously scribbling on their notepads, as if they’d just gotten the scoop of their careers.\n"),
|
||||
T_NARRATOR("Even here, away from Yōkai Mountain, the effects of the so-called ‘eldritch lunacy’ seemed to be reaching everyone…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ NULL }
|
||||
}
|
57
src/cutscenes/marisa_bad_end.inc.h
Normal file
57
src/cutscenes/marisa_bad_end.inc.h
Normal file
|
@ -0,0 +1,57 @@
|
|||
|
||||
.name = "Marisa (Bad Ending)",
|
||||
.bgm = "ending",
|
||||
|
||||
.phases = (CutscenePhase[]) {
|
||||
{ "cutscenes/locations/sdm", {
|
||||
T_NARRATOR("— The Scarlet Devil Mansion\n"),
|
||||
T_NARRATOR("A peculiar western mansion in an eastern wonderland.\n"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_bad/01", {
|
||||
T_NARRATOR("A nervous witch sat flipping through endless stacks of books, drinking tea instead of her usual sake…\n"),
|
||||
T_MARISA("Hey Patchy, do ya got any more books on Magitech?"),
|
||||
T_PATCHOULI("Yes, but I doubt they’ll be of much use. That machine is beyond that subject entirely."),
|
||||
T_MARISA("Ugh, yer probably right, as usual."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_bad/01", {
|
||||
T_NARRATOR("Marisa closed the book and let it slam onto the floor."),
|
||||
T_PATCHOULI("Mind your manners! That’s a very old tome!"),
|
||||
T_MARISA("It’s so irritatin’! I thought I solved everything but now that Tower’s just sittin’ there, ‘n the whole Mountain’s been quarantined…!"),
|
||||
T_MARISA("It just ain’t satisfyin’, y’know?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_bad/01", {
|
||||
T_FLANDRE("I could destroy it! Make it go BOOM!"),
|
||||
T_MARISA("Gah! … now what did I say about sneakin’ up on people, Flan?"),
|
||||
T_FLANDRE("Sorry!"),
|
||||
T_PATCHOULI("Thank you for the offer, Flandre, but no."),
|
||||
T_FLANDRE("Awww… how boring…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_bad/02", {
|
||||
T_NARRATOR("After having tea with the pair of them, Marisa wandered the stacks, looking for inspiration.\n"),
|
||||
T_NARRATOR("Suddenly, she felt as if she'd stepped into a pothole, tripping and falling flat on her face.\n"),
|
||||
T_NARRATOR("When she looked back at the floor, she didn’t see anything except the expertly-organized rows of immaculate bookshelves.\n"),
|
||||
T_NARRATOR("Then, she noticed something cold and thin underneath her hand."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_bad/02", {
|
||||
T_NARRATOR("After some time, she realized it was a ‘Smart Device’ of some kind, like a handheld computer.\n"),
|
||||
T_NARRATOR("This one was far more advanced than any she'd seen from the Outside World, including that young occultist’s phone.\n"),
|
||||
T_NARRATOR("It behaved both as a solid and a liquid, able to rapidly change shapes by applying pressure to certain sides, into different form factors: a phone, a book, a digital typewriter with a keyboard…\n"),
|
||||
T_NARRATOR("Once Marisa worked out how to make it display something, the title of a book appeared on its dim screen…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_bad/02", {
|
||||
T_NARRATOR("Practical & Advanced Computational Applications of the Grand Unified Theory\n\n"),
|
||||
T_NARRATOR("— by Usami Renko"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_bad/02", {
|
||||
T_CENTERED("— Bad End —", "Try to reach the end without using a Continue!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ NULL }
|
||||
}
|
97
src/cutscenes/marisa_extra_intro.inc.h
Normal file
97
src/cutscenes/marisa_extra_intro.inc.h
Normal file
|
@ -0,0 +1,97 @@
|
|||
|
||||
.name = "Marisa (Extra Stage Intro)",
|
||||
.bgm = "intro",
|
||||
|
||||
.phases = (CutscenePhase[]) {
|
||||
{ "cutscenes/locations/sdm", {
|
||||
T_NARRATOR("— The Scarlet Devil Mansion\n"),
|
||||
T_NARRATOR("A haunted western mansion in an eastern wonderland."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/01", {
|
||||
T_PATCHOULI("I get such a nervous feeling around the newcomers…"),
|
||||
T_MARISA("They're fine, they're fine! Look at ‘er, isn’t she kinda cute?"),
|
||||
T_PATCHOULI("Sure, but… you said you knew them, from another world?"),
|
||||
T_MARISA("Well, another me knew another them in another world."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/01", {
|
||||
T_MARISA("Actually, I kinda saw every possible iteration of myself, spannin’ out over infinity, folding onto ourselves, kinda like a really incredible katana gettin’ forged. It was intense!"),
|
||||
T_PATCHOULI("It’s just like you to accidentally see the Time Knife…"),
|
||||
T_MARISA("The what-now?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/01", {
|
||||
T_NARRATOR("Marisa had just finished accompanying Kurumi to do some ‘service’ on the Tower of Babel.\n"),
|
||||
T_NARRATOR("Kurumi and Remilia, the mistress of the mansion, were loudly gossiping amongst themselves off to the side of the library, laughing cheerfully over… tea?\n"),
|
||||
T_NARRATOR("Marisa hoped it was tea, at least."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/01", {
|
||||
T_REMILIA("Ah, it’s so nice to have a fresh face around here!"),
|
||||
T_KURUMI("Yeah! It was so boring being around that egghead pseudo-gami all the time."),
|
||||
T_REMILIA("And my younger sister is a tad too reclusive to spend much time with me, these days…"),
|
||||
T_REMILIA("Perhaps we could go out on the town sometime soon?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/01", {
|
||||
T_KURUMI("Sure! Know any good spots? For, you know…"),
|
||||
T_REMILIA("Oh yes, a few, but you must keep it down for now…"),
|
||||
T_MARISA("Oi, I’m right here! No conspiring to eat humans, got it?!"),
|
||||
T_REMILIA("It’s perfectly consensual, dear Ms. Kirisame, I assure you."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/01", {
|
||||
T_REMILIA("Forgive the young witch, she’s such a spoil-sport at times."),
|
||||
T_KURUMI("Mm, indeed~."),
|
||||
T_MARISA("Ugh, and after I said y’all were cool…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/01", {
|
||||
T_NARRATOR("It was then that a low rumble echoed through the chambers of the mansion. Slowly at first, but then gaining in intensity."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/02", {
|
||||
T_MARISA("Huh? What’s that?"),
|
||||
T_PATCHOULI("Perhaps the fairy maids are remodeling upstairs?"),
|
||||
T_NARRATOR("\nThe mansion began to shake intense, as if down to its very foundation, before ceasing entirely, as quickly as it started.\n"),
|
||||
T_NARRATOR("Before the dust had a chance to settle, an eerie, long siren began blaring in the distance outside the mansion."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/02", {
|
||||
T_MARISA("Huh?! That’s one of the sirens I had the kappa set up!"),
|
||||
T_PATCHOULI("Sirens?"),
|
||||
T_MARISA("Yeah, to monitor the Tower of Babel in case anything changed."),
|
||||
T_MARISA("Ah heck, and I guess that means…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/02", {
|
||||
T_KURUMI("Y-you saw me back there, right?! You were there! I didn’t do anything!"),
|
||||
T_MARISA("Hey, nobody's accusin’ ya of anythin’. All ya did was shut a door, right? To keep the fairies out?"),
|
||||
T_KURUMI("Y-yeah! But… but maybe it’s like that phrase? ‘One door closes, another opens'?"),
|
||||
T_MARISA("What do ya mean?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/02", {
|
||||
T_NARRATOR("Kurumi got rather pale, even for a vampire."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/02", {
|
||||
T_KURUMI("Oh no…"),
|
||||
T_MARISA("Well c’mon, spit it out. ‘s not like we’ll hold it against ya if ya messed up. We’ll just go back, get the kappa, and—"),
|
||||
T_KURUMI("Hey, um, uh, maybe you should check it out?"),
|
||||
T_KURUMI("Like, alone? Because I'd just get in your way?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/02", {
|
||||
T_KURUMI("But, uh, you know, there might be a really powerful enemy there now? So please be careful???"),
|
||||
T_MARISA("Aw, are ya worried about me? That’s cute."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_extra/02", {
|
||||
T_NARRATOR("Marisa grinned and magically summoned her broom, mounting it deftly, and shooting right out of an open window in a flash.\n"),
|
||||
T_NARRATOR("Her sights were set on the Tower that loomed and rumbled on the top of Yōkai Mountain…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ NULL }
|
||||
}
|
102
src/cutscenes/marisa_good_end.inc.h
Normal file
102
src/cutscenes/marisa_good_end.inc.h
Normal file
|
@ -0,0 +1,102 @@
|
|||
|
||||
.name = "Marisa (Good Ending)",
|
||||
.bgm = "ending",
|
||||
|
||||
.phases = (CutscenePhase[]) {
|
||||
{ "cutscenes/locations/moriya", {
|
||||
T_NARRATOR("— The Moriya Shrine\n"),
|
||||
T_NARRATOR("A workaholic shrine at the top of of Yōkai Mountain."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/locations/moriya", {
|
||||
T_NARRATOR("Once defeated, the newcomers agreed to turn off their ‘Tower of Babel’.\n"),
|
||||
T_NARRATOR("Elly and Kurumi were made to scrub the floors of the Moriya Shrine…\n"),
|
||||
T_NARRATOR("… which had become filthy from the local fairies partying non-stop, and some ‘political demonstrations’ by the newly-formed ‘Insect Party’."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/locations/moriya", {
|
||||
T_SANAE("—which had a brand new math co-processor, allowing it to have advanced vector processing capabilities. Combined with other improvements, such as its DVD drive—"),
|
||||
T_ELLY("Ah, I see. Facinating. Wow. Incredible."),
|
||||
T_KANAKO("You two having fun?"),
|
||||
T_ELLY("… y-yes, ma’am."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/locations/moriya", {
|
||||
T_KANAKO("Seems like it’s taking a bit more time for the madness to wear off for her. How’s all that ‘knowledge’ working out for you, newcomer?"),
|
||||
T_SANAE("I remembered way more about retro video game consoles than I wanted to! And now *you’ve* gotta deal with it!"),
|
||||
T_NARRATOR("\nElly nodded and smiled politely, trying to hold back tears of boredom."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/locations/moriya", {
|
||||
T_NARRATOR("In the end, the newcomers seemed to calm down once the effects of the Tower had worn off, just like everyone else in Gensōkyō.\n"),
|
||||
T_NARRATOR("During an early afternoon, Marisa arrived with a picnic basket and a grin on her face."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/01", {
|
||||
T_ELLY("Ah, Ms. Kirisame. Hello again."),
|
||||
T_MARISA("Yo, Elly!"),
|
||||
T_ELLY("Do you… need something from me?"),
|
||||
T_MARISA("Heck yeah I do! A drinkin’ partner!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/01", {
|
||||
T_ELLY("Isn’t it a little early for that?"),
|
||||
T_MARISA("Oh c’mon, it’s noon somewhere in the infinite multiverse, ain’t it?"),
|
||||
T_NARRATOR("\nElly laughed. She wanted an excuse for a break anyways.\n"),
|
||||
T_NARRATOR("And so, they drank, and caught up on alternate worlds, uncanny timelines, and the strangeness of their circumstances.\n"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/02", {
|
||||
T_MARISA("I mean, ‘course ya lost. Mathematics has always been linked with magic. Don’t ya know about Issac Newton?"),
|
||||
T_ELLY("The famous European mathematician? I studied his works, yes. But what about him?"),
|
||||
T_MARISA("He was an Alchemist!"),
|
||||
T_ELLY("He was a… what?!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/02", {
|
||||
T_MARISA("Yup. There’s even a copy of his occult book down at the library I frequent."),
|
||||
T_MARISA("He was into a lotta wild stuff."),
|
||||
T_MARISA("Kinda funny ya hadn’t heard of that, actually."),
|
||||
T_ELLY("Hah. That… *is* kind of funny, actually. I never ran across any of *those* books of his…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/02", {
|
||||
T_MARISA("Oh, cheer up, will ya? Y’all’ll get to chill out here for a long as ya like."),
|
||||
T_ELLY("But what if this Gensōkyō collapses, too…?"),
|
||||
T_MARISA("It could! The Hakurei Barrier could fall tomorrow! But what’s the point in worryin’ about all that?"),
|
||||
T_MARISA("Right now, I say we enjoy what we got!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/02", {
|
||||
T_ELLY("I suppose you’re right."),
|
||||
T_ELLY("Although, I have to ask… how did you remember me?"),
|
||||
T_MARISA("Oh, that’s easy. Mushrooms!"),
|
||||
T_ELLY("Mush… rooms? As in, mycelium?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/02", {
|
||||
T_MARISA("Yeah! A while ago, I was cookin’ up some especially powerful ones I found in a cave near the one-armed hermit's house…"),
|
||||
T_MARISA("And then, BOOM, there was a small explosion!"),
|
||||
T_MARISA("Before I knew it was seein’ all sorts’a ‘versions’ of myself out there. My lives were flashin’ before my eyes!"),
|
||||
T_MARISA("Anyway, took me a bit, but I kinda remembered that vampire girl Kurumi, and when I saw ya, and I was like, ‘Whoa, really?! For real?!’"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/02", {
|
||||
T_ELLY("That’s ridiculous. You’re ridiculous."),
|
||||
T_ELLY("But I suppose in a place like Gensōkyō, that's not even that strange…"),
|
||||
T_ELLY("… and you were such a child when I met you. It’s odd to see you all grown up, even if it’s another you entirely."),
|
||||
T_MARISA("I’ll have you know that I’m not a day wiser! Gahaha!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/02", {
|
||||
T_NARRATOR("Many residents of Gensokyo wondered if the Tower would be a blight on the skyline for all eternity.\n"),
|
||||
T_NARRATOR("Others questioned if its power could be harnessed to serve the greater good.\n"),
|
||||
T_NARRATOR("But for the guardians of Gensōkyō, they would need to be vigilant, lest it suddenly spring to life again…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/marisa_good/02", {
|
||||
T_CENTERED("— Good End —", "Extra Stage unlocked!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ NULL }
|
||||
}
|
4
src/cutscenes/meson.build
Normal file
4
src/cutscenes/meson.build
Normal file
|
@ -0,0 +1,4 @@
|
|||
cutscenes_src = files(
|
||||
'cutscene.c',
|
||||
'scenes.c',
|
||||
)
|
62
src/cutscenes/reimu_bad_end.inc.h
Normal file
62
src/cutscenes/reimu_bad_end.inc.h
Normal file
|
@ -0,0 +1,62 @@
|
|||
|
||||
.name = "Reimu (Bad Ending)",
|
||||
.bgm = "ending",
|
||||
|
||||
.phases = (CutscenePhase[]) {
|
||||
{ "cutscenes/locations/hakurei", {
|
||||
T_NARRATOR("— The Hakurei Shrine\n"),
|
||||
T_NARRATOR("The shrine at the border of fantasy and reality.\n"),
|
||||
T_NARRATOR("The Tower had powered down, yet the instigators were nowhere to be found.\n"),
|
||||
T_NARRATOR("Reimu returned to her shrine, unsure if she had done anything at all.\n"),
|
||||
T_NARRATOR("The Gods and yōkai of the Mountain shuffled about the shrine quietly, with bated breath.\n"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_bad/01", {
|
||||
T_NARRATOR("Reimu sighed, and made her announcement.\n"),
|
||||
T_REIMU("Listen up! I stopped the spread of madness, but until further notice, Yōkai Mountain is off limits!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_bad/01", {
|
||||
T_NARRATOR("Nobody was pleased with that.\n"),
|
||||
T_NARRATOR("The Moriya Gods were visibly shocked.\n"),
|
||||
T_NARRATOR("The kappa demanded that they be able to fetch their equipment, and tend to their hydroponic cucumber farms.\n"),
|
||||
T_NARRATOR("The tengu furiously scribbled down notes, once again as if they’d had the scoop of the century, before also beginning to make their own demands.\n"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/location/hakurei", {
|
||||
T_NARRATOR("Once Reimu had managed to placate the crowd, she sat in the back of the Hakurei Shrine, bottle of sake in hand.\n"),
|
||||
T_NARRATOR("She didn’t feel like drinking, however. She nursed it without even uncorking it, sighing to herself."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_bad/02", {
|
||||
T_YUKARI("Oh my, a little dissatisfied, aren’t we?"),
|
||||
T_NARRATOR("\nReimu was never surprised by Yukari’s sudden appearances anymore, of course.\n"),
|
||||
T_YUKARI("It’s unlike you to stop until everything is business as usual, Reimu."),
|
||||
T_YUKARI("Depressed?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_bad/02", {
|
||||
T_REIMU("Yeah. That place sucked."),
|
||||
T_YUKARI("Perhaps I should’ve gone with you, hmm?"),
|
||||
T_REIMU("Huh? Why? Do you wanna gap the whole thing out of Gensōkyō?"),
|
||||
T_YUKARI("That might be a bit much, even for me, dear."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_bad/02", {
|
||||
T_REIMU("Whatever that ‘madness’ thing was, it was getting to me, too. It made me feel frustrated and… lonely?"),
|
||||
T_REIMU("But why would I feel lonely? It doesn’t make any sense."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_bad/02", {
|
||||
T_YUKARI("So, what will you do?"),
|
||||
T_REIMU("Find a way to get rid of it, eventually. I’m sure there’s an answer somewhere out there…"),
|
||||
T_NARRATOR("\nYukari smiled.\n"),
|
||||
T_YUKARI("Glad to hear it. It’s your duty, after all."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_bad/02", {
|
||||
T_CENTERED("— Bad End —", "Try to reach the end without using a Continue!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ NULL }
|
||||
}
|
103
src/cutscenes/reimu_extra_intro.inc.h
Normal file
103
src/cutscenes/reimu_extra_intro.inc.h
Normal file
|
@ -0,0 +1,103 @@
|
|||
|
||||
.name = "Reimu (Extra Stage Intro)",
|
||||
.bgm = "intro",
|
||||
|
||||
.phases = (CutscenePhase[]) {
|
||||
{ "cutscenes/locations/moriya", {
|
||||
T_NARRATOR("— The Moriya Shrine"),
|
||||
T_NARRATOR("A workaholic shrine at the top of Yōkai Mountain."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/locations/moriya", {
|
||||
T_NARRATOR("Reimu was reading a book she'd borrowed from the Human Village’s book rental store, Suzunaan."),
|
||||
T_NARRATOR("It was her first ‘science fiction’ series, apparently about a flat, circular world that floated through space on the backs of four turtles…"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/01", {
|
||||
T_NARRATOR("Suddenly, Yukari slid in through one of her gaps."),
|
||||
T_NARRATOR("As usual, Reimu could feel her presence right away.\n"),
|
||||
T_NARRATOR("Unexpectedly, Ran and Chen were also in tow, taking positions behind her with solemn, silent expressions."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/01", {
|
||||
T_YUKARI("My oh my, passing the time at rival shrines, are we?"),
|
||||
T_REIMU("Guard duty."),
|
||||
T_YUKARI("Hm? Not trusting enough of Ms. Kochiya to—"),
|
||||
T_REIMU("No."),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/01", {
|
||||
T_YUKARI("*giggle* I see."),
|
||||
T_NARRATOR("\nAs if on cue, Elly came running up the steps of the Moriya Shrine, breathlessly, clad in a Moriya shrine maiden uniform.\n"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/02", {
|
||||
T_ELLY("Ms. Hakurei! There’s been- Oh, my apologies, we have a guest. How nice to meet—"),
|
||||
T_ELLY("Wait, no, there’s no time for that!"),
|
||||
T_REIMU("Slow down. What’s wrong?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/02", {
|
||||
T_NARRATOR("Elly brought out some kind of small communicator device. It projected an image into the air, like an illusion.\n"),
|
||||
T_ELLY("It’s the Tower! It’s spinning back up!"),
|
||||
T_REIMU("Spinning? It looks completely stationary from here."),
|
||||
T_ELLY("No! I mean it’s powering up, charging up! Whatever! It’s turning back on!!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/02", {
|
||||
T_REIMU("Huh? It thought you were in control of it."),
|
||||
T_ELLY("I am! I was! I should be…! But this device isn’t letting me do anything!"),
|
||||
T_ELLY("And I thought about going back to the Tower to turn it off again, but I-… no, i-it’s not safe to."),
|
||||
T_REIMU("Eh? Why not?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/02", {
|
||||
T_KURUMI("Hm? What’s all this, then?"),
|
||||
T_ELLY("Kurumi, the Tower's turning back on!"),
|
||||
T_KURUMI("Huh?! But all I did was seal off the mansion's main doors, so the fairies would leave it alone, just like you asked!"),
|
||||
T_ELLY("I… what?!"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/02", {
|
||||
T_KURUMI("Y-you left me a note that said, ‘Kurumi, go close off the mansion module so the fairies stop playing inside of it'!"),
|
||||
T_KURUMI("So I did!!"),
|
||||
T_ELLY("I didn’t leave you any such note! This isn’t even my handwriting! You *know* I have a hard time with stroke order—"),
|
||||
T_REIMU("Okay, excuse me, but what does this have to do with anything?"),
|
||||
{ 0 },
|
||||
}},
|
||||
{ "cutscenes/reimu_extra/02", {
|
||||
T_ELLY("I… I may have stolen it from someone."),
|
||||