diff --git a/flight.rdbg b/flight.rdbg index 5da8056..f805329 100644 Binary files a/flight.rdbg and b/flight.rdbg differ diff --git a/gamestate.c b/gamestate.c index 1cca247..ea211ad 100644 --- a/gamestate.c +++ b/gamestate.c @@ -40,6 +40,12 @@ static cpVect v2_to_cp(V2 v) return cpv(v.x, v.y); } +bool is_burning(Entity *missile) +{ + assert(missile->is_missile); + return missile->time_burned_for < MISSILE_BURN_TIME; +} + bool was_entity_deleted(GameState *gs, EntityID id) { if (id.generation == 0) @@ -87,11 +93,11 @@ bool is_cloaked(GameState *gs, Entity *e, Entity *this_players_perspective) bool cloaked = cloaking_active(gs, e); if (e->is_player) { - return cloaked && e->presenting_squad != this_players_perspective->presenting_squad; + return cloaked && e->owning_squad != this_players_perspective->owning_squad; } else { - return cloaked && this_players_perspective->presenting_squad != e->last_cloaked_by_squad; + return cloaked && this_players_perspective->owning_squad != e->last_cloaked_by_squad; } } @@ -197,6 +203,75 @@ void box_remove_from_boxes(GameState *gs, Entity *box) box->prev_box = (EntityID){0}; } +V2 player_vel(GameState *gs, Entity *e); +V2 entity_vel(GameState *gs, Entity *e) +{ + assert(e->is_box || e->is_player || e->is_grid || e->is_explosion); + if (e->is_box) + return box_vel(e); + if (e->is_player) + return player_vel(gs, e); + if (e->is_grid) + return grid_vel(e); + if (e->is_explosion) + return e->explosion_vel; + assert(false); + return (V2){0}; +} + +static THREADLOCAL float to_face = 0.0f; +static THREADLOCAL float nearest_dist = INFINITY; +static THREADLOCAL bool target_found = false; +static void on_missile_shape(cpShape *shape, cpContactPointSet *points, void *data) +{ + Entity *launcher = (Entity *)data; + Entity *other = cp_shape_entity(shape); + GameState *gs = entitys_gamestate(launcher); + assert(other->is_box || other->is_player || other->is_missile); + + V2 to = V2sub(entity_pos(other), entity_pos(launcher)); + bool should_attack = true; + if (other->is_box && box_grid(other) == box_grid(launcher)) + should_attack = false; + if (other->owning_squad == launcher->owning_squad) + should_attack = false; + + if (should_attack && V2length(to) < nearest_dist) + { + target_found = true; + nearest_dist = V2length(to); + + // lookahead by their velocity + V2 rel_velocity = V2sub(entity_vel(gs, other), entity_vel(gs, launcher)); + float dist = V2dist(entity_pos(other), entity_pos(launcher)); + + float time_of_travel = sqrtf( (2.0f * dist) / (MISSILE_BURN_FORCE/MISSILE_MASS) ); + + V2 other_future_pos = V2add(entity_pos(other), V2scale(rel_velocity, time_of_travel)); + + V2 adjusted_to = V2sub(other_future_pos, entity_pos(launcher)); + + to_face = V2angle(adjusted_to); + } +} + +LauncherTarget missile_launcher_target(GameState *gs, Entity *launcher) +{ + to_face = 0.0f; + cpBody *tmp = cpBodyNew(0.0f, 0.0f); + cpBodySetPosition(tmp, v2_to_cp(entity_pos(launcher))); + cpShape *circle = cpCircleShapeNew(tmp, MISSILE_RANGE, cpv(0, 0)); + + nearest_dist = INFINITY; + to_face = 0.0f; + target_found = false; + cpSpaceShapeQuery(gs->space, circle, on_missile_shape, (void *)launcher); + + cpBodyFree(tmp); + cpShapeFree(circle); + return (LauncherTarget){.target_found = target_found, .facing_angle = to_face}; +} + void on_entity_child_shape(cpBody *body, cpShape *shape, void *data); // gs is for iterating over all child shapes and destroying those, too @@ -366,6 +441,13 @@ void create_player(Player *player) #endif } +void create_missile(GameState *gs, Entity *e) +{ + create_body(gs, e); + create_rectangle_shape(gs, e, e, (V2){0}, V2scale(MISSILE_COLLIDER_SIZE, 0.5f), PLAYER_MASS); + e->is_missile = true; +} + void create_player_entity(GameState *gs, Entity *e) { e->is_player = true; @@ -566,6 +648,34 @@ static void on_damage(cpArbiter *arb, cpSpace *space, cpDataPointer userData) entity_a = cp_shape_entity(a); entity_b = cp_shape_entity(b); + Entity *potential_missiles[] = {entity_a, entity_b}; + for (Entity **missile_ptr = potential_missiles; missile_ptr - potential_missiles < ARRLEN(potential_missiles); missile_ptr++) + { + Entity *missile = entity_a; + cpVect (*getPointFunc)(const cpArbiter *arb, int i) = NULL; + if (missile == entity_a) + getPointFunc = cpArbiterGetPointA; + if (missile == entity_b) + getPointFunc = cpArbiterGetPointB; + + if (missile->is_missile) + { + int count = cpArbiterGetCount(arb); + for (int i = 0; i < count; i++) + { + cpVect collision_point = getPointFunc(arb, i); + V2 local_collision_point = cp_to_v2(cpBodyWorldToLocal(missile->body, collision_point)); + if (local_collision_point.x > MISSILE_COLLIDER_SIZE.x * 0.2f) + { + missile->damage += MISSILE_DAMAGE_THRESHOLD * 2.0f; + } + } + } + } + + // if(entity_a->is_missile) {getPointFunc = cpArbiterGetPointA; + // if(entity_b->is_missile) getPointFunc = cpArbiterGetPointB; + float damage = V2length(cp_to_v2(cpArbiterTotalImpulse(arb))) * COLLISION_DAMAGE_SCALING; if (entity_a->is_box && entity_a->box_type == BoxExplosive) @@ -847,6 +957,7 @@ enum GameVersion { VInitial, VMoreBoxes, + VMissileMerge, VMax, // this minus one will be the version used }; @@ -989,13 +1100,19 @@ SerMaybeFailure ser_entity(SerState *ser, GameState *gs, Entity *e) if (ser->version >= VMoreBoxes && !ser->save_or_load_from_disk) SER_VAR(&e->time_was_last_cloaked); + if (ser->version >= VMissileMerge) + SER_VAR(&e->owning_squad); + SER_VAR(&e->is_player); if (e->is_player) { SER_ASSERT(e->no_save_to_disk); SER_MAYBE_RETURN(ser_entityid(ser, &e->currently_inside_of_box)); - SER_VAR(&e->presenting_squad); + if (ser->version < VMissileMerge) + { + SER_VAR_NAME(&e->owning_squad, "&e->presenting_squad"); + } SER_VAR(&e->squad_invited_to); SER_VAR(&e->goldness); } @@ -1015,6 +1132,15 @@ SerMaybeFailure ser_entity(SerState *ser, GameState *gs, Entity *e) SER_MAYBE_RETURN(ser_entityid(ser, &e->boxes)); } + if (ser->version >= VMissileMerge) + { + SER_VAR(&e->is_missile) + if (e->is_missile) + { + SER_VAR(&e->time_burned_for); + } + } + SER_VAR(&e->is_box); if (e->is_box) { @@ -1058,6 +1184,9 @@ SerMaybeFailure ser_entity(SerState *ser, GameState *gs, Entity *e) case BoxCloaking: SER_VAR(&e->cloaking_power); break; + case BoxMissileLauncher: + SER_VAR(&e->missile_construction_charge); + break; default: break; } @@ -1799,7 +1928,7 @@ void process(GameState *gs, float dt) } } assert(p->is_player); - p->presenting_squad = player->squad; + p->owning_squad = player->squad; if (p->squad_invited_to != SquadNone) { @@ -2077,6 +2206,24 @@ void process(GameState *gs, float dt) } } + if (e->is_missile) + { + if (is_burning(e)) + { + e->time_burned_for += dt; + cpBodyApplyForceAtWorldPoint(e->body, v2_to_cp(V2rotate((V2){.x = MISSILE_BURN_FORCE, .y = 0.0f}, entity_rotation(e))), v2_to_cp(entity_pos(e))); + } + if (e->damage >= MISSILE_DAMAGE_THRESHOLD) + { + + Entity *explosion = new_entity(gs); + explosion->is_explosion = true; + explosion->explosion_pos = entity_pos(e); + explosion->explosion_vel = cp_to_v2(cpBodyGetVelocity(e->body)); + entity_destroy(gs, e); + } + } + if (e->is_box) { if (e->is_platonic) @@ -2194,6 +2341,29 @@ void process(GameState *gs, float dt) cpBodyFree(tmp); } } + if (cur_box->box_type == BoxMissileLauncher) + { + LauncherTarget target = missile_launcher_target(gs, cur_box); + + if (cur_box->missile_construction_charge < 1.0f) + { + float want_use_energy = dt * MISSILE_CHARGE_RATE; + float energy_charged = want_use_energy - batteries_use_energy(gs, grid, &non_battery_energy_left_over, want_use_energy); + cur_box->missile_construction_charge += energy_charged; + } + + if (target.target_found && cur_box->missile_construction_charge >= 1.0f) + { + cur_box->missile_construction_charge = 0.0f; + Entity *new_missile = new_entity(gs); + create_missile(gs, new_missile); + new_missile->owning_squad = cur_box->owning_squad; // missiles have teams and attack eachother! + float missile_spawn_dist = sqrtf((BOX_SIZE / 2.0f) * (BOX_SIZE / 2.0f) * 2.0f) + MISSILE_COLLIDER_SIZE.x / 2.0f + 0.1f; + cpBodySetPosition(new_missile->body, v2_to_cp(V2add(entity_pos(cur_box), V2rotate((V2){.x = missile_spawn_dist, 0.0f}, target.facing_angle)))); + cpBodySetAngle(new_missile->body, target.facing_angle); + cpBodySetVelocity(new_missile->body, v2_to_cp(box_vel(cur_box))); + } + } if (cur_box->box_type == BoxScanner) { // set the nearest platonic solid! only on server as only the server sees everything diff --git a/loaded/missile.png b/loaded/missile.png new file mode 100644 index 0000000..d3c7ede Binary files /dev/null and b/loaded/missile.png differ diff --git a/loaded/missile_burning.png b/loaded/missile_burning.png new file mode 100644 index 0000000..d3fe210 Binary files /dev/null and b/loaded/missile_burning.png differ diff --git a/loaded/missile_launcher.png b/loaded/missile_launcher.png new file mode 100644 index 0000000..97522d0 Binary files /dev/null and b/loaded/missile_launcher.png differ diff --git a/main.c b/main.c index 31bdc07..f6d239c 100644 --- a/main.c +++ b/main.c @@ -111,6 +111,8 @@ static sg_image image_solarpanel_charging; static sg_image image_scanner_head; static sg_image image_itemswitch; static sg_image image_cloaking_panel; +static sg_image image_missile; +static sg_image image_missile_burning; static enum BoxType toolbar[TOOLBAR_SLOTS] = { BoxHullpiece, @@ -191,12 +193,15 @@ static struct BoxInfo .type = BoxCloaking, .image_path = "loaded/cloaking_device.png", }, + { + .type = BoxMissileLauncher, + .image_path = "loaded/missile_launcher.png", + }, }; #define ENTITIES_ITER(cur) \ for (Entity *cur = gs.entities; cur < gs.entities + gs.cur_next_entity; \ cur++) \ if (cur->exists) -#define ARRLEN(arr) (sizeof(arr) / sizeof(*arr)) // suppress compiler warning about ^^ above used in floating point context #define ARRLENF(arr) ((float)sizeof(arr) / sizeof(*arr)) static struct SquadMeta @@ -534,6 +539,8 @@ static void init(void) image_scanner_head = load_image("loaded/scanner_head.png"); image_itemswitch = load_image("loaded/itemswitch.png"); image_cloaking_panel = load_image("loaded/cloaking_panel.png"); + image_missile_burning = load_image("loaded/missile_burning.png"); + image_missile = load_image("loaded/missile.png"); } // socket initialization @@ -1793,7 +1800,7 @@ static void frame(void) // all of these box types show team colors so are drawn with the hue shifting shader // used with the player - if (b->box_type == BoxCloaking) + if (b->box_type == BoxCloaking || b->box_type == BoxMissileLauncher) { pipeline_scope(hueshift_pipeline) { @@ -1808,6 +1815,23 @@ static void frame(void) } sgp_reset_image(0); + if (b->box_type == BoxMissileLauncher) + { + set_color(RED); + draw_circle(entity_pos(b), MISSILE_RANGE); + + // draw the charging missile + transform_scope + { + sgp_rotate_at(missile_launcher_target(&gs, b).facing_angle, entity_pos(b).x, entity_pos(b).y); + sgp_set_color(1.0f, 1.0f, 1.0f, b->missile_construction_charge); + sgp_set_image(0, image_missile); + pipeline_scope(goodpixel_pipeline) + draw_texture_centered(entity_pos(b), BOX_SIZE); + sgp_reset_image(0); + } + } + if (b->box_type == BoxScanner) { sgp_set_image(0, image_scanner_head); @@ -1823,6 +1847,7 @@ static void frame(void) set_color(WHITE); } + // scanner range, visualizes what scanner can scan if (b->box_type == BoxScanner) { set_color(BLUE); @@ -1857,6 +1882,28 @@ static void frame(void) } } + // draw missile + if (e->is_missile) + { + transform_scope + { + sgp_rotate_at(entity_rotation(e), entity_pos(e).x, entity_pos(e).y); + sgp_set_color(1.0f, 1.0f, 1.0f, 1.0f); + + if (is_burning(e)) + { + sgp_set_image(0, image_missile_burning); + } + else + { + sgp_set_image(0, image_missile); + } + + pipeline_scope(goodpixel_pipeline) + draw_texture_rectangle_centered(entity_pos(e), MISSILE_SPRITE_SIZE); + } + } + // draw player if (e->is_player && get_entity(&gs, e->currently_inside_of_box) == NULL) @@ -1868,7 +1915,7 @@ static void frame(void) pipeline_scope(hueshift_pipeline) { - setup_hueshift(e->presenting_squad); + setup_hueshift(e->owning_squad); sgp_set_image(0, image_player); draw_texture_rectangle_centered( entity_pos(e), V2scale(PLAYER_SIZE, player_scaling)); diff --git a/tooling.ahk b/tooling.ahk index f5d8c5a..77cf1f3 100644 --- a/tooling.ahk +++ b/tooling.ahk @@ -14,7 +14,7 @@ WinActivate, flightbuild If WinActive("flightbuild") { Send, {Enter} - Send, remedybg continue-execution && sleep 0.1 && remedybg.exe stop-debugging && msbuild && remedybg.exe start-debugging {Enter} + Send, remedybg continue-execution && timeout 1 && remedybg.exe stop-debugging && msbuild && remedybg.exe start-debugging {Enter} } return @@ -25,6 +25,6 @@ WinActivate, flightbuild If WinActive("flightbuild") { Send, {Enter} - Send, remedybg continue-execution && sleep 0.1 && remedybg.exe stop-debugging && msbuild && remedybg.exe start-debugging && sleep 0.2 && x64\Debug\Flight.exe {Enter} + Send, remedybg continue-execution && timeout 1 && remedybg.exe stop-debugging && msbuild && remedybg.exe start-debugging && sleep 0.2 && x64\Debug\Flight.exe {Enter} } return \ No newline at end of file diff --git a/types.h b/types.h index 67f2473..c3391f9 100644 --- a/types.h +++ b/types.h @@ -10,6 +10,16 @@ #define PLAYER_MASS 0.5f #define PLAYER_JETPACK_FORCE 1.5f #define PLAYER_JETPACK_TORQUE 0.05f +#define MISSILE_RANGE 4.0f +#define MISSILE_BURN_TIME 1.5f +#define MISSILE_BURN_FORCE 2.0f +#define MISSILE_MASS 1.0f +// how many missiles grown per second +#define MISSILE_DAMAGE_THRESHOLD 0.2f +#define MISSILE_CHARGE_RATE 0.5f +// centered on the sprite +#define MISSILE_SPRITE_SIZE ((V2){.x = BOX_SIZE, .y = BOX_SIZE}) +#define MISSILE_COLLIDER_SIZE ((V2){.x = BOX_SIZE*0.5f, .y = BOX_SIZE*0.5f}) // #define PLAYER_JETPACK_FORCE 20.0f // distance at which things become geostationary and no more solar power! #define PLAYER_JETPACK_ROTATION_ENERGY_PER_SECOND 0.2f @@ -80,6 +90,8 @@ #define THREADLOCAL __thread #endif +#define ARRLEN(x) (sizeof(x) / sizeof((x)[0])) + // must make this header and set the target address, just #define SERVER_ADDRESS "127.0.0.1" #include "ipsettings.h" // don't leak IP! @@ -154,6 +166,7 @@ enum BoxType BoxScanner, BoxGyroscope, BoxCloaking, + BoxMissileLauncher, BoxLast, }; @@ -236,7 +249,7 @@ typedef struct Entity // player bool is_player; - enum Squad presenting_squad; // also controls what the player can see, because of cloaking! + enum Squad owning_squad; // also controls what the player can see, because of cloaking! EntityID currently_inside_of_box; enum Squad squad_invited_to; // if squad none, then no squad invite float goldness; // how much the player is a winner @@ -247,6 +260,10 @@ typedef struct Entity V2 explosion_vel; float explosion_progresss; // in seconds + // missile + bool is_missile; + float time_burned_for; // until MISSILE_BURN_TIME + // grids bool is_grid; float total_energy_capacity; @@ -254,7 +271,6 @@ typedef struct Entity // boxes bool is_box; - enum Squad owning_squad; // which squad owns this box enum BoxType box_type; bool is_platonic; // can't be destroyed, unaffected by physical forces bool always_visible; // always serialized to the player. @Robust check if not used @@ -263,6 +279,9 @@ typedef struct Entity enum CompassRotation compass_rotation; bool indestructible; + // missile launcher + float missile_construction_charge; + // used by medbay and cockpit EntityID player_who_is_inside_of_me; @@ -401,6 +420,7 @@ bool client_to_server_deserialize(GameState *gs, struct ClientToServer *msg, uns bool client_to_server_serialize(GameState *gs, struct ClientToServer *msg, unsigned char*bytes, size_t *out_len, size_t max_len); // entities +bool is_burning(Entity *missile); Entity *get_entity(struct GameState *gs, EntityID id); Entity *new_entity(struct GameState *gs); EntityID get_id(struct GameState *gs, Entity *e); @@ -412,6 +432,12 @@ void entity_ensure_in_orbit(Entity *e); void entity_destroy(GameState *gs, Entity *e); #define BOX_CHAIN_ITER(gs, cur, starting_box) for (Entity *cur = get_entity(gs, starting_box); cur != NULL; cur = get_entity(gs, cur->next_box)) #define BOXES_ITER(gs, cur, grid_entity_ptr) BOX_CHAIN_ITER(gs, cur, (grid_entity_ptr)->boxes) +typedef struct LauncherTarget +{ + bool target_found; + float facing_angle; // in global coords +} LauncherTarget; +LauncherTarget missile_launcher_target(GameState *gs, Entity *launcher); // grid void grid_create(struct GameState *gs, Entity *e);