Add Reimu Hakurei as a playable character (#106)

* Reimu (#101)

* add the reimu

* Add Reimu story

* account for various blunders

* add reimu dialog picture

* Reimu: WIP yin-yang orbs

* reimu: fix up indents

* Reimu: swap the shotmode names to match the kanji order in her Japanese name

* Reimu: compatibility with the latest system

* WIP ReimuA crap

* ReimuA homing trails

* more ReimuA stuff

* more ReimuA adjustments + enhanced DPS stats

* Reimu: stubs for new player animation sequences

* Reimu: occupy the 0th character slot

* Reimu: tweak needle sprite

* Reimu: buff movement speed to better match Touhou

* Reimu: fixup for the recent projectile changes

* ReimuA: make homing shots a bit smaller; give them custom effect on collision

* Reimu: add intermediate frames; move some loose sprites to the atlas

* Reimu: fix compile errors

* replace DBL_MAX by INFINITY

* Don’t draw reimu orbs twice

fixes #127

* add new reimu dialog pic

* ReimuA adjustments (mostly homing); it's still OP

* wip ReimuB gaps

* still not sure where i'm going with these gaps

* meh

* Reimu: premultiplied alpha fixups after rebase

* reimuB shot pattern with basic power scaling (not balanced at all)

* reimuB: some lame-ass particle effects

* ReimuB bomb effect prototype

* reimuA bomb prototype

* fix reimu shots for the new damage system

* Reimu: use the new player_is_bomb_active() function, add placeholder BG for ReimuB

* some reimuB bomb projectiles

* ReimuB bomb bg and some framebuffer utils required to support it.

Might reuse this at least in part for ReimuA unless we come up with
something better.

* hack to fix ReimuB bomb fade; refactoring needed

* reimuA damaging bombs

* fix ub

* prevent nan when reimuA bombs without enemies present

* add a bomb_bg to reimuA

* ...

* various fantasy seal tweaks

* Reimu: placeholder bomb sounds; slight fantasy seal buff

* fix null pointer dereference

* Reimu "balance" adjustments; minor fixes

* putting bandaids over gunshot wounds

* Add aoe damage and bullet cancel to ReimuB's bomb

* more exorcism porn

* make reimu bomb bg runes better visible on dark backgrounds

* More ReimuA shot changes
This commit is contained in:
Andrei Alexeyev 2018-08-11 22:13:48 +03:00 committed by GitHub
parent 157a59dbbd
commit 3615b95f13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 2267 additions and 86 deletions

View file

@ -8,6 +8,7 @@ atlases = [
'common_ui',
'huge',
'portraits',
'reimu',
]
atlas_common_args = [

View file

@ -0,0 +1,3 @@
w = 128
h = 128

View file

@ -0,0 +1,4 @@
w = 20
h = 30

View file

@ -1,6 +1,3 @@
#w = 76
#h = 95
w = 48
h = 60

View file

@ -0,0 +1,3 @@
w = 54
h = 66

View file

@ -0,0 +1,4 @@
w = 16
h = 39

View file

@ -0,0 +1,4 @@
w = 14
h = 72

View file

@ -0,0 +1,3 @@
w = 7
h = 65

View file

@ -0,0 +1,4 @@
w = 16
h = 26

View file

@ -0,0 +1,3 @@
w = 24
h = 24

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
atlas/reimu/proj/needle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
atlas/reimu/proj/ofuda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
atlas/reimu/yinyang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -58,6 +58,67 @@
================================================================================
○ Hakurei Reimu
For Reimu, today should have been a peaceful day. It was spring, so early
mornings were still chilly and flowers were just beginning to burst into
bright blooms around the shrine. But unfortunately, Reimu did not get to
enjoy her luxury of sleeping in as she was rudely awoken by insistent
prodding into her cheek.
“Mmmh?”
“Wake up, Reimu. There is a serious incident unraveling itself as we speak!”
Although annoyed at being woken up in her own shrine, she nevertheless
cracked an unamused eye open to stare at her beauty sleeps disturbance. The
voice was familiar, and so was the tall, unsettling woman peering back at
her from her own personal crack of distorted reality.
“What now, Yukari? Cant you see I am doing the important duty of self-care
as a shrine maiden?”
“You should think about switching duties then,” Yukari quipped back,
reaching over to outright pinch Reimus cheek. “The barrier is being
distorted by someone with a great deal of unexpected power, and it is of a
sort that I cannot manipulate.”
“So, you cant fix it, then?”
“Of course not. Its too… logical. Even though I created this barrier with
mathematics in mind, my parameters were fictional. Its a power grounded in
reality, and it seems to have outright torn open a hole in the barrier up on
Yōkai Mountain, creating a tunnel to somewhere else entirely.”
Now fully awake, Reimu sat up and batted Yukaris intrusive hands away. But
even though she sincerely wanted to ignore the yōkai sage that plagued her
life as a constant irritation, she too could feel something off in the air.
The Hakurei Shrine sat at the very edge of Gensōkyō, and it held the barrier
steady as a medium between both sides of the boundary. Items from the
Outside World commonly ended up strewn around the grounds, and so did the
occasional high school girl as well. Despite the unusual atmosphere of the
shrine, Reimu immediately knew just by glancing around that it was not
normal for her shrines walls to look patchy and moldy in places, or for the
floor to have random cracks and stains not present from everyday use.
“You can see it, can you not? The Hakurei Shrine from the Outside World is
merging with ours as our fantasy disappears. Soon it will replace yours
entirely, and you will live in the broken down version of your beloved
home.”
That was all it took to get Reimu in action as she slipped into her uniform
and grabbed her Yin-Yang Orbs and purification rod. Although the weakening
of the Hakurei Barrier was worrying enough on its own, there was no way she
could allow her shrine to turn into the run-down mess it was on the other
side of the border. Whoever was behind this needed to stop, for her sake and
everyone elses. And if nothing else, the shrine maiden was loath to become
homeless. That simply would not do.
“Yukari, youll take me there, wont you?”
Unfortunately for the shrine maiden, she was met with silence. It seemed as
though she would have to take the long route to Yōkai Mountain.
○ Kirisame Marisa
Early one spring day, just after dawn had passed, Marisa was finding a

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
resources/gfx/gaplight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

View file

@ -0,0 +1,2 @@
wrap_t = mirror
wrap_s = clamp

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1
region_y = 1
region_w = 256
region_h = 256
# -- Pasted from the override file --
w = 128
h = 128

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1731
region_y = 1
region_w = 80
region_h = 120
# -- Pasted from the override file --
w = 20
h = 30

View file

@ -0,0 +1,12 @@
@sprite_count = 11
main = d10 4 5 6 7
left = m d10 0 1 2 3
right = d10 0 1 2 3
main2left = m d2 8 9 10
main2right = d2 8 9 10
left2main = m d2 10 9
left2right = m d2 10 9 m 8 9 10
right2main = d2 10 9
right2left = d2 10 9 m 8 9 10

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 411
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 521
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 631
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 741
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 851
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 961
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1071
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1181
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1291
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1401
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1511
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1621
region_y = 1
region_w = 108
region_h = 132
# -- Pasted from the override file --
w = 54
h = 66

View file

@ -0,0 +1,7 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 259
region_y = 1
region_w = 150
region_h = 150

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1813
region_y = 1
region_w = 64
region_h = 156
# -- Pasted from the override file --
w = 16
h = 39

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 2011
region_y = 1
region_w = 28
region_h = 144
# -- Pasted from the override file --
w = 14
h = 72

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 2041
region_y = 1
region_w = 14
region_h = 130
# -- Pasted from the override file --
w = 7
h = 65

View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1879
region_y = 1
region_w = 64
region_h = 104
# -- Pasted from the override file --
w = 16
h = 26

BIN
resources/gfx/runes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

12
resources/gfx/yinyang.spr Normal file
View file

@ -0,0 +1,12 @@
# Autogenerated by the atlas packer, do not modify
texture = atlas_reimu_0
region_x = 1945
region_y = 1
region_w = 64
region_h = 64
# -- Pasted from the override file --
w = 24
h = 24

Binary file not shown.

View file

@ -33,3 +33,4 @@ warp = 80
damage_feedback = 120
hit0 = 120
hit1 = 120
bomb_reimu_a = 128

View file

@ -48,4 +48,33 @@ vec4 color_mul_alpha(vec4 c) {
return vec4(c.rgb * c.a, c.a);
}
mat2 rot(float a) {
return mat2(cos(a), -sin(a), sin(a), cos(a));
}
float line_segment(vec2 p, vec2 a, vec2 b, float thickness) {
vec2 pa = p - a;
vec2 ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return smoothstep(thickness, 0.0, length(pa - ba*h));
}
// Taken from http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
vec3 rgb2hsv(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
#endif

View file

@ -16,11 +16,15 @@ glsl_files = files(
'ingame_menu.frag.glsl',
'laser_generic.vert.glsl',
'marisa_hakkero.frag.glsl',
'marisa_hakkero.vert.glsl',
'marisa_laser.frag.glsl',
'maristar_bombbg.frag.glsl',
'masterspark.frag.glsl',
'max_to_alpha.frag.glsl',
'player_death.frag.glsl',
'reimu_bomb_bg.frag.glsl',
'reimu_gap.frag.glsl',
'reimu_gap_light.frag.glsl',
'spellcard_intro.frag.glsl',
'spellcard_outro.frag.glsl',
'spellcard_walloftext.frag.glsl',
@ -36,6 +40,7 @@ glsl_files = files(
'sprite_negative.frag.glsl',
'sprite_silhouette.frag.glsl',
'sprite_silhouette.vert.glsl',
'sprite_yinyang.frag.glsl',
'sprite_youmu_charged_shot.frag.glsl',
'sprite_youmu_charged_shot.vert.glsl',
'sprite_youmu_myon_shot.frag.glsl',

View file

@ -0,0 +1,122 @@
#version 330 core
#include "interface/standard.glslh"
#include "lib/util.glslh"
#include "lib/render_context.glslh"
UNIFORM(1) vec2 aspect;
UNIFORM(2) float zoom;
UNIFORM(3) float time;
UNIFORM(4) sampler2D runes;
float _split(float x) {
return sqrt(0.25 - pow(x - 0.5, 2));
}
vec2 runes_transform_uv(vec2 uv) {
const float segments = 32;
uv.y = mod(uv.y, 1.0);
uv.y += floor(uv.x);
uv.y /= segments;
return uv;
}
float split(float x) {
/*
if(x <= 0.25) {
return 0.25;
}
if(x >= 0.75) {
return 0.75;
}
*/
if(x < 0.5) {
x = 1.0 - _split(2 * x) - 0.5;
} else {
x = _split(2 * x - 1) + 0.5;
}
return 0.5 * x + 0.25;
}
float yinyang(vec2 uv, out float light_dist, out float dark_dist) {
float s = split(uv.x);
float v;
v = smoothstep(0.0, 0.01, uv.y - s);
light_dist = length((uv - vec2(0.75, 0.5)) * 16.0);
v = mix(v, 1.0, smoothstep(1.0, 0.95, light_dist));
dark_dist = length((uv - vec2(0.25, 0.5)) * 16.0);
v = mix(v, 0.0, smoothstep(1.0, 0.95, dark_dist));
return v;
}
float yinyang_spin(vec2 uv, vec2 aspect, float a, out float light_dist, out float dark_dist) {
mat2 r = rot(a);
uv -= 0.5;
uv *= aspect;
uv *= r;
uv += 0.5;
return yinyang(uv, light_dist, dark_dist);
}
void main(void) {
vec2 uv = texCoord;
float ld, dd;
float yy = yinyang_spin(uv, aspect * zoom, -time, ld, dd);
vec2 uv_r = uv;
vec2 uv_g = uv;
vec2 uv_b = uv;
vec3 c = vec3(0);
float rfactor = sqrt(dd*dd + ld*ld) * 0.01 * yy;
float kek = (1 - 2 * yy);
ld = sqrt(ld);
dd = sqrt(dd);
mat2 m_r = rot(dd * 0.00005 * kek) / (1 + ld * -0.0006 * kek);
mat2 m_g = rot(dd * 0.00009 * kek) / (1 + ld * -0.0004 * kek);
mat2 m_b = rot(dd * 0.00012 * kek) / (1 + ld * -0.0002 * kek);
const int samples = 24;
const float isamples = 1.0 / samples;
for(int i = 0; i < samples; ++i) {
uv_r -= 0.5;
uv_r *= m_r;
uv_r += 0.5;
c.r += texture(tex, uv_r).r;
uv_g -= 0.5;
uv_g *= m_g;
uv_g += 0.5;
c.g += texture(tex, uv_g).g;
uv_b -= 0.5;
uv_b *= m_b;
uv_b += 0.5;
c.b += texture(tex, uv_b).b;
}
fragColor = vec4(c * isamples, 1) * 0.9 * mix(r_color, vec4(r_color.a), 1);
const float rune_lines = 24;
vec2 runes_uv = (uv - 0.5) * rot(pi * 0.5) + 0.5;
float line = floor(runes_uv.y * rune_lines);
runes_uv.x += 0.931223 * line + time * 0.1 * cos(pi * line);
runes_uv.y *= -rune_lines;
runes_uv = runes_transform_uv(runes_uv);
float rune_factor = texture(runes, runes_uv).r;
fragColor.rgb = pow(fragColor.rgb, vec3(1 + (1 - 2 * rune_factor) * 0.1));
// fragColor = mix(fragColor, 0.9 * fragColor + vec4(0.1), rune_factor * r_color);
fragColor += r_color * rune_factor * 0.1;
fragColor.a *= (1 - rune_factor * 0.1);
}

View file

@ -0,0 +1,2 @@
glsl_objects = standard.vert reimu_bomb_bg.frag

View file

@ -0,0 +1,77 @@
#version 330 core
#include "lib/render_context.glslh"
#include "interface/standard.glslh"
#include "lib/util.glslh"
#define NUM_GAPS 4
// #define DRAW_LINKS
UNIFORM(1) vec2 viewport;
UNIFORM(2) float time;
UNIFORM(3) vec2 gap_size;
UNIFORM(4) vec2 gaps[NUM_GAPS];
UNIFORM(8) float gap_angles[NUM_GAPS];
UNIFORM(12) int gap_links[NUM_GAPS];
void main(void) {
vec4 bg = texture(tex, texCoord);
fragColor = bg;
vec2 frag_loc = texCoord * viewport;
frag_loc.y = viewport.y - frag_loc.y;
float t = time * 6;
const float mag = 0.5;
const float pmag = 0.123;
frag_loc.x += mag * sin(t);
frag_loc.y += mag * sin(t * 1.22 - frag_loc.x * pmag);
frag_loc.x += mag * sin(t * -1.36 - frag_loc.y * pmag);
frag_loc.y += mag * sin(t * -1.29 - frag_loc.x * pmag);
frag_loc.x += mag * sin(t * 1.35 - frag_loc.y * pmag);
const float h0 = 1;
const float h1 = h0 * 0.8;
const vec4 gap_color = vec4(0.75, 0, 0.4, 1);
for(int i = 0; i < NUM_GAPS; ++i) {
vec2 gap = gaps[i];
float gap_angle = gap_angles[i];
int link = gap_links[i];
vec2 next_gap = gaps[link];
float next_gap_angle = gap_angles[link];
mat2 gap_rot = rot(gap_angle);
float edge = length(gap_rot * (frag_loc - gap) / gap_size);
float gap_mask = smoothstep(h0, h1, edge);
vec2 tc_inv = 1 - texCoord;
float _ = time * 0.2;//0.5 + 0.5 * sin(time);
vec2 tc = vec2(texCoord.x, 1 - texCoord.y) * viewport;
tc -= gap.xy;
tc *= rot((next_gap_angle - gap_angle));
tc += next_gap.xy;
tc /= viewport;
tc.y = 1 - tc.y;
fragColor = mix(fragColor, mix(gap_color, texture(tex, tc), 1 - pow(edge, 3)), gap_mask);
}
#ifdef DRAW_LINKS
for(int i = 0; i < NUM_GAPS; ++i) {
vec2 gap = gaps[i];
float gap_angle = gap_angles[i];
int link = gap_links[i];
vec2 next_gap = gaps[link];
float next_gap_angle = gap_angles[link];
vec2 l = texCoord * viewport;
l.y = viewport.y - l.y;
fragColor = mix(fragColor, vec4(float(i&1), i/float(NUM_GAPS-1), 1-i/float(NUM_GAPS-1), 1), line_segment(l, gap, next_gap, 1));
}
#endif
}

View file

@ -0,0 +1,2 @@
glsl_objects = standard.vert reimu_gap.frag

View file

@ -0,0 +1,50 @@
#version 330 core
#include "lib/render_context.glslh"
#include "interface/standard.glslh"
#include "lib/util.glslh"
UNIFORM(1) float time;
UNIFORM(2) float strength;
float g(float x) {
return sin(1.1 * x) * cos(1.98 * x);
}
float wave(float x) {
return g(x - 3 * g(0.6 * x) + 0.23 * g(1.2123 * x) + 0.7634 * g(1.12365 * x) + 1.541 * g(0.26 * x));
}
float light_mask(vec2 tc) {
float p = 20.0;
tc.x = mix(
clamp(0.5 - 0.5 * pow(1 - tc.x * 2, p), 0.0, 0.5),
clamp(0.5 + 0.5 * pow((tc.x - 0.5) * 2, p), 0.5, 1.0),
step(0.5, tc.x)
);
return smoothstep(1.0, 0.0, length((tc - 0.5) * 2.0));
}
void main(void) {
float s = 0.6;
float l = 1.0;
float o = 0.123 * texCoord.x;
vec4 layer0 = texture(tex, texCoord + vec2(0, 0.23134 * time)) * vec4(hsv2rgb(vec3(time + (texCoord.y + 1*o), s, l)), 1);
layer0 *= pow(0.5 + 0.5 * wave(3215*pi + 0.9314*texCoord.y - time * 0.46), 1.0);
vec4 layer1 = texture(tex, texCoord + vec2(0, 0.32155 * time)) * vec4(hsv2rgb(vec3(time - (texCoord.y + 2*o), s, l)), 1);
layer1 *= pow(0.5 + 0.5 * wave(7234*pi + 0.9612*texCoord.y + time * 0.64), 1.0);
vec4 layer2 = texture(tex, texCoord - vec2(0, 0.30133 * time)) * vec4(hsv2rgb(vec3(time + (texCoord.y + 3*o), s, l)), 1);
layer2 *= pow(0.5 + 0.5 * wave(4312*pi + 0.9195*texCoord.y + time * 0.42), 1.0);
vec4 layer3 = texture(tex, texCoord - vec2(0, 0.26424 * time)) * vec4(hsv2rgb(vec3(time - (texCoord.y + 4*o), s, l)), 1);
layer3 *= pow(0.5 + 0.5 * wave(2642*pi + 0.9195*texCoord.y + time * 0.60), 1.0);
float mask = light_mask(texCoord);
fragColor = (layer0 + layer1 + layer2 + layer3) * mask * strength * (1.5 + 0.25 * sin(time));
fragColor.a = 0;
}

View file

@ -0,0 +1,2 @@
glsl_objects = standard.vert reimu_gap_light.frag

View file

@ -0,0 +1,9 @@
#version 330 core
#include "interface/sprite.glslh"
void main(void) {
vec4 texel = texture(tex, texCoord);
fragColor.rgb = color.rgb * texel.g - vec3(0.5 * texel.r) + vec3(texel.b);
fragColor.a = texel.a * color.a;
}

View file

@ -0,0 +1 @@
glsl_objects = sprite_default.vert sprite_yinyang.frag

View file

@ -489,7 +489,7 @@ static DamageResult ent_damage_boss(EntityInterface *ent, const DamageInfo *dmg)
play_loop("hit0");
}
return true;
return DMG_RESULT_OK;
}
static void boss_give_spell_bonus(Boss *boss, Attack *a, Player *plr) {
@ -769,7 +769,7 @@ void process_boss(Boss **pboss) {
play_sound_ex("bossdeath", BOSS_DEATH_DELAY * 2, false);
} else {
if(cabs(boss->pos - global.plr.pos) < 16) {
player_death(&global.plr);
ent_damage(&global.plr.ent, &(DamageInfo) { .type = DMG_ENEMY_COLLISION });
}
}

View file

@ -1,4 +1,5 @@
dialog_src = files(
'marisa.c',
'reimu.c',
'youmu.c',
)

View file

@ -86,6 +86,22 @@ void bad_ending_youmu(Ending *e) {
track_ending(ENDING_BAD_2);
}
void bad_ending_reimu(Ending *e) {
add_ending_entry(e, -1, "“Now, let your mind be expanded! Do you have the potential to understand the code behind reality?”", NULL);
add_ending_entry(e, -1, "Reimu may have been the mouthpiece of the gods, but this truth was far beyond the words they would push into her brain. Numbers overlapped endlessly and seamlessly over each other, and she quickly found herself in an inescapable mental haze. Without realizing it, she had stopped floating, and the Yin-Yang Orbs dropped uselessly alongside her, having lost all of their holy powers.", NULL);
add_ending_entry(e, -1, "Tired of what she perceived as nonsense, she willed the numbers to end, and they did, taking any directives of the Hakurei god along with them to create an unbearable silence that drifted darkly into unconsciousness.", NULL);
add_ending_entry(e, -1, "“Auughh!” \nBolting upwards in her futon, Reimu yelled out both in inexplicable frustration and pain as a sudden migraine woke her up on an otherwise peaceful morning. Grumbling, she glanced all around her room instinctually, as if she had seen something wrong with it before.", NULL);
add_ending_entry(e, -1, "But it looked just the same as it normally did, even through the angry throbbing in her skull. Knowing that further sleep was impossible, however, she picked herself up and started to get ready for the rest of the day. Of course, she didnt get too far before being interrupted by an overly familiar yōkai sage.", NULL);
add_ending_entry(e, -1, "“What do you want?” Reimu asked grouchily, rubbing her forehead as she stared at a strangely concerned Yukari.", NULL);
add_ending_entry(e, -1, "“Did you really defeat Wriggle as the main culprit of the incident yesterday?”", NULL);
add_ending_entry(e, -1, "“Huh? I dont even remember who I fought. Does it even matter?”", NULL);
add_ending_entry(e, -1, "“Yes, it does! Because Wriggle shouldnt have that sort of power. That means… she must be more dangerous than she has been letting on! Never mind, fufu!” ", NULL);
add_ending_entry(e, -1, "Watching Yukari change her tone mid-sentence, Reimu felt disconcerted. The gap hag must have been onto something, but just as soon as she had realized it, that thought had been taken away by a mysterious force.", NULL);
add_ending_entry(e, -1, "Clearly this peace she had achieved by defeating Wriggle was just a farce, but she had no idea who was really behind it. What she needed now was another chance in another time-- to find the real logical perpetrator of the incident, she would have to go back and do it all over again.", NULL);
add_ending_entry(e, -1, "-BAD END-\nTry a clear without continues next time!", NULL);
track_ending(ENDING_BAD_3);
}
void good_ending_marisa(Ending *e) {
add_ending_entry(e, -1, "“Now, let your mind be expanded! Do you have the potential to understand the code behind reality?”", NULL);
add_ending_entry(e, -1, "“Ha! Who do ya take me for? Of course!”", NULL);
@ -116,6 +132,24 @@ void good_ending_youmu(Ending *e) {
track_ending(ENDING_GOOD_2);
}
void good_ending_reimu(Ending *e) {
add_ending_entry(e, -1, "“Now, let your mind be expanded! Do you have the potential to understand the code behind reality?”", NULL);
add_ending_entry(e, -1, "“What a pointless question! Be defeated already!”", NULL);
add_ending_entry(e, -1, "Reimu, with her straightforward clarity, was able to hold fast and ignore the barrage focused upon her mind by sealing herself off via her fantasy nature. Raising her purification rod as a command, her Yin-Ying Orbs soared into the false sky and coalesced into one, a glowing orb that quickly grew to humongous proportions.", NULL);
add_ending_entry(e, -1, "She flicked her rod downwards, and Elly found herself facing this massive spirit orb unprepared. It plowed into her, and then also into her tower, smashing all in its path with a godly roar. When the dust and rubble settled, Reimu landed on the plains below, in front of the weakened Elly.", NULL);
add_ending_entry(e, -1, "“Are you satisfied? You destroyed my research, my life, and my home. And for what? An illogical world fit for neither yōkai nor humans!” Elly shouted, trying to but failing to crawl towards her fallen scythe.", NULL);
add_ending_entry(e, -1, "“Thats not true. Youre upset because you blame Gensōkyō for rejecting you when the reality is that you simply got lost, didnt you? Gensōkyō welcomes everyone, and it will still welcome you,” Reimu replied, kicking aside Ellys scythe before reaching out to pull her up. Elly hesitantly took it, her knees still shaking from Reimus spiritual blow.", NULL);
add_ending_entry(e, -1, "“I may be able to return to Gensōkyō as a yōkai, but what will happen to my work? I was so close to the truth and you blew it all away. There's no way I could be satisfied without knowing!”", NULL);
add_ending_entry(e, -1, "Reimu mulled over this for a moment before swinging her rod over her back, looking suddenly determined.", NULL);
add_ending_entry(e, -1, "“This truth of yours almost pulled Gensōkyō apart. For that Ill just have to figure out the rest in your stead and then deal with it in any way that Ill need to. Seems like I cant go home yet…” Reimu declared, breaking it off with a sigh at the end.", NULL);
add_ending_entry(e, -1, "“Oh well, Ill just make this as quick as I can. Feel free to invite yourself over to my shrine if you want. Everyone does that even if I say no, so I cant really stop you.”", NULL);
add_ending_entry(e, -1, "Elly blinked for a few seconds in shock, and then started to laugh. Just like in the old days, Reimu still treated everyone with perfect neutrality. If anyone was best suited to discovering something without biases, it was Reimu, the Shrine Maiden of Paradise.", NULL);
add_ending_entry(e, -1, "Reimu herself had already started floating away towards the artificial horizon, and Elly watched, now completely at ease. After all, if Reimu was on the case, it would always work out right in the end. She was the unavoidable constant in a sea of variables, and using her shrine maiden intuition as her compass, there was no way that she could fail to trailblaze the way to the truth.", NULL);
add_ending_entry(e, -1, "The only question now is what secret Reimu would, in fact, see at the end of the world.", NULL);
add_ending_entry(e, -1, "-GOOD END-\nExtra Stage has opened up for you! Do you have what it takes to discover the hidden truth behind everything?", NULL);
track_ending(ENDING_GOOD_3);
}
static void create_ending(Ending *e) {
memset(e, 0, sizeof(Ending));

View file

@ -17,12 +17,14 @@ enum {
};
enum {
// do not reorder these or change the values
// WARNING: Reordering this will break current progress files.
ENDING_BAD_1,
ENDING_BAD_2,
ENDING_GOOD_1,
ENDING_GOOD_2,
ENDING_BAD_3,
ENDING_GOOD_3,
NUM_ENDINGS,
};
@ -37,5 +39,7 @@ void ending_preload(void);
*/
void bad_ending_marisa(Ending *e);
void bad_ending_youmu(Ending *e);
void bad_ending_reimu(Ending *e);
void good_ending_marisa(Ending *e);
void good_ending_youmu(Ending *e);
void good_ending_reimu(Ending *e);

View file

@ -66,6 +66,7 @@ Enemy *create_enemy_p(EnemyList *enemies, complex pos, float hp, EnemyVisualRule
// XXX: some code relies on the insertion logic
Enemy *e = (Enemy*)alist_insert(enemies, enemies->first, objpool_acquire(stage_object_pools.enemies));
// Enemy *e = (Enemy*)alist_append(enemies, objpool_acquire(stage_object_pools.enemies));
e->moving = false;
e->dir = 0;

View file

@ -118,7 +118,13 @@ DamageResult ent_damage(EntityInterface *ent, const DamageInfo *damage) {
return DMG_RESULT_INAPPLICABLE;
}
return ent->damage_func(ent, damage);
DamageResult res = ent->damage_func(ent, damage);
if(res == DMG_RESULT_OK) {
player_register_damage(&global.plr, ent, damage);
}
return res;
}
void ent_area_damage(complex origin, float radius, const DamageInfo *damage) {

View file

@ -1062,3 +1062,30 @@ void player_preload(void) {
"extra_bomb",
NULL);
}
// FIXME: where should this be?
complex plrutil_homing_target(complex org, complex fallback) {
double mindst = INFINITY;
complex target = fallback;
if(global.boss && boss_is_vulnerable(global.boss)) {
target = global.boss->pos;
mindst = cabs(target - org);
}
for(Enemy *e = global.enemies.first; e; e = e->next) {
if(e->hp == ENEMY_IMMUNE) {
continue;
}
double dst = cabs(e->pos - org);
if(dst < mindst) {
mindst = dst;
target = e->pos;
}
}
return target;
}

View file

@ -168,3 +168,6 @@ bool player_is_alive(Player *plr);
double player_get_bomb_progress(Player *plr, double *out_speed);
void player_preload(void);
// FIXME: where should this be?
complex plrutil_homing_target(complex org, complex fallback);

View file

@ -15,13 +15,17 @@
#include "plrmodes.h"
#include "plrmodes/marisa.h"
#include "plrmodes/youmu.h"
#include "plrmodes/reimu.h"
static PlayerCharacter *player_characters[] = {
&character_reimu,
&character_marisa,
&character_youmu,
};
static PlayerMode *player_modes[] = {
&plrmode_reimu_a,
&plrmode_reimu_b,
&plrmode_marisa_a,
&plrmode_marisa_b,
&plrmode_youmu_a,

View file

@ -17,17 +17,17 @@
#include "dialog.h"
typedef enum {
/* do not reorder - screws replays */
// WARNING: Reordering this will break current replays, and possibly even progress files.
PLR_CHAR_MARISA = 0,
PLR_CHAR_YOUMU = 1,
// PLR_CHAR_REIMU = 2,
PLR_CHAR_REIMU = 0,
PLR_CHAR_MARISA = 1,
PLR_CHAR_YOUMU = 2,
NUM_CHARACTERS,
} CharacterID;
typedef enum {
/* do not reorder - screws replays */
// WARNING: Reordering this will break current replays, and possibly even progress files.
PLR_SHOT_A,
PLR_SHOT_B,
@ -38,6 +38,9 @@ typedef enum {
PLR_SHOT_YOUMU_MIRROR = PLR_SHOT_A,
PLR_SHOT_YOUMU_HAUNTING = PLR_SHOT_B,
PLR_SHOT_REIMU_SPIRIT = PLR_SHOT_A,
PLR_SHOT_REIMU_DREAM = PLR_SHOT_B,
} ShotModeID;
typedef enum {

View file

@ -11,7 +11,6 @@
#include "global.h"
#include "plrmodes.h"
#include "marisa.h"
#include "renderer/api.h"
PlayerCharacter character_marisa = {
.id = PLR_CHAR_MARISA,

View file

@ -526,6 +526,10 @@ static int masterspark(Enemy *e, int t2) {
}
static void marisa_laser_bombbg(Player *plr) {
if(!player_is_bomb_active(plr)) {
return;
}
float t = player_get_bomb_progress(&global.plr, NULL);
float fade = 1;

View file

@ -234,6 +234,10 @@ static void marisa_star_bomb(Player *plr) {
}
static void marisa_star_bombbg(Player *plr) {
if(!player_is_bomb_active(plr)) {
return;
}
float t = player_get_bomb_progress(&global.plr, NULL);
ShaderProgram *s = r_shader_get("maristar_bombbg");

View file

@ -3,6 +3,9 @@ plrmodes_src = files(
'marisa_a.c',
'marisa_b.c',
'marisa.c',
'reimu_a.c',
'reimu_b.c',
'reimu.c',
'youmu_a.c',
'youmu_b.c',
'youmu.c',

145
src/plrmodes/reimu.c Normal file
View file

@ -0,0 +1,145 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2018, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2018, Andrei Alexeyev <akari@alienslab.net>.
*/
#include "taisei.h"
#include "global.h"
#include "plrmodes.h"
#include "reimu.h"
#include "stagedraw.h"
static Framebuffer *bomb_buffer;
PlayerCharacter character_reimu = {
.id = PLR_CHAR_REIMU,
.lower_name = "reimu",
.proper_name = "Reimu",
.full_name = "Hakurei Reimu",
.title = "Shrine Maiden",
.dialog_sprite_name = "dialog/reimu",
.player_sprite_name = "player/reimu",
.ending = {
.good = good_ending_reimu,
.bad = bad_ending_reimu,
},
};
double reimu_common_property(Player *plr, PlrProperty prop) {
switch(prop) {
case PLR_PROP_BOMB_TIME:
return 300;
case PLR_PROP_COLLECT_RADIUS:
return (plr->inputflags & INFLAG_FOCUS) ? 60 : 30;
case PLR_PROP_SPEED:
// NOTE: For equivalents in Touhou units, divide these by 1.25.
return (plr->inputflags & INFLAG_FOCUS) ? 2.5 : 5.625;
case PLR_PROP_POC:
return VIEWPORT_H / 3.5;
case PLR_PROP_DEATHBOMB_WINDOW:
return 12;
}
UNREACHABLE;
}
static int reimu_ofuda_trail(Projectile *p, int t) {
int r = linear(p, t);
if(t < 0) {
return r;
}
p->color.g *= 0.95;
return r;
}
int reimu_common_ofuda(Projectile *p, int t) {
if(t == EVENT_DEATH) {
return ACTION_ACK;
}
p->angle = carg(p->args[0]);
if(t == EVENT_BIRTH) {
return ACTION_ACK;
}
p->pos += p->args[0];
PARTICLE(
// .sprite_ptr = p->sprite,
.sprite = "hghost",
.color = &p->color,
.timeout = 12,
.pos = p->pos + p->args[0] * 0.3,
.args = { p->args[0] * 0.5, 0, 1+2*I },
.rule = reimu_ofuda_trail,
.draw_rule = ScaleFade,
.layer = LAYER_PARTICLE_LOW,
.flags = PFLAG_NOREFLECT,
);
return ACTION_NONE;
}
void reimu_common_draw_yinyang(Enemy *e, int t, const Color *c) {
r_draw_sprite(&(SpriteParams) {
.sprite = "yinyang",
.shader = "sprite_yinyang",
.pos = { creal(e->pos), cimag(e->pos) },
.rotation.angle = global.frames * -6 * DEG2RAD,
// .color = rgb(0.95, 0.75, 1.0),
.color = c,
});
}
static void capture_frame(Framebuffer *dest, Framebuffer *src) {
r_state_push();
r_framebuffer(dest);
r_shader_standard();
r_color4(1, 1, 1, 1);
r_blend(BLEND_NONE);
draw_framebuffer_tex(src, VIEWPORT_W, VIEWPORT_H);
r_state_pop();
}
void reimu_common_bomb_bg(Player *p, float alpha) {
if(alpha <= 0)
return;
r_state_push();
r_color(HSLA_MUL_ALPHA(global.frames / 30.0, 0.2, 0.9, alpha));
r_shader("reimu_bomb_bg");
r_texture(1, "runes");
r_uniform_int("runes", 1);
r_uniform_float("zoom", VIEWPORT_H / sqrt(VIEWPORT_W*VIEWPORT_W + VIEWPORT_H*VIEWPORT_H));
r_uniform_vec2("aspect", VIEWPORT_W / (float)VIEWPORT_H, 1);
r_uniform_float("time", 9000 + 3 * global.frames / 60.0);
draw_framebuffer_tex(bomb_buffer, VIEWPORT_W, VIEWPORT_H);
r_state_pop();
capture_frame(bomb_buffer, r_framebuffer_current());
}
void reimu_common_bomb_buffer_init(void) {
FBAttachmentConfig cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.attachment = FRAMEBUFFER_ATTACH_COLOR0;
cfg.tex_params.type = TEX_TYPE_RGB;
cfg.tex_params.filter.min = TEX_FILTER_LINEAR;
cfg.tex_params.filter.mag = TEX_FILTER_LINEAR;
cfg.tex_params.wrap.s = TEX_WRAP_MIRROR;
cfg.tex_params.wrap.t = TEX_WRAP_MIRROR;
bomb_buffer = stage_add_foreground_framebuffer(1, 1, &cfg);
}

23
src/plrmodes/reimu.h Normal file
View file

@ -0,0 +1,23 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2018, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2018, Andrei Alexeyev <akari@alienslab.net>.
*/
#pragma once
#include "taisei.h"
#include "plrmodes.h"
#include "dialog/reimu.h"
extern PlayerCharacter character_reimu;
extern PlayerMode plrmode_reimu_a;
extern PlayerMode plrmode_reimu_b;
double reimu_common_property(Player *plr, PlrProperty prop);
int reimu_common_ofuda(Projectile *p, int t);
void reimu_common_draw_yinyang(Enemy *e, int t, const Color *c);
void reimu_common_bomb_bg(Player *p, float alpha);
void reimu_common_bomb_buffer_init(void);

653
src/plrmodes/reimu_a.c Normal file
View file

@ -0,0 +1,653 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2018, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2018, Andrei Alexeyev <akari@alienslab.net>.
*/
#include "taisei.h"
#include "global.h"
#include "plrmodes.h"
#include "reimu.h"
// FIXME: We probably need a better way to store shot-specific state.
// See also MarisaA.
static struct {
uint prev_inputflags;
bool respawn_slaves;
} reimu_spirit_state;
static void reimu_spirit_preload(void) {
const int flags = RESF_DEFAULT;
preload_resources(RES_SPRITE, flags,
"yinyang",
"proj/ofuda",
"proj/needle",
"part/ofuda_glow",
NULL);
preload_resources(RES_SHADER_PROGRAM, flags,
"sprite_yinyang",
NULL);
//preload_resources(RES_SFX, flags | RESF_OPTIONAL,
//NULL);
}
static int reimu_spirit_needle(Projectile *p, int t) {
int r = linear(p, t);
if(t < 0) {
return r;
}
Color c = p->color;
color_mul(&c, RGBA_MUL_ALPHA(0.75, 0.5, 1, 0.5));
c.a = 0;
PARTICLE(
.sprite_ptr = p->sprite,
.color = &c,
.timeout = 12,
.pos = p->pos,
.args = { p->args[0] * 0.8, 0, 0+3*I },
.rule = linear,
.draw_rule = ScaleFade,
.layer = LAYER_PARTICLE_LOW,
.flags = PFLAG_NOREFLECT,
);
return r;
}
#define REIMU_SPIRIT_HOMING_SCALE 0.75
static void reimu_spirit_homing_draw(Projectile *p, int t) {
r_mat_push();
r_mat_translate(creal(p->pos), cimag(p->pos), 0);
r_mat_rotate(p->angle + M_PI/2, 0, 0, 1);
r_mat_scale(REIMU_SPIRIT_HOMING_SCALE, REIMU_SPIRIT_HOMING_SCALE, 1);
ProjDrawCore(p, &p->color);
r_mat_pop();
}
static Projectile* reimu_spirit_spawn_ofuda_particle(Projectile *p, int t, double vfactor) {
Color *c = HSLA_MUL_ALPHA(t * 0.1, 0.6, 0.7, 0.45);
c->a = 0;
return PARTICLE(
.sprite = "ofuda_glow",
// .color = rgba(0.5 + 0.5 + psin(global.frames * 0.75), psin(t*0.5), 1, 0.5),
.color = c,
.timeout = 12,
.pos = p->pos,
.args = { p->args[0] * (0.6 + 0.4 * frand()) * vfactor, 0, (1+2*I) * REIMU_SPIRIT_HOMING_SCALE },
.angle = p->angle,
.rule = linear,
.draw_rule = ScaleFade,
.layer = LAYER_PARTICLE_LOW,
.flags = PFLAG_NOREFLECT,
);
}
static int reimu_spirit_homing_impact(Projectile *p, int t) {
if(t < 0) {
return ACTION_ACK;
}
Projectile *trail = reimu_spirit_spawn_ofuda_particle(p, global.frames, 1);
trail->rule = NULL;
trail->timeout = 6;
trail->angle = p->angle;
trail->ent.draw_layer = LAYER_PLAYER_FOCUS - 1; // TODO: add a layer for "super high" particles?
trail->args[2] *= 1.5 * (1 - t/p->timeout);
p->angle += 0.2;
return ACTION_NONE;
}
static Projectile* reimu_spirit_spawn_homing_impact(Projectile *p, int t) {
return PARTICLE(
.proto = p->proto,
.color = &p->color,
.timeout = 32,
.pos = p->pos,
.args = { 0, 0, (1+2*I) * REIMU_SPIRIT_HOMING_SCALE },
.angle = p->angle,
.rule = reimu_spirit_homing_impact,
.draw_rule = ScaleFade,
.layer = LAYER_PARTICLE_HIGH,
.flags = PFLAG_NOREFLECT,
);
}
static inline double reimu_spirit_homing_aimfactor(double t, double maxt) {
t = clamp(t, 0, maxt);
double q = pow(1 - t / maxt, 3);
return 4 * q * (1 - q);
}
static int reimu_spirit_homing(Projectile *p, int t) {
if(t < 0) {
if(t == EVENT_DEATH && !global.game_over && projectile_in_viewport(p)) {
reimu_spirit_spawn_homing_impact(p, t);
}
return ACTION_ACK;
}
p->args[3] = plrutil_homing_target(p->pos, p->args[3]);
double v = cabs(p->args[0]);
complex aimdir = cexp(I*carg(p->args[3] - p->pos));
double aim = reimu_spirit_homing_aimfactor(t, p->args[1]);
p->args[0] += v * 0.25 * aim * aimdir;
p->args[0] = v * cexp(I*carg(p->args[0]));
p->angle = carg(p->args[0]);
double s = 1;// max(pow(2*t/creal(p->args[1]), 2), 0.1); //(0.25 + 0.75 * (1 - aim));
p->pos += p->args[0] * s;
reimu_spirit_spawn_ofuda_particle(p, t, 0.5);
return ACTION_NONE;
}
static Color *reimu_spirit_orb_color(Color *c, int i) {
*c = *RGBA((0.2 + (i==0))/1.2, (0.2 + (i==1))/1.2, (0.2 + 1.5*(i==2))/1.2, 0.0);
return c;
}
static void reimu_spirit_bomb_orb_visual(Projectile *p, int t) {
complex pos = p->pos;
for(int i = 0; i < 3; i++) {
complex offset = (10 + pow(t, 0.5)) * cexp(I * (2 * M_PI / 3*i + sqrt(1 + t * t / 300.0)));
Color c;
r_draw_sprite(&(SpriteParams) {
.sprite = "proj/glowball",
.shader = "sprite_bullet",
.pos = { creal(pos+offset), cimag(pos+offset) },
.color = reimu_spirit_orb_color(&c, i),
// .shader_params = &(ShaderCustomParams) {.vector = {0.3,0,0,0}},
});
}
}
static int reimu_spirit_bomb_orb_trail(Projectile *p, int t) {
if(t < 0) {
return ACTION_ACK;
}
p->pos += p->args[0];
p->angle -= 0.05;
// p->color = *HSLA(2.0*t/p->timeout, 0.5, 0.5, 0.0);
return ACTION_NONE;
}
static void reimu_spirit_bomb_orb_draw_impact(Projectile *p, int t) {
float attack = min(1, (7 + 5 * p->args[0]) * t / p->timeout);
float decay = t / p->timeout;
Color c = p->color;
color_lerp(&c, RGBA(0.2, 0.1, 0, 1.0), decay);
color_mul_scalar(&c, pow(1 - decay, 2) * 0.75);
r_draw_sprite(&(SpriteParams) {
.sprite_ptr = p->sprite,
.pos = { creal(p->pos), cimag(p->pos) },
.color = &c,
.shader_ptr = p->shader,
.shader_params = &p->shader_params,
.scale.both = (0.75 + 0.25 / (pow(decay, 3.0) + 1.0)) + sqrt(5 * (1 - attack)),
});
}
static int reimu_spirit_bomb_orb(Projectile *p, int t) {
int index = creal(p->args[1]) + 0.5;
if(t == EVENT_BIRTH) {
if(index == 0)
global.shake_view = 4;
p->args[3] = global.plr.pos;
return ACTION_ACK;
}
if(t == EVENT_DEATH) {
if(global.game_over) {
return ACTION_ACK;
}
global.shake_view = 20;
global.shake_view_fade = 0.6;
double damage = 2000;
double range = 300;
ent_area_damage(p->pos, range, &(DamageInfo){damage, DMG_PLAYER_BOMB});
stage_clear_hazards_at(p->pos, range, CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
int count = 21;
double offset = frand();
for(int i = 0; i < count; i++) {
PARTICLE(
.sprite_ptr = get_sprite("proj/glowball"),
.shader = "sprite_bullet",
.color = HSLA(3 * (float)i / count + offset, 1, 0.5, 0), // reimu_spirit_orb_color(&(Color){0}, i%3),w
.timeout = 60,
.pos = p->pos,
.args = { cexp(I * 2 * M_PI / count * (i + offset)) * 15 },
.angle = 2*M_PI*frand(),
.rule = linear,
.draw_rule = Fade,
.layer = LAYER_BOSS,
.flags = PFLAG_NOREFLECT,
);
}
for(int i = 0; i < 3; ++i) {
PARTICLE(
.sprite = "blast",
.size = 64 * (I+1),
.color = color_mul_scalar(reimu_spirit_orb_color(&(Color){0}, i), 2),
.pos = p->pos + 30 * cexp(I*2*M_PI/3*(i+t*0.1)),
.timeout = 40,
.draw_rule = ScaleFade,
.layer = LAYER_BOSS + 2,
.args = { 0, 0, 7.5*I },
);
PARTICLE(
.sprite = "fantasyseal_impact",
.color = reimu_spirit_orb_color(&(Color){0}, i),
.pos = p->pos + 2 * cexp(I*2*M_PI/3*(i+t*0.1)),
.timeout = 120,
.draw_rule = reimu_spirit_bomb_orb_draw_impact,
.layer = LAYER_BOSS + 1,
.args = { frand() },
);
}
play_sound("boom");
play_sound("spellend");
return ACTION_ACK;
}
if(!player_is_bomb_active(&global.plr) > 0) {
return ACTION_DESTROY;
}
double circletime = 100+20*index;
if(t == circletime) {
p->args[3] = global.plr.pos - 128*I;
play_sound("redirect");
}
complex target_circle = global.plr.pos + 10 * sqrt(t) * p->args[0]*(1 + 0.1 * sin(0.2*t));
p->args[0] *= cexp(I*0.12);
double circlestrength = 1.0 / (1 + exp(t-circletime));
p->args[3] = plrutil_homing_target(p->pos, p->args[3]);
complex target_homing = p->args[3];
complex homing = target_homing - p->pos;
complex v = 0.3 * (circlestrength * (target_circle - p->pos) + 0.2 * (1-circlestrength) * (homing + 2*homing/(cabs(homing)+0.01)));
p->args[2] += (v - p->args[2]) * 0.2;
p->pos += p->args[2];
for(int i = 0; i < 3 /*&& circlestrength < 1*/; i++) {
complex pos = p->pos + 10 * cexp(I*2*M_PI/3*(i+t*0.1));
complex v = global.plr.pos - pos;
v *= 3 * circlestrength / cabs(v);
PARTICLE(
.sprite_ptr = get_sprite("part/stain"),
// .color = reimu_spirit_orb_color(&(Color){0}, i),
.color = HSLA(t/p->timeout, 0.5, 0.5, 0.0),
.pos = pos,
.angle = 2*M_PI*frand(),
.timeout = 30,
.draw_rule = ScaleFade,
.rule = reimu_spirit_bomb_orb_trail,
.args = { v, 0, 0.8 },
);
}
return ACTION_NONE;
}
static void reimu_spirit_bomb(Player *p) {
int count = 6;
for(int i = 0; i < count; i++) {
PROJECTILE(
.pos = p->pos,
.draw_rule = reimu_spirit_bomb_orb_visual,
.rule = reimu_spirit_bomb_orb,
.args = { cexp(I*2*M_PI/count*i), i, 0, 0},
.timeout = 160 + 20 * i,
.type = PlrProj,
.damage = 0,
.size = 10 + 10*I,
.layer = LAYER_PLAYER_FOCUS - 1,
);
}
play_sound("bomb_reimu_a");
play_sound("bomb_marisa_b");
}
static void reimu_spirit_bomb_bg(Player *p) {
if(!player_is_bomb_active(p)) {
return;
}
double speed;
double t = player_get_bomb_progress(p, &speed);
float alpha = 0;
if(t > 0)
alpha = min(1,10*t);
if(t > 0.7)
alpha *= 1-pow((t-0.7)/0.3,4);
reimu_common_bomb_bg(p, alpha);
colorfill(0, 0.05 * alpha, 0.1 * alpha, alpha * 0.5);
}
static void reimu_spirit_shot(Player *p) {
play_loop("generic_shot");
if(!(global.frames % 3)) {
int i = 1 - 2 * (bool)(global.frames % 6);
PROJECTILE(
.proto = pp_ofuda,
.pos = p->pos + 10 * i - 15.0*I,
.color = RGBA_MUL_ALPHA(1, 1, 1, 0.5),
.rule = reimu_common_ofuda,
.args = { -20.0*I },
.type = PlrProj,
.damage = 100 - 8 * (p->power / 100),
.shader = "sprite_default",
);
}
for(int pwr = 0; pwr <= p->power/100; ++pwr) {
int t = (global.frames - 5 * pwr);
if(!(t % 16)) {
for(int i = -1; i < 2; i += 2) {
float spread = i * M_PI/32 * (1 + 0.35 * pwr);
PROJECTILE(
.proto = pp_hakurei_seal,
.pos = p->pos - I + 5 * i,
.color = color_mul_scalar(RGBA(1, 1, 1, 0.5), 0.7),
.rule = linear,
.args = { -18.0*I*cexp(I*spread) },
.type = PlrProj,
.damage = 60 - 5 * (p->power / 100),
.shader = "sprite_default",
);
}
}
}
}
static void reimu_spirit_slave_shot(Enemy *e, int t) {
int st = t;
if(st % 3) {
return;
}
if(global.plr.inputflags & INFLAG_FOCUS) {
PROJECTILE(
.proto = pp_needle,
.pos = e->pos - 25.0*I,
.color = RGBA_MUL_ALPHA(1, 1, 1, 0.5),
.rule = reimu_spirit_needle,
.args = { -20.0*I },
.type = PlrProj,
.damage_type = DMG_PLAYER_SHOT,
.damage = cimag(e->args[2]),
.shader = "sprite_default",
);
} else if(!(st % 12)) {
complex v = -10 * I * cexp(I*cimag(e->args[0]));
PROJECTILE(
.proto = pp_ofuda,
.pos = e->pos,
.color = RGBA_MUL_ALPHA(1, 0.9, 0.95, 0.7),
.rule = reimu_spirit_homing,
.draw_rule = reimu_spirit_homing_draw,
.args = { v , 60, 0, e->pos + v * VIEWPORT_H * VIEWPORT_W /*creal(e->pos)*/ },
.type = PlrProj,
.damage_type = DMG_PLAYER_SHOT,
.damage = creal(e->args[2]),
// .timeout = 60,
.shader = "sprite_default",
.flags = PFLAG_NOCOLLISIONEFFECT,
);
}
}
static int reimu_spirit_slave(Enemy *e, int t) {
TIMER(&t);
AT(EVENT_BIRTH) {
e->pos = global.plr.pos;
return ACTION_NONE;
}
if(t < 0) {
return ACTION_NONE;
}
if(player_should_shoot(&global.plr, true)) {
reimu_spirit_slave_shot(e, t);
}
if(creal(e->args[3]) != 0) {
int death_begin_time = creal(e->args[3]);
int death_duration = cimag(e->args[3]);
double death_progress = (t - death_begin_time) / (double)death_duration;
e->pos = global.plr.pos * death_progress + e->pos0 * (1 - death_progress);
if(death_progress >= 1) {
return ACTION_DESTROY;
}
return ACTION_NONE;
}
double speed = 0.005 * min(1, t / 12.0);
if(global.plr.inputflags & INFLAG_FOCUS) {
GO_TO(e, global.plr.pos + cimag(e->args[1]) * cexp(I*(creal(e->args[0]) + t * creal(e->args[1]))), speed * cabs(e->args[1]));
} else {
GO_TO(e, global.plr.pos + e->pos0, speed * cabs(e->pos0));
}
return ACTION_NONE;
}
static int reimu_spirit_yinyang_flare(Projectile *p, int t) {
double a = p->angle;
int r = linear(p, t);
p->angle = a;
return r;
}
static void reimu_spirit_yinyang_focused_visual(Enemy *e, int t, bool render) {
if(!render && player_should_shoot(&global.plr, true)) {
PARTICLE(
.sprite = "stain",
.color = RGBA(1, 0.0 + 0.5 * frand(), 0, 0),
.timeout = 12 + 2 * nfrand(),
.pos = e->pos,
.args = { -5*I * (1 + frand()), 0, 0.5 + 0*I },
.angle = 2*M_PI*frand(),
.rule = reimu_spirit_yinyang_flare,
.draw_rule = ScaleFade,
.layer = LAYER_PARTICLE_HIGH,
.flags = PFLAG_NOREFLECT,
);
}
if(render) {
reimu_common_draw_yinyang(e, t, RGB(1.0, 0.8, 0.8));
}
}
static void reimu_spirit_yinyang_unfocused_visual(Enemy *e, int t, bool render) {
if(!render && player_should_shoot(&global.plr, true)) {
PARTICLE(
.sprite = "stain",
.color = RGBA(1, 0.25, 0.0 + 0.5 * frand(), 0),
.timeout = 12 + 2 * nfrand(),
.pos = e->pos,
.args = { -5*I * (1 + frand()), 0, 0.5 + 0*I },
.angle = 2*M_PI*frand(),
.rule = reimu_spirit_yinyang_flare,
.draw_rule = ScaleFade,
.layer = LAYER_PARTICLE_HIGH,
.flags = PFLAG_NOREFLECT,
);
}
if(render) {
reimu_common_draw_yinyang(e, t, RGB(0.95, 0.75, 1.0));
}
}
static inline Enemy* reimu_spirit_spawn_slave(Player *plr, complex pos, complex a0, complex a1, complex a2, complex a3, EnemyVisualRule visual) {
Enemy *e = create_enemy_p(&plr->slaves, pos, ENEMY_IMMUNE, visual, reimu_spirit_slave, a0, a1, a2, a3);
e->ent.draw_layer = LAYER_PLAYER_SLAVE;
return e;
}
static void reimu_spirit_kill_slaves(EnemyList *slaves) {
for(Enemy *e = slaves->first, *next; e; e = next) {
next = e->next;
if(e->hp == ENEMY_IMMUNE && creal(e->args[3]) == 0) {
// delete_enemy(slaves, e);
// e->args[3] = 1;
e->args[3] = global.frames - e->birthtime + 3 * I;
e->pos0 = e->pos;
}
}
}
static void reimu_spirit_respawn_slaves(Player *plr, short npow, complex param) {
double dmg_homing = 120 - 12 * plr->power / 100; // every 12 frames
double dmg_needle = 90 - 10 * plr->power / 100; // every 3 frames
complex dmg = dmg_homing + I * dmg_needle;
EnemyVisualRule visual;
if(plr->inputflags & INFLAG_FOCUS) {
visual = reimu_spirit_yinyang_focused_visual;
} else {
visual = reimu_spirit_yinyang_unfocused_visual;
}
reimu_spirit_kill_slaves(&plr->slaves);
switch(npow / 100) {
case 0:
break;
case 1:
reimu_spirit_spawn_slave(plr, 50.0*I, 0 , +0.10 + 60*I, dmg, 0, visual);
break;
case 2:
reimu_spirit_spawn_slave(plr, +40, 0 +M_PI/24*I, +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, -40, M_PI -M_PI/24*I, +0.10 + 60*I, dmg, 0, visual);
break;
case 3:
reimu_spirit_spawn_slave(plr, 50.0*I, 0*2*M_PI/3 , +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, +40, 1*2*M_PI/3 +M_PI/24*I, +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, -40, 2*2*M_PI/3 -M_PI/24*I, +0.10 + 60*I, dmg, 0, visual);
break;
case 4:
reimu_spirit_spawn_slave(plr, +40, 0 +M_PI/32*I, +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, -40, M_PI -M_PI/32*I, +0.10 + 60*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, +80, 0 +M_PI/16*I, -0.05 + 80*I, dmg, 0, visual);
reimu_spirit_spawn_slave(plr, -80, M_PI -M_PI/16*I, -0.05 + 80*I, dmg, 0, visual);
break;
default:
UNREACHABLE;
}
}
static void reimu_spirit_init(Player *plr) {
memset(&reimu_spirit_state, 0, sizeof(reimu_spirit_state));
reimu_spirit_state.prev_inputflags = plr->inputflags;
reimu_spirit_respawn_slaves(plr, plr->power, 0);
reimu_common_bomb_buffer_init();
}
static void reimu_spirit_think(Player *plr) {
if((bool)(reimu_spirit_state.prev_inputflags & INFLAG_FOCUS) ^ (bool)(plr->inputflags & INFLAG_FOCUS)) {
reimu_spirit_state.respawn_slaves = true;
}
if(reimu_spirit_state.respawn_slaves) {
if(plr->slaves.first) {
reimu_spirit_kill_slaves(&plr->slaves);
} else {
reimu_spirit_respawn_slaves(plr, plr->power, 0);
reimu_spirit_state.respawn_slaves = false;
}
}
reimu_spirit_state.prev_inputflags = plr->inputflags;
}
static void reimu_spirit_power(Player *plr, short npow) {
if(plr->power / 100 != npow / 100) {
reimu_spirit_respawn_slaves(plr, npow, 0);
}
}
double reimu_spirit_property(Player *plr, PlrProperty prop) {
double base_value = reimu_common_property(plr, prop);
switch(prop) {
case PLR_PROP_SPEED: {
return base_value * (pow(player_get_bomb_progress(plr, NULL), 0.5));
}
default: {
return base_value;
}
}
}
PlayerMode plrmode_reimu_a = {
.name = "Spirit Sign",
.character = &character_reimu,
.dialog = &dialog_reimu,
.shot_mode = PLR_SHOT_REIMU_SPIRIT,
.procs = {
.property = reimu_spirit_property,
.init = reimu_spirit_init,
.think = reimu_spirit_think,
.bomb = reimu_spirit_bomb,
.bombbg = reimu_spirit_bomb_bg,
.shot = reimu_spirit_shot,
.power = reimu_spirit_power,
.preload = reimu_spirit_preload,
},
};

558
src/plrmodes/reimu_b.c Normal file
View file

@ -0,0 +1,558 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2018, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2018, Andrei Alexeyev <akari@alienslab.net>.
*/
#include "taisei.h"
#include "global.h"
#include "plrmodes.h"
#include "reimu.h"
#include "stagedraw.h"
#define GAP_LENGTH 128
#define GAP_WIDTH 16
#define GAP_OFFSET 8
#define GAP_LIMIT 10
// #define GAP_LENGTH 160
// #define GAP_WIDTH 160
// #define GAP_OFFSET 82
#define NUM_GAPS 4
#define FOR_EACH_GAP(gap) for(Enemy *gap = global.plr.slaves.first; gap; gap = gap->next) if(gap->logic_rule == reimu_dream_gap)
static Enemy *gap_renderer;
static complex reimu_dream_gap_target_pos(Enemy *e) {
double x, y;
if(creal(e->pos0)) {
x = creal(e->pos0) * VIEWPORT_W;
} else {
x = creal(global.plr.pos);
}
if(cimag(e->pos0)) {
y = cimag(e->pos0) * VIEWPORT_H;
} else {
y = cimag(global.plr.pos);
}
bool focus = global.plr.inputflags & INFLAG_FOCUS;
// complex ofs = GAP_OFFSET * (1 + I) + (GAP_LENGTH * 0.5 + GAP_LIMIT) * e->args[0];
double ofs_x = GAP_OFFSET + (GAP_LENGTH * 0.5 + GAP_LIMIT) * creal(e->args[0]);
double ofs_y = GAP_OFFSET + (!focus) * (GAP_LENGTH * 0.5 + GAP_LIMIT) * cimag(e->args[0]);
x = clamp(x, ofs_x, VIEWPORT_W - ofs_x);
y = clamp(y, ofs_y, VIEWPORT_H - ofs_y);
if(focus) {
/*
if(cimag(e->pos0)) {
x = VIEWPORT_W - x;
}
*/
} else {
if(creal(e->pos0)) {
y = VIEWPORT_H - y;
}
}
return x + I * y;
}
static int reimu_dream_gap_bomb_projectile(Projectile *p, int t) {
if(t == EVENT_BIRTH) {
return ACTION_ACK;
}
if(t == EVENT_DEATH) {
return ACTION_ACK;
}
p->pos += p->args[0];
if(!(t % 3)) {
double range = GAP_LENGTH * 0.35;
double damage = 50;
// Yes, I know, this is inefficient as hell, but I'm too lazy to write a
// stage_clear_hazards_inside_rectangle function.
stage_clear_hazards_at(p->pos, range, CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW);
ent_area_damage(p->pos, range, &(DamageInfo) { damage, DMG_PLAYER_BOMB });
}
return ACTION_NONE;
}
static void reimu_dream_gap_bomb_projectile_draw(Projectile *p, int t) {
r_draw_sprite(&(SpriteParams) {
.sprite_ptr = p->sprite,
.shader_ptr = p->shader,
.color = &p->color,
.shader_params = &p->shader_params,
.pos = { creal(p->pos), cimag(p->pos) },
.scale.both = 0.75 * clamp(t / 5.0, 0.1, 1.0),
});
}
static void reimu_dream_gap_bomb(Enemy *e, int t) {
if(!(t % 4)) {
PROJECTILE(
.sprite = "glowball",
.size = 32 * (1 + I),
.color = HSLA(t/30.0, 0.5, 0.5, 0.5),
.pos = e->pos + e->args[0] * (frand() - 0.5) * GAP_LENGTH * 0.5,
.rule = reimu_dream_gap_bomb_projectile,
.draw_rule = reimu_dream_gap_bomb_projectile_draw,
.type = PlrProj,
.damage_type = DMG_PLAYER_BOMB,
.damage = 75,
.args = { -20 * e->pos0 },
);
global.shake_view += 5;
if(!(t % 16)) {
play_sound("boon");
}
}
}
static int reimu_dream_gap(Enemy *e, int t) {
if(t == EVENT_DEATH) {
free_ref(creal(e->args[3]));
return ACTION_ACK;
}
if(t == EVENT_BIRTH) {
return ACTION_ACK;
}
if(player_is_bomb_active(&global.plr)) {
reimu_dream_gap_bomb(e, t + cimag(e->args[3]));
} else {
complex new_pos = reimu_dream_gap_target_pos(e);
if(t == 0) {
e->pos = new_pos;
} else {
e->pos += (new_pos - e->pos) * 0.1 * (1 - sqrt(gap_renderer->args[0]));
}
}
return ACTION_NONE;
}
static void reimu_dream_gap_link(Enemy *g0, Enemy *g1) {
int ref0 = add_ref(g0);
int ref1 = add_ref(g1);
g0->args[3] = ref1;
g1->args[3] = ref0;
}
static Enemy* reimu_dream_gap_get_linked(Enemy *gap) {
return REF(creal(gap->args[3]));
}
static void reimu_dream_gap_draw_lights(int time, double strength) {
if(strength <= 0) {
return;
}
r_texture(0, "gaplight");
r_shader("reimu_gap_light");
r_uniform_float("time", time / 60.0);
r_uniform_float("strength", strength);
FOR_EACH_GAP(gap) {
const float len = GAP_LENGTH * 3 * sqrt(log(strength + 1) / 0.693);
complex center = gap->pos - gap->pos0 * (len * 0.5 - GAP_WIDTH * 0.6);
r_mat_push();
r_mat_translate(creal(center), cimag(center), 0);
r_mat_rotate(carg(gap->pos0)+M_PI, 0, 0, 1);
r_mat_scale(len, GAP_LENGTH, 1);
r_draw_quad();
r_mat_pop();
}
}
static void reimu_dream_gap_renderer_visual(Enemy *e, int t, bool render) {
if(!render) {
return;
}
float gaps[NUM_GAPS][2];
float angles[NUM_GAPS];
int links[NUM_GAPS];
int i = 0;
FOR_EACH_GAP(gap) {
gaps[i][0] = creal(gap->pos);
gaps[i][1] = cimag(gap->pos);
angles[i] = carg(gap->pos0);
links[i] = cimag(reimu_dream_gap_get_linked(gap)->args[3]);
++i;
}
FBPair *framebuffers = stage_get_fbpair(FBPAIR_FG);
fbpair_swap(framebuffers);
Framebuffer *target_fb = framebuffers->back;
// This change must propagate
r_state_pop();
r_framebuffer(target_fb);
r_state_push();
r_shader("reimu_gap");
r_uniform_vec2("viewport", VIEWPORT_W, VIEWPORT_H);
r_uniform_float("time", t / (float)FPS);
r_uniform_vec2("gap_size", GAP_WIDTH/2.0, GAP_LENGTH/2.0);
r_uniform("gaps[0]", NUM_GAPS, gaps);
r_uniform("gap_angles[0]", NUM_GAPS, angles);
r_uniform("gap_links[0]", NUM_GAPS, links);
draw_framebuffer_tex(framebuffers->front, VIEWPORT_W, VIEWPORT_H);
FOR_EACH_GAP(gap) {
r_mat_push();
r_mat_translate(creal(gap->pos), cimag(gap->pos), 0);
complex stretch_vector = gap->args[0];
for(float ofs = -0.5; ofs <= 0.5; ofs += 1) {
r_draw_sprite(&(SpriteParams) {
.sprite = "yinyang",
.shader = "sprite_yinyang",
.pos = {
creal(stretch_vector) * GAP_LENGTH * ofs,
cimag(stretch_vector) * GAP_LENGTH * ofs
},
.rotation.angle = global.frames * -6 * DEG2RAD,
.color = RGB(0.95, 0.75, 1.0),
.scale.both = 0.5,
});
}
r_mat_pop();
}
reimu_dream_gap_draw_lights(t, pow(e->args[0], 2));
}
static int reimu_dream_gap_renderer(Enemy *e, int t) {
if(t < 0) {
return ACTION_ACK;
}
if(player_is_bomb_active(&global.plr)) {
e->args[0] = approach(e->args[0], 1.0, 0.1);
} else {
e->args[0] = approach(e->args[0], 0.0, 0.025);
}
return ACTION_NONE;
}
static void reimu_dream_preload(void) {
const int flags = RESF_DEFAULT;
preload_resources(RES_SHADER_PROGRAM, flags,
"reimu_gap",
NULL);
}
static void reimu_dream_bomb(Player *p) {
play_sound("bomb_marisa_a");
}
static void reimu_dream_bomb_bg(Player *p) {
float a = gap_renderer->args[0];
reimu_common_bomb_bg(p, a);
}
static void reimu_dream_spawn_warp_effect(complex pos, bool exit) {
PARTICLE(
.sprite = "myon",
.pos = pos,
.color = RGBA(0.5, 0.5, 0.5, 0.5),
.timeout = 20,
.angle = frand() * M_PI * 2,
.draw_rule = ScaleFade,
.args = { 0, 0, 1 + 2 * I },
.layer = LAYER_PLAYER_FOCUS,
);
PARTICLE(
.sprite = exit ? "stain" : "stardust",
.pos = pos,
.color = color_mul_scalar(RGBA(0.75, 0.4 * frand(), 0.4, 0), 0.8),
.timeout = 20,
.angle = frand() * M_PI * 2,
.draw_rule = ScaleFade,
.args = { 0, 0, 0.25 + 1.5 * I },
.layer = LAYER_PLAYER_FOCUS,
);
}
static void reimu_dream_bullet_warp(Projectile *p, int t) {
if(creal(p->args[3]) > 0 /*global.plr.power / 100*/) {
return;
}
double p_long_side = max(creal(p->size), cimag(p->size));
complex half = 0.5 * (1 + I);
Rect p_bbox = { p->pos - p_long_side * half, p->pos + p_long_side * half };
FOR_EACH_GAP(gap) {
double a = (carg(-gap->pos0) - carg(p->args[0]));
if(fabs(a) < 2*M_PI/3) {
continue;
}
Rect gap_bbox, overlap;
complex gap_size = (GAP_LENGTH + I * GAP_WIDTH) * cexp(I*carg(gap->args[0]));
complex p0 = gap->pos - gap_size * 0.5;
complex p1 = gap->pos + gap_size * 0.5;
gap_bbox.top_left = min(creal(p0), creal(p1)) + I * min(cimag(p0), cimag(p1));
gap_bbox.bottom_right = max(creal(p0), creal(p1)) + I * max(cimag(p0), cimag(p1));
if(rect_rect_intersection(p_bbox, gap_bbox, true, &overlap)) {
complex o = (overlap.top_left + overlap.bottom_right) / 2;
double fract;
if(creal(gap_size) > cimag(gap_size)) {
fract = creal(o - gap_bbox.top_left) / creal(gap_size);
} else {
fract = cimag(o - gap_bbox.top_left) / cimag(gap_size);
}
Enemy *ngap = reimu_dream_gap_get_linked(gap);
o = ngap->pos + ngap->args[0] * GAP_LENGTH * (1 - fract - 0.5);
reimu_dream_spawn_warp_effect(gap->pos + gap->args[0] * GAP_LENGTH * (fract - 0.5), false);
reimu_dream_spawn_warp_effect(o, true);
p->args[0] = -cabs(p->args[0]) * ngap->pos0;
p->pos = o + p->args[0];
p->args[3] += 1;
}
}
}
static int reimu_dream_ofuda(Projectile *p, int t) {
if(t >= 0) {
reimu_dream_bullet_warp(p, t);
}
complex ov = p->args[0];
double s = cabs(ov);
p->args[0] *= clamp(s * (1.5 - t / 10.0), s*1.0, 1.5*s) / s;
int r = reimu_common_ofuda(p, t);
p->args[0] = ov;
return r;
}
static int reimu_dream_needle(Projectile *p, int t) {
if(t >= 0) {
reimu_dream_bullet_warp(p, t);
}
p->angle = carg(p->args[0]);
if(t < 0) {
return ACTION_ACK;
}
p->pos += p->args[0];
Color *c = color_mul(COLOR_COPY(&p->color), RGBA_MUL_ALPHA(0.75, 0.5, 1, 0.35));
c->a = 0;
PARTICLE(
.sprite_ptr = p->sprite,
.color = c,
.timeout = 12,
.pos = p->pos,
.args = { p->args[0] * 0.8, 0, 0+3*I },
.rule = linear,
.draw_rule = ScaleFade,
.layer = LAYER_PARTICLE_LOW,
.flags = PFLAG_NOREFLECT,
);
return ACTION_NONE;
}
static void reimu_dream_shot(Player *p) {
play_loop("generic_shot");
int dmg = 50;
if(!(global.frames % 6)) {
for(int i = -1; i < 2; i += 2) {
complex shot_dir = i * ((p->inputflags & INFLAG_FOCUS) ? 1 : I);
complex spread_dir = shot_dir * cexp(I*M_PI*0.5);
for(int j = -1; j < 2; j += 2) {
PROJECTILE(
.proto = pp_ofuda,
.pos = p->pos + 10 * j * spread_dir,
.color = RGBA_MUL_ALPHA(1, 1, 1, 0.5),
.rule = reimu_dream_ofuda,
.args = { -20.0 * shot_dir },
.type = PlrProj,
.damage = dmg,
.shader = "sprite_default",
);
}
}
}
}
static void reimu_dream_slave_visual(Enemy *e, int t, bool render) {
if(render) {
r_draw_sprite(&(SpriteParams) {
.sprite = "yinyang",
.shader = "sprite_yinyang",
.pos = {
creal(e->pos),
cimag(e->pos),
},
.rotation.angle = global.frames * -6 * DEG2RAD,
.color = RGB(0.95, 0.75, 1.0),
.scale.both = 0.5,
});
}
}
static int reimu_dream_slave(Enemy *e, int t) {
if(t < 0) {
return ACTION_ACK;
}
// double a = M_PI * psin(t * 0.1) + creal(e->args[0]) + M_PI/2;
double a = t * -0.1 + creal(e->args[0]) + M_PI/2;
complex ofs = e->pos0;
complex shotdir = e->args[1];
if(global.plr.inputflags & INFLAG_FOCUS) {
ofs = cimag(ofs) + I * creal(ofs);
shotdir = cimag(shotdir) + I * creal(shotdir);
}
if(t == 0) {
e->pos = global.plr.pos;
} else {
double x = creal(ofs);
double y = cimag(ofs);
complex tpos = global.plr.pos + x * sin(a) + y * I * cos(a);
e->pos += (tpos - e->pos) * 0.5;
}
if(player_should_shoot(&global.plr, true)) {
if(!(global.frames % 6)) {
PROJECTILE(
.proto = pp_needle2,
.pos = e->pos,
.color = RGBA_MUL_ALPHA(1, 1, 1, 0.35),
.rule = reimu_dream_needle,
.args = { 20.0 * shotdir },
.type = PlrProj,
.damage = 35,
.shader = "sprite_default",
);
}
}
return ACTION_NONE;
}
static Enemy* reimu_dream_spawn_slave(Player *plr, complex pos, complex a0, complex a1, complex a2, complex a3) {
Enemy *e = create_enemy_p(&plr->slaves, pos, ENEMY_IMMUNE, reimu_dream_slave_visual, reimu_dream_slave, a0, a1, a2, a3);
e->ent.draw_layer = LAYER_PLAYER_SLAVE;
return e;
}
static void reimu_dream_kill_slaves(EnemyList *slaves) {
for(Enemy *e = slaves->first, *next; e; e = next) {
next = e->next;
if(e->logic_rule == reimu_dream_slave) {
delete_enemy(slaves, e);
}
}
}
static void reimu_dream_respawn_slaves(Player *plr, short npow) {
reimu_dream_kill_slaves(&plr->slaves);
int p = 2 * (npow / 100);
double s = 1;
for(int i = 0; i < p; ++i, s = -s) {
reimu_dream_spawn_slave(plr, 48+32*I, ((double)i/p)*(M_PI*2), s*I, 0, 0);
}
}
static void reimu_dream_power(Player *p, short npow) {
if(p->power / 100 != npow / 100) {
reimu_dream_respawn_slaves(p, npow);
}
}
static Enemy* reimu_dream_spawn_gap(Player *plr, complex pos, complex a0, complex a1, complex a2, complex a3) {
Enemy *gap = create_enemy_p(&plr->slaves, pos, ENEMY_IMMUNE, NULL, reimu_dream_gap, a0, a1, a2, a3);
gap->ent.draw_layer = LAYER_PLAYER_SLAVE;
return gap;
}
static void reimu_dream_think(Player *plr) {
if(player_is_bomb_active(plr)) {
global.shake_view_fade = max(global.shake_view_fade, 5);
}
}
static void reimu_dream_init(Player *plr) {
Enemy* left = reimu_dream_spawn_gap(plr, -1, I, 0, 0, 0);
Enemy* top = reimu_dream_spawn_gap(plr, -I, 1, 0, 0, 0);
Enemy* right = reimu_dream_spawn_gap(plr, 1, I, 0, 0, 0);
Enemy* bottom = reimu_dream_spawn_gap(plr, I, 1, 0, 0, 0);
reimu_dream_gap_link(top, left);
reimu_dream_gap_link(bottom, right);
gap_renderer= create_enemy_p(&plr->slaves, 0, ENEMY_IMMUNE, reimu_dream_gap_renderer_visual, reimu_dream_gap_renderer, 0, 0, 0, 0);
gap_renderer->ent.draw_layer = LAYER_PLAYER_FOCUS;
int idx = 0;
FOR_EACH_GAP(gap) {
gap->args[3] = creal(gap->args[3]) + I*idx++;
}
reimu_dream_respawn_slaves(plr, plr->power);
reimu_common_bomb_buffer_init();
}
PlayerMode plrmode_reimu_b = {
.name = "Dream Sign",
.character = &character_reimu,
.dialog = &dialog_reimu,
.shot_mode = PLR_SHOT_REIMU_DREAM,
.procs = {
.property = reimu_common_property,
.bomb = reimu_dream_bomb,
.bombbg = reimu_dream_bomb_bg,
.shot = reimu_dream_shot,
.power = reimu_dream_power,
.init = reimu_dream_init,
.preload = reimu_dream_preload,
.think = reimu_dream_think,
},
};

View file

@ -79,6 +79,10 @@ void youmu_common_shot(Player *plr) {
}
void youmu_common_bombbg(Player *plr) {
if(!player_is_bomb_active(plr)) {
return;
}
float t = player_get_bomb_progress(&global.plr, NULL);
float fade = 1;

View file

@ -15,28 +15,7 @@
#include "youmu.h"
static complex youmu_homing_target(complex org, complex fallback) {
double mindst = DBL_MAX;
complex target = fallback;
if(global.boss && boss_is_vulnerable(global.boss)) {
target = global.boss->pos;
mindst = cabs(target - org);
}
for(Enemy *e = global.enemies.first; e; e = e->next) {
if(e->hp == ENEMY_IMMUNE){
continue;
}
double dst = cabs(e->pos - org);
if(dst < mindst) {
mindst = dst;
target = e->pos;
}
}
return target;
return plrutil_homing_target(org, fallback);
}
static void youmu_homing_draw_common(Projectile *p, float clrfactor, float scale, float alpha) {

View file

@ -232,6 +232,7 @@ static Projectile* _create_projectile(ProjArgs *args) {
p->ent.draw_func = ent_draw_projectile;
projectile_set_prototype(p, args->proto);
// p->collision_size *= 10;
// p->size *= 5;
@ -245,6 +246,7 @@ static Projectile* _create_projectile(ProjArgs *args) {
// But in that case, code that uses this function's return value must be careful to not dereference a NULL pointer.
proj_call_rule(p, EVENT_BIRTH);
return alist_append(args->dest, p);
}
@ -368,9 +370,9 @@ void calc_projectile_collision(Projectile *p, ProjCollisionResult *out_col) {
void apply_projectile_collision(ProjectileList *projlist, Projectile *p, ProjCollisionResult *col, ProjectileListInterface *out_list_pointers) {
switch(col->type) {
case PCOL_NONE: {
case PCOL_NONE:
case PCOL_VOID:
break;
}
case PCOL_PLAYER_GRAZE: {
if(p->flags & PFLAG_GRAZESPAM) {
@ -386,25 +388,17 @@ void apply_projectile_collision(ProjectileList *projlist, Projectile *p, ProjCol
}
case PCOL_ENTITY: {
if(ent_damage(col->entity, &col->damage) == DMG_RESULT_OK) {
player_register_damage(&global.plr, col->entity, &col->damage);
}
ent_damage(col->entity, &col->damage);
break;
}
case PCOL_VOID: {
break;
}
default: {
log_fatal("Invalid collision type %x", col->type);
}
default:
UNREACHABLE;
}
if(col->fatal) {
delete_projectile(projlist, p, out_list_pointers);
} else {
} else if(out_list_pointers) {
*&out_list_pointers->list_interface = p->list_interface;
}
}

View file

@ -5,5 +5,9 @@ PP_PLAYER(marisa, 15, 50)
PP_PLAYER(maristar, 25, 25)
PP_PLAYER(youhoming, 50, 50)
PP_PLAYER(youmu, 16, 24)
PP_PLAYER(ofuda, 16, 26)
PP_PLAYER(needle, 14, 72)
PP_PLAYER(needle2, 7, 65)
PP_PLAYER(hakurei_seal,16, 39)
#undef PP_PLAYER

View file

@ -23,6 +23,13 @@
#define OBJPOOLSTATS_DEFAULT 0
#endif
typedef struct CustomFramebuffer {
LIST_INTERFACE(struct CustomFramebuffer);
Framebuffer fb;
float scale;
StageFBPair scaling_base;
} CustomFramebuffer;
static struct {
struct {
ShaderProgram *shader;
@ -37,6 +44,7 @@ static struct {
PostprocessShader *viewport_pp;
FBPair fb_pairs[NUM_FBPAIRS];
CustomFramebuffer *custom_fbs;
bool framerate_graphs;
bool objpool_stats;
@ -82,6 +90,21 @@ static void update_fb_size(StageFBPair fb_id) {
set_fb_size(fb_id, &w, &h);
fbpair_resize_all(stagedraw.fb_pairs + fb_id, w, h);
fbpair_viewport(stagedraw.fb_pairs + fb_id, 0, 0, w, h);
if(fb_id != FBPAIR_FG_AUX) {
for(CustomFramebuffer *cfb = stagedraw.custom_fbs; cfb; cfb = cfb->next) {
if(cfb->scaling_base == fb_id) {
int sw = w * cfb->scale;
int sh = h * cfb->scale;
for(uint i = 0; i < FRAMEBUFFER_MAX_ATTACHMENTS; ++i) {
fbutil_resize_attachment(&cfb->fb, i, sw, sh);
}
r_framebuffer_viewport(&cfb->fb, 0, 0, sw, sh);
}
}
}
}
static bool stage_draw_event(SDL_Event *e, void *arg) {
@ -167,6 +190,58 @@ static void stage_draw_setup_framebuffers(void) {
fbpair_viewport(stagedraw.fb_pairs + FBPAIR_BG, 0, 0, bg_width, bg_height);
}
static Framebuffer* add_custom_framebuffer(StageFBPair fbtype, float scale_factor, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) {
CustomFramebuffer *cfb = calloc(1, sizeof(*cfb));
list_push(&stagedraw.custom_fbs, cfb);
FBAttachmentConfig cfg[num_attachments];
memcpy(cfg, attachments, sizeof(cfg));
int width, height;
set_fb_size(fbtype, &width, &height);
cfb->scale = scale_factor;
width *= scale_factor;
height *= scale_factor;
for(uint i = 0; i < num_attachments; ++i) {
cfg[i].tex_params.width = width;
cfg[i].tex_params.height = height;
}
r_framebuffer_create(&cfb->fb);
fbutil_create_attachments(&cfb->fb, num_attachments, cfg);
r_framebuffer_viewport(&cfb->fb, 0, 0, width, height);
r_state_push();
r_framebuffer(&cfb->fb);
r_clear_color4(0, 0, 0, 0);
r_clear(CLEAR_ALL);
r_state_pop();
return &cfb->fb;
}
Framebuffer* stage_add_foreground_framebuffer(float scale_factor, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) {
return add_custom_framebuffer(FBPAIR_FG, scale_factor, num_attachments, attachments);
}
Framebuffer* stage_add_background_framebuffer(float scale_factor, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) {
return add_custom_framebuffer(FBPAIR_BG, scale_factor, num_attachments, attachments);
}
static void stage_draw_destroy_framebuffers(void) {
for(uint i = 0; i < NUM_FBPAIRS; ++i) {
fbpair_destroy(stagedraw.fb_pairs + i);
}
for(CustomFramebuffer *cfb = stagedraw.custom_fbs, *next; cfb; cfb = next) {
next = cfb->next;
fbutil_destroy_attachments(&cfb->fb);
r_framebuffer_destroy(&cfb->fb);
free(list_unlink(&stagedraw.custom_fbs, cfb));
}
}
void stage_draw_init(void) {
preload_resources(RES_POSTPROCESS, RESF_OPTIONAL,
"viewport",
@ -234,10 +309,7 @@ void stage_draw_init(void) {
void stage_draw_shutdown(void) {
events_unregister_handler(stage_draw_event);
for(uint i = 0; i < NUM_FBPAIRS; ++i) {
fbpair_destroy(stagedraw.fb_pairs + i);
}
stage_draw_destroy_framebuffers();
}
FBPair* stage_get_fbpair(StageFBPair id) {
@ -622,7 +694,9 @@ void stage_draw_scene(StageInfo *stage) {
r_shader_standard();
// draw bomb background
if(global.plr.mode->procs.bombbg && player_is_bomb_active(&global.plr)) {
// FIXME: we need a more flexible and consistent way for entities to hook
// into the various stages of scene drawing code.
if(global.plr.mode->procs.bombbg /*&& player_is_bomb_active(&global.plr)*/) {
global.plr.mode->procs.bombbg(&global.plr);
}
} else if(!key_nobg) {

View file

@ -27,3 +27,5 @@ void stage_draw_scene(StageInfo *stage);
bool stage_should_draw_particle(Projectile *p);
FBPair* stage_get_fbpair(StageFBPair id) attr_returns_nonnull;
Framebuffer* stage_add_foreground_framebuffer(float scale_factor, uint num_attachments, FBAttachmentConfig attachments[num_attachments]);
Framebuffer* stage_add_background_framebuffer(float scale_factor, uint num_attachments, FBAttachmentConfig attachments[num_attachments]);

View file

@ -11,47 +11,20 @@
#include "fbpair.h"
#include "global.h"
#include "util.h"
#include "util/graphics.h"
static void fbpair_create_fb(Framebuffer *fb, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) {
r_framebuffer_create(fb);
for(uint i = 0; i < num_attachments; ++i) {
Texture *tex = calloc(1, sizeof(Texture));
r_texture_create(tex, &attachments[i].tex_params);
r_framebuffer_attach(fb, tex, 0, attachments[i].attachment);
}
fbutil_create_attachments(fb, num_attachments, attachments);
}
static void fbpair_destroy_fb(Framebuffer *fb) {
for(uint i = 0; i < FRAMEBUFFER_MAX_ATTACHMENTS; ++i) {
Texture *tex = r_framebuffer_get_attachment(fb, i);
if(tex != NULL) {
r_texture_destroy(tex);
free(tex);
}
}
fbutil_destroy_attachments(fb);
r_framebuffer_destroy(fb);
}
static void fbpair_resize_fb(Framebuffer *fb, FramebufferAttachment attachment, uint width, uint height) {
Texture *tex = r_framebuffer_get_attachment(fb, attachment);
if(tex == NULL || (tex->w == width && tex->h == height)) {
return;
}
// TODO: We could render a rescaled version of the old texture contents here
TextureParams params;
r_texture_get_params(tex, &params);
r_texture_destroy(tex);
params.width = width;
params.height = height;
params.mipmaps = 0; // FIXME
r_texture_create(tex, &params);
r_framebuffer_attach(fb, tex, 0, attachment);
fbutil_resize_attachment(fb, attachment, width, height);
}
void fbpair_create(FBPair *pair, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) {

View file

@ -114,3 +114,42 @@ void draw_framebuffer_tex(Framebuffer *fb, double width, double height) {
r_cull(cull_saved);
}
void fbutil_create_attachments(Framebuffer *fb, uint num_attachments, FBAttachmentConfig attachments[num_attachments]) {
for(uint i = 0; i < num_attachments; ++i) {
Texture *tex = calloc(1, sizeof(Texture));
log_debug("%i %i", attachments[i].tex_params.width, attachments[i].tex_params.height);
r_texture_create(tex, &attachments[i].tex_params);
r_framebuffer_attach(fb, tex, 0, attachments[i].attachment);
}
}
void fbutil_destroy_attachments(Framebuffer *fb) {
for(uint i = 0; i < FRAMEBUFFER_MAX_ATTACHMENTS; ++i) {
Texture *tex = r_framebuffer_get_attachment(fb, i);
if(tex != NULL) {
r_texture_destroy(tex);
free(tex);
}
}
}
void fbutil_resize_attachment(Framebuffer *fb, FramebufferAttachment attachment, uint width, uint height) {
Texture *tex = r_framebuffer_get_attachment(fb, attachment);
if(tex == NULL || (tex->w == width && tex->h == height)) {
return;
}
// TODO: We could render a rescaled version of the old texture contents here
TextureParams params;
r_texture_get_params(tex, &params);
r_texture_destroy(tex);
params.width = width;
params.height = height;
params.mipmaps = 0; // FIXME
r_texture_create(tex, &params);
r_framebuffer_attach(fb, tex, 0, attachment);
}

View file

@ -16,3 +16,7 @@ void colorfill(float r, float g, float b, float a);
void fade_out(float f);
void draw_stars(int x, int y, int numstars, int numfrags, int maxstars, int maxfrags, float alpha, float star_width);
void draw_framebuffer_tex(Framebuffer *fb, double width, double height);
void fbutil_create_attachments(Framebuffer *fb, uint num_attachments, FBAttachmentConfig attachments[num_attachments]);
void fbutil_destroy_attachments(Framebuffer *fb);
void fbutil_resize_attachment(Framebuffer *fb, FramebufferAttachment attachment, uint width, uint height);