#include static void world_assign_new_job_offers(world* world, bool force); static double distance_between_location(world_location* location1, world_location* location2); static void world_payout_salaries(world* world); static void enable_insights_for_current_month(world* world); static vec2f get_world_location_for_job(platform_window* window, world* world, active_job* job); static employee* get_employee_by_id(world_location* location, u32 id); static void end_contract_with_employee(world* world, employee* emp); vec2f px_to_coords(platform_window* window, double x, double y); static bool world_check_location_accessibility(world_location* orig, world_location* source, int depth, double dist); static void world_update_location_scores(world* world); float dotsize = 5; static s32 get_random_number(s32 min, s32 max) { log_assert(min <= max, "Min cannot be larger than max"); // it is assumed srand has been initialized at world load. if (min == max) return min; return min + rand() % (max-min); } void world_report_event_ex(world* world, char* msg, event_type type, void* data, scheduled_job_time scheduled_time) { event new_event; new_event.data = data; new_event.type = type; new_event.job_time = scheduled_time; char txt_buf[50]; struct tm* time = gmtime(&world->simulation_time); strftime(txt_buf, 50, "%H:%M %d/%m/%Y", time); snprintf(new_event.message, MAX_EVENT_MESSAGE_LENGTH, "[%s] %s", txt_buf, msg); if (world->log.events.length < LOG_HISTORY_LENGTH) { array_push(&world->log.events, &new_event); world->log.write_cursor++; world->log.has_unread_messages = true; } else { if (world->log.write_cursor >= LOG_HISTORY_LENGTH) world->log.write_cursor = 0; u16 write_location = world->log.write_cursor; world->log.write_cursor++; event* e = array_at(&world->log.events, write_location); *e = new_event; world->log.has_unread_messages = true; } if (type != EVENT_TYPE_INFO) audio_play_sound(snd_event, AUDIO_CHANNEL_SFX_2); } void world_report_event(world* world, char* msg, event_type type, void* data) { world_report_event_ex(world, msg, type, data, (scheduled_job_time){0,0,0,0}); } static job_endpoints job_offer_get_endpoints(job_offer* offer) { world_location* source = *(world_location**)array_at(&offer->connections, 0); world_location* dest = *(world_location**)array_at(&offer->connections, offer->connections.length-1); return (job_endpoints){source,dest}; } static s32 employee_calculate_happiness_stars(employee* emp) { s32 stars = emp->happiness / 0.2f; if (stars > 5) stars = 0; return stars; } static world_location world_create_location(u8 size, double latitude, double longitude, char* name, bool is_owned) { world_location location; location.size = size; location.latitude = latitude; location.longitude = longitude; string_copyn(location.name, name, MAX_WORLD_LOCATION_NAME_LENGTH); location.is_owned = is_owned; location.connections = array_create(sizeof(world_location*)); location.employees = array_create(sizeof(employee*)); array_reserve(&location.employees, MAX_EMPLOYEE_COUNT); location.job_offers = array_create(sizeof(job_offer)); array_reserve(&location.job_offers, MAX_JOBOFFER_COUNT); location.trucks = array_create(sizeof(truck)); array_reserve(&location.trucks, MAX_TRUCK_COUNT); location.schedule.jobs = array_create(sizeof(scheduled_job)); array_reserve(&location.schedule.jobs, TIME_SLOTS_PER_DAY*NUM_DAYS); location.insights = array_create(sizeof(money_data_collection)); location.resumes = array_create(sizeof(resume)); location.id = assets_hash_path(location.name); location.score = 0.0f; return location; } static world_location* world_get_location_by_name(world* world, char* str) { for (s32 i = 0; i < world->locations.length; i++) { world_location* source = array_at(&world->locations, i); if (strcmp(source->name, str) == 0) return source; } return 0; } static void connect_locations(world* world, char* str1, char* str2) { world_location* loc1 = world_get_location_by_name(world, str1); world_location* loc2 = world_get_location_by_name(world, str2); array_push(&loc1->connections, &loc2); } static void world_create_manual_connections(world* world) { connect_locations(world, "Minsk", "Warsaw"); connect_locations(world, "Moscow", "Volgograd"); connect_locations(world, "Volgograd", "Baku"); connect_locations(world, "Mexico City", "Tuxtla GutiĆ©rrez"); connect_locations(world, "Sofia", "Istanbul"); connect_locations(world, "Istanbul", "Van"); connect_locations(world, "Tabriz", "Tehran"); connect_locations(world, "Baghdad", "Tehran"); connect_locations(world, "Baghdad", "Rutba"); connect_locations(world, "Tehran", "Dushanbe"); connect_locations(world, "Tabriz", "Baku"); connect_locations(world, "Kuwait", "Dammam"); connect_locations(world, "Cairo", "Amman"); } static void world_create_connections(world* world) { world_create_manual_connections(world); double max_distance = 410; for (s32 i = 0; i < world->locations.length; i++) { world_location* source = array_at(&world->locations, i); if (source->longitude < -90) max_distance = 600; else max_distance = 410; double closest = FLT_MAX; world_location* closest_loc = 0; for (s32 d = 0; d < world->locations.length; d++) { world_location* destination = array_at(&world->locations, d); if (destination == source) continue; double total_dist = distance_between_location(source, destination); #if 0 printf("%s %s -> %f\n", source->name, destination->name, distance_between_location(source, destination)); #endif if (total_dist < max_distance) { array_push(&source->connections, &destination); } else { if (total_dist < closest) { closest = total_dist; closest_loc = destination; } } } } } world_location* get_world_location_by_id(world* world, s32 id) { for (s32 i = 0; i < world->locations.length; i++) { world_location* source = array_at(&world->locations, i); if (source->id == id) return source; } return 0; } world_location* get_world_location_by_name(world* world, char* str) { for (s32 i = 0; i < world->locations.length; i++) { world_location* source = array_at(&world->locations, i); if (strcmp(source->name, str) == 0) return source; } return 0; } void add_truck_to_world_location(world* world, world_location* location, truck* tr) { log_assert(location->trucks.length < MAX_TRUCK_COUNT, "Too many trucks"); tr->id = world->next_id++; array_push(&location->trucks, tr); } void add_employee_to_world_location(world_location* location, employee* employee) { log_assert(location->employees.length < MAX_EMPLOYEE_COUNT, "Too many employees"); employee->current_location_id = location->id; array_push(&location->employees, &employee); } static employee* create_employee(world* world, world_location* hired_at) { employee* employee1 = mem_alloc(sizeof(employee)); char* firstname = array_at(&world->firstnames, get_random_number(0, world->firstnames.length)); char* lastname = array_at(&world->lastnames, get_random_number(0, world->lastnames.length)); snprintf(employee1->name, MAX_EMPLOYEE_NAME_LENGTH, "%s %s", firstname, lastname); employee1->age = get_random_number(18, 60); employee1->hire_date = world->current_time; employee1->days_below_happiness_treshold = 0; employee1->experience = (float)((employee1->age - 18.0f) * (get_random_number(1, 100)/100.0f)); employee1->salary = BASE_PAY + (employee1->experience * RAISE_PER_YEAR); if (employee1->salary > MAX_PAY) employee1->salary = MAX_PAY; employee1->happiness = 1.0f; employee1->original_location_id = hired_at->id; employee1->current_location_id = INVALID_ID; employee1->assigned_truck = 0; employee1->active_job_id = INVALID_ID; // Portrait generation. { employee1->portrait_hair_type = get_random_number(0, PORTRAIT_MAX_HAIR_COUNT); employee1->hair_color = hair_palette[get_random_number(0, (sizeof(hair_palette)/sizeof(color)))]; employee1->face_color = skin_palette[get_random_number(0, (sizeof(skin_palette)/sizeof(color)))]; employee1->body_color = body_palette[get_random_number(0, (sizeof(body_palette)/sizeof(color)))]; } return employee1; } static bool world_create_default_state(world* world) { world_location* start_location = get_world_location_by_name(world, "Maastricht"); if (!start_location) return false; start_location->is_owned = true; start_location->purchase_year = world->current_time.tm_year; employee* employee1 = create_employee(world, start_location); employee1->id = world->next_id++; add_employee_to_world_location(start_location, employee1); employee* employee2 = create_employee(world, start_location); employee2->id = world->next_id++; add_employee_to_world_location(start_location, employee2); employee* employee3 = create_employee(world, start_location); employee3->id = world->next_id++; add_employee_to_world_location(start_location, employee3); truck_dealer* dealer = array_at(&world->truck_dealers, 0); truck* tr = array_at(&dealer->trucks, 0); add_truck_to_world_location(world, start_location, tr); add_truck_to_world_location(world, start_location, tr); add_truck_to_world_location(world, start_location, tr); { truck* t1 = array_at(&start_location->trucks, 0); truck* t2 = array_at(&start_location->trucks, 1); t1->assigned_employee = employee1; employee1->assigned_truck = t1; t2->assigned_employee = employee2; employee2->assigned_truck = t2; } return true; } static bool world_load_boat_routes_from_file(world* world) { world->boat_routes = array_create(sizeof(boat_route)); file_content locations_file = platform_read_file_content("data/world/boat-routes.json", "rb"); if (locations_file.file_error) return false; cJSON *json_object = cJSON_Parse(locations_file.content); if (!json_object) return false; cJSON *route; cJSON_ArrayForEach(route, json_object) { boat_route new_route; memset(&new_route, 0, sizeof(new_route)); boat_route_point new_point = (boat_route_point){0.0f,0.0f}; s32 index = 0; cJSON *route_point; cJSON_ArrayForEach(route_point, route) { if (index % 2 == 0) { new_point.x = route_point->valuedouble; } else { new_point.y = route_point->valuedouble; new_route.points[new_route.count++] = new_point; } index++; } array_push(&world->boat_routes, &new_route); } cJSON_Delete(json_object); platform_destroy_file_content(&locations_file); return true; } static bool world_load_companies_from_file(world* world) { world->companies = array_create(sizeof(company)); file_content locations_file = platform_read_file_content("data/world/companies.json", "rb"); if (locations_file.file_error) return false; cJSON *json_object = cJSON_Parse(locations_file.content); if (!json_object) return false; cJSON *country_entry; cJSON_ArrayForEach(country_entry, json_object) { company new_company; new_company.products = array_create(sizeof(product)); cJSON* name = cJSON_GetObjectItem(country_entry, "name"); if (!name) return false; cJSON* logo = cJSON_GetObjectItem(country_entry, "logo"); if (!logo) return false; string_copyn(new_company.name, name->valuestring, MAX_PRODUCT_NAME_LENGTH); new_company.logo = assets_find_image_ref(0, assets_hash_path(logo->valuestring)); if (!new_company.logo) return false; cJSON* products = cJSON_GetObjectItem(country_entry, "products"); if (!products) return false; cJSON *product_entry; cJSON_ArrayForEach(product_entry, products) { char tmp_buf[MAX_PRODUCT_NAME_LENGTH]; string_copyn(tmp_buf, product_entry->valuestring, MAX_PRODUCT_NAME_LENGTH); array_push(&new_company.products, tmp_buf); } array_push(&world->companies, &new_company); } cJSON_Delete(json_object); platform_destroy_file_content(&locations_file); return true; } static bool world_load_lastnames_from_file(world* world) { world->lastnames = array_create(MAX_ENPOLYEE_LASTNAME_LENGTH); array_reserve(&world->lastnames, 5000); file_content firstname_file = platform_read_file_content("data/world/last-names.json", "rb"); if (firstname_file.file_error) return false; cJSON *json_object = cJSON_Parse(firstname_file.content); if (!json_object) return false; cJSON *name_entry; cJSON_ArrayForEach(name_entry, json_object) { char buffer[MAX_ENPOLYEE_LASTNAME_LENGTH]; string_copyn(buffer, name_entry->valuestring, MAX_ENPOLYEE_LASTNAME_LENGTH); array_push(&world->lastnames, buffer); } cJSON_Delete(json_object); platform_destroy_file_content(&firstname_file); return true; } static bool world_load_trucks_from_file(world* world) { world->truck_dealers = array_create(sizeof(truck_dealer)); array_reserve(&world->firstnames, 5); file_content firstname_file = platform_read_file_content("data/world/dealers.json", "rb"); if (firstname_file.file_error) return false; cJSON *json_object = cJSON_Parse(firstname_file.content); if (!json_object) return false; cJSON *dealer_entry; cJSON_ArrayForEach(dealer_entry, json_object) { truck_dealer new_dealer; new_dealer.trucks = array_create(sizeof(truck)); cJSON* name = cJSON_GetObjectItem(dealer_entry, "name"); if (!name) return false; string_copyn(new_dealer.name, name->valuestring, MAX_DEALER_NAME_LENGTH); cJSON* logo = cJSON_GetObjectItem(dealer_entry, "logo"); if (!logo) return false; new_dealer.logo = assets_find_image_ref(0, assets_hash_path(logo->valuestring)); if (!new_dealer.logo) return false; cJSON* trucks = cJSON_GetObjectItem(dealer_entry, "trucks"); cJSON *truck_entry; cJSON_ArrayForEach(truck_entry, trucks) { truck new_truck; new_truck.assigned_employee = 0; cJSON* logo = cJSON_GetObjectItem(truck_entry, "logo"); if (!logo) return false; new_truck.logo = assets_find_image_ref(0, assets_hash_path(logo->valuestring)); if (!new_truck.logo) return false; cJSON* name = cJSON_GetObjectItem(truck_entry, "name"); if (!name) return false; string_copyn(new_truck.name, name->valuestring, MAX_TRUCK_NAME_LENGTH); new_truck.type = assets_hash_path(new_truck.name); cJSON* hp = cJSON_GetObjectItem(truck_entry, "power"); if (!hp) return false; new_truck.hp = hp->valueint; cJSON* price = cJSON_GetObjectItem(truck_entry, "price"); if (!price) return false; new_truck.price = price->valueint; cJSON* fuelcapacity = cJSON_GetObjectItem(truck_entry, "fuelcapacity"); if (!fuelcapacity) return false; new_truck.fuelcapacity = fuelcapacity->valueint; cJSON* torque = cJSON_GetObjectItem(truck_entry, "torque"); if (!torque) return false; new_truck.torque = torque->valueint; cJSON* fuelusage = cJSON_GetObjectItem(truck_entry, "fuelusage"); if (!fuelusage) return false; new_truck.fuelusage = fuelusage->valuedouble; array_push(&new_dealer.trucks, &new_truck); } array_push(&world->truck_dealers, &new_dealer); } cJSON_Delete(json_object); platform_destroy_file_content(&firstname_file); return true; } static bool world_load_firstnames_from_file(world* world) { world->firstnames = array_create(MAX_ENPOLYEE_FIRSTNAME_LENGTH); array_reserve(&world->firstnames, 5000); file_content firstname_file = platform_read_file_content("data/world/first-names.json", "rb"); if (firstname_file.file_error) return false; cJSON *json_object = cJSON_Parse(firstname_file.content); if (!json_object) return false; cJSON *name_entry; cJSON_ArrayForEach(name_entry, json_object) { char buffer[MAX_EMPLOYEE_NAME_LENGTH]; string_copyn(buffer, name_entry->valuestring, MAX_ENPOLYEE_FIRSTNAME_LENGTH); array_push(&world->firstnames, buffer); } cJSON_Delete(json_object); platform_destroy_file_content(&firstname_file); return true; } static bool world_load_locations_from_file(world* world) { world->locations = array_create(sizeof(world_location)); file_content locations_file = platform_read_file_content("data/world/locations.json", "rb"); if (locations_file.file_error) return false; cJSON *json_object = cJSON_Parse(locations_file.content); if (!json_object) return false; cJSON *location_entry; cJSON_ArrayForEach(location_entry, json_object) { cJSON* name = cJSON_GetObjectItem(location_entry, "name"); if (!name) return false; cJSON* size = cJSON_GetObjectItem(location_entry, "size"); if (!size) return false; cJSON* latitude = cJSON_GetObjectItem(location_entry, "latitude"); if (!latitude) return false; cJSON* longitude = cJSON_GetObjectItem(location_entry, "longitude"); if (!longitude) return false; world_location location = world_create_location(size->valueint, latitude->valuedouble, longitude->valuedouble, name->valuestring, false); array_push(&world->locations, &location); } cJSON_Delete(json_object); platform_destroy_file_content(&locations_file); world_create_connections(world); return true; } float world_location_get_price(world_location* location) { float base_price = 300000.0f; return base_price / location->size; } static money_data_collection* get_current_insights_data_for_location(world* world, world_location* location) { s32 index = world->current_time.tm_year - location->purchase_year; if (index >= location->insights.length) { money_data_collection data = {0}; for (s32 i = 0; i < MONTHS_IN_YEAR; i++) { data.months[i].total_income = NAN; } array_push(&location->insights, &data); } return (money_data_collection*)array_at(&location->insights, index); } static money_data_collection* get_current_insights_data(world* world) { s32 index = world->current_time.tm_year - world->start_year; if (index >= world->insights.length) { money_data_collection data = {0}; for (s32 i = 0; i < MONTHS_IN_YEAR; i++) { data.months[i].total_income = NAN; } array_push(&world->insights, &data); } return (money_data_collection*)array_at(&world->insights, index); } world* world_create_new() { world* new_world = mem_alloc(sizeof(world)); new_world->simulation_time = time(NULL); new_world->start_year = gmtime(&new_world->simulation_time)->tm_year; new_world->money = 10000000.0f; new_world->next_id = 1; new_world->active_jobs = array_create(sizeof(active_job)); new_world->investments = (company_investments){0}; new_world->simulation_speed = 1; new_world->days_since_last_random_event = 0;//-365; // No random events in the first year. new_world->log.events = array_create(sizeof(event)); new_world->log.write_cursor = 0; new_world->log.has_unread_messages = false; new_world->bank_info = (bank){0}; new_world->bank_info.loan1 = (bank_loan){10000, 3, 12, 846.94, 0}; new_world->bank_info.loan2 = (bank_loan){50000, 5, 12, 4280.38, 0}; new_world->bank_info.loan3 = (bank_loan){150000, 8, 12, 13048.27, 0}; array_reserve(&new_world->log.events, LOG_HISTORY_LENGTH); new_world->insights = array_create(sizeof(money_data_collection)); array_reserve(&new_world->insights, 10); new_world->insights.reserve_jump = 10; array_reserve(&new_world->active_jobs, 100); srand(new_world->simulation_time); new_world->current_time = *gmtime(&new_world->simulation_time); if (!world_load_locations_from_file(new_world)) { log_info("Failed to load locations from data/world/locations.json"); mem_free(new_world); return 0; } if (!world_load_companies_from_file(new_world)) { log_info("Missing companies in data/world/companies.json"); mem_free(new_world); return 0; } if (!world_load_firstnames_from_file(new_world)) { log_info("Missing names in data/world/first-names.json"); mem_free(new_world); return 0; } if (!world_load_lastnames_from_file(new_world)) { log_info("Missing names in data/world/last-names.json"); mem_free(new_world); return 0; } if (!world_load_trucks_from_file(new_world)) { log_info("Missing names in data/world/dealers.json"); mem_free(new_world); return 0; } if (!world_load_boat_routes_from_file(new_world)) { log_info("Missing names in data/world/boat-routes.json"); mem_free(new_world); return 0; } if (!world_create_default_state(new_world)) { log_info("Could not create world"); mem_free(new_world); return 0; } world_assign_new_job_offers(new_world, true); enable_insights_for_current_month(new_world); world_update_location_scores(new_world); return new_world; } vec2f px_to_coords(platform_window* window, double x, double y) { vec2f extra = {9 * scale, 4 * scale}; float orig_lon = x - extra.x - (area.x/zoom); orig_lon = (orig_lon * 360.0f / area.w - 180.0f); float orig_lat = y - extra.y - (area.y/zoom); orig_lat = (orig_lat * 180.0f / -area.h + 90.0f); vec2f map_pos = {orig_lon, orig_lat}; return map_pos; } static vec2f coords_to_px(platform_window* window, double lon, double lat) { vec2f extra = {9 * scale, 4 * scale}; vec2f map_pos = {area.x/zoom + (float)(area.w * (180.0f + lon) / 360.0f), area.y/zoom + (float)(area.h * (90.0f - lat) / 180.0f)}; map_pos.x += extra.x; map_pos.y += extra.y; return map_pos; } static void world_remove_expired_job_offers(world* world) { for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); for (s32 x = 0; x < location->job_offers.length; x++) { job_offer* offer = array_at(&location->job_offers, x); if (offer->expire_date <= world->simulation_time) { // TODO: only free here when not scheduled! //array_destroy(&offer->connections); array_remove_at(&location->job_offers, x); x--; } } } } bool job_offer_has_ship_day(job_offer* offer, weekday day_to_find) { for (s32 s = 0; s < offer->shipday_count; s++) { weekday day = offer->shipdays[s]; if (day == day_to_find) return true; } return false; } static void world_find_location_deep(s32 depth, world_location* source, array* buf) { retry:; double current_furthest_point = 0.0; world_location* current_source = source; for (int d = 0; d < depth; d++) { for (int i = 0; i < current_source->connections.length; i++) { s32 rand_conn = get_random_number(0, current_source->connections.length); world_location* connection = *(world_location**)array_at(¤t_source->connections, rand_conn); double dist = distance_between_location(connection, source); if (dist < current_furthest_point) continue; current_furthest_point = dist; current_source = connection; array_push(buf, &connection); break; } } if (depth != buf->length) { buf->length = 0; array_push(buf, &source); goto retry; } } static s32 world_get_max_ship_days_for_location(world_location* location) { s32 shipdays = (int)round(MAX_SHIPDAYS*location->score); if (shipdays < MIN_POSSIBLE_SHIPDAYS) shipdays = MIN_POSSIBLE_SHIPDAYS; return shipdays; } static s32 world_get_max_depth_for_location(world_location* location) { s32 max_depth = 6; s32 location_depth = (int)round(max_depth*location->score); if (location_depth < 1) location_depth = 1; return location_depth; } int compare( const void* a, const void* b) { weekday int_a = * ( (int*) a ); weekday int_b = * ( (int*) b ); if ( int_a == int_b ) return 0; else if ( int_a < int_b ) return -1; else return 1; } static void world_assign_new_job_offers(world* world, bool force) { for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; if (location->job_offers.length >= MAX_JOBOFFER_COUNT) continue; float rand_offer_chance = (1/10.0f); // 1 in 10 change of getting a job offer by default rand_offer_chance += ((world->investments.marketing / INVESTMENT_INCREMENT)*0.005f); // 100 eur = 0.5% increase in job offers. if (rand_offer_chance > 0.4f) rand_offer_chance = 0.4f; // Cap investment influence at 40%. rand_offer_chance += (location->score / 4.0f); if (!force && get_random_number(0, 100) > rand_offer_chance*100.0f) continue; s32 company_id = get_random_number(0, world->companies.length); company* company = array_at(&world->companies, company_id); s32 product_id = get_random_number(0, company->products.length); product* product = array_at(&company->products, product_id); job_offer new_offer = (job_offer){world->next_id++, world->simulation_time+DAYS(10), company, product, 0, {DAY_INVALID,DAY_INVALID,DAY_INVALID,DAY_INVALID}, 0}; new_offer.connections = array_create(sizeof(world_location*)); s32 max_shipdays = world_get_max_ship_days_for_location(location); s32 amount_of_shipdays = get_random_number(1, max_shipdays+1); for (s32 s = 0; s < amount_of_shipdays; s++) { s32 random_day = get_random_number(0, NUM_DAYS); if (!job_offer_has_ship_day(&new_offer, random_day)) { new_offer.shipdays[new_offer.shipday_count++] = random_day; } else { s--; } } // Sort for rendering order (because durations might overlap. see #11) qsort(&new_offer.shipdays, amount_of_shipdays, sizeof(int), compare ); s32 max_depth = world_get_max_depth_for_location(location); s32 amount_of_connections = get_random_number(1, max_depth + 1) + 1; // +1 because we add original location to connection list. array_push(&new_offer.connections, &location); world_find_location_deep(amount_of_connections, location, &new_offer.connections); float total_dist = 0.0; for (s32 d = 0; d < new_offer.connections.length-1; d++) { world_location* source = *(world_location**)array_at(&new_offer.connections, d); world_location* dest = *(world_location**)array_at(&new_offer.connections, d+1); total_dist += distance_between_location(source, dest); } new_offer.total_distance = total_dist; new_offer.reward = (u32)((new_offer.total_distance * 2.1) * (1 + (0.2f * (location->score))) * (1 + (get_random_number(0, 10) / 100.0f))); // lets assume most experienced drivers drive at 90km/h double min_duration_hours = (new_offer.total_distance/90.0); double max_diration_hours = min_duration_hours * SHIPTIME_DURATION_MULTIPLIER_MIN; new_offer.duration_sec_min = (time_t)(min_duration_hours * 60 * 60); new_offer.duration_sec_max = (time_t)(max_diration_hours * 60 * 60); // printf("Distance: %.0f, duration between %.2f and %.2f hours.", total_dist, min_duration_hours, max_diration_hours); array_push(&location->job_offers, &new_offer); } } static void world_assign_resumes_to_locations(world* world) { // Expire old ones. for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); for (s32 r = 0; r < location->resumes.length; r++) { resume* resume = array_at(&location->resumes, r); // It is assumed a simulation day is longer than RESUME_FADEOUT_MS // else the animation will be cut short. if (resume->animation.started) { array_remove_at(&location->resumes, r); r--; } if (resume->expire_date <= world->simulation_time) { resume->animation.started = true; resume->hired = false; } } } // Assign new ones. for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; float rand_offer_chance = (1/20.0f); // 1 in 20 change of getting a new resume submission. rand_offer_chance += ((world->investments.human_resources / INVESTMENT_INCREMENT)*0.005f); // 100 eur = 0.5% increase in submissions. if (rand_offer_chance > 0.4f) rand_offer_chance = 0.4f; // Cap investment influence at 40%. rand_offer_chance += (location->score / 4.0f); if (get_random_number(0, 100) > rand_offer_chance*100.0f) continue; resume new_resume; new_resume.animation = animation_create(RESUME_FADEOUT_MS); new_resume.expire_date = world->simulation_time+DAYS(10); new_resume.employee = create_employee(world, location); array_push(&location->resumes, &new_resume); } } static double distance_between_location(world_location* location1, world_location* location2) { double lat1 = location1->latitude; double lat2 = location2->latitude; double lon1 = location1->longitude; double lon2 = location2->longitude; double rlat1 = M_PI*lat1/180; double rlat2 = M_PI*lat2/180; double theta = lon1 - lon2; double rtheta =M_PI*theta/180; double dist = sin(rlat1)*sin(rlat2) +cos(rlat1)* cos(rlat2)*cos(rtheta); dist = acos(dist); dist = dist*180/M_PI; dist = dist*60*1.1515; return dist*1.609344; // Miles to km } static float get_employee_experience_factor(employee* emp) { float experience_factor = emp->experience/MAX_EFFECTIVE_EXPERIENCE; if (experience_factor > 1.0f) experience_factor = 1.0f; return experience_factor; } static float get_shiptime_factor(employee* emp) { float experience_factor = get_employee_experience_factor(emp); float shiptime_factor = (SHIPTIME_DURATION_MULTIPLIER_MIN - (SHIPTIME_DURATION_MULTIPLIER_MIN - SHIPTIME_DURATION_MULTIPLIER_MAX) * experience_factor); return shiptime_factor; } static void world_start_scheduled_job(world* world, scheduled_job* scheduled_job, scheduled_job_time scheduled_time) { if (!scheduled_time.assignee) { scheduled_job->trust -= MISSED_DELIVERY_TRUST_PENALTY; char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "A shipment has been missed because there is no assignee for the timeslot."); world_report_event_ex(world, error_msg, EVENT_TYPE_MISSED_SHIPMENT_NO_ASSIGNEE, scheduled_job, scheduled_time); return; } if (!scheduled_time.assignee->assigned_truck) { scheduled_job->trust -= MISSED_DELIVERY_TRUST_PENALTY; char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s missed a shipment because they do not have a truck.", scheduled_time.assignee->name); world_report_event(world, error_msg, EVENT_TYPE_MISSED_SHIPMENT_NO_TRUCK, scheduled_time.assignee); return; } world_location* orig = *(world_location**)array_at(&scheduled_job->offer.connections, 0); if (scheduled_time.assignee->current_location_id != orig->id) { scheduled_job->trust -= MISSED_DELIVERY_TRUST_PENALTY; char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s missed a shipment because they are not at the right location. (Expected location: %s)", scheduled_time.assignee->name, orig->name); world_report_event_ex(world, error_msg, EVENT_TYPE_MISSED_SHIPMENT_NOT_AT_LOCATION, scheduled_job, scheduled_time); return; } // Remove employee from employee list when current location is external. if (scheduled_time.assignee->current_location_id != scheduled_time.assignee->original_location_id) { world_location* current_loc = get_world_location_by_id(world, scheduled_time.assignee->current_location_id); log_assert(current_loc, "Current location cannot be 0"); employee* emp = get_employee_by_id(current_loc, scheduled_time.assignee->id); if (emp) { array_remove_by(¤t_loc->employees, &emp); } } scheduled_time.assignee->current_location_id = INVALID_ID; scheduled_time.assignee->active_job_id = scheduled_job->offer.id; active_job new_job; new_job.stay_at_destination = scheduled_time.stay_at_destination; new_job.day = scheduled_time.day; new_job.timeslot = scheduled_time.timeslot; new_job.offer = scheduled_job->offer; new_job.assignee = *scheduled_time.assignee; new_job.assigned_truck = *scheduled_time.assignee->assigned_truck; time_t leave_time = world->simulation_time-(world->simulation_time%(15*60)); new_job.left_at = leave_time; float shiptime_factor = get_shiptime_factor(&new_job.assignee); new_job.duration_sec = new_job.offer.duration_sec_min * shiptime_factor; new_job.done_at = new_job.left_at + new_job.duration_sec; new_job.reversed = false; float gasprice = ((new_job.offer.total_distance/100.0f) * new_job.assigned_truck.fuelusage) * DIESEL_PRICE_PER_LITER; ADD_EXPENSE(world, orig, expenses_from_fuel, gasprice); array_push(&world->active_jobs, &new_job); audio_set_sound_volume(snd_accelerate, 0.08f); audio_play_sound(snd_accelerate, AUDIO_CHANNEL_SFX_3); } static void world_start_scheduled_jobs(world* world, s32 day, s32 timeslot) { for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; for (s32 r = 0; r < location->schedule.jobs.length; r++) { scheduled_job* scheduled_job = array_at(&location->schedule.jobs, r); for (s32 t = 0; t < scheduled_job->offer.shipday_count; t++) { scheduled_job_time scheduled_time = scheduled_job->timeslots[t]; if (scheduled_time.day == day && scheduled_time.timeslot == timeslot) { world_start_scheduled_job(world, scheduled_job, scheduled_time); } } } } } static employee* get_global_employee_by_id(world* world, u32 id) { for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; for (s32 i = 0; i < location->employees.length; i++) { employee* em = *(employee**)array_at(&location->employees, i); if (em->id == id) return em; } } return 0; } static employee* get_employee_by_id(world_location* location, u32 id) { for (s32 i = 0; i < location->employees.length; i++) { employee* em = *(employee**)array_at(&location->employees, i); if (em->id == id) return em; } return 0; } static job_offer* get_job_offer_by_id(world_location* location, u32 id) { for (s32 i = 0; i < location->job_offers.length; i++) { job_offer* job = array_at(&location->job_offers, i); if (job->id == id) return job; } return 0; } static active_job* get_active_job_by_id(world* world, u32 id) { for (s32 i = 0; i < world->active_jobs.length; i++) { active_job* job = array_at(&world->active_jobs, i); if (job->offer.id == id) return job; } return 0; } static active_job* get_active_job_by_ref(world* world, active_job_ref ref) { for (s32 i = 0; i < world->active_jobs.length; i++) { active_job* job = array_at(&world->active_jobs, i); if (job->offer.id == ref.offerid && job->day == ref.day && job->timeslot == ref.timeslot) return job; } return 0; } static scheduled_job* get_scheduled_job_by_id(world_location* location, u32 id) { for (s32 i = 0; i < location->schedule.jobs.length; i++) { scheduled_job* scheduled_job = array_at(&location->schedule.jobs, i); if (scheduled_job->offer.id == id) return scheduled_job; } return 0; } static void world_update_active_jobs(world* world) { for (s32 i = 0; i < world->active_jobs.length; i++) { active_job* job = array_at(&world->active_jobs, i); if (job->done_at <= world->simulation_time) { job_endpoints endpoints = job_offer_get_endpoints(&job->offer); scheduled_job* sc_job = get_scheduled_job_by_id(endpoints.source, job->offer.id); // Get scheduled job from source location if (job->reversed || job->stay_at_destination) { // Driver has returned from the round-trip or will stay at location. world_location* orig_location = get_world_location_by_id(world, job->assignee.original_location_id); employee* e = get_employee_by_id(orig_location, job->assignee.id); e->current_location_id = job->stay_at_destination ? endpoints.dest->id : endpoints.source->id; e->active_job_id = INVALID_ID; // Employee is scheduled to stay at location if (job->stay_at_destination && orig_location->id != e->current_location_id) { array_push(&endpoints.dest->employees, &e); } // Employee started the job from an external location and are returning if (job->reversed && orig_location->id != e->current_location_id) { array_push(&endpoints.source->employees, &e); } // Remove job. array_remove_at(&world->active_jobs, i); i--; } else { // Job is done, return to original location if not staying. if (sc_job) { // If job is still available, update trust. sc_job->trust += (0.01f / sc_job->trust); if (sc_job->trust > 1.0f) { sc_job->trust = 1.0f; } } ADD_INCOME(world, endpoints.source, income_from_trips, job->offer.reward); float gasprice = ((job->offer.total_distance/100.0f) * job->assigned_truck.fuelusage) * DIESEL_PRICE_PER_LITER; ADD_EXPENSE(world, endpoints.source, expenses_from_fuel, gasprice); job->left_at = job->done_at; job->done_at = job->left_at + job->duration_sec; job->reversed = true; // Update experience of employee world_location* orig_location = get_world_location_by_id(world, job->assignee.original_location_id); employee* e = get_employee_by_id(orig_location, job->assignee.id); // 16k/month is max effective increase where 1 job = 0.05 years of experience. float multiplier = 1.0f + (world->investments.training/100.0f) * 0.025f; if (multiplier < 1.0f) multiplier = 1.0f; if (multiplier > 5.0f) multiplier = 5.0f; e->experience += 0.01f * multiplier; } } } } static float get_worked_hours_per_week_for_employee(world* world, employee* emp, scheduled_job* excluding) { float total = 0; for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; for (s32 f = 0; f < location->schedule.jobs.length; f++) { scheduled_job* job = array_at(&location->schedule.jobs, f); if (job == excluding) continue; for (s32 s = 0; s < MAX_SHIPDAYS; s++) { scheduled_job_time slot = job->timeslots[s]; if (slot.assignee == emp) { float hworked = job->offer.duration_sec_min * get_shiptime_factor(emp); if (!slot.stay_at_destination) hworked *= 2; total += hworked; } } } } return total / 3600.0f; } // Run once per day. static void world_update_employee_happiness(world* world, employee* emp) { // Calculate overworking float max_weekly_hours = MAX_WORKED_HOURS_WEEKLY; float hours_worked = get_worked_hours_per_week_for_employee(world, emp, 0); float hours_overworked = hours_worked - max_weekly_hours; if (hours_overworked > 0) emp->happiness = 1.0f - ((hours_overworked/3.0f)*0.2f); else emp->happiness = 1.0f; #if 0 // Calculate underpay float expected_pay = BASE_PAY + (emp->experience * RAISE_PER_YEAR); if (expected_pay > MAX_PAY) expected_pay = MAX_PAY; float underpay = expected_pay - emp->salary; emp->happiness -= (underpay/100.0f)*0.2f; // 1 Star per 100 euro underpay/overpay #endif if (emp->happiness < 0.0f) emp->happiness = 0.0f; if (emp->happiness > 1.0f) emp->happiness = 1.0f; if (emp->happiness < MINIMUM_EMPLOYEE_HAPPINESS) { emp->days_below_happiness_treshold++; } else { emp->days_below_happiness_treshold--; } if (emp->days_below_happiness_treshold < 0) emp->days_below_happiness_treshold = 0; if (emp->days_below_happiness_treshold >= EMPLOYEE_MAX_UNHAPPY_DAYS_BEFORE_QUITTING) { char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s quit their job because they were unhappy for too long.", emp->name); world_report_event(world, error_msg, EVENT_TYPE_EMPLOYEE_QUIT, get_world_location_by_id(world, emp->original_location_id)); end_contract_with_employee(world, emp); } } // Pay investments daily. Run once per day. static void world_pay_investments(world* world) { //ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.safety/28.0f); ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.marketing/28.0f); ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.human_resources/28.0f); ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.training/28.0f); ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.legal/28.0f); } // Payout salaries daily. Run once per day. static void world_payout_salaries(world* world) { for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; for (s32 r = 0; r < location->employees.length; r++) { employee* emp = *(employee**)array_at(&location->employees, r); // Make sure employees are not paid twice when at external location. if (emp->current_location_id == location->id) { world_update_employee_happiness(world, emp); ADD_EXPENSE(world, location, expenses_from_employees, (emp->salary/28.0f)); } } } } static void enable_insights_for_current_month(world* world) { if (isnan(EXPENSES.total_income)) EXPENSES.total_income = 0; // Validate insights for current month. for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; money_data_collection* collection = get_current_insights_data_for_location(world, location); collection->months[world->current_time.tm_mon].total_income = 0; } } static world_location* get_random_owned_location(world* world) { s32 count = 0; for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; count++; } s32 rand_nr = get_random_number(0, count); count = 0; for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; count++; if (count == rand_nr) return location; } return 0; } static void end_contract_with_random_employee(world* world) { world_location* location = get_random_owned_location(world); employee* emp = *(employee**)array_at(&location->employees, get_random_number(0, location->employees.length)); char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s quit their job. Their routes need a new assignee!", emp->name); world_report_event(world, error_msg, EVENT_TYPE_EMPLOYEE_QUIT, get_world_location_by_id(world, emp->original_location_id)); end_contract_with_employee(world, emp); } static void end_contract_with_random_job(world* world) { world_location* location = get_random_owned_location(world); if (location->schedule.jobs.length == 0) return; scheduled_job* scheduled_job = array_at(&location->schedule.jobs, get_random_number(0, location->schedule.jobs.length)); char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s cancelled a contract for location %s.", scheduled_job->offer.company->name, location->name); world_report_event(world, error_msg, EVENT_TYPE_EMPLOYEE_QUIT,location); array_remove_by(&location->schedule.jobs, scheduled_job); } static float get_fine_multiplier(world* world) { // $100 = 1% off of fine. float multiplier = 1.0f; multiplier -= (world->investments.legal / 100.0f) * 0.01f; return multiplier; } static void give_random_fine(world* world) { world_location* location = get_random_owned_location(world); employee* emp = *(employee**)array_at(&location->employees, get_random_number(0, location->employees.length)); s32 fine = get_random_number(world->money / 20, world->money / 10) * get_fine_multiplier(world); // fine is between 5% and 10% of current money. minimum 2k. if (fine < 2000) fine = 2000; char error_msg[MAX_EVENT_MESSAGE_LENGTH]; s32 rand_event = get_random_number(0, 3); switch(rand_event) { case 0: snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "You were fined $%d because %s did not use their tachograph correctly.", fine, emp->name); break; case 1: snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "You were fined $%d because %s did not secure their load correctly.", fine, emp->name); break; case 2: snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "You were fined $%d because %s had the incorrect shipping papers on their trip.", fine, emp->name); break; } ADD_EXPENSE(world, location, expenses_from_utility, fine); world_report_event(world, error_msg, EVENT_TYPE_FINED, emp); } static void brake_down_random_truck(world* world) { retry:; world_location* location = get_random_owned_location(world); employee* emp = *(employee**)array_at(&location->employees, get_random_number(0, location->employees.length)); if (emp->assigned_truck == 0) goto retry; s32 fine = emp->assigned_truck->price / 5.0f; char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s's truck broke down and was repaired for $%d", emp->name, fine); ADD_EXPENSE(world, location, expenses_from_repairs, fine); world_report_event(world, error_msg, EVENT_TYPE_FINED, emp); } static void do_random_inspection(world* world) { world_location* location = get_random_owned_location(world); int total_employees_overworking = 0; for (s32 x = 0; x < location->employees.length; x++) { employee* em = *(employee**)array_at(&location->employees, x); float total_hours = get_worked_hours_per_week_for_employee(world, em, 0); if (total_hours > MAX_WORKED_HOURS_WEEKLY) total_employees_overworking++; } if (total_employees_overworking == 0) { char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "A random inspection was performed in %s and no issues were found.", location->name); world_report_event(world, error_msg, EVENT_TYPE_INFO,location); } else { s32 fine = total_employees_overworking * 1000 * get_fine_multiplier(world); char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "A random inspection was performed in %s and %d employees were found to be working more hours than allowed. Fine: $%d.", location->name, total_employees_overworking, fine); ADD_EXPENSE(world, location, expenses_from_utility, fine); world_report_event(world, error_msg, EVENT_TYPE_FINED, 0); } } static void world_start_random_events(world* world) { world->days_since_last_random_event++; // Minor events { #define MIN_DELAY_BETWEEN_EVENTS (60) float chance_of_random_event = 0.0f; if (world->days_since_last_random_event > MIN_DELAY_BETWEEN_EVENTS) { chance_of_random_event = ((world->days_since_last_random_event - MIN_DELAY_BETWEEN_EVENTS)*0.5f)/100.0f; if (chance_of_random_event > 1.0f) chance_of_random_event = 1.0f; bool run_event = chance_of_random_event >= (get_random_number(0, 100)/100.0f); if (run_event) { s32 rand_event = get_random_number(0, 5); switch(rand_event) { case 0: end_contract_with_random_employee(world); break; case 1: end_contract_with_random_job(world); break; case 2: give_random_fine(world); break; case 3: brake_down_random_truck(world); break; case 4: do_random_inspection(world); break; } world->days_since_last_random_event = 0; } } } } static void end_contract_with_employee(world* world, employee* emp) { if (emp->assigned_truck) emp->assigned_truck->assigned_employee = 0; // Remove assigned timeslots. for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; for (s32 r = 0; r < location->schedule.jobs.length; r++) { scheduled_job* scheduled_job = array_at(&location->schedule.jobs, r); for (s32 t = 0; t < scheduled_job->offer.shipday_count; t++) { scheduled_job_time scheduled_time = scheduled_job->timeslots[t]; if (scheduled_time.assignee == emp) { scheduled_job->timeslots[t].assignee = 0; } } } } // Remove active job active_job* curr_job = get_active_job_by_id(world, emp->active_job_id); if (curr_job) array_remove_by(&world->active_jobs, curr_job); // Remove employee from original location world_location* orig_loc = get_world_location_by_id(world, emp->original_location_id); if (orig_loc) array_remove_by(&orig_loc->employees, &emp); // Remove employee from current location world_location* curr_loc = get_world_location_by_id(world, emp->current_location_id); if (curr_loc) array_remove_by(&curr_loc->employees, &emp); } static void world_update_location_scores(world* world) { for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); location->is_accessible = world_check_location_accessibility(location, location, 1, 0); if (!location->is_owned) continue; s32 total_scheduled_slots = 0; for (int x = 0; x < location->schedule.jobs.length; x++) { scheduled_job* job = array_at(&location->schedule.jobs, x); total_scheduled_slots += job->offer.shipday_count; } float total_happiness = 0.0f; int total_employees_counted = 0; for (s32 x = 0; x < location->employees.length; x++) { employee* em = *(employee**)array_at(&location->employees, x); if (em->original_location_id != location->id) continue; total_employees_counted++; total_happiness += em->happiness; } if (total_employees_counted == 0) total_employees_counted = 1; float total_trust = 0.0f; int total_trust_counted = 0; for (s32 x = 0; x < location->schedule.jobs.length; x++) { scheduled_job* scheduled_job = array_at(&location->schedule.jobs, x); total_trust += scheduled_job->trust; total_trust_counted++; } if (total_trust_counted == 0) total_trust_counted = 1; float fill = total_scheduled_slots / ((float)TIME_SLOTS_PER_WEEK / 3); if (fill > 1.0f) fill = 1.0f; float happiness = total_happiness / total_employees_counted; float trust = total_trust / total_trust_counted; location->score = ((fill*3.0f)+(happiness*0.5f)+(trust*1.5f)) / 5.0f; //printf("Score for location %s: (Schedule: %f Happiness: %f Trust: %f) = %f%% stars: %d\n", // location->name, fill, happiness, trust, location->score*100.0f, (int)round(location->score * 5)); } } static void world_age_employee(world* world) { for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); if (!location->is_owned) continue; for (s32 x = 0; x < location->employees.length; x++) { employee* em = *(employee**)array_at(&location->employees, x); em->age += 1; em->experience += 1; if (em->age >= RETIREMENT_AGE) { char error_msg[MAX_EVENT_MESSAGE_LENGTH]; snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s retired at age %d. Their routes need a new assignee!", em->name, RETIREMENT_AGE); world_report_event(world, error_msg, EVENT_TYPE_EMPLOYEE_QUIT, location); end_contract_with_employee(world, em); x--; } } } } #define UPDATE_LOAN(_loan)\ if (_loan.is_active) {\ _loan.days_left--;\ ADD_EXPENSE(world, 0, expenses_from_loans, _loan.monthly_payment/28.0f);\ if (_loan.days_left <= 0) {\ _loan.is_active = false;\ }\ } static void world_update_loans(world* world) { UPDATE_LOAN(world->bank_info.loan1); UPDATE_LOAN(world->bank_info.loan2); UPDATE_LOAN(world->bank_info.loan3); } static void world_run_simulation_tick(world* world) { s32 elapsed_sec = MINUTES(world->simulation_speed); s64 prev_stamp = world->simulation_time; world->simulation_time += elapsed_sec; struct tm prev_time_buf; prev_time_buf = *gmtime(&prev_stamp); struct tm* prev_time = &prev_time_buf; struct tm curr_time_buf; curr_time_buf = *gmtime(&world->simulation_time); struct tm* curr_time = &curr_time_buf; world->current_time = curr_time_buf; if (prev_time->tm_year != curr_time->tm_year) { // Run once per year world_age_employee(world); } if (prev_time->tm_wday != curr_time->tm_wday) { // Run once per day world_remove_expired_job_offers(world); world_assign_resumes_to_locations(world); world_start_random_events(world); world_assign_new_job_offers(world, false); world_update_location_scores(world); if (curr_time->tm_mday <= 28) { world_payout_salaries(world); // Pay salary first 28 days of month. world_pay_investments(world); world_update_loans(world); } } if (curr_time->tm_hour >= WORK_HOUR_START && curr_time->tm_hour < WORK_HOUR_END) { // Run every 15min s32 hour_part = 60 / TIME_SLOTS_PER_HOUR; s32 prev_part = prev_time->tm_min / hour_part; s32 curr_part = curr_time->tm_min / hour_part; if (prev_part != curr_part) { world_start_scheduled_jobs(world, curr_time->tm_wday, ((curr_time->tm_hour-WORK_HOUR_START)*TIME_SLOTS_PER_HOUR) + curr_part); } } if (curr_time->tm_mday < prev_time->tm_mday) { // Run once per month enable_insights_for_current_month(world); } world_update_active_jobs(world); } void world_update(platform_window* window, world* world) { static float delta = 0; delta += frame_delta; //if (delta >= (1/60.0f)) { // tick runs at 60 ticks per second regardless of fps. world_run_simulation_tick(world); delta = 0; //} } static vec2f get_world_location_for_job(platform_window* window, world* world, active_job* job) { time_t total_time = job->done_at - job->left_at; time_t elapsed_time = world->simulation_time - job->left_at; float complete_percentage = elapsed_time/(float)total_time; if (job->reversed) complete_percentage = 1.0f - complete_percentage; if (complete_percentage > 1.0f) complete_percentage = 1.0f; double current_km = job->offer.total_distance*complete_percentage; double calculating_km = 0.0; for (s32 d = 0; d < job->offer.connections.length-1; d++) { world_location* source = *(world_location**)array_at(&job->offer.connections, d); world_location* dest = *(world_location**)array_at(&job->offer.connections, d+1); double dist_between_points = distance_between_location(source, dest); double next_landmark = calculating_km + dist_between_points; double dist_from_source = current_km - calculating_km; if (calculating_km < current_km && next_landmark >= current_km) { float progress_between_points = dist_from_source / dist_between_points; double lon = source->longitude + (dest->longitude - source->longitude) * progress_between_points; double lat = source->latitude + (dest->latitude - source->latitude) * progress_between_points; vec2f map_pos = coords_to_px(window, lon, lat); map_pos.x *= zoom; map_pos.y *= zoom; map_pos.x += camera_x; map_pos.y += camera_y; return map_pos; } else { calculating_km = next_landmark; } } return (vec2f){0.0f, 0.0f}; } static bool world_check_location_accessibility(world_location* orig, world_location* source, int depth, double dist) { if (orig->is_owned) return true; for (s32 d = 0; d < source->connections.length; d++) { world_location* destination = *(world_location**)array_at(&source->connections, d); if (destination == source) continue; if (destination == orig) continue; if (destination->is_owned && world_get_max_depth_for_location(destination) >= depth) { return true; } else if (distance_between_location(destination, orig) > dist){ bool result = world_check_location_accessibility(orig, destination, depth+1, distance_between_location(destination, orig)); if (result) return true; } else if (depth > world_get_max_depth_for_location(orig)) { return false; } } return false; } bool world_map_location_is_in_sun(vec2f px_pos); world_update_result world_render(platform_window* window, world* world) { world_update_result result = {0,0}; renderer->set_render_depth(3); // Draw locations for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); vec2f map_pos = coords_to_px(window, location->longitude, location->latitude); map_pos.x *= zoom; map_pos.y *= zoom; map_pos.x += camera_x; map_pos.y += camera_y; location->map_position_x = map_pos.x; location->map_position_y = map_pos.y; s32 circle_x = location->map_position_x - dotsize/2; s32 circle_y = location->map_position_y - dotsize/2; bool hovered = mouse_interacts(circle_x, circle_y, dotsize, dotsize); location->is_hovered = hovered; if (hovered && location->is_accessible) { platform_set_cursor(window, CURSOR_POINTER); if (is_left_clicked()) { result.clicked_location = location; audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1); } } color tint = location->is_owned ? COLOR_LOCATION_DOT_OWNED : COLOR_LOCATION_DOT_UNOWNED; if (location->is_hovered) { tint = COLOR_DOT_HOVERED; } if (!location->is_accessible) { tint = COLOR_LOCATION_DOT_UNACCESSIBLE; } renderer->render_image_tint(img_locationdot, circle_x, circle_y, dotsize, dotsize, tint); if (!world_map_location_is_in_sun(map_pos)) { renderer->render_image_tint(img_locationdot, circle_x, circle_y, dotsize, dotsize, rgba(255, 255, 0, 110)); } } renderer->set_render_depth(2); // Draw active jobs for (s32 i = 0; i < world->active_jobs.length; i++) { active_job* job = array_at(&world->active_jobs, i); vec2f pos = get_world_location_for_job(window, world, job); job->px_pos = pos; s32 circle_x = job->px_pos.x - dotsize/2; s32 circle_y = job->px_pos.y - dotsize/2; bool hovered = mouse_interacts(circle_x, circle_y, dotsize, dotsize); job->is_hovered = hovered; if (hovered) { platform_set_cursor(window, CURSOR_POINTER); if (is_left_clicked()) result.clicked_job = job; } renderer->render_image_tint(img_locationdot, job->px_pos.x-(dotsize/2),job->px_pos.y-(dotsize/2), dotsize,dotsize, job->is_hovered ? COLOR_DOT_HOVERED : rgb(255,0,0)); } renderer->set_render_depth(1); dotsize = 5 * scale * zoom; // Draw connections for (s32 i = 0; i < world->locations.length; i++) { world_location* source = array_at(&world->locations, i); for (s32 d = 0; d < source->connections.length; d++) { world_location* destination = *(world_location**)array_at(&source->connections, d); if (destination == source) continue; color line_clr = COLOR_CONNECTION_BETWEEN_LOCATION_ACCESSIBLE; if (!source->is_accessible) line_clr = COLOR_CONNECTION_BETWEEN_LOCATION_INACCESSIBLE; if (!destination->is_accessible) line_clr = COLOR_CONNECTION_BETWEEN_LOCATION_INACCESSIBLE; line_clr.a = 100; if (!world_map_location_is_in_sun((vec2f){source->map_position_x, source->map_position_y})) { line_clr = rgba(255,255,0, 50); } renderer->render_line(source->map_position_x, source->map_position_y, destination->map_position_x, destination->map_position_y, 2, line_clr); } } // Draw actions. for (s32 i = 0; i < world->locations.length; i++) { world_location* location = array_at(&world->locations, i); const img_size = 5*scale*zoom; if (location->job_offers.length != 0) { renderer->render_image(img_handshake, location->map_position_x + dotsize/4, location->map_position_y - img_size - dotsize/4, img_size, img_size); } if (location->resumes.length != 0) { renderer->render_image(img_resume_action, location->map_position_x - img_size - dotsize/4, location->map_position_y - img_size - dotsize/4, img_size, img_size); } } renderer->set_render_depth(1); update_render_scenery(world); return result; }