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:
Andrei Alexeyev 2020-11-28 12:11:10 +02:00 committed by GitHub
parent b257f665f6
commit 9b5d515721
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 1871 additions and 592 deletions

Binary file not shown.

View 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);
}

View file

@ -0,0 +1,2 @@
objects = cutscene.frag standard.vert

View file

@ -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',

View 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;
}

View file

@ -0,0 +1,2 @@
objects = text_dialog.vert text_cutscene.frag

View file

@ -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);

View file

@ -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
View 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
View 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
View 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("Whats 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 teas fine. I just feel weird. I cant put my finger on it."),
T_YOUMU("Im feeling something similar, Im 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("Somethings definitely not right…"),
T_YOUMU("Isnt 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? Whats all this, then?"),
T_KANAKO("Weve 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 theyd 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, theyd 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 wont stop. Ive 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 couldnt help overhearing your conversation. Based on my gut feeling, and what youve 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. Its 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 theyd 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 }
}

View 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 theyll 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! Thats a very old tome!"),
T_MARISA("Its so irritatin! I thought I solved everything but now that Towers just sittin there, n the whole Mountains been quarantined…!"),
T_MARISA("It just aint satisfyin, yknow?"),
{ 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 didnt 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 occultists 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 }
}

View 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, isnt 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("Its 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, its 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, Im right here! No conspiring to eat humans, got it?!"),
T_REMILIA("Its perfectly consensual, dear Ms. Kirisame, I assure you."),
{ 0 },
}},
{ "cutscenes/marisa_extra/01", {
T_REMILIA("Forgive the young witch, shes such a spoil-sport at times."),
T_KURUMI("Mm, indeed~."),
T_MARISA("Ugh, and after I said yall 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? Whats 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?! Thats 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 didnt 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 its 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 cmon, spit it out. s not like well hold it against ya if ya messed up. Well 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? Thats 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 }
}

View 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, maam."),
{ 0 },
}},
{ "cutscenes/locations/moriya", {
T_KANAKO("Seems like its taking a bit more time for the madness to wear off for her. Hows 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 *youve* 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("Isnt it a little early for that?"),
T_MARISA("Oh cmon, its noon somewhere in the infinite multiverse, aint 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. Dont 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. Theres 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 hadnt 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? Yallll 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 whats 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 youre right."),
T_ELLY("Although, I have to ask… how did you remember me?"),
T_MARISA("Oh, thats 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 sortsa 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("Thats ridiculous. Youre 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. Its odd to see you all grown up, even if its another you entirely."),
T_MARISA("Ill have you know that Im 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 }
}

View file

@ -0,0 +1,4 @@
cutscenes_src = files(
'cutscene.c',
'scenes.c',
)

View 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 theyd 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 didnt 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, arent we?"),
T_NARRATOR("\nReimu was never surprised by Yukaris sudden appearances anymore, of course.\n"),
T_YUKARI("Its 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 shouldve 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 doesnt 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. Im sure theres an answer somewhere out there…"),
T_NARRATOR("\nYukari smiled.\n"),
T_YUKARI("Glad to hear it. Its your duty, after all."),
{ 0 },
}},
{ "cutscenes/reimu_bad/02", {
T_CENTERED("— Bad End —", "Try to reach the end without using a Continue!"),
{ 0 },
}},
{ NULL }
}

View 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 Villages 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! Theres been- Oh, my apologies, we have a guest. How nice to meet—"),
T_ELLY("Wait, no, theres no time for that!"),
T_REIMU("Slow down. Whats 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("Its the Tower! Its spinning back up!"),
T_REIMU("Spinning? It looks completely stationary from here."),
T_ELLY("No! I mean its powering up, charging up! Whatever! Its 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 isnt letting me do anything!"),
T_ELLY("And I thought about going back to the Tower to turn it off again, but I-… no, i-its not safe to."),
T_REIMU("Eh? Why not?"),
{ 0 },
}},
{ "cutscenes/reimu_extra/02", {
T_KURUMI("Hm? Whats 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 didnt leave you any such note! This isnt 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."),