/* * 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 "player.h" #include "audio/audio.h" #include "entity.h" #include "gamepad.h" #include "global.h" #include "plrmodes.h" #include "projectile.h" #include "replay/stage.h" #include "replay/struct.h" #include "stage.h" #include "stagedraw.h" #include "stagetext.h" #include "stats.h" #include "util/glm.h" #include "util/graphics.h" DEFINE_ENTITY_TYPE(PlayerIndicators, { Player *plr; struct { Sprite *focus; } sprites; float focus_alpha; int focus_time; }); void player_init(Player *plr) { memset(plr, 0, sizeof(Player)); plr->pos = PLR_SPAWN_POS; plr->lives = PLR_START_LIVES; plr->bombs = PLR_START_BOMBS; plr->point_item_value = PLR_START_PIV; plr->power_stored = 100; plr->deathtime = -1; plr->continuetime = -1; plr->bomb_triggertime = -1; plr->bomb_endtime = 0; plr->mode = plrmode_find(0, 0); } void player_stage_pre_init(Player *plr) { plr->recoverytime = 0; plr->respawntime = 0; plr->bomb_triggertime = -1; plr->bomb_endtime = 0; plr->deathtime = -1; plr->axis_lr = 0; plr->axis_ud = 0; } double player_property(Player *plr, PlrProperty prop) { return plr->mode->procs.property(plr, prop); } static void ent_draw_player(EntityInterface *ent); static DamageResult ent_damage_player(EntityInterface *ent, const DamageInfo *dmg); DECLARE_TASK(player_logic, { BoxedPlayer plr; }); DECLARE_TASK(player_indicators, { BoxedPlayer plr; }); void player_stage_post_init(Player *plr) { assert(plr->mode != NULL); // ensure the essential callbacks are there. other code tests only for the optional ones assert(plr->mode->procs.property != NULL); assert(plr->mode->character != NULL); assert(plr->mode->dialog != NULL); plrchar_render_bomb_portrait(plr->mode->character, &plr->bomb_portrait); aniplayer_create(&plr->ani, plrchar_player_anim(plr->mode->character), "main"); plr->ent.draw_layer = LAYER_PLAYER; plr->ent.draw_func = ent_draw_player; plr->ent.damage_func = ent_damage_player; ent_register(&plr->ent, ENT_TYPE_ID(Player)); COEVENT_INIT_ARRAY(plr->events); INVOKE_TASK_DELAYED(1, player_logic, ENT_BOX(plr)); INVOKE_TASK_DELAYED(1, player_indicators, ENT_BOX(plr)); if(plr->mode->procs.init != NULL) { plr->mode->procs.init(plr); } plr->extralife_threshold = player_next_extralife_threshold(plr->extralives_given); while(plr->points >= plr->extralife_threshold) { plr->extralife_threshold = player_next_extralife_threshold(++plr->extralives_given); } } void player_free(Player *plr) { COEVENT_CANCEL_ARRAY(plr->events); r_texture_destroy(plr->bomb_portrait.tex); aniplayer_free(&plr->ani); ent_unregister(&plr->ent); } static void player_full_power(Player *plr) { stage_clear_hazards(CLEAR_HAZARDS_ALL); stagetext_add("Full Power!", VIEWPORT_W * 0.5 + VIEWPORT_H * 0.33 * I, ALIGN_CENTER, res_font("big"), RGB(1, 1, 1), 0, 60, 20, 20); } static int player_track_effective_power_change(Player *plr) { int old_effective = plr->_prev_effective_power; int new_effective = player_get_effective_power(plr); if(old_effective != new_effective) { plr->_prev_effective_power = new_effective; coevent_signal(&plr->events.effective_power_changed); } return new_effective; } bool player_set_power(Player *plr, short npow) { int old_stored = plr->power_stored; int new_stored = clamp(npow, 0, PLR_MAX_POWER_STORED); plr->power_stored = new_stored; if(old_stored / 100 < new_stored / 100) { play_sfx("powerup"); } bool change = old_stored != new_stored; if(change) { if(new_stored >= PLR_MAX_POWER_EFFECTIVE && old_stored < PLR_MAX_POWER_EFFECTIVE) { player_full_power(plr); } coevent_signal(&plr->events.stored_power_changed); } player_track_effective_power_change(plr); return change; } bool player_add_power(Player *plr, short pdelta) { return player_set_power(plr, plr->power_stored + pdelta); } int player_get_effective_power(Player *plr) { int p; if(player_is_powersurge_active(plr)) { p = plr->powersurge.player_power; } else { p = plr->power_stored; } return clamp(p, 0, PLR_MAX_POWER_EFFECTIVE); } void player_move(Player *plr, cmplx delta) { delta *= player_property(plr, PLR_PROP_SPEED); plr->uncapped_velocity = delta; cmplx lastpos = plr->pos; cmplx ofs = CMPLX(PLR_MIN_BORDER_DIST, PLR_MIN_BORDER_DIST); cmplx vp = CMPLX(VIEWPORT_W, VIEWPORT_H); plr->pos = cwclamp(lastpos + delta, ofs, vp - ofs); plr->velocity = plr->pos - lastpos; } void player_draw_overlay(Player *plr) { float a = 1 - plr->bomb_cutin_alpha; if(a <= 0 || a >= 1) { return; } r_state_push(); r_shader("sprite_default"); float char_in = clamp(a * 1.5f, 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 = &plr->bomb_portrait; 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 * powf(1 - char_in, 4 - i * 0.3f) - i + char_xofs, VIEWPORT_H - char_spr->h * 0.5f }, .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.0f + 0.02f * (min(1, a * 1.2f)) + i * 0.5 * powf(1 - o, 2), }); } r_draw_sprite(&(SpriteParams) { .sprite_ptr = char_spr, .pos = { char_spr->w * 0.5f + VIEWPORT_W * powf(1 - char_in, 4) + char_xofs, VIEWPORT_H - char_spr->h * 0.5f }, .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), }); float spell_in = min(1, a * 3.0); float spell_out = min(1, 3 - (3 * a)); float spell_opacity = min(1, a * 5) * spell_out * spell_out; float spell_x = 128 * (1 - powf(1 - spell_in, 5)) + (VIEWPORT_W + 256) * powf(1 - spell_in, 3); float spell_y = VIEWPORT_H - 128 * sqrtf(a); Sprite *spell_spr = res_sprite("spell"); r_draw_sprite(&(SpriteParams) { .sprite_ptr = spell_spr, .pos = { spell_x, spell_y }, .color = color_mul_scalar(RGBA(1, 1, 1, spell_in * 0.5), spell_opacity), .scale.both = 3 - 2 * (1 - pow(1 - spell_in, 3)) + 2 * (1 - spell_out), }); Font *font = res_font("standard"); r_mat_mv_push(); r_mat_mv_translate(spell_x - spell_spr->w * 0.5 + 10, spell_y + 5 - font_get_metrics(font)->descent, 0); TextParams tp = { // .pos = { spell_x - spell_spr->w * 0.5 + 10, spell_y + 5 - font_get_metrics(font)->descent }, .shader = "text_hud", .font_ptr = font, .color = color_mul_scalar(RGBA(1, 1, 1, spell_in), spell_opacity), }; r_mat_mv_push(); r_mat_mv_scale(2 - 1 * spell_opacity, 2 - 1 * spell_opacity, 1); text_draw(plr->mode->spellcard_name, &tp); r_mat_mv_pop(); r_mat_mv_pop(); r_state_pop(); } static void ent_draw_player(EntityInterface *ent) { Player *plr = ENT_CAST(ent, Player); if(plr->deathtime >= global.frames) { return; } ShaderCustomParams shader_params = { 1.0f }; ShaderProgram *shader = res_shader("sprite_particle"); if(plr->focus_circle_alpha) { r_draw_sprite(&(SpriteParams) { .sprite_ptr = res_sprite("fairy_circle"), .shader_ptr = shader, .shader_params = &shader_params, .rotation.angle = DEG2RAD * global.frames * 10, .color = RGBA_MUL_ALPHA(1, 1, 1, 0.2 * plr->focus_circle_alpha), .pos = { re(plr->pos), im(plr->pos) }, }); } Color c; if(!player_is_vulnerable(plr)) { float f = 0.3*sin(0.1*global.frames); c = *RGBA_MUL_ALPHA(1.0+f, 1.0, 1.0-f, 0.7+f); } else { c = *RGBA_MUL_ALPHA(1.0, 1.0, 1.0, 1.0); } r_draw_sprite(&(SpriteParams) { .sprite_ptr = aniplayer_get_frame(&plr->ani), .shader_ptr = shader, .shader_params = &shader_params, .pos.as_cmplx = plr->pos, .color = &c, }); } static void player_draw_indicators(EntityInterface *ent) { PlayerIndicators *indicators = ENT_CAST(ent, PlayerIndicators); Player *plr = indicators->plr; float focus_opacity = indicators->focus_alpha; int t = global.frames - indicators->focus_time; cmplxf pos = plr->pos; if(focus_opacity > 0) { float trans_frames = 12; float trans_factor = 1.0f - min(trans_frames, t) / trans_frames; float rot_speed = DEG2RAD * global.frames * (1.0f + 3.0f * trans_factor); float scale = 1.0f + trans_factor; SpriteParams sp = { .sprite_ptr = indicators->sprites.focus, .rotation.angle = rot_speed, .color = RGBA_MUL_ALPHA(1, 1, 1, focus_opacity), .pos.as_cmplx = pos, .scale.both = scale, }; r_draw_sprite(&sp); sp.rotation.angle *= -1; r_draw_sprite(&sp); } float ps_opacity = 1.0; float ps_fill_factor = 1.0; if(player_is_powersurge_active(plr)) { ps_opacity *= clamp((global.frames - plr->powersurge.time.activated) / 30.0, 0, 1); } else if(plr->powersurge.time.expired == 0) { ps_opacity = 0; } else { ps_opacity *= (1 - clamp((global.frames - plr->powersurge.time.expired) / 40.0, 0, 1)); ps_fill_factor = pow(ps_opacity, 2); } if(ps_opacity > 0) { r_state_push(); r_mat_mv_push(); r_mat_mv_translate(re(pos), im(pos), 0); r_mat_mv_scale(140, 140, 0); r_shader("healthbar_radial"); r_uniform_vec4_rgba("borderColor", RGBA(0.5, 0.5, 0.5, 0.5)); r_uniform_vec4_rgba("glowColor", RGBA(0.5, 0.5, 0.5, 0.75)); r_uniform_vec4_rgba("fillColor", RGBA(1.5, 0.5, 0.0, 0.75)); r_uniform_vec4_rgba("altFillColor", RGBA(0.0, 0.5, 1.5, 0.75)); r_uniform_vec4_rgba("coreFillColor", RGBA(0.8, 0.8, 0.8, 0.25)); r_uniform_vec2("fill", plr->powersurge.positive * ps_fill_factor, plr->powersurge.negative * ps_fill_factor); r_uniform_float("opacity", ps_opacity); r_draw_quad(); r_mat_mv_pop(); r_state_pop(); char buf[64]; format_huge_num(0, plr->powersurge.bonus.baseline, sizeof(buf), buf); Font *fnt = res_font("monotiny"); float x = re(pos); float y = im(pos) + 80; float text_opacity = ps_opacity * 0.75; x += text_draw(buf, &(TextParams) { .shader = "text_hud", .font_ptr = fnt, .pos = { x, y }, .color = RGBA_MUL_ALPHA(1.0, 1.0, 1.0, text_opacity), .align = ALIGN_CENTER, }); snprintf(buf, sizeof(buf), " +%u", plr->powersurge.bonus.gain_rate); text_draw(buf, &(TextParams) { .shader = "text_hud", .font_ptr = fnt, .pos = { x, y }, .color = RGBA_MUL_ALPHA(0.3, 0.6, 1.0, text_opacity), .align = ALIGN_LEFT, }); r_shader("sprite_filled_circle"); r_uniform_vec4("color_inner", 0, 0, 0, 0); r_uniform_vec4("color_outer", 0, 0, 0.1 * ps_opacity, 0.1 * ps_opacity); } } DEFINE_TASK(player_indicators) { PlayerIndicators *indicators = TASK_HOST_ENT(PlayerIndicators); indicators->plr = TASK_BIND(ARGS.plr); indicators->ent.draw_layer = LAYER_PLAYER_FOCUS; indicators->ent.draw_func = player_draw_indicators; indicators->sprites.focus = res_sprite("focus"); Player *plr = indicators->plr; bool was_focused = false; bool is_focused = false; for(;;) { is_focused = (plr->inputflags & INFLAG_FOCUS) && player_is_alive(plr); if(is_focused && !was_focused) { indicators->focus_time = global.frames; indicators->focus_alpha = min(indicators->focus_alpha, 0.1f); } was_focused = is_focused; fapproach_p(&indicators->focus_alpha, is_focused, 1.0f/30.0f); YIELD; } } static void player_fail_spell(Player *plr) { Boss *boss = global.boss; if(!boss || global.stage->type == STAGE_SPELL) { return; } Attack *atk = boss->current; if(!atk || !attack_is_active(atk) || attack_was_failed(atk)) { return; } global.boss->current->failtime = global.frames; if(global.boss->current->type == AT_ExtraSpell) { boss_finish_current_attack(global.boss); } } bool player_should_shoot(Player *plr) { return (plr->inputflags & INFLAG_SHOT) && !dialog_is_active(global.dialog) && player_is_alive(&global.plr); } void player_placeholder_bomb_logic(Player *plr) { if(!player_is_bomb_active(plr)) { return; } DamageInfo dmg; dmg.amount = 100; dmg.type = DMG_PLAYER_BOMB; for(Enemy *en = global.enemies.first; en; en = en->next) { ent_damage(&en->ent, &dmg); } if(global.boss) { ent_damage(&global.boss->ent, &dmg); } stage_clear_hazards(CLEAR_HAZARDS_ALL); } static void player_powersurge_expired(Player *plr); static void _powersurge_trail_draw(Projectile *p, float t, float cmul) { float nt = t / p->timeout; float s = 1 + (2 + 0.5 * psin((t+global.frames*1.23)/5.0)) * nt * nt; r_draw_sprite(&(SpriteParams) { .sprite_ptr = p->sprite, .scale.both = s, .pos = { re(p->pos), im(p->pos) }, .color = color_mul_scalar(RGBA(0.8, 0.1 + 0.2 * psin((t+global.frames)/5.0), 0.1, 0.0), 0.5 * (1 - nt) * cmul), .shader_params = &(ShaderCustomParams){{ -2 * nt * nt }}, .shader_ptr = p->shader, }); } static void powersurge_trail_draw(Projectile *p, int t, ProjDrawRuleArgs args) { if(t > 0) { _powersurge_trail_draw(p, t - 0.5, 0.25); _powersurge_trail_draw(p, t, 0.25); } else { _powersurge_trail_draw(p, t, 0.5); } } TASK(powersurge_player_particles, { BoxedPlayer plr; }) { Player *plr = TASK_BIND(ARGS.plr); DECLARE_ENT_ARRAY(Projectile, trails, 32); DECLARE_ENT_ARRAY(Projectile, fields, 4); ShaderProgram *trail_shader = res_shader("sprite_silhouette"); Sprite *field_sprite = res_sprite("part/powersurge_field"); for(int t = 0; player_is_powersurge_active(plr); ++t, YIELD) { ENT_ARRAY_COMPACT(&trails); ENT_ARRAY_ADD(&trails, PARTICLE( .sprite_ptr = aniplayer_get_frame(&plr->ani), .shader_ptr = trail_shader, .pos = plr->pos, .color = RGBA(1, 1, 1, 0.5), .draw_rule = powersurge_trail_draw, .move = move_towards(0, plr->pos, 0), .timeout = 15, .layer = LAYER_PARTICLE_HIGH, .flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE, )); if(t % 6 == 0 && plr->powersurge.bonus.discharge_range > 0) { real scale = 2 * plr->powersurge.bonus.discharge_range / field_sprite->w; real angle = rng_angle(); Color *color = color_mul_scalar(rng_bool() ? RGBA(1.5, 0.5, 0.0, 0.1) : RGBA(0.0, 0.5, 1.5, 0.1), 0.25); ENT_ARRAY_COMPACT(&fields); ENT_ARRAY_ADD(&fields, PARTICLE( .sprite_ptr = field_sprite, .pos = plr->pos, .color = color, .draw_rule = pdraw_timeout_fade(1, 0), .timeout = 14, .angle = angle, .layer = LAYER_PLAYER - 1, .flags = PFLAG_NOREFLECT | PFLAG_NOMOVE | PFLAG_REQUIREDPARTICLE, .scale = scale, )); ENT_ARRAY_ADD(&fields, PARTICLE( .sprite_ptr = field_sprite, .pos = plr->pos, .color = RGBA(0.5, 0.5, 0.5, 0), .draw_rule = pdraw_timeout_fade(1, 0), .timeout = 3, .angle = angle, .layer = LAYER_PLAYER - 1, .flags = PFLAG_NOREFLECT | PFLAG_NOMOVE, .scale = scale, )); } ENT_ARRAY_FOREACH(&trails, Projectile *p, { p->move.attraction_point = plr->pos; p->move.attraction = 1 - (global.frames - p->birthtime) / p->timeout; }); ENT_ARRAY_FOREACH(&fields, Projectile *p, { p->pos = plr->pos; }); } } void player_powersurge_calc_bonus(Player *plr, PowerSurgeBonus *b) { b->gain_rate = round(1000 * plr->powersurge.negative * plr->powersurge.negative); b->baseline = plr->powersurge.total_charge + plr->powersurge.damage_done * 0.4; b->score = b->baseline; b->discharge_power = sqrtf(0.2 * b->baseline + 1024 * log1pf(b->baseline)) * smoothstep(0, 1, 0.0001 * b->baseline); b->discharge_range = 1.2 * b->discharge_power; b->discharge_damage = 10 * pow(b->discharge_power, 1.1); } static void player_powersurge_logic(Player *plr) { if(dialog_is_active(global.dialog)) { return; } plr->powersurge.positive = max(0, plr->powersurge.positive - lerp(PLR_POWERSURGE_POSITIVE_DRAIN_MIN, PLR_POWERSURGE_POSITIVE_DRAIN_MAX, plr->powersurge.positive)); plr->powersurge.negative = max(0, plr->powersurge.negative - lerp(PLR_POWERSURGE_NEGATIVE_DRAIN_MIN, PLR_POWERSURGE_NEGATIVE_DRAIN_MAX, plr->powersurge.negative)); if(stage_is_cleared()) { player_cancel_powersurge(plr); return; } if(plr->powersurge.positive <= plr->powersurge.negative) { player_powersurge_expired(plr); return; } player_powersurge_calc_bonus(plr, &plr->powersurge.bonus); plr->powersurge.total_charge += plr->powersurge.bonus.gain_rate; } DEFINE_TASK(player_logic) { Player *plr = TASK_BIND(ARGS.plr); uint prev_inputflags = 0; plr->_prev_effective_power = player_get_effective_power(plr); for(;;) { YIELD; if(prev_inputflags != plr->inputflags) { coevent_signal(&plr->events.inputflags_changed); prev_inputflags = plr->inputflags; } fapproach_p(&plr->bomb_cutin_alpha, 0, 1/200.0); if( plr->respawntime - PLR_RESPAWN_TIME/2 == global.frames && plr->lives < 0 && global.replay.input.replay == NULL ) { stage_gameover(); } if(plr->continuetime == global.frames) { plr->lives = PLR_START_LIVES; plr->bombs = PLR_START_BOMBS; plr->point_item_value = PLR_START_PIV; plr->life_fragments = 0; plr->bomb_fragments = 0; stats_track_continue_used(&plr->stats); player_set_power(plr, 0); stage_clear_hazards(CLEAR_HAZARDS_ALL); spawn_items(plr->deathpos, ITEM_POWER, (int)ceil(PLR_MAX_POWER_EFFECTIVE/(real)POWER_VALUE)); } aniplayer_update(&plr->ani); if(plr->lives < 0) { continue; } if(plr->respawntime > global.frames) { double x = PLR_SPAWN_POS_X; double y = lerp(PLR_SPAWN_POS_Y, VIEWPORT_H + 64, smoothstep(0.0, 1.0, (plr->respawntime - global.frames) / (double)PLR_RESPAWN_TIME)); plr->pos = CMPLX(x, y); stage_clear_hazards(CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW); continue; } if(player_is_powersurge_active(plr)) { player_powersurge_logic(plr); } fapproach_p(&plr->focus_circle_alpha, !!(plr->inputflags & INFLAG_FOCUS), 1.0f/15.0f); if(player_should_shoot(plr)) { coevent_signal(&plr->events.shoot); } if(global.frames == plr->deathtime) { player_realdeath(plr); } else if(plr->deathtime > global.frames) { stage_clear_hazards(CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW); } player_track_effective_power_change(plr); } } static bool player_can_bomb(Player *plr) { return ( !player_is_bomb_active(plr) && ( plr->bombs > 0 || plr->iddqd ) && global.frames >= plr->respawntime ); } static bool player_bomb(Player *plr) { if(global.boss && global.boss->current && global.boss->current->type == AT_ExtraSpell) return false; int bomb_time = floor(player_property(plr, PLR_PROP_BOMB_TIME)); if(bomb_time <= 0) { return false; } if(player_can_bomb(plr)) { stats_track_bomb_used(&plr->stats); player_fail_spell(plr); // player_cancel_powersurge(plr); // stage_clear_hazards(CLEAR_HAZARDS_ALL); plr->bombs--; if(plr->deathtime >= global.frames) { // death bomb - unkill the player! plr->deathtime = -1; if(plr->bombs) { plr->bombs--; } } if(plr->bombs < 0) { plr->bombs = 0; } plr->bomb_triggertime = global.frames; plr->bomb_endtime = plr->bomb_triggertime + bomb_time; plr->bomb_cutin_alpha = 1; assert(player_is_alive(plr)); collect_all_items(1); coevent_signal(&plr->events.bomb_used); return true; } return false; } static bool player_powersurge(Player *plr) { if( !player_is_alive(plr) || player_is_bomb_active(plr) || player_is_powersurge_active(plr) || plr->power_stored < PLR_POWERSURGE_POWERCOST ) { return false; } plr->powersurge.positive = 1.0; plr->powersurge.negative = 0.0; plr->powersurge.time.activated = global.frames; plr->powersurge.total_charge = 0; plr->powersurge.damage_accum = 0; plr->powersurge.player_power = plr->power_stored; player_powersurge_calc_bonus(plr, &plr->powersurge.bonus); player_add_power(plr, -PLR_POWERSURGE_POWERCOST); play_sfx("powersurge_start"); collect_all_items(1); stagetext_add("Power Surge!", plr->pos - 64 * I, ALIGN_CENTER, res_font("standard"), RGBA(0.75, 0.75, 0.75, 0.75), 0, 45, 10, 20); INVOKE_TASK(powersurge_player_particles, ENT_BOX(plr)); return true; } bool player_is_recovering(Player *plr) { return global.frames < plr->recoverytime; } bool player_is_powersurge_active(Player *plr) { return plr->powersurge.positive > plr->powersurge.negative; } bool player_is_bomb_active(Player *plr) { return global.frames < plr->bomb_endtime; } bool player_is_vulnerable(Player *plr) { return player_is_alive(plr) && !player_is_recovering(plr) && !player_is_bomb_active(plr) && !plr->iddqd; } bool player_is_alive(Player *plr) { return plr->deathtime < global.frames && global.frames >= plr->respawntime; } static void powersurge_distortion_draw(Projectile *p, int t, ProjDrawRuleArgs args) { if(config_get_int(CONFIG_POSTPROCESS) < 1) { return; } double radius = args[0].as_float[0] * pow(1 - t / p->timeout, 8) * (2 * t / 10.0); Framebuffer *fb_aux = stage_get_fbpair(FBPAIR_FG_AUX)->front; Framebuffer *fb_main = r_framebuffer_current(); r_framebuffer(fb_aux); r_shader("circle_distort"); r_uniform_vec3("distortOriginRadius", re(p->pos), VIEWPORT_H - im(p->pos), radius); r_uniform_vec2("viewport", VIEWPORT_W, VIEWPORT_H); r_blend(BLEND_NONE); draw_framebuffer_tex(fb_main, VIEWPORT_W, VIEWPORT_H); r_framebuffer(fb_main); r_shader_standard(); r_color4(1.0, 0.9, 0.8, 1.0); r_blend(BLEND_PREMUL_ALPHA); stage_draw_begin_noshake(); draw_framebuffer_tex(fb_aux, VIEWPORT_W, VIEWPORT_H); stage_draw_end_noshake(); } TASK(powersurge_discharge_clearhazards, { cmplx pos; real range; int timeout; }) { for(int t = 0; t < ARGS.timeout; ++t, YIELD) { stage_clear_hazards_at(ARGS.pos, ARGS.range, CLEAR_HAZARDS_ALL | CLEAR_HAZARDS_NOW | CLEAR_HAZARDS_SPAWN_VOLTAGE); } } static void player_powersurge_expired(Player *plr) { plr->powersurge.time.expired = global.frames; PowerSurgeBonus bonus; player_powersurge_calc_bonus(plr, &bonus); Sprite *blast = res_sprite("part/blast_huge_halo"); float scale = 2 * bonus.discharge_range / blast->w; play_sfx("powersurge_end"); PARTICLE( .size = 1+I, .pos = plr->pos, .timeout = 60, .draw_rule = { powersurge_distortion_draw, .args[0].as_float = { bonus.discharge_range }, }, .layer = LAYER_PLAYER, .flags = PFLAG_REQUIREDPARTICLE | PFLAG_NOREFLECT, ); PARTICLE( .sprite_ptr = blast, .pos = plr->pos, .color = RGBA(0.6, 1.0, 4.4, 0.0), .draw_rule = pdraw_timeout_scalefade(2, 0, 1, 0), .timeout = 20, .angle = rng_angle(), .scale = scale, .flags = PFLAG_REQUIREDPARTICLE | PFLAG_NOREFLECT, ); player_add_points(&global.plr, bonus.score, plr->pos); ent_area_damage(plr->pos, bonus.discharge_range, &(DamageInfo) { bonus.discharge_damage, DMG_PLAYER_DISCHARGE }, NULL, NULL); INVOKE_TASK(powersurge_discharge_clearhazards, plr->pos, bonus.discharge_range, 10); log_debug( "Power Surge expired at %i (duration: %i); baseline = %u; score = %u; discharge = %g, dmg = %g, range = %g", plr->powersurge.time.expired, plr->powersurge.time.expired - plr->powersurge.time.activated, bonus.baseline, bonus.score, bonus.discharge_power, bonus.discharge_damage, bonus.discharge_range ); plr->powersurge.damage_done = 0; } void player_cancel_powersurge(Player *plr) { if(player_is_powersurge_active(plr)) { plr->powersurge.positive = plr->powersurge.negative; player_powersurge_expired(plr); } } void player_extend_powersurge(Player *plr, float pos, float neg) { if(player_is_powersurge_active(plr)) { plr->powersurge.positive = clamp(plr->powersurge.positive + pos, 0, 1); plr->powersurge.negative = clamp(plr->powersurge.negative + neg, 0, 1); if(plr->powersurge.positive <= plr->powersurge.negative) { player_powersurge_expired(plr); } } } double player_get_bomb_progress(Player *plr) { if(!player_is_bomb_active(plr)) { return 1; } int begin = plr->bomb_triggertime; int end = plr->bomb_endtime; assert(begin <= end); real bomb_time = end - begin; return (bomb_time - (end - global.frames)) / bomb_time; } void player_realdeath(Player *plr) { int deathtime = plr->deathtime; assert(global.frames == deathtime); plr->respawntime = deathtime + PLR_RESPAWN_TIME; plr->recoverytime = deathtime + PLR_RECOVERY_TIME; plr->inputflags &= ~INFLAGS_MOVE; plr->deathpos = plr->pos; plr->pos = VIEWPORT_W/2 + VIEWPORT_H*I+30.0*I; stage_clear_hazards(CLEAR_HAZARDS_ALL); player_fail_spell(plr); if(global.stage->type != STAGE_SPELL && global.boss && global.boss->current && global.boss->current->type == AT_ExtraSpell) { // deaths in extra spells "don't count" return; } int total_power = plr->power_stored; int drop = max(2, (total_power * 0.15) / POWER_VALUE); spawn_items(plr->deathpos, ITEM_POWER, drop); player_set_power(plr, total_power * 0.7); plr->bombs = PLR_START_BOMBS; plr->bomb_fragments = 0; plr->voltage *= 0.9; plr->lives--; stats_track_life_used(&plr->stats); } static void player_death_effect_draw_overlay(Projectile *p, int t, ProjDrawRuleArgs args) { FBPair *framebuffers = stage_get_fbpair(FBPAIR_FG); r_framebuffer(framebuffers->front); r_uniform_sampler("noise_tex", "static"); r_uniform_int("frames", global.frames); r_uniform_float("progress", t / p->timeout); r_uniform_vec2("origin", re(p->pos), VIEWPORT_H - im(p->pos)); r_uniform_vec2("clear_origin", re(global.plr.pos), VIEWPORT_H - im(global.plr.pos)); r_uniform_vec2("viewport", VIEWPORT_W, VIEWPORT_H); r_uniform_float("size", hypotf(VIEWPORT_W, VIEWPORT_H)); draw_framebuffer_tex(framebuffers->back, VIEWPORT_W, VIEWPORT_H); fbpair_swap(framebuffers); // This change must propagate, hence the r_state salsa. Yes, pop then push, I know what I'm doing. r_state_pop(); r_framebuffer(framebuffers->back); r_state_push(); } static void player_death_effect_draw_sprite(Projectile *p, int t, ProjDrawRuleArgs args) { float s = t / p->timeout; float stretch_range = 3, sx, sy; s = glm_ease_quad_in(s); sx = (1 - pow(2 * pow(1 - s, 4) - 1, 4)); sx = lerp(1 + (stretch_range - 1) * sx, stretch_range * sx, s); sy = 1 + 2 * (stretch_range - 1) * pow(s, 4); if(sx <= 0 || sy <= 0) { return; } SpriteParamsBuffer spbuf; SpriteParams sp = projectile_sprite_params(p, &spbuf); sp.scale.x *= sx; sp.scale.y *= sy; sp.rotation.angle = 0; r_draw_sprite(&sp); } TASK(player_death_blastspam, { cmplx pos; }) { for(int i = 0; i < 12; ++i) { RNG_ARRAY(R, 4); PARTICLE( .proto = pp_blast, .pos = ARGS.pos + vrng_range(R[0], 2, 3) * vrng_dir(R[1]), .color = RGBA(0.15, 0.2, 0.5, 0), .timeout = i + vrng_range(R[2], 10, 14), .draw_rule = pdraw_timeout_scalefade(0, 1, 1, 0), .angle = vrng_angle(R[3]), .flags = PFLAG_NOREFLECT, .layer = LAYER_OVERLAY, ); } } void player_death(Player *plr) { if(!player_is_vulnerable(plr) || stage_is_cleared()) { return; } play_sfx("death"); for(int i = 0; i < 60; i++) { RNG_ARRAY(R, 2); PARTICLE( .sprite = "flare", .pos = plr->pos, .timeout = 40, .draw_rule = pdraw_timeout_scale(2, 0.01), .move = move_linear(vrng_range(R[0], 3, 10) * vrng_dir(R[1])), .flags = PFLAG_NOREFLECT, ); } stage_clear_hazards(CLEAR_HAZARDS_ALL); PARTICLE( .sprite = "blast", .pos = plr->pos, .color = RGBA(0.5, 0.15, 0.15, 0), .timeout = 35, .draw_rule = pdraw_timeout_scalefade(0, 3.4, 1, 0), .angle = rng_angle(), .flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE | PFLAG_NOMOVE | PFLAG_MANUALANGLE, ); PARTICLE( .pos = plr->pos, .size = 1+I, .timeout = 90, .draw_rule = player_death_effect_draw_overlay, .blend = BLEND_NONE, .flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE, .layer = LAYER_OVERLAY, .shader = "player_death", ); Projectile *p = PARTICLE( .sprite_ptr = aniplayer_get_frame(&plr->ani), .pos = plr->pos, .timeout = 38, .draw_rule = player_death_effect_draw_sprite, .flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE, .layer = LAYER_PLAYER_FOCUS, // LAYER_OVERLAY | 1, ); INVOKE_TASK_AFTER(&p->events.killed, player_death_blastspam, p->pos); plr->deathtime = global.frames + floor(player_property(plr, PLR_PROP_DEATHBOMB_WINDOW)); if(player_is_powersurge_active(plr)) { player_cancel_powersurge(plr); // player_bomb(plr); } } static DamageResult ent_damage_player(EntityInterface *ent, const DamageInfo *dmg) { Player *plr = ENT_CAST(ent, Player); if( !player_is_vulnerable(plr) || (dmg->type != DMG_ENEMY_SHOT && dmg->type != DMG_ENEMY_COLLISION) ) { return DMG_RESULT_IMMUNE; } player_death(plr); return DMG_RESULT_OK; } void player_damage_hook(Player *plr, EntityInterface *target, DamageInfo *dmg) { if(player_is_powersurge_active(plr) && dmg->type == DMG_PLAYER_SHOT) { dmg->amount *= 1.2; } } static PlrInputFlag key_to_inflag(KeyIndex key) { switch(key) { case KEY_UP: return INFLAG_UP; case KEY_DOWN: return INFLAG_DOWN; case KEY_LEFT: return INFLAG_LEFT; case KEY_RIGHT: return INFLAG_RIGHT; case KEY_FOCUS: return INFLAG_FOCUS; case KEY_SHOT: return INFLAG_SHOT; case KEY_SKIP: return INFLAG_SKIP; default: return 0; } } static bool player_updateinputflags(Player *plr, PlrInputFlag flags) { if(flags == plr->inputflags) { return false; } plr->inputflags = flags; return true; } static bool player_updateinputflags_moveonly(Player *plr, PlrInputFlag flags) { return player_updateinputflags(plr, (flags & INFLAGS_MOVE) | (plr->inputflags & ~INFLAGS_MOVE)); } static bool player_setinputflag(Player *plr, KeyIndex key, bool mode) { PlrInputFlag newflags = plr->inputflags; PlrInputFlag keyflag = key_to_inflag(key); if(!keyflag) { return false; } if(mode) { newflags |= keyflag; } else { newflags &= ~keyflag; } return player_updateinputflags(plr, newflags); } static bool player_set_axis(int *aptr, uint16_t value) { int16_t new = (int16_t)value; if(*aptr == new) { return false; } *aptr = new; return true; } PlayerEventResult player_event( Player *plr, ReplayState *rpy_in, ReplayState *rpy_out, ReplayEventCode type, uint16_t value ) { bool useful = true; bool cheat = false; switch(type) { case EV_PRESS: if(dialog_is_active(global.dialog) && (value == KEY_SHOT || value == KEY_BOMB)) { useful = dialog_page(global.dialog); break; } switch(value) { case KEY_BOMB: useful = player_bomb(plr); break; case KEY_SPECIAL: if(dialog_is_active(global.dialog)) { useful = false; break; } useful = player_powersurge(plr); if(!useful/* && plr->iddqd*/) { player_cancel_powersurge(plr); useful = true; } break; case KEY_IDDQD: plr->iddqd = !plr->iddqd; cheat = true; break; case KEY_POWERUP: useful = player_add_power(plr, 100); cheat = true; break; case KEY_POWERDOWN: useful = player_add_power(plr, -100); cheat = true; break; default: useful = player_setinputflag(plr, value, true); break; } break; case EV_RELEASE: useful = player_setinputflag(plr, value, false); break; case EV_AXIS_LR: useful = player_set_axis(&plr->axis_lr, value); break; case EV_AXIS_UD: useful = player_set_axis(&plr->axis_ud, value); break; case EV_INFLAGS: useful = player_updateinputflags(plr, value); break; case EV_CONTINUE: // continuing in the same frame will desync the replay, // so schedule it for the next one plr->continuetime = global.frames + 1; useful = true; break; default: log_warn("Can not handle event: [%i:%02x:%04x]", global.frames, type, value); useful = false; break; } if(rpy_in && rpy_in->stage) { assert(rpy_in->mode == REPLAY_PLAY); if(!useful) { log_warn("Replay input: useless event: [%i:%02x:%04x]", global.frames, type, value); } if(cheat) { log_warn("Replay input: Cheat event: [%i:%02x:%04x]", global.frames, type, value); if( !(rpy_in->replay->flags & REPLAY_GFLAG_CHEATS) || !(rpy_in->stage->flags & REPLAY_SFLAG_CHEATS)) { log_warn("...but this replay was NOT properly cheat-flagged! Not cool, not cool at all"); } } if(type == EV_CONTINUE && ( !(rpy_in->replay->flags & REPLAY_GFLAG_CONTINUES) || !(rpy_in->stage->flags & REPLAY_SFLAG_CONTINUES))) { log_warn("Replay input: Continue event [%i:%02x:%04x], but this replay was not properly continue-flagged", global.frames, type, value); } } if(rpy_out && rpy_out->stage) { assert(rpy_out->mode == REPLAY_RECORD); if(useful) { replay_stage_event(rpy_out->stage, global.frames, type, value); if(type == EV_CONTINUE) { rpy_out->replay->flags |= REPLAY_GFLAG_CONTINUES; rpy_out->stage->flags |= REPLAY_SFLAG_CONTINUES; } if(cheat) { rpy_out->replay->flags |= REPLAY_GFLAG_CHEATS; rpy_out->stage->flags |= REPLAY_SFLAG_CHEATS; } } else { log_debug("Replay output: Useless event discarded: [%i:%02x:%04x]", global.frames, type, value); } } PlayerEventResult res = 0; if(useful) { res |= PLREVT_USEFUL; } if(cheat) { res |= PLREVT_CHEAT; } return res; } // free-axis movement static bool player_applymovement_gamepad(Player *plr) { if(!plr->axis_lr && !plr->axis_ud) { if(plr->gamepadmove) { plr->gamepadmove = false; plr->inputflags &= ~INFLAGS_MOVE; } return false; } cmplx direction = ( gamepad_normalize_axis_value(plr->axis_lr) + gamepad_normalize_axis_value(plr->axis_ud) * I ); if(cabs(direction) > 1) { direction /= cabs(direction); } int sr = sign(re(direction)); int si = sign(im(direction)); player_updateinputflags_moveonly(plr, (INFLAG_UP * (si == -1)) | (INFLAG_DOWN * (si == 1)) | (INFLAG_LEFT * (sr == -1)) | (INFLAG_RIGHT * (sr == 1)) ); if(direction) { plr->gamepadmove = true; player_move(&global.plr, direction); } return true; } static const char *moveseqname(int dir) { switch(dir) { case -1: return "left"; case 0: return "main"; case 1: return "right"; default: log_fatal("Invalid player animation dir given"); } } static void player_ani_moving(Player *plr, int dir) { if(plr->lastmovesequence == dir) return; const char *seqname = moveseqname(dir); const char *lastseqname = moveseqname(plr->lastmovesequence); char *transition = strjoin(lastseqname,"2",seqname,NULL); aniplayer_hard_switch(&plr->ani,transition,1); aniplayer_queue(&plr->ani,seqname,0); plr->lastmovesequence = dir; mem_free(transition); } void player_applymovement(Player *plr) { plr->velocity = 0; plr->uncapped_velocity = 0; if(!player_is_alive(plr)) return; bool gamepad = player_applymovement_gamepad(plr); int up = plr->inputflags & INFLAG_UP; int down = plr->inputflags & INFLAG_DOWN; int left = plr->inputflags & INFLAG_LEFT; int right = plr->inputflags & INFLAG_RIGHT; if(left && !right) { player_ani_moving(plr,-1); } else if(right && !left) { player_ani_moving(plr,1); } else { player_ani_moving(plr,0); } if(gamepad) { return; } cmplx direction = 0; if(up) direction -= 1.0*I; if(down) direction += 1.0*I; if(left) direction -= 1.0; if(right) direction += 1.0; if(cabs(direction)) direction /= cabs(direction); if(direction) player_move(&global.plr, direction); } void player_fix_input(Player *plr, ReplayState *rpy_out) { // correct input state to account for any events we might have missed, // usually because the pause menu ate them up PlrInputFlag newflags = plr->inputflags; bool invert_shot = config_get_int(CONFIG_SHOT_INVERTED); for(KeyIndex key = KEYIDX_FIRST; key <= KEYIDX_LAST; ++key) { int flag = key_to_inflag(key); if(flag && !(plr->gamepadmove && (flag & INFLAGS_MOVE))) { bool flagset = plr->inputflags & flag; bool keyheld = gamekeypressed(key); if(invert_shot && key == KEY_SHOT) { keyheld = !keyheld; } if(flagset && !keyheld) { newflags &= ~flag; } else if(!flagset && keyheld) { newflags |= flag; } } } if(newflags != plr->inputflags) { player_event(plr, NULL, rpy_out, EV_INFLAGS, newflags); } int axis_lr, axis_ud; gamepad_get_player_analog_input(&axis_lr, &axis_ud); if(plr->axis_lr != axis_lr) { player_event(plr, NULL, rpy_out, EV_AXIS_LR, axis_lr); } if(plr->axis_ud != axis_ud) { player_event(plr, NULL, rpy_out, EV_AXIS_UD, axis_ud); } if( config_get_int(CONFIG_AUTO_SURGE) && plr->power_stored == PLR_MAX_POWER_STORED && !player_is_powersurge_active(plr) && !dialog_is_active(global.dialog) ) { player_event(plr, NULL, rpy_out, EV_PRESS, KEY_SPECIAL); } } void player_graze(Player *plr, cmplx pos, int pts, int effect_intensity, const Color *color) { if(++plr->graze >= PLR_MAX_GRAZE) { log_debug("Graze counter overflow"); plr->graze = PLR_MAX_GRAZE; } pos = (pos + plr->pos) * 0.5; player_add_points(plr, pts, pos); play_sfx("graze"); Color *c = COLOR_COPY(color); color_add(c, RGBA(1, 1, 1, 1)); color_mul_scalar(c, 0.5); c->a = 0; for(int i = 0; i < effect_intensity; ++i) { RNG_ARRAY(R, 4); PARTICLE( .sprite = "graze", .color = c, .pos = pos, .draw_rule = pdraw_timeout_scalefade_exp(1, 0, 1, 0, 2), .move = move_asymptotic_simple(0.2 * vrng_range(R[0], 1, 6) * vrng_dir(R[1]), 16 * (1 + 0.5 * vrng_sreal(R[3]))), .timeout = vrng_range(R[2], 4, 29), .flags = PFLAG_NOREFLECT, // .layer = LAYER_PARTICLE_LOW, ); color_mul_scalar(c, 0.4); } spawn_items(pos, ITEM_POWER_MINI, 1); } static void player_add_fragments(Player *plr, int frags, int *pwhole, int *pfrags, int maxfrags, int maxwhole, const char *fragsnd, const char *upsnd, int score_per_excess) { int total_frags = *pfrags + maxfrags * *pwhole; int excess_frags = total_frags + frags - maxwhole * maxfrags; if(excess_frags > 0) { player_add_points(plr, excess_frags * score_per_excess, plr->pos); frags -= excess_frags; } *pfrags += frags; int up = *pfrags / maxfrags; *pwhole += up; *pfrags %= maxfrags; if(up) { play_sfx(upsnd); } if(frags) { // FIXME: when we have the extra life/bomb sounds, // don't play this if upsnd was just played. play_sfx(fragsnd); } if(*pwhole >= maxwhole) { *pwhole = maxwhole; *pfrags = 0; } } void player_add_life_fragments(Player *plr, int frags) { if(global.stage->type == STAGE_SPELL) { return; } player_add_fragments( plr, frags, &plr->lives, &plr->life_fragments, PLR_MAX_LIFE_FRAGMENTS, PLR_MAX_LIVES, "item_generic", // FIXME: replacement needed "extra_life", (plr->point_item_value * 10) / PLR_MAX_LIFE_FRAGMENTS ); } void player_add_bomb_fragments(Player *plr, int frags) { if(global.stage->type == STAGE_SPELL) { return; } player_add_fragments( plr, frags, &plr->bombs, &plr->bomb_fragments, PLR_MAX_BOMB_FRAGMENTS, PLR_MAX_BOMBS, "item_generic", // FIXME: replacement needed "extra_bomb", (plr->point_item_value * 5) / PLR_MAX_BOMB_FRAGMENTS ); } void player_add_lives(Player *plr, int lives) { player_add_life_fragments(plr, PLR_MAX_LIFE_FRAGMENTS * lives); } void player_add_bombs(Player *plr, int bombs) { player_add_bomb_fragments(plr, PLR_MAX_BOMB_FRAGMENTS * bombs); } static void scoretext_update(StageText *txt, int t, float a) { double r = bits_to_double((uintptr_t)txt->custom.data1); txt->pos -= I * cexp(I*r) * a; } #define SCORETEXT_PIV_BIT ((uintptr_t)1 << ((sizeof(uintptr_t) * 8) - 1)) static StageText *find_scoretext_combination_candidate(cmplx pos, bool is_piv) { for(StageText *stxt = stagetext_list_head(); stxt; stxt = stxt->next) { if( stxt->custom.update == scoretext_update && stxt->time.spawn > global.frames && (bool)((uintptr_t)(stxt->custom.data2) & SCORETEXT_PIV_BIT) == is_piv && cabs(pos - stxt->pos) < 32 ) { return stxt; } } return NULL; } static void add_score_text(Player *plr, cmplx location, uint points, bool is_piv) { double rnd = rng_f64s(); StageText *stxt = find_scoretext_combination_candidate(location, is_piv); if(stxt) { points += ((uintptr_t)stxt->custom.data2 & ~SCORETEXT_PIV_BIT); } float importance; float a; Color c; struct { int delay, lifetime, fadeintime, fadeouttime; } timings; timings.delay = 6; timings.fadeintime = 10; timings.fadeouttime = 20; if(is_piv) { importance = sqrt(min(points/500.0, 1)); a = lerp(0.4, 1.0, importance); c = *color_lerp(RGB(0.5, 0.8, 1.0), RGB(1.0, 0.3, 1.0), importance); timings.lifetime = 35 + 10 * importance; } else { importance = clamp(0.25 * (double)points / (double)plr->point_item_value, 0, 1); a = clamp(0.5 + 0.5 * cbrtf(importance), 0, 1); c = *color_lerp(RGB(1.0, 0.8, 0.4), RGB(0.4, 1.0, 0.3), importance); timings.lifetime = 25 + 20 * importance; } a *= config_get_float(CONFIG_SCORETEXT_ALPHA); color_mul_scalar(&c, a); if(!stxt) { if(c.a < 1e-4) { return; } stxt = stagetext_add( NULL, location, ALIGN_CENTER, res_font("small"), &c, timings.delay, timings.lifetime, timings.fadeintime, timings.fadeouttime ); stxt->custom.data1 = (void*)(uintptr_t)double_to_bits(rnd); stxt->custom.update = scoretext_update; } else { stxt->color = c; } format_huge_num(0, points, sizeof(stxt->text), stxt->text); stxt->custom.data2 = (void*)(uintptr_t)(points | (SCORETEXT_PIV_BIT * is_piv)); if(is_piv) { strlcat(stxt->text, " PIV", sizeof(stxt->text)); } } void player_add_points(Player *plr, uint points, cmplx location) { plr->points += points; while(plr->points >= plr->extralife_threshold) { plr->extralife_threshold = player_next_extralife_threshold(++plr->extralives_given); player_add_lives(plr, 1); } add_score_text(plr, location, points, false); } void player_add_piv(Player *plr, uint piv, cmplx location) { uint v = plr->point_item_value + piv; if(v > PLR_MAX_PIV || v < plr->point_item_value) { plr->point_item_value = PLR_MAX_PIV; } else { plr->point_item_value = v; } add_score_text(plr, location, piv, true); } void player_add_voltage(Player *plr, uint voltage) { uint v = plr->voltage + voltage; if(v > PLR_MAX_VOLTAGE || v < plr->voltage) { plr->voltage = PLR_MAX_VOLTAGE; } else { plr->voltage = v; } player_add_bomb_fragments(plr, voltage); } bool player_drain_voltage(Player *plr, uint voltage) { // TODO: animate (or maybe handle that at stagedraw level) if(plr->voltage >= voltage) { plr->voltage -= voltage; return true; } plr->voltage = 0; return false; } void player_register_damage(Player *plr, EntityInterface *target, const DamageInfo *damage) { if(!DAMAGETYPE_IS_PLAYER(damage->type)) { return; } cmplx pos = NAN; if(target != NULL) { switch(target->type) { case ENT_TYPE_ID(Enemy): { pos = ENT_CAST(target, Enemy)->pos; player_add_points(&global.plr, damage->amount * 0.5, pos); break; } case ENT_TYPE_ID(Boss): { pos = ENT_CAST(target, Boss)->pos; player_add_points(&global.plr, damage->amount * 0.2, pos); break; } default: break; } } if(!isnan(re(pos)) && damage->type == DMG_PLAYER_DISCHARGE) { double rate = NOT_NULL(target)->type == ENT_TYPE_ID(Boss) ? 110 : 256; spawn_and_collect_items(pos, 1, ITEM_VOLTAGE, (int)(damage->amount / rate)); } if(player_is_powersurge_active(plr)) { plr->powersurge.damage_done += damage->amount; plr->powersurge.damage_accum += damage->amount; if(!isnan(re(pos))) { double rate = 500; while(plr->powersurge.damage_accum > rate) { plr->powersurge.damage_accum -= rate; spawn_item(pos, ITEM_SURGE); } } } #ifdef PLR_DPS_STATS while(global.frames > plr->dmglogframe) { memmove(plr->dmglog + 1, plr->dmglog, sizeof(plr->dmglog) - sizeof(*plr->dmglog)); plr->dmglog[0] = 0; plr->dmglogframe++; } plr->dmglog[0] += damage->amount; #endif } uint64_t player_next_extralife_threshold(uint64_t step) { static uint64_t base = 5000000; return base * (step * step + step + 2) / 2; } void player_preload(ResourceGroup *rg) { const int flags = RESF_DEFAULT; res_group_preload(rg, RES_SHADER_PROGRAM, flags, "circle_distort", "player_death", NULL); res_group_preload(rg, RES_TEXTURE, flags, "static", NULL); res_group_preload(rg, RES_SPRITE, flags, "fairy_circle", "focus", "part/blast_huge_halo", "part/powersurge_field", NULL); res_group_preload(rg, RES_SFX, flags | RESF_OPTIONAL, "death", "extra_bomb", "extra_life", "generic_shot", "graze", "hit0", "hit1", "powerup", "powersurge_start", "powersurge_end", NULL); } // FIXME: where should this be? cmplx plrutil_homing_target(cmplx org, cmplx fallback) { double mindst = INFINITY; cmplx target = fallback; if(global.boss && boss_is_vulnerable(global.boss)) { target = global.boss->pos; mindst = cabs(target - org); } for(Enemy *e = global.enemies.first; e; e = e->next) { if(!enemy_is_targetable(e)) { continue; } double dst = cabs(e->pos - org); if(dst < mindst) { mindst = dst; target = e->pos; } } return target; } void plrutil_slave_retract(BoxedPlayer bplr, cmplx *pos, real retract_time) { cmplx pos0 = *pos; Player *plr; for(int i = 1; i <= retract_time; ++i) { YIELD; plr = NOT_NULL(ENT_UNBOX(bplr)); *pos = clerp(pos0, plr->pos, i / retract_time); } }