From 6f7374c2fa58c8692b51018864b802e6b876d305 Mon Sep 17 00:00:00 2001 From: Aldrik Ramaekers Date: Sat, 23 Nov 2024 21:52:24 +0100 Subject: A new start --- src/data.c | 113 ++ src/game.c | 134 +++ src/include/data.h | 100 ++ src/include/game.h | 31 + src/include/scenery.h | 29 + src/include/scenes/error_scene.h | 15 + src/include/scenes/loading_scene.h | 15 + src/include/scenes/loading_world_scene.h | 16 + src/include/scenes/menu_scene.h | 15 + src/include/scenes/place_detail.h | 16 + src/include/scenes/save_state_select.h | 15 + src/include/scenes/settings_scene.h | 15 + src/include/scenes/world_map.h | 16 + src/include/settings.h | 28 + src/include/tooltip.h | 18 + src/include/ui/animation.h | 24 + src/include/ui/button.h | 20 + src/include/ui/colors.h | 62 ++ src/include/ui/panel.h | 15 + src/include/ui/portrait.h | 28 + src/include/ui/selectors.h | 13 + src/include/world.h | 383 +++++++ src/main.c | 182 +++ src/music.c | 22 + src/scenery.c | 53 + src/scenes/error_scene.c | 74 ++ src/scenes/loading_scene.c | 85 ++ src/scenes/loading_world_scene.c | 116 ++ src/scenes/menu_scene.c | 93 ++ src/scenes/place_detail.c | 1771 ++++++++++++++++++++++++++++++ src/scenes/save_state_select.c | 105 ++ src/scenes/settings_scene.c | 152 +++ src/scenes/world_map.c | 1094 ++++++++++++++++++ src/tooltip.c | 22 + src/ui/animation.c | 18 + src/ui/button.c | 101 ++ src/ui/panel.c | 29 + src/ui/portrait.c | 21 + src/ui/selectors.c | 212 ++++ src/world.c | 1321 ++++++++++++++++++++++ 40 files changed, 6592 insertions(+) create mode 100644 src/data.c create mode 100644 src/game.c create mode 100644 src/include/data.h create mode 100644 src/include/game.h create mode 100644 src/include/scenery.h create mode 100644 src/include/scenes/error_scene.h create mode 100644 src/include/scenes/loading_scene.h create mode 100644 src/include/scenes/loading_world_scene.h create mode 100644 src/include/scenes/menu_scene.h create mode 100644 src/include/scenes/place_detail.h create mode 100644 src/include/scenes/save_state_select.h create mode 100644 src/include/scenes/settings_scene.h create mode 100644 src/include/scenes/world_map.h create mode 100644 src/include/settings.h create mode 100644 src/include/tooltip.h create mode 100644 src/include/ui/animation.h create mode 100644 src/include/ui/button.h create mode 100644 src/include/ui/colors.h create mode 100644 src/include/ui/panel.h create mode 100644 src/include/ui/portrait.h create mode 100644 src/include/ui/selectors.h create mode 100644 src/include/world.h create mode 100644 src/main.c create mode 100644 src/music.c create mode 100644 src/scenery.c create mode 100644 src/scenes/error_scene.c create mode 100644 src/scenes/loading_scene.c create mode 100644 src/scenes/loading_world_scene.c create mode 100644 src/scenes/menu_scene.c create mode 100644 src/scenes/place_detail.c create mode 100644 src/scenes/save_state_select.c create mode 100644 src/scenes/settings_scene.c create mode 100644 src/scenes/world_map.c create mode 100644 src/tooltip.c create mode 100644 src/ui/animation.c create mode 100644 src/ui/button.c create mode 100644 src/ui/panel.c create mode 100644 src/ui/portrait.c create mode 100644 src/ui/selectors.c create mode 100644 src/world.c (limited to 'src') 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(¤t_source->connections, rand_conn); + + bool already_in_path = false; + for (s32 c = 0; c < buf->length; c++) { + if (*(world_location**)array_at(buf, c) == connection) already_in_path = true; + } + + if (already_in_path && attempt < current_source->connections.length) { + goto try_again; + } + best_match = already_in_path ? 0 : connection; + + #if 0 + double best_match_distance = 0; + + for (s32 i = 0; i < current_source->connections.length; i++) { + world_location* connection = *(world_location**)array_at(¤t_source->connections, i); + if (connection == source) continue; + + bool already_in_path = false; + for (s32 c = 0; c < buf->length; c++) { + if (*(world_location**)array_at(buf, c) == connection) already_in_path = true; + } + + if (!already_in_path) { + double total_distance = fabs(connection->latitude - current_source->latitude) + fabs(connection->longitude - current_source->longitude); + + if (!best_match || best_match_distance < total_distance) { + best_match_distance = total_distance; + best_match = connection; + } + } + } + #endif + + if (!best_match) break; + array_push(buf, &best_match); + current_source = best_match; + } +} + +static void world_assign_new_job_offers(world* world) +{ + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + if (!location->is_owned) continue; + if (location->job_offers.length >= MAX_JOBOFFER_COUNT) continue; + + s32 company_id = get_random_number(0, world->companies.length); + company* company = array_at(&world->companies, company_id); + s32 product_id = get_random_number(0, company->products.length); + product* product = array_at(&company->products, product_id); + + job_offer new_offer = (job_offer){world->next_id++, world->simulation_time+DAYS(10), company, product, 0, {DAY_INVALID,DAY_INVALID,DAY_INVALID,DAY_INVALID}, 0}; + new_offer.connections = array_create(sizeof(world_location*)); + + s32 amount_of_shipdays = get_random_number(1, MAX_SHIPDAYS+1); + for (s32 s = 0; s < amount_of_shipdays; s++) { + s32 random_day = get_random_number(0, NUM_DAYS); + if (!job_offer_has_ship_day(&new_offer, random_day)) { + new_offer.shipdays[new_offer.shipday_count++] = random_day; + } + else { + s--; + } + } + + s32 amount_of_connections = get_random_number(1, 5) + 1; // +1 because we add original location to connection list. + + array_push(&new_offer.connections, &location); + world_find_location_deep(amount_of_connections, location, &new_offer.connections); + + float total_dist = 0.0; + for (s32 d = 0; d < new_offer.connections.length-1; d++) + { + world_location* source = *(world_location**)array_at(&new_offer.connections, d); + world_location* dest = *(world_location**)array_at(&new_offer.connections, d+1); + total_dist += distance_between_location(source, dest); + } + new_offer.total_distance = total_dist; + new_offer.reward = (u32)((JOB_OFFER_REWARD_PER_CONNECTION * location->reliability) * amount_of_connections-1); // -1 because source is is connection list. + + // lest assume most experienced drivers drive at 95km/h + double min_duration_hours = (new_offer.total_distance/95.0); + double max_diration_hours = min_duration_hours * SHIPTIME_DURATION_MULTIPLIER_MIN; + + new_offer.duration_sec_min = (time_t)(min_duration_hours * 60 * 60); + new_offer.duration_sec_max = (time_t)(max_diration_hours * 60 * 60); + + // printf("Distance: %.0f, duration between %.2f and %.2f hours.", total_dist, min_duration_hours, max_diration_hours); + + array_push(&location->job_offers, &new_offer); + } +} + +static void world_assign_resumes_to_locations(world* world) +{ + // Expire old ones. + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + + for (s32 r = 0; r < location->resumes.length; r++) + { + resume* resume = array_at(&location->resumes, r); + + // It is assumed a simulation day is longer than RESUME_FADEOUT_MS + // else the animation will be cut short. + if (resume->animation.started) { + array_remove_at(&location->resumes, r); + r--; + } + + if (resume->expire_date <= world->simulation_time) { + resume->animation.started = true; + resume->hired = false; + } + } + } + + // Assign new ones. + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + if (!location->is_owned) continue; + + // 0.5 reliability = 0% + // 1.0 reliability = 10% + // 1.5 reliability = 20% + // etc. + // % = change of a new resume being submitted per day. + #if 1 + if (get_random_number(0, 10) >= ceil(location->reliability)) continue; + #endif + resume new_resume; + new_resume.animation = animation_create(RESUME_FADEOUT_MS); + new_resume.expire_date = world->simulation_time+DAYS(10); + new_resume.employee = create_employee(world, location); + + array_push(&location->resumes, &new_resume); + } +} + +static double distance_between_location(world_location* location1, world_location* location2) +{ + double lat1 = location1->latitude; + double lat2 = location2->latitude; + double lon1 = location1->longitude; + double lon2 = location2->longitude; + + double rlat1 = M_PI*lat1/180; + double rlat2 = M_PI*lat2/180; + double theta = lon1 - lon2; + double rtheta =M_PI*theta/180; + double dist = + sin(rlat1)*sin(rlat2) +cos(rlat1)* + cos(rlat2)*cos(rtheta); + dist = acos(dist); + dist = dist*180/M_PI; + dist = dist*60*1.1515; + return dist*1.609344; // Miles to km +} + +static float get_employee_experience_factor(employee* emp) +{ + float experience_factor = emp->experience/MAX_EFFECTIVE_EXPERIENCE; + if (experience_factor > 1.0f) experience_factor = 1.0f; + return experience_factor; +} + +static float get_shiptime_factor(employee* emp) +{ + float experience_factor = get_employee_experience_factor(emp); + float shiptime_factor = (SHIPTIME_DURATION_MULTIPLIER_MIN - (SHIPTIME_DURATION_MULTIPLIER_MIN - SHIPTIME_DURATION_MULTIPLIER_MAX) * experience_factor); + return shiptime_factor; +} + +static void world_start_scheduled_job(world* world, scheduled_job* scheduled_job, scheduled_job_time scheduled_time) +{ + if (!scheduled_time.assignee) { + scheduled_job->trust -= MISSED_DELIVERY_TRUST_PENALTY; + char error_msg[MAX_EVENT_MESSAGE_LENGTH]; + snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "A shipment has been missed because there is no assignee for the timeslot."); + world_report_event_ex(world, error_msg, EVENT_TYPE_MISSED_SHIPMENT_NO_ASSIGNEE, scheduled_job, scheduled_time); + return; + } + + if (!scheduled_time.assignee->assigned_truck) { + scheduled_job->trust -= MISSED_DELIVERY_TRUST_PENALTY; + char error_msg[MAX_EVENT_MESSAGE_LENGTH]; + snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s missed a shipment because they do not have a truck.", scheduled_time.assignee->name); + world_report_event(world, error_msg, EVENT_TYPE_MISSED_SHIPMENT_NO_TRUCK, scheduled_time.assignee); + return; + } + + world_location* orig = *(world_location**)array_at(&scheduled_job->offer.connections, 0); + if (scheduled_time.assignee->current_location_id != orig->id) { + scheduled_job->trust -= MISSED_DELIVERY_TRUST_PENALTY; + char error_msg[MAX_EVENT_MESSAGE_LENGTH]; + snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s missed a shipment because they are not at the right location.", scheduled_time.assignee->name); + world_report_event_ex(world, error_msg, EVENT_TYPE_MISSED_SHIPMENT_NOT_AT_LOCATION, scheduled_job, scheduled_time); + return; + } + + // Remove employee from employee list when current location is external. + if (scheduled_time.assignee->current_location_id != scheduled_time.assignee->original_location_id) { + world_location* current_loc = get_world_location_by_id(world, scheduled_time.assignee->current_location_id); + log_assert(current_loc, "Current location cannot be 0"); + employee* emp = get_employee_by_id(current_loc, scheduled_time.assignee->id); + if (emp) { + array_remove_by(¤t_loc->employees, &emp); + } + } + + scheduled_time.assignee->current_location_id = INVALID_ID; + scheduled_time.assignee->active_job_id = scheduled_job->offer.id; + + active_job new_job; + new_job.stay_at_destination = scheduled_time.stay_at_destination; + new_job.day = scheduled_time.day; + new_job.timeslot = scheduled_time.timeslot; + new_job.offer = scheduled_job->offer; + new_job.assignee = *scheduled_time.assignee; + new_job.assigned_truck = *scheduled_time.assignee->assigned_truck; + + time_t leave_time = world->simulation_time-(world->simulation_time%(15*60)); + new_job.left_at = leave_time; + + float shiptime_factor = get_shiptime_factor(&new_job.assignee); + new_job.duration_sec = new_job.offer.duration_sec_min * shiptime_factor; + + new_job.done_at = new_job.left_at + new_job.duration_sec; + new_job.reversed = false; + + float gasprice = ((new_job.offer.total_distance/100.0f) * new_job.assigned_truck.fuelusage) * DIESEL_PRICE_PER_LITER; + ADD_EXPENSE(world, orig, expenses_from_fuel, gasprice); + + array_push(&world->active_jobs, &new_job); + + audio_set_sound_volume(snd_accelerate, 0.08f); + audio_play_sound(snd_accelerate, AUDIO_CHANNEL_SFX_3); +} + +static void world_start_scheduled_jobs(world* world, s32 day, s32 timeslot) +{ + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + if (!location->is_owned) continue; + + for (s32 r = 0; r < location->schedule.jobs.length; r++) + { + scheduled_job* scheduled_job = array_at(&location->schedule.jobs, r); + + for (s32 t = 0; t < scheduled_job->offer.shipday_count; t++) { + scheduled_job_time scheduled_time = scheduled_job->timeslots[t]; + + if (scheduled_time.day == day && scheduled_time.timeslot == timeslot) { + world_start_scheduled_job(world, scheduled_job, scheduled_time); + } + } + } + } +} + +static employee* get_global_employee_by_id(world* world, u32 id) +{ + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + if (!location->is_owned) continue; + + for (s32 i = 0; i < location->employees.length; i++) + { + employee* em = *(employee**)array_at(&location->employees, i); + if (em->id == id) return em; + } + } + return 0; +} + +static employee* get_employee_by_id(world_location* location, u32 id) +{ + for (s32 i = 0; i < location->employees.length; i++) + { + employee* em = *(employee**)array_at(&location->employees, i); + if (em->id == id) return em; + } + return 0; +} + +static job_offer* get_job_offer_by_id(world_location* location, u32 id) +{ + for (s32 i = 0; i < location->job_offers.length; i++) + { + job_offer* job = array_at(&location->job_offers, i); + if (job->id == id) return job; + } + return 0; +} + +static active_job* get_active_job_by_id(world* world, u32 id) +{ + for (s32 i = 0; i < world->active_jobs.length; i++) + { + active_job* job = array_at(&world->active_jobs, i); + if (job->offer.id == id) return job; + } + return 0; +} + +static active_job* get_active_job_by_ref(world* world, active_job_ref ref) +{ + for (s32 i = 0; i < world->active_jobs.length; i++) + { + active_job* job = array_at(&world->active_jobs, i); + if (job->offer.id == ref.offerid && job->day == ref.day && job->timeslot == ref.timeslot) return job; + } + return 0; +} + +static scheduled_job* get_scheduled_job_by_id(world_location* location, u32 id) +{ + for (s32 i = 0; i < location->schedule.jobs.length; i++) + { + scheduled_job* scheduled_job = array_at(&location->schedule.jobs, i); + if (scheduled_job->offer.id == id) return scheduled_job; + } + return 0; +} + +static void world_update_active_jobs(world* world) +{ + for (s32 i = 0; i < world->active_jobs.length; i++) + { + active_job* job = array_at(&world->active_jobs, i); + + if (job->done_at <= world->simulation_time) { + job_endpoints endpoints = job_offer_get_endpoints(&job->offer); + scheduled_job* sc_job = get_scheduled_job_by_id(endpoints.source, job->offer.id); // Get scheduled job from source location + + if (job->reversed || job->stay_at_destination) { // Driver has returned from the round-trip or will stay at location. + world_location* orig_location = get_world_location_by_id(world, job->assignee.original_location_id); + employee* e = get_employee_by_id(orig_location, job->assignee.id); + e->current_location_id = job->stay_at_destination ? endpoints.dest->id : endpoints.source->id; + e->active_job_id = INVALID_ID; + + // Employee is scheduled to stay at location + if (job->stay_at_destination) { + array_push(&endpoints.dest->employees, &e); + } + // Employee started the job from an external location and are returning + if (job->reversed && orig_location->id != e->current_location_id) { + array_push(&endpoints.source->employees, &e); + } + + // Remove job. + array_remove_at(&world->active_jobs, i); + i--; + } + else { // Job is done, return to original location if not staying. + if (sc_job) sc_job->trust += (0.1f / sc_job->trust); // If job is still available, update trust. + ADD_INCOME(world, endpoints.source, income_from_trips, job->offer.reward); + + float gasprice = ((job->offer.total_distance/100.0f) * job->assigned_truck.fuelusage) * DIESEL_PRICE_PER_LITER; + ADD_EXPENSE(world, endpoints.source, expenses_from_fuel, gasprice); + + job->left_at = job->done_at; + job->done_at = job->left_at + job->duration_sec; + job->reversed = true; + } + } + } +} + +static float get_worked_hours_per_week_for_employee(world* world, employee* emp, scheduled_job* excluding) { + float total = 0; + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + if (!location->is_owned) continue; + + for (s32 f = 0; f < location->schedule.jobs.length; f++) { + scheduled_job* job = array_at(&location->schedule.jobs, f); + if (job == excluding) continue; + + for (s32 s = 0; s < MAX_SHIPDAYS; s++) { + scheduled_job_time slot = job->timeslots[s]; + if (slot.assignee == emp) { + float hworked = job->offer.duration_sec_min * get_shiptime_factor(emp); + if (!slot.stay_at_destination) hworked *= 2; + total += hworked; + } + } + } + } + return total / 3600.0f; +} + +// Run once per day. +static void world_update_employee_happiness(world* world, employee* emp) { + // Calculate overworking + float max_weekly_hours = MAX_WORKED_HOURS_WEEKLY; + float hours_worked = get_worked_hours_per_week_for_employee(world, emp, 0); + float hours_overworked = hours_worked - max_weekly_hours; + if (hours_overworked > 0) emp->happiness = 1.0f - ((hours_overworked/3.0f)*0.2f); + else emp->happiness = 1.0f; + + // Calculate underpay + float expected_pay = BASE_PAY + (emp->experience * RAISE_PER_YEAR); + if (expected_pay > MAX_PAY) expected_pay = MAX_PAY; + float underpay = expected_pay - emp->salary; + + emp->happiness -= (underpay/100.0f)*0.2f; // 1 Star per 100 euro underpay/overpay + if (emp->happiness < 0.0f) emp->happiness = 0.0f; + if (emp->happiness > 1.0f) emp->happiness = 1.0f; + + if (emp->happiness < MINIMUM_EMPLOYEE_HAPPINESS) { + emp->days_below_happiness_treshold++; + } + else { + emp->days_below_happiness_treshold--; + } + if (emp->days_below_happiness_treshold < 0) emp->days_below_happiness_treshold = 0; + + if (emp->days_below_happiness_treshold >= EMPLOYEE_MAX_UNHAPPY_DAYS_BEFORE_QUITTING) { + + char error_msg[MAX_EVENT_MESSAGE_LENGTH]; + snprintf(error_msg, MAX_EVENT_MESSAGE_LENGTH, "%s quit their job because they were unhappy for too long.", emp->name); + world_report_event(world, error_msg, EVENT_TYPE_EMPLOYEE_QUIT, get_world_location_by_id(world, emp->original_location_id)); + + end_contract_with_employee(world, emp); + } +} + +// Pay investments daily. Run once per day. +static void world_pay_investments(world* world) +{ + ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.safety/28.0f); + ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.marketing/28.0f); + ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.human_resources/28.0f); + ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.training/28.0f); + ADD_EXPENSE(world, 0, expenses_from_utility, world->investments.legal/28.0f); +} + +// Payout salaries daily. Run once per day. +static void world_payout_salaries(world* world) +{ + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + if (!location->is_owned) continue; + + for (s32 r = 0; r < location->employees.length; r++) + { + employee* emp = *(employee**)array_at(&location->employees, r); + + // Make sure employees are not paid twice when at external location. + if (emp->current_location_id == location->id) { + world_update_employee_happiness(world, emp); + ADD_EXPENSE(world, location, expenses_from_employees, (emp->salary/28.0f)); + } + } + } +} + +static void enable_insights_for_current_month(world* world) +{ + if (isnan(EXPENSES.total_income)) EXPENSES.total_income = 0; // Validate insights for current month. + + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + if (!location->is_owned) continue; + + money_data_collection* collection = get_current_insights_data_for_location(world, location); + collection->months[world->current_time.tm_mon].total_income = 0; + } +} + +static void world_start_random_events(world* world) +{ + world->days_since_last_random_event++; + + // Minor events + { + #define MIN_DELAY_BETWEEN_EVENTS (60) + float change_of_random_event = 0.0f; + if (world->days_since_last_random_event > MIN_DELAY_BETWEEN_EVENTS) { + change_of_random_event = ((world->days_since_last_random_event - MIN_DELAY_BETWEEN_EVENTS)*0.5f)/100.0f; + if (change_of_random_event > 1.0f) change_of_random_event = 1.0f; + + bool run_event = change_of_random_event >= (get_random_number(0, 100)/100.0f); + + (void)run_event; // TODO start event. + } + } +} + +static void end_contract_with_employee(world* world, employee* emp) +{ + if (emp->assigned_truck) emp->assigned_truck->assigned_employee = 0; + + // Remove assigned timeslots. + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + if (!location->is_owned) continue; + + for (s32 r = 0; r < location->schedule.jobs.length; r++) + { + scheduled_job* scheduled_job = array_at(&location->schedule.jobs, r); + + for (s32 t = 0; t < scheduled_job->offer.shipday_count; t++) { + scheduled_job_time scheduled_time = scheduled_job->timeslots[t]; + + if (scheduled_time.assignee == emp) { + scheduled_job->timeslots[t].assignee = 0; + } + } + } + } + + // Remove active job + active_job* curr_job = get_active_job_by_id(world, emp->active_job_id); + array_remove_by(&world->active_jobs, curr_job); + + // Remove employee from original location + world_location* orig_loc = get_world_location_by_id(world, emp->original_location_id); + array_remove_by(&orig_loc->employees, &emp); + + // Remove employee from current location + world_location* curr_loc = get_world_location_by_id(world, emp->current_location_id); + if (curr_loc) array_remove_by(&curr_loc->employees, &emp); +} + +static void world_run_simulation_tick(world* world) +{ + s32 elapsed_sec = MINUTES(world->simulation_speed); + s64 prev_stamp = world->simulation_time; + world->simulation_time += elapsed_sec; + + struct tm prev_time_buf; + prev_time_buf = *gmtime(&prev_stamp); + struct tm* prev_time = &prev_time_buf; + + struct tm curr_time_buf; + curr_time_buf = *gmtime(&world->simulation_time); + struct tm* curr_time = &curr_time_buf; + world->current_time = curr_time_buf; + + if (prev_time->tm_wday != curr_time->tm_wday) { // Run once per day + world_remove_expired_job_offers(world); + world_assign_resumes_to_locations(world); + world_start_random_events(world); + + if (curr_time->tm_mday <= 28) { + world_payout_salaries(world); // Pay salary first 28 days of month. + world_pay_investments(world); + } + } + + if (prev_time->tm_wday == SUNDAY && curr_time->tm_wday == MONDAY) { // Run once per week + world_assign_new_job_offers(world); + } + + if (curr_time->tm_hour >= WORK_HOUR_START && curr_time->tm_hour < WORK_HOUR_END) { // Run every 15min + s32 hour_part = 60 / TIME_SLOTS_PER_HOUR; + s32 prev_part = prev_time->tm_min / hour_part; + s32 curr_part = curr_time->tm_min / hour_part; + if (prev_part != curr_part) { + world_start_scheduled_jobs(world, curr_time->tm_wday, ((curr_time->tm_hour-WORK_HOUR_START)*TIME_SLOTS_PER_HOUR) + curr_part); + } + } + + if (curr_time->tm_mday < prev_time->tm_mday) { // Run once per month + enable_insights_for_current_month(world); + } + + world_update_active_jobs(world); +} + +void world_update(platform_window* window, world* world) +{ + world_run_simulation_tick(world); +} + +static vec2f get_world_location_for_job(platform_window* window, world* world, active_job* job) +{ + time_t total_time = job->done_at - job->left_at; + time_t elapsed_time = world->simulation_time - job->left_at; + float complete_percentage = elapsed_time/(float)total_time; + if (job->reversed) complete_percentage = 1.0f - complete_percentage; + if (complete_percentage > 1.0f) complete_percentage = 1.0f; + double current_km = job->offer.total_distance*complete_percentage; + double calculating_km = 0.0; + for (s32 d = 0; d < job->offer.connections.length-1; d++) + { + world_location* source = *(world_location**)array_at(&job->offer.connections, d); + world_location* dest = *(world_location**)array_at(&job->offer.connections, d+1); + double dist_between_points = distance_between_location(source, dest); + double next_landmark = calculating_km + dist_between_points; + double dist_from_source = current_km - calculating_km; + + if (calculating_km < current_km && next_landmark >= current_km) { + float progress_between_points = dist_from_source / dist_between_points; + double lon = source->longitude + (dest->longitude - source->longitude) * progress_between_points; + double lat = source->latitude + (dest->latitude - source->latitude) * progress_between_points; + + vec2f map_pos = coords_to_px(window, lon, lat); + return map_pos; + } + else { + calculating_km = next_landmark; + } + } + return (vec2f){0.0f, 0.0f}; +} + +world_update_result world_render(platform_window* window, world* world) +{ + world_update_result result = {0,0}; + + renderer->set_render_depth(3); + // Draw locations + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* location = array_at(&world->locations, i); + vec2f map_pos = coords_to_px(window, location->longitude, location->latitude); + map_pos.x *= zoom; + map_pos.y *= zoom; + map_pos.x += camera_x; + map_pos.y += camera_y; + + location->map_position_x = map_pos.x; + location->map_position_y = map_pos.y; + + s32 circle_x = location->map_position_x - dotsize/2; + s32 circle_y = location->map_position_y - dotsize/2; + bool hovered = mouse_interacts(circle_x, circle_y, dotsize, dotsize); + location->is_hovered = hovered; + if (hovered) { + platform_set_cursor(window, CURSOR_POINTER); + if (is_left_clicked()) { + result.clicked_location = location; + audio_play_sound(snd_click, AUDIO_CHANNEL_SFX_1); + } + } + + color tint = location->is_owned ? COLOR_LOCATION_DOT_OWNED : COLOR_LOCATION_DOT_UNOWNED; + if (location->is_hovered) { + tint = COLOR_DOT_HOVERED; + } + renderer->render_image_tint(img_locationdot, circle_x, circle_y, dotsize, dotsize, tint); + } + + renderer->set_render_depth(2); + // Draw active jobs + for (s32 i = 0; i < world->active_jobs.length; i++) + { + active_job* job = array_at(&world->active_jobs, i); + vec2f pos = get_world_location_for_job(window, world, job); + job->px_pos = pos; + + s32 circle_x = job->px_pos.x - dotsize/2; + s32 circle_y = job->px_pos.y - dotsize/2; + bool hovered = mouse_interacts(circle_x, circle_y, dotsize, dotsize); + job->is_hovered = hovered; + if (hovered) { + platform_set_cursor(window, CURSOR_POINTER); + if (is_left_clicked()) result.clicked_job = job; + } + + renderer->render_image_tint(img_locationdot, job->px_pos.x-(dotsize/2),job->px_pos.y-(dotsize/2), dotsize,dotsize, job->is_hovered ? COLOR_DOT_HOVERED : rgb(255,0,0)); + } + + renderer->set_render_depth(1); + dotsize = 5 * scale * zoom; + // Draw connections + for (s32 i = 0; i < world->locations.length; i++) + { + world_location* source = array_at(&world->locations, i); + + for (s32 d = 0; d < source->connections.length; d++) + { + world_location* destination = *(world_location**)array_at(&source->connections, d); + if (destination == source) continue; + + renderer->render_line(source->map_position_x, source->map_position_y, destination->map_position_x, destination->map_position_y, 1, rgb(255,0,0)); + } + } + + renderer->set_render_depth(1); + update_render_scenery(world); + + return result; +} \ No newline at end of file -- cgit v1.2.3-70-g09d2