New character art by Afensorm + other visual tweaks (#170)
* implement player spellcard declarations on bomb * stagedraw: move "bottom text" to a separate (higher) layer * update boss spell declaration effect * import afensorm's character portrait art * improve dialog visuals * acknowledge afensorm in credits and COPYING * 'alphamap' functionality; effects for wriggle and youmu portraits * update afens art * add cirno alphamap * dialog: draw active speaker above other other * charselect: use r_draw_sprite
8
COPYING
|
@ -23,10 +23,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
|
||||
The original soundtrack is composed by Tuck V and available under CC-BY 4.0.
|
||||
It comprises the audio files under resources/bgm/.
|
||||
It comprises the audio files under resources/00-taisei.pkgdir/bgm/.
|
||||
https://www.youtube.com/channel/UCaw73cuHLnFCSpjOtt_9pyg
|
||||
https://creativecommons.org/licenses/by/4.0/
|
||||
|
||||
The character portraits under atlas/portraits/ are created by afensorm as a
|
||||
commission, and are available under CC-BY 4.0.
|
||||
https://afens.surge.sh
|
||||
https://gensokyo.social/@afensorm
|
||||
https://pixiv.me/afens
|
||||
|
||||
The photos used in the Spellcard and menu backgrounds are generally in the
|
||||
public domain or CC0 and were not taken by us. Here are some honorary mentions.
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ atlases = [
|
|||
['common_ui', ['--width=1024', '--height=1024']],
|
||||
['gray16', [preset_png]],
|
||||
['huge', []],
|
||||
['portraits', ['--width=2048', '--height=4096']],
|
||||
['portraits', ['--width=3600', '--height=4096']],
|
||||
]
|
||||
|
||||
atlas_profiles = [
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
w = 336.5
|
||||
h = 500
|
||||
w = 326.5
|
||||
h = 478
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
w = 324
|
||||
w = 388
|
||||
h = 500
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
w = 284
|
||||
h = 500
|
||||
w = 338.5
|
||||
h = 493.5
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
w = 348
|
||||
h = 500
|
||||
w = 340
|
||||
h = 513
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
w = 277.5
|
||||
h = 500
|
||||
w = 402.5
|
||||
h = 478
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
w = 290.5
|
||||
h = 500
|
||||
w = 257
|
||||
h = 498.5
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
w = 350.5
|
||||
h = 500
|
||||
w = 311
|
||||
h = 491.5
|
||||
|
|
4
atlas/overrides/dialog/scuttle.spr
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
w = 263.5
|
||||
h = 474
|
||||
offset_x = -16
|
|
@ -1,3 +1,3 @@
|
|||
|
||||
w = 333.5
|
||||
h = 500
|
||||
w = 322
|
||||
h = 545.5
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
w = 293
|
||||
h = 500
|
||||
w = 288.5
|
||||
h = 491
|
||||
offset_x = 32
|
||||
|
|
BIN
atlas/portraits/dialog/cirno.alphamap.webp
Normal file
After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 206 KiB |
BIN
atlas/portraits/dialog/cirno.webp
Normal file
After Width: | Height: | Size: 241 KiB |
Before Width: | Height: | Size: 229 KiB |
BIN
atlas/portraits/dialog/elly.webp
Normal file
After Width: | Height: | Size: 205 KiB |
Before Width: | Height: | Size: 190 KiB |
BIN
atlas/portraits/dialog/hina.webp
Normal file
After Width: | Height: | Size: 190 KiB |
Before Width: | Height: | Size: 356 KiB |
BIN
atlas/portraits/dialog/iku.webp
Normal file
After Width: | Height: | Size: 215 KiB |
Before Width: | Height: | Size: 256 KiB |
BIN
atlas/portraits/dialog/kurumi.webp
Normal file
After Width: | Height: | Size: 189 KiB |
Before Width: | Height: | Size: 164 KiB |
BIN
atlas/portraits/dialog/marisa.webp
Normal file
After Width: | Height: | Size: 194 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 192 KiB |
BIN
atlas/portraits/dialog/scuttle.webp
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
atlas/portraits/dialog/wriggle.alphamap.webp
Normal file
After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 208 KiB |
BIN
atlas/portraits/dialog/wriggle.webp
Normal file
After Width: | Height: | Size: 247 KiB |
BIN
atlas/portraits/dialog/youmu.alphamap.webp
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 165 KiB |
BIN
atlas/portraits/dialog/youmu.webp
Normal file
After Width: | Height: | Size: 171 KiB |
BIN
resources/00-taisei.pkgdir/gfx/atlas_portraits_0.alphamap.webp
Normal file
After Width: | Height: | Size: 42 KiB |
|
@ -1,6 +1,7 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
source = res/gfx/atlas_portraits_0.webp
|
||||
alphamap = res/gfx/atlas_portraits_0.alphamap.webp
|
||||
|
||||
# -- Pasted from the global override file --
|
||||
|
||||
|
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.9 MiB |
|
@ -2,11 +2,11 @@
|
|||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 2
|
||||
region_y = 1006
|
||||
region_w = 673
|
||||
region_h = 1000
|
||||
region_y = 962
|
||||
region_w = 653
|
||||
region_h = 956
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 336.5
|
||||
h = 500
|
||||
w = 326.5
|
||||
h = 478
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 1350
|
||||
region_y = 1006
|
||||
region_w = 648
|
||||
region_x = 811
|
||||
region_y = 2
|
||||
region_w = 776
|
||||
region_h = 1000
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 324
|
||||
w = 388
|
||||
h = 500
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 587
|
||||
region_y = 2010
|
||||
region_w = 568
|
||||
region_h = 1000
|
||||
region_x = 2275
|
||||
region_y = 2
|
||||
region_w = 677
|
||||
region_h = 987
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 284
|
||||
h = 500
|
||||
w = 338.5
|
||||
h = 493.5
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 707
|
||||
region_x = 1591
|
||||
region_y = 2
|
||||
region_w = 696
|
||||
region_h = 1000
|
||||
region_w = 680
|
||||
region_h = 1026
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 348
|
||||
h = 500
|
||||
w = 340
|
||||
h = 513
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 1159
|
||||
region_y = 2010
|
||||
region_w = 555
|
||||
region_h = 1000
|
||||
region_x = 2
|
||||
region_y = 2
|
||||
region_w = 805
|
||||
region_h = 956
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 277.5
|
||||
h = 500
|
||||
w = 402.5
|
||||
h = 478
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 2
|
||||
region_y = 2010
|
||||
region_w = 581
|
||||
region_h = 1000
|
||||
region_x = 1190
|
||||
region_y = 1032
|
||||
region_w = 514
|
||||
region_h = 997
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 290.5
|
||||
h = 500
|
||||
w = 257
|
||||
h = 498.5
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 2
|
||||
region_x = 2956
|
||||
region_y = 2
|
||||
region_w = 701
|
||||
region_h = 1000
|
||||
region_w = 622
|
||||
region_h = 983
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 350.5
|
||||
h = 500
|
||||
w = 311
|
||||
h = 491.5
|
||||
|
|
13
resources/00-taisei.pkgdir/gfx/dialog/scuttle.spr
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 659
|
||||
region_y = 1006
|
||||
region_w = 527
|
||||
region_h = 948
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 263.5
|
||||
h = 474
|
||||
offset_x = -16
|
|
@ -1,12 +1,12 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 679
|
||||
region_y = 1006
|
||||
region_w = 667
|
||||
region_h = 1000
|
||||
region_x = 2275
|
||||
region_y = 993
|
||||
region_w = 644
|
||||
region_h = 1091
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 333.5
|
||||
h = 500
|
||||
w = 322
|
||||
h = 545.5
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
# Autogenerated by the atlas packer, do not modify
|
||||
|
||||
texture = atlas_portraits_0
|
||||
region_x = 1407
|
||||
region_y = 2
|
||||
region_w = 586
|
||||
region_h = 1000
|
||||
region_x = 2956
|
||||
region_y = 989
|
||||
region_w = 577
|
||||
region_h = 982
|
||||
|
||||
# -- Pasted from the override file --
|
||||
|
||||
w = 293
|
||||
h = 500
|
||||
w = 288.5
|
||||
h = 491
|
||||
offset_x = 32
|
||||
|
|
|
@ -70,6 +70,8 @@ glsl_files = files(
|
|||
'standardnotex.vert.glsl',
|
||||
'text_default.frag.glsl',
|
||||
'text_default.vert.glsl',
|
||||
'text_dialog.frag.glsl',
|
||||
'text_dialog.vert.glsl',
|
||||
'text_example.frag.glsl',
|
||||
'text_example.vert.glsl',
|
||||
'text_hud.frag.glsl',
|
||||
|
|
29
resources/00-taisei.pkgdir/shader/text_dialog.frag.glsl
Normal file
|
@ -0,0 +1,29 @@
|
|||
#version 330 core
|
||||
|
||||
#include "lib/render_context.glslh"
|
||||
#include "lib/sprite_main.frag.glslh"
|
||||
#include "lib/util.glslh"
|
||||
|
||||
float sampleNoise(vec2 tc) {
|
||||
tc.y *= fwidth(tc.x) / fwidth(tc.y);
|
||||
return texture(tex_aux[0], tc).r;
|
||||
}
|
||||
|
||||
void spriteMain(out vec4 fragColor) {
|
||||
float t = customParams.x;
|
||||
float r = customParams.y;
|
||||
|
||||
vec4 glyph = texture(tex, texCoord);
|
||||
float noise = sampleNoise(texCoordOverlay);
|
||||
|
||||
float d = 0.5;
|
||||
float x = 1 - t;
|
||||
float slope = x;
|
||||
float f = smoothstep(0.5 - d * (1.0 - x), 0.5 + d * x, r + (1 - 2 * r) * mix(texCoordOverlay.y, 1.0 - texCoordOverlay.x, slope) - x + 0.5);
|
||||
|
||||
vec4 textfrag = vec4(vec3(glyph.r + 0.25 * glyph.g), glyph.g);
|
||||
textfrag *= smoothstep(1 - 2 * f, 1-f, noise);
|
||||
textfrag *= color;
|
||||
|
||||
fragColor = textfrag;
|
||||
}
|
2
resources/00-taisei.pkgdir/shader/text_dialog.prog
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
objects = text_dialog.vert text_dialog.frag
|
8
resources/00-taisei.pkgdir/shader/text_dialog.vert.glsl
Normal file
|
@ -0,0 +1,8 @@
|
|||
#version 330 core
|
||||
|
||||
#define SPRITE_OUT_COLOR
|
||||
#define SPRITE_OUT_TEXCOORD
|
||||
#define SPRITE_OUT_TEXCOORD_OVERLAY
|
||||
#define SPRITE_OUT_CUSTOM
|
||||
|
||||
#include "lib/sprite_default.vert.glslh"
|
|
@ -30,7 +30,7 @@ void spriteMain(out vec4 fragColor) {
|
|||
fragColor = mix(shadowfrag, textfrag, sqrt(textfrag.a));
|
||||
|
||||
// The overlay coordinates are outside of [0,1] in the padding region, so we make sure there are no wrap around artifacts when a bit of text is distorted to this region.
|
||||
tc_overlay = clamp(tc_overlay, 0.01, 0.99);
|
||||
tc_overlay = clamp(tc_overlay, 0.01, 0.99);
|
||||
|
||||
fragColor *= clamp((texture(tex_aux[0], tc_overlay).r + 0.5) * 2.5 * t-0.5, 0.0, 1.0);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,14 @@
|
|||
|
||||
#include "interface/standard.glslh"
|
||||
|
||||
UNIFORM(1) sampler2D alphamap;
|
||||
UNIFORM(2) int have_alphamap;
|
||||
|
||||
void main(void) {
|
||||
fragColor = textureLod(tex, texCoordRaw, 0);
|
||||
fragColor.rgb *= fragColor.a;
|
||||
|
||||
if(have_alphamap != 0) {
|
||||
fragColor.a *= textureLod(alphamap, texCoordRaw, 0).r;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ def write_sprite_def(dst, texture, region, overrides=None):
|
|||
update_text_file(dst, text)
|
||||
|
||||
|
||||
def write_texture_def(dst, texture, texture_fmt, global_overrides=None, local_overrides=None):
|
||||
def write_texture_def(dst, texture, texture_fmt, global_overrides=None, local_overrides=None, have_alphamap=False):
|
||||
dst.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
text = (
|
||||
|
@ -87,6 +87,9 @@ def write_texture_def(dst, texture, texture_fmt, global_overrides=None, local_ov
|
|||
f'source = res/gfx/{texture}.{texture_fmt}\n'
|
||||
)
|
||||
|
||||
if have_alphamap:
|
||||
text += f'alphamap = res/gfx/{texture}.alphamap.{texture_fmt}\n'
|
||||
|
||||
if global_overrides is not None:
|
||||
text += f'\n# -- Pasted from the global override file --\n\n{global_overrides.strip()}\n'
|
||||
|
||||
|
@ -140,6 +143,14 @@ def get_override_file_name(basename):
|
|||
return f'{basename}.spr'
|
||||
|
||||
|
||||
def find_alphamap(basepath):
|
||||
for fmt in texture_formats:
|
||||
mpath = basepath.with_suffix(f'.alphamap.{fmt}')
|
||||
|
||||
if mpath.is_file():
|
||||
return mpath
|
||||
|
||||
|
||||
def gen_atlas(overrides, src, dst, binsize, atlasname, tex_format=texture_formats[0], border=1, force_single=False, crop=True, leanify=True):
|
||||
overrides = Path(overrides).resolve()
|
||||
src = Path(src).resolve()
|
||||
|
@ -165,7 +176,7 @@ def gen_atlas(overrides, src, dst, binsize, atlasname, tex_format=texture_format
|
|||
rects = []
|
||||
|
||||
for path in src.glob('**/*.*'):
|
||||
if path.is_file() and path.suffix[1:].lower() in texture_formats:
|
||||
if path.is_file() and path.suffix[1:].lower() in texture_formats and path.with_suffix('').suffix != '.alphamap':
|
||||
img = Image.open(path)
|
||||
sprite_name = path.relative_to(src).with_suffix('').as_posix()
|
||||
sprite_config_path = overrides / (get_override_file_name(sprite_name) + '.conf')
|
||||
|
@ -237,10 +248,10 @@ def gen_atlas(overrides, src, dst, binsize, atlasname, tex_format=texture_format
|
|||
# dstfile = temp_dst / f'{textureid}.{tex_format}'
|
||||
# NOTE: we always save PNG first and convert with an external tool later if needed.
|
||||
dstfile = temp_dst / f'{textureid}.png'
|
||||
dstfile_alphamap = temp_dst / f'{textureid}.alphamap.png'
|
||||
print(dstfile)
|
||||
|
||||
dstfile_meta = temp_dst / f'{textureid}.tex'
|
||||
write_texture_def(dstfile_meta, textureid, tex_format, texture_global_overrides, texture_local_overrides)
|
||||
|
||||
actual_size = [0, 0]
|
||||
|
||||
|
@ -254,21 +265,32 @@ def gen_atlas(overrides, src, dst, binsize, atlasname, tex_format=texture_format
|
|||
else:
|
||||
actual_size = (bin.width, bin.height)
|
||||
|
||||
composite_cmd = [
|
||||
base_composite_cmd = [
|
||||
'convert',
|
||||
'-verbose',
|
||||
'-size', f'{actual_size[0]}x{actual_size[1]}',
|
||||
'xc:none',
|
||||
]
|
||||
|
||||
composite_cmd = base_composite_cmd.copy() + ['xc:none']
|
||||
alphamap_composite_cmd = None
|
||||
|
||||
for rect in bin:
|
||||
img_path, name = rect.rid
|
||||
border = get_border(name)
|
||||
alphamap_path = find_alphamap(img_path)
|
||||
|
||||
composite_cmd += [
|
||||
str(img_path), '-geometry', '{:+}{:+}'.format(rect.x + border, rect.y + border), '-composite'
|
||||
]
|
||||
|
||||
if alphamap_path:
|
||||
if alphamap_composite_cmd is None:
|
||||
alphamap_composite_cmd = base_composite_cmd.copy() + ['xc:white']
|
||||
|
||||
alphamap_composite_cmd += [
|
||||
str(alphamap_path), '-geometry', '{:+}{:+}'.format(rect.x + border, rect.y + border), '-composite'
|
||||
]
|
||||
|
||||
override_path = overrides / get_override_file_name(name)
|
||||
|
||||
if override_path.exists():
|
||||
|
@ -286,8 +308,16 @@ def gen_atlas(overrides, src, dst, binsize, atlasname, tex_format=texture_format
|
|||
|
||||
composite_cmd += [str(dstfile)]
|
||||
|
||||
@executor.submit
|
||||
def process(dstfile=dstfile):
|
||||
write_texture_def(
|
||||
dstfile_meta,
|
||||
textureid,
|
||||
tex_format,
|
||||
texture_global_overrides,
|
||||
texture_local_overrides,
|
||||
have_alphamap=alphamap_composite_cmd is not None
|
||||
)
|
||||
|
||||
def process(composite_cmd, dstfile):
|
||||
subprocess.check_call(composite_cmd)
|
||||
|
||||
oldfmt = dstfile.suffix[1:].lower()
|
||||
|
@ -316,14 +346,18 @@ def gen_atlas(overrides, src, dst, binsize, atlasname, tex_format=texture_format
|
|||
if leanify:
|
||||
subprocess.check_call(['leanify', '-v', str(dstfile)])
|
||||
|
||||
futures.append(process)
|
||||
futures.append(executor.submit(lambda: process(composite_cmd, dstfile)))
|
||||
|
||||
if alphamap_composite_cmd is not None:
|
||||
alphamap_composite_cmd += [str(dstfile_alphamap)]
|
||||
futures.append(executor.submit(lambda: process(alphamap_composite_cmd, dstfile_alphamap)))
|
||||
|
||||
# Wait for subprocesses to complete.
|
||||
wait_for_futures(futures)
|
||||
executor.shutdown(wait=True)
|
||||
|
||||
# Only now, if everything is ok so far, copy everything to the destination, possibly overwriting previous results
|
||||
pattern = re.compile(rf'^atlas_{re.escape(atlasname)}_\d+.({"|".join(texture_formats + ["tex"])})$')
|
||||
pattern = re.compile(rf'^atlas_{re.escape(atlasname)}_\d+(?:\.alphamap)?.({"|".join(texture_formats + ["tex"])})$')
|
||||
for path in dst.iterdir():
|
||||
if pattern.match(path.name):
|
||||
path.unlink()
|
||||
|
|
91
src/boss.c
|
@ -14,6 +14,7 @@
|
|||
#include "stagetext.h"
|
||||
#include "stagedraw.h"
|
||||
#include "entity.h"
|
||||
#include "util/glm.h"
|
||||
|
||||
static void ent_draw_boss(EntityInterface *ent);
|
||||
static DamageResult ent_damage_boss(EntityInterface *ent, const DamageInfo *dmg);
|
||||
|
@ -186,7 +187,7 @@ static void update_healthbar(Boss *boss) {
|
|||
boss_is_fleeing(boss) ||
|
||||
!boss->current ||
|
||||
boss->current->type == AT_Move ||
|
||||
global.dialog
|
||||
dialog_is_active(global.dialog)
|
||||
) {
|
||||
target_opacity = 0.0;
|
||||
}
|
||||
|
@ -310,7 +311,7 @@ static void update_hud(Boss *boss) {
|
|||
target_opacity = 0.0;
|
||||
}
|
||||
|
||||
if(!boss->current || boss->current->type == AT_Move || global.dialog) {
|
||||
if(!boss->current || boss->current->type == AT_Move || dialog_is_active(global.dialog)) {
|
||||
target_opacity = 0.0;
|
||||
}
|
||||
|
||||
|
@ -409,7 +410,7 @@ static void draw_spell_name(Boss *b, int time, bool healthbar_radial) {
|
|||
|
||||
r_draw_sprite(&(SpriteParams) {
|
||||
.sprite = "spell",
|
||||
.pos = { (VIEWPORT_W - 128) * (1 - pow(1 - f2, 5)) -256 * pow(1 - f2, 2), y_offset },
|
||||
.pos = { (VIEWPORT_W - 128), y_offset * (1 - pow(1 - f2, 5)) + VIEWPORT_H * pow(1 - f2, 2) },
|
||||
.color = color_mul_scalar(RGBA(1, 1, 1, f2 * 0.5), opacity * f2) ,
|
||||
.scale.both = 3 - 2 * (1 - pow(1 - f2, 3)),
|
||||
});
|
||||
|
@ -424,7 +425,7 @@ static void draw_spell_name(Boss *b, int time, bool healthbar_radial) {
|
|||
r_mat_translate(creal(x), cimag(x),0);
|
||||
float scale = f+1.*(1-f)*(1-f)*(1-f);
|
||||
r_mat_scale(scale,scale,1);
|
||||
r_mat_rotate_deg(360*f,1,1,0);
|
||||
r_mat_rotate(glm_ease_quad_out(f) * 2 * M_PI, 0.8, -0.2, 0);
|
||||
|
||||
float spellname_opacity_noplr = opacity_noplr * min(1, warn_progress/0.6);
|
||||
float spellname_opacity = spellname_opacity_noplr * b->hud.plrproximity_opacity;
|
||||
|
@ -501,6 +502,72 @@ static void draw_spell_name(Boss *b, int time, bool healthbar_radial) {
|
|||
}
|
||||
}
|
||||
|
||||
static void draw_spell_portrait(Boss *b, int time) {
|
||||
const int anim_time = 200;
|
||||
|
||||
if(time <= 0 || time >= anim_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!b->dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(b->current->draw_rule == NULL) {
|
||||
// No spell background? Assume not cut-in is intended, either.
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: Mostly copypasted from player.c::player_draw_overlay()
|
||||
// TODO: Maybe somehow generalize and make it more intelligible
|
||||
|
||||
float a = time / (float)anim_time;
|
||||
|
||||
r_state_push();
|
||||
r_shader("sprite_default");
|
||||
|
||||
float char_in = clamp(a * 1.5, 0, 1);
|
||||
float char_out = min(1, 2 - (2 * a));
|
||||
float char_opacity_in = 0.75 * min(1, a * 5);
|
||||
float char_opacity = char_opacity_in * char_out * char_out;
|
||||
float char_xofs = -20 * a;
|
||||
|
||||
Sprite *char_spr = b->dialog;
|
||||
|
||||
r_mat_push();
|
||||
r_mat_scale(-1, 1, 1);
|
||||
r_mat_translate(-VIEWPORT_W, 0, 0);
|
||||
r_cull(CULL_FRONT);
|
||||
|
||||
for(int i = 1; i <= 3; ++i) {
|
||||
float t = a * 200;
|
||||
float dur = 20;
|
||||
float start = 200 - dur * 5;
|
||||
float end = start + dur;
|
||||
float ofs = 0.2 * dur * (i - 1);
|
||||
float o = 1 - smoothstep(start + ofs, end + ofs, t);
|
||||
|
||||
r_draw_sprite(&(SpriteParams) {
|
||||
.sprite_ptr = char_spr,
|
||||
.pos = { char_spr->w * 0.5 + VIEWPORT_W * pow(1 - char_in, 4 - i * 0.3) - i + char_xofs, VIEWPORT_H - char_spr->h * 0.5 },
|
||||
.color = color_mul_scalar(color_add(RGBA(0.2, 0.2, 0.2, 0), RGBA(i==1, i==2, i==3, 0)), char_opacity_in * (1 - char_in * o) * o),
|
||||
.flip.x = true,
|
||||
.scale.both = 1.0 + 0.02 * (min(1, a * 1.2)) + i * 0.5 * pow(1 - o, 2),
|
||||
});
|
||||
}
|
||||
|
||||
r_draw_sprite(&(SpriteParams) {
|
||||
.sprite_ptr = char_spr,
|
||||
.pos = { char_spr->w * 0.5 + VIEWPORT_W * pow(1 - char_in, 4) + char_xofs, VIEWPORT_H - char_spr->h * 0.5 },
|
||||
.color = RGBA_MUL_ALPHA(1, 1, 1, char_opacity * min(1, char_in * 2) * (1 - min(1, (1 - char_out) * 5))),
|
||||
.flip.x = true,
|
||||
.scale.both = 1.0 + 0.1 * (1 - char_out),
|
||||
});
|
||||
|
||||
r_mat_pop();
|
||||
r_state_pop();
|
||||
}
|
||||
|
||||
static void BossGlow(Projectile *p, int t) {
|
||||
float s = 1.0+t/(double)p->timeout*0.5;
|
||||
float fade = 1 - (1.5 - s);
|
||||
|
@ -638,7 +705,17 @@ void draw_boss_overlay(Boss *boss) {
|
|||
draw_boss_text(ALIGN_LEFT, 10, 20 + 8 * !radial_style, boss->name, get_font("standard"), RGBA(o, o, o, o));
|
||||
|
||||
if(ATTACK_IS_SPELL(boss->current->type)) {
|
||||
draw_spell_name(boss, global.frames - boss->current->starttime, radial_style);
|
||||
int t_portrait, t_spell;
|
||||
t_portrait = t_spell = global.frames - boss->current->starttime;
|
||||
|
||||
if(boss->current->type == AT_ExtraSpell) {
|
||||
t_portrait += ATTACK_START_DELAY_EXTRA;
|
||||
} else {
|
||||
t_portrait += ATTACK_START_DELAY;
|
||||
}
|
||||
|
||||
draw_spell_portrait(boss, t_portrait);
|
||||
draw_spell_name(boss, t_spell, radial_style);
|
||||
}
|
||||
|
||||
float remaining = boss->hud.attack_timer;
|
||||
|
@ -919,7 +996,7 @@ void process_boss(Boss **pboss) {
|
|||
|
||||
spawn_particle_effects(boss);
|
||||
|
||||
if(!boss->current || global.dialog) {
|
||||
if(!boss->current || dialog_is_active(global.dialog)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1098,7 +1175,7 @@ void process_boss(Boss **pboss) {
|
|||
boss->current->starttime = global.frames;
|
||||
boss->current->rule(boss, EVENT_BIRTH);
|
||||
|
||||
if(global.dialog) {
|
||||
if(dialog_is_active(global.dialog)) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ static struct {
|
|||
|
||||
#define CREDITS_FADEOUT 180
|
||||
|
||||
#define ENTRY_TIME 345
|
||||
#define ENTRY_TIME 322
|
||||
|
||||
static void credits_add(char *data, int time);
|
||||
|
||||
|
@ -70,6 +70,13 @@ static void credits_fill(void) {
|
|||
"Original soundtrack"
|
||||
), ENTRY_TIME);
|
||||
|
||||
credits_add((
|
||||
"afensorm\n"
|
||||
"https://gensokyo.social/@afensorm\n"
|
||||
"https://pixiv.me/afens\n\n"
|
||||
"Character art"
|
||||
), ENTRY_TIME);
|
||||
|
||||
credits_add((
|
||||
"Lalasa\n"
|
||||
"Ola Kruzel\n"
|
||||
|
@ -127,7 +134,7 @@ static void credits_fill(void) {
|
|||
"http://www.libpng.org/\n\n"
|
||||
"Ogg Opus\n"
|
||||
"https://www.opus-codec.org/"
|
||||
), 350);
|
||||
), 327);
|
||||
|
||||
credits_add((
|
||||
"\n"
|
||||
|
@ -138,7 +145,7 @@ static void credits_fill(void) {
|
|||
"M cross environment\n"
|
||||
"http://mxe.cc/\n\n"
|
||||
"and many other projects"
|
||||
), 350);
|
||||
), 327);
|
||||
|
||||
credits_add((
|
||||
"Mochizuki Ado\n"
|
||||
|
|
253
src/dialog.c
|
@ -12,8 +12,7 @@
|
|||
#include "global.h"
|
||||
|
||||
Dialog *create_dialog(const char *left, const char *right) {
|
||||
Dialog *d = malloc(sizeof(Dialog));
|
||||
memset(d, 0, sizeof(Dialog));
|
||||
Dialog *d = calloc(1, sizeof(Dialog));
|
||||
|
||||
if(left) {
|
||||
d->images[Left] = get_sprite(left);
|
||||
|
@ -28,6 +27,20 @@ Dialog *create_dialog(const char *left, const char *right) {
|
|||
return d;
|
||||
}
|
||||
|
||||
static int message_index(Dialog *d, int offset) {
|
||||
int idx = d->pos + offset;
|
||||
|
||||
if(idx >= d->count) {
|
||||
idx = d->count - 1;
|
||||
}
|
||||
|
||||
while(idx >= 0 && d->messages[idx].side == BGM) {
|
||||
--idx;
|
||||
}
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
void dset_image(Dialog *d, Side side, const char *name) {
|
||||
d->images[side] = get_sprite(name);
|
||||
}
|
||||
|
@ -42,22 +55,65 @@ DialogMessage* dadd_msg(Dialog *d, Side side, const char *msg) {
|
|||
}
|
||||
|
||||
void delete_dialog(Dialog *d) {
|
||||
int i;
|
||||
for(i = 0; i < d->count; i++)
|
||||
for(int i = 0; i < d->count; i++) {
|
||||
free(d->messages[i].msg);
|
||||
}
|
||||
|
||||
free(d->messages);
|
||||
free(d);
|
||||
}
|
||||
|
||||
void draw_dialog(Dialog *dialog) {
|
||||
CullFaceMode cull_saved = r_cull_current();
|
||||
if(dialog == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
float o = dialog->opacity;
|
||||
|
||||
if(o == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
r_state_push();
|
||||
r_state_push();
|
||||
r_shader("sprite_default");
|
||||
|
||||
r_mat_push();
|
||||
r_mat_translate(VIEWPORT_W/2.0, VIEWPORT_H*3.0/4.0, 0);
|
||||
r_mat_translate(VIEWPORT_X, 0, 0);
|
||||
|
||||
int i;
|
||||
for(i = 0; i < 2; i++) {
|
||||
const double dialog_width = VIEWPORT_W * 1.2;
|
||||
|
||||
r_mat_push();
|
||||
r_mat_translate(dialog_width/2.0, 64, 0);
|
||||
|
||||
int cur_idx = message_index(dialog, 0);
|
||||
int pre_idx = message_index(dialog, -1);
|
||||
|
||||
assume(cur_idx >= 0);
|
||||
|
||||
int cur_side = dialog->messages[cur_idx].side;
|
||||
int pre_side = pre_idx >= 0 ? dialog->messages[pre_idx].side : 2;
|
||||
|
||||
Color clr = { 0 };
|
||||
|
||||
const float page_time = 10;
|
||||
float page_alpha = min(global.frames - (dialog->page_time), page_time) / page_time;
|
||||
|
||||
const float page_text_time = 60;
|
||||
float page_text_alpha = min(global.frames - dialog->page_time, page_text_time) / page_text_time;
|
||||
|
||||
int loop_start = 1;
|
||||
int loop_incr = 1;
|
||||
|
||||
if(cur_side == 0) {
|
||||
loop_start = 1;
|
||||
loop_incr = -1;
|
||||
} else {
|
||||
loop_start = 0;
|
||||
loop_incr = 1;
|
||||
}
|
||||
|
||||
for(int i = loop_start; i < 2 && i >= 0; i += loop_incr) {
|
||||
r_mat_push();
|
||||
|
||||
if(i == Left) {
|
||||
|
@ -67,72 +123,130 @@ void draw_dialog(Dialog *dialog) {
|
|||
r_cull(CULL_BACK);
|
||||
}
|
||||
|
||||
if(global.frames - dialog->birthtime < 30)
|
||||
r_mat_translate(120 - (global.frames - dialog->birthtime)*4, 0, 0);
|
||||
|
||||
int cur = dialog->messages[dialog->pos].side;
|
||||
int pre = 2;
|
||||
if(dialog->pos > 0)
|
||||
pre = dialog->messages[dialog->pos-1].side;
|
||||
|
||||
short dir = (1 - 2*(i == dialog->messages[dialog->pos].side));
|
||||
if(global.frames - dialog->page_time < 10 && ((i != pre && i == cur) || (i == pre && i != cur))) {
|
||||
int time = (global.frames - dialog->page_time) * dir;
|
||||
r_mat_translate(time, time, 0);
|
||||
float clr = min(1.0 - 0.07*time,1);
|
||||
r_color3(clr, clr, clr);
|
||||
} else {
|
||||
r_mat_translate(dir*10, dir*10, 0);
|
||||
r_color3(1 - (dir>0)*0.7, 1 - (dir>0)*0.7, 1 - (dir>0)*0.7);
|
||||
if(o < 1) {
|
||||
r_mat_translate(120 * (1 - o), 0, 0);
|
||||
}
|
||||
|
||||
r_mat_translate(VIEWPORT_W*7.0/18.0, 0, 0);
|
||||
if(dialog->images[i])
|
||||
draw_sprite_batched_p(0, 0, dialog->images[i]);
|
||||
|
||||
r_mat_pop();
|
||||
float dir = (1 - 2 * (i == cur_side));
|
||||
float ofs = 10 * dir;
|
||||
|
||||
r_color3(1,1,1);
|
||||
if(page_alpha < 10 && ((i != pre_side && i == cur_side) || (i == pre_side && i != cur_side))) {
|
||||
r_mat_translate(ofs * page_alpha, ofs * page_alpha, 0);
|
||||
float brightness = min(1.0 - 0.7 * page_alpha * dir, 1);
|
||||
clr.r = clr.g = clr.b = brightness;
|
||||
clr.a = 1;
|
||||
} else {
|
||||
r_mat_translate(ofs, ofs, 0);
|
||||
clr = *RGB(1 - (dir > 0) * 0.7, 1 - (dir > 0) * 0.7, 1 - (dir > 0) * 0.7);
|
||||
}
|
||||
|
||||
color_mul_scalar(&clr, o);
|
||||
r_color(&clr);
|
||||
|
||||
if(dialog->images[i]) {
|
||||
Sprite *s = dialog->images[i];
|
||||
r_mat_translate((dialog_width - s->w)/2 + 32, VIEWPORT_H - s->h/2, 0);
|
||||
draw_sprite_batched_p(0, 0, s);
|
||||
r_state_push();
|
||||
r_shader_standard_notex();
|
||||
r_mat_push();
|
||||
r_mat_scale(s->w, s->h, 1);
|
||||
// r_mat_translate(0.5, 0.5, 0);
|
||||
r_color4(0.2, 0.2, 0.2, 0.2);
|
||||
// r_draw_quad();
|
||||
r_mat_pop();
|
||||
r_state_pop();
|
||||
}
|
||||
|
||||
r_mat_pop();
|
||||
}
|
||||
|
||||
r_mat_pop();
|
||||
r_cull(cull_saved);
|
||||
r_state_pop();
|
||||
|
||||
o *= smooth(clamp((global.frames - dialog->birthtime - 10) / 30.0, 0, 1));
|
||||
|
||||
FloatRect dialog_bg_rect = {
|
||||
.extent = { VIEWPORT_W-40, 110 },
|
||||
.offset = { VIEWPORT_W/2, VIEWPORT_H-55 },
|
||||
};
|
||||
|
||||
r_mat_push();
|
||||
if(global.frames - dialog->birthtime < 25)
|
||||
r_mat_translate(0, 100-(global.frames-dialog->birthtime)*4, 0);
|
||||
|
||||
r_color4(0,0,0,0.8);
|
||||
|
||||
if(o < 1) {
|
||||
r_mat_translate(0, 100 * (1 - o), 0);
|
||||
}
|
||||
r_color4(0, 0, 0, 0.8 * o);
|
||||
r_mat_push();
|
||||
r_mat_translate(VIEWPORT_W/2, VIEWPORT_H-75, 0);
|
||||
r_mat_scale(VIEWPORT_W-40, 110, 1);
|
||||
|
||||
r_mat_translate(dialog_bg_rect.x, dialog_bg_rect.y, 0);
|
||||
r_mat_scale(dialog_bg_rect.w, dialog_bg_rect.h, 1);
|
||||
r_shader_standard_notex();
|
||||
r_draw_quad();
|
||||
r_shader_standard();
|
||||
|
||||
r_mat_pop();
|
||||
r_color4(1,1,1,1);
|
||||
|
||||
if(dialog->messages[dialog->pos].side == Right)
|
||||
r_color3(0.6,0.6,1);
|
||||
Font *font = get_font("standard");
|
||||
|
||||
r_shader("text_default");
|
||||
text_draw_wrapped(dialog->messages[dialog->pos].msg, VIEWPORT_W * 0.85, &(TextParams) {
|
||||
.pos = { VIEWPORT_W/2, VIEWPORT_H-110 },
|
||||
.align = ALIGN_CENTER
|
||||
r_mat_mode(MM_TEXTURE);
|
||||
r_mat_push();
|
||||
// r_mat_scale(2, 0.2, 0);
|
||||
// r_mat_translate(0, -global.frames/page_text_time, 0);
|
||||
r_mat_mode(MM_MODELVIEW);
|
||||
|
||||
dialog_bg_rect.w = VIEWPORT_W * 0.86;
|
||||
dialog_bg_rect.x -= dialog_bg_rect.w * 0.5;
|
||||
dialog_bg_rect.y -= dialog_bg_rect.h * 0.5;
|
||||
// dialog_bg_rect.h = dialog_bg_rect.w;
|
||||
|
||||
if(pre_idx >= 0 && page_text_alpha < 1) {
|
||||
if(pre_side == Right) {
|
||||
clr = *RGB(0.6, 0.6, 1.0);
|
||||
} else {
|
||||
clr = *RGB(1.0, 1.0, 1.0);
|
||||
}
|
||||
|
||||
color_mul_scalar(&clr, o);
|
||||
|
||||
text_draw_wrapped(dialog->messages[pre_idx].msg, VIEWPORT_W * 0.86, &(TextParams) {
|
||||
.shader = "text_dialog",
|
||||
.aux_textures = { get_tex("cell_noise") },
|
||||
.shader_params = &(ShaderCustomParams) {{ o * (1.0 - (0.2 + 0.8 * page_text_alpha)), 1 }},
|
||||
.color = &clr,
|
||||
.pos = { VIEWPORT_W/2, VIEWPORT_H-110 + font_get_lineskip(font) },
|
||||
.align = ALIGN_CENTER,
|
||||
.font_ptr = font,
|
||||
.overlay_projection = &dialog_bg_rect,
|
||||
});
|
||||
}
|
||||
|
||||
if(cur_side == Right) {
|
||||
clr = *RGB(0.6, 0.6, 1.0);
|
||||
} else {
|
||||
clr = *RGB(1.0, 1.0, 1.0);
|
||||
}
|
||||
|
||||
color_mul_scalar(&clr, o);
|
||||
|
||||
text_draw_wrapped(dialog->messages[cur_idx].msg, VIEWPORT_W * 0.86, &(TextParams) {
|
||||
.shader = "text_dialog",
|
||||
.aux_textures = { get_tex("cell_noise") },
|
||||
.shader_params = &(ShaderCustomParams) {{ o * page_text_alpha, 0 }},
|
||||
.color = &clr,
|
||||
.pos = { VIEWPORT_W/2, VIEWPORT_H-110 + font_get_lineskip(font) },
|
||||
.align = ALIGN_CENTER,
|
||||
.font_ptr = font,
|
||||
.overlay_projection = &dialog_bg_rect,
|
||||
});
|
||||
|
||||
if(dialog->messages[dialog->pos].side == Right)
|
||||
r_color3(1,1,1);
|
||||
r_mat_mode(MM_TEXTURE);
|
||||
r_mat_pop();
|
||||
r_mat_mode(MM_MODELVIEW);
|
||||
|
||||
r_mat_pop();
|
||||
r_shader("sprite_default");
|
||||
r_mat_pop();
|
||||
r_state_pop();
|
||||
}
|
||||
|
||||
bool page_dialog(Dialog **d) {
|
||||
if(!*d) {
|
||||
if(!*d || (*d)->pos >= (*d)->count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -146,9 +260,6 @@ bool page_dialog(Dialog **d) {
|
|||
(*d)->page_time = global.frames;
|
||||
|
||||
if((*d)->pos >= (*d)->count) {
|
||||
delete_dialog(*d);
|
||||
*d = NULL;
|
||||
|
||||
// XXX: maybe this can be handled elsewhere?
|
||||
if(!global.boss)
|
||||
global.timer++;
|
||||
|
@ -165,12 +276,30 @@ void process_dialog(Dialog **d) {
|
|||
return;
|
||||
}
|
||||
|
||||
int to = (*d)->messages[(*d)->pos].timeout;
|
||||
if(dialog_is_active(*d)) {
|
||||
int to = (*d)->messages[(*d)->pos].timeout;
|
||||
|
||||
if(
|
||||
(to && to >= global.frames) ||
|
||||
((global.plr.inputflags & INFLAG_SKIP) && global.frames - (*d)->page_time > 3)
|
||||
) {
|
||||
page_dialog(d);
|
||||
if(
|
||||
(to && to >= global.frames) ||
|
||||
((global.plr.inputflags & INFLAG_SKIP) && global.frames - (*d)->page_time > 3)
|
||||
) {
|
||||
page_dialog(d);
|
||||
}
|
||||
}
|
||||
|
||||
// important to check this again; the page_dialog call may have ended the dialog
|
||||
|
||||
if(dialog_is_active(*d)) {
|
||||
fapproach_asymptotic_p(&(*d)->opacity, 1, 0.05, 1e-3);
|
||||
} else {
|
||||
fapproach_asymptotic_p(&(*d)->opacity, 0, 0.1, 1e-3);
|
||||
if((*d)->opacity == 0) {
|
||||
delete_dialog(*d);
|
||||
*d = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool dialog_is_active(Dialog *d) {
|
||||
return d && (d->pos < d->count);
|
||||
}
|
||||
|
|
10
src/dialog.h
|
@ -14,7 +14,6 @@
|
|||
#include "resource/sprite.h"
|
||||
|
||||
struct DialogMessage;
|
||||
struct DialogSpeaker;
|
||||
|
||||
typedef enum {
|
||||
Right,
|
||||
|
@ -34,10 +33,10 @@ typedef struct Dialog {
|
|||
|
||||
int count;
|
||||
int pos;
|
||||
|
||||
int page_time;
|
||||
|
||||
int birthtime;
|
||||
|
||||
float opacity;
|
||||
} Dialog;
|
||||
|
||||
Dialog *create_dialog(const char *left, const char *right)
|
||||
|
@ -52,12 +51,13 @@ DialogMessage* dadd_msg(Dialog *d, Side side, const char *msg)
|
|||
void delete_dialog(Dialog *d)
|
||||
attr_nonnull(1);
|
||||
|
||||
void draw_dialog(Dialog *dialog)
|
||||
attr_nonnull(1);
|
||||
void draw_dialog(Dialog *dialog);
|
||||
|
||||
bool page_dialog(Dialog **d) attr_nonnull(1);
|
||||
void process_dialog(Dialog **d) attr_nonnull(1);
|
||||
|
||||
bool dialog_is_active(Dialog *d);
|
||||
|
||||
// FIXME: might not be the best place for this
|
||||
typedef struct PlayerDialogProcs {
|
||||
void (*stage1_pre_boss)(Dialog *d);
|
||||
|
|
|
@ -68,7 +68,7 @@ MenuData* create_char_menu(void) {
|
|||
} |