<commit_before>#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>

#include "game/objects/har.h"
#include "game/objects/scrap.h"
#include "game/objects/projectile.h"
#include "game/protos/intersect.h"
#include "game/protos/object_specializer.h"
#include "game/scenes/arena.h"
#include "game/game_state.h"
#include "game/serial.h"
#include "resources/af_loader.h"
#include "resources/ids.h"
#include "resources/animation.h"
#include "controller/controller.h"
#include "utils/log.h"
#include "utils/random.h"

#define FUDGEFACTOR 0.003f
#define IS_ZERO(n) (n < 0.8 && n > -0.8)

void har_finished(object *obj);
int har_act(object *obj, int act_type);
void har_spawn_scrap(object *obj, vec2i pos, int amount);

void har_free(object *obj) {
    har *h = object_get_userdata(obj);
    free(h);
}

void har_stunned_done(object *har_obj) {
    har *h = object_get_userdata(har_obj);

    if (h->state == STATE_STUNNED) {
        // refill endurance
        h->endurance = h->endurance_max;
        h->state = STATE_STANDING;
        har_set_ani(har_obj, ANIM_IDLE, 1);
    }
}

void har_fire_hook(object *obj, int action) {
    har *h = object_get_userdata(obj);
    if (h->action_hook_cb) {
        h->action_hook_cb(action, h->action_hook_cb_data);
    }
    int pos = obj->age % OBJECT_EVENT_BUFFER_SIZE;
    h->act_buf[pos].actions[h->act_buf[pos].count] = (unsigned char)action;
    h->act_buf[pos].count++;
    h->act_buf[pos].age = obj->age;;
}

// Simple helper function
void har_set_ani(object *obj, int animation_id, int repeat) {
    har *h = object_get_userdata(obj);
    af_move *move = af_get_move(h->af_data, animation_id);
    char *s = (char*)str_c(&move->move_string);
    object_set_animation(obj, &move->ani);
    if (s != NULL && strcasecmp(s, "!") && strcasecmp(s, "0") && h->delay > 0) {
        DEBUG("delaying move %d %s by %d ticks", move->id, s, h->delay);
        object_set_delay(obj, h->delay);
    }
    object_set_repeat(obj, repeat);
    object_set_stride(obj, 1);
    object_tick(obj);
    // update this so mx/my have correct origins
    obj->start = obj->pos;
    h->damage_done = 0;
    h->damage_received = 0;
    h->executing_move = 0;
    h->flinching = 0;
}

int har_is_active(object *obj) {
    har *h = object_get_userdata(obj);
    return h->executing_move;
}

int har_is_walking(har *h) {
    if (h->state == STATE_WALKTO || h->state == STATE_WALKFROM) {
        return 1;
    }
    return 0;
}

int har_is_crouching(har *h) {
    if (h->state == STATE_CROUCHING || h->state == STATE_CROUCHBLOCK) {
        return 1;
    }
    return 0;
}

int har_is_blocking(har *h, af_move *move) {
    if (move->category == CAT_CLOSE) {
        // throws cannot be blocked
        return 0;
    }
    if (h->state == STATE_CROUCHBLOCK && move->category != CAT_JUMPING && h->executing_move == 0) {
        return 1;
    }
    if (h->state == STATE_WALKFROM && move->category != CAT_LOW && h->executing_move == 0) {
        return 1;
    }
    return 0;
}

int frame_isset(sd_stringparser_frame *frame, const char *tag) {
    const sd_stringparser_tag_value *v;
    sd_stringparser_get_tag(frame->parser, frame->id, tag, &v);
    return v->is_set;
}

int frame_get(sd_stringparser_frame *frame, const char *tag) {
    const sd_stringparser_tag_value *v;
    sd_stringparser_get_tag(frame->parser, frame->id, tag, &v);
    return v->value;
}

int har_is_invincible(object *obj, af_move *move) {
    sd_stringparser_frame f = obj->animation_state.parser->current_frame;
    if (frame_isset(&f, "zz")) {
        // blocks everything
        return 1;
    }
    switch (move->category) {
        // XX 'zg' is not handled here, but the game doesn't use it...
        case CAT_LOW:
            if (frame_isset(&f, "zl")) {
                return 1;
            }
            break;
        case CAT_MEDIUM:
            if (frame_isset(&f, "zm")) {
                return 1;
            }
            break;
        case CAT_HIGH:
            if (frame_isset(&f, "zh")) {
                return 1;
            }
            break;
        case CAT_JUMPING:
            if (frame_isset(&f, "zj")) {
                return 1;
            }
            break;
        case CAT_PROJECTILE:
            if (frame_isset(&f, "zp")) {
                return 1;
            }
            break;
    }
    return 0;
}

// Callback for spawning new objects, eg. projectiles
void cb_har_spawn_object(object *parent, int id, vec2i pos, int g, void *userdata) {
    har *h = userdata;

    // If this is a scrap item, handle it as such ...
    if(id == ANIM_SCRAP_METAL || id == ANIM_BOLT || id == ANIM_SCREW || id == ANIM_BURNING_OIL) {
        // If the scrap obviously has a wrong starting position, fix it
        /*vec2i p_size = object_get_size(parent);*/
        vec2i p_pos = object_get_pos(parent);
        if(pos.x == 0) {
            pos.x = p_pos.x;// + p_size.x / 2;
        }
        if(pos.y == 0) {
            pos.y = p_pos.y;//y + p_size.y / 2;
        }

        // Use our existing function for spawning scrap
        har_spawn_scrap(parent, pos, 12);
        return;
    }

    // ... otherwise expect it is a projectile
    af_move *move = af_get_move(h->af_data, id);
    if(move != NULL) {
        object *obj = malloc(sizeof(object));
        object_create(obj, parent->gs, pos, vec2f_create(0,0));
        object_set_userdata(obj, h);
        object_set_stl(obj, object_get_stl(parent));
        object_set_animation(obj, &move->ani);
        object_set_gravity(obj, g/50);
        object_set_pal_offset(obj, object_get_pal_offset(parent));
        // Set all projectiles to their own layer + har layer
        object_set_layers(obj, LAYER_PROJECTILE|(h->player_id == 0 ? LAYER_HAR2 : LAYER_HAR1)); 
        // To avoid projectile-to-projectile collisions, set them to same group
        object_set_group(obj, GROUP_PROJECTILE); 
        object_set_repeat(obj, 0);
        object_set_shadow(obj, 1);
        object_set_direction(obj, object_get_direction(parent));
        projectile_create(obj);
        game_state_add_object(parent->gs, obj, RENDER_LAYER_MIDDLE);
    }
}

void har_move(object *obj) {
    vec2f vel = object_get_vel(obj);
    obj->pos.x += vel.x;
    obj->pos.y += vel.y;
    har *h = object_get_userdata(obj);
    if(obj->pos.y > 190) {
        if (h->state != STATE_FALLEN) {
            // We collided with ground, so set vertical velocity to 0 and
            // make sure object is level with ground
            obj->pos.y = 190;
            object_set_vel(obj, vec2f_create(vel.x, 0));
        }

        // Change animation from jump to walk or idle,
        // depending on horizontal velocity
        object_set_gravity(obj, 1);
        if(h->state == STATE_JUMPING) {
            /*if(object_get_hstate(obj) == OBJECT_MOVING) {*/
                /*h->state = STATE_WALKING;*/
                /*har_set_ani(obj, ANIM_WALKING, 1);*/
            /*} else {*/
                h->state = STATE_STANDING;
                har_set_ani(obj, ANIM_IDLE, 1);
                har_fire_hook(obj, ACT_STOP);
                har_fire_hook(obj, ACT_FLUSH);
            /*}*/
        } else if (h->state == STATE_FALLEN || h->state == STATE_RECOIL) {
            float dampen = 0.4;
            vec2f vel = object_get_vel(obj);
            vec2i pos = object_get_pos(obj);
            if(pos.y > 190) {
                // TODO spawn clouds of dust
                pos.y = 190;
                vel.y = -vel.y * dampen;
                vel.x = vel.x * dampen;
            }

            if (pos.x <= 15 || pos.x >= 305) {
                vel.x = 0.0;
            }

            if(pos.y >= 185 && 
                    obj->animation_state.parser->current_frame.is_final_frame) {
                if (h->state == STATE_FALLEN) {
                    if (h->health <= 0 && h->endurance <= 0) {
                        h->state = STATE_DEFEAT;
                        har_set_ani(obj, ANIM_DEFEAT, 0);
                    } else {
                        h->state = STATE_STANDING_UP;
                        har_set_ani(obj, ANIM_STANDUP, 0);
                    }
                } else {
                    har_finished(obj);
                }
            }
            object_set_pos(obj, pos);
            object_set_vel(obj, vel);
        }
    } else {
        object_set_vel(obj, vec2f_create(vel.x, vel.y + obj->gravity));
    }
}

void har_take_damage(object *obj, str* string, float damage) {
    har *h = object_get_userdata(obj);
    int oldhealth = h->health;
    h->health -= damage;
    if(h->health <= 0) { h->health = 0; }

    if (oldhealth <= 0) {
        // har has no health left and is left only with endurance.
        // one hit will end them
        h->endurance = 0;
    } else {
        h->endurance -= damage * 8;
        if(h->endurance <= 0) {
            if (h->state == STATE_STUNNED) {
                // refill endurance
                h->endurance = h->endurance_max;
            } else {
                h->endurance = 0;
            }
        }
    }

    // chronos' stasis does not have a hit animation
    if (string->data) {
        sd_stringparser_frame f;
        const sd_stringparser_tag_value *v;
        h->state = STATE_RECOIL;
        // Set hit animation
        object_set_animation(obj, &af_get_move(h->af_data, ANIM_DAMAGE)->ani);
        object_set_repeat(obj, 0);
        if (h->health <= 0 && h->endurance <= 0) {
            // taken from MASTER.DAT
            // XXX changed the last frame to 200 ticks to ensure the HAR falls down
            char *final = "-x-20ox-20L1-ox-20L2-x-20zzs4l25sp13M1-zzM200";
            char *str = malloc(str_size(string) + strlen(final) + 1);
            // append the 'final knockback' string to the hit string, replacing the final frame
            sprintf(str, "%s", string->data);
            char *last = strrchr(str, '-');
            sprintf(last, "%s", final);
            object_set_custom_string(obj, str);
            free(str);
        } else {
            object_set_custom_string(obj, str_c(string));
        }
        object_tick(obj);
        h->flinching = 1;
        // XXX hack - if the first frame has the 'k' tag, treat it as some vertical knockback
        // we can't do this in player.c because it breaks the jaguar leap, which also uses the 'k' tag.
        sd_stringparser_peek(obj->animation_state.parser, 0, &f);
        sd_stringparser_get_tag(f.parser, f.id, "k", &v);
        if (v->is_set) {
                obj->vel.y -= 7;
        }
    }
}
void har_spawn_oil(object *obj, vec2i pos, int amount, float gravity, int layer) {
    float rv = 0.0f;
    float velx, vely;
    har *h = object_get_userdata(obj);

    // burning oil
    for(int i = 0; i < amount; i++) {
        // Calculate velocity etc.
        rv = rand_int(100) / 100.0f - 0.5;
        velx = (5 * cos(90 + i-(amount) / 2 + rv)) * object_get_direction(obj);
        vely = -12 * sin(i / amount + rv);

        // Make sure scrap has somekind of velocity
        // (to prevent floating scrap objects)
        if(vely < 0.1 && vely > -0.1) vely += 0.21;

        // Create the object
        object *scrap = malloc(sizeof(object));
        int anim_no = ANIM_BURNING_OIL;
        object_create(scrap, obj->gs, pos, vec2f_create(velx, vely));
        object_set_animation(scrap, &af_get_move(h->af_data, anim_no)->ani);
        object_set_stl(scrap, object_get_stl(obj));
        object_set_gravity(scrap, gravity);
        object_set_layers(scrap, LAYER_SCRAP);
        object_tick(scrap);
        scrap_create(scrap);
        game_state_add_object(obj->gs, scrap, layer);
    }

}

// TODO: This is kind of a hack. It's used to check if either 
// HAR is doing destruction. If there is any way to do this better,
// this should be changed.
int is_destruction(game_state *gs) {
    object *obj_a = game_state_get_player(gs, 0)->har;
    object *obj_b = game_state_get_player(gs, 1)->har;
    har *har_a = object_get_userdata(obj_a);
    har *har_b = object_get_userdata(obj_b);
    af_move *move_a = af_get_move(har_a->af_data, obj_a->cur_animation->id);
    af_move *move_b = af_get_move(har_b->af_data, obj_b->cur_animation->id);
    return (move_a->category == CAT_DESTRUCTION || move_b->category == CAT_DESTRUCTION);
}

void har_spawn_scrap(object *obj, vec2i pos, int amount) {
    float rv = 0.0f;
    float velx, vely;
    // wild ass guess
    int oil_amount = amount / 3;
    har *h = object_get_userdata(obj);
    har_spawn_oil(obj, pos, oil_amount, 1, RENDER_LAYER_TOP);

    // scrap metal
    // TODO this assumes the default scrap level and does not consider BIG[1-9]
    int scrap_amount = 0;
    int destr = is_destruction(obj->gs);
    if(destr) {
        scrap_amount = 30;
    } else if (amount > 11 && amount < 14) {
        scrap_amount = 1;
    } else if (amount > 13 && amount < 16) {
        scrap_amount = 2;
    } else if (amount > 15) {
        scrap_amount = 3;
    }
    for(int i = 0; i < scrap_amount; i++) {
        // Calculate velocity etc.
        rv = rand_int(100) / 100.0f - 0.5;
        velx = (5 * cos(90 + i-(scrap_amount) / 2 + rv)) * object_get_direction(obj);
        vely = -12 * sin(i / scrap_amount + rv);

        // Make destruction moves look more impressive :P
        if(destr) {
            velx *= 5;
            vely *= 5;
        }

        // Make sure scrap has somekind of velocity
        // (to prevent floating scrap objects)
        if(vely < 0.1 && vely > -0.1) vely += 0.21;

        // Create the object
        object *scrap = malloc(sizeof(object));
        int anim_no = rand_int(3) + ANIM_SCRAP_METAL;
        object_create(scrap, obj->gs, pos, vec2f_create(velx, vely));
        object_set_animation(scrap, &af_get_move(h->af_data, anim_no)->ani);
        object_set_stl(scrap, object_get_stl(obj));
        object_set_gravity(scrap, 1);
        object_set_pal_offset(scrap, object_get_pal_offset(obj));
        object_set_layers(scrap, LAYER_SCRAP);
        object_tick(scrap);
        object_set_shadow(scrap, 1);
        scrap_create(scrap);
        game_state_add_object(obj->gs, scrap, RENDER_LAYER_TOP);
    }

}

void har_block(object *obj, vec2i hit_coord) {
    har *h = obj->userdata;
    if (h->state == STATE_WALKFROM) {
        object_set_animation(obj, &af_get_move(h->af_data, ANIM_STANDING_BLOCK)->ani);
    } else {
        object_set_animation(obj, &af_get_move(h->af_data, ANIM_CROUCHING_BLOCK)->ani);
    }
    // the HARs have a lame blank frame in their animation string, so use a custom one
    object_set_custom_string(obj, "A5");
    object_set_repeat(obj, 0);
    object_tick(obj);
    // blocking spark
    if (h->damage_received) {
        // don't make another scrape
        return;
    }
    object *scrape = malloc(sizeof(object));
    object_create(scrape, obj->gs, hit_coord, vec2f_create(0, 0));
    object_set_animation(scrape, &af_get_move(h->af_data, ANIM_BLOCKING_SCRAPE)->ani);
    object_set_stl(scrape, object_get_stl(obj));
    object_set_direction(scrape, object_get_direction(obj));
    object_set_repeat(scrape, 0);
    object_set_gravity(scrape, 0);
    object_set_layers(scrape, LAYER_SCRAP);
    object_tick(scrape);
    object_tick(scrape);
    game_state_add_object(obj->gs, scrape, RENDER_LAYER_MIDDLE);
    h->damage_received = 1;
    h->flinching = 1;
}

void har_check_closeness(object *obj_a, object *obj_b) {
    vec2i pos_a = object_get_pos(obj_a);
    vec2i pos_b = object_get_pos(obj_b);
    har *a = object_get_userdata(obj_a);
    har *b = object_get_userdata(obj_b);
    int hard_limit = 35; // Push opponent if HARs too close. Harrison-Stetson method value.
    int soft_limit = 45; // Sets HAR A as being close to HAR B if closer than this.

    if (b->state == STATE_RECOIL || a->state == STATE_RECOIL || b->state == STATE_JUMPING || a->state == STATE_JUMPING || a->state == STATE_FALLEN || b->state == STATE_FALLEN) {
        return;
    }

    // Reset closeness state
    a->close = 0;
    a->hard_close = 0;

    // If HARs get too close together, handle it
    if(har_is_walking(a) && object_get_direction(obj_a) == OBJECT_FACE_LEFT) {
        if(pos_a.x < pos_b.x + hard_limit && pos_a.x > pos_b.x) {
            // don't allow hars to overlap in the corners
            if (pos_b.x > 15) {
                pos_b.x = pos_a.x - hard_limit;
                object_set_pos(obj_b, pos_b);
            } else {
                pos_a.x = pos_b.x + hard_limit;
                object_set_pos(obj_a, pos_a);
            }
            a->hard_close = 1;
        }
        if(pos_a.x < pos_b.x + soft_limit && pos_a.x > pos_b.x) {
            if (b->state == STATE_STANDING || har_is_walking(b) || har_is_crouching(b)) {
                a->close = 1;
            }
            a->hard_close = 1;
        }
    }
    if(har_is_walking(a) && object_get_direction(obj_a) == OBJECT_FACE_RIGHT) {
        if(pos_a.x + hard_limit > pos_b.x && pos_a.x < pos_b.x) {
            // don't allow hars to overlap in the corners
            if (pos_b.x < 305) {
                pos_b.x = pos_a.x + hard_limit;
                object_set_pos(obj_b, pos_b);
            } else {
                pos_a.x = pos_b.x - hard_limit;
                object_set_pos(obj_a, pos_a);
            }
            a->hard_close = 1;
        }
        if(pos_a.x + soft_limit > pos_b.x && pos_a.x < pos_b.x) {
            if (b->state == STATE_STANDING || har_is_walking(b) || har_is_crouching(b)) {
                a->close = 1;
            }
            a->hard_close = 1;
        }
    }
}<commit_msg>HAR can now be thrown in stunned state<commit_after>#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.h>

#include "game/objects/har.h"
#include "game/objects/scrap.h"
#include "game/objects/projectile.h"
#include "game/protos/intersect.h"
#include "game/protos/object_specializer.h"
#include "game/scenes/arena.h"
#include "game/game_state.h"
#include "game/serial.h"
#include "resources/af_loader.h"
#include "resources/ids.h"
#include "resources/animation.h"
#include "controller/controller.h"
#include "utils/log.h"
#include "utils/random.h"

#define FUDGEFACTOR 0.003f
#define IS_ZERO(n) (n < 0.8 && n > -0.8)

void har_finished(object *obj);
int har_act(object *obj, int act_type);
void har_spawn_scrap(object *obj, vec2i pos, int amount);

void har_free(object *obj) {
    har *h = object_get_userdata(obj);
    free(h);
}

void har_stunned_done(object *har_obj) {
    har *h = object_get_userdata(har_obj);

    if (h->state == STATE_STUNNED) {
        // refill endurance
        h->endurance = h->endurance_max;
        h->state = STATE_STANDING;
        har_set_ani(har_obj, ANIM_IDLE, 1);
    }
}

void har_fire_hook(object *obj, int action) {
    har *h = object_get_userdata(obj);
    if (h->action_hook_cb) {
        h->action_hook_cb(action, h->action_hook_cb_data);
    }
    int pos = obj->age % OBJECT_EVENT_BUFFER_SIZE;
    h->act_buf[pos].actions[h->act_buf[pos].count] = (unsigned char)action;
    h->act_buf[pos].count++;
    h->act_buf[pos].age = obj->age;;
}

// Simple helper function
void har_set_ani(object *obj, int animation_id, int repeat) {
    har *h = object_get_userdata(obj);
    af_move *move = af_get_move(h->af_data, animation_id);
    char *s = (char*)str_c(&move->move_string);
    object_set_animation(obj, &move->ani);
    if (s != NULL && strcasecmp(s, "!") && strcasecmp(s, "0") && h->delay > 0) {
        DEBUG("delaying move %d %s by %d ticks", move->id, s, h->delay);
        object_set_delay(obj, h->delay);
    }
    object_set_repeat(obj, repeat);
    object_set_stride(obj, 1);
    object_tick(obj);
    // update this so mx/my have correct origins
    obj->start = obj->pos;
    h->damage_done = 0;
    h->damage_received = 0;
    h->executing_move = 0;
    h->flinching = 0;
}

int har_is_active(object *obj) {
    har *h = object_get_userdata(obj);
    return h->executing_move;
}

int har_is_walking(har *h) {
    if (h->state == STATE_WALKTO || h->state == STATE_WALKFROM) {
        return 1;
    }
    return 0;
}

int har_is_crouching(har *h) {
    if (h->state == STATE_CROUCHING || h->state == STATE_CROUCHBLOCK) {
        return 1;
    }
    return 0;
}

int har_is_blocking(har *h, af_move *move) {
    if (move->category == CAT_CLOSE) {
        // throws cannot be blocked
        return 0;
    }
    if (h->state == STATE_CROUCHBLOCK && move->category != CAT_JUMPING && h->executing_move == 0) {
        return 1;
    }
    if (h->state == STATE_WALKFROM && move->category != CAT_LOW && h->executing_move == 0) {
        return 1;
    }
    return 0;
}

int frame_isset(sd_stringparser_frame *frame, const char *tag) {
    const sd_stringparser_tag_value *v;
    sd_stringparser_get_tag(frame->parser, frame->id, tag, &v);
    return v->is_set;
}

int frame_get(sd_stringparser_frame *frame, const char *tag) {
    const sd_stringparser_tag_value *v;
    sd_stringparser_get_tag(frame->parser, frame->id, tag, &v);
    return v->value;
}

int har_is_invincible(object *obj, af_move *move) {
    sd_stringparser_frame f = obj->animation_state.parser->current_frame;
    if (frame_isset(&f, "zz")) {
        // blocks everything
        return 1;
    }
    switch (move->category) {
        // XX 'zg' is not handled here, but the game doesn't use it...
        case CAT_LOW:
            if (frame_isset(&f, "zl")) {
                return 1;
            }
            break;
        case CAT_MEDIUM:
            if (frame_isset(&f, "zm")) {
                return 1;
            }
            break;
        case CAT_HIGH:
            if (frame_isset(&f, "zh")) {
                return 1;
            }
            break;
        case CAT_JUMPING:
            if (frame_isset(&f, "zj")) {
                return 1;
            }
            break;
        case CAT_PROJECTILE:
            if (frame_isset(&f, "zp")) {
                return 1;
            }
            break;
    }
    return 0;
}

// Callback for spawning new objects, eg. projectiles
void cb_har_spawn_object(object *parent, int id, vec2i pos, int g, void *userdata) {
    har *h = userdata;

    // If this is a scrap item, handle it as such ...
    if(id == ANIM_SCRAP_METAL || id == ANIM_BOLT || id == ANIM_SCREW || id == ANIM_BURNING_OIL) {
        // If the scrap obviously has a wrong starting position, fix it
        /*vec2i p_size = object_get_size(parent);*/
        vec2i p_pos = object_get_pos(parent);
        if(pos.x == 0) {
            pos.x = p_pos.x;// + p_size.x / 2;
        }
        if(pos.y == 0) {
            pos.y = p_pos.y;//y + p_size.y / 2;
        }

        // Use our existing function for spawning scrap
        har_spawn_scrap(parent, pos, 12);
        return;
    }

    // ... otherwise expect it is a projectile
    af_move *move = af_get_move(h->af_data, id);
    if(move != NULL) {
        object *obj = malloc(sizeof(object));
        object_create(obj, parent->gs, pos, vec2f_create(0,0));
        object_set_userdata(obj, h);
        object_set_stl(obj, object_get_stl(parent));
        object_set_animation(obj, &move->ani);
        object_set_gravity(obj, g/50);
        object_set_pal_offset(obj, object_get_pal_offset(parent));
        // Set all projectiles to their own layer + har layer
        object_set_layers(obj, LAYER_PROJECTILE|(h->player_id == 0 ? LAYER_HAR2 : LAYER_HAR1)); 
        // To avoid projectile-to-projectile collisions, set them to same group
        object_set_group(obj, GROUP_PROJECTILE); 
        object_set_repeat(obj, 0);
        object_set_shadow(obj, 1);
        object_set_direction(obj, object_get_direction(parent));
        projectile_create(obj);
        game_state_add_object(parent->gs, obj, RENDER_LAYER_MIDDLE);
    }
}

void har_move(object *obj) {
    vec2f vel = object_get_vel(obj);
    obj->pos.x += vel.x;
    obj->pos.y += vel.y;
    har *h = object_get_userdata(obj);
    if(obj->pos.y > 190) {
        if (h->state != STATE_FALLEN) {
            // We collided with ground, so set vertical velocity to 0 and
            // make sure object is level with ground
            obj->pos.y = 190;
            object_set_vel(obj, vec2f_create(vel.x, 0));
        }

        // Change animation from jump to walk or idle,
        // depending on horizontal velocity
        object_set_gravity(obj, 1);
        if(h->state == STATE_JUMPING) {
            /*if(object_get_hstate(obj) == OBJECT_MOVING) {*/
                /*h->state = STATE_WALKING;*/
                /*har_set_ani(obj, ANIM_WALKING, 1);*/
            /*} else {*/
                h->state = STATE_STANDING;
                har_set_ani(obj, ANIM_IDLE, 1);
                har_fire_hook(obj, ACT_STOP);
                har_fire_hook(obj, ACT_FLUSH);
            /*}*/
        } else if (h->state == STATE_FALLEN || h->state == STATE_RECOIL) {
            float dampen = 0.4;
            vec2f vel = object_get_vel(obj);
            vec2i pos = object_get_pos(obj);
            if(pos.y > 190) {
                // TODO spawn clouds of dust
                pos.y = 190;
                vel.y = -vel.y * dampen;
                vel.x = vel.x * dampen;
            }

            if (pos.x <= 15 || pos.x >= 305) {
                vel.x = 0.0;
            }

            // prevent har from sliding after defeat
            if(obj->animation_state.parser->current_frame.is_final_frame) {
                if (h->health <= 0 && h->endurance <= 0) {
                    h->state = STATE_DEFEAT;
                    har_set_ani(obj, ANIM_DEFEAT, 0);
                }
            }

            if(pos.y >= 185 && 
                    IS_ZERO(vel.x) &&
                    obj->animation_state.parser->current_frame.is_final_frame) {
                if (h->state == STATE_FALLEN) {
                    if (h->health <= 0 && h->endurance <= 0) {
                        h->state = STATE_DEFEAT;
                        har_set_ani(obj, ANIM_DEFEAT, 0);
                    } else {
                        h->state = STATE_STANDING_UP;
                        har_set_ani(obj, ANIM_STANDUP, 0);
                    }
                } else {
                    har_finished(obj);
                }
            }
            object_set_pos(obj, pos);
            object_set_vel(obj, vel);
        }
    } else {
        object_set_vel(obj, vec2f_create(vel.x, vel.y + obj->gravity));
    }
}

void har_take_damage(object *obj, str* string, float damage) {
    har *h = object_get_userdata(obj);
    int oldhealth = h->health;
    h->health -= damage;
    if(h->health <= 0) { h->health = 0; }

    if (oldhealth <= 0) {
        // har has no health left and is left only with endurance.
        // one hit will end them
        h->endurance = 0;
    } else {
        h->endurance -= damage * 8;
        if(h->endurance <= 0) {
            if (h->state == STATE_STUNNED) {
                // refill endurance
                h->endurance = h->endurance_max;
            } else {
                h->endurance = 0;
            }
        }
    }

    // chronos' stasis does not have a hit animation
    if (string->data) {
        sd_stringparser_frame f;
        const sd_stringparser_tag_value *v;
        h->state = STATE_RECOIL;
        // Set hit animation
        object_set_animation(obj, &af_get_move(h->af_data, ANIM_DAMAGE)->ani);
        object_set_repeat(obj, 0);
        if (h->health <= 0 && h->endurance <= 0) {
            // taken from MASTER.DAT
            // XXX changed the last frame to 200 ticks to ensure the HAR falls down
            char *final = "-x-20ox-20L1-ox-20L2-x-20zzs4l25sp13M1-zzM200";
            char *str = malloc(str_size(string) + strlen(final) + 1);
            // append the 'final knockback' string to the hit string, replacing the final frame
            sprintf(str, "%s", string->data);
            char *last = strrchr(str, '-');
            sprintf(last, "%s", final);
            object_set_custom_string(obj, str);
            free(str);
        } else {
            object_set_custom_string(obj, str_c(string));
        }
        object_tick(obj);
        h->flinching = 1;
        // XXX hack - if the first frame has the 'k' tag, treat it as some vertical knockback
        // we can't do this in player.c because it breaks the jaguar leap, which also uses the 'k' tag.
        sd_stringparser_peek(obj->animation_state.parser, 0, &f);
        sd_stringparser_get_tag(f.parser, f.id, "k", &v);
        if (v->is_set) {
                obj->vel.y -= 7;
        }
    }
}
void har_spawn_oil(object *obj, vec2i pos, int amount, float gravity, int layer) {
    float rv = 0.0f;
    float velx, vely;
    har *h = object_get_userdata(obj);

    // burning oil
    for(int i = 0; i < amount; i++) {
        // Calculate velocity etc.
        rv = rand_int(100) / 100.0f - 0.5;
        velx = (5 * cos(90 + i-(amount) / 2 + rv)) * object_get_direction(obj);
        vely = -12 * sin(i / amount + rv);

        // Make sure scrap has somekind of velocity
        // (to prevent floating scrap objects)
        if(vely < 0.1 && vely > -0.1) vely += 0.21;

        // Create the object
        object *scrap = malloc(sizeof(object));
        int anim_no = ANIM_BURNING_OIL;
        object_create(scrap, obj->gs, pos, vec2f_create(velx, vely));
        object_set_animation(scrap, &af_get_move(h->af_data, anim_no)->ani);
        object_set_stl(scrap, object_get_stl(obj));
        object_set_gravity(scrap, gravity);
        object_set_layers(scrap, LAYER_SCRAP);
        object_tick(scrap);
        scrap_create(scrap);
        game_state_add_object(obj->gs, scrap, layer);
    }

}

// TODO: This is kind of a hack. It's used to check if either 
// HAR is doing destruction. If there is any way to do this better,
// this should be changed.
int is_destruction(game_state *gs) {
    object *obj_a = game_state_get_player(gs, 0)->har;
    object *obj_b = game_state_get_player(gs, 1)->har;
    har *har_a = object_get_userdata(obj_a);
    har *har_b = object_get_userdata(obj_b);
    af_move *move_a = af_get_move(har_a->af_data, obj_a->cur_animation->id);
    af_move *move_b = af_get_move(har_b->af_data, obj_b->cur_animation->id);
    return (move_a->category == CAT_DESTRUCTION || move_b->category == CAT_DESTRUCTION);
}

void har_spawn_scrap(object *obj, vec2i pos, int amount) {
    float rv = 0.0f;
    float velx, vely;
    // wild ass guess
    int oil_amount = amount / 3;
    har *h = object_get_userdata(obj);
    har_spawn_oil(obj, pos, oil_amount, 1, RENDER_LAYER_TOP);

    // scrap metal
    // TODO this assumes the default scrap level and does not consider BIG[1-9]
    int scrap_amount = 0;
    int destr = is_destruction(obj->gs);
    if(destr) {
        scrap_amount = 30;
    } else if (amount > 11 && amount < 14) {
        scrap_amount = 1;
    } else if (amount > 13 && amount < 16) {
        scrap_amount = 2;
    } else if (amount > 15) {
        scrap_amount = 3;
    }
    for(int i = 0; i < scrap_amount; i++) {
        // Calculate velocity etc.
        rv = rand_int(100) / 100.0f - 0.5;
        velx = (5 * cos(90 + i-(scrap_amount) / 2 + rv)) * object_get_direction(obj);
        vely = -12 * sin(i / scrap_amount + rv);

        // Make destruction moves look more impressive :P
        if(destr) {
            velx *= 5;
            vely *= 5;
        }

        // Make sure scrap has somekind of velocity
        // (to prevent floating scrap objects)
        if(vely < 0.1 && vely > -0.1) vely += 0.21;

        // Create the object
        object *scrap = malloc(sizeof(object));
        int anim_no = rand_int(3) + ANIM_SCRAP_METAL;
        object_create(scrap, obj->gs, pos, vec2f_create(velx, vely));
        object_set_animation(scrap, &af_get_move(h->af_data, anim_no)->ani);
        object_set_stl(scrap, object_get_stl(obj));
        object_set_gravity(scrap, 1);
        object_set_pal_offset(scrap, object_get_pal_offset(obj));
        object_set_layers(scrap, LAYER_SCRAP);
        object_tick(scrap);
        object_set_shadow(scrap, 1);
        scrap_create(scrap);
        game_state_add_object(obj->gs, scrap, RENDER_LAYER_TOP);
    }

}

void har_block(object *obj, vec2i hit_coord) {
    har *h = obj->userdata;
    if (h->state == STATE_WALKFROM) {
        object_set_animation(obj, &af_get_move(h->af_data, ANIM_STANDING_BLOCK)->ani);
    } else {
        object_set_animation(obj, &af_get_move(h->af_data, ANIM_CROUCHING_BLOCK)->ani);
    }
    // the HARs have a lame blank frame in their animation string, so use a custom one
    object_set_custom_string(obj, "A5");
    object_set_repeat(obj, 0);
    object_tick(obj);
    // blocking spark
    if (h->damage_received) {
        // don't make another scrape
        return;
    }
    object *scrape = malloc(sizeof(object));
    object_create(scrape, obj->gs, hit_coord, vec2f_create(0, 0));
    object_set_animation(scrape, &af_get_move(h->af_data, ANIM_BLOCKING_SCRAPE)->ani);
    object_set_stl(scrape, object_get_stl(obj));
    object_set_direction(scrape, object_get_direction(obj));
    object_set_repeat(scrape, 0);
    object_set_gravity(scrape, 0);
    object_set_layers(scrape, LAYER_SCRAP);
    object_tick(scrape);
    object_tick(scrape);
    game_state_add_object(obj->gs, scrape, RENDER_LAYER_MIDDLE);
    h->damage_received = 1;
    h->flinching = 1;
}

void har_check_closeness(object *obj_a, object *obj_b) {
    vec2i pos_a = object_get_pos(obj_a);
    vec2i pos_b = object_get_pos(obj_b);
    har *a = object_get_userdata(obj_a);
    har *b = object_get_userdata(obj_b);
    int hard_limit = 35; // Push opponent if HARs too close. Harrison-Stetson method value.
    int soft_limit = 45; // Sets HAR A as being close to HAR B if closer than this.

    if (b->state == STATE_RECOIL || a->state == STATE_RECOIL || b->state == STATE_JUMPING || a->state == STATE_JUMPING || a->state == STATE_FALLEN || b->state == STATE_FALLEN) {
        return;
    }

    // Reset closeness state
    a->close = 0;
    a->hard_close = 0;

    // If HARs get too close together, handle it
    if(har_is_walking(a) && object_get_direction(obj_a) == OBJECT_FACE_LEFT) {
        if(pos_a.x < pos_b.x + hard_limit && pos_a.x > pos_b.x) {
            // don't allow hars to overlap in the corners
            if (pos_b.x > 15) {
                pos_b.x = pos_a.x - hard_limit;
                object_set_pos(obj_b, pos_b);
            } else {
                pos_a.x = pos_b.x + hard_limit;
                object_set_pos(obj_a, pos_a);
            }
            a->hard_close = 1;
        }
        if(pos_a.x < pos_b.x + soft_limit && pos_a.x > pos_b.x) {
            if (b->state == STATE_STANDING || b->state == STATE_STUNNED || har_is_walking(b) || har_is_crouching(b)) {
                a->close = 1;
            }
            a->hard_close = 1;
        }
    }
    if(har_is_walking(a) && object_get_direction(obj_a) == OBJECT_FACE_RIGHT) {
        if(pos_a.x + hard_limit > pos_b.x && pos_a.x < pos_b.x) {
            // don't allow hars to overlap in the corners
            if (pos_b.x < 305) {
                pos_b.x = pos_a.x + hard_limit;
                object_set_pos(obj_b, pos_b);
            } else {
                pos_a.x = pos_b.x - hard_limit;
                object_set_pos(obj_a, pos_a);
            }
            a->hard_close = 1;
        }
        if(pos_a.x + soft_limit > pos_b.x && pos_a.x < pos_b.x) {
            if (b->state == STATE_STANDING || b->state == STATE_STUNNED || har_is_walking(b) || har_is_crouching(b)) {
                a->close = 1;
            }
            a->hard_close = 1;
        }
    }
}
{'fraction_non_alphanumeric': 0.12903225806451613, 'fraction_numerical': 0.013415080012374497, 'mean_word_length': 2.137009263343626, 'pattern_counts': {'":': 0, '<': 53, '<?xml version=': 0, '>': 330, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6850514', 'n_tokens_mistral': 13016, 'n_tokens_neox': 12318, 'n_words': 2771}