diff --git a/atlas/meson.build b/atlas/meson.build index 8427b14ca1..cc53ed52e8 100644 --- a/atlas/meson.build +++ b/atlas/meson.build @@ -43,6 +43,7 @@ atlases = [ ['common_ui', ['--width=1024', '--height=1024']], ['huge', []], ['portraits', ['--width=4096', '--height=4096']], + ['temp_stagex', []], ] atlas_profiles = [ diff --git a/atlas/temp_stagex/stagex/yumemi_slaves/one_core.webp b/atlas/temp_stagex/stagex/yumemi_slaves/one_core.webp new file mode 100644 index 0000000000..31ce795e07 Binary files /dev/null and b/atlas/temp_stagex/stagex/yumemi_slaves/one_core.webp differ diff --git a/atlas/temp_stagex/stagex/yumemi_slaves/one_frame.webp b/atlas/temp_stagex/stagex/yumemi_slaves/one_frame.webp new file mode 100644 index 0000000000..dd5b8beede Binary files /dev/null and b/atlas/temp_stagex/stagex/yumemi_slaves/one_frame.webp differ diff --git a/atlas/temp_stagex/stagex/yumemi_slaves/one_outer.webp b/atlas/temp_stagex/stagex/yumemi_slaves/one_outer.webp new file mode 100644 index 0000000000..1653cb824d Binary files /dev/null and b/atlas/temp_stagex/stagex/yumemi_slaves/one_outer.webp differ diff --git a/atlas/temp_stagex/stagex/yumemi_slaves/zero_core.webp b/atlas/temp_stagex/stagex/yumemi_slaves/zero_core.webp new file mode 100644 index 0000000000..2ca2b4b0a4 Binary files /dev/null and b/atlas/temp_stagex/stagex/yumemi_slaves/zero_core.webp differ diff --git a/atlas/temp_stagex/stagex/yumemi_slaves/zero_frame.webp b/atlas/temp_stagex/stagex/yumemi_slaves/zero_frame.webp new file mode 100644 index 0000000000..fa6bc9580f Binary files /dev/null and b/atlas/temp_stagex/stagex/yumemi_slaves/zero_frame.webp differ diff --git a/atlas/temp_stagex/stagex/yumemi_slaves/zero_outer.webp b/atlas/temp_stagex/stagex/yumemi_slaves/zero_outer.webp new file mode 100644 index 0000000000..23e881517a Binary files /dev/null and b/atlas/temp_stagex/stagex/yumemi_slaves/zero_outer.webp differ diff --git a/resources/00-taisei.pkgdir/bgm/stagex.opus b/resources/00-taisei.pkgdir/bgm/stagex.opus new file mode 100644 index 0000000000..929cd514d2 Binary files /dev/null and b/resources/00-taisei.pkgdir/bgm/stagex.opus differ diff --git a/resources/00-taisei.pkgdir/bgm/stagexboss.opus b/resources/00-taisei.pkgdir/bgm/stagexboss.opus new file mode 100644 index 0000000000..5da8ad9b86 Binary files /dev/null and b/resources/00-taisei.pkgdir/bgm/stagexboss.opus differ diff --git a/resources/00-taisei.pkgdir/gfx/atlas_temp_stagex_0.tex b/resources/00-taisei.pkgdir/gfx/atlas_temp_stagex_0.tex new file mode 100644 index 0000000000..cbbcf1b316 --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/atlas_temp_stagex_0.tex @@ -0,0 +1,7 @@ +# Autogenerated by the atlas packer, do not modify + +source = res/gfx/atlas_temp_stagex_0.webp + +# -- Pasted from the global override file -- + +anisotropy = 1 diff --git a/resources/00-taisei.pkgdir/gfx/atlas_temp_stagex_0.webp b/resources/00-taisei.pkgdir/gfx/atlas_temp_stagex_0.webp new file mode 100644 index 0000000000..8bade63b12 Binary files /dev/null and b/resources/00-taisei.pkgdir/gfx/atlas_temp_stagex_0.webp differ diff --git a/resources/00-taisei.pkgdir/gfx/boss/yumemi.ani b/resources/00-taisei.pkgdir/gfx/boss/yumemi.ani new file mode 100644 index 0000000000..e188b9a4d0 --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/boss/yumemi.ani @@ -0,0 +1,3 @@ +@sprite_count = 1 + +main = d1 0 diff --git a/resources/00-taisei.pkgdir/gfx/boss/yumemi.frame0000.spr b/resources/00-taisei.pkgdir/gfx/boss/yumemi.frame0000.spr new file mode 100644 index 0000000000..04d1872388 --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/boss/yumemi.frame0000.spr @@ -0,0 +1,4 @@ +texture = boss/yumemi_placeholder + +w = 79.5 +h = 110 diff --git a/resources/00-taisei.pkgdir/gfx/boss/yumemi_placeholder.webp b/resources/00-taisei.pkgdir/gfx/boss/yumemi_placeholder.webp new file mode 100644 index 0000000000..d42103c85a Binary files /dev/null and b/resources/00-taisei.pkgdir/gfx/boss/yumemi_placeholder.webp differ diff --git a/resources/00-taisei.pkgdir/gfx/stageex/bg.webp b/resources/00-taisei.pkgdir/gfx/stagex/bg.webp similarity index 100% rename from resources/00-taisei.pkgdir/gfx/stageex/bg.webp rename to resources/00-taisei.pkgdir/gfx/stagex/bg.webp diff --git a/resources/00-taisei.pkgdir/gfx/stagex/bg_binary.tex b/resources/00-taisei.pkgdir/gfx/stagex/bg_binary.tex new file mode 100644 index 0000000000..556ce088f9 --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/stagex/bg_binary.tex @@ -0,0 +1,2 @@ + +format = r8 diff --git a/resources/00-taisei.pkgdir/gfx/stageex/bg_binary.webp b/resources/00-taisei.pkgdir/gfx/stagex/bg_binary.webp similarity index 100% rename from resources/00-taisei.pkgdir/gfx/stageex/bg_binary.webp rename to resources/00-taisei.pkgdir/gfx/stagex/bg_binary.webp diff --git a/resources/00-taisei.pkgdir/gfx/stageex/code.num_slices b/resources/00-taisei.pkgdir/gfx/stagex/code.num_slices similarity index 100% rename from resources/00-taisei.pkgdir/gfx/stageex/code.num_slices rename to resources/00-taisei.pkgdir/gfx/stagex/code.num_slices diff --git a/resources/00-taisei.pkgdir/gfx/stageex/code.tex b/resources/00-taisei.pkgdir/gfx/stagex/code.tex similarity index 100% rename from resources/00-taisei.pkgdir/gfx/stageex/code.tex rename to resources/00-taisei.pkgdir/gfx/stagex/code.tex diff --git a/resources/00-taisei.pkgdir/gfx/stageex/code.webp b/resources/00-taisei.pkgdir/gfx/stagex/code.webp similarity index 100% rename from resources/00-taisei.pkgdir/gfx/stageex/code.webp rename to resources/00-taisei.pkgdir/gfx/stagex/code.webp diff --git a/resources/00-taisei.pkgdir/gfx/stageex/dissolve_mask.webp b/resources/00-taisei.pkgdir/gfx/stagex/dissolve_mask.webp similarity index 100% rename from resources/00-taisei.pkgdir/gfx/stageex/dissolve_mask.webp rename to resources/00-taisei.pkgdir/gfx/stagex/dissolve_mask.webp diff --git a/resources/00-taisei.pkgdir/gfx/stagex/hex_tiles.tex b/resources/00-taisei.pkgdir/gfx/stagex/hex_tiles.tex new file mode 100644 index 0000000000..788ca23015 --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/stagex/hex_tiles.tex @@ -0,0 +1,2 @@ + +format = rg8 diff --git a/resources/00-taisei.pkgdir/gfx/stagex/hex_tiles.webp b/resources/00-taisei.pkgdir/gfx/stagex/hex_tiles.webp new file mode 100644 index 0000000000..3ef3f94978 Binary files /dev/null and b/resources/00-taisei.pkgdir/gfx/stagex/hex_tiles.webp differ diff --git a/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_core.spr b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_core.spr new file mode 100644 index 0000000000..c72631923f --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_core.spr @@ -0,0 +1,7 @@ +# Autogenerated by the atlas packer, do not modify + +texture = atlas_temp_stagex_0 +region_x = 0.00177304964539007 +region_y = 0.01063829787234043 +region_w = 0.16312056737588654 +region_h = 0.97872340425531912 diff --git a/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_frame.spr b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_frame.spr new file mode 100644 index 0000000000..3326a7fd94 --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_frame.spr @@ -0,0 +1,7 @@ +# Autogenerated by the atlas packer, do not modify + +texture = atlas_temp_stagex_0 +region_x = 0.16843971631205673 +region_y = 0.01063829787234043 +region_w = 0.16312056737588654 +region_h = 0.97872340425531912 diff --git a/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_outer.spr b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_outer.spr new file mode 100644 index 0000000000..34af420094 --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/one_outer.spr @@ -0,0 +1,7 @@ +# Autogenerated by the atlas packer, do not modify + +texture = atlas_temp_stagex_0 +region_x = 0.33510638297872342 +region_y = 0.01063829787234043 +region_w = 0.16312056737588654 +region_h = 0.97872340425531912 diff --git a/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_core.spr b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_core.spr new file mode 100644 index 0000000000..cafebf040d --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_core.spr @@ -0,0 +1,7 @@ +# Autogenerated by the atlas packer, do not modify + +texture = atlas_temp_stagex_0 +region_x = 0.50177304964539005 +region_y = 0.01063829787234043 +region_w = 0.16312056737588654 +region_h = 0.97872340425531912 diff --git a/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_frame.spr b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_frame.spr new file mode 100644 index 0000000000..adf36af864 --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_frame.spr @@ -0,0 +1,7 @@ +# Autogenerated by the atlas packer, do not modify + +texture = atlas_temp_stagex_0 +region_x = 0.66843971631205679 +region_y = 0.01063829787234043 +region_w = 0.16312056737588654 +region_h = 0.97872340425531912 diff --git a/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_outer.spr b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_outer.spr new file mode 100644 index 0000000000..edd725ec3c --- /dev/null +++ b/resources/00-taisei.pkgdir/gfx/stagex/yumemi_slaves/zero_outer.spr @@ -0,0 +1,7 @@ +# Autogenerated by the atlas packer, do not modify + +texture = atlas_temp_stagex_0 +region_x = 0.83510638297872342 +region_y = 0.01063829787234043 +region_w = 0.16312056737588654 +region_h = 0.97872340425531912 diff --git a/resources/00-taisei.pkgdir/shader/bombshield.frag.glsl b/resources/00-taisei.pkgdir/shader/bombshield.frag.glsl new file mode 100644 index 0000000000..5000357206 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/bombshield.frag.glsl @@ -0,0 +1,55 @@ +#version 330 core + +#include "lib/util.glslh" +#include "interface/standard.glslh" + +UNIFORM(1) sampler2D shell_tex; +UNIFORM(2) sampler2D grid_tex; +UNIFORM(3) vec2 grid_aspect; +UNIFORM(4) sampler2D code_tex; +UNIFORM(5) vec2 code_aspect; + +UNIFORM(6) float time; +UNIFORM(7) float alpha; + +vec2 edts(vec2 p) { + vec2 uv = p * p; + vec2 s = 2.0 + uv - uv.yx; + vec2 t = (2.0 * sqrt(2.0)) * p; + vec2 t1 = s + t; + vec2 t2 = s - t; + // t1 = abs(t1); t2 = abs(t2); + return 0.5 * (sqrt(t1) - sqrt(t2)); +} + +void main(void) { + vec2 uv = texCoord; + float f = distance(uv, vec2(0.5)); + + if(f > 0.5) { + discard; + } + + f = 1.0 - 2.0 * f; + float t = time; + vec2 warped_uv = edts((2.0 - pow(f, 1.0/3.0)) * (uv - 0.5)) + 0.5; + + vec2 grid = texture(grid_tex, grid_aspect * (warped_uv + vec2(t * 2.0, 0.0))).rg; + + vec3 c0 = vec3(0.85 * (warped_uv.y), 0.5, 1.0 - 0.5 * warped_uv.y); + c0 = pow(texture(shell_tex, warped_uv + vec2(t * 1.3, t * 0.73)).rgb, 1.0 - c0); + + vec3 c1 = texture(code_tex, code_aspect * (warped_uv + vec2(t * 2.1, t * -0.42))).rrr; + c1 *= vec3(0.2, 0.7, 1.0); + + float ring = clamp(smoothstep(0.0, 0.03, f) * (1.0 - f), 0.0, 1.0); + float bg = smoothstep(0.0, 0.1, f); + float h = smoothmax(bg * grid.r, ring, grid.r * (10 - 8 * ((1 - f * f) * alpha)) + ring); + + float alpha2 = alpha * alpha; + + c0 *= h; + c1 *= grid.g * alpha2; + + fragColor = vec4((c0 + c1) * alpha2, 0.6 * bg * alpha); +} diff --git a/resources/00-taisei.pkgdir/shader/bombshield.prog b/resources/00-taisei.pkgdir/shader/bombshield.prog new file mode 100644 index 0000000000..995be5f366 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/bombshield.prog @@ -0,0 +1,2 @@ + +objects = standardnotex.vert bombshield.frag diff --git a/resources/00-taisei.pkgdir/shader/extra_bg.glslh b/resources/00-taisei.pkgdir/shader/extra_bg.glslh index 412da8149d..b3dca89b69 100644 --- a/resources/00-taisei.pkgdir/shader/extra_bg.glslh +++ b/resources/00-taisei.pkgdir/shader/extra_bg.glslh @@ -1,4 +1,6 @@ +#include "lib/util.glslh" + vec2 code_transform_uv(float num_segs, float inv_num_segs, vec2 uv) { uv *= inv_num_segs; float cur_segment = floor(mod(uv.y, num_segs)) * inv_num_segs; @@ -53,10 +55,25 @@ vec4 sample_background( code_uv.y += t * 0.2; vec4 background = texture(sampler_bg, bg_uv); - vec4 background_binary = texture(sampler_bg_binary, bg_binary_uv); + float background_binary = texture(sampler_bg_binary, bg_binary_uv).r; vec4 code = sample_code(sampler_code, code_tex_params, code_uv) * r; - background = background * background_binary + code; + background = background * background_binary; + + vec3 hsv = rgb2hsv(background.rgb); + hsv.y *= 0.5 + 0.5 * uv.y; + hsv.z *= 0.6 + 0.4 * uv.y; + background.rgb = hsv2rgb(hsv); + + hsv = rgb2hsv(code.rgb); + float xf = 1.0 - 4.0 * (uv.x - uv.x * uv.x); + float yf = 1.0 - 4.0 * (uv.y - uv.y * uv.y); + hsv.x += 0.1 * yf - 0.1 * (xf * xf * xf * xf) * (1.0 - yf * yf); + hsv.y *= 0.5 + 0.5 * (1 - pow(1 - uv.y, 2.0)); + code.rgb = hsv2rgb(hsv); + code.rgb *= (1.0 - yf * yf); + + background += code; background.rgb = pow(background.rgb, mix(1 - vec3(1.0, 0.5, 0.3), vec3(1), r)); return background; diff --git a/resources/00-taisei.pkgdir/shader/extra_glitch.frag.glsl b/resources/00-taisei.pkgdir/shader/extra_glitch.frag.glsl new file mode 100644 index 0000000000..6e36aeb308 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/extra_glitch.frag.glsl @@ -0,0 +1,95 @@ +#version 330 core + +#include "lib/render_context.glslh" +#include "interface/standard.glslh" + +UNIFORM(1) vec3 times; +UNIFORM(2) sampler2D mask; + +const vec3 cutoffs = vec3(0.53, 0.56, 0.51); +const vec2 grid = vec2(8.0, 16.0); +const vec3 waverectfactors = vec3(7.0); + +float hash(vec2 co) { + float a = 12.9898; + float b = 78.233; + float c = 43758.5453; + float dt = dot(co, vec2(a, b)); + float sn = mod(dt, 3.14); + return fract(sin(sn) * c); +} + +float wave(float x) { + float y = sin(x) + cos(3.0*x) + sin(5.0*x) + cos(7.0*x); + float m = 3.0; + return clamp((m + y) / (2.0 * m), 0.0, 1.0); +} + +vec3 wave(vec3 v) { + return vec3( + wave(v.x), + wave(v.y), + wave(v.z) + ); +} + +float rect(float x, float g) { + return round(g * x) / g; +} + +vec2 rect(vec2 v, vec2 g) { + return vec2( + rect(v.x, g.x), + rect(v.y, g.y) + ); +} + +vec3 rect(vec3 v, vec3 g) { + return vec3( + rect(v.x, g.x), + rect(v.y, g.y), + rect(v.z, g.z) + ); +} + +vec3 glitch(vec2 uv, vec3 time, vec2 grid, vec3 cutoffs, vec3 waverectfactors) { + uv = rect(uv, grid); + vec3 t = time; + vec3 v = wave(t); + vec3 mstatic = step(cutoffs, v); + vec3 mchaotic = step(cutoffs, v * v); + v = rect(v, waverectfactors); + vec3 mask = vec3( + hash(uv + t.r * mchaotic.r), + hash(uv + t.g * mchaotic.g), + hash(uv + t.b * mchaotic.b) + ); + mask = step(mstatic * v, mask); + mask = (1.0 - mask); + // mask *= v; + return mask; +} + +void main(void) { + vec2 uv = texCoord; + + vec4 texNormal = texture(tex, uv); + vec3 m = texture(mask, uv).rgb; + float a = m.r + m.g + m.b; + + if(a == 0) { + fragColor = texNormal; + return; + } + + vec3 k = glitch(uv, times, grid, cutoffs, waverectfactors); + vec3 o = k * a * 0.25; + + vec3 texGlitched; + texGlitched.r = texture(tex, uv - o.r / grid).r; + texGlitched.g = texture(tex, uv - o.g / grid).g; + texGlitched.b = texture(tex, uv - o.b / grid).b; + + fragColor.rgb = mix(texNormal.rgb, texGlitched, k); + fragColor.a = texNormal.a; +} diff --git a/resources/00-taisei.pkgdir/shader/extra_glitch.prog b/resources/00-taisei.pkgdir/shader/extra_glitch.prog new file mode 100644 index 0000000000..85c96ed25b --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/extra_glitch.prog @@ -0,0 +1,2 @@ + +objects = extra_glitch.frag standard.vert diff --git a/resources/00-taisei.pkgdir/shader/extra_tower_apply_mask.frag.glsl b/resources/00-taisei.pkgdir/shader/extra_tower_apply_mask.frag.glsl index 982d03286e..c91c259d6a 100644 --- a/resources/00-taisei.pkgdir/shader/extra_tower_apply_mask.frag.glsl +++ b/resources/00-taisei.pkgdir/shader/extra_tower_apply_mask.frag.glsl @@ -11,7 +11,7 @@ UNIFORM(3) sampler2D background_tex; UNIFORM(4) sampler2D background_binary_tex; UNIFORM(5) sampler2D code_tex; UNIFORM(6) vec4 code_tex_params; // (aspect_x, aspect_y, num_segs, 1/num_segs) -UNIFORM(7) vec2 dissolve; // (dissolve, dissolve^2) +UNIFORM(7) float global_dissolve; UNIFORM(8) float time; vec4 sample_background(vec2 uv, float r, float t) { @@ -31,7 +31,11 @@ void main(void) { float depthval = texture(depth_tex, texCoord).r; vec4 masks = texture(mask_tex, texCoord); - float global_dissolve_phase = smoothstep(dissolve.y, dissolve.x, masks.a); + float q = 1 - smoothstep(0.8, 1, depthval); + q = 1 - sqrt(q); + float d = 1 - clamp(global_dissolve - 3.5 * q, 0, 1); + + float global_dissolve_phase = smoothstep(d*d, 1.5 * d, masks.a); masks.rg = mix(masks.rg, vec2(1, 0), global_dissolve_phase); if(masks.g == 1) { @@ -41,14 +45,22 @@ void main(void) { } float maskval = masks.r; - vec4 background = sample_background(texCoord, maskval, time); maskval = 0.5 + 0.5 * sin(pi * (maskval + 1.42 * smoothstep(0.5, 1.0, depthval) * masks.b)); + vec4 background = sample_background(texCoord, + mix(0.2 + maskval*0.8, 1, max(global_dissolve_phase, smoothstep(0.95, 1, depthval))), + time + 0.5 * (masks.r - 1) + d - masks.g + ); maskval = smoothstep(0, 1, maskval + 0.5); maskval = smoothstep(0.75, 1.0, maskval); maskval = mix(maskval, 1, masks.g); + maskval = mix(maskval, 0, global_dissolve_phase); + if(depthval < 1) { + maskval *= smoothstep(1, 0.995, masks.r) * masks.g; + } + vec4 result = background; result = alphaCompose(result, tower * maskval); depthval = mix(depthval, 0, global_dissolve_phase); diff --git a/resources/00-taisei.pkgdir/shader/extra_tower_mask.frag.glsl b/resources/00-taisei.pkgdir/shader/extra_tower_mask.frag.glsl index 188c37b582..93cba29839 100644 --- a/resources/00-taisei.pkgdir/shader/extra_tower_mask.frag.glsl +++ b/resources/00-taisei.pkgdir/shader/extra_tower_mask.frag.glsl @@ -1,22 +1,28 @@ #version 330 core -#include "interface/tower_light.glslh" +#include "lib/render_context.glslh" +#include "interface/extra_tower_mask.glslh" #include "lib/util.glslh" -UNIFORM(24) float time; -UNIFORM(25) sampler2D tex2; -UNIFORM(26) float dissolve; - void main(void) { - vec4 texel2 = texture(tex2, texCoord); - vec4 texel = texture(tex, texCoord); + // Unfortunately this produces severe artifacts if computed in the vertex shader + vec2 uv; + uv.y = distance(worldPos.xyz, vec3(0, 0, worldPos.z)) / 10 + worldPos.z / 120; + uv.x = atan(worldPos.x, worldPos.y) + worldPos.z * 0.04; + uv.x = 0.5 + 0.5 * sin(uv.x); + + vec4 s_mask = texture(tex_mask, uv); + vec4 s_noise = texture(tex_noise, uv); + vec4 s_mod = texture(tex_mod, texCoord); + + s_noise.r = fract(s_noise.r + s_mod.r); - float m = texel.r; - m = 0.5 + 0.5 * sin(pi*m + time); - m = 0.5 + 0.5 * sin(2*pi*m + time); + float m = s_noise.r; + m = 0.5 + 0.5 * sin( pi*m + time + s_mod.r); + m = 0.5 + 0.5 * sin(2*pi*m + time - s_mod.r); - float dissolve_phase = smoothstep(dissolve * dissolve, dissolve, texel.r); - float d = mix(1.0 - texel2.r, 1.0, dissolve_phase); + float dissolve_phase = smoothstep(dissolve * dissolve, dissolve, s_noise.r); + float d = mix(1.0 - s_mask.r, 1.0, dissolve_phase); - fragColor = vec4(m, d, texel2.g, texel.r); + fragColor = vec4(m, d, s_mask.g, s_noise.r); } diff --git a/resources/00-taisei.pkgdir/shader/extra_tower_mask.vert.glsl b/resources/00-taisei.pkgdir/shader/extra_tower_mask.vert.glsl index 2db0d1827c..2f189d43bf 100644 --- a/resources/00-taisei.pkgdir/shader/extra_tower_mask.vert.glsl +++ b/resources/00-taisei.pkgdir/shader/extra_tower_mask.vert.glsl @@ -1,11 +1,17 @@ #version 330 core #include "lib/render_context.glslh" -#include "interface/tower_light.glslh" +#include "lib/util.glslh" +#include "interface/extra_tower_mask.glslh" void main(void) { - gl_Position = r_projectionMatrix * r_modelViewMatrix * vec4(position, 1.0); - texCoord = (r_textureMatrix * vec4(texCoordRawIn, 0.0, 1.0)).xy; - normal = normalIn; - l = lightvec - (r_modelViewMatrix*vec4(position,1.0)).xyz; + vec4 pos4 = vec4(position, 1.0); + vec4 camPos4 = r_modelViewMatrix * pos4; + camPos = camPos4.xyz; + worldPos = (world_from_model * pos4).xyz; + + gl_Position = r_projectionMatrix * camPos4; + texCoord = texCoordRawIn; + + modelPos = position; } diff --git a/resources/00-taisei.pkgdir/shader/interface/extra_tower_mask.glslh b/resources/00-taisei.pkgdir/shader/interface/extra_tower_mask.glslh new file mode 100644 index 0000000000..42e281b575 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/interface/extra_tower_mask.glslh @@ -0,0 +1,20 @@ + +#ifndef I_EXTRA_TOWER_MASK_H +#define I_EXTRA_TOWER_MASK_H + +#include "standard.glslh" + +UNIFORM(1) sampler2D tex_noise; +UNIFORM(2) sampler2D tex_mask; +UNIFORM(3) sampler2D tex_mod; + +UNIFORM(4) float time; +UNIFORM(5) sampler2D tex2; +UNIFORM(6) float dissolve; +UNIFORM(7) mat4 world_from_model; + +VARYING(3) vec3 camPos; +VARYING(4) vec3 worldPos; +VARYING(5) vec3 modelPos; + +#endif diff --git a/resources/00-taisei.pkgdir/shader/interface/tower_light.glslh b/resources/00-taisei.pkgdir/shader/interface/tower_light.glslh deleted file mode 100644 index 225a920d95..0000000000 --- a/resources/00-taisei.pkgdir/shader/interface/tower_light.glslh +++ /dev/null @@ -1,13 +0,0 @@ - -#ifndef I_TOWER_LIGHT_H -#define I_TOWER_LIGHT_H - -#include "standard.glslh" - -UNIFORM(1) vec4 color; -UNIFORM(2) float strength; -UNIFORM(3) vec3 lightvec; - -VARYING(3) vec3 l; // TODO: rename this? - -#endif \ No newline at end of file diff --git a/resources/00-taisei.pkgdir/shader/lib/util.glslh b/resources/00-taisei.pkgdir/shader/lib/util.glslh index b3ae418583..e57f7d3d61 100644 --- a/resources/00-taisei.pkgdir/shader/lib/util.glslh +++ b/resources/00-taisei.pkgdir/shader/lib/util.glslh @@ -90,6 +90,16 @@ vec3 sample_normalmap(sampler2D src, vec2 uv) { return vec3(xy, z); } +float smoothmin(float a, float b, float k) { + float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); + return mix(b, a, h) - k * h * (1.0 - h); +} + +float smoothmax(float a, float b, float k) { + float h = clamp(0.5 + 0.5 * (a - b) / k, 0.0, 1.0); + return mix(b, a, h) + k * h * (1.0 - h); +} + float flip_topleft_to_native(float x) { #ifdef NATIVE_ORIGIN_BOTTOMLEFT return 1 - x; diff --git a/resources/00-taisei.pkgdir/shader/meson.build b/resources/00-taisei.pkgdir/shader/meson.build index 346239c21f..c3a46dffb8 100644 --- a/resources/00-taisei.pkgdir/shader/meson.build +++ b/resources/00-taisei.pkgdir/shader/meson.build @@ -16,6 +16,7 @@ glsl_files = [ 'blur25.frag.glsl', 'blur5.frag.glsl', 'blur9.frag.glsl', + 'bombshield.frag.glsl', 'boss_death.frag.glsl', 'boss_zoom.frag.glsl', 'calabi-yau-quintic.frag.glsl', @@ -24,6 +25,7 @@ glsl_files = [ 'cutscene.frag.glsl', 'envmap_reflect.frag.glsl', 'extra_bg.frag.glsl', + 'extra_glitch.frag.glsl', 'extra_tower_apply_mask.frag.glsl', 'extra_tower_mask.frag.glsl', 'extra_tower_mask.vert.glsl', @@ -79,6 +81,8 @@ glsl_files = [ 'sprite_silhouette.vert.glsl', 'sprite_yinyang.frag.glsl', 'sprite_youmu_myon_shot.frag.glsl', + 'sprite_yumemi_overlay.frag.glsl', + 'sprite_yumemi_overlay.vert.glsl', 'ssr.vert.glsl', 'ssr_water.frag.glsl', 'stage1_water.frag.glsl', @@ -107,6 +111,8 @@ glsl_files = [ 'tunnel.frag.glsl', 'youmu_bomb_bg.frag.glsl', 'youmua_bomb.frag.glsl', + 'yumemi_spellbg_voronoi.frag.glsl', + 'yumemi_spellbg_voronoi_compose.frag.glsl', 'zbuf_fog.frag.glsl', 'zbuf_fog_tonemap.frag.glsl', # @end glsl diff --git a/resources/00-taisei.pkgdir/shader/multiply2.prog b/resources/00-taisei.pkgdir/shader/multiply2.prog new file mode 100644 index 0000000000..c1c3d9ec22 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/multiply2.prog @@ -0,0 +1,2 @@ + +objects = standard.vert multiply2.frag diff --git a/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.frag.glsl b/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.frag.glsl new file mode 100644 index 0000000000..f3d2e4a574 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.frag.glsl @@ -0,0 +1,41 @@ +#version 330 core + +#include "interface/sprite.glslh" +#include "lib/util.glslh" +#include "lib/sprite_main.frag.glslh" +#include "extra_bg.glslh" + +void spriteMain(out vec4 fragColor) { + float time = customParams.x; + vec2 code_aspect = customParams.yz; + float num_segs = customParams.w; + float inv_num_segs = 1.0 / num_segs; + + vec4 mask = texture(tex, texCoord); + vec2 codeUV = texCoordRaw + mask.rb * (1 - mask.g) * 0.1; + + vec4 code_params = vec4(code_aspect * vec2(1.5, 1), num_segs, inv_num_segs); + vec4 code = sample_code(tex_aux0, code_params, codeUV + vec2(0.03, -0.05) * time); + + float m = 0.5 + 0.5 * sin(time * 3.21 + codeUV.y * 96.31); + float ca = (1 - smoothstep(0.01, 0.2, mix(mask.b, mask.r, m))); + ca = sin(4 * pi * ca + 32.311 * codeUV.x - 1.31 * time); + ca = smoothstep(-0.5, 1.0, ca); + + code.rgb = 1 - pow(mask.rgb, code.rgb); + code.g *= 1 - 2 * (mask.b + mask.r); + code.rgb *= ca * 0.5; + + fragColor = vec4(code.rgb + mask.rgb * code.a * 2, 0); + + code = sample_code(tex_aux0, code_params, codeUV + vec2(-0.0359, -0.04837) * time); + fragColor.rgb += vec3(mask.rgb * code.a * 2); + + fragColor *= color * mask.a; + +// fragColor = color * vec4(mask.ggg, mask.a); + + // fragColor = vec4(vec3(ca, code.a, 0), 1); + // fragColor = smoothstep(0.05, 0.1, mask); +// fragColor = vec4(codeUV.x, codeUV.y, 0, 1.0); +} diff --git a/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.prog b/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.prog new file mode 100644 index 0000000000..b41a7ad8d6 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.prog @@ -0,0 +1,2 @@ + +objects = sprite_yumemi_overlay.frag sprite_yumemi_overlay.vert diff --git a/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.vert.glsl b/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.vert.glsl new file mode 100644 index 0000000000..2471ee1113 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/sprite_yumemi_overlay.vert.glsl @@ -0,0 +1,17 @@ +#version 330 core + +#include "interface/sprite.glslh" +#include "lib/util.glslh" +#include "lib/render_context.glslh" + +void main(void) { + gl_Position = r_projectionMatrix * spriteVMTransform * vec4(vertPos, 0.0, 1.0); + + color = spriteRGBA; + texCoordRaw = vertTexCoord; + texCoord = uv_to_region(spriteTexRegion, vertTexCoord); + // texCoordOverlay = (spriteTexTransform * vec4(vertTexCoord, 0.0, 1.0)).xy; + // texRegion = spriteTexRegion; + // dimensions = spriteDimensions; + customParams = spriteCustomParams; +} diff --git a/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi.frag.glsl b/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi.frag.glsl new file mode 100644 index 0000000000..0ac9b99450 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi.frag.glsl @@ -0,0 +1,61 @@ +#version 330 core + +#include "interface/standard.glslh" +#include "lib/util.glslh" + +/* + * Based on https://www.shadertoy.com/view/MtlyR8 + */ + +#define rnd(p) fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453) +#define srnd(p) (2.0 * rnd(p) - 1.0) + +#define N 3 + +UNIFORM(1) float time; +UNIFORM(2) vec4 color; + +void main(void) { + vec2 uv = 12.0 * texCoordRaw; + float m = 1e9, m2, c = 1e2, v, w; + + // visit 3x3 neighbor tiles + for(int k = 0; k < N * N; ++k) { + // tile center + vec2 iU = floor(uv) + 0.5; + + // neighbor cell + vec2 g = iU + vec2(k % N, k / N) - 1.0; + + float jitter = srnd(g); + + // vector to jittered cell node + vec2 p = g + 0.1 * jitter - uv; + p += 0.1 * sin(time + vec2(1.6, 0.0) + pi * jitter); + + // --- choose distance kind ------------------ + + // L2 distance + // v = length(p); + + // L1 distance + v = abs(p.x) + abs(p.y); + + // Linfinity distance ( = Manhattan = taxicab distance) + // p = abs(p); v = max(p.x, p.y); + + if(v < m) { + // keep 1st and 2nd min distances to node + m2 = m; + m = v; + } else if(v < m2) { + m2 = v; + } + } + + float f = 0.5 + 0.5 * sin(time - uv.y + 2.0 * m); + f = smoothstep(m2 - m, m2 * m, f); + v = mix(m2 * m, m2 - m, f); + + fragColor = color / v; +} diff --git a/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi.prog b/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi.prog new file mode 100644 index 0000000000..fcbc9be7fb --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi.prog @@ -0,0 +1,2 @@ + +objects = standard.vert yumemi_spellbg_voronoi.frag diff --git a/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi_compose.frag.glsl b/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi_compose.frag.glsl new file mode 100644 index 0000000000..ae037b29d4 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi_compose.frag.glsl @@ -0,0 +1,10 @@ +#version 330 core + +#include "lib/render_context.glslh" +#include "interface/standard.glslh" + +UNIFORM(1) sampler2D tex2; + +void main(void) { + fragColor = vec4(texture(tex, texCoord).rgb * texture(tex2, texCoordRaw).rgb, 1.0); +} diff --git a/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi_compose.prog b/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi_compose.prog new file mode 100644 index 0000000000..9fc070bdb8 --- /dev/null +++ b/resources/00-taisei.pkgdir/shader/yumemi_spellbg_voronoi_compose.prog @@ -0,0 +1,2 @@ + +objects = standard.vert yumemi_spellbg_voronoi_compose.frag diff --git a/src/dialog.c b/src/dialog.c index 6a99954611..962fed14df 100644 --- a/src/dialog.c +++ b/src/dialog.c @@ -295,14 +295,20 @@ void dialog_draw(Dialog *dialog) { color_mul_scalar(&clr, a->opacity); - r_flush_sprites(); - r_draw_sprite(&(SpriteParams) { + SpriteParams sp = { .blend = BLEND_PREMUL_ALPHA, .color = &clr, .pos.x = (dialog_width - portrait->w) / 2 + 32 + a->offset.x, .pos.y = VIEWPORT_H - portrait->h / 2 + a->offset.y, .sprite_ptr = portrait, - }); + }; + + // r_flush_sprites(); + r_draw_sprite(&sp); + + if(a->draw_dynamic_overlay) { + a->draw_dynamic_overlay(&sp); + } r_mat_mv_pop(); } diff --git a/src/dialog.h b/src/dialog.h index f3b6da778b..5b5ed09514 100644 --- a/src/dialog.h +++ b/src/dialog.h @@ -13,6 +13,7 @@ #include "resource/resource.h" #include "resource/sprite.h" #include "coroutine/coevent.h" +#include "renderer/api.h" typedef enum DialogSide { DIALOG_SIDE_RIGHT, @@ -34,6 +35,8 @@ typedef struct DialogActor { Sprite composite; + void (*draw_dynamic_overlay)(SpriteParams*); + float opacity; float target_opacity; diff --git a/src/dialog/dialog_interface.h b/src/dialog/dialog_interface.h index 9161a1337c..c55561524e 100644 --- a/src/dialog/dialog_interface.h +++ b/src/dialog/dialog_interface.h @@ -26,6 +26,8 @@ WITHOUT_EVENTS (Stage5PostBoss) \ WITH_EVENTS (Stage6PreBoss, (boss_appears, music_changes)) \ WITHOUT_EVENTS (Stage6PreFinal) \ + WITH_EVENTS (StageExPreBoss, (boss_appears, music_changes)) \ + WITHOUT_EVENTS (StageExPostBoss) \ diff --git a/src/dialog/marisa.c b/src/dialog/marisa.c index 0fce2f07b6..6ff1f3aa31 100644 --- a/src/dialog/marisa.c +++ b/src/dialog/marisa.c @@ -663,6 +663,271 @@ DIALOG_TASK(marisa, Stage6PreFinal) { DIALOG_END(); } +/* + * Extra Stage + */ + +DIALOG_TASK(marisa, StageExPreBoss) { + DIALOG_BEGIN(StageExPreBoss); + + ACTOR_LEFT(marisa); + ACTOR_RIGHT(yumemi); + HIDE(yumemi); + + FACE(marisa, inquisitive); + MSG(marisa, "Whoa, very cool."); + MSG(marisa, "What’s all that text floating out there? I can barely make it out…"); + + MSG_UNSKIPPABLE(yumemi, 180, "It’s a computer programming language."); + EVENT(boss_appears); + SHOW(yumemi); + FACE(yumemi, smug); + MSG(yumemi, "… hmm, so it’s the witch scenario this time…"); + MSG(yumemi, "I’m afraid those languages aren’t of any use to someone like you."); + MSG(marisa, "Oh yeah, you’re right! It’s that ‘C’? Maybe ‘Java’?"); + MSG(marisa, "It looks like it’s been put through a bunch of funky filters, I can’t make it out."); + + FACE(yumemi, surprised); + MSG(yumemi, "Hmm? A fairytale witch who knows about computers?"); + + FACE(marisa, happy); + MSG(marisa, "Heh heh, I’ve helped the kappa out with computer problems tons of times!"); + + MSG(yumemi, "Is that so?"); + FACE(yumemi, smug); + MSG(yumemi, "How absurd."); + MSG(yumemi, "But even knowing those things, this place is beyond the comprehension of any fantasy creature."); + FACE(yumemi, eyes_closed); + MSG(yumemi, "Those women I conscripted before are proof of that."); + + FACE(marisa, puzzled); + MSG(marisa, "Hey, no point blamin’ students for a bad teacher."); + + MSG(yumemi, "…"); + FACE(yumemi, normal); + MSG(yumemi, "I built this machine through the merging of computer science and unified physics."); + FACE(yumemi, eyes_closed); + MSG(yumemi, "When it was completed, its impressive display of power and ability…"); + FACE(yumemi, sigh); + MSG(yumemi, "… did nothing."); + FACE(marisa, normal); + FACE(yumemi, sad); + MSG(yumemi, "Nobody cared. They were disillusioned before they'd even seen it."); + MSG(yumemi, "‘Big deal. Rich people can already go to the moon. It’s just another toy for them.’"); + FACE(yumemi, eyes_closed); + MSG(yumemi, "… even though I wouldn’t dare give those lunar-bound idiots even a glimpse."); + + FACE(yumemi, normal); + FACE(marisa, puzzled); + MSG(marisa, "Yeah, uh, screw those guys, I guess?"); + + FACE(yumemi, smug); + MSG(yumemi, "… heh."); + FACE(yumemi, normal); + MSG(yumemi, "You know, I knew people who used to practice witchcraft."); + FACE(marisa, normal); + MSG(yumemi, "Some of them even wore silly hats, just like yours."); + FACE(yumemi, smug); + MSG(yumemi, "I stopped talking to them once I realized the truth."); + + FACE(marisa, sweat_smile); + MSG(marisa, "Aw, why?"); + FACE(marisa, happy); + MSG(marisa, "Witches have got the best liquor, don’t ya know?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "We used to go to bars to meet them, actually."); + FACE(marisa, normal); + FACE(yumemi, sad); + MSG(yumemi, "Those were simpler times…"); + + FACE(marisa, puzzled); + MSG(marisa, "‘We’? Ya mean those former-underlings of yours?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "…"); + FACE(yumemi, normal); + MSG(yumemi, "It was all a distraction from the real problem."); + FACE(marisa, normal); + MSG(yumemi, "Comforting, at first, to think there was some hidden meaning to reality, but…"); + + FACE(marisa, sweat_smile); + MSG(marisa, "Hey, why don’t we work this all out over some sake and physics textbooks?"); + FACE(marisa, happy); + MSG(marisa, "If you can look past the hat, I’m a pretty understandin’ lady!"); + + FACE(yumemi, smug); + MSG(yumemi, "I don’t take orders from fairytales."); + + FACE(marisa, normal); + MSG(marisa, "Well, first off, I was askin’, not orderin’."); + FACE(marisa, happy); + MSG(marisa, "Second off, I’m a completely ordinary human."); + + FACE(yumemi, surprised); + MSG(yumemi, "You’re a witch, aren't you?"); + + MSG(marisa, "A completely ordinary witch."); + FACE(marisa, normal); + MSG(marisa, "Sake’s gettin’ cold."); + MSG(marisa, "How about you do me a solid, turn this thing off, and you can tell me ALL about that Grand Unified Theory of yours back at my place?"); + + FACE(yumemi, normal); + MSG(yumemi, "If it’s all the same to you, I’d rather keep it on, and stay here."); + FACE(marisa, happy); + MSG(marisa, "Good thing it’s not all the same to me, then!"); + FACE(yumemi, surprised); + MSG(yumemi, "Huh? It’s an expression-"); + MSG(marisa, "Gahahahah!"); + + FACE(yumemi, sigh); + MSG(yumemi, "I won’t take sass from some ‘ordinary’ witch."); + FACE(marisa, smug); + MSG(marisa, "Why not? My sass is legendary."); + + FACE(yumemi, normal); + MSG(yumemi, "Because you’re not real. You’re a figment of delusional minds."); + MSG(yumemi, "People waste their time thinking about ‘Otherworlds’, about Gensōkyō, while everything else decays and dies."); + FACE(marisa, normal); + MSG(yumemi, "I’m going to give them no other escape than the world they truly live in."); + FACE(yumemi, sigh); + MSG(yumemi, "It’s likely they’ll find some other distraction, some other way to destroy themselves…"); + FACE(yumemi, sad); + MSG(yumemi, "It probably won’t change a thing."); + FACE(yumemi, eyes_closed); + MSG(yumemi, "But at least I’ll have the satisfaction of having tried at all."); + + FACE(marisa, sweat_smile); + MSG(marisa, "Yikes."); + FACE(marisa, normal); + MSG(marisa, "Y’know destroyin’ fantasy will just make your problems worse, right?"); + MSG(marisa, "If all I did was studyin’, I’d be dull in no time flat!"); + FACE(marisa, happy); + MSG(marisa, "Goofin’ off and thinkin’ about weird stuff keeps your mind sharp! Let’s you recharge!"); + + FACE(yumemi, smug); + MSG(yumemi, "Yes, of course it’d work for the likes of you."); + FACE(yumemi, normal); + MSG(yumemi, "But for us mere humans, well, we’re unable to handle the Siren’s song of these places."); + + FACE(marisa, normal); + MSG(marisa, "I really am human, y’know."); + MSG(marisa, "And from one human to another, please, turn off the dang madness rays, would ya?"); + + EVENT(music_changes); + FACE(yumemi, sigh); + MSG(yumemi, "Fantasy *is* tenacious, isn’t it? I guess it was always going to come down to a fight, no matter what."); + MSG(yumemi, "It’s not like I have anything to lose."); + FACE(yumemi, normal); + MSG(yumemi, "If nothing else, destroying you will provide valuable data in annihilating the rest of you."); + + DIALOG_END(); +} + +DIALOG_TASK(marisa, StageExPostBoss) { + DIALOG_BEGIN(StageExPostBoss); + + ACTOR_LEFT(marisa); + ACTOR_RIGHT(yumemi); + VARIANT(yumemi, defeated); + FACE(yumemi, defeated); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "Ugh…"); + + FACE(marisa, smug); + MSG(marisa, "What’s that thing the kappa keep sayin’? ‘Garbage in, garbage out’?"); + MSG(marisa, "I guess that makes ya the garbage? And now you’re gettin’… taken outside?"); + FACE(marisa, puzzled); + MSG(marisa, "No, that’s ‘takin’ out the trash’…"); + + FACE(yumemi, sad); + MSG(yumemi, "God, you’re irritating…"); + + FACE(marisa, smug); + MSG(marisa, "‘God’? What’s a ‘God’ gotta do with it? Ain’t that against your non-beliefs?"); + MSG(marisa, "Or are ya just mad ya got schooled by an ‘imaginary’ girl wearin’ a silly hat?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "…"); + FACE(yumemi, normal); + FACE(marisa, normal); + MSG(yumemi, "Despite the extensive calculations, I still lost."); + MSG(yumemi, "I suppose everything really was doomed from the start. People are too invested in fantasical delusions…"); + + MSG(marisa, "Listen up, brain genius."); + FACE(marisa, smug); + MSG(marisa, "You’re doin’ mathematics and mathematicians a disservice by bein’ all hard-line like this."); + FACE(marisa, happy); + MSG(marisa, "Mathematicians are some of the weirdest folks out there!"); + FACE(marisa, inquisitive); + MSG(marisa, "Ya ever just sit down ‘n really LOOK at the Mandelbrot Set?"); + FACE(marisa, happy); + MSG(marisa, "How do ya think they come up with all that weird crap?"); + + FACE(yumemi, surprised); + MSG(yumemi, "Computer Science, and now Topology…?"); + FACE(marisa, normal); + MSG(yumemi, "How is knowledge of things not utterly incompatible with your existence…?"); + + FACE(marisa, inquisitive); + MSG(marisa, "Wait, is it supposed to be?!"); + MSG(marisa, "Why didn’t ya say that before?!"); + FACE(marisa, smug); + MSG(marisa, "I gotta run home ‘n burn all my Fractal Geometry textbooks right away!"); + FACE(marisa, happy); + MSG(marisa, "Gahahaha!"); + + FACE(yumemi, sigh); + MSG(yumemi, "… okay, okay, I get it."); + FACE(marisa, normal); + MSG(yumemi, "I didn’t… expect to find somethi—"); + FACE(yumemi, normal); + MSG(yumemi, "… someone, like you here."); + + MSG(marisa, "There’s somethin’ ya gotta learn, poindexter."); + MSG(marisa, "Gensōkyō’s one of the weirdest places, anywhere, anywhen."); + MSG(marisa, "Nothin’ here is like you’d expect, not even the unexpected!"); + FACE(marisa, happy); + MSG(marisa, "Magic here’s all about mixin’ fantasy with reality. Always has been."); + MSG(marisa, "It don’t matter where yer at - if you’re unbalanced about it, you’re gonna go nowhere in life."); + FACE(marisa, smug); + MSG(marisa, "And thaaaaat means, no friggin’ genocide, ya got it?!"); + + FACE(marisa, normal); + FACE(yumemi, sad); + MSG(yumemi, "Is… there a point to this lecture? Aren’t you going to punish me anyways?"); + + FACE(marisa, happy); + MSG(marisa, "Gettin’ sassed on by me ain’t punishment enough? Gahahah!"); + + MSG(yumemi, "Punish me with…"); + FACE(yumemi, sad); + MSG(yumemi, "…"); + + FACE(marisa, sweat_smile); + MSG(marisa, "Whoa, whoa, for real? Ya think I’d do that?!"); + MSG(marisa, "Hey now, ya seem pretty miserable as it is. No point in addin’ to that misery."); + FACE(marisa, smug); + MSG(marisa, "How would I even explain that to the others?"); + MSG(marisa, "‘Oh yeah, I murdered that girl, no big deal’?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "I’m sorry."); + FACE(marisa, normal); + FACE(yumemi, sad); + MSG(yumemi, "This was all a mistake. It was never going to solve anything anyways."); + MSG(yumemi, "And… this isn’t what she—…"); + MSG(yumemi, "… ugh, I’ve been such an idiot!"); + + FACE(marisa, normal); + MSG(marisa, "Tragically stupid smart people are a dime a dozen in these parts."); + FACE(marisa, happy); + MSG(marisa, "You’ll fit right in!"); + + DIALOG_END(); +} + /* * Register the tasks */ diff --git a/src/dialog/reimu.c b/src/dialog/reimu.c index 96fa37d4ad..7cc5689844 100644 --- a/src/dialog/reimu.c +++ b/src/dialog/reimu.c @@ -7,6 +7,7 @@ */ #include "dialog_macros.h" +#include "dynstage.h" /* * Stage 1 @@ -643,6 +644,279 @@ DIALOG_TASK(reimu, Stage6PreFinal) { DIALOG_END(); } +/* + * Extra Stage + */ + +DIALOG_TASK(reimu, StageExPreBoss) { + DIALOG_BEGIN(StageExPreBoss); + + ACTOR_LEFT(reimu); + ACTOR_RIGHT(yumemi); + HIDE(yumemi); + yumemi.draw_dynamic_overlay = dynstage_get_exports()->stagex_draw_boss_portrait_overlay; + + EVENT(boss_appears); + MSG_UNSKIPPABLE(yumemi, 180, "So it's this scenario that's playing out, hmm?"); + SHOW(yumemi); + FACE(yumemi, surprised); + MSG(yumemi, "The impossible shrine maiden has finally arrived."); + FACE(yumemi, smug); + MSG(yumemi, "Here to set me on the ‘right path’, protagonist girl?"); + MSG(yumemi, "Just like in all of your other tall tales…"); + + FACE(reimu, unsettled); + MSG(reimu, "I get the worst feeling looking at you."); + + FACE(yumemi, surprised); + MSG(yumemi, "Oh? How’s that?"); + + FACE(reimu, sigh); + MSG(reimu, "It’s mostly just how miserable you look."); + + FACE(yumemi, normal); + MSG(yumemi, "Do I look especially miserable? Hmm."); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "And what would you know about misery, living in this fantastical paradise?"); + + FACE(reimu, unamused); + MSG(yumemi, "Have the other fairytale creatures hurt your feelings?"); + + FACE(yumemi, normal); + FACE(reimu, unsettled); + MSG(reimu, "*Other* fairytale creatures?"); + + FACE(reimu, unamused); + MSG(reimu, "I’m human, you know."); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "Oh, please. You’re an invincible, flying, magic-wielding shrine maiden—"); + FACE(yumemi, normal); + + FACE(reimu, sigh); + MSG(reimu, "I don’t use magic."); + + FACE(yumemi, surprised); + MSG(yumemi, "Hah."); + FACE(yumemi, smug); + FACE(reimu, irritated); + MSG(yumemi, "Hahaha."); + FACE(yumemi, normal); + MSG(yumemi, "Perhaps you're unaware, but the world - the real world - is dying, shrine maiden."); + + FACE(yumemi, sad); + FACE(reimu, unamused); + MSG(yumemi, "People pour their hearts and souls into fantasy, leaving none of that energy, that drive, for the real world."); + MSG(yumemi, "People talk of ‘solutions’ and ‘progress,’ but it’s pointless so long as people wile away their lives in useless places like this."); + + FACE(reimu, puzzled); + MSG(reimu, "How’s that Gensōkyō’s problem?"); + FACE(reimu, unamused); + MSG(reimu, "It’s not like we invaded you and forced you into it or anything."); + + FACE(yumemi, sigh); + MSG(yumemi, "If only it were that simple."); + FACE(yumemi, normal); + FACE(reimu, unamused); + MSG(yumemi, "Places like this tempt people into forgetting their real lives."); + FACE(yumemi, sad); + MSG(yumemi, "And by abandoning the real world, they doom not just themselves, but everyone around them."); + + FACE(reimu, unamused); + MSG(reimu, "People are allowed to have a break from reality."); + FACE(reimu, puzzled); + MSG(reimu, "Haven’t you ever read a book?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "You mean fiction? I haven’t had a need for it in years."); + FACE(yumemi, sigh); + MSG(yumemi, "It’s a waste of time."); + + FACE(reimu, unsettled); + MSG(reimu, "Wow, no wonder you’re so miserable."); + + FACE(yumemi, sad); + MSG(yumemi, "Even as I introduced this technology to the world, nobody woke up from their dissociative haze. It was as it ever was."); + FACE(yumemi, eyes_closed); + MSG(yumemi, "But at least, with this machine I’ve built, I can erase every so-called ‘Otherworld’ from the fabric of social reality."); + FACE(yumemi, normal); + + FACE(reimu, assertive); + MSG(reimu, "People won’t stop day-dreaming just because you tell them to. Fantasy is in people’s hearts and minds."); + FACE(reimu, sigh); + MSG(reimu, "So, even if you managed to destroy Gensōkyō—"); + + MSG(yumemi, "Yōkai, Gods, magicians, flying shrine maidens…"); + FACE(yumemi, eyes_closed); + MSG(yumemi, "It’s all delusional nonsense."); + + FACE(reimu, unsettled); + MSG(reimu, "So you want genocide?"); + + FACE(yumemi, surprised); + MSG(yumemi, "Genocide…? What a harsh word."); + FACE(yumemi, normal); + FACE(reimu, unamused); + MSG(yumemi, "You’re not real. No one is being killed."); + + MSG(reimu, "No wonder those two got freaked out just thinking about you. You’re completely unreasonable."); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "Ah, I thought I’d made them both see things my way, by introducing them to the Grand Unified Theory."); + FACE(yumemi, normal); + MSG(yumemi, "But in the end, they just betrayed me."); + FACE(yumemi, sad); + MSG(yumemi, "In hindsight, it makes sense, of course. They can’t betray what they are."); + + FACE(reimu, irritated); + MSG(reimu, "It’s hard to keep people on your side when all you do is threaten them, idiot!"); + FACE(reimu, assertive); + MSG(reimu, "Have you tried not being a genocidal maniac?"); + + FACE(yumemi, sigh); + MSG(yumemi, "I’m tired, shrine maiden. Too tired to explain my life to a figment of imagination."); + FACE(reimu, unamused); + FACE(yumemi, eyes_closed); + MSG(yumemi, "Perhaps I’ve already given up trying to save my society, too."); + + FACE(reimu, assertive); + MSG(reimu, "Sucks to be you. Don’t make it our problem."); + + EVENT(music_changes); + + FACE(yumemi, normal); + MSG(yumemi, "Oh, but it’s inherently your problem, isn’t it? Your very existence is the root of my misery."); + FACE(reimu, unsettled); + FACE(yumemi, eyes_closed); + MSG(yumemi, "Farewell, shrine maiden. I would say it’s been nice knowing you, but…"); + FACE(yumemi, sad); + MSG(yumemi, "There’s really no way I could’ve known you at all."); + + DIALOG_END(); +} + +DIALOG_TASK(reimu, StageExPostBoss) { + DIALOG_BEGIN(StageExPostBoss); + + ACTOR_LEFT(reimu); + ACTOR_RIGHT(yumemi); + yumemi.draw_dynamic_overlay = dynstage_get_exports()->stagex_draw_boss_portrait_overlay; + + VARIANT(yumemi, defeated); + FACE(yumemi, defeated); + MSG(yumemi, "I yield."); + + FACE(reimu, sigh); + MSG(reimu, "Finally."); + + FACE(yumemi, sad); + MSG(yumemi, "Even with the full computational capacity of this machine, I wasn’t able to calculate a winning trajectory…"); + FACE(reimu, unsettled); + MSG(yumemi, "Nor did any of you succumb to the truth."); + FACE(yumemi, sigh); + MSG(yumemi, "I suppose it really was hopeless all along."); + + FACE(reimu, puzzled); + MSG(reimu, "What ‘truth’?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "That this isn’t real."); + MSG(yumemi, "That it’s fantasy, a delusion."); + MSG(yumemi, "That faith and magic—"); + + FACE(reimu, unsettled); + MSG(reimu, "That faith and magic don’t exist?"); + MSG(reimu, "That we’re no different than legends and myths?"); + MSG(reimu, "That if the barrier separating Gensōkyō from the Outside World were to fall, I’d lose my powers, or worse, and that all yokai would go extinct?"); + + FACE(reimu, assertive); + MSG(reimu, "We knew all of that already! Everyone here does!"); + + FACE(yumemi, surprised); + MSG(yumemi, "Are… are you serious? But then, how could you—"); + + FACE(reimu, irritated); + MSG(reimu, "Outside World humans are so irritating!"); + MSG(reimu, "Did you even bother talking to anyone when you got here?!"); + MSG(reimu, "No, of course not! Why would you?! You’re too full of yourself to ask anyone about anything!"); + FACE(reimu, assertive); + FACE(yumemi, sad); + MSG(reimu, "What, was your big plan to use those ‘madness rays’ or whatever it was to make us all ‘think ourselves out of existence’?"); + + MSG(yumemi, "But how—"); + + FACE(reimu, irritated); + MSG(reimu, "That never would’ve worked! We’re always thinking about how precarious Gensōkyō is!"); + MSG(reimu, "What do you think my job *is*, exactly?"); + FACE(reimu, assertive); + MSG(reimu, "All you really did was make everyone act all chu—…"); + + FACE(reimu, unamused); + MSG(reimu, "Chuu—…"); + + FACE(reimu, irritated); + FACE(yumemi, surprised); + MSG(reimu, "…ugh, it’s that weird word Sumireko keeps using…"); + + FACE(reimu, assertive); + MSG(reimu, "Y-You just made everyone act even more delusional than they already are!"); + + MSG(yumemi, "Sumire…ko?"); + MSG(yumemi, "Usami Sumireko?"); + + FACE(reimu, puzzled); + MSG(reimu, "Huh? You know her or something?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "… You’re right. I’ve been a complete idiot."); + MSG(yumemi, "I surrender. I’ll shut everything down."); + + FACE(reimu, puzzled); + MSG(reimu, "What?! That’s all it took to talk you down?!"); + FACE(reimu, assertive); + MSG(reimu, "That’s infuriatingly random, you know!"); + + FACE(reimu, sigh); + MSG(reimu, "Maybe I should’ve talked her into it and saved me all this trouble!"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "Do you know if…"); + + FACE(yumemi, sad); + FACE(reimu, puzzled); + MSG(yumemi, "… no. I doubt she’d be here too. It’d be too convenient."); + FACE(reimu, unamused); + MSG(yumemi, "I’ve failed. I accept your punishment."); + FACE(yumemi, eyes_closed); + MSG(yumemi, "Finish me off. I won’t resist."); + + FACE(reimu, puzzled); + MSG(reimu, "‘Finish you off’?"); + MSG(yumemi, "End my life. I threatened your world with ‘genocide’, didn’t I?"); + FACE(reimu, unamused); + MSG(reimu, "Huh? But who would you even be a martyr for? You don’t have any followers left."); + + FACE(yumemi, sad); + MSG(yumemi, "What—?"); + + FACE(reimu, sigh); + MSG(reimu, "Ugh, I’ll let the Moriya Gods deal with you."); + MSG(reimu, "Be prepared to give endless lectures about that ‘Grand Unified Whatever’."); + + FACE(yumemi, normal); + MSG(yumemi, "The ‘Grand Unified Theory’?"); + FACE(reimu, unamused); + MSG(yumemi, "That’s… my punishment for all this? To give physics lectures?"); + + FACE(reimu, assertive); + MSG(reimu, "Do something useful with that big brain of yours for once!"); + FACE(reimu, irritated); + MSG(reimu, "Count yourself lucky that Kanako probably won’t put you on fairy vomit duty!"); + + DIALOG_END(); +} + /* * Register the tasks */ diff --git a/src/dialog/youmu.c b/src/dialog/youmu.c index c95d257cb3..ff874620d1 100644 --- a/src/dialog/youmu.c +++ b/src/dialog/youmu.c @@ -599,6 +599,329 @@ DIALOG_TASK(youmu, Stage6PreFinal) { DIALOG_END(); } +/* + * Extra Stage + */ + +DIALOG_TASK(youmu, StageExPreBoss) { + DIALOG_BEGIN(StageExPreBoss); + + ACTOR_LEFT(youmu); + ACTOR_RIGHT(yumemi); + + EVENT(boss_appears); + SHOW(yumemi); + FACE(youmu, normal); + MSG(youmu, "Greetings, weary adversary."); + FACE(yumemi, surprised); + MSG(yumemi, "‘Weary’?"); + FACE(youmu, eyes_closed); + MSG(youmu, "‘The slings and arrows of outrageous fortune’, perhaps?"); + FACE(youmu, smug); + MSG(youmu, "Given the ostentatiousness of this contraption we find ourselves in."); + + MSG(yumemi, "Outrageous fortune? Perish the thought."); + FACE(yumemi, normal); + MSG(yumemi, "Hmm…"); + FACE(yumemi, surprised); + MSG(yumemi, "… so the swordswoman has actual katanas…"); + FACE(yumemi, smug); + FACE(youmu, normal); + MSG(yumemi, "Hilarious."); + FACE(yumemi, normal); + MSG(yumemi, "In my time, those swords of yours were banned over two hundred years ago."); + MSG(yumemi, "The great machine of war became more detached and brutal ever since the Industrial Revolution. Nobody has any use for them."); + FACE(yumemi, eyes_closed); + MSG(yumemi, "Just another relic of human history."); + + FACE(youmu, eyes_closed); + MSG(youmu, "Yes, I am aware of the Outside World’s current martial doctrines."); + FACE(youmu, chuuni); + MSG(youmu, "And I’m glad to hear that you agree, that these are more… elegant weapons, for a more honourable age."); + + FACE(yumemi, surprised); + MSG(yumemi, "…"); + MSG(yumemi, "(First Shakespeare, and then…)"); + FACE(yumemi, normal); + MSG(yumemi, "Your martial art is a pastime of the wealthy."); + FACE(youmu, normal); + FACE(yumemi, eyes_closed); + MSG(yumemi, "But of course, I’m sure many people fruitlessly ‘train’ in virtual reality with those weapons, for absolutely no purpose other than delusions of grandeur."); + + MSG(youmu, "Fine blades such as these are hard to come by for the working class, it’s true."); + FACE(youmu, chuuni); + MSG(youmu, "But I am privileged enough to be in the service of Lady Yuyuko, who graciously provides—"); + + FACE(youmu, normal); + FACE(yumemi, sigh); + MSG(yumemi, "Please, stop. I don’t care about any of that. It’s meaningless."); + + FACE(youmu, unamused); + MSG(youmu, "Hmph. I was warned of your attitude."); + MSG(youmu, "And what I’ve heard is evidently true. You care for nothing and no one."); + FACE(youmu, sigh); + MSG(youmu, "A hollow husk of a woman, even compared to me."); + + FACE(yumemi, sad); + MSG(yumemi, "Excuse me?"); + + FACE(youmu, normal); + MSG(youmu, "Your expressions, body language, words…"); + MSG(youmu, "Yes, you’ve lost the spark which gives one meaning in life."); + + FACE(yumemi, normal); + MSG(yumemi, "You know nothing about me."); + MSG(yumemi, "You exist in an unreal world, living a false life, surrounded by simple and childish delusions about your powers and abilities."); + + FACE(youmu, smug); + MSG(youmu, "As opposed to whom? You?"); + + FACE(yumemi, surprised); + MSG(yumemi, "Y—…"); + FACE(yumemi, eyes_closed); + MSG(yumemi, "Yes. Me."); + + FACE(youmu, eyes_closed); + MSG(youmu, "I see."); + + FACE(youmu, smug); + FACE(yumemi, sigh); + MSG(yumemi, "I don’t have the patience to take criticism from the likes of you. I’ll destroy this delusion and be on my way."); + + FACE(youmu, puzzled); + MSG(youmu, "How odd. Have the denizens of Gensōkyō given you cause for retribution?"); + + FACE(yumemi, normal); + FACE(youmu, normal); + MSG(yumemi, "That remains to be seen."); + + FACE(youmu, puzzled); + MSG(youmu, "Excuse me?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "It could be this Gensōkyō, or some other Gensōkyō…"); + FACE(youmu, normal); + FACE(yumemi, sigh); + MSG(yumemi, "To be honest, I can’t tell the difference anymore."); + + MSG(youmu, "Some… other Gensōkyō?"); + + FACE(yumemi, normal); + MSG(yumemi, "All of them have a common effect on the real world."); + FACE(yumemi, eyes_closed); + MSG(yumemi, "They lead people astray, into forgetting about…"); + FACE(youmu, normal); + FACE(yumemi, sad); + MSG(yumemi, "… ah, I just don’t care anymore."); + MSG(yumemi, "It probably won’t change a thing."); + FACE(yumemi, sigh); + MSG(yumemi, "Perhaps I just wanted to see if this contraption worked, in the end."); + FACE(yumemi, normal); + MSG(yumemi, "It’s not worth explaining anything else."); + + FACE(youmu, unamused); + MSG(youmu, "…"); + MSG(youmu, "Is that… it?"); + + FACE(yumemi, surprised); + MSG(yumemi, "What else were you expecting? A grand vision for the future?"); + FACE(yumemi, sigh); + MSG(yumemi, "Please, I’m past all that."); + + MSG(youmu, "… pathetic!"); + + FACE(yumemi, surprised); + MSG(yumemi, "Excuse me?"); + + MSG(youmu, "Elly had a purpose, a passion! To save her friends, and to restore her world!"); + MSG(youmu, "But you…! You just want an apocalypse! Over something you no longer care for?!"); + + FACE(yumemi, sad); + MSG(yumemi, "I don’t have anything *left* to care for, so what does it matter?"); + FACE(yumemi, eyes_closed); + MSG(yumemi, "It’s not as if you’re real. You’re a fairytale out of a children’s book."); + + MSG(youmu, "And what could you have possibly lost that would explain this casually genocidal attitude?"); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "Isn’t it obvious by now?"); + FACE(yumemi, sad); + MSG(yumemi, "Gensōkyō stole away the only person I ever loved."); + MSG(yumemi, "And I don’t care which Gensōkyō did it. Not anymore."); + + FACE(youmu, eyes_closed); + MSG(youmu, "…"); + FACE(yumemi, eyes_closed); + MSG(yumemi, "…"); + + MSG(youmu, "Spirited away, hmm?"); + FACE(youmu, unamused); + MSG(youmu, "And this is how you choose to honour their memory? I see."); + + FACE(youmu, normal); + FACE(yumemi, sigh); + MSG(yumemi, "No, you don’t. You’re a figment of a weary society’s delusional imagination."); + EVENT(music_changes); + MSG(yumemi, "I’m tired of explaining myself to the likes of you."); + FACE(yumemi, smug); + MSG(yumemi, "Let me demonstrate the full power of this technological terror I’ve constructed."); + + DIALOG_END(); +} + +DIALOG_TASK(youmu, StageExPostBoss) { + DIALOG_BEGIN(StageExPostBoss); + + ACTOR_LEFT(youmu); + ACTOR_RIGHT(yumemi); + VARIANT(yumemi, defeated); + FACE(yumemi, defeated); + + FACE(yumemi, sad); + MSG(yumemi, "I yield."); + MSG(yumemi, "To think I’ll meet my end to someone like you…"); + + FACE(youmu, eyes_closed); + MSG(youmu, "An academic such as yourself ought to know that martial arts are a field of study, like any other."); + FACE(youmu, chuuni); + MSG(youmu, "You lost to a subject matter expert, nothing more."); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "… ugh…"); + + FACE(youmu, eyes_closed); + MSG(youmu, "With as empty as your heart is, you might be unaware of the gift you’ve given me, weary traveller."); + + FACE(yumemi, surprised); + MSG(yumemi, "Gift…? And what’s that…?"); + + MSG(youmu, "Before this incident, I was terrified of my own existence."); + MSG(youmu, "Between worlds, perpetually troubled by my mortality and immortality…"); + MSG(youmu, "… my human side, and my phantom side."); + FACE(yumemi, normal); + FACE(youmu, normal); + MSG(youmu, "When I came to this infernal place, something changed within my heart, mind, and soul."); + FACE(youmu, chuuni); + MSG(youmu, "Suddenly, I was no longer frightened of my true nature! My fear burned away!"); + MSG(youmu, "I recognized my talents and beauty as a warrior, fully and earnestly!"); + MSG(youmu, "The ‘destructive madness’ you sought to destroy me with? It only made me stronger!"); + MSG(youmu, "And it has persisted even with your audacious machine deactivated!"); + + FACE(yumemi, surprised); + MSG(yumemi, "Am I hearing you clearly…? Instead of realizing your unreal nature, you became permanently more confident in yourself?"); + MSG(youmu, "Indeed. I was as startled as you are at first."); + + FACE(youmu, normal); + FACE(yumemi, smug); + MSG(yumemi, "Hah. Hahaha. Incredible."); + + FACE(youmu, smug); + MSG(youmu, "Empty-hearted retribution will never defeat the will of those who believe in themselves."); + FACE(youmu, chuuni); + FACE(yumemi, normal); + MSG(youmu, "The outcome of this battle was—"); + + WAIT(60); + ACTOR_LEFT(elly); + MSG(elly, "I’m here, Yōmu! I couldn’t stand the idea of you having to face my mistake alone, and—"); + MSG(elly, "…?!"); + MSG(elly, "Y-you already defeated her?!"); + + FACE(youmu, smug); + MSG(youmu, "Fear not, I’ve resolved the situation."); + FACE(youmu, eyes_closed); + MSG(youmu, "Her techniques were powerful and precise, but as I was just saying…"); + + FACE(yumemi, sad); + MSG(yumemi, "…"); + MSG(yumemi, "‘The outcome of this battle was determined before it began’?"); + + FACE(youmu, smug); + MSG(youmu, "Precisely."); + FACE(elly, blush); + MSG(elly, "Yōmu, you’re absolutely incredible!"); + FACE(youmu, chuuni); + MSG(youmu, "Flattery will get you everywhere, my dear."); + MSG(yumemi, "After all that effort you put into stealing the Tower from me…"); + FACE(youmu, normal); + FACE(elly, normal); + MSG(yumemi, "… that’s all it took for you to give up on your vision, Elly? A delusional katana-wielding woman with cheesy action-hero one-liners?"); + MSG(yumemi, "I never did stand a chance, did I?"); + + FACE(elly, angry); + MSG(elly, "No, I suppose you didn’t."); + FACE(yumemi, eyes_closed); + MSG(yumemi, "Hah…"); + MSG(yumemi, "Sentence me as you wish. I won’t resist."); + + FACE(youmu, eyes_closed); + FACE(elly, normal); + MSG(youmu, "Very well."); + FACE(youmu, normal); + MSG(youmu, "Allowing you to leave is out of the question. The Tower may continue to drive you mad, and you may come back, with reinforcements."); + MSG(youmu, "As such, I would order you to power down the tower, and agree to live here in Gensōkyō peacefully. However…"); + FACE(youmu, eyes_closed); + MSG(youmu, "As Elly is one of the most impacted victims, I must ask for her input."); + + FACE(elly, angry); + MSG(elly, "…"); + FACE(elly, normal); + MSG(elly, "…"); + MSG(elly, "I know her pain. She’s suffered enough."); + MSG(elly, "I’ll agree to whatever you decide, Yōmu."); + + FACE(youmu, normal); + MSG(youmu, "Very well, then."); + FACE(youmu, eyes_closed); + MSG(youmu, "And so, those are the terms. Will you abide by them, weary traveller?"); + + FACE(yumemi, sad); + MSG(yumemi, "… you’re not going to…?"); + + FACE(youmu, eyes_closed); + MSG(youmu, "Take your life? Perish the thought."); + FACE(youmu, normal); + MSG(youmu, "A human life is too precious and fleeting for such violence."); + + FACE(yumemi, sad); + MSG(yumemi, "I… see."); + MSG(yumemi, "Then, I agree. I’ll turn it all off."); + FACE(yumemi, normal); + MSG(yumemi, "As I’ll be here for a while, I figure I may as well ask something, if you’ll indulge me."); + + FACE(youmu, puzzled); + MSG(youmu, "What is it?"); + + MSG(yumemi, "Is there someone here… who can manipulate the boundaries of reality?"); + + FACE(youmu, eeeeh); + MSG(youmu, "…?!"); + FACE(youmu, eyes_closed); + MSG(youmu, "… ahem. Yes, there is. I’m well-acquainted with her, oddly enough."); + + FACE(yumemi, surprised); + MSG(yumemi, "…?!"); + + MSG(elly, "Are you talking about that blonde lady, the one always visiting Lady Yuyuko?"); + MSG(elly, "What was her name again?"); + MSG(elly, "The eyes she has in her ‘gaps’ give me the creeps…"); + + FACE(youmu, eeeeh); + MSG(youmu, "S-she is frightening to most people in Gensōkyō, myself included, even in my… improved state."); + FACE(youmu, puzzled); + MSG(youmu, "Does that appear to be the one you seek?"); + + MSG(yumemi, "I’m not sure. Maybe…"); + + FACE(youmu, eyes_closed); + MSG(youmu, "Perhaps Lady Yuyuko will grace you with her legendary generosity, and grant you an audience with her… companion."); + + FACE(yumemi, eyes_closed); + MSG(yumemi, "Thank you. That’s more than I can ask."); + + DIALOG_END(); +} + /* * Register the tasks */ diff --git a/src/enemy_classes.c b/src/enemy_classes.c index 52ad9b2fb7..3573471456 100644 --- a/src/enemy_classes.c +++ b/src/enemy_classes.c @@ -461,9 +461,6 @@ void ecls_anyenemy_fake3dmovein( assert(duration > 0); BaseEnemyVisualParams *vp = e->visual.drawdata; - vec3 initpos_projected; - camera3d_project(cam, initpos_3d, initpos_projected); - vp->fakepos.pos = CMPLXF(initpos_projected[0], initpos_projected[1]); EnemyFlag tempflags; spawnanim_set_flags(e, &tempflags); @@ -472,6 +469,10 @@ void ecls_anyenemy_fake3dmovein( e->ent.draw_layer = LAYER_ENEMY_BACKGROUND | (layer & LAYER_LOW_MASK); for(int i = 1;;) { + vec3 initpos_projected; + camera3d_project(cam, initpos_3d, initpos_projected); + vp->fakepos.pos = CMPLXF(initpos_projected[0], initpos_projected[1]); + float f = i / (float)duration; vp->fakepos.blendfactor = (1.0 - glm_ease_sine_inout(f)); vp->scale = glm_ease_sine_inout(f); diff --git a/src/menu/musicroom.c b/src/menu/musicroom.c index 023c11c9a8..ff39706dff 100644 --- a/src/menu/musicroom.c +++ b/src/menu/musicroom.c @@ -287,6 +287,8 @@ MenuData *create_musicroom_menu(void) { add_bgm(m, "stage6boss_phase1", preload); add_bgm(m, "stage6boss_phase2", preload); add_bgm(m, "stage6boss_phase3", preload); + add_bgm(m, "stagex", preload); + add_bgm(m, "stagexboss", preload); add_bgm(m, "ending", preload); add_bgm(m, "credits", preload); add_bgm(m, "gameover", preload); diff --git a/src/progress.c b/src/progress.c index 33c0d0ac30..1b59e68107 100644 --- a/src/progress.c +++ b/src/progress.c @@ -1013,7 +1013,7 @@ uint32_t progress_times_any_good_ending_achieved(void) { } static ProgressBGMID progress_bgm_id(const char *bgm) { - static const char* map[] = { + static const char *map[] = { [PBGM_MENU] = "menu", [PBGM_STAGE1] = "stage1", [PBGM_STAGE1_BOSS] = "stage1boss", @@ -1035,6 +1035,8 @@ static ProgressBGMID progress_bgm_id(const char *bgm) { [PBGM_BONUS1] = "scuttle", [PBGM_GAMEOVER] = "gameover", [PBGM_INTRO] = "intro", + [PBGM_STAGEX] = "stagex", + [PBGM_STAGEX_BOSS] = "stagexboss", }; for(int i = 0; i < ARRAY_SIZE(map); ++i) { diff --git a/src/progress.h b/src/progress.h index d7d9a71170..9c11820413 100644 --- a/src/progress.h +++ b/src/progress.h @@ -49,6 +49,8 @@ typedef enum ProgressBGMID { PBGM_BONUS1, // Scuttle theme PBGM_GAMEOVER, PBGM_INTRO, + PBGM_STAGEX, + PBGM_STAGEX_BOSS, } ProgressBGMID; typedef struct GlobalProgress { diff --git a/src/stage.c b/src/stage.c index f826c61ac1..f1b56ffb19 100644 --- a/src/stage.c +++ b/src/stage.c @@ -756,6 +756,7 @@ TASK(clear_dialog) { void stage_begin_dialog(Dialog *d) { assert(global.dialog == NULL); + assert(!global.gameover); global.dialog = d; dialog_init(d); INVOKE_TASK_WHEN(&d->events.fadeout_ended, clear_dialog); diff --git a/src/stagedraw.c b/src/stagedraw.c index 18126a84ae..a122aaf76e 100644 --- a/src/stagedraw.c +++ b/src/stagedraw.c @@ -615,6 +615,10 @@ static bool powersurge_draw_predicate(EntityInterface *ent) { return i->type == ITEM_VOLTAGE; } + if(ent->type == ENT_TYPE_ID(YumemiSlave)) { + return true; + } + return false; } diff --git a/src/stages/entities.h b/src/stages/entities.h index e898528b8b..9de0dc60f4 100644 --- a/src/stages/entities.h +++ b/src/stages/entities.h @@ -15,6 +15,7 @@ #include "stage4/entities.h" #include "stage5/entities.h" #include "stage6/entities.h" +#include "stagex/entities.h" #define ENTITIES_STAGES(X, ...) \ ENTITIES_STAGE1(X, __VA_ARGS__) \ @@ -23,4 +24,5 @@ ENTITIES_STAGE4(X, __VA_ARGS__) \ ENTITIES_STAGE5(X, __VA_ARGS__) \ ENTITIES_STAGE6(X, __VA_ARGS__) \ + ENTITIES_STAGEX(X, __VA_ARGS__) \ END_OF_ENTITIES diff --git a/src/stages/meson.build b/src/stages/meson.build index f22bd6a422..21cb1c4730 100644 --- a/src/stages/meson.build +++ b/src/stages/meson.build @@ -10,6 +10,7 @@ stages = [ 'stage4', 'stage5', 'stage6', + 'stagex', ] foreach stage : stages @@ -17,10 +18,6 @@ foreach stage : stages stages_src += get_variable('@0@_src'.format(stage)) endforeach -stages_src += files( - 'extra.c', # stub -) - if use_testing_stages stages_src += files( 'dpstest.c', diff --git a/src/stages/stages.c b/src/stages/stages.c index d9018cc19b..476af85cfb 100644 --- a/src/stages/stages.c +++ b/src/stages/stages.c @@ -13,7 +13,7 @@ #include "stages/stage4/stage4.h" #include "stages/stage5/stage5.h" #include "stages/stage6/stage6.h" -#include "stages/extra.h" +#include "stages/stagex/stagex.h" #ifdef TAISEI_BUILDCONF_TESTING_STAGES #include "stages/dpstest.h" @@ -26,7 +26,9 @@ StagesExports stages_exports = { .stage4 = { &stage4_procs, (AttackInfo*)&stage4_spells }, .stage5 = { &stage5_procs, (AttackInfo*)&stage5_spells }, .stage6 = { &stage6_procs, (AttackInfo*)&stage6_spells }, - .stagex = { &extra_procs, NULL }, + .stagex = { &stagex_procs, (AttackInfo*)&stagex_spells }, + + .stagex_draw_boss_portrait_overlay = stagex_draw_yumemi_portrait_overlay, #ifdef TAISEI_BUILDCONF_TESTING_STAGES .testing = { diff --git a/src/stages/stages.h b/src/stages/stages.h index 651355821b..194e0de0eb 100644 --- a/src/stages/stages.h +++ b/src/stages/stages.h @@ -10,6 +10,7 @@ #include "taisei.h" #include "stageinfo.h" +#include "renderer/api.h" typedef struct StagesExports { struct { @@ -23,6 +24,9 @@ typedef struct StagesExports { AttackInfo *benchmark_spell; } testing; #endif + + // FIXME: exposing it like that feels kind of silly… + void (*stagex_draw_boss_portrait_overlay)(SpriteParams *sp); } StagesExports; #ifdef TAISEI_BUILDCONF_DYNSTAGE diff --git a/src/stages/stagex/background_anim.c b/src/stages/stagex/background_anim.c new file mode 100644 index 0000000000..199ba55236 --- /dev/null +++ b/src/stages/stagex/background_anim.c @@ -0,0 +1,228 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "background_anim.h" // IWYU pragma: keep +#include "draw.h" + +#include "random.h" +#include "stageutils.h" +#include "util/glm.h" + +TASK(animate_value, { float *val; float target; float rate; }) { + while(*ARGS.val != ARGS.target) { + fapproach_p(ARGS.val, ARGS.target, ARGS.rate); + YIELD; + } +} + +TASK(animate_value_asymptotic, { float *val; float target; float rate; float epsilon; }) { + if(ARGS.epsilon == 0) { + ARGS.epsilon = 1e-5; + } + + while(*ARGS.val != ARGS.target) { + fapproach_asymptotic_p(ARGS.val, ARGS.target, ARGS.rate, ARGS.epsilon); + YIELD; + } +} + +static void animate_bg_intro(StageXDrawData *draw_data) { + const float w = -0.005f; + float transition = 0.0; + float t = 0.0; + + stage_3d_context.cam.pos[2] = 2.0 * STAGEX_BG_SEGMENT_PERIOD/3; + stage_3d_context.cam.rot.pitch = 40; + stage_3d_context.cam.rot.roll = 10; + + float prev_x0 = stage_3d_context.cam.pos[0]; + float prev_x1 = stage_3d_context.cam.pos[1]; + + for(int frame = 0;; ++frame) { + float dt = 5.0f * (1.0f - transition); + float rad = 4.3f * glm_ease_back_in(glm_ease_sine_out(1.0f - transition)); + stage_3d_context.cam.rot.pitch = -10 + 50 * glm_ease_sine_in(1.0f - transition); + stage_3d_context.cam.pos[0] += rad*cosf(-w*t) - prev_x0; + stage_3d_context.cam.pos[1] += rad*sinf(-w*t) - prev_x1; + stage_3d_context.cam.pos[2] += w*5.6f/M_PI*dt; + prev_x0 = stage_3d_context.cam.pos[0]; + prev_x1 = stage_3d_context.cam.pos[1]; + stage_3d_context.cam.rot.roll -= 180.0f/M_PI*w*dt; + + t += dt; + + if(frame > 60) { + if(transition == 1.0f) { + return; + } + + if(frame == 61) { + INVOKE_TASK_DELAYED(60, animate_value, &stage_3d_context.cam.vel[2], -0.28f, 0.002f); + } + + fapproach_p(&transition, 1.0f, 1.0f/300.0f); + } + + YIELD; + } +} + +static void animate_bg_descent(StageXDrawData *draw_data, CoEvent *stop_condition) { + const float max_dist = 1.25f; + const float max_spin = RAD2DEG*0.01f; + float dist = 0.0f; + float target_dist = 0.0f; + float spin = 0.0f; + + CoEventSnapshot snap = coevent_snapshot(stop_condition); + + for(float a = 0; coevent_poll(stop_condition, &snap) == CO_EVENT_PENDING; a += 0.01) { + float r = smoothmin(dist, max_dist, max_dist * 0.5); + + stage_3d_context.cam.pos[0] = r * cos(a); + stage_3d_context.cam.pos[1] = -r * sin(a); + // stage_3d_context.cam.rot.roll += spin * sin(0.4331*a); + fapproach_asymptotic_p(&draw_data->tower_spin, spin * sin(0.331*a), 0.02, 1e-4); + fapproach_p(&spin, max_spin, max_spin / 240.0f); + + fapproach_asymptotic_p(&dist, target_dist, 0.01, 1e-4); + fapproach_asymptotic_p(&target_dist, 0, 0.03, 1e-4); + + if(rng_chance(0.02)) { + target_dist += max_dist; + } + + YIELD; + } +} + +static void animate_bg_midboss(StageXDrawData *draw_data, CoEvent *stop_condition) { + float camera_shift_rate = 0; + + // stagetext_add("Midboss time!", CMPLX(VIEWPORT_W, VIEWPORT_H)/2, ALIGN_CENTER, res_font("big"), RGB(1,1,1), 0, 120, 30, 30); + + CoEventSnapshot snap = coevent_snapshot(stop_condition); + + while(coevent_poll(stop_condition, &snap) == CO_EVENT_PENDING) { + // for(;;) { + fapproach_p(&camera_shift_rate, 1, 1.0f/120.0f); + fapproach_asymptotic_p(&draw_data->plr_influence, 0, 0.01, 1e-4); + fapproach_asymptotic_p(&stage_3d_context.cam.vel[2], -0.12, 0.02, 1e-4); + fapproach_asymptotic_p(&stage_3d_context.cam.rot.pitch, 30, 0.01 * camera_shift_rate, 1e-4); + + float a = glm_rad(stage_3d_context.cam.rot.roll + 67.5); + cmplxf p = cdir(a) * -3.5f; + fapproach_asymptotic_p(&stage_3d_context.cam.pos[0], re(p), 0.01 * camera_shift_rate, 1e-4); + fapproach_asymptotic_p(&stage_3d_context.cam.pos[1], im(p), 0.01 * camera_shift_rate, 1e-4); + + YIELD; + } +} + +static void animate_bg_post_midboss(StageXDrawData *draw_data, int anim_time) { + float center_distance = -3.5f; + float camera_shift_rate = 0; + + // stagetext_add("Midboss defeated!", CMPLX(VIEWPORT_W, VIEWPORT_H)/2, ALIGN_CENTER, res_font("big"), RGB(1,1,1), 0, 120, 30, 30); + + while(anim_time-- > 0) { + fapproach_p(&camera_shift_rate, 1, 1.0f/120.0f); + fapproach_asymptotic_p(&draw_data->plr_influence, 1, 0.01, 1e-4); + fapproach_p(&stage_3d_context.cam.vel[2], -0.56f, 0.002f); + fapproach_asymptotic_p(¢er_distance, 0, 0.03, 1e-4); + fapproach_asymptotic_p(&stage_3d_context.cam.rot.pitch, -10, 0.01 * camera_shift_rate, 1e-4); + + float a = glm_rad(stage_3d_context.cam.rot.roll + 67.5); + cmplxf p = cdir(a) * center_distance; + fapproach_asymptotic_p(&stage_3d_context.cam.pos[0], re(p), 0.01 * camera_shift_rate, 1e-4); + fapproach_asymptotic_p(&stage_3d_context.cam.pos[1], im(p), 0.01 * camera_shift_rate, 1e-4); + + YIELD; + } +} + +TASK(animate_light, { StageXDrawData *draw_data; }) { + StageXDrawData *draw_data = ARGS.draw_data; + + float rate = 1.0f/20.0f; + + for(;;) { + Color c; + c.r = 0.5f + 0.5f * psinf(draw_data->fog.t); + c.g = 0.25f * c.r * powf(pcosf(draw_data->fog.t), 1.25f); + c.b = c.r * c.g; + + float w = 1.0f - sqrtf(erff(8.0f * c.g)); + w = lerpf(1.0f, w, draw_data->fog.red_flash_intensity); +// c.b += w*w*w*w*w; + + float b = 0.1; + c.r = lerpf(c.r, 0.3f*b, w); + c.g = lerpf(c.g, 0.1f*b, w); + c.b = lerpf(c.b, 0.8f*b, w); + c.a = 1.0f; + + color_lerp(&c, RGBA(1, 0.5, 0.6, 1), 0.1); + draw_data->fog.color = c; + + YIELD; + draw_data->fog.t += rate; + } +} + +#include "camcontrol.h" + +TASK(animate_bg, { StageXDrawData *draw_data; }) { + StageXDrawData *draw_data = ARGS.draw_data; + draw_data->fog.exponent = 4; + + INVOKE_TASK(animate_light, draw_data); + + INVOKE_TASK_DELAYED(140, animate_value, &draw_data->fog.opacity, 1.0f, 1.0f/80.0f); + INVOKE_TASK_DELAYED(140, animate_value_asymptotic, &draw_data->fog.exponent, 42.0f, 0.004f); + INVOKE_TASK(animate_value, &draw_data->plr_influence, 1.0f, 1.0f/120.0f); + animate_bg_intro(draw_data); + INVOKE_TASK_DELAYED(520, animate_value, &draw_data->fog.red_flash_intensity, 1.0f, 1.0f/320.0f); + + // glm_vec3_zero(stage_3d_context.cam.vel); + // camcontrol_init(&stage_3d_context.cam); + // return; + + INVOKE_TASK_DELAYED(200, animate_value, &stage_3d_context.cam.vel[2], -0.36f, 0.002f); + animate_bg_descent(draw_data, &draw_data->events.next_phase); + animate_bg_midboss(draw_data, &draw_data->events.next_phase); + animate_bg_post_midboss(draw_data, 60 * 7); + animate_bg_descent(draw_data, &draw_data->events.next_phase); + INVOKE_TASK(animate_value_asymptotic, &draw_data->fog.exponent, 8.0f, 0.01f); + INVOKE_TASK(animate_value, &draw_data->tower_global_dissolution, 1.0f, 1.0f/500.0f); + INVOKE_TASK(animate_value_asymptotic, &stage_3d_context.cam.pos[0], 0, 0.02, 1e-4); + INVOKE_TASK(animate_value_asymptotic, &stage_3d_context.cam.pos[1], 0, 0.02, 1e-4); +} + +void stagex_bg_trigger_next_phase(void) { + StageXDrawData *draw_data = stagex_get_draw_data(); + coevent_signal(&draw_data->events.next_phase); +} + +void stagex_bg_trigger_tower_dissolve(void) { + StageXDrawData *draw_data = stagex_get_draw_data(); + INVOKE_TASK(animate_value, &draw_data->tower_partial_dissolution, 1.0f, 1.0f/600.0f); +} + +void stagex_bg_init_fullstage(void) { + StageXDrawData *draw_data = stagex_get_draw_data(); + + Camera3D *cam = &stage_3d_context.cam; +// cam->aspect = STAGE3D_DEFAULT_ASPECT; // FIXME +// cam->pos[0] = 0; +// cam->pos[1] = 0; +// cam->vel[2] = -0.56f; +// cam->rot.v[0] = 0; + + INVOKE_TASK(animate_bg, draw_data); +} diff --git a/src/stages/stagex/background_anim.h b/src/stages/stagex/background_anim.h new file mode 100644 index 0000000000..119e600025 --- /dev/null +++ b/src/stages/stagex/background_anim.h @@ -0,0 +1,14 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#pragma once +#include "taisei.h" + +void stagex_bg_init_fullstage(void); +void stagex_bg_trigger_next_phase(void); +void stagex_bg_trigger_tower_dissolve(void); diff --git a/src/stages/stagex/draw.c b/src/stages/stagex/draw.c new file mode 100644 index 0000000000..8025c6a34a --- /dev/null +++ b/src/stages/stagex/draw.c @@ -0,0 +1,485 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "draw.h" + +#include "color.h" +#include "global.h" +#include "random.h" +#include "renderer/api.h" +#include "stagedraw.h" +#include "stageutils.h" +#include "util.h" +#include "util/compat.h" +#include "util/glm.h" +#include "util/graphics.h" +#include "util/io.h" +#include "util/miscmath.h" + +static StageXDrawData *draw_data; + +/* + * BEGIN utils + */ + +StageXDrawData *stagex_get_draw_data(void) { + return draw_data; +} + +bool stagex_drawing_into_glitchmask(void) { + return r_framebuffer_current() == draw_data->fb.glitch_mask; +} + +static float fsplitmix(uint32_t *state) { + return (float)(splitmix32(state) / (double)UINT32_MAX); +} + +static bool should_draw_tower(void) { + return draw_data->tower_global_dissolution < 0.999; +} + +static bool should_draw_tower_postprocess(void) { + return should_draw_tower() && draw_data->tower_partial_dissolution > 0; +} + +/* + * END utils + */ + +/* + * BEGIN background render + */ + +static void lightwave(Camera3D *cam, PointLight3D *light, float t) { + float s = -sawtooth(t); + float a = 1.0f - s * s; + a = smoothstep(0.0, 1.0, a); + a = smoothstep(0.0, 1.0, a); + + glm_vec3_scale(light->pos, s, light->pos); + glm_vec3_add(light->pos, cam->pos, light->pos); + glm_vec3_scale(light->radiance, a, light->radiance); +} + +static void stagex_bg_setup_pbr_lighting(Camera3D *cam, vec3 seg_pos) { + StageXDrawData *draw_data = stagex_get_draw_data(); + + float p = 150; + float f = 10000 * draw_data->fog.opacity; + + Color c = draw_data->fog.color; + color_mul_scalar(&c, f); + + float l = 500; + + PointLight3D lights[] = { + { { cam->pos[0], cam->pos[1], cam->pos[2] }, { 0.8, 0.5, 0.6 } }, + { { 0, 0, STAGEX_BG_MAX_RANGE/6 }, { l, l/4, 0 } }, + { { 0, 0, cam->pos[2] - 60 }, { 100, 0, 500 } }, + }; + + lightwave(cam, lights+1, draw_data->fog.t / M_TAU + 0.1f); + + glm_vec3_scale(lights[0].radiance, p, lights[0].radiance); + glm_vec3_scale(lights[1].radiance, draw_data->fog.red_flash_intensity, lights[1].radiance); + + vec3 r; + camera3d_unprojected_ray(cam, global.plr.pos, r); + glm_vec3_scale(r, 2, r); + glm_vec3_add(lights[0].pos, r, lights[0].pos); + + camera3d_set_point_light_uniforms(cam, ARRAY_SIZE(lights), lights); +} + +static void stagex_bg_setup_pbr_env(Camera3D *cam, PBREnvironment *env, vec3 seg_pos) { + stagex_bg_setup_pbr_lighting(cam, seg_pos); + env->environment_map = draw_data->env_map; + // glm_vec3_broadcast(0.5, env->ambient_color); + // glm_vec3_copy((vec3) { 0.5, 0.3, 0.0 }, env->ambient_color); + camera3d_apply_inverse_transforms(cam, env->cam_inverse_transform); +} + +static void bg_begin_3d(void) { + // stage_3d_context.cam.rot.pitch += draw_data->plr_pitch; + // stage_3d_context.cam.rot.yaw += draw_data->plr_yaw; +} + +static void bg_end_3d(void) { + // stage_3d_context.cam.rot.pitch -= draw_data->plr_pitch; + // stage_3d_context.cam.rot.yaw -= draw_data->plr_yaw; +} + +static uint bg_tower_pos(Stage3D *s3d, vec3 pos, float maxrange) { + vec3 p = { 0, 0, 0 }; + vec3 r = { 0, 0, -STAGEX_BG_SEGMENT_PERIOD }; + return stage3d_pos_ray_nearfirst_nsteps(s3d, pos, p, r, 15, 0); +} + +static void bg_tower_draw_solid(vec3 pos) { + r_state_push(); + r_mat_mv_push(); + r_mat_mv_translate_v(pos); + + r_shader("pbr"); + + PBREnvironment env = { 0 }; + stagex_bg_setup_pbr_env(&stage_3d_context.cam, &env, pos); + + pbr_draw_model(&draw_data->models.metal, &env); + pbr_draw_model(&draw_data->models.stairs, &env); + pbr_draw_model(&draw_data->models.wall, &env); + + r_mat_mv_pop(); + r_state_pop(); +} + +static void bg_tower_draw_mask(vec3 pos) { + r_state_push(); + r_mat_mv_push(); + r_mat_mv_translate_v(pos); + + uint32_t tmp = float_to_bits(pos[2]); + float phase = fsplitmix(&tmp); + r_shader("extra_tower_mask"); + r_uniform_sampler("tex_noise", "cell_noise"); + r_uniform_sampler("tex_mask", "stagex/dissolve_mask"); + r_uniform_float("time", global.frames/60.0f + phase * M_PI * 2.0f); + r_uniform_float("dissolve", draw_data->tower_partial_dissolution * draw_data->tower_partial_dissolution); + + mat4 inv, w; + camera3d_apply_inverse_transforms(&stage_3d_context.cam, inv); + glm_mat4_mul(inv, *r_mat_mv_current_ptr(), w); + r_uniform_mat4("world_from_model", w); + + r_uniform_sampler("tex_mod", NOT_NULL(draw_data->models.metal.mat->roughness_map)); + r_draw_model_ptr(draw_data->models.metal.mdl, 0, 0); + r_uniform_sampler("tex_mod", NOT_NULL(draw_data->models.stairs.mat->roughness_map)); + r_draw_model_ptr(draw_data->models.stairs.mdl, 0, 0); + r_uniform_sampler("tex_mod", NOT_NULL(draw_data->models.wall.mat->roughness_map)); + r_draw_model_ptr(draw_data->models.wall.mdl, 0, 0); + + r_mat_mv_pop(); + r_state_pop(); +} + +static void set_bg_uniforms(void) { + r_uniform_sampler("background_tex", "stagex/bg"); + r_uniform_sampler("background_binary_tex", "stagex/bg_binary"); + r_uniform_sampler("code_tex", "stagex/code"); + r_uniform_vec4("code_tex_params", + draw_data->codetex_aspect[0], + draw_data->codetex_aspect[1], + draw_data->codetex_num_segments, + 1.0f / draw_data->codetex_num_segments + ); + + r_uniform_float("global_dissolve", draw_data->tower_global_dissolution * 5); + r_uniform_float("time", global.frames / 60.0f); +} + +void stagex_draw_background(void) { + if(should_draw_tower()) { + bg_begin_3d(); + + Stage3DSegment segs[] = { + { bg_tower_draw_solid, bg_tower_pos }, + }; + + stage3d_draw(&stage_3d_context, STAGEX_BG_MAX_RANGE, ARRAY_SIZE(segs), segs); + + bg_end_3d(); + } else { + r_state_push(); + r_mat_proj_push_ortho(VIEWPORT_W, VIEWPORT_H); + r_mat_mv_push_identity(); + r_disable(RCAP_DEPTH_TEST); + r_shader("extra_bg"); + set_bg_uniforms(); + r_mat_mv_scale(VIEWPORT_W, VIEWPORT_H, 1); + r_mat_mv_translate(0.5, 0.5, 0); + r_draw_quad(); + r_mat_mv_pop(); + r_mat_proj_pop(); + r_state_pop(); + } +} + +/* + * END background render + */ + +/* + * BEGIN background effects + */ + +static void render_tower_mask(Framebuffer *fb) { + r_state_push(); + r_mat_proj_push(); + r_enable(RCAP_DEPTH_TEST); + + r_framebuffer(fb); + r_clear(BUFFER_ALL, RGBA(1, 1, 1, draw_data->tower_global_dissolution), 1); + + bg_begin_3d(); + Stage3DSegment segs[] = { + { bg_tower_draw_mask, bg_tower_pos }, + }; + + stage3d_draw(&stage_3d_context, STAGEX_BG_MAX_RANGE, ARRAY_SIZE(segs), segs); + bg_end_3d(); + + r_mat_proj_pop(); + r_state_pop(); +} + +static bool bg_effect_tower_mask(Framebuffer *fb) { + if(!should_draw_tower_postprocess()) { + return false; + } + + Framebuffer *mask_fb = draw_data->fb.tower_mask; + render_tower_mask(mask_fb); + + r_blend(BLEND_NONE); + + r_state_push(); + r_enable(RCAP_DEPTH_TEST); + r_enable(RCAP_DEPTH_WRITE); + r_depth_func(DEPTH_ALWAYS); + r_shader("extra_tower_apply_mask"); + r_uniform_sampler("mask_tex", r_framebuffer_get_attachment(mask_fb, FRAMEBUFFER_ATTACH_COLOR0)); + r_uniform_sampler("depth_tex", r_framebuffer_get_attachment(fb, FRAMEBUFFER_ATTACH_DEPTH)); + set_bg_uniforms(); + draw_framebuffer_tex(fb, VIEWPORT_W, VIEWPORT_H); + r_state_pop(); + + return true; +} + +static bool bg_effect_copy_depth(Framebuffer *fb) { + if(should_draw_tower_postprocess()) { + r_framebuffer_copy(r_framebuffer_current(), fb, BUFFER_DEPTH); + } + + return false; +} + +static bool bg_effect_fog(Framebuffer *fb) { + if(!should_draw_tower()) { + return false; + } + + Color c = draw_data->fog.color; + color_mul_scalar(&c, draw_data->fog.opacity); + + r_shader("zbuf_fog"); + r_uniform_sampler("depth", r_framebuffer_get_attachment(fb, FRAMEBUFFER_ATTACH_DEPTH)); + r_uniform_vec4_rgba("fog_color", &c); + r_uniform_float("start", 0.0); + r_uniform_float("end", 1.0); + r_uniform_float("exponent", draw_data->fog.exponent); + r_uniform_float("sphereness", 0.0); + draw_framebuffer_tex(fb, VIEWPORT_W, VIEWPORT_H); + r_shader_standard(); + return true; +} + +ShaderRule stagex_bg_effects[] = { + bg_effect_tower_mask, + bg_effect_copy_depth, + bg_effect_fog, + NULL +}; + +/* + * END background effects + */ + +/* + * BEGIN postprocess + */ + +static bool glitchmask_draw_predicate(EntityInterface *ent) { + switch(ent->type) { + case ENT_TYPE_ID(Enemy): + case ENT_TYPE_ID(YumemiSlave): +// case ENT_TYPE_ID(BossShield): + return true; + + default: + return false; + } +} + +static void render_glitch_mask(Framebuffer *fb) { + r_state_push(); + r_framebuffer(fb); + r_clear(BUFFER_ALL, RGBA(0, 0, 0, 0), 1); + r_shader("sprite_default"); + ent_draw(glitchmask_draw_predicate); + r_state_pop(); +} + +static bool postprocess_glitch(Framebuffer *fb) { + Framebuffer *glitch_mask = draw_data->fb.glitch_mask; + render_glitch_mask(glitch_mask); + + vec3 t = { 0.143133f, 0.53434f, 0.25332f }; + glm_vec3_adds(t, global.frames / 60.0f, t); + + r_state_push(); + r_clear(BUFFER_ALL, RGBA(0, 0, 0, 1), 1); + r_blend(BLEND_NONE); + r_shader("extra_glitch"); + r_uniform_vec3_vec("times", t); + r_uniform_sampler("mask", r_framebuffer_get_attachment(glitch_mask, FRAMEBUFFER_ATTACH_COLOR0)); + draw_framebuffer_tex(fb, VIEWPORT_W, VIEWPORT_H); + r_state_pop(); + + return true; +} + +ShaderRule stagex_postprocess_effects[] = { + postprocess_glitch, + NULL +}; + +/* + * END postprocess + */ + +/* + * BEGIN camera update loop + */ + +TASK(update_camera) { + for(;;) { + stage3d_update(&stage_3d_context); + stage_3d_context.cam.rot.roll += draw_data->tower_spin; + float p = draw_data->plr_influence; + float yaw = 10.0f * (re(global.plr.pos) / VIEWPORT_W - 0.5f) * p; + float pitch = 10.0f * (im(global.plr.pos) / VIEWPORT_H - 0.5f) * p; + fapproach_asymptotic_p(&draw_data->plr_yaw, yaw, 0.03, 1e-4); + fapproach_asymptotic_p(&draw_data->plr_pitch, pitch, 0.03, 1e-4); + YIELD; + } +} + +/* + * END camera update loop + */ + +/* + * BEGIN init/shutdown + */ + +static void init_tower_mask_fb(void) { + FBAttachmentConfig a[2] = { 0 }; + a[0].attachment = FRAMEBUFFER_ATTACH_COLOR0; + a[0].tex_params.type = TEX_TYPE_RGBA_16; + a[0].tex_params.filter.min = TEX_FILTER_LINEAR; + a[0].tex_params.filter.mag = TEX_FILTER_LINEAR; + a[0].tex_params.width = VIEWPORT_W; + a[0].tex_params.height = VIEWPORT_H; + a[0].tex_params.wrap.s = TEX_WRAP_MIRROR; + a[0].tex_params.wrap.t = TEX_WRAP_MIRROR; + + a[1].attachment = FRAMEBUFFER_ATTACH_DEPTH; + a[1].tex_params.type = TEX_TYPE_DEPTH; + a[1].tex_params.filter.min = TEX_FILTER_NEAREST; + a[1].tex_params.filter.mag = TEX_FILTER_NEAREST; + a[1].tex_params.width = VIEWPORT_W; + a[1].tex_params.height = VIEWPORT_H; + a[1].tex_params.wrap.s = TEX_WRAP_MIRROR; + a[1].tex_params.wrap.t = TEX_WRAP_MIRROR; + + draw_data->fb.tower_mask = stage_add_background_framebuffer("Tower mask", 0.25, 0.5, ARRAY_SIZE(a), a); +} + +static void init_glitch_mask_fb(void) { + FBAttachmentConfig a[1] = { 0 }; + a[0].attachment = FRAMEBUFFER_ATTACH_COLOR0; + a[0].tex_params.type = TEX_TYPE_RGB_8; + a[0].tex_params.filter.min = TEX_FILTER_NEAREST; + a[0].tex_params.filter.mag = TEX_FILTER_NEAREST; + a[0].tex_params.width = VIEWPORT_W / 30; + a[0].tex_params.height = VIEWPORT_H / 20; + a[0].tex_params.wrap.s = TEX_WRAP_REPEAT; + a[0].tex_params.wrap.t = TEX_WRAP_REPEAT; + + draw_data->fb.glitch_mask = stage_add_static_framebuffer("Glitch mask", ARRAY_SIZE(a), a); +} + +static void init_spellbg_fb(void) { + FBAttachmentConfig a[1] = { 0 }; + a[0].attachment = FRAMEBUFFER_ATTACH_COLOR0; + a[0].tex_params.type = TEX_TYPE_RGB_8; + a[0].tex_params.filter.min = TEX_FILTER_LINEAR; + a[0].tex_params.filter.mag = TEX_FILTER_LINEAR; + a[0].tex_params.width = VIEWPORT_W / 30; + a[0].tex_params.height = VIEWPORT_H / 20; + a[0].tex_params.wrap.s = TEX_WRAP_REPEAT; + a[0].tex_params.wrap.t = TEX_WRAP_REPEAT; + + draw_data->fb.spell_background_lq = stage_add_background_framebuffer("Extra spell bg", 0.25, 0.5, ARRAY_SIZE(a), a); +} + +void stagex_drawsys_init(void) { + draw_data = ALLOC(typeof(*draw_data)); + + stage3d_init(&stage_3d_context, 16); + stage_3d_context.cam.far = 128; + + init_tower_mask_fb(); + init_glitch_mask_fb(); + init_spellbg_fb(); + + SDL_RWops *stream = vfs_open("res/gfx/stagex/code.num_slices", VFS_MODE_READ); + + if(!stream) { + log_fatal("VFS error: %s", vfs_get_error()); + } + + char buf[32]; + SDL_RWgets(stream, buf, sizeof(buf)); + draw_data->codetex_num_segments = strtol(buf, NULL, 0); + SDL_RWclose(stream); + + Texture *tex_code = res_texture("stagex/code"); + uint w, h; + r_texture_get_size(tex_code, 0, &w, &h); + + // FIXME: this doesn't seem right, i don't know what the fuck i'm doing! + float viewport_aspect = (float)VIEWPORT_W / (float)VIEWPORT_H; + float seg_w = w / draw_data->codetex_num_segments; + float seg_h = h; + float seg_aspect = seg_w / seg_h; + draw_data->codetex_aspect[0] = 1; + draw_data->codetex_aspect[1] = 0.5/(seg_aspect/viewport_aspect); + + pbr_load_model(&draw_data->models.metal, "stage5/metal", "stage5/metal"); + pbr_load_model(&draw_data->models.stairs, "stage5/stairs", "stage5/stairs"); + pbr_load_model(&draw_data->models.wall, "stage5/wall", "stage5/wall"); + + draw_data->env_map = res_texture("stage5/envmap"); + + COEVENT_INIT_ARRAY(draw_data->events); + + INVOKE_TASK(update_camera); +} + +void stagex_drawsys_shutdown(void) { + COEVENT_CANCEL_ARRAY(draw_data->events); + mem_free(draw_data); + draw_data = NULL; + stage3d_shutdown(&stage_3d_context); +} + +/* + * END init/shutdown + */ diff --git a/src/stages/stagex/draw.h b/src/stages/stagex/draw.h new file mode 100644 index 0000000000..b210653fd2 --- /dev/null +++ b/src/stages/stagex/draw.h @@ -0,0 +1,65 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#pragma once +#include "taisei.h" + +#include "renderer/api.h" +#include "stageinfo.h" +#include "stageutils.h" + +typedef struct StageXDrawData { + float plr_yaw; + float plr_pitch; + float plr_influence; + + struct { + Color color; + float red_flash_intensity; + float opacity; + float exponent; + float t; + } fog; + + struct { + Framebuffer *tower_mask; + Framebuffer *glitch_mask; + Framebuffer *spell_background_lq; + } fb; + + struct { + PBRModel metal; + PBRModel stairs; + PBRModel wall; + } models; + + Texture *env_map; + + float codetex_num_segments; + float codetex_aspect[2]; + float tower_global_dissolution; + float tower_partial_dissolution; + float tower_spin; + + COEVENTS_ARRAY(next_phase) events; +} StageXDrawData; + +void stagex_drawsys_init(void); +void stagex_drawsys_shutdown(void); + +StageXDrawData *stagex_get_draw_data(void) + attr_returns_nonnull attr_returns_max_aligned; + +bool stagex_drawing_into_glitchmask(void); +void stagex_draw_background(void); + +extern ShaderRule stagex_bg_effects[]; +extern ShaderRule stagex_postprocess_effects[]; + +#define STAGEX_BG_SEGMENT_PERIOD 11.2 +#define STAGEX_BG_MAX_RANGE (64 * STAGEX_BG_SEGMENT_PERIOD) diff --git a/src/stages/extra.c b/src/stages/stagex/entities.h similarity index 63% rename from src/stages/extra.c rename to src/stages/stagex/entities.h index b715817b3e..efc34a7456 100644 --- a/src/stages/extra.c +++ b/src/stages/stagex/entities.h @@ -6,8 +6,10 @@ * Copyright (c) 2012-2024, Andrei Alexeyev . */ -#include "extra.h" +#pragma once +#include "taisei.h" -// NOTE: See the 'yumemi' branch for the work-in-progress extra stage - -StageProcs extra_procs = { }; +#define ENTITIES_STAGEX(X, ...) \ + X(YumemiSlave, __VA_ARGS__) \ + X(BossShield, __VA_ARGS__) \ + END_OF_ENTITIES diff --git a/src/stages/stagex/meson.build b/src/stages/stagex/meson.build new file mode 100644 index 0000000000..2617c934dd --- /dev/null +++ b/src/stages/stagex/meson.build @@ -0,0 +1,17 @@ + +stagex_src = files( + 'background_anim.c', + 'draw.c', + 'nonspells/boss_nonspells.c', + 'spells/trap_representation.c', + 'spells/fork_bomb.c', + 'spells/infinity_net.c', + 'spells/sierpinski.c', + 'spells/mem_copy.c', + 'spells/pipe_dream.c', + 'spells/alignment.c', + 'spells/rings.c', + 'stagex.c', + 'timeline.c', + 'yumemi.c', +) diff --git a/src/stages/stagex/nonspells/boss_nonspells.c b/src/stages/stagex/nonspells/boss_nonspells.c new file mode 100644 index 0000000000..672fe437a7 --- /dev/null +++ b/src/stages/stagex/nonspells/boss_nonspells.c @@ -0,0 +1,493 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "nonspells.h" + +TASK(wallproj, { BoxedProjectile p; }) { + Projectile *p = TASK_BIND(ARGS.p); + + for(;;YIELD) { + real x = re(p->pos); + real y = im(p->pos); + real th = 16 * rng_sreal(); + + if(x < th || x > VIEWPORT_W - th || y < th || y > VIEWPORT_H - th) { + break; + } + } + + p->move.acceleration = 0; + p->move.retention = 1; + p->move.velocity = 0; + + projectile_set_prototype(p, pp_ball); + p->color.a = 0; + spawn_projectile_highlight_effect(p); + + real rate = 0; + + for(;;YIELD) { + p->color.g = rate; + p->move.acceleration = rate * 0.05 * cnormalize(global.plr.pos - p->pos); +// p->move.acceleration *= 0.6; +// p->move.acceleration += rate * 0.05 * cnormalize(global.plr.pos - p->pos); + rate = approach(rate, 1, 0.0002); + } +} + +static void yumemi_slave_aimed_burst(YumemiSlave *slave, int count, real *a, real s, bool wallproj) { + for(int burst = 0; burst < count; ++burst) { + int cnt = 3; + + for(int i = 0; i < cnt; ++i) { + play_sfx_loop("shot1_loop"); + + cmplx pos = slave->pos + 32 * cdir(s * (*a + i*M_TAU/cnt)); + cmplx aim = cnormalize(global.plr.pos - pos); + aim *= 10; + + Projectile *p = PROJECTILE( + .proto = pp_rice, + .color = RGBA(1, 0, 0.25, 0), + .pos = pos, + .move = move_asymptotic_simple(aim, -2), + ); + + if(wallproj) { + INVOKE_TASK(wallproj, ENT_BOX(p)); + } + + YIELD; + } + + *a += M_PI/32; + } + + WAIT(20); +} + +static void yumemi_slave_aimed_funnel(YumemiSlave *slave, int count, real *a, real s, bool wallproj) { + real max_angle = M_PI/2; + real min_angle = M_PI/24; + + for(int burst = 0; burst < count; ++burst) { + real bf = burst / (count - 1.0); + cmplx rot = cdir(lerp(max_angle, min_angle, bf) * 0.03); + + int cnt = 3; + for(int i = 0; i < cnt; ++i) { + play_sfx_loop("shot1_loop"); + + cmplx pos = slave->pos + 32 * cdir(s * (*a + i*M_TAU/cnt)); + cmplx aim = cnormalize(global.plr.pos - pos); + aim *= 10; + + Projectile *p; + + p = PROJECTILE( + .proto = pp_crystal, + .color = RGBA(1, 0.4, 1 - bf, 0), + .pos = pos, + // .move = move_asymptotic_simple(aim * rot, -2), + .move = { .velocity = aim, .retention = rot, }, + ); + + if(wallproj) { + INVOKE_TASK(wallproj, ENT_BOX(p)); + } + + p = PROJECTILE( + .proto = pp_crystal, + .color = RGBA(1, 0.4, 1 - bf, 0), + .pos = pos, + // .move = move_asymptotic_simple(aim / rot, -2), + .move = { .velocity = aim, .retention = 1/rot, }, + ); + + if(wallproj) { + INVOKE_TASK(wallproj, ENT_BOX(p)); + } + + YIELD; + } + + *a += M_PI/32; + } + + WAIT(20); +} + +TASK(yumemi_opening_slave, { + cmplx pos; + int type; + CoEvent *sync_event; +}) { + YumemiSlave *slave = stagex_host_yumemi_slave(ARGS.pos, ARGS.type); + WAIT_EVENT_ONCE(ARGS.sync_event); + + WAIT(ARGS.type * 60); + + real s = 1 - 2 * ARGS.type; + real a = 0; + + for(;;) { + yumemi_slave_aimed_burst(slave, 25, &a, s, false); + stagex_yumemi_slave_laser_sweep(slave, s, global.plr.pos); + WAIT(40); + } +} + +DEFINE_EXTERN_TASK(stagex_boss_nonspell_1) { + STAGE_BOOKMARK(non1); + + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + + CoEvent *sync_event = &ARGS.attack->events.started; + + cmplx p = VIEWPORT_W/2 + 100*I; + real xofs = 140; + + INVOKE_SUBTASK_DELAYED(20, yumemi_opening_slave, + .pos = p - xofs, + .type = 0, + .sync_event = sync_event + ); + + INVOKE_SUBTASK_DELAYED(40, yumemi_opening_slave, + .pos = p + xofs, + .type = 1, + .sync_event = sync_event + ); + + WAIT_EVENT_ONCE(&ARGS.attack->events.initiated); + boss->move = move_towards(boss->move.velocity, VIEWPORT_W/2 + 180*I, 0.015); + BEGIN_BOSS_ATTACK(&ARGS); + AWAIT_SUBTASKS; +} + +TASK(yumemi_non2_slave, { + cmplx pos; + int type; + CoEvent *sync_event; +}) { + YumemiSlave *slave = stagex_host_yumemi_slave(ARGS.pos, ARGS.type); + WAIT_EVENT_ONCE(ARGS.sync_event); + + WAIT(ARGS.type * 60); + + real s = 1 - 2 * ARGS.type; + real a = 0; + + for(;;) { + yumemi_slave_aimed_funnel(slave, 20, &a, s, false); + WAIT(10); + stagex_yumemi_slave_laser_sweep(slave, s, global.plr.pos); + WAIT(40); + // yumemi_slave_aimed_burst(slave, 15, &a, s); + } +} + +DEFINE_EXTERN_TASK(stagex_boss_nonspell_2) { + STAGE_BOOKMARK(non2); + + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + CoEvent *sync_event = &ARGS.attack->events.started; + + cmplx p = VIEWPORT_W/2 + 100*I; + real xofs = 140; + + INVOKE_SUBTASK_DELAYED(20, yumemi_non2_slave, + .pos = p - xofs, + .type = 0, + .sync_event = sync_event + ); + + INVOKE_SUBTASK_DELAYED(45, yumemi_non2_slave, + .pos = p + xofs, + .type = 1, + .sync_event = sync_event + ); + + boss->move = move_towards(boss->move.velocity, VIEWPORT_W/2 + 180*I, 0.015); + + BEGIN_BOSS_ATTACK(&ARGS); + AWAIT_SUBTASKS; +} + +TASK(yumemi_non3_slave, { + cmplx pos; + int type; + CoEvent *sync_event; +}) { + YumemiSlave *slave = stagex_host_yumemi_slave(ARGS.pos, ARGS.type); + WAIT_EVENT_ONCE(ARGS.sync_event); + + WAIT(ARGS.type * 60); + + real s = 1 - 2 * ARGS.type; + real a = 0; + + for(;;) { + yumemi_slave_aimed_funnel(slave, 20, &a, s, false); + WAIT(10); + stagex_yumemi_slave_laser_sweep(slave, s, global.plr.pos); + WAIT(40); + yumemi_slave_aimed_burst(slave, 15, &a, s, false); + } +} + +DEFINE_EXTERN_TASK(stagex_boss_nonspell_3) { + STAGE_BOOKMARK(non3); + + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + CoEvent *sync_event = &ARGS.attack->events.started; + + cmplx p = VIEWPORT_W/2 + 100*I; + real xofs = 140; + + INVOKE_SUBTASK_DELAYED(20, yumemi_non3_slave, + .pos = p - xofs, + .type = 0, + .sync_event = sync_event + ); + + INVOKE_SUBTASK_DELAYED(45, yumemi_non3_slave, + .pos = p + xofs, + .type = 1, + .sync_event = sync_event + ); + + boss->move = move_towards(boss->move.velocity, VIEWPORT_W/2 + 180*I, 0.015); + + BEGIN_BOSS_ATTACK(&ARGS); + AWAIT_SUBTASKS; +} + +TASK(yumemi_non4_slave, { + cmplx pos; + int type; + CoEvent *sync_event; +}) { + YumemiSlave *slave = stagex_host_yumemi_slave(ARGS.pos, ARGS.type); + WAIT_EVENT_ONCE(ARGS.sync_event); + + WAIT(ARGS.type * 60); + + real s = 1 - 2 * ARGS.type; + real a = 0; + + for(;;) { + yumemi_slave_aimed_burst(slave, 15, &a, s, true); + WAIT(60); + } +} + +DEFINE_EXTERN_TASK(stagex_boss_nonspell_4) { + STAGE_BOOKMARK(non4); + + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + CoEvent *sync_event = &ARGS.attack->events.started; + + cmplx p = VIEWPORT_W/2 + 100*I; + real xofs = 140; + + INVOKE_SUBTASK_DELAYED(20, yumemi_non4_slave, + .pos = p - xofs, + .type = 0, + .sync_event = sync_event + ); + + INVOKE_SUBTASK_DELAYED(45, yumemi_non4_slave, + .pos = p + xofs, + .type = 1, + .sync_event = sync_event + ); + + boss->move = move_towards(boss->move.velocity, VIEWPORT_W/2 + 180*I, 0.015); + + BEGIN_BOSS_ATTACK(&ARGS); + AWAIT_SUBTASKS; +} + +TASK(yumemi_non5_slave, { + cmplx pos; + int type; + CoEvent *sync_event; +}) { + YumemiSlave *slave = stagex_host_yumemi_slave(ARGS.pos, ARGS.type); + WAIT_EVENT_ONCE(ARGS.sync_event); + + WAIT(ARGS.type * 60); + + real s = 1 - 2 * ARGS.type; + real a = 0; + + for(int i = 0; i < 2 * ARGS.type; ++i) { + yumemi_slave_aimed_burst(slave, 15, &a, s, true); + WAIT(60); + } + + for(;;) { + int charge_time = 60; + + INVOKE_SUBTASK(common_charge, + .pos = slave->pos, + .color = RGBA(0.3, 0.3, 0.8, 0), + .time = charge_time, + .sound = COMMON_CHARGE_SOUNDS + ); + + WAIT(charge_time); + + yumemi_slave_aimed_funnel(slave, 10, &a, s, true); + WAIT(60); + + for(int i = 0; i < 5; ++i) { + yumemi_slave_aimed_burst(slave, 15, &a, s, true); + WAIT(60); + } + } +} + +DEFINE_EXTERN_TASK(stagex_boss_nonspell_5) { + STAGE_BOOKMARK(non5); + + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + CoEvent *sync_event = &ARGS.attack->events.started; + + cmplx p = VIEWPORT_W/2 + 100*I; + real xofs = 140; + + INVOKE_SUBTASK_DELAYED(20, yumemi_non5_slave, + .pos = p - xofs, + .type = 0, + .sync_event = sync_event + ); + + INVOKE_SUBTASK_DELAYED(45, yumemi_non5_slave, + .pos = p + xofs, + .type = 1, + .sync_event = sync_event + ); + + boss->move = move_towards(boss->move.velocity, VIEWPORT_W/2 + 180*I, 0.015); + + BEGIN_BOSS_ATTACK(&ARGS); + AWAIT_SUBTASKS; +} + +TASK(yumemi_non6_slave, { + cmplx pos; + cmplx center; + int type; +}) { + YumemiSlave *slave = stagex_host_yumemi_slave(ARGS.pos, ARGS.type); + + WAIT(120); + + real a = 0; + real s = 1 - ARGS.type * 2; + +// yumemi_slave_aimed_lasers(slave, 1, global.plr.pos); +// yumemi_slave_aimed_funnel(slave, 10, &a, s, false); + + cmplx r = cdir(0); + + for(;;) { + int cnt = 12; + + for(int i = 0; i < cnt; ++i) { + cmplx d = cdir(s * (a + i*M_TAU/cnt)); + cmplx pos = slave->pos + 32 * cdir(s * (a + i*M_TAU/cnt)); + cmplx aim = 2.5 * -d; + + Projectile *p; + + p = PROJECTILE( + .proto = pp_crystal, + .color = RGBA(0.5, 0, 1, 1), + .pos = pos, + .move = move_linear(aim * r), + .layer = LAYER_BULLET | 0x10, + ); + + p = PROJECTILE( + .proto = pp_crystal, + .color = RGBA(0.5, 0, 1, 1), + .pos = pos, + .move = move_linear(aim / r), + .layer = LAYER_BULLET | 0x10, + ); + + //YIELD; + WAIT(1); + } + + a += M_PI/32; + } + + STALL; +} + +DEFINE_EXTERN_TASK(stagex_boss_nonspell_6) { + STAGE_BOOKMARK(non6); + +#if 0 + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + boss->move = move_towards(VIEWPORT_W/2 + 180*I, 0.015); + BEGIN_BOSS_ATTACK(&ARGS); + + cmplx c = (VIEWPORT_W + VIEWPORT_H*I) * 0.5; + real dist = 180; + int cnt = 6; + + for(int i = 0; i < cnt; ++ i) { + INVOKE_SUBTASK(yumemi_non6_slave, + .pos = c + dist * cdir(i*M_TAU/cnt + M_PI/2), + .type = i & 1, + .center = c + ); + WAIT(25); + } + + STALL; +#endif + + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + CoEvent *sync_event = &ARGS.attack->events.started; + + cmplx p = VIEWPORT_W/2 + 100*I; + real xofs = 140; + + INVOKE_SUBTASK_DELAYED(20, yumemi_non3_slave, + .pos = p - xofs, + .type = 0, + .sync_event = sync_event + ); + + INVOKE_SUBTASK_DELAYED(45, yumemi_non3_slave, + .pos = p + xofs, + .type = 1, + .sync_event = sync_event + ); + + INVOKE_SUBTASK_DELAYED(90, yumemi_non6_slave, + .pos = p - xofs / 2 + 32 * I, + .type = 1 + ); + + INVOKE_SUBTASK_DELAYED(115, yumemi_non6_slave, + .pos = p + xofs / 2 + 32 * I, + .type = 0 + ); + + boss->move = move_towards(boss->move.velocity, VIEWPORT_W/2 + 180*I, 0.015); + + BEGIN_BOSS_ATTACK(&ARGS); + AWAIT_SUBTASKS; +} diff --git a/src/stages/stagex/nonspells/nonspells.h b/src/stages/stagex/nonspells/nonspells.h new file mode 100644 index 0000000000..6bc43b38ad --- /dev/null +++ b/src/stages/stagex/nonspells/nonspells.h @@ -0,0 +1,22 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#pragma once +#include "taisei.h" + +#include "stages/common_imports.h" // IWYU pragma: export +#include "../yumemi.h" // IWYU pragma: export + +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_midboss_nonspell_1, BossAttack); + +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_boss_nonspell_1, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_boss_nonspell_2, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_boss_nonspell_3, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_boss_nonspell_4, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_boss_nonspell_5, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_boss_nonspell_6, BossAttack); diff --git a/src/stages/stagex/spells/alignment.c b/src/stages/stagex/spells/alignment.c new file mode 100644 index 0000000000..742f315405 --- /dev/null +++ b/src/stages/stagex/spells/alignment.c @@ -0,0 +1,156 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "spells.h" + +#define GRID_W 32 +#define GRID_H (int)(((real)VIEWPORT_H/VIEWPORT_W) * (GRID_W + 0.5)) + +static Laser *make_grid_laser(cmplx a, cmplx b) { + auto l = create_laserline_ab(a, b, 10, 140, 200, RGBA(1, 0.0, 0, 0)); + play_sfx("boon"); + INVOKE_TASK_DELAYED(120, common_play_sfx, "laser1"); + l->width_exponent = 0; + return l; +} + +TASK(grid_lasers_axis, { BoxedLaserArray *lasers; int count; cmplx a; cmplx b; }) { + for(int x = 0; x < ARGS.count; ++x) { + cmplx a = ARGS.a * (x + 0.5) / GRID_W; + cmplx b = a + ARGS.b; + ENT_ARRAY_ADD(ARGS.lasers, make_grid_laser(a, b)); + // WAIT(1); + } +} + +TASK(grid_lasers) { + DECLARE_ENT_ARRAY(Laser, lasers, GRID_W + GRID_H); + INVOKE_SUBTASK(grid_lasers_axis, &lasers, GRID_W, VIEWPORT_W, I*VIEWPORT_H); + INVOKE_SUBTASK(grid_lasers_axis, &lasers, GRID_H, VIEWPORT_H*I, VIEWPORT_W); + + int alive = 0; + + for(;;YIELD) { + ENT_ARRAY_FOREACH(&lasers, Laser *l, { + ++alive; + + l->color = *RGBA(0.7, 0, 0, 0); + color_lerp(&l->color, RGBA(1, 0.7, 0, 0), (l->width - 3) / 7); + + if(l->collision_active) { + auto rd = NOT_NULL(laser_get_ruledata_linear(l)); + cmplx q = rd->velocity * l->timespan; + real w = 0.5*VIEWPORT_W/GRID_W; + cmplx expand = -conj(I * w * cnormalize(q)); + + Rect hurtbox = { + .top_left = l->pos - expand, + .bottom_right = l->pos + q + expand, + }; + + for(Projectile *p = global.projs.first, *next; p; p = next) { + next = p->next; + + if(p->type != PROJ_ENEMY || !point_in_rect(p->pos, hurtbox)) { + continue; + } + + cmplx v = 0; + real g; + + if(re(expand)) { // vertical + g = 2 * ((re(p->pos) - hurtbox.left) / (hurtbox.right - hurtbox.left) - 0.5); + } else { // horizontal + g = 2 * ((im(p->pos) - hurtbox.top) / (hurtbox.bottom - hurtbox.top) - 0.5); + } + + g = copysign(1 - fabs(g), g); + + if(g) { + p->pos += g * expand; + + cmplx d = global.plr.pos - p->pos; + real xdist = fabs(re(d)); + real ydist = fabs(im(d)); + if(xdist < ydist) { + v = im(d)*I; + } else { + v = re(d); + } + + p->move.velocity = cabs(p->move.velocity) * cnormalize(v); + p->angle = carg(p->move.velocity); + p->flags |= PFLAG_MANUALANGLE; + } + } + } + }); + + if(alive == 0) { + return; + } + } + + AWAIT_SUBTASKS; +} + +TASK(spam, { BoxedBoss boss; }) { + auto boss = TASK_BIND(ARGS.boss); + + cmplx dir = -I; + cmplx r = cdir(M_TAU/43); + + real p = 0; + + for(;;) { + PROJECTILE( + .pos = boss->pos + dir * 64 * sin(p), + .proto = pp_wave, + .color = RGB(0.2, 0.4, 1), + .move = move_linear(dir * 1), + ); + PROJECTILE( + .pos = boss->pos + dir * -64 * sin(p), + .proto = pp_wave, + .color = RGB(0.2, 0.4, 1), + .move = move_linear(dir * -1), + ); + + dir *= r; + p += 0.14315; + + WAIT(4); + play_sfx_loop("shot1_loop"); + } + + for(;;) { + RADIAL_LOOP(l, 128, rng_dir()) { + PROJECTILE( + .pos = boss->pos, + .proto = pp_plainball, + .color = RGB(0.2, 0.4, 1), + .move = move_accelerated(0, l.dir * 0.05), + ); + } + + WAIT(60); + } +} + +DEFINE_EXTERN_TASK(stagex_spell_alignment) { + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + boss->move = move_towards(boss->move.velocity, BOSS_DEFAULT_GO_POS, 0.02); + BEGIN_BOSS_ATTACK(&ARGS); + + INVOKE_SUBTASK_DELAYED(60, spam, ENT_BOX(boss)); + + for(;;) { + INVOKE_SUBTASK(grid_lasers); + WAIT(360); + } +} diff --git a/src/stages/stagex/spells/fork_bomb.c b/src/stages/stagex/spells/fork_bomb.c new file mode 100644 index 0000000000..5c470a8872 --- /dev/null +++ b/src/stages/stagex/spells/fork_bomb.c @@ -0,0 +1,155 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "spells.h" + +static void draw_scuttle_proj(Projectile *p, int t, ProjDrawRuleArgs args) { + Animation *ani = res_anim("boss/scuttle"); + AniSequence *seq = get_ani_sequence(ani, "main"); + r_draw_sprite(&(SpriteParams){ + .shader_ptr = res_shader("sprite_default"), + .pos.as_cmplx = p->pos, + .scale.as_cmplx = p->scale, + .sprite_ptr = animation_get_frame(ani, seq, global.frames), + .color = &p->color, + .rotation.angle = p->angle+M_PI/2, + }); +} + +attr_unused // TODO: remove me +TASK(scuttle_proj_death, { cmplx pos; real angle; }) { + int count = 10; + for(int i = 0; i < count; i++) { + real phi = M_TAU/count*i; + cmplx vel = (2+cos(2*(phi-ARGS.angle)))*cdir(phi); + PROJECTILE( + .pos = ARGS.pos, + .proto = pp_bullet, + .color = RGBA(1,0.2,0.5,1), + .move = move_linear(vel), + ); + } +} + +#define FORK_GRID_SIZE 7 + +TASK(fork_proj, { cmplx pos; cmplx vel; int split_time; int *fork_grid; cmplx direction; int gx; int gy; }) { + Projectile *p = TASK_BIND(PROJECTILE( + .pos = ARGS.pos, + .proto = pp_bigball, + .color = RGBA(0.5,0.2,1,1), + .move = move_linear(ARGS.vel), + )); + + WAIT(ARGS.split_time); + p->move = move_dampen(p->move.velocity, 0.01); + /*int count = 6; + for(int i = 0; i < count; i++) { + cmplx vel = 1 * cdir(M_TAU / count * i); + PROJECTILE( + .pos = p->pos, + .proto = pp_bullet, + .color = RGBA(0.5,0.2,1,1), + .move = move_linear(vel), + ); + }*/ + + + //WAIT(ARGS.split_time); + + if(!ARGS.fork_grid[ARGS.gy*FORK_GRID_SIZE+ARGS.gx]) { + ARGS.fork_grid[ARGS.gy*FORK_GRID_SIZE+ARGS.gx] = 1; + + + cmplx nvel = I*ARGS.vel; + cmplx ndirection = I*ARGS.direction; + + int ngx1 = ARGS.gx + re(ndirection); + int ngy1 = ARGS.gy + im(ndirection); + play_sfx("shot1"); + if(ngx1 >= 0 && ngx1 < FORK_GRID_SIZE && ngy1 >= 0 && ngy1 < FORK_GRID_SIZE && !ARGS.fork_grid[ngy1*FORK_GRID_SIZE+ngx1]) { + INVOKE_TASK(fork_proj, {p->pos, nvel, ARGS.split_time, + .fork_grid = ARGS.fork_grid, + .direction = ndirection, + .gx = ngx1, + .gy = ngy1 + }); + } + int ngx2 = ARGS.gx - re(ndirection); + int ngy2 = ARGS.gy - im(ndirection); + if(ngx2 >= 0 && ngx2 < FORK_GRID_SIZE && ngy2 >= 0 && ngy2 < FORK_GRID_SIZE && !ARGS.fork_grid[ngy2*FORK_GRID_SIZE+ngx2]) { + INVOKE_TASK(fork_proj, {p->pos, -nvel, ARGS.split_time, + .fork_grid = ARGS.fork_grid, + .direction = -ndirection, + .gx = ngx2, + .gy = ngy2 + }); + } + + } + WAIT(4*ARGS.split_time); + + play_sfx("shot2"); + cmplx aim = cnormalize(global.plr.pos-p->pos); + Rect shoot_rect = { 0, CMPLX(VIEWPORT_W, VIEWPORT_H) }; + + if(point_in_rect(p->pos, shoot_rect)) { + PROJECTILE( + .pos = p->pos, + .draw_rule = {draw_scuttle_proj}, + .size = CMPLX(75, 100), + .collision_size = 0.5*CMPLX(32, 50), + .color = RGBA(1, 1, 1, 0.5), + .angle = carg(ARGS.vel), + .scale = 0.5, + .move = move_accelerated(aim,0) + ); + } + kill_projectile(p); +} + + +DEFINE_EXTERN_TASK(stagex_spell_fork_bomb) { + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + boss->move = move_towards(boss->move.velocity, CMPLX(VIEWPORT_W/2, VIEWPORT_H/2), 0.02); + BEGIN_BOSS_ATTACK(&ARGS); + + int split_time = 30; + + int fork_grid[FORK_GRID_SIZE*FORK_GRID_SIZE]; + + real spacing = 100; + + + for(int i = 0;; i++) { + memset(fork_grid, 0, sizeof(fork_grid)); + + int gx = FORK_GRID_SIZE/2; + int gy = FORK_GRID_SIZE/2; + + fork_grid[gy*FORK_GRID_SIZE+gx] = 1; + + cmplx vel = spacing/split_time*cdir(M_PI/4*i); + play_sfx("shot_special1"); + INVOKE_SUBTASK(fork_proj, {boss->pos, vel, split_time, + .fork_grid = fork_grid, + .direction = 1, + .gx = gx+1, + .gy = gy + }); + INVOKE_SUBTASK(fork_proj, {boss->pos, -vel, split_time, + .fork_grid = fork_grid, + .direction = -1, + .gx = gx-1, + .gy = gy + }); + WAIT(5*split_time); + INVOKE_SUBTASK(common_charge, {boss->pos, RGBA(0.5, 0.6, 2.0, 0.0), 3*split_time, .sound = COMMON_CHARGE_SOUNDS}); + WAIT(3*split_time); + } +} diff --git a/src/stages/stagex/spells/infinity_net.c b/src/stages/stagex/spells/infinity_net.c new file mode 100644 index 0000000000..b1b2103812 --- /dev/null +++ b/src/stages/stagex/spells/infinity_net.c @@ -0,0 +1,438 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "spells.h" + +#include "util/sort_r.h" + +typedef struct LatticeNode LatticeNode; +struct LatticeNode { + cmplx ofs; + LatticeNode *neighbours[6]; + BoxedProjectile dot; + int particle_spawn_time; + uint32_t visited; + uint32_t occupancy; +}; + +typedef struct Infnet { + LatticeNode *nodes; + int *idxmap; + int num_nodes; + int num_nodes_used; + cmplx origin; + real radius; +} Infnet; + +static int lattice_index(int sidelen, int x, int y) { + if(x >= sidelen || y >= sidelen || x < 0 || y < 0) { + return -1; + } + + return y * sidelen + x; +} + +static void init_lattice_nodes(int sidelen, LatticeNode out_nodes[]) { + cmplx origin = CMPLX(0.5, 0.5); + LatticeNode *node = out_nodes; + + for(int i = 0; i < sidelen; ++i) { + real ifactor = i / (sidelen - 1.0); + for(int j = 0; j < sidelen; ++j) { + cmplx ofs = ((j + 0.5 * (i & 1)) / (sidelen - 1.0)) + I * ifactor - origin; + + int nindices[6]; + + if(i & 1) { + nindices[0] = lattice_index(sidelen, j, i - 1); + nindices[1] = lattice_index(sidelen, j + 1, i - 1); + nindices[2] = lattice_index(sidelen, j + 1, i ); + nindices[3] = lattice_index(sidelen, j + 1, i + 1); + nindices[4] = lattice_index(sidelen, j , i + 1); + nindices[5] = lattice_index(sidelen, j - 1, i ); + } else { + nindices[0] = lattice_index(sidelen, j - 1, i - 1); + nindices[1] = lattice_index(sidelen, j , i - 1); + nindices[2] = lattice_index(sidelen, j + 1, i ); + nindices[3] = lattice_index(sidelen, j, i + 1); + nindices[4] = lattice_index(sidelen, j - 1, i + 1); + nindices[5] = lattice_index(sidelen, j - 1, i ); + } + + *node = (LatticeNode) { + .ofs = ofs * 2, + }; + + for(int n = 0; n < 6; ++n) { + if(nindices[n] >= 0) { + node->neighbours[n] = &out_nodes[nindices[n]]; + } + } + + ++node; + } + } +} + +static int idxmap_cmp(const void *p1, const void *p2, void *ctx) { + LatticeNode *nodes = ctx; + const int *i1 = p1; + const int *i2 = p2; + + real d1 = cabs2(nodes[*i1].ofs); + real d2 = cabs2(nodes[*i2].ofs); + + int c = (d1 > d2) - (d2 > d1); + + if(c == 0) { + // stabilize sort + return p1 < p2 ? -1 : 1; + } + + return c; +} + +static void init_idxmap(int num_nodes, int idxmap[num_nodes], LatticeNode nodes[num_nodes]) { + for(int i = 0; i < num_nodes; ++i) { + idxmap[i] = i; + } + + sort_r(idxmap, num_nodes, sizeof(*idxmap), idxmap_cmp, nodes); +} + +#define MAX_RANK 0xffffffff + +static uint rank_step(uint id, uint base, int dir, LatticeNode *prev) { + LatticeNode *n = prev->neighbours[dir]; + + if(!n || !ENT_UNBOX(n->dot)) { + return base == 0xff ? 0 : MAX_RANK; + } + + uint rank = 0; + rank <<= 1; rank |= (n->visited == id); + rank <<= 8; rank |= n->occupancy; + + if(base != 0xff) { + rank <<= 10; + rank += rank_step(id, 0xff, dir, n); + rank += rank_step(id, 0xff, (6 + dir - 1) % 6, n); + rank += rank_step(id, 0xff, (6 + dir + 1) % 6, n); + rank += rank_step(id, 0xff, (6 + dir - 2) % 6, n); + rank += rank_step(id, 0xff, (6 + dir + 2) % 6, n); + rank <<= 4; rank |= base; + } + + return rank; +} + +static int build_path(Infnet *infnet, int max_steps, int path[max_steps], int start, uint32_t id) { + path[0] = infnet->idxmap[start]; + LatticeNode *prev_node = &infnet->nodes[path[0]]; + int prev_direction = rng_i32_range(0, 6); + bool prefer_ccw = rng_bool(); + + for(int step = 1; step < max_steps; ++step) { + prev_node->visited = id; + prev_node->occupancy++; + + int dir_priority[] = { + prev_direction, // continue forward + (6 + prev_direction + 1) % 6, // 1 turn clockwise + (6 + prev_direction - 1) % 6, // 1 turn counterclockwise + (6 + prev_direction + 2) % 6, // 2 turns clockwise + (6 + prev_direction - 2) % 6, // 2 turns counterclockwise + (6 + prev_direction + 3) % 6, // turn back + }; + + // Randomize left/right turns + if(rng_chance(0.12)) prefer_ccw = !prefer_ccw; + + if(prefer_ccw) SWAP(dir_priority[1], dir_priority[2]); + if(prefer_ccw) SWAP(dir_priority[3], dir_priority[4]); + + if(rng_chance(0.75)) { + // Push forward movement down the priority list + SWAP(dir_priority[0], dir_priority[1]); + SWAP(dir_priority[1], dir_priority[2]); + } + + int step_dir = -1; + uint step_rank = MAX_RANK; + + // Consider turning straight back on first step only, + // because the "forward" direction was actually picked randomly there. + for(int i = 0; i < ARRAY_SIZE(dir_priority) - (step > 1); ++i) { + int dir = dir_priority[i]; + uint rank = rank_step(id, i, dir, prev_node); + + if(rank < step_rank) { + step_dir = dir; + step_rank = rank; + } + } + + if(step_dir < 0) { + return step; + } + + LatticeNode *next_node = NOT_NULL(prev_node->neighbours[step_dir]); + assert(ENT_UNBOX(next_node->dot)); + path[step] = next_node - infnet->nodes; + prev_node = next_node; + prev_direction = step_dir; + } + + return max_steps; +} + +static void emit_particle(Infnet *infnet, int index, Projectile *dot, Sprite *spr) { + if(dot->ent.draw_layer == LAYER_NODRAW) { + return; + } + + if(global.frames - infnet->nodes[index].particle_spawn_time < 6) { + return; + } + + PARTICLE( + .sprite_ptr = spr, + .pos = dot->pos, + .color = color_mul_scalar(COLOR_COPY(&dot->color), 0.75), + .timeout = 10, + .draw_rule = pdraw_timeout_scalefade(0.5, 1, 1, 0), + .angle = rng_angle(), + .flags = PFLAG_MANUALANGLE, + ); + + infnet->nodes[index].particle_spawn_time = global.frames; +} + +TASK(infnet_lasers, { Infnet *infnet; int start_idx; }) { + Infnet *infnet = ARGS.infnet; + int max_steps = infnet->num_nodes_used; + int path[max_steps]; + int path_size = build_path( + infnet, ARRAY_SIZE(path), path, ARGS.start_idx, THIS_TASK.unique_id); + + BoxedLaser lasers[path_size - 1]; + memset(&lasers, 0, sizeof(lasers)); + + int lasers_spawned = 0; + int lasers_alive = 0; + + Sprite *stardust_spr = res_sprite("part/stardust"); + + int t = 0; + + const int laser_charge_time = 120; + const int laser_duration = 240; + + INVOKE_SUBTASK_DELAYED(laser_charge_time, common_play_sfx, "boon"); + + while(!lasers_spawned || lasers_alive) { + lasers_alive = 0; + + for(int i = 0; i < path_size - 1; ++i) { + int index0 = path[i]; + int index1 = path[i + 1]; + + LatticeNode *na = infnet->nodes + index0; + LatticeNode *nb = infnet->nodes + index1; + + Projectile *a = ENT_UNBOX(na->dot); + Projectile *b = ENT_UNBOX(nb->dot); + + if(a && b) { + // Bump occupancy if not yet spawned or still present, but not if already dead + if(!lasers[i].ent || ENT_UNBOX(lasers[i])) { + na->occupancy++; + nb->occupancy++; + } + + if(!lasers[i].ent && t >= 10*i) { + Laser *l = create_laserline_ab( + 0, 0, 10, laser_charge_time, laser_duration, RGBA(0,0,0,0)); + lasers[i] = ENT_BOX(l); + ++lasers_spawned; + } + + Laser *l = ENT_UNBOX(lasers[i]); + if(l) { + cmplx pa = infnet->radius * na->ofs + infnet->origin; + cmplx pb = infnet->radius * nb->ofs + infnet->origin; + laserline_set_ab(l, pa, pb); + + if(l->collision_active) { + emit_particle(infnet, index0, a, stardust_spr); + emit_particle(infnet, index1, b, stardust_spr); + } + + l->color = *RGBA(1.0, 0.1, 0.4, 0); + color_lerp(&l->color, RGBA(1, 0.4, 0.1, 0), (l->width - 3)/7); + } + } else { + Laser *l = lasers[i].ent ? ENT_UNBOX(lasers[i]) : NULL; + if(l) { + clear_laser(l, CLEAR_HAZARDS_FORCE); + } + } + + if(ENT_UNBOX(lasers[i])) { + ++lasers_alive; + } + } + + ++t; + YIELD; + } +} + +TASK(animate_infnet, { Infnet *infnet; }) { + Infnet *infnet = ARGS.infnet; + + real intro_time = 300; + real mid_time = 1500; + real final_time = 2000; + + real rbase = hypot(VIEWPORT_W, VIEWPORT_H); + real radius0 = rbase * 8; + real radius1 = rbase * 4; + real radius2 = rbase; + + infnet->radius = radius0; + + real min_player_dist = 120; + cmplx plrvec = infnet->origin - global.plr.pos; + + if(cabs(plrvec) < min_player_dist) { + infnet->origin = global.plr.pos + min_player_dist * cnormalize(plrvec); + } + + real t = 0; + int alive = 0; + + cmplx orig_ofs[infnet->num_nodes_used]; + for(int i = 0; i < ARRAY_SIZE(orig_ofs); ++i) { + LatticeNode *n = infnet->nodes + infnet->idxmap[i]; + orig_ofs[i] = n->ofs; + } + + do { + YIELD; + + real final_factor = glm_ease_sine_out(min(1, t / final_time)); + real intro_factor = glm_ease_back_out(min(1, t / intro_time)); + + infnet->radius = lerp(radius1, radius2, final_factor); + infnet->radius = lerp(radius0, infnet->radius, intro_factor); + + alive = 0; + + for(int j = 0; j < infnet->num_nodes_used; ++j) { + int i = infnet->idxmap[j]; + LatticeNode *node = &infnet->nodes[i]; + Projectile *dot = ENT_UNBOX(node->dot); + + if(UNLIKELY(!dot)) { + continue; + } + + ++alive; + node->occupancy = 0; + + real rot_phase = t / 200.0; + dot->pos = infnet->radius * node->ofs + infnet->origin; + dot->prevpos = dot->pos0 = dot->pos; + + real d = glm_ease_sine_inout(min(1, t / mid_time)); + real dphase = (global.frames - dot->birthtime - t) * 0.0025 * d; + + dot->color = *color_lerp( + RGBA(1, 0, 0, 0), RGBA(0, 1, 0, 0), pcos(rot_phase - 2*dphase)); + + cmplx pivot = intro_factor * (0.03 - dphase) * cdir(rot_phase + 0.05); + node->ofs = orig_ofs[j]; + node->ofs -= pivot; + node->ofs *= cdir(M_PI * sin(rot_phase)); + node->ofs += pivot; + + if(projectile_in_viewport(dot)) { + dot->flags &= ~PFLAG_NOCOLLISION; + dot->ent.draw_layer = LAYER_BULLET; + } else { + dot->flags |= PFLAG_NOCOLLISION; + dot->ent.draw_layer = LAYER_NODRAW; + } + }; + + capproach_asymptotic_p(&infnet->origin, global.plr.pos, 0.001, 1e-9); + t += 1; + } while(alive > 0); +} + +DEFINE_EXTERN_TASK(stagex_spell_infinity_network) { + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + BEGIN_BOSS_ATTACK(&ARGS); + boss->in_background = true; + + int spawntime = 300; + int sidelen = 37; + + int num_nodes = sidelen * sidelen; + LatticeNode nodes[sidelen * sidelen]; + init_lattice_nodes(sidelen, nodes); + + int idxmap[num_nodes]; + init_idxmap(num_nodes, idxmap, nodes); + + int num_nodes_used = 0; + + for(int j = 0; j < num_nodes; ++j) { + int i = idxmap[j]; + if(cabs2(nodes[i].ofs) >= 1) { + break; + } + num_nodes_used++; + } + + Infnet infnet = { + .nodes = nodes, + .num_nodes = num_nodes, + .num_nodes_used = num_nodes_used, + .origin = (VIEWPORT_W+VIEWPORT_H*I) * 0.5, + .idxmap = idxmap, + }; + + INVOKE_SUBTASK(animate_infnet, &infnet); + + for(int j = 0; j < num_nodes_used; ++j) { + int i = idxmap[j]; + + nodes[i].dot = ENT_BOX(PROJECTILE( + .proto = pp_ball, + .color = color_lerp(RGBA(1, 0, 0, 0), RGBA(0, 0, 1, 0), j / (num_nodes_used - 1.0)), + .pos = infnet.origin + infnet.radius * nodes[i].ofs, + .flags = PFLAG_INDESTRUCTIBLE | PFLAG_NOCLEAR | PFLAG_NOAUTOREMOVE, + )); + + // FIXME: really should be num_nodes_used, but spawntime is tuned for this value already… + if(j % (1 + num_nodes / spawntime) == 0) YIELD; + } + + INVOKE_SUBTASK(infnet_lasers, &infnet, 0); + + for(;;) { + WAIT(60); + real f = rng_real(); + f *= rng_real(); + f *= rng_real(); + int start = infnet.num_nodes_used * f; + INVOKE_SUBTASK(infnet_lasers, &infnet, start); + } +} diff --git a/src/stages/stagex/spells/mem_copy.c b/src/stages/stagex/spells/mem_copy.c new file mode 100644 index 0000000000..4f28592cad --- /dev/null +++ b/src/stages/stagex/spells/mem_copy.c @@ -0,0 +1,400 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "spells.h" + +// TODO naming not ideal… CELLS vs CELL +#define CELLS_W 3 +#define CELLS_H 3 +#define CELL_W 6 +#define CELL_H 8 +#define CLEAR_TIME 240 +#define CELL_SIZE CMPLX(VIEWPORT_W/CELLS_W, VIEWPORT_H/CELLS_H) + +TASK(cell_proj_aim, { BoxedProjectile pbox; }) { + play_sfx("redirect"); + Projectile *p = TASK_BIND(ARGS.pbox); + + p->flags &= ~(PFLAG_NOCLEAR | PFLAG_NOAUTOREMOVE); + cmplx aim = cnormalize(global.plr.pos-p->pos); + p->move = move_accelerated(0,0.01*aim); + projectile_set_prototype(p, pp_ball); + spawn_projectile_highlight_effect(p); + p->color = *RGBA(0.2, 0, 1, 1); +} + +typedef enum { + CD_NONE, + CD_UP, + CD_RIGHT, + CD_DOWN, + CD_LEFT, + // CD_PROJEKT_RED, + CD_FIRST = CD_UP, + CD_LAST = CD_LEFT, +} CellDirection; + + +static void cell_split_idx(int cell, int *x, int *y) { + *x = cell % CELLS_W; + *y = cell / CELLS_W; +} + +static int find_player_cell(void) { + int x = clamp(0, 0.99, re(global.plr.pos) / VIEWPORT_W) * CELLS_W; + int y = clamp(0, 0.99, im(global.plr.pos) / VIEWPORT_H) * CELLS_H; + int idx = y * CELLS_W + x; + assert(idx >= 0 && idx < CELLS_W * CELLS_H); + return idx; +} + +static void cell_apply_dir_xy(int x, int y, CellDirection dir, int *nx, int *ny) { + *nx = x; + *ny = y; + switch(dir) { + case CD_UP: + (*ny)--; + break; + case CD_DOWN: + (*ny)++; + break; + case CD_RIGHT: + (*nx)++; + break; + case CD_LEFT: + (*nx)--; + break; + default: + break; + } +} + +static int cell_apply_dir(int cell, CellDirection dir) { + int x, y; + cell_split_idx(cell, &x, &y); + + int nx =-1, ny=-1; + cell_apply_dir_xy(x, y, dir, &nx, &ny); + + assert(nx >= 0 && nx < CELLS_W); + assert(ny >= 0 && ny < CELLS_H); + + return ny*CELLS_W + nx; +} + +static CellDirection cell_find_expansion_dir(int cell) { + int x, y; + cell_split_idx(cell, &x, &y); + + int dir; + int nx = -1, ny = -1; + do { + dir = rng_irange(CD_FIRST, CD_LAST + 1); + cell_apply_dir_xy(x, y, dir, &nx, &ny); + } while(nx < 0 || ny < 0 || nx >= CELLS_W || ny >= CELLS_H); + + return dir; +} + +static real cell_dir_transition(int cell, CellDirection dir) { + int x = cell % CELL_W; + int y = cell / CELL_W; + switch(dir) { + case CD_UP: + return (CELL_H-y)/(real)CELL_H; + case CD_DOWN: + return y/(real)CELL_H; + case CD_LEFT: + return (CELL_W-x)/(real)CELL_W; + case CD_RIGHT: + return x/(real)CELL_W; + default: + break; + } + + return 0; +} + +static cmplx cell_topleft(int cell) { + int cx, cy; + cell_split_idx(cell, &cx, &cy); + + return CMPLX(cx*re(CELL_SIZE), cy*im(CELL_SIZE)); +} + +TASK(cell_proj_colors, { BoxedProjectile *projs; int cell_idx; int *highlighted_cell; }) { + static Color normal_color = { 1.0, 0.2, 0.5, 0 }; + static Color highlight_color = { 0.2, 1.0, 0.5, 0 }; + static Color inactive_color = { 0.2, 0.2, 0.3, 0.2 }; + + for(;;YIELD) { + Color *target_color; + + if(*ARGS.highlighted_cell == -1) { + target_color = &normal_color; + } else if(*ARGS.highlighted_cell == ARGS.cell_idx) { + target_color = &highlight_color; + } else { + target_color = &inactive_color; + } + + for(int i = 0; i < CELL_H*CELL_W; ++i) { + Projectile *p = ENT_UNBOX(ARGS.projs[i]); + + if(!p || p->proto != pp_bigball) { + continue; + } + + color_lerp(&p->color, target_color, 0.05); + } + } +} + +TASK(oscillate, { BoxedProjectile p; }) { + auto p = TASK_BIND(ARGS.p); + + cmplx target = p->pos; + cmplx ofs = rng_dir(); + ofs *= rng_range(6, 10) * 2; + p->pos = p->pos0 = p->pos + ofs; + + p->move = move_from_towards(p->pos, target, 0.1); + p->move.retention = 0.9 + 0.2i; + + WAIT(90); + + p->move = move_dampen(0, 0); + p->pos = target; +} + +TASK(spawn_cell, { int idx; int missing; CoEvent *destroy; CellDirection *clear_dir; int *highlighted_cell; }) { + play_sfx("shot2"); + + BoxedProjectile projs[CELL_W*CELL_H] = {}; + cmplx topleft = cell_topleft(ARGS.idx); + + assert(ARGS.missing >= 0); + + for(int y = 0; y < CELL_H; y++) { + for(int x = 0; x < CELL_W; x++) { + if(y*CELL_W+x == ARGS.missing) { + continue; + } + cmplx offset = CMPLX( + re(CELL_SIZE)*(x+0.5)/CELL_W, + im(CELL_SIZE)*(y+0.5)/CELL_H); + + auto p = ENT_BOX(PROJECTILE( + .pos = topleft + offset, + .proto = pp_bigball, + .flags = PFLAG_NOCLEAR | PFLAG_NOAUTOREMOVE, + .color = RGBA(1, rng_f32(), 0.0, 0), + )); + projs[y*CELL_W+x] = p; + + // Shake projectiles randomly for a bit to stop players from + // ignoring the core mechanic by safe-spotting between the gaps. + // Don't do it near the hole though, to prevent randomly killing + // people that are playing correctly. + if( + (y+1) * CELL_W + (x) != ARGS.missing && + (y-1) * CELL_W + (x) != ARGS.missing && + (y) * CELL_W + (x+1) != ARGS.missing && + (y) * CELL_W + (x-1) != ARGS.missing + ) { + INVOKE_TASK(oscillate, p); + } + } + } + + INVOKE_SUBTASK(cell_proj_colors, projs, ARGS.idx, ARGS.highlighted_cell); + + for(;;) { + WAIT_EVENT_OR_DIE(ARGS.destroy); + if(*ARGS.clear_dir == CD_NONE) { // just shrink projectiles + play_sfx("warp"); + for(int i = 0; i < CELL_W*CELL_H; i++) { + Projectile *p = ENT_UNBOX(projs[i]); + if(p != NULL) { + projectile_set_prototype(p, pp_flea); + spawn_projectile_highlight_effect(p); + p->color = *RGBA(1, 0.5, 0, 1); + } + } + play_sfx("warp"); + WAIT(CLEAR_TIME+40); + for(int i = 0; i < CELL_W*CELL_H; i++) { + Projectile *p = ENT_UNBOX(projs[i]); + if(p != NULL) { + projectile_set_prototype(p, pp_bigball); + spawn_projectile_highlight_effect(p); + } + } + } else { + CellDirection dir = *ARGS.clear_dir; + + for(int i = 0; i < CELL_W*CELL_H; i++) { + real delay = 0.5 * CLEAR_TIME * cell_dir_transition(i, dir); + Projectile *p = ENT_UNBOX(projs[i]); + if(p != NULL) { + INVOKE_TASK_DELAYED(delay, cell_proj_aim, ENT_BOX(p)); + } + } + break; + } + } +} + +TASK(clearing_laser_rect, { int start_cell; int end_cell; int start_delay; int move_duration; int end_delay; bool collide; const Color *color; }) { + cmplx topleft0 = cell_topleft(ARGS.start_cell); + cmplx topleft1 = cell_topleft(ARGS.end_cell); + + cmplx diff = topleft1-topleft0; + + cmplx positions[] = { + topleft0, + topleft0 + re(CELL_SIZE), + topleft0 + CELL_SIZE, + topleft0 + I * im(CELL_SIZE), + topleft0, + }; + + real dur = ARGS.start_delay + ARGS.move_duration + ARGS.end_delay; + + real lwidth = 20; + + DECLARE_ENT_ARRAY(Laser, lasers, 4); + for(int i = 0; i < 4; i++) { + Laser *l = create_laserline_ab(positions[i], positions[i+1], lwidth, ARGS.collide ? ARGS.start_delay : (ARGS.start_delay+ARGS.move_duration), dur, ARGS.color); + l->unclearable = true; + l->width_exponent = 0; + + ENT_ARRAY_ADD(&lasers, l); + } + WAIT(ARGS.start_delay); + + for(int i = 0; i < ARGS.move_duration; i++) { + real f = (i+1)/(real)ARGS.move_duration; + ENT_ARRAY_FOREACH_COUNTER(&lasers, int j, Laser *l, { + l->pos = positions[j] + diff*glm_ease_quad_in(f); + }); + YIELD; + } +} + +DEFINE_EXTERN_TASK(stagex_spell_mem_copy) { + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + BEGIN_BOSS_ATTACK(&ARGS); + + COEVENTS_ARRAY(destroy[CELLS_W*CELLS_H]) cell_events; + TASK_HOST_EVENTS(cell_events); + + int highlighted_cell = -1; + int cell_gaps[CELLS_W*CELLS_H]; + + for(int i = 0; i < ARRAY_SIZE(cell_gaps); i++) { + cell_gaps[i] = -1; + } + + WAIT(60); + + CellDirection clear_dir = CD_NONE; + int plr_cell = find_player_cell(); + for(int x = 0; x < CELLS_W; x++) { + for(int y = 0; y < CELLS_H; y++) { + int idx = y*CELLS_W+x; + if(idx == plr_cell) { + continue; + } + cell_gaps[idx] = rng_irange(0, CELL_W * CELL_H); + INVOKE_SUBTASK(spawn_cell, + idx, cell_gaps[idx], &cell_events.destroy[idx], &clear_dir, &highlighted_cell); + WAIT(10); + } + } + + for(int step = 0;; step++) { + WAIT(60); + + plr_cell = find_player_cell(); + clear_dir = 0; + coevent_signal(&cell_events.destroy[plr_cell]); + + clear_dir = cell_find_expansion_dir(plr_cell); + int start_delay = 40; + int end_delay = 240; + play_sfx("laser1"); + INVOKE_SUBTASK(clearing_laser_rect, + .start_cell = plr_cell, + .end_cell = cell_apply_dir(plr_cell, clear_dir), + .start_delay = start_delay, + .move_duration = CLEAR_TIME, + .end_delay = end_delay, + .collide = true, + .color = RGBA(1,0,0,0.5) + ); + WAIT(start_delay); + + + int dest_cell = cell_apply_dir(plr_cell, clear_dir); + coevent_signal(&cell_events.destroy[dest_cell]); + cell_gaps[dest_cell] = -1; + + boss->move = move_towards(boss->move.velocity, + CMPLX(re(cell_topleft(dest_cell)+CELL_SIZE*0.5), 100), 0.01); + WAIT(CLEAR_TIME); + + int src_cell; + do { + src_cell = rng_irange(0, CELLS_W*CELLS_H); + } while(cell_gaps[src_cell] == -1); + + highlighted_cell = src_cell; + + play_sfx("laser1"); + INVOKE_SUBTASK(clearing_laser_rect, + .start_cell = dest_cell, + .end_cell = src_cell, + .start_delay = 0, + .move_duration = 30, + .end_delay = 240, + .color = RGBA(0,0,1,0.5) + ); + + INVOKE_SUBTASK_DELAYED(180, common_charge, boss->pos, RGBA(0.5, 0.6, 2.0, 0.0), 60, .sound = COMMON_CHARGE_SOUNDS); + + int delay = 240; + int pinginterval = 40; + + while(delay - pinginterval > 0) { + delay -= WAIT(pinginterval); + play_sfx("warp"); + INVOKE_SUBTASK(clearing_laser_rect, + .start_cell = src_cell, + .end_cell = dest_cell, + .start_delay = 0, + .move_duration = 30, + .end_delay = 30, + .color = RGBA(0,0,1,0.5) + ); + } + + WAIT(delay); + + cell_gaps[dest_cell] = cell_gaps[src_cell]; + INVOKE_SUBTASK(spawn_cell, dest_cell, cell_gaps[dest_cell], &cell_events.destroy[dest_cell], &clear_dir, &highlighted_cell); + + if(step == 0) { + cell_gaps[plr_cell] = rng_irange(0, CELL_W * CELL_H); + INVOKE_SUBTASK(spawn_cell, plr_cell, cell_gaps[plr_cell], &cell_events.destroy[plr_cell], &clear_dir, &highlighted_cell); + } + + highlighted_cell = -1; + WAIT(120); + } +} diff --git a/src/stages/stagex/spells/pipe_dream.c b/src/stages/stagex/spells/pipe_dream.c new file mode 100644 index 0000000000..b344f7e950 --- /dev/null +++ b/src/stages/stagex/spells/pipe_dream.c @@ -0,0 +1,76 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "spells.h" + +TASK(animate_radii, { real *radius_inner; real *radius_outer; }) { + *ARGS.radius_inner = 100; + *ARGS.radius_outer = 200; + + // TODO when animate easing from pbr_bg is somehow merged +} + +TASK(pipe_conversions, { BoxedBoss bbox; BoxedProjectileArray *projs; }) { + Boss *boss = TASK_BIND(ARGS.bbox); + real radius_inner = 100; + real radius_outer = 200; + + INVOKE_SUBTASK(animate_radii, &radius_inner, &radius_outer); + + for(;;) { + ENT_ARRAY_FOREACH(ARGS.projs, Projectile *p, { + real r = cabs(boss->pos - p->pos); + if(r < radius_inner) { + p->move.retention = 1; + p->move.attraction = 0; + projectile_set_prototype(p, pp_wave); + p->color = *RGB(1, 1, 0); + } else if(r < radius_outer) { + p->move.retention = cdir(0.01); + p->move.attraction = 0.0; + + p->move.attraction_point = global.plr.pos; + projectile_set_prototype(p, pp_ball); + p->color = *RGB(1, 0, 1); + } else { + p->move.retention = 1.01*cdir(-0.02); + p->move.attraction = 0.0; + + projectile_set_prototype(p, pp_bigball); + p->color = *RGB(0, 0.5, 1); + } + }); + YIELD; + } + +} + +DEFINE_EXTERN_TASK(stagex_spell_pipe_dream) { + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + boss->move = move_towards(boss->move.velocity, CMPLX(VIEWPORT_W/2, VIEWPORT_H/2), 0.02); + BEGIN_BOSS_ATTACK(&ARGS); + + int enough = 1000; + DECLARE_ENT_ARRAY(Projectile, projs, enough); + + INVOKE_SUBTASK(pipe_conversions, ENT_BOX(boss), &projs); + + real golden = (1+sqrt(5))/2; + for(int i = 0; ; i++) { + ENT_ARRAY_ADD(&projs, PROJECTILE( + .proto = pp_wave, + .pos = boss->pos, + .color = RGB(1, 1, 0), + .move = move_linear(3*cdir(M_TAU/golden/golden*i)), + )); + YIELD; + if(i % 100 == 0) { + ENT_ARRAY_COMPACT(&projs); + } + } +} diff --git a/src/stages/stagex/spells/rings.c b/src/stages/stagex/spells/rings.c new file mode 100644 index 0000000000..5c5a031cdd --- /dev/null +++ b/src/stages/stagex/spells/rings.c @@ -0,0 +1,122 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "spells.h" + +TASK(ring, { + cmplx pos; + real spawn_interval; + real expand_time; + real gap_factor; + real direction; +}) { + static const Color color0 = { 1, 0.5, 0, 0 }; + static const Color color1 = { 1, 0, 0.5, 0 }; + + // "time base", i.e. how many laser-time units is a full circle revolution + // This affects the quantization sample count (~2x this number of samples taken for the full circle) + // In other words: bigger number = smoother arcs = slower + const real T = 53; + const real target_radius = hypot(VIEWPORT_W, VIEWPORT_H) * 0.5; + const real width = 15; + + real radius = 0; + real expand_time = ARGS.expand_time; + real spawn_interval = ARGS.spawn_interval; + real wlen = spawn_interval * target_radius / expand_time; + + auto louter = create_laser(ARGS.pos, T, expand_time, RGB(0.5, 0.25, 0), + laser_rule_arc(0, M_TAU/T, 0)); + louter->width = 2; + louter->width_exponent = 0; + louter->collision_active = false; + laser_make_static(louter); + auto b_louter = ENT_BOX(louter); + + int num_segs = rng_i32_range(2, 6); + DECLARE_ENT_ARRAY(Laser, segs, num_segs); + DECLARE_ENT_ARRAY(Laser, walls, num_segs); + + real f = T/(real)num_segs; + real s = ARGS.direction; + real o = T * rng_real() * s; + + for(int i = 0; i < num_segs; ++i) { + auto l = create_laser(ARGS.pos, f * ARGS.gap_factor, expand_time + spawn_interval, &color0, + laser_rule_arc(0, s*M_TAU/T, o+i*f)); + l->width = width; + l->width_exponent = 0; + laser_make_static(l); + ENT_ARRAY_ADD(&segs, l); + + auto lwall = create_laser(0, 4, expand_time + spawn_interval, &color0, + laser_rule_linear(0)); + lwall->width = width; + lwall->width_exponent = 0; + laser_make_static(lwall); + ENT_ARRAY_ADD(&walls, lwall); + } + + for(int t = 0; t < expand_time + spawn_interval; ++t, YIELD) { + radius = approach(radius, target_radius + wlen, target_radius / expand_time); + + if((louter = ENT_UNBOX(b_louter))) { + auto rd = NOT_NULL(laser_get_ruledata_arc(louter)); + rd->radius = radius; + } + + ENT_ARRAY_FOREACH(&segs, Laser *l, { + auto rd = NOT_NULL(laser_get_ruledata_arc(l)); + rd->radius = radius; + rd->time_ofs -= 0.5 * T/radius; + color_lerp(&l->color, &color1, 0.0025); + }); + + ENT_ARRAY_FOREACH_COUNTER(&walls, int i, Laser *lwall, { + auto ref = ENT_ARRAY_GET(&segs, i); + + if(!ref || global.frames - ref->birthtime >= ref->deathtime) { + lwall->deathtime = 0; + continue; + } + + lwall->color = ref->color; + + cmplx a = laser_pos_at(ref, 0); + cmplx v = cnormalize(ARGS.pos - a); + cmplx ofs = v * width * 0.25; + laserline_set_ab(lwall, a + ofs, a - ofs + min(wlen, radius) * v); + }); + } +} + +DEFINE_EXTERN_TASK(stagex_spell_rings) { + auto boss = INIT_BOSS_ATTACK(&ARGS); + boss->move = move_from_towards(boss->pos, (VIEWPORT_W+VIEWPORT_H*I)*0.5, 0.05); + BEGIN_BOSS_ATTACK(&ARGS); + + const int spawn_interval = 120; + const int expand_time = 60 * 12; + const real gap_factor = 0.8; + + real dir = rng_sign(); + + for(;;) { + common_charge(spawn_interval, &boss->pos, 0, RGBA(1, 0.5, 0, 0)); + play_sfx("redirect"); + INVOKE_TASK(ring, + .pos = boss->pos, + .direction = dir, + .spawn_interval = spawn_interval, + .expand_time = expand_time, + .gap_factor = gap_factor, + ); + // NOTE: keeping or randomizing the direction may create unwinnable scenarios + dir = -dir; + } +} diff --git a/src/stages/stagex/spells/sierpinski.c b/src/stages/stagex/spells/sierpinski.c new file mode 100644 index 0000000000..6dbaff774f --- /dev/null +++ b/src/stages/stagex/spells/sierpinski.c @@ -0,0 +1,104 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "spells.h" + +static int idxmod(int i, int n) { // TODO move into utils? + return (n + i) % n; +} + +TASK(rule90, { cmplx origin; int duration; }) { + /* + * Rule 90 automaton with a twist + */ + + enum { N = 65 }; // NOTE: must be odd (or it'll halt quickly) + bool state[N] = { 0 }; + // initialize with 1 bit in the middle to get a nice Sierpinski triangle pattern + state[N/2] = 1; + + static Color color0 = { 0.2, 0.25, 1.0, 1.0 }; + static Color color1 = { 1.2, 0.25, 0.8, 1.0 }; + + int delay = 3; + int ncycles = ARGS.duration / delay; + + for(int cycle = 0; cycle < ncycles; ++cycle, WAIT(3)) { + bool next_state[N] = { 0 }; + + for(int i = 0; i < N; ++i) { + next_state[i] = state[idxmod(i - 1, N)] ^ state[idxmod(i + 1, N)]; + } + + Color clr = color0; + float f = cycle / (ncycles - 1.0f); + f = smoothstep(0, 1, f); + f = smoothstep(0, 1, f); + f = smoothstep(0, 1, f); + f = smoothstep(0, 1, f); + color_lerp(&clr, &color1, f); + + for(int i = 0; i < N; ++i) { + if(!state[i]) { + continue; + } + + play_sfx_loop("shot1_loop");; + + cmplx dir = cdir((i + 0.5) / N * M_TAU + M_PI/2); + cmplx v = dir; + + attr_unused Projectile *cell = PROJECTILE( + .proto = pp_thickrice, + .color = &clr, + .pos = ARGS.origin - 100 * dir, + .move = move_accelerated(v, 0.04 * v), + .max_viewport_dist = VIEWPORT_W/2, + ); + } + + memcpy(state, next_state, sizeof(state)); + } + +} + +TASK(slave, { cmplx origin; int type; }) { + auto slave = stagex_host_yumemi_slave(ARGS.origin, ARGS.type); + + INVOKE_SUBTASK(common_move, + .pos = &slave->pos, + .move_params = move_towards(ARGS.origin - 64i, 0.025), + .ent = ENT_BOX(slave).as_generic + ); + + common_charge(60, &slave->pos, 0, RGBA(0.2, 0.3, 1.0, 0)); + INVOKE_SUBTASK(rule90, slave->pos, 180); + WAIT(60); + common_charge(120, &slave->pos, 0, RGBA(1, 0.3, 0.2, 0)); + stagex_yumemi_slave_laser_sweep(slave, ARGS.type ? 1 : -1, global.plr.pos); +} + +DEFINE_EXTERN_TASK(stagex_spell_sierpinski) { + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + cmplx origin = VIEWPORT_W/2 + 260*I; + boss->move = move_towards(boss->move.velocity, origin, 0.02); + BEGIN_BOSS_ATTACK(&ARGS); + + Rect bounds = viewport_bounds(128); + bounds.top += 64; + bounds.bottom = VIEWPORT_H / 2 + 64; + + int type = 0; + + for(;;) { + boss->move.attraction_point = common_wander(boss->pos, VIEWPORT_W/3, bounds); + INVOKE_SUBTASK(slave, boss->pos, type); + type = !type; + WAIT(200); + } +} diff --git a/src/stages/stagex/spells/spells.h b/src/stages/stagex/spells/spells.h new file mode 100644 index 0000000000..6d1bda5207 --- /dev/null +++ b/src/stages/stagex/spells/spells.h @@ -0,0 +1,22 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#pragma once +#include "taisei.h" + +#include "stages/common_imports.h" // IWYU pragma: export +#include "../yumemi.h" // IWYU pragma: export + +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_spell_trap_representation, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_spell_fork_bomb, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_spell_infinity_network, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_spell_sierpinski, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_spell_mem_copy, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_spell_pipe_dream, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_spell_alignment, BossAttack); +DECLARE_EXTERN_TASK_WITH_INTERFACE(stagex_spell_rings, BossAttack); diff --git a/src/stages/stagex/spells/trap_representation.c b/src/stages/stagex/spells/trap_representation.c new file mode 100644 index 0000000000..18b25522c5 --- /dev/null +++ b/src/stages/stagex/spells/trap_representation.c @@ -0,0 +1,76 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "spells.h" + +static void spawn_circle(cmplx pos, real phase, real radius, int count, real collapse_time) { + for(int i = 0; i < count; i++) { + cmplx offset = radius*cdir(M_TAU/count*(i+phase)); + + PROJECTILE( + .pos = pos + offset, + .proto = pp_bullet, + .color = RGBA(0.5,0.2,1,1), + .move = move_linear(-offset/collapse_time), + .timeout = collapse_time, + .max_viewport_dist = 2*radius, + ); + } +} + +static void draw_wriggle_proj(Projectile *p, int t, ProjDrawRuleArgs args) { + Animation *ani = res_anim("boss/wriggle"); + AniSequence *seq = get_ani_sequence(ani, "fly"); + r_draw_sprite(&(SpriteParams){ + .shader_ptr = res_shader("sprite_default"), + .pos.as_cmplx = p->pos, + .scale.as_cmplx = p->scale, + .sprite_ptr = animation_get_frame(ani, seq, global.frames), + .color = &p->color, + .rotation.angle = p->angle+M_PI/2, + }); +} + +DEFINE_EXTERN_TASK(stagex_spell_trap_representation) { + Boss *boss = INIT_BOSS_ATTACK(&ARGS); + BEGIN_BOSS_ATTACK(&ARGS); + boss->move = move_towards(boss->move.velocity, CMPLX(VIEWPORT_W/2, VIEWPORT_H/2), 0.02); + + real radius = 200; + int count = 20; + int final_count = 15; + WAIT(60); + + for(;;) { + for(int n = 2; n <= 4; n++) { + int c = 1<<(n-1); + if(n == 4) { + INVOKE_SUBTASK(common_charge, {boss->pos, RGBA(0.5,0.6,2.0,0.0), 120, .sound = COMMON_CHARGE_SOUNDS}); + } + for(int i = 0; i < c; i++) { + play_sfx("shot1"); + spawn_circle(global.plr.pos, 0, radius, count, 4*120/n); + WAIT(120/c); + } + } + for(int i = 0; i < final_count; i++) { + cmplx vel = cnormalize(global.plr.pos-boss->pos)*cdir(M_TAU/final_count*i); + PROJECTILE( + .pos = boss->pos, + .draw_rule = {draw_wriggle_proj}, + .size = CMPLX(75,100), + .collision_size = CMPLX(60,70), + .color = RGBA(1.0,1.0,1,0.5), + .move = move_accelerated(vel,0.05*vel), + ); + } + boss->pos = CMPLX(VIEWPORT_W*rng_real(), -300); + + WAIT(60); + } +} diff --git a/src/stages/stagex/stagex.c b/src/stages/stagex/stagex.c new file mode 100644 index 0000000000..401f620572 --- /dev/null +++ b/src/stages/stagex/stagex.c @@ -0,0 +1,143 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "stagex.h" + +#include "background_anim.h" +#include "draw.h" +#include "spells/spells.h" +#include "timeline.h" // IWYU pragma: keep +#include "yumemi.h" + +#include "global.h" +#include "stage.h" + +/* + * See the definition of AttackInfo in boss.h for information on how to set up the idmaps. + * To add, remove, or reorder spells, see this stage's header file. + */ + +struct stagex_spells_s stagex_spells = { + .midboss = { + .trap_representation = { + {-1, -1, -1, 3}, AT_Spellcard, "6.2.6.1p5 “Trap Representation”", 60, 20000, + TASK_INDIRECT_INIT(BossAttack, stagex_spell_trap_representation), + stagex_draw_yumemi_spellbg_voronoi, CMPLX(VIEWPORT_W/2,VIEWPORT_H/2), 7, + }, + .fork_bomb = { + {-1, -1, -1, 4}, AT_Spellcard, "IEEE 1003.1-1988 “fork()”", 60, 20000, + TASK_INDIRECT_INIT(BossAttack, stagex_spell_fork_bomb), + stagex_draw_yumemi_spellbg_voronoi, CMPLX(VIEWPORT_W/2,VIEWPORT_H/2), 7, + }, + }, + .boss = { + .infinity_network = { + {-1, -1, -1, 1}, AT_SurvivalSpell, "Network “Ethereal Ethernet”", 60, 80000, + TASK_INDIRECT_INIT(BossAttack, stagex_spell_infinity_network), + stagex_draw_yumemi_spellbg_voronoi, VIEWPORT_W/2.0+100.0*I, 7, + }, + .sierpinski = { + {-1, -1, -1, 2}, AT_Spellcard, "Automaton “Legacy of Sierpiński”", 90, 60000, + TASK_INDIRECT_INIT(BossAttack, stagex_spell_sierpinski), + stagex_draw_yumemi_spellbg_voronoi, VIEWPORT_W/2.0+120.0*I, 7, + }, + .mem_copy = { + {-1, -1, -1, 5}, AT_Spellcard, "Memory “Block-wise Copy”", 90, 150000, + TASK_INDIRECT_INIT(BossAttack, stagex_spell_mem_copy), + stagex_draw_yumemi_spellbg_voronoi, VIEWPORT_W/2.0+120.0*I, 7, + }, + .pipe_dream = { + {-1, -1, -1, 6}, AT_Spellcard, "Philosophy “Pipe Dream”", 90, 150000, + TASK_INDIRECT_INIT(BossAttack, stagex_spell_pipe_dream), + stagex_draw_yumemi_spellbg_voronoi, VIEWPORT_W/2.0+120.0*I, 7, + }, + .alignment = { + {-1, -1, -1, 7}, AT_Spellcard, "Address Space “Pointer Alignment”", 90, 80000, + TASK_INDIRECT_INIT(BossAttack, stagex_spell_alignment), + stagex_draw_yumemi_spellbg_voronoi, VIEWPORT_W/2.0+120.0*I, 7, + }, + .rings = { + {-1, -1, -1, 8}, AT_Spellcard, "Protection Domain “Ring ∞”", 90, 80000, + TASK_INDIRECT_INIT(BossAttack, stagex_spell_rings), + stagex_draw_yumemi_spellbg_voronoi, VIEWPORT_W/2.0+120.0*I, 7, + }, + }, +}; + +static void stagex_begin(void) { + stagex_drawsys_init(); + stagex_bg_init_fullstage(); + stage_start_bgm("stagex"); + + INVOKE_TASK(stagex_timeline); +} + +static void stagex_spellpractice_begin(void) { + stagex_drawsys_init(); + + StageXDrawData *draw_data = stagex_get_draw_data(); + draw_data->tower_global_dissolution = 1; + draw_data->tower_partial_dissolution = 1; + + Boss *boss = stagex_spawn_yumemi(BOSS_DEFAULT_SPAWN_POS); + boss_add_attack_from_info(boss, global.stage->spell, true); + boss_engage(boss); + global.boss = boss; + + stage_start_bgm("stagexboss"); +} + +static void stagex_end(void) { + stagex_drawsys_shutdown(); +} + +static void stagex_preload(ResourceGroup *rg) { + res_group_preload(rg, RES_TEXTURE, RESF_DEFAULT, + "cell_noise", + "stagex/bg", + "stagex/bg_binary", + "stagex/code", + "stagex/dissolve_mask", + NULL); + res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT, + "extra_bg", + "extra_tower_apply_mask", + "extra_tower_mask", + "zbuf_fog", + NULL); + res_group_preload(rg, RES_MATERIAL, RESF_DEFAULT, + "stage5/metal", + "stage5/stairs", + "stage5/wall", + NULL); + res_group_preload(rg, RES_MODEL, RESF_DEFAULT, + "stage5/metal", + "stage5/stairs", + "stage5/wall", + "tower_alt_uv", + NULL); +} + +StageProcs stagex_procs = { + .begin = stagex_begin, + .end = stagex_end, + .draw = stagex_draw_background, + .preload = stagex_preload, + .shader_rules = stagex_bg_effects, + .postprocess_rules = stagex_postprocess_effects, + .spellpractice_procs = &stagex_spell_procs, +}; + +StageProcs stagex_spell_procs = { + .begin = stagex_spellpractice_begin, + .end = stagex_end, + .draw = stagex_draw_background, + .preload = stagex_preload, + .shader_rules = stagex_bg_effects, + .postprocess_rules = stagex_postprocess_effects, +}; diff --git a/src/stages/stagex/stagex.h b/src/stages/stagex/stagex.h new file mode 100644 index 0000000000..9bbd5ded7c --- /dev/null +++ b/src/stages/stagex/stagex.h @@ -0,0 +1,40 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#pragma once +#include "taisei.h" + +#include "stageinfo.h" + +extern struct stagex_spells_s { + // this struct must contain only fields of type AttackInfo + // order of fields affects the visual spellstage number, but not its real internal ID + + struct { + AttackInfo trap_representation; + AttackInfo fork_bomb; + } midboss; + + struct { + AttackInfo infinity_network; + AttackInfo sierpinski; + AttackInfo mem_copy; + AttackInfo pipe_dream; + AttackInfo alignment; + AttackInfo rings; + } boss; + + // required for iteration + AttackInfo null; +} stagex_spells; + +extern StageProcs stagex_procs; +extern StageProcs stagex_spell_procs; + +Boss *stagex_spawn_yumemi(cmplx pos); +void stagex_draw_yumemi_portrait_overlay(SpriteParams *sp); diff --git a/src/stages/stagex/timeline.c b/src/stages/stagex/timeline.c new file mode 100644 index 0000000000..21e6078f06 --- /dev/null +++ b/src/stages/stagex/timeline.c @@ -0,0 +1,602 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "timeline.h" // IWYU pragma: keep +#include "background_anim.h" +#include "nonspells/nonspells.h" +#include "stagex.h" +#include "yumemi.h" + +#include "stages/common_imports.h" + +#define BPM168_4BEATS 86 + +static void stagex_dialog_post_boss(void) { + PlayerMode *pm = global.plr.mode; + INVOKE_TASK_INDIRECT(StageExPostBossDialog, pm->dialog->StageExPostBoss); +} + +TASK(glider_bullet, { + cmplx pos; real dir; real spacing; int interval; +}) { + const int nproj = 5; + const int nstep = 4; + BoxedProjectile projs[nproj]; + + const cmplx positions[][5] = { + {1+I, -1, 1, -I, 1-I}, + {2, I, 1, -I, 1-I}, + {2, 1+I, 2-I, -I, 1-I}, + {2, 0, 2-I, 1-I, 1-2*I}, + }; + + cmplx trans = cdir(ARGS.dir+1*M_PI/4)*ARGS.spacing; + + for(int i = 0; i < nproj; i++) { + projs[i] = ENT_BOX(PROJECTILE( + .pos = ARGS.pos+positions[0][i]*trans, + .proto = pp_ball, + .color = RGBA(0,0,1,1), + )); + } + + for(int step = 0;; step++) { + int cur_step = step%nstep; + int next_step = (step+1)%nstep; + + int dead_count = 0; + for(int i = 0; i < nproj; i++) { + Projectile *p = ENT_UNBOX(projs[i]); + if(p == NULL) { + dead_count++; + } else { + p->move.retention = 1; + p->move.velocity = -(positions[cur_step][i]-(1-I)*(cur_step==3)-positions[next_step][i])*trans/ARGS.interval; + } + } + if(dead_count == nproj) { + return; + } + WAIT(ARGS.interval); + } +} + +TASK(glider_fairy, { + cmplx pos; cmplx dir; +}) { + Enemy *e = TASK_BIND(espawn_big_fairy(VIEWPORT_W/2-10*I, ITEMS( + .power = 3, + .points = 5, + ))); + + YIELD; + + for(int i = 0; i < 80; ++i) { + e->pos += cnormalize(ARGS.pos-e->pos)*2; + YIELD; + } + + for(int i = 0; i < 3; i++) { + real aim = carg(global.plr.pos - e->pos); + INVOKE_TASK(glider_bullet, e->pos, aim-0.7, 20, 6); + INVOKE_TASK(glider_bullet, e->pos, aim, 25, 3); + INVOKE_TASK(glider_bullet, e->pos, aim+0.7, 20, 6); + WAIT(80-20*i); + } + + for(;;) { + e->pos += 2*(re(e->pos)-VIEWPORT_W/2 > 0)-1; + YIELD; + } +} + +TASK(aimgrind_fairy, { + cmplx pos; +}) { + Enemy *e = TASK_BIND(espawn_big_fairy(ARGS.pos, NULL)); + cmplx v = CMPLX(1-2*(re(ARGS.pos)pos; + cmplx aim = cnormalize(d); + real r = cabs(d)*(0.6-0.2*k); + real v0 = 6; + real phi = acos(1-0.5*v0*v0/r/r); + for(int j = -1; j <= 1; j+=2) { + PROJECTILE( + .pos = e->pos, + .proto = pp_bullet, + .color = RGBA(0.1,0.4,1,1), + .move = {-v0*j*aim*I,0,cdir(phi*j)}, + .timeout = M_PI/phi, + ); + } + PROJECTILE( + .pos = e->pos, + .proto = pp_ball, + .color = RGBA(0.8,0.1,0.5,1), + .move = move_asymptotic(0, aim*5*cdir(0.3*sin(i)),0.1), + ); + } + WAIT(10); + + } + e->move = move_linear(v); + +} + +TASK(rocket_proj, { cmplx pos; cmplx dir; }) { + Projectile *p = TASK_BIND(PROJECTILE( + .pos = ARGS.pos, + .proto = pp_bigball, + .color = RGBA(0,0.2,1,0.0), + .move = move_accelerated(0,0.2*ARGS.dir) + )); + real phase = rng_angle(); + real period = rng_range(0.2,0.5); + for(int i = 0; ; i++) { + PROJECTILE( + .pos = p->pos, + .proto = pp_bullet, + .color = RGBA(0.1,0.2,0.9,1.0), + .move = move_accelerated(0,0.01*cdir(period*i+phase)), + ); + YIELD; + } +} + + +TASK(rocket_fairy, { cmplx pos; }) { + Enemy *e = TASK_BIND(espawn_fairy_red(ARGS.pos, NULL)); + + e->move = move_linear(0.5*I); + + cmplx aim = rng_dir(); + int rockets = 3; + for(int i = 0; i < rockets; i++) { + INVOKE_TASK(rocket_proj, e->pos, aim*cdir(M_TAU/rockets*i)); + WAIT(10); + } +} + +TASK(ngoner_proj, { cmplx pos; cmplx target; int stop_time; int laser_time; cmplx laser_vel; }) { + Projectile *p = TASK_BIND(PROJECTILE( + .pos = ARGS.pos, + .proto = pp_bullet, + .color = RGBA(0,0.2,1,0.0), + .move = move_linear(ARGS.target/ARGS.stop_time) + )); + WAIT(ARGS.stop_time); + p->move = move_dampen(p->move.velocity, 0.5); + + WAIT(ARGS.laser_time-ARGS.stop_time); + p->move = move_linear(ARGS.laser_vel); + + PROJECTILE( + .pos = p->pos, + .proto = pp_ball, + .color = RGBA(0.1,0.9,1,0.0), + .move = move_linear(2*cnormalize(ARGS.target)), + ); +} + +TASK(ngoner_laser, { cmplx pos; cmplx dir; }) { + create_laser(ARGS.pos, 60, 360, RGBA(0.1,0.9,1.0,0.1), laser_rule_linear(ARGS.dir)); + create_laser(ARGS.pos, 60, 360, RGBA(0.1,0.9,1.0,0.1), laser_rule_linear(ARGS.dir)); +} + +TASK(ngoner_fairy, { cmplx pos; }) { + Enemy *e = TASK_BIND(espawn_fairy_red(ARGS.pos, NULL)); + + cmplx rot = rng_dir(); + + int corners = 6; + int projs_per_site = 11; + int assembly_time = 10; + real site_length = 70; + + real b = site_length / 2 * tan(M_PI* (0.5 - 1.0 / corners)); + + int laser_time = corners*projs_per_site + assembly_time + 10; + + for(int i = 0; i < corners; i++) { + cmplx offset = rot*b*cdir(M_TAU/corners*i); + INVOKE_TASK_DELAYED(laser_time, ngoner_laser, e->pos + offset, 5*I*cnormalize(offset)); + } + + for(int s = 0; s < projs_per_site; s++) { + for(int i = 0; i < corners; i++) { + real phase = M_TAU/corners*(s/(real)projs_per_site-0.5); + real radius = b/cos(phase); + cmplx target = rot*radius*cdir(phase+M_TAU/corners*i); + + int laser_delay = laser_time + 2*abs(s-projs_per_site/2) - i - s*corners; + cmplx laser_vel = rot*3.5*cdir(M_TAU/corners*i+0.06*(s-projs_per_site/2.0)); + + + INVOKE_TASK(ngoner_proj, e->pos, target, assembly_time, laser_delay, laser_vel); + YIELD; + } + } + e->move = move_linear(I); +} + +static Boss *stagex_spawn_scuttle(cmplx pos0) { + Boss *scuttle = create_boss("Scutƫle", "scuttle", pos0); + boss_set_portrait(scuttle, "scuttle", NULL, "normal"); + scuttle->shadowcolor = *RGBA(0.5, 0.0, 0.22, 1); + scuttle->glowcolor = *RGBA(0.30, 0.0, 0.12, 0); + + return scuttle; +} + +TASK(scuttle_appear, { cmplx pos; }) { + STAGE_BOOKMARK(midboss); + Boss *boss = global.boss = TASK_BIND(stagex_spawn_scuttle(ARGS.pos)); + + boss_add_attack_from_info(boss, &stagex_spells.midboss.trap_representation, false); + boss_add_attack_from_info(boss, &stagex_spells.midboss.fork_bomb, false); + boss_engage(global.boss); +} + +TASK(scuttleproj_appear) { + STAGE_BOOKMARK(scuttleproj); + + Projectile *p = TASK_BIND(PROJECTILE( + .pos = VIEWPORT_W/2, + .proto = pp_soul, + .color = RGBA(0,0.2,1,0), + .move = move_towards(0, global.plr.pos, 0.01), + .flags = PFLAG_NOCLEAR | PFLAG_NOCOLLISION | PFLAG_NOAUTOREMOVE, + )); + + WAIT(20); + + int num_spots = 32; + for(int i = 0; i < BPM168_4BEATS * 3; i++) { + int spot = rng_range(0,num_spots); + cmplx offset = cdir(M_TAU/num_spots*spot); + real clr = rng_range(0,1); + + cmplx vel = 2*rng_dir(); + PROJECTILE( + .pos = p->pos + 50*offset, + .proto = pp_bullet, + .color = RGBA(clr,0.2,1,0), + .flags = PFLAG_MANUALANGLE, + .angle = carg(offset), + .move = move_linear(vel), + .timeout = rng_range(20,60), + ); + YIELD; + p->move.attraction_point = global.plr.pos; + + if(i % 5 == 0) { + if(rng_chance(0.2)) { + p->sprite = res_sprite("proj/bigball"); + p->pos += 30*rng_dir(); + } else { + p->sprite = res_sprite("proj/soul"); + } + } + } + kill_projectile(p); + + INVOKE_TASK(scuttle_appear, p->pos); +} + +TASK(yumemi_appear, { BoxedBoss boss; }) { + Boss *boss = TASK_BIND(ARGS.boss); + boss->move = move_from_towards(boss->pos, VIEWPORT_W/2 + 180*I, 0.015); +} + +TASK(spawn_boss) { + STAGE_BOOKMARK(boss); + + Boss *boss = global.boss = stagex_spawn_yumemi(5*VIEWPORT_W/4 - 200*I); + PlayerMode *pm = global.plr.mode; + + Attack *opening_attack = boss_add_attack(boss, AT_Normal, "Opening", 60, 40000, NULL); + StageExPreBossDialogEvents *e; + + INVOKE_TASK_INDIRECT(StageExPreBossDialog, pm->dialog->StageExPreBoss, &e); + INVOKE_TASK_WHEN(&e->boss_appears, yumemi_appear, ENT_BOX(boss)); + INVOKE_TASK_WHEN(&e->music_changes, stagex_boss_nonspell_1, ENT_BOX(boss), opening_attack); + INVOKE_TASK_WHEN(&e->music_changes, common_start_bgm, "stagexboss"); + + WAIT_EVENT(&global.dialog->events.fadeout_began); + + boss_add_attack_from_info(boss, &stagex_spells.boss.sierpinski, false); + boss_add_attack_task(boss, AT_Normal, "non2", 60, 40000, TASK_INDIRECT(BossAttack, stagex_boss_nonspell_2), NULL); + boss_add_attack_from_info(boss, &stagex_spells.boss.mem_copy, false); + boss_add_attack_task(boss, AT_Normal, "non3", 60, 40000, TASK_INDIRECT(BossAttack, stagex_boss_nonspell_3), NULL); + boss_add_attack_from_info(boss, &stagex_spells.boss.infinity_network, false); + boss_add_attack_task(boss, AT_Normal, "non4", 60, 40000, TASK_INDIRECT(BossAttack, stagex_boss_nonspell_4), NULL); + boss_add_attack_from_info(boss, &stagex_spells.boss.pipe_dream, false); + boss_add_attack_task(boss, AT_Normal, "non5", 60, 40000, TASK_INDIRECT(BossAttack, stagex_boss_nonspell_5), NULL); + boss_add_attack_from_info(boss, &stagex_spells.boss.alignment, false); + boss_add_attack_task(boss, AT_Normal, "non6", 60, 40000, TASK_INDIRECT(BossAttack, stagex_boss_nonspell_6), NULL); + boss_add_attack_from_info(boss, &stagex_spells.boss.rings, false); + + boss_engage(boss); +} + +#if 0 +DEFINE_EXTERN_TASK(stagex_timeline) { + for(int i = 0; i < 20; i++) { + real rx = rng_range(-1,1)*100; + real ry = rng_range(-1,1)*50; + INVOKE_TASK_DELAYED(400+i*50, rocket_fairy, CMPLX(VIEWPORT_W*0.5+rx, VIEWPORT_H*0.3+ry)); + } + for(int i = 0; i < 20; i++) { + real rx = rng_range(-1,1)*100; + real ry = rng_range(-1,1)*50; + INVOKE_TASK_DELAYED(1000+i*100, aimgrind_fairy, CMPLX(VIEWPORT_W*0.5+rx, VIEWPORT_H*0.3+ry)); + } + for(int i = 0; i < 20; i++) { + real rx = rng_range(-1,1)*100; + real ry = rng_range(-1,1)*50; + INVOKE_TASK_DELAYED(1500+i*70, ngoner_fairy, CMPLX(VIEWPORT_W*0.5+rx, VIEWPORT_H*0.3+ry)); + } + for(int i = 0; i < 4;i++) { + INVOKE_TASK_DELAYED(2000+i*100, glider_fairy, CMPLX(VIEWPORT_W*(i&1), VIEWPORT_H*0.5), 3*I); + WAIT(140); + } + + INVOKE_TASK_DELAYED(2500, scuttleproj_appear); + while(!global.boss) YIELD; + WAIT_EVENT(&global.boss->events.defeated); + + WAIT(1000); + + stagex_get_draw_data()->tower_global_dissolution = 1; + INVOKE_TASK(spawn_boss); + while(!global.boss) YIELD; + WAIT_EVENT(&global.boss->events.defeated); + + stage_unlock_bgm("stagexboss"); + + WAIT(240); + stagex_dialog_post_boss(); + WAIT_EVENT(&global.dialog->events.fadeout_began); + + WAIT(5); + stage_finish(GAMEOVER_SCORESCREEN); +} +#endif + +TASK(rotate_velocity, { + MoveParams *move; + real angle; + int duration; +}) { + cmplx r = cdir(ARGS.angle / ARGS.duration); + ARGS.move->retention *= r; + WAIT(ARGS.duration); + ARGS.move->retention /= r; +} + +static void set_turning_motion(Enemy *e, cmplx v, real turn_angle, int turn_delay, int turn_duration) { + e->move = move_linear(v); + INVOKE_SUBTASK_DELAYED(turn_delay, rotate_velocity, + .move = &e->move, + .angle = turn_angle, + .duration = turn_duration + ); +} + +TASK(midswirl, { + cmplx pos; + cmplx vel; + real turn_angle; + int turn_delay; + int turn_duration; +}) { + Enemy *e = TASK_BIND(espawn_swirl(ARGS.pos, ITEMS(.points = 1))); + set_turning_motion(e, ARGS.vel, ARGS.turn_angle, ARGS.turn_delay, ARGS.turn_duration); + + for(;;WAIT(6)) { +// play_sfx("shot1"); + + cmplx aim = cnormalize(global.plr.pos - e->pos); + + PROJECTILE( + .pos = e->pos, + .proto = pp_ball, + .color = RGB(0, 0, 1), + .move = move_asymptotic_simple(aim * 5, 6), + ); + } + + STALL; +} + +TASK(midswirls, { + int count; + cmplx pos; + cmplx vel; + real turn_angle; + int turn_delay; + int turn_duration; +}) { + for(int i = 0; i < ARGS.count; ++i, WAIT(12)) { + INVOKE_TASK(midswirl, + .pos = ARGS.pos, + .vel = ARGS.vel, + .turn_angle = ARGS.turn_angle, + .turn_delay = ARGS.turn_delay, + .turn_duration = ARGS.turn_duration + ); + } +} + +static int midboss_section(void) { + int t = 0; + + STAGE_BOOKMARK(midboss); + stagex_bg_trigger_next_phase(); + t += WAIT(BPM168_4BEATS * 1.25); + play_sfx("shot_special1"); + + INVOKE_TASK(midswirls, + .count = 8, + .pos = 0 + 64*I, + .vel = 8, + .turn_angle = M_PI, + .turn_delay = 20, + .turn_duration = 30 + ); + + t += WAIT(BPM168_4BEATS); + + INVOKE_TASK(midswirls, + .count = 8, + .pos = VIEWPORT_W + 64*I, + .vel = -8, + .turn_angle = -M_PI, + .turn_delay = 20, + .turn_duration = 30 + ); + + t += WAIT(BPM168_4BEATS); + + INVOKE_TASK(midswirls, + .count = 8, + .pos = 0 + 260*I, + .vel = 8, + .turn_angle = 3*M_PI/2, + .turn_delay = 20, + .turn_duration = 30 + ); + + INVOKE_TASK(midswirls, + .count = 8, + .pos = VIEWPORT_W + 260*I, + .vel = -8, + .turn_angle = -3*M_PI/2, + .turn_delay = 20, + .turn_duration = 30 + ); + + t += WAIT(BPM168_4BEATS * 2); + + INVOKE_TASK(scuttleproj_appear); + INVOKE_TASK(ngoner_fairy, 140 + 140 * I); + t += WAIT(BPM168_4BEATS); + INVOKE_TASK(ngoner_fairy, VIEWPORT_W - 140 + 140 * I); + t += WAIT(BPM168_4BEATS); + while(!global.boss) { + ++t; + YIELD; + } + log_debug("midboss spawn: %i", t); + t += WAIT_EVENT_OR_DIE(&NOT_NULL(global.boss)->events.defeated).frames; + log_debug("midboss defeat: %i", t); + STAGE_BOOKMARK(post-midboss); + + return t; +} + +TASK(laser45, { cmplx origin; cmplx dir; cmplx r; const Color *clr; }) { + int d0 = 60; + int d1 = 15; + + play_sfx("laser1"); + + MoveParams *move; + auto l = TASK_BIND(create_dynamic_laser(ARGS.origin, 120, (d0+d1) * 4, ARGS.clr, &move)); + l->width_exponent = 0.5; + + cmplx r = ARGS.r; + *move = move_linear(ARGS.dir * 3); + + for(;;) { + WAIT(d0); + move->velocity *= r; + WAIT(d1); + move->velocity *= r; + } +} + +TASK(fairy_laser45, { cmplx origin; }) { + auto e = TASK_BIND(espawn_huge_fairy(ARGS.origin, ITEMS(.points = 5))); + ecls_anyfairy_summon(e, 60); + + for(int i = 0; i < 3; ++i) { + RADIAL_LOOP(l, 8, I) { + INVOKE_TASK(laser45, e->pos, l.dir, cdir(M_PI/4), RGB(0.5, 0.1, 0.8)); + INVOKE_TASK_DELAYED(30, laser45, e->pos, l.dir, cdir(-M_PI/4), RGB(0.1, 0.5, 0.8)); + } + WAIT(400); + } + + enemy_kill(e); +} + +TASK(intro_swirl, { cmplx origin; cmplx dir; }) { + auto e = TASK_BIND(espawn_swirl(ARGS.origin, ITEMS(.points = 0))); + vec3 p = { 0, 0, stage_3d_context.cam.pos[2] - 150 }; + ecls_anyenemy_fake3dmovein(e, &stage_3d_context.cam, p, 180); + e->move = move_accelerated(0, ARGS.dir * -0.05); + WAIT(5); + PROJECTILE( + .proto = pp_bullet, + .pos = e->pos, + .move = move_asymptotic_simple(2 * cnormalize(global.plr.pos - e->pos), 2), + .color = RGB(0, 0, 1), + ); + // enemy_kill(e); +} + +DEFINE_EXTERN_TASK(stagex_timeline) { + WAIT(BPM168_4BEATS * 2); + + { + int t = BPM168_4BEATS * 1000; + int interval = 8; + cmplx r = cdir(0.3); + cmplx dir = -I; + for(;t - interval > 0; t -= interval) { + INVOKE_SUBTASK(intro_swirl, 0.5*(VIEWPORT_W+VIEWPORT_H*I) + 80 * dir, dir); + dir *= r; + WAIT(interval); + } + log_debug("%i", t); + } + + + // WAIT(400); + + // INVOKE_TASK(fairy_laser45, 0.5*(VIEWPORT_W+VIEWPORT_H*I)); + + WAIT(5762); + int midboss_time = midboss_section(); + stagex_bg_trigger_next_phase(); + WAIT(4140 - midboss_time); + stagex_bg_trigger_tower_dissolve(); + STAGE_BOOKMARK(post-midboss-filler); + WAIT(BPM168_4BEATS * 24); + stagex_bg_trigger_next_phase(); + + STAGE_BOOKMARK(pre-boss); + WAIT(BPM168_4BEATS * 6); + + INVOKE_TASK(spawn_boss); + while(!global.boss) YIELD; + WAIT_EVENT_OR_DIE(&global.boss->events.defeated); + + stage_unlock_bgm("stagexboss"); + + WAIT(240); + stagex_dialog_post_boss(); + WAIT_EVENT_OR_DIE(&global.dialog->events.fadeout_began); + + WAIT(5); + stage_finish(GAMEOVER_SCORESCREEN); +} diff --git a/src/stages/extra.h b/src/stages/stagex/timeline.h similarity index 69% rename from src/stages/extra.h rename to src/stages/stagex/timeline.h index 07f245fde9..989efefd5e 100644 --- a/src/stages/extra.h +++ b/src/stages/stagex/timeline.h @@ -9,8 +9,6 @@ #pragma once #include "taisei.h" -#include "stageinfo.h" +#include "coroutine/taskdsl.h" -// NOTE: See the 'yumemi' branch for the work-in-progress extra stage - -extern StageProcs extra_procs; +DECLARE_EXTERN_TASK(stagex_timeline); diff --git a/src/stages/stagex/yumemi.c b/src/stages/stagex/yumemi.c new file mode 100644 index 0000000000..b236f4a546 --- /dev/null +++ b/src/stages/stagex/yumemi.c @@ -0,0 +1,372 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#include "yumemi.h" +#include "draw.h" +#include "stagex.h" + +#include "audio/audio.h" +#include "common_tasks.h" +#include "global.h" +#include "renderer/api.h" +#include "util/glm.h" + +#define BOMBSHIELD_RADIUS 128 + +DEFINE_ENTITY_TYPE(BossShield, { + cmplx pos; + Boss *boss; + BoxedEnemy hitbox; + float alpha; + + ShaderProgram *shader; + + Texture *grid_tex; + Texture *code_tex; + Texture *shell_tex; + + vec2 grid_aspect; + vec2 code_aspect; + + struct { + Uniform *alpha; + Uniform *time; + Uniform *code_tex; + Uniform *grid_tex; + Uniform *shell_tex; + Uniform *code_aspect; + Uniform *grid_aspect; + } uniforms; +}); + +static void draw_yumemi_slave(EntityInterface *ent) { + YumemiSlave *slave = ENT_CAST(ent, YumemiSlave); + + float time = global.frames - slave->spawn_time; + float s = slave->rotation_factor; + float opacity; + float scale; + + if(stagex_drawing_into_glitchmask()) { + opacity = lerpf(2.0f, 80.0f, slave->glitch_strength); + scale = 1.1f; + } else { + opacity = slave->alpha; + scale = 1.0f + 6.0f * glm_ease_elast_in(1.0f - slave->alpha); + } + + scale *= 0.5f; + + SpriteParams sp = { + .pos.as_cmplx = slave->pos, + .scale = scale, + .color = color_mul_scalar(RGBA(1, 1, 1, 0.5), opacity), + }; + + sp.sprite_ptr = slave->sprites.frame; + sp.rotation.angle = -M_PI/73 * time * s; + r_draw_sprite(&sp); + + sp.sprite_ptr = slave->sprites.outer; + sp.rotation.angle = +M_PI/73 * time * s; + r_draw_sprite(&sp); + + sp.sprite_ptr = slave->sprites.core; + sp.rotation.angle = 0; + sp.color = color_mul_scalar(RGBA(0.4, 0.4, 0.4, 0.1), opacity); + sp.pos.as_cmplx += 3 * cdir(M_PI/72 * time * s); + r_draw_sprite(&sp); +} + +static void bombshield_draw(EntityInterface *ent) { + BossShield *shield = ENT_CAST(ent, BossShield); + float alpha = shield->alpha; + + if(alpha <= 0) { + return; + } + + float size = BOMBSHIELD_RADIUS * 2 * (2.0 - 1.0 * glm_ease_quad_out(alpha)); + + r_shader_ptr(shield->shader); + + r_mat_mv_push(); + r_mat_mv_translate(re(shield->pos), im(shield->pos), 0); + + if(stagex_drawing_into_glitchmask()) { + float x = rng_f32s() * 16; + float y = rng_f32s() * 16; + r_mat_mv_translate(x, y, 0); + } + + r_mat_mv_scale(size, size, 1); + r_uniform_float(shield->uniforms.alpha, alpha); + r_uniform_float(shield->uniforms.time, global.frames / 60.0 * 0.2); + r_uniform_sampler(shield->uniforms.code_tex, shield->code_tex); + r_uniform_sampler(shield->uniforms.grid_tex, shield->grid_tex); + r_uniform_sampler(shield->uniforms.shell_tex, shield->shell_tex); + r_uniform_vec2_vec(shield->uniforms.code_aspect, shield->code_aspect); + r_uniform_vec2_vec(shield->uniforms.grid_aspect, shield->grid_aspect); + r_draw_quad(); + r_mat_mv_pop(); +} + +static bool is_bombshield_actiev(BossShield *shield) { + Boss *boss = NOT_NULL(shield->boss); + return + boss_is_vulnerable(boss) && + player_is_bomb_active(&global.plr) && + boss->current && boss->current->type == AT_Spellcard && + 1; +} + +static Enemy *get_bombshield_hitbox(BossShield *shield) { + Enemy *hitbox = ENT_UNBOX(shield->hitbox); + + if(is_bombshield_actiev(shield)) { + if(!hitbox) { + hitbox = create_enemy_p(&global.enemies, 0, 1, ENEMY_NOVISUAL); + hitbox->flags = ( + EFLAG_IMPENETRABLE | + EFLAG_INVULNERABLE | + EFLAG_NO_AUTOKILL | + EFLAG_NO_DEATH_EXPLOSION | + EFLAG_NO_HURT | + EFLAG_NO_VISUAL_CORRECTION | + 0 + ); + hitbox->hit_radius = BOMBSHIELD_RADIUS; + shield->hitbox = ENT_BOX(hitbox); + } + } else { + if(hitbox) { + enemy_kill(hitbox); + hitbox = shield->hitbox.ent = NULL; + } + } + + return hitbox; +} + +TASK(yumemi_bombshield_expire, { BoxedBossShield shield; }) { + BossShield *shield = TASK_BIND(ARGS.shield); + Enemy *hitbox = ENT_UNBOX(shield->hitbox); + + if(hitbox) { + enemy_kill(hitbox); + shield->hitbox.ent = NULL; + } +} + +TASK(yumemi_bombshield_controller, { BoxedBoss boss; }) { + Boss *boss = TASK_BIND(ARGS.boss); + BossShield *shield = TASK_HOST_ENT(BossShield); + + shield->boss = boss; + shield->ent.draw_func = bombshield_draw; + shield->ent.draw_layer = LAYER_BOSS | 0x10; + + shield->shader = res_shader("bombshield"); + shield->grid_tex = res_texture("stagex/hex_tiles"); + shield->code_tex = res_texture("stagex/bg_binary"); + shield->shell_tex = res_texture("stagex/bg"); + + uint w, h; + + r_texture_get_size(shield->grid_tex, 0, &w, &h); + shield->grid_aspect[0] = 512.0f / (float)w; + shield->grid_aspect[1] = 512.0f / (float)h; + + r_texture_get_size(shield->code_tex, 0, &w, &h); + shield->code_aspect[0] = 256.0f / (float)w; + shield->code_aspect[1] = 256.0f / (float)h; + + shield->uniforms.alpha = r_shader_uniform(shield->shader, "alpha"); + shield->uniforms.time = r_shader_uniform(shield->shader, "time"); + shield->uniforms.code_tex = r_shader_uniform(shield->shader, "code_tex"); + shield->uniforms.grid_tex = r_shader_uniform(shield->shader, "grid_tex"); + shield->uniforms.shell_tex = r_shader_uniform(shield->shader, "shell_tex"); + shield->uniforms.code_aspect = r_shader_uniform(shield->shader, "code_aspect"); + shield->uniforms.grid_aspect = r_shader_uniform(shield->shader, "grid_aspect"); + + INVOKE_TASK_AFTER(&TASK_EVENTS(THIS_TASK)->finished, yumemi_bombshield_expire, ENT_BOX(shield)); + + for(;;YIELD) { + shield->pos = boss->pos; + Enemy *hitbox = get_bombshield_hitbox(shield); + + if(hitbox) { + hitbox->pos = shield->pos; + boss->bomb_damage_multiplier = 0; + boss->shot_damage_multiplier = 0; + fapproach_p(&shield->alpha, 1.0f, 1.0/30.0f); + } else { + boss->bomb_damage_multiplier = 1; + boss->shot_damage_multiplier = 1; + fapproach_p(&shield->alpha, 0.0f, 1.0/15.0f); + } + } +} + +Boss *stagex_spawn_yumemi(cmplx pos) { + Boss *yumemi = create_boss("Okazaki Yumemi", "yumemi", pos - 400 * I); + boss_set_portrait(yumemi, "yumemi", NULL, "normal"); + yumemi->shadowcolor = *RGBA(0.5, 0.0, 0.22, 1); + yumemi->glowcolor = *RGBA(0.30, 0.0, 0.12, 0); + yumemi->move = move_towards(0, pos, 0.01); + yumemi->pos = pos; + + INVOKE_TASK(yumemi_bombshield_controller, ENT_BOX(yumemi)); + + return yumemi; +} + +void stagex_init_yumemi_slave(YumemiSlave *slave, cmplx pos, int type) { + slave->pos = pos; + slave->ent.draw_func = draw_yumemi_slave; + slave->ent.draw_layer = LAYER_BOSS - 1; + slave->spawn_time = global.frames; + + switch(type) { + case 0: + slave->sprites.core = res_sprite("stagex/yumemi_slaves/zero_core"); + slave->sprites.frame = res_sprite("stagex/yumemi_slaves/zero_frame"); + slave->sprites.outer = res_sprite("stagex/yumemi_slaves/zero_outer"); + slave->rotation_factor = 1; + break; + + case 1: + slave->sprites.core = res_sprite("stagex/yumemi_slaves/one_core"); + slave->sprites.frame = res_sprite("stagex/yumemi_slaves/one_frame"); + slave->sprites.outer = res_sprite("stagex/yumemi_slaves/one_outer"); + slave->rotation_factor = -1; + break; + + default: UNREACHABLE; + } +} + +TASK(yumemi_slave_fadein, { BoxedYumemiSlave slave; }) { + YumemiSlave *slave = TASK_BIND(ARGS.slave); + + slave->alpha = 0; + slave->glitch_strength = 1; + + while( + fapproach_p(&slave->alpha, 1.0f, 1.0f/60.0f) < 0.0f || + fapproach_p(&slave->glitch_strength, 0.0f, 1.0f/80.0f) > 0.0f + ) { + YIELD; + } +} + +TASK(yumemi_slave_fader, { BoxedYumemiSlave slave; }) { + YumemiSlave *slave = NOT_NULL(ENT_UNBOX(ARGS.slave)); + YumemiSlave *fader = TASK_HOST_ENT(YumemiSlave); + + fader->ent.draw_func = draw_yumemi_slave; + fader->ent.draw_layer = LAYER_BOSS - 1; + fader->sprites = slave->sprites; + fader->spawn_time = slave->spawn_time; + fader->rotation_factor = slave->rotation_factor; + fader->alpha = slave->alpha; + fader->pos = slave->pos; + fader->glitch_strength = 1; + + slave = NULL; + + while(fapproach_p(&fader->alpha, 0.0f, 1.0f / 60.0f) > 0) { + YIELD; + } +} + +YumemiSlave *stagex_host_yumemi_slave(cmplx pos, int type) { + YumemiSlave *slave = TASK_HOST_ENT(YumemiSlave); + TASK_HOST_EVENTS(slave->events); + stagex_init_yumemi_slave(slave, pos, type); + INVOKE_TASK(yumemi_slave_fadein, ENT_BOX(slave)); + INVOKE_TASK_AFTER(&slave->events.despawned, yumemi_slave_fader, ENT_BOX(slave)); + return slave; +} + +void stagex_despawn_yumemi_slave(YumemiSlave *slave) { + coevent_signal_once(&slave->events.despawned); +} + +TASK(laser_sweep_sound) { + SFXPlayID csound = play_sfx("charge_generic"); + WAIT(50); + stop_sfx(csound); + play_sfx("laser1"); +} + +void stagex_yumemi_slave_laser_sweep(YumemiSlave *slave, real s, cmplx target) { + int cnt = 32; + + INVOKE_SUBTASK(laser_sweep_sound); + real g = carg(target - slave->pos); + real angle_ofs = (s < 0) * M_PI; + + for(int i = 0; i < cnt; ++i) { + real x = i/(real)cnt; + cmplx o = 32 * cdir(s * x * M_TAU + g + M_PI/2 + angle_ofs); + cmplx pos = slave->pos + o; + cmplx aim = cnormalize(target - pos + o); + Color *c = RGBA(0.1 + 0.9 * x * x, 1 - 0.9 * (1 - pow(1 - x, 2)), 0.1, 0); + create_laserline(pos, 40 * aim, 60 + i, 80 + i, c); + WAIT(1); + } + + WAIT(10 + cnt); +} + +void stagex_draw_yumemi_portrait_overlay(SpriteParams *sp) { + StageXDrawData *draw_data = stagex_get_draw_data(); + + sp->sprite_ptr = res_sprite("dialog/yumemi_misc_code_mask"); + sp->shader_ptr = res_shader("sprite_yumemi_overlay"); + sp->aux_textures[0] = res_texture("stagex/code"); + sp->shader_params = &(ShaderCustomParams) { + global.frames / 60.0, + draw_data->codetex_aspect[0], + draw_data->codetex_aspect[1], + draw_data->codetex_num_segments, + }; + + r_draw_sprite(sp); +} + +static void render_spellbg_mask(Framebuffer *fb) { + r_state_push(); + r_framebuffer(fb); + r_blend(BLEND_NONE); + r_clear(BUFFER_ALL, RGBA(0, 0, 0, 1), 1); + r_shader("yumemi_spellbg_voronoi"); + r_mat_mv_push(); + r_mat_mv_scale(VIEWPORT_W, VIEWPORT_H, 1); + r_mat_mv_translate(0.5, 0.5, 0); + r_uniform_float("time", global.frames / 60.0f); + r_uniform_vec4("color", 0.1, 0.2, 0.05, 1.0); + r_draw_quad(); + r_mat_mv_pop(); + r_state_pop(); +} + +void stagex_draw_yumemi_spellbg_voronoi(Boss *boss, int time) { + StageXDrawData *draw_data = stagex_get_draw_data(); + render_spellbg_mask(draw_data->fb.spell_background_lq); + + r_state_push(); + r_blend(BLEND_NONE); + r_clear(BUFFER_ALL, RGBA(0, 0, 0, 1), 1); + r_shader("yumemi_spellbg_voronoi_compose"); + r_uniform_sampler("tex2", r_framebuffer_get_attachment(draw_data->fb.spell_background_lq, FRAMEBUFFER_ATTACH_COLOR0)); + // draw_framebuffer_tex(draw_data->fb.spell_background_lq, VIEWPORT_W, VIEWPORT_H); + fill_viewport(0, time/700.0+0.5, 0, "stagex/bg"); + r_state_pop(); +} diff --git a/src/stages/stagex/yumemi.h b/src/stages/stagex/yumemi.h new file mode 100644 index 0000000000..2fc4a6b25c --- /dev/null +++ b/src/stages/stagex/yumemi.h @@ -0,0 +1,39 @@ +/* + * This software is licensed under the terms of the MIT License. + * See COPYING for further information. + * --- + * Copyright (c) 2011-2024, Lukas Weber . + * Copyright (c) 2012-2024, Andrei Alexeyev . + */ + +#pragma once +#include "taisei.h" + +#include "entity.h" +#include "resource/sprite.h" +#include "coroutine/coevent.h" + +DEFINE_ENTITY_TYPE(YumemiSlave, { + struct { + Sprite *core, *frame, *outer; + } sprites; + + cmplx pos; + int spawn_time; + float alpha; + float rotation_factor; + float glitch_strength; + + COEVENTS_ARRAY( + despawned + ) events; +}); + +void stagex_init_yumemi_slave(YumemiSlave *slave, cmplx pos, int type); +YumemiSlave *stagex_host_yumemi_slave(cmplx pos, int type); +void stagex_despawn_yumemi_slave(YumemiSlave *slave); +void stagex_yumemi_slave_laser_sweep(YumemiSlave *slave, real s, cmplx target); + +Boss *stagex_spawn_yumemi(cmplx pos); + +void stagex_draw_yumemi_spellbg_voronoi(Boss *boss, int time); diff --git a/src/stageutils.c b/src/stageutils.c index dec58b4ae1..6b08b5a95c 100644 --- a/src/stageutils.c +++ b/src/stageutils.c @@ -60,7 +60,7 @@ void camera3d_apply_inverse_transforms(Camera3D *cam, mat4 mat) { mat4 temp; glm_mat4_identity(temp); camera3d_apply_transforms(cam, temp); - glm_mat4_inv_fast(temp, mat); + glm_mat4_inv_precise(temp, mat); } void stage3d_apply_inverse_transforms(Stage3D *s, mat4 mat) { @@ -102,7 +102,7 @@ void camera3d_project(Camera3D *cam, vec3 pos, vec3 dest) { mat4 mpersp; glm_perspective(cam->fovy, cam->aspect, cam->near, cam->far, mpersp); - camera3d_apply_rotations(cam, mpersp); + camera3d_apply_transforms(cam, mpersp); glm_project(pos, mpersp, viewport, dest); } diff --git a/src/util/graphics.c b/src/util/graphics.c index 83988b659c..5beaf3dc50 100644 --- a/src/util/graphics.c +++ b/src/util/graphics.c @@ -182,3 +182,4 @@ void draw_framebuffer_attachment(Framebuffer *fb, double width, double height, F void draw_framebuffer_tex(Framebuffer *fb, double width, double height) { draw_framebuffer_attachment(fb, width, height, FRAMEBUFFER_ATTACH_COLOR0); } +