From 6f7374c2fa58c8692b51018864b802e6b876d305 Mon Sep 17 00:00:00 2001 From: Aldrik Ramaekers Date: Sat, 23 Nov 2024 21:52:24 +0100 Subject: A new start --- src/world.c | 1321 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1321 insertions(+) create mode 100644 src/world.c (limited to 'src/world.c') diff --git a/src/world.c b/src/world.c new file mode 100644 index 0000000..fbb2ec2 --- /dev/null +++ b/src/world.c @@ -0,0 +1,1321 @@ +static void world_assign_new_job_offers(world* world); +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); + +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. + 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; + } + + 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.reliability = 1.0f; + + return location; +} + +static void world_create_connections(world* world) +{ + const double max_distance = 2.5; // about 155km + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* source = array_at(&world->locations, i); + for (s32 d = 0; d < world->locations.length; d++) + { + world_location* destination = array_at(&world->locations, d); + if (destination == source) continue; + + if (fabs(source->latitude - destination->latitude) < max_distance || + fabs(source->longitude - destination->longitude) < max_distance) { + array_push(&source->connections, &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 = (u8)((employee1->age - 18) * (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); + + world_create_connections(world); + + 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); + + world_create_connections(world); + + 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); + + world_create_connections(world); + + 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 = 100000.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 = 300;//-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; + 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); + enable_insights_for_current_month(new_world); + + return new_world; +} + +static vec2f coords_to_px(platform_window* window, double lon, double lat) +{ + vec2f extra = {9 * scale, 4 * scale}; + vec2f map_pos = {area.x + (float)(area.w * (180.0f + lon) / 360.0f), + area.y + (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) +{ + world_location* current_source = source; + while (buf->length < depth) { + world_location* best_match = 0; + s32 attempt = 0; + + try_again: + best_match = 0; + attempt++; + s32 rand_conn = get_random_number(0, current_source->connections.length); + world_location* connection = *(world_location**)array_at(¤t_source->connections, rand_conn); + + bool already_in_path = false; + for (s32 c = 0; c < buf->length; c++) { + if (*(world_location**)array_at(buf, c) == connection) already_in_path = true; + } + + if (already_in_path && attempt < current_source->connections.length) { + goto try_again; + } + best_match = already_in_path ? 0 : connection; + + #if 0 + double best_match_distance = 0; + + for (s32 i = 0; i < current_source->connections.length; i++) { + world_location* connection = *(world_location**)array_at(¤t_source->connections, i); + if (connection == source) continue; + + bool already_in_path = false; + for (s32 c = 0; c < buf->length; c++) { + if (*(world_location**)array_at(buf, c) == connection) already_in_path = true; + } + + if (!already_in_path) { + double total_distance = fabs(connection->latitude - current_source->latitude) + fabs(connection->longitude - current_source->longitude); + + if (!best_match || best_match_distance < total_distance) { + best_match_distance = total_distance; + best_match = connection; + } + } + } + #endif + + if (!best_match) break; + array_push(buf, &best_match); + current_source = best_match; + } +} + +static void world_assign_new_job_offers(world* world) +{ + 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; + + 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 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--; + } + } + + s32 amount_of_connections = get_random_number(1, 5) + 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)((JOB_OFFER_REWARD_PER_CONNECTION * location->reliability) * amount_of_connections-1); // -1 because source is is connection list. + + // lest assume most experienced drivers drive at 95km/h + double min_duration_hours = (new_offer.total_distance/95.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; + + // 0.5 reliability = 0% + // 1.0 reliability = 10% + // 1.5 reliability = 20% + // etc. + // % = change of a new resume being submitted per day. + #if 1 + if (get_random_number(0, 10) >= ceil(location->reliability)) continue; + #endif + 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.", scheduled_time.assignee->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) { + 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) sc_job->trust += (0.1f / sc_job->trust); // If job is still available, update trust. + 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; + } + } + } +} + +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; + + // 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 + 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 void world_start_random_events(world* world) +{ + world->days_since_last_random_event++; + + // Minor events + { + #define MIN_DELAY_BETWEEN_EVENTS (60) + float change_of_random_event = 0.0f; + if (world->days_since_last_random_event > MIN_DELAY_BETWEEN_EVENTS) { + change_of_random_event = ((world->days_since_last_random_event - MIN_DELAY_BETWEEN_EVENTS)*0.5f)/100.0f; + if (change_of_random_event > 1.0f) change_of_random_event = 1.0f; + + bool run_event = change_of_random_event >= (get_random_number(0, 100)/100.0f); + + (void)run_event; // TODO start event. + } + } +} + +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); + 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); + 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_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_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); + + if (curr_time->tm_mday <= 28) { + world_payout_salaries(world); // Pay salary first 28 days of month. + world_pay_investments(world); + } + } + + if (prev_time->tm_wday == SUNDAY && curr_time->tm_wday == MONDAY) { // Run once per week + world_assign_new_job_offers(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) +{ + world_run_simulation_tick(world); +} + +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); + return map_pos; + } + else { + calculating_km = next_landmark; + } + } + return (vec2f){0.0f, 0.0f}; +} + +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) { + 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; + } + renderer->render_image_tint(img_locationdot, circle_x, circle_y, dotsize, dotsize, tint); + } + + 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; + + renderer->render_line(source->map_position_x, source->map_position_y, destination->map_position_x, destination->map_position_y, 1, rgb(255,0,0)); + } + } + + renderer->set_render_depth(1); + update_render_scenery(world); + + return result; +} \ No newline at end of file -- cgit v1.2.3-70-g09d2