summaryrefslogtreecommitdiff
path: root/src/world.c
diff options
context:
space:
mode:
authorAldrik Ramaekers <aldrikboy@gmail.com>2024-11-23 21:52:24 +0100
committerAldrik Ramaekers <aldrikboy@gmail.com>2024-11-23 21:52:24 +0100
commit6f7374c2fa58c8692b51018864b802e6b876d305 (patch)
treea7e8ead757e9f4de1920395336dcac1c8a989576 /src/world.c
A new start
Diffstat (limited to 'src/world.c')
-rw-r--r--src/world.c1321
1 files changed, 1321 insertions, 0 deletions
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(&current_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(&current_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(&current_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