summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/data.c113
-rw-r--r--src/game.c134
-rw-r--r--src/include/data.h100
-rw-r--r--src/include/game.h31
-rw-r--r--src/include/scenery.h29
-rw-r--r--src/include/scenes/error_scene.h15
-rw-r--r--src/include/scenes/loading_scene.h15
-rw-r--r--src/include/scenes/loading_world_scene.h16
-rw-r--r--src/include/scenes/menu_scene.h15
-rw-r--r--src/include/scenes/place_detail.h16
-rw-r--r--src/include/scenes/save_state_select.h15
-rw-r--r--src/include/scenes/settings_scene.h15
-rw-r--r--src/include/scenes/world_map.h16
-rw-r--r--src/include/settings.h28
-rw-r--r--src/include/tooltip.h18
-rw-r--r--src/include/ui/animation.h24
-rw-r--r--src/include/ui/button.h20
-rw-r--r--src/include/ui/colors.h62
-rw-r--r--src/include/ui/panel.h15
-rw-r--r--src/include/ui/portrait.h28
-rw-r--r--src/include/ui/selectors.h13
-rw-r--r--src/include/world.h383
-rw-r--r--src/main.c182
-rw-r--r--src/music.c22
-rw-r--r--src/scenery.c53
-rw-r--r--src/scenes/error_scene.c74
-rw-r--r--src/scenes/loading_scene.c85
-rw-r--r--src/scenes/loading_world_scene.c116
-rw-r--r--src/scenes/menu_scene.c93
-rw-r--r--src/scenes/place_detail.c1771
-rw-r--r--src/scenes/save_state_select.c105
-rw-r--r--src/scenes/settings_scene.c152
-rw-r--r--src/scenes/world_map.c1094
-rw-r--r--src/tooltip.c22
-rw-r--r--src/ui/animation.c18
-rw-r--r--src/ui/button.c101
-rw-r--r--src/ui/panel.c29
-rw-r--r--src/ui/portrait.c21
-rw-r--r--src/ui/selectors.c212
-rw-r--r--src/world.c1321
40 files changed, 6592 insertions, 0 deletions
diff --git a/src/data.c b/src/data.c
new file mode 100644
index 0000000..3698908
--- /dev/null
+++ b/src/data.c
@@ -0,0 +1,113 @@
+void data_load()
+{
+ // Loading screen
+ img_logo = assets_load_image_from_file("data/img/logo.png");
+
+ // Fonts
+ for (int i = 0; i < FONT_COUNT; i++) {
+ u16 size = FONT_START + (FONT_SIZE_SPACING * i);
+ font_regular[i] = assets_load_font_from_file("data/fonts/Exo-Regular.ttf", size);
+ }
+
+ // Images
+ img_logo_fruitosis = assets_load_image_from_file("data/img/logo_fruitosis.png");
+
+ img_truck_unknown = assets_load_image_from_file("data/img/truck-unknown.png");
+ img_iveco_stralis_hiway = assets_load_image_from_file("data/img/iveco-stralis-hiway.png");
+ img_iveco_stralis_activespace = assets_load_image_from_file("data/img/iveco-stralis-activespace.png");
+
+ img_logo_mercedes = assets_load_image_from_file("data/img/mercedes-logo.png");
+ img_logo_iveco = assets_load_image_from_file("data/img/iveco-logo.png");
+ img_logo_volvo = assets_load_image_from_file("data/img/volvo-logo.png");
+
+ img_white = assets_load_image_from_file("data/img/white.png");
+ img_location_pin = assets_load_image_from_file("data/img/location-pin.png");
+ img_city = assets_load_image_from_file("data/img/city.png");
+ img_boat = assets_load_image_from_file("data/img/boat.png");
+ img_star = assets_load_image_from_file("data/img/star.png");
+ img_road = assets_load_image_from_file("data/img/road.png");
+ img_timer = assets_load_image_from_file("data/img/timer.png");
+ img_coins = assets_load_image_from_file("data/img/coins.png");
+ img_arrow_left_rounded = assets_load_image_from_file("data/img/arrow-left-rounded.png");
+ img_globe = assets_load_image_from_file("data/img/globe.png");
+ img_questionmark = assets_load_image_from_file("data/img/question-mark.png");
+ img_checkmark = assets_load_image_from_file("data/img/checkmark.png");
+ img_grid = assets_load_image_from_file("data/img/grid.png");
+ img_lock = assets_load_image_from_file("data/img/lock.png");
+ img_arrow_left = assets_load_image_from_file("data/img/arrow-left.png");
+ img_arrow_right = assets_load_image_from_file("data/img/arrow-right.png");
+ img_pause = assets_load_image_from_file("data/img/pause.png");
+ img_close = assets_load_image_from_file("data/img/close.png");
+ img_back = assets_load_image_from_file("data/img/back.png");
+ img_locationdot = assets_load_image_from_file("data/img/location_dot.png");
+ img_dot = assets_load_image_from_file("data/img/dot.png");
+ img_carwheel = assets_load_image_from_file("data/img/car-wheel.png");
+ img_bank = assets_load_image_from_file("data/img/bank.png");
+ img_graph = assets_load_image_from_file("data/img/statistics.png");
+ img_list = assets_load_image_from_file("data/img/list.png");
+ img_tabitem = assets_load_image_from_file("data/img/tab-item.png");
+ img_portrait = assets_load_image_from_file("data/img/portrait.png");
+ img_resume = assets_load_image_from_file("data/img/resume.png");
+ img_signature = assets_load_image_from_file("data/img/signature.png");
+ img_hired = assets_load_image_from_file("data/img/hired.png");
+ img_denied = assets_load_image_from_file("data/img/denied.png");
+ img_world_map = assets_load_image_from_file("data/img/world_background.png");
+
+ img_panel_bottom = assets_load_image_from_file("data/img/panel_bottom.png");
+ img_panel_top = assets_load_image_from_file("data/img/panel_top.png");
+ img_panel_left = assets_load_image_from_file("data/img/panel_left.png");
+ img_panel_right = assets_load_image_from_file("data/img/panel_right.png");
+ img_panel_bottomleft = assets_load_image_from_file("data/img/panel_bottomleft.png");
+ img_panel_bottomright = assets_load_image_from_file("data/img/panel_bottomright.png");
+ img_panel_topleft = assets_load_image_from_file("data/img/panel_topleft.png");
+ img_panel_topright = assets_load_image_from_file("data/img/panel_topright.png");
+
+ img_button_bottom = assets_load_image_from_file("data/img/button_bottom.png");
+ img_button_top = assets_load_image_from_file("data/img/button_top.png");
+ img_button_left = assets_load_image_from_file("data/img/button_left.png");
+ img_button_right = assets_load_image_from_file("data/img/button_right.png");
+ img_button_bottomleft = assets_load_image_from_file("data/img/button_bottomleft.png");
+ img_button_bottomright = assets_load_image_from_file("data/img/button_bottomright.png");
+ img_button_topleft = assets_load_image_from_file("data/img/button_topleft.png");
+ img_button_topright = assets_load_image_from_file("data/img/button_topright.png");
+
+ img_portrait_body = assets_load_image_from_file("data/img/portrait/body.png");
+ img_portrait_head = assets_load_image_from_file("data/img/portrait/face.png");
+ for (s32 i = 0; i < PORTRAIT_MAX_HAIR_COUNT; i++)
+ {
+ char hair_path[50];
+ sprintf(hair_path, "data/img/portrait/hair/hair%d.png", i+1);
+ img_portrait_hair[i] = assets_load_image_from_file(hair_path);
+
+ }
+
+ // Sound effects
+ snd_click = assets_load_wav_from_file("data/sounds/click.wav");
+ snd_click2 = assets_load_wav_from_file("data/sounds/click2.wav");
+ snd_click3 = assets_load_wav_from_file("data/sounds/click3.wav");
+ snd_event = assets_load_wav_from_file("data/sounds/event.wav");
+ snd_accelerate = assets_load_wav_from_file("data/sounds/accelerate.wav");
+
+ // Songs
+ {
+ platform_set_active_directory(binary_path);
+
+ array files = array_create(sizeof(found_file));
+ array filters = string_split("*.mp3");
+ bool is_cancelled = false;
+ platform_list_files_block(&files, "data/music/", filters, true, 0, true, &is_cancelled, 0);
+ log_assert(files.length < SOUNGS_COUNT, "Not enough space for songs.");
+ for (s32 i = 0; i < files.length; i++)
+ {
+ found_file *file = array_at(&files, i);
+
+ if (platform_file_exists(file->path))
+ {
+ snd_songs[i] = assets_load_music_from_file(file->path);
+ }
+ }
+
+ array_destroy(&files);
+ array_destroy(&filters);
+ }
+} \ No newline at end of file
diff --git a/src/game.c b/src/game.c
new file mode 100644
index 0000000..de41182
--- /dev/null
+++ b/src/game.c
@@ -0,0 +1,134 @@
+
+static game _game_instance;
+
+static void game_init_current_scene()
+{
+ switch (_game_instance.current_state)
+ {
+ case GAME_STATE_LOADING:
+ loading_scene_init();
+ break;
+ case GAME_STATE_MENU:
+ menu_scene_init();
+ break;
+ case GAME_STATE_SELECT_SAVE:
+ save_state_select_scene_init();
+ break;
+ case GAME_STATE_WORLD_MAP:
+ world_map_scene_init();
+ break;
+ case GAME_STATE_LOADING_WORLD:
+ loading_world_scene_init();
+ break;
+ case GAME_STATE_ERROR:
+ error_scene_init();
+ break;
+ case GAME_STATE_PLACE_DETAIL:
+ place_detail_scene_init();
+ break;
+ case GAME_STATE_SETTINGS:
+ settings_scene_init();
+ break;
+ }
+}
+
+static void game_destroy_current_scene()
+{
+ switch (_game_instance.current_state)
+ {
+ case GAME_STATE_LOADING:
+ loading_scene_destroy();
+ break;
+ case GAME_STATE_MENU:
+ menu_scene_destroy();
+ break;
+ case GAME_STATE_SELECT_SAVE:
+ save_state_select_scene_destroy();
+ break;
+ case GAME_STATE_WORLD_MAP:
+ world_map_scene_destroy();
+ break;
+ case GAME_STATE_LOADING_WORLD:
+ loading_world_scene_destroy();
+ break;
+ case GAME_STATE_ERROR:
+ error_scene_destroy();
+ break;
+ case GAME_STATE_PLACE_DETAIL:
+ place_detail_scene_destroy();
+ break;
+ case GAME_STATE_SETTINGS:
+ settings_scene_destroy();
+ break;
+ }
+}
+
+void game_set_active_scene(game_state state)
+{
+ game_destroy_current_scene();
+ _game_instance.current_state = state;
+ game_init_current_scene();
+}
+
+void game_update(platform_window* window)
+{
+ switch (_game_instance.current_state)
+ {
+ case GAME_STATE_LOADING:
+ loading_scene_update(window);
+ break;
+ case GAME_STATE_MENU:
+ menu_scene_update(window);
+ break;
+ case GAME_STATE_SELECT_SAVE:
+ save_state_select_scene_update(window);
+ break;
+ case GAME_STATE_WORLD_MAP:
+ world_map_scene_update(window);
+ break;
+ case GAME_STATE_LOADING_WORLD:
+ loading_world_scene_update(window);
+ break;
+ case GAME_STATE_ERROR:
+ error_scene_update(window);
+ break;
+ case GAME_STATE_PLACE_DETAIL:
+ place_detail_scene_update(window);
+ break;
+ case GAME_STATE_SETTINGS:
+ settings_scene_update(window);
+ break;
+ }
+}
+
+void game_render(platform_window* window)
+{
+ switch (_game_instance.current_state)
+ {
+ case GAME_STATE_LOADING:
+ loading_scene_render(window);
+ break;
+ case GAME_STATE_MENU:
+ menu_scene_render(window);
+ break;
+ case GAME_STATE_SELECT_SAVE:
+ save_state_select_scene_render(window);
+ break;
+ case GAME_STATE_WORLD_MAP:
+ world_map_scene_render(window);
+ break;
+ case GAME_STATE_LOADING_WORLD:
+ loading_world_scene_render(window);
+ break;
+ case GAME_STATE_ERROR:
+ error_scene_render(window);
+ break;
+ case GAME_STATE_PLACE_DETAIL:
+ place_detail_scene_render(window);
+ break;
+ case GAME_STATE_SETTINGS:
+ settings_scene_render(window);
+ break;
+ }
+ update_render_tooltip();
+} \ No newline at end of file
diff --git a/src/include/data.h b/src/include/data.h
new file mode 100644
index 0000000..1f13791
--- /dev/null
+++ b/src/include/data.h
@@ -0,0 +1,100 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_DATA
+#define INCLUDE_DATA
+
+#define FONT_SIZE_SPACING 4
+#define FONT_COUNT 20
+#define FONT_START 8
+
+image* img_logo_fruitosis;
+
+image* img_truck_unknown;
+image* img_iveco_stralis_activespace;
+image* img_iveco_stralis_hiway;
+
+image* img_logo_mercedes;
+image* img_logo_iveco;
+image* img_logo_volvo;
+
+image* img_white;
+image* img_location_pin;
+image* img_city;
+image* img_boat;
+image* img_star;
+image* img_road;
+image* img_timer;
+image* img_coins;
+image* img_arrow_left_rounded;
+image* img_globe;
+image* img_questionmark;
+image* img_checkmark;
+image* img_grid;
+image* img_lock;
+image* img_arrow_left;
+image* img_arrow_right;
+image* img_logo;
+image* img_pause;
+image* img_close;
+image* img_back;
+image* img_locationdot;
+image* img_dot;
+image* img_carwheel;
+image* img_graph;
+image* img_list;
+image* img_bank;
+image* img_tabitem;
+image* img_portrait;
+image* img_resume;
+image* img_signature;
+image* img_hired;
+image* img_denied;
+image* img_world_map;
+
+image* img_button_bottom;
+image* img_button_top;
+image* img_button_left;
+image* img_button_right;
+image* img_button_bottomleft;
+image* img_button_bottomright;
+image* img_button_topleft;
+image* img_button_topright;
+
+image* img_panel_bottom;
+image* img_panel_top;
+image* img_panel_left;
+image* img_panel_right;
+image* img_panel_bottomleft;
+image* img_panel_bottomright;
+image* img_panel_topleft;
+image* img_panel_topright;
+
+#define PORTRAIT_MAX_HAIR_COUNT 5
+image* img_portrait_body;
+image* img_portrait_head;
+image* img_portrait_hair[PORTRAIT_MAX_HAIR_COUNT];
+
+font* font_regular[FONT_COUNT];
+
+sound* snd_click;
+sound* snd_click2;
+sound* snd_click3;
+sound* snd_event;
+sound* snd_accelerate;
+
+#define SOUNGS_COUNT 30
+sound* snd_songs[SOUNGS_COUNT];
+
+void data_load();
+
+font empty_font_d = {0};
+
+#define SIZE_RDF(_w, _size) ((s32)(_size * (_w) + 3) & ~0x03)
+#define SIZE_RD(_w, _size) ((s32)((_size * (_w/1280.0f)) + 3) & ~0x03)
+#define FONT_REGULAR(_size) (_size < FONT_START || _size > (FONT_START+(FONT_SIZE_SPACING*FONT_COUNT))) ? (&empty_font_d) : font_regular[(_size-FONT_START)/FONT_SIZE_SPACING];
+
+#endif \ No newline at end of file
diff --git a/src/include/game.h b/src/include/game.h
new file mode 100644
index 0000000..2c09ada
--- /dev/null
+++ b/src/include/game.h
@@ -0,0 +1,31 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_GAME
+#define INCLUDE_GAME
+
+typedef enum t_game_state
+{
+ GAME_STATE_LOADING = 1,
+ GAME_STATE_MENU = 2,
+ GAME_STATE_SELECT_SAVE = 3,
+ GAME_STATE_WORLD_MAP = 4,
+ GAME_STATE_LOADING_WORLD = 5,
+ GAME_STATE_ERROR = 6,
+ GAME_STATE_PLACE_DETAIL = 7,
+ GAME_STATE_SETTINGS = 8,
+} game_state;
+
+typedef struct t_game
+{
+ game_state current_state;
+} game;
+
+void game_set_active_scene(game_state state);
+void game_update(platform_window* window);
+void game_render(platform_window* window);
+
+#endif \ No newline at end of file
diff --git a/src/include/scenery.h b/src/include/scenery.h
new file mode 100644
index 0000000..08afec0
--- /dev/null
+++ b/src/include/scenery.h
@@ -0,0 +1,29 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_SCENERY
+#define INCLUDE_SCENERY
+
+#define BOAT_ROUTE_MAX_POINTS 300
+
+typedef struct t_boat_route_point
+{
+ float x;
+ float y;
+} boat_route_point;
+
+typedef struct t_boat_route
+{
+ boat_route_point points[BOAT_ROUTE_MAX_POINTS];
+ u8 count;
+ u8 current_point;
+ float current_point_duration;
+ bool reversed;
+} boat_route;
+
+void update_render_scenery(world* world);
+
+#endif \ No newline at end of file
diff --git a/src/include/scenes/error_scene.h b/src/include/scenes/error_scene.h
new file mode 100644
index 0000000..b2b9a0d
--- /dev/null
+++ b/src/include/scenes/error_scene.h
@@ -0,0 +1,15 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_ERROR_SCENE
+#define INCLUDE_ERROR_SCENE
+
+void error_scene_init();
+void error_scene_render(platform_window* window);
+void error_scene_update(platform_window* window);
+void error_scene_destroy();
+
+#endif \ No newline at end of file
diff --git a/src/include/scenes/loading_scene.h b/src/include/scenes/loading_scene.h
new file mode 100644
index 0000000..3e49a9a
--- /dev/null
+++ b/src/include/scenes/loading_scene.h
@@ -0,0 +1,15 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_LOADING_SCENE
+#define INCLUDE_LOADING_SCENE
+
+void loading_scene_init();
+void loading_scene_render(platform_window* window);
+void loading_scene_update(platform_window* window);
+void loading_scene_destroy();
+
+#endif \ No newline at end of file
diff --git a/src/include/scenes/loading_world_scene.h b/src/include/scenes/loading_world_scene.h
new file mode 100644
index 0000000..864fe8d
--- /dev/null
+++ b/src/include/scenes/loading_world_scene.h
@@ -0,0 +1,16 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_LOADING_WORLD_SCENE
+#define INCLUDE_LOADING_WORLD_SCENE
+
+void loading_world_scene_init();
+void start_loading_world(char* saved_file_path);
+void loading_world_scene_render(platform_window* window);
+void loading_world_scene_update(platform_window* window);
+void loading_world_scene_destroy();
+
+#endif \ No newline at end of file
diff --git a/src/include/scenes/menu_scene.h b/src/include/scenes/menu_scene.h
new file mode 100644
index 0000000..e68b778
--- /dev/null
+++ b/src/include/scenes/menu_scene.h
@@ -0,0 +1,15 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_MENU_SCENE
+#define INCLUDE_MENU_SCENE
+
+void menu_scene_init();
+void menu_scene_render(platform_window* window);
+void menu_scene_update(platform_window* window);
+void menu_scene_destroy();
+
+#endif \ No newline at end of file
diff --git a/src/include/scenes/place_detail.h b/src/include/scenes/place_detail.h
new file mode 100644
index 0000000..91c34ab
--- /dev/null
+++ b/src/include/scenes/place_detail.h
@@ -0,0 +1,16 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_PLACE_DETAIL_SCENE
+#define INCLUDE_PLACE_DETAIL_SCENE
+
+void place_detail_scene_init();
+void place_detail_set_active_location(world_location* location);
+void place_detail_scene_render(platform_window* window);
+void place_detail_scene_update(platform_window* window);
+void place_detail_scene_destroy();
+
+#endif \ No newline at end of file
diff --git a/src/include/scenes/save_state_select.h b/src/include/scenes/save_state_select.h
new file mode 100644
index 0000000..260bec2
--- /dev/null
+++ b/src/include/scenes/save_state_select.h
@@ -0,0 +1,15 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_SELECT_SAVE_SCENE
+#define INCLUDE_SELECT_SAVE_SCENE
+
+void save_state_select_scene_init();
+void save_state_select_scene_render(platform_window* window);
+void save_state_select_scene_update(platform_window* window);
+void save_state_select_scene_destroy();
+
+#endif \ No newline at end of file
diff --git a/src/include/scenes/settings_scene.h b/src/include/scenes/settings_scene.h
new file mode 100644
index 0000000..701e6a9
--- /dev/null
+++ b/src/include/scenes/settings_scene.h
@@ -0,0 +1,15 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_SETTINGS_SCENE
+#define INCLUDE_SETTINGS_SCENE
+
+void settings_scene_init();
+void settings_scene_render(platform_window* window);
+void settings_scene_update(platform_window* window);
+void settings_scene_destroy();
+
+#endif \ No newline at end of file
diff --git a/src/include/scenes/world_map.h b/src/include/scenes/world_map.h
new file mode 100644
index 0000000..d678868
--- /dev/null
+++ b/src/include/scenes/world_map.h
@@ -0,0 +1,16 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_WORLD_MAP_SCENE
+#define INCLUDE_WORLD_MAP_SCENE
+
+void world_map_set_active_world(world* world);
+void world_map_scene_init();
+void world_map_scene_render(platform_window* window);
+void world_map_scene_update(platform_window* window);
+void world_map_scene_destroy();
+
+#endif \ No newline at end of file
diff --git a/src/include/settings.h b/src/include/settings.h
new file mode 100644
index 0000000..cba99ad
--- /dev/null
+++ b/src/include/settings.h
@@ -0,0 +1,28 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_SETTINGS
+#define INCLUDE_SETTINGS
+
+#define AUDIO_CHANNEL_SFX_1 1
+#define AUDIO_CHANNEL_SFX_2 2
+#define AUDIO_CHANNEL_SFX_3 3
+
+u16 key_back = KEY_ESCAPE;
+u16 key_accept = KEY_ENTER;
+u16 key_insights_graph = KEY_F1;
+u16 key_insights_chart = KEY_F2;
+u16 key_events = KEY_F3;
+u16 key_bank = KEY_F4;
+
+float volume_global = 0.2f;
+float volume_music = 0.2f;
+float volume_sfx = 0.2f;
+
+bool option_vsync = true;
+bool option_fullscreen = false;
+
+#endif \ No newline at end of file
diff --git a/src/include/tooltip.h b/src/include/tooltip.h
new file mode 100644
index 0000000..8748c81
--- /dev/null
+++ b/src/include/tooltip.h
@@ -0,0 +1,18 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_TOOPTIP
+#define INCLUDE_TOOPTIP
+
+char tooltip_buffer[100];
+s32 tooltip_x = 0;
+s32 tooltip_y = 0;
+bool tooltop_visible = false;
+
+void show_tooltip(s32 x, s32 y, char* buf);
+void update_render_tooltip();
+
+#endif \ No newline at end of file
diff --git a/src/include/ui/animation.h b/src/include/ui/animation.h
new file mode 100644
index 0000000..1a10c0f
--- /dev/null
+++ b/src/include/ui/animation.h
@@ -0,0 +1,24 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_ANIMATION
+#define INCLUDE_ANIMATION
+
+typedef struct t_animation
+{
+ float time;
+ bool started;
+ float duration;
+ float percentage;
+} animation;
+
+#define AN_LI(_cx,_dx,_an) (_cx + (_dx-_cx)*_an.percentage)
+#define AN_LI_TINT(_color, _an) rgba((_color).r,(_color).g,(_color).b,255*_an.percentage)
+
+animation animation_create(s32 duration);
+float animation_update(animation* an);
+
+#endif \ No newline at end of file
diff --git a/src/include/ui/button.h b/src/include/ui/button.h
new file mode 100644
index 0000000..e381631
--- /dev/null
+++ b/src/include/ui/button.h
@@ -0,0 +1,20 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_BUTTON
+#define INCLUDE_BUTTON
+
+typedef enum t_button_type
+{
+ BUTTON_STATIC = 0,
+ BUTTON_ENABLED = 1,
+ BUTTON_DISABLED = 2,
+ BUTTON_HIGHLIGHTED = 3,
+} button_type;
+
+bool button_render(float scale, button_type enabled, char* text, s32 x, s32 y, s32 w, s32 h);
+
+#endif \ No newline at end of file
diff --git a/src/include/ui/colors.h b/src/include/ui/colors.h
new file mode 100644
index 0000000..f8a7f91
--- /dev/null
+++ b/src/include/ui/colors.h
@@ -0,0 +1,62 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_COLORS
+#define INCLUDE_COLORS
+
+#define COLOR_WORLD_MAP_BACKGROUND rgb(32,52,63)
+#define COLOR_WHITE rgb(255,255,255)
+#define COLOR_BLACK rgb(0,0,0)
+#define COLOR_INSPECT_ACTIVE_JOB_LINE_CONNECTION rgb(40,40,40)
+#define COLOR_DOT_HOVERED rgb(100,220,100)
+
+#define COLOR_TEXT_NEGATIVE rgb(226,86,86)
+#define COLOR_TITLE rgb(8, 10, 12)
+#define COLOR_TEXT_SHADOW rgb(24,24,24)
+#define COLOR_TEXT rgb(207,207,207)
+
+#define COLOR_BUTTON_DISABLED_TINT rgb(70,70,70)
+
+#define COLOR_BUTTON_HIGHLIGHTED_TINT rgb(240,160,160)
+#define COLOR_BUTTON_ACTIVE_TINT rgb(210,210,210)
+#define COLOR_PANEL_BACKGROUND rgb(66,63,58)
+
+#define COLOR_BUTTON rgb(98, 95, 90)
+#define COLOR_BUTTON_ACTIVE rgb(81, 78, 74)
+#define COLOR_BUTTON_HIGHLIGHTED_ACTIVE rgb(92, 60, 56)
+#define COLOR_BUTTON_DISABLED rgb(27, 26, 25)
+
+#define COLOR_LOCATION_DOT_UNOWNED rgb(220,220,220)
+#define COLOR_LOCATION_DOT_OWNED rgb(220,100,100)
+
+#define COLOR_LIST_ENTRY_BACKGROUND rgb(46,43,40)
+#define COLOR_LIST_ENTRY_BACKGROUND_ACTIVE rgb(63,59,56)
+
+#define COLOR_SCHEDULE_ROW_ACTIVE rgb(118,115,100)
+#define COLOR_SCHEDULE_TILE_FIXED rgb(178,239,155)
+#define COLOR_SCHEDULE_TILE_HOVERED rgb(26,200,237)
+#define COLOR_SCHEDULE_TILE_INVALID rgb(244,70,73)
+#define COLOR_SCHEDULE_TILE_INVALID_SELECTED rgb(252,159,160)
+#define COLOR_SCHEDULE_TILE_SELECTED rgb(174,212,230)
+#define COLOR_SCHEDULE_TILE_HIGHLIGHTED rgb(237,177,26)
+
+#define COLOR_SELECTOR_UNDERLINE rgb(180,180,180)
+
+#define LEGENDA_HOVER_BACKGROUND_COLOR rgba(255,0,0,50)
+#define LEGENDA_COLOR_DISABLED rgb(120,120,120)
+#define LEGENDA_SUB_COLOR_DISABLED rgba(120,120,120,100)
+
+#define COLOR_SCHEDULE_BG rgb(142,138,132)
+#define COLOR_SCHEDULE_BORDER rgb(75,73,69)
+#define COLOR_SCHEDULE_BORDER_THIN rgb(88,85,80)
+
+#define COLOR_TEXTBOX_TINT rgb(100,100,100)
+#define COLOR_TEXTBOX_FILL rgb(38, 37, 35)
+
+#define COLOR_WRONG rgb(168,45,45)
+#define COLOR_CORRECT rgb(47,168,45)
+
+#endif \ No newline at end of file
diff --git a/src/include/ui/panel.h b/src/include/ui/panel.h
new file mode 100644
index 0000000..83103d2
--- /dev/null
+++ b/src/include/ui/panel.h
@@ -0,0 +1,15 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_PANEL
+#define INCLUDE_PANEL
+
+// 1280 is our reference width.
+#define UI_SCALE(_w) (_w/1280.0f)
+
+void panel_render(float scale, s32 x, s32 y, s32 w, s32 h);
+
+#endif \ No newline at end of file
diff --git a/src/include/ui/portrait.h b/src/include/ui/portrait.h
new file mode 100644
index 0000000..8bff8e1
--- /dev/null
+++ b/src/include/ui/portrait.h
@@ -0,0 +1,28 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_PORTRAIT
+#define INCLUDE_PORTRAIT
+
+color hair_palette[] = {
+ rgb(199, 186, 168), rgb(188, 181, 160), rgb(191, 173, 148), rgb(203, 181, 138), rgb(187, 162, 120),
+ rgb(174, 153, 122), rgb(172, 134, 109), rgb(205, 169, 129), rgb(168, 115, 88), rgb(137, 82, 71),
+ rgb(158, 131, 99), rgb(150, 116, 93), rgb(128, 93, 73), rgb(202, 162, 136), rgb(197, 146, 137),
+ rgb(194, 136, 129), rgb(165, 127, 117), rgb(152, 111, 113), rgb(123, 103, 93), rgb(98, 79, 73)
+};
+
+color skin_palette[] = {
+ rgb(233, 203, 167), rgb(238, 208, 183), rgb(247, 221, 196), rgb(247, 226, 171), rgb(239, 199, 148), rgb(239, 192, 136),
+ rgb(231, 188, 145), rgb(236, 192, 131), rgb(208, 158, 125), rgb(203, 150, 98), rgb(171, 139, 100), rgb(148, 98, 61),
+};
+
+color body_palette[] = {
+ rgb(33, 28, 32), rgb(76, 74, 77), rgb(109, 103, 107), rgb(171, 133, 86), rgb(213, 175, 126), rgb(240, 229, 225),
+};
+
+void draw_employee_portrait(employee* emp, float x, float y, float w, float h);
+
+#endif \ No newline at end of file
diff --git a/src/include/ui/selectors.h b/src/include/ui/selectors.h
new file mode 100644
index 0000000..6832ff7
--- /dev/null
+++ b/src/include/ui/selectors.h
@@ -0,0 +1,13 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_EMPLOYEE_SELECTOR
+#define INCLUDE_EMPLOYEE_SELECTOR
+
+employee* employee_selector_render(platform_window* window, float scale, bool enabled, employee* current_val, s32 x, s32 y, s32 w, s32 h, animation an, scheduled_job* offer);
+world_location* location_selector_render(platform_window* window, float scale, bool enabled, world_location* current_val, s32 x, s32 y, s32 w, s32 h);
+
+#endif \ No newline at end of file
diff --git a/src/include/world.h b/src/include/world.h
new file mode 100644
index 0000000..4f8af10
--- /dev/null
+++ b/src/include/world.h
@@ -0,0 +1,383 @@
+/*
+* BSD 2-Clause “Simplified” License
+* Copyright (c) 2019, Aldrik Ramaekers, aldrik.ramaekers@protonmail.com
+* All rights reserved.
+*/
+
+#ifndef INCLUDE_WORLD
+#define INCLUDE_WORLD
+
+#define MAX_WORLD_LOCATION_NAME_LENGTH 20
+#define MAX_ENPOLYEE_FIRSTNAME_LENGTH 14
+#define MAX_ENPOLYEE_LASTNAME_LENGTH 14
+#define MAX_EMPLOYEE_NAME_LENGTH 30
+#define MAX_COMPANY_NAME_LENGTH 50
+#define MAX_DEALER_NAME_LENGTH 20
+#define MAX_TRUCK_NAME_LENGTH 30
+#define MAX_PRODUCT_NAME_LENGTH 50
+#define MAX_EMPLOYEE_NR_LENGTH 12
+#define MAX_INPUT_LENGTH_FOR_EMPLOYEE_SELECTOR 10
+
+// Static = loaded from file
+// Save State = loaded from save state
+// Dynamic = set at runtime
+
+#define MINUTES(_n) (_n*60)
+#define HOURS(_n) (_n*MINUTES(60))
+#define DAYS(_n) (_n*HOURS(24))
+
+typedef struct t_world_location world_location;
+typedef struct t_employee employee;
+
+#define WORK_HOUR_START 8
+#define WORK_HOUR_END 20
+#define TIME_SLOTS_PER_HOUR 4
+#define TIME_SLOTS_PER_DAY ((WORK_HOUR_END-WORK_HOUR_START)*TIME_SLOTS_PER_HOUR)
+#define TIME_SLOTS_PER_WEEK (TIME_SLOTS_PER_DAY*7)
+
+#define MAX_JOBOFFER_COUNT 25
+#define MAX_EMPLOYEE_COUNT (TIME_SLOTS_PER_WEEK)
+#define MAX_TRUCK_COUNT (TIME_SLOTS_PER_WEEK)
+
+#define MISSED_DELIVERY_TRUST_PENALTY (0.5f)
+#define MAX_EFFECTIVE_EXPERIENCE 25.0f // anything past 25 years of experience has no extra positives.
+#define SHIPTIME_DURATION_MULTIPLIER_MAX 1.0f
+#define SHIPTIME_DURATION_MULTIPLIER_MIN 1.15f
+#define DIESEL_PRICE_PER_LITER 1.4f
+
+#define INVALID_ID 0
+#define MAX_WORKED_HOURS_WEEKLY (45.0f)
+#define MINIMUM_EMPLOYEE_HAPPINESS (0.4f)
+#define EMPLOYEE_MAX_UNHAPPY_DAYS_BEFORE_QUITTING (60.0f)
+
+#define CDAYTORDAY(_day) (_day == 0 ? 7 : _day) // m = 1, s = 7
+#define RDAYTOCDAY(_day) (_day == 7 ? 0 : _day) // m = 1, s = 0
+
+#define BASE_PAY 1800
+#define RAISE_PER_YEAR 55
+#define MAX_PAY 3250
+
+typedef enum t_minor_event
+{
+ EXTERNAL_INSPECTION, // Check overworking, safety
+} minor_event;
+
+typedef enum t_weekday
+{
+ MONDAY = 1,
+ TUESDAY = 2,
+ WEDNESDAY = 3,
+ THURSDAY = 4,
+ FRIDAY = 5,
+ SATURDAY = 6,
+ SUNDAY = 0,
+
+ DAY_INVALID = 99
+} weekday;
+
+typedef struct t_product
+{
+ char name[MAX_PRODUCT_NAME_LENGTH];
+} product;
+
+typedef struct t_company
+{
+ // Static
+ char name[MAX_COMPANY_NAME_LENGTH];
+ array products;
+
+ // Dynamic
+ image* logo;
+} company;
+
+typedef struct t_truck
+{
+ // Static
+ char name[MAX_TRUCK_NAME_LENGTH];
+ u16 hp;
+ u32 price;
+ u16 fuelcapacity;
+ u16 torque;
+ float fuelusage;
+
+ // Dynamic
+ image* logo;
+
+ // Save State
+ s32 type;
+ s32 id;
+ employee* assigned_employee;
+} truck;
+
+typedef struct t_truck_dealer
+{
+ // Static
+ char name[MAX_DEALER_NAME_LENGTH];
+ array trucks;
+
+ // Dynamic
+ image* logo;
+} truck_dealer;
+
+#define JOB_OFFER_REWARD_PER_CONNECTION 160
+#define MAX_SHIPDAYS 4
+
+typedef struct t_job_offer
+{
+ u32 id;
+ s64 expire_date;
+ company* company;
+ product* product;
+ s32 shipday_count;
+ weekday shipdays[MAX_SHIPDAYS];
+ u32 reward;
+ array connections; // Should not be freed if offer has been accepted.
+ double total_distance; // in KM
+ time_t duration_sec_min; // experienced drivers
+ time_t duration_sec_max; // inexperienced drivers
+} job_offer;
+
+typedef struct t_employee
+{
+ u32 id;
+ char name[MAX_EMPLOYEE_NAME_LENGTH];
+ u8 age;
+ struct tm hire_date;
+ u8 experience;
+ float salary;
+ float happiness;
+ s16 days_below_happiness_treshold;
+ u32 current_location_id;
+ u32 original_location_id;
+ truck* assigned_truck;
+ u32 active_job_id;
+
+ // Portrait
+ u8 portrait_hair_type;
+ color hair_color;
+ color face_color;
+ color body_color;
+} employee;
+
+#define RESUME_FADEOUT_MS 300
+typedef struct t_resume
+{
+ employee* employee;
+ s64 expire_date;
+ bool hired;
+ animation animation;
+} resume;
+
+typedef struct t_scheduled_job_time
+{
+ s16 day; // sunday = 0
+ s16 timeslot;
+ bool stay_at_destination;
+ employee* assignee;
+} scheduled_job_time;
+
+typedef struct t_scheduled_job
+{
+ float trust;
+ job_offer offer;
+ world_location* location;
+ scheduled_job_time timeslots[MAX_SHIPDAYS];
+} scheduled_job;
+
+typedef struct t_active_job
+{
+ // save state
+ s16 day;
+ s16 timeslot;
+ bool stay_at_destination;
+ job_offer offer;
+ employee assignee;
+ truck assigned_truck;
+ time_t duration_sec;
+ time_t left_at;
+ time_t done_at;
+ bool reversed;
+
+ // dynamic
+ vec2f px_pos;
+ bool is_hovered;
+} active_job;
+
+typedef struct t_active_job_ref
+{
+ s16 day;
+ s16 timeslot;
+ u32 offerid;
+} active_job_ref;
+
+typedef struct t_world_update_result
+{
+ active_job* clicked_job;
+ world_location* clicked_location;
+} world_update_result;
+
+#define NUM_DAYS 7
+typedef struct t_schedule
+{
+ array jobs;
+} schedule;
+
+typedef struct t_world_location
+{
+ // Static
+ u8 size;
+ double latitude;
+ double longitude;
+ char name[MAX_WORLD_LOCATION_NAME_LENGTH];
+ s32 map_position_x;
+ s32 map_position_y;
+
+ // Save State
+ bool is_owned;
+ array employees; // Contains internal and external employees. Employee can be at 2 locations at any given time.
+ array job_offers;
+ array resumes;
+ array trucks;
+ float reliability;
+ schedule schedule;
+ u16 purchase_year;
+ array insights;
+
+ // Dynamic
+ array connections;
+ bool is_hovered;
+ u32 id;
+} world_location;
+
+typedef struct t_job_endpoints
+{
+ world_location* source;
+ world_location* dest;
+} job_endpoints;
+
+typedef enum t_event_type
+{
+ EVENT_TYPE_MISSED_SHIPMENT_NO_TRUCK, // go to employee detail
+ EVENT_TYPE_MISSED_SHIPMENT_NOT_AT_LOCATION, // go to schedule
+ EVENT_TYPE_MISSED_SHIPMENT_NO_ASSIGNEE, // go to schedule
+ EVENT_TYPE_EMPLOYEE_QUIT, // go to schedule
+} event_type;
+
+#define MAX_EVENT_MESSAGE_LENGTH 150
+
+typedef struct t_event
+{
+ void* data;
+ event_type type;
+ scheduled_job_time job_time;
+ char message[MAX_EVENT_MESSAGE_LENGTH];
+} event;
+
+#define MIN_SIMULATION_SPEED 0
+#define MAX_SIMULATION_SPEED 8
+
+#define LOG_HISTORY_LENGTH 25
+
+typedef struct t_event_log
+{
+ array events;
+ u16 write_cursor;
+ bool has_unread_messages;
+} event_log;
+
+#define ADD_EXPENSE(_world,_loc,_var,_amount)\
+ {\
+ _world->money -= _amount;\
+ money_data_collection* collection = get_current_insights_data(_world);\
+ collection->months[_world->current_time.tm_mon].total_expenses -= _amount;\
+ collection->months[_world->current_time.tm_mon].total_profit -= _amount;\
+ collection->months[_world->current_time.tm_mon]._var -= _amount;\
+ if (_loc) {\
+ collection = get_current_insights_data_for_location(_world,_loc);\
+ collection->months[_world->current_time.tm_mon].total_expenses -= _amount;\
+ collection->months[_world->current_time.tm_mon].total_profit -= _amount;\
+ collection->months[_world->current_time.tm_mon]._var -= _amount;}\
+ }
+
+#define ADD_INCOME(_world,_loc,_var,_amount)\
+ {\
+ _world->money += _amount;\
+ money_data_collection* collection = get_current_insights_data(_world);\
+ collection->months[_world->current_time.tm_mon].total_income += _amount;\
+ collection->months[_world->current_time.tm_mon].total_profit += _amount;\
+ collection->months[_world->current_time.tm_mon]._var += _amount;\
+ if (_loc) {\
+ collection = get_current_insights_data_for_location(_world,_loc);\
+ collection->months[_world->current_time.tm_mon].total_income += _amount;\
+ collection->months[_world->current_time.tm_mon].total_profit += _amount;\
+ collection->months[_world->current_time.tm_mon]._var += _amount;}\
+ }
+
+#define EXPENSES get_current_insights_data(world)->months[world->current_time.tm_mon]
+
+typedef struct t_money_data
+{
+ float total_income;
+ float total_expenses;
+ float total_profit;
+
+ float income_from_trips;
+
+ float expenses_from_trucks;
+ float expenses_from_utility;
+ float expenses_from_healthcare;
+ float expenses_from_repairs;
+ float expenses_from_fuel;
+ float expenses_from_employees;
+} money_data;
+
+#define MONTHS_IN_YEAR 12
+
+typedef struct t_money_data_collection
+{
+ money_data months[MONTHS_IN_YEAR];
+} money_data_collection;
+
+typedef struct t_company_investments
+{
+ u32 safety;
+ u32 marketing;
+ u32 human_resources;
+ u32 training;
+ u32 legal;
+} company_investments;
+
+typedef struct t_world
+{
+ // Save State
+ s64 simulation_time;
+ u16 start_year;
+ float money;
+ u32 next_id;
+ u8 simulation_speed;
+ array active_jobs;
+ event_log log;
+ array insights;
+ company_investments investments;
+ u16 days_since_last_random_event;
+
+ // Dynamic
+ array locations;
+ array companies;
+ array firstnames;
+ array lastnames;
+ array truck_dealers;
+ array boat_routes;
+ struct tm current_time;
+} world;
+
+world* world_create_new();
+void world_report_event(world* world, char* msg, event_type type, void* data);
+world_location* get_world_location_by_id(world* world, s32 id);
+world_location* get_world_location_by_name(world* world, char* str);
+float world_location_get_price(world_location* location);
+void add_truck_to_world_location(world* world, world_location* location, truck* tr);
+void world_update(platform_window* window, world* world);
+world_update_result world_render(platform_window* window, world* world);
+
+#endif \ No newline at end of file
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..ce94b9c
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,182 @@
+#define ASSET_FONT_COUNT 25
+#define ASSET_WORKER_COUNT 4
+#define ASSET_QUEUE_COUNT 100
+#define ASSET_IMAGE_COUNT 70
+#define ASSET_SOUND_COUNT 30
+#define NUM_AUDIO_CHANNELS 8
+#define GAME_VERSION "0.1"
+
+#include "../project-base/src/project_base.h"
+
+platform_window* main_window;
+
+vec4 area;
+float scale = 1.0f;
+float zoom = 1.0f;
+float camera_x = 0.0f;
+float camera_y = 0.0f;
+font* fnt_rd8;
+font* fnt_rd12;
+font* fnt_rd16;
+font* fnt_rd20;
+font* fnt_rd24;
+font* fnt_rd28;
+font* fnt_rd32;
+font* fnt_rd36;
+font* fnt_rd40;
+font* fnt_rd44;
+font* fnt_rd48;
+
+#include "include/settings.h"
+#include "include/ui/colors.h"
+#include "include/ui/animation.h"
+#include "include/world.h"
+#include "include/ui/portrait.h"
+#include "include/scenery.h"
+#include "include/data.h"
+#include "include/game.h"
+#include "include/tooltip.h"
+#include "include/ui/panel.h"
+#include "include/ui/button.h"
+#include "include/ui/selectors.h"
+#include "include/scenes/menu_scene.h"
+#include "include/scenes/loading_scene.h"
+#include "include/scenes/save_state_select.h"
+#include "include/scenes/world_map.h"
+#include "include/scenes/loading_world_scene.h"
+#include "include/scenes/error_scene.h"
+#include "include/scenes/place_detail.h"
+#include "include/scenes/settings_scene.h"
+
+#include "music.c"
+#include "world.c"
+#include "data.c"
+#include "game.c"
+#include "scenery.c"
+#include "ui/panel.c"
+#include "ui/button.c"
+#include "ui/animation.c"
+#include "ui/portrait.c"
+#include "tooltip.c"
+#include "scenes/menu_scene.c"
+#include "scenes/loading_scene.c"
+#include "scenes/save_state_select.c"
+#include "scenes/world_map.c"
+#include "scenes/loading_world_scene.c"
+#include "scenes/error_scene.c"
+#include "scenes/place_detail.c"
+#include "scenes/settings_scene.c"
+#include "ui/selectors.c"
+
+#define CONFIG_DIRECTORY "trucker_x"
+
+static void draw_debug_overlay(platform_window* window)
+{
+ static bool enabled = false;
+ if (keyboard_is_key_pressed(KEY_F1)) enabled = !enabled;
+ if (!enabled) return;
+
+ renderer->set_render_depth(20);
+
+ renderer->render_rectangle(0,0,200*scale,200*scale,rgb(70,70,70));
+
+ font* fnt = FONT_REGULAR(SIZE_RD(area.w, 24));
+
+ {
+ char deltabuf[20];
+ sprintf(deltabuf, "Frame: %.5f", frame_delta);
+ renderer->render_text(fnt, 10, 10, deltabuf, rgb(255,0,0));
+ }
+
+ {
+ char deltabuf[20];
+ sprintf(deltabuf, "Game: %.5f", update_delta);
+ renderer->render_text(fnt, 10, 10+(fnt->px_h+2)*1, deltabuf, rgb(255,0,0));
+ }
+
+ {
+ char deltabuf[20];
+ sprintf(deltabuf, "FPS: %.0f", 1.0f/frame_delta);
+ renderer->render_text(fnt, 10, 10+(fnt->px_h+2)*2, deltabuf, rgb(255,0,0));
+ }
+
+ renderer->set_render_depth(19);
+}
+
+void update_render_game(platform_window* window)
+{
+ area = camera_get_target_rectangle(window);
+ scale = UI_SCALE(area.w);
+
+ fnt_rd8 = FONT_REGULAR(SIZE_RDF(scale, 8));
+ fnt_rd12 = FONT_REGULAR(SIZE_RDF(scale, 12));
+ fnt_rd16 = FONT_REGULAR(SIZE_RDF(scale, 16));
+ fnt_rd20 = FONT_REGULAR(SIZE_RDF(scale, 20));
+ fnt_rd24 = FONT_REGULAR(SIZE_RDF(scale, 24));
+ fnt_rd28 = FONT_REGULAR(SIZE_RDF(scale, 28));
+ fnt_rd32 = FONT_REGULAR(SIZE_RDF(scale, 32));
+ fnt_rd36 = FONT_REGULAR(SIZE_RDF(scale, 36));
+ fnt_rd40 = FONT_REGULAR(SIZE_RDF(scale, 40));
+ fnt_rd44 = FONT_REGULAR(SIZE_RDF(scale, 44));
+ fnt_rd48 = FONT_REGULAR(SIZE_RDF(scale, 48));
+
+ #ifdef MODE_DEBUG
+ draw_debug_overlay(window);
+ #endif
+
+ game_update(window);
+ game_render(window);
+
+ update_music();
+}
+
+int main(int argc, char** argv)
+{
+ platform_init(argc, argv, CONFIG_DIRECTORY);
+
+ #define VALIDATE_VOLUME(_vol) if (_vol < 0.0f) _vol = 0.0f; else if (_vol > 1.0f) _vol = 1.0f;
+ volume_global = settings_get_number_or_default("v_global", 100) / 100.0f;
+ volume_music = settings_get_number_or_default("v_music", 100) / 100.0f;
+ volume_sfx = settings_get_number_or_default("v_sfx", 100) / 100.0f;
+ option_vsync = settings_get_number_or_default("vsync", 1);
+ option_fullscreen = settings_get_number_or_default("fullscreen", 1);
+ VALIDATE_VOLUME(volume_global);
+ VALIDATE_VOLUME(volume_music);
+ VALIDATE_VOLUME(volume_sfx);
+
+ s32 window_w = settings_get_number_or_default("window_w", 1280);
+ s32 window_h = settings_get_number_or_default("window_h", 720);
+
+ main_window = platform_open_window("TruckerX",
+ window_w, window_h,
+ 9999, 9999,
+ 960, 540 + platform_get_titlebar_height(),
+ update_render_game, 0);
+
+ platform_toggle_vsync(main_window, option_vsync);
+ if (option_fullscreen) platform_toggle_fullscreen(main_window, option_fullscreen);
+
+ data_load();
+ game_set_active_scene(GAME_STATE_LOADING);
+
+ audio_set_mixer_volume(AUDIO_CHANNEL_SFX_1, volume_sfx*volume_global);
+ audio_set_mixer_volume(AUDIO_CHANNEL_SFX_2, volume_sfx*volume_global);
+
+ while(platform_keep_running(main_window)) {
+ main_window->do_draw = true;
+ platform_handle_events();
+ }
+
+ settings_set_number("window_w", main_window->width);
+ settings_set_number("window_h", main_window->height + platform_get_titlebar_height());
+ settings_set_number("v_global", volume_global*100);
+ settings_set_number("v_music", volume_music*100);
+ settings_set_number("v_sfx", volume_sfx*100);
+ settings_set_number("vsync", option_vsync);
+ settings_set_number("fullscreen", option_fullscreen);
+
+ settings_write_to_file();
+ platform_destroy();
+
+ return 0;
+} \ No newline at end of file
diff --git a/src/music.c b/src/music.c
new file mode 100644
index 0000000..c85b63c
--- /dev/null
+++ b/src/music.c
@@ -0,0 +1,22 @@
+
+void update_music()
+{
+ static s32 current_song_index = 0;
+ if (!audio_music_is_playing()) {
+ sound* snd = snd_songs[current_song_index];
+ if (!snd) {
+ current_song_index = 0;
+ }
+ else {
+ if (!snd->loaded) return;
+
+ char buf[MAX_INPUT_LENGTH];
+ sprintf(buf, "Now playing: \"%s\".", (char*)snd->start_addr);
+ log_info(buf);
+
+ audio_set_music_volume(volume_music*volume_global);
+ audio_play_sound(snd, -1);
+ current_song_index++;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/scenery.c b/src/scenery.c
new file mode 100644
index 0000000..9e647d6
--- /dev/null
+++ b/src/scenery.c
@@ -0,0 +1,53 @@
+
+
+static void update_render_path(world* world, boat_route *route)
+{
+ boat_route_point prev_point = route->points[route->current_point];
+ boat_route_point next_point = route->points[route->current_point + (route->reversed ? -1 : 1)];
+
+ float dist = sqrt(pow(prev_point.x - next_point.x, 2) + pow(prev_point.y - next_point.y, 2));
+ float time_between_points = (dist*1000000.0f);
+
+ float percentage = route->current_point_duration/time_between_points;
+
+ vec2f start_pos = {area.x + (prev_point.x*area.w), area.y + (prev_point.y*area.h)};
+ vec2f end_pos = {area.x + (next_point.x*area.w), area.y + (next_point.y*area.h)};
+
+ float currx = start_pos.x - (start_pos.x - end_pos.x)*percentage;
+ float curry = start_pos.y - (start_pos.y - end_pos.y)*percentage;
+
+ vec2f map_pos = {currx*zoom+camera_x, curry*zoom+camera_y};
+
+ float rad = atan2(end_pos.y-start_pos.y, end_pos.x-start_pos.x);
+
+ gl_render_set_rotation(-rad);
+ renderer->render_image(img_boat, map_pos.x-3, map_pos.y-3, 6, 6);
+ gl_render_set_rotation(0.0f);
+
+ route->current_point_duration += (frame_delta*1000.0f)*world->simulation_speed;
+ if (route->current_point_duration > time_between_points) {
+ if (!route->reversed) route->current_point++;
+ else route->current_point--;
+ route->current_point_duration = 0;
+ }
+ if (!route->reversed && route->current_point >= route->count-1) route->reversed = true;
+ if (route->reversed && route->current_point <= 0) route->reversed = false;
+}
+
+void update_render_scenery(world* world) {
+
+ renderer->render_set_scissor(main_window, area.x,area.y,area.w,area.h);
+ for (s32 i = 0; i < world->boat_routes.length; i++) {
+ boat_route* route = array_at(&world->boat_routes, i);
+ update_render_path(world, route);
+ }
+ renderer->render_reset_scissor();
+
+
+#if 0
+ static s32 count = 0;
+ if (is_left_down() && count++ % 10 == 0) {
+ printf("%f, %f,\n", (_global_mouse.x-area.x)/(float)area.w, (_global_mouse.y-area.y)/(float)area.h);
+ }
+#endif
+} \ No newline at end of file
diff --git a/src/scenes/error_scene.c b/src/scenes/error_scene.c
new file mode 100644
index 0000000..8d4446a
--- /dev/null
+++ b/src/scenes/error_scene.c
@@ -0,0 +1,74 @@
+
+void error_scene_init()
+{
+
+}
+
+static void error_scene_draw_info(platform_window* window)
+{
+ s32 screen_center_x = area.x + (area.w/2);
+ s32 screen_center_y = area.y + (area.h/2);
+
+ float vertical_pad = 20 * scale;
+ float horizontal_pad = vertical_pad;
+ float spacing = 5 * scale;
+
+ s32 panel_h = 280 * scale;
+ s32 panel_item_size = (panel_h - (vertical_pad*2) - (spacing*1)) / 2;
+ s32 panel_w = (panel_item_size * 3) + (horizontal_pad*2) + (spacing*2);
+
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = screen_center_y - (panel_h/2);
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ // info text
+ {
+ {
+ char* title = "ERROR";
+ font* font_title = FONT_REGULAR(SIZE_RD(area.w, 32));
+ s32 text_w = renderer->calculate_text_width(font_title, title);
+ s32 text_x = screen_center_x - (text_w/2);
+ s32 text_y = panel_y + (vertical_pad*2);
+ renderer->render_text(font_title, text_x+2, text_y+2, title, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_title, text_x, text_y, title, COLOR_TEXT);
+ }
+ {
+ char* text = "An error occured, please verify\nyour game files.";
+ font* font_info = FONT_REGULAR(SIZE_RD(area.w, 28));
+ s32 text_w = panel_w - (horizontal_pad*4);
+ s32 text_x = screen_center_x - (text_w/2);
+ s32 text_y = screen_center_y - (font_info->px_h/2);
+ renderer->render_text_cutoff(font_info, text_x+2, text_y+2, text, COLOR_TEXT_SHADOW, 9999);
+ renderer->render_text_cutoff(font_info, text_x, text_y, text, COLOR_TEXT, 9999);
+ }
+
+ }
+
+ // back button
+ {
+ s32 back_h = img_back->height * scale/2;
+ s32 back_w = img_back->width * scale/2;
+ s32 back_x = panel_x + (panel_item_size/3);
+ s32 back_y = panel_y + panel_h - (back_h/2) - 1;
+
+ if (push_back_button(scale, back_x, back_y, back_w, back_h)) {
+ game_set_active_scene(GAME_STATE_MENU);
+ }
+ }
+}
+
+void error_scene_render(platform_window* window)
+{
+ menu_draw_background(window);
+ error_scene_draw_info(window);
+}
+
+void error_scene_update(platform_window* window)
+{
+
+}
+
+void error_scene_destroy()
+{
+
+} \ No newline at end of file
diff --git a/src/scenes/loading_scene.c b/src/scenes/loading_scene.c
new file mode 100644
index 0000000..2240aa8
--- /dev/null
+++ b/src/scenes/loading_scene.c
@@ -0,0 +1,85 @@
+
+#define MAX_CREDITED_NAMES 5
+#define MAX_CREDIT_NAME_LENGTH 30
+#define COMPLETE_CREDIT_LENGTH (MAX_CREDIT_NAME_LENGTH*MAX_CREDITED_NAMES)+20
+char complete_credit_text[COMPLETE_CREDIT_LENGTH];
+
+void loading_scene_init()
+{
+ strcpy(complete_credit_text, "Music by ");
+
+ // Load names to credit.
+ {
+ platform_set_active_directory(binary_path);
+
+ array files = array_create(sizeof(found_file));
+ array filters = string_split("AUTHOR.txt");
+ bool is_cancelled = false;
+ platform_list_files_block(&files, "data/music/", filters, true, 0, true, &is_cancelled, 0);
+ log_assert(files.length <= MAX_CREDITED_NAMES, "Not enough space for credited names.");
+ for (s32 i = 0; i < files.length; i++)
+ {
+ found_file *file = array_at(&files, i);
+
+ if (platform_file_exists(file->path))
+ {
+ file_content name = platform_read_file_content(file->path, "rb");
+ if (name.file_error) continue;
+
+ string_appendn(complete_credit_text, name.content, COMPLETE_CREDIT_LENGTH);
+ if (i != files.length-1) {
+ string_appendn(complete_credit_text, ", ", COMPLETE_CREDIT_LENGTH);
+ }
+ platform_destroy_file_content(&name);
+ }
+
+ mem_free(file->matched_filter);
+ mem_free(file->path);
+ }
+
+ array_destroy(&files);
+ array_destroy(&filters);
+ }
+}
+
+void loading_scene_render(platform_window* window)
+{
+ renderer->render_rectangle(area.x, area.y, area.w, area.h, COLOR_WHITE);
+
+ font* font_reg = FONT_REGULAR(SIZE_RD(area.w, 36));
+ s32 target_size = area.h/5;
+ s32 logo_text_pad = 20;
+ s32 total_height = target_size + logo_text_pad + font_reg->px_h;
+
+ s32 screen_center_x = area.x + (area.w/2);
+ s32 screen_center_y = area.y + (area.h/2);
+ s32 logo_x = screen_center_x - (target_size/2);
+ s32 logo_y = screen_center_y - (total_height/2);
+ renderer->render_image(img_logo, logo_x, logo_y, target_size, target_size);
+
+ s32 text_y = logo_y + target_size + logo_text_pad;
+ char* company_name = "Tar Software";
+ s32 company_name_width = renderer->calculate_text_width(font_reg, company_name);
+ s32 text_x = screen_center_x - (company_name_width/2);
+
+ renderer->render_text(font_reg, text_x, text_y, company_name, COLOR_TITLE);
+
+ // Credits
+ font* font_s = FONT_REGULAR(SIZE_RD(area.w, 20));
+ s32 credit_pad = 30*scale;
+ renderer->render_text(font_s, area.x + credit_pad, area.y+area.h-credit_pad-font_s->px_h, complete_credit_text, COLOR_TITLE);
+}
+
+void loading_scene_update(platform_window* window)
+{
+ platform_set_cursor(window, CURSOR_LOADING);
+
+ if (global_asset_collection.done_loading_assets) {
+ game_set_active_scene(GAME_STATE_MENU);
+ }
+}
+
+void loading_scene_destroy()
+{
+
+} \ No newline at end of file
diff --git a/src/scenes/loading_world_scene.c b/src/scenes/loading_world_scene.c
new file mode 100644
index 0000000..3d15909
--- /dev/null
+++ b/src/scenes/loading_world_scene.c
@@ -0,0 +1,116 @@
+
+void loading_world_scene_init()
+{
+
+}
+
+u64 load_start_stamp = 0;
+
+static void* start_loading_world_t(void* arg)
+{
+ load_start_stamp = platform_get_time(TIME_FULL, TIME_US); // Used for displaying info texts.
+ #ifdef MODE_DEBUG
+ // thread_sleep(1000*200);
+ #endif
+
+ char* path = (char*)arg;
+ world* world_to_load = 0;
+
+ if (path) {
+ // Load from file here
+ }
+ else {
+ world_to_load = world_create_new();
+ }
+
+ #ifdef MODE_DEBUG
+ u64 load_end_stamp = platform_get_time(TIME_FULL, TIME_US);
+ u64 elapsed_ns = load_end_stamp - load_start_stamp;
+ char info_msg[50];
+ sprintf(info_msg, "Loaded world in %.2fms", elapsed_ns/1000.0f);
+ log_info(info_msg);
+ #endif
+
+ if (!world_to_load) {
+ log_info("Failed to load world");
+ game_set_active_scene(GAME_STATE_ERROR);
+ }
+ else {
+ world_map_set_active_world(world_to_load);
+ game_set_active_scene(GAME_STATE_WORLD_MAP);
+ }
+
+ return 0;
+}
+
+void start_loading_world(char* saved_file_path)
+{
+ game_set_active_scene(GAME_STATE_LOADING_WORLD);
+ thread_start(start_loading_world_t, saved_file_path);
+}
+
+static void loading_world_draw_info_texts(font* font, s32 center_x, s32 y)
+{
+ char* texts[5] = {
+ "(Building cities)",
+ "(Connecting roads)",
+ "(Manufacturing trucks)",
+ "(Finding truck drivers)",
+ "(This is taking very long..)",
+ };
+
+ u64 load_end_stamp = platform_get_time(TIME_FULL, TIME_US);
+ u64 elapsed_ns = load_end_stamp - load_start_stamp;
+ float elapsed_sec = elapsed_ns / 1000.0f;
+ int text_index = elapsed_sec / 2000.0f; // 2 sec per text.
+ if (text_index >= 5) text_index = 4;
+
+ s32 text_len = renderer->calculate_text_width(font, texts[text_index]);
+ renderer->render_text(font, center_x - (text_len/2), y, texts[text_index], COLOR_TEXT);
+}
+
+static void loading_world_draw_animation(platform_window* window)
+{
+ s32 screen_center_x = area.x + (area.w/2);
+ s32 screen_center_y = area.y + (area.h/2);
+
+ float vertical_pad = 20 * scale;
+ float horizontal_pad = vertical_pad;
+ float spacing = 5 * scale;
+
+ s32 panel_h = 280 * scale;
+ s32 panel_item_size = (panel_h - (vertical_pad*2) - (spacing*1)) / 2;
+ s32 panel_w = (panel_item_size * 3) + (horizontal_pad*2) + (spacing*2);
+
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = screen_center_y - (panel_h/2);
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ s32 carwheel_size = panel_h/4;
+
+ static float rotation = 0.0f;
+ rotation -= 0.05f;
+ gl_render_set_rotation(rotation);
+ renderer->render_image(img_carwheel, screen_center_x - (carwheel_size/2), screen_center_y - (carwheel_size/2), carwheel_size, carwheel_size);
+ gl_render_set_rotation(0.0f);
+
+
+ font* font_info = FONT_REGULAR(SIZE_RD(area.w, 28));
+ loading_world_draw_info_texts(font_info, screen_center_x, panel_y + panel_h - font_info->px_h - vertical_pad*2);
+}
+
+void loading_world_scene_render(platform_window* window)
+{
+ menu_draw_background(window);
+ loading_world_draw_animation(window);
+}
+
+void loading_world_scene_update(platform_window* window)
+{
+
+}
+
+void loading_world_scene_destroy()
+{
+
+} \ No newline at end of file
diff --git a/src/scenes/menu_scene.c b/src/scenes/menu_scene.c
new file mode 100644
index 0000000..73ec22d
--- /dev/null
+++ b/src/scenes/menu_scene.c
@@ -0,0 +1,93 @@
+
+void menu_scene_init()
+{
+
+}
+
+static void menu_draw_background(platform_window* window)
+{
+ vec4 area = camera_get_target_rectangle(window);
+ renderer->render_rectangle(area.x, area.y, area.w, area.h, COLOR_WORLD_MAP_BACKGROUND);
+ renderer->render_image(img_world_map, area.x, area.y, area.w, area.h);
+}
+
+static void menu_draw_options(platform_window* window)
+{
+ s32 screen_center_x = area.x + (area.w/2);
+ s32 screen_center_y = area.y + (area.h/2);
+
+ s32 panel_w = 198 * scale;
+ s32 panel_h = 193 * scale;
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = screen_center_y - (panel_h/2);
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ s32 button_w = 178 * scale;
+ s32 button_h = 37 * scale;
+ s32 vertical_pad = 10 * scale;
+ s32 pad_x = (panel_w - button_w)/2;
+ float pad_y = (panel_h - (vertical_pad*2) - button_h*4)/5.0f;
+
+ if (button_render(scale, BUTTON_ENABLED, "New Game", panel_x + pad_x, vertical_pad + panel_y + pad_y*1, button_w, button_h))
+ {
+ start_loading_world(0); // Start new world
+ }
+
+ if (button_render(scale, BUTTON_ENABLED, "Continue", panel_x + pad_x, vertical_pad + panel_y + pad_y*2 + button_h*1, button_w, button_h))
+ {
+ game_set_active_scene(GAME_STATE_SELECT_SAVE);
+ }
+
+ if (button_render(scale, BUTTON_ENABLED, "Settings", panel_x + pad_x, vertical_pad + panel_y + pad_y*3 + button_h*2, button_w, button_h))
+ {
+ game_set_active_scene(GAME_STATE_SETTINGS);
+ }
+
+ if (button_render(scale, BUTTON_ENABLED, "Quit", panel_x + pad_x, vertical_pad + panel_y + pad_y*4 + button_h*3, button_w, button_h))
+ {
+ window->is_open = false;
+ }
+}
+
+static void menu_draw_title(platform_window* window)
+{
+ s32 panel_w = 198 * scale;
+ s32 panel_h = 70 * scale;
+ s32 panel_pad = 50 * scale;
+ s32 panel_x = area.x + panel_pad;
+ s32 panel_y = area.y + area.h - panel_h - panel_pad;
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ font* font_reg = FONT_REGULAR(SIZE_RD(area.w, 44));
+ font* font_sml = FONT_REGULAR(SIZE_RD(area.w, 20));
+ s32 text_pad = 5*scale;
+ s32 total_text_h = font_reg->px_h + text_pad + font_sml->px_h;
+ s32 text_y = panel_y + (panel_h/2) - (total_text_h/2);
+ char* game_title = "TruckerX";
+ char* game_version = "rev "GAME_VERSION;
+ s32 game_title_width = renderer->calculate_text_width(font_reg, game_title);
+ s32 text_x = panel_x + (panel_w/2) - (game_title_width/2);
+
+ renderer->render_text(font_reg, text_x+2, text_y+2, game_title, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_reg, text_x, text_y, game_title, COLOR_TEXT);
+
+ renderer->render_text(font_sml, 10*scale + text_x+2,font_reg->px_h + text_pad + text_y+2, game_version, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_sml, 10*scale + text_x, font_reg->px_h + text_pad + text_y, game_version, COLOR_TEXT);
+}
+
+void menu_scene_render(platform_window* window)
+{
+ menu_draw_background(window);
+ menu_draw_options(window);
+ menu_draw_title(window);
+}
+
+void menu_scene_update(platform_window* window)
+{
+
+}
+
+void menu_scene_destroy()
+{
+
+} \ No newline at end of file
diff --git a/src/scenes/place_detail.c b/src/scenes/place_detail.c
new file mode 100644
index 0000000..699fe0e
--- /dev/null
+++ b/src/scenes/place_detail.c
@@ -0,0 +1,1771 @@
+typedef enum t_place_detail_state
+{
+ PLACE_DETAIL_SHOW_MAIN,
+ PLACE_DETAIL_SHOW_RESUMES,
+ PLACE_DETAIL_SHOW_DEALERS,
+ PLACE_DETAIL_SHOW_EMPLOYEE,
+ PLACE_DETAIL_SHOW_SCHEDULE,
+} place_detail_state;
+
+typedef enum t_place_detail_info_state
+{
+ PLACE_DETAIL_EMPLOYEES = 0,
+ PLACE_DETAIL_JOBOFFERS = 1,
+ PLACE_DETAIL_SCHEDULE = 2,
+ PLACE_DETAIL_GARAGE = 3,
+} place_detail_info_state;
+
+typedef enum t_schedule_state
+{
+ SCHEDULING_JOB,
+ RESCHEDULING_JOB,
+ VIEWING,
+} schedule_state;
+
+// Animations
+#define TAG_ANIMATION_DURATION 100
+#define EMPLOYEE_SELECTOR_ANIMATION_DURATION 100
+#define TRUCK_SWAP_ANIMATION_DURATION 200
+animation tag_animation = {0,0,0,1};
+animation employee_selector_animation = {0,0,0,1};
+animation truck_swap_animation = {0,0,0,1};
+
+// States
+world_location* _active_location;
+employee* _active_employee;
+scheduled_job _active_scheduling_job;
+scheduled_job* _active_selected_scheduled_job;
+schedule_state _active_schedule_state = VIEWING;
+s32 _active_schedule_selected_job_index = 0;
+place_detail_info_state selected_tab_index = PLACE_DETAIL_EMPLOYEES;
+place_detail_state current_detail_state = PLACE_DETAIL_SHOW_MAIN;
+static s16 selected_truck_index = 0;
+static u8 active_dealer_index = 0;
+#define INVALID_VAL -999
+static s32 index_of_truck = INVALID_VAL;
+
+typedef struct t_tab
+{
+ s32 x;
+ s32 y;
+ s32 w;
+ s32 h;
+ float scale;
+} tab;
+
+void _goto_default_detail_state()
+{
+ index_of_truck = INVALID_VAL;
+ selected_truck_index = 0;
+ active_dealer_index = 0;
+ _active_employee = 0;
+ _active_schedule_state = VIEWING;
+ current_detail_state = PLACE_DETAIL_SHOW_MAIN;
+ _active_schedule_selected_job_index = 0;
+ _active_selected_scheduled_job = 0;
+ keyboard_set_input_text("");
+ _global_keyboard.take_input = false;
+}
+
+void place_detail_set_active_location(world_location* location)
+{
+ _active_location = location;
+ selected_tab_index = PLACE_DETAIL_EMPLOYEES;
+ current_detail_state = PLACE_DETAIL_SHOW_MAIN;
+}
+
+void place_detail_scene_init()
+{
+
+}
+
+static char* get_shipday_list_string(job_offer* offer, char* buf, s32 len)
+{
+ memset(buf, 0, len);
+ bool first = true;
+ if (job_offer_has_ship_day(offer, MONDAY)) { string_appendn(buf, "Mon", len); first = false; }
+ if (job_offer_has_ship_day(offer, TUESDAY)) { if (!first) string_appendn(buf, ", ", len); string_appendn(buf, "Tue", len); first = false; }
+ if (job_offer_has_ship_day(offer, WEDNESDAY)) { if (!first) string_appendn(buf, ", ", len); string_appendn(buf, "Wed", len); first = false; }
+ if (job_offer_has_ship_day(offer, THURSDAY)) { if (!first) string_appendn(buf, ", ", len); string_appendn(buf, "Thu", len); first = false; }
+ if (job_offer_has_ship_day(offer, FRIDAY)) { if (!first) string_appendn(buf, ", ", len); string_appendn(buf, "Fri", len); first = false; }
+ if (job_offer_has_ship_day(offer, SATURDAY)) { if (!first) string_appendn(buf, ", ", len); string_appendn(buf, "Sat", len); first = false; }
+ if (job_offer_has_ship_day(offer, SUNDAY)) { if (!first) string_appendn(buf, ", ", len); string_appendn(buf, "Sun", len); first = false; }
+
+ return buf;
+}
+
+static scheduled_job* get_scheduled_job_at_time(s32 day, s32 timeslot)
+{
+ for (s32 i = 0; i < _active_location->schedule.jobs.length; i++) {
+ scheduled_job* slot = array_at(&_active_location->schedule.jobs, i);
+ for (s32 x = 0; x < slot->offer.shipday_count; x++) {
+ if (slot->timeslots[x].day == day && slot->timeslots[x].timeslot == timeslot) return slot;
+ }
+ }
+ return 0;
+}
+
+static s32 find_empty_timeslot_for_day(s32 day)
+{
+ log_assert(day >= 0 && day < NUM_DAYS, "Invalid day");
+ for (s32 i = 0; i < TIME_SLOTS_PER_DAY; i++) {
+ scheduled_job* job = get_scheduled_job_at_time(day, i);
+ if (!job || (_active_schedule_state == RESCHEDULING_JOB && job == _active_selected_scheduled_job)) return i;
+ }
+ return -1;
+}
+
+static s32 find_empty_timeslot_for_day_right_to_left(s32 day)
+{
+ log_assert(day >= 0 && day < NUM_DAYS, "Invalid day");
+ for (s32 i = TIME_SLOTS_PER_DAY-1; i >= 0; i--) {
+ scheduled_job* job = get_scheduled_job_at_time(day, i);
+ if (!job || (_active_schedule_state == RESCHEDULING_JOB && job == _active_selected_scheduled_job)) return i;
+ }
+ return -1;
+}
+
+
+static scheduled_job create_empty_job_schedule(job_offer* job)
+{
+ scheduled_job new_job;
+ new_job.location = _active_location;
+ new_job.offer = *job;
+ new_job.trust = 1.0f;
+ for (s32 i = 0; i < MAX_SHIPDAYS; i++) {
+ new_job.timeslots[i] = (scheduled_job_time){-1, -1, 0, 0};
+ }
+ for (s32 i = 0; i < job->shipday_count; i++) {
+ new_job.timeslots[i] = (scheduled_job_time){job->shipdays[i], find_empty_timeslot_for_day(job->shipdays[i]), 0, 0};
+ }
+ return new_job;
+}
+
+#define scroll_speed 15
+#define HANDLE_TAB_SCROLL\
+ bool hovering_tab = (_global_mouse.y >= orig_y && _global_mouse.y <= orig_y + h \
+ && _global_mouse.x >= x && _global_mouse.x <= x + w);\
+ if (scroll_h > 0) {\
+ if (hovering_tab) {\
+ if (_global_mouse.scroll_state < 0) current_scroll += scroll_speed;\
+ if (_global_mouse.scroll_state > 0) current_scroll -= scroll_speed;\
+ }\
+ if (current_scroll > scroll_h) current_scroll = scroll_h;\
+ if (current_scroll < 0) current_scroll = 0;\
+ y -= current_scroll;\
+ }
+
+#define HANDLE_TAB_START(_count, _parts)\
+ s32 item_h = 34 * tab.scale;\
+ s32 total_h = (item_h+1) * (_count) + (10 * tab.scale);\
+ s32 scroll_h = total_h - h;\
+ s32 orig_y = y;\
+ static s32 current_scroll = 0;\
+ font* fnt = fnt_rd16;\
+ s32 info_text_h = fnt->px_h*4;\
+ scroll_h += info_text_h;\
+ s32 item_part_w = (w-item_h) / _parts;\
+ (void)item_part_w;
+
+#define HANDLE_TAB_INFO(_info, _btn_name, _next_state)\
+ s32 btn_h = info_text_h/1.3;\
+ s32 btn_w = btn_h*4;\
+ total_h += info_text_h;\
+ s32 info_text_y = y + (btn_h/2)-(fnt->px_h/2);\
+ renderer->render_text(fnt, x+1, info_text_y+1, _info, COLOR_TEXT_SHADOW);\
+ renderer->render_text(fnt, x, info_text_y, _info, COLOR_TEXT);\
+ if (_btn_name && button_render(tab.scale, true, _btn_name, x+w-btn_w-1, y, btn_w, btn_h)) {\
+ current_detail_state = _next_state;\
+ }\
+ y += info_text_h;
+
+#define HANDLE_TAB_ITEM_INTERACTION(_take) HANDLE_TAB_ITEM_INTERACTIONX(_take, i);
+#define HANDLE_TAB_ITEM_INTERACTIONX(_take, _count)\
+ s32 item_y = y + ((item_h+1) * _count);\
+ bool hovered = (_global_mouse.x >= x && _global_mouse.x <= x + w\
+ && _global_mouse.y >= item_y && _global_mouse.y <= item_y + item_h\
+ && hovering_tab);\
+ color tint = COLOR_LIST_ENTRY_BACKGROUND;\
+ if (_take && hovered) {\
+ tint = COLOR_LIST_ENTRY_BACKGROUND_ACTIVE;\
+ platform_set_cursor(window, CURSOR_POINTER);\
+ }\
+ renderer->render_rectangle(x, item_y, w, item_h, tint);
+
+#define TAB_ITEM_PUSH_TEXT(_buf,_width,_total)\
+ s32 text_w = renderer->calculate_text_width(fnt, _buf);\
+ s32 x_start = (x + item_h*2 - text_pad) + _total;\
+ s32 text_x = x_start + (_width/2)-(text_w/2);\
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);\
+ renderer->render_text(fnt, text_x+1, text_y+1, _buf, COLOR_TEXT_SHADOW);\
+ renderer->render_text(fnt, text_x, text_y,_buf, COLOR_TEXT);\
+ renderer->render_rectangle(x_start + _width, item_y+(5*tab.scale), 1, item_h-+(10*tab.scale), COLOR_BUTTON);\
+ _total += _width;
+
+#define PUSH_NOT_AVAILABLE_TEXT(_cond, _text)\
+ if (_cond) {\
+ font* fnt = fnt_rd20;\
+ char* txt = _text;\
+ s32 textw = renderer->calculate_text_width(fnt, txt);\
+ renderer->render_text(fnt, x+(w/2)-(textw/2), y+(tab.h/2)-(fnt->px_h/2)-(30*scale), txt, COLOR_TEXT);\
+ }
+
+static void place_detail_draw_job_offers(platform_window* window, tab tab, float x, float y, float w, float h)
+{
+ HANDLE_TAB_START(_active_location->job_offers.length, 2);
+ HANDLE_TAB_SCROLL;
+
+ char info_buf[50];
+ sprintf(info_buf, "%d Job offers open.", _active_location->job_offers.length);
+ HANDLE_TAB_INFO(info_buf, 0, 0);
+
+ PUSH_NOT_AVAILABLE_TEXT(!_active_location->job_offers.length, "No job offers available.");
+
+ for (s32 i = 0; i < _active_location->job_offers.length; i++) {
+ job_offer* offer = array_at(&_active_location->job_offers, i);
+ HANDLE_TAB_ITEM_INTERACTION(true);
+
+ if (hovered && is_left_clicked()) {
+ current_detail_state = PLACE_DETAIL_SHOW_SCHEDULE;
+ _active_schedule_state = SCHEDULING_JOB;
+ _active_schedule_selected_job_index = 0;
+ _active_selected_scheduled_job = 0;
+ _active_scheduling_job = create_empty_job_schedule(offer);
+
+ tag_animation = animation_create(TAG_ANIMATION_DURATION);
+ tag_animation.started = true;
+ audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);
+ }
+
+ float icon_pad = item_h * 0.15;
+ float text_pad = icon_pad*2;
+ float icon_s = item_h - (icon_pad*2);
+ // icon
+ {
+ renderer->render_image(offer->company->logo, x + icon_pad, item_y + icon_pad, icon_s*2, icon_s);
+ renderer->render_rectangle(x + icon_pad + icon_s*2 + icon_pad, item_y+(5*tab.scale), 1, item_h-+(10*tab.scale), COLOR_BUTTON);
+ }
+
+ float width_of_piece = 80 * tab.scale;
+ float price_text_w = 0;
+
+ // price
+ {
+ char pricebuf[25];
+ sprintf(pricebuf, "$%d/trip", offer->reward);
+ TAB_ITEM_PUSH_TEXT(pricebuf, width_of_piece, price_text_w);
+ }
+
+ // distance
+ {
+ char pricebuf[25];
+ sprintf(pricebuf, "%.0fkm", offer->total_distance);
+ TAB_ITEM_PUSH_TEXT(pricebuf, width_of_piece, price_text_w);
+ }
+
+ // dur
+ {
+ char pricebuf[25];
+ sprintf(pricebuf, "%.0fh-%.0fh", offer->duration_sec_min/3600.0f, offer->duration_sec_max/3600.0f);
+ TAB_ITEM_PUSH_TEXT(pricebuf, width_of_piece, price_text_w);
+ }
+
+ // Name
+ {
+ char daybuf[50];
+ char buf[200];
+ sprintf(buf, "%s wants you to ship %s to %s every %s", offer->company->name, offer->product->name,
+ (*(world_location**)array_at(&offer->connections, offer->connections.length-1))->name, get_shipday_list_string(offer, daybuf, 50));
+ s32 text_x = x + item_h*2 + price_text_w + icon_pad;
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);
+ renderer->render_text(fnt, text_x+1, text_y+1, buf, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, buf, COLOR_TEXT);
+ }
+ }
+}
+
+
+static void place_detail_draw_trucks(platform_window* window, tab tab, float x, float y, float w, float h)
+{
+ HANDLE_TAB_START(_active_location->trucks.length, 5);
+ HANDLE_TAB_SCROLL;
+
+ char info_buf[50];
+ sprintf(info_buf, "%d Trucks in garage.", _active_location->trucks.length);
+ HANDLE_TAB_INFO(info_buf, "Purchase", PLACE_DETAIL_SHOW_DEALERS);
+
+ PUSH_NOT_AVAILABLE_TEXT(!_active_location->trucks.length, "No trucks in garage.");
+
+ for (s32 i = 0; i < _active_location->trucks.length; i++) {
+ truck* emp = array_at(&_active_location->trucks, i);
+ HANDLE_TAB_ITEM_INTERACTION(false);
+
+ float icon_pad = item_h * 0.15;
+ float text_pad = icon_pad*2;
+ float icon_s = item_h - (icon_pad*2);
+ // icon
+ {
+ renderer->render_image(emp->logo, x + icon_pad, item_y + icon_pad, icon_s, icon_s);
+ renderer->render_rectangle(x + icon_pad + icon_s + icon_pad, item_y+(5*tab.scale), 1, item_h-+(10*tab.scale), COLOR_BUTTON);
+ }
+
+ // Name
+ {
+ char buffer[MAX_WORLD_LOCATION_NAME_LENGTH + 20];
+ sprintf(buffer, "%s, ID: #%d", emp->name, emp->id);
+
+ s32 text_x = x + item_h + text_pad;
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);
+ renderer->render_text(fnt, text_x+1, text_y+1, buffer, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, buffer, COLOR_TEXT);
+ renderer->render_rectangle(x + item_h + item_part_w, item_y+(5*tab.scale), 1, item_h-+(10*tab.scale), COLOR_BUTTON);
+ }
+
+ // current location
+ {
+ char buffer[MAX_EMPLOYEE_NAME_LENGTH + 20];
+ if (emp->assigned_employee) sprintf(buffer, "Assigned to %s", emp->assigned_employee->name);
+ else strcpy(buffer, "No assignee");
+ s32 text_x = x + item_h + item_part_w*1 + text_pad;
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);
+ renderer->render_text(fnt, text_x+1, text_y+1, buffer, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, buffer, COLOR_TEXT);
+ }
+ }
+}
+
+static void place_detail_draw_employees(platform_window* window, tab tab, float x, float y, float w, float h)
+{
+ HANDLE_TAB_START(_active_location->employees.length+2, 4);
+ HANDLE_TAB_SCROLL;
+
+ char info_buf[50];
+ sprintf(info_buf, "%d Employees currently on site.", _active_location->employees.length);
+ HANDLE_TAB_INFO(info_buf, "Hire", PLACE_DETAIL_SHOW_RESUMES);
+
+ s32 current_index = 0;
+ bool show_internal = true;
+ do_again:;
+
+ for (s32 i = 0; i <= _active_location->employees.length; i++) {
+ employee* emp = 0;
+ if (i > 0) {
+ emp = *(employee**)array_at(&_active_location->employees, i-1);
+ bool is_internal = (emp->original_location_id == _active_location->id);
+ if (show_internal != is_internal) continue;
+ }
+
+ HANDLE_TAB_ITEM_INTERACTIONX(i != 0, current_index);
+ if (i != 0 && hovered && is_left_clicked()) {
+ place_detail_show_employee_detail(emp);
+ audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);
+ }
+ current_index++;
+
+ if (i == 0) {
+ char* buffer = show_internal ? "Internal" : "External";
+ s32 text_x = x + item_h;
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);
+ renderer->render_text(fnt, text_x+1, text_y+1, buffer, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, buffer, COLOR_TEXT);
+ continue;
+ }
+
+ float icon_pad = item_h * 0.15;
+ float text_pad = icon_pad*2;
+ float icon_s = item_h - (icon_pad*2);
+ // icon
+ {
+ draw_employee_portrait(emp, x + icon_pad, item_y + icon_pad, icon_s, icon_s);
+ renderer->render_rectangle(x + icon_pad + icon_s + icon_pad, item_y+(5*tab.scale), 1, item_h-+(10*tab.scale), COLOR_BUTTON);
+ }
+
+ // Name
+ {
+ char buffer[MAX_EMPLOYEE_NAME_LENGTH + 20];
+ sprintf(buffer, "%s, ID: #%d", emp->name, emp->id);
+ s32 text_x = x + item_h + text_pad;
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);
+ renderer->render_text(fnt, text_x+1, text_y+1, buffer, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, buffer, COLOR_TEXT);
+ renderer->render_rectangle(x + item_h + item_part_w, item_y+(5*tab.scale), 1, item_h-(10*tab.scale), COLOR_BUTTON);
+ }
+
+ // Name
+ {
+ char buffer[MAX_EMPLOYEE_NAME_LENGTH + 20];
+ if (emp->assigned_truck) sprintf(buffer, "Truck: %s, #%d", emp->assigned_truck->name, emp->assigned_truck->id);
+ else strcpy(buffer, "No assigned truck");
+ s32 text_x = x + item_h + item_part_w*1 + text_pad;
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);
+ renderer->render_text(fnt, text_x+1, text_y+1, buffer, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, buffer, COLOR_TEXT);
+ renderer->render_rectangle(x + item_h + item_part_w*2, item_y+(5*tab.scale), 1, item_h-(10*tab.scale), COLOR_BUTTON);
+ }
+
+ // current location
+ {
+ char buffer[MAX_WORLD_LOCATION_NAME_LENGTH + 20];
+ if (emp->active_job_id != INVALID_ID) {
+ active_job* j = get_active_job_by_id(_active_world, emp->active_job_id);
+ job_endpoints endpoints = job_offer_get_endpoints(&j->offer);
+ sprintf(buffer, "Driving to %s", endpoints.dest->name);
+ }
+ else if (emp->current_location_id != _active_location->id) { // employee located at other location
+ sprintf(buffer, "Located at %s", get_world_location_by_id(_active_world, emp->current_location_id)->name);
+ }
+ else { // At original location
+ sprintf(buffer, "%s", "Idling");
+ }
+
+ s32 text_x = x + item_h + item_part_w*2 + text_pad;
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);
+ renderer->render_text(fnt, text_x+1, text_y+1, buffer, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, buffer, COLOR_TEXT);
+ renderer->render_rectangle(x + item_h + item_part_w*3, item_y+(5*tab.scale), 1, item_h-+(10*tab.scale), COLOR_BUTTON);
+ }
+
+ // Happiness
+ {
+ s32 text_x = x + item_h + item_part_w*3 + text_pad;
+ s32 text_y = item_y + (item_h/2) - (fnt->px_h/2);
+
+ s32 stars = employee_calculate_happiness_stars(emp);
+ s32 textw = renderer->render_text(fnt, text_x, text_y, "Happiness: ", COLOR_TEXT);
+ text_x += (scale*5);
+ for (s32 i = 0; i < 5; i++)
+ {
+ if (i < stars) renderer->render_image(img_star, text_x + textw + (i*(fnt->px_h+2)), text_y, fnt->px_h, fnt->px_h);
+ else renderer->render_image_tint(img_star, text_x + textw + (i*(fnt->px_h+2)), text_y, fnt->px_h, fnt->px_h, rgba(255,255,255,30));
+ }
+
+ //renderer->render_rectangle(x + item_h + item_part_w*4, item_y+(5*tab.scale), 1, item_h-+(10*tab.scale), COLOR_BUTTON);
+ }
+ }
+
+ if (show_internal) {
+ show_internal = false;
+ goto do_again;
+ }
+}
+
+static void render_logo_at(image* img, s32 pad, s32 x, s32 y, s32 w, s32 h)
+{
+ if (img->width >= img->height)
+ {
+ float s_scale = w/(float)img->width;
+ float new_w = w - (pad*2);
+ float new_h = (img->height * s_scale) - (pad*2);
+ float new_x = x + w/2 - new_w/2;
+ float new_y = y + h/2 - new_h/2;
+ renderer->render_image(img,new_x,new_y,new_w,new_h);
+ }
+}
+
+static void place_detail_draw_active_dealer(platform_window* window, truck_dealer* dealer, float scale, s32 x, s32 y, s32 w, s32 h)
+{
+ button_render(scale, false, 0, x, y, w, h);
+ static truck_dealer* prev_dealer = 0;
+ if (prev_dealer != dealer) {
+ prev_dealer = dealer;
+ selected_truck_index = 0;
+ }
+
+ if (selected_truck_index < 0) selected_truck_index = 0;
+ if (selected_truck_index >= dealer->trucks.length) selected_truck_index = dealer->trucks.length-1;
+ truck* tr_curr = (selected_truck_index >= 0 && selected_truck_index <= dealer->trucks.length-1) ? array_at(&dealer->trucks, selected_truck_index) : 0;
+ truck* tr_prev = (selected_truck_index-1 >= 0) ? array_at(&dealer->trucks, selected_truck_index-1) : 0;
+ truck* tr_next = (selected_truck_index+1 <= dealer->trucks.length-1) ? array_at(&dealer->trucks, selected_truck_index+1) : 0;
+ if (!tr_curr) return;
+ s32 total_img_space = (w)/3 * 2;
+ s32 img_w_center = total_img_space/10*5;
+ s32 img_w_side = total_img_space/10*2.5;
+ s32 pad = 40 * scale;
+ s32 img_x = x;
+ s32 img_y = y;
+
+ static animation swap_animation = {0,0,0,0};
+ static animation purchase_animation = {0,0,0,0};
+ if (swap_animation.started) animation_update(&swap_animation);
+ if (purchase_animation.started) animation_update(&purchase_animation);
+ static s32 target_truck_index = 0;
+
+ truck* tn_prev = (target_truck_index-1 >= 0) ? array_at(&dealer->trucks, target_truck_index-1) : 0;
+ truck* tn_next = (target_truck_index+1 <= dealer->trucks.length-1) ? array_at(&dealer->trucks, target_truck_index+1) : 0;
+
+ // Truck images
+ {
+ s32 overlap = 50 * scale;
+
+ s32 y_side = img_y + (h / 2) - (img_w_side/2);
+ s32 x_left = img_x + overlap;
+ s32 y_left = y_side;
+ s32 w_left = img_w_side;
+ s32 h_left = img_w_side;
+
+ s32 x_right = img_x+img_w_side+img_w_center - overlap;
+ s32 y_right = y_side;
+ s32 w_right = img_w_side;
+ s32 h_right = img_w_side;
+
+ s32 x_center = img_x+img_w_side;
+ s32 y_center = img_y + (h / 2) - (img_w_center/2);
+ s32 w_center = img_w_center;
+ s32 h_center = img_w_center;
+
+ #define LI(_cx,_dx) (_cx + (_dx-_cx)*swap_animation.percentage)
+ #define LIP(_cx,_dx) (_cx + (_dx-_cx)*purchase_animation.percentage)
+ #define LI_TINT(_c,_d) rgba(255,255,255,_c + (_d-_c)*swap_animation.percentage)
+ #define LI_TINTP(_c,_d) rgba(255,255,255,_c + (_d-_c)*purchase_animation.percentage)
+
+ if (tr_prev) {
+ if (target_truck_index < selected_truck_index) {
+ color tint = LI_TINT(50, 255);
+ renderer->render_image_tint(tr_prev->logo,LI(x_left,x_center),LI(y_left,y_center),LI(w_left,w_center),LI(h_left,h_center), tint);
+
+ if (swap_animation.started && tn_prev) {
+ color tint = LI_TINT(0, 50);
+ renderer->render_image_tint(tn_prev->logo,x_left,y_left,w_left,h_left, tint);
+ }
+ }
+ else {
+ color tint = LI_TINT(50, 0);
+ renderer->render_image_tint(tr_prev->logo,x_left,y_left,w_left,h_left, tint);
+ }
+ }
+ if (tr_next) {
+ if (target_truck_index < selected_truck_index) {
+ color tint = LI_TINT(50, 0);
+ renderer->render_image_tint(tr_next->logo,x_right,y_right,w_right,h_right, tint);
+ }
+ else {
+ color tint = LI_TINT(50, 255);
+ renderer->render_image_tint(tr_next->logo,LI(x_right,x_center),LI(y_right,y_center),LI(w_right,w_center),LI(h_right,h_center), tint);
+
+ if (swap_animation.started && tn_next) {
+ color tint = LI_TINT(0, 50);
+ renderer->render_image_tint(tn_next->logo,x_right,y_right,w_right,h_right, tint);
+ }
+ }
+ }
+ if (!purchase_animation.started)
+ {
+ color tint = LI_TINT(255, 50);
+ if (target_truck_index < selected_truck_index)
+ renderer->render_image_tint(tr_curr->logo,LI(x_center,x_right),LI(y_center,y_right),LI(w_center,w_right),LI(h_center,h_right), tint);
+ else
+ renderer->render_image_tint(tr_curr->logo,LI(x_center,x_left),LI(y_center,y_left),LI(w_center,w_left),LI(h_center,h_left), tint);
+ }
+ else {
+ vec4 area = camera_get_target_rectangle(window);
+ s32 x_off = area.x;
+ s32 h_off = area.h/2;
+ s32 y_off = area.y+(area.h/2)-(h_off/2);
+ s32 w_off = h_off;
+
+ color tint = LI_TINTP(255, 0);
+ renderer->render_image_tint(tr_curr->logo,LIP(x_center,x_off),LIP(y_center,y_off),LIP(w_center,w_off),LIP(h_center,h_off), tint);
+ }
+ }
+
+ font* fnt = fnt_rd24;
+ s32 text_y = img_y + pad;
+ s32 text_x = x + total_img_space;
+
+ #define PUSH_TEXT(_str, _data)\
+ {char buf[50]; snprintf(buf,50,_str,_data);\
+ renderer->render_text(fnt, text_x+1, text_y+1, buf, COLOR_TEXT_SHADOW);\
+ renderer->render_text(fnt, text_x, text_y, buf, COLOR_TEXT);}\
+ text_y+=fnt->px_h+(10*scale);
+
+ PUSH_TEXT("Name: %s", tr_curr->name);
+ PUSH_TEXT("Power: %dhp", tr_curr->hp);
+ PUSH_TEXT("Price: $%d", tr_curr->price);
+ PUSH_TEXT("Fuel Capacity: %dL", tr_curr->fuelcapacity);
+ PUSH_TEXT("Torque %drpm", tr_curr->torque);
+ PUSH_TEXT("Fuel Usage %.1fL per 100Km", tr_curr->fuelusage);
+
+ #define TRUCK_SWAP_DELAY 200
+ #define TRUCK_PURCHASE_DELAY 800
+ s32 btn_size = 30*scale;
+ s32 btn_x = text_x;
+
+ button_type type1 = purchase_animation.started || swap_animation.started ? BUTTON_STATIC : (selected_truck_index > 0) ? BUTTON_ENABLED : BUTTON_DISABLED;
+ if (button_render(scale, type1, "<", btn_x, text_y, btn_size, btn_size)) {
+ if (!swap_animation.started) {
+ swap_animation = animation_create(TRUCK_SWAP_DELAY);
+ swap_animation.started = true;
+ target_truck_index = selected_truck_index-1;
+ }
+ }
+ btn_x += btn_size;
+ s32 center_btn_w = 100*scale;
+ button_type type2 = purchase_animation.started || swap_animation.started ? BUTTON_STATIC : BUTTON_ENABLED;
+ if (button_render(scale, type2, "purchase", btn_x, text_y, center_btn_w, btn_size)) {
+ ADD_EXPENSE(_active_world, _active_location, expenses_from_trucks, tr_curr->price)
+ add_truck_to_world_location(_active_world, _active_location, tr_curr);
+ purchase_animation = animation_create(TRUCK_PURCHASE_DELAY);
+ purchase_animation.started = true;
+ }
+ btn_x += center_btn_w;
+ button_type type3 = purchase_animation.started || swap_animation.started ? BUTTON_STATIC : (selected_truck_index < dealer->trucks.length-1) ? BUTTON_ENABLED : BUTTON_DISABLED;
+ if (button_render(scale, type3, ">", btn_x, text_y, btn_size, btn_size)) {
+ if (!swap_animation.started) {
+ swap_animation = animation_create(TRUCK_SWAP_DELAY);
+ swap_animation.started = true;
+ target_truck_index = selected_truck_index+1;
+ }
+ }
+ if (swap_animation.percentage == 1.0f) {
+ swap_animation = (animation){0,0,0,0};
+ selected_truck_index = target_truck_index;
+ }
+ if (purchase_animation.percentage == 1.0f) {
+ purchase_animation = (animation){0,0,0,0};
+ current_detail_state = PLACE_DETAIL_SHOW_MAIN;
+ selected_truck_index = 0;
+ active_dealer_index = 0;
+ }
+}
+
+void place_detail_show_schedule_with_highlighted_job(world_location* loc, scheduled_job* job, scheduled_job_time job_time) {
+ _goto_default_detail_state();
+ place_detail_set_active_location(loc);
+ game_set_active_scene(GAME_STATE_PLACE_DETAIL);
+
+ if (job) {
+ for (s32 i = 0; i < MAX_SHIPDAYS; i++) {
+ if (job_time.day == job->timeslots[i].day) {
+ _active_schedule_selected_job_index = i;
+ break;
+ }
+ }
+ }
+
+ _active_schedule_state = VIEWING;
+ _active_selected_scheduled_job = job;
+ selected_tab_index = PLACE_DETAIL_SHOW_MAIN;
+ current_detail_state = PLACE_DETAIL_SHOW_SCHEDULE;
+ tag_animation = animation_create(TAG_ANIMATION_DURATION);
+ tag_animation.started = true;
+}
+
+void place_detail_show_employee_detail(employee* emp) {
+ _goto_default_detail_state();
+ world_location* original_loc = get_world_location_by_id(_active_world, emp->original_location_id);
+ place_detail_set_active_location(original_loc);
+ game_set_active_scene(GAME_STATE_PLACE_DETAIL);
+ current_detail_state = PLACE_DETAIL_SHOW_EMPLOYEE;
+ _active_employee = emp;
+ tag_animation = animation_create(TAG_ANIMATION_DURATION);
+ tag_animation.started = true;
+}
+
+static s32 place_detail_draw_selected_employee_truck_selector(platform_window* window, s32 x, s32 y, s32 w, s32 h)
+{
+ s32 btn_w = 40*scale;
+ s32 pad = 30*scale;
+ s32 truck_h = h / 2;
+ s32 truck_w = truck_h;
+ s32 truck_x = x + (pad*2) + btn_w;
+ s32 truck_y = y + pad;
+
+ world_location* original_location = get_world_location_by_id(_active_world, _active_employee->original_location_id);
+
+ if (index_of_truck == INVALID_VAL) {
+ index_of_truck = _active_employee->assigned_truck ? ((void*)_active_employee->assigned_truck - (void*)original_location->trucks.data) / sizeof(truck) : -1;
+ }
+
+ s32 bg_h = h;
+ s32 bg_w = truck_w + (btn_w*2) + (pad*4);
+ button_draw_background(scale, x,y,bg_w,bg_h, COLOR_WHITE, COLOR_BUTTON);
+
+ static bool animation_going_left = false;
+
+ // Draw truck info
+ {
+ #define TRUCK_ANIMATION_OFFSET (animation_going_left ? -60*scale : 60*scale)
+ s32 truck_offset_x = TRUCK_ANIMATION_OFFSET - (TRUCK_ANIMATION_OFFSET*truck_swap_animation.percentage);
+ color truck_tint = AN_LI_TINT(COLOR_WHITE, truck_swap_animation);
+
+ truck* selected_truck = 0;
+ if (index_of_truck != -1) selected_truck = array_at(&original_location->trucks, index_of_truck);
+ renderer->render_image_tint(selected_truck ? selected_truck->logo : img_truck_unknown, truck_x+truck_offset_x, truck_y, truck_w, truck_h, truck_tint);
+ if (selected_truck) {
+ font* fnt = fnt_rd24;
+ char buf[40];
+ sprintf(buf, "%s, ID: #%d", selected_truck->name, selected_truck->id);
+ s32 text_w = renderer->calculate_text_width(fnt, buf);
+ s32 text_x = truck_x + (truck_w/2)-(text_w/2);
+ renderer->render_text(fnt, text_x+1+truck_offset_x, truck_y+truck_h+1, buf, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x+truck_offset_x,truck_y+truck_h, buf, COLOR_TEXT);
+ }
+ }
+
+ // Change truck
+ {
+ if (button_render(scale, BUTTON_ENABLED, "<", truck_x-btn_w-pad,truck_y, btn_w, truck_h)) {
+ s32 prev_index = index_of_truck;
+ index_of_truck = -1;
+
+ try_again:
+ for (s32 i = prev_index-1; i >= 0; i--) {
+ truck* emp = array_at(&original_location->trucks, i);
+ if (emp->assigned_employee == 0 || emp->assigned_employee == _active_employee) {
+ index_of_truck = i;
+ break;
+ }
+ }
+
+ if (prev_index == -1) {
+ prev_index = original_location->trucks.length;
+ goto try_again;
+ }
+
+ truck_swap_animation = animation_create(TRUCK_SWAP_ANIMATION_DURATION);
+ truck_swap_animation.started = true;
+ animation_going_left = true;
+ }
+ if (button_render(scale, BUTTON_ENABLED, ">", truck_x+truck_w+pad,truck_y, btn_w, truck_h)) {
+ s32 prev_index = index_of_truck;
+ index_of_truck = -1;
+ for (s32 i = prev_index+1; i < original_location->trucks.length; i++) {
+ truck* emp = array_at(&original_location->trucks, i);
+ if (emp->assigned_employee == 0 || emp->assigned_employee == _active_employee) {
+ index_of_truck = i;
+ break;
+ }
+ }
+
+ truck_swap_animation = animation_create(TRUCK_SWAP_ANIMATION_DURATION);
+ truck_swap_animation.started = true;
+ animation_going_left = false;
+ }
+ }
+
+
+ // Save changes button
+ {
+ s32 btn_h = 30*scale;
+ btn_w = 160*scale;
+ s32 btn_y = y + h - pad - btn_h;
+ s32 btn_x = x + (bg_w/2) - (btn_w/2);
+ if (button_render(scale, BUTTON_ENABLED, "Save Changes", btn_x, btn_y, btn_w, btn_h)) {
+ if (index_of_truck != -1) {
+ if (_active_employee->assigned_truck) _active_employee->assigned_truck->assigned_employee = 0;
+ _active_employee->assigned_truck = array_at(&_active_location->trucks, index_of_truck);
+ log_assert(_active_employee->assigned_truck->assigned_employee == 0 &&
+ _active_employee->assigned_truck->assigned_employee != _active_employee, "Truck already has assignee");
+
+ _active_employee->assigned_truck->assigned_employee = _active_employee;
+ }
+ else {
+ if (_active_employee->assigned_truck) _active_employee->assigned_truck->assigned_employee = 0;
+ _active_employee->assigned_truck = 0;
+ }
+
+ index_of_truck = INVALID_VAL;
+ current_detail_state = PLACE_DETAIL_SHOW_MAIN;
+ }
+ }
+
+ animation_update(&truck_swap_animation);
+ return bg_w;
+}
+
+static void place_detail_draw_selected_employee(platform_window* window)
+{
+ float offset = scale * 40.0;
+ s32 item_h = 82 * 0.4 * scale;
+ s32 h = (area.h * 0.6 - (offset*2)) + item_h;
+ s32 w = (area.w * 0.9 - (offset*2));
+ s32 x = offset + area.x + (area.w*0.05);
+ s32 pos_y = area.y + (area.w*0.12);
+ s32 y = pos_y;
+
+ s32 truck_selector_w = place_detail_draw_selected_employee_truck_selector(window, x, y, w, h);
+
+ {
+ font* fnt = fnt_rd24;
+
+ s32 info_panel_x = x + truck_selector_w+offset;
+ s32 info_panel_w = w - offset - truck_selector_w;
+ s32 pad = 30*scale;
+
+ s32 text_y = y + pad;
+ s32 text_x = info_panel_x + pad;
+ s32 text_offset = fnt->px_h + (10*scale);
+ s32 info_h = (h/2)-(pad/2);
+ button_draw_background(scale, info_panel_x, y, info_panel_w, info_h, COLOR_WHITE, COLOR_BUTTON);
+
+ s32 portrait_h = (info_h)-(pad*2);
+ draw_employee_portrait(_active_employee, text_x, text_y, portrait_h, portrait_h);
+
+ text_x += portrait_h + pad;
+
+ #define PUSH_INFO_TEXT(_fmt, _data)\
+ {\
+ char buf[100];\
+ sprintf(buf, _fmt, _data);\
+ renderer->render_text(fnt, text_x, text_y, buf, COLOR_TEXT);\
+ text_y += text_offset;\
+ }
+
+ PUSH_INFO_TEXT("Name: %s", _active_employee->name);
+ PUSH_INFO_TEXT("Age: %d", _active_employee->age);
+ PUSH_INFO_TEXT("Experience: %d years", _active_employee->experience);
+ PUSH_INFO_TEXT("Salary: $%.2f/month", _active_employee->salary);
+
+ // Happiness
+ {
+ s32 stars = employee_calculate_happiness_stars(_active_employee);
+ s32 textw = renderer->render_text(fnt, text_x, text_y, "Happiness: ", COLOR_TEXT);
+ for (s32 i = 0; i < 5; i++)
+ {
+ if (i < stars) renderer->render_image(img_star, text_x + textw + (i*(fnt->px_h+2)), text_y, fnt->px_h, fnt->px_h);
+ else renderer->render_image_tint(img_star, text_x + textw + (i*(fnt->px_h+2)), text_y, fnt->px_h, fnt->px_h, rgba(255,255,255,30));
+ }
+ text_y += text_offset;
+ }
+
+ #define SALARY_RAISE_INCREASE 70
+ // Buttons
+ {
+ s32 pad_between_items = 10 * scale;
+ s32 button_w = 164 * scale;
+ s32 button_h = 37 * scale;
+ s32 btn_y = y+info_h+pad+info_h-button_h;
+ s32 btn_x = x + w - button_w;
+ if (button_render(scale, BUTTON_ENABLED, "End Contract", btn_x, btn_y, button_w, button_h)) {
+ audio_play_sound(snd_click3, AUDIO_CHANNEL_SFX_1);
+ end_contract_with_employee(_active_world, _active_employee);
+ _active_employee = 0;
+ _goto_default_detail_state();
+ }
+ btn_y -= (button_h+pad_between_items);
+
+ if (button_render(scale, BUTTON_ENABLED, "Give Raise", btn_x, btn_y, button_w, button_h)) {
+ audio_play_sound(snd_click2, AUDIO_CHANNEL_SFX_1);
+ _active_employee->salary += SALARY_RAISE_INCREASE;
+ }
+ btn_y -= (button_h+pad_between_items);
+ }
+ }
+}
+
+static void place_detail_draw_schedule_employee_options(platform_window* window, s32 pad, float scale, s32 x, s32 y, s32 w, s32 h)
+{
+ bool viewing = (_active_schedule_state == VIEWING && !_active_selected_scheduled_job);
+ if (viewing) return;
+
+ job_endpoints endpoints = job_offer_get_endpoints(&_active_scheduling_job.offer);
+ bool enabled = endpoints.dest->is_owned;
+
+ if (enabled) button_draw_background(scale, x,y,w,h, COLOR_WHITE, COLOR_BUTTON);
+ else button_draw_background(scale, x,y,w,h, COLOR_BUTTON_DISABLED_TINT, COLOR_BUTTON_DISABLED);
+
+ s32 checkbox_s = h/3;
+ scheduled_job_time selected_timeslot = _active_scheduling_job.timeslots[_active_schedule_selected_job_index];
+ if (_active_schedule_state == VIEWING) selected_timeslot = _active_selected_scheduled_job->timeslots[_active_schedule_selected_job_index];
+
+ char* check_txt = selected_timeslot.stay_at_destination ? "X" : "";
+ if (button_render(scale, enabled && _active_schedule_state != VIEWING ? BUTTON_ENABLED : BUTTON_DISABLED, check_txt, x+pad,y+(h/2)-(checkbox_s/2),checkbox_s,checkbox_s)) {
+ _active_scheduling_job.timeslots[_active_schedule_selected_job_index].stay_at_destination = !selected_timeslot.stay_at_destination;
+ }
+
+ font* fnt = fnt_rd20;
+ s32 textx = x + pad + checkbox_s + pad/2;
+ s32 texty = y+(h/2)-(fnt->px_h/2);
+ renderer->render_text(fnt, textx, texty, "Stay at destination", COLOR_TEXT);
+
+ if (!enabled) {
+ s32 icon_s = h/2;
+ renderer->render_image(img_lock, x + (w/2)-(icon_s/2), y + (h/2)-(icon_s/2), icon_s, icon_s);
+ }
+}
+
+static void place_detail_draw_schedule_employee_selector(platform_window* window, s32 pad, float scale, s32 x, s32 y, s32 w, s32 h)
+{
+ if (_active_schedule_state == VIEWING && !_active_selected_scheduled_job) {
+ button_render(scale, BUTTON_STATIC, 0, x,y,w,h);
+ font* fnt = fnt_rd20;
+ s32 texty = y + (h/2)-(fnt->px_h/2);
+ s32 textx = x + pad;
+ renderer->render_text(fnt, textx, texty, "Select a timeslot for details.", COLOR_TEXT);
+ }
+ else {
+ scheduled_job_time selected_timeslot = _active_scheduling_job.timeslots[_active_schedule_selected_job_index];
+
+ employee* selected_employee = selected_timeslot.assignee;
+ bool is_viewing_existing_timeslot = _active_selected_scheduled_job && _active_schedule_state == VIEWING;
+ if (is_viewing_existing_timeslot) {
+ selected_employee = _active_selected_scheduled_job->timeslots[_active_schedule_selected_job_index].assignee;
+ }
+
+ // When timeslot changes, set selected employee id as keyboard input text.
+ static s32 prev_selected_job_index = 0;
+ if (is_viewing_existing_timeslot) prev_selected_job_index = -1;
+ if (prev_selected_job_index != _active_schedule_selected_job_index) {
+ prev_selected_job_index = _active_schedule_selected_job_index;
+ if (selected_employee) {
+ char idbuf[20];
+ sprintf(idbuf, "%d", selected_employee->id);
+ keyboard_set_input_text(idbuf);
+ }
+ else {
+ keyboard_set_input_text("");
+ }
+ }
+
+ _active_scheduling_job.timeslots[_active_schedule_selected_job_index].assignee =
+ employee_selector_render(window, scale, !is_viewing_existing_timeslot, selected_employee,x,y,w,h, employee_selector_animation, &_active_scheduling_job);
+ animation_update(&employee_selector_animation);
+ }
+}
+
+static bool scheduling_job_is_correct()
+{
+ for (s32 i = 0; i < _active_scheduling_job.offer.shipday_count; i++) {
+ scheduled_job_time scheduled_time = _active_scheduling_job.timeslots[i];
+ if (!scheduled_time.assignee) { return false; }
+ if (scheduled_time.timeslot == -1) { return false; }
+ }
+ return true;
+}
+
+static void draw_duration_of_scheduled_job_entry(scheduled_job_time scheduled_time, s32 hour_w, s32 tx, s32 ty, s32 timeslot_w, s32 x, s32 y, s32 timeslot_h, color tile_color)
+{
+ #define TILE_X(_x) (x+hour_w+(timeslot_w*_x)+1)
+ #define TILE_Y(_y) (y+(timeslot_h*(CDAYTORDAY(_y)))+1)
+
+ s32 duration_of_job_measured_in_timeslots_max = ceil((_active_scheduling_job.offer.duration_sec_max/60.0f) / (60.0f/TIME_SLOTS_PER_HOUR));
+ if (!scheduled_time.stay_at_destination) duration_of_job_measured_in_timeslots_max*=2;
+ log_assert(duration_of_job_measured_in_timeslots_max < 36*TIME_SLOTS_PER_HOUR, "Multi-day trip not supported yet in schedule preview.");
+
+ s32 slots_outside_of_schedule = (24-(WORK_HOUR_END-WORK_HOUR_START))*TIME_SLOTS_PER_HOUR;
+ s32 overflow_to_next_day = -(((TIME_SLOTS_PER_DAY-scheduled_time.timeslot) -
+ (duration_of_job_measured_in_timeslots_max)) +
+ slots_outside_of_schedule);
+ color tile_color_bg = tile_color;
+ tile_color_bg.a = 50;
+
+ s32 highlight_w = timeslot_w*duration_of_job_measured_in_timeslots_max;
+ if (tx+highlight_w > x+(timeslot_w*(TIME_SLOTS_PER_DAY+TIME_SLOTS_PER_HOUR)))
+ highlight_w -= (tx+highlight_w) - (x+(timeslot_w*(TIME_SLOTS_PER_DAY+TIME_SLOTS_PER_HOUR)));
+ renderer->render_rectangle(tx, ty, highlight_w, timeslot_h, tile_color_bg);
+ s32 highlight_y = ty+timeslot_h;
+ if (scheduled_time.day == SUNDAY) highlight_y = TILE_Y(MONDAY);
+ if (overflow_to_next_day > 0) renderer->render_rectangle(TILE_X(0), highlight_y, timeslot_w*overflow_to_next_day, timeslot_h, tile_color_bg);
+}
+
+static void place_detail_draw_schedule(platform_window* window)
+{
+ float offset = scale * 40.0;
+ s32 item_h = 82 * 0.4 * scale;
+ s32 h = (area.h * 0.6 - (offset*2)) + item_h;
+ s32 pad_between_items = 10 * scale;
+ s32 employee_select_h = (h/4);
+ h -= employee_select_h + pad_between_items;
+ s32 w = (area.w * 0.9 - (offset*2));
+ s32 x = offset + area.x + (area.w*0.05);
+ s32 pos_y = area.y + (area.w*0.12);
+ s32 y = pos_y;
+ s32 btn_pad = pad_between_items/2;
+
+ s32 button_w = 124 * scale;
+ s32 button_h = (employee_select_h-btn_pad)/2;
+ s32 schedule_pad = 20*scale;
+
+ #define COLS (TIME_SLOTS_PER_DAY+TIME_SLOTS_PER_HOUR)
+ #define ROWS 8
+ s32 timeslot_w = (w-schedule_pad*2)/COLS;
+ s32 timeslot_h = (h-schedule_pad*2)/ROWS;
+ s32 hour_w = timeslot_w*4;
+ s32 new_w = (timeslot_w)*COLS+schedule_pad*2;
+ s32 new_h = (timeslot_h)*ROWS+schedule_pad*2;
+ s32 off_x = w - new_w;
+ s32 off_y = h - new_h;
+ w = new_w;
+ h = new_h;
+ x += off_x/2;
+ y += off_y/2;
+
+ s32 employee_select_y = y + h + pad_between_items;
+ s32 btn_y = y+h+pad_between_items;
+ if (_active_schedule_state == SCHEDULING_JOB || _active_schedule_state == RESCHEDULING_JOB)
+ {
+ button_type type = scheduling_job_is_correct() ? BUTTON_ENABLED : BUTTON_DISABLED;
+ if (button_render(scale, type, "Accept", x-button_w+w, btn_y, button_w, button_h)) {
+ audio_play_sound(snd_click2, AUDIO_CHANNEL_SFX_1);
+ if (_active_schedule_state == SCHEDULING_JOB)
+ array_push(&_active_location->schedule.jobs, &_active_scheduling_job);
+ else if (_active_schedule_state == RESCHEDULING_JOB)
+ *_active_selected_scheduled_job = _active_scheduling_job;
+
+ job_offer* offer = get_job_offer_by_id(_active_location, _active_scheduling_job.offer.id);
+ if (offer) array_remove_by(&_active_location->job_offers, offer);
+
+ _goto_default_detail_state();
+ }
+ btn_y += btn_pad + button_h;
+ if (button_render(scale, BUTTON_ENABLED, "Cancel", x-button_w+w, btn_y, button_w, button_h)) {
+ _goto_default_detail_state();
+ }
+ }
+ else if (_active_schedule_state == VIEWING && _active_selected_scheduled_job) {
+ if (button_render(scale, BUTTON_ENABLED, "Reschedule", x-button_w+w, btn_y, button_w, button_h)) {
+ _active_schedule_state = RESCHEDULING_JOB;
+ _active_scheduling_job = *_active_selected_scheduled_job;
+ }
+ }
+
+ s32 options_w = 230*scale;
+ s32 selector_w = w-options_w-(pad_between_items*2)-button_w;
+
+ button_render(scale, BUTTON_STATIC, 0, x,y,w,h);
+
+ x+=schedule_pad;
+ y+=schedule_pad;
+ w-=schedule_pad*2;
+ h-=schedule_pad*2;
+
+ #define TILE_X(_x) (x+hour_w+(timeslot_w*_x)+1)
+ #define TILE_Y(_y) (y+(timeslot_h*(CDAYTORDAY(_y)))+1)
+
+
+ vec2f hovered_tile = (vec2f){(s32)((_global_mouse.x-x)/timeslot_w)-TIME_SLOTS_PER_HOUR, (s32)((_global_mouse.y-y)/timeslot_h)};
+ s32 hovered_day = RDAYTOCDAY((s32)hovered_tile.y);
+ scheduled_job* existing_job_at_hovered_tile = get_scheduled_job_at_time(hovered_day, (s32)hovered_tile.x);
+ font* fnt = fnt_rd24;
+
+ static bool dragging_tile = false;
+
+ // newly scheduling job
+ if (_active_schedule_state == SCHEDULING_JOB || _active_schedule_state == RESCHEDULING_JOB)
+ {
+ for (s32 i = 0; i < _active_scheduling_job.offer.shipday_count; i++) {
+ scheduled_job_time scheduled_time = _active_scheduling_job.timeslots[i];
+ s32 dday = CDAYTORDAY(scheduled_time.day);
+ renderer->render_rectangle(x+hour_w, y+(dday*timeslot_h), w-hour_w, timeslot_h, COLOR_SCHEDULE_ROW_ACTIVE);
+
+ bool being_hovered = false;
+ bool valid_tile = (!existing_job_at_hovered_tile || (_active_schedule_state == RESCHEDULING_JOB && existing_job_at_hovered_tile == _active_selected_scheduled_job));
+ if (valid_tile && hovered_tile.x >= 0 && hovered_tile.x < TIME_SLOTS_PER_DAY && hovered_tile.y == dday) { // If in current row.
+
+ // Interaction.
+ if (_active_scheduling_job.timeslots[i].timeslot == hovered_tile.x) {
+ platform_set_cursor(window, CURSOR_POINTER);
+ being_hovered = true;
+
+ // Check if player started to drag.
+ if (is_left_clicked_peak()) { // is_left_clicked_peak instead of is_left_clicked so employee selector can reset input.
+ dragging_tile = true;
+ _active_schedule_selected_job_index = i;
+ audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);
+ }
+ }
+ }
+
+ s32 tx = TILE_X(scheduled_time.timeslot);
+ s32 ty = TILE_Y(scheduled_time.day);
+
+ bool is_dragging_current_tile = (dragging_tile && _active_schedule_selected_job_index == i);
+ color tile_color = (_active_schedule_selected_job_index == i) ? COLOR_SCHEDULE_TILE_HOVERED : COLOR_SCHEDULE_TILE_SELECTED;
+
+ // Handle dragging.
+ if (being_hovered || is_dragging_current_tile) {
+
+ // Get index of new timeslot.
+ s32 index_to_set = hovered_tile.x;
+ s32 possible_tile_left = find_empty_timeslot_for_day(scheduled_time.day);
+ s32 possible_tile_right = find_empty_timeslot_for_day_right_to_left(scheduled_time.day);
+ if (index_to_set < 0) index_to_set = possible_tile_left;
+ if (index_to_set >= TIME_SLOTS_PER_DAY) index_to_set = possible_tile_right;
+
+ // Set new timeslot.
+ if (is_dragging_current_tile) {
+ platform_set_cursor(window, CURSOR_DRAG);
+
+ scheduled_job* existing_job_at_tile = get_scheduled_job_at_time(scheduled_time.day, index_to_set);
+ if (existing_job_at_tile == _active_selected_scheduled_job) existing_job_at_tile = 0; // Make sure reschedule can be set at current location.
+ if (index_to_set != -1 && !existing_job_at_tile) _active_scheduling_job.timeslots[i].timeslot = index_to_set;
+
+ if (!is_left_down()) {
+ dragging_tile = false;
+ }
+
+ tx = TILE_X(_active_scheduling_job.timeslots[i].timeslot);
+ ty = TILE_Y(scheduled_time.day);
+ }
+
+ // Draw duration of job.
+ draw_duration_of_scheduled_job_entry(scheduled_time, hour_w, tx, ty, timeslot_w, x, y, timeslot_h, tile_color);
+
+ // Draw left and right arrow.
+ s32 icon_s = timeslot_w*0.7f;
+ s32 icon_y = ty + (timeslot_h-icon_s)/2;
+ s32 icon_pad = (3*scale);
+ if (possible_tile_left != -1 && possible_tile_left != index_to_set) {
+ renderer->render_image_tint(img_arrow_left_rounded, tx - icon_s - icon_pad-1, icon_y, icon_s+2, icon_s,COLOR_SCHEDULE_BORDER_THIN);
+ renderer->render_image_tint(img_arrow_left_rounded, tx - icon_s - icon_pad, icon_y, icon_s, icon_s,tile_color);
+ }
+ if (possible_tile_right != -1 && possible_tile_right != index_to_set) {
+ gl_render_set_rotation(M_PI);
+ renderer->render_image_tint(img_arrow_left_rounded, tx + timeslot_w + icon_pad-1, icon_y, icon_s+2, icon_s,COLOR_SCHEDULE_BORDER_THIN);
+ renderer->render_image_tint(img_arrow_left_rounded, tx + timeslot_w + icon_pad, icon_y, icon_s, icon_s,tile_color);
+ gl_render_set_rotation(0.0f);
+ }
+ }
+
+ // Draw tile.
+ renderer->render_rectangle(tx, ty, timeslot_w, timeslot_h, tile_color);
+
+ // Status icon if tile being scheduled.
+ s32 icon_size = timeslot_w*0.6f;
+ if (scheduled_time.assignee == 0) {
+ renderer->render_image(img_questionmark, tx+(timeslot_w/2)-(icon_size/2),ty+(timeslot_h/5),icon_size,icon_size);
+ }
+ else {
+ renderer->render_image(img_checkmark, tx+(timeslot_w/2)-(icon_size/2),ty+(timeslot_h/5),icon_size,icon_size);
+ }
+ }
+ }
+
+ // existing jobs
+ {
+ for (s32 i = 0; i < _active_location->schedule.jobs.length; i++) {
+ scheduled_job* job = array_at(&_active_location->schedule.jobs, i);
+
+ // if rescheduling, dont show the timeslots for the job being rescheduled.
+ if (_active_schedule_state == RESCHEDULING_JOB && _active_selected_scheduled_job == job) {
+ continue;
+ }
+
+ for (s32 t = 0; t < job->offer.shipday_count; t++) {
+ scheduled_job_time scheduled_time = job->timeslots[t];
+ s32 tx = TILE_X(scheduled_time.timeslot);
+ s32 ty = TILE_Y(scheduled_time.day);
+ bool is_selected_job = (_active_selected_scheduled_job == job);
+ bool is_selected = (is_selected_job && _active_schedule_selected_job_index == t);
+ bool is_inspecting_employee = false;
+
+ if (_active_selected_scheduled_job != 0 && scheduled_time.assignee == _active_selected_scheduled_job->timeslots[_active_schedule_selected_job_index].assignee && !is_selected) {
+ is_inspecting_employee = true;
+ }
+
+ color tc = COLOR_SCHEDULE_TILE_FIXED;
+ if (is_inspecting_employee) {
+ tc = COLOR_SCHEDULE_TILE_HIGHLIGHTED;
+ }
+ else if (is_selected) {
+ tc = COLOR_SCHEDULE_TILE_HOVERED;
+ }
+ if (scheduled_time.assignee == 0) tc = is_selected ? COLOR_SCHEDULE_TILE_HOVERED : COLOR_SCHEDULE_TILE_INVALID;
+ if (is_inspecting_employee || is_selected) {
+ // Draw duration of job.
+ draw_duration_of_scheduled_job_entry(scheduled_time, hour_w, tx, ty, timeslot_w, x, y, timeslot_h, tc);
+ }
+
+ renderer->render_rectangle(tx, ty, timeslot_w, timeslot_h, tc);
+ if (is_selected_job && !is_selected) {
+ s32 dotsize = timeslot_w/3;
+ s32 dotoffsetx = (timeslot_w/2)-(dotsize/2);
+ s32 dotoffsety = timeslot_h-(timeslot_h/4)-(dotsize/2);
+ renderer->render_image_tint(img_dot, tx+dotoffsetx,ty+dotoffsety,dotsize,dotsize,COLOR_SCHEDULE_TILE_HOVERED);
+ }
+
+ // Status icon
+ s32 icon_size = timeslot_w*0.6f;
+ if (scheduled_time.assignee == 0) {
+ renderer->render_image(img_questionmark, tx+(timeslot_w/2)-(icon_size/2),ty+(timeslot_h/5),icon_size,icon_size);
+ }
+
+ #if 0
+ color col = COLOR_SCHEDULE_TILE_FIXED;
+ if (is_selected_job) col = COLOR_SCHEDULE_TILE_HIGHLIGHTED;
+ if (is_selected) col = COLOR_SCHEDULE_TILE_HOVERED;
+ renderer->render_rectangle(tx, ty, timeslot_w, timeslot_h, col);
+ #endif
+
+ #if 0
+ // Viewing job info box.
+ if (is_selected) {
+ s32 tooltip_offset_from_tile = 15*scale;
+ s32 tooltip_pad = 15*scale;
+ s32 tooltip_h = fnt_s->px_h + (tooltip_pad*2);
+ s32 tooltip_x = tx + timeslot_w + tooltip_offset_from_tile;
+ s32 tooltip_y = ty + (timeslot_h/2)-(tooltip_h/2);
+ // if (tx > area.x + (area.w/2))
+
+ char* tooltip_text = "Maastricht -> Machester";
+ s32 tooltip_text_w = renderer->calculate_text_width(fnt_s, tooltip_text);
+ s32 tooltip_w = tooltip_text_w + (tooltip_pad*2);
+
+ renderer->set_render_depth(5);
+ button_draw_background(scale, tooltip_x,tooltip_y,tooltip_w,tooltip_h, COLOR_WHITE, COLOR_BUTTON);
+ renderer->render_text(fnt_s, tooltip_x+tooltip_pad, tooltip_y+tooltip_pad, tooltip_text, COLOR_TEXT);
+ renderer->set_render_depth(4);
+ }
+ #endif
+
+ if (_active_schedule_state == VIEWING && mouse_interacts(tx,ty,timeslot_w,timeslot_h)) {
+ platform_set_cursor(window, CURSOR_POINTER);
+
+ if (is_left_clicked()) {
+ scheduled_job* prev_selected_scheduled_job = _active_selected_scheduled_job;
+ _active_schedule_state = VIEWING;
+ _active_selected_scheduled_job = job;
+ _active_schedule_selected_job_index = t;
+ audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);
+
+ if (_active_selected_scheduled_job != prev_selected_scheduled_job) {
+ tag_animation = animation_create(TAG_ANIMATION_DURATION);
+ tag_animation.started = true;
+ }
+
+ employee_selector_animation = animation_create(EMPLOYEE_SELECTOR_ANIMATION_DURATION);
+ employee_selector_animation.started = true;
+ }
+ }
+ }
+ }
+ }
+
+ // rows
+ for (s32 ty = 0; ty < ROWS; ty++)
+ {
+ s32 row_y = y + (ty*timeslot_h);
+
+ if (ty==0) {
+ // COLOR_SCHEDULE_ROW_ACTIVE
+ renderer->render_rectangle(x, y, timeslot_w*4, h, COLOR_SCHEDULE_BG);
+ renderer->render_rectangle(x, y, w, timeslot_h, COLOR_SCHEDULE_BG);
+ }
+ else {
+ // Days
+ {
+ char buf[20];
+ switch(ty) {
+ case 1: strcpy(buf, "Mon"); break;
+ case 2: strcpy(buf, "Tue"); break;
+ case 3: strcpy(buf, "Wed"); break;
+ case 4: strcpy(buf, "Thu"); break;
+ case 5: strcpy(buf, "Fri"); break;
+ case 6: strcpy(buf, "Sat"); break;
+ case 7: strcpy(buf, "Sun"); break;
+ }
+
+ s32 tw = renderer->calculate_text_width(fnt, buf);
+ s32 textx = x + (hour_w/2)-(tw/2);
+ s32 texty = row_y + (timeslot_h/2)-(fnt->px_h/2);
+ renderer->render_text(fnt, textx, texty, buf, COLOR_TEXT);
+ }
+ }
+ renderer->render_rectangle(x, row_y, w, 1, COLOR_SCHEDULE_BORDER);
+ }
+
+ // cols
+ for (s32 tx = 0; tx < COLS; tx++)
+ {
+ s32 row_x = x + (tx*timeslot_w);
+ bool is_big_line = tx % 4 == 0;
+ if (tx>=4 || is_big_line) {
+ s32 liney = y;
+ s32 lineh = h;
+ if (!is_big_line) {
+ liney = y+timeslot_h;
+ lineh = h-timeslot_h;
+ }
+ renderer->render_rectangle(row_x, liney, 1, lineh, (is_big_line) ? COLOR_SCHEDULE_BORDER : COLOR_SCHEDULE_BORDER_THIN);
+ }
+ if (tx!=0 && is_big_line) {
+
+ char buf[20];
+ sprintf(buf, "%d:00", WORK_HOUR_START+((tx-1)/TIME_SLOTS_PER_HOUR));
+ s32 tw = renderer->calculate_text_width(fnt, buf);
+ s32 textx = row_x + (hour_w/2)-(tw/2);
+ s32 texty = y + (timeslot_h/2)-(fnt->px_h/2);
+ renderer->render_text(fnt, textx, texty, buf, COLOR_TEXT);
+ }
+ }
+
+ // outline
+ renderer->render_rectangle_outline(x,y,w+1,h+1,1,COLOR_SCHEDULE_BORDER);
+
+ x -= schedule_pad;
+ place_detail_draw_schedule_employee_options(window, schedule_pad, scale, x + selector_w + pad_between_items, employee_select_y, options_w, employee_select_h);
+ place_detail_draw_schedule_employee_selector(window, schedule_pad, scale, x, employee_select_y, selector_w, employee_select_h);
+
+ // Deselect selected job.
+ if (mouse_interacts(x+schedule_pad,y,w+1,h+1) && is_left_clicked()) {
+ if (_active_schedule_state == VIEWING) {
+ _active_selected_scheduled_job = 0;
+ }
+ }
+}
+
+static void place_detail_draw_dealers(platform_window* window)
+{
+ float offset = scale * 40.0;
+ s32 item_h = 82 * 0.4 * scale;
+ s32 h = (area.h * 0.6 - (offset*2)) + item_h;
+ s32 w = (area.w * 0.9 - (offset*2));
+ s32 x = offset + area.x + (area.w*0.05);
+ s32 pos_y = area.y + (area.w*0.12);
+ s32 y = pos_y;
+
+ s32 item_count = _active_world->truck_dealers.length;
+
+ s32 spacing = 6;
+ s32 logo_space_w = w / 7.0f;
+ s32 logo_space_h = (h-(spacing*(item_count-1))) / 3.0f;
+
+ for (s32 i = 0; i < item_count; i++)
+ {
+ truck_dealer* d = array_at(&_active_world->truck_dealers, i);
+ if(button_render(scale, true, 0, x, y + (logo_space_h*i)+spacing*i, logo_space_w, logo_space_h)) active_dealer_index = i;
+ render_logo_at(d->logo, 40 * scale, x, y + (logo_space_h*i)+spacing*i, logo_space_w, logo_space_h);
+ }
+
+ if (active_dealer_index < _active_world->truck_dealers.length) {
+ truck_dealer* d = array_at(&_active_world->truck_dealers, active_dealer_index);
+
+ s32 panel_x = x + spacing + logo_space_w;
+ s32 panel_w = (x + w) - panel_x;
+ place_detail_draw_active_dealer(window, d, scale, panel_x, y, panel_w, h);
+ }
+}
+
+static void place_detail_draw_resumes(platform_window* window)
+{
+ s32 screen_center_x = area.x + (area.w/2);
+ s32 screen_center_y = area.y + (area.h/2);
+ s32 panel_w = area.w*0.75;
+ s32 panel_h = area.h*0.75;
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = screen_center_y - (panel_h/2);
+
+ bool resume_available = _active_location->resumes.length;
+
+ font* fntb = fnt_rd32;
+ font* fnt = fnt_rd16;
+
+ if (!resume_available) {
+ char* message = "No resumes available.";
+ s32 text_w = renderer->calculate_text_width(fnt, message);
+ s32 text_x = screen_center_x - (text_w/2);
+ s32 text_y = screen_center_y - (fnt->px_h/2);
+ renderer->render_text(fnt, text_x+1, text_y+1, message, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, message, COLOR_TEXT);
+ }
+ else {
+ s32 panel_w = area.w*0.2;
+ s32 panel_h = area.h*0.3;
+ panel_x = screen_center_x - (panel_w/2);
+ panel_y = screen_center_y - (panel_h/2);
+ resume* resume = array_at(&_active_location->resumes, 0);
+
+ float opacity = 1.0f - resume->animation.percentage;
+ if (opacity < 0.0f) opacity = 0.0f;
+
+ color color_bg = COLOR_WHITE;
+ color_bg.a = opacity*255;
+
+ renderer->render_image_tint(img_resume, panel_x, panel_y, panel_w, panel_h, color_bg);
+
+ char buffer[100];
+ sprintf(buffer, "Resume");
+ s32 text_x = panel_x + (panel_w/2) - (renderer->calculate_text_width(fntb, buffer)/2);
+ s32 text_y = panel_y + (20*scale);
+
+ color t_shadow = COLOR_TEXT_SHADOW;
+ t_shadow.a = opacity*255;
+
+ renderer->render_text(fntb, text_x, text_y, buffer, t_shadow);
+
+ text_x = panel_x + (20*scale);
+ text_y += fntb->px_h+(15*scale);
+
+ sprintf(buffer, "Name: %s", resume->employee->name);
+ renderer->render_text(fnt, text_x, text_y, buffer, t_shadow);
+
+ text_y += fnt->px_h + 8*scale;
+ sprintf(buffer, "Age: %d", resume->employee->age);
+ renderer->render_text(fnt, text_x, text_y, buffer, t_shadow);
+
+ text_y += fnt->px_h + 8*scale;
+ sprintf(buffer, "Experience: %d years", resume->employee->experience);
+ renderer->render_text(fnt, text_x, text_y, buffer, t_shadow);
+
+ text_y += fnt->px_h + 8*scale;
+ sprintf(buffer, "Salary: $%.2f/month", resume->employee->salary);
+ renderer->render_text(fnt, text_x, text_y, buffer, t_shadow);
+
+ struct tm* curr_time = gmtime(&resume->expire_date);
+ text_y += fnt->px_h + 8*scale;
+ strftime(buffer, 50, "Offer open untill %d/%m/%Y", curr_time);
+ renderer->render_text(fnt, text_x, text_y, buffer, t_shadow);
+ text_y += fnt->px_h + 8*scale;
+
+ s32 signature_h = ((panel_y+panel_h)-text_y)*0.6;
+ text_y += (10 * scale);
+ renderer->render_image(img_signature, text_x, text_y, signature_h*2, signature_h);
+
+ if (!resume->animation.started) {
+ s32 button_w = 124 * scale;
+ s32 button_h = 37 * scale;
+ s32 pad = 20 * scale;
+ s32 btn_y = panel_y + panel_h + pad;
+ s32 btn_x = panel_x;
+ if (button_render(scale, true, "Hire", btn_x, btn_y, button_w, button_h)) {
+ audio_play_sound(snd_click2, AUDIO_CHANNEL_SFX_1);
+ resume->animation.started = true;
+ resume->hired = true;
+ resume->employee->id = _active_world->next_id++;
+ resume->employee->hire_date = _active_world->current_time;
+ add_employee_to_world_location(_active_location, resume->employee);
+ }
+ btn_x = panel_x + panel_w - button_w;
+ if (button_render(scale, true, "Decline", btn_x, btn_y, button_w, button_h)) {
+ audio_play_sound(snd_click3, AUDIO_CHANNEL_SFX_1);
+ resume->animation.started = true;
+ resume->hired = false;
+ }
+ }
+ else {
+ s32 pad = scale * 50;
+ s32 stamp_x = panel_x + pad;
+ s32 stamp_y = panel_y + pad;
+ s32 stamp_w = panel_w - (pad*2);
+ s32 stamp_h = panel_h - (pad*2);
+ if (resume->hired) {
+ renderer->render_image_tint(img_hired, stamp_x, stamp_y, stamp_w, stamp_h, color_bg);
+ }
+ else {
+ renderer->render_image_tint(img_denied, stamp_x, stamp_y, stamp_w, stamp_h, color_bg);
+ }
+
+ animation_update(&resume->animation);
+ if (resume->animation.percentage == 1.0f) {
+ array_remove_at(&_active_location->resumes, 0);
+ }
+ }
+ }
+}
+
+static void place_detail_draw_info(platform_window* window, tab tab)
+{
+ float pad = tab.scale * 10;
+ float x = tab.x+pad;
+ float y = tab.y+pad;
+ float w = tab.w-(pad*2);
+ float h = tab.h-(pad*2);
+
+ renderer->render_set_scissor(window, x, y, w, h);
+ if (selected_tab_index == PLACE_DETAIL_EMPLOYEES) {
+ place_detail_draw_employees(window, tab, x, y, w, h);
+ }
+ else if (selected_tab_index == PLACE_DETAIL_JOBOFFERS) {
+ place_detail_draw_job_offers(window, tab, x, y, w, h);
+ }
+ else if (selected_tab_index == PLACE_DETAIL_GARAGE) {
+ place_detail_draw_trucks(window, tab, x, y, w, h);
+ }
+
+ renderer->render_reset_scissor(window);
+}
+
+static tab place_detail_draw_tab_bg(platform_window* window)
+{
+ float offset = scale * 40.0;
+ s32 cornor_size = img_button_topleft->width*(scale/2);
+ s32 item_h = 82 * 0.4 * scale;
+ s32 h = (area.h * 0.6 - (offset*2));
+ s32 w = (area.w * 0.9 - (offset*2));
+ s32 top_width = w - (cornor_size*2);
+ s32 size_height = h - (cornor_size*2);
+ s32 x = offset + area.x + (area.w*0.05);
+ s32 pos_y = area.y + (area.w*0.12);
+ s32 y = pos_y + item_h;
+
+ color fill = COLOR_BUTTON_ACTIVE_TINT;
+
+ // button_render(scale, BUTTON_STATIC, 0, panel_x + pad_x, vertical_pad + panel_y + pad_y*2 + button_h*1, button_w, button_h);
+
+ // left
+ renderer->render_image_tint(img_button_left, x, y-1, cornor_size, size_height+2, fill);
+
+ // right
+ renderer->render_image_tint(img_button_right, x + cornor_size + top_width, y-1, cornor_size, size_height+2, fill);
+
+ // bottom
+ renderer->render_image_tint(img_button_bottomleft, x, y + size_height, cornor_size, cornor_size, fill);
+ renderer->render_image_tint(img_button_bottom, x + cornor_size, y + size_height, top_width, cornor_size, fill);
+ renderer->render_image_tint(img_button_bottomright, x + cornor_size + top_width, y + size_height, cornor_size, cornor_size, fill);
+
+ // fill
+ s32 pad = cornor_size-1;
+ fill = COLOR_BUTTON_ACTIVE;
+ renderer->render_rectangle(x+pad, y-1, w-(pad*2), h-(pad*2), fill);
+
+ return (tab){x,y,w,h, scale};
+}
+
+static void place_detail_push_tab(place_detail_info_state index, platform_window* window, char* text)
+{
+ float offset = scale * 40.0;
+ s32 pos_y = area.y + (area.w*0.12);
+ s32 item_w = 426 * 0.4 * scale;
+ s32 item_h = 82 * 0.4 * scale;
+ s32 pos_x = offset + area.x + (area.w*0.05) + (item_w*index);
+ bool hovered = mouse_interacts(pos_x,pos_y,item_w,item_h);
+
+ color tint = COLOR_WHITE;
+ if (selected_tab_index == index) {
+ tint = COLOR_BUTTON_ACTIVE_TINT;
+ }
+ if (hovered) {
+ platform_set_cursor(window, CURSOR_POINTER);
+ if (is_left_clicked()) {
+ if (selected_tab_index != index) audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);
+ selected_tab_index = index;
+ }
+ }
+
+ renderer->render_image_tint(img_tabitem, pos_x, pos_y, item_w, item_h, tint);
+
+ {
+ font* fnt = fnt_rd24;
+ s32 text_x = pos_x + (item_w/2) - (renderer->calculate_text_width(fnt, text)/2);
+ s32 text_y = pos_y + (item_h/2) - (fnt->px_h/2);
+
+ renderer->render_text(fnt, text_x+1, text_y+1, text, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, text, COLOR_TEXT);
+ }
+}
+
+static tab place_detail_draw_tabs(platform_window* window)
+{
+ place_detail_push_tab(PLACE_DETAIL_EMPLOYEES, window, "Employees");
+ place_detail_push_tab(PLACE_DETAIL_JOBOFFERS, window, "Job Offers");
+ place_detail_push_tab(PLACE_DETAIL_SCHEDULE, window, "Schedule");
+ place_detail_push_tab(PLACE_DETAIL_GARAGE, window, "Garage");
+ return place_detail_draw_tab_bg(window);
+}
+
+static void place_detail_draw_title(platform_window* window)
+{
+ char buf[200];
+ if (current_detail_state == PLACE_DETAIL_SHOW_MAIN) {
+ strcpy(buf, _active_location->name);
+ }
+ else if (current_detail_state == PLACE_DETAIL_SHOW_RESUMES) {
+ strcpy(buf, "Hire new employees");
+ }
+ else if (current_detail_state == PLACE_DETAIL_SHOW_DEALERS) {
+ strcpy(buf, "Dealers");
+ }
+ else if (current_detail_state == PLACE_DETAIL_SHOW_EMPLOYEE) {
+ sprintf(buf, "%s, #%d", _active_employee->name, _active_employee->id);
+ }
+ else if (current_detail_state == PLACE_DETAIL_SHOW_SCHEDULE) {
+ if (_active_schedule_state == VIEWING)
+ sprintf(buf, "Schedule for %s", _active_location->name);
+ else {
+ sprintf(buf, "%s wants you to ship %s to %s",
+ _active_scheduling_job.offer.company->name,
+ _active_scheduling_job.offer.product->name,
+ (*(world_location**)array_at(&_active_scheduling_job.offer.connections,
+ _active_scheduling_job.offer.connections.length-1))->name);
+ }
+ }
+
+ font* fnt = fnt_rd36;
+ float text_pad = scale * 40.0;
+ s32 text_x = text_pad + area.x + (area.w*0.05);
+ s32 text_y = text_pad + area.y + (area.w*0.05);
+
+ // Title
+ {
+ renderer->render_text(fnt, text_x+2, text_y+2, buf, COLOR_TEXT_SHADOW);
+ renderer->render_text(fnt, text_x, text_y, buf, COLOR_TEXT);
+ }
+
+ color tags_text_color = AN_LI_TINT(COLOR_TEXT, tag_animation);
+ color tag_icon_color = AN_LI_TINT(COLOR_WHITE, tag_animation);
+
+ // Tags
+ #define PUSH_TAG(_text, _icon){\
+ char* text = _text;\
+ s32 textw = renderer->calculate_text_width(fnt, text);\
+ text_x -= textw;\
+ renderer->render_text(fnt, text_x, text_y, text, tags_text_color);\
+ text_x -= icon_s + pad_between_icon_and_text;\
+ renderer->render_image_tint(_icon, text_x, text_y,icon_s,icon_s, tag_icon_color);\
+ text_x -= pad_between_tags;}
+
+ text_y += (fnt->px_h/2);
+ fnt = fnt_rd20;
+ text_y -= (fnt->px_h/2);
+ s32 icon_s = fnt->px_h;
+ s32 pad_between_icon_and_text = 5*scale;
+ s32 pad_between_tags = 20*scale;
+
+ #define ANIMATION_OFFSET (30*scale)
+ text_x = area.x + (area.w*0.05) + (area.w*0.9) - text_pad + (ANIMATION_OFFSET-(ANIMATION_OFFSET*tag_animation.percentage));
+ #undef ANIMATION_OFFSET
+
+ job_offer* job_to_inspect = 0;
+ if (current_detail_state == PLACE_DETAIL_SHOW_SCHEDULE && _active_schedule_state != VIEWING)
+ job_to_inspect = &_active_scheduling_job.offer;
+ if (current_detail_state == PLACE_DETAIL_SHOW_SCHEDULE && _active_schedule_state == VIEWING && _active_selected_scheduled_job != 0)
+ job_to_inspect = &_active_selected_scheduled_job->offer;
+
+ if (current_detail_state == PLACE_DETAIL_SHOW_SCHEDULE && job_to_inspect) {
+ // price
+ {
+ char pricebuf[25];
+ sprintf(pricebuf, "$%d/trip", job_to_inspect->reward);
+ PUSH_TAG(pricebuf, img_coins);
+ }
+
+ // distance
+ {
+ char pricebuf[25];
+ sprintf(pricebuf, "%.0fkm", job_to_inspect->total_distance);
+ PUSH_TAG(pricebuf, img_road);
+ }
+
+ // duration
+ {
+ char pricebuf[25];
+ sprintf(pricebuf, "%.0fh-%.0fh", job_to_inspect->duration_sec_min/3600.0f, job_to_inspect->duration_sec_max/3600.0f);
+ PUSH_TAG(pricebuf, img_timer);
+ }
+
+ if (_active_schedule_state == VIEWING) {
+ char namebuf[MAX_WORLD_LOCATION_NAME_LENGTH*3];
+ job_endpoints endpoints = job_offer_get_endpoints(job_to_inspect);
+ sprintf(namebuf, "%s to %s", endpoints.source->name, endpoints.dest->name);
+ PUSH_TAG(namebuf, img_globe);
+ }
+ }
+ else if (current_detail_state == PLACE_DETAIL_SHOW_EMPLOYEE) {
+ // Original location
+ {
+ char pricebuf[100];
+ sprintf(pricebuf, "From: %s", get_world_location_by_id(_active_world, _active_employee->original_location_id)->name);
+ PUSH_TAG(pricebuf, img_city);
+ }
+
+ // Current location or active trip destination
+ {
+ char pricebuf[100];
+ if (_active_employee->current_location_id != INVALID_ID) {
+ sprintf(pricebuf, "Currently at: %s", get_world_location_by_id(_active_world, _active_employee->current_location_id)->name);
+ }
+ else if (_active_employee->active_job_id != INVALID_ID) {
+ active_job* j = get_active_job_by_id(_active_world, _active_employee->active_job_id);
+ job_endpoints endpoints = job_offer_get_endpoints(&j->offer);
+ sprintf(pricebuf, "Driving to: %s", endpoints.dest->name);
+ }
+ PUSH_TAG(pricebuf, img_location_pin);
+ }
+ }
+
+ animation_update(&tag_animation);
+}
+
+static void place_detail_draw_panel(platform_window* window)
+{
+ s32 panel_w = area.w*0.9;
+ s32 panel_h = area.h*0.7;
+ s32 panel_x = area.x + area.w*0.05;
+ s32 panel_y = area.y + area.w*0.05;
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ // back button
+ {
+ s32 back_h = img_back->height * scale/2;
+ s32 back_w = img_back->width * scale/2;
+ s32 back_x = panel_x + (panel_w/10.0f);
+ s32 back_y = panel_y + panel_h - (back_h/2) - 1;
+
+ if (push_back_button(scale, back_x, back_y, back_w, back_h)) {
+ if (current_detail_state == PLACE_DETAIL_SHOW_MAIN) {
+ game_set_active_scene(GAME_STATE_WORLD_MAP);
+ }
+ else {
+ current_detail_state = PLACE_DETAIL_SHOW_MAIN;
+ }
+ _goto_default_detail_state();
+ }
+ }
+
+ if (mouse_interacts(panel_x,panel_y,panel_w,panel_h)) {
+ reset_left_click();
+ }
+ else if (is_left_clicked()) {
+ _goto_default_detail_state();
+ audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);
+ game_set_active_scene(GAME_STATE_WORLD_MAP);
+ }
+}
+
+void place_detail_scene_render(platform_window* window)
+{
+ renderer->set_render_depth(5);
+ world_map_draw_info_panel(window, false);
+
+ renderer->set_render_depth(4);
+ if (current_detail_state == PLACE_DETAIL_SHOW_MAIN) {
+ tab t = place_detail_draw_tabs(window);
+ place_detail_draw_info(window, t);
+
+ if (selected_tab_index == PLACE_DETAIL_SCHEDULE) {
+ selected_tab_index = PLACE_DETAIL_SHOW_MAIN;
+ current_detail_state = PLACE_DETAIL_SHOW_SCHEDULE;
+ }
+ }
+ if (current_detail_state == PLACE_DETAIL_SHOW_RESUMES) {
+ place_detail_draw_resumes(window);
+ }
+ if (current_detail_state == PLACE_DETAIL_SHOW_DEALERS) {
+ place_detail_draw_dealers(window);
+ }
+ if (current_detail_state == PLACE_DETAIL_SHOW_EMPLOYEE) {
+ place_detail_draw_selected_employee(window);
+ }
+ if (current_detail_state == PLACE_DETAIL_SHOW_SCHEDULE) {
+ place_detail_draw_schedule(window);
+ }
+
+ renderer->set_render_depth(3);
+ place_detail_draw_title(window);
+
+ renderer->set_render_depth(2);
+ place_detail_draw_panel(window);
+
+ renderer->set_render_depth(1);
+ menu_draw_background(window);
+}
+
+void place_detail_scene_update(platform_window* window)
+{
+ // world_update(window, _active_world, false);
+
+ if (keyboard_is_key_pressed(KEY_ESCAPE)) {
+ if (current_detail_state == PLACE_DETAIL_SHOW_MAIN) {
+ game_set_active_scene(GAME_STATE_WORLD_MAP);
+ }
+ else {
+ current_detail_state = PLACE_DETAIL_SHOW_MAIN;
+ }
+ _goto_default_detail_state();
+ }
+}
+
+void place_detail_scene_destroy()
+{
+
+} \ No newline at end of file
diff --git a/src/scenes/save_state_select.c b/src/scenes/save_state_select.c
new file mode 100644
index 0000000..1db5d11
--- /dev/null
+++ b/src/scenes/save_state_select.c
@@ -0,0 +1,105 @@
+
+void save_state_select_scene_init()
+{
+
+}
+
+static bool push_save_state_button(float scale, bool enabled, s32 x, s32 y, s32 size)
+{
+ if (enabled) {
+ button_render(scale, true, 0, x, y, size, size);
+ }
+ else {
+ button_render(scale, true, 0, x, y, size, size);
+ float close_size = size/3;
+ float close_x = x + (size/2)-(close_size/2);
+ float close_y = y + (size/2)-(close_size/2);
+ renderer->render_image(img_close, close_x, close_y, close_size, close_size);
+ }
+
+ return false;
+}
+
+static bool push_back_button(float scale, s32 back_x, s32 back_y, s32 back_w, s32 back_h)
+{
+ bool result = false;
+ color tint = COLOR_WHITE;
+ if (mouse_interacts(back_x,back_y,back_w,back_h)) {
+ tint = COLOR_BUTTON_ACTIVE_TINT;
+ platform_set_cursor(main_window, CURSOR_POINTER);
+ if (is_left_clicked()) {
+ result = true;
+ audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);
+ }
+ }
+
+ renderer->render_image_tint(img_back, back_x, back_y, back_w, back_h, tint);
+
+ font* font_sml = fnt_rd20;
+ char* back_text = "Back";
+ s32 back_text_width = renderer->calculate_text_width(font_sml, back_text);
+ s32 text_x = back_x + (back_w/2) - (back_text_width/2) + (back_w/12);
+ s32 text_y = back_y + (back_h/2) - (font_sml->px_h/2);
+
+ renderer->render_text(font_sml, text_x+2, text_y+2, back_text, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_sml, text_x, text_y, back_text, COLOR_TEXT);
+ return result;
+}
+
+static void save_state_draw_options(platform_window* window)
+{
+ s32 screen_center_x = area.x + (area.w/2);
+ s32 screen_center_y = area.y + (area.h/2);
+
+ float vertical_pad = 20 * scale;
+ float horizontal_pad = vertical_pad;
+ float spacing = 5 * scale;
+
+ s32 panel_h = 280 * scale;
+ s32 panel_item_size = (panel_h - (vertical_pad*2) - (spacing*1)) / 2;
+ s32 panel_w = (panel_item_size * 3) + (horizontal_pad*2) + (spacing*2);
+
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = screen_center_y - (panel_h/2);
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ // top row
+ for (s32 i = 0; i < 3; i++)
+ push_save_state_button(scale, 0, panel_x + horizontal_pad + (panel_item_size*i) +
+ (spacing*i), panel_y + horizontal_pad, panel_item_size);
+
+ // bottom row
+ for (s32 i = 0; i < 3; i++)
+ push_save_state_button(scale, 0, panel_x + horizontal_pad + (panel_item_size*i) + (spacing*i),
+ panel_y + horizontal_pad + panel_item_size+spacing, panel_item_size);
+
+ // back button
+ {
+ s32 back_h = img_back->height * scale/2;
+ s32 back_w = img_back->width * scale/2;
+ s32 back_x = panel_x + (panel_item_size/3);
+ s32 back_y = panel_y + panel_h - (back_h/2) - 1;
+
+ if (push_back_button(scale, back_x, back_y, back_w, back_h)) {
+ game_set_active_scene(GAME_STATE_MENU);
+ }
+ }
+}
+
+void save_state_select_scene_render(platform_window* window)
+{
+ menu_draw_background(window);
+ save_state_draw_options(window);
+}
+
+void save_state_select_scene_update(platform_window* window)
+{
+ if (keyboard_is_key_pressed(KEY_ESCAPE)) {
+ game_set_active_scene(GAME_STATE_MENU);
+ }
+}
+
+void save_state_select_scene_destroy()
+{
+
+} \ No newline at end of file
diff --git a/src/scenes/settings_scene.c b/src/scenes/settings_scene.c
new file mode 100644
index 0000000..23655d3
--- /dev/null
+++ b/src/scenes/settings_scene.c
@@ -0,0 +1,152 @@
+enum settings_state
+{
+ SETTINGS_AUDIO,
+ SETTINGS_DISPLAY,
+ SETTINGS_KEYBINDINGS,
+};
+
+enum settings_state settings_state = SETTINGS_AUDIO;
+
+void settings_scene_init() {
+
+}
+
+static void draw_display_settings(s32 x, s32 y, s32 w, s32 h)
+{
+ s32 option_spacing = 20*scale;
+ s32 checkbox_s = 30*scale;
+ s32 checkbox_offset = ((checkbox_s-fnt_rd24->px_h)/2);
+ s32 text_y = y + option_spacing;
+ s32 text_x = x + checkbox_s + option_spacing;
+
+ #define PUSH_DISPLAY_OPTION(_str, _opt)\
+ renderer->render_text(fnt_rd24, text_x, text_y, _str, COLOR_TEXT);\
+ if (button_draw_background(scale, x,text_y-checkbox_offset, checkbox_s, checkbox_s, COLOR_WHITE, COLOR_BUTTON)) {\
+ if (is_left_clicked()) { \
+ _opt = !_opt;\
+ if (&_opt == &option_vsync) platform_toggle_vsync(main_window, option_vsync);\
+ if (&_opt == &option_fullscreen) platform_toggle_fullscreen(main_window, option_fullscreen);\
+ audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);\
+ }\
+ }\
+ if (_opt) {\
+ s32 tw = renderer->calculate_text_width(fnt_rd24, "X");\
+ renderer->render_text(fnt_rd24, x + checkbox_s/2 - tw/2,text_y-checkbox_offset + checkbox_s/2 - fnt_rd24->px_h/2, "X", COLOR_TEXT);\
+ }\
+ text_y += (checkbox_s + option_spacing);
+
+ PUSH_DISPLAY_OPTION("Vsync", option_vsync);
+ PUSH_DISPLAY_OPTION("Fullscreen", option_fullscreen);
+}
+
+static void draw_audio_settings(s32 x, s32 y, s32 w, s32 h)
+{
+ s32 option_spacing = 20*scale;
+ s32 slider_h = 30*scale;
+ s32 slider_offset = ((slider_h-fnt_rd24->px_h)/2);
+ s32 slider_w = w/2;
+ s32 slider_x = x + slider_w;
+ s32 text_y = y + option_spacing;
+
+ // Music
+ #define PUSH_VOLUME_OPTION(_text, _opt)\
+ {\
+ static bool is_editing = false;\
+ renderer->render_text(fnt_rd24, x, text_y, _text, COLOR_TEXT);\
+ s32 slider_y = text_y - slider_offset;\
+ float percentage = is_editing ? ((_global_mouse.x - (slider_x+10*scale)) / (float)(slider_w-20*scale)) : _opt;\
+ if (percentage < 0.0f) percentage = 0.0f;\
+ if (percentage > 1.0f) percentage = 1.0f;\
+ button_draw_background_percentage(scale, slider_x,slider_y,slider_w,slider_h, COLOR_WHITE, COLOR_BUTTON, percentage, COLOR_WHITE);\
+ bool hovered = mouse_interacts(slider_x,slider_y,slider_w,slider_h);\
+ if (hovered && is_left_clicked()) is_editing = true;\
+ if (is_editing) {\
+ audio_set_mixer_volume(AUDIO_CHANNEL_SFX_1, volume_sfx*volume_global);\
+ audio_set_mixer_volume(AUDIO_CHANNEL_SFX_2, volume_sfx*volume_global);\
+ audio_set_music_volume(volume_music*volume_global);\
+ }\
+ if (is_editing) {\
+ if (!is_left_down()) { is_editing = false; audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1); }\
+ _opt = percentage;\
+ }\
+ text_y += (slider_h + option_spacing);\
+ }
+
+ PUSH_VOLUME_OPTION("Global", volume_global);
+ PUSH_VOLUME_OPTION("Music", volume_music);
+ PUSH_VOLUME_OPTION("Interface", volume_sfx);
+}
+
+static void settings_draw_options(platform_window* window)
+{
+ s32 screen_center_x = area.x + (area.w/2);
+ s32 screen_center_y = area.y + (area.h/2);
+
+ s32 panel_w = 400 * scale;
+ s32 panel_h = 500 * scale;
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = screen_center_y - (panel_h/2);
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ // Buttons
+ s32 button_pad = 10*scale;
+ s32 button_w = (panel_w-(button_pad*4))/3;
+ s32 button_h = 37*scale;
+ s32 button_y = panel_y + button_pad*1.3f;
+ s32 button_x = panel_x + button_pad;
+
+ if (button_render(scale, BUTTON_ENABLED, "Audio", button_x, button_y, button_w, button_h))
+ {
+ settings_state = SETTINGS_AUDIO;
+ }
+
+ if (button_render(scale, BUTTON_ENABLED, "Display", button_x + (button_w + button_pad), button_y, button_w, button_h))
+ {
+ settings_state = SETTINGS_DISPLAY;
+ }
+
+ if (button_render(scale, BUTTON_ENABLED, "Keybindings", button_x + (button_w + button_pad)*2, button_y, button_w, button_h))
+ {
+ settings_state = SETTINGS_KEYBINDINGS;
+ }
+
+ s32 detail_pad = 25*scale;
+ s32 detail_w = panel_w - (detail_pad*2)-(button_pad*2);
+ s32 detail_h = panel_h - (detail_pad*2)-(button_pad*3)-button_h;
+ s32 detail_x = panel_x+detail_pad+button_pad;
+ s32 detail_y = button_y + button_h + detail_pad+button_pad;
+
+ if (settings_state == SETTINGS_AUDIO) {
+ draw_audio_settings(detail_x, detail_y, detail_w, detail_h);
+ }
+ else if (settings_state == SETTINGS_DISPLAY) {
+ draw_display_settings(detail_x, detail_y, detail_w, detail_h);
+ }
+
+ // back button
+ {
+ s32 back_h = img_back->height * scale/2;
+ s32 back_w = img_back->width * scale/2;
+ s32 back_x = panel_x + (panel_w/10);
+ s32 back_y = panel_y + panel_h - (back_h/2) - 1;
+
+ if (push_back_button(scale, back_x, back_y, back_w, back_h)) {
+ game_set_active_scene(GAME_STATE_MENU);
+ }
+ }
+}
+
+void settings_scene_render(platform_window* window) {
+ menu_draw_background(window);
+ settings_draw_options(window);
+}
+
+void settings_scene_update(platform_window* window) {
+ if (keyboard_is_key_pressed(KEY_ESCAPE)) {
+ game_set_active_scene(GAME_STATE_MENU);
+ }
+}
+
+void settings_scene_destroy() {
+
+} \ No newline at end of file
diff --git a/src/scenes/world_map.c b/src/scenes/world_map.c
new file mode 100644
index 0000000..eb106cb
--- /dev/null
+++ b/src/scenes/world_map.c
@@ -0,0 +1,1094 @@
+typedef enum t_world_map_scene_state
+{
+ WORLD_SCENE_STATE_IDLE,
+ WORLD_SCENE_STATE_PURCHASE_LOCATION,
+ WORLD_SCENE_STATE_LOG,
+ WORLD_SCENE_STATE_INSIGHTS,
+ WORLD_SCENE_STATE_INVEST,
+} world_map_scene_state;
+
+typedef union t_world_map_scene_data
+{
+ world_location* location_to_purchase;
+} world_map_scene_data;
+
+s32 insights_selected_year_index = 0; // years since first year.
+world* _active_world = 0;
+world_map_scene_state scene_state = WORLD_SCENE_STATE_IDLE;
+world_map_scene_data scene_data = {0};
+active_job_ref currently_viewing_active_job = {0,0,0};
+
+animation log_button_flash_animation = {0,0,0,1};
+
+void place_detail_show_employee_detail(employee* emp);
+void place_detail_show_schedule_with_highlighted_job(world_location* loc, scheduled_job* job, scheduled_job_time job_time);
+
+void world_map_set_active_world(world* world)
+{
+ _active_world = world;
+}
+
+void world_map_scene_init()
+{
+
+}
+
+static bool push_info_panel_button(float scale, image* img, s32 x, s32 y, s32 size, bool enabled, bool flashing)
+{
+ button_type type = (enabled ? ((flashing && log_button_flash_animation.percentage >= 0.5f) ? BUTTON_HIGHLIGHTED : BUTTON_ENABLED) : BUTTON_DISABLED);
+ bool result = button_render(scale, type, 0, x, y, size, size);
+
+ float img_size = size / 2;
+ renderer->render_image(img, x+(img_size/2), y + (img_size/2), img_size, img_size);
+ return result;
+}
+
+static void world_map_draw_speed_panel(platform_window* window, bool enabled)
+{
+ s32 screen_center_x = area.x + (area.w/2);
+
+ s32 panel_h = 60 * scale;
+ s32 panel_w = 180 * scale;
+
+ float btn_size = 23 * scale;
+ float pad_top = 6 * scale;
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = area.y + area.h - panel_h - (100 * scale);
+ s32 text_y = panel_y + pad_top;
+ float side_btn_offset = pad_top;
+
+ // Background panel
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ // Button decrease simulation speed
+ if (push_info_panel_button(scale, img_arrow_left, screen_center_x - (panel_w/2) + side_btn_offset, text_y, btn_size, enabled, false)) {
+ _active_world->simulation_speed/=2;
+ }
+ // Button increase simulation speed
+ if (push_info_panel_button(scale, img_arrow_right, screen_center_x + (panel_w/2) - btn_size - side_btn_offset, text_y, btn_size, enabled, false)) {
+ _active_world->simulation_speed*=2;
+ if (_active_world->simulation_speed == 0) _active_world->simulation_speed = 1;
+ }
+
+ // Validate new simulation speed
+ if (_active_world->simulation_speed < MIN_SIMULATION_SPEED) _active_world->simulation_speed = MIN_SIMULATION_SPEED;
+ if (_active_world->simulation_speed > MAX_SIMULATION_SPEED) _active_world->simulation_speed = MAX_SIMULATION_SPEED;
+
+ // Draw the current speed, or pause icon when paused
+ if (_active_world->simulation_speed != 0) {
+ font* fnt = fnt_rd20;
+
+ char buf[10];
+ sprintf(buf, "%dx", _active_world->simulation_speed);
+
+ s32 tw = renderer->calculate_text_width(fnt, buf);
+ s32 textx = panel_x + (panel_w/2)-(tw/2);
+ renderer->render_text(fnt, textx, text_y+(5*scale), buf, COLOR_TEXT);
+ }
+ else {
+ s32 icon_pad = 4*scale;
+ s32 icon_s = btn_size-(icon_pad*2);
+ renderer->render_image(img_pause, panel_x+(panel_w/2)-(icon_s/2), text_y+icon_pad, icon_s, icon_s);
+ }
+
+ if (mouse_interacts(panel_x,panel_y,panel_w,panel_h)) {
+ reset_left_click();
+ }
+}
+
+static void world_map_draw_info_panel(platform_window* window, bool enabled)
+{
+ world_map_draw_speed_panel(window, enabled);
+
+ s32 screen_center_x = area.x + (area.w/2);
+
+ float vertical_pad = 20 * scale;
+
+ s32 panel_h = 120 * scale;
+ s32 panel_w = 280 * scale;
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = area.y + area.h - panel_h - (10 * scale);
+
+ // Draw background panel
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ if (enabled) {
+ char txt_buf[50];
+ char* text = txt_buf;
+ s32 text_y;
+ s32 text_x;
+ font* font_big = fnt_rd32;
+ s32 game_title_width;
+
+ // Draw current date and time
+ {
+ strftime(txt_buf, 50, "%H:%M %d/%m/%Y", &_active_world->current_time);
+ game_title_width = renderer->calculate_text_width(font_big, text);
+ text_y = panel_y + vertical_pad;
+ text_x = screen_center_x - (game_title_width/2);
+
+ renderer->render_text(font_big, text_x+1, text_y+1, text, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_big, text_x, text_y, text, COLOR_TEXT);
+ }
+
+ // Draw money
+ {
+ sprintf(txt_buf, "$%.0f", _active_world->money);
+ text = txt_buf;
+ font* font_medium = fnt_rd24;
+ game_title_width = renderer->calculate_text_width(font_medium, text);
+ text_y = text_y + font_big->px_h + (10 * scale);
+ text_x = screen_center_x - (game_title_width/2);
+
+ renderer->render_text(font_medium, text_x+1, text_y+1, text, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_medium, text_x, text_y, text, COLOR_TEXT);
+ }
+
+ float btn_size = 35 * scale;
+ float btn_spacing = 2 * scale;
+ float btn_y = panel_y + panel_h - btn_size - vertical_pad/2;
+ float total_btn_w = (btn_size+btn_spacing)*4-btn_spacing;
+
+ if (log_button_flash_animation.percentage == 1.0f) log_button_flash_animation = animation_create(1000);
+ log_button_flash_animation.started = true;
+ animation_update(&log_button_flash_animation);
+
+ // Graph button
+ if (push_info_panel_button(scale, img_graph, screen_center_x - (total_btn_w/2) + (btn_size+btn_spacing)*0, btn_y, btn_size, enabled, false)) {
+ scene_state = (scene_state == WORLD_SCENE_STATE_INSIGHTS) ? WORLD_SCENE_STATE_IDLE : WORLD_SCENE_STATE_INSIGHTS;
+ }
+
+ // Event log button
+ if (push_info_panel_button(scale, img_list, screen_center_x - (total_btn_w/2) + (btn_size+btn_spacing)*1, btn_y, btn_size, enabled, _active_world->log.has_unread_messages)) {
+ scene_state = (scene_state == WORLD_SCENE_STATE_LOG) ? WORLD_SCENE_STATE_IDLE : WORLD_SCENE_STATE_LOG;
+ }
+
+ // Bank button
+ push_info_panel_button(scale, img_bank, screen_center_x - (total_btn_w/2) + (btn_size+btn_spacing)*2, btn_y, btn_size, enabled, false);
+
+ // Event log button
+ if (push_info_panel_button(scale, img_list, screen_center_x - (total_btn_w/2) + (btn_size+btn_spacing)*3, btn_y, btn_size, enabled, _active_world->log.has_unread_messages)) {
+ scene_state = (scene_state == WORLD_SCENE_STATE_INVEST) ? WORLD_SCENE_STATE_IDLE : WORLD_SCENE_STATE_INVEST;
+ }
+ }
+ else {
+ s32 icon_s = panel_h/2;
+ renderer->render_image(img_pause, panel_x+(panel_w/2)-(icon_s/2), panel_y+(panel_h/2)-(icon_s/2), icon_s, icon_s);
+ }
+
+ if (mouse_interacts(panel_x,panel_y,panel_w,panel_h)) {
+ reset_left_click();
+ }
+}
+
+static void _insights_draw_grid(float scale, bool invalid_location, money_data_collection* collection, s32 grid_rows, s32 grid_cols, s32 textpad, s32 x, s32 y, s32 w, s32 h, s32 width_per_item, s32 height_per_item)
+{
+ font* fnt = fnt_rd16;
+ font* fnt_title = fnt_rd24;
+
+ for (s32 i = 0; i < MONTHS_IN_YEAR; i++) {
+ if (!collection) break;
+ money_data data = collection->months[i];
+ if (isnan(data.total_income)) continue;
+
+ char textbuf[50];
+ #define PUSH_VAL(_index, _val, _neg)\
+ sprintf(textbuf, "$%.0f", fabs(_val));\
+ renderer->render_text(fnt, x + ((i+3)*width_per_item) + (textpad/2), y + (_index*height_per_item)+(height_per_item/2)-(fnt->px_h/2), textbuf, (_neg) ? COLOR_TEXT_NEGATIVE : COLOR_TEXT);
+
+ PUSH_VAL(1, data.income_from_trips, false);
+ PUSH_VAL(2, data.expenses_from_utility, true);
+ PUSH_VAL(3, data.expenses_from_healthcare, true);
+ PUSH_VAL(4, data.expenses_from_repairs, true);
+ PUSH_VAL(5, data.expenses_from_fuel, true);
+ PUSH_VAL(6, data.expenses_from_employees, true);
+ PUSH_VAL(7, data.expenses_from_trucks, true);
+ // ADD NEW ENTRY HERE
+ PUSH_VAL(9, data.total_income, false);
+ PUSH_VAL(10, data.total_expenses, true);
+ PUSH_VAL(11, data.total_profit, data.total_profit < 0.0f);
+ }
+
+ renderer->render_rectangle(x, y, width_per_item*3, h, COLOR_SCHEDULE_BG);
+ renderer->render_rectangle(x, y, w, height_per_item, COLOR_SCHEDULE_BG);
+
+ for (s32 tx = 3; tx < grid_cols; tx++) {
+ s32 posx = x + tx * width_per_item;
+ renderer->render_rectangle(posx,y,1,h, COLOR_SCHEDULE_BORDER_THIN);
+
+ char buf[50];
+ buf[0] = 0;
+
+ switch(tx-2) {
+ case 1: strcpy(buf, "Jan"); break;
+ case 2: strcpy(buf, "Feb"); break;
+ case 3: strcpy(buf, "Mar"); break;
+ case 4: strcpy(buf, "Apr"); break;
+ case 5: strcpy(buf, "May"); break;
+ case 6: strcpy(buf, "Jun"); break;
+ case 7: strcpy(buf, "Jul"); break;
+ case 8: strcpy(buf, "Aug"); break;
+ case 9: strcpy(buf, "Sep"); break;
+ case 10: strcpy(buf, "Oct"); break;
+ case 11: strcpy(buf, "Nov"); break;
+ case 12: strcpy(buf, "Dec"); break;
+ default: break;
+ }
+
+ s32 textw = renderer->calculate_text_width(fnt_title, buf);
+ renderer->render_text(fnt_title, posx + (width_per_item/2)-(textw/2), y+(height_per_item/2)-(fnt_title->px_h/2), buf, COLOR_TEXT);
+ }
+
+ for (s32 ty = 0; ty < grid_rows; ty++) {
+ s32 posy = y + ty * height_per_item;
+ renderer->render_rectangle(x,posy,w,1, COLOR_SCHEDULE_BORDER_THIN);
+
+ char buf[50];
+ buf[0] = 0;
+
+ switch(ty) {
+ case 1: strcpy(buf, "Income"); break;
+ case 2: strcpy(buf, "Utility"); break;
+ case 3: strcpy(buf, "Employee Benefits"); break;
+ case 4: strcpy(buf, "Repairs"); break;
+ case 5: strcpy(buf, "Fuel"); break;
+ case 6: strcpy(buf, "Salaries"); break;
+ case 7: strcpy(buf, "Trucks"); break;
+ // ADD NEW ENTRY HERE
+ case 9: strcpy(buf, "Total Income"); break;
+ case 10: strcpy(buf, "Total Expenses"); break;
+ case 11: strcpy(buf, "Total Profit"); break;
+ default: break;
+ }
+
+ renderer->render_text(fnt_title, x + textpad, posy+(height_per_item/2)-(fnt_title->px_h/2), buf, COLOR_TEXT);
+ }
+
+ if (!collection) {
+ char* txtbuf = invalid_location ? "Invalid location" : "No data for time period";
+ s32 textw = renderer->calculate_text_width(fnt_title, txtbuf);
+ s32 textx = x + (width_per_item*9) - (textw/2);
+ s32 texty = y + (h/2)-(fnt_title->px_h/2);
+ renderer->render_text(fnt_title, textx, texty, txtbuf, COLOR_TEXT);
+ }
+
+ renderer->render_rectangle_outline(x,y,w,h, 1, COLOR_SCHEDULE_BORDER);
+}
+
+static void _insights_draw_chart(platform_window*window, bool invalid_location, float scale, money_data_collection* collection, s32 grid_rows, s32 grid_cols, s32 textpad, s32 x, s32 y, s32 w, s32 h, s32 width_per_item, s32 height_per_item)
+{
+ font* fnt = fnt_rd16;
+ font* fnt_big = fnt_rd24;
+
+ s32 xaxis_height = fnt_big->px_h;
+ h -= xaxis_height;
+
+ s32 max_val = 0;
+ s32 min_val = INT_MAX;
+ for (s32 i = 0; i < MONTHS_IN_YEAR; i++) {
+ if (!collection) break;
+ money_data data = collection->months[i];
+
+ #define CHECK_MIN_MAX(_val)\
+ if (_val > max_val) max_val = _val;\
+ if (_val < min_val) min_val = _val;
+
+ CHECK_MIN_MAX(data.income_from_trips);
+ CHECK_MIN_MAX(data.expenses_from_utility);
+ CHECK_MIN_MAX(data.expenses_from_healthcare);
+ CHECK_MIN_MAX(data.expenses_from_repairs);
+ CHECK_MIN_MAX(data.expenses_from_fuel);
+ CHECK_MIN_MAX(data.expenses_from_employees);
+ CHECK_MIN_MAX(data.expenses_from_trucks);
+ // ADD NEW ENTRY HERE
+ CHECK_MIN_MAX(data.total_income);
+ CHECK_MIN_MAX(data.total_expenses);
+ CHECK_MIN_MAX(data.total_profit);
+ }
+
+ if (min_val == INT_MAX) min_val = 0;
+
+ s32 steps = 10;
+ s32 step_size = 0;
+
+ // Round min and max to nearest 10k
+ {
+ s32 max = max_val >= abs(min_val) ? max_val : abs(min_val);
+ s32 round_to = 10000;
+ if (max > 10000) round_to = 10000;
+ if (max > 100000) round_to = 100000;
+ if (max > 1000000) round_to = 1000000;
+ if (max > 10000000) round_to = 10000000;
+ if (max > 100000000) round_to = 100000000;
+
+ max_val = max_val + round_to - max_val % round_to;
+ if (min_val > round_to) min_val = min_val - min_val % round_to;
+ else min_val = min_val - round_to - (min_val%round_to);
+
+ // make sure negative and positive y-axis is symmetrical
+ if (max_val > abs(min_val)) {
+ min_val = -max_val;
+ }
+ else if (max_val < abs(min_val)) {
+ max_val = abs(min_val);
+ }
+
+ step_size = (max_val-min_val)/steps;
+ }
+
+ static bool enabled_categories[10] = {1,1,1,1,1,1,1,1,1,1};
+
+ color colors[] = {
+ rgb(87, 82, 208),
+ rgb(3, 118, 247),
+ rgb(54, 166, 214),
+ rgb(50, 140, 250),
+ rgb(90, 196, 248),
+ rgb(78, 213, 95),
+ rgb(252, 200, 3),
+ rgb(249, 146, 5),
+ rgb(249, 59, 47),
+ rgb(249, 45, 82),
+ };
+
+ char* legenda_items[] = {
+ "Income",
+ "Utility",
+ "Employee Benefits",
+ "Repairs",
+ "Fuel",
+ "Salaries",
+ "Trucks",
+ // ADD NEW ENTRY HERE
+ "Total Income",
+ "Total Expenses",
+ "Total Profit",
+ };
+
+ // Legenda
+ {
+ s32 text_spacing = 10*scale;
+ s32 outline_spacing = text_spacing/2;
+ s32 _index = 0;
+
+ #define PUSH_LEGENDA_ITEM(_text){\
+ s32 yy = y+((fnt_big->px_h+text_spacing)*_index);\
+ s32 totalw = width_per_item*3;\
+ bool hovered = (_global_mouse.x >= x-outline_spacing && _global_mouse.x <= x+totalw+outline_spacing\
+ && _global_mouse.y >= yy-outline_spacing && _global_mouse.y <= yy + fnt_big->px_h+outline_spacing);\
+ if (hovered) {platform_set_cursor(window, CURSOR_POINTER); if (is_left_clicked())\
+ { enabled_categories[_index] = !enabled_categories[_index]; audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1); }}\
+ renderer->render_rectangle(x,yy,fnt_big->px_h, fnt_big->px_h, enabled_categories[_index] ? colors[_index] : LEGENDA_COLOR_DISABLED);\
+ if (hovered) renderer->render_rectangle(x-outline_spacing,yy-outline_spacing,totalw+(text_spacing),fnt_big->px_h+(text_spacing), LEGENDA_HOVER_BACKGROUND_COLOR);\
+ renderer->render_text(fnt_big,x+fnt_big->px_h+text_spacing,yy,_text,COLOR_TEXT);_index++;}
+
+ for (s32 i = 0; i < sizeof(legenda_items)/sizeof(char*); i++) {
+ PUSH_LEGENDA_ITEM(legenda_items[i]);
+ }
+ }
+
+ // Graph y axis
+ {
+ s32 linex = x + 4*width_per_item;
+
+ float step_h = (h)/(float)steps;
+ for (s32 i = 0; i < steps+1; i++) {
+ char buf[20];
+ s32 val = max_val - (step_size*i);
+ sprintf(buf, "%d", val);
+ s32 textw = renderer->calculate_text_width(fnt, buf);
+ renderer->render_text(fnt, x+(width_per_item*4)-textw-(10*scale), y+(step_h*i)-(fnt->px_h/2), buf, COLOR_TEXT);
+ renderer->render_rectangle(linex, y+h*(i/(float)steps), width_per_item*(MONTHS_IN_YEAR-1), 1, LEGENDA_SUB_COLOR_DISABLED);
+ }
+
+ {
+ renderer->render_rectangle(linex, y + h - (h*1.0f), width_per_item*(MONTHS_IN_YEAR-1), 1, LEGENDA_COLOR_DISABLED);
+ renderer->render_rectangle(linex, y + h - (h*0.5f), width_per_item*(MONTHS_IN_YEAR-1), 1, LEGENDA_COLOR_DISABLED);
+ renderer->render_rectangle(linex, y + h - (h*0.0f), width_per_item*(MONTHS_IN_YEAR-1), 1, LEGENDA_COLOR_DISABLED);
+ }
+ }
+
+ // Graph x axis
+ for (s32 i = 0; i < MONTHS_IN_YEAR; i++) {
+ char buf[50];
+ buf[0] = 0;
+
+ switch(i+1) {
+ case 1: strcpy(buf, "Jan"); break;
+ case 2: strcpy(buf, "Feb"); break;
+ case 3: strcpy(buf, "Mar"); break;
+ case 4: strcpy(buf, "Apr"); break;
+ case 5: strcpy(buf, "May"); break;
+ case 6: strcpy(buf, "Jun"); break;
+ case 7: strcpy(buf, "Jul"); break;
+ case 8: strcpy(buf, "Aug"); break;
+ case 9: strcpy(buf, "Sep"); break;
+ case 10: strcpy(buf, "Oct"); break;
+ case 11: strcpy(buf, "Nov"); break;
+ case 12: strcpy(buf, "Dec"); break;
+ default: break;
+ }
+ s32 textw = renderer->calculate_text_width(fnt,buf);
+
+ renderer->render_text(fnt, x+(width_per_item*(i+4))-(textw/2), y+h+(10*scale), buf, COLOR_TEXT);
+ }
+
+ // Graph data
+ {
+ s32 total_diff = max_val-min_val;
+ if (total_diff == 0) total_diff = 1;
+
+ s32 dot_size = 4*scale;
+ s32 dot_offset =dot_size/2;
+ s32 _index = 0;
+ //if (min_val < 0) min_val = 0;
+
+ #define DRAW_LINES_FOR_DATA(_var){\
+ s32 last_dot_x = 0;\
+ s32 last_dot_y = 0;\
+ for (s32 i = 0; i < MONTHS_IN_YEAR; i++) {\
+ if (!collection) break;\
+ if (!enabled_categories[_index]) break;\
+ money_data data = collection->months[i];\
+ if (isnan(data.total_income)) continue;\
+ s32 val_diff = (_var) - min_val;\
+ s32 dot_x = x + (i+4)*width_per_item;\
+ s32 dot_y = y + h - (h*(val_diff/(float)total_diff));\
+ renderer->render_image_tint(img_dot, dot_x-dot_offset, dot_y-dot_offset, dot_size, dot_size, colors[_index]);\
+ if (mouse_interacts(dot_x-dot_offset, dot_y-dot_offset, dot_size, dot_size)) {\
+ reset_left_click();\
+ s32 info_x = dot_x;\
+ s32 info_y = dot_y+(dotsize/2);\
+ char info_txt[50];\
+ sprintf(info_txt, "%s: $%.0f", legenda_items[_index], _var);\
+ show_tooltip(info_x, info_y, info_txt);\
+ }\
+ if (last_dot_x != 0) {\
+ renderer->render_line(dot_x, dot_y, last_dot_x, last_dot_y, 1, colors[_index]);\
+ }\
+ last_dot_x = dot_x;\
+ last_dot_y = dot_y;\
+ }_index++;\
+ }
+
+ DRAW_LINES_FOR_DATA(data.income_from_trips);
+ DRAW_LINES_FOR_DATA(data.expenses_from_utility);
+ DRAW_LINES_FOR_DATA(data.expenses_from_healthcare);
+ DRAW_LINES_FOR_DATA(data.expenses_from_repairs);
+ DRAW_LINES_FOR_DATA(data.expenses_from_fuel);
+ DRAW_LINES_FOR_DATA(data.expenses_from_employees);
+ DRAW_LINES_FOR_DATA(data.expenses_from_trucks);
+ // ADD NEW ENTRY HERE
+ DRAW_LINES_FOR_DATA(data.total_income);
+ DRAW_LINES_FOR_DATA(data.total_expenses);
+ DRAW_LINES_FOR_DATA(data.total_profit);
+ }
+
+ if (!collection) {
+ char* txtbuf = invalid_location ? "Invalid location" : "No data for time period";
+ s32 textw = renderer->calculate_text_width(fnt_big, txtbuf);
+ s32 textx = x + (width_per_item*9) - (textw/2);
+ s32 texty = y + (h/2)-(fnt_big->px_h/2);
+ renderer->render_text(fnt_big, textx, texty, txtbuf, COLOR_TEXT);
+ }
+}
+
+static void world_map_draw_insights(platform_window* window)
+{
+ s32 w = area.w*0.9;
+ s32 h = area.h*0.7;
+ s32 x = area.x + area.w*0.05;
+ s32 y = area.y + area.w*0.05;
+ s32 w_orig = w;
+ s32 h_orig = h;
+ s32 x_orig = x;
+ s32 y_orig = y;
+ panel_render(scale, x, y, w, h);
+
+ #define GRID_ROWS ((sizeof(money_data)/sizeof(float))+2)
+ #define GRID_COLS (MONTHS_IN_YEAR+3)
+
+ s32 button_row_h = 44*scale;
+ s32 btn_size = 34*scale;
+ s32 spacing = 5*scale;
+
+ s32 pad = 40*scale;
+ s32 halfpad = pad/2;
+ s32 textpad = 20*scale;
+
+ w -= pad*2;
+ h -= pad*2;
+
+ h -= button_row_h+spacing;
+
+ s32 width_per_item = (w/GRID_COLS);
+ s32 height_per_item = (h/GRID_ROWS);
+ s32 new_w = width_per_item*GRID_COLS;
+ s32 new_h = height_per_item*GRID_ROWS;
+ s32 off_x = w - new_w;
+ s32 off_y = h - new_h;
+ w = new_w;
+ h = new_h;
+
+ s32 panel_w = w+(halfpad*2);
+
+ enum insights_panel_format
+ {
+ CHART,
+ GRID,
+ };
+ static enum insights_panel_format current_format = GRID;
+ static world_location* active_world_location_filter = 0;
+
+ {
+ s32 btn_row_x = x + off_x/2+halfpad;
+ s32 btn_row_y = y + off_y/2+halfpad;
+
+ s32 button_pad = (button_row_h - btn_size)/2;
+ s32 btn_start_x = btn_row_x + panel_w - (4*scale);
+ s32 btn_left_start_x = btn_row_x+(4*scale)+button_pad;
+ s32 year_w = (80*scale);
+ s32 year_text_w = year_w+(button_pad*2);
+ s32 btn_y = btn_row_y + button_pad;
+
+ // background
+ button_render(scale, BUTTON_STATIC, 0, btn_row_x, btn_row_y,panel_w, button_row_h);
+
+ // Buttons right
+ if (push_info_panel_button(scale, img_graph, btn_start_x - ((btn_size + button_pad)*1), btn_y, btn_size, current_format!=CHART, false)) (current_format = CHART);
+ if (push_info_panel_button(scale, img_grid, btn_start_x - ((btn_size + button_pad)*2), btn_y, btn_size, current_format!=GRID, false)) (current_format = GRID);
+
+ font* fnt = fnt_rd24;
+ s32 current_year = 1900+_active_world->start_year+insights_selected_year_index;
+
+ // Buttons left
+ button_render(scale, BUTTON_STATIC, 0, btn_left_start_x+btn_size+button_pad, btn_y,year_w, btn_size);
+ char buf[10];
+ sprintf(buf, "%d", current_year);
+ s32 textw = renderer->calculate_text_width(fnt,buf);
+ renderer->render_text(fnt,btn_left_start_x+(btn_size)+(year_text_w/2)-(textw/2), btn_y+(btn_size/2)-(fnt->px_h/2), buf, COLOR_TEXT);
+
+ s32 btn_right_x = btn_left_start_x + (btn_size) + year_text_w;
+ if (push_info_panel_button(scale, img_arrow_left, btn_left_start_x, btn_y, btn_size, insights_selected_year_index>=1, false)) (insights_selected_year_index--);
+ if (push_info_panel_button(scale, img_arrow_right, btn_right_x, btn_y,
+ btn_size, insights_selected_year_index<_active_world->insights.length-1, false)) (insights_selected_year_index++);
+
+ // Location selector
+ s32 tb_filter_x = btn_right_x + btn_size + button_pad;
+ s32 tb_width = 520*scale;
+ active_world_location_filter = location_selector_render(window, scale, true, active_world_location_filter, tb_filter_x, btn_y, tb_width, btn_size);
+
+ // Clear button
+ s32 clear_btn_x = tb_filter_x + tb_width + button_pad;
+ if (push_info_panel_button(scale, img_globe, clear_btn_x, btn_y, btn_size, _global_keyboard.input_text_len, false)) {
+ active_world_location_filter = 0;
+ keyboard_set_input_text("");
+ }
+ }
+
+ x+=off_x/2+pad;
+ y+=off_y/2+pad+button_row_h+spacing;
+
+ button_render(scale, BUTTON_STATIC, 0, x-halfpad,y-halfpad,panel_w,h+(halfpad*2));
+
+ static money_data_collection* prev_collection = 0;
+ money_data_collection* collection = get_current_insights_data(_active_world); // Get current year page, also a hack to make sure a new page exists at year switch.
+ if (!prev_collection || collection != prev_collection) {
+ prev_collection = collection;
+ insights_selected_year_index = _active_world->insights.length-1;
+ }
+
+ money_data_collection* collection_to_use = array_at(&_active_world->insights, insights_selected_year_index); // Get page to use
+ if (active_world_location_filter) {
+ if (active_world_location_filter->is_owned) {
+ s32 index_of_filter_page = insights_selected_year_index;
+ s32 purchase_year_diff = active_world_location_filter->purchase_year - _active_world->start_year;
+ index_of_filter_page -= purchase_year_diff;
+ if (index_of_filter_page < 0) collection_to_use = 0;
+ else if (index_of_filter_page > active_world_location_filter->insights.length-1) collection_to_use = 0;
+ else collection_to_use = array_at(&active_world_location_filter->insights, index_of_filter_page);
+ }
+ else {
+ collection_to_use = 0;
+ }
+ }
+
+ bool is_typing_in_filter_tb = (!active_world_location_filter && _global_keyboard.input_text_len);
+ if (is_typing_in_filter_tb) collection_to_use = 0;
+
+ if (current_format == GRID) _insights_draw_grid(scale, is_typing_in_filter_tb, collection_to_use, GRID_ROWS, GRID_COLS, textpad, x, y, w, h, width_per_item, height_per_item);
+ if (current_format == CHART) _insights_draw_chart(window, is_typing_in_filter_tb, scale, collection_to_use, GRID_ROWS, GRID_COLS, textpad, x, y, w, h, width_per_item, height_per_item);
+
+ if (mouse_interacts(x_orig, y_orig, w_orig, h_orig)) {
+ reset_left_click();
+ } else if (is_left_clicked()) {
+ scene_state = WORLD_SCENE_STATE_IDLE;
+ }
+}
+
+static void world_map_draw_event_log(platform_window* window)
+{
+ _active_world->log.has_unread_messages = false;
+
+ font* fnt = fnt_rd16;
+
+ s32 panel_h = area.h*0.9f;
+ s32 panel_pad = area.h*0.05f;
+
+ s32 panel_w = area.w/3.5f;
+
+ s32 panel_x = area.x + panel_pad;
+ s32 panel_y = area.y + panel_pad;
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ float text_pad = panel_w*0.05f;
+
+ s32 text_y = panel_y + text_pad;
+ s32 text_x = panel_x + text_pad;
+ s32 text_w = panel_w - (text_pad*2);
+
+ renderer->render_set_scissor(window, text_x, text_y, panel_w, panel_h-(text_pad*2));
+
+ if (_active_world->log.events.length) {
+ s32 read_cursor = _active_world->log.write_cursor;
+ for (s32 i = 0; i < _active_world->log.events.length; i++) {
+ read_cursor--;
+ if (read_cursor < 0) read_cursor = _active_world->log.events.length-1;
+
+ event* e = array_at(&_active_world->log.events, read_cursor);
+
+ float inner_pad = 5*scale;
+
+
+ s32 texth = renderer->render_text_cutoff(fnt, text_x+inner_pad, text_y+inner_pad, e->message, COLOR_TEXT, text_w-(inner_pad*2));
+ s32 highlight_w = text_w;
+ s32 highlight_h = texth+(inner_pad);
+
+ if (mouse_interacts(text_x, text_y, highlight_w, highlight_h)) {
+ platform_set_cursor(window, CURSOR_POINTER);
+ renderer->render_rectangle(text_x, text_y, text_w, highlight_h, rgba(255,255,255,20));
+
+ if (is_left_clicked()) {
+ switch (e->type)
+ {
+ case EVENT_TYPE_MISSED_SHIPMENT_NO_TRUCK:
+ place_detail_show_employee_detail((employee*)e->data);
+ break;
+ case EVENT_TYPE_MISSED_SHIPMENT_NO_ASSIGNEE:
+ case EVENT_TYPE_MISSED_SHIPMENT_NOT_AT_LOCATION: {
+ scheduled_job* job = (scheduled_job*)e->data;
+ place_detail_show_schedule_with_highlighted_job(job->location, job, e->job_time);
+ } break;
+ case EVENT_TYPE_EMPLOYEE_QUIT:
+ place_detail_show_schedule_with_highlighted_job((world_location*)e->data, 0, e->job_time);
+ break;
+
+ default:
+ log_assert(0, "Invalid event type.");
+ break;
+ }
+ }
+ }
+
+ text_y += highlight_h;
+ if (text_y > panel_y + panel_h) break;
+ }
+ }
+ else {
+ char* buf = "No events.";
+ s32 tw = renderer->calculate_text_width(fnt, buf);
+ renderer->render_text_cutoff(fnt, text_x + (text_w/2)-(tw/2), text_y, "No events.", COLOR_TEXT, text_w);
+ }
+
+ renderer->render_reset_scissor(window);
+
+ if (mouse_interacts(panel_x, panel_y, panel_w, panel_h)) {
+ reset_left_click();
+ } else if (is_left_clicked()) {
+ scene_state = WORLD_SCENE_STATE_IDLE;
+ }
+}
+
+s32 hovered_investment_panel_item_index = -1;
+static s32 world_map_push_invest_panel_item(s32 x, s32 y, s32 w, s32 index, char* text, s32* fval)
+{
+ s32 pad = 40*scale;
+ s32 halfpad = pad/2;
+ s32 panel_w = w-(pad);
+ float item_pad = 30*scale;
+ float item_spacing = 5*scale;
+ float item_halfpad = item_pad/2;
+
+ font* fnt = fnt_rd24;
+ char value_str[30];
+ sprintf(value_str, "$%d", *fval);
+ float item_h = fnt->px_h+item_pad;
+ float item_x = x+halfpad;
+ float item_y = y+halfpad+(index*(item_h+item_spacing));
+ float item_w = panel_w;
+ float content_x = item_x+item_halfpad;
+ float content_y = item_y+item_halfpad;
+ float total_val_editor_w = item_w*0.3f;
+ float button_left_x = item_x + item_w - total_val_editor_w - item_halfpad;
+ float btn_size = fnt->px_h*2;
+ float btn_y = item_y + ((item_h-btn_size)/2);
+ button_render(scale, BUTTON_STATIC, 0, item_x, item_y,item_w,item_h);
+ renderer->render_text(fnt, content_x, content_y, text, COLOR_TEXT);
+ // Button decrease simulation speed
+ if (push_info_panel_button(scale, img_arrow_left, button_left_x, btn_y, btn_size, *fval > 0, false)) {
+ *fval -= 100.0f;
+ }
+ // Button increase simulation speed
+ if (push_info_panel_button(scale, img_arrow_right, button_left_x+total_val_editor_w-btn_size, btn_y, btn_size, true, false)) {
+ *fval += 100.0f;
+ }
+ s32 item_text_w = renderer->calculate_text_width(fnt, value_str);
+ s32 item_text_x = button_left_x + (total_val_editor_w/2) - (item_text_w/2);
+ renderer->render_text(fnt, item_text_x, content_y, value_str, COLOR_TEXT);
+
+ if (mouse_interacts_peak(item_x, item_y, item_w, item_h)) {
+ hovered_investment_panel_item_index = index;
+ }
+
+ return item_y + item_h;
+}
+
+static void world_map_draw_invest_panel(platform_window* window)
+{
+ s32 w = area.w*0.5;
+ s32 h = area.h*0.7;
+ s32 x = area.x + area.w*0.25;
+ s32 y = area.y + area.w*0.05;
+ panel_render(scale, x, y, w, h);
+
+ s32 pad = 40*scale;
+ s32 halfpad = pad/2;
+ s32 panel_w = w-(pad);
+
+ #define TOTAL_INVEST_ITEM_COUNT (5)
+
+ hovered_investment_panel_item_index = -1;
+
+ world_map_push_invest_panel_item(x, y, w, 0, "Workspace safety", (s32*)(&_active_world->investments.safety));
+ world_map_push_invest_panel_item(x, y, w, 1, "Marketing", (s32*)(&_active_world->investments.marketing));
+ world_map_push_invest_panel_item(x, y, w, 2, "Human resources", (s32*)(&_active_world->investments.human_resources));
+ world_map_push_invest_panel_item(x, y, w, 3, "Employee training", (s32*)(&_active_world->investments.training));
+ s32 item_bottom = world_map_push_invest_panel_item(x, y, w, 4, "Legal department", (s32*)(&_active_world->investments.legal));
+
+ float detail_panel_y = item_bottom + halfpad;
+ float detail_panel_h = h - (detail_panel_y-y)-halfpad;
+
+ button_render(scale, BUTTON_STATIC, 0, x+halfpad, detail_panel_y,panel_w,detail_panel_h);
+
+ s32 info_text_pad = 20*scale;
+
+ char* info_text = 0;
+ switch (hovered_investment_panel_item_index)
+ {
+ case 0: info_text = "Increasing the budget for workspace safety will ensure employees receive the proper workspace safety training.";
+ break;
+ case 1: info_text = "Increasing the marketing budget will give your business more publicity and will secure more work proposals.";
+ break;
+ case 2: info_text = "Investing in human resources will guarantee more responses to vacancies.";
+ break;
+ case 3: info_text = "Providing training for your employees will increase happiness for your employees and make them work more efficiently.";
+ break;
+ case 4: info_text = "The legal department is essential for settling legal disputes.";
+ break;
+ }
+
+ if (info_text)
+ renderer->render_text_cutoff(fnt_rd24, x+halfpad+info_text_pad, detail_panel_y+info_text_pad, info_text, COLOR_TEXT, panel_w - (info_text_pad*2));
+
+ if (mouse_interacts(x,y,w,h)) {
+ reset_left_click();
+ } else if (is_left_clicked()) {
+ scene_state = WORLD_SCENE_STATE_IDLE;
+ }
+}
+
+static void world_map_draw_purchase_location_panel(platform_window* window)
+{
+ s32 screen_center_x = area.x + (area.w/2);
+ s32 screen_center_y = area.y + (area.h/2);
+
+ float vertical_pad = 20 * scale;
+
+ s32 panel_h = 160 * scale;
+ s32 panel_w = 280 * scale;
+
+ s32 panel_x = screen_center_x - (panel_w/2);
+ s32 panel_y = screen_center_y - (panel_h/2);
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ // info text
+ {
+ font* font_title = FONT_REGULAR(SIZE_RD(area.w, 32));
+ {
+ char* title = scene_data.location_to_purchase->name;
+ s32 text_w = renderer->calculate_text_width(font_title, title);
+ s32 text_x = screen_center_x - (text_w/2);
+ s32 text_y = panel_y + (vertical_pad);
+ renderer->render_text(font_title, text_x+2, text_y+2, title, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_title, text_x, text_y, title, COLOR_TEXT);
+ }
+ {
+ char buf[100];
+ sprintf(buf, "Purchase a garage for $%.0f?", world_location_get_price(scene_data.location_to_purchase));
+ char* text = buf;
+ font* font_info = FONT_REGULAR(SIZE_RD(area.w, 20));
+ s32 text_w = renderer->calculate_text_width(font_info, text);
+ s32 text_x = screen_center_x - (text_w/2);
+ s32 text_y = panel_y + vertical_pad*1.5+font_title->px_h;
+ renderer->render_text(font_info, text_x+2, text_y+2, text, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_info, text_x, text_y, text, COLOR_TEXT);
+ }
+ }
+
+ s32 button_w = 178 * scale;
+ s32 button_h = 37 * scale;
+ if (button_render(scale, true, "Purchase", screen_center_x - (button_w/2), panel_y + panel_h - button_h - vertical_pad, button_w, button_h))
+ {
+ // Mark as owned.
+ scene_data.location_to_purchase->is_owned = true;
+ scene_data.location_to_purchase->purchase_year = _active_world->current_time.tm_year;
+
+ // Make sure insights are ready to display.
+ money_data_collection* collection = get_current_insights_data_for_location(_active_world, scene_data.location_to_purchase);
+ collection->months[_active_world->current_time.tm_mon].total_income = 0;
+
+ _active_world->money -= world_location_get_price(scene_data.location_to_purchase);
+
+ scene_state = WORLD_SCENE_STATE_IDLE;
+ }
+
+ button_w = 30 * scale;
+ if (button_render(scale, true, "X", panel_x+panel_w-button_w, panel_y+2, button_w, button_w))
+ {
+ scene_state = WORLD_SCENE_STATE_IDLE;
+ }
+
+ if (mouse_interacts(panel_x,panel_y,panel_w,panel_h)) {
+ reset_left_click();
+ } else if (is_left_clicked()) {
+ scene_state = WORLD_SCENE_STATE_IDLE;
+ }
+}
+
+static void world_map_draw_viewing_job(platform_window* window)
+{
+ active_job* job = get_active_job_by_ref(_active_world, currently_viewing_active_job);
+ if (!job) {
+ currently_viewing_active_job.offerid = INVALID_ID;
+ return;
+ }
+
+ s32 panel_offset_from_job = area.h*0.1f;
+ s32 panel_h = area.h*0.32f;
+ s32 panel_w = area.w*0.15f;
+
+ s32 panel_x = job->px_pos.x;
+ s32 panel_y = job->px_pos.y;
+
+ if (job->px_pos.y > area.y + (area.h/2)) {
+ panel_y -= panel_h - panel_offset_from_job;
+
+ renderer->render_line(job->px_pos.x+(dotsize/2), job->px_pos.y+(dotsize/2), panel_x + (panel_w/3), panel_y+panel_h-5, 3, COLOR_INSPECT_ACTIVE_JOB_LINE_CONNECTION);
+ renderer->render_line(job->px_pos.x+(dotsize/2), job->px_pos.y+(dotsize/2), panel_x + (panel_w/3*2), panel_y+panel_h-5, 3, COLOR_INSPECT_ACTIVE_JOB_LINE_CONNECTION);
+ }
+ else {
+ panel_y += panel_offset_from_job;
+
+ renderer->render_line(job->px_pos.x+(dotsize/2), job->px_pos.y+(dotsize/2), panel_x + (panel_w/3), panel_y+5, 3, COLOR_INSPECT_ACTIVE_JOB_LINE_CONNECTION);
+ renderer->render_line(job->px_pos.x+(dotsize/2), job->px_pos.y+(dotsize/2), panel_x + (panel_w/3*2), panel_y+5, 3, COLOR_INSPECT_ACTIVE_JOB_LINE_CONNECTION);
+ }
+
+ panel_render(scale, panel_x, panel_y, panel_w, panel_h);
+
+ s32 panel_pad = 10*scale;
+ s32 truck_img_x = panel_x + panel_pad;
+ s32 truck_img_y = panel_y + panel_pad;
+ s32 truck_img_s = panel_w * 0.6f;
+
+ s32 portrait_s = truck_img_s / 2;
+
+ renderer->render_image(job->assigned_truck.logo, truck_img_x+(panel_pad), truck_img_y, truck_img_s, truck_img_s);
+ draw_employee_portrait(&job->assignee, truck_img_x+(panel_pad) + truck_img_s - (portrait_s/3), truck_img_y + (truck_img_s / 3), portrait_s, portrait_s);
+
+ {
+ s32 text_x = truck_img_x;
+ float text_pad = (5*scale);
+ s32 text_y = truck_img_y + truck_img_s + panel_pad;
+ font* fnt = fnt_rd16;
+ font* fnt_s = fnt_rd12;
+ renderer->render_text(fnt,text_x,text_y,job->assignee.name,COLOR_TEXT);
+ text_y += fnt->px_h + text_pad;
+
+ // Name + origin
+ {
+ world_location* origin = get_world_location_by_id(_active_world, job->assignee.original_location_id);
+ char buf[100];
+ sprintf(buf, "Works at %s", origin->name);
+ renderer->render_text(fnt_s,text_x,text_y,buf,COLOR_TEXT);
+ text_y += fnt_s->px_h + text_pad;
+ }
+
+ // Job info
+ {
+ text_y += (20*scale);
+
+ char daybuf[10];
+ switch(job->day) {
+ case 1: strcpy(daybuf, "Mon"); break;
+ case 2: strcpy(daybuf, "Tue"); break;
+ case 3: strcpy(daybuf, "Wed"); break;
+ case 4: strcpy(daybuf, "Thu"); break;
+ case 5: strcpy(daybuf, "Fri"); break;
+ case 6: strcpy(daybuf, "Sat"); break;
+ case 0: strcpy(daybuf, "Sun"); break;
+ }
+
+ char buf[100];
+ sprintf(buf, "Left on %s %02d:%02d", daybuf, WORK_HOUR_START + (job->timeslot/TIME_SLOTS_PER_HOUR), job->timeslot%TIME_SLOTS_PER_HOUR);
+ renderer->render_text(fnt_s,text_x,text_y,buf,COLOR_TEXT);
+ text_y += fnt_s->px_h + text_pad;
+ }
+
+ // Job status
+ if (!job->reversed)
+ {
+ job_endpoints endpoints = job_offer_get_endpoints(&job->offer);
+ char buf[100];
+ sprintf(buf, "Shipping %s to %s", job->offer.product->name, endpoints.dest->name);
+ text_y += renderer->render_text_cutoff(fnt_s,text_x,text_y,buf,COLOR_TEXT, panel_w - (panel_pad*2));
+ text_y += text_pad;
+ }
+ else {
+ world_location* source = *(world_location**)array_at(&job->offer.connections, 0); // Get source location
+ char buf[100];
+ sprintf(buf, "Returning to %s", source->name);
+ renderer->render_text_cutoff(fnt_s,text_x,text_y,buf,COLOR_TEXT, panel_w - (panel_pad*2));
+ text_y += fnt_s->px_h + text_pad;
+ }
+ }
+
+ if (mouse_interacts(panel_x,panel_y,panel_w,panel_h)) {
+ reset_left_click();
+ } else if (is_left_clicked()) {
+ currently_viewing_active_job.offerid = INVALID_ID;
+ }
+}
+
+static void world_handle_scroll(platform_window* window)
+{
+ static float target_zoom = 1.0f;
+ if (global_ui_context.mouse->scroll_state == SCROLL_UP)
+ {
+ if (target_zoom < zoom) target_zoom = zoom;
+ target_zoom+=0.1f;
+ }
+ if (global_ui_context.mouse->scroll_state == SCROLL_DOWN)
+ {
+ if (target_zoom > zoom) target_zoom = zoom;
+ target_zoom-=0.1f;
+ }
+
+ // Keep camera position
+ if (target_zoom != zoom) {
+ vec4 area = camera_get_target_rectangle(window);
+ int orig_w = area.w * zoom;
+ int orig_h = area.h * zoom;
+ int new_w = area.w * target_zoom;
+ int new_h = area.h * target_zoom;
+
+ float errorw = new_w - orig_w;
+ float errorh = new_h - orig_h;
+ camera_x -= errorw / 25.0f;
+ camera_y -= errorh / 40.0f;
+ }
+
+ // Smooth scrolling
+ if (target_zoom != zoom)
+ {
+ float error = target_zoom - zoom;
+ zoom += error / 10;
+ }
+
+ if (zoom < 1.0f) zoom = 1.0f;
+ if (zoom > 5.0f) zoom = 5.0f;
+
+ if (camera_x > 0.0f) camera_x = 0.0f;
+ if (camera_y > 0.0f) camera_y = 0.0f;
+
+ //if (is_left_down_peak())
+ {
+
+ }
+}
+
+void world_map_scene_render(platform_window* window)
+{
+ renderer->set_render_depth(5);
+
+ world_handle_scroll(window);
+
+ world_map_draw_info_panel(window, true);
+
+ renderer->set_render_depth(4);
+ switch (scene_state)
+ {
+ case WORLD_SCENE_STATE_INSIGHTS:
+ world_map_draw_insights(window);
+ break;
+ case WORLD_SCENE_STATE_LOG:
+ world_map_draw_event_log(window);
+ break;
+ case WORLD_SCENE_STATE_IDLE: break;
+ case WORLD_SCENE_STATE_PURCHASE_LOCATION:
+ world_map_draw_purchase_location_panel(window);
+ break;
+ case WORLD_SCENE_STATE_INVEST:
+ world_map_draw_invest_panel(window);
+ break;
+ }
+
+ renderer->set_render_depth(3);
+ if (currently_viewing_active_job.offerid != INVALID_ID) world_map_draw_viewing_job(window);
+
+ if (_active_world) {
+ world_update_result click_result = world_render(window, _active_world);
+
+ if (click_result.clicked_location) {
+ if (click_result.clicked_location->is_owned) {
+ place_detail_set_active_location(click_result.clicked_location);
+ game_set_active_scene(GAME_STATE_PLACE_DETAIL);
+ }
+ else {
+ scene_data.location_to_purchase = click_result.clicked_location;
+ scene_state = WORLD_SCENE_STATE_PURCHASE_LOCATION;
+ }
+ }
+ else if (click_result.clicked_job) {
+ currently_viewing_active_job = (active_job_ref){click_result.clicked_job->day, click_result.clicked_job->timeslot, click_result.clicked_job->offer.id};
+ }
+ }
+
+ renderer->set_render_depth(0);
+
+ vec4 area = camera_get_target_rectangle(window);
+ renderer->render_rectangle(area.x, area.y, area.w, area.h, COLOR_WORLD_MAP_BACKGROUND);
+ renderer->render_image(img_world_map, area.x + camera_x, area.y + camera_y, area.w*zoom, area.h*zoom);
+}
+
+void world_map_scene_update(platform_window* window)
+{
+ world_update(window, _active_world);
+ if (keyboard_is_key_pressed(KEY_ESCAPE)) {
+ scene_state = WORLD_SCENE_STATE_IDLE;
+ }
+}
+
+void world_map_scene_destroy()
+{
+
+} \ No newline at end of file
diff --git a/src/tooltip.c b/src/tooltip.c
new file mode 100644
index 0000000..5098ba7
--- /dev/null
+++ b/src/tooltip.c
@@ -0,0 +1,22 @@
+#define TOOLTIP_PAD (scale*14)
+
+void show_tooltip(s32 x, s32 y, char* buf) {
+ tooltop_visible = true;
+ string_copyn(tooltip_buffer, buf, 100);
+ tooltip_x = x+(20*scale);
+ tooltip_y = y-(fnt_rd16->px_h + (TOOLTIP_PAD))/2;
+}
+
+void update_render_tooltip() {
+ if (!tooltop_visible) return;
+ s32 current_render_depth = gl_render_depth;
+ renderer->set_render_depth(10);
+
+ s32 info_w = renderer->calculate_text_width(fnt_rd16, tooltip_buffer) + (TOOLTIP_PAD);
+ s32 info_h = fnt_rd16->px_h + (TOOLTIP_PAD);
+ button_draw_background(scale, tooltip_x, tooltip_y, info_w, info_h, COLOR_WHITE, COLOR_BUTTON);
+ renderer->render_text(fnt_rd16, tooltip_x+(TOOLTIP_PAD/2), tooltip_y+(TOOLTIP_PAD/2), tooltip_buffer, COLOR_TEXT);
+
+ renderer->set_render_depth(current_render_depth);
+ tooltop_visible = false;
+}
diff --git a/src/ui/animation.c b/src/ui/animation.c
new file mode 100644
index 0000000..62775bf
--- /dev/null
+++ b/src/ui/animation.c
@@ -0,0 +1,18 @@
+animation animation_create(s32 duration)
+{
+ animation an;
+ an.time = 0;
+ an.started = false;
+ an.duration = duration;
+ an.percentage = 0.0f;
+ return an;
+}
+
+float animation_update(animation* an)
+{
+ if (!an->started) return an->percentage;
+ an->time += frame_delta*1000.0f;
+ if (an->time > an->duration) an->time = an->duration;
+ an->percentage = an->time/an->duration;
+ return an->percentage;
+} \ No newline at end of file
diff --git a/src/ui/button.c b/src/ui/button.c
new file mode 100644
index 0000000..773fb7b
--- /dev/null
+++ b/src/ui/button.c
@@ -0,0 +1,101 @@
+bool button_draw_background_percentage(float scale, s32 x, s32 y, s32 w, s32 h, color tint, color fill, float percentage, color bar_fill)
+{
+ s32 cornor_size = img_button_topleft->width*(scale/2);
+ s32 top_width = w - (cornor_size*2);
+ s32 size_height = h - (cornor_size*2);
+
+ // top
+ renderer->render_image_tint(img_button_topleft, x, y, cornor_size, cornor_size, tint);
+ renderer->render_image_tint(img_button_top, x + cornor_size, y, top_width, cornor_size, tint);
+ renderer->render_image_tint(img_button_topright, x + cornor_size + top_width, y, cornor_size, cornor_size, tint);
+
+ // left
+ renderer->render_image_tint(img_button_left, x, y + cornor_size-1, cornor_size, size_height+2, tint);
+
+ // right
+ renderer->render_image_tint(img_button_right, x + cornor_size + top_width, y + cornor_size-1, cornor_size, size_height+2, tint);
+
+ // bottom
+ renderer->render_image_tint(img_button_bottomleft, x, y + cornor_size + size_height, cornor_size, cornor_size, tint);
+ renderer->render_image_tint(img_button_bottom, x + cornor_size, y + cornor_size + size_height, top_width, cornor_size, tint);
+ renderer->render_image_tint(img_button_bottomright, x + cornor_size + top_width, y + cornor_size + size_height, cornor_size, cornor_size, tint);
+
+ // fill
+ s32 pad = cornor_size-1;
+ renderer->render_rectangle(x+pad, y+pad, w-(pad*2), h-(pad*2), fill);
+ renderer->render_rectangle(x+pad, y+pad, (w-(pad*2))*percentage, h-(pad*2), bar_fill);
+
+ return _global_mouse.x >= x && _global_mouse.x <= x + w && _global_mouse.y >= y && _global_mouse.y <= y + h;
+}
+
+bool button_draw_background(float scale, s32 x, s32 y, s32 w, s32 h, color tint, color fill)
+{
+ s32 cornor_size = img_button_topleft->width*(scale/2);
+ s32 top_width = w - (cornor_size*2);
+ s32 size_height = h - (cornor_size*2);
+
+ // top
+ renderer->render_image_tint(img_button_topleft, x, y, cornor_size, cornor_size, tint);
+ renderer->render_image_tint(img_button_top, x + cornor_size, y, top_width, cornor_size, tint);
+ renderer->render_image_tint(img_button_topright, x + cornor_size + top_width, y, cornor_size, cornor_size, tint);
+
+ // left
+ renderer->render_image_tint(img_button_left, x, y + cornor_size-1, cornor_size, size_height+2, tint);
+
+ // right
+ renderer->render_image_tint(img_button_right, x + cornor_size + top_width, y + cornor_size-1, cornor_size, size_height+2, tint);
+
+ // bottom
+ renderer->render_image_tint(img_button_bottomleft, x, y + cornor_size + size_height, cornor_size, cornor_size, tint);
+ renderer->render_image_tint(img_button_bottom, x + cornor_size, y + cornor_size + size_height, top_width, cornor_size, tint);
+ renderer->render_image_tint(img_button_bottomright, x + cornor_size + top_width, y + cornor_size + size_height, cornor_size, cornor_size, tint);
+
+ // fill
+ s32 pad = cornor_size-1;
+ renderer->render_rectangle(x+pad, y+pad, w-(pad*2), h-(pad*2), fill);
+
+ return _global_mouse.x >= x && _global_mouse.x <= x + w && _global_mouse.y >= y && _global_mouse.y <= y + h;
+}
+
+bool button_render(float scale, button_type enabled, char* text, s32 x, s32 y, s32 w, s32 h)
+{
+ bool result = false;
+
+ color tint = COLOR_WHITE;
+ color fill = COLOR_BUTTON;
+
+ if (enabled == BUTTON_ENABLED || enabled == BUTTON_HIGHLIGHTED) {
+ if (mouse_interacts(x,y,w,h)) {
+ platform_set_cursor(main_window, CURSOR_POINTER);
+ if (is_left_clicked()) {
+ result = true;
+ audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1);
+ }
+ tint = COLOR_BUTTON_ACTIVE_TINT;
+ fill = COLOR_BUTTON_ACTIVE;
+ }
+
+ if (enabled == BUTTON_HIGHLIGHTED) {
+ tint = COLOR_BUTTON_HIGHLIGHTED_TINT;
+ fill = COLOR_BUTTON_HIGHLIGHTED_ACTIVE;
+ }
+ }
+ else if (enabled == BUTTON_DISABLED) {
+ tint = COLOR_BUTTON_DISABLED_TINT;
+ fill = COLOR_BUTTON_DISABLED;
+ }
+
+ button_draw_background(scale,x,y,w,h,tint,fill);
+
+ // text
+ if (text) {
+ font* font_sml = fnt_rd24;
+ s32 text_y = y + (h/2) - (font_sml->px_h/2);
+ s32 game_title_width = renderer->calculate_text_width(font_sml, text);
+ s32 text_x = x + (w/2) - (game_title_width/2);
+
+ renderer->render_text(font_sml, text_x+1, text_y+1, text, COLOR_TEXT_SHADOW);
+ renderer->render_text(font_sml, text_x, text_y, text, COLOR_TEXT);
+ }
+ return result;
+} \ No newline at end of file
diff --git a/src/ui/panel.c b/src/ui/panel.c
new file mode 100644
index 0000000..af24e66
--- /dev/null
+++ b/src/ui/panel.c
@@ -0,0 +1,29 @@
+
+void panel_render(float scale, s32 x, s32 y, s32 w, s32 h)
+{
+ s32 cornor_size = img_panel_topleft->width*(scale/2);
+ log_assert(w > cornor_size*2, "Panel width too small");
+ log_assert(h > cornor_size*2, "Panel height too small");
+ s32 top_width = w - (cornor_size*2);
+ s32 size_height = h - (cornor_size*2);
+
+ // top
+ renderer->render_image(img_panel_topleft, x, y, cornor_size, cornor_size);
+ renderer->render_image(img_panel_top, x + cornor_size, y, top_width, cornor_size);
+ renderer->render_image(img_panel_topright, x + cornor_size + top_width, y, cornor_size, cornor_size);
+
+ // left
+ renderer->render_image(img_panel_left, x, y + cornor_size-1, cornor_size, size_height+2);
+
+ // right
+ renderer->render_image(img_panel_right, x + cornor_size + top_width, y + cornor_size-1, cornor_size, size_height+2);
+
+ // bottom
+ renderer->render_image(img_panel_bottomleft, x, y + cornor_size + size_height, cornor_size, cornor_size);
+ renderer->render_image(img_panel_bottom, x + cornor_size, y + cornor_size + size_height, top_width, cornor_size);
+ renderer->render_image(img_panel_bottomright, x + cornor_size + top_width, y + cornor_size + size_height, cornor_size, cornor_size);
+
+ // fill
+ s32 pad = cornor_size-1;
+ renderer->render_rectangle(x+pad, y+pad, w-(pad*2), h-(pad*2), COLOR_PANEL_BACKGROUND);
+} \ No newline at end of file
diff --git a/src/ui/portrait.c b/src/ui/portrait.c
new file mode 100644
index 0000000..09301fe
--- /dev/null
+++ b/src/ui/portrait.c
@@ -0,0 +1,21 @@
+void draw_employee_portrait(employee* emp, float x, float y, float w, float h)
+{
+ float body_s = h*0.8f;
+ float body_x = x + (w/2)-(body_s/2);
+ float body_y = y+h-body_s;
+
+ float head_s = h*0.45f;
+ float head_x = x + (w/2)-(head_s/2);
+ float head_y = y+(h*0.11f);
+
+ float hair_s = h*0.6f;
+ float hair_x = x + (w/2)-(hair_s/2);
+ float hair_y = y;
+
+ //renderer->render_image(img_portrait, x,y,w,h);
+
+ renderer->render_image_tint(img_portrait_head, head_x, head_y, head_s, head_s, emp->face_color);
+ renderer->render_image_tint(img_portrait_body, body_x, body_y, body_s, body_s, emp->body_color);
+ renderer->render_image_tint(img_portrait_hair[emp->portrait_hair_type], hair_x, hair_y, hair_s, hair_s, emp->hair_color);
+
+} \ No newline at end of file
diff --git a/src/ui/selectors.c b/src/ui/selectors.c
new file mode 100644
index 0000000..dc5a6bc
--- /dev/null
+++ b/src/ui/selectors.c
@@ -0,0 +1,212 @@
+
+employee* employee_selector_render(platform_window* window, float scale, bool enabled, employee* current_val, s32 x, s32 y, s32 w, s32 h, animation an, scheduled_job* offer) {
+ #define ANIMATION_OFFSET (-30*scale)
+ color text_color = AN_LI_TINT(COLOR_TEXT, an);
+ color c_tb_tint = AN_LI_TINT(COLOR_TEXTBOX_TINT, an);
+ color c_tb_fill = AN_LI_TINT(COLOR_TEXTBOX_FILL, an);
+ color c_white = AN_LI_TINT(COLOR_WHITE, an);
+ color c_correct = AN_LI_TINT(COLOR_CORRECT, an);
+ color c_wrong = AN_LI_TINT(COLOR_WRONG, an);
+ color c_btn = AN_LI_TINT(COLOR_BUTTON, an);
+
+ employee* result = current_val;
+
+ s32 tb_pad = 20*scale;
+ button_render(scale, BUTTON_STATIC, 0, x,y,w,h);
+
+ s32 tb_width = 220*scale;
+ s32 tb_height = h-(tb_pad*2);
+ s32 tb_y = y+tb_pad;
+ s32 tb_x = x+tb_pad + (ANIMATION_OFFSET - (ANIMATION_OFFSET*an.percentage));
+
+ {
+ // Start taking input on click.
+ if (button_draw_background(scale, tb_x,tb_y,tb_width,tb_height, c_tb_tint, c_tb_fill) && enabled) {
+ platform_set_cursor(window, CURSOR_POINTER);
+ if (is_left_clicked()) {
+ _global_keyboard.take_input = true;
+ _global_keyboard.input_mode = INPUT_NUMERIC;
+ }
+ }
+ // Clear on outside click or enter.
+ else if (is_left_clicked_peak()) {
+ _global_keyboard.take_input = false;
+ }
+ if (keyboard_is_key_pressed(KEY_ENTER)) {
+ _global_keyboard.take_input = false;
+ }
+
+ bool is_editing = _global_keyboard.take_input;
+
+ // Limit input length
+ if (is_editing) {
+ if (_global_keyboard.input_text_len > MAX_INPUT_LENGTH_FOR_EMPLOYEE_SELECTOR)
+ {
+ _global_keyboard.input_text_len = MAX_INPUT_LENGTH_FOR_EMPLOYEE_SELECTOR;
+ _global_keyboard.cursor = MAX_INPUT_LENGTH_FOR_EMPLOYEE_SELECTOR;
+ _global_keyboard.input_text[MAX_INPUT_LENGTH_FOR_EMPLOYEE_SELECTOR] = 0;
+ }
+
+ {
+ // Activation underline.
+ s32 linew = tb_width*0.95f;
+ s32 offsetw = (tb_width - linew)/2;
+ renderer->render_rectangle(tb_x+offsetw, tb_y+tb_height - 6, linew, 2, COLOR_SELECTOR_UNDERLINE);
+ }
+ }
+
+ font* fnt_big = fnt_rd32;
+ s32 textx = tb_x+(10*scale);
+ s32 texty = tb_y+(tb_height/2)-(fnt_big->px_h/2);
+
+ u32 text_id = string_to_u32(_global_keyboard.input_text);
+ employee* emp = is_editing ? get_global_employee_by_id(_active_world, text_id) : current_val;
+
+ if (is_editing || emp || _global_keyboard.input_text_len) // Editing, employee is selected, or player is typing.
+ {
+ s32 cursorh = tb_height*0.6f;
+
+ // Either display keyboard input or selected employee.
+ char idbuf[20];
+ if (is_editing || !emp) sprintf(idbuf, "ID: %s", _global_keyboard.input_text);
+ else if (emp) sprintf(idbuf, "ID: %d", emp->id);
+
+ s32 textw = renderer->render_text(fnt_big, textx, texty, idbuf, text_color);
+ if (is_editing) renderer->render_rectangle(textx+textw, tb_y+(tb_height/2)-(cursorh/2), 3, cursorh, text_color);
+ result = emp;
+ }
+ else if (!is_editing && !emp) { // Not editing and no employee selected.
+ renderer->render_text(fnt_big, textx, texty, "Search by ID..", text_color);
+ }
+
+ s32 status_s = tb_height/2;
+ s32 status_x = tb_x + tb_width - (status_s/4*3);
+ s32 status_y = tb_y - (status_s/4);
+ button_draw_background(scale,status_x,status_y,status_s,status_s,c_white,c_btn);
+ s32 status_icon_s = status_s*0.5f;
+ s32 status_icon_offset = (status_s - status_icon_s)/2;
+ renderer->render_image_tint(result ? img_checkmark : img_questionmark,
+ status_x+status_icon_offset, status_y+status_icon_offset, status_icon_s, status_icon_s, result ? c_correct : c_wrong);
+ }
+
+ // Draw employee data
+ if (result) {
+ s32 por_x = tb_x + tb_width + tb_pad;
+ draw_employee_portrait(result, por_x, tb_y, tb_height, tb_height);
+
+ s32 text_x = por_x + tb_height + tb_pad;
+ font* fnt = fnt_rd24;
+ font* fnt_s = fnt_rd20;
+ renderer->render_text(fnt,text_x,tb_y,result->name,text_color);
+
+ tb_y += fnt->px_h + (5*scale);
+
+ {
+ world_location* origin = get_world_location_by_id(_active_world, result->original_location_id);
+ char buf[100];
+ sprintf(buf, "Works at %s", origin->name);
+ renderer->render_text(fnt_s,text_x,tb_y,buf,text_color);
+ tb_y += fnt->px_h + (12*scale);
+ }
+
+ float hours_for_this_job = (offer->offer.duration_sec_min * get_shiptime_factor(result)) / 3600.0f;
+ float total_hours = get_worked_hours_per_week_for_employee(_active_world, result, (_active_schedule_state == RESCHEDULING_JOB ? _active_selected_scheduled_job : 0));
+
+ // If being scheduled, also add timeslots that are currently being scheduled.
+ if (enabled) {
+ for (s32 i = 0; i < MAX_SHIPDAYS; i++) {
+ scheduled_job_time slot = offer->timeslots[i];
+ if (slot.assignee == result) {
+ total_hours += hours_for_this_job;
+ if (!slot.stay_at_destination) total_hours += hours_for_this_job;
+ }
+ }
+ }
+ bool overworked = (total_hours > MAX_WORKED_HOURS_WEEKLY);
+ total_hours = ceil(total_hours);
+
+ color c = COLOR_TEXT;
+ if (overworked) c = COLOR_TEXT_NEGATIVE;
+ char txt_status[50];
+ sprintf(txt_status, "Currently scheduled for %.0fh/week", total_hours);
+ renderer->render_text(fnt_s,text_x,tb_y,txt_status,c);
+ }
+
+ return result;
+ #undef ANIMATION_OFFSET
+}
+
+world_location* location_selector_render(platform_window* window, float scale, bool enabled, world_location* current_val, s32 x, s32 y, s32 w, s32 h)
+{
+ world_location* result = current_val;
+ button_render(scale, BUTTON_STATIC, 0, x,y,w,h);
+
+ s32 tb_width = w;
+ s32 tb_height = h;
+ s32 tb_y = y;
+ s32 tb_x = x;
+
+ {
+ // Start taking input on click.
+ if (button_draw_background(scale, tb_x,tb_y,tb_width,tb_height, COLOR_TEXTBOX_TINT, COLOR_TEXTBOX_FILL) && enabled) {
+ platform_set_cursor(window, CURSOR_POINTER);
+ if (is_left_clicked()) {
+ _global_keyboard.take_input = true;
+ _global_keyboard.input_mode = INPUT_FULL;
+ }
+ }
+ // Clear on outside click or enter.
+ else if (is_left_clicked_peak()) {
+ _global_keyboard.take_input = false;
+ }
+ if (keyboard_is_key_pressed(KEY_ENTER)) {
+ _global_keyboard.take_input = false;
+ }
+
+ bool is_editing = _global_keyboard.take_input;
+
+ // Limit input length
+ if (is_editing) {
+ if (_global_keyboard.input_text_len > MAX_WORLD_LOCATION_NAME_LENGTH)
+ {
+ _global_keyboard.input_text_len = MAX_WORLD_LOCATION_NAME_LENGTH;
+ _global_keyboard.cursor = MAX_WORLD_LOCATION_NAME_LENGTH;
+ _global_keyboard.input_text[MAX_WORLD_LOCATION_NAME_LENGTH] = 0;
+ }
+ }
+
+ font* fnt_big = fnt_rd32;
+ s32 textx = tb_x+(10*scale);
+ s32 texty = tb_y+(tb_height/2)-(fnt_big->px_h/2);
+
+ world_location* emp = is_editing ? get_world_location_by_name(_active_world, _global_keyboard.input_text) : current_val;
+
+ if (is_editing || emp || _global_keyboard.input_text_len) // Editing, employee is selected, or player is typing.
+ {
+ s32 cursorh = tb_height*0.6f;
+
+ // Either display keyboard input or selected employee.
+ char idbuf[50];
+ if (is_editing || !emp) sprintf(idbuf, "Name: %s", _global_keyboard.input_text);
+ else if (emp) sprintf(idbuf, "Name: %s", emp->name);
+
+ s32 textw = renderer->render_text(fnt_big, textx, texty, idbuf, COLOR_TEXT);
+ if (is_editing) renderer->render_rectangle(textx+textw, tb_y+(tb_height/2)-(cursorh/2), 3, cursorh, COLOR_TEXT);
+ result = emp;
+ }
+ else if (!is_editing && !emp) { // Not editing and no employee selected.
+ renderer->render_text(fnt_big, textx, texty, "Filter location..", COLOR_TEXT);
+ }
+
+ s32 status_s = tb_height/2;
+ s32 status_x = tb_x + tb_width - (status_s/4*3);
+ s32 status_y = tb_y - (status_s/4);
+ button_draw_background(scale,status_x,status_y,status_s,status_s,COLOR_WHITE,COLOR_BUTTON);
+ s32 status_icon_s = status_s*0.5f;
+ s32 status_icon_offset = (status_s - status_icon_s)/2;
+ renderer->render_image_tint(result ? img_checkmark : img_questionmark,
+ status_x+status_icon_offset, status_y+status_icon_offset, status_icon_s, status_icon_s, result ? COLOR_CORRECT : COLOR_WRONG);
+ }
+
+ return result;
+} \ No newline at end of file
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